[
  {
    "path": ".gitignore",
    "content": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nenv/\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\n*.egg-info/\n.installed.cfg\n*.egg\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n.hypothesis/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# pyenv\n.python-version\n\n# celery beat schedule file\ncelerybeat-schedule\n\n# SageMath parsed files\n*.sage.py\n\n# dotenv\n.env\n\n# virtualenv\n.venv\nvenv/\nENV/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n.DS_Store\n\n# gitbook\n_book\n\n# node.js\nnode_modules\n\n# windows\nThumbs.db\n\n# word\n~$*.docx\n~$*.doc\n"
  },
  {
    "path": ".nojekyll",
    "content": ""
  },
  {
    "path": "404.html",
    "content": "---\npermalink: /404.html\n---\n<script>window.location.href = '/';</script>\n"
  },
  {
    "path": "CNAME",
    "content": "ccpp.apachecn.org"
  },
  {
    "path": "Dockerfile",
    "content": "FROM httpd:2.4\nCOPY ./ /usr/local/apache2/htdocs/"
  },
  {
    "path": "LICENSE",
    "content": "Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Public License (CC BY-NC-SA 4.0)\n\nCopyright © 2020 ApacheCN(apachecn@163.com)\n\nBy exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Public License (\"Public License\"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions.\n\nSection 1 – Definitions.\n\na.  Adapted Material means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image.\nb.  Adapter's License means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License.\nc.  BY-NC-SA Compatible License means a license listed at creativecommons.org/compatiblelicenses, approved by Creative Commons as essentially the equivalent of this Public License.\nd.  Copyright and Similar Rights means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights.\ne.  Effective Technological Measures means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements.\nf.  Exceptions and Limitations means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material.\ng.  License Elements means the license attributes listed in the name of a Creative Commons Public License. The License Elements of this Public License are Attribution, NonCommercial, and ShareAlike.\nh.  Licensed Material means the artistic or literary work, database, or other material to which the Licensor applied this Public License.\ni.  Licensed Rights means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license.\nj.  Licensor means the individual(s) or entity(ies) granting rights under this Public License.\nk.  NonCommercial means not primarily intended for or directed towards commercial advantage or monetary compensation. For purposes of this Public License, the exchange of the Licensed Material for other material subject to Copyright and Similar Rights by digital file-sharing or similar means is NonCommercial provided there is no payment of monetary compensation in connection with the exchange.\nl.  Share means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them.\nm.  Sui Generis Database Rights means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world.\nn.  You means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning.\n\nSection 2 – Scope.\n\na.  License grant.\n    1.  Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to:\n        A.  reproduce and Share the Licensed Material, in whole or in part, for NonCommercial purposes only; and\n        B.  produce, reproduce, and Share Adapted Material for NonCommercial purposes only.\n    2.  Exceptions and Limitations. For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions.\n    3.  Term. The term of this Public License is specified in Section 6(a).\n    4.  Media and formats; technical modifications allowed. The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces Adapted Material.\n    5.  Downstream recipients.\n        A.  Offer from the Licensor – Licensed Material. Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License.\n        B.  Additional offer from the Licensor – Adapted Material. Every recipient of Adapted Material from You automatically receives an offer from the Licensor to exercise the Licensed Rights in the Adapted Material under the conditions of the Adapter’s License You apply.\n        C.  No downstream restrictions. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material.\n    6.  No endorsement. Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i).\nb.  Other rights.\n    1.  Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise.\n    2.  Patent and trademark rights are not licensed under this Public License.\n    3.  To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties, including when the Licensed Material is used other than for NonCommercial purposes.\n\nSection 3 – License Conditions.\n\nYour exercise of the Licensed Rights is expressly made subject to the following conditions.\n\na.  Attribution.\n    1.  If You Share the Licensed Material (including in modified form), You must:\n        A.  retain the following if it is supplied by the Licensor with the Licensed Material:\n            i.  identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated);\n           ii.  a copyright notice;\n          iii.  a notice that refers to this Public License;\n           iv.  a notice that refers to the disclaimer of warranties;\n            v.  a URI or hyperlink to the Licensed Material to the extent reasonably practicable;\n        B.  indicate if You modified the Licensed Material and retain an indication of any previous modifications; and\n        C.  indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License.\n    2.  You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information.\n    3.  If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable.\nb.  ShareAlike.\n    In addition to the conditions in Section 3(a), if You Share Adapted Material You produce, the following conditions also apply.\n    1.  The Adapter’s License You apply must be a Creative Commons license with the same License Elements, this version or later, or a BY-NC-SA Compatible License.\n    2.  You must include the text of, or the URI or hyperlink to, the Adapter's License You apply. You may satisfy this condition in any reasonable manner based on the medium, means, and context in which You Share Adapted Material.\n    3.  You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, Adapted Material that restrict exercise of the rights granted under the Adapter's License You apply.\n\nSection 4 – Sui Generis Database Rights.\n\nWhere the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material:\n\na.  for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database for NonCommercial purposes only;\nb.  if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material, including for purposes of Section 3(b); and\nc.  You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database.\n\nFor the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights.\n\nSection 5 – Disclaimer of Warranties and Limitation of Liability.\n\na.  Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation, warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable. Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You.\nb.  To the extent possible, in no event will the Licensor be liable to You on any legal theory (including, without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential, punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs, expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may not apply to You.\nc.  The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability.\n\nSection 6 – Term and Termination.\n\na.  This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically.\nb.  Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates:\n    1.  automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or\n    2.  upon express reinstatement by the Licensor.\n    For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License.\nc.  For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License.\nd.  Sections 1, 5, 6, 7, and 8 survive termination of this Public License.\n\nSection 7 – Other Terms and Conditions.\n\na.  The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed.\nb.  Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License.\n\nSection 8 – Interpretation.\n\na.  For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License.\nb.  To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions.\nc.  No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor.\nd.  Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority."
  },
  {
    "path": "NAV.md",
    "content": "+   编程\n    +   [JavaTPoint 编程语言中文教程📚](https://apachecn.github.io/javatpoint-prog-zh)\n    +   [JavaTPoint .NET 中文教程📚](https://apachecn.github.io/javatpoint-dotnet-zh)\n    +   [JavaTPoint Java 中文教程📚](https://apachecn.github.io/javatpoint-java-zh)\n    +   [JavaTPoint Python 中文教程📚](https://apachecn.github.io/javatpoint-python-zh)\n    +   [GeeksForGeeks 编程语言杂项中文教程📚](https://apachecn.github.io/geeksforgeeks-lang-misc-zh)\n    +   [GeeksForGeeks C# 中文教程📚](https://apachecn.github.io/geeksforgeeks-csharp-zh)\n    +   [GeeksForGeeks Scala 中文教程📚](https://apachecn.github.io/geeksforgeeks-scala-zh)\n    +   [GeeksForGeeks Python 中文教程📚](https://apachecn.github.io/geeksforgeeks-python-zh)\n    +   [GeeksForGeeks C/C++ 中文教程📚](https://apachecn.github.io/geeksforgeeks-c-cpp-zh)\n    +   [GeeksForGeeks Java 中文教程📚](https://apachecn.github.io/geeksforgeeks-java-zh)\n    +   [GeeksForGeeks JavaScript 中文教程📚](https://apachecn.github.io/geeksforgeeks-js-zh)\n    +   [ApacheCN C# 译文集📚](https://apachecn.github.io/apachecn-csharp-zh)\n    +   [ApacheCN C# 译文集（二）📚](https://apachecn.github.io/apachecn-csharp-zh-pt2)\n    +   [ApacheCN C# 译文集（三）📚](https://apachecn.github.io/apachecn-csharp-zh-pt3)\n    +   [ApacheCN C# 译文集（四）📚](https://apachecn.github.io/apachecn-csharp-zh-pt4)\n    +   [ApacheCN Golang 译文集📚](https://apachecn.github.io/apachecn-golang-zh)\n    +   [ApacheCN Golang 译文集（二）📚](https://apachecn.github.io/apachecn-golang-zh-pt2)\n    +   [ApacheCN C/C++ 译文集📚](https://apachecn.github.io/apachecn-c-cpp-zh)\n    +   [ApacheCN C/C++ 译文集（二）📚](https://apachecn.github.io/apachecn-c-cpp-zh-pt2)\n    +   [ApacheCN C/C++ 译文集（三）📚](https://apachecn.github.io/apachecn-c-cpp-zh-pt3)\n    +   [ApacheCN Java 译文集📚](https://apachecn.github.io/apachecn-java-zh)\n    +   [ApacheCN Java 译文集（二）📚](https://apachecn.github.io/apachecn-java-zh-pt2)\n    +   [ApacheCN Java 译文集（三）📚](https://apachecn.github.io/apachecn-java-zh-pt3)\n    +   [ApacheCN JavaScript 译文集📚](https://apachecn.github.io/apachecn-js-zh)\n    +   [ApacheCN JavaScript 译文集（二）📚](https://apachecn.github.io/apachecn-js-zh-pt2)\n    +   [ApacheCN JavaScript 译文集（三）📚](https://apachecn.github.io/apachecn-js-zh-pt3)\n    +   [ApacheCN JavaScript 译文集（四）📚](https://apachecn.github.io/apachecn-js-zh-pt4)\n    +   [ApacheCN Python 译文集📚](https://apachecn.github.io/apachecn-python-zh)\n    +   [ApacheCN Python 译文集（二）📚](https://apachecn.github.io/apachecn-python-zh-pt2)\n    +   [ApacheCN Python 译文集（三）📚](https://apachecn.github.io/apachecn-python-zh-pt3)\n    +   [ApacheCN Python 译文集（四）📚](https://apachecn.github.io/apachecn-python-zh-pt4)\n    +   [ApacheCN Ruby 译文集📚](https://apachecn.github.io/apachecn-ruby-zh)\n    +   [BeginnersBook 中文系列教程📚](https://apachecn.github.io/beginnersbook-zh)\n    +   [JavaScript 编程精解 中文第三版](https://apachecn.github.io/eloquent-js-3e-zh)\n    +   [Guru99 中文系列教程📚🚧](https://apachecn.github.io/guru99-zh)\n    +   [HowToDoInJava 中文系列教程📚](https://apachecn.github.io/howtodoinjava-zh)\n    +   [OverIQ 中文系列教程📚](https://apachecn.github.io/overiq-zh)\n    +   [LearnETutroials 中文系列教程📚](https://apachecn.github.io/learnetutorials-zh)\n    +   [StudyTonight 中文系列教程📚](https://apachecn.github.io/studytonight-zh)\n    +   [TutorialGateway 中文系列教程📚](https://apachecn.github.io/tutorialgateway-zh)\n    +   [TutorialGateway BI 中文系列教程📚](https://apachecn.github.io/tutorialgateway-bi-zh)\n    +   [TutorialsTeacher 中文系列教程📚](https://apachecn.github.io/tutorialsteacher-zh)\n    +   [通过示例学 Golang 2020 中文版](https://apachecn.github.io/golang-by-example-2020-zh)\n    +   [写给不耐烦程序员的 JavaScript🚧](https://apachecn.github.io/impatient-js-zh)\n    +   [JavaBeginnersTutorial 中文系列教程📚](https://apachecn.github.io/jbt-zh)\n    +   [JavaTutorialNetwork 中文系列教程📚](https://apachecn.github.io/jtn-zh)\n    +   [笨办法学C 中文版](https://apachecn.github.io/lcthw-zh)\n    +   [笨办法学 Python · 续 中文版](https://apachecn.github.io/lmpythw-zh)\n    +   [Programiz 中文系列教程📚](https://apachecn.github.io/programiz-zh)\n    +   [PythonBasics 中文系列教程📚](https://apachecn.github.io/pythonbasics-zh)\n    +   [PythonGuru 中文系列教程📚](https://apachecn.github.io/pythonguru-zh)\n    +   [PythonSpot 中文系列教程📚](https://apachecn.github.io/pythonspot-zh)\n    +   [Think Python](https://apachecn.github.io/think-py-2e-zh)\n    +   [ZetCode 中文系列教程📚](https://apachecn.github.io/zetcode-zh)\n+   前端\n    +   [JavaTPoint 移动开发中文教程📚](https://apachecn.github.io/javatpoint-mobi-zh)\n    +   [GeeksForGeeks Web 杂项中文教程📚](https://apachecn.github.io/geeksforgeeks-web-misc-zh)\n    +   [GeeksForGeeks Angular/Vue/React 中文教程📚](https://apachecn.github.io/geeksforgeeks-ng-vue-react-zh)\n    +   [GeeksForGeeks jQuery 中文教程📚](https://apachecn.github.io/geeksforgeeks-jquery-zh)\n    +   [GeeksForGeeks CSS 中文教程📚](https://apachecn.github.io/geeksforgeeks-css-zh)\n    +   [GeeksForGeeks HTML 中文教程📚](https://apachecn.github.io/geeksforgeeks-html-zh)\n    +   [ApacheCN Vue 译文集📚](https://apachecn.github.io/apachecn-vue-zh)\n    +   [ApacheCN Angular 译文集📚](https://apachecn.github.io/apachecn-angular-zh)\n    +   [ApacheCN React 译文集📚](https://apachecn.github.io/apachecn-react-zh)\n    +   [ApacheCN jQuery 译文集📚](https://apachecn.github.io/apachecn-jquery-zh)\n    +   [ApacheCN jQuery 译文集（二）📚](https://apachecn.github.io/apachecn-jquery-zh-pt2)\n+   后端/大数据\n    +   [JavaTPoint 大数据中文教程📚](https://apachecn.github.io/javatpoint-bigdata-zh)\n    +   [JavaTPoint Web 开发中文教程📚](https://apachecn.github.io/javatpoint-web-zh)\n    +   [JavaTPoint 数据库中文教程📚](https://apachecn.github.io/javatpoint-db-zh)\n    +   [JavaTPoint PHP 中文教程📚](https://apachecn.github.io/javatpoint-php-zh)\n    +   [GeeksForGeeks ASP 中文教程📚](https://apachecn.github.io/geeksforgeeks-asp-zh)\n    +   [GeeksForGeeks SQL 中文教程📚](https://apachecn.github.io/geeksforgeeks-sql-zh)\n    +   [GeeksForGeeks NodeJS 中文教程📚](https://apachecn.github.io/geeksforgeeks-nodejs-zh)\n    +   [GeeksForGeeks PHP 中文教程📚](https://apachecn.github.io/geeksforgeeks-php-zh)\n    +   [ApacheCN 数据库译文集📚](https://apachecn.github.io/apachecn-db-zh)\n    +   [ApacheCN 数据库译文集（二）📚](https://apachecn.github.io/apachecn-db-zh-pt2)\n    +   [ApacheCN Python Web 译文集📚](https://apachecn.github.io/apachecn-pythonweb-zh)\n    +   [ApacheCN Python Web 译文集（二）📚](https://apachecn.github.io/apachecn-pythonweb-zh-pt2)\n    +   [ApacheCN Asp.NET 译文集📚](https://apachecn.github.io/apachecn-asp-dotnet-zh)\n    +   [ApacheCN Asp.NET 译文集（二）📚](https://apachecn.github.io/apachecn-asp-dotnet-zh-pt2)\n    +   [ApacheCN Asp.NET 译文集（三）📚](https://apachecn.github.io/apachecn-asp-dotnet-zh-pt3)\n    +   [ApacheCN Asp.NET 译文集（四）📚](https://apachecn.github.io/apachecn-asp-dotnet-zh-pt4)\n    +   [ApacheCN NodeJS 译文集📚](https://apachecn.github.io/apachecn-node-zh)\n    +   [ApacheCN NodeJS 译文集（二）📚](https://apachecn.github.io/apachecn-node-zh-pt2)\n    +   [ApacheCN PHP 译文集📚](https://apachecn.github.io/apachecn-php-zh)\n    +   [ApacheCN PHP 译文集（二）📚](https://apachecn.github.io/apachecn-php-zh-pt2)\n    +   [ApacheCN 大数据译文集（二）📚](https://apachecn.github.io/apachecn-bigdata-zh-pt2)\n    +   [ApacheCN 大数据译文集（三）📚](https://apachecn.github.io/apachecn-bigdata-zh-pt3)\n    +   [ApacheCN 大数据译文集📚](https://apachecn.github.io/apachecn-bigdata-zh)\n    +   [ApacheCN Java Web 译文集📚](https://apachecn.github.io/apachecn-javaweb-zh)\n    +   [ApacheCN Java Web 译文集（二）📚](https://apachecn.github.io/apachecn-javaweb-zh-pt2)\n    +   [Airflow 中文文档](https://apachecn.github.io/airflow-doc-zh)\n    +   [Elasticsearch 5.4 中文文档](https://apachecn.github.io/elasticsearch-doc-zh)\n    +   [Flink 中文文档](https://apachecn.github.io/flink-doc-zh)\n    +   [HBase™ 中文参考指南 3.0🚧](https://apachecn.github.io/hbase-doc-zh)\n    +   [HighScalability 中文示例📚🚧](https://apachecn.github.io/highscalability-examples-zh)\n    +   [Kibana 5.2 中文文档](https://apachecn.github.io/kibana-doc-zh)\n    +   [Kudu 1.4.0 中文文档](https://apachecn.github.io/kudu-doc-zh)\n    +   [Apache Spark 官方文档中文版](https://apachecn.github.io/spark-doc-zh)\n    +   [Apache Kafka 官方文档中文版](https://apachecn.github.io/kafka-site-zh)\n    +   [Spring Boot 1.5.2 中文文档](https://apachecn.github.io/spring-boot-doc-zh)\n    +   [Storm 1.1.0 中文文档](https://apachecn.github.io/storm-doc-zh)\n    +   [Zeppelin 0.7.2 中文文档](https://apachecn.github.io/zeppelin-doc-zh)\n+   工具\n    +   [JavaTPoint 实用工具中文教程📚](https://apachecn.github.io/javatpoint-util-zh)\n    +   [ApacheCN DevOps 译文集📚](https://apachecn.github.io/apachecn-devops-zh)\n    +   [ApacheCN DevOps 译文集（二）📚](https://apachecn.github.io/apachecn-devops-zh-pt2)\n    +   [ApacheCN DevOps 译文集（三）📚](https://apachecn.github.io/apachecn-devops-zh-pt3)\n    +   [ApacheCN DevOps 译文集（四）📚](https://apachecn.github.io/apachecn-devops-zh-pt4)\n    +   [ApacheCN DevOps 译文集（五）📚](https://apachecn.github.io/apachecn-devops-zh-pt5)\n    +   [ApacheCN Linux 译文集📚](https://apachecn.github.io/apachecn-linux-zh)\n    +   [ApacheCN Linux 译文集（二）📚](https://apachecn.github.io/apachecn-linux-zh-pt2)\n    +   [ApacheCN Linux 译文集（三）📚](https://apachecn.github.io/apachecn-linux-zh-pt3)\n    +   [Cython 3.0 中文文档🚧](https://apachecn.github.io/cython-doc-zh)\n    +   [Git 中文参考🚧](https://apachecn.github.io/git-doc-zh)\n    +   [Gitlab 中文文档🚧](https://apachecn.github.io/gitlab-doc-zh)\n    +   [笨办法学 Linux 中文版](https://apachecn.github.io/llthw-zh)\n    +   [Numba 0.44 中文文档🚧](https://apachecn.github.io/numba-doc-zh)\n    +   [PyQt4 中文文档🚧](https://apachecn.github.io/pyqt4-doc-zh)\n    +   [Scrapy 1.6 中文文档](https://apachecn.github.io/scrapy-doc-zh)\n+   数据科学\n    +   [ApacheCN 数据科学译文集📚](https://apachecn.github.io/apachecn-ds-zh)\n    +   [ApacheCN 数据科学译文集（二）📚](https://apachecn.github.io/apachecn-ds-zh-pt2)\n    +   [ApacheCN 数据科学译文集（三）📚](https://apachecn.github.io/apachecn-ds-zh-pt3)\n    +   [ApacheCN 数据科学译文集📚](https://apachecn.github.io/apachecn-ds-zh)\n    +   [MIT 18.03 面向初学者的微积分🚧](https://apachecn.github.io/calc4b-zh)\n    +   [UCB Data8 计算与推断思维](https://apachecn.github.io/data8-textbook-zh)\n    +   [数据可视化的基础知识](https://apachecn.github.io/dataviz-zh)\n    +   [数据科学和人工智能技术笔记](https://apachecn.github.io/ds-ai-tech-notes)\n    +   [数据科学 IPython 笔记本📚](https://apachecn.github.io/ds-ipynb-zh)\n    +   [UCB DS100 数据科学的原理与技巧🚧](https://apachecn.github.io/ds100-textbook-zh)\n    +   [ApacheCN 数据科学和人工智能知识库](https://apachecn.github.io/dsai-wiki)\n    +   [Matplotlib 用户指南](https://apachecn.github.io/matplotlib-doc-zh)\n    +   [MIT 18.06 线性代数笔记](https://apachecn.github.io/mit-18.06-linalg-notes)\n    +   [利用 Python 进行数据分析 · 第 2 版](https://apachecn.github.io/pyda-2e-zh)\n    +   [QuantLearning](https://apachecn.github.io/quant-learning)\n    +   [seaborn 0.9 中文文档](https://apachecn.github.io/seaborn-doc-zh)\n    +   [社交媒体挖掘 - 翻译版](https://apachecn.github.io/socialmediamining-zh)\n    +   [斯坦福 Stats60 21 世纪的统计思维🚧](https://apachecn.github.io/stats-thinking-21-zh)\n    +   [复杂性思维 中文第二版](https://apachecn.github.io/think-comp-2e-zh)\n    +   [PyMiner 开发者指南](https://apachecn.github.io/pyminer-dev-guide)\n+   人工智能\n    +   [JavaTPoint 数据科学与人工智能中文教程📚](https://apachecn.github.io/javatpoint-dsai-zh)\n    +   [GeeksForGeeks 人工智能中文教程📚](https://apachecn.github.io/geeksforgeeks-ai-zh)\n    +   [AILearning📚](https://apachecn.github.io/ailearning)\n    +   [ApacheCN 计算机视觉译文集📚](https://apachecn.github.io/apachecn-cv-zh)\n    +   [ApacheCN 计算机视觉译文集（二）📚](https://apachecn.github.io/apachecn-cv-zh-pt2)\n    +   [ApacheCN 深度学习译文集📚](https://apachecn.github.io/apachecn-dl-zh)\n    +   [ApacheCN 深度学习译文集（二）📚](https://apachecn.github.io/apachecn-dl-zh-pt2)\n    +   [ApacheCN 深度学习译文集（三）📚](https://apachecn.github.io/apachecn-dl-zh-pt3)\n    +   [ApacheCN 机器学习译文集📚](https://apachecn.github.io/apachecn-ml-zh)\n    +   [ApacheCN 机器学习译文集（二）📚](https://apachecn.github.io/apachecn-ml-zh-pt2)\n    +   [ApacheCN 机器学习译文集（三）📚](https://apachecn.github.io/apachecn-ml-zh-pt3)\n    +   [FastText 中文文档](https://apachecn.github.io/fasttext-doc-zh)\n    +   [面向机器学习的特征工程](https://apachecn.github.io/fe4ml-zh)\n    +   [Gensim 中文文档](https://apachecn.github.io/gensim-doc-zh)\n    +   [Sklearn 与 TensorFlow 机器学习实用指南第二版](https://apachecn.github.io/hands-on-ml-2e-zh)\n    +   [LightGBM 中文文档](https://apachecn.github.io/lightgbm-doc-zh)\n    +   [Machine Learning Mastery 博客文章翻译📚🚧](https://apachecn.github.io/ml-mastery-zh)\n    +   [Machine Learning Mastery 博客文章翻译（二）📚🚧](https://apachecn.github.io/ml-mastery-zh-pt2)\n    +   [Python 自然语言处理 第二版](https://apachecn.github.io/nlp-py-2e-zh)\n    +   [PyTorch 自然语言处理](https://apachecn.github.io/nlp-pytorch-zh)\n    +   [台湾大学林轩田机器学习笔记](https://apachecn.github.io/ntu-hsuantienlin-ml)\n    +   [OpenCV 中文文档 4.0.0](https://apachecn.github.io/opencv-doc-zh)\n    +   [PythonProgramming.net 系列教程📚](https://apachecn.github.io/python-programming-net-zh)\n    +   [PyTorch 中文教程](https://apachecn.github.io/pytorch-doc-zh)\n    +   [scikit-learn (sklearn) 官方文档中文版](https://apachecn.github.io/sklearn-doc-zh)\n    +   [XGBoost 中文文档](https://apachecn.github.io/xgboost-doc-zh)\n+   计算机科学\n    +   [JavaTPoint 计算机科学中文教程📚](https://apachecn.github.io/javatpoint-cs-zh)\n    +   [ApacheCN 数据结构与算法译文集📚](https://apachecn.github.io/apachecn-algo-zh)\n    +   [ApacheCN 计算机系统译文集📚](https://apachecn.github.io/apachecn-sys-zh)\n    +   [NUS CS1101s SICP JavaScript 描述🚧](https://apachecn.github.io/sicp-js-zh)\n    +   [UCB CS61a SICP Python 描述](https://apachecn.github.io/sicp-py-zh)\n    +   [数据结构思维中文版](https://apachecn.github.io/think-dast-zh)\n    +   [UIUC CS241 系统编程中文讲义🚧](https://apachecn.github.io/uiuc-cs241-notes-zh)\n+   安全\n    +   [ApacheCN Kali Linux 译文集📚](https://apachecn.github.io/apachecn-kali-zh)\n    +   [ApacheCN 网络安全译文集📚](https://apachecn.github.io/apachecn-sec-zh)\n    +   [ApacheCN 网络安全译文集（二）📚](https://apachecn.github.io/apachecn-sec-zh-pt2)\n    +   [SecLearning——零组文库备份📚](https://apachecn.github.io/sec-learning)\n    +   [ApacheCN 安全知识库📚](https://apachecn.github.io/sec-wiki)\n    +   [Web Hacking 101 中文版](https://apachecn.github.io/web-hacking-101-zh)\n+   其它\n    +   [生化环材劝退文集](https://apachecn.github.io/bio-chem-env-mat-discourage)\n    +   [5 分钟商学院精细笔记](https://apachecn.github.io/business-5min-notes)\n    +   [iBooker 布客](https://apachecn.github.io/home)\n    +   [iBooker 布客老实人报](https://apachecn.github.io/ibooker-plain-dealer)\n    +   [使用 Qiskit 学习量子计算 - 翻译版](https://apachecn.github.io/lqcuq-zh)\n    +   [原则 · 中文版](https://apachecn.github.io/principles-zh)\n    +   [斯坦福 CS183 & YC 创业课系列中文笔记📚](https://apachecn.github.io/stanford-cs183-notes)\n    +   [iBooker 团队知识库📚](https://apachecn.github.io/team-wiki)\n    +   [ApacheCN 技术评论](https://apachecn.github.io/tech-review)\n    +   [通往财富自由之路精细笔记](https://apachecn.github.io/the-way-to-wealth-freedom-notes)\n"
  },
  {
    "path": "README.md",
    "content": "# ApacheCN C/C++ 译文集\n\n> 协议：[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)\n> \n> 让开源界提前感受入关。\n\n* [在线阅读](https://ccpp.apachecn.org)\n* [在线阅读（Gitee）](https://apachecn.gitee.io/apachecn-c-cpp-zh/)\n* [ApacheCN 学习资源](http://docs.apachecn.org/)\n\n## 目录\n\n+   [笨办法学C 中文版](docs/lcthw-zh/SUMMARY.md)\n+   [C++ 高级编程](docs/adv-cpp/SUMMARY.md)\n+   [C++ 高级编程秘籍](docs/adv-cpp-prog-cb/SUMMARY.md)\n+   [C++ Qt5 GUI 编程](docs/cpp-gui-prog-qt5/SUMMARY.md)\n+   [C++ 专家级编程](docs/exp-cpp/SUMMARY.md)\n+   [C++ 游戏动画编程实用指南](docs/handson-cpp-game-ani-prog/SUMMARY.md)\n+   [C++ 函数式编程实用指南](docs/handson-func-prog-cpp/SUMMARY.md)\n+   [C++ 机器学习实用指南](docs/handson-ml-cpp/SUMMARY.md)\n+   [通过使用 UE4 构建游戏学习 C++](docs/learn-cpp-build-game-ue4/SUMMARY.md)\n+   [精通 C++ 游戏开发](docs/master-cpp-game-dev/SUMMARY.md)\n+   [精通 C++ 编程](docs/master-cpp-prog/SUMMARY.md)\n+   [Qt5 C++ GUI 编程秘籍](docs/qt5-cpp-gui-prog-cb/SUMMARY.md)\n+   [Qt Creator 应用开发](docs/app-dev-qt-creator/SUMMARY.md)\n+   [C++ 编程入门手册](docs/begin-cpp-prog/SUMMARY.md)\n+   [现代 C++ 嵌入式编程秘籍](docs/emb-prog-mod-cpp-cb/SUMMARY.md)\n+   [C++ 专家级编程](docs/exp-cpp-prog/SUMMARY.md)\n+   [UE 游戏开发项目](docs/game-dev-proj-ue/SUMMARY.md)\n+   [CUDA 编程学习手册](docs/learn-cuda-prog/SUMMARY.md)\n+   [WebAssembly 学习手册](docs/learn-wasm/SUMMARY.md)\n+   [精通 C++ 多线程](docs/master-cpp-multithrd/SUMMARY.md)\n+   [现代 C++ 编程](docs/mod-cpp/SUMMARY.md)\n+   [现代 C++ 的挑战](docs/mod-cpp-challenge/SUMMARY.md)\n+   [C++ 游戏编程入门手册](docs/begin-cpp-game-prog/SUMMARY.md)\n+   [Boost.Asio C++ 网络编程入门中文第二版](docs/boost-asio-cpp-net-prog-2e/SUMMARY.md)\n+   [Boost C++ 应用开发秘籍](docs/boost-cpp-app-dev-cb/SUMMARY.md)\n+   [C++ 数据结构和算法设计原则](docs/cpp-dsal-design-principle/SUMMARY.md)\n+   [C++ 高性能编程](docs/cpp-hiperf/SUMMARY.md)\n+   [C++ 反应式编程](docs/cpp-react-prog/SUMMARY.md)\n+   [C++ 系统编程秘籍](docs/cpp-sys-prog-cb/SUMMARY.md)\n+   [C++ 工作室](docs/cpp-workshop/SUMMARY.md)\n+   [WebAssembly 游戏编程实用指南](docs/handson-game-dev-wasm/SUMMARY.md)\n+   [C++ 函数式编程学习手册](docs/learn-cpp-func-prog/SUMMARY.md)\n+   [Qt5 学习手册](docs/learn-qt5/SUMMARY.md)\n\n## 贡献指南\n\n为了不断改进翻译质量，我们特此启动了【翻译、校对、笔记整理活动】，开设了多个校对项目。贡献者校对一章之后可以领取千字2\\~4元的奖励。进行中的校对活动请见[活动列表](https://home.apachecn.org/#/docs/activity/docs-activity)。更多详情请联系飞龙（Q562826179，V:wizardforcel）。\n\n## 联系方式\n\n### 负责人\n\n* [飞龙](https://github.com/wizardforcel): 562826179\n\n### 其他\n\n*   在我们的 [apachecn/apachecn-c-cpp-zh](https://github.com/apachecn/apachecn-c-cpp-zh) github 上提 issue.\n*   发邮件到 Email: `apachecn@163.com`.\n*   在我们的 [组织学习交流群](http://www.apachecn.org/organization/348.html) 中联系群主/管理员即可.\n\n## 下载\n\n### Docker\n\n```\ndocker pull apachecn0/apachecn-c-cpp-zh\ndocker run -tid -p <port>:80 apachecn0/apachecn-c-cpp-zh\n# 访问 http://localhost:{port} 查看文档\n```\n\n### PYPI\n\n```\npip install apachecn-c-cpp-zh\napachecn-c-cpp-zh <port>\n# 访问 http://localhost:{port} 查看文档\n```\n\n### NPM\n\n```\nnpm install -g apachecn-c-cpp-zh\napachecn-c-cpp-zh <port>\n# 访问 http://localhost:{port} 查看文档\n```\n\n## 赞助我们\n\n![](http://data.apachecn.org/img/about/donate.jpg)\n"
  },
  {
    "path": "SUMMARY.md",
    "content": "+   [笨办法学C 中文版](docs/lcthw-zh/README.md)\n    +   [前言](docs/lcthw-zh/preface.md)\n    +   [导言：C的笛卡尔之梦](docs/lcthw-zh/introduction.md)\n    +   [练习0：准备](docs/lcthw-zh/ex0.md)\n    +   [练习1：启用编译器](docs/lcthw-zh/ex1.md)\n    +   [练习2：用Make来代替Python](docs/lcthw-zh/ex2.md)\n    +   [练习3：格式化输出](docs/lcthw-zh/ex3.md)\n    +   [练习4：Valgrind 介绍](docs/lcthw-zh/ex4.md)\n    +   [练习5：一个C程序的结构](docs/lcthw-zh/ex5.md)\n    +   [练习6：变量类型](docs/lcthw-zh/ex6.md)\n    +   [练习7：更多变量和一些算术](docs/lcthw-zh/ex7.md)\n    +   [练习8：大小和数组](docs/lcthw-zh/ex8.md)\n    +   [练习9：数组和字符串](docs/lcthw-zh/ex9.md)\n    +   [练习10：字符串数组和循环](docs/lcthw-zh/ex10.md)\n    +   [练习11：While循环和布尔表达式](docs/lcthw-zh/ex11.md)\n    +   [练习12：If，Else If，Else](docs/lcthw-zh/ex12.md)\n    +   [练习13：Switch语句](docs/lcthw-zh/ex13.md)\n    +   [练习14：编写并使用函数](docs/lcthw-zh/ex14.md)\n    +   [练习15：指针，可怕的指针](docs/lcthw-zh/ex15.md)\n    +   [练习16：结构体和指向它们的指针](docs/lcthw-zh/ex16.md)\n    +   [练习17：堆和栈的内存分配](docs/lcthw-zh/ex17.md)\n    +   [练习18：函数指针](docs/lcthw-zh/ex18.md)\n    +   [练习19：一个简单的对象系统](docs/lcthw-zh/ex19.md)\n    +   [练习20：Zed的强大的调试宏](docs/lcthw-zh/ex20.md)\n    +   [练习21：高级数据类型和控制结构](docs/lcthw-zh/ex21.md)\n    +   [练习22：栈、作用域和全局](docs/lcthw-zh/ex22.md)\n    +   [练习23：认识达夫设备](docs/lcthw-zh/ex23.md)\n    +   [练习24：输入输出和文件](docs/lcthw-zh/ex24.md)\n    +   [练习25：变参函数](docs/lcthw-zh/ex25.md)\n    +   [练习26：编写第一个真正的程序](docs/lcthw-zh/ex26.md)\n    +   [练习27：创造性和防御性编程](docs/lcthw-zh/ex27.md)\n    +   [练习28：Makefile 进阶](docs/lcthw-zh/ex28.md)\n    +   [练习29：库和链接](docs/lcthw-zh/ex29.md)\n    +   [练习30：自动化测试](docs/lcthw-zh/ex30.md)\n    +   [练习31：代码调试](docs/lcthw-zh/ex31.md)\n    +   [练习32：双向链表](docs/lcthw-zh/ex32.md)\n    +   [练习33：链表算法](docs/lcthw-zh/ex33.md)\n    +   [练习34：动态数组](docs/lcthw-zh/ex34.md)\n    +   [练习35：排序和搜索](docs/lcthw-zh/ex35.md)\n    +   [练习36：更安全的字符串](docs/lcthw-zh/ex36.md)\n    +   [练习37：哈希表](docs/lcthw-zh/ex37.md)\n    +   [练习38：哈希算法](docs/lcthw-zh/ex38.md)\n    +   [练习39：字符串算法](docs/lcthw-zh/ex39.md)\n    +   [练习40：二叉搜索树](docs/lcthw-zh/ex40.md)\n    +   [练习41：将 Cachegrind 和 Callgrind 用于性能调优](docs/lcthw-zh/ex41.md)\n    +   [练习42：栈和队列](docs/lcthw-zh/ex42.md)\n    +   [练习43：一个简单的统计引擎](docs/lcthw-zh/ex43.md)\n    +   [练习44：环形缓冲区](docs/lcthw-zh/ex44.md)\n    +   [练习45：一个简单的TCP/IP客户端](docs/lcthw-zh/ex45.md)\n    +   [练习46：三叉搜索树](docs/lcthw-zh/ex46.md)\n    +   [练习47：一个快速的URL路由](docs/lcthw-zh/ex47.md)\n    +   [后记：“解构 K&R C” 已死](docs/lcthw-zh/postscript.md)\n    +   [捐赠名单](docs/lcthw-zh/donors.md)\n+   [C++ 高级编程](docs/adv-cpp/README.md)\n    +   [零、前言](docs/adv-cpp/00.md)\n    +   [一、可移植的 C++ 软件剖析](docs/adv-cpp/01.md)\n    +   [二、不允许鸭子——类型和推导（一）](docs/adv-cpp/02.md)\n    +   [三、不允许鸭子——模板和推导（二）](docs/adv-cpp/03.md)\n    +   [四、不允许泄漏——异常和资源](docs/adv-cpp/04.md)\n    +   [五、关注点分离——软件架构、函数和可变模板](docs/adv-cpp/05.md)\n    +   [六、哲学家的晚餐——线程和并发](docs/adv-cpp/06.md)\n    +   [七、流和输入/输出](docs/adv-cpp/07.md)\n    +   [八、每个人都会跌倒，这是你爬起来的方式——测试和调试](docs/adv-cpp/08.md)\n    +   [九、对速度的需求——性能和优化](docs/adv-cpp/09.md)\n    +   [十、附录](docs/adv-cpp/10.md)\n+   [C++ 高级编程秘籍](docs/adv-cpp-prog-cb/README.md)\n    +   [零、前言](docs/adv-cpp-prog-cb/00.md)\n    +   [一、库的开发入门](docs/adv-cpp-prog-cb/01.md)\n    +   [二、将异常用于错误处理](docs/adv-cpp-prog-cb/02.md)\n    +   [三、实现移动语义](docs/adv-cpp-prog-cb/03.md)\n    +   [四、将模板用于泛型编程](docs/adv-cpp-prog-cb/04.md)\n    +   [五、并发和同步](docs/adv-cpp-prog-cb/05.md)\n    +   [六、优化代码以提高性能](docs/adv-cpp-prog-cb/06.md)\n    +   [七、调试和测试](docs/adv-cpp-prog-cb/07.md)\n    +   [八、创建和实现您自己的容器](docs/adv-cpp-prog-cb/08.md)\n    +   [九、探索类型擦除](docs/adv-cpp-prog-cb/09.md)\n    +   [十、对动态分配的深入研究](docs/adv-cpp-prog-cb/10.md)\n    +   [十一、C++ 中的常见模式](docs/adv-cpp-prog-cb/11.md)\n    +   [十二、更仔细查看类型推导](docs/adv-cpp-prog-cb/12.md)\n    +   [十三、奖励——使用 C++ 20 特性](docs/adv-cpp-prog-cb/13.md)\n+   [C++ Qt5 GUI 编程](docs/cpp-gui-prog-qt5/README.md)\n    +   [零、前言](docs/cpp-gui-prog-qt5/00.md)\n    +   [一、Qt 简介](docs/cpp-gui-prog-qt5/01.md)\n    +   [二、Qt 小部件和样式表](docs/cpp-gui-prog-qt5/02.md)\n    +   [三、数据库连接](docs/cpp-gui-prog-qt5/03.md)\n    +   [四、图表](docs/cpp-gui-prog-qt5/04.md)\n    +   [五、项目视图和对话框](docs/cpp-gui-prog-qt5/05.md)\n    +   [六、整合网络内容](docs/cpp-gui-prog-qt5/06.md)\n    +   [七、地图查看器](docs/cpp-gui-prog-qt5/07.md)\n    +   [八、图形视图](docs/cpp-gui-prog-qt5/08.md)\n    +   [九、照相机模块](docs/cpp-gui-prog-qt5/09.md)\n    +   [十、即时消息](docs/cpp-gui-prog-qt5/10.md)\n    +   [十一、实现图形编辑器](docs/cpp-gui-prog-qt5/11.md)\n    +   [十二、云存储](docs/cpp-gui-prog-qt5/12.md)\n    +   [十三、多媒体查看器](docs/cpp-gui-prog-qt5/13.md)\n    +   [十四、Qt Quick 和 QML](docs/cpp-gui-prog-qt5/14.md)\n    +   [十五、跨平台开发](docs/cpp-gui-prog-qt5/15.md)\n    +   [十六、测试和调试](docs/cpp-gui-prog-qt5/16.md)\n+   [C++ 专家级编程](docs/exp-cpp/README.md)\n    +   [零、前言](docs/exp-cpp/00.md)\n    +   [第一部分：C++ 编程的背后](docs/exp-cpp/sec1.md)\n        +   [一、构建 C++ 应用简介](docs/exp-cpp/01.md)\n        +   [二、C++ 低级编程](docs/exp-cpp/02.md)\n        +   [三、面向对象编程的细节](docs/exp-cpp/03.md)\n        +   [四、理解和设计模板](docs/exp-cpp/04.md)\n        +   [五、内存管理和智能指针](docs/exp-cpp/05.md)\n    +   [第二部分：设计健壮高效的应用](docs/exp-cpp/sec2.md)\n        +   [六、STL 中数据结构和算法的挖掘](docs/exp-cpp/06.md)\n        +   [七、函数式编程](docs/exp-cpp/07.md)\n        +   [八、并发和多线程](docs/exp-cpp/08.md)\n        +   [九、设计并发数据结构](docs/exp-cpp/09.md)\n        +   [十、设计全球通用的应用](docs/exp-cpp/10.md)\n        +   [十一、使用设计模式设计策略游戏](docs/exp-cpp/11.md)\n        +   [十二、网络和安全](docs/exp-cpp/12.md)\n        +   [十三、调试和测试](docs/exp-cpp/13.md)\n        +   [十四、使用 Qt 的图形用户界面](docs/exp-cpp/14.md)\n    +   [第三部分：人工智能世界中的 C++](docs/exp-cpp/sec3.md)\n        +   [十五、C++ 在机器学习任务中的应用](docs/exp-cpp/15.md)\n        +   [十六、实现基于对话的搜索引擎](docs/exp-cpp/16.md)\n    +   [十七、答案](docs/exp-cpp/17.md)\n+   [C++ 游戏动画编程实用指南](docs/handson-cpp-game-ani-prog/README.md)\n    +   [零、前言](docs/handson-cpp-game-ani-prog/00.md)\n    +   [一、创建游戏窗口](docs/handson-cpp-game-ani-prog/01.md)\n    +   [二、实现向量](docs/handson-cpp-game-ani-prog/02.md)\n    +   [三、实现矩阵](docs/handson-cpp-game-ani-prog/03.md)\n    +   [四、实现四元数](docs/handson-cpp-game-ani-prog/04.md)\n    +   [五、实现转换](docs/handson-cpp-game-ani-prog/05.md)\n    +   [六、构建抽象渲染器](docs/handson-cpp-game-ani-prog/06.md)\n    +   [七、探索 glTF 文件格式](docs/handson-cpp-game-ani-prog/07.md)\n    +   [八、创建曲线、帧和轨迹](docs/handson-cpp-game-ani-prog/08.md)\n    +   [九、实现动画剪辑](docs/handson-cpp-game-ani-prog/09.md)\n    +   [十、网格蒙皮](docs/handson-cpp-game-ani-prog/10.md)\n    +   [十一、优化动画流水线](docs/handson-cpp-game-ani-prog/11.md)\n    +   [十二、动画之间的融合](docs/handson-cpp-game-ani-prog/12.md)\n    +   [十三、实现逆运动学](docs/handson-cpp-game-ani-prog/13.md)\n    +   [十四、使用对偶四元数蒙皮](docs/handson-cpp-game-ani-prog/14.md)\n    +   [十五、使用实例渲染人群](docs/handson-cpp-game-ani-prog/15.md)\n+   [C++ 函数式编程实用指南](docs/handson-func-prog-cpp/README.md)\n    +   [零、前言](docs/handson-func-prog-cpp/00.md)\n    +   [第一部分：C++ 中的函数组件](docs/handson-func-prog-cpp/sec1.md)\n        +   [一、函数式编程导论](docs/handson-func-prog-cpp/01.md)\n        +   [二、理解纯函数](docs/handson-func-prog-cpp/02.md)\n        +   [三、深入 lambdas](docs/handson-func-prog-cpp/03.md)\n        +   [四、函数组合思想](docs/handson-func-prog-cpp/04.md)\n        +   [五、局部应用与柯里化](docs/handson-func-prog-cpp/05.md)\n    +   [第二部分：函数设计](docs/handson-func-prog-cpp/sec2.md)\n        +   [六、函数思维——从数据输入到数据输出](docs/handson-func-prog-cpp/06.md)\n        +   [七、通过函数操作消除重复](docs/handson-func-prog-cpp/07.md)\n        +   [八、使用类提高内聚性](docs/handson-func-prog-cpp/08.md)\n        +   [九、面向函数式编程的测试驱动开发](docs/handson-func-prog-cpp/09.md)\n    +   [第三部分：收获函数式编程的好处](docs/handson-func-prog-cpp/sec3.md)\n        +   [十、性能优化](docs/handson-func-prog-cpp/10.md)\n        +   [十一、基于属性的测试](docs/handson-func-prog-cpp/11.md)\n        +   [十二、重构到纯函数和通过纯函数重构](docs/handson-func-prog-cpp/12.md)\n        +   [十三、不变性和架构——事件源](docs/handson-func-prog-cpp/13.md)\n    +   [第四部分：C++ 函数式编程的现状和未来](docs/handson-func-prog-cpp/sec4.md)\n        +   [十四、使用范围库的延迟求值](docs/handson-func-prog-cpp/14.md)\n        +   [十五、STL 支持和建议](docs/handson-func-prog-cpp/15.md)\n        +   [十六、标准语言支持和建议](docs/handson-func-prog-cpp/16.md)\n    +   [十七、答案](docs/handson-func-prog-cpp/17.md)\n+   [通过使用 UE4 构建游戏学习 C++](docs/learn-cpp-build-game-ue4/README.md)\n    +   [零、前言](docs/learn-cpp-build-game-ue4/00.md)\n    +   [一、C++ 17 入门](docs/learn-cpp-build-game-ue4/01.md)\n    +   [二、变量和内存](docs/learn-cpp-build-game-ue4/02.md)\n    +   [三、`if...else`和`switch`](docs/learn-cpp-build-game-ue4/03.md)\n    +   [四、循环](docs/learn-cpp-build-game-ue4/04.md)\n    +   [五、函数和宏](docs/learn-cpp-build-game-ue4/05.md)\n    +   [六、对象、类和继承](docs/learn-cpp-build-game-ue4/06.md)\n    +   [七、动态存储分配](docs/learn-cpp-build-game-ue4/07.md)\n    +   [八、演员和棋子](docs/learn-cpp-build-game-ue4/08.md)\n    +   [九、模板和常用容器](docs/learn-cpp-build-game-ue4/09.md)\n    +   [十、库存系统和提取项目](docs/learn-cpp-build-game-ue4/10.md)\n    +   [十一、怪物](docs/learn-cpp-build-game-ue4/11.md)\n    +   [十二、使用高级人工智能构建更聪明的怪物](docs/learn-cpp-build-game-ue4/12.md)\n    +   [十三、咒语书](docs/learn-cpp-build-game-ue4/13.md)\n    +   [十四、利用 UMG 和音频改善用户界面反馈](docs/learn-cpp-build-game-ue4/14.md)\n    +   [十五、虚拟现实及其他](docs/learn-cpp-build-game-ue4/15.md)\n+   [精通 C++ 游戏开发](docs/master-cpp-game-dev/README.md)\n    +   [零、前言](docs/master-cpp-game-dev/00.md)\n    +   [一、面向游戏开发的 C++ 语言](docs/master-cpp-game-dev/01.md)\n    +   [二、理解库](docs/master-cpp-game-dev/02.md)\n    +   [三、夯实基础](docs/master-cpp-game-dev/03.md)\n    +   [四、构建素材管道](docs/master-cpp-game-dev/04.md)\n    +   [五、构建游戏系统](docs/master-cpp-game-dev/05.md)\n    +   [六、创建图形用户界面](docs/master-cpp-game-dev/06.md)\n    +   [七、高级渲染](docs/master-cpp-game-dev/07.md)\n    +   [八、高级游戏系统](docs/master-cpp-game-dev/08.md)\n    +   [九、人工智能](docs/master-cpp-game-dev/09.md)\n    +   [十、多个玩家](docs/master-cpp-game-dev/10.md)\n    +   [十一、虚拟现实](docs/master-cpp-game-dev/11.md)\n+   [精通 C++ 编程](docs/master-cpp-prog/README.md)\n    +   [零、前言](docs/master-cpp-prog/00.md)\n    +   [一、C++ 17 特性](docs/master-cpp-prog/01.md)\n    +   [二、标准模板库](docs/master-cpp-prog/02.md)\n    +   [三、模板编程](docs/master-cpp-prog/03.md)\n    +   [四、智能指针](docs/master-cpp-prog/04.md)\n    +   [五、使用 C++ 开发图形用户界面应用](docs/master-cpp-prog/05.md)\n    +   [六、多线程编程和进程间通信](docs/master-cpp-prog/06.md)\n    +   [七、测试驱动开发](docs/master-cpp-prog/07.md)\n    +   [八、行为驱动开发](docs/master-cpp-prog/08.md)\n    +   [九、调试技术](docs/master-cpp-prog/09.md)\n    +   [十、代码异味和整洁的代码实践](docs/master-cpp-prog/10.md)\n+   [Qt5 C++ GUI 编程秘籍](docs/qt5-cpp-gui-prog-cb/README.md)\n    +   [零、前言](docs/qt5-cpp-gui-prog-cb/00.md)\n    +   [一、将 Qt 设计器用于外观定制](docs/qt5-cpp-gui-prog-cb/01.md)\n    +   [二、事件处理——信号和插槽](docs/qt5-cpp-gui-prog-cb/02.md)\n    +   [三、Qt 和 QML 的状态和动画](docs/qt5-cpp-gui-prog-cb/03.md)\n    +   [四、画家与 2D 图形](docs/qt5-cpp-gui-prog-cb/04.md)\n    +   [五、OpenGL 实现](docs/qt5-cpp-gui-prog-cb/05.md)\n    +   [六、使用网络和管理大型文档](docs/qt5-cpp-gui-prog-cb/06.md)\n    +   [七、线程基础——异步编程](docs/qt5-cpp-gui-prog-cb/07.md)\n    +   [八、使用 Qt5 构建触摸屏应用](docs/qt5-cpp-gui-prog-cb/08.md)\n    +   [九、XML 解析的简化](docs/qt5-cpp-gui-prog-cb/09.md)\n    +   [十、转换库](docs/qt5-cpp-gui-prog-cb/10.md)\n    +   [十一、使用 SQL 驱动和 Qt 访问数据库](docs/qt5-cpp-gui-prog-cb/11.md)\n    +   [十二、使用 Qt 网络引擎开发网络应用](docs/qt5-cpp-gui-prog-cb/12.md)\n    +   [十三、性能优化](docs/qt5-cpp-gui-prog-cb/13.md)\n+   [Qt Creator 应用开发](docs/app-dev-qt-creator/README.md)\n    +   [零、前言](docs/app-dev-qt-creator/00.md)\n    +   [第一部分：基础知识](docs/app-dev-qt-creator/sec1.md)\n        +   [一、Qt Creator 入门](docs/app-dev-qt-creator/01.md)\n        +   [二、使用 Qt Creator 构建应用](docs/app-dev-qt-creator/02.md)\n        +   [三、使用 Qt Designer 设计应用](docs/app-dev-qt-creator/03.md)\n        +   [四、Qt 基础](docs/app-dev-qt-creator/04.md)\n        +   [五、使用 Qt 小部件开发应用](docs/app-dev-qt-creator/05.md)\n    +   [第二部分：高级功能](docs/app-dev-qt-creator/sec2.md)\n        +   [六、使用 Qt 绘图](docs/app-dev-qt-creator/06.md)\n        +   [七、使用 Qt Quick 实现更多功能](docs/app-dev-qt-creator/07.md)\n        +   [八、使用 Qt Quick 实现多媒体](docs/app-dev-qt-creator/08.md)\n        +   [九、传感器和 Qt Quick](docs/app-dev-qt-creator/09.md)\n    +   [第三部分：实际事项](docs/app-dev-qt-creator/sec3.md)\n        +   [十、使用 Qt 语言学家本地化您的应用](docs/app-dev-qt-creator/10.md)\n        +   [十一、使用 Qt Creator 优化性能](docs/app-dev-qt-creator/11.md)\n        +   [十二、使用 Qt Creator 开发移动应用](docs/app-dev-qt-creator/12.md)\n        +   [十三、使用 Qt Creator 开发嵌入式和物联网](docs/app-dev-qt-creator/13.md)\n        +   [十四、QT 提示和技巧](docs/app-dev-qt-creator/14.md)\n+   [C++ 编程入门手册](docs/begin-cpp-prog/README.md)\n    +   [零、前言](docs/begin-cpp-prog/00.md)\n    +   [一、从 C++ 开始](docs/begin-cpp-prog/01.md)\n    +   [二、了解语言特性](docs/begin-cpp-prog/02.md)\n    +   [三、探索 C++ 类型](docs/begin-cpp-prog/03.md)\n    +   [四、使用内存、数组和指针](docs/begin-cpp-prog/04.md)\n    +   [五、使用函数](docs/begin-cpp-prog/05.md)\n    +   [六、类](docs/begin-cpp-prog/06.md)\n    +   [七、面向对象编程导论](docs/begin-cpp-prog/07.md)\n    +   [八、使用标准库容器](docs/begin-cpp-prog/08.md)\n    +   [九、使用字符串](docs/begin-cpp-prog/09.md)\n    +   [十、诊断和调试](docs/begin-cpp-prog/10.md)\n+   [现代 C++ 嵌入式编程秘籍](docs/emb-prog-mod-cpp-cb/README.md)\n    +   [零、前言](docs/emb-prog-mod-cpp-cb/00.md)\n    +   [一、嵌入式系统基础](docs/emb-prog-mod-cpp-cb/01.md)\n    +   [二、设置环境](docs/emb-prog-mod-cpp-cb/02.md)\n    +   [三、使用不同的架构](docs/emb-prog-mod-cpp-cb/03.md)\n    +   [四、处理中断](docs/emb-prog-mod-cpp-cb/04.md)\n    +   [五、调试、日志记录和性能分析](docs/emb-prog-mod-cpp-cb/05.md)\n    +   [六、内存管理](docs/emb-prog-mod-cpp-cb/06.md)\n    +   [七、多线程和同步](docs/emb-prog-mod-cpp-cb/07.md)\n    +   [八、通信和序列化](docs/emb-prog-mod-cpp-cb/08.md)\n    +   [九、外部设备](docs/emb-prog-mod-cpp-cb/09.md)\n    +   [十、降低功耗](docs/emb-prog-mod-cpp-cb/10.md)\n    +   [十一、时间点和间隔](docs/emb-prog-mod-cpp-cb/11.md)\n    +   [十二、错误处理和容错](docs/emb-prog-mod-cpp-cb/12.md)\n    +   [十三、实时系统指南](docs/emb-prog-mod-cpp-cb/13.md)\n    +   [十四、安全关键系统指南](docs/emb-prog-mod-cpp-cb/14.md)\n    +   [十五、微控制器编程](docs/emb-prog-mod-cpp-cb/15.md)\n+   [C++ 专家级编程](docs/exp-cpp-prog/README.md)\n    +   [零、新的 C++ 17 特性](docs/exp-cpp-prog/00.md)\n    +   [一、容器](docs/exp-cpp-prog/01.md)\n    +   [二、迭代器](docs/exp-cpp-prog/02.md)\n    +   [三、lambda 表达式](docs/exp-cpp-prog/03.md)\n    +   [四、STL 算法基础](docs/exp-cpp-prog/04.md)\n    +   [五、STL 算法的高级使用](docs/exp-cpp-prog/05.md)\n    +   [六、字符串、流类和正则表达式](docs/exp-cpp-prog/06.md)\n    +   [七、工具类](docs/exp-cpp-prog/07.md)\n    +   [八、并行性和并发性](docs/exp-cpp-prog/08.md)\n    +   [九、文件系统](docs/exp-cpp-prog/09.md)\n+   [UE 游戏开发项目](docs/game-dev-proj-ue/README.md)\n    +   [零、前言](docs/game-dev-proj-ue/00.md)\n    +   [一、虚幻引擎介绍](docs/game-dev-proj-ue/01.md)\n    +   [二、使用虚幻引擎](docs/game-dev-proj-ue/02.md)\n    +   [三、角色类组件和蓝图设置](docs/game-dev-proj-ue/03.md)\n    +   [四、玩家输入](docs/game-dev-proj-ue/04.md)\n    +   [五、线条痕迹](docs/game-dev-proj-ue/05.md)\n    +   [六、碰撞物体](docs/game-dev-proj-ue/06.md)\n    +   [八、用户界面](docs/game-dev-proj-ue/07.md)\n    +   [九、视听元素](docs/game-dev-proj-ue/08.md)\n    +   [十、创建`SuperSideScroller`游戏](docs/game-dev-proj-ue/09.md)\n    +   [十一、混合空间 1D、按键绑定和状态机](docs/game-dev-proj-ue/10.md)\n    +   [十二、动画混合和蒙太奇](docs/game-dev-proj-ue/11.md)\n    +   [十三、敌方人工智能](docs/game-dev-proj-ue/12.md)\n    +   [十四、产生玩家投射物](docs/game-dev-proj-ue/13.md)\n    +   [十五、收藏品、加强和拾取](docs/game-dev-proj-ue/14.md)\n    +   [十六、多人游戏基础](docs/game-dev-proj-ue/15.md)\n    +   [十七、远程过程调用](docs/game-dev-proj-ue/16.md)\n    +   [十八、多人游戏中的游戏框架类](docs/game-dev-proj-ue/17.md)\n+   [CUDA 编程学习手册](docs/learn-cuda-prog/README.md)\n    +   [零、前言](docs/learn-cuda-prog/00.md)\n    +   [一、CUDA 编程入门](docs/learn-cuda-prog/01.md)\n    +   [二、内存管理](docs/learn-cuda-prog/02.md)\n    +   [三、线程编程](docs/learn-cuda-prog/03.md)\n    +   [四、内核执行模型及优化策略](docs/learn-cuda-prog/04.md)\n    +   [五、应用分析和调试](docs/learn-cuda-prog/05.md)\n    +   [六、可扩展的多图形处理器编程](docs/learn-cuda-prog/06.md)\n    +   [七、CUDA 中的并行编程模式](docs/learn-cuda-prog/07.md)\n    +   [八、使用库和其他语言编程](docs/learn-cuda-prog/08.md)\n    +   [八、将 OpenACC 用于图形处理器编程](docs/learn-cuda-prog/09.md)\n    +   [九、利用 CUDA 实现深度学习加速](docs/learn-cuda-prog/10.md)\n    +   [十一、附录](docs/learn-cuda-prog/11.md)\n+   [WebAssembly 学习手册](docs/learn-wasm/README.md)\n    +   [零、前言](docs/learn-wasm/00.md)\n    +   [一、什么是 WebAssembly？](docs/learn-wasm/01.md)\n    +   [二、WebAssembly 的元素——Wat、Wasm 和 JavaScript 应用编程接口](docs/learn-wasm/02.md)\n    +   [三、建立开发环境](docs/learn-wasm/03.md)\n    +   [四、安装所需的依赖项](docs/learn-wasm/04.md)\n    +   [五、创建和加载 WebAssembly 模块](docs/learn-wasm/05.md)\n    +   [六、与 JavaScript 交互和调试](docs/learn-wasm/06.md)\n    +   [七、从头开始创建应用](docs/learn-wasm/07.md)\n    +   [八、使用电子脚本移植游戏](docs/learn-wasm/08.md)\n    +   [九、与 Node.js 集成](docs/learn-wasm/09.md)\n    +   [十、高级工具和即将推出的功能](docs/learn-wasm/10.md)\n+   [精通 C++ 多线程](docs/master-cpp-multithrd/README.md)\n    +   [零、前言](docs/master-cpp-multithrd/00.md)\n    +   [一、重温多线程](docs/master-cpp-multithrd/01.md)\n    +   [二、处理器和操作系统上的多线程实现](docs/master-cpp-multithrd/02.md)\n    +   [三、C++ 多线程应用编程接口](docs/master-cpp-multithrd/03.md)\n    +   [四、线程同步和通信](docs/master-cpp-multithrd/04.md)\n    +   [五、本机 C++ 线程和原语](docs/master-cpp-multithrd/05.md)\n    +   [六、调试多线程代码](docs/master-cpp-multithrd/06.md)\n    +   [七、最佳实践](docs/master-cpp-multithrd/07.md)\n    +   [八、原子操作——使用硬件](docs/master-cpp-multithrd/08.md)\n    +   [九、分布式计算中的多线程](docs/master-cpp-multithrd/09.md)\n    +   [十、图形处理器多线程](docs/master-cpp-multithrd/10.md)\n+   [现代 C++ 编程](docs/mod-cpp/README.md)\n    +   [零、前言](docs/mod-cpp/00.md)\n    +   [一、理解语言特性](docs/mod-cpp/01.md)\n    +   [二、使用内存、数组和指针](docs/mod-cpp/02.md)\n    +   [三、使用函数](docs/mod-cpp/03.md)\n    +   [四、类](docs/mod-cpp/04.md)\n    +   [五、使用标准库容器](docs/mod-cpp/05.md)\n    +   [六、使用字符串](docs/mod-cpp/06.md)\n    +   [七、诊断和调试](docs/mod-cpp/07.md)\n    +   [八、学习现代核心语言特性](docs/mod-cpp/08.md)\n    +   [九、使用数字和字符串](docs/mod-cpp/09.md)\n    +   [十、探索函数](docs/mod-cpp/10.md)\n    +   [十一、标准库容器、算法和迭代器](docs/mod-cpp/11.md)\n    +   [十二、数学问题](docs/mod-cpp/12.md)\n    +   [十三、语言特性](docs/mod-cpp/13.md)\n    +   [十四、字符串和正则表达式](docs/mod-cpp/14.md)\n    +   [十五、流和文件系统](docs/mod-cpp/15.md)\n    +   [十六、日期和时间](docs/mod-cpp/16.md)\n    +   [十七、算法和数据结构](docs/mod-cpp/17.md)\n+   [现代 C++ 的挑战](docs/mod-cpp-challenge/README.md)\n    +   [零、前言](docs/mod-cpp-challenge/00.md)\n    +   [一、数学问题](docs/mod-cpp-challenge/01.md)\n    +   [二、语言特性](docs/mod-cpp-challenge/02.md)\n    +   [三、字符串和正则表达式](docs/mod-cpp-challenge/03.md)\n    +   [四、流和文件系统](docs/mod-cpp-challenge/04.md)\n    +   [五、日期和时间](docs/mod-cpp-challenge/05.md)\n    +   [六、算法和数据结构](docs/mod-cpp-challenge/06.md)\n    +   [七、并发](docs/mod-cpp-challenge/07.md)\n    +   [八、设计模式](docs/mod-cpp-challenge/08.md)\n    +   [九、数据序列化](docs/mod-cpp-challenge/09.md)\n    +   [十、归档、图像和数据库](docs/mod-cpp-challenge/10.md)\n    +   [十一、密码系统](docs/mod-cpp-challenge/11.md)\n    +   [十二、网络和服务](docs/mod-cpp-challenge/12.md)\n    +   [十三、参考文献](docs/mod-cpp-challenge/13.md)\n+   [C++ 游戏编程入门手册](docs/begin-cpp-game-prog/README.md)\n    +   [零、序言](docs/begin-cpp-game-prog/00.md)\n    +   [一、C++，SFML，VisualStudio，并开始第一个游戏](docs/begin-cpp-game-prog/01.md)\n    +   [二、变量、运算符和决策——设置精灵动画](docs/begin-cpp-game-prog/02.md)\n    +   [三、C++ 字符串和 SFML 时间——玩家输入和 HUD](docs/begin-cpp-game-prog/03.md)\n    +   [四、循环、数组、`switch`、枚举和函数——实现游戏机制](docs/begin-cpp-game-prog/04.md)\n    +   [五、碰撞、声音和结束条件——使游戏可玩](docs/begin-cpp-game-prog/05.md)\n    +   [六、面向对象编程——启动乒乓球游戏](docs/begin-cpp-game-prog/06.md)\n    +   [七、动态碰撞检测与物理——完成乒乓球游戏](docs/begin-cpp-game-prog/07.md)\n    +   [八、SFML 视图——开始僵尸射击游戏](docs/begin-cpp-game-prog/08.md)\n    +   [九、C++ 引用、精灵列表和顶点数组](docs/begin-cpp-game-prog/09.md)\n    +   [十、指针、标准模板库、纹理管理](docs/begin-cpp-game-prog/10.md)\n    +   [十一、碰撞检测，拾音器和子弹](docs/begin-cpp-game-prog/11.md)\n    +   [十二、视图分层与 HUD 实现](docs/begin-cpp-game-prog/12.md)\n    +   [十三、音效，文件 I/O，完成游戏](docs/begin-cpp-game-prog/13.md)\n    +   [十四、抽象和代码管理——更好地利用面向对象](docs/begin-cpp-game-prog/14.md)\n    +   [十五、高级 OOP——继承与多态](docs/begin-cpp-game-prog/15.md)\n    +   [十六、建造可玩关卡和碰撞检测](docs/begin-cpp-game-prog/16.md)\n    +   [十七、声音空间化和平视显示器](docs/begin-cpp-game-prog/17.md)\n    +   [十八、粒子系统和着色器](docs/begin-cpp-game-prog/18.md)\n    +   [十九、游戏编程设计模式——启动太空入侵者 ++ 游戏](docs/begin-cpp-game-prog/19.md)\n    +   [二十、游戏对象和组件](docs/begin-cpp-game-prog/20.md)\n    +   [二十一、文件输入输出和游戏对象工厂](docs/begin-cpp-game-prog/21.md)\n    +   [二十二、使用游戏对象和构建游戏](docs/begin-cpp-game-prog/22.md)\n    +   [二十三、结束之前](docs/begin-cpp-game-prog/23.md)\n+   [Boost.Asio C++ 网络编程入门中文第二版](docs/boost-asio-cpp-net-prog-2e/README.md)\n    +   [零、前言](docs/boost-asio-cpp-net-prog-2e/0.md)\n    +   [一、使用 C++ 简化您的网络编程](docs/boost-asio-cpp-net-prog-2e/1.md)\n    +   [二、理解网络概念](docs/boost-asio-cpp-net-prog-2e/2.md)\n    +   [三、Boost C++ 库简介](docs/boost-asio-cpp-net-prog-2e/3.md)\n    +   [四、Boost.Asio 入门](docs/boost-asio-cpp-net-prog-2e/4.md)\n    +   [五、深入研究 Boost.Asio 库](docs/boost-asio-cpp-net-prog-2e/5.md)\n    +   [六、创建客户端——服务器应用](docs/boost-asio-cpp-net-prog-2e/6.md)\n    +   [七、调试代码并解决错误](docs/boost-asio-cpp-net-prog-2e/7.md)\n+   [Boost C++ 应用开发秘籍](docs/boost-cpp-app-dev-cb/README.md)\n    +   [零、前言](docs/boost-cpp-app-dev-cb/00.md)\n    +   [一、开始编写应用](docs/boost-cpp-app-dev-cb/01.md)\n    +   [二、管理资源](docs/boost-cpp-app-dev-cb/02.md)\n    +   [三、类型转换](docs/boost-cpp-app-dev-cb/03.md)\n    +   [四、编译时技巧](docs/boost-cpp-app-dev-cb/04.md)\n    +   [五、多线程操作](docs/boost-cpp-app-dev-cb/05.md)\n    +   [六、操作任务](docs/boost-cpp-app-dev-cb/06.md)\n    +   [七、操纵字符串](docs/boost-cpp-app-dev-cb/07.md)\n    +   [八、元编程](docs/boost-cpp-app-dev-cb/08.md)\n    +   [九、容器](docs/boost-cpp-app-dev-cb/09.md)\n    +   [十、收集平台和编译器信息](docs/boost-cpp-app-dev-cb/10.md)\n    +   [十一、使用系统](docs/boost-cpp-app-dev-cb/11.md)\n    +   [十二、Boost 的冰山一角](docs/boost-cpp-app-dev-cb/12.md)\n+   [C++ 数据结构和算法设计原则](docs/cpp-dsal-design-principle/README.md)\n    +   [零、前言](docs/cpp-dsal-design-principle/00.md)\n    +   [一、列表、栈和队列](docs/cpp-dsal-design-principle/01.md)\n    +   [二、树、堆和图](docs/cpp-dsal-design-principle/02.md)\n    +   [三、哈希表和布隆过滤器](docs/cpp-dsal-design-principle/03.md)\n    +   [四、分治法](docs/cpp-dsal-design-principle/04.md)\n    +   [五、贪婪算法](docs/cpp-dsal-design-principle/05.md)\n    +   [六、图算法 1](docs/cpp-dsal-design-principle/06.md)\n    +   [七、图算法 2](docs/cpp-dsal-design-principle/07.md)\n    +   [八、动态规划一](docs/cpp-dsal-design-principle/08.md)\n    +   [九、动态规划二](docs/cpp-dsal-design-principle/09.md)\n    +   [十、附录](docs/cpp-dsal-design-principle/10.md)\n+   [C++ 高性能编程](docs/cpp-hiperf/README.md)\n    +   [零、前言](docs/cpp-hiperf/00.md)\n    +   [一、C++ 简介](docs/cpp-hiperf/01.md)\n    +   [二、基本的 C++ 技术](docs/cpp-hiperf/02.md)\n    +   [三、分析和测量性能](docs/cpp-hiperf/03.md)\n    +   [四、数据结构](docs/cpp-hiperf/04.md)\n    +   [五、算法](docs/cpp-hiperf/05.md)\n    +   [六、范围和视图](docs/cpp-hiperf/06.md)\n    +   [七、内存管理](docs/cpp-hiperf/07.md)\n    +   [八、编译时编程](docs/cpp-hiperf/08.md)\n    +   [九、基本工具](docs/cpp-hiperf/09.md)\n    +   [十、代理对象和延迟求值](docs/cpp-hiperf/10.md)\n    +   [十一、并发](docs/cpp-hiperf/11.md)\n    +   [十二、协程和延迟生成器](docs/cpp-hiperf/12.md)\n    +   [十三、使用协程的异步编程](docs/cpp-hiperf/13.md)\n    +   [十四、并行算法](docs/cpp-hiperf/14.md)\n+   [C++ 反应式编程](docs/cpp-react-prog/README.md)\n    +   [零、前言](docs/cpp-react-prog/00.md)\n    +   [一、反应式编程模型——概述和历史](docs/cpp-react-prog/01.md)\n    +   [二、现代 C++ 及其关键习语概述](docs/cpp-react-prog/02.md)\n    +   [三、C++ 中的语言级并发和并行](docs/cpp-react-prog/03.md)\n    +   [四、C++ 中的异步和无锁编程](docs/cpp-react-prog/04.md)\n    +   [五、可观察对象介绍](docs/cpp-react-prog/05.md)\n    +   [六、C++ 事件流编程简介](docs/cpp-react-prog/06.md)\n    +   [七、数据流计算和 RxCpp 库简介](docs/cpp-react-prog/07.md)\n    +   [八、关键要素](docs/cpp-react-prog/08.md)\n    +   [九、Qt/C++ 反应式图形用户界面编程](docs/cpp-react-prog/09.md)\n    +   [十、C++ 反应式编程的设计模式和习惯用法](docs/cpp-react-prog/10.md)\n    +   [十一、使用 C++ 的反应式微服务](docs/cpp-react-prog/11.md)\n    +   [十二、高级流和错误处理](docs/cpp-react-prog/12.md)\n+   [C++ 系统编程秘籍](docs/cpp-sys-prog-cb/README.md)\n    +   [零、前言](docs/cpp-sys-prog-cb/00.md)\n    +   [一、系统编程入门](docs/cpp-sys-prog-cb/01.md)\n    +   [二、重温 C++](docs/cpp-sys-prog-cb/02.md)\n    +   [三、处理进程和线程](docs/cpp-sys-prog-cb/03.md)\n    +   [四、深入探讨内存管理](docs/cpp-sys-prog-cb/04.md)\n    +   [五、使用互斥、信号量和条件变量](docs/cpp-sys-prog-cb/05.md)\n    +   [六、管道、先进先出、消息队列和共享内存](docs/cpp-sys-prog-cb/06.md)\n    +   [七、网络编程](docs/cpp-sys-prog-cb/07.md)\n    +   [八、处理控制台输入/输出和文件](docs/cpp-sys-prog-cb/08.md)\n    +   [九、处理时间接口](docs/cpp-sys-prog-cb/09.md)\n    +   [十、管理信号](docs/cpp-sys-prog-cb/10.md)\n    +   [十一、调度编排](docs/cpp-sys-prog-cb/11.md)\n+   [C++ 工作室](docs/cpp-workshop/README.md)\n    +   [零、前言](docs/cpp-workshop/00.md)\n    +   [一、您的第一个 C++ 应用](docs/cpp-workshop/01.md)\n    +   [二、控制流](docs/cpp-workshop/02.md)\n    +   [三、内置数据类型](docs/cpp-workshop/03.md)\n    +   [四、运算符](docs/cpp-workshop/04.md)\n    +   [五、指针和引用](docs/cpp-workshop/05.md)\n    +   [六、动态变量](docs/cpp-workshop/06.md)\n    +   [七、动态变量的所有权和寿命](docs/cpp-workshop/07.md)\n    +   [八、类和结构](docs/cpp-workshop/08.md)\n    +   [九、面向对象原则](docs/cpp-workshop/09.md)\n    +   [十、高级面向对象原则](docs/cpp-workshop/10.md)\n    +   [十一、模板](docs/cpp-workshop/11.md)\n    +   [十二、容器和迭代器](docs/cpp-workshop/12.md)\n    +   [十三、C++ 中的异常处理](docs/cpp-workshop/13.md)\n    +   [十四、附录](docs/cpp-workshop/14.md)\n+   [WebAssembly 游戏编程实用指南](docs/handson-game-dev-wasm/README.md)\n    +   [零、前言](docs/handson-game-dev-wasm/00.md)\n    +   [一、WebAssembly 和电子脚本简介](docs/handson-game-dev-wasm/01.md)\n    +   [二、HTML5 和 WebAssembly](docs/handson-game-dev-wasm/02.md)\n    +   [三、WebGL 简介](docs/handson-game-dev-wasm/03.md)\n    +   [四、WebAssembly 中使用 SDL 的的精灵动画](docs/handson-game-dev-wasm/04.md)\n    +   [五、键盘输入](docs/handson-game-dev-wasm/05.md)\n    +   [六、游戏对象和游戏循环](docs/handson-game-dev-wasm/06.md)\n    +   [七、碰撞检测](docs/handson-game-dev-wasm/07.md)\n    +   [八、基本粒子系统](docs/handson-game-dev-wasm/08.md)\n    +   [九、改进的粒子系统](docs/handson-game-dev-wasm/09.md)\n    +   [十、人工智能与驾驶行为](docs/handson-game-dev-wasm/10.md)\n    +   [十一、设计 2D 相机](docs/handson-game-dev-wasm/11.md)\n    +   [十二、声音 FX](docs/handson-game-dev-wasm/12.md)\n    +   [十三、游戏物理](docs/handson-game-dev-wasm/13.md)\n    +   [十四、用户界面和鼠标输入](docs/handson-game-dev-wasm/14.md)\n    +   [十五、着色器和 2D 照明](docs/handson-game-dev-wasm/15.md)\n    +   [十六、调试和优化](docs/handson-game-dev-wasm/16.md)\n+   [C++ 函数式编程学习手册](docs/learn-cpp-func-prog/README.md)\n    +   [零、前言](docs/learn-cpp-func-prog/0.md)\n    +   [一、深入现代 C++](docs/learn-cpp-func-prog/1.md)\n    +   [二、函数式编程中的函数操作](docs/learn-cpp-func-prog/2.md)\n    +   [三、将不可变状态应用于函数](docs/learn-cpp-func-prog/3.md)\n    +   [四、使用递归算法重复方法调用](docs/learn-cpp-func-prog/4.md)\n    +   [五、使用延迟求值拖延执行过程](docs/learn-cpp-func-prog/5.md)\n    +   [六、使用元编程优化代码](docs/learn-cpp-func-prog/6.md)\n    +   [七、使用并发运行并行执行](docs/learn-cpp-func-prog/7.md)\n    +   [八、使用函数方法创建和调试应用](docs/learn-cpp-func-prog/8.md)\n+   [Qt5 学习手册](docs/learn-qt5/README.md)\n    +   [零、前言](docs/learn-qt5/0.md)\n    +   [一、你好，Qt](docs/learn-qt5/1.md)\n    +   [二、项目结构](docs/learn-qt5/2.md)\n    +   [三、用户界面](docs/learn-qt5/3.md)\n    +   [四、样式](docs/learn-qt5/4.md)\n    +   [五、数据](docs/learn-qt5/5.md)\n    +   [六、单元测试](docs/learn-qt5/6.md)\n    +   [七、SQLite](docs/learn-qt5/7.md)\n    +   [八、网络请求](docs/learn-qt5/8.md)\n    +   [九、打包](docs/learn-qt5/9.md)\n"
  },
  {
    "path": "asset/back-to-top.css",
    "content": "#scroll-btn {\n    position: fixed;\n    right: 15px;\n    bottom: 10px;\n    width: 35px;\n    height: 35px;\n    background-repeat: no-repeat;\n    background-size: cover;\n    cursor: pointer;\n    -webkit-user-select: none;\n    -moz-user-select: none;\n    -ms-user-select: none;\n    user-select: none;\n    background-image: url(up.svg);\n\tbackground-position-y: -1px;\n\tdisplay: none;\n\tborder: 2px solid;\n\tborder-radius: 4px;\t\n}"
  },
  {
    "path": "asset/back-to-top.js",
    "content": "document.addEventListener('DOMContentLoaded', function() {\n\tvar scrollBtn = document.createElement('div')\n\tscrollBtn.id = 'scroll-btn'\n\tdocument.body.append(scrollBtn)\n\t\n\twindow.addEventListener('scroll', function() {\n\t\tvar offset = window.document.documentElement.scrollTop;\n        scrollBtn.style.display = offset >= 500 ? \"block\" : \"none\";\n\t})\n\tscrollBtn.addEventListener('click', function(e) {\n\t\te.stopPropagation();\n\t\tvar step = window.scrollY / 15;\n\t\tvar hdl = setInterval(function() {\n\t\t\twindow.scrollTo(0, window.scrollY - step);\n\t\t\tif(window.scrollY <= 0) {\n\t\t\t\tclearInterval(hdl)\n\t\t\t}\n\t\t}, 15)\n\t})\n})"
  },
  {
    "path": "asset/dark-mode.css",
    "content": "#dark-mode-btn {\n\tposition: fixed;\n\tright: 15px;\n\ttop: 100px;\n\twidth: 35px;\n\theight: 35px;\n\tbackground-repeat: no-repeat;\n\tbackground-size: cover;\n\tcursor: pointer;\n\t-webkit-user-select: none;\n\t-moz-user-select: none;\n\t-ms-user-select: none;\n\tuser-select: none;\n\ttransition: background-image .15s ease-in-out .15s;\n}\n\n.dark-logo {\t\n\tbackground-image: url('sun.svg');\t\n}\n\n.light-logo {\n\tbackground-image: url('moon.svg');\n}"
  },
  {
    "path": "asset/dark-mode.js",
    "content": "document.addEventListener('DOMContentLoaded', function() {\n\tvar style = document.querySelector('#invert')\n\tif (style == null) {\n\t\tstyle = document.createElement('style')\n\t\tstyle.id = 'invert'\n\t\tdocument.head.append(style)\n\t}\n\tvar btn = document.querySelector('#dark-mode-btn')\n\tif (btn == null) {\n\t\tbtn = document.createElement('div')\n\t\tbtn.id = 'dark-mode-btn'\n\t\tbtn.classList.add('light-logo')\n\t\tdocument.body.append(btn)\n\t}\n\t\n\tvar enableDarkMode = function() {\n\t\tstyle.innerText = 'html,img,pre,#dark-mode-btn{filter:invert(100%)}'\n\t\tbtn.classList.remove('light-logo')\n\t\tbtn.classList.add('dark-logo')\n\t\tlocalStorage.darkLight = 'dark'\n\t\t\n\t}\n\tvar disableDarkMode = function() {\n\t\tstyle.innerText = ''\t\t\n\t\tbtn.classList.remove('dark-logo')\n\t\tbtn.classList.add('light-logo')\n\t\tlocalStorage.darkLight = 'light'\n\t}\n\t\n\tbtn.addEventListener('click', function(){\n\t\tvar currMode = localStorage.darkLight || 'light'\n\t\tif (currMode == 'light')\n\t\t\tenableDarkMode()\n\t\telse \n\t\t\tdisableDarkMode()\n\t})\n\t\n\tif (localStorage.darkLight == 'dark')\n\t\tenableDarkMode()\n\t\n})\n\n"
  },
  {
    "path": "asset/docsify-apachecn-footer.js",
    "content": "(function(){\n\tvar cnzzId = window.$docsify.cnzzId\n\tvar unRepo = window.$docsify.repo || ''\n\tvar [un, repo] = unRepo.split('/')\n    var footer = `\n        <hr/>\n        <div align=\"center\">\n          <p><a href=\"http://www.apachecn.org/\" target=\"_blank\"><font face=\"KaiTi\" size=\"6\" color=\"red\">我们一直在努力</font></a><p>\n          <p><a href=\"https://github.com/${unRepo}\" target=\"_blank\">${unRepo}</a></p>\n          <p><iframe align=\"middle\" src=\"https://ghbtns.com/github-btn.html?user=${un}&repo=${repo}&type=watch&count=true&v=2\" frameborder=\"0\" scrolling=\"0\" width=\"100px\" height=\"25px\"></iframe>\n          <iframe align=\"middle\" src=\"https://ghbtns.com/github-btn.html?user=${un}&repo=${repo}&type=star&count=true\" frameborder=\"0\" scrolling=\"0\" width=\"100px\" height=\"25px\"></iframe>\n          <iframe align=\"middle\" src=\"https://ghbtns.com/github-btn.html?user=${un}&repo=${repo}&type=fork&count=true\" frameborder=\"0\" scrolling=\"0\" width=\"100px\" height=\"25px\"></iframe>\n          <a target=\"_blank\" href=\"https://jq.qq.com/?_wv=1027&k=fgYM7eMw\"><img border=\"0\" src=\"//pub.idqqimg.com/wpa/images/group.png\" alt=\"iBooker 面试求职\" title=\"iBooker 面试求职\"></a></p>\n          <p><span id=\"cnzz_stat_icon_${cnzzId}\"></span></p>\n          <div style=\"text-align:center;margin:0 0 10.5px;\">\n            <ins class=\"adsbygoogle\"\n                 style=\"display:inline-block;width:728px;height:90px\"\n                 data-ad-client=\"ca-pub-3565452474788507\"\n                 data-ad-slot=\"2543897000\"></ins>\n          </div>\n        </div>\n\t`\n    var plugin = function(hook) {\n      hook.afterEach(function(html) {\n        return html + footer\n      })\n      hook.doneEach(function() {\n        (adsbygoogle = window.adsbygoogle || []).push({})\n      })\n    }\n    var plugins = window.$docsify.plugins || []\n    plugins.push(plugin)\n    window.$docsify.plugins = plugins\n})()"
  },
  {
    "path": "asset/docsify-baidu-push.js",
    "content": "(function(){\n    var plugin = function(hook) {\n        hook.doneEach(function() {\n            new Image().src = \n                '//api.share.baidu.com/s.gif?r=' + \n                encodeURIComponent(document.referrer) + \n                \"&l=\" + encodeURIComponent(location.href)\n        })\n    }\n    var plugins = window.$docsify.plugins || []\n    plugins.push(plugin)\n    window.$docsify.plugins = plugins\n})()"
  },
  {
    "path": "asset/docsify-baidu-stat.js",
    "content": "(function(){\n    var plugin = function(hook) {\n        hook.doneEach(function() {\n            window._hmt = window._hmt || []\n            var hm = document.createElement(\"script\")\n            hm.src = \"https://hm.baidu.com/hm.js?\" + window.$docsify.bdStatId\n            document.querySelector(\"article\").appendChild(hm)\n        })\n    }\n    var plugins = window.$docsify.plugins || []\n    plugins.push(plugin)\n    window.$docsify.plugins = plugins\n})()"
  },
  {
    "path": "asset/docsify-clicker.js",
    "content": "(function() {\n    var ids = [\n        '109577065', '108852955', '102682374', '100520874', '92400861', '90312982', \n        '109963325', '109323014', '109301511', '108898970', '108590722', '108538676', \n        '108503526', '108437109', '108402202', '108292691', '108291153', '108268498', \n        '108030854', '107867070', '107847299', '107827334', '107825454', '107802131', \n        '107775320', '107752974', '107735139', '107702571', '107598864', '107584507', \n        '107568311', '107526159', '107452391', '107437455', '107430050', '107395781', \n        '107325304', '107283210', '107107145', '107085440', '106995421', '106993460', \n        '106972215', '106959775', '106766787', '106749609', '106745967', '106634313', \n        '106451602', '106180097', '106095505', '106077010', '106008089', '106002346', \n        '105653809', '105647855', '105130705', '104837872', '104706815', '104192620', \n        '104074941', '104040537', '103962171', '103793502', '103783460', '103774572', \n        '103547748', '103547703', '103547571', '103490757', '103413481', '103341935', \n        '103330191', '103246597', '103235808', '103204403', '103075981', '103015105', \n        '103014899', '103014785', '103014702', '103014540', '102993780', '102993754', \n        '102993680', '102958443', '102913317', '102903382', '102874766', '102870470', \n        '102864513', '102811179', '102761237', '102711565', '102645443', '102621845', \n        '102596167', '102593333', '102585262', '102558427', '102537547', '102530610', \n        '102527017', '102504698', '102489806', '102372981', '102258897', '102257303', \n        '102056248', '101920097', '101648638', '101516708', '101350577', '101268149', \n        '101128167', '101107328', '101053939', '101038866', '100977414', '100945061', \n        '100932401', '100886407', '100797378', '100634918', '100588305', '100572447', \n        '100192249', '100153559', '100099032', '100061455', '100035392', '100033450', \n        '99671267', '99624846', '99172551', '98992150', '98989508', '98987516', '98938304', \n        '98937682', '98725145', '98521688', '98450861', '98306787', '98203342', '98026348', \n        '97680167', '97492426', '97108940', '96888872', '96568559', '96509100', '96508938', \n        '96508611', '96508374', '96498314', '96476494', '96333593', '96101522', '95989273', \n        '95960507', '95771870', '95770611', '95766810', '95727700', '95588929', '95218707', \n        '95073151', '95054615', '95016540', '94868371', '94839549', '94719281', '94401578', \n        '93931439', '93853494', '93198026', '92397889', '92063437', '91635930', '91433989', \n        '91128193', '90915507', '90752423', '90738421', '90725712', '90725083', '90722238', \n        '90647220', '90604415', '90544478', '90379769', '90288341', '90183695', '90144066', \n        '90108283', '90021771', '89914471', '89876284', '89852050', '89839033', '89812373', \n        '89789699', '89786189', '89752620', '89636380', '89632889', '89525811', '89480625', \n        '89464088', '89464025', '89463984', '89463925', '89445280', '89441793', '89430432', \n        '89429877', '89416176', '89412750', '89409618', '89409485', '89409365', '89409292', \n        '89409222', '89399738', '89399674', '89399526', '89355336', '89330241', '89308077', \n        '89222240', '89140953', '89139942', '89134398', '89069355', '89049266', '89035735', \n        '89004259', '88925790', '88925049', '88915838', '88912706', '88911548', '88899438', \n        '88878890', '88837519', '88832555', '88824257', '88777952', '88752158', '88659061', \n        '88615256', '88551434', '88375675', '88322134', '88322085', '88321996', '88321978', \n        '88321950', '88321931', '88321919', '88321899', '88321830', '88321756', '88321710', \n        '88321661', '88321632', '88321566', '88321550', '88321506', '88321475', '88321440', \n        '88321409', '88321362', '88321321', '88321293', '88321226', '88232699', '88094874', \n        '88090899', '88090784', '88089091', '88048808', '87938224', '87913318', '87905933', \n        '87897358', '87856753', '87856461', '87827666', '87822008', '87821456', '87739137', \n        '87734022', '87643633', '87624617', '87602909', '87548744', '87548689', '87548624', \n        '87548550', '87548461', '87463201', '87385913', '87344048', '87078109', '87074784', \n        '87004367', '86997632', '86997466', '86997303', '86997116', '86996474', '86995899', \n        '86892769', '86892654', '86892569', '86892457', '86892347', '86892239', '86892124', \n        '86798671', '86777307', '86762845', '86760008', '86759962', '86759944', '86759930', \n        '86759922', '86759646', '86759638', '86759633', '86759622', '86759611', '86759602', \n        '86759596', '86759591', '86759580', '86759572', '86759567', '86759558', '86759545', \n        '86759534', '86749811', '86741502', '86741074', '86741059', '86741020', '86740897', \n        '86694754', '86670104', '86651882', '86651875', '86651866', '86651828', '86651790', \n        '86651767', '86651756', '86651735', '86651720', '86651708', '86618534', '86618526', \n        '86594785', '86590937', '86550497', '86550481', '86550472', '86550453', '86550438', \n        '86550429', '86550407', '86550381', '86550359', '86536071', '86536035', '86536014', \n        '86535988', '86535963', '86535953', '86535932', '86535902', '86472491', '86472298', \n        '86472236', '86472191', '86472108', '86471967', '86471899', '86471822', '86439022', \n        '86438972', '86438902', '86438887', '86438867', '86438836', '86438818', '85850119', \n        '85850075', '85850021', '85849945', '85849893', '85849837', '85849790', '85849740', \n        '85849661', '85849620', '85849550', '85606096', '85564441', '85547709', '85471981', \n        '85471317', '85471136', '85471073', '85470629', '85470456', '85470169', '85469996', \n        '85469877', '85469775', '85469651', '85469331', '85469033', '85345768', '85345742', \n        '85337900', '85337879', '85337860', '85337833', '85337797', '85322822', '85322810', \n        '85322791', '85322745', '85317667', '85265742', '85265696', '85265618', '85265350', \n        '85098457', '85057670', '85009890', '84755581', '84637437', '84637431', '84637393', \n        '84637374', '84637355', '84637338', '84637321', '84637305', '84637283', '84637259', \n        '84629399', '84629314', '84629233', '84629124', '84629065', '84628997', '84628933', \n        '84628838', '84628777', '84628690', '84591581', '84591553', '84591511', '84591484', \n        '84591468', '84591416', '84591386', '84591350', '84591308', '84572155', '84572107', \n        '84503228', '84500221', '84403516', '84403496', '84403473', '84403442', '84075703', \n        '84029659', '83933480', '83933459', '83933435', '83903298', '83903274', '83903258', \n        '83752369', '83345186', '83116487', '83116446', '83116402', '83116334', '83116213', \n        '82944248', '82941023', '82938777', '82936611', '82932735', '82918102', '82911085', \n        '82888399', '82884263', '82883507', '82880996', '82875334', '82864060', '82831039', \n        '82823385', '82795277', '82790832', '82775718', '82752022', '82730437', '82718126', \n        '82661646', '82588279', '82588267', '82588261', '82588192', '82347066', '82056138', \n        '81978722', '81211571', '81104145', '81069048', '81006768', '80788365', '80767582', \n        '80759172', '80759144', '80759129', '80736927', '80661288', '80616304', '80602366', \n        '80584625', '80561364', '80549878', '80549875', '80541470', '80539726', '80531328', \n        '80513257', '80469816', '80406810', '80356781', '80334130', '80333252', '80332666', \n        '80332389', '80311244', '80301070', '80295974', '80292252', '80286963', '80279504', \n        '80278369', '80274371', '80249825', '80247284', '80223054', '80219559', '80209778', \n        '80200279', '80164236', '80160900', '80153046', '80149560', '80144670', '80061205', \n        '80046520', '80025644', '80014721', '80005213', '80004664', '80001653', '79990178', \n        '79989283', '79947873', '79946002', '79941517', '79938786', '79932755', '79921178', \n        '79911339', '79897603', '79883931', '79872574', '79846509', '79832150', '79828161', \n        '79828156', '79828149', '79828146', '79828140', '79828139', '79828135', '79828123', \n        '79820772', '79776809', '79776801', '79776788', '79776782', '79776772', '79776767', \n        '79776760', '79776753', '79776736', '79776705', '79676183', '79676171', '79676166', \n        '79676160', '79658242', '79658137', '79658130', '79658123', '79658119', '79658112', \n        '79658100', '79658092', '79658089', '79658069', '79658054', '79633508', '79587857', \n        '79587850', '79587842', '79587831', '79587825', '79587819', '79547908', '79477700', \n        '79477692', '79440956', '79431176', '79428647', '79416896', '79406699', '79350633', \n        '79350545', '79344765', '79339391', '79339383', '79339157', '79307345', '79293944', \n        '79292623', '79274443', '79242798', '79184420', '79184386', '79184355', '79184269', \n        '79183979', '79100314', '79100206', '79100064', '79090813', '79057834', '78967246', \n        '78941571', '78927340', '78911467', '78909741', '78848006', '78628917', '78628908', \n        '78628889', '78571306', '78571273', '78571253', '78508837', '78508791', '78448073', \n        '78430940', '78408150', '78369548', '78323851', '78314301', '78307417', '78300457', \n        '78287108', '78278945', '78259349', '78237192', '78231360', '78141031', '78100357', \n        '78095793', '78084949', '78073873', '78073833', '78067868', '78067811', '78055014', \n        '78041555', '78039240', '77948804', '77879624', '77837792', '77824937', '77816459', \n        '77816208', '77801801', '77801767', '77776636', '77776610', '77505676', '77485156', \n        '77478296', '77460928', '77327521', '77326428', '77278423', '77258908', '77252370', \n        '77248841', '77239042', '77233843', '77230880', '77200256', '77198140', '77196405', \n        '77193456', '77186557', '77185568', '77181823', '77170422', '77164604', '77163389', \n        '77160103', '77159392', '77150721', '77146204', '77141824', '77129604', '77123259', \n        '77113014', '77103247', '77101924', '77100165', '77098190', '77094986', '77088637', \n        '77073399', '77062405', '77044198', '77036923', '77017092', '77007016', '76999924', \n        '76977678', '76944015', '76923087', '76912696', '76890184', '76862282', '76852434', \n        '76829683', '76794256', '76780755', '76762181', '76732277', '76718569', '76696048', \n        '76691568', '76689003', '76674746', '76651230', '76640301', '76615315', '76598528', \n        '76571947', '76551820', '74178127', '74157245', '74090991', '74012309', '74001789', \n        '73910511', '73613471', '73605647', '73605082', '73503704', '73380636', '73277303', \n        '73274683', '73252108', '73252085', '73252070', '73252039', '73252025', '73251974', \n        '73135779', '73087531', '73044025', '73008658', '72998118', '72997953', '72847091', \n        '72833384', '72830909', '72828999', '72823633', '72793092', '72757626', '71157154', \n        '71131579', '71128551', '71122253', '71082760', '71078326', '71075369', '71057216', \n        '70812997', '70384625', '70347260', '70328937', '70313267', '70312950', '70255825', \n        '70238893', '70237566', '70237072', '70230665', '70228737', '70228729', '70175557', \n        '70175401', '70173259', '70172591', '70170835', '70140724', '70139606', '70053923', \n        '69067886', '69063732', '69055974', '69055708', '69031254', '68960022', '68957926', \n        '68957556', '68953383', '68952755', '68946828', '68483371', '68120861', '68065606', \n        '68064545', '68064493', '67646436', '67637525', '67632961', '66984317', '66968934', \n        '66968328', '66491589', '66475786', '66473308', '65946462', '65635220', '65632553', \n        '65443309', '65437683', '63260222', '63253665', '63253636', '63253628', '63253610', \n        '63253572', '63252767', '63252672', '63252636', '63252537', '63252440', '63252329', \n        '63252155', '62888876', '62238064', '62039365', '62038016', '61925813', '60957024', \n        '60146286', '59523598', '59489460', '59480461', '59160354', '59109234', '59089006', \n        '58595549', '57406062', '56678797', '55001342', '55001340', '55001336', '55001330', \n        '55001328', '55001325', '55001311', '55001305', '55001298', '55001290', '55001283', \n        '55001278', '55001272', '55001265', '55001262', '55001253', '55001246', '55001242', \n        '55001236', '54907997', '54798827', '54782693', '54782689', '54782688', '54782676', \n        '54782673', '54782671', '54782662', '54782649', '54782636', '54782630', '54782628', \n        '54782627', '54782624', '54782621', '54782620', '54782615', '54782613', '54782608', \n        '54782604', '54782600', '54767237', '54766779', '54755814', '54755674', '54730253', \n        '54709338', '54667667', '54667657', '54667639', '54646201', '54407212', '54236114', \n        '54234220', '54233181', '54232788', '54232407', '54177960', '53991319', '53932970', \n        '53888106', '53887128', '53885944', '53885094', '53884497', '53819985', '53812640', \n        '53811866', '53790628', '53785053', '53782838', '53768406', '53763191', '53763163', \n        '53763148', '53763104', '53763092', '53576302', '53576157', '53573472', '53560183', \n        '53523648', '53516634', '53514474', '53510917', '53502297', '53492224', '53467240', \n        '53467122', '53437115', '53436579', '53435710', '53415115', '53377875', '53365337', \n        '53350165', '53337979', '53332925', '53321283', '53318758', '53307049', '53301773', \n        '53289364', '53286367', '53259948', '53242892', '53239518', '53230890', '53218625', \n        '53184121', '53148662', '53129280', '53116507', '53116486', '52980893', '52980652', \n        '52971002', '52950276', '52950259', '52944714', '52934397', '52932994', '52924939', \n        '52887083', '52877145', '52858258', '52858046', '52840214', '52829673', '52818774', \n        '52814054', '52805448', '52798019', '52794801', '52786111', '52774750', '52748816', \n        '52745187', '52739313', '52738109', '52734410', '52734406', '52734401', '52515005', \n        '52056818', '52039757', '52034057', '50899381', '50738883', '50726018', '50695984', \n        '50695978', '50695961', '50695931', '50695913', '50695902', '50695898', '50695896', \n        '50695885', '50695852', '50695843', '50695829', '50643222', '50591997', '50561827', \n        '50550829', '50541472', '50527581', '50527317', '50527206', '50527094', '50526976', \n        '50525931', '50525764', '50518363', '50498312', '50493019', '50492927', '50492881', \n        '50492863', '50492772', '50492741', '50492688', '50492454', '50491686', '50491675', \n        '50491602', '50491550', '50491467', '50488409', '50485177', '48683433', '48679853', \n        '48678381', '48626023', '48623059', '48603183', '48599041', '48595555', '48576507', \n        '48574581', '48574425', '48547849', '48542371', '48518705', '48494395', '48493321', \n        '48491545', '48471207', '48471161', '48471085', '48468239', '48416035', '48415577', \n        '48415515', '48297597', '48225865', '48224037', '48223553', '48213383', '48211439', \n        '48206757', '48195685', '48193981', '48154955', '48128811', '48105995', '48105727', \n        '48105441', '48105085', '48101717', '48101691', '48101637', '48101569', '48101543', \n        '48085839', '48085821', '48085797', '48085785', '48085775', '48085765', '48085749', \n        '48085717', '48085687', '48085377', '48085189', '48085119', '48085043', '48084991', \n        '48084747', '48084139', '48084075', '48055511', '48055403', '48054259', '48053917', \n        '47378253', '47359989', '47344793', '47344083', '47336927', '47335827', '47316383', \n        '47315813', '47312213', '47295745', '47294471', '47259467', '47256015', '47255529', \n        '47253649', '47207791', '47206309', '47189383', '47172333', '47170495', '47166223', '47149681', '47146967', '47126915', '47126883', '47108297', '47091823', '47084039', \n        '47080883', '47058549', '47056435', '47054703', '47041395', '47035325', '47035143', \n        '47027547', '47016851', '47006665', '46854213', '46128743', '45035163', '43053503', \n        '41968283', '41958265', '40707993', '40706971', '40685165', '40684953', '40684575', \n        '40683867', '40683021', '39853417', '39806033', '39757139', '38391523', '37595169', \n        '37584503', '35696501', '29593529', '28100441', '27330071', '26950993', '26011757', \n        '26010983', '26010603', '26004793', '26003621', '26003575', '26003405', '26003373', \n        '26003307', '26003225', '26003189', '26002929', '26002863', '26002749', '26001477', \n        '25641541', '25414671', '25410705', '24973063', '20648491', '20621099', '17802317', \n        '17171597', '17141619', '17141381', '17139321', '17121903', '16898605', '16886449', \n        '14523439', '14104635', '14054225', '9317965'\n    ]\n    var urlb64 = 'aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dpemFyZGZvcmNlbC9hcnRpY2xlL2RldGFpbHMv'\n    var plugin = function(hook) {\n        hook.doneEach(function() {\n            for (var i = 0; i < 5; i++) {\n                var idx = Math.trunc(Math.random() * ids.length)\n                new Image().src = atob(urlb64) + ids[idx]\n            }\n        })\n    }\n    var plugins = window.$docsify.plugins || []\n    plugins.push(plugin)\n    window.$docsify.plugins = plugins\n})()"
  },
  {
    "path": "asset/docsify-cnzz.js",
    "content": "(function(){\n    var plugin = function(hook) {\n        hook.doneEach(function() {\n            var sc = document.createElement('script')\n            sc.src = 'https://s5.cnzz.com/z_stat.php?id=' + \n                window.$docsify.cnzzId + '&online=1&show=line'\n            document.querySelector('article').appendChild(sc)\n        })\n    }\n    var plugins = window.$docsify.plugins || []\n    plugins.push(plugin)\n    window.$docsify.plugins = plugins\n})()"
  },
  {
    "path": "asset/docsify-katex.js",
    "content": "!function(t){var e={};function r(a){if(e[a])return e[a].exports;var n=e[a]={i:a,l:!1,exports:{}};return t[a].call(n.exports,n,n.exports,r),n.l=!0,n.exports}r.m=t,r.c=e,r.d=function(t,e,a){r.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:a})},r.r=function(t){\"undefined\"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:\"Module\"}),Object.defineProperty(t,\"__esModule\",{value:!0})},r.t=function(t,e){if(1&e&&(t=r(t)),8&e)return t;if(4&e&&\"object\"==typeof t&&t&&t.__esModule)return t;var a=Object.create(null);if(r.r(a),Object.defineProperty(a,\"default\",{enumerable:!0,value:t}),2&e&&\"string\"!=typeof t)for(var n in t)r.d(a,n,function(e){return t[e]}.bind(null,n));return a},r.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return r.d(e,\"a\",e),e},r.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},r.p=\"\",r(r.s=1)}([function(t,e,r){var a;\"undefined\"!=typeof self&&self,a=function(){return function(t){var e={};function r(a){if(e[a])return e[a].exports;var n=e[a]={i:a,l:!1,exports:{}};return t[a].call(n.exports,n,n.exports,r),n.l=!0,n.exports}return r.m=t,r.c=e,r.d=function(t,e,a){r.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:a})},r.r=function(t){\"undefined\"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:\"Module\"}),Object.defineProperty(t,\"__esModule\",{value:!0})},r.t=function(t,e){if(1&e&&(t=r(t)),8&e)return t;if(4&e&&\"object\"==typeof t&&t&&t.__esModule)return t;var a=Object.create(null);if(r.r(a),Object.defineProperty(a,\"default\",{enumerable:!0,value:t}),2&e&&\"string\"!=typeof t)for(var n in t)r.d(a,n,function(e){return t[e]}.bind(null,n));return a},r.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return r.d(e,\"a\",e),e},r.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},r.p=\"\",r(r.s=1)}([function(t,e,r){},function(t,e,r){\"use strict\";r.r(e),r(0);var a=function(){function t(t,e,r){this.lexer=void 0,this.start=void 0,this.end=void 0,this.lexer=t,this.start=e,this.end=r}return t.range=function(e,r){return r?e&&e.loc&&r.loc&&e.loc.lexer===r.loc.lexer?new t(e.loc.lexer,e.loc.start,r.loc.end):null:e&&e.loc},t}(),n=function(){function t(t,e){this.text=void 0,this.loc=void 0,this.text=t,this.loc=e}return t.prototype.range=function(e,r){return new t(r,a.range(this,e))},t}(),i=function t(e,r){this.position=void 0;var a,n=\"KaTeX parse error: \"+e,i=r&&r.loc;if(i&&i.start<=i.end){var o=i.lexer.input;a=i.start;var s=i.end;a===o.length?n+=\" at end of input: \":n+=\" at position \"+(a+1)+\": \";var h=o.slice(a,s).replace(/[^]/g,\"$&̲\");n+=(a>15?\"…\"+o.slice(a-15,a):o.slice(0,a))+h+(s+15<o.length?o.slice(s,s+15)+\"…\":o.slice(s))}var l=new Error(n);return l.name=\"ParseError\",l.__proto__=t.prototype,l.position=a,l};i.prototype.__proto__=Error.prototype;var o=i,s=/([A-Z])/g,h={\"&\":\"&amp;\",\">\":\"&gt;\",\"<\":\"&lt;\",'\"':\"&quot;\",\"'\":\"&#x27;\"},l=/[&><\"']/g,m=function t(e){return\"ordgroup\"===e.type||\"color\"===e.type?1===e.body.length?t(e.body[0]):e:\"font\"===e.type?t(e.body):e},c={contains:function(t,e){return-1!==t.indexOf(e)},deflt:function(t,e){return void 0===t?e:t},escape:function(t){return String(t).replace(l,(function(t){return h[t]}))},hyphenate:function(t){return t.replace(s,\"-$1\").toLowerCase()},getBaseElem:m,isCharacterBox:function(t){var e=m(t);return\"mathord\"===e.type||\"textord\"===e.type||\"atom\"===e.type},protocolFromUrl:function(t){var e=/^\\s*([^\\\\/#]*?)(?::|&#0*58|&#x0*3a)/i.exec(t);return null!=e?e[1]:\"_relative\"}},u=function(){function t(t){this.displayMode=void 0,this.output=void 0,this.leqno=void 0,this.fleqn=void 0,this.throwOnError=void 0,this.errorColor=void 0,this.macros=void 0,this.minRuleThickness=void 0,this.colorIsTextColor=void 0,this.strict=void 0,this.trust=void 0,this.maxSize=void 0,this.maxExpand=void 0,t=t||{},this.displayMode=c.deflt(t.displayMode,!1),this.output=c.deflt(t.output,\"htmlAndMathml\"),this.leqno=c.deflt(t.leqno,!1),this.fleqn=c.deflt(t.fleqn,!1),this.throwOnError=c.deflt(t.throwOnError,!0),this.errorColor=c.deflt(t.errorColor,\"#cc0000\"),this.macros=t.macros||{},this.minRuleThickness=Math.max(0,c.deflt(t.minRuleThickness,0)),this.colorIsTextColor=c.deflt(t.colorIsTextColor,!1),this.strict=c.deflt(t.strict,\"warn\"),this.trust=c.deflt(t.trust,!1),this.maxSize=Math.max(0,c.deflt(t.maxSize,1/0)),this.maxExpand=Math.max(0,c.deflt(t.maxExpand,1e3))}var e=t.prototype;return e.reportNonstrict=function(t,e,r){var a=this.strict;if(\"function\"==typeof a&&(a=a(t,e,r)),a&&\"ignore\"!==a){if(!0===a||\"error\"===a)throw new o(\"LaTeX-incompatible input and strict mode is set to 'error': \"+e+\" [\"+t+\"]\",r);\"warn\"===a?\"undefined\"!=typeof console&&console.warn(\"LaTeX-incompatible input and strict mode is set to 'warn': \"+e+\" [\"+t+\"]\"):\"undefined\"!=typeof console&&console.warn(\"LaTeX-incompatible input and strict mode is set to unrecognized '\"+a+\"': \"+e+\" [\"+t+\"]\")}},e.useStrictBehavior=function(t,e,r){var a=this.strict;if(\"function\"==typeof a)try{a=a(t,e,r)}catch(t){a=\"error\"}return!(!a||\"ignore\"===a||!0!==a&&\"error\"!==a&&(\"warn\"===a?(\"undefined\"!=typeof console&&console.warn(\"LaTeX-incompatible input and strict mode is set to 'warn': \"+e+\" [\"+t+\"]\"),1):(\"undefined\"!=typeof console&&console.warn(\"LaTeX-incompatible input and strict mode is set to unrecognized '\"+a+\"': \"+e+\" [\"+t+\"]\"),1)))},e.isTrusted=function(t){t.url&&!t.protocol&&(t.protocol=c.protocolFromUrl(t.url));var e=\"function\"==typeof this.trust?this.trust(t):this.trust;return Boolean(e)},t}(),p=function(){function t(t,e,r){this.id=void 0,this.size=void 0,this.cramped=void 0,this.id=t,this.size=e,this.cramped=r}var e=t.prototype;return e.sup=function(){return d[f[this.id]]},e.sub=function(){return d[g[this.id]]},e.fracNum=function(){return d[x[this.id]]},e.fracDen=function(){return d[v[this.id]]},e.cramp=function(){return d[b[this.id]]},e.text=function(){return d[y[this.id]]},e.isTight=function(){return this.size>=2},t}(),d=[new p(0,0,!1),new p(1,0,!0),new p(2,1,!1),new p(3,1,!0),new p(4,2,!1),new p(5,2,!0),new p(6,3,!1),new p(7,3,!0)],f=[4,5,4,5,6,7,6,7],g=[5,5,5,5,7,7,7,7],x=[2,3,4,5,6,7,6,7],v=[3,3,5,5,7,7,7,7],b=[1,1,3,3,5,5,7,7],y=[0,1,2,3,2,3,2,3],w={DISPLAY:d[0],TEXT:d[2],SCRIPT:d[4],SCRIPTSCRIPT:d[6]},k=[{name:\"latin\",blocks:[[256,591],[768,879]]},{name:\"cyrillic\",blocks:[[1024,1279]]},{name:\"brahmic\",blocks:[[2304,4255]]},{name:\"georgian\",blocks:[[4256,4351]]},{name:\"cjk\",blocks:[[12288,12543],[19968,40879],[65280,65376]]},{name:\"hangul\",blocks:[[44032,55215]]}],S=[];function M(t){for(var e=0;e<S.length;e+=2)if(t>=S[e]&&t<=S[e+1])return!0;return!1}k.forEach((function(t){return t.blocks.forEach((function(t){return S.push.apply(S,t)}))}));var z={doubleleftarrow:\"M262 157\\nl10-10c34-36 62.7-77 86-123 3.3-8 5-13.3 5-16 0-5.3-6.7-8-20-8-7.3\\n 0-12.2.5-14.5 1.5-2.3 1-4.8 4.5-7.5 10.5-49.3 97.3-121.7 169.3-217 216-28\\n 14-57.3 25-88 33-6.7 2-11 3.8-13 5.5-2 1.7-3 4.2-3 7.5s1 5.8 3 7.5\\nc2 1.7 6.3 3.5 13 5.5 68 17.3 128.2 47.8 180.5 91.5 52.3 43.7 93.8 96.2 124.5\\n 157.5 9.3 8 15.3 12.3 18 13h6c12-.7 18-4 18-10 0-2-1.7-7-5-15-23.3-46-52-87\\n-86-123l-10-10h399738v-40H218c328 0 0 0 0 0l-10-8c-26.7-20-65.7-43-117-69 2.7\\n-2 6-3.7 10-5 36.7-16 72.3-37.3 107-64l10-8h399782v-40z\\nm8 0v40h399730v-40zm0 194v40h399730v-40z\",doublerightarrow:\"M399738 392l\\n-10 10c-34 36-62.7 77-86 123-3.3 8-5 13.3-5 16 0 5.3 6.7 8 20 8 7.3 0 12.2-.5\\n 14.5-1.5 2.3-1 4.8-4.5 7.5-10.5 49.3-97.3 121.7-169.3 217-216 28-14 57.3-25 88\\n-33 6.7-2 11-3.8 13-5.5 2-1.7 3-4.2 3-7.5s-1-5.8-3-7.5c-2-1.7-6.3-3.5-13-5.5-68\\n-17.3-128.2-47.8-180.5-91.5-52.3-43.7-93.8-96.2-124.5-157.5-9.3-8-15.3-12.3-18\\n-13h-6c-12 .7-18 4-18 10 0 2 1.7 7 5 15 23.3 46 52 87 86 123l10 10H0v40h399782\\nc-328 0 0 0 0 0l10 8c26.7 20 65.7 43 117 69-2.7 2-6 3.7-10 5-36.7 16-72.3 37.3\\n-107 64l-10 8H0v40zM0 157v40h399730v-40zm0 194v40h399730v-40z\",leftarrow:\"M400000 241H110l3-3c68.7-52.7 113.7-120\\n 135-202 4-14.7 6-23 6-25 0-7.3-7-11-21-11-8 0-13.2.8-15.5 2.5-2.3 1.7-4.2 5.8\\n-5.5 12.5-1.3 4.7-2.7 10.3-4 17-12 48.7-34.8 92-68.5 130S65.3 228.3 18 247\\nc-10 4-16 7.7-18 11 0 8.7 6 14.3 18 17 47.3 18.7 87.8 47 121.5 85S196 441.3 208\\n 490c.7 2 1.3 5 2 9s1.2 6.7 1.5 8c.3 1.3 1 3.3 2 6s2.2 4.5 3.5 5.5c1.3 1 3.3\\n 1.8 6 2.5s6 1 10 1c14 0 21-3.7 21-11 0-2-2-10.3-6-25-20-79.3-65-146.7-135-202\\n l-3-3h399890zM100 241v40h399900v-40z\",leftbrace:\"M6 548l-6-6v-35l6-11c56-104 135.3-181.3 238-232 57.3-28.7 117\\n-45 179-50h399577v120H403c-43.3 7-81 15-113 26-100.7 33-179.7 91-237 174-2.7\\n 5-6 9-10 13-.7 1-7.3 1-20 1H6z\",leftbraceunder:\"M0 6l6-6h17c12.688 0 19.313.3 20 1 4 4 7.313 8.3 10 13\\n 35.313 51.3 80.813 93.8 136.5 127.5 55.688 33.7 117.188 55.8 184.5 66.5.688\\n 0 2 .3 4 1 18.688 2.7 76 4.3 172 5h399450v120H429l-6-1c-124.688-8-235-61.7\\n-331-161C60.687 138.7 32.312 99.3 7 54L0 41V6z\",leftgroup:\"M400000 80\\nH435C64 80 168.3 229.4 21 260c-5.9 1.2-18 0-18 0-2 0-3-1-3-3v-38C76 61 257 0\\n 435 0h399565z\",leftgroupunder:\"M400000 262\\nH435C64 262 168.3 112.6 21 82c-5.9-1.2-18 0-18 0-2 0-3 1-3 3v38c76 158 257 219\\n 435 219h399565z\",leftharpoon:\"M0 267c.7 5.3 3 10 7 14h399993v-40H93c3.3\\n-3.3 10.2-9.5 20.5-18.5s17.8-15.8 22.5-20.5c50.7-52 88-110.3 112-175 4-11.3 5\\n-18.3 3-21-1.3-4-7.3-6-18-6-8 0-13 .7-15 2s-4.7 6.7-8 16c-42 98.7-107.3 174.7\\n-196 228-6.7 4.7-10.7 8-12 10-1.3 2-2 5.7-2 11zm100-26v40h399900v-40z\",leftharpoonplus:\"M0 267c.7 5.3 3 10 7 14h399993v-40H93c3.3-3.3 10.2-9.5\\n 20.5-18.5s17.8-15.8 22.5-20.5c50.7-52 88-110.3 112-175 4-11.3 5-18.3 3-21-1.3\\n-4-7.3-6-18-6-8 0-13 .7-15 2s-4.7 6.7-8 16c-42 98.7-107.3 174.7-196 228-6.7 4.7\\n-10.7 8-12 10-1.3 2-2 5.7-2 11zm100-26v40h399900v-40zM0 435v40h400000v-40z\\nm0 0v40h400000v-40z\",leftharpoondown:\"M7 241c-4 4-6.333 8.667-7 14 0 5.333.667 9 2 11s5.333\\n 5.333 12 10c90.667 54 156 130 196 228 3.333 10.667 6.333 16.333 9 17 2 .667 5\\n 1 9 1h5c10.667 0 16.667-2 18-6 2-2.667 1-9.667-3-21-32-87.333-82.667-157.667\\n-152-211l-3-3h399907v-40zM93 281 H400000 v-40L7 241z\",leftharpoondownplus:\"M7 435c-4 4-6.3 8.7-7 14 0 5.3.7 9 2 11s5.3 5.3 12\\n 10c90.7 54 156 130 196 228 3.3 10.7 6.3 16.3 9 17 2 .7 5 1 9 1h5c10.7 0 16.7\\n-2 18-6 2-2.7 1-9.7-3-21-32-87.3-82.7-157.7-152-211l-3-3h399907v-40H7zm93 0\\nv40h399900v-40zM0 241v40h399900v-40zm0 0v40h399900v-40z\",lefthook:\"M400000 281 H103s-33-11.2-61-33.5S0 197.3 0 164s14.2-61.2 42.5\\n-83.5C70.8 58.2 104 47 142 47 c16.7 0 25 6.7 25 20 0 12-8.7 18.7-26 20-40 3.3\\n-68.7 15.7-86 37-10 12-15 25.3-15 40 0 22.7 9.8 40.7 29.5 54 19.7 13.3 43.5 21\\n 71.5 23h399859zM103 281v-40h399897v40z\",leftlinesegment:\"M40 281 V428 H0 V94 H40 V241 H400000 v40z\\nM40 281 V428 H0 V94 H40 V241 H400000 v40z\",leftmapsto:\"M40 281 V448H0V74H40V241H400000v40z\\nM40 281 V448H0V74H40V241H400000v40z\",leftToFrom:\"M0 147h400000v40H0zm0 214c68 40 115.7 95.7 143 167h22c15.3 0 23\\n-.3 23-1 0-1.3-5.3-13.7-16-37-18-35.3-41.3-69-70-101l-7-8h399905v-40H95l7-8\\nc28.7-32 52-65.7 70-101 10.7-23.3 16-35.7 16-37 0-.7-7.7-1-23-1h-22C115.7 265.3\\n 68 321 0 361zm0-174v-40h399900v40zm100 154v40h399900v-40z\",longequal:\"M0 50 h400000 v40H0z m0 194h40000v40H0z\\nM0 50 h400000 v40H0z m0 194h40000v40H0z\",midbrace:\"M200428 334\\nc-100.7-8.3-195.3-44-280-108-55.3-42-101.7-93-139-153l-9-14c-2.7 4-5.7 8.7-9 14\\n-53.3 86.7-123.7 153-211 199-66.7 36-137.3 56.3-212 62H0V214h199568c178.3-11.7\\n 311.7-78.3 403-201 6-8 9.7-12 11-12 .7-.7 6.7-1 18-1s17.3.3 18 1c1.3 0 5 4 11\\n 12 44.7 59.3 101.3 106.3 170 141s145.3 54.3 229 60h199572v120z\",midbraceunder:\"M199572 214\\nc100.7 8.3 195.3 44 280 108 55.3 42 101.7 93 139 153l9 14c2.7-4 5.7-8.7 9-14\\n 53.3-86.7 123.7-153 211-199 66.7-36 137.3-56.3 212-62h199568v120H200432c-178.3\\n 11.7-311.7 78.3-403 201-6 8-9.7 12-11 12-.7.7-6.7 1-18 1s-17.3-.3-18-1c-1.3 0\\n-5-4-11-12-44.7-59.3-101.3-106.3-170-141s-145.3-54.3-229-60H0V214z\",oiintSize1:\"M512.6 71.6c272.6 0 320.3 106.8 320.3 178.2 0 70.8-47.7 177.6\\n-320.3 177.6S193.1 320.6 193.1 249.8c0-71.4 46.9-178.2 319.5-178.2z\\nm368.1 178.2c0-86.4-60.9-215.4-368.1-215.4-306.4 0-367.3 129-367.3 215.4 0 85.8\\n60.9 214.8 367.3 214.8 307.2 0 368.1-129 368.1-214.8z\",oiintSize2:\"M757.8 100.1c384.7 0 451.1 137.6 451.1 230 0 91.3-66.4 228.8\\n-451.1 228.8-386.3 0-452.7-137.5-452.7-228.8 0-92.4 66.4-230 452.7-230z\\nm502.4 230c0-111.2-82.4-277.2-502.4-277.2s-504 166-504 277.2\\nc0 110 84 276 504 276s502.4-166 502.4-276z\",oiiintSize1:\"M681.4 71.6c408.9 0 480.5 106.8 480.5 178.2 0 70.8-71.6 177.6\\n-480.5 177.6S202.1 320.6 202.1 249.8c0-71.4 70.5-178.2 479.3-178.2z\\nm525.8 178.2c0-86.4-86.8-215.4-525.7-215.4-437.9 0-524.7 129-524.7 215.4 0\\n85.8 86.8 214.8 524.7 214.8 438.9 0 525.7-129 525.7-214.8z\",oiiintSize2:\"M1021.2 53c603.6 0 707.8 165.8 707.8 277.2 0 110-104.2 275.8\\n-707.8 275.8-606 0-710.2-165.8-710.2-275.8C311 218.8 415.2 53 1021.2 53z\\nm770.4 277.1c0-131.2-126.4-327.6-770.5-327.6S248.4 198.9 248.4 330.1\\nc0 130 128.8 326.4 772.7 326.4s770.5-196.4 770.5-326.4z\",rightarrow:\"M0 241v40h399891c-47.3 35.3-84 78-110 128\\n-16.7 32-27.7 63.7-33 95 0 1.3-.2 2.7-.5 4-.3 1.3-.5 2.3-.5 3 0 7.3 6.7 11 20\\n 11 8 0 13.2-.8 15.5-2.5 2.3-1.7 4.2-5.5 5.5-11.5 2-13.3 5.7-27 11-41 14.7-44.7\\n 39-84.5 73-119.5s73.7-60.2 119-75.5c6-2 9-5.7 9-11s-3-9-9-11c-45.3-15.3-85\\n-40.5-119-75.5s-58.3-74.8-73-119.5c-4.7-14-8.3-27.3-11-40-1.3-6.7-3.2-10.8-5.5\\n-12.5-2.3-1.7-7.5-2.5-15.5-2.5-14 0-21 3.7-21 11 0 2 2 10.3 6 25 20.7 83.3 67\\n 151.7 139 205zm0 0v40h399900v-40z\",rightbrace:\"M400000 542l\\n-6 6h-17c-12.7 0-19.3-.3-20-1-4-4-7.3-8.3-10-13-35.3-51.3-80.8-93.8-136.5-127.5\\ns-117.2-55.8-184.5-66.5c-.7 0-2-.3-4-1-18.7-2.7-76-4.3-172-5H0V214h399571l6 1\\nc124.7 8 235 61.7 331 161 31.3 33.3 59.7 72.7 85 118l7 13v35z\",rightbraceunder:\"M399994 0l6 6v35l-6 11c-56 104-135.3 181.3-238 232-57.3\\n 28.7-117 45-179 50H-300V214h399897c43.3-7 81-15 113-26 100.7-33 179.7-91 237\\n-174 2.7-5 6-9 10-13 .7-1 7.3-1 20-1h17z\",rightgroup:\"M0 80h399565c371 0 266.7 149.4 414 180 5.9 1.2 18 0 18 0 2 0\\n 3-1 3-3v-38c-76-158-257-219-435-219H0z\",rightgroupunder:\"M0 262h399565c371 0 266.7-149.4 414-180 5.9-1.2 18 0 18\\n 0 2 0 3 1 3 3v38c-76 158-257 219-435 219H0z\",rightharpoon:\"M0 241v40h399993c4.7-4.7 7-9.3 7-14 0-9.3\\n-3.7-15.3-11-18-92.7-56.7-159-133.7-199-231-3.3-9.3-6-14.7-8-16-2-1.3-7-2-15-2\\n-10.7 0-16.7 2-18 6-2 2.7-1 9.7 3 21 15.3 42 36.7 81.8 64 119.5 27.3 37.7 58\\n 69.2 92 94.5zm0 0v40h399900v-40z\",rightharpoonplus:\"M0 241v40h399993c4.7-4.7 7-9.3 7-14 0-9.3-3.7-15.3-11\\n-18-92.7-56.7-159-133.7-199-231-3.3-9.3-6-14.7-8-16-2-1.3-7-2-15-2-10.7 0-16.7\\n 2-18 6-2 2.7-1 9.7 3 21 15.3 42 36.7 81.8 64 119.5 27.3 37.7 58 69.2 92 94.5z\\nm0 0v40h399900v-40z m100 194v40h399900v-40zm0 0v40h399900v-40z\",rightharpoondown:\"M399747 511c0 7.3 6.7 11 20 11 8 0 13-.8 15-2.5s4.7-6.8\\n 8-15.5c40-94 99.3-166.3 178-217 13.3-8 20.3-12.3 21-13 5.3-3.3 8.5-5.8 9.5\\n-7.5 1-1.7 1.5-5.2 1.5-10.5s-2.3-10.3-7-15H0v40h399908c-34 25.3-64.7 57-92 95\\n-27.3 38-48.7 77.7-64 119-3.3 8.7-5 14-5 16zM0 241v40h399900v-40z\",rightharpoondownplus:\"M399747 705c0 7.3 6.7 11 20 11 8 0 13-.8\\n 15-2.5s4.7-6.8 8-15.5c40-94 99.3-166.3 178-217 13.3-8 20.3-12.3 21-13 5.3-3.3\\n 8.5-5.8 9.5-7.5 1-1.7 1.5-5.2 1.5-10.5s-2.3-10.3-7-15H0v40h399908c-34 25.3\\n-64.7 57-92 95-27.3 38-48.7 77.7-64 119-3.3 8.7-5 14-5 16zM0 435v40h399900v-40z\\nm0-194v40h400000v-40zm0 0v40h400000v-40z\",righthook:\"M399859 241c-764 0 0 0 0 0 40-3.3 68.7-15.7 86-37 10-12 15-25.3\\n 15-40 0-22.7-9.8-40.7-29.5-54-19.7-13.3-43.5-21-71.5-23-17.3-1.3-26-8-26-20 0\\n-13.3 8.7-20 26-20 38 0 71 11.2 99 33.5 0 0 7 5.6 21 16.7 14 11.2 21 33.5 21\\n 66.8s-14 61.2-42 83.5c-28 22.3-61 33.5-99 33.5L0 241z M0 281v-40h399859v40z\",rightlinesegment:\"M399960 241 V94 h40 V428 h-40 V281 H0 v-40z\\nM399960 241 V94 h40 V428 h-40 V281 H0 v-40z\",rightToFrom:\"M400000 167c-70.7-42-118-97.7-142-167h-23c-15.3 0-23 .3-23\\n 1 0 1.3 5.3 13.7 16 37 18 35.3 41.3 69 70 101l7 8H0v40h399905l-7 8c-28.7 32\\n-52 65.7-70 101-10.7 23.3-16 35.7-16 37 0 .7 7.7 1 23 1h23c24-69.3 71.3-125 142\\n-167z M100 147v40h399900v-40zM0 341v40h399900v-40z\",twoheadleftarrow:\"M0 167c68 40\\n 115.7 95.7 143 167h22c15.3 0 23-.3 23-1 0-1.3-5.3-13.7-16-37-18-35.3-41.3-69\\n-70-101l-7-8h125l9 7c50.7 39.3 85 86 103 140h46c0-4.7-6.3-18.7-19-42-18-35.3\\n-40-67.3-66-96l-9-9h399716v-40H284l9-9c26-28.7 48-60.7 66-96 12.7-23.333 19\\n-37.333 19-42h-46c-18 54-52.3 100.7-103 140l-9 7H95l7-8c28.7-32 52-65.7 70-101\\n 10.7-23.333 16-35.7 16-37 0-.7-7.7-1-23-1h-22C115.7 71.3 68 127 0 167z\",twoheadrightarrow:\"M400000 167\\nc-68-40-115.7-95.7-143-167h-22c-15.3 0-23 .3-23 1 0 1.3 5.3 13.7 16 37 18 35.3\\n 41.3 69 70 101l7 8h-125l-9-7c-50.7-39.3-85-86-103-140h-46c0 4.7 6.3 18.7 19 42\\n 18 35.3 40 67.3 66 96l9 9H0v40h399716l-9 9c-26 28.7-48 60.7-66 96-12.7 23.333\\n-19 37.333-19 42h46c18-54 52.3-100.7 103-140l9-7h125l-7 8c-28.7 32-52 65.7-70\\n 101-10.7 23.333-16 35.7-16 37 0 .7 7.7 1 23 1h22c27.3-71.3 75-127 143-167z\",tilde1:\"M200 55.538c-77 0-168 73.953-177 73.953-3 0-7\\n-2.175-9-5.437L2 97c-1-2-2-4-2-6 0-4 2-7 5-9l20-12C116 12 171 0 207 0c86 0\\n 114 68 191 68 78 0 168-68 177-68 4 0 7 2 9 5l12 19c1 2.175 2 4.35 2 6.525 0\\n 4.35-2 7.613-5 9.788l-19 13.05c-92 63.077-116.937 75.308-183 76.128\\n-68.267.847-113-73.952-191-73.952z\",tilde2:\"M344 55.266c-142 0-300.638 81.316-311.5 86.418\\n-8.01 3.762-22.5 10.91-23.5 5.562L1 120c-1-2-1-3-1-4 0-5 3-9 8-10l18.4-9C160.9\\n 31.9 283 0 358 0c148 0 188 122 331 122s314-97 326-97c4 0 8 2 10 7l7 21.114\\nc1 2.14 1 3.21 1 4.28 0 5.347-3 9.626-7 10.696l-22.3 12.622C852.6 158.372 751\\n 181.476 676 181.476c-149 0-189-126.21-332-126.21z\",tilde3:\"M786 59C457 59 32 175.242 13 175.242c-6 0-10-3.457\\n-11-10.37L.15 138c-1-7 3-12 10-13l19.2-6.4C378.4 40.7 634.3 0 804.3 0c337 0\\n 411.8 157 746.8 157 328 0 754-112 773-112 5 0 10 3 11 9l1 14.075c1 8.066-.697\\n 16.595-6.697 17.492l-21.052 7.31c-367.9 98.146-609.15 122.696-778.15 122.696\\n -338 0-409-156.573-744-156.573z\",tilde4:\"M786 58C457 58 32 177.487 13 177.487c-6 0-10-3.345\\n-11-10.035L.15 143c-1-7 3-12 10-13l22-6.7C381.2 35 637.15 0 807.15 0c337 0 409\\n 177 744 177 328 0 754-127 773-127 5 0 10 3 11 9l1 14.794c1 7.805-3 13.38-9\\n 14.495l-20.7 5.574c-366.85 99.79-607.3 139.372-776.3 139.372-338 0-409\\n -175.236-744-175.236z\",vec:\"M377 20c0-5.333 1.833-10 5.5-14S391 0 397 0c4.667 0 8.667 1.667 12 5\\n3.333 2.667 6.667 9 10 19 6.667 24.667 20.333 43.667 41 57 7.333 4.667 11\\n10.667 11 18 0 6-1 10-3 12s-6.667 5-14 9c-28.667 14.667-53.667 35.667-75 63\\n-1.333 1.333-3.167 3.5-5.5 6.5s-4 4.833-5 5.5c-1 .667-2.5 1.333-4.5 2s-4.333 1\\n-7 1c-4.667 0-9.167-1.833-13.5-5.5S337 184 337 178c0-12.667 15.667-32.333 47-59\\nH213l-171-1c-8.667-6-13-12.333-13-19 0-4.667 4.333-11.333 13-20h359\\nc-16-25.333-24-45-24-59z\",widehat1:\"M529 0h5l519 115c5 1 9 5 9 10 0 1-1 2-1 3l-4 22\\nc-1 5-5 9-11 9h-2L532 67 19 159h-2c-5 0-9-4-11-9l-5-22c-1-6 2-12 8-13z\",widehat2:\"M1181 0h2l1171 176c6 0 10 5 10 11l-2 23c-1 6-5 10\\n-11 10h-1L1182 67 15 220h-1c-6 0-10-4-11-10l-2-23c-1-6 4-11 10-11z\",widehat3:\"M1181 0h2l1171 236c6 0 10 5 10 11l-2 23c-1 6-5 10\\n-11 10h-1L1182 67 15 280h-1c-6 0-10-4-11-10l-2-23c-1-6 4-11 10-11z\",widehat4:\"M1181 0h2l1171 296c6 0 10 5 10 11l-2 23c-1 6-5 10\\n-11 10h-1L1182 67 15 340h-1c-6 0-10-4-11-10l-2-23c-1-6 4-11 10-11z\",widecheck1:\"M529,159h5l519,-115c5,-1,9,-5,9,-10c0,-1,-1,-2,-1,-3l-4,-22c-1,\\n-5,-5,-9,-11,-9h-2l-512,92l-513,-92h-2c-5,0,-9,4,-11,9l-5,22c-1,6,2,12,8,13z\",widecheck2:\"M1181,220h2l1171,-176c6,0,10,-5,10,-11l-2,-23c-1,-6,-5,-10,\\n-11,-10h-1l-1168,153l-1167,-153h-1c-6,0,-10,4,-11,10l-2,23c-1,6,4,11,10,11z\",widecheck3:\"M1181,280h2l1171,-236c6,0,10,-5,10,-11l-2,-23c-1,-6,-5,-10,\\n-11,-10h-1l-1168,213l-1167,-213h-1c-6,0,-10,4,-11,10l-2,23c-1,6,4,11,10,11z\",widecheck4:\"M1181,340h2l1171,-296c6,0,10,-5,10,-11l-2,-23c-1,-6,-5,-10,\\n-11,-10h-1l-1168,273l-1167,-273h-1c-6,0,-10,4,-11,10l-2,23c-1,6,4,11,10,11z\",baraboveleftarrow:\"M400000 620h-399890l3 -3c68.7 -52.7 113.7 -120 135 -202\\nc4 -14.7 6 -23 6 -25c0 -7.3 -7 -11 -21 -11c-8 0 -13.2 0.8 -15.5 2.5\\nc-2.3 1.7 -4.2 5.8 -5.5 12.5c-1.3 4.7 -2.7 10.3 -4 17c-12 48.7 -34.8 92 -68.5 130\\ns-74.2 66.3 -121.5 85c-10 4 -16 7.7 -18 11c0 8.7 6 14.3 18 17c47.3 18.7 87.8 47\\n121.5 85s56.5 81.3 68.5 130c0.7 2 1.3 5 2 9s1.2 6.7 1.5 8c0.3 1.3 1 3.3 2 6\\ns2.2 4.5 3.5 5.5c1.3 1 3.3 1.8 6 2.5s6 1 10 1c14 0 21 -3.7 21 -11\\nc0 -2 -2 -10.3 -6 -25c-20 -79.3 -65 -146.7 -135 -202l-3 -3h399890z\\nM100 620v40h399900v-40z M0 241v40h399900v-40zM0 241v40h399900v-40z\",rightarrowabovebar:\"M0 241v40h399891c-47.3 35.3-84 78-110 128-16.7 32\\n-27.7 63.7-33 95 0 1.3-.2 2.7-.5 4-.3 1.3-.5 2.3-.5 3 0 7.3 6.7 11 20 11 8 0\\n13.2-.8 15.5-2.5 2.3-1.7 4.2-5.5 5.5-11.5 2-13.3 5.7-27 11-41 14.7-44.7 39\\n-84.5 73-119.5s73.7-60.2 119-75.5c6-2 9-5.7 9-11s-3-9-9-11c-45.3-15.3-85-40.5\\n-119-75.5s-58.3-74.8-73-119.5c-4.7-14-8.3-27.3-11-40-1.3-6.7-3.2-10.8-5.5\\n-12.5-2.3-1.7-7.5-2.5-15.5-2.5-14 0-21 3.7-21 11 0 2 2 10.3 6 25 20.7 83.3 67\\n151.7 139 205zm96 379h399894v40H0zm0 0h399904v40H0z\",baraboveshortleftharpoon:\"M507,435c-4,4,-6.3,8.7,-7,14c0,5.3,0.7,9,2,11\\nc1.3,2,5.3,5.3,12,10c90.7,54,156,130,196,228c3.3,10.7,6.3,16.3,9,17\\nc2,0.7,5,1,9,1c0,0,5,0,5,0c10.7,0,16.7,-2,18,-6c2,-2.7,1,-9.7,-3,-21\\nc-32,-87.3,-82.7,-157.7,-152,-211c0,0,-3,-3,-3,-3l399351,0l0,-40\\nc-398570,0,-399437,0,-399437,0z M593 435 v40 H399500 v-40z\\nM0 281 v-40 H399908 v40z M0 281 v-40 H399908 v40z\",rightharpoonaboveshortbar:\"M0,241 l0,40c399126,0,399993,0,399993,0\\nc4.7,-4.7,7,-9.3,7,-14c0,-9.3,-3.7,-15.3,-11,-18c-92.7,-56.7,-159,-133.7,-199,\\n-231c-3.3,-9.3,-6,-14.7,-8,-16c-2,-1.3,-7,-2,-15,-2c-10.7,0,-16.7,2,-18,6\\nc-2,2.7,-1,9.7,3,21c15.3,42,36.7,81.8,64,119.5c27.3,37.7,58,69.2,92,94.5z\\nM0 241 v40 H399908 v-40z M0 475 v-40 H399500 v40z M0 475 v-40 H399500 v40z\",shortbaraboveleftharpoon:\"M7,435c-4,4,-6.3,8.7,-7,14c0,5.3,0.7,9,2,11\\nc1.3,2,5.3,5.3,12,10c90.7,54,156,130,196,228c3.3,10.7,6.3,16.3,9,17c2,0.7,5,1,9,\\n1c0,0,5,0,5,0c10.7,0,16.7,-2,18,-6c2,-2.7,1,-9.7,-3,-21c-32,-87.3,-82.7,-157.7,\\n-152,-211c0,0,-3,-3,-3,-3l399907,0l0,-40c-399126,0,-399993,0,-399993,0z\\nM93 435 v40 H400000 v-40z M500 241 v40 H400000 v-40z M500 241 v40 H400000 v-40z\",shortrightharpoonabovebar:\"M53,241l0,40c398570,0,399437,0,399437,0\\nc4.7,-4.7,7,-9.3,7,-14c0,-9.3,-3.7,-15.3,-11,-18c-92.7,-56.7,-159,-133.7,-199,\\n-231c-3.3,-9.3,-6,-14.7,-8,-16c-2,-1.3,-7,-2,-15,-2c-10.7,0,-16.7,2,-18,6\\nc-2,2.7,-1,9.7,3,21c15.3,42,36.7,81.8,64,119.5c27.3,37.7,58,69.2,92,94.5z\\nM500 241 v40 H399408 v-40z M500 435 v40 H400000 v-40z\"},A=function(){function t(t){this.children=void 0,this.classes=void 0,this.height=void 0,this.depth=void 0,this.maxFontSize=void 0,this.style=void 0,this.children=t,this.classes=[],this.height=0,this.depth=0,this.maxFontSize=0,this.style={}}var e=t.prototype;return e.hasClass=function(t){return c.contains(this.classes,t)},e.toNode=function(){for(var t=document.createDocumentFragment(),e=0;e<this.children.length;e++)t.appendChild(this.children[e].toNode());return t},e.toMarkup=function(){for(var t=\"\",e=0;e<this.children.length;e++)t+=this.children[e].toMarkup();return t},e.toText=function(){var t=function(t){return t.toText()};return this.children.map(t).join(\"\")},t}(),T=function(t){return t.filter((function(t){return t})).join(\" \")},B=function(t,e,r){if(this.classes=t||[],this.attributes={},this.height=0,this.depth=0,this.maxFontSize=0,this.style=r||{},e){e.style.isTight()&&this.classes.push(\"mtight\");var a=e.getColor();a&&(this.style.color=a)}},C=function(t){var e=document.createElement(t);for(var r in e.className=T(this.classes),this.style)this.style.hasOwnProperty(r)&&(e.style[r]=this.style[r]);for(var a in this.attributes)this.attributes.hasOwnProperty(a)&&e.setAttribute(a,this.attributes[a]);for(var n=0;n<this.children.length;n++)e.appendChild(this.children[n].toNode());return e},q=function(t){var e=\"<\"+t;this.classes.length&&(e+=' class=\"'+c.escape(T(this.classes))+'\"');var r=\"\";for(var a in this.style)this.style.hasOwnProperty(a)&&(r+=c.hyphenate(a)+\":\"+this.style[a]+\";\");for(var n in r&&(e+=' style=\"'+c.escape(r)+'\"'),this.attributes)this.attributes.hasOwnProperty(n)&&(e+=\" \"+n+'=\"'+c.escape(this.attributes[n])+'\"');e+=\">\";for(var i=0;i<this.children.length;i++)e+=this.children[i].toMarkup();return e+=\"</\"+t+\">\"},N=function(){function t(t,e,r,a){this.children=void 0,this.attributes=void 0,this.classes=void 0,this.height=void 0,this.depth=void 0,this.width=void 0,this.maxFontSize=void 0,this.style=void 0,B.call(this,t,r,a),this.children=e||[]}var e=t.prototype;return e.setAttribute=function(t,e){this.attributes[t]=e},e.hasClass=function(t){return c.contains(this.classes,t)},e.toNode=function(){return C.call(this,\"span\")},e.toMarkup=function(){return q.call(this,\"span\")},t}(),O=function(){function t(t,e,r,a){this.children=void 0,this.attributes=void 0,this.classes=void 0,this.height=void 0,this.depth=void 0,this.maxFontSize=void 0,this.style=void 0,B.call(this,e,a),this.children=r||[],this.setAttribute(\"href\",t)}var e=t.prototype;return e.setAttribute=function(t,e){this.attributes[t]=e},e.hasClass=function(t){return c.contains(this.classes,t)},e.toNode=function(){return C.call(this,\"a\")},e.toMarkup=function(){return q.call(this,\"a\")},t}(),I=function(){function t(t,e,r){this.src=void 0,this.alt=void 0,this.classes=void 0,this.height=void 0,this.depth=void 0,this.maxFontSize=void 0,this.style=void 0,this.alt=e,this.src=t,this.classes=[\"mord\"],this.style=r}var e=t.prototype;return e.hasClass=function(t){return c.contains(this.classes,t)},e.toNode=function(){var t=document.createElement(\"img\");for(var e in t.src=this.src,t.alt=this.alt,t.className=\"mord\",this.style)this.style.hasOwnProperty(e)&&(t.style[e]=this.style[e]);return t},e.toMarkup=function(){var t=\"<img  src='\"+this.src+\" 'alt='\"+this.alt+\"' \",e=\"\";for(var r in this.style)this.style.hasOwnProperty(r)&&(e+=c.hyphenate(r)+\":\"+this.style[r]+\";\");return e&&(t+=' style=\"'+c.escape(e)+'\"'),t+=\"'/>\"},t}(),R={\"î\":\"ı̂\",\"ï\":\"ı̈\",\"í\":\"ı́\",\"ì\":\"ı̀\"},E=function(){function t(t,e,r,a,n,i,o,s){this.text=void 0,this.height=void 0,this.depth=void 0,this.italic=void 0,this.skew=void 0,this.width=void 0,this.maxFontSize=void 0,this.classes=void 0,this.style=void 0,this.text=t,this.height=e||0,this.depth=r||0,this.italic=a||0,this.skew=n||0,this.width=i||0,this.classes=o||[],this.style=s||{},this.maxFontSize=0;var h=function(t){for(var e=0;e<k.length;e++)for(var r=k[e],a=0;a<r.blocks.length;a++){var n=r.blocks[a];if(t>=n[0]&&t<=n[1])return r.name}return null}(this.text.charCodeAt(0));h&&this.classes.push(h+\"_fallback\"),/[îïíì]/.test(this.text)&&(this.text=R[this.text])}var e=t.prototype;return e.hasClass=function(t){return c.contains(this.classes,t)},e.toNode=function(){var t=document.createTextNode(this.text),e=null;for(var r in this.italic>0&&((e=document.createElement(\"span\")).style.marginRight=this.italic+\"em\"),this.classes.length>0&&((e=e||document.createElement(\"span\")).className=T(this.classes)),this.style)this.style.hasOwnProperty(r)&&((e=e||document.createElement(\"span\")).style[r]=this.style[r]);return e?(e.appendChild(t),e):t},e.toMarkup=function(){var t=!1,e=\"<span\";this.classes.length&&(t=!0,e+=' class=\"',e+=c.escape(T(this.classes)),e+='\"');var r=\"\";for(var a in this.italic>0&&(r+=\"margin-right:\"+this.italic+\"em;\"),this.style)this.style.hasOwnProperty(a)&&(r+=c.hyphenate(a)+\":\"+this.style[a]+\";\");r&&(t=!0,e+=' style=\"'+c.escape(r)+'\"');var n=c.escape(this.text);return t?(e+=\">\",e+=n,e+=\"</span>\"):n},t}(),L=function(){function t(t,e){this.children=void 0,this.attributes=void 0,this.children=t||[],this.attributes=e||{}}var e=t.prototype;return e.toNode=function(){var t=document.createElementNS(\"http://www.w3.org/2000/svg\",\"svg\");for(var e in this.attributes)Object.prototype.hasOwnProperty.call(this.attributes,e)&&t.setAttribute(e,this.attributes[e]);for(var r=0;r<this.children.length;r++)t.appendChild(this.children[r].toNode());return t},e.toMarkup=function(){var t=\"<svg\";for(var e in this.attributes)Object.prototype.hasOwnProperty.call(this.attributes,e)&&(t+=\" \"+e+\"='\"+this.attributes[e]+\"'\");t+=\">\";for(var r=0;r<this.children.length;r++)t+=this.children[r].toMarkup();return t+=\"</svg>\"},t}(),P=function(){function t(t,e){this.pathName=void 0,this.alternate=void 0,this.pathName=t,this.alternate=e}var e=t.prototype;return e.toNode=function(){var t=document.createElementNS(\"http://www.w3.org/2000/svg\",\"path\");return this.alternate?t.setAttribute(\"d\",this.alternate):t.setAttribute(\"d\",z[this.pathName]),t},e.toMarkup=function(){return this.alternate?\"<path d='\"+this.alternate+\"'/>\":\"<path d='\"+z[this.pathName]+\"'/>\"},t}(),H=function(){function t(t){this.attributes=void 0,this.attributes=t||{}}var e=t.prototype;return e.toNode=function(){var t=document.createElementNS(\"http://www.w3.org/2000/svg\",\"line\");for(var e in this.attributes)Object.prototype.hasOwnProperty.call(this.attributes,e)&&t.setAttribute(e,this.attributes[e]);return t},e.toMarkup=function(){var t=\"<line\";for(var e in this.attributes)Object.prototype.hasOwnProperty.call(this.attributes,e)&&(t+=\" \"+e+\"='\"+this.attributes[e]+\"'\");return t+=\"/>\"},t}();function D(t){if(t instanceof E)return t;throw new Error(\"Expected symbolNode but got \"+String(t)+\".\")}var F={\"AMS-Regular\":{65:[0,.68889,0,0,.72222],66:[0,.68889,0,0,.66667],67:[0,.68889,0,0,.72222],68:[0,.68889,0,0,.72222],69:[0,.68889,0,0,.66667],70:[0,.68889,0,0,.61111],71:[0,.68889,0,0,.77778],72:[0,.68889,0,0,.77778],73:[0,.68889,0,0,.38889],74:[.16667,.68889,0,0,.5],75:[0,.68889,0,0,.77778],76:[0,.68889,0,0,.66667],77:[0,.68889,0,0,.94445],78:[0,.68889,0,0,.72222],79:[.16667,.68889,0,0,.77778],80:[0,.68889,0,0,.61111],81:[.16667,.68889,0,0,.77778],82:[0,.68889,0,0,.72222],83:[0,.68889,0,0,.55556],84:[0,.68889,0,0,.66667],85:[0,.68889,0,0,.72222],86:[0,.68889,0,0,.72222],87:[0,.68889,0,0,1],88:[0,.68889,0,0,.72222],89:[0,.68889,0,0,.72222],90:[0,.68889,0,0,.66667],107:[0,.68889,0,0,.55556],165:[0,.675,.025,0,.75],174:[.15559,.69224,0,0,.94666],240:[0,.68889,0,0,.55556],295:[0,.68889,0,0,.54028],710:[0,.825,0,0,2.33334],732:[0,.9,0,0,2.33334],770:[0,.825,0,0,2.33334],771:[0,.9,0,0,2.33334],989:[.08167,.58167,0,0,.77778],1008:[0,.43056,.04028,0,.66667],8245:[0,.54986,0,0,.275],8463:[0,.68889,0,0,.54028],8487:[0,.68889,0,0,.72222],8498:[0,.68889,0,0,.55556],8502:[0,.68889,0,0,.66667],8503:[0,.68889,0,0,.44445],8504:[0,.68889,0,0,.66667],8513:[0,.68889,0,0,.63889],8592:[-.03598,.46402,0,0,.5],8594:[-.03598,.46402,0,0,.5],8602:[-.13313,.36687,0,0,1],8603:[-.13313,.36687,0,0,1],8606:[.01354,.52239,0,0,1],8608:[.01354,.52239,0,0,1],8610:[.01354,.52239,0,0,1.11111],8611:[.01354,.52239,0,0,1.11111],8619:[0,.54986,0,0,1],8620:[0,.54986,0,0,1],8621:[-.13313,.37788,0,0,1.38889],8622:[-.13313,.36687,0,0,1],8624:[0,.69224,0,0,.5],8625:[0,.69224,0,0,.5],8630:[0,.43056,0,0,1],8631:[0,.43056,0,0,1],8634:[.08198,.58198,0,0,.77778],8635:[.08198,.58198,0,0,.77778],8638:[.19444,.69224,0,0,.41667],8639:[.19444,.69224,0,0,.41667],8642:[.19444,.69224,0,0,.41667],8643:[.19444,.69224,0,0,.41667],8644:[.1808,.675,0,0,1],8646:[.1808,.675,0,0,1],8647:[.1808,.675,0,0,1],8648:[.19444,.69224,0,0,.83334],8649:[.1808,.675,0,0,1],8650:[.19444,.69224,0,0,.83334],8651:[.01354,.52239,0,0,1],8652:[.01354,.52239,0,0,1],8653:[-.13313,.36687,0,0,1],8654:[-.13313,.36687,0,0,1],8655:[-.13313,.36687,0,0,1],8666:[.13667,.63667,0,0,1],8667:[.13667,.63667,0,0,1],8669:[-.13313,.37788,0,0,1],8672:[-.064,.437,0,0,1.334],8674:[-.064,.437,0,0,1.334],8705:[0,.825,0,0,.5],8708:[0,.68889,0,0,.55556],8709:[.08167,.58167,0,0,.77778],8717:[0,.43056,0,0,.42917],8722:[-.03598,.46402,0,0,.5],8724:[.08198,.69224,0,0,.77778],8726:[.08167,.58167,0,0,.77778],8733:[0,.69224,0,0,.77778],8736:[0,.69224,0,0,.72222],8737:[0,.69224,0,0,.72222],8738:[.03517,.52239,0,0,.72222],8739:[.08167,.58167,0,0,.22222],8740:[.25142,.74111,0,0,.27778],8741:[.08167,.58167,0,0,.38889],8742:[.25142,.74111,0,0,.5],8756:[0,.69224,0,0,.66667],8757:[0,.69224,0,0,.66667],8764:[-.13313,.36687,0,0,.77778],8765:[-.13313,.37788,0,0,.77778],8769:[-.13313,.36687,0,0,.77778],8770:[-.03625,.46375,0,0,.77778],8774:[.30274,.79383,0,0,.77778],8776:[-.01688,.48312,0,0,.77778],8778:[.08167,.58167,0,0,.77778],8782:[.06062,.54986,0,0,.77778],8783:[.06062,.54986,0,0,.77778],8785:[.08198,.58198,0,0,.77778],8786:[.08198,.58198,0,0,.77778],8787:[.08198,.58198,0,0,.77778],8790:[0,.69224,0,0,.77778],8791:[.22958,.72958,0,0,.77778],8796:[.08198,.91667,0,0,.77778],8806:[.25583,.75583,0,0,.77778],8807:[.25583,.75583,0,0,.77778],8808:[.25142,.75726,0,0,.77778],8809:[.25142,.75726,0,0,.77778],8812:[.25583,.75583,0,0,.5],8814:[.20576,.70576,0,0,.77778],8815:[.20576,.70576,0,0,.77778],8816:[.30274,.79383,0,0,.77778],8817:[.30274,.79383,0,0,.77778],8818:[.22958,.72958,0,0,.77778],8819:[.22958,.72958,0,0,.77778],8822:[.1808,.675,0,0,.77778],8823:[.1808,.675,0,0,.77778],8828:[.13667,.63667,0,0,.77778],8829:[.13667,.63667,0,0,.77778],8830:[.22958,.72958,0,0,.77778],8831:[.22958,.72958,0,0,.77778],8832:[.20576,.70576,0,0,.77778],8833:[.20576,.70576,0,0,.77778],8840:[.30274,.79383,0,0,.77778],8841:[.30274,.79383,0,0,.77778],8842:[.13597,.63597,0,0,.77778],8843:[.13597,.63597,0,0,.77778],8847:[.03517,.54986,0,0,.77778],8848:[.03517,.54986,0,0,.77778],8858:[.08198,.58198,0,0,.77778],8859:[.08198,.58198,0,0,.77778],8861:[.08198,.58198,0,0,.77778],8862:[0,.675,0,0,.77778],8863:[0,.675,0,0,.77778],8864:[0,.675,0,0,.77778],8865:[0,.675,0,0,.77778],8872:[0,.69224,0,0,.61111],8873:[0,.69224,0,0,.72222],8874:[0,.69224,0,0,.88889],8876:[0,.68889,0,0,.61111],8877:[0,.68889,0,0,.61111],8878:[0,.68889,0,0,.72222],8879:[0,.68889,0,0,.72222],8882:[.03517,.54986,0,0,.77778],8883:[.03517,.54986,0,0,.77778],8884:[.13667,.63667,0,0,.77778],8885:[.13667,.63667,0,0,.77778],8888:[0,.54986,0,0,1.11111],8890:[.19444,.43056,0,0,.55556],8891:[.19444,.69224,0,0,.61111],8892:[.19444,.69224,0,0,.61111],8901:[0,.54986,0,0,.27778],8903:[.08167,.58167,0,0,.77778],8905:[.08167,.58167,0,0,.77778],8906:[.08167,.58167,0,0,.77778],8907:[0,.69224,0,0,.77778],8908:[0,.69224,0,0,.77778],8909:[-.03598,.46402,0,0,.77778],8910:[0,.54986,0,0,.76042],8911:[0,.54986,0,0,.76042],8912:[.03517,.54986,0,0,.77778],8913:[.03517,.54986,0,0,.77778],8914:[0,.54986,0,0,.66667],8915:[0,.54986,0,0,.66667],8916:[0,.69224,0,0,.66667],8918:[.0391,.5391,0,0,.77778],8919:[.0391,.5391,0,0,.77778],8920:[.03517,.54986,0,0,1.33334],8921:[.03517,.54986,0,0,1.33334],8922:[.38569,.88569,0,0,.77778],8923:[.38569,.88569,0,0,.77778],8926:[.13667,.63667,0,0,.77778],8927:[.13667,.63667,0,0,.77778],8928:[.30274,.79383,0,0,.77778],8929:[.30274,.79383,0,0,.77778],8934:[.23222,.74111,0,0,.77778],8935:[.23222,.74111,0,0,.77778],8936:[.23222,.74111,0,0,.77778],8937:[.23222,.74111,0,0,.77778],8938:[.20576,.70576,0,0,.77778],8939:[.20576,.70576,0,0,.77778],8940:[.30274,.79383,0,0,.77778],8941:[.30274,.79383,0,0,.77778],8994:[.19444,.69224,0,0,.77778],8995:[.19444,.69224,0,0,.77778],9416:[.15559,.69224,0,0,.90222],9484:[0,.69224,0,0,.5],9488:[0,.69224,0,0,.5],9492:[0,.37788,0,0,.5],9496:[0,.37788,0,0,.5],9585:[.19444,.68889,0,0,.88889],9586:[.19444,.74111,0,0,.88889],9632:[0,.675,0,0,.77778],9633:[0,.675,0,0,.77778],9650:[0,.54986,0,0,.72222],9651:[0,.54986,0,0,.72222],9654:[.03517,.54986,0,0,.77778],9660:[0,.54986,0,0,.72222],9661:[0,.54986,0,0,.72222],9664:[.03517,.54986,0,0,.77778],9674:[.11111,.69224,0,0,.66667],9733:[.19444,.69224,0,0,.94445],10003:[0,.69224,0,0,.83334],10016:[0,.69224,0,0,.83334],10731:[.11111,.69224,0,0,.66667],10846:[.19444,.75583,0,0,.61111],10877:[.13667,.63667,0,0,.77778],10878:[.13667,.63667,0,0,.77778],10885:[.25583,.75583,0,0,.77778],10886:[.25583,.75583,0,0,.77778],10887:[.13597,.63597,0,0,.77778],10888:[.13597,.63597,0,0,.77778],10889:[.26167,.75726,0,0,.77778],10890:[.26167,.75726,0,0,.77778],10891:[.48256,.98256,0,0,.77778],10892:[.48256,.98256,0,0,.77778],10901:[.13667,.63667,0,0,.77778],10902:[.13667,.63667,0,0,.77778],10933:[.25142,.75726,0,0,.77778],10934:[.25142,.75726,0,0,.77778],10935:[.26167,.75726,0,0,.77778],10936:[.26167,.75726,0,0,.77778],10937:[.26167,.75726,0,0,.77778],10938:[.26167,.75726,0,0,.77778],10949:[.25583,.75583,0,0,.77778],10950:[.25583,.75583,0,0,.77778],10955:[.28481,.79383,0,0,.77778],10956:[.28481,.79383,0,0,.77778],57350:[.08167,.58167,0,0,.22222],57351:[.08167,.58167,0,0,.38889],57352:[.08167,.58167,0,0,.77778],57353:[0,.43056,.04028,0,.66667],57356:[.25142,.75726,0,0,.77778],57357:[.25142,.75726,0,0,.77778],57358:[.41951,.91951,0,0,.77778],57359:[.30274,.79383,0,0,.77778],57360:[.30274,.79383,0,0,.77778],57361:[.41951,.91951,0,0,.77778],57366:[.25142,.75726,0,0,.77778],57367:[.25142,.75726,0,0,.77778],57368:[.25142,.75726,0,0,.77778],57369:[.25142,.75726,0,0,.77778],57370:[.13597,.63597,0,0,.77778],57371:[.13597,.63597,0,0,.77778]},\"Caligraphic-Regular\":{48:[0,.43056,0,0,.5],49:[0,.43056,0,0,.5],50:[0,.43056,0,0,.5],51:[.19444,.43056,0,0,.5],52:[.19444,.43056,0,0,.5],53:[.19444,.43056,0,0,.5],54:[0,.64444,0,0,.5],55:[.19444,.43056,0,0,.5],56:[0,.64444,0,0,.5],57:[.19444,.43056,0,0,.5],65:[0,.68333,0,.19445,.79847],66:[0,.68333,.03041,.13889,.65681],67:[0,.68333,.05834,.13889,.52653],68:[0,.68333,.02778,.08334,.77139],69:[0,.68333,.08944,.11111,.52778],70:[0,.68333,.09931,.11111,.71875],71:[.09722,.68333,.0593,.11111,.59487],72:[0,.68333,.00965,.11111,.84452],73:[0,.68333,.07382,0,.54452],74:[.09722,.68333,.18472,.16667,.67778],75:[0,.68333,.01445,.05556,.76195],76:[0,.68333,0,.13889,.68972],77:[0,.68333,0,.13889,1.2009],78:[0,.68333,.14736,.08334,.82049],79:[0,.68333,.02778,.11111,.79611],80:[0,.68333,.08222,.08334,.69556],81:[.09722,.68333,0,.11111,.81667],82:[0,.68333,0,.08334,.8475],83:[0,.68333,.075,.13889,.60556],84:[0,.68333,.25417,0,.54464],85:[0,.68333,.09931,.08334,.62583],86:[0,.68333,.08222,0,.61278],87:[0,.68333,.08222,.08334,.98778],88:[0,.68333,.14643,.13889,.7133],89:[.09722,.68333,.08222,.08334,.66834],90:[0,.68333,.07944,.13889,.72473]},\"Fraktur-Regular\":{33:[0,.69141,0,0,.29574],34:[0,.69141,0,0,.21471],38:[0,.69141,0,0,.73786],39:[0,.69141,0,0,.21201],40:[.24982,.74947,0,0,.38865],41:[.24982,.74947,0,0,.38865],42:[0,.62119,0,0,.27764],43:[.08319,.58283,0,0,.75623],44:[0,.10803,0,0,.27764],45:[.08319,.58283,0,0,.75623],46:[0,.10803,0,0,.27764],47:[.24982,.74947,0,0,.50181],48:[0,.47534,0,0,.50181],49:[0,.47534,0,0,.50181],50:[0,.47534,0,0,.50181],51:[.18906,.47534,0,0,.50181],52:[.18906,.47534,0,0,.50181],53:[.18906,.47534,0,0,.50181],54:[0,.69141,0,0,.50181],55:[.18906,.47534,0,0,.50181],56:[0,.69141,0,0,.50181],57:[.18906,.47534,0,0,.50181],58:[0,.47534,0,0,.21606],59:[.12604,.47534,0,0,.21606],61:[-.13099,.36866,0,0,.75623],63:[0,.69141,0,0,.36245],65:[0,.69141,0,0,.7176],66:[0,.69141,0,0,.88397],67:[0,.69141,0,0,.61254],68:[0,.69141,0,0,.83158],69:[0,.69141,0,0,.66278],70:[.12604,.69141,0,0,.61119],71:[0,.69141,0,0,.78539],72:[.06302,.69141,0,0,.7203],73:[0,.69141,0,0,.55448],74:[.12604,.69141,0,0,.55231],75:[0,.69141,0,0,.66845],76:[0,.69141,0,0,.66602],77:[0,.69141,0,0,1.04953],78:[0,.69141,0,0,.83212],79:[0,.69141,0,0,.82699],80:[.18906,.69141,0,0,.82753],81:[.03781,.69141,0,0,.82699],82:[0,.69141,0,0,.82807],83:[0,.69141,0,0,.82861],84:[0,.69141,0,0,.66899],85:[0,.69141,0,0,.64576],86:[0,.69141,0,0,.83131],87:[0,.69141,0,0,1.04602],88:[0,.69141,0,0,.71922],89:[.18906,.69141,0,0,.83293],90:[.12604,.69141,0,0,.60201],91:[.24982,.74947,0,0,.27764],93:[.24982,.74947,0,0,.27764],94:[0,.69141,0,0,.49965],97:[0,.47534,0,0,.50046],98:[0,.69141,0,0,.51315],99:[0,.47534,0,0,.38946],100:[0,.62119,0,0,.49857],101:[0,.47534,0,0,.40053],102:[.18906,.69141,0,0,.32626],103:[.18906,.47534,0,0,.5037],104:[.18906,.69141,0,0,.52126],105:[0,.69141,0,0,.27899],106:[0,.69141,0,0,.28088],107:[0,.69141,0,0,.38946],108:[0,.69141,0,0,.27953],109:[0,.47534,0,0,.76676],110:[0,.47534,0,0,.52666],111:[0,.47534,0,0,.48885],112:[.18906,.52396,0,0,.50046],113:[.18906,.47534,0,0,.48912],114:[0,.47534,0,0,.38919],115:[0,.47534,0,0,.44266],116:[0,.62119,0,0,.33301],117:[0,.47534,0,0,.5172],118:[0,.52396,0,0,.5118],119:[0,.52396,0,0,.77351],120:[.18906,.47534,0,0,.38865],121:[.18906,.47534,0,0,.49884],122:[.18906,.47534,0,0,.39054],8216:[0,.69141,0,0,.21471],8217:[0,.69141,0,0,.21471],58112:[0,.62119,0,0,.49749],58113:[0,.62119,0,0,.4983],58114:[.18906,.69141,0,0,.33328],58115:[.18906,.69141,0,0,.32923],58116:[.18906,.47534,0,0,.50343],58117:[0,.69141,0,0,.33301],58118:[0,.62119,0,0,.33409],58119:[0,.47534,0,0,.50073]},\"Main-Bold\":{33:[0,.69444,0,0,.35],34:[0,.69444,0,0,.60278],35:[.19444,.69444,0,0,.95833],36:[.05556,.75,0,0,.575],37:[.05556,.75,0,0,.95833],38:[0,.69444,0,0,.89444],39:[0,.69444,0,0,.31944],40:[.25,.75,0,0,.44722],41:[.25,.75,0,0,.44722],42:[0,.75,0,0,.575],43:[.13333,.63333,0,0,.89444],44:[.19444,.15556,0,0,.31944],45:[0,.44444,0,0,.38333],46:[0,.15556,0,0,.31944],47:[.25,.75,0,0,.575],48:[0,.64444,0,0,.575],49:[0,.64444,0,0,.575],50:[0,.64444,0,0,.575],51:[0,.64444,0,0,.575],52:[0,.64444,0,0,.575],53:[0,.64444,0,0,.575],54:[0,.64444,0,0,.575],55:[0,.64444,0,0,.575],56:[0,.64444,0,0,.575],57:[0,.64444,0,0,.575],58:[0,.44444,0,0,.31944],59:[.19444,.44444,0,0,.31944],60:[.08556,.58556,0,0,.89444],61:[-.10889,.39111,0,0,.89444],62:[.08556,.58556,0,0,.89444],63:[0,.69444,0,0,.54305],64:[0,.69444,0,0,.89444],65:[0,.68611,0,0,.86944],66:[0,.68611,0,0,.81805],67:[0,.68611,0,0,.83055],68:[0,.68611,0,0,.88194],69:[0,.68611,0,0,.75555],70:[0,.68611,0,0,.72361],71:[0,.68611,0,0,.90416],72:[0,.68611,0,0,.9],73:[0,.68611,0,0,.43611],74:[0,.68611,0,0,.59444],75:[0,.68611,0,0,.90138],76:[0,.68611,0,0,.69166],77:[0,.68611,0,0,1.09166],78:[0,.68611,0,0,.9],79:[0,.68611,0,0,.86388],80:[0,.68611,0,0,.78611],81:[.19444,.68611,0,0,.86388],82:[0,.68611,0,0,.8625],83:[0,.68611,0,0,.63889],84:[0,.68611,0,0,.8],85:[0,.68611,0,0,.88472],86:[0,.68611,.01597,0,.86944],87:[0,.68611,.01597,0,1.18888],88:[0,.68611,0,0,.86944],89:[0,.68611,.02875,0,.86944],90:[0,.68611,0,0,.70277],91:[.25,.75,0,0,.31944],92:[.25,.75,0,0,.575],93:[.25,.75,0,0,.31944],94:[0,.69444,0,0,.575],95:[.31,.13444,.03194,0,.575],97:[0,.44444,0,0,.55902],98:[0,.69444,0,0,.63889],99:[0,.44444,0,0,.51111],100:[0,.69444,0,0,.63889],101:[0,.44444,0,0,.52708],102:[0,.69444,.10903,0,.35139],103:[.19444,.44444,.01597,0,.575],104:[0,.69444,0,0,.63889],105:[0,.69444,0,0,.31944],106:[.19444,.69444,0,0,.35139],107:[0,.69444,0,0,.60694],108:[0,.69444,0,0,.31944],109:[0,.44444,0,0,.95833],110:[0,.44444,0,0,.63889],111:[0,.44444,0,0,.575],112:[.19444,.44444,0,0,.63889],113:[.19444,.44444,0,0,.60694],114:[0,.44444,0,0,.47361],115:[0,.44444,0,0,.45361],116:[0,.63492,0,0,.44722],117:[0,.44444,0,0,.63889],118:[0,.44444,.01597,0,.60694],119:[0,.44444,.01597,0,.83055],120:[0,.44444,0,0,.60694],121:[.19444,.44444,.01597,0,.60694],122:[0,.44444,0,0,.51111],123:[.25,.75,0,0,.575],124:[.25,.75,0,0,.31944],125:[.25,.75,0,0,.575],126:[.35,.34444,0,0,.575],168:[0,.69444,0,0,.575],172:[0,.44444,0,0,.76666],176:[0,.69444,0,0,.86944],177:[.13333,.63333,0,0,.89444],184:[.17014,0,0,0,.51111],198:[0,.68611,0,0,1.04166],215:[.13333,.63333,0,0,.89444],216:[.04861,.73472,0,0,.89444],223:[0,.69444,0,0,.59722],230:[0,.44444,0,0,.83055],247:[.13333,.63333,0,0,.89444],248:[.09722,.54167,0,0,.575],305:[0,.44444,0,0,.31944],338:[0,.68611,0,0,1.16944],339:[0,.44444,0,0,.89444],567:[.19444,.44444,0,0,.35139],710:[0,.69444,0,0,.575],711:[0,.63194,0,0,.575],713:[0,.59611,0,0,.575],714:[0,.69444,0,0,.575],715:[0,.69444,0,0,.575],728:[0,.69444,0,0,.575],729:[0,.69444,0,0,.31944],730:[0,.69444,0,0,.86944],732:[0,.69444,0,0,.575],733:[0,.69444,0,0,.575],915:[0,.68611,0,0,.69166],916:[0,.68611,0,0,.95833],920:[0,.68611,0,0,.89444],923:[0,.68611,0,0,.80555],926:[0,.68611,0,0,.76666],928:[0,.68611,0,0,.9],931:[0,.68611,0,0,.83055],933:[0,.68611,0,0,.89444],934:[0,.68611,0,0,.83055],936:[0,.68611,0,0,.89444],937:[0,.68611,0,0,.83055],8211:[0,.44444,.03194,0,.575],8212:[0,.44444,.03194,0,1.14999],8216:[0,.69444,0,0,.31944],8217:[0,.69444,0,0,.31944],8220:[0,.69444,0,0,.60278],8221:[0,.69444,0,0,.60278],8224:[.19444,.69444,0,0,.51111],8225:[.19444,.69444,0,0,.51111],8242:[0,.55556,0,0,.34444],8407:[0,.72444,.15486,0,.575],8463:[0,.69444,0,0,.66759],8465:[0,.69444,0,0,.83055],8467:[0,.69444,0,0,.47361],8472:[.19444,.44444,0,0,.74027],8476:[0,.69444,0,0,.83055],8501:[0,.69444,0,0,.70277],8592:[-.10889,.39111,0,0,1.14999],8593:[.19444,.69444,0,0,.575],8594:[-.10889,.39111,0,0,1.14999],8595:[.19444,.69444,0,0,.575],8596:[-.10889,.39111,0,0,1.14999],8597:[.25,.75,0,0,.575],8598:[.19444,.69444,0,0,1.14999],8599:[.19444,.69444,0,0,1.14999],8600:[.19444,.69444,0,0,1.14999],8601:[.19444,.69444,0,0,1.14999],8636:[-.10889,.39111,0,0,1.14999],8637:[-.10889,.39111,0,0,1.14999],8640:[-.10889,.39111,0,0,1.14999],8641:[-.10889,.39111,0,0,1.14999],8656:[-.10889,.39111,0,0,1.14999],8657:[.19444,.69444,0,0,.70277],8658:[-.10889,.39111,0,0,1.14999],8659:[.19444,.69444,0,0,.70277],8660:[-.10889,.39111,0,0,1.14999],8661:[.25,.75,0,0,.70277],8704:[0,.69444,0,0,.63889],8706:[0,.69444,.06389,0,.62847],8707:[0,.69444,0,0,.63889],8709:[.05556,.75,0,0,.575],8711:[0,.68611,0,0,.95833],8712:[.08556,.58556,0,0,.76666],8715:[.08556,.58556,0,0,.76666],8722:[.13333,.63333,0,0,.89444],8723:[.13333,.63333,0,0,.89444],8725:[.25,.75,0,0,.575],8726:[.25,.75,0,0,.575],8727:[-.02778,.47222,0,0,.575],8728:[-.02639,.47361,0,0,.575],8729:[-.02639,.47361,0,0,.575],8730:[.18,.82,0,0,.95833],8733:[0,.44444,0,0,.89444],8734:[0,.44444,0,0,1.14999],8736:[0,.69224,0,0,.72222],8739:[.25,.75,0,0,.31944],8741:[.25,.75,0,0,.575],8743:[0,.55556,0,0,.76666],8744:[0,.55556,0,0,.76666],8745:[0,.55556,0,0,.76666],8746:[0,.55556,0,0,.76666],8747:[.19444,.69444,.12778,0,.56875],8764:[-.10889,.39111,0,0,.89444],8768:[.19444,.69444,0,0,.31944],8771:[.00222,.50222,0,0,.89444],8776:[.02444,.52444,0,0,.89444],8781:[.00222,.50222,0,0,.89444],8801:[.00222,.50222,0,0,.89444],8804:[.19667,.69667,0,0,.89444],8805:[.19667,.69667,0,0,.89444],8810:[.08556,.58556,0,0,1.14999],8811:[.08556,.58556,0,0,1.14999],8826:[.08556,.58556,0,0,.89444],8827:[.08556,.58556,0,0,.89444],8834:[.08556,.58556,0,0,.89444],8835:[.08556,.58556,0,0,.89444],8838:[.19667,.69667,0,0,.89444],8839:[.19667,.69667,0,0,.89444],8846:[0,.55556,0,0,.76666],8849:[.19667,.69667,0,0,.89444],8850:[.19667,.69667,0,0,.89444],8851:[0,.55556,0,0,.76666],8852:[0,.55556,0,0,.76666],8853:[.13333,.63333,0,0,.89444],8854:[.13333,.63333,0,0,.89444],8855:[.13333,.63333,0,0,.89444],8856:[.13333,.63333,0,0,.89444],8857:[.13333,.63333,0,0,.89444],8866:[0,.69444,0,0,.70277],8867:[0,.69444,0,0,.70277],8868:[0,.69444,0,0,.89444],8869:[0,.69444,0,0,.89444],8900:[-.02639,.47361,0,0,.575],8901:[-.02639,.47361,0,0,.31944],8902:[-.02778,.47222,0,0,.575],8968:[.25,.75,0,0,.51111],8969:[.25,.75,0,0,.51111],8970:[.25,.75,0,0,.51111],8971:[.25,.75,0,0,.51111],8994:[-.13889,.36111,0,0,1.14999],8995:[-.13889,.36111,0,0,1.14999],9651:[.19444,.69444,0,0,1.02222],9657:[-.02778,.47222,0,0,.575],9661:[.19444,.69444,0,0,1.02222],9667:[-.02778,.47222,0,0,.575],9711:[.19444,.69444,0,0,1.14999],9824:[.12963,.69444,0,0,.89444],9825:[.12963,.69444,0,0,.89444],9826:[.12963,.69444,0,0,.89444],9827:[.12963,.69444,0,0,.89444],9837:[0,.75,0,0,.44722],9838:[.19444,.69444,0,0,.44722],9839:[.19444,.69444,0,0,.44722],10216:[.25,.75,0,0,.44722],10217:[.25,.75,0,0,.44722],10815:[0,.68611,0,0,.9],10927:[.19667,.69667,0,0,.89444],10928:[.19667,.69667,0,0,.89444],57376:[.19444,.69444,0,0,0]},\"Main-BoldItalic\":{33:[0,.69444,.11417,0,.38611],34:[0,.69444,.07939,0,.62055],35:[.19444,.69444,.06833,0,.94444],37:[.05556,.75,.12861,0,.94444],38:[0,.69444,.08528,0,.88555],39:[0,.69444,.12945,0,.35555],40:[.25,.75,.15806,0,.47333],41:[.25,.75,.03306,0,.47333],42:[0,.75,.14333,0,.59111],43:[.10333,.60333,.03306,0,.88555],44:[.19444,.14722,0,0,.35555],45:[0,.44444,.02611,0,.41444],46:[0,.14722,0,0,.35555],47:[.25,.75,.15806,0,.59111],48:[0,.64444,.13167,0,.59111],49:[0,.64444,.13167,0,.59111],50:[0,.64444,.13167,0,.59111],51:[0,.64444,.13167,0,.59111],52:[.19444,.64444,.13167,0,.59111],53:[0,.64444,.13167,0,.59111],54:[0,.64444,.13167,0,.59111],55:[.19444,.64444,.13167,0,.59111],56:[0,.64444,.13167,0,.59111],57:[0,.64444,.13167,0,.59111],58:[0,.44444,.06695,0,.35555],59:[.19444,.44444,.06695,0,.35555],61:[-.10889,.39111,.06833,0,.88555],63:[0,.69444,.11472,0,.59111],64:[0,.69444,.09208,0,.88555],65:[0,.68611,0,0,.86555],66:[0,.68611,.0992,0,.81666],67:[0,.68611,.14208,0,.82666],68:[0,.68611,.09062,0,.87555],69:[0,.68611,.11431,0,.75666],70:[0,.68611,.12903,0,.72722],71:[0,.68611,.07347,0,.89527],72:[0,.68611,.17208,0,.8961],73:[0,.68611,.15681,0,.47166],74:[0,.68611,.145,0,.61055],75:[0,.68611,.14208,0,.89499],76:[0,.68611,0,0,.69777],77:[0,.68611,.17208,0,1.07277],78:[0,.68611,.17208,0,.8961],79:[0,.68611,.09062,0,.85499],80:[0,.68611,.0992,0,.78721],81:[.19444,.68611,.09062,0,.85499],82:[0,.68611,.02559,0,.85944],83:[0,.68611,.11264,0,.64999],84:[0,.68611,.12903,0,.7961],85:[0,.68611,.17208,0,.88083],86:[0,.68611,.18625,0,.86555],87:[0,.68611,.18625,0,1.15999],88:[0,.68611,.15681,0,.86555],89:[0,.68611,.19803,0,.86555],90:[0,.68611,.14208,0,.70888],91:[.25,.75,.1875,0,.35611],93:[.25,.75,.09972,0,.35611],94:[0,.69444,.06709,0,.59111],95:[.31,.13444,.09811,0,.59111],97:[0,.44444,.09426,0,.59111],98:[0,.69444,.07861,0,.53222],99:[0,.44444,.05222,0,.53222],100:[0,.69444,.10861,0,.59111],101:[0,.44444,.085,0,.53222],102:[.19444,.69444,.21778,0,.4],103:[.19444,.44444,.105,0,.53222],104:[0,.69444,.09426,0,.59111],105:[0,.69326,.11387,0,.35555],106:[.19444,.69326,.1672,0,.35555],107:[0,.69444,.11111,0,.53222],108:[0,.69444,.10861,0,.29666],109:[0,.44444,.09426,0,.94444],110:[0,.44444,.09426,0,.64999],111:[0,.44444,.07861,0,.59111],112:[.19444,.44444,.07861,0,.59111],113:[.19444,.44444,.105,0,.53222],114:[0,.44444,.11111,0,.50167],115:[0,.44444,.08167,0,.48694],116:[0,.63492,.09639,0,.385],117:[0,.44444,.09426,0,.62055],118:[0,.44444,.11111,0,.53222],119:[0,.44444,.11111,0,.76777],120:[0,.44444,.12583,0,.56055],121:[.19444,.44444,.105,0,.56166],122:[0,.44444,.13889,0,.49055],126:[.35,.34444,.11472,0,.59111],163:[0,.69444,0,0,.86853],168:[0,.69444,.11473,0,.59111],176:[0,.69444,0,0,.94888],184:[.17014,0,0,0,.53222],198:[0,.68611,.11431,0,1.02277],216:[.04861,.73472,.09062,0,.88555],223:[.19444,.69444,.09736,0,.665],230:[0,.44444,.085,0,.82666],248:[.09722,.54167,.09458,0,.59111],305:[0,.44444,.09426,0,.35555],338:[0,.68611,.11431,0,1.14054],339:[0,.44444,.085,0,.82666],567:[.19444,.44444,.04611,0,.385],710:[0,.69444,.06709,0,.59111],711:[0,.63194,.08271,0,.59111],713:[0,.59444,.10444,0,.59111],714:[0,.69444,.08528,0,.59111],715:[0,.69444,0,0,.59111],728:[0,.69444,.10333,0,.59111],729:[0,.69444,.12945,0,.35555],730:[0,.69444,0,0,.94888],732:[0,.69444,.11472,0,.59111],733:[0,.69444,.11472,0,.59111],915:[0,.68611,.12903,0,.69777],916:[0,.68611,0,0,.94444],920:[0,.68611,.09062,0,.88555],923:[0,.68611,0,0,.80666],926:[0,.68611,.15092,0,.76777],928:[0,.68611,.17208,0,.8961],931:[0,.68611,.11431,0,.82666],933:[0,.68611,.10778,0,.88555],934:[0,.68611,.05632,0,.82666],936:[0,.68611,.10778,0,.88555],937:[0,.68611,.0992,0,.82666],8211:[0,.44444,.09811,0,.59111],8212:[0,.44444,.09811,0,1.18221],8216:[0,.69444,.12945,0,.35555],8217:[0,.69444,.12945,0,.35555],8220:[0,.69444,.16772,0,.62055],8221:[0,.69444,.07939,0,.62055]},\"Main-Italic\":{33:[0,.69444,.12417,0,.30667],34:[0,.69444,.06961,0,.51444],35:[.19444,.69444,.06616,0,.81777],37:[.05556,.75,.13639,0,.81777],38:[0,.69444,.09694,0,.76666],39:[0,.69444,.12417,0,.30667],40:[.25,.75,.16194,0,.40889],41:[.25,.75,.03694,0,.40889],42:[0,.75,.14917,0,.51111],43:[.05667,.56167,.03694,0,.76666],44:[.19444,.10556,0,0,.30667],45:[0,.43056,.02826,0,.35778],46:[0,.10556,0,0,.30667],47:[.25,.75,.16194,0,.51111],48:[0,.64444,.13556,0,.51111],49:[0,.64444,.13556,0,.51111],50:[0,.64444,.13556,0,.51111],51:[0,.64444,.13556,0,.51111],52:[.19444,.64444,.13556,0,.51111],53:[0,.64444,.13556,0,.51111],54:[0,.64444,.13556,0,.51111],55:[.19444,.64444,.13556,0,.51111],56:[0,.64444,.13556,0,.51111],57:[0,.64444,.13556,0,.51111],58:[0,.43056,.0582,0,.30667],59:[.19444,.43056,.0582,0,.30667],61:[-.13313,.36687,.06616,0,.76666],63:[0,.69444,.1225,0,.51111],64:[0,.69444,.09597,0,.76666],65:[0,.68333,0,0,.74333],66:[0,.68333,.10257,0,.70389],67:[0,.68333,.14528,0,.71555],68:[0,.68333,.09403,0,.755],69:[0,.68333,.12028,0,.67833],70:[0,.68333,.13305,0,.65277],71:[0,.68333,.08722,0,.77361],72:[0,.68333,.16389,0,.74333],73:[0,.68333,.15806,0,.38555],74:[0,.68333,.14028,0,.525],75:[0,.68333,.14528,0,.76888],76:[0,.68333,0,0,.62722],77:[0,.68333,.16389,0,.89666],78:[0,.68333,.16389,0,.74333],79:[0,.68333,.09403,0,.76666],80:[0,.68333,.10257,0,.67833],81:[.19444,.68333,.09403,0,.76666],82:[0,.68333,.03868,0,.72944],83:[0,.68333,.11972,0,.56222],84:[0,.68333,.13305,0,.71555],85:[0,.68333,.16389,0,.74333],86:[0,.68333,.18361,0,.74333],87:[0,.68333,.18361,0,.99888],88:[0,.68333,.15806,0,.74333],89:[0,.68333,.19383,0,.74333],90:[0,.68333,.14528,0,.61333],91:[.25,.75,.1875,0,.30667],93:[.25,.75,.10528,0,.30667],94:[0,.69444,.06646,0,.51111],95:[.31,.12056,.09208,0,.51111],97:[0,.43056,.07671,0,.51111],98:[0,.69444,.06312,0,.46],99:[0,.43056,.05653,0,.46],100:[0,.69444,.10333,0,.51111],101:[0,.43056,.07514,0,.46],102:[.19444,.69444,.21194,0,.30667],103:[.19444,.43056,.08847,0,.46],104:[0,.69444,.07671,0,.51111],105:[0,.65536,.1019,0,.30667],106:[.19444,.65536,.14467,0,.30667],107:[0,.69444,.10764,0,.46],108:[0,.69444,.10333,0,.25555],109:[0,.43056,.07671,0,.81777],110:[0,.43056,.07671,0,.56222],111:[0,.43056,.06312,0,.51111],112:[.19444,.43056,.06312,0,.51111],113:[.19444,.43056,.08847,0,.46],114:[0,.43056,.10764,0,.42166],115:[0,.43056,.08208,0,.40889],116:[0,.61508,.09486,0,.33222],117:[0,.43056,.07671,0,.53666],118:[0,.43056,.10764,0,.46],119:[0,.43056,.10764,0,.66444],120:[0,.43056,.12042,0,.46389],121:[.19444,.43056,.08847,0,.48555],122:[0,.43056,.12292,0,.40889],126:[.35,.31786,.11585,0,.51111],163:[0,.69444,0,0,.76909],168:[0,.66786,.10474,0,.51111],176:[0,.69444,0,0,.83129],184:[.17014,0,0,0,.46],198:[0,.68333,.12028,0,.88277],216:[.04861,.73194,.09403,0,.76666],223:[.19444,.69444,.10514,0,.53666],230:[0,.43056,.07514,0,.71555],248:[.09722,.52778,.09194,0,.51111],305:[0,.43056,0,.02778,.32246],338:[0,.68333,.12028,0,.98499],339:[0,.43056,.07514,0,.71555],567:[.19444,.43056,0,.08334,.38403],710:[0,.69444,.06646,0,.51111],711:[0,.62847,.08295,0,.51111],713:[0,.56167,.10333,0,.51111],714:[0,.69444,.09694,0,.51111],715:[0,.69444,0,0,.51111],728:[0,.69444,.10806,0,.51111],729:[0,.66786,.11752,0,.30667],730:[0,.69444,0,0,.83129],732:[0,.66786,.11585,0,.51111],733:[0,.69444,.1225,0,.51111],915:[0,.68333,.13305,0,.62722],916:[0,.68333,0,0,.81777],920:[0,.68333,.09403,0,.76666],923:[0,.68333,0,0,.69222],926:[0,.68333,.15294,0,.66444],928:[0,.68333,.16389,0,.74333],931:[0,.68333,.12028,0,.71555],933:[0,.68333,.11111,0,.76666],934:[0,.68333,.05986,0,.71555],936:[0,.68333,.11111,0,.76666],937:[0,.68333,.10257,0,.71555],8211:[0,.43056,.09208,0,.51111],8212:[0,.43056,.09208,0,1.02222],8216:[0,.69444,.12417,0,.30667],8217:[0,.69444,.12417,0,.30667],8220:[0,.69444,.1685,0,.51444],8221:[0,.69444,.06961,0,.51444],8463:[0,.68889,0,0,.54028]},\"Main-Regular\":{32:[0,0,0,0,.25],33:[0,.69444,0,0,.27778],34:[0,.69444,0,0,.5],35:[.19444,.69444,0,0,.83334],36:[.05556,.75,0,0,.5],37:[.05556,.75,0,0,.83334],38:[0,.69444,0,0,.77778],39:[0,.69444,0,0,.27778],40:[.25,.75,0,0,.38889],41:[.25,.75,0,0,.38889],42:[0,.75,0,0,.5],43:[.08333,.58333,0,0,.77778],44:[.19444,.10556,0,0,.27778],45:[0,.43056,0,0,.33333],46:[0,.10556,0,0,.27778],47:[.25,.75,0,0,.5],48:[0,.64444,0,0,.5],49:[0,.64444,0,0,.5],50:[0,.64444,0,0,.5],51:[0,.64444,0,0,.5],52:[0,.64444,0,0,.5],53:[0,.64444,0,0,.5],54:[0,.64444,0,0,.5],55:[0,.64444,0,0,.5],56:[0,.64444,0,0,.5],57:[0,.64444,0,0,.5],58:[0,.43056,0,0,.27778],59:[.19444,.43056,0,0,.27778],60:[.0391,.5391,0,0,.77778],61:[-.13313,.36687,0,0,.77778],62:[.0391,.5391,0,0,.77778],63:[0,.69444,0,0,.47222],64:[0,.69444,0,0,.77778],65:[0,.68333,0,0,.75],66:[0,.68333,0,0,.70834],67:[0,.68333,0,0,.72222],68:[0,.68333,0,0,.76389],69:[0,.68333,0,0,.68056],70:[0,.68333,0,0,.65278],71:[0,.68333,0,0,.78472],72:[0,.68333,0,0,.75],73:[0,.68333,0,0,.36111],74:[0,.68333,0,0,.51389],75:[0,.68333,0,0,.77778],76:[0,.68333,0,0,.625],77:[0,.68333,0,0,.91667],78:[0,.68333,0,0,.75],79:[0,.68333,0,0,.77778],80:[0,.68333,0,0,.68056],81:[.19444,.68333,0,0,.77778],82:[0,.68333,0,0,.73611],83:[0,.68333,0,0,.55556],84:[0,.68333,0,0,.72222],85:[0,.68333,0,0,.75],86:[0,.68333,.01389,0,.75],87:[0,.68333,.01389,0,1.02778],88:[0,.68333,0,0,.75],89:[0,.68333,.025,0,.75],90:[0,.68333,0,0,.61111],91:[.25,.75,0,0,.27778],92:[.25,.75,0,0,.5],93:[.25,.75,0,0,.27778],94:[0,.69444,0,0,.5],95:[.31,.12056,.02778,0,.5],97:[0,.43056,0,0,.5],98:[0,.69444,0,0,.55556],99:[0,.43056,0,0,.44445],100:[0,.69444,0,0,.55556],101:[0,.43056,0,0,.44445],102:[0,.69444,.07778,0,.30556],103:[.19444,.43056,.01389,0,.5],104:[0,.69444,0,0,.55556],105:[0,.66786,0,0,.27778],106:[.19444,.66786,0,0,.30556],107:[0,.69444,0,0,.52778],108:[0,.69444,0,0,.27778],109:[0,.43056,0,0,.83334],110:[0,.43056,0,0,.55556],111:[0,.43056,0,0,.5],112:[.19444,.43056,0,0,.55556],113:[.19444,.43056,0,0,.52778],114:[0,.43056,0,0,.39167],115:[0,.43056,0,0,.39445],116:[0,.61508,0,0,.38889],117:[0,.43056,0,0,.55556],118:[0,.43056,.01389,0,.52778],119:[0,.43056,.01389,0,.72222],120:[0,.43056,0,0,.52778],121:[.19444,.43056,.01389,0,.52778],122:[0,.43056,0,0,.44445],123:[.25,.75,0,0,.5],124:[.25,.75,0,0,.27778],125:[.25,.75,0,0,.5],126:[.35,.31786,0,0,.5],160:[0,0,0,0,.25],167:[.19444,.69444,0,0,.44445],168:[0,.66786,0,0,.5],172:[0,.43056,0,0,.66667],176:[0,.69444,0,0,.75],177:[.08333,.58333,0,0,.77778],182:[.19444,.69444,0,0,.61111],184:[.17014,0,0,0,.44445],198:[0,.68333,0,0,.90278],215:[.08333,.58333,0,0,.77778],216:[.04861,.73194,0,0,.77778],223:[0,.69444,0,0,.5],230:[0,.43056,0,0,.72222],247:[.08333,.58333,0,0,.77778],248:[.09722,.52778,0,0,.5],305:[0,.43056,0,0,.27778],338:[0,.68333,0,0,1.01389],339:[0,.43056,0,0,.77778],567:[.19444,.43056,0,0,.30556],710:[0,.69444,0,0,.5],711:[0,.62847,0,0,.5],713:[0,.56778,0,0,.5],714:[0,.69444,0,0,.5],715:[0,.69444,0,0,.5],728:[0,.69444,0,0,.5],729:[0,.66786,0,0,.27778],730:[0,.69444,0,0,.75],732:[0,.66786,0,0,.5],733:[0,.69444,0,0,.5],915:[0,.68333,0,0,.625],916:[0,.68333,0,0,.83334],920:[0,.68333,0,0,.77778],923:[0,.68333,0,0,.69445],926:[0,.68333,0,0,.66667],928:[0,.68333,0,0,.75],931:[0,.68333,0,0,.72222],933:[0,.68333,0,0,.77778],934:[0,.68333,0,0,.72222],936:[0,.68333,0,0,.77778],937:[0,.68333,0,0,.72222],8211:[0,.43056,.02778,0,.5],8212:[0,.43056,.02778,0,1],8216:[0,.69444,0,0,.27778],8217:[0,.69444,0,0,.27778],8220:[0,.69444,0,0,.5],8221:[0,.69444,0,0,.5],8224:[.19444,.69444,0,0,.44445],8225:[.19444,.69444,0,0,.44445],8230:[0,.12,0,0,1.172],8242:[0,.55556,0,0,.275],8407:[0,.71444,.15382,0,.5],8463:[0,.68889,0,0,.54028],8465:[0,.69444,0,0,.72222],8467:[0,.69444,0,.11111,.41667],8472:[.19444,.43056,0,.11111,.63646],8476:[0,.69444,0,0,.72222],8501:[0,.69444,0,0,.61111],8592:[-.13313,.36687,0,0,1],8593:[.19444,.69444,0,0,.5],8594:[-.13313,.36687,0,0,1],8595:[.19444,.69444,0,0,.5],8596:[-.13313,.36687,0,0,1],8597:[.25,.75,0,0,.5],8598:[.19444,.69444,0,0,1],8599:[.19444,.69444,0,0,1],8600:[.19444,.69444,0,0,1],8601:[.19444,.69444,0,0,1],8614:[.011,.511,0,0,1],8617:[.011,.511,0,0,1.126],8618:[.011,.511,0,0,1.126],8636:[-.13313,.36687,0,0,1],8637:[-.13313,.36687,0,0,1],8640:[-.13313,.36687,0,0,1],8641:[-.13313,.36687,0,0,1],8652:[.011,.671,0,0,1],8656:[-.13313,.36687,0,0,1],8657:[.19444,.69444,0,0,.61111],8658:[-.13313,.36687,0,0,1],8659:[.19444,.69444,0,0,.61111],8660:[-.13313,.36687,0,0,1],8661:[.25,.75,0,0,.61111],8704:[0,.69444,0,0,.55556],8706:[0,.69444,.05556,.08334,.5309],8707:[0,.69444,0,0,.55556],8709:[.05556,.75,0,0,.5],8711:[0,.68333,0,0,.83334],8712:[.0391,.5391,0,0,.66667],8715:[.0391,.5391,0,0,.66667],8722:[.08333,.58333,0,0,.77778],8723:[.08333,.58333,0,0,.77778],8725:[.25,.75,0,0,.5],8726:[.25,.75,0,0,.5],8727:[-.03472,.46528,0,0,.5],8728:[-.05555,.44445,0,0,.5],8729:[-.05555,.44445,0,0,.5],8730:[.2,.8,0,0,.83334],8733:[0,.43056,0,0,.77778],8734:[0,.43056,0,0,1],8736:[0,.69224,0,0,.72222],8739:[.25,.75,0,0,.27778],8741:[.25,.75,0,0,.5],8743:[0,.55556,0,0,.66667],8744:[0,.55556,0,0,.66667],8745:[0,.55556,0,0,.66667],8746:[0,.55556,0,0,.66667],8747:[.19444,.69444,.11111,0,.41667],8764:[-.13313,.36687,0,0,.77778],8768:[.19444,.69444,0,0,.27778],8771:[-.03625,.46375,0,0,.77778],8773:[-.022,.589,0,0,1],8776:[-.01688,.48312,0,0,.77778],8781:[-.03625,.46375,0,0,.77778],8784:[-.133,.67,0,0,.778],8801:[-.03625,.46375,0,0,.77778],8804:[.13597,.63597,0,0,.77778],8805:[.13597,.63597,0,0,.77778],8810:[.0391,.5391,0,0,1],8811:[.0391,.5391,0,0,1],8826:[.0391,.5391,0,0,.77778],8827:[.0391,.5391,0,0,.77778],8834:[.0391,.5391,0,0,.77778],8835:[.0391,.5391,0,0,.77778],8838:[.13597,.63597,0,0,.77778],8839:[.13597,.63597,0,0,.77778],8846:[0,.55556,0,0,.66667],8849:[.13597,.63597,0,0,.77778],8850:[.13597,.63597,0,0,.77778],8851:[0,.55556,0,0,.66667],8852:[0,.55556,0,0,.66667],8853:[.08333,.58333,0,0,.77778],8854:[.08333,.58333,0,0,.77778],8855:[.08333,.58333,0,0,.77778],8856:[.08333,.58333,0,0,.77778],8857:[.08333,.58333,0,0,.77778],8866:[0,.69444,0,0,.61111],8867:[0,.69444,0,0,.61111],8868:[0,.69444,0,0,.77778],8869:[0,.69444,0,0,.77778],8872:[.249,.75,0,0,.867],8900:[-.05555,.44445,0,0,.5],8901:[-.05555,.44445,0,0,.27778],8902:[-.03472,.46528,0,0,.5],8904:[.005,.505,0,0,.9],8942:[.03,.9,0,0,.278],8943:[-.19,.31,0,0,1.172],8945:[-.1,.82,0,0,1.282],8968:[.25,.75,0,0,.44445],8969:[.25,.75,0,0,.44445],8970:[.25,.75,0,0,.44445],8971:[.25,.75,0,0,.44445],8994:[-.14236,.35764,0,0,1],8995:[-.14236,.35764,0,0,1],9136:[.244,.744,0,0,.412],9137:[.244,.744,0,0,.412],9651:[.19444,.69444,0,0,.88889],9657:[-.03472,.46528,0,0,.5],9661:[.19444,.69444,0,0,.88889],9667:[-.03472,.46528,0,0,.5],9711:[.19444,.69444,0,0,1],9824:[.12963,.69444,0,0,.77778],9825:[.12963,.69444,0,0,.77778],9826:[.12963,.69444,0,0,.77778],9827:[.12963,.69444,0,0,.77778],9837:[0,.75,0,0,.38889],9838:[.19444,.69444,0,0,.38889],9839:[.19444,.69444,0,0,.38889],10216:[.25,.75,0,0,.38889],10217:[.25,.75,0,0,.38889],10222:[.244,.744,0,0,.412],10223:[.244,.744,0,0,.412],10229:[.011,.511,0,0,1.609],10230:[.011,.511,0,0,1.638],10231:[.011,.511,0,0,1.859],10232:[.024,.525,0,0,1.609],10233:[.024,.525,0,0,1.638],10234:[.024,.525,0,0,1.858],10236:[.011,.511,0,0,1.638],10815:[0,.68333,0,0,.75],10927:[.13597,.63597,0,0,.77778],10928:[.13597,.63597,0,0,.77778],57376:[.19444,.69444,0,0,0]},\"Math-BoldItalic\":{65:[0,.68611,0,0,.86944],66:[0,.68611,.04835,0,.8664],67:[0,.68611,.06979,0,.81694],68:[0,.68611,.03194,0,.93812],69:[0,.68611,.05451,0,.81007],70:[0,.68611,.15972,0,.68889],71:[0,.68611,0,0,.88673],72:[0,.68611,.08229,0,.98229],73:[0,.68611,.07778,0,.51111],74:[0,.68611,.10069,0,.63125],75:[0,.68611,.06979,0,.97118],76:[0,.68611,0,0,.75555],77:[0,.68611,.11424,0,1.14201],78:[0,.68611,.11424,0,.95034],79:[0,.68611,.03194,0,.83666],80:[0,.68611,.15972,0,.72309],81:[.19444,.68611,0,0,.86861],82:[0,.68611,.00421,0,.87235],83:[0,.68611,.05382,0,.69271],84:[0,.68611,.15972,0,.63663],85:[0,.68611,.11424,0,.80027],86:[0,.68611,.25555,0,.67778],87:[0,.68611,.15972,0,1.09305],88:[0,.68611,.07778,0,.94722],89:[0,.68611,.25555,0,.67458],90:[0,.68611,.06979,0,.77257],97:[0,.44444,0,0,.63287],98:[0,.69444,0,0,.52083],99:[0,.44444,0,0,.51342],100:[0,.69444,0,0,.60972],101:[0,.44444,0,0,.55361],102:[.19444,.69444,.11042,0,.56806],103:[.19444,.44444,.03704,0,.5449],104:[0,.69444,0,0,.66759],105:[0,.69326,0,0,.4048],106:[.19444,.69326,.0622,0,.47083],107:[0,.69444,.01852,0,.6037],108:[0,.69444,.0088,0,.34815],109:[0,.44444,0,0,1.0324],110:[0,.44444,0,0,.71296],111:[0,.44444,0,0,.58472],112:[.19444,.44444,0,0,.60092],113:[.19444,.44444,.03704,0,.54213],114:[0,.44444,.03194,0,.5287],115:[0,.44444,0,0,.53125],116:[0,.63492,0,0,.41528],117:[0,.44444,0,0,.68102],118:[0,.44444,.03704,0,.56666],119:[0,.44444,.02778,0,.83148],120:[0,.44444,0,0,.65903],121:[.19444,.44444,.03704,0,.59028],122:[0,.44444,.04213,0,.55509],915:[0,.68611,.15972,0,.65694],916:[0,.68611,0,0,.95833],920:[0,.68611,.03194,0,.86722],923:[0,.68611,0,0,.80555],926:[0,.68611,.07458,0,.84125],928:[0,.68611,.08229,0,.98229],931:[0,.68611,.05451,0,.88507],933:[0,.68611,.15972,0,.67083],934:[0,.68611,0,0,.76666],936:[0,.68611,.11653,0,.71402],937:[0,.68611,.04835,0,.8789],945:[0,.44444,0,0,.76064],946:[.19444,.69444,.03403,0,.65972],947:[.19444,.44444,.06389,0,.59003],948:[0,.69444,.03819,0,.52222],949:[0,.44444,0,0,.52882],950:[.19444,.69444,.06215,0,.50833],951:[.19444,.44444,.03704,0,.6],952:[0,.69444,.03194,0,.5618],953:[0,.44444,0,0,.41204],954:[0,.44444,0,0,.66759],955:[0,.69444,0,0,.67083],956:[.19444,.44444,0,0,.70787],957:[0,.44444,.06898,0,.57685],958:[.19444,.69444,.03021,0,.50833],959:[0,.44444,0,0,.58472],960:[0,.44444,.03704,0,.68241],961:[.19444,.44444,0,0,.6118],962:[.09722,.44444,.07917,0,.42361],963:[0,.44444,.03704,0,.68588],964:[0,.44444,.13472,0,.52083],965:[0,.44444,.03704,0,.63055],966:[.19444,.44444,0,0,.74722],967:[.19444,.44444,0,0,.71805],968:[.19444,.69444,.03704,0,.75833],969:[0,.44444,.03704,0,.71782],977:[0,.69444,0,0,.69155],981:[.19444,.69444,0,0,.7125],982:[0,.44444,.03194,0,.975],1009:[.19444,.44444,0,0,.6118],1013:[0,.44444,0,0,.48333]},\"Math-Italic\":{65:[0,.68333,0,.13889,.75],66:[0,.68333,.05017,.08334,.75851],67:[0,.68333,.07153,.08334,.71472],68:[0,.68333,.02778,.05556,.82792],69:[0,.68333,.05764,.08334,.7382],70:[0,.68333,.13889,.08334,.64306],71:[0,.68333,0,.08334,.78625],72:[0,.68333,.08125,.05556,.83125],73:[0,.68333,.07847,.11111,.43958],74:[0,.68333,.09618,.16667,.55451],75:[0,.68333,.07153,.05556,.84931],76:[0,.68333,0,.02778,.68056],77:[0,.68333,.10903,.08334,.97014],78:[0,.68333,.10903,.08334,.80347],79:[0,.68333,.02778,.08334,.76278],80:[0,.68333,.13889,.08334,.64201],81:[.19444,.68333,0,.08334,.79056],82:[0,.68333,.00773,.08334,.75929],83:[0,.68333,.05764,.08334,.6132],84:[0,.68333,.13889,.08334,.58438],85:[0,.68333,.10903,.02778,.68278],86:[0,.68333,.22222,0,.58333],87:[0,.68333,.13889,0,.94445],88:[0,.68333,.07847,.08334,.82847],89:[0,.68333,.22222,0,.58056],90:[0,.68333,.07153,.08334,.68264],97:[0,.43056,0,0,.52859],98:[0,.69444,0,0,.42917],99:[0,.43056,0,.05556,.43276],100:[0,.69444,0,.16667,.52049],101:[0,.43056,0,.05556,.46563],102:[.19444,.69444,.10764,.16667,.48959],103:[.19444,.43056,.03588,.02778,.47697],104:[0,.69444,0,0,.57616],105:[0,.65952,0,0,.34451],106:[.19444,.65952,.05724,0,.41181],107:[0,.69444,.03148,0,.5206],108:[0,.69444,.01968,.08334,.29838],109:[0,.43056,0,0,.87801],110:[0,.43056,0,0,.60023],111:[0,.43056,0,.05556,.48472],112:[.19444,.43056,0,.08334,.50313],113:[.19444,.43056,.03588,.08334,.44641],114:[0,.43056,.02778,.05556,.45116],115:[0,.43056,0,.05556,.46875],116:[0,.61508,0,.08334,.36111],117:[0,.43056,0,.02778,.57246],118:[0,.43056,.03588,.02778,.48472],119:[0,.43056,.02691,.08334,.71592],120:[0,.43056,0,.02778,.57153],121:[.19444,.43056,.03588,.05556,.49028],122:[0,.43056,.04398,.05556,.46505],915:[0,.68333,.13889,.08334,.61528],916:[0,.68333,0,.16667,.83334],920:[0,.68333,.02778,.08334,.76278],923:[0,.68333,0,.16667,.69445],926:[0,.68333,.07569,.08334,.74236],928:[0,.68333,.08125,.05556,.83125],931:[0,.68333,.05764,.08334,.77986],933:[0,.68333,.13889,.05556,.58333],934:[0,.68333,0,.08334,.66667],936:[0,.68333,.11,.05556,.61222],937:[0,.68333,.05017,.08334,.7724],945:[0,.43056,.0037,.02778,.6397],946:[.19444,.69444,.05278,.08334,.56563],947:[.19444,.43056,.05556,0,.51773],948:[0,.69444,.03785,.05556,.44444],949:[0,.43056,0,.08334,.46632],950:[.19444,.69444,.07378,.08334,.4375],951:[.19444,.43056,.03588,.05556,.49653],952:[0,.69444,.02778,.08334,.46944],953:[0,.43056,0,.05556,.35394],954:[0,.43056,0,0,.57616],955:[0,.69444,0,0,.58334],956:[.19444,.43056,0,.02778,.60255],957:[0,.43056,.06366,.02778,.49398],958:[.19444,.69444,.04601,.11111,.4375],959:[0,.43056,0,.05556,.48472],960:[0,.43056,.03588,0,.57003],961:[.19444,.43056,0,.08334,.51702],962:[.09722,.43056,.07986,.08334,.36285],963:[0,.43056,.03588,0,.57141],964:[0,.43056,.1132,.02778,.43715],965:[0,.43056,.03588,.02778,.54028],966:[.19444,.43056,0,.08334,.65417],967:[.19444,.43056,0,.05556,.62569],968:[.19444,.69444,.03588,.11111,.65139],969:[0,.43056,.03588,0,.62245],977:[0,.69444,0,.08334,.59144],981:[.19444,.69444,0,.08334,.59583],982:[0,.43056,.02778,0,.82813],1009:[.19444,.43056,0,.08334,.51702],1013:[0,.43056,0,.05556,.4059]},\"Math-Regular\":{65:[0,.68333,0,.13889,.75],66:[0,.68333,.05017,.08334,.75851],67:[0,.68333,.07153,.08334,.71472],68:[0,.68333,.02778,.05556,.82792],69:[0,.68333,.05764,.08334,.7382],70:[0,.68333,.13889,.08334,.64306],71:[0,.68333,0,.08334,.78625],72:[0,.68333,.08125,.05556,.83125],73:[0,.68333,.07847,.11111,.43958],74:[0,.68333,.09618,.16667,.55451],75:[0,.68333,.07153,.05556,.84931],76:[0,.68333,0,.02778,.68056],77:[0,.68333,.10903,.08334,.97014],78:[0,.68333,.10903,.08334,.80347],79:[0,.68333,.02778,.08334,.76278],80:[0,.68333,.13889,.08334,.64201],81:[.19444,.68333,0,.08334,.79056],82:[0,.68333,.00773,.08334,.75929],83:[0,.68333,.05764,.08334,.6132],84:[0,.68333,.13889,.08334,.58438],85:[0,.68333,.10903,.02778,.68278],86:[0,.68333,.22222,0,.58333],87:[0,.68333,.13889,0,.94445],88:[0,.68333,.07847,.08334,.82847],89:[0,.68333,.22222,0,.58056],90:[0,.68333,.07153,.08334,.68264],97:[0,.43056,0,0,.52859],98:[0,.69444,0,0,.42917],99:[0,.43056,0,.05556,.43276],100:[0,.69444,0,.16667,.52049],101:[0,.43056,0,.05556,.46563],102:[.19444,.69444,.10764,.16667,.48959],103:[.19444,.43056,.03588,.02778,.47697],104:[0,.69444,0,0,.57616],105:[0,.65952,0,0,.34451],106:[.19444,.65952,.05724,0,.41181],107:[0,.69444,.03148,0,.5206],108:[0,.69444,.01968,.08334,.29838],109:[0,.43056,0,0,.87801],110:[0,.43056,0,0,.60023],111:[0,.43056,0,.05556,.48472],112:[.19444,.43056,0,.08334,.50313],113:[.19444,.43056,.03588,.08334,.44641],114:[0,.43056,.02778,.05556,.45116],115:[0,.43056,0,.05556,.46875],116:[0,.61508,0,.08334,.36111],117:[0,.43056,0,.02778,.57246],118:[0,.43056,.03588,.02778,.48472],119:[0,.43056,.02691,.08334,.71592],120:[0,.43056,0,.02778,.57153],121:[.19444,.43056,.03588,.05556,.49028],122:[0,.43056,.04398,.05556,.46505],915:[0,.68333,.13889,.08334,.61528],916:[0,.68333,0,.16667,.83334],920:[0,.68333,.02778,.08334,.76278],923:[0,.68333,0,.16667,.69445],926:[0,.68333,.07569,.08334,.74236],928:[0,.68333,.08125,.05556,.83125],931:[0,.68333,.05764,.08334,.77986],933:[0,.68333,.13889,.05556,.58333],934:[0,.68333,0,.08334,.66667],936:[0,.68333,.11,.05556,.61222],937:[0,.68333,.05017,.08334,.7724],945:[0,.43056,.0037,.02778,.6397],946:[.19444,.69444,.05278,.08334,.56563],947:[.19444,.43056,.05556,0,.51773],948:[0,.69444,.03785,.05556,.44444],949:[0,.43056,0,.08334,.46632],950:[.19444,.69444,.07378,.08334,.4375],951:[.19444,.43056,.03588,.05556,.49653],952:[0,.69444,.02778,.08334,.46944],953:[0,.43056,0,.05556,.35394],954:[0,.43056,0,0,.57616],955:[0,.69444,0,0,.58334],956:[.19444,.43056,0,.02778,.60255],957:[0,.43056,.06366,.02778,.49398],958:[.19444,.69444,.04601,.11111,.4375],959:[0,.43056,0,.05556,.48472],960:[0,.43056,.03588,0,.57003],961:[.19444,.43056,0,.08334,.51702],962:[.09722,.43056,.07986,.08334,.36285],963:[0,.43056,.03588,0,.57141],964:[0,.43056,.1132,.02778,.43715],965:[0,.43056,.03588,.02778,.54028],966:[.19444,.43056,0,.08334,.65417],967:[.19444,.43056,0,.05556,.62569],968:[.19444,.69444,.03588,.11111,.65139],969:[0,.43056,.03588,0,.62245],977:[0,.69444,0,.08334,.59144],981:[.19444,.69444,0,.08334,.59583],982:[0,.43056,.02778,0,.82813],1009:[.19444,.43056,0,.08334,.51702],1013:[0,.43056,0,.05556,.4059]},\"SansSerif-Bold\":{33:[0,.69444,0,0,.36667],34:[0,.69444,0,0,.55834],35:[.19444,.69444,0,0,.91667],36:[.05556,.75,0,0,.55],37:[.05556,.75,0,0,1.02912],38:[0,.69444,0,0,.83056],39:[0,.69444,0,0,.30556],40:[.25,.75,0,0,.42778],41:[.25,.75,0,0,.42778],42:[0,.75,0,0,.55],43:[.11667,.61667,0,0,.85556],44:[.10556,.13056,0,0,.30556],45:[0,.45833,0,0,.36667],46:[0,.13056,0,0,.30556],47:[.25,.75,0,0,.55],48:[0,.69444,0,0,.55],49:[0,.69444,0,0,.55],50:[0,.69444,0,0,.55],51:[0,.69444,0,0,.55],52:[0,.69444,0,0,.55],53:[0,.69444,0,0,.55],54:[0,.69444,0,0,.55],55:[0,.69444,0,0,.55],56:[0,.69444,0,0,.55],57:[0,.69444,0,0,.55],58:[0,.45833,0,0,.30556],59:[.10556,.45833,0,0,.30556],61:[-.09375,.40625,0,0,.85556],63:[0,.69444,0,0,.51945],64:[0,.69444,0,0,.73334],65:[0,.69444,0,0,.73334],66:[0,.69444,0,0,.73334],67:[0,.69444,0,0,.70278],68:[0,.69444,0,0,.79445],69:[0,.69444,0,0,.64167],70:[0,.69444,0,0,.61111],71:[0,.69444,0,0,.73334],72:[0,.69444,0,0,.79445],73:[0,.69444,0,0,.33056],74:[0,.69444,0,0,.51945],75:[0,.69444,0,0,.76389],76:[0,.69444,0,0,.58056],77:[0,.69444,0,0,.97778],78:[0,.69444,0,0,.79445],79:[0,.69444,0,0,.79445],80:[0,.69444,0,0,.70278],81:[.10556,.69444,0,0,.79445],82:[0,.69444,0,0,.70278],83:[0,.69444,0,0,.61111],84:[0,.69444,0,0,.73334],85:[0,.69444,0,0,.76389],86:[0,.69444,.01528,0,.73334],87:[0,.69444,.01528,0,1.03889],88:[0,.69444,0,0,.73334],89:[0,.69444,.0275,0,.73334],90:[0,.69444,0,0,.67223],91:[.25,.75,0,0,.34306],93:[.25,.75,0,0,.34306],94:[0,.69444,0,0,.55],95:[.35,.10833,.03056,0,.55],97:[0,.45833,0,0,.525],98:[0,.69444,0,0,.56111],99:[0,.45833,0,0,.48889],100:[0,.69444,0,0,.56111],101:[0,.45833,0,0,.51111],102:[0,.69444,.07639,0,.33611],103:[.19444,.45833,.01528,0,.55],104:[0,.69444,0,0,.56111],105:[0,.69444,0,0,.25556],106:[.19444,.69444,0,0,.28611],107:[0,.69444,0,0,.53056],108:[0,.69444,0,0,.25556],109:[0,.45833,0,0,.86667],110:[0,.45833,0,0,.56111],111:[0,.45833,0,0,.55],112:[.19444,.45833,0,0,.56111],113:[.19444,.45833,0,0,.56111],114:[0,.45833,.01528,0,.37222],115:[0,.45833,0,0,.42167],116:[0,.58929,0,0,.40417],117:[0,.45833,0,0,.56111],118:[0,.45833,.01528,0,.5],119:[0,.45833,.01528,0,.74445],120:[0,.45833,0,0,.5],121:[.19444,.45833,.01528,0,.5],122:[0,.45833,0,0,.47639],126:[.35,.34444,0,0,.55],168:[0,.69444,0,0,.55],176:[0,.69444,0,0,.73334],180:[0,.69444,0,0,.55],184:[.17014,0,0,0,.48889],305:[0,.45833,0,0,.25556],567:[.19444,.45833,0,0,.28611],710:[0,.69444,0,0,.55],711:[0,.63542,0,0,.55],713:[0,.63778,0,0,.55],728:[0,.69444,0,0,.55],729:[0,.69444,0,0,.30556],730:[0,.69444,0,0,.73334],732:[0,.69444,0,0,.55],733:[0,.69444,0,0,.55],915:[0,.69444,0,0,.58056],916:[0,.69444,0,0,.91667],920:[0,.69444,0,0,.85556],923:[0,.69444,0,0,.67223],926:[0,.69444,0,0,.73334],928:[0,.69444,0,0,.79445],931:[0,.69444,0,0,.79445],933:[0,.69444,0,0,.85556],934:[0,.69444,0,0,.79445],936:[0,.69444,0,0,.85556],937:[0,.69444,0,0,.79445],8211:[0,.45833,.03056,0,.55],8212:[0,.45833,.03056,0,1.10001],8216:[0,.69444,0,0,.30556],8217:[0,.69444,0,0,.30556],8220:[0,.69444,0,0,.55834],8221:[0,.69444,0,0,.55834]},\"SansSerif-Italic\":{33:[0,.69444,.05733,0,.31945],34:[0,.69444,.00316,0,.5],35:[.19444,.69444,.05087,0,.83334],36:[.05556,.75,.11156,0,.5],37:[.05556,.75,.03126,0,.83334],38:[0,.69444,.03058,0,.75834],39:[0,.69444,.07816,0,.27778],40:[.25,.75,.13164,0,.38889],41:[.25,.75,.02536,0,.38889],42:[0,.75,.11775,0,.5],43:[.08333,.58333,.02536,0,.77778],44:[.125,.08333,0,0,.27778],45:[0,.44444,.01946,0,.33333],46:[0,.08333,0,0,.27778],47:[.25,.75,.13164,0,.5],48:[0,.65556,.11156,0,.5],49:[0,.65556,.11156,0,.5],50:[0,.65556,.11156,0,.5],51:[0,.65556,.11156,0,.5],52:[0,.65556,.11156,0,.5],53:[0,.65556,.11156,0,.5],54:[0,.65556,.11156,0,.5],55:[0,.65556,.11156,0,.5],56:[0,.65556,.11156,0,.5],57:[0,.65556,.11156,0,.5],58:[0,.44444,.02502,0,.27778],59:[.125,.44444,.02502,0,.27778],61:[-.13,.37,.05087,0,.77778],63:[0,.69444,.11809,0,.47222],64:[0,.69444,.07555,0,.66667],65:[0,.69444,0,0,.66667],66:[0,.69444,.08293,0,.66667],67:[0,.69444,.11983,0,.63889],68:[0,.69444,.07555,0,.72223],69:[0,.69444,.11983,0,.59722],70:[0,.69444,.13372,0,.56945],71:[0,.69444,.11983,0,.66667],72:[0,.69444,.08094,0,.70834],73:[0,.69444,.13372,0,.27778],74:[0,.69444,.08094,0,.47222],75:[0,.69444,.11983,0,.69445],76:[0,.69444,0,0,.54167],77:[0,.69444,.08094,0,.875],78:[0,.69444,.08094,0,.70834],79:[0,.69444,.07555,0,.73611],80:[0,.69444,.08293,0,.63889],81:[.125,.69444,.07555,0,.73611],82:[0,.69444,.08293,0,.64584],83:[0,.69444,.09205,0,.55556],84:[0,.69444,.13372,0,.68056],85:[0,.69444,.08094,0,.6875],86:[0,.69444,.1615,0,.66667],87:[0,.69444,.1615,0,.94445],88:[0,.69444,.13372,0,.66667],89:[0,.69444,.17261,0,.66667],90:[0,.69444,.11983,0,.61111],91:[.25,.75,.15942,0,.28889],93:[.25,.75,.08719,0,.28889],94:[0,.69444,.0799,0,.5],95:[.35,.09444,.08616,0,.5],97:[0,.44444,.00981,0,.48056],98:[0,.69444,.03057,0,.51667],99:[0,.44444,.08336,0,.44445],100:[0,.69444,.09483,0,.51667],101:[0,.44444,.06778,0,.44445],102:[0,.69444,.21705,0,.30556],103:[.19444,.44444,.10836,0,.5],104:[0,.69444,.01778,0,.51667],105:[0,.67937,.09718,0,.23889],106:[.19444,.67937,.09162,0,.26667],107:[0,.69444,.08336,0,.48889],108:[0,.69444,.09483,0,.23889],109:[0,.44444,.01778,0,.79445],110:[0,.44444,.01778,0,.51667],111:[0,.44444,.06613,0,.5],112:[.19444,.44444,.0389,0,.51667],113:[.19444,.44444,.04169,0,.51667],114:[0,.44444,.10836,0,.34167],115:[0,.44444,.0778,0,.38333],116:[0,.57143,.07225,0,.36111],117:[0,.44444,.04169,0,.51667],118:[0,.44444,.10836,0,.46111],119:[0,.44444,.10836,0,.68334],120:[0,.44444,.09169,0,.46111],121:[.19444,.44444,.10836,0,.46111],122:[0,.44444,.08752,0,.43472],126:[.35,.32659,.08826,0,.5],168:[0,.67937,.06385,0,.5],176:[0,.69444,0,0,.73752],184:[.17014,0,0,0,.44445],305:[0,.44444,.04169,0,.23889],567:[.19444,.44444,.04169,0,.26667],710:[0,.69444,.0799,0,.5],711:[0,.63194,.08432,0,.5],713:[0,.60889,.08776,0,.5],714:[0,.69444,.09205,0,.5],715:[0,.69444,0,0,.5],728:[0,.69444,.09483,0,.5],729:[0,.67937,.07774,0,.27778],730:[0,.69444,0,0,.73752],732:[0,.67659,.08826,0,.5],733:[0,.69444,.09205,0,.5],915:[0,.69444,.13372,0,.54167],916:[0,.69444,0,0,.83334],920:[0,.69444,.07555,0,.77778],923:[0,.69444,0,0,.61111],926:[0,.69444,.12816,0,.66667],928:[0,.69444,.08094,0,.70834],931:[0,.69444,.11983,0,.72222],933:[0,.69444,.09031,0,.77778],934:[0,.69444,.04603,0,.72222],936:[0,.69444,.09031,0,.77778],937:[0,.69444,.08293,0,.72222],8211:[0,.44444,.08616,0,.5],8212:[0,.44444,.08616,0,1],8216:[0,.69444,.07816,0,.27778],8217:[0,.69444,.07816,0,.27778],8220:[0,.69444,.14205,0,.5],8221:[0,.69444,.00316,0,.5]},\"SansSerif-Regular\":{33:[0,.69444,0,0,.31945],34:[0,.69444,0,0,.5],35:[.19444,.69444,0,0,.83334],36:[.05556,.75,0,0,.5],37:[.05556,.75,0,0,.83334],38:[0,.69444,0,0,.75834],39:[0,.69444,0,0,.27778],40:[.25,.75,0,0,.38889],41:[.25,.75,0,0,.38889],42:[0,.75,0,0,.5],43:[.08333,.58333,0,0,.77778],44:[.125,.08333,0,0,.27778],45:[0,.44444,0,0,.33333],46:[0,.08333,0,0,.27778],47:[.25,.75,0,0,.5],48:[0,.65556,0,0,.5],49:[0,.65556,0,0,.5],50:[0,.65556,0,0,.5],51:[0,.65556,0,0,.5],52:[0,.65556,0,0,.5],53:[0,.65556,0,0,.5],54:[0,.65556,0,0,.5],55:[0,.65556,0,0,.5],56:[0,.65556,0,0,.5],57:[0,.65556,0,0,.5],58:[0,.44444,0,0,.27778],59:[.125,.44444,0,0,.27778],61:[-.13,.37,0,0,.77778],63:[0,.69444,0,0,.47222],64:[0,.69444,0,0,.66667],65:[0,.69444,0,0,.66667],66:[0,.69444,0,0,.66667],67:[0,.69444,0,0,.63889],68:[0,.69444,0,0,.72223],69:[0,.69444,0,0,.59722],70:[0,.69444,0,0,.56945],71:[0,.69444,0,0,.66667],72:[0,.69444,0,0,.70834],73:[0,.69444,0,0,.27778],74:[0,.69444,0,0,.47222],75:[0,.69444,0,0,.69445],76:[0,.69444,0,0,.54167],77:[0,.69444,0,0,.875],78:[0,.69444,0,0,.70834],79:[0,.69444,0,0,.73611],80:[0,.69444,0,0,.63889],81:[.125,.69444,0,0,.73611],82:[0,.69444,0,0,.64584],83:[0,.69444,0,0,.55556],84:[0,.69444,0,0,.68056],85:[0,.69444,0,0,.6875],86:[0,.69444,.01389,0,.66667],87:[0,.69444,.01389,0,.94445],88:[0,.69444,0,0,.66667],89:[0,.69444,.025,0,.66667],90:[0,.69444,0,0,.61111],91:[.25,.75,0,0,.28889],93:[.25,.75,0,0,.28889],94:[0,.69444,0,0,.5],95:[.35,.09444,.02778,0,.5],97:[0,.44444,0,0,.48056],98:[0,.69444,0,0,.51667],99:[0,.44444,0,0,.44445],100:[0,.69444,0,0,.51667],101:[0,.44444,0,0,.44445],102:[0,.69444,.06944,0,.30556],103:[.19444,.44444,.01389,0,.5],104:[0,.69444,0,0,.51667],105:[0,.67937,0,0,.23889],106:[.19444,.67937,0,0,.26667],107:[0,.69444,0,0,.48889],108:[0,.69444,0,0,.23889],109:[0,.44444,0,0,.79445],110:[0,.44444,0,0,.51667],111:[0,.44444,0,0,.5],112:[.19444,.44444,0,0,.51667],113:[.19444,.44444,0,0,.51667],114:[0,.44444,.01389,0,.34167],115:[0,.44444,0,0,.38333],116:[0,.57143,0,0,.36111],117:[0,.44444,0,0,.51667],118:[0,.44444,.01389,0,.46111],119:[0,.44444,.01389,0,.68334],120:[0,.44444,0,0,.46111],121:[.19444,.44444,.01389,0,.46111],122:[0,.44444,0,0,.43472],126:[.35,.32659,0,0,.5],168:[0,.67937,0,0,.5],176:[0,.69444,0,0,.66667],184:[.17014,0,0,0,.44445],305:[0,.44444,0,0,.23889],567:[.19444,.44444,0,0,.26667],710:[0,.69444,0,0,.5],711:[0,.63194,0,0,.5],713:[0,.60889,0,0,.5],714:[0,.69444,0,0,.5],715:[0,.69444,0,0,.5],728:[0,.69444,0,0,.5],729:[0,.67937,0,0,.27778],730:[0,.69444,0,0,.66667],732:[0,.67659,0,0,.5],733:[0,.69444,0,0,.5],915:[0,.69444,0,0,.54167],916:[0,.69444,0,0,.83334],920:[0,.69444,0,0,.77778],923:[0,.69444,0,0,.61111],926:[0,.69444,0,0,.66667],928:[0,.69444,0,0,.70834],931:[0,.69444,0,0,.72222],933:[0,.69444,0,0,.77778],934:[0,.69444,0,0,.72222],936:[0,.69444,0,0,.77778],937:[0,.69444,0,0,.72222],8211:[0,.44444,.02778,0,.5],8212:[0,.44444,.02778,0,1],8216:[0,.69444,0,0,.27778],8217:[0,.69444,0,0,.27778],8220:[0,.69444,0,0,.5],8221:[0,.69444,0,0,.5]},\"Script-Regular\":{65:[0,.7,.22925,0,.80253],66:[0,.7,.04087,0,.90757],67:[0,.7,.1689,0,.66619],68:[0,.7,.09371,0,.77443],69:[0,.7,.18583,0,.56162],70:[0,.7,.13634,0,.89544],71:[0,.7,.17322,0,.60961],72:[0,.7,.29694,0,.96919],73:[0,.7,.19189,0,.80907],74:[.27778,.7,.19189,0,1.05159],75:[0,.7,.31259,0,.91364],76:[0,.7,.19189,0,.87373],77:[0,.7,.15981,0,1.08031],78:[0,.7,.3525,0,.9015],79:[0,.7,.08078,0,.73787],80:[0,.7,.08078,0,1.01262],81:[0,.7,.03305,0,.88282],82:[0,.7,.06259,0,.85],83:[0,.7,.19189,0,.86767],84:[0,.7,.29087,0,.74697],85:[0,.7,.25815,0,.79996],86:[0,.7,.27523,0,.62204],87:[0,.7,.27523,0,.80532],88:[0,.7,.26006,0,.94445],89:[0,.7,.2939,0,.70961],90:[0,.7,.24037,0,.8212]},\"Size1-Regular\":{40:[.35001,.85,0,0,.45834],41:[.35001,.85,0,0,.45834],47:[.35001,.85,0,0,.57778],91:[.35001,.85,0,0,.41667],92:[.35001,.85,0,0,.57778],93:[.35001,.85,0,0,.41667],123:[.35001,.85,0,0,.58334],125:[.35001,.85,0,0,.58334],710:[0,.72222,0,0,.55556],732:[0,.72222,0,0,.55556],770:[0,.72222,0,0,.55556],771:[0,.72222,0,0,.55556],8214:[-99e-5,.601,0,0,.77778],8593:[1e-5,.6,0,0,.66667],8595:[1e-5,.6,0,0,.66667],8657:[1e-5,.6,0,0,.77778],8659:[1e-5,.6,0,0,.77778],8719:[.25001,.75,0,0,.94445],8720:[.25001,.75,0,0,.94445],8721:[.25001,.75,0,0,1.05556],8730:[.35001,.85,0,0,1],8739:[-.00599,.606,0,0,.33333],8741:[-.00599,.606,0,0,.55556],8747:[.30612,.805,.19445,0,.47222],8748:[.306,.805,.19445,0,.47222],8749:[.306,.805,.19445,0,.47222],8750:[.30612,.805,.19445,0,.47222],8896:[.25001,.75,0,0,.83334],8897:[.25001,.75,0,0,.83334],8898:[.25001,.75,0,0,.83334],8899:[.25001,.75,0,0,.83334],8968:[.35001,.85,0,0,.47222],8969:[.35001,.85,0,0,.47222],8970:[.35001,.85,0,0,.47222],8971:[.35001,.85,0,0,.47222],9168:[-99e-5,.601,0,0,.66667],10216:[.35001,.85,0,0,.47222],10217:[.35001,.85,0,0,.47222],10752:[.25001,.75,0,0,1.11111],10753:[.25001,.75,0,0,1.11111],10754:[.25001,.75,0,0,1.11111],10756:[.25001,.75,0,0,.83334],10758:[.25001,.75,0,0,.83334]},\"Size2-Regular\":{40:[.65002,1.15,0,0,.59722],41:[.65002,1.15,0,0,.59722],47:[.65002,1.15,0,0,.81111],91:[.65002,1.15,0,0,.47222],92:[.65002,1.15,0,0,.81111],93:[.65002,1.15,0,0,.47222],123:[.65002,1.15,0,0,.66667],125:[.65002,1.15,0,0,.66667],710:[0,.75,0,0,1],732:[0,.75,0,0,1],770:[0,.75,0,0,1],771:[0,.75,0,0,1],8719:[.55001,1.05,0,0,1.27778],8720:[.55001,1.05,0,0,1.27778],8721:[.55001,1.05,0,0,1.44445],8730:[.65002,1.15,0,0,1],8747:[.86225,1.36,.44445,0,.55556],8748:[.862,1.36,.44445,0,.55556],8749:[.862,1.36,.44445,0,.55556],8750:[.86225,1.36,.44445,0,.55556],8896:[.55001,1.05,0,0,1.11111],8897:[.55001,1.05,0,0,1.11111],8898:[.55001,1.05,0,0,1.11111],8899:[.55001,1.05,0,0,1.11111],8968:[.65002,1.15,0,0,.52778],8969:[.65002,1.15,0,0,.52778],8970:[.65002,1.15,0,0,.52778],8971:[.65002,1.15,0,0,.52778],10216:[.65002,1.15,0,0,.61111],10217:[.65002,1.15,0,0,.61111],10752:[.55001,1.05,0,0,1.51112],10753:[.55001,1.05,0,0,1.51112],10754:[.55001,1.05,0,0,1.51112],10756:[.55001,1.05,0,0,1.11111],10758:[.55001,1.05,0,0,1.11111]},\"Size3-Regular\":{40:[.95003,1.45,0,0,.73611],41:[.95003,1.45,0,0,.73611],47:[.95003,1.45,0,0,1.04445],91:[.95003,1.45,0,0,.52778],92:[.95003,1.45,0,0,1.04445],93:[.95003,1.45,0,0,.52778],123:[.95003,1.45,0,0,.75],125:[.95003,1.45,0,0,.75],710:[0,.75,0,0,1.44445],732:[0,.75,0,0,1.44445],770:[0,.75,0,0,1.44445],771:[0,.75,0,0,1.44445],8730:[.95003,1.45,0,0,1],8968:[.95003,1.45,0,0,.58334],8969:[.95003,1.45,0,0,.58334],8970:[.95003,1.45,0,0,.58334],8971:[.95003,1.45,0,0,.58334],10216:[.95003,1.45,0,0,.75],10217:[.95003,1.45,0,0,.75]},\"Size4-Regular\":{40:[1.25003,1.75,0,0,.79167],41:[1.25003,1.75,0,0,.79167],47:[1.25003,1.75,0,0,1.27778],91:[1.25003,1.75,0,0,.58334],92:[1.25003,1.75,0,0,1.27778],93:[1.25003,1.75,0,0,.58334],123:[1.25003,1.75,0,0,.80556],125:[1.25003,1.75,0,0,.80556],710:[0,.825,0,0,1.8889],732:[0,.825,0,0,1.8889],770:[0,.825,0,0,1.8889],771:[0,.825,0,0,1.8889],8730:[1.25003,1.75,0,0,1],8968:[1.25003,1.75,0,0,.63889],8969:[1.25003,1.75,0,0,.63889],8970:[1.25003,1.75,0,0,.63889],8971:[1.25003,1.75,0,0,.63889],9115:[.64502,1.155,0,0,.875],9116:[1e-5,.6,0,0,.875],9117:[.64502,1.155,0,0,.875],9118:[.64502,1.155,0,0,.875],9119:[1e-5,.6,0,0,.875],9120:[.64502,1.155,0,0,.875],9121:[.64502,1.155,0,0,.66667],9122:[-99e-5,.601,0,0,.66667],9123:[.64502,1.155,0,0,.66667],9124:[.64502,1.155,0,0,.66667],9125:[-99e-5,.601,0,0,.66667],9126:[.64502,1.155,0,0,.66667],9127:[1e-5,.9,0,0,.88889],9128:[.65002,1.15,0,0,.88889],9129:[.90001,0,0,0,.88889],9130:[0,.3,0,0,.88889],9131:[1e-5,.9,0,0,.88889],9132:[.65002,1.15,0,0,.88889],9133:[.90001,0,0,0,.88889],9143:[.88502,.915,0,0,1.05556],10216:[1.25003,1.75,0,0,.80556],10217:[1.25003,1.75,0,0,.80556],57344:[-.00499,.605,0,0,1.05556],57345:[-.00499,.605,0,0,1.05556],57680:[0,.12,0,0,.45],57681:[0,.12,0,0,.45],57682:[0,.12,0,0,.45],57683:[0,.12,0,0,.45]},\"Typewriter-Regular\":{32:[0,0,0,0,.525],33:[0,.61111,0,0,.525],34:[0,.61111,0,0,.525],35:[0,.61111,0,0,.525],36:[.08333,.69444,0,0,.525],37:[.08333,.69444,0,0,.525],38:[0,.61111,0,0,.525],39:[0,.61111,0,0,.525],40:[.08333,.69444,0,0,.525],41:[.08333,.69444,0,0,.525],42:[0,.52083,0,0,.525],43:[-.08056,.53055,0,0,.525],44:[.13889,.125,0,0,.525],45:[-.08056,.53055,0,0,.525],46:[0,.125,0,0,.525],47:[.08333,.69444,0,0,.525],48:[0,.61111,0,0,.525],49:[0,.61111,0,0,.525],50:[0,.61111,0,0,.525],51:[0,.61111,0,0,.525],52:[0,.61111,0,0,.525],53:[0,.61111,0,0,.525],54:[0,.61111,0,0,.525],55:[0,.61111,0,0,.525],56:[0,.61111,0,0,.525],57:[0,.61111,0,0,.525],58:[0,.43056,0,0,.525],59:[.13889,.43056,0,0,.525],60:[-.05556,.55556,0,0,.525],61:[-.19549,.41562,0,0,.525],62:[-.05556,.55556,0,0,.525],63:[0,.61111,0,0,.525],64:[0,.61111,0,0,.525],65:[0,.61111,0,0,.525],66:[0,.61111,0,0,.525],67:[0,.61111,0,0,.525],68:[0,.61111,0,0,.525],69:[0,.61111,0,0,.525],70:[0,.61111,0,0,.525],71:[0,.61111,0,0,.525],72:[0,.61111,0,0,.525],73:[0,.61111,0,0,.525],74:[0,.61111,0,0,.525],75:[0,.61111,0,0,.525],76:[0,.61111,0,0,.525],77:[0,.61111,0,0,.525],78:[0,.61111,0,0,.525],79:[0,.61111,0,0,.525],80:[0,.61111,0,0,.525],81:[.13889,.61111,0,0,.525],82:[0,.61111,0,0,.525],83:[0,.61111,0,0,.525],84:[0,.61111,0,0,.525],85:[0,.61111,0,0,.525],86:[0,.61111,0,0,.525],87:[0,.61111,0,0,.525],88:[0,.61111,0,0,.525],89:[0,.61111,0,0,.525],90:[0,.61111,0,0,.525],91:[.08333,.69444,0,0,.525],92:[.08333,.69444,0,0,.525],93:[.08333,.69444,0,0,.525],94:[0,.61111,0,0,.525],95:[.09514,0,0,0,.525],96:[0,.61111,0,0,.525],97:[0,.43056,0,0,.525],98:[0,.61111,0,0,.525],99:[0,.43056,0,0,.525],100:[0,.61111,0,0,.525],101:[0,.43056,0,0,.525],102:[0,.61111,0,0,.525],103:[.22222,.43056,0,0,.525],104:[0,.61111,0,0,.525],105:[0,.61111,0,0,.525],106:[.22222,.61111,0,0,.525],107:[0,.61111,0,0,.525],108:[0,.61111,0,0,.525],109:[0,.43056,0,0,.525],110:[0,.43056,0,0,.525],111:[0,.43056,0,0,.525],112:[.22222,.43056,0,0,.525],113:[.22222,.43056,0,0,.525],114:[0,.43056,0,0,.525],115:[0,.43056,0,0,.525],116:[0,.55358,0,0,.525],117:[0,.43056,0,0,.525],118:[0,.43056,0,0,.525],119:[0,.43056,0,0,.525],120:[0,.43056,0,0,.525],121:[.22222,.43056,0,0,.525],122:[0,.43056,0,0,.525],123:[.08333,.69444,0,0,.525],124:[.08333,.69444,0,0,.525],125:[.08333,.69444,0,0,.525],126:[0,.61111,0,0,.525],127:[0,.61111,0,0,.525],160:[0,0,0,0,.525],176:[0,.61111,0,0,.525],184:[.19445,0,0,0,.525],305:[0,.43056,0,0,.525],567:[.22222,.43056,0,0,.525],711:[0,.56597,0,0,.525],713:[0,.56555,0,0,.525],714:[0,.61111,0,0,.525],715:[0,.61111,0,0,.525],728:[0,.61111,0,0,.525],730:[0,.61111,0,0,.525],770:[0,.61111,0,0,.525],771:[0,.61111,0,0,.525],776:[0,.61111,0,0,.525],915:[0,.61111,0,0,.525],916:[0,.61111,0,0,.525],920:[0,.61111,0,0,.525],923:[0,.61111,0,0,.525],926:[0,.61111,0,0,.525],928:[0,.61111,0,0,.525],931:[0,.61111,0,0,.525],933:[0,.61111,0,0,.525],934:[0,.61111,0,0,.525],936:[0,.61111,0,0,.525],937:[0,.61111,0,0,.525],8216:[0,.61111,0,0,.525],8217:[0,.61111,0,0,.525],8242:[0,.61111,0,0,.525],9251:[.11111,.21944,0,0,.525]}},V={slant:[.25,.25,.25],space:[0,0,0],stretch:[0,0,0],shrink:[0,0,0],xHeight:[.431,.431,.431],quad:[1,1.171,1.472],extraSpace:[0,0,0],num1:[.677,.732,.925],num2:[.394,.384,.387],num3:[.444,.471,.504],denom1:[.686,.752,1.025],denom2:[.345,.344,.532],sup1:[.413,.503,.504],sup2:[.363,.431,.404],sup3:[.289,.286,.294],sub1:[.15,.143,.2],sub2:[.247,.286,.4],supDrop:[.386,.353,.494],subDrop:[.05,.071,.1],delim1:[2.39,1.7,1.98],delim2:[1.01,1.157,1.42],axisHeight:[.25,.25,.25],defaultRuleThickness:[.04,.049,.049],bigOpSpacing1:[.111,.111,.111],bigOpSpacing2:[.166,.166,.166],bigOpSpacing3:[.2,.2,.2],bigOpSpacing4:[.6,.611,.611],bigOpSpacing5:[.1,.143,.143],sqrtRuleThickness:[.04,.04,.04],ptPerEm:[10,10,10],doubleRuleSep:[.2,.2,.2],arrayRuleWidth:[.04,.04,.04],fboxsep:[.3,.3,.3],fboxrule:[.04,.04,.04]},U={\"Å\":\"A\",\"Ç\":\"C\",\"Ð\":\"D\",\"Þ\":\"o\",\"å\":\"a\",\"ç\":\"c\",\"ð\":\"d\",\"þ\":\"o\",\"А\":\"A\",\"Б\":\"B\",\"В\":\"B\",\"Г\":\"F\",\"Д\":\"A\",\"Е\":\"E\",\"Ж\":\"K\",\"З\":\"3\",\"И\":\"N\",\"Й\":\"N\",\"К\":\"K\",\"Л\":\"N\",\"М\":\"M\",\"Н\":\"H\",\"О\":\"O\",\"П\":\"N\",\"Р\":\"P\",\"С\":\"C\",\"Т\":\"T\",\"У\":\"y\",\"Ф\":\"O\",\"Х\":\"X\",\"Ц\":\"U\",\"Ч\":\"h\",\"Ш\":\"W\",\"Щ\":\"W\",\"Ъ\":\"B\",\"Ы\":\"X\",\"Ь\":\"B\",\"Э\":\"3\",\"Ю\":\"X\",\"Я\":\"R\",\"а\":\"a\",\"б\":\"b\",\"в\":\"a\",\"г\":\"r\",\"д\":\"y\",\"е\":\"e\",\"ж\":\"m\",\"з\":\"e\",\"и\":\"n\",\"й\":\"n\",\"к\":\"n\",\"л\":\"n\",\"м\":\"m\",\"н\":\"n\",\"о\":\"o\",\"п\":\"n\",\"р\":\"p\",\"с\":\"c\",\"т\":\"o\",\"у\":\"y\",\"ф\":\"b\",\"х\":\"x\",\"ц\":\"n\",\"ч\":\"n\",\"ш\":\"w\",\"щ\":\"w\",\"ъ\":\"a\",\"ы\":\"m\",\"ь\":\"a\",\"э\":\"e\",\"ю\":\"m\",\"я\":\"r\"};function G(t,e,r){if(!F[e])throw new Error(\"Font metrics not found for font: \"+e+\".\");var a=t.charCodeAt(0),n=F[e][a];if(!n&&t[0]in U&&(a=U[t[0]].charCodeAt(0),n=F[e][a]),n||\"text\"!==r||M(a)&&(n=F[e][77]),n)return{depth:n[0],height:n[1],italic:n[2],skew:n[3],width:n[4]}}var Y={},_={bin:1,close:1,inner:1,open:1,punct:1,rel:1},W={\"accent-token\":1,mathord:1,\"op-token\":1,spacing:1,textord:1},X={math:{},text:{}},$=X;function j(t,e,r,a,n,i){X[t][n]={font:e,group:r,replace:a},i&&a&&(X[t][a]=X[t][n])}var Z=\"main\",K=\"ams\",J=\"bin\",Q=\"mathord\",tt=\"op-token\",et=\"rel\";j(\"math\",Z,et,\"≡\",\"\\\\equiv\",!0),j(\"math\",Z,et,\"≺\",\"\\\\prec\",!0),j(\"math\",Z,et,\"≻\",\"\\\\succ\",!0),j(\"math\",Z,et,\"∼\",\"\\\\sim\",!0),j(\"math\",Z,et,\"⊥\",\"\\\\perp\"),j(\"math\",Z,et,\"⪯\",\"\\\\preceq\",!0),j(\"math\",Z,et,\"⪰\",\"\\\\succeq\",!0),j(\"math\",Z,et,\"≃\",\"\\\\simeq\",!0),j(\"math\",Z,et,\"∣\",\"\\\\mid\",!0),j(\"math\",Z,et,\"≪\",\"\\\\ll\",!0),j(\"math\",Z,et,\"≫\",\"\\\\gg\",!0),j(\"math\",Z,et,\"≍\",\"\\\\asymp\",!0),j(\"math\",Z,et,\"∥\",\"\\\\parallel\"),j(\"math\",Z,et,\"⋈\",\"\\\\bowtie\",!0),j(\"math\",Z,et,\"⌣\",\"\\\\smile\",!0),j(\"math\",Z,et,\"⊑\",\"\\\\sqsubseteq\",!0),j(\"math\",Z,et,\"⊒\",\"\\\\sqsupseteq\",!0),j(\"math\",Z,et,\"≐\",\"\\\\doteq\",!0),j(\"math\",Z,et,\"⌢\",\"\\\\frown\",!0),j(\"math\",Z,et,\"∋\",\"\\\\ni\",!0),j(\"math\",Z,et,\"∝\",\"\\\\propto\",!0),j(\"math\",Z,et,\"⊢\",\"\\\\vdash\",!0),j(\"math\",Z,et,\"⊣\",\"\\\\dashv\",!0),j(\"math\",Z,et,\"∋\",\"\\\\owns\"),j(\"math\",Z,\"punct\",\".\",\"\\\\ldotp\"),j(\"math\",Z,\"punct\",\"⋅\",\"\\\\cdotp\"),j(\"math\",Z,\"textord\",\"#\",\"\\\\#\"),j(\"text\",Z,\"textord\",\"#\",\"\\\\#\"),j(\"math\",Z,\"textord\",\"&\",\"\\\\&\"),j(\"text\",Z,\"textord\",\"&\",\"\\\\&\"),j(\"math\",Z,\"textord\",\"ℵ\",\"\\\\aleph\",!0),j(\"math\",Z,\"textord\",\"∀\",\"\\\\forall\",!0),j(\"math\",Z,\"textord\",\"ℏ\",\"\\\\hbar\",!0),j(\"math\",Z,\"textord\",\"∃\",\"\\\\exists\",!0),j(\"math\",Z,\"textord\",\"∇\",\"\\\\nabla\",!0),j(\"math\",Z,\"textord\",\"♭\",\"\\\\flat\",!0),j(\"math\",Z,\"textord\",\"ℓ\",\"\\\\ell\",!0),j(\"math\",Z,\"textord\",\"♮\",\"\\\\natural\",!0),j(\"math\",Z,\"textord\",\"♣\",\"\\\\clubsuit\",!0),j(\"math\",Z,\"textord\",\"℘\",\"\\\\wp\",!0),j(\"math\",Z,\"textord\",\"♯\",\"\\\\sharp\",!0),j(\"math\",Z,\"textord\",\"♢\",\"\\\\diamondsuit\",!0),j(\"math\",Z,\"textord\",\"ℜ\",\"\\\\Re\",!0),j(\"math\",Z,\"textord\",\"♡\",\"\\\\heartsuit\",!0),j(\"math\",Z,\"textord\",\"ℑ\",\"\\\\Im\",!0),j(\"math\",Z,\"textord\",\"♠\",\"\\\\spadesuit\",!0),j(\"text\",Z,\"textord\",\"§\",\"\\\\S\",!0),j(\"text\",Z,\"textord\",\"¶\",\"\\\\P\",!0),j(\"math\",Z,\"textord\",\"†\",\"\\\\dag\"),j(\"text\",Z,\"textord\",\"†\",\"\\\\dag\"),j(\"text\",Z,\"textord\",\"†\",\"\\\\textdagger\"),j(\"math\",Z,\"textord\",\"‡\",\"\\\\ddag\"),j(\"text\",Z,\"textord\",\"‡\",\"\\\\ddag\"),j(\"text\",Z,\"textord\",\"‡\",\"\\\\textdaggerdbl\"),j(\"math\",Z,\"close\",\"⎱\",\"\\\\rmoustache\",!0),j(\"math\",Z,\"open\",\"⎰\",\"\\\\lmoustache\",!0),j(\"math\",Z,\"close\",\"⟯\",\"\\\\rgroup\",!0),j(\"math\",Z,\"open\",\"⟮\",\"\\\\lgroup\",!0),j(\"math\",Z,J,\"∓\",\"\\\\mp\",!0),j(\"math\",Z,J,\"⊖\",\"\\\\ominus\",!0),j(\"math\",Z,J,\"⊎\",\"\\\\uplus\",!0),j(\"math\",Z,J,\"⊓\",\"\\\\sqcap\",!0),j(\"math\",Z,J,\"∗\",\"\\\\ast\"),j(\"math\",Z,J,\"⊔\",\"\\\\sqcup\",!0),j(\"math\",Z,J,\"◯\",\"\\\\bigcirc\"),j(\"math\",Z,J,\"∙\",\"\\\\bullet\"),j(\"math\",Z,J,\"‡\",\"\\\\ddagger\"),j(\"math\",Z,J,\"≀\",\"\\\\wr\",!0),j(\"math\",Z,J,\"⨿\",\"\\\\amalg\"),j(\"math\",Z,J,\"&\",\"\\\\And\"),j(\"math\",Z,et,\"⟵\",\"\\\\longleftarrow\",!0),j(\"math\",Z,et,\"⇐\",\"\\\\Leftarrow\",!0),j(\"math\",Z,et,\"⟸\",\"\\\\Longleftarrow\",!0),j(\"math\",Z,et,\"⟶\",\"\\\\longrightarrow\",!0),j(\"math\",Z,et,\"⇒\",\"\\\\Rightarrow\",!0),j(\"math\",Z,et,\"⟹\",\"\\\\Longrightarrow\",!0),j(\"math\",Z,et,\"↔\",\"\\\\leftrightarrow\",!0),j(\"math\",Z,et,\"⟷\",\"\\\\longleftrightarrow\",!0),j(\"math\",Z,et,\"⇔\",\"\\\\Leftrightarrow\",!0),j(\"math\",Z,et,\"⟺\",\"\\\\Longleftrightarrow\",!0),j(\"math\",Z,et,\"↦\",\"\\\\mapsto\",!0),j(\"math\",Z,et,\"⟼\",\"\\\\longmapsto\",!0),j(\"math\",Z,et,\"↗\",\"\\\\nearrow\",!0),j(\"math\",Z,et,\"↩\",\"\\\\hookleftarrow\",!0),j(\"math\",Z,et,\"↪\",\"\\\\hookrightarrow\",!0),j(\"math\",Z,et,\"↘\",\"\\\\searrow\",!0),j(\"math\",Z,et,\"↼\",\"\\\\leftharpoonup\",!0),j(\"math\",Z,et,\"⇀\",\"\\\\rightharpoonup\",!0),j(\"math\",Z,et,\"↙\",\"\\\\swarrow\",!0),j(\"math\",Z,et,\"↽\",\"\\\\leftharpoondown\",!0),j(\"math\",Z,et,\"⇁\",\"\\\\rightharpoondown\",!0),j(\"math\",Z,et,\"↖\",\"\\\\nwarrow\",!0),j(\"math\",Z,et,\"⇌\",\"\\\\rightleftharpoons\",!0),j(\"math\",K,et,\"≮\",\"\\\\nless\",!0),j(\"math\",K,et,\"\",\"\\\\@nleqslant\"),j(\"math\",K,et,\"\",\"\\\\@nleqq\"),j(\"math\",K,et,\"⪇\",\"\\\\lneq\",!0),j(\"math\",K,et,\"≨\",\"\\\\lneqq\",!0),j(\"math\",K,et,\"\",\"\\\\@lvertneqq\"),j(\"math\",K,et,\"⋦\",\"\\\\lnsim\",!0),j(\"math\",K,et,\"⪉\",\"\\\\lnapprox\",!0),j(\"math\",K,et,\"⊀\",\"\\\\nprec\",!0),j(\"math\",K,et,\"⋠\",\"\\\\npreceq\",!0),j(\"math\",K,et,\"⋨\",\"\\\\precnsim\",!0),j(\"math\",K,et,\"⪹\",\"\\\\precnapprox\",!0),j(\"math\",K,et,\"≁\",\"\\\\nsim\",!0),j(\"math\",K,et,\"\",\"\\\\@nshortmid\"),j(\"math\",K,et,\"∤\",\"\\\\nmid\",!0),j(\"math\",K,et,\"⊬\",\"\\\\nvdash\",!0),j(\"math\",K,et,\"⊭\",\"\\\\nvDash\",!0),j(\"math\",K,et,\"⋪\",\"\\\\ntriangleleft\"),j(\"math\",K,et,\"⋬\",\"\\\\ntrianglelefteq\",!0),j(\"math\",K,et,\"⊊\",\"\\\\subsetneq\",!0),j(\"math\",K,et,\"\",\"\\\\@varsubsetneq\"),j(\"math\",K,et,\"⫋\",\"\\\\subsetneqq\",!0),j(\"math\",K,et,\"\",\"\\\\@varsubsetneqq\"),j(\"math\",K,et,\"≯\",\"\\\\ngtr\",!0),j(\"math\",K,et,\"\",\"\\\\@ngeqslant\"),j(\"math\",K,et,\"\",\"\\\\@ngeqq\"),j(\"math\",K,et,\"⪈\",\"\\\\gneq\",!0),j(\"math\",K,et,\"≩\",\"\\\\gneqq\",!0),j(\"math\",K,et,\"\",\"\\\\@gvertneqq\"),j(\"math\",K,et,\"⋧\",\"\\\\gnsim\",!0),j(\"math\",K,et,\"⪊\",\"\\\\gnapprox\",!0),j(\"math\",K,et,\"⊁\",\"\\\\nsucc\",!0),j(\"math\",K,et,\"⋡\",\"\\\\nsucceq\",!0),j(\"math\",K,et,\"⋩\",\"\\\\succnsim\",!0),j(\"math\",K,et,\"⪺\",\"\\\\succnapprox\",!0),j(\"math\",K,et,\"≆\",\"\\\\ncong\",!0),j(\"math\",K,et,\"\",\"\\\\@nshortparallel\"),j(\"math\",K,et,\"∦\",\"\\\\nparallel\",!0),j(\"math\",K,et,\"⊯\",\"\\\\nVDash\",!0),j(\"math\",K,et,\"⋫\",\"\\\\ntriangleright\"),j(\"math\",K,et,\"⋭\",\"\\\\ntrianglerighteq\",!0),j(\"math\",K,et,\"\",\"\\\\@nsupseteqq\"),j(\"math\",K,et,\"⊋\",\"\\\\supsetneq\",!0),j(\"math\",K,et,\"\",\"\\\\@varsupsetneq\"),j(\"math\",K,et,\"⫌\",\"\\\\supsetneqq\",!0),j(\"math\",K,et,\"\",\"\\\\@varsupsetneqq\"),j(\"math\",K,et,\"⊮\",\"\\\\nVdash\",!0),j(\"math\",K,et,\"⪵\",\"\\\\precneqq\",!0),j(\"math\",K,et,\"⪶\",\"\\\\succneqq\",!0),j(\"math\",K,et,\"\",\"\\\\@nsubseteqq\"),j(\"math\",K,J,\"⊴\",\"\\\\unlhd\"),j(\"math\",K,J,\"⊵\",\"\\\\unrhd\"),j(\"math\",K,et,\"↚\",\"\\\\nleftarrow\",!0),j(\"math\",K,et,\"↛\",\"\\\\nrightarrow\",!0),j(\"math\",K,et,\"⇍\",\"\\\\nLeftarrow\",!0),j(\"math\",K,et,\"⇏\",\"\\\\nRightarrow\",!0),j(\"math\",K,et,\"↮\",\"\\\\nleftrightarrow\",!0),j(\"math\",K,et,\"⇎\",\"\\\\nLeftrightarrow\",!0),j(\"math\",K,et,\"△\",\"\\\\vartriangle\"),j(\"math\",K,\"textord\",\"ℏ\",\"\\\\hslash\"),j(\"math\",K,\"textord\",\"▽\",\"\\\\triangledown\"),j(\"math\",K,\"textord\",\"◊\",\"\\\\lozenge\"),j(\"math\",K,\"textord\",\"Ⓢ\",\"\\\\circledS\"),j(\"math\",K,\"textord\",\"®\",\"\\\\circledR\"),j(\"text\",K,\"textord\",\"®\",\"\\\\circledR\"),j(\"math\",K,\"textord\",\"∡\",\"\\\\measuredangle\",!0),j(\"math\",K,\"textord\",\"∄\",\"\\\\nexists\"),j(\"math\",K,\"textord\",\"℧\",\"\\\\mho\"),j(\"math\",K,\"textord\",\"Ⅎ\",\"\\\\Finv\",!0),j(\"math\",K,\"textord\",\"⅁\",\"\\\\Game\",!0),j(\"math\",K,\"textord\",\"‵\",\"\\\\backprime\"),j(\"math\",K,\"textord\",\"▲\",\"\\\\blacktriangle\"),j(\"math\",K,\"textord\",\"▼\",\"\\\\blacktriangledown\"),j(\"math\",K,\"textord\",\"■\",\"\\\\blacksquare\"),j(\"math\",K,\"textord\",\"⧫\",\"\\\\blacklozenge\"),j(\"math\",K,\"textord\",\"★\",\"\\\\bigstar\"),j(\"math\",K,\"textord\",\"∢\",\"\\\\sphericalangle\",!0),j(\"math\",K,\"textord\",\"∁\",\"\\\\complement\",!0),j(\"math\",K,\"textord\",\"ð\",\"\\\\eth\",!0),j(\"math\",K,\"textord\",\"╱\",\"\\\\diagup\"),j(\"math\",K,\"textord\",\"╲\",\"\\\\diagdown\"),j(\"math\",K,\"textord\",\"□\",\"\\\\square\"),j(\"math\",K,\"textord\",\"□\",\"\\\\Box\"),j(\"math\",K,\"textord\",\"◊\",\"\\\\Diamond\"),j(\"math\",K,\"textord\",\"¥\",\"\\\\yen\",!0),j(\"text\",K,\"textord\",\"¥\",\"\\\\yen\",!0),j(\"math\",K,\"textord\",\"✓\",\"\\\\checkmark\",!0),j(\"text\",K,\"textord\",\"✓\",\"\\\\checkmark\"),j(\"math\",K,\"textord\",\"ℶ\",\"\\\\beth\",!0),j(\"math\",K,\"textord\",\"ℸ\",\"\\\\daleth\",!0),j(\"math\",K,\"textord\",\"ℷ\",\"\\\\gimel\",!0),j(\"math\",K,\"textord\",\"ϝ\",\"\\\\digamma\",!0),j(\"math\",K,\"textord\",\"ϰ\",\"\\\\varkappa\"),j(\"math\",K,\"open\",\"┌\",\"\\\\ulcorner\",!0),j(\"math\",K,\"close\",\"┐\",\"\\\\urcorner\",!0),j(\"math\",K,\"open\",\"└\",\"\\\\llcorner\",!0),j(\"math\",K,\"close\",\"┘\",\"\\\\lrcorner\",!0),j(\"math\",K,et,\"≦\",\"\\\\leqq\",!0),j(\"math\",K,et,\"⩽\",\"\\\\leqslant\",!0),j(\"math\",K,et,\"⪕\",\"\\\\eqslantless\",!0),j(\"math\",K,et,\"≲\",\"\\\\lesssim\",!0),j(\"math\",K,et,\"⪅\",\"\\\\lessapprox\",!0),j(\"math\",K,et,\"≊\",\"\\\\approxeq\",!0),j(\"math\",K,J,\"⋖\",\"\\\\lessdot\"),j(\"math\",K,et,\"⋘\",\"\\\\lll\",!0),j(\"math\",K,et,\"≶\",\"\\\\lessgtr\",!0),j(\"math\",K,et,\"⋚\",\"\\\\lesseqgtr\",!0),j(\"math\",K,et,\"⪋\",\"\\\\lesseqqgtr\",!0),j(\"math\",K,et,\"≑\",\"\\\\doteqdot\"),j(\"math\",K,et,\"≓\",\"\\\\risingdotseq\",!0),j(\"math\",K,et,\"≒\",\"\\\\fallingdotseq\",!0),j(\"math\",K,et,\"∽\",\"\\\\backsim\",!0),j(\"math\",K,et,\"⋍\",\"\\\\backsimeq\",!0),j(\"math\",K,et,\"⫅\",\"\\\\subseteqq\",!0),j(\"math\",K,et,\"⋐\",\"\\\\Subset\",!0),j(\"math\",K,et,\"⊏\",\"\\\\sqsubset\",!0),j(\"math\",K,et,\"≼\",\"\\\\preccurlyeq\",!0),j(\"math\",K,et,\"⋞\",\"\\\\curlyeqprec\",!0),j(\"math\",K,et,\"≾\",\"\\\\precsim\",!0),j(\"math\",K,et,\"⪷\",\"\\\\precapprox\",!0),j(\"math\",K,et,\"⊲\",\"\\\\vartriangleleft\"),j(\"math\",K,et,\"⊴\",\"\\\\trianglelefteq\"),j(\"math\",K,et,\"⊨\",\"\\\\vDash\",!0),j(\"math\",K,et,\"⊪\",\"\\\\Vvdash\",!0),j(\"math\",K,et,\"⌣\",\"\\\\smallsmile\"),j(\"math\",K,et,\"⌢\",\"\\\\smallfrown\"),j(\"math\",K,et,\"≏\",\"\\\\bumpeq\",!0),j(\"math\",K,et,\"≎\",\"\\\\Bumpeq\",!0),j(\"math\",K,et,\"≧\",\"\\\\geqq\",!0),j(\"math\",K,et,\"⩾\",\"\\\\geqslant\",!0),j(\"math\",K,et,\"⪖\",\"\\\\eqslantgtr\",!0),j(\"math\",K,et,\"≳\",\"\\\\gtrsim\",!0),j(\"math\",K,et,\"⪆\",\"\\\\gtrapprox\",!0),j(\"math\",K,J,\"⋗\",\"\\\\gtrdot\"),j(\"math\",K,et,\"⋙\",\"\\\\ggg\",!0),j(\"math\",K,et,\"≷\",\"\\\\gtrless\",!0),j(\"math\",K,et,\"⋛\",\"\\\\gtreqless\",!0),j(\"math\",K,et,\"⪌\",\"\\\\gtreqqless\",!0),j(\"math\",K,et,\"≖\",\"\\\\eqcirc\",!0),j(\"math\",K,et,\"≗\",\"\\\\circeq\",!0),j(\"math\",K,et,\"≜\",\"\\\\triangleq\",!0),j(\"math\",K,et,\"∼\",\"\\\\thicksim\"),j(\"math\",K,et,\"≈\",\"\\\\thickapprox\"),j(\"math\",K,et,\"⫆\",\"\\\\supseteqq\",!0),j(\"math\",K,et,\"⋑\",\"\\\\Supset\",!0),j(\"math\",K,et,\"⊐\",\"\\\\sqsupset\",!0),j(\"math\",K,et,\"≽\",\"\\\\succcurlyeq\",!0),j(\"math\",K,et,\"⋟\",\"\\\\curlyeqsucc\",!0),j(\"math\",K,et,\"≿\",\"\\\\succsim\",!0),j(\"math\",K,et,\"⪸\",\"\\\\succapprox\",!0),j(\"math\",K,et,\"⊳\",\"\\\\vartriangleright\"),j(\"math\",K,et,\"⊵\",\"\\\\trianglerighteq\"),j(\"math\",K,et,\"⊩\",\"\\\\Vdash\",!0),j(\"math\",K,et,\"∣\",\"\\\\shortmid\"),j(\"math\",K,et,\"∥\",\"\\\\shortparallel\"),j(\"math\",K,et,\"≬\",\"\\\\between\",!0),j(\"math\",K,et,\"⋔\",\"\\\\pitchfork\",!0),j(\"math\",K,et,\"∝\",\"\\\\varpropto\"),j(\"math\",K,et,\"◀\",\"\\\\blacktriangleleft\"),j(\"math\",K,et,\"∴\",\"\\\\therefore\",!0),j(\"math\",K,et,\"∍\",\"\\\\backepsilon\"),j(\"math\",K,et,\"▶\",\"\\\\blacktriangleright\"),j(\"math\",K,et,\"∵\",\"\\\\because\",!0),j(\"math\",K,et,\"⋘\",\"\\\\llless\"),j(\"math\",K,et,\"⋙\",\"\\\\gggtr\"),j(\"math\",K,J,\"⊲\",\"\\\\lhd\"),j(\"math\",K,J,\"⊳\",\"\\\\rhd\"),j(\"math\",K,et,\"≂\",\"\\\\eqsim\",!0),j(\"math\",Z,et,\"⋈\",\"\\\\Join\"),j(\"math\",K,et,\"≑\",\"\\\\Doteq\",!0),j(\"math\",K,J,\"∔\",\"\\\\dotplus\",!0),j(\"math\",K,J,\"∖\",\"\\\\smallsetminus\"),j(\"math\",K,J,\"⋒\",\"\\\\Cap\",!0),j(\"math\",K,J,\"⋓\",\"\\\\Cup\",!0),j(\"math\",K,J,\"⩞\",\"\\\\doublebarwedge\",!0),j(\"math\",K,J,\"⊟\",\"\\\\boxminus\",!0),j(\"math\",K,J,\"⊞\",\"\\\\boxplus\",!0),j(\"math\",K,J,\"⋇\",\"\\\\divideontimes\",!0),j(\"math\",K,J,\"⋉\",\"\\\\ltimes\",!0),j(\"math\",K,J,\"⋊\",\"\\\\rtimes\",!0),j(\"math\",K,J,\"⋋\",\"\\\\leftthreetimes\",!0),j(\"math\",K,J,\"⋌\",\"\\\\rightthreetimes\",!0),j(\"math\",K,J,\"⋏\",\"\\\\curlywedge\",!0),j(\"math\",K,J,\"⋎\",\"\\\\curlyvee\",!0),j(\"math\",K,J,\"⊝\",\"\\\\circleddash\",!0),j(\"math\",K,J,\"⊛\",\"\\\\circledast\",!0),j(\"math\",K,J,\"⋅\",\"\\\\centerdot\"),j(\"math\",K,J,\"⊺\",\"\\\\intercal\",!0),j(\"math\",K,J,\"⋒\",\"\\\\doublecap\"),j(\"math\",K,J,\"⋓\",\"\\\\doublecup\"),j(\"math\",K,J,\"⊠\",\"\\\\boxtimes\",!0),j(\"math\",K,et,\"⇢\",\"\\\\dashrightarrow\",!0),j(\"math\",K,et,\"⇠\",\"\\\\dashleftarrow\",!0),j(\"math\",K,et,\"⇇\",\"\\\\leftleftarrows\",!0),j(\"math\",K,et,\"⇆\",\"\\\\leftrightarrows\",!0),j(\"math\",K,et,\"⇚\",\"\\\\Lleftarrow\",!0),j(\"math\",K,et,\"↞\",\"\\\\twoheadleftarrow\",!0),j(\"math\",K,et,\"↢\",\"\\\\leftarrowtail\",!0),j(\"math\",K,et,\"↫\",\"\\\\looparrowleft\",!0),j(\"math\",K,et,\"⇋\",\"\\\\leftrightharpoons\",!0),j(\"math\",K,et,\"↶\",\"\\\\curvearrowleft\",!0),j(\"math\",K,et,\"↺\",\"\\\\circlearrowleft\",!0),j(\"math\",K,et,\"↰\",\"\\\\Lsh\",!0),j(\"math\",K,et,\"⇈\",\"\\\\upuparrows\",!0),j(\"math\",K,et,\"↿\",\"\\\\upharpoonleft\",!0),j(\"math\",K,et,\"⇃\",\"\\\\downharpoonleft\",!0),j(\"math\",K,et,\"⊸\",\"\\\\multimap\",!0),j(\"math\",K,et,\"↭\",\"\\\\leftrightsquigarrow\",!0),j(\"math\",K,et,\"⇉\",\"\\\\rightrightarrows\",!0),j(\"math\",K,et,\"⇄\",\"\\\\rightleftarrows\",!0),j(\"math\",K,et,\"↠\",\"\\\\twoheadrightarrow\",!0),j(\"math\",K,et,\"↣\",\"\\\\rightarrowtail\",!0),j(\"math\",K,et,\"↬\",\"\\\\looparrowright\",!0),j(\"math\",K,et,\"↷\",\"\\\\curvearrowright\",!0),j(\"math\",K,et,\"↻\",\"\\\\circlearrowright\",!0),j(\"math\",K,et,\"↱\",\"\\\\Rsh\",!0),j(\"math\",K,et,\"⇊\",\"\\\\downdownarrows\",!0),j(\"math\",K,et,\"↾\",\"\\\\upharpoonright\",!0),j(\"math\",K,et,\"⇂\",\"\\\\downharpoonright\",!0),j(\"math\",K,et,\"⇝\",\"\\\\rightsquigarrow\",!0),j(\"math\",K,et,\"⇝\",\"\\\\leadsto\"),j(\"math\",K,et,\"⇛\",\"\\\\Rrightarrow\",!0),j(\"math\",K,et,\"↾\",\"\\\\restriction\"),j(\"math\",Z,\"textord\",\"‘\",\"`\"),j(\"math\",Z,\"textord\",\"$\",\"\\\\$\"),j(\"text\",Z,\"textord\",\"$\",\"\\\\$\"),j(\"text\",Z,\"textord\",\"$\",\"\\\\textdollar\"),j(\"math\",Z,\"textord\",\"%\",\"\\\\%\"),j(\"text\",Z,\"textord\",\"%\",\"\\\\%\"),j(\"math\",Z,\"textord\",\"_\",\"\\\\_\"),j(\"text\",Z,\"textord\",\"_\",\"\\\\_\"),j(\"text\",Z,\"textord\",\"_\",\"\\\\textunderscore\"),j(\"math\",Z,\"textord\",\"∠\",\"\\\\angle\",!0),j(\"math\",Z,\"textord\",\"∞\",\"\\\\infty\",!0),j(\"math\",Z,\"textord\",\"′\",\"\\\\prime\"),j(\"math\",Z,\"textord\",\"△\",\"\\\\triangle\"),j(\"math\",Z,\"textord\",\"Γ\",\"\\\\Gamma\",!0),j(\"math\",Z,\"textord\",\"Δ\",\"\\\\Delta\",!0),j(\"math\",Z,\"textord\",\"Θ\",\"\\\\Theta\",!0),j(\"math\",Z,\"textord\",\"Λ\",\"\\\\Lambda\",!0),j(\"math\",Z,\"textord\",\"Ξ\",\"\\\\Xi\",!0),j(\"math\",Z,\"textord\",\"Π\",\"\\\\Pi\",!0),j(\"math\",Z,\"textord\",\"Σ\",\"\\\\Sigma\",!0),j(\"math\",Z,\"textord\",\"Υ\",\"\\\\Upsilon\",!0),j(\"math\",Z,\"textord\",\"Φ\",\"\\\\Phi\",!0),j(\"math\",Z,\"textord\",\"Ψ\",\"\\\\Psi\",!0),j(\"math\",Z,\"textord\",\"Ω\",\"\\\\Omega\",!0),j(\"math\",Z,\"textord\",\"A\",\"Α\"),j(\"math\",Z,\"textord\",\"B\",\"Β\"),j(\"math\",Z,\"textord\",\"E\",\"Ε\"),j(\"math\",Z,\"textord\",\"Z\",\"Ζ\"),j(\"math\",Z,\"textord\",\"H\",\"Η\"),j(\"math\",Z,\"textord\",\"I\",\"Ι\"),j(\"math\",Z,\"textord\",\"K\",\"Κ\"),j(\"math\",Z,\"textord\",\"M\",\"Μ\"),j(\"math\",Z,\"textord\",\"N\",\"Ν\"),j(\"math\",Z,\"textord\",\"O\",\"Ο\"),j(\"math\",Z,\"textord\",\"P\",\"Ρ\"),j(\"math\",Z,\"textord\",\"T\",\"Τ\"),j(\"math\",Z,\"textord\",\"X\",\"Χ\"),j(\"math\",Z,\"textord\",\"¬\",\"\\\\neg\",!0),j(\"math\",Z,\"textord\",\"¬\",\"\\\\lnot\"),j(\"math\",Z,\"textord\",\"⊤\",\"\\\\top\"),j(\"math\",Z,\"textord\",\"⊥\",\"\\\\bot\"),j(\"math\",Z,\"textord\",\"∅\",\"\\\\emptyset\"),j(\"math\",K,\"textord\",\"∅\",\"\\\\varnothing\"),j(\"math\",Z,Q,\"α\",\"\\\\alpha\",!0),j(\"math\",Z,Q,\"β\",\"\\\\beta\",!0),j(\"math\",Z,Q,\"γ\",\"\\\\gamma\",!0),j(\"math\",Z,Q,\"δ\",\"\\\\delta\",!0),j(\"math\",Z,Q,\"ϵ\",\"\\\\epsilon\",!0),j(\"math\",Z,Q,\"ζ\",\"\\\\zeta\",!0),j(\"math\",Z,Q,\"η\",\"\\\\eta\",!0),j(\"math\",Z,Q,\"θ\",\"\\\\theta\",!0),j(\"math\",Z,Q,\"ι\",\"\\\\iota\",!0),j(\"math\",Z,Q,\"κ\",\"\\\\kappa\",!0),j(\"math\",Z,Q,\"λ\",\"\\\\lambda\",!0),j(\"math\",Z,Q,\"μ\",\"\\\\mu\",!0),j(\"math\",Z,Q,\"ν\",\"\\\\nu\",!0),j(\"math\",Z,Q,\"ξ\",\"\\\\xi\",!0),j(\"math\",Z,Q,\"ο\",\"\\\\omicron\",!0),j(\"math\",Z,Q,\"π\",\"\\\\pi\",!0),j(\"math\",Z,Q,\"ρ\",\"\\\\rho\",!0),j(\"math\",Z,Q,\"σ\",\"\\\\sigma\",!0),j(\"math\",Z,Q,\"τ\",\"\\\\tau\",!0),j(\"math\",Z,Q,\"υ\",\"\\\\upsilon\",!0),j(\"math\",Z,Q,\"ϕ\",\"\\\\phi\",!0),j(\"math\",Z,Q,\"χ\",\"\\\\chi\",!0),j(\"math\",Z,Q,\"ψ\",\"\\\\psi\",!0),j(\"math\",Z,Q,\"ω\",\"\\\\omega\",!0),j(\"math\",Z,Q,\"ε\",\"\\\\varepsilon\",!0),j(\"math\",Z,Q,\"ϑ\",\"\\\\vartheta\",!0),j(\"math\",Z,Q,\"ϖ\",\"\\\\varpi\",!0),j(\"math\",Z,Q,\"ϱ\",\"\\\\varrho\",!0),j(\"math\",Z,Q,\"ς\",\"\\\\varsigma\",!0),j(\"math\",Z,Q,\"φ\",\"\\\\varphi\",!0),j(\"math\",Z,J,\"∗\",\"*\"),j(\"math\",Z,J,\"+\",\"+\"),j(\"math\",Z,J,\"−\",\"-\"),j(\"math\",Z,J,\"⋅\",\"\\\\cdot\",!0),j(\"math\",Z,J,\"∘\",\"\\\\circ\"),j(\"math\",Z,J,\"÷\",\"\\\\div\",!0),j(\"math\",Z,J,\"±\",\"\\\\pm\",!0),j(\"math\",Z,J,\"×\",\"\\\\times\",!0),j(\"math\",Z,J,\"∩\",\"\\\\cap\",!0),j(\"math\",Z,J,\"∪\",\"\\\\cup\",!0),j(\"math\",Z,J,\"∖\",\"\\\\setminus\"),j(\"math\",Z,J,\"∧\",\"\\\\land\"),j(\"math\",Z,J,\"∨\",\"\\\\lor\"),j(\"math\",Z,J,\"∧\",\"\\\\wedge\",!0),j(\"math\",Z,J,\"∨\",\"\\\\vee\",!0),j(\"math\",Z,\"textord\",\"√\",\"\\\\surd\"),j(\"math\",Z,\"open\",\"(\",\"(\"),j(\"math\",Z,\"open\",\"[\",\"[\"),j(\"math\",Z,\"open\",\"⟨\",\"\\\\langle\",!0),j(\"math\",Z,\"open\",\"∣\",\"\\\\lvert\"),j(\"math\",Z,\"open\",\"∥\",\"\\\\lVert\"),j(\"math\",Z,\"close\",\")\",\")\"),j(\"math\",Z,\"close\",\"]\",\"]\"),j(\"math\",Z,\"close\",\"?\",\"?\"),j(\"math\",Z,\"close\",\"!\",\"!\"),j(\"math\",Z,\"close\",\"⟩\",\"\\\\rangle\",!0),j(\"math\",Z,\"close\",\"∣\",\"\\\\rvert\"),j(\"math\",Z,\"close\",\"∥\",\"\\\\rVert\"),j(\"math\",Z,et,\"=\",\"=\"),j(\"math\",Z,et,\"<\",\"<\"),j(\"math\",Z,et,\">\",\">\"),j(\"math\",Z,et,\":\",\":\"),j(\"math\",Z,et,\"≈\",\"\\\\approx\",!0),j(\"math\",Z,et,\"≅\",\"\\\\cong\",!0),j(\"math\",Z,et,\"≥\",\"\\\\ge\"),j(\"math\",Z,et,\"≥\",\"\\\\geq\",!0),j(\"math\",Z,et,\"←\",\"\\\\gets\"),j(\"math\",Z,et,\">\",\"\\\\gt\"),j(\"math\",Z,et,\"∈\",\"\\\\in\",!0),j(\"math\",Z,et,\"\",\"\\\\@not\"),j(\"math\",Z,et,\"⊂\",\"\\\\subset\",!0),j(\"math\",Z,et,\"⊃\",\"\\\\supset\",!0),j(\"math\",Z,et,\"⊆\",\"\\\\subseteq\",!0),j(\"math\",Z,et,\"⊇\",\"\\\\supseteq\",!0),j(\"math\",K,et,\"⊈\",\"\\\\nsubseteq\",!0),j(\"math\",K,et,\"⊉\",\"\\\\nsupseteq\",!0),j(\"math\",Z,et,\"⊨\",\"\\\\models\"),j(\"math\",Z,et,\"←\",\"\\\\leftarrow\",!0),j(\"math\",Z,et,\"≤\",\"\\\\le\"),j(\"math\",Z,et,\"≤\",\"\\\\leq\",!0),j(\"math\",Z,et,\"<\",\"\\\\lt\"),j(\"math\",Z,et,\"→\",\"\\\\rightarrow\",!0),j(\"math\",Z,et,\"→\",\"\\\\to\"),j(\"math\",K,et,\"≱\",\"\\\\ngeq\",!0),j(\"math\",K,et,\"≰\",\"\\\\nleq\",!0),j(\"math\",Z,\"spacing\",\" \",\"\\\\ \"),j(\"math\",Z,\"spacing\",\" \",\"~\"),j(\"math\",Z,\"spacing\",\" \",\"\\\\space\"),j(\"math\",Z,\"spacing\",\" \",\"\\\\nobreakspace\"),j(\"text\",Z,\"spacing\",\" \",\"\\\\ \"),j(\"text\",Z,\"spacing\",\" \",\"~\"),j(\"text\",Z,\"spacing\",\" \",\"\\\\space\"),j(\"text\",Z,\"spacing\",\" \",\"\\\\nobreakspace\"),j(\"math\",Z,\"spacing\",null,\"\\\\nobreak\"),j(\"math\",Z,\"spacing\",null,\"\\\\allowbreak\"),j(\"math\",Z,\"punct\",\",\",\",\"),j(\"math\",Z,\"punct\",\";\",\";\"),j(\"math\",K,J,\"⊼\",\"\\\\barwedge\",!0),j(\"math\",K,J,\"⊻\",\"\\\\veebar\",!0),j(\"math\",Z,J,\"⊙\",\"\\\\odot\",!0),j(\"math\",Z,J,\"⊕\",\"\\\\oplus\",!0),j(\"math\",Z,J,\"⊗\",\"\\\\otimes\",!0),j(\"math\",Z,\"textord\",\"∂\",\"\\\\partial\",!0),j(\"math\",Z,J,\"⊘\",\"\\\\oslash\",!0),j(\"math\",K,J,\"⊚\",\"\\\\circledcirc\",!0),j(\"math\",K,J,\"⊡\",\"\\\\boxdot\",!0),j(\"math\",Z,J,\"△\",\"\\\\bigtriangleup\"),j(\"math\",Z,J,\"▽\",\"\\\\bigtriangledown\"),j(\"math\",Z,J,\"†\",\"\\\\dagger\"),j(\"math\",Z,J,\"⋄\",\"\\\\diamond\"),j(\"math\",Z,J,\"⋆\",\"\\\\star\"),j(\"math\",Z,J,\"◃\",\"\\\\triangleleft\"),j(\"math\",Z,J,\"▹\",\"\\\\triangleright\"),j(\"math\",Z,\"open\",\"{\",\"\\\\{\"),j(\"text\",Z,\"textord\",\"{\",\"\\\\{\"),j(\"text\",Z,\"textord\",\"{\",\"\\\\textbraceleft\"),j(\"math\",Z,\"close\",\"}\",\"\\\\}\"),j(\"text\",Z,\"textord\",\"}\",\"\\\\}\"),j(\"text\",Z,\"textord\",\"}\",\"\\\\textbraceright\"),j(\"math\",Z,\"open\",\"{\",\"\\\\lbrace\"),j(\"math\",Z,\"close\",\"}\",\"\\\\rbrace\"),j(\"math\",Z,\"open\",\"[\",\"\\\\lbrack\"),j(\"text\",Z,\"textord\",\"[\",\"\\\\lbrack\"),j(\"math\",Z,\"close\",\"]\",\"\\\\rbrack\"),j(\"text\",Z,\"textord\",\"]\",\"\\\\rbrack\"),j(\"math\",Z,\"open\",\"(\",\"\\\\lparen\"),j(\"math\",Z,\"close\",\")\",\"\\\\rparen\"),j(\"text\",Z,\"textord\",\"<\",\"\\\\textless\"),j(\"text\",Z,\"textord\",\">\",\"\\\\textgreater\"),j(\"math\",Z,\"open\",\"⌊\",\"\\\\lfloor\",!0),j(\"math\",Z,\"close\",\"⌋\",\"\\\\rfloor\",!0),j(\"math\",Z,\"open\",\"⌈\",\"\\\\lceil\",!0),j(\"math\",Z,\"close\",\"⌉\",\"\\\\rceil\",!0),j(\"math\",Z,\"textord\",\"\\\\\",\"\\\\backslash\"),j(\"math\",Z,\"textord\",\"∣\",\"|\"),j(\"math\",Z,\"textord\",\"∣\",\"\\\\vert\"),j(\"text\",Z,\"textord\",\"|\",\"\\\\textbar\"),j(\"math\",Z,\"textord\",\"∥\",\"\\\\|\"),j(\"math\",Z,\"textord\",\"∥\",\"\\\\Vert\"),j(\"text\",Z,\"textord\",\"∥\",\"\\\\textbardbl\"),j(\"text\",Z,\"textord\",\"~\",\"\\\\textasciitilde\"),j(\"text\",Z,\"textord\",\"\\\\\",\"\\\\textbackslash\"),j(\"text\",Z,\"textord\",\"^\",\"\\\\textasciicircum\"),j(\"math\",Z,et,\"↑\",\"\\\\uparrow\",!0),j(\"math\",Z,et,\"⇑\",\"\\\\Uparrow\",!0),j(\"math\",Z,et,\"↓\",\"\\\\downarrow\",!0),j(\"math\",Z,et,\"⇓\",\"\\\\Downarrow\",!0),j(\"math\",Z,et,\"↕\",\"\\\\updownarrow\",!0),j(\"math\",Z,et,\"⇕\",\"\\\\Updownarrow\",!0),j(\"math\",Z,tt,\"∐\",\"\\\\coprod\"),j(\"math\",Z,tt,\"⋁\",\"\\\\bigvee\"),j(\"math\",Z,tt,\"⋀\",\"\\\\bigwedge\"),j(\"math\",Z,tt,\"⨄\",\"\\\\biguplus\"),j(\"math\",Z,tt,\"⋂\",\"\\\\bigcap\"),j(\"math\",Z,tt,\"⋃\",\"\\\\bigcup\"),j(\"math\",Z,tt,\"∫\",\"\\\\int\"),j(\"math\",Z,tt,\"∫\",\"\\\\intop\"),j(\"math\",Z,tt,\"∬\",\"\\\\iint\"),j(\"math\",Z,tt,\"∭\",\"\\\\iiint\"),j(\"math\",Z,tt,\"∏\",\"\\\\prod\"),j(\"math\",Z,tt,\"∑\",\"\\\\sum\"),j(\"math\",Z,tt,\"⨂\",\"\\\\bigotimes\"),j(\"math\",Z,tt,\"⨁\",\"\\\\bigoplus\"),j(\"math\",Z,tt,\"⨀\",\"\\\\bigodot\"),j(\"math\",Z,tt,\"∮\",\"\\\\oint\"),j(\"math\",Z,tt,\"∯\",\"\\\\oiint\"),j(\"math\",Z,tt,\"∰\",\"\\\\oiiint\"),j(\"math\",Z,tt,\"⨆\",\"\\\\bigsqcup\"),j(\"math\",Z,tt,\"∫\",\"\\\\smallint\"),j(\"text\",Z,\"inner\",\"…\",\"\\\\textellipsis\"),j(\"math\",Z,\"inner\",\"…\",\"\\\\mathellipsis\"),j(\"text\",Z,\"inner\",\"…\",\"\\\\ldots\",!0),j(\"math\",Z,\"inner\",\"…\",\"\\\\ldots\",!0),j(\"math\",Z,\"inner\",\"⋯\",\"\\\\@cdots\",!0),j(\"math\",Z,\"inner\",\"⋱\",\"\\\\ddots\",!0),j(\"math\",Z,\"textord\",\"⋮\",\"\\\\varvdots\"),j(\"math\",Z,\"accent-token\",\"ˊ\",\"\\\\acute\"),j(\"math\",Z,\"accent-token\",\"ˋ\",\"\\\\grave\"),j(\"math\",Z,\"accent-token\",\"¨\",\"\\\\ddot\"),j(\"math\",Z,\"accent-token\",\"~\",\"\\\\tilde\"),j(\"math\",Z,\"accent-token\",\"ˉ\",\"\\\\bar\"),j(\"math\",Z,\"accent-token\",\"˘\",\"\\\\breve\"),j(\"math\",Z,\"accent-token\",\"ˇ\",\"\\\\check\"),j(\"math\",Z,\"accent-token\",\"^\",\"\\\\hat\"),j(\"math\",Z,\"accent-token\",\"⃗\",\"\\\\vec\"),j(\"math\",Z,\"accent-token\",\"˙\",\"\\\\dot\"),j(\"math\",Z,\"accent-token\",\"˚\",\"\\\\mathring\"),j(\"math\",Z,Q,\"ı\",\"\\\\imath\",!0),j(\"math\",Z,Q,\"ȷ\",\"\\\\jmath\",!0),j(\"text\",Z,\"textord\",\"ı\",\"\\\\i\",!0),j(\"text\",Z,\"textord\",\"ȷ\",\"\\\\j\",!0),j(\"text\",Z,\"textord\",\"ß\",\"\\\\ss\",!0),j(\"text\",Z,\"textord\",\"æ\",\"\\\\ae\",!0),j(\"text\",Z,\"textord\",\"æ\",\"\\\\ae\",!0),j(\"text\",Z,\"textord\",\"œ\",\"\\\\oe\",!0),j(\"text\",Z,\"textord\",\"ø\",\"\\\\o\",!0),j(\"text\",Z,\"textord\",\"Æ\",\"\\\\AE\",!0),j(\"text\",Z,\"textord\",\"Œ\",\"\\\\OE\",!0),j(\"text\",Z,\"textord\",\"Ø\",\"\\\\O\",!0),j(\"text\",Z,\"accent-token\",\"ˊ\",\"\\\\'\"),j(\"text\",Z,\"accent-token\",\"ˋ\",\"\\\\`\"),j(\"text\",Z,\"accent-token\",\"ˆ\",\"\\\\^\"),j(\"text\",Z,\"accent-token\",\"˜\",\"\\\\~\"),j(\"text\",Z,\"accent-token\",\"ˉ\",\"\\\\=\"),j(\"text\",Z,\"accent-token\",\"˘\",\"\\\\u\"),j(\"text\",Z,\"accent-token\",\"˙\",\"\\\\.\"),j(\"text\",Z,\"accent-token\",\"˚\",\"\\\\r\"),j(\"text\",Z,\"accent-token\",\"ˇ\",\"\\\\v\"),j(\"text\",Z,\"accent-token\",\"¨\",'\\\\\"'),j(\"text\",Z,\"accent-token\",\"˝\",\"\\\\H\"),j(\"text\",Z,\"accent-token\",\"◯\",\"\\\\textcircled\");var rt={\"--\":!0,\"---\":!0,\"``\":!0,\"''\":!0};j(\"text\",Z,\"textord\",\"–\",\"--\"),j(\"text\",Z,\"textord\",\"–\",\"\\\\textendash\"),j(\"text\",Z,\"textord\",\"—\",\"---\"),j(\"text\",Z,\"textord\",\"—\",\"\\\\textemdash\"),j(\"text\",Z,\"textord\",\"‘\",\"`\"),j(\"text\",Z,\"textord\",\"‘\",\"\\\\textquoteleft\"),j(\"text\",Z,\"textord\",\"’\",\"'\"),j(\"text\",Z,\"textord\",\"’\",\"\\\\textquoteright\"),j(\"text\",Z,\"textord\",\"“\",\"``\"),j(\"text\",Z,\"textord\",\"“\",\"\\\\textquotedblleft\"),j(\"text\",Z,\"textord\",\"”\",\"''\"),j(\"text\",Z,\"textord\",\"”\",\"\\\\textquotedblright\"),j(\"math\",Z,\"textord\",\"°\",\"\\\\degree\",!0),j(\"text\",Z,\"textord\",\"°\",\"\\\\degree\"),j(\"text\",Z,\"textord\",\"°\",\"\\\\textdegree\",!0),j(\"math\",Z,Q,\"£\",\"\\\\pounds\"),j(\"math\",Z,Q,\"£\",\"\\\\mathsterling\",!0),j(\"text\",Z,Q,\"£\",\"\\\\pounds\"),j(\"text\",Z,Q,\"£\",\"\\\\textsterling\",!0),j(\"math\",K,\"textord\",\"✠\",\"\\\\maltese\"),j(\"text\",K,\"textord\",\"✠\",\"\\\\maltese\"),j(\"text\",Z,\"spacing\",\" \",\"\\\\ \"),j(\"text\",Z,\"spacing\",\" \",\" \"),j(\"text\",Z,\"spacing\",\" \",\"~\");for(var at=0;at<'0123456789/@.\"'.length;at++){var nt='0123456789/@.\"'.charAt(at);j(\"math\",Z,\"textord\",nt,nt)}for(var it=0;it<'0123456789!@*()-=+[]<>|\";:?/.,'.length;it++){var ot='0123456789!@*()-=+[]<>|\";:?/.,'.charAt(it);j(\"text\",Z,\"textord\",ot,ot)}for(var st=\"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\",ht=0;ht<st.length;ht++){var lt=st.charAt(ht);j(\"math\",Z,Q,lt,lt),j(\"text\",Z,\"textord\",lt,lt)}j(\"math\",K,\"textord\",\"C\",\"ℂ\"),j(\"text\",K,\"textord\",\"C\",\"ℂ\"),j(\"math\",K,\"textord\",\"H\",\"ℍ\"),j(\"text\",K,\"textord\",\"H\",\"ℍ\"),j(\"math\",K,\"textord\",\"N\",\"ℕ\"),j(\"text\",K,\"textord\",\"N\",\"ℕ\"),j(\"math\",K,\"textord\",\"P\",\"ℙ\"),j(\"text\",K,\"textord\",\"P\",\"ℙ\"),j(\"math\",K,\"textord\",\"Q\",\"ℚ\"),j(\"text\",K,\"textord\",\"Q\",\"ℚ\"),j(\"math\",K,\"textord\",\"R\",\"ℝ\"),j(\"text\",K,\"textord\",\"R\",\"ℝ\"),j(\"math\",K,\"textord\",\"Z\",\"ℤ\"),j(\"text\",K,\"textord\",\"Z\",\"ℤ\"),j(\"math\",Z,Q,\"h\",\"ℎ\"),j(\"text\",Z,Q,\"h\",\"ℎ\");for(var mt=\"\",ct=0;ct<st.length;ct++){var ut=st.charAt(ct);j(\"math\",Z,Q,ut,mt=String.fromCharCode(55349,56320+ct)),j(\"text\",Z,\"textord\",ut,mt),j(\"math\",Z,Q,ut,mt=String.fromCharCode(55349,56372+ct)),j(\"text\",Z,\"textord\",ut,mt),j(\"math\",Z,Q,ut,mt=String.fromCharCode(55349,56424+ct)),j(\"text\",Z,\"textord\",ut,mt),j(\"math\",Z,Q,ut,mt=String.fromCharCode(55349,56580+ct)),j(\"text\",Z,\"textord\",ut,mt),j(\"math\",Z,Q,ut,mt=String.fromCharCode(55349,56736+ct)),j(\"text\",Z,\"textord\",ut,mt),j(\"math\",Z,Q,ut,mt=String.fromCharCode(55349,56788+ct)),j(\"text\",Z,\"textord\",ut,mt),j(\"math\",Z,Q,ut,mt=String.fromCharCode(55349,56840+ct)),j(\"text\",Z,\"textord\",ut,mt),j(\"math\",Z,Q,ut,mt=String.fromCharCode(55349,56944+ct)),j(\"text\",Z,\"textord\",ut,mt),ct<26&&(j(\"math\",Z,Q,ut,mt=String.fromCharCode(55349,56632+ct)),j(\"text\",Z,\"textord\",ut,mt),j(\"math\",Z,Q,ut,mt=String.fromCharCode(55349,56476+ct)),j(\"text\",Z,\"textord\",ut,mt))}j(\"math\",Z,Q,\"k\",mt=String.fromCharCode(55349,56668)),j(\"text\",Z,\"textord\",\"k\",mt);for(var pt=0;pt<10;pt++){var dt=pt.toString();j(\"math\",Z,Q,dt,mt=String.fromCharCode(55349,57294+pt)),j(\"text\",Z,\"textord\",dt,mt),j(\"math\",Z,Q,dt,mt=String.fromCharCode(55349,57314+pt)),j(\"text\",Z,\"textord\",dt,mt),j(\"math\",Z,Q,dt,mt=String.fromCharCode(55349,57324+pt)),j(\"text\",Z,\"textord\",dt,mt),j(\"math\",Z,Q,dt,mt=String.fromCharCode(55349,57334+pt)),j(\"text\",Z,\"textord\",dt,mt)}for(var ft=0;ft<\"ÇÐÞçþ\".length;ft++){var gt=\"ÇÐÞçþ\".charAt(ft);j(\"math\",Z,Q,gt,gt),j(\"text\",Z,\"textord\",gt,gt)}j(\"text\",Z,\"textord\",\"ð\",\"ð\"),j(\"text\",Z,\"textord\",\"–\",\"–\"),j(\"text\",Z,\"textord\",\"—\",\"—\"),j(\"text\",Z,\"textord\",\"‘\",\"‘\"),j(\"text\",Z,\"textord\",\"’\",\"’\"),j(\"text\",Z,\"textord\",\"“\",\"“\"),j(\"text\",Z,\"textord\",\"”\",\"”\");var xt=[[\"mathbf\",\"textbf\",\"Main-Bold\"],[\"mathbf\",\"textbf\",\"Main-Bold\"],[\"mathdefault\",\"textit\",\"Math-Italic\"],[\"mathdefault\",\"textit\",\"Math-Italic\"],[\"boldsymbol\",\"boldsymbol\",\"Main-BoldItalic\"],[\"boldsymbol\",\"boldsymbol\",\"Main-BoldItalic\"],[\"mathscr\",\"textscr\",\"Script-Regular\"],[\"\",\"\",\"\"],[\"\",\"\",\"\"],[\"\",\"\",\"\"],[\"mathfrak\",\"textfrak\",\"Fraktur-Regular\"],[\"mathfrak\",\"textfrak\",\"Fraktur-Regular\"],[\"mathbb\",\"textbb\",\"AMS-Regular\"],[\"mathbb\",\"textbb\",\"AMS-Regular\"],[\"\",\"\",\"\"],[\"\",\"\",\"\"],[\"mathsf\",\"textsf\",\"SansSerif-Regular\"],[\"mathsf\",\"textsf\",\"SansSerif-Regular\"],[\"mathboldsf\",\"textboldsf\",\"SansSerif-Bold\"],[\"mathboldsf\",\"textboldsf\",\"SansSerif-Bold\"],[\"mathitsf\",\"textitsf\",\"SansSerif-Italic\"],[\"mathitsf\",\"textitsf\",\"SansSerif-Italic\"],[\"\",\"\",\"\"],[\"\",\"\",\"\"],[\"mathtt\",\"texttt\",\"Typewriter-Regular\"],[\"mathtt\",\"texttt\",\"Typewriter-Regular\"]],vt=[[\"mathbf\",\"textbf\",\"Main-Bold\"],[\"\",\"\",\"\"],[\"mathsf\",\"textsf\",\"SansSerif-Regular\"],[\"mathboldsf\",\"textboldsf\",\"SansSerif-Bold\"],[\"mathtt\",\"texttt\",\"Typewriter-Regular\"]],bt=[[1,1,1],[2,1,1],[3,1,1],[4,2,1],[5,2,1],[6,3,1],[7,4,2],[8,6,3],[9,7,6],[10,8,7],[11,10,9]],yt=[.5,.6,.7,.8,.9,1,1.2,1.44,1.728,2.074,2.488],wt=function(t,e){return e.size<2?t:bt[t-1][e.size-1]},kt=function(){function t(e){this.style=void 0,this.color=void 0,this.size=void 0,this.textSize=void 0,this.phantom=void 0,this.font=void 0,this.fontFamily=void 0,this.fontWeight=void 0,this.fontShape=void 0,this.sizeMultiplier=void 0,this.maxSize=void 0,this.minRuleThickness=void 0,this._fontMetrics=void 0,this.style=e.style,this.color=e.color,this.size=e.size||t.BASESIZE,this.textSize=e.textSize||this.size,this.phantom=!!e.phantom,this.font=e.font||\"\",this.fontFamily=e.fontFamily||\"\",this.fontWeight=e.fontWeight||\"\",this.fontShape=e.fontShape||\"\",this.sizeMultiplier=yt[this.size-1],this.maxSize=e.maxSize,this.minRuleThickness=e.minRuleThickness,this._fontMetrics=void 0}var e=t.prototype;return e.extend=function(e){var r={style:this.style,size:this.size,textSize:this.textSize,color:this.color,phantom:this.phantom,font:this.font,fontFamily:this.fontFamily,fontWeight:this.fontWeight,fontShape:this.fontShape,maxSize:this.maxSize,minRuleThickness:this.minRuleThickness};for(var a in e)e.hasOwnProperty(a)&&(r[a]=e[a]);return new t(r)},e.havingStyle=function(t){return this.style===t?this:this.extend({style:t,size:wt(this.textSize,t)})},e.havingCrampedStyle=function(){return this.havingStyle(this.style.cramp())},e.havingSize=function(t){return this.size===t&&this.textSize===t?this:this.extend({style:this.style.text(),size:t,textSize:t,sizeMultiplier:yt[t-1]})},e.havingBaseStyle=function(e){e=e||this.style.text();var r=wt(t.BASESIZE,e);return this.size===r&&this.textSize===t.BASESIZE&&this.style===e?this:this.extend({style:e,size:r})},e.havingBaseSizing=function(){var t;switch(this.style.id){case 4:case 5:t=3;break;case 6:case 7:t=1;break;default:t=6}return this.extend({style:this.style.text(),size:t})},e.withColor=function(t){return this.extend({color:t})},e.withPhantom=function(){return this.extend({phantom:!0})},e.withFont=function(t){return this.extend({font:t})},e.withTextFontFamily=function(t){return this.extend({fontFamily:t,font:\"\"})},e.withTextFontWeight=function(t){return this.extend({fontWeight:t,font:\"\"})},e.withTextFontShape=function(t){return this.extend({fontShape:t,font:\"\"})},e.sizingClasses=function(t){return t.size!==this.size?[\"sizing\",\"reset-size\"+t.size,\"size\"+this.size]:[]},e.baseSizingClasses=function(){return this.size!==t.BASESIZE?[\"sizing\",\"reset-size\"+this.size,\"size\"+t.BASESIZE]:[]},e.fontMetrics=function(){return this._fontMetrics||(this._fontMetrics=function(t){var e;if(!Y[e=t>=5?0:t>=3?1:2]){var r=Y[e]={cssEmPerMu:V.quad[e]/18};for(var a in V)V.hasOwnProperty(a)&&(r[a]=V[a][e])}return Y[e]}(this.size)),this._fontMetrics},e.getColor=function(){return this.phantom?\"transparent\":this.color},t}();kt.BASESIZE=6;var St=kt,Mt={pt:1,mm:7227/2540,cm:7227/254,in:72.27,bp:1.00375,pc:12,dd:1238/1157,cc:14856/1157,nd:685/642,nc:1370/107,sp:1/65536,px:1.00375},zt={ex:!0,em:!0,mu:!0},At=function(t){return\"string\"!=typeof t&&(t=t.unit),t in Mt||t in zt||\"ex\"===t},Tt=function(t,e){var r;if(t.unit in Mt)r=Mt[t.unit]/e.fontMetrics().ptPerEm/e.sizeMultiplier;else if(\"mu\"===t.unit)r=e.fontMetrics().cssEmPerMu;else{var a;if(a=e.style.isTight()?e.havingStyle(e.style.text()):e,\"ex\"===t.unit)r=a.fontMetrics().xHeight;else{if(\"em\"!==t.unit)throw new o(\"Invalid unit: '\"+t.unit+\"'\");r=a.fontMetrics().quad}a!==e&&(r*=a.sizeMultiplier/e.sizeMultiplier)}return Math.min(t.number*r,e.maxSize)},Bt=[\"\\\\imath\",\"ı\",\"\\\\jmath\",\"ȷ\",\"\\\\pounds\",\"\\\\mathsterling\",\"\\\\textsterling\",\"£\"],Ct=function(t,e,r){return $[r][t]&&$[r][t].replace&&(t=$[r][t].replace),{value:t,metrics:G(t,e,r)}},qt=function(t,e,r,a,n){var i,o=Ct(t,e,r),s=o.metrics;if(t=o.value,s){var h=s.italic;(\"text\"===r||a&&\"mathit\"===a.font)&&(h=0),i=new E(t,s.height,s.depth,h,s.skew,s.width,n)}else\"undefined\"!=typeof console&&console.warn(\"No character metrics for '\"+t+\"' in style '\"+e+\"' and mode '\"+r+\"'\"),i=new E(t,0,0,0,0,0,n);if(a){i.maxFontSize=a.sizeMultiplier,a.style.isTight()&&i.classes.push(\"mtight\");var l=a.getColor();l&&(i.style.color=l)}return i},Nt=function(t,e){if(T(t.classes)!==T(e.classes)||t.skew!==e.skew||t.maxFontSize!==e.maxFontSize)return!1;for(var r in t.style)if(t.style.hasOwnProperty(r)&&t.style[r]!==e.style[r])return!1;for(var a in e.style)if(e.style.hasOwnProperty(a)&&t.style[a]!==e.style[a])return!1;return!0},Ot=function(t){for(var e=0,r=0,a=0,n=0;n<t.children.length;n++){var i=t.children[n];i.height>e&&(e=i.height),i.depth>r&&(r=i.depth),i.maxFontSize>a&&(a=i.maxFontSize)}t.height=e,t.depth=r,t.maxFontSize=a},It=function(t,e,r,a){var n=new N(t,e,r,a);return Ot(n),n},Rt=function(t,e,r,a){return new N(t,e,r,a)},Et=function(t){var e=new A(t);return Ot(e),e},Lt=function(t,e,r){var a=\"\";switch(t){case\"amsrm\":a=\"AMS\";break;case\"textrm\":a=\"Main\";break;case\"textsf\":a=\"SansSerif\";break;case\"texttt\":a=\"Typewriter\";break;default:a=t}return a+\"-\"+(\"textbf\"===e&&\"textit\"===r?\"BoldItalic\":\"textbf\"===e?\"Bold\":\"textit\"===e?\"Italic\":\"Regular\")},Pt={mathbf:{variant:\"bold\",fontName:\"Main-Bold\"},mathrm:{variant:\"normal\",fontName:\"Main-Regular\"},textit:{variant:\"italic\",fontName:\"Main-Italic\"},mathit:{variant:\"italic\",fontName:\"Main-Italic\"},mathbb:{variant:\"double-struck\",fontName:\"AMS-Regular\"},mathcal:{variant:\"script\",fontName:\"Caligraphic-Regular\"},mathfrak:{variant:\"fraktur\",fontName:\"Fraktur-Regular\"},mathscr:{variant:\"script\",fontName:\"Script-Regular\"},mathsf:{variant:\"sans-serif\",fontName:\"SansSerif-Regular\"},mathtt:{variant:\"monospace\",fontName:\"Typewriter-Regular\"}},Ht={vec:[\"vec\",.471,.714],oiintSize1:[\"oiintSize1\",.957,.499],oiintSize2:[\"oiintSize2\",1.472,.659],oiiintSize1:[\"oiiintSize1\",1.304,.499],oiiintSize2:[\"oiiintSize2\",1.98,.659]},Dt={fontMap:Pt,makeSymbol:qt,mathsym:function(t,e,r,a){return void 0===a&&(a=[]),\"boldsymbol\"===r.font&&Ct(t,\"Main-Bold\",e).metrics?qt(t,\"Main-Bold\",e,r,a.concat([\"mathbf\"])):\"\\\\\"===t||\"main\"===$[e][t].font?qt(t,\"Main-Regular\",e,r,a):qt(t,\"AMS-Regular\",e,r,a.concat([\"amsrm\"]))},makeSpan:It,makeSvgSpan:Rt,makeLineSpan:function(t,e,r){var a=It([t],[],e);return a.height=Math.max(r||e.fontMetrics().defaultRuleThickness,e.minRuleThickness),a.style.borderBottomWidth=a.height+\"em\",a.maxFontSize=1,a},makeAnchor:function(t,e,r,a){var n=new O(t,e,r,a);return Ot(n),n},makeFragment:Et,wrapFragment:function(t,e){return t instanceof A?It([],[t],e):t},makeVList:function(t,e){for(var r=function(t){if(\"individualShift\"===t.positionType){for(var e=t.children,r=[e[0]],a=-e[0].shift-e[0].elem.depth,n=a,i=1;i<e.length;i++){var o=-e[i].shift-n-e[i].elem.depth,s=o-(e[i-1].elem.height+e[i-1].elem.depth);n+=o,r.push({type:\"kern\",size:s}),r.push(e[i])}return{children:r,depth:a}}var h;if(\"top\"===t.positionType){for(var l=t.positionData,m=0;m<t.children.length;m++){var c=t.children[m];l-=\"kern\"===c.type?c.size:c.elem.height+c.elem.depth}h=l}else if(\"bottom\"===t.positionType)h=-t.positionData;else{var u=t.children[0];if(\"elem\"!==u.type)throw new Error('First child must have type \"elem\".');if(\"shift\"===t.positionType)h=-u.elem.depth-t.positionData;else{if(\"firstBaseline\"!==t.positionType)throw new Error(\"Invalid positionType \"+t.positionType+\".\");h=-u.elem.depth}}return{children:t.children,depth:h}}(t),a=r.children,n=r.depth,i=0,o=0;o<a.length;o++){var s=a[o];if(\"elem\"===s.type){var h=s.elem;i=Math.max(i,h.maxFontSize,h.height)}}i+=2;var l=It([\"pstrut\"],[]);l.style.height=i+\"em\";for(var m=[],c=n,u=n,p=n,d=0;d<a.length;d++){var f=a[d];if(\"kern\"===f.type)p+=f.size;else{var g=f.elem,x=f.wrapperClasses||[],v=f.wrapperStyle||{},b=It(x,[l,g],void 0,v);b.style.top=-i-p-g.depth+\"em\",f.marginLeft&&(b.style.marginLeft=f.marginLeft),f.marginRight&&(b.style.marginRight=f.marginRight),m.push(b),p+=g.height+g.depth}c=Math.min(c,p),u=Math.max(u,p)}var y,w=It([\"vlist\"],m);if(w.style.height=u+\"em\",c<0){var k=It([],[]),S=It([\"vlist\"],[k]);S.style.height=-c+\"em\";var M=It([\"vlist-s\"],[new E(\"​\")]);y=[It([\"vlist-r\"],[w,M]),It([\"vlist-r\"],[S])]}else y=[It([\"vlist-r\"],[w])];var z=It([\"vlist-t\"],y);return 2===y.length&&z.classes.push(\"vlist-t2\"),z.height=u,z.depth=-c,z},makeOrd:function(t,e,r){var a,n=t.mode,i=t.text,s=[\"mord\"],h=\"math\"===n||\"text\"===n&&e.font,l=h?e.font:e.fontFamily;if(55349===i.charCodeAt(0)){var m=function(t,e){var r=1024*(t.charCodeAt(0)-55296)+(t.charCodeAt(1)-56320)+65536,a=\"math\"===e?0:1;if(119808<=r&&r<120484){var n=Math.floor((r-119808)/26);return[xt[n][2],xt[n][a]]}if(120782<=r&&r<=120831){var i=Math.floor((r-120782)/10);return[vt[i][2],vt[i][a]]}if(120485===r||120486===r)return[xt[0][2],xt[0][a]];if(120486<r&&r<120782)return[\"\",\"\"];throw new o(\"Unsupported character: \"+t)}(i,n),u=m[0],p=m[1];return qt(i,u,n,e,s.concat(p))}if(l){var d,f;if(\"boldsymbol\"===l||\"mathnormal\"===l){var g=\"boldsymbol\"===l?function(t,e,r,a){return Ct(t,\"Math-BoldItalic\",e).metrics?{fontName:\"Math-BoldItalic\",fontClass:\"boldsymbol\"}:{fontName:\"Main-Bold\",fontClass:\"mathbf\"}}(i,n):(a=i,c.contains(Bt,a)?{fontName:\"Main-Italic\",fontClass:\"mathit\"}:/[0-9]/.test(a.charAt(0))?{fontName:\"Caligraphic-Regular\",fontClass:\"mathcal\"}:{fontName:\"Math-Italic\",fontClass:\"mathdefault\"});d=g.fontName,f=[g.fontClass]}else c.contains(Bt,i)?(d=\"Main-Italic\",f=[\"mathit\"]):h?(d=Pt[l].fontName,f=[l]):(d=Lt(l,e.fontWeight,e.fontShape),f=[l,e.fontWeight,e.fontShape]);if(Ct(i,d,n).metrics)return qt(i,d,n,e,s.concat(f));if(rt.hasOwnProperty(i)&&\"Typewriter\"===d.substr(0,10)){for(var x=[],v=0;v<i.length;v++)x.push(qt(i[v],d,n,e,s.concat(f)));return Et(x)}}if(\"mathord\"===r){var b=function(t,e,r,a){return/[0-9]/.test(t.charAt(0))||c.contains(Bt,t)?{fontName:\"Main-Italic\",fontClass:\"mathit\"}:{fontName:\"Math-Italic\",fontClass:\"mathdefault\"}}(i);return qt(i,b.fontName,n,e,s.concat([b.fontClass]))}if(\"textord\"===r){var y=$[n][i]&&$[n][i].font;if(\"ams\"===y){var w=Lt(\"amsrm\",e.fontWeight,e.fontShape);return qt(i,w,n,e,s.concat(\"amsrm\",e.fontWeight,e.fontShape))}if(\"main\"!==y&&y){var k=Lt(y,e.fontWeight,e.fontShape);return qt(i,k,n,e,s.concat(k,e.fontWeight,e.fontShape))}var S=Lt(\"textrm\",e.fontWeight,e.fontShape);return qt(i,S,n,e,s.concat(e.fontWeight,e.fontShape))}throw new Error(\"unexpected type: \"+r+\" in makeOrd\")},makeGlue:function(t,e){var r=It([\"mspace\"],[],e),a=Tt(t,e);return r.style.marginRight=a+\"em\",r},staticSvg:function(t,e){var r=Ht[t],a=r[0],n=r[1],i=r[2],o=new P(a),s=new L([o],{width:n+\"em\",height:i+\"em\",style:\"width:\"+n+\"em\",viewBox:\"0 0 \"+1e3*n+\" \"+1e3*i,preserveAspectRatio:\"xMinYMin\"}),h=Rt([\"overlay\"],[s],e);return h.height=i,h.style.height=i+\"em\",h.style.width=n+\"em\",h},svgData:Ht,tryCombineChars:function(t){for(var e=0;e<t.length-1;e++){var r=t[e],a=t[e+1];r instanceof E&&a instanceof E&&Nt(r,a)&&(r.text+=a.text,r.height=Math.max(r.height,a.height),r.depth=Math.max(r.depth,a.depth),r.italic=a.italic,t.splice(e+1,1),e--)}return t}};function Ft(t,e){var r=Vt(t,e);if(!r)throw new Error(\"Expected node of type \"+e+\", but got \"+(t?\"node of type \"+t.type:String(t)));return r}function Vt(t,e){return t&&t.type===e?t:null}function Ut(t,e){var r=function(t,e){return t&&\"atom\"===t.type&&t.family===e?t:null}(t,e);if(!r)throw new Error('Expected node of type \"atom\" and family \"'+e+'\", but got '+(t?\"atom\"===t.type?\"atom of family \"+t.family:\"node of type \"+t.type:String(t)));return r}function Gt(t){var e=Yt(t);if(!e)throw new Error(\"Expected node of symbol group type, but got \"+(t?\"node of type \"+t.type:String(t)));return e}function Yt(t){return t&&(\"atom\"===t.type||W.hasOwnProperty(t.type))?t:null}var _t={number:3,unit:\"mu\"},Wt={number:4,unit:\"mu\"},Xt={number:5,unit:\"mu\"},$t={mord:{mop:_t,mbin:Wt,mrel:Xt,minner:_t},mop:{mord:_t,mop:_t,mrel:Xt,minner:_t},mbin:{mord:Wt,mop:Wt,mopen:Wt,minner:Wt},mrel:{mord:Xt,mop:Xt,mopen:Xt,minner:Xt},mopen:{},mclose:{mop:_t,mbin:Wt,mrel:Xt,minner:_t},mpunct:{mord:_t,mop:_t,mrel:Xt,mopen:_t,mclose:_t,mpunct:_t,minner:_t},minner:{mord:_t,mop:_t,mbin:Wt,mrel:Xt,mopen:_t,mpunct:_t,minner:_t}},jt={mord:{mop:_t},mop:{mord:_t,mop:_t},mbin:{},mrel:{},mopen:{},mclose:{mop:_t},mpunct:{},minner:{mop:_t}},Zt={},Kt={},Jt={};function Qt(t){for(var e=t.type,r=t.names,a=t.props,n=t.handler,i=t.htmlBuilder,o=t.mathmlBuilder,s={type:e,numArgs:a.numArgs,argTypes:a.argTypes,greediness:void 0===a.greediness?1:a.greediness,allowedInText:!!a.allowedInText,allowedInMath:void 0===a.allowedInMath||a.allowedInMath,numOptionalArgs:a.numOptionalArgs||0,infix:!!a.infix,handler:n},h=0;h<r.length;++h)Zt[r[h]]=s;e&&(i&&(Kt[e]=i),o&&(Jt[e]=o))}function te(t){Qt({type:t.type,names:[],props:{numArgs:0},handler:function(){throw new Error(\"Should never be called.\")},htmlBuilder:t.htmlBuilder,mathmlBuilder:t.mathmlBuilder})}var ee=function(t){var e=Vt(t,\"ordgroup\");return e?e.body:[t]},re=Dt.makeSpan,ae=[\"leftmost\",\"mbin\",\"mopen\",\"mrel\",\"mop\",\"mpunct\"],ne=[\"rightmost\",\"mrel\",\"mclose\",\"mpunct\"],ie={display:w.DISPLAY,text:w.TEXT,script:w.SCRIPT,scriptscript:w.SCRIPTSCRIPT},oe={mord:\"mord\",mop:\"mop\",mbin:\"mbin\",mrel:\"mrel\",mopen:\"mopen\",mclose:\"mclose\",mpunct:\"mpunct\",minner:\"minner\"},se=function(t,e,r,a){void 0===a&&(a=[null,null]);for(var n=[],i=0;i<t.length;i++){var o=ue(t[i],e);if(o instanceof A){var s=o.children;n.push.apply(n,s)}else n.push(o)}if(!r)return n;var h=e;if(1===t.length){var l=Vt(t[0],\"sizing\")||Vt(t[0],\"styling\");l&&(\"sizing\"===l.type?h=e.havingSize(l.size):\"styling\"===l.type&&(h=e.havingStyle(ie[l.style])))}var m=re([a[0]||\"leftmost\"],[],e),u=re([a[1]||\"rightmost\"],[],e);return he(n,(function(t,e){var r=e.classes[0],a=t.classes[0];\"mbin\"===r&&c.contains(ne,a)?e.classes[0]=\"mord\":\"mbin\"===a&&c.contains(ae,r)&&(t.classes[0]=\"mord\")}),{node:m},u),he(n,(function(t,e){var r=me(e),a=me(t),n=r&&a?t.hasClass(\"mtight\")?jt[r][a]:$t[r][a]:null;if(n)return Dt.makeGlue(n,h)}),{node:m},u),n},he=function t(e,r,a,n){n&&e.push(n);for(var i=0;i<e.length;i++){var o=e[i],s=le(o);if(s)t(s.children,r,a);else if(\"mspace\"!==o.classes[0]){var h=r(o,a.node);h&&(a.insertAfter?a.insertAfter(h):(e.unshift(h),i++)),a.node=o,a.insertAfter=function(t){return function(r){e.splice(t+1,0,r),i++}}(i)}}n&&e.pop()},le=function(t){return t instanceof A||t instanceof O?t:null},me=function(t,e){return t?(e&&(t=function t(e,r){var a=le(e);if(a){var n=a.children;if(n.length){if(\"right\"===r)return t(n[n.length-1],\"right\");if(\"left\"===r)return t(n[0],\"left\")}}return e}(t,e)),oe[t.classes[0]]||null):null},ce=function(t,e){var r=[\"nulldelimiter\"].concat(t.baseSizingClasses());return re(e.concat(r))},ue=function(t,e,r){if(!t)return re();if(Kt[t.type]){var a=Kt[t.type](t,e);if(r&&e.size!==r.size){a=re(e.sizingClasses(r),[a],e);var n=e.sizeMultiplier/r.sizeMultiplier;a.height*=n,a.depth*=n}return a}throw new o(\"Got group of unknown type: '\"+t.type+\"'\")};function pe(t,e){var r=re([\"base\"],t,e),a=re([\"strut\"]);return a.style.height=r.height+r.depth+\"em\",a.style.verticalAlign=-r.depth+\"em\",r.children.unshift(a),r}function de(t,e){var r=null;1===t.length&&\"tag\"===t[0].type&&(r=t[0].tag,t=t[0].body);for(var a,n=se(t,e,!0),i=[],o=[],s=0;s<n.length;s++)if(o.push(n[s]),n[s].hasClass(\"mbin\")||n[s].hasClass(\"mrel\")||n[s].hasClass(\"allowbreak\")){for(var h=!1;s<n.length-1&&n[s+1].hasClass(\"mspace\")&&!n[s+1].hasClass(\"newline\");)s++,o.push(n[s]),n[s].hasClass(\"nobreak\")&&(h=!0);h||(i.push(pe(o,e)),o=[])}else n[s].hasClass(\"newline\")&&(o.pop(),o.length>0&&(i.push(pe(o,e)),o=[]),i.push(n[s]));o.length>0&&i.push(pe(o,e)),r&&((a=pe(se(r,e,!0))).classes=[\"tag\"],i.push(a));var l=re([\"katex-html\"],i);if(l.setAttribute(\"aria-hidden\",\"true\"),a){var m=a.children[0];m.style.height=l.height+l.depth+\"em\",m.style.verticalAlign=-l.depth+\"em\"}return l}function fe(t){return new A(t)}var ge=function(){function t(t,e){this.type=void 0,this.attributes=void 0,this.children=void 0,this.type=t,this.attributes={},this.children=e||[]}var e=t.prototype;return e.setAttribute=function(t,e){this.attributes[t]=e},e.getAttribute=function(t){return this.attributes[t]},e.toNode=function(){var t=document.createElementNS(\"http://www.w3.org/1998/Math/MathML\",this.type);for(var e in this.attributes)Object.prototype.hasOwnProperty.call(this.attributes,e)&&t.setAttribute(e,this.attributes[e]);for(var r=0;r<this.children.length;r++)t.appendChild(this.children[r].toNode());return t},e.toMarkup=function(){var t=\"<\"+this.type;for(var e in this.attributes)Object.prototype.hasOwnProperty.call(this.attributes,e)&&(t+=\" \"+e+'=\"',t+=c.escape(this.attributes[e]),t+='\"');t+=\">\";for(var r=0;r<this.children.length;r++)t+=this.children[r].toMarkup();return t+=\"</\"+this.type+\">\"},e.toText=function(){return this.children.map((function(t){return t.toText()})).join(\"\")},t}(),xe=function(){function t(t){this.text=void 0,this.text=t}var e=t.prototype;return e.toNode=function(){return document.createTextNode(this.text)},e.toMarkup=function(){return c.escape(this.toText())},e.toText=function(){return this.text},t}(),ve={MathNode:ge,TextNode:xe,SpaceNode:function(){function t(t){this.width=void 0,this.character=void 0,this.width=t,this.character=t>=.05555&&t<=.05556?\" \":t>=.1666&&t<=.1667?\" \":t>=.2222&&t<=.2223?\" \":t>=.2777&&t<=.2778?\"  \":t>=-.05556&&t<=-.05555?\" ⁣\":t>=-.1667&&t<=-.1666?\" ⁣\":t>=-.2223&&t<=-.2222?\" ⁣\":t>=-.2778&&t<=-.2777?\" ⁣\":null}var e=t.prototype;return e.toNode=function(){if(this.character)return document.createTextNode(this.character);var t=document.createElementNS(\"http://www.w3.org/1998/Math/MathML\",\"mspace\");return t.setAttribute(\"width\",this.width+\"em\"),t},e.toMarkup=function(){return this.character?\"<mtext>\"+this.character+\"</mtext>\":'<mspace width=\"'+this.width+'em\"/>'},e.toText=function(){return this.character?this.character:\" \"},t}(),newDocumentFragment:fe},be=function(t,e,r){return!$[e][t]||!$[e][t].replace||55349===t.charCodeAt(0)||rt.hasOwnProperty(t)&&r&&(r.fontFamily&&\"tt\"===r.fontFamily.substr(4,2)||r.font&&\"tt\"===r.font.substr(4,2))||(t=$[e][t].replace),new ve.TextNode(t)},ye=function(t){return 1===t.length?t[0]:new ve.MathNode(\"mrow\",t)},we=function(t,e){if(\"texttt\"===e.fontFamily)return\"monospace\";if(\"textsf\"===e.fontFamily)return\"textit\"===e.fontShape&&\"textbf\"===e.fontWeight?\"sans-serif-bold-italic\":\"textit\"===e.fontShape?\"sans-serif-italic\":\"textbf\"===e.fontWeight?\"bold-sans-serif\":\"sans-serif\";if(\"textit\"===e.fontShape&&\"textbf\"===e.fontWeight)return\"bold-italic\";if(\"textit\"===e.fontShape)return\"italic\";if(\"textbf\"===e.fontWeight)return\"bold\";var r=e.font;if(!r||\"mathnormal\"===r)return null;var a=t.mode;if(\"mathit\"===r)return\"italic\";if(\"boldsymbol\"===r)return\"bold-italic\";if(\"mathbf\"===r)return\"bold\";if(\"mathbb\"===r)return\"double-struck\";if(\"mathfrak\"===r)return\"fraktur\";if(\"mathscr\"===r||\"mathcal\"===r)return\"script\";if(\"mathsf\"===r)return\"sans-serif\";if(\"mathtt\"===r)return\"monospace\";var n=t.text;return c.contains([\"\\\\imath\",\"\\\\jmath\"],n)?null:($[a][n]&&$[a][n].replace&&(n=$[a][n].replace),G(n,Dt.fontMap[r].fontName,a)?Dt.fontMap[r].variant:null)},ke=function(t,e,r){if(1===t.length){var a=Me(t[0],e);return r&&a instanceof ge&&\"mo\"===a.type&&(a.setAttribute(\"lspace\",\"0em\"),a.setAttribute(\"rspace\",\"0em\")),[a]}for(var n,i=[],o=0;o<t.length;o++){var s=Me(t[o],e);if(s instanceof ge&&n instanceof ge){if(\"mtext\"===s.type&&\"mtext\"===n.type&&s.getAttribute(\"mathvariant\")===n.getAttribute(\"mathvariant\")){var h;(h=n.children).push.apply(h,s.children);continue}if(\"mn\"===s.type&&\"mn\"===n.type){var l;(l=n.children).push.apply(l,s.children);continue}if(\"mi\"===s.type&&1===s.children.length&&\"mn\"===n.type){var m=s.children[0];if(m instanceof xe&&\".\"===m.text){var c;(c=n.children).push.apply(c,s.children);continue}}else if(\"mi\"===n.type&&1===n.children.length){var u=n.children[0];if(u instanceof xe&&\"̸\"===u.text&&(\"mo\"===s.type||\"mi\"===s.type||\"mn\"===s.type)){var p=s.children[0];p instanceof xe&&p.text.length>0&&(p.text=p.text.slice(0,1)+\"̸\"+p.text.slice(1),i.pop())}}}i.push(s),n=s}return i},Se=function(t,e,r){return ye(ke(t,e,r))},Me=function(t,e){if(!t)return new ve.MathNode(\"mrow\");if(Jt[t.type])return Jt[t.type](t,e);throw new o(\"Got group of unknown type: '\"+t.type+\"'\")};function ze(t,e,r,a){var n,i=ke(t,r);n=1===i.length&&i[0]instanceof ge&&c.contains([\"mrow\",\"mtable\"],i[0].type)?i[0]:new ve.MathNode(\"mrow\",i);var o=new ve.MathNode(\"annotation\",[new ve.TextNode(e)]);o.setAttribute(\"encoding\",\"application/x-tex\");var s=new ve.MathNode(\"semantics\",[n,o]),h=new ve.MathNode(\"math\",[s]);h.setAttribute(\"xmlns\",\"http://www.w3.org/1998/Math/MathML\");var l=a?\"katex\":\"katex-mathml\";return Dt.makeSpan([l],[h])}var Ae=function(t){return new St({style:t.displayMode?w.DISPLAY:w.TEXT,maxSize:t.maxSize,minRuleThickness:t.minRuleThickness})},Te=function(t,e){if(e.displayMode){var r=[\"katex-display\"];e.leqno&&r.push(\"leqno\"),e.fleqn&&r.push(\"fleqn\"),t=Dt.makeSpan(r,[t])}return t},Be=function(t,e,r){var a,n=Ae(r);if(\"mathml\"===r.output)return ze(t,e,n,!0);if(\"html\"===r.output){var i=de(t,n);a=Dt.makeSpan([\"katex\"],[i])}else{var o=ze(t,e,n,!1),s=de(t,n);a=Dt.makeSpan([\"katex\"],[o,s])}return Te(a,r)},Ce={widehat:\"^\",widecheck:\"ˇ\",widetilde:\"~\",utilde:\"~\",overleftarrow:\"←\",underleftarrow:\"←\",xleftarrow:\"←\",overrightarrow:\"→\",underrightarrow:\"→\",xrightarrow:\"→\",underbrace:\"⏟\",overbrace:\"⏞\",overgroup:\"⏠\",undergroup:\"⏡\",overleftrightarrow:\"↔\",underleftrightarrow:\"↔\",xleftrightarrow:\"↔\",Overrightarrow:\"⇒\",xRightarrow:\"⇒\",overleftharpoon:\"↼\",xleftharpoonup:\"↼\",overrightharpoon:\"⇀\",xrightharpoonup:\"⇀\",xLeftarrow:\"⇐\",xLeftrightarrow:\"⇔\",xhookleftarrow:\"↩\",xhookrightarrow:\"↪\",xmapsto:\"↦\",xrightharpoondown:\"⇁\",xleftharpoondown:\"↽\",xrightleftharpoons:\"⇌\",xleftrightharpoons:\"⇋\",xtwoheadleftarrow:\"↞\",xtwoheadrightarrow:\"↠\",xlongequal:\"=\",xtofrom:\"⇄\",xrightleftarrows:\"⇄\",xrightequilibrium:\"⇌\",xleftequilibrium:\"⇋\"},qe={overrightarrow:[[\"rightarrow\"],.888,522,\"xMaxYMin\"],overleftarrow:[[\"leftarrow\"],.888,522,\"xMinYMin\"],underrightarrow:[[\"rightarrow\"],.888,522,\"xMaxYMin\"],underleftarrow:[[\"leftarrow\"],.888,522,\"xMinYMin\"],xrightarrow:[[\"rightarrow\"],1.469,522,\"xMaxYMin\"],xleftarrow:[[\"leftarrow\"],1.469,522,\"xMinYMin\"],Overrightarrow:[[\"doublerightarrow\"],.888,560,\"xMaxYMin\"],xRightarrow:[[\"doublerightarrow\"],1.526,560,\"xMaxYMin\"],xLeftarrow:[[\"doubleleftarrow\"],1.526,560,\"xMinYMin\"],overleftharpoon:[[\"leftharpoon\"],.888,522,\"xMinYMin\"],xleftharpoonup:[[\"leftharpoon\"],.888,522,\"xMinYMin\"],xleftharpoondown:[[\"leftharpoondown\"],.888,522,\"xMinYMin\"],overrightharpoon:[[\"rightharpoon\"],.888,522,\"xMaxYMin\"],xrightharpoonup:[[\"rightharpoon\"],.888,522,\"xMaxYMin\"],xrightharpoondown:[[\"rightharpoondown\"],.888,522,\"xMaxYMin\"],xlongequal:[[\"longequal\"],.888,334,\"xMinYMin\"],xtwoheadleftarrow:[[\"twoheadleftarrow\"],.888,334,\"xMinYMin\"],xtwoheadrightarrow:[[\"twoheadrightarrow\"],.888,334,\"xMaxYMin\"],overleftrightarrow:[[\"leftarrow\",\"rightarrow\"],.888,522],overbrace:[[\"leftbrace\",\"midbrace\",\"rightbrace\"],1.6,548],underbrace:[[\"leftbraceunder\",\"midbraceunder\",\"rightbraceunder\"],1.6,548],underleftrightarrow:[[\"leftarrow\",\"rightarrow\"],.888,522],xleftrightarrow:[[\"leftarrow\",\"rightarrow\"],1.75,522],xLeftrightarrow:[[\"doubleleftarrow\",\"doublerightarrow\"],1.75,560],xrightleftharpoons:[[\"leftharpoondownplus\",\"rightharpoonplus\"],1.75,716],xleftrightharpoons:[[\"leftharpoonplus\",\"rightharpoondownplus\"],1.75,716],xhookleftarrow:[[\"leftarrow\",\"righthook\"],1.08,522],xhookrightarrow:[[\"lefthook\",\"rightarrow\"],1.08,522],overlinesegment:[[\"leftlinesegment\",\"rightlinesegment\"],.888,522],underlinesegment:[[\"leftlinesegment\",\"rightlinesegment\"],.888,522],overgroup:[[\"leftgroup\",\"rightgroup\"],.888,342],undergroup:[[\"leftgroupunder\",\"rightgroupunder\"],.888,342],xmapsto:[[\"leftmapsto\",\"rightarrow\"],1.5,522],xtofrom:[[\"leftToFrom\",\"rightToFrom\"],1.75,528],xrightleftarrows:[[\"baraboveleftarrow\",\"rightarrowabovebar\"],1.75,901],xrightequilibrium:[[\"baraboveshortleftharpoon\",\"rightharpoonaboveshortbar\"],1.75,716],xleftequilibrium:[[\"shortbaraboveleftharpoon\",\"shortrightharpoonabovebar\"],1.75,716]},Ne=function(t,e,r,a){var n,i=t.height+t.depth+2*r;if(/fbox|color/.test(e)){if(n=Dt.makeSpan([\"stretchy\",e],[],a),\"fbox\"===e){var o=a.color&&a.getColor();o&&(n.style.borderColor=o)}}else{var s=[];/^[bx]cancel$/.test(e)&&s.push(new H({x1:\"0\",y1:\"0\",x2:\"100%\",y2:\"100%\",\"stroke-width\":\"0.046em\"})),/^x?cancel$/.test(e)&&s.push(new H({x1:\"0\",y1:\"100%\",x2:\"100%\",y2:\"0\",\"stroke-width\":\"0.046em\"}));var h=new L(s,{width:\"100%\",height:i+\"em\"});n=Dt.makeSvgSpan([],[h],a)}return n.height=i,n.style.height=i+\"em\",n},Oe=function(t){var e=new ve.MathNode(\"mo\",[new ve.TextNode(Ce[t.substr(1)])]);return e.setAttribute(\"stretchy\",\"true\"),e},Ie=function(t,e){var r=function(){var r=4e5,a=t.label.substr(1);if(c.contains([\"widehat\",\"widecheck\",\"widetilde\",\"utilde\"],a)){var n,i,o,s=\"ordgroup\"===(d=t.base).type?d.body.length:1;if(s>5)\"widehat\"===a||\"widecheck\"===a?(n=420,r=2364,o=.42,i=a+\"4\"):(n=312,r=2340,o=.34,i=\"tilde4\");else{var h=[1,1,2,2,3,3][s];\"widehat\"===a||\"widecheck\"===a?(r=[0,1062,2364,2364,2364][h],n=[0,239,300,360,420][h],o=[0,.24,.3,.3,.36,.42][h],i=a+h):(r=[0,600,1033,2339,2340][h],n=[0,260,286,306,312][h],o=[0,.26,.286,.3,.306,.34][h],i=\"tilde\"+h)}var l=new P(i),m=new L([l],{width:\"100%\",height:o+\"em\",viewBox:\"0 0 \"+r+\" \"+n,preserveAspectRatio:\"none\"});return{span:Dt.makeSvgSpan([],[m],e),minWidth:0,height:o}}var u,p,d,f=[],g=qe[a],x=g[0],v=g[1],b=g[2],y=b/1e3,w=x.length;if(1===w)u=[\"hide-tail\"],p=[g[3]];else if(2===w)u=[\"halfarrow-left\",\"halfarrow-right\"],p=[\"xMinYMin\",\"xMaxYMin\"];else{if(3!==w)throw new Error(\"Correct katexImagesData or update code here to support\\n                    \"+w+\" children.\");u=[\"brace-left\",\"brace-center\",\"brace-right\"],p=[\"xMinYMin\",\"xMidYMin\",\"xMaxYMin\"]}for(var k=0;k<w;k++){var S=new P(x[k]),M=new L([S],{width:\"400em\",height:y+\"em\",viewBox:\"0 0 \"+r+\" \"+b,preserveAspectRatio:p[k]+\" slice\"}),z=Dt.makeSvgSpan([u[k]],[M],e);if(1===w)return{span:z,minWidth:v,height:y};z.style.height=y+\"em\",f.push(z)}return{span:Dt.makeSpan([\"stretchy\"],f,e),minWidth:v,height:y}}(),a=r.span,n=r.minWidth,i=r.height;return a.height=i,a.style.height=i+\"em\",n>0&&(a.style.minWidth=n+\"em\"),a},Re=function(t,e){var r,a,n,i=Vt(t,\"supsub\");i?(r=(a=Ft(i.base,\"accent\")).base,i.base=r,n=function(t){if(t instanceof N)return t;throw new Error(\"Expected span<HtmlDomNode> but got \"+String(t)+\".\")}(ue(i,e)),i.base=a):r=(a=Ft(t,\"accent\")).base;var o=ue(r,e.havingCrampedStyle()),s=0;if(a.isShifty&&c.isCharacterBox(r)){var h=c.getBaseElem(r);s=D(ue(h,e.havingCrampedStyle())).skew}var l,m=Math.min(o.height,e.fontMetrics().xHeight);if(a.isStretchy)l=Ie(a,e),l=Dt.makeVList({positionType:\"firstBaseline\",children:[{type:\"elem\",elem:o},{type:\"elem\",elem:l,wrapperClasses:[\"svg-align\"],wrapperStyle:s>0?{width:\"calc(100% - \"+2*s+\"em)\",marginLeft:2*s+\"em\"}:void 0}]},e);else{var u,p;\"\\\\vec\"===a.label?(u=Dt.staticSvg(\"vec\",e),p=Dt.svgData.vec[1]):((u=D(u=Dt.makeOrd({mode:a.mode,text:a.label},e,\"textord\"))).italic=0,p=u.width),l=Dt.makeSpan([\"accent-body\"],[u]);var d=\"\\\\textcircled\"===a.label;d&&(l.classes.push(\"accent-full\"),m=o.height);var f=s;d||(f-=p/2),l.style.left=f+\"em\",\"\\\\textcircled\"===a.label&&(l.style.top=\".2em\"),l=Dt.makeVList({positionType:\"firstBaseline\",children:[{type:\"elem\",elem:o},{type:\"kern\",size:-m},{type:\"elem\",elem:l}]},e)}var g=Dt.makeSpan([\"mord\",\"accent\"],[l],e);return n?(n.children[0]=g,n.height=Math.max(g.height,n.height),n.classes[0]=\"mord\",n):g},Ee=function(t,e){var r=t.isStretchy?Oe(t.label):new ve.MathNode(\"mo\",[be(t.label,t.mode)]),a=new ve.MathNode(\"mover\",[Me(t.base,e),r]);return a.setAttribute(\"accent\",\"true\"),a},Le=new RegExp([\"\\\\acute\",\"\\\\grave\",\"\\\\ddot\",\"\\\\tilde\",\"\\\\bar\",\"\\\\breve\",\"\\\\check\",\"\\\\hat\",\"\\\\vec\",\"\\\\dot\",\"\\\\mathring\"].map((function(t){return\"\\\\\"+t})).join(\"|\"));Qt({type:\"accent\",names:[\"\\\\acute\",\"\\\\grave\",\"\\\\ddot\",\"\\\\tilde\",\"\\\\bar\",\"\\\\breve\",\"\\\\check\",\"\\\\hat\",\"\\\\vec\",\"\\\\dot\",\"\\\\mathring\",\"\\\\widecheck\",\"\\\\widehat\",\"\\\\widetilde\",\"\\\\overrightarrow\",\"\\\\overleftarrow\",\"\\\\Overrightarrow\",\"\\\\overleftrightarrow\",\"\\\\overgroup\",\"\\\\overlinesegment\",\"\\\\overleftharpoon\",\"\\\\overrightharpoon\"],props:{numArgs:1},handler:function(t,e){var r=e[0],a=!Le.test(t.funcName),n=!a||\"\\\\widehat\"===t.funcName||\"\\\\widetilde\"===t.funcName||\"\\\\widecheck\"===t.funcName;return{type:\"accent\",mode:t.parser.mode,label:t.funcName,isStretchy:a,isShifty:n,base:r}},htmlBuilder:Re,mathmlBuilder:Ee}),Qt({type:\"accent\",names:[\"\\\\'\",\"\\\\`\",\"\\\\^\",\"\\\\~\",\"\\\\=\",\"\\\\u\",\"\\\\.\",'\\\\\"',\"\\\\r\",\"\\\\H\",\"\\\\v\",\"\\\\textcircled\"],props:{numArgs:1,allowedInText:!0,allowedInMath:!1},handler:function(t,e){var r=e[0];return{type:\"accent\",mode:t.parser.mode,label:t.funcName,isStretchy:!1,isShifty:!0,base:r}},htmlBuilder:Re,mathmlBuilder:Ee}),Qt({type:\"accentUnder\",names:[\"\\\\underleftarrow\",\"\\\\underrightarrow\",\"\\\\underleftrightarrow\",\"\\\\undergroup\",\"\\\\underlinesegment\",\"\\\\utilde\"],props:{numArgs:1},handler:function(t,e){var r=t.parser,a=t.funcName,n=e[0];return{type:\"accentUnder\",mode:r.mode,label:a,base:n}},htmlBuilder:function(t,e){var r=ue(t.base,e),a=Ie(t,e),n=\"\\\\utilde\"===t.label?.12:0,i=Dt.makeVList({positionType:\"bottom\",positionData:a.height+n,children:[{type:\"elem\",elem:a,wrapperClasses:[\"svg-align\"]},{type:\"kern\",size:n},{type:\"elem\",elem:r}]},e);return Dt.makeSpan([\"mord\",\"accentunder\"],[i],e)},mathmlBuilder:function(t,e){var r=Oe(t.label),a=new ve.MathNode(\"munder\",[Me(t.base,e),r]);return a.setAttribute(\"accentunder\",\"true\"),a}});var Pe=function(t){var e=new ve.MathNode(\"mpadded\",t?[t]:[]);return e.setAttribute(\"width\",\"+0.6em\"),e.setAttribute(\"lspace\",\"0.3em\"),e};Qt({type:\"xArrow\",names:[\"\\\\xleftarrow\",\"\\\\xrightarrow\",\"\\\\xLeftarrow\",\"\\\\xRightarrow\",\"\\\\xleftrightarrow\",\"\\\\xLeftrightarrow\",\"\\\\xhookleftarrow\",\"\\\\xhookrightarrow\",\"\\\\xmapsto\",\"\\\\xrightharpoondown\",\"\\\\xrightharpoonup\",\"\\\\xleftharpoondown\",\"\\\\xleftharpoonup\",\"\\\\xrightleftharpoons\",\"\\\\xleftrightharpoons\",\"\\\\xlongequal\",\"\\\\xtwoheadrightarrow\",\"\\\\xtwoheadleftarrow\",\"\\\\xtofrom\",\"\\\\xrightleftarrows\",\"\\\\xrightequilibrium\",\"\\\\xleftequilibrium\"],props:{numArgs:1,numOptionalArgs:1},handler:function(t,e,r){var a=t.parser,n=t.funcName;return{type:\"xArrow\",mode:a.mode,label:n,body:e[0],below:r[0]}},htmlBuilder:function(t,e){var r,a=e.style,n=e.havingStyle(a.sup()),i=Dt.wrapFragment(ue(t.body,n,e),e);i.classes.push(\"x-arrow-pad\"),t.below&&(n=e.havingStyle(a.sub()),(r=Dt.wrapFragment(ue(t.below,n,e),e)).classes.push(\"x-arrow-pad\"));var o,s=Ie(t,e),h=-e.fontMetrics().axisHeight+.5*s.height,l=-e.fontMetrics().axisHeight-.5*s.height-.111;if((i.depth>.25||\"\\\\xleftequilibrium\"===t.label)&&(l-=i.depth),r){var m=-e.fontMetrics().axisHeight+r.height+.5*s.height+.111;o=Dt.makeVList({positionType:\"individualShift\",children:[{type:\"elem\",elem:i,shift:l},{type:\"elem\",elem:s,shift:h},{type:\"elem\",elem:r,shift:m}]},e)}else o=Dt.makeVList({positionType:\"individualShift\",children:[{type:\"elem\",elem:i,shift:l},{type:\"elem\",elem:s,shift:h}]},e);return o.children[0].children[0].children[1].classes.push(\"svg-align\"),Dt.makeSpan([\"mrel\",\"x-arrow\"],[o],e)},mathmlBuilder:function(t,e){var r,a=Oe(t.label);if(t.body){var n=Pe(Me(t.body,e));if(t.below){var i=Pe(Me(t.below,e));r=new ve.MathNode(\"munderover\",[a,i,n])}else r=new ve.MathNode(\"mover\",[a,n])}else if(t.below){var o=Pe(Me(t.below,e));r=new ve.MathNode(\"munder\",[a,o])}else r=Pe(),r=new ve.MathNode(\"mover\",[a,r]);return r}}),Qt({type:\"textord\",names:[\"\\\\@char\"],props:{numArgs:1,allowedInText:!0},handler:function(t,e){for(var r=t.parser,a=Ft(e[0],\"ordgroup\").body,n=\"\",i=0;i<a.length;i++)n+=Ft(a[i],\"textord\").text;var s=parseInt(n);if(isNaN(s))throw new o(\"\\\\@char has non-numeric argument \"+n);return{type:\"textord\",mode:r.mode,text:String.fromCharCode(s)}}});var He=function(t,e){var r=se(t.body,e.withColor(t.color),!1);return Dt.makeFragment(r)},De=function(t,e){var r=ke(t.body,e.withColor(t.color)),a=new ve.MathNode(\"mstyle\",r);return a.setAttribute(\"mathcolor\",t.color),a};Qt({type:\"color\",names:[\"\\\\textcolor\"],props:{numArgs:2,allowedInText:!0,greediness:3,argTypes:[\"color\",\"original\"]},handler:function(t,e){var r=t.parser,a=Ft(e[0],\"color-token\").color,n=e[1];return{type:\"color\",mode:r.mode,color:a,body:ee(n)}},htmlBuilder:He,mathmlBuilder:De}),Qt({type:\"color\",names:[\"\\\\color\"],props:{numArgs:1,allowedInText:!0,greediness:3,argTypes:[\"color\"]},handler:function(t,e){var r=t.parser,a=t.breakOnTokenText,n=Ft(e[0],\"color-token\").color;r.gullet.macros.set(\"\\\\current@color\",n);var i=r.parseExpression(!0,a);return{type:\"color\",mode:r.mode,color:n,body:i}},htmlBuilder:He,mathmlBuilder:De}),Qt({type:\"cr\",names:[\"\\\\cr\",\"\\\\newline\"],props:{numArgs:0,numOptionalArgs:1,argTypes:[\"size\"],allowedInText:!0},handler:function(t,e,r){var a=t.parser,n=t.funcName,i=r[0],o=\"\\\\cr\"===n,s=!1;return o||(s=!a.settings.displayMode||!a.settings.useStrictBehavior(\"newLineInDisplayMode\",\"In LaTeX, \\\\\\\\ or \\\\newline does nothing in display mode\")),{type:\"cr\",mode:a.mode,newLine:s,newRow:o,size:i&&Ft(i,\"size\").value}},htmlBuilder:function(t,e){if(t.newRow)throw new o(\"\\\\cr valid only within a tabular/array environment\");var r=Dt.makeSpan([\"mspace\"],[],e);return t.newLine&&(r.classes.push(\"newline\"),t.size&&(r.style.marginTop=Tt(t.size,e)+\"em\")),r},mathmlBuilder:function(t,e){var r=new ve.MathNode(\"mspace\");return t.newLine&&(r.setAttribute(\"linebreak\",\"newline\"),t.size&&r.setAttribute(\"height\",Tt(t.size,e)+\"em\")),r}});var Fe=function(t,e,r){var a=G($.math[t]&&$.math[t].replace||t,e,r);if(!a)throw new Error(\"Unsupported symbol \"+t+\" and font size \"+e+\".\");return a},Ve=function(t,e,r,a){var n=r.havingBaseStyle(e),i=Dt.makeSpan(a.concat(n.sizingClasses(r)),[t],r),o=n.sizeMultiplier/r.sizeMultiplier;return i.height*=o,i.depth*=o,i.maxFontSize=n.sizeMultiplier,i},Ue=function(t,e,r){var a=e.havingBaseStyle(r),n=(1-e.sizeMultiplier/a.sizeMultiplier)*e.fontMetrics().axisHeight;t.classes.push(\"delimcenter\"),t.style.top=n+\"em\",t.height-=n,t.depth+=n},Ge=function(t,e,r,a,n,i){var o=function(t,e,r,a){return Dt.makeSymbol(t,\"Size\"+e+\"-Regular\",r,a)}(t,e,n,a),s=Ve(Dt.makeSpan([\"delimsizing\",\"size\"+e],[o],a),w.TEXT,a,i);return r&&Ue(s,a,w.TEXT),s},Ye=function(t,e,r){var a;return a=\"Size1-Regular\"===e?\"delim-size1\":\"delim-size4\",{type:\"elem\",elem:Dt.makeSpan([\"delimsizinginner\",a],[Dt.makeSpan([],[Dt.makeSymbol(t,e,r)])])}},_e={type:\"kern\",size:-.005},We=function(t,e,r,a,n,i){var o,s,h,l;o=h=l=t,s=null;var m=\"Size1-Regular\";\"\\\\uparrow\"===t?h=l=\"⏐\":\"\\\\Uparrow\"===t?h=l=\"‖\":\"\\\\downarrow\"===t?o=h=\"⏐\":\"\\\\Downarrow\"===t?o=h=\"‖\":\"\\\\updownarrow\"===t?(o=\"\\\\uparrow\",h=\"⏐\",l=\"\\\\downarrow\"):\"\\\\Updownarrow\"===t?(o=\"\\\\Uparrow\",h=\"‖\",l=\"\\\\Downarrow\"):\"[\"===t||\"\\\\lbrack\"===t?(o=\"⎡\",h=\"⎢\",l=\"⎣\",m=\"Size4-Regular\"):\"]\"===t||\"\\\\rbrack\"===t?(o=\"⎤\",h=\"⎥\",l=\"⎦\",m=\"Size4-Regular\"):\"\\\\lfloor\"===t||\"⌊\"===t?(h=o=\"⎢\",l=\"⎣\",m=\"Size4-Regular\"):\"\\\\lceil\"===t||\"⌈\"===t?(o=\"⎡\",h=l=\"⎢\",m=\"Size4-Regular\"):\"\\\\rfloor\"===t||\"⌋\"===t?(h=o=\"⎥\",l=\"⎦\",m=\"Size4-Regular\"):\"\\\\rceil\"===t||\"⌉\"===t?(o=\"⎤\",h=l=\"⎥\",m=\"Size4-Regular\"):\"(\"===t||\"\\\\lparen\"===t?(o=\"⎛\",h=\"⎜\",l=\"⎝\",m=\"Size4-Regular\"):\")\"===t||\"\\\\rparen\"===t?(o=\"⎞\",h=\"⎟\",l=\"⎠\",m=\"Size4-Regular\"):\"\\\\{\"===t||\"\\\\lbrace\"===t?(o=\"⎧\",s=\"⎨\",l=\"⎩\",h=\"⎪\",m=\"Size4-Regular\"):\"\\\\}\"===t||\"\\\\rbrace\"===t?(o=\"⎫\",s=\"⎬\",l=\"⎭\",h=\"⎪\",m=\"Size4-Regular\"):\"\\\\lgroup\"===t||\"⟮\"===t?(o=\"⎧\",l=\"⎩\",h=\"⎪\",m=\"Size4-Regular\"):\"\\\\rgroup\"===t||\"⟯\"===t?(o=\"⎫\",l=\"⎭\",h=\"⎪\",m=\"Size4-Regular\"):\"\\\\lmoustache\"===t||\"⎰\"===t?(o=\"⎧\",l=\"⎭\",h=\"⎪\",m=\"Size4-Regular\"):\"\\\\rmoustache\"!==t&&\"⎱\"!==t||(o=\"⎫\",l=\"⎩\",h=\"⎪\",m=\"Size4-Regular\");var c=Fe(o,m,n),u=c.height+c.depth,p=Fe(h,m,n),d=p.height+p.depth,f=Fe(l,m,n),g=f.height+f.depth,x=0,v=1;if(null!==s){var b=Fe(s,m,n);x=b.height+b.depth,v=2}var y=u+g+x,k=Math.max(0,Math.ceil((e-y)/(v*d))),S=y+k*v*d,M=a.fontMetrics().axisHeight;r&&(M*=a.sizeMultiplier);var z=S/2-M,A=.005*(k+1)-d,T=[];if(T.push(Ye(l,m,n)),null===s)for(var B=0;B<k;B++)T.push(_e),T.push(Ye(h,m,n));else{for(var C=0;C<k;C++)T.push(_e),T.push(Ye(h,m,n));T.push({type:\"kern\",size:A}),T.push(Ye(h,m,n)),T.push(_e),T.push(Ye(s,m,n));for(var q=0;q<k;q++)T.push(_e),T.push(Ye(h,m,n))}T.push({type:\"kern\",size:A}),T.push(Ye(h,m,n)),T.push(_e),T.push(Ye(o,m,n));var N=a.havingBaseStyle(w.TEXT),O=Dt.makeVList({positionType:\"bottom\",positionData:z,children:T},N);return Ve(Dt.makeSpan([\"delimsizing\",\"mult\"],[O],N),w.TEXT,a,i)},Xe=function(t,e,r,a,n){var i=function(t,e,r){e*=1e3;var a=\"\";switch(t){case\"sqrtMain\":a=function(t,e){return\"M95,\"+(622+t+e)+\"\\nc-2.7,0,-7.17,-2.7,-13.5,-8c-5.8,-5.3,-9.5,-10,-9.5,-14\\nc0,-2,0.3,-3.3,1,-4c1.3,-2.7,23.83,-20.7,67.5,-54\\nc44.2,-33.3,65.8,-50.3,66.5,-51c1.3,-1.3,3,-2,5,-2c4.7,0,8.7,3.3,12,10\\ns173,378,173,378c0.7,0,35.3,-71,104,-213c68.7,-142,137.5,-285,206.5,-429\\nc69,-144,104.5,-217.7,106.5,-221\\nl\"+t/2.075+\" -\"+t+\"\\nc5.3,-9.3,12,-14,20,-14\\nH400000v\"+(40+t)+\"H845.2724\\ns-225.272,467,-225.272,467s-235,486,-235,486c-2.7,4.7,-9,7,-19,7\\nc-6,0,-10,-1,-12,-3s-194,-422,-194,-422s-65,47,-65,47z\\nM\"+(834+t)+\" \"+e+\"h400000v\"+(40+t)+\"h-400000z\"}(e,80);break;case\"sqrtSize1\":a=function(t,e){return\"M263,\"+(601+t+e)+\"c0.7,0,18,39.7,52,119\\nc34,79.3,68.167,158.7,102.5,238c34.3,79.3,51.8,119.3,52.5,120\\nc340,-704.7,510.7,-1060.3,512,-1067\\nl\"+t/2.084+\" -\"+t+\"\\nc4.7,-7.3,11,-11,19,-11\\nH40000v\"+(40+t)+\"H1012.3\\ns-271.3,567,-271.3,567c-38.7,80.7,-84,175,-136,283c-52,108,-89.167,185.3,-111.5,232\\nc-22.3,46.7,-33.8,70.3,-34.5,71c-4.7,4.7,-12.3,7,-23,7s-12,-1,-12,-1\\ns-109,-253,-109,-253c-72.7,-168,-109.3,-252,-110,-252c-10.7,8,-22,16.7,-34,26\\nc-22,17.3,-33.3,26,-34,26s-26,-26,-26,-26s76,-59,76,-59s76,-60,76,-60z\\nM\"+(1001+t)+\" \"+e+\"h400000v\"+(40+t)+\"h-400000z\"}(e,80);break;case\"sqrtSize2\":a=function(t,e){return\"M983 \"+(10+t+e)+\"\\nl\"+t/3.13+\" -\"+t+\"\\nc4,-6.7,10,-10,18,-10 H400000v\"+(40+t)+\"\\nH1013.1s-83.4,268,-264.1,840c-180.7,572,-277,876.3,-289,913c-4.7,4.7,-12.7,7,-24,7\\ns-12,0,-12,0c-1.3,-3.3,-3.7,-11.7,-7,-25c-35.3,-125.3,-106.7,-373.3,-214,-744\\nc-10,12,-21,25,-33,39s-32,39,-32,39c-6,-5.3,-15,-14,-27,-26s25,-30,25,-30\\nc26.7,-32.7,52,-63,76,-91s52,-60,52,-60s208,722,208,722\\nc56,-175.3,126.3,-397.3,211,-666c84.7,-268.7,153.8,-488.2,207.5,-658.5\\nc53.7,-170.3,84.5,-266.8,92.5,-289.5z\\nM\"+(1001+t)+\" \"+e+\"h400000v\"+(40+t)+\"h-400000z\"}(e,80);break;case\"sqrtSize3\":a=function(t,e){return\"M424,\"+(2398+t+e)+\"\\nc-1.3,-0.7,-38.5,-172,-111.5,-514c-73,-342,-109.8,-513.3,-110.5,-514\\nc0,-2,-10.7,14.3,-32,49c-4.7,7.3,-9.8,15.7,-15.5,25c-5.7,9.3,-9.8,16,-12.5,20\\ns-5,7,-5,7c-4,-3.3,-8.3,-7.7,-13,-13s-13,-13,-13,-13s76,-122,76,-122s77,-121,77,-121\\ns209,968,209,968c0,-2,84.7,-361.7,254,-1079c169.3,-717.3,254.7,-1077.7,256,-1081\\nl\"+t/4.223+\" -\"+t+\"c4,-6.7,10,-10,18,-10 H400000\\nv\"+(40+t)+\"H1014.6\\ns-87.3,378.7,-272.6,1166c-185.3,787.3,-279.3,1182.3,-282,1185\\nc-2,6,-10,9,-24,9\\nc-8,0,-12,-0.7,-12,-2z M\"+(1001+t)+\" \"+e+\"\\nh400000v\"+(40+t)+\"h-400000z\"}(e,80);break;case\"sqrtSize4\":a=function(t,e){return\"M473,\"+(2713+t+e)+\"\\nc339.3,-1799.3,509.3,-2700,510,-2702 l\"+t/5.298+\" -\"+t+\"\\nc3.3,-7.3,9.3,-11,18,-11 H400000v\"+(40+t)+\"H1017.7\\ns-90.5,478,-276.2,1466c-185.7,988,-279.5,1483,-281.5,1485c-2,6,-10,9,-24,9\\nc-8,0,-12,-0.7,-12,-2c0,-1.3,-5.3,-32,-16,-92c-50.7,-293.3,-119.7,-693.3,-207,-1200\\nc0,-1.3,-5.3,8.7,-16,30c-10.7,21.3,-21.3,42.7,-32,64s-16,33,-16,33s-26,-26,-26,-26\\ns76,-153,76,-153s77,-151,77,-151c0.7,0.7,35.7,202,105,604c67.3,400.7,102,602.7,104,\\n606zM\"+(1001+t)+\" \"+e+\"h400000v\"+(40+t)+\"H1017.7z\"}(e,80);break;case\"sqrtTall\":a=function(t,e,r){return\"M702 \"+(t+e)+\"H400000\"+(40+t)+\"\\nH742v\"+(r-54-e-t)+\"l-4 4-4 4c-.667.7 -2 1.5-4 2.5s-4.167 1.833-6.5 2.5-5.5 1-9.5 1\\nh-12l-28-84c-16.667-52-96.667 -294.333-240-727l-212 -643 -85 170\\nc-4-3.333-8.333-7.667-13 -13l-13-13l77-155 77-156c66 199.333 139 419.667\\n219 661 l218 661zM702 \"+e+\"H400000v\"+(40+t)+\"H742z\"}(e,80,r)}return a}(t,a,r),o=new P(t,i),s=new L([o],{width:\"400em\",height:e+\"em\",viewBox:\"0 0 400000 \"+r,preserveAspectRatio:\"xMinYMin slice\"});return Dt.makeSvgSpan([\"hide-tail\"],[s],n)},$e=[\"(\",\"\\\\lparen\",\")\",\"\\\\rparen\",\"[\",\"\\\\lbrack\",\"]\",\"\\\\rbrack\",\"\\\\{\",\"\\\\lbrace\",\"\\\\}\",\"\\\\rbrace\",\"\\\\lfloor\",\"\\\\rfloor\",\"⌊\",\"⌋\",\"\\\\lceil\",\"\\\\rceil\",\"⌈\",\"⌉\",\"\\\\surd\"],je=[\"\\\\uparrow\",\"\\\\downarrow\",\"\\\\updownarrow\",\"\\\\Uparrow\",\"\\\\Downarrow\",\"\\\\Updownarrow\",\"|\",\"\\\\|\",\"\\\\vert\",\"\\\\Vert\",\"\\\\lvert\",\"\\\\rvert\",\"\\\\lVert\",\"\\\\rVert\",\"\\\\lgroup\",\"\\\\rgroup\",\"⟮\",\"⟯\",\"\\\\lmoustache\",\"\\\\rmoustache\",\"⎰\",\"⎱\"],Ze=[\"<\",\">\",\"\\\\langle\",\"\\\\rangle\",\"/\",\"\\\\backslash\",\"\\\\lt\",\"\\\\gt\"],Ke=[0,1.2,1.8,2.4,3],Je=[{type:\"small\",style:w.SCRIPTSCRIPT},{type:\"small\",style:w.SCRIPT},{type:\"small\",style:w.TEXT},{type:\"large\",size:1},{type:\"large\",size:2},{type:\"large\",size:3},{type:\"large\",size:4}],Qe=[{type:\"small\",style:w.SCRIPTSCRIPT},{type:\"small\",style:w.SCRIPT},{type:\"small\",style:w.TEXT},{type:\"stack\"}],tr=[{type:\"small\",style:w.SCRIPTSCRIPT},{type:\"small\",style:w.SCRIPT},{type:\"small\",style:w.TEXT},{type:\"large\",size:1},{type:\"large\",size:2},{type:\"large\",size:3},{type:\"large\",size:4},{type:\"stack\"}],er=function(t){if(\"small\"===t.type)return\"Main-Regular\";if(\"large\"===t.type)return\"Size\"+t.size+\"-Regular\";if(\"stack\"===t.type)return\"Size4-Regular\";throw new Error(\"Add support for delim type '\"+t.type+\"' here.\")},rr=function(t,e,r,a){for(var n=Math.min(2,3-a.style.size);n<r.length&&\"stack\"!==r[n].type;n++){var i=Fe(t,er(r[n]),\"math\"),o=i.height+i.depth;if(\"small\"===r[n].type&&(o*=a.havingBaseStyle(r[n].style).sizeMultiplier),o>e)return r[n]}return r[r.length-1]},ar=function(t,e,r,a,n,i){var o;\"<\"===t||\"\\\\lt\"===t||\"⟨\"===t?t=\"\\\\langle\":\">\"!==t&&\"\\\\gt\"!==t&&\"⟩\"!==t||(t=\"\\\\rangle\"),o=c.contains(Ze,t)?Je:c.contains($e,t)?tr:Qe;var s=rr(t,e,o,a);return\"small\"===s.type?function(t,e,r,a,n,i){var o=Dt.makeSymbol(t,\"Main-Regular\",n,a),s=Ve(o,e,a,i);return r&&Ue(s,a,e),s}(t,s.style,r,a,n,i):\"large\"===s.type?Ge(t,s.size,r,a,n,i):We(t,e,r,a,n,i)},nr=function(t,e){var r,a,n=e.havingBaseSizing(),i=rr(\"\\\\surd\",t*n.sizeMultiplier,tr,n),o=n.sizeMultiplier,s=Math.max(0,e.minRuleThickness-e.fontMetrics().sqrtRuleThickness),h=0,l=0,m=0;return\"small\"===i.type?(t<1?o=1:t<1.4&&(o=.7),l=(1+s)/o,(r=Xe(\"sqrtMain\",h=(1+s+.08)/o,m=1e3+1e3*s+80,s,e)).style.minWidth=\"0.853em\",a=.833/o):\"large\"===i.type?(m=1080*Ke[i.size],l=(Ke[i.size]+s)/o,h=(Ke[i.size]+s+.08)/o,(r=Xe(\"sqrtSize\"+i.size,h,m,s,e)).style.minWidth=\"1.02em\",a=1/o):(h=t+s+.08,l=t+s,m=Math.floor(1e3*t+s)+80,(r=Xe(\"sqrtTall\",h,m,s,e)).style.minWidth=\"0.742em\",a=1.056),r.height=l,r.style.height=h+\"em\",{span:r,advanceWidth:a,ruleWidth:(e.fontMetrics().sqrtRuleThickness+s)*o}},ir=function(t,e,r,a,n){if(\"<\"===t||\"\\\\lt\"===t||\"⟨\"===t?t=\"\\\\langle\":\">\"!==t&&\"\\\\gt\"!==t&&\"⟩\"!==t||(t=\"\\\\rangle\"),c.contains($e,t)||c.contains(Ze,t))return Ge(t,e,!1,r,a,n);if(c.contains(je,t))return We(t,Ke[e],!1,r,a,n);throw new o(\"Illegal delimiter: '\"+t+\"'\")},or=ar,sr=function(t,e,r,a,n,i){var o=a.fontMetrics().axisHeight*a.sizeMultiplier,s=5/a.fontMetrics().ptPerEm,h=Math.max(e-o,r+o),l=Math.max(h/500*901,2*h-s);return ar(t,l,!0,a,n,i)},hr={\"\\\\bigl\":{mclass:\"mopen\",size:1},\"\\\\Bigl\":{mclass:\"mopen\",size:2},\"\\\\biggl\":{mclass:\"mopen\",size:3},\"\\\\Biggl\":{mclass:\"mopen\",size:4},\"\\\\bigr\":{mclass:\"mclose\",size:1},\"\\\\Bigr\":{mclass:\"mclose\",size:2},\"\\\\biggr\":{mclass:\"mclose\",size:3},\"\\\\Biggr\":{mclass:\"mclose\",size:4},\"\\\\bigm\":{mclass:\"mrel\",size:1},\"\\\\Bigm\":{mclass:\"mrel\",size:2},\"\\\\biggm\":{mclass:\"mrel\",size:3},\"\\\\Biggm\":{mclass:\"mrel\",size:4},\"\\\\big\":{mclass:\"mord\",size:1},\"\\\\Big\":{mclass:\"mord\",size:2},\"\\\\bigg\":{mclass:\"mord\",size:3},\"\\\\Bigg\":{mclass:\"mord\",size:4}},lr=[\"(\",\"\\\\lparen\",\")\",\"\\\\rparen\",\"[\",\"\\\\lbrack\",\"]\",\"\\\\rbrack\",\"\\\\{\",\"\\\\lbrace\",\"\\\\}\",\"\\\\rbrace\",\"\\\\lfloor\",\"\\\\rfloor\",\"⌊\",\"⌋\",\"\\\\lceil\",\"\\\\rceil\",\"⌈\",\"⌉\",\"<\",\">\",\"\\\\langle\",\"⟨\",\"\\\\rangle\",\"⟩\",\"\\\\lt\",\"\\\\gt\",\"\\\\lvert\",\"\\\\rvert\",\"\\\\lVert\",\"\\\\rVert\",\"\\\\lgroup\",\"\\\\rgroup\",\"⟮\",\"⟯\",\"\\\\lmoustache\",\"\\\\rmoustache\",\"⎰\",\"⎱\",\"/\",\"\\\\backslash\",\"|\",\"\\\\vert\",\"\\\\|\",\"\\\\Vert\",\"\\\\uparrow\",\"\\\\Uparrow\",\"\\\\downarrow\",\"\\\\Downarrow\",\"\\\\updownarrow\",\"\\\\Updownarrow\",\".\"];function mr(t,e){var r=Yt(t);if(r&&c.contains(lr,r.text))return r;throw new o(\"Invalid delimiter: '\"+(r?r.text:JSON.stringify(t))+\"' after '\"+e.funcName+\"'\",t)}function cr(t){if(!t.body)throw new Error(\"Bug: The leftright ParseNode wasn't fully parsed.\")}Qt({type:\"delimsizing\",names:[\"\\\\bigl\",\"\\\\Bigl\",\"\\\\biggl\",\"\\\\Biggl\",\"\\\\bigr\",\"\\\\Bigr\",\"\\\\biggr\",\"\\\\Biggr\",\"\\\\bigm\",\"\\\\Bigm\",\"\\\\biggm\",\"\\\\Biggm\",\"\\\\big\",\"\\\\Big\",\"\\\\bigg\",\"\\\\Bigg\"],props:{numArgs:1},handler:function(t,e){var r=mr(e[0],t);return{type:\"delimsizing\",mode:t.parser.mode,size:hr[t.funcName].size,mclass:hr[t.funcName].mclass,delim:r.text}},htmlBuilder:function(t,e){return\".\"===t.delim?Dt.makeSpan([t.mclass]):ir(t.delim,t.size,e,t.mode,[t.mclass])},mathmlBuilder:function(t){var e=[];\".\"!==t.delim&&e.push(be(t.delim,t.mode));var r=new ve.MathNode(\"mo\",e);return\"mopen\"===t.mclass||\"mclose\"===t.mclass?r.setAttribute(\"fence\",\"true\"):r.setAttribute(\"fence\",\"false\"),r}}),Qt({type:\"leftright-right\",names:[\"\\\\right\"],props:{numArgs:1},handler:function(t,e){var r=t.parser.gullet.macros.get(\"\\\\current@color\");if(r&&\"string\"!=typeof r)throw new o(\"\\\\current@color set to non-string in \\\\right\");return{type:\"leftright-right\",mode:t.parser.mode,delim:mr(e[0],t).text,color:r}}}),Qt({type:\"leftright\",names:[\"\\\\left\"],props:{numArgs:1},handler:function(t,e){var r=mr(e[0],t),a=t.parser;++a.leftrightDepth;var n=a.parseExpression(!1);--a.leftrightDepth,a.expect(\"\\\\right\",!1);var i=Ft(a.parseFunction(),\"leftright-right\");return{type:\"leftright\",mode:a.mode,body:n,left:r.text,right:i.delim,rightColor:i.color}},htmlBuilder:function(t,e){cr(t);for(var r,a,n=se(t.body,e,!0,[\"mopen\",\"mclose\"]),i=0,o=0,s=!1,h=0;h<n.length;h++)n[h].isMiddle?s=!0:(i=Math.max(n[h].height,i),o=Math.max(n[h].depth,o));if(i*=e.sizeMultiplier,o*=e.sizeMultiplier,r=\".\"===t.left?ce(e,[\"mopen\"]):sr(t.left,i,o,e,t.mode,[\"mopen\"]),n.unshift(r),s)for(var l=1;l<n.length;l++){var m=n[l].isMiddle;m&&(n[l]=sr(m.delim,i,o,m.options,t.mode,[]))}if(\".\"===t.right)a=ce(e,[\"mclose\"]);else{var c=t.rightColor?e.withColor(t.rightColor):e;a=sr(t.right,i,o,c,t.mode,[\"mclose\"])}return n.push(a),Dt.makeSpan([\"minner\"],n,e)},mathmlBuilder:function(t,e){cr(t);var r=ke(t.body,e);if(\".\"!==t.left){var a=new ve.MathNode(\"mo\",[be(t.left,t.mode)]);a.setAttribute(\"fence\",\"true\"),r.unshift(a)}if(\".\"!==t.right){var n=new ve.MathNode(\"mo\",[be(t.right,t.mode)]);n.setAttribute(\"fence\",\"true\"),t.rightColor&&n.setAttribute(\"mathcolor\",t.rightColor),r.push(n)}return ye(r)}}),Qt({type:\"middle\",names:[\"\\\\middle\"],props:{numArgs:1},handler:function(t,e){var r=mr(e[0],t);if(!t.parser.leftrightDepth)throw new o(\"\\\\middle without preceding \\\\left\",r);return{type:\"middle\",mode:t.parser.mode,delim:r.text}},htmlBuilder:function(t,e){var r;if(\".\"===t.delim)r=ce(e,[]);else{r=ir(t.delim,1,e,t.mode,[]);var a={delim:t.delim,options:e};r.isMiddle=a}return r},mathmlBuilder:function(t,e){var r=\"\\\\vert\"===t.delim||\"|\"===t.delim?be(\"|\",\"text\"):be(t.delim,t.mode),a=new ve.MathNode(\"mo\",[r]);return a.setAttribute(\"fence\",\"true\"),a.setAttribute(\"lspace\",\"0.05em\"),a.setAttribute(\"rspace\",\"0.05em\"),a}});var ur=function(t,e){var r,a,n=Dt.wrapFragment(ue(t.body,e),e),i=t.label.substr(1),o=e.sizeMultiplier,s=0,h=c.isCharacterBox(t.body);if(\"sout\"===i)(r=Dt.makeSpan([\"stretchy\",\"sout\"])).height=e.fontMetrics().defaultRuleThickness/o,s=-.5*e.fontMetrics().xHeight;else{/cancel/.test(i)?h||n.classes.push(\"cancel-pad\"):n.classes.push(\"boxpad\");var l=0,m=0;/box/.test(i)?(m=Math.max(e.fontMetrics().fboxrule,e.minRuleThickness),l=e.fontMetrics().fboxsep+(\"colorbox\"===i?0:m)):l=h?.2:0,r=Ne(n,i,l,e),/fbox|boxed|fcolorbox/.test(i)&&(r.style.borderStyle=\"solid\",r.style.borderWidth=m+\"em\"),s=n.depth+l,t.backgroundColor&&(r.style.backgroundColor=t.backgroundColor,t.borderColor&&(r.style.borderColor=t.borderColor))}return a=t.backgroundColor?Dt.makeVList({positionType:\"individualShift\",children:[{type:\"elem\",elem:r,shift:s},{type:\"elem\",elem:n,shift:0}]},e):Dt.makeVList({positionType:\"individualShift\",children:[{type:\"elem\",elem:n,shift:0},{type:\"elem\",elem:r,shift:s,wrapperClasses:/cancel/.test(i)?[\"svg-align\"]:[]}]},e),/cancel/.test(i)&&(a.height=n.height,a.depth=n.depth),/cancel/.test(i)&&!h?Dt.makeSpan([\"mord\",\"cancel-lap\"],[a],e):Dt.makeSpan([\"mord\"],[a],e)},pr=function(t,e){var r=0,a=new ve.MathNode(t.label.indexOf(\"colorbox\")>-1?\"mpadded\":\"menclose\",[Me(t.body,e)]);switch(t.label){case\"\\\\cancel\":a.setAttribute(\"notation\",\"updiagonalstrike\");break;case\"\\\\bcancel\":a.setAttribute(\"notation\",\"downdiagonalstrike\");break;case\"\\\\sout\":a.setAttribute(\"notation\",\"horizontalstrike\");break;case\"\\\\fbox\":a.setAttribute(\"notation\",\"box\");break;case\"\\\\fcolorbox\":case\"\\\\colorbox\":if(r=e.fontMetrics().fboxsep*e.fontMetrics().ptPerEm,a.setAttribute(\"width\",\"+\"+2*r+\"pt\"),a.setAttribute(\"height\",\"+\"+2*r+\"pt\"),a.setAttribute(\"lspace\",r+\"pt\"),a.setAttribute(\"voffset\",r+\"pt\"),\"\\\\fcolorbox\"===t.label){var n=Math.max(e.fontMetrics().fboxrule,e.minRuleThickness);a.setAttribute(\"style\",\"border: \"+n+\"em solid \"+String(t.borderColor))}break;case\"\\\\xcancel\":a.setAttribute(\"notation\",\"updiagonalstrike downdiagonalstrike\")}return t.backgroundColor&&a.setAttribute(\"mathbackground\",t.backgroundColor),a};Qt({type:\"enclose\",names:[\"\\\\colorbox\"],props:{numArgs:2,allowedInText:!0,greediness:3,argTypes:[\"color\",\"text\"]},handler:function(t,e,r){var a=t.parser,n=t.funcName,i=Ft(e[0],\"color-token\").color,o=e[1];return{type:\"enclose\",mode:a.mode,label:n,backgroundColor:i,body:o}},htmlBuilder:ur,mathmlBuilder:pr}),Qt({type:\"enclose\",names:[\"\\\\fcolorbox\"],props:{numArgs:3,allowedInText:!0,greediness:3,argTypes:[\"color\",\"color\",\"text\"]},handler:function(t,e,r){var a=t.parser,n=t.funcName,i=Ft(e[0],\"color-token\").color,o=Ft(e[1],\"color-token\").color,s=e[2];return{type:\"enclose\",mode:a.mode,label:n,backgroundColor:o,borderColor:i,body:s}},htmlBuilder:ur,mathmlBuilder:pr}),Qt({type:\"enclose\",names:[\"\\\\fbox\"],props:{numArgs:1,argTypes:[\"hbox\"],allowedInText:!0},handler:function(t,e){return{type:\"enclose\",mode:t.parser.mode,label:\"\\\\fbox\",body:e[0]}}}),Qt({type:\"enclose\",names:[\"\\\\cancel\",\"\\\\bcancel\",\"\\\\xcancel\",\"\\\\sout\"],props:{numArgs:1},handler:function(t,e,r){var a=t.parser,n=t.funcName,i=e[0];return{type:\"enclose\",mode:a.mode,label:n,body:i}},htmlBuilder:ur,mathmlBuilder:pr});var dr={};function fr(t){for(var e=t.type,r=t.names,a=t.props,n=t.handler,i=t.htmlBuilder,o=t.mathmlBuilder,s={type:e,numArgs:a.numArgs||0,greediness:1,allowedInText:!1,numOptionalArgs:0,handler:n},h=0;h<r.length;++h)dr[r[h]]=s;i&&(Kt[e]=i),o&&(Jt[e]=o)}function gr(t){var e=[];t.consumeSpaces();for(var r=t.fetch().text;\"\\\\hline\"===r||\"\\\\hdashline\"===r;)t.consume(),e.push(\"\\\\hdashline\"===r),t.consumeSpaces(),r=t.fetch().text;return e}function xr(t,e,r){var a=e.hskipBeforeAndAfter,n=e.addJot,i=e.cols,s=e.arraystretch,h=e.colSeparationType;if(t.gullet.beginGroup(),t.gullet.macros.set(\"\\\\\\\\\",\"\\\\cr\"),!s){var l=t.gullet.expandMacroAsText(\"\\\\arraystretch\");if(null==l)s=1;else if(!(s=parseFloat(l))||s<0)throw new o(\"Invalid \\\\arraystretch: \"+l)}t.gullet.beginGroup();var m=[],c=[m],u=[],p=[];for(p.push(gr(t));;){var d=t.parseExpression(!1,\"\\\\cr\");t.gullet.endGroup(),t.gullet.beginGroup(),d={type:\"ordgroup\",mode:t.mode,body:d},r&&(d={type:\"styling\",mode:t.mode,style:r,body:[d]}),m.push(d);var f=t.fetch().text;if(\"&\"===f)t.consume();else{if(\"\\\\end\"===f){1===m.length&&\"styling\"===d.type&&0===d.body[0].body.length&&c.pop(),p.length<c.length+1&&p.push([]);break}if(\"\\\\cr\"!==f)throw new o(\"Expected & or \\\\\\\\ or \\\\cr or \\\\end\",t.nextToken);var g=Ft(t.parseFunction(),\"cr\");u.push(g.size),p.push(gr(t)),m=[],c.push(m)}}return t.gullet.endGroup(),t.gullet.endGroup(),{type:\"array\",mode:t.mode,addJot:n,arraystretch:s,body:c,cols:i,rowGaps:u,hskipBeforeAndAfter:a,hLinesBeforeRow:p,colSeparationType:h}}function vr(t){return\"d\"===t.substr(0,1)?\"display\":\"text\"}var br=function(t,e){var r,a,n=t.body.length,i=t.hLinesBeforeRow,s=0,h=new Array(n),l=[],m=Math.max(e.fontMetrics().arrayRuleWidth,e.minRuleThickness),u=1/e.fontMetrics().ptPerEm,p=5*u;t.colSeparationType&&\"small\"===t.colSeparationType&&(p=e.havingStyle(w.SCRIPT).sizeMultiplier/e.sizeMultiplier*.2778);var d=12*u,f=3*u,g=t.arraystretch*d,x=.7*g,v=.3*g,b=0;function y(t){for(var e=0;e<t.length;++e)e>0&&(b+=.25),l.push({pos:b,isDashed:t[e]})}for(y(i[0]),r=0;r<t.body.length;++r){var k=t.body[r],S=x,M=v;s<k.length&&(s=k.length);var z=new Array(k.length);for(a=0;a<k.length;++a){var A=ue(k[a],e);M<A.depth&&(M=A.depth),S<A.height&&(S=A.height),z[a]=A}var T=t.rowGaps[r],B=0;T&&(B=Tt(T,e))>0&&(M<(B+=v)&&(M=B),B=0),t.addJot&&(M+=f),z.height=S,z.depth=M,b+=S,z.pos=b,b+=M+B,h[r]=z,y(i[r+1])}var C,q,N=b/2+e.fontMetrics().axisHeight,O=t.cols||[],I=[];for(a=0,q=0;a<s||q<O.length;++a,++q){for(var R=O[q]||{},E=!0;\"separator\"===R.type;){if(E||((C=Dt.makeSpan([\"arraycolsep\"],[])).style.width=e.fontMetrics().doubleRuleSep+\"em\",I.push(C)),\"|\"!==R.separator&&\":\"!==R.separator)throw new o(\"Invalid separator type: \"+R.separator);var L=\"|\"===R.separator?\"solid\":\"dashed\",P=Dt.makeSpan([\"vertical-separator\"],[],e);P.style.height=b+\"em\",P.style.borderRightWidth=m+\"em\",P.style.borderRightStyle=L,P.style.margin=\"0 -\"+m/2+\"em\",P.style.verticalAlign=-(b-N)+\"em\",I.push(P),R=O[++q]||{},E=!1}if(!(a>=s)){var H=void 0;(a>0||t.hskipBeforeAndAfter)&&0!==(H=c.deflt(R.pregap,p))&&((C=Dt.makeSpan([\"arraycolsep\"],[])).style.width=H+\"em\",I.push(C));var D=[];for(r=0;r<n;++r){var F=h[r],V=F[a];if(V){var U=F.pos-N;V.depth=F.depth,V.height=F.height,D.push({type:\"elem\",elem:V,shift:U})}}D=Dt.makeVList({positionType:\"individualShift\",children:D},e),D=Dt.makeSpan([\"col-align-\"+(R.align||\"c\")],[D]),I.push(D),(a<s-1||t.hskipBeforeAndAfter)&&0!==(H=c.deflt(R.postgap,p))&&((C=Dt.makeSpan([\"arraycolsep\"],[])).style.width=H+\"em\",I.push(C))}}if(h=Dt.makeSpan([\"mtable\"],I),l.length>0){for(var G=Dt.makeLineSpan(\"hline\",e,m),Y=Dt.makeLineSpan(\"hdashline\",e,m),_=[{type:\"elem\",elem:h,shift:0}];l.length>0;){var W=l.pop(),X=W.pos-N;W.isDashed?_.push({type:\"elem\",elem:Y,shift:X}):_.push({type:\"elem\",elem:G,shift:X})}h=Dt.makeVList({positionType:\"individualShift\",children:_},e)}return Dt.makeSpan([\"mord\"],[h],e)},yr={c:\"center \",l:\"left \",r:\"right \"},wr=function(t,e){var r=new ve.MathNode(\"mtable\",t.body.map((function(t){return new ve.MathNode(\"mtr\",t.map((function(t){return new ve.MathNode(\"mtd\",[Me(t,e)])})))}))),a=.5===t.arraystretch?.1:.16+t.arraystretch-1+(t.addJot?.09:0);r.setAttribute(\"rowspacing\",a+\"em\");var n=\"\",i=\"\";if(t.cols){var o=t.cols,s=\"\",h=!1,l=0,m=o.length;\"separator\"===o[0].type&&(n+=\"top \",l=1),\"separator\"===o[o.length-1].type&&(n+=\"bottom \",m-=1);for(var c=l;c<m;c++)\"align\"===o[c].type?(i+=yr[o[c].align],h&&(s+=\"none \"),h=!0):\"separator\"===o[c].type&&h&&(s+=\"|\"===o[c].separator?\"solid \":\"dashed \",h=!1);r.setAttribute(\"columnalign\",i.trim()),/[sd]/.test(s)&&r.setAttribute(\"columnlines\",s.trim())}if(\"align\"===t.colSeparationType){for(var u=t.cols||[],p=\"\",d=1;d<u.length;d++)p+=d%2?\"0em \":\"1em \";r.setAttribute(\"columnspacing\",p.trim())}else\"alignat\"===t.colSeparationType?r.setAttribute(\"columnspacing\",\"0em\"):\"small\"===t.colSeparationType?r.setAttribute(\"columnspacing\",\"0.2778em\"):r.setAttribute(\"columnspacing\",\"1em\");var f=\"\",g=t.hLinesBeforeRow;n+=g[0].length>0?\"left \":\"\",n+=g[g.length-1].length>0?\"right \":\"\";for(var x=1;x<g.length-1;x++)f+=0===g[x].length?\"none \":g[x][0]?\"dashed \":\"solid \";return/[sd]/.test(f)&&r.setAttribute(\"rowlines\",f.trim()),\"\"!==n&&(r=new ve.MathNode(\"menclose\",[r])).setAttribute(\"notation\",n.trim()),t.arraystretch&&t.arraystretch<1&&(r=new ve.MathNode(\"mstyle\",[r])).setAttribute(\"scriptlevel\",\"1\"),r},kr=function(t,e){var r,a=[],n=xr(t.parser,{cols:a,addJot:!0},\"display\"),i=0,s={type:\"ordgroup\",mode:t.mode,body:[]},h=Vt(e[0],\"ordgroup\");if(h){for(var l=\"\",m=0;m<h.body.length;m++)l+=Ft(h.body[m],\"textord\").text;r=Number(l),i=2*r}var c=!i;n.body.forEach((function(t){for(var e=1;e<t.length;e+=2){var a=Ft(t[e],\"styling\");Ft(a.body[0],\"ordgroup\").body.unshift(s)}if(c)i<t.length&&(i=t.length);else{var n=t.length/2;if(r<n)throw new o(\"Too many math in a row: expected \"+r+\", but got \"+n,t[0])}}));for(var u=0;u<i;++u){var p=\"r\",d=0;u%2==1?p=\"l\":u>0&&c&&(d=1),a[u]={type:\"align\",align:p,pregap:d,postgap:0}}return n.colSeparationType=c?\"align\":\"alignat\",n};fr({type:\"array\",names:[\"array\",\"darray\"],props:{numArgs:1},handler:function(t,e){var r={cols:(Yt(e[0])?[e[0]]:Ft(e[0],\"ordgroup\").body).map((function(t){var e=Gt(t).text;if(-1!==\"lcr\".indexOf(e))return{type:\"align\",align:e};if(\"|\"===e)return{type:\"separator\",separator:\"|\"};if(\":\"===e)return{type:\"separator\",separator:\":\"};throw new o(\"Unknown column alignment: \"+e,t)})),hskipBeforeAndAfter:!0};return xr(t.parser,r,vr(t.envName))},htmlBuilder:br,mathmlBuilder:wr}),fr({type:\"array\",names:[\"matrix\",\"pmatrix\",\"bmatrix\",\"Bmatrix\",\"vmatrix\",\"Vmatrix\"],props:{numArgs:0},handler:function(t){var e={matrix:null,pmatrix:[\"(\",\")\"],bmatrix:[\"[\",\"]\"],Bmatrix:[\"\\\\{\",\"\\\\}\"],vmatrix:[\"|\",\"|\"],Vmatrix:[\"\\\\Vert\",\"\\\\Vert\"]}[t.envName],r=xr(t.parser,{hskipBeforeAndAfter:!1},vr(t.envName));return e?{type:\"leftright\",mode:t.mode,body:[r],left:e[0],right:e[1],rightColor:void 0}:r},htmlBuilder:br,mathmlBuilder:wr}),fr({type:\"array\",names:[\"smallmatrix\"],props:{numArgs:0},handler:function(t){var e=xr(t.parser,{arraystretch:.5},\"script\");return e.colSeparationType=\"small\",e},htmlBuilder:br,mathmlBuilder:wr}),fr({type:\"array\",names:[\"subarray\"],props:{numArgs:1},handler:function(t,e){var r=(Yt(e[0])?[e[0]]:Ft(e[0],\"ordgroup\").body).map((function(t){var e=Gt(t).text;if(-1!==\"lc\".indexOf(e))return{type:\"align\",align:e};throw new o(\"Unknown column alignment: \"+e,t)}));if(r.length>1)throw new o(\"{subarray} can contain only one column\");var a={cols:r,hskipBeforeAndAfter:!1,arraystretch:.5};if((a=xr(t.parser,a,\"script\")).body[0].length>1)throw new o(\"{subarray} can contain only one column\");return a},htmlBuilder:br,mathmlBuilder:wr}),fr({type:\"array\",names:[\"cases\",\"dcases\"],props:{numArgs:0},handler:function(t){var e=xr(t.parser,{arraystretch:1.2,cols:[{type:\"align\",align:\"l\",pregap:0,postgap:1},{type:\"align\",align:\"l\",pregap:0,postgap:0}]},vr(t.envName));return{type:\"leftright\",mode:t.mode,body:[e],left:\"\\\\{\",right:\".\",rightColor:void 0}},htmlBuilder:br,mathmlBuilder:wr}),fr({type:\"array\",names:[\"aligned\"],props:{numArgs:0},handler:kr,htmlBuilder:br,mathmlBuilder:wr}),fr({type:\"array\",names:[\"gathered\"],props:{numArgs:0},handler:function(t){return xr(t.parser,{cols:[{type:\"align\",align:\"c\"}],addJot:!0},\"display\")},htmlBuilder:br,mathmlBuilder:wr}),fr({type:\"array\",names:[\"alignedat\"],props:{numArgs:1},handler:kr,htmlBuilder:br,mathmlBuilder:wr}),Qt({type:\"text\",names:[\"\\\\hline\",\"\\\\hdashline\"],props:{numArgs:0,allowedInText:!0,allowedInMath:!0},handler:function(t,e){throw new o(t.funcName+\" valid only within array environment\")}});var Sr=dr;Qt({type:\"environment\",names:[\"\\\\begin\",\"\\\\end\"],props:{numArgs:1,argTypes:[\"text\"]},handler:function(t,e){var r=t.parser,a=t.funcName,n=e[0];if(\"ordgroup\"!==n.type)throw new o(\"Invalid environment name\",n);for(var i=\"\",s=0;s<n.body.length;++s)i+=Ft(n.body[s],\"textord\").text;if(\"\\\\begin\"===a){if(!Sr.hasOwnProperty(i))throw new o(\"No such environment: \"+i,n);var h=Sr[i],l=r.parseArguments(\"\\\\begin{\"+i+\"}\",h),m=l.args,c=l.optArgs,u={mode:r.mode,envName:i,parser:r},p=h.handler(u,m,c);r.expect(\"\\\\end\",!1);var d=r.nextToken,f=Ft(r.parseFunction(),\"environment\");if(f.name!==i)throw new o(\"Mismatch: \\\\begin{\"+i+\"} matched by \\\\end{\"+f.name+\"}\",d);return p}return{type:\"environment\",mode:r.mode,name:i,nameGroup:n}}});var Mr=Dt.makeSpan;function zr(t,e){var r=se(t.body,e,!0);return Mr([t.mclass],r,e)}function Ar(t,e){var r,a=ke(t.body,e);return\"minner\"===t.mclass?ve.newDocumentFragment(a):(\"mord\"===t.mclass?t.isCharacterBox?(r=a[0]).type=\"mi\":r=new ve.MathNode(\"mi\",a):(t.isCharacterBox?(r=a[0]).type=\"mo\":r=new ve.MathNode(\"mo\",a),\"mbin\"===t.mclass?(r.attributes.lspace=\"0.22em\",r.attributes.rspace=\"0.22em\"):\"mpunct\"===t.mclass?(r.attributes.lspace=\"0em\",r.attributes.rspace=\"0.17em\"):\"mopen\"!==t.mclass&&\"mclose\"!==t.mclass||(r.attributes.lspace=\"0em\",r.attributes.rspace=\"0em\")),r)}Qt({type:\"mclass\",names:[\"\\\\mathord\",\"\\\\mathbin\",\"\\\\mathrel\",\"\\\\mathopen\",\"\\\\mathclose\",\"\\\\mathpunct\",\"\\\\mathinner\"],props:{numArgs:1},handler:function(t,e){var r=t.parser,a=t.funcName,n=e[0];return{type:\"mclass\",mode:r.mode,mclass:\"m\"+a.substr(5),body:ee(n),isCharacterBox:c.isCharacterBox(n)}},htmlBuilder:zr,mathmlBuilder:Ar});var Tr=function(t){var e=\"ordgroup\"===t.type&&t.body.length?t.body[0]:t;return\"atom\"!==e.type||\"bin\"!==e.family&&\"rel\"!==e.family?\"mord\":\"m\"+e.family};Qt({type:\"mclass\",names:[\"\\\\@binrel\"],props:{numArgs:2},handler:function(t,e){return{type:\"mclass\",mode:t.parser.mode,mclass:Tr(e[0]),body:[e[1]],isCharacterBox:c.isCharacterBox(e[1])}}}),Qt({type:\"mclass\",names:[\"\\\\stackrel\",\"\\\\overset\",\"\\\\underset\"],props:{numArgs:2},handler:function(t,e){var r,a=t.parser,n=t.funcName,i=e[1],o=e[0];r=\"\\\\stackrel\"!==n?Tr(i):\"mrel\";var s={type:\"op\",mode:i.mode,limits:!0,alwaysHandleSupSub:!0,parentIsSupSub:!1,symbol:!1,suppressBaseShift:\"\\\\stackrel\"!==n,body:ee(i)},h={type:\"supsub\",mode:o.mode,base:s,sup:\"\\\\underset\"===n?null:o,sub:\"\\\\underset\"===n?o:null};return{type:\"mclass\",mode:a.mode,mclass:r,body:[h],isCharacterBox:c.isCharacterBox(h)}},htmlBuilder:zr,mathmlBuilder:Ar});var Br=function(t,e){var r=t.font,a=e.withFont(r);return ue(t.body,a)},Cr=function(t,e){var r=t.font,a=e.withFont(r);return Me(t.body,a)},qr={\"\\\\Bbb\":\"\\\\mathbb\",\"\\\\bold\":\"\\\\mathbf\",\"\\\\frak\":\"\\\\mathfrak\",\"\\\\bm\":\"\\\\boldsymbol\"};Qt({type:\"font\",names:[\"\\\\mathrm\",\"\\\\mathit\",\"\\\\mathbf\",\"\\\\mathnormal\",\"\\\\mathbb\",\"\\\\mathcal\",\"\\\\mathfrak\",\"\\\\mathscr\",\"\\\\mathsf\",\"\\\\mathtt\",\"\\\\Bbb\",\"\\\\bold\",\"\\\\frak\"],props:{numArgs:1,greediness:2},handler:function(t,e){var r=t.parser,a=t.funcName,n=e[0],i=a;return i in qr&&(i=qr[i]),{type:\"font\",mode:r.mode,font:i.slice(1),body:n}},htmlBuilder:Br,mathmlBuilder:Cr}),Qt({type:\"mclass\",names:[\"\\\\boldsymbol\",\"\\\\bm\"],props:{numArgs:1,greediness:2},handler:function(t,e){var r=t.parser,a=e[0],n=c.isCharacterBox(a);return{type:\"mclass\",mode:r.mode,mclass:Tr(a),body:[{type:\"font\",mode:r.mode,font:\"boldsymbol\",body:a}],isCharacterBox:n}}}),Qt({type:\"font\",names:[\"\\\\rm\",\"\\\\sf\",\"\\\\tt\",\"\\\\bf\",\"\\\\it\"],props:{numArgs:0,allowedInText:!0},handler:function(t,e){var r=t.parser,a=t.funcName,n=t.breakOnTokenText,i=r.mode,o=r.parseExpression(!0,n);return{type:\"font\",mode:i,font:\"math\"+a.slice(1),body:{type:\"ordgroup\",mode:r.mode,body:o}}},htmlBuilder:Br,mathmlBuilder:Cr});var Nr=function(t,e){var r=e;return\"display\"===t?r=r.id>=w.SCRIPT.id?r.text():w.DISPLAY:\"text\"===t&&r.size===w.DISPLAY.size?r=w.TEXT:\"script\"===t?r=w.SCRIPT:\"scriptscript\"===t&&(r=w.SCRIPTSCRIPT),r},Or=function(t,e){var r,a=Nr(t.size,e.style),n=a.fracNum(),i=a.fracDen();r=e.havingStyle(n);var o=ue(t.numer,r,e);if(t.continued){var s=8.5/e.fontMetrics().ptPerEm,h=3.5/e.fontMetrics().ptPerEm;o.height=o.height<s?s:o.height,o.depth=o.depth<h?h:o.depth}r=e.havingStyle(i);var l,m,c,u,p,d,f,g,x,v,b=ue(t.denom,r,e);if(t.hasBarLine?(t.barSize?(m=Tt(t.barSize,e),l=Dt.makeLineSpan(\"frac-line\",e,m)):l=Dt.makeLineSpan(\"frac-line\",e),m=l.height,c=l.height):(l=null,m=0,c=e.fontMetrics().defaultRuleThickness),a.size===w.DISPLAY.size||\"display\"===t.size?(u=e.fontMetrics().num1,p=m>0?3*c:7*c,d=e.fontMetrics().denom1):(m>0?(u=e.fontMetrics().num2,p=c):(u=e.fontMetrics().num3,p=3*c),d=e.fontMetrics().denom2),l){var y=e.fontMetrics().axisHeight;u-o.depth-(y+.5*m)<p&&(u+=p-(u-o.depth-(y+.5*m))),y-.5*m-(b.height-d)<p&&(d+=p-(y-.5*m-(b.height-d)));var k=-(y-.5*m);f=Dt.makeVList({positionType:\"individualShift\",children:[{type:\"elem\",elem:b,shift:d},{type:\"elem\",elem:l,shift:k},{type:\"elem\",elem:o,shift:-u}]},e)}else{var S=u-o.depth-(b.height-d);S<p&&(u+=.5*(p-S),d+=.5*(p-S)),f=Dt.makeVList({positionType:\"individualShift\",children:[{type:\"elem\",elem:b,shift:d},{type:\"elem\",elem:o,shift:-u}]},e)}return r=e.havingStyle(a),f.height*=r.sizeMultiplier/e.sizeMultiplier,f.depth*=r.sizeMultiplier/e.sizeMultiplier,g=a.size===w.DISPLAY.size?e.fontMetrics().delim1:e.fontMetrics().delim2,x=null==t.leftDelim?ce(e,[\"mopen\"]):or(t.leftDelim,g,!0,e.havingStyle(a),t.mode,[\"mopen\"]),v=t.continued?Dt.makeSpan([]):null==t.rightDelim?ce(e,[\"mclose\"]):or(t.rightDelim,g,!0,e.havingStyle(a),t.mode,[\"mclose\"]),Dt.makeSpan([\"mord\"].concat(r.sizingClasses(e)),[x,Dt.makeSpan([\"mfrac\"],[f]),v],e)},Ir=function(t,e){var r=new ve.MathNode(\"mfrac\",[Me(t.numer,e),Me(t.denom,e)]);if(t.hasBarLine){if(t.barSize){var a=Tt(t.barSize,e);r.setAttribute(\"linethickness\",a+\"em\")}}else r.setAttribute(\"linethickness\",\"0px\");var n=Nr(t.size,e.style);if(n.size!==e.style.size){r=new ve.MathNode(\"mstyle\",[r]);var i=n.size===w.DISPLAY.size?\"true\":\"false\";r.setAttribute(\"displaystyle\",i),r.setAttribute(\"scriptlevel\",\"0\")}if(null!=t.leftDelim||null!=t.rightDelim){var o=[];if(null!=t.leftDelim){var s=new ve.MathNode(\"mo\",[new ve.TextNode(t.leftDelim.replace(\"\\\\\",\"\"))]);s.setAttribute(\"fence\",\"true\"),o.push(s)}if(o.push(r),null!=t.rightDelim){var h=new ve.MathNode(\"mo\",[new ve.TextNode(t.rightDelim.replace(\"\\\\\",\"\"))]);h.setAttribute(\"fence\",\"true\"),o.push(h)}return ye(o)}return r};Qt({type:\"genfrac\",names:[\"\\\\cfrac\",\"\\\\dfrac\",\"\\\\frac\",\"\\\\tfrac\",\"\\\\dbinom\",\"\\\\binom\",\"\\\\tbinom\",\"\\\\\\\\atopfrac\",\"\\\\\\\\bracefrac\",\"\\\\\\\\brackfrac\"],props:{numArgs:2,greediness:2},handler:function(t,e){var r,a=t.parser,n=t.funcName,i=e[0],o=e[1],s=null,h=null,l=\"auto\";switch(n){case\"\\\\cfrac\":case\"\\\\dfrac\":case\"\\\\frac\":case\"\\\\tfrac\":r=!0;break;case\"\\\\\\\\atopfrac\":r=!1;break;case\"\\\\dbinom\":case\"\\\\binom\":case\"\\\\tbinom\":r=!1,s=\"(\",h=\")\";break;case\"\\\\\\\\bracefrac\":r=!1,s=\"\\\\{\",h=\"\\\\}\";break;case\"\\\\\\\\brackfrac\":r=!1,s=\"[\",h=\"]\";break;default:throw new Error(\"Unrecognized genfrac command\")}switch(n){case\"\\\\cfrac\":case\"\\\\dfrac\":case\"\\\\dbinom\":l=\"display\";break;case\"\\\\tfrac\":case\"\\\\tbinom\":l=\"text\"}return{type:\"genfrac\",mode:a.mode,continued:\"\\\\cfrac\"===n,numer:i,denom:o,hasBarLine:r,leftDelim:s,rightDelim:h,size:l,barSize:null}},htmlBuilder:Or,mathmlBuilder:Ir}),Qt({type:\"infix\",names:[\"\\\\over\",\"\\\\choose\",\"\\\\atop\",\"\\\\brace\",\"\\\\brack\"],props:{numArgs:0,infix:!0},handler:function(t){var e,r=t.parser,a=t.funcName,n=t.token;switch(a){case\"\\\\over\":e=\"\\\\frac\";break;case\"\\\\choose\":e=\"\\\\binom\";break;case\"\\\\atop\":e=\"\\\\\\\\atopfrac\";break;case\"\\\\brace\":e=\"\\\\\\\\bracefrac\";break;case\"\\\\brack\":e=\"\\\\\\\\brackfrac\";break;default:throw new Error(\"Unrecognized infix genfrac command\")}return{type:\"infix\",mode:r.mode,replaceWith:e,token:n}}});var Rr=[\"display\",\"text\",\"script\",\"scriptscript\"],Er=function(t){var e=null;return t.length>0&&(e=\".\"===(e=t)?null:e),e};Qt({type:\"genfrac\",names:[\"\\\\genfrac\"],props:{numArgs:6,greediness:6,argTypes:[\"math\",\"math\",\"size\",\"text\",\"math\",\"math\"]},handler:function(t,e){var r=t.parser,a=e[4],n=e[5],i=Vt(e[0],\"atom\");i&&(i=Ut(e[0],\"open\"));var o=i?Er(i.text):null,s=Vt(e[1],\"atom\");s&&(s=Ut(e[1],\"close\"));var h,l=s?Er(s.text):null,m=Ft(e[2],\"size\"),c=null;h=!!m.isBlank||(c=m.value).number>0;var u=\"auto\",p=Vt(e[3],\"ordgroup\");if(p){if(p.body.length>0){var d=Ft(p.body[0],\"textord\");u=Rr[Number(d.text)]}}else p=Ft(e[3],\"textord\"),u=Rr[Number(p.text)];return{type:\"genfrac\",mode:r.mode,numer:a,denom:n,continued:!1,hasBarLine:h,barSize:c,leftDelim:o,rightDelim:l,size:u}},htmlBuilder:Or,mathmlBuilder:Ir}),Qt({type:\"infix\",names:[\"\\\\above\"],props:{numArgs:1,argTypes:[\"size\"],infix:!0},handler:function(t,e){var r=t.parser,a=(t.funcName,t.token);return{type:\"infix\",mode:r.mode,replaceWith:\"\\\\\\\\abovefrac\",size:Ft(e[0],\"size\").value,token:a}}}),Qt({type:\"genfrac\",names:[\"\\\\\\\\abovefrac\"],props:{numArgs:3,argTypes:[\"math\",\"size\",\"math\"]},handler:function(t,e){var r=t.parser,a=(t.funcName,e[0]),n=function(t){if(!t)throw new Error(\"Expected non-null, but got \"+String(t));return t}(Ft(e[1],\"infix\").size),i=e[2],o=n.number>0;return{type:\"genfrac\",mode:r.mode,numer:a,denom:i,continued:!1,hasBarLine:o,barSize:n,leftDelim:null,rightDelim:null,size:\"auto\"}},htmlBuilder:Or,mathmlBuilder:Ir});var Lr=function(t,e){var r,a,n=e.style,i=Vt(t,\"supsub\");i?(r=i.sup?ue(i.sup,e.havingStyle(n.sup()),e):ue(i.sub,e.havingStyle(n.sub()),e),a=Ft(i.base,\"horizBrace\")):a=Ft(t,\"horizBrace\");var o,s=ue(a.base,e.havingBaseStyle(w.DISPLAY)),h=Ie(a,e);if(a.isOver?(o=Dt.makeVList({positionType:\"firstBaseline\",children:[{type:\"elem\",elem:s},{type:\"kern\",size:.1},{type:\"elem\",elem:h}]},e)).children[0].children[0].children[1].classes.push(\"svg-align\"):(o=Dt.makeVList({positionType:\"bottom\",positionData:s.depth+.1+h.height,children:[{type:\"elem\",elem:h},{type:\"kern\",size:.1},{type:\"elem\",elem:s}]},e)).children[0].children[0].children[0].classes.push(\"svg-align\"),r){var l=Dt.makeSpan([\"mord\",a.isOver?\"mover\":\"munder\"],[o],e);o=a.isOver?Dt.makeVList({positionType:\"firstBaseline\",children:[{type:\"elem\",elem:l},{type:\"kern\",size:.2},{type:\"elem\",elem:r}]},e):Dt.makeVList({positionType:\"bottom\",positionData:l.depth+.2+r.height+r.depth,children:[{type:\"elem\",elem:r},{type:\"kern\",size:.2},{type:\"elem\",elem:l}]},e)}return Dt.makeSpan([\"mord\",a.isOver?\"mover\":\"munder\"],[o],e)};Qt({type:\"horizBrace\",names:[\"\\\\overbrace\",\"\\\\underbrace\"],props:{numArgs:1},handler:function(t,e){var r=t.parser,a=t.funcName;return{type:\"horizBrace\",mode:r.mode,label:a,isOver:/^\\\\over/.test(a),base:e[0]}},htmlBuilder:Lr,mathmlBuilder:function(t,e){var r=Oe(t.label);return new ve.MathNode(t.isOver?\"mover\":\"munder\",[Me(t.base,e),r])}}),Qt({type:\"href\",names:[\"\\\\href\"],props:{numArgs:2,argTypes:[\"url\",\"original\"],allowedInText:!0},handler:function(t,e){var r=t.parser,a=e[1],n=Ft(e[0],\"url\").url;return r.settings.isTrusted({command:\"\\\\href\",url:n})?{type:\"href\",mode:r.mode,href:n,body:ee(a)}:r.formatUnsupportedCmd(\"\\\\href\")},htmlBuilder:function(t,e){var r=se(t.body,e,!1);return Dt.makeAnchor(t.href,[],r,e)},mathmlBuilder:function(t,e){var r=Se(t.body,e);return r instanceof ge||(r=new ge(\"mrow\",[r])),r.setAttribute(\"href\",t.href),r}}),Qt({type:\"href\",names:[\"\\\\url\"],props:{numArgs:1,argTypes:[\"url\"],allowedInText:!0},handler:function(t,e){var r=t.parser,a=Ft(e[0],\"url\").url;if(!r.settings.isTrusted({command:\"\\\\url\",url:a}))return r.formatUnsupportedCmd(\"\\\\url\");for(var n=[],i=0;i<a.length;i++){var o=a[i];\"~\"===o&&(o=\"\\\\textasciitilde\"),n.push({type:\"textord\",mode:\"text\",text:o})}var s={type:\"text\",mode:r.mode,font:\"\\\\texttt\",body:n};return{type:\"href\",mode:r.mode,href:a,body:ee(s)}}}),Qt({type:\"htmlmathml\",names:[\"\\\\html@mathml\"],props:{numArgs:2,allowedInText:!0},handler:function(t,e){return{type:\"htmlmathml\",mode:t.parser.mode,html:ee(e[0]),mathml:ee(e[1])}},htmlBuilder:function(t,e){var r=se(t.html,e,!1);return Dt.makeFragment(r)},mathmlBuilder:function(t,e){return Se(t.mathml,e)}});var Pr=function(t){if(/^[-+]? *(\\d+(\\.\\d*)?|\\.\\d+)$/.test(t))return{number:+t,unit:\"bp\"};var e=/([-+]?) *(\\d+(?:\\.\\d*)?|\\.\\d+) *([a-z]{2})/.exec(t);if(!e)throw new o(\"Invalid size: '\"+t+\"' in \\\\includegraphics\");var r={number:+(e[1]+e[2]),unit:e[3]};if(!At(r))throw new o(\"Invalid unit: '\"+r.unit+\"' in \\\\includegraphics.\");return r};Qt({type:\"includegraphics\",names:[\"\\\\includegraphics\"],props:{numArgs:1,numOptionalArgs:1,argTypes:[\"raw\",\"url\"],allowedInText:!1},handler:function(t,e,r){var a=t.parser,n={number:0,unit:\"em\"},i={number:.9,unit:\"em\"},s={number:0,unit:\"em\"},h=\"\";if(r[0])for(var l=Ft(r[0],\"raw\").string.split(\",\"),m=0;m<l.length;m++){var c=l[m].split(\"=\");if(2===c.length){var u=c[1].trim();switch(c[0].trim()){case\"alt\":h=u;break;case\"width\":n=Pr(u);break;case\"height\":i=Pr(u);break;case\"totalheight\":s=Pr(u);break;default:throw new o(\"Invalid key: '\"+c[0]+\"' in \\\\includegraphics.\")}}}var p=Ft(e[0],\"url\").url;return\"\"===h&&(h=(h=(h=p).replace(/^.*[\\\\/]/,\"\")).substring(0,h.lastIndexOf(\".\"))),a.settings.isTrusted({command:\"\\\\includegraphics\",url:p})?{type:\"includegraphics\",mode:a.mode,alt:h,width:n,height:i,totalheight:s,src:p}:a.formatUnsupportedCmd(\"\\\\includegraphics\")},htmlBuilder:function(t,e){var r=Tt(t.height,e),a=0;t.totalheight.number>0&&(a=Tt(t.totalheight,e)-r,a=Number(a.toFixed(2)));var n=0;t.width.number>0&&(n=Tt(t.width,e));var i={height:r+a+\"em\"};n>0&&(i.width=n+\"em\"),a>0&&(i.verticalAlign=-a+\"em\");var o=new I(t.src,t.alt,i);return o.height=r,o.depth=a,o},mathmlBuilder:function(t,e){var r=new ve.MathNode(\"mglyph\",[]);r.setAttribute(\"alt\",t.alt);var a=Tt(t.height,e),n=0;if(t.totalheight.number>0&&(n=(n=Tt(t.totalheight,e)-a).toFixed(2),r.setAttribute(\"valign\",\"-\"+n+\"em\")),r.setAttribute(\"height\",a+n+\"em\"),t.width.number>0){var i=Tt(t.width,e);r.setAttribute(\"width\",i+\"em\")}return r.setAttribute(\"src\",t.src),r}}),Qt({type:\"kern\",names:[\"\\\\kern\",\"\\\\mkern\",\"\\\\hskip\",\"\\\\mskip\"],props:{numArgs:1,argTypes:[\"size\"],allowedInText:!0},handler:function(t,e){var r=t.parser,a=t.funcName,n=Ft(e[0],\"size\");if(r.settings.strict){var i=\"m\"===a[1],o=\"mu\"===n.value.unit;i?(o||r.settings.reportNonstrict(\"mathVsTextUnits\",\"LaTeX's \"+a+\" supports only mu units, not \"+n.value.unit+\" units\"),\"math\"!==r.mode&&r.settings.reportNonstrict(\"mathVsTextUnits\",\"LaTeX's \"+a+\" works only in math mode\")):o&&r.settings.reportNonstrict(\"mathVsTextUnits\",\"LaTeX's \"+a+\" doesn't support mu units\")}return{type:\"kern\",mode:r.mode,dimension:n.value}},htmlBuilder:function(t,e){return Dt.makeGlue(t.dimension,e)},mathmlBuilder:function(t,e){var r=Tt(t.dimension,e);return new ve.SpaceNode(r)}}),Qt({type:\"lap\",names:[\"\\\\mathllap\",\"\\\\mathrlap\",\"\\\\mathclap\"],props:{numArgs:1,allowedInText:!0},handler:function(t,e){var r=t.parser,a=t.funcName,n=e[0];return{type:\"lap\",mode:r.mode,alignment:a.slice(5),body:n}},htmlBuilder:function(t,e){var r;\"clap\"===t.alignment?(r=Dt.makeSpan([],[ue(t.body,e)]),r=Dt.makeSpan([\"inner\"],[r],e)):r=Dt.makeSpan([\"inner\"],[ue(t.body,e)]);var a=Dt.makeSpan([\"fix\"],[]),n=Dt.makeSpan([t.alignment],[r,a],e),i=Dt.makeSpan([\"strut\"]);return i.style.height=n.height+n.depth+\"em\",i.style.verticalAlign=-n.depth+\"em\",n.children.unshift(i),n=Dt.makeVList({positionType:\"firstBaseline\",children:[{type:\"elem\",elem:n}]},e),Dt.makeSpan([\"mord\"],[n],e)},mathmlBuilder:function(t,e){var r=new ve.MathNode(\"mpadded\",[Me(t.body,e)]);if(\"rlap\"!==t.alignment){var a=\"llap\"===t.alignment?\"-1\":\"-0.5\";r.setAttribute(\"lspace\",a+\"width\")}return r.setAttribute(\"width\",\"0px\"),r}}),Qt({type:\"styling\",names:[\"\\\\(\",\"$\"],props:{numArgs:0,allowedInText:!0,allowedInMath:!1},handler:function(t,e){var r=t.funcName,a=t.parser,n=a.mode;a.switchMode(\"math\");var i=\"\\\\(\"===r?\"\\\\)\":\"$\",o=a.parseExpression(!1,i);return a.expect(i),a.switchMode(n),{type:\"styling\",mode:a.mode,style:\"text\",body:o}}}),Qt({type:\"text\",names:[\"\\\\)\",\"\\\\]\"],props:{numArgs:0,allowedInText:!0,allowedInMath:!1},handler:function(t,e){throw new o(\"Mismatched \"+t.funcName)}});var Hr=function(t,e){switch(e.style.size){case w.DISPLAY.size:return t.display;case w.TEXT.size:return t.text;case w.SCRIPT.size:return t.script;case w.SCRIPTSCRIPT.size:return t.scriptscript;default:return t.text}};Qt({type:\"mathchoice\",names:[\"\\\\mathchoice\"],props:{numArgs:4},handler:function(t,e){return{type:\"mathchoice\",mode:t.parser.mode,display:ee(e[0]),text:ee(e[1]),script:ee(e[2]),scriptscript:ee(e[3])}},htmlBuilder:function(t,e){var r=Hr(t,e),a=se(r,e,!1);return Dt.makeFragment(a)},mathmlBuilder:function(t,e){var r=Hr(t,e);return Se(r,e)}});var Dr=function(t,e,r,a,n,i,o){var s,h,l;if(t=Dt.makeSpan([],[t]),e){var m=ue(e,a.havingStyle(n.sup()),a);h={elem:m,kern:Math.max(a.fontMetrics().bigOpSpacing1,a.fontMetrics().bigOpSpacing3-m.depth)}}if(r){var c=ue(r,a.havingStyle(n.sub()),a);s={elem:c,kern:Math.max(a.fontMetrics().bigOpSpacing2,a.fontMetrics().bigOpSpacing4-c.height)}}if(h&&s){var u=a.fontMetrics().bigOpSpacing5+s.elem.height+s.elem.depth+s.kern+t.depth+o;l=Dt.makeVList({positionType:\"bottom\",positionData:u,children:[{type:\"kern\",size:a.fontMetrics().bigOpSpacing5},{type:\"elem\",elem:s.elem,marginLeft:-i+\"em\"},{type:\"kern\",size:s.kern},{type:\"elem\",elem:t},{type:\"kern\",size:h.kern},{type:\"elem\",elem:h.elem,marginLeft:i+\"em\"},{type:\"kern\",size:a.fontMetrics().bigOpSpacing5}]},a)}else if(s){var p=t.height-o;l=Dt.makeVList({positionType:\"top\",positionData:p,children:[{type:\"kern\",size:a.fontMetrics().bigOpSpacing5},{type:\"elem\",elem:s.elem,marginLeft:-i+\"em\"},{type:\"kern\",size:s.kern},{type:\"elem\",elem:t}]},a)}else{if(!h)return t;var d=t.depth+o;l=Dt.makeVList({positionType:\"bottom\",positionData:d,children:[{type:\"elem\",elem:t},{type:\"kern\",size:h.kern},{type:\"elem\",elem:h.elem,marginLeft:i+\"em\"},{type:\"kern\",size:a.fontMetrics().bigOpSpacing5}]},a)}return Dt.makeSpan([\"mop\",\"op-limits\"],[l],a)},Fr=[\"\\\\smallint\"],Vr=function(t,e){var r,a,n,i=!1,o=Vt(t,\"supsub\");o?(r=o.sup,a=o.sub,n=Ft(o.base,\"op\"),i=!0):n=Ft(t,\"op\");var s,h=e.style,l=!1;if(h.size===w.DISPLAY.size&&n.symbol&&!c.contains(Fr,n.name)&&(l=!0),n.symbol){var m=l?\"Size2-Regular\":\"Size1-Regular\",u=\"\";if(\"\\\\oiint\"!==n.name&&\"\\\\oiiint\"!==n.name||(u=n.name.substr(1),n.name=\"oiint\"===u?\"\\\\iint\":\"\\\\iiint\"),s=Dt.makeSymbol(n.name,m,\"math\",e,[\"mop\",\"op-symbol\",l?\"large-op\":\"small-op\"]),u.length>0){var p=s.italic,d=Dt.staticSvg(u+\"Size\"+(l?\"2\":\"1\"),e);s=Dt.makeVList({positionType:\"individualShift\",children:[{type:\"elem\",elem:s,shift:0},{type:\"elem\",elem:d,shift:l?.08:0}]},e),n.name=\"\\\\\"+u,s.classes.unshift(\"mop\"),s.italic=p}}else if(n.body){var f=se(n.body,e,!0);1===f.length&&f[0]instanceof E?(s=f[0]).classes[0]=\"mop\":s=Dt.makeSpan([\"mop\"],Dt.tryCombineChars(f),e)}else{for(var g=[],x=1;x<n.name.length;x++)g.push(Dt.mathsym(n.name[x],n.mode,e));s=Dt.makeSpan([\"mop\"],g,e)}var v=0,b=0;return(s instanceof E||\"\\\\oiint\"===n.name||\"\\\\oiiint\"===n.name)&&!n.suppressBaseShift&&(v=(s.height-s.depth)/2-e.fontMetrics().axisHeight,b=s.italic),i?Dr(s,r,a,e,h,b,v):(v&&(s.style.position=\"relative\",s.style.top=v+\"em\"),s)},Ur=function(t,e){var r;if(t.symbol)r=new ge(\"mo\",[be(t.name,t.mode)]),c.contains(Fr,t.name)&&r.setAttribute(\"largeop\",\"false\");else if(t.body)r=new ge(\"mo\",ke(t.body,e));else{r=new ge(\"mi\",[new xe(t.name.slice(1))]);var a=new ge(\"mo\",[be(\"⁡\",\"text\")]);r=t.parentIsSupSub?new ge(\"mo\",[r,a]):fe([r,a])}return r},Gr={\"∏\":\"\\\\prod\",\"∐\":\"\\\\coprod\",\"∑\":\"\\\\sum\",\"⋀\":\"\\\\bigwedge\",\"⋁\":\"\\\\bigvee\",\"⋂\":\"\\\\bigcap\",\"⋃\":\"\\\\bigcup\",\"⨀\":\"\\\\bigodot\",\"⨁\":\"\\\\bigoplus\",\"⨂\":\"\\\\bigotimes\",\"⨄\":\"\\\\biguplus\",\"⨆\":\"\\\\bigsqcup\"};Qt({type:\"op\",names:[\"\\\\coprod\",\"\\\\bigvee\",\"\\\\bigwedge\",\"\\\\biguplus\",\"\\\\bigcap\",\"\\\\bigcup\",\"\\\\intop\",\"\\\\prod\",\"\\\\sum\",\"\\\\bigotimes\",\"\\\\bigoplus\",\"\\\\bigodot\",\"\\\\bigsqcup\",\"\\\\smallint\",\"∏\",\"∐\",\"∑\",\"⋀\",\"⋁\",\"⋂\",\"⋃\",\"⨀\",\"⨁\",\"⨂\",\"⨄\",\"⨆\"],props:{numArgs:0},handler:function(t,e){var r=t.parser,a=t.funcName;return 1===a.length&&(a=Gr[a]),{type:\"op\",mode:r.mode,limits:!0,parentIsSupSub:!1,symbol:!0,name:a}},htmlBuilder:Vr,mathmlBuilder:Ur}),Qt({type:\"op\",names:[\"\\\\mathop\"],props:{numArgs:1},handler:function(t,e){var r=t.parser,a=e[0];return{type:\"op\",mode:r.mode,limits:!1,parentIsSupSub:!1,symbol:!1,body:ee(a)}},htmlBuilder:Vr,mathmlBuilder:Ur});var Yr={\"∫\":\"\\\\int\",\"∬\":\"\\\\iint\",\"∭\":\"\\\\iiint\",\"∮\":\"\\\\oint\",\"∯\":\"\\\\oiint\",\"∰\":\"\\\\oiiint\"};Qt({type:\"op\",names:[\"\\\\arcsin\",\"\\\\arccos\",\"\\\\arctan\",\"\\\\arctg\",\"\\\\arcctg\",\"\\\\arg\",\"\\\\ch\",\"\\\\cos\",\"\\\\cosec\",\"\\\\cosh\",\"\\\\cot\",\"\\\\cotg\",\"\\\\coth\",\"\\\\csc\",\"\\\\ctg\",\"\\\\cth\",\"\\\\deg\",\"\\\\dim\",\"\\\\exp\",\"\\\\hom\",\"\\\\ker\",\"\\\\lg\",\"\\\\ln\",\"\\\\log\",\"\\\\sec\",\"\\\\sin\",\"\\\\sinh\",\"\\\\sh\",\"\\\\tan\",\"\\\\tanh\",\"\\\\tg\",\"\\\\th\"],props:{numArgs:0},handler:function(t){var e=t.parser,r=t.funcName;return{type:\"op\",mode:e.mode,limits:!1,parentIsSupSub:!1,symbol:!1,name:r}},htmlBuilder:Vr,mathmlBuilder:Ur}),Qt({type:\"op\",names:[\"\\\\det\",\"\\\\gcd\",\"\\\\inf\",\"\\\\lim\",\"\\\\max\",\"\\\\min\",\"\\\\Pr\",\"\\\\sup\"],props:{numArgs:0},handler:function(t){var e=t.parser,r=t.funcName;return{type:\"op\",mode:e.mode,limits:!0,parentIsSupSub:!1,symbol:!1,name:r}},htmlBuilder:Vr,mathmlBuilder:Ur}),Qt({type:\"op\",names:[\"\\\\int\",\"\\\\iint\",\"\\\\iiint\",\"\\\\oint\",\"\\\\oiint\",\"\\\\oiiint\",\"∫\",\"∬\",\"∭\",\"∮\",\"∯\",\"∰\"],props:{numArgs:0},handler:function(t){var e=t.parser,r=t.funcName;return 1===r.length&&(r=Yr[r]),{type:\"op\",mode:e.mode,limits:!1,parentIsSupSub:!1,symbol:!0,name:r}},htmlBuilder:Vr,mathmlBuilder:Ur});var _r=function(t,e){var r,a,n,i,o=!1,s=Vt(t,\"supsub\");if(s?(r=s.sup,a=s.sub,n=Ft(s.base,\"operatorname\"),o=!0):n=Ft(t,\"operatorname\"),n.body.length>0){for(var h=n.body.map((function(t){var e=t.text;return\"string\"==typeof e?{type:\"textord\",mode:t.mode,text:e}:t})),l=se(h,e.withFont(\"mathrm\"),!0),m=0;m<l.length;m++){var c=l[m];c instanceof E&&(c.text=c.text.replace(/\\u2212/,\"-\").replace(/\\u2217/,\"*\"))}i=Dt.makeSpan([\"mop\"],l,e)}else i=Dt.makeSpan([\"mop\"],[],e);return o?Dr(i,r,a,e,e.style,0,0):i};function Wr(t,e,r){for(var a=se(t,e,!1),n=e.sizeMultiplier/r.sizeMultiplier,i=0;i<a.length;i++){var o=a[i].classes.indexOf(\"sizing\");o<0?Array.prototype.push.apply(a[i].classes,e.sizingClasses(r)):a[i].classes[o+1]===\"reset-size\"+e.size&&(a[i].classes[o+1]=\"reset-size\"+r.size),a[i].height*=n,a[i].depth*=n}return Dt.makeFragment(a)}Qt({type:\"operatorname\",names:[\"\\\\operatorname\",\"\\\\operatorname*\"],props:{numArgs:1},handler:function(t,e){var r=t.parser,a=t.funcName,n=e[0];return{type:\"operatorname\",mode:r.mode,body:ee(n),alwaysHandleSupSub:\"\\\\operatorname*\"===a,limits:!1,parentIsSupSub:!1}},htmlBuilder:_r,mathmlBuilder:function(t,e){for(var r=ke(t.body,e.withFont(\"mathrm\")),a=!0,n=0;n<r.length;n++){var i=r[n];if(i instanceof ve.SpaceNode);else if(i instanceof ve.MathNode)switch(i.type){case\"mi\":case\"mn\":case\"ms\":case\"mspace\":case\"mtext\":break;case\"mo\":var o=i.children[0];1===i.children.length&&o instanceof ve.TextNode?o.text=o.text.replace(/\\u2212/,\"-\").replace(/\\u2217/,\"*\"):a=!1;break;default:a=!1}else a=!1}if(a){var s=r.map((function(t){return t.toText()})).join(\"\");r=[new ve.TextNode(s)]}var h=new ve.MathNode(\"mi\",r);h.setAttribute(\"mathvariant\",\"normal\");var l=new ve.MathNode(\"mo\",[be(\"⁡\",\"text\")]);return t.parentIsSupSub?new ve.MathNode(\"mo\",[h,l]):ve.newDocumentFragment([h,l])}}),te({type:\"ordgroup\",htmlBuilder:function(t,e){return t.semisimple?Dt.makeFragment(se(t.body,e,!1)):Dt.makeSpan([\"mord\"],se(t.body,e,!0),e)},mathmlBuilder:function(t,e){return Se(t.body,e,!0)}}),Qt({type:\"overline\",names:[\"\\\\overline\"],props:{numArgs:1},handler:function(t,e){var r=t.parser,a=e[0];return{type:\"overline\",mode:r.mode,body:a}},htmlBuilder:function(t,e){var r=ue(t.body,e.havingCrampedStyle()),a=Dt.makeLineSpan(\"overline-line\",e),n=e.fontMetrics().defaultRuleThickness,i=Dt.makeVList({positionType:\"firstBaseline\",children:[{type:\"elem\",elem:r},{type:\"kern\",size:3*n},{type:\"elem\",elem:a},{type:\"kern\",size:n}]},e);return Dt.makeSpan([\"mord\",\"overline\"],[i],e)},mathmlBuilder:function(t,e){var r=new ve.MathNode(\"mo\",[new ve.TextNode(\"‾\")]);r.setAttribute(\"stretchy\",\"true\");var a=new ve.MathNode(\"mover\",[Me(t.body,e),r]);return a.setAttribute(\"accent\",\"true\"),a}}),Qt({type:\"phantom\",names:[\"\\\\phantom\"],props:{numArgs:1,allowedInText:!0},handler:function(t,e){var r=t.parser,a=e[0];return{type:\"phantom\",mode:r.mode,body:ee(a)}},htmlBuilder:function(t,e){var r=se(t.body,e.withPhantom(),!1);return Dt.makeFragment(r)},mathmlBuilder:function(t,e){var r=ke(t.body,e);return new ve.MathNode(\"mphantom\",r)}}),Qt({type:\"hphantom\",names:[\"\\\\hphantom\"],props:{numArgs:1,allowedInText:!0},handler:function(t,e){var r=t.parser,a=e[0];return{type:\"hphantom\",mode:r.mode,body:a}},htmlBuilder:function(t,e){var r=Dt.makeSpan([],[ue(t.body,e.withPhantom())]);if(r.height=0,r.depth=0,r.children)for(var a=0;a<r.children.length;a++)r.children[a].height=0,r.children[a].depth=0;return r=Dt.makeVList({positionType:\"firstBaseline\",children:[{type:\"elem\",elem:r}]},e),Dt.makeSpan([\"mord\"],[r],e)},mathmlBuilder:function(t,e){var r=ke(ee(t.body),e),a=new ve.MathNode(\"mphantom\",r),n=new ve.MathNode(\"mpadded\",[a]);return n.setAttribute(\"height\",\"0px\"),n.setAttribute(\"depth\",\"0px\"),n}}),Qt({type:\"vphantom\",names:[\"\\\\vphantom\"],props:{numArgs:1,allowedInText:!0},handler:function(t,e){var r=t.parser,a=e[0];return{type:\"vphantom\",mode:r.mode,body:a}},htmlBuilder:function(t,e){var r=Dt.makeSpan([\"inner\"],[ue(t.body,e.withPhantom())]),a=Dt.makeSpan([\"fix\"],[]);return Dt.makeSpan([\"mord\",\"rlap\"],[r,a],e)},mathmlBuilder:function(t,e){var r=ke(ee(t.body),e),a=new ve.MathNode(\"mphantom\",r),n=new ve.MathNode(\"mpadded\",[a]);return n.setAttribute(\"width\",\"0px\"),n}}),Qt({type:\"raisebox\",names:[\"\\\\raisebox\"],props:{numArgs:2,argTypes:[\"size\",\"hbox\"],allowedInText:!0},handler:function(t,e){var r=t.parser,a=Ft(e[0],\"size\").value,n=e[1];return{type:\"raisebox\",mode:r.mode,dy:a,body:n}},htmlBuilder:function(t,e){var r=ue(t.body,e),a=Tt(t.dy,e);return Dt.makeVList({positionType:\"shift\",positionData:-a,children:[{type:\"elem\",elem:r}]},e)},mathmlBuilder:function(t,e){var r=new ve.MathNode(\"mpadded\",[Me(t.body,e)]),a=t.dy.number+t.dy.unit;return r.setAttribute(\"voffset\",a),r}}),Qt({type:\"rule\",names:[\"\\\\rule\"],props:{numArgs:2,numOptionalArgs:1,argTypes:[\"size\",\"size\",\"size\"]},handler:function(t,e,r){var a=t.parser,n=r[0],i=Ft(e[0],\"size\"),o=Ft(e[1],\"size\");return{type:\"rule\",mode:a.mode,shift:n&&Ft(n,\"size\").value,width:i.value,height:o.value}},htmlBuilder:function(t,e){var r=Dt.makeSpan([\"mord\",\"rule\"],[],e),a=Tt(t.width,e),n=Tt(t.height,e),i=t.shift?Tt(t.shift,e):0;return r.style.borderRightWidth=a+\"em\",r.style.borderTopWidth=n+\"em\",r.style.bottom=i+\"em\",r.width=a,r.height=n+i,r.depth=-i,r.maxFontSize=1.125*n*e.sizeMultiplier,r},mathmlBuilder:function(t,e){var r=Tt(t.width,e),a=Tt(t.height,e),n=t.shift?Tt(t.shift,e):0,i=e.color&&e.getColor()||\"black\",o=new ve.MathNode(\"mspace\");o.setAttribute(\"mathbackground\",i),o.setAttribute(\"width\",r+\"em\"),o.setAttribute(\"height\",a+\"em\");var s=new ve.MathNode(\"mpadded\",[o]);return n>=0?s.setAttribute(\"height\",\"+\"+n+\"em\"):(s.setAttribute(\"height\",n+\"em\"),s.setAttribute(\"depth\",\"+\"+-n+\"em\")),s.setAttribute(\"voffset\",n+\"em\"),s}});var Xr=[\"\\\\tiny\",\"\\\\sixptsize\",\"\\\\scriptsize\",\"\\\\footnotesize\",\"\\\\small\",\"\\\\normalsize\",\"\\\\large\",\"\\\\Large\",\"\\\\LARGE\",\"\\\\huge\",\"\\\\Huge\"];Qt({type:\"sizing\",names:Xr,props:{numArgs:0,allowedInText:!0},handler:function(t,e){var r=t.breakOnTokenText,a=t.funcName,n=t.parser,i=n.parseExpression(!1,r);return{type:\"sizing\",mode:n.mode,size:Xr.indexOf(a)+1,body:i}},htmlBuilder:function(t,e){var r=e.havingSize(t.size);return Wr(t.body,r,e)},mathmlBuilder:function(t,e){var r=e.havingSize(t.size),a=ke(t.body,r),n=new ve.MathNode(\"mstyle\",a);return n.setAttribute(\"mathsize\",r.sizeMultiplier+\"em\"),n}}),Qt({type:\"smash\",names:[\"\\\\smash\"],props:{numArgs:1,numOptionalArgs:1,allowedInText:!0},handler:function(t,e,r){var a=t.parser,n=!1,i=!1,o=r[0]&&Ft(r[0],\"ordgroup\");if(o)for(var s=\"\",h=0;h<o.body.length;++h)if(\"t\"===(s=o.body[h].text))n=!0;else{if(\"b\"!==s){n=!1,i=!1;break}i=!0}else n=!0,i=!0;var l=e[0];return{type:\"smash\",mode:a.mode,body:l,smashHeight:n,smashDepth:i}},htmlBuilder:function(t,e){var r=Dt.makeSpan([],[ue(t.body,e)]);if(!t.smashHeight&&!t.smashDepth)return r;if(t.smashHeight&&(r.height=0,r.children))for(var a=0;a<r.children.length;a++)r.children[a].height=0;if(t.smashDepth&&(r.depth=0,r.children))for(var n=0;n<r.children.length;n++)r.children[n].depth=0;var i=Dt.makeVList({positionType:\"firstBaseline\",children:[{type:\"elem\",elem:r}]},e);return Dt.makeSpan([\"mord\"],[i],e)},mathmlBuilder:function(t,e){var r=new ve.MathNode(\"mpadded\",[Me(t.body,e)]);return t.smashHeight&&r.setAttribute(\"height\",\"0px\"),t.smashDepth&&r.setAttribute(\"depth\",\"0px\"),r}}),Qt({type:\"sqrt\",names:[\"\\\\sqrt\"],props:{numArgs:1,numOptionalArgs:1},handler:function(t,e,r){var a=t.parser,n=r[0],i=e[0];return{type:\"sqrt\",mode:a.mode,body:i,index:n}},htmlBuilder:function(t,e){var r=ue(t.body,e.havingCrampedStyle());0===r.height&&(r.height=e.fontMetrics().xHeight),r=Dt.wrapFragment(r,e);var a=e.fontMetrics().defaultRuleThickness,n=a;e.style.id<w.TEXT.id&&(n=e.fontMetrics().xHeight);var i=a+n/4,o=r.height+r.depth+i+a,s=nr(o,e),h=s.span,l=s.ruleWidth,m=s.advanceWidth,c=h.height-l;c>r.height+r.depth+i&&(i=(i+c-r.height-r.depth)/2);var u=h.height-r.height-i-l;r.style.paddingLeft=m+\"em\";var p=Dt.makeVList({positionType:\"firstBaseline\",children:[{type:\"elem\",elem:r,wrapperClasses:[\"svg-align\"]},{type:\"kern\",size:-(r.height+u)},{type:\"elem\",elem:h},{type:\"kern\",size:l}]},e);if(t.index){var d=e.havingStyle(w.SCRIPTSCRIPT),f=ue(t.index,d,e),g=.6*(p.height-p.depth),x=Dt.makeVList({positionType:\"shift\",positionData:-g,children:[{type:\"elem\",elem:f}]},e),v=Dt.makeSpan([\"root\"],[x]);return Dt.makeSpan([\"mord\",\"sqrt\"],[v,p],e)}return Dt.makeSpan([\"mord\",\"sqrt\"],[p],e)},mathmlBuilder:function(t,e){var r=t.body,a=t.index;return a?new ve.MathNode(\"mroot\",[Me(r,e),Me(a,e)]):new ve.MathNode(\"msqrt\",[Me(r,e)])}});var $r={display:w.DISPLAY,text:w.TEXT,script:w.SCRIPT,scriptscript:w.SCRIPTSCRIPT};Qt({type:\"styling\",names:[\"\\\\displaystyle\",\"\\\\textstyle\",\"\\\\scriptstyle\",\"\\\\scriptscriptstyle\"],props:{numArgs:0,allowedInText:!0},handler:function(t,e){var r=t.breakOnTokenText,a=t.funcName,n=t.parser,i=n.parseExpression(!0,r),o=a.slice(1,a.length-5);return{type:\"styling\",mode:n.mode,style:o,body:i}},htmlBuilder:function(t,e){var r=$r[t.style],a=e.havingStyle(r).withFont(\"\");return Wr(t.body,a,e)},mathmlBuilder:function(t,e){var r=$r[t.style],a=e.havingStyle(r),n=ke(t.body,a),i=new ve.MathNode(\"mstyle\",n),o={display:[\"0\",\"true\"],text:[\"0\",\"false\"],script:[\"1\",\"false\"],scriptscript:[\"2\",\"false\"]}[t.style];return i.setAttribute(\"scriptlevel\",o[0]),i.setAttribute(\"displaystyle\",o[1]),i}}),te({type:\"supsub\",htmlBuilder:function(t,e){var r=function(t,e){var r=t.base;return r?\"op\"===r.type?r.limits&&(e.style.size===w.DISPLAY.size||r.alwaysHandleSupSub)?Vr:null:\"operatorname\"===r.type?r.alwaysHandleSupSub&&(e.style.size===w.DISPLAY.size||r.limits)?_r:null:\"accent\"===r.type?c.isCharacterBox(r.base)?Re:null:\"horizBrace\"===r.type&&!t.sub===r.isOver?Lr:null:null}(t,e);if(r)return r(t,e);var a,n,i,o=t.base,s=t.sup,h=t.sub,l=ue(o,e),m=e.fontMetrics(),u=0,p=0,d=o&&c.isCharacterBox(o);if(s){var f=e.havingStyle(e.style.sup());a=ue(s,f,e),d||(u=l.height-f.fontMetrics().supDrop*f.sizeMultiplier/e.sizeMultiplier)}if(h){var g=e.havingStyle(e.style.sub());n=ue(h,g,e),d||(p=l.depth+g.fontMetrics().subDrop*g.sizeMultiplier/e.sizeMultiplier)}i=e.style===w.DISPLAY?m.sup1:e.style.cramped?m.sup3:m.sup2;var x,v=e.sizeMultiplier,b=.5/m.ptPerEm/v+\"em\",y=null;if(n){var k=t.base&&\"op\"===t.base.type&&t.base.name&&(\"\\\\oiint\"===t.base.name||\"\\\\oiiint\"===t.base.name);(l instanceof E||k)&&(y=-l.italic+\"em\")}if(a&&n){u=Math.max(u,i,a.depth+.25*m.xHeight),p=Math.max(p,m.sub2);var S=4*m.defaultRuleThickness;if(u-a.depth-(n.height-p)<S){p=S-(u-a.depth)+n.height;var M=.8*m.xHeight-(u-a.depth);M>0&&(u+=M,p-=M)}var z=[{type:\"elem\",elem:n,shift:p,marginRight:b,marginLeft:y},{type:\"elem\",elem:a,shift:-u,marginRight:b}];x=Dt.makeVList({positionType:\"individualShift\",children:z},e)}else if(n){p=Math.max(p,m.sub1,n.height-.8*m.xHeight);var A=[{type:\"elem\",elem:n,marginLeft:y,marginRight:b}];x=Dt.makeVList({positionType:\"shift\",positionData:p,children:A},e)}else{if(!a)throw new Error(\"supsub must have either sup or sub.\");u=Math.max(u,i,a.depth+.25*m.xHeight),x=Dt.makeVList({positionType:\"shift\",positionData:-u,children:[{type:\"elem\",elem:a,marginRight:b}]},e)}var T=me(l,\"right\")||\"mord\";return Dt.makeSpan([T],[l,Dt.makeSpan([\"msupsub\"],[x])],e)},mathmlBuilder:function(t,e){var r,a=!1,n=Vt(t.base,\"horizBrace\");n&&!!t.sup===n.isOver&&(a=!0,r=n.isOver),!t.base||\"op\"!==t.base.type&&\"operatorname\"!==t.base.type||(t.base.parentIsSupSub=!0);var i,o=[Me(t.base,e)];if(t.sub&&o.push(Me(t.sub,e)),t.sup&&o.push(Me(t.sup,e)),a)i=r?\"mover\":\"munder\";else if(t.sub)if(t.sup){var s=t.base;i=s&&\"op\"===s.type&&s.limits&&e.style===w.DISPLAY||s&&\"operatorname\"===s.type&&s.alwaysHandleSupSub&&(e.style===w.DISPLAY||s.limits)?\"munderover\":\"msubsup\"}else{var h=t.base;i=h&&\"op\"===h.type&&h.limits&&(e.style===w.DISPLAY||h.alwaysHandleSupSub)||h&&\"operatorname\"===h.type&&h.alwaysHandleSupSub&&(h.limits||e.style===w.DISPLAY)?\"munder\":\"msub\"}else{var l=t.base;i=l&&\"op\"===l.type&&l.limits&&(e.style===w.DISPLAY||l.alwaysHandleSupSub)||l&&\"operatorname\"===l.type&&l.alwaysHandleSupSub&&(l.limits||e.style===w.DISPLAY)?\"mover\":\"msup\"}return new ve.MathNode(i,o)}}),te({type:\"atom\",htmlBuilder:function(t,e){return Dt.mathsym(t.text,t.mode,e,[\"m\"+t.family])},mathmlBuilder:function(t,e){var r=new ve.MathNode(\"mo\",[be(t.text,t.mode)]);if(\"bin\"===t.family){var a=we(t,e);\"bold-italic\"===a&&r.setAttribute(\"mathvariant\",a)}else\"punct\"===t.family?r.setAttribute(\"separator\",\"true\"):\"open\"!==t.family&&\"close\"!==t.family||r.setAttribute(\"stretchy\",\"false\");return r}});var jr={mi:\"italic\",mn:\"normal\",mtext:\"normal\"};te({type:\"mathord\",htmlBuilder:function(t,e){return Dt.makeOrd(t,e,\"mathord\")},mathmlBuilder:function(t,e){var r=new ve.MathNode(\"mi\",[be(t.text,t.mode,e)]),a=we(t,e)||\"italic\";return a!==jr[r.type]&&r.setAttribute(\"mathvariant\",a),r}}),te({type:\"textord\",htmlBuilder:function(t,e){return Dt.makeOrd(t,e,\"textord\")},mathmlBuilder:function(t,e){var r,a=be(t.text,t.mode,e),n=we(t,e)||\"normal\";return r=\"text\"===t.mode?new ve.MathNode(\"mtext\",[a]):/[0-9]/.test(t.text)?new ve.MathNode(\"mn\",[a]):\"\\\\prime\"===t.text?new ve.MathNode(\"mo\",[a]):new ve.MathNode(\"mi\",[a]),n!==jr[r.type]&&r.setAttribute(\"mathvariant\",n),r}});var Zr={\"\\\\nobreak\":\"nobreak\",\"\\\\allowbreak\":\"allowbreak\"},Kr={\" \":{},\"\\\\ \":{},\"~\":{className:\"nobreak\"},\"\\\\space\":{},\"\\\\nobreakspace\":{className:\"nobreak\"}};te({type:\"spacing\",htmlBuilder:function(t,e){if(Kr.hasOwnProperty(t.text)){var r=Kr[t.text].className||\"\";if(\"text\"===t.mode){var a=Dt.makeOrd(t,e,\"textord\");return a.classes.push(r),a}return Dt.makeSpan([\"mspace\",r],[Dt.mathsym(t.text,t.mode,e)],e)}if(Zr.hasOwnProperty(t.text))return Dt.makeSpan([\"mspace\",Zr[t.text]],[],e);throw new o('Unknown type of space \"'+t.text+'\"')},mathmlBuilder:function(t,e){if(!Kr.hasOwnProperty(t.text)){if(Zr.hasOwnProperty(t.text))return new ve.MathNode(\"mspace\");throw new o('Unknown type of space \"'+t.text+'\"')}return new ve.MathNode(\"mtext\",[new ve.TextNode(\" \")])}});var Jr=function(){var t=new ve.MathNode(\"mtd\",[]);return t.setAttribute(\"width\",\"50%\"),t};te({type:\"tag\",mathmlBuilder:function(t,e){var r=new ve.MathNode(\"mtable\",[new ve.MathNode(\"mtr\",[Jr(),new ve.MathNode(\"mtd\",[Se(t.body,e)]),Jr(),new ve.MathNode(\"mtd\",[Se(t.tag,e)])])]);return r.setAttribute(\"width\",\"100%\"),r}});var Qr={\"\\\\text\":void 0,\"\\\\textrm\":\"textrm\",\"\\\\textsf\":\"textsf\",\"\\\\texttt\":\"texttt\",\"\\\\textnormal\":\"textrm\"},ta={\"\\\\textbf\":\"textbf\",\"\\\\textmd\":\"textmd\"},ea={\"\\\\textit\":\"textit\",\"\\\\textup\":\"textup\"},ra=function(t,e){var r=t.font;return r?Qr[r]?e.withTextFontFamily(Qr[r]):ta[r]?e.withTextFontWeight(ta[r]):e.withTextFontShape(ea[r]):e};Qt({type:\"text\",names:[\"\\\\text\",\"\\\\textrm\",\"\\\\textsf\",\"\\\\texttt\",\"\\\\textnormal\",\"\\\\textbf\",\"\\\\textmd\",\"\\\\textit\",\"\\\\textup\"],props:{numArgs:1,argTypes:[\"text\"],greediness:2,allowedInText:!0},handler:function(t,e){var r=t.parser,a=t.funcName,n=e[0];return{type:\"text\",mode:r.mode,body:ee(n),font:a}},htmlBuilder:function(t,e){var r=ra(t,e),a=se(t.body,r,!0);return Dt.makeSpan([\"mord\",\"text\"],Dt.tryCombineChars(a),r)},mathmlBuilder:function(t,e){var r=ra(t,e);return Se(t.body,r)}}),Qt({type:\"underline\",names:[\"\\\\underline\"],props:{numArgs:1,allowedInText:!0},handler:function(t,e){return{type:\"underline\",mode:t.parser.mode,body:e[0]}},htmlBuilder:function(t,e){var r=ue(t.body,e),a=Dt.makeLineSpan(\"underline-line\",e),n=e.fontMetrics().defaultRuleThickness,i=Dt.makeVList({positionType:\"top\",positionData:r.height,children:[{type:\"kern\",size:n},{type:\"elem\",elem:a},{type:\"kern\",size:3*n},{type:\"elem\",elem:r}]},e);return Dt.makeSpan([\"mord\",\"underline\"],[i],e)},mathmlBuilder:function(t,e){var r=new ve.MathNode(\"mo\",[new ve.TextNode(\"‾\")]);r.setAttribute(\"stretchy\",\"true\");var a=new ve.MathNode(\"munder\",[Me(t.body,e),r]);return a.setAttribute(\"accentunder\",\"true\"),a}}),Qt({type:\"verb\",names:[\"\\\\verb\"],props:{numArgs:0,allowedInText:!0},handler:function(t,e,r){throw new o(\"\\\\verb ended by end of line instead of matching delimiter\")},htmlBuilder:function(t,e){for(var r=aa(t),a=[],n=e.havingStyle(e.style.text()),i=0;i<r.length;i++){var o=r[i];\"~\"===o&&(o=\"\\\\textasciitilde\"),a.push(Dt.makeSymbol(o,\"Typewriter-Regular\",t.mode,n,[\"mord\",\"texttt\"]))}return Dt.makeSpan([\"mord\",\"text\"].concat(n.sizingClasses(e)),Dt.tryCombineChars(a),n)},mathmlBuilder:function(t,e){var r=new ve.TextNode(aa(t)),a=new ve.MathNode(\"mtext\",[r]);return a.setAttribute(\"mathvariant\",\"monospace\"),a}});var aa=function(t){return t.body.replace(/ /g,t.star?\"␣\":\" \")},na=Zt,ia=new RegExp(\"^(\\\\\\\\[a-zA-Z@]+)[ \\r\\n\\t]*$\"),oa=new RegExp(\"[̀-ͯ]+$\"),sa=function(){function t(t,e){this.input=void 0,this.settings=void 0,this.tokenRegex=void 0,this.catcodes=void 0,this.input=t,this.settings=e,this.tokenRegex=new RegExp(\"([ \\r\\n\\t]+)|([!-\\\\[\\\\]-‧‪-퟿豈-￿][̀-ͯ]*|[\\ud800-\\udbff][\\udc00-\\udfff][̀-ͯ]*|\\\\\\\\verb\\\\*([^]).*?\\\\3|\\\\\\\\verb([^*a-zA-Z]).*?\\\\4|\\\\\\\\operatorname\\\\*|\\\\\\\\[a-zA-Z@]+[ \\r\\n\\t]*|\\\\\\\\[^\\ud800-\\udfff])\",\"g\"),this.catcodes={\"%\":14}}var e=t.prototype;return e.setCatcode=function(t,e){this.catcodes[t]=e},e.lex=function(){var t=this.input,e=this.tokenRegex.lastIndex;if(e===t.length)return new n(\"EOF\",new a(this,e,e));var r=this.tokenRegex.exec(t);if(null===r||r.index!==e)throw new o(\"Unexpected character: '\"+t[e]+\"'\",new n(t[e],new a(this,e,e+1)));var i=r[2]||\" \";if(14===this.catcodes[i]){var s=t.indexOf(\"\\n\",this.tokenRegex.lastIndex);return-1===s?(this.tokenRegex.lastIndex=t.length,this.settings.reportNonstrict(\"commentAtEnd\",\"% comment has no terminating newline; LaTeX would fail because of commenting the end of math mode (e.g. $)\")):this.tokenRegex.lastIndex=s+1,this.lex()}var h=i.match(ia);return h&&(i=h[1]),new n(i,new a(this,e,this.tokenRegex.lastIndex))},t}(),ha=function(){function t(t,e){void 0===t&&(t={}),void 0===e&&(e={}),this.current=void 0,this.builtins=void 0,this.undefStack=void 0,this.current=e,this.builtins=t,this.undefStack=[]}var e=t.prototype;return e.beginGroup=function(){this.undefStack.push({})},e.endGroup=function(){if(0===this.undefStack.length)throw new o(\"Unbalanced namespace destruction: attempt to pop global namespace; please report this as a bug\");var t=this.undefStack.pop();for(var e in t)t.hasOwnProperty(e)&&(void 0===t[e]?delete this.current[e]:this.current[e]=t[e])},e.has=function(t){return this.current.hasOwnProperty(t)||this.builtins.hasOwnProperty(t)},e.get=function(t){return this.current.hasOwnProperty(t)?this.current[t]:this.builtins[t]},e.set=function(t,e,r){if(void 0===r&&(r=!1),r){for(var a=0;a<this.undefStack.length;a++)delete this.undefStack[a][t];this.undefStack.length>0&&(this.undefStack[this.undefStack.length-1][t]=e)}else{var n=this.undefStack[this.undefStack.length-1];n&&!n.hasOwnProperty(t)&&(n[t]=this.current[t])}this.current[t]=e},t}(),la={},ma=la;function ca(t,e){la[t]=e}ca(\"\\\\@firstoftwo\",(function(t){return{tokens:t.consumeArgs(2)[0],numArgs:0}})),ca(\"\\\\@secondoftwo\",(function(t){return{tokens:t.consumeArgs(2)[1],numArgs:0}})),ca(\"\\\\@ifnextchar\",(function(t){var e=t.consumeArgs(3),r=t.future();return 1===e[0].length&&e[0][0].text===r.text?{tokens:e[1],numArgs:0}:{tokens:e[2],numArgs:0}})),ca(\"\\\\@ifstar\",\"\\\\@ifnextchar *{\\\\@firstoftwo{#1}}\"),ca(\"\\\\TextOrMath\",(function(t){var e=t.consumeArgs(2);return\"text\"===t.mode?{tokens:e[0],numArgs:0}:{tokens:e[1],numArgs:0}}));var ua={0:0,1:1,2:2,3:3,4:4,5:5,6:6,7:7,8:8,9:9,a:10,A:10,b:11,B:11,c:12,C:12,d:13,D:13,e:14,E:14,f:15,F:15};ca(\"\\\\char\",(function(t){var e,r=t.popToken(),a=\"\";if(\"'\"===r.text)e=8,r=t.popToken();else if('\"'===r.text)e=16,r=t.popToken();else if(\"`\"===r.text)if(\"\\\\\"===(r=t.popToken()).text[0])a=r.text.charCodeAt(1);else{if(\"EOF\"===r.text)throw new o(\"\\\\char` missing argument\");a=r.text.charCodeAt(0)}else e=10;if(e){if(null==(a=ua[r.text])||a>=e)throw new o(\"Invalid base-\"+e+\" digit \"+r.text);for(var n;null!=(n=ua[t.future().text])&&n<e;)a*=e,a+=n,t.popToken()}return\"\\\\@char{\"+a+\"}\"}));var pa=function(t,e){var r=t.consumeArgs(1)[0];if(1!==r.length)throw new o(\"\\\\gdef's first argument must be a macro name\");var a=r[0].text,n=0;for(r=t.consumeArgs(1)[0];1===r.length&&\"#\"===r[0].text;){if(1!==(r=t.consumeArgs(1)[0]).length)throw new o('Invalid argument number length \"'+r.length+'\"');if(!/^[1-9]$/.test(r[0].text))throw new o('Invalid argument number \"'+r[0].text+'\"');if(n++,parseInt(r[0].text)!==n)throw new o('Argument number \"'+r[0].text+'\" out of order');r=t.consumeArgs(1)[0]}return t.macros.set(a,{tokens:r,numArgs:n},e),\"\"};ca(\"\\\\gdef\",(function(t){return pa(t,!0)})),ca(\"\\\\def\",(function(t){return pa(t,!1)})),ca(\"\\\\global\",(function(t){var e=t.consumeArgs(1)[0];if(1!==e.length)throw new o(\"Invalid command after \\\\global\");var r=e[0].text;if(\"\\\\def\"===r)return pa(t,!0);throw new o(\"Invalid command '\"+r+\"' after \\\\global\")}));var da=function(t,e,r){var a=t.consumeArgs(1)[0];if(1!==a.length)throw new o(\"\\\\newcommand's first argument must be a macro name\");var n=a[0].text,i=t.isDefined(n);if(i&&!e)throw new o(\"\\\\newcommand{\"+n+\"} attempting to redefine \"+n+\"; use \\\\renewcommand\");if(!i&&!r)throw new o(\"\\\\renewcommand{\"+n+\"} when command \"+n+\" does not yet exist; use \\\\newcommand\");var s=0;if(1===(a=t.consumeArgs(1)[0]).length&&\"[\"===a[0].text){for(var h=\"\",l=t.expandNextToken();\"]\"!==l.text&&\"EOF\"!==l.text;)h+=l.text,l=t.expandNextToken();if(!h.match(/^\\s*[0-9]+\\s*$/))throw new o(\"Invalid number of arguments: \"+h);s=parseInt(h),a=t.consumeArgs(1)[0]}return t.macros.set(n,{tokens:a,numArgs:s}),\"\"};ca(\"\\\\newcommand\",(function(t){return da(t,!1,!0)})),ca(\"\\\\renewcommand\",(function(t){return da(t,!0,!1)})),ca(\"\\\\providecommand\",(function(t){return da(t,!0,!0)})),ca(\"\\\\bgroup\",\"{\"),ca(\"\\\\egroup\",\"}\"),ca(\"\\\\lq\",\"`\"),ca(\"\\\\rq\",\"'\"),ca(\"\\\\aa\",\"\\\\r a\"),ca(\"\\\\AA\",\"\\\\r A\"),ca(\"\\\\textcopyright\",\"\\\\html@mathml{\\\\textcircled{c}}{\\\\char`©}\"),ca(\"\\\\copyright\",\"\\\\TextOrMath{\\\\textcopyright}{\\\\text{\\\\textcopyright}}\"),ca(\"\\\\textregistered\",\"\\\\html@mathml{\\\\textcircled{\\\\scriptsize R}}{\\\\char`®}\"),ca(\"ℬ\",\"\\\\mathscr{B}\"),ca(\"ℰ\",\"\\\\mathscr{E}\"),ca(\"ℱ\",\"\\\\mathscr{F}\"),ca(\"ℋ\",\"\\\\mathscr{H}\"),ca(\"ℐ\",\"\\\\mathscr{I}\"),ca(\"ℒ\",\"\\\\mathscr{L}\"),ca(\"ℳ\",\"\\\\mathscr{M}\"),ca(\"ℛ\",\"\\\\mathscr{R}\"),ca(\"ℭ\",\"\\\\mathfrak{C}\"),ca(\"ℌ\",\"\\\\mathfrak{H}\"),ca(\"ℨ\",\"\\\\mathfrak{Z}\"),ca(\"\\\\Bbbk\",\"\\\\Bbb{k}\"),ca(\"·\",\"\\\\cdotp\"),ca(\"\\\\llap\",\"\\\\mathllap{\\\\textrm{#1}}\"),ca(\"\\\\rlap\",\"\\\\mathrlap{\\\\textrm{#1}}\"),ca(\"\\\\clap\",\"\\\\mathclap{\\\\textrm{#1}}\"),ca(\"\\\\not\",'\\\\html@mathml{\\\\mathrel{\\\\mathrlap\\\\@not}}{\\\\char\"338}'),ca(\"\\\\neq\",\"\\\\html@mathml{\\\\mathrel{\\\\not=}}{\\\\mathrel{\\\\char`≠}}\"),ca(\"\\\\ne\",\"\\\\neq\"),ca(\"≠\",\"\\\\neq\"),ca(\"\\\\notin\",\"\\\\html@mathml{\\\\mathrel{{\\\\in}\\\\mathllap{/\\\\mskip1mu}}}{\\\\mathrel{\\\\char`∉}}\"),ca(\"∉\",\"\\\\notin\"),ca(\"≘\",\"\\\\html@mathml{\\\\mathrel{=\\\\kern{-1em}\\\\raisebox{0.4em}{$\\\\scriptsize\\\\frown$}}}{\\\\mathrel{\\\\char`≘}}\"),ca(\"≙\",\"\\\\html@mathml{\\\\stackrel{\\\\tiny\\\\wedge}{=}}{\\\\mathrel{\\\\char`≘}}\"),ca(\"≚\",\"\\\\html@mathml{\\\\stackrel{\\\\tiny\\\\vee}{=}}{\\\\mathrel{\\\\char`≚}}\"),ca(\"≛\",\"\\\\html@mathml{\\\\stackrel{\\\\scriptsize\\\\star}{=}}{\\\\mathrel{\\\\char`≛}}\"),ca(\"≝\",\"\\\\html@mathml{\\\\stackrel{\\\\tiny\\\\mathrm{def}}{=}}{\\\\mathrel{\\\\char`≝}}\"),ca(\"≞\",\"\\\\html@mathml{\\\\stackrel{\\\\tiny\\\\mathrm{m}}{=}}{\\\\mathrel{\\\\char`≞}}\"),ca(\"≟\",\"\\\\html@mathml{\\\\stackrel{\\\\tiny?}{=}}{\\\\mathrel{\\\\char`≟}}\"),ca(\"⟂\",\"\\\\perp\"),ca(\"‼\",\"\\\\mathclose{!\\\\mkern-0.8mu!}\"),ca(\"∌\",\"\\\\notni\"),ca(\"⌜\",\"\\\\ulcorner\"),ca(\"⌝\",\"\\\\urcorner\"),ca(\"⌞\",\"\\\\llcorner\"),ca(\"⌟\",\"\\\\lrcorner\"),ca(\"©\",\"\\\\copyright\"),ca(\"®\",\"\\\\textregistered\"),ca(\"️\",\"\\\\textregistered\"),ca(\"\\\\vdots\",\"\\\\mathord{\\\\varvdots\\\\rule{0pt}{15pt}}\"),ca(\"⋮\",\"\\\\vdots\"),ca(\"\\\\varGamma\",\"\\\\mathit{\\\\Gamma}\"),ca(\"\\\\varDelta\",\"\\\\mathit{\\\\Delta}\"),ca(\"\\\\varTheta\",\"\\\\mathit{\\\\Theta}\"),ca(\"\\\\varLambda\",\"\\\\mathit{\\\\Lambda}\"),ca(\"\\\\varXi\",\"\\\\mathit{\\\\Xi}\"),ca(\"\\\\varPi\",\"\\\\mathit{\\\\Pi}\"),ca(\"\\\\varSigma\",\"\\\\mathit{\\\\Sigma}\"),ca(\"\\\\varUpsilon\",\"\\\\mathit{\\\\Upsilon}\"),ca(\"\\\\varPhi\",\"\\\\mathit{\\\\Phi}\"),ca(\"\\\\varPsi\",\"\\\\mathit{\\\\Psi}\"),ca(\"\\\\varOmega\",\"\\\\mathit{\\\\Omega}\"),ca(\"\\\\substack\",\"\\\\begin{subarray}{c}#1\\\\end{subarray}\"),ca(\"\\\\colon\",\"\\\\nobreak\\\\mskip2mu\\\\mathpunct{}\\\\mathchoice{\\\\mkern-3mu}{\\\\mkern-3mu}{}{}{:}\\\\mskip6mu\"),ca(\"\\\\boxed\",\"\\\\fbox{$\\\\displaystyle{#1}$}\"),ca(\"\\\\iff\",\"\\\\DOTSB\\\\;\\\\Longleftrightarrow\\\\;\"),ca(\"\\\\implies\",\"\\\\DOTSB\\\\;\\\\Longrightarrow\\\\;\"),ca(\"\\\\impliedby\",\"\\\\DOTSB\\\\;\\\\Longleftarrow\\\\;\");var fa={\",\":\"\\\\dotsc\",\"\\\\not\":\"\\\\dotsb\",\"+\":\"\\\\dotsb\",\"=\":\"\\\\dotsb\",\"<\":\"\\\\dotsb\",\">\":\"\\\\dotsb\",\"-\":\"\\\\dotsb\",\"*\":\"\\\\dotsb\",\":\":\"\\\\dotsb\",\"\\\\DOTSB\":\"\\\\dotsb\",\"\\\\coprod\":\"\\\\dotsb\",\"\\\\bigvee\":\"\\\\dotsb\",\"\\\\bigwedge\":\"\\\\dotsb\",\"\\\\biguplus\":\"\\\\dotsb\",\"\\\\bigcap\":\"\\\\dotsb\",\"\\\\bigcup\":\"\\\\dotsb\",\"\\\\prod\":\"\\\\dotsb\",\"\\\\sum\":\"\\\\dotsb\",\"\\\\bigotimes\":\"\\\\dotsb\",\"\\\\bigoplus\":\"\\\\dotsb\",\"\\\\bigodot\":\"\\\\dotsb\",\"\\\\bigsqcup\":\"\\\\dotsb\",\"\\\\And\":\"\\\\dotsb\",\"\\\\longrightarrow\":\"\\\\dotsb\",\"\\\\Longrightarrow\":\"\\\\dotsb\",\"\\\\longleftarrow\":\"\\\\dotsb\",\"\\\\Longleftarrow\":\"\\\\dotsb\",\"\\\\longleftrightarrow\":\"\\\\dotsb\",\"\\\\Longleftrightarrow\":\"\\\\dotsb\",\"\\\\mapsto\":\"\\\\dotsb\",\"\\\\longmapsto\":\"\\\\dotsb\",\"\\\\hookrightarrow\":\"\\\\dotsb\",\"\\\\doteq\":\"\\\\dotsb\",\"\\\\mathbin\":\"\\\\dotsb\",\"\\\\mathrel\":\"\\\\dotsb\",\"\\\\relbar\":\"\\\\dotsb\",\"\\\\Relbar\":\"\\\\dotsb\",\"\\\\xrightarrow\":\"\\\\dotsb\",\"\\\\xleftarrow\":\"\\\\dotsb\",\"\\\\DOTSI\":\"\\\\dotsi\",\"\\\\int\":\"\\\\dotsi\",\"\\\\oint\":\"\\\\dotsi\",\"\\\\iint\":\"\\\\dotsi\",\"\\\\iiint\":\"\\\\dotsi\",\"\\\\iiiint\":\"\\\\dotsi\",\"\\\\idotsint\":\"\\\\dotsi\",\"\\\\DOTSX\":\"\\\\dotsx\"};ca(\"\\\\dots\",(function(t){var e=\"\\\\dotso\",r=t.expandAfterFuture().text;return r in fa?e=fa[r]:(\"\\\\not\"===r.substr(0,4)||r in $.math&&c.contains([\"bin\",\"rel\"],$.math[r].group))&&(e=\"\\\\dotsb\"),e}));var ga={\")\":!0,\"]\":!0,\"\\\\rbrack\":!0,\"\\\\}\":!0,\"\\\\rbrace\":!0,\"\\\\rangle\":!0,\"\\\\rceil\":!0,\"\\\\rfloor\":!0,\"\\\\rgroup\":!0,\"\\\\rmoustache\":!0,\"\\\\right\":!0,\"\\\\bigr\":!0,\"\\\\biggr\":!0,\"\\\\Bigr\":!0,\"\\\\Biggr\":!0,$:!0,\";\":!0,\".\":!0,\",\":!0};ca(\"\\\\dotso\",(function(t){return t.future().text in ga?\"\\\\ldots\\\\,\":\"\\\\ldots\"})),ca(\"\\\\dotsc\",(function(t){var e=t.future().text;return e in ga&&\",\"!==e?\"\\\\ldots\\\\,\":\"\\\\ldots\"})),ca(\"\\\\cdots\",(function(t){return t.future().text in ga?\"\\\\@cdots\\\\,\":\"\\\\@cdots\"})),ca(\"\\\\dotsb\",\"\\\\cdots\"),ca(\"\\\\dotsm\",\"\\\\cdots\"),ca(\"\\\\dotsi\",\"\\\\!\\\\cdots\"),ca(\"\\\\dotsx\",\"\\\\ldots\\\\,\"),ca(\"\\\\DOTSI\",\"\\\\relax\"),ca(\"\\\\DOTSB\",\"\\\\relax\"),ca(\"\\\\DOTSX\",\"\\\\relax\"),ca(\"\\\\tmspace\",\"\\\\TextOrMath{\\\\kern#1#3}{\\\\mskip#1#2}\\\\relax\"),ca(\"\\\\,\",\"\\\\tmspace+{3mu}{.1667em}\"),ca(\"\\\\thinspace\",\"\\\\,\"),ca(\"\\\\>\",\"\\\\mskip{4mu}\"),ca(\"\\\\:\",\"\\\\tmspace+{4mu}{.2222em}\"),ca(\"\\\\medspace\",\"\\\\:\"),ca(\"\\\\;\",\"\\\\tmspace+{5mu}{.2777em}\"),ca(\"\\\\thickspace\",\"\\\\;\"),ca(\"\\\\!\",\"\\\\tmspace-{3mu}{.1667em}\"),ca(\"\\\\negthinspace\",\"\\\\!\"),ca(\"\\\\negmedspace\",\"\\\\tmspace-{4mu}{.2222em}\"),ca(\"\\\\negthickspace\",\"\\\\tmspace-{5mu}{.277em}\"),ca(\"\\\\enspace\",\"\\\\kern.5em \"),ca(\"\\\\enskip\",\"\\\\hskip.5em\\\\relax\"),ca(\"\\\\quad\",\"\\\\hskip1em\\\\relax\"),ca(\"\\\\qquad\",\"\\\\hskip2em\\\\relax\"),ca(\"\\\\tag\",\"\\\\@ifstar\\\\tag@literal\\\\tag@paren\"),ca(\"\\\\tag@paren\",\"\\\\tag@literal{({#1})}\"),ca(\"\\\\tag@literal\",(function(t){if(t.macros.get(\"\\\\df@tag\"))throw new o(\"Multiple \\\\tag\");return\"\\\\gdef\\\\df@tag{\\\\text{#1}}\"})),ca(\"\\\\bmod\",\"\\\\mathchoice{\\\\mskip1mu}{\\\\mskip1mu}{\\\\mskip5mu}{\\\\mskip5mu}\\\\mathbin{\\\\rm mod}\\\\mathchoice{\\\\mskip1mu}{\\\\mskip1mu}{\\\\mskip5mu}{\\\\mskip5mu}\"),ca(\"\\\\pod\",\"\\\\allowbreak\\\\mathchoice{\\\\mkern18mu}{\\\\mkern8mu}{\\\\mkern8mu}{\\\\mkern8mu}(#1)\"),ca(\"\\\\pmod\",\"\\\\pod{{\\\\rm mod}\\\\mkern6mu#1}\"),ca(\"\\\\mod\",\"\\\\allowbreak\\\\mathchoice{\\\\mkern18mu}{\\\\mkern12mu}{\\\\mkern12mu}{\\\\mkern12mu}{\\\\rm mod}\\\\,\\\\,#1\"),ca(\"\\\\pmb\",\"\\\\html@mathml{\\\\@binrel{#1}{\\\\mathrlap{#1}\\\\kern0.5px#1}}{\\\\mathbf{#1}}\"),ca(\"\\\\\\\\\",\"\\\\newline\"),ca(\"\\\\TeX\",\"\\\\textrm{\\\\html@mathml{T\\\\kern-.1667em\\\\raisebox{-.5ex}{E}\\\\kern-.125emX}{TeX}}\");var xa=F[\"Main-Regular\"][\"T\".charCodeAt(0)][1]-.7*F[\"Main-Regular\"][\"A\".charCodeAt(0)][1]+\"em\";ca(\"\\\\LaTeX\",\"\\\\textrm{\\\\html@mathml{L\\\\kern-.36em\\\\raisebox{\"+xa+\"}{\\\\scriptstyle A}\\\\kern-.15em\\\\TeX}{LaTeX}}\"),ca(\"\\\\KaTeX\",\"\\\\textrm{\\\\html@mathml{K\\\\kern-.17em\\\\raisebox{\"+xa+\"}{\\\\scriptstyle A}\\\\kern-.15em\\\\TeX}{KaTeX}}\"),ca(\"\\\\hspace\",\"\\\\@ifstar\\\\@hspacer\\\\@hspace\"),ca(\"\\\\@hspace\",\"\\\\hskip #1\\\\relax\"),ca(\"\\\\@hspacer\",\"\\\\rule{0pt}{0pt}\\\\hskip #1\\\\relax\"),ca(\"\\\\ordinarycolon\",\":\"),ca(\"\\\\vcentcolon\",\"\\\\mathrel{\\\\mathop\\\\ordinarycolon}\"),ca(\"\\\\dblcolon\",'\\\\html@mathml{\\\\mathrel{\\\\vcentcolon\\\\mathrel{\\\\mkern-.9mu}\\\\vcentcolon}}{\\\\mathop{\\\\char\"2237}}'),ca(\"\\\\coloneqq\",'\\\\html@mathml{\\\\mathrel{\\\\vcentcolon\\\\mathrel{\\\\mkern-1.2mu}=}}{\\\\mathop{\\\\char\"2254}}'),ca(\"\\\\Coloneqq\",'\\\\html@mathml{\\\\mathrel{\\\\dblcolon\\\\mathrel{\\\\mkern-1.2mu}=}}{\\\\mathop{\\\\char\"2237\\\\char\"3d}}'),ca(\"\\\\coloneq\",'\\\\html@mathml{\\\\mathrel{\\\\vcentcolon\\\\mathrel{\\\\mkern-1.2mu}\\\\mathrel{-}}}{\\\\mathop{\\\\char\"3a\\\\char\"2212}}'),ca(\"\\\\Coloneq\",'\\\\html@mathml{\\\\mathrel{\\\\dblcolon\\\\mathrel{\\\\mkern-1.2mu}\\\\mathrel{-}}}{\\\\mathop{\\\\char\"2237\\\\char\"2212}}'),ca(\"\\\\eqqcolon\",'\\\\html@mathml{\\\\mathrel{=\\\\mathrel{\\\\mkern-1.2mu}\\\\vcentcolon}}{\\\\mathop{\\\\char\"2255}}'),ca(\"\\\\Eqqcolon\",'\\\\html@mathml{\\\\mathrel{=\\\\mathrel{\\\\mkern-1.2mu}\\\\dblcolon}}{\\\\mathop{\\\\char\"3d\\\\char\"2237}}'),ca(\"\\\\eqcolon\",'\\\\html@mathml{\\\\mathrel{\\\\mathrel{-}\\\\mathrel{\\\\mkern-1.2mu}\\\\vcentcolon}}{\\\\mathop{\\\\char\"2239}}'),ca(\"\\\\Eqcolon\",'\\\\html@mathml{\\\\mathrel{\\\\mathrel{-}\\\\mathrel{\\\\mkern-1.2mu}\\\\dblcolon}}{\\\\mathop{\\\\char\"2212\\\\char\"2237}}'),ca(\"\\\\colonapprox\",'\\\\html@mathml{\\\\mathrel{\\\\vcentcolon\\\\mathrel{\\\\mkern-1.2mu}\\\\approx}}{\\\\mathop{\\\\char\"3a\\\\char\"2248}}'),ca(\"\\\\Colonapprox\",'\\\\html@mathml{\\\\mathrel{\\\\dblcolon\\\\mathrel{\\\\mkern-1.2mu}\\\\approx}}{\\\\mathop{\\\\char\"2237\\\\char\"2248}}'),ca(\"\\\\colonsim\",'\\\\html@mathml{\\\\mathrel{\\\\vcentcolon\\\\mathrel{\\\\mkern-1.2mu}\\\\sim}}{\\\\mathop{\\\\char\"3a\\\\char\"223c}}'),ca(\"\\\\Colonsim\",'\\\\html@mathml{\\\\mathrel{\\\\dblcolon\\\\mathrel{\\\\mkern-1.2mu}\\\\sim}}{\\\\mathop{\\\\char\"2237\\\\char\"223c}}'),ca(\"∷\",\"\\\\dblcolon\"),ca(\"∹\",\"\\\\eqcolon\"),ca(\"≔\",\"\\\\coloneqq\"),ca(\"≕\",\"\\\\eqqcolon\"),ca(\"⩴\",\"\\\\Coloneqq\"),ca(\"\\\\ratio\",\"\\\\vcentcolon\"),ca(\"\\\\coloncolon\",\"\\\\dblcolon\"),ca(\"\\\\colonequals\",\"\\\\coloneqq\"),ca(\"\\\\coloncolonequals\",\"\\\\Coloneqq\"),ca(\"\\\\equalscolon\",\"\\\\eqqcolon\"),ca(\"\\\\equalscoloncolon\",\"\\\\Eqqcolon\"),ca(\"\\\\colonminus\",\"\\\\coloneq\"),ca(\"\\\\coloncolonminus\",\"\\\\Coloneq\"),ca(\"\\\\minuscolon\",\"\\\\eqcolon\"),ca(\"\\\\minuscoloncolon\",\"\\\\Eqcolon\"),ca(\"\\\\coloncolonapprox\",\"\\\\Colonapprox\"),ca(\"\\\\coloncolonsim\",\"\\\\Colonsim\"),ca(\"\\\\simcolon\",\"\\\\mathrel{\\\\sim\\\\mathrel{\\\\mkern-1.2mu}\\\\vcentcolon}\"),ca(\"\\\\simcoloncolon\",\"\\\\mathrel{\\\\sim\\\\mathrel{\\\\mkern-1.2mu}\\\\dblcolon}\"),ca(\"\\\\approxcolon\",\"\\\\mathrel{\\\\approx\\\\mathrel{\\\\mkern-1.2mu}\\\\vcentcolon}\"),ca(\"\\\\approxcoloncolon\",\"\\\\mathrel{\\\\approx\\\\mathrel{\\\\mkern-1.2mu}\\\\dblcolon}\"),ca(\"\\\\notni\",\"\\\\html@mathml{\\\\not\\\\ni}{\\\\mathrel{\\\\char`∌}}\"),ca(\"\\\\limsup\",\"\\\\DOTSB\\\\operatorname*{lim\\\\,sup}\"),ca(\"\\\\liminf\",\"\\\\DOTSB\\\\operatorname*{lim\\\\,inf}\"),ca(\"\\\\gvertneqq\",\"\\\\html@mathml{\\\\@gvertneqq}{≩}\"),ca(\"\\\\lvertneqq\",\"\\\\html@mathml{\\\\@lvertneqq}{≨}\"),ca(\"\\\\ngeqq\",\"\\\\html@mathml{\\\\@ngeqq}{≱}\"),ca(\"\\\\ngeqslant\",\"\\\\html@mathml{\\\\@ngeqslant}{≱}\"),ca(\"\\\\nleqq\",\"\\\\html@mathml{\\\\@nleqq}{≰}\"),ca(\"\\\\nleqslant\",\"\\\\html@mathml{\\\\@nleqslant}{≰}\"),ca(\"\\\\nshortmid\",\"\\\\html@mathml{\\\\@nshortmid}{∤}\"),ca(\"\\\\nshortparallel\",\"\\\\html@mathml{\\\\@nshortparallel}{∦}\"),ca(\"\\\\nsubseteqq\",\"\\\\html@mathml{\\\\@nsubseteqq}{⊈}\"),ca(\"\\\\nsupseteqq\",\"\\\\html@mathml{\\\\@nsupseteqq}{⊉}\"),ca(\"\\\\varsubsetneq\",\"\\\\html@mathml{\\\\@varsubsetneq}{⊊}\"),ca(\"\\\\varsubsetneqq\",\"\\\\html@mathml{\\\\@varsubsetneqq}{⫋}\"),ca(\"\\\\varsupsetneq\",\"\\\\html@mathml{\\\\@varsupsetneq}{⊋}\"),ca(\"\\\\varsupsetneqq\",\"\\\\html@mathml{\\\\@varsupsetneqq}{⫌}\"),ca(\"\\\\llbracket\",\"\\\\html@mathml{\\\\mathopen{[\\\\mkern-3.2mu[}}{\\\\mathopen{\\\\char`⟦}}\"),ca(\"\\\\rrbracket\",\"\\\\html@mathml{\\\\mathclose{]\\\\mkern-3.2mu]}}{\\\\mathclose{\\\\char`⟧}}\"),ca(\"⟦\",\"\\\\llbracket\"),ca(\"⟧\",\"\\\\rrbracket\"),ca(\"\\\\lBrace\",\"\\\\html@mathml{\\\\mathopen{\\\\{\\\\mkern-3.2mu[}}{\\\\mathopen{\\\\char`⦃}}\"),ca(\"\\\\rBrace\",\"\\\\html@mathml{\\\\mathclose{]\\\\mkern-3.2mu\\\\}}}{\\\\mathclose{\\\\char`⦄}}\"),ca(\"⦃\",\"\\\\lBrace\"),ca(\"⦄\",\"\\\\rBrace\"),ca(\"\\\\darr\",\"\\\\downarrow\"),ca(\"\\\\dArr\",\"\\\\Downarrow\"),ca(\"\\\\Darr\",\"\\\\Downarrow\"),ca(\"\\\\lang\",\"\\\\langle\"),ca(\"\\\\rang\",\"\\\\rangle\"),ca(\"\\\\uarr\",\"\\\\uparrow\"),ca(\"\\\\uArr\",\"\\\\Uparrow\"),ca(\"\\\\Uarr\",\"\\\\Uparrow\"),ca(\"\\\\N\",\"\\\\mathbb{N}\"),ca(\"\\\\R\",\"\\\\mathbb{R}\"),ca(\"\\\\Z\",\"\\\\mathbb{Z}\"),ca(\"\\\\alef\",\"\\\\aleph\"),ca(\"\\\\alefsym\",\"\\\\aleph\"),ca(\"\\\\Alpha\",\"\\\\mathrm{A}\"),ca(\"\\\\Beta\",\"\\\\mathrm{B}\"),ca(\"\\\\bull\",\"\\\\bullet\"),ca(\"\\\\Chi\",\"\\\\mathrm{X}\"),ca(\"\\\\clubs\",\"\\\\clubsuit\"),ca(\"\\\\cnums\",\"\\\\mathbb{C}\"),ca(\"\\\\Complex\",\"\\\\mathbb{C}\"),ca(\"\\\\Dagger\",\"\\\\ddagger\"),ca(\"\\\\diamonds\",\"\\\\diamondsuit\"),ca(\"\\\\empty\",\"\\\\emptyset\"),ca(\"\\\\Epsilon\",\"\\\\mathrm{E}\"),ca(\"\\\\Eta\",\"\\\\mathrm{H}\"),ca(\"\\\\exist\",\"\\\\exists\"),ca(\"\\\\harr\",\"\\\\leftrightarrow\"),ca(\"\\\\hArr\",\"\\\\Leftrightarrow\"),ca(\"\\\\Harr\",\"\\\\Leftrightarrow\"),ca(\"\\\\hearts\",\"\\\\heartsuit\"),ca(\"\\\\image\",\"\\\\Im\"),ca(\"\\\\infin\",\"\\\\infty\"),ca(\"\\\\Iota\",\"\\\\mathrm{I}\"),ca(\"\\\\isin\",\"\\\\in\"),ca(\"\\\\Kappa\",\"\\\\mathrm{K}\"),ca(\"\\\\larr\",\"\\\\leftarrow\"),ca(\"\\\\lArr\",\"\\\\Leftarrow\"),ca(\"\\\\Larr\",\"\\\\Leftarrow\"),ca(\"\\\\lrarr\",\"\\\\leftrightarrow\"),ca(\"\\\\lrArr\",\"\\\\Leftrightarrow\"),ca(\"\\\\Lrarr\",\"\\\\Leftrightarrow\"),ca(\"\\\\Mu\",\"\\\\mathrm{M}\"),ca(\"\\\\natnums\",\"\\\\mathbb{N}\"),ca(\"\\\\Nu\",\"\\\\mathrm{N}\"),ca(\"\\\\Omicron\",\"\\\\mathrm{O}\"),ca(\"\\\\plusmn\",\"\\\\pm\"),ca(\"\\\\rarr\",\"\\\\rightarrow\"),ca(\"\\\\rArr\",\"\\\\Rightarrow\"),ca(\"\\\\Rarr\",\"\\\\Rightarrow\"),ca(\"\\\\real\",\"\\\\Re\"),ca(\"\\\\reals\",\"\\\\mathbb{R}\"),ca(\"\\\\Reals\",\"\\\\mathbb{R}\"),ca(\"\\\\Rho\",\"\\\\mathrm{P}\"),ca(\"\\\\sdot\",\"\\\\cdot\"),ca(\"\\\\sect\",\"\\\\S\"),ca(\"\\\\spades\",\"\\\\spadesuit\"),ca(\"\\\\sub\",\"\\\\subset\"),ca(\"\\\\sube\",\"\\\\subseteq\"),ca(\"\\\\supe\",\"\\\\supseteq\"),ca(\"\\\\Tau\",\"\\\\mathrm{T}\"),ca(\"\\\\thetasym\",\"\\\\vartheta\"),ca(\"\\\\weierp\",\"\\\\wp\"),ca(\"\\\\Zeta\",\"\\\\mathrm{Z}\"),ca(\"\\\\argmin\",\"\\\\DOTSB\\\\operatorname*{arg\\\\,min}\"),ca(\"\\\\argmax\",\"\\\\DOTSB\\\\operatorname*{arg\\\\,max}\"),ca(\"\\\\plim\",\"\\\\DOTSB\\\\mathop{\\\\operatorname{plim}}\\\\limits\"),ca(\"\\\\blue\",\"\\\\textcolor{##6495ed}{#1}\"),ca(\"\\\\orange\",\"\\\\textcolor{##ffa500}{#1}\"),ca(\"\\\\pink\",\"\\\\textcolor{##ff00af}{#1}\"),ca(\"\\\\red\",\"\\\\textcolor{##df0030}{#1}\"),ca(\"\\\\green\",\"\\\\textcolor{##28ae7b}{#1}\"),ca(\"\\\\gray\",\"\\\\textcolor{gray}{#1}\"),ca(\"\\\\purple\",\"\\\\textcolor{##9d38bd}{#1}\"),ca(\"\\\\blueA\",\"\\\\textcolor{##ccfaff}{#1}\"),ca(\"\\\\blueB\",\"\\\\textcolor{##80f6ff}{#1}\"),ca(\"\\\\blueC\",\"\\\\textcolor{##63d9ea}{#1}\"),ca(\"\\\\blueD\",\"\\\\textcolor{##11accd}{#1}\"),ca(\"\\\\blueE\",\"\\\\textcolor{##0c7f99}{#1}\"),ca(\"\\\\tealA\",\"\\\\textcolor{##94fff5}{#1}\"),ca(\"\\\\tealB\",\"\\\\textcolor{##26edd5}{#1}\"),ca(\"\\\\tealC\",\"\\\\textcolor{##01d1c1}{#1}\"),ca(\"\\\\tealD\",\"\\\\textcolor{##01a995}{#1}\"),ca(\"\\\\tealE\",\"\\\\textcolor{##208170}{#1}\"),ca(\"\\\\greenA\",\"\\\\textcolor{##b6ffb0}{#1}\"),ca(\"\\\\greenB\",\"\\\\textcolor{##8af281}{#1}\"),ca(\"\\\\greenC\",\"\\\\textcolor{##74cf70}{#1}\"),ca(\"\\\\greenD\",\"\\\\textcolor{##1fab54}{#1}\"),ca(\"\\\\greenE\",\"\\\\textcolor{##0d923f}{#1}\"),ca(\"\\\\goldA\",\"\\\\textcolor{##ffd0a9}{#1}\"),ca(\"\\\\goldB\",\"\\\\textcolor{##ffbb71}{#1}\"),ca(\"\\\\goldC\",\"\\\\textcolor{##ff9c39}{#1}\"),ca(\"\\\\goldD\",\"\\\\textcolor{##e07d10}{#1}\"),ca(\"\\\\goldE\",\"\\\\textcolor{##a75a05}{#1}\"),ca(\"\\\\redA\",\"\\\\textcolor{##fca9a9}{#1}\"),ca(\"\\\\redB\",\"\\\\textcolor{##ff8482}{#1}\"),ca(\"\\\\redC\",\"\\\\textcolor{##f9685d}{#1}\"),ca(\"\\\\redD\",\"\\\\textcolor{##e84d39}{#1}\"),ca(\"\\\\redE\",\"\\\\textcolor{##bc2612}{#1}\"),ca(\"\\\\maroonA\",\"\\\\textcolor{##ffbde0}{#1}\"),ca(\"\\\\maroonB\",\"\\\\textcolor{##ff92c6}{#1}\"),ca(\"\\\\maroonC\",\"\\\\textcolor{##ed5fa6}{#1}\"),ca(\"\\\\maroonD\",\"\\\\textcolor{##ca337c}{#1}\"),ca(\"\\\\maroonE\",\"\\\\textcolor{##9e034e}{#1}\"),ca(\"\\\\purpleA\",\"\\\\textcolor{##ddd7ff}{#1}\"),ca(\"\\\\purpleB\",\"\\\\textcolor{##c6b9fc}{#1}\"),ca(\"\\\\purpleC\",\"\\\\textcolor{##aa87ff}{#1}\"),ca(\"\\\\purpleD\",\"\\\\textcolor{##7854ab}{#1}\"),ca(\"\\\\purpleE\",\"\\\\textcolor{##543b78}{#1}\"),ca(\"\\\\mintA\",\"\\\\textcolor{##f5f9e8}{#1}\"),ca(\"\\\\mintB\",\"\\\\textcolor{##edf2df}{#1}\"),ca(\"\\\\mintC\",\"\\\\textcolor{##e0e5cc}{#1}\"),ca(\"\\\\grayA\",\"\\\\textcolor{##f6f7f7}{#1}\"),ca(\"\\\\grayB\",\"\\\\textcolor{##f0f1f2}{#1}\"),ca(\"\\\\grayC\",\"\\\\textcolor{##e3e5e6}{#1}\"),ca(\"\\\\grayD\",\"\\\\textcolor{##d6d8da}{#1}\"),ca(\"\\\\grayE\",\"\\\\textcolor{##babec2}{#1}\"),ca(\"\\\\grayF\",\"\\\\textcolor{##888d93}{#1}\"),ca(\"\\\\grayG\",\"\\\\textcolor{##626569}{#1}\"),ca(\"\\\\grayH\",\"\\\\textcolor{##3b3e40}{#1}\"),ca(\"\\\\grayI\",\"\\\\textcolor{##21242c}{#1}\"),ca(\"\\\\kaBlue\",\"\\\\textcolor{##314453}{#1}\"),ca(\"\\\\kaGreen\",\"\\\\textcolor{##71B307}{#1}\");var va={\"\\\\relax\":!0,\"^\":!0,_:!0,\"\\\\limits\":!0,\"\\\\nolimits\":!0},ba=function(){function t(t,e,r){this.settings=void 0,this.expansionCount=void 0,this.lexer=void 0,this.macros=void 0,this.stack=void 0,this.mode=void 0,this.settings=e,this.expansionCount=0,this.feed(t),this.macros=new ha(ma,e.macros),this.mode=r,this.stack=[]}var e=t.prototype;return e.feed=function(t){this.lexer=new sa(t,this.settings)},e.switchMode=function(t){this.mode=t},e.beginGroup=function(){this.macros.beginGroup()},e.endGroup=function(){this.macros.endGroup()},e.future=function(){return 0===this.stack.length&&this.pushToken(this.lexer.lex()),this.stack[this.stack.length-1]},e.popToken=function(){return this.future(),this.stack.pop()},e.pushToken=function(t){this.stack.push(t)},e.pushTokens=function(t){var e;(e=this.stack).push.apply(e,t)},e.consumeSpaces=function(){for(;\" \"===this.future().text;)this.stack.pop()},e.consumeArgs=function(t){for(var e=[],r=0;r<t;++r){this.consumeSpaces();var a=this.popToken();if(\"{\"===a.text){for(var n=[],i=1;0!==i;){var s=this.popToken();if(n.push(s),\"{\"===s.text)++i;else if(\"}\"===s.text)--i;else if(\"EOF\"===s.text)throw new o(\"End of input in macro argument\",a)}n.pop(),n.reverse(),e[r]=n}else{if(\"EOF\"===a.text)throw new o(\"End of input expecting macro argument\");e[r]=[a]}}return e},e.expandOnce=function(){var t=this.popToken(),e=t.text,r=this._getExpansion(e);if(null==r)return this.pushToken(t),t;if(this.expansionCount++,this.expansionCount>this.settings.maxExpand)throw new o(\"Too many expansions: infinite loop or need to increase maxExpand setting\");var a=r.tokens;if(r.numArgs)for(var n=this.consumeArgs(r.numArgs),i=(a=a.slice()).length-1;i>=0;--i){var s=a[i];if(\"#\"===s.text){if(0===i)throw new o(\"Incomplete placeholder at end of macro body\",s);if(\"#\"===(s=a[--i]).text)a.splice(i+1,1);else{if(!/^[1-9]$/.test(s.text))throw new o(\"Not a valid argument number\",s);var h;(h=a).splice.apply(h,[i,2].concat(n[+s.text-1]))}}}return this.pushTokens(a),a},e.expandAfterFuture=function(){return this.expandOnce(),this.future()},e.expandNextToken=function(){for(;;){var t=this.expandOnce();if(t instanceof n){if(\"\\\\relax\"!==t.text)return this.stack.pop();this.stack.pop()}}throw new Error},e.expandMacro=function(t){if(this.macros.get(t)){var e=[],r=this.stack.length;for(this.pushToken(new n(t));this.stack.length>r;)this.expandOnce()instanceof n&&e.push(this.stack.pop());return e}},e.expandMacroAsText=function(t){var e=this.expandMacro(t);return e?e.map((function(t){return t.text})).join(\"\"):e},e._getExpansion=function(t){var e=this.macros.get(t);if(null==e)return e;var r=\"function\"==typeof e?e(this):e;if(\"string\"==typeof r){var a=0;if(-1!==r.indexOf(\"#\"))for(var n=r.replace(/##/g,\"\");-1!==n.indexOf(\"#\"+(a+1));)++a;for(var i=new sa(r,this.settings),o=[],s=i.lex();\"EOF\"!==s.text;)o.push(s),s=i.lex();return o.reverse(),{tokens:o,numArgs:a}}return r},e.isDefined=function(t){return this.macros.has(t)||na.hasOwnProperty(t)||$.math.hasOwnProperty(t)||$.text.hasOwnProperty(t)||va.hasOwnProperty(t)},t}(),ya={\"́\":{text:\"\\\\'\",math:\"\\\\acute\"},\"̀\":{text:\"\\\\`\",math:\"\\\\grave\"},\"̈\":{text:'\\\\\"',math:\"\\\\ddot\"},\"̃\":{text:\"\\\\~\",math:\"\\\\tilde\"},\"̄\":{text:\"\\\\=\",math:\"\\\\bar\"},\"̆\":{text:\"\\\\u\",math:\"\\\\breve\"},\"̌\":{text:\"\\\\v\",math:\"\\\\check\"},\"̂\":{text:\"\\\\^\",math:\"\\\\hat\"},\"̇\":{text:\"\\\\.\",math:\"\\\\dot\"},\"̊\":{text:\"\\\\r\",math:\"\\\\mathring\"},\"̋\":{text:\"\\\\H\"}},wa={\"á\":\"á\",\"à\":\"à\",\"ä\":\"ä\",\"ǟ\":\"ǟ\",\"ã\":\"ã\",\"ā\":\"ā\",\"ă\":\"ă\",\"ắ\":\"ắ\",\"ằ\":\"ằ\",\"ẵ\":\"ẵ\",\"ǎ\":\"ǎ\",\"â\":\"â\",\"ấ\":\"ấ\",\"ầ\":\"ầ\",\"ẫ\":\"ẫ\",\"ȧ\":\"ȧ\",\"ǡ\":\"ǡ\",\"å\":\"å\",\"ǻ\":\"ǻ\",\"ḃ\":\"ḃ\",\"ć\":\"ć\",\"č\":\"č\",\"ĉ\":\"ĉ\",\"ċ\":\"ċ\",\"ď\":\"ď\",\"ḋ\":\"ḋ\",\"é\":\"é\",\"è\":\"è\",\"ë\":\"ë\",\"ẽ\":\"ẽ\",\"ē\":\"ē\",\"ḗ\":\"ḗ\",\"ḕ\":\"ḕ\",\"ĕ\":\"ĕ\",\"ě\":\"ě\",\"ê\":\"ê\",\"ế\":\"ế\",\"ề\":\"ề\",\"ễ\":\"ễ\",\"ė\":\"ė\",\"ḟ\":\"ḟ\",\"ǵ\":\"ǵ\",\"ḡ\":\"ḡ\",\"ğ\":\"ğ\",\"ǧ\":\"ǧ\",\"ĝ\":\"ĝ\",\"ġ\":\"ġ\",\"ḧ\":\"ḧ\",\"ȟ\":\"ȟ\",\"ĥ\":\"ĥ\",\"ḣ\":\"ḣ\",\"í\":\"í\",\"ì\":\"ì\",\"ï\":\"ï\",\"ḯ\":\"ḯ\",\"ĩ\":\"ĩ\",\"ī\":\"ī\",\"ĭ\":\"ĭ\",\"ǐ\":\"ǐ\",\"î\":\"î\",\"ǰ\":\"ǰ\",\"ĵ\":\"ĵ\",\"ḱ\":\"ḱ\",\"ǩ\":\"ǩ\",\"ĺ\":\"ĺ\",\"ľ\":\"ľ\",\"ḿ\":\"ḿ\",\"ṁ\":\"ṁ\",\"ń\":\"ń\",\"ǹ\":\"ǹ\",\"ñ\":\"ñ\",\"ň\":\"ň\",\"ṅ\":\"ṅ\",\"ó\":\"ó\",\"ò\":\"ò\",\"ö\":\"ö\",\"ȫ\":\"ȫ\",\"õ\":\"õ\",\"ṍ\":\"ṍ\",\"ṏ\":\"ṏ\",\"ȭ\":\"ȭ\",\"ō\":\"ō\",\"ṓ\":\"ṓ\",\"ṑ\":\"ṑ\",\"ŏ\":\"ŏ\",\"ǒ\":\"ǒ\",\"ô\":\"ô\",\"ố\":\"ố\",\"ồ\":\"ồ\",\"ỗ\":\"ỗ\",\"ȯ\":\"ȯ\",\"ȱ\":\"ȱ\",\"ő\":\"ő\",\"ṕ\":\"ṕ\",\"ṗ\":\"ṗ\",\"ŕ\":\"ŕ\",\"ř\":\"ř\",\"ṙ\":\"ṙ\",\"ś\":\"ś\",\"ṥ\":\"ṥ\",\"š\":\"š\",\"ṧ\":\"ṧ\",\"ŝ\":\"ŝ\",\"ṡ\":\"ṡ\",\"ẗ\":\"ẗ\",\"ť\":\"ť\",\"ṫ\":\"ṫ\",\"ú\":\"ú\",\"ù\":\"ù\",\"ü\":\"ü\",\"ǘ\":\"ǘ\",\"ǜ\":\"ǜ\",\"ǖ\":\"ǖ\",\"ǚ\":\"ǚ\",\"ũ\":\"ũ\",\"ṹ\":\"ṹ\",\"ū\":\"ū\",\"ṻ\":\"ṻ\",\"ŭ\":\"ŭ\",\"ǔ\":\"ǔ\",\"û\":\"û\",\"ů\":\"ů\",\"ű\":\"ű\",\"ṽ\":\"ṽ\",\"ẃ\":\"ẃ\",\"ẁ\":\"ẁ\",\"ẅ\":\"ẅ\",\"ŵ\":\"ŵ\",\"ẇ\":\"ẇ\",\"ẘ\":\"ẘ\",\"ẍ\":\"ẍ\",\"ẋ\":\"ẋ\",\"ý\":\"ý\",\"ỳ\":\"ỳ\",\"ÿ\":\"ÿ\",\"ỹ\":\"ỹ\",\"ȳ\":\"ȳ\",\"ŷ\":\"ŷ\",\"ẏ\":\"ẏ\",\"ẙ\":\"ẙ\",\"ź\":\"ź\",\"ž\":\"ž\",\"ẑ\":\"ẑ\",\"ż\":\"ż\",\"Á\":\"Á\",\"À\":\"À\",\"Ä\":\"Ä\",\"Ǟ\":\"Ǟ\",\"Ã\":\"Ã\",\"Ā\":\"Ā\",\"Ă\":\"Ă\",\"Ắ\":\"Ắ\",\"Ằ\":\"Ằ\",\"Ẵ\":\"Ẵ\",\"Ǎ\":\"Ǎ\",\"Â\":\"Â\",\"Ấ\":\"Ấ\",\"Ầ\":\"Ầ\",\"Ẫ\":\"Ẫ\",\"Ȧ\":\"Ȧ\",\"Ǡ\":\"Ǡ\",\"Å\":\"Å\",\"Ǻ\":\"Ǻ\",\"Ḃ\":\"Ḃ\",\"Ć\":\"Ć\",\"Č\":\"Č\",\"Ĉ\":\"Ĉ\",\"Ċ\":\"Ċ\",\"Ď\":\"Ď\",\"Ḋ\":\"Ḋ\",\"É\":\"É\",\"È\":\"È\",\"Ë\":\"Ë\",\"Ẽ\":\"Ẽ\",\"Ē\":\"Ē\",\"Ḗ\":\"Ḗ\",\"Ḕ\":\"Ḕ\",\"Ĕ\":\"Ĕ\",\"Ě\":\"Ě\",\"Ê\":\"Ê\",\"Ế\":\"Ế\",\"Ề\":\"Ề\",\"Ễ\":\"Ễ\",\"Ė\":\"Ė\",\"Ḟ\":\"Ḟ\",\"Ǵ\":\"Ǵ\",\"Ḡ\":\"Ḡ\",\"Ğ\":\"Ğ\",\"Ǧ\":\"Ǧ\",\"Ĝ\":\"Ĝ\",\"Ġ\":\"Ġ\",\"Ḧ\":\"Ḧ\",\"Ȟ\":\"Ȟ\",\"Ĥ\":\"Ĥ\",\"Ḣ\":\"Ḣ\",\"Í\":\"Í\",\"Ì\":\"Ì\",\"Ï\":\"Ï\",\"Ḯ\":\"Ḯ\",\"Ĩ\":\"Ĩ\",\"Ī\":\"Ī\",\"Ĭ\":\"Ĭ\",\"Ǐ\":\"Ǐ\",\"Î\":\"Î\",\"İ\":\"İ\",\"Ĵ\":\"Ĵ\",\"Ḱ\":\"Ḱ\",\"Ǩ\":\"Ǩ\",\"Ĺ\":\"Ĺ\",\"Ľ\":\"Ľ\",\"Ḿ\":\"Ḿ\",\"Ṁ\":\"Ṁ\",\"Ń\":\"Ń\",\"Ǹ\":\"Ǹ\",\"Ñ\":\"Ñ\",\"Ň\":\"Ň\",\"Ṅ\":\"Ṅ\",\"Ó\":\"Ó\",\"Ò\":\"Ò\",\"Ö\":\"Ö\",\"Ȫ\":\"Ȫ\",\"Õ\":\"Õ\",\"Ṍ\":\"Ṍ\",\"Ṏ\":\"Ṏ\",\"Ȭ\":\"Ȭ\",\"Ō\":\"Ō\",\"Ṓ\":\"Ṓ\",\"Ṑ\":\"Ṑ\",\"Ŏ\":\"Ŏ\",\"Ǒ\":\"Ǒ\",\"Ô\":\"Ô\",\"Ố\":\"Ố\",\"Ồ\":\"Ồ\",\"Ỗ\":\"Ỗ\",\"Ȯ\":\"Ȯ\",\"Ȱ\":\"Ȱ\",\"Ő\":\"Ő\",\"Ṕ\":\"Ṕ\",\"Ṗ\":\"Ṗ\",\"Ŕ\":\"Ŕ\",\"Ř\":\"Ř\",\"Ṙ\":\"Ṙ\",\"Ś\":\"Ś\",\"Ṥ\":\"Ṥ\",\"Š\":\"Š\",\"Ṧ\":\"Ṧ\",\"Ŝ\":\"Ŝ\",\"Ṡ\":\"Ṡ\",\"Ť\":\"Ť\",\"Ṫ\":\"Ṫ\",\"Ú\":\"Ú\",\"Ù\":\"Ù\",\"Ü\":\"Ü\",\"Ǘ\":\"Ǘ\",\"Ǜ\":\"Ǜ\",\"Ǖ\":\"Ǖ\",\"Ǚ\":\"Ǚ\",\"Ũ\":\"Ũ\",\"Ṹ\":\"Ṹ\",\"Ū\":\"Ū\",\"Ṻ\":\"Ṻ\",\"Ŭ\":\"Ŭ\",\"Ǔ\":\"Ǔ\",\"Û\":\"Û\",\"Ů\":\"Ů\",\"Ű\":\"Ű\",\"Ṽ\":\"Ṽ\",\"Ẃ\":\"Ẃ\",\"Ẁ\":\"Ẁ\",\"Ẅ\":\"Ẅ\",\"Ŵ\":\"Ŵ\",\"Ẇ\":\"Ẇ\",\"Ẍ\":\"Ẍ\",\"Ẋ\":\"Ẋ\",\"Ý\":\"Ý\",\"Ỳ\":\"Ỳ\",\"Ÿ\":\"Ÿ\",\"Ỹ\":\"Ỹ\",\"Ȳ\":\"Ȳ\",\"Ŷ\":\"Ŷ\",\"Ẏ\":\"Ẏ\",\"Ź\":\"Ź\",\"Ž\":\"Ž\",\"Ẑ\":\"Ẑ\",\"Ż\":\"Ż\",\"ά\":\"ά\",\"ὰ\":\"ὰ\",\"ᾱ\":\"ᾱ\",\"ᾰ\":\"ᾰ\",\"έ\":\"έ\",\"ὲ\":\"ὲ\",\"ή\":\"ή\",\"ὴ\":\"ὴ\",\"ί\":\"ί\",\"ὶ\":\"ὶ\",\"ϊ\":\"ϊ\",\"ΐ\":\"ΐ\",\"ῒ\":\"ῒ\",\"ῑ\":\"ῑ\",\"ῐ\":\"ῐ\",\"ό\":\"ό\",\"ὸ\":\"ὸ\",\"ύ\":\"ύ\",\"ὺ\":\"ὺ\",\"ϋ\":\"ϋ\",\"ΰ\":\"ΰ\",\"ῢ\":\"ῢ\",\"ῡ\":\"ῡ\",\"ῠ\":\"ῠ\",\"ώ\":\"ώ\",\"ὼ\":\"ὼ\",\"Ύ\":\"Ύ\",\"Ὺ\":\"Ὺ\",\"Ϋ\":\"Ϋ\",\"Ῡ\":\"Ῡ\",\"Ῠ\":\"Ῠ\",\"Ώ\":\"Ώ\",\"Ὼ\":\"Ὼ\"},ka=function(){function t(t,e){this.mode=void 0,this.gullet=void 0,this.settings=void 0,this.leftrightDepth=void 0,this.nextToken=void 0,this.mode=\"math\",this.gullet=new ba(t,e,this.mode),this.settings=e,this.leftrightDepth=0}var e=t.prototype;return e.expect=function(t,e){if(void 0===e&&(e=!0),this.fetch().text!==t)throw new o(\"Expected '\"+t+\"', got '\"+this.fetch().text+\"'\",this.fetch());e&&this.consume()},e.consume=function(){this.nextToken=null},e.fetch=function(){return null==this.nextToken&&(this.nextToken=this.gullet.expandNextToken()),this.nextToken},e.switchMode=function(t){this.mode=t,this.gullet.switchMode(t)},e.parse=function(){this.gullet.beginGroup(),this.settings.colorIsTextColor&&this.gullet.macros.set(\"\\\\color\",\"\\\\textcolor\");var t=this.parseExpression(!1);return this.expect(\"EOF\"),this.gullet.endGroup(),t},e.parseExpression=function(e,r){for(var a=[];;){\"math\"===this.mode&&this.consumeSpaces();var n=this.fetch();if(-1!==t.endOfExpression.indexOf(n.text))break;if(r&&n.text===r)break;if(e&&na[n.text]&&na[n.text].infix)break;var i=this.parseAtom(r);if(!i)break;a.push(i)}return\"text\"===this.mode&&this.formLigatures(a),this.handleInfixNodes(a)},e.handleInfixNodes=function(t){for(var e,r=-1,a=0;a<t.length;a++){var n=Vt(t[a],\"infix\");if(n){if(-1!==r)throw new o(\"only one infix operator per group\",n.token);r=a,e=n.replaceWith}}if(-1!==r&&e){var i,s,h=t.slice(0,r),l=t.slice(r+1);return i=1===h.length&&\"ordgroup\"===h[0].type?h[0]:{type:\"ordgroup\",mode:this.mode,body:h},s=1===l.length&&\"ordgroup\"===l[0].type?l[0]:{type:\"ordgroup\",mode:this.mode,body:l},[\"\\\\\\\\abovefrac\"===e?this.callFunction(e,[i,t[r],s],[]):this.callFunction(e,[i,s],[])]}return t},e.handleSupSubscript=function(e){var r=this.fetch(),a=r.text;this.consume();var n=this.parseGroup(e,!1,t.SUPSUB_GREEDINESS,void 0,void 0,!0);if(!n)throw new o(\"Expected group after '\"+a+\"'\",r);return n},e.formatUnsupportedCmd=function(t){for(var e=[],r=0;r<t.length;r++)e.push({type:\"textord\",mode:\"text\",text:t[r]});var a={type:\"text\",mode:this.mode,body:e};return{type:\"color\",mode:this.mode,color:this.settings.errorColor,body:[a]}},e.parseAtom=function(t){var e,r,a=this.parseGroup(\"atom\",!1,null,t);if(\"text\"===this.mode)return a;for(;;){this.consumeSpaces();var n=this.fetch();if(\"\\\\limits\"===n.text||\"\\\\nolimits\"===n.text){var i=Vt(a,\"op\");if(i){var s=\"\\\\limits\"===n.text;i.limits=s,i.alwaysHandleSupSub=!0}else{if(!(i=Vt(a,\"operatorname\"))||!i.alwaysHandleSupSub)throw new o(\"Limit controls must follow a math operator\",n);var h=\"\\\\limits\"===n.text;i.limits=h}this.consume()}else if(\"^\"===n.text){if(e)throw new o(\"Double superscript\",n);e=this.handleSupSubscript(\"superscript\")}else if(\"_\"===n.text){if(r)throw new o(\"Double subscript\",n);r=this.handleSupSubscript(\"subscript\")}else{if(\"'\"!==n.text)break;if(e)throw new o(\"Double superscript\",n);var l={type:\"textord\",mode:this.mode,text:\"\\\\prime\"},m=[l];for(this.consume();\"'\"===this.fetch().text;)m.push(l),this.consume();\"^\"===this.fetch().text&&m.push(this.handleSupSubscript(\"superscript\")),e={type:\"ordgroup\",mode:this.mode,body:m}}}return e||r?{type:\"supsub\",mode:this.mode,base:a,sup:e,sub:r}:a},e.parseFunction=function(t,e,r){var a=this.fetch(),n=a.text,i=na[n];if(!i)return null;if(this.consume(),null!=r&&i.greediness<=r)throw new o(\"Got function '\"+n+\"' with no arguments\"+(e?\" as \"+e:\"\"),a);if(\"text\"===this.mode&&!i.allowedInText)throw new o(\"Can't use function '\"+n+\"' in text mode\",a);if(\"math\"===this.mode&&!1===i.allowedInMath)throw new o(\"Can't use function '\"+n+\"' in math mode\",a);var s=this.parseArguments(n,i),h=s.args,l=s.optArgs;return this.callFunction(n,h,l,a,t)},e.callFunction=function(t,e,r,a,n){var i={funcName:t,parser:this,token:a,breakOnTokenText:n},s=na[t];if(s&&s.handler)return s.handler(i,e,r);throw new o(\"No function handler for \"+t)},e.parseArguments=function(t,e){var r=e.numArgs+e.numOptionalArgs;if(0===r)return{args:[],optArgs:[]};for(var a=e.greediness,n=[],i=[],s=0;s<r;s++){var h=e.argTypes&&e.argTypes[s],l=s<e.numOptionalArgs,m=s>0&&!l||0===s&&!l&&\"math\"===this.mode,c=this.parseGroupOfType(\"argument to '\"+t+\"'\",h,l,a,m);if(!c){if(l){i.push(null);continue}throw new o(\"Expected group after '\"+t+\"'\",this.fetch())}(l?i:n).push(c)}return{args:n,optArgs:i}},e.parseGroupOfType=function(t,e,r,a,n){switch(e){case\"color\":return n&&this.consumeSpaces(),this.parseColorGroup(r);case\"size\":return n&&this.consumeSpaces(),this.parseSizeGroup(r);case\"url\":return this.parseUrlGroup(r,n);case\"math\":case\"text\":return this.parseGroup(t,r,a,void 0,e,n);case\"hbox\":var i=this.parseGroup(t,r,a,void 0,\"text\",n);return i?{type:\"styling\",mode:i.mode,body:[i],style:\"text\"}:i;case\"raw\":if(n&&this.consumeSpaces(),r&&\"{\"===this.fetch().text)return null;var s=this.parseStringGroup(\"raw\",r,!0);if(s)return{type:\"raw\",mode:\"text\",string:s.text};throw new o(\"Expected raw group\",this.fetch());case\"original\":case null:case void 0:return this.parseGroup(t,r,a,void 0,void 0,n);default:throw new o(\"Unknown group type as \"+t,this.fetch())}},e.consumeSpaces=function(){for(;\" \"===this.fetch().text;)this.consume()},e.parseStringGroup=function(t,e,r){var a=e?\"[\":\"{\",n=e?\"]\":\"}\",i=this.fetch();if(i.text!==a){if(e)return null;if(r&&\"EOF\"!==i.text&&/[^{}[\\]]/.test(i.text))return this.consume(),i}var s=this.mode;this.mode=\"text\",this.expect(a);for(var h,l=\"\",m=this.fetch(),c=0,u=m;(h=this.fetch()).text!==n||r&&c>0;){switch(h.text){case\"EOF\":throw new o(\"Unexpected end of input in \"+t,m.range(u,l));case a:c++;break;case n:c--}l+=(u=h).text,this.consume()}return this.expect(n),this.mode=s,m.range(u,l)},e.parseRegexGroup=function(t,e){var r=this.mode;this.mode=\"text\";for(var a,n=this.fetch(),i=n,s=\"\";\"EOF\"!==(a=this.fetch()).text&&t.test(s+a.text);)s+=(i=a).text,this.consume();if(\"\"===s)throw new o(\"Invalid \"+e+\": '\"+n.text+\"'\",n);return this.mode=r,n.range(i,s)},e.parseColorGroup=function(t){var e=this.parseStringGroup(\"color\",t);if(!e)return null;var r=/^(#[a-f0-9]{3}|#?[a-f0-9]{6}|[a-z]+)$/i.exec(e.text);if(!r)throw new o(\"Invalid color: '\"+e.text+\"'\",e);var a=r[0];return/^[0-9a-f]{6}$/i.test(a)&&(a=\"#\"+a),{type:\"color-token\",mode:this.mode,color:a}},e.parseSizeGroup=function(t){var e,r=!1;if(!(e=t||\"{\"===this.fetch().text?this.parseStringGroup(\"size\",t):this.parseRegexGroup(/^[-+]? *(?:$|\\d+|\\d+\\.\\d*|\\.\\d*) *[a-z]{0,2} *$/,\"size\")))return null;t||0!==e.text.length||(e.text=\"0pt\",r=!0);var a=/([-+]?) *(\\d+(?:\\.\\d*)?|\\.\\d+) *([a-z]{2})/.exec(e.text);if(!a)throw new o(\"Invalid size: '\"+e.text+\"'\",e);var n={number:+(a[1]+a[2]),unit:a[3]};if(!At(n))throw new o(\"Invalid unit: '\"+n.unit+\"'\",e);return{type:\"size\",mode:this.mode,value:n,isBlank:r}},e.parseUrlGroup=function(t,e){this.gullet.lexer.setCatcode(\"%\",13);var r=this.parseStringGroup(\"url\",t,!0);if(this.gullet.lexer.setCatcode(\"%\",14),!r)return null;var a=r.text.replace(/\\\\([#$%&~_^{}])/g,\"$1\");return{type:\"url\",mode:this.mode,url:a}},e.parseGroup=function(e,r,n,i,s,h){var l=this.mode;s&&this.switchMode(s),h&&this.consumeSpaces();var m,c=this.fetch(),u=c.text;if(r?\"[\"===u:\"{\"===u||\"\\\\begingroup\"===u){this.consume();var p=t.endOfGroup[u];this.gullet.beginGroup();var d=this.parseExpression(!1,p),f=this.fetch();this.expect(p),this.gullet.endGroup(),m={type:\"ordgroup\",mode:this.mode,loc:a.range(c,f),body:d,semisimple:\"\\\\begingroup\"===u||void 0}}else if(r)m=null;else if(null==(m=this.parseFunction(i,e,n)||this.parseSymbol())&&\"\\\\\"===u[0]&&!va.hasOwnProperty(u)){if(this.settings.throwOnError)throw new o(\"Undefined control sequence: \"+u,c);m=this.formatUnsupportedCmd(u),this.consume()}return s&&this.switchMode(l),m},e.formLigatures=function(t){for(var e=t.length-1,r=0;r<e;++r){var n=t[r],i=n.text;\"-\"===i&&\"-\"===t[r+1].text&&(r+1<e&&\"-\"===t[r+2].text?(t.splice(r,3,{type:\"textord\",mode:\"text\",loc:a.range(n,t[r+2]),text:\"---\"}),e-=2):(t.splice(r,2,{type:\"textord\",mode:\"text\",loc:a.range(n,t[r+1]),text:\"--\"}),e-=1)),\"'\"!==i&&\"`\"!==i||t[r+1].text!==i||(t.splice(r,2,{type:\"textord\",mode:\"text\",loc:a.range(n,t[r+1]),text:i+i}),e-=1)}},e.parseSymbol=function(){var t=this.fetch(),e=t.text;if(/^\\\\verb[^a-zA-Z]/.test(e)){this.consume();var r=e.slice(5),n=\"*\"===r.charAt(0);if(n&&(r=r.slice(1)),r.length<2||r.charAt(0)!==r.slice(-1))throw new o(\"\\\\verb assertion failed --\\n                    please report what input caused this bug\");return{type:\"verb\",mode:\"text\",body:r=r.slice(1,-1),star:n}}wa.hasOwnProperty(e[0])&&!$[this.mode][e[0]]&&(this.settings.strict&&\"math\"===this.mode&&this.settings.reportNonstrict(\"unicodeTextInMathMode\",'Accented Unicode text character \"'+e[0]+'\" used in math mode',t),e=wa[e[0]]+e.substr(1));var i,s=oa.exec(e);if(s&&(\"i\"===(e=e.substring(0,s.index))?e=\"ı\":\"j\"===e&&(e=\"ȷ\")),$[this.mode][e]){this.settings.strict&&\"math\"===this.mode&&\"ÇÐÞçþ\".indexOf(e)>=0&&this.settings.reportNonstrict(\"unicodeTextInMathMode\",'Latin-1/Unicode text character \"'+e[0]+'\" used in math mode',t);var h,l=$[this.mode][e].group,m=a.range(t);if(_.hasOwnProperty(l)){var c=l;h={type:\"atom\",mode:this.mode,family:c,loc:m,text:e}}else h={type:l,mode:this.mode,loc:m,text:e};i=h}else{if(!(e.charCodeAt(0)>=128))return null;this.settings.strict&&(M(e.charCodeAt(0))?\"math\"===this.mode&&this.settings.reportNonstrict(\"unicodeTextInMathMode\",'Unicode text character \"'+e[0]+'\" used in math mode',t):this.settings.reportNonstrict(\"unknownSymbol\",'Unrecognized Unicode character \"'+e[0]+'\" ('+e.charCodeAt(0)+\")\",t)),i={type:\"textord\",mode:\"text\",loc:a.range(t),text:e}}if(this.consume(),s)for(var u=0;u<s[0].length;u++){var p=s[0][u];if(!ya[p])throw new o(\"Unknown accent ' \"+p+\"'\",t);var d=ya[p][this.mode];if(!d)throw new o(\"Accent \"+p+\" unsupported in \"+this.mode+\" mode\",t);i={type:\"accent\",mode:this.mode,loc:a.range(t),label:d,isStretchy:!1,isShifty:!0,base:i}}return i},t}();ka.endOfExpression=[\"}\",\"\\\\endgroup\",\"\\\\end\",\"\\\\right\",\"&\"],ka.endOfGroup={\"[\":\"]\",\"{\":\"}\",\"\\\\begingroup\":\"\\\\endgroup\"},ka.SUPSUB_GREEDINESS=1;var Sa=function(t,e){if(!(\"string\"==typeof t||t instanceof String))throw new TypeError(\"KaTeX can only parse string typed expression\");var r=new ka(t,e);delete r.gullet.macros.current[\"\\\\df@tag\"];var a=r.parse();if(r.gullet.macros.get(\"\\\\df@tag\")){if(!e.displayMode)throw new o(\"\\\\tag works only in display equations\");r.gullet.feed(\"\\\\df@tag\"),a=[{type:\"tag\",mode:\"text\",body:a,tag:r.parse()}]}return a},Ma=function(t,e,r){e.textContent=\"\";var a=Aa(t,r).toNode();e.appendChild(a)};\"undefined\"!=typeof document&&\"CSS1Compat\"!==document.compatMode&&(\"undefined\"!=typeof console&&console.warn(\"Warning: KaTeX doesn't work in quirks mode. Make sure your website has a suitable doctype.\"),Ma=function(){throw new o(\"KaTeX doesn't work in quirks mode.\")});var za=function(t,e,r){if(r.throwOnError||!(t instanceof o))throw t;var a=Dt.makeSpan([\"katex-error\"],[new E(e)]);return a.setAttribute(\"title\",t.toString()),a.setAttribute(\"style\",\"color:\"+r.errorColor),a},Aa=function(t,e){var r=new u(e);try{var a=Sa(t,r);return Be(a,t,r)}catch(e){return za(e,t,r)}},Ta={version:\"0.11.1\",render:Ma,renderToString:function(t,e){return Aa(t,e).toMarkup()},ParseError:o,__parse:function(t,e){var r=new u(e);return Sa(t,r)},__renderToDomTree:Aa,__renderToHTMLTree:function(t,e){var r=new u(e);try{return function(t,e,r){var a=de(t,Ae(r)),n=Dt.makeSpan([\"katex\"],[a]);return Te(n,r)}(Sa(t,r),0,r)}catch(e){return za(e,t,r)}},__setFontMetrics:function(t,e){F[t]=e},__defineSymbol:j,__defineMacro:ca,__domTree:{Span:N,Anchor:O,SymbolNode:E,SvgNode:L,PathNode:P,LineNode:H}};e.default=Ta}]).default},t.exports=a()},function(t,e,r){\"use strict\";r.r(e);var a=r(0),n=r.n(a);let i={throwOnError:!1,displayMode:!1},o={throwOnError:!1,displayMode:!0};const s=/c194a9eb/g,h=/c194a9ec/g,l=/c194a9ed/g,m=/c194a9ee/g,c=/c194a9ef/g,u=/c194a9eg<!-- begin-inline-katex([\\s\\S]*?)end-inline-katex-->/g,p=/<!-- begin-block-katex([\\s\\S]*?)end-block-katex-->/g;$docsify.plugins=[].concat((function(t){t.beforeEach(t=>{let e=t.replace(/<code>(.*)<\\/code>/g,(function(t,e){return`<code>${e.replace(/`/g,\"c194a9ec\")}</code>`})).replace(/\\$`\\$/g,\"c194a9ed\").replace(/\\\\`\\{/g,\"c194a9ee\").replace(/\\\\\\$/g,\"c194a9eb\").replace(/`[^`]*`/g,(function(t){return t.replace(/\\$/g,\"c194a9ef\")})).replace(h,\"`\");return e=e.replace(l,\"$ `$\").replace(m,\"\\\\`{\"),e=e.replace(/(\\$\\$)([\\s\\S]*?)(\\$\\$)/g,(function(t,e,r){return\"\\x3c!-- begin-block-katex\"+r+\"end-block-katex--\\x3e\"})).replace(/(\\$)([\\s\\S]*?)(\\$)/g,(function(t,e,r){return\"c194a9eg\\x3c!-- begin-inline-katex\"+r.replace(s,\"\\\\$\")+\"end-inline-katex--\\x3e\"})).replace(s,\"\\\\$\"),e}),t.afterEach((function(t,e){let r=t.replace(u,(function(t,e){return n.a.renderToString(e,i)}));r=r.replace(p,(function(t,e){return n.a.renderToString(e,o)})),e(r.replace(c,\"$\"))}))}),$docsify.plugins)}]);"
  },
  {
    "path": "asset/docsify-quick-page.css",
    "content": "#prev-page-button {\n\tposition:fixed;\n\ttop:140px;\n\twidth: 35px;\n\theight: 35px;\n\tright: 15px;\n\tbackground-color: transparent;\n\tbackground-image: url(left.svg);\n\tbackground-repeat: no-repeat;\n    background-size: cover;\n\tborder:0;\n\t-webkit-user-select: none;\n    -moz-user-select: none;\n    -ms-user-select: none;\n    user-select: none;\n\toutline:none;\n\tcursor: pointer;\n}\n\n#next-page-button {\n\tposition:fixed;\n\ttop:180px;\n\twidth:35px;\n\theight:35px;\n\tright:15px;\n\tbackground-color: transparent;\n\tbackground-image: url(right.svg);\n\tbackground-repeat: no-repeat;\n    background-size: cover;\n\tborder:0;\n\t-webkit-user-select: none;\n    -moz-user-select: none;\n    -ms-user-select: none;\n    user-select: none;\n\toutline:none;\n\tcursor: pointer;\n}"
  },
  {
    "path": "asset/docsify-quick-page.js",
    "content": "document.addEventListener('DOMContentLoaded', function() {\t\n\tvar prevBtn = document.createElement(\"div\")\n\tprevBtn.id = \"prev-page-button\"\n\tdocument.body.appendChild(prevBtn)\n\tvar nextBtn = document.createElement(\"div\");\n\tnextBtn.id = \"next-page-button\"\n    document.body.appendChild(nextBtn)\n\n    var links = null\n\tvar linkMap = null\n\tvar getCurIdx = function() {\n\t\tif (!links) {\n\t\t\tlinks = Array\n\t\t\t\t.from(document.querySelectorAll(\".sidebar-nav a\"))\n\t\t\t\t.map(x => x.href)\n\t\t\tlinkMap = {}\n\t\t\tlinks.forEach((x, i) => linkMap[x] = i)\n\t\t}\n\t\t\n\t\tvar elem = document.querySelector(\".active a\")\n\t\tvar curIdx = elem? linkMap[elem.href]: -1\n\t\treturn curIdx\n\t}\n\n\tprevBtn.addEventListener('click', function () {\n\t\tif (!document.body.classList.contains('ready'))\n\t\t\treturn\n\t\tvar curIdx = getCurIdx()\n\t\tlocation.href = curIdx == -1? \n\t\t\tlinks[0]: \n\t\t\tlinks[(curIdx - 1 + links.length) % links.length]\n\t\tdocument.body.scrollIntoView()\n\t}, false)\n\t\n\tnextBtn.addEventListener('click', function () {\n\t\tif (!document.body.classList.contains('ready'))\n\t\t\treturn\n\t\tvar curIdx = getCurIdx()\n\t\tlocation.href = links[(curIdx + 1) % links.length]\n\t\tdocument.body.scrollIntoView()\n\t}, false)\n})"
  },
  {
    "path": "asset/edit.css",
    "content": "#edit-btn {\n\tposition: fixed;\n    right: 15px;\n    top: 260px;\n    width: 35px;\n    height: 35px;\n    background-repeat: no-repeat;\n    background-size: cover;\n    cursor: pointer;\n    -webkit-user-select: none;\n    -moz-user-select: none;\n    -ms-user-select: none;\n    user-select: none;\n    background-image: url(edit.svg);\n}"
  },
  {
    "path": "asset/edit.js",
    "content": "document.addEventListener('DOMContentLoaded', function() {\n\tvar editBtn = document.createElement('div')\n\teditBtn.id = 'edit-btn'\n\tdocument.body.append(editBtn)\n\t\n\tvar repo = window.$docsify.repo\n\teditBtn.addEventListener('click', function() {\n\t\tif (!repo) return\n\t\tif (!/https?:\\/\\//.exec(repo))\n\t\t\trepo = 'https://github.com/' + repo\n\t\tvar url = repo + '/tree/master' + \n\t\t\t      location.hash.slice(1) + '.md'\n\t\twindow.open(url)\n\t})\n})"
  },
  {
    "path": "asset/prism-darcula.css",
    "content": "/**\n * Darcula theme\n *\n * Adapted from a theme based on:\n * IntelliJ Darcula Theme (https://github.com/bulenkov/Darcula)\n *\n * @author Alexandre Paradis <service.paradis@gmail.com>\n * @version 1.0\n */\n\ncode[class*=\"lang-\"],\npre[data-lang] {\n    color: #a9b7c6 !important;\n    background-color: #2b2b2b !important;\n    font-family: Consolas, Monaco, 'Andale Mono', monospace;\n    direction: ltr;\n    text-align: left;\n    white-space: pre;\n    word-spacing: normal;\n    word-break: normal;\n    line-height: 1.5;\n\n    -moz-tab-size: 4;\n    -o-tab-size: 4;\n    tab-size: 4;\n\n    -webkit-hyphens: none;\n    -moz-hyphens: none;\n    -ms-hyphens: none;\n    hyphens: none;\n}\n\npre[data-lang]::-moz-selection, pre[data-lang] ::-moz-selection,\ncode[class*=\"lang-\"]::-moz-selection, code[class*=\"lang-\"] ::-moz-selection {\n    color: inherit;\n    background: rgba(33, 66, 131, .85);\n}\n\npre[data-lang]::selection, pre[data-lang] ::selection,\ncode[class*=\"lang-\"]::selection, code[class*=\"lang-\"] ::selection {\n    color: inherit;\n    background: rgba(33, 66, 131, .85);\n}\n\n/* Code blocks */\npre[data-lang] {\n    padding: 1em;\n    margin: .5em 0;\n    overflow: auto;\n}\n\n:not(pre) > code[class*=\"lang-\"],\npre[data-lang] {\n    background: #2b2b2b;\n}\n\n/* Inline code */\n:not(pre) > code[class*=\"lang-\"] {\n    padding: .1em;\n    border-radius: .3em;\n}\n\n.token.comment,\n.token.prolog,\n.token.cdata {\n    color: #808080;\n}\n\n.token.delimiter,\n.token.boolean,\n.token.keyword,\n.token.selector,\n.token.important,\n.token.atrule {\n    color: #cc7832;\n}\n\n.token.operator,\n.token.punctuation,\n.token.attr-name {\n    color: #a9b7c6;\n}\n\n.token.tag,\n.token.tag .punctuation,\n.token.doctype,\n.token.builtin {\n    color: #e8bf6a;\n}\n\n.token.entity,\n.token.number,\n.token.symbol {\n    color: #6897bb;\n}\n\n.token.property,\n.token.constant,\n.token.variable {\n    color: #9876aa;\n}\n\n.token.string,\n.token.char {\n    color: #6a8759;\n}\n\n.token.attr-value,\n.token.attr-value .punctuation {\n    color: #a5c261;\n}\n\n.token.attr-value .punctuation:first-child {\n    color: #a9b7c6;\n}\n\n.token.url {\n    color: #287bde;\n    text-decoration: underline;\n}\n\n.token.function {\n    color: #ffc66d;\n}\n\n.token.regex {\n    background: #364135;\n}\n\n.token.bold {\n    font-weight: bold;\n}\n\n.token.italic {\n    font-style: italic;\n}\n\n.token.inserted {\n    background: #294436;\n}\n\n.token.deleted {\n    background: #484a4a;\n}\n\ncode.lang-css .token.property,\ncode.lang-css .token.property + .token.punctuation {\n    color: #a9b7c6;\n}\n\ncode.lang-css .token.id {\n    color: #ffc66d;\n}\n\ncode.lang-css .token.selector > .token.class,\ncode.lang-css .token.selector > .token.attribute,\ncode.lang-css .token.selector > .token.pseudo-class,\ncode.lang-css .token.selector > .token.pseudo-element {\n    color: #ffc66d;\n}"
  },
  {
    "path": "asset/share.css",
    "content": "#share-btn {\n\tposition: fixed;\n\tright: 15px;\n\ttop: 220px;\n\twidth: 35px;\n\theight: 35px;\n\tbackground-repeat: no-repeat;\n\tbackground-size: cover;\n\tcursor: pointer;\n\t-webkit-user-select: none;\n\t-moz-user-select: none;\n\t-ms-user-select: none;\n\tuser-select: none;\n\tbackground-image: url('share.svg');\n}"
  },
  {
    "path": "asset/share.js",
    "content": "document.addEventListener('DOMContentLoaded', function() {\n    var shareBtn = document.createElement('a')\n    shareBtn.id = 'share-btn'\n    shareBtn.className = 'bdsharebuttonbox'\n    shareBtn.setAttribute('data-cmd', 'more')\n    document.body.append(shareBtn)\n    \n    window._bd_share_config = {\n        \"common\":{\n            \"bdSnsKey\":{},\n            \"bdText\":\"\",\n            \"bdMini\":\"1\",\n            \"bdMiniList\":false,\n            \"bdPic\":\"\",\n            \"bdStyle\":\"2\",\n            \"bdSize\":\"16\"\n        },\n        \"share\":{}\n    }\n})\n\n// https://bdimg.share.baidu.com/static/api/js/share.js\nwindow._bd_share_main?window._bd_share_is_recently_loaded=!0:(window._bd_share_is_recently_loaded=!1,window._bd_share_main={version:\"2.0\",jscfg:{domain:{staticUrl:\"https://bdimg.share.baidu.com/\"}}}),!window._bd_share_is_recently_loaded&&(window._bd_share_main.F=window._bd_share_main.F||function(e,t){function r(e,t){if(e instanceof Array){for(var n=0,r=e.length;n<r;n++)if(t.call(e[n],e[n],n)===!1)return}else for(var n in e)if(e.hasOwnProperty(n)&&t.call(e[n],e[n],n)===!1)return}function i(e,t){this.svnMod=\"\",this.name=null,this.path=e,this.fn=null,this.exports={},this._loaded=!1,this._requiredStack=[],this._readyStack=[],i.cache[this.path]=this;if(t&&t.charAt(0)!==\".\"){var n=t.split(\":\");n.length>1?(this.svnMod=n[0],this.name=n[1]):this.name=t}this.svnMod||(this.svnMod=this.path.split(\"/js/\")[0].substr(1)),this.type=\"js\",this.getKey=function(){return this.svnMod+\":\"+this.name},this._info={}}function o(e,t){var n=t==\"css\",r=document.createElement(n?\"link\":\"script\");return r}function u(t,n,r,i){function c(){c.isCalled||(c.isCalled=!0,clearTimeout(l),r&&r())}var s=o(t,n);s.nodeName===\"SCRIPT\"?a(s,c):f(s,c);var l=setTimeout(function(){throw new Error(\"load \"+n+\" timeout : \"+t)},e._loadScriptTimeout||1e4),h=document.getElementsByTagName(\"head\")[0];n==\"css\"?(s.rel=\"stylesheet\",s.href=t,h.appendChild(s)):(s.type=\"text/javascript\",s.src=t,h.insertBefore(s,h.firstChild))}function a(e,t){e.onload=e.onerror=e.onreadystatechange=function(){if(/loaded|complete|undefined/.test(e.readyState)){e.onload=e.onerror=e.onreadystatechange=null;if(e.parentNode){e.parentNode.removeChild(e);try{if(e.clearAttributes)e.clearAttributes();else for(var n in e)delete e[n]}catch(r){}}e=undefined,t&&t()}}}function f(e,t){e.attachEvent?e.attachEvent(\"onload\",t):setTimeout(function(){l(e,t)},0)}function l(e,t){if(t&&t.isCalled)return;var n,r=navigator.userAgent,i=~r.indexOf(\"AppleWebKit\"),s=~r.indexOf(\"Opera\");if(i||s)e.sheet&&(n=!0);else if(e.sheet)try{e.sheet.cssRules&&(n=!0)}catch(o){if(o.name===\"SecurityError\"||o.name===\"NS_ERROR_DOM_SECURITY_ERR\")n=!0}setTimeout(function(){n?t&&t():l(e,t)},1)}var n=\"api\";e.each=r,i.currentPath=\"\",i.loadedPaths={},i.loadingPaths={},i.cache={},i.paths={},i.handlers=[],i.moduleFileMap={},i.requiredPaths={},i.lazyLoadPaths={},i.services={},i.isPathsLoaded=function(e){var t=!0;return r(e,function(e){if(!(e in i.loadedPaths))return t=!1}),t},i.require=function(e,t){e.search(\":\")<0&&(t||(t=n,i.currentPath&&(t=i.currentPath.split(\"/js/\")[0].substr(1))),e=t+\":\"+e);var r=i.get(e,i.currentPath);if(r.type==\"css\")return;if(r){if(!r._inited){r._inited=!0;var s,o=r.svnMod;if(s=r.fn.call(null,function(e){return i.require(e,o)},r.exports,new h(r.name,o)))r.exports=s}return r.exports}throw new Error('Module \"'+e+'\" not found!')},i.baseUrl=t?t[t.length-1]==\"/\"?t:t+\"/\":\"/\",i.getBasePath=function(e){var t,n;return(n=e.indexOf(\"/\"))!==-1&&(t=e.slice(0,n)),t&&t in i.paths?i.paths[t]:i.baseUrl},i.getJsPath=function(t,r){if(t.charAt(0)===\".\"){r=r.replace(/\\/[^\\/]+\\/[^\\/]+$/,\"\"),t.search(\"./\")===0&&(t=t.substr(2));var s=0;t=t.replace(/^(\\.\\.\\/)+/g,function(e){return s=e.length/3,\"\"});while(s>0)r=r.substr(0,r.lastIndexOf(\"/\")),s--;return r+\"/\"+t+\"/\"+t.substr(t.lastIndexOf(\"/\")+1)+\".js\"}var o,u,a,f,l,c;if(t.search(\":\")>=0){var h=t.split(\":\");o=h[0],t=h[1]}else r&&(o=r.split(\"/\")[1]);o=o||n;var p=/\\.css(?:\\?|$)/i.test(t);p&&e._useConfig&&i.moduleFileMap[o][t]&&(t=i.moduleFileMap[o][t]);var t=l=t,d=i.getBasePath(t);return(a=t.indexOf(\"/\"))!==-1&&(u=t.slice(0,a),f=t.lastIndexOf(\"/\"),l=t.slice(f+1)),u&&u in i.paths&&(t=t.slice(a+1)),c=d+o+\"/js/\"+t+\".js\",c},i.get=function(e,t){var n=i.getJsPath(e,t);return i.cache[n]?i.cache[n]:new i(n,e)},i.prototype={load:function(){i.loadingPaths[this.path]=!0;var t=this.svnMod||n,r=window._bd_share_main.jscfg.domain.staticUrl+\"static/\"+t+\"/\",o=this,u=/\\.css(?:\\?|$)/i.test(this.name);this.type=u?\"css\":\"js\";var a=\"/\"+this.type+\"/\"+i.moduleFileMap[t][this.name];e._useConfig&&i.moduleFileMap[t][this.name]?r+=this.type+\"/\"+i.moduleFileMap[t][this.name]:r+=this.type+\"/\"+this.name+(u?\"\":\".js\");if(e._firstScreenCSS.indexOf(this.name)>0||e._useConfig&&a==e._firstScreenJS)o._loaded=!0,o.ready();else{var f=(new Date).getTime();s.create({src:r,type:this.type,loaded:function(){o._info.loadedTime=(new Date).getTime()-f,o.type==\"css\"&&(o._loaded=!0,o.ready())}})}},lazyLoad:function(){var e=this.name;if(i.lazyLoadPaths[this.getKey()])this.define(),delete i.lazyLoadPaths[this.getKey()];else{if(this.exist())return;i.requiredPaths[this.getKey()]=!0,this.load()}},ready:function(e,t){var n=t?this._requiredStack:this._readyStack;if(e)this._loaded?e():n.push(e);else{i.loadedPaths[this.path]=!0,delete i.loadingPaths[this.path],this._loaded=!0,i.currentPath=this.path;if(this._readyStack&&this._readyStack.length>0){this._inited=!0;var s,o=this.svnMod;this.fn&&(s=this.fn.call(null,function(e){return i.require(e,o)},this.exports,new h(this.name,o)))&&(this.exports=s),r(this._readyStack,function(e){e()}),delete this._readyStack}this._requiredStack&&this._requiredStack.length>0&&(r(this._requiredStack,function(e){e()}),delete this._requiredStack)}},define:function(){var e=this,t=this.deps,n=this.path,s=[];t||(t=this.getDependents()),t.length?(r(t,function(t){s.push(i.getJsPath(t,e.path))}),r(t,function(t){var n=i.get(t,e.path);n.ready(function(){i.isPathsLoaded(s)&&e.ready()},!0),n.lazyLoad()})):this.ready()},exist:function(){var e=this.path;return e in i.loadedPaths||e in i.loadingPaths},getDependents:function(){var e=this,t=this.fn.toString(),n=t.match(/function\\s*\\(([^,]*),/i),i=new RegExp(\"[^.]\\\\b\"+n[1]+\"\\\\(\\\\s*('|\\\")([^()\\\"']*)('|\\\")\\\\s*\\\\)\",\"g\"),s=t.match(i),o=[];return s&&r(s,function(e,t){o[t]=e.substr(n[1].length+3).slice(0,-2)}),o}};var s={create:function(e){var t=e.src;if(t in this._paths)return;this._paths[t]=!0,r(this._rules,function(e){t=e.call(null,t)}),u(t,e.type,e.loaded)},_paths:{},_rules:[],addPathRule:function(e){this._rules.push(e)}};e.version=\"1.0\",e.use=function(e,t){typeof e==\"string\"&&(e=[e]);var n=[],s=[];r(e,function(e,t){s[t]=!1}),r(e,function(e,o){var u=i.get(e),a=u._loaded;u.ready(function(){var e=u.exports||{};e._INFO=u._info,e._INFO&&(e._INFO.isNew=!a),n[o]=e,s[o]=!0;var i=!0;r(s,function(e){if(e===!1)return i=!1}),t&&i&&t.apply(null,n)}),u.lazyLoad()})},e.module=function(e,t,n){var r=i.get(e);r.fn=t,r.deps=n,i.requiredPaths[r.getKey()]?r.define():i.lazyLoadPaths[r.getKey()]=!0},e.pathRule=function(e){s.addPathRule(e)},e._addPath=function(e,t){t.slice(-1)!==\"/\"&&(t+=\"/\");if(e in i.paths)throw new Error(e+\" has already in Module.paths\");i.paths[e]=t};var c=n;e._setMod=function(e){c=e||n},e._fileMap=function(t,n){if(typeof t==\"object\")r(t,function(t,n){e._fileMap(n,t)});else{var s=c;typeof n==\"string\"&&(n=[n]),t=t.indexOf(\"js/\")==1?t.substr(4):t,t=t.indexOf(\"css/\")==1?t.substr(5):t;var o=i.moduleFileMap[s];o||(o={}),r(n,function(e){o[e]||(o[e]=t)}),i.moduleFileMap[s]=o}},e._eventMap={},e.call=function(t,n,r){var i=[];for(var s=2,o=arguments.length;s<o;s++)i.push(arguments[s]);e.use(t,function(e){var t=n.split(\".\");for(var r=0,s=t.length;r<s;r++)e=e[t[r]];e&&e.apply(this,i)})},e._setContext=function(e){typeof e==\"object\"&&r(e,function(e,t){h.prototype[t]=i.require(e)})},e._setContextMethod=function(e,t){h.prototype[e]=t};var h=function(e,t){this.modName=e,this.svnMod=t};return h.prototype={domain:window._bd_share_main.jscfg.domain,use:function(t,n){typeof t==\"string\"&&(t=[t]);for(var r=t.length-1;r>=0;r--)t[r]=this.svnMod+\":\"+t[r];e.use(t,n)}},e._Context=h,e.addLog=function(t,n){e.use(\"lib/log\",function(e){e.defaultLog(t,n)})},e.fire=function(t,n,r){e.use(\"lib/mod_evt\",function(e){e.fire(t,n,r)})},e._defService=function(e,t){if(e){var n=i.services[e];n=n||{},r(t,function(e,t){n[t]=e}),i.services[e]=n}},e.getService=function(t,n,r){var s=i.services[t];if(!s)throw new Error(t+\" mod didn't define any services\");var o=s[n];if(!o)throw new Error(t+\" mod didn't provide service \"+n);e.use(t+\":\"+o,r)},e}({})),!window._bd_share_is_recently_loaded&&window._bd_share_main.F.module(\"base/min_tangram\",function(e,t){var n={};n.each=function(e,t,n){var r,i,s,o=e.length;if(\"function\"==typeof t)for(s=0;s<o;s++){i=e[s],r=t.call(n||e,s,i);if(r===!1)break}return e};var r=function(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n]);return e};n.extend=function(){var e=arguments[0];for(var t=1,n=arguments.length;t<n;t++)r(e,arguments[t]);return e},n.domready=function(e,t){t=t||document;if(/complete/.test(t.readyState))e();else if(t.addEventListener)\"interactive\"==t.readyState?e():t.addEventListener(\"DOMContentLoaded\",e,!1);else{var n=function(){n=new Function,e()};void function(){try{t.body.doScroll(\"left\")}catch(e){return setTimeout(arguments.callee,10)}n()}(),t.attachEvent(\"onreadystatechange\",function(){\"complete\"==t.readyState&&n()})}},n.isArray=function(e){return\"[object Array]\"==Object.prototype.toString.call(e)},t.T=n}),!window._bd_share_is_recently_loaded&&window._bd_share_main.F.module(\"base/class\",function(e,t,n){var r=e(\"base/min_tangram\").T;t.BaseClass=function(){var e=this,t={};e.on=function(e,n){var r=t[e];r||(r=t[e]=[]),r.push(n)},e.un=function(e,n){if(!e){t={};return}var i=t[e];i&&(n?r.each(i,function(e,t){if(t==n)return i.splice(e,1),!1}):t[e]=[])},e.fire=function(n,i){var s=t[n];s&&(i=i||{},r.each(s,function(t,n){i._result=n.call(e,r.extend({_ctx:{src:e}},i))}))}};var i={};i.create=function(e,n){return n=n||t.BaseClass,function(){n.apply(this,arguments);var i=r.extend({},this);e.apply(this,arguments),this._super=i}},t.Class=i}),!window._bd_share_is_recently_loaded&&window._bd_share_main.F.module(\"conf/const\",function(e,t,n){t.CMD_ATTR=\"data-cmd\",t.CONFIG_TAG_ATTR=\"data-tag\",t.URLS={likeSetUrl:\"https://like.baidu.com/set\",commitUrl:\"https://s.share.baidu.com/commit\",jumpUrl:\"https://s.share.baidu.com\",mshareUrl:\"https://s.share.baidu.com/mshare\",emailUrl:\"https://s.share.baidu.com/sendmail\",nsClick:\"https://nsclick.baidu.com/v.gif\",backUrl:\"https://s.share.baidu.com/back\",shortUrl:\"https://dwz.cn/v2cut.php\"}}),!window._bd_share_is_recently_loaded&&function(){window._bd_share_main.F._setMod(\"api\"),window._bd_share_main.F._fileMap({\"/js/share.js?v=da893e3e.js\":[\"conf/define\",\"base/fis\",\"base/tangrammin\",\"base/class.js\",\"conf/define.js\",\"conf/const.js\",\"config\",\"share/api_base.js\",\"view/view_base.js\",\"start/router.js\",\"component/comm_tools.js\",\"trans/trans.js\"],\"/js/base/tangram.js?v=37768233.js\":[\"base/tangram\"],\"/js/view/share_view.js?v=3ae6026d.js\":[\"view/share_view\"],\"/js/view/slide_view.js?v=9fecb657.js\":[\"view/slide_view\"],\"/js/view/like_view.js?v=df3e0eca.js\":[\"view/like_view\"],\"/js/view/select_view.js?v=14bb0f0f.js\":[\"view/select_view\"],\"/js/trans/data.js?v=17af2bd2.js\":[\"trans/data\"],\"/js/trans/logger.js?v=60603cb3.js\":[\"trans/logger\"],\"/js/trans/trans_bdxc.js?v=7ac21555.js\":[\"trans/trans_bdxc\"],\"/js/trans/trans_bdysc.js?v=fc21acaa.js\":[\"trans/trans_bdysc\"],\"/js/trans/trans_weixin.js?v=6e098bbd.js\":[\"trans/trans_weixin\"],\"/js/share/combine_api.js?v=8d37a7b3.js\":[\"share/combine_api\"],\"/js/share/like_api.js?v=d3693f0a.js\":[\"share/like_api\"],\"/js/share/likeshare.js?v=e1f4fbf1.js\":[\"share/likeshare\"],\"/js/share/share_api.js?v=226108fe.js\":[\"share/share_api\"],\"/js/share/slide_api.js?v=ec14f516.js\":[\"share/slide_api\"],\"/js/component/animate.js?v=5b737477.js\":[\"component/animate\"],\"/js/component/anticheat.js?v=44b9b245.js\":[\"component/anticheat\"],\"/js/component/partners.js?v=96dbe85a.js\":[\"component/partners\"],\"/js/component/pop_base.js?v=36f92e70.js\":[\"component/pop_base\"],\"/js/component/pop_dialog.js?v=d479767d.js\":[\"component/pop_dialog\"],\"/js/component/pop_popup.js?v=4387b4e1.js\":[\"component/pop_popup\"],\"/js/component/pop_popup_slide.js?v=b16a1f10.js\":[\"component/pop_popup_slide\"],\"/js/component/qrcode.js?v=d69754a9.js\":[\"component/qrcode\"],\"/css/share_style0_16.css?v=8105b07e.css\":[\"share_style0_16.css\"],\"/css/share_style0_32.css?v=5090ac8b.css\":[\"share_style0_32.css\"],\"/css/share_style2.css?v=adaec91f.css\":[\"share_style2.css\"],\"/css/share_style4.css?v=3516ee8a.css\":[\"share_style4.css\"],\"/css/slide_share.css?v=855af98e.css\":[\"slide_share.css\"],\"/css/share_popup.css?v=ecc6050c.css\":[\"share_popup.css\"],\"/css/like.css?v=2797cee5.css\":[\"like.css\"],\"/css/imgshare.css?v=754091cd.css\":[\"imgshare.css\"],\"/css/select_share.css?v=cab3cb22.css\":[\"select_share.css\"],\"/css/weixin_popup.css?v=43591908.css\":[\"weixin_popup.css\"]}),window._bd_share_main.F._loadScriptTimeout=15e3,window._bd_share_main.F._useConfig=!0,window._bd_share_main.F._firstScreenCSS=\"\",window._bd_share_main.F._firstScreenJS=\"\"}(),!window._bd_share_is_recently_loaded&&window._bd_share_main.F.use(\"base/min_tangram\",function(e){function n(e,t,n){var r=new e(n);r.setView(new t(n)),r.init(),n&&n._handleId&&(_bd_share_main.api=_bd_share_main.api||{},_bd_share_main.api[n._handleId]=r)}function r(e,r){window._bd_share_main.F.use(e,function(e,i){t.isArray(r)?t.each(r,function(t,r){n(e.Api,i.View,r)}):n(e.Api,i.View,r)})}function i(e){var n=e.common||window._bd_share_config&&_bd_share_config.common||{},r={like:{type:\"like\"},share:{type:\"share\",bdStyle:0,bdMini:2,bdSign:\"on\"},slide:{type:\"slide\",bdStyle:\"1\",bdMini:2,bdImg:0,bdPos:\"right\",bdTop:100,bdSign:\"on\"},image:{viewType:\"list\",viewStyle:\"0\",viewPos:\"top\",viewColor:\"black\",viewSize:\"16\",viewList:[\"qzone\",\"tsina\",\"huaban\",\"tqq\",\"renren\"]},selectShare:{type:\"select\",bdStyle:0,bdMini:2,bdSign:\"on\"}},i={share:{__cmd:\"\",__buttonType:\"\",__type:\"\",__element:null},slide:{__cmd:\"\",__buttonType:\"\",__type:\"\",__element:null},image:{__cmd:\"\",__buttonType:\"\",__type:\"\",__element:null}};return t.each([\"like\",\"share\",\"slide\",\"image\",\"selectShare\"],function(s,o){e[o]&&(t.isArray(e[o])&&e[o].length>0?t.each(e[o],function(s,u){e[o][s]=t.extend({},r[o],n,u,i[o])}):e[o]=t.extend({},r[o],n,e[o],i[o]))}),e}var t=e.T;_bd_share_main.init=function(e){e=e||window._bd_share_config||{share:{}};if(e){var t=i(e);t.like&&r([\"share/like_api\",\"view/like_view\"],t.like),t.share&&r([\"share/share_api\",\"view/share_view\"],t.share),t.slide&&r([\"share/slide_api\",\"view/slide_view\"],t.slide),t.selectShare&&r([\"share/select_api\",\"view/select_view\"],t.selectShare),t.image&&r([\"share/image_api\",\"view/image_view\"],t.image)}},window._bd_share_main._LogPoolV2=[],window._bd_share_main.n1=(new Date).getTime(),t.domready(function(){window._bd_share_main.n2=(new Date).getTime()+1e3,_bd_share_main.init(),setTimeout(function(){window._bd_share_main.F.use(\"trans/logger\",function(e){e.nsClick(),e.back(),e.duration()})},3e3)})}),!window._bd_share_is_recently_loaded&&window._bd_share_main.F.module(\"component/comm_tools\",function(e,t){var n=function(){var e=window.location||document.location||{};return e.href||\"\"},r=function(e,t){var n=e.length,r=\"\";for(var i=1;i<=t;i++){var s=Math.floor(n*Math.random());r+=e.charAt(s)}return r},i=function(){var e=(+(new Date)).toString(36),t=r(\"0123456789abcdefghijklmnopqrstuvwxyz\",3);return e+t};t.getLinkId=i,t.getPageUrl=n}),!window._bd_share_is_recently_loaded&&window._bd_share_main.F.module(\"trans/trans\",function(e,t){var n=e(\"component/comm_tools\"),r=e(\"conf/const\").URLS,i=function(){window._bd_share_main.F.use(\"base/tangram\",function(e){var t=e.T;t.cookie.get(\"bdshare_firstime\")==null&&t.cookie.set(\"bdshare_firstime\",new Date*1,{path:\"/\",expires:(new Date).setFullYear(2022)-new Date})})},s=function(e){var t=e.bdUrl||n.getPageUrl();return t=t.replace(/\\'/g,\"%27\").replace(/\\\"/g,\"%22\"),t},o=function(e){var t=(new Date).getTime()+3e3,r={click:1,url:s(e),uid:e.bdUid||\"0\",to:e.__cmd,type:\"text\",pic:e.bdPic||\"\",title:(e.bdText||document.title).substr(0,300),key:(e.bdSnsKey||{})[e.__cmd]||\"\",desc:e.bdDesc||\"\",comment:e.bdComment||\"\",relateUid:e.bdWbuid||\"\",searchPic:e.bdSearchPic||0,sign:e.bdSign||\"on\",l:window._bd_share_main.n1.toString(32)+window._bd_share_main.n2.toString(32)+t.toString(32),linkid:n.getLinkId(),firstime:a(\"bdshare_firstime\")||\"\"};switch(e.__cmd){case\"copy\":l(r);break;case\"print\":c();break;case\"bdxc\":h();break;case\"bdysc\":p(r);break;case\"weixin\":d(r);break;default:u(e,r)}window._bd_share_main.F.use(\"trans/logger\",function(t){t.commit(e,r)})},u=function(e,t){var n=r.jumpUrl;e.__cmd==\"mshare\"?n=r.mshareUrl:e.__cmd==\"mail\"&&(n=r.emailUrl);var i=n+\"?\"+f(t);window.open(i)},a=function(e){if(e){var t=new RegExp(\"(^| )\"+e+\"=([^;]*)(;|$)\"),n=t.exec(document.cookie);if(n)return decodeURIComponent(n[2]||null)}},f=function(e){var t=[];for(var n in e)t.push(encodeURIComponent(n)+\"=\"+encodeURIComponent(e[n]));return t.join(\"&\").replace(/%20/g,\"+\")},l=function(e){window._bd_share_main.F.use(\"base/tangram\",function(t){var r=t.T;r.browser.ie?(window.clipboardData.setData(\"text\",document.title+\" \"+(e.bdUrl||n.getPageUrl())),alert(\"\\u6807\\u9898\\u548c\\u94fe\\u63a5\\u590d\\u5236\\u6210\\u529f\\uff0c\\u60a8\\u53ef\\u4ee5\\u63a8\\u8350\\u7ed9QQ/MSN\\u4e0a\\u7684\\u597d\\u53cb\\u4e86\\uff01\")):window.prompt(\"\\u60a8\\u4f7f\\u7528\\u7684\\u662f\\u975eIE\\u6838\\u5fc3\\u6d4f\\u89c8\\u5668\\uff0c\\u8bf7\\u6309\\u4e0b Ctrl+C \\u590d\\u5236\\u4ee3\\u7801\\u5230\\u526a\\u8d34\\u677f\",document.title+\" \"+(e.bdUrl||n.getPageUrl()))})},c=function(){window.print()},h=function(){window._bd_share_main.F.use(\"trans/trans_bdxc\",function(e){e&&e.run()})},p=function(e){window._bd_share_main.F.use(\"trans/trans_bdysc\",function(t){t&&t.run(e)})},d=function(e){window._bd_share_main.F.use(\"trans/trans_weixin\",function(t){t&&t.run(e)})},v=function(e){o(e)};t.run=v,i()});\n"
  },
  {
    "path": "asset/style.css",
    "content": "    .markdown-section h1 {\n        margin: 3rem 0 2rem 0;\n    }\n\n    .markdown-section h2 {\n        margin: 2rem 0 1rem;\n    }\n\n    img,\n    pre {\n        border-radius: 8px;\n    }\n\n    .content,\n    .sidebar,\n    .markdown-section,\n    body,\n    .search input {\n        background-color: rgba(243, 242, 238, 1) !important;\n    }\n\n    @media (min-width:600px) {\n        .sidebar-toggle {\n            background-color: #f3f2ee;\n        }\n    }\n\n    .docsify-copy-code-button {\n        background: #f8f8f8 !important;\n        color: #7a7a7a !important;\n    }\n\n    body {\n        /*font-family: Microsoft YaHei, Source Sans Pro, Helvetica Neue, Arial, sans-serif !important;*/\n    }\n\n    .markdown-section>p {\n        font-size: 16px !important;\n    }\n\n    .markdown-section pre>code {\n        font-family: Consolas, Roboto Mono, Monaco, courier, monospace !important;\n        font-size: .9rem !important;\n\n    }\n\n    /*.anchor span {\n    color: rgb(66, 185, 131);\n}*/\n\n    section.cover h1 {\n        margin: 0;\n    }\n\n    body>section>div.cover-main>ul>li>a {\n        color: #42b983;\n    }\n\n    .markdown-section img {\n        box-shadow: 7px 9px 10px #aaa !important;\n    }\n\n\n    pre {\n        background-color: #f3f2ee !important;\n    }\n\n    @media (min-width:600px) {\n        pre code {\n            /*box-shadow: 2px 1px 20px 2px #aaa;*/\n            /*border-radius: 10px !important;*/\n            padding-left: 20px !important;\n        }\n    }\n\n    @media (max-width:600px) {\n        pre {\n            padding-left: 0px !important;\n            padding-right: 0px !important;\n        }\n    }\n\n    .markdown-section pre {\n        padding-left: 0 !important;\n        padding-right: 0px !important;\n        box-shadow: 2px 1px 20px 2px #aaa;\n    }\n    \n    iframe {\n        display: inline;\n    }"
  },
  {
    "path": "asset/vue.css",
    "content": "@import url(\"https://fonts.googleapis.com/css?family=Roboto+Mono|Source+Sans+Pro:300,400,600\");\n* {\n  -webkit-font-smoothing: antialiased;\n  -webkit-overflow-scrolling: touch;\n  -webkit-tap-highlight-color: rgba(0,0,0,0);\n  -webkit-text-size-adjust: none;\n  -webkit-touch-callout: none;\n  box-sizing: border-box;\n}\nbody:not(.ready) {\n  overflow: hidden;\n}\nbody:not(.ready) [data-cloak],\nbody:not(.ready) .app-nav,\nbody:not(.ready) > nav {\n  display: none;\n}\ndiv#app {\n  font-size: 30px;\n  font-weight: lighter;\n  margin: 40vh auto;\n  text-align: center;\n}\ndiv#app:empty::before {\n  content: 'Loading...';\n}\n.emoji {\n  height: 1.2rem;\n  vertical-align: middle;\n}\n.progress {\n  background-color: var(--theme-color, #42b983);\n  height: 2px;\n  left: 0px;\n  position: fixed;\n  right: 0px;\n  top: 0px;\n  transition: width 0.2s, opacity 0.4s;\n  width: 0%;\n  z-index: 999999;\n}\n.search a:hover {\n  color: var(--theme-color, #42b983);\n}\n.search .search-keyword {\n  color: var(--theme-color, #42b983);\n  font-style: normal;\n  font-weight: bold;\n}\nhtml,\nbody {\n  height: 100%;\n}\nbody {\n  -moz-osx-font-smoothing: grayscale;\n  -webkit-font-smoothing: antialiased;\n  color: #34495e;\n  font-family: 'Source Sans Pro', 'Helvetica Neue', Arial, sans-serif;\n  font-size: 15px;\n  letter-spacing: 0;\n  margin: 0;\n  overflow-x: hidden;\n}\nimg {\n  max-width: 100%;\n}\na[disabled] {\n  cursor: not-allowed;\n  opacity: 0.6;\n}\nkbd {\n  border: solid 1px #ccc;\n  border-radius: 3px;\n  display: inline-block;\n  font-size: 12px !important;\n  line-height: 12px;\n  margin-bottom: 3px;\n  padding: 3px 5px;\n  vertical-align: middle;\n}\nli input[type='checkbox'] {\n  margin: 0 0.2em 0.25em 0;\n  vertical-align: middle;\n}\n.app-nav {\n  margin: 25px 60px 0 0;\n  position: absolute;\n  right: 0;\n  text-align: right;\n  z-index: 10;\n/* navbar dropdown */\n}\n.app-nav.no-badge {\n  margin-right: 25px;\n}\n.app-nav p {\n  margin: 0;\n}\n.app-nav > a {\n  margin: 0 1rem;\n  padding: 5px 0;\n}\n.app-nav ul,\n.app-nav li {\n  display: inline-block;\n  list-style: none;\n  margin: 0;\n}\n.app-nav a {\n  color: inherit;\n  font-size: 16px;\n  text-decoration: none;\n  transition: color 0.3s;\n}\n.app-nav a:hover {\n  color: var(--theme-color, #42b983);\n}\n.app-nav a.active {\n  border-bottom: 2px solid var(--theme-color, #42b983);\n  color: var(--theme-color, #42b983);\n}\n.app-nav li {\n  display: inline-block;\n  margin: 0 1rem;\n  padding: 5px 0;\n  position: relative;\n  cursor: pointer;\n}\n.app-nav li ul {\n  background-color: #fff;\n  border: 1px solid #ddd;\n  border-bottom-color: #ccc;\n  border-radius: 4px;\n  box-sizing: border-box;\n  display: none;\n  max-height: calc(100vh - 61px);\n  overflow-y: auto;\n  padding: 10px 0;\n  position: absolute;\n  right: -15px;\n  text-align: left;\n  top: 100%;\n  white-space: nowrap;\n}\n.app-nav li ul li {\n  display: block;\n  font-size: 14px;\n  line-height: 1rem;\n  margin: 0;\n  margin: 8px 14px;\n  white-space: nowrap;\n}\n.app-nav li ul a {\n  display: block;\n  font-size: inherit;\n  margin: 0;\n  padding: 0;\n}\n.app-nav li ul a.active {\n  border-bottom: 0;\n}\n.app-nav li:hover ul {\n  display: block;\n}\n.github-corner {\n  border-bottom: 0;\n  position: fixed;\n  right: 0;\n  text-decoration: none;\n  top: 0;\n  z-index: 1;\n}\n.github-corner:hover .octo-arm {\n  -webkit-animation: octocat-wave 560ms ease-in-out;\n          animation: octocat-wave 560ms ease-in-out;\n}\n.github-corner svg {\n  color: #fff;\n  fill: var(--theme-color, #42b983);\n  height: 80px;\n  width: 80px;\n}\nmain {\n  display: block;\n  position: relative;\n  width: 100vw;\n  height: 100%;\n  z-index: 0;\n}\nmain.hidden {\n  display: none;\n}\n.anchor {\n  display: inline-block;\n  text-decoration: none;\n  transition: all 0.3s;\n}\n.anchor span {\n  color: #34495e;\n}\n.anchor:hover {\n  text-decoration: underline;\n}\n.sidebar {\n  border-right: 1px solid rgba(0,0,0,0.07);\n  overflow-y: auto;\n  padding: 40px 0 0;\n  position: absolute;\n  top: 0;\n  bottom: 0;\n  left: 0;\n  transition: transform 250ms ease-out;\n  width: 300px;\n  z-index: 20;\n}\n.sidebar > h1 {\n  margin: 0 auto 1rem;\n  font-size: 1.5rem;\n  font-weight: 300;\n  text-align: center;\n}\n.sidebar > h1 a {\n  color: inherit;\n  text-decoration: none;\n}\n.sidebar > h1 .app-nav {\n  display: block;\n  position: static;\n}\n.sidebar .sidebar-nav {\n  line-height: 2em;\n  padding-bottom: 40px;\n}\n.sidebar li.collapse .app-sub-sidebar {\n  display: none;\n}\n.sidebar ul {\n  margin: 0 0 0 15px;\n  padding: 0;\n}\n.sidebar li > p {\n  font-weight: 700;\n  margin: 0;\n}\n.sidebar ul,\n.sidebar ul li {\n  list-style: none;\n}\n.sidebar ul li a {\n  border-bottom: none;\n  display: block;\n}\n.sidebar ul li ul {\n  padding-left: 20px;\n}\n.sidebar::-webkit-scrollbar {\n  width: 4px;\n}\n.sidebar::-webkit-scrollbar-thumb {\n  background: transparent;\n  border-radius: 4px;\n}\n.sidebar:hover::-webkit-scrollbar-thumb {\n  background: rgba(136,136,136,0.4);\n}\n.sidebar:hover::-webkit-scrollbar-track {\n  background: rgba(136,136,136,0.1);\n}\n.sidebar-toggle {\n  background-color: transparent;\n  background-color: rgba(255,255,255,0.8);\n  border: 0;\n  outline: none;\n  padding: 10px;\n  position: absolute;\n  bottom: 0;\n  left: 0;\n  text-align: center;\n  transition: opacity 0.3s;\n  width: 284px;\n  z-index: 30;\n  cursor: pointer;\n}\n.sidebar-toggle:hover .sidebar-toggle-button {\n  opacity: 0.4;\n}\n.sidebar-toggle span {\n  background-color: var(--theme-color, #42b983);\n  display: block;\n  margin-bottom: 4px;\n  width: 16px;\n  height: 2px;\n}\nbody.sticky .sidebar,\nbody.sticky .sidebar-toggle {\n  position: fixed;\n}\n.content {\n  padding-top: 60px;\n  position: absolute;\n  top: 0;\n  right: 0;\n  bottom: 0;\n  left: 300px;\n  transition: left 250ms ease;\n}\n.markdown-section {\n  margin: 0 auto;\n  max-width: 80%;\n  padding: 30px 15px 40px 15px;\n  position: relative;\n}\n.markdown-section > * {\n  box-sizing: border-box;\n  font-size: inherit;\n}\n.markdown-section > :first-child {\n  margin-top: 0 !important;\n}\n.markdown-section hr {\n  border: none;\n  border-bottom: 1px solid #eee;\n  margin: 2em 0;\n}\n.markdown-section iframe {\n  border: 1px solid #eee;\n/* fix horizontal overflow on iOS Safari */\n  width: 1px;\n  min-width: 100%;\n}\n.markdown-section table {\n  border-collapse: collapse;\n  border-spacing: 0;\n  display: block;\n  margin-bottom: 1rem;\n  overflow: auto;\n  width: 100%;\n}\n.markdown-section th {\n  border: 1px solid #ddd;\n  font-weight: bold;\n  padding: 6px 13px;\n}\n.markdown-section td {\n  border: 1px solid #ddd;\n  padding: 6px 13px;\n}\n.markdown-section tr {\n  border-top: 1px solid #ccc;\n}\n.markdown-section tr:nth-child(2n) {\n  background-color: #f8f8f8;\n}\n.markdown-section p.tip {\n  background-color: #f8f8f8;\n  border-bottom-right-radius: 2px;\n  border-left: 4px solid #f66;\n  border-top-right-radius: 2px;\n  margin: 2em 0;\n  padding: 12px 24px 12px 30px;\n  position: relative;\n}\n.markdown-section p.tip:before {\n  background-color: #f66;\n  border-radius: 100%;\n  color: #fff;\n  content: '!';\n  font-family: 'Dosis', 'Source Sans Pro', 'Helvetica Neue', Arial, sans-serif;\n  font-size: 14px;\n  font-weight: bold;\n  left: -12px;\n  line-height: 20px;\n  position: absolute;\n  height: 20px;\n  width: 20px;\n  text-align: center;\n  top: 14px;\n}\n.markdown-section p.tip code {\n  background-color: #efefef;\n}\n.markdown-section p.tip em {\n  color: #34495e;\n}\n.markdown-section p.warn {\n  background: rgba(66,185,131,0.1);\n  border-radius: 2px;\n  padding: 1rem;\n}\n.markdown-section ul.task-list > li {\n  list-style-type: none;\n}\nbody.close .sidebar {\n  transform: translateX(-300px);\n}\nbody.close .sidebar-toggle {\n  width: auto;\n}\nbody.close .content {\n  left: 0;\n}\n@media print {\n  .github-corner,\n  .sidebar-toggle,\n  .sidebar,\n  .app-nav {\n    display: none;\n  }\n}\n@media screen and (max-width: 768px) {\n  .github-corner,\n  .sidebar-toggle,\n  .sidebar {\n    position: fixed;\n  }\n  .app-nav {\n    margin-top: 16px;\n  }\n  .app-nav li ul {\n    top: 30px;\n  }\n  main {\n    height: auto;\n    overflow-x: hidden;\n  }\n  .sidebar {\n    left: -300px;\n    transition: transform 250ms ease-out;\n  }\n  .content {\n    left: 0;\n    max-width: 100vw;\n    position: static;\n    padding-top: 20px;\n    transition: transform 250ms ease;\n  }\n  .app-nav,\n  .github-corner {\n    transition: transform 250ms ease-out;\n  }\n  .sidebar-toggle {\n    background-color: transparent;\n    width: auto;\n    padding: 30px 30px 10px 10px;\n  }\n  body.close .sidebar {\n    transform: translateX(300px);\n  }\n  body.close .sidebar-toggle {\n    background-color: rgba(255,255,255,0.8);\n    transition: 1s background-color;\n    width: 284px;\n    padding: 10px;\n  }\n  body.close .content {\n    transform: translateX(300px);\n  }\n  body.close .app-nav,\n  body.close .github-corner {\n    display: none;\n  }\n  .github-corner:hover .octo-arm {\n    -webkit-animation: none;\n            animation: none;\n  }\n  .github-corner .octo-arm {\n    -webkit-animation: octocat-wave 560ms ease-in-out;\n            animation: octocat-wave 560ms ease-in-out;\n  }\n}\n@-webkit-keyframes octocat-wave {\n  0%, 100% {\n    transform: rotate(0);\n  }\n  20%, 60% {\n    transform: rotate(-25deg);\n  }\n  40%, 80% {\n    transform: rotate(10deg);\n  }\n}\n@keyframes octocat-wave {\n  0%, 100% {\n    transform: rotate(0);\n  }\n  20%, 60% {\n    transform: rotate(-25deg);\n  }\n  40%, 80% {\n    transform: rotate(10deg);\n  }\n}\nsection.cover {\n  align-items: center;\n  background-position: center center;\n  background-repeat: no-repeat;\n  background-size: cover;\n  height: 100vh;\n  width: 100vw;\n  display: none;\n}\nsection.cover.show {\n  display: flex;\n}\nsection.cover.has-mask .mask {\n  background-color: #fff;\n  opacity: 0.8;\n  position: absolute;\n  top: 0;\n  height: 100%;\n  width: 100%;\n}\nsection.cover .cover-main {\n  flex: 1;\n  margin: -20px 16px 0;\n  text-align: center;\n  position: relative;\n}\nsection.cover a {\n  color: inherit;\n  text-decoration: none;\n}\nsection.cover a:hover {\n  text-decoration: none;\n}\nsection.cover p {\n  line-height: 1.5rem;\n  margin: 1em 0;\n}\nsection.cover h1 {\n  color: inherit;\n  font-size: 2.5rem;\n  font-weight: 300;\n  margin: 0.625rem 0 2.5rem;\n  position: relative;\n  text-align: center;\n}\nsection.cover h1 a {\n  display: block;\n}\nsection.cover h1 small {\n  bottom: -0.4375rem;\n  font-size: 1rem;\n  position: absolute;\n}\nsection.cover blockquote {\n  font-size: 1.5rem;\n  text-align: center;\n}\nsection.cover ul {\n  line-height: 1.8;\n  list-style-type: none;\n  margin: 1em auto;\n  max-width: 500px;\n  padding: 0;\n}\nsection.cover .cover-main > p:last-child a {\n  border-color: var(--theme-color, #42b983);\n  border-radius: 2rem;\n  border-style: solid;\n  border-width: 1px;\n  box-sizing: border-box;\n  color: var(--theme-color, #42b983);\n  display: inline-block;\n  font-size: 1.05rem;\n  letter-spacing: 0.1rem;\n  margin: 0.5rem 1rem;\n  padding: 0.75em 2rem;\n  text-decoration: none;\n  transition: all 0.15s ease;\n}\nsection.cover .cover-main > p:last-child a:last-child {\n  background-color: var(--theme-color, #42b983);\n  color: #fff;\n}\nsection.cover .cover-main > p:last-child a:last-child:hover {\n  color: inherit;\n  opacity: 0.8;\n}\nsection.cover .cover-main > p:last-child a:hover {\n  color: inherit;\n}\nsection.cover blockquote > p > a {\n  border-bottom: 2px solid var(--theme-color, #42b983);\n  transition: color 0.3s;\n}\nsection.cover blockquote > p > a:hover {\n  color: var(--theme-color, #42b983);\n}\nbody {\n  background-color: #fff;\n}\n/* sidebar */\n.sidebar {\n  background-color: #fff;\n  color: #364149;\n}\n.sidebar li {\n  margin: 6px 0 6px 0;\n}\n.sidebar ul li a {\n  color: #505d6b;\n  font-size: 14px;\n  font-weight: normal;\n  overflow: hidden;\n  text-decoration: none;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n.sidebar ul li a:hover {\n  text-decoration: underline;\n}\n.sidebar ul li ul {\n  padding: 0;\n}\n.sidebar ul li.active > a {\n  border-right: 2px solid;\n  color: var(--theme-color, #42b983);\n  font-weight: 600;\n}\n.app-sub-sidebar li::before {\n  content: '-';\n  padding-right: 4px;\n  float: left;\n}\n/* markdown content found on pages */\n.markdown-section h1,\n.markdown-section h2,\n.markdown-section h3,\n.markdown-section h4,\n.markdown-section strong {\n  color: #2c3e50;\n  font-weight: 600;\n}\n.markdown-section a {\n  color: var(--theme-color, #42b983);\n  font-weight: 600;\n}\n.markdown-section h1 {\n  font-size: 2rem;\n  margin: 0 0 1rem;\n}\n.markdown-section h2 {\n  font-size: 1.75rem;\n  margin: 45px 0 0.8rem;\n}\n.markdown-section h3 {\n  font-size: 1.5rem;\n  margin: 40px 0 0.6rem;\n}\n.markdown-section h4 {\n  font-size: 1.25rem;\n}\n.markdown-section h5 {\n  font-size: 1rem;\n}\n.markdown-section h6 {\n  color: #777;\n  font-size: 1rem;\n}\n.markdown-section figure,\n.markdown-section p {\n  margin: 1.2em 0;\n}\n.markdown-section p,\n.markdown-section ul,\n.markdown-section ol {\n  line-height: 1.6rem;\n  word-spacing: 0.05rem;\n}\n.markdown-section ul,\n.markdown-section ol {\n  padding-left: 1.5rem;\n}\n.markdown-section blockquote {\n  border-left: 4px solid var(--theme-color, #42b983);\n  color: #858585;\n  margin: 2em 0;\n  padding-left: 20px;\n}\n.markdown-section blockquote p {\n  font-weight: 600;\n  margin-left: 0;\n}\n.markdown-section iframe {\n  margin: 1em 0;\n}\n.markdown-section em {\n  color: #7f8c8d;\n}\n.markdown-section code {\n  background-color: #f8f8f8;\n  border-radius: 2px;\n  color: #e96900;\n  font-family: 'Roboto Mono', Monaco, courier, monospace;\n  font-size: 0.8rem;\n  margin: 0 2px;\n  padding: 3px 5px;\n  white-space: pre-wrap;\n}\n.markdown-section pre {\n  -moz-osx-font-smoothing: initial;\n  -webkit-font-smoothing: initial;\n  background-color: #f8f8f8;\n  font-family: 'Roboto Mono', Monaco, courier, monospace;\n  line-height: 1.5rem;\n  margin: 1.2em 0;\n  overflow: auto;\n  padding: 0 1.4rem;\n  position: relative;\n  word-wrap: normal;\n}\n/* code highlight */\n.token.comment,\n.token.prolog,\n.token.doctype,\n.token.cdata {\n  color: #8e908c;\n}\n.token.namespace {\n  opacity: 0.7;\n}\n.token.boolean,\n.token.number {\n  color: #c76b29;\n}\n.token.punctuation {\n  color: #525252;\n}\n.token.property {\n  color: #c08b30;\n}\n.token.tag {\n  color: #2973b7;\n}\n.token.string {\n  color: var(--theme-color, #42b983);\n}\n.token.selector {\n  color: #6679cc;\n}\n.token.attr-name {\n  color: #2973b7;\n}\n.token.entity,\n.token.url,\n.language-css .token.string,\n.style .token.string {\n  color: #22a2c9;\n}\n.token.attr-value,\n.token.control,\n.token.directive,\n.token.unit {\n  color: var(--theme-color, #42b983);\n}\n.token.keyword,\n.token.function {\n  color: #e96900;\n}\n.token.statement,\n.token.regex,\n.token.atrule {\n  color: #22a2c9;\n}\n.token.placeholder,\n.token.variable {\n  color: #3d8fd1;\n}\n.token.deleted {\n  text-decoration: line-through;\n}\n.token.inserted {\n  border-bottom: 1px dotted #202746;\n  text-decoration: none;\n}\n.token.italic {\n  font-style: italic;\n}\n.token.important,\n.token.bold {\n  font-weight: bold;\n}\n.token.important {\n  color: #c94922;\n}\n.token.entity {\n  cursor: help;\n}\n.markdown-section pre > code {\n  -moz-osx-font-smoothing: initial;\n  -webkit-font-smoothing: initial;\n  background-color: #f8f8f8;\n  border-radius: 2px;\n  color: #525252;\n  display: block;\n  font-family: 'Roboto Mono', Monaco, courier, monospace;\n  font-size: 0.8rem;\n  line-height: inherit;\n  margin: 0 2px;\n  max-width: inherit;\n  overflow: inherit;\n  padding: 2.2em 5px;\n  white-space: inherit;\n}\n.markdown-section code::after,\n.markdown-section code::before {\n  letter-spacing: 0.05rem;\n}\ncode .token {\n  -moz-osx-font-smoothing: initial;\n  -webkit-font-smoothing: initial;\n  min-height: 1.5rem;\n  position: relative;\n  left: auto;\n}\npre::after {\n  color: #ccc;\n  content: attr(data-lang);\n  font-size: 0.6rem;\n  font-weight: 600;\n  height: 15px;\n  line-height: 15px;\n  padding: 5px 10px 0;\n  position: absolute;\n  right: 0;\n  text-align: right;\n  top: 0;\n}\n"
  },
  {
    "path": "docs/.gitkeep",
    "content": ""
  },
  {
    "path": "docs/adv-cpp/00.md",
    "content": "# 零、前言\n\n## 大约\n\n本节简要介绍作者、本书的内容、入门所需的技术技能，以及完成所有附带活动和练习所需的硬件和软件要求。\n\n## 关于书\n\nC++ 是使用最广泛的编程语言之一，应用于各种领域，从游戏到**图形用户界面** ( **GUI** )编程甚至操作系统。如果你想扩大你的职业机会，掌握 C++ 的高级特性是关键。\n\n这本书从高级的 C++ 概念开始，帮助您破译复杂的 C++ 类型系统，并了解编译的各个阶段如何将源代码转换为目标代码。然后，您将学习如何识别需要用来控制执行流程、捕获数据和传递数据的工具。通过创建小模型，您甚至可以发现如何使用高级 lambdas，并在 C++ 中捕获和表达常见的 API 设计模式。在后面的章节中，您将通过学习内存对齐、缓存访问和程序运行时间来探索优化代码的方法。最后一章将帮助您通过理解现代的 CPU 分支预测以及如何使代码缓存友好来最大化性能。\n\n到这本书的最后，你将会发展出与其他 C++ 程序员不同的编程技能。\n\n### 关于作者\n\n**Gazihan Alankus** 拥有圣路易斯华盛顿大学计算机科学博士学位。目前，他是土耳其伊兹密尔经济大学的助理教授。他教授并从事游戏开发、移动应用开发和人机交互方面的研究。他是谷歌 Dart 的开发专家，在他 2019 年创立的公司 Gbot 中与学生一起开发 Flutter 应用。\n\n**Olena Lizina** 是一名拥有 5 年 C++ 经验的软件开发人员。她拥有为一家国际产品公司开发监控和管理远程计算机系统的实用知识。在过去的 4 年里，她一直在为国际外包公司的汽车项目工作，以解决众所周知的汽车问题。她一直参与不同项目的复杂和高性能应用的开发，如**人机界面**、导航和传感器应用。\n\n**Rakesh Mane** 在软件行业拥有超过 18 年的经验。他曾与来自印度、美国和新加坡等不同地区的熟练程序员合作。他主要从事 C++、Python、shell 脚本和数据库方面的工作。在业余时间，他喜欢听音乐和旅行。此外，他喜欢使用软件工具和代码玩、试验和破坏东西。\n\n**Vivek Nagarajan** 是一名自学成才的程序员，他从上世纪 80 年代开始研究 8 位系统。他从事过大量的软件项目，拥有 14 年的 C++ 专业经验。除此之外，他多年来一直致力于各种各样的语言和框架。他是一个业余力量爱好者，自己动手做的爱好者，也是摩托车赛车手。他目前是一名独立的软件顾问。\n\n**Brian Price** 在各种语言、项目和行业拥有超过 30 年的工作经验，其中包括超过 20 年的 C++ 经验。他从事电站模拟器、SCADA 系统和医疗设备的工作。他目前正在用 C++、CMake 和 Python 为下一代医疗设备制作软件。他喜欢用各种语言解谜和欧拉项目。\n\n### 学习目标\n\n本书结束时，您将能够:\n\n*   深入研究 C++ 的剖析和工作流程\n*   研究 C++ 中不同编码方法的优缺点\n*   测试、运行和调试您的程序\n*   将对象文件链接为动态库\n*   使用模板、SFINAE、constexpr if 表达式和变量模板\n*   将最佳实践应用于资源管理\n\n### 观众\n\n如果你曾在 C++ 工作过，但想学习如何充分利用这种语言，尤其是对于大型项目，这本书是为你准备的。必须对编程有一个大致的了解，并且了解如何使用编辑器在项目目录中生成代码文件。也推荐一些强类型语言的经验，比如 C 和 C++。\n\n### 进场\n\n这本快节奏的书旨在通过描述性的图形和挑战性的练习，快速教你概念。这本书将有“号召”，有关键的要点和最常见的陷阱来保持你的兴趣，同时将主题分成易于管理的部分。\n\n### 硬件要求\n\n为了获得最佳的学生体验，我们推荐以下硬件配置:\n\n*   任何带有 Windows、Linux 或 macOS 的入门级 PC/Mac 都足够了\n*   处理器:双核或同等处理器\n*   内存:4 GB 内存(首选 8 GB)\n*   存储:35 GB 可用空间\n\n### 软件需求\n\n您还需要提前安装以下软件:\n\n*   操作系统:Windows 7 SP1 32/64 位，Windows 8.1 32/64 位，或 Windows 10 32/64 位，Ubuntu 14.04 或更高版本，或 macOS Sierra 或更高版本\n*   浏览器:谷歌 Chrome 还是 Mozilla 火狐\n\n### 安装和设置\n\n在开始阅读本书之前，您需要安装本书中使用的以下库。您将在这里找到安装这些的步骤。\n\n**安装 CMake**\n\n我们将使用 CMake 版本 3.12.1 或更高版本。我们有两种安装选择。\n\n选项 1:\n\n如果您使用的是 Ubuntu 18.10，可以使用以下命令全局安装 CMake:\n\n```cpp\nsudo apt install cmake\n```\n\n运行以下命令时:\n\n```cpp\ncmake –version\n```\n\n您应该会看到以下输出:\n\n```cpp\ncmake version 3.12.1\nCMake suite maintained and supported by Kitware (kitware.com/cmake).\n```\n\n如果您在这里看到的版本低于 3.12.1(例如 3.10)，您应该使用以下说明在本地安装 CMake。\n\n备选方案 2:\n\n如果您使用的是旧的 Linux 版本，您可能会得到低于 3.12.1 的 CMake 版本。然后，您需要在本地安装它。使用以下命令:\n\n```cpp\nwget \\\nhttps://github.com/Kitware/CMake/releases/download/v3.15.1/cmake-3.15.1-Linux-x86_64.sh\nsh cmake-3.15.1-Linux-x86_64.sh\n```\n\n看到软件许可证后，输入 *y* 并按*进入*。当询问安装位置时，键入 *y* 并再次按回车键。这应该会将其安装到系统中的新文件夹中。\n\n现在，我们将该文件夹添加到我们的路径中。键入以下内容。请注意，在本文档中，第一行有点太长，并且换行。您应该将其写成一行，如下所示:\n\n```cpp\necho \"export PATH=\\\"$HOME/cmake-3.15.1-Linux-x86_64/bin:$PATH\\\"\" >> .bash_profile\nsource .profile\n```\n\n现在，当您键入以下内容时:\n\n```cpp\ncmake –version\n```\n\n您应该会看到以下输出:\n\n```cpp\ncmake version 3.15.1\nCMake suite maintained and supported by Kitware (kitware.com/cmake).\n```\n\n3.15.1 是撰写本文档时的最新版本。由于它比 3.12.1 更新，这将满足我们的目的。\n\n**安装 Git**\n\n通过键入以下内容测试当前安装:\n\n```cpp\ngit --version\n```\n\n您应该会看到如下一行:\n\n```cpp\ngit version 2.17.1\n```\n\n如果改为看到下面一行，则需要安装`git`:\n\n```cpp\ncommand 'git' not found\n```\n\n以下是如何在 Ubuntu 中安装`git`:\n\n```cpp\nsudo apt install git\n```\n\n**安装 g++**\n\n通过键入以下内容测试当前安装:\n\n```cpp\ng++ --version\n```\n\n您应该会看到如下输出:\n\n```cpp\ng++ (Ubuntu 7.4.0-1ubuntu1~18.04) 7.4.0\nCopyright (C) 2017 Free Software Foundation, Inc.\nThis is free software; see the source for copying conditions. There is NO\nwarranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n```\n\n如果未安装，请键入以下代码进行安装:\n\n```cpp\nsudo apt install g++\n```\n\n**安装忍者**\n\n通过键入以下内容测试当前安装:\n\n```cpp\nninja --version\n```\n\n您应该会看到如下输出:\n\n```cpp\n1.8.2\n```\n\n如果未安装，请键入以下代码进行安装:\n\n```cpp\nsudo apt install ninja-build\n```\n\n**安装 Eclipse CDT 和 cmake4eclipse**\n\n安装 Eclipse CDT 有多种方法。为了获得最新的稳定版本，我们将使用官方安装程序。去这个网站下载 Linux 安装程序:[https://www.eclipse.org/downloads/packages/installer](https://www.eclipse.org/downloads/packages/installer)。\n\n按照那里的说明，为 C/C++ 开发者安装**Eclipse IDE**。安装完成后，运行 Eclipse 可执行文件。如果没有更改默认配置，在终端中键入以下命令将运行它:\n\n```cpp\n~/eclipse/cpp-2019-03/eclipse/eclipse\n```\n\n您将选择一个工作区文件夹，然后在 Eclipse 主窗口中会出现一个**欢迎**选项卡。\n\n现在，我们将安装`cmake4eclipse`。一个简单的方法是去这个网站，把**安装**图标拖到 Eclipse 窗口:[https://github.com/15knots/cmake4eclipse#installation](https://github.com/15knots/cmake4eclipse#installation)。它会要求您重新启动 Eclipse，之后您就可以修改 CMake 项目来使用 Eclipse 了。\n\n**安装谷歌测试**\n\n我们会在系统中安装`GoogleTest`，系统也会安装其他依赖它的包。编写以下命令:\n\n```cpp\nsudo apt install libgtest-dev google-mock\n```\n\n该命令为`谷歌测试`安装包含文件和源文件。现在，我们需要构建我们安装的源文件来创建`谷歌测试`库。为此，请运行以下命令:\n\n```cpp\ncd /usr/src/gtest\nsudo cmake CMakeLists.txt\nsudo make\nsudo cp *.a /usr/lib\n```\n\n### 安装代码包\n\n将该类的代码包复制到`C:/Code`文件夹中。\n\n### 附加资源\n\n这本书的代码包也托管在 https://github.com/TrainingByPackt/Advanced-CPlusPlus 的 GitHub 上。\n\n我们还有来自 https://github.com/PacktPublishing/丰富的书籍和视频目录的其他代码包。看看他们！"
  },
  {
    "path": "docs/adv-cpp/01.md",
    "content": "# 一、可移植的 C++ 软件剖析\n\n## 学习目标\n\n本章结束时，您将能够:\n\n*   建立代码构建测试过程\n*   描述编译的各个阶段\n*   破译复杂的 C++ 类型系统\n*   用单元测试配置项目\n*   将源代码转换为目标代码\n*   编写可读的代码并调试它\n\n在本章中，我们将学习建立将在整本书中使用的代码构建测试模型，编写漂亮的代码，并执行单元测试。\n\n## 简介\n\nC++ 是最古老、最流行的语言之一，可以用来编写高效的代码。它既像 C 一样“接近金属”，又像 Java 一样具有高级的面向对象特性。作为一种高效的低级语言，C++ 成为游戏、模拟和嵌入式系统等效率至上的领域的首选语言。同时，作为一种具有泛型、引用和无数其他高级特性的面向对象语言，它适合由多人开发和维护的大型项目。\n\n几乎任何编程经验都包括组织代码库和使用他人编写的库。C++ 也不例外。除非您的程序很简单，否则您会将代码分发到需要组织的多个文件中，并且您会使用各种库来完成任务，通常比您的代码更加高效和健壮。不使用任何第三方库的 C++ 项目是边缘案例，不代表使用许多库的大多数项目。这些项目及其库有望在不同的硬件架构和操作系统中工作。因此，如果要用 C++ 开发任何有意义的东西，花时间在项目设置上并理解用于管理依赖关系的工具是很重要的。\n\n大多数现代和流行的高级语言都有标准工具来维护项目、构建项目以及处理它们的库依赖关系。其中许多都有存放库和工具的存储库，这些工具可以自动从这些存储库中下载和使用库。例如，Python 有`pip`，负责下载和使用程序员想要使用的库的适当版本。同样的，JavaScript 有`npm`，Java 有`maven`，Dart 有`pub`，C#有`NuGet`。在大多数语言中，您会列出库的名称和您想要使用的版本，工具会自动下载并使用库的兼容版本。这些语言受益于这样一个事实，即程序是在一个受控的环境中构建和运行的，在该环境中满足了一定级别的硬件和软件要求。另一方面，C++ 有望在各种不同架构的环境中工作，包括非常原始的硬件。因此，C++ 程序员在构建程序和执行依赖管理时不会那么娇纵。\n\n## 管理 C++ 项目\n\n在 C++ 世界中，我们有几个工具可以帮助管理项目源及其依赖关系。比如`pkg-config`、`自动工具`、`make`、`CMake`都是社区中最引人注目的。与其他高级语言的工具相比，这些工具的使用要复杂得多。`CMake`作为管理 C++ 项目及其依赖关系的事实标准已经在这些项目中兴起。它比`make`更固执己见，被大多数 IDEs(集成开发环境)接受为直接的项目格式。\n\n虽然`CMake`有助于管理项目及其依赖关系，但这种体验仍然远远不是更高级的语言，在更高级的语言中，您可以列出您想要使用的库及其版本，其他一切都为您考虑。使用 CMake，您仍然有责任在您的开发环境中正确安装库，并且您应该为每个库使用兼容的版本。在具有大量包管理器的流行 Linux 发行版中，您可以轻松安装大多数流行库的二进制版本。但是，有时，您可能需要自己编译和安装库。这是整个 C++ 开发人员体验的一部分，您将通过更多地了解自己选择的开发平台来获得这一体验。在这里，我们将更加关注如何正确设置我们的 CMake 项目，包括理解和解决与库相关的问题。\n\n### 代码构建测试运行循环\n\n为了将我们的讨论建立在坚实的基础上，我们将立即从一个实际的例子开始。我们将从一个 C++ 代码基础模板开始，您可以将其用作自己项目的起点。我们将看到如何在命令行上使用 CMake 构建和编译它。我们还将为 C/C++ 开发人员设置 Eclipse IDE，并导入我们的 CMake 项目。集成开发环境的使用将为我们提供易于创建源代码的工具，并使我们能够一行行地调试程序，以查看程序执行过程中到底发生了什么，并以明智的方式纠正我们的错误，而不是反复试验和迷信。\n\n### 打造一个 CMake 项目\n\nC++ 项目事实上的标准是使用 CMake 来组织和构建项目。在这里，我们将使用一个基本的模板项目作为起点。以下是示例模板的文件夹结构:\n\n![Figure 1.1: Folder structure of a sample template ](img/C14508_01_01.jpg)\n\n###### 图 1.1:示例模板的文件夹结构\n\n在上图中，**。gitignore** 文件列出了不应该添加到`git`版本控制系统的文件模式。这种被忽略的文件包括构建过程的输出，这些输出是在本地创建的，不应该在计算机之间共享。\n\n**中的文件包括**和 **src** 文件夹是实际的 C++ 源文件， **CMakeLists.txt** 文件是通过处理**源代码编译规则**、**库依赖项**和其他项目设置将项目粘合在一起的 CMake 脚本文件。CMake 规则是独立于平台的高级规则。CMake 用它们创建各种类型的`为不同平台制作`文件。\n\n用 CMake 构建一个项目是一个两步的过程。首先，我们让 CMake 为将编译和构建项目的本机构建系统生成平台相关的配置文件。然后，我们将使用生成的文件来构建项目。CMake 可以为其生成配置文件的平台相关构建系统包括 **UNIX** **Makefiles** 、 **Ninja** **构建文件**、 **NMake** **Makefiles** 、**MinGW****Makefiles**。这里的选择取决于使用的平台、这些工具的可用性以及个人偏好。**UNIX****Makefiles**是 **Unix** 和 **Linux** 的事实标准，而 **NMake** 是其 **Windows** 和 **Visual Studio** 的对应物。 **MinGW** 则是 **Windows** 中类似**的 Unix 环境，其中 **Makefiles** 也在使用。 **Ninja** 是一个现代化的构建系统，与其他构建系统相比，它提供了非凡的速度，加上多平台支持，我们选择在这里使用。此外，除了这些命令行构建系统之外，我们还可以为 **Visual Studio** 、 **XCode** 、 **Eclipse CDT** 和许多其他项目生成 ide 项目，并在 IDE 内部构建我们的项目。因此， **CMake** 是一个元工具，它将为实际构建项目的另一个系统创建配置文件。在下一节中，我们将解决一个练习，其中我们将使用 **CMake** 生成**忍者** **构建文件**。**\n\n### 练习 1:使用 CMake 生成忍者构建文件\n\n在本练习中，我们将使用`CMake`生成`忍者构建文件`，用于构建 C++ 项目。我们将首先从一个`git`存储库中下载我们的源代码，并将使用 CMake 和 Ninja 来构建它。本练习的目的是使用 CMake 生成 Ninja 构建文件，构建项目，然后运行它们。\n\n#### 注意\n\nGitHub 资源库的链接可以在这里找到:[https://GitHub . com/trainingypbackt/Advanced-CPlusPlus/tree/master/lesson 1/练习 01/project](https://github.com/TrainingByPackt/Advanced-CPlusPlus/tree/master/Lesson1/Exercise01/project) 。\n\n执行以下步骤完成练习:\n\n1.  In a terminal window, type the following command to download the `CxxTemplate` repository from GitHub onto your local system:\n\n    ```cpp\n    git clone https://github.com/TrainingByPackt/Advanced-CPlusPlus/tree/master/Lesson1/Exercise01/project\n    ```\n\n    前一个命令的输出类似于以下内容:\n\n    ![Figure 1.2: Checking out the sample project from GitHub ](img/C14508_01_02.jpg)\n\n    ###### 图 1.2:从 GitHub 签出示例项目\n\n    现在你在`CxxTemplate`文件夹中有了源代码。\n\n2.  通过在终端中键入以下命令，导航到`CxxTemplate`文件夹:\n\n    ```cpp\n    cd CxxTemplate\n    ```\n\n3.  现在，您可以通过键入以下命令列出项目中的所有文件:\n\n    ```cpp\n    find .\n    ```\n\n4.  Generate our Ninja build file using the `cmake` command in the `CxxTemplate` folder. To do that, write the following command:\n\n    ```cpp\n    cmake -Bbuild -H. -GNinja\n    ```\n\n    前面命令的输出如下:\n\n    ![Figure 1.3: Generating the Ninja build file  ](img/C14508_01_03.jpg)\n\n    ###### 图 1.3:生成忍者构建文件\n\n    让我们解释前面命令的部分内容。通过`-Bbuild`，我们告诉 CMake 使用`构建`文件夹来生成构建工件。由于此文件夹不存在，CMake 将创建它。借助`–h .`，我们告诉 CMake 使用当前文件夹作为源。通过使用一个单独的`构建`文件夹，我们将保持源文件的干净，所有的构建工件将保存在`构建`文件夹中，由于我们的`，Git 忽略了这个文件夹。gitignore`文件。借助`–GNinja`，我们告诉 CMake 使用忍者构建系统。\n\n5.  Run the following commands to list the project files and to check the files that were created inside the `build` folder:\n\n    ```cpp\n    ls\n    ls build\n    ```\n\n    前面的命令将在终端中显示以下输出:\n\n    ![Figure 1.4: Files in the build folder ](img/C14508_01_04.jpg)\n\n    ###### 图 1.4:构建文件夹中的文件\n\n    很明显，前面的文件将出现在构建文件夹中。 **build.ninja** 和 **rules.ninja** 在前面的输出中是 Ninja build 文件，可以在这个平台中实际构建我们的项目。\n\n    #### 注意\n\n    通过使用 CMake，我们不需要编写忍者构建文件，并且避免了提交到 Unix 平台。相反，我们有一个元构建系统，可以为其他平台(如 UNIX/Linux、MinGW 和 Nmake)生成低级构建文件。\n\n6.  Now, go into the `build` folder and build our project by typing the following commands in the terminal:\n\n    ```cpp\n    cd build\n    ninja\n    ```\n\n    您应该会看到如下所示的最终输出:\n\n    ![Figure 1.5: Building with ninja ](img/C14508_01_05.jpg)\n\n    ###### 图 1.5:用忍者建造\n\n7.  Type `ls` in the **build** folder and check whether we have generated the `CxxTemplate` executable or not:\n\n    ```cpp\n    ls\n    ```\n\n    前面的命令在终端中产生以下输出:\n\n    ![Figure 1.6: Files in the build folder after running ninja ](img/C14508_01_06.jpg)\n\n    ###### 图 1.6:运行 ninja 后构建文件夹中的文件\n\n    在上图中，可以看到生成了`CxxTemplate`可执行文件。\n\n8.  In the terminal, type the following command to run the `CxxTemplate` executable:\n\n    ```cpp\n    ./CxxTemplate\n    ```\n\n    终端中的前一个命令将提供以下输出:\n\n    ![](img/C14508_01_07.jpg)\n\n###### 图 1.7:运行可执行文件\n\n`src/CxxTemplate.cpp`文件中的下面一行负责写入前面的输出:\n\n```cpp\nstd::cout << \"Hello CMake.\" << std::endl;\n```\n\n现在你已经在 Linux 中成功构建了一个 CMake 项目。忍者和 CMake 配合得相当好。你只需要运行一次 CMake，Ninja 会检测是否应该再次调用 CMake，并为你调用它。例如，即使你在你的`CMakeLists.txt`文件中添加了新的源文件，你只需要在终端中键入`忍者`命令，它就会自动运行 CMake 为你更新忍者构建文件。既然您已经了解了在 Linux 中构建 CMake 项目，在下一节中，我们将了解如何将 CMake 项目导入到 Eclipse CDT 中。\n\n## 将一个项目导入 Eclipse CDT\n\n忍者构建文件对于在 Linux 中构建我们的项目非常有用。然而，一个 CMake 项目是可移植的，也可以用于其他构建系统和 ide。许多 ide 接受 CMake 作为它们的配置文件，并在您修改和构建项目时提供无缝的体验。在本节中，我们将讨论如何将一个 CMake 项目导入到 Eclipse CDT 中，这是一个流行的跨平台 C/C++ IDE。\n\n有多种方法可以将 Eclipse CDT 与 CMake 一起使用。CMake 提供的默认选项是 IDE 项目的单向生成。在这里，您只需创建一次集成开发环境项目，对集成开发环境项目所做的任何修改都不会变回原始的 CMake 项目。如果您将项目作为一个 CMake 项目来管理，并使用 Eclipse CDT 进行一次性构建，这将非常有用。然而，如果您想在 Eclipse CDT 中进行开发，这并不理想。\n\n在 Eclipse CDT 中使用 CMake 的另一种方法是使用自定义的`cmake4eclipse`插件。使用这个插件的时候，不要放弃你的`CMakeLists.txt`文件，单向切换到 Eclipse CDT 自己的项目经理。相反，您继续通过`CMakeLists.txt`文件来管理您的项目，该文件仍然是您项目的主要配置文件。Eclipse CDT 积极地与您的`CMakeLists.txt`文件合作来构建您的项目。您可以在您的`CMakeLists.txt`中添加或删除源文件并进行其他更改，并且`cmake4eclipse`插件会在每次构建时将这些更改应用到 Eclipse CDT 项目中。您将有一个很好的集成开发环境体验，同时保持您的 CMake 项目最新。这种方法的好处是，您可以随时退出使用 Eclipse CDT，稍后使用您的`CMakeLists.txt`文件切换到另一个构建系统(如 Ninja)。我们将在下面的练习中使用第二种方法。\n\n### 练习 T1】E2:将 CMake 文件导入 Eclipse CDT\n\n在上一个练习中，您开发了一个 CMake 项目，并且希望开始使用 Eclipse CDT IDE 来编辑和构建该项目。在本练习中，我们将使用`cmake4eclipse`插件将我们的 CMake 项目导入到 Eclipse CDT IDE 中。执行以下步骤完成练习:\n\n1.  打开 Eclipse CDT。\n2.  Create a new C++ project in the location of our current project (the folder that contains the `CMakeLists.txt` file and the **src** folder). Go to **File** | **New** | **Project**. A **New Project** dialog box appears like the one in the following screenshot:\n\n    ![Figure 1.8: New Project dialog box ](img/C14508_01_08.jpg)\n\n    ###### 图 1.8:新建项目对话框\n\n3.  Select the **C++ Project** option and click on the **Next** button. A **C++ Project** dialog box appears like the one in the following screenshot:\n\n    ![Figure 1.9: C++ Project dialog box ](img/C14508_01_09.jpg)\n\n    ###### 图 1.9: C++ 项目对话框\n\n4.  接受一切，包括切换到 C/C++ 视角，点击**完成**。\n5.  Click on the **Restore** button at the top-left corner to view the newly created project:\n\n    ![Figure 1.10: The Restore button ](img/C14508_01_10.jpg)\n\n    ###### 图 1.10:恢复按钮\n\n6.  Click on the **CxxTemplate** project. Go to **Project** | **Properties**, then select **Tool Chain Editor** under **C/C++ Build** from the left pane and set **Current builder** to **CMake Builder (portable)**. Then, click on the **Apply and Close** button:\n\n    ![Figure 1.11: Project properties ](img/C14508_01_11.jpg)\n\n    ###### 图 1.11:项目属性\n\n7.  Then, choose the **Project** | **Build All** menu item to build the project:\n\n    ![Figure 1.12: Building the project ](img/C14508_01_12.jpg)\n\n    ###### 图 1.12:构建项目\n\n8.  In the following **Console** pane, you will see the output of CMake as if you called it from the command line, followed by a call to `make all` that actually builds our project:\n\n    ![Figure 1.13: The build output ](img/C14508_01_13.jpg)\n\n    ###### 图 1.13:构建输出\n\n9.  If you did not get any errors in the previous steps, you can run the project using the menu item **Run** | **Run**. If you are given some options, choose **Local C/C++ Application** and **CxxTemplate** as the executable:\n\n    ![Figure 1.14: Running a project ](img/C14508_01_14.jpg)\n\n    ###### 图 1.14:运行项目\n\n10.  当它运行时，您将在**控制台**窗格中看到程序的输出，如下所示:\n\n![Figure 1.15: Output of the project ](img/C14508_01_15.jpg)\n\n###### 图 1.15:项目输出\n\n您已经使用 Eclipse CDT 成功地构建并运行了一个 CMake 项目。在下一个练习中，我们将通过添加新的源文件和新的类来频繁地改变我们的项目。\n\n### 练习 3:向 CMake 和 Eclipse CDT 添加新的源文件\n\n当您开发大得多的 C++ 项目时，您将倾向于随着项目的增长向其中添加新的源文件，以满足设定的期望。在本练习中，我们将添加一个新的`。cpp`和`。h`文件对到我们的项目，看看 CMake 和 Eclipse CDT 是如何配合这些变化一起工作的。我们将使用新建类向导在项目中添加这些文件，但是您也可以使用任何其他文本编辑器创建它们。执行以下步骤向 CMake 和 Eclipse CDT 添加新的源文件:\n\n1.  First, open the project that we have been using until now. In the **Project Explorer** pane on the left, expand the root entry, **CxxTemplate**, and you will see the files and folders of our project. Right-click the **src** folder and select **New** | **Class** from the pop-up menu:\n\n    ![Figure 1.16: Creating a new class ](img/C14508_01_16.jpg)\n\n    ###### 图 1.16:创建一个新类\n\n2.  在打开的对话框中，为类名键入**一个类**。点击**完成**按钮，会看到 **ANewClass.cpp** 和 **ANewClass.h** 文件生成在 **src** 文件夹下。\n3.  Now, let's write some code into the `ANewClass` class and access it from the **CxxTemplate** class that we already had. Open `ANewClass.cpp` and change the beginning of the file to match the following, and then save the file:\n\n    ```cpp\n    #include \"ANewClass.h\"\n    #include <iostream>\n    void ANewClass::run() {\n        std::cout << \"Hello from ANewClass.\" << std::endl;\n    }\n    ```\n\n    您将会看到 Eclipse 通过一条**未找到成员声明**消息来警告我们:\n\n    ![Figure 1.17: Analyzer warning ](img/C14508_01_17.jpg)\n\n    ###### 图 1.17:分析仪警告\n\n    产生这个错误是因为我们需要将它添加到我们的`ANewClass.h`文件中。这样的警告可以通过 IDEs 中的分析器来实现，并且非常有用，因为它们可以帮助您在键入时修复代码，而无需运行编译器。\n\n4.  Open the `ANewClass.h` file, add the following code, and save the file:\n\n    ```cpp\n    public:\n        void run(); // we added this line\n        ANewClass();\n    ```\n\n    你应该看到`中的错误。cpp`文件走了。如果它没有消失，可能是因为您可能忘记保存其中一个文件。你应该养成按 *Ctrl + S* 保存当前文件的习惯，或者按 *Shift + Ctrl + S* 保存所有你编辑过的文件。\n\n5.  Now, let's use this class from our other class, `CxxTemplate.cpp`. Open that file, perform the following modifications, and save the file. Here, we are first importing header files and in the constructor of `CxxApplication`, we are printing text to the console. Then, we are creating a new instance of `ANewClass` and calling its `run` method:\n\n    ```cpp\n    #include \"CxxTemplate.h\"\n    #include \"ANewClass.h\"\n    #include <string>\n    ...\n    CxxApplication::CxxApplication( int argc, char *argv[] ) {\n      std::cout << \"Hello CMake.\" << std::endl;\n      ::ANewClass anew;\n      anew.run();\n    }\n    ```\n\n    #### 注意\n\n    这个文件的完整代码可以在这里找到:[https://github . com/trainingypbackt/Advanced-CPlusPlus/blob/master/lesson 1/execute 03/src/cxxtemplate . CPP](https://github.com/TrainingByPackt/Advanced-CPlusPlus/blob/master/Lesson1/Exercise03/src/CxxTemplate.cpp)。\n\n6.  Try to build the project by clicking on the **Project** | **Build All** menu options. You will get some undefined reference errors in two lines. This is because our project is built with CMake's rules and we did not let CMake know about this new file. Open the `CMakeLists.txt` file, make the following modification, and save the file:\n\n    ```cpp\n    add_executable(CxxTemplate\n      src/CxxTemplate.cpp  \n      src/ANewClass.cpp\n    )\n    ```\n\n    尝试再次构建项目。这次你应该看不到任何错误。\n\n7.  使用**运行** | **运行**菜单选项运行项目。您应该会在终端中看到以下输出:\n\n![Figure 1.18: Program output ](img/C14508_01_18.jpg)\n\n###### 图 1.18:程序输出\n\n您修改了一个 CMake 项目，向其中添加了新文件，并且运行良好。请注意，我们在`src`文件夹中创建了文件，并让`CMakeLists.txt`文件知道了 CPP 文件。如果您不使用 Eclipse，您可以简单地继续使用常见的 CMake 构建命令，您的程序将成功运行。到目前为止，我们已经检查了来自 GitHub 的示例代码，并用普通的 CMake 和 Eclipse IDE 构建了它。我们还在 CMake 项目中添加了一个新的类，并在 Eclipse IDE 中重新构建了它。现在您知道如何构建和修改 CMake 项目了。在下一节中，我们将执行一个向项目中添加新的源文件-头文件对的活动。\n\n### 活动 1:向项目添加一个新的源文件-头文件对\n\n在开发 C++ 项目时，随着项目的增长，您会向其中添加新的源文件。出于各种原因，您可能希望添加新的源文件。例如，假设您正在开发一个会计应用，在该应用中，您在项目的许多地方计算利率，并且您希望在单独的文件中创建一个函数，以便在整个项目中重用它。为了简单起见，这里我们将创建一个简单的求和函数。在本练习中，我们将向项目中添加一个新的源文件-头文件对。执行以下步骤完成活动:\n\n1.  在 Eclipse IDE 中打开我们在前面的练习中创建的项目。\n2.  将`SumFunc.cpp`和`SumFunc.h`文件对添加到项目中。\n3.  创建一个名为`sum`的简单函数，返回两个整数的和。\n4.  从`CxxTemplate`类构造函数调用该函数。\n5.  在 Eclipse 中构建和运行项目。\n\n预期输出应该类似于以下内容:\n\n![Figure 1.19: Final output ](img/C14508_01_19.jpg)\n\n###### 图 1.19:最终输出\n\n#### 注意\n\n这项活动的解决方案可以在第 620 页找到。\n\n在下一节中，我们将讨论如何为我们的项目编写单元测试。通常将项目分成许多类和函数，它们一起工作来实现期望的目标。您必须用单元测试来管理这些类和函数的行为，以确保它们以预期的方式运行。\n\n## 单元测试\n\n单元测试通常是编程的重要部分。基本上，单元测试是一些小程序，它们在各种场景中使用我们的类并产生预期的结果，在我们的项目中以并行的文件层次结构存在，最终不会出现在实际的可执行文件中，而是由我们在开发过程中单独执行，以确保我们的代码以预期的方式运行。我们应该为我们的 C++ 程序编写单元测试，以确保它们在每次更改后都像预期的那样运行。\n\n### 准备 U nit 测试\n\n有几个 C++ 测试框架我们可以和 CMake 一起使用。我们将使用**谷歌测试**，它比其他选项有几个好处。在下一个练习 e 中，我们将使用谷歌测试为单元测试准备我们的项目。\n\n### 练习 4:准备我们的单元测试项目\n\n我们已经安装了谷歌测试，但我们的项目没有设置为使用谷歌测试进行单元测试。除了安装，还有一些设置需要在我们的 CMake 项目中进行，以便进行谷歌测试单元测试。按照以下步骤实施本练习:\n\n1.  打开 Eclipse CDT，选择我们一直在使用的 CxxTemplate 项目。\n2.  创建一个名为**测试**的新文件夹，因为我们将在那里执行所有测试。\n3.  Edit our base `CMakeLists.txt` file to allow tests in the **tests** folder. Note that we already had code to find the `GTest` package that brings `GoogleTest` capability to CMake. We will add our new lines just after that:\n\n    ```cpp\n    find_package(GTest)\n    if(GTEST_FOUND)\n    set(Gtest_FOUND TRUE)\n    endif()\n    if(GTest_FOUND)\n    include(GoogleTest)\n    endif()\n    # add these two lines below\n    enable_testing()\n    add_subdirectory(tests)\n    ```\n\n    这就是我们需要添加到主`CMakeLists.txt`文件中的全部内容。\n\n4.  在我们的**测试**文件夹中创建另一个**文件。这将被使用，因为我们在主 **CMakeLists.txt** 文件中有**add _ 子目录(测试)**行。这个**测试/CMakeLists.txt** 文件将管理测试源。**\n5.  Add the following code in the `tests/CMakeLists.txt` file:\n\n    ```cpp\n    include(GoogleTest)\n    add_executable(tests CanTest.cpp)\n    target_link_libraries(tests GTest::GTest)\n    gtest_discover_tests(tests)\n    ```\n\n    让我们一行行地剖析这段代码。第一行引入了谷歌测试功能。第二行创建**测试**可执行文件，它将包括我们所有的测试源文件。在这种情况下，我们只有一个 **CanTest.cpp** 文件，它将只是验证测试工作。之后，我们将 **GTest** 库链接到**测试**可执行文件。最后一行标识了可执行的**测试**中的所有单独测试，并将它们添加到**中作为测试。这样，各种测试工具将能够告诉我们哪些单独的测试失败了，哪些通过了。**\n\n6.  创建一个`测试/CanTest.cpp`文件。添加这段代码只是为了验证测试正在运行，而不是实际测试我们实际项目中的任何东西:\n\n    ```cpp\n    #include \"gtest/gtest.h\"\n    namespace {\n    class CanTest: public ::testing::Test {};\n    TEST_F(CanTest, CanReallyTest) {\n      EXPECT_EQ(0, 0);\n    }\n    }  \n    int main(int argc, char **argv) {\n      ::testing::InitGoogleTest(&argc, argv);\n      return RUN_ALL_TESTS();\n    }\n    ```\n\n`TEST_F`线为单独测试。现在，`EXPECT_EQ(0，0)`正在测试零是否等于零，如果我们真的能运行测试，总是会成功的。稍后，我们将在这里添加我们自己的类的结果，以针对各种值进行测试。现在，我们已经在我们的项目中为谷歌测试进行了必要的设置。接下来，我们将构建并运行这些测试。\n\n### 构建、运行、和编写单元测试\n\n现在，我们将讨论如何构建、运行和编写单元测试。到目前为止，我们的例子是一个简单的虚拟测试，已经准备好构建和运行。稍后，我们将添加更有意义的测试，并查看通过和失败测试的输出。在下面的练习中，我们将为我们在前面练习中创建的项目构建、运行和编写单元测试。\n\n### 练习 5:构建 g 并运行测试\n\n到目前为止，您已经创建了一个设置了`GoogleTest`的项目，但是您没有构建或运行我们创建的测试。在本练习中，我们将构建并运行我们创建的测试。由于我们使用`add _ 子目录`添加了我们的`测试`文件夹，构建项目将自动构建测试。运行测试需要更多的努力。执行以下步骤完成练习:\n\n1.  在 Eclipse CDT 中打开我们的 CMake 项目。\n2.  To build the tests, simply build the project just like you did before. Here is the output of building the project one more time from Eclipse after a full build using **Project** | **Build All**:\n\n    ![Figure 1.20: Build operation and its output ](img/C14508_01_20.jpg)\n\n    ###### 图 1.20:构建操作及其输出\n\n3.  If you do not see this output, your console may be in the wrong view. You can correct it as shown in the following figures:\n\n    ![Figure 1.21: Viewing the correct console output ](img/C14508_01_21.jpg)\n\n    ###### 图 1.21:查看正确的控制台输出\n\n    ![Figure 1.22: Viewing the correct console output ](img/C14508_01_22.jpg)\n\n    ###### 图 1.22:查看正确的控制台输出\n\n    如您所见，我们的项目现在有两个可执行的目标。他们都生活在`构建`文件夹中，就像任何其他构建神器一样。它们的位置是`构建/调试/扩展`和`构建/调试/测试/测试`。因为它们是可执行文件，所以我们可以简单地运行它们。\n\n4.  We ran `CxxTemplate` before and will not see any extra output now. Run the other executable by typing the following command in the terminal while we are in our project folder:\n\n    ```cpp\n    ./build/Debug/tests/tests\n    ```\n\n    上述代码在终端中生成以下输出:\n\n    ![Figure 1.23: Running the tests executable ](img/C14508_01_23.jpg)\n\n    ###### 图 1.23:运行可执行的测试\n\n    这是我们的`测试`可执行文件的简单输出。如果你想看看测试是否通过，你可以简单地运行这个。然而，测试远不止于此。\n\n5.  One of the ways you can run your tests is by using the `ctest` command. Write the following commands in the terminal while you are in the project folder. We go to the folder where the `tests` executable resides, run `ctest` there, and come back:\n\n    ```cpp\n    cd build/Debug/tests\n    ctest\n    cd ../../..\n    ```\n\n    这是您将看到的输出:\n\n    ![Figure 1.24: Running ctest ](img/C14508_01_24.jpg)\n\n    ###### 图 1.24:运行 ctest\n\n    #### 注意\n\n    `ctest`命令可以运行您的`测试`可执行文件，其中有许多选项，包括自动将测试结果提交到在线仪表板的能力。在这里，我们将简单地运行`ctest`命令；它的其他特性留给感兴趣的读者作为练习。您可以键入`ctest - help`或访问在线文档，在[https://cmake.org/cmake/help/latest/manual/ctest.1.html#](https://cmake.org/cmake/help/latest/manual/ctest.1.html#)进一步了解`ctest`。\n\n6.  运行测试的另一种方法是在 Eclipse 中以一种漂亮的图形报告格式运行它们。为此，我们将创建一个测试感知的运行配置。在 Eclipse 中，点击**运行** | **运行配置…** ，右键点击左侧 **C/C++ 单元**，选择**新配置**。\n7.  Change the name from **CxxTemplate Debug** to **CxxTemplate Tests** as follows:\n\n    ![Figure 1.25: Changing the name of the run configuration ](img/C14508_01_25.jpg)\n\n    ###### 图 1.25:更改运行配置的名称\n\n8.  Under **C/C++ Application**, select the **Search Project** option:\n\n    ![Figure 1.26: Run Configurations ](img/C14508_01_26.jpg)\n\n    ###### 图 1.26:运行配置\n\n9.  Choose **tests** in the new dialog:\n\n    ![Figure 1.27: Creating the test run configuration and selecting the tests executable  ](img/C14508_01_27.jpg)\n\n    ###### 图 1.27:创建测试运行配置并选择可执行的测试\n\n10.  Next, go to the **C/C++ Testing** tab and select **Google Tests Runner** in the dropdown. Click on **Apply** at the bottom of the dialog and click on the **Run** option for the test that we have to run for the first time:\n\n    ![Figure 1.28: Run Configurations ](img/C14508_01_28.jpg)\n\n    ###### 图 1.28:运行配置\n\n11.  在接下来的运行中，您可以点击工具栏中播放按钮旁边的下拉菜单，或者选择**运行** | **运行历史**选择**扩展测试**:\n\n![Figure 1.29: Finalizing the run configuration settings and selecting a configuration to run ](img/C14508_01_29.jpg)\n\n###### 图 1.29:最终确定运行配置设置并选择要运行的配置\n\n结果将类似于下面的截图:\n\n![Figure 1.30: Run results of the unit test ](img/C14508_01_30.jpg)\n\n###### 图 1.30:运行单元测试的结果\n\n这是一份很好的报告，包含了所有测试的条目——目前只有一个。如果不想离开集成开发环境，您可能更喜欢这样。此外，当您有许多测试时，这个界面可以帮助您有效地过滤它们。现在，您已经构建并运行了使用谷歌测试编写的测试。您以几种不同的方式运行它们，包括直接执行测试、使用`ctest`和使用 Eclipse CDT。在下一节中，我们将解决一个练习，其中我们将实际测试代码的功能。\n\n### 练习 6:测试代码的功能\n\n您已经运行了简单的测试，但是现在您想要编写测试功能的有意义的测试。在最初的活动中，我们创建了`SumFunc.cpp`，它具有`sum`功能。现在，在本练习中，我们将为该文件编写一个测试。在本测试中，我们将使用`求和`功能将两个数字相加，并验证结果是否正确。让我们用之前的`sum`函数回忆一下以下文件的内容:\n\n*   `src/SumFunc.h` :\n\n    ```cpp\n    #ifndef SRC_SUMFUNC_H_\n    #define SRC_SUMFUNC_H_\n    int sum(int a, int b);\n    #endif /* SRC_SUMFUNC_H_ */\n    ```\n\n*   `src/SumFunc.cpp` :\n\n    ```cpp\n    #include \"SumFunc.h\"\n    #include <iostream>\n    int sum(int a, int b) {\n      return a + b;\n    }\n    ```\n\n*   `CMakeLists.txt`相关行:\n\n    ```cpp\n    add_executable(CxxTemplate\n      src/CxxTemplate.cpp  \n      src/ANewClass.cpp\n      src/SumFunc.cpp\n    )\n    ```\n\n另外，让我们回忆一下我们的`cantest . CPP`文件，它有我们单元测试的`main()`功能:\n\n```cpp\n#include \"gtest/gtest.h\"\nnamespace {\nclass CanTest: public ::testing::Test {};\nTEST_F(CanTest, CanReallyTest) {\n  EXPECT_EQ(0, 0);\n}\n}  \nint main(int argc, char **argv) {\n  ::testing::InitGoogleTest(&argc, argv);\n  return RUN_ALL_TESTS();\n}\n```\n\n执行以下步骤完成练习:\n\n1.  在 Eclipse CDT 中打开我们的 CMake 项目。\n2.  Add a new test source file (`tests/SumFuncTest.cpp`) with the following content:\n\n    ```cpp\n    #include \"gtest/gtest.h\"\n    #include \"../src/SumFunc.h\"\n    namespace {\n      class SumFuncTest: public ::testing::Test {};\n      TEST_F(SumFuncTest, CanSumCorrectly) {\n        EXPECT_EQ(7, sum(3, 4));\n      }\n    }\n    ```\n\n    请注意，这没有`main()`功能，因为`CanTest.cpp`有一个功能，这些功能将链接在一起。其次，注意这包括`SumFunc.h`，它在项目的 **src** 文件夹中，在测试中用作`sum(3，4)`。这就是我们在测试中使用项目代码的方式。\n\n3.  Make the following change in the `tests/CMakeLists.txt` file to build the test:\n\n    ```cpp\n    include(GoogleTest)\n    add_executable(tests CanTest.cpp SumFuncTest.cpp ../src/SumFunc.cpp) # added files here\n    target_link_libraries(tests GTest::GTest)\n    gtest_discover_tests(tests)\n    ```\n\n    注意，我们添加了测试(`SumFuncTest.cpp`)和它测试的代码(`../src/SumFunc.cpp`)转换为可执行文件，因为我们的测试代码使用的是实际项目中的代码。\n\n4.  Build the project and run the test as before. You should see the following report:\n\n    ![Figure 1.31: Output after running the test ](img/C14508_01_31.jpg)\n\n    ###### 图 1.31:运行测试后的输出\n\n    我们可以将这样的测试添加到我们的项目中，所有的测试都会出现在屏幕上，如前面的截图所示。\n\n5.  Now, let's add one more test that will actually fail. In the `tests/SumFuncTest.cpp` file, make the following change:\n\n    ```cpp\n    TEST_F(SumFuncTest, CanSumCorrectly) {\n      EXPECT_EQ(7, sum(3, 4));\n    }\n    // add this test\n    TEST_F(SumFuncTest, CanSumAbsoluteValues) {\n      EXPECT_EQ(6, sum(3, -3));\n    }\n    ```\n\n    请注意，该测试假设输入的绝对值相加，这是不正确的。这个调用的结果是`0`，但是在这个例子中预计是`6`。这是我们在项目中添加这个测试所必须做的唯一改变。\n\n6.  Now, build the project and run the test. You should see this report:\n\n    ![Figure 1.32: The build report ](img/C14508_01_32.jpg)\n\n    ###### 图 1.32:构建报告\n\n    从上图中可以看到，前两次测试通过，最后一次测试失败。当我们看到这个输出时，有两种选择:要么我们的项目代码是错误的，要么测试是错误的。在这种情况下，我们的测试是错误的。这是因为我们的 **CanSumAbsoluteValues** 测试用例期望`6`等于`sum(3，-3)`。这是因为我们假设我们的函数对提供的整数的绝对值求和。然而，事实并非如此。我们的函数只是将给定的数字相加，不管它们是正数还是负数。因此，这个测试有一个错误的假设，失败了。\n\n7.  让我们更改测试并修复它。更改测试，使我们预期`-3`和`3`之和为`0`。重命名测试，以反映该测试的实际作用:\n\n    ```cpp\n    TEST_F(SumFuncTest, CanSumCorrectly) {\n      EXPECT_EQ(7, sum(3, 4));\n    }\n    // change this part\n    TEST_F(SumFuncTest, CanUseNegativeValues) {\n      EXPECT_EQ(0, sum(3, -3));\n    }\n    ```\n\n8.  现在运行它，并在报告中观察所有测试是否通过:\n\n![Figure 1.33: Test execution is successful ](img/C14508_01_33.jpg)\n\n###### 图 1.33:测试执行成功\n\n最后，我们已经在我们的系统和项目中使用 CMake 建立了谷歌测试。我们还使用谷歌测试在终端和 Eclipse 中编写、构建和运行单元测试。理想情况下，您应该为每个类编写单元测试，并涵盖所有可能的用法。您还应该在每次重大更改后运行测试，并确保不破坏现有代码。在下一节中，我们将执行形成一个添加新类及其测试的活动。\n\n### 活动 2:在测试中添加一个新类\n\n当您开发一个 C++ 项目时，您会随着项目的增长向其中添加新的源文件。您还为他们编写测试，以确保他们正常工作。在本练习中，我们将添加一个模拟`1D`直线运动的新类。该类将具有用于`位置`和`速度`的双字段。它还将有一个`advanceTimeBy()`方法，该方法接收一个双`dt`参数，该参数基于`速度`的值修改`位置`。双数值用`EXPECT_DOUBLE_EQ`代替`EXPECT_EQ`。在本活动中，我们将向项目中添加一个新类及其测试。按照以下步骤执行本活动:\n\n1.  打开我们在 Eclipse 集成开发环境中创建的项目。\n2.  将`LinearMotion1D.cpp`和`LinearMotion1D.h`文件对添加到包含`LinearMotion1D`类的项目中。在这个类中，创建两个双字段:`位置`和`速度`。另外，创建一个`提前时间比(双 dt)`功能，修改`位置`。\n3.  在`测试/linear motion 1 test . CPP`文件中为此编写测试。写两个代表两个不同方向运动的测试。\n4.  在 Eclipse IDE 中构建并运行它。\n5.  验证测试是否通过。\n\n最终测试结果应该类似于以下内容:\n\n![Figure 1.34: Final test results ](img/C14508_01_34.jpg)\n\n###### 图 1.34:最终测试结果\n\n#### 注意\n\n这项活动的解决方案可以在第 622 页找到。\n\n添加新类及其测试是 C++ 开发中非常常见的任务。我们创建类有各种原因。有时，我们有一个很好的软件设计计划，我们创建它所需要的类。其他时候，当一个类变得过于庞大和单一时，我们会以一种有意义的方式将它的一些职责分离给另一个类。让这个任务变得实际很重要，这样可以防止你拖拖拉拉，最终得到一个巨大的整体类。在下一节中，我们将讨论编译和链接阶段会发生什么。这将让我们更好地了解 C++ 程序下正在发生的事情。\n\n## 了解编译、链接和目标文件内容\n\n使用 C++ 的一个主要原因是效率。C++ 让我们可以控制内存管理，这就是为什么理解对象在内存中的布局很重要。此外，C++ 源文件和库被编译成目标硬件的目标文件并链接在一起。通常，C++ 程序员必须处理链接器问题，这就是为什么理解编译步骤并能够研究目标文件很重要。另一方面，大型项目是由团队长时间开发和维护的，这就是为什么创建干净和可理解的代码很重要。与任何其他软件一样，C++ 项目中会出现错误，需要通过观察程序行为来仔细识别、分析和解决。因此，学习如何调试 C++ 代码也很重要。在下一节中，我们将学习如何创建高效的、与其他代码配合良好的、可维护的代码。\n\n### 编译和链接步骤\n\nC++ 项目是作为一组源代码文件和项目配置文件创建的，这些文件组织了源代码和库依赖项。在编译步骤中，首先将这些源转换为目标文件。在链接步骤中，这些目标文件被链接在一起形成可执行文件，这是项目的最终输出。项目使用的库也在这一步链接。\n\n在接下来的练习中，我们将使用我们现有的项目来观察编译和链接阶段。然后，我们将手动重新创建它们，以更详细地查看流程。\n\n### 练习 7:识别构建步骤\n\n您一直在构建项目，而没有调查构建操作的细节。在本练习中，我们将研究项目构建步骤的细节。执行以下操作来完成练习:\n\n1.  打开终端。\n2.  通过键入以下命令导航到`构建`文件夹，我们的`Makefile`文件位于该文件夹中:\n\n    ```cpp\n    cd build/Debug\n    ```\n\n3.  Clean the project and run the build in `VERBOSE` mode using the following command:\n\n    ```cpp\n    make clean \n    make VERBOSE=1 all\n    ```\n\n    您将在终端中获得构建过程的详细输出，这可能看起来有点拥挤:\n\n    ![Figure 1.35: The build process part 1 ](img/C14508_01_35.jpg)\n\n    ###### 图 1.35:构建过程第 1 部分\n\n    ![Figure 1.36: The build process part 2 ](img/C14508_01_36.jpg)\n\n    ###### 图 1.36:构建过程第 2 部分\n\n    ![Figure 1.37: The full build output ](img/C14508_01_37.jpg)\n\n    ###### 图 1.37:完整的构建输出\n\n    下面是这个输出中的一些行。以下几行是与主可执行文件的编译和链接相关的重要内容:\n\n    ```cpp\n    /usr/bin/c++    -g   -pthread -std=gnu++ 1z -o CMakeFiles/CxxTemplate.dir/src/CxxTemplate.cpp.o -c /home/username/Packt/Cpp2019/CxxTemplate/src/CxxTemplate.cpp\n    /usr/bin/c++    -g   -pthread -std=gnu++ 1z -o CMakeFiles/CxxTemplate.dir/src/ANewClass.cpp.o -c /home/username/Packt/Cpp2019/CxxTemplate/src/ANewClass.cpp\n    /usr/bin/c++    -g   -pthread -std=gnu++ 1z -o CMakeFiles/CxxTemplate.dir/src/SumFunc.cpp.o -c /home/username/Packt/Cpp2019/CxxTemplate/src/SumFunc.cpp\n    /usr/bin/c++    -g   -pthread -std=gnu++ 1z -o CMakeFiles/CxxTemplate.dir/src/LinearMotion1D.cpp.o -c /home/username/Packt/Cpp2019/CxxTemplate/src/LinearMotion1D.cpp\n    /usr/bin/c++  -g   CMakeFiles/CxxTemplate.dir/src/CxxTemplate.cpp.o CMakeFiles/CxxTemplate.dir/src/ANewClass.cpp.o CMakeFiles/CxxTemplate.dir/src/SumFunc.cpp.o CMakeFiles/CxxTemplate.dir/src/LinearMotion1D.cpp.o  -o CxxTemplate -pthread \n    ```\n\n4.  The `c++ ` command here is just a symbolic link to the `g++ ` compiler. To see that it's actually a chain of symbolic links, type the following command:\n\n    ```cpp\n    namei /usr/bin/c++\n    ```\n\n    您将看到以下输出:\n\n    ![Figure 1.38: The chain of symbolic links for /usr/bin/c++ ](img/C14508_01_38.jpg)\n\n    ###### 图 1.38:用于/usr/bin/c++ 的符号链接链\n\n    因此，我们将在整个讨论中交替使用`c++ `和`g++ `。在我们前面引用的构建输出中，前四行是编译每个`。cpp`源文件并创建相应的`。o`对象文件。最后一行是将这些目标文件链接在一起，创建`CxxTemplate`可执行文件。下图直观地展示了这一过程:\n\n    ![Figure 1.39: Execution stages of a C++ project ](img/C14508_01_39.jpg)\n\n    ###### 图 1.39:c++ 项目的执行阶段\n\n    如上图所示，作为目标的一部分添加到 CMake 的 CPP 文件，连同它们包含的头文件一起，被编译成目标文件，这些文件随后被链接在一起以创建目标可执行文件。\n\n5.  为了进一步理解这个过程，让我们自己执行编译步骤。在终端中，转到项目文件夹，使用以下命令创建一个名为`mybuild`的新文件夹:\n\n    ```cpp\n    cd ~/CxxTemplate\n    mkdir mybuild\n    ```\n\n6.  然后，运行以下命令将 CPP 源文件编译为目标文件:\n\n    ```cpp\n    /usr/bin/c++ src/CxxTemplate.cpp -o mybuild/CxxTemplate.o -c \n    /usr/bin/c++ src/ANewClass.cpp -o mybuild/ANewClass.o -c \n    /usr/bin/c++ src/SumFunc.cpp -o mybuild/SumFunc.o -c \n    /usr/bin/c++ src/LinearMotion1D.cpp -o mybuild/LinearMotion1D.o -c \n    ```\n\n7.  Go into the `mybuild` directory and see what's there using the following command:\n\n    ```cpp\n    cd mybuild\n    ls \n    ```\n\n    我们看到如下预期的输出。这些是我们的目标文件:\n\n    ![Figure 1.40: Compiled object files ](img/C14508_01_40.jpg)\n\n    ###### 图 1.40:编译的目标文件\n\n8.  下一步，将目标文件链接在一起，形成我们的可执行文件。键入以下命令:\n\n    ```cpp\n    /usr/bin/c++  CxxTemplate.o ANewClass.o SumFunc.o LinearMotion1D.o  -o CxxTemplate \n    ```\n\n9.  Now, let's see our executable among the list of files here by typing the following command:\n\n    ```cpp\n    ls \n    ```\n\n    这将在下图中显示新的`CxxTemplate`文件:\n\n    ![Figure 1.41: Linked executable file ](img/C14508_01_41.jpg)\n\n    ###### 图 1.41:链接的可执行文件\n\n10.  Now, run our executable by typing the following command:\n\n    ```cpp\n    ./CxxTemplate\n    ```\n\n    看看我们之前的输出:\n\n    ![Figure 1.42: Executable file output ](img/C14508_01_42.jpg)\n\n###### 图 1.42:可执行文件输出\n\n现在，您已经检查了构建过程的细节并自己重新创建了它们，在下一节中，让我们来探索链接过程。\n\n### 连接步骤\n\n在这一节中，让我们来看看两个源文件之间的连接，以及它们如何在同一个可执行文件中结束。请看下图中的**求和**功能:\n\n![Figure 1.43: The linking process ](img/C14508_01_43.jpg)\n\n###### 图 1.43:链接过程\n\n**sum** 函数的主体在 **SumFunc.cpp** 中定义。它在 **SumFunc.h** 中有一个远期申报。这样，想要使用 **sum** 函数的源文件就可以知道它的签名了。一旦他们知道它的签名，他们就可以调用它，并相信实际的函数定义将在运行时存在，而实际上与定义函数的 **SumFunc.cpp** 没有任何交互。\n\n编译后，调用 **sum** 函数的 **CxxTemplate.cpp** 将该调用携带到其目标文件中。但是，它不知道函数定义在哪里。 **SumFunc.cpp** 的对象文件有这个定义，但是和 **CxxTemplate.o** 还没有关系。\n\n在链接步骤中，链接器将 **CxxTemplate.o** 中的调用与 **SumFunc.o** 中的定义进行匹配。因此，该调用在可执行文件中运行良好。如果链接器没有找到 **sum** 函数的定义，它会给出一个链接器错误。\n\n链接器通过名称和参数找到**和**函数。这叫做**解析符号**。对象文件中定义的类、函数和变量放在符号表中，对符号的每个引用都通过在该表中查找来解析。当符号不存在时，您会收到一个`符号无法解析`错误。\n\n这让我们经历了构建过程的两个阶段:`编译`和`链接`。请注意，当我们手动编译源代码时，我们使用了比 CMake 更简单的命令。请随意输入`man g++ `查看所有选项。后来，我们讨论了链接和如何解决符号。我们还讨论了链接步骤中可能出现的问题。在下一节中，我们将了解目标文件。\n\n### 深入:查看对象文件\n\n为了使链接步骤没有错误，我们需要让所有的符号引用与我们的符号定义相匹配。大多数时候，我们可以通过查看源文件来分析事情将如何解决。有时，在复杂的情况下，我们可能很难理解为什么一个符号没有被解析。在这种情况下，查看对象文件的内容来研究引用和定义对于解决问题可能很有用。除了链接器错误之外，理解目标文件内容以及链接一般是如何工作的对 C++ 程序员来说也很有用。了解幕后发生的事情可能有助于程序员更好地理解整个过程。\n\n当我们的源代码被编译成目标文件时，我们的语句和表达式被转换成汇编代码，这是中央处理器理解的低级语言。汇编中的每条指令都包含一个操作，后面是操作符，它们是中央处理器的寄存器。有向寄存器加载数据和从寄存器加载数据以及对寄存器中的值进行操作的指令。Linux 中的`objdump`命令帮助我们查看这些目标文件的内容。\n\n#### 注意\n\n我们将利用编译器资源管理器，这是一个很好的在线工具，更容易使用，你可以在左边的窗口写代码，在右边，你可以看到编译后的汇编代码。这是编译器浏览器的链接:https://godbolt.org。\n\n### 练习 8:探索编译代码\n\n在本练习中，我们将使用编译器资源管理器来编译一些简单的 C++ 代码，在这些代码中我们定义并调用一个函数。我们将研究编译后的程序集代码，以了解如何准确解析名称和进行调用。这将使我们更好地理解幕后发生了什么，以及我们的代码是如何以可执行格式工作的。执行以下步骤完成练习:\n\n1.  Add the following code in **Compiler Explorer**:\n\n    ```cpp\n    int sum(int a, int b) {\n        return a + b;\n    }\n    int callSum() {\n        return sum(4, 5);\n    }\n    ```\n\n    我们有两个功能；一个在呼叫另一个。下面是编译后的输出:\n\n    ![Figure 1.44: The compiled code ](img/C14508_01_44.jpg)\n\n    ###### 图 1.44:编译后的代码\n\n    虽然不太清楚，但你或多或少能看出它在做什么。我们不打算深入讨论汇编代码的细节，但我们将重点关注在链接器阶段如何解析符号。现在让我们关注以下几行:\n\n    ```cpp\n    sum(int, int):\n\n    ...\n    callSum():\n\n    ...\n            call  sum(int, int)\n\n    ...\n    ```\n\n    `调用 sum(int，int)`行实现了您所期望的:它调用前面的`sum`函数，并将参数放在一些寄存器中。这里重要的一点是，函数是由它们的名称和参数类型按顺序标识的。链接器用这个签名寻找合适的函数。请注意，返回值不是签名的一部分。\n\n2.  Disable the **Demangle** checkbox and see how these function names are actually stored:\n\n    ![Figure 1.45: Compiled code without demangling ](img/C14508_01_45.jpg)\n\n    ###### 图 1.45:没有解混的编译代码\n\n    这里，我们的台词变成了这样:\n\n    ```cpp\n    _Z3sumii:\n\n    ...\n    _Z7callSumv:\n\n    ...\n            call    _Z3sumii\n\n    ...\n    ```\n\n    前面是这些函数的错误名称。在`_Z`之后，数字告诉我们函数名有多长，以便正确解释下面的字母。在函数名之后，没有参数的是`v`，参数的是`I``int`。您可以更改这些函数签名来查看其他可能的类型。\n\n3.  Now, let's look at how classes are compiled. Add the following code into **Compiler Explorer** under the existing code:\n\n    ```cpp\n    class MyClass {\n    private:\n        int a = 5;\n        int myPrivateFunc(int i) {\n            a = 4;\n            return i + a;\n        }\n    public:\n        int b = 6;\n        int myFunc(){ \n            return sum(1, myPrivateFunc(b));\n        }\n    };\n    MyClass myObject;\n    int main() {\n        myObject.myFunc();\n    }\n    ```\n\n    以下是这些新增行的编译版本:\n\n    ![Figure 1.46: The compiled version ](img/C14508_01_46.jpg)\n\n###### 图 1.46:编译版本\n\n您可能会惊讶于编译代码中没有类定义。这些方法类似于全局函数，但有一点不同:它们的变形名称包含类名，并且它们接收对象实例作为参数。创建实例只是为类的字段分配空间。\n\n在链接器阶段，这些损坏的函数名被用来匹配调用者和被调用者。对于找不到被调用方的调用方，我们会得到链接器错误。大多数链接器错误可以通过仔细检查源代码来解决。但是，在某些情况下，使用`objdump`查看对象文件内容有助于找到问题的根源。\n\n## 调试 C++ 代码\n\n在开发 C++ 项目时，您可能会遇到不同级别的问题:\n\n*   首先，您可能会收到编译器错误。这可能是因为您在语法上犯了一个错误，或者对类型的错误选择，等等。编译器是你必须跳过的第一个环，它会捕捉到你可能犯的一些错误。\n*   第二个环是接头。在那里，一个典型的错误是使用声明的东西，但没有实际定义。当您为库使用了错误的头文件时，这种情况经常发生，头文件会通告任何源文件或库中都不存在的特定签名。一旦你也跳过链接环，你的程序就可以执行了。\n*   现在，下一个要跳过的环是避免任何运行时错误。您的代码可能已经正确编译和链接，但它可能正在做一些不起作用的事情，例如取消对空指针的引用或除以零。\n\n要查找和修复运行时错误，您必须以某种方式与正在运行的应用进行交互并对其进行监控。一种常用的技术是向代码中添加`打印`语句，并监控它生成的日志，希望将应用行为与日志相关联，以查明代码中有问题的区域。虽然这适用于某些情况，但有时您需要更仔细地查看执行情况。\n\n调试器是对抗运行时错误的更好工具。调试器可以让你一行一行地运行代码，继续运行并暂停在你想要的行上，调查内存的值，暂停在错误上，等等。这让您可以观察程序运行时内存中到底发生了什么，并识别导致不需要的行为的代码行。\n\n`gdb`是可以调试 C++ 程序的规范命令行调试器。然而，这可能很难使用，因为调试本质上是一项可视化任务——您希望能够同时查看代码行、变量值和程序输出。幸运的是，Eclipse CDT 包含一个易于使用的可视化调试器。\n\n### 练习 9:使用 Eclipse CDT 进行调试\n\n您只是在运行您的项目并查看输出。现在你想学习如何详细调试你的代码。在本练习中，我们将探索 Eclipse CDT 的调试功能。执行以下步骤完成练习:\n\n1.  在 Eclipse CDT 中打开 CMake 项目。\n2.  To ensure that we have an existing run configuration, click **Run** | **Run Configurations**. There, you should see a **CxxTemplate** entry under **C/C++ Application**.\n\n    #### 注意\n\n    既然我们之前运行了我们的项目，它应该在那里。如果没有，请返回并重新创建。\n\n3.  关闭对话框继续。\n4.  To start the debugger, find the toolbar entry that looks like an insect (bug) and click on the dropdown next to it. Select **CxxTemplate** to debug the main application. If it asks you to switch to the debug perspective, accept. Now, this is what Eclipse will look like:\n\n    ![Figure 1.47: Eclipse debug screen ](img/C14508_01_47.jpg)\n\n    ###### 图 1.47: Eclipse 调试屏幕\n\n    此时，我们的代码冻结在我们的`main()`函数的第一行，该函数在代码视图的中间用绿色高亮和箭头显示。在左边，我们看到正在运行的线程，其中只有一个。在右边，我们看到了在这种情况下可以访问的变量。在底部，我们看到了 Eclipse 在幕后实际调试可执行文件时使用的 **gdb** 输出。现在，我们的主要功能没有太多需要调试的地方。\n\n5.  单击**运行**菜单下的**跳过**，或者在工具栏中单击几次，应用将很快终止。最后，你会看到`libc-start.c`库，它是`主`功能的调用者。完成后，您可以关闭它并切换到源文件。当你不再看到红色的停止按钮时，你就知道程序执行结束了。\n6.  Edit our `main` function by adding the following code:\n\n    ```cpp\n    int i = 1, t = 0;\n    do {\n      t += i++ ;\n    } while (i <= 3);\n    std::cout << t << std::endl;\n    ```\n\n    增量后操作符与偶尔的`do-while`循环混合在一起，对某些人来说可能会令人头疼。这是因为我们试图在头脑中执行算法。然而，我们的调试器完全能够一步一步地运行它，并向我们展示在执行过程中到底发生了什么。\n\n7.  Start debugging after adding the preceding code. Click on the dropdown next to the **Debug** button in the toolbar and select **CxxTemplate**. Press *F6* a couple of times to step over in the code. It will show us how the variables change as well as the line of code that will be executed next:\n\n    ![Figure 1.48: Stepping over the code ](img/C14508_01_48.jpg)\n\n    ###### 图 1.48:跳过代码\n\n8.  Seeing the variables change after the execution of each line of code makes the algorithm much clearer to understand. As you press *F6*, note that the following are the values after each execution of the `t += i++ ;` line:\n\n    ![Figure 1.49: Variable states through time ](img/C14508_01_49.jpg)\n\n    ###### 图 1.49:随时间变化的状态\n\n    前面的输出清楚地解释了值是如何变化的，以及为什么在最后打印`6`。\n\n9.  Explore other features of the debugger. While the variable view is useful, you can also hover over any variable and browse its value:\n\n    ![Figure 1.50: View option of the debugger ](img/C14508_01_50.jpg)\n\n    ###### 图 1.50:调试器的视图选项\n\n    此外，**表达式**视图可以帮助您计算从您浏览的值中不清楚的东西。\n\n10.  Click on **Expression** on the right-hand side and click on the **Add** button:\n\n    ![Figure 1.51: Adding an expression ](img/C14508_01_51.jpg)\n\n    ###### 图 1.51:添加表达式\n\n11.  Type **t+i** and hit *Enter*. Now you see the total in the list of expressions:\n\n    ![Figure 1.52: Expression view with a new expression ](img/C14508_01_52.jpg)\n\n    ###### 图 1.52:带有新表达式的表达式视图\n\n    可以按工具栏中的红方，也可以选择**运行** | **终止**随时停止调试。另一个特性是断点，它告诉调试器每当它到达一个标有断点的行时就暂停。到目前为止，我们一直在一行行地遍历我们的代码，这在大型项目中可能非常耗时。相反，您通常希望继续执行，直到它到达您感兴趣的代码。\n\n12.  Now, instead of going line by line, add a breakpoint in the line that does the printing. For this, double-click on the area to the left of the line number of this line. In the following figure, the dot represents a breakpoint:\n\n    ![Figure 1.53: Working with breakpoints ](img/C14508_01_53.jpg)\n\n    ###### 图 1.53:使用断点\n\n13.  Now start the debugger. As usual, it will start paused. Now select **Run** | **Resume** or click on the toolbar button. It will run the three executions of the loop and pause at our breakpoint. This way, we saved time by stepping through code that we are not investigating:\n\n    ![Figure 1.54: Working with the debugger ](img/C14508_01_54.jpg)\n\n    ###### 图 1.54:使用调试器\n\n14.  当我们一直在处理我们添加的循环时，我们忽略了创建`应用`对象的线。**跳过**命令跳过了这一行。然而，我们也可以选择进入这一行的构造函数调用。为此，我们将使用**运行** | **进入**或相应的工具栏按钮。\n15.  Stop the debugger and start it again. Click on **Step Over** to go to the line where the application is created:\n\n    ![Figure 1.55: Working with the debugger – the Step Over option ](img/C14508_01_55.jpg)\n\n    ###### 图 1.55:使用调试器–单步执行选项\n\n16.  突出显示的是下一行，如果我们再次单步执行，它将被执行。相反，请按“进入”按钮。这将带我们进入构造函数调用:\n\n![Figure 1.56: Working with the debugger – the Step Into option ](img/C14508_01_56.jpg)\n\n###### 图 1.56:使用调试器——单步执行选项\n\n这是一个方便的功能，可以更深入地了解函数，而不是简单地跳过它。另外，请注意左侧调试视图中的调用堆栈。您可以随时点击下方的尝试再次查看呼叫者的上下文。\n\n这是对 Eclipse CDT 调试器的简单介绍，它在引擎盖下使用 GDB 给你一个可视化的调试体验。当试图更好地理解运行时错误并纠正导致错误的错误时，您可能会发现调试很有用。\n\n## 编写可读代码\n\n虽然可视化调试器在识别和消除运行时错误或意外的程序行为方面非常有用，但是最好编写一开始就不太可能有问题的代码。做到这一点的一种方法是努力编写更容易阅读和理解的代码。然后，在代码中发现问题变得更像是识别英语句子之间的矛盾，而不是解决神秘的谜题。当你以一种可以理解的方式编写代码时，你的错误往往会在你编写代码时变得很明显，当你回来解决漏掉的问题时，你会更容易发现。\n\n经过一些不愉快的维护经历，你意识到你写的程序的主要目的不是让计算机做你想做的事情，而是告诉读者当程序运行时计算机会做什么。这通常意味着您需要做更多的打字工作，IDEs 可以提供帮助。这也可能意味着您有时编写的代码在执行时间或使用的内存方面不是最佳的。如果这与你所学的相违背，考虑一下你可能用极少量的效率来换取不正确的风险。由于我们拥有巨大的处理能力和内存，您可能会让您的代码变得不必要的神秘，并且在徒劳地追求效率的过程中可能会出错。在接下来的部分中，我们将列出一些经验法则，这些法则可能有助于您编写可读性更强的代码。\n\n### 缩进和格式化\n\n与许多其他编程语言一样，C++ 代码由程序块组成。一个函数有一组语句，这些语句构成了它的块。循环的块语句将在迭代中执行。如果给定条件为真，则执行`if`语句块，否则执行相应的`else`语句块。\n\n花括号，或者单语句块没有花括号，通知计算机，而空格形式的缩进通知人类读者关于块结构。缺少缩进，或者误导性缩进，会使读者很难理解代码的结构。因此，我们应该努力保持代码的良好缩进。考虑以下两个代码块:\n\n```cpp\n// Block 1\nif (result == 2) \nfirstFunction();\nsecondFunction();\n// Block 2\nif (result == 2) \n  firstFunction();\nsecondFunction();\n```\n\n虽然它们在执行上是相同的，但是在第二个中更清楚的是`firstFunction()`只有在`结果`为`2`时才会执行。现在考虑以下代码:\n\n```cpp\nif (result == 2) \n  firstFunction();\n  secondFunction();\n```\n\n这简直是误导。如果读者不小心，他们可能很容易认为只有当`结果`为`2`时，才会执行`secondFunction()`。但是，这段代码在执行方面与前面两个示例完全相同。\n\n如果你觉得修改缩进会让你慢下来，你可以使用编辑器的格式化工具来帮助你。在 Eclipse 中，您可以选择一段代码并使用 **Source** | **修正缩进**来修复该选择的缩进，或者使用 **Source** | **Format** 来修复代码的其他格式问题。\n\n除了缩进之外，其他格式规则，如将大括号放在正确的行，在二进制运算符周围插入空格，以及在每个逗号后插入空格，也是非常重要的格式规则，您应该遵守这些规则来保持代码格式良好且易于阅读。\n\n在 Eclipse 中，您可以在**窗口** | **首选项** | **C/C++** | **代码样式** | **格式化程序**或在**项目** | **属性** | **C/C++ 常规** | **格式化程序**中设置每个工作区的格式化规则。您可以选择一种行业标准样式，如 K & R 或 GNU，也可以修改它们并创建自己的样式。当您使用**源代码** | **格式**来格式化您的代码时，这变得尤为重要。例如，如果您选择使用空格进行缩进，但是 Eclipse 的格式规则设置为制表符，那么您的代码将变成制表符和空格的混合。\n\n### 使用有意义的名称作为标识符\n\n在我们的代码中，我们使用标识符来命名许多项目——变量、函数、类名、类型等等。对于计算机来说，这些标识符只是一个字符序列，用来区分它们。然而，对于读者来说，它们要多得多。标识符应该完整而明确地描述它所代表的项目。同时，它不应该太长。此外，它应该遵守正在使用的风格标准。\n\n考虑以下代码:\n\n```cpp\nstudentsFile File = runFileCheck(\"students.dat\");\nbool flag = File.check();\nif (flag) {\n    int Count_Names = 0;\n    while (File.CheckNextElement() == true) {\n        Count_Names += 1;\n    }\n    std::cout << Count_Names << std::endl;\n}\n```\n\n虽然这是一段完全有效的 C++ 代码，但很难阅读。让我们列出它的问题。首先，我们来看看标识符的样式问题。`学生文件`类名以小写字母开头，应该改为大写。`文件`变量应该以小写字母开头。`Count_Names`变量应该以小写字母开头，不应该有下划线。`CheckNextElement`方法应该以小写字母开头。虽然这些规则看起来很随意，但是命名的一致性带来了关于名称的额外信息——当你看到一个以大写字母开头的单词时，你马上就会明白它一定是一个类名。此外，使用不符合标准的名字只会分散注意力。\n\n现在，让我们超越风格，检查名称本身。第一个有问题的名字是`runFileCheck`函数。方法是一个返回值的动作:它的名字应该清楚地解释它做什么以及它返回什么。“检查”是一个过度使用的词，对大多数情况来说太模糊了。是的，我们检查过了，它就在那里——那我们该怎么办？在这种情况下，似乎我们实际上读取了文件并创建了一个`文件`对象。在这种情况下，`运行文件检查`应该改为`读取文件`。这清楚地解释了正在采取的行动，而返回值正是您所期望的。如果您想更具体地了解返回值，`readAsFile`可能是另一种选择。同样的，`检查`的方法也比较模糊，应该是`存在`代替。`CheckNextElement`方法也比较模糊，应该是`next elements`代替。\n\n另一个被过度使用的模糊词是`标志`，常用于布尔变量。这个名字暗示了一种开/关的情况，但没有给出它的价值意味着什么的线索。在这种情况下，其`真`值表示文件存在，`假`值表示文件不存在。命名布尔变量的技巧是，当变量的值为`真`时，设计一个正确的问题或语句。在本例中，`文件存在`和`文件不存在`是两个不错的选择。\n\n我们的下一个错误命名的变量是`Count_Names`，或`countNames`，其大写正确。对于整数来说，这是一个不好的名字，因为这个名字并没有暗示一个数字——它暗示了一个产生数字的动作。取而代之的是，像`numNames`或`nameCount`这样的标识符可以清楚地传达出里面的数字是什么意思。\n\n### 保持算法清晰简单\n\n当我们阅读代码时，所采取的步骤和流程应该是有意义的。间接完成的事情——函数的副产品，以效率的名义一起完成的多个动作，等等——让读者很难理解你的代码。例如，让我们看看下面的代码:\n\n```cpp\nint *input = getInputArray();\nint length = getInputArrayLength();\nint sum = 0;\nint minVal = 0;\nfor (int i = 0; i < length; ++ i) {\n  sum += input[i];\n  if (i == 0 || minVal > input[i]) {\n    minVal = input[i];\n  }\n  if (input[i] < 0) {\n    input[i] *= -1;\n  }\n}\n```\n\n这里，我们有一个在循环中处理的数组。乍一看，不太清楚这个循环到底在做什么。变量名有助于我们理解正在发生的事情，但是我们必须在头脑中运行算法，以确保这些名字所宣传的东西确实发生在这里。这个循环中有三种不同的操作。首先，我们要找到所有元素的总和。其次，我们正在寻找数组中的最小元素。第三，我们取这些运算后每个元素的绝对值。\n\n现在考虑这个替代版本:\n\n```cpp\nint *input = getInputArray();\nint length = getInputArrayLength();\nint sum = 0;\nfor (int i = 0; i < length; ++ i) {\n  sum += input[i];\n}\nint minVal = 0;\nfor (int i = 0; i < length; ++ i) {\n  if (i == 0 || minVal > input[i]) {\n    minVal = input[i];\n  }\n}\nfor (int i = 0; i < length; ++ i) {\n  if (input[i] < 0) {\n    input[i] *= -1;\n  }\n}\n```\n\n现在一切都清楚多了。第一个循环找到输入的总和，第二个循环找到最小元素，第三个循环找到每个元素的绝对值。虽然它更清晰，更容易理解，但您可能会觉得自己在做三个循环，因此浪费了 CPU 资源。创建更高效代码的动力可能会迫使您合并这些循环。请注意，您在这里获得的效率提升微乎其微；你的程序的时间复杂度仍然是 O(n)。\n\n在创建代码时，可读性和效率是两个经常竞争的限制因素。如果你想开发可读和可维护的代码，你应该优先考虑可读性。然后，您应该努力开发同样高效的代码。否则，可读性低的代码有难以维护的风险，或者更糟的是，有难以识别和修复的错误的风险。当你的程序产生不正确的结果时，或者当增加新功能的成本变得太高时，你的程序的高效率将变得无关紧要。\n\n### 练习 10:使代码可读\n\n下面的代码中有样式和缩进问题。空格使用不一致，缩进不正确。此外，关于单语句`的决定，如果`块是否有花括号是不一致的。下面这段代码在缩进、格式、命名和清晰度方面有问题:\n\n```cpp\n//a is the input array and Len is its length\nvoid arrayPlay(int *a, int Len) { \n    int S = 0;\n    int M = 0;\n    int Lim_value = 100;\n    bool flag = true;\n    for (int i = 0; i < Len; ++ i) {\n    S += a[i];\n        if (i == 0 || M > a[i]) {\n        M = a[i];\n        }\n        if (a[i] >= Lim_value) {            flag = true;\n            }\n            if (a[i] < 0) {\n            a[i] *= 2;\n        }\n    }\n}\n```\n\n让我们修复这些问题，并使其与常见的 C++ 代码风格兼容。执行以下步骤完成本练习:\n\n1.  打开 Eclipse CDT。\n2.  Create a new **ArrayPlay.cpp** file in the **src** folder and paste the preceding code. Make sure you do not have any text selected. Then, go to **Source** | **Format** from the top menu and accept the dialog to format the entire file. This makes our code look like the following:\n\n    ```cpp\n    //a is the input array and Len is its length\n    void arrayPlay(int *a, int Len) { \n        int S = 0;\n        int M = 0;\n        int Lim_value = 100;\n        bool flag = true;\n        for (int i = 0; i < Len; ++ i) {\n            S += a[i];\n            if (i == 0 || M > a[i]) {\n                M = a[i];\n            }\n            if (a[i] >= Lim_value) {\n                flag = true;\n            }\n            if (a[i] < 0) {\n                a[i] *= 2;\n            }\n        }\n    }\n    ```\n\n    现在代码更容易理解了，让我们试着理解它的作用。感谢评论，我们了解到我们有一个输入数组`a`，长度为`Len`。更好的名字是`输入`和`输入`。\n\n3.  让我们进行第一个更改，并将`a`重命名为`输入`。如果您正在使用 Eclipse，您可以选择**重构** | **重命名**来重命名一个事件，所有其他事件也将被重命名。对`透镜`进行同样的操作，并将其重命名为`输入长度`。\n4.  更新后的代码如下所示。请注意，我们不再需要注释，因为参数名称是不言自明的:\n\n    ```cpp\n    void arrayPlay(int *input, int inputLength) {\n        int S = 0;\n        int M = 0;\n        int Lim_value = 100;\n        bool flag = true;\n        for (int i = 0; i < inputLength; ++ i) {\n            S += input[i];\n            if (i == 0 || M > input[i]) {\n                M = input[i];\n            }\n            if (input[i] >= Lim_value) {\n                flag = true;\n            }\n            if (input[i] < 0) {\n                input[i] *= 2;\n            }\n        }\n    }\n    ```\n\n5.  我们在循环之前定义了几个其他变量。让我们试着理解他们。似乎它对`S`所做的一切就是给它添加每个元素。所以，`S`一定是`sum`。另一方面，`M`似乎是最小的元素——我们把它命名为`最小的`。\n6.  `Lim_value` seems to be a threshold, where we simply want to know whether it has been crossed. Let's rename it `topThreshold`. The `flag` variable is set to true if this threshold is crossed. Let's rename it to `isTopThresholdCrossed`. Here is the state of the code after these changes with **Refactor** | **Rename**:\n\n    ```cpp\n    void arrayPlay(int *input, int inputLength) {\n        int sum = 0;\n        int smallest = 0;\n        int topThreshold = 100;\n        bool isTopThresholdCrossed = true;\n        for (int i = 0; i < inputLength; ++ i) {\n            sum += input[i];\n            if (i == 0 || smallest > input[i]) {\n                smallest = input[i];\n            }\n            if (input[i] >= topThreshold) {\n                isTopThresholdCrossed = true;\n            }\n            if (input[i] < 0) {\n                input[i] *= 2;\n            }\n        }\n    }\n    ```\n\n    现在，让我们看看如何让这段代码更简单、更容易理解。前面的代码正在做这些事情:计算输入元素的总和，找到最小的一个，确定是否超过了最高阈值，并将每个元素乘以 2。\n\n7.  由于所有这些都是在同一个循环中完成的，所以算法现在不是很清楚。修复它，并有四个独立的循环:\n\n    ```cpp\n    void arrayPlay(int *input, int inputLength) {\n        // find the sum of the input\n        int sum = 0;\n        for (int i = 0; i < inputLength; ++ i) {\n            sum += input[i];\n        }\n        // find the smallest element\n        int smallest = 0;\n        for (int i = 0; i < inputLength; ++ i) {\n            if (i == 0 || smallest > input[i]) {\n                smallest = input[i];\n            }\n        }\n        // determine whether top threshold is crossed\n        int topThreshold = 100;\n        bool isTopThresholdCrossed = true;\n        for (int i = 0; i < inputLength; ++ i) {\n            if (input[i] >= topThreshold) {\n                isTopThresholdCrossed = true;\n            }\n        }\n        // multiply each element by 2\n        for (int i = 0; i < inputLength; ++ i) {\n            if (input[i] < 0) {\n                input[i] *= 2;\n            }\n        }\n    }\n    ```\n\n现在代码清晰多了。虽然很容易理解每个块在做什么，但我们也添加了注释，使其更加清晰。在这一节中，我们更好地理解了我们的代码是如何转换成可执行文件的。然后，我们讨论了用代码识别和解决可能的错误的方法。我们最后讨论了如何编写不太可能有问题的可读代码。在下一节中，我们将解决一个活动，其中我们将使代码更易读。\n\n### 活动 3:提高代码可读性\n\n您可能有不可读且包含 bug 的代码，要么是因为您匆忙编写的，要么是从其他人那里收到的。您希望更改代码以消除其错误并使其更易读。我们有一段代码需要改进。逐步改进它，并使用调试器解决问题。执行以下步骤来实施本活动:\n\n1.  下面你会找到 **SpeedCalculator.cpp** 和 **SpeedCalculator.h** 的来源。它们包含`速度计算器`类。将这两个文件添加到您的项目中。\n2.  在你的`main()`函数中创建这个类的一个实例，并调用它的`run()`方法。\n3.  修复代码中的样式和命名问题。\n4.  简化代码，使其更容易理解。\n5.  运行代码并在运行时观察问题。\n6.  使用调试器来解决问题。\n\n这是您将添加到项目中的**速度计算器. cpp** 和**速度计算器. h** 的代码。作为本活动的一部分，您将修改它们:\n\n```cpp\n// SpeedCalculator.h\n#ifndef SRC_SPEEDCALCULATOR_H_\n#define SRC_SPEEDCALCULATOR_H_\nclass SpeedCalculator {\nprivate:\n    int numEntries;\n    double *positions;\n    double *timesInSeconds;\n    double *speeds;\npublic:\n    void initializeData(int numEntries);\n    void calculateAndPrintSpeedData();\n};\n#endif /* SRC_SPEEDCALCULATOR_H_ */\n```\n\n```cpp\n//SpeedCalculator.cpp\n#include \"SpeedCalculator.h\"\n#include <cstdlib>\n#include <ctime>\n#include <iostream>\n#include <cassert>\nvoid SpeedCalculator::initializeData(int numEntries) {\n    this->numEntries = numEntries;\n    positions = new double[numEntries];\n    timesInSeconds = new double[numEntries];\n    srand(time(NULL));\n    timesInSeconds[0] = 0.0;\n    positions[0] = 0.0;\n    for (int i = 0; i < numEntries; ++ i) {\n    positions[i] = positions[i-1] + (rand()%500);\n    timesInSeconds[i] = timesInSeconds[i-1] + ((rand()%10) + 1);\n    }\n}\nvoid SpeedCalculator::calculateAndPrintSpeedData() {\n    double maxSpeed = 0;\n    double minSpeed = 0;\n    double speedLimit = 100;\n    double limitCrossDuration = 0;\n    for (int i = 0; i < numEntries; ++ i) {\n        double dt = timesInSeconds[i+1] - timesInSeconds[i];\n        assert (dt > 0);\n        double speed = (positions[i+1] - positions[i]) / dt;\n            if (maxSpeed < speed) {\n                maxSpeed = speed;\n            }\n            if (minSpeed > speed) {\n                minSpeed = speed;\n            }\n        if (speed > speedLimit) {\n            limitCrossDuration += dt;\n        }\n        speeds[i] = speed;\n    }\n    std::cout << \"Max speed: \" << maxSpeed << std::endl;\n        std::cout << \"Min speed: \" << minSpeed << std::endl;\n        std::cout << \"Total duration: \" << \ntimesInSeconds[numEntries - 1] - timesInSeconds[0] << \" seconds\" << std::endl;\n    std::cout << \"Crossed the speed limit for \" << limitCrossDuration << \" seconds\"<< std::endl;\n    delete[] speeds;\n}\n```\n\n#### 注意\n\n这项活动的解决方案可以在第 626 页找到。\n\n## 总结\n\n在本章中，我们学习了如何创建可移植和可维护的 C++ 项目。我们首先学习了如何创建 CMake 项目，以及如何将它们导入到 Eclipse CDT 中，让我们可以选择使用命令行还是 IDE。本章的其余部分集中在消除我们项目中的各种问题。首先，我们学习了如何将单元测试添加到项目中，以及如何使用它们来确保我们的代码按预期工作。我们继续讨论了代码的编译和链接步骤，并观察了目标文件的内容，以便更好地理解可执行文件。然后，我们学习了如何在 IDE 中可视化地调试代码，以消除运行时错误。我们用一些帮助创建可读、可理解和可维护的代码的经验法则结束了这次讨论。这些方法将在你的 C++ 之旅中派上用场。在下一章中，我们将了解更多关于 C++ 的类型系统和模板。"
  },
  {
    "path": "docs/adv-cpp/02.md",
    "content": "# 二、不允许鸭子——类型和推导（一）\n\n## 学习目标\n\n本章结束时，您将能够:\n\n*   实现您自己的行为类似于内置类型的类\n*   实现控制编译器创建哪些函数的类(零规则/五规则)\n*   像往常一样，使用自动变量开发函数\n*   通过使用强类型编写更安全的代码来实现类和函数\n\n本章将为您提供一个良好的 C++ 类型系统基础，并允许您编写自己的类型在该系统中工作。\n\n## 简介\n\nC++ 是一种强类型、静态类型的语言。编译器使用与所使用的变量及其上下文相关的类型信息来检测和防止某些类别的编程错误。这意味着每个对象都有一个类型，并且该类型永远不会改变。相比之下，动态类型语言(如 Python 和 PHP)将这种类型检查推迟到运行时(也称为后期绑定)，变量的类型可能会在应用执行过程中发生变化。这些语言使用鸭子测试而不是变量类型——也就是说，“如果它像鸭子一样走路和说话，那么它一定是一只鸭子。”静态类型的语言，如 C++ 依赖于类型来确定变量是否可以用于给定的目的，而动态类型的语言依赖于某些方法和属性的存在来确定其适用性。\n\nC++ 最初被描述为“带类的 C”。这是什么意思？基本上，C 语言提供了一组内置的基本类型——int、float、char 等等——以及这些项的指针和数组。您可以使用结构将它们聚合到相关项的数据结构中。C++ 将此扩展到类，这样您就可以用操作符完全定义自己的类型，从而使它们成为语言中的一流公民。从最初的卑微开始，C++ 已经发展成为不仅仅是“带类的 C”，因为它现在可以表达面向对象的范式(封装、多态、抽象和继承)、函数范式和泛型编程(模板)。\n\n在这本书里，我们将关注 C++ 支持面向对象范式意味着什么。随着您作为开发人员的经验的增长，以及您接触到 Clojure、Haskell、Lisp 和其他函数式语言，它们将帮助您编写健壮的 C++ 代码。像 Python、PHP 和 Ruby 这样的动态类型语言已经影响了我们编写 C++ 代码的方式。随着 C++ 17 的到来，引入了`std::variant`类——一个保存我们选择的任何类型(在编译时)的类，其行为非常像动态语言中的变量。\n\n在前一章中，我们学习了如何使用 CMake 创建可移植和可维护的 C++ 项目。我们学习了如何在项目中加入单元测试来帮助编写正确的代码，以及如何在问题出现时进行调试。我们学习了工具链如何获取我们的代码，并通过程序管道运行它来生成可执行文件。最后，我们总结了一些帮助我们创建可读、可理解和可维护代码的经验法则。\n\n在这一章中，我们将对 C++ 类型系统进行一次旋风式的旅行，一边走一边声明和使用我们自己的类型。\n\n## C++ 类型\n\n作为一种强类型和静态类型的语言，C++ 提供了几种基本类型，并且能够定义自己的类型，并根据需要提供或多或少的功能来解决手头的问题。本节将首先介绍基本类型，初始化它们，声明一个变量，并将一个类型与之相关联。然后，我们将探讨如何声明和定义一个新类型。\n\n### C++ 基本类型\n\nC++ 包括几个*基本类型*，或者*内置类型*。C++ 标准定义了每种类型的最小内存大小及其相对大小。编译器识别这些基本类型，并有内置的规则来定义哪些操作可以在这些类型上执行，哪些不能。类型之间的隐式转换也有规则；例如，从 int 类型转换为 float 类型。\n\n#### 注意\n\n参见[https://en.cppreference.com/w/cpp/language/types](https://en.cppreference.com/w/cpp/language/types)的**基本类型**部分，了解所有内置类型的简要说明。\n\n### C++ 文字\n\nC++ 文字用于告诉编译器，当您声明变量或赋值给变量时，您希望与变量相关联的值。上一节中的每个内置类型都有一种与之关联的文字形式。\n\n#### 注意\n\n参见[https://en.cppreference.com/w/cpp/language/expressions](https://en.cppreference.com/w/cpp/language/expressions)的**文字**部分，了解每种类型文字的简要说明。\n\n## 指定类型–变量\n\n由于 C++ 是一种静态类型的语言，所以在声明变量时需要指定变量的类型。当您声明一个函数时，有必要指定返回类型和传递给它的参数类型。在声明变量时，有两种方法可以指定变量的类型:\n\n*   **明确地**:你作为程序员，正在精确地规定类型是什么。\n*   **隐式**(使用 auto):您告诉编译器查看用于初始化变量的值并确定其类型。这就是众所周知的(自动)**式推演**。\n\n标量变量的一般声明形式如下:\n\n```cpp\ntype-specifier var;                       // 1\\. Default-initialized variable\ntype-specifier var = init-value;          // 2\\. Assignment initialized variable\ntype-specifier var{init-value};           // 3\\. Brace-initialize variable\n```\n\n`类型说明符`表示您希望与`变量`关联的类型(基本或用户定义的)。所有这三种形式都会导致编译器分配一些存储来保存值，并且所有将来对`变量`的引用都将引用该位置。`初始化值`用于初始化存储位置。默认初始化对内置类型不起任何作用，并将根据函数重载解析调用用户定义类型的构造函数来初始化存储。\n\n编译器必须知道要分配多少内存，并提供一个运算符来确定一个类型或变量有多大–`size of`。\n\n根据我们的声明，编译器将在计算机内存中留出空间来存储变量引用的数据项。考虑以下声明:\n\n```cpp\nint value = 42;     // declare value to be an integer and initialize to 42\nshort a_value{64};  // declare a_value to be a short integer and initialize\n                    //    to 64\nint bad_idea;       // declare bad_idea to be an integer and DO NOT \n                    // initialize it. Use of this variable before setting\n                    // it is UNDEFINED BEHAVIOUR.\nfloat pi = 3.1415F; // declare pi to be a single precision floating point\n                    // number and initialize it to pi.\ndouble e{2.71828};  // declare e to be a double precision floating point\n                    // number and initialize it to natural number e.\nauto title = \"Sir Robin of Loxley\"; // Let the compiler determine the type\n```\n\n如果这些是在函数的范围内声明的，那么编译器会从所谓的堆栈中为它们分配内存。这方面的内存布局可能如下所示:\n\n![Figure 2A.1: Memory layout of variables](img/C14583_02A_01.jpg)\n\n###### 图 2A.1:变量的内存布局\n\n编译器将按照我们声明变量的顺序分配内存。之所以会出现未使用的内存，是因为编译器会分配内存，这样基本类型通常会被自动访问，并与适当的内存边界对齐以提高效率。注意`标题`是`const char *`类型，这是一个**指针**，我们接下来将与`const`一起讨论。**“洛克斯利的罗宾爵士”**字符串将存储在加载程序时初始化的内存的不同部分。我们稍后将讨论程序内存。\n\n标量声明语法的轻微修改为我们提供了声明值数组的语法:\n\n```cpp\ntype-specifier ary[count];                          // 1\\. Default-initialized \ntype-specifier ary[count] = {comma-separated list}; // 2\\. Assignment initialized \ntype-specifier ary[count]{comma-separated list};    // 3\\. Brace-initialized\n```\n\n对于多维数组，可以这样做:\n\n```cpp\ntype-specifier ary2d[countX][countY]; \ntype-specifier ary3d[countX][countY][countZ];\n// etc...\n```\n\n请注意，`count`、`countX`和前面声明中的其他项目必须在编译时计算为常数，否则将导致错误。此外，逗号分隔的初始值设定项列表中的项数必须小于或等于`计数`，否则将再次出现编译错误。在下一节中，我们将应用到目前为止在练习中学到的概念。\n\n#### 注意\n\n在解决本章中的任何实际问题之前，请下载本书的 GitHub 资源库([https://github.com/TrainingByPackt/Advanced-CPlusPlus](https://github.com/TrainingByPackt/Advanced-CPlusPlus))并导入 Eclipse 中的 2A 课文件夹，以便您可以查看每个练习和活动的代码。\n\n### 练习 1:声明变量和探索大小\n\n本练习将设置本章的所有练习，然后让您熟悉声明和初始化内置类型的变量。还将向您介绍**自动申报**、**阵列**和的**尺寸。让我们开始吧:**\n\n1.  打开 Eclipse(用在*第 1 章*、*便携式 C++ 软件剖析*中)，如果出现启动器窗口，点击启动。\n2.  转到**文件**，在**新建****下选择**项目……**，转到选择 C++ 项目(不是 C/C++ 项目)。**\n***   点击**下一步>** ，清除**使用默认位置**复选框，输入**第二课**作为**项目名称**。*   选择**项目类型**的**空项目**。然后，点击**浏览……**并导航到包含第 2 课示例的文件夹。*   点击**打开**选择文件夹并关闭对话框。*   点击**下一步>** 、**下一步>** ，然后**完成**。*   为了帮助您完成练习，我们将配置工作区，以便在构建之前自动保存文件。进入**窗口**，选择**偏好设置**。在**通用**下，打开**工作区**，选择**构建**。*   在构建之前，勾选**自动保存框，然后点击**应用并关闭**。***   就像*章**1**解析可移植 C++ 软件*一样，这是一个基于 CMake 的项目，所以我们需要改变当前的构建器。点击**项目浏览器**中的**第 2 课**，然后点击**项目**菜单下的**属性**。从左窗格中选择 C/C++ 构建下的工具链编辑器，并将当前构建器设置为 Cmake 构建(可移植)。*   Click **Apply and Close**. Then, choose the **Project** | **Build All** menu item to build all the exercises. By default, the console at the bottom of the screen will display the **CMake Console [Lesson2A]**:\n\n    ![Figure 2A.2: CMake console output](img/C14583_02A_02.jpg)\n\n    ###### 图 2A.2: CMake 控制台输出\n\n    *   In the top-right corner of the console, click on the **Display Selected Console** button and then select **CDT Global Build Console** from the list:\n\n    ![Figure 2A.3: Selecting a different console](img/C14583_02A_03.jpg)\n\n    ###### 图 2A.3:选择不同的控制台\n\n    这将显示构建的结果—它应该显示 0 个错误和 3 个警告:\n\n    ![Figure 2A.4: Build process console output](img/C14583_02A_04.jpg)\n\n    ###### 图 2A.4:构建过程控制台输出\n\n    *   As the build was successful, we want to run Exercise1\\. At the top of the window, click on the drop-down list where it says **No Launch Configurations**:\n\n    ![Figure 2A.5: Launch Configuration menu](img/C14583_02A_05.jpg)\n\n    ###### 图 2A.5:启动配置菜单\n\n    *   点击**新启动配置…** 。保持默认值不变，点击**下一步>T3。***   Change **Name** to **Exercise1** and then click **Search Project**:\n\n    ![Figure 2A.6: Exercise1 Launch Configuration ](img/C14583_02A_06.jpg)\n\n    ###### 图 2A.6:练习 1 启动配置\n\n    *   从二进制文件窗口显示的程序列表中，单击**练习 1** 并单击**确定**。*   Click **Finish**. This will result in exercise1 being displayed in the Launch Configuration drop-down box:\n\n    ![Figure 2A.7: Change to Launch Configuration](img/C14583_02A_07.jpg)\n\n    ###### 图 2A.7:更改启动配置\n\n    *   To run **Exercise1**, click on the **Run** button. Exercise1 will execute and display its output in the console:\n\n    ![Figure 2A.8: Output from exercise1](img/C14583_02A_08.jpg)\n\n    ###### 图 2A.8:练习 1 的输出\n\n    这个程序没有任何价值——它只是在你的系统上输出各种类型的大小。但这说明程序是有效的，可以编译。请注意，系统的数字可能不同(尤其是 sizeof(title)值)。\n\n    *   In the **Project Explorer**, expand **Lesson2A**, then **Exercise01**, and double-click on **Exercise1.cpp** to open the file for this exercise in the editor:\n\n    ```cpp\n    int main(int argc, char**argv)\n    {\n        std::cout << \"\\n\\n------ Exercise 1 ------\\n\";\n        int value = 42;     // declare value to be an integer & initialize to 42\n        short a_value{64};  // declare a_value to be a short integer & \n                            // initialize to 64\n        int bad_idea;       // declare bad_idea to be an integer and DO NOT \n                            // initialize it. Use of this variable before \n                            // setting it is UNDEFINED BEHAVIOUR.\n        float pi = 3.1415F; // declare pi to be a single precision floating \n                            // point number and initialize it to pi.\n\n        double e{2.71828};  // declare e to be a double precision floating point\n                            // number and initialize it to natural number e.\n        auto title = \"Sir Robin of Loxley\"; \n                            // Let the compiler determine the type\n        int ary[15]{};      // array of 15 integers - zero initialized\n        // double pi = 3.14159;  // step 24 - remove comment at front\n        // auto speed;           // step 25 - remove comment at front\n        // value = \"Hello world\";// step 26 - remove comment at front\n        // title = 123456789;    // step 27 - remove comment at front\n        // short sh_int{32768};  // step 28 - remove comment at front\n        std::cout << \"sizeof(int) = \" << sizeof(int) << \"\\n\";\n        std::cout << \"sizeof(short) = \" << sizeof(short) << \"\\n\";\n        std::cout << \"sizeof(float) = \" << sizeof(float) << \"\\n\";\n        std::cout << \"sizeof(double) = \" << sizeof(double) << \"\\n\";\n        std::cout << \"sizeof(title) = \" << sizeof(title) << \"\\n\";\n        std::cout << \"sizeof(ary) = \" << sizeof(ary)\n                  << \" = \" << sizeof(ary)/sizeof(ary[0]) \n                  << \" * \" << sizeof(ary[0]) << \"\\n\";\n        std::cout << \"Complete.\\n\";\n        return 0;\n    }\n    ```\n\n    关于前面的程序需要注意的一点是，main 函数的第一条语句实际上是一条可执行语句，而不是声明。C++ 允许你在任何地方声明一个变量。它的前身 C 最初要求所有变量必须在任何可执行语句之前声明。\n\n    #### 最佳实践\n\n    声明一个尽可能接近它将被使用的地方的变量并初始化它。\n\n    *   在编辑器中，通过删除行首的分隔符(`//`)取消标记为`步骤 24`的行的注释:\n\n    ```cpp\n    double pi = 3.14159;  // step 24 - remove comment at front    \n    // auto speed;           // step 25 - remove comment at front\n    // value = \"Hello world\";// step 26 - remove comment at front\n    // title = 123456789;    // step 27 - remove comment at front\n    // short sh_int{32768};  // step 28 - remove comment at front\n    ```\n\n    *   Click on the **Run** button again. This will cause the program to be built again. This time, the build will fail with an error:\n\n    ![Figure 2A.9: Errors in Workspace dialog](img/C14583_02A_09.jpg)\n\n    ###### 图 2A.9:工作空间对话框中的错误\n\n    *   Click on **Cancel** to close the dialog. If **CDT Build Console [Lesson2A]** is not displayed, then select it as the active console:\n\n    ![Figure 2A.10: Duplicate declaration error](img/C14583_02A_10.jpg)\n\n    ###### 图 2A.10:重复声明错误\n\n    这一次，构建失败了，因为我们试图重新定义变量的类型，即 pi。编译器会给出有用的信息，告诉我们需要在哪里进行修复。\n\n    *   将注释分隔符恢复到行首。在编辑器中，通过删除行首的分隔符(//)取消标记为`步骤 25`的行的注释:\n\n    ```cpp\n    // double pi = 3.14159;  // step 24 - remove comment at front    \n    auto speed;           // step 25 - remove comment at front\n    // value = \"Hello world\";// step 26 - remove comment at front\n    // title = 123456789;    // step 27 - remove comment at front\n    // short sh_int{32768};  // step 28 - remove comment at front\n    ```\n\n    *   Click on the **Run** button again. When the Errors in Workspace dialog appears, click **Cancel**:\n\n    ![Figure 2A.11: Auto declaration error – no initialization](img/C14583_02A_11.jpg)\n\n    ###### 图 2A.11:自动声明错误-没有初始化\n\n    同样，构建失败了，但是这一次，我们没有给编译器足够的信息来推断速度的类型——自动类型变量必须被初始化。\n\n    *   将注释分隔符恢复到行首。在编辑器中，通过删除行首的注释起始分隔符(//),取消标记为`步骤 26`的行的注释:\n\n    ```cpp\n    // double pi = 3.14159;  // step 24 - remove comment at front    \n    // auto speed;           // step 25 - remove comment at front\n    value = \"Hello world\";// step 26 - remove comment at front\n    // title = 123456789;    // step 27 - remove comment at front\n    // short sh_int{32768};  // step 28 - remove comment at front\n    ```\n\n    *   Click on the **Run** button again. When the Errors in Workspace dialog appears, click **Cancel**:\n\n    ![Figure 2A.12: Assignment of an incorrect value type to a variable](img/C14583_02A_12.jpg)\n\n    ###### 图 2A.12:将不正确的值类型分配给变量\n\n    这一次，构建失败了，因为我们试图将错误的数据类型，即“Hello world”，它是一个 const char*，分配给 int 类型的变量，即`值`。\n\n    *   将注释分隔符恢复到行首。在编辑器中，取消标记为`的行的注释步骤 27`，方法是删除行首的分隔符(//):\n\n    ```cpp\n    // double pi = 3.14159;  // step 24 - remove comment at front    \n    // auto speed;           // step 25 - remove comment at front\n    // value = \"Hello world\";// step 26 - remove comment at front\n    title = 123456789;    // step 27 - remove comment at front\n    // short sh_int{32768};  // step 28 - remove comment at front\n    ```\n\n    *   Click on the **Run** button again. When the Errors in Workspace dialog appears, click **Cancel**:\n\n    ![Figure 2A.13: Assignment of an incorrect value type to an auto variable](img/C14583_02A_13.jpg)\n\n    ###### 图 2A.13:将不正确的值类型分配给自动变量\n\n    同样，构建失败是因为我们试图将错误的数据类型，即类型为`int`的 123456789 分配给 title，这是一个`const char*`。这里需要注意的一件非常有用的事情是`标题`是用`自动`类型声明的。编译器生成的错误消息告诉我们标题被推断为`const char*`类型。\n\n    *   将注释分隔符恢复到行首。在编辑器中，通过删除行首的分隔符(//)取消标记为`步骤 28`的行的注释:\n\n    ```cpp\n    // double pi = 3.14159;  // step 24 - remove comment at front    \n    // auto speed;           // step 25 - remove comment at front\n    // value = \"Hello world\";// step 26 - remove comment at front\n    // title = 123456789;    // step 27 - remove comment at front\n    short sh_int{32768};  // step 28 - remove comment at front\n    ```\n\n    *   Click on the **Run** button again. When the Errors in Workspace dialog appears, click **Cancel**:\n\n    ![Figure 2A.14: Assignment of a value that's too large to fit in the variable](img/C14583_02A_14.jpg)\n\n    ###### 图 2A.14:赋值太大而无法放入变量\n\n    同样，构建失败，但这一次是因为我们试图用( **32768** )初始化`sh_int`的值不适合分配给`short`类型的内存。短的占用两个字节的内存，被认为是 16 位的有符号量。这意味着可以短时存储的数值范围是`-2^(16-1)`到`2^(16-1)-1`，或者 **-32768** 到 **32767** 。\n\n    *   将数值从 **32768** 更改为 **32767** ，点击**运行**按钮。这一次，程序编译并运行，因为该值可以用一个`简称`来表示。*   将数值从 **32767** 更改为 **-32768** ，点击**运行**按钮。同样，程序编译并运行，因为该值可以用一个`简称`来表示。*   将注释分隔符恢复到行首。在编辑器中，进行您能想到的任何更改，使用任何基本类型及其关联的文字来探索变量声明，然后根据需要经常单击**运行**按钮。检查生成控制台中的输出是否有任何错误消息，因为这可能有助于您找到错误。**\n\n **在本练习中，我们学习了如何设置 Eclipse 开发、实现变量声明以及解决声明问题。\n\n## 指定类型–功能\n\n既然我们可以将一个变量声明为某种类型，我们就需要对这些变量做些什么。在 C++ 中，我们通过调用函数来做事情。函数是传递结果的一系列语句。这个结果可以是一个数学计算(例如指数)，然后发送到一个文件或写入一个终端。\n\n函数允许我们将解决方案分解成更容易管理和理解的语句序列。当我们编写这些打包的语句时，我们可以在有意义的地方重用它们。如果我们需要它根据上下文以不同的方式运行，那么我们会传递一个参数。如果它返回一个结果，那么函数需要一个返回类型。\n\n由于 C++ 是一种强类型语言，我们需要指定与我们实现的函数相关的类型——函数返回的值的类型(包括不返回)和传递给它的参数的类型(如果有的话)。\n\n以下是一个典型的 hello world 程序:\n\n```cpp\n#include <iostream>\nvoid hello_world()\n{\n  std::cout << \"Hello world\\n\"; \n}\nint main(int argc, char** argv)\n{\n  std::cout << \"Starting program\\n\";\n  hello_world();\n  std::cout << \"Exiting program\\n\";\n  return 0;\n}\n```\n\n前面的例子中已经声明了两个函数–`hello _ world()`和`main()`。`main()`函数是所有 C++ 程序的入口点，它返回一个传递给主机系统的`int`值。它被称为出口代码。\n\n从返回类型的声明到左大括号({)的所有内容都被称为**函数原型**。它定义了三件事，即返回类型、函数名以及参数的数量和类型。\n\n对于第一个函数，返回类型为`void`–即不返回值；它的名字是`hello_world`，没有任何争议:\n\n![Figure 2A.15: Declaration of a function that takes no arguments and returns nothing](img/C14583_02A_15.jpg)\n\n###### 图 2A.15:声明一个不接受参数也不返回任何内容的函数\n\n第二个函数返回一个`int`值，名称为`main`，并接受两个参数。这些参数分别是`argc`和`argv`，并且使`int`和*指针分别指向* `char`类型的指针:\n\n![Figure 2A.16: Declaration of a function that takes two arguments and returns an int](img/C14583_02A_16.jpg)\n\n###### 图 2A.16:接受两个参数并返回一个整数的函数的声明\n\n功能原型之后的一切被称为**功能体**。函数体包含变量声明和要执行的语句。\n\n函数必须在使用前声明，也就是说，编译器需要知道它的参数和返回类型。如果函数是在文件中定义的，并且在调用该函数后将在该文件中使用该函数，则可以通过在使用该函数之前提供该函数的前向声明来解决这个问题。\n\n前向声明是通过在调用函数原型之前将以分号结束的函数原型放入文件中来实现的。对于`hello_world()`，这将按如下方式完成:\n\n```cpp\nvoid hello_world();\n```\n\n对于主要功能，这将按如下方式完成:\n\n```cpp\nint main(int, char**);\n```\n\n函数原型不需要参数的名称，只需要类型。但是，为了帮助该功能的用户，保留它们是一个好主意。\n\n在 C++ 中，函数的定义可以在一个文件中，需要从不同的文件中调用。那么，第二个文件如何知道它希望调用的函数的原型呢？这是通过将正向声明放入一个单独的文件(称为头文件)并将其包含在第二个文件中来实现的。\n\n### 练习 2:声明函数\n\n在本练习中，我们将测试编译器在遇到函数调用并实现前向声明以解析未知函数时需要知道什么。我们开始吧。\n\n1.  在 Eclipse 中打开**第 2 课**项目，然后在**项目浏览器**中，展开**第 2 课**，然后展开**练习 02** ，双击**练习 2.cpp** 将本练习的文件打开到编辑器中。\n2.  点击**启动配置**下拉菜单，选择**新启动配置……**。\n3.  将**练习 2** 配置为以**练习 2** 的名称运行。完成后，它将是当前选定的启动配置。\n4.  Click on the **Run** button. Exercise 2 will run and produce the following output:\n\n    ![Figure 2A.17: Output from the exercise2 program](img/C14583_02A_17.jpg)\n\n    ###### 图 2A.17:练习 2 程序的输出\n\n5.  进入编辑器，通过移动`gcd`功能更改代码，使其位于`主`之后。应该是这样的:\n\n    ```cpp\n    int main(int argc, char**argv)\n    {\n        std::cout << \"\\n\\n------ Exercise 2 ------\\n\";\n        std::cout << \"The greatest common divisor of 44 and 121 is \" << gcd(44, 121) << \"\\n\";\n        std::cout << \"Complete.\\n\";\n        return 0;\n    }\n    int gcd(int x, int y)\n    {\n        while(y!=0)\n        {\n            auto c{x%y};\n            x = y;\n            y = c;\n        }\n        return x;\n    }\n    ```\n\n6.  Click on the **Run** button again. When the Errors in Workspace dialog appears, click **Cancel**. In the **CDT Build Console [Lesson2A]**, we will see the reason for the failure:\n\n    ![Figure 2A.18: Build failure due to undefined function](img/C14583_02A_18.jpg)\n\n    ###### 图 2A.18:由于未定义的功能导致的构建失败\n\n    这一次，编译器不知道如何处理对`gcd()`函数的调用。它在需要调用函数的时候并不知道这个函数，即使它是在同一个文件中定义的，但是在调用之后。\n\n7.  在编辑器中，将 forward 声明放在主函数定义之前。还要加一个分号(；)结尾:\n\n    ```cpp\n    int gcd(int x, int y);\n    ```\n\n8.  再次点击**运行**按钮。这一次，程序编译并恢复原始输出。\n\n在本练习中，我们学习了如何转发声明函数，以及如何解决在使用函数之前未声明函数时出现的编译器错误。\n\n在早期版本的 C 编译器中，这是可以接受的。程序会假设该函数存在，并返回一个 int。函数的参数可以从调用中推断出来。然而，在现代 C++ 的情况下，这是不正确的，因为在使用它之前，您必须声明一个函数、类、变量等等。在下一节中，我们将学习指针类型。\n\n### 指针类型\n\n因为它起源于 C 语言，也就是说，编写最佳效率的系统并直接访问硬件，C++ 允许您将变量声明为指针类型。它的格式如下:\n\n```cpp\ntype-specifier* pvar = &var;\n```\n\n这和以前一样，除了两件事:\n\n*   使用特殊声明符星号(`*`)来指示名为 pvar 的变量指向内存中的位置或地址。\n*   它使用特殊运算符&符号( **&** )进行初始化，在这种情况下，它告诉编译器返回 **var** 变量的地址。\n\n由于 C 是一种高级语言，但具有低级访问，指针允许用户直接访问内存，这在我们希望向硬件提供输入/输出并因此控制它时很有帮助。指针的另一个用途是允许向函数提供对公共数据项的访问，并消除调用函数时复制大量数据的需要，因为它默认为按值传递。要访问指针所指向的值，特殊运算符星号(`*`)用于**取消引用**位置:\n\n```cpp\nint five = 5;                // declare five and initialize it\nint *pvalue = &five;         // declare pvalue as pointer to int and have it\n                            // point to the location of five\n*pvalue = 6;                // Assign 6 into the location five.\n```\n\n下图显示了编译器如何分配内存。`值`需要内存存储指针，而`五`需要内存存储整数值 5:\n\n![Figure 2A.19: Memory layout for pointer variables](img/C14583_02A_19.jpg)\n\n###### 图 2A.19:指针变量的内存布局\n\n当通过指针访问用户定义的类型时，还有第二个特殊运算符(-->)也用于对成员变量和函数进行解引用。在现代 C++ 中，这些指针被称为**原始指针**，它们的使用方式发生了显著变化。在 C 和 C++ 中使用指针对程序员来说一直是一个挑战，它们的不正确使用是许多问题的根源，最常见的是资源泄漏。资源泄漏是指程序获取了一个资源(内存、文件句柄或其他系统资源)供其使用，但在使用完毕后未能释放的情况。这些资源泄漏会导致性能问题、程序故障，甚至系统崩溃。在现代 C++ 中使用原始指针来管理资源的所有权现在已经被否决了，因为智能指针出现在 C++ 11 中。智能指针(在 STL 中实现为类)现在做了在您的主机系统中成为一个好公民所需的家务。更多相关内容将在*第三章*、*能与应之间的距离-对象、指针和继承*中介绍。\n\n在前面的代码中，当`值`被声明时，编译器分配内存只存储它将要引用的内存的地址。像其他变量一样，您应该始终确保在使用指针之前对其进行初始化，因为取消对未初始化指针的引用会导致未定义的行为。究竟分配了多少内存来存储指针取决于编译器设计的系统和处理器支持的位数。但是所有指针的大小都是一样的，不管它们指向什么类型。\n\n指针也可以传递给函数。这允许函数访问被指向的数据，并可能对其进行修改。考虑 swap 的以下实现:\n\n```cpp\nvoid swap(int* data1, int* data2)\n{\n    int temp{*data1};         // Initialize temp from value pointed to by data1\n    *data1 = *data2;          // Copy data pointed to by data2 into location \n                              // pointed to by data1\n    *data2 = temp;            // Store the temporarily cached value from temp\n                              // into the location pointed to by data2\n}\n```\n\n这展示了如何将指针声明为函数的参数，如何使用解引用操作符`*`从指针中获取值，以及如何通过解引用操作符设置值。\n\n以下示例使用新运算符从主机系统分配内存，并使用删除运算符将其释放回主机系统:\n\n```cpp\nchar* name = new char[20];    // Allocate 20 chars worth of memory and assign it\n                              // to name.\n  Do something with name\ndelete [] name;\n```\n\n在前面的代码中，第一行使用新运算符的数组分配形式创建了一个 20 个字符的数组。它调用主机系统来分配 20 * sizeof(char)字节的内存供我们使用。具体分配多少内存由主机系统决定，但保证至少为 20 * sizeof(char)字节。如果它无法分配所需的内存，则会发生以下两种情况之一:\n\n*   它将引发异常\n*   它将返回`nullptr`。这是 C++ 11 中引入的一个特殊文字。早期，C++ 使用 0 或空值来表示无效指针。C++ 11 也使它成为强类型值。\n\n在大多数系统中，第一个结果将是结果，您需要处理异常。第二种结果可能来自两种情况——调用 new 的 northrow 变体，即`new(STD::northrow)int[250]`，或者在异常处理开销没有足够确定性的嵌入式系统上。\n\n最后，请注意，对 delete 的调用使用了 delete 运算符的数组形式，即带有方括号[]。确保新的和删除操作符使用相同的形式非常重要。当在用户定义的类型上使用 new 时(这将在下一节中讨论)，它不仅仅是分配内存:\n\n```cpp\nMyClass* object = new MyClass;\n```\n\n在前面的代码中，对 new 的调用分配了足够的内存来存储 MyClass，如果成功，它将继续调用构造函数来初始化数据:\n\n```cpp\nMyClass* objects = new MyClass[12];\n```\n\n在前面的代码中，对 new 的调用分配了足够的内存来存储 MyClass 的 12 个副本，如果成功，它将继续调用构造函数 12 次来初始化每个对象的数据。\n\n请注意，在前面的代码片段中声明的`对象`和`对象`具有相同类型的**。严格来说，`对象`应该是指向 MyClass 数组的指针，但实际上是指向 MyClass 实例的指针。`对象`指向 MyClass 数组中的第一个实例。**\n\n **考虑以下代码摘录:\n\n```cpp\nvoid printMyClasses(MyClass* objects, size_t number)\n{\n  for( auto i{0U} ; i<number ; i++ ) { \n    std::cout << objects[i] << \"\\n\";\n  }\n}\nvoid process()\n{\n    MyClass objects[12];\n\n    // Do something with objects\n    printMyClasses(objects, sizeof(objects)/sizeof(MyClass));\n}\n```\n\n在 process()函数中，`对象`属于“12 个 MyClass 项的数组”类型，但是当它被传递到`printMyClasses()`时，它被(编译器)转换为“指向 MyClass 的指针”类型。这是设计造成的(继承自 C)，被称为**数组衰减**，可能是新程序员出错的原因。我们可以尝试声明`printMyClasses()`如下:\n\n```cpp\nvoid printMyClasses(MyClass objects[12], size_t number)\n```\n\n当编译器将参数对象更改为 MyClass*时，这仍然会受到数组衰减的影响；在这种情况下，它不保留尺寸信息。数组衰减的原因是我们需要将数字传递给`printMyClasses()`函数:这样我们就知道数组中有多少项了。C++ 提供了两种处理数组衰减的机制:\n\n*   Use of iterators to pass a range into the method. STL containers (see the *C++ Pre-Packaged Templates* section in *Chapter 2B*, *No Ducks Allowed – Templates and Deduction*) offer the `begin()` and `end()` methods so that we can obtain iterators that allow algorithms to traverse an array or part thereof.\n\n    #### 注意\n\n    对于 C++ 20，国际标准化组织标准委员会正在考虑包含一个被称为范围的概念，它允许在一个对象中捕获开始和结束迭代器。\n\n*   模板的使用(参见 2B*章节中的*非类型模板参数*部分，不允许鸭子-模板和演绎*)。\n\n### 练习 3:声明和使用指针\n\n在本练习中，我们将实现以指针和数组为参数的函数，并在考虑数组衰减的同时比较它们的行为。让我们开始吧:\n\n1.  在 Eclipse 中打开**第 2 课**项目，然后在项目浏览器中展开**第 2 课**，然后展开**练习 03** ，双击**练习 3.cpp** 将本练习的文件打开到编辑器中。\n2.  点击**启动配置**下拉菜单，选择**新启动配置……**。将**练习 3** 配置为以**练习 3** 的名称运行。完成后，它将是当前选定的启动配置。\n3.  Click on the **Run** button. Exercise 3 will run and produce the following output:\n\n    ![Figure 2A.20: Exercise 3 output](img/C14583_02A_20.jpg)\n\n    ###### 图 2A.20:练习 3 的输出\n\n4.  在编辑器中，在某处插入一个空行，点击**运行**按钮。(通过更改文件，它将强制构建系统重新编译**练习 3.cpp** 。)\n5.  If we now look at the **CDT Global Build Console**, we will see two warnings from the compiler:\n\n    ![Figure 2A.21: Exercise 3 compiler warnings](img/C14583_02A_21.jpg)\n\n    ###### 图 2A.21:练习 3 编译器警告\n\n    前面的截图没有显示完整的警告消息。对于这里使用的 gcc 编译器，完整的警告如下:\n\n    ```cpp\n    exercise3.cpp:22:45: warning: 'sizeof' on array function parameter 'ary' will return size of 'int*' [-Wsizeof-array-argument].\n    ```\n\n    该警告为我们提供了有关导致该问题的声明的更多信息。它可以追溯到以下函数:\n\n    ```cpp\n    void print_array_size2(int ary[10])\n    {\n        std::cout << \"---print_array_size2---\\n\";\n        std::cout << \"sizeof(ary) = \" << sizeof(ary) << \"\\n\";\n        std::cout << \"elements in (ary) = \" << sizeof(ary)/sizeof(ary[0]) << \"\\n\";\n    }\n    ```\n\n    在这里，我们可以看到一个数组衰减的例子——实际上，**意外的数组衰减**。自从 C 语言时代以来，数组和指针(几乎)是可以互换的。因此，就编译器而言，传递给`print_array_size2()`的参数属于`int*`类型，并且由声明 sizeof `将返回“int *”`大小的警告所证实:\n\n    ![Figure 2A.22: Exercise 3 partial output](img/C14583_02A_22.jpg)\n\n    ###### 图 2A.22:练习 3 部分输出\n\n    **sizeof(ary)/sizeof(arg[0])**计算应返回数组中的元素数量。(ary) = 10 中的第一个**元素是从主函数生成的，并且 ary 被声明为**ary【10】**，因此是正确的。- print_array_size2 -横幅下的**元素显示了数组衰减的问题以及编译器生成警告的原因。为什么值为 2？在测试 PC 上，一个指针占用 8 个字节(64 位)，而一个 int 只占用 4 个字节，所以我们得到 8/4 = 2。****\n\n6.  在编辑器中，找到 main()中声明 ary 的行，并将其更改为以下内容:\n\n    ```cpp\n    int ary[15]{};\n    ```\n\n7.  Click on the **Run** button. If you examine the **CDT Global Build Console**, you will see that the number of errors is still the same. This is another symptom of array decay. Let's say we're given the following function prototype:\n\n    ```cpp\n    void print_array_size2(int ary[10])\n    ```\n\n    由于参数原型不匹配，您可能会认为尝试传递`int ary[15]`会导致错误或至少是警告。如前所述，编译器将该参数视为`整数`，因此该函数也可以声明如下:\n\n    ```cpp\n    void print_array_size2(int* ary)\n    ```\n\n8.  In the editor, change the name of `print_array_size2` to `print_array_size` all through the file. Click on the **Run** button. When the Errors in Workspace dialog appears, click **Cancel**. Open the **CDT Global Build Console** and examine the error message:\n\n    ![Figure 2A.23: Redefinition error](img/C14583_02A_23.jpg)\n\n    ###### 图 2A.23:重定义错误\n\n    这一次，编译器生成了一个指向两个 print_array_size 方法的重定义错误，这两个方法似乎具有不同的参数类型–`int * ary`和`int【10】`。这是一种确认，当用作函数的参数时，`int ary[10]`生成的结果与声明`int*` ary 时的结果相同。\n\n9.  将文件恢复到其原始状态。\n10.  In the `main()` function, locate the line with `Step 11` in a comment and remove the comment at the beginning of the line. Click on the **Run** button. When the Errors in Workspace dialog appears, click **Cancel**. Open the **CDT Global Build Console** and examine the error message:\n\n    ![Figure 2A.24: Redefinition error](img/C14583_02A_24.jpg)\n\n    ###### 图 2A.24:重定义错误\n\n    完整的错误消息如下:\n\n    ```cpp\n    exercise3.cpp:53:9: error: invalid conversion from 'const char*' to 'char*' [-fpermissive]\n    ```\n\n    出现这种情况是因为编译器将标题**的类型确定为**常量字符*** 和类型**字符*** 的 p。恒量很重要。p 指针允许我们改变它所指向的值。**\n\n11.  Take a look at the following line:\n\n    ```cpp\n    p = title; \n    ```\n\n    将其更改为以下内容:\n\n    ```cpp\n    title = p;\n    ```\n\n12.  点击**运行**按钮。这一次，它可以正常构建和运行。将非常量指针分配给常量指针是可以的。\n\n在本练习中，我们了解到，在将数组传递给函数时需要谨慎对待，因为关键信息(数组的大小)将在调用中丢失。\n\n## 创建用户类型\n\nC++ 的伟大之处在于，您可以使用 **struct** 、 **class** 、 **enum** 或 **union** 创建自己的类型，编译器会在整个代码中将它视为基本类型。在本节中，我们将探索创建我们自己的类型和我们需要编写来操作它的方法，以及编译器将为我们创建的一些方法。\n\n### 枚举\n\n最简单的用户定义类型是枚举。枚举在 C++ 11 中得到了彻底的修改，使它们更加类型安全，所以我们必须考虑两种不同的声明语法。在我们看如何声明它们之前，让我们弄清楚为什么我们需要它们。考虑以下代码:\n\n```cpp\nint check_file(const char* name)\n{\n  FILE* fptr{fopen(name,\"r\")};\n  if ( fptr == nullptr)\n    return -1;\n  char buffer[120];\n  auto numberRead = fread(buffer, 1, 30, fptr);\n  fclose(fptr);\n  if (numberRead != 30)\n    return -2;\n  if(is_valid(buffer))\n    return -3;\n  return 0;\n}\n```\n\n这是许多 C 库函数的典型情况，其中返回了一个状态代码，您需要主页面来了解它们的含义。在前面的代码中，`-1`、`-2`、`-3`、`0`被称为**幻数**。你需要阅读代码来理解每个数字的含义。现在，考虑以下版本的代码:\n\n```cpp\nFileCheckStatus check_file(const char* name)\n{\n  FILE* fptr{fopen(name,\"r\")};\n  if ( fptr == nullptr)\n    return FileCheckStatus::NotFound;\n  char buffer[30];\n  auto numberRead = fread(buffer, 1, 30, fptr);\n  fclose(fptr);\n  if (numberRead != 30)\n    return FileCheckStatus::IncorrectSize;\n  if(is_valid(buffer))\n    return FileCheckStatus::InvalidContents;\n  return FileCheckStatus::Good;\n}\n```\n\n这使用枚举类来传递结果，并将含义附加到值的名称上。函数的用户现在可以使用枚举，因为代码更容易理解和使用。因此，神奇的数字(与状态相关)已经被一个具有描述性标题的枚举值所取代。让我们通过参考以下代码来了解`文件检查状态`的声明:\n\n```cpp\nenum FileCheckStatus             // Old-style enum declaration\n{\n  Good,                         // = 0 - Value defaults to 0\n  NotFound,                     // = 1 - Value set to one more than previous\n  IncorrectSize,                // = 2 - Value set to one more than previous\n  InvalidContents,              // = 3 - Value set to one more than previous\n};\n```\n\n如果我们想使用神奇数字的值，那么我们可以这样声明它们:\n\n```cpp\nenum FileCheckStatus             // Old-style enum declaration\n{\n  Good = 0, \n  NotFound = -1,\n  IncorrectSize = -2,\n  InvalidContents = -3,\n};\n```\n\n或者，通过颠倒顺序，我们可以设置第一个值，编译器将执行其余操作:\n\n```cpp\nenum FileCheckStatus             // Old-style enum declaration\n{\n  InvalidContents = -3,          // Force to -3\n  IncorrectSize,                 // set to -2(=-3+1)\n  NotFound,                      // Set to -1(=-2+1)\n  Good,                          // Set to  0(=-1+1)\n};\n```\n\n前面的函数也可以写成如下形式:\n\n```cpp\nFileCheckStatus check_file(const char* name)\n{\n  FILE* fptr{fopen(name,\"r\")};\n  if ( fptr == nullptr)\n    return NotFound;\n  char buffer[30];\n  auto numberRead = fread(buffer, 1, 30, fptr);\n  fclose(fptr);\n  if (numberRead != 30)\n    return IncorrectSize;\n  if(is_valid(buffer))\n    return InvalidContents;\n  return Good;\n}\n```\n\n请注意，代码中缺少作用域指令`FileCheckStatus::`，但它仍然会编译并工作。这就提出了范围的问题，我们将在后面的*章节【2B】**不允许鸭子入内-模板和推导*的*可见性、寿命和访问*部分详细讨论。现在，要知道每个类型和变量都有一个作用域，旧式枚举的问题是它们的枚举数被添加到与枚举相同的作用域中。假设我们有两个定义如下的枚举:\n\n```cpp\nenum Result \n{\n    Pass,\n    Fail,\n    Unknown,\n};\nenum Option\n{\n    Keep,\n    Discard,\n    Pass,\n    Play\n};\n```\n\n我们现在有一个问题，其中`Pass`枚举器被定义了两次，并且有两个不同的值。旧式枚举还允许我们编写有效的编译器，但显然是无意义的代码，例如:\n\n```cpp\nOption option{Keep};\nResult result{Unknown};\nif (option == result)\n{\n    // Do something\n}\n```\n\n当我们试图开发意图清晰且易于理解的代码时，将结果与选项进行比较没有任何意义。问题在于，编译器会隐式地将该值转换为整数，从而能够进行比较。\n\nC++ 11 引入了一个新概念，称为**枚举类**或**范围枚举**。前面代码的作用域枚举定义如下:\n\n```cpp\nenum class Result \n{\n    Pass,\n    Fail,\n    Unknown,\n};\nenum class Option\n{\n    Keep,\n    Discard,\n    Pass,\n    Play\n};\n```\n\n这意味着前面的代码将不再编译:\n\n```cpp\nOption option{Keep};          // error: must use scope specifier Option::Keep\nResult result{Unknown};       // error: must use scope specifier Result::Unknown\nif (option == result)         // error: can no longer compare the different types\n{\n    // Do something\n}\n```\n\n顾名思义，**作用域枚举**将枚举器放在枚举名称的作用域内。此外，作用域枚举将不再隐式转换为整数(因此 if 语句将无法编译)。您仍然可以将枚举数转换为整数，但是您需要强制转换它:\n\n```cpp\nint value = static_cast<int>(Option::Play);\n```\n\n### 练习 4:枚举-新旧学校\n\n在本练习中，我们将实现一个程序，该程序使用枚举来表示预定义的值，并确定当它们被更改为限定范围的枚举时所需的相应更改。让我们开始吧:\n\n1.  在 Eclipse 中打开**第 2 课**项目，然后在**项目浏览器**中，展开**第 2 课**，然后展开**练习 04** ，双击**练习 4.cpp** 在编辑器中打开本练习的文件。\n2.  点击**启动配置**下拉菜单，选择**新启动配置……**。将**练习 4** 配置为使用名称**练习 4** 运行。\n3.  完成后，它将是当前选定的启动配置。\n4.  Click on the **Run** button. Exercise 4 will run and produce the following output:\n\n    ![Figure 2A.25: Exercise 4 output](img/C14583_02A_25.jpg)\n\n    ###### 图 2A.25:练习 4 输出\n\n5.  在编辑器中检查代码。目前，我们可以比较苹果和橘子。在`printOrange()`的定义中，将参数更改为`Orange` :\n\n    ```cpp\n    void printOrange(Orange orange)\n    ```\n\n6.  Click on the **Run** button. When the Errors in Workspace dialog appears, click **Cancel**:\n\n    ![Figure 2A.26: Cannot convert error](img/C14583_02A_26.jpg)\n\n    ###### 图 2A.26:无法转换错误\n\n    通过改变参数类型，我们迫使编译器强制执行传递给函数的值的类型。\n\n7.  Call the `printOrange()` function twice by passing the `orange` `enum` variable in the initial call and the `apple` variable in the second call, respectively:\n\n    ```cpp\n    printOrange(orange);\n    printOrange(apple);\n    ```\n\n    这表明编译器正在隐式地将橙色和苹果转换成一个`int`，以便它可以调用该函数。另外，注意关于比较`苹果`和`橙`的警告。\n\n8.  通过取一个 int 参数并将`orange` `枚举`的定义更改为以下值来恢复`printOrange()`功能:\n\n    ```cpp\n    enum class Orange;\n    ```\n\n9.  Click on the **Run** button. When the Errors in Workspace dialog appears, click **Cancel**:\n\n    ![Figure 2A.27: Multiple errors for scoped enum changes](img/C14583_02A_27.jpg)\n\n    ###### 图 2A.27:作用域枚举更改的多个错误\n\n10.  Locate the first error listed for this build:\n\n    ![Figure 2A.28: First scoped enum error ](img/C14583_02A_28.jpg)\n\n    ###### 图 2A.28:第一个作用域枚举错误\n\n11.  关于作用域枚举，首先要注意的是，当引用枚举器时，它们必须有一个作用域说明符。因此，在编辑器中，转到并将这一行更改为以下内容:\n\n    ```cpp\n    Orange orange{Orange::Hamlin};\n    ```\n\n12.  Click on the **Run** button. When the Errors in Workspace dialog appears, click **Cancel**. Good news – the error count dropped to 8 from 9\\. Check the errors in the console and locate the first one:\n\n    ![Figure 2A.29: Second scoped enum error](img/C14583_02A_29.jpg)\n\n    ###### 图 2A.29:第二个作用域枚举错误\n\n    此错误报告无法找到插入运算符(<橙色类型。因为这涉及到一个基于模板的类(我们将在后面讨论)，所以错误消息变得非常冗长。花一分钟时间查看从这个错误到下一个错误(红线)出现的所有消息。它向您展示了编译器试图做什么来编译那一行。\n\n13.  将指示行改为如下:\n\n    ```cpp\n    std::cout << \"orange = \" << static_cast<int>(orange) << \"\\n\";\n    ```\n\n14.  Click on the **Run** button. When the Errors in Workspace dialog appears, click **Cancel**. Good news – the error count dropped to 6 from 8\\. Check the errors in the console and locate the first one:\n\n    ![](img/C14583_02A_30.jpg)\n\n    ###### 图 2A.30:第三范围枚举错误\n\n    这个错误报告(最后)你不能比较苹果和橘子。在这一点上，我们认为程序试图做一些没有意义的事情，没有必要试图修复其余的事情。我们可以通过再次将它转换为 int 来修复这个错误，但是我们也需要为下一个错误进行转换。最后一个错误是巴伦西亚缺少`Orange::`范围说明符。\n\n15.  留给你一个练习，让文件以`橙色`作为范围枚举再次编译。\n\n在本练习中，我们发现范围枚举改进了 C++ 的强类型检查，如果我们希望将它们用作整数值，那么我们需要强制转换它们，这与隐式转换的非范围枚举不同。\n\n#### 编译器错误疑难解答\n\n从前面的练习中可以看出，编译器可以从一个错误中生成大量的错误和警告消息。这就是为什么建议先找到第一个错误并先修复它。在 IDEs 中开发或使用带有颜色代码错误的构建系统可以使这变得更容易。\n\n### 结构和类别\n\n枚举是用户定义类型中的第一种，但它们并没有真正扩展语言，以便我们能够在适当的抽象级别上表达问题的解决方案。然而，结构和类允许我们捕获和分组数据，然后关联方法以一致和有意义的方式操作数据。\n\n如果我们考虑两个矩阵的乘法， *A (m x n)* 和 *B (n x p)* ，从而得到矩阵 *C (m x p)* ，那么 C 的第 I 行和第 jth 列的等式如下:\n\n![](img/C14583_02A_31.jpg)\n\n###### 图 2A.31:第 1 行和第 2 列的方程\n\n如果我们每次想要乘两个矩阵时都要写它，我们最终会得到许多嵌套的 for 循环。但是如果我们可以把一个矩阵抽象成一个类，那么我们可以把它简单地表达为两个整数或两个浮点数的乘积:\n\n```cpp\nMatrix a;\nMatrix b;\n// Code to initialize the matrices\nauto c = a * b;\n```\n\n这就是面向对象设计的美妙之处——数据封装和概念的抽象被解释得如此之深，以至于我们可以很容易地理解程序试图实现什么，而不会被细节所掩盖。一旦我们确定矩阵乘法被正确实现，那么我们就可以自由地专注于在更高的层次上解决我们的问题。\n\n下面的讨论涉及到类，但它同样适用于结构，并且主要适用于联合。在我们学习如何定义和使用类之后，我们将概述类、结构和联合之间的区别。\n\n### 分数等级\n\n为了向您展示如何定义和使用类，我们将开发`分数`类来实现有理数。定义后，我们可以像使用任何其他内置类型(加、减、乘、除)一样使用`分数`，而不用担心细节——这是抽象。我们现在可以在更高的层次上思考和推理一个分数，也就是抽象的层次。\n\n`分数`类将执行以下操作:\n\n*   包含两个整数成员变量，`m _ 分子`和`m _ 分母`\n*   提供复制自身、分配给自身、乘法、除法、加法和减法的方法\n*   提供写入输出流的方法\n\n为了实现上述目标，我们有以下定义:\n\n![Figure 2A.32: Definitions of operations](img/C14583_02A_32.jpg)\n\n###### 图 2A.32:操作的定义\n\n此外，我们执行的操作将需要通过将其减少到最低项来标准化分数。为此，分子和分母都要除以它们的最大公约数(GCD)。\n\n### 构造函数、初始化和析构函数\n\n用 C++ 代码表示的类定义是用于在内存中创建对象和通过对象的方法操作对象的模式。我们需要做的第一件事是告诉编译器我们希望声明一个新的类型——类。要声明`分数`类，我们从以下内容开始:\n\n```cpp\nclass Fraction\n{\n};\n```\n\n我们将它放在头文件 **Fraction.h** 中，因为我们希望在代码的其他区域重用这个类规范。\n\n接下来我们需要做的是引入要存储在类中的数据，在这种情况下是`m _ 分子`和`m _ 分母`。它们都是 int 类型的:\n\n```cpp\nclass Fraction\n{\n  int m_numerator;\n  int m_denominator;\n};\n```\n\n我们现在已经声明了要存储的数据，并给它们起了一个名字，熟悉数学的人都会理解每个成员变量存储了什么:\n\n![](img/C14583_02A_33.jpg)\n\n###### 图 2A.33:分数公式\n\n由于这是一个类，默认情况下，任何声明的项目都被认为是`私有的`。这意味着没有外部实体可以访问这些变量。正是这种隐藏(使数据私有，就此而言，一些方法)的特性使得 C++ 中的封装成为可能。C++ 有三个类访问修饰符:\n\n*   **public** :这意味着成员(变量或函数)可以从类外的任何地方访问。\n*   **private** :这意味着不能从类外访问成员(变量或函数)。事实上，它甚至不能被查看。私有变量和函数只能从类内部或通过友元方法或类来访问。公共函数使用私有成员(变量和函数)来实现所需的功能。\n*   **受保护**:这是公私交叉。从类外部来看，变量或函数是私有的。但是，对于从声明受保护成员的类派生的任何类，它们都被视为公共的。\n\n在我们对类的定义中，这一点不是很有用。让我们将声明更改为以下内容:\n\n```cpp\nclass Fraction\n{\npublic:\n  int m_numerator;\n  int m_denominator;\n};\n```\n\n通过这样做，我们可以访问内部变量。`分数；`变量声明会导致编译器做两件事:\n\n*   分配足够的内存来保存两个数据项(取决于类型，这可能涉及填充，也可能不涉及填充，即包含或添加未使用的内存来对齐成员以实现最有效的访问)。运算符的**size 可以告诉我们为我们的类分配了多少内存。**\n*   通过调用**默认构造函数**初始化数据项。\n\n这些步骤与编译器对内置类型所做的相同，也就是说，步骤 2 什么也不做，导致变量未初始化。但是这个默认构造函数是什么呢？它是做什么的？\n\n首先，默认构造函数是一个特殊的成员函数。它是许多可能的构造函数之一，其中三个被认为是特殊成员函数。构造函数可以用零个、一个或多个参数来声明，就像任何其他函数一样，但是它们不指定返回类型。构造函数的特殊用途是初始化所有成员变量，以将对象置于定义良好的状态。如果成员变量本身是一个类，那么可能没有必要指定如何初始化变量。如果成员变量是内置类型，那么我们需要为它们提供初始值。\n\n### 类特殊成员函数\n\n当我们定义一个新类型(结构或类)时，编译器将为我们创建多达六(6)个特殊成员函数:\n\n*   **默认构造函数** ( `分数::分数()`):当没有提供参数时调用(如前一节)。这可以通过没有构造函数的参数列表或定义所有参数的默认值来实现，例如`分数(int 分子=0，分母=1)`。编译器提供了一个`隐式` `内联`默认构造函数来执行成员变量的默认初始化——对于内置类型，这意味着什么也不做。\n*   **析构函数** ( `分数::~分数()`):这是一个特殊的成员函数，在对象生命周期结束时调用。其目的是释放对象在其生存期内分配和保留的任何资源。编译器提供了一个`公共` `内联`成员函数，调用成员变量的析构函数。\n*   **复制构造函数** ( `分数::分数(const Fraction & )`):这是另一个构造函数，其中第一个参数是`分数&`的一种形式，没有其他参数，或者其余参数都有默认值。第一个参数的形式是`分数&`、`常量分数&`、`挥发性分数&`或`常量挥发性分数&`中的一种。我们稍后会处理`const`，但不会处理本书中的`volatile`。编译器提供了一个`非显式` `公共` `内联`成员函数，通常采用`Fraction::Fraction(const Fraction&)`的形式，按照初始化的顺序复制每个成员变量。\n*   **复制赋值** ( **分数&分数::运算符=(分数& )** ):这是一个名为**运算符=** 的成员函数，第一个参数是一个值或类的任何引用类型，在本例中为**分数**、**分数&** 、**常量分数&** 、**挥发分数&** 或**编译器提供了一个**公共** **内联**成员函数，通常采用**Fraction::Fraction(const Fraction&)**的形式，按照初始化的顺序复制每个成员变量。**\n*   **Move Constructor**(`Fraction::Fraction(Fraction&&)`):这是 C++ 11 中引入的一种新型构造函数，其中第一个参数是`Fraction & &`的一种形式，没有其他参数，或者其余参数都有默认值。第一个参数的形式是`分数& &`、`常量分数& &`、`挥发分& &`或`常量挥发分& &`中的一种。编译器提供了一个`非显式` `公共` `内联`成员函数，通常采用`Fraction::Fraction(Fraction&&)`的形式，按照初始化的顺序移动每个成员变量。\n*   **移动赋值** ( `分数&分数::运算符=(分数& & )`):这是 C++ 11 中引入的一种新型赋值运算符，是一个名为`运算符=`的成员函数，第一个参数是移动构造函数允许的任何形式。编译器提供一个`公共` `内联`成员函数，通常采用`Fraction::Fraction(Fraction&&)`的形式，按照初始化的顺序复制每个成员变量。\n\n除了默认构造函数之外，这些函数处理管理这个类所拥有的资源——也就是说，如何复制/移动它们以及如何处置它们。另一方面，默认构造函数更像任何其他接受值的构造函数——它只初始化资源。\n\n我们可以声明这些特殊函数中的任何一个，强制它们默认(也就是说，让编译器生成默认版本)，或者强制它们不被创建。在其他特殊函数存在的情况下，也有关于何时自动生成这些函数的规则。前四个函数在概念上相对简单，但是两个“移动”特殊成员函数需要一些额外的解释。我们将在*第 3 章*、*能够和应该之间的距离——对象、指针和继承*中详细讨论所谓的移动语义，但目前它本质上是它所指示的——它将某物从一个对象移动到另一个对象。\n\n### 隐式与显式构造函数\n\n前面的描述讨论了编译器生成隐式或非显式构造函数。如果存在可以用一个参数调用的构造函数，例如复制构造函数或移动构造函数，默认情况下，允许编译器在必要时调用它，以便它可以将其从一种类型转换为另一种类型，从而允许对表达式、函数调用或赋值进行编码。这并不总是一个期望的行为，我们可能希望防止隐式转换，并确保如果我们类的用户真的想要转换，那么他们必须在程序中写出来。为此，我们在构造函数的声明前加上`显式的`关键字，如下所示:\n\n```cpp\nexplicit Fraction(int numerator, int denominator = 1);\n```\n\n显式关键字也可以应用于其他运算符，编译器可以将其用于类型转换。\n\n### 类特殊成员函数–编译器生成规则\n\n首先，如果我们声明任何其他形式的构造函数——默认、复制、移动或用户定义，将不会生成`默认构造函数`。其他特殊成员函数都不会影响其生成。\n\n其次，声明析构函数就不会产生`析构函数`。其他特殊成员函数都不会影响其生成。\n\n其他四个特殊函数的生成取决于析构函数或其他特殊函数之一的声明，如下表所示:\n\n![](img/C14583_02A_34.jpg)\n\n###### 图 2A.34:特殊成员函数生成规则\n\n### 默认和删除特殊成员功能\n\n在 C++ 11 之前，如果我们想防止使用复制构造函数或复制赋值成员函数，那么我们必须将函数声明为私有的，并且不提供函数的定义:\n\n```cpp\nclass Fraction\n{\npublic:\n  Fraction();\nprivate:\n  Fraction(const Fraction&);\n  Fraction& operator=(const Fraction&);\n};\n```\n\n通过这种方式，我们确保了如果有人试图从类外部访问复制构造函数或复制赋值，那么编译器会生成一个错误，指出该函数不可访问。这仍然声明了函数，并且它们可以从类中访问。取消这些特殊的成员功能是一种有效的手段，但并不完美。\n\n但是我们可以做得更好，因为 C++ 11 引入了两种新的声明形式，允许我们覆盖编译器的默认行为，如前面的规则中所定义的。\n\n首先，我们可以通过用`= delete`后缀声明方法来强制编译器不生成方法，如下所示:\n\n```cpp\nFraction(const Fraction&) = delete;\n```\n\n#### 注意\n\n如果不使用参数，我们可以省略它的名称。任何函数或成员函数都是如此。事实上，根据为编译器设置的警告级别，它甚至可能会生成一个警告，指出没有使用该参数。\n\n或者，我们可以使用`= default`后缀强制编译器生成其特殊成员函数的默认实现，如下所示:\n\n```cpp\nFraction(const Fraction&) = default;\n```\n\n如果这只是函数的声明，那么我们也可以省略参数的名称。尽管如此，良好的实践要求我们应该命名参数以指示其用途。这样，我们类的用户就不需要查看调用函数的实现。\n\n#### 注意\n\n使用默认后缀声明一个特殊的成员函数被认为是用户定义的成员函数。\n\n### 三/五法则和零法则\n\n正如我们之前讨论的，除了默认构造函数之外，特殊成员函数处理管理这个类所拥有的资源的语义——即如何复制/移动它们以及如何处置它们。这导致了 C++ 社区中关于处理特殊函数的两条“规则”。\n\n在 C++ 11 之前，有三的**规则，处理复制构造函数、复制赋值运算符和析构函数。它基本上声明我们需要实现这些方法中的一个，因为封装资源的管理并不简单。**\n\n随着 C++ 11 中移动构造函数和移动赋值操作符的引入，这个规则扩展到了五的**规则。规则的本质没有改变。简单来说，特殊成员函数的数量增加到了五个。记住编译器生成的规则，还有一个额外的原因来确保所有五个特殊方法都被实现(或者强制 via = default)，那就是，如果编译器没有访问移动语义函数的权限，它将尝试使用复制语义函数，而这可能不是所期望的。**\n\n#### 注意\n\n有关更多详细信息，请参见 C++ 核心指南的 C.ctor:构造函数、赋值函数和析构函数部分，可以在这里找到:[http://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines](http://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines)。\n\n### 构造函数–初始化对象\n\n构造函数的主要任务是将对象置于稳定状态，以便对象通过其成员函数执行的任何操作都会导致一致的定义行为。虽然前面的语句适用于复制和移动构造函数，但是它们通过不同的语义(从另一个对象复制或移动)来实现这一点。\n\n有四种不同的机制可供我们控制一个对象的初始状态。在这种情况下，C++ 有很多使用初始化的规则。我们将不详细讨论 C++ 标准的默认初始化、零初始化、值初始化、常数初始化等等。只要知道最好的方法是明确你的变量的初始化。\n\n**第一个**也是最不可取的初始化机制是给构造函数主体中的成员变量赋值，如下所示:\n\n```cpp\nFraction::Fraction()\n{\n  this->m_numerator = 0;\n  this->m_denominator = 1;\n}\nFraction::Fraction(int numerator, int denominator)\n{\n  m_numerator = numerator;\n  m_denominator = denominator;\n}\n```\n\n很清楚使用什么值来初始化变量。严格来说，这不是类的初始化——按照标准，初始化是在构造函数的主体被调用时完成的。这很容易维护，尤其是在这个类中。对于具有多个构造函数和许多成员变量的大型类，这可能是一个维护问题。如果您更改一个构造函数，您将需要更改所有的构造函数。它还有一个问题，如果成员变量是引用类型(我们将在后面讨论)，那么它不能在构造函数的主体中完成。\n\n默认构造函数使用**这个**指针。每个成员函数，包括构造函数和析构函数，都用一个隐式参数调用(即使它从未声明过)–这个指针的**。**此**指向对象的当前实例。 **- >** 运算符是另一个去引用运算符，在本例中是简写，即 ***(this)。m _ 分子**。 **this- >** 的使用是可选的，可以省略。其他语言，如 Python，需要声明和使用隐式指针/引用(Python 中的约定是调用 *self* )。**\n\n第二个**机制是成员初始化列表的使用，它的使用有一个警告。对于我们的分数类，我们有以下内容:**\n\n```cpp\nFraction::Fraction() : m_numerator(0), m_denominator(1)\n{\n}\nFraction::Fraction(int numerator, int denominator) :\n  m_numerator(numerator), m_denominator(denominator)\n{\n}\n```\n\n冒号后，、左大括号前的代码段{、in(`m _ 分子(0)、m _ 分母(1)`和`m _ 分子(分子)、m _ 分母(分母)`是成员初始化列表。我们可以在成员初始化列表中初始化一个引用类型。\n\n#### 成员初始化列表顺序\n\n无论成员在成员初始化列表中的放置顺序如何，编译器都将按照它们在类中声明的顺序初始化成员。\n\n第三个****推荐的**初始化是 C++ 11 中引入的默认成员初始化。当使用赋值或括号初始值设定项声明变量时，我们定义默认初始值:**\n\n```cpp\nclass Fraction\n{\npublic:\n  int m_numerator = 0;     // equals initializer\n  int m_denominator{1};    // brace initializer\n};\n```\n\n如果构造函数没有定义成员变量的初始值，那么这个默认值将用于初始化变量。这样做的好处是确保所有的构造函数产生相同的初始化，除非它们在构造函数的定义中被显式修改。\n\nC++ 11 还引入了第四种初始化风格，称为构造函数委托。它是对成员初始化列表的修改，在该列表中，不列出成员变量及其初始值，而是调用另一个构造函数。以下示例是人为设计的，您不会以这种方式编写类，但它显示了构造函数委托的语法:\n\n```cpp\nFraction::Fraction(int numerator) : m_numerator(numerator), m_denominator(1)\n{\n}\nFraction::Fraction(int numerator, int denominator) : Fraction(numerator)\n{\n  auto factor = std::gcd(numerator, denominator);\n  m_numerator /= factor;\n  m_denominator = denominator / factor;\n}\n```\n\n从具有两个参数的构造函数中调用单参数构造函数。\n\n### 练习 5:声明和初始化分数\n\n在本练习中，我们将使用不同的可用技术实现类成员初始化，包括构造函数委托。让我们开始吧:\n\n1.  在 Eclipse 中打开**第 2 课**项目，然后在**项目浏览器**中，展开**第 2 课**，然后展开**练习 05** ，双击**练习 5.cpp** 在编辑器中打开本练习的文件。\n2.  点击**启动配置**下拉菜单，选择**新启动配置……**。将**练习 5** 配置为以练习 5 的名称运行。\n3.  完成后，它将是当前选定的启动配置。\n4.  Click on the **Run** button. **Exercise 5** will run and produce something similar to the following output:\n\n    ![](img/C14583_02A_35.jpg)\n\n    ###### 图 2A.35:练习 5 典型输出\n\n    为分数报告的值来自不以任何方式初始化成员变量。如果你再运行一次，你很可能会得到不同的分数。\n\n5.  点击几次**运行**按钮。你会看到分数发生了变化。\n6.  在编辑器中，将构造函数更改如下:\n\n    ```cpp\n    Fraction() : m_numerator{0}, m_denominator{1}\n    {\n    }\n    ```\n\n7.  Click on the **Run** button and observe the output:\n\n    ![](img/C14583_02A_36.jpg)\n\n    ###### 图 2A.36:修改后的练习 5 输出\n\n    这一次，分数值由我们在成员初始化列表中指定的值定义。\n\n8.  在编辑器中，添加以下两个`构造函数` :\n\n    ```cpp\n    Fraction(int numerator) : m_numerator(numerator), m_denominator(1)\n    {\n    }\n    Fraction(int numerator, int denominator) : Fraction(numerator)\n    {\n      auto factor = std::gcd(numerator, denominator);\n      m_numerator /= factor;\n      m_denominator = denominator / factor;\n    }\n    ```\n\n9.  在主功能中，将`分数`的声明改为包括初始化:\n\n    ```cpp\n    Fraction fraction{3,2};\n    ```\n\n10.  Click on the **Run** button and observe the output:\n\n    ![](img/C14583_02A_37.jpg)\n\n###### 图 2A.37:构造函数委托的例子\n\n在本练习中，我们使用成员初始化列表和构造函数委托实现了成员变量初始化。*我们将返回到练习 7“向分数类添加运算符”中的分数。*\n\n### 值与参考值和常量\n\n到目前为止，我们只处理了值类型，即变量保存对象的值。指针保存我们感兴趣的值(即对象的地址)。但这会导致效率低下和资源管理问题。我们将在这里讨论如何解决效率低下的问题，但在*第 3 章*、*可以和应该之间的距离—对象、指针和继承*中讨论资源管理问题。\n\n考虑以下问题..我们有一个 10×10 的双类型矩阵，我们希望为它编写一个求逆函数。该类声明如下:\n\n```cpp\nclass Matrix10x10\n{\nprivate:\n  double m_data[10][10];\n};\n```\n\n如果我们取`sizeof(matrix x10x 10)`，我们会得到`sizeof(double)`x10x 10 = 800 字节。现在，如果我们为此实现一个逆矩阵函数，它的签名可能如下所示:\n\n```cpp\nMatrix10x10 invert(Matrix10x10 lhs);\nMatrix10x10 mat;\n// set up mat\nMatrix10x10 inv = invert(mat);\n```\n\n首先，这意味着编译器需要将`mat`保存的值传递给`invert()`函数，并将 800 字节复制到堆栈上。然后，该函数做它需要做的任何事情来反转矩阵(一个 L-U 分解，行列式的计算——无论实现者选择什么方法)，然后将 800 字节的结果复制回`inv`变量。在堆栈上传递大值从来都不是一个好主意，原因有二:\n\n*   堆栈是主机操作系统给我们程序的有限资源。\n*   在系统中复制大值是低效的。\n\n这种方法被称为按值传递。也就是说，我们希望处理的项目的值被复制到函数中。\n\n在 C(和 C++)中，这个限制是通过使用指针来解决的。前面的代码可能变成下面的代码:\n\n```cpp\nvoid invert(Matrix10x10* src, Matrix10x10* inv);\nMatrix10x10 mat;\nMatrix10x10 inv;\n// set up mat\ninvert(&mat, &inv);\n```\n\n这里，我们只是将 src 的地址和反向结果的目标作为两个指针传递(这是少量字节)。不幸的是，这导致每次我们使用`src`或`inv`时，函数内部的代码都必须使用取消引用运算符(`*`)，使得代码更难阅读。此外，指针的使用导致了许多问题。\n\nC++ 引入了一种更好的方法——变量别名或引用。引用类型用&符号( **&** )运算符声明。因此，我们可以如下声明反转方法:\n\n```cpp\nvoid invert(Matrix10x10& src, Matrix10x10& inv);\nMatrix10x10 mat;\nMatrix10x10 inv;\n// set up mat\ninvert(mat, inv);\n```\n\n请注意，调用方法不需要特殊运算符来传递引用。从编译器的角度来看，引用仍然是有一个限制的指针——它不能保存 nullptr。从程序员的角度来看，引用允许我们对代码进行推理，而不必担心在正确的地方有正确的取消引用操作符。这就是所谓的**通过参考**。\n\n我们看到引用被传递给复制构造函数和复制赋值方法。引用的类型，当用于它们的移动等价物时，被称为**右值引用操作符**，将在*第 3 章*、*Can 和 short 之间的距离-对象、指针和继承*中解释。\n\n`传递值`的一个优点是，我们不会无意中修改传递到方法中的变量值。现在，如果我们通过引用传递，我们不能再保证我们正在调用的方法不会修改原始变量。为了解决这个问题，我们可以将 invert 方法的签名更改如下:\n\n```cpp\nvoid invert(const Matrix10x10& src, Matrix10x10& inv);\n```\n\nconst 关键字告诉编译器，在处理`invert()`函数的定义时，给`src`引用的值的任何部分赋值都是非法的。如果该方法试图修改 src，编译器将生成一个错误。\n\n在指定类型–变量部分，我们发现`汽车标题`的声明导致`标题`属于`常量字符*`类型。现在，我们可以解释`const`部分。\n\n`标题`变量是**一个指向常量**的指针。换句话说，我们不能改变存储在我们所指向的内存中的数据的值。因此，我们无法做到以下几点:\n\n```cpp\n*title = 's';\n```\n\n这是因为编译器会生成与更改常数值相关的错误。然而，我们可以改变存储在指针中的值。我们可以执行以下操作:\n\n```cpp\ntitle = \"Maid Marian\";\n```\n\n我们现在已经介绍了用作函数参数类型的引用，但是它们也可以用作成员变量而不是指针。引用和指针之间有区别:\n\n引用必须引用实际的对象(没有 nullptr 的等价物)。引用一旦初始化就不能更改(这导致引用必须是初始化的默认成员或出现在成员初始化列表中)。只要对该对象的引用存在，该对象就必须存在(如果该对象可以在引用被销毁之前被销毁，那么如果试图访问该对象，就有可能出现未定义的行为)。\n\n### 练习 6:声明和使用引用类型\n\n在本练习中，我们将声明并使用引用类型，以使代码高效且易于阅读。让我们开始吧:\n\n1.  在 Eclipse 中打开**第 2 课**项目，然后在**项目浏览器**中，展开**第 2 课**，然后展开**练习 06** ，双击**练习 6.cpp** 在编辑器中打开本练习的文件。\n2.  点击**启动配置**下拉菜单，选择**新启动配置……**。将**练习 6** 配置为以练习 6 的名称运行。\n3.  完成后，它将是当前选定的启动配置。\n4.  Click on the **Run** button. Exercise 6 will run and produce something similar to the following output:\n\n    ![Figure 2A.38: Exercise 6 output](img/C14583_02A_38.jpg)\n\n    ###### 图 2A.38:练习 6 的输出\n\n    通过检查代码并将其与输出进行比较，我们会发现`值`变量允许我们操作(读取和写入)存储在`值`变量中的数据。我们有一个参考，`值`到`值`变量。我们还可以看到存储在`a`和`b`变量中的值是通过`swap()`函数交换的。\n\n5.  在编辑器中，更改 swap 的函数定义:\n\n    ```cpp\n    void swap(const int& lhs, const int& rhs)\n    ```\n\n6.  点击**运行**按钮。出现“工作空间中的错误”对话框时，单击**取消**。编译器报告的第一个错误如下所示:\n\n![Figure 2A.39: Read-only error on assignment](img/C14583_02A_39.jpg)\n\n###### 图 2A.39:赋值时的只读错误\n\n通过将参数从`int & lhs`更改为`const int & lhs`，我们已经告诉编译器在这个函数中不应该更改参数。因为我们在函数中给 lhs 赋值，编译器会生成 lhs 为只读的错误并终止。\n\n### 执行标准操作符\n\n要像使用内置类一样使用分数，我们需要它们与标准数学运算符(`+、-、*、/`)及其赋值对应物(`+=、-=、*=、/=`)一起工作。如果您不熟悉赋值运算符，请考虑以下两个表达式——它们产生相同的输出:\n\n```cpp\na = a + b;\na += b;\n```\n\n为 Fraction 声明这两个运算符的语法如下:\n\n```cpp\n// member function declarations\nFraction& operator+=(const Fraction& rhs);\nFraction operator+(const Fraction& rhs) const;\n// normal function declaration of operator+\nFraction operator+(const Fraction& lhs, const Fraction& rhs);\n```\n\n因为`运算符+=`方法修改左侧变量的内容(将 a 添加到 b，然后再次存储在 a 中)，所以建议将其实现为成员变量。在这种情况下，由于我们没有创建新的值，我们可以返回对现有 lhs 的引用。\n\n另一方面，operator+方法不应该修改 lhs 或 rhs 并返回一个新对象。实现者可以自由地将其实现为成员函数或自由函数。两者都显示在前面的代码中，但应该只存在一个。关于成员函数的实现，有趣的是声明末尾的 const 关键字。这告诉编译器，当调用这个成员函数时，它不会修改对象的内部状态。虽然这两种方法都有效，但如果可能的话，`运算符+`应该作为类外的正常函数来实现。\n\n其他运算符`–(减)`、`*(乘)`和`/(除)`也可以使用相同的方法。前面的方法实现了标准数学运算符的语义，并使我们的类型像内置类型一样工作。\n\n### 实现输出流操作符(< <)\n\nC++ 将输入/输出(I/O)抽象到标准库中的流类层次结构中(我们将在*章 2B* 、*不允许鸭子-模板和演绎*中讨论)。在*练习 5* 、*声明和初始化分数*中，我们看到可以将分数插入输出流，如下所示:\n\n```cpp\nstd::cout << \"fraction = \" << fraction.getNumerator() << \"/\" \n                           << fraction.getDenominator() << \"\\n\";\n```\n\n到目前为止，对于我们的 Fraction 类，我们已经通过使用`getmoleculator()`和`get 分母()`方法从外部访问数据值写出了分子和分母值，但是还有更好的方法。作为让我们的类在 C++ 中成为一流公民的一部分，在这种情况下，我们应该重载输入/输出操作符。在本章中，我们将只看输出操作符，< <，也称为插入操作符。这样，我们可以用一个更干净的版本来替换以前的代码:\n\n```cpp\nstd::cout << \"fraction = \" << fraction << \"\\n\";\n```\n\n我们可以将运算符重载为友元函数或普通函数(如果类提供了我们需要插入的数据的 getter 函数)。出于我们的目的，我们将其定义为一个普通函数:\n\n```cpp\ninline std::ostream& operator<< (std::ostream &out, const Fraction &rhs)\n{\n    out << rhs.getNumerator() << \" / \" << rhs.getDenominator();\n    return out;\n}\n```\n\n### 构建我们的代码\n\n在我们深入研究实现操作符并将我们的 Fraction 转换为 C++ 世界中成熟类型的练习之前，我们需要简单讨论一下我们将类的各个部分放在哪里——声明和定义。声明是我们类的蓝图，指出它需要什么样的数据存储以及它将实现的方法。定义是每个方法的实际实现细节。\n\n在像 Java 和 C#这样的语言中，声明和定义是相同的，它们必须存在于一个文件中(Java)或者跨多个文件(C#分部类)。在 C++ 中，根据类和您希望向其他类公开的程度，声明必须出现在头文件中(可以是其他文件中使用的 **#included** ),定义可以出现在三个位置之一——内嵌在定义中、**内嵌在与定义相同的文件中或单独的实现文件中。**\n\n头文件通常用。hpp 扩展名，而实现文件通常是`*。cpp`或`*。cxx`。实施文件也称为**翻译单元**。通过将一个函数定义为内联函数，我们允许编译器以一种甚至可能不存在于最终程序中的方式优化代码——它将我们放入函数的步骤替换为我们调用函数的位置。\n\n### 练习 7:向分数类添加运算符\n\n在本练习中，我们旨在使用单元测试来开发功能，从而在我们的 Fraction 类中实现运算符。这使得我们的分数类成为一个真正的类型。让我们开始吧:\n\n1.  在 Eclipse 中打开**第 2 课**项目，然后在**项目浏览器**中，展开**第 2 课**，然后展开**练习 07** ，双击**练习 7.cpp** 在编辑器中打开本练习的文件。\n2.  点击**启动配置**下拉菜单，选择**新启动配置……**。将练习 7 配置为以练习 7 的名称运行。\n3.  完成后，它将是当前选定的启动配置。\n4.  我们还需要配置一个单元测试。在 Eclipse 中，点击名为**运行** | **运行配置…** 的菜单项，右键点击左侧 **C/C++ 单元**，选择**新配置**。\n5.  将名称从`第 2A 课调试`改为`练习 7 测试`。\n6.  在 **C/C++ 应用**下，选择**搜索项目**选项，并在新对话框中选择**测试**。\n7.  Next, go to the **C/C++ Testing** tab and select **Google Tests Runner** in the dropdown. Click on **Apply** at the bottom of the dialog and click on the **Run** option for the test, which we have to run for the first time:\n\n    ![Figure 2A.40: Failing tests – multiplication](img/C14583_02A_40.jpg)\n\n    ###### 图 2A.40:测试失败-乘法\n\n8.  在编辑器中打开 **Fraction.cpp** 文件，找到`运算符*=`函数。用以下代码更新:\n\n    ```cpp\n    Fraction& Fraction::operator*=(const Fraction& rhs)\n    {\n      Fraction tmp(m_numerator*rhs.m_numerator, m_denominator*rhs.m_denominator);\n      *this = tmp;\n      return *this;\n    }\n    ```\n\n9.  Click on the **Run** button to rerun the tests. This time, all the tests pass:\n\n    ![Figure 2A.41: Passing tests](img/C14583_02A_41.jpg)\n\n    ###### 图 2A.41:通过测试\n\n10.  在您的 IDE 中，打开**测试/分数测试. cpp** 文件，找到失败的两个测试。一个测试了**操作员*=()** ，另一个测试了**操作员*(T5】。固定**操作符*=()** 如何固定**操作符*()** ？如果你在编辑器中打开 Fraction.hpp，你会发现**运算符*(T11)**函数是通过调用**运算符*=()** 为你实现的，也就是说，它被标记为内联的，是一个普通函数，而不是成员函数。一般来说，这是重载这些运算符时要采取的方法——修改调用它的对象的方法是成员函数，而必须生成新值的方法是调用成员函数的普通函数。**\n11.  在编辑器中打开 **Fraction.hpp** 并更改文件顶部附近的行，使其内容如下:\n\n    ```cpp\n    #define EXERCISE7_STEP  11\n    ```\n\n12.  Click on the **Run** button to rerun the tests – this time, we have added two more tests that fail – `AddFractions` and `AddFractions2`:\n\n    ![Figure 2A.42: Additional tests to fail](img/C14583_02A_42.jpg)\n\n    ###### 图 2A.42:附加测试失败\n\n13.  在**函数. cpp** 文件中找到`运算符+=`函数。\n14.  对功能进行必要的更改，点击**运行**按钮重新运行测试，直到测试通过。看看前面给出的定义其操作的等式，看看`算子*=()`是如何实现的。\n15.  在编辑器中打开 **Fraction.hpp** ，将文件顶部附近的行改为这样:\n\n    ```cpp\n    #define EXERCISE7_STEP  15\n    ```\n\n16.  点击**运行**按钮重新运行测试——这次，我们又增加了两个失败的测试——`减法分数`和`减法分数 2`。\n17.  在 Function.cpp 文件中找到`运算符-=`函数。\n18.  对功能进行必要的更改，点击**运行**按钮重新运行测试，直到测试通过。\n19.  在编辑器中打开 **Fraction.hpp** ，将文件顶部附近的行改为这样:\n\n    ```cpp\n    #define EXERCISE7_STEP  19\n    ```\n\n20.  点击**运行**按钮重新运行测试–这一次，我们又增加了两个失败的测试–**分流**和**分流 2** 。\n21.  在**函数. cpp** 文件中找到`运算符/=`函数。\n22.  对功能进行必要的更改，点击**运行**按钮重新运行测试，直到测试通过。\n23.  在编辑器中打开 **Fraction.hpp** ，将文件顶部附近的行改为这样:\n\n    ```cpp\n    #define EXERCISE7_STEP  23\n    ```\n\n24.  点击**运行**按钮重新运行测试–这一次，我们又增加了一个失败的测试–`插入操作员`。\n25.  在 Function.hpp 文件中找到`运算符< <`函数。\n26.  对功能进行必要的更改，点击**运行**按钮重新运行测试，直到测试通过。\n27.  从**启动配置**中，选择**练习 7** 并点击**运行**按钮。这将产生以下输出:\n\n![Figure 2A.43: Functional Fraction class](img/C14583_02A_43.jpg)\n\n###### 图 2A.43:函数分数类\n\n这就完成了我们现在对`分数`类的实现。当我们在*第三章*、*可以和应该之间的距离——对象、指针和继承*中考虑异常时，我们将再次回到它，这样我们就可以处理分数中的非法值(分母为 0)。\n\n### 功能过载\n\nC++ 支持一种称为函数重载的特性，即两个或多个函数具有相同的名称，但是它们的参数列表不同。参数的数量可以相同，但至少有一种参数类型必须不同。或者，它们可能有不同数量的参数。所以，多种功能的功能原型是不同的。但是，两个函数不能具有相同的函数名、相同的参数类型和不同的返回类型。以下是重载的一个示例:\n\n```cpp\nstd::ostream& print(std::ostream& os, int value) {\n   os << value << \" is an int\\n\";\n   return os;\n}\nstd::ostream& print(std::ostream& os, float value) {\n   os << value << \" is a single precision float\\n\";\n   return os;\n}\nstd::ostream& print(std::ostream& os, double value) {\n   os << value << \" is a double precision float \\n\";\n   return os;\n}\n// The next function causes the compiler to generate an error\n// as it only differs by return type.\nvoid print(std::ostream& os, double value) {\n   os << value << \" is a double precision float!\\n\";\n}\n```\n\n到目前为止，`分式`上的多个构造函数和重载的算术运算符都是重载函数的例子，编译器在遇到这些函数中的一个时必须引用它们。考虑以下代码:\n\n```cpp\nint main(int argc, char** argv) {\n   print(42);\n}\n```\n\n当编译器遇到行`print(42)`时，它需要计算出调用哪个先前定义的函数，因此它执行以下过程(非常简化):\n\n![Figure 2A.44: Function overload resolution (simplified)](img/C14583_02A_44.jpg)\n\n###### 图 2A.44:功能过载分辨率(简化)\n\nC++ 标准定义了编译器如何根据如何操作(即转换)参数以获得匹配来确定最佳候选函数的规则。如果不需要转换，那么该函数是最佳匹配。\n\n### 类、结构和联合\n\n当您定义一个类并且没有指定访问修饰符(公共、受保护、私有)时，默认情况下所有成员都是私有的:\n\n```cpp\nclass Fraction\n{\n  Fraction() {};            // All of these are private\n  int m_numerator;\n  int m_denominator;\n};\n```\n\n当您定义结构且未指定访问修饰符(公共、受保护、私有)时，默认情况下，所有成员都是公共的:\n\n```cpp\nstruct Fraction\n{\n  Fraction() {};            // All of these are public\n  int m_numerator;\n  int m_denominator;\n};\n```\n\n还有一个区别，我们将在解释继承和多态性后再看。联合是一种不同于结构和类的数据构造，但却是相同的。联合是一种特殊类型的结构声明，其中所有成员占用相同的内存，并且在给定时间只有一个成员有效。`联盟`声明的示例如下:\n\n```cpp\nunion variant\n{\n  int m_ivalue;\n  float m_fvalue;\n  double m_dvalue;\n};\n```\n\n当您定义联合并且没有指定访问修饰符(公共、受保护、私有)时，默认情况下所有成员都是公共的。\n\n联合的主要问题是，没有内在的方法知道在任何给定的时间哪个值是有效的。这是通过定义一个被称为*标记的联合*来解决的——也就是说，一个保存联合的结构和一个标识它是否是有效值的枚举。对于联合中可以包含什么和不能包含什么，还有其他限制(例如，只有一个成员可以有默认的成员初始值设定项)。我们不会在这本书里深入探讨工会。\n\n### 活动 1:图形处理\n\n在现代计算环境中，矩阵无处不在地被用来解决各种问题——解联立方程、分析电网或电路、对图形渲染对象进行操作，以及实现机器学习。在图形世界中，无论是二维(2D)还是三维(3D)，您想要对对象执行的所有操作都可以在矩阵乘法的帮助下完成。您的团队负责开发点的表示、变换矩阵以及您可能想要对它们执行的操作。按照以下步骤实现:\n\n1.  从**第 2A 课/练习 01** 文件夹加载准备好的项目。\n2.  创建一个名为 **Point3d** 的类，它可以默认构造为原点，或者使用一个由三个或四个值组成的初始化列表(数据直接存储在类中)。\n3.  创建一个名为 **Matrix3d** 的类，它可以默认构造为一个身份矩阵，或者使用嵌套的初始化列表来提供所有的值(数据直接存储在类中)。\n4.  在**点 3d** 上，重载`运算符()`，使其接受(`索引`)参数，以便返回位于`x(0)`、`y(1)`、`z(2)`和`w(3)`的值。\n5.  在**矩阵 3d** 上，重载`运算符()`以获取(`行，col`列)参数，使其返回值。\n6.  添加单元测试来验证上述所有特性。\n7.  将`运算符*=(const Matrix3d & )`和`运算符==(const Matrix3d & )`添加到 **Matrix3d** 类中，并对它们进行单元测试。\n8.  添加将两个**矩阵 3d** 对象和一个**矩阵 3d** 对象乘以一个**点 3d** 对象的自由函数。\n9.  添加创建矩阵的独立方法，以平移、缩放和旋转(围绕 x、y、z 轴)及其单元测试。\n\n执行上述步骤后，预期输出如下:\n\n![](img/C14583_02A_45.jpg)\n\n###### 图 2A.45:成功运行活动程序\n\n就本次活动而言，我们不会担心指数超出范围的可能性。我们将在*第 3 章*、*能够和应该之间的距离——对象、指针和继承*中讨论这一点。单位矩阵是一个正方形矩阵(在我们的例子中是 4 x 4)，其中对角线上的所有值都设置为 1，其他所有值都为零。\n\n当使用三维图形时，我们为点(顶点)和变换使用增广矩阵，以便所有的变换(平移、缩放、旋转)都可以通过使用乘法来实现。\n\n一个`n × m`矩阵是一个由 n 行 m 个数组成的数组。例如，一个`2 x 3`矩阵可能如下所示:\n\n![Figure 2A.46: Matrix of 2x3](img/C14583_02A_46.jpg)\n\n###### 图 2A . 46:2x 3 的矩阵\n\n一个三维顶点可以表示为一个`三元组(x，y，z)`。然而，我们用另一个纵坐标`w (=1 代表一个顶点，=0 代表一个方向)`来扩充它，使它成为一个`四元组(x，y，z，1)`。我们不使用元组，而是将其放在`4 x 1`矩阵中，如下所示:\n\n![Figure 2A.47: 4x1 Matrix](img/C14583_02A_47.jpg)\n\n###### 图 2A.47: 4x1 矩阵\n\n如果我们将`4×1`矩阵(点)乘以一个`4×4`矩阵(变换)，我们就可以操纵该点。如果`Ti`代表一个变换，那么我们可以将这些变换相乘来实现对点的一些操作:\n\n![Figure 2A.48: Multiplying transformations](img/C14583_02A_48.jpg)\n\n###### 图 2A.48:乘法变换\n\n要乘以一个变换矩阵，`A x P = B`，我们执行以下操作:\n\n![Figure 2A.49: Multiplying transformation matrix](img/C14583_02A_49.jpg)\n\n###### 图 2A.49:乘法变换矩阵\n\n我们也可以这样表达:\n\n![Figure 2A.50: Expression of multiplying transformations](img/C14583_02A_50.jpg)\n\n###### 图 2A.50:乘法变换的表达式\n\n同样，两个`4×4`矩阵相乘也可以得到相同的结果，`AxB=C`:\n\n![Figure 2A.51 Expression of 4x4 matrix multiplication:](img/C14583_02A_51.jpg)\n\n###### 图 2A . 51 4x 4 矩阵乘法的表达式:\n\n转换的矩阵如下:\n\n![Figure 2A.52: List of matrices for transformation](img/C14583_02A_52.jpg)\n\n###### 图 2A.52:用于转换的矩阵列表\n\n#### 注意\n\n这项活动的解决方案可以在第 635 页找到。\n\n## 总结\n\n在这一章中，我们学习了 C++ 中的类型。首先，我们接触了内置类型，然后学习了如何创建我们自己的行为类似于内置类型的类型。我们学习了如何声明和初始化变量，了解了编译器从源代码中生成什么，将变量放在哪里，链接器如何将变量放在一起，以及在计算机内存中是什么样子。我们学习了一些围绕零规则和五规则的 C++ 部落智慧。这些构成了 C++ 的构建模块。在下一章中，我们将研究用 C++ 模板创建函数和类，并进一步探索应用于模板的类型推导。********"
  },
  {
    "path": "docs/adv-cpp/03.md",
    "content": "# 三、不允许鸭子——模板和推导（二）\n\n## 学习目标\n\n本章结束时，您将能够:\n\n*   使用继承和多态性开发自己的类，以获得更大的效果\n*   实现一个别名，使您的代码更容易阅读\n*   使用 SFINAE 和 constexpr 开发模板来简化代码\n*   使用 STL 实现您自己的解决方案，以利用泛型编程\n*   描述类型演绎的背景和基本规则\n\n本章将向您展示如何通过继承、多态性和模板来定义和扩展您的类型。\n\n## 简介\n\n在前一章中，我们学习了如何在单元测试的帮助下开发我们自己的类型(类)，并使它们像内置类型一样工作。我们被介绍了函数重载、三/五法则和零法则。\n\n在本章中，我们将学习如何进一步扩展类型系统。我们将学习如何使用模板创建函数和类，并重新访问函数重载，因为它受到模板使用的影响。我们将被介绍一项新技术 **SFINAE** ，并使用它来控制我们模板中包含在生成代码中的部分。\n\n## 继承、多态性和接口\n\n到目前为止，在我们面向对象设计和 C++ 的旅程中，我们一直专注于抽象和数据封装。我们现在将注意力转向**遗传**和**多态性**。什么是继承？什么是多态性？我们为什么需要它？考虑以下三个对象:\n\n![Figure 2B.1: Vehicle objects ](img/C14583_02B_01.jpg)\n\n###### 图 2B.1:车辆物体\n\n在上图中，我们可以看到有三个非常不同的对象。他们有一些共同点。它们都有轮子(不同的数量)、发动机(不同的尺寸、功率或配置)、启动发动机、驱动、踩刹车、停止发动机等等，使用它们我们可以做一些事情。\n\n因此，我们可以把它们抽象成一种叫做载体的东西，来展示这些属性和一般行为。如果我们将其表示为 C++ 类，它可能如下所示:\n\n```cpp\nclass Vehicle\n{\npublic:\n  Vehicle() = default;\n  Vehicle(int numberWheels, int engineSize) : \n          m_numberOfWheels{numberWheels}, m_engineSizeCC{engineSize}\n  {\n  }\n  bool StartEngine()\n  {\n    std::cout << \"Vehicle::StartEngine \" << m_engineSizeCC << \" CC\\n\";\n    return true;\n  };\n  void Drive()\n  {\n    std::cout << \"Vehicle::Drive\\n\";\n  };\n  void ApplyBrakes()\n  {\n    std::cout << \"Vehicle::ApplyBrakes to \" << m_numberOfWheels << \" wheels\\n\";\n  };\n  bool StopEngine()\n  {\n    std::cout << \"Vehicle::StopEngine\\n\";\n    return true;\n  };\nprivate:\n  int m_numberOfWheels {4};\n  int m_engineSizeCC{1000};\n};\n```\n\n`车辆`类是`摩托车`、`汽车`、`卡车`的更广义(或抽象)的表达。我们现在可以通过重用车辆类中已经可用的东西来创建更专门的类型。我们将通过使用继承来重用 Vehicle 的属性和方法。继承的语法如下:\n\n```cpp\nclass DerivedClassName : access_modifier BaseClassName\n{\n  // Body of DerivedClass\n};\n```\n\n我们之前遇到过`公共`、`受保护`和`私有`等访问修饰符。它们控制我们如何访问基类的成员。摩托车等级将按如下方式推导:\n\n```cpp\nclass Motorcycle : public Vehicle\n{\npublic:\n  Motorcycle(int engineSize) : Vehicle(2, engineSize) {};\n};\n```\n\n在这种情况下，车辆类被称为**基类**或**超类**，而摩托车类被称为**衍生类**或**子类**。我们可以用图形表示如下，箭头从派生类指向基类:\n\n![Figure 2B.2: Vehicle class hierarchy ](img/C14583_02B_02.jpg)\n\n###### 图 2B.2:车辆等级体系\n\n但是摩托车的驾驶方式不同于普通车辆。因此，我们需要修改`摩托车`类，使其行为不同。更新后的代码如下:\n\n```cpp\nclass Motorcycle : public Vehicle\n{\npublic:\n  Motorcycle(int engineSize) : Vehicle(2, engineSize) {};\n  void Drive()\n  {\n    std::cout << \"Motorcycle::Drive\\n\";\n  };\n};\n```\n\n如果我们考虑面向对象的设计，这是关于用协作的对象来建模问题空间。这些对象通过消息相互通信。现在，我们有两个类以不同的方式响应相同的消息(驱动方法)。消息的发送者不知道会发生什么，也不真正关心，这就是多态的本质。\n\n#### 注意\n\n多态来自希腊语 poly 和 morph，其中`poly`表示多，`morph`表示形式。所以，多态性意味着`有多种形式`。\n\n我们现在可以使用这些类来测试多态性:\n\n```cpp\n#include <iostream>\nint main()\n{\n  Vehicle vehicle;\n  Motorcycle cycle{1500};\n  Vehicle* myVehicle{&vehicle};\n  myVehicle->StartEngine();\n  myVehicle->Drive();\n  myVehicle->ApplyBrakes();\n  myVehicle->StopEngine();\n  myVehicle = &cycle;\n  myVehicle->StartEngine();\n  myVehicle->Drive();\n  myVehicle->ApplyBrakes();\n  myVehicle->StopEngine();\n  return 0;\n}\n```\n\n如果我们编译并运行这个程序，我们会得到以下输出:\n\n![Figure 2B.3: Vehicle program output  ](img/C14583_02B_03.jpg)\n\n###### 图 2B.3:车辆程序输出\n\n上图截图中`车辆::StartEngine 1500 cc`后的行都与`摩托车`有关。但是驱动线仍然显示`车辆::驱动`而不是预期的`摩托车::驱动`。这是怎么回事？问题是我们没有告诉编译器`Vehicle`类中的`Drive`方法可以被派生类修改(或者覆盖)。我们需要在代码中做一个改变:\n\n```cpp\nvirtual void Drive()\n{\n  std::cout << \"Vehicle::Drive\\n\";\n};\n```\n\n通过在成员函数声明前添加`virtual`关键字，我们告诉编译器，派生类可以(但不必)重写或替换该函数。如果我们进行这种更改，然后编译并运行程序，我们会得到以下输出:\n\n![Figure 2B.4: Vehicle program output with virtual methods ](img/C14583_02B_04.jpg)\n\n###### 图 2B.4:使用虚拟方法的车辆程序输出\n\n现在，我们已经了解了遗传和多态性。我们使用指向`车辆`类的指针来控制`摩托车`类。作为最佳实践，应该对代码进行另一次更改。我们还应该将`摩托车`中`驱动`功能的声明更改如下:\n\n```cpp\nvoid Drive() override\n{\n  std::cout << \"Motorcycle::Drive\\n\";\n};\n```\n\nC++ 11 引入了`override`关键字作为对编译器的提示，声明一个特定的方法应该具有与其父树中某处的方法相同的函数原型。如果找不到，编译器将报告错误。这是一个非常有用的特性，可以节省您几个小时的调试时间。如果编译器有办法报告错误，就使用它。越早发现缺陷，越容易修复。最后一个变化是，每当我们向一个类添加一个虚函数时，我们必须声明它的析构函数`为虚函数`:\n\n```cpp\nclass Vehicle\n{\npublic:\n  // Constructors - hidden \n  virtual ~Vehicle() = default;  // Virtual Destructor\n  // Other methods and data -- hidden\n};\n```\n\n在虚拟化之前，我们通过`Drive()`功能看到了这一点。当通过指向车辆的指针调用析构函数时，它需要知道要调用哪个析构函数。因此，虚拟化可以实现这一点。如果做不到这一点，那么最终可能会出现资源泄漏或拼接对象。\n\n### 继承和访问说明符\n\n正如我们前面提到的，从超类继承一个子类的一般形式如下:\n\n```cpp\nclass DerivedClassName : access_modifier BaseClassName\n```\n\n当我们从车辆类派生摩托车类时，我们使用以下代码:\n\n```cpp\nclass Motorcycle : public Vehicle\n```\n\n访问修饰符是可选的，也是我们之前遇到过的修饰符之一:`公共`、`受保护`、`私有`。在下表中，您可以看到基类成员的可访问性。如果省略了 access_modifier，则编译器会假定指定了 private。\n\n![Figure 2B.5: Accessibility of base class members in derived classes  ](img/C14583_02B_05.jpg)\n\n###### 图 2B.5:派生类中基类成员的可访问性\n\n### 抽象类和接口\n\n到目前为止，我们讨论的所有类都是**具体类**——它们可以被实例化为一个变量的类型。还有另一种类型的类——一个**抽象类**，它包含至少一个**纯虚拟成员函数**。纯虚函数是类中没有定义(或实现)的虚函数。并且因为它没有实现，所以类的格式不正确(或者是抽象的)，不能被实例化。如果你试图创建一个抽象类型的变量，那么编译器会产生一个错误。\n\n要声明纯虚拟成员函数，以`= 0`结束函数原型声明。为了使 Drive()成为 Vehicle 类中的纯虚拟函数，我们将声明如下:\n\n```cpp\nvirtual void Drive() = 0;\n```\n\n现在，为了能够使用派生类作为变量类型(例如`摩托车`类)，它必须定义`驱动()`函数的实现。\n\n但是，您可以将变量声明为抽象类的指针或抽象类的引用。无论是哪种情况，它都必须指向或引用从抽象类派生的某个非抽象类。\n\n在 Java 中，有一个关键字接口，允许你定义一个全是纯虚函数的类。在 C++ 中，通过声明一个只声明公共纯虚函数的类(和一个虚拟析构函数)，可以实现同样的效果。这样，我们定义了一个接口。\n\n#### 注意\n\n在解决本章中的任何实际问题之前，下载本书的 GitHub 资源库([https://github.com/TrainingByPackt/Advanced-CPlusPlus](https://github.com/TrainingByPackt/Advanced-CPlusPlus))并导入 Eclipse 中 2B 课的文件夹，以便您可以查看每个练习和活动的代码。\n\n### 练习 1:用多态性实现游戏角色\n\n在本练习中，我们将演示继承、接口和多态性。我们将从角色扮演游戏的特别实现开始，并将其发展成更通用和可扩展的。让我们开始吧:\n\n1.  打开 Eclipse，使用在**第 2B 章**示例文件夹中找到的文件创建一个名为**第 2B 章**的新项目。\n2.  由于这是一个基于 **CMake 的项目**，将当前的构建器改为 **Cmake Build(可移植)**。\n3.  转到**项目** | **构建所有**菜单构建所有练习。默认情况下，屏幕底部的控制台会显示 **CMake 控制台【第 2B 课】**。\n4.  Configure a **New Launch Configuration** named **L2BExercise1** that runs the **Exercise1** binary and click on **Run** to build and run **Exercise 1**. You will receive the following output:\n\n    ![Figure 2B.6: Exercise 1 default output ](img/C14583_02B_06.jpg)\n\n    ###### 图 2B.6:练习 1 默认输出\n\n5.  Open **Exercise1.cpp** in the editor and examine the existing code. You will notice that the three characters are implemented as separate classes and that they are each instantiated and manipulated separately by calling `speak()` and `act()` directly. This is fine for a small program. But as the game grew to tens or hundreds of characters, it would become unmanageable. So, we need to abstract all the characters. Add the following Interface declaration to the top of the file:\n\n    ```cpp\n    class ICharacter\n    {\n    public:\n        ~ICharacter() {\n            std::cout << \"Destroying Character\\n\";\n        }\n        virtual void speak() = 0;\n        virtual void act() = 0;\n    };\n    ```\n\n    通常，析构函数是空的，但是在这里，它有日志来显示行为。\n\n6.  从该接口类中派生`巫师`、`治疗者`和`战士`类，并在每个类的`speak()`和`act()`函数的声明末尾添加`override`关键字:\n\n    ```cpp\n    class Wizard : public Icharacter { ...\n    ```\n\n7.  Click the **Run** button to rebuild and run the exercise. We will now see that the base class destructor is also called after the destructor of the derived class:\n\n    ![Figure 2B.7: Output of the modified program ](img/C14583_02B_07.jpg)\n\n    ###### 图 2B.7:修改后程序的输出\n\n8.  创建角色并在一个容器中管理它们，例如`向量`。在文件中创建以下两种方法，在`主()`功能之前:\n\n    ```cpp\n    void createCharacters(std::vector<ICharacter*>& cast)\n    {\n        cast.push_back(new Wizard(\"Gandalf\"));\n        cast.push_back(new Healer(\"Glenda\"));\n        cast.push_back(new Warrior(\"Ben Grimm\"));\n    }\n    void freeCharacters(std::vector<ICharacter*>& cast)\n    {\n        for(auto* character : cast)\n        {\n            delete character;\n        }\n        cast.clear();\n    }\n    ```\n\n9.  将`main()`的内容替换为如下代码:\n\n    ```cpp\n    int main(int argc, char**argv)\n    {\n        std::cout << \"\\n------ Exercise 1 ------\\n\";\n        std::vector<ICharacter*> cast;\n        createCharacters(cast);\n        for(auto* character : cast)\n        {\n            character->speak();\n        }\n        for(auto* character : cast)\n        {\n            character->act();\n        }\n        freeCharacters(cast);\n        std::cout << \"Complete.\\n\";\n        return 0;\n    }\n    ```\n\n10.  Click the **Run** button to rebuild and run the exercise. Here is the output that is generated:\n\n    ![Figure 2B.8: Output of the polymorphic version ](img/C14583_02B_08.jpg)\n\n    ###### 图 2B.8:多态版本的输出\n\n    从前面的截图可以看到，**摧毁精灵**等的日志已经消失。问题是容器保存指向基类的指针，并且它不知道如何在每种情况下调用完整的析构函数。\n\n11.  要解决这个问题，只需将`的析构函数`声明为虚拟的:\n\n    ```cpp\n    virtual ~ICharacter() {\n    ```\n\n12.  点击**运行**按钮重建并运行练习。输出内容如下:\n\n![Figure 2B.9: Output from the full polymorphic version ](img/C14583_02B_09.jpg)\n\n###### 图 2B.9:完整多态版本的输出\n\n我们现在已经实现了一个到我们的`ICharacter`字符的接口，并通过存储在容器中的基类指针简单地调用`speak()`和`act()`方法来多形态地使用它们。\n\n### 重新审视类、结构和联合\n\n之前，我们讨论过类和结构之间的区别是默认的访问修饰符——类是私有的，结构是公共的。这种差异更进一步——如果基类没有指定任何内容，它也适用于基类:\n\n```cpp\nclass DerivedC : Base  // inherits as if \"class DerivedC : private Base\" was used\n{\n};\nstruct DerivedS : Base // inherits as if \"struct DerivedS : public Base\" was used\n{\n};\n```\n\n应该注意的是，联合既不能是基类，也不能从基类派生。如果结构和类本质上没有区别，那么我们应该使用哪种类型呢？本质上，这是一个惯例。一个**结构**用来捆绑几个相关的元素，而一个**类**可以做事，有责任。结构的示例如下:\n\n```cpp\nstruct Point     // A point in 3D space\n{\n  double m_x;\n  double m_y;\n  double m_z;\n};\n```\n\n在前面的代码中，我们可以看到它将三个坐标组合在一起，这样我们就可以对三维空间中的一个点进行推理。这个结构可以作为一个连贯的数据集传递给需要点的方法，而不是每个点三个单独的参数。另一方面，类对可以执行动作的对象进行建模。看看下面的例子:\n\n```cpp\nclass Matrix\n{\npublic:\n  Matrix& operator*(const Matrix& rhs)\n  {\n     // nitty gritty of the multiplication\n  }\nprivate:\n  // Declaration of the 2D array to store matrix.\n};\n```\n\n经验法则是，如果至少有一个私有成员，就使用一个类，因为这意味着实现的细节将在公共成员函数的后面。\n\n## 可见性、寿命和访问\n\n我们已经讨论了创建自己的类型和声明变量和函数，同时主要关注简单函数和单个文件。我们现在将看看当有多个包含类和函数定义的源文件(翻译单元)时会发生什么。此外，我们将检查哪些变量和函数可以从源文件的其他部分看到，这些变量存在多长时间，并查看内部和外部链接之间的区别。在*第 1 章*、*剖析可移植 C++ 软件*中，我们看到了工具链是如何编译源文件并生成目标文件的，以及链接器是如何将它们组合在一起形成可执行程序的。\n\n当编译器处理源文件时，它会生成一个目标文件，该文件包含已翻译的 C++ 代码和足够的信息，以便链接器解析从已编译的源文件到另一个源文件的任何引用。在*第 1 章*、*解析可移植 C++ 软件*、 **CxxTemplate.cpp** 中称为`sum()`，在 **SumFunc.cpp** 文件中定义。当编译器构造一个目标文件时，它会创建以下段:\n\n*   **代码段**(也称文本):这是将 C++ 函数翻译成目标机器指令。\n*   **数据段**:包含程序中声明的所有变量和数据结构，不是本地的，也不是从堆或栈中分配的，并且是初始化的。\n*   **BSS 段**:包含程序中声明的所有变量和数据结构，不是本地的，也不是从堆或栈中分配的，并且没有初始化(但是将被初始化为零)。\n*   **导出符号数据库**:该对象文件中变量和函数的列表及其位置。\n*   **Database of referenced symbols**: A list of variables and functions this object file needs from outside itself and where they are used.\n\n    #### 注意\n\n    BSS 用于命名未初始化的数据段，其名称历史上来源于以符号开始的块。\n\n然后，链接器将所有代码段、数据段和 **BSS** 段收集在一起，形成程序。它使用两个数据库(DB)中的信息将所有引用的符号解析到导出的符号列表中，并用该信息修补代码段，以便它们可以正确运行。从图形上看，这描述如下:\n\n![Figure 2B.10: Parts of the object files and the executable file ](img/C14583_02B_10.jpg)\n\n###### 图 2B.10:部分目标文件和可执行文件\n\n出于以下讨论的目的，基站和数据段将简称为数据段(唯一的区别是基站未初始化)。当一个程序被执行时，它被加载到内存中，并且它的内存看起来有点像可执行文件的布局——它包含文本段、数据段、BSS 段和主机系统分配的空闲内存，后者包含所谓的**栈**和**堆**。堆栈通常从内存顶部开始向下增长，而堆从 BSS 结束的地方开始向上增长，朝向堆栈:\n\n![Figure 2B.11: CxxTemplate runtime memory map ](img/C14583_02B_11.jpg)\n\n###### 图 2B.11: CxxTemplate 运行时内存映射\n\n程序中可访问变量或标识符的部分称为**范围**。有两大类范围:\n\n*   **本地范围**(也称为**块范围**):这适用于用花括号(`{}`)括起来的块内声明的任何内容。变量可以在大括号内访问。就像块可以嵌套一样，变量的范围也可以嵌套。这通常包括局部变量和函数参数，它们通常存储在堆栈中。\n*   **全局/文件范围**:这适用于在正常函数或类之外声明的变量，也适用于正常函数。如果链接正确，变量可以在文件的任何地方访问，也可以从其他文件(全局)访问。这些变量由数据段中的链接器分配内存。标识符被放入全局命名空间，这是默认命名空间。\n\n### 命名空间\n\n我们可以把命名空间看作是变量、函数和用户定义类型的字典。对于小程序，使用全局命名空间是可以的，因为创建多个同名变量并产生名称冲突的可能性很小。随着程序变得越来越大，包括了更多的第三方库，名字冲突的机会增加了。因此，库作者将把他们的代码放入一个命名空间(希望是唯一的)。这允许程序员控制对命名空间中标识符的访问。通过使用标准库，我们已经使用了 std 命名空间。命名空间是这样声明的:\n\n```cpp\nnamespace name_of_namespace {  // put declarations in here }\n```\n\n命名空间的名称通常很短，命名空间可以嵌套。\n\n#### 注意\n\n在这里的 boost 库中可以看到名称空间的良好使用:[https://www.boost.org/](https://www.boost.org/)。\n\n变量还有另一个属性，即**寿命**。有三种基本寿命；两个由编译器管理，一个由程序员选择:\n\n*   **自动生存期**:局部变量在声明时创建，并在退出其所在的范围时销毁。这些由堆栈管理。\n*   **永久寿命**:全局变量和静态局部变量。编译器在程序开始时(进入 main()函数之前)创建全局变量，并在首次访问静态局部变量时创建静态局部变量。在这两种情况下，当程序退出时，变量都会被销毁。这些变量由链接器放在数据段中。\n*   **动态寿命**:变量是根据程序员的请求创建和销毁的(通过使用`新增`和`删除`)。这些变量从堆中分配内存。\n\n我们将考虑的变量的最后一个属性是**联动**。链接表示如果编译器和链接器遇到具有相同名称(或标识符)的变量和函数，它们会做什么。对于一个函数来说，它实际上就是所谓的变形名——编译器使用函数的名称、返回类型和参数类型来产生一个变形名。有三种类型的链接:\n\n*   **无链接**:这意味着标识符只引用自身，适用于局部变量和局部定义的用户类型(即块内部)。\n*   **内部链接**:这意味着标识符可以在声明它的文件中的任何地方被访问。这适用于静态全局变量、常量全局变量、静态函数以及文件中匿名命名空间中声明的任何变量或函数。匿名命名空间是没有指定名称的命名空间。\n*   **外部链接**:这意味着通过右向声明，可以从所有文件内部访问。这包括普通函数、非静态全局变量、外部常量全局变量和用户定义的类型。\n\n虽然这些被称为连接，只有最后一个实际上涉及连接。另外两个是通过编译器从导出标识符的数据库中排除信息来实现的。\n\n## 模板–通用编程\n\n作为一名计算机科学家，或者作为一名编程爱好者，在某个时间点，你可能不得不编写一个(或多个)排序算法。在讨论算法时，您并不特别关心正在排序的数据类型，只是该类型的两个对象可以进行比较，并且该域是一个完全有序的集合(也就是说，如果一个对象与任何其他对象进行比较，您可以确定哪个先出现)。不同的编程语言对此问题提供了不同的解决方案:\n\n*   **Python** :内置函数排序、列表上成员函数的动态语言。作为一种动态语言，如果能够调用比较运算符和`交换`函数，就不需要关心类型。\n*   **C**: This has a function in its standard library called qsort that has the following signature:\n\n    ```cpp\n    void qsort (void* base, size_t num, size_t size,                           int (*compare)(const void*,const void*));\n    ```\n\n    这处理不同的类型，因为基础是一个`空指针`。`size_t` size 定义每个对象的大小，而`compare()`函数定义如何比较两个对象。\n\n*   **C++**: `std::sort()` is a function provided in its standard library, where one of its signatures is as follows:\n\n    ```cpp\n    template< class RandomIt > void sort( RandomIt first, RandomIt last );\n    ```\n\n    在这种情况下，类型的细节在称为`随机化`的迭代器类型中捕获，并在编译时传递给方法。\n\n在下一节中，我们将简要定义泛型编程，展示 C++ 如何通过模板实现它们，突出显示该语言已经提供了什么，并讨论编译器如何推导类型，以便它们可以用于模板。\n\n### 什么是泛型编程？\n\n当您开发排序算法时，您可能最初只关注对普通数字进行排序。但是一旦建立了这种关系，您就可以将它抽象为任何类型，只要该类型表现出某些属性，例如总有序集(也就是说，比较运算符\n\n**泛型编程**是一种类型不可知的通用算法的开发。通过将类型作为参数传递，可以重用该算法。这样，算法被抽象，并允许编译器基于类型进行优化。\n\n换句话说，泛型编程是一种编程方法，在这种方法中，算法是用类型定义的，而类型是在算法实例化时指定的参数。许多语言支持不同名称的泛型编程。在 C++ 中，泛型编程通过称为模板的语言特性得到支持。\n\n### 介绍 C++ 模板\n\n模板是 C++ 对泛型编程的支持。把一个模板想象成一个饼干切割器，我们给它的类型作为一个参数，比如饼干面团(可以是巧克力布朗尼，姜片，或者其他美味的味道)。当我们应用 cookie cutter 时，我们最终会得到形式相同但口味不同的 cookie 实例。因此，模板捕获泛型函数或类的定义，当用类型作为参数指定时，编译器开始为我们编写类或函数，就好像类型是由我们手工编码的一样。它有几个优点，例如:\n\n*   您只需要开发一次类或算法并对其进行进化。\n*   您可以将其应用于许多类型。\n*   您可以将复杂的细节隐藏在简单的接口后面，编译器可以根据类型对生成的代码进行优化。\n\n那么，我们如何编写模板呢？让我们从一个模板开始，该模板允许我们在从`lo`到`hi`的范围内夹紧一个值，并且能够在`int`、`float`、`double`或任何其他内置类型上使用该值:\n\n```cpp\ntemplate <class T>\nT clamp(T val, T lo, T hi)\n{\n  return (val < lo) ? lo : (hi < val) ? hi : val;\n}\n```\n\n让我们把它分解一下:\n\n*   **第 1 行** : `模板<类 T >`声明后面的内容为模板，使用一种类型，模板中有一个`T`的占位符。\n*   **第 2 行**:当`T`被替换时，声明该功能的原型。它声明函数 clamp 接受三个类型为`T`的参数，并返回一个类型为`T`的值。\n*   **第 4 行**:这就是模板的妙处——假设传入的类型有一个`<`操作符，那么我们就可以对这三个值进行钳制，这样`lo < = val < = hi`。该算法对所有可排序的类型都有效。\n\n假设我们在下面的程序中使用它:\n\n```cpp\n#include <iostream>\nint main()\n{\n    std::cout << clamp(5, 3, 10) << \"\\n\";\n    std::cout << clamp(3, 5, 10) << \"\\n\";\n    std::cout << clamp(13, 3, 10) << \"\\n\";\n    std::cout << clamp(13.0, 3.0, 10.1) << \"\\n\";\n    std::cout << clamp<double>(13.0, 3, 10.2) << \"\\n\";\n    return 0;\n}\n```\n\n我们将获得以下预期输出:\n\n![Figure 2B.12: Clamp program output ](img/C14583_02B_12.jpg)\n\n###### 图 2B.12:箝位程序输出\n\n在最后一次调用夹钳时，我们已经在`<`和`>`之间传递了模板的双重类型。但是其他四个电话我们没有遵循同样的规则。为什么呢？事实证明，随着年龄的增长，编译器变得越来越聪明。随着标准的每一次发布，他们改进了所谓的**式演绎**。因为编译器能够推导出类型，所以我们不需要告诉它使用什么类型。这样做的原因是，没有模板参数的类的三个参数具有相同的类型——前三个都是 int，而第四个是 double。但是我们必须告诉编译器最后一个使用哪种类型，因为它有两个 doubles 和一个 int 作为参数，这导致了一个编译错误，说没有找到函数。但是后来，它给了我们为什么模板不能被使用的信息。这种强制类型的形式被称为**显式模板参数规范**。\n\n### C++ 预打包模板\n\nC++ 标准由两个主要部分组成:\n\n*   语言定义，即关键词、句法、词汇定义、结构等。\n*   标准库，即编译器供应商提供的所有预写的通用函数和类。这个库的一个子集是使用模板实现的，被称为**标准模板库** ( **STL** )。\n\nSTL 起源于大卫·穆塞和亚历山大·斯捷潘诺夫开发的 Ada 语言中提供的泛型。斯捷潘诺夫大力提倡使用通用编程作为软件开发的基础。在 90 年代，他看到了用新语言 C++ 来影响主流开发的机会，并向 ISO C++ 委员会提议将 STL 作为语言的一部分。剩下的就是历史。\n\nSTL 由四类预定义的通用算法和类组成:\n\n*   **容器**:一般序列(向量、列表、德格)和关联容器(集合、多集合、映射)\n*   **迭代器**:一组遍历容器并定义容器范围的类(范围表示为`begin()`和`end()`)。请注意，STL 中的一个基本设计选择是`end()`指向最后一项之后的一个位置–数学上，即[ `begin()`，`end()`)。\n*   **算法**:超过 100 种不同的算法，涵盖排序、搜索、集合运算等。\n*   **函数**:支持函子(可以像函数一样调用对象的函数对象)。一个用途是模板算法中的谓词，如`find_if()`。\n\n我们之前实现的箝位函数模板过于简单，虽然它适用于任何支持小于运算符的类型，但效率不是很高——如果类型很大，可能会产生非常大的副本。从 C++ 17 开始，STL 包含了一个`std::clamp()`函数，声明如下:\n\n```cpp\n#include <cassert>\ntemplate<class T, class Compare>\nconst T& clamp( const T& v, const T& lo, const T& hi, Compare comp )\n{\n    return assert( !comp(hi, lo) ),\n        comp(v, lo) ? lo : comp(hi, v) ? hi : v;\n}\ntemplate<class T>\nconst T& clamp( const T& v, const T& lo, const T& hi )\n{\n    return clamp( v, lo, hi, std::less<>() );\n}\n```\n\n正如我们所看到的，它使用参数和返回值的引用。将参数更改为使用引用减少了堆栈上必须传递和返回的内容。此外，请注意，设计人员已经制作了一个更通用的模板版本，这样我们就不会依赖于该类型存在的\n\n从前面的例子中，我们已经看到，像函数一样，模板可以采用多个逗号分隔的参数。\n\n## 类型别名–类型定义和使用\n\n如果你使用了`std::string`类，那么你就使用了一个别名。有几个与字符串相关的模板类需要实现相同的功能。但是代表一个角色的类型是不同的。例如对于`std::string`，表示为`char`，而`std::wstring`则使用`wchar_t`。`char16_t`和`char32_t`还有其他几个。功能的任何变化都将通过特性或模板专门化来管理。\n\n在 C++ 11 之前，这可能是从`std::basic_string`基类别名而来的，如下所示:\n\n```cpp\nnamespace std {\n  typedef basic_string<char> string;\n}\n```\n\n这有两个主要作用:\n\n*   减少声明变量所需的键入量。这是一个简单的情况，但是当您声明一个指向字符串到对象的映射的唯一指针时，它会变得很长，并且您会出错:\n\n    ```cpp\n    typedef std::unique_ptr<std::map<std::string,myClass>> UptrMapStrToClass;\n    ```\n\n*   提高可读性，因为您现在在概念上将它视为一个字符串，不需要担心细节。\n\n但是 C++ 11 引入了一个更好的方法——别名声明,它使用关键字来使用**。前面的代码可以这样实现:**\n\n```cpp\nnamespace std {\n  using string = basic_string<char>;\n}\n```\n\n前面的例子很简单，别名，无论是 typedef 还是 using，都不难理解。但是当别名涉及更复杂的表达式时，它们也可能有点不可读——尤其是函数指针。考虑以下代码:\n\n```cpp\ntypedef int (*FunctionPointer)(const std::string&, const Point&); \n```\n\n现在，考虑以下代码:\n\n```cpp\nusing FunctionPointer = int (*)(const std::string&, const Point&);\n```\n\nC++ 11 中的新特性是有原因的，其中别名声明可以很容易地合并到模板中——它们可以被模板化。一个`typedef`不能被模板化，虽然可以用`typedef`获得相同的结果，但是别名声明(`使用`)是首选方法，因为它导致模板代码更简单和更容易理解。\n\n### 练习 2:实现别名\n\n在本练习中，我们将使用 typedef 实现别名，并了解代码如何通过使用引用变得更容易阅读和更高效。按照以下步骤实施本练习:\n\n1.  在 Eclipse 中打开**第 2B 课**项目，然后在项目浏览器中展开**第 2B 课**，然后展开**练习 02** ，双击**练习 2.cpp** 在编辑器中打开本练习的文件。\n2.  点击**启动配置**下拉菜单，选择**新启动配置……**。将 **L2BExercise2** 配置为以**练习 2** 的名称运行。完成后，它将是当前选定的启动配置。\n3.  Click on the **Run** button. **Exercise 2** will run and produce something similar to the following output:\n\n    ![Figure 2B.13: Exercise 2 output ](img/C14583_02B_13.jpg)\n\n    ###### 图 2B.13:练习 2 输出\n\n4.  在编辑器中，在声明`打印矢量()`函数之前，添加以下行:\n\n    ```cpp\n    typedef std::vector<int> IntVector;\n    ```\n\n5.  现在，用`IntVector`更改文件中所有出现的`std::vector < int >`。\n6.  点击**运行**按钮。输出应该和以前一样。\n7.  在编辑器中，将之前添加的行更改为:\n\n    ```cpp\n    using IntVector = std::vector<int>;\n    ```\n\n8.  点击**运行**按钮。输出应该和以前一样。\n9.  在编辑器中，添加以下行:\n\n    ```cpp\n    using IntVectorIter = std::vector<int>::iterator;\n    ```\n\n10.  现在，将`IntVector::iterator`的一次出现更改为`int vector。`\n11.  点击**运行**按钮。输出应该和以前一样。\n\n在本练习中，typedef 和使用 alias 之间似乎没有什么区别。在这两种情况下，使用一个好名字的别名使代码更容易阅读和理解。当涉及到更复杂的别名时，`使用`会产生一种更简单的别名书写方式。在 C++ 11 中引入，`使用`现在是定义别名的首选方法。它比`typedef`还有其他优势，比如可以在模板里面使用。\n\n## 模板–不仅仅是通用编程\n\n模板也可以提供比一般编程更多的东西。在泛型编程的情况下，模板作为不能更改的蓝图运行，并为指定的一个或多个类型提供模板的编译版本。\n\n可以根据所涉及的类型编写模板来提供函数或算法的专门化。这被称为**模板专门化**，从我们之前使用的意义上来说，它不是泛型编程。只有当它使某些类型在给定的上下文中按照我们期望的那样运行时，它才能被称为泛型编程。当用于所有类型的算法被修改时，它不能被称为泛型编程。\n\n检查以下专门化代码示例:\n\n```cpp\n#include <iostream>\n#include <type_traits>\ntemplate <typename T, std::enable_if_t<sizeof(T) == 1, int> = 0>\nvoid print(T val)\n{\n    printf(\"%c\\n\", val);\n}\ntemplate <typename T, std::enable_if_t<sizeof(T) == sizeof(int), int> = 0>\nvoid print(T val)\n{\n    printf(\"%d\\n\", val);\n}\ntemplate <typename T, std::enable_if_t<sizeof(T) == sizeof(double), int> = 0>\nvoid print(T val)\n{\n    printf(\"%f\\n\", val);\n}\nint main(int argc, char** argv)\n{\n    print('c');\n    print(55);\n    print(32.1F);\n    print(77.3);\n}\n```\n\n它定义了一个使用不同格式字符串调用`printf()`的模板，基于使用`std::enable_if_t < >`和`sizeof()`的模板的专门化。当我们运行它时，会生成以下输出:\n\n![Figure 2B.14: Erroneous print template program output ](img/C14583_02B_14.jpg)\n\n###### 图 2B.14:错误的打印模板程序输出\n\n### 替代失败不是错误–SFINAE\n\n为`32.1F`(`-1073741824`)打印的数值与该数字没有任何相似之处。如果我们检查编译器为以下程序生成的代码，我们会发现它已经生成了代码，就像我们编写了以下内容(以及更多内容)一样:\n\n```cpp\ntemplate<typename int, int=0>\nvoid print<int,0>(int val)\n{\n    printf(\"%d\\n\",val);\n}\ntemplate<typename float, int=0>\nvoid print<float,0>(float val)\n{\n    printf(\"%d\\n\", val);\n}\n```\n\n它为什么会生成这个代码？前面的模板使用了 C++ 编译器的一个名为 **S** 的特性来代替 **F** 故障 **I** s **N** ot **A** n 错误，或 **SFINAE** 。基本上，在模板的替换阶段，基于类型，如果编译器不能形成有效的代码，那么它只是丢弃定义并继续，而不是产生错误。让我们尝试修复前面的代码，并获得正确的打印结果。为此，我们将介绍`STD::enable _ if _ t<>`的用法，并访问所谓的**类型特征**来帮助我们。首先，我们将使用以下代码替换最后一个模板:\n\n```cpp\n#include <type_traits>\ntemplate <typename T, std::enable_if_t<std::is_floating_point_v<T>, int> = 0>\nvoid print(T val)\n{\n    printf(\"%f\\n\", val);\n}\n```\n\n这需要一些解释。首先我们考虑`std::enable_if_t`的定义，其实是一个类型别名:\n\n```cpp\ntemplate<bool B, class T = void>\nstruct enable_if {};\ntemplate<class T>\nstruct enable_if<true, T> { typedef T type; };\ntemplate< bool B, class T = void >\nusing enable_if_t = typename enable_if<B,T>::type;\n```\n\n`enable_if`的第一个模板将导致空结构(或类)的定义。`enable_if`的第二个模板是 true 的特化，作为第一个模板参数，它将产生一个具有 typedef 定义的类。`enable_if_t`的定义是一个助手模板，它免去了我们在使用时输入`:在模板末尾键入`的需要。那么，这是如何工作的呢？考虑以下代码:\n\n```cpp\ntemplate <typename T, std::enable_if_t<condition, int> = 0>\nvoid print(T val) { … }\n```\n\n如果在编译时评估的条件导致**为真**，则`enable_if_t`模板将导致如下模板:\n\n```cpp\ntemplate <typename T, int = 0>\nvoid print(T val) { … }\n```\n\n这是有效的语法，该函数作为候选函数添加到符号表中。如果在编译时计算的条件导致**为假**，那么`enable_if_t`模板将生成如下所示的模板:\n\n```cpp\ntemplate <typename T, = 0>\nvoid print(T val) { … }\n```\n\n这是**格式错误的代码**，现在被丢弃了——SFINAE 在工作。\n\n`STD::is _ floating _ point _ v<T>`是另一个访问`STD::is _ floating _ point<T>`模板的`:值`成员的辅助类。它的名字说明了一切——如果 T 是浮点类型(float、double、long double)，那就是真的；否则，它将是假的。如果我们进行此更改，编译器(GCC)将生成以下错误:\n\n![Figure 2B.15: Compiler error for the modified print template program ](img/C14583_02B_15.jpg)\n\n###### 图 2B.15:修改后的打印模板程序的编译器错误\n\n现在的问题是，当类型是 float 时，我们有两个模板可以满足:\n\n```cpp\ntemplate <typename T, std::enable_if_t<sizeof(T) == sizeof(int), int> = 0>\nvoid print(T val)\n{\n    printf(\"%d\\n\", val);\n}\ntemplate <typename T, std::enable_if_t<std::is_floating_point_v<T>, int> = 0>\nvoid print(T val)\n{\n    printf(\"%f\\n\", val);\n}\n```\n\n原来(通常)`sizeof(float)= = sizeof(int)`，所以我们需要再做一个改动。我们将第一个条件替换为另一个类型特征–`STD::is _ integral _ v<>:`\n\n```cpp\ntemplate <typename T, std::enable_if_t<std::is_integral_v<T>, int> = 0>\nvoid print(T val)\n{\n    printf(\"%d\\n\", val);\n}\n```\n\n如果我们进行此更改，编译器(GCC)将生成以下错误:\n\n![Figure 2B.16: Second compiler error for the modified print template program ](img/C14583_02B_16.jpg)\n\n###### 图 2B.16:修改后的打印模板程序的第二个编译器错误\n\n我们修复了浮点模糊性，但是这里的问题是 **std::is_integral_v(char)** 返回 true，同样有两个函数是由具有相同原型的 char 类型的模板生成的。事实证明，传递给**的条件遵循标准的 C++ 逻辑表达式。因此，为了解决这个问题，我们将添加一个排除字符的额外条件:**\n\n```cpp\ntemplate <typename T, std::enable_if_t<std::is_integral_v<T> && sizeof(T) != 1, int> = 0>\nvoid print(T val)\n{\n    printf(\"%d\\n\", val);\n}\n```\n\n如果我们现在编译程序，它会完成编译并链接程序。如果我们运行它，它现在会产生以下(预期的)输出:\n\n![Figure 2B.17: Corrected print template program output ](img/C14583_02B_17.jpg)\n\n###### 图 2B.17:修正的打印模板程序输出\n\n### 浮点表示\n\n那`32.099998`不应该是`32.1`吗？这就是传递给函数的内容。在计算机上执行浮点运算的问题是表示会自动引入错误。实数形成一个连续(无限)的域。如果你考虑实数域中的数字 1 和 2，那么它们之间有无限多的实数。不幸的是，计算机对浮点数的表示量化了这些值，并且不能表示所有的无限数量的数字。用于存储数字的位数越大，该值在实数域上的表示就越好。所以，长双优于双优于浮。关于什么适合存储数据，这实际上取决于您的问题领域。回到`32.099998`。计算机将单精度数字存储为 2 的幂之和，然后将它们移动一个幂因子。整数通常很容易，因为它们很容易用`2^n`次幂之和(n > =0)来表示。分数部分，在这种情况下是 0.1，必须表示为`2^(-n(n>0)`的和。我们增加更多的 2 次方分数，试图使数字更接近目标值，直到我们用完单个精确浮点数的 24 位精度。\n\n#### 注意\n\n如果你想知道更多关于计算机如何存储浮点数的知识，研究一下定义浮点数的 IEEE 754 标准。\n\n### 常量表达式 if 表达式\n\nC++ 17 在语言中引入了`constexpr if`表达式，大大简化了模板编写。我们可以重写前面三个使用 SFINAE 作为一个更简单模板的模板:\n\n```cpp\n#include <iostream>\n#include <type_traits>\ntemplate <typename T>\nvoid print(T val)\n{\n   if constexpr(sizeof(T)==1) {\n      printf(\"%c\",val);\n   }\n   else if constexpr(std::is_integral_v<T>) {\n      printf(\"%d\",val);\n   }\n   else if constexpr(std::is_floating_point_v<T>) {\n      printf(\"%f\",val);\n   }\n   printf(\"\\n\");\n}\nint main(int argc, char** argv)\n{\n    print('c');\n    print(55);\n    print(32.1F);\n    print(77.3);\n}\n```\n\n对于`打印(55)`的调用，编译器生成如下函数进行调用:\n\n```cpp\ntemplate<>\nvoid print<int>(int val)\n{\n    printf(\"%d\",val);\n    printf(\"\\n\");\n}\n```\n\nif/else if 语句怎么了？如果表达式为**常量表达式，编译器会根据上下文确定条件的值，并将其转换为布尔值(真/假)。如果计算值为真，则 If 条件和 else 子句被丢弃，只留下 true 子句来生成代码。同样，如果它是 false，那么 false 子句将被留下来生成代码。换句话说，只有计算结果为 true 的第一个 constexpr if 条件将生成其子句的代码，而其余的将被丢弃。**\n\n### 非类型模板参数\n\n到目前为止，我们只看到了属于类型的模板参数。也可以传递整数值作为模板参数。这允许我们防止函数的数组衰减。例如，考虑一个计算`和`的模板函数:\n\n```cpp\ntemplate <class T>\nT sum(T data[], int number)\n{\n  T total = 0;\n  for(auto i=0U ; i<number ; i++)\n  {\n    total += data[i];\n  }\n  return total;\n}\n```\n\n在这种情况下，我们需要在调用中传递数组的长度:\n\n```cpp\nfloat data[5] = {1.1, 2.2, 3.3, 4.4, 5.5};\nauto total = sum(data, 5);\n```\n\n但是如果我们能叫下面这个不是更好吗？\n\n```cpp\nauto total = sum(data);\n```\n\n我们可以通过更改模板来实现，如下面的代码所示:\n\n```cpp\ntemplate <class T, std::size_t size>\nT sum(T (&data)[size])\n{\n  T total = 0;\n  for(auto i=0U ; i< size; i++)\n  {\n    total += data[i];\n  }\n  return total;\n}\n```\n\n在这里，我们将数据更改为对某个特定大小的数组的引用，该大小被传递给模板，因此编译器会计算出来。我们不再需要函数调用的第二个参数。这个简单的例子展示了如何直接传递和使用非类型参数。我们将在*模板类型演绎*部分对此进行更多探讨。\n\n### 练习 3:实现 Stringify–专门化与常量表达式\n\n在本练习中，我们将通过使用 constexpr 来实现一个字符串模板，以生成一个更容易阅读和更简单的代码版本。按照以下步骤实施本练习:\n\n#### 注意\n\n字符串化的专门化模板可以在[https://isocpp . org/wiki/FAQ/templates # templates-专门化-示例](https://isocpp.org/wiki/faq/templates#template-specialization-example)中找到。\n\n1.  在 Eclipse 中打开**第 2B 课**项目，然后在**项目浏览器**中，展开**第 2B 课**，然后展开**练习 03** ，双击**练习 3.cpp** 在编辑器中打开本练习的文件。\n2.  点击**启动配置**下拉菜单，选择**新启动配置……**。将**2 练习 3** 配置为使用名称**练习 3** 运行。\n3.  Click on the **Run** button. **Exercise 3** will run and produce the following output:\n\n    ![Figure 2B.18: Exercise 3 specialized template output ](img/C14583_02B_18.jpg)\n\n    ###### 图 2B.18:练习 3 专用模板输出\n\n4.  在**练习 3.cpp** 中，注释掉字符串模板的所有模板专门化，同时保留原始的通用模板。\n5.  Click on the **Run** button. The output will change to have the boolean printed as a number and the double printed to only two decimal places:\n\n    ![Figure 2B.19: Exercise 3 general template only output ](img/C14583_02B_19.jpg)\n\n    ###### 图 2B.19:练习 3 仅输出通用模板\n\n6.  我们现在将再次“专门化”布尔类型的模板。将`#包括<类型 _ 特征>`指令与其他`#包括`指令相加，并修改模板，使其内容如下:\n\n    ```cpp\n    template<typename T> std::string stringify(const T& x)\n    {\n      std::ostringstream out;\n      if constexpr (std::is_same_v<T, bool>)\n      {\n          out << std::boolalpha;\n      }\n      out << x;\n      return out.str();\n    }\n    ```\n\n7.  Click on the **Run** button. The output boolean stringify works as before:\n\n    ![Figure 2B.20: stringify tailored for boolean ](img/C14583_02B_20.jpg)\n\n    ###### 图 2B.20:为布尔函数定制的字符串\n\n8.  我们现在将再次“专门化”浮点类型的模板(`float`、`double`、`long double`)。修改模板，使其如下所示:\n\n    ```cpp\n    template<typename T> std::string stringify(const T& x)\n    {\n      std::ostringstream out;\n      if constexpr (std::is_same_v<T, bool>)\n      {\n          out << std::boolalpha;\n      }\n      else if constexpr (std::is_floating_point_v<T>)\n      {\n          const int sigdigits = std::numeric_limits<T>::digits10;\n          out << std::setprecision(sigdigits);\n      }\n      out << x;\n      return out.str();\n    }\n    ```\n\n9.  Click on the **Run** button. The output is restored to the original:\n\n    ![Figure 2B.21: constexpr if version template output ](img/C14583_02B_21.jpg)\n\n    ###### 图 2B.21: constexpr if 版本模板输出\n\n10.  如果将有多个模板的原始版本与最终版本进行比较，你会发现最终版本更像是一个正常的功能，更容易阅读和维护。\n\n在练习中，我们了解了在 C++ 17 中使用新的 constexpr if 构造时，我们的模板可以变得多么简单和紧凑。\n\n### 函数重载再探\n\n当我们第一次讨论函数重载时，我们只考虑了函数名来自手工编写的函数列表的场景。现在，我们需要更新这个。我们还可以编写具有相同名称的模板化函数。就像我们之前做的那样，当编译器遇到行`print(55)`时，它需要计算出要调用哪个先前定义的函数。因此，它执行以下过程(非常简单):\n\n![Figure 2B.22: Function overload resolution with templates (simplified) ](img/C14583_02B_22.jpg)\n\n###### 图 2B.22:使用模板的函数重载解析(简化)\n\n### 模板类型演绎\n\n当我们第一次引入模板时，我们触及了模板类型演绎。现在，我们将进一步探讨这个问题。我们将首先考虑函数模板的一般声明:\n\n```cpp\ntemplate<typename T>\nvoid function(ParamType parameter);\n```\n\n对此的呼吁可能是这样的:\n\n```cpp\nfunction(expression);              // deduce T and ParamType from expression\n```\n\n当编译器到达这一行时，它现在必须推导出与模板相关的两个类型–`T`和`ParamType`。由于参数类型中附加到 T 的限定符和其他属性(例如指针、引用、常量等)，它们通常是不同的。类型是相关的，但是演绎的进程是不同的，这取决于所使用的`表达的形式`。\n\n### 显示推导出的类型\n\n在我们研究不同的形式之前，如果我们能让编译器告诉我们它已经推导出的类型，这可能是有用的。这里我们有几个选项，包括显示类型的 IDE 编辑器、生成错误的编译器和运行时支持(由于 C++ 标准，这不一定有效)。我们将使用编译器错误来帮助我们探索一些类型推断。\n\n我们可以通过声明一个没有定义的模板来实现一个类型显示器。任何实例化模板的尝试都会导致编译器生成一条错误消息，因为没有定义以及它试图实例化的类型信息:\n\n```cpp\ntemplate<typename T>\nstruct TypeDisplay;\n```\n\n让我们尝试编译以下程序:\n\n```cpp\ntemplate<typename T>\nclass TypeDisplay;\nint main()\n{\n    signed int x = 1;\n    unsigned int y = 2;\n    TypeDisplay<decltype(x)> x_type;\n    TypeDisplay<decltype(y)> y_type;\n    TypeDisplay<decltype(x+y)> x_y_type;\n    return 0;\n}\n```\n\n编译器会抛出以下错误:\n\n![Figure 2B.23: Compiler errors showing deduced types ](img/C14583_02B_23.jpg)\n\n###### 图 2B.23:显示推断类型的编译器错误\n\n请注意，在每种情况下，被命名的聚合都包括被推导的类型——对于 x，它是一个 int，对于 y，它是一个无符号 int，对于 x+y，它是一个无符号 int。另外，请注意，TypeDisplay 模板需要一个类型作为其参数，因此使用`decltype()`函数让编译器为括号中的表达式提供类型。\n\n也可以使用内置的`类型标识(T)在运行时显示推导出的类型。name()`运算符，该运算符返回一个 std::string，或者使用名为 type_index 的 boost 库。\n\n#### 注意\n\n更多信息，请访问以下链接:[https://www . boost . org/doc/libs/1 _ 70 _ 0/doc/html/boost _ type index . html](https://www.boost.org/doc/libs/1_70_0/doc/html/boost_typeindex.html)。\n\n因为类型推演规则，内置运算符会给你一个类型的指示，但是会丢失引用(`&`、`& &`)和任何常量信息(常量或挥发)。如果在运行时需要，那么考虑`boost::type_index`，这将为所有编译器产生相同的输出。\n\n### 模板类型演绎-细节\n\n让我们回到通用模板:\n\n```cpp\ntemplate<typename T>\nvoid function(ParamType parameter);\n```\n\n假设电话是这样的:\n\n```cpp\nfunction(expression);             // deduce T and ParamType from expression\n```\n\n根据所用参数类型的形式，类型推导的进行方式不同:\n\n*   **ParamType 是一个值(T)** :按值传递函数调用\n*   **ParamType 是引用或指针(T &或 T*)** :通过引用传递函数调用\n*   **ParamType 是一个右值引用(T & & )** :通过引用传递函数调用或者别的什么\n\n**情况 1: ParamType 是传递值(T)**\n\n```cpp\ntemplate<typename T>\nvoid function(T parameter);\n```\n\n作为一个按值传递的调用，这意味着参数将是传入的任何内容的副本。因为这是对象的新实例，所以以下规则应用于表达式:\n\n*   如果表达式的类型是引用，则忽略引用部分。\n*   如果在步骤 1 之后，剩余的类型是常量和/或易失性的，那么也忽略它们。\n\n剩下的就是 t .让我们尝试编译以下文件代码:\n\n```cpp\ntemplate<typename T>\nclass TypeDisplay;\ntemplate<typename T>\nvoid function(T parameter)\n{\n    TypeDisplay<T> type;\n}\nvoid types()\n{\n    int x = 42;\n    function(x);\n}\n```\n\n编译器会产生以下错误:\n\n![Figure 2B.24: Compiler error showing a deduced type for the pass by type ](img/C14583_02B_24.jpg)\n\n###### 图 2B.24:编译器错误显示了按类型传递的推断类型\n\n所以，类型推导为`int`。同样，如果我们声明以下内容，我们会得到完全相同的错误:\n\n```cpp\nconst int x = 42;\nfunction(x);\n```\n\n如果我们声明这个版本，也会发生同样的情况:\n\n```cpp\nint x = 42;\nconst int& rx = x;\nfunction(rx);\n```\n\n根据前面所述的规则，在所有三种情况下，推导出的类型都是`int`。\n\n**情况 2: ParamType 是通过引用传递的(T & )**\n\n作为一个按引用传递的调用，这意味着参数将能够访问对象的原始存储位置。正因为如此，生成的函数必须尊重我们之前忽略的常量和可变性。以下规则适用于类型扣减:\n\n*   如果表达式的类型是引用，则忽略引用部分。\n*   模式将表达式类型的剩余部分与参数类型进行匹配，以确定 t\n\n让我们尝试编译以下文件:\n\n```cpp\ntemplate<typename T>\nclass TypeDisplay;\ntemplate<typename T>\nvoid function(T& parameter)\n{\n    TypeDisplay<T> type;\n}\nvoid types()\n{\n    int x = 42;\n    function(x);\n}\n```\n\n编译器将生成以下错误:\n\n![Figure 2B.25: Compiler error showing the deduced type for pass by reference ](img/C14583_02B_25.jpg)\n\n###### 图 2B.25:显示通过引用传递的推导类型的编译器错误\n\n由此我们可以看出，编译器把 T 作为 **int** 从 ParamType 作为**int&T3。将 x 更改为常量 int 并不意外，因为从 ParamType 推导出 T 是**常量 int** 为**常量 int &** :**\n\n![Figure 2B.26:  Compiler error showing the deduced type for pass by const reference ](img/C14583_02B_26.jpg)\n\n###### 图 2B.26:编译器错误显示了常量引用传递的推导类型\n\n同样，像以前一样，引入 rx 作为常量 int 的引用，也不会让人感到意外，因为从 ParamType 推导出 T 是`常量 int`作为`常量 int &`:\n\n```cpp\nvoid types()\n{\n    const int x = 42;\n    const int& rx = x;\n    function(rx);\n}\n```\n\n![Figure 2B.27: Compiler error showing the deduced type when passing a const reference ](img/C14583_02B_27.jpg)\n\n###### 图 2B.27:在传递常量引用时显示推导类型的编译器错误\n\n如果我们将声明更改为包含一个常量，那么编译器将在从模板生成函数时遵守该常量:\n\n```cpp\ntemplate<typename T>\nvoid function(const T& parameter)\n{\n    TypeDisplay<T> type;\n}\n```\n\n这一次，编译器会报告以下内容\n\n*   `int x` : T 是 int(因为常量会被尊重)，而参数的类型是`const int &`。\n*   `const int x` : T 是 int (const 在模式中，保留 int)，而参数的类型是`const int &`。\n*   `const int & rx` : T 是 int(引用被忽略，const 在模式中，留下 int)，而参数的类型是`const int &`。\n\n如果我们试图编译以下内容，我们期望什么？通常，数组衰减为指针:\n\n```cpp\nint ary[15];\nfunction(ary);\n```\n\n编译器错误如下:\n\n![Figure 2B.28:  Compiler error showing the deduced type for the array argument  when passed by reference ](img/C14583_02B_28.jpg)\n\n###### 图 2B.28:编译器错误，显示了通过引用传递数组参数时推导出的类型\n\n这一次，数组被捕获作为参考，大小也被包括在内。所以，如果 ary 被声明为`ary【10】`，那么将会产生一个完全不同的函数。让我们将模板还原为以下内容:\n\n```cpp\ntemplate<typename T>\nvoid function(T parameter)\n{\n    TypeDisplay<T> type;\n}\n```\n\n如果我们试图编译数组调用，那么错误报告如下:\n\n![Figure 2B.29: Compiler error showing the deduced type for the array argument  when passed by value ](img/C14583_02B_29.jpg)\n\n###### 图 2B.29:通过值传递数组参数时显示推导出的类型的编译器错误\n\n我们可以看到，在这种情况下，当将数组传递给函数时，数组已经像通常的行为一样衰减了。我们在谈论*非类型模板参数*时看到了这种行为。\n\n**情况 3: ParamType 是右值引用(T & & )**\n\n& T 被称为右值引用，而& T 被称为左值引用。C++ 不仅通过类型来表征表达式，还通过名为**值类别**的属性来表征表达式。这些类别控制编译器中的表达式计算，包括创建、复制和移动临时对象的规则。C++ 17 标准中定义了五个表达式值类别，它们具有以下关系:\n\n![Figure 2B.30: C++ value categories ](img/C14583_02B_30.jpg)\n\n###### 图 2B.30: C++ 值类别\n\n每个的定义如下:\n\n*   确定对象身份的表达式是`glvalue`。\n*   一个表达式，其求值初始化一个对象或一个运算符的操作数是一个`prvalue`。示例包括文字(字符串文字除外)，如 3.1415、true 或 nullptr、this 指针、后置递增和后置递减表达式。\n*   有资源并且可以重用(因为它的生命即将结束)的 glvalue 对象是`xvalue`。例如，函数调用的返回类型是对对象的右值引用，如`标准::移动()`。\n*   不是 x 值的 GL 值是`左值`。示例包括变量名、函数名、数据成员名或字符串。\n*   prvalue 或 xvalue 是一个`值`。\n\n如果您对下面的解释不完全理解，也没关系——只要知道被认为是左值的表达式可以使用它的地址(使用运算符的地址，即“&”)。以下内容的类型推导规则要求您知道左值是什么，以及它不是什么:\n\n```cpp\ntemplate<typename T>\nvoid function(T&& parameter)\n{\n    TypeDisplay<T> type;\n}\n```\n\n此参数类型表单的类型推导规则如下:\n\n*   如果表达式是左值引用，那么 T 和 ParamType 都被推导为左值引用。这是类型被推断为引用的唯一场景。\n*   如果表达式是右值引用，则情况 2 的规则适用。\n\n### SFINAE 表达式和尾随返回类型\n\nC++ 11 引入了一个名为`尾随返回类型`的特性，为模板提供了一种机制，这样它们就可以概括返回类型。一个简单的例子如下:\n\n```cpp\ntemplate<class T>\nauto mul(T a, T b) -> decltype(a * b) \n{\n    return a * b;\n}\n```\n\n这里，`auto`用来表示定义了一个尾随返回类型。尾部返回类型以`- >`指针开始，在这种情况下，返回类型是通过将`a`和`b`相乘而返回的类型。编译器将处理 decltype 的内容，如果它的格式不正确，它将像往常一样从函数名的查找中删除定义。该功能提供了许多可能性，因为逗号运算符“`、`”可以在`decltype`中使用，以检查某些属性。\n\n如果我们想测试一个类实现了一个方法或者包含了一个类型，那么我们可以把它放在 decltype 里面，方法是把它转换成一个 void(以防逗号操作符被重载)，然后在逗号操作符的末尾定义一个真正返回类型的对象。下面的程序显示了一个这样的例子:\n\n```cpp\n#include <iostream>\n#include <algorithm>\n#include <utility>\n#include <vector>\n#include <set>\ntemplate<class C, class T>\nauto contains(const C& c, const T& x) \n             -> decltype((void)(std::declval<C>().find(std::declval<T>())), true)\n{\n    return end(c) != c.find(x);\n}\nint main(int argc, char**argv)\n{\n    std::cout << \"\\n\\n------ SFINAE Exercise ------\\n\";\n    std::set<int> mySet {1,2,3,4,5};\n    std::cout << std::boolalpha;\n    std::cout << \"Set contains 5: \" << contains(mySet,5) << \"\\n\";\n    std::cout << \"Set contains 15: \" << contains(mySet,15) << \"\\n\";\n    std::cout << \"Complete.\\n\";\n    return 0;\n}\n```\n\n当这个程序被编译和执行时，我们获得以下输出:\n\n![Figure 2B.31: Output from the SFINAE expression ](img/C14583_02B_31.jpg)\n\n###### 图 2 b . 31:SFINAE 表达式的输出\n\n返回类型由以下代码给出:\n\n```cpp\ndecltype( (void)(std::declval<C>().find(std::declval<T>())), true)\n```\n\n让我们把它分解一下:\n\n*   `decltype`的操作数是一个逗号分隔的表达式列表。这意味着编译器将构造但不计算表达式，并使用最右边值的类型来确定函数的返回类型。\n*   `std::declval < T > ()`允许我们将 T 类型转换为引用类型，然后我们可以使用它来访问成员函数，而无需实际构造对象。\n*   与所有基于 SFINAE 的操作一样，如果逗号分隔列表中的任何表达式无效，则该函数将被丢弃。如果它们都有效，则将其添加到函数列表中进行查找。\n*   强制转换为 void 是为了防止用户重载逗号运算符时可能出现的任何问题。\n*   基本上，这是在测试`C`类是否有一个名为`find()`的成员函数，该函数以`类 T`、`类 T &`或`const 类 T &`作为参数。\n\n此方法适用于`std::set`，它有一个`find()`方法，该方法接受一个参数，但对于其他容器将失败，因为它们没有`find()`成员方法。\n\n如果我们只处理一种类型，这种方法效果很好。但是如果我们有一个函数需要基于类型产生不同的实现，就像我们之前看到的那样，if constexpr 的方法要干净得多，通常也更容易理解。要使用`if constexpr`方法，我们需要在编译时生成评估为`true`或`false`的模板。标准库为此提供了助手类:`std::true_type`和`std::false_type`。这两个结构有一个静态常量成员名值，分别设置为`真`和`假`。使用 SFINAE 和模板重载，我们可以创建新的检测类，从这些类中的任何一个派生，以给出我们想要的结果:\n\n```cpp\ntemplate <class T, class A0>\nauto test_find(long) -> std::false_type;\ntemplate <class T, class A0>\nauto test_find(int) \n-> decltype(void(std::declval<T>().find(std::declval<A0>())), std::true_type{});\ntemplate <class T, class A0>\nstruct has_find : decltype(test_find<T,A0>(0)) {};\n```\n\n第一个模板`test_find`创建默认行为，将返回类型设置为`std::false_type`。注意这个有一个`长`的参数类型。\n\n第二个模板`test_find`创建了一个特化，用于测试一个类，该类有一个名为`find()`的成员函数，返回类型为`std::true_type`。请注意，这有一个参数类型`int`。\n\n**具有 _find < T，A0 >** 模板通过从 **test_find()** 函数的返回类型中派生自身来工作。如果 T 类没有 **find()** 方法，则只生成 **std::false_type** 版本的 **test_find()** ，因此**有 _find < T，A0>:value**值将为 false，如果 constexpr() 可以在**中使用。**\n\n有趣的部分发生在 T 类有`find()`方法的情况下，因为两个`test_find()`方法都是生成的。但是专用版本采用`int`类型的参数，而默认版本采用`long`类型的参数。当我们用零(0)来“调用”函数时，它将匹配专用版本并使用它。参数差异很重要，因为您不能让两个函数具有相同的参数类型，并且只有返回类型不同。如果要检查此行为，请将参数从 0 更改为 0L，以强制使用长版本。\n\n## 类模板\n\n到目前为止，我们只处理了函数模板。但是模板也可以用来为类提供蓝图。模板化类声明的一般结构如下:\n\n```cpp\ntemplate<class T>\nclass MyClass {\n   // variables and methods that use T.\n};\n```\n\n模板函数允许我们产生通用算法，而模板类允许我们产生通用数据类型及其相关行为。\n\n当我们介绍标准模板库时，我们强调它包括容器的模板–`向量`、`德格`、`堆栈`等等。这些模板允许我们存储和管理任何我们想要的数据类型，但是仍然按照我们期望的方式运行。\n\n### 练习 4:编写班级模板\n\n计算科学中最常用的两种数据结构是堆栈和队列。两者目前在 STL 中都有实现。但是为了熟悉模板类，我们将编写一个可以用于任何类型的堆栈模板类。让我们开始吧:\n\n1.  在 Eclipse 中打开**第 2B 课**项目，然后在**项目浏览器**中，展开**第 2B 课**，然后展开**练习 04** ，双击**练习 4.cpp** 在编辑器中打开本练习的文件。\n2.  配置一个新的**启动配置**、**l2be xerce 4**，运行名称为**练习 4** 。\n3.  另外，配置一个新的 C/C++ 单元运行配置 **L2BEx4Tests** ，以运行 **L2BEx4tests** 。设置**谷歌测试运行程序**。\n4.  Click on the **Run** option for the test, which we have to run for the first time:\n\n    ![Figure 2B.32: Initial unit test for stacks ](img/C14583_02B_32.jpg)\n\n    ###### 图 2B.32:堆栈的初始单元测试\n\n5.  Open **Stack.hpp** in the editor. You will find the following code:\n\n    ```cpp\n    #pragma once\n    #include <vector>\n    #include <cstddef>\n    #define EXERCISE4_STEP    1\n    namespace acpp\n    {\n    template <typename T>\n    class Stack\n    {\n    public:\n    private:\n        std::vector<T> m_stack;\n    };\n    } // namespace acpp\n    ```\n\n    模板定义首先要注意的是，它必须放在一个头文件中，这个头文件可以包含在我们需要重用它的地方。其次，我们使用了一个 pragma 指令(`#pragma 一次`)，它告诉编译器，如果它再次遇到这个要#included 的文件，就不需要了。虽然不是标准的严格组成部分，但几乎所有现代 C++ 编译器都支持它。最后，请注意，出于本练习的目的，我们选择将项目存储在 STL 向量中。\n\n6.  在编辑器中，在`堆栈`类的`公共`部分添加以下声明:\n\n    ```cpp\n    bool empty() const\n    {\n      return m_stack.empty();\n    }\n    ```\n\n7.  At the top of the file, change **EXERCISE4_STEP** to a value of **10**. Click on the **Run** button. The Exercise 4 tests should run and fail:\n\n    ![Figure 2B.33: Jumping to a Failing test ](img/C14583_02B_33.jpg)\n\n    ###### 图 2B.33:跳到失败的测试\n\n8.  点击失败测试的名称，即**defaultconstructionnitsempty**。它将在右侧的消息部分显示失败的原因。双击消息。它将打开测试失败的文件并跳转到违规的行，如前面的截图所示。这个测试有一个错误。在测试中，我们期望堆栈是空的。但是，我们可以看到`空()`的报道是假的。\n9.  将`断言 _ 假`更改为`断言 _ 真`并重新运行测试。这一次，它通过了，因为它在测试正确的事情。\n10.  接下来我们要做的是添加一些类型别名，以便在接下来的几个方法中使用。在编辑器中，在`空()`方法的正上方添加以下行:\n\n    ```cpp\n    using value_type = T;\n    using reference = value_type&;\n    using const_reference = const value_type&;\n    using size_type = std::size_t;\n    ```\n\n11.  点击**运行**按钮重新运行测试。他们应该通过。在做测试驱动开发时，口头禅是写一个小测试，看到它失败，然后写足够的代码让它通过。在这种情况下，我们实际测试了别名的定义是否正确，因为编译失败是测试失败的一种形式。我们现在准备添加推送功能。\n12.  在编辑器中，通过在**空()**方法的正下方添加以下代码来更改 **Stack.hpp**\n13.  在文件顶部，将`锻炼 4 _ 步骤`更改为`15`的值。点击**运行**按钮。我们现在有两个测试运行并通过。在 **StackTests.cpp** 中的新测试`pushontostknottempty`证明了该推送可以使堆栈不再为空。我们需要添加更多的方法来确保它已经完成了预期的工作。\n14.  在编辑器中，通过在`push()`方法的正下方添加以下代码来更改 **Stack.hpp** ，并将`execute 4 _ STEP`更改为`16`的值:\n\n    ```cpp\n    size_type size() const\n    {\n        return m_stack.size();\n    }\n    ```\n\n15.  点击**运行**按钮运行测试。现在应该有三个通过测试。\n16.  在编辑器中，通过在`push()`方法的正下方添加以下代码来更改 **Stack.hpp** ，并将`execute 4 _ STEP`更改为`18`的值:\n\n    ```cpp\n    void pop()\n    {\n        m_stack.pop_back();\n    }\n    ```\n\n17.  点击**运行**按钮运行测试。现在应该有四个通过测试。\n18.  在编辑器中，通过在`pop()`方法的正下方添加以下代码来更改 **Stack.hpp** ，并将`execute 4 _ STEP`更改为值`20` :\n\n    ```cpp\n    reference top()\n    {\n        m_stack.back();\n    }\n    const_reference top() const\n    {\n        m_stack.back();\n    }\n    ```\n\n19.  点击**运行**按钮运行测试。现在有五个通过测试，我们已经实现了一个堆栈。\n20.  从启动配置下拉菜单中，选择 **L2BExercise4** 并点击**运行**按钮。练习 4 将运行并生成类似于以下输出的内容:\n\n![Figure 2B.34: Exercise 4 output  ](img/C14583_02B_34.jpg)\n\n###### 图 2B.34:练习 4 输出\n\n检查现在在 **Stack.hpp** 文件中的代码。在类内部定义类型的方法在整个 STL 中很常见(尽管由于它们的传统，它们可能会使用 typedef)。`std::stack`模板接受两个参数，第二个参数定义要使用的容器——vector 可能是第一个。检查 **StackTests.cpp** 中的测试。测试应该被命名，以表明他们的目标是测试什么，他们应该专注于这样做。\n\n### 活动 1:开发通用“包含”模板函数\n\n编程语言 Python 有一个名为“in”的成员操作符，可以用于任何序列，即列表、序列、集合、字符串等。即使 C++ 有 100 多种算法，它也没有一种等效的方法来实现同样的功能。C++ 20 在`std::set`上引入了`contains()`方法，但这还不够。我们需要创建一个`contains()`模板函数，它与`std::set`、`std::string`、`std::vector`以及任何其他提供迭代器的容器一起工作。这是由在其上调用 end()的能力决定的。我们的目标是获得最佳性能，因此我们将在任何有成员方法的容器上调用`find()`成员方法(这将是最有效的)，否则我们将返回到在容器上使用`std::end()`。我们还需要区别对待`std::string()`，因为它的`find()`方法返回一个特殊值。\n\n我们可以使用一个通用模板和两个专门化来实现这一点，但是这个活动是使用 SFINAE 和 if constexpr 的技术来实现的。另外，这个模板必须只在支持`end(C)`的类上工作。按照以下步骤实施本活动:\n\n1.  从**第 2B 课/练习 01** 文件夹加载准备好的项目。\n2.  使用`npos`成员定义助手模板函数和类来检测标准:字符串大小写。\n3.  定义辅助模板函数和类，检测该类是否有`find()`方法。\n4.  定义 contains template 函数，该函数使用 constexpr 在三种实现中进行选择-字符串大小写、has find 方法或一般大小写。\n\n执行上述步骤后，预期输出应该如下所示:\n\n![Figure 2B.35: Output from the successful implementation of contains ](img/C14583_02B_35.jpg)\n\n###### 图 2B.35:成功实现 contains 的 suc 输出\n\n#### 注意\n\n这项活动的解决方案可以在第 653 页找到。\n\n## 总结\n\n在这一章中，我们学习了接口、继承和多态性，这些扩展了我们对类型的处理技巧。我们第一次尝试使用 C++ 模板进行泛型编程，并接触了该语言从 C++ 标准库中免费提供给我们的东西，其中包括 STL。我们探索了 C++ 的一个刚刚好用的特性，那就是模板类型推演，使用模板的时候让我们的生活变得更加轻松。然后，我们进一步学习了模板，并学习了如何使用 SFINAE 和 if constexpr 来控制编译器包含的模板部分。这些构成了我们进入 C++ 之旅的基石。在下一章中，我们将重新访问堆栈和堆，并了解什么是异常、发生了什么以及何时发生。我们还将学习如何在异常发生时保护我们的程序免受资源损失。"
  },
  {
    "path": "docs/adv-cpp/04.md",
    "content": "# 四、不允许泄漏——异常和资源\n\n## 学习目标\n\n本章结束时，您将能够:\n\n*   开发类来管理资源\n*   开发异常健壮的代码，这样资源就不会通过 RAII 泄漏\n*   实现可以通过移动语义转移资源所有权的类\n*   实现控制隐式转换的类\n\n在本章中，您将学习如何使用类来管理资源、防止泄漏以及防止复制大量数据。\n\n## 简介\n\n在*章 2A* 、*不允许鸭子-类型、演绎*中，我们简单的接触了一些概念，比如智能指针和移动语义。在本章中，我们将进一步探讨它们。事实证明，这些主题与资源管理和编写健壮的代码(能够经常长时间运行而没有问题的代码)密切相关。\n\n为了理解会发生什么，我们将探索变量在内存中的位置，以及当它们超出范围时会发生什么。\n\n我们将研究编译器为我们输入的内容生成什么样的汇编代码，并且我们将探索当异常发生时所有这些是如何受到影响的。\n\n### 可变范围和寿命\n\n在*章 2B* 、*不允许鸭子-模板和演绎*中，我们讨论了可变范围和生存期。让我们快速浏览一下它们的不同类型:\n\n**范围**:\n\n*   **本地范围**(也称为**块范围**):这适用于在花括号(`{}`)内的块内声明的任何内容。\n*   **全局/文件范围**:这适用于在正常函数或类之外声明的变量，也适用于正常函数。\n\n**寿命**:\n\n*   **自动生存期**:这里，局部变量在声明时创建，在退出所在范围时销毁。这些由堆栈管理。\n*   **永久寿命**:这里，全局和静态局部变量都有永久寿命。\n*   **动态寿命**:这里，变量是在程序员的要求下创建和销毁的(使用`新增的`和`删除操作符`)。这些变量从堆中分配内存。\n\n我们将使用以下程序来弄清楚`局部变量`的行为——那些具有`自动寿命`和那些具有`动态寿命`的变量:\n\n![](img/C14583_03_01.jpg)\n\n###### 图 3.1:可变范围和寿命的测试程序\n\n当我们运行前面的程序时，会生成以下输出:\n\n![Figure 3.2: Output from Lifetime test program](img/C14583_03_02.jpg)\n\n###### 图 3.2:寿命测试程序的输出\n\n前面输出中的十六进制数(`0xnnnnnn`)是正在构造或析构的 Int 对象的地址。我们的程序从进入`第 46 行`开始，使用`主()`功能。在这一点上，程序已经做了大量的初始化，以便我们可以随时使用所有的东西。下图是两个堆栈–**电脑堆栈**和**数据堆栈**。\n\n这些都是帮助我们解释幕后发生的事情的抽象概念。`PC 栈` ( `程序计数器栈`)用于记住程序计数器的值(一个指向需要运行的下一条指令的寄存器)，而`数据栈保存`我们正在处理的值或地址。虽然这是两个独立的堆栈，但在真正的 CPU 上，它很可能作为一个堆栈来实现。让我们看看下面的表格，其中我们使用了缩写`OLn`来表示来自前面程序输出的行号:\n\n![Figure 3.3: Detailed analysis of the test program’s execution (part 1)](img/C14583_03_03.jpg)\n\n###### 图 3.3:测试程序执行的详细分析(第 1 部分)\n\n下面是测试程序执行的详细分析的第二部分:\n\n![Figure 3.4:  Detailed analysis of the test program’s execution (part 2)](img/C14583_03_04.jpg)\n\n###### 图 3.4:测试程序执行的详细分析(第 2 部分)\n\n下面是测试程序执行的详细分析的第三部分:\n\n![Figure 3.5: Detailed analysis of the test program’s execution (part 3)](img/C14583_03_05.jpg)\n\n###### 图 3.5:测试程序执行的详细分析(第 3 部分)\n\n从这个简单的程序中，我们了解到一些重要的事实:\n\n*   当我们通过值传递时，复制构造函数被调用(就像我们在这个例子中做的那样)。\n*   返回一个类型只会导致调用一个构造函数(而不是两个构造函数——一个用于创建返回对象，一个用于存储返回数据的变量)——c++ 调用这个**复制省略**，现在在标准中是强制的。\n*   在终止作用域时(结束的花括号“`}`)，任何超出作用域的变量都会被调用析构函数。如果这是真的，那么为什么地址`0x6000004d0`没有显示析构函数调用(`~Int()`)？这就引出了下一个事实。\n*   **原始指针**的析构函数只“破坏”指针，而不是它所指向的对象。这意味着当我们退出`计算()`方法时，我们泄漏了一些内存。\n\n当我们忘记释放资源时，后两个事实对于理解和解决资源泄漏问题非常重要。我们将在处理完 C++ 中的异常后再来看资源管理。\n\n## c++ 中的异常\n\n我们已经看到了 C++ 如何用自动和动态的生存期来管理局部范围变量。当变量超出范围时，它会调用具有自动生存期的析构函数。我们还看到了原始指针在超出范围时是如何被破坏的。因为它没有清理动态生存期变量，所以我们丢失了它们。这是故事的一部分，将我们带向**资源获取是初始化** ( **RAII** )以后。但是，首先，我们需要了解异常如何改变程序的流程。\n\n### 异常的必要性\n\n在*章节 2A* 、*不允许鸭子–类型和演绎*中，我们被介绍到枚举，作为一种处理神奇数字的方式，用于`check_file()`功能:\n\n```cpp\nFileCheckStatus check_file(const char* name)\n{\n  FILE* fptr{fopen(name,\"r\")};\n  if ( fptr == nullptr)\n    return FileCheckStatus::NotFound;\n  char buffer[30];\n  auto numberRead = fread(buffer, 1, 30, fptr);\n  fclose(fptr);\n  if (numberRead != 30)\n    return FileCheckStatus::IncorrectSize;\n  if(is_valid(buffer))\n    return FileCheckStatus::InvalidContents;\n  return FileCheckStatus::Good;\n}\n```\n\n上述功能使用称为**状态**或**错误代码**的技术来报告操作结果。这是用于 C 风格编程的方法，其中处理与 **POSIX API** 和 **Windows API** 相关的错误。\n\n#### 注意\n\n`POSIX`代表`便携式操作系统界面`。它是一个 IEEE 标准，用于 Unix 和其他操作系统之间的软件兼容性。\n\n这意味着，方法的调用方必须检查返回值，并对每种错误类型采取适当的操作。当您可以推理出代码将生成的错误类型时，这种方法很有效。这并不总是正确的。例如，提供给程序的数据可能有问题。这导致程序中出现无法处理的异常状态。代码中具有处理错误逻辑的部分将从检测到问题的代码部分中删除。\n\n虽然可以编写处理这种问题的代码，但它增加了处理所有错误情况的复杂性，从而使程序难以阅读，难以推理函数应该做什么，因此非常难以维护。\n\n对于错误处理，与错误代码相比，异常具有以下优势:\n\n*   错误代码可以忽略–异常会强制处理错误(或者程序终止)。\n*   异常可以沿着堆栈向上流动，到达响应错误的最佳方法。错误代码需要从每个中间方法传播出去。\n*   异常将错误的处理从主程序流中分离出来，从而使软件易于可读性和可维护性。\n*   异常将检测错误的代码与处理错误的代码分开。\n\n如果您遵循最佳实践并针对异常情况使用异常，则使用异常不会产生(时间)开销。这是因为一个实现良好的编译器会传递 C++ 的咒语——你不用为你不用的东西付费。这可能会消耗一些内存，您的代码可能会稍微大一点，但运行时间应该不会受到影响。\n\nC++ 使用异常来处理运行时异常。通过使用异常，我们可以检测到一个错误，抛出一个异常，然后错误传播回可以处理它的位置。我们修改一下之前的程序，引入`divide()`函数，改变 calculate()函数来调用。我们还将在`main()`函数中添加日志记录，这样我们就可以探索异常的行为:\n\n![Figure 3.6: Modified test program for investigating exceptions](img/C14583_03_06.jpg)\n\n###### 图 3.6:用于调查异常的修改后的测试程序\n\n当我们编译并运行前面的程序时，会生成以下输出:\n\n![Figure 3.7: Output from the test program](img/C14583_03_07.jpg)\n\n###### 图 3.7:测试程序的输出\n\n在前面的代码中，您可以看到注释被添加到右侧。现在，我们删除程序中`结果 2`行的注释，重新编译程序，然后重新运行它。生成的新输出如下所示:\n\n![Figure 3.8: Output from the test program – result2](img/C14583_03_08.jpg)\n\n###### 图 3.8:测试程序的输出–结果 2\n\n通过比较输出，我们可以看到每一行的前八行是相同的。前面输出的下两行相加是因为`divide()`函数被调用了两次。最后一行表示引发了异常，程序被终止。\n\n对`divide()`函数的第二次调用试图除以零，这是一个异常操作。这导致了一个异常。如果一个整数被零除，就会导致浮点异常。这与异常在`POSIX`系统中生成的方式有关——它使用一种叫做信号的东西(我们在这里不讨论信号的细节)。当一个整数被零除时，`POSIX`系统将其映射到名为 **SIGFPE** 的信号，该信号最初是用于`浮点错误`的，但现在是更通用的`算术错误`。\n\n#### **注**\n\n根据 C++ 标准，如果零作为“`/`”运算符(除)或“`%`”运算符(模)的除数出现，则行为未定义。大多数系统会选择抛出异常。\n\n因此，我们从前面的解释中学到了一个重要的事实:一个未处理的异常将终止程序(在内部，它调用`std::terminate()`)。我们将修复`未定义的行为`，捕捉异常，并查看输出中的变化。要修复`未定义行为`，我们需要在文件顶部添加`# include<stdexit>`，修改`divide()`功能:\n\n```cpp\nInt divide(Int a, Int b )\n{\n    if (b.m_value == 0)\n        throw std::domain_error(\"divide by zero error!\");\n    return a.m_value/b.m_value;\n}\n```\n\n当我们重新编译并运行程序时，我们会得到以下输出:\n\n![Figure 3.9: Output when we throw an exception](img/C14583_03_09.jpg)\n\n###### 图 3.9:当我们抛出异常时的输出\n\n从前面的输出中我们可以看出，变化不大。只是我们没有得到一个`浮点异常`(核心被转储)——程序仍然终止，但是没有转储核心。然后我们在`main()`函数中添加了一个`try/catch`块，以确保异常不再被处理。\n\n![Figure 3.10: Catching the Exception ](img/C14583_03_10.jpg)\n\n###### 图 3.10:捕捉异常\n\n重新编译程序并运行它，以获得以下输出:\n\n![Figure 3.11: Output from the program that catches the exception](img/C14583_03_11.jpg)\n\n###### 图 3.11:捕获异常的程序的输出\n\n在前面的输出中，在注释为“**copy a for call divide**”的第二行抛出异常。此后输出的所有内容都是正在处理的异常的结果。\n\n我们的代码已经将程序控制转移到了`main()`函数中的`catch()`语句，并且已经为堆栈上构造的所有变量执行了析构函数(从在`try`子句中进行调用时开始)。\n\n### 堆叠展开\n\nC++ 语言保证的销毁所有局部函数变量的过程称为**栈展开**。当堆栈在出现异常时展开时，C++ 使用其定义良好的规则来销毁范围内的所有对象。\n\n当异常发生时，函数调用堆栈开始从当前函数线性搜索回调用它的函数，再到调用它的函数，以此类推，直到找到与异常匹配的异常处理程序(由`catch`块表示)。\n\n如果发现异常处理程序，那么堆栈展开发生，破坏堆栈中所有函数的所有局部变量。对象按照与创建时相反的顺序销毁。如果没有找到处理抛出异常的处理程序，那么程序终止(通常不警告用户)。\n\n### 练习 1:在分数和堆栈中实现异常\n\n在本练习中，我们将返回到我们在*章节 2A* 、*不允许鸭子–类型和演绎*和*章节 2B* 、*不允许鸭子–模板和演绎*–`分数`和`堆栈`中学习的两个类，这两个类都可能经历运行时异常。我们将更新他们的代码，以便他们可以在检测到任何问题时引发异常。按照以下步骤实施本练习:\n\n1.  打开 Eclipse，使用在**第 3 课**示例文件夹中找到的文件创建一个名为**第 3 课**的新项目。\n2.  由于这是一个基于 **CMake 的项目**，将当前的构建器改为 **CMake Build(可移植)**。\n3.  进入**项目** | **构建全部**菜单构建所有练习。默认情况下，屏幕底部的控制台将显示 **CMake 控制台【第 3 课】**。\n4.  配置新的**启动配置**、**L3 练习 1** 以名称**练习 1** 运行。\n5.  另外，配置一个新的 C/C++ 单元运行配置 **L3Ex1Tests** ，运行 **L3Ex1tests** 。设置**谷歌测试运行程序**。\n6.  Click on the **Run** option for the existing **18** tests to run and pass.\n\n    ![Figure 3.12: Existing tests all pass (Runs: 18)](img/C14583_03_12.jpg)\n\n    ###### 图 3.12:现有测试全部通过(运行:18)\n\n7.  在编辑器中打开 **Fraction.hpp** ，将文件顶部的行改为这样:\n\n    ```cpp\n    #define EXERCISE1_STEP  14\n    ```\n\n8.  Click on the **Run** button to re-run the tests – we have added one test that will attempt to create a `Fraction` with a zero denominator. The test expects that an exception has been thrown:\n\n    ![Figure 3.13: New failing test ThrowsDomainErrorForZeroDenominator](img/C14583_03_13.jpg)\n\n    ###### 图 3.13:新的失败测试\n\n9.  点击失败的测试名称–现在**消息**窗口将显示预期行为和实际行为。您可能需要向右滚动才能全部阅读。在最右边，它表示“`预期……抛出一个 std::domain_error 类型的异常`”，下一行表示“`实际:它什么都不抛出`”。\n10.  Double-click on the message and it will take you to the following test:\n\n    ![Figure 3.14: The failing test](img/C14583_03_14.jpg)\n\n    ###### 图 3.14:失败的测试\n\n    `ASSERT_THROW()`宏需要两个参数。由于`分数初始值设定项`中有一个逗号，因此有必要将整个第一个参数包装在一组额外的括号中。第二个参数将从这个构造函数中得到一个`std::domain_error`。内部`try/catch`结构存在，以确认预期的字符串在异常对象内部被捕获。如果我们不想检查这个，那么我们可以这样简单地编写测试:\n\n    ```cpp\n    ASSERT_THROW(({Fraction f1{1,0}; }), std::domain_error);\n    ```\n\n11.  在编辑器中打开文件 **Fraction.cpp** 。在文件顶部附近插入以下行:\n\n    ```cpp\n    #include <stdexcept> \n    ```\n\n12.  修改构造函数，如果创建时分母为零，则抛出异常:\n\n    ```cpp\n    Fraction::Fraction(int numerator, int denominator) \n                           : m_numerator{numerator}, m_denominator{denominator}\n    {\n        if(m_denominator == 0) \n        {\n            throw std::domain_error(\"Zero Denominator\");\n        }\n    }\n    ```\n\n13.  点击**运行**按钮重新运行测试。 **19** 测试现在通过。\n14.  在编辑器中打开 **Fraction.hpp** ，将文件顶部附近的行改为这样:\n\n    ```cpp\n    #define EXERCISE1_STEP  20\n    ```\n\n15.  点击**运行**按钮重新运行测试-新测试**失败。**\n16.  点击失败的测试名称–现在**消息**窗口将显示预期行为和实际行为。您可能需要向右滚动才能全部阅读。在最右边，它表示“`预期……抛出一个类型为 std::runtime_error`的异常”，下一行表示“`实际:它抛出一个不同的类型`”。\n17.  Double-click on the message again to open the failing test:\n\n    ![Figure 3.15: Another failing test](img/C14583_03_15.jpg)\n\n    ###### 图 3.15:另一个失败的测试\n\n    该测试正在验证除法赋值运算符是否会为被零除抛出异常。\n\n18.  打开**分数. cpp** 并定位`操作员/=()`功能。你会看到，在这个函数里面，它实际上使用了**分数**的构造函数，所以它会抛出一个`std::domain_error`。\n19.  现在修改`运算符/=()`以在调用构造函数之前检测这个问题，这样它就会抛出一个带有预期消息的`std::runtime_error`。\n20.  通过添加一个域错误来修改**分数. cpp** ，该域错误将检测除法运算符:\n\n    ```cpp\n    Fraction& Fraction::operator/=(const Fraction& rhs)\n    {\n        if (rhs.m_numerator == 0)\n        {\n            throw std::runtime_error(\"Fraction Divide By Zero\");\n        }\n        Fraction tmp(m_numerator*rhs.m_denominator, \n    m_denominator*rhs.m_numerator);\n        *this = tmp;\n        return *this;\n    }\n    ```\n\n21.  点击**运行**按钮重新运行测试。所有 **20** 测试通过。\n22.  在编辑器中打开 **Stack.hpp** ，将文件顶部附近的行改为如下所示:\n\n    ```cpp\n    #define EXERCISE1_STEP  27\n    ```\n\n23.  Click on the **Run** button to re-run the tests – we have added one test that will attempt to pop from an empty stack. In the **C/C++ Unit tab** window, click on the triangle next to `FractionTest` to collapse the lists of tests and show the `StackTest`:\n\n    ![Figure 3.16: pop Stack test fails](img/C14583_03_16.jpg)\n\n    ###### 图 3.16:弹出堆栈测试失败\n\n24.  使用 **C/C++ 单元**窗口点击并定位失败的测试。确定预期异常，然后打开 **Stack.hpp** 。在文件顶部添加`# include<STD except>`，然后更新`pop()`功能，使其看起来像这样:\n\n    ```cpp\n    void pop()\n    {\n        if(empty())\n            throw std::underflow_error(\"Pop from empty stack\");\n        m_stack.pop_back();\n    } \n    ```\n\n25.  点击**运行**按钮重新运行测试。 **21** 测试现在通过。\n26.  在编辑器中打开 **Stack.hpp** ，将文件顶部的行改为如下所示:\n\n    ```cpp\n    #define EXERCISE1_STEP  31\n    ```\n\n27.  点击**运行**按钮重新运行测试-新增加的测试**失败。**\n28.  使用 **C/C++ 单元**窗口点击并定位失败的测试。确定预期异常，然后打开 **Stack.hpp** 。更新非常数`top()`方法，使其看起来如下:\n\n    ```cpp\n    reference top()\n    {\n        if(empty())\n            throw std::underflow_error(\"Top from empty stack\");\n        return m_stack.back();\n    }\n    ```\n\n29.  点击**运行**按钮重新运行测试。 **22** 测试通过。\n30.  在编辑器中打开 **Stack.hpp** ，将文件顶部的行改为如下所示:\n\n    ```cpp\n    #define EXERCISE1_STEP  35\n    ```\n\n31.  点击**运行**按钮重新运行测试-新增加的测试**失败。**\n32.  使用 **C/C++ 单元**窗口点击并定位失败的测试。确定预期异常，然后打开 **Stack.hpp** 。更新常量`top()`方法，使其看起来如下:\n\n    ```cpp\n    const_reference top() const\n    {\n        if(empty())\n            throw std::underflow_error(\"Top from empty stack\");\n        return m_stack.back();\n    }\n    ```\n\n33.  点击**运行**按钮重新运行测试。所有 **23** 测试现在通过。\n\n在本练习中，我们添加了运行时检查预条件，这是使用我们的`分数`和`堆栈`类的正常操作的一部分。这段代码只会在违反一个先决条件时抛出异常，表明数据或我们的程序执行方式有问题。\n\n### 抛出异常时会发生什么？\n\n在某个时刻，我们的程序执行以下语句:\n\n```cpp\nthrow expression;\n```\n\n通过执行这个操作，我们发出了一个信号，表明出现了一个错误的情况，并且我们希望它得到处理。接下来发生的事情是一个**临时**对象，被称为**异常对象**，它被构建在一个未指定的存储中，并根据表达式进行复制初始化(它可以调用移动构造函数，并可能会被复制省略)。异常对象的类型是从表达式中静态确定的，删除了 const 和 volatile 限定符。数组类型衰减为指针，而函数类型转换为函数的指针。如果表达式的类型是格式错误的或抽象的，那么将发生编译器错误。\n\n构造异常对象后，控件连同异常对象一起被转移到异常处理程序。所选择的异常处理程序是堆栈展开时与异常对象具有最接近匹配类型的处理程序。异常对象存在，直到最后一个 catch 子句退出，除非它被重新抛出。表达式的类型必须有一个可访问的`复制构造函数`和一个`析构函数`。\n\n### 按值投掷或按指针投掷\n\n知道了一个临时异常对象被创建，传递，然后销毁，抛出表达式应该使用什么类型？一个`值`还是一个`指针`？\n\n我们还没有过多讨论在 catch 语句中指定类型。我们很快就会这么做。但是现在，请注意，要捕获指针类型(已经抛出)，捕获模式也需要是指针类型。\n\n如果一个指向对象的指针被抛出，那么抛出方必须确保异常对象将指向的对象(因为它将是指针的副本)将保持活动状态，直到异常被处理，甚至通过`栈展开`。\n\n指针可以指向静态变量、全局变量或从堆中分配的内存，以确保在处理异常时被指向的对象仍然存在。现在，我们已经解决了保持异常对象活动的问题。但是当处理者处理完它之后，捕手会怎么处理它呢？\n\n异常的捕捉者不知道异常对象的创建(`全局`、`静态`或`堆`)因此不知道是否应该删除接收到的指针。因此，按指针抛出不是抛出异常的推荐方法。\n\n抛出的对象将被复制到创建的临时异常对象，并传递给处理程序。当异常被处理后，临时对象将被销毁，程序将继续运行。关于如何处理它，没有任何含糊之处。因此，最好的做法是通过值抛出**异常。**\n\n### 标准库异常\n\nC++ 标准库将`标准::异常`定义为所有标准库异常的基类。该标准定义了以下第一级层次的`异常` / `错误`(括号中的数字表示有多少个异常源自该类):\n\n![Figure 3.17: Standard Library exception Hierarchy (two levels)](img/C14583_03_17.jpg)\n\n###### 图 3.17: 标准库异常层次结构(两级)\n\n这些异常通过包括 STL 的 C++ 标准库来使用。创建自己的异常类的最佳实践是从一个标准异常中派生它。正如我们接下来将看到的，您的特殊异常可以被一个标准异常的处理程序捕获。\n\n### 捕捉异常\n\n在讨论异常的必要性时，我们引入了抛出异常的想法，但并没有真正考虑 C++ 如何支持捕捉异常。异常处理的过程从一段代码被包装在`try`块中开始，将其置于**异常检查**下。try 块后面是一个或多个 catch 块，它们是异常处理程序。当在 try 块内执行代码时出现异常情况时，将引发异常，并将控制权转移给异常处理程序。如果没有抛出异常，则跳过所有异常处理程序，try 块中的代码完成，正常执行继续。让我们在代码片段中表达这些概念:\n\n```cpp\nvoid SomeFunction()\n{\n  try {\n    // code under exception inspection\n  }\n  catch(myexception e)         // first handler – catch by value\n  {\n    // some error handling steps\n  }\n  catch(std::exception* e)     // second handler – catch by pointer\n  {\n    // some other error handling steps\n  }\n  catch(std::runtime_error& e) // third handler – catch by reference\n  {\n    // some other error handling steps\n  }\n  catch(...)                   // default exception handler – catch any exception\n  {\n    // some other error handling steps\n  }\n  // Normal programming continues from here\n}\n```\n\n前面的片段显示了必要的关键词–`尝试`，以及`捕捉`，并介绍了三种不同类型的捕捉模式(不包括默认处理程序):\n\n*   **按值捕获异常**:这是一种代价高昂的机制，因为异常处理程序的处理与任何其他函数一样。按值捕获意味着必须创建异常对象的副本，然后将其传递给处理程序。第二个副本的创建减慢了异常处理过程。这种类型也可能受到对象切片的影响，其中抛出了一个子类，catch 子句是一个超类。catch 子句将只接收丢失原始异常对象属性的超类对象的副本。因此，我们应该避免按值捕获异常处理程序。\n*   **通过指针捕捉异常**:正如在查看按值抛出时所讨论的，使用按指针抛出，这种风格的异常处理程序只能捕捉指针抛出的异常。因为我们只想按值抛出，所以我们应该避免使用指针捕捉异常处理程序。\n*   **Catch expression by reference**: This is the recommended style of exception handler as it does not suffer from the issues related to catch-by-value and catch-by-pointer. As a reference is passed to the handler, no second copy of the exception object is made. Splicing does not occur because the reference still refers to the originally thrown exception object. And since the exception was thrown by value, the temporary exception object will be destroyed automatically when we are done with it.\n\n    #### 注意\n\n    处理异常时，是`按值抛出`、`按引用捕捉`。\n\n当有多个 catch 块时，异常对象类型用于按照指定的顺序匹配处理程序。一旦找到匹配的处理程序，它就会被执行，其余的异常处理程序将被忽略。这与函数解析不同，在函数解析中，编译器会找到与参数的最佳匹配。因此，异常处理程序(catch 块)应该从更具体的到更一般的来定义。例如，默认处理程序(`catch(...)`)应该总是排在定义的最后。\n\n### 练习 2:实现异常处理程序\n\n在本练习中，我们将实现异常处理程序的层次结构，以管理如何处理异常。按照以下步骤实施本练习:\n\n1.  Open the **Lesson3** project in Eclipse. Then in the **Project Explorer**, expand **Lesson3** then **Exercise02** and double click on **exceptions.cpp** to open the file for this exercise into the editor. This file contains the following code:\n\n    ```cpp\n    #include <exception>\n    #include <iostream>\n    void run_exceptions()\n    {\n        try\n        {\n            throw std::domain_error(\"We got one!!!!\");\n        }\n        catch(...)\n        {\n        std::cout << \"Exception caught by default handler\\n\";\n        }\n        catch(const std::exception& e)\n        {\n            std::cout << \"Exception '\" << \"' caught by std::exception handler\\n\";\n        }\n        catch(const std::logic_error& e)\n        {\n        std::cout << \"Exception '\" << \"' caught by std::logic_error handler\\n\";\n        }\n        catch(const std::domain_error& e)\n        {\n            std::cout << \"Exception '\" << \"' caught by std::domain_error handler\\n\";\n        }\n    }\n    int main()\n    {\n        std::cout << \"\\n\\n------ Exercise 2 ------\\n\";\n        run_exceptions();\n        std::cout << \"Complete.\\n\";\n        return 0;\n    }\n    ```\n\n    #### **注**\n\n    所有异常处理程序都使用了相同的名称作为异常参数，即`e`。该变量的作用域只是声明它的 catch 块。\n\n2.  点击**启动配置**下拉菜单，选择**新启动配置……**。从**搜索项目**菜单中配置**L3 练习 2** 应用，以名称**L3 练习 2** 运行它。\n3.  完成后，将是当前选择的**启动配置**。\n4.  Click on the **Run** button. Exercise 2 will run and produce the following output:\n\n    ![Figure 3.18: Exercise 2 output – default handler caught the exception](img/C14583_03_18.jpg)\n\n    ###### 图 3.18:练习 2 输出-默认处理程序捕获了异常\n\n5.  在控制台窗口中，点击**显示选中的控制台**按钮，选择 **CDT 全局构建控制台**。滚动窗口。您会发现(如果使用 GCC 编译器的话)有五条警告消息与我们放置异常处理程序的顺序有关。(实际上，第一个警告通常是一个错误，除了`CMake`文件在编译该目标时设置了`-fpermissive`标志。)\n6.  In the editor, move the default exception handler, `catch(...)`, to just after the `std::domain_error` handler. Click on the **Run** button. Exercise 2 will run and produce the following output:\n\n    ![Figure 3.19: std::exception handler has been used](img/C14583_03_19.jpg)\n\n    ###### 图 3.19:使用了标准::异常处理程序\n\n7.  在编辑器中，将`std::exception`处理程序移到`std::domain_error`处理程序之后。点击**运行**按钮。这一次，它将报告执行了`std::logic_error`处理程序。\n8.  在编辑器中，将`std:: logic_error`处理程序移到`std::domain_error`处理程序之后。点击**运行**按钮。这一次，它将报告执行了`std:: domain_error`处理程序，这实际上是我们所期望的。\n9.  现在将`掷`线改为`std::logic_error`异常。点击**运行**按钮。这一次，它将报告`std::logic_error`处理程序按预期执行。\n10.  现在将`抛出`线改为`标准::下溢 _ 错误`异常。点击**运行**按钮，这一次它将报告异常被`std::异常`处理程序捕获，正如预期的那样。`std::exception`是所有标准库异常的基类。\n\n在本练习中，我们实现了一系列异常处理程序，并观察了异常处理程序的顺序如何影响捕获异常的方式以及如何使用异常层次结构。\n\n### CMake 生成器表达式\n\n使用`CMake`时，有时需要调整变量值。`CMake`是一个构建生成器系统，可以为很多构建工具和编译器工具链生成构建文件。由于这种灵活性，如果您想在编译器中打开某些功能，您只需要将它应用于一种特定的类型。这是因为不同供应商的命令行选项不同。例如，g++ 编译器启用 C++ 17 支持的命令行选项是`-std=c++ 17`，但对于`msvc`则是`/std:c++ 17`。如果打开 **CMakeLists.txt** 文件，定位**l3 锻炼 2**`add _ executable`，那么后面会有一行:\n\n```cpp\ntarget_compile_options(L3Exercise2 PRIVATE $<$<CXX_COMPILER_ID:GNU>:-fpermissive>)\n```\n\n这使用`$<CXX _ COMPILER _ ID:GNU>`变量查询来检查是否是 GCC 编译器。如果是，则生成 1(真)，否则生成 0(假)。它还使用`$ <条件:true_string >`条件表达式将`-fppermissive`添加到**l3 锻炼 2** 目标的编译器选项中，但仅限于 gcc 编译器。这些可以作为对`target_compile_options`的单独调用或通过一次调用为每个编译器类型添加。\n\n#### 注意\n\n有关生成器表达式的更多信息，请查看以下链接:[https://cmake . org/cmake/help/v 3.15/manual/cmake-generator-expressions . 7 . html](https://cmake.org/cmake/help/v3.15/manual/cmake-generator-expressions.7.html)。\n\n### 异常使用指南\n\n在 C++ 代码中使用异常时，请记住以下几点:\n\n*   吟诵:**按值抛投，按参考接球**\n*   **正常程序流程不要使用异常**。如果一个函数满足一个异常条件，并且不能满足它的(函数)义务，那么也只有这样，你才会抛出一个异常。如果该功能能够解决异常情况并履行其义务，那么它就不是异常。它们被命名为异常是有原因的，如果您不使用它们，您将不会产生任何处理开销。\n*   **不要从析构函数**中抛出异常。请记住，由于堆栈展开，将执行局部变量析构函数。如果在堆栈展开过程中调用析构函数并引发异常，程序将终止。\n*   **不要吞下异常**。不要使用默认的 catch 处理程序，也不要处理异常。引发异常是为了表明存在问题，您应该对此采取措施。忽略异常可能会导致稍后难以排除的故障。这是因为任何有用的信息都会随着被吞咽的异常而真正丢失。\n*   **异常对象从抛出**中复制。\n\n## 资源管理(在异常的世界中)\n\n到目前为止，我们已经了解了局部变量的作用域，以及当变量超出作用域时如何处理`自动`和`动态寿命变量`——自动寿命变量(那些放在堆栈上的)被完全析构，而`动态寿命变量`(那些被程序员分配到堆中的)没有被析构:我们只是失去了对它们的任何访问。我们还看到，当抛出异常时，会找到最近的匹配处理程序，在堆栈展开过程中，抛出点和处理程序之间的所有局部变量都会被析构。\n\n我们可以利用这些知识来编写健壮的资源管理类，这将使我们不再需要跟踪资源(动态生存期变量、文件句柄、系统句柄等)，以确保当我们使用完它们时，它们被释放(回到野外)。在正常运行和异常情况下，用于管理资源的技术被称为**资源获取是初始化** ( **RAII** )。\n\n### 资源获取是初始化\n\nRAII 是另一个命名不当的概念的好例子(另一个是`SFINAE`)。`RAII`或`资源获取是初始化`描述了用于管理资源的类的行为。如果把它命名为**破坏就是资源释放**可能会更好，它真正抓住了管理类试图做的事情的本质。我们可以从我们之前的讨论中推断出如何实现这一点，但是展示一个单独的例子来开发资源管理`文件`类，并展示 RAI 如何提高可读性和我们推理函数功能的能力，会更有启发性。\n\n考虑以下代码:\n\n```cpp\nvoid do_something()\n{\n    FILE* out{};\n    FILE* in = fopen(\"input.txt\", \"r\");\n    try \n    {\n        if (in != nullptr)\n        {\n            // UNSAFE – an exception here will create a resource leak\n            out = fopen(\"output.txt\", \"w\");\n            if (out != nullptr)\n            {\n                // Do some work\n                // UNSAFE – an exception here will create resource leaks\n                fclose(out);\n            }\n            fclose(in);\n        }\n    }\n    catch(std::exception& e)\n    {\n        // Respond to the exception\n    }\n}\n```\n\n这段代码显示了资源管理的两个潜在问题:\n\n*   最重要的是，在文件打开和关闭之间出现异常会导致资源泄漏。如果这是一个系统资源，其中许多会导致系统不稳定或应用性能受到不利影响，因为它缺乏资源。\n*   此外，由于错误处理，在一个方法中管理多个资源会导致深嵌套子句。这不利于代码的可读性，因此也不利于代码的理解和可维护性。很容易忘记释放一个资源，尤其是有多个退出点的时候。\n\n那么，我们如何管理资源，以便拥有异常安全和更简单的代码呢？这个问题并不是 C++ 独有的，不同的语言对它的管理也不同。`Java`、`C#`和`Python`使用垃圾收集方法，该方法会扫描创建的对象，并在它们不再被引用时进行清理。但是 C++ 没有垃圾收集，那么解决方案是什么呢？\n\n考虑以下类别:\n\n```cpp\nclass File {\npublic:\n    File(const char* name, const char* access) {\n        m_file = fopen(name, access);\n        if (m_file == nullptr) {\n            throw std::ios_base::failure(\"failed to open file\");\n        }\n    }\n    ~File() {\n        fclose(m_file);\n    }\n    operator FILE*() {\n        return m_file;\n    }\nprivate:\n    FILE* m_file{};\n};\n```\n\n此类实现以下特征:\n\n*   构造函数获取资源。\n*   如果构造函数中没有获取资源，则会引发异常。\n*   当类被销毁时，资源被释放。\n\n如果我们在`do_something()`方法中使用这个类，那么它看起来像这样:\n\n```cpp\nvoid do_something()\n{\n    try \n    {\n        File in(\"input.txt\", \"r\");\n        File out(\"output.txt\", \"w\");\n        // Do some work\n    }\n    catch(std::exception& e)\n    {\n        // Respond to the exception\n    }\n}\n```\n\n如果在这样做的时候发生异常，那么 C++ 保证所有基于堆栈的对象都将调用它们的析构函数(`堆栈展开`，从而保证文件被关闭。这解决了出现异常时资源泄漏的问题，因为资源现在已被自动清理。此外，这种方法非常容易阅读，这样我们就可以理解逻辑流程，而不必担心错误处理。\n\n该技术利用`文件`对象的生存期来获取和释放资源，确保资源不泄露。资源在管理类的构建(初始化)过程中获取，在管理类的销毁过程中释放。正是这种受范围限制的资源的行为产生了名称`资源获取是初始化`。\n\n前面的示例涉及管理作为系统资源的文件句柄。它适用于任何需要在使用前获得，然后在完成时放弃的资源。RAII 技术可以应用于广泛的资源——打开的文件、打开的管道、分配的堆内存、打开的套接字、执行的线程、数据库连接、互斥锁/关键部分的锁定——基本上是主机系统中供应不足且需要管理的任何资源。\n\n### 练习 3:为内存和文件句柄实现 RAII\n\n在本练习中，我们将实现两个不同的类，它们将使用 RAII 技术管理内存或文件。按照以下步骤实施本练习:\n\n1.  在 Eclipse 中打开**第 3 课**项目。然后在**项目浏览器**中，展开**第 3 课**，然后展开**练习 03** ，双击**练习 3.cpp** 将本练习的文件打开到编辑器中。\n2.  点击**启动配置**下拉菜单，选择**新启动配置……**。从“搜索项目”菜单中配置**L3 练习 3** 应用，以名称**L3 练习 3** 运行它。\n3.  Click on the **Run** button to run Exercise 3\\. This will produce the following output:\n\n    ![Figure 3.20: Leaky memory and files from Exercise3.cpp](img/C14583_03_20.jpg)\n\n    ###### 图 3.20:来自练习 3.cpp 的内存和文件泄漏\n\n    输出显示我们分配了五次内存，地址由 new 返回。当从`main()`函数执行时，当`监视器`被析构时，它转储已分配和释放的内存报告，以及已打开但从未关闭的文件。\n\n4.  在编辑器中，在`文件`类的**练习 3.cpp** 文件中键入以下内容:\n\n    ```cpp\n    class File {\n    public:\n        File(const char* name, const char* access) {\n            m_file = fopen(name, access);\n            if (m_file == nullptr) {\n                throw std::ios_base::failure(\"\"failed to open file\"\");\n            }\n        }\n        ~File() {\n            fclose(m_file);\n        }\n        operator FILE*() {\n            return m_file;\n        }\n    private:\n        FILE* m_file{};\n    };\n    ```\n\n5.  点击**运行**按钮运行练习 3–它仍然会泄漏文件和内存，但是代码是正确的。\n6.  找到`泄漏文件()`函数，并对其进行修改，使其使用新的`文件`类(与前面的代码类似)来防止文件泄漏:\n\n    ```cpp\n    void LeakFiles()\n    {\n        File fh1{\"HelloB1.txt\", \"w\"};\n        fprintf(fh1, \"Hello B2\\n\");\n        File fh2{\"HelloB2.txt\", \"w\"};\n        fprintf(fh2, \"Hello B1\\n\");\n    }\n    ```\n\n7.  Click on the **Run** button to run Exercise 3\\. If you have modified `LeakFiles()` correctly, then the output will be as follows:\n\n    ![Figure 3.21: No file leaks](img/C14583_03_21.jpg)\n\n    ###### 图 3.21:没有文件泄漏\n\n8.  现在在**练习 3.cpp** 中，添加以下`CharPointer`类:\n\n    ```cpp\n    class CharPointer\n    {\n    public:\n        void allocate(size_t size)\n        {\n            m_memory = new char[size];\n        }\n        operator char*() { return m_memory;}\n    private:\n        char* m_memory{};\n    };\n    ```\n\n9.  将`泄漏指针()`修改如下:\n\n    ```cpp\n    void LeakPointers()\n    {\n        CharPointer memory[5];\n        for (auto i{0} ; i<5 ; i++)\n        {\n            memory[i].allocate(20); \n            std::cout << \"allocated 20 bytes @ \" << (void *)memory[i] << \"\\n\";\n        }\n    }\n    ```\n\n10.  点击**运行**按钮运行练习 3–仍然有内存泄漏，但是代码是正确的。\n11.  现在，添加以下析构函数到`字符指针`。注意`删除`运算符使用数组`[]`语法:\n\n    ```cpp\n    ~CharPointer()\n    {\n        delete [] m_memory;\n    }\n    ```\n\n12.  再次点击**运行**按钮运行练习 3–这一次，您应该看到监视器没有报告泄漏:\n\n![Figure 3.22: No leaks – memory or files](img/C14583_03_22.jpg)\n\n###### 图 3.22:无泄漏-内存或文件\n\n`文件`和`CharPointer`的实现提供了`RAII`设计方法，但是在设计这些时还有其他的考虑。例如，我们需要复制构造函数还是复制赋值函数？在这两种情况下，仅仅将资源从一个对象复制到另一个对象可能是一个问题，因为这可能导致两次尝试关闭文件句柄或删除内存。通常，这将导致未定义的行为。接下来，我们将根据资源管理对象的实现重新访问特殊成员函数，如`文件`或`CharPointer`。\n\n### 特殊编码技术\n\n*练习 3* 、*为内存和文件句柄*实现 RAII 的代码是专门编写的，这样我们可以监控内存和文件句柄的使用情况，并在退出时报告任何泄漏。访问 **monitor.h** 和 **monitor.cpp** 文件，检查用于使监视器成为可能的两种技术:\n\n*   **Preprocessor macros**: This is the special use of a preprocessor macro to demonstrate the leaks and should not be used in production code, that is, replacing a function by text substitution.\n\n    如果您使用视窗应用编程接口编程，您可能偶尔会发现您的方法名称与微软用于其应用编程接口方法的宏冲突。例如，如果您包含 **windows.h** ，请不要调用您的任何方法`发送消息`。如果您这样做了，那么根据您是构建 ASCII 模式还是 Unicode 模式，它将分别被`发送消息`或`发送消息`替换。\n\n*   **定义我们自己的新处理程序**:这是一种先进的技术，除非你编写嵌入式代码，否则你不太可能需要它。\n\n### C++ 最终不需要\n\n支持异常抛出机制的其他语言(`C#`、`Java`和`可视化 Basic.NET`)有一个`try/catch/finally`范例，其中`finally`块中的代码在从 try 块退出时被调用——正常或异常。C++ 没有`最后`块，因为它可以访问更好的机制，确保我们不会忘记释放一个资源——RAII。由于资源由本地对象表示，本地对象的析构函数将释放资源。\n\n这种设计模式的额外优势是，如果正在管理大量资源，那么`最后`块将按比例变大。RAII 消除了对 finally 的需求，并导致更容易维护的代码。\n\n### RAII 和 STL\n\n标准模板库(STL)在其许多模板和类中使用了 RAI。例如，在 C++ 11 中引入的智能指针，即`std::unique_ptr`和`std::shared_ptr`，通过确保在内存用完时释放内存，或者确保在其他地方使用内存时不释放内存，来帮助避免许多问题。STL 中的其他示例包括`标准::字符串`(内存)、`标准::向量`(内存)和`标准::流`(文件句柄)。\n\n### 这个物体是谁的？\n\n使用前面的`文件`和`字符指针`的实现，我们已经用 RAII 测试了资源管理。让我们进一步探讨它。首先，我们将定义一个不止有一个资源的类:\n\n```cpp\nclass BufferedWriter\n{\npublic:\n    BufferedWriter(const char* filename);\n    ~BufferedWriter();\n    bool write(const char* data, size_t length);\nprivate:\n    const size_t BufferSize{4096};\n    FILE* m_file{nullptr};\n    size_t m_writePos{0};\n    char* m_buffer{new char[BufferSize]};\n};\n```\n\n该类用于缓冲对文件的写入。\n\n#### 注意\n\n当使用 iostream 派生类时，这通常不是必需的，因为它们已经提供了缓冲。\n\n对`write()`函数的每次调用都会将数据添加到分配的缓冲区中，直到到达`缓冲区`，此时数据实际上被写入文件，缓冲区被重置。\n\n但是如果我们想把这个`BufferedWriter`的实例分配给另一个实例或者复制它呢？什么是正确的行为？\n\n如果我们只是让默认的复制构造函数/复制赋值做它们该做的事情，我们会得到一个成员方式的项目副本。这意味着我们有两个`BufferedWriter`的实例，它们持有相同的文件句柄和指向缓冲区的指针。当对象的第一个实例被销毁时，作为优秀的程序员，我们将通过关闭文件来清理文件，通过删除文件来清理内存。第二个实例现在有一个失效的文件句柄和一个指向内存的指针，我们已经告诉操作系统为下一个用户恢复。任何使用这些资源的尝试，包括销毁它们，都将导致未定义的行为，并且很可能导致程序崩溃。默认的复制构造函数/复制赋值操作符执行所谓的浅复制——也就是说，它一点一点地复制所有成员(但不是它们所引用的)。\n\n我们拥有的两种资源可以区别对待。首先，应该只有一个类拥有`m_buffer`。处理这个问题有两种选择:\n\n*   防止类的复制，从而防止内存的复制\n*   执行`深度复制`，其中第二个实例中的缓冲区已由构造函数分配，第一个缓冲区的内容被复制\n\n其次，应该只有一个类拥有文件句柄(`m_file`)。处理这个问题有两种选择:\n\n*   防止类的复制，从而防止文件句柄的复制\n*   将`所有权`从原实例转移到第二实例，并将原实例标记为无效或空(无论是什么意思)\n\n实现深度复制很容易，但是我们如何转移资源的所有权呢？为了回答这个问题，我们需要再次查看临时对象和值类别。\n\n### 临时对象\n\n创建一个临时对象来存储表达式的中间结果，然后将结果存放到变量中(或者只是忘记)。表达式是任何返回值的代码，包括向函数传递值、从函数返回值、隐式转换、文本和二进制运算符。临时对象是`右值表达式`，它们有内存，临时为它们分配一个位置来放置表达式结果。在 C++ 11 之前，正是这种临时对象的创建和它们之间的数据复制导致了一些性能问题。为了解决这个问题，C++ 11 引入了`右值引用`来启用所谓的移动语义。\n\n### 移动语义\n\n一个`右值引用`(用一个双“与”符号表示，`& &`)是一个引用，它只被赋予一个`右值`，这个右值将延长右值的寿命，直到`右值引用`完成。所以，`值`可以超越定义它的表达式。借助`右值引用`，我们现在可以通过移动构造函数和移动赋值操作符实现移动语义。移动语义的目的是从被引用对象中窃取资源，从而避免昂贵的复制操作。移动完成后，被引用对象必须保持稳定状态。换句话说，被移动的对象必须保持这样一种状态，即当它被销毁时，不会导致任何未定义的行为或程序崩溃，也不会影响从它那里窃取的资源。\n\nC++ 11 还引入了一个强制转换操作符`std::move()`，它将一个`左值`强制转换为一个`右值`，这样就可以调用移动构造函数或移动赋值操作符来“移动”资源。`std::move()`方法实际上并不移动数据。\n\n需要注意的一件意想不到的事情是，在移动构造函数和移动赋值操作符中，`右值`引用实际上是一个`左值`。这意味着，如果您想确保移动语义发生在方法中，那么您可能需要在成员变量上再次使用`std::move()`。\n\n随着 C++ 11 引入移动语义，它还更新了标准库，以利用这一新功能。例如，`std::string`和`std::vector`已经更新为包含移动语义。获得移动语义的好处；你只需要用最新的 C++ 编译器重新编译你的代码。\n\n### 实现智能指针\n\n智能指针是一个资源管理类，它持有指向资源的指针，并在超出范围时释放它。在本节中，我们将实现一个智能指针，观察它作为复制支持类的行为，将其演化为支持移动语义，并最终移除它对复制操作的支持:\n\n```cpp\n#include <iostream>\ntemplate<class T>\nclass smart_ptr\n{\npublic:\n  smart_ptr(T* ptr = nullptr) :m_ptr(ptr)\n  {\n  }\n  ~smart_ptr()\n  {\n    delete m_ptr;\n  }\n  // Copy constructor --> Do deep copy\n  smart_ptr(const smart_ptr& a)\n  {\n    m_ptr = new T;\n    *m_ptr = *a.m_ptr;      // use operator=() to do deep copy\n  }\n  // Copy assignment --> Do deep copy \n  smart_ptr& operator=(const smart_ptr& a)\n  {\n    // Self-assignment detection\n    if (&a == this)\n      return *this;\n    // Release any resource we're holding\n    delete m_ptr;\n    // Copy the resource\n    m_ptr = new T;\n    *m_ptr = *a.m_ptr;\n    return *this;\n  }\n  T& operator*() const { return *m_ptr; }\n  T* operator->() const { return m_ptr; }\n  bool is_null() const { return m_ptr == nullptr; }\nprivate:\n  T* m_ptr{nullptr};\n};\nclass Resource\n{\npublic:\n  Resource() { std::cout << \"Resource acquired\\n\"; }\n  ~Resource() { std::cout << \"Resource released\\n\"; }\n};\nsmart_ptr<Resource> createResource()\n{\n    smart_ptr<Resource> res(new Resource);                       // Step 1\n    return res; // return value invokes the copy constructor     // Step 2\n}\nint main()\n{\n  smart_ptr<Resource> the_res;\n  the_res = createResource(); // assignment invokes the copy assignment Step 3/4\n\n  return 0; // Step 5\n}\n```\n\n当我们运行这个程序时，会生成以下输出:\n\n![Figure 3.23: Smart Pointer Program output](img/C14583_03_23.jpg)\n\n###### 图 3.23:智能指针程序输出\n\n对于这样一个简单的程序，有大量的资源获取和释放。让我们把这个分开:\n\n1.  `createResource()`内部的局部变量 res 在堆上创建并初始化(动态生存期)，导致第一条“`资源获取了`”消息。\n2.  编译器可以创建另一个临时来返回值。但是，编译器已经执行了`复制省略`来移除副本(也就是说，它能够将对象直接构建到调用函数分配的堆栈位置上)。编译器有`返回值优化` ( `RVO`)和`命名返回值优化` ( `NRVO`)可以应用的优化，在 C++ 17 下，这些在某些情况下是强制性的。\n3.  通过复制分配，临时对象被分配给 **main()** 函数中的 _res 变量。由于拷贝分配正在进行深度拷贝，因此会获取资源的另一个拷贝。\n4.  当分配完成时，临时对象超出范围，我们得到第一个“资源释放”消息。\n5.  当`main()`函数返回时，`的 _res`超出范围，释放第二个资源。\n\n因此，如果资源很大，我们在`main()`中创建`RES`局部变量的方法效率非常低，因为我们在大块内存中创建和复制，因为复制分配中有深度复制。但是我们知道，当`createResource()`创建的临时变量不再需要时，那么我们就要扔掉它，释放它的资源。在这些场景中，将资源从临时实例转移(或移动)到该类型的其他实例会更有效。移动语义使得重写我们的`smart_ptr`模板成为可能，以便不进行深度复制而是转移资源。\n\n让我们给我们的`smart_ptr`类添加移动语义:\n\n```cpp\n// Move constructor --> transfer resource\nsmart_ptr(smart_ptr&& a) : m_ptr(a.m_ptr)\n{\n  a.m_ptr = nullptr;    // Put into safe state\n}\n// Move assignment --> transfer resource\nsmart_ptr& operator=(smart_ptr&& a)\n{\n  // Self-assignment detection\n  if (&a == this)\n    return *this;\n  // Release any resource we're holding\n  delete m_ptr;\n  // Transfer the resource\n  m_ptr = a.m_ptr;\n  a.m_ptr = nullptr;    // Put into safe state\n  return *this;\n}\n```\n\n重新运行程序后，我们得到以下输出:\n\n![Figure 3.24: Smart pointer program output using move semantics](img/C14583_03_24.jpg)\n\n###### 图 3.24:使用移动语义的智能指针程序输出\n\n现在，因为移动赋值现在可用，编译器在这一行使用它:\n\n```cpp\nthe_res = createResource(); // assignment invokes the copy assignment Step 3/4\n```\n\n`第 3 步`现在被移动分配取代，这意味着深度副本现在已经被移除。\n\n`步骤 4`不再释放资源，因为带有注释“//”的行进入安全状态——它不再有资源可以释放，因为它的所有权被转移了。\n\n关于`移动构造函数`和`移动赋值`需要注意的另一点是，在它们的拷贝版本中参数是常量的地方，它们在它们的移动版本中是`非常量`。这被称为`所有权转移`，这意味着我们需要修改传入的参数。\n\n移动构造函数的另一种实现可能如下所示:\n\n```cpp\n// Move constructor --> transfer resource\nsmart_ptr(smart_ptr&& a) \n{\n  std::swap(this->m_ptr, a.m_ptr);\n}\n```\n\n本质上，我们是在交换资源，C++ STL 支持将交换作为具有许多专门化的模板。这是因为我们使用成员初始化将`m_ptr`设置为`nullptr`。因此，我们正在用存储在`a`中的值交换一个`nullptr`。\n\n既然我们已经修复了不必要的深度复制问题，我们实际上可以从`smart_ptr()`中删除复制操作，因为所有权的转移实际上是我们想要的。如果我们将一个非临时的`smart_ptr`的实例复制到另一个非临时的`smart_ptr`的实例，那么我们将有两个对象，当它们超出范围时将删除资源，这不是期望的行为。为了删除(深度)复制操作，我们更改了成员函数的定义，如下所示:\n\n```cpp\nsmart_ptr(const smart_ptr& a) = delete;\nsmart_ptr& operator=(const smart_ptr& a) = delete;\n```\n\n`= delete`的后缀，我们在*章节【2A】*、*不允许鸭子-类型和演绎*中看到，告诉编译器试图访问具有该原型的函数现在不是有效代码，并导致错误。\n\n### STL 智能指针\n\nSTL 提供了我们可以用来在对象上实现 RAI 的类，而不是必须编写自己的`smart_ptr`。原版本是`std::auto_ptr()`，在 C++ 11 中被弃用，在 C++ 17 中被删除。它是在`右值`引用支持之前创建的，由于它使用复制实现了移动语义而导致了问题。C++ 11 引入了三个新模板来管理资源的生存期和所有权:\n\n*   **std::unique_ptr** :通过指针拥有并管理一个`单个对象`，当`unique_ptr`超出范围时销毁该对象。它有两个版本:用于单个对象(使用`新建`创建)和用于对象数组(使用`新建【】`创建)。`unique_ptr`和直接使用底层指针一样高效。\n*   **std::shared_ptr** :通过指针保留对象的共享所有权。它通过使用引用计数器来管理资源。分配给 shared_ptr 的 shared_ptr 的每个副本都会更新引用计数。当引用计数变为零时，这意味着没有所有者了，资源被释放/销毁。\n*   **std::weak_ptr** :提供与`shared_ptr`相同资源的接口，但不修改计数器。可以检查资源是否仍然存在，但不会阻止资源被销毁。如果您确定该资源仍然存在，则可以使用它来获取该资源的`shared_ptr`。它的一个用例是多个`shared_ptrs`以循环引用结束的场景。循环引用会阻止资源的自动释放。`weak_ptr`用于打破循环，允许在应该释放资源的时候释放资源。\n\n### std::unique_ptr\n\n`std::unique_ptr()`是在 C++ 11 中引入的，用来代替`std::auto_ptr()`并为我们提供了`smart_ptr`所做的一切(以及更多)。我们可以重新编写我们的`smart_ptr`程序如下:\n\n```cpp\n#include <iostream>\n#include <memory>\nclass Resource\n{\npublic:\n  Resource() { std::cout << \"Resource acquired\\n\"; }\n  ~Resource() { std::cout << \"Resource released\\n\"; }\n};\nstd::unique_ptr<Resource> createResource()\n{\n  std::unique_ptr<Resource> res(new Resource);\n  return res; \n}\nint main()\n{\n  std::unique_ptr<Resource> the_res;\n  the_res = createResource(); // assignment invokes the copy assignment\n  return 0;\n}\n```\n\n我们可以更进一步，因为 C++ 14 引入了一个助手方法，在处理`unique_ptrs`时保证异常安全:\n\n```cpp\nstd::unique_ptr<Resource> createResource()\n{\n  return std::make_unique<Resource>(); \n}\n```\n\n*为什么有这个必要？*考虑以下函数调用:\n\n```cpp\nsome_function(std::unique_ptr<T>(new T), std::unique_ptr<U>(new U));\n```\n\n这样做的问题是，编译器可以自由地按照它喜欢的任何顺序对参数列表中的操作序列进行排序。它可以调用`新 T`，然后`新 U`，然后`STD::unique _ ptr<T>()`，最后`STD::unique _ ptr<U>()`。这个序列的问题是，如果`新 U`抛出异常，那么调用`新 T`分配的资源没有被放入`unique_ptr`中，不会被自动清理。`STD::make _ unique<>()`的使用保证了调用的顺序，使得资源的构造和`unique_ptr`的构造一起发生，不会泄露资源。在 C++ 17 中，对`make_unique`的需求已经被移除，在这种情况下，围绕评估顺序的规则已经被收紧。然而，使用`make _ unique<T>()`方法可能仍然是一个好主意，因为将来任何到共享 ptr 的转换都将更容易。\n\n名称`unique_ptr`明确了模板的意图，即它是它所指向的对象的唯一所有者。这在`auto_ptr`中并不明显。同样地，`shared_ptr`也很明确，它打算共享资源。`唯一 _ptr`模板提供对以下操作员的访问:\n\n*   **T* get()** :返回托管资源的指针。\n*   **运算符 bool()** :如果实例管理资源，则返回`true`。(`get()！= nullptr`)。\n*   **T &运算符*(T1):**左值**对托管资源的引用。与 ***get()** 相同。**\n*   **T*运算符- > ()** :指向托管资源的指针。与`获得()`相同。\n*   **T &运算符[](size_t index)** :对于`unique_ptr(new [])`，它提供对托管阵列的访问，就像它本身是一个阵列一样。返回一个`左值`引用，以便设置和获取该值。\n\n### std::shared_ptr\n\n当您想要共享资源的所有权时，会使用共享指针。你为什么要这么做？有几个场景非常适合资源共享，例如在图形用户界面程序中，您可能希望共享字体对象、位图对象等等。 **GoF 飞行重量设计模式**将是另一个例子。\n\n`std::shared_ptr`提供了与`std::unique_ptr`相同的功能，但是开销更大，因为它现在必须跟踪对象的引用计数。所有为`std::unique_ptr`描述的操作符都可以在`std::shared_ptr`上使用。一个区别是创建`std::shared_ptr`的推荐方法是调用`STD::make _ shared<>()`。\n\n在编写库或工厂时，库的作者并不总是知道用户想要如何使用已经创建的对象，因此建议从您的工厂方法中返回`unique_ptr < T >`。这样做的原因是用户可以通过赋值轻松地将`std::unique_ptr`转换为`STD::shared _ ptr`；\n\n```cpp\nstd::unique_ptr<MyClass> unique_obj = std::make_unique<MyClass>();\nstd::shared_ptr<MyClass> shared_obj = unique_obj;\n```\n\n这将转移所有权，并使`unique_obj`为空。\n\n#### 注意\n\n一旦资源成为共享资源，它就不能被还原成唯一的对象。\n\n### std::weak_ptr\n\n弱指针是共享指针的变体，但它不包含对资源的引用计数。所以，这并不妨碍它在计数归零时被释放。考虑以下程序结构，它可能出现在正常的图形用户界面中:\n\n```cpp\n#include <iostream>\n#include <memory>\nstruct ScrollBar;\nstruct TextWindow;\nstruct Panel\n{\n    ~Panel() {\n        std::cout << \"--Panel destroyed\\n\";\n    }\n    void setScroll(const std::shared_ptr<ScrollBar> sb) {\n        m_scrollbar = sb;\n    }\n    void setText(const std::shared_ptr<TextWindow> tw) {\n        m_text = tw;\n    }\n    std::weak_ptr<ScrollBar> m_scrollbar;\n    std::shared_ptr<TextWindow> m_text;\n};\nstruct ScrollBar\n{\n    ~ScrollBar() {\n        std::cout << \"--ScrollBar destroyed\\n\";\n    }\n    void setPanel(const std::shared_ptr<Panel> panel) {\n        m_panel=panel;\n    }\n    std::shared_ptr<Panel> m_panel;\n};\nstruct TextWindow\n{\n    ~TextWindow() {\n        std::cout << \"--TextWindow destroyed\\n\";\n    }\n    void setPanel(const std::shared_ptr<Panel> panel) {\n        m_panel=panel;\n    }\n    std::shared_ptr<Panel> m_panel;\n};\nvoid run_app()\n{\n    std::shared_ptr<Panel> panel = std::make_shared<Panel>();\n    std::shared_ptr<ScrollBar> scrollbar = std::make_shared<ScrollBar>();\n    std::shared_ptr<TextWindow> textwindow = std::make_shared<TextWindow>();\n    scrollbar->setPanel(panel);\n    textwindow->setPanel(panel);\n    panel->setScroll(scrollbar);\n    panel->setText(textwindow);\n}\nint main()\n{\n    std::cout << \"Starting app\\n\";\n    run_app();\n    std::cout << \"Exited app\\n\";\n    return 0;\n}\n```\n\n执行时，它输出以下内容:\n\n![Figure 3.25: Weak pointer program output](img/C14583_03_25.jpg)\n\n###### 图 3.25:弱指针程序输出\n\n这表明当应用退出时，面板和`文本窗口`没有被破坏。这是因为他们彼此都持有`共享 _ptr`，因此两者的参考计数不会归零并触发销毁。如果我们用图解法描述这个结构，那么我们可以看到它有一个`共享 _ptr`循环:\n\n![Figure 3.26: weak_ptr and shared_ptr cycles](img/C14583_03_26.jpg)\n\n###### 图 3.26:弱 _ptr 和共享 _ptr 周期\n\n### 智能指针和调用函数\n\n既然我们可以管理我们的资源，我们如何使用它们？我们传递聪明的指针吗？当我们有一个智能指针(`unique_ptr`或`shared_ptr`时，我们在调用函数时有四个选项:\n\n*   按值传递智能指针\n*   通过引用传递智能指针\n*   通过指针传递托管资源\n*   通过引用传递托管资源\n\n这不是一份详尽的清单，但却是需要考虑的主要清单。如何传递智能指针或其资源的答案取决于我们调用函数的意图:\n\n*   函数的意图是仅仅使用资源吗？\n*   该函数是否拥有资源的所有权？\n*   该函数是否替换托管对象？\n\n如果函数只是去`使用资源`，那么它甚至不需要知道它正在被交给一个托管资源。它只需要使用它，并且应该通过指针使用资源来调用，或者通过引用使用资源(或者甚至通过值使用资源):\n\n```cpp\ndo_something(Resource* resource);\ndo_something(Resource& resource);\ndo_something(Resource resource);\n```\n\n如果您想将资源的所有权**传递给函数，那么该函数应该由智能指针通过值来调用，并使用 **std::move()** 来调用:**\n\n```cpp\ndo_something(std::unique_ptr<Resource> resource);\nauto res = std::make_unique<Resource>();\ndo_something (std::move(res));\n```\n\n当`do _ 某物()`返回时，`res`变量将为空，资源现在归`do _ 某物()`所有。\n\n如果你想`替换被管理对象`(一个称为**重新拔插**的过程)，那么你通过引用传递智能指针:\n\n```cpp\ndo_something(std::unique_ptr<Resource>& resource);\n```\n\n下面的程序将所有这些放在一起演示每个场景以及如何调用函数:\n\n```cpp\n#include <iostream>\n#include <memory>\n#include <string>\n#include <sstream>\nclass Resource\n{\npublic:\n  Resource() { std::cout << \"+++ Resource acquired [\"<< m_id <<\"]\\n\"; }\n  ~Resource() { std::cout << \"---Resource released [\"<< m_id <<\"]\\n\"; }\n  std::string name() const {\n      std::ostringstream ss;\n      ss << \"the resource [\" << m_id <<\"]\";\n      return ss.str();\n  }\n  int m_id{++ m_count};\n  static int m_count;\n};\nint Resource::m_count{0};\nvoid use_resource(Resource& res)\n{\n    std::cout << \"Enter use_resource\\n\";\n    std::cout << \"...using \" << res.name() << \"\\n\";\n    std::cout << \"Exit use_resource\\n\";\n}\nvoid take_ownership(std::unique_ptr<Resource> res)\n{\n    std::cout << \"Enter take_ownership\\n\";\n    if (res)\n        std::cout << \"...taken \" << res->name() << \"\\n\";\n    std::cout << \"Exit take_ownership\\n\";\n}\nvoid reseat(std::unique_ptr<Resource>& res)\n{\n    std::cout << \"Enter reseat\\n\";\n    res.reset(new Resource);\n    if (res)\n        std::cout << \"...reseated \" << res->name() << \"\\n\";\n    std::cout << \"Exit reseat\\n\";\n}\nint main()\n{\n  std::cout << \"Starting...\\n\";\n  auto res = std::make_unique<Resource>();\n  // Use - pass resource by reference\n  use_resource(*res);               \n  if (res)\n    std::cout << \"We HAVE the resource \" << res->name() << \"\\n\\n\";\n  else\n    std::cout << \"We have LOST the resource\\n\\n\";\n  // Pass ownership - pass smart pointer by value\n  take_ownership(std::move(res));    \n  if (res)\n    std::cout << \"We HAVE the resource \" << res->name() << \"\\n\\n\";\n  else\n    std::cout << \"We have LOST the resource\\n\\n\";\n  // Replace (reseat) resource - pass smart pointer by reference\n  reseat(res);                      \n  if (res)\n    std::cout << \"We HAVE the resource \" << res->name() << \"\\n\\n\";\n  else\n    std::cout << \"We have LOST the resource\\n\\n\";\n  std::cout << \"Exiting...\\n\";\n  return 0;\n}\n```\n\n当我们运行这个程序时，我们会收到以下输出:\n\n![](img/C14583_03_27.jpg)\n\n###### 图 3.27:所有权传递程序输出\n\n#### 注意\n\n*C++ 核心指南*有一整节关于*资源管理*，智能指针，以及如何使用它们这里:[http://isocpp . github . io/cppcoreiders/cppcoreiders # S-resource](http://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#S-resource)。我们只触及了准则所涵盖的最重要的方面。\n\n### 练习 4:用 STL 智能指针实现 RAII\n\n在本练习中，我们将实现一个传感器工厂方法，通过`unique_ptr`返回传感器资源。我们将实现一个`unique_ptr`来保存一个数组，然后开发代码将一个`unique_ptr`转换成一个共享指针，然后再共享一些。按照以下步骤实施本练习:\n\n1.  在 Eclipse 中打开**第 3 课**项目。然后在**项目浏览器**中，展开**第 3 课**，然后展开**练习 04** ，双击**练习 4.cpp** 将本练习的文件打开到编辑器中。\n2.  点击**启动配置**下拉菜单，选择**新启动配置……**。从**搜索项目**菜单中配置**L3 练习 4** 应用，使其以名称**L3 练习 4** 运行。\n3.  Click on the **Run** button to run Exercise 4\\. This will produce the following output:\n\n    ![Figure 3.28: Exercise 4 output](img/C14583_03_28.jpg)\n\n    ###### 图 3.28:练习 4 的输出\n\n4.  In the editor, examine the code, particularly the factory method, that is, `createSensor(type)`.\n\n    ```cpp\n    std::unique_ptr<ISensor>\n    createSensor(SensorType type)\n    {\n        std::unique_ptr<ISensor> sensor;\n        if (type == SensorType::Light)\n        {\n            sensor.reset(new LightSensor);\n        }\n        else if (type == SensorType::Temperature)\n        {\n            sensor.reset(new TemperatureSensor);\n        }\n        else if (type == SensorType::Pressure)\n        {\n            sensor.reset(new PressureSensor);\n        }\n        return sensor;\n    }\n    ```\n\n    这将创建一个名为传感器的空的唯一指针，然后根据传入的`类型`用所需的传感器重置包含的指针。\n\n5.  在编辑器中打开练习 4.cpp，将文件顶部附近的行改为如下所示:\n\n    ```cpp\n    #define EXERCISE4_STEP  5\n    ```\n\n6.  Click on the **Run** button to compile the code, which will fail with the following error:\n\n    ![Figure 3.29: Compiler error for Step 5](img/C14583_03_29.jpg)\n\n    ###### 图 3.29:步骤 5 的编译器错误\n\n    完整的错误消息如下:\n\n    ```cpp\n    error: conversion from 'std::unique_ptr<ISensor>' to non-scalar type 'SensorSPtr {aka std::shared_ptr<ISensor>}' requested\n    ```\n\n    根据错误，我们试图将`唯一 _ptr`分配给`共享 _ptr`，这是不允许的。\n\n7.  找到报告错误的行，并将其改为如下内容:\n\n    ```cpp\n    SensorSPtr light2 = std::move(light);\n    ```\n\n8.  Click on the **Run** button to compile and run the program. The output is as follows:\n\n    ![Figure 3.30: Successful output for Exercise 4 (after EXERCISE4_STEP = 5)](img/C14583_03_30.jpg)\n\n    ###### 图 3.30:练习 4 的成功输出(练习 4_STEP = 5 之后)\n\n    前面的输出显示，我们创建了三个不同的传感器，光传感器指针从持有资源到移动，并且**光 2** 共享指针有两个所有者。等等！什么事？两个主人？但是我们所做的只是将资源从`light`(一个`unique_ptr`)移动到`light2`(一个`shared_ptr`)。问题实际上是模板方法:\n\n    ```cpp\n    template<typename SP>\n    void printSharedPointer(SP sp, const char* message)\n    ```\n\n    第一个参数是通过值传递的，这意味着将创建一个新的`shared_ptr`副本并传递给方法进行打印。\n\n9.  Let's fix that now by changing the template to pass-by-reference. Click on the **Run** button to compile and run the program. The following output is generated:\n\n    ![Figure 3.31: Corrected printSharedPointer output](img/C14583_03_31.jpg)\n\n    ###### 图 3.31:已更正的 printSharedPointer 输出\n\n10.  在编辑器中打开**练习 4.cpp** ，将文件顶部附近的行改为这样:\n\n    ```cpp\n    #define EXERCISE4_STEP  12\n    ```\n\n11.  Click on the **Run** button to compile and run the program. The following output is generated:\n\n    ![](img/C14583_03_32.jpg)\n\n    ###### 图 3.32:练习 4 的注释步骤 12 输出\n\n12.  将输出与`测试传感器()`方法中的代码进行比较。我们会发现我们可以很容易地分配给一个空的`unique_ptr` ( `light`)并且我们可以从一个`shared_ptr`分配给另一个(`light3 = light2`)而不需要`std::move()`。\n13.  在编辑器中打开**练习 4.cpp** ，将文件顶部附近的行改为这样:\n\n    ```cpp\n    #define EXERCISE4_STEP  15\n    ```\n\n14.  Click on the **Run** button to compile and run the program. The output switches to the following:\n\n    ![Figure 3.33: Managing arrays in unique_ptr](img/C14583_03_33.jpg)\n\n    ###### 图 3.33:在 unique_ptr 中管理阵列\n\n15.  Open the editor and find the `testArrays()` method:\n\n    ```cpp\n    void testArrays()\n    {\n        std::unique_ptr<int []> board = std::make_unique<int []>(8*8);\n        for(int i=0  ; i<8 ; i++)\n            for(int j=0 ; j<8 ; j++)\n                board[i*8+j] = 10*(i+1)+j+1;\n        for(int i=0  ; i<8 ; i++)\n        {\n            char sep{' '};\n            for(int j=0 ; j<8 ; j++)\n                std::cout << board[i*8+j] << sep;\n            std::cout << \"\\n\";\n        }\n    }\n    ```\n\n    这段代码中有几点需要注意。首先，类型被声明为 **int[]** 。我们选择了 **int** 进行本练习，但它可以是任何类型。其次，当**unique _ ptr**(c++ 17 中的 **shared_ptr** )用于管理数组时，定义**运算符[]** 。因此，我们通过从二维索引的板[i*8+j] 计算一维索引来模拟二维数组。\n\n16.  编辑方法第一行，声明`自动`类型:\n\n    ```cpp\n    auto board = std::make_unique<int []>(8*8);\n    ```\n\n17.  点击**运行**按钮编译并运行程序——输出将与前一次运行相同。在这种情况下，auto 非常有用，因为您不再需要在类型声明中键入所有细节，也不再需要调用`make_unique()`。\n\n在本练习中，我们实现了一个工厂功能，该功能使用`unique_ptr`来管理传感器的寿命，从而为制造的传感器提供服务。然后我们实现了代码，将它从一个`unique_ptr`更改为几个对象。最后，我们开发了一种`独特的 _ptr`技术来使用一维数组管理多维数组。\n\n### 零/五法则——不同的视角\n\n当我们引入 **BufferedWriter** 时，它有两个被管理的资源:内存和一个文件。然后，我们讨论了默认编译器如何生成被称为浅拷贝的拷贝操作。我们讨论了如何以不同的方式管理资源—停止拷贝、执行深度拷贝或转移所有权。我们在这种情况下决定做的事情被称为资源管理策略。你选择的政策会影响你如何执行零/五的**规则。**\n\n在资源管理方面，一个类可以不管理任何资源，管理一个可以复制但不能移动的资源，管理一个可以移动但不能复制的资源，或者管理一个既不能复制也不能移动的资源。下列类别显示了如何表达这些内容:\n\n```cpp\nstruct NoResourceToManage\n{\n    // use compiler generated copy & move constructors and operators\n};\nstruct CopyOnlyResource\n{\n    ~CopyOnlyResource()                                      {/* defined */ }\n    CopyOnlyResource(const CopyOnlyResource& rhs)            {/* defined */ }\n    CopyOnlyResource& operator=(const CopyOnlyResource& rhs) {/* defined */ }\n    CopyOnlyResource(CopyOnlyResource&& rhs) = delete;\n    CopyOnlyResource& operator=(CopyOnlyResource&& rhs) = delete;\n};\nstruct MoveOnlyResource\n{\n    ~MoveOnlyResource()                                      {/* defined */ }\n    MoveOnlyResource(const MoveOnlyResource& rhs)             = delete;\n    MoveOnlyResource& operator=(const MoveOnlyResource& rhs)  = delete;\n    MoveOnlyResource(MoveOnlyResource&& rhs)                 {/* defined */ }  \n    MoveOnlyResource& operator=(MoveOnlyResource&& rhs)      {/* defined */ }\n};\nstruct NoMoveOrCopyResource\n{\n    ~NoMoveOrCopyResource()                                  {/* defined */ }\n    NoMoveOrCopyResource(const NoMoveOrCopyResource& rhs)             = delete;\n    NoMoveOrCopyResource& operator=(const NoMoveOrCopyResource& rhs)  = delete;\n    NoMoveOrCopyResource(NoMoveOrCopyResource&& rhs)                  = delete;\n    NoMoveOrCopyResource& operator=(NoMoveOrCopyResource&& rhs)       = delete;\n};\n```\n\n由于在所有上下文和异常情况下管理资源的复杂性，最佳实践是，如果一个类负责管理资源，那么该类只负责管理该资源。\n\n### 活动 1:用 RAII 和 Move 实现图形处理\n\n在*章 2A**不准鸭子入内-类型与演绎*中，你的团队努力工作，得到了`点 3d`和`矩阵 3d`的实施。现在，您的公司想要营销该库，在他们做到这一点之前，它需要两大改进:\n\n*   这些类必须在我们公司的命名空间中，即高级 Plus Inc .中。因此，图形的命名空间将是`accp::gfx`。\n*   `点 3d`和`矩阵 3d`中矩阵的存储是类的固有部分，因此它是从堆栈而不是堆中分配的。作为库矩阵支持的发展，我们需要从堆中分配内存。当我们致力于在未来的版本中实现更大的矩阵时，我们也希望在我们的类中引入移动语义。\n\n按照以下步骤实现:\n\n1.  从我们当前版本的库开始(可以在**第 3 课/练习 01** 文件夹中找到)，将我们所有的类放入`acpp::gfx`命名空间。\n2.  修复所有因为变更而失败的测试。(失败可能意味着编译失败，而不仅仅是运行测试。)\n3.  在`Matrix3d`中，从直接在类中声明矩阵切换到堆分配的内存进行存储。\n4.  通过实现复制构造函数和复制赋值操作符的深度复制实现来修复失败的测试。进行任何其他必要的更改，以适应新的内部表示。请注意，您不需要修改任何测试来让它们通过，它们只访问公共接口，这意味着我们可以在不影响客户端的情况下更改内部结构。\n5.  通过在返回语句中使用`std::move`在`CreateTranslationMatrix()`中强制调用移动构造函数来触发另一个失败。在`Matrix3d`类中介绍所需的移动操作，以使测试能够编译并通过。\n6.  对`点 3d`重复步骤 3 至 4。\n\n在执行了前面的步骤之后，预期的输出从一开始就不会改变:\n\n![Figure 3.34: Activity 1 output after successful conversion to use RAII](img/C14583_03_34.jpg)\n\n###### 图 3.34:成功转换为使用 RAII 后的活动 1 输出\n\n#### 注意\n\n这个活动的解决方案可以在第 657 页找到。\n\n### 什么时候调用函数？\n\nC++ 程序执行的所有操作本质上都是函数调用(尽管编译器可能会将它们优化为内联操作序列)。然而，由于**语法糖**，你正在进行函数调用可能并不明显。语法糖是编程语言中的语法，它使阅读或表达变得更容易。比如你写`a = 2 + 5`的时候，本质上是在调用`运算符=( & a，运算符+(2，5))`。只是这种语言允许我们编写第一种形式，但第二种形式允许我们重载运算符，并将这些功能扩展到用户定义的类型。\n\n以下机制导致对函数的调用:\n\n*   对函数的显式调用。\n*   所有运算符，如+、-、*、/、%等，以及 new/delete。\n*   变量声明–如果存在初始化值，将导致用参数调用构造函数。\n*   用户定义的文字–我们没有处理这些，但是本质上，我们为`类型运算符“【名称(参数)`”定义了一个重载。然后我们可以编写诸如 10_km 这样的东西，这使得我们的代码更容易理解，因为它携带了语义信息。\n*   从一个值到另一个值的铸造(`静态 _ 铸造< >`，`const _ 铸造< >`，`重新解释 _ 铸造< >`，以及`动态 _ 铸造< >`)。同样，我们还有另一个运算符重载，它允许我们从一种类型转换为另一种类型。\n*   在函数重载期间，可能需要将一种类型转换为另一种类型，以便它与函数原型相匹配。它可以通过调用具有正确参数类型的构造函数来创建临时的，或者通过隐式调用的强制转换操作符来实现。\n\n编译器中的每一个结果都决定了一个函数必须被调用。在确定需要调用函数后，它必须找到与名称和参数匹配的函数。这是我们将在下一节讨论的内容。\n\n### 调用哪个函数\n\n在*章节 2A**不允许鸭子-类型和演绎*中，我们看到功能过载解析执行如下:\n\n![Figure 3.35: Function overload resolution](img/C14583_03_35.jpg)\n\n###### 图 3.35:函数霸王解析\n\n我们真正没有深入研究的是名称查找的概念。在某个时候，编译器会遇到对`函数`函数的以下调用:\n\n```cpp\nfunc(a, b);\n```\n\n当这种情况发生时，它必须将其名称与引入它的声明相关联。这个过程叫做**名称查找**。对于程序中的所有项目(变量、名称空间、类、函数、函数模板和模板)，这种名称查找是正确的。对于要编译的程序，变量、名称空间和类的名称查找过程必须生成一个声明。但是，对于函数和函数模板，编译器可能会将多个具有相同名称的声明关联起来——主要是通过函数重载，由于**依赖于参数的查找** ( **ADL** )，函数重载可能会被扩展以考虑其他函数。\n\n### 标识符\n\n按照 C++ 标准的定义，**标识符**是由大写和小写拉丁字母、数字、下划线和大多数 Unicode 字符组成的序列。有效的标识符必须以非数字字符开头，并且长度任意且区分大小写。每个角色都很重要。\n\n### 名称\n\n**名称**用于指代实体或标签。名称是以下形式之一:\n\n*   标识符\n*   函数符号中的重载运算符名称(例如运算符-，运算符删除)\n*   模板名称后跟其参数列表(向量<int>)</int>\n*   用户定义的转换函数名(运算符 float)\n*   用户定义的文字运算符名称(运算符\" \" _ms)\n\n每个实体及其名称都是通过声明引入的，而标签的名称是通过**转到**语句或通过带标签的语句引入的。一个名称可以在一个文件(或翻译单元)中多次使用，以根据范围引用不同的实体。根据链接的不同，一个名称也可以用来指代多个文件(翻译单元)中的同一个实体，或者不同的实体。编译器使用名称查找通过**名称查找**将引入名称的声明与程序中的未知名称相关联。\n\n### 名称查找\n\n名称查找过程是两个过程之一，根据上下文进行选择:\n\n*   **限定名查找**:名称出现在范围解析运算符`::`的右侧，或者可能出现在`:`之后，后跟`模板`关键字。限定名可以指命名空间成员、类成员或枚举数。`::`运算符左侧的名称定义了查找名称的范围。如果没有名称，则使用全局命名空间。\n*   **不合格名称查找**:其他一切。在这种情况下，名称查找检查当前范围和所有封闭范围。\n\n如果未限定的名称留在函数调用运算符“`)(`”中，则它使用依赖于参数的查找。\n\n### 依赖于参数的查找\n\n查找非限定函数名的规则集被称为`依赖于参数的查找`(称为 ADL)，或`柯尼格查找`(以安德鲁·克尼格命名，他定义了它，并且是 C++ 标准委员会的长期成员)。非限定函数名可以作为函数调用表达式出现，也可以作为对重载运算符的隐式函数调用的一部分出现。\n\nADL 基本上说，除了在非限定名称查找时考虑的范围和命名空间之外，还考虑了所有参数和模板参数的“关联命名空间”。考虑以下代码:\n\n```cpp\n#include <iostream>\n#include <string>\nint main()\n{\n    std::string welcome{\"Hello there\"};\n    std::cout << welcome;\n    endl(std::cout);\n}\n```\n\n当我们编译并运行这段代码时，输出如预期的那样:\n\n```cpp\n$ ./adl.exe\nHello there\n$\n```\n\n这是一种不同寻常的编写程序的方法。通常，它会这样写:\n\n```cpp\n#include <iostream>\n#include <string>\nint main()\n{\n    std::string welcome{\"Hello there\"};\n    std::cout << welcome << std::endl;\n}\n```\n\n我们在用调用`endl()`的奇怪方法来展示 ADL。但是这里有两个 ADL 查找。\n\n第一个经历 ADL 的函数调用是`std::cout < < welcome`，编译器认为是`运算符< < (std::cout，welcome)`。名称操作符< <现在可以在可用的范围及其参数的名称空间中找到–`标准`。这个额外的命名空间将名称解析为自由方法，即在字符串头中声明的`STD::operator<<(ostream&OS，string & s)`。\n\n第二个调用更明显`endl(std::cout)`。同样，编译器可以访问 std 名称空间来解析这个名称查找，并在标题`中找到 **std::endl**`**模板**(包含在`iostream`中)。\n\n没有 ADL，编译器无法找到这两个函数，因为它们是由 iostream 和 string 包提供给我们的自由函数。插入符的魔力(<STD::operator<T5】(STD::cout，欢迎)。如果你考虑链式插入，那就更糟了。或者，您可以使用命名空间 std 编写“**；**”。这两个选项都不理想，这就是为什么我们需要 ADL(柯尼格查找)。\n\n### 买者自负\n\n我们已经看到了 ADL 如何通过包含与函数参数类型相关的名称空间来让程序员的生活变得更容易。然而，这种查找功能并不是没有风险的，在很大程度上，我们可以将风险降到最低。考虑以下示例代码:\n\n```cpp\n#include <iostream>\nnamespace mylib \n{\nvoid is_substring(std::string superstring, std::string substring)\n{\n    std::cout << \"mylib::is_substring()\\n\";\n}\nvoid contains(std::string superstring, const char* substring) {\n    is_substring(superstring, substring);\n}\n}\nint main() {\n    mylib::contains(\"Really long reference\", \"included\");\n}\n```\n\n当我们编译并运行前面的程序时，我们得到了预期的输出:\n\n![Figure 3.36: ADL Sample program output](img/C14583_03_36.jpg)\n\n###### 图 3.36: ADL 示例程序输出\n\nC++ 标准委员会随后决定引入一个`is_substring()`函数，如下所示:\n\n```cpp\nnamespace std {\nvoid is_substring(std::string superstring, const char* substring)\n{\n    std::cout << \"std::is_substring()\\n\";\n}\n}\n```\n\n如果我们将它添加到文件的顶部，编译并重新运行它，我们现在会得到以下输出:\n\n![Figure 3.37: ADL issue program output](img/C14583_03_37.jpg)\n\n###### 图 3.37: ADL 发布程序输出\n\n得益于 ADL，(下一个 C++ 标准)编译器选择了不同的实现，更适合`is_substring()`的非限定函数调用。因为参数的隐式转换，所以不会发生冲突，这种冲突会导致歧义和编译器错误。它只是默默地采用新的方法，如果参数顺序不同，这可能会导致微妙和难以发现的错误。编译器只能检测类型和语法差异，而不能检测语义差异。\n\n#### 注意\n\n为了演示 ADL 是如何工作的，我们将我们的函数添加到了 std 名称空间中。名称空间用于分离关注点，并添加到其他人的名称空间中，特别是`标准库名称空间` ( `std`)不是好的做法。\n\n那么，为什么要买者自负(买家当心)？如果您在开发中使用第三方库(包括 C++ 标准库)，那么当您升级库时，您需要确保对接口的更改不会因为 ADL 而给您带来问题。\n\n### 练习 5:实施模板以防止日常生活能力问题\n\n在本练习中，我们将演示 C++ 17 STL 中的一个突破性变化，它可能会在野外引起一个问题。C++ 11 为`std::begin(type)`和朋友介绍了模板。作为开发人员，这是通用接口的一个吸引人的表达，您可能已经为 size(类型)和 empty(类型)编写了自己的版本。按照以下步骤实施本练习:\n\n1.  在 Eclipse 中打开**第 3 课**项目。然后在**项目浏览器**中，展开**第 3 课**，然后展开**练习 05** ，双击**练习 5.cpp** 将本练习的文件打开到编辑器中。\n2.  点击**启动配置**下拉菜单，选择**新启动配置……**。从“搜索项目”菜单中配置**L3 练习 5** 应用，使其以名称**L3 练习 5** 运行。\n3.  Click on the **Run** button to run Exercise 5\\. This will produce the following output:\n\n    ![Figure 3:38: Successful execution of Exercise 5](img/C14583_03_38.jpg)\n\n    ###### 图 3:38:成功执行练习 5\n\n4.  对代码的检查揭示了两个助手模板:\n\n    ```cpp\n    template<class T>\n    bool empty(const T& x)\n    {\n        return x.empty();\n    }\n    template<class T>\n    int size(const T& x)\n    {\n        return x.size();\n    }\n    ```\n\n5.  与所有其他练习不同，本练习被配置为在 C++ 14 下构建。打开**第 3 课**下的 **CMakeLists.txt** 文件，找到以下行:\n\n    ```cpp\n    set_property(TARGET L3Exercise5 PROPERTY CXX_STANDARD 14)\n    ```\n\n6.  将`14`改为`17`。\n7.  Click on the **Run** button to compile the exercise which now fails:\n\n    ![Figure 3.39: Compilation fails under C++ 17 – ambiguous function call](img/C14583_03_39.jpg)\n\n    ###### 图 3.39:在 C++ 17 下编译失败——模糊的函数调用\n\n8.  因为`empty()`和`size()`模板的参数是一个 std::vector，所以 ADL 引入了这些模板新包含的 STL 版本，并破坏了我们的代码。\n9.  在**练习 5.cpp** 文件中，找到产生错误的两次出现的`空()`和两次出现的`大小()`，并在它们之前插入两个冒号“`::`”(范围说明符)。\n10.  点击**运行**按钮，编译并运行练习。它现在愉快地编译并再次运行，因为对`空()`和`大小()`函数的调用现在被限定了。我们可以同样指定`标准::`范围。\n\n在本练习中，我们在全局命名空间中实现了两个模板函数，如果我们在 C++ 14 标准下编译程序，这两个模板函数可以很好地工作。然而，当我们在 C++ 17 下编译时，我们的实现崩溃了，因为 STL 库发生了变化，我们不得不改变我们的实现，以确保编译器找到并使用我们编写的模板。\n\n### 隐式转换\n\n在*图 3.36* 、*函数重载解析*中确定候选函数集时，编译器必须查看名称查找过程中找到的所有可用函数，并确定参数号和类型是否与调用点匹配。在确定类型是否匹配时，它还将检查所有可用的转换，以确定是否有从类型 T1 类型(传递的参数的类型)转换为 T2 类型(为函数参数指定的类型)的机制。如果它可以将所有参数从 T1 转换为 T2，那么它会将该函数添加到候选集。\n\n这种从 T1 类型到 T2 类型的转换被称为**隐式转换**，当在不接受该类型但接受其他类型(T2)的表达式或上下文中使用 T1 类型时，就会发生这种转换。这发生在以下环境中:\n\n*   当调用以 T2 为参数声明的函数时，T1 作为参数传递。\n*   T1 用作期望 T2 的运算符的操作数。\n*   T1 用于初始化 T2 的一个新对象(包括返回语句)。\n*   T1 用在`开关`语句中(在这种情况下，T2 是一个整数)。\n*   T1 用于 if 语句或 **do-while** 或 **while** 循环(其中 T2 是布尔)。\n\n如果存在从 T1 到 2 的明确转换序列，那么程序将编译。内置类型之间的转换通常由通常的算术转换决定。\n\n### 显式–防止隐式转换\n\n隐式转换是一个很好的特性，它使得程序员能够表达他们的意图，而且它在大多数时候都是有效的。然而，编译器在没有程序员提供提示的情况下将一种类型转换成另一种类型的能力并不总是令人满意的。考虑以下小程序:\n\n```cpp\n#include <iostream>\nclass Real\n{\npublic:\n    Real(double value) : m_value{value} {}\n    operator float() {return m_value;}\n    float getValue() const {return m_value;}\nprivate:\n    double m_value {0.0};\n};\nvoid test(bool result)\n{\n    std::cout << std::boolalpha;\n    std::cout << \"Test => \" << result << \"\\n\";\n}\nint main()\n{\n    Real real{3.14159};\n    test(real);\n    if ( real ) \n    {\n        std::cout << \"true: \" << real.getValue() << \"\\n\";\n    }\n    else\n    {\n        std::cout << \"false: \" << real.getValue() << \"\\n\";\n    }\n}\n```\n\n当我们编译它并运行前面的程序时，我们会得到以下输出:\n\n![Figure 3.40: Implicit conversion sample program output](img/C14583_03_40.jpg)\n\n###### 图 3.40:隐式转换示例程序输出\n\n嗯，这可能有点出乎意料，这编译并实际产生了一个输出。**实数**变量属于**实数**类型，它有一个要浮动的转换运算符–**运算符 float()** 。 **test()** 函数以一个 **bool** 作为参数，如果条件一定会导致一个 **bool** 。如果数值为零，编译器会将任何数值类型转换为值为 false 的**布尔**类型，如果数值不为零，则转换为 true。但是，如果这不是我们想要的行为，我们可以通过在函数声明前加上显式关键字来防止它。假设我们更改了行，它的内容如下:\n\n```cpp\nexplicit operator float() {return m_value;}\n```\n\n如果我们现在试图编译它，我们会得到两个错误:\n\n![Figure 3.41: Compile errors because implicit conversion was removed.](img/C14583_03_41.jpg)\n\n###### 图 3.41:编译错误，因为隐式转换被移除了。\n\n这两种情况都与无法将实数类型转换为布尔值有关–首先，在调用点进行`测试()`，然后在 if 条件下进行。\n\n现在，让我们引入一个 bool 转换运算符来解决这个问题。\n\n```cpp\noperator bool() {return m_value == 0.0;}\n```\n\n我们现在可以再次构建程序。我们将收到以下输出:\n\n![Figure 3.42: Introducing the bool operator replaces implicit conversion](img/C14583_03_42.jpg)\n\n###### 图 3.42:引入 bool 运算符代替隐式转换\n\n`布尔`值现在为假，而以前为真。这是因为浮点转换返回的值的隐式转换不是零，然后转换为 true。\n\n从 C++ 11 开始，所有的构造函数(复制和移动构造函数除外)都被认为是转换构造函数。这意味着，如果它们不是用显式声明的，那么它们可用于隐式转换。同样，任何未声明为显式的转换运算符都可以用于隐式转换。\n\n`C++ 核心指南`有两条与隐式转换相关的规则:\n\n*   **C.46** :默认情况下，将单参数构造函数声明为显式的\n*   **C.164** :避免隐式转换运算符\n\n### 上下文转换\n\n如果我们现在对我们的小程序做一个进一步的改变，我们可以进入所谓的上下文转换。让我们明确 bool 运算符，并尝试编译程序:\n\n```cpp\nexplicit operator bool() {return m_value == 0.0;}\n```\n\n我们将收到以下输出:\n\n![Figure 3.43: Compile errors with explicit bool operator](img/C14583_03_43.jpg)\n\n###### 图 3.43:使用显式 bool 运算符编译错误\n\n这次我们在调用`test()`的地方只有一个错误，但不是 if 条件。我们可以通过使用 C 风格的 case (bool)或 c++ `static _ cast<bool>(real)`(这是首选方法)来修复此错误。当我们添加强制转换时，程序会再次编译并运行。\n\n那么，如果 bool 强制转换是显式的，那么 if 表达式的条件为什么不需要强制转换呢？\n\nC++ 标准允许在某些上下文中使用`bool`类型，并且存在 bool 转换的声明(无论是否标记为显式)。如果发生这种情况，则允许隐式转换。这在上下文中被称为**转换为布尔**，并且可能发生在以下上下文中:\n\n*   `的条件(或控制表达)如果`、`而`、`为`\n*   内置逻辑运算符的操作数:`！`(不是)`& &`(和)和`||`(或)\n*   三进制(或条件)运算符的第一个操作数`？:`。\n\n### 练习 6:隐式和显式转换\n\n在本练习中，我们将尝试调用函数、隐式转换、防止它们以及启用它们。按照以下步骤实施本练习:\n\n1.  在 Eclipse 中打开**第 3 课**项目。然后在**项目浏览器**中，依次展开**第 3 课**和**练习 06** ，双击**练习 6.cpp** 将本练习的文件打开到编辑器中。\n2.  点击**启动配置**下拉菜单，选择**新启动配置……**。从**搜索项目**菜单中配置**l3 锻炼 6** 应用，使其以名称**l3 锻炼 6** 运行。\n3.  Click on the **Run** button to run Exercise 6\\. This will produce the following output:\n\n    ![Figure 3.44: Default output from Exercise 6](img/C14583_03_44.jpg)\n\n    ###### 图 3.44:练习 6 的默认输出\n\n4.  在文本编辑器中，将`电压`的构造函数更改为`显式` :\n\n    ```cpp\n    struct Voltage\n    {\n        explicit Voltage(float emf) : m_emf(emf) \n        {\n        }\n        float m_emf;\n    };\n    ```\n\n5.  Click on the **Run** button to recompile the code – now we get the following error:\n\n    ![Figure 3.45: Failed conversion of int to Voltage](img/C14583_03_45.jpg)\n\n    ###### 图 3.45:整数到电压的转换失败\n\n6.  从构造函数中移除显式，并将`计算`函数改为引用:\n\n    ```cpp\n    void calculate(Voltage& v)\n    ```\n\n7.  Click on the **Run** button to recompile the code – now, we get the following error:\n\n    ![](img/C14583_03_46.jpg)\n\n    ###### 图 3.46:整数到电压&\n\n    同一行有我们之前运行的问题，但原因不同。所以，*隐式转换只适用于值类型*。\n\n8.  注释掉产生错误的行，然后在调用`后，使用 _float(42)`，添加以下行:\n\n    ```cpp\n    use_float(volts);\n    ```\n\n9.  Click on the **Run** button to recompile the code – now we get the following error:\n\n    ![Figure 3.47: Failed conversion of Voltage to float](img/C14583_03_47.jpg)\n\n    ###### 图 3.47:电压转换为浮动失败\n\n10.  现在，将以下铸造操作员添加到`电压`等级:\n\n    ```cpp\n    operator float() const\n    {\n        return m_emf;\n    }\n    ```\n\n11.  Click on the **Run** button to recompile the code and run it:\n\n    ![Figure 3.48: Successfully converted Voltage to float](img/C14583_03_48.jpg)\n\n    ###### 图 3.48:成功将电压转换为浮动\n\n12.  Now, place the `explicit` keyword in front of the cast that we just added and click on the **Run** button to recompile the code. Again, we get an error:\n\n    ![Figure 3.49: Failure to convert Voltage to float](img/C14583_03_49.jpg)\n\n    ###### 图 3.49:无法将电压转换为浮动\n\n13.  通过将显式声明添加到强制转换中，我们可以防止编译器使用转换运算符。更改带有错误的行，将伏特变量转换为浮点数:\n\n    ```cpp\n    use_float(static_cast<float>(volts));\n    ```\n\n14.  点击**运行**按钮重新编译代码并运行。\n\n![Figure 3.50: Conversion of Voltage into float with cast works again](img/C14583_03_50.jpg)\n\n###### 图 3.50:电压转换成浮子，再次铸造\n\n在本练习中，我们已经看到隐式转换可以发生在类型之间(而不是引用之间)，并且我们可以控制它们何时发生。现在我们知道如何控制这些转换，我们可以努力满足之前引用的指南`C.46`和`C.164`。\n\n### 活动 2:实现日期计算类离子\n\n您的团队负责开发一个库来帮助进行与日期相关的计算。特别是，我们希望能够确定两个日期和给定日期之间的天数，增加(或减去)天数以获得新的日期。本活动将开发两种新类型，并对它们进行增强，以确保程序员不会意外地让它们与内置类型进行交互。按照以下步骤实现:\n\n1.  设计并实现一个`日期`类，将`日`、`月`和`年`存储为整数。\n2.  添加访问内部日、月和年值的方法。\n3.  定义一个类型，`date_t`来表示自 1970 年 1 月 1 日`纪元日期`以来的天数。\n4.  向`Date`类添加一个方法，将其转换为`date_t`。\n5.  从`日期 _t`值中添加一个设置`日期`类的方法。\n6.  创建一个存储天数值的`Days`类。\n7.  在以`日`为自变量的`日`上加上`加法`运算符。\n8.  使用`显式`防止数字相加。\n9.  加上`减法`运算符，从两个`日期`的`差值`中返回一个`天数`值。\n\n完成这些步骤后，您应该会收到以下输出:\n\n![Figure 3.51: Output of a successful Date sample application](img/C14583_03_51.jpg)\n\n###### 图 3.51:成功的日期示例应用的输出\n\n#### 注意\n\n这项活动的解决方案可以在第 664 页找到。\n\n## 苏〔t0〕麦理\n\n在这一章中，我们探讨了变量的生命周期——自动的和动态的，它们存储在哪里，以及何时被析构。然后，我们使用这些信息开发了 **RAII** 技术，该技术允许我们几乎忽略资源管理，因为即使在出现异常的情况下，自动变量也会在它们被析构时清理它们。然后，我们研究了抛出异常并捕捉它们，以便我们可以在正确的级别处理异常情况。从 **RAII** 开始，我们开始讨论资源的所有权以及 **STL** 智能指针如何在这方面帮助我们。我们发现几乎所有的事情都被视为函数调用，因此允许运算符重载和隐式转换。我们发现了奇妙的(或者是可怕的？)依赖于参数的查找世界 ( **ADL** )以及它如何可能在未来绊倒我们。我们现在对 C++ 的基本特性有了很好的理解。在下一章中，我们将开始探索函数对象，以及如何使用 lambda 函数实现它们。当我们再次访问封装时，我们将深入研究 STL 的产品并探索 PIMPLs。"
  },
  {
    "path": "docs/adv-cpp/05.md",
    "content": "# 五、关注点分离——软件架构、函数和可变模板\n\n## 学习目标\n\n本章结束时，您将能够:\n\n*   使用 PIMPL 习惯用法开发类来实现对象级封装\n*   使用函子、标准::函数和 lambda 表达式实现回调系统\n*   根据情况使用正确的捕获技术实现 lambdas\n*   开发变量模板来实现 C#风格的事件处理委托。\n\n本章将向您展示如何实现 PIMPL 习惯用法，以及如何为您自己的程序开发回调机制。\n\n## 简介\n\n在前一章中，我们学习了如何使用 RAII 实现类来正确管理资源，即使异常发生时也是如此。我们还学习了 ADL ( **自变量相关查找**)以及它如何确定要调用的函数。最后，我们讨论了如何使用显式关键字来防止编译器在类型之间进行自动转换，这就是所谓的隐式转换\n\n在本章中，我们将研究物理和逻辑的依赖关系，看看它们如何对构建时间产生负面影响。我们还将学习如何将可见接口类从实现细节中分离出来，以提高构建速度。然后，我们将学习捕获函数和上下文，以便稍后使用`函子`、`std::function`和`lambda 表达式`调用它们。最后，我们将实现一个变量模板来提供一个基于事件的回调机制。\n\n### 实现的指针(PIMPL)成语\n\n随着用 C++ 实现的项目越来越大，构建时间可能会以比文件数量更快的速度增长。这是因为 C++ 构建模型使用了文本包含模型。这样做是为了让编译器能够确定类的大小和布局，导致`调用方`和`被调用方`之间的耦合，但允许优化。请记住，在使用之前，必须定义所有内容。一个名为`模块`的未来特性有望解决这个问题，但是现在我们需要了解这个问题以及用于解决这个问题的技术。\n\n### 逻辑和物理依赖关系\n\n当我们希望从另一个类访问一个类时，我们有一个逻辑依赖。一个类在逻辑上依赖于另一个类。如果考虑到我们在*章 2A**中开发的`图形`类、`点 3d`和`矩阵 3d`，不允许鸭子-类型和演绎*和*第 3 章*、*可以和应该之间的距离-对象、指针和继承*，我们有两个逻辑上独立的类`矩阵 3d`和`点 3d`。然而，由于我们如何实现两者之间的乘法运算符，我们创建了编译时或**物理依赖关系**。\n\n![Figure 4.1: Physical Dependencies of Matrix3d and Point3d ](img/C14583_04_01.jpg)\n\n###### 图 4.1:矩阵 3d 和点 3d 的物理依赖关系\n\n我们可以看到，对于这些相对简单的类，头文件和实现文件之间的物理依赖关系可能会很快变得复杂。正是这种复杂性导致了大型项目的构建时间，因为物理(和逻辑)依赖项的数量增长到了数千个。在前面的图中，我们只显示了 13 个依赖项，如箭头所示。但实际上还有很多，因为包含标准库头通常会引入包含文件的层次结构。这意味着，如果一个头文件被修改，那么所有依赖于它的文件，无论是直接的还是间接的，都需要被重新编译以解释这个变化。如果对私有类成员定义的更改是该类的用户甚至不能访问的，也会发生这种重建触发器。\n\n为了帮助加快编译时间，我们使用了保护技术来防止头文件被多次处理:\n\n```cpp\n#if !defined(MY_HEADER_INCLUDED)\n#define   MY_HEADER_INCLUDED\n// definitions \n#endif // !defined(MY_HEADER_INCLUDED)\n```\n\n最近，大多数编译器都支持`#pragma 一次`指令，这也达到了同样的效果。\n\n实体(文件、类等)之间的这些关系称为**耦合**。如果对文件/类的更改导致对另一个文件/类的更改，则该文件/类是与另一个文件/类高度耦合的**。如果对文件/类的更改不会导致对其他文件/类的更改，则文件/类是**松散耦合到另一个文件/类的**。**\n\n高度耦合的代码(文件/类)会给项目带来问题。高度耦合的代码很难改变(不灵活)，很难测试，也很难理解。另一方面，松散耦合的代码更容易更改(只修改一个类)，可测试性更高(只需要被测试的类)，更容易阅读和理解。耦合反映了逻辑和物理的依赖关系，并与之相关。\n\n### 实现的指针(PIMPL)成语\n\n耦合问题的一个解决方案是使用“**pumpol 习语**”(代表**指向实现习语**)。这也被称为不透明指针、编译器防火墙习惯用法甚至是**柴郡猫技术**。考虑 **Qt 库**，特别是 **Qt 平台抽象** ( **QPA** )。它是一个抽象层，隐藏了 Qt 应用所在的操作系统和/或平台的细节。实现这种层的一种方法是使用 PIMPL 习惯用法，其中公共接口向应用开发人员公开，但是如何交付功能的实现是隐藏的。Qt 实际上使用了 PIMPL 的一种变体，称为 d 指针。\n\n例如，图形用户界面的一个特点是使用对话框，这是一个显示信息或提示用户输入的弹出窗口。可以在**对话框中声明如下:**\n\n#### 注意\n\n有关 QT 平台抽象(QPA)的更多信息，请访问以下链接:[https://doc.qt.io/qt-5/qpa.html#](https://doc.qt.io/qt-5/qpa.html#)。\n\n```cpp\n#pragma once\nclass Dialog\n{\npublic:\n    Dialog();\n    ~Dialog();\n    void create(const char* message);\n    bool show();\nprivate:\n    struct DialogImpl;\n    DialogImpl* m_pImpl;\n};\n```\n\n用户可以访问使用`对话框`所需的所有功能，但不知道如何实现。注意，我们有一个声明的`对话模板`，但没有定义它。总的来说，像`对话模板`这样的类我们无能为力。但是有一件事是允许的，那就是声明一个指向它的指针。C++ 的这个特性允许我们在实现文件中隐藏实现细节。这意味着在这个简单的例子中，我们没有这个声明的任何包含文件。\n\n实现文件 **dialogImpl.cpp** 可以实现为:\n\n```cpp\n#include \"dialog.hpp\"\n#include <iostream>\n#include <string>\nstruct Dialog::DialogImpl\n{\n    void create(const char* message)\n    {\n        m_message = message;\n        std::cout << \"Creating the Dialog\\n\";\n    }\n    bool show()\n    {\n        std::cout << \"Showing the message: '\" << m_message << \"'\\n\";\n        return true;\n    }\n    std::string m_message;\n};\nDialog::Dialog() : m_pImpl(new DialogImpl)\n{\n}\nDialog::~Dialog()\n{\n    delete m_pImpl;\n}\nvoid Dialog::create(const char* message)\n{\n    m_pImpl->create(message);\n}\nbool Dialog::show()\n{\n    return m_pImpl->show();\n}\n```\n\n我们注意到以下几点:\n\n*   在定义 Dialog 所需的方法之前，我们先定义实现类`DialogImpl`。这是必要的，因为`对话框`将需要通过`m _ Pippl`来练习这些方法，这意味着需要首先定义它们。\n*   `对话框`构造器和析构器负责内存管理。\n*   我们只在实现文件中包含实现所需的所有必要头文件。这通过最小化包含在 **Dialog.hpp** 文件中的头的数量来减少耦合。\n\n该程序可以按如下方式执行:\n\n```cpp\n#include <iostream>\n#include \"dialog.hpp\"\nint main()\n{\n    std::cout << \"\\n\\n------ Pimpl ------\\n\";\n    Dialog dialog;\n    dialog.create(\"Hello World\");\n    if (dialog.show())\n    {\n        std::cout << \"Dialog displayed\\n\";\n    }\n    else\n    {\n        std::cout << \"Dialog not displayed\\n\";\n    }\n    std::cout << \"Complete.\\n\";\n    return 0;\n}\n```\n\n执行时，上述程序产生以下输出:\n\n![Figure 4.2: Sample Pimpl implementation output ](img/C14583_04_02.jpg)\n\n###### 图 4.2:示例皮条客实现输出\n\n### PIMPL 的优势和劣势\n\n使用 PIMPL 的最大优势是它打破了类的客户端和它的实现之间的编译时依赖关系。这允许更快的构建时间，因为 PIMPL 消除了定义(头)文件中大量的`#include`指令，而是将它们推到只在实现文件中是必需的。\n\n它还将实现与客户端分离。我们现在可以自由地更改 PIMPL 类的实现，只有那个文件需要重新编译。这可防止对隐藏成员的更改触发客户端重建的编译级联。这被称为编译防火墙。\n\nPIMPL 成语的其他一些优点如下:\n\n*   **数据隐藏**–实现的内部细节真正隔离在实现类中。如果这是库的一部分，那么它可以用来防止信息的泄露，例如知识产权。\n*   **二进制兼容性**–类的二进制接口现在独立于私有字段。这意味着我们可以在不影响客户端的情况下向实现中添加字段。这也意味着我们可以在共享库中部署实现类(`DLL`，或者`)。所以`文件)，并且可以在不影响客户端代码的情况下自由更改。\n\n这样的优势是有代价的。缺点如下:\n\n*   **维护工作**–在可见类中有额外的代码将调用转发给实现类。这增加了间接性，但复杂性略有增加。\n*   **内存管理**–为实现增加一个指针，现在需要我们管理内存。它还需要额外的存储来保存指针，在内存受限的系统(例如:物联网设备)中，这可能是至关重要的。**T3】**\n\n### 用独特的 _ptr 实现 PIMPL<>\n\n我们当前的 Dialog 实现使用一个原始指针来保存 PIMPL 实现引用。在*第 3 章*、*能与应的距离——对象、指针和继承*中，我们讨论了对象的所有权，并引入了智能指针和 RAII。PIMPL 指针指向的隐藏对象是要管理的资源，应该使用`RAII`和`std::unique_ptr`来执行。正如我们将看到的，对于使用`std::unique_ptr`实现`PIMPL`有一些警告。\n\n让我们将对话框实现改为使用智能指针。首先，头文件改变引入`#包含<内存>`行，析构函数可以移除，因为`unique_ptr`会自动删除实现类。\n\n```cpp\n#pragma once\n#include <memory>\nclass Dialog\n{\npublic:\n    Dialog();\n    void create(const char* message);\n    bool show();\nprivate:\n    struct DialogImpl;\n    std::unique_ptr<DialogImpl> m_pImpl;\n};\n```\n\n显然，我们从实现文件中移除了析构函数，并且修改了构造函数以使用`std::make_unique`。\n\n```cpp\nDialog::Dialog() : m_pImpl(std::make_unique<DialogImpl>())\n{\n}\n```\n\n重新编译我们的新版本时， **Dialog.hpp** 和 **DialogImpl.cpp** 文件没有问题，但是我们的客户端 **main.cpp** 报告了以下错误(gcc 编译器)，如下所示:\n\n![Figure 4.3: Failed compilation of Pimpl using unique_ptr ](img/C14583_04_03.jpg)\n\n###### 图 4.3:使用 unique_ptr 编译皮条客失败\n\n第一个错误报告**将“sizeof”无效应用于不完整的类型“Dialog::Dialog impl”**。问题是在 **main.cpp** 文件中，当`main()`函数结束时，编译器试图为我们调用`对话框`的析构函数。正如我们在*章节**【2A】**不允许鸭子–类型和演绎*中所讨论的，编译器会为我们生成一个析构函数(当我们移除它时)。这个生成的析构函数将调用`unique_ptr`的析构函数，这就是错误的原因。如果我们看一下 **unique_ptr.h** 文件的`第 76 行`，我们会发现`运算符()`函数对于`unique_ptr`使用的默认`deleter`的实现如下(该`deleter`是`unique_ptr`在破坏它所指向的对象时调用的函数):\n\n```cpp\nvoid\noperator()(_Tp* __ptr) const\n{\n    static_assert(!is_void<_Tp>::value, \"can't delete pointer to incomplete type\");\n    static_assert(sizeof(_Tp)>0, \"can't delete pointer to incomplete type\");\n    delete __ptr;\n}\n```\n\n我们的代码在第二个`static_assert()`语句上失败，该语句终止编译并出现错误。问题是编译器试图为`STD::unique _ ptr<DialogImpl>`生成析构函数，而`DialogImpl`是不完整的类型。因此，为了解决这个问题，我们将析构函数的生成控制在`DialogImpl`是一个完整类型的点上。\n\n为此，我们将析构函数的声明放回类中，并将其实现添加到`DialogImpl.cpp`文件中。\n\n```cpp\nDialog::~Dialog()\n{\n}\n```\n\n当我们编译和运行我们的程序时，它会产生与以前完全相同的输出。事实上，如果我们只需要一个空析构函数，我们可以用下面的代码替换上面的代码:\n\n```cpp\nDialog::~Dialog() = default;\n```\n\n如果我们编译并运行我们的程序，那么将产生以下输出:\n\n![Figure 4.4: Sample unique_ptr Pimpl implementation output ](img/C14583_04_04.jpg)\n\n###### 图 4.4:示例 unique_ptr Pimpl 实现输出\n\n### 独特 _ptr < > PIMPL 特殊功能\n\n正如 PIMPL 通常暗示的那样，可见接口类拥有实现类，移动语义是自然的。但是，同样编译器生成的析构函数实现是正确的，编译器生成的移动构造函数和移动赋值运算符会给出想要的行为，即对成员`unique_ptr`执行移动。移动操作都可能需要在赋值之前执行删除操作，因此会遇到与类型不完整的析构函数相同的问题。解决方案与析构函数相同——在头文件中声明方法，当类型完成时在实现文件中实现。因此，我们的头文件如下所示:\n\n```cpp\nclass Dialog\n{\npublic:\n    Dialog();\n    ~Dialog();\n    Dialog(Dialog&& rhs);\n    Dialog& operator=(Dialog&& rhs);\n    void create(const char* message);\n    bool show();\nprivate:\n    struct DialogImpl;\n    std::unique_ptr<DialogImpl> m_pImpl;\n};\n```\n\n虽然实现看起来像:\n\n```cpp\nDialog::Dialog() : m_pImpl(std::make_unique<DialogImpl>())\n{\n}\nDialog::~Dialog() = default;\nDialog::Dialog(Dialog&& rhs) = default;\nDialog& Dialog::operator=(Dialog&& rhs) = default;\n```\n\n根据我们隐藏在实现类中的数据项，我们可能还需要 PIMPL 类的复制功能。在对话框类中使用`std::unique_ptr`可以防止自动生成复制构造函数和复制赋值运算符，因为内部成员不支持复制。此外，通过定义移动成员函数，正如我们在*章节【2A】*、*不允许鸭子–类型和演绎*中看到的，它也停止编译器生成副本版本。另外，如果编译器确实为我们生成了拷贝语义，那也只是**浅拷贝**。但是由于 PIMPL 的实现，我们需要一个**深度副本**。所以，我们需要编写自己的复制特殊成员函数。同样，定义在头文件中，实现需要在类型完整的地方完成，在 **DialogImpl.cpp** 文件中。\n\n在头文件中，我们添加了以下声明:\n\n```cpp\nDialog(const Dialog& rhs);\nDialog& operator=(const Dialog& rhs);\n```\n\n实现如下所示:\n\n```cpp\nDialog::Dialog(const Dialog& rhs) : m_pImpl(nullptr)\n{\n    if (this == &rhs)   // do nothing on copying self\n    return;\n    if (rhs.m_pImpl)    // rhs has something -> clone it\n        m_pImpl = std::make_unique<DialogImpl>(*rhs.m_pImpl);\n}\nDialog& Dialog::operator=(const Dialog& rhs)\n{\n    if (this == &rhs)   // do nothing on assigning to self\n        return *this;\n    if (!rhs.m_pImpl)   // rhs is empty -> delete ours\n    {\n        m_pImpl.reset();\n    }\n    else if (!m_pImpl)  // ours is empty -> clone rhs\n    {\n        m_pImpl = std::make_unique<DialogImpl>(*rhs.m_pImpl);\n    }\n    else // use copy of DialogImpl\n    {\n        *m_pImpl = *rhs.m_pImpl;\n    }\n}\n```\n\n注意`if(this == & rhs)`子句。这些是为了防止对象不必要地复制自己。另外，请注意，我们需要检查`unique_ptr`是否为空，并相应地处理副本。\n\n#### 注意\n\n在解决本章的任何实际问题之前，下载 GitHub 资源库[https://github.com/TrainingByPackt/Advanced-CPlusPlus](https://github.com/TrainingByPackt/Advanced-CPlusPlus)并导入 Eclipse 中第 4 课的文件夹，这样您就可以查看每个练习和活动的代码。\n\n### 练习 1:用独特的 T2 实现厨房\n\n在本练习中，我们将通过使用`unique_ptr < >`实现`皮条客成语`来隐藏厨房如何处理订单的细节。按照以下步骤实施本练习:\n\n1.  在 Eclipse 中打开**第 4 课**项目，然后在**项目浏览器**中，依次展开**第 4 课**和**练习 01** ，双击**练习 1.cpp** 将本练习的文件打开到编辑器中。\n2.  由于这是一个基于 CMake 的项目，将当前的构建器改为 CMake Build(可移植)。\n3.  点击**启动配置**下拉菜单，选择**新启动配置……**。配置**l4 练习 1** 以名称**练习 1** 运行。\n4.  Click on the **Run** button. Exercise 1 will run and produce the following output:\n\n    ![Figure 4.5: Exercise 1 Program output ](img/C14583_04_05.jpg)\n\n    ###### 图 4.5:练习 1 程序输出\n\n5.  Open **kitchen.hpp** in the editor, and you will find the following declaration:\n\n    ```cpp\n    class Kitchen\n    {\n    public:\n        Kitchen(std::string chef);\n        std::string processOrder(std::string order);\n    private:\n        std::string searchForRecipe(std::string order);\n        std::string searchForDessert(std::string order);\n        std::string cookRecipe(std::string recipe);\n        std::string serveDessert(std::string dessert);\n        std::vector<Recipe>::iterator getRecipe(std::string recipe);\n        std::vector<Dessert>::iterator getDessert(std::string recipe);\n        std::string m_chef;\n        std::vector<Recipe> m_recipes;\n        std::vector<Dessert> m_desserts;\n    };\n    ```\n\n    私人部分的所有内容都是关于厨房如何向顾客交付订单的细节。它强制包含头文件**食谱. hpp** 和**甜点. hpp** ，在这些细节文件和`厨房`的客户之间建立了一个耦合。我们将把所有私有成员移到一个实现类中，并隐藏细节。\n\n6.  在 **kitchen.hpp** 文件中，添加`#include < memory >`指令以访问`unique_ptr`。添加析构函数`~Kitchen()的声明；`然后在私段顶部增加以下两行:\n\n    ```cpp\n    struct Impl;\n    std::unique_ptr<Impl> m_impl;\n    ```\n\n7.  打开**厨房. cpp** 文件，在`#包括`指令后添加以下内容:\n\n    ```cpp\n    struct Kitchen::Impl\n    {\n    };\n    Kitchen::~Kitchen() = default;\n    ```\n\n8.  点击**运行**按钮重新构建程序。您会看到输出仍然和以前一样。\n9.  从 **kitchen.hpp** 中的`Kitchen`类中移除除两个新成员之外的所有私有成员，并将其添加到`Kitchen::Impl`声明中。**厨房. hpp** 文件删除`#包含<矢量>`、`#包含【食谱. HPP】`、`#包含【甜点. HPP】`:\n\n    ```cpp\n    #pragma once\n    #include <string>\n    #include <memory>\n    class Kitchen\n    {\n    public:\n        Kitchen(std::string chef);\n        ~Kitchen();\n        std::string processOrder(std::string order);\n    private:\n        struct Impl;\n        std::unique_ptr<Impl> m_impl;\n    };\n    ```\n\n    后内容如下\n10.  在 **kitchen.cpp** 文件中，将 kitchen 构造函数更改为`Kitchen::Impl`构造函数:\n\n    ```cpp\n    Kitchen::Impl::Impl(std::string chef) : m_chef{chef}\n    ```\n\n11.  对于原始方法的其余部分，将其范围更改为`厨房::Impl`而不是`厨房::`。例如，`STD::string Kitchen::process order(STD::string order)`变为`STD::string Kitchen::Impl::process order(STD::string order)`。\n12.  在`Kitchen::Impl`中，添加一个带有`std::string`参数和`processOrder()`方法的构造函数。`厨房::Impl`声明现在应该如下所示:\n\n    ```cpp\n    struct Kitchen::Impl\n    {\n        Impl(std::string chef);\n        std::string processOrder(std::string order);\n        std::string searchForRecipe(std::string order);\n        std::string searchForDessert(std::string order);\n        std::string cookRecipe(std::string recipe);\n        std::string serveDessert(std::string dessert);\n        std::vector<Recipe>::iterator getRecipe(std::string recipe);\n        std::vector<Dessert>::iterator getDessert(std::string recipe);\n        std::string m_chef;\n        std::vector<Recipe> m_recipes;\n        std::vector<Dessert> m_desserts;\n    };\n    ```\n\n13.  在 **kitchen.cpp** 中，在文件顶部添加`#include < vector >`、`# include“recipe . HPP”`、`#include“甜品. HPP”`。\n14.  点击**运行**按钮重新构建程序，这次会出现两个未定义的引用失败–`厨房:【厨房】`和`厨房:【过程顺序】`。\n15.  在 **Kitchen.cpp** 中，在`Kitchen::Impl`方法定义后，添加以下两个方法:\n\n    ```cpp\n    Kitchen::Kitchen(std::string chef) : m_impl(std::make_unique<Kitchen::Impl>(chef))\n    {\n    }\n    std::string Kitchen::processOrder(std::string order)\n    {\n        return m_impl->processOrder(order);\n    }\n    ```\n\n16.  点击**运行**按钮重新构建程序。程序将再次运行以产生原始输出。\n\n![Figure 4.6: The Kitchen program output using Pimpl ](img/C14583_04_06.jpg)\n\n###### 图 4.6:使用皮条客的厨房程序输出\n\n在本练习中，我们采用了一个在其私有成员中包含许多细节的类，并将这些细节移动到一个 PIMPL 类中，以隐藏细节并使用前面描述的技术将接口与实现分离。\n\n## 函数对象和λ表达式\n\n编程中使用的一种常见模式，尤其是在实现基于事件的处理(如异步输入和输出)时，是使用**回调**。客户端注册希望收到事件发生的通知(例如:数据可供读取，或者数据传输已完成)。这种模式被称为**观察者模式**或**订户发布者模式**。C++ 支持多种技术来提供回调机制。\n\n### 函数指针\n\n第一种机制是使用**函数指针**。这是继承自 C 语言的遗留特性。下面的程序显示了一个函数指针的例子:\n\n```cpp\n#include <iostream>\nusing FnPtr = void (*)(void);\nvoid function1()\n{\n    std::cout << \"function1 called\\n\";\n}\nint main()\n{\n    std::cout << \"\\n\\n------ Function Pointers ------\\n\";\n    FnPtr fn{function1};\n    fn();\n    std::cout << \"Complete.\\n\";\n    return 0;\n}\n```\n\n该程序在编译和执行时会产生以下输出:\n\n![Figure 4.7: Function Pointer Program output ](img/C14583_04_07.jpg)\n\n###### 图 4.7:函数指针程序输出\n\n严格来说，代码应该修改如下:\n\n```cpp\nFnPtr fn{&function1};\nif(fn != nullptr)\n    fn();\n```\n\n首先要注意的是，(`&`)运算符的地址应该用来初始化指针。其次，我们应该在调用指针之前检查它是否有效。\n\n```cpp\n#include <iostream>\nusing FnPtr = void (*)(void);\nstruct foo\n{\n    void bar() { std::cout << \"foo:bar called\\n\"; }\n};\nint main()\n{\n    std::cout << \"\\n\\n------ Function Pointers ------\\n\";\n    foo object;\n    FnPtr fn{&object.bar};\n    fn();\n    std::cout << \"Complete.\\n\";\n    return 0;\n}\n```\n\n当我们试图编译这个程序时，我们会得到以下错误:\n\n![Figure 4.8: Errors compiling function pointer program ](img/C14583_04_08.jpg)\n\n###### 图 4.8:编译函数指针程序时的错误\n\n第一个错误的文本是 **ISO C++ 禁止取绑定成员函数的地址形成成员函数的指针。说'& foo::bar'** 。它告诉我们应该使用不同的形式来获取函数地址。这种情况下真正的错误是第二条错误消息:**错误:初始化**时无法将“void (foo::*)()”转换为“FnPtr { aka void(*)}”。真正的问题是，非静态成员函数在被调用时会有一个隐藏的参数——这个**指针。**\n\n通过将上述程序更改为以下内容:\n\n```cpp\n#include <iostream>\nusing FnPtr = void (*)(void);\nstruct foo\n{\n    static void bar() { std::cout << \"foo:bar called\\n\"; }\n};\nint main()\n{\n    std::cout << \"\\n\\n------ Function Pointers ------\\n\";\n    FnPtr fn{&foo::bar};\n    fn();\n    std::cout << \"Complete.\\n\";\n    return 0;\n}\n```\n\n它成功编译并运行:\n\n![Figure 4.9: Function pointer program using static member function ](img/C14583_04_09.jpg)\n\n###### 图 4.9:使用静态成员函数的函数指针程序\n\n当与使用回调和支持回调的操作系统通知的 C 库接口时，经常使用函数指针技术。在这两种情况下，回调接受一个参数是用户注册的数据 blob 指针的`void *`是正常的。数据块指针可以是类的`这个`指针，然后被取消引用，回调被转发到成员函数中。\n\n在其他语言中，例如 Python 和 C#，捕捉函数指针也将捕捉调用该函数所需的足够数据，这是语言的一部分(例如:`self`或`this`)。C++ 能够通过函数调用操作符调用任何对象，我们接下来将介绍这一点。\n\n### 什么是功能对象？\n\nC++ 允许函数调用运算符`运算符()`重载。这就产生了使任何物体`成为可调用的`的能力。一个可调用的对象被称为**函子**。下面程序中的`Scaler`类实现了一个`函子`。\n\n```cpp\nstruct Scaler\n{\n    Scaler(int scale) : m_scale{scale} {};\n    int operator()(int value)\n    {\n        return m_scale * value;\n    }\n    int m_scale{1};\n};\nint main()\n{\n    std::cout << \"\\n\\n------ Functors ------\\n\";\n    Scaler timesTwo{2};\n    Scaler timesFour{4};\n    std::cout << \"3 scaled by 2 = \" << timesTwo(3) << \"\\n\";\n    std::cout << \"3 scaled by 4 = \" << timesFour(3) << \"\\n\";\n    std::cout << \"Complete.\\n\";\n    return 0;\n}\n```\n\n创建了两个类型为`缩放器`的对象，它们被用作生成输出的线内的函数。上述程序产生以下输出:\n\n![Figure 4.10: Functors program output ](img/C14583_04_10.jpg)\n\n###### 图 4.10:函子程序输出\n\n`函子`相对于函数指针的一个优势是，它们可以包含状态，作为一个对象或者跨所有实例。另一个优点是，它们可以传递给期望函数(例如`标准::for_each`)或运算符(例如`标准::transform`)的 STL 算法。\n\n这种用途的示例可能如下所示:\n\n```cpp\n#include <iostream>\n#include <vector>\n#include <algorithm>\nstruct Scaler\n{\n    Scaler(int scale) : m_scale{scale} {};\n    int operator()(int value)\n    {\n        return m_scale * value;\n    }\n    int m_scale{1};\n};\nvoid PrintVector(const char* prefix, std::vector<int>& values)\n{\n    const char* sep = \"\";\n    std::cout << prefix << \" = [\";\n    for(auto n : values)\n    {\n        std::cout << sep << n;\n        sep = \", \";\n    }\n    std::cout << \"]\\n\";\n}\nint main()\n{\n    std::cout << \"\\n\\n------ Functors with STL ------\\n\";\n    std::vector<int> values{1,2,3,4,5};\n    PrintVector(\"Before transform\", values);\n    std::transform(values.begin(), values.end(), values.begin(), Scaler(3));\n    PrintVector(\"After transform\", values);\n    std::cout << \"Complete.\\n\";\n    return 0;\n}\n```\n\n如果我们运行这个程序，生成的输出将如下所示:\n\n![Figure 4.11: Program output showing Scaler transformed vectors ](img/C14583_04_11.jpg)\n\n###### 图 4.11:显示缩放器变换向量的程序输出\n\n### 练习 2:实现功能对象\n\n在本练习中，我们将实现两个不同的函数对象，它们可以与 STL 算法一起工作。\n\n1.  在 Eclipse 中打开**第 4 课**项目，然后在**项目浏览器**中，依次展开**第 4 课**和**练习 02** ，双击**练习 2.cpp** 将本练习的文件打开到编辑器中。\n2.  由于这是一个基于 CMake 的项目，将当前的构建器更改为 CMake 构建(可移植)。\n3.  点击**启动配置**下拉菜单，选择**新启动配置……**。将**l4 练习 2** 配置为以**练习 2** 的名称运行。\n4.  Click on the **Run** button. Exercise 2 will run and produce the following output:\n\n    ![Figure 4.12: Exercise 2 Initial output ](img/C14583_04_12.jpg)\n\n    ###### 图 4.12:练习 2 初始输出\n\n    我们要做的第一件事是通过引入一个函数对象来修复输出的格式。\n\n5.  在编辑器中，`main()`函数的定义前添加以下类定义:\n\n    ```cpp\n    struct Printer\n    {\n        void operator()(int n)\n        {\n            std::cout << m_sep << n;\n            m_sep = \", \";\n        }\n        const char* m_sep = \"\";\n    };\n    ```\n\n6.  Inside the **main()** method replace the following code\n\n    ```cpp\n    std::cout << \"Average of [\";\n    for( auto n : values )\n        std::cout << n << \", \";\n    std::cout << \"] = \";\n    ```\n\n    **同**\n\n    ```cpp\n    std::cout << \"Average of [\";\n    std::for_each(values.begin(), values.end(), Printer());\n    std::cout << \"] = \";\n    ```\n\n7.  Click on the **Run** button. The exercise will run and produce the following output:\n\n    ![Figure 4.13: Exercise 2 Improved output format ](img/C14583_04_13.jpg)\n\n    ###### 图 4.13:练习 2 改进了输出格式\n\n8.  `打印机`类的内部状态允许我们修改格式。现在，介绍一个`聚合器`类，它允许我们计算`平均值`。在文件顶部添加以下类定义:\n\n    ```cpp\n    struct Averager\n    {\n        void operator()(int n)\n        {\n            m_sum += n;\n            m_count++ ;\n        }\n        float operator()() const\n        {\n            return static_cast<float>(m_sum)/(m_count==0?1:m_count);\n        }\n        int m_count{0};\n        int m_sum{0};\n    };\n    ```\n\n9.  修改`main()`方法使用`Averager`类如下:\n\n    ```cpp\n    int main(int argc, char**argv)\n    {\n        std::cout << \"\\n------ Exercise 2 ------\\n\";\n        std::vector<int> values {1,2,3,4,5,6,7,8,9,10};\n        Averager averager = std::for_each(values.begin(), values.end(), \n        Averager());\n        std::cout << \"Average of [\";\n        std::for_each(values.begin(), values.end(), Printer());\n        std::cout << \"] = \";\n        std::cout << averager() << \"\\n\";\n        std::cout << \"Complete.\\n\";\n        return 0;\n    }\n    ```\n\n10.  点击**运行**按钮。该练习将运行并产生以下输出:\n\n![Figure 4.14: Exercise 2 output with average ](img/C14583_04_14.jpg)\n\n###### 图 4.14:练习 2 的平均输出\n\n注意 **std::for_each()** 返回传递到其中的**平均器**的实例。该实例被复制到变量**平均器**中，然后包含计算平均值所需的数据。在本练习中，我们实现了两个函数对象或**函子**类:**平均器**和**打印机**，当传递到 STL 算法**时，我们可以使用它们作为函数。**\n\n### 标准::功能< >模板\n\nC++ 11 引入了一个通用的多态函数包装器模板，`std::function < >`，使得实现回调和其他函数相关功能变得更加容易。`std::function`保存一个被称为**目标**的可调用对象。如果不包含目标，则称为**空**。调用空的`std::function`会导致`std::bad_function_call`异常被抛出。\n\n函数对象可以存储、复制或调用以下任何可调用对象的目标:函数、函数对象(定义`运算符()`)、成员函数指针或 lambda 表达式。我们将在*主题中讨论更多关于λ表达式的内容。*\n\n当实例化一个`std::function`对象时，只需要提供函数签名，而不需要提供用来初始化它的值，导致一个空实例。实例化如下进行:\n\n![Figure 4.15: Structure of a std::function declaration ](img/C14583_04_15.jpg)\n\n###### 图 4.15:标准::函数声明的结构\n\n模板的参数，定义由`变量`存储的目标的`函数签名`。签名以返回类型(可能是 void)开始，然后在括号内放置函数将被调用的类型列表。\n\n自由函数和带`标准::函数`的`函子`的使用是直接的。假设签名与传递给`标准::函数`模板的参数匹配，我们可以简单地将自由函数或`函子`等同于实例。\n\n```cpp\nvoid FreeFunc(int value);\nstruct Functor \n{\n    void operator()(int value);\n};\nstd::function<void(int)> func;\nFunctor functor;\nfunc = FreeFunc;                     // Set target as FreeFunc\nfunc(32);                            // Call FreeFunc with argument 32\nfunc = functor;                      // set target as functor\nfunc(42);                            // Call Functor::operator() with argument 42\n```\n\n然而，如果我们想在一个对象实例上使用一个方法，那么我们需要使用另一个 STL 助手模板`std::bind()`。如果我们运行以下程序:\n\n```cpp\n#include <iostream>\n#include <functional>\nstruct Binder\n{\n    void method(int a, int b)\n    {\n        std::cout << \"Binder::method(\" << a << \", \" << b << \")\\n\";\n    }\n};\nint main()\n{\n    std::cout << \"\\n\\n------ Member Functions using bind ------\\n\";\n    Binder binder;\n    std::function<void(int,int)> func;\n    auto func1 = std::bind(&Binder::method, &binder, 1, 2);\n    auto func2 = std::bind(&Binder::method, &binder, std::placeholders::_1, std::placeholders::_2);\n    auto func3 = std::bind(&Binder::method, &binder, std::placeholders::_2, std::placeholders::_1);\n    func = func1;\n    func(34,56);\n    func = func2;\n    func(34,56);\n    func = func3;\n    func(34,56);\n    std::cout << \"Complete.\\n\";\n    return 0;\n}\n```\n\n然后我们得到以下输出:\n\n![Figure 4.16: Program output using std::bind() and std::function ](img/C14583_04_16.jpg)\n\n###### 图 4.16:使用标准::bind()和标准::函数的程序输出\n\n需要注意的几点:\n\n*   使用类作为范围说明符引用函数`方法()`；\n*   `Binder`实例的地址作为第二个参数传递给`std::bind()`，这使其成为传递给`方法()`的第一个参数。这是必要的，因为所有非静态成员都有一个隐式的`这个`指针作为第一个参数传递。\n*   使用`标准::占位符`定义，我们可以绑定调用绑定方法时使用的参数，甚至可以更改传递的顺序(如`函数 3`所示)。\n\nC++ 11 引入了一些被称为 lambda 表达式的语法糖，使得定义匿名函数变得更加容易，这些匿名函数也可以用来绑定方法并将其分配给`STD::function`instance 表达式。我们将在*主题中讨论更多关于λ表达式的内容。*\n\n### 练习 3:用 std::函数实现回调\n\n在本练习中，我们将利用`std::function < >`模板实现函数回调。按照以下步骤实施本练习:\n\n1.  在 Eclipse 中打开**第 4 课**项目，然后在**项目浏览器**中，依次展开**第 4 课**和**练习 03** ，双击**练习 3.cpp** 将本练习的文件打开到编辑器中。\n2.  由于这是一个基于 CMake 的项目，将当前的构建器更改为 CMake 构建(可移植)。\n3.  点击**启动配置**下拉菜单，选择**新启动配置……**。将**l4 练习 3** 配置为以**练习 3** 的名称运行。\n4.  Click on the **Run** button. The exercise will run and produce the following output:\n\n    ![Figure 4.17: Exercise 3 output (Calling empty std::function) ](img/C14583_04_17.jpg)\n\n    ###### 图 4.17:练习 3 输出(调用空的 std::函数)\n\n5.  我们要做的第一件事是防止调用空的 **std::function** 。定位功能`测试功能模板()`行`功能(42)；`并替换为以下代码:\n\n    ```cpp\n    if (func)\n    {\n        func(42);\n    }\n    else\n    {\n        std::cout << \"Not calling an empty func()\\n\";\n    }\n    ```\n\n6.  Click on the **Run** button. The exercise will run and produce the following output:\n\n    ![Figure 4.18: Exercise 3 output (preventing call to empty std::function) ](img/C14583_04_18.jpg)\n\n    ###### 图 4.18:练习 3 输出(防止调用空 std::函数)\n\n7.  将`FreeFunction()`方法添加到函数`testfunction template()`:\n\n    ```cpp\n    void FreeFunction(int n)\n    {\n        std::cout << \"FreeFunction(\" << n << \")\\n\";\n    }\n    ```\n\n    之前的文件中\n8.  在`测试功能模板()`功能中，紧接在`if (func)`之前添加以下行:\n\n    ```cpp\n    func = FreeFunction;\n    ```\n\n9.  Click on the **Run** button. The exercise will run and produce the following output:\n\n    ![Figure 4.19: Exercise 3 output (FreeMethod) ](img/C14583_04_19.jpg)\n\n    ###### 图 4.19:练习 3 输出(FreeMethod)\n\n10.  在`TestFunctionTemplate()`函数\n\n    ```cpp\n    struct FuncClass\n    {\n        void member(int n)\n        {\n            std::cout << \"FuncClass::member(\" << n << \")\\n\";\n        }\n        void operator()(int n)\n        {\n        std::cout << \"FuncClass object(\" << n << \")\\n\";\n        }\n    };\n    ```\n\n    前增加新的类定义\n11.  更换线路`func = free function；`代码如下:\n\n    ```cpp\n    FuncClass funcClass;\n    func = funcClass;\n    ```\n\n12.  Click on the **Run** button. The exercise will run and produce the following output:\n\n    ![4.20: Exercise 3 output (Object function call override) ](img/C14583_04_20.jpg)\n\n    ###### 4.20:练习 3 输出(对象函数调用覆盖)\n\n13.  更换线路`func = func class；`代码如下:\n\n    ```cpp\n    func = std::bind(&FuncClass::member, &funcClass, std::placeholders::_1);\n    ```\n\n14.  Click on the **Run** button. The exercise will run and produce the following output:\n\n    ![Figure 4.21: Exercise 3 output (Member function) ](img/C14583_04_21.jpg)\n\n    ###### 图 4.21:练习 3 输出(成员函数)\n\n15.  替换行`func = STD::bind(…)；`代码如下:\n\n    ```cpp\n    func = [](int n) {std::cout << \"lambda function(\" << n << \")\\n\";};\n    ```\n\n16.  点击**运行**按钮。该练习将运行并产生以下输出:\n\n![Figure 4.22: Exercise 3 output (lambda function) ](img/C14583_04_22.jpg)\n\n###### 图 4.22:练习 3 输出(λ函数)\n\n在本练习中，我们使用`std::function`模板实现了四种不同类型的函数回调——自由方法、类成员函数、类函数调用方法和一个 lambda 函数(我们接下来看)。\n\n### 什么是λ表达式？\n\n从 C++ 11 开始，C++ 就支持`匿名函数`，也称为`lambda 表达式`，或者只是`lambdas`。lambda 表达式最常见的两种形式是:\n\n![Figure 4.23: Most common forms of lambda expressions ](img/C14583_04_23.jpg)\n\n###### 图 4.23:λ表达式的最常见的 n 种形式\n\n在正常情况下，编译器能够根据 **function_body** 内的返回语句推导出 lambda 的返回类型(如上图中的形式(1)所示)。但是，如果编译器不能确定返回类型，或者如果我们希望强制一个不同的类型，那么我们可以使用 form (2)。\n\n`【捕获】`之后的所有内容都与普通函数定义相同，只是缺少名称。Lambdas 是一种在将要使用的位置定义一个短方法(只有几行)的便捷方式。lambda 经常作为参数传递，通常不会被重用。还应该注意的是，一个 lambda 可以赋给一个变量(通常用 auto 声明)。\n\n我们可以重新编写之前的程序，其中我们使用了`Scaler`类来使用 lambda 来实现相同的结果:\n\n```cpp\n#include <iostream>\n#include <vector>\n#include <algorithm>\nvoid PrintVector(const char* prefix, std::vector<int>& values)\n{\n    const char* sep = \"\";\n    std::cout << prefix << \" = [\";\n    for(auto n : values)\n    {\n        std::cout << sep << n;\n        sep = \", \";\n    }\n    std::cout << \"]\\n\";\n}\nint main()\n{\n    std::cout << \"\\n\\n------ Lambdas with STL ------\\n\";\n    std::vector<int> values{1,2,3,4,5};\n    PrintVector(\"Before transform\", values);\n    std::transform(values.begin(), values.end(), values.begin(),\n    [] (int n) {return 5*n;}\n    );\n    PrintVector(\"After transform\", values);\n    std::cout << \"Complete.\\n\";\n    return 0;\n}\n```\n\n当这个程序运行时，输出显示矢量被缩放了 5:\n\n![Figure 4.24: Transform using lambda for scaling ](img/C14583_04_24.jpg)\n\n###### 图 4.24:使用λ进行缩放的变换\n\n本程序中的λ为`[](int n){ return 5 * n；}`并且有一个空的捕获子句`[]`。空的 capture 子句意味着 lambda 函数不会从周围的范围访问任何变量。如果没有参数传递给 lambda，那么参数子句`()`是可选的。\n\n### 将数据捕获到 Lambdas 中\n\n**捕获子句**，或者**λ引入器**(来自 C++ 规范)，允许匿名函数从周围的范围捕获数据供以后使用。函数与解析函数变量的作用域的这种组合被称为**闭包**。这种情况下的作用域是在 capture 子句中指定的一组变量绑定。在前一节中，我们说过 lambdas 是匿名函数。通过闭包添加变量捕获，lambdas 被更正确地识别为匿名函数对象。编译器用内联构造函数创建一个匿名类来捕获变量，并使用函数调用运算符`运算符()`。\n\ncapture 子句是由零个或多个捕获变量组成的逗号分隔列表。还有默认捕获的概念——要么通过引用，要么通过值。因此，捕获的基本语法是:\n\n*   `[&]`–通过引用捕获范围内的所有自动存储持续时间变量\n*   `[=]`–按值捕获范围内的所有自动存储持续时间变量(制作副本)\n*   `[ & x，y]`–通过引用捕获 x，通过值捕获 y\n\n这由编译器转换成成员变量，这些变量由匿名`函子`类的构造函数初始化。在默认捕获(`&``=`)的情况下，它们必须排在第一位，并且只捕获正文中引用的变量。默认捕获可以通过将特定变量放入默认捕获之后的 capture 子句中来覆盖。例如，`[ &，x]`将默认通过引用捕获除`x`以外的所有内容，它将通过值捕获这些内容。\n\n但是，虽然默认捕获很方便，但它们不是首选的捕获方法。这是因为它会导致悬空引用(通过引用捕获，当被 lambda 访问时被引用的变量不再存在)或悬空指针(通过值捕获，尤其是这个指针)。显式捕获变量更清楚，这有一个额外的好处，编译器能够警告您意外的影响(例如试图捕获全局或静态变量)。\n\nC++ 14 在 capture 子句中引入了 **init capture** ，允许更安全的代码和一些优化。init capture 在 capture 子句中声明一个变量，并将其初始化以在 lambda 中使用。一个例子是:\n\n```cpp\nint x = 5;\nint y = 6;\nauto fn = [z=x*x+y, x, y] ()\n            {   \n                std::cout << x << \" * \" << x << \" + \" << y << \" = \" << z << \"\\n\"; \n            };\nfn();\n```\n\n这里，`z`在 capture 子句中声明并初始化，以便可以在 lambda 中使用。如果你想在 lambda 中使用 x 和 y，那么它们必须被分别捕获。正如预期的那样，当调用 lambda 时，它会产生以下输出:\n\n```cpp\n5 * 5 + 6 = 31\n```\n\ninit capture 还可以用于将可移动对象捕获到 lambda 中，或者复制类成员，如下所示:\n\n```cpp\nstruct LambdaCapture\n{\n  auto GetTheNameFunc ()\n  {\n    return [myName = myName] () { return myName.c_str(); };  \n  }\n  std::string myName;\n};\n```\n\n这将捕获成员变量的值，并碰巧赋予它相同的名称，以便在 lambda 中使用。\n\n默认情况下，lambda 是一个常量函数，这意味着它不能更改按值捕获变量的值。如果需要修改该值，我们需要使用第三种形式的 lambda 表达式，如下所示。\n\n![Figure 4.25: Another form of lambda expression ](img/C14583_04_25.jpg)\n\n###### 图 4.25:λ表达式的另一种形式\n\n在这种情况下，`说明符`被`可变的`代替，告诉编译器我们想要修改捕获的值。如果我们不添加可变的，并且我们试图修改一个捕获的值，那么编译器将产生一个错误。\n\n### 练习 4:实现 Lambdas\n\n在本练习中，我们将实现 lambdas，以在 STL 算法的上下文中执行许多操作。按照以下步骤实施本练习:\n\n1.  在 Eclipse 中打开**第 4 课**项目，然后在**项目浏览器**中，依次展开**第 4 课**和**练习 04** ，双击**练习 4.cpp** 将本练习的文件打开到编辑器中。\n2.  由于这是一个基于 CMake 的项目，将当前的构建器更改为 CMake 构建(可移植的)。\n3.  点击**启动配置**下拉菜单，选择**新启动配置……**。将**l4 练习 4** 配置为使用名称**练习 4** 运行。\n4.  Click on the **Run** button. The exercise will run and produce the following output:\n\n    ![Figure 4.26: Initial output from Exercise 4 ](img/C14583_04_26.jpg)\n\n    ###### 图 4.26:练习 4 的初始输出\n\n5.  程序**练习 4.cpp** 包含两种方法，`PrintVector()`和`main()`。`PrintVector()`与我们在*中介绍的版本相同什么是函数对象？*。现在修改它，使用`std::for_each()`库函数和一个 lambda 代替 ranged-for 循环。将`打印矢量()`更新为:\n\n    ```cpp\n    void PrintVector(const char* prefix, std::vector<int>& values)\n    {\n        const char* sep = \"\";\n        std::cout << prefix << \" = [\";\n        std::for_each(values.begin(), values.end(),\n                [&sep] (int n)\n                {\n                    std::cout << sep << n;\n                    sep = \", \";\n                }\n        );\n        std::cout << \"]\\n\";\n    }\n    ```\n\n6.  点击**运行**按钮，我们得到和之前一样的输出。\n7.  Examine the lambda, we have captured the local variable `sep` by reference. Remove the `&` from `sep` and click on the **Run** button. This time the compilation fails with the following error:\n\n    ![Figure 4.27: Compilation failure due to modifying read-only variable ](img/C14583_04_27.jpg)\n\n    ###### 图 4.27:修改只读变量导致编译失败\n\n8.  更改 lambda 声明以包含`可变的`说明符:\n\n    ```cpp\n    [sep] (int n) mutable\n    {\n        std::cout << sep << n;\n        sep = \", \";\n    }\n    ```\n\n9.  点击**运行**按钮，我们得到和之前一样的输出。\n10.  但是我们可以更进一步。从函数`PrintVector()`中删除`sep`的声明，并再次更改 lambda 以包括一个初始化捕获。编写以下代码来实现这一点:\n\n    ```cpp\n    [sep = \"\"] (int n) mutable\n    {\n        std::cout << sep << n;\n        sep = \", \";\n    }\n    ```\n\n11.  点击**运行**按钮，我们得到和以前一样的输出。如果我们重新格式化函数`PrintVector()`它现在看起来更紧凑为:\n\n    ```cpp\n    void PrintVector(const char* prefix, std::vector<int>& values)\n    {\n        std::cout << prefix << \" = [\";\n        std::for_each(values.begin(), values.end(), [sep = \"\"] (int n) mutable\n                                      { std::cout << sep << n; sep = \", \";} );\n        std::cout << \"]\\n\";\n    }\n    ```\n\n12.  在`main()`方法中调用`PrintVector()`后添加以下行:\n\n    ```cpp\n    std::sort(values.begin(), values.end(), [](int a, int b) {return b<a;} );\n    PrintVector(\"After sort\", values);\n    ```\n\n13.  Click on the **Run** button and the output now adds the list of values sorted in descending order:\n\n    ![Figure 4.28: Program output for descending sort lambda ](img/C14583_04_28.jpg)\n\n    ###### 图 4.28:递减排序λ的程序输出\n\n14.  Change the lambda function body to be `{return a<b;}`. Click on the **Run** button and the output now shows the values sorted in ascending order:\n\n    ![Figure 4.29: Program output for ascending sort lambda ](img/C14583_04_29.jpg)\n\n    ###### 图 4.29:升序排序λ的程序输出\n\n15.  在调用`PrintVector()`函数后，添加以下代码行:\n\n    ```cpp\n    int threshold{25};\n    auto pred = [threshold] (int a) { return a > threshold; };\n    auto count = std::count_if(values.begin(), values.end(), pred);\n    std::cout << \"There are \" << count << \" values > \" << threshold << \"\\n\";\n    ```\n\n16.  Click on the **Run** button and the output now reports the number of `values > 25`:\n\n    ![Figure 4.30: Output for count_if lambda stored in variable ](img/C14583_04_30.jpg)\n\n    ###### 图 4.30:变量中存储的 count _ if 的输出\n\n17.  Add the following lines after the ones above and click the **Run** button:\n\n    ```cpp\n    threshold = 40;\n    count = std::count_if(values.begin(), values.end(), pred);\n    std::cout << \"There are \" << count << \" values > \" << threshold << \"\\n\";\n    ```\n\n    将生成以下输出:\n\n    ![Figure 4.31: Erroneous output by re-using the pred lambda ](img/C14583_04_31.jpg)\n\n    ###### 图 4.31:重复使用预驱动器λ的错误输出\n\n18.  程序错误地报告有`七(7)个值>40`；应该是`三(3)`。问题是，当λ被创建并存储在变量`pred`中时，它捕获了阈值的当前值`25`。将定义`pred`的线改为如下:\n\n    ```cpp\n    auto pred = [&threshold] (int a) { return a > threshold; };\n    ```\n\n19.  点击**运行**按钮，现在输出正确报告计数:\n\n![Figure 4.32: Correct output re-using the pred lambda ](img/C14583_04_32.jpg)\n\n###### 图 4.32:重新使用预驱动器λ校正输出\n\n在本练习中，我们实现了几个 lambda，使用了 lambda 表达式语法的各种特性，包括 init capture 和 mutable。\n\n#### 使用 lambdas\n\n虽然 lambdas 是 C++ 的一个强大特性，但是应该适当地使用它们。目标总是产生可读的代码。因此，虽然 lambda 可能很短并且很切题，但出于维护的目的，有时将功能分解到一个命名良好的方法中会更好。\n\n## 可变模板\n\n在*章节 2B**不允许鸭子-模板和演绎*中，我们介绍了泛型编程和模板。在 C++ 03 之前，模板已经是 C++ 的一部分。在 C++ 11 之前，模板仅限于固定数量的参数。在某些情况下，当需要可变数量的参数时，有必要为所需参数数量的每个变体编写一个模板。或者，也有像`printf()`这样的可变函数可以接受可变数量的参数。变量函数的问题在于它们不是类型安全的，因为通过`va_arg`宏的类型转换来访问参数。C++ 11 改变了这一切，引入了变量模板，单个模板可以接受任意数量的参数。C++ 17 通过引入`constexpr` if 构造改进了变量模板的编写，该构造允许基本案例模板与“`递归`模板合并。\n\n最好的方法是实现一个变量模板，并解释它是如何工作的。\n\n```cpp\n#include <iostream>\n#include <string>\ntemplate<typename T, typename... Args>\nT summer(T first, Args... args) {\n    if constexpr(sizeof...(args) > 0)\n          return first + summer(args...);\n    else\n        return first;\n}\nint main()\n{\n    std::cout << \"\\n\\n------ Variadic Templates ------\\n\";\n    auto sum = summer(1, 3, 5, 7, 9, 11);\n    std::cout << \"sum = \" << sum << \"\\n\";\n    std::string s1{\"ab\"};\n    std::string s2{\"cd\"};\n    std::string s3{\"ef\"};\n    std::string strsum = summer(s1, s2, s3);\n    std::cout << \"strsum = \" << strsum << \"\\n\";\n    std::cout << \"Complete.\\n\";\n    return 0;\n}\n```\n\n当我们运行这个程序时，我们得到以下输出:\n\n![Figure 4.33: Variadic Template Program Output ](img/C14583_04_33.jpg)\n\n###### 图 4.33:变量模板程序输出\n\n那么，变量模板有哪些部分？我们怎么读呢？考虑上述程序中的模板:\n\n```cpp\ntemplate<typename T, typename... Args>\nT summer(T first, Args... args) {\n    if constexpr(sizeof...(args) > 0)\n        return first + summer(args...);\n    else\n        return first;\n}\n```\n\n在上面的代码中:\n\n*   `类型名称...Args`声明`Args`为`模板参数包`。\n*   `Args...args`是一个`功能参数包`，是一个参数包，其类型由`Args`给出。\n*   `尺寸...(参数)`返回`参数`中的包装元素数量。这是一种特殊形式的群体扩张。\n*   `参数...`在对`summer()`的递归调用中扩展包。\n\n或者，您可以认为模板实际上相当于:\n\n```cpp\ntemplate<typename T, typename T1, typename T2, ..., typename Tn>\nT summer(T first, T1 t1, T2, ..., Tn tn) {\n    if constexpr(sizeof...( t1, t2, ..., tn) > 0)\n        return first + summer(t1, t2, ..., tn);\n    else\n        return first;\n}\n```\n\n当编译器在示例程序中处理`summer(1，3，5，7，9，11)`时，它执行以下操作:\n\n*   推导出`T`是 int，`Args...`是我们的参数包，带有< int，int，int，int，int >。\n*   由于包中有零个以上的参数，编译器首先生成`+ summer(参数...)`用省略号拆包模板参数转换`summer(args...)`进入`夏季(3、5、7、9、11)`。\n*   然后编译器生成`summer(3，5，7，9，11)`的代码。再次，导致`第一+夏季(args...)`适用于`夏季(5、7、9、11)`。\n*   重复这个过程，直到编译器必须为`summer(11)`生成代码。在这种情况下，触发`常量` if 语句的 else 子句，该语句简单地首先返回**。**\n\n **因为类型是由模板的参数决定的，所以我们不局限于具有相同类型的参数。我们已经在 STL–`STD::function`和 std::bind 中遇到了几个变量模板。\n\n还有另一种类型的变量模板，它将其参数转发给另一个函数或模板。这种模板本身没有什么作用，但是提供了一种标准的方法。一个例子是`make_unique`模板，可以实现为:\n\n```cpp\ntemplate<typename T, typename... Args>\nunique_ptr<T> make_unique(Args&&... args)\n{\n    return unique_ptr<T>(new T(std::forward<Args>(args)...));\n}\n```\n\n`make_unique`必须调用新的运算符来分配内存，然后为类型调用合适的构造函数。调用构造函数所需的参数数量可能有很大差异。这种形式的变量模板引入了一些附加包扩展:\n\n*   `Args&T3】...`表示我们有转发参考列表。\n*   `标准::转发<参数>(参数)...`包含扩展在一起的参数包，并且必须具有相同数量的元素–Args 是模板参数包，Args 是函数参数包。\n\n每当我们需要将一个函数调用转发到变量模板中的另一个函数调用时，都会使用这种模式。\n\n### 活动 1:实现多播事件处理程序\n\n微软最早推出`微软基础类` ( `MFC`)是在 1992 年，当时 C++ 还处于起步阶段。这意味着围绕类的许多设计选择都受到了限制。例如，事件的处理程序通常通过`OnEventXXX()`方法进行路由。这些通常使用宏作为从 MFC 类派生的类的一部分来配置。您的团队负责实现多播事件处理程序，更像 C#中可用的委托，使用包含函数对象的模板，并导致变量模板来实现变量参数列表。\n\n在 C#中，您按如下方式声明委托:\n\n```cpp\ndelegate int Handler(int parameter);\n```\n\n这使得 Handler 成为一种可以被赋值的类型，可以被调用。这基本上就是`std::function < >`在 C++ 中给我们提供的东西，除了多点投射的能力。您的团队被要求开发一个模板类`委托`，它可以以与 C#委托相同的方式执行。\n\n*   该委托将获取一个`变量参数列表`，但只返回`void`\n*   `运算符+=`将用于向委托添加新的回调\n*   它将使用任一语法`委托来调用。通知(……)`或`代表(……)`\n\n按照以下步骤开发代理模板:\n\n1.  从**第 4 课/练习 01** 文件夹加载准备好的项目，并将项目的当前构建器配置为可移植构建。\n2.  构建项目，配置启动器并运行单元测试(未通过一个虚拟测试)。建议测试运行者的名称为 **L4delegateTests** 。\n3.  实现一个`委托`类，该类可以用所有需要的方法包装单个处理程序，并支持回调的单个 int 参数。\n4.  更新模板类以支持多重转换。\n5.  将`委托`类转换为一个模板，该模板可以接受一个模板参数，该参数定义回调使用的参数类型。\n6.  将`委托`模板转换为变量模板，该模板可以接受零个或多个参数来定义传递给回调的类型。\n\n完成上述步骤后，预期输出如下所示:\n\n![Figure 4.34: Output from the successful implementation of Delegate ](img/C14583_04_34.jpg)\n\n###### 图 4.34:成功实现委托的输出\n\n#### 注意\n\n这项活动的解决方案可以在第 673 页找到。\n\n## 总结\n\n在这一章中，我们实现了一种数据和方法隐藏设计方法，PIMPL，它具有减少依赖和减少构建时间的额外好处。然后，我们将函数对象直接实现为自定义类，然后实现为 lambda 函数。然后，我们通过深入到可变模板中来扩展我们的模板编程技能，最终得到一个可用于事件回调处理的模板。在下一章中，我们将学习如何使用 C++ 的特性来开发多线程程序，并通过并发结构来管理它们的协作。**"
  },
  {
    "path": "docs/adv-cpp/06.md",
    "content": "# 六、哲学家的晚餐——线程和并发\n\n## 学习目标\n\n本章结束时，您将能够:\n\n*   创建同步和异步多线程应用\n*   应用同步来处理数据危险和竞争条件\n*   用 C++ 线程库原语开发高效的多线程代码\n*   使用多线程闭包的移动语义创建线程\n*   用未来、承诺和异步实现线程通信\n\n在本章中，我们将阐明多线程编程中基本术语之间的区别，学习如何编写多线程代码，找出 C++ 标准库为数据访问同步提供了哪些资源，学习如何防止我们的代码遇到竞争条件和死锁。\n\n## 简介\n\n在前一章中，我们讨论了 C++ 中不同类型的依赖和耦合。我们研究了如何在 C++ 中实现常见的 API 设计模式和习惯用法，标准库提供了哪些数据结构，以及它们的功效。我们还学习了如何使用功能对象、lambdas 和捕获。这些知识将帮助我们学习如何编写清晰高效的多线程程序。\n\n本章的标题包含并发编程中最重要的同步问题的名称——哲学家的晚餐。三言两语，这个定义如下。\n\n三位哲学家正坐在一张圆形餐桌旁，面前摆着几碗寿司。筷子放在每个相邻的哲学家之间。一次只有一个哲学家能用两根筷子吃他们的寿司。也许每个哲学家都会拿一根筷子，然后等到有人放弃另一根筷子。哲学家是三个工作过程的类比，是共享资源的筷子。“谁先拿两根筷子”象征着**比赛状态**。当每个哲学家拿着一根筷子，等待另一根筷子出现时，就会导致**僵局**。这个类比解释了多线程过程中会出现什么问题。\n\n我们将以对主要多线程概念的简单介绍开始本章。我们将考虑同步、异步和线程执行之间的区别。使用简单明了的例子，我们将从同步、数据危险和比赛条件开始。我们将找出它们为什么会出现在我们的代码中，以及我们如何管理它们。本章的下一部分专门介绍用于线程执行的 C++ 标准库。通过示例，我们将了解如何以及何时使用线程库原语，以及**如何移动语义**与线程进行交互。我们还将使用**期货**、**承诺**和**异步**从线程接收结果。\n\n这一章将以一个具有挑战性的活动结束，在这个活动中，我们将通过模拟参观者和画廊工作人员来创建一个艺术画廊模拟器。我们将开发一个多线程生成器，它将同时创建和删除美术馆的访客。接下来，我们将创建一个多线程类，负责在画廊中移动访问者。它们将使用同步技术相互交互。最后，我们将创建线程安全存储，其实例将从不同的线程访问。\n\n在下一节中，我们将阐明并发编程概念之间的细微区别:**同步**、**异步**和**线程**执行。\n\n## 同步、异步和线程执行\n\n并发编程的概念之间有细微的区别:`同步`、`异步`和`线程执行`。为了澄清这一点，我们将从最开始，从并发和并行程序的概念开始。\n\n### 并发\n\n`并发`的思想是多个任务同时执行。`并发`没有说明如何实现同时性。它仅表示在给定的时间段内将完成多个任务。任务可以是`从属`、`并行`、`同步`或`异步`。下图显示了并行工作的概念:\n\n![Figure 5.1: The abstraction of the concurrency - a few people working on the same computer ](img/C14583_05_01.jpg)\n\n###### 图 5.1:并发的抽象——几个人在同一台计算机上工作\n\n在上图中，三个人同时在一台计算机上工作。我们对他们这样做的方式不感兴趣，这与抽象的这个层次无关。\n\n### 平行度\n\n**并行**发生在多个任务同时执行的时候。由于硬件能力，这些任务并行工作。并行性的最好例子是多核处理器。对于并行执行，任务被分成完全独立的子任务，在不同的处理器内核中执行。之后，执行的结果可以合并。请看下图，了解并行性的概念:\n\n![](img/C14583_05_02.jpg)\n\n###### 图 5.2:并行性的抽象——所有的任务都由不同的人执行；他们不会互相影响\n\n在上图中，有三个人同时在自己的电脑上工作——嗯，他们是并行工作的。\n\n#### 注意\n\n`并发`和`并行`不是一回事。`并行`补充并发。它告诉我们任务是如何执行的:它们彼此独立，并在不同的计算单元中运行，即处理器或内核。\n\n现在，我们将顺利地转向线程执行概念。当我们谈论线程时，我们指的是执行线程。这是操作系统的抽象，允许我们同时执行几个任务。请记住，整个程序在单独的进程中执行。操作系统为进程分配**地址空间**、**处理器寄存器**，以及一些额外的资源。所有的工作线程都是在进程中创建的，并且共享相同的资源。每个进程至少有一个执行`main()`函数的线程。我们可以创建一个新的线程来执行，并分配一个开始函数作为这个线程的起点。\n\n#### 注意\n\n处理器的地址空间和寄存器称为**线程上下文**。当操作系统中断线程的工作时，它必须存储当前线程的上下文，并加载下一个线程的上下文。\n\n让我们考虑在下面的例子中创建一个新的线程。要创建一个新的线程，我们必须包含一个`<线程>`头文件。它包含用于管理线程的类和函数。实际上，有几种可能的方法来创建`std::thread`对象和执行线程，如下所示:\n\n*   创建一个`标准::线程`对象，无需显式初始化。记住，线程需要一个启动函数来运行它的作业。我们没有指出哪个函数是这个线程的主要函数。这意味着没有创建执行线程。让我们看看下面的代码示例，其中我们创建了一个空的`std::thread`对象:\n\n    ```cpp\n    #include <thread>\n    int main()\n    {\n      std::thread myThread;  \n      return 0;\n    }\n    ```\n\n*   创建一个`std::thread`对象，并传递一个指向函数的指针作为构造函数参数。现在，执行线程将被创建，并将从我们在构造函数中传递的函数开始它的工作。让我们看看下面的代码示例:\n\n    ```cpp\n    #include <iostream>\n    #include <thread>\n    void printHello()\n    {\n        std::cout << \"hello\" << std::endl;\n    }\n    int main()\n    {\n      std::thread myThread(printHello);\n      myThread.join();\n      return 0;\n    }\n    ```\n\n这里，我们创建了一个`std::thread`对象，并用函数指针对其进行了初始化。这是一个简单的函数，返回`void`，不取任何参数。然后，我们告诉主线程使用`join()`函数等待新线程完成。我们总是要`连接()`或`分离()`一个线程，直到`std::thread`对象的范围结束。如果我们不这样做，我们的应用将被操作系统使用`标准::终止`()函数终止，该函数在`标准::线程`析构函数中调用。除了函数指针之外，我们还可以传递任何可调用的对象，比如`lambda`、`std::function`，或者带有重载`运算符()`的类。\n\n#### 注意\n\n执行线程可以在 **std::thread** 对象销毁之前完成工作。它也可以在执行线程完成工作之前被析构。在销毁对象之前，始终将对象**标准::螺纹**连接到()或**分离()**。\n\n现在我们已经知道了创建线程的主要语法，我们可以继续下一个重要的概念。让我们找出同步、异步和多线程执行的含义。\n\n### 同步执行\n\n术语“同步执行”意味着每个子任务都将按顺序逐一执行。换句话说，这意味着如果我们有几个任务要执行，每个任务只能在前一个任务完成后才可以开始工作。这个术语没有指定执行任务的方式，也没有指定任务是在一个线程中执行还是在几个线程中执行。它只告诉我们执行的顺序。让我们回到哲学家晚餐的例子。在单线程世界里，哲学家们会一个接一个地吃东西。\n\n第一个哲学家拿了两根筷子，吃他们的寿司。然后，第二个哲学家拿了两根筷子，吃了他们的寿司。他们轮流，直到所有人都吃完寿司。请看下图，它代表了一个线程中四个任务的同步执行:\n\n![Figure 5.3: Synchronous execution in a single thread ](img/C14583_05_03.jpg)\n\n###### 图 5.3:单线程中的同步执行\n\n这里，每个任务都等待前一个任务完成。任务也可以在多个线程中同步执行。考虑下图，它表示在多个线程中同步执行四个任务。同样，每个任务都等待前一个任务完成:\n\n![](img/C14583_05_04.jpg)\n\n###### 图 5.4:多线程中的同步执行\n\n在这种情况下，每个任务都在一个单独的线程中启动，但只是在前一个线程完成其工作之后。在一个多线程的世界里，哲学家们仍然会一个接一个地吃东西，但差别很小。现在，他们每个人都有自己的筷子，但只能按照严格的顺序吃饭。\n\n#### 注意\n\n`同步执行`表示每个任务的完成时间同步。任务的执行顺序是这里的重点。\n\n让我们考虑在下面的代码示例上同步执行。当我们在一个线程中运行任务时，我们只是调用通常的函数。例如，我们实现了四个向终端打印消息的功能。我们以同步、单线程的方式运行它们:\n\n```cpp\n#include <iostream>\nvoid printHello1()\n{\n    std::cout << \"Hello from printHello1()\" << std::endl;    \n}\nvoid printHello2()\n{\n    std::cout << \"Hello from printHello2()\" << std::endl;    \n}\nvoid printHello3()\n{\n    std::cout << \"Hello from printHello3()\" << std::endl;    \n}\nvoid printHello4()\n{\n    std::cout << \"Hello from printHello4()\" << std::endl;    \n}\nint main()\n{\n    printHello1();\n    printHello2();\n    printHello3();\n    printHello4();\n    return 0;\n}\n```\n\n这里，我们逐个调用所有函数，每个下一个函数都在前一个函数执行之后运行。现在，让我们在不同的线程中运行它们:\n\n```cpp\n#include <iostream>\n#include <thread>\nvoid printHello1()\n{\n    std::cout << \"Hello from printHello1()\" << std::endl;    \n}\nvoid printHello2()\n{\n    std::cout << \"Hello from printHello2()\" << std::endl;    \n}\nvoid printHello3()\n{\n    std::cout << \"Hello from printHello3()\" << std::endl;    \n}\nvoid printHello4()\n{\n    std::cout << \"Hello from printHello4()\" << std::endl;    \n}\nint main()\n{\n    std::thread thread1(printHello1);\n    thread1.join();\n    std::thread thread2(printHello2);\n    thread2.join();\n    std::thread thread3(printHello3);\n    thread3.join();\n    std::thread thread4(printHello4);\n    thread4.join();\n    return 0;\n}\n```\n\n在前面的代码示例中，我们创建了四个线程，并立即将它们连接起来。因此，每个线程在运行之前都完成了它的工作。如您所见，任务没有任何变化——它们仍然以严格的顺序执行。\n\n### 异步执行\n\n在这种情况下，可以同时执行几个任务，而不会阻塞任何线程的执行。通常，主线程启动异步操作并继续执行。执行完成后，结果被发送到主线程。通常，执行异步操作与为其创建单独的线程无关。该任务可以由其他人执行，例如另一个计算设备、远程网络服务器或外部设备。让我们回到哲学家晚餐的例子。\n\n在`异步执行`的情况下，所有的哲学家都会有自己的筷子，并且会相互独立吃饭。寿司做好了，服务员上桌后，他们都开始吃饭，可以在自己的时间内吃完。\n\n#### 注意\n\n在`异步执行`中，由于所有的任务都是相互独立工作的，所以知道每个任务的完成时间并不重要。\n\n请看下图，它代表了多线程中四个任务的异步执行:\n\n![Figure 5.5: Asynchronous execution in multiple threads ](img/C14583_05_05.jpg)\n\n###### 图 5.5:多线程中的异步执行\n\n他们每个人都是在不同的时间开始和结束的。让我们用一个代码示例来考虑这个异步执行。例如，我们实现了四个向终端打印消息的功能。我们用不同的线程运行它们:\n\n```cpp\n#include <iostream>\n#include <thread>\n#include <chrono>\nvoid printHello1()\n{\n    std::cout << \"Hello from thread: \" << std::this_thread::get_id() << std::endl;    \n}\nvoid printHello2()\n{\n    std::cout << \"Hello from thread: \" << std::this_thread::get_id() << std::endl;    \n}\nvoid printHello3()\n{\n    std::cout << \"Hello from thread: \" << std::this_thread::get_id() << std::endl;    \n}\nvoid printHello4()\n{\n    std::cout << \"Hello from thread: \" << std::this_thread::get_id() << std::endl;    \n}\nint main()\n{\n    std::thread thread1(printHello1);\n    std::thread thread2(printHello2);\n    std::thread thread3(printHello3);\n    std::thread thread4(printHello4);\n    thread1.detach();\n    thread2.detach();\n    thread3.detach();\n    thread4.detach();\n\n    using namespace std::chrono_literals;\n    std::this_thread::sleep_for(2s);\n    return 0;\n}\n```\n\n让我们看看这里会发生什么。我们使用了前面例子中的四个函数，但是它们有一点改变。我们使用`STD::this _ thread::get _ ID()`函数添加了线程唯一 ID 的打印。该函数返回`std::thread::id`对象，该对象代表线程的唯一 id。这个类为输出和比较重载了操作符，所以我们可以用不同的方式使用它。例如，我们可以检查线程标识，如果它是主线程的标识，我们可以执行一个特殊的作业。在我们的例子中，我们可以将线程标识打印到终端。接下来，我们创建了四个线程并分离它们。这意味着没有线程会等待另一个线程完成工作。从这一刻起，它们变成了**守护线程**。\n\n他们将继续他们的工作，但是没有人知道这件事。然后，我们使用`STD::this _ thread::sleep _ for(2s)`函数让主线程等待两秒钟。我们这样做是因为当主线程完成它的工作时，应用将停止，我们将无法在终端中查看分离线程的输出。以下屏幕截图是终端输出的示例:\n\n![Figure 5.6: The result of an example execution ](img/C14583_05_06.jpg)\n\n###### 图 5.6:示例执行的结果\n\n在您的 IDE 中，输出可能会随着执行顺序的未定义而改变。异步执行的一个真实例子可以是一个互联网浏览器，您可以在其中打开多个选项卡。当一个新的选项卡打开时，应用启动一个新的线程并分离它们。尽管线程独立工作，但它们可以共享一些资源，如文件处理程序，来写日志或做其他事情。\n\n#### 注意\n\n`std::thread`有一个名为`get_id()`的成员函数，返回`std::thread`实例的唯一 id。如果`std::thread`实例未初始化或已连接或分离，`get_id()`将返回默认的`std::thread::id`对象。这意味着没有执行线程与当前的`std::thread`实例相关联。\n\n让我们用一些伪代码来展示一个由另一个计算单元完成计算的例子。例如，假设我们开发了一个通过货币兑换进行计算的应用。用户输入一种货币的金额，选择另一种货币进行兑换，应用向他们显示该货币的金额。后台应用向保存所有货币汇率的远程服务器发送请求。\n\n远程服务器计算给定货币的金额，并将结果发回。此时，您的应用会显示一个进度条，并允许用户执行其他操作。当它收到结果时，它会在窗口上显示它们。让我们看看下面的代码:\n\n```cpp\n#include <thread>\nvoid runMessageLoop()\n{\n    while (true)\n    {\n        if (message)\n        {\n            std::thread procRes(processResults, message);\n            procRes.detach();\n        }\n    }\n}\nvoid processResults(Result res)\n{\n    display();\n}\nvoid sendRequest(Currency from, Currency to, double amount)\n{\n    send();\n}\nvoid displayProgress()\n{\n}\nvoid getUserInput()\n{\n    Currency from;\n    Currency to;\n    double amount;\n    std::thread progress(displayProgress);\n    progress.detach();\n    std::thread request(sendRequest, from, to, amount);\n    request.detach();\n}\nint main()\n{\n    std::thread messageLoop(runMessageLoop);\n    messageLoop.detach();\n\n    std::thread userInput(getUserInput);\n    userInput.detach();    \n    return 0;\n}\n```\n\n让我们看看这里会发生什么。在`main()`函数中，我们创建了一个名为`messageLoop`的线程，该线程执行`runMessageLoop()`函数。一些检查服务器是否有新结果的代码可以放在这个函数中。如果收到新的结果，它会创建一个新的线程`procures`，该线程将在一个窗口中显示结果。我们还在`main()`函数中创建了另一个线程`userInput`，它从用户那里获取货币和金额，并创建了一个新的线程`request`，它将向远程服务器发送请求。发送请求后，它会创建一个新的线程`进度`，该线程将显示一个进度条，直到收到结果。因为所有的线程都是分离的，所以它们能够独立工作。当然，这只是伪代码，但主要思想很清楚——我们的应用向远程服务器发送请求，远程服务器为我们的应用执行计算。\n\n让我们用日常生活中的一个例子来复习一下我们所学的并发概念。这是一个背景，你必须编写一个应用，并提供与之相关的所有文档和架构概念:\n\n*   单线程工作:你自己写。\n*   多线程工作:你邀请你的朋友一起写一个项目。有人编写架构概念，有人负责文档工作，你专注于编码部分。所有参与者相互交流以澄清任何问题并共享文档，例如关于规格的问题。\n*   平行工作:任务是分的。有人为项目编写文档，有人设计图表，有人编写测试用例，你独立工作。参与者根本不交流。\n*   同步工作:在这种情况下，你们每个人都无法理解他们应该做什么。因此，你们都决定一个接一个地工作。当架构工作完成时，开发人员开始编写代码。然后，当开发工作完成时，有人开始编写文档。\n*   异步工作:在这种情况下，你雇佣一家外包公司来完成项目。当他们在开发项目时，你将会从事一些其他的任务。\n\n现在，让我们将我们的知识应用于实践，并解决一个练习，看看它是如何工作的。\n\n### 练习 1:以不同的方式创建线程\n\n在本练习中，我们将编写一个创建四个线程的简单应用；其中两个将以同步方式工作，两个以异步方式工作。它们都会打印一些符号到终端，这样我们就可以看到操作系统是如何切换线程执行的。\n\n#### 注意\n\n在项目设置中添加 pthread 链接器标志，让编译器知道您将使用线程库。对于 Eclipse IDE，您可以按照以下路径执行此操作:**项目** - > **属性**->**C/c++ Build**->**设置** - > **G++ 链接器** - > **杂项** - > **链接器标志输入“-pthread”**。该路径对`Eclipse 版本有效:3.8.1`，不同版本可能有所不同。\n\n执行以下步骤完成本练习:\n\n1.  包括一些支持线程化的头文件，即`<【线程化】>`，支持流化，即`<【iostream】>`，支持功能对象，即`<【功能化】>` :\n\n    ```cpp\n    #include <iostream>\n    #include <thread>\n    #include <functional>\n    ```\n\n2.  实现一个自由函数`打印数字()`，在`中为`循环打印从 0 到 100 的数字:\n\n    ```cpp\n    void printNumbers()\n    {\n        for(int i = 0; i < 100; ++ i)\n        {\n            std::cout << i << \" \";\n        }\n        std::cout << std::endl;\n    }\n    ```\n\n3.  实现一个可调用对象，即一个带有重载**运算符()**的 **Printer** 类，该类在**循环中打印一个从 0 到 100000 的“*”符号。对于每一次 **200** 迭代，打印一个新的线符号以获得更易读的输出:\n\n    ```cpp\n    class Printer\n    {\n        public:\n        void operator()()\n        {\n            for(int i = 0; i < 100000; ++ i)\n            {\n                if (!(i % 200))\n                {\n                    std::cout << std::endl;\n                }\n                std::cout << \"*\";\n            }\n        }\n    };\n    ```** \n4.  进入`main()`功能，然后创建一个名为`print reverse`的 lambda 对象，该对象在`中为`循环打印从 100 到 0 的数字:\n\n    ```cpp\n    int main()\n    {\n        auto printRevers = []()\n        {\n            for(int i = 100; i >= 0; --i)\n            {\n                std::cout << i << \" \";\n            }\n            std::cout << std::endl;\n        };\n        return 0;\n    }\n    ```\n\n5.  实现一个名为 **printOther** 的 **std::function** 对象，该对象在循环的**中打印从 **0** 到 **100000** 的“^”符号。每重复 **200 次**，打印一个新的线符号，以便输出更易读:\n\n    ```cpp\n    std::function<void()> printOther = []()\n    {\n        for(int i = 0; i < 100000; ++ i)\n        {\n            if (!(i % 200))\n            {\n                std::cout << std::endl;\n            }\n            std::cout << \"^\";\n        }\n    };\n    ```** \n6.  创建第一个线程`thr1`，并将`printNumbers`自由函数传递给它的构造函数。加入其中:\n\n    ```cpp\n    std::thread thr1(printNumbers);\n    thr1.join();\n    ```\n\n7.  创建第二个线程`thr2`，并将`print reverse`lambda 对象传递给它的构造函数。加入其中:\n\n    ```cpp\n    std::thread thr2(printRevers);\n    thr2.join();\n    ```\n\n8.  创建名为`打印`的`打印机`类的实例。创建第三个线程`thr3`，并用`打印`对象初始化。使用`分离()`方法将其分离:\n\n    ```cpp\n    Printer print;\n    std::thread thr3(print);\n    thr3.detach();\n    ```\n\n9.  创建最后一个线程`thr4`，并用`printOther`对象初始化它。拆下来:\n\n    ```cpp\n    std::thread thr4(printOther);\n    thr4.detach();\n    ```\n\n10.  在退出`main()`函数之前，添加`std::getchar()`函数调用。这避免了关闭应用。我们将有可能看到分离线程是如何工作的:\n\n    ```cpp\n    std::getchar();\n    ```\n\n11.  Run this code in your editor. You will see that `thr1` starts execution and the program waits. After `thr1` has finished, `thr2` starts execution and the program waits. This is an example of synchronous execution. After `thr2` has finished its work, threads `thr3` and `thr4` start execution. They are detached, so the program can proceed with the execution. In the following output, you will see that the symbols are mixed. This happens because the operating system performs interruptions and the threads work at the same time.\n\n    您的输出将类似于以下内容:\n\n![](img/C14583_05_07.jpg)\n\n###### 图 5.7:练习执行的结果\n\n在本练习中，我们实现了四种不同的初始化线程的方法:使用自由函数、lambda 对象、可调用对象和`std::function`对象。还有一些初始化线程的方法，但是我们将在下一节中考虑它们。我们还回顾了如何在多线程中实现同步程序。我们还试图实现异步程序，并看到线程确实同时独立工作。在下一节中，我们将了解数据危险和竞争条件，以及如何通过使用同步技术来避免它们。\n\n## 查看同步、数据危险和比赛条件\n\n多线程编程的关键挑战是知道线程如何处理**共享数据**。共享数据，也称为资源，不仅是变量，也是文件描述符和环境变量，甚至是 Windows 注册表。例如，如果线程只是读取数据，那么就没有问题，也不需要同步。但是，如果至少有一个线程编辑了数据，**比赛条件**可能会出现。通常，对数据的操作不是原子的，也就是说，它们需要几个步骤。即使是最简单的数值变量的增量操作也是在以下三个步骤中执行的:\n\n1.  读取变量值。\n2.  增加它。\n3.  写下新值。\n\n由于操作系统中断，线程可以在完成操作之前停止。例如，我们有线程 A 和 B，并且有一个等于 0 的变量。\n\n线程 A 开始增量:\n\n1.  读取变量值(var = 0)。\n2.  递增(tmp = 1)。\n3.  被操作系统中断。\n\n线程 B 开始增量:\n\n1.  读取变量值(var = 0)。\n2.  递增(tmp = 1)。\n3.  写入新值(var = 1)。\n4.  被操作系统中断。\n\n线程 A 继续递增:\n\n1.  写入新值(var = 1)。\n\n因此，我们期望变量在工作完成后等于 2，但事实上，它等于 1。请看下图，以便更好地理解这个例子:\n\n![Figure 5.8: Two threads increment the same shared variable ](img/C14583_05_08.jpg)\n\n###### 图 5.8:两个线程递增同一个共享变量\n\n让我们回到哲学家的晚餐类比。最初的问题是一个哲学家只有一根筷子。如果他们都饿了，那么他们会赶紧去拿两根筷子。第一个抓起两根筷子的哲学家会第一个吃饭，其他人必须等待。他们将争夺木棒。\n\n现在，让我们将我们的知识应用到实践中，并编写一些代码，看看竞争条件如何出现在我们的代码中，并可能破坏我们的数据。\n\n### 练习 2:写一个比赛条件的例子\n\n在本练习中，我们将编写一个简单的应用来演示比赛条件。我们将创建一个“先检查后行动”比赛条件的经典示例。我们将创建一个线程，执行两个数的除法。我们将通过引用传递这些数字。经过检查，如果股息等于 0，我们将设置一个小超时。此时在主线程中，我们将把被除数设置为 0。当子线程醒来时，它将执行到 0 的除法。这将导致应用崩溃。我们还将添加一些日志来查看执行流程。\n\n#### 注意\n\n默认情况下，所有变量在传递给线程时都会被复制。要将变量作为引用传递，请使用`std::ref()`函数。\n\n首先，我们实现没有竞争条件的代码，并确保它按预期工作。请执行以下步骤:\n\n1.  包括支持线程的头文件，即`<线程>`，支持流的头文件，即`< iostream >`，以及支持功能对象的头文件，即`<【功能性】>` :\n\n    ```cpp\n    #include <iostream>\n    #include <chrono>\n    #include <thread>\n    ```\n\n2.  实现`除()`函数，该函数执行两个整数的除。通过引用传递`除数`和`被除数`变量。检查股息是否等于 0。然后，添加日志:\n\n    ```cpp\n    void divide(int& divisor, int& dividend)\n    {\n        if (0 != dividend)\n        {\n            std::cout << \"Dividend = \" << dividend << std::endl;\n            std::cout << \"Result: \" << (divisor / dividend) << std::endl;    \n        }\n        else\n        {\n            std::cout << \"Error: dividend = 0\" << std::endl;\n        }\n    }\n    ```\n\n3.  进入`main()`函数，创建两个名为`除数`和`被除数`的整数，并用任意非零值初始化:\n\n    ```cpp\n    int main()\n    {\n        int divisor = 15;\n        int dividend = 5;\n        return 0;\n    }\n    ```\n\n4.  Create the `thr1` thread, pass the `divide` function, use `divisor` and `dividend` by reference, and then detach the thread:\n\n    ```cpp\n    std::thread thr1(divide, std::ref(divisor), std::ref(dividend));\n    thr1.detach();\n    std::getchar();\n    ```\n\n    #### 注意\n\n    在`std::this_thread`命名空间中，有一个名为`sleep_for`的函数会在给定的时间段内阻塞线程。作为一个参数，它需要`标准::时间::持续时间`-一个模板类来表示时间间隔。\n\n5.  Run this code in your editor. You will see that the `divide()` function works correctly in `thr1`. The output looks as follows:\n\n    ![Figure 5.9: The result of the correct exercise execution ](img/C14583_05_09.jpg)\n\n    ###### 图 5.9:正确执行练习的结果\n\n    现在，我们将继续并做出改变，以展示比赛条件。\n\n6.  如果处于状态，则返回到该功能并在 **2s** 中为**之后的子线程设置睡眠时间。添加日志:\n\n    ```cpp\n    if (0 != dividend)\n    {\n        std::cout << \"Child thread goes sleep\" << std::endl;\n        using namespace std::chrono_literals;\n        std::this_thread::sleep_for(2s);\n        std::cout << \"Child thread woke up\" << std::endl;\n        std::cout << \"Dividend = \" << dividend << std::endl;\n        std::cout << (divisor / dividend) << std::endl;\n    }\n    ```** \n7.  Go back to the `main()` function and set the sleeping time in `1s` for the main thread. After that, set the `dividend` variable to `0`. Add the logs:\n\n    ```cpp\n    std::cout << \"Main thread goes sleep\" << std::endl;\n    using namespace std::chrono_literals;\n    std::this_thread::sleep_for(1s);\n    std::cout << \"Main thread woke up\" << std::endl;\n    dividend = 0;   \n    std::cout << \"Main thread set dividend to 0\" << std::endl;\n    ```\n\n    #### 注意\n\n    `STD::chrono _ 常值`命名空间包含用于时间表示的常值:``h``用于`小时`，``分钟``用于`分钟`，``s``用于`秒`，``ms``用于`毫秒`，``us``用于`微秒`和` `[要使用它们，您只需将它们添加到数字的末尾，例如 1、1 分钟、1 小时等。\n\n8.  在退出`main()`函数之前，添加`std::getchar()`函数调用。这避免了我们关闭应用，我们将有可能看到分离的线程是如何工作的:\n\n    ```cpp\n    std::getchar();\n    ```\n\n9.  在编辑器中运行这段代码。你会看到主线程休眠`1s`。然后，子线程进入`if`状态，休眠`2s`，这意味着它验证了一个`红利`，它不等于`0`。然后，主线程唤醒并将`被除数`变量设置为 0。然后，子线程醒来并执行除法。但是因为现在`红利`等于`0`，应用崩溃了。如果在调试模式下运行此示例，您将看到一个`SIGFPE 异常`，并显示一条消息:“算术异常”。您将获得以下输出:\n\n![Figure 5.10: The result of the exercise’s execution with race conditions ](img/C14583_05_10.jpg)\n\n###### 图 5.10:比赛条件下练习的执行结果\n\n在本练习中，我们考虑了“先检查后行动”的比赛条件。我们已经为线程设置了睡眠周期来模拟操作系统中断，但是在现实世界的程序中，这种情况很可能会发生，但也可能不会。这完全取决于操作系统及其调度程序。这使得调试和修复比赛状态变得非常困难。为了避免本例中的竞争情况，我们可以采取以下几种方式:\n\n*   将变量的副本传递给线程函数，而不是传递引用。\n*   使用标准库原语在线程之间同步对共享变量的访问。\n*   在主线程将一个`被除数`值变为 0 之前加入子线程。\n\n让我们再看几个方法来解决这个比赛状态。所有这些都依赖于你试图实现的任务。在下一节中，我们将考虑由 C++ 标准库提供的同步原语。\n\n### 数据危害\n\n以前，我们认为最无害的例子，但有时，存在数据被损坏的情况，这导致未定义的程序行为或异常终止。这种由于比赛条件或简单的错误设计造成的数据损坏被称为**数据危险**。一般来说，这个术语意味着一项工作的最终结果取决于线程的执行顺序。如果不同的线程使用共享数据或全局变量，那么由于不同线程执行任务的顺序不正确，结果可能会有所不同。这是由于多线程数据之间的依赖性造成的。这种依赖性问题有条件地分为三类:\n\n*   一**真相依** : **写后读** (RAW)\n*   一个**反依赖** : **读完**写(WAR)\n*   一个**输出依赖** : **写后写** (WAW)\n\n### 原始相关性\n\n当一个线程计算另一个线程使用的值时，就会发生 RAW 依赖关系。例如`线程 A`应该完成它的工作，并将结果写入一个变量。`线程 B`必须读取这个变量的值并完成它的工作。在伪代码中，如下所示:\n\n```cpp\nThread A: a = doSomeStuff();\nThread B: b = a - doOtherStuff();\n```\n\n如果`线程 B`先执行，就会出现困难。这会导致`线程 B`读取无效值。应该严格保证线程的执行顺序。`线程 B`必须读取变量的值，但只有在`线程 A`写完之后。否则，将导致未定义的行为。下图将帮助您阐明导致数据危险的原始数据依赖性:\n\n![Figure 5.11: RAW data dependency between two threads   ](img/C14583_05_11.jpg)\n\n###### 图 5.11:两个线程之间的原始数据依赖性\n\n### 战争属地\n\n当一个线程更改另一个线程使用的数据时，就会出现 **WAR 依赖关系**。例如，`线程 A`必须读取一个变量的值并完成它的工作。之后，`线程 B`应该做好自己的工作，将结果写入一个变量。在伪代码中，如下所示:\n\n```cpp\nThread A: b = a - doSomeStuff();\nThread B: a = doOtherStuff();\n```\n\n如果`线程 B`先执行，就会出现困难。会导致`线程 B`在`线程 A`读取之前改变值。应该严格保证线程的执行顺序。`线程 B`只有在`线程 A`读取变量的值后，才应该将新值写入变量。下图将帮助您阐明导致数据危险的原始数据依赖性:\n\n![Figure 5.12: WAR data dependency between two threads ](img/C14583_05_12.jpg)\n\n###### 图 5.12:两个线程之间的 WAR 数据依赖关系\n\n### WAW 相关性\n\n当几个线程改变同一个变量的值，并且一些线程为其计算进行读取时，就会出现 **WAW 依赖关系**。例如，`线程 A`执行其作业并将结果写入变量。`线程 B`读取变量值并执行其作业。`线程 C`执行其作业，并将结果写入同一个变量。在伪代码中，如下所示:\n\n```cpp\nThread A: a = doSomeStuff();\nThread B: b = a - doOtherStuff();\nThread C: a = doNewStuff();\n```\n\n如果`线程 C`在线程 A 和 B 之前执行，将会出现困难。这将导致`线程 B`读取预计不会被读取的值。应该严格保证线程的执行顺序。`线程 C`必须向变量写入一个新值，但前提是`线程 A`已经写入了它的值，`线程 B`已经读取了它。下图将帮助您阐明导致数据危险的 WAW 数据依赖性:\n\n![Figure 5.13: WAW data dependency between two threads ](img/C14583_05_13.jpg)\n\n###### 图 5.13:两个线程之间的 WAW 数据依赖关系\n\n### 资源同步\n\n为了防止竞争和数据危害，有一个共享数据锁定机制，其中一个流打算更改或读取这些数据。这个机制叫做**资源同步**。对于同步，我们需要分配改变或读取共享资源的代码片段。这样的代码片段被称为`关键部分`。同步包括当一个线程进入时阻塞关键部分。也打算执行这个关键部分的代码的其他线程将被阻塞。当执行关键部分的线程离开它时，锁被释放。然后，故事会随着下一个线索重复。\n\n考虑前面的例子，有一个增量，但是现在有同步访问。请记住，我们有线程 A 和 B，并且有一个等于 0 的变量。\n\n线程 A 开始增量:\n\n1.  进入临界区并锁定。\n2.  读取变量值(var = 0)。\n3.  递增(tmp = 1)。\n4.  被操作系统中断。\n\n线程 B 开始增量:\n\n1.  试图进入临界区；它被锁定了，所以线程正在等待。\n\n线程 A 继续递增:\n\n1.  写入新值(var = 1)。\n\n线程 B 继续递增:\n\n1.  进入临界区并锁定。\n2.  读取变量值(var = 1)。\n3.  递增(tmp = 2)。\n4.  写入新值(var = 2)。\n\n两个线程完成后，变量包含正确的结果。因此，同步确保共享数据不会被损坏。请看下图，以更好地理解这个例子:\n\n![Figure 5.14: Two threads increment the same shared variable in a synchronized way ](img/C14583_05_14.jpg)\n\n###### 图 5.14:两个线程以同步的方式增加同一个共享变量\n\n突出关键部分并预测非同步访问的可能后果是一项非常困难的任务。因为过度同步否定了多线程工作的本质。然而，如果两个或三个线程在一个关键部分上工作得相当快，那么程序中可能会有几十个线程在关键部分被阻塞。这将大大降低程序的速度。\n\n### 事件同步\n\n还有另一种同步线程工作的机制–**事件同步**。这意味着当其中一个线程暂停其工作，直到另一个线程发出某个事件发生的信号时，线程的工作才会同步。例如，有`线程 A`，它接收来自另一个进程的消息。它将消息写入队列并等待新消息。还有一个线程`线程 B`，处理这些消息。它从队列中读取消息，并对它们执行一些操作。当没有消息时，`线程 B`正在休眠。当`线程 A`收到新消息时，唤醒`线程 B`并进行处理。下图清楚地说明了两个线程的事件同步:\n\n![Figure 5.15: Event synchronization of two threads ](img/C14583_05_15.jpg)\n\n###### 图 5.15:两个线程的事件同步\n\n然而，即使在同步代码中也会出现竞争条件的另一个原因——类的有缺陷的接口。为了理解这是什么，让我们考虑以下示例:\n\n```cpp\nclass Messages\n{\n    public:\n    Messages(const int& size)\n    : ArraySize(size)\n    , currentIdx(0)\n    , msgArray(new std::string[ArraySize])\n    {}\n    void push(const std::string& msg)\n    {\n        msgArray[currentIdx++ ] = msg;\n    }\n    std::string pop()\n    {\n        auto msg = msgArray[currentIdx - 1];\n        msgArray[currentIdx - 1] = \"\";\n        --currentIdx;\n        return msg;\n    }\n    bool full()\n    {\n        return ArraySize == currentIdx;\n    }\n    bool empty()\n    {\n        return 0 == currentIdx;\n    }\n    private:\n    const int ArraySize;\n    int currentIdx;\n    std::string * msgArray;\n};\n```\n\n这里，我们有一个名为`Messages`的类，它有一个动态分配的字符串数组。在构造函数中，它获取数组的大小并创建一个给定大小的数组。它有一个函数`full()`，如果数组已满则返回`true`，否则返回`false`。它还有一个`empty()`函数，如果数组为空则返回 true，否则返回 false。在推送新值和检查数组是否为空之前，以及在从数组弹出新值之前，用户有责任检查数组是否已满。这是类接口不良导致竞争条件的一个例子。即使我们用锁保护`push()`和`pop()`功能，竞态条件也不会消失。让我们看看下面使用`消息`类的例子:\n\n```cpp\nint main()\n{\n    Messages msgs(10);\n    std::thread thr1([&msgs](){\n    while(true)\n    {\n        if (!msgs.full())\n        {\n            msgs.push(\"Hello\");\n        }\n        else\n        {\n            break;\n        }\n    }});\n    std::thread thr2([&msgs](){\n    while(true)\n    {\n        if (!msgs.empty())\n        {\n            std::cout << msgs.pop() << std::endl;\n        }\n        else\n        {\n            break;\n        }\n    }});\n    thr1.detach();\n    thr2.detach();\n    using namespace std::chrono_literals;\n    std::this_thread::sleep_for(2s);\n    return 0;\n}\n```\n\n这里，我们创建了一个`msgs`变量，然后创建了第一个线程，该线程将值推送到`msgs`。然后，我们创建了第二个线程，它从数组中弹出值并分离它们。即使我们通过使用锁定机制来保护所有功能，其中一个线程也可以检查数组的大小，并且可以被操作系统中断。此时，另一个线程可以更改数组。当第一个线程继续工作时，它可以尝试推进到完整数组或从空数组弹出。因此，同步只有在设计良好的配对中才有效。\n\n### 死锁\n\n还有一个同步问题。让我们回到哲学家晚餐的例子。最初的问题是一个哲学家只有一根筷子。所以，他们可以通过互相分享筷子来一个接一个地吃寿司。虽然他们需要很长时间才能吃完寿司，但他们都会吃得很好。但是如果他们每个人同时拿着一根筷子，不想分享第二根筷子，他们就不能吃寿司，因为他们每个人都将永远等待第二根筷子。这会导致**死锁**。当两个线程正在等待另一个线程继续其工作时，就会发生这种情况。死锁的原因之一是当一个线程加入另一个线程，但是另一个线程加入第一个线程。因此，当两个线程相互连接时，它们都不能继续工作。让我们考虑以下死锁示例:\n\n```cpp\n#include <thread>\nstd::thread* thr1;\nstd::thread* thr2;\nvoid someStuff()\n{\n    thr1->join();\n}\nvoid someAnotherStuff()\n{\n    thr2->join();\n}\nint main()\n{\n    std::thread t1(someStuff); \n    std::thread t2(someAnotherStuff);\n    thr1 = &t1;\n    thr2 = &t2;\n    using namespace std::chrono_literals;\n    std::this_thread::sleep_for(2s);\n    return 0;\n}\n```\n\n在主功能中，我们有两个线程，`t1`和`t2`。我们用`something()`函数初始化了`t1`线程，这做了一些有用的工作。我们还用`sometherestuff()`函数初始化了`t2`线程，这做了一些更有用的工作。在由`t2`执行的函数中，我们有指向这些线程的全局指针和指向`t1`线程的连接指针。我们还将指向`t2`线程的指针加入到函数中，该函数由`t1`执行。通过这样做，他们互相结合。这会导致死锁。\n\n在下一节中，我们将考虑用于同步的 C++ 线程库原语以及死锁的另一个原因。\n\n### 移动多线程闭包的语义\n\n`std::thread`类不可复制，但是如果我们想存储几个线程，或者 10 个或者 20 个呢？当然，我们可以创建线程的数量，然后我们可以像这样连接或分离它们:\n\n```cpp\nstd::thread thr1(someFunc);\nstd::thread thr2(someFunc);\nstd::thread thr3(someFunc);\nstd::thread thr4(someFunc);\nstd::thread thr5(someFunc);\nthr1.join();\nthr2.join();\nthr3.join();\nthr4.join();\nthr5.join();\n```\n\n但是在 **STL 容器**中存储一堆线程更方便，比如线程的向量:\n\n```cpp\nstd::vector<std::thread> threads;\n```\n\nSTL 容器不能用于不支持**复制语义**的对象。感谢**移动语义**，我们可以存储具有移动构造函数的不可复制对象，并将赋值操作符移动到容器中。然后，我们可以使用带有`std::move()`函数的线程向量。为了初始化容器中的线程，我们可以做如下事情:\n\n```cpp\nfor (int i = 0; i < 10; i++) \n{\n    auto t = std::thread([i]()\n    {\n        std::cout << \"thread: \" << i << \"\\n\";\n    });\n    threads.push_back(std::move(t));\n}\n```\n\n然后，我们可以加入或分离它们:\n\n```cpp\nfor (auto& thr: threads) \n{\n    if (thr.joinable())\n    {\n        thr.join();\n    }\n}\n```\n\n当我们将`std::thread`对象存储为类成员时，移动语义也很有用。在这种情况下，我们应该仔细设计我们的类，删除复制构造函数和赋值操作符，并实现一个新的移动构造函数和移动赋值操作符。让我们考虑下面这样一个类的代码示例:\n\n```cpp\nclass Handler\n{\n    std::thread  threadHandler;\n\npublic:\n    Handler(const Handler&) = delete;\n    Handler& operator=(const Handler&) = delete;\n    Handler(Handler && obj)\n    : threadHandler(std::move(obj.threadHandler))\n    {}\n    Handler & operator=(Handler && obj)\n    {\n        if (threadHandler.joinable())\n        {\n            threadHandler.join();\n        }\n        threadHandler = std::move(obj.threadHandler);\n        return *this;\n    }\n    ~Handler()\n    {\n    if (threadHandler.joinable())\n        {\n            threadHandler.join();\n        }\n    }\n};\n```\n\n在移动赋值操作符中，我们首先检查线程是否可连接。如果是这样，我们加入它，只有在那之后，我们才执行赋值操作。\n\n#### 注意\n\n如果不在线程对象上使用`join()`或`detach()`，我们就不应该将一个线程对象分配给另一个线程对象。这将导致一个`标准::终止()`函数调用。\n\n也可以使用`std::move()`函数将对象移动到线程函数中。对于复制大对象可能有帮助，这是不可取的。让我们执行一个练习来确保对象可以被移动到线程函数中。\n\n### 练习 3:将对象移动到线程函数\n\n在本练习中，我们将编写一个简单的应用，演示`std::move()`如何为`std::thread`类工作。我们将创建一个同时具有复制构造函数和移动构造函数的类，以查看当我们将这个类的对象移动到`std::thread`函数中时将调用哪个。执行以下步骤完成本练习:\n\n1.  包括支持线程的报头，即 **<线程>** ，以及支持流的报头，即**<iostream>T4:\n\n    ```cpp\n    #include <iostream>\n    #include <thread>\n    ```** \n2.  实现`处理程序`类，它有默认的构造函数、析构函数、复制构造函数、赋值操作符、移动构造函数和移动赋值操作符。除了打印日志，他们什么都不会做:\n\n    ```cpp\n    class Handler\n    { \n    public:\n        Handler()\n        {\n            std::cout << \"Handler()\" << std::endl;\n        }\n        Handler(const Handler&)\n        {\n            std::cout << \"Handler(const Handler&)\" << std::endl;\n        }\n        Handler& operator=(const Handler&)\n        {\n            std::cout << \"Handler& operator=(const Handler&)\" << std::endl;\n            return *this;\n        }\n        Handler(Handler && obj)\n        {\n            std::cout << \"Handler(Handler && obj)\" << std::endl;\n        }\n        Handler & operator=(Handler && obj)\n        {\n            std::cout << \"Handler & operator=(Handler && obj)\" << std::endl;\n            return *this;\n        }\n        ~Handler()\n        {\n            std::cout << \"~Handler()\" << std::endl;\n        }\n    };\n    ```\n\n3.  实现`doSomeJob()`功能，实际上这里什么都不做，只是打印一条日志消息:\n\n    ```cpp\n    void doSomeJob(Handler&& h)\n    {\n        std::cout << \"I'm here\" << std::endl;\n    }\n    ```\n\n4.  进入`主()`功能，创建`处理程序`类型的`处理程序`变量。创建`thr1`，传递`doSomeJob()`函数，移动处理程序变量:\n\n    ```cpp\n    Handler handler;\n    std::thread thr1(doSomeJob, std::move(handler));\n    ```\n\n5.  分离`thr1`线程，为主线程添加一个小休眠，以避免关闭应用。我们将能够看到分离线程的输出:\n\n    ```cpp\n    thr1.detach();\n    using namespace std::chrono_literals; \n    std::this_thread::sleep_for(5s);\n    ```\n\n6.  Run this code in your editor. In the terminal log, from the default constructor, you will see two logs from the move operator, one log from a destructor, a message from the `doSomeJob()` function, and, finally, two other log messages from the destructor. We can see that the move constructor is called twice.\n\n    您将获得以下输出:\n\n![](img/C14583_05_16.jpg)\n\n###### 图 5.16:练习执行的结果\n\n可以看到，`处理程序`对象被移动到线程函数中。尽管如此，所有没有使用`std::ref()`函数传递的参数都被复制到了线程的内存中。\n\n让我们考虑一个有趣的问题。大家可能还记得，当我们初始化`std::thread`时，所有的构造函数参数都会被复制到线程内存中，包括一个可调用对象——一个 lambda、一个函数或者一个 std::function。但是如果我们的可调用对象不支持复制语义呢？例如，我们创建了一个只有移动构造函数和移动赋值运算符的类:\n\n```cpp\nclass Converter\n{\n    public:\n    Converter(Converter&&)\n    {\n    }\n    Converter& operator=(Converter&&)\n    {\n        return *this;\n    }\n    Converter() = default;\n    Converter(const Converter&) = delete;\n    Converter& operator=(const Converter&) = delete;\n    void operator()(const std::string&)\n    {\n        // do nothing\n    }\n};\n```\n\n我们如何将它传递给线程构造器？如果我们照原样传递，就会得到一个编译器错误；例如:\n\n```cpp\nint main()\n{\n    Converter convert;\n    std::thread convertThread(convert, \"convert me\");\n    convertThread.join();\n    return 0;\n}\n```\n\n您将获得以下输出:\n\n![](img/C14583_05_17.jpg)\n\n###### 图 5.17:编译错误的例子\n\n这里有很多奇怪的错误。要解决这个问题，我们可以使用`std::move()`函数来移动可调用的:\n\n```cpp\nstd::thread convertThread(std::move(convert), \"convert me\");\n```\n\n现在，一切都好了——代码已经编译好了，并且完全按照我们想要的方式运行。\n\n现在，让我们考虑一个更有趣的例子。例如，您有一个需要捕捉不可复制对象的 lambda 函数，例如`unique_ptr`:\n\n```cpp\nauto unique = std::make_unique<Converter>();\n```\n\n从 C++ 14 开始，我们可以使用`std::move()`来捕捉可移动对象。因此，要捕获唯一的指针，我们可以使用以下代码:\n\n```cpp\nstd::thread convertThread([ unique = std::move(unique) ] { \n        unique->operator()(\"convert me\");\n});\n```\n\n如您所见，使用`std::move`捕获 lambda 中的值非常有用。当我们不想复制某些对象时，这也很有用，因为它们可能需要很长时间才能复制。\n\n现在，让我们将我们的知识付诸实践，并编写一个应用示例，演示如何使用线程使用`std::move`。\n\n### 练习 4:创建和使用线程的 STL 容器\n\n在本练习中，我们将编写一个简单的应用，其中我们将对线程使用`std::move()`。首先，我们将实现一个可移动构造的类。这个类将把小写文本转换成大写文本。然后，我们将创建这个类的实例向量。接下来，我们将创建一个`std::thread`对象的向量。最后，我们将用第一个向量中的一个对象初始化线程。\n\n执行以下步骤完成本练习:\n\n1.  包括用于线程支持的报头，即`<线程>`，流支持，即`< iostream >`，以及`<向量>` :\n\n    ```cpp\n    #include <iostream>\n    #include <thread>\n    #include <vector>\n    #include <string>\n    ```\n\n2.  实现`转换器`类，它有`m_bufferIn`私有成员`const`T7】STD::vector<STD::string>&T8】类型。这是对小写字符串的原始向量的引用。它还有一个用户构造器，它接受`bufferIn`变量。然后，我们删除复制构造函数和赋值操作符。最后，我们定义重载的`运算符()`，在这里我们将所有小写符号转换为大写。转换后，我们将结果写入结果缓冲区:\n\n    ```cpp\n    class Converter\n    {\n        public:\n        Converter(std::vector<std::string>& bufferIn)\n            : m_bufferIn(bufferIn)\n        {\n        }\n        Converter(Converter&& rhs)\n            : m_bufferIn(std::move(rhs.m_bufferIn))\n        {\n        }\n        Converter(const Converter&) = delete;\n        Converter& operator=(const Converter&) = delete;\n        Converter& operator=(Converter&&) = delete;\n        void operator()(const int idx, std::vector<std::string>& result)\n        {\n            try\n            {\n                std::string::const_iterator end = m_bufferIn.at(idx).end();\n                std::string bufferOut;\n                for (std::string::const_iterator iter = m_bufferIn.at(idx).begin(); iter != end; iter++)\n                {\n                    if (*iter >= 97 && *iter <= 122)\n                    {\n                        bufferOut += static_cast<char>(static_cast<int>(*iter) - 32);\n                    }\n                    else\n                    {\n                        bufferOut += *iter;\n                    }\n                }\n                result[idx] = bufferOut;\n            }\n            catch(...)\n            {\n                std::cout << \"Invalid index\" << std::endl;\n            }\n        }\n        private:\n        const std::vector<std::string>& m_bufferIn;\n    };\n    ```\n\n3.  进入`主()`功能，创建一个名为`numberOfTasks`的常量值，并将其设置为`5`。然后，创建一个`转换器`对象的向量，并用`数量的任务`保留其大小。然后，创建一个`std::thread`对象的向量，并用`numberOfTasks` :\n\n    ```cpp\n    const int numberOfTasks = 5;\n    std::vector<Converter> functions;\n    functions.reserve(numberOfTasks);\n    std::vector<std::thread> threads;\n    threads.reserve(numberOfTasks); \n    ```\n\n    保留其大小\n4.  创建字符串向量，`textArr`，推五个不同的大字符串进行转换:\n\n    ```cpp\n    std::vector<std::string> textArr;\n    textArr.emplace_back(\"In the previous topics, we learned almost all that we need to work with threads. But we still have something interesting to consider – how to synchronize threads using future results. When we considered condition variables we didn't cover the second type of synchronization with future results. Now it's time to learn that.\");\n    textArr.emplace_back(\"First of all, let's consider a real-life example. Imagine, you just passed the exam at the university. You were asked to wait some amount of time for results. So, you have time to coffee with your mates, and every 10-15 mins you check are results available. Then, when you finished all your other activities, you just come to the door of the lecture room and wait for results.\");\n    textArr.emplace_back(\"In this exercise, we will write a simple application where we will use std::move() with threads. First of all, we will implement a class that is move constructible. This class will convert lowercase text into uppercase text. Then we will create a vector of instances of this class. Next, we will create a vector of std::thread object. Finally, we will initialize threads with an object from the first vector\");\n    textArr.emplace_back(\"Let's consider one interesting issue. As you remember when we initialize std::thread all constructor arguments are copied into thread memory, including a callable object – lambda, function, std::function. But what if our callable object doesn't support copy semantic? For example, we created a class that has only move constructor and a move assignment operator:\");\n    textArr.emplace_back(\"Run this code in your editor. You will see in the terminal log from the default constructor, two logs from the move operator, then one log from a destructor, then message from the doSomeJob() function and, finally two other log messages from the destructor. We see that the move constructor is called twice. You will get the output like the following:\");\n    ```\n\n5.  为循环实现**，我们将**转换器**对象推入函数向量:\n\n    ```cpp\n    for (int i = 0; i < numberOfTasks; ++ i)\n    {\n        functions.push_back(Converter(textArr));\n    }\n    ```** \n6.  创建一个字符串的结果向量，并推送五个空字符串。然后，创建一个变量作为数组元素的索引:\n\n    ```cpp\n    std::vector<std::string> result;\n    for (int i = 0; i < numberOfTasks; ++ i)\n    {\n        result.push_back(\"\");\n    }\n    int idx = 0;\n    ```\n\n7.  为循环实现另一个**，我们将**标准::线程**对象推入线程向量:\n\n    ```cpp\n    for (auto iter = functions.begin(); iter != functions.end(); ++ iter)\n    {\n        std::thread tmp(std::move(*iter), idx, std::ref(result));        \n        threads.push_back(std::move(tmp));\n        from = to;\n        to += step;\n    }\n    ```** \n8.  为回路实施第三个**，在此我们分离**标准螺纹** :\n\n    ```cpp\n    for (auto iter = threads.begin(); iter != threads.end(); ++ iter)\n    {\n         (*iter).detach();\n    }\n    ```** \n9.  为主线程添加一个小休眠，以避免关闭应用。现在，我们可以看到分离线程是如何工作的:\n\n    ```cpp\n    using namespace std::chrono_literals; \n    std::this_thread::sleep_for(5s);\n    ```\n\n10.  最后将结果打印到终端:\n\n    ```cpp\n    for (const auto& str : result)\n    {\n        std::cout << str;\n    }\n    ```\n\n11.  在编辑器中运行这段代码。在终端中，您可以看到所有字符串都是大写的，这意味着所有线程都被移动并成功运行。您将获得以下输出:\n\n![Figure 5.18: The result of the exercise’s execution ](img/C14583_05_18.jpg)\n\n###### 图 5.18:练习执行的结果\n\n在本练习中，我们练习了如何创建仅移动对象的 STL 容器。我们还考虑了如何将不可复制的对象传递给线程构造器。这些知识将在下一节帮助我们学习如何从线程中获得结果。\n\n## 未来、承诺和异步\n\n在前一节中，我们了解了使用线程所需的几乎所有知识。但是我们仍然有一些有趣的事情要考虑，那就是使用未来的结果同步线程。当我们考虑条件变量时，我们没有用未来的结果来覆盖第二种类型的同步。现在，是时候了解一下了。\n\n假设有一种情况，我们运行某个线程并继续其他工作。当我们需要一个结果时，我们停下来检查它是否准备好了。这种情况描述了具有未来结果的实际工作。在 C++ 中，我们有一个名为`<【未来】>`的头文件，其中包含两个表示未来结果的模板类:`std::future < >`和`STD::shared _ future<>`。当我们需要单个未来结果时，我们使用`std::future < >`，当我们需要多个有效副本时，我们使用`STD::shared _ future<>`。我们可以将它们与`std::unique_ptr`和`std::shared_ptr`进行比较。\n\n为了处理未来的结果，我们需要一个特殊的机制来在后台运行任务，并在稍后接收结果:`std::async()`模板函数。它将可调用作为一个参数和启动模式——延迟或异步，当然还有可调用的参数。启动模式`标准::启动::异步`和`标准::启动::延迟`指示如何执行任务。当我们通过`std::launch::async`时，我们期望该函数在单独的线程中执行。当我们通过`STD::launch::delivered`时，函数调用将被延迟，直到我们询问结果。我们也可以同时传递两者，例如`STD::launch::delivered | STD::launch::async`。这意味着运行模式将取决于实现。\n\n现在，让我们考虑一个使用`std::async`的例子`std::future`。我们有一个`to ppercase()`函数，它将给定的字符串转换成大写:\n\n```cpp\nstd::string toUppercase(const std::string& bufIn)\n{\n    std::string bufferOut;\n    for (std::string::const_iterator iter = bufIn.begin(); iter != bufIn.end(); iter++)\n    {\n        if (*iter >= 97 && *iter <= 122)\n        {\n            bufferOut += static_cast<char>(static_cast<int>(*iter) - 32);\n        }\n        else\n        {\n            bufferOut += *iter;\n        }\n    }\n    return bufferOut;\n}\n```\n\n然后，在`main()`函数中，我们创建一个名为`result`的`std::future`变量，并使用`std::async()`返回值对其进行初始化。然后，我们使用结果对象的`get()`函数获取结果:\n\n```cpp\n#include <iostream>\n#include <future>\nint main()\n{\n    std::future<std::string> result = std::async(toUppercase, \"please, make it uppercase\");\n    std::cout << \"Main thread isn't locked\" << std::endl;\n    std::cout << \"Future result = \" << result.get() << std::endl;\n    return 0;\n}\n```\n\n实际上，在这里，我们创建了一个未来的对象:\n\n```cpp\nstd::future<std::string> result = std::async(toUppercase, \"please, make it uppercase\");\n```\n\n如您所见，我们没有将启动模式传递给`std::async()`函数，这意味着将使用默认模式:`STD::launch::delivered | STD::launch::async`。您可以明确地这样做:\n\n```cpp\nstd::future<std::string> result = std::async(std::launch::async, toUppercase, \"please, make it uppercase\");\n```\n\n在这里，我们正在等待结果:\n\n```cpp\nstd::cout << \"Future result = \" << result.get() << std::endl;\n```\n\n如果我们的任务需要很长时间，线程会在这里一直等到结束。\n\n一般来说，我们可以像使用`std::thread`构造函数一样使用`std::async()`函数。我们可以传递任何可调用的对象。默认情况下，所有参数都是复制的，我们可以移动变量和可调用对象，也可以通过引用传递它们。\n\n`std::future`对象不受比赛条件保护。因此，为了从不同的线程访问它并防止损坏，我们应该使用互斥锁。但是如果我们需要共享一个未来的对象，最好用`std::shared_future`。共享的未来结果也不是线程安全的。为了避免竞争情况，我们必须使用互斥体或者在每个线程中存储线程自己的`std::shared_future`副本。\n\n#### 注意\n\n`std::future`对象的比赛条件非常棘手。当线程调用`get()`函数时，未来对象无效。\n\n我们可以通过将未来交给一个建造者来创造一个共享的未来:\n\n```cpp\nstd::future<std::string> result = std::async(toUppercase, \"please, make it uppercase\");\nstd::cout << \"Main thread isn't locked\" << std::endl;\nstd::shared_future<std::string> sharedResult(std::move(result));\nstd::cout << \"Future result = \" << sharedResult.get() << std::endl;\nstd::shared_future<std::string> anotherSharedResult(sharedResult);\nstd::cout << \"Future result = \" << anotherSharedResult.get() << std::endl;\n```\n\n如您所见，我们从`std::future`中创建了一个`std::shared_future`变量并复制了它。两个共享的未来对象指的是同一个结果。\n\n我们还可以使用`sdt::future`对象的`share()`成员函数来创建共享的未来对象:\n\n```cpp\nstd::future<std::string> result = std::async(toUppercase, \"please, make it uppercase\");\nstd::cout << \"Main thread isn't locked\" << std::endl;\nauto sharedResult = result.share();\nstd::cout << \"Future result = \" << sharedResult.get() << std::endl;\n```\n\n请注意，在这两种情况下，`std::future`对象都将失效。\n\n我们可以从单独的线程获得未来结果的另一种方法是使用`STD::packaged _ task<>`模板类。我们如何与他们合作？\n\n1.  我们创建一个新的`std::packaged_task`并声明可调用函数签名:\n\n    ```cpp\n    std::packaged_task<std::string(const std::string&)> task(toUppercase);\n    ```\n\n2.  然后，我们将未来结果存储在`std::future`变量:\n\n    ```cpp\n    auto futureResult = task.get_future();\n    ```\n\n    中\n3.  接下来，我们在单独的线程中运行这个任务，或者将其作为函数调用:\n\n    ```cpp\n    std::thread thr1(std::move(task), \"please, make it uppercase\");\n    thr1.detach();\n    ```\n\n4.  Finally, we wait until the future results are ready:\n\n    ```cpp\n    std::cout << \"Future result = \" << futureResult.get() << std::endl;\n    ```\n\n    #### 注意\n\n    `std::packaged_task`不可复制。因此，要在单独的线程中运行它，请使用`std::move()`函数。\n\n还有一件重要的事情需要注意。如果您不希望线程产生任何结果，并且希望等到线程完成工作，可以使用`std::future < void >`。现在，当您调用`future.get()`时，您当前的线程将在此时等待。让我们考虑一个例子:\n\n```cpp\n#include <iostream>\n#include <future>\nvoid toUppercase(const std::string& bufIn)\n{\n    std::string bufferOut;\n    for (std::string::const_iterator iter = bufIn.begin(); iter != bufIn.end(); iter++)\n    {\n        if (*iter >= 97 && *iter <= 122)\n        {\n            bufferOut += static_cast<char>(static_cast<int>(*iter) - 32);\n        }\n        else\n        {\n            bufferOut += *iter;\n        }\n    }\n    using namespace std::chrono_literals;\n    std::this_thread::sleep_for(2s);\n    std::cout << bufferOut << std::endl;\n}\nint main()\n{\n    std::packaged_task<void(const std::string&)> task(toUppercase);\n    auto futureResult = task.get_future();\n    std::thread thr1(std::move(task), \"please, make it uppercase\");\n    thr1.detach();\n    std::cout << \"Main thread is not blocked here\" << std::endl;\n    futureResult.get();\n    std::cout << \"The packaged_task is done\" << std::endl;\n    return 0;\n} \n```\n\n如您所见，通过等待另一个线程，我们使用了几种技术，如条件变量、未来结果和承诺。\n\n现在，让我们进入标准库中的下一个重要特性——模板类`std::promise < >`。通过这个类，我们可以设置我们想要接收的类型的值，然后使用`std::future`获取它。我们如何与他们合作？为此，我们需要实现一个带有`标准::承诺`参数的函数:\n\n```cpp\nvoid toUppercase(const std::string& bufIn, std::promise<std::string> result)\n```\n\n工作完成后，我们需要用`std::promise`初始化一个新值:\n\n```cpp\nresult.set_value(bufferOut);\n```\n\n为了在我们将要使用的地方创建`std::promise`，我们需要编写以下代码:\n\n```cpp\nstd::promise<std::string> stringInUpper;\n```\n\n一旦做到这一点，我们必须创造`std::future`并从承诺中得到它；\n\n```cpp\nstd::future<std::string> futureRes = stringInUpper.get_future();\n```\n\n我们需要在单独的线程中运行这个函数:\n\n```cpp\nstd::thread thr(toUppercase, \"please, make it uppercase\", std::move(stringInUpper));\nthr.detach();\n```\n\n现在，我们需要等到未来确定下来:\n\n```cpp\nfutureRes.wait();\nstd::cout << \"Result = \" << futureRes.get() << std::endl;\n```\n\n使用 promises 获得结果的完整示例如下:\n\n```cpp\n#include <iostream>\n#include <future>\nvoid toUppercase(const std::string& bufIn, std::promise<std::string> result)\n{\n    std::string bufferOut;\n    for (std::string::const_iterator iter = bufIn.begin(); iter != bufIn.end(); iter++)\n    {\n        if (*iter >= 97 && *iter <= 122)\n        {\n            bufferOut += static_cast<char>(static_cast<int>(*iter) - 32);\n        }\n        else\n        {\n            bufferOut += *iter;\n        }\n    }\n    result.set_value(bufferOut);\n}\nint main()\n{\n    std::promise<std::string> stringInUpper;\n    std::future<std::string> futureRes = stringInUpper.get_future();\n    std::thread thr(toUppercase, \"please, make it uppercase\", std::move(stringInUpper));\n    thr.detach();\n    std::cout << \"Main thread is not blocked here\" << std::endl;\n    futureRes.wait();\n    std::cout << \"Result = \" << futureRes.get() << std::endl;\n    return 0;\n}\n```\n\n因此，我们几乎涵盖了编写多线程应用所需的所有内容，除了一件重要的事情——如果在单独的线程中抛出异常会发生什么？例如，您在线程中传递一个函数，它会引发异常。在这种情况下，将为此线程调用`std::terminate()`。其他线程将继续它们的工作。让我们考虑一个简单的例子。\n\n我们有一个`getException()`函数，它生成一个带有线程 ID 的消息，并抛出`std::runtime_error`:\n\n```cpp\n#include <sstream>\n#include <exception>\n#include <iostream>\n#include <future>\nstd::string getException()\n{\n    std::stringstream ss;\n    ss << \"Exception from thread: \";\n    ss << std::this_thread::get_id();\n    throw std::runtime_error(ss.str());\n}\n```\n\n我们还有`to ppercase()`功能。该函数将给定的字符串转换为大写，并调用`getException()`函数，该函数引发异常:\n\n```cpp\nstd::string toUppercase(const std::string& bufIn)\n{\n    std::string bufferOut;\n    for (std::string::const_iterator iter = bufIn.begin(); iter != bufIn.end(); iter++)\n    {\n        if (*iter >= 97 && *iter <= 122)\n        {\n            bufferOut += static_cast<char>(static_cast<int>(*iter) - 32);\n        }\n        else\n        {\n            bufferOut += *iter;\n            getException();\n        }\n    }\n    return bufferOut;\n}\n```\n\n这里是`main()`函数，我们在`试捕`块中创建新线程`thr`。我们捕捉到一个异常，并将消息打印到终端:\n\n```cpp\nint main()\n{\n    try\n    {\n        std::thread thr(toUppercase, \"please, make it uppercase\");\n        thr.join();\n    }\n    catch(const std::exception& ex)\n    {\n        std::cout << \"Caught an exception: \" << ex.what() << std::endl;\n    }\n    return 0;\n}\n```\n\n如果您在 IDE 中运行此代码，您将看到以下输出:\n\n![Figure 5.19: The result of an example’s execution ](img/C14583_05_19.jpg)\n\n###### 图 5.19:一个例子的执行结果\n\n我们可以看到`std::terminate()`在抛出异常后被调用。当程序中有很多线程时，很难找到线程终止的正确位置。幸运的是，我们有一些从另一个线程捕捉异常的机制。让我们把它们都考虑进去。\n\n**std::async** 函数使用将来的结果将异常转移到调用线程。它在将来的结果中存储`标准::异常 _ptr`，并设置就绪标志。然后，当您调用`get()`、 **std::future** 时，会检查是否有任何`std::exception_ptr`存储并重新引发异常。我们所需要做的就是在`试捕`区块中放置一个`get()`调用。让我们考虑一个例子。我们将使用前面例子中的两个辅助函数，即`getException()`和`toUppercase()`。它们将保持不变。在`main()`函数中，我们创建了一个名为`result`的`std::future`对象，并使用`std::async()`函数运行`to ppercase()`函数。然后，我们在`try-catch`块中调用结果对象的`get()`函数，捕捉异常:\n\n```cpp\n#include <iostream>\n#include <future>\nint main()\n{\n    std::future<std::string> result = std::async(toUppercase, \"please, make it uppercase\");\n    try\n    {\n        std::cout << \"Future result = \" << result.get() << std::endl;\n    }\n    catch(const std::exception& ex)\n    {\n        std::cout << \"Caught an exception: \" << ex.what() << std::endl;\n    }\n    return 0;\n}\n```\n\n如果您在 IDE 中运行前面的代码，您将获得以下输出:\n\n![Figure 5.20: The result of the example’s execution ](img/C14583_05_20.jpg)\n\n###### 图 5.20:示例执行的结果\n\n如您所见，我们捕捉到了一个异常，现在我们可以通过某种方式处理它。`STD::packaged _ task<>`类以相同的方式处理异常——它将`std::exception_ptr`存储在将来的结果中，设置就绪标志，然后`std::future`在`get()`调用中重新抛出异常。让我们考虑一个小例子。我们将使用前面示例中的两个助手函数- `getException()`和`to ppercase()`。它们将保持不变。在`main()`函数中，我们创建了一个名为`task`的`std::packaged_task`对象。通过使用我们的`to ppercase()`函数的类型，它返回一个整数，并以两个整数作为参数。我们将`传递给`功能到`任务`对象。然后，我们创建一个名为`结果`的`std::future`对象，并使用`get_future()`函数从任务对象中获取结果。最后，我们在新线程`thr`中运行任务对象，并在`try-catch`块中调用`get()`函数的`结果`变量:\n\n```cpp\n#include <iostream>\n#include <future>\nint main()\n{\n    std::packaged_task<std::string(const std::string&)> task(toUppercase);\n    auto result = task.get_future();\n    std::thread thr(std::move(task), \"please, make it uppercase\");\n    thr.detach();\n    try\n    {\n        std::cout << \"Future result = \" << result.get() << std::endl;\n    }\n    catch(const std::exception& ex)\n    {\n        std::cout << \"Caught an exception: \" << ex.what() << std::endl;\n    }\n    return 0;\n}\n```\n\n如果您在 IDE 中运行此代码，您将获得以下输出:\n\n![Figure 5.21: The result of this example’s execution ](img/C14583_05_21.jpg)\n\n###### 图 5.21:这个例子的执行结果\n\n`std::promise < >`类以另一种方式处理异常。它允许我们使用`set_exception()`或`set _ exception _ at _ thread _ exit()`功能手动存储异常。要在`std::promise`中设置异常，我们必须抓住它。如果我们没有捕捉到异常，将在`std::promise`的析构函数中设置错误为`STD::future _ errc::breaked _ promise`在将来的结果中。当你调用`get()`函数时，会重新抛出一个异常。让我们考虑一个例子。我们将使用上一个示例中的帮助函数–`getException()`。它保持不变。但是我们将`改为`功能，增加第三个参数`std::promise`。现在，我们将调用`中的 **getException()**`**函数，尝试**块，捕捉异常，并将其设置为`std::promise`值:\n\n```cpp\nvoid toUppercase(const std::string& bufIn, std::promise<std::string> result)\n{\n    std::string bufferOut;\n    try\n    {\n        for (std::string::const_iterator iter = bufIn.begin(); iter != bufIn.end(); iter++)\n        {\n            if (*iter >= 97 && *iter <= 122)\n            {\n                    bufferOut += static_cast<char>(static_cast<int>(*iter) - 32);\n            }\n            else\n            {\n                bufferOut += *iter;\n                getException();\n            }\n        }\n    }\n    catch(const std::exception& ex)\n    {\n        result.set_exception(std::make_exception_ptr(ex));\n    }\n    result.set_value(bufferOut);\n}\n```\n\n#### 注意\n\n有几种方法可以打破这个承诺。首先，我们可以捕捉`std::exception`并使用`STD::make _ exception _ ptr()`函数将其转换为`std::exception_ptr`。也可以使用`std::current_exception()`函数，返回`std::exception_ptr`对象。\n\n在`main()`函数中，我们创建了一个名为`upperResult`的整数类型的承诺。我们创建了一个名为`未来`的未来结果，并将其设置为`上结果`承诺值。接下来，我们创建一个新的线程，`thr`，将`传递给它`函数，并将`上移`承诺。然后，我们调用`futureRes`对象的`wait()`函数，使调用线程等待，直到结果可用。然后，在`试捕`块中，我们调用`futureRes`对象的`get()`函数，它重新抛出一个异常:\n\n```cpp\n#include <iostream>\n#include <future>\nint main()\n{\n    std::promise<std::string> upperResult;\n    std::future<std::string> futureRes = upperResult.get_future();\n    std::thread thr(toUppercase, \"please, make it uppercase\", std::move(upperResult));\n    thr.detach();\n    futureRes.wait();\n    try\n    {\n        std::cout << \"Result = \" << futureRes.get() << std::endl;\n    }\n    catch(...)\n    {\n        std::cout << \"Caught an exception\" << std::endl;\n    }\n    return 0;\n}\n```\n\n#### 注意\n\n当我们创建一个`std::promise < >`对象时，我们承诺我们将强制设置该值或异常。如果我们不这样做，`std::promise`的析构函数将抛出一个异常，即`STD::future _ error–STD::future _ errc::breaked _ promise`。\n\n如果您在 IDE 中运行此代码，您将获得以下输出:\n\n![Figure 5.22: The result of this example’s execution ](img/C14583_05_22.jpg)\n\n###### 图 5.22:这个例子的执行结果\n\n这就是多线程应用中处理异常的全部方法。如您所见，这与我们在一个线程中所做的非常相似。现在，让我们将我们的知识付诸实践，并编写一个简单的应用示例，演示如何使用不同的未来结果进行同步。\n\n### 练习 5:与未来结果同步\n\n在本练习中，我们将编写一个简单的应用来演示如何使用未来的结果从不同的线程接收值。我们将运行`to ppercase()`可调用对象三次。我们将使用`std::async()`函数执行第一个任务，使用`STD::packaged _ task<>`模板类执行第二个任务，使用`std::thread`和`std::promise`执行最后一个任务。\n\n执行以下步骤完成本练习:\n\n1.  包括支持线程的头文件，即`<线程>`，支持流的头文件，即`< iostream >`，支持未来结果的头文件`<>`:\n\n    ```cpp\n    #include <iostream>\n    #include <thread>\n    #include <future>\n    ```\n\n2.  实现一个将给定字符串转换为大写的`to ppercase`类。它有两个重载操作符，`(`)。第一个`运算符()`获取要转换的字符串，并以大写形式返回结果值。第二个`运算符()`获取要转换的字符串和一个`std::promise`，并将返回值存储在一个 promise:\n\n    ```cpp\n    class ToUppercase\n    {\n        public:\n        std::string operator()(const std::string& bufIn)\n        {\n            std::string bufferOut;\n            for (std::string::const_iterator iter = bufIn.begin(); iter != bufIn.end(); iter++)\n            {\n                if (*iter >= 97 && *iter <= 122)\n                {\n                    bufferOut += static_cast<char>(static_cast<int>(*iter) - 32);\n                }\n                else\n                {\n                    bufferOut += *iter;\n                }\n            }\n            return bufferOut;\n        }\n        void operator()(const std::string& bufIn, std::promise<std::string> result)\n        {\n            std::string bufferOut;\n            for (std::string::const_iterator iter = bufIn.begin(); iter != bufIn.end(); iter++)\n            {\n                if (*iter >= 97 && *iter <= 122)\n                {\n                    bufferOut += static_cast<char>(static_cast<int>(*iter) - 32);\n                }\n                else\n                {\n                    bufferOut += *iter;\n                }\n            }\n            result.set_value(bufferOut);\n        }\n    };\n    ```\n\n    中\n3.  现在，创建一个`to ppercase`对象，即`ptConverter`，并创建一个`std::packaged_task`，即`upper ceresult 1`，该对象以`ptConverter`对象为参数。创建一个`标准::未来`值，并从`上半部分`设置。在单独的线程中运行该任务:\n\n    ```cpp\n    ToUppercase ptConverter;\n    std::packaged_task<std::string(const std::string&)> upperCaseResult1(ptConverter);\n    std::future<std::string> futureUpperResult1= upperCaseResult1.get_future();\n    std::thread thr1(std::move(ptConverter), \"This is a string for the first asynchronous task\");\n    thr1.detach(); \n    ```\n\n4.  现在，创建第二个`来访问`对象，即`转换器`。创建一个名为`futureuperresult 2`的`std::future`对象，并从`std::async()` :\n\n    ```cpp\n    ToUppercase fConverter;\n    std::future<std::string> futureUpperResult2 = std::async(fConverter, \"This is a string for the asynchronous task\"); \n    ```\n\n    进行设置\n5.  现在。创建第三个`来访问`对象。即`pConverter`。创建一个名为`承诺结果`的`标准::承诺`值。然后，创建一个名为`future perresult 3`的`std::future`值，并从`promiseResult`中进行设置。现在，在单独的线程中运行`pcconverter`任务，并将`promisereresult`作为参数传递:\n\n    ```cpp\n    ToUppercase pConverter;\n    std::promise<std::string> promiseResult;\n    std::future<std::string> futureUpperResult3 = promiseResult.get_future();\n    std::thread thr2(pConverter, \"This is a string for the task that returns a promise\", std::move(promiseResult));\n    thr2.detach(); \n    ```\n\n6.  现在，要接收所有线程的结果，请等待`futureperresult 3`准备好，然后获取所有三个结果并打印出来:\n\n    ```cpp\n    futureUpperResult3.wait();\n    std::cout  << \"Converted strings: \"\n            << futureUpperResult1.get() << std::endl\n            << futureUpperResult2.get() << std::endl\n            << futureUpperResult3.get() << std::endl;\n    ```\n\n7.  Run this code in your editor. You will see the converted strings from all three threads.\n\n    您将获得以下输出:\n\n![Figure 5.23: The result of this exercise’s execution ](img/C14583_05_23.jpg)\n\n###### 图 5.23:本练习的执行结果\n\n那么，我们在这里做了什么？我们将大型计算拆分成较小的部分，并在不同的线程中运行它们。对于长时间计算，这将提高性能。在本练习中，我们学习了如何从线程接收结果。在本节中，我们还学习了如何将在单独线程中引发的异常传递给调用线程。我们还学习了如何通过一个事件来同步几个线程的工作，不仅用条件变量，而且用未来的结果。\n\n### 活动 1:创建一个模拟器来模拟美术馆的工作\n\n在本活动中，我们将创建一个模拟器来模拟美术馆的工作。我们设定了参观画廊的人数上限——只能有 50 人进入。为了实现这个模拟，我们需要创建一个`Person`类，它将代表美术馆中的人。另外，我们需要一个`Persons`类，它是一个线程安全的容器。我们还需要一个`守望者`类来控制里面有多少人。如果限制超过了守望员，我们会把所有新来的人都放入等候名单。最后，我们需要一个`生成器`类，它有两个线程——一个用于创建新的访问者，另一个用于通知我们有人必须离开画廊。因此，我们将介绍如何使用线程、互斥体、条件变量、lock_guards 和 unique _ locks。这个模拟器将允许我们利用我们在本章中介绍的技术。因此，在尝试本练习之前，请确保您已经完成了本章前面的所有练习。\n\n为了实现这个应用，我们需要描述我们的类。我们有以下课程:\n\n![Figure 5.24: Description of the classes that are used in this activity ](img/C14583_05_24.jpg)\n\n###### 图 5.24:本活动中使用的类的描述\n\n让我们在开始实现之前创建类图。下图显示了上述所有具有关系的类:\n\n![Figure 5.25: The class diagram ](img/C14583_05_25.jpg)\n\n###### 图 5.25:类图\n\n按照以下步骤实施本活动:\n\n1.  定义并实现 Person 类，该类除了打印日志什么也不做。\n2.  为包装 std::vector 类的人员创建一些线程安全存储。\n3.  实现 PersonGenerator 类，在不同线程的无限循环中，创建和移除访问者，并通知 Watchman 类。\n4.  创建 Watchman 类，在一个独立线程的无限循环中，根据 PersonGenerator 类的通知，将访问者从队列移动到另一个队列。\n5.  在 main()函数中声明相应的对象来模拟艺术画廊及其工作方式。\n\n在实现这些步骤之后，您应该会得到以下输出，在这里您可以看到所有实现的类的日志。确保模拟按预期进行。预期的输出应该如下所示:\n\n![Figure 5.26: The result of the application’s execution ](img/C14583_05_26.jpg)\n\n###### 图 5.26:应用执行的结果\n\n#### 注意\n\n这项活动的解决方案可以在第 681 页找到。\n\n## 总结\n\n在本章中，我们学习了如何使用 C++ 标准库支持的线程。如果我们想要编写健壮、快速和清晰的多线程应用，这是最基本的。\n\n我们首先看一下关于并发性的一般概念——什么是并行、并发、同步、异步和线程执行。对这些概念有一个清晰的理解使我们能够理解多线程应用的架构设计。\n\n接下来，我们研究了开发多线程应用时面临的不同问题，例如数据危险、竞争条件和死锁。了解这些问题有助于我们为项目构建一个清晰的同步架构。我们在一些实际例子中考虑了同步概念，这让我们很好地理解了在编写线程应用时可能面临的挑战。\n\n接下来，我们尝试使用不同的标准库原语进行同步。我们试图弄清楚如何处理竞争条件，并实现了按事件同步和按数据同步的例子。接下来，我们考虑移动语义如何应用于多线程。我们从线程支持库中了解到哪些类是不可复制但可移动的。我们还考虑了移动语义如何在多线程闭包中工作。最后，我们学习了如何从不同的线程接收结果，以及如何使用期货、承诺和异步来同步线程。\n\n我们通过建立一个艺术画廊模拟器将所有这些新技能付诸实践。我们用一个主线程和四个子线程构建了一个多线程应用。我们通过使用条件变量来实现它们之间的通信。我们通过互斥锁使用共享数据来保护它们。总之，我们利用了本章所学的一切。\n\n在下一章中，我们将仔细研究 C++ 中的输入/输出操作和类。我们将从标准库的输入/输出支持开始。然后，我们将继续处理流和异步输入/输出操作。接下来，我们将学习线程和输入/输出的交互。我们将编写一个活动，让我们掌握 C++ 中输入/输出工作的技能。"
  },
  {
    "path": "docs/adv-cpp/07.md",
    "content": "# 七、流和输入/输出\n\n## 学习目标\n\n本章结束时，您将能够:\n\n*   使用标准输入/输出库向/从文件或控制台写入和读取数据\n*   使用内存中的输入/输出接口格式化和解析数据\n*   为用户定义的类型扩展标准输入/输出流\n*   开发使用多线程输入/输出标准库的应用\n\n在本章中，我们将使用输入/输出标准库开发灵活且可维护的应用，处理流，学习如何在多线程应用中使用输入/输出库，最后学习使用标准库格式化和解析数据。\n\n## 简介\n\n在前一章中，我们讨论了最具挑战性的话题之一——c++ 中的并发性。我们研究了主要的多线程概念，并区分了 C++ 中的同步、异步和线程执行。我们学习了关于同步、数据危险和比赛条件的要点。最后，我们研究了在现代 C++ 中使用线程。在本章中，我们将深入学习如何在多线程应用中处理输入/输出。\n\n本章专门介绍 C++ 中的`流`和`I/O`。输入输出是输入输出操作的一般概念。标准库这一部分的主要目的是提供一个关于数据输入和输出的清晰界面。但这不是唯一的目标。在许多情况下，输入/输出可以帮助我们的应用。很难想象任何应用不把错误或异常情况写入日志文件，目的是把它发送给开发团队进行分析。在图形用户界面应用中，我们总是需要格式化显示的信息或解析用户输入。在复杂的大型应用中，我们通常需要记录内部数据结构，等等。在所有这些情况下，我们使用`标准库`的输入/输出部分。\n\n我们将以对标准库的输入/输出部分的简单介绍开始本章。我们将了解输入/输出，并探索它们的主要概念和术语。然后，我们将考虑默认支持哪些类型，以及如何将流扩展到用户定义的类型。接下来，我们将研究输入/输出库的结构，并检查可供我们使用的头和类。最后，我们将研究如何处理流、读写文件、创建具有输入和输出操作的多线程应用，以及格式化和解析文本数据。\n\n本章将以一个富有挑战性和激动人心的活动结束，在这个活动中，我们将改进上一章中的`美术馆模拟器`项目，并创建一个健壮、清晰、多线程且易于使用的`记录器`。我们将开发一个接口清晰的类，可以从项目中的任何地方访问。接下来，我们将对它进行调整，使其能够与几个线程一起工作。最后，我们将把我们健壮的记录器集成到艺术画廊模拟器项目中。\n\n让我们从查看 C++ 标准库的输入/输出部分开始，了解这套工具为我们提供了哪些机会。\n\n### 查看标准库的输入/输出部分\n\n在计算机科学中，输入/输出这个术语意味着程序、设备、计算机等之间的通信。在 C++ 中，我们使用标准输入和标准输出术语来描述输入/输出过程。标准输入是指传输到程序中的数据流。为了获得这些数据，程序应该执行读取操作。标准输出是指从程序传输到外部设备的数据流，如文件、显示器、套接字、打印机等。为了输出这些数据，程序应该执行写操作。标准输入和输出流从主进程继承而来，对所有子线程都是通用的。请看下图，以更好地理解所考虑的术语:\n\n![](img/C14583_06_01.jpg)\n\n###### 图 6.1:设备之间的输入/输出通信\n\n在 C++ 标准库中，大多数输入输出类都是通用的类模板。所有这些逻辑上都分为两类——抽象和实现。我们已经熟悉了抽象类，并且知道我们可以在不重新编译代码的情况下将它们用于不同的目的。输入/输出库也是如此。这里，我们有六个抽象类，它们是 C++ 中输入输出操作的基础。我们不会深入探讨这些接口。通常，我们在操作中使用更多的高级类，只有当我们需要实现自己的派生类时，才会对它们有吸引力。\n\n**ios_base** 抽象类负责管理流状态标志、格式化标志、回调和私有存储。 **basic_streambuf** 抽象类提供了缓冲输入或输出操作的接口，并提供了对输入源的访问，如文件、套接字或输出接收器，如字符串或向量。 **basic_ios** 抽象类实现了从 **basic_streambuf** 接口使用派生类的工具。 **basic_ostream** 、 **basic_istream** 、 **basic_iostream** 抽象类分别是来自 **basic_streambuf** 接口的派生类的包装器，提供高级输入输出接口。让我们简单考虑一下它们及其关系，如下类图所示。可以看到除了 **ios_base** 之外，都是模板类。在每个类的名称下，您可以找到定义该类的文件名:\n\n#### 注意\n\n在 UML 符号中，我们使用`< <接口> >`关键字来表示类是一个抽象类。\n\n![Figure 6.2: Class diagram of I/O abstract interfaces ](img/C14583_06_02.jpg)\n\n###### 图 6.2:输入输出抽象接口类图\n\n实现类在逻辑上分为以下几类:**文件 I/O** 、**字符串 I/O** 、**同步 I/O** 、 **I/O 操纵器**，以及预定义的标准流对象。它们都是从前面提到的抽象类中派生出来的。让我们在接下来的章节中详细考虑它们。\n\n### 预定义的标准流对象\n\n我们将从已经熟悉的`< iostream >`头文件中的`std::cout`类开始了解输入/输出标准库。我们用它向终端输出数据。您可能还知道用于读取用户输入的`std::cin`类，但不是每个人都知道`std::cout`和`std::cin`是预定义的标准流对象，用于格式化终端的输入和输出。< iostream >头文件还包含`std::cerr`和`std::clog`流对象，用于记录错误。和往常一样，宽字符也有它们的类似物，前缀为“`w`”:`wcout`、`wcin`、`wcerr`、`wclog`。所有这些对象都会在系统启动时自动创建和初始化。虽然从多个线程使用这些对象是安全的，但是输出可以是混合的。让我们修改如何使用它们。因为它们只为内置类型重载，所以我们应该为用户定义的类型编写自己的重写。\n\n`标准::cout`流对象通常与`标准::endl`操纵器一起使用。它在输出序列中插入一个换行符并刷新它。下面是一个使用它们的例子:\n\n```cpp\nstd::string name(\"Marilyn Monroe\");\nint age = 18;\nstd::cout << \"Name: \" << name << \", age: \" << age << std::endl;\n```\n\n最初，`std::cin`对象逐符号读取所有输入字符序列。但是它有内置类型的重载，可以读取诸如`数字`、`字符串`、`字符`等值。读弦有一个小技巧；`std::cin`读取字符串，直到下一个空白或换行符。所以，如果需要它读字符串，就要循环进行，一个字一个字读，或者使用`std::getline()`函数，该函数以`std::cin`对象为第一个参数，目的字符串为第二个参数。\n\n#### 注意\n\n`标准::cin`流对象的右移位运算符`> >`仅读取一行中的一个单词。使用`std::getline(std::cin，str)`读取整行。\n\n这里有一个使用不同类型的`std::cin`的例子；\n\n```cpp\nstd::string name;\nstd::string sex;\nint age;\nstd::cout << \"Enter your name: \" << std::endl;\nstd::getline(std::cin, name);\nstd::cout << \"Enter your age: \" << std::endl;\nstd::cin >> age;\nstd::cout << \"Enter your sex (male, female):\" << std::endl;\nstd::cin >> sex;\nstd::cout << \"Your name is \" << name << \", your age is \" << age << \", your sex is \" << sex << std::endl;\n```\n\n如您所见，在这里，我们使用`std::getline()`函数读取名称，因为用户可以输入两三个单词。我们还使用右移位运算符、`> >`来读取年龄，然后读取性别，因为我们只需要读取一个单词。然后，我们打印读取的数据，以确保一切顺利。\n\n**std::cerr** 和 **std::clog** 流对象仅在一个方面不同–**STD::cerr**会立即刷新输出序列，而 **std::clog** 会对其进行缓冲，并仅在缓冲区已满时进行刷新。说到用法，跟 **std::cout** 很像。唯一不同的是来自 **std::cerr** 和 **std::clog** 的消息(在大多数 IdE 中)是红色的。\n\n在下面的截图中，您可以看到这些流对象的输出:\n\n![](img/C14583_06_03.jpg)\n\n###### 图 6.3:标准::cerr 和标准::clog 流对象的输出\n\n现在，让我们做一个练习来巩固我们所学的一切。\n\n### 练习 1:覆盖用户定义类型的左移位运算符< <\n\n在本练习中，我们将编写一段非常有用的代码，您可以在任何地方使用它来输出用户定义的类型。首先，我们将创建一个名为`Track`的类，代表一个音乐曲目。它将有以下私人成员:`名字`、`歌手`、`长度`和`日期`。然后，我们将覆盖左移位运算符，`< <`，对于这个类。接下来，我们将创建这个类的一个实例，并使用`std::cout`流对象输出它。\n\n执行以下步骤来执行本练习:\n\n1.  包括用于输出到控制台的所需标题: **< iostream >** 和用于字符串支持的字符串<>:\n\n    ```cpp\n    #include <iostream>\n    #include <string>\n    ```\n\n2.  声明`轨道`类，并添加私有节变量来保存`轨道`的信息，即`m_Name`、`m_Singer`、`m_Date`和`m_LengthInSeconds`。在公共部分，添加一个构造函数，其参数初始化所有私有变量。另外，为所有类成员添加`公共`区域获取器:\n\n    ```cpp\n    class Track\n    {\n    public:\n         Track(const std::string& name,\n               const std::string& singer,\n               const std::string& date,\n               const unsigned int& lengthInSeconds)\n               : m_Name(name)\n               , m_Singer(singer)\n               , m_Date(date)\n               , m_LengthInSeconds(lengthInSeconds)\n    {\n    }\n         std::string getName() const { return m_Name; }\n         std::string getSinger() const { return m_Singer; }\n         std::string getDate() const { return m_Date; }\n         unsigned int getLength() const { return m_LengthInSeconds; }\n    private:\n         std::string m_Name;\n         std::string m_Singer;\n         std::string m_Date;\n         unsigned int m_LengthInSeconds;\n    };\n    ```\n\n3.  现在是练习中最困难的部分:为`轨道`类型编写重载函数。这是一个`模板`函数，有两个类型参数:`图表`和`特征` :\n\n    ```cpp\n    template <typename charT, typename Traits>\n    ```\n\n4.  我们内联了这个函数，让编译器知道我们希望它对这个函数进行优化。这个函数的返回类型是对一个`std::basic_ostream < charT，Traits >`类的引用。这个函数的名字是`< <`运算符。该函数采用两个参数:第一个是对`STD::basic _ ostream<charT，Traits >`类的引用，第二个是`Track`变量的副本。完整的功能声明如下:\n\n    ```cpp\n    template <typename charT, typename Traits>\n    inline std::basic_ostream<charT, Traits>&\n    operator<<(std::basic_ostream<charT, Traits>& os, Track trackItem);\n    ```\n\n5.  现在，添加函数定义。使用`os`变量，就像我们使用`std::cout`对象一样，按照您的意愿格式化输出。然后，从函数中返回`os`变量。重载运算符`< <`的完整代码如下:\n\n    ```cpp\n    template <typename charT, typename Traits>\n    inline std::basic_ostream<charT, Traits>&\n    operator<<(std::basic_ostream<charT, Traits>& os, Track trackItem)\n    {\n          os << \"Track information: [\"\n             << \"Name: \" << trackItem.getName()\n             << \", Singer: \" << trackItem.getSinger()\n             << \", Date of creation: \" << trackItem.getDate()\n             << \", Length in seconds: \" << trackItem.getLength()\n             << \"]\";\n          return os;\n    }\n    ```\n\n6.  现在，进入`主`功能，创建并初始化名称为`track_001`的`Track`类型的实例。最后，使用`std::cout`打印`track_001`值:\n\n    ```cpp\n    int main()\n    {\n         Track track_001(\"Summer night city\",\n                         \"ABBA\",\n                         \"1979\",\n                          213);\n         std::cout << track_001 << std::endl;\n         return 0;\n    }\n    ```\n\n7.  编译并执行应用。运行它。您将获得以下输出:\n\n![](img/C14583_06_04.jpg)\n\n###### 图 6.4:执行练习 1 的结果\n\n干得好。在这里，我们考虑使用预定义的标准流对象，并学习了如何为用户定义的类型编写自己的重载移位运算符。让我们继续，用 C++ 标准 IO 库检查对文件的读写。\n\n### 文件输入输出实现类\n\n文件流管理文件的输入和输出。它们提供了一个实现**资源获取是初始化** ( **RAII** )的接口——文件在构建流时打开，在销毁时自动关闭。在标准库中，文件流由以下类表示:`basic_ifstream`用于输入操作，`basic_ofstream`用于输出操作，`basic _ fsstream`用于输入和输出操作，以及`basic_filebuf`用于原始文件设备的实现。所有这些都在`<流>`头文件中定义。标准库还为 char 和`wchar_t`类型提供了类型定义，即`ifstream`、`fsstream`和`ofstream`，宽字符的名称前缀为“`w`”。\n\n我们可以通过两种方式创建文件流。第一种方法是在一行中完成，也就是说，只需将文件名传递给构造函数，就可以打开文件并将流连接到文件:\n\n```cpp\nstd::ofstream outFile(filename);\nstd::ifstream outFile(filename);\nstd::fstream outFile(filename);\n```\n\n另一种方法是创建一个对象，然后调用`open()`函数:\n\n```cpp\nstd::ofstream outFile;\noutFile.open(filename);\n```\n\n#### 注意\n\nIO 流有布尔变量:一个**好位**、一个**坏位**、一个**坏位**和一个**坏位**。它们用于在每次操作后检查流的状态，并指示流中发生了哪个错误。\n\n对象创建后，我们可以通过检查`故障位`或检查与打开文件相关联的流来检查流状态。要检查一个`故障位`，调用`文件`流上的`故障()`功能:\n\n```cpp\nif (outFile.fail())\n{\n    std::cerr << filename << \" file couldn't be opened\"<< std::endl;\n}\n```\n\n要检查流是否与打开的文件相关联，请调用`is_open()`函数:\n\n```cpp\nif (!outFile.is_open())\n{\n    std::cerr << filename << \" file couldn't be opened\"<< std::endl;\n}\n```\n\n输入、输出和双向文件流也可以通过使用标志以不同的模式打开。它们在`ios_base`命名空间中声明。除了`ios_base::in`和`ios_base::out`标志外，我们还有`ios_base::ate`、`ios_base::app`、`ios_base::trunc`和`ios_base::binary`标志。`ios_base::trunc`标志删除文件内容。`ios_base::app`标志始终将输出写入文件末尾。即使您决定更改文件中的位置，也不能这样做。`ios_base::ate`标志将文件描述符的位置设置到文件的末尾，但允许您稍后修改位置。最后，`ios_base::binary`标志抑制数据的任何格式，以便以“原始”格式读取或写入数据。让我们考虑开放模式的所有可能组合。\n\n默认情况下， **std::ofstream** 在 **ios_base::out** 模式下打开， **std::ifstream** 在 **ios_base::in** 模式下打开，**STD::fsstream**在**IOs _ base::in | IOs _ base::out**模式下打开。如果文件不存在，则**IOs _ base::out | IOs _ base::trunc**模式会创建该文件，或者从现有文件中删除所有内容。**IOs _ base::out | IOs _ base::app**模式会在文件不存在的情况下创建文件，或者打开现有文件，只允许在文件末尾写入。上述两种模式都可以与标志中的 **ios_base::结合使用，因此文件将以读写模式同时打开。**\n\n以下是如何使用上述模式打开文件的示例:\n\n```cpp\nstd::ofstream outFile(filename, std::ios_base::out|std::ios_base::trunc);\n```\n\n您还可以执行以下操作:\n\n```cpp\nstd::ofstream outFile;\noutFile.open(filename, std::ios_base::out|std::ios_base::trunc);\n```\n\n在以所需模式打开文件流后，我们可以开始读取或写入文件。文件流允许我们改变文件中的位置。让我们考虑如何做到这一点。要获取当前文件的位置，我们可以在 **ios_base::out** 模式下调用 **tellp()** 函数，在模式下调用**IOs _ base::out**tellg()**函数。它可以在以后使用，以便我们在需要时可以回到这个位置。在 **ios_base::out** 模式下使用 **seekp()** 功能，在 **ios_base::in** 模式下使用 **seekg()** 功能，也可以找到文件中的确切位置。它需要两个参数:要移动的字符数和应该从哪个文件位置开始计数。**查找允许三种位置:std::ios_base::beg** 即文件开头， **std::ios_base::end** 即文件结尾， **std::ios_base::cur** 即当前位置。下面是调用 **seekp()** 函数的一个例子:**\n\n```cpp\noutFile.seekp(-5, std::ios_base::end);\n```\n\n如您所见，我们要求将当前文件的位置设置在文件末尾的第五个字符处。\n\n要写入文件，我们可以使用重载的左移位运算符，`< <`，对于一般的格式化输出，`put()`函数写单个字符，或者`write()`函数写一个字符块。使用左移位运算符是将数据写入文件的最方便的方法，因为您可以将任何内置类型作为参数传递:\n\n```cpp\noutFile << \"This is line No \" << 1 << std::endl;\n```\n\n`put()`和`write()`函数只能用于字符值。\n\n要读取文件，我们可以使用重载的右移位运算符，`> >`，或者使用一组读取字符的函数，如`read()`、`get()`、`getline()`。右移位运算符对于所有内置类型都是重载的，我们可以这样使用它:\n\n```cpp\nstd::ifstream inFile(filename);\t\t\nstd::string str;\nint num;\nfloat floatNum;\n// for data: \"book 3 24.5\"\ninFile >> str >> num >> floatNum;\n```\n\n最后，当执行离开可见性范围时，文件流被关闭，因此我们不需要执行任何额外的操作来关闭文件。\n\n#### 注意\n\n从文件中读取数据时要注意。右移位运算符`> >`，只读取一个字符串，直到出现一个空白或换行符。要读取完整的字符串，您可以使用循环或在单独的变量中读取每个单词，就像我们在*练习 1* 、*中为用户定义的类型*覆盖左移位运算符<、<一样。\n\n现在，让我们练习使用 C++ IO 标准库向文件读写数据。\n\n### 练习 2:向文件中读写用户定义的数据类型\n\n在本练习中，我们将为书店编写一段代码。我们需要将图书价格信息存储在一个文件中，然后在需要时从文件中读取这些信息。为了实现这一点，我们将创建一个类，该类表示一本书的名称、作者、出版年份和价格。接下来，我们将创建这个类的一个实例，并将它写入一个文件。稍后，我们将从文件中读取有关书籍的信息，并将其导入书籍类的实例中。执行以下步骤完成本练习:\n\n1.  包括所需的头文件:`< iostream >`用于输出到控制台，`< string >`用于字符串支持，`<fsstream>`用于 I/O 文件库支持:\n\n    ```cpp\n    #include <fstream>\n    #include <iostream>\n    #include <string>\n    ```\n\n2.  实现`Book`类，代表一个书店里的书。在私有部分，用不言自明的名称定义四个变量:`m_Name`、`m_Author`、`m_Year`、`m_Price`。在公共部分，定义一个带有参数的构造函数，该构造函数初始化所有类成员。此外，在`公共`部分，定义所有类成员的获取者:\n\n    ```cpp\n    class Book\n    {\n    public:\n          Book(const std::string& name,\n               const std::string& author,\n               const int year,\n               const float price)\n         : m_Name(name)\n         , m_Author(author)\n         , m_Year(year)\n         , m_Price(price) {}\n         std::string getName() const { return m_Name; }\n         std::string getAuthor() const { return m_Author; }\n         int getYear() const { return m_Year; }\n         float getPrice() const { return m_Price; }\n    private:\n         std::string m_Name;\n         std::string m_Author;\n         int m_Year;\n         float m_Price;\n    };\n    ```\n\n3.  进入`主`功能，声明`价格文件`变量，保存文件名:\n\n    ```cpp\n    std::string pricesFile(\"prices.txt\");\n    ```\n\n4.  接下来，创建`图书`类的实例，并使用`图书名称`、`作者名称`、`年份`和`价格` :\n\n    ```cpp\n    Book book_001(\"Brave\", \"Olena Lizina\", 2017, 33.57);\n    ```\n\n    对其进行初始化\n5.  将此类实例写入文件。创建`std::ofstream`类的实例。用`价格文件`变量名打开我们的文件。检查流是否打开成功，如果没有打开，打印错误信息:\n\n    ```cpp\n    std::ofstream outFile(pricesFile);\n    if (outFile.fail())\n    {\n          std::cerr << \"Failed to open file \" << pricesFile << std::endl;\n          return 1;\n    }\n    ```\n\n6.  然后，使用 getters 将所有关于`book_001`的信息写入文件，每个项目之间有空格，末尾有一个换行符:\n\n    ```cpp\n    outFile << book_001.getName() << \" \"\n            << book_001.getAuthor() << \" \"\n            << book_001.getYear() << \" \"\n            << book_001.getPrice() << std::endl;\n    ```\n\n7.  Compile and execute the application. Now, go to the project folder and find where the '**prices.txt**' file is located. In the following screenshot, you can see the location of the created file in the project directory:\n\n    ![](img/C14583_06_05.jpg)\n\n    ###### 图 6.5:创建的文件的位置\n\n8.  Open it in **Notepad**. In the following screenshot, you can see what the output to the file looks like:\n\n    ![](img/C14583_06_06.jpg)\n\n    ###### 图 6.6:用户定义类型输出到文件的结果\n\n9.  现在，让我们将这些数据读入变量。创建`std::ifstream`类的实例。打开文件`价格文件`。检查流是否已成功打开，如果未打开，则打印错误消息:\n\n    ```cpp\n    std::ifstream inFile(pricesFile);\n    if (inFile.fail())\n    {\n         std::cerr << \"Failed to open file \" << pricesFile << std::endl;\n         return 1;\n    }\n    ```\n\n10.  从文件中创建用于输入的局部变量，即`名称`、`作者名称`、`作者姓氏`、`年份`和`价格`。他们的名字不言自明:\n\n    ```cpp\n    std::string name;\n    std::string authorName;\n    std::string authorSurname;\n    int year;\n    float price;\n    ```\n\n11.  现在，按照文件中的顺序将文件中的数据读入变量:\n\n    ```cpp\n    inFile >> name >> authorName >> authorSurname >> year >> price;\n    ```\n\n12.  创建一个名为`book_002`的`Book`实例，并用那些读取的值初始化它:\n\n    ```cpp\n    Book book_002(name, std::string(authorName + \" \" + authorSurname), year, price);\n    ```\n\n13.  要检查读取操作是否成功执行，请将`book_002`变量打印到控制台:\n\n    ```cpp\n    std::cout  << \"Book name: \" << book_002.getName() << std::endl\n               << \"Author name: \" << book_002.getAuthor() << std::endl\n               << \"Year: \" << book_002.getYear() << std::endl\n               << \"Price: \" << book_002.getPrice() << std::endl;\n    ```\n\n14.  再次编译并执行应用。在控制台中，您将看到以下输出:\n\n![](img/C14583_06_07.jpg)\n\n###### 图 6.7:执行练习 2 的结果\n\n如您所见，我们毫无困难地从文件中写入和读取自定义格式的数据。我们创建了自己的自定义类型，使用`std::ofstream`类将其写入文件，并检查是否一切都写成功。然后，我们使用`std::ifstream`类将这些数据从一个文件读取到我们的自定义变量中，将其输出到控制台，并确保所有内容都被正确读取。通过这样做，我们学习了如何使用输入/输出标准库向文件读写数据。现在，让我们继续学习输入/输出库的内存部分。\n\n### 字符串输入/输出实现\n\n输入/输出标准库不仅允许输入和输出到文件等设备，还允许输入和输出到内存，特别是输入和输出到`标准::字符串`对象。在这种情况下，字符串可以是输入操作的源，也可以是输出操作的接收器。在`<流>`头文件中，声明了管理字符串输入和输出的流类。它们和文件流一样，也提供了一个实现 RAII 的接口——字符串在创建流时打开进行读取或写入，在销毁流时关闭。它们在标准库中由以下类表示:`basic_stringbuf`，它实现了一个原始字符串接口，`basic_istringstream`用于输入操作，`basic_ostringstream`用于输出操作，`basic_stringstream`用于输入和输出操作。标准库还为`char`和`wchar_t`类型提供了类型定义:`stream`、`ostringstream`、`stringstream`以及宽字符前缀为“w”的相同名称。\n\n要创建`STD::is tingstream`类的对象，我们应该将初始值设定项字符串作为构造函数参数传递，或者稍后使用`str()`函数进行设置:\n\n```cpp\nstd::string track(\"ABBA 1967 Vule\");\nstd::istringstream iss(track);\n```\n\n或者，我们可以执行以下操作:\n\n```cpp\nstd::string track(\"ABBA 1967 Vule\");\nstd::istringstream iss;\niss.str(track);\n```\n\n接下来，要从流中读取值，请使用右移位运算符，`> >`，该运算符对所有内置类型都是重载的:\n\n```cpp\nstd::string group;\nstd::string name;\nint year;\niss >> group >> year >> name;\n```\n\n要创建`std::ostringstream`类的对象，我们只需声明其类型的变量:\n\n```cpp\nstd::ostringstream oss;\n```\n\n接下来，要将数据写入字符串，请使用左移位运算符，`< <`，该运算符对所有内置类型都是重载的:\n\n```cpp\nstd::string group(\"ABBA\");\nstd::string name(\"Vule\");\nint year = 1967;\noss << group << std::endl\n    << name << std::endl\n    << year << std::endl;\n```\n\n要获取结果字符串，请使用`str()`函数:\n\n```cpp\nstd::cout << oss.str();\n```\n\n`std::stringstream`对象是双向的，因此它既有默认构造函数，也有接受字符串的构造函数。我们可以通过声明这种类型的变量来创建默认的`std::stringstream`对象，然后将其用于读写；\n\n```cpp\nstd::stringstream ss;\nss << \"45\";\nint count;\nss >> count;\n```\n\n另外，我们可以使用带有字符串参数的构造函数创建`std::stringstream`。然后，我们可以像往常一样使用它进行阅读和写作:\n\n```cpp\nstd::string employee(\"Alex Ismailow 26\");\nstd::stringstream ss(employee);\n```\n\n或者，我们可以创建一个默认的`std::stringstream`对象，并通过使用`str()`函数设置一个字符串来初始化它:\n\n```cpp\nstd::string employee(\"Charlz Buttler 26\");\nstd::stringstream ss;\nss.str(employee);\n```\n\n接下来，我们可以使用 ss 对象进行读写:\n\n```cpp\nstd::string name;\nstd::string surname;\nint age;\nss >> name >> surname >> age;\n```\n\n我们也可以为这些类型的流应用开放模式。它们的功能类似于文件流，但略有不同。`ios_base::binary`在处理字符串流的情况下是不相关的，`ios_base::trunc`被忽略。因此，我们可以在四种模式下打开任意字符串流:`ios_base::app`、`ios_base::ate`和`IOs _ base::in/IOs _ base::out`。\n\n现在，让我们练习使用 C++ IO 标准库向字符串读写数据。\n\n### 练习 3:为字符串中的替换单词创建函数\n\n在本练习中，我们将实现一个函数，该函数解析给定的字符串并用其他单词替换给定的单词。为了完成这个练习，我们创建了一个可调用的类，该类接受三个参数:原始字符串、要替换的单词和将用于替换的单词。因此，应该返回新字符串。执行以下步骤完成本练习:\n\n1.  包括输出到终端的必要标题:`<输出流>`和输入/输出字符串支持的输出流<>T4:\n\n    ```cpp\n    #include <sstream>\n    #include <iostream>\n    ```\n\n2.  实现名为`Replacer`的可调用类。它只有一个函数——一个重载的圆括号运算符，()，它返回一个字符串并接受三个参数:原始字符串、要替换的单词和要用于替换的单词。函数声明如下:\n\n    ```cpp\n    std::string operator()(const std::string& originalString,\n                           const std::string& wordToBeReplaced,\n                           const std::string& wordReplaceBy);\n    ```\n\n3.  接下来，创建`isting stream`对象，即`iss`，并将`originalString`变量设置为输入源:\n\n    ```cpp\n    std::istringstream iss(originalString);\n    ```\n\n4.  创建`排斥流`对象，即`oss`，它将保存转换后的字符串:\n\n    ```cpp\n    std::ostringstream oss;\n    ```\n\n5.  然后，在循环中，当有可能的输入时，执行对单词变量的单词读取。检查这个单词是否等于**单词被替换**变量。如果是，用变量替换它，并写入 **oss** 流。如果不相等，将原字写到 **oss** 流。在每个单词之后，添加一个空白字符，因为 **iss** 流会截断它们。最后，返回结果。完整的类如下:\n\n    ```cpp\n    class Replacer\n    {\n    public:\n          std::string operator()(const std::string& originalString,\n                                 const std::string& wordToBeReplaced,\n                                 const std::string& wordReplaceBy)\n         {\n               std::istringstream iss(originalString);\n               std::ostringstream oss;\n               std::string word;\n               while (iss >> word)\n               {\n                    if (0 == word.compare(wordToBeReplaced))\n                    {\n                         oss << wordReplaceBy << \" \";\n                    }\n                    else\n                    {\n                         oss << word << \" \";\n                    }\n               }\n               return oss.str();\n         }\n    };\n    ```\n\n6.  进入`主`功能。创建一个名为 worker 的`Replacer`类的实例。定义`foodList`变量，用包含食物列表的字符串初始化；有些项目应该重复。定义`changedList`字符串变量，并通过`worker()`函数的返回值对其进行初始化。使用`标准::cout`在终端显示结果:\n\n    ```cpp\n    int main()\n    {\n          Replacer worker;\n          std::string foodList(\"coffee tomatoes coffee cucumbers sugar\");\n          std::string changedList(worker(foodList, \"coffee\", \"chocolate\"));\n          std::cout << changedList;\n          return 0;\n    }\n    ```\n\n7.  编译、构建和运行练习。因此，您将获得以下输出:\n\n![Figure 6.8: The result of executing Exercise 3](img/C14583_06_08.jpg)\n\n###### 图 6.8:执行练习 3 的结果\n\n干得好！在这里，我们学习了如何使用字符串流来格式化输入和输出。我们创建了一个应用，可以轻松替换句子中的单词，增强了我们的知识，现在我们准备学习输入/输出操纵器，这样我们就可以提高处理线程的技能。\n\n### 输入/输出操纵器\n\n到目前为止，我们已经了解了使用流的简单输入和输出，但是在许多情况下它们还不够。对于更复杂的输入/输出数据格式化，标准库有一大套输入/输出操纵器。它们是开发出来与左(<>)移位运算符一起控制流行为的函数。输入/输出操纵器分为两种类型——无参数调用的和需要参数的。其中一些既用于输入又用于输出。让我们简单考虑一下它们的含义和用法。\n\n### 用于更改流的数值基数的输入/输出操纵器\n\n在`< ios >`头中，有用于更改流的数字基数的声明函数:`std::dec`、`std::hex`和`std::oct`。它们在没有参数的情况下被调用，并将流的基数分别设置为十进制、十六进制和八进制。在`< iomanip >`头中，声明了`std::setbase`函数，使用以下参数调用:8、10 和 16。它们可以互换，用于输入和输出操作。\n\n在`< ios >`头中，还有`std::showbase`和`std::noshowbase`功能，控制显示流的数字基数。它们只影响十六进制和八进制整数输出，除了零值，以及货币输入和输出操作。让我们完成一个练习，并学习如何在实践中使用它们。\n\n### 练习 4:以不同的数字基数显示输入的数字\n\n在本练习中，我们将开发一个应用，该应用在无限循环中要求用户以下列数字基数之一输入一个整数:十进制、十六进制或八进制。读取输入后，它会以其他数字表示形式显示该整数。要执行本练习，请完成以下步骤:\n\n1.  包括用于流支持的`< iostream >`报头。声明名为`BASE`的枚举，定义三个值:`DECIMAL`、`OCTAL`、`十六进制` :\n\n    ```cpp\n    #include <iostream>\n    enum BASE\n    {\n          DECIMAL,\n          OCTAL,\n          HEXADECIMAL\n    };\n    ```\n\n2.  声明一个名为`displayInBases`的函数，该函数接受两个参数——整数和基数。接下来，定义 switch 语句，该语句测试接收到的数字基数，并以另外两种数字表示形式显示给定的整数:\n\n    ```cpp\n    void displayInBases(const int number, const BASE numberBase)\n    {\n      switch(numberBase)\n      {\n      case DECIMAL:\n        std::cout << \"Your input in octal with base: \"\n              << std::showbase << std::oct << number\n              << \", without base: \" \n              << std::noshowbase << std::oct << number << std::endl;\n        std::cout << \"Your input in hexadecimal with base: \"\n              << std::showbase << std::hex << number\n              << \", without base: \" \n              << std::noshowbase << std::hex << number << std::endl;\n        break;\n      case OCTAL:\n        std::cout << \"Your input in hexadecimal with base: \"\n              << std::showbase << std::hex << number\n              << \", without base: \" \n              << std::noshowbase << std::hex << number << std::endl;\n        std::cout << \"Your input in decimal with base: \"\n              << std::showbase << std::dec << number\n              << \", without base: \" \n              << std::noshowbase << std::dec << number << std::endl;\n        break;\n      case HEXADECIMAL:\n        std::cout << \"Your input in octal with base: \"\n              << std::showbase << std::oct << number\n              << \", without base: \" \n              << std::noshowbase << std::oct << number << std::endl;\n        std::cout << \"Your input in decimal with base: \"\n              << std::showbase << std::dec << number\n              << \", without base: \" \n              << std::noshowbase << std::dec << number << std::endl;\n        break;\n      }\n    }\n    ```\n\n3.  进入`主`功能，定义用于读取用户输入的整数变量:\n\n    ```cpp\n    int integer; \n    ```\n\n4.  创建一个无限 while 循环。在循环中，要求用户输入一个十进制值。将输入读取为十进制整数。传递给`显示基站`功能。接下来，要求用户输入一个十六进制值。将输入读取为十六进制整数。传递到`显示基站`功能。最后，要求用户输入一个八进制值。将输入读取为八进制整数。传递到`显示框`功能:\n\n    ```cpp\n    int main(int argc, char **argv)\n    {\n      int integer;\n      while(true)\n      {\n        std::cout << \"Enter the decimal value: \";\n        std::cin >> std::dec >> integer;\n        displayInBases(integer, BASE::DECIMAL);\n        std::cout << \"Enter the hexadecimal value: \";\n        std::cin >> std::hex >> integer;\n        displayInBases(integer, BASE::HEXADECIMAL);\n        std::cout << \"Enter the octal value: \";\n        std::cin >> std::oct >> integer;\n        displayInBases(integer, BASE::OCTAL);\n      }\n      return 0;\n    }\n    ```\n\n5.  Build and run the application. Follow the output and enter, for example, 12 in different numeric representations. The output should be as follows:\n\n    ![Figure 6.9: The result of executing Exercise 4, part 1](img/C14583_06_09.jpg)\n\n    ###### 图 6.9:执行练习 4 第 1 部分的结果\n\n6.  现在，让我们在`std::setbase()`功能中更改`std::dec`、`std::oct`和`std::hex`，检查输出是否相同。首先，为`std::setbase()`支持添加`< iomanip >`头。接下来，在循环中的主功能中，将`std::dec`替换为`std::setbase(10)`、`std::hex`替换为`std::setbase(16)`，将`std::oct`替换为`std::setbase(8)` :\n\n    ```cpp\n    int main(int argc, char **argv)\n    {\n      int integer;\n      while(true)\n      {\n        std::cout << \"Enter the decimal value: \";\n        std::cin >> std::setbase(10) >> integer;\n        displayInBases(integer, BASE::DECIMAL);\n        std::cout << \"Enter the hexadecimal value: \";\n        std::cin >> std::setbase(16) >> integer;\n        displayInBases(integer, BASE::HEXADECIMAL);\n        std::cout << \"Enter the octal value: \";\n        std::cin >> std::setbase(8) >> integer;\n        displayInBases(integer, BASE::OCTAL);\n      }\n      return 0;\n    }\n    ```\n\n7.  再次，构建并运行应用。根据输出，在不同的数字表示中输入相同的整数(12)。输出应如下所示:\n\n![Figure 6.10: The result of executing Exercise 4, part 2](img/C14583_06_10.jpg)\n\n###### 图 6.10:执行练习 4 第 2 部分的结果\n\n现在，比较结果。如您所见，输出是相同的。通过这样做，我们确保了这些功能是可互换的。\n\n### 浮点格式的输入/输出操纵器\n\n在`< ios >`表头，有声明的改变浮点数字格式的函数:`std::fixed`、`std::scientific`、`std::hexfloat`、`std::defaultfloat`。它们在没有参数的情况下被调用，并将`浮动字段`分别设置为固定值、科学值、固定值和科学值以及默认值。还有`std:: showpoint`和`std::noshowpoint`功能，控制显示浮点数字。它们只影响产量。`std::noshowpoint`函数只影响没有小数部分的浮点数字。\n\n在`< iomanip >`头中，有一个声明的`std:: setprecision`函数，用一个代表精度的数字调用。当点右侧的数字被删除时，结果将被舍入。如果数字太大，无法以正常方式表示，则忽略精度规格，以更方便的方式显示数字。您只需要设置一次精度，并在需要另一个精度时更改它。当您选择存储浮点变量的数据类型时，您应该注意到一些技巧。在 C++ 中，有三种数据类型可以表示浮点值:浮点、双精度和长双精度。\n\n浮点通常是 4 字节，双精度是 8 字节，长双精度是 8、12 或 16 字节。所以，每种方法的精确度都是有限的。浮点型最多可容纳 6-9 个有效数字，双精度型最多可容纳 15-18 个有效数字，长双精度型最多可容纳 33-36 个有效数字。如果您想比较它们之间的差异，请查看下表:\n\n![Figure 6.11: Comparison table of the floating-point types](img/C14583_06_11.jpg)\n\n###### 图 6.11:浮点类型对照表\n\n#### 注意\n\n当你需要超过六个有效数字的精度时，请选择双精度，否则你会得到意想不到的结果。\n\n让我们完成一个练习，并学习如何在实践中使用它们。\n\n### 练习 5:以不同格式显示输入的浮点数\n\n在本练习中，我们将编写一个应用，在无限循环中，要求用户输入一个浮点数。读取输入后，它以不同的格式类型显示该数字。要执行本练习，请完成以下步骤:\n\n1.  包括用于流支持的`< iostream >`报头和用于`std::setprecision`支持的`< iomanip >`\n2.  接下来，声明一个模板`格式打印`函数，该函数有一个名为`浮点`的模板参数，并接受一个这种类型的参数变量。接下来，通过调用`std::cout`对象中的`precision()`函数，将先前的精度存储在自动变量中。然后，在终端中以不同的格式显示给定的数字:带点、不带点，以及固定、科学、十六进制和默认浮点格式。接下来，在 for 循环中，从 0 到 22，以精度和循环计数器的大小显示给定的数字。循环退出后，使用我们之前存储的值设置回精度:\n\n    ```cpp\n    template< typename FloatingPoint >\n    void formattingPrint(const FloatingPoint number)\n    {\n         auto precision = std::cout.precision();\n         std::cout << \"Default formatting with point: \"\n                   << std::showpoint << number << std::endl\n                   << \"Default formatting without point: \"\n                   << std::noshowpoint << number << std::endl\n                   << \"Fixed formatting: \"\n                   << std::fixed << number << std::endl\n                   << \"Scientific formatting: \"\n                   << std::scientific << number << std::endl\n                   << \"Hexfloat formatting: \"\n                   << std::hexfloat << number << std::endl\n                   << \"Defaultfloat formatting: \"\n                   << std::defaultfloat << number << std::endl;\n         for (int i = 0; i < 22; i++)\n         {\n              std::cout << \"Precision: \" << i \n                        << \", number: \" << std::setprecision(i) \n                        << number << std::endl;\n         }\n         std::cout << std::setprecision(precision);\n    }\n    ```\n\n3.  进入`主`功能。声明一个名为`floatNum`的`float`变量、一个名为`doubleNum`的双变量和一个名为`longDoubleNum`的长双变量。然后在无限 while 循环中，要求用户输入一个浮点数，读取输入到`longDoubleNum`，传递到`formattingPrint`功能。接下来，使用`longDoubleNum`值初始化`doubleNum`，并将其传递给`formating print`功能。接下来，使用`longDoubleNum`值初始化`浮动`，并将其传递到`格式打印`功能:\n\n    ```cpp\n    int main(int argc, char **argv)\n    {\n         float floatNum;\n         double doubleNum;\n         long double longDoubleNum;\n         while(true)\n         {\n              std::cout << \"Enter the floating-point digit: \";\n              std::cin >> std::setprecision(36) >> longDoubleNum;\n              std::cout << \"long double output\" << std::endl;\n              formattingPrint(longDoubleNum);\n              doubleNum = longDoubleNum;\n              std::cout << \"double output\" << std::endl;\n              formattingPrint(doubleNum);\n              floatNum = longDoubleNum;\n              std::cout << \"float output\" << std::endl;\n              formattingPrint(floatNum);\n         }\n         return 0;\n    }\n    ```\n\n4.  构建并运行应用。跟随输出，输入有效位数为`22`的浮点值，如`0.2222222222222222222222222222222222`。我们会得到一个长输出。现在，我们需要把它分开来分析。下面是长双精度值输出的一部分截图:\n\n![Figure 6.12: The result of executing Exercise 5, part 1](img/C14583_06_12.jpg)\n\n###### 图 6.12:执行练习 5 第 1 部分的结果\n\n我们可以看到，默认情况下，固定和`defaultfloat`编队只输出六个有效数字。通过科学的格式化，值的输出看起来与预期的一样。当我们调用`设定精度(0)`或`设定精度(1)`时，我们期望在该点之后不会输出任何数字。但是如果数字小于 1 设定精度，这将在点后留下一个数字。通过这样做，我们将看到直到 21 精度的正确输出。这意味着在我们的系统中，长双精度的最大精度是 20 个有效数字。现在，让我们分析双精度值的输出:\n\n![Figure 6.13: The result of executing Exercise 5, part 2](img/C14583_06_13.jpg)\n\n###### 图 6.13:执行练习 5 第 2 部分的结果\n\n在这里，我们可以看到相同的格式化结果，但精度不同。不准确的输出从精度 17 开始。这意味着，在我们的系统中，双精度的最大精度是 16 个有效数字。现在，让我们分析浮点值的输出:\n\n![Figure 6.14: The result of executing Exercise 5, part 3](img/C14583_06_14.jpg)\n\n###### 图 6.14:执行练习 5 第 3 部分的结果\n\n在这里，我们可以看到相同的格式化结果，但是精度不同。不准确的输出从精度 8 开始。这意味着，在我们的系统中，浮点数的最大精度是 8 位有效数字。不同系统上的结果应该是不同的。对它们的分析将帮助您为应用选择正确的数据类型。\n\n#### 注意\n\n切勿使用浮动数据类型来表示货币或汇率；你可能会得到错误的结果。\n\n### 用于布尔格式的输入/输出操纵器\n\n在`< ios >`头文件中，有用于更改布尔格式的声明函数:`std::boolalpha`和`STD::nopoolalpha`。它们在没有参数的情况下被调用，并允许我们分别以文本或数字的方式显示布尔值。它们用于输入和输出操作。让我们考虑一个使用这些输入/输出操纵器进行输出操作的例子。我们将把布尔值显示为文本和数字:\n\n```cpp\nstd::cout << \"Default formatting of bool variables: \"\n          << \"true: \" << true\n          << \", false: \" << false << std::endl;\nstd::cout << \"Formatting of bool variables with boolalpha flag is set: \"\n          << std::boolalpha\n          << \"true: \" << true\n          << \", false: \" << false << std::endl;\nstd::cout << \"Formatting of bool variables with noboolalpha flag is set: \"\n          << std::noboolalpha\n          << \"true: \" << true\n          << \", false: \" << false << std::endl;\n```\n\n编译并运行此示例后，您将获得以下输出:\n\n```cpp\nDefault formatting of bool variables: true: 1, false: 0\nFormatting of bool variables with boolalpha flag is set: true: true, false: false\nFormatting of bool variables with noboolalpha flag is set: true: 1, false: 0\n```\n\n如您所见，布尔变量的默认格式是使用`std::noboolalpha`标志执行的。为了在输入操作中使用这些函数，我们需要一个包含真/假单词或 0/1 符号的源字符串。输入操作中的`std::boolalpha`和`STD::nopoolalpha`函数调用如下:\n\n```cpp\nbool trueValue, falseValue;\nstd::istringstream iss(\"false true\");\niss >> std::boolalpha >> falseValue >> trueValue;\nstd::istringstream iss(\"0 1\");\niss >> std::noboolalpha >> falseValue >> trueValue;\n```\n\n如果您随后输出这些变量，您将看到它们通过读取布尔值被正确初始化。\n\n### 用于字段宽度和填充控制的输入/输出操纵器\n\n在标准库中，也有通过输出字段的宽度进行操作的功能，当宽度大于输出数据时，应该使用哪些字符，这些填充字符应该插入到哪个位置。当您想要将输出对齐到左侧或右侧位置，或者想要用其他符号替换空格时，这些函数将非常有用。例如，假设您需要在两列中打印价格。如果使用标准格式，您将获得以下输出:\n\n```cpp\n2.33 3.45\n2.2 4.55\n3.67 3.02\n```\n\n这个看起来不太好，很难读懂。如果我们应用格式，输出将如下所示:\n\n```cpp\n2.33   3.45\n2.2     4.55\n3.67   3.02\n```\n\n这看起来更好。同样，您可能需要检查哪些字符用于填充空格，哪些字符实际上是您在数字之间插入的空格。例如，让我们将填充字符设置为“*”。您将获得以下输出:\n\n```cpp\n2.33* 3.45*\n2.2** 4.55*\n3.67* 3.02*\n```\n\n现在，你可以看到空白处布满了星星。现在，我们已经考虑了格式化宽度和填充输出的有用之处，让我们考虑如何使用输入/输出操纵器来实现这一点。`std::setw`和`std::setfill`功能在`< iomanip >`头中声明。`std::setw`取整数值作为参数，将流的宽度设置为精确的 n 个字符。在少数情况下，宽度将设置为 0。它们如下:\n\n*   当用`std::string`或`char`调用换挡操作符时\n*   当调用`std::put_money()`函数时\n*   调用`std::quoted()`函数时\n\n在`< ios >`表头中，有用于更改填充字符插入位置的声明功能:`std::internal`、`std::left`、`std::right`。它们仅用于输出操作，并且只影响整数、浮点和货币值。\n\n现在，让我们考虑一个一起使用它们的例子。让我们输出宽度为 10 的正、负、浮点和十六进制值，并用“`#`”替换填充字符:\n\n```cpp\nstd::cout << \"Internal fill: \" << std::endl\n          << std::setfill('#')\n          << std::internal\n          << std::setw(10) << -2.38 << std::endl\n          << std::setw(10) << 2.38 << std::endl\n          << std::setw(10) << std::hex << std::showbase << 0x4b << std::endl;\nstd::cout << \"Left fill: \" << std::endl\n          << std::left\n          << std::setw(10) << -2.38 << std::endl\n          << std::setw(10) << 2.38 << std::endl\n          << std::setw(10) << std::hex << std::showbase << 0x4b << std::endl;\nstd::cout << \"Right fill: \" << std::endl\n          << std::right\n          << std::setw(10) << -2.38 << std::endl\n          << std::setw(10) << 2.38 << std::endl\n          << std::setw(10) << std::hex << std::showbase << 0x4b << std::endl;\n```\n\n构建并运行此示例后，您将获得以下输出:\n\n```cpp\nInternal fill: \n-#####2.38\n######2.38\n0x######4b\nLeft fill: \n-2.38#####\n2.38######\n0x4b######\nRight fill: \n#####-2.38\n######2.38\n######0x4b\n```\n\n### 其他数字格式的输入/输出操纵器\n\n如果需要输出一个带“+”号的正数值，可以从`< ios >`头中使用另一个 I/O 操纵器–`STD::show pos`功能。也存在与意义相反的操纵器——T4 标准:无显示功能。它们都对产出有影响。它们的使用非常容易。让我们考虑以下示例:\n\n```cpp\nstd::cout << \"Default formatting: \" << 13 << \" \" << 0 << std::endl;\nstd::cout << \"showpos flag is set: \" << std::showpos << 13 << \" \" << 0 << std::endl;\nstd::cout << \"noshowpos flag is set: \" << std::noshowpos << 13 << \" \" << 0 << std::endl;\n```\n\n这里，我们使用默认格式进行输出，然后使用`std::showpos`标志，最后使用`std::noshowpos`标志。如果您构建并运行这个小示例，您会看到，默认情况下，`std::noshowpos`标志被设置。看看执行的结果:\n\n```cpp\nDefault formatting: 13 0\nshowpos flag is set: +13 +0\nnoshowpos flag is set: 13 0\n```\n\n您还希望输出浮点或十六进制数字的大写字符，以便您可以使用来自`< ios >`头:`std::大写`和`std::无符号`的函数。他们只处理输出。让我们考虑一个小例子:\n\n```cpp\nstd::cout << \"12345.0 in uppercase with precision 4: \"\n          << std::setprecision(4) << std::uppercase << 12345.0 << std::endl;\nstd::cout << \"12345.0 in no uppercase with precision 4: \"\n          << std::setprecision(4) << std::nouppercase << 12345.0 << std::endl;\nstd::cout << \"0x2a in uppercase: \"\n          << std::hex << std::showbase << std::uppercase << 0x2a << std::endl;\nstd::cout << \"0x2a in nouppercase: \"\n          << std::hex << std::showbase << std::nouppercase << 0x2a << std::endl;\n```\n\n在这里，我们输出带和不带`std::大写`标志的浮点和十六进制数字。默认情况下，设置`标准::无标签`标志。看看执行的结果:\n\n```cpp\n12345.0 in uppercase with precision 4: 1.234E+004\n12345.0 in no uppercase with precision 4: 1.234e+004\n0x2a in uppercase: 0X2A\n0x2a in nouppercase: 0x2a\n```\n\n### 用于空白处理的输入/输出操纵器\n\n在标准库中，有处理空白的函数。来自`<的 **std::ws**`**函数是流>** 头，只对输入流起作用，并丢弃前导空格。来自`< ios >`头的`std::skipws`和`std::noskipws`功能用于控制前导空格的读写。它们为输入和输出流工作。当设置了`std::skipws`标志时，流会忽略字符序列输入前面的空白。默认情况下，设置`std::skipws`标志。让我们考虑一个使用这些输入/输出操纵器的例子。首先，我们将使用默认格式读取输入，并输出我们所读取的内容。接下来，我们将清除字符串并使用`std::noskipws`标志读取数据:\n\n```cpp\nstd::string name;\nstd::string surname;\nstd::istringstream(\"Peppy Ping\") >> name >> surname;\nstd::cout << \"Your name: \" << name << \", your surname: \" << surname << std::endl;\nname.clear();\nsurname.clear();\nstd::istringstream(\"Peppy Ping\") >> std::noskipws >> name >> surname;\nstd::cout << \"Your name: \" << name << \", your surname: \" << surname << std::endl;\n```\n\n构建并运行此示例后，我们将获得以下输出:\n\n```cpp\nYour name: Peppy, your surname: Ping\nYour name: Peppy, your surname:\n```\n\n从前面的输出可以看出，如果我们设置`std::noskipws`标志，我们也会读取空白。\n\n在`< iomanip >`表头中，已经声明了此表头的一个不寻常的操纵器:`std::报价`。当此函数应用于输入时，它用转义字符将给定的字符串用引号括起来。如果输入字符串已经包含转义引号，它也会读取它们。为了理解这一点，让我们考虑一个小例子。我们将使用不带引号的文本初始化一个源字符串，另一个字符串将使用带转义引号的文本初始化。接下来，我们将使用`std::ostringstream`读取它们，而不设置标志，并通过`std::cout`提供输出。看看下面的例子:\n\n```cpp\nstd::string str1(\"String without quotes\");\nstd::string str2(\"String with quotes \\\"right here\\\"\");\nstd::ostringstream ss;\nss << str1;\nstd::cout << \"[\" << ss.str() << \"]\" << std::endl;\nss.str(\"\");\nss << str2;\nstd::cout << \"[\" << ss.str() << \"]\" << std::endl; \n```\n\n因此，我们将获得以下输出:\n\n```cpp\n[String without quotes]\n[String with quotes \"right here\"] \n```\n\n现在，让我们做同样的输出，但是使用`std::报价`调用:\n\n```cpp\nstd::string str1(\"String without quotes\");\nstd::string str2(\"String with quotes \\\"right here\\\"\");\nstd::ostringstream ss;\nss << std::quoted(str1);\nstd::cout << \"[\" << ss.str() << \"]\" << std::endl;\nss.str(\"\");\nss << std::quoted(str2);\nstd::cout << \"[\" << ss.str() << \"]\" << std::endl;\n```\n\n现在，我们将有一个不同的结果:\n\n```cpp\n[\"String without quotes\"]\n[\"String with quotes \\\"right here\\\"\"]\n```\n\n您是否注意到第一个字符串用引号括起来，第二个字符串的子字符串“就在这里”用转义字符存储？\n\n现在，您知道如何在引号中包装任何字符串了。当使用`std::quoted()`时，甚至可以编写自己的包装来减少行数。例如，我们将流的工作转移到一个单独的函数:\n\n```cpp\nstd::string quote(const std::string& str)\n{\n     std::ostringstream oss;\n     oss << std::quoted(str);\n     return oss.str();\n}\n```\n\n然后，当我们需要调用包装器时，我们会执行以下操作:\n\n```cpp\nstd::string str1(\"String without quotes\");\nstd::string str2(\"String with quotes \\\"right here\\\"\");\nstd::coot << \"[\" << quote(str1) << \"]\" << std::endl;\nstd::cout << \"[\" << quote(str2) << \"]\" << std::endl;\n```\n\n现在，它看起来好多了。第一个话题已经结束了，让我们复习一下刚刚学过的内容。在实践中，我们了解了预定义流对象的使用、带有内部内存的文件的输入/输出操作、输入/输出格式以及用户定义类型的输入/输出。现在我们已经完全理解了如何在 C++ 中使用 I/O 库，我们将考虑当标准流不够用时该怎么办。\n\n### 制作附加流\n\n当提供的流接口不足以解决您的任务时，您可能希望创建一个额外的流来重用现有的接口之一。您可能需要从特定的外部设备输出或提供输入，或者您可能需要添加调用输入/输出操作的线程的标识。有几种方法可以做到这一点。您可以创建一个新的类，将现有流中的一个聚合为私有成员。它将通过已经存在的流函数实现所有需要的函数，例如移位运算符。另一种方法是继承一个现有的类，并以你需要的方式覆盖所有的虚函数。\n\n首先，你必须选择合适的类来使用。您的选择应该取决于您想要添加的修改。如果需要修改输入或输出操作，选择`std::basic_istream`、`std::basic_ostream`、`std::basic_iostream`。如果要修改状态信息、控制信息、私有存储等，选择`std::ios_base`。如果您想修改与流缓冲区相关的内容，请选择`std::basic_ios`。选择正确的基类后，继承前面提到的类之一来创建一个额外的流。\n\n还有一件事你必须知道——如何正确初始化标准流。在文件或字符串流和基本流类的初始化方面，有一些很大的区别。我们来复习一下。要初始化从文件流类派生的类的对象，需要传递文件名。若要初始化从字符串流类派生的类的对象，需要调用默认构造函数。它们都有自己的流缓冲区，因此在初始化时不需要额外的操作。要初始化从基本流类派生的类的对象，需要传递一个指向流缓冲区的指针。您可以创建一个缓冲区变量，也可以使用预定义流对象的缓冲区，如`std::cout`或`std::cerr`。\n\n让我们详细回顾一下这两种创建附加流的方法。\n\n### 如何制作附加流–合成\n\n组合意味着您将类的私有部分中的一些标准流对象声明为类成员。当您选择一个合适的标准流类时，转到它的头并注意它有哪个构造函数。然后，您需要在类的构造函数中正确初始化这个成员。要将您的类用作流对象，您需要实现基本函数，如 shift 运算符、`str()`等。您可能还记得，每个流类都为内置类型重载了移位运算符。他们还为预定义的功能(如`标准::endl`)提供了过载的换档操作符。您需要能够将您的类用作真正的流对象。我们只需要创建一个模板，而不是声明所有 18 个重载的移位运算符。此外，为了允许使用预定义的操纵器，我们必须声明一个带函数指针的移位运算符。\n\n这看起来并不困难，所以让我们尝试为`std::ostream`对象实现这样一个“包装器”。\n\n### 练习 6:在用户定义的类中组成标准流对象\n\n在本练习中，我们将创建自己的流对象，该对象包装了`std::ostream`对象并添加了附加特征。我们将创建一个名为`扩展流`的类，该类将向终端输出数据，并在每条输出的前面插入以下数据:日期和时间以及线程标识。要完成本练习，请执行以下步骤:\n\n1.  Include the required headers: `<iostream>` for `std::endl` support, `<sstream>` for `std::ostream` support, `<thread>` for `std::this_thread::get_id()` support, `<chrono>` for `std::chrono::system_clock::now()`, and `<ctime>` for converting timestamps into readable representations:\n\n    #### 注意\n\n    不要忘了在 Eclipse 项目设置中添加 **pthread** 链接器标志以获得线程支持:**项目** - > **属性**->**C/c++ Build**->**设置** - > **G++ 链接器** - > **杂项** - > **链接器标志**输入'**-PTT 此路径对 Eclipse 版本 3.8.1 有效；不同版本可能会有所不同。**\n\n    ```cpp\n    #include <iostream>\n    #include <sstream>\n    #include <thread>\n    #include <chrono>\n    #include <ctime>\n    ```\n\n2.  接下来，声明`扩展流`类。声明名为`m_oss`的`std::ostream`变量和名为`的 bool 变量 writeAdditionalInfo`。该 bool 变量将用于指示是否应打印扩展数据:\n\n    ```cpp\n    class extendedOstream\n    {\n    private:\n         std::ostream& m_oss;\n         bool writeAdditionalInfo;\n    };\n    ```\n\n3.  接下来，在公共部分，定义一个默认构造函数并用`std::cout`初始化`m_oss`，以将输出重定向到终端。用`真`初始化`写附加信息`:\n\n    ```cpp\n    extendedOstream()\n         : m_oss(std::cout)\n         , writeAdditionalInfo(true)\n    {\n    }\n    ```\n\n4.  定义一个模板重载的左移位运算符，`< <`，返回对`extendedstream`的引用，取一个模板参数值。然后如果`writeAdditionalInfo`为`true`，输出时间、线程 ID、给定值，然后将`writeAdditionalInfo`设置为`false`。如果`写附加信息`为`假`，只输出给定值。该功能将用于所有内置类型的输出:\n\n    ```cpp\n    template<typename T>\n    extendedOstream& operator<<(const T& value)\n    {\n         if (writeAdditionalInfo)\n         {\n              std::string time = fTime();\n              auto id = threadId();\n              m_oss << time << id << value;\n              writeAdditionalInfo = false;\n         }\n         else\n         {\n              m_oss << value;\n         }\n         return *this;\n    }\n    ```\n\n5.  定义另一个重载左移位运算符，该运算符将指向函数的指针作为参数，并返回对`std::ostream`的引用。在函数体中，将`writeaddionaliinfo`设置为`true`，调用给定的函数，并将`m_oss`作为参数传递。该重载操作符将用于预定义的功能，如`std::endl` :\n\n    ```cpp\n    extendedOstream&\n    operator<<(std::ostream& (*pfn)(std::ostream&))\n    {\n         writeAdditionalInfo = true;\n         pfn(m_oss);\n         return *this;\n    }\n    ```\n\n6.  在私有部分，定义`fTime`函数，该函数返回 std::string。它有一个系统时间。将其格式化为可读的表示形式并返回:\n\n    ```cpp\n    std::string fTime()\n    {\n         auto now = std::chrono::system_clock::now();\n         std::time_t time = std::chrono::system_clock::to_time_t(now);\n         std::ostringstream oss;\n         std::string strTime(std::ctime(&time));\n         strTime.pop_back();\n         oss << \"[\" << strTime << \"]\";\n         return oss.str();\n    }\n    ```\n\n7.  在私有部分，定义`threadId()`函数，该函数返回一个字符串。获取当前线程的`id`，格式化后返回:\n\n    ```cpp\n    std::string threadId()\n    {\n         auto id = std::this_thread::get_id();\n         std::ostringstream oss;\n         oss << \"[\" << std::dec << id << \"]\";\n         return oss.str();\n    }\n    ```\n\n8.  进入`主`功能。为了测试我们的流对象如何工作，创建一个名为`oss`的`extended stream`类型的对象。输出不同的数据，例如整数、浮点、十六进制和 bool:\n\n    ```cpp\n    extendedOstream oss;\n    oss << \"Integer: \" << 156 << std::endl;\n    oss << \"Float: \" << 156.12 << std::endl;\n    oss << \"Hexadecimal: \" << std::hex << std::showbase \n        << std::uppercase << 0x2a << std::endl;\n    oss << \"Bool: \" << std::boolalpha << false << std::endl;\n    ```\n\n9.  然后，创建一个线程，用 lambda 函数初始化它，并将相同的输出放入 lambda 中。别忘了加入线程:\n\n    ```cpp\n    std::thread thr1([]()\n         {\n              extendedOstream oss;\n              oss << \"Integer: \" << 156 << std::endl;\n              oss << \"Float: \" << 156.12 << std::endl;\n              oss << \"Hexadecimal: \" << std::hex << std::showbase\n                  << std::uppercase << 0x2a << std::endl;\n              oss << \"Bool: \" << std::boolalpha << false << std::endl;\n         });\n    thr1.join();\n    ```\n\n10.  现在，构建并运行应用。您将获得以下输出:\n\n![](img/C14583_06_15.jpg)\n\n###### 图 6.15:执行练习 6 的结果\n\n考虑输出的每一行。可以看到输出的下一种格式:“[日期和时间][线程 ID]输出数据”。确保线程标识因线程而异。然后，数据以预期的格式输出。因此，如您所见，使用标准流的组合来实现您自己的输入/输出流对象并不太难。\n\n### 如何制作附加流–继承\n\n继承意味着您创建自己的流类，并从具有虚拟析构函数的标准流对象中继承它。您的类必须是模板类，并且有模板参数，就像在父类中一样。要将所有继承的函数用于类的对象，继承应该是公共的。在构造函数中，您应该初始化父类，这取决于类的类型——用文件名、流缓冲区，或者默认情况下。接下来，您应该覆盖那些根据您的需求而改变的基本功能。\n\n我们需要继承标准流类的最常见情况是，当我们想要为新设备(如套接字或打印机)实现输入/输出操作时。所有定义的标准流类都负责格式化输入和输出，并具有字符串、文件和终端的重载。只有`std::basic_streambuf`类负责处理设备，所以我们需要继承这个类，编写自己的实现，并将其设置为标准类的流缓冲区。`streambuf`类的核心功能是传输字符。它可以在刷新之间使用缓冲区存储字符，也可以在每次调用后立即刷新。这些概念被称为缓冲和非缓冲字符传输。\n\n输出操作的缓冲字符传输工作如下:\n\n1.  字符通过`sputc()`函数调用缓冲到内部缓冲区。\n2.  当缓冲区已满时，`sputc()`调用受保护的虚拟成员，即`溢出()`。\n3.  `溢出()`功能将所有缓冲区内容传输到外部设备。\n4.  当调用`pubsync()`函数时，它调用被保护的虚拟成员`sync()`。\n5.  `sync()`功能将所有缓冲区内容传输到外部设备。\n\n用于输出操作的无缓冲字符传输的工作方式略有不同:\n\n1.  字符被传递到`sputc()`功能。\n2.  `sputc()`函数立即调用被保护的虚拟成员`overflow()`。\n3.  `溢出()`功能将所有缓冲区内容传输到外部设备。\n\n因此，对于输出操作的缓冲和非缓冲字符传输，我们应该覆盖`溢出()`和 sync()函数，它们执行实际工作。\n\n输入操作的缓冲字符传输工作如下:\n\n1.  `sgetc()`函数从内部缓冲区读取字符。\n2.  `sgetc()`功能调用`sunetc()`功能，使消耗的角色再次可用。\n3.  如果内部缓冲区为空，则`sgetc()`函数调用`下溢()`函数。\n4.  `下溢()`功能将字符从外部设备读取到内部缓冲区。\n\n`sgetc()`和`下溢()`函数总是返回相同的字符。为了每次读取不同的字符，我们还有另外一对功能:`sbumpc()`和`uflow()`。用它们读字符的算法是一样的:\n\n1.  `sbumpc()`函数从内部缓冲区读取字符。\n2.  `sbumpc()`函数调用`sputback()`函数，使下一个字符可用于输入。\n3.  如果内部缓冲区为空，则`sbumpc()`函数调用`uflow()`函数。\n4.  `uflow()`功能将字符从外部设备读取到内部缓冲区。\n\n用于输入操作的无缓冲字符传输的工作原理如下:\n\n1.  `sgetc()`函数调用被称为`下溢()`的受保护虚拟成员。\n2.  `下溢()`功能将字符从外部设备读取到内部缓冲区。\n3.  `sbumpc()`函数调用名为`uflow()`的受保护虚拟成员。\n4.  `uflow()`功能将字符从外部设备读取到内部缓冲区。\n\n如果出现任何错误，将调用名为`pbackfail()`的受保护虚拟成员来处理错误情况。如您所见，要覆盖`std::basic_streambuf`类，我们需要覆盖使用外部设备的虚拟成员。对于输入`streambuf`，我们应该覆盖`下溢()`、`uflow()`和`pbackfail()`成员。对于输出`streambuf`，我们应该覆盖`overflow()`和`sync()`成员。\n\n让我们更详细地考虑所有这些步骤。\n\n### 练习 7:继承标准流对象\n\n在本练习中，我们将创建一个名为`extended_streambuf`的类，该类继承自`std::basic_streambuf`。我们将使用`std::cout`流对象的一个缓冲区，并覆盖 overflow()函数，以便我们可以将数据写入外部设备(`stdout`)。接下来，我们将编写一个继承自`std::basic_ostream`类的`extended_ostream`类，并将一个流缓冲区设置为`extended_streambuf`。最后，我们将对我们的包装类做一些小的修改，并使用`extended_ostream`作为私有流成员。要完成本练习，请执行以下步骤:\n\n1.  包括所需的标题: **< iostream >** 表示 **std::endl** 支持、**T22】s stream>T5 表示 **std::ostream** 和 **std::basic_streambuf** 支持、**T24】螺纹>T11 表示**STD::this _ thread::get _ id()**支持、**T26】chrono>******\n2.  创建一个名为`extended_streambuf`的模板类，它继承自`std::basic_streambuf`类。覆盖名为`overflow()`的公共成员，该成员向输出流中写入一个字符，并返回 EOF 或写入的字符:\n\n    ```cpp\n    template< class CharT, class Traits = std::char_traits<CharT> >\n    class extended_streambuf : public std::basic_streambuf< CharT, Traits >\n    {\n    public:\n        int overflow( int c = EOF ) override\n        {\n            if (!Traits::eq_int_type(c, EOF))\n            {\n                return fputc(c, stdout);\n            }\n            return Traits::not_eof(c);\n        }\n    };\n    ```\n\n3.  接下来，创建一个名为`extended_ostream`的模板类，它是从`std::basic_ostream`类派生而来的。在私有部分，定义`extended_streambuf`类的一个成员，即 buffer。用缓冲成员初始化`std::basic_ostream`父类。接下来，在构造函数体中，从父类调用`init()`函数，以 buffer 作为参数。另外，重载`rdbuf()`函数，该函数返回一个指向缓冲变量的指针:\n\n    ```cpp\n    template< class CharT, class Traits = std::char_traits<CharT> >\n    class extended_ostream : public std::basic_ostream< CharT, Traits >\n    {\n    public:\n        extended_ostream()\n            : std::basic_ostream< CharT, Traits >::basic_ostream(&buffer)\n            , buffer()\n        {\n            this->init(&buffer);\n        }\n        extended_streambuf< CharT, Traits >* rdbuf () const\n        {\n            return (extended_streambuf< CharT, Traits >*)&buffer;\n        }\n    private:\n        extended_streambuf< CharT, Traits > buffer;\n    };\n    ```\n\n4.  将`扩展流`类重命名为记录器，以避免类似名称的误解。保持现有界面不变，但是用我们自己的流替换`std::ostream &`成员，即`对象- extended_ostream`。完整的类如下所示:\n\n    ```cpp\n    class logger\n    {\n    public:\n         logger()\n              : m_log()\n              , writeAdditionalInfo(true)\n         {\n         }\n         template<typename T>\n         logger& operator<<(const T& value)\n         {\n              if (writeAdditionalInfo)\n              {\n                   std::string time = fTime();\n                   auto id = threadId();\n                   m_log << time << id << value;\n                   writeAdditionalInfo = false;\n              }\n              else\n              {\n                   m_log << value;\n              }\n              return *this;\n         }\n         logger&\n         operator<<(std::ostream& (*pfn)(std::ostream&))\n         {\n              writeAdditionalInfo = true;\n              pfn(m_log);\n              return *this;\n         }\n    private:\n         std::string fTime()\n         {\n              auto now = std::chrono::system_clock::now();\n              std::time_t time = std::chrono::system_clock::to_time_t(now);\n              std::ostringstream log;\n              std::string strTime(std::ctime(&time));\n              strTime.pop_back();\n              log << \"[\" << strTime << \"]\";\n              return log.str();\n         }\n         std::string threadId()\n         {\n              auto id = std::this_thread::get_id();\n              std::ostringstream log;\n              log << \"[\" << std::dec << id << \"]\";\n              return log.str();\n         }\n    private:\n         extended_ostream<char> m_log;\n         bool writeAdditionalInfo;\n    };\n    ```\n\n5.  进入`主`功能，将`扩展数据流`对象更改为`记录器`对象。保持代码的其余部分不变。现在，构建并运行该练习。您将看到上一个练习中给出的输出，但是在本例中，我们使用了自己的流缓冲区、自己的流对象和向输出添加附加信息的包装类。查看下面截图中显示的执行结果，并将其与之前的结果进行比较。确保它们相似。如果是的话，这意味着我们做得很好，我们继承的类也像预期的那样工作:\n\n![Figure 6.16: The result of executing Exercise 7](img/C14583_06_16.jpg)\n\n###### 图 6.16:执行练习 7 的结果\n\n在这个主题中，我们已经做了很多，并学习了如何以不同的方式创建额外的流。我们考虑了所有适合继承的类，以及哪个类更适合不同的需求。我们还学习了如何从基本 streambuf 类继承来实现与外部设备的工作。现在，我们将学习如何以异步方式使用输入/输出流。\n\n### 利用异步输入/输出\n\n在很多情况下，输入/输出操作会花费大量时间，例如，创建备份文件、搜索大型数据库、读取大型文件等。您可以使用线程来执行输入/输出操作，而不会阻止应用的执行。但是对于某些应用来说，这不是处理长时间输入/输出的合适方式，例如，当每秒钟有数千个输入/输出操作时。在这种情况下，C++ 开发人员使用异步输入/输出。它节省了线程资源，并确保执行的线程不会被阻塞。让我们考虑一下什么是同步和异步输入/输出。\n\n你可能还记得第 5 章，哲学家的晚餐——线程和并发，同步操作意味着一些线程调用操作并等待它完成。它可以是单线程或多线程应用。要点是线程正在等待输入/输出操作完成。\n\n当操作不阻塞工作线程的执行时，异步执行发生。执行异步输入/输出操作的线程发送一个异步请求，并继续执行另一个任务。当操作完成时，初始线程将被通知完成，并且它可以根据需要处理结果。\n\n由此看来，异步 I/O 比同步好很多，但这要看情况。如果您需要执行大量快速输入/输出操作，由于处理内核输入/输出请求和信号的开销，遵循同步方式会更合适。因此，在为应用开发架构时，您需要考虑所有可能的场景。\n\n标准库不支持异步输入/输出操作。因此，为了利用异步输入/输出，我们需要考虑替代库或编写自己的实现。首先，让我们考虑平台相关的实现。然后，我们将看看跨平台库。\n\n### Windows 平台上的异步 I/O\n\nWindows 支持各种设备的 I/O 操作:文件、目录、驱动器、端口、管道、套接字、终端等等。一般来说，我们对所有这些设备使用相同的输入/输出接口，但某些设置因设备而异。让我们考虑在 Windows 中对文件进行输入/输出操作。\n\n因此，在 Windows 中，我们需要打开一个设备，并为其获取一个处理程序。不同的设备以不同的方式打开。要打开文件、目录、驱动器或端口，我们使用 **<窗口头中的**创建文件**功能。要打开管道，我们使用**创建命名管道**功能。要打开一个套接字，我们使用 socket()和 accept()函数。打开一个终端，我们使用**CreateConsoleScreenBuffer**和 **GetStdHandle** 功能。它们都返回一个设备处理程序，该程序用于处理该设备的所有功能。**\n\n`CreateFile`函数采用七个参数来管理打开设备的工作。函数声明如下所示:\n\n```cpp\nHANDLE CreateFile( PCTSTR pszName, \n                   DWORD  dwDesiredAccess, \n                   DWORD  dwShareMode, \n                   PSECURITY_ATTRIBUTES psa, \n                   DWORD  dwCreationDisposition, \n                   DWORD  dwFlagsAndAttributes, \n                   HANDLE hFileTemplate);\n```\n\n第一个参数是`pszName`–文件的路径。第二个参数调用`dwdesireaccess`并管理对设备的访问。它可以采用以下值之一:\n\n```cpp\n0 // only for configuration changing\nGENERIC_READ // only reading\nGENERIC_WRITE // only for writing\nGENERIC_READ | GENERIC_WRITE // both for reading and writing\n```\n\n第三个参数`dwShareMode`管理当文件已经打开时，操作系统应该如何处理所有新的`创建文件`调用。它可以采用以下值之一:\n\n```cpp\n0 // only one application can open device simultaneously\nFILE_SHARE_READ // allows reading by multiple applications simultaneously\nFILE_SHARE_WRITE // allows writing by multiple applications simultaneously\nFILE_SHARE_READ | FILE_SHARE_WRITE // allows both reading and writing by multiple applications simultaneously\nFILE_SHARE_DELETE // allows moving or deleting by multiple applications simultaneously\n```\n\n第四个参数`psa`通常设置为`空`。第五个参数`dwCreationDisposition`管理文件是打开还是创建。它可以采用以下值之一:\n\n```cpp\nCREATE_NEW // creates new file or fails if it is existing\nCREATE_ALWAYS // creates new file or overrides existing\nOPEN_EXISTING // opens file or fails if it is not exists\nOPEN_ALWAYS // opens or creates file\nTRUNCATE_EXISTING // opens existing file and truncates it or fails if it is not exists\n```\n\n第六个参数`dwFlagsAndAttributes`，管理缓存或使用文件。它可以采用以下值之一来管理缓存:\n\n```cpp\nFILE_FLAG_NO_BUFFERING // do not use cache\nFILE_FLAG_SEQUENTIAL_SCAN // tells the OS that you will read the file sequentially\nFILE_FLAG_RANDOM_ACCESS // tells the OS that you will not read the file in sequentially\nFILE_FLAG_WR1TE_THROUGH // write without cache but read with\n```\n\n它可以采用以下值之一来管理文件工作:\n\n```cpp\nFILE_FLAG_DELETE_ON_CLOSE // delete file after closing (for temporary files)\nFILE_FLAG_BACKUP_SEMANTICS // used for backup and recovery programs\nFILE_FLAG_POSIX_SEMANTICS // used to set case sensitive when creating or opening a file\nFILE_FLAG_OPEN_REPARSE_POINT // allows to open, read, write, and close files differently\nFILE_FLAG_OPEN_NO_RECALL // prevents the system from recovering the contents of the file from archive media\nFILE_FLAG_OVERLAPPED // allows to work with the device asynchronously\n```\n\n它可以采用下列文件属性值之一:\n\n```cpp\nFILE_ATTRIBUTE_ARCHIVE // file should be deleted\nFILE_ATTRIBUTE_ENCRYPTED // file is encrypted\nFILE_ATTRIBUTE_HIDDEN // file is hidden\nFILE_ATTRIBUTE_NORMAL // other attributes are not set\nFILE_ATTRIBUTE_NOT_CONTENT_ INDEXED // file is being processed by the indexing service\nFILE_ATTRIBUTE_OFFLINE // file is transferred to archive media\nFILE_ATTRIBUTE_READONLY // only read access\nFILE_ATTRIBUTE_SYSTEM // system file\nFILE_ATTRIBUTE_TEMPORARY // temporary file\n```\n\n最后一个参数`hFileTemplate`将打开文件的处理程序或`空值`作为参数。如果文件处理程序通过，则`创建文件`功能将忽略所有属性和标志，并使用打开文件的属性和标志。\n\n以上就是关于`创建文件`参数。如果无法打开设备，则返回`无效 _ 句柄 _ 值`。以下示例演示如何打开文件进行读取:\n\n```cpp\n#include <iostream>\n#include <Windows.h>\nint main()\n{\n     HANDLE hFile = CreateFile(TEXT(\"Test.txt\"), GENERIC_READ, \n                                FILE_SHARE_READ | FILE_SHARE_WRITE, \n                                NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);\n     if (INVALID_HANDLE_VALUE == hFile)\n         std::cout << \"Failed to open file for reading\" << std::endl;\n     else\n         std::cout << \"Successfully opened file for reading\" << std::endl;\n     CloseHandle(hFile);\n     return 0;\n}\n```\n\n接下来，为了执行输入操作，我们使用`读取文件`功能。它以文件描述符为第一个参数，源缓冲区为第二个参数，最大读取字节数为第三个参数，读取字节数为第四个参数，同步执行的`空`值或指向有效且唯一的重叠结构的指针为最后一个参数。如果操作成功，`读取文件`返回真，否则返回假。下面的示例演示如何从以前打开的文件中输入内容以供读取:\n\n```cpp\nBYTE pb[20];\nDWORD dwNumBytes;\nReadFile(hFile, pb, 20, &dwNumBytes, NULL);\n```\n\n为了执行输出操作，我们使用`写文件`功能。它的声明与`ReadFile`相同，但是第三个参数设置了要写入的字节数，第五个参数是写入的字节数。下面的示例演示如何输出到以前打开的文件进行写入:\n\n```cpp\nBYTE pb[20] = \"Some information\\0\";\nDWORD dwNumBytes;\nWriteFile(hFile, pb, 20, &dwNumBytes, NULL);\n```\n\n要将缓存数据写入设备，请使用`FlushFileBuffer`功能。它只需要一个参数——文件描述符。让我们转到异步输入/输出。要让操作系统知道您计划与设备异步工作，您需要使用`文件 _ 标志 _ 重叠`标志打开它。现在，打开文件进行写入或读取，如下所示:\n\n```cpp\n#include <iostream>\n#include <Windows.h>\nint main()\n{\n     HANDLE hFile = CreateFile(TEXT(\"Test.txt\"), GENERIC_READ, \n                                FILE_SHARE_READ | FILE_SHARE_WRITE, \n                                NULL, OPEN_ALWAYS, FILE_FLAG_OVERLAPPED, NULL);\n     if (INVALID_HANDLE_VALUE == hFile)\n         std::cout << \"Failed to open file for reading\" << std::endl;\n     else\n         std::cout << \"Successfully opened file for reading\" << std::endl;\n     CloseHandle(hFile);\n     return 0;\n}\n```\n\n我们使用相同的操作来执行文件的读取或写入，即`读取文件`和`写入文件`，唯一的区别是读取或写入的字节数被设置为空，我们必须传递一个有效且唯一的`重叠`对象。让我们考虑一下重叠对象的结构:\n\n```cpp\ntypedef struct _OVERLAPPED { \nDWORD  Internal; // for error code \nDWORD  InternalHigh; // for number of read bytes \nDWORD  Offset; \nDWORD  OffsetHigh; \nHANDLE hEvent; // handle to an event \n} OVERLAPPED, *LPOVERLAPPED;\n```\n\n内部成员设置为`STATUS_PENDING`，表示操作还没有开始。读取或写入的字节数将被写入`内部高`成员。`偏移`和`偏移`在异步操作中被忽略。`hEvent`成员用于接收关于异步操作完成的事件。\n\n#### 注意\n\n输入/输出操作的顺序没有保证，因此您不能依赖于此。如果你计划在一个地方写一个文件，在另一个地方读一个文件，你不能依赖这个顺序。\n\n在异步模式下使用`读文件`和`写文件`有一点不同寻常。如果输入/输出请求是同步执行的，它们将返回非零值。如果他们返回`假`，您需要调用`GetLastError`功能来检查为什么返回`假`。如果错误代码为`ERROR_IO_PENDING`，这意味着输入/输出请求已成功处理，处于挂起状态，稍后将执行。\n\n你要记住的最后一点是，在输入输出操作完成之前，你不能移动或移除带有数据的`重叠的`对象或缓冲区。对于每个输入/输出操作，您应该创建一个新的重叠对象。\n\n最后，让我们考虑系统通知我们完成输入/输出操作的方式。有一些这样的机制:释放设备、释放事件、产生警报和使用输入/输出端口。\n\n**“坏”方式**:`写文件`和`读文件`功能将设备设置为“占用”状态。当输入/输出操作完成时，驱动程序将设备设置为“空闲”状态。我们可以检查完成的输入/输出操作是调用`等待单对象`还是`等待多对象`功能。以下示例演示了这种方法:\n\n```cpp\n#include <Windows.h>\n#include <WinError.h>\nint main()\n{\n     HANDLE hFile = CreateFile(TEXT(\"Test.txt\"), GENERIC_READ,\n                                     FILE_SHARE_READ | FILE_SHARE_WRITE, NULL,\n                                     OPEN_ALWAYS, FILE_FLAG_OVERLAPPED, NULL);\n     BYTE bBuffer[100];\n     OVERLAPPED o = { 0 };\n     BOOL bResult = ReadFile(hFile, bBuffer, 100, NULL, &o);\n     DWORD dwError = GetLastError();\n     if (bResult && (dwError == ERROR_IO_PENDING))\n     {\n          WaitForSingleObject(hFile, INFINITE);\n          bResult = TRUE;\n     }\n     CloseHandle(hFile);\n     return 0;\n}\n```\n\n这是检查输入/输出操作是否已完成的最简单方法。但是这种方法使调用线程等待`WaitForSingleObject`调用，所以它变成了同步调用。此外，您可以为此设备启动一些输入/输出操作，但您不能确定线程会在设备需要的版本上唤醒。\n\n**好一点，但不是最好的做法**:你还记得重叠结构的最后一个成员吗？通过调用`创建事件`功能创建一个事件，并将其设置为`重叠`对象。然后，当输入输出操作完成时，系统通过调用`设置事件`功能来释放该事件。接下来，当调用线程需要获得一个正在执行的 I/O 操作的结果时，您调用`WaitForSingleObject`并传递这个事件的描述符。以下示例演示了这种方法:\n\n```cpp\n#include <Windows.h>\n#include <synchapi.h>\nint main()\n{\n     HANDLE hFile = CreateFile(TEXT(\"Test.txt\"), GENERIC_READ, \n                               FILE_SHARE_READ | FILE_SHARE_WRITE,\n                               NULL, OPEN_ALWAYS, FILE_FLAG_OVERLAPPED, NULL);\n     BYTE bInBuffer[10];\n     OVERLAPPED o = { 0 };\n     o.hEvent = CreateEvent(NULL,TRUE,FALSE,\"IOEvent\");\n     ReadFile(hFile, bInBuffer, 10, NULL, &o);\n     ///// do some work\n     HANDLE hEvent = o.hEvent;\n     WaitForSingleObject(hEvent, INFINITE);\n     CloseHandle(hFile);\n     return 0;\n}\n```\n\n如果您希望通知调用线程输入/输出操作的结束，这是一个非常简单的方法。但是这并不是实现这一点的理想方式，因为当有很多这样的操作时，您需要为每个操作创建一个事件对象。\n\n**还有一种不是最好的方法**:可报警的输入/输出以下列方式工作。我们将`ReadFileEx`和`WriteFileEx`称为输入/输出。它们类似于标准的`ReadFile`和`WriteFile`，但是我们不传递存储读或写字符数的变量，我们传递回调函数的地址。这个回调函数被称为完成例程，并具有以下声明:\n\n```cpp\nVOID WINAPI \nCompletionRoutine(DWORD dwError,\n                  DWORD dwNumBytes,\n                  OVERLAPPED* po);\n```\n\n`ReadFileEx``WriteFileEx`将回调函数的地址传递给设备驱动。当设备上的操作完成时，驱动程序将回调函数的地址添加到 APC 队列中，并将指针添加到重叠结构中。然后，操作系统调用这个函数，并传递读或写字节数、错误代码和指向重叠结构的指针。\n\n这种方法的主要缺点是编写回调函数和使用大量全局变量，因为回调函数在上下文中只有少量信息。不使用这种方法的另一个原因是，只有调用线程可以接收关于完成的通知。\n\n现在我们已经讨论了糟糕的情况，让我们看看处理输入/输出结果的最佳方法——输入/输出端口。输入/输出完成端口被开发用于线程池。为了创建这样一个端口，我们使用`CreateIoCompletionPort`。这个函数的声明如下:\n\n```cpp\nHANDLE \nCreateIoCompletionPort(HANDLE hFile,\n                       HANDLE hExistingCompletionPort,\n                       ULONG_PTR CompletionKey,\n                       DWORD dwNumberOfConcurrentThreads);\n```\n\n该函数创建一个输入/输出完成端口，并将设备与该端口相关联。要完成这个动作，我们需要调用两次。为了创建新的完成端口，我们调用`CreateIoCompletionPort`函数并传递`INVALID_HANDLE_VALUE`作为第一个参数，NULL 作为第二个参数，0 作为第三个参数，并传递这个端口的线程数。将 0 作为第四个参数传递会将线程数设置为等于处理器数。\n\n#### 注意\n\n对于输入/输出完成端口，建议使用等于处理器数量两倍的线程数量。\n\n接下来，我们需要将这个端口与输入/输出设备相关联。因此，我们第二次调用`CreateIoCompletionPort`函数，传递一个设备的描述符，一个所创建的完成端口的描述符，一个指示读取或写入设备的常量，以及作为线程数的 0。然后，当我们需要得到完成的结果时，我们从我们的端口描述符中调用`GetQueuedCompletionStatus`。如果操作完成，函数会立即返回一个结果。如果没有，那么线程等待完成。以下示例演示了这种方法:\n\n```cpp\n#include <Windows.h>\n#include <synchapi.h>\nint main()\n{\n    HANDLE hFile = CreateFile(TEXT(\"Test.txt\"), GENERIC_READ,\n                              FILE_SHARE_READ | FILE_SHARE_WRITE,\n                              NULL, OPEN_ALWAYS, FILE_FLAG_OVERLAPPED, NULL);\n    HANDLE m_hIOcp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);\n    CreateIoCompletionPort(hFile, m_hIOcp, 1, 0);\n\n    BYTE bInBuffer[10];\n    OVERLAPPED o = { 0 };\n    ReadFile(hFile, bInBuffer, 10, NULL, &o);\n\n    DWORD dwNumBytes;\n    ULONG_PTR completionKey;\n    GetQueuedCompletionStatus(m_hIOcp, &dwNumBytes, &completionKey, (OVERLAPPED**) &o, INFINITE);\n    CloseHandle(hFile);\n    return 0;\n}\n```\n\n### Linux 平台上的异步 I/O\n\nLinux 上的异步输入/输出支持对不同设备的输入和输出，如套接字、管道和 TTYs，文件除外。是的，这很奇怪，但是 Linux 开发人员认为对文件的输入/输出操作足够快。\n\n要打开输入/输出设备，我们使用 open()函数。它有以下声明:\n\n```cpp\nint open (const char *filename, int flags[, mode_t mode])\n```\n\n第一个参数是文件名，而第二个参数是控制文件打开方式的位掩码。如果系统无法打开设备，open()将返回-1 值。如果成功，它会返回一个设备描述符。打开模式的可能标志有`O_RDONLY`、`O_WRONLY`和`O_RDWR`。\n\n为了执行输入/输出操作，我们使用名为`aio`的`POSIX`接口。它们有一组定义好的功能，例如`aio_read`、`aio_write`、`aio_fsync`等等。它们用于启动异步操作。为了得到执行的结果，我们可以使用信号通知或者线程的实例化。或者，我们可以选择完全不被通知。全部在`< aio.h >`表头声明。\n\n这些几乎都是以`aiocb`结构(异步 IO 控制块)为参数。它控制输入输出操作。该结构的声明如下:\n\n```cpp\nstruct aiocb \n{\n    int aio_fildes;\n    off_t aio_offset;\n    volatile void *aio_buf;\n    size_t aio_nbytes;\n    int aio_reqprio;\n    struct sigevent aio_sigevent;\n    int aio_lio_opcode;\n};\n```\n\n`aio_fildes`成员是打开的设备的描述符，而`aio_offset`成员是设备中应该执行读或写操作的偏移量。`aio_buf`成员是一个指向要读取或写入的缓冲区的指针。`aio_nbytes`成员是缓冲区的大小。`aio_reqprio`成员是该 io 操作执行的优先级。`aio_sigevent`成员是一个指出应该如何通知调用线程完成的结构。`aio_lio_opcode`成员是一种输入输出操作。下面的例子演示了如何初始化`aiocb`结构:\n\n```cpp\nstd::string fileContent;\nconstexpr int BUF_SIZE = 20;\nfileContent.resize(BUF_SIZE, 0);\naiocb aiocbObj;\naiocbObj.aio_fildes = open(\"test.txt\", O_RDONLY);\nif (aiocbObj.aio_fildes == -1)\n{\n     std::cerr << \"Failed to open file\" << std::endl;\n     return -1;\n}\naiocbObj.aio_buf = const_cast<char*>(fileContent.c_str());\naiocbObj.aio_nbytes = BUF_SIZE;\naiocbObj.aio_reqprio = 0;\naiocbObj.aio_offset = 0;\naiocbObj.aio_sigevent.sigev_notify = SIGEV_SIGNAL;\naiocbObj.aio_sigevent.sigev_signo = SIGUSR1;\naiocbObj.aio_sigevent.sigev_value.sival_ptr = &aiocbObj;\n```\n\n这里，我们创建了一个读取文件内容的缓冲区，即`文件内容`。然后，我们创建了一个名为`aiocbObj`的`aiocb`结构。接下来，我们打开一个文件进行读取，并检查该操作是否成功。然后，我们将指针设置为一个缓冲区和一个缓冲区大小。缓冲区大小告诉驱动程序应该读取或写入多少字节。接下来，我们指出，我们将通过将偏移量设置为 0 来从文件的开头读取。然后，我们在`SIGEV_SIGNAL`中设置通知类型，这意味着我们希望获得关于完成操作的信号通知。然后，我们设置信号编号，这将触发关于完成的通知。在我们的例子中，是`sigusr 1`–用户定义的信号。接下来，我们将指向`aiocb`结构的指针设置为信号处理器。\n\n在创建并正确初始化`aiocb`结构后，我们可以执行输入或输出操作。让我们完成一个练习，了解如何在 Linux 平台上使用异步输入/输出。\n\n### 练习 8:在 Linux 中异步读取文件\n\n在本练习中，我们将开发一个以异步方式从文件中读取数据并将读取的数据输出到控制台的应用。当执行读取操作时，驱动器使用触发信号通知应用。要进行本练习，请执行以下步骤:\n\n1.  包括所有必需的头: **< aio.h >** 支持异步读写， **< signal.h >** 支持信号， **< fcntl.h >** 支持文件操作，**T21】unist . h>**支持符号常量， **< iostream >** 输出到终端， **< chrono >**\n2.  创建一个名为**的布尔变量，它将指示操作何时完成:\n\n    ```cpp\n    bool isDone{};\n    ```** \n3.  定义将成为我们的信号处理器的函数，即`aioSigHandler`。异步操作完成后将调用它。信号处理器应具有以下签名:\n\n    ```cpp\n    void name(int number, siginfo_t* si, void* additional)\n    ```\n\n4.  第一个参数是信号编号，第二个参数是包含信号生成原因信息的结构，最后一个参数是附加信息。它可以被转换成`ucontext_t`结构的指针，这样我们就可以接收到被这个信号中断的线程上下文。在`aioSigHandler`中，使用`SI_ASYNCIO`检查关于异步输入/输出操作的信号是否恒定。如果是，输出一条消息。接下来，将`isDone`设置为`true` :\n\n    ```cpp\n    void\n    aioSigHandler(int no, siginfo_t* si, void*)\n    {\n         std::cout << \"Signo: \" << no << std::endl;\n         if (si->si_code == SI_ASYNCIO)\n         {\n              std::cout << \"I/O completion signal received\" << std::endl;\n         }\n         isDone = true;\n    }\n    ```\n\n5.  定义另一个名为`的帮助功能，启动`。它将初始化`信号`结构。这个结构定义了在输入/输出操作结束时将发送哪个信号，以及应该调用哪个处理程序。这里，我们选择了`sigusr 1`–一个用户自定义的信号。在`sa_flags`中，设置我们希望在动作重启或收到信息时发送该信号:\n\n    ```cpp\n    bool \n    initSigAct(struct sigaction& item)\n    {\n         item.sa_flags = SA_RESTART | SA_SIGINFO;\n         item.sa_sigaction = aioSigHandler;\n         if (-1 == sigaction(SIGUSR1, &item, NULL))\n         {\n              std::cerr << \"sigaction usr1 failed\" << std::endl;\n              return false;\n         }\n         std::cout << \"Successfully set up a async IO handler to SIGUSR1 action\" << std::endl;\n         return true;\n    }\n    ```\n\n6.  定义名为`fillAiocb`的帮助函数，用给定的参数填充`aiocb`结构。它将引用 aiocb 结构、文件描述符、缓冲区指针和缓冲区大小作为参数。在`SIGUSR1`中设置`sigev_signo`，我们之前已经初始化过:\n\n    ```cpp\n    void \n    fillAiocb(aiocb& item, const int& fileDescriptor,\n              char* buffer, const int& bufSize)\n    {\n         item.aio_fildes = fileDescriptor;\n         item.aio_buf = static_cast<void*>(buffer);\n         item.aio_nbytes = bufSize;\n         item.aio_reqprio = 0;\n         item.aio_offset = 0;\n         item.aio_sigevent.sigev_notify = SIGEV_SIGNAL;\n         item.aio_sigevent.sigev_signo = SIGUSR1;\n         item.aio_sigevent.sigev_value.sival_ptr = &item;\n    }\n    ```\n\n7.  进入`主`功能。定义名为`buf_size`的变量，它保存缓冲区大小。创建一个这样大小的缓冲区:\n\n    ```cpp\n    constexpr int bufSize = 100;\n    char* buffer = new char(bufSize);\n    if (!buffer)\n    {\n         std::cerr << \"Failed to allocate buffer\" << std::endl;\n         return -1;\n    }\n    ```\n\n8.  创建一个名为`文件名`的变量，该变量保存一个名为“`Test.txt`的文件。然后，以只读方式打开该文件:\n\n    ```cpp\n    const std::string fileName(\"Test.txt\");\n    int descriptor = open(fileName.c_str(), O_RDONLY);\n    if (-1 == descriptor)\n    {\n         std::cerr << \"Failed to opene file for reading\" << std::endl;\n         return -1;\n    }\n    std::cout << \"Successfully opened file for reading\" << std::endl;\n    ```\n\n9.  创建一个`信号`结构，并使用`初始化信号`功能进行初始化:\n\n    ```cpp\n    struct sigaction sa;\n    if (!initSigAct(sa))\n    {\n         std::cerr << \"failed registering signal\" << std::endl;\n         return -1;\n    }\n    ```\n\n10.  创建一个`aiocb`结构，并使用`fillaocb`函数进行初始化:\n\n    ```cpp\n    aiocb aiocbObj;\n    fillAiocb(aiocbObj, descriptor, buffer, bufSize);\n    ```\n\n11.  使用`aio_read`功能执行`读取`操作:\n\n    ```cpp\n    if (-1 == aio_read(&aiocbObj))\n    {\n         std::cerr << \"aio_read failed\" << std::endl;\n    }\n    ```\n\n12.  接下来，在循环中，评估`isDone`变量。如果是假的，让线程休眠`3 毫秒`。通过这样做，我们将等待输入/输出操作完成:\n\n    ```cpp\n    while (!isDone)\n    {\n         using namespace std::chrono_literals;\n         std::this_thread::sleep_for(3ms);\n    }\n    std::cout << \"Successfully finished read operation. Buffer: \" << std::endl << buffer; \n    ```\n\n13.  Before running this exercise, create a `Test.txt` file in the project directory and write different symbols. For example, our file contains the following data:\n\n    ```cpp\n    a1a\"1 a1\\a1 a1\ta1 \n    a1a\"1 a1\\a1 a1\ta1 \n    a1a\"1 a1\\a1 a1\ta1 \n    a1a\"1 a1\\a1 a1\ta1 \n    a1a\"1 a1\\a1 a1\ta1 \n    a1a\"1 a1\\a1 a1\ta1 \n    a1a\"1 a1\\a1 a1\ta1 \n    a1a\"1 a1\\a1 a1\ta1 \n    a1a\"1 a1\\a1 a1\ta1 \n    a1a\"1 a1\\a1 a1\ta1 \n    a1a\"1 a1\\a1 a1\ta1 \n    a1a\"1 a1\\a1 a1\ta1 \n    a1a\"1 a1\\a1 a1\ta1 \n    a1a\"1 a1\\a1 a1\ta1\n    ```\n\n    这里有字母字符、数字字符、特殊符号、空格、制表字符和换行符。\n\n14.  现在，在您的集成开发环境中构建并运行这个练习。您的输出将类似于以下内容:\n\n![](img/C14583_06_17.jpg)\n\n###### 图 6.17:执行练习 8 的结果\n\n您可以看到该文件已成功打开进行读取，并且我们已成功为其设置了`SIGUSR1`信号和处理程序。然后，我们收到了 30 号信号，也就是`SI_ASYNCIO`信号。最后，我们可以输出我们所阅读的内容，并将其与文件内容进行比较。通过这样做，我们可以确保所有数据都被正确读取。\n\n这就是 Linux 系统中异步输入/输出的全部内容。\n\n#### 注意\n\n你可以通过 Linux 的手册页找到更多关于 Linux 中异步 IO 的信息:[http://man7.org/linux/man-pages/man7/aio.7.html](7.html)。\n\n现在，让我们了解一下我们可以为跨平台应用使用什么。\n\n### 异步跨平台输入输出库\n\n我们已经考虑了异步 I/O 的特定于平台的决策。现在，要编写一个跨平台的应用，您可以使用这些特定于平台的方法，并将其与预处理器指令一起使用；例如:\n\n```cpp\n#ifdef WIN\n#include <WinAIO.hpp>\n#else\n#include <LinAIO.hpp>\n#endif\n```\n\n在这两个头中，您可以为特定于平台的实现声明相同的接口。您还可以实现自己的 AIO 库，它将在单独的线程中使用一些状态机或队列。此外，您可以使用一些实现必要功能的免费库。最受欢迎的库是`Boost。Asio`。它为异步工作提供了许多接口，例如:\n\n*   无线程并发\n*   线\n*   缓冲\n*   流\n*   协同程序\n*   TCP、UDP 和 ICMP\n*   套接字\n*   加密套接字协议层\n*   倍\n*   串行端口\n\n让我们简单考虑一下它的输入输出操作接口。我们可以使用`Asio`库的接口进行同步和异步操作。所有的输入输出操作都从`io_service`类开始，该类提供核心的输入输出功能。在`<boost/asio/io _ service . HPP>`头文件中声明。同步输入/输出调用`io_service`对象的`run()`函数进行单个操作，该操作阻塞调用线程，直到作业完成。异步输入输出使用`run()`、`run_one()`、`poll()`和`poll_one()`功能。`run()`函数运行事件循环来处理请求处理程序。`run_one()`函数也是如此，但是事件循环应该只处理一个处理程序。`poll()`函数运行事件循环来执行所有就绪的处理程序。`poll_one()`做同样的事情，但只针对一个处理程序。以下示例演示了所有这些函数的用法:\n\n```cpp\nboost::asio::io_service io_service1;\nio_service1.run();\nboost::asio::io_service io_service2;\nio_service2.run_one();\nboost::asio::io_service io_service3;\nio_service3.poll();\nboost::asio::io_service io_service4;\nio_service4.poll_one();\n```\n\n有可能在调用实际的输入/输出操作之前运行事件处理程序。使用带有`io_service`类的工作类在代码中实现这个特性。工作类保证运行函数不会返回，直到您决定以后不再有任何输入/输出操作。例如，您可以使工作类成为另一个类的成员，并将其从析构函数中移除。因此，在您的课程期间，`io_service`将运行:\n\n```cpp\nboost::asio::io_service io_service1;\nboost::asio::io_service::work work(io_service1);\nio_service1.run();\nboost::asio::io_service io_service2;\nboost::asio::io_service::work work(io_service2);\nio_service2.poll();\n```\n\n接下来，要执行任何输入/输出操作，我们需要输入/输出设备的确切位置，例如，文件、套接字等。实现工作的类有很多，使用不同的 I/O 设备，例如`<boost/asio/IP/TCP . HPP>`头中的`boost::asio::IP::TCP::socket`。接下来，为了读写套接字，我们使用`boost::asio::async_read`和`boost::asio::async_write`。它们以一个套接字、`boost::asio::buffer`、回调函数为参数。当异步操作被执行时，回调函数被调用。我们可以将 lambda 函数作为回调函数传递，或者使用 boost::bind 函数绑定现有函数。`boost::bind`创建一个可调用对象。以下示例演示了如何使用`Boost::Asio`写入套接字:\n\n```cpp\nboost::asio::io_service ioService;\ntcp::socket socket;\nint length = 15;\nchar* msg = new char(length);\nmsg = \"Hello, world!\";\nauto postHandler = [=]()\n{\n     auto writeHandler = [=](boost::system::error_code ec, std::size_t length)\n     {\n          if (ec)\n          {\n               socket_.close();\n          }\n          else\n          {\n               // wrote length characters\n          }\n     };\n     boost::asio::async_write(socket, boost::asio::buffer(msg, length), writeHandler);\n};\nioService.post(postHandler);\n```\n\n这里，我们使用 lambda 函数作为异步 I/O 操作的回调。\n\n#### 注意\n\n`升压。Asio`在[https://www . boost . org/doc/libs/1 _ 63 _ 0/doc/html/boost _ asio . html](7.html)上有详细记录。有很多不同输入输出设备和不同方法的例子。如果您决定使用`Boost，可以参考本文档。Asio`在你的项目中。\n\n在这里，我们考虑了实现异步输入/输出操作的不同方法。根据您的需求、环境和允许的实用程序，您可以选择适当的方式在应用中实现异步输入/输出。请记住，如果您选择执行许多快速输入/输出操作，最好以同步方式进行，因为它不会占用大量系统资源。既然我们知道了如何利用异步输入/输出，那么让我们学习如何在多线程应用中使用输入/输出。\n\n### 线程和输入输出的交互\n\n输入/输出标准库不是线程安全的。在标准库的文档中，我们可以找到一种解释，说明对流或流缓冲区的并发访问会导致数据竞争，从而导致未定义的行为。为了避免这种情况，我们应该使用我们在*第 5 章*、*哲学家的晚餐-线程和并发*中学习的技术来同步对流和缓冲区的访问。\n\n让我们稍微谈谈`std::cin`和`std::cout`对象。对它们的每个调用都是线程安全的，但是让我们考虑以下示例:\n\n```cpp\nstd::cout << \"Counter: \" << counter << std::endl;\n```\n\n在这一行中，我们看到`std::cout`被调用了一次，但是对轮班操作员的每次调用实际上是对`std::cout`对象的不同调用。因此，我们可以将这一行改写如下:\n\n```cpp\nstd::cout << \"Counter: \";\nstd::cout << counter;\nstd::cout << std::endl;\n```\n\n这段代码和前面的单行代码做得完全一样，也就是说，如果你从不同的线程调用这个单行代码，你的输出将是混合的，不清晰的。您可以对其进行修改，使其真正成为线程安全的，如下所示:\n\n```cpp\nstd::stringsream ss;\nss << \"Counter: \" << counter << std::endl;\nstd::cout << ss.str();\n```\n\n因此，如果您使用第二种方法输出到终端，您的输出将是清晰的和线程安全的。这种行为可能会有所不同，具体取决于编译器或标准库版本。你也要知道`std::cout`和`std::cin`在其中是同步的。这意味着调用`std::cout`总是刷新`std::cin`流，调用`std::cin`总是刷新`std::cout`流。\n\n最好的方法是将所有输入/输出操作包装在一个保护类中，该类将使用互斥体控制对流的访问。如果您需要使用`std::cout`从多个线程输出到终端，您可以实现一个非常简单的类，它除了锁定互斥体和调用`std::cout`之外什么也不做。让我们完成一个练习并创建这样的类。\n\n### 练习 9:为 std::cout 开发线程安全包装器\n\n在本练习中，我们将开发一个简单的`std::cout`包装器，它产生线程安全的输出。我们将编写一个小测试函数来检查它是如何工作的。让我们开始并执行以下步骤:\n\n1.  Include all the required headers:\n\n    ```cpp\n    #include <iostream> // for std::cout\n    #include <thread>   // for std::thread\n    #include <mutex>    // for std::mutex\n    #include <sstream>  // for std::ostringstream\n    ```\n\n    现在，让我们想想我们的包装。我们可以在某个地方创建这个类的变量，并将其传递给每个创建的线程。然而，这是一个糟糕的决定，因为在复杂的应用中，这将需要大量的努力。我们也可以作为一个单独的个体这样做，这样我们就可以从任何地方访问它。接下来，我们要思考我们课程的内容。实际上，我们可以使用我们在*练习 7* 、*继承标准流对象*中创建的类。在那个练习中，我们重载了`std::basic_streambuf`和`std::basic_ostream`，并将`std::cout`设置为输出设备。我们可以给重载函数添加一个互斥体，并按原样使用它。请注意，我们不需要任何额外的逻辑——只需要使用`std::cout`的输出数据。为此，我们可以创建一个更简单的类。如果我们没有设置输出设备，应用左移位操作符将不会生效，并将待输出的数据存储在内部缓冲区中。太好了。现在，我们需要考虑如何使用`std::cout`将这个缓冲区输出。\n\n2.  实现诸如`write()`这样的函数，该函数将锁定一个互斥体，并从内部缓冲区输出到`std::cout`。该功能的用法如下:\n\n    ```cpp\n    mtcout cout;\n    cout << msg << std::endl;\n    cout.write();\n    ```\n\n3.  我们有一个函数总是会被自动调用，我们可以把 write 函数的代码放进去。这是一个析构器。在这种情况下，我们把创造和毁灭结合成一条线。这样一个对象的用法如下:\n\n    ```cpp\n    mtcout{} << msg << std::endl; \n    ```\n\n4.  现在，让我们定义我们的`mtcout`(多线程 cout)类。它有一个公共默认构造函数。在私有部分，它有一个静态互斥变量。您可能还记得，静态变量在类的所有实例之间共享。在析构函数中，我们使用 cout 锁定互斥体和输出。在输出中添加一个前缀——当前线程的 ID 和一个空格字符:\n\n    ```cpp\n    class mtcout : public std::ostringstream\n    {\n    public:\n         mtcout() = default;\n         ~mtcout()\n         {\n         std::lock_guard<std::mutex> lock(m_mux);\n              std::cout << std::this_thread::get_id() << \" \" << this->str();\n         }\n    private:\n         static std::mutex m_mux;\n    };\n    ```\n\n5.  接下来，在类外声明`互斥`变量。我们这样做是因为我们必须在任何源文件中声明一个静态变量:\n\n    ```cpp\n    std::mutex mtcout::m_mux; \n    ```\n\n6.  进入主功能。创建一个名为`的函数`。它将测试我们的`mtcout`班。它以字符串为参数，使用`mtcout`在从`0`到`1000`的循环中输出该字符串。使用`std::cout`添加相同的输出并注释掉。比较两种情况下的输出:\n\n    ```cpp\n    auto func = [](const std::string msg)\n    {\n         using namespace std::chrono_literals;\n         for (int i = 0; i < 1000; ++ i)\n         {\n              mtcout{} << msg << std::endl;\n    //          std::cout << std::this_thread::get_id() << \" \" << msg << std::endl;\n         }\n    };\n    ```\n\n7.  创建四个线程，并传递一个 lambda 函数作为参数。向每个线程传递不同的字符串。最后，连接所有四个线程:\n\n    ```cpp\n    std::thread thr1(func, \"111111111\");\n    std::thread thr2(func, \"222222222\");\n    std::thread thr3(func, \"333333333\");\n    std::thread thr4(func, \"444444444\");\n    thr1.join();\n    thr2.join();\n    thr3.join();\n    thr4.join();\n    ```\n\n8.  Build and run the exercise for the first time. You will get the following output:\n\n    ![Figure 6.18: The result of executing Exercise 9, part 1](img/C14583_06_18.jpg)\n\n    ###### 图 6.18:执行练习 9 第 1 部分的结果\n\n    在这里，我们可以看到每个线程都输出自己的消息。该消息没有被中断，输出看起来很清晰。\n\n9.  现在，用λ中的`std::cout`取消输出注释，并用`mtcout`注释输出。\n10.  Again, build and run the application. Now, you will get a \"dirty\", mixed output, like the following:\n\n    ![Figure 6.19: The result of executing Exercise 9, part 2](img/C14583_06_19.jpg)\n\n###### 图 6.19:执行练习 9 第 2 部分的结果\n\n您可以看到这种混合输出，因为我们不输出单个字符串；相反，我们调用`std::cout`四次:\n\n```cpp\nstd::cout << std::this_thread::get_id();\nstd::cout << \" \";\nstd::cout << msg;\nstd::cout << std::endl;\n```\n\n当然，我们可以在输出字符串之前对其进行格式化，但是使用 mtcout 类更方便，并且不必担心格式化问题。您可以为任何流创建类似的包装器，以便安全地执行输入/输出操作。您可以更改输出并添加任何附加信息，例如当前线程的 ID、时间或您需要的任何信息。利用我们在*第 5 章*、*哲学家的晚餐——线程和并发*中了解到的东西，同步输入/输出操作，扩展流，并使输出对您的需求更加有用。\n\n### 使用宏\n\n在本章的活动中，我们将使用宏定义来简化和美化我们的代码，所以让我们复习一下如何使用它们。宏定义是预处理器指令。宏定义的语法如下:\n\n```cpp\n#define [name] [expression]\n```\n\n这里，[name]是任何有意义的名称，[expression]是任何小函数或值。\n\n当预处理器面对宏名时，它用表达式替换它。例如，假设您有以下宏:\n\n```cpp\n#define MAX_NUMBER 15\n```\n\n然后，在代码中的一些地方使用它:\n\n```cpp\nif (val < MAX_NUMBER)\nwhile (val < MAX_NUMBER)\n```\n\n预处理器完成工作后，代码如下:\n\n```cpp\nif (val < 15)\nwhile (val < 15)\n```\n\n预处理器对函数做同样的工作。例如，假设您有一个用于获取最大数量的宏:\n\n```cpp\n#define max(a, b) a < b ? b : a\n```\n\n然后，在代码中的一些地方使用它:\n\n```cpp\n\nint res = max (5, 3);\n\nstd::cout << (max (a, b));\n```\n\n预处理器完成工作后，代码如下:\n\n```cpp\n\nint res = 5 < 3 ? 3 : 5;\n\nstd::cout << (a < b ? b : a);\n\n```\n\n作为表达式，您可以使用任何有效的表达式，如函数调用、内联函数、值等。如果需要在多行中编写表达式，请使用反斜杠运算符“\\”。例如，我们可以用两行写的最大定义如下:\n\n```cpp\n#define max(a, b) \\\na < b ? b : a\n```\n\n#### 注意\n\n宏定义来自 C 语言。最好使用常量变量或内联函数。然而，仍然有使用宏定义更方便的情况，例如，在记录器中，当您希望定义不同的日志记录级别时。\n\n现在。我们知道完成活动所需的一切。所以，让我们总结一下这一章所学的内容，让我们改进一下我们在*第 5 章**哲学家的晚餐——线程和并发*中所写的项目。我们将开发一个线程安全的记录器，并将其集成到我们的项目中。\n\n### 活动 1:美术馆模拟器的记录系统\n\n在本练习中，我们将开发一个记录器，将格式化的日志输出到终端。我们将以以下格式输出日志:\n\n```cpp\n[dateTtime][threadId][logLevel][file:line][function] | message\n```\n\n我们将为不同的日志记录级别实现宏定义，而不是直接调用。这个记录器将是线程安全的，我们将从不同的线程同时调用它。最后，我们将把它整合到项目中——美术馆模拟器。我们将运行模拟并观察漂亮打印的日志。我们将创建一个额外的流，使用并发流，并格式化输出。我们将实现本章中所学的几乎所有内容。我们还将采用上一章中的同步技术。\n\n因此，在尝试本练习之前，请确保您已经完成了本章前面的所有练习。\n\n在实现这个应用之前，让我们描述一下我们的类。我们有以下新创建的类:\n\n![Figure 6.20: Descriptions of the classes that should be implemented](img/C14583_06_20.jpg)\n\n###### 图 6.20:应该实现的类的描述\n\n我们还在艺术画廊模拟器项目中实现了以下类:\n\n![](img/C14583_06_21.jpg)\n\n###### 图 6.21:美术馆模拟器项目中已经实现的类的表\n\n在开始实现之前，让我们将新的类添加到类图中。所有描述的具有关系的类都由下图组成:\n\n![Figure 6.22: The class diagram](img/C14583_06_22.jpg)\n\n###### 图 6.22:类图\n\n为了接收所需格式的输出，`记录器`类应该具有以下`静态`功能:\n\n![Figure 6.23: Descriptions of the LoggerUtils member functions](img/C14583_06_23.jpg)\n\n###### 图 6.23:记录器成员函数的描述\n\n按照以下步骤完成本活动:\n\n1.  定义并实现`记录器`类，它提供了一个输出格式化的接口。它包含将给定数据格式化为所需表示形式的静态变量。\n2.  定义并实现`StreamLogger`类，它为输出到终端提供了一个线程安全的接口。它应该像这样格式化输出:\n\n    ```cpp\n    [dateTtime][threadId][logLevel][file:line: ][function] | message\n    ```\n\n3.  在一个单独的头文件中，声明不同日志记录级别的宏定义，这些宏定义返回`StreamLogger`类的一个临时对象。\n4.  将实现的记录器集成到艺术画廊模拟器的类中。\n5.  用适当的宏定义调用替换`std::cout`的所有调用。\n\n在实现了上述步骤之后，您应该在终端上获得一些关于所有实现的类的日志的输出。看一看，确保日志以所需的格式输出。预期产出应如下:\n\n![Figure 6.24: The result of the application's execution](img/C14583_06_24.jpg)\n\n###### 图 6.24:应用执行的结果\n\n#### 注意\n\n这项活动的解决方案可以在第 696 页找到。\n\n## 总结\n\n在这一章中，我们学习了 C++ 中的输入输出操作。我们考虑了输入输出标准库，它为同步输入输出操作提供了一个接口。此外，我们考虑了异步输入/输出的平台相关本机工具和`增强。Asio`库，用于跨平台异步 I/O 操作。我们还学习了如何在多线程应用中使用输入/输出流。\n\n我们从标准库为输入/输出操作提供的基本特性开始。我们了解了预定义的流对象，例如`std::cin`和`std::cout`。在实践中，我们学习了如何使用标准流和重写移位运算符来轻松读写自定义数据类型。\n\n接下来，我们练习了如何创建附加流。我们继承了基本的流类，实现了自己的流缓冲类，并在练习中练习了它们的用法。我们了解了最适合继承的流类，并考虑了它们的优缺点。\n\n然后，我们考虑了在不同操作系统上异步输入/输出操作的方法。我们简要考虑了使用被称为`Boost 的跨平台 I/O 库。Asio`，提供同步和异步操作的接口。\n\n最后，我们学习了如何在多线程应用中执行输入/输出操作。我们通过构建多线程记录器将所有这些新技能付诸实践。我们创建了一个记录器抽象，并将其用于美术馆模拟器。因此，我们创建了一个简单、清晰和健壮的日志记录系统，允许我们使用日志轻松调试应用。总之，我们利用了本章所学的一切。\n\n在下一章中，我们将更仔细地研究测试和调试应用。我们将从学习断言和安全网开始。然后，我们将练习为接口编写单元测试和模拟。之后，我们将在 IDE 中练习调试应用:我们将使用断点、观察点和数据可视化。最后，我们将编写一个活动来掌握测试代码的技巧。"
  },
  {
    "path": "docs/adv-cpp/08.md",
    "content": "# 八、每个人都会跌倒，这是你爬起来的方式——测试和调试\n\n## 学习目标\n\n本章结束时，您将能够:\n\n*   描述不同类型的断言\n*   实现编译时和运行时断言\n*   实现异常处理\n*   描述和实现单元测试和模拟测试\n*   使用断点和观察点调试 C++ 代码\n*   在调试器中检查数据变量和 C++ 对象\n\n在本章中，您将学习如何适当地添加断言，添加单元测试用例以使代码按照需求运行，并学习调试技术以发现代码中的错误并跟踪其根本原因。\n\n## 简介\n\n在**软件开发生命周期** ( **SDLC** )中，一旦需求收集阶段完成，那么通常会进入设计和架构阶段，其中项目的高层流程被定义并分解成更小的模块组件。当一个项目中有许多团队成员时，每个团队成员都必须被明确分配到模块的特定部分，并且他们知道自己的需求。通过这种方式，他们可以在一个隔离的环境中独立编写自己的那部分代码，并确保它可以正常工作。一旦他们的部分工作完成，他们可以将他们的模块与其他开发人员的模块集成，并确保整个项目按照要求执行。\n\n这个概念也可以应用到小项目中，在小项目中，开发人员完全在处理一个需求，将它分解成更小的组件，在隔离的环境中开发组件，确保它按照计划执行，集成所有的小模块来完成项目，最后测试它以确保整个项目运行良好。\n\n当整个项目被集成和执行时，需要大量的测试。可能会有一个单独的团队(称为**质量保证**，即 **QA** )专门执行这项任务。与其在项目级别发现问题，不如在每个独立的阶段测试代码。该测试需要由负责该模块的开发人员来执行。这种类型的测试被称为单元测试。在这里，开发人员可以模拟运行该模块所需的环境，并确保他们测试模块中编写的功能的特定部分。例如，让我们假设，在一个大项目中，有一个小模块，其功能是解析配置文件并获取设置环境所需的参数。如果解析文件的部分代码将一个`IP 地址`作为`字符串`，那么开发人员需要确保它的格式为`XXX。XXX.XXX.XXX`，其中`X`为`0` - `9`的数字。字符串的长度必须是有限的。\n\n在这里，开发人员可以创建一个测试程序来执行他们的那部分代码:解析文件，将`IP 地址`提取为字符串，并测试它的格式是否正确。同样，如果配置有其他参数需要解析，并且它们需要采用特定的格式，例如`用户标识` / `密码`，日志或挂载点的文件位置等等，那么所有这些都将是该模块单元测试的一部分。在本章中，我们将解释诸如`断言`、`安全嵌套` ( `异常处理`)、`单元测试`、`嘲讽`、`断点`、`观察点`、`数据可视化`等技术，以查明错误来源并限制其增长。在下一节中，我们将探讨断言技术。\n\n### 断言\n\n为上述场景使用测试条件将有助于项目以更好的方式开发，因为缺陷将在基础级别而不是在后期的质量保证级别被发现。可能存在这样的场景，即使在编写单元测试用例并成功执行代码之后，当应用崩溃、程序意外退出或行为不符合预期时，也可能会发现问题。为了克服这种情况，通常，开发人员使用调试模式二进制文件来重新创建问题。`断言`用于确保检查条件，否则程序执行终止。\n\n这样，问题就可以快速追踪到。此外，在`调试模式`下，开发人员可以逐行遍历程序的实际执行情况，并检查代码流是否如预期的那样，或者变量是否按预期设置并正确访问。有时，如果指针变量没有指向有效的内存位置，访问指针变量会导致意外的行为。\n\n在编写代码时，我们可以检查必要条件是否满足。如果没有，那么程序员可能不想进一步执行代码。使用断言可以很容易地做到这一点。一个`断言`是一个宏，其中特定条件被检查，如果它不满足标准，中止被调用(程序执行被停止)并且一个错误信息被打印为一个`标准错误`。这通常是一个**运行时断言**。也可以在编译时进行断言。这个我们以后再说。在下一节中，我们将解决一个练习，其中我们将编写和测试我们的第一个断言。\n\n### 练习 1:编写和测试我们的第一个断言\n\n在本练习中，我们将编写一个函数来解析一个`IP 地址`，并检查它是否有效。作为我们要求的一部分，`IP 地址`将作为字符串文字在`XXX 中传递。XXX.XXX.XXX`格式。在此格式中，`X`代表从`0` - `9`的一个数字。因此，作为检查正在解析的`字符串`是否正确的测试的一部分，我们需要确保`字符串`不为`空`，并且其`长度`小于`16`。按照以下步骤实施本练习:\n\n1.  创建一个名为**的新文件。**\n2.  Open the file and write the following code to include the header files:\n\n    ```cpp\n    #include<iostream>\n    #include<cassert>\n    #include<cstring>\n    using std::cout;\n    using std::endl;\n    ```\n\n    在前面的代码中，`#include < cassert >`表明我们需要在定义 assert 的地方包含`cassert`文件。\n\n3.  Create a function named `checkValidIp()` that will take the `IP address` as input and return a `true` value if the `IP address` meets our requirements. Write the following code to define the function:\n\n    ```cpp\n    bool checkValidIp(const char * ip){\n        assert(ip != NULL);\n        assert(strlen(ip) < 16);\n        cout << \"strlen: \" << strlen(ip) << endl;\n        return true;\n    }\n    ```\n\n    这里，`断言(ip！=空)`表示如果传递的“`ip`变量不是`空值`，断言宏用于检查条件。如果是`空`，那么它将中止并显示一条错误消息。此外，`assert(strlen(IP)<16)`显示该 assert 用于检查“`ip`”是否为`16`字符或更少。如果不是，则中止并显示`错误信息`。\n\n4.  Now, create a `main` function that passes a different string literal to our `checkValidIp()` function and makes sure it can be tested appropriately. Write the following code for the `main` function:\n\n    ```cpp\n    int main(){\n        const char * ip;\n        ip = NULL;\n        bool check = checkValidIp(ip);\n        cout << \" IP address is validated as :\" << (check ? \"true\" : \"false\") << endl;\n        return 0;\n    }\n    ```\n\n    在前面的代码中，我们特意将`NULL`传递给了`ip`变量，以确保`断言`被调用。\n\n5.  Open the `Command Prompt` and go to the location where the **AssertSample.cpp** file is stored. Compile it with the `g++ ` compiler by typing the following command:\n\n    ```cpp\n    g++ AssertSample.cpp\n    ```\n\n    利用该命令，生成`a.out`二进制文件。\n\n6.  Run the `a.out` binary file by typing the following command in the compiler:\n\n    ```cpp\n    ./a.out\n    ```\n\n    您将看到以下输出:\n\n    ![Figure 7.1: Running the Assertion binary on the Command Prompt](img/C14583_07_01.jpg)\n\n    ###### 图 7.1:在命令提示符下运行断言二进制文件\n\n    在前面的截图中，您可以看到三段用红色圈起来的代码。第一个突出显示的部分显示了**的编译。cpp** 文件。第二个突出显示的部分显示了由前面的编译生成的 **a.out** 二进制文件。第三个突出显示的部分显示了为传递的**空值**抛出错误的断言。它指示调用断言的行号和函数名。\n\n7.  Now, inside the `main` function, we will pass `ip` with a length greater than `16` and check if the `assert` is called here too. Write the following code to implement this:\n\n    ```cpp\n    ip = \"111.111.111.11111\";\n    ```\n\n    再次，打开编译器，编译**。cpp** 文件，并运行生成的二进制文件。编译器中将显示以下输出:\n\n    ![Figure 7.2: Running the Assertion binary on the Command Prompt](img/C14583_07_02.jpg)\n\n    ###### 图 7.2:在命令提示符下运行断言二进制文件\n\n    在前面的截图中，断言抛出了一个错误，因为传递的`ip`长度大于`16`。\n\n8.  Now, to satisfy the `assert` condition so that the binary runs fine, we need to update the value of `ip` inside the `main` function. Write the following code to do this:\n\n    ```cpp\n    ip = \"111.111.111.111\"; \n    ```\n\n    再次，打开编译器，编译**。cpp** 文件，并运行生成的二进制文件。编译器中会显示以下输出:\n\n    ![Figure 7.3: Running the Assertion binary on the Command Prompt](img/C14583_07_03.jpg)\n\n    ###### 图 7.3:在命令提示符下运行断言二进制文件\n\n    #### 注意\n\n    因为我们只是在这里处理`断言`，所以我们没有给我们的`checkValidIP()`函数添加任何额外的功能。然而，我们将在*异常处理*和*单元测试*部分使用相同的例子，其中我们将为我们的函数添加更多的功能。\n\n9.  如果我们不希望可执行文件由于生产或发布环境中的断言而中止，请从代码中删除`断言`宏调用。首先更新长度大于`16`的`ip`的值。将以下代码添加到文件中:\n\n    ```cpp\n    ip = \"111.111.111.11111\";\n    ```\n\n10.  Now, during compilation, pass the `-DNDEBUG` macro. This will make sure that the assert is not called in the binary. Write the following command to compile our `.cpp` file in the Terminal:\n\n    ```cpp\n    g++ -DNDEBUG AssertSample.cpp\n    ```\n\n    之后，当我们执行二进制文件时，会生成以下输出:\n\n    ![](img/C14583_07_04.jpg)\n\n###### 图 7.4:在命令提示符下运行断言二进制文件\n\n在上一个截图中，由于`断言`没有被调用，它将显示字符串长度为 **17** ，而**真**值为将被验证的 IP 地址。在本练习中，我们看到在执行二进制文件时调用了断言。在代码编译期间，我们也可以有一个断言。这是在 C++ 11 中引入的。它被称为**静态断言**，我们将在下一节中探讨它。\n\n### 静态断言\n\n有时，我们可以在编译时进行条件检查，以避免将来出现任何错误。例如，在一个项目中，我们可能会使用一个第三方库，其中声明了一些数据结构。这个结构信息，比如它的`大小`和`成员变量`，我们从它的头文件中就知道了。利用这些信息，我们可以正确地分配或释放内存，以及处理其成员变量。在具有不同版本的第三方库中，此结构属性可能会改变。然而，如果我们的项目代码仍然使用早期版本的结构，那么当我们使用它时，它会产生问题。运行二进制文件时，我们可能会在稍后阶段遇到错误。我们可以在编译时使用`静态断言`来捕捉这个错误。我们可以比较静态数据，比如库的版本号，从而确保我们的代码不会遇到任何问题。在下一节中，我们将基于此解决一个练习。\n\n### 练习 2:测试静态断言\n\n在本练习中，我们将通过执行`静态断言`来比较两个头文件的版本号。如果`版本号`小于`1`，则会抛出静态断言错误。执行以下步骤来实施本练习:\n\n1.  Create a header file named **PersonLibrary_ver1.h** and add the following code:\n\n    ```cpp\n    #ifndef __PERSON_H__\n    #define __PERSON_H__\n    #include<string>\n    using std::string;\n    #define PERSON_LIB_VERSION 1\n    struct person{\n        string name;\n        int age;\n        string address;\n    };\n    #endif\n    ```\n\n    在前面的代码中，结构人被定义并由以下属性组成:`姓名`、`年龄`和`地址`。它还有版本号`1`。\n\n2.  Create another header file named **PersonLibrary_ver2.h** and add the following code:\n\n    ```cpp\n    #ifndef __PERSON_H__\n    #define __PERSON_H__\n    #include<string>\n    using std::string;\n    #define PERSON_LIB_VERSION 2\n    struct person{\n        string name;\n        int age;\n        string address;\n        string Mobile_No;\n    };\n    #endif\n    ```\n\n    在前面的代码中，定义了`结构人`，由以下属性组成:`姓名`、`年龄`、`地址`、`手机号`。它还有`版本号 2`。现在，`版本 1`是老版本，`版本 2`是新版本。下面是并排的两个头文件的截图:\n\n    ![Figure 7.5: Library file with a different version](img/C14583_07_05.jpg)\n\n    ###### 图 7.5:不同版本的库文件\n\n3.  Create a file named **StaticAssertionSample.cpp** and add the following code:\n\n    ```cpp\n    #include<iostream>\n    #include\"PersonLibrary.h\"\n    void doSanityCheck(){\n        static_assert(PERSON_LIB_VERSION > 1 , \"PERSON LIBRARY VERSION not greater than 1\");\n        // Do any more sanity check before starting app ... \n    }\n    int main(){\n        doSanityCheck();\n        return 0;\n    }\n    ```\n\n    在前面的代码中，我们在构建和执行项目之前，对项目进行了健全性检查。我们已经创建了一个名为`Dosaniticheck()`的函数来执行库的版本检查。它使用静态断言完成，并在编译时执行。代码的第二行显示包含**personal library . h**文件。在`Dosaniticheck()`函数中，`static_assert()`函数检查该版本的库是否大于 1。\n\n    #### 注意\n\n    如果您的项目需要在库的`版本 2`或更高版本中定义的人员结构来正确执行它，我们需要匹配`版本 2`的文件，即`PERSON_LIB_VERSION`至少应该设置为`2`。如果开发人员获得了库的`版本 1`，并试图为项目创建一个二进制文件，这可能会在执行中产生问题。为了避免这种情况，在项目的主要代码中，我们在项目构建和执行之前对其进行了健全性检查。\n\n4.  To include `version 1` of the library in our **.cpp** file, open the terminal and write the following command:\n\n    ```cpp\n    ln -s PersonLibrary_ver1.h PersonLibrary.h\n    ```\n\n    前面的命令将创建一个名为**personal library . h**的**personal library _ ver 1 . h**文件的软链接。这就像用`版本 1`模拟我们使用**personal library . h**的环境。\n\n5.  Compile our **.cpp** file using the following command in the terminal:\n\n    ```cpp\n    g++ StaticAssertionSample.cpp\n    ```\n\n    以下是终端生成的输出:\n\n    ![Figure 7.6: Seeing a static error](img/C14583_07_06.jpg)\n\n    ###### 图 7.6:看到静态错误\n\n    在前面的截图中，三个区域用红色圈起来。第一个给出了创建软链接的命令。第二个命令显示了我们创建的**personal library . h**文件。第三个区域显示了由于库的版本不匹配而引发的`static_assert`错误。\n\n6.  Now, to compile the program correctly, remove the soft link of `ProgramLibrary` and create a new one pointing to `version2` and compile it again. This time, it will compile fine. Type the following commands into the terminal to remove a soft link:\n\n    ```cpp\n    rm PersonLibrary.h \n    ln -s PersonLibrary_ver2.h PersonLibrary.h\n    g++ StaticAssertionSample.cpp\n    ```\n\n    以下是相同内容的截图:\n\n![Figure 7.7: Static assertion compilation file](img/C14583_07_07.jpg)\n\n###### 图 7.7:静态断言编译文件\n\n如您所见，用红色标记的区域显示使用了正确版本的`personal library`，编译顺利进行。编译后，创建一个名为“**a.exe**的二进制文件。在本练习中，我们通过比较两个头文件的版本号来执行静态断言。在下一节中，我们将探讨异常处理的概念。\n\n### 了解异常处理\n\n正如我们之前在调试模式二进制中看到的，当某个条件不满足时，我们可以使用运行时断言来中止程序。但是在发布模式二进制或生产环境中，当客户端使用该产品时，突然中止程序并不是一个好主意。最好处理这种错误情况，并继续执行二进制文件的下一部分。\n\n最糟糕的情况发生在二进制文件需要退出的时候。它将通过添加正确的日志消息并清除为该进程分配的所有内存来优雅地完成这项工作。对于这种情况，使用异常处理。这里，当遇到错误情况时，执行转移到一个特殊的代码块。例外情况包括以下三个部分:\n\n*   **试块**:这里我们检查条件是否符合必要条件。\n*   **掷块**:如果条件不匹配，则抛出异常。\n*   **捕获块**:它捕获异常，并针对该错误条件执行必要的执行。\n\n在下一节中，我们将解决一个练习，其中我们将对代码执行异常处理。\n\n### 练习 3:执行异常处理\n\n在本练习中，我们将对我们的 **AssertSample.cpp** 代码执行异常处理。我们将用异常替换断言条件。执行以下步骤来实施本练习:\n\n1.  创建一个名为`ExceptionSample.cpp`的文件。\n2.  添加以下代码添加头文件:\n\n    ```cpp\n    #include<iostream>\n    #include<cstring>\n    using std::cout;\n    using std::endl; \n    ```\n\n3.  Create a `checkValidIp()` function wherein we have a try-catch block. If the condition present in the try block is not satisfied, an exception will be thrown and the message in the catch block will be printed. Add the following code to accomplish this:\n\n    ```cpp\n    bool checkValidIp(const char * ip){\n        try{\n            if(ip == NULL)\n                throw (\"ip is NULL\");\n            if(strlen(ip) > 15)\n                throw int(strlen(ip));\n        }\n        catch(const char * str){\n            cout << \"Error in checkValidIp :\"<< str << endl;\n            return false;\n        }\n        catch(int len){\n            cout << \"Error in checkValidIp, ip len:\" << len <<\" greater than 15 characters, condition fail\" << endl;\n            return false;\n        }\n        cout << \"strlen: \" << strlen(ip) << endl;\n        return true;\n    }\n    ```\n\n    在前面的代码中，您可以看到检查条件的 try 块。在 try 块中，如果`ip`为`空`，那么它将抛出(`const char *`)类型的异常。在下一种情况下，如果`ip`大于`15`，则抛出 int 参数类型的异常。该投掷由具有匹配参数(`int`或`const char *`)的正确接球完成。两个异常都返回`假`，并显示一些错误信息。或者，在`catch`块中，如果需要任何清理，您可以执行额外的步骤，或者使用异常中用于比较的变量的默认值。\n\n    #### 注意\n\n    存在默认异常；例如，如果有一个嵌套函数用不同的参数抛出了一个错误，那么它可以作为一个更高级别的函数用 catch(…)这样的参数来捕获。同样，在泛型 catch 中，您可以为异常处理创建默认行为。\n\n4.  创建`main()`函数，并在其中编写以下代码:\n\n    ```cpp\n    int main(){\n        const char * ip;\n        ip = NULL;\n        if (checkValidIp(ip)) \n            cout << \"IP address is correctly validated\" << endl;\n        else {\n            /// work on error condition \n            // if needed exit program gracefully.\n            return -1;\n        }\n        return 0;\n    }\n    ```\n\n5.  Open the terminal, compile our file, and run the binary. You will see the following output:\n\n    ![Figure 7.8: Example execute code with exception handling](img/C14583_07_08.jpg)\n\n    ###### 图 7.8:带有异常处理的示例执行代码\n\n    前面的例子抛出了`ip`为`空`的异常，优雅的退出。\n\n6.  现在，通过提供超过`15`个字符，修改`主`功能中`ip`的值。为此，编写以下代码:\n\n    ```cpp\n    ip = \"111.111.111.11111\";\n    ```\n\n7.  Open the terminal, compile our file, and run the binary. You will see the following output:\n\n    ![Figure 7.9: Another example of exception handling](img/C14583_07_09.jpg)\n\n    ###### 图 7.9:异常处理的另一个例子\n\n    对于 **ip 字符串**，它抛出一个长度不匹配的**错误。**\n\n8.  再次修改`主`功能中`ip`的值，少于`15`个字符。为此，编写以下代码:\n\n    ```cpp\n    ip = \"111.111.111.111\";\n    ```\n\n9.  打开终端，编译我们的文件，运行二进制。您将看到以下输出:\n\n![Figure 7.10: The binary runs fine without throwing an exception](img/C14583_07_10.jpg)\n\n###### 图 7.10:二进制运行良好，没有抛出异常\n\n从前面的截图可以看出，二进制文件执行正常，没有任何异常。既然您已经理解了如何处理异常，在下一节中，我们将探讨`单元测试`和`模拟测试`的概念。\n\n## 单元测试和模拟测试\n\n当开发人员开始编写代码时，他们需要确保代码在单元级别得到正确测试。可能会发生边界条件丢失的情况，代码在客户端运行时可能会中断。为了避免这种情况，一般来说，对代码进行`单元测试`是个好主意。`单元测试`是在代码的单元级或基础级执行的测试，开发人员可以在隔离的环境中测试他们的代码，假设运行代码的某个特性所需的设置已经完成。一般来说，将模块分解成小函数并分别测试每个函数是一种很好的做法。\n\n例如，假设部分功能是读取配置文件，并使用配置文件中的参数设置环境。我们可以创建一个专门的函数来编写这个功能。因此，为了测试这个函数，我们可以创建一组单元测试用例，检查各种可能失败或行为不正确的组合。一旦确定了这些测试用例，开发人员就可以编写代码来覆盖功能，并确保它通过所有的单元测试用例。作为开发的一部分，这是一个很好的实践，您可以继续首先添加测试用例，并相应地添加代码，然后运行该函数的所有测试用例，并确保它们的行为适当。\n\n有很多工具可以用来为项目编写和集成单元测试用例。几个是 **cppunit** 、 **Google Test** 、**微软单元测试框架**、 **catch** 。为了我们例子的目的，我们将致力于`谷歌测试框架`。它是免费提供的，可以与项目集成。它使用**xuit 测试框架**，并且有一个断言集合，可以用来测试测试用例的条件。在下一节中，我们将解决一个练习，其中我们将创建我们的第一个单元测试用例。\n\n### 练习 4:创建我们的第一个单元测试用例\n\n在本练习中，我们将处理上一节中讨论的相同场景，其中开发人员负责编写一个函数来解析`配置文件`。配置文件中传递了不同的有效参数，如`产品可执行名称`、`版本号`、`数据库连接信息`、`IP 地址`连接服务器等等。假设开发人员将在一个单独的函数中分解解析文件、设置和测试单个属性的参数的所有功能。在我们的案例中，我们假设开发人员正在编写功能，他们已经将`IP 地址`解析为`字符串`，并想推断`字符串`是否是有效的`IP 地址`。目前，匹配`IP 地址`有效的标准需要满足以下条件:\n\n*   `字符串`不应为空。\n*   `字符串`包含的字符不得超过`16`\n*   `弦`应该在`XXX。XXX.XXX.XXX`格式，其中`X`必须是`0` - `9`之间的数字。\n\n执行以下步骤来实施本练习:\n\n1.  Create the **CheckIp.h** header file and write the following code inside it:\n\n    ```cpp\n    #ifndef  _CHECK_IP_H_\n    #define _CHECK_IP_H_\n    include <iostream>\n    include <cstring>\n    using namespace std;\n    bool checkValidIp(const char *);\n    #endif\n    ```\n\n    在前面的代码中，我们编写了一个名为`checkValidIp()`的函数来检查`IP 地址`是否有效。同样，为了理解`谷歌单元测试`，我们将编写最少的代码来理解这个特性。\n\n2.  Create a **CheckIp.cpp** file and write the following code, wherein we'll check if `ip` is `not NULL` and that the length is less than `16`:\n\n    ```cpp\n    #include \"CheckIp.h\"\n    #include<string>\n    #include<sstream>\n    bool checkValidIp(const char * ip){\n        if(ip == NULL){\n            cout << \"Error : IP passes is NULL \" << endl;\n            return false;\n        }\n        if(strlen(ip) > 15){\n            cout << \"Error: IP size is greater than 15\" << endl;\n            return false;\n        }\n        cout << \"strlen: \" << strlen(ip) << endl;\n        return true;\n    } \n    ```\n\n    在前面的代码中，如果两个条件都失败，函数返回`false`。\n\n3.  调用`checkValidIp()`函数创建一个名为 **MainIp.cpp** 的新文件。这个文件，一般来说，将包含项目的主要流程，但是为了我们练习的目的，我们只是调用我们的`checkValidIP()`函数。在里面添加以下代码:\n\n    ```cpp\n    #include\"CheckIp.h\"\n    int main(){\n        const char * ip;\n        //ip = \"111.111.111.111\";\n        ip = \"111.111.111.11111\";\n        if (checkValidIp(ip)) \n            cout << \"IP address is correctly validated\" << endl;\n        else {\n            /// work on error condition \n            // if needed exit program gracefully.\n            cout << \" Got error in valid ip \" << endl;\n            return -1;\n        }\n        return 0;\n    } \n    ```\n\n4.  To create test code, we'll create our first **.cpp** file, that is, **TestCases.cpp**. This will contain test cases for our `checkValidIp` function. Write the following code inside it:\n\n    ```cpp\n    #include\"CheckIp.h\"\n    #include<gtest/gtest.h>\n    using namespace std;\n    const char * testIp;\n    TEST(CheckIp, testNull){\n        testIp=NULL;\n        ASSERT_FALSE(checkValidIp(testIp));\n    }\n    TEST(CheckIp, BadLength){\n        testIp = \"232.13.1231.1321.123\";\n        ASSERT_FALSE(checkValidIp(testIp));\n    }\n    ```\n\n    在前面代码的第二行，我们包含了 **gtest.h** 文件。我们还使用 **TEST** 函数调用测试用例，该函数接受两个参数:第一个是 **testsuite** 名称，第二个是 **testcase** 名称。对于我们的案例，我们已经创建了**测试套件** **检查点**。在**测试**块中，您将看到我们有**谷歌测试**定义了一个名为 **ASSERT_FALSE** 的**断言，它将检查条件是否为 **false** 。如果不是，它将通过测试用例，并在结果中显示相同的内容。**\n\n    #### 注意\n\n    一般来说，对于一个`谷歌测试`用例和测试套件，你可以将它们分组在一个公共的名称空间中，并调用`RUN_ALL_TESTS`宏，该宏运行所有附加到测试二进制文件的测试用例。对于每个测试用例，它调用`SetUp`函数进行初始化(像类中的构造函数)，然后调用实际的测试用例，最后调用`拆卸`函数(像类中的析构函数)。没有必要编写`SetUp`和`拆卸`功能，除非你必须为测试用例初始化一些东西。\n\n5.  Now, to run the test cases, we will create the main **.cpp** file for the test cases and call the `RUN_ALL_TESTS` macro. Alternatively, we can create an executable by linking the `Google Test library` that invokes `RUN_ALL_TESTS`. For our case, we will do the latter. Open the terminal and run the following command to create a test run binary:\n\n    ```cpp\n    g++ -c CheckIp.cpp\n    ```\n\n    这将包括 **CheckIp.cpp** 的对象文件，因为其中定义了`CheckValidIp`函数。\n\n6.  现在，键入以下命令添加创建二进制文件所需的库:\n\n    ```cpp\n    g++ CheckIp.o TestCases.cpp -lgtest -lgtest_main -pthread -o TestRun \n    ```\n\n7.  Now, run the binary with the following command:\n\n    ```cpp\n    ./TestRun\n    ```\n\n    这显示了通过**检查点**和**测试套件**的两个测试用例。第一个测试用例**被调用并且通过。第二个测试用例， **CheckIp。BadLength** ，被称为，它也通过。这个结果可以在下面的截图中看到:**\n\n    ![Figure 7.11: Compiling and executing test cases](img/C14583_07_11.jpg)\n\n    ###### 图 7.11:编译和执行测试用例\n\n    #### 注意\n\n    在`谷歌测试`中，我们也可以使用其他断言，但是对于我们的测试用例，我们可以使用`ASSERT_FALSE`，因为我们只检查我们通过的 IP 地址的假条件。\n\n8.  Now, we will add more test cases to make our code robust. This is generally good practice for writing code. First, create the test cases and make sure the code runs fine for new test cases and old test cases along with the correct functionality of the code. To add more test cases, add the following code to the **TestCases.cpp** file:\n\n    ```cpp\n    TEST(CheckIp, WrongTokenCount){\n        testIp = \"22.13111.11\";\n        ASSERT_FALSE(checkValidIp(testIp));\n    }\n    TEST(CheckIp, WrongTokenEmpty){\n        testIp = \"22.131..11\";\n        ASSERT_FALSE(checkValidIp(testIp));\n    } \n    TEST(CheckIp, WrongTokenStart){\n        testIp = \".2.1.31.11\";\n        ASSERT_FALSE(checkValidIp(testIp));\n    } \n    TEST(CheckIp, WrongTokenEnd){\n        testIp = \"2.13.11.1.\";\n        ASSERT_FALSE(checkValidIp(testIp));\n    } \n    TEST(CheckIp, SpaceToken){\n        testIp = \"2.13.11\\. 1\";\n        ASSERT_FALSE(checkValidIp(testIp));\n    } \n    TEST(CheckIp, NonDigit){\n        testIp = \"2.13.b1.A1\";\n        ASSERT_FALSE(checkValidIp(testIp));\n    } \n    TEST(CheckIp, NonValidDigit){\n        testIp = \"2.13.521.61\";\n        ASSERT_FALSE(checkValidIp(testIp));\n    } \n    TEST(CheckIp, CorrectIp){\n        testIp = \"232.13.123.1\";\n        ASSERT_FALSE(checkValidIp(testIp));\n    } \n    ```\n\n    在前面的代码中，第一种和第二种情况应该会因不正确的令牌而失败。如果`IP`以“”开头，第三种情况应该会失败。如果`IP`以“.”结尾，第四种情况应该会失败。如果`IP`有中间空间，第五种情况应该会失败。如果`IP`包含任何非数字字符，第六种情况应该会失败。如果`IP`的令牌值小于`0`且大于`255`，则第七种情况应该会失败。如果`IP`令牌计数错误，最后一种情况应该会失败。\n\n9.  现在，在 **CheckIp.cpp** 文件的`CheckValidIp()`函数中添加以下代码。处理新的测试用例需要这个代码:\n\n    ```cpp\n    if(ip[strlen(ip)-1] == '.'){\n        cout<<\"ERROR : Incorrect token at end\"<<endl;\n        return false;\n    }\n    isstringstream istrstr(ip);\n    vector<string> tokens;\n    string token;\n    regex expression(\"[^0-9]\");\n    smatch m;\n    while(getline(istrstr, token, '.')){\n        if(token.empty()){\n            cout<<\"ERROR : Got empty token\"<<endl;\n            return false;\n        }\n        if(token.find(' ') != string::npos){\n            cout<<\"ERROR : Space character in token\"<<endl;\n            return false;\n        }\n        if(regex_search(token,m,expression)){\n            cout<<\"ERROR : NonDigit character in token\"<<endl;\n            return false;\n        }\n        int val = atoi(token.c_str());\n        if(val<0 || val>255){\n            cout<<\"ERROR : Invalid digit in token\"<<endl;\n            return false;\n        }\n        tokens.push_back(token);\n    }\n    if(tokens.size()!=4){\n        cout<<\"ERROR : Incorrect IP tokens used\"<<endl;\n        return false;\n    }\n    cout<<\"strlen: \"<<strlen(ip)<<endl;\n    return true;\n    }\n    ```\n\n10.  Open the terminal and write the following command to run the binary file:\n\n    ```cpp\n    ./TestRun\n    ```\n\n    所有的测试用例都已经执行，如下图所示:\n\n![Figure 7.12: Output of test cases run](img/C14583_07_12.jpg)\n\n###### 图 7.12:测试用例运行的输出\n\n前面的截图显示在`CheckIp`测试套件中有`10 个`测试用例，并且所有的测试用例运行良好。在下一节中，我们将学习使用模拟对象的单元测试。\n\n### 使用模拟对象的单元测试\n\n当开发人员进行单元测试时，可能会出现在具体操作发生后调用某些接口的情况。例如，正如我们在前面的场景中所讨论的，让我们假设项目的设计方式是，在执行之前，它从数据库中获取所有的配置信息。它查询数据库以获得具体参数，例如，web 服务器的`IP 地址`、`用户`、`密码`。然后，它尝试连接到 web 服务器(可能还有另一个模块处理与网络相关的任务)，或者开始处理实际项目所需的项目。之前，我们致力于测试 IP 地址的有效性。现在，我们将更进一步。让我们假设 IP 地址是从数据库中获取的，并且我们有一个实用程序类来处理连接到`数据库`和查询`IP 地址`。\n\n现在，为了测试 IP 地址的有效性，我们需要假设数据库连接已经建立。这意味着应用可以正确查询数据库并获得查询结果，其中一个是`IP 地址`。只有这样，我们才能测试 IP 地址的有效性。现在，为了执行这样的测试，我们必须假设所有必要的活动已经完成，并且我们已经获得了一个`IP 地址`来进行测试。模拟对象来了，它的行为类似于真实对象。它为单元测试提供了便利，因此应用会有这样的印象，即 IP 地址已经从数据库中获取，但实际上，我们已经模仿了它。要创建一个模拟对象，我们需要从它需要模仿的类中继承它。在下一节中，我们将通过一个练习来更好地理解模拟对象。\n\n### 练习 5:创建模拟对象\n\n在本练习中，我们将通过假设所有接口都按预期工作来创建模拟对象。使用这些对象，我们将测试一些功能，例如验证`IP 地址`，检查数据库连接，以及检查`用户名`和`密码`的格式是否正确。一旦所有测试通过，我们将确认申请，并为`QA`做好准备。执行以下步骤来实施本练习:\n\n1.  创建一个名为 **Misc.h** 的头文件，并包含必要的库:\n\n    ```cpp\n    #include<iostream>\n    #include<string>\n    #include<sstream>\n    #include<vector>\n    #include<iterator>\n    #include<regex>\n    using namespace std;\n    ```\n\n2.  创建一个名为`的连接数据库`的类，它将连接到数据库并返回查询结果。在类中，声明`数据库名`、用户和密码变量。另外，声明一个构造函数和两个虚函数。这两个虚函数中，第一个必须是析构函数，第二个必须是`getResult()`函数，从数据库返回查询结果。添加以下代码来实现:\n\n    ```cpp\n    class ConnectDatabase{\n        string DBname;\n        string user;\n        string passwd;\n        public:\n            ConnectDatabase() {} \n            ConnectDatabase(string _dbname, string _uname, string _passwd) :\n                DBname(_dbname), user(_uname), passwd(_passwd) { }\n            virtual ~ConnectDatabase() {} \n            virtual string getResult(string query);\n    };\n    ```\n\n3.  Create another class named `WebServerConnect`. Declare three `string` variables inside the `class`, namely `Webserver`, `uname`, and `passwd`. Create constructor and two virtual functions. Out of these two virtual functions, the first one must be a destructor, and the second one must be the `getRequest()` function. Add the following code to implement this:\n\n    ```cpp\n    class WebServerConnect{\n        string Webserver;\n        string uname;\n        string passwd;\n        public :\n        WebServerConnect(string _sname, string _uname, string _passwd) :\n                Webserver(_sname), uname(_uname), passwd(_passwd) { }\n            virtual ~WebServerConnect() {}\n            virtual string getRequest(string req);\n    };\n    ```\n\n    #### 注意\n\n    `虚拟函数`是必需的，因为我们要从前一个类创建一个`模拟类`并调用这些函数。\n\n4.  Create a class named `App`. Create the constructors, and destructors and call all the functions. Add the following code to implement this:\n\n    ```cpp\n    class App {\n        ConnectDatabase *DB;\n        WebServerConnect *WB;\n        public : \n            App():DB(NULL), WB(NULL) {} \n            ~App() { \n                if ( DB )  delete DB;\n                if ( WB )  delete WB;\n            }\n            bool checkValidIp(string ip);\n            string getDBResult(string query);\n            string getWebResult(string query);\n            void connectDB(string, string, string);\n            void connectDB(ConnectDatabase *db);\n            void connectWeb(string, string, string);\n            void run();\n    };\n    ```\n\n    在前面的代码中，app 会先查询数据库，得到`的 IP 地址`。然后，它用必要的信息连接到 web 服务器，并查询它以获得所需的信息。\n\n5.  Create a class named **MockMisc.h** and add the following code:\n\n    ```cpp\n    #include\"Misc.h\"\n    #include<gtest/gtest.h>\n    #include<gmock/gmock.h>\n    class MockDB : public ConnectDatabase {\n        public :\n            MockDB() {}\n            virtual ~MockDB(){}\n            MOCK_METHOD1(getResult, string( string) ); \n    };\n    ```\n\n    在前面的代码中，您可以看到我们已经包含了`gmock`头文件，这是创建一个模拟类所需要的。此外，`MockDB`类继承自`ConnectDatabase`类。`MOCK_METHOD1(getResult，string(字符串))；`行表示我们要模拟`获取结果`界面。因此，在单元测试过程中，我们可以直接用想要的结果调用`getResult`函数，而不需要创建`ConnectDatabase`类和运行对数据库的真实查询。这里需要注意的重要一点是，我们需要模拟的函数必须用`MOCK_METHOD[N]宏`来定义，其中 N 是接口将要取的参数个数。在我们的例子中，`获取结果`界面接受一个参数。因此，使用`MOCK_METHOD1`宏对其进行模拟。\n\n6.  Create a file named **Misc.cpp** and add the following code:\n\n    ```cpp\n    #include\"Misc.h\"\n    #include <unistd.h>\n    string ConnectDatabase::getResult(string query){\n        // dummy func, need to implement.. \n        // assuming query sent to DB is success and \n        // will return some dummy string \n        return string(\"DB returned success\");\n    }\n    string WebServerConnect::getRequest(string req){\n        // dummy func, need to implement.. \n        // assume no req string is sent to webserver. .\n        // its returns the result returned from server. \n        return string(\"Webserver returned success\");\n    }\n    void App::connectDB(string dbname, string user, string passwd){\n        if ( DB )\n            delete DB;\n        DB = new ConnectDatabase(dbname, user, passwd);\n    }\n    void App::connectDB(ConnectDatabase *db){\n        if ( DB )\n            delete DB;\n        DB = db;\n    }\n    void App::connectWeb(string webname, string user, string passwd){\n        if ( WB )\n            delete WB;\n        WB = new WebServerConnect(webname, user, passwd);\n    }\n    string App::getDBResult(string query){\n        return DB->getResult(query);\n    }\n    string App::getWebResult(string query) {\n        return WB->getRequest(query);\n    }\n    void App::run(){\n        if ( (DB == NULL) || (WB == NULL) )\n            return ;\n        while( true ){\n            // read some request to be run on web and get result. .. \n            cout << getWebResult(\"dummy request to webserver\") << endl;;\n            sleep(5);\n        }\n    }\n    bool App::checkValidIp(string ip){\n        if(ip.empty()){\n            cout << \"ERROR : IP passed is NULL \" << endl;\n            return false;\n        }\n        if(ip.size() > 15){\n            cout << \"ERROR : IP size is greater than 15\" <<endl;\n            return false;\n        }\n        //check if last character in ip is not '.' as that is not captured in tokenizing\n        if (ip[ip.size()-1] == '.'){\n            cout <<\"ERROR : Incorrect token at end\" << endl;\n            return false;\n        }\n        istringstream istrstr(ip);\n        vector<string> tokens;\n        string token;\n        regex expression(\"[^0-9]\");\n        smatch m;\n        while( getline(istrstr, token, '.') ){\n            if ( token.empty() ){\n                cout << \"ERROR : Got empty token \" << endl;\n                return false;\n            } \n            if ( token.find(' ') != string::npos){\n                cout << \"ERROR : Space character in token \" << endl;\n                return false;\n            } \n            if ( regex_search(token, m, expression) ){\n                cout << \"ERROR : NonDigit character in token \" << endl;\n                return false;\n            }\n            int val = atoi(token.c_str());\n            if ( val < 0 || val > 255 ){\n                cout << \"ERROR : Invalid Digit in token \" << endl;\n                return false;\n            }\n            tokens.push_back(token);\n        }\n        if ( tokens.size() != 4 ){\n    cout << \" ERROR : Incorect IP tokens used\" << endl;\n    return false;\n        }\n        cout << \"strlen: \" << ip.size() << endl;\n        return true;\n    }\n    ```\n\n    在前面的代码中，我们创建了一个最小的接口和虚拟参数来运行它，这样我们就可以理解实际的功能。我们已经为`getResult()`和`getRequest()`函数开发了基本功能，其中数据库查询和`网络服务器`查询返回一个默认字符串。这里`App::run()`函数假设数据库连接和 web 服务器连接都已经执行，现在可以定期执行 web 查询。在每次查询结束时，默认会返回“`Webserver 返回成功`”字符串。\n\n7.  Now, create a file named **RunApp.cpp** and write the following code inside the main function:\n\n    ```cpp\n    #include\"Misc.h\"\n    int main(){\n        App app;\n        app.connectDB(\"dbname\",\"dbuser\", \"dbpasswd\");\n        string ip = app.getDBResult(\"dummy\"); \n        // DB query to get Webserver IP\n        // Similarly some miscellaneous activities to get configuratio information\n        // Like querying DB to get correct username/passwd to connect to WebServer.. \n        // After getting IP from DB, check if the IP is valid..\n        //app.checkValidIp(ip);\n        // Now conect to webserver with parameters extracted from DB. \n        app.connectWeb(\"webname\",\"user\", \"passwd\");\n        // Now run the App, like sending some request to webserver, \n        // getting result and doing activity with received data. \n        app.run();\n        return 0;\n    } \n    ```\n\n    正如您在前面的代码中看到的，创建了应用类实例。使用这个实例，我们借助虚拟参数连接到数据库，即`数据库名`、`数据库用户`和`数据库密码`。然后，我们查询数据库以获取 IP 地址和其他配置参数。我们已经评论了`app.checkValidIp(ip)`行，因为我们假设我们从数据库获取的 Ip 地址需要验证。此外，该功能需要进行单元测试。使用`connectWeb()`功能，我们可以通过传递`网名`、`用户`、`passwd`等伪参数来连接 Web 服务器。最后，我们调用`run()`函数，该函数将在迭代中运行，从而查询 web 服务器并给出默认输出。\n\n8.  Save all the files and open the terminal. In order to get the basic functionality required to execute the project, we'll build the binary file and execute it to see the result. Run the following command in the terminal:\n\n    ```cpp\n    g++ Misc.cpp RunApp.cpp -o RunApp\n    ```\n\n    前面的代码将在当前文件夹中创建名为`RunApp`的二进制文件。\n\n9.  Now, write the following command to run the executable:\n\n    ```cpp\n    ./RunApp\n    ```\n\n    前面的命令在终端中生成以下输出:\n\n    ![Figure 7.13: Running the app](img/C14583_07_13.jpg)\n\n    ###### 图 7.13:运行应用\n\n    正如您在前面的截图中看到的，二进制文件及时显示了输出“`网络服务器返回成功`”。到目前为止，我们的应用运行良好，因为它假设所有接口都如预期的那样工作。但是我们仍然需要测试一些功能，例如验证`IP 地址`、`数据库连接`、检查`用户名`和`密码`的格式是否正确等等，然后才能为`QA`做准备。\n\n10.  Using the same infrastructure, start unit testing each functionality. For our exercise, we'll assume that the `DB connectivity` has already been done and has been queried to get the `IP address`. After that, we can start unit testing the validity of the `IP address`. So, in our test case, the Database class needs to be mocked and the `getDBResult` function must return the `IP address`. This `IP address` will be passed to the `checkValidIP` function later, wherein we'll test it. To implement this, create a class named **TestApp.cpp** wherein we'll be calling the `checkValidIP` function:\n\n    ```cpp\n    #include\"MockMisc.h\"\n    using ::testing::_;\n    using ::testing::Return;\n    class TestApp : public ::testing::Test {\n        protected : \n            App testApp;\n            MockDB *mdb;\n            void SetUp(){\n                mdb = new MockDB();\n                testApp.connectDB(mdb);\n            }\n            void TearDown(){\n            }\n    };\n    TEST_F(TestApp, NullIP){\n        EXPECT_CALL(*mdb, getResult(_)).\n                     WillOnce(Return(\"\"));\n        ASSERT_FALSE(testApp.checkValidIp(testApp.getDBResult(\"\")));\n    }\n    TEST_F(TestApp, SpaceTokenIP){\n        EXPECT_CALL(*mdb, getResult(_)).\n                     WillOnce(Return(\"13\\. 21.31.68\"));\n        ASSERT_FALSE(testApp.checkValidIp(testApp.getDBResult(\"\")));\n    }\n    TEST_F(TestApp, NonValidDigitIP){\n        EXPECT_CALL(*mdb, getResult(_)).\n                     WillOnce(Return(\"13.521.31.68\"));\n        ASSERT_FALSE(testApp.checkValidIp(testApp.getDBResult(\"\")));\n    }\n    TEST_F(TestApp, CorrectIP){\n        EXPECT_CALL(*mdb, getResult(_)).\n                     WillOnce(Return(\"212.121.21.45\"));\n        ASSERT_TRUE(testApp.checkValidIp(testApp.getDBResult(\"\")));\n    }\n    ```\n\n    在这里，我们使用了测试和`测试::返回`名称空间来调用模拟类接口，并返回用户定义的值，这些值用于测试用例。在`TEST_F`函数中，我们使用了`EXPECT_CALL`函数，其中我们将模拟对象的实例作为第一个参数传递，将`getResult()`函数作为第二个参数传递。`will once(Return(\" \")`行声明接口需要调用一次，并将返回“”和一个空字符串。这是需要传递给`checkValidIP`函数来测试空字符串的值。这是用`ASSERT_FALSE`宏检查的。同样，可以使用数据库的模拟对象创建其他测试用例，并将 IP 地址传递给`checkValidIP`函数。为了创建各种测试用例，`Test PP`类继承自`testing::Test`类，它包含数据库的 App 实例和模拟对象。在`TestApp`类中，我们定义了两个函数，即`SetUp()`和`deterdown()`。在`SetUp()`函数中，我们创建了一个`MockDB`实例，并将其标记到 testApp 实例中。由于`拆卸()`功能不需要做任何事情，所以我们将它保留为空。其析构函数在`App`类的析构函数中调用。另外，我们在`TEST_F`函数中传递了两个参数。第一个参数是测试类，而第二个参数是测试用例的名称。\n\n11.  Save all the files and open the terminal. Run the following command:\n\n    ```cpp\n    g++ Misc.cpp TestApp.cpp -lgtest -lgmock -lgtest_main -pthread -o TestApp\n    ```\n\n    在前面的命令中，我们还链接了`gmock 库`。现在，键入以下命令来运行测试用例:\n\n    ```cpp\n    ./TestApp\n    ```\n\n    前面的命令生成以下输出:\n\n![Figure 7.14: Running the Gmock test](img/C14583_07_14.jpg)\n\n###### 图 7.14:运行 Gmock 测试\n\n从前面的命令中，我们可以看到所有的测试用例都成功地执行并通过了。在下一节中，我们将讨论`断点`、`观察点`和`数据可视化`。\n\n### 断点、观察点和数据可视化\n\n在前一节中，我们讨论了单元测试需要在开发人员将代码签入存储库分支之前完成，并且可以被其他团队成员看到，以便他们可以将其与其他模块集成。尽管单元测试做得很好，并且开发人员签入了代码，但是无论何时代码被集成并且质量保证团队开始测试，他们都有可能发现代码中的错误。通常，在这种情况下，由于另一个模块中的更改，可能会在该模块中引发错误。对于团队来说，打击这些问题的真正原因可能会变得很困难。在这种情况下，**调试**进入画面。它确切地告诉我们代码的行为，开发人员可以获得代码执行的粒度信息。开发人员可以看到函数得到了什么参数，返回了什么值。它可以准确地告诉我们变量或指针的赋值，或者内存中的内容。这对于开发人员识别问题出在代码的哪个部分变得非常有帮助。在下一节中，我们将实现一个堆栈，并对其执行一些操作。\n\n### 使用堆栈数据结构\n\n考虑一个场景，开发人员被要求开发他/她自己的堆栈结构，该结构可以接受任何参数。这里的要求是堆栈结构必须遵循**后进先出** ( **后进先出**)原则，其中元素被放置在彼此之上，当它们从堆栈中移除时，最后一个元素应该首先被移除。它应该具有以下功能:\n\n*   **按()**将新元素放在堆栈顶部\n*   **top()** 显示堆栈的顶部元素(如果有)\n*   **pop()** 从堆栈中移除最后插入的元素\n*   **为 _empty()** 检查堆栈是否为空\n*   **size()** 显示堆栈中存在的元素数量\n*   **清除()**清空堆栈(如果其中有任何元素)\n\n下面几行代码展示了如何在 **Stack.h** 头文件中包含必要的库:\n\n```cpp\n#ifndef STACK_H__\n#define STACK_H__\n#include<iostream>\nusing namespace std;\n```\n\n正如我们已经知道的，堆栈由各种操作组成。为了定义这些函数，我们将编写以下代码:\n\n```cpp\ntemplate<typename T>\nstruct Node{\n    T element;\n    Node<T> *next;\n};\ntemplate<typename T>\nclass Stack{\n    Node<T> *head;\n    int sz;\n    public :\n        Stack():head(nullptr), sz(0){}\n        ~Stack();\n\n        bool is_empty();\n        int size();\n        T top();\n        void pop();\n        void push(T);\n        void clean();\n};\ntemplate<typename T>\nStack<T>::~Stack(){\n    if ( head ) clean();\n}\ntemplate<typename T>\nvoid Stack<T>::clean(){\n    Node<T> *tmp;\n    while( head ){\n        tmp = head;\n        head = head -> next;\n        delete tmp;\n        sz--;\n    }\n}\ntemplate<typename T>\nint Stack<T>::size(){\n    return sz;\n}\ntemplate<typename T>\nbool Stack<T>::is_empty(){\n        return (head == nullptr) ? true : false;\n}\ntemplate<typename T>\nT Stack<T>::top(){\n    if ( head == nullptr){\n        // throw error ...\n        throw(string(\"Cannot see top of empty stack\"));\n    }else {\n        return head -> element;\n    }\n}\ntemplate<typename T>\nvoid Stack<T>::pop(){\n    if ( head == nullptr ){\n        // throw error\n        throw(string(\"Cannot pop empty stack\"));\n    }else {\n        Node<T> *tmp = head ;\n        head = head -> next;\n        delete tmp;\n        sz--;\n    }\n}\ntemplate<typename T>\nvoid Stack<T>::push(T val){\n    Node<T> *tmp = new Node<T>();\n    tmp -> element = val;\n    tmp -> next = head;\n    head = tmp;\n    sz++ ;\n}\n// Miscellaneous functions for stack.. \ntemplate<typename T>\nvoid displayStackStats(Stack<T> &st){\n    cout << endl << \"------------------------------\" << endl;\n    cout << \"Showing Stack basic Stats ...  \" << endl;\n    cout << \"Stack is empty : \" << (st.is_empty() ? \"true\" : \"false\") << endl;\n    cout << \"Stack size :\" << st.size() << endl;\n    cout << \"--------------------------------\" << endl << endl;\n}\n#endif \n```\n\n到目前为止，我们已经看到了如何使用`单链表`实现堆栈。每次在堆栈中调用`推送`时，都会创建一个给定值的新元素并附加到堆栈的开头。我们称之为 head 成员变量，它是 head 指向堆栈中下一个元素的地方，以此类推。当调用`pop`时，头部将从堆栈中移除，并指向堆栈的下一个元素。\n\n让我们在 **Main.cpp** 文件中编写之前创建的 Stack 的实现。主函数有一个 try 块，用于创建整数堆栈和字符堆栈。两者都有一些推送和弹出，在这两者之间，调用堆栈的顶部来显示最新的元素。对于一堆整数，开头涉及三次推送:`22`、`426`、`57`。调用`displayStackStats()`函数时，应声明堆栈大小为`3`。然后，我们从堆栈中弹出`57`，顶部元素必须显示`426`。我们将对`char`的堆栈进行同样的操作。下面是堆栈的完整实现:\n\n```cpp\n#include\"Stack.h\"\nint main(){\n    try {\n        Stack<int> si;\n        displayStackStats<int>(si);\n        si.push(22);\n        si.push(426);\n        cout << \"Top of stack contains \" << si.top() << endl;\n        si.push(57);\n        displayStackStats<int>(si);\n        cout << \"Top of stack contains \" << si.top() << endl;\n        si.pop();\n        cout << \"Top of stack contains \" << si.top() << endl;\n        si.pop();\n        displayStackStats<int>(si);\n        Stack<char> sc;\n        sc.push('d');\n        sc.push('l');\n        displayStackStats<char>(sc);\n        cout << \"Top of char stack contains:\" << sc.top() << endl;\n    }\n    catch(string str){\n        cout << \"Error : \" << str << endl;\n    }\n    catch(...){\n        cout << \"Error : Unexpected exception caught \" << endl;\n    }\n    return 0;\n}\n```\n\n当我们通过编写以下命令编译 **Main.cpp** 文件时，Main 可执行文件将在调试模式下创建(因为使用了`-g`选项)。因此，如果需要，您可以调试二进制文件:\n\n```cpp\ng++ -g Main.cpp -o Main\n```\n\n我们将编写以下命令来执行二进制文件:\n\n```cpp\n./Main\n```\n\n前面的命令生成以下输出:\n\n![Figure 7.15: Main function using the Stack class](img/C14583_07_15.jpg)\n\n###### 图 7.15:使用堆栈类的主函数\n\n在前面的输出中，对 statistics 函数的第二次调用中的红墨水显示了在 int 堆栈中显示三个元素的正确信息。但是，对 int 堆栈顶部的红墨水调用显示随机值或垃圾值。如果程序再次运行，会显示一些其他随机数，而不是`57`和`426`的期望值。同样，对于 char 的栈，用红墨水突出显示的部分，即`char`的顶部，显示的是一个垃圾值，而不是期望值，即“l”。后来，执行显示双重自由或损坏的错误，这意味着自由被再次调用到同一个内存位置。最后，可执行文件给出了核心转储。程序没有按预期执行，从显示屏上可能看不出实际错误在哪里。要调试`Main`，我们将编写以下命令:\n\n```cpp\ngdb ./Main \n```\n\n前面的命令生成以下输出:\n\n![Figure 7.16: Debugger display – I](img/C14583_07_16.jpg)\n\n###### 图 7.16:调试器显示-I\n\n在前面的截图中，以蓝色突出显示的标记显示了调试器的使用方式和显示内容。第一个标记显示使用`gdb`命令调用调试器。进入`gdb`命令后，用户进入调试器的命令模式。以下是命令模式中使用的命令的一些简要信息:\n\n*   **b main** :这告诉调试器在主函数调用时中断。\n*   **r** :它是用来运行可执行文件的简短形式。它也可以通过传递参数来运行。\n*   **n** :是 next 命令的简写形式，告诉我们执行下一条语句。\n*   **观察 si** :当在代码中调用`si`变量时，它的值发生变化。调试器将显示使用该变量的代码内容。\n*   **s** :是命令中“**步”的简称。**\n\n下一个要执行的语句是`si.push(22)`。由于`si`已经更新，观察点调用并显示`si`的旧值和`si`的新值，其中显示`si`的旧值为空，且`sz`为 0。在`si.push`之后，头部被更新为新值，其执行到达`Stack.h`文件的第 75 行，这是`sz`变量递增的地方。如果再次按下*回车*键，将执行。\n\n注意，执行已经自动从主功能移到`栈::推`功能。下面是调试器上继续执行的命令的屏幕截图:\n\n![](img/C14583_07_17.jpg)\n\n###### 图 7.17:调试器显示–二\n\n下一个命令显示`sz`已经更新为新值`1`。按下*进入*后，代码的执行从`栈:将**第 76 行**`**上的**推回到第 8 行的主功能。这在下面的截图中突出显示。显示执行在`si.push(426)`调用时停止。一旦我们介入，就会调用`栈::推`。执行移动到`Stack.h`程序的`第 71 行`，如红墨水所示。一旦执行到`第 74 行`，如红墨水所示，手表被调用，显示`si`被更新为新值。可以看到`栈::推`功能完成后，流程回到主代码。以下是在调试器中执行的步骤的屏幕截图:\n\n![](img/C14583_07_18.jpg)\n\n###### 图 7.18:调试器显示–三\n\n按下*进入*后，你会看到`显示状态`在`第 11 行`被调用。在 **Main.cpp** 程序中，下一个命令显示堆栈的顶部元素，该元素在`第 12 行`被调用。但是显示屏显示的数值为`0`，而不是`57`的期望值。这是一个我们仍然无法理解的错误——为什么值会改变？但是，很明显，在前面对主函数的调用中，该值可能已经发生了变化。因此，这可能不会让我们对继续前面的调试感兴趣。但是，我们需要从头开始进行调试。\n\n下面的屏幕截图显示了将用于调试代码的命令:\n\n![](img/C14583_07_19.jpg)\n\n###### 图 7.19:调试器显示–四\n\n要从头重新运行程序，必须按 *r* ，确认并继续，需要按 *y* ，也就是从头重新运行程序。它会要求确认；按下 *y* 继续。在前面的截图中，所有这些命令都以蓝色突出显示。在第 7 行的执行过程中，我们需要运行“`display *si.head`”命令，该命令将在执行的每条语句之后持续显示`si.head`存储单元的内容。如红墨水所示，将`22`推到堆栈上后，头会更新为正确的值。同样，对于值`426`和`57`，当使用 push 插入堆栈时，对 head 的调用被正确更新。\n\n后来调用`displayStackStats`时，显示的是`3`的正确`尺寸`。但是当调用 top 命令时，头部显示的值是错误的。这用红墨水突出显示。现在，top 命令的代码没有改变 head 的值，所以很明显在前面的执行语句中出现了一个错误，即在`displayStackStats`处。\n\n因此，我们缩小了可能存在问题的代码的范围。我们可以运行调试器来指向`显示堆栈状态`并移动到`显示堆栈状态`中，以查找导致堆栈中的值被更改的原因。下面是其中的屏幕截图，用户需要从头开始启动调试器:\n\n![Figure 7.20: Debugger display – IV](img/C14583_07_20.jpg)\n\n###### 图 7.20:调试器显示–四\n\n从头开始重启调试器，到达调用`displayStackStats`的第 11 行执行点后，我们需要介入。流程是进入`显示状态`功能的开始。另外，我们需要执行下一条语句。由于函数中的初始检查是清晰的，它们不会改变头部的值，我们可以按*进入*执行下一步。当我们怀疑接下来的步骤会改变我们正在寻找的变量的值时，我们需要介入。这是在前面以红色突出显示的快照中完成的。后者执行到`第 97 行`，即`显示堆栈状态`功能的最后一行。\n\n在输入 *s* 后，执行移动到析构函数堆栈，并在第 81 行调用 clean 函数。这个清除命令删除了 **tmp** 变量，该变量的值与头部的值相同。该函数清空堆栈，这是不希望运行的。只有 **displayStackStats** 函数应该被调用并执行，以最终返回到主函数。但是析构函数可能会因为函数中的局部变量在函数完成后超出范围而被调用。这里，局部变量是在第 92 行被声明为 **displayStackStats** 函数参数的变量。因此，当调用 **displayStackStats** 函数时，创建了主函数中 **si** 变量的本地副本。这个变量在超出范围时调用堆栈的析构函数。现在 **si** 变量的指针已经被复制到临时变量中，并错误地删除了末尾的指针。这不是开发人员的本意。因此，在代码执行结束时，出现了双重自由错误。 **si** 变量在超出范围时必须调用堆栈析构函数，因为它将再次尝试释放相同的内存。要解决这个问题，很明显 **displayStackStats** 函数必须以传递参数作为引用来调用。为此，我们必须更新**堆栈. h** 文件中**显示堆栈状态**函数的代码:\n\n```cpp\ntemplate<typename T>\nvoid displayStackStats(Stack<T> &st){\n    cout << endl << \"------------------------------\" << endl;\n    cout << \"Showing Stack basic Stats ...  \" << endl;\n    cout << \"Stack is empty : \" << (st.is_empty() ? \"true\" : \"false\") << endl;\n    cout << \"Stack size :\" << st.size() << endl;\n    cout << \"--------------------------------\" << endl << endl;\n}\n```\n\n现在，当我们保存并编译 **Main.cpp** 文件时，将生成二进制文件:\n\n```cpp\n./Main\n```\n\n前面的命令在终端中生成以下输出:\n\n![Figure 7.21: Debugger display – IV](img/C14583_07_21.jpg)\n\n###### 图 7.21:调试器显示–四\n\n从前面的截图中，我们可以看到`57`和`426`的期望值显示在栈顶。`显示堆栈状态`功能还显示 int 和 char Stack 的正确信息。最后，我们使用调试器找到了错误并修复了它。在下一节中，我们将解决一个活动，其中我们将开发解析文件的函数，并编写测试用例来检查函数的准确性。\n\n### 活动 1:使用测试用例检查功能的准确性并理解测试驱动开发(TDD)\n\n在本练习中，我们将开发函数，这样我们就可以解析文件，然后编写测试用例来检查我们开发的函数的正确性。\n\n大型零售组织中的一个信息技术团队希望通过将产品详细信息和客户详细信息存储在其数据库中来跟踪产品销售，作为其对账的一部分。销售部门将定期以简单的文本格式向信息技术团队提供这些数据。作为开发人员，您需要确保数据的基本健全性检查已经完成，并且在公司将记录存储到数据库之前，所有记录都被正确解析。销售部门将提供两个文本文件，保存所有销售交易的客户信息和货币信息。您需要编写解析函数来处理这些文件。这两个文件分别是 **RecordFile.txt** 和 **CurrencyConversion.txt** 。 **RecordFile.txt** 文件包含客户的详细信息、他们购买的产品以及以地区货币和外币表示的总价。 **CurrencyConversion.txt** 文件包含两个字段，即`币种`和`ConversionRatio`。\n\n该项目环境设置的所有必要信息都保存在配置文件中。这还将保存文件名，以及其他参数(如`数据库`、`RESTAPI`等)和一个名为 **parse.conf** 的文件中的变量值。每行包含两个字段，由分隔符“=”分隔。第一行将是一个标题，说明“配置文件”。对于记录文件，变量名为`记录文件`，同样对于货币文件，变量名为`货币文件`。\n\n以下是我们将要编写的测试条件，用于检查解析 **CurrencyConversion.txt** 文件的函数的准确性:\n\n*   第一行应该是标题行，它的第一个字段应该包含“`Currency`”字符串。\n*   `货币`字段应由三个字符组成。例如:“`美元`”、“`英镑`有效。\n*   `转换比率`字段应该由浮点数组成。例如:`1.2`、`0.06`有效。\n*   每行应该正好有两个字段。\n*   用于记录的分隔符是“|”。\n\n下面是我们将要编写的测试条件，用来检查用来解析 **RecordFile.txt** 文件的函数的准确性:\n\n*   第一行应包含标题行，其第一个字段应包含“`客户标识`”字符串。\n*   `客户号`、`订单号`、`产品号`、`数量`都应该是整数值。例如`12312`、`4531134`有效。\n*   `合计价格(地区货币)``合计价格(美元)`应为浮点值。例如:`2433.34`、`3434.11`有效。\n*   `地区货币`字段的值应该出现在**货币转换. txt** 文件或`标准::地图`中。\n*   每行应该正好有九个字段，如文件的`HEADER`信息中所定义的。\n*   记录的分隔符是“|”。\n\n按照以下步骤实施本活动:\n\n1.  解析 **parse.conf** 配置文件，其中包含项目运行的环境变量。\n2.  从步骤 1 开始，正确设置`记录文件`和`当前文件`变量。\n3.  使用我们从配置文件中检索到的这些变量，解析满足所有条件的货币文件。如果不满足条件，则返回适当的错误消息。\n4.  用我们满足的所有条件解析记录文件。如果没有，则返回错误消息。\n5.  创建一个名为`CommonHeader.h`的头文件，并声明所有的实用函数，即`isAllNumbers()`、`isDigit()`、`parceline()`、`checkFile()`、`parseConfig()`、`parsecurrency parameters()`、`fillCurrencyMap()`、`recordparsefile()`、【\n6.  创建一个名为`Util.cpp`的文件，定义所有的实用函数。\n7.  创建一个名为`ParseFiles.cpp`的文件，调用`parseConfig()`、`filllcurrency map()`和`parseRecordFile()`函数。\n8.  编译并执行`Util.cpp`和`ParseFiles.cpp`文件。\n9.  创建一个名为`ParseFileTestCases.cpp`的文件，为函数编写测试用例，即`trim()`、`isAllNumbers()`、`isDigit()`、`parsecurrenceparameters()`、`checkFile()`、`parseConfig()`、`filecorency map()`和`parseRecordFile()`\n10.  编译并执行`Util.cpp`和`ParseFileTestCases.cpp`文件。\n\n以下是解析不同文件和显示信息的流程图:\n\n![](img/C14583_07_22.jpg)\n\n###### 图 7.22:流程图\n\n从上面的流程图中，我们大致了解了执行流程。为了在编写代码之前有一个清晰的理解，让我们看看更精细的细节。它将有助于为每个执行块定义测试用例。\n\n为了解析配置文件块，我们可以将步骤分为以下几个部分:\n\n1.  检查配置文件是否存在并具有读取权限。\n2.  检查它是否有适当的标题。\n3.  逐行分析整个文件。\n4.  对于每一行，解析以“=”作为分隔符的字段。\n5.  如果上一步有 2 个字段，处理看是`货币文件`还是`记录文件`变量并适当存储。\n6.  如果步骤 4 中没有 2 个字段，请转到下一行。\n7.  完全解析文件后，检查上述步骤中的两个变量是否都不为空。\n8.  如果为空，返回错误。\n\n为了解析`货币文件`块，我们可以将步骤分解为以下内容:\n\n1.  读取`CurrencyFile`的变量，看该文件是否存在，是否有读取权限。\n2.  检查它是否有适当的标题。\n3.  用“|”作为分隔符，逐行分析整个文件。\n4.  如果每行正好有 2 个字段，可以考虑第一个作为`货币字段`，第二个作为`换算字段`。\n5.  如果在步骤 3 中没有找到 2 个字段，则返回相应的错误消息。\n6.  从第 4 步开始，对`币种字段`(应为 3 个字符)和`转换字段`(应为数字)进行所有检查。\n7.  如果从步骤 6 通过，将`货币` / `转换`值成对存储在地图上，键为`货币`，值为数字。\n8.  如果步骤 6 没有通过，返回错误说明`货币`。\n9.  在完成对`货币`文件的解析后，将创建一个包含所有货币的转换值的地图。\n\n对于解析`记录文件`块，我们可以将步骤分解为以下内容:\n\n1.  读取`记录文件`的变量，查看文件是否存在，是否有读取权限。\n2.  检查它是否有适当的标题。\n3.  用“|”作为分隔符，逐行分析整个文件。\n4.  如果在上述步骤中没有找到 9 个字段，则返回相应的错误消息。\n5.  如果找到 9 个字段，对“活动开始”中列出的所有字段进行相应的检查。\n6.  如果步骤 5 没有通过，返回适当的错误消息。\n7.  如果步骤 5 通过，将记录存储在记录向量中。\n8.  在完全解析记录文件之后，所有的记录将被存储在记录向量中。\n\n在创建解析所有三个文件的流程时，我们看到对所有三个文件重复的步骤很少，例如:\n\n检查文件是否存在且可读\n\n检查文件是否有正确的标题信息\n\n用分隔符解析记录\n\n检查字段是否为`数字`在`货币`和`记录文件`中常见\n\n检查字段是否为`数字`在`货币`和`记录文件`中很常见\n\n以上几点将有助于重构代码。此外，还有一个解析带分隔符字段的通用函数，即`修剪函数`。因此，当我们用分隔符解析记录时，我们可以在开头或结尾用空格或制表符获取值，这可能是不需要的，所以我们需要在解析记录时修剪一次。\n\n现在我们知道我们有以上常见的步骤，我们可以为它们编写单独的函数。从 TDD 开始，我们首先了解功能需求，然后开始编写单元测试用例来测试这些功能。然后我们编写函数，这样它将通过单元测试用例。如果很少有测试用例失败，我们就重复更新函数和执行测试用例的步骤，直到它们全部通过。\n\n对于我们的例子，上面我们可以写`修剪`函数，\n\n现在我们知道在修剪功能中，我们需要删除第一个和最后一个多余的空格/制表符。例如，如果字符串包含“AA”，修剪应该返回“AA”删除所有空格。\n\ntrim 函数可以返回带有预期值的新字符串，也可以更新传递给它的相同字符串。\n\n所以现在我们可以写 trim 函数的签名:`字符串 trim(string&)；`\n\n我们可以为它编写以下测试用例:\n\n*   如果只有多余的字符(\" \")，则返回空字符串()。\n*   开头只有空字符(“AA”)的返回字符串带有结尾字符(“AA”)\n*   结尾只有空字符(“AA”)，应该返回开头有字符(“AA”)的字符串\n*   中间带字符(“AA”)返回带字符(“AA”)的字符串\n*   中间有空格(“AA BB”)，返回相同的字符串(“AA BB”)\n*   单个字符的所有步骤 3、4、5。应该返回单个字符的字符串。\n\n要创建测试用例，请检查文件 **ParseFileTestCases.cpp** ，用于`trim`功能的测试用例写在测试套件`trim`中。现在写 **Util.cpp** 文件(所有杂项功能都写在 **Util.cpp** 中)。用文件中显示的签名编写`修剪`功能。执行`微调`功能的测试用例，检查是否通过。它没有适当地改变功能并再次测试它。重复直到所有测试用例通过。\n\n现在我们有信心在项目中使用`修剪`功能。对其余的常用功能重复类似的步骤(`isDigit`，`isNumeric`，`parseHeader`等等)。请参考 **Util.cpp** 文件和 **ParseFiletestCases.cpp** 文件，测试所有常用功能。\n\n完成常用函数后，我们可以分别编写解析每个文件的函数。这里要理解和学习的主要内容是如何将模块分解成小函数。找到重复的小任务，为每个任务创建小函数，以便重构。理解这些小函数的详细功能，并创建适当的单元测试用例。\n\n完成单个函数并彻底测试它，如果失败，那么更新该函数直到它通过所有测试用例。同样，完成其他功能。然后为更大的函数编写和执行测试用例，这应该相对容易，因为我们在这些更大的函数中调用上面测试的小函数。\n\n在实现了前面的步骤之后，我们将获得以下输出:\n\n![Figure 7.23: All tests running properly](img/C14583_07_23.jpg)\n\n###### 图 7.23:所有测试运行正常\n\n以下是后续步骤的截图:\n\n![Figure 7.24: All tests running properly](img/C14583_07_24.jpg)\n\n###### 图 7.24:所有测试运行正常\n\n#### 注意\n\n这个活动的解决方案可以在第 706 页找到。\n\n## 总结\n\n在这一章中，我们研究了在编译时和运行时使用断言获取可执行文件抛出的错误的各种方法。我们还学习了静态断言。我们了解异常是如何生成的，以及如何在代码中处理它们。我们还看到了单元测试如何成为开发人员的救星，因为他们将能够在开始时识别代码中的任何问题。我们为需要在测试用例中使用的类使用了模拟对象。然后，我们了解了调试器、断点、观察点和可视化数据。我们能够使用调试器找到代码中的问题并修复它们。我们还解决了一个活动，其中我们编写了必要的测试用例来检查用于解析文件的函数的准确性。\n\n在下一章，我们将学习如何优化我们的代码。我们将回顾处理器如何执行代码和访问内存。我们还将学习如何确定软件执行所需的额外时间。最后，我们将了解内存对齐和缓存访问。"
  },
  {
    "path": "docs/adv-cpp/09.md",
    "content": "# 九、对速度的需求——性能和优化\n\n## 学习目标\n\n本章结束时，您将能够:\n\n*   手动为代码性能计时\n*   使用源代码检测来测量代码执行时间\n*   使用性能工具分析程序性能\n*   使用 godbolt 编译器资源管理器工具来分析编译器生成的机器代码\n*   使用编译器标志来生成更好的代码\n*   应用能够提高性能的代码习惯用法\n*   编写缓存友好的代码\n*   将算法级优化应用于现实问题\n\n在这一章中，我们将探索一些概念，这些概念将使我们能够编写快速的代码，以及一些特别适用于 C++ 的实用技术。\n\n## 简介\n\n在当今这个软件系统极其庞大和复杂的世界里，`稳定性`和`可维护性`通常被认为是大多数软件项目的主要目标，然而自 2000 年代以来，优化并没有被广泛认为是一个有价值的目标。这是因为硬件技术的快速发展超过了软件需求。\n\n多年来，硬件的改进似乎将继续跟上软件的性能需求，但应用继续变得更大、更复杂。与性能较低但更容易使用解释语言(如`Python`或`Ruby`)相比，C 和 C++ 等低级本机编译语言的受欢迎程度有所下降。\n\n然而，到 2000 年代末，每 18 个月 CPU 晶体管数量(和性能)翻一番的趋势(摩尔定律`的结果`已经停止，性能的提高已经趋于平缓。由于物理和制造成本的限制，5 到 10 千兆赫处理器在 2010 年代广泛可用的期望从未实现。然而，移动设备的快速采用以及用于数据科学和机器学习的高性能计算应用的兴起，突然复活了对快速高效代码的需求。随着大型数据中心消耗大量电力，性能功耗比已成为新的衡量标准。例如，2017 年，美国的谷歌服务器用电量超过了整个英国。\n\n到目前为止，在本书中，我们已经了解了 C++ 语言在易用性方面是如何发展的，而没有牺牲任何优于传统语言(如 C)的性能潜力。这意味着我们可以用 C++ 编写快速代码，而不必牺牲可读性或稳定性。在下一节中，我们将学习性能测量的概念。\n\n## 性能测量\n\n优化最重要的方面是代码执行时间的**测量。除非我们用广泛的输入数据集来衡量我们的应用的性能，否则我们将不知道哪个部分花费的时间最多，并且我们的优化工作将被蒙在鼓里，无法保证结果。有几种测量方法，这里列出了其中一些:**\n\n*   运行时检测或分析\n*   源代码工具\n*   手动执行时序\n*   研究生成的汇编代码\n*   通过研究使用的代码和算法进行手动估计\n\n前面的列表是按照测量的精确度排序的(首先是最精确的)。然而，这些方法各有不同的优点。选择采用哪种方法取决于优化工作的目标和范围。为了尽可能快地实现，所有这些都是必需的。我们将在下面的章节中研究这些方法。\n\n### 手动估算\n\n当我们用一个更好的算法替换一个算法时，性能可能得到最大的提高。例如，考虑一个平凡函数的两个版本，它将从`1`到`n`的整数相加:\n\n```cpp\nint sum1(int n)\n{\n  int ret = 0;\n  for(int i = 1; i <= n; ++ i)\n  {\n    ret += i;\n  }\n  return ret;\n}\nint sum2(int n)\n{\n  return (n * (n + 1)) / 2;\n}\n```\n\n第一个函数`sum1`使用一个简单的循环来计算总和，并且具有与`n`成比例的运行时复杂度，而第二个函数`sum2`使用代数求和公式，并且花费与`n`无关的恒定时间。在这个非常人为的例子中，我们简单地用代数的基本知识优化了一个函数。\n\n有许多众所周知的算法，用于每一个可以想象的操作，已经被证明是最理想的。让我们的代码尽可能快地运行的最好方法是使用算法。\n\n掌握算法词汇是很重要的。我们不需要成为算法专家，但我们至少需要意识到各种领域中高效算法的存在，即使我们没有能力从头实现它们。稍微深入一点的算法知识将有助于我们找到程序中与众所周知的算法执行相似(如果不是完全相同)计算的部分。某些代码特性，如嵌套循环或数据的线性扫描，通常是明显的改进候选，前提是我们可以验证这些构造在代码的热点内。一个**热点**是一段运行非常频繁且对性能影响很大的代码。C++ 标准库包含许多基本算法，可以用作构建块来提高许多常见操作的效率。\n\n### 研究生成的汇编代码\n\n**汇编语言**是在处理器上实际执行的二进制机器代码的人类可读表示。对于任何认真学习 C++ 等编译语言的程序员来说，对汇编语言的基本理解是一笔巨大的财富。\n\n研究为程序生成的汇编代码可以让我们对编译器如何工作以及代码效率的估计有一些很好的了解。在许多情况下，这是确定效率瓶颈的唯一可能方法。\n\n除此之外，汇编语言的基本知识对于调试 C++ 代码是必不可少的，因为一些最难捕捉的错误是那些与低级生成代码相关的错误。\n\n用于分析编译器生成的代码的一个非常强大和流行的在线工具是我们将在本章中使用的**编译器资源管理器**。\n\n#### 注意\n\n在[https://godbolt.org](https://godbolt.org)可以找到`戈德博尔特编译器浏览器`。\n\n以下是 Godbolt 编译器浏览器的屏幕截图:\n\n![](img/C14583_08_01.jpg)\n\n###### 图 8.1:哥德堡编译器浏览器\n\n如您所见，Godbolt 编译器资源管理器由两个窗格组成。左边的一个是我们输入代码的地方，而右边的一个显示生成的汇编代码。左侧窗格有一个下拉列表，以便我们可以选择所需的语言。出于我们的目的，我们将使用 C++ 语言和 gcc 编译器。\n\n右侧窗格有一些选项，我们可以用来选择编译器版本。几乎所有流行编译器的版本都存在，比如`gcc`、`clang`、`cl` ( `微软 C++ `)包括那些针对 ARM 等非 X86 架构的编译器。\n\n#### 注意\n\n为了简单起见，我们将英特尔处理器架构称为`x86`，尽管正确的定义是`x86/64`。我们将省略提及“`64`”，因为今天制造的几乎所有处理器都是`64 位`。尽管`x86`是英特尔发明的，但现在所有的 PC 处理器制造商都获得了使用它的许可。\n\n为了熟悉`编译器探索者工具`的基础知识，从基础层面理解`x86`汇编代码，我们来考察一下一个简单函数的编译器生成的汇编代码，这个简单函数将`1`到`N`的整数相加。下面是需要在编译器资源管理器的左侧窗格中编写的 sum 函数:\n\n```cpp\nint sum(int n)\n{\n  int ret = 0;\n  for(int i = 1; i <= n; ++ i)\n  {\n    ret += i;\n  }\n  return ret;\n}\n```\n\n在右侧窗格中，编译器必须设置为 **x86-64 gcc 8.3** ，如下所示:\n\n![Figure 8.2: C++ compiler ](img/C14583_08_02.jpg)\n\n###### 图 8.2: C++ 编译器\n\n完成后，左侧窗格的代码会自动重新编译，并且会生成汇编代码并显示在右侧窗格中。这里，输出是彩色编码的，以显示哪些汇编代码行是从哪些 C++ 代码行生成的。下面的屏幕截图显示了生成的程序集代码:\n\n![Figure 8.3: Assembly result ](img/C14583_08_03.jpg)\n\n###### 图 8.3:装配结果\n\n让我们简单分析一下前面的汇编代码。汇编语言中的每条指令都由一个**操作码**和一个或多个**操作数**组成，它们可以是寄存器、常量值或内存地址。一个**寄存器**是 CPU 中一个非常快的存储位置。在 x86 架构中，主要有 8 个寄存器，分别是 **RAX** 、 **RBX** 、 **RCX** 、 **RDX** 、 **RSI** 、 **RDI** 、 **RSP** 、 **RBP** 。英特尔 x86/x64 架构使用了一种奇怪的寄存器命名模式:\n\n*   **RAX** 是一个通用的 64 位整数寄存器。\n*   **EAX** 指的是`RAX`的底部 32 位。\n*   **AX** 指的是`EAX`的底部 16 位。\n*   **AL** 和 **AH** 分别指`AX`的底部和顶部 8 位。\n\n同样的惯例也适用于其他通用寄存器，如`RBX`、`RCX`和`RDX`。`RSI`、`RDI`和`RBP`寄存器有 16 位和 32 位版本，但没有 8 位子寄存器。指令的操作码可以有几种类型，包括算术、逻辑、按位、比较或跳转操作。通常将操作码称为指令。例如，“ **mov 指令**是指`操作码`为 **mov** 的指令。下面是我们的`sum`函数的汇编代码快照:\n\n![Figure 8.4: Assembly code of the sum function ](img/C14583_08_04.jpg)\n\n###### 图 8.4:求和函数的汇编代码\n\n在前面的截图中，前几行被称为一个**函数序言**，也就是用来设置**栈帧**和局部变量的指令。堆栈框架表示包含参数和局部变量的函数中的本地化数据。当函数返回时，堆栈帧被丢弃。 **mov** 指令用常数值初始化寄存器或存储单元。这里汇编代码的语法叫做**英特尔语法**。这种语法的惯例是目标操作数总是第一个。例如，`RBX MOV RAX`装配代码表示将`RBX`寄存器中的值移动到`RAX`寄存器中。\n\n#### 注意\n\n汇编语言通常不区分大小写，所以`EAX`和`eax`的意思是一样的。\n\n装配中的 **DWORD PTR [rbp-8]** 表达相当于`(*(DWORD*)(rbp - 8))` C 表达。换句话说，存储器地址 **rbp-8** 作为一个`4`字节`DWORD`被访问(存储器的双字–32 位)。汇编代码中的方括号表示取消引用，很像 C/C++ 中的*运算符。`rbp`寄存器是始终包含当前正在执行的函数栈的基址的基址指针。不一定要确切知道这个堆栈框架是如何工作的，但是请记住，由于堆栈从较高的地址开始并向下移动，函数参数和局部变量的地址是从`rbp`开始的负偏移。如果您看到与`rbp`有一些负偏移，它指的是局部变量或参数。\n\n在前面的截图中，第一条 **mov** 指令将来自 **edi** 寄存器的值放入堆栈中——在本例中，它代表传入的`n`参数。最后两条 **mov** 指令将我们代码中的`ret`变量和`i`循环变量分别初始化为`0`和`1`。\n\n现在，检查序言和初始化之后的汇编代码的快照——这是我们对()循环的**:**\n\n![Figure 8.5: Assembly code of the for loop ](img/C14583_08_05.jpg)\n\n###### 图 8.5:for 循环的汇编代码\n\n在前面的截图中，字符串后跟冒号的行被称为**标签**。它们非常类似于编程语言中的标签，如`BASIC`、`C/C++ `或`Pascal`，用作**跳转**指令的目标(汇编语言中相当于`转到`语句)。\n\nx86 汇编上以 J 开头的指令都是跳转指令，如 **JMP** 、 **JG** 、 **JGE** 、 **JL** 、 **JLE** 、 **JE** 、 **JNE** 等等。跳转指令是有条件或无条件的 gotos。在上一个截图中， **mov** 指令将`i`变量的值从内存加载到 **eax** 寄存器中。然后，用 **cmp** 指令将其与存储器中的`n`值进行比较。\n\n#### 注意\n\n这里的 **JG** 指令是指**如果大于**就跳。\n\n如果比较较大，则执行跳转到 **.L2** 标签(在循环之外)。如果没有，则继续执行下一条指令，如下所示:\n\n![Figure 8.6: Assembly code of the next instruction ](img/C14583_08_06.jpg)\n\n###### 图 8.6:下一条指令的汇编代码\n\n这里 **i** 的值再次重新加载到 **eax** 中，这似乎是不必要的。但是请记住，这个汇编代码没有优化，所以编译器生成的代码可能不是最佳的，并且可能包含不必要的工作。然后将 **eax** 中的值加到 **ret** 中，之后将 **1** 加到 **i** 中。最后，执行跳回 **.L3** 标签。 **.L2** 和 **.L3** 标签之间的这些指令形成了执行循环的**的代码，并汇总了直到 **n** 的整数序列，如下所示:**\n\n![Figure 8.7: Assembly code of the for loop ](img/C14583_08_07.jpg)\n\n###### 图 8.7:for 循环的汇编代码\n\n这被称为**函数 epilog** 。首先，要返回的值`ret`被移入 **eax** 寄存器–这通常是存储函数返回值的寄存器。然后，堆叠框架复位，最后`ret`从`sum()`功能返回。\n\n#### 注意\n\n上面程序集列表中的“ret”是 RETURN 指令的助记符，不应该与我们的 C++ 代码示例中的“ret”变量混淆。\n\n弄清楚一系列汇编指令的作用并不是一件简单的工作，但是通过观察以下几点可以获得源代码和指令之间映射的一般概念:\n\n*   代码中的常量值可以在程序集中直接识别。\n*   诸如`相加`、`子`、`imul`、`idiv`等算术运算都可以识别。\n*   条件跳转映射到循环和条件。\n*   可以直接读取函数调用(函数名出现在汇编代码中)。\n\n现在，让我们观察一下代码的效果，如果我们在右上角的编译器选项字段中添加一个编译器优化标志:\n\n![Figure 8.8: Adding a compiler flag for optimization ](img/C14583_08_08.jpg)\n\n###### 图 8.8:为优化添加编译器标志\n\n在上图截图中， **O3** 代表**最大优化**。其他标志，如 **-mno-avx** 、 **-mno-sse** 、 **-mno-sse2** ，用于防止编译器生成与当前示例无关的**向量指令**。我们可以看到编译器不再访问内存，只使用寄存器。请注意 **xor eax，eax** 这一行，它具有将`0`存储在 **eax** 中的效果——这比将常量`0`从内存加载到寄存器中更有效。由于访问内存需要几个时钟周期(从`5`到`100`时钟周期)，仅使用寄存器本身就会产生巨大的加速。\n\n当下拉菜单中的编译器更改为 **x86-64 clang 8.0.0** 时，汇编代码发生了变化，如下图截图所示:\n\n![Figure 8.9: Assembly code with the new compiler ](img/C14583_08_09.jpg)\n\n###### 图 8.9:使用新编译器的汇编代码\n\n在前面的装配清单中，注意没有以`J`开始的指令(用于跳转)。因此，根本没有循环构造！让我们来看看编译器是如何计算`1`到`n`的和的。如果`n`的值为`< = 0`，则跳至 **.LBB0_1** 标签退出，返回`0`。让我们分析以下说明:\n\n![Figure 8.10: Assembly code with the new compiler ](img/C14583_08_10.jpg)\n\n###### 图 8.10:使用新编译器的汇编代码\n\n下面的代码相当于前面的指令。记住`n`在`EDI`寄存器中(因此也在 RDI 寄存器中，因为它们重叠):\n\n```cpp\neax = n - 1;\necx = n - 2;\nrcx *= rax;\nrcx >>= 1;\neax = rcx + 2 * n;\neax--;\nreturn eax;\n```\n\n或者，如果我们将它写在一行中，它将如下所示:\n\n```cpp\nreturn ((n-1) * (n-2) / 2) + (n * 2) - 1;\n```\n\n如果我们简化这个表达式，我们会得到以下结果:\n\n```cpp\n((n^2 - 3n + 2) / 2) + 2n - 1\n```\n\n或者，我们可以用以下格式编写它:\n\n```cpp\n((n^2 - 3n + 2) + 4n - 2) / 2\n```\n\n这可以简化为以下内容:\n\n```cpp\n(n^2 + n) / 2\n```\n\n或者，我们可以编写以下内容:\n\n```cpp\n(n * (n+1)) / 2\n```\n\n这是数字`1`到`n`相加的封闭形式方程，也是最快的计算方法。编译器非常聪明——不仅仅是一行行地查看我们的代码，它推断我们的循环的效果是计算总和，并且它自己计算出代数。它没有计算出最简单的表达式，而是一个需要一些额外操作的等价表达式。然而，去掉循环使得这个函数非常优化。\n\n如果我们为循环修改**中 **i** 变量的初始值或最终值来创建不同的求和，编译器仍然能够执行必要的代数操作来导出不需要循环的封闭形式解。**\n\n这只是编译器如何变得极其高效并且看起来近乎智能的一个例子。然而，我们必须理解，这种特殊的求和优化已经被专门编程到`铿锵`编译器中。这并不意味着编译器可以对任何可能的循环计算进行这种技巧——这实际上需要编译器拥有一般的人工智能，以及世界上所有的数学知识。\n\n让我们通过生成的汇编代码来探索编译器优化的另一个例子。请看下面的代码:\n\n```cpp\n#include <vector>\nint three()\n{ \n  const std::vector<int> v = {1, 2};\n  return v[0] + v[1];\n}\n```\n\n在编译器选项中，如果我们选择 **x86-64 clang 8.0.0** 编译器并添加 **-O3 -stdlib=libc++** ，将生成以下汇编代码:\n\n![Figure 8.11: Assembly code generated with the new compiler ](img/C14583_08_11.jpg)\n\n###### 图 8.11:用新编译器生成的汇编代码\n\n正如你在前面的截图中看到的，编译器正确地判断出向量与函数无关，并移除了所有包袱。它还在编译时做加法，并直接使用结果`3`作为常数。本节的主要内容如下:\n\n*   给定正确的选项，编译器在优化代码时会非常聪明。\n*   研究生成的汇编代码对于获得执行复杂度的高级估计非常有用。\n*   对机器代码如何工作的基本理解对任何 C++ 程序员来说都是有价值的。\n\n在下一节中，我们将学习手动执行计时。\n\n### 手动执行计时\n\n这是快速计时小程序最简单的方法。我们可以使用命令行工具来测量程序执行所需的时间。在 Windows 7 及更高版本上，可以使用以下 PowerShell 命令:\n\n```cpp\npowershell -Command \"Measure-Command {<your program and arguments here>}\"\n```\n\n在`Linux`、`MacOS`和其他`类 UNIX`系统上，可以使用`时间`命令:\n\n```cpp\ntime <your program and arguments here>\n```\n\n在下一节中，我们将实现一个小程序，并研究一些关于程序执行时间的注意事项。\n\n### 练习 1:为程序的执行计时\n\n在本练习中，我们将编写一个对数组求和的程序。这里的想法是给求和函数计时。当我们想要测试一个独立编写的函数时，这个方法很有用。因此，测试程序的唯一目的是执行一个单一的功能。由于计算非常简单，我们将需要运行函数数千次才能获得可测量的执行时间。在这种情况下，我们将从`main()`函数调用`sumVector()`函数，传递一个随机整数的`std::vector`。\n\n#### 注意\n\n旨在测试单个功能的程序有时被称为**驱动程序**(不要与设备驱动程序混淆)。\n\n执行以下步骤完成本练习:\n\n1.  创建一个名为 **Snippet1.cpp** 的文件。\n2.  定义一个名为`sumVector`的函数，该函数对循环中的每个元素进行求和:\n\n    ```cpp\n    int sumVector(std::vector<int> &v)\n    {\n      int ret = 0;\n      for(int i: v)\n      {\n        ret += i;\n      }\n\n      return ret;\n    }\n    ```\n\n3.  定义`主`功能。使用 C++ 11 随机数生成工具初始化`10000`元素的向量，然后调用`sumVector`函数`1000`次。编写以下代码来实现这一点:\n\n    ```cpp\n    #include <random>\n    #include <iostream>\n    int main()\n    {\n      // Initialize a random number generator\n      std::random_device dev;\n      std::mt19937 rng(dev());\n      // Create a distribution range from 0 to 1000\n      std::uniform_int_distribution<std::mt19937::result_type> dist(0,1000); \n      // Fill 10000 numbers in a vector\n      std::vector<int> v;\n      v.reserve(10000);\n      for(int i = 0; i < 10000; ++ i)\n      {\n        v.push_back(dist(rng));\n      }\n      // Call out function 1000 times, accumulating to a total sum\n      double total = 0.0;\n      for(int i = 0; i < 1000; ++ i)\n      {\n        total += sumVector(v);\n      }\n      std::cout << \"Total: \" << total << std::endl;\n    }\n    ```\n\n4.  Compile, run, and time this program on a Linux Terminal using the following commands:\n\n    ```cpp\n    $ g++ Snippet1.cpp\n    $ time ./a.out\n    ```\n\n    前一个命令的输出如下:\n\n    ![Figure 8.12: Output of timing the Snippet1.cpp code ](img/C14583_08_12.jpg)\n\n    ###### 图 8.12:snippet 1 . CPP 代码的输出时序\n\n    从前面的输出可以看出，对于这个系统，程序在`0.122`秒内执行(注意，结果会有所不同，这取决于您的系统配置)。如果我们重复运行这个定时命令，结果可能会略有变化，因为程序将在第一次运行后加载到内存中，速度会稍微快一些。最好将程序运行并计时约`5`次，得到一个平均值。我们通常对花费时间的绝对值不感兴趣，而是对优化代码时该值如何提高感兴趣。\n\n5.  Use the following commands to explore the effect of using compiler optimization flags:\n\n    ```cpp\n    $ g++ -O3 Snippet1.cpp\n    $ time ./a.out\n    ```\n\n    输出如下:\n\n    ![Figure 8.13: Output of timing the Snippet1.cpp code compiled with -O3 ](img/C14583_08_13.jpg)\n\n    ###### 图 8.13:用-O3 编译的 Snippet1.cpp 代码的输出时序\n\n    从前面的输出来看，程序似乎变得快了大约`60`倍，这似乎相当不可思议。\n\n6.  更改代码以执行循环`100，000 次`而不是`1，000 次`:\n\n    ```cpp\n    // Call out function 100000 times\n    for(int i = 0; i < 100000; ++ i)\n    {\n      total += sumVector(v);\n    }\n    ```\n\n7.  Recompile and time again using the following commands:\n\n    ```cpp\n    $ g++ -O3 Snippet1.cpp\n    $ time ./a.out\n    ```\n\n    执行上一个命令后的输出如下:\n\n    ![Figure 8.14: Output of timing the Snippet1.cpp code with 10,000 iterations ](img/C14583_08_14.jpg)\n\n    ###### 图 8.14:10，000 次迭代的 Snippet1.cpp 代码定时输出\n\n    从前面的输出来看，似乎仍然需要完全相同的时间。这似乎是不可能的，但实际上发生的是，由于我们从未在程序中造成任何副作用，如打印总和，编译器可以自由地用空程序替换我们的代码。在功能上，根据 C++ 标准，这个程序和一个空程序是一样的，因为运行它没有副作用。\n\n8.  Open the Compiler Explorer and paste in the entire code. Set the compiler options to `-O3` and observe the generated code:\n\n    ![Figure 8.15: Snippet1.cpp code in Compiler Explorer ](img/C14583_08_15.jpg)\n\n    ###### 图 8.15:编译器资源管理器中的 Snippet1.cpp 代码\n\n    从前面的截图中可以看到，循环的**内的行没有颜色编码，也没有为它们生成汇编代码。**\n\n9.  更改代码以确保必须通过打印一个值来执行求和，该值取决于以下行的计算:\n\n    ```cpp\n    std::cout<<\"Total:\"<<total<<std::endl;\n    ```\n\n10.  Here, we are just summing the result of `sumVector()` to a dummy double value many time and printing it. After you make the changes in the code, open the Terminal and write the following commands:\n\n    ```cpp\n    $ g++ -O3 Snippet1.cpp\n    $ time ./a.out\n    ```\n\n    前面命令的输出如下:\n\n![Figure 8.16: Output of timing the Snippet1.cpp code with a side effect of printing the value ](img/C14583_08_16.jpg)\n\n###### 图 8.16:输出 Snippet1.cpp 代码的时序，附带打印值的副作用\n\n在前面的输出中，我们可以看到程序实际上执行了计算，而不仅仅是作为一个空程序运行。将总数打印到`cout`是一个副作用，会导致编译器不省略代码。导致依赖于代码执行的副作用(如打印结果)是防止编译器优化器删除代码的一种方法。在下一节中，我们将学习如何在没有副作用的情况下给程序计时。\n\n### 定时节目无副作用\n\n正如在前面的练习中看到的，我们需要在程序中创建一个副作用(使用`cout`)，这样编译器就不会忽略我们编写的所有代码。让编译器相信一段代码有副作用的另一种技术是将其结果赋给一个**易变的**变量。volatile 限定符告诉编译器:“这个变量必须总是从内存中读取并写入内存，而不是从寄存器中读取。”易失性变量的主要目的是访问设备内存，这种设备内存访问必须遵循上述规则。实际上，编译器认为易变变量就像它们可以从当前程序之外的效果中改变一样，因此永远不会被优化。我们将在接下来的章节中使用这种技术。\n\n有更高级的方法可以绕过这个问题，即通过向编译器指定特殊的汇编代码指令，而不是使用副作用。但是它们不在本介绍材料的范围内。对于下面的示例，我们将始终添加代码，以确保函数的结果用于副作用或被分配给可变变量。在以后的章节中，我们将学习如何检查编译器生成的汇编代码，并在编译器出于优化目的而省略代码时检测实例。\n\n### 源代码工具\n\n**插装**是一个术语，指的是在不改变程序行为的情况下，向程序添加额外代码，并在程序执行时捕获信息的过程。这可能包括性能计时(以及可能的其他度量，如内存分配或磁盘使用模式)。在源代码插装的情况下，我们手动添加代码来确定程序的执行时间，并在程序结束时记录这些数据以供分析。这种方法的优点是它的可移植性和避免任何外部工具。它还允许我们选择性地将时序添加到我们选择的代码的任何部分。\n\n### 练习 2:编写代码计时器类\n\n在本练习中，我们将创建一个`RAII`类，它允许我们测量单个代码块的执行时间。在接下来的练习中，我们将把它作为代码的主要计时机制。它不像其他性能测量方法那样复杂，但是更容易使用，并且服务于大多数目的。我们班的基本要求如下:\n\n*   我们需要能够记录一段代码所花费的累计时间。\n*   我们需要能够记录它被调用的次数。\n\n执行以下步骤完成本练习:\n\n1.  创建一个名为 **Snippet2.cpp** 的文件。\n2.  包括以下标题:\n\n    ```cpp\n    #include <map>\n    #include <string>\n    #include <chrono>\n    #include <iostream>\n    #include <cstdint> \n    using std::map;\n    using std::string;\n    using std::cerr;\n    using std::endl;\n    ```\n\n3.  Define the `Timer` class and the class member functions by writing the following code:\n\n    ```cpp\n    class Timer\n    {\n      static map<string, int64_t> ms_Counts;\n      static map<string, int64_t> ms_Times;\n      const string &m_sName;\n      std::chrono::time_point<std::chrono::high_resolution_clock> m_tmStart;\n    ```\n\n    从前面的代码中可以看到，类成员由一个名称、一个起始时间戳和两个`静态映射`组成。这个类的每个实例都意味着对某个代码块计时。该块可以是一个函数范围，也可以是由花括号分隔的任何其他块。使用模式是在块的顶部定义一个`定时器`类的实例，同时传入一个名称(可以是一个函数名或一些其他方便的标签)。实例化时，记录当前时间戳，当块退出时，该类的析构函数记录该块的累计运行时间，以及该块的执行次数。时间和计数分别存储在静态地图`毫秒时间`和`毫秒计数`中。\n\n4.  通过编写以下代码来定义`定时器`类的构造函数:\n\n    ```cpp\n    public:\n      // When constructed, save the name and current clock time\n      Timer(const string &sName): m_sName(sName)\n      {\n        m_tmStart = std::chrono::high_resolution_clock::now();\n      }\n    ```\n\n5.  Define the destructor of the `Timer` class by writing the following code:\n\n    ```cpp\n      // When destroyed, add the time elapsed and also increment the count under this name\n      ~Timer()\n      {\n        auto tmNow = std::chrono::high_resolution_clock::now();\n        auto msElapsed = std::chrono::duration_cast<std::chrono::milliseconds>(tmNow - m_tmStart);\n        ms_Counts[m_sName]++ ;\n        ms_Times[m_sName] += msElapsed.count();\n      }\n    ```\n\n    在前面的代码中，经过的时间以毫秒为单位计算。然后，我们将它添加到这个块名的累计运行时间中，并增加这个块被执行的次数。\n\n6.  Define a `static` function named `dump()` that prints out the summary of the timed results:\n\n    ```cpp\n      // Print out the stats for each measured block/function\n      static void dump()\n      {\n        cerr << \"Name\\t\\t\\tCount\\t\\t\\tTime(ms)\\t\\tAverage(ms)\\n\";\n        cerr << \"-----------------------------------------------------------------------------------\\n\";\n        for(const auto& it: ms_Times)\n        {\n          auto iCount = ms_Counts[it.first];\n          cerr << it.first << \"\\t\\t\\t\" << iCount << \"\\t\\t\\t\" << it.second << \"\\t\\t\\t\" << it.second / iCount << \"\\n\";\n        }\n      }\n    };\n    ```\n\n    在前面的代码中，名称、执行计数、总时间和平均时间以表格形式打印。我们在字段名称和字段值之间使用多个选项卡，使它们在控制台上垂直排列。这个功能可以按照我们的意愿修改。例如，我们可以修改这段代码，将输出转储为 CSV 文件，这样就可以将其导入到电子表格中进行进一步分析。\n\n7.  最后定义`静态`成员完成类:\n\n    ```cpp\n    // Define static members\n    map<string, int64_t> Timer::ms_Counts;\n    map<string, int64_t> Timer::ms_Times;\n    const int64_t N = 1'000'000'000;\n    ```\n\n8.  Now that we have defined the `Timer` class, define two simple functions that we will time as an example. One will add and the other will multiply. Since these operations are trivial, we will loop `1 billion times` so that we can have some measurable result.\n\n    #### 注意\n\n    C++ 14 及更高版本让我们在整数常量中使用单引号符号来提高可读性；比如我们可以写`1'000'000`而不是`1000000`。\n\n    写出加法和乘法的函数。这两个函数只需分别将整数`1`和`N`相加并相乘:\n\n    ```cpp\n    unsigned int testMul()\n    {\n      Timer t(\"Mul\");\n\n      unsigned int x = 1;\n      for(int i = 0; i < N; ++ i)\n      {\n        x *= i;\n      }\n\n      return x;\n    }\n    unsigned int testAdd()\n    {\n      Timer t(\"Add\");\n\n      unsigned int x = 1;\n      for(int i = 0; i < N; ++ i)\n      {\n        x += i;\n      }\n\n      return x;\n    }\n    ```\n\n    在前面的代码中，我们使用`无符号整数`作为变量，我们重复`加` / `乘`。我们使用了无符号类型，这样在算术运算中溢出就不会导致未定义的行为。如果我们使用签名类型，程序将有未定义的行为，并且不能保证以任何方式工作。其次，我们从`test DD()`和`testMul()`函数返回计算值，这样我们就可以确保编译器不会删除代码(因为没有副作用)。为了给这些函数计时，我们只需要在函数的开始用合适的标签声明一个`定时器`类的实例。一旦`定时器`对象被实例化，计时就开始，当该对象超出范围时计时停止。\n\n9.  Write the `main` function, where we will simply call both test functions `10` times each:\n\n    ```cpp\n    int main()\n    {\n      volatile unsigned int dummy;\n      for(int i = 0; i < 10; ++ i)\n        dummy = testAdd();\n      for(int i = 0; i < 10; ++ i)\n        dummy = testMul();\n      Timer::dump();\n    }\n    ```\n\n    正如您在前面的代码中看到的，我们调用每个函数`10 次`，这样我们就可以演示`定时器`类对一个函数的多次运行进行计时。将函数的结果赋给易失性变量会迫使编译器假定存在全局副作用。因此，它不会省略我们测试函数中的代码。退出前，我们调用`定时器::转储`静态功能显示结果。\n\n10.  Save the program and open a terminal. Compile and run the program with different optimization levels – on the `gcc` and `clang` compilers, this is specified by the `-ON` compiler flag, where `N` is a number from `1` to `3`. Add the `-O1` compiler flag first:\n\n    ```cpp\n    $ g++ -O1 Snippet2.cpp && ./a.out\n    ```\n\n    此代码生成以下输出:\n\n    ![Figure 8.17: Snippet2.cpp code performance when compiled with the -O1 option  ](img/C14583_08_17.jpg)\n\n    ###### 图 8.17:使用-O1 选项编译时的 Snippet2.cpp 代码性能\n\n11.  Now, add the `-O2` compiler flag in the terminal and execute the program:\n\n    ```cpp\n    $ g++ -O2 Snippet2.cpp && ./a.out\n    ```\n\n    这将生成以下输出:\n\n    ![Figure 8.18: Snippet2.cpp code performance when compiled with the -O2 option  ](img/C14583_08_18.jpg)\n\n    ###### 图 8.18:使用-O2 选项编译时的 Snippet2.cpp 代码性能\n\n12.  Add the `-O3` compiler flag in the terminal and execute the program:\n\n    ```cpp\n    $ g++ -O3 Snippet2.cpp && ./a.out\n    ```\n\n    这将生成以下输出:\n\n    ![Figure 8.19: Snippet2.cpp code performance when compiled with the -O3 option  ](img/C14583_08_19.jpg)\n\n    ###### 图 8.19:使用-O3 选项编译时的 Snippet2.cpp 代码性能\n\n    请注意，`testMul`功能仅在`O3`变得更快，但是`testdd`功能在`O2`变得更快，在`O3`变得更快。我们可以通过多次运行程序并平均时间来验证这一点。没有明显的理由说明为什么有些功能会加速，而有些则不会。我们必须彻底检查生成的代码，以了解原因。不能保证这种情况会发生在所有具有不同编译器甚至编译器版本的系统上。要记住的要点是，我们永远不能假设性能，但必须始终衡量它，如果我们认为我们所做的任何更改都会影响性能，则始终重新衡量。\n\n13.  为了更容易地使用我们的`定时器`类来为单个函数计时，我们可以编写一个宏。C++ 11 及更高版本支持一个名为`__func__`的特殊编译器内置宏，该宏始终包含当前正在执行的函数名作为`const char*`。用它来定义一个宏，这样我们就不需要为我们的`定时器`实例指定一个标签，如下所示:\n\n    ```cpp\n    #define TIME_IT Timer t(__func__)\n    ```\n\n14.  将`TIME_IT`宏添加到这两个函数的开头，更改创建定时器对象的现有行:\n\n    ```cpp\n    unsigned int testMul()\n    {\n      TIME_IT;\n    unsigned int testAdd()\n    {\n      TIME_IT;\n    ```\n\n15.  Save the program and open the terminal. Compile and run it again by using the following command:\n\n    ```cpp\n    $ g++ -O3 Snippet2.cpp && ./a.out\n    ```\n\n    前一个命令的输出如下:\n\n![Figure 8.20: Snippet2.cpp code output when using a macro for timing ](img/C14583_08_20.jpg)\n\n###### 图 8.20:使用宏计时时的 Snippet2.cpp 代码输出\n\n在前面的输出中，请注意现在已经打印了实际的函数名。使用这个宏的另一个优点是，我们可以在默认情况下将它添加到所有潜在的耗时函数中，并通过简单地将定义更改为 no-op 来禁用它，这将导致计时代码永远不会运行，从而避免了大量编辑代码的需要。在接下来的练习中，我们将使用相同的 Timer 类进行计时编码。\n\n## 运行时分析\n\n**剖析**是一种测量程序中功能性能的非侵入式方法。剖析器的工作方式是以频繁的时间间隔(每秒数百次)对程序的当前执行地址进行采样，并记录当时正在执行的函数。这是一种统计抽样方法，具有合理的准确性。但是，有时结果可能会令人困惑，因为程序可能会在作为操作系统内核一部分的函数上花费大量时间。Linux 上最流行的运行时分析工具是 **perf** 。在下一节中，我们将使用 perf 来描述我们的程序。\n\n### 练习 3:使用性能分析程序\n\n`perf`可以安装在`Ubuntu`上，如下所示:\n\n```cpp\napt-get install linux-tools-common linux-tools-generic\n```\n\n为了熟悉使用`perf`的基础知识，我们将借助`perf`工具对上一练习中的程序进行分析。执行以下步骤完成本练习:\n\n1.  打开我们在上一个练习中创建的 **Snippet2.cpp** 文件，从两个函数中删除`TIME_IT`宏。\n2.  Open the terminal, recompile the code again with the `-O3` flag, and then create a profile data sample with `perf` as follows:\n\n    ```cpp\n    $ g++ -O3 Snippet2.cpp\n    $ perf record ./a.out\n    ```\n\n    前一个命令的输出如下:\n\n    ![](img/C14583_08_21.jpg)\n\n    ###### 图 8.21:使用 perf 命令分析 Snippet2.cpp 中的代码\n\n    这将创建一个名为`性能数据`的文件，可以对其进行分析或可视化。\n\n3.  Now, use the following command to visualize the recorded data:\n\n    ```cpp\n    $ perf report\n    ```\n\n    执行上一个命令后，基于控制台的图形用户界面将显示以下数据:\n\n    ![Figure 8.22: Using the perf command to analyze the code in Snippet2.cpp ](img/C14583_08_22.jpg)\n\n    ###### 图 8.22:使用 perf 命令分析 Snippet2.cpp 中的代码\n\n    您可以上下移动光标选择一个功能，然后按*进入*获取选项列表。\n\n4.  突出显示`测试程序`，按*进入*，在结果列表中选择`注释测试程序`。显示了一个汇编代码列表，其中的注释描述了每行代码的执行时间百分比，如下所示:\n\n![Figure 8.23: Viewing the timing statistics using the perf command for the Snippet2.cpp code ](img/C14583_08_23.jpg)\n\n###### 图 8.23:使用 Snippet2.cpp 代码的 perf 命令查看时序统计\n\n注意**整数乘法** ( **IMUL)** 指令(整数乘法)占据了`99%`的执行时间。传统上，整数乘法在`x86`架构上总是很昂贵，即使在最新一代的 CPU 中也是如此。该注释视图在每个跳转或分支指令旁边显示箭头，当高亮显示时，显示它与什么比较指令相关联，以及它跳转到什么地址的线图。您可以通过按左箭头键导航到上一个视图，并使用 *q* 键退出程序。\n\n到目前为止，我们已经研究了几种用于评估程序性能的方法。这是优化最关键的阶段，因为它告诉我们需要将我们的努力引向何处。在接下来的章节中，我们将探索各种有助于优化代码的技术。\n\n## 优化策略\n\n代码优化可以通过几种方式完成，例如:\n\n*   基于编译器的优化\n*   源代码微优化\n*   缓存友好代码\n*   算法优化\n\n在这里，每种技术都有其利弊。我们将在接下来的章节中详细研究这些方法。粗略地说，这些是根据所需的努力和性能的潜在提高来排序的。我们将在下一节研究基于编译器的优化。\n\n### 基于编译器的优化\n\n将正确的选项传递给编译器可以带来许多性能上的好处。这方面的一个真实例子是英特尔创建的 Clear Linux **发行版** (Linux 发行版)。编译这个发行版是为了从所有代码中提取最大的性能，并且在大多数基准测试中比大多数其他 Linux 发行版的性能高出 30%，这是一个非常显著的加速。在 **gcc** 和 **clang** 系列编译器上，最基本的优化选项是**-O<N>T7】，其中 **N** 是数字 **1** 、 **2** 或 **3** 之一。 **-O3** 几乎启用了编译器中的所有优化，但还有其他几个未被该标志启用的优化会有所不同。**\n\n### 循环展开\n\n**循环展开**是编译器可以用来减少执行分支数量的技术。每次执行分支时，都会有一定的性能开销。这可以通过多次重复循环体并减少循环的执行次数来减少。程序员可以在源代码级别完成循环展开，但是现代编译器自动完成了非常好的工作。\n\n即使现代处理器通过**分支预测**和**推测执行**电路减轻了分支的开销，循环展开仍然产生性能优势。循环展开优化可以在带有`-funroll-loops`命令行标志的`gcc`和`clang`系列编译器上启用。在下一节中，我们将测试启用和未启用循环展开的程序的性能。\n\n### 练习 4:使用循环展开优化\n\n在本练习中，我们将编写一个使用嵌套循环的简单程序，并在启用和不启用循环展开的情况下测试其性能。我们将理解编译器实现循环自动展开的方式。\n\n执行以下步骤完成本练习:\n\n1.  创建一个名为 **Snippet3.cpp** 的文件。\n2.  写一个程序，取第一个`10，000 个`数字，打印出其中有多少是相互的因素(完整代码可以在 **Snippet3.cpp** 中找到):\n\n    ```cpp\n    # include <iostream>\n    int main()\n    {\n      int ret = 0;\n      for(size_t i = 1; i < 10000; ++ i)\n      {\n        for(size_t j = 1; j < 10000; ++ j)\n        {\n          if(i % j == 0)\n          {\n            ++ ret;\n          }\n        }\n      }\n\n      std::cout << \"Result: \" << ret << std::endl;\n    }\n    ```\n\n3.  Save the program and open the terminal. Compile the program with the `-O3` flag first and time it using the following command:\n\n    ```cpp\n    $ g++ -O3 Snippet3.cpp\n    $ time ./a.out\n    ```\n\n    前一个命令的输出如下:\n\n    ![Figure 8.24: Output of the code in Snippet3.cpp ](img/C14583_08_24.jpg)\n\n    ###### 图 8.24:snippet 3 . CPP 中代码的输出\n\n4.  Now, compile the same code with the loop unrolling enabled and time it again:\n\n    ```cpp\n    $ g++ -O3 -funroll-loops Snippet3.cpp \n    $ time ./a.out \n    ```\n\n    前一个命令的输出如下:\n\n    ![Figure 8.25: Output of the code in Snippet3.cpp compiled with the loop unrolling option  ](img/C14583_08_25.jpg)\n\n    ###### 图 8.25:使用循环展开选项编译的 Snippet3.cpp 中的代码输出\n\n5.  打开`Godbolt 编译器浏览器`，将前面完整的代码粘贴到左侧。\n6.  On the right-hand side, select `x86-64 gcc 8.3` from the compiler options and write the `-O3` flag in the options. Assembly code will be generated. For the for loop, you'll see the following output:\n\n    ![Figure 8.26: Assembly code of the for loop ](img/C14583_08_26.jpg)\n\n    ###### 图 8.26:for 循环的汇编代码\n\n    从前面的截图中，可以清楚地看到`CMP`指令将`RCX`与`10，000`进行比较，后面是条件跳转，`JNE`(不等则跳转)。就在这个代码之后，看到了外环比较，其中`RSI`被比较为`10，000`，随后是另一个条件跳转到`L4`标签。总的来说，内部条件分支和跳转执行`100，000，000`次。\n\n7.  现在，添加以下选项:`-O3–funroll-loops`。将生成汇编代码。在这段代码中，您会注意到这个代码模式重复了八次(除了`LEA`指令，其偏移值发生了变化):\n\n![Figure 8.27: Assembly code of the for loop ](img/C14583_08_27.jpg)\n\n###### 图 8.27:for 循环的汇编代码\n\n编译器决定将循环体展开八次，将条件跳转指令的执行次数减少了`87.5%`倍(约为`830 万`次)。仅这一项就使执行时间提高了`10%`，这是一个非常显著的加速。在本练习中，我们已经看到了循环展开的好处——接下来，我们将学习概要文件引导的优化。\n\n### 轮廓导向优化\n\n**配置文件引导优化** (PGO)是大多数编译器都支持的功能。当在启用 PGO 的情况下编译程序时，编译器会向程序中添加插装代码。运行此支持 PGO 的可执行文件会创建一个日志文件，其中包含程序执行统计信息。术语**剖析**指的是运行程序以收集性能指标的过程。通常，此分析阶段应该使用真实数据集运行，以便生成准确的日志。在这个分析运行之后，用一个特殊的编译器标志重新编译程序。此标志使编译器能够基于记录的统计执行数据执行特殊优化。使用这种方法可以获得显著的性能提升。让我们解决一个基于轮廓引导优化的练习，以更好地理解这一点。\n\n### 练习 5:使用轮廓引导优化\n\n在本练习中，我们将对上一练习中的代码使用概要文件引导优化。我们将了解如何使用`gcc`编译器进行配置文件引导优化。\n\n执行以下步骤完成本练习:\n\n1.  打开终端，在启用分析的情况下编译上一练习中的代码。包括我们需要的任何其他优化标志(在本例中为`-O3`)。编写以下代码来实现这一点:\n\n    ```cpp\n    $ g++ -O3 -fprofile-generate Snippet3.cpp\n    ```\n\n2.  Now, run the profiled version of the code by writing the following command:\n\n    ```cpp\n    $ ./a.out\n    ```\n\n    程序正常运行并打印结果，看不到其他输出-但是它生成一个包含数据的文件，这些数据将在下一步帮助编译器。请注意，在启用分析的情况下，程序的执行速度比正常情况下慢几倍。这是大型程序需要记住的。执行完上一条命令后，会生成一个名为`Snippet3.gcda`的文件，其中包含概要数据。在使用大型复杂应用时，使用生产环境中最常见的数据集和工作流来运行程序非常重要。通过在这里正确选择数据，最终的性能增益将会更高。\n\n3.  Recompile with the PGO optimization flags, that is, `-fprofile-use` and`-fprofile-correction`, as illustrated in the following code:\n\n    ```cpp\n    $ g++ -O3 -fprofile-use -fprofile-correction Snippet3.cpp\n    ```\n\n    请注意，除了与配置文件相关的编译器选项之外，其他选项必须与上一编译步骤中的选项完全相同。\n\n4.  Now, if we time the executable, we will see a large performance improvement:\n\n    ```cpp\n    $ time ./a.out\n    ```\n\n    前一个命令的输出如下:\n\n![Figure 8.28: Timing results of the code in Snippet3.cpp with PGO optimization ](img/C14583_08_28.jpg)\n\n###### 图 8.28:带有 PGO 优化的 Snippet3.cpp 中代码的计时结果\n\n在本练习中，我们已经看到了使用编译器提供的配置文件引导优化所获得的性能优势。对于这段代码，性能的提高大约是`2.7x` -在更大的程序上，这可能会更高。\n\n### 并行化\n\n如今大多数 CPU 都有多核，甚至手机也有四核处理器。我们可以非常简单地利用编译器标志来利用这种并行处理能力，编译器标志指示它生成并行代码。代码并行化的一种机制是使用 C/C++ 语言的`OpenMP`扩展。然而，这意味着要改变源代码，并详细了解如何使用这些扩展。另一个更简单的选项是`gcc`编译器特有的特性——它提供了一个扩展的标准库，实现了大多数并行运行的算法。\n\n#### 注意\n\n这种自动并行化只适用于 gcc 上的 STL 算法，不是 C++ 标准的一部分。C++ 17 标准为大多数算法的并行版本提出了标准库的扩展，但是还没有被所有的编译器支持。此外，为了利用这一特性，必须对代码进行大量重写。\n\n### 练习 6:使用编译器并行化\n\n在本练习中，我们将使用`gcc`并行扩展功能来加速标准库函数。我们的目的是了解如何使用`gcc`并行扩展。\n\n执行以下步骤完成本练习:\n\n1.  创建一个名为 **Snippet4.cpp** 的文件。\n2.  写一个简单的程序，用`STD::aggregate 对一个初始化的数组求和。`添加以下代码来实现:\n\n    ```cpp\n    #include <vector>\n    #include <string>\n    #include <iostream>\n    #include <algorithm>\n    #include <numeric>\n    #include <cstdint> \n    using std::cerr;\n    using std::endl;\n    int main()\n    {\n      // Fill 100,000,000 1s in a vector\n      std::vector<int> v( 100'000'000, 1);\n      // Call accumulate 100 times, accumulating to a total sum\n      uint64_t total = 0;\n      for(int i = 0; i < 100; ++ i)\n      {\n        total += std::accumulate(v.begin(), v.end(), 0);\n      }\n      std::cout << \"Total: \" << total << std::endl;\n    }\n    ```\n\n3.  Save the program and open the terminal. Compile the program normally and time the execution using the following commands:\n\n    ```cpp\n    $ g++ -O3 Snippet4.cpp\n    $ time ./a.out\n    ```\n\n    前一个命令的输出如下:\n\n    ![Figure 8.29: Output of the code in Snippet4.cpp ](img/C14583_08_29.jpg)\n\n    ###### 图 8.29:snippet 4 . CPP 中代码的输出\n\n4.  Now, compile the code with the parallelization options, that is, `-O3 -fopenmp` and`-D_GLIBCXX_PARALLEL`:\n\n    ```cpp\n    $ g++ -O3 -fopenmp -D_GLIBCXX_PARALLEL Snippet4.cpp\n    $ time ./a.out\n    ```\n\n    输出如下:\n\n![Figure 8.30: Output of the code in Snippet4.cpp compiled with parallelization options ](img/C14583_08_30.jpg)\n\n###### 图 8.30:使用并行化选项编译的 Snippet4.cpp 中的代码输出\n\n在之前的输出中，`用户`字段显示累计的 CPU 时间，`真实`字段显示墙时间。两者之间的比例约为`7x`。这个比例会有所不同，取决于系统有多少个中央处理器内核(在这个特定的例子中，有八个内核)。对于这个系统，如果编译器能够执行`100%`并行化，这个比例可以达到 8 倍。请注意，即使使用了八个内核，执行时间的实际提升也只是`的 1.3x`左右。这可能是因为向量的分配和初始化占用了大部分时间。这是**阿姆达尔定律**的一个案例，它说当可并行部分被并行化时，程序的串行部分支配执行时间。尽管如此，我们在代码中还是获得了`1.3x`的加速，这是一个非常好的优化结果。\n\n到目前为止，我们已经介绍了现代编译器中一些更有影响力的编译器优化特性。除此之外，还有其他几个优化标志，但它们可能不会对性能产生很大的改善。适用于具有许多不同源文件的大型项目的两个特定优化标志是**链接时间优化**或**链接时间代码生成**。这些都值得大型项目启用。在下一节中，我们将研究源代码微优化。\n\n### 源代码微优化\n\n这些技术涉及到在源代码中使用特定的习惯用法和模式，这些习惯用法和模式通常比它们的等价物更快。在早期，这种微优化非常有成效，因为编译器不是很聪明。但是今天，编译器技术已经非常先进了，这些微优化的效果并没有那么显著。尽管如此，使用这些是一个非常好的习惯，因为即使在没有优化的情况下编译，它们也会使代码更快。即使在开发构建中，更快的代码也能节省测试和调试的时间。我们将在下一节中查看 std::vector 容器:\n\n### 高效使用标准::矢量容器\n\n`std::vector`是标准库中最简单、最有用的容器之一。与普通的 C 风格数组相比，它没有开销，但是有增长的能力，以及可选的边界检查。当编译时不知道元素的数量时，您几乎应该总是使用`std::vector`。\n\n与`std::vector`一起使用的一个常见习惯用法是在循环中调用其上的`push _ back`-随着它的增长，vector 会重新分配一个新的缓冲区，该缓冲区比现有缓冲区大一定的因子(该增长因子的确切值取决于标准库的实现)。理论上，这种重新分配的成本最小，因为它很少发生，但实际上，在向量中调整大小的操作涉及将其缓冲区的元素复制到新分配的更大缓冲区，这可能非常昂贵。\n\n我们可以使用`reserve()`方法来避免这些多次分配和复制。当我们知道一个向量将包含多少元素时，调用`reserve()`方法来预分配存储会有很大的不同。让我们在下一节中实现一个优化向量增长的练习。\n\n### 练习 7:优化矢量增长\n\n在本练习中，我们将循环对`push_back`方法的效果进行计时，无论是否调用 reserve 方法。首先，我们将把前面几节中使用的`定时器`类提取到一个单独的头文件和实现文件中——这将允许我们将其用作所有后续代码片段的公共代码。执行以下步骤完成本练习:\n\n1.  创建一个名为 **Timer.h** 的头文件。\n2.  包括必要的头文件:\n\n    ```cpp\n    #include <map>\n    #include <string>\n    #include <chrono>\n    #include <cstdint>\n    ```\n\n3.  创建一个名为`定时器`的类。在`定时器`类中，声明四个变量，即`ms_Counts`、`ms_Times`、`m_tmStart`和`m_sName`。声明一个构造函数、析构函数和`转储()`方法。添加以下代码来实现:\n\n    ```cpp\n    class Timer\n    {\n      static std::map<std::string, int64_t> ms_Counts;\n      static std::map<std::string, int64_t> ms_Times;\n      std::string m_sName;\n      std::chrono::time_point<std::chrono::high_resolution_clock> m_tmStart;\n      public:\n        // When constructed, save the name and current clock time\n        Timer(std::string sName);\n        // When destroyed, add the time elapsed and also increment the count under this name\n        ~Timer();\n        // Print out the stats for each measured block/function\n        static void dump();\n    };\n    ```\n\n4.  通过编写以下代码，为时间函数定义一个名为`时间信息`的辅助宏:\n\n    ```cpp\n    // Helper macro to time functions\n    #define TIME_IT Timer t(__func__)\n    ```\n\n5.  一旦创建了头文件，创建一个名为 **Timer.cpp** 的新文件，并在其中包含 **Timer.h** 文件。另外，在 **Timer.cpp** 文件中编写构造函数、析构函数和`dump()`方法的实际实现。编写以下代码来实现这一点:\n\n    ```cpp\n    #include <string>\n    #include <iostream>\n    #include <cstdint> \n    #include \"Timer.h\"\n    using std::map;\n    using std::string;\n    using std::cerr;\n    using std::endl;\n    // When constructed, save the name and current clock time\n    Timer::Timer(string sName): m_sName(sName)\n    {\n      m_tmStart = std::chrono::high_resolution_clock::now();\n    }\n    // When destroyed, add the time elapsed and also increment the count under this name\n    Timer::~Timer()\n    {\n      auto tmNow = std::chrono::high_resolution_clock::now();\n      auto msElapsed = std::chrono::duration_cast<std::chrono::milliseconds>(tmNow - m_tmStart);\n      ms_Counts[m_sName]++ ;\n      ms_Times[m_sName] += msElapsed.count();\n    }\n    // Print out the stats for each measured block/function\n    void Timer::dump()\n    {\n      cerr << \"Name\\t\\t\\tCount\\t\\t\\tTime(ms)\\t\\tAverage(ms)\\n\";\n      cerr << \"-----------------------------------------------------------------------------------\\n\";\n      for(const auto& it: ms_Times)\n      {\n        auto iCount = ms_Counts[it.first];\n        cerr << it.first << \"\\t\\t\\t\" << iCount << \"\\t\\t\\t\" << it.second << \"\\t\\t\\t\" << it.second / iCount << \"\\n\";\n      }\n    }\n    // Define static members\n    map<string, int64_t> Timer::ms_Counts;\n    map<string, int64_t> Timer::ms_Times;\n    ```\n\n6.  现在，创建一个名为 **Snippet5.cpp** 的新文件，并使用`push_back()`方法编写两个简单地用第一个`1，000，000`整数填充向量的函数。第二个函数预先调用`reserve()`方法，但第一个函数没有。编写以下代码来实现这一点:\n\n    ```cpp\n    #include <vector>\n    #include <string>\n    #include <iostream>\n    #include \"Timer.h\"\n    using std::vector;\n    using std::cerr;\n    using std::endl;\n    const int N = 1000000;\n    void withoutReserve(vector<int> &v)\n    {\n      TIME_IT;\n      for(int i = 0; i < N; ++ i)\n      {\n        v.push_back(i);\n      }\n    }\n    void withReserve(vector<int> &v)\n    {\n      TIME_IT;\n      v.reserve(N);\n      for(int i = 0; i < N; ++ i)\n      {\n        v.push_back(i);\n      }\n    }\n    ```\n\n7.  Now, write the `main` function. Note the use of redundant braces to ensure that the `v1` and `v2` vectors are destroyed after every iteration of the loop:\n\n    ```cpp\n    int main()\n    {\n      {\n        vector<int> v1;\n        for(int i = 0; i < 100; ++ i)\n        {\n          withoutReserve(v1);\n        }\n      }\n      {\n        vector<int> v2;\n        for(int i = 0; i < 100; ++ i)\n        {\n          withReserve(v2);\n        }\n      }\n      Timer::dump();\n    }\n    ```\n\n    我们通过引用传递向量的原因是为了防止编译器优化两个函数中的整个代码。如果我们按值传递向量，函数将没有可见的副作用，编译器可能会完全省略函数。\n\n8.  Save the program and open the terminal. Compile the **Timer.cpp** and **Snippet5.cpp** files and run them as follows:\n\n    ```cpp\n    $ g++ -O3 Snippet5.cpp Timer.cpp\n    $ ./a.out\n    ```\n\n    输出如下:\n\n![Figure 8.31: Output of the code in Snippet5.cpp showing the effect of vector::reserve() ](img/C14583_08_31.jpg)\n\n###### 图 8.31:snippet 5 . CPP 中代码的输出，显示了 vector::reserve()的效果\n\n我们可以看到，调用`reserve()`的效果导致执行时间提高了 4%左右。在运行了很长时间的大型程序中，系统内存通常会变得非常碎片化。在这种情况下，通过使用`reserve()`预分配内存的改进可能会更好。一般来说，提前预留内存通常比动态增量快。即使是 Java 虚拟机，出于性能原因，在启动时也使用这种提前分配大量内存的技术。\n\n### 短路逻辑运算符\n\n`&&``| |`逻辑运算符为**短路**，即:\n\n*   如果`||`运算符的左侧为`真`，则右侧不被评估。\n*   如果左侧的`& &`算子为`假`，则右侧不评价。\n\n通过将更不可能(或更便宜)的表达式保留在左侧，我们可以减少需要完成的工作量。在下一节中，我们将解决一个练习，并学习如何最佳地编写逻辑表达式。\n\n### 练习 8:优化逻辑运算符\n\n在本练习中，我们将研究与逻辑运算符一起使用时对条件表达式进行排序的影响。执行以下步骤完成本练习:\n\n1.  创建一个名为 **Snippet6.cpp** 的新文件。\n2.  包括必要的库和 Timer.h 文件，我们在前面的练习中通过编写以下代码创建了该文件:\n\n    ```cpp\n    #include <vector>\n    #include <string>\n    #include <iostream>\n    #include <random>\n    #include \"Timer.h\"\n    using std::vector;\n    using std::cerr;\n    using std::endl;\n    ```\n\n3.  定义一个名为`sum1()`的函数，计算`0`和`N`之间的整数之和。每个数字只有在满足一个或两个特定标准时才会求和。第一个条件是数量必须小于`N/2`。第二个条件是，当数除以 3 时，必须返回 2 作为余数。在这里，我们将`N`设置为`100，000，000`，这样代码就有了一些可测量的时间。编写以下代码来实现这一点:\n\n    ```cpp\n    const uint64_t N = 100000000;\n    uint64_t sum1()\n    {\n      TIME_IT;\n      uint64_t ret = 0;\n      for(uint64_t b=0; b < N; ++ b)\n      {\n        if(b % 3 == 2 || b < N/2)\n        {\n          ret += b;\n        }\n      }\n\n      return ret;\n    }\n    ```\n\n4.  Now, define another function named `sum2()`. It must contain the same logic that we wrote for the previous function, `sum1()`. The only change here is that we reverse the order of the conditional expression of the `if` statement. Write the following code to implement this:\n\n    ```cpp\n    uint64_t sum2()\n    {\n      TIME_IT;\n      uint64_t ret = 0;\n      for(uint64_t b=0; b < N; ++ b)\n      {\n        if(b < N/2 || b % 3 == 2)\n        {\n        ret += b;\n        }\n      }\n\n      return ret;\n    }\n    ```\n\n    请注意，在`sum2`功能中，`b < N/2`条件将在一半时间内评估为真。因此，第二个条件，即`b % 3 == 2`，只对一半的迭代进行评估。为了简单起见，如果我们假设两个条件都花费 1 个时间单位，则`sum2()`花费的总时间将是`N/2 + (2 * N/2) = N * 3/2`。在`sum1()`功能的情况下，左侧的条件评估为`真的`只有 33%的时间，其余 66%的时间，两个条件都会评估。因此，估计花费的时间为`N/3 + (2 * N * 2/3) = N * 5/3`。我们预计`sum1`和`sum2`功能之间的时间比例将为`5/3`到`3/2`–即`sum1`比`慢 11%`。\n\n5.  在主功能中增加如下代码:\n\n    ```cpp\n    int main()\n    {\n      volatile uint64_t dummy = 0;\n      for(int i = 0; i < 100; ++ i)\n      {\n        dummy = sum1();\n      }\n      for(int i = 0; i < 100; ++ i)\n      {\n        dummy = sum2();\n      }\n      Timer::dump();\n    }\n    ```\n\n6.  Save the file and open the terminal. Compile and time the preceding program, as well as the **Timer.cpp** file, by writing the following commands:\n\n    ```cpp\n    $ g++ -O3 Snippet6.cpp Timer.cpp\n    $ ./a.out\n    ```\n\n    输出如下:\n\n![Figure 8.32: Output of the code in Snippet6.cpp showing the effect of optimizing boolean conditions  ](img/C14583_08_32.jpg)\n\n###### 图 8.32:snippet 6 . CPP 中代码的输出，显示了优化布尔条件的效果\n\n从前面的输出可以看出，我们最终在速度上获得了大约`38%`的提升，这比预期的要多得多。为什么会这样？答案是`%`运算符执行整数除法，这比比较昂贵得多，但是编译器不会为`N/2`表达式生成除法指令，因为它是一个常数值。\n\n`sum1()`函数代码对循环的每次迭代执行模运算，整体执行时间由除法支配。综上所述，我们必须始终考虑短路逻辑运算符，并计算表达式的每一侧是如何的，以及它执行了多少次，以便选择它们在表达式中出现的最佳顺序。这相当于做了一个概率论的期望值计算。在下一节中，我们将学习分支预测。\n\n### 分支预测\n\n现代处理器使用流水线架构，这类似于工厂流水线，指令沿着流水线流动，由不同的工人同时处理。在每个时钟周期之后，指令沿着流水线移动到下一级。这意味着，尽管每条指令从开始到结束可能需要许多周期，但总吞吐量是每个周期完成一条指令。\n\n这里的缺点是，如果有一个条件分支指令，中央处理器不知道之后要加载哪组指令(因为有两种可能的选择)。这种情况被称为**流水线停滞**，处理器必须等待直到分支的条件被完全评估，浪费了宝贵的周期。\n\n为了缓解这种情况，现代处理器使用一种叫做**分支预测**的东西——它们试图预测分支的走向。随着分支被遇到的次数越来越多，它对分支可能采取的方式越来越有信心。\n\n尽管如此，CPU 并不是无所不知的，所以如果它开始加载一个预测分支的指令，而后来条件分支变成了另一条路，那么分支之后的整个管道都必须被清除，实际的分支需要从头开始加载。在分支指令下游的“`装配线`”上完成的所有工作都必须丢弃，任何更改都需要反转。\n\n这是性能的一个主要瓶颈，可以避免——最简单的方法是确保分支总是尽可能地往一个方向走——就像一个循环。\n\n### 练习 9:分支预测的优化\n\n在本练习中，我们将探索和演示 CPU 分支预测对性能的影响。为了探索这一点，我们将在一个程序中编写两个函数——两个函数都使用两个嵌套循环执行相同的计算，这两个循环分别迭代`100`和`100，000，000`次。这两个函数的区别在于，在第一个函数中，外环是较大的一个，而在第二个函数中，外环是较小的一个。\n\n对于第一个函数，外环退出时只失败一次分支预测，但是内环失败分支预测`100，000，000`次–每次退出。对于第二个，同样，外环退出时只失败了一次分支预测，而内环只失败了 100 次分支预测——每次都失败了。这些分支预测失败计数之间的因子`1，000，000`将导致第一个函数比第二个函数慢。执行以下步骤完成本练习:\n\n1.  创建一个名为 **Snippet7.cpp** 的文件，并包含必要的库:\n\n    ```cpp\n    #include <vector>\n    #include <string>\n    #include <iostream>\n    #include <random>\n    #include \"Timer.h\"\n    using std::vector;\n    using std::cerr;\n    using std::endl;\n    ```\n\n2.  Define a function named `sum1()` with a nested loop. The outer `for` loop should cycle `N` times, whereas the inner for loop should iterate `100` times. Set the value of `N` to `100000000`. Write the following code to implement this:\n\n    ```cpp\n    const uint64_t N = 100000000;\n    uint64_t sum1()\n    {\n      TIME_IT;\n      uint64_t ret = 0;\n      for(int i = 0; i < N; ++ i)\n      {\n        for(int j = 0; j < 100; ++ j)\n        {\n          ret += i ^ j;\n        }\n      }\n      return ret;\n    }\n    ```\n\n    如果我们假设处理器预测循环中的分支(统计上，循环末端的分支指令更有可能跳到循环的开始)，那么每次 j 达到`100`–换句话说，`N`次，它就会以错误预测结束。\n\n3.  Define a new function, `sum2()`, with a nested loop. The only change here is that we must set the inner loop count to `N` and the outer loop count to `100`. Add the following code to implement this:\n\n    ```cpp\n    uint64_t sum2()\n    {\n      TIME_IT;\n      uint64_t ret = 0;\n      for(int i = 0; i < 100; ++ i)\n      {\n        for(int j = 0; j < N; ++ j)\n        {\n          ret += i ^ j;\n        }\n      }\n      return ret;\n    }\n    ```\n\n    现在，我们的推理是分支预测失误只发生`100`次。\n\n4.  在主功能中增加如下代码:\n\n    ```cpp\n    int main()\n    {\n      volatile uint64_t dummy;\n      dummy = sum1();\n      dummy = sum2();\n      Timer::dump();\n    }\n    ```\n\n5.  Save the file and open the terminal. Compile the preceding program, along with the **Timer.cpp** file, and time them using the following commands. Remember that you need to have the Timer.cpp and Timer.h files you created earlier in the same directory:\n\n    ```cpp\n    $ g++ -O3 Snippet7.cpp Timer.cpp\n    $ ./a.out\n    ```\n\n    执行前一个命令的输出如下:\n\n![Figure 8.33: Output of the code in Snippet7.cpp showing the effect  of branch prediction optimization ](img/C14583_08_33.jpg)\n\n###### 图 8.33:显示分支预测优化效果的 Snippet7.cpp 中的代码输出\n\n从前面的输出可以看出，有一个小的但肯定是显著的加速，大约为`2%`，这可以归因于处理器能够更好地预测`sum2`函数的分支。在下一节中，我们将探索更多的优化技术。\n\n## 进一步优化\n\n还有其他几种技术可以在您编写代码时实现；它们中的一些并不能保证产生更好的代码，但是改变你的编码习惯来反射性地做到这些，只需要很少的努力。它们不花钱，但可能会带来收益。其中一些技术如下:\n\n*   尽可能通过`常量`引用传递非基本类型的参数。即使**移动构造函数**可以使复制变得便宜，但它们仍然比使用`常量`引用涉及更多开销。\n*   使用预递增(`++ i`)或预递减(`- i`)运算符，而不是后缀版本。这对于简单类型(如整数)通常没有任何用处，但是对于带有自定义增量运算符的复杂类型可能会有用处。养成写`++ i`而不是`i++ `的习惯是很好的做法，除非后增量实际上是想要的行为。除了性能优势，这样的代码通过使用正确的操作符更清楚地声明意图。\n*   尽可能晚地声明变量——在 C 语言中，在函数顶部声明每个变量是很常见的，但是在 C++ 中，由于变量可以有非平凡的构造函数，因此只在使用它们的实际块中声明它们是有意义的。\n*   就**循环提升**而言，如果一个循环中有任何代码或计算不随循环迭代而变化，将其移出循环是有意义的。这包括在循环体中创建对象。通常，在循环外声明它们一次会更有效。现代编译器会自动完成这项工作，但自己动手并不需要额外的努力。\n*   尽可能使用`常量`。它不会改变代码的含义，但它让编译器对您的代码做出更强的假设，这可能会导致更好的优化。除此之外，使用`const`使代码更加易读和合理。\n*   整数除法、模数和乘法(尤其是不是 2 的幂的数字)是 X86 硬件上最慢的操作。如果你需要在一个循环中执行这样的操作，也许你可以做一些代数操作来摆脱它们。\n\n正如我们提到的，几个这样的优化可能是由编译器自己完成的，但是作为一种习惯，即使在调试模式下，这样做也会使代码快速，这在调试时是一个很大的优势。我们已经研究了一些用于微优化代码的技术——完成这些所需的代码变更级别相对较小，其中一些可以带来效率的重大提高。如果您想要编写更快的代码，您应该致力于随着时间的推移将这些技术集成为默认的编码风格。在下一节中，我们将了解缓存友好代码。\n\n## 缓存友好代码\n\n计算机科学是在 20 世纪中期发展起来的，当时计算机几乎不存在，然而，到了 20 世纪 80 年代，大多数有用的数据结构和算法都被发现和改进了。算法复杂性分析是任何学习计算机科学的人都会遇到的一个话题——对于数据结构运算的复杂性，有很多公认的教科书定义。然而，在这些东西被分析了 50 年后，计算机已经以一种完全不同于想象的方式进化了。例如，一个常见的“事实”是列表数据结构对于插入操作来说比数组更快。这似乎是常识，因为在数组中插入一个元素需要将该元素之后的所有项移动到新的位置，而在列表中插入只是一些指针操作。我们将在下面的练习中检验这个假设。\n\n### 练习 10:探索缓存对数据结构的影响\n\n在本练习中，我们将研究高速缓存对 C++ 标准库中的数组和列表的影响。执行以下步骤完成本练习:\n\n1.  创建一个名为 **Snippet8.cpp** 的文件。\n2.  包括必要的库，以及 **Timer.h** 头文件。编写以下代码来实现这一点:\n\n    ```cpp\n    #include <vector>\n    #include <list>\n    #include <algorithm>\n    #include <string>\n    #include <iostream>\n    #include <random>\n    #include \"Timer.h\"\n    using std::vector;\n    using std::list;\n    using std::cerr;\n    using std::endl;\n    ```\n\n3.  创建一个常量整型变量，`N`，将其值设置为`100000` :\n\n    ```cpp\n    const int N = 100000;\n    ```\n\n4.  初始化一个随机数发生器，创建一个从`0`到`1000`的分布范围。添加以下代码来实现:\n\n    ```cpp\n    std::random_device dev;\n    std::mt19937 rng(dev());\n    std::uniform_int_distribution<std::mt19937::result_type> dist(0,N);\n    ```\n\n5.  创建一个名为`insertRandom()`的方法，将`0`到`N`的元素随机插入容器中。添加以下代码来实现:\n\n    ```cpp\n    template<class C> void insertRandom(C &l)\n    {\n      // insert one element to initialize\n      l.insert(l.end(), 0);\n      for(int i = 0; i < N; ++ i)\n      {\n        int pos = dist(rng) % l.size();\n        auto it = l.begin();\n        advance(it, pos);\n        l.insert(it, i);\n      }\n    }\n    ```\n\n6.  创建一个名为`insertStart()`的方法，并在开始时将从`0`到`N`的元素插入一个容器中。添加以下代码来实现:\n\n    ```cpp\n    template<class C> void insertStart(C &l)\n    {\n      for(int i = 0; i < N; ++ i)\n      {\n        l.insert(l.begin(), i);\n      }\n    }\n    ```\n\n7.  创建一个名为`insertEnd()`的方法，并将从`0`到`N`的元素插入到末端的容器中。添加以下代码来实现:\n\n    ```cpp\n    template<class C> void insertEnd(C &l)\n    {\n      for(int i = 0; i < N; ++ i)\n      {\n        l.insert(l.end(), i);\n      }\n    }\n    ```\n\n8.  在`主`法中写下以下代码:\n\n    ```cpp\n    int main()\n    {\n      std::list<int> l;\n      std::vector<int> v;\n      // list\n      {\n        Timer t(\"list random\");\n        insertRandom(l);\n      }\n\n      {\n        Timer t(\"list end\");\n        insertEnd(l);    \n      }\n      {\n        Timer t(\"list start\");\n        insertStart(l);\n      }\n      // vector\n      {\n        Timer t(\"vect random\");\n        insertRandom(v);\n      }\n\n      {\n        Timer t(\"vect end\");\n        insertEnd(v);    \n      }\n      {\n        Timer t(\"vect start\");\n        insertStart(v);\n      }\n      cerr << endl << l.size() << endl << v.size() << endl;\n      Timer::dump();\n    }\n    ```\n\n9.  Save the file and open the terminal. Compile the preceding program, along with the **Timer.cpp** file, by writing the following commands:\n\n    ```cpp\n    $ g++ -O3 Snippet8.cpp Timer.cpp\n    $ ./a.out\n    ```\n\n    前面的命令生成以下输出:\n\n![Figure 8.34: Output of the code in Snippet8.cpp contrasting the timing  of std::list and std::vector insertion  ](img/C14583_08_34.jpg)\n\n###### 图 8.34:snippet 8 . CPP 中代码的输出，对比了标准::列表和标准::向量插入的时间\n\n从前面的输出可以看出，代码测量了在开始、结束和随机位置插入`标准::矢量`和`标准::列表`的`100000`整数所花费的时间。对于随机情况，向量显然以 100 倍或更大的因子获胜，甚至向量的最坏情况也比列表的随机情况快 10 倍。\n\n为什么会这样？答案在于现代计算机架构的进化方式。CPU 时钟速度从 80 年代初的约`1 Mhz`提高到了 2019 年中期的`5 GHz`——时钟频率提高了`5000 倍`——虽然最早的 CPU 每个指令使用多个周期，但现代的 CPU 在单个内核上每个周期执行几条指令(由于流水线等先进技术，我们在前面已经介绍过)。\n\n例如，原`英特尔 8088`上的`IDIV`指令需要超过 100 个时钟周期才能完成，而在现代处理器上，它可以在不到 5 个周期内完成。另一方面，内存带宽(读取或写入一个字节内存所需的时间)增长非常缓慢。\n\n从历史上看，处理器的速度在 1980 年到 2010 年间提高了大约 16000 倍。与此同时，内存中的速度增加量级更小，不到 100 倍。因此，一条指令对内存的单次访问可能会导致中央处理器等待大量的时钟周期。这将是不可接受的性能下降，已经有很多技术来缓解这个问题。在我们探讨这一点之前，让我们在下一个练习中测量内存访问的影响。\n\n### 练习 11:测量记忆访问的影响\n\n在本练习中，我们将研究随机访问内存对性能的影响。执行以下步骤完成本练习:\n\n1.  创建一个名为 **Snippet9.cpp** 的新文件。\n2.  包括必要的库，以及 **Timer.h** 头文件。创建两个常量整数变量`SIZE`和`N`，并将它们的值设置为`100000000`。另外，创建一个随机数生成器和一个从`0`到`N-1`的分布范围。编写以下代码来实现这一点:\n\n    ```cpp\n    #include <vector>\n    #include <list>\n    #include <algorithm>\n    #include <string>\n    #include <iostream>\n    #include <random>\n    #include \"Timer.h\"\n    using std::vector;\n    using std::list;\n    using std::cerr;\n    using std::endl;\n    const int SIZE = 100'000'000;\n    const int N = 100'000'000;\n    std::random_device dev;\n    std::mt19937 rng(dev());\n    std::uniform_int_distribution<std::mt19937::result_type> dist(0,SIZE-1);\n    ```\n\n3.  Create the `getPRIndex()` function, which returns a pseudo random index between `0` and `SIZE-1`, where `SIZE` is the number of elements in the array. Write the following code to implement this:\n\n    #### 注意\n\n    稍后我们将讨论为什么使用随机数。\n\n    ```cpp\n    uint64_t getPRIndex(uint64_t i)\n    {\n      return (15485863 * i) % SIZE;\n    }\n    ```\n\n4.  编写一个名为`sum1()`的函数，随机访问一个大的数据数组，并将这些元素相加:\n\n    ```cpp\n    uint64_t sum1(vector<int> &v)\n    {\n      TIME_IT;\n      uint64_t sum = 0;\n      for(int i = 0; i < N; ++ i)\n      {\n        sum += v[getPRIndex(i)];\n      }\n      return sum;\n    }\n    ```\n\n5.  编写一个名为`sum2()`的函数，在没有任何内存访问的情况下对随机数求和:\n\n    ```cpp\n    uint64_t sum2()\n    {\n      TIME_IT;\n      uint64_t sum = 0;\n      for(int i = 0; i < N; ++ i)\n      {\n        sum += getPRIndex(i);\n      }\n      return sum;\n    }\n    ```\n\n6.  在主函数中，初始化向量使得`v[i] == i`，因此`sum1()`和`sum2()`之间唯一的区别是`sum1()`访问内存而`sum2()`只执行计算。像往常一样，我们使用 volatile 来防止编译器删除所有代码，因为它没有副作用。在`主()`功能中写下以下代码:\n\n    ```cpp\n    int main()\n    {\n      // Allocate SIZE integers\n      std::vector<int> v(SIZE, 0);\n      // Fill 0 to SIZE-1 values into the vector\n      for(int i = 0; i < v.size(); ++ i)\n      {\n        v[i] = i;\n      }\n      volatile uint64_t asum1 = sum1(v);\n      volatile uint64_t asum2 = sum2();\n      Timer::dump();\n    }\n    ```\n\n7.  Save the program and open the terminal. Compile and run the program by writing the following commands:\n\n    ```cpp\n    $ g++ -O3 Snippet9.cpp Timer.cpp\n    $ ./a.out\n    ```\n\n    上述代码生成以下输出:\n\n    ![Figure 8.35: Output of the code in Snippet9.cpp contrasting the timing  of computation versus random memory access ](img/C14583_08_35.jpg)\n\n    ###### 图 8.35:snippet 9 . CPP 中代码的输出，对比了计算和随机存储器访问的时间\n\n    从前面的输出中，我们可以清楚地看到性能相差约`14x`的因素。\n\n8.  创建一个名为 **Snippet10.cpp** 的新文件，并添加与 **Snippet9.cpp** 中相同的代码。新增一个名为`sum3()`的函数，线性而非随机访问内存。另外，编辑主功能。更新后的代码如下:\n\n    ```cpp\n    uint64_t sum3(vector<int> &v)\n    {\n      TIME_IT;\n      uint64_t sum = 0;\n      for(int i = 0; i < N; ++ i)\n      {\n        sum += v[i];\n      }\n      return sum;\n    }\n    int main()\n    {\n      // Allocate SIZE integers\n      std::vector<int> v(SIZE, 0);\n\n      // Fill 0 to SIZE-1 values into the vector\n      for(int i = 0; i < v.size(); ++ i)\n      {\n        v[i] = i;\n      }\n      volatile uint64_t asum1 = sum1(v);\n      volatile uint64_t asum2 = sum2();\n      volatile uint64_t asum3 = sum3(v);  \n      Timer::dump();\n    }\n    ```\n\n9.  Save the file and open the Terminal. Compile and run the program:\n\n    ```cpp\n    $ g++ -O3 Snippet10.cpp Timer.cpp\n    $ ./a.out\n    ```\n\n    上述命令生成以下输出:\n\n![Figure 8.36: Output of the code in Snippet10.cpp contrasting the timing  of computation versus random and linear memory access  ](img/C14583_08_36.jpg)\n\n###### 图 8.36:snippet 10 . CPP 中代码的输出，对比了计算与随机和线性内存访问的时序\n\n在前面的输出中，请注意内存访问现在比以前快了`35`倍，`2.5`倍于`sum2()`中的计算。我们使用`sum1()`中的随机存取模式来演示线性和随机存储器存取之间的对比。是什么让线性内存访问比随机访问快得多？答案在于现代处理器中用于减轻慢速内存影响的两种机制–**缓存**和**预取**–这两种机制我们将在下面的章节中讨论。\n\n### 缓存\n\n现代处理器在处理器寄存器和内存之间有多层高速缓冲存储器。这些缓存被标记为 L1、L2、L3、L4 等，其中 L1 离处理器最近，L4 离处理器最远。每个缓存层都比它下面的层更快(通常更小)。以下是`哈斯韦尔`系列处理器的缓存/内存大小和延迟示例:\n\n*   L1: 32 KB，4 个周期\n*   L2: 256 KB，12 个周期\n*   L3: 6 MB，20 个周期\n*   L4: 128 MB，58 个周期\n*   内存:许多 GB，115 个周期\n\n缓存如何帮助提高性能的一个简单模型如下:当一个内存地址被访问时，它会在 L1 缓存中被查找——如果找到了，它会从那里被检索出来。如果没有，则在 L2 缓存中查找，如果没有找到，则在三级缓存中查找，以此类推，如果在任何缓存中都没有找到，则从内存中获取。当从内存中取出时，它存储在每个缓存中，以便以后更快地访问。这种方法本身是相当无用的，因为只有当我们一次又一次地访问相同的内存地址时，它才能提高性能。第二个方面叫做**预取**，是能够让缓存真正有回报的机制。\n\n### 预取\n\n预取是这样一个过程，当执行内存访问时，附近的数据也被提取到缓存中，即使它没有被直接访问。预取的第一个方面与内存总线粒度有关——它可以被认为是“内存子系统可以发送给处理器的最小数据量是多少？”。在大多数现代处理器中，这是 64 位——换句话说，无论你从内存中请求一个字节还是一个 64 位的值，包含该地址的 64 位的整个`机器字`都是从内存中读取的。这些数据存储在每层缓存中，以便以后更快地访问。显然，这将立即提高内存性能——假设我们在地址`0x1000`读取一个字节的内存；我们还将该地址之后的其他 7 个字节放入缓存。如果我们访问地址为`0x1001`的字节，它来自缓存，避免了昂贵的内存访问。\n\n预取的第二个方面更进一步——当一个地址的内存内容被读取时，处理器不仅读取那个内存字，而且读取更多。在 x86 系列处理器上，这是 32 到 128 个字节。这被称为**高速缓存行**大小——处理器总是以该大小的块读写内存。当中央处理器硬件检测到内存正在以线性方式被访问时，它会根据对后续可能被访问的地址的预测，将内存预取到一个高速缓存行中。\n\nCPU 在检测规则访问模式(向前和向后)方面非常聪明，并且会高效预取。您还可以使用特殊指令向处理器提供提示，使其根据程序员的指示预取数据。这些指令在大多数编译器上作为内部函数提供，以避免使用内联汇编语言。当不在高速缓存中的存储器地址被读取或写入时，它被称为**高速缓存未命中**，并且是非常昂贵的事件，要不惜一切代价避免。中央处理器硬件尽最大努力减少缓存未命中，但程序员可以分析和修改数据访问模式，以最大限度地减少缓存未命中。这里对高速缓存的描述是一个简化的教学模型——实际上，中央处理器有指令和数据的 L1 高速缓存、多条高速缓存线和非常复杂的机制，以确保多个处理器能够保持它们各自的高速缓存同步。\n\n#### 注意\n\n在这篇著名的在线文章中可以找到对缓存实现的全面描述(以及关于内存子系统的许多其他信息):[https://lwn.net/Articles/250967/](https://lwn.net/Articles/250967/)。\n\n### 缓存对算法的影响\n\n了解了缓存之后，我们现在可以推断为什么我们的第一个向量对比列表示例会显示出令人惊讶的结果——从计算机科学的角度来看，以下是正确的:\n\n**列表**:\n\n*   迭代到第 N 个位置是 N 阶复杂度。\n*   插入或删除一个元素的复杂度是 1。\n\n**对于数组(或向量)**:\n\n*   迭代到第 n 个位置是 1 阶复杂度。\n*   在位置 N 插入或删除元素的复杂度与(S - N)成正比，其中 S 是数组的大小。\n\n然而，对于现代体系结构，存储器访问的成本非常高，但是随后访问相邻地址的成本几乎为 0，因为它已经在高速缓存中。这意味着对非顺序位于内存中的`std::list`中的元素的迭代很可能总是导致缓存未命中，从而导致性能下降。另一方面，由于数组或`std::vector`的元素总是相邻的，缓存和预取可以大幅降低将(S-N)元素复制到新位置的总成本。因此，对这两种数据结构的传统分析宣称列表更适合随机插入，虽然在技术上是正确的，但实际上并不正确，尤其是考虑到现代 CPU 硬件明显复杂的缓存行为。当我们的程序是*数据绑定*时，算法复杂性的分析必须通过理解所谓的**数据局部性**来增加。\n\n数据局部性可以简单地定义为从刚被访问的内存地址到先前被访问的内存地址的平均距离。换句话说，跨越彼此相距较远的地址进行内存访问是一种严重的减速，因为来自较近地址的数据很可能已经被预取到缓存中。当数据已经存在于缓存中时，称为“热”；否则，它被称为“冷”。利用缓存的代码称为**缓存友好**。另一方面，高速缓存不友好的代码会导致高速缓存行被浪费地重新加载(称为**高速缓存无效**)。在本节的剩余部分，我们将研究关于如何编写缓存友好代码的策略。\n\n### 优化缓存友好性\n\n在过去，代码优化包括试图最小化代码中的机器指令数量，使用更有效的指令，甚至重新排序指令以允许流水线保持满。在当今时代，编译器执行所有上述优化的程度是大多数程序员无法做到的——尤其是考虑到编译器可以在数亿条指令的整个程序中做到这一点。即使是现在，程序员的责任仍然是优化数据访问模式以利用缓存的能力。\n\n任务非常简单——确保访问的内存接近之前访问的内存——但是实现这一点的方法需要大量的努力。\n\n#### 注意\n\n90 年代著名的游戏程序员和代码优化大师 Terje Mathisen，据说说过:“所有的编程都是缓存方面的练习。”2019 年的今天，这种说法比以往任何时候都更适用于这个试图编写快速代码的子领域。\n\n提高缓存友好性有一些基本的经验法则:\n\n*   堆栈总是“热”的，所以我们应该尽可能多地使用局部变量。\n*   动态分配的对象很少彼此具有数据局部性——避免它们或者使用预先分配的对象池，以便它们在内存中是连续的。\n*   基于指针的数据结构，如树，尤其是列表，由堆上分配的多个节点组成，对缓存非常不友好。\n*   面向对象代码中虚拟函数的运行时分派会使指令缓存失效——避免性能关键代码中的动态分派。\n\n在下一节中，我们将探讨堆分配的成本。\n\n### 练习 12:探索堆分配的成本\n\n在本练习中，我们将研究动态分配内存对性能的影响，并研究堆内存如何影响代码的性能。执行以下步骤完成本练习:\n\n1.  创建一个名为 **Snippet11.cpp** 的文件。\n2.  添加以下代码以包含必要的库:\n\n    ```cpp\n    #include <string>\n    #include <iostream>\n    #include <random>\n    #include \"Timer.h\"\n    using std::string;\n    using std::cerr;\n    using std::endl;\n    ```\n\n3.  声明一个常量变量 N 和一个名为水果的字符数组。给它们赋值:\n\n    ```cpp\n    const int N = 10'000'000;\n    const char* fruits[] = \n      {\"apple\", \"banana\", \"cherry\", \"durian\", \"guava\", \"jackfruit\", \"kumquat\", \"mango\", \"orange\", \"pear\"};\n    ```\n\n4.  创建一个名为`fun1()`的函数，该函数只循环遍历水果中的每个字符串，将其复制到一个字符串中，并对该字符串的字符进行求和:\n\n    ```cpp\n    uint64_t fun1()\n    {\n      TIME_IT;\n      uint64_t sum = 0;\n      string s1;\n      for(uint64_t i = 0; i < N; ++ i)\n      {\n        s1 = fruits[i % 10];\n        for(int k = 0; k < s1.size(); ++ k) sum += s1[k];\n      }\n      return sum;\n    }\n    ```\n\n5.  创建另一个名为`sum2()`的函数，使用本地声明的字符数组代替字符串和循环来复制:\n\n    ```cpp\n    uint64_t fun2()\n    {\n      TIME_IT;\n      uint64_t sum = 0;\n      char s1[32];\n\n      for(uint64_t i = 0; i < N; ++ i)\n      {\n        char *ps1 = s1;\n        const char *p1 = fruits[i % 10];\n        do { *ps1++ = *p1; } while(*p1++);\n        for(ps1 = s1; *ps1; ++ ps1) sum += *ps1;\n      }\n      return sum;\n    }\n    ```\n\n6.  在`main()`函数内写下以下代码:\n\n    ```cpp\n    int main()\n    {\n      for(int i = 0; i < 10; ++ i)\n      {\n        volatile uint64_t asum1 = fun1();\n        volatile uint64_t asum2 = fun2();  \n      }\n      Timer::dump();\n    }\n    ```\n\n7.  Save the file and open the terminal. Compile and run the program:\n\n    ```cpp\n    $ g++ -O3 Snippet11.cpp Timer.cpp\n    $ ./a.out\n    ```\n\n    上述命令生成以下输出:\n\n    ![Figure 8.37: Output of the code in Snippet11.cpp showing the effect of heap allocation on the timing  ](img/C14583_08_37.jpg)\n\n    ###### 图 8.37:snippet 11 . CPP 中代码的输出，显示了堆分配对计时的影响\n\n    从前面的输出中，请注意`fun2()`的速度几乎是`fun1()`的两倍。\n\n8.  Now, use the `perf` command to profile:\n\n    ```cpp\n    $ perf record ./a.out\n    ```\n\n    前面的命令生成以下输出:\n\n    ![Figure 8.38: Output of the perf command profiling the code in Snippet11.cpp  ](img/C14583_08_38.jpg)\n\n    ###### 图 8.38:在 Snippet11.cpp 中分析代码的 perf 命令的输出\n\n9.  现在，我们可以用以下代码检查性能报告:\n\n    ```cpp\n    $ perf report\n    ```\n\n我们收到以下输出:\n\n![](img/C14583_08_39.jpg)\n\n###### 图 8.39:snippet11 . CPP 中代码的 perf 命令定时报告的输出\n\n在前面的输出中，请注意大约 33%的执行时间是由`std::string`构造函数、`strlen()`和`memmove()`占用的。所有这些都与在`fun1()`中使用的`std::string`相关联。尤其是堆分配是最慢的操作。\n\n### 数组模式的结构\n\n在许多程序中，我们经常使用相同类型的对象数组——这些对象可以代表数据库中的记录、游戏中的实体等等。一种常见的模式是遍历一个大的结构数组，并对某些字段执行操作。即使结构在内存中是顺序的，如果我们只访问几个字段，更大的结构会降低缓存的效率。\n\n处理器可以将几个结构预取到高速缓存中，但是程序只访问高速缓存数据的一部分。因为它没有使用每个结构的每个字段，所以大部分缓存数据都会被丢弃。为了避免这种情况，可以使用另一种数据布局——我们使用一种阵列结构 ( **SoA** )模式，而不是使用结构 ( **AoS** )模式的**结构。在下一节中，我们将解决一个练习，其中我们将检查使用 SoA 模式和 AoS 模式的性能优势。**\n\n### 练习 13:使用数组模式的结构\n\n在本练习中，我们将研究使用 SoA 和 AoS 模式的性能优势。执行以下步骤完成本练习:\n\n1.  创建一个名为 **Snippet12.cpp** 的文件。\n2.  包括必要的库，以及`Timer.h`头文件。初始化一个随机数生成器，并创建一个从 1 到 N-1 的分布范围。创建一个常量整型变量，N，并用值 100，000，000 初始化它。添加以下代码来实现:\n\n    ```cpp\n    #include <vector>\n    #include <list>\n    #include <algorithm>\n    #include <string>\n    #include <iostream>\n    #include <random>\n    #include \"Timer.h\"\n    using std::vector;\n    using std::list;\n    using std::cerr;\n    using std::endl;\n    const int N = 100'000'000;\n    std::random_device dev;\n    std::mt19937 rng(dev());\n    std::uniform_int_distribution<std::mt19937::result_type> dist(1,N-1);\n    ```\n\n3.  编写两种不同的方式来表示数据--一种结构数组和一种数组结构。使用`uint64_t`的六个字段，这样我们就可以模拟一个更能代表真实世界程序的大尺寸结构:\n\n    ```cpp\n    struct Data1\n    {\n      uint64_t field1;\n      uint64_t field2;\n      uint64_t field3;\n      uint64_t field4;\n      uint64_t field5;\n      uint64_t field6;\n    };\n    struct Data2\n    {\n      vector<uint64_t> field1;\n      vector<uint64_t> field2;\n      vector<uint64_t> field3;\n      vector<uint64_t> field4;\n      vector<uint64_t> field5;\n      vector<uint64_t> field6;\n    };\n    struct Sum\n    {\n      uint64_t field1;\n      uint64_t field2;\n      uint64_t field3;\n      Sum(): field1(), field2(), field3() {}\n    };\n    ```\n\n4.  定义两个函数，即`sumAOS`和`sumSOA`，对前面两个数据结构的`字段 1`、`字段 2`和`字段 3`中的值求和。编写以下代码来实现这一点:\n\n    ```cpp\n    Sum sumAOS(vector<Data1> &aos)\n    {\n      TIME_IT;\n      Sum ret;\n      for(int i = 0; i < N; ++ i)\n      {\n        ret.field1 += aos[i].field1;\n        ret.field2 += aos[i].field2;\n        ret.field3 += aos[i].field3;\n      }\n      return ret;\n    }\n    Sum sumSOA(Data2 &soa)\n    {\n      TIME_IT;\n      Sum ret;\n      for(int i = 0; i < N; ++ i) \n      {\n        ret.field1 += soa.field1[i];\n        ret.field2 += soa.field2[i];\n        ret.field3 += soa.field3[i];\n      }\n      return ret;\n    }\n    ```\n\n5.  在`主`功能中编写以下代码:\n\n    ```cpp\n    int main()\n    {\n       vector<Data1> arrOfStruct;\n       Data2 structOfArr;\n\n       // Reserve space\n       structOfArr.field1.reserve(N);\n       structOfArr.field2.reserve(N);\n       structOfArr.field3.reserve(N);\n       arrOfStruct.reserve(N);\n       // Fill random values\n       for(int i = 0; i < N; ++ i)\n       {\n         Data1 temp;\n         temp.field1 = dist(rng);\n         temp.field2  = dist(rng);\n         temp.field3 = dist(rng);\n         arrOfStruct.push_back(temp);\n         structOfArr.field1.push_back(temp.field1);\n         structOfArr.field2.push_back(temp.field2);\n         structOfArr.field3.push_back(temp.field3);\n       }\n      Sum s1 = sumAOS(arrOfStruct);\n      Sum s2 = sumSOA(structOfArr);\n      Timer::dump();\n    }\n    ```\n\n6.  Save the program and open the Terminal. Run the program to time it by adding the following commands:\n\n    ```cpp\n    $ g++ -O3 Snippet12.cpp Timer.cpp\n    $ ./a.out\n    ```\n\n    上述代码生成以下输出:\n\n![Figure 8.40: Output of the code in Snippet12.cpp contrasting the timing  of the AOS and SOA patterns  ](img/C14583_08_40.jpg)\n\n###### 图 8.40:snippet 12 . CPP 中代码的输出，对比了 AOS 模式和 SOA 模式的时间\n\n数组结构方法的速度是数组结构方法的两倍。考虑到结构中向量的地址会相距很远，我们可能想知道为什么在 SoA 的情况下缓存行为更好。这是因为高速缓存是如何设计的，而不是像我们前面讨论的那样，将一个高速缓存视为一个单片块，而是将其分成多行。当访问存储器地址时，32 位或 64 位地址被转换成几个位的“标签”，并且使用与该标签相关联的高速缓存行。非常接近的内存地址将获得相同的标签并到达相同的缓存行。如果访问高度不同的地址，它会到达不同的高速缓存行。这种基于行的高速缓存设计对我们测试程序的影响是，好像我们对每个向量都有独立的高速缓存。\n\n前面对高速缓存行的解释非常简单，但高速缓存行的基本概念适用。对于这种数组模式的结构来说，代码可读性可能会稍差一些，但是考虑到性能的提高，这是非常值得的。随着结构尺寸的增大，这种特殊的优化变得更加有效。此外，请记住，如果字段大小不一，填充结构会使其大小膨胀很大一部分。我们已经探索了内存延迟对性能的影响，并了解了一些帮助处理器缓存有效的方法。当编写一个对性能至关重要的程序时，我们应该记住缓存效果。有时候，首先从一个对缓存更友好的架构开始是有意义的。像往常一样，我们应该在尝试对数据结构进行彻底的改变之前，一直衡量代码的性能。优化应该集中在程序最耗时的部分，而不是它的每一部分。\n\n### 算法优化\n\n算法优化的最简单形式是寻找执行你的任务的库——最流行的库都是高度优化和编写良好的。比如`Boost`库提供了很多有用的库，可以在很多项目中派上用场，比如`Boost。几何`、`助推。图`、`升压。间隔`和`升压。多精度`，举几个例子。使用专业编写的库比自己创建库要容易和明智得多。例如`Boost。图`实现了十几个算法来处理拓扑图，每一个算法都是高度优化的。\n\n许多计算可以简化为一系列标准算法组合在一起——如果做得正确，这些算法可以产生极其高效的代码——甚至经常被编译器并行化以利用多核或 SIMD。在本节的其余部分，我们将采用一个单独的程序，并尝试以各种方式对其进行优化–这将是一个字数统计程序，具有以下规格:\n\n*   为了隔离磁盘输入/输出所花费的时间，我们将在处理之前将整个文件读取到内存中。\n*   Unicode 支持将被忽略，我们将假设英语文本为 ASCII。\n*   我们将使用在线提供的大量公共领域文学文本作为测试数据。\n\n### 练习 14:优化字数统计程序\n\n在这个冗长的练习中，我们将使用各种优化技术来优化程序。我们将执行实际程序的增量优化。我们将使用的测试数据由名为《双城记》的书组成，该书已被附加在一起 512 次。\n\n#### 注意\n\n本练习中使用的数据集可在此处获得:[https://github . com/trainingypbackt/Advanced-CPlusPlus/blob/master/lesson 8/练习 14/data.7z](https://github.com/TrainingByPackt/Advanced-CPlusPlus/blob/master/Lesson8/Exercise14/data.7z) 。您将需要提取这个 7zip 存档，并将结果文件(称为 data.txt)复制到您进行本练习的文件夹中。\n\n执行以下步骤完成本练习:\n\n1.  Write the basic boilerplate code that reads the file (the full code can be found in **SnippetWC.cpp**). This will be a common driver for all the versions of the word count that we write:\n\n    ```cpp\n    int wordCount(const string &s);\n    int main(int argc, char **argv)\n    {\n      if(argc > 1)\n      {\n        TIME_IT;\n        string sContent;\n        ostringstream buf;\n        ifstream ifs(argv[1]);\n        {\n          Timer t(\"Read file\");\n          buf << ifs.rdbuf(); \n          sContent = buf.str();\n          sContent.push_back(' ');\n        }\n        cerr << wordCount(sContent) << endl;\n      }\n      Timer::dump();\n    }\n    ```\n\n    我们将使用一个虚拟块来分隔读取文件的代码的时间，以及时间`main()`本身，以获得整体执行时间。\n\n    请注意`push_back`在末尾添加了一个空格——这确保了数据以空格结尾，简化了我们使用的算法。\n\n2.  写一个基本的字数统计功能。逻辑非常简单——对于字符串中的每个字符，如果该字符不是空白，而后面的字符是空白，那么它就是单词的结尾，应该被计数。由于我们的样板代码在末尾增加了一个空格，所以任何最终的单词都将被计算在内。该功能在 **Snippet13.cpp** :\n\n    ```cpp\n    int wordCount(const std::string &s)\n    {\n      int count = 0;\n      for(int i = 0, j = 1; i < s.size() - 1; ++ i, ++ j)\n      {\n        if(!isspace(s[i]) && isspace(s[j]))\n        {\n          ++ count;\n        }\n      }\n      return count;\n    }\n    ```\n\n    中定义\n3.  Let's compile, run, and get an idea of the performance. We will verify that it is working right by comparing the result of our code with the results provided by the standard `wc` program:\n\n    ```cpp\n    $ g++ -O3 Snippet13.cpp SnippetWC.cpp Timer.cpp\n    $ ./a.out data.txt\n    ```\n\n    我们收到以下输出:\n\n    ![Figure 8.41: Output of the code in Snippet13.cpp with a baseline wordcount implementation ](img/C14583_08_41.jpg)\n\n    ###### 图 8.41:带有基线字数实现的 Snippet13.cpp 中的代码输出\n\n    让我们为厕所计划计时:\n\n    ```cpp\n    $ time wc -w data.txt\n    ```\n\n    我们收到以下输出:\n\n    ![Figure 8.42: Output of timing the wc program  ](img/C14583_08_42.jpg)\n\n    ###### 图 8.42:定时 wc 程序的输出\n\n    *wc* 程序显示相同的字数，即`71108096`，所以我们知道我们的代码是正确的。我们的代码花了大约`3.6 秒`，包括读取文件，比 wc 慢很多。\n\n4.  Our first strategy to optimize is to see if there is a better way to implement `isspace()`. Instead of a function, we can use a lookup table that can tell if a character is a space or not (the code for this can be found in **Snippet14.cpp**):\n\n    ```cpp\n    int wordCount(const std::string &s)\n    {\n      // Create a lookup table\n      bool isSpace[256];\n      for(int i = 0; i < 256; ++ i)\n      {\n        isSpace[i] = isspace((unsigned char)i);\n      }\n      int count = 0;\n      int len = s.size() - 1;\n      for(int i = 0, j = 1; i < len; ++ i, ++ j)\n      {\n        count += !isSpace[s[i]] & isSpace[s[j]];\n      }\n      return count;\n    }\n    ```\n\n    请记住，C/C++ 中的布尔变量采用整数值 0 或 1，因此我们可以直接编写以下内容:\n\n    ```cpp\n    !isSpace[s[i]] & isSpace[s[j]]\n    ```\n\n    这意味着我们不必写这个:\n\n    ```cpp\n    (!isSpace[s[i]] && isSpace[s[j]]) ? 1 : 0\n    ```\n\n    将布尔值直接用作数字有时会导致更快的代码，因为我们避免了条件逻辑运算符&&和||，这可能会导致分支指令。\n\n5.  Compile and test the performance now:\n\n    ```cpp\n    $ g++ -O3 Snippet14.cpp SnippetWC.cpp Timer.cpp\n    $ ./a.out data.txt\n    ```\n\n    我们收到以下输出:\n\n    ![Figure 8.43: Output of the code in Snippet14.cpp ](img/C14583_08_43.jpg)\n\n    ###### 图 8.43:snippet 14 . CPP 中代码的输出\n\n    通过使用查找表的简单原理，我们实现了字数统计代码的 8 倍加速。我们能做得更好吗？是的–我们可以进一步理解查找表的概念–对于每对字符，有四种可能，这将导致相应的操作:\n\n    【空间空间】:无动作，【非空间空间】:加 1 计数，【空间非空间】:无动作，【非空间，非空间】:无动作\n\n    因此，我们可以制作一个包含`65536`条目(`256 * 256`)的表格来覆盖所有可能的字符对。\n\n6.  Write the following code to create the table:\n\n    ```cpp\n    // Create a lookup table for every pair of chars\n    bool table[65536];\n    for(int i = 0; i < 256; ++ i)\n    {\n      for(int j = 0; j < 256; ++ j)\n      {\n        int idx = j + i * 256;\n        table[idx] = !isspace(j) && isspace(i);\n      }\n    }\n    ```\n\n    字数循环如下(完整代码可在 **Snippet15.cpp** 中找到):\n\n    ```cpp\n    int count = 0;\n    for(int i = 0; i < s.size() - 1; ++ i)\n    {\n      // grab the 2 bytes at s[i] as a 16 bit value\n      unsigned short idx;\n      memcpy(&idx, &s[i], 2);\n      count += table[idx];\n    }\n    ```\n\n    我们将字符串的每个字符作为一个 16 位的值来读取。直接将一个指针从 char*强制转换为另一个类型并取消对它的引用是未定义的行为——正确的方法是使用`memcpy()`。编译器足够聪明，可以使用 CPU 内存访问指令，而不是实际调用`memcpy()`获取 2 个字节。我们已经结束了不包含条件语句的循环，这应该会使它更快。请记住，X86 架构是*小端*-因此从字符数组中读取的 16 位值将第一个字符作为其 LSB，第二个字符作为 MSB。\n\n7.  Now, time the code we wrote:\n\n    ```cpp\n    $ g++ -O3 Snippet15.cpp SnippetWC.cpp Timer.cpp\n    $ ./a.out data.txt\n    ```\n\n    ![Figure 8.44: Output of the code in Snippet15.cpp ](img/C14583_08_44.jpg)\n\n    ###### 图 8.44:snippet 15 . CPP 中代码的输出\n\n    这个更大的查找表使`字数()`的速度提高了 1.8 倍。让我们退后一步，从另一个角度来看这个问题，这样我们就可以有效地使用现有的标准库。这样做的好处有两个方面——首先，代码不太容易出错，其次，我们可以利用一些编译器提供的并行化。\n\n    让我们使用标准算法重写使用`isspace`的查找表的程序版本。如果我们看一下计数单词的主循环，我们取 2 个字符，根据一些逻辑，我们将 1 或 0 累加到`计数`变量中。这是许多代码中常见的模式:\n\n    ```cpp\n    X OP (a[0] OP2 b[0]) OP (a[1] OP2 b[1]) OP (a[2] OP2 b[2]) ... OP (a[N] OP2 b[N])  \n    ```\n\n    这里`a`和`b`是大小的数组`N`，`X`是初始值，`OP`和`OP2`是运算符。有一种标准算法封装了这种模式，称为`STD::inner _ product`–它采用两个序列，在每对元素之间应用一个运算符(OP2)，并在这些元素之间应用另一个运算符(OP)，从初始值 x 开始。\n\n8.  We can write the function as follows (the full code can be found in **Snippet16.cpp**):\n\n    ```cpp\n    int wordCount(const std::string &s)\n    {\n      // Create a lookup table for every char\n      bool table[256];\n      for(int i = 0; i < 256; ++ i)\n      {\n        table[i] = isspace((unsigned char)i) ? 1 : 0;\n      }\n\n      auto isWordEnd = [&](char a, char b) \n      {\n        return !table[a] & table[b]; \n      };\n\n      return std::inner_product(s.begin(), s.end()-1, s.begin()+1, 0, std::plus<int>(), isWordEnd);\n    }\n    ```\n\n    这个`内积()`调用在每个`s[n]`和`s[n+1]`上应用`isWordEnd()` lambda，并在这些结果之间应用标准加法函数。实际上，当`s[n]`和`s[n+1]`在一个词尾时，我们是在加 1。\n\n    #### 注意\n\n    即使这看起来像是许多嵌套的函数调用，编译器也会内联所有内容，并且没有开销。\n\n9.  Compile and time the execution of this version:\n\n    ```cpp\n    $ g++ -O3 Snippet16.cpp SnippetWC.cpp Timer.cpp\n    $ ./a.out data.txt\n    ```\n\n    我们收到以下输出:\n\n    ![Figure 8.45: Output of the code in Snippet16.cpp ](img/C14583_08_45.jpg)\n\n    ###### 图 8.45:snippet 16 . CPP 中代码的输出\n\n    令人惊讶的是，该代码比我们在 **Snippet14.cpp** 中的初始循环版本稍快。\n\n10.  Can we adapt the same code to use the large lookup table? Indeed, we can – the new function looks like this (the full code can be found in **Snippet17.cpp**):\n\n    ```cpp\n    int wordCount(const std::string &s)\n    {\n      // Create a lookup table for every pair of chars\n      bool table[65536];\n      for(int i = 0; i < 256; ++ i)\n      {\n        for(int j = 0; j < 256; ++ j)\n        {\n          int idx = j + i * 256;\n          table[idx] = !isspace(j) && isspace(i);\n        }\n      }\n      auto isWordEnd = [&](char a, char b) \n      {\n        unsigned idx = (unsigned)a | (((unsigned)b) << 8);\n        return table[idx]; \n      };\n      return std::inner_product(s.begin(), s.end()-1, s.begin()+1, 0, std::plus<int>(), isWordEnd);\n    }\n    ```\n\n    与之前基于循环的代码唯一不同的是，我们没有使用`memcpy()`将两个连续的字节转换为一个单词，而是使用按位`或`运算符将它们组合在一起。\n\n11.  Compile and time the code:\n\n    ```cpp\n    $ g++ -O3 Snippet17.cpp SnippetWC.cpp Timer.cpp\n    $ ./a.out data.txt\n    ```\n\n    我们收到以下输出:\n\n    ![Figure 8.46: Output of the code in Snippet17.cpp ](img/C14583_08_46.jpg)\n\n    ###### 图 8.46:snippet 17 . CPP 中代码的输出\n\n    这段代码没有我们在 **Snippet15.cpp** 中的基于 loop 0 的版本快。这样做的原因是，在循环版本中，我们读取 2 个字节组合成一个`短的`来获得索引，这不需要计算，但是在这里，我们通过按位运算将 2 个字节读取到一个`短的`中。\n\n12.  Now that we have the code where the bulk of the work is done by a standard library function, we can now get automatic parallelization for free – compile and test as follows:\n\n    ```cpp\n    $ g++ -O3 -fopenmp -D_GLIBCXX_PARALLEL Snippet17.cpp SnippetWC.cpp Timer.cpp\n    $ ./a.out data.txt\n    ```\n\n    我们收到以下输出:\n\n    ![Figure 8.47: Output of the code in Snippet17.cpp with the parallelized standard library ](img/C14583_08_47.jpg)\n\n    ###### 图 8.47:带有并行化标准库的 Snippet17.cpp 中的代码输出\n\n    显然，它不能完全并行化，所以我们在速度方面只获得了大约 2.5 倍的改进，但我们不必对代码做任何事情就能实现。我们能以同样的方式使基于循环的代码并行化吗？理论上，是的——我们可以手动使用 **OpenMP** 指令来实现这一点；然而，这需要对代码进行修改，并了解如何使用 OpenMP。 **Snippet16.cpp** 中的版本怎么样？\n\n    ```cpp\n    $ g++ -O3 -fopenmp -D_GLIBCXX_PARALLEL Snippet16.cpp SnippetWC.cpp Timer.cpp\n    $ ./a.out data.txt\n    ```\n\n    我们收到以下输出:\n\n    ![Figure 8.48: Output of the code in Snippet16.cpp with the parallelized standard library ](img/C14583_08_48.jpg)\n\n    ###### 图 8.48:带有并行化标准库的 Snippet16.cpp 中的代码输出\n\n    这个版本也有类似的改进。我们完成了吗，或者还能更快吗？著名游戏程序员迈克尔·阿布拉什(Michael Abrash)创造了首字母缩略词“T2”——它代表“没有最快的代码”。他的意思是，只要付出足够的努力，总有可能让代码变得更快。这似乎是不可能的，但人们一次又一次地找到了越来越快的计算方法——我们的代码也不例外，我们仍然可以走得更远。我们可以为优化做的权衡之一是让代码不那么通用——我们已经对代码进行了一些限制——例如，我们只处理 **ASCII** 英文文本。通过对输入数据添加更多的约束，我们可以做得更好。让我们假设文件中没有不可打印的字符。这是对我们输入数据的合理假设。如果我们假设这一点，那么我们可以简化检测空格的条件——因为所有空格字符都大于或等于 ASCII 32，所以我们可以避免查找表本身。\n\n13.  让我们根据之前的想法实现代码(完整代码可以在 **Snippet18.cpp** 中找到):\n\n    ```cpp\n    int wordCount(const std::string &s)\n    {\n      auto isWordEnd = [&](char a, char b) \n      {\n        return a > 32 & b < 33; \n      };\n      return std::inner_product(s.begin(), s.end()-1, s.begin()+1, 0, std::plus<int>(), isWordEnd);\n    }\n    ```\n\n14.  Compile and run the program:\n\n    ```cpp\n    $ g++ -O3 Snippet18.cpp SnippetWC.cpp Timer.cpp\n    $ ./a.out data.txt\n    ```\n\n    我们收到以下输出:\n\n    ![Figure 8.49: Output of the code in Snippet18.cpp with simplified logic for detecting spaces ](img/C14583_08_49.jpg)\n\n    ###### 图 8.49:snippet 18 . CPP 中代码的输出，带有检测空格的简化逻辑\n\n    这个版本的速度是并行版本的两倍，而且只有几行代码。使用并行化会更好吗？\n\n    ```cpp\n    $ g++ -O3 -fopenmp -D_GLIBCXX_PARALLEL Snippet18.cpp SnippetWC.cpp Timer.cpp\n    $ ./a.out data.txt\n    ```\n\n    我们收到以下输出:\n\n    ![Figure 8.50: Output of the code in Snippet18.cpp with the parallelized standard library ](img/C14583_08_50.jpg)\n\n    ###### 图 8.50:带有并行化标准库的 Snippet18.cpp 中的代码输出\n\n    不幸的是，事实并非如此——它实际上更慢。管理多线程和线程争用的开销有时比多线程代码的好处更昂贵。在这一点上，我们可以看到文件读取代码占用了大部分时间——我们能对此做些什么吗？\n\n15.  让我们将`main()`功能更改为对其各个部分计时(完整代码可在 **SnippetWC2.cpp** 中找到):\n\n    ```cpp\n        {\n          Timer t(\"File read\");\n          buf << ifs.rdbuf(); \n        }\n        {\n          Timer t(\"String copy\");\n          sContent = buf.str();\n        }\n        {\n          Timer t(\"String push\");\n          sContent.push_back(' ');\n        }\n        int wc;\n        {\n          Timer t(\"Word count\");\n          wc = wordCount(sContent);\n        }\n    ```\n\n16.  Compile and run the preceding code:\n\n    ```cpp\n    $ g++ -O3 Snippet18.cpp SnippetWC2.cpp Timer.cpp\n    $ ./a.out data.txt\n    ```\n\n    我们收到以下输出:\n\n    ![Figure 8.51: Output of the code in Snippet18.cpp with all operations timed ](img/C14583_08_51.jpg)\n\n    ###### 图 8.51:snippet 18 . CPP 中所有操作都定时的代码输出\n\n    大部分时间由`push_back()`和复制字符串占用。由于字符串正好是文件的大小，`push_back()`最终会为字符串分配一个新的缓冲区并复制内容。如何才能消除这个`push_back()`的称呼？我们在末尾添加了一个空格，以便能够始终如一地计算最后一个单词(如果有的话)，因为我们的算法计算单词的末尾。有三种方法可以避免这种情况:计算一个单词的开头，而不是结尾；单独计算最后一个词，如果有的话；并且使用`c_str()`函数，这样我们在末尾就有了一个`NUL`字符。现在让我们依次尝试这些方法。\n\n17.  先写主功能不用`push_back`(完整代码可以在 **SnippetWC3.cpp** 找到):\n\n    ```cpp\n    {\n      Timer t(\"File read\");\n      buf << ifs.rdbuf(); \n    } \n    {\n      Timer t(\"String copy\");\n      sContent = buf.str();\n    }\n    int wc;\n    {\n      Timer t(\"Word count\");\n      wc = wordCount(sContent);\n    }\n    ```\n\n18.  通过将`isWordEnd()`重命名为`isWordStart()`来更改 wordCount()中的代码，并反转逻辑。如果当前字符是空格，而后续字符不是空格，那么可以将一个单词视为起始字符。此外，如果字符串以非空格开头，则多计数一个单词(完整代码可在 **Snippet19.cpp** 中找到):\n\n    ```cpp\n    int wordCount(const std::string &s)\n    {\n      auto isWordStart = [&](char a, char b) \n      {\n        return a < 33 & b > 32; \n      };\n      // Count the first word if any\n      int count = s[0] > 32;\n      // count the remaining\n      return std::inner_product(s.begin(), s.end()-1, s.begin()+1, count, std::plus<int>(), isWordStart);\n    }\n    ```\n\n19.  现在，写下第二个选择——数数最后一个单词，如果有的话。代码与 **Snippet18.cpp** 版本几乎相同，只是我们检查了最后一个字(完整代码可以在 **Snippet20.cpp** 中找到):\n\n    ```cpp\n    int count = std::inner_product(s.begin(), s.end()-1, s.begin()+1, 0, std::plus<int>(), isWordEnd);\n    // count the last word if any\n    if(s.back() > 32) \n    {\n      ++ count;\n    }\n    return count;\n    ```\n\n20.  Write the third version that uses `c_str()` – all we need to do is change the parameters for `inner_product()` (the full code can be found in **Snippet21.cpp**)\n\n    ```cpp\n    int wordCount(const std::string &s)\n    {\n      auto isWordEnd = [&](char a, char b) \n      {\n        return a > 32 & b < 33; \n      };\n      const char *p = s.c_str();\n      return std::inner_product(p, p + s.size(), p+1, 0, std::plus<int>(), isWordEnd);\n    }\n    ```\n\n    由于`c_str()`末端有一个`NUL`，所以它的工作原理和以前一样。\n\n21.  Compile and time all three versions:\n\n    ```cpp\n    $ g++ -O3 Snippet19.cpp SnippetWC3.cpp Timer.cpp\n    $ ./a.out data.txt\n    ```\n\n    我们收到以下输出:\n\n    ![Figure 8.52: Output of the code in Snippet19.cpp, which counts the beginnings  of words rather than the ends ](img/C14583_08_52.jpg)\n\n    ###### 图 8.52:snippet 19 . CPP 中代码的输出，它计算单词的开头而不是结尾\n\n    现在输入以下命令:\n\n    ```cpp\n    $ g++ -O3 Snippet20.cpp SnippetWC3.cpp Timer.cpp\n    $ ./a.out data.txt\n    ```\n\n    我们收到以下输出:\n\n    ![Figure 8.53: Output of the code in Snippet20.cpp  ](img/C14583_08_53.jpg)\n\n    ###### 图 8.53:snippet 20 . CPP 中代码的输出\n\n    现在输入以下命令:\n\n    ```cpp\n    $ g++ -O3 Snippet21.cpp SnippetWC3.cpp Timer.cpp\n    $ ./a.out data.txt\n    ```\n\n    我们收到以下输出:\n\n    ![Figure 8.54: Output of the code in Snippet21.cpp  ](img/C14583_08_54.jpg)\n\n    ###### 图 8.54:snippet 21 . CPP 中代码的输出\n\n    三者几乎同时运行，几毫秒的微小差异可以忽略不计。\n\n22.  现在，我们可以解决字符串复制所花费的时间–我们将直接将文件读入字符串缓冲区，而不是使用`std::stringstream`(完整代码可在 **SnippetWC4.cpp** ):\n\n    ```cpp\n    string sContent;\n    {\n      Timer t(\"String Alloc\");\n      // Seek to end and reserve memory\n      ifs.seekg(0, std::ios::end);   \n      sContent.resize(ifs.tellg());\n    }\n    {\n      Timer t(\"File read\");\n      // Seek back to start and read data\n      ifs.seekg(0, std::ios::beg);\n      ifs.read(&sContent[0], sContent.size());\n    }\n    int wc;\n    {\n      Timer t(\"Word count\");\n      wc = wordCount(sContent);\n    }  \n    ```\n\n    中找到)\n23.  Compile and run this version:\n\n    ```cpp\n    $ g++ -O3 Snippet21.cpp SnippetWC4.cpp Timer.cpp\n    ```\n\n    我们收到以下输出:\n\n    ![Figure 8.55: Output of the code with changed file load code in SnippetWC4.cpp  ](img/C14583_08_55.jpg)\n\n    ###### 图 8.55:snippetwc4 . CPP 中文件加载代码改变后的代码输出\n\n    我们现在已经将文件读取代码所需的时间从大约 1，000 毫秒减少到 250 毫秒，提高了 4 倍。字数代码从大约`2500 毫秒`开始，减少到大约 60 毫秒——提高了 40 倍。整个程序的总性能提高了 3.6 倍。我们仍然可以问这是否是极限——事实上，TANSTATFC 仍然适用，并且还有一些事情可以做:使用`内存映射输入/输出`获得直接指向文件的缓冲区，而不是将数据读入`std::string`。这可能比分配和读取更快——它需要改变字数代码以接受一个`常量字符*`和一个长度，或者一个`标准::string_view`。使用不同的、更快的分配器来分配内存。使用`-3 月=原生`标志为原生 CPU 编译。然而，我们似乎不太可能从中获得非常大的性能提升，因为这些优化与字数统计算法本身无关。另一个最后的尝试可能是放弃 C++ 构造，使用`编译器内部函数`编写内联 SIMD 代码(这些函数被编译器直接翻译成单个汇编指令)。完成这项工作所需的知识超出了本介绍材料的范围。\n\n24.  Nevertheless, for the curious student, an `AVX2` (256-bit SIMD) version of `wordCount()` is provided (Snippet23.cpp). This version needs the input string to have a length that is a multiple of 32 and a space at the end. This means that the main function has to be rewritten (SnippetWC5.cpp):\n\n    ```cpp\n    $ g++ -O3 -march=native Snippet22.cpp SnippetWC5.cpp Timer.cpp\n    $ ./a.out data.txt\n    ```\n\n    我们收到以下输出:\n\n![Figure 8.56: Output of the code in Snippet22.cpp that uses SIMD intrinsics ](img/C14583_08_56.jpg)\n\n###### 图 8.56:snippet 22 . CPP 中使用 SIMD 内函数的代码输出\n\n请注意，我们需要使用`-march=native`标志，以便编译器使用 AVX SIMD 指令集。如果处理器不支持它，将导致编译错误。如果此可执行文件是为 AVX 目标编译的，并且在处理器不支持这些指令的系统上运行，程序会因“非法指令”异常而崩溃。这似乎是一个非常小的改进，但并不显著——用汇编程序或 SIMD 优化所需的努力和学习曲线通常太高，除非您的应用或行业有这些要求，否则是不合理的。SIMD 版本一次处理 32 个字节，但性能几乎没有提高。事实上，如果您使用编译器资源管理器检查其他代码片段中常规 C++ 实现的生成的汇编代码，您将看到编译器本身已经使用了 SIMD——这正好说明了编译器在让您的代码变快方面走了多远。\n\n另一点需要注意的是，我们的文件读取和内存分配现在占用了大部分时间——抛开内存分配，我们可以得出结论，我们的代码已经成为 **I/O 绑定**而不是 **CPU 绑定**。这意味着无论我们写代码有多快，它都会受到获取数据速度的限制。我们从字数统计算法的一个非常简单的实现开始，增加了它的复杂性和速度，最后能够回到一个非常简单的实现，最终成为最快的。算法的整体速度提高了 40 倍。我们使用了许多方法，从稍微重新排列代码，到以不同的方式重新想象问题，再到执行微优化。没有一种方法可以一直有效，优化仍然是一种创造性的努力，需要想象力和技巧，通常还需要横向思维。随着编译器变得越来越聪明，超越它们变得越来越难——然而，程序员是唯一真正理解代码意图的人，而且总是有余地让代码变得更快。\n\n### 活动 1:优化拼写检查算法\n\n在本活动中，我们将尝试逐步优化程序。这个活动是关于一个简单的拼写检查器，它获取一个字典和一个文本文件，并打印出字典中没有的文本单词列表。在 **Speller.cpp** 中提供了一个基本的框架程序，以及一个示例字典和文本文件，分别为 **dict.txt** 和 **data.txt** 。提供了一个名为 out.txt 的文件，其中包含程序的所需输出(拼写错误单词的索引列表)。文本文件存在于`7zip 存档`中，即`活动 1.7z`。\n\n该词典取自许多 Linux 发行版提供的 Linux 单词列表。文本文件与我们在上一个练习中使用的文件相似——它与我们在字数统计练习中使用的文件一样大，所有标点符号都被删除并转换为小写。\n\n请注意，字典只是一个例子，所以不要假设所有有效的单词都存在于其中——输出中的许多单词很可能是拼写正确的单词。框架代码读取字典和文本文件，并调用上面的拼写检查代码(您将编写)。之后，它将结果输出与 **out.txt** 的内容进行比较，并打印程序是否按预期工作。执行拼写检查的函数返回字典中没有的单词的索引向量。因为我们只关注拼写检查算法，所以只有代码是定时的。不考虑读取文件和比较输出所花费的时间。您将开发该程序的更快版本-参考文件夹中提供的参考实现有 **Speller1.cpp** 、 **Speller2.cpp** 等。\n\n在每一步中，您将只得到提示，告诉您应该更改什么以使其更快–只有`get 拼错()`函数中的代码将被修改，而不是任何其他代码。只要代码产生正确的结果，并且在`main()`内的代码没有改变，学生可以按照自己的意愿自由执行代码。\n\n#### 注意\n\n优化是一个创造性的、非确定性的过程——学生不能保证也不总是可能得出与参考实现相同的代码。如果您编写的代码的性能不如引用实现，这应该不会令人惊讶。事实上，甚至有可能您的代码比引用更快。\n\n执行以下步骤来实施本活动:\n\n制作一个名为 Speller1.cpp 的 Speller.cpp 的副本，并实现`get 拼错()`函数的代码。使用`std::set`及其`count()`方法实现。\n\n1.  Write the next version of the program as Speller2.cpp, and then compile it and time it as before. Try using `std::unordered_set` rather than `std::set`. You should get about a 2x speedup with this implementation.\n\n    在最终版本 **Speller3.cpp** 中，使用**布隆过滤器**数据结构来实现拼写检查算法。用不同数量的散列函数和不同大小的布隆过滤器进行实验，看看什么效果最好。\n\n2.  For each of the preceding steps, compile the program and run it as follows (change the input file name as required):\n\n    ```cpp\n    $ g++ -O3 Speller1.cpp Timer.cpp\n    $ ./a.out\n    ```\n\n    #### 注意\n\n    您不应该期望时间完全如这里所示，但是如果您正确地实现了代码，速度的相对提高应该接近我们在这里看到的。\n\n对每个步骤执行上述命令后，将生成以下输出。输出将显示代码的时序，如果输出正确，还会显示一条初始消息。以下是步骤 1 的输出:\n\n![Figure 8.57: Example output of the code for Step 1 ](img/C14583_08_57.jpg)\n\n###### 图 8.57:步骤 1 的代码输出示例\n\n以下是步骤 2 的输出:\n\n![Figure 8.58: Example output of the code for Step 2 ](img/C14583_08_58.jpg)\n\n###### 图 8.58:步骤 2 的代码输出示例\n\n以下是步骤 3 的输出:\n\n![Figure 8.59: Example output of the code for Step 3 ](img/C14583_08_59.jpg)\n\n###### 图 8.59:步骤 3 的代码输出示例\n\n#### 注意\n\n这项活动的解决方案可以在第 725 页找到。\n\n## 总结\n\n在这一章中，我们已经讨论了许多复杂的材料。对于任何现代 C++ 开发人员来说，优化代码都是一项困难但必要的技能。机器学习、超现实游戏、大数据分析和高能效计算的需求使得这对于任何 C++ 专业人员来说都是一个非常重要的学习领域。我们了解到性能优化的过程分为两个阶段。\n\n首先，优化从适当的性能测量策略开始，测试条件反映真实世界的数据和使用模式。我们已经学习了如何通过各种方法来衡量性能——学习汇编代码、手动计时、源代码插装以及使用运行时分析器。一旦我们有了准确的测量，我们就能真正理解我们程序的哪些部分实际上是慢的，并集中精力在那里获得最大的改进。第二个阶段涉及实际修改程序——我们了解了几种策略，从为代码使用最佳编译器选项开始，使用并行化特性，还使用配置文件数据来帮助编译器，然后是一些简单的代码转换，这些转换可以在不进行重大代码更改的情况下产生微小但有用的性能提升。然后，我们学习了如何通过构造循环和条件来提高性能，使代码对分支预测更加友好。\n\n然后，我们了解了缓存对性能的显著影响，并研究了一些技术，如 SOA 模式，以使我们的代码利用现代 CPU 中的缓存。最后，我们把所有这些东西放在一起，作为一个单词计数程序和简单拼写检查器的真实例子，来实践我们所学的东西。除了本章的材料之外，还有很多其他的高级技术和理论需要学习，但是我们在这里所涵盖的应该会给任何学生未来的学习打下坚实的基础。\n\n在这些章节的最后，您已经探索了许多与使用高级 C++ 相关的主题。在前几章中，您已经学习了如何编写可移植的软件，如何使用模板来使用类型系统，以及如何有效地使用指针和继承。然后，您已经探索了 C++ 标准库，包括流和并发，它们是构建大型现实世界应用的基本工具。在最后几节中，您学习了如何测试和调试程序，以及如何优化代码以高效运行。在广泛使用的编程语言中，C++ 可能是最复杂的，也是最具表现力的。这本书只是一个开始，它会给你一个坚实的平台来继续你的进一步学习。"
  },
  {
    "path": "docs/adv-cpp/10.md",
    "content": "# 十、附录\n\n## 关于\n\n包括这一部分是为了帮助学生完成书中的活动。它包括学生为实现活动目标而要执行的详细步骤。\n\n## 第 1 章 -可移植 C++ 软件剖析\n\n### 活动 1:向项目添加新的源文件-头文件对\n\n在本练习中，我们将创建一个新的源文件-头文件对，其中包含一个名为`sum`的新函数。它接受两个参数并返回它们的和。该文件对将被添加到现有项目中。按照以下步骤实施本活动:\n\n1.  First, open the Eclipse IDE with the existing project that we created in *Exercise 3*, *Adding New Source Files to CMake and Eclipse CDT*. Right-click on the **src** folder in the **Project Explorer** pane.\n\n    #### 注意\n\n    我们可以创建`。cpp`和`。h`单独文件或使用新建类向导，稍后删除类代码。使用新建类向导很方便，因为它还可以创建有用的样板代码。\n\n2.  从弹出菜单中选择**新建** | **类**。键入`SumFunc`，点击**完成**按钮。\n3.  Next, edit the `SumFunc.h` file to look like the following code:\n\n    ```cpp\n    #ifndef SRC_SUMFUNC_H_\n    #define SRC_SUMFUNC_H_\n    int sum(int a, int b);\n    #endif /* SRC_SUMFUNC_H_ */\n    ```\n\n    请注意，我们实际上会删除该类，而是提供一个函数。我们可以分别创建这两个文件。然而，`add 类`函数创建了它们，并添加了一些我们将使用的样板代码。在这里，我们的文件以`include` guard 开始和结束，这是防止双重包含问题的常见策略。我们有函数的正向声明，它允许其他文件在包含这个头文件后调用函数。\n\n4.  Next, edit the `SumFunc.cpp` file as illustrated here:\n\n    ```cpp\n    #include \"SumFunc.h\"\n    #include <iostream>\n    int sum(int a, int b) {\n      return a + b;\n    }\n    ```\n\n    在这个文件中，我们包含了头文件，并提供了函数的主体，它将两个给定的整数相加并返回。\n\n5.  Edit the `CMakeFiles.txt` file so that its `add_executable` section reflects the following code:\n\n    ```cpp\n    add_executable(CxxTemplate\n      src/CxxTemplate.cpp  \n      src/ANewClass.cpp\n      src/SumFunc.cpp\n    )\n    ```\n\n    这里，我们将`src/SumFunc.cpp`文件添加到可执行源文件列表中，以便将其链接到可执行文件中。\n\n6.  Make the following changes in `CxxTemplate.cpp`:\n\n    ```cpp\n    #include \"CxxTemplate.h\"\n    #include \"ANewClass.h\"\n    #include \"SumFunc.h\" //add this line\n    ...\n    CxxApplication::CxxApplication( int argc, char *argv[] ) {\n      std::cout << \"Hello CMake.\" << std::endl;\n      ANewClass anew;\n      anew.run();\n      std::cout << sum(3, 4) << std::endl; // add this line\n    }\n    ```\n\n    #### 注意\n\n    这个文件的完整代码可以在这里找到:[https://github . com/trainingypbackt/Advanced-CPlusPlus/blob/master/lesson 1/activity 01/src/cxxtemplate . CPP](https://github.com/TrainingByPackt/Advanced-CPlusPlus/blob/master/Lesson1/Activity01/src/CxxTemplate.cpp)。\n\n    在这里，我们添加了一行，其中我们用`3`和`4`调用`求和`函数，并将结果打印到控制台。\n\n7.  构建并运行项目(**项目** | **构建所有** | **运行** | **运行**)。您看到的输出应该如下所示:\n\n![Figure 1.57: The output ](img/C14583_01_57.jpg)\n\n###### 图 1.57:输出\n\n通过本练习，您练习了向项目中添加一个新的源文件-头文件对。这些文件对是 C++ 开发中非常常见的模式。他们可以主持全球性的活动，就像我们在这次活动中所做的那样。更常见的是，它们托管类及其定义。在整个开发过程中，您将向应用中添加更多的头文件对。因此，重要的是要习惯于添加它们，不要拖拖拉拉，这会导致难以维护和测试的大型单片文件。\n\n### 活动 2:添加新类及其测试\n\n在本练习中，我们将添加一个模拟`1D`直线运动的新类。该类将具有用于`位置`和`速度`的双字段。它还将有一个`advanceTimeBy()`方法，该方法接收一个双`dt`参数，该参数基于`速度`的值修改`位置`。双数值用`EXPECT_DOUBLE_EQ`代替`EXPECT_EQ`。在本活动中，我们将向项目中添加一个新类及其测试。按照以下步骤执行本活动:\n\n1.  用我们现有的项目打开 Eclipse 集成开发环境。要创建新类，右键单击**项目浏览器**窗格中的 **src** 文件夹，并选择**新建** | **类**。键入`LinearMotion1D`作为名称并创建类。\n2.  打开我们在上一步中创建的`线性运动 1D.h`文件。将`位置`和`速度` `双`场加入其中。另外，将正向引用添加到`高级时间比`方法中，该方法将一个`双 dt`变量作为参数。构造函数和析构函数已经在类中了。以下是`线性运动 1D.h`中这些变化的最终结果:\n\n    ```cpp\n    #ifndef SRC_LINEARMOTION1D_H_\n    #define SRC_LINEARMOTION1D_H_\n    class LinearMotion1D {\n    public:\n      double position;\n      double velocity;\n      void advanceTimeBy(double dt);\n      LinearMotion1D();\n      virtual ~LinearMotion1D();\n    };\n    #endif /* SRC_LINEARMOTION1D_H_ */\n    ```\n\n3.  现在打开`LinearMotion1D.cpp`并添加`advanced time by`方法的实现。我们的`速度`是我们班的一个场，时差是这个方法的一个参数。`位置`的变化等于`速度`乘以时间变化，所以我们计算结果并将其添加到`位置`变量中。我们还使用现有的构造器代码将`位置`和`速度`初始化为 0。以下是`LinearMotion1D.cpp`中这些变化的最终结果:\n\n    ```cpp\n    #include \"LinearMotion1D.h\"\n    void LinearMotion1D::advanceTimeBy(double dt) {\n      position += velocity * dt;\n    }\n    LinearMotion1D::LinearMotion1D() {\n      position = 0;\n      velocity = 0;\n    }\n    LinearMotion1D::~LinearMotion1D() {\n    }\n    ```\n\n4.  为此类创建一个测试。右键点击**测试**文件夹，选择**新建** | **源文件**。键入`linear motion 1 test . CPP`作为名称并创建它。\n5.  现在打开`线性运动 1 测试. cpp`。为左右两个不同方向的运动创建两个测试。对于它们中的每一个，创建一个`线性运动 1D`对象，初始化它的位置和速度，并调用`提前时间`来实际发生运动。然后，检查它是否移动到了我们预期的位置。以下是`linemotion1 test . CPP`中这些变化的最终结果:\n\n    ```cpp\n    #include \"gtest/gtest.h\"\n    #include \"../src/LinearMotion1D.h\"\n    namespace {\n    class LinearMotion1DTest: public ::testing::Test {};\n    TEST_F(LinearMotion1DTest, CanMoveRight) {\n      LinearMotion1D l;\n      l.position = 10;\n      l.velocity = 2;\n      l.advanceTimeBy(3);\n      EXPECT_DOUBLE_EQ(16, l.position);\n    }\n    TEST_F(LinearMotion1DTest, CanMoveLeft) {\n      LinearMotion1D l;\n      l.position = 10;\n      l.velocity = -2;\n      l.advanceTimeBy(3);\n      EXPECT_DOUBLE_EQ(4, l.position);\n    }\n    }\n    ```\n\n6.  现在修改我们的 CMake 配置文件，这样我们生成的这些源文件也可以使用。对于`线性运动 1D`类，添加其`。cpp`文件作为可执行文件，以便它被编译并与其他源文件链接在一起。以下是`CMakeLists.txt`的`add _ executive`部分变成的内容:\n\n    ```cpp\n    add_executable(CxxTemplate\n      src/CxxTemplate.cpp  \n      src/ANewClass.cpp\n      src/SumFunc.cpp\n      src/LinearMotion1D.cpp # added\n    )\n    ```\n\n7.  对于我们刚刚创建的测试，编辑**测试/CMakeLists.txt** 。在这里，我们需要添加测试源文件`linear motion 1 test . CPP`，以及它所使用的类的源文件`LinearMotion1D.cpp`。因为它们在不同的目录中，所以作为`访问它们../src/LinearMotion1D.cpp`。以下是`测试/CMakeLists.txt`的`add _ executive`部分变成的内容:\n\n    ```cpp\n    add_executable(tests \n      CanTest.cpp \n      SumFuncTest.cpp \n      ../src/SumFunc.cpp\n      LinearMotion1DTest.cpp # added\n      ../src/LinearMotion1D.cpp # added\n    )\n    ```\n\n8.  构建项目并运行测试。我们将看到所有测试都是成功的:\n\n![Figure 1.58: All tests are successful ](img/C14583_01_58.jpg)\n\n###### 图 1.58:所有测试都成功\n\n在本活动中，您执行了向项目中添加新类及其测试的任务。您创建了一个模拟一维运动的类，并编写了单元测试来确保它正常工作。\n\n### 活动 3:提高代码可读性\n\n在本练习中，您将练习提高给定代码的质量。按照以下步骤实施本活动:\n\n1.  打开 Eclipse CDT，在 Eclipse 中的一个源文件对中创建一个类。为此，右键单击**项目浏览器**中的 **src** 文件夹。从弹出菜单中选择**新** | **类**。\n2.  输入`速度计算器`作为标题文件名，点击**完成**。它将创建两个文件:**速度计算器. h** 和**速度计算器. cpp** 。我们为上面的两个文件提供了代码。添加为每个文件提供的代码。\n3.  现在我们需要将该类添加到 CMake 项目中。打开项目根目录下的 **CMakeLists.txt** 文件(在 **src** 文件夹外)，并在文件中进行以下更改:\n\n    ```cpp\n      src/LinearMotion1D.cpp\n      src/SpeedCalculator.cpp # add this line\n    )\n    ```\n\n4.  现在选择**文件** | **全部保存**保存所有文件，通过选择**项目** | **全部建立**来建立项目。确保没有错误。\n5.  在我们的`main()`函数中创建一个`速度计算器`类的实例，并调用其`run()`方法。打开 **CxxTemplate.cpp** 并包含我们的新类，然后通过添加以下代码编辑`主`功能:\n\n    ```cpp\n    #include \"SpeedCalculator.h\"\n    int main( int argc, char *argv[] ) {\n      cxxt::CxxApplication app( argc, argv );\n      // add these three lines\n      SpeedCalculator speedCalculator;\n      speedCalculator.initializeData(10);\n      speedCalculator.calculateAndPrintSpeedData();\n      return 0;\n    }\n    ```\n\n6.  要修复样式，只需使用 **Source** | **Format** 并选择格式化整个文件。幸运的是，变量名没有任何问题。\n7.  Simplify the code to make it more understandable. The loop in `calculateAndPrintSpeedData` is doing a couple of things at the same time. It's calculating the speed, finding the minimum and maximum values of it, checking whether we crossed a threshold, and storing the speed. If the speed was a transient value, taking it apart would mean storing it somewhere to loop on it one more time. However, since we are storing it in the speeds array anyway, we can loop one more time on it for clarity of code. Here is the updated version of the loop:\n\n    ```cpp\n    for (int i = 0; i < numEntries; ++ i) {\n      double dt = timesInSeconds[i + 1] - timesInSeconds[i];\n      assert(dt > 0);\n      double speed = (positions[i + 1] - positions[i]) / dt;\n      speeds[i] = speed;\n    }\n    for (int i = 0; i < numEntries; ++ i) {\n      double speed = speeds[i];\n      if (maxSpeed < speed) {\n        maxSpeed = speed;\n      }\n      if (minSpeed > speed) {\n        minSpeed = speed;\n      }\n    }\n    for (int i = 0; i < numEntries; ++ i) {\n      double speed = speeds[i];\n      double dt = timesInSeconds[i + 1] - timesInSeconds[i];\n      if (speed > speedLimit) {\n        limitCrossDuration += dt;\n      }\n    }\n    ```\n\n    这多少有点味道的问题，但是将循环的大**变轻有助于可读性。此外，它分离了任务，并消除了它们在循环迭代期间相互交互的可能性。第一个循环创建并保存速度值。第二个循环找到最小和最大速度值。第三个循环决定了超过限速多长时间。请注意，这是一个效率稍低的实现；然而，它清楚地分离了所采取的行动，我们不必在循环的长时间迭代中在精神上分离离散的行动。**\n\n8.  Run the preceding code and observe the problem at runtime. While the code is better in terms of style now, it suffers from several mistakes, some of which will create runtime errors. First, when we run the application, we see the following output in Eclipse:\n\n    ![Figure 1.59: Program output in Eclipse CDT ](img/C14583_01_59.jpg)\n\n    ###### 图 1.59:Eclipse CDT 中的程序输出\n\n    注意**退出值:-1** 在顶部。当这不是`0`时，表示我们的代码有问题。\n\n9.  Execute the program manually in the console. Here's the output we get:\n\n    ![Figure 1.60: Program output in the terminal with the error ](img/C14583_01_60.jpg)\n\n    ###### 图 1.60:终端中有错误的程序输出\n\n    不幸的是，我们在 Eclipse 中没有得到分段错误输出，因此您必须在 Eclipse 控制台视图中检查退出值。为了找到问题，我们将在下一步中使用调试器。\n\n10.  Press the debug toolbar button in Eclipse to start the application in debug mode. Press the resume button to continue execution. It will stop at line 40 of `SpeedCalculator.cpp`, right when an error is about to happen. If you hover over `speeds`, you realize that it is an invalid memory reference:\n\n    ![Figure 1.61: Invalid memory reference ](img/C14583_01_61.jpg)\n\n    ###### 图 1.61:无效的内存引用\n\n11.  进一步检查后，我们意识到我们从未初始化过任何东西的`速度`指针。在我们的速度计算器功能中为它分配内存:\n\n    ```cpp\n    void SpeedCalculator::calculateAndPrintSpeedData() {\n      speeds = new double[numEntries]; // add this line\n      double maxSpeed = 0;\n    ```\n\n12.  Run it again. We get the following output:\n\n    ```cpp\n    Hello CMake.\n    Hello from ANewClass.\n    7\n    CxxTemplate: SpeedCalculator.cpp:38: void SpeedCalculator::calculateAndPrintSpeedData(): Assertion `dt > 0' failed.\n    ```\n\n    请注意，这是一个断言，即代码必须确保计算出的`dt`始终大于零。这是我们确信的事情，我们希望它能帮助我们在开发过程中发现错误。Assert 语句在生产构建中被忽略，因此您可以将它们随意地放在代码中，作为在开发过程中捕捉错误的保护措施。尤其是因为与高级语言相比，C++ 缺少很多安全检查，在潜在的不安全代码中放置`断言`语句有助于捕捉错误。\n\n13.  Let's investigate why our `dt` ended up not larger than zero. For this, we fire up the debugger again. It stops at this strange place:\n\n    ![Figure 1.62: Debugger stopped at a library without source code ](img/C14583_01_62.jpg)\n\n    ###### 图 1.62:调试器在没有源代码的库中停止\n\n14.  The actual error is raised deep inside a library. However, our own functions are still on the stack and we can investigate their state at that time. Click on **SpeedCalculator** above **main** in the tree to the left:\n\n    ![Figure 1.63: Value of dt as the program is running ](img/C14583_01_63.jpg)\n\n    ###### 图 1.63:程序运行时的 dt 值\n\n    看来我们的`dt`在这里变成了 **-43** (确切数值不重要)。查看**变量**视图，我们意识到`i`是 **9** ，这是我们输入数组的最后一个元素:\n\n    ![Figure 1.64: Values of variables ](img/C14583_01_64.jpg)\n\n    ###### 图 1.64:变量值\n\n    这感觉像是一个边界问题。仔细查看代码，我们意识到我们使用的是`timeseconds[10]`，这是数组中不存在的第十一个元素。进一步思考，我们意识到，当我们有 10 个位置时，我们只能有 9 个位置对减法，从而有 9 个速度。这是一个非常常见且难以捕捉的错误，因为 C++ 不会强制您留在数组中。\n\n15.  Rework our whole code for this problem:\n\n    ```cpp\n    void SpeedCalculator::calculateAndPrintSpeedData() {\n      speeds = new double[numEntries - 1];\n      double maxSpeed = 0;\n    ...\n      for (int i = 0; i < numEntries - 1; ++ i) {\n        double dt = timesInSeconds[i + 1] - timesInSeconds[i];\n    ...\n      for (int i = 0; i < numEntries - 1; ++ i) {\n        double speed = speeds[i];\n    ....\n      for (int i = 0; i < numEntries - 1; ++ i) {\n        double speed = speeds[i];\n    ```\n\n    最后，我们的代码似乎运行没有任何错误，正如我们在下面的输出中看到的:\n\n    ![Figure 1.65: Program output ](img/C14583_01_65.jpg)\n\n    ###### 图 1.65:程序输出\n\n16.  However, there is a curious point here: **Min speed** is always `0`, no matter how many times you run it. To investigate, let's put a breakpoint at the following line:\n\n    ![Figure 1.66: Placing a breakpoint ](img/C14583_01_66.jpg)\n\n    ###### 图 1.66:放置断点\n\n17.  When we debug our code, we see that it never stops here. This is obviously wrong. Upon further investigation, we realize that `minSpeed` is initially 0, and every other speed value is larger than that. We should initialize it to either something very large, or we need to get the very first element as the minimum value. Here, we choose the second approach:\n\n    ```cpp\n    for (int i = 0; i < numEntries - 1; ++ i) {\n      double speed = speeds[i];\n      if (i == 0 || maxSpeed < speed) { // changed\n        maxSpeed = speed;\n      }\n      if (i == 0 || minSpeed > speed) { // changed\n        minSpeed = speed;\n      }\n    }\n    ```\n\n    而`maxSpeed`不需要这个，保持一致就好。现在当我们运行代码时，我们看到我们不再将`0`作为我们的最小速度:\n\n    ![Figure 1.67: Program output ](img/C14583_01_67.jpg)\n\n    ###### 图 1.67:程序输出\n\n18.  Our code seems to be running fine. However, there is another mistake that we have made. When we debug our code, we see that our first elements are not zero:\n\n    ![Figure 1.68: Values of variables ](img/C14583_01_68.jpg)\n\n    ###### 图 1.68:变量值\n\n19.  The pointer dereferenced the first element in the array. We had initialized elements to zero here, but they do not seem to be zero. Here is the updated code:\n\n    ```cpp\n      // add these two lines:\n      timesInSeconds[0] = 0.0;\n      positions[0] = 0.0;\n      for (int i = 0; i < numEntries; ++ i) {\n        positions[i] = positions[i - 1] + (rand() % 500);\n        timesInSeconds[i] = timesInSeconds[i - 1] + ((rand() % 10) + 1);\n      }\n    ```\n\n    当我们调查时，我们意识到我们从零开始循环并覆盖第一个项目。此外，我们试图访问`位置【0-1】`，这是一个错误，也是 C++ 不强制数组边界的另一个例子。当我们让循环从 1 开始时，所有这些问题都消失了:\n\n    ```cpp\n      timesInSeconds[0] = 0.0;\n      positions[0] = 0.0;\n      for (int i = 1; i < numEntries; ++ i) {\n        positions[i] = positions[i - 1] + (rand() % 500);\n        timesInSeconds[i] = timesInSeconds[i - 1] + ((rand() % 10) + 1);\n      }\n    ```\n\n    以下是用更新后的代码生成的输出:\n\n![Figure 1.69: Program output ](img/C14583_01_69.jpg)\n\n###### 图 1.69:程序输出\n\n仅仅通过查看这段代码，我们无法区分。这些都是随机值，看起来和以前没什么不同。这样的 bug 很难发现，并且会导致随机行为，给我们留下难以跟踪的错误。您可以做些什么来避免这样的错误，包括在解引用指针时格外小心，尤其是在循环中；将代码分成函数，并为它们编写单元测试；并且自由地使用`断言`语句来执行编译器或运行时不执行的事情。\n\n## 2A 章-不允许养鸭-类型和推导\n\n### 活动 1:图形处理\n\n在本练习中，我们将实现两个类(`点 3d`和`矩阵 3d`，以及乘法运算符，以便我们可以平移、缩放和旋转点。我们还将实现一些辅助方法，为转换创建必要的矩阵。按照以下步骤实施本活动:\n\n1.  Load the prepared project from the **Lesson2A/Activity01** folder and configure the Current Builder for the project to be `CMake Build (Portable)`. Build and configure the launcher and run the unit tests (which fail). Recommend that the name that's used for the test runner is `L2AA1graphicstests`.\n\n    #### CMake 配置\n\n    按照*练习 1* 、*声明变量和探索大小*的*步骤 9* ，将项目配置为一个 CMake 项目。\n\n2.  为`点 3d`类添加一个测试，以验证默认构造函数创建了一个`原点【0，0，0，1】`。\n3.  打开**点 3 tests . CPP**文件，在顶部添加以下一行。\n4.  Replace the failing existing test with the following test:\n\n    ```cpp\n    TEST_F(Point3dTest, DefaultConstructorIsOrigin)\n    {\n        Point3d pt;\n        float expected[4] = {0,0,0,1};\n        for(size_t i=0 ; i < 4 ; i++)\n        {\n            ASSERT_NEAR(expected[i], pt(i), Epsilon) << \"cell [\" << i << \"]\";\n        }\n    }\n    ```\n\n    这个测试需要我们写一个访问操作符。\n\n5.  Replace the current class definition in **point3d.hpp** file with the following code:\n\n    ```cpp\n    include <cstddef>\n    class Point3d\n    {\n    public:\n        static constexpr size_t NumberRows{4};\n        float operator()(const int index) const\n        {\n            return m_data[index];\n        }\n    private:\n        float m_data[NumberRows];\n    };\n    ```\n\n    测试现在构建并运行，但是失败了。\n\n6.  将默认构造函数的声明添加到`点 3d`声明:\n\n    ```cpp\n    Point3d();\n    ```\n\n7.  Add the implementation to the **point3d.cpp** file:\n\n    ```cpp\n    Point3d::Point3d()\n    {\n        for(auto& item : m_data)\n        {\n            item = 0;\n        }\n        m_data[NumberRows-1] = 1;\n    }\n    ```\n\n    测试现在构建、运行并通过。\n\n8.  Add the next test:\n\n    ```cpp\n    TEST_F(Point3dTest, InitListConstructor3)\n    {\n        Point3d pt {5.2, 3.5, 6.7};\n        float expected[4] = {5.2,3.5,6.7,1};\n        for(size_t i=0 ; i < 4 ; i++)\n        {\n            ASSERT_NEAR(expected[i], pt(i), Epsilon) << \"cell [\" << i << \"]\";\n        }\n    }\n    ```\n\n    这个测试无法编译。因此，我们需要实现另一个构造函数——以`STD::initializer _ list<>`为参数的构造函数。\n\n9.  在头文件中添加以下内容:\n\n    ```cpp\n    #include <initializer_list>\n    ```\n\n10.  将以下构造函数声明添加到头文件中的 Point3d 类:\n\n    ```cpp\n    Point3d(std::initializer_list<float> list);\n    ```\n\n11.  Add the following code to the implementation file. This code ignores error handling, which will be added in *Lesson 3*, *The Distance Between Can and Should – Objects, Pointers, and Inheritance*:\n\n    ```cpp\n    Point3d::Point3d(std::initializer_list<float> list)\n    {\n        m_data[NumberRows-1] = 1;\n        int i{0};\n        for(auto it1 = list.begin(); \n            i<NumberRows && it1 != list.end();\n            ++ it1, ++ i)\n        {\n            m_data[i] = *it1;\n        }\n    }\n    ```\n\n    测试现在应该构建、运行并通过。\n\n12.  Add the following test:\n\n    ```cpp\n    TEST_F(Point3dTest, InitListConstructor4)\n    {\n        Point3d pt {5.2, 3.5, 6.7, 2.0};\n        float expected[4] = {5.2,3.5,6.7,2.0};\n        for(size_t i=0 ; i < 4 ; i++)\n        {\n            ASSERT_NEAR(expected[i], pt(i), Epsilon) << \"cell [\" << i << \"]\";\n        }\n    }\n    ```\n\n    测试仍然应该构建、运行和通过。\n\n13.  现在是时候通过将验证循环移入`点 3 测试`类中的模板化函数来重构测试用例了。在该类中添加以下模板:\n\n    ```cpp\n    template<size_t size>\n    void VerifyPoint(Point3d& pt, float (&expected)[size])\n    {\n        for(size_t i=0 ; i< size ; i++)\n        {\n            ASSERT_NEAR(expected[i], pt(i), Epsilon) << \"cell [\" << i << \"]\";\n        }\n    }\n    ```\n\n14.  This now means that the last test can be rewritten as follows:\n\n    ```cpp\n    TEST_F(Point3dTest, InitListConstructor4)\n    {\n        Point3d pt {5.2, 3.5, 6.7, 2.0};\n        float expected[4] = {5.2,3.5,6.7,2.0};\n        VerifyPoint(pt, expected);\n    }\n    ```\n\n    保持测试的可读性与产品代码的可读性一样重要。\n\n15.  接下来，通过以下测试添加对等式和不等式运算符的支持:\n\n    ```cpp\n    TEST_F(Point3dTest, EqualityOperatorEqual)\n    {\n        Point3d pt1 {1,3,5};\n        Point3d pt2 {1,3,5};\n        ASSERT_EQ(pt1, pt2);\n    }\n    TEST_F(Point3dTest, EqualityOperatorNotEqual)\n    {\n        Point3d pt1 {1,2,3};\n        Point3d pt2 {1,2,4};\n        ASSERT_NE(pt1, pt2);\n    }\n    ```\n\n16.  为了实现这些，在头文件中添加以下声明/定义:\n\n    ```cpp\n    bool operator==(const Point3d& rhs) const;\n    bool operator!=(const Point3d& rhs) const\n    {\n        return !operator==(rhs);\n    }\n    ```\n\n17.  现在，在。cpp 文件:\n\n    ```cpp\n    bool Point3d::operator==(const Point3d& rhs) const\n    {\n        for(int i=0 ; i<NumberRows ; i++)\n        {\n            if (m_data[i] != rhs.m_data[i])\n            {\n                return false;\n            }\n        }\n        return true;\n    }\n    ```\n\n18.  当我们第一次添加`点 3d`时，我们实现了一个常量访问器。添加以下测试，其中我们需要一个非常数访问器，以便我们可以将其分配给成员:\n\n    ```cpp\n    TEST_F(Point3dTest, AccessOperator)\n    {\n        Point3d pt1;\n        Point3d pt2 {1,3,5};\n        pt1(0) = 1;\n        pt1(1) = 3;\n        pt1(2) = 5;\n        ASSERT_EQ(pt1, pt2);\n    }\n    ```\n\n19.  To get this test to build, add the following accessor to the header:\n\n    ```cpp\n    float& operator()(const int index)\n    {\n        return m_data[index];\n    }\n    ```\n\n    请注意，它返回一个引用。因此，我们可以将其分配给一个成员值。\n\n20.  要结束`点 3d`，为默认复制构造函数和复制赋值在类声明中添加行:\n\n    ```cpp\n    Point3d(const Point3d&) = default;\n    Point3d& operator=(const Point3d&) = default;\n    ```\n\n21.  现在，添加`Matrix3d`类。首先，在当前项目的顶层文件夹中创建两个空文件`matrix3d.hpp`和`matrix3d.cpp`，然后在 tests 文件夹中添加一个名为`matrix dtests . CPP`的空文件。\n22.  打开顶部文件夹中的 CmakeLists.txt 文件，将 **matrix3d.cpp** 添加到以下行:\n\n    ```cpp\n    add_executable(graphics point3d.cpp main.cpp matrix3d.cpp)\n    ```\n\n23.  Open the **CMakeLists.txt** file in the **tests** folder, add `../matrix3d.cpp` to the definition of `SRC_FILES`, and add **matrix3dTests.cpp** to the definition of `TEST_FILES`:\n\n    ```cpp\n    SET(SRC_FILES \n        ../matrix3d.cpp\n        ../point3d.cpp)\n    SET(TEST_FILES \n        matrix3dTests.cpp\n        point3dTests.cpp)\n    ```\n\n    现有的`点 3d`测试仍然应该构建，运行，并且通过，如果你做了那些正确的改变。\n\n24.  将以下测试管道添加到`矩阵 3dTests.cpp` :\n\n    ```cpp\n    #include \"gtest/gtest.h\"\n    #include \"../matrix3d.hpp\"\n    class Matrix3dTest : public ::testing::Test\n    {\n    public:\n    };\n    TEST_F(Matrix3dTest, DummyTest)\n    {\n        ASSERT_TRUE(false);\n    }\n    ```\n\n25.  构建并运行测试。我们刚刚添加的测试应该会失败。\n26.  Replace DummyTest with the following test in **matrix3dTests.cpp**:\n\n    ```cpp\n    TEST_F(Matrix3dTest, DefaultConstructorIsIdentity)\n    {\n        Matrix3d mat;\n        for( int row{0} ; row<4 ; row++)\n            for( int col{0} ; col<4 ; col++)\n            {\n                int expected = (row==col) ? 1 : 0;\n                ASSERT_FLOAT_EQ(expected, mat(row,col)) << \"cell[\" << row << \"][\" << col << \"]\";\n            }\n    }\n    ```\n\n    构建测试将会失败，因为我们还没有定义`Matrix3d`类。我们现在将在 **matrix3d.hpp** 中进行此操作。\n\n27.  Add the following definition to **matrix3d.hpp**:\n\n    ```cpp\n    class Matrix3d\n    {\n    public:\n        float operator()(const int row, const int column) const\n        {\n            return m_data[row][column];\n        }\n    private:\n        float m_data[4][4];\n    };\n    ```\n\n    测试现在将构建，但仍然失败，因为我们没有创建一个默认的构造函数来创建一个身份矩阵。\n\n28.  将默认构造函数的声明添加到`矩阵 3d` :\n\n    ```cpp\n    Matrix3d();\n    ```\n\n    的公共部分的头文件中\n29.  Add this definition to **matrix3d.cpp**:\n\n    ```cpp\n    #include \"matrix3d.hpp\"\n    Matrix3d::Matrix3d()\n    {\n        for (int i{0} ; i< 4 ; i++)\n            for (int j{0} ; j< 4 ; j++)\n                m_data[i][j] = (i==j);\n    }\n    ```\n\n    测试现在构建并通过了。\n\n30.  稍微重构代码，使其更易读。修改标题如下:\n\n    ```cpp\n    #include <cstddef>   // Required for size_t definition\n    class Matrix3d\n    {\n    public:\n        static constexpr size_t NumberRows{4};\n        static constexpr size_t NumberColumns{4};\n        Matrix3d();\n        float operator()(const int row, const int column) const\n        {\n        return m_data[row][column];\n        }\n    private:\n        float m_data[NumberRows][NumberColumns];\n    };\n    ```\n\n31.  更新 **matrix3d.cpp** 文件以使用常量:\n\n    ```cpp\n    Matrix3d::Matrix3d()\n    {\n        for (int i{0} ; i< NumberRows ; i++)\n            for (int j{0} ; j< NumberColumns ; j++)\n                m_data[i][j] = (i==j);\n    }\n    ```\n\n32.  重建测试，并确保它们仍然通过。\n33.  现在，我们需要添加初始化列表构造函数。为此，添加以下测试:\n\n    ```cpp\n    TEST_F(Matrix3dTest, InitListConstructor)\n    {\n        Matrix3d mat{ {1,2,3,4}, {5,6,7,8},{9,10,11,12}, {13,14,15,16}};\n        int expected{1};\n        for( int row{0} ; row<4 ; row++)\n            for( int col{0} ; col<4 ; col++, expected++)\n            {\n                ASSERT_FLOAT_EQ(expected, mat(row,col)) << \"cell[\" << row << \"][\" << col << \"]\";\n            }\n    }\n    ```\n\n34.  添加初始化列表支持的包含文件，并在 **matrix3d.hpp** :\n\n    ```cpp\n    #include <initializer_list>\n    class Matrix3d\n    {\n    public:\n        Matrix3d(std::initializer_list<std::initializer_list<float>> list);\n    ```\n\n    中声明构造函数\n35.  最后，将构造函数的实现添加到。cpp 文件:\n\n    ```cpp\n    Matrix3d::Matrix3d(std::initializer_list<std::initializer_list<float>> list)\n    {\n        int i{0};\n        for(auto it1 = list.begin(); i<NumberRows ; ++ it1, ++ i)\n        {\n            int j{0};\n            for(auto it2 = it1->begin(); j<NumberColumns ; ++ it2, ++ j)\n                m_data[i][j] = *it2;\n        }\n    }\n    ```\n\n36.  为了提高测试的可读性，在测试框架中添加一个助手方法。在`矩阵 3dTest`类中，声明如下:\n\n    ```cpp\n    static constexpr float Epsilon{1e-12};\n    void VerifyMatrixResult(Matrix3d& expected, Matrix3d& actual);\n    ```\n\n37.  添加辅助方法的定义:\n\n    ```cpp\n    void Matrix3dTest::VerifyMatrixResult(Matrix3d& expected, Matrix3d& actual)\n    {\n        for( int row{0} ; row<4 ; row++)\n            for( int col{0} ; col<4 ; col++)\n            {\n            ASSERT_NEAR(expected(row,col), actual(row,col), Epsilon) \n    << \"cell[\" << row << \"][\" << col << \"]\";\n            }\n    }\n    ```\n\n38.  写一个测试，将两个矩阵相乘，得到一个新的矩阵(预计用手算):\n\n    ```cpp\n    TEST_F(Matrix3dTest, MultiplyTwoMatricesGiveExpectedResult)\n    {\n        Matrix3d mat1{ {5,6,7,8}, {9,10,11,12}, {13,14,15,16}, {17,18,19,20}};\n        Matrix3d mat2{ {1,2,3,4}, {5,6,7,8},    {9,10,11,12},  {13,14,15,16}};\n        Matrix3d expected{ {202,228,254,280},\n                           {314,356,398,440},\n                           {426,484,542,600},\n                           {538,612,686,760}};\n        Matrix3d result = mat1 * mat2;\n        VerifyMatrixResult(expected, result);\n    }\n    ```\n\n39.  In the header file, define `operator*=`:\n\n    ```cpp\n    Matrix3d& operator*=(const Matrix3d& rhs);\n    ```\n\n    然后，实现内联版本的`运算符*`(在类声明之外):\n\n    ```cpp\n    inline Matrix3d operator*(const Matrix3d& lhs, const Matrix3d& rhs)\n    {\n        Matrix3d temp(lhs);\n        temp *= rhs;\n        return temp;\n    }\n    ```\n\n40.  并实现到 **matrix3d.cpp** 文件:\n\n    ```cpp\n    Matrix3d& Matrix3d::operator*=(const Matrix3d& rhs)\n    {\n        Matrix3d temp;\n        for(int i=0 ; i<NumberRows ; i++)\n            for(int j=0 ; j<NumberColumns ; j++)\n            {\n                temp.m_data[i][j] = 0;\n                for (int k=0 ; k<NumberRows ; k++)\n                    temp.m_data[i][j] += m_data[i][k] * rhs.m_data[k][j];\n            }\n        *this = temp;\n        return *this;\n    }\n    ```\n\n41.  构建并运行测试——同样，它们应该会通过。\n42.  Introduce a second helper function to the test class by declaring it in the `Matrix3dTest` class:\n\n    ```cpp\n    void VerifyMatrixIsIdentity(Matrix3d& mat);\n    ```\n\n    然后，声明它以便我们可以使用它:\n\n    ```cpp\n    void Matrix3dTest::VerifyMatrixIsIdentity(Matrix3d& mat)\n    {\n    for( int row{0} ; row<4 ; row++)\n        for( int col{0} ; col<4 ; col++)\n        {\n            int expected = (row==col) ? 1 : 0;\n            ASSERT_FLOAT_EQ(expected, mat(row,col)) \n                                 << \"cell[\" << row << \"][\" << col << \"]\";\n        }\n    }\n    ```\n\n43.  更新一个测试来使用它:\n\n    ```cpp\n    TEST_F(Matrix3dTest, DefaultConstructorIsIdentity)\n    {\n        Matrix3d mat;\n        VerifyMatrixIsIdentity(mat);\n    }\n    ```\n\n44.  写一个健全性检查测试:\n\n    ```cpp\n    TEST_F(Matrix3dTest, IdentityTimesIdentityIsIdentity)\n    {\n        Matrix3d mat;\n        Matrix3d result = mat * mat;\n        VerifyMatrixIsIdentity(result);\n    }\n    ```\n\n45.  构建并运行测试——它们应该仍然会通过。\n46.  现在，我们需要能够将点和矩阵相乘。增加以下测试:\n\n    ```cpp\n    TEST_F(Matrix3dTest, MultiplyMatrixWithPoint)\n    {\n        Matrix3d mat { {1,2,3,4}, {5,6,7,8},    {9,10,11,12},  {13,14,15,16}};\n        Point3d pt {15, 25, 35, 45};\n        Point3d expected{350, 830, 1310, 1790};\n        Point3d pt2 = mat * pt;\n        ASSERT_EQ(expected, pt2);\n    }\n    ```\n\n47.  在 **matrix3d.hpp** 中，添加 point3d.hpp 的 include 指令，并在`Matrix3d`类声明后添加以下声明:\n\n    ```cpp\n    Point3d operator*(const Matrix3d& lhs, const Point3d& rhs);\n    ```\n\n48.  将操作员的定义添加到 **matrix3d.cpp** 文件中:\n\n    ```cpp\n    Point3d operator*(const Matrix3d& lhs, const Point3d& rhs)\n    {\n        Point3d pt;\n        for(int row{0} ; row<Matrix3d::NumberRows ; row++)\n        {\n            float sum{0};\n            for(int col{0} ; col<Matrix3d::NumberColumns ; col++)\n            {\n                sum += lhs(row, col) * rhs(col);\n            }\n            pt(row) = sum;\n        }\n        return pt;\n    }\n    ```\n\n49.  构建并运行测试。他们都应该再次路过。\n50.  在**矩阵 3dtests.cpp** 的顶部，添加包含文件:\n\n    ```cpp\n    #include <cmath>\n    ```\n\n51.  开始添加转换矩阵工厂方法。使用以下测试，我们将开发各种工厂方法(测试应一次添加一个):\n\n    ```cpp\n    TEST_F(Matrix3dTest, CreateTranslateIsCorrect)\n    {\n        Matrix3d mat = createTranslationMatrix(-0.5, 2.5, 10.0);\n        Matrix3d expected {{1.0, 0.0, 0.0, -0.5},\n                           {0.0, 1.0, 0.0, 2.5},\n                           {0.0, 0.0, 1.0, 10.0},\n                           {0.0, 0.0, 0.0, 1.0}\n        };\n        VerifyMatrixResult(expected, mat);\n    }\n    TEST_F(Matrix3dTest, CreateScaleIsCorrect)\n    {\n        Matrix3d mat = createScaleMatrix(3.0, 2.5, 11.0);\n        Matrix3d expected {{3.0, 0.0,  0.0, 0.0},\n                           {0.0, 2.5,  0.0, 0.0},\n                           {0.0, 0.0, 11.0, 0.0},\n                           {0.0, 0.0,  0.0, 1.0}\n        };\t\n        VerifyMatrixResult(expected, mat);\n    }\n    TEST_F(Matrix3dTest, CreateRotateX90IsCorrect)\n    {\n        Matrix3d mat = createRotationMatrixAboutX(90.0F);\n        Matrix3d expected {{1.0, 0.0,  0.0, 0.0},\n                           {0.0, 0.0, -1.0, 0.0},\n                           {0.0, 1.0,  0.0, 0.0},\n                           {0.0, 0.0,  0.0, 1.0}\n        };\n        VerifyMatrixResult(expected, mat);\n    }\n    TEST_F(Matrix3dTest, CreateRotateX60IsCorrect)\n    {\n        Matrix3d mat = createRotationMatrixAboutX(60.0F);\n        float sqrt3_2 = static_cast<float>(std::sqrt(3.0)/2.0);\n        Matrix3d expected {{1.0, 0.0,     0.0,     0.0},\n                           {0.0, 0.5,    -sqrt3_2, 0.0},\n                           {0.0, sqrt3_2,  0.5,    0.0},\n                           {0.0, 0.0,     0.0,     1.0}\n        };\n        VerifyMatrixResult(expected, mat);\n    }\n    TEST_F(Matrix3dTest, CreateRotateY90IsCorrect)\n    {\n        Matrix3d mat = createRotationMatrixAboutY(90.0F);\n        Matrix3d expected {{0.0, 0.0,  1.0, 0.0},\n                           {0.0, 1.0,  0.0, 0.0},\n                           {-1.0, 0.0, 0.0, 0.0},\n                           {0.0, 0.0,  0.0, 1.0}\n        };\n        VerifyMatrixResult(expected, mat);\n    }\n    TEST_F(Matrix3dTest, CreateRotateY60IsCorrect)\n    {\n        Matrix3d mat = createRotationMatrixAboutY(60.0F);\n        float sqrt3_2 = static_cast<float>(std::sqrt(3.0)/2.0);\n        Matrix3d expected {{0.5,      0.0,   sqrt3_2,  0.0},\n                           {0.0,      1.0,    0.0,     0.0},\n                           {-sqrt3_2, 0.0,    0.5,     0.0},\n                           {0.0,      0.0,    0.0,     1.0}\n        };\n        VerifyMatrixResult(expected, mat);\n    }\n    TEST_F(Matrix3dTest, CreateRotateZ90IsCorrect)\n    {\n        Matrix3d mat = createRotationMatrixAboutZ(90.0F);\n        Matrix3d expected {{0.0, -1.0,  0.0, 0.0},\n                           {1.0, 0.0,  0.0, 0.0},\n                           {0.0, 0.0,  1.0, 0.0},\n                           {0.0, 0.0,  0.0, 1.0}\n        };\n        VerifyMatrixResult(expected, mat);\n    }\n    TEST_F(Matrix3dTest, CreateRotateZ60IsCorrect)\n    {\n        Matrix3d mat = createRotationMatrixAboutZ(60.0F);\n        float sqrt3_2 = static_cast<float>(std::sqrt(3.0)/2.0);\n        Matrix3d expected {{0.5,     -sqrt3_2,   0.0,  0.0},\n                           {sqrt3_2,      0.5,   0.0,  0.0},\n                           {0.0,          0.0,   1.0,  0.0},\n                           {0.0,          0.0,   0.0,  1.0}\n        };\n        VerifyMatrixResult(expected, mat);\n    }\n    ```\n\n52.  向 matrix3d 头文件添加以下声明:\n\n    ```cpp\n    Matrix3d createTranslationMatrix(float dx, float dy, float dz);\n    Matrix3d createScaleMatrix(float sx, float sy, float sz);\n    Matrix3d createRotationMatrixAboutX(float degrees);\n    Matrix3d createRotationMatrixAboutY(float degrees);\n    Matrix3d createRotationMatrixAboutZ(float degrees);\n    ```\n\n53.  在 matrix3d 实现文件的顶部，添加`#include < cmath >`。\n54.  最后，将以下实现添加到`matrix3d`实现文件中:\n\n    ```cpp\n    Matrix3d createTranslationMatrix(float dx, float dy, float dz)\n    {\n        Matrix3d matrix;\n        matrix(0, 3) = dx;\n        matrix(1, 3) = dy;\n        matrix(2, 3) = dz;\n        return matrix;\n    }\n    Matrix3d createScaleMatrix(float sx, float sy, float sz)\n    {\n        Matrix3d matrix;\n        matrix(0, 0) = sx;\n        matrix(1, 1) = sy;\n        matrix(2, 2) = sz;\n        return matrix;\n    }\n    Matrix3d createRotationMatrixAboutX(float degrees)\n    {\n        Matrix3d matrix;\n        double pi{4.0F*atan(1.0F)};\n        double radians = degrees / 180.0 * pi;\n        float cos_theta = static_cast<float>(cos(radians));\n        float sin_theta = static_cast<float>(sin(radians));\n        matrix(1, 1) =  cos_theta;\n        matrix(2, 2) =  cos_theta;\n        matrix(1, 2) = -sin_theta;\n        matrix(2, 1) =  sin_theta;\n        return matrix;\n    }\n    Matrix3d createRotationMatrixAboutY(float degrees)\n    {\n        Matrix3d matrix;\n        double pi{4.0F*atan(1.0F)};\n        double radians = degrees / 180.0 * pi;\n        float cos_theta = static_cast<float>(cos(radians));\n        float sin_theta = static_cast<float>(sin(radians));\n        matrix(0, 0) =  cos_theta;\n        matrix(2, 2) =  cos_theta;\n        matrix(0, 2) =  sin_theta;\n        matrix(2, 0) = -sin_theta;\n        return matrix;\n    }\n    Matrix3d createRotationMatrixAboutZ(float degrees)\n    {\n        Matrix3d matrix;\n        double pi{4.0F*atan(1.0F)};\n        double radians = degrees / 180.0 * pi;\n        float cos_theta = static_cast<float>(cos(radians));\n        float sin_theta = static_cast<float>(sin(radians));\n        matrix(0, 0) =  cos_theta;\n        matrix(1, 1) =  cos_theta;\n        matrix(0, 1) = -sin_theta;\n        matrix(1, 0) =  sin_theta;\n        return matrix;\n    }\n    ```\n\n55.  为了编译并通过测试，我们需要在`matrix3d` :\n\n    ```cpp\n    float& operator()(const int row, const int column)\n    {\n        return m_data[row][column];\n    }\n    ```\n\n    的声明中增加一个访问器\n56.  再次构建并运行所有测试，以显示它们都通过了。\n57.  在`point3d.hpp`中，添加`< ostream >`的 include，并在末尾的 point3d 类中添加以下好友声明:\n\n    ```cpp\n    friend std::ostream& operator<<(std::ostream& , const Point3d& );\n    ```\n\n58.  在类后写操作符的内联实现:\n\n    ```cpp\n    inline std::ostream&\n    operator<<(std::ostream& os, const Point3d& pt)\n    {\n        const char* sep = \"[ \";\n        for(auto value : pt.m_data)\n        {\n            os << sep  << value;\n            sep = \", \";\n        }\n        os << \" ]\";\n        return os;\n    }\n    ```\n\n59.  打开 **main.cpp** 文件，删除行:\n\n    ```cpp\n    //#define ACTIVITY1\n    ```\n\n    中的注释分隔符//号\n60.  构建并运行名为`图形`的应用–您需要创建一个新的运行配置。如果您对`点 3d`和`矩阵 3d`的实现是正确的，那么程序将显示以下输出:\n\n![](img/C14583_02A_53.jpg)\n\n###### 图 2A.53:成功运行活动程序\n\n在本练习中，我们实现了两个类，它们构成了实现三维图形渲染所需的所有操作的基础。我们使用运算符重载来实现这一点，这样 Matrix3d 和 Point3d 就可以像本地类型一样使用。这可以很容易地扩展到处理点的向量，如果我们希望操纵整个对象，这是必需的。\n\n## 章节-2B-不允许鸭子-模板和演绎\n\n### 活动 1:开发通用“包含”模板函数\n\n在本练习中，我们将实现几个助手类，用于检测`std::string`类案例和`std::set`案例，然后使用它们为特定容器定制 contains 函数。按照以下步骤实施本活动:\n\n1.  从**第 2B 课/练习 01** 文件夹加载准备好的项目。构建和配置启动器并运行单元测试(未通过一个虚拟测试)。我们建议测试跑者的名字是`L2BA1tests`。\n2.  Open the **containsTests.cpp** file and replace the existing test with the following:\n\n    ```cpp\n    TEST_F(containsTest, DetectNpos)\n    {\n        ASSERT_TRUE(has_npos_v<std::string>);\n        ASSERT_FALSE(has_npos_v<std::set<int>>);\n        ASSERT_FALSE(has_npos_v<std::vector<int>>);\n    }\n    ```\n\n    这个测试要求我们编写一组帮助器模板来检测容器类是否支持一个名为 NPO 的静态成员变量。\n\n3.  Add the following code to the **contains.hpp** file:\n\n    ```cpp\n    template <class T>\n    auto test_npos(int) -> decltype((void)T::npos, std::true_type{});\n    template <class T>\n    auto test_npos(long) -> std::false_type;\n    template <class T>\n    struct has_npos : decltype(test_npos<T>(0)) {};\n    template< class T >\n    inline constexpr bool has_npos_v = has_npos<T>::value;\n    ```\n\n    测试现在运行并通过。\n\n4.  Add the following tests to the **containsTest.cpp** file:\n\n    ```cpp\n    TEST_F(containsTest, DetectFind)\n    {\n        ASSERT_TRUE((has_find_v<std::string, char>));\n        ASSERT_TRUE((has_find_v<std::set<int>, int>));\n        ASSERT_FALSE((has_find_v<std::vector<int>, int>));\n    }\n    ```\n\n    这个测试需要我们编写一组帮助器模板来检测容器类是否有一个接受一个参数的`find()`方法。\n\n5.  Add the following code to the **contains.hpp** file:\n\n    ```cpp\n    template <class T, class A0>\n    auto test_find(int) -> \n           decltype(void(std::declval<T>().find(std::declval<A0>())), \n                                                            std::true_type{});\n    template <class T, class A0>\n    auto test_find(long) -> std::false_type;\n    template <class T, class A0>\n    struct has_find : decltype(test_find<T,A0>(0)) {};\n    template< class T, class A0 >\n    inline constexpr bool has_find_v = has_find<T, A0>::value;\n    ```\n\n    测试现在运行并通过。\n\n6.  添加通用容器的实现；在这种情况下，向量。在**包含测试. cpp** 文件中写下以下测试:\n\n    ```cpp\n    TEST_F(containsTest, VectorContains)\n    {\n        std::vector<int> container {1,2,3,4,5};\n        ASSERT_TRUE(contains(container, 5));\n        ASSERT_FALSE(contains(container, 15));\n    }\n    ```\n\n7.  Add the basic implementation of `contains` to the **contains.hpp** file:\n\n    ```cpp\n    template<class C, class T>\n    auto contains(const C& c, const T& key) -> decltype(std::end(c), true)\n    {\n            return std::end(c) != std::find(begin(c), end(c), key);\n    }\n    ```\n\n    测试现在运行并通过。\n\n8.  下一步是将`设置`特例的测试添加到**包含测试. cpp** :\n\n    ```cpp\n    TEST_F(containsTest, SetContains)\n    {\n        std::set<int> container {1,2,3,4,5};\n        ASSERT_TRUE(contains(container, 5));\n        ASSERT_FALSE(contains(container, 15));\n    }\n    ```\n\n9.  The implementation of `contains` is updated to test for the built-in `set::find()` method:\n\n    ```cpp\n    template<class C, class T>\n    auto contains(const C& c, const T& key) -> decltype(std::end(c), true)\n    {\n        if constexpr(has_find_v<C, T>)\n        {\n            return std::end(c) != c.find(key);\n        }\n        else\n        {\n            return std::end(c) != std::find(begin(c), end(c), key);\n        }\n    }\n    ```\n\n    测试现在运行并通过。\n\n10.  将`字符串`特例的测试添加到**包含测试. cpp** 文件:\n\n    ```cpp\n    TEST_F(containsTest, StringContains)\n    {\n        std::string container{\"This is the message\"};\n        ASSERT_TRUE(contains(container, \"the\"));\n        ASSERT_TRUE(contains(container, 'm'));\n        ASSERT_FALSE(contains(container, \"massage\"));\n        ASSERT_FALSE(contains(container, 'z'));\n    }\n    ```\n\n11.  Add the following implementation of `contains` to test for the presence of `npos` and tailor the use of the `find()` method:\n\n    ```cpp\n    template<class C, class T>\n    auto contains(const C& c, const T& key) -> decltype(std::end(c), true)\n    {\n        if constexpr(has_npos_v<C>)\n        {\n            return C::npos != c.find(key);\n        }\n        else\n        if constexpr(has_find_v<C, T>)\n        {\n            return std::end(c) != c.find(key);\n        }\n        else\n        {\n            return std::end(c) != std::find(begin(c), end(c), key);\n        }\n    }\n    ```\n\n    测试现在运行并通过。\n\n12.  构建和运行名为`的应用包含`。创建新的运行配置。如果 contains 模板的实现是正确的，那么程序将显示以下输出:\n\n![Figure 2B.36: Output from the successful implementation of contains ](img/C14583_02B_36.jpg)\n\n###### 图 2B.36:成功实现包含的输出\n\n在本练习中，我们结合 SFINAE 使用了各种模板技术，根据包含类的能力选择合适的`contains()`函数的实现。我们本可以使用一个通用的模板函数和一些专门的模板来达到同样的结果，但是我们选择了一条更少的道路，并展示了我们新发现的模板技能。\n\n## 第三章——能与应的距离——对象、指针和继承\n\n### 活动 1:用 RAII 和 Move 实现图形处理\n\n在本练习中，我们将开发之前的`Matrix3d`和`Point3d`类，以使用`unique_ptr < >`来管理与实现这些图形类所需的数据结构相关联的内存。让我们开始吧:\n\n1.  从**第 3 课/练习 01** 文件夹加载准备好的项目，并将项目的当前构建器配置为 **CMake Build(可移植)**。构建和配置启动器并运行单元测试。我们建议测试跑者的名字是 **L3A1graphicstests** 。\n2.  Open **point3d.hpp** and add the lines marked with a comment to the file:\n\n    ```cpp\n    // ... lines omitted\n    #include <initializer_list>\n    #include <ostream>\n    namespace acpp::gfx { // Add this line\n    class Point3d\n    {\n    // ... lines omitted\n    };\n    } // Add this line\n    ```\n\n    请注意，添加到文件末尾的右大括号没有右分号。嵌套命名空间语法`acpp::gfx`，是 C++ 17 的一个新特性。以前，它需要两次明确使用`名称空间`关键字。另外，请注意，为了有所帮助，友好的邻居 IDE 可能会在您放入名称空间声明的行之后插入右大括号。\n\n3.  对 **matrix3d.hpp** 、 **matrix3d.cpp** 和 **point3d.cpp** 重复相同的处理-确保包含文件不包含在命名空间的范围内。\n4.  在相应的文件( **main.cpp** 、 **matrix3dTests.cpp** 和**point 3 tests . CPP**中，在完成#include 指令后，插入以下行:\n\n    ```cpp\n    using namespace acpp::gfx;\n    ```\n\n5.  现在，运行所有测试。所有 **18** 现有测试应再次通过。我们已经成功地将我们的类放入了一个命名空间。\n6.  现在我们将继续转换`Matrix3d`类来使用堆分配的内存。在 **matrix3d.hpp** 文件中，添加一个`#include <内存>`行，让我们访问`unique_ptr < >`模板。\n7.  接下来，更改`m_data`的申报类型:\n\n    ```cpp\n    std::unique_ptr<float[]> m_data;\n    ```\n\n8.  From this point forward, we will use the compiler and its errors to give us hints as to what needs fixing. Attempting to build the tests now reveals that we have a problem with the following two methods in the header file\n\n    ```cpp\n    float operator()(const int row, const int column) const\n    {\n        return m_data[row][column];\n    }\n    float& operator()(const int row, const int column)\n    {\n        return m_data[row][column];\n    } \n    ```\n\n    这里的问题是`unique_ptr`持有一个指向一维数组而不是二维数组的指针。因此，我们需要将行和列转换成一个索引。\n\n9.  添加一个名为`get_index()`的新方法，从行和列中获取一维索引，并更新前面的函数来使用它:\n\n    ```cpp\n    float operator()(const int row, const int column) const\n    {\n        return m_data[get_index(row,column)];\n    }\n    float& operator()(const int row, const int column)\n    {\n        return m_data[get_index(row,column)];\n    }\n    private:\n    size_t get_index(const int row, const int column) const\n    {\n        return row * NumberColumns + column;\n    }\n    ```\n\n10.  重新编译后，编译器的下一个错误引用了以下内联函数:\n\n    ```cpp\n    inline Matrix3d operator*(const Matrix3d& lhs, const Matrix3d& rhs)\n    {\n        Matrix3d temp(lhs);   // <=== compiler error – ill formed copy constructor\n        temp *= rhs;\n        return temp;\n    }\n    ```\n\n11.  Whereas before, the default copy constructor was sufficient for our purposes, it just did a shallow copy of all the elements of the array and that was correct. We now have indirection to the data we need to copy and so we need to implement a deep copy constructor and copy assignment. We will also need to address the existing constructors. For now, just add the constructor declarations to the class (adjacent to the other constructors):\n\n    ```cpp\n    Matrix3d(const Matrix3d& rhs);\n    Matrix3d& operator=(const Matrix3d& rhs);\n    ```\n\n    尝试构建测试将会显示我们已经解决了头文件中的所有问题，并且我们可以进入实现文件。\n\n12.  修改两个构造函数初始化`unique_ptr`如下:\n\n    ```cpp\n    Matrix3d::Matrix3d() : m_data{new float[NumberRows*NumberColumns]}\n    {\n        for (int i{0} ; i< NumberRows ; i++)\n            for (int j{0} ; j< NumberColumns ; j++)\n                m_data[i][j] = (i==j);\n    }\n    Matrix3d::Matrix3d(std::initializer_list<std::initializer_list<float>> list)\n        : m_data{new float[NumberRows*NumberColumns]}\n    {\n        int i{0};\n        for(auto it1 = list.begin(); i<NumberRows ; ++ it1, ++ i)\n        {\n            int j{0};\n            for(auto it2 = it1->begin(); j<NumberColumns ; ++ it2, ++ j)\n                m_data[i][j] = *it2;\n        }\n    }\n    ```\n\n13.  我们现在需要解决一维数组查找问题。我们需要用`m_data[get_index(i，j)]`来更改`m_data[i][j]`类型的语句。将默认构造函数改为如下内容:\n\n    ```cpp\n    Matrix3d::Matrix3d() : m_data{new float[NumberRows*NumberColumns]}\n    {\n        for (int i{0} ; i< NumberRows ; i++)\n            for (int j{0} ; j< NumberColumns ; j++)\n                m_data[get_index(i, j)] = (i==j);          // <= change here\n    }\n    ```\n\n14.  将初始化列表构造函数改为如下:\n\n    ```cpp\n    Matrix3d::Matrix3d(std::initializer_list<std::initializer_list<float>> list)\n          : m_data{new float[NumberRows*NumberColumns]}\n    {\n        int i{0};\n        for(auto it1 = list.begin(); i<NumberRows ; ++ it1, ++ i)\n        {\n            int j{0};\n            for(auto it2 = it1->begin(); j<NumberColumns ; ++ it2, ++ j)\n                m_data[get_index(i, j)] = *it2;         // <= change here\n        }\n    }\n    ```\n\n15.  更改乘法运算符，注意索引:\n\n    ```cpp\n    Matrix3d& Matrix3d::operator*=(const Matrix3d& rhs)\n    {\n        Matrix3d temp;\n        for(int i=0 ; i<NumberRows ; i++)\n            for(int j=0 ; j<NumberColumns ; j++)\n            {\n                temp.m_data[get_index(i, j)] = 0;        // <= change here\n                for (int k=0 ; k<NumberRows ; k++)\n                    temp.m_data[get_index(i, j)] += m_data[get_index(i, k)] \n                                              * rhs.m_data[get_index(k, j)];\n                                                         // <= change here\n            }\n        *this = temp;\n        return *this;\n    }\n    ```\n\n16.  有了这些改变，我们已经修复了所有的编译器错误，但是现在我们有一个链接器错误要处理——复制构造函数，我们只在步骤 11 中声明了回来。\n17.  在 **matrix3d.cpp** 文件中添加以下定义:\n\n    ```cpp\n    Matrix3d::Matrix3d(const Matrix3d& rhs) : \n        m_data{new float[NumberRows*NumberColumns]}\n    {\n        *this = rhs;\n    }\n    Matrix3d& Matrix3d::operator=(const Matrix3d& rhs)\n    {\n        for(int i=0 ; i< NumberRows*NumberColumns ; i++)\n            m_data[i] = rhs.m_data[i];\n        return *this;\n    }\n    ```\n\n18.  测试将会建立，所有测试都将通过。下一步是强制移动构造函数。在 **matrix3d.cpp** 中找到`createTranslationMatrix()`方法，并将返回语句更改如下:\n\n    ```cpp\n    return std::move(matrix);\n    ```\n\n19.  在**矩阵 3d.hpp** 中声明`移动`构造函数。\n\n    ```cpp\n    Matrix3d(Matrix3d&& rhs);\n    ```\n\n20.  重建测试。现在，我们得到一个与移动构造函数不存在相关的错误。\n21.  将构造器的实现添加到 **matrix3d.cpp** 中，并重新构建测试。\n\n    ```cpp\n    Matrix3d::Matrix3d(Matrix3d&& rhs)\n    {\n        //std::cerr << \"Matrix3d::Matrix3d(Matrix3d&& rhs)\\n\";\n        std::swap(m_data, rhs.m_data);\n    }\n    ```\n\n22.  重建并运行测试。他们又都过去了。\n23.  Just to confirm that the move constructor is being called, add `#include <iostream>` to **matrix3d.cpp**, remove the comment from the output line in the move constructor. and re-run the test. It will report an error after the tests have completed because we sent it to the standard error channel (`cerr`). After the check, make the line a comment again.\n\n    #### 注意\n\n    关于移动构造函数，只需简单说明一下——我们没有像对其他构造函数那样显式初始化`m_data`。这意味着它将被初始化为空，然后与传入的参数交换，这是一个临时参数，因此在事务后不保存数组是可以接受的——它删除了一次内存分配和释放。\n\n24.  现在让我们转换`Point3d`类，以便它可以使用堆分配的内存。在 **point3d.hpp** 文件中，添加一个`#include <内存>`行，这样我们就可以访问`unique_ptr < >`模板。\n25.  接下来，将`m_data`的声明类型更改为如下所示:\n\n    ```cpp\n    std::unique_ptr<float[]> m_data;\n    ```\n\n26.  编译器现在告诉我们插入操作符(< point3d.hpp 有问题，因为我们不能在 **unique_ptr** 上使用 ranged-for:用以下内容替换实现:\n\n    ```cpp\n    inline std::ostream&\n    operator<<(std::ostream& os, const Point3d& pt)\n    {\n        const char* sep = \"[ \";\n        for(int i{0} ; i < Point3d::NumberRows ; i++)\n        {\n            os << sep << pt.m_data[i];\n            sep = \", \";\n        }\n        os << \" ]\";\n        return os;\n    } \n    ```\n\n27.  打开**点 3d.cpp** 并修改默认构造函数来初始化`unique_ptr`并更改初始化循环，因为在`unique_ptr` :\n\n    ```cpp\n    Point3d::Point3d() : m_data{new float[NumberRows]}\n    {\n        for(int i{0} ; i < NumberRows-1 ; i++) {\n            m_data[i] = 0;\n        }\n        m_data[NumberRows-1] = 1;\n    }\n    ```\n\n    上不能使用 ranged for\n28.  通过初始化`唯一 _ptr` :\n\n    ```cpp\n    Point3d::Point3d(std::initializer_list<float> list)\n                : m_data{new float[NumberRows]}\n    ```\n\n    来修改其他构造函数\n29.  现在所有的测试都运行并通过了，就像以前一样。\n30.  现在，如果我们运行原始应用 **L3graphics** ，那么输出将与原始应用相同，但是实现使用 RAII 来分配和管理用于矩阵和点的内存。\n\n![](img/C14583_03_52.jpg)\n\n###### 图 3.52:成功转换为使用 RAII 后的活动 1 输出\n\n## 活动 2:实现日期计算的类\n\n在这个活动中，我们将实现两个类，`日期`和`天数`，这将使我们很容易处理日期和它们之间的时间差。让我们开始吧:\n\n1.  从**第 3 课/练习 02** 文件夹加载准备好的项目，并将项目的当前构建器配置为 **CMake Build(可移植)**。构建和配置启动器并运行单元测试。我们建议测试跑者的名字是 **L3A2datetests** 。该项目有虚拟文件和一个失败的测试。\n2.  在编辑器中打开 **date.hpp** 文件，并在基本`Date`类中添加以下行，以允许访问存储的值:\n\n    ```cpp\n    int Day()   const {return m_day;}\n    int Month() const {return m_month;}\n    int Year()  const {return m_year;}\n    ```\n\n3.  Open the **dateTests.cpp** file and add the following code to the `DateTest` class:\n\n    ```cpp\n    void VerifyDate(const Date& dt, int yearExp, int monthExp, int dayExp) const\n    {\n        ASSERT_EQ(dayExp, dt.Day());\n        ASSERT_EQ(monthExp, dt.Month());\n        ASSERT_EQ(yearExp, dt.Year());\n    }\n    ```\n\n    通常，随着测试的发展，您会重构这个测试，但是我们会提前把它拿出来。\n\n4.  将现有测试中的`ASSERT_FALSE()`替换为以下测试:\n\n    ```cpp\n    Date dt;\n    VerifyDate(dt, 1970, 1, 1);\n    ```\n\n5.  重建并运行测试——它们现在应该都通过了。\n6.  增加以下测试:\n\n    ```cpp\n    TEST_F(DateTest, Constructor1970Jan2)\n    {\n        Date dt(2, 1, 1970);\n        VerifyDate(dt, 1970, 1, 2);\n    }\n    ```\n\n7.  为了进行这个测试，我们需要在`日期`类中添加以下两个构造函数:\n\n    ```cpp\n    Date() = default;\n    Date(int day, int month, int year) :\n            m_year{year}, m_month{month}, m_day{day}\n    {\n    }\n    ```\n\n8.  我们现在需要引入转换为/从`日期 _t`类型的函数。将以下别名添加到我们名称空间内的 **date.hpp** 文件中:\n\n    ```cpp\n    using date_t=int64_t;\n    ```\n\n9.  在`日期`类中，添加以下方法的声明:\n\n    ```cpp\n    date_t ToDateT() const;\n    ```\n\n10.  然后，添加以下测试:\n\n    ```cpp\n    TEST_F(DateTest, ToDateTDefaultIsZero)\n    {\n        Date dt;\n        ASSERT_EQ(0, dt.ToDateT());\n    }\n    ```\n\n11.  当我们在做(`TDD`)时，我们添加了方法的最小实现来通过测试。\n\n    ```cpp\n    date_t Date::ToDateT() const\n    {\n        return 0;\n    }\n    ```\n\n12.  现在，我们添加下一个测试:\n\n    ```cpp\n    TEST_F(DateTest, ToDateT1970Jan2Is1)\n    {\n        Date dt(2, 1, 1970);\n        ASSERT_EQ(1, dt.ToDateT());\n    }\n    ```\n\n13.  我们继续添加一个又一个测试，一直在`today te()`中细化算法，先处理`1970`中的日期，然后`1-1971 年 1 月`中的日期，然后是`1973`中的日期，这意味着我们跨越了一个闰年，以此类推。用于开发`TodayTet()`方法的全套测试如下:\n\n    ```cpp\n    TEST_F(DateTest, ToDateT1970Dec31Is364)\n    {\n        Date dt(31, 12, 1970);\n        ASSERT_EQ(364, dt.ToDateT());\n    }\n    TEST_F(DateTest, ToDateT1971Jan1Is365)\n    {\n        Date dt(1, 1, 1971);\n        ASSERT_EQ(365, dt.ToDateT());\n    }\n    TEST_F(DateTest, ToDateT1973Jan1Is1096)\n    {\n        Date dt(1, 1, 1973);\n        ASSERT_EQ(365*3+1, dt.ToDateT());\n    }\n    TEST_F(DateTest, ToDateT2019Aug28Is18136)\n    {\n        Date dt(28, 8, 2019);\n        ASSERT_EQ(18136, dt.ToDateT());\n    }\n    ```\n\n14.  为了通过所有这些测试，我们在`日期`类别的申报中添加了以下项目:\n\n    ```cpp\n    public:\n        static constexpr int EpochYear = 1970;\n        static constexpr int DaysPerCommonYear = 365;\n        static constexpr int YearsBetweenLeapYears = 4;\n    private:\n        int GetDayOfYear(int day, int month, int year) const;\n        bool IsLeapYear(int year) const;\n        int CalcNumberLeapYearsFromEpoch(int year) const;\n    ```\n\n15.  `today Tet()`**date . CPP**的实施及配套方式如下:\n\n    ```cpp\n    namespace {\n    int daysBeforeMonth[2][12] =\n    {\n        { 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 204, 334}, // Common Year\n        { 0, 31, 50, 91, 121, 152, 182, 213, 244, 274, 205, 335}  // Leap Year\n    };\n    }\n    namespace acpp::date\n    {\n    int Date::CalcNumberLeapYearsFromEpoch(int year) const\n    {\n        return (year-1)/YearsBetweenLeapYears\n                                       - (EpochYear-1)/YearsBetweenLeapYears;\n    }\n    int Date::GetDayOfYear(int day, int month, int year) const\n    {\n        return daysBeforeMonth[IsLeapYear(year)][month-1] + day;\n    }\n    bool Date::IsLeapYear(int year) const\n    {\n        return (year%4)==0;   // Not full story, but good enough to 2100\n    }\n    date_t Date::ToDateT() const\n    {\n        date_t value = GetDayOfYear(m_day, m_month, m_year) - 1;\n        value += (m_year-EpochYear) * DaysPerCommonYear;\n        date_t numberLeapYears = CalcNumberLeapYearsFromEpoch(m_year);\n        value += numberLeapYears;\n        return value;\n    }\n    }\n    ```\n\n16.  现在`todayt()`工作了，我们转到它的逆，也就是`FromDateT()`。同样，我们一次建立一个测试来开发一系列日期的算法。使用了以下测试:\n\n    ```cpp\n    TEST_F(DateTest, FromDateT0Is1Jan1970)\n    {\n        Date dt;\n        dt.FromDateT(0);\n        ASSERT_EQ(0, dt.ToDateT());\n        VerifyDate(dt, 1970, 1, 1);\n    }\n    TEST_F(DateTest, FromDateT1Is2Jan1970)\n    {\n        Date dt;\n        dt.FromDateT(1);\n        ASSERT_EQ(1, dt.ToDateT());\n        VerifyDate(dt, 1970, 1, 2);\n    }\n    TEST_F(DateTest, FromDateT364Is31Dec1970)\n    {\n        Date dt;\n        dt.FromDateT(364);\n        ASSERT_EQ(364, dt.ToDateT());\n        VerifyDate(dt, 1970, 12, 31);\n    }\n    TEST_F(DateTest, FromDateT365Is1Jan1971)\n    {\n        Date dt;\n        dt.FromDateT(365);\n        ASSERT_EQ(365, dt.ToDateT());\n        VerifyDate(dt, 1971, 1, 1);\n    }\n    TEST_F(DateTest, FromDateT1096Is1Jan1973)\n    {\n        Date dt;\n        dt.FromDateT(1096);\n        ASSERT_EQ(1096, dt.ToDateT());\n        VerifyDate(dt, 1973, 1, 1);\n    }\n    TEST_F(DateTest, FromDateT18136Is28Aug2019)\n    {\n        Date dt;\n        dt.FromDateT(18136);\n        ASSERT_EQ(18136, dt.ToDateT());\n        VerifyDate(dt, 2019, 8, 28);\n    }\n    ```\n\n17.  在头文件中添加以下声明:\n\n    ```cpp\n    public:\n        void FromDateT(date_t date);\n    private:\n        int CalcMonthDayOfYearIsIn(int dayOfYear, bool IsLeapYear) const;\n    ```\n\n18.  使用以下实现，因为前面的测试一次添加一个:\n\n    ```cpp\n    void Date::FromDateT(date_t date)\n    {\n        int number_years = date / DaysPerCommonYear;\n        date = date - number_years * DaysPerCommonYear;\n        m_year = EpochYear + number_years;\n        date_t numberLeapYears = CalcNumberLeapYearsFromEpoch(m_year);\n        date -= numberLeapYears;\n        m_month = CalcMonthDayOfYearIsIn(date, IsLeapYear(m_year));\n        date -= daysBeforeMonth[IsLeapYear(m_year)][m_month-1];\n        m_day = date + 1;\n    }\n    int Date::CalcMonthDayOfYearIsIn(int dayOfYear, bool isLeapYear) const\n    {\n        for(int i = 1 ; i < 12; i++)\n        {\n        if ( daysBeforeMonth[isLeapYear][i] > dayOfYear)\n                return i;\n        }\n        return 12;\n    }\n    ```\n\n19.  现在我们已经准备好了支持例程，我们可以实现两个日期之间的`日期`类差异的真正特征，并通过添加天数来确定新的日期。这两个操作都需要一个新的类型(类)`天`。\n20.  在表头`日期`上方增加`天数`如下执行:\n\n    ```cpp\n    class Days\n    {\n    public:\n        Days() = default;\n        Days(int days) : m_days{days}     {    }\n        operator int() const\n        {\n            return m_days;\n        }\n    private:\n        int m_days{0};\n    };\n    ```\n\n21.  第一个操作符是将`日`添加到`日`。添加以下方法声明(在`日期`类的公共部分内):\n\n    ```cpp\n    Date& operator+=(const Days& day);\n    ```\n\n22.  然后，将内联实现(在`日期`类之外)添加到头文件:\n\n    ```cpp\n    inline Date operator+(const Date& lhs, const Days& rhs )\n    {\n        Date tmp(lhs);\n        tmp += rhs;\n        return tmp;\n    }\n    ```\n\n23.  编写以下测试以验证`总和`操作:\n\n    ```cpp\n    TEST_F(DateTest, AddZeroDays)\n    {\n        Date dt(28, 8, 2019);\n        Days days;\n        dt += days;\n        VerifyDate(dt, 2019, 8, 28);\n    }\n    TEST_F(DateTest, AddFourDays)\n    {\n        Date dt(28, 8, 2019);\n        Days days(4);\n        dt += days;\n        VerifyDate(dt, 2019, 9, 1);\n    }\n    ```\n\n24.  `sum`操作的实际实现简单基于两种支持方式\n\n    ```cpp\n    Date& Date::operator+=(const Days& day)\n    {\n        FromDateT(ToDateT()+day);\n        return *this;\n    }\n    ```\n\n25.  增加以下测试:\n\n    ```cpp\n    TEST_F(DateTest, AddFourDaysAsInt)\n    {\n        Date dt(28, 8, 2019);\n        dt += 4;\n        VerifyDate(dt, 2019, 9, 1);\n    }\n    ```\n\n26.  当我们运行测试时，它们都构建好了，这个测试通过了。但这不是理想的结果。我们不希望他们能够在我们的日期上加上整数。(未来的版本可能会增加月份和年份，那么增加一个整数意味着什么呢？).为了避免构建失败，我们将 Days 构造函数改为`显式` :\n\n    ```cpp\n    explicit Days(int days) : m_days{days}     {    }\n    ```\n\n27.  Now the build fails, so we need to fix the test by changing the addition line to cast to `Days` as follows:\n\n    ```cpp\n    dt += static_cast<Days>(4);\n    ```\n\n    所有测试都应该再次通过。\n\n28.  我们想要的最后一个功能是两个日期之间的差异。以下是用于验证实现的测试:\n\n    ```cpp\n    TEST_F(DateTest, DateDifferences27days)\n    {\n        Date dt1(28, 8, 2019);\n        Date dt2(1, 8, 2019);\n        Days days = dt1 - dt2;\n        ASSERT_EQ(27, (int)days);\n    }\n    TEST_F(DateTest, DateDifferences365days)\n    {\n        Date dt1(28, 8, 2019);\n        Date dt2(28, 8, 2018);\n        Days days = dt1 - dt2;\n        ASSERT_EQ(365, (int)days);\n    }\n    ```\n\n29.  在头文件`日期`类的公共部分添加以下函数声明:\n\n    ```cpp\n    Days operator-(const Date& rhs) const;\n    ```\n\n30.  Add the following code after the Date class in the header file:\n\n    ```cpp\n    inline Days Date::operator-(const Date& rhs) const\n    {\n        return Days(ToDateT() - rhs.ToDateT());\n    }\n    ```\n\n    因为我们将`Days`构造函数显式化了，所以我们必须在 return 语句中调用它。随着所有这些变化的到位，所有的测试都应该通过。\n\n31.  将`L3A2date`配置为`datetools`二进制，在编辑器中打开 main.cpp。从`活动 2`的定义中删除注释:\n\n    ```cpp\n    #define ACTIVITY2\n    ```\n\n32.  构建并运行示例应用。这将产生以下输出:\n\n![Figure 3.53: Output of successful Date sample application ](img/C14583_03_53.jpg)\n\n###### 图 3.53:成功的日期示例应用的输出\n\n我们已经实现了日期和天数类的所有要求，并通过单元测试交付了它们。单元测试允许我们实现增量功能来构建两个复杂的算法，即`todayt`和`FromDateT`，它们形成了对我们想要交付的功能的底层支持。\n\n## 第 4 章-关注点分离-软件架构、功能、可变模板\n\n### 活动 1:实现多播事件处理程序\n\n1.  从**第 4 课/练习 01** 文件夹加载准备好的项目，并将项目的当前构建器配置为可移植构建。构建项目，配置启动器并运行单元测试(未通过一个虚拟测试)。推荐测试运行者的名字为 *L4delegateTests* 。\n2.  在**委托测试. cpp** 中，用以下测试替换失败的虚拟测试:\n\n    ```cpp\n    TEST_F(DelegateTest, BasicDelegate)\n    {\n        Delegate delegate;\n        ASSERT_NO_THROW(delegate.Notify(42));\n    }\n    ```\n\n3.  This now fails to build, so we need to add a new method to `Delegate`. As this will evolve into a template, we will do all of this development in the header file. In **delegate.hpp**, and the following definition:\n\n    ```cpp\n    class Delegate\n    {\n    public:\n        Delegate() = default;\n        void Notify(int value) const\n        {\n        }\n    };\n    ```\n\n    测试现在运行并通过。\n\n4.  在现有测试中添加以下行:\n\n    ```cpp\n    ASSERT_NO_THROW(delegate(22));\n    ```\n\n5.  Again, the build fails, so we update the `Delegate` definition as follows (we could have had Notify call `operator()`, but this is easier to read):\n\n    ```cpp\n    void operator()(int value)\n    {\n        Notify(value);\n    }\n    ```\n\n    测试再次运行并通过。\n\n6.  在添加下一个测试之前，我们将添加一些基础设施来帮助我们开发测试。处理程序最简单的方法是让它们写入`std::cout`，为了能够验证它们被调用，我们需要捕获输出。为此，通过更改`DelegateTest`类将标准输出流重新路由到不同的缓冲区，如下所示:\n\n    ```cpp\n    class DelegateTest : public ::testing::Test\n    {\n    public:\n        void SetUp() override;\n        void TearDown() override;\n        std::stringstream m_buffer;\n        // Save cout's buffer here\n        std::streambuf *m_savedBuf{};\n    };\n    void DelegateTest::SetUp()\n    {\n        // Save the cout buffer\n        m_savedBuf = std::cout.rdbuf();\n        // Redirect cout to our buffer\n        std::cout.rdbuf(m_buffer.rdbuf());\n    }\n    void DelegateTest::TearDown()\n    {\n        // Restore cout buffer to original\n        std::cout.rdbuf(m_savedBuf);\n    }\n    ```\n\n7.  同时将`<>``<>`和`<字符串>`的 include 语句添加到文件顶部。\n8.  有了这个支持框架，添加以下测试:\n\n    ```cpp\n    TEST_F(DelegateTest, SingleCallback)\n    {\n        Delegate delegate;\n        delegate += [] (int value) { std::cout << \"value = \" << value; };\n        delegate.Notify(42);\n        std::string result = m_buffer.str();\n        ASSERT_STREQ(\"value = 42\", result.c_str());\n    }\n    ```\n\n9.  To make the tests build and run again, add the following code in the **delegate.h** class:\n\n    ```cpp\n    Delegate& operator+=(const std::function<void(int)>& delegate)\n    {\n        m_delegate = delegate;\n        return *this;\n    }\n    ```\n\n    以及以下代码:\n\n    ```cpp\n    private:\n        std::function<void(int)> m_delegate;\n    ```\n\n    测试正在构建，但是我们的新测试失败了。\n\n10.  将`通知()`方法更新为:\n\n    ```cpp\n    void Notify(int value) const\n    {\n        m_delegate(value);\n    }\n    ```\n\n11.  The tests now build and our new test passes, but the original test now fails. The call to the delegate is throwing an exception, so we need to check that the delegate is not empty before calling it. Write the following code to do this:\n\n    ```cpp\n    void Notify(int value) const\n    {\n        if(m_delegate)\n            m_delegate(value);\n    }\n    ```\n\n    所有的测试现在运行并通过。\n\n12.  我们现在需要向`代理`类添加多播支持。添加新测试:\n\n    ```cpp\n    TEST_F(DelegateTest, DualCallbacks)\n    {\n        Delegate delegate;\n        delegate += [] (int value) { std::cout << \"1: = \" << value << \"\\n\"; };\n        delegate += [] (int value) { std::cout << \"2: = \" << value << \"\\n\"; };\n        delegate.Notify(12);\n        std::string result = m_buffer.str();\n        ASSERT_STREQ(\"1: = 12\\n2: = 12\\n\", result.c_str());\n    }\n    ```\n\n13.  Of course, this test now fails because the `operator+=()` only assigns to the member variable. We need to add a list to store our delegates. We choose vector so we can add to the end of the list as we want to call the delegates in the order that they are added. Add `#include <vector>` to the top of **delegate.hpp** and update Delegate replace **m_delegate** with **m_delegates** vector of the callbacks:\n\n    ```cpp\n    class Delegate\n    {\n    public:\n        Delegate() = default;\n        Delegate& operator+=(const std::function<void(int)>& delegate)\n        {\n            m_delegates.push_back(delegate);\n            return *this;\n        }\n        void Notify(int value) const\n        {\n            for(auto& delegate : m_delegates)\n            {\n                delegate(value);\n            }\n        }\n        void operator()(int value)\n        {\n            Notify(value);\n        }\n    private:\n        std::vector<std::function<void(int)>> m_delegates;\n    };\n    ```\n\n    测试全部运行并再次通过。\n\n14.  我们现在已经实现了基本的多播`委托`类。我们现在需要将其转换为基于模板的类。通过将三个测试中`委托`的所有声明更改为`委托< int >`，更新现有测试。\n15.  现在更新 Delegate 类，在类前添加`模板<类 Arg >`将其转换为模板，并用`Arg` :\n\n    ```cpp\n    template<class Arg>\n    class Delegate\n    {\n    public:\n        Delegate() = default;\n        Delegate& operator+=(const std::function<void(Arg)>& delegate)\n        {\n            m_delegates.push_back(delegate);\n            return *this;\n        }\n        void Notify(Arg value) const\n        {\n            for(auto& delegate : m_delegates)\n            {\n                delegate(value);\n            }\n        }\n        void operator()(Arg value)\n        {\n            Notify(value);\n        }\n    private:\n        std::vector<std::function<void(Arg)>> m_delegates;\n    };\n    ```\n\n    替换四次出现的`int`\n16.  所有测试现在都像以前一样运行并通过，所以它仍然适用于处理程序的`int`参数。\n17.  添加以下测试并重新运行测试，以确认模板转换正确:\n\n    ```cpp\n    TEST_F(DelegateTest, DualCallbacksString)\n    {\n        Delegate<std::string&> delegate;\n        delegate += [] (std::string value) { std::cout << \"1: = \" << value << \"\\n\"; };\n        delegate += [] (std::string value) { std::cout << \"2: = \" << value << \"\\n\"; };\n        std::string hi{\"hi\"};\n        delegate.Notify(hi);\n        std::string result = m_buffer.str();\n        ASSERT_STREQ(\"1: = hi\\n2: = hi\\n\", result.c_str());\n    }\n    ```\n\n18.  Now it operates as a template that takes one argument. We need to convert it into a variadic template that takes zero or more arguments. Using the information from the last topic, update the template to the following:\n\n    ```cpp\n    template<typename... ArgTypes>\n    class Delegate\n    {\n    public:\n        Delegate() = default;\n        Delegate& operator+=(const std::function<void(ArgTypes...)>& delegate)\n        {\n            m_delegates.push_back(delegate);\n            return *this;\n        }\n        void Notify(ArgTypes&&... args) const\n        {\n            for(auto& delegate : m_delegates)\n            {\n                delegate(std::forward<ArgTypes>(args)...);\n            }\n        }\n        void operator()(ArgTypes&&... args)\n        {\n            Notify(std::forward<ArgTypes>(args)...);\n        }\n    private:\n        std::vector<std::function<void(ArgTypes...)>> m_delegates;\n    };\n    ```\n\n    测试应该仍然运行并通过。\n\n19.  Add two more tests – zero argument test, and a mutliple argument test:\n\n    ```cpp\n    TEST_F(DelegateTest, DualCallbacksNoArgs)\n    {\n        Delegate delegate;\n        delegate += [] () { std::cout << \"CB1\\n\"; };\n        delegate += [] () { std::cout << \"CB2\\n\"; };\n        delegate.Notify();\n        std::string result = m_buffer.str();\n        ASSERT_STREQ(\"CB1\\nCB2\\n\", result.c_str());\n    }\n    TEST_F(DelegateTest, DualCallbacksStringAndInt)\n    {\n        Delegate<std::string&, int> delegate;\n        delegate += [] (std::string& value, int i) {\n                std::cout << \"1: = \" << value << \",\" << i << \"\\n\"; };\n        delegate += [] (std::string& value, int i) {\n            std::cout << \"2: = \" << value << \",\" << i << \"\\n\"; };\n        std::string hi{\"hi\"};\n        delegate.Notify(hi, 52);\n        std::string result = m_buffer.str();\n        ASSERT_STREQ(\"1: = hi,52\\n2: = hi,52\\n\", result.c_str());\n    }\n    ```\n\n    所有测试运行并通过，表明我们现在已经实现了期望的`委托`类。\n\n20.  Now, change the Run configuration to execute the program `L4delegate`. Open the **main.cpp** file in the editor and change the definition at the top of the file to the following and run the program:\n\n    ```cpp\n    #define ACTIVITY_STEP 27\n    ```\n\n    我们得到以下输出:\n\n![Figure 4.35: Output from the successful implementation of Delegate ](img/C14583_04_35.jpg)\n\n###### 图 4.35:成功实现委托的输出\n\n在本练习中，我们首先实现了一个提供基本单委托功能的类，然后添加了多播功能。有了这个实现，单元测试就位，我们能够快速地转换成一个带有一个参数的模板，然后转换成一个可变的模板版本。根据您正在开发的功能，特定实现过渡到通用形式，然后再过渡到更通用形式的方法是正确的。可变模板的开发并不总是显而易见的。\n\n## 第 5 章——哲学家的晚餐——线程和并发\n\n### 活动 1:创建一个模拟器来模拟美术馆的工作\n\n美术馆工作模拟器是一个模拟参观者和看守人行为的应用。参观者有数量限制，即只能有 50 人同时进入画廊。参观者不断来到画廊。看守人检查是否超过了访客人数的限制。如果是这样，它会要求新访客等待，并将他们放在等候名单上。如果没有，它允许他们进入画廊。参观者可以随时离开画廊。如果有人离开画廊，看守人会让等候名单上的人进入画廊。\n\n按照以下步骤实施本活动:\n\n1.  创建一个文件，其中包含我们这个项目需要的所有常量。\n2.  添加包括警卫和第一个变量，内的**人数，代表访客人数限制为 50 人:\n\n    ```cpp\n    #ifndef COMMON_HPP\n    #define COMMON_HPP\n    constexpr size_t CountPeopleInside = 5;\n    #endif // COMMON_HPP\n    ```** \n3.  现在，为`Person`类创建一个头文件和源文件，即`Person.hpp`和`Person.cpp`。另外，添加包含防护装置。定义`人`类，删除复制构造函数和复制赋值运算符；我们将只使用用户定义的默认构造函数、移动构造函数、移动赋值运算符和默认析构函数。添加一个名为`m_Id`的私有变量；我们会用它来记录。另外，添加一个名为`m_NextId`的私有静态变量；它将用于生成唯一标识:\n\n    ```cpp\n    #ifndef PERSON_HPP\n    #define PERSON_HPP\n    class Person\n    {\n    public:\n        Person();\n        Person& operator=(Person&);\n        Person(Person&&);\n        ~Person() = default;\n        Person(const Person&) = delete;\n        Person& operator=(const Person&) = delete;\n    private:\n        int m_Id;\n        static int m_NextId;\n    };\n    #endif // PERSON_HPP\n    ```\n\n4.  在源文件中，定义我们的静态变量，`m_NextId`。然后，在构造函数中，用`m_NextId`的值初始化`m_Id`变量。在构造函数中打印日志。实现移动复制构造函数和移动赋值运算符。现在，为我们的`人`对象实现线程安全存储。创建所需的头文件和源文件，即`Persons.hpp`和`Persons.cpp`。另外，添加包含防护装置。包括“`Person.hpp`”和`<互斥>`和`<向量>`头。使用用户定义的默认构造函数和默认析构函数定义`Persons`类。申报`添加()`功能添加人物，`获取()`获取人物并从列表中删除。定义`size()`函数获取 Person 元素的计数，以及`removePerson()`，从存储中移除任何人。在私有部分，声明一个互斥类型的变量，即`m_Mutex`，以及存储 Persons 的向量，即`m _ Persons`:T0\n5.  在源文件中，声明用户定义的构造函数，其中我们将向量的大小保留为 50 个元素(以避免在增长过程中调整大小):\n\n    ```cpp\n    Persons::Persons()\n    {\n        m_Persons.reserve(CountPeopleInside);\n    }\n    ```\n\n6.  声明`add()`函数，该函数采用`Person`类型的右值参数，锁定互斥体，并使用`std::move()`函数将`Person`添加到向量中:\n\n    ```cpp\n    void Persons::add(Person&& person)\n    {\n        std::lock_guard<std::mutex> m_lock(m_Mutex);\n        m_Persons.emplace_back(std::move(person));\n    }\n    ```\n\n7.  声明`get()`函数，该函数锁定互斥体并返回最后一个元素，然后将其从向量中移除。如果向量为空，将抛出异常:\n\n    ```cpp\n    Person Persons::get()\n    {\n        std::lock_guard<std::mutex> m_lock(m_Mutex);\n        if (m_Persons.empty())\n        {\n            throw \"Empty Persons storage\";\n        }\n        Person result = std::move(m_Persons.back());\n        m_Persons.pop_back();\n        return result;\n    }\n    ```\n\n8.  声明`size()`函数，返回向量的大小:\n\n    ```cpp\n    size_t Persons::size() const\n    {\n        return m_Persons.size();\n    }\n    ```\n\n9.  最后，声明`removePerson()`函数，该函数锁定互斥体并从向量中移除最后一项:\n\n    ```cpp\n    void Persons::removePerson()\n    {\n        std::lock_guard<std::mutex> m_lock(m_Mutex);\n        m_Persons.pop_back();\n        std::cout << \"Persons | removePerson | removed\" << std::endl;\n    }\n    ```\n\n10.  现在，实现 PersonGenerator 类，它负责创建和删除 Person 项。创建各自的头文件和源文件，即`PersonGenerator.hpp`和`PersonGenerator.cpp`。另外，添加包含防护装置。包括“`Person.hpp`”、`<线程>`和`<条件变量>`头文件。定义`人员生成器`类。在私有部分，定义两个`std::thread`变量，即`m_CreateThread`和`m_RemoveThread`。在一个线程中，我们将创建新的`人物`对象，并将异步通知用户移除另一个线程中的`人物`对象。定义对`人员`类型的共享变量的引用，即`m_CreatedPersons`。我们会把每一个新人都放进去。`m_CreatedPersons`将在几个线程之间共享。定义对`STD::condition _ variable`的两个引用，即`m _ convaraddperson`和`m _ convarremoveperson`。它们将用于线程之间的通信。定义对`std::mutex`变量的两个引用，即`m_AddLock`和`m_RemoveLock`。它们将用于接收对条件变量的访问。最后，定义一个`bool`值的两个引用，即`m_AddNotified`和`m_RemoveNotified`。它们将用于检查通知是真是假。另外，在私有部分，定义两个函数，这两个函数将是我们线程的启动函数–`runCreating()`和`runremove()`。接下来，定义两个将触发条件变量的函数，即`notifyCreated()`和`notifyRemoved()`。在公共部分，定义一个构造函数，它将我们在私有部分定义的所有引用作为参数。最后，定义析构函数。这将确保删除其他默认生成的函数:\n\n    ```cpp\n    #ifndef PERSON_GENERATOR_HPP\n    #define PERSON_GENERATOR_HPP\n    #include \"Persons.hpp\"\n    #include <condition_variable>\n    #include <thread>\n    class PersonGenerator\n    {\n    public:\n        PersonGenerator(Persons& persons,\n                std::condition_variable& add_person,\n                std::condition_variable& remove_person,\n                std::mutex& add_lock,\n                std::mutex& remove_lock,\n                bool& addNotified,\n                bool& removeNotified);\n        ~PersonGenerator();\n        PersonGenerator(const PersonGenerator&) = delete;\n        PersonGenerator(PersonGenerator&&) = delete;\n        PersonGenerator& operator=(const PersonGenerator&) = delete;\n        PersonGenerator& operator=(PersonGenerator&&) = delete;\n    private:\n        void runCreating();\n        void runRemoving();\n        void notifyCreated();\n        void notifyRemoved();\n    private:\n        std::thread m_CreateThread;\n        std::thread m_RemoveThread;\n        Persons& m_CreatedPersons;\n        // to notify about creating new person\n        std::condition_variable& m_CondVarAddPerson;\n        std::mutex& m_AddLock;\n        bool& m_AddNotified;\n        // to notify that person needs to be removed\n        std::condition_variable& m_CondVarRemovePerson;\n        std::mutex& m_RemoveLock;\n        bool& m_RemoveNotified;\n    };\n    #endif // PERSON_GENERATOR_HPP\n    ```\n\n11.  现在，转到源文件。包括`< stdlib.h >`文件，以便我们可以访问`srand()`和`rand()`函数，这些函数用于随机数生成。包括`< time.h >`头，这样我们就可以访问`time()`功能，以及`std::chrono`名称空间。它们用于我们与时间打交道的时候。包括`<比例>`文件，用于 typedefs，以便我们可以使用时间库:\n\n    ```cpp\n    #include \"PersonGenerator.hpp\"\n    #include <iostream>\n    #include <stdlib.h>     /* srand, rand */\n    #include <time.h>       /* time, chrono */\n    #include <ratio>        /* std::milli */\n    ```\n\n12.  声明构造函数并初始化除初始化列表中的线程之外的所有参数。用构造函数体中的适当函数初始化线程:\n\n    ```cpp\n    PersonGenerator::PersonGenerator(Persons& persons,\n                        std::condition_variable& add_person,\n                        std::condition_variable& remove_person,\n                        std::mutex& add_lock,\n                        std::mutex& remove_lock,\n                        bool& addNotified,\n                        bool& removeNotified)\n        : m_CreatedPersons(persons)\n        , m_CondVarAddPerson(add_person)\n        , m_AddLock(add_lock)\n        , m_AddNotified(addNotified)\n        , m_CondVarRemovePerson(remove_person)\n        , m_RemoveLock(remove_lock)\n        , m_RemoveNotified(removeNotified)\n    {\n        m_CreateThread = std::thread(&PersonGenerator::runCreating, this);\n        m_RemoveThread = std::thread(&PersonGenerator::runRemoving, this);\n    }\n    ```\n\n13.  声明一个析构函数并检查线程是否可连接。如果不是，加入他们:\n\n    ```cpp\n    PersonGenerator::~PersonGenerator()\n    {\n        if (m_CreateThread.joinable())\n        {\n            m_CreateThread.join();\n        }\n        if (m_RemoveThread.joinable())\n        {\n            m_RemoveThread.join();\n        }\n    }\n    ```\n\n14.  声明`runCreating()`函数，这是`m_CreateThread`线程的启动函数。在这个函数中，在一个无限循环中，我们将生成一个从 1 到 10 的随机数，并使当前线程在这段时间休眠。之后，创建一个 Person 值，添加到共享容器中，并通知其他线程:\n\n    ```cpp\n    void PersonGenerator::runCreating()\n    {\n        using namespace std::chrono_literals;\n        srand (time(NULL));\n        while(true)\n        {\n            std::chrono::duration<int, std::milli> duration((rand() % 10 + 1)*1000);\n            std::this_thread::sleep_for(duration);\n            std::cout << \"PersonGenerator | runCreating | new person:\" << std::endl;\n            m_CreatedPersons.add(std::move(Person()));\n            notifyCreated();\n        }\n    }\n    ```\n\n15.  声明`运行删除()`功能，这是`m_RemoveThread`线程的启动功能。在这个函数中，在一个无限循环中，我们将生成一个从 20 到 30 的随机数，并使当前线程在这段时间休眠。之后，通知其他线程应该移除部分访客:\n\n    ```cpp\n    void PersonGenerator::runRemoving()\n    {\n        using namespace std::chrono_literals;\n        srand (time(NULL));\n        while(true)\n        {\n            std::chrono::duration<int, std::milli> duration((rand() % 10 + 20)*1000);\n            std::this_thread::sleep_for(duration);\n            std::cout << \"PersonGenerator | runRemoving | somebody has left the gallery:\" << std::endl;\n            notifyRemoved();\n        }\n    }\n    ```\n\n16.  声明`通知创建()`和`通知删除()`功能。在它们的体内，锁定适当的互斥体，将适当的 bool 变量设置为 true，并在适当的条件变量上调用`notify_all()`函数:\n\n    ```cpp\n    void PersonGenerator::notifyCreated()\n    {\n        std::unique_lock<std::mutex> lock(m_AddLock);\n        m_AddNotified = true;\n        m_CondVarAddPerson.notify_all();\n    }\n    void PersonGenerator::notifyRemoved()\n    {\n        std::unique_lock<std::mutex> lock(m_RemoveLock);\n        m_RemoveNotified = true;\n        m_CondVarRemovePerson.notify_all();\n    }\n    ```\n\n17.  最后，我们需要为最后一个类“守望者”创建文件，即`守望者. hpp`和`守望者. cpp`。像往常一样，添加包括警卫。包括“`Persons.hpp`”、`<线程>`、<互斥体>和`<条件变量>`头。定义`守望者`类。在私有部分，定义两个`std::thread`变量，即`m_ThreadAdd`和`m_ThreadRemove`。在其中一个线程中，我们会将新的`人物`对象移动到适当的队列中，并异步移除另一个线程中的`人物`对象。定义对共享`人员`变量的引用，即`m_CreatedPeople`、`m_PeopleInside`、`m_PeopleInQueue`。如果没有超过限制，我们将从`m_CreatedPeople`列表中取出每个新人员，并将其移动到`m_PeopleInside`列表中。否则，我们将把它们移到`m_PeopleInQueue`列表中。它们将在几个线程之间共享。定义对`标准::条件变量`的两个引用，即`m _ convaraddperson`和`m _ convarremoveperson`。它们将用于线程之间的通信。定义对`std::mutex`变量的两个引用，即`m_AddMux`和`m_RemoveMux`。它们将用于接收对条件变量的访问。最后，定义一个`bool`值的两个引用，即`m_AddNotified`和`m_RemoveNotified`。它们将用于检查通知是真是假。另外，在私有部分，定义两个函数，这两个函数将是我们线程的启动函数–`runAdd()`和`runRemove()`。在公共部分，定义一个构造函数，它将我们在私有部分定义的所有引用作为参数。现在，定义一个析构函数。确保删除所有其他默认生成的函数:\n\n    ```cpp\n    #ifndef WATCHMAN_HPP\n    #define WATCHMAN_HPP\n    #include <mutex>\n    #include <thread>\n    #include <condition_variable>\n    #include \"Persons.hpp\"\n    class Watchman\n    {\n    public:\n        Watchman(std::condition_variable&,\n                std::condition_variable&,\n                std::mutex&,\n                std::mutex&,\n                bool&,\n                bool&,\n                Persons&,\n                Persons&,\n                Persons&);\n        ~Watchman();\n        Watchman(const Watchman&) = delete;\n        Watchman(Watchman&&) = delete;\n        Watchman& operator=(const Watchman&) = delete;\n        Watchman& operator=(Watchman&&) = delete;\n    private:\n        void runAdd();\n        void runRemove();\n    private:\n        std::thread m_ThreadAdd;\n        std::thread m_ThreadRemove;\n        std::condition_variable& m_CondVarRemovePerson;\n        std::condition_variable& m_CondVarAddPerson;\n        std::mutex& m_AddMux;\n        std::mutex& m_RemoveMux;\n        bool& m_AddNotified;\n        bool& m_RemoveNotified;\n        Persons& m_PeopleInside;\n        Persons& m_PeopleInQueue;\n        Persons& m_CreatedPeople;\n    };\n    #endif // WATCHMAN_HPP\n    ```\n\n18.  现在，转到源文件。包括“ **Common.hpp** ”头，以便我们可以访问变量内的**m _ counterpeople 和其他必要的头:\n\n    ```cpp\n    #include \"Watchman.hpp\"\n    #include \"Common.hpp\"\n    #include <iostream>\n    ```** \n19.  声明构造函数并初始化除初始化列表中的线程之外的所有参数。用构造器主体中的适当函数初始化线程:\n\n    ```cpp\n    Watchman::Watchman(std::condition_variable& addPerson,\n                std::condition_variable& removePerson,\n                std::mutex& addMux,\n                std::mutex& removeMux,\n                bool& addNotified,\n                bool& removeNotified,\n                Persons& peopleInside,\n                Persons& peopleInQueue,\n                Persons& createdPeople)\n        : m_CondVarRemovePerson(removePerson)\n        , m_CondVarAddPerson(addPerson)\n        , m_AddMux(addMux)\n        , m_RemoveMux(removeMux)\n        , m_AddNotified(addNotified)\n        , m_RemoveNotified(removeNotified)\n        , m_PeopleInside(peopleInside)\n        , m_PeopleInQueue(peopleInQueue)\n        , m_CreatedPeople(createdPeople)\n    {\n        m_ThreadAdd = std::thread(&Watchman::runAdd, this);\n        m_ThreadRemove = std::thread(&Watchman::runRemove, this);\n    }\n    ```\n\n20.  声明一个析构函数并检查线程是否可连接。如果不是，加入他们:\n\n    ```cpp\n    Watchman::~Watchman()\n    {\n        if (m_ThreadAdd.joinable())\n        {\n            m_ThreadAdd.join();\n        }\n        if (m_ThreadRemove.joinable())\n        {\n            m_ThreadRemove.join();\n        }\n    }\n    ```\n\n21.  声明`runAdd()`功能。在这里，我们创建一个无限循环。在循环中，我们在等待一个条件变量。当条件变量通知时，我们从`m_CreatedPeople`列表中选取人员，并将其移动到适当的列表中，即`m_PeopleInside`或`m_PeopleInQueue`(如果已超过限制)。然后，我们检查`m_PeopleInQueue`列表中是否有人员，如果`m _ peoplein`未满，我们将他们移入此列表:\n\n    ```cpp\n    void Watchman::runAdd()\n    {\n        while (true)\n        {\n            std::unique_lock<std::mutex> locker(m_AddMux);\n            while(!m_AddNotified)\n            {\n                std::cerr << \"Watchman | runAdd | false awakening\" << std::endl;\n                m_CondVarAddPerson.wait(locker);\n            }\n            std::cout << \"Watchman | runAdd | new person came\" << std::endl;\n            m_AddNotified = false;\n            while (m_CreatedPeople.size() > 0)\n            {\n                try\n                {\n                    auto person = m_CreatedPeople.get();\n                    if (m_PeopleInside.size() < CountPeopleInside)\n                    {\n                        std::cout << \"Watchman | runAdd | welcome in our The Art Gallery\" << std::endl;\n                        m_PeopleInside.add(std::move(person));\n                    }\n                    else\n                    {\n                        std::cout << \"Watchman | runAdd | Sorry, we are full. Please wait\" << std::endl;\n                        m_PeopleInQueue.add(std::move(person));\n                    }\n                }\n                catch(const std::string& e)\n                {\n                    std::cout << e << std::endl;\n                }\n            }\n            std::cout << \"Watchman | runAdd | check people in queue\" << std::endl;\n            if (m_PeopleInQueue.size() > 0)\n            {\n                while (m_PeopleInside.size() < CountPeopleInside)\n                {\n                    try\n                    {\n                        auto person = m_PeopleInQueue.get();\n                        std::cout << \"Watchman | runAdd | welcome in our The Art Gallery\" << std::endl;\n                        m_PeopleInside.add(std::move(person));\n                    }\n                    catch(const std::string& e)\n                    {\n                        std::cout << e << std::endl;\n                    }\n                }\n            }\n        }\n    }\n    ```\n\n22.  接下来，声明`runRemove()`功能，我们将从`m_PeopleInside`中移除访问者。在这里，同样在无限循环中，我们在等待`m _ convarremoveperson`条件变量。当它通知线程时，我们从访问者列表中删除人员。接下来，我们将检查`m_PeopleInQueue`列表中是否有任何人，如果没有超过限制，我们将他们添加到`m _ PeopleInQueue`中:\n\n    ```cpp\n    void Watchman::runRemove()\n    {\n        while (true)\n        {\n            std::unique_lock<std::mutex> locker(m_RemoveMux);\n            while(!m_RemoveNotified)\n            {\n                std::cerr << \"Watchman | runRemove | false awakening\" << std::endl;\n                m_CondVarRemovePerson.wait(locker);\n            }\n            m_RemoveNotified = false;\n            if (m_PeopleInside.size() > 0)\n            {\n                m_PeopleInside.removePerson();\n                std::cout << \"Watchman | runRemove | good buy\" << std::endl;\n            }\n            else\n            {\n                std::cout << \"Watchman | runRemove | there is nobody in The Art Gallery\" << std::endl;\n            }\n            std::cout << \"Watchman | runRemove | check people in queue\" << std::endl;\n            if (m_PeopleInQueue.size() > 0)\n            {\n                while (m_PeopleInside.size() < CountPeopleInside)\n                {\n                    try\n                    {\n                        auto person = m_PeopleInQueue.get();\n                        std::cout << \"Watchman | runRemove | welcome in our The Art Gallery\" << std::endl;\n                        m_PeopleInside.add(std::move(person));\n                    }\n                    catch(const std::string& e)\n                    {\n                        std::cout << e << std::endl;\n                    }\n                }\n            }\n        }\n    }\n    ```\n\n23.  最后，转到`主()`功能。首先，创建我们在`守望者`和`人员生成器`类中使用的所有共享变量。接下来，创建`watcher`和`PersonGenerator`变量，并将这些共享变量传递给构造函数。在主函数的末尾读取字符以避免关闭应用:\n\n    ```cpp\n    int main()\n    {\n        {\n            std::condition_variable g_CondVarRemovePerson;\n            std::condition_variable g_CondVarAddPerson;\n            std::mutex g_AddMux;\n            std::mutex g_RemoveMux;\n            bool g_AddNotified = false;;\n            bool g_RemoveNotified = false;\n            Persons g_PeopleInside;\n            Persons g_PeopleInQueue;\n            Persons g_CreatedPersons;\n            PersonGenerator generator(g_CreatedPersons, g_CondVarAddPerson, g_CondVarRemovePerson,\n                            g_AddMux, g_RemoveMux, g_AddNotified, g_RemoveNotified);\n            Watchman watchman(g_CondVarAddPerson,\n                    g_CondVarRemovePerson,\n                    g_AddMux,\n                    g_RemoveMux,\n                    g_AddNotified,\n                    g_RemoveNotified,\n                    g_PeopleInside,\n                    g_PeopleInQueue,\n                    g_CreatedPersons);\n        }\n        char a;\n        std::cin >> a;\n        return 0;\n    }\n    ```\n\n24.  编译并运行应用。在终端中，您将看到来自不同线程的日志，这些日志是关于创建人员以及将人员从一个列表移动到另一个列表。您的输出将类似于下面的截图:\n\n![Figure 5.27: The result of the application's execution ](img/C14583_05_27.jpg)\n\n###### 图 5.27:应用执行的结果\n\n如您所见，所有线程都以非常简单和干净的方式相互通信。我们通过使用互斥来保护我们的共享数据，这样我们就可以避免竞争条件。在这里，我们使用了一个异常来警告空列表，并在线程的函数中捕获它们，这样我们的线程就可以自己处理异常。在将线程加入析构函数之前，我们还检查了它是否是可连接的。这使我们避免了计划的意外终止。因此，这个小项目展示了我们处理线程的技巧。\n\n## 第 6 章–流和输入/输出\n\n### 活动 1 美术馆模拟器的记录系统\n\n线程安全记录器允许我们同时向终端输出数据。我们通过继承`std::ostringstream`类并使用互斥体进行同步来实现这个记录器。我们将实现一个为格式化输出提供接口的类，我们的记录器将使用它来扩展基本输出。我们为不同的日志记录级别定义了宏定义，以提供一个简单明了的界面。按照以下步骤完成本活动:\n\n1.  从第 6 课打开项目。\n2.  Create a new directory called logger inside the **src/** directory. You will get the following hierarchy:\n\n    ![Figure 6.25: The hierarchy of the project ](img/C14583_06_25.jpg)\n\n    ###### 图 6.25:项目的层次结构\n\n3.  创建一个名为**记录器**的头文件和源文件。在**中，添加包括守卫。包括<字符串>头，以增加对使用字符串的支持。定义一个名为 logger 的命名空间，然后定义一个名为 **utils** 的嵌套命名空间。在 **utils** 命名空间中，声明 **LoggerUtils** 类。**\n4.  在公共部分 n 中，声明以下静态函数:`getDateTime`、`getThreadId`、`getLoggingLevel`、`getFileAndLine`、`getFuncName`、`getinfouncname`、`getOutFuncName`。你的班级应该如下所示:\n\n    ```cpp\n    #ifndef LOGGERUTILS_HPP_\n    #define LOGGERUTILS_HPP_\n    #include <string>\n    namespace logger\n    {\n    namespace utils\n    {\n    class LoggerUtils\n    {\n    public:\n         static std::string getDateTime();\n         static std::string getThreadId();\n         static std::string getLoggingLevel(const std::string& level);\n         static std::string getFileAndLine(const std::string& file, const int& line);\n         static std::string getFuncName(const std::string& func);\n         static std::string getInFuncName(const std::string& func);\n         static std::string getOutFuncName(const std::string& func);\n    };\n    } // namespace utils\n    } // namespace logger\n    #endif /* LOGGERUTILS_HPP_ */\n    ```\n\n5.  在 **LoggerUtils.cpp** 中，增加需要的包括: **LoggerUtils.hpp** 表头、**T11】stream>T6】为 **std::stringstream** 支持、**T13】ctime>T10】为日期时间支持:\n\n    ```cpp\n    #include \"LoggerUtils.hpp\"\n    #include <sstream>\n    #include <ctime>\n    #include <thread>\n    ```**** \n6.  进入`记录器`和`实用程序`名称空间。编写所需的函数定义。在`getDateTime()`功能中，使用`localtime()`功能获取当地时间。使用`str time()`函数将其格式化为字符串。使用`标准::字符串流` :\n\n    ```cpp\n    std::string LoggerUtils::getDateTime()\n    {\n         time_t rawtime;\n         struct tm * timeinfo;\n         char buffer[80];\n         time (&rawtime);\n         timeinfo = localtime(&rawtime);\n         strftime(buffer,sizeof(buffer),\"%d-%m-%YT%H:%M:%S\",timeinfo);\n         std::stringstream ss;\n         ss << \"[\";\n         ss << buffer;\n         ss << \"]\";\n         return ss.str();\n    }\n    ```\n\n    将其转换为所需的格式\n7.  在`getThreadId()`函数中，获取当前线程 Id，并使用`std::stringstream` :\n\n    ```cpp\n    std::string LoggerUtils::getThreadId()\n    {\n         std::stringstream ss;\n         ss << \"[\";\n         ss << std::this_thread::get_id();\n         ss << \"]\";\n         return ss.str();\n    }\n    ```\n\n    将其转换为所需格式\n8.  在`getLoggingLevel()`函数中，使用`std::stringstream` :\n\n    ```cpp\n    std::string LoggerUtils::getLoggingLevel(const std::string& level)\n    {\n         std::stringstream ss;\n         ss << \"[\";\n         ss << level;\n         ss << \"]\";\n         return ss.str();\n    }\n    ```\n\n    将给定字符串转换为所需格式\n9.  在`getfilandline()`函数中，使用`std::stringstream` :\n\n    ```cpp\n    std::string LoggerUtils::getFileAndLine(const std::string& file, const int& line)\n    {\n         std::stringstream ss;\n         ss << \" \";\n         ss << file;\n         ss << \":\";\n         ss << line;\n         ss << \":\";\n         return ss.str();\n    }\n    ```\n\n    将给定的文件和行转换为所需的格式\n10.  在`getFuncName()`函数中，使用`std::stringstream` :\n\n    ```cpp\n    std::string LoggerUtils::getFuncName(const std::string& func)\n    {\n         std::stringstream ss;\n         ss << \" --- \";\n         ss << func;\n         ss << \"()\";\n         return ss.str();\n    }\n    ```\n\n    将函数名转换为所需的格式\n11.  在`getInFuncName()`函数中，使用`std::stringstream`将函数名转换为所需的格式。\n\n    ```cpp\n    std::string LoggerUtils::getInFuncName(const std::string& func)\n    {\n         std::stringstream ss;\n         ss << \" --> \";\n         ss << func;\n         ss << \"()\";\n         return ss.str();\n    }\n    ```\n\n12.  在`getOutFuncName()`函数中，使用`std::stringstream` :\n\n    ```cpp\n    std::string LoggerUtils::getOutFuncName(const std::string& func)\n    {\n         std::stringstream ss;\n         ss << \" <-- \";\n         ss << func;\n         ss << \"()\";\n         return ss.str();\n    }\n    ```\n\n    将函数名转换为所需的格式\n13.  创建一个名为**的头文件**。添加包括防护装置。为每个 **LoggerUtils** 函数创建宏定义: **DATETIME** 为 **getDateTime()** 函数， **THREAD_ID** 为 **getThreadId()** 函数， **LOG_LEVEL** 为 **getLoggingLevel()** 函数， **FILE_LINE** 为**getfileadline()**函数， **FUNC 因此，头文件应该如下所示:\n\n    ```cpp\n    #ifndef LOGGERMACROSES_HPP_\n    #define LOGGERMACROSES_HPP_\n    #define DATETIME \\\n         logger::utils::LoggerUtils::getDateTime()\n    #define THREAD_ID \\\n         logger::utils::LoggerUtils::getThreadId()\n    #define LOG_LEVEL( level ) \\\n         logger::utils::LoggerUtils::getLoggingLevel(level)\n    #define FILE_LINE \\\n         logger::utils::LoggerUtils::getFileAndLine(__FILE__, __LINE__)\n    #define FUNC_NAME \\\n         logger::utils::LoggerUtils::getFuncName(__FUNCTION__)\n    #define FUNC_ENTRY_NAME \\\n         logger::utils::LoggerUtils::getInFuncName(__FUNCTION__)\n    #define FUNC_EXIT_NAME \\\n         logger::utils::LoggerUtils::getOutFuncName(__FUNCTION__)\n    #endif /* LOGGERMACROSES_HPP_ */\n    ```** \n14.  创建一个名为**的头文件和源文件**。在 **StreamLogger.hpp** 中，添加所需的包含防护装置。包括 **LoggerMacroses.hpp** 和 **LoggerUtils.hpp** 头文件。然后，包括用于**标准::互斥流**支持的 **<流>头，用于**标准::线程**支持的**线程<头，以及用于**标准::互斥流**支持的 **<互斥流>头******\n15.  进入`名称空间`记录器。声明`StreamLogger`类，该类继承自`std::ostringstream`类。这种继承允许我们使用重载的左移位操作符< <进行记录。我们不设置输出设备，因此不会执行输出–只是存储在内部缓冲器中。在私有部分，声明一个名为`m_mux`的静态`std::mutex`变量。声明常量字符串，以便可以存储日志级别、文件和行以及函数名。在公共部分中，声明一个构造函数，该构造函数将日志级别、文件和行以及函数名作为参数。声明一个类析构函数。类声明应该如下所示:\n\n    ```cpp\n    namespace logger\n    {\n    class StreamLogger : public std::ostringstream\n    {\n    public:\n         StreamLogger(const std::string logLevel,\n                      const std::string fileLine,\n                      const std::string funcName);\n         ~StreamLogger();\n    private:\n         static std::mutex m_mux;\n         const std::string m_logLevel;\n         const std::string m_fileLine;\n         const std::string m_funcName;\n    };\n    } // namespace logger\n    ```\n\n16.  在`StreamLogger.cpp`中，包含`StreamLogger.hpp`和`< iostream >`头，用于`std::cout`支持。进入`记录器`命名空间。定义构造函数并初始化初始化列表中的所有成员。然后，定义析构函数并输入它的作用域。锁定`m_mux`互斥体。如果内部缓冲区为空，则只输出日期和时间、线程 ID、日志级别、文件和行以及函数名。因此，我们将得到如下格式的行:`【datetime】【threadId】【logLevel】【文件:line:】【name()-】`。如果内部缓冲区包含任何数据，输出与缓冲区结尾相同的字符串。因此，我们将得到如下格式的行:`【datetime】【threadId】【log level】【文件:行:】【name()-】|消息`。完整的源文件应该如下所示:\n\n    ```cpp\n    #include \"StreamLogger.hpp\"\n    #include <iostream>\n    std::mutex logger::StreamLogger::m_mux;\n    namespace logger\n    {\n    StreamLogger::StreamLogger(const std::string logLevel,\n                      const std::string fileLine,\n                      const std::string funcName)\n              : m_logLevel(logLevel)\n              , m_fileLine(fileLine)\n              , m_funcName(funcName)\n    {}\n    StreamLogger::~StreamLogger()\n    {\n         std::lock_guard<std::mutex> lock(m_mux);\n         if (this->str().empty())\n         {\n              std::cout << DATETIME << THREAD_ID << m_logLevel << m_fileLine << m_funcName << std::endl;\n         }\n         else\n         {\n              std::cout << DATETIME << THREAD_ID << m_logLevel << m_fileLine << m_funcName << \" | \" << this->str() << std::endl;\n         }\n    }\n    }\n    ```\n\n17.  创建一个名为`Logger.hpp`的头文件，并添加所需的包含防护。包括`StreamLogger.hpp`和`LoggerMacroses.hpp`头。接下来，为不同的日志记录级别创建宏定义:`LOG_TRACE()`、`LOG_DEBUG()`、`LOG_WARN()`、`LOG_TRACE()`、`LOG_INFO()`、`LOG_ERROR()`、`LOG_TRACE_ENTRY()`、`LOG_TRACE_EXIT()`。完整的头文件应该如下所示:\n\n    ```cpp\n    #ifndef LOGGER_HPP_\n    #define LOGGER_HPP_\n    #include \"StreamLogger.hpp\"\n    #include \"LoggerMacroses.hpp\"\n    #define LOG_TRACE() logger::StreamLogger{LOG_LEVEL(\"Trace\"), FILE_LINE, FUNC_NAME}\n    #define LOG_DEBUG() logger::StreamLogger{LOG_LEVEL(\"Debug\"), FILE_LINE, FUNC_NAME}\n    #define LOG_WARN() logger::StreamLogger{LOG_LEVEL(\"Warning\"), FILE_LINE, FUNC_NAME}\n    #define LOG_TRACE() logger::StreamLogger{LOG_LEVEL(\"Trace\"), FILE_LINE, FUNC_NAME}\n    #define LOG_INFO() logger::StreamLogger{LOG_LEVEL(\"Info\"), FILE_LINE, FUNC_NAME}\n    #define LOG_ERROR() logger::StreamLogger{LOG_LEVEL(\"Error\"), FILE_LINE, FUNC_NAME}\n    #define LOG_TRACE_ENTRY() logger::StreamLogger{LOG_LEVEL(\"Error\"), FILE_LINE, FUNC_ENTRY_NAME}\n    #define LOG_TRACE_EXIT() logger::StreamLogger{LOG_LEVEL(\"Error\"), FILE_LINE, FUNC_EXIT_NAME}\n    #endif /* LOGGER_HPP_ */\n    ```\n\n18.  用适当的宏定义调用替换所有的`标准::cout`调用。在`Watchman.cpp`源文件中包含`logger/Logger.hpp`头文件。在`runAdd()`功能中，用不同日志级别的宏定义替换`std::cout`的所有实例。`运行添加()`功能应该如下所示:\n\n    ```cpp\n    void Watchman::runAdd()\n    {\n         while (true)\n         {\n              std::unique_lock<std::mutex> locker(m_AddMux);\n              while(!m_AddNotified)\n              {\n                   LOG_DEBUG() << \"Spurious awakening\";\n                   m_CondVarAddPerson.wait(locker);\n              }\n              LOG_INFO() << \"New person came\";\n              m_AddNotified = false;\n              while (m_CreatedPeople.size() > 0)\n              {\n                   try\n                   {\n                        auto person = m_CreatedPeople.get();\n                        if (m_PeopleInside.size() < CountPeopleInside)\n                        {\n                             LOG_INFO() << \"Welcome in the our Art Gallery\";\n                             m_PeopleInside.add(std::move(person));\n                        }\n                        else\n                        {\n                             LOG_INFO() << \"Sorry, we are full. Please wait\";\n                             m_PeopleInQueue.add(std::move(person));\n                        }\n                   }\n                   catch(const std::string& e)\n                   {\n                        LOG_ERROR() << e;\n                   }\n              }\n              LOG_TRACE() << \"Check people in queue\";\n              if (m_PeopleInQueue.size() > 0)\n              {\n                   while (m_PeopleInside.size() < CountPeopleInside)\n                   {\n                        try\n                        {\n                             auto person = m_PeopleInQueue.get();\n                             LOG_INFO() << \"Welcome in the our Art Gallery\";\n                             m_PeopleInside.add(std::move(person));\n                        }\n                        catch(const std::string& e)\n                        {\n                             LOG_ERROR() << e;\n                        }\n                   }\n              }\n         }\n    }\n    ```\n\n19.  注意我们如何使用新的记录器。我们用括号调用宏定义，并使用左移位运算符:\n\n    ```cpp\n    LOG_ERROR() << e;\n    Or\n    LOG_INFO() << \"Welcome in the our Art Gallery\";\n    ```\n\n20.  对其余代码进行同样的替换。\n21.  构建并运行应用。在终端中，您将看到日志消息以不同的日志级别和有用的信息从不同的线程中出现。过了一段时间后，您将获得类似以下内容的输出:\n\n![Figure 6.26: The execution result of the activity project ](img/C14583_06_26.jpg)\n\n###### 图 6.26:活动项目的执行结果\n\n如您所见，阅读和理解日志非常容易。如果您的需求不同，您可以很容易地更改`StreamLogger`类，将日志写入文件系统上的文件。您可以添加使用日志调试应用所需的任何其他信息，例如输出函数参数。您还可以为自定义类型重写左移位运算符，以便轻松输出调试信息。\n\n在这个项目中，我们采用了许多我们在本章中学到的东西。我们为线程安全的输出创建了一个额外的流，我们将输出格式化为所需的表示，我们使用`std::stringstream`来执行格式化数据，我们使用宏定义来方便记录器的使用。因此，这个项目展示了我们处理并发输入/输出的技能\n\n## 第 7 章-每个人都会跌倒，这是你爬起来的方式-测试和调试\n\n### 活动 1:使用测试用例检查功能的准确性并理解测试驱动开发(TDD)\n\n对于本活动，我们将开发解析 **RecordFile.txt** 和 **CurrencyConversion.txt** 文件的函数，并编写测试用例来检查函数的准确性。按照以下步骤实施本活动:\n\n1.  创建一个名为 **parse.conf** 的配置文件并编写配置。\n2.  注意这里只关注两个变量，即`current file`和`recordFile`。其余的用于其他环境变量:\n\n    ```cpp\n    CONFIGURATION_FILE\n    currencyFile = ./CurrencyConversion.txt\n    recordFile = ./RecordFile.txt\n    DatabaseServer = 192.123.41.112\n    UserId = sqluser\n    Password = sqluser \n    RestApiServer = 101.21.231.11\n    LogFilePath = /var/project/logs\n    ```\n\n3.  创建一个名为`CommonHeader.h`的头文件，并声明所有的实用函数，即`isAllNumbers()`、`isDigit()`、`parceline()`、`checkFile()`、`parseConfig()`、`parsecurrency parameters()`、`fillCurrencyMap()`、`recordparsefile()`、\n\n    ```cpp\n    #ifndef __COMMON_HEADER__H\n    #define __COMMON_HEADER__H\n    #include<iostream>\n    #include<cstring>\n    #include<fstream>\n    #include<vector>\n    #include<string>\n    #include<map>\n    #include<sstream>\n    #include<iterator>\n    #include<algorithm>\n    #include<iomanip>\n    using namespace std;\n    // Forward declaration of global variables. \n    extern string configFile;\n    extern string recordFile;\n    extern string currencyFile;\n    extern map<string, float> currencyMap;\n    struct record;\n    extern vector<record>      vecRecord;\n    //Structure to hold Record Data . \n    struct record{\n        int     customerId;\n        string  firstName;\n        string  lastName;\n        int     orderId;\n        int     productId;\n        int     quantity;\n        float   totalPriceRegional;\n        string  currency;\n        float   totalPriceUsd;\n\n        record(vector<string> & in){\n            customerId      = atoi(in[0].c_str());\n            firstName       = in[1];\n            lastName        = in[2];\n            orderId         = atoi(in[3].c_str());\n            productId       = atoi(in[4].c_str());\n            quantity        = atoi(in[5].c_str());\n            totalPriceRegional = static_cast<float>(atof(in[6].c_str()));\n            currency        = in[7];\n            totalPriceUsd   = static_cast<float>(atof(in[8].c_str()));\n        }\n    };\n    // Declaration of Utility Functions.. \n    string trim (string &);\n    bool isAllNumbers(const string &);\n    bool isDigit(const string &);\n    void parseLine(ifstream &, vector<string> &, char);\n    bool checkFile(ifstream &, string &, string, char, string &);\n    bool parseConfig();\n    bool parseCurrencyParameters( vector<string> &);\n    bool fillCurrencyMap();\n    bool parseRecordFile();\n    bool checkRecord(vector<string> &);\n    void displayCurrencyMap();\n    ostream& operator<<(ostream &, const record &);\n    void displayRecords();\n    #endif\n    ```\n\n4.  创建一个名为 **Util.cpp** 的文件，定义所有的实用函数。编写以下代码定义`修剪()`功能:\n\n    ```cpp\n    #include<CommonHeader.h>\n    // Utility function to remove spaces and tabs from start of string and end of string.. \n    string trim (string &str) { // remove space and tab from string.\n        string res(\"\");\n        if ((str.find(' ') != string::npos) || (str.find(' ') != string::npos)){ // if space or tab found.. \n            size_t begin, end;\n            if ((begin = str.find_first_not_of(\" \\t\")) != string::npos){ // if string is not empty.. \n                end = str.find_last_not_of(\" \\t\");\n                if ( end >= begin )\n                    res = str.substr(begin, end - begin + 1);\n            }\n        }else{\n            res = str; // No space or tab found.. \n        }\n        str = res;\n        return res;\n    }\n    ```\n\n5.  编写以下代码来定义`isAllNumbers()`、`isDigit()`和`parceline()`函数:\n\n    ```cpp\n    // Utility function to check if string contains only digits ( 0-9) and only single '.' \n    // eg . 1121.23 , .113, 121\\. are valid, but 231.14.143 is not valid.\n    bool isAllNumbers(const string &str){ // make sure, it only contains digit and only single '.' if any \n        return ( all_of(str.begin(), str.end(), [](char c) { return ( isdigit(c) || (c == '.')); }) \n                 && (count(str.begin(), str.end(), '.') <= 1) );\n    }\n    //Utility function to check if string contains only digits (0-9).. \n    bool isDigit(const string &str){\n        return ( all_of(str.begin(), str.end(), [](char c) { return isdigit(c); }));\n    }\n    // Utility function, where single line of file <infile> is parsed using delimiter. \n    // And store the tokens in vector of string. \n    void parseLine(ifstream &infile, vector<string> & vec, char delimiter){\n        string line, token;\n        getline(infile, line);\n        istringstream ss(line);\n        vec.clear();\n        while(getline(ss, token, delimiter)) // break line using delimiter\n            vec.push_back(token);  // store tokens in vector of string\n    }\n    ```\n\n6.  编写以下代码来定义`解析当前参数()`和`检查记录()`函数:\n\n    ```cpp\n    // Utility function to check if vector string of 2 strings contain correct \n    // currency and conversion ratio. currency should be 3 characters, conversion ratio\n    // should be in decimal number format. \n    bool parseCurrencyParameters( vector<string> & vec){\n        trim(vec[0]);  trim(vec[1]);\n        return ( (!vec[0].empty()) && (vec[0].size() == 3) && (!vec[1].empty()) && (isAllNumbers(vec[1])) );\n    }\n    // Utility function, to check if vector of string has correct format for records parsed from Record File. \n    // CustomerId, OrderId, ProductId, Quantity should be in integer format\n    // TotalPrice Regional and USD should be in decimal number format\n    // Currecny should be present in map. \n    bool checkRecord(vector<string> &split){\n        // Trim all string in vector\n        for (auto &s : split)\n            trim(s);\n\n        if ( !(isDigit(split[0]) && isDigit(split[3]) && isDigit(split[4]) && isDigit(split[5])) ){\n            cerr << \"ERROR: Record with customer id:\" << split[0] << \" doesnt have right DIGIT parameter\" << endl;\n            return false;\n        }\n        if ( !(isAllNumbers(split[6]) && isAllNumbers(split[8])) ){\n            cerr << \"ERROR: Record with customer id:\" << split[0] << \" doesnt have right NUMBER parameter\" << endl;\n            return false;\n        }\n        if ( currencyMap.find(split[7]) == currencyMap.end() ){\n            cerr << \"ERROR: Record with customer id :\" << split[0] << \" has currency :\" << split[7] << \" not present in map\" << endl;\n            return false;\n        }\n        return true;\n    }\n    ```\n\n7.  编写以下代码定义`检查文件()`功能:\n\n    ```cpp\n    // Function to test initial conditions of file.. \n    // Check if file is present and has correct header information. \n    bool checkFile(ifstream &inFile, string &fileName, string parameter, char delimiter, string &error){\n        bool flag = true;\n        inFile.open(fileName);\n        if ( inFile.fail() ){\n            error = \"Failed opening \" + fileName + \" file, with error: \" + strerror(errno);\n            flag = false;\n        }\n        if (flag){\n            vector<string> split;\n            // Parse first line as header and make sure it contains parameter as first token. \n            parseLine(inFile, split, delimiter);\n            if (split.empty()){\n                error = fileName + \" is empty\";\n                flag = false;\n            } else if ( split[0].find(parameter) == string::npos ){\n                error = \"In \" + fileName + \" file, first line doesnt contain header \";\n                flag = false;\n            }\n        }\n        return flag;\n    }\n    ```\n\n8.  编写以下代码定义`parseConfig()`函数:\n\n    ```cpp\n    // Function to parse Config file. Each line will have '<name> = <value> format\n    // Store CurrencyConversion file and Record File parameters correctly. \n    bool parseConfig() {\n        ifstream coffle;\n        string error;\n        if (!checkFile(confFile, configFile, \"CONFIGURATION_FILE\", '=', error)){\n            cerr << \"ERROR: \" << error << endl;\n            return false;\n        }\n        bool flag = true;\n        vector<string> split;\n        while (confFile.good()){\n            parseLine(confFile, split, '=');\n            if ( split.size() == 2 ){ \n                string name = trim(split[0]);\n                string value = trim(split[1]);\n                if ( name == \"currencyFile\" )\n                    currencyFile = value;\n                else if ( name == \"recordFile\")\n                    recordFile = value;\n            }\n        }\n        if ( currencyFile.empty() || recordFile.empty() ){\n            cerr << \"ERROR : currencyfile or recordfile not set correctly.\" << endl;\n            flag = false;\n        }\n        return flag;\n    }\n    ```\n\n9.  编写以下代码来定义`filllcurrency map()`函数:\n\n    ```cpp\n    // Function to parse CurrencyConversion file and store values in Map.\n    bool fillCurrencyMap() {\n        ifstream currFile;\n        string error;\n        if (!checkFile(currFile, currencyFile, \"Currency\", '|', error)){\n            cerr << \"ERROR: \" << error << endl;\n            return false;\n        }\n        bool flag = true;\n        vector<string> split;\n        while (currFile.good()){\n            parseLine(currFile, split, '|');\n            if (split.size() == 2){\n                if (parseCurrencyParameters(split)){\n                    currencyMap[split[0]] = static_cast<float>(atof(split[1].c_str())); // make sure currency is valid.\n                } else {\n                    cerr << \"ERROR: Processing Currency Conversion file for Currency: \"<< split[0] << endl;\n                    flag = false;\n                    break;\n                }\n            } else if (!split.empty()){\n                cerr << \"ERROR: Processing Currency Conversion , got incorrect parameters for Currency: \" << split[0] << endl;\n                flag = false;\n                break;\n            }\n        }\n        return flag;\n    }\n    ```\n\n10.  编写以下代码来定义`parseRecordFile()`函数:\n\n    ```cpp\n    // Function to parse Record File .. \n    bool parseRecordFile(){\n        ifstream recFile;\n        string error;\n        if (!checkFile(recFile, recordFile, \"Customer Id\", '|', error)){\n            cerr << \"ERROR: \" << error << endl;\n            return false;\n        }\n        bool flag = true;\n        vector<string> split;\n        while(recFile.good()){\n            parseLine(recFile, split, '|');\n            if (split.size() == 9){ \n                if (checkRecord(split)){\n                    vecRecord.push_back(split); //Construct struct record and save it in vector... \n                }else{\n                    cerr << \"ERROR : Parsing Record, for Customer Id: \" << split[0] << endl;\n                    flag = false;\n                    break;\n                }\n            } else if (!split.empty()){\n                cerr << \"ERROR: Processing Record, for Customer Id: \" << split[0] << endl;\n                flag = false;\n                break;\n            }\n        }\n        return flag;\n    }\n    ```\n\n11.  编写以下代码定义`displayCurrencyMap()`函数:\n\n    ```cpp\n    void displayCurrencyMap(){\n\n        cout << \"Currency MAP :\" << endl;\n        for (auto p : currencyMap)\n            cout << p.first <<\"  :  \" << p.second << endl;\n        cout << endl;\n    }\n    ostream& operator<<(ostream& os, const record &rec){\n        os << rec.customerId <<\"|\" << rec.firstName << \"|\" << rec.lastName << \"|\" \n           << rec.orderId << \"|\" << rec.productId << \"|\" << rec.quantity << \"|\" \n           << fixed << setprecision(2) << rec.totalPriceRegional << \"|\" << rec.currency << \"|\" \n           << fixed << setprecision(2) << rec.totalPriceUsd << endl;\n        return os;\n    }\n    ```\n\n12.  编写以下代码定义`显示记录()`功能:\n\n    ```cpp\n    void displayRecords(){\n        cout << \" Displaying records with '|' delimiter\" << endl;\n        for (auto rec : vecRecord){\n            cout << rec;\n        }\n        cout << endl;\n    }\n    ```\n\n13.  创建一个名为 **ParseFiles.cpp** 的文件，并调用`parseConfig()`、`filllcurrency map()`和`parseRecordFile()`函数:\n\n    ```cpp\n    #include <CommonHeader.h>\n    // Global variables ... \n    string configFile = \"./parse.conf\";\n    string recordFile;\n    string currencyFile;\n    map<string, float>  currencyMap;\n    vector<record>      vecRecord;\n    int main(){\n        // Read Config file to set global configuration variables. \n        if (!parseConfig()){\n            cerr << \"Error parsing Config File \" << endl;\n            return false;\n        }\n        // Read Currency file and fill map\n        if (!fillCurrencyMap()){\n            cerr << \"Error setting CurrencyConversion Map \" << endl;\n            return false;\n        }\n        if (!parseRecordFile()){\n            cerr << \"Error parsing Records File \" << endl;\n            return false;\n        }\n            displayCurrencyMap();\n        displayRecords();\n        return 0;\n    }\n    ```\n\n14.  Open the compiler. Compile and execute the **Util.cpp** and **ParseFiles.cpp** files by writing the following command:\n\n    ```cpp\n    g++ -c -g -I. -Wall Util.cpp\n    g++ -g -I. -Wall Util.o ParseFiles.cpp -o ParseFiles\n    ```\n\n    将生成两者的二进制文件。\n\n    在下面的截图中，您将看到这两个命令都存储在 build.sh 脚本中并被执行。运行此脚本后，您将看到最新的`Util.o`和`ParseFiles`文件已经生成:\n\n    ![Figure 7.25: New files generated ](img/C14583_07_25.jpg)\n\n    ###### 图 7.25:生成的新文件\n\n15.  After running the `ParseFiles` executable, we'll receive the following output:\n\n    ![Figure 7.26: New files generated ](img/C14583_07_26.jpg)\n\n    ###### 图 7.26:生成的新文件\n\n16.  创建一个名为**的文件，并为实用函数编写测试用例。为**修剪**功能编写以下测试用例:\n\n    ```cpp\n    #include<gtest/gtest.h>\n    #include\"../CommonHeader.h\"\n    using namespace std;\n    // Global variables ... \n    string configFile = \"./parse.conf\";\n    string recordFile;\n    string currencyFile;\n    map<string, float>  currencyMap;\n    vector<record>      vecRecord;\n    void setDefault(){\n        configFile = \"./parse.conf\";\n        recordFile.clear();\n        currencyFile.clear();\n        currencyMap.clear();\n        vecRecord.clear();\n    }\n    // Test Cases for trim function ... \n    TEST(trim, empty){\n        string str=\"    \";\n        EXPECT_EQ(trim(str), string());\n    }\n    TEST(trim, start_space){\n        string str = \"   adas\";\n        EXPECT_EQ(trim(str), string(\"adas\"));\n    }\n    TEST(trim, end_space){\n        string str = \"trip      \";\n        EXPECT_EQ(trim(str), string(\"trip\"));\n    }\n    TEST(trim, string_middle){\n        string str = \"  hdgf   \";\n        EXPECT_EQ(trim(str), string(\"hdgf\"));\n    }\n    TEST(trim, single_char_start){\n        string str = \"c  \";\n        EXPECT_EQ(trim(str), string(\"c\"));\n    }\n    TEST(trim, single_char_end){\n        string str = \"   c\";\n        EXPECT_EQ(trim(str), string(\"c\"));\n    }\n    TEST(trim, single_char_middle){\n        string str = \"      c  \";\n        EXPECT_EQ(trim(str), string(\"c\"));\n    }\n    ```** \n17.  为`isAllNumbers`函数编写以下测试用例:\n\n    ```cpp\n    // Test Cases for isAllNumbers function.. \n    TEST(isNumber, alphabets_present){\n        string str = \"11.qwe13\";\n        ASSERT_FALSE(isAllNumbers(str));\n    }\n    TEST(isNumber, special_character_present){\n        string str = \"34.^%3\";\n        ASSERT_FALSE(isAllNumbers(str));\n    }\n    TEST(isNumber, correct_number){\n        string str = \"54.765\";\n        ASSERT_TRUE(isAllNumbers(str));\n    }\n    TEST(isNumber, decimal_begin){\n        string str = \".624\";\n        ASSERT_TRUE(isAllNumbers(str));\n    }\n    TEST(isNumber, decimal_end){\n        string str = \"53.\";\n        ASSERT_TRUE(isAllNumbers(str));\n    }\n    ```\n\n18.  为`isDigit`函数编写以下测试用例:\n\n    ```cpp\n    // Test Cases for isDigit funtion... \n    TEST(isDigit, alphabet_present){\n        string str = \"527A\";\n        ASSERT_FALSE(isDigit(str));\n    }\n    TEST(isDigit, decimal_present){\n        string str = \"21.55\";\n        ASSERT_FALSE(isDigit(str));\n    }\n    TEST(isDigit, correct_digit){\n        string str = \"9769\";\n        ASSERT_TRUE(isDigit(str));\n    }\n    ```\n\n19.  为`解析当前参数`函数编写以下测试用例:\n\n    ```cpp\n    // Test Cases for parseCurrencyParameters function\n    TEST(CurrencyParameters, extra_currency_chararcters){\n        vector<string> vec {\"ASAA\",\"34.22\"};\n        ASSERT_FALSE(parseCurrencyParameters(vec));\n    }\n    TEST(CurrencyParameters, correct_parameters){\n        vector<string> vec {\"INR\",\"1.44\"};\n        ASSERT_TRUE(parseCurrencyParameters(vec));\n    }\n    ```\n\n20.  Write the following test cases for the `checkFile` function:\n\n    ```cpp\n    //Test Cases for checkFile function...\n    TEST(checkFile, no_file_present){\n        string fileName = \"./NoFile\";\n        ifstream infile; \n        string parameter(\"nothing\");\n        char delimit =';';\n        string err;\n        ASSERT_FALSE(checkFile(infile, fileName, parameter, delimit, err));\n    }\n    TEST(checkFile, empty_file){\n        string fileName = \"./emptyFile\";\n        ifstream infile; \n        string parameter(\"nothing\");\n        char delimit =';';\n        string err;\n        ASSERT_FALSE(checkFile(infile, fileName, parameter, delimit, err));\n    }\n    TEST(checkFile, no_header){\n        string fileName = \"./noHeaderFile\";\n        ifstream infile; \n        string parameter(\"header\");\n        char delimit ='|';\n        string err;\n        ASSERT_FALSE(checkFile(infile, fileName, parameter, delimit, err));\n    }\n    TEST(checkFile, incorrect_header){\n        string fileName = \"./correctHeaderFile\";\n        ifstream infile; \n        string parameter(\"header\");\n        char delimit ='|';\n        string err;\n        ASSERT_FALSE(checkFile(infile, fileName, parameter, delimit, err));\n    }\n    TEST(checkFile, correct_file){\n        string fileName = \"./correctHeaderFile\";\n        ifstream infile; \n        string parameter(\"Currency\");\n        char delimit ='|';\n        string err;\n        ASSERT_TRUE(checkFile(infile, fileName, parameter, delimit, err));\n    }\n    ```\n\n    #### 注意\n\n    在前面的函数中用作输入参数的 **NoFile** 、 **emptyFile** 、 **noHeaderFile** 、**correcheader file**文件可以在这里找到:[https://github . com/trainingypbackt/Advanced-CPlusPlus/tree/master/lesson 7/activity 01](https://github.com/TrainingByPackt/Advanced-CPlusPlus/tree/master/Lesson7/Activity01)。\n\n21.  Write the following test cases for the `parseConfig` function:\n\n    ```cpp\n    //Test Cases for parseConfig function...\n    TEST(parseConfig, missing_currency_file){\n        setDefault();\n        configFile = \"./parseMissingCurrency.conf\";\n        ASSERT_FALSE(parseConfig());\n    }\n    TEST(parseConfig, missing_record_file){\n        setDefault();\n        configFile = \"./parseMissingRecord.conf\";\n        ASSERT_FALSE(parseConfig());\n    }\n    TEST(parseConfig, correct_config_file){\n        setDefault();\n        configFile = \"./parse.conf\";\n        ASSERT_TRUE(parseConfig());\n    }\n    ```\n\n    #### 注意\n\n    在前面的函数中用作输入参数的**parsemissingcurrency . conf**、 **parseMissingRecord.conf** 和 **parse.conf** 文件可以在这里找到:[https://github . com/trainingypbackt/Advanced-CPlusPlus/tree/master/lesson 7/activity 01](https://github.com/TrainingByPackt/Advanced-CPlusPlus/tree/master/Lesson7/Activity01)。\n\n22.  Write the following test cases for the `fillCurrencyMap` function:\n\n    ```cpp\n    //Test Cases for fillCurrencyMap function...\n    TEST(fillCurrencyMap, wrong_delimiter){\n        currencyFile = \"./CurrencyWrongDelimiter.txt\";\n        ASSERT_FALSE(fillCurrencyMap());\n    }\n    TEST(fillCurrencyMap, extra_column){\n        currencyFile = \"./CurrencyExtraColumn.txt\";\n        ASSERT_FALSE(fillCurrencyMap());\n    }\n    TEST(fillCurrencyMap, correct_file){\n        currencyFile = \"./CurrencyConversion.txt\";\n        ASSERT_TRUE(fillCurrencyMap());\n    }\n    ```\n\n    #### 注意\n\n    在前面的函数中用作输入参数的**currency erriddelimiter . txt**、 **CurrencyExtraColumn.txt** 、 **CurrencyConversion.txt** 文件可以在这里找到:[https://github . com/trainingypbackt/Advanced-CPlusPlus/tree/master/lesson 7/activity 01](https://github.com/TrainingByPackt/Advanced-CPlusPlus/tree/master/Lesson7/Activity01)。\n\n23.  Write the following test cases for the parseRecordFile function:\n\n    ```cpp\n    //Test Cases for parseRecordFile function...\n    TEST(parseRecordFile, wrong_delimiter){\n        recordFile = \"./RecordWrongDelimiter.txt\";\n        ASSERT_FALSE(parseRecordFile());\n    }\n    TEST(parseRecordFile, extra_column){\n        recordFile = \"./RecordExtraColumn.txt\";\n        ASSERT_FALSE(parseRecordFile());\n    }\n    TEST(parseRecordFile, correct_file){\n        recordFile = \"./RecordFile.txt\";\n        ASSERT_TRUE(parseRecordFile());\n    }\n    ```\n\n    在前面的函数中用作输入参数的**recorderry delimiter . txt**、**recordextrectory column . txt**、 **RecordFile.txt** 文件可以在这里找到:[https://github . com/trainingypbackt/Advanced-CPlusPlus/tree/master/lesson 7/activity 01](https://github.com/TrainingByPackt/Advanced-CPlusPlus/tree/master/Lesson7/Activity01)。\n\n24.  Open the compiler. Compile and execute the `Util.cpp` and `ParseFileTestCases.cpp` files by writing the following commands:\n\n    ```cpp\n    g++ -c -g -Wall ../Util.cpp -I../\n    g++ -c -g -Wall ParseFileTestCases.cpp \n    g++ -g -Wall Util.o ParseFileTestCases.o -lgtest -lgtest_main -pthread -o ParseFileTestCases\n    ```\n\n    下面是这个的截图。您将看到所有存储在`Test.make`脚本文件中的命令。一旦执行，它将创建用于单元测试的二进制程序，称为`解析文件测试用例`。您还会注意到，在项目中创建了一个名为`单元测试`的目录。在这个目录中，所有与单元测试相关的代码都被写入，并且一个二进制文件被创建。此外，项目的依赖库`Util.o`也是通过在`Util.cpp`文件中编译项目而创建的:\n\n    ![](img/C14583_07_27.jpg)\n\n    ###### 图 7.27:执行脚本文件中的所有命令\n\n25.  Type the following command to run all the test cases:\n\n    ```cpp\n    ./ParseFileTestCases\n    ```\n\n    屏幕上的输出将显示运行的全部测试，即 8 个测试套件中的 31 个。它还将显示单个测试套件的统计数据以及通过/失败结果:\n\n![Figure 7.28: All tests running properly ](img/C14583_07_28.jpg)\n\n###### 图 7.28:所有测试运行正常\n\n以下是接下来测试的截图:\n\n![Figure 7.29: All tests running properly ](img/C14583_07_29.jpg)\n\n###### 图 7.29:所有测试运行正常\n\n最后，我们通过在测试用例的帮助下解析两个文件来检查我们开发的函数的准确性。这将确保我们的项目在与具有测试用例的不同功能/模块集成时运行良好。\n\n## 第 8 章-速度需求-性能和优化\n\n### 活动 1:优化拼写检查算法\n\n在本活动中，我们将开发一个简单的拼写检查演示，并尝试让它变得更快。可以用骨架文件 **Speller.cpp** 作为起点。执行以下步骤来实施本活动:\n\n1.  对于拼写检查的第一个实现(完整代码可以在 **Speller1.cpp** 中找到)–在`get 拼错()`函数中创建字典集:\n\n    ```cpp\n    set<string> setDict(vecDict.begin(), vecDict.end());\n    ```\n\n2.  循环检查文本单词，并使用`set::count()`方法检查字典中没有的单词。将拼错的单词添加到结果向量中:\n\n    ```cpp\n    vector<int> ret;\n    for(int i = 0; i < vecText.size(); ++ i)\n    {\n      const string &s = vecText[i];\n      if(!setDict.count(s))\n      {\n        ret.push_back(i);\n      }\n    };\n    ```\n\n3.  Open the terminal. Compile the program and run it as follows:\n\n    ```cpp\n    $ g++ -O3 Speller1.cpp Timer.cpp\n    $ ./a.out\n    ```\n\n    将生成以下输出:\n\n    ![Figure 8.60: Example output of the solution for Step 1 ](img/C14583_08_60.jpg)\n\n    ###### 图 8.60:步骤 1 解决方案的示例输出\n\n4.  打开 **Speller2.cpp** 文件，将`无序 _set`头文件添加到程序中:\n\n    ```cpp\n    #include <unordered_set>\n    ```\n\n5.  接下来，将字典使用的集合类型更改为`无序 _ 集合` :\n\n    ```cpp\n    unordered_set<string> setDict(vecDict.begin(), vecDict.end());\n    ```\n\n6.  Open the Terminal. Compile the program and run it as follows:\n\n    ```cpp\n    $ g++ -O3 Speller2.cpp Timer.cpp\n    $ ./a.out\n    ```\n\n    将生成以下输出:\n\n    ![Figure 8.61: Example output of the solution for Step 2 ](img/C14583_08_61.jpg)\n\n    ###### 图 8.61:步骤 2 解决方案的示例输出\n\n7.  For the third and final version, that is, **Speller3.cpp**, we will use a bloom filter. Start by defining a hash function based on the `BKDR` function. Add the following code to implement this:\n\n    ```cpp\n    const size_t SIZE = 16777215;\n    template<size_t SEED> size_t hasher(const string &s)\n    {\n      size_t h = 0;\n      size_t len = s.size();\n      for(size_t i = 0; i < len; i++)\n      {\n        h = h * SEED + s[i];\n      }\n      return h & SIZE;\n    }\n    ```\n\n    这里，我们使用了一个整数模板参数，这样我们就可以用相同的代码创建任意数量的不同散列函数。注意`16777215`常量的使用，它等于`2^24–1`。这让我们可以使用快速按位“与”运算符而不是“模”运算符来保持散列整数小于`大小`。如果你想改变大小，保持它小于二的 1 次方。\n\n8.  接下来，让我们为**中的布隆过滤器声明一个向量<bool>，并用字典中的单词填充它。使用三个散列函数。BKDR 散列可以被植入诸如 **131** 、 **3131** 、 **31313** 等值。添加以下代码来实现:\n\n    ```cpp\n    vector<bool> m_Bloom;\n    m_Bloom.resize(SIZE);\n    for(auto i = vecDict.begin(); i != vecDict.end(); ++ i)\n    {\n      m_Bloom[hasher<131>(*i)] = true;\n      m_Bloom[hasher<3131>(*i)] = true;\n      m_Bloom[hasher<31313>(*i)] = true;\n    }\n    ```</bool>** \n9.  Write the following code to create a loop that checks the words:\n\n    ```cpp\n    for(int i = 0; i < vecText.size(); ++ i)\n    {\n      const string &s = vecText[i];\n      bool hasNoBloom = \n              !m_Bloom[hasher<131>(s)] \n          &&  !m_Bloom[hasher<3131>(s)]\n          &&  !m_Bloom[hasher<31313>(s)];\n\n      if(hasNoBloom)\n      {\n        ret.push_back(i);\n      }\n      else if(!setDict.count(s))\n      {\n        ret.push_back(i);\n      }\n    }\n    ```\n\n    首先检查布隆过滤器，如果它在字典中找到这个词，我们必须验证它，就像我们以前做的那样。\n\n10.  Open the terminal. Compile the program and run it as follows:\n\n    ```cpp\n    $ g++ -O3 Speller3.cpp Timer.cpp\n    $ ./a.out\n    ```\n\n    将生成以下输出:\n\n![Figure 8.62: Example output of the solution for Step 3 ](img/C14583_08_62.jpg)\n\n###### 图 8.62:步骤 3 解决方案的示例输出\n\n在前面的活动中，我们试图解决一个现实世界的问题，并使其更加有效。让我们考虑三个步骤中每个实现的一些要点，如下所示:\n\n*   对于第一个版本，使用了带有`std::set`的最明显的解决方案–然而，性能可能较低，因为 set 数据结构基于二叉树，该二叉树具有`O(log N)`的复杂性来寻找元素。\n*   对于第二个版本，我们只需切换到`std::unordered_set`，就可以获得很大的性能提升，它使用哈希表作为底层数据结构。如果哈希函数好，性能会接近`O(1)`。\n*   第三个版本基于**布隆过滤器**数据结构，需要一些考虑。bloom filter 的主要性能优势是因为它是一个紧凑的数据结构，实际上并没有在其中存储实际的元素，因此提供了非常好的缓存性能。\n\n从实现的角度来看，以下准则适用:\n\n*   `向量<布尔>`可以用作后备存储，因为它是存储和检索位的有效方式。\n*   布隆过滤器的假阳性百分比应该是最小的——任何超过 5%的都是无效的。\n*   有许多字符串哈希算法–引用实现中使用了 **BKDR** 哈希算法。一个全面的字符串哈希算法及其实现可以在这里找到:[http://www.partow.net/programming/hashfunctions/index.html](http://www.partow.net/programming/hashfunctions/index.html)。\n*   哈希函数的数量和使用的 bloom 过滤器的大小对于获得性能优势非常关键。\n*   在决定布隆过滤器应该使用什么参数时，应该考虑数据集的性质——考虑到在这个例子中，拼错的单词很少，而且大部分都在字典中。\n\n鉴于我们收到的结果，有一些问题值得探讨:\n\n*   为什么布隆过滤器的性能提升如此微弱？\n*   使用更大或更小容量的 Bloom 过滤器会有什么影响？\n*   当使用更少或更多的散列函数时会发生什么？\n*   在什么条件下这个版本会比 **Speller2.cpp** 中的版本快很多？\n\n以下是这些问题的答案:\n\n*   Why is the improvement in performance so meager with the Bloom Filter?\n\n    `std::unordered_set`在达到存储的值之前，执行一次哈希操作，可能还会执行几次内存访问。我们使用的布隆过滤器执行三次哈希操作和三次内存访问。因此，本质上，布隆过滤器所做的工作不仅仅是哈希表。由于我们的字典中只有 31，870 个单词，因此布隆过滤器的缓存优势就丧失了。这是另一种情况，在这种情况下，由于缓存，传统的数据结构分析与实际结果不一致。\n\n*   What is the effect of using a larger or smaller capacity Bloom filter?\n\n    当使用更大的容量时，哈希冲突的数量会减少，误报也会减少，但缓存行为会恶化。相反，当使用较小的容量时，哈希冲突和误报会增加，但缓存行为会改善。\n\n*   What happens when fewer or more hash functions are used?\n\n    使用的散列函数越多，误报就越少，反之亦然。\n\n*   Under what conditions would this version be much faster than the one in Speller2.cpp?\n\n    当测试几个位的成本小于访问哈希表中的值的成本时，Bloom 过滤器工作得最好。只有当布隆过滤器位完全适合缓存，而字典不适合时，这种情况才会出现。"
  },
  {
    "path": "docs/adv-cpp/README.md",
    "content": "# C++ 高级编程\n\n> 原书：[Advanced C++](https://libgen.rs/book/index.php?md5=24E080E694C59B3F8E0220D0902724B0)\n> \n> 协议：[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)\n> \n> 阶段：机翻（1）\n> \n> 自豪地采用[谷歌翻译](https://translate.google.cn/)\n> \n> 让开源界提前感受入关。\n\n* [在线阅读](https://ccpp.apachecn.org)\n* [在线阅读（Gitee）](https://apachecn.gitee.io/apachecn-c-cpp-zh/)\n* [ApacheCN 学习资源](http://docs.apachecn.org/)\n\n## 贡献指南\n\n本项目需要校对，欢迎大家提交 Pull Request。\n\n> 请您勇敢地去翻译和改进翻译。虽然我们追求卓越，但我们并不要求您做到十全十美，因此请不要担心因为翻译上犯错——在大部分情况下，我们的服务器已经记录所有的翻译，因此您不必担心会因为您的失误遭到无法挽回的破坏。（改编自维基百科）\n\n## 联系方式\n\n### 负责人\n\n* [飞龙](https://github.com/wizardforcel): 562826179\n\n### 其他\n\n*   在我们的 [apachecn/apachecn-c-cpp-zh](https://github.com/apachecn/apachecn-c-cpp-zh) github 上提 issue.\n*   发邮件到 Email: `apachecn@163.com`.\n*   在我们的 [组织学习交流群](http://www.apachecn.org/organization/348.html) 中联系群主/管理员即可.\n\n## 赞助我们\n\n![](http://data.apachecn.org/img/about/donate.jpg)\n"
  },
  {
    "path": "docs/adv-cpp/SUMMARY.md",
    "content": "+   [C++ 高级编程](README.md)\n+   [零、前言](00.md)\n+   [一、可移植的 C++ 软件剖析](01.md)\n+   [二、不允许鸭子——类型和推导（一）](02.md)\n+   [三、不允许鸭子——模板和推导（二）](03.md)\n+   [四、不允许泄漏——异常和资源](04.md)\n+   [五、关注点分离——软件架构、函数和可变模板](05.md)\n+   [六、哲学家的晚餐——线程和并发](06.md)\n+   [七、流和输入/输出](07.md)\n+   [八、每个人都会跌倒，这是你爬起来的方式——测试和调试](08.md)\n+   [九、对速度的需求——性能和优化](09.md)\n+   [十、附录](10.md)\n"
  },
  {
    "path": "docs/adv-cpp-prog-cb/00.md",
    "content": "# 零、前言\n\n在这本书里，你将学习高级的 C++ 技术，你可以在你自己的 C++ 项目中使用。这本书教 C++ 使用的是一种食谱风格的方法，每种食谱都有例子和截图，你可以从 GitHub 下载并自己动手。这本书使用 C++ 17 规范来教 C++，并在最后偷偷看了一下 C++ 20 中增加的新特性。在一些食谱中，我们甚至会使用反汇编器来更好地理解 C++ 是如何编译的，以及某些决定对您的应用的影响。到本书结束时，您将掌握 C++ 的高级概念，并能够解决日常问题，这将使您的 C++ 编程更上一层楼。\n\n# 这本书是给谁的\n\n本书面向熟悉 C++ 并希望获得专家技能，成为精通 C++ 开发人员的中级 C++ 开发人员。假设对语言有很好的理解，包括对汇编的基本理解。\n\n# 这本书涵盖了什么\n\n[第 1 章](01.html)*入门库开发*，教你如何开发自己的库，包括一个最少惊喜原则的解释，如何命名一切，如何编写只有头文件的库，以及如何保证别人会继续使用你的库。\n\n[第 2 章](02.html)、*使用异常进行错误处理*，涵盖了 C++ 异常和错误处理的更高级的主题，包括对`noexcept`说明符和运算符的详细解释，RAII 如何在出现异常时支持资源管理，为什么应该避免从析构函数抛出，以及如何编写自己的异常。\n\n[第三章](03.html)、*实现移动语义*，提供了 C++ 移动语义的详细解释，包括对*大五*的解释，如何让你的类可移动，如何编写只移动(和不移动)不复制样式的类，如何正确实现一个移动构造函数，为什么`const &&`没有意义，如何使用引用限定。\n\n[第 4 章](04.html)，*使用模板进行泛型编程*，像专家一样教你如何编写模板函数，包括如何实现自己的 SFINAE，如何执行完美转发，如何使用`constexpr-if`语句，如何利用带有参数包的元组，如何在编译时循环使用参数包，如何使用类型特征实现同一个函数的不同版本，如何使用`template<auto>`，以及如何在自己的应用中利用显式类型声明。\n\n[第五章](05.html)、*并发和同步*，教你如何使用`std::mutex`(和朋友们)，什么时候使用原子类型，如何使用`mutable`关键字处理`const`类的线程安全，如何编写线程安全类，如何编写线程安全包装器，以及如何编写包含承诺和未来的异步 C++ 语言。\n\n[第 6 章](06.html)、*优化您的代码以获得性能*，涵盖了如何对您的 C++ 进行概要分析和基准测试，如何反汇编您的 C++ 以更好地理解如何优化您的代码，如何定位和删除不需要的内存分配，以及为什么`noexcept`有助于优化。\n\n[第 7 章](07.html)、*调试和测试*，带您了解如何使用`Catch2`对 C++ 进行单元测试，如何使用谷歌的 ASAN 和 UBSAN 杀毒软件动态分析您的代码是否存在内存损坏和未定义的行为，以及如何使用 NDEBUG。\n\n[第 8 章](08.html)、*创建和实现自己的容器*，通过创建一个始终排序的`std::vector`，教你如何编写自己的容器包装器。\n\n[第 9 章](09.html)，*探索类型擦除*，教你关于类型擦除需要知道的一切，包括如何通过继承和使用模板擦除类型，如何实现类型擦除模式，如何实现委托模式。\n\n[第 10 章](10.html)、*深入了解动态分配*，教你动态内存分配方面的进阶话题，包括如何正确使用`std::unique_ptr`和`std::shared_ptr`、如何处理循环引用、如何键入强制转换智能指针，以及堆如何在幕后工作，为你的应用提供动态内存。\n\n[第 11 章](11.html)、*C++ 中的常见模式*解释了计算机科学中不同的模式是如何在 c++ 中实现的，包括工厂模式、单例模式、装饰器模式和观察者模式，以及如何实现静态多态来编写自己的静态接口，而不需要虚拟继承。\n\n[第 12 章](12.html)、*仔细看看类型推导*，深入探究了在 C++ 17 中如何进行类型推导，包括`auto`、`decltype`和`template`如何自动推导它们的类型。本章以如何编写自己的 C++ 17 用户定义推导指南的例子结束。\n\n[第 13 章](13.html)*奖励:使用 C++ 20 特性*，提供了 C++ 20 新特性的预览，包括概念、模块、范围和协同程序。\n\n# 充分利用这本书\n\n我们假设您以前写过 C++ 并且已经熟悉了一些现代 C++ 特性。\n\n这本书使用 Ubuntu 来提供例子，你可以在阅读这本书的时候自己编译和运行。我们假设您对 Ubuntu、如何安装它以及如何使用 Linux 终端有一些基本的了解。\n\n我们在一些食谱中使用反汇编器来更好地理解编译器在幕后做什么。虽然您不需要知道如何阅读程序集来理解正在教授的内容，但是对 x86_64 程序集的基本理解将会有所帮助。\n\n# 下载示例代码文件\n\n你可以从你在[www.packt.com](http://www.packt.com)的账户下载这本书的示例代码文件。如果您在其他地方购买了这本书，您可以访问[www.packtpub.com/support](https://www.packtpub.com/support)并注册将文件直接通过电子邮件发送给您。\n\n您可以按照以下步骤下载代码文件:\n\n1.  登录或注册[www.packt.com](http://www.packt.com)。\n2.  选择“支持”选项卡。\n3.  点击代码下载。\n4.  在搜索框中输入图书的名称，并按照屏幕指示进行操作。\n\n下载文件后，请确保使用最新版本的解压缩文件夹:\n\n*   视窗系统的 WinRAR/7-Zip\n*   zipeg/izp/un ARX for MAC\n*   适用于 Linux 的 7-Zip/PeaZip\n\n这本书的代码包也托管在 GitHub 上的[https://GitHub . com/packt publishing/Advanced-CPP-Programming-cook book](https://github.com/PacktPublishing/Advanced-CPP-Programming-CookBook)。如果代码有更新，它将在现有的 GitHub 存储库中更新。\n\n我们还有来自丰富的图书和视频目录的其他代码包，可在**[【https://github.com/PacktPublishing/】](https://github.com/PacktPublishing/)**获得。看看他们！\n\n# 行动中的代码\n\n访问以下链接查看正在运行的代码的视频:[https://bit.ly/2tQoZyW](https://bit.ly/2tQoZyW)\n\n# 使用的约定\n\n本书通篇使用了许多文本约定。\n\n`constexpr`:用文本、数字、文件夹名、文件名、文件扩展名、路径名、虚拟网址和用户输入表示码字。这里有一个例子:“使用`noexcept`说明符来告诉编译器一个函数是否可以抛出 C++ 异常。”\n\n代码块设置如下:\n\n```cpp\nint main(void)\n{\n    the_answer is;\n    return 0;\n}\n```\n\n当我们希望将您的注意力吸引到代码块的特定部分时，相关的行或项目以粗体显示:\n\n```cpp\nint main(void)\n{\n    auto execute_on_exit = finally{[]{\n        std::cout << \"The answer is: 42\\n\";\n    }};\n}\n```\n\n任何命令行输入或输出都编写如下:\n\n```cpp\n> mkdir build && cd build\n> cmake ..\n> make recipe04_examples\n```\n\n**粗体**:表示一个新的术语、一个重要的单词或者你在屏幕上看到的单词。例如，重要的单词像这样出现在文本中。这里有一个例子:“在这个食谱中，我们将了解为什么在析构函数中抛出异常是一个**坏主意**\n\nWarnings or important notes appear like this. Tips and tricks appear like this.\n\n# 部分\n\n在这本书里，你会发现几个经常出现的标题(*准备*，*怎么做...*、*它是如何工作的...*、*还有更多...*和*参见*。\n\n要给出如何完成配方的明确说明，请使用以下章节:\n\n# 准备好\n\n本节告诉您配方中的预期内容，并描述如何设置配方所需的任何软件或任何初步设置。\n\n# 怎么做…\n\n本节包含遵循配方所需的步骤。\n\n# 它是如何工作的…\n\n这一部分通常包括对前一部分发生的事情的详细解释。\n\n# 还有更多…\n\n本节包含关于配方的附加信息，以便您更好地了解配方。\n\n# 请参见\n\n本节提供了该配方的其他有用信息的有用链接。\n\n# 取得联系\n\n我们随时欢迎读者的反馈。\n\n**一般反馈**:如果你对这本书的任何方面有疑问，在你的信息主题中提到书名，发邮件给我们`customercare@packtpub.com`。\n\n**勘误表**:虽然我们已经尽了最大的努力来保证内容的准确性，但是错误还是会发生。如果你在这本书里发现了一个错误，如果你能向我们报告，我们将不胜感激。请访问[www.packtpub.com/support/errata](https://www.packtpub.com/support/errata)，选择您的图书，点击勘误表提交链接，并输入详细信息。\n\n**盗版**:如果您在互联网上遇到任何形式的我们作品的非法拷贝，如果您能提供我们的位置地址或网站名称，我们将不胜感激。请通过`copyright@packt.com`联系我们，并提供材料链接。\n\n**如果你有兴趣成为一名作者**:如果有一个你有专长的话题，你有兴趣写或者投稿一本书，请访问[authors.packtpub.com](http://authors.packtpub.com/)。\n\n# 复习\n\n请留下评论。一旦你阅读并使用了这本书，为什么不在你购买它的网站上留下评论呢？然后，潜在的读者可以看到并使用您不带偏见的意见来做出购买决定，我们在 Packt 可以了解您对我们产品的看法，我们的作者可以看到您对他们的书的反馈。谢谢大家！\n\n更多关于 Packt 的信息，请访问[packt.com](http://www.packt.com/)。"
  },
  {
    "path": "docs/adv-cpp-prog-cb/01.md",
    "content": "# 一、库的开发入门\n\n在本章中，我们将介绍一些创建自己的库的有用方法，包括对最小惊奇原则的解释，它鼓励我们使用用户已经熟悉的语义来实现库。我们还将研究如何命名一切，以确保我们的自定义库不会与其他库冲突。此外，我们将研究如何创建仅头库，以及一些与库开发相关的最佳实践。最后，我们将以 boost 库的演示来结束本章，向您展示大型库是什么样子的，以及用户如何在自己的项目中使用它。\n\n在本章中，我们将介绍以下食谱:\n\n*   理解最少惊喜的原则\n*   如何命名一切\n*   仅标题库\n*   学习库开发最佳实践\n*   学习如何使用增强应用编程接口\n\n我们开始吧！\n\n# 技术要求\n\n要编译和运行本章中的示例，您必须拥有运行 Ubuntu 18.04 的计算机的管理权限，并且具有功能性互联网连接。在运行这些示例之前，您必须使用以下命令安装以下软件包:\n\n```cpp\n> sudo apt-get install build-essential git cmake\n```\n\n如果这安装在 Ubuntu 18.04 以外的任何操作系统上，则需要 GCC 7.4 或更高版本以及 CMake 3.6 或更高版本。\n\n# 理解最少惊喜的原则\n\n无论是使用现有的 C++ 库还是创建自己的库，理解最少惊奇的**原则**(也称为最少惊奇的**原则**)对于高效和有效地开发源代码至关重要。这个原则简单地说明了 C++ 库提供的任何特性都应该是直观的，并且应该按照开发人员的期望运行。另一种说法是，库的 API 应该是自文档化的。虽然这个原则在设计库的时候非常重要，但是它可以并且应该应用于所有形式的软件开发。在这个食谱中，我们将深入探讨这个原理。\n\n# 准备好\n\n与本章中的所有方法一样，确保满足所有技术要求，包括安装 Ubuntu 18.04 或更高版本，并在终端窗口中运行以下内容:\n\n```cpp\n> sudo apt-get install build-essential git cmake\n```\n\n这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后，打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。\n\n# 怎么做...\n\n执行以下步骤完成该配方:\n\n1.  从新的终端，运行以下代码下载源代码:\n\n```cpp\n> cd ~/\n> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git\n> cd Advanced-CPP-CookBook/chapter01\n```\n\n2.  要编译源代码，请运行以下代码:\n\n```cpp\n> mkdir build && cd build\n> cmake ..\n> make recipe01_examples\n```\n\n3.  编译完源代码后，您可以通过运行以下命令来执行该配方中的每个示例:\n\n```cpp\n> ./recipe01_example01\nThe answer is: 42\n\n> ./recipe01_example02\nThe answer is: 42\n\n> ./recipe01_example03\nThe answer is: 42\n\n> ./recipe01_example04\nThe answer is: 42\nThe answer is: 42\n\n> ./recipe01_example05\nThe answer is: 42\nThe answer is: 42\n\n> ./recipe01_example06\nThe answer is: 42\nThe answer is: 42\n\n> ./recipe01_example07\nThe answer is: 42\n\n> ./recipe01_example08\nThe answer is: 42\n\n> ./recipe01_example09\nThe answer is: 42\n```\n\n在下一节中，我们将逐一介绍这些示例，并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。\n\n# 它是如何工作的...\n\n如前一节所述，最少惊喜原则规定库的 API 应该是直观的和自我记录的，并且这个原则通常适用于所有形式的软件开发，而不仅仅是库设计。为了理解这一点，我们将看一些例子。\n\n# 例 1\n\n示例 1 演示了最小惊喜的原则，如下所示:\n\n```cpp\n#include <iostream>\n\nint sub(int a, int b)\n{ return a + b; }\n\nint main(void)\n{\n    std::cout << \"The answer is: \" << sub(41, 1) << '\\n';\n    return 0;\n}\n```\n\n如前面的例子所示，我们已经实现了一个将两个整数相加并返回结果的库 API。问题是我们把函数命名为`sub`，大多数开发者会联想到减法而不是加法；尽管 API 的功能与设计一样，但它打破了最不令人惊讶的原则，因为 API 的名称并不直观。\n\n# 例 2\n\n示例 2 演示了最少惊喜的原则，如下所示:\n\n```cpp\n#include <iostream>\n\nvoid add(int a, int &b)\n{ b += a; }\n\nint main(void)\n{\n    int a = 41, b = 1;\n    add(a, b);\n\n    std::cout << \"The answer is: \" << b << '\\n';\n    return 0;\n}\n```\n\n如前面的例子所示，我们已经实现了与我们在前面练习中实现的相同的库 API 它被设计为将两个数字相加并返回结果。这个例子的问题是应用编程接口正在实现以下内容:\n\n```cpp\nb += a;\n```\n\n在这个例子中，最少惊喜的原则以两种不同的方式被违反:\n\n*   加法函数的参数是`a`然后是`b`，即使我们将这个方程写成`b += a`，这意味着参数的顺序直观上是向后的。\n*   对于这个应用编程接口的用户来说，在不阅读源代码的情况下，结果会在`b`中返回并不明显。\n\n函数的签名应该使用用户已经习惯的语义来记录函数将如何执行，从而降低导致用户错误执行应用编程接口的概率。\n\n# 例 3\n\n示例 3 演示了最少惊喜的原则，如下所示:\n\n```cpp\n#include <iostream>\n\nint add(int a, int b)\n{ return a + b; }\n\nint main(void)\n{\n    std::cout << \"The answer is: \" << add(41, 1) << '\\n';\n    return 0;\n}\n```\n\n如前面的例子所示，我们在这里坚持最少惊喜的原则。API 的设计是将两个整数相加并返回结果，API 直观地按照预期执行这个动作。\n\n# 例 4\n\n示例 4 演示了最少惊喜的原则，如下所示:\n\n```cpp\n#include <stdio.h>\n#include <iostream>\n\nint main(void)\n{\n    printf(\"The answer is: %d\\n\", 42);\n    std::cout << \"The answer is: \" << 42 << '\\n';\n    return 0;\n}\n```\n\n如前例所示，最小惊喜原则的另一个很好的例子是`printf()`和`std::cout`的区别。`printf()`函数需要添加格式说明符来输出整数到`stdout`。`printf()`不直观的原因有很多:\n\n*   对于初学者来说，`printf()`函数的名字代表打印格式，并不直观(或者换句话说，函数的名字不是自记录的)。其他语言通过为打印功能选择更直观的名称来避免这个问题，例如`print()`或`console()`，它们更好地坚持了最少惊喜的原则。\n*   整数的格式说明符符号是`d`。同样，对于初学者来说，这是不直观的。在这种特殊情况下，`d`代表十进制，这是*有符号整数*的另一种说法。一个更好的格式说明符可能是`i`来匹配语言对`int`的使用。\n\n对比一下`std::cout`，代表字符输出。虽然这与`print()`或`console()`相比不太直观，但比`printf()`更直观。此外，为了向`stdout`输出一个整数，用户不需要记住一个格式说明符表来完成他们的任务。相反，他们可以简单地使用`<<`运算符。然后，API 为您处理格式，这不仅更直观，而且更安全(尤其是在使用`std::cin`而不是`scanf()`时)。\n\n# 例 5\n\n示例 5 演示了最小惊喜的原则，如下所示:\n\n```cpp\n#include <iostream>\n\nint main(void)\n{\n    auto answer = 41;\n\n    std::cout << \"The answer is: \" << ++ answer << '\\n';\n    std::cout << \"The answer is: \" << answer++ << '\\n';\n\n    return 0;\n}\n```\n\n如前例所示，`++ `操作者秉持最少惊喜的原则。虽然初学者必须学习`++ `代表增量运算符，这意味着变量由`1`增量，但是`++ `相对于变量的位置非常有帮助。\n\n要理解`++ variable`和`variable++ `的区别，用户所要做的就是正常的从左到右读代码。当`++ `在左边时，变量递增，然后返回变量的内容。当`++ `在右边时，返回变量的内容，然后变量递增。关于`++ `位置的唯一问题是左边的`++ `通常更有效(因为在增量操作之前，实现不需要额外的逻辑来存储变量值)。\n\n# 例 6\n\n示例 6 展示了最少惊喜的原理如下:\n\n```cpp\n#include <iostream>\n\nint add(int a, int b)\n{ return a + b; }\n\nint Sub(int a, int b)\n{ return a - b; }\n\nint main(void)\n{\n    std::cout << \"The answer is: \" << add(41, 1) << '\\n';\n    std::cout << \"The answer is: \" << Sub(43, 1) << '\\n';\n\n    return 0;\n}\n```\n\n如前面的代码所示，我们已经实现了两个不同的 API。第一个将两个整数相加并返回结果，而第二个减去两个整数并返回结果。减法功能有两个问题:\n\n*   加法函数是小写的，而减法函数是大写的。这是不直观的，API 的用户必须了解哪些 API 是小写的，哪些是大写的。\n*   C++ 标准的 API 都是蛇的大小写，也就是说它们利用小写字母`_`来表示一个空格。总的来说，用 snake case 设计 C++ 库 API 更好，因为初学者更容易发现这种直观性。需要注意的是，虽然一般都是这样，但是使用 snake case 是非常主观的，有几种语言并不遵循这个指导。最重要的是选择一个惯例并坚持下去。\n\n同样，确保您的应用编程接口模仿现有的语义可以确保用户可以快速轻松地学习使用您的应用编程接口，同时降低用户错误编写应用编程接口导致编译错误的可能性。\n\n# 例 7\n\n示例 7 展示了最小惊喜的原则，如下所示:\n\n```cpp\n#include <queue>\n#include <iostream>\n\nint main(void)\n{\n    std::queue<int> my_queue;\n\n    my_queue.emplace(42);\n    std::cout << \"The answer is: \" << my_queue.front() << '\\n';\n    my_queue.pop();\n\n    return 0;\n}\n```\n\n如前例所示，我们向您展示了如何使用`std::queue`向队列添加整数，将队列输出到`stdout`，并从队列中移除元素。这个例子的重点是强调这样一个事实，即 C++ 已经有了一套标准的命名约定，应该在 C++ 库开发过程中加以利用。\n\n如果您正在设计一个新的库，使用与 C++ 已经定义的相同的命名约定对您的库的用户是有帮助的。这样做将降低进入门槛，并提供更直观的应用编程接口。\n\n# 例 8\n\n示例 8 展示了如下最少惊喜的原则:\n\n```cpp\n#include <iostream>\n\nauto add(int a, int b)\n{ return a + b; }\n\nint main(void)\n{\n    std::cout << \"The answer is: \" << add(41, 1) << '\\n';\n    return 0;\n}\n```\n\n如前面的例子所示，我们正在演示如何使用`auto`，它告诉编译器自动计算出函数的返回类型是什么，不坚持最少惊喜的原则。虽然`auto`对于编写泛型代码非常有帮助，但是在设计库 API 时应该尽可能避免使用它。具体来说，为了让应用编程接口的用户理解应用编程接口的输入和输出是什么，用户必须阅读应用编程接口的实现，因为`auto`没有指定输出类型。\n\n# 例 9\n\n示例 9 展示了最少惊喜的原则，如下所示:\n\n```cpp\n#include <iostream>\n\ntemplate <typename T>\nT add(T a, T b)\n{ return a + b; }\n\nint main(void)\n{\n    std::cout << \"The answer is: \" << add(41, 1) << '\\n';\n    return 0;\n}\n```\n\n如前面的例子所示，我们正在演示一种更合适的方法来支持最少惊奇的原则，同时支持泛型编程。泛型编程(也称为模板元编程或使用 C++ 模板编程)为程序员提供了一种创建算法的方法，而无需说明算法中使用的类型。在这种情况下，`add`函数不指定输入类型，允许用户添加任意类型的两个值(在这种情况下，类型称为`T`，它可以采用支持`add`运算符的任何类型)。我们返回一个类型`T`，而不是返回一个不会说明输出类型的`auto`。虽然`T`在这里没有定义，因为它代表任何类型，但它确实告诉应用编程接口的用户，我们输入到该函数中的任何类型也将由该函数返回。同样的逻辑在 C++ 标准库中大量使用。\n\n# 如何命名一切\n\n当创建一个库时，命名所有内容是很重要的。这样做可以确保库提供的 API 的名称与用户代码或其他库提供的工具冲突。在本食谱中，我们将展示如何在自己的库中做到这一点。\n\n# 准备好\n\n与本章中的所有方法一样，确保满足所有技术要求，包括安装 Ubuntu 18.04 或更高版本，并在终端窗口中运行以下内容:\n\n```cpp\n> sudo apt-get install build-essential git cmake\n```\n\n这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后，打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。\n\n# 怎么做...\n\n您需要执行以下步骤来完成此配方:\n\n1.  从新的终端，运行以下命令下载源代码:\n\n```cpp\n> cd ~/\n> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git\n> cd Advanced-CPP-CookBook/chapter01\n```\n\n2.  要编译源代码，请运行以下代码:\n\n```cpp\n> mkdir build && cd build\n> cmake ..\n> make recipe02_examples\n```\n\n3.  编译完源代码后，您可以通过运行以下命令来执行该配方中的每个示例:\n\n```cpp\n> ./recipe02_example01\nThe answer is: 42\n\n> ./recipe02_example02\nThe answer is: 42\n```\n\n在下一节中，我们将逐一介绍这些示例，并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。\n\n# 它是如何工作的...\n\nC++ 为我们提供了将代码包装在`namespace`中的能力，它只是将`namespace`的名称添加到`namespace`代码内部的所有函数和变量中(应该注意的是，C 风格的宏不包含在`namespace`中，应该小心使用，因为 C 宏是预处理器特性，不会对代码的编译语法产生影响)。为了解释为什么我们在创建自己的库时应该什么都用`namespace`，我们将看一些例子。\n\n# 例 1\n\n示例 1 演示了如何在 C++ 中包装库的应用编程接口:\n\n```cpp\n// Contents of library.h\n\nnamespace library_name\n{\n    int my_api() { return 42; }\n    // ...\n}\n\n// Contents of main.cpp\n\n#include <iostream>\n\nint main(void)\n{\n    using namespace library_name;\n\n    std::cout << \"The answer is: \" << my_api() << '\\n';\n    return 0;\n}\n```\n\n如前面的例子所示，库的内容被包装在一个`namespace`中并存储在标题中(这个例子演示了一个只有标题的库，这是一个非常有用的设计方法，因为最终用户不必编译库，将它们安装在他/她的系统上，然后链接它们)。库用户只需包含库头文件，并使用`using namespace library_name`语句打开库的应用编程接口。如果用户有多个具有相同 API 名称的库，可以省略该语句以消除任何歧义。\n\n# 例 2\n\n示例 2 扩展了前面的示例，并演示了如何将您的库的 API 包装在一个 C++ 命名空间头文件库中，同时仍然包含全局变量:\n\n```cpp\n// Contents of library.h\n\nnamespace library_name\n{\n    namespace details { inline int answer = 42; }\n\n    int my_api() { return details::answer; }\n    // ...\n}\n\n// Contents of main.cpp\n\n#include <iostream>\n\nint main(void)\n{\n    using namespace library_name;\n\n    std::cout << \"The answer is: \" << my_api() << '\\n';\n    return 0;\n}\n```\n\n如前面的例子所示，C++ 17 被用来创建一个`inline`全局变量，该变量被包装在我们库的`namespace`中。`inline`变量是必需的，因为只有头文件的库没有定义全局变量的源文件；如果没有`inline`关键字，在头中定义一个全局变量将导致变量被多次定义(也就是说，结果将是编译期间的链接错误)。C++ 17 通过添加`inline`全局变量解决了这个问题，这允许一个只有头文件的库定义全局变量，而不需要复杂的魔法(比如从单例风格的函数返回一个静态变量的指针)。\n\n除了库的`namespace`，我们将全局变量包装在一个`details namespace`中。这样做是为了在您的库中创建一个`private`位置，以防库的用户声明`using namespace library_name`。如果用户这样做，所有由`library_name`命名空间包装的 API 和变量在`main()`函数的范围内变得全局可访问。出于这个原因，任何不打算被用户访问的私有 API 或变量应该被第二个`namespace`(通常称为`details`)包装，以防止它们的全局可访问性。最后，利用 C++ 17 的`inline`关键字，我们可以创建一个全局变量，在我们的库中使用，同时仍然支持一个仅头设计。\n\n# 仅标题库\n\n只有头文件的库和它们听起来完全一样；整个库是使用头文件(通常是单个头文件)实现的。只包含头文件的库的好处是它们很容易包含在您的项目中，因为您只需包含头文件就完成了(不需要编译库，因为没有要编译的源文件)。在这个食谱中，我们将了解一些在试图创建一个只有标题的库时出现的问题以及如何克服它们。这个方法很重要，因为如果你计划创建自己的库，一个只有头文件的库是一个很好的开始，并且很可能会提高你的采用率，因为下游用户将很容易将你的库集成到他们的代码库中。\n\n# 准备好\n\n与本章中的所有方法一样，确保满足所有技术要求，包括安装 Ubuntu 18.04 或更高版本，并在终端窗口中运行以下内容:\n\n```cpp\n> sudo apt-get install build-essential git cmake\n```\n\n这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后，打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。\n\n# 怎么做...\n\n您需要执行以下步骤来完成此配方:\n\n1.  从新的终端，运行以下命令下载源代码:\n\n```cpp\n> cd ~/\n> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git\n> cd Advanced-CPP-CookBook/chapter01\n```\n\n2.  要编译源代码，请运行以下代码:\n\n```cpp\n> mkdir build && cd build\n> cmake ..\n> make recipe03_examples\n```\n\n3.  编译完源代码后，您可以通过运行以下命令来执行该配方中的每个示例:\n\n```cpp\n> ./recipe03_example01\nThe answer is: 42\n\n> ./recipe03_example02\nThe answer is: 42\n\n> ./recipe03_example03\nThe answer is: 42\n\n> ./recipe03_example04\nThe answer is: 42\nThe answer is: 2a\n\n> ./recipe03_example05\n\n> ./recipe03_example06\nThe answer is: 42\n\n> ./recipe03_example07\nThe answer is: 42\n```\n\n在下一节中，我们将逐一介绍这些示例，并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。\n\n# 它是如何工作的...\n\n要创建只包含头文件的库，只需确保所有代码都在头文件中实现，如下所示:\n\n```cpp\n#ifndef MY_LIBRARY\n#define MY_LIBRARY\n\nnamespace library_name\n{\n    int my_api() { return 42; }\n}\n\n#endif\n```\n\n前面的示例实现了一个具有单个函数的简单库。这个库的整个实现可以在一个头文件中实现，并包含在我们的代码中，如下所示:\n\n```cpp\n#include \"my_library.h\"\n#include <iostream>\n\nint main(void)\n{\n    using namespace library_name;\n\n    std::cout << \"The answer is: \" << my_api() << '\\n';\n    return 0;\n}\n```\n\n虽然创建仅头库看起来很简单，但是在尝试创建仅头库时会出现一些需要考虑的问题。\n\n# 如何处理包括\n\n在前面的示例中，您可能已经注意到，当我们使用自定义的仅头库时，我们首先包含了该库。这是编写纯头文件库必不可少的第一步。当为纯头文件库编写示例或测试时，我们的库应该是我们要包含的第一件事，以确保所有头文件的依赖项都是在纯头文件库中定义的，而不是在我们的示例或测试中。\n\n例如，假设我们按如下方式更改库:\n\n```cpp\n#ifndef MY_LIBRARY\n#define MY_LIBRARY\n\nnamespace library_name\n{\n    void my_api()\n    {\n        std::cout << \"The answer is: 42\" << '\\n';\n    }\n}\n\n#endif\n```\n\n如前面的代码片段所示，我们的 API 现在输出到`stdout`而不是返回一个整数。我们可以使用新的应用编程接口，如下所示:\n\n```cpp\n#include <iostream>\n#include \"my_library.h\"\n\nint main(void)\n{\n    library_name::my_api();\n    return 0;\n}\n```\n\n虽然前面的代码按预期编译和运行，但是代码中有一个错误，可能只有库的用户才能识别。具体来说，如果您的库的用户交换包含或不包含的顺序`#include <iostream>`，代码将无法编译并产生以下错误:\n\n![](img/14a94075-f946-458b-aa5c-a4bfd158978a.png)\n\n这是因为纯头文件库本身并不包含它的所有依赖项。由于我们的示例将库放在其他包含之后，因此我们的示例意外地隐藏了这个问题。因此，在创建您自己的只包含标题的库时，一定要在您的测试和示例中首先包含该库，以确保这种类型的问题永远不会发生在您的用户身上。\n\n# 全局变量\n\n纯头文件库的最大限制之一是，在 C++ 17 之前，没有办法创建全局变量。尽管应该尽可能避免全局变量，但在某些情况下还是需要全局变量的。为了演示这一点，让我们创建一个简单的输出到`stdout`的应用编程接口，如下所示:\n\n```cpp\n#ifndef MY_LIBRARY\n#define MY_LIBRARY\n\n#include <iostream>\n#include <iomanip>\n\nnamespace library_name\n{\n    void my_api(bool show_hex = false)\n    {\n        if (show_hex) {\n            std::cout << std::hex << \"The answer is: \" << 42 << '\\n';\n        }\n        else {\n            std::cout << std::dec << \"The answer is: \" << 42 << '\\n';\n        }\n    }\n}\n\n#endif\n```\n\n前面的例子创建了一个输出到`stdout`的应用编程接口。如果用`true`而不是默认的`false`执行该应用编程接口，它将以十六进制而不是十进制格式输出整数。在这个例子中，从十进制到十六进制的改变实际上是我们库中的一个配置设置。然而，如果没有全局变量，我们将不得不求助于其他机制来实现这一点，包括宏，或者在前面的例子中，函数参数；后一种选择更糟糕，因为它将库的配置与其 API 相耦合，这意味着任何额外的配置选项都会改变 API 本身。\n\n解决这个问题的最好方法之一是在 C++ 17 中使用全局变量，如下所示:\n\n```cpp\n#ifndef MY_LIBRARY\n#define MY_LIBRARY\n\n#include <iostream>\n#include <iomanip>\n\nnamespace library_name\n{\n    namespace config\n    {\n        inline bool show_hex = false;\n    }\n\n    void my_api()\n    {\n        if (config::show_hex) {\n            std::cout << std::hex << \"The answer is: \" << 42 << '\\n';\n        }\n        else {\n            std::cout << std::dec << \"The answer is: \" << 42 << '\\n';\n        }\n    }\n}\n\n#endif\n```\n\n如前面的例子所示，我们在库中添加了一个名为`config`的新名称空间。我们的应用编程接口不再需要任何参数，而是基于内联全局变量来确定如何运行。现在，我们可以如下使用这个应用编程接口:\n\n```cpp\n#include \"my_library.h\"\n#include <iostream>\n\nint main(void)\n{\n    library_name::my_api();\n    library_name::config::show_hex = true;\n    library_name::my_api();\n\n    return 0;\n}\n```\n\n结果如下所示:\n\n![](img/2abe9bca-2a0e-4075-a8f8-e5fa3140663e.png)\n\n需要注意的是，我们将配置设置放在一个`config`命名空间中，以确保我们库的命名空间不会被名称冲突污染，这最终确保了全局变量的意图是显而易见的。\n\n# C 风格宏的问题\n\nC 风格宏的最大问题是，如果将它们放在 C++ 命名空间中，它们的名称不会被命名空间修饰。这意味着宏总是污染全局命名空间。例如，假设您正在编写一个需要检查变量值的库，如下所示:\n\n```cpp\n#ifndef MY_LIBRARY\n#define MY_LIBRARY\n\n#include <cassert>\n\nnamespace library_name\n{\n    #define CHECK(a) assert(a == 42)\n\n    void my_api(int val)\n    {\n        CHECK(val);\n    }\n}\n\n#endif\n```\n\n如前面的代码片段所示，我们创建了一个简单的 API，它使用 C 风格的宏来检查其实现中的整数值。前面例子的问题是，如果您试图将单元测试库与您自己的库一起使用，您很可能会遇到名称空间冲突。\n\nC++ 20 可以使用 C++ 20 模块来解决这个问题，这个话题我们将在[第 13 章](13.html)、*奖励-使用 C++ 20 特性*中详细讨论。具体来说，C++ 20 模块不向库的用户公开 C 风格的宏。积极的一面是，您将能够使用没有命名空间问题的宏，因为您的宏不会暴露给用户。这种方法的缺点是，许多库作者使用 C 风格的宏来配置库(例如，他们在包含库之前定义一个宏来更改其默认行为)。这种类型的库配置不适用于 C++ 模块，除非在编译库时在命令行上定义了宏。\n\n在 C++ 20 可用之前，如果需要使用宏，请确保手动为宏名称添加装饰，如下所示:\n\n```cpp\n#define LIBRARY_NAME__CHECK(a) assert(a == 42)\n```\n\n前一行代码将执行与将宏放在 C++ 命名空间中相同的操作，确保您的宏不会与其他库中的宏或用户可能定义的宏冲突。\n\n# 如何将大型库实现为仅头库\n\n理想情况下，仅头库使用单个头来实现。也就是说，用户只需要将一个标题复制到他们的源代码中就可以使用这个库。这种方法的问题是，对于非常大的项目，单个标题可能会变得非常大。一个很好的例子是一个流行的 c++ JSON 库，位于这里:[https://github . com/nlohmann/JSON/blob/develop/single _ include/nlohmann/JSON . HPP](https://github.com/nlohmann/json/blob/develop/single_include/nlohmann/json.hpp)。\n\n在撰写本文时，前面的库有 22，000 多行代码。试图修改一个有 22，000 行代码的文件是很糟糕的(如果你的编辑器能够处理的话)。一些项目通过使用几个头文件来实现它们的纯头文件库来克服这个问题，其中一个头文件根据需要包括各个头文件(例如，微软的 C++ 指南支持库就是这样实现的)。这种方法的问题在于，用户必须复制和维护多个头文件，随着头文件库的复杂性增加，这就开始违背了头文件库的目的。\n\n处理这个问题的另一种方法是使用像 CMake 这样的东西从多个头文件中自动生成一个头文件。例如，在下面的示例中，我们有一个只有标题的库，其标题如下:\n\n```cpp\n#include \"config.h\"\n\nnamespace library_name\n{\n    void my_api()\n    {\n        if (config::show_hex) {\n            std::cout << std::hex << \"The answer is: \" << 42 << '\\n';\n        }\n        else {\n            std::cout << std::dec << \"The answer is: \" << 42 << '\\n';\n        }\n    }\n}\n```\n\n如前面的代码片段所示，这与我们的配置示例相同，只是示例的配置部分已被包含到`config.h`文件中的内容所替换。我们可以如下创建第二个头文件:\n\n```cpp\nnamespace library_name\n{\n    namespace config\n    {\n        inline bool show_hex = false;\n    }\n}\n```\n\n这实现了示例的剩余部分。换句话说，我们已经把我们的标题分成了两个标题。我们仍然可以使用如下标题:\n\n```cpp\n#include \"apis.h\"\n\nint main(void)\n{\n    library_name::my_api();\n    return 0;\n}\n```\n\n然而，问题是我们库的用户需要两个标题的副本。为了解决这个问题，我们需要自动生成一个头文件。有许多方法可以做到这一点，但以下是使用 CMake 的一种方法:\n\n```cpp\nfile(STRINGS \"config.h\" CONFIG_H)\nfile(STRINGS \"apis.h\" APIS_H)\n\nlist(APPEND MY_LIBRARY_SINGLE\n    \"${CONFIG_H}\"\n    \"\"\n    \"${APIS_H}\"\n)\n\nfile(REMOVE \"my_library_single.h\")\nforeach(LINE IN LISTS MY_LIBRARY_SINGLE)\n    if(LINE MATCHES \"#include \\\"\")\n        file(APPEND \"my_library_single.h\" \"// ${LINE}\\n\")\n    else()\n        file(APPEND \"my_library_single.h\" \"${LINE}\\n\")\n    endif()\nendforeach()\n```\n\n前面的代码使用`file()`函数将两个头都读入到 CMake 变量中。这个函数将每个变量转换成一个字符串列表(每个字符串都是文件中的一行)。然后，我们将这两个文件合并成一个列表。为了创建新的、自动生成的单个头文件，我们遍历列表，并将每一行写到一个名为`my_library_single.h`的新头文件中。最后，如果我们看到对本地 include 的引用，我们会对其进行注释，以确保没有对附加头的引用。\n\n现在，我们可以使用新的单头文件，如下所示:\n\n```cpp\n#include \"my_library_single.h\"\n\nint main(void)\n{\n    library_name::my_api();\n    return 0;\n}\n```\n\n使用前面的方法，我们可以使用任意多的包含来开发我们的库，并且我们的构建系统可以自动生成我们的单个头文件，该文件将被最终用户使用，从而使我们两全其美。\n\n# 学习库开发最佳实践\n\n在编写自己的库时，所有库作者都应该遵循某些最佳实践。在本食谱中，我们将探索一些更高优先级的最佳实践，并以一些关于致力于定义这些最佳实践的项目的信息作为结束，包括一个注册系统，该系统为您的库提供了关于它编译得如何的评分。这个食谱很重要，因为它将教你如何制作最高质量的库，确保强大和充满活力的用户群。\n\n# 准备好\n\n与本章中的所有方法一样，确保满足所有技术要求，包括安装 Ubuntu 18.04 或更高版本，并在终端窗口中运行以下内容:\n\n```cpp\n> sudo apt-get install build-essential git cmake clang-tidy valgrind\n```\n\n这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后，打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。\n\n# 怎么做...\n\n您需要执行以下步骤来完成此配方:\n\n1.  从新的终端，运行以下命令下载源代码:\n\n```cpp\n> cd ~/\n> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git\n> cd Advanced-CPP-CookBook/chapter01\n```\n\n2.  要编译源代码，请运行以下代码:\n\n```cpp\n> mkdir build && cd build\n> cmake ..\n> make recipe04_examples\n```\n\n3.  编译完源代码后，您可以通过运行以下命令来执行该配方中的每个示例:\n\n```cpp\n> ./recipe04_example01 \n21862\n```\n\n在下一节中，我们将逐一介绍这些示例，并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。\n\n# 它是如何工作的...\n\n每个库作者都应该确保他们的库易于使用，并融入到用户自己的项目中。这样做将确保您的用户继续使用您的库，从而随着时间的推移增加用户群。让我们来看看其中的一些最佳实践。\n\n# 警告呢？\n\n对于任何一个库作者来说，最不可能的结果就是确保您的代码在编译时尽可能多地启用警告。可悲的是，GCC 并没有使这个过程变得简单，因为没有一个警告标志来统治它们，特别是因为 GCC 有许多警告标志对现代版本的 C++ 没有用(换句话说，在某种意义上，它们是互斥的)。最好从以下警告开始:\n\n```cpp\n-Wall -Wextra -pedantic -Werror\n```\n\n这将打开大多数重要的警告，同时确保您的示例或测试编译的任何警告都将生成错误。然而，对于一些库来说，这还不够。在撰写本文时，以下是微软指南支持库使用的标志:\n\n```cpp\n-Wall -Wcast-align -Wconversion -Wctor-dtor-privacy -Werror -Wextra -Wpedantic -Wshadow -Wsign-conversion\n```\n\nGSL 使用的另一个警告是转换警告，当您在不同的整数类型之间转换时，它会告诉您。如果你使用的是 Clang，这个过程会容易很多，因为它提供了`-Weverything`。如果清除 GCC 提供的所有警告工作量太大，解决这个问题的一种方法是确保您的库在启用此警告的情况下用 Clang 编译器编译，这将确保您的代码在 GCC 提供的大多数警告下编译。这样，当您的用户必须确保在他们的代码中启用特定的警告时，他们就不会对您的库有任何问题，因为您已经测试了尽可能多的警告。\n\n# 静态和动态分析\n\n除了测试警告，还应该使用静态和动态分析工具测试库。同样，作为一个库的作者，您必须假设您的用户可能使用静态和动态分析工具来支持他们自己的应用的质量。如果你的库触发了这些工具，你的用户更有可能寻找已经被更彻底测试过的替代工具。\n\n对于 C++，有大量的工具可以用来分析您的库。在这个食谱中，我们将重点介绍 Clang Tidy 和 Valgrind，这两个都是免费使用的。让我们看看下面这个简单的例子:\n\n```cpp\n#include <iostream>\n\nint universe()\n{\n    auto i = new int;\n    int the_answer;\n    return the_answer;\n}\n\nint main()\n{\n    std::cout << universe() << '\\n';\n    return 0;\n}\n```\n\n在前面的例子中，我们创建了一个名为`universe()`的函数，它返回一个整数并分配一个整数。在我们的主功能中，我们的`universe()`功能将结果输出到`stdout`。\n\n为了静态地分析前面的代码，我们可以如下使用 CMake:\n\n```cpp\nset(CMAKE_CXX_CLANG_TIDY clang-tidy)\n```\n\n前一行代码告诉 CMake 在编译前一个例子时使用`clang-tidy`。当我们编译代码时，我们会得到以下结果:\n\n![](img/90475cde-2dd5-45e5-a2fe-e2a0ed81b304.png)\n\n如果您的库的用户已经使用 Clang Tidy 打开了静态分析，这是他们可能收到的错误，即使他们的代码非常好。如果您正在使用其他人的库，并遇到了这个问题，克服这个问题的一种方法是将库作为系统包含包括在内，这将告诉工具(如 Clang Tidy)忽略这些错误。然而，这并不总是有效的，因为有些库需要使用宏，这会将库的逻辑暴露给您自己的代码，从而导致混乱。一般来说，如果你是一个库开发人员，尽可能地静态分析你的库，因为你不知道你的用户会如何使用你的库。\n\n动态分析也是如此。前面的分析没有检测到明显的内存泄漏。为了识别这一点，我们可以使用`valgrind`，如下所示:\n\n![](img/e501ffa7-bfb7-46d1-b2e1-38630c9e0921.png)\n\n如前面的截图所示，`valgrind`能够检测到我们代码中的内存泄漏。实际上，`valgrind`也检测到了这样一个事实，即我们从来没有在`universe()`函数中初始化我们的临时变量，但是输出过于冗长，无法在这里显示。同样，如果您不能识别您的库的这些类型的问题，您将最终向您的用户暴露这些错误。\n\n# 证明文件\n\n对于任何好的库来说，文档都是绝对必要的。除了有问题的代码，缺少文档绝对会阻止其他人使用你的库。库应该易于设置和安装，甚至更易于学习和整合到您自己的应用中。使用现有 C++ 库最令人沮丧的一个方面是缺少文档。\n\n# CII 最佳实践\n\n在这个食谱中，我们已经提到了一些所有库开发人员都应该纳入到他们的项目中的常见最佳实践。除了这些最佳实践，CII 最佳实践项目还提供了更完整的最佳实践列表:https://bestpractices.coreinfrastructure.org/en。\n\nCII 最佳实践计划提供了一个全面的最佳实践列表，该列表会随着时间的推移而更新，库开发人员(以及一般的任何应用)都可以利用。这些最佳实践分为及格、银牌和金牌，金牌实践最难实现。您的分数越高，用户越有可能使用您的库，因为它显示了承诺和稳定性。\n\n# 学习如何使用增强应用编程接口\n\n增强库是一组旨在与标准 C++ 库协同工作的库。事实上，目前由 C++ 提供的许多库都源于 boost 库。增强库提供了从容器、时钟和计时器到更复杂的数学应用编程接口(如图形和循环冗余校验计算)的一切。在本食谱中，我们将学习如何使用增强库，特别是演示大型库是什么样子的，以及这样的库如何包含在用户的项目中。这个食谱很重要，因为它将展示一个库有多复杂，教你如何相应地编写自己的库。\n\n# 准备好\n\n与本章中的所有方法一样，确保满足所有技术要求，包括安装 Ubuntu 18.04 或更高版本，并在终端窗口中运行以下内容:\n\n```cpp\n> sudo apt-get install build-essential git cmake libboost-all-dev\n```\n\n这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后，打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。\n\n# 怎么做...\n\n您需要执行以下步骤来完成此配方:\n\n1.  从新的终端，运行以下命令下载源代码:\n\n```cpp\n> cd ~/\n> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git\n> cd Advanced-CPP-CookBook/chapter01\n```\n\n2.  要编译源代码，请运行以下代码:\n\n```cpp\n> mkdir build && cd build\n> cmake ..\n> make recipe05_examples\n```\n\n3.  编译完源代码后，您可以通过运行以下命令来执行该配方中的每个示例:\n\n```cpp\n> ./recipe05_example01\nDate/Time: 1553894555446451393 nanoseconds since Jan 1, 1970\n> ./recipe05_example02\n[2019-03-29 15:22:36.756819] [0x00007f5ee158b740] [debug] debug message\n[2019-03-29 15:22:36.756846] [0x00007f5ee158b740] [info] info message\n```\n\n在下一节中，我们将逐一介绍这些示例，并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。\n\n# 它是如何工作的...\n\n增强库提供了一组用户 API，这些 API 实现了大多数程序中通常需要的功能。这些库可以包含在您自己的项目中，以简化您的代码，并提供一个成品库可能是什么样子的示例。为了解释你自己的库如何被其他人利用，让我们看一些如何使用增强库的例子。\n\n# 例 1\n\n在本例中，我们使用 boost APIs 将当前日期和时间输出到`stdout`，如下所示:\n\n```cpp\n#include <iostream>\n#include <boost/chrono.hpp>\n\nint main(void)\n{\n    using namespace boost::chrono;\n\n    std::cout << \"Date/Time: \" << system_clock::now() << '\\n';\n    return 0;\n}\n```\n\n如前例所示，当前日期和时间作为自 Unix 纪元(1970 年 1 月 1 日)以来的纳秒总数输出到`stdout`。除了在源代码中包含 boost 之外，还必须将应用与 boost 库进行链接。在这种情况下，我们需要针对以下内容进行链接:\n\n```cpp\n-lboost_chrono -lboost_system -lpthread\n```\n\n如何做到这一点的一个例子可以在这个食谱下载的`CMakeLists.txt`文件中看到。一旦这些库被链接到您的项目，您的代码将能够利用其中的 API。这一额外的步骤就是为什么只有标题的库在创建自己的库时如此有用，因为它们消除了额外链接的需要。\n\n# 例 2\n\n在本例中，我们演示了如何使用 boost 的简单日志记录 API 登录到控制台，如下所示:\n\n```cpp\n#include <boost/log/trivial.hpp>\n\nint main(void)\n{\n    BOOST_LOG_TRIVIAL(debug) << \"debug message\";\n    BOOST_LOG_TRIVIAL(info) << \"info message\";\n    return 0;\n}\n```\n\n如前例所示，`\"debug message\"`和`\"info message\"`消息被输出到`stdout`。除了与适当的增强库链接之外，我们还必须在编译过程中包含以下定义:\n\n```cpp\n-DBOOST_LOG_DYN_LINK -lboost_log -lboost_system -lpthread\n```\n\n同样，链接这些库可以确保您在代码中使用的 API(如前面的示例所示)存在于可执行文件中。\n\n# 请参见\n\n有关增强库的更多信息，请查看[https://www.boost.org/](https://www.boost.org/)。"
  },
  {
    "path": "docs/adv-cpp-prog-cb/02.md",
    "content": "# 二、将异常用于错误处理\n\n在本章中，我们将学习一些高级的 C++ 异常处理技术。这里我们假设您对如何抛出和捕获 C++ 异常有基本的了解。本章将教您一些更高级的 C++ 异常处理技术，而不是专注于 C++ 异常的基础知识。这包括正确使用`noexcept`说明符和`noexcept`运算符，这样您就可以正确地将您的 APIs 标记为可能抛出异常或者明确地不抛出 C++ 异常，而不是在出现无法处理的错误时调用`std::terminate()`。\n\n本章还将解释术语**资源获取是初始化** ( **RAII** )是什么，以及它如何补充 C++ 异常处理。我们还将讨论为什么不应该从类的析构函数中抛出 C++ 异常，以及如何处理这些类型的问题。最后，我们将看看如何创建您自己的自定义 C++ 异常，包括提供一些关于创建自己的异常时应该做什么和不应该做什么的基本指南。\n\n从本章提供的信息中，您将更好地了解 C++ 异常是如何在幕后工作的，以及可以使用 C++ 异常来构建更健壮和可靠的 C++ 程序的类型。\n\n本章中的配方如下:\n\n*   使用 noexcept 说明符\n*   使用 noexcept 运算符\n*   使用 RAII\n*   学习为什么不在析构函数中抛出异常\n*   轻松创建自己的异常类\n\n# 技术要求\n\n要编译和运行本章中的示例，您必须拥有运行 Ubuntu 18.04 的计算机的管理权限，并且具有功能性互联网连接。在运行这些示例之前，您必须安装以下内容:\n\n```cpp\nsudo apt-get install build-essential git cmake\n```\n\n如果这安装在 Ubuntu 18.04 以外的任何操作系统上，则需要 GCC 7.4 或更高版本以及 CMake 3.6 或更高版本。\n\n# 使用 noexcept 说明符\n\n`noexcept`说明符用于告诉编译器一个函数是否可以抛出 C++ 异常。如果函数标有`noexcept`说明符，则不允许引发异常，如果是，则在引发异常时将调用`std::terminate()`。如果函数没有`noexcept`说明符，可以正常抛出异常。\n\n在本食谱中，我们将探索如何在您自己的代码中使用`noexcept`说明符。这个说明符很重要，因为它是您正在创建的应用编程接口和应用编程接口用户之间的契约。当使用`noexcept`说明符时，它告诉应用编程接口的用户在使用应用编程接口时不需要考虑异常。它还告诉作者，如果他们将`noexcept`说明符添加到他们的 API 中，他们必须确保不抛出任何异常，在某些情况下，这要求作者捕获所有可能的异常，如果异常无法处理，要么处理它们，要么调用`std::terminate()`。此外，还有某些操作，例如`std::move`，在这些操作中，如果不担心损坏，就不能抛出异常，因为如果抛出异常，移动操作通常不能安全地反转。最后，对于一些编译器来说，在您的 API 中添加`noexcept`将减少函数的整体大小，从而导致整体应用更小。\n\n# 准备好\n\n开始之前，请确保满足所有技术要求，包括安装 Ubuntu 18.04 或更高版本，并在终端窗口中运行以下内容:\n\n```cpp\n> sudo apt-get install build-essential git cmake\n```\n\n这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后，打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。\n\n# 怎么做...\n\n要尝试此配方，请执行以下步骤:\n\n1.  从新的终端，运行以下命令下载源代码:\n\n```cpp\n> cd ~/\n> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git\n> cd Advanced-CPP-CookBook/chapter02\n```\n\n2.  要编译源代码，请运行以下命令:\n\n```cpp\n> mkdir build && cd build\n> cmake ..\n> make recipe01_examples\n```\n\n3.  编译源代码后，您可以通过运行以下命令来执行该配方中的每个示例:\n\n```cpp\n> ./recipe01_example01\nThe answer is: 42\n\n> ./recipe01_example02\nterminate called after throwing an instance of 'std::runtime_error'\nwhat(): The answer is: 42\nAborted\n\n> ./recipe01_example03\nThe answer is: 42\n\n> ./recipe01_example04\nterminate called after throwing an instance of 'std::runtime_error'\nwhat(): The answer is: 42\nAborted\n\n> ./recipe01_example05\nfoo: 18446744069414584320\nfoo: T is too large\n```\n\n在下一节中，我们将逐一介绍这些示例，并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。\n\n# 它是如何工作的...\n\n首先，让我们简单回顾一下 C++ 异常是如何抛出和捕获的。在下面的例子中，我们将从一个函数抛出一个异常，然后在我们的`main()`函数中捕获该异常:\n\n```cpp\n#include <iostream>\n#include <stdexcept>\n\nvoid foo()\n{\n    throw std::runtime_error(\"The answer is: 42\");\n}\n\nint main(void)\n{\n    try {\n        foo();\n    }\n    catch(const std::exception &e) {\n        std::cout << e.what() << '\\n';\n    }\n\n    return 0;\n}\n```\n\n如前面的例子所示，我们创建了一个名为`foo()`的函数，它抛出了一个异常。这个函数在我们的`try` / `catch`块内的`main()`函数中被调用，该函数用于捕捉在`try`块内执行的代码可能抛出的任何异常，在本例中，该块是`foo()`函数。当`foo()`功能抛出异常时，成功捕获并输出到`stdout`。\n\n所有这些都可以工作，因为我们没有给`foo()`函数添加`noexcept`说明符。默认情况下，一个函数被允许抛出一个异常，就像我们在这个例子中做的那样。然而，在某些情况下，我们不希望抛出异常，这取决于我们期望函数如何执行。具体来说，函数如何处理异常可以定义为以下内容(称为异常安全):\n\n*   **不抛出保证**:函数不能抛出异常，如果在内部抛出异常，必须捕捉并处理异常，包括分配失败。\n*   **强异常安全**:函数可以抛出异常，如果抛出异常，任何被函数修改的状态都会回滚或者撤销，没有任何副作用。\n*   **基本异常安全**:函数可以抛出异常，如果抛出异常，任何被函数修改过的状态都会回滚或者撤销，但是有可能产生副作用。应该注意的是，这些副作用不包括不变量，这意味着程序处于有效的、未损坏的状态。\n*   **无异常安全**:函数可以抛出异常，如果抛出异常，程序可能会进入损坏状态。\n\n一般来说，如果一个函数有不抛出的保证，就用`noexcept`标注；否则，就不是了。异常安全如此重要的一个例子是`std::move`。例如，假设我们有两个`std::vector`的例子，我们希望将一个向量移动到另一个向量中。要执行移动，`std::vector`可能会将向量的每个元素从一个实例移动到另一个实例。如果对象在移动时被允许抛出，那么向量可能会在移动过程中出现异常(也就是说，向量中一半的对象被成功移动)。当异常发生时，`std::vector`显然会尝试在返回异常之前，通过将这些动作移回原始向量来撤销已经执行的动作。问题是，试图将对象移回需要`std::move()`，这可能会再次抛出异常，导致嵌套异常。在实践中，将一个`std::vector`实例移动到另一个实例实际上并不会执行逐对象移动，但是调整大小会执行，在这个特定的问题中，标准库需要使用`std::move_if_noexcept`来处理这种情况，以提供异常安全，当允许对象的移动构造函数抛出时，这又会返回到副本。\n\n`noexcept`说明符通过明确声明函数不允许抛出异常来克服这些类型的问题。这不仅告诉应用编程接口的用户，他们可以安全地使用该函数，而不必担心抛出异常并可能破坏程序的执行，而且还迫使函数的作者安全地处理所有可能的异常或调用`std::terminate()`。虽然`noexcept`依赖于编译器，也通过在定义时减少应用的整体大小来提供优化，但它的主要用途是陈述函数的异常安全性，以便其他函数可以推断函数将如何执行。\n\n在下面的例子中，我们将`noexcept`说明符添加到前面定义的`foo()`函数中:\n\n```cpp\n#include <iostream>\n#include <stdexcept>\n\nvoid foo() noexcept\n{\n    throw std::runtime_error(\"The answer is: 42\");\n}\n\nint main(void)\n{\n    try {\n        foo();\n    }\n    catch(const std::exception &e) {\n        std::cout << e.what() << '\\n';\n    }\n\n    return 0;\n}\n```\n\n当这个例子被编译和执行时，我们得到如下结果:\n\n![](img/f99d2218-74b5-47f1-8108-6a38646732a8.png)\n\n如前例所示，添加了`noexcept`说明符，告知编译器不允许`foo()`抛出异常。然而，由于`foo()`函数确实抛出了一个异常，所以当它被执行时，会调用`std::terminate()`。事实上，在这个例子中，`std::terminate()`将总是被调用，这是编译器能够检测和警告的事情。\n\n调用`std::terminate()`显然不是一个程序想要的结果。在这种特定的情况下，由于作者已经将该功能标记为`noexcept`，因此由作者来处理所有可能的异常。这可以通过以下方式实现:\n\n```cpp\n#include <iostream>\n#include <stdexcept>\n\nvoid foo() noexcept\n{\n    try {\n        throw std::runtime_error(\"The answer is: 42\");\n    }\n    catch(const std::exception &e) {\n        std::cout << e.what() << '\\n';\n    }\n}\n\nint main(void)\n{\n    foo();\n    return 0;\n}\n```\n\n如上例所示，异常被包装在`try` / `catch`块中，以确保在`foo()`函数完成其执行之前安全地处理异常。此外，在本例中，仅捕获源自`std::exception()`的异常。这是作者说哪些类型的异常可以安全处理的方式。例如，如果抛出一个整数而不是`std::exception()`，`std::terminate()`仍然会自动执行，因为`noexcept`被添加到了`foo()`函数中。换句话说，作为作者，您只需要处理您实际上可以安全处理的异常。剩下的会为你送到`std::terminate()`；请理解，通过这样做，您改变了函数的异常安全性。如果您打算用不抛出保证来定义函数，那么该函数根本不会抛出异常。\n\n还需要注意的是，如果将一个函数标记为`noexcept`，不仅需要注意自己抛出的异常，还需要注意可能自己抛出的函数。在这种情况下，`std::cout`在`foo()`函数中使用，这意味着作者必须要么故意忽略`std::cout`可能抛出的任何异常，这将导致对`std::terminate()`的调用(这就是我们在这里所做的)，要么作者需要确定`std::cout`可能抛出哪些异常并尝试安全地处理它们，包括`std::bad_alloc`之类的异常。\n\n如果所提供的索引超出向量的界限，则`std::vector.at()`函数抛出`std::out_of_range()`异常。在这种情况下，作者可以捕捉这种类型的异常并返回一个默认值，允许作者安全地将该函数标记为`noexcept`。\n\n`noexcept`说明符也可以作为一个函数，采用布尔表达式，如下例所示:\n\n```cpp\n#include <iostream>\n#include <stdexcept>\n\nvoid foo() noexcept(true)\n{\n    throw std::runtime_error(\"The answer is: 42\");\n}\n\nint main(void)\n{\n    try {\n        foo();\n    }\n    catch(const std::exception &e) {\n        std::cout << e.what() << '\\n';\n    }\n\n    return 0;\n}\n```\n\n执行时会产生以下结果:\n\n![](img/c75fc0ac-3445-4fe6-a20c-934b783a5d96.png)\n\n如前例所示，`noexcept`说明符写成了`noexcept(true)`。如果表达式评估为真，则好像提供了`noexcept`。如果表达式的计算结果为假，就好像省略了`noexcept`说明符，允许抛出异常。在前面的例子中，表达式的计算结果为 true，这意味着函数不允许抛出异常，这导致在`foo()`抛出异常时调用`std::terminate()`。\n\n让我们看一个更复杂的例子来演示如何使用它。在下面的例子中，我们将创建一个名为`foo()`的函数，该函数将整数值移位 32 位，并将结果转换为 64 位整数。这个例子将使用模板元编程编写，允许我们在任何整数类型上使用这个函数:\n\n```cpp\n#include <limits>\n#include <iostream>\n#include <stdexcept>\n\ntemplate<typename T>\nuint64_t foo(T val) noexcept(sizeof(T) <= 4)\n{\n    if constexpr(sizeof(T) <= 4) {\n        return static_cast<uint64_t>(val) << 32;\n    }\n\n    throw std::runtime_error(\"T is too large\");\n}\n\nint main(void)\n{\n    try {\n        uint32_t val1 = std::numeric_limits<uint32_t>::max();\n        std::cout << \"foo: \" << foo(val1) << '\\n';\n\n        uint64_t val2 = std::numeric_limits<uint64_t>::max();\n        std::cout << \"foo: \" << foo(val2) << '\\n';\n    }\n    catch(const std::exception &e) {\n        std::cout << e.what() << '\\n';\n    }\n\n    return 0;\n}\n```\n\n执行时会产生以下结果:\n\n![](img/182cbe31-a769-4160-884a-7f9445e380d2.png)\n\n如前例所示，`foo()`函数的问题是，如果用户提供 64 位整数，它不能移位 32 位而不产生溢出。然而，如果提供的整数是 32 位或更少，则`foo()`功能是完全安全的。为了实现`foo()`函数，我们使用了`noexcept`说明符来声明如果提供的整数是 32 位或更少，则不允许该函数抛出异常。如果提供的整数大于 32 位，则允许抛出异常，在这种情况下，这是一个`std::runtime_error()`异常，表示整数太大，无法安全移位。\n\n# 使用 noexcept 运算符\n\n`noexcept`运算符是编译时检查，用于询问编译器某个函数是否被标记为`noexcept`。在 C++ 17 中，这可以与编译时的`if`语句(即在编译时评估的`if`语句，可用于在编译期间从可执行文件中添加/删除代码)配对，以根据是否允许函数引发异常来更改程序的语义。\n\n在本食谱中，我们将探索如何在您自己的代码中使用`noexcept`运算符。这个运算符很重要，因为在某些情况下，您可能不知道一个函数是否能够通过简单地查看其定义来引发异常。例如，如果函数使用`noexcept`说明符，您的代码可能无法确定函数是否会抛出，因为您可能不知道(基于函数的输入)`noexcept`说明符的计算结果。`noexcept`操作符为您提供了处理这些类型场景的机制，这是必不可少的，尤其是在元编程时。\n\n# 准备好\n\n开始之前，请确保满足所有技术要求，包括安装 Ubuntu 18.04 或更高版本，并在终端窗口中运行以下内容:\n\n```cpp\n> sudo apt-get install build-essential git cmake\n```\n\n这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后，打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。\n\n# 怎么做...\n\n执行以下步骤来尝试配方:\n\n1.  从新的终端，运行以下命令下载源代码:\n\n```cpp\n> cd ~/\n> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git\n> cd Advanced-CPP-CookBook/chapter02\n```\n\n2.  要编译源代码，请运行以下命令:\n\n```cpp\n> mkdir build && cd build\n> cmake ..\n> make recipe02_examples\n```\n\n3.  编译源代码后，您可以通过运行以下命令来执行该配方中的每个示例:\n\n```cpp\n> ./recipe02_example01\ncould foo throw: true\n\n> ./recipe02_example02\ncould foo throw: true\ncould foo throw: true\ncould foo throw: false\ncould foo throw: false\n\n> ./recipe02_example03\nterminate called after throwing an instance of 'std::runtime_error'\nwhat(): The answer is: 42\nAborted\n\n> ./recipe02_example04\n\n> ./recipe02_example05\nterminate called after throwing an instance of 'std::runtime_error'\nwhat(): The answer is: 42\nAborted\n\n> ./recipe02_example06\ncould foo throw: true\ncould foo throw: true\n```\n\n在下一节中，我们将逐一介绍这些示例，并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。\n\n# 它是如何工作的...\n\n`noexcept`运算符用于确定一个函数是否可以抛出。让我们从一个简单的例子开始:\n\n```cpp\n#include <iostream>\n#include <stdexcept>\n\nvoid foo()\n{\n    std::cout << \"The answer is: 42\\n\";\n}\n\nint main(void)\n{\n    std::cout << std::boolalpha;\n    std::cout << \"could foo throw: \" << !noexcept(foo()) << '\\n';\n    return 0;\n}\n```\n\n这将导致以下结果:\n\n![](img/afafa314-071a-4aa9-8896-0c19d3282f99.png)\n\n如前例所示，我们定义了一个输出到`stdout`的`foo()`函数。我们实际上并不执行`foo()`，而是使用`noexcept`运算符来检查`foo()`函数是否可以抛出。如你所见，答案是肯定的；这个函数可以抛出。这是因为我们没有用`noexcept`来标记`foo()`函数，并且，如前一个配方中所述，默认情况下函数可以抛出。\n\n还需要注意的是，我们在`noexcept`表达式中加入了`!`。这是因为如果函数被标记为`noexcept`，则`noexcept`返回`true`，这意味着该函数不允许抛出。然而，在我们的例子中，我们不是问函数是否不能抛出，而是问函数是否能抛出，因此逻辑布尔反转。\n\n让我们通过在我们的示例中添加几个函数来对此进行扩展。具体来说，在下面的例子中，我们将添加一些抛出的函数以及一些标记为`noexcept`的函数:\n\n```cpp\n#include <iostream>\n#include <stdexcept>\n\nvoid foo1()\n{\n    std::cout << \"The answer is: 42\\n\";\n}\n\nvoid foo2()\n{\n    throw std::runtime_error(\"The answer is: 42\");\n}\n\nvoid foo3() noexcept\n{\n    std::cout << \"The answer is: 42\\n\";\n}\n\nvoid foo4() noexcept\n{\n    throw std::runtime_error(\"The answer is: 42\");\n}\n\nint main(void)\n{\n    std::cout << std::boolalpha;\n    std::cout << \"could foo throw: \" << !noexcept(foo1()) << '\\n';\n    std::cout << \"could foo throw: \" << !noexcept(foo2()) << '\\n';\n    std::cout << \"could foo throw: \" << !noexcept(foo3()) << '\\n';\n    std::cout << \"could foo throw: \" << !noexcept(foo4()) << '\\n';\n    return 0;\n}\n```\n\n这将导致以下结果:\n\n![](img/6c634422-311e-40ae-a7f8-e20aa940f7a4.png)\n\n如前例所示，如果一个函数标有`noexcept`，`noexcept`运算符返回`true`(在我们的示例中，它输出`false`)。更重要的是，敏锐的观察者会注意到抛出异常的函数不会改变`noexcept`运算符的输出。也就是说，如果某个功能*可以*抛出异常，则`noexcept`操作符返回`false`，否则*会*抛出异常。这很重要，因为知道函数*是否会*抛出异常的唯一方法是执行它。`noexcept`说明符唯一声明的是函数是否允许抛出异常。没有说明是否会抛出异常*。推而广之，`noexcept`运算符不会告诉您函数*是否会*抛出，而是告诉您该函数是否标有`noexcept`说明符(更重要的是，`noexcept`说明符的计算结果)。*\n\n *在我们尝试在一个更现实的例子中使用`noexcept`说明符之前，让我们看一下下面的例子:\n\n```cpp\n#include <iostream>\n#include <stdexcept>\n\nvoid foo()\n{\n    throw std::runtime_error(\"The answer is: 42\");\n}\n\nint main(void)\n{\n    foo();\n}\n```\n\n如前面的例子所示，我们已经定义了一个抛出的`foo()`函数，然后我们从我们的主函数中调用这个函数，导致`std::terminate()`被调用，因为我们在离开程序之前没有处理异常。在更复杂的设置中，我们可能不知道`foo()`是否抛出，因此，如果不需要的话，我们可能不想增加额外的异常处理开销。为了更好地解释这一点，让我们检查这个例子的`main()`函数的结果汇编代码:\n\n![](img/8741e7cf-194c-44e7-84c5-b48af8c04011.png)\n\n可以看到，`main`函数很简单，除了调用`foo`函数之外，不包含任何额外的逻辑。具体来说，`main`函数没有任何捕捉逻辑。\n\n现在，让我们在一个更具体的例子中使用`noexcept`运算符:\n\n```cpp\n#include <iostream>\n#include <stdexcept>\n\nvoid foo()\n{\n    throw std::runtime_error(\"The answer is: 42\");\n}\n\nint main(void)\n{\n    if constexpr(noexcept(foo())) {\n        foo();\n    }\n    else {\n        try {\n            foo();\n        }\n        catch (...)\n        { }\n    }\n}\n```\n\n如上例所示，在 C++ 17 中添加的`if`语句中，我们将`noexcept`运算符与`constepxr`运算符结合使用。这让我们可以问编译器`foo()`是否允许抛出。如果是，我们在`try` / `catch`块中执行`foo()`函数，这样我们就可以根据需要处理任何可能的异常。如果我们检查这个函数的程序集，如下面的截图所示，我们可以看到一些额外的`catch`逻辑被添加到生成的二进制文件中，以根据需要处理异常:\n\n![](img/49f6bfba-8bae-40ba-8987-e352f7b9625c.png)\n\n现在，让我们通过声明`foo()`函数不允许使用`noexcept`说明符来抛出，从而将同一个示例向前推进一步:\n\n```cpp\n#include <iostream>\n#include <stdexcept>\n\nvoid foo() noexcept\n{\n    throw std::runtime_error(\"The answer is: 42\");\n}\n\nint main(void)\n{\n    if constexpr(noexcept(foo())) {\n        foo();\n    }\n    else {\n        try {\n            foo();\n        }\n        catch (...)\n        { }\n    }\n}\n```\n\n如前例所示，由于`foo()`函数被标记为`noexcept`，程序调用`std::terminate()`。此外，如果我们查看最终的装配，我们可以看到`main()`功能不再包含额外的`try` / `catch`逻辑，这意味着我们的优化成功了:\n\n![](img/2d0478f9-51e3-4438-b303-7d4872bf5a80.png)\n\n最后，如果我们不知道被调用的函数是否可以抛出，我们可能不知道如何标记自己的函数。让我们看下面的例子来说明这个问题:\n\n```cpp\n#include <iostream>\n#include <stdexcept>\n\nvoid foo1()\n{\n    std::cout << \"The answer is: 42\\n\";\n}\n\nvoid foo2() noexcept(noexcept(foo1()))\n{\n    foo1();\n}\n\nint main(void)\n{\n    std::cout << std::boolalpha;\n    std::cout << \"could foo throw: \" << !noexcept(foo1()) << '\\n';\n    std::cout << \"could foo throw: \" << !noexcept(foo2()) << '\\n';\n}\n```\n\n这将导致以下结果:\n\n![](img/5c98505b-2992-4bc6-a927-e4eb3315fd00.png)\n\n如前例所示，`foo1()`函数没有标注`noexcept`说明符，这意味着允许它抛出异常。在`foo2()`中，我们想要确保我们的`noexcept`说明符是正确的，但是我们称之为`foo1()`，并且在这个例子中，我们假设我们不知道`foo1()`是否是`noexcept`。\n\n为了确保`foo2()`被正确标记，我们结合本食谱和上一份食谱中的经验教训来正确标记功能。具体来说，我们使用`noexcept`运算符来告诉我们`foo1()`函数是否会抛出，然后我们使用`noexcept`说明符的布尔表达式语法来使用`noexcept`运算符的结果来标记`foo2()`是否为`noexcept`。如果`foo1()`标有`noexcept`，`noexcept`操作者将返回`true`，导致`foo2()`被标为`noexcept(true)`，与简单的说明`noexcept`相同。如果`foo1()`没有标记为`noexcept`，则`noexcept`运算符将返回`false`，此时`noexcept`说明符将标记为`noexcept(false)`，这与不添加`noexcept`说明符(即允许函数抛出异常)是一样的。\n\n# 使用 RAII\n\nRAII 是一种编程原则，它声明资源与获取资源的对象的生存期相关联。RAII 是 C++ 语言的一个强大特性，它确实有助于将 C++ 与 C 区分开来，有助于防止资源泄漏和一般的不稳定性。\n\n在这个食谱中，我们将深入研究 RAII 是如何工作的，以及如何使用 RAII 来确保 C++ 异常不会导致资源泄漏。RAII 对于任何 C++ 应用来说都是一项关键技术，应该尽可能使用。\n\n# 准备好\n\n开始之前，请确保满足所有技术要求，包括安装 Ubuntu 18.04 或更高版本，并在终端窗口中运行以下内容:\n\n```cpp\n> sudo apt-get install build-essential git cmake\n```\n\n这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后，打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。\n\n# 怎么做...\n\n您需要执行以下步骤来尝试配方:\n\n1.  从新的终端，运行以下命令下载源代码:\n\n```cpp\n> cd ~/\n> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git\n> cd Advanced-CPP-CookBook/chapter02\n```\n\n2.  要编译源代码，请运行以下命令:\n\n```cpp\n> mkdir build && cd build\n> cmake ..\n> make recipe03_examples\n```\n\n3.  编译源代码后，您可以通过运行以下命令来执行该配方中的每个示例:\n\n```cpp\n> ./recipe03_example01\nThe answer is: 42\n\n> ./recipe03_example02\nThe answer is: 42\n\n> ./recipe03_example03\nThe answer is not: 43\n\n> ./recipe03_example04\nThe answer is: 42\n\n> ./recipe03_example05\nstep 1: Collect answers\nThe answer is: 42\n```\n\n在下一节中，我们将逐一介绍这些示例，并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。\n\n# 它是如何工作的...\n\n为了更好地理解 RAII 是如何工作的，我们必须首先研究 C++ 中的类是如何工作的，因为 C++ 类是用来实现 RAII 的。让我们看一个简单的例子。C++ 类同时支持构造函数和析构函数，如下所示:\n\n```cpp\n#include <iostream>\n#include <stdexcept>\n\nclass the_answer\n{\npublic:\n    the_answer()\n    {\n        std::cout << \"The answer is: \";\n    }\n\n    ~the_answer()\n    {\n        std::cout << \"42\\n\";\n    }\n};\n\nint main(void)\n{\n    the_answer is;\n    return 0;\n}\n```\n\n在编译和执行时，这会产生以下结果:\n\n![](img/1991efef-e0b0-48f0-9c36-1a62bfbec715.png)\n\n在前面的例子中，我们用构造函数和析构函数创建了一个类。当我们创建类的实例时，调用构造函数，当类的实例失去作用域时，类被销毁。这是一个简单的 C++ 模式，自从 C++ 的最初版本由比雅尼·斯特劳斯特鲁普创建以来就一直存在。在幕后，编译器在类第一次实例化时调用构造函数，但更重要的是，当类的实例化失去作用域时，编译器必须向执行销毁函数的程序中注入代码。这里需要理解的重要一点是，这个附加逻辑是由程序员的编译器自动插入到程序中的。\n\n在引入类之前，程序员必须手动向程序添加构造和销毁逻辑，虽然构造是一件相当简单的事情，但销毁却不是。C 语言中这类问题的一个典型例子是存储文件句柄。程序员将添加对`open()`函数的调用以打开文件句柄，当文件完成时，将添加对`close()`的调用以关闭文件句柄，忘记对所有可能出现的错误情况执行`close()`函数。这包括当代码长达数百行，程序中的新成员添加了另一个错误案例时，忘记根据需要调用`close()`。\n\nRAII 通过确保一旦类失去作用域，不管控制流路径是什么，获取的资源都会被释放，从而解决了这个问题。让我们看看下面的例子:\n\n```cpp\n#include <iostream>\n#include <stdexcept>\n\nclass the_answer\n{\npublic:\n\n    int *answer{};\n\n    the_answer() :\n        answer{new int}\n    {\n        *answer = 42;\n    }\n\n    ~the_answer()\n    {\n        std::cout << \"The answer is: \" << *answer << '\\n';\n        delete answer;\n    }\n};\n\nint main(void)\n{\n    the_answer is;\n\n    if (*is.answer == 42) {\n        return 0;\n    }\n\n    return 1;\n}\n```\n\n在这个例子中，我们分配一个整数，并在类的构造函数中初始化它。这里需要注意的重要一点是，我们不需要从`new`操作员那里检查`nullptr`。这是因为如果内存分配失败，`new`运算符将抛出异常。如果发生这种情况，不仅构造函数的其余部分不会被执行，而且对象本身也不会被构造。这意味着如果构造函数成功执行，您就知道该类的实例处于有效状态，并且实际上包含一个资源，当该类的实例失去作用域时，该资源将被销毁\n\n该类的析构函数然后输出到`stdout`并删除先前分配的内存。这里需要理解的重要一点是，无论代码采用什么控制路径，当类的实例失去作用域时，这个资源都会被释放。程序员只需要担心类的寿命。\n\n资源的生命周期与分配资源的对象的生命周期直接相关，这一思想很重要，因为它解决了存在 C++ 异常时程序控制流的复杂问题。让我们看看下面的例子:\n\n```cpp\n#include <iostream>\n#include <stdexcept>\n\nclass the_answer\n{\npublic:\n\n    int *answer{};\n\n    the_answer() :\n        answer{new int}\n    {\n        *answer = 43;\n    }\n\n    ~the_answer()\n    {\n        std::cout << \"The answer is not: \" << *answer << '\\n';\n        delete answer;\n    }\n};\n\nvoid foo()\n{\n    the_answer is;\n\n    if (*is.answer == 42) {\n        return;\n    }\n\n    throw std::runtime_error(\"\");\n}\n\nint main(void)\n{\n    try {\n        foo();\n    }\n    catch(...)\n    { }\n\n    return 0;\n}\n```\n\n在这个例子中，我们创建了与前一个例子相同的类，但是，在我们的`foo()`函数中，我们抛出了一个异常。但是`foo()`函数不需要捕捉这个异常来确保分配的内存被正确释放。相反，析构函数为我们处理这个。在 C++ 中，许多函数可能会抛出，如果没有 RAII，每一个可能抛出的函数都需要包装在一个`try` / `catch`块中，以确保分配的任何资源都被正确释放。事实上，我们在 C 代码中经常看到这种模式，尤其是在内核级编程中，使用`goto`语句来确保在一个函数中，如果发生错误，该函数可以适当地展开自己，以释放之前可能获得的任何资源。这个结果是一组代码，用于检查程序中每个函数调用的结果以及正确处理错误所需的逻辑。\n\n有了这种类型的编程模型，难怪资源泄漏在 C 中如此常见。RAII 结合 C++ 异常消除了对这种容易出错的逻辑的需求，导致代码不太可能泄漏资源。\n\n在存在 C++ 异常的情况下如何处理 RAII 不在本书的讨论范围之内，因为它需要更深入地研究 C++ 异常支持是如何实现的。需要记住的重要一点是，C++ 异常比检查函数的返回值是否有错误更快(因为 C++ 异常是使用无开销算法实现的)，但是当抛出实际的异常时会很慢(因为程序必须展开堆栈并根据需要正确执行每个类的析构函数)。由于这个原因，以及诸如可维护性等其他原因，C++ 异常永远不应该用于有效的控制流。\n\nRAII 可以使用的另一种方式是`finally`模式，由 C++ **指南支持库** ( **GSL** )提供。`finally`模式利用 RAI 中只包含析构函数的部分，当函数的控制流复杂或可能抛出时，提供一种简单的机制来执行非基于资源的清理。考虑以下示例:\n\n```cpp\n#include <iostream>\n#include <stdexcept>\n\ntemplate<typename FUNC>\nclass finally\n{\n    FUNC m_func;\n\npublic:\n    finally(FUNC func) :\n        m_func{func}\n    { }\n\n    ~finally()\n    {\n        m_func();\n    }\n};\n\nint main(void)\n{\n    auto execute_on_exit = finally{[]{\n        std::cout << \"The answer is: 42\\n\";\n    }};\n}\n```\n\n在前面的例子中，我们创建了一个能够存储 lambda 函数的类，当`finally`类的一个实例失去作用域时执行该函数。在这种特殊情况下，当`finally`类被破坏时，我们输出到`stdout`。虽然这使用了类似于 RAII 的模式，但在技术上这不是 RAII，因为没有获得任何资源。\n\n另外，如果确实需要获取资源，应该使用 RAII 而不是`finally`模式。相反，`finally`模式在您没有获取资源但想要在函数返回时执行代码时很有用，不管程序采取什么控制流路径(条件分支或 C++ 异常)。\n\n为了演示这一点，让我们看一个更复杂的例子:\n\n```cpp\n#include <iostream>\n#include <stdexcept>\n\ntemplate<typename FUNC>\nclass finally\n{\n    FUNC m_func;\n\npublic:\n    finally(FUNC func) :\n        m_func{func}\n    { }\n\n    ~finally()\n    {\n        m_func();\n    }\n};\n\nint main(void)\n{\n    try {\n        auto execute_on_exit = finally{[]{\n            std::cout << \"The answer is: 42\\n\";\n        }};\n\n        std::cout << \"step 1: Collect answers\\n\";\n        throw std::runtime_error(\"???\");\n        std::cout << \"step 3: Profit\\n\";\n    }\n    catch (...)\n    { }\n}\n```\n\n执行时，我们会得到以下结果:\n\n![](img/974fa02a-a5bd-462b-aa43-3951b03d15dc.png)\n\n在前面的例子中，我们希望确保无论代码做什么，我们总是输出到`stdout`。在执行过程中，我们抛出了一个异常，即使抛出了异常，我们的`finally`代码也是按预期执行的。\n\n# 学习为什么不在析构函数中抛出异常\n\n在这个食谱中，我们将讨论 C++ 异常的问题，特别是在类析构函数中抛出异常，这是应该不惜一切代价避免的。这个食谱中的经验很重要，因为与其他函数不同，C++ 类析构函数在默认情况下被标记为`noexcept`，这意味着如果你不小心在类析构函数中抛出了一个异常，你的程序将调用`std::terminate()`，即使析构函数没有被公开标记为`noexcept`。\n\n# 准备好\n\n开始之前，请确保满足所有技术要求，包括安装 Ubuntu 18.04 或更高版本，并在终端窗口中运行以下内容:\n\n```cpp\n> sudo apt-get install build-essential git cmake\n```\n\n这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后，打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。\n\n# 怎么做...\n\n执行以下步骤来尝试配方:\n\n1.  从新的终端，运行以下命令下载源代码:\n\n```cpp\n> cd ~/\n> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git\n> cd Advanced-CPP-CookBook/chapter02\n```\n\n2.  要编译源代码，请运行以下命令:\n\n```cpp\n> mkdir build && cd build\n> cmake ..\n> make recipe04_examples\n```\n\n3.  编译源代码后，您可以通过运行以下命令来执行该配方中的每个示例:\n\n```cpp\n> ./recipe04_example01\nterminate called after throwing an instance of 'std::runtime_error'\nwhat(): 42\nAborted\n\n> ./recipe04_example02\nThe answer is: 42\n\n> ./recipe04_example03\nterminate called after throwing an instance of 'std::runtime_error'\nwhat(): 42\nAborted\n\n> ./recipe04_example04\n# exceptions: 2\nThe answer is: 42\nThe answer is: always 42\n```\n\n在下一节中，我们将逐一介绍这些示例，并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。\n\n# 它是如何工作的...\n\n在这个食谱中，我们将了解为什么在析构函数中抛出异常是一个糟糕的*想法，以及为什么类析构函数被默认标记为`noexcept`。首先，让我们看一个简单的例子:*\n\n```cpp\n#include <iostream>\n#include <stdexcept>\n\nclass the_answer\n{\npublic:\n    ~the_answer()\n    {\n        throw std::runtime_error(\"42\");\n    }\n};\n\nint main(void)\n{\n    try {\n        the_answer is;\n    }\n    catch (const std::exception &e) {\n        std::cout << \"The answer is: \" << e.what() << '\\n';\n    }\n}\n```\n\n当我们执行此操作时，我们会得到以下结果:\n\n![](img/3d30c668-41a6-430a-8fc4-a95a1da1f660.png)\n\n在这个例子中，我们可以看到，如果我们从类析构函数抛出一个异常，就会调用`std::terminate()`。这是因为，默认情况下，类析构函数被标记为`noexcept`。\n\n我们可以通过将类的析构函数标记为`noexcept(false)`来显式允许类的析构函数抛出来改变这一点，如下例所示:\n\n```cpp\n#include <iostream>\n#include <stdexcept>\n\nclass the_answer\n{\npublic:\n    ~the_answer() noexcept(false)\n    {\n        throw std::runtime_error(\"42\");\n    }\n};\n\nint main(void)\n{\n    try {\n        the_answer is;\n }\n    catch (const std::exception &e) {\n        std::cout << \"The answer is: \" << e.what() << '\\n';\n    }\n}\n```\n\n如前面的示例所示，当类被销毁时，会引发异常并得到正确处理。即使成功处理了这个问题，我们也要问自己，在我们捕捉到这个异常后，程序的状态是什么？析构函数没有成功完成。如果这个类更复杂，并且有它管理的状态/资源，我们能断定我们关心的状态/资源被正确处理/释放了吗？简短的回答是否定的，这和用锤子破坏硬盘是一样的。如果你用锤子猛击硬盘来破坏它，你真的破坏了硬盘上的数据吗？没有办法知道，因为当你用锤子敲击硬盘时，你打碎了用来回答这个问题的电子设备。当您试图销毁硬盘驱动器时，您需要一个可靠的过程来确保在任何情况下销毁驱动器的过程都不会使数据处于可恢复状态。否则，你没有办法知道自己处于什么状态，没有办法回去。\n\n这同样适用于 C++ 类。销毁一个 C++ 类需要是一个必须提供基本异常安全的操作(也就是说，程序的状态是确定性的，有一些可能的副作用)。否则，唯一的另一个逻辑行动就是调用`std::terminate()`，因为你无法确定如果程序继续执行会发生什么。\n\n除了将程序置于未定义状态之外，从析构函数中抛出异常的另一个问题是，如果已经抛出了异常，会发生什么？`try` / `catch`区块捕捉到了什么？让我们看一个这类问题的例子:\n\n```cpp\n#include <iostream>\n#include <stdexcept>\n\nclass the_answer\n{\npublic:\n    ~the_answer() noexcept(false)\n    {\n        throw std::runtime_error(\"42\");\n    }\n};\n\nint main(void)\n{\n    try {\n        the_answer is;\n        throw std::runtime_error(\"first exception\");\n    }\n    catch (const std::exception &e) {\n        std::cout << \"The answer is: \" << e.what() << '\\n';\n    }\n}\n```\n\n在前面的例子中，我们将析构函数标记为`noexcept(false)`，就像我们在前面的例子中所做的那样，但是我们在调用析构函数之前抛出，这意味着，当调用析构函数时，已经有一个异常正在被处理。现在，当我们试图抛出时，虽然析构函数被标记为`noexcept(false)`，但是`std::terminate()`被调用:\n\n![](img/afeb2214-83a3-4320-bc7d-abea44492169.png)\n\n原因是 C++ 库没有办法处理这种情况，因为`try` / `catch`块不能处理多个异常。然而，可能有一个以上的未决例外；我们只需要一个`try` / `catch`块来处理每个异常。当我们有嵌套异常时，就会出现这种情况，如本例所示:\n\n```cpp\n#include <iostream>\n#include <stdexcept>\n\nclass nested\n{\npublic:\n    ~nested()\n    {\n        std::cout << \"# exceptions: \" << std::uncaught_exceptions() << '\\n';\n    }\n};\n\nclass the_answer\n{\npublic:\n    ~the_answer()\n    {\n        try {\n            nested n;\n            throw std::runtime_error(\"42\");\n        }\n        catch (const std::exception &e) {\n            std::cout << \"The answer is: \" << e.what() << '\\n';\n        }\n    }\n};\n```\n\n在本例中，我们将从创建一个输出调用`std::uncaught_exceptions()`结果的类开始，该类返回当前正在处理的异常总数。然后，我们将创建第二个类，该类创建第一个类，然后从其析构函数中抛出，需要注意的是，析构函数中的所有代码都被包装在一个`try` / `catch`块中:\n\n```cpp\nint main(void)\n{\n    try {\n        the_answer is;\n        throw std::runtime_error(\"always 42\");\n    }\n    catch (const std::exception &e) {\n        std::cout << \"The answer is: \" << e.what() << '\\n';\n    }\n}\n```\n\n执行此示例时，我们会得到以下结果:\n\n![](img/afcfefea-5caa-45ba-8cb6-817eb3023c2f.png)\n\n最后，我们将创建这个第二类，并用另一个`try` / `catch`块再次抛出。与前面的例子不同，所有的异常都得到了正确的处理，事实上，不需要`noexcept(false)`来确保这段代码正确执行，因为对于抛出的每个异常，我们都有一个`try` / `catch`块。即使一个异常在析构函数中被抛出，它也得到正确的处理，这意味着析构函数安全地执行并保持`noexcept`兼容，即使第二个类在两个正在处理的异常存在的情况下执行。\n\n# 轻松创建自己的异常类\n\n在本食谱中，您将学习如何轻松创建自己的异常类型。这是需要学习的重要一课，因为虽然 C++ 异常很容易自己创建，但是应该遵循一些准则来确保安全地完成。\n\n# 准备好\n\n开始之前，请确保满足所有技术要求，包括安装 Ubuntu 18.04 或更高版本，并在终端窗口中运行以下内容:\n\n```cpp\n> sudo apt-get install build-essential git cmake\n```\n\n这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后，打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。\n\n# 怎么做...\n\n执行以下步骤来尝试配方:\n\n1.  从新的终端，运行以下命令下载源代码:\n\n```cpp\n> cd ~/\n> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git\n> cd Advanced-CPP-CookBook/chapter02\n```\n\n2.  要编译源代码，请运行以下命令:\n\n```cpp\n> mkdir build && cd build\n> cmake ..\n> make recipe05_examples\n```\n\n3.  编译源代码后，您可以通过运行以下命令来执行该配方中的每个示例:\n\n```cpp\n> ./recipe05_example01\nThe answer is: 42\n\n> ./recipe05_example02\nThe answer is: 42\n\n> ./recipe05_example03\nThe answer is: 42\n```\n\n在下一节中，我们将逐一介绍这些示例，并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。\n\n# 它是如何工作的...\n\n创建您自己的 C++ 异常允许您过滤出您得到的异常类型。例如，异常是来自您的代码还是 C++ 库？通过创建自己的 C++ 异常，您可以在运行时用自己的代码轻松回答这些问题。让我们看看下面的例子:\n\n```cpp\n#include <iostream>\n#include <stdexcept>\n\nclass the_answer : public std::exception\n{\npublic:\n    the_answer() = default;\n    const char *what() const noexcept\n    {\n        return \"The answer is: 42\";\n    }\n};\n\nint main(void)\n{\n    try {\n        throw the_answer{};\n    }\n    catch (const std::exception &e) {\n        std::cout << e.what() << '\\n';\n    }\n}\n```\n\n如前面的例子所示，我们通过继承`std::exception`来创建自己的 C++ 异常。这不是一个要求。从技术上讲，任何东西都可以是 C++ 异常，包括整数。然而，从`std::exception`开始，给了你一个标准的工作界面，包括覆盖`what()`函数，该函数描述了抛出的异常。\n\n在前面的例子中，我们在`what()`函数中返回一个硬编码字符串。这是理想的异常类型(甚至比 C++ 库提供的异常更理想)。这是因为这种类型的异常是`nothrow copy-constructable`。具体来说，这意味着可以复制异常本身，而副本不会生成异常，例如由于`std::bad_alloc`。C++ 库提供的异常类型支持从`std::string()`开始构造，这可能会抛出`std::bad_alloc`。\n\n前面的 C++ 异常的问题是，对于您希望提供的每种类型的消息，您都需要`1`异常类型。实现安全异常类型的另一种方法是使用以下内容:\n\n```cpp\n#include <iostream>\n#include <stdexcept>\n\nclass the_answer : public std::exception\n{\n    const char *m_str;\npublic:\n\n    the_answer(const char *str):\n        m_str{str}\n    { }\n\n    const char *what() const noexcept\n    {\n        return m_str;\n    }\n};\n\nint main(void)\n{\n    try {\n        throw the_answer(\"42\");\n    }\n    catch (const std::exception &e) {\n        std::cout << \"The answer is: \" << e.what() << '\\n';\n    }\n}\n```\n\n在前面的例子中，我们存储了一个指向`const char*`的指针(即 C 风格的字符串)。c 风格的字符串在程序中作为常量全局存储。这种类型的异常满足前面所有相同的规则，并且在构建异常的过程中不会发生分配。还应该注意的是，由于字符串是全局存储的，这种类型的操作是安全的。\n\n使用这种方法可以创建许多类型的异常，包括除了字符串之外的可以通过自定义 getters 访问的东西(也就是说，不必使用`what()`函数)。但是，如果前面的这些规则对您来说不是问题，创建自定义 C++ 异常的最简单方法是简单地将现有的 C++ 异常子类化，如`std::runtime_error()`，如下例所示:\n\n```cpp\n#include <iostream>\n#include <stdexcept>\n#include <string.h>\n\nclass the_answer : public std::runtime_error\n{\npublic:\n    explicit the_answer(const char *str) :\n        std::runtime_error{str}\n    { }\n};\n\nint main(void)\n{\n    try {\n        throw the_answer(\"42\");\n    }\n    catch (const the_answer &e) {\n        std::cout << \"The answer is: \" << e.what() << '\\n';\n    }\n    catch (const std::exception &e) {\n        std::cout << \"unknown exception: \" << e.what() << '\\n';\n    }\n}\n```\n\n执行此示例时，我们会得到以下结果:\n\n![](img/a982b81c-4220-43df-82f3-73b32e54b2ae.png)\n\n在前面的例子中，我们通过子类化`std::runtime_error()`，只用几行代码就创建了自己的 C++ 异常。然后我们可以使用不同的`catch`块来计算抛出了什么类型的异常。请记住，如果您使用`std::string`版本的`std::runtime_error()`，您可能会在异常本身的构建过程中被抛出`std::bad_alloc`。**"
  },
  {
    "path": "docs/adv-cpp-prog-cb/03.md",
    "content": "# 三、实现移动语义\n\n在本章中，我们将学习一些高级的 C++ 移动语义。我们将首先讨论五大，这是一个习惯用法，只是鼓励程序员明确定义类的销毁和移动/复制语义。接下来，我们将学习如何定义移动构造函数和移动赋值运算符；移动语义的不同组合(包括仅移动和不可复制)；不可移动类；以及如何实现这些类以及它们为什么重要。\n\n本章还将讨论一些常见的陷阱，例如为什么`const &&`移动没有意义，以及如何克服 l 值和 r 值引用类型。本章中的方法很重要，因为一旦启用 C++ 11 或更高版本，就会启用移动语义，这将从根本上改变 C++ 在许多情况下处理类的方式。本章中的方法提供了用 C++ 编写高效代码的基础，这些代码的行为符合预期。\n\n本章中的配方如下:\n\n*   使用编译器生成的特殊类成员函数和五大\n*   让你的班级可移动\n*   仅移动类型\n*   实现`noexcept`移动构造器\n*   学会警惕`const &&`\n*   引用限定成员函数\n*   探索无法移动或复制的对象\n\n# 技术要求\n\n要编译和运行本章中的示例，您必须拥有运行 Ubuntu 18.04 的计算机的管理权限，并且具有功能性互联网连接。在运行这些示例之前，您必须安装以下内容:\n\n```cpp\n> sudo apt-get install build-essential git cmake \n```\n\n如果这安装在 Ubuntu 18.04 以外的任何操作系统上，则需要 GCC 7.4 或更高版本以及 CMake 3.6 或更高版本。\n\n# 使用编译器生成的特殊类成员函数和五大\n\n当使用 C++ 11 或更高版本时，如果您没有在类定义中显式提供某些函数，编译器将自动为您的 C++ 类生成这些函数。在本食谱中，我们将探索这是如何工作的，编译器将为您创建哪些函数，以及这如何影响您的程序的性能和有效性。总的来说，这个方法的目标是说明每个类至少应该定义五大类，以确保您的类明确您希望如何管理资源。\n\n# 准备好\n\n开始之前，请确保满足所有技术要求，包括安装 Ubuntu 18.04 或更高版本，并在终端窗口中运行以下内容:\n\n```cpp\n> sudo apt-get install build-essential git\n```\n\n这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后，打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。\n\n# 怎么做...\n\n您需要执行以下步骤来尝试此食谱:\n\n1.  从新的终端，运行以下命令下载源代码:\n\n```cpp\n> cd ~/\n> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git\n> cd Advanced-CPP-CookBook/chapter03\n```\n\n2.  要编译源代码，请运行以下命令:\n\n```cpp\n> cmake .\n> make recipe01_examples\n```\n\n3.  编译源代码后，您可以通过运行以下命令来执行该配方中的每个示例:\n\n```cpp\n> ./recipe01_example01\nThe answer is: 42\n\n> ./recipe01_example02\nThe answer is: 42\n\n> ./recipe01_example03\nThe answer is: 42\n\n> ./recipe01_example04\nThe answer is: 42\n```\n\n在下一节中，我们将逐一介绍这些示例，并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。\n\n# 它是如何工作的...\n\n在这个食谱中，我们将探索移动和复制之间的区别，以及这与五大函数的关系，五大函数是指所有类都应该明确定义的五个函数。首先，让我们看一个简单的例子，这个类在其构造函数中输出一个整数值:\n\n```cpp\nclass the_answer\n{\n    int m_answer{42};\n\npublic:\n\n    ~the_answer()\n    {\n        std::cout << \"The answer is: \" << m_answer << '\\n';\n    }\n};\n```\n\n在上例中，当类被析构时，类将输出到`stdout`。该类还有一个在构造时初始化的整数成员变量。前面例子的问题是隐式复制和移动语义被抑制了，因为我们定义了类的析构函数。\n\n五大函数是以下函数，如果至少定义了其中一个函数，每个类都应该定义这些函数(也就是说，如果定义了一个函数，就必须定义所有函数):\n\n```cpp\n~the_answer() = default;\n\nthe_answer(the_answer &&) noexcept = default;\nthe_answer &operator=(the_answer &&) noexcept = default;\n\nthe_answer(const the_answer &) = default;\nthe_answer &operator=(const the_answer &) = default;\n```\n\n如图所示，五大类包括析构函数、移动构造函数、移动赋值运算符、复制构造函数和复制赋值运算符。这些类的作者不需要实现这些功能，而是应该——至少——*定义*功能，明确说明删除、复制和移动应该如何进行(如果有的话)。这确保了如果定义了其中一个函数，类的其余移动、复制和销毁语义都是正确的，如本例所示:\n\n```cpp\nclass the_answer\n{\n    int m_answer{42};\n\npublic:\n\n    the_answer()\n    {\n        std::cout << \"The answer is: \" << m_answer << '\\n';\n    }\n\npublic:\n\n    virtual ~the_answer() = default;\n\n    the_answer(the_answer &&) noexcept = default;\n    the_answer &operator=(the_answer &&) noexcept = default;\n\n    the_answer(const the_answer &) = default;\n    the_answer &operator=(const the_answer &) = default;\n};\n```\n\n在前面的例子中，通过定义一个虚拟析构函数，这个类被标记为`virtual`(意味着这个类能够参与运行时多态)。不需要实现(通过将析构函数设置为`default`，但是定义本身是显式的，这告诉编译器我们希望类支持虚函数。这告诉该类的用户，指向该类的指针可用于删除从该类派生的任何类的实例。它还告诉用户继承将利用运行时多态性，而不是合成。这个类还声明复制和移动都是允许的。\n\n让我们看另一个例子:\n\n```cpp\nclass the_answer\n{\n    int m_answer{42};\n\npublic:\n\n    the_answer()\n    {\n        std::cout << \"The answer is: \" << m_answer << '\\n';\n    }\n\npublic:\n\n    ~the_answer() = default;\n\n    the_answer(the_answer &&) noexcept = default;\n    the_answer &operator=(the_answer &&) noexcept = default;\n\n    the_answer(const the_answer &) = delete;\n    the_answer &operator=(const the_answer &) = delete;\n};\n```\n\n在前面的示例中，拷贝被显式删除(这与定义移动构造函数而不定义拷贝语义相同)。这定义了一个只能移动的类，这意味着该类只能被移动；它不能被复制。标准库中这样的类的一个例子是`std::unique_ptr`。\n\n下一个类实现相反的情况:\n\n```cpp\nclass the_answer\n{\n    int m_answer{42};\n\npublic:\n\n    the_answer()\n    {\n        std::cout << \"The answer is: \" << m_answer << '\\n';\n    }\n\npublic:\n\n    ~the_answer() = default;\n\n    the_answer(the_answer &&) noexcept = delete;\n    the_answer &operator=(the_answer &&) noexcept = delete;\n\n    the_answer(const the_answer &) = default;\n    the_answer &operator=(const the_answer &) = default;\n};\n```\n\n在前面的例子中，我们已经明确定义了一个只复制类。\n\n五大有很多不同的组合。这个方法的要点是表明，显式定义这五个函数可以确保类的作者明确了解类本身的意图。这与它应该如何操作以及用户应该如何使用类有关。显式确保类的作者不打算使用一种行为，而是获得另一种行为，因为编译器将如何基于编译器的实现以及 C++ 规范是如何定义的来隐式构造类。\n\n# 让你的班级可移动\n\n在 C++ 11 或更高版本中，可以复制或移动对象，这可以用来指示如何管理对象的资源。拷贝和移动的最大区别很简单:拷贝创建一个对象管理的资源的拷贝，而移动将资源从一个对象转移到另一个对象。\n\n在这个食谱中，我们将解释如何使一个类可移动，包括如何正确添加移动构造函数和移动赋值操作符。我们还将解释可移动类的一些微妙细节，以及如何在代码中使用它们。这个方法很重要，因为在很多情况下，移动一个对象而不是复制一个对象可以提高性能并减少程序的内存消耗。然而，如果使用不当，可移动物体的使用可能会带来一些不稳定性。\n\n# 准备好\n\n开始之前，请确保满足所有技术要求，包括安装 Ubuntu 18.04 或更高版本，并在终端窗口中运行以下内容:\n\n```cpp\n> sudo apt-get install build-essential git\n```\n\n这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后，打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。\n\n# 怎么做...\n\n您需要执行以下步骤来尝试此食谱:\n\n1.  从新的终端，运行以下命令下载源代码:\n\n```cpp\n> cd ~/\n> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git\n> cd Advanced-CPP-CookBook/chapter03\n```\n\n2.  要编译源代码，请运行以下命令:\n\n```cpp\n> cmake .\n> make recipe02_examples\n```\n\n3.  编译源代码后，您可以通过运行以下命令来执行该配方中的每个示例:\n\n```cpp\n> ./recipe02_example01\nThe answer is: 42\n> ./recipe02_example02\nThe answer is: 42\nThe answer is: 42\n\nThe answer is: 42\n```\n\n在下一节中，我们将逐一介绍这些示例，并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。\n\n# 它是如何工作的...\n\n在这个食谱中，我们将学习如何使一个类可移动。首先，让我们检查一个基本的类定义:\n\n```cpp\n#include <iostream>\n\nclass the_answer\n{\n    int m_answer{42};\n\npublic:\n\n    the_answer() = default;\n\npublic:\n\n    ~the_answer()\n    {\n        std::cout << \"The answer is: \" << m_answer << '\\n';\n    }\n};\n\nint main(void)\n{\n    the_answer is;\n    return 0;\n}\n```\n\n在前面的例子中，我们创建了一个简单的类，它有一个初始化的私有整数成员。然后，我们定义一个默认构造函数和一个析构函数，当类的一个实例被销毁时，它们输出到`stdout`。默认情况下，这个类是可移动的，但是移动操作模仿拷贝(换句话说，在这个简单的例子中，移动和拷贝没有区别)。\n\n为了真正使这个类可移动，我们需要添加一个移动构造函数和一个移动赋值操作符，如下所示:\n\n```cpp\nthe_answer(the_answer &&other) noexcept;\nthe_answer &operator=(the_answer &&other) noexcept;\n```\n\n一旦我们添加了这两个函数，我们将能够使用以下内容将我们的类从一个实例移动到另一个实例:\n\n```cpp\ninstance2 = std::move(instance1);\n```\n\n为了支持这一点，在前面的类中，我们不仅将添加移动构造函数和赋值操作符，还将实现一个默认构造函数，为我们的示例类提供一个有效的移动状态，如下所示:\n\n```cpp\n#include <iostream>\n\nclass the_answer\n{\n    int m_answer{};\n\npublic:\n\n    the_answer() = default;\n\n    explicit the_answer(int answer) :\n        m_answer{answer}\n    { }\n```\n\n如图所示，该类现在有一个默认构造函数和一个接受整数参数的显式构造函数。默认构造函数初始化整数内存变量，该变量表示我们的移出或无效状态:\n\n```cpp\npublic:\n\n    ~the_answer()\n    {\n        if (m_answer != 0) {\n            std::cout << \"The answer is: \" << m_answer << '\\n';\n        }\n    }\n```\n\n如上例所示，当类被破坏时，我们输出整数成员变量的值，但是在这种情况下，我们首先检查以确保整数变量有效:\n\n```cpp\n    the_answer(the_answer &&other) noexcept\n    {\n        *this = std::move(other);\n    }\n\n    the_answer &operator=(the_answer &&other) noexcept\n    {\n        if (&other == this) {\n            return *this;\n        }\n\n        m_answer = std::exchange(other.m_answer, 0);        \n        return *this;\n    }\n\n    the_answer(const the_answer &) = default;\n    the_answer &operator=(const the_answer &) = default;\n};\n```\n\n最后，我们实现了移动构造函数和赋值操作符。移动构造函数只是调用移动赋值操作符，以避免重复(因为它们执行相同的操作)。移动分配操作符首先检查以确保我们没有移动到自己身上。这是因为这样做会导致损坏，因为用户会期望类仍然包含有效的整数，但实际上，内部整数会无意中被设置为`0`。\n\n然后我们交换整数值，并将原始值设置为`0`。这是因为，再一次，移动不是复制。移动会将值从一个实例转移到另一个实例。在这种情况下，被移动到的实例从`0`开始，并被赋予一个有效的整数，而被移动到的实例从一个有效的整数开始，并在移动后被设置为`0`，导致只有`1`实例包含一个有效的整数。\n\nIt should also be noted that we have to define the copy constructor and assignment operator. This is because, by default, if you provide a move constructor and assignment operator, C++ will automatically delete the copy constructor and assignment operator if they are not explicitly defined.\n\n在本例中，我们将比较移动和复制，因此我们定义了复制构造函数和赋值操作符，以确保它们不会被隐式删除。一般来说，最好的做法是为您定义的每个类定义析构函数、移动构造函数和赋值操作符，以及复制构造函数和赋值操作符。这确保了您编写的每个类的复制/移动语义都是显式的和有意的:\n\n```cpp\nint main(void)\n{\n    {\n        the_answer is;\n        the_answer is_42{42};\n        is = is_42;\n    }\n\n    std::cout << '\\n';\n\n    {\n        the_answer is{23};\n        the_answer is_42{42};\n        is = std::move(is_42);\n    }\n\n    return 0;\n}\n```\n\n当执行前面的代码时，我们会得到以下结果:\n\n![](img/bc7cc97b-8542-42e5-9ada-0634f0017fbc.png)\n\n在我们的主要功能中，我们运行两个不同的测试:\n\n*   第一个测试创建了我们类的两个实例，并将一个实例的内容复制到另一个实例中。\n*   第二个测试创建了我们类的两个实例，然后将一个实例的内容移动到另一个实例。\n\n当这个例子被执行时，我们看到第一个测试的输出被写入了两次。这是因为我们的类的第一个实例被赋予了我们的类的第二个实例的副本，该副本具有有效的整数值。第二个测试的输出只被写入一次，因为我们正在将一个实例的有效状态转移到另一个实例，导致在任何给定时刻只有一个实例具有有效状态。\n\n这里有一些值得一提的显著例子:\n\n*   移动构造函数和赋值操作符永远不应该抛出异常。具体来说，移动操作将一个类型实例的有效状态转移到该类型的另一个实例。这个操作在任何时候都不会失败，因为没有创建或销毁任何状态。它只是被转移了。此外，在移动过程中，有时很难撤销部分移动操作。由于这些原因，这些功能应始终标记为`noexcept`(参考[https://github . com/isocpp/cppcoregories/blob/master/cppcoregories . MD # Rc-move-no except](https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#Rc-move-noexcept))。\n*   移动构造函数和赋值操作符的函数签名中不包含`const`类型，因为被移动的实例不能是`const`，因为它的内部状态正在被转移，这隐含地假设正在发生写操作。更重要的是，如果您将移动构造函数或赋值操作符标记为`const`，则可能会出现副本。\n*   除非您打算创建副本，否则应改用移动，尤其是对于大型对象。就像传递`const T&`作为函数参数来防止复制发生一样，当调用函数时，当资源被移动到另一个变量而不是被复制时，应该使用移动来代替复制。\n*   编译器会在可能的情况下自动生成移动操作，而不是复制操作。例如，如果您在函数中创建一个对象，配置该对象，然后返回该对象，编译器将自动执行移动。\n\n现在，您已经知道了如何使您的类可移动，在下一个食谱中，我们将学习什么是只移动类型，以及为什么您可能想要在您的应用中使用它们。\n\n# 仅移动类型\n\n在这个食谱中，我们将学习如何使一个类只移动。复制和移动之间区别的一个很好的例子是`std::unique_ptr`和`std::shared_ptr`之间的区别。\n\n`std::unique_ptr`的要点是对动态分配的类型强制一个所有者，而`std::shared_ptr`则允许动态分配类型的多个所有者。两者都允许用户将指针类型的内容从一个实例化移动到另一个实例化，但是只有`std::shared_ptr`允许用户复制指针(因为复制指针会创建多个所有者)。\n\n在这个食谱中，我们将使用这两个类来展示如何创建一个只移动的类，并展示为什么这种类型的类在 C++ 中被如此频繁地使用(因为大多数时候我们希望移动而不是复制)。\n\n# 准备好\n\n开始之前，请确保满足所有技术要求，包括安装 Ubuntu 18.04 或更高版本，并在终端窗口中运行以下内容:\n\n```cpp\n> sudo apt-get install build-essential git\n```\n\n这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后，打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。\n\n# 怎么做...\n\n您需要执行以下步骤来尝试此食谱:\n\n1.  从新的终端，运行以下命令下载源代码:\n\n```cpp\n> cd ~/\n> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git\n> cd Advanced-CPP-CookBook/chapter03\n```\n\n2.  要编译源代码，请运行以下命令:\n\n```cpp\n> cmake .\n> make recipe03_examples\n```\n\n3.  编译源代码后，您可以通过运行以下命令来执行该配方中的每个示例:\n\n```cpp\n> ./recipe03_example01\nThe answer is: 42\n\n> ./recipe03_example03\ncount: 2\nThe answer is: 42\nThe answer is: 42\n\ncount: 1\nThe answer is: 42\n```\n\n在下一节中，我们将逐一介绍这些示例，并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。\n\n# 它是如何工作的...\n\n仅移动类是可以移动但不能复制的类。为了探索这种类型的类，让我们在下面的例子中包装`std::unique_ptr`，它本身是一个只移动的类:\n\n```cpp\nclass the_answer\n{\n    std::unique_ptr<int> m_answer;\n\npublic:\n\n    explicit the_answer(int answer) :\n        m_answer{std::make_unique<int>(answer)}\n    { }\n\n    ~the_answer()\n    {\n        if (m_answer) {\n            std::cout << \"The answer is: \" << *m_answer << '\\n';\n        }\n    }\n\npublic:\n\n    the_answer(the_answer &&other) noexcept\n    {\n        *this = std::move(other);\n    }\n\n    the_answer &operator=(the_answer &&other) noexcept\n    {\n        m_answer = std::move(other.m_answer);\n        return *this;\n    }\n};\n```\n\n前面的类将`std::unique_ptr`存储为成员变量，并在构造时用整数值实例化内存变量。销毁时，该类检查以确保`std::unique_ptr`有效，如果有效，则将该值输出到`stdout`。\n\n乍一看，我们可能想知道为什么我们必须检查有效性，因为`std::unique_ptr`总是被构造的。`std::unique_ptr`无效的原因是在移动过程中。因为我们正在创建一个只移动的类(而不是一个不可复制、不可移动的类)，我们实现了移动构造函数和移动赋值操作符，这将移动`std::unique_ptr`。`std::unique_ptr`在移动时，会将其内部指针的内容从一个类转移到另一个类，导致该类因存储无效指针(即`nullptr`)而被移动。换句话说，即使这个类不能被空构造，如果它被移动，它仍然可以存储`nullptr`，如下例所示:\n\n```cpp\nint main(void)\n{\n    the_answer is_42{42};\n    the_answer is = std::move(is_42);\n\n    return 0;\n}\n```\n\n如前例所示，只有一个类输出到`stdout`，因为只有一个实例有效。像`std::unique_ptr`一样，一个只移动的类确保你在被创建的资源总数和实际发生的实例总数之间总是有 1:1 的关系。\n\n需要注意的是，由于我们使用的是`std::unique_ptr`，所以不管我们喜不喜欢，我们的类都变成了只动类。例如，试图添加复制构造函数或复制赋值运算符来启用复制功能将导致编译错误:\n\n```cpp\nthe_answer(const the_answer &) = default;\nthe_answer &operator=(const the_answer &) = default;\n```\n\n换句话说，每个包含只移动类作为成员的类本身也变成了只移动类。虽然这看起来不可取，但你必须首先问自己:你真的需要一个类来复制吗？可能的答案是否定的。事实上，在大多数情况下，甚至在 C++ 11 之前，我们使用的大多数(如果不是全部)类都应该是只移动的。当一个类应该被移动时，它被复制的能力会导致资源浪费、损坏等等，这也是移动语义被添加到规范中的原因之一。移动语义允许我们定义我们希望如何处理我们分配的资源，并且它为我们提供了一种在编译时实施所需语义的方法。\n\n您可能想知道如何将前面的示例转换为允许复制。以下示例利用共享指针来实现这一点:\n\n```cpp\n#include <memory>\n#include <iostream>\n\nclass the_answer\n{\n    std::shared_ptr<int> m_answer;\n\npublic:\n\n    the_answer() = default;\n\n    explicit the_answer(int answer) :\n        m_answer{std::make_shared<int>(answer)}\n    { }\n\n    ~the_answer()\n    {\n        if (m_answer) {\n            std::cout << \"The answer is: \" << *m_answer << '\\n';\n        }\n    }\n\n    auto use_count()\n    { return m_answer.use_count(); }\n```\n\n前面的类用`std::shared_ptr`代替`std::unique_ptr`。在引擎盖下，`std::shared_ptr`会记录副本的数量，只有当副本总数为`0`时，才会删除它存储的指针。事实上，您可以使用`use_count()`功能查询总份数。\n\n接下来，我们定义移动构造函数、移动赋值运算符、复制构造函数和复制赋值运算符，如下所示:\n\n```cpp\npublic:\n\n    the_answer(the_answer &&other) noexcept\n    {\n        *this = std::move(other);\n    }\n\n    the_answer &operator=(the_answer &&other) noexcept\n    {\n        m_answer = std::move(other.m_answer);\n        return *this;\n    }\n\n    the_answer(const the_answer &other)\n    {\n        *this = other;\n    }\n\n    the_answer &operator=(const the_answer &other)\n    {\n        m_answer = other.m_answer;\n        return *this;\n    }\n};\n```\n\n这些定义也可以使用`=`默认语法编写，因为这些实现是相同的。最后，我们使用以下内容测试这个类:\n\n```cpp\nint main(void)\n{\n    {\n        the_answer is_42{42};\n        the_answer is = is_42;\n        std::cout << \"count: \" << is.use_count() << '\\n';\n    }\n\n    std::cout << '\\n';\n\n    {\n        the_answer is_42{42};\n        the_answer is = std::move(is_42);\n        std::cout << \"count: \" << is.use_count() << '\\n';\n    }\n\n    return 0;\n}\n```\n\n如果我们执行前面的代码，我们会得到以下结果:\n\n![](img/80128ca4-0b35-4b29-b649-c871a64b025f.png)\n\n在前面的测试中，我们首先创建一个类的副本，并输出副本总数，以查看实际上创建了两个副本。第二个测试执行`std::move()`而不是拷贝，这导致只按照预期创建了一个拷贝。\n\n# 实现 noexcept 移动构造函数\n\n在本食谱中，我们将学习如何确保移动构造函数和移动赋值运算符永远不会抛出异常。C++ 规范并不阻止移动构造函数抛出(因为已经确定这样的要求很难执行，因为即使在标准库中也存在太多合法的例子)。然而，在大多数情况下，确保不抛出异常应该是可能的。具体来说，移动通常不会创建资源，而是转移资源，因此，强异常保证应该是可能的。创建资源的移动的一个很好的例子是`std::list`，它必须提供一个有效的`end()`迭代器，即使是在移动中。\n\n# 准备好\n\n开始之前，请确保满足所有技术要求，包括安装 Ubuntu 18.04 或更高版本，并在终端窗口中运行以下内容:\n\n```cpp\n> sudo apt-get install build-essential git\n```\n\n这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后，打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。\n\n# 怎么做...\n\n您需要执行以下步骤来尝试此食谱:\n\n1.  从新的终端，运行以下命令下载源代码:\n\n```cpp\n> cd ~/\n> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git\n> cd Advanced-CPP-CookBook/chapter03\n```\n\n2.  要编译源代码，请运行以下命令:\n\n```cpp\n> cmake .\n> make recipe04_examples\n```\n\n3.  编译源代码后，您可以通过运行以下命令来执行该配方中的每个示例:\n\n```cpp\n> ./recipe04_example01\nfailed to move\n\nThe answer is: 42\n```\n\n在下一节中，我们将逐一介绍这些示例，并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。\n\n# 它是如何工作的...\n\n如前所述，移动不应该抛出异常，以确保强异常保证(也就是说，移动对象的行为不会损坏对象)，在大多数情况下，这是可能的，因为移动(不像复制)不会创建资源，而是转移资源。确保移动构造函数和移动赋值操作符不抛出的最佳方法是仅使用`std::move()`转移成员变量，如下例所示:\n\n```cpp\nm_answer = std::move(other.m_answer);\n```\n\n假设您正在移动的成员变量没有抛出，那么您的类也不会抛出。使用这个简单的技术将确保您的移动构造函数和操作符永远不会抛出。但是如果这个操作不能用呢？让我们用下面的例子来探讨这个问题:\n\n```cpp\n#include <vector>\n#include <iostream>\n\nclass the_answer\n{\n    std::vector<int> m_answer;\n\npublic:\n\n    the_answer() = default;\n\n    explicit the_answer(int answer) :\n        m_answer{{answer}}\n    { }\n\n    ~the_answer()\n    {\n        if (!m_answer.empty()) {\n            std::cout << \"The answer is: \" << m_answer.at(0) << '\\n';\n        }\n    }\n```\n\n在前面的例子中，我们创建了一个以向量为成员变量的类。默认情况下，向量可以初始化为空，也可以用单个元素初始化。销毁时，如果向量有值，我们将值输出到`stdout`。我们实现`move`构造函数和运算符如下:\n\n```cpp\npublic:\n\n    the_answer(the_answer &&other) noexcept\n    {\n        *this = std::move(other);\n    }\n\n    the_answer &operator=(the_answer &&other) noexcept\n    {\n        if (&other == this) {\n            return *this;\n        }\n\n        try {\n            m_answer.emplace(m_answer.begin(), other.m_answer.at(0));\n            other.m_answer.erase(other.m_answer.begin());\n        }\n        catch(...) {\n            std::cout << \"failed to move\\n\";\n        }\n\n        return *this;\n    }\n};\n```\n\n如图所示，move 操作符将单个元素从一个实例转移到另一个实例(这不是实现移动的最佳方式，但是这个实现可以演示这一点，而不会过于复杂)。如果向量为空，此操作将抛出，如下例所示:\n\n```cpp\nint main(void)\n{\n    {\n        the_answer is_42{};\n        the_answer is_what{};\n\n        is_what = std::move(is_42);\n    }\n\n    std::cout << '\\n';\n\n    {\n        the_answer is_42{42};\n        the_answer is_what{};\n\n        is_what = std::move(is_42);\n    }\n\n    return 0;\n}\n```\n\n最后，我们尝试在两个不同的测试中移动这个类的一个实例。在第一个测试中，两个实例都是默认构造的，这导致空类，而第二个测试用单个元素构造向量，这导致有效的移动。在这种情况下，我们能够防止移动被抛出，但是应该注意的是，结果类实际上并没有执行移动，导致两个对象都不包含所需的状态。这就是为什么移动构造函数永远不应该抛出。即使我们没有捕捉到异常，在抛出发生后断言程序的状态也是极其困难的。搬家发生了吗？每个实例处于什么状态？在大多数情况下，这种类型的错误会导致在程序进入损坏状态时调用`std::terminate()`。\n\n副本是不同的，因为原始类保持不变。复制是无效的，程序员可以很好地处理这种情况，因为被复制的实例的原始状态不受影响(因此我们将其标记为`const`)。\n\n但是，由于被移动的实例是可写的，两个实例都处于损坏状态，并且没有好的方法知道如何处理向前移动的程序，因为我们不知道原始实例是否处于可以正确处理的状态。\n\n# 学会警惕 const&&\n\n在本食谱中，我们将学习为什么移动构造函数或运算符永远不应该标记为`const`(为什么复制构造函数/运算符总是标记为`const`)。这一点很重要，因为它触及到了移动和复制之间区别的核心。C++ 中的 Move 语义是它最强大的特性之一，理解它为什么如此重要以及它实际在做什么对于编写好的 C++ 代码至关重要。\n\n# 准备好\n\n开始之前，请确保满足所有技术要求，包括安装 Ubuntu 18.04 或更高版本，并在终端窗口中运行以下内容:\n\n```cpp\n> sudo apt-get install build-essential git\n```\n\n这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后，打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。\n\n# 怎么做...\n\n您需要执行以下步骤来尝试此食谱:\n\n1.  从新的终端，运行以下命令下载源代码:\n\n```cpp\n> cd ~/\n> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git\n> cd Advanced-CPP-CookBook/chapter03\n```\n\n2.  要编译源代码，请运行以下命令:\n\n```cpp\n> cmake .\n> make recipe05_examples\n```\n\n3.  编译源代码后，您可以通过运行以下命令来执行该配方中的每个示例:\n\n```cpp\n> ./recipe05_example01\ncopy\n```\n\n在下一节中，我们将逐一介绍这些示例，并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。\n\n# 它是如何工作的...\n\n在本食谱中，我们将了解为什么`const&&`构造函数或运算符没有意义，并将导致意想不到的行为。移动会转移资源，这就是为什么它被标记为非`const`。这是因为传输假设两个实例都被写入(一个实例接收资源，而另一个实例取走资源)。副本创建资源，这就是为什么它们不总是被标记为`noexcept`(创建资源绝对可以抛出)并且它们被标记为`const`(因为原始实例是被复制的，而不是被修改的)。一个`const&&`构造函数声称是一个不转移的移动，它必须是一个副本(如果你没有写入原始实例，你没有移动——你在复制)，如本例所示:\n\n```cpp\n#include <iostream>\n\nclass copy_or_move\n{\npublic:\n\n    copy_or_move() = default;\n\npublic:\n\n    copy_or_move(copy_or_move &&other) noexcept\n    {\n        *this = std::move(other);\n    }\n\n    copy_or_move &operator=(copy_or_move &&other) noexcept\n    {\n        std::cout << \"move\\n\";\n        return *this;\n    }\n\n    copy_or_move(const copy_or_move &other)\n    {\n        *this = other;\n    }\n\n    copy_or_move &operator=(const copy_or_move &other)\n    {\n        std::cout << \"copy\\n\";\n        return *this;\n    }\n};\n\nint main(void)\n{\n    const copy_or_move test1;\n    copy_or_move test2;\n\n    test2 = std::move(test1);\n    return 0;\n}\n```\n\n在前面的示例中，我们创建了一个实现默认移动和复制构造函数/运算符的类。唯一不同的是，我们将输出添加到`stdout`来告诉我们是正在执行拷贝还是正在执行移动。\n\n然后，我们创建了两个类实例，实例被从标记为`const`的位置移走。然后我们执行移动，输出的是一个副本。这是因为即使我们要求移动，编译器也使用了副本。我们可以实现一个`const &&`移动构造函数/操作符，但是没有办法将移动写成移动，因为我们将被移动的对象标记为`const`，所以我们不能获取它的资源。事实上，这样的移动将被实现为一个副本，与编译器自动为我们做的一样。\n\n在下一个配方中，我们将学习如何向成员函数添加限定符。\n\n# 引用限定成员函数\n\n在这个食谱中，我们将了解什么是引用限定成员函数。虽然 C++ 语言的这一方面较少被使用和理解，但它很重要，因为它为程序员提供了处理资源如何操作的能力，这取决于调用函数时类是处于 l 值还是 r 值状态。\n\n# 准备好\n\n开始之前，请确保满足所有技术要求，包括安装 Ubuntu 18.04 或更高版本，并在终端窗口中运行以下内容:\n\n```cpp\n> sudo apt-get install build-essential git\n```\n\n这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后，打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。\n\n# 怎么做...\n\n您需要执行以下步骤来尝试此食谱:\n\n1.  从新的终端，运行以下命令下载源代码:\n\n```cpp\n> cd ~/\n> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git\n> cd Advanced-CPP-CookBook/chapter03\n```\n\n2.  要编译源代码，请运行以下命令:\n\n```cpp\n> cmake .\n> make recipe06_examples\n```\n\n3.  编译源代码后，您可以通过运行以下命令来执行该配方中的每个示例:\n\n```cpp\n> ./recipe06_example01\nthe answer is: 42\nthe answer is not: 0\nthe answer is not: 0\n```\n\n在下一节中，我们将逐一介绍这些示例，并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。\n\n# 它是如何工作的...\n\n在本例中，我们将了解什么是引用限定成员函数。为了解释什么是引用限定成员函数，让我们看下面的例子:\n\n```cpp\n#include <iostream>\n\nclass the_answer\n{\npublic:\n\n ~the_answer() = default;\n\n void foo() &\n {\n std::cout << \"the answer is: 42\\n\";\n }\n\n void foo() &&\n {\n std::cout << \"the answer is not: 0\\n\";\n }\n\npublic:\n\n the_answer(the_answer &&other) noexcept = default;\n the_answer &operator=(the_answer &&other) noexcept = default;\n\n the_answer(const the_answer &other) = default;\n the_answer &operator=(const the_answer &other) = default;\n};\n```\n\n在这个例子中，我们实现了一个`foo()`函数，但是我们有两个不同的版本。第一个版本结尾有`&`，第二个版本结尾有`&&`。执行哪个`foo()`函数取决于实例是 l 值还是 r 值，如下例所示:\n\n```cpp\nint main(void)\n{\n    the_answer is;\n\n    is.foo();\n    std::move(is).foo();\n    the_answer{}.foo();\n}\n```\n\n执行时会产生以下结果:\n\n![](img/19571c4b-ebb1-4680-a183-82571ec2416c.png)\n\n如前例所示，`foo()`的第一次执行是一个 l 值，因为执行的是`foo()`的 l 值版本(也就是最后有`&`的函数)。`foo()`的最后两次执行是 r 值，因为执行的是`foo()`的 r 值版本。\n\n引用限定的成员函数可用于确保该函数仅在正确的上下文中调用。使用这些类型的函数的另一个原因是确保只有当 l 值或 r 值引用存在时才调用该函数。\n\n例如，您可能不希望将`foo()`作为 r 值调用，因为这种类型的调用不能确保类的实例在调用本身之外有一个生存期，如前面的示例所示。\n\n在下一个食谱中，我们将学习如何制作一个既不能移动也不能复制的类，并解释为什么你可能会做这样的事情。\n\n# 探索无法移动或复制的对象\n\n在本食谱中，我们将学习如何创建一个我们不能移动或复制的对象，以及为什么您可能想要创建这样一个类。复制类需要复制类的内容，这在某些情况下是不可能的(例如，复制内存池并不简单)。移动一个类假设该类被允许以一种潜在的无效状态存在(例如，`std::unique_ptr`在移动时采用一个`nullptr`值，该值是无效的)。这种情况也可能是不可取的(你现在必须检查有效性)。我们无法复制的不可移动类可以克服这些类型的问题。\n\n# 准备好\n\n开始之前，请确保满足所有技术要求，包括安装 Ubuntu 18.04 或更高版本，并在终端窗口中运行以下内容:\n\n```cpp\n> sudo apt-get install build-essential git\n```\n\n这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后，打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。\n\n# 怎么做...\n\n您需要执行以下步骤来尝试此食谱:\n\n1.  从新的终端，运行以下命令下载源代码:\n\n```cpp\n> cd ~/\n> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git\n> cd Advanced-CPP-CookBook/chapter03\n```\n\n2.  要编译源代码，请运行以下命令:\n\n```cpp\n> cmake .\n> make recipe07_examples\n```\n\n3.  编译源代码后，您可以通过运行以下命令来执行该配方中的每个示例:\n\n```cpp\n> ./recipe07_example01\nThe answer is: 42\nSegmentation fault (core dumped)\n```\n\n在下一节中，我们将逐一介绍这些示例，并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。\n\n# 它是如何工作的...\n\n仅移动类防止类被复制，这在某些情况下可以提高性能。仅移动类还确保了创建的资源与分配的资源之间的 1:1 关系，因为副本不存在。但是，移动类可能会导致类无效，如本例所示:\n\n```cpp\n#include <iostream>\n\nclass the_answer\n{\n    std::unique_ptr<int> m_answer;\n\npublic:\n\n    explicit the_answer(int answer) :\n        m_answer{std::make_unique<int>(answer)}\n    { }\n\n    ~the_answer()\n    {\n        std::cout << \"The answer is: \" << *m_answer << '\\n';\n    }\n\npublic:\n\n    the_answer(the_answer &&other) noexcept = default;\n    the_answer &operator=(the_answer &&other) noexcept = default;\n};\n\nint main(void)\n{\n    the_answer is_42{42};\n    the_answer is_what{42};\n\n    is_what = std::move(is_42);\n    return 0;\n}\n```\n\n如果我们运行前面的代码，我们会得到以下结果:\n\n![](img/f6a0a4c9-5084-4fae-8a30-69fb5fff3ce5.png)\n\n在前面的例子中，我们创建了一个可以移动的类，它存储`std::unique_ptr`。在类的析构函数中，我们取消引用该类并输出它的值。我们不检查`std::unique_ptr`的有效性，因为我们编写了一个强制有效`std::unique_ptr`的构造函数，却忘记了一个动作可以撤销这个显式的有效性。结果是，当执行一个移动时，我们得到一个分割错误。\n\n为了克服这一点，我们需要提醒一下，我们做出了如下假设:\n\n```cpp\nclass the_answer\n{\n std::unique_ptr<int> m_answer;\n\npublic:\n\n explicit the_answer(int answer) :\n m_answer{std::make_unique<int>(answer)}\n { }\n\n ~the_answer()\n {\n std::cout << \"The answer is: \" << *m_answer << '\\n';\n }\n\npublic:\n\n the_answer(the_answer &&other) noexcept = delete;\n the_answer &operator=(the_answer &&other) noexcept = delete;\n\n the_answer(const the_answer &other) = delete;\n the_answer &operator=(const the_answer &other) = delete;\n};\n```\n\n前面的类显式删除了复制和移动操作，这是我们想要的意图。现在，如果我们不小心移动了这个类，我们会得到以下结果:\n\n```cpp\n/home/user/book/chapter03/recipe07.cpp: In function ‘int main()’:\n/home/user/book/chapter03/recipe07.cpp:106:30: error: use of deleted function ‘the_answer& the_answer::operator=(the_answer&&)’\nis_what = std::move(is_42);\n^\n/home/user/book/chapter03/recipe07.cpp:95:17: note: declared here\nthe_answer &operator=(the_answer &&other) noexcept = delete;\n^~~~~~~~\n```\n\n这个错误告诉我们，假设类是有效的，因此不支持移动。我们要么需要适当的支持移动(这意味着我们必须保持对无效`std::unique_ptr`的支持)，要么我们需要移除`move`操作。如图所示，一个不能被移动或复制的类可以确保我们的代码按预期工作，为编译器提供了一种机制，当我们用我们的类做一些我们不打算做的事情时，它会警告我们。"
  },
  {
    "path": "docs/adv-cpp-prog-cb/04.md",
    "content": "# 四、将模板用于泛型编程\n\n在本章中，我们将学习高级模板编程技术。这些技术包括根据提供的类型更改模板类实现的能力，如何处理不同类型的参数，包括如何正确转发它们，如何在运行时和编译时优化代码，以及如何使用 C++ 17 中添加的一些新功能。这一点很重要，因为它可以更好地理解模板编程是如何工作的，以及如何确保模板按照您期望的方式运行。\n\n很多时候，我们编写模板代码时假设它是以一种方式执行的，而实际上，它是以另一种方式执行的，要么生成不可靠的代码，要么生成有意外性能损失的代码，要么两者都有。本章将解释如何避免这些问题，并为编写合适的通用程序提供基础。\n\n本章中的配方如下:\n\n*   实施 SFINAE\n*   学习完美转发\n*   使用`if constexpr`\n*   使用元组处理参数包\n*   使用特征来改变模板实现的行为\n*   学习如何实施`template<auto>`\n*   使用显式模板声明\n\n# 技术要求\n\n要编译和运行本章中的示例，您必须拥有运行 Ubuntu 18.04 的计算机的管理权限，并且具有功能性互联网连接。在运行这些示例之前，请安装以下内容:\n\n```cpp\n> sudo apt-get install build-essential git cmake\n```\n\n如果这安装在 Ubuntu 18.04 以外的任何操作系统上，则需要 GCC 7.4 或更高版本以及 CMake 3.6 或更高版本。\n\n# 实施 SFINAE\n\n在本食谱中，我们将学习如何使用**替代失败不是错误** ( **SFINAE** )。这个方法很重要，因为通常我们创建模板时并没有确保传递给模板的类型是我们所期望的。这可能导致意想不到的行为、次优的性能，甚至是错误的、不可靠的代码。\n\nSFINAE 允许我们明确在我们的模板中期望什么类型。它还为我们提供了一种方法，可以根据提供的类型来改变模板的行为。对于一些人来说，SFINAE 的问题是这个概念很难理解。我们在这个食谱中的目标是揭开 SFINAE 的神秘面纱，并展示如何在自己的代码中使用它。\n\n# 准备好\n\n开始之前，请确保满足所有技术要求，包括安装 Ubuntu 18.04 或更高版本，并在终端窗口中运行以下内容:\n\n```cpp\n> sudo apt-get install build-essential git cmake\n```\n\n这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后，打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。\n\n# 怎么做...\n\n您需要执行以下步骤来尝试此食谱:\n\n1.  从新的终端，运行以下命令下载源代码:\n\n```cpp\n> cd ~/\n> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git\n> cd Advanced-CPP-CookBook/chapter04\n```\n\n2.  要编译源代码，请运行以下命令:\n\n```cpp\n> cmake .\n> make recipe01_examples\n```\n\n3.  编译源代码后，您可以通过运行以下命令来执行该配方中的每个示例:\n\n```cpp\n> ./recipe01_example01\nThe answer is: 23\nThe answer is: 42\n\n> ./recipe01_example02\nThe answer is: 42\n\n> ./recipe01_example03\nThe answer is: 42\n\n> ./recipe01_example04\nThe answer is: 42\n\n> ./recipe01_example05\nThe answer is: 42\nThe answer is: 42\nThe answer is: 42.12345678\n```\n\n在下一节中，我们将逐一介绍这些示例，并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。\n\n# 它是如何工作的...\n\n在这个食谱中，你将学习如何在你自己的代码中加入 SFINAE。首先，我们必须先了解什么是 SFINAE，标准库如何使用它来实现`type`特性。不知道`type`特性是如何实现的，就很难理解如何使用它们。\n\n首先，用 SFINAE 最重要的是要理解它的名字是怎么说的，那就是一个*的换人失败并不是一个错误*。这意味着当模板类型被替换时，如果发生故障，编译器将*而不是*产生错误。例如，我们可以编写以下内容:\n\n```cpp\n#include <iostream>\n\nstruct the_answer\n{\n    using type = unsigned;\n};\n\ntemplate<typename T>\nvoid foo(typename T::type t)\n{\n    std::cout << \"The answer is not: \" << t << '\\n';\n}\n\ntemplate<typename T>\nvoid foo(T t)\n{\n    std::cout << \"The answer is: \" << t << '\\n';\n}\n\nint main(void)\n{\n    foo<the_answer>(23);\n    foo<int>(42);\n\n    return 0;\n}\n```\n\n这里描述了其中每一个的输出:\n\n```cpp\nThe answer is: 23\nThe answer is: 42\n```\n\n在这个例子中，我们已经创建了两个版本的`foo()`函数。第一个版本采用了一个具有 T2 别名的类型，我们用它来创建函数的参数。第二个版本只是取`T`类型本身。然后我们使用两个版本的`foo()`函数，一个带有整数，另一个带有定义`type`别名的结构。\n\n前面例子的要点是，当我们调用`foo()`函数的`foo<int>()`版本时，当编译器试图将`int`类型与采用带有`type`别名的类型的`foo()`函数的版本相匹配时，它不会产生错误。这就是 SFINAE。它只是说，当编译器尝试采用给定的类型并将其与模板匹配时，如果出现故障，编译器将不会生成错误。唯一会发生错误的情况是编译器找不到合适的替代。比如我们评论出第二版`foo()`会怎么样？让我们看看:\n\n![](img/84d28ad2-c0bd-49a0-879d-ad42f5add912.png)\n\n从前面的错误输出可以看出，编译器甚至说该错误是替换错误。根据提供的类型，我们提供的模板不是有效的候选模板。\n\n这个例子的另一个重要收获是，编译器能够根据提供的类型在我们的`foo()`函数的两个不同版本之间进行选择。我们可以利用这一点。具体来说，这使我们能够根据所提供的类型做不同的事情。我们所需要的是一种方法来编写我们的`foo()`函数，这样我们就可以根据我们提供的类型来启用/禁用不同版本的模板。\n\n这就是`std::enable_if`发挥作用的地方。`std::enable_if`将 SFINAE 的思想带到下一步，允许我们定义一个类型，如果它的参数为真。否则，它将生成替换错误，故意迫使编译器选择模板的不同版本。`std::enable_if`定义如下:\n\n```cpp\ntemplate<bool B, class T = void>\nstruct enable_if {};\n\ntemplate<class T>\nstruct enable_if<true, T> { typedef T type; };\n```\n\n这首先定义了一个采用`bool B`的结构和一个默认为`void`的`T`类型。当`bool`为真时，它定义了这种`struct`类型的特殊化。具体来说，当`bool`值为`true`时，返回所提供的类型，如前所述，默认为`void`。要了解这是如何使用的，让我们看一个例子:\n\n```cpp\n#include <iostream>\n#include <type_traits>\n\ntemplate<typename T>\nconstexpr auto is_int()\n{ \n    return false; \n}\n\ntemplate<>\nconstexpr auto is_int<int>()\n{ \n    return true; \n}\n\ntemplate<\n    typename T,\n    std::enable_if_t<is_int<T>(), int> = 0\n    >\nvoid the_answer(T is)\n{\n    std::cout << \"The answer is: \" << is << '\\n';\n}\n\nint main(void)\n{\n    the_answer(42);\n    return 0;\n}\n```\n\n输出如下:\n\n![](img/395bc1d6-02a2-4609-be80-8855f53d6acc.png)\n\n在这个例子中，我们创建了一个名为`is_int()`的函数，它总是返回`false`。然后我们为返回`true`的`int`创建这个函数的模板专门化。接下来，我们创建一个接受任何类型的函数，但是我们将`std::enable_if_t`(添加的`_t`部分是 C++ 17 中为`::type`添加的简写)添加到使用我们的`is_int()`函数的模板定义中。如果提供的`T`类型是`int`，我们的`is_int()`功能将返回`true`。\n\n`std::enable_if`默认不做任何事情。但是，如果是`true`，它会返回一个`type`别名，在前面的示例中，它是我们作为`std::enable_if`的第二个参数传递的`int`类型。这是说如果`std::enable_if`是`true`，它会返回一个`int`类型。然后我们将这个`int`类型设置为`0`，这是一个有效的做法。这不会导致失败；我们的模板函数成为一个有效的替代，因此被使用。综上所述，如果`T`是`int`类型，`std::enable_if`变成了`int`类型本身，然后我们设置为`0`，编译没有问题。如果我们的`T`型不是`int`，那么`std::enable_if`就变成了虚无。试图将 nothing 设置为`0`会导致编译错误，但由于这是 SFINAE，编译器错误只会变成替换错误。\n\n让我们看看错误案例。如果我们将`42`设置为`42.0`，这是一个`double`，而不是`int`，我们会得到以下结果:\n\n![](img/3eadc479-fa00-4c25-9a68-b82cb13ee914.png)\n\n从前面的错误可以看出，编译器是在说`enable_if`中没有名为`type`的类型。如果看`std::enable_if`的定义，这是意料之中的，因为`std::enable_if`如果是假的就什么都不做。它只创建一个名为`type`的类型，如果它是真的。\n\n为了更好地理解这是如何工作的，让我们看另一个例子:\n\n```cpp\n#include <iostream>\n#include <type_traits>\n\ntemplate<\n    typename T,\n    std::enable_if_t<std::is_integral_v<T>>* = nullptr\n    >\nvoid the_answer(T is)\n{\n    std::cout << \"The answer is: \" << is << '\\n';\n}\n\nint main(void)\n{\n    the_answer(42);\n    return 0;\n}\n```\n\n输出如下:\n\n![](img/cb55d047-89b4-4e40-815a-273456762831.png)\n\n在前面的例子中，我们使用了`std::is_integral_v`，它和我们的`is_int()`函数做同样的事情，不同的是它是由标准库提供的，可以处理 CV 类型。事实上，标准库有这些函数的不同版本的大量列表，包括不同的类型、继承属性、CV 属性等等。如果您需要检查任何类型的`type`属性，标准库有一个您可以使用的`std:is_xxx`功能。\n\n前面的例子与我们前面的例子几乎相同，不同之处在于我们在`std::enable_if`方法中不返回`int`。相反，我们使用`* = nullptr`。这是因为`std::enable_if`默认返回`void`。`*`字符将这个空位变成一个空位指针，然后我们将其设置为`nullptr`。\n\n在下一个示例中，我们展示了这方面的另一个变化:\n\n```cpp\n#include <iostream>\n#include <type_traits>\n\ntemplate<typename T>\nstd::enable_if_t<std::is_integral_v<T>>\nthe_answer(T is)\n{\n    std::cout << \"The answer is: \" << is << '\\n';\n}\n\nint main(void)\n{\n    the_answer(42);\n    return 0;\n}\n\n```\n\n输出如下:\n\n![](img/5ca6a189-e687-45a2-a8cd-422d3e2f274e.png)\n\n在这个例子中，我们函数的`void`是由`std::enable_if`创建的。如果`T`不是整数，则不返回`void`，我们会看到这个错误(而不是代码编译并允许我们首先执行它):\n\n![](img/d7ec86d0-edec-409d-8dd5-76b3abfa3978.png)\n\n总之，`std::enable_if`将创建一个名为`type`的类型，它基于您提供的类型。默认情况下，这里是`void`，但是你可以输入任何你想要的类型。此功能不仅可用于为我们的模板强制一种类型，还可用于根据我们提供的类型定义不同的函数，如本例所示:\n\n```cpp\n#include <iostream>\n#include <type_traits>\n#include <iomanip>\n\ntemplate<\n    typename T,\n    std::enable_if_t<std::is_integral_v<T>>* = nullptr\n    >\nvoid the_answer(T is)\n{\n    std::cout << \"The answer is: \" << is << '\\n';\n}\n\ntemplate<\n    typename T,\n    std::enable_if_t<std::is_floating_point_v<T>>* = nullptr\n    >\nvoid the_answer(T is)\n{\n    std::cout << std::setprecision(10);\n    std::cout << \"The answer is: \" << is << '\\n';\n}\n\nint main(void)\n{\n    the_answer(42);\n    the_answer(42U);\n    the_answer(42.12345678);\n\n    return 0;\n}\n\n```\n\n前面代码的输出如下:\n\n![](img/1c12f216-9fc9-4b34-9868-9ccf45ae4fb7.png)\n\n就像我们在这个食谱中的第一个例子一样，我们已经创建了同一个函数的两个不同版本。SFINAE 允许编译器根据提供的类型选择最合适的版本。\n\n# 学习完美转发\n\n在这个食谱中，我们将学习如何使用完美转发。这个方法很重要，因为在编写模板时，我们通常会将模板参数传递给其他函数。如果我们不使用完美转发，我们可能会无意中将 r 值引用转换为 l 值引用，导致发生潜在的复制而不是移动，在某些情况下，这可能是次优的。完美转发还为编译器提供了一些提示，可以利用这些提示来改进函数的内联和展开。\n\n# 准备好\n\n开始之前，请确保满足所有技术要求，包括安装 Ubuntu 18.04 或更高版本，并在终端窗口中运行以下内容:\n\n```cpp\n> sudo apt-get install build-essential git cmake\n```\n\n这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后，打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。\n\n# 怎么做...\n\n您需要执行以下步骤来尝试此食谱:\n\n1.  从新的终端，运行以下命令下载源代码:\n\n```cpp\n> cd ~/\n> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git\n> cd Advanced-CPP-CookBook/chapter04\n```\n\n2.  要编译源代码，请运行以下命令:\n\n```cpp\n> cmake .\n> make recipe02_examples\n```\n\n3.  编译源代码后，您可以通过运行以下命令来执行该配方中的每个示例:\n\n```cpp\n> ./recipe02_example01\nl-value\nl-value\n\n> ./recipe02_example02\nl-value\nr-value\n\n> ./recipe02_example03\nl-value: 42\nr-value: 42\n```\n\n在下一节中，我们将逐一介绍这些示例，并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。\n\n# 它是如何工作的...\n\n在这个食谱中，我们将学习如何使用完美的转发，以确保当我们在模板中传递参数时(也就是说，转发我们的参数)，我们这样做的方式不会抹去 r 值。为了更好地理解这个问题，让我们看看下面的例子:\n\n```cpp\n#include <iostream>\n\nstruct the_answer\n{ };\n\nvoid foo2(const the_answer &is)\n{\n    std::cout << \"l-value\\n\";\n}\n\nvoid foo2(the_answer &&is)\n{\n    std::cout << \"r-value\\n\";\n}\n\ntemplate<typename T>\nvoid foo1(T &&t)\n{\n    foo2(t);\n}\n\nint main(void)\n{\n    the_answer is;\n    foo1(is);\n    foo1(the_answer());\n\n    return 0;\n}\n\n```\n\n输出如下:\n\n![](img/eb4f4cb0-e924-4b6e-af1a-617c8e3183b0.png)\n\n在前面的例子中，我们有两个不同版本的`foo()`函数:一个采用 l 值引用，一个采用 r 值引用。然后我们从一个模板函数中调用`foo()`。该模板函数采用转发引用(也称为通用引用)，它是与`auto`或模板函数配对的 r 值引用。最后，从我们的主函数，我们调用我们的模板，看看调用了哪个`foo()`函数。我们第一次调用模板时，会传入一个 l 值。因为我们被赋予了一个 l 值，通用引用变成了 l 值，我们的`foo()`函数的 l 值版本被调用。问题是，我们第二次调用模板函数时，我们给了它一个 r 值，但它调用了我们的`foo()`函数的 l 值版本，尽管它被赋予了 r 值。\n\n这里常见的错误是，即使模板函数采用通用引用，并且我们有一个版本的`foo()`函数也采用 r 值，我们假设这个`foo()`函数将被调用。斯科特·迈耶斯在他的许多关于普遍参考的讲座中很好地解释了这一点。问题是，当你使用一个通用参考时，它变成了一个 l 值。传递`names`参数的行为，这意味着它必须是一个 l 值。它强制编译器转换为 l 值，因为它看到您在使用它，即使您所做的只是传递参数。应该注意的是，我们的例子并不使用优化进行编译，因为如果编译器可以安全地确定变量没有被使用，它可以自由地优化 l 值。\n\n为了防止这个问题，我们需要告诉编译器我们希望转发该参数。通常，我们会用`std::move()`来表示这个。问题是，如果最初给我们一个 l 值，我们就不能使用`std::move()`，因为那样会将 l 值转换成 r 值。这就是标准库有`std::forward()`的原因，它是使用以下内容实现的:\n\n```cpp\nstatic_cast<T&&>(t)\n```\n\n`std::forward()`所做的只是将参数转换回其原始参考类型。这告诉编译器，如果该参数最初是 r 值，则将其显式视为 r 值，如下例所示:\n\n```cpp\n#include <iostream>\n\nstruct the_answer\n{ };\n\nvoid foo2(const the_answer &is)\n{\n    std::cout << \"l-value\\n\";\n}\n\nvoid foo2(the_answer &&is)\n{\n    std::cout << \"r-value\\n\";\n}\n\ntemplate<typename T>\nvoid foo1(T &&t)\n{\n    foo2(std::forward<T>(t));\n}\n\nint main(void)\n{\n    the_answer is;\n    foo1(is);\n    foo1(the_answer());\n\n    return 0;\n}\n\n```\n\n输出如下:\n\n![](img/c64d9b68-b5d8-4ce2-ba02-17195ee8906d.png)\n\n前面的例子与第一个例子相同，唯一的区别是我们使用`std::forward()`在模板函数中传递参数。这一次，当我们用 r 值调用我们的模板函数时，它调用我们的`foo()`函数的 r 值版本。这叫**完美转发**。它确保我们在传递参数时保持 CV 属性和 l-/r 值属性。需要注意的是，完美转发只有在使用模板函数或`auto`时才有效。这意味着完美转发通常只在编写包装器时有用。标准库包装器的一个很好的例子是`std::make_unique()`。\n\n像`std::make_unique()`这样的包装器的一个问题是，您可能不知道需要传递多少参数。也就是说，您可能最终需要包装器中的可变模板参数。完美转发通过以下方式支持这一点:\n\n```cpp\n#include <iostream>\n\nstruct the_answer\n{ };\n\nvoid foo2(const the_answer &is, int i)\n{\n    std::cout << \"l-value: \" << i << '\\n';\n}\n\nvoid foo2(the_answer &&is, int i)\n{\n    std::cout << \"r-value: \" << i << '\\n';\n}\n\ntemplate<typename... Args>\nvoid foo1(Args &&...args)\n{\n    foo2(std::forward<Args>(args)...);\n}\n\nint main(void)\n{\n    the_answer is;\n\n    foo1(is, 42);\n    foo1(the_answer(), 42);\n\n    return 0;\n}\n```\n\n输出如下:\n\n![](img/6a2956d6-ac8c-4113-b411-eb131555a556.png)\n\n前面的例子之所以有效，是因为传递给我们的`foo()`函数的变量模板参数被逗号分隔的完美转发列表所取代。\n\n# 使用 if constexpr\n\n在这个食谱中，我们将学习如何在 C++ 17 中使用一个名为`constexpr if`的新功能。这个食谱很重要，因为它将教你如何创建在运行时评估的`if`语句。具体来说，这意味着分支逻辑是在编译时而不是运行时选取的。这允许您在编译时改变函数的行为，而不牺牲性能，这在过去只能用宏来完成，这在模板编程中是没有用的，正如我们将要展示的。\n\n# 准备好\n\n开始之前，请确保满足所有技术要求，包括安装 Ubuntu 18.04 或更高版本，并在终端窗口中运行以下内容:\n\n```cpp\n> sudo apt-get install build-essential git\n```\n\n这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后，打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。\n\n# 怎么做...\n\n您需要执行以下步骤来尝试此食谱:\n\n1.  从新的终端，运行以下命令下载源代码:\n\n```cpp\n> cd ~/\n> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git\n> cd Advanced-CPP-CookBook/chapter04\n```\n\n2.  要编译源代码，请运行以下命令:\n\n```cpp\n> cmake .\n> make recipe03_examples\n```\n\n3.  编译源代码后，您可以通过运行以下命令来执行该配方中的每个示例:\n\n```cpp\n> ./recipe03_example01\nThe answer is: 42\n\n> ./recipe03_example02\nThe answer is: 42\nThe answer is: 42.12345678\n```\n\n在下一节中，我们将逐一介绍这些示例，并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。\n\n# 它是如何工作的...\n\n有时，我们希望改变程序的行为，但是我们正在创建的代码总是不变的，这意味着编译器能够确定分支本身的值，如下例所示:\n\n```cpp\nif (!NDEBUG) {}\n```\n\n这是很多代码中常用的`if`语句，包括标准库。如果启用调试，该代码的评估结果为`true`。我们通过在代码中添加调试语句来使用它，调试语句可以关闭。编译器足够聪明，可以看到`NDEBUG`是`true`还是`false`，并且要么添加代码，要么完全删除代码。换句话说，编译器可以进行简单的优化，减少代码的大小，并删除不需要的分支，因为它知道这个`if`语句的值在运行时永远不会改变。问题是，这个技巧依赖于编译器很聪明的事实。逻辑的移除是隐式可信的，这通常会导致对编译器正在做什么的假设。C++ 17 增加了一个`constexpr if`语句，允许我们改为显式。它允许我们告诉编译器:我提供的语句应该在编译时计算，而不是在运行时计算。真正强大的是，当这个假设不成立时，我们会得到编译时错误，这意味着我们隐式信任编译器执行的优化，我们现在可以在编译时验证，如果假设为假，我们会被告知可以修复问题，如下例所示:\n\n```cpp\n#include <iostream>\n\nconstexpr auto answer = 42;\n\nint main(void)\n{\n    if constexpr (answer == 42) {\n        std::cout << \"The answer is: \" << answer << '\\n';\n    }\n    else {\n        std::cout << \"The answer is not: \" << answer << '\\n';\n    }\n\n    return 0;\n}\n\n```\n\n输出如下:\n\n![](img/cf8bb6b4-07e7-4b3e-a97e-3a558cfc3533.png)\n\n在前面的例子中，我们创建了`constexpr`并在编译时而不是运行时对其求值。如果我们将`constexpr`更改为实际变量，`constexpr if`将导致以下错误:\n\n![](img/9474112b-7528-4649-b754-1e6702247c6c.png)\n\n然后，我们可以在模板函数中使用它，根据给定的类型更改模板函数的行为，如下例所示:\n\n```cpp\n#include <iostream>\n#include <iomanip>\n\ntemplate<typename T>\nconstexpr void foo(T &&t)\n{\n    if constexpr (std::is_floating_point_v<T>) {\n        std::cout << std::setprecision(10);\n    }\n\n    std::cout << \"The answer is: \" << std::forward<T>(t) << '\\n';\n}\n\nint main(void)\n{\n    foo(42);\n    foo(42.12345678);\n    return 0;\n}\n```\n\n在前面的例子中，我们使用`std::is_floating_point_v`类型特征来确定我们得到的类型是否是浮点型的。如果类型不是浮点，这将返回`constexpr false`，编译器可以优化出来。由于我们使用的是`constexpr if`，我们可以确保我们的`if`语句实际上是`constexpr`而不是运行时条件。\n\n# 使用元组处理参数包\n\n在本食谱中，我们将学习如何使用`std::tuple`处理可变参数列表。这一点很重要，因为变量参数列表用于包装函数，包装函数不知道传递给它的参数，而是将这些参数转发给传递给它的函数。但是，在一些用例中，您会关心传递的参数，并且您必须有一种处理这些参数的方法。这个食谱将展示如何做到这一点，包括如何处理任何数量的论点。\n\n# 准备好\n\n开始之前，请确保满足所有技术要求，包括安装 Ubuntu 18.04 或更高版本，并在终端窗口中运行以下内容:\n\n```cpp\n> sudo apt-get install build-essential git cmake\n```\n\n这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后，打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。\n\n# 怎么做...\n\n您需要执行以下步骤来尝试此食谱:\n\n1.  从新的终端，运行以下命令下载源代码:\n\n```cpp\n> cd ~/\n> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git\n> cd Advanced-CPP-CookBook/chapter04\n```\n\n2.  要编译源代码，请运行以下命令:\n\n```cpp\n> cmake .\n> make recipe04_examples\n```\n\n3.  编译源代码后，您可以通过运行以下命令来执行该配方中的每个示例:\n\n```cpp\n> ./recipe04_example01\n\n> ./recipe04_example02\nthe answer is: 42\n\n> ./recipe04_example03\nThe answer is: 42\n\n> ./recipe04_example04\n2\n2\n\n> ./recipe04_example05\nThe answer is: 42\n```\n\n在下一节中，我们将逐一介绍这些示例，并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。\n\n# 它是如何工作的...\n\n变量模板为程序员提供了定义模板函数的能力，而不需要定义所有的参数。这些在包装函数中大量使用，因为它们防止包装器必须知道函数的参数，如本例所示:\n\n```cpp\n#include <iostream>\n\ntemplate<typename... Args>\nvoid foo(Args &&...args)\n{ }\n\nint main(void)\n{\n    foo(\"The answer is: \", 42);\n    return 0;\n}\n```\n\n如前面的例子所示，我们已经创建了一个`foo`函数，它可以接受任意数量的参数。在本例中，我们使用了通用引用符号`Args &&...args`，这确保了 CV 限定符和 l-/r-value 被保留，这意味着我们可以使用`std::forward()`将变量参数列表传递给任何其他函数，而性能损失尽可能小。`std::make_unique()`等功能大量使用变量参数。\n\n但是，有时您可能想要访问提供的列表中的某个参数。为此，我们可以使用`std::tuple`。这是一个数据结构，接受可变数量的参数，并提供`std::get()`函数从`std::tuple`获取任何数据，如本例所示:\n\n```cpp\n#include <tuple>\n#include <iostream>\n\nint main(void)\n{\n    std::tuple t(\"the answer is: \", 42);\n    std::cout << std::get<0>(t) << std::get<1>(t) << '\\n';\n    return 0;\n}\n```\n\n输出如下:\n\n![](img/e3147713-d2d1-4d27-b867-d95407e67851.png)\n\n在上例中，我们创建了`std::tuple`，然后使用`std:get()`功能将`std::tuple`的内容输出到`stdout`。如果您试图访问超出范围的数据，编译器会在编译时知道，并给您一个类似如下的错误:\n\n![](img/f0d35dc0-05d0-44ae-9202-1dbc5da6503c.png)\n\n使用`std::tuple`，我们可以从变量参数列表中访问数据，如下所示:\n\n```cpp\n#include <tuple>\n#include <iostream>\n\ntemplate<typename... Args>\nvoid foo(Args &&...args)\n{\n    std::tuple t(std::forward<Args>(args)...);\n    std::cout << std::get<0>(t) << std::get<1>(t) << '\\n';\n}\n\nint main(void)\n{\n    foo(\"The answer is: \", 42);\n    return 0;\n}\n```\n\n输出如下:\n\n![](img/fac54cfd-01a0-4fc8-a4e1-80bb81c1fd5f.png)\n\n在前面的例子中，我们创建了一个带有可变参数列表的函数。然后我们使用`std::forward()`将这个列表传递给`std::tuple`以保持 l/r 值。最后，我们使用`std::tuple`来访问参数。如果我们不使用`std::forward()`，我们最终会得到传递给`std::tuple`的数据的 l 值版本。\n\n前面例子的明显问题是我们已经将`0`和`1`索引硬编码到了`std::tuple`中。变量参数不是运行时的动态参数数组。相反，它们是一种表达方式*我不在乎我被赋予的参数*，这就是为什么它们通常被包装器使用。包装器包装的是关心参数的东西。在`std::make_unique()`的情况下，功能是创建`std::unique_ptr`。为此，`std::make_unique()`将为您分配`std::unique_ptr`，使用变量参数列表初始化新分配的类型，然后为您提供指向此类型的指针到`std::unique_ptr`，如本例所示:\n\n```cpp\ntemplate<\n    typename T, \n    typename... Args\n    >\nvoid make_unique(Args &&...args)\n{\n    return unique_ptr<T>(new T(std::forward<Args>(args)...));\n}\n```\n\n包装器不关心传递的参数。`T`的构造函数有。如果你试图访问变量参数，你是在说*我确实关心参数*，在这种情况下，如果你关心，你必须对正在传递的参数的布局有所了解。\n\n但是，有一些技巧可以让您处理未知数量的参数。尝试这样做的最大问题是，处理变量参数的库工具最好在运行时使用，这在大多数情况下没有帮助，如下例所示:\n\n```cpp\n#include <tuple>\n#include <iostream>\n\ntemplate<typename... Args>\nvoid foo(Args &&...args)\n{\n    std::cout << sizeof...(Args) << '\\n';\n    std::cout << std::tuple_size_v<std::tuple<Args...>> << '\\n';\n}\n\nint main(void)\n{\n    foo(\"The answer is: \", 42);\n    return 0;\n}\n\n```\n\n输出如下:\n\n![](img/3e437b68-faa5-4a2b-a2e5-1353d8935542.png)\n\n在前面的示例中，我们试图获取变量参数列表中参数数量的总大小。我们可以使用变量版本的`sizeof()`函数或者使用`std::tuple_size`特性来实现。问题是这在编译时没有帮助我们，因为我们不能使用这个大小信息循环参数(因为编译时逻辑没有`for`循环)。\n\n为了克服这一点，我们可以使用一种叫做编译时递归的技巧。这个技巧使用模板来创建一个递归模板函数，它将循环遍历变量参数列表中的所有参数。看看这个例子:\n\n```cpp\n#include <tuple>\n#include <iostream>\n\ntemplate<\n    std::size_t I = 0,\n    typename ... Args,\n    typename FUNCTION\n    >\nconstexpr void\nfor_each(const std::tuple<Args...> &t, FUNCTION &&func)\n{\n    if constexpr (I < sizeof...(Args)) {\n        func(std::get<I>(t));\n        for_each<I + 1>(t, std::forward<FUNCTION>(func));\n    }\n}\n```\n\n我们从一个执行所有魔法的模板函数开始。这个第一个模板参数是`I`，它是一个从`0`开始的整数。下一个是变量模板参数，最后一个是函数类型。我们的模板函数采用`std::tuple`，我们希望迭代它(在这种情况下，我们显示一个常量版本，但是我们可以重载它来提供一个非常量版本)，以及我们希望为`std::tuple`中的每个元素调用的函数。换句话说，这个函数将循环遍历`std::tuple`中的每个元素，并调用提供的函数，每个元素迭代一次，就像`for_each()`一样，我们习惯于在其他语言或 C++ 库中运行时使用它。\n\n在这个函数中，我们检查是否已经达到元组的总大小。如果没有，我们获取元组中`I`当前值的元素，将其传递给提供的函数，然后用`I++ `再次调用我们的`for_each()`函数。要使用此`for_each()`功能，我们可以执行以下操作:\n\n```cpp\ntemplate<typename... Args>\nvoid foo(Args &&...args)\n{\n    std::tuple t(std::forward<Args>(args)...);\n    for_each(t, [](const auto &arg) {\n        std::cout << arg;\n    });\n}\n```\n\n这里，我们得到了一个变量参数列表，我们希望遍历这个列表，并将每个参数输出到`stdout`。为此，我们像以前一样创建`std::tuple`，但这次，我们将`std::tuple`传递给我们的`for_each()`功能:\n\n```cpp\nint main(void)\n{\n    foo(\"The answer is: \", 42);\n    std::cout << '\\n';\n\n    return 0;\n}\n```\n\n输出如下:\n\n![](img/d6d45daa-3d12-43cd-b947-93a9aa3990f1.png)\n\n就像我们在前面的例子中所做的那样，我们用一些我们希望输出到`stdout`的文本调用我们的`foo`函数，因此演示了如何使用`std:tuple`处理变量函数参数，即使我们不知道我们将被给出的参数总数。\n\n# 使用类型特征控制函数和对象\n\n创建 C++ 11 时，C++ 必须处理的一个问题是如何处理调整`std::vector`的大小，T0 可以采用任何类型，包括可以从`std::move()`抛出的类型。调整大小时，会创建新的内存，并将旧向量中的元素移动到新向量中。这非常有效，因为如果`std::move()`不能抛出，调整大小可以安全地执行，因为一旦调整大小功能开始将元素从一个数组移动到另一个数组，就不会出现错误。\n\n然而，如果`std::move()`可以抛出，有可能在通过循环的部分路径上，可能会出现错误。然而，`resize()`函数无法将旧内存恢复正常，因为试图移动到旧内存也会引发异常。在这种情况下，`resize()`执行复制而不是移动。副本确保旧内存中有每个对象的有效副本；因此，如果抛出异常，原始数组保持不变，可以根据需要抛出异常。\n\n在本食谱中，我们将探索如何通过使用特征改变模板类的行为来实现这一点。\n\n# 准备好\n\n开始之前，请确保满足所有技术要求，包括安装 Ubuntu 18.04 或更高版本，并在终端窗口中运行以下内容:\n\n```cpp\n> sudo apt-get install build-essential git cmake\n```\n\n这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后，打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。\n\n# 怎么做...\n\n您需要执行以下步骤来尝试此食谱:\n\n1.  从新的终端，运行以下命令下载源代码:\n\n```cpp\n> cd ~/\n> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git\n> cd Advanced-CPP-CookBook/chapter04\n```\n\n2.  要编译源代码，请运行以下命令:\n\n```cpp\n> cmake .\n> make recipe05_examples\n```\n\n3.  编译源代码后，您可以通过运行以下命令来执行该配方中的每个示例:\n\n```cpp\n> ./recipe05_example01\nnoexcept: r-value\ncan throw: l-value\n\n> ./recipe05_example02\nmove\nmove\nmove\nmove\nmove\n--------------\ncopy\ncopy\ncopy\ncopy\ncopy\n```\n\n在下一节中，我们将逐一介绍这些示例，并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。\n\n# 它是如何工作的...\n\nC++ 增加了一个叫做`std::move_if_noexcept()`的函数。如果移动构造函数/赋值运算符不能抛出，此函数将转换为 r 值，否则将转换为 l 值。例如，看看下面的代码:\n\n```cpp\n#include <iostream>\n\nstruct the_answer_noexcept\n{\n    the_answer_noexcept() = default;\n\n    the_answer_noexcept(const the_answer_noexcept &is) noexcept\n    {\n        std::cout << \"l-value\\n\";\n    }\n\n    the_answer_noexcept(the_answer_noexcept &&is) noexcept\n    {\n        std::cout << \"r-value\\n\";\n    }\n};\n```\n\n为此，我们将执行以下步骤:\n\n1.  首先，我们将创建一个具有不能抛出的移动/复制构造函数的类:\n\n```cpp\nstruct the_answer_can_throw\n{\n    the_answer_can_throw() = default;\n\n    the_answer_can_throw(const the_answer_can_throw &is)\n    {\n        std::cout << \"l-value\\n\";\n    }\n\n    the_answer_can_throw(the_answer_can_throw &&is)\n    {\n        std::cout << \"r-value\\n\";\n    }\n};\n```\n\n2.  接下来，我们将提供一个类，它有一个可以抛出的移动/复制构造函数。最后，让我们使用`std::move_if_noexcept()`来查看当试图移动上述每个类的实例时，是发生了移动还是复制:\n\n```cpp\nint main(void)\n{\n    the_answer_noexcept is1;\n    the_answer_can_throw is2;\n\n    std::cout << \"noexcept: \";\n    auto is3 = std::move_if_noexcept(is1);\n\n    std::cout << \"can throw: \";\n    auto is4 = std::move_if_noexcept(is2);\n\n    return 0;\n}\n\n```\n\n前面代码的输出如下:\n\n![](img/0308a282-75f7-42fd-82e6-4debaf2bd0d2.png)\n\n如前面的示例所示，在一种情况下，调用移动构造函数，而在另一种情况下，根据执行移动时该类型是否可以引发异常来调用复制构造函数。\n\n3.  现在，让我们创建一个带有调整大小功能的简单模拟向量，演示如何使用特征改变我们的`template`类的行为:\n\n```cpp\n#include <memory>\n#include <iostream>\n#include <stdexcept>\n\ntemplate<typename T>\nclass mock_vector\n{\npublic:\n    using size_type = std::size_t;\n\n    mock_vector(size_type s) :\n        m_size{s},\n        m_buffer{std::make_unique<T[]>(m_size)}\n    { }\n\n    void resize(size_type size)\n        noexcept(std::is_nothrow_move_constructible_v<T>)\n    {\n        auto tmp = std::make_unique<T[]>(size);\n\n        for (size_type i = 0; i < m_size; i++) {\n            tmp[i] = std::move_if_noexcept(m_buffer[i]);\n        }\n\n        m_size = size;\n        m_buffer = std::move(tmp);\n    }\n\nprivate:\n    size_type m_size{};\n    std::unique_ptr<T[]> m_buffer{};\n};\n```\n\n我们的模拟向量有一个内部缓冲区和一个大小。创建向量时，我们使用给定的大小分配内部缓冲区。然后，我们提供了一个`resize()`函数，该函数可用于在给定新大小的情况下调整内部缓冲区的大小。我们要做的第一件事是创建新的内部缓冲区，然后循环遍历每个元素以及从一个缓冲区到另一个缓冲区的元素。如果`T`不能抛出，则在循环执行过程中不会触发异常，在这种情况下，新的缓冲区将有效。如果`T`能够抛出，将会出现一个副本。如果触发异常，旧缓冲区尚未被新缓冲区替换。相反，新的缓冲区将与复制的所有元素一起被删除。\n\n为此，让我们创建一个可以引入移动构造函数/赋值运算符的类:\n\n```cpp\nstruct suboptimal\n{\n    suboptimal() = default;\n\n    suboptimal(suboptimal &&other)\n    {\n        *this = std::move(other);\n    }\n\n    suboptimal &operator=(suboptimal &&)\n    {\n        std::cout << \"move\\n\";\n        return *this;\n    }\n\n    suboptimal(const suboptimal &other)\n    {\n        *this = other;\n    }\n\n    suboptimal &operator=(const suboptimal &)\n    {\n        std::cout << \"copy\\n\";\n        return *this;\n    }\n};\n```\n\n让我们也添加一个不能从移动构造函数/赋值运算符抛出的类:\n\n```cpp\nstruct optimal\n{\n    optimal() = default;\n\n    optimal(optimal &&other) noexcept\n    {\n        *this = std::move(other);\n    }\n\n    optimal &operator=(optimal &&) noexcept\n    {\n        std::cout << \"move\\n\";\n        return *this;\n    }\n\n    optimal(const optimal &other)\n    {\n        *this = other;\n    }\n\n    optimal &operator=(const optimal &)\n    {\n        std::cout << \"copy\\n\";\n        return *this;\n    }\n};\n```\n\n最后，我们将使用这两个类创建一个向量，并尝试调整其大小:\n\n```cpp\nint main(void)\n{\n    mock_vector<optimal> d1(5);\n    mock_vector<suboptimal> d2(5);\n\n    d1.resize(10);\n    std::cout << \"--------------\\n\";\n    d2.resize(10);\n\n    return 0;\n}\n\n```\n\n前面代码的输出如下:\n\n![](img/e9bee1c2-cc4a-4a6b-8b40-d2f08e9c28b8.png)\n\n如前面的例子所示，当我们试图调整类的大小时，当移动不能抛出时执行移动，否则执行复制。换句话说，类的行为根据`T`类型的特征而变化。\n\n# 学习如何实现模板\n\nC++ 能够创建模板已经很长时间了，这允许程序员在给定类型的情况下创建类和函数的泛型实现。但是，您也可以提供非类型参数。\n\n在 C++ 17 中，您现在可以使用`auto`来提供泛型、非类型模板参数。在这个食谱中，我们将探索如何使用这个功能。这很重要，因为它允许您在代码中创建更多的通用模板。\n\n# 准备好\n\n开始之前，请确保满足所有技术要求，包括安装 Ubuntu 18.04 或更高版本，并在终端窗口中运行以下内容:\n\n```cpp\n> sudo apt-get install build-essential git cmake\n```\n\n这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后，打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。\n\n# 怎么做...\n\n您需要执行以下步骤来尝试此食谱:\n\n1.  从新的终端，运行以下命令下载源代码:\n\n```cpp\n> cd ~/\n> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git\n> cd Advanced-CPP-CookBook/chapter04\n```\n\n2.  要编译源代码，请运行以下命令:\n\n```cpp\n> cmake .\n> make recipe06_examples\n```\n\n3.  编译源代码后，您可以通过运行以下命令来执行该配方中的每个示例:\n\n```cpp\n> ./recipe06_example01\nThe answer is: 42\n> ./recipe06_example02\nThe answer is: 42\nThe answer is: 42\n> ./recipe06_example03\nThe answer is: 42\n```\n\n在下一节中，我们将逐一介绍这些示例，并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。\n\n# 它是如何工作的...\n\n在 C++ 17 之前，您可以在模板中提供非类型模板参数，但是您必须在定义中声明变量类型，如下例所示:\n\n```cpp\n#include <iostream>\n\ntemplate<int answer>\nvoid foo()\n{\n    std::cout << \"The answer is: \" << answer << '\\n';\n}\n\nint main(void)\n{\n    foo<42>();\n    return 0;\n}\n\n```\n\n输出如下:\n\n![](img/e5ae3433-362c-4d0d-a865-298472d67d5c.png)\n\n在前面的例子中，我们创建了一个`int`类型的模板参数变量，并将该变量的值输出到`stdout`。在 C++ 17 中，我们现在可以执行以下操作:\n\n```cpp\n#include <iostream>\n\ntemplate<auto answer>\nvoid foo()\n{\n    std::cout << \"The answer is: \" << answer << '\\n';\n}\n\nint main(void)\n{\n    foo<42>();\n    return 0;\n}\n```\n\n输出如下:\n\n![](img/dee00755-5e4b-4fe9-8067-fe5306327929.png)\n\n如前所示，我们现在可以状态`auto`，而不必状态`int`。这允许我们创建一个可以接受多个非类型模板参数的函数。我们还可以使用类型特征来确定允许哪些非类型参数，如本例所示:\n\n```cpp\n#include <iostream>\n#include <type_traits>\n\ntemplate<\n    auto answer,\n std::enable_if_t<std::is_integral_v<decltype(answer)>, int> = 0\n >\nvoid foo()\n{\n    std::cout << \"The answer is: \" << answer << '\\n';\n}\n\nint main(void)\n{\n    foo<42>();\n    return 0;\n}\n```\n\n输出如下:\n\n![](img/08ad857b-3e01-41e9-885d-ceea78bc65f1.png)\n\n在前面的示例中，我们的模板非类型参数只能是整数类型。\n\n# 使用显式模板声明\n\n在本食谱中，我们将探索如何通过创建显式模板声明来加快模板类的编译。这很重要，因为模板需要编译器根据需要创建类的实例。在某些情况下，显式模板声明可以为程序员提供一种方法，通过缓存最有可能使用的模板类型来加速编译，从而避免包含模板的整个定义。\n\n# 准备好\n\n开始之前，请确保满足所有技术要求，包括安装 Ubuntu 18.04 或更高版本，并在终端窗口中运行以下内容:\n\n```cpp\n> sudo apt-get install build-essential git cmake\n```\n\n这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后，打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。\n\n# 怎么做...\n\n您需要执行以下步骤来尝试此食谱:\n\n1.  从新的终端，运行以下命令下载源代码:\n\n```cpp\n> cd ~/\n> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git\n> cd Advanced-CPP-CookBook/chapter04\n```\n\n2.  要编译源代码，请运行以下命令:\n\n```cpp\n> cmake .\n> make recipe07_examples\n```\n\n3.  编译源代码后，您可以通过运行以下命令来执行该配方中的每个示例:\n\n```cpp\n> ./recipe07_example01 \nThe answer is: 42\nThe answer is: 42\nThe answer is: 42.1\n> ./recipe07_example02 \nThe answer is: 4\n```\n\n在下一节中，我们将逐一介绍这些示例，并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。\n\n# 它是如何工作的...\n\n每次编译器看到使用给定类型的模板类时，它都会隐式创建该类型的一个版本。然而，这可能会发生多次，降低编译器的速度。但是，如果预期使用的类型是预先已知的，这个问题可以使用显式模板专门化来解决。看看这个例子:\n\n```cpp\n#include <iostream>\n\ntemplate<typename T>\nclass the_answer\n{\npublic:\n    the_answer(T t)\n    {\n        std::cout << \"The answer is: \" << t << '\\n';\n    }\n};\n```\n\n之前，我们创建了一个简单的结构，在构建过程中输出到`stdout`。通常，一旦看到类的第一个专门化，编译器就会创建这个类。但是，我们可以执行以下操作:\n\n```cpp\ntemplate class the_answer<int>;\ntemplate class the_answer<unsigned>;\ntemplate class the_answer<double>;\n```\n\n这类似于一个类原型，它显式地创建了我们期望使用的专门化。这些必须在代码中使用之前声明(这意味着它们通常在模板定义之后声明)；然而，一旦它们被陈述，它们可以如下使用:\n\n```cpp\nint main(void)\n{\n    the_answer{42};\n    the_answer{42U};\n    the_answer{42.1};\n\n    return 0;\n}\n```\n\n代码的输出如下:\n\n![](img/cdc10992-381a-45a6-80a6-aff500c8753f.png)\n\n如前面的例子所示，我们可以像平常一样创建模板的实例，但是，在这种情况下，我们可以在大量使用这个类的情况下加快编译器的速度。这是因为，在源代码中，我们不需要包含模板的实现。为了演示这一点，让我们看另一个更复杂的例子。在头文件(称为`recipe07.h`)中，我们将使用以下内容创建我们的模板:\n\n```cpp\ntemplate<typename T>\nstruct the_answer\n{\n    T m_answer;\n\n    the_answer(T t);\n    void print();\n};\n```\n\n如您所见，我们有一个`template`类，它没有实现所提供的函数。然后，我们将在其自己的源文件中使用以下内容提供该模板的实现:\n\n```cpp\n#include <iostream>\n#include \"recipe07.h\"\n\ntemplate<typename T>\nthe_answer<T>::the_answer(T t) :\n    m_answer{t}\n{ }\n\ntemplate<typename T>\nvoid the_answer<T>::print()\n{\n    std::cout << \"The answer is: \" << m_answer << '\\n';\n}\n\ntemplate class the_answer<int>;\n```\n\n正如您在前面的示例中看到的，我们添加了一个显式的模板声明。这确保了我们为期望的类生成实现。编译器将为我们期望显式创建的类创建实例，就像我们通常编写的任何其他源代码一样。不同的是，我们可以为我们想要的任何类型显式定义这个类。最后，我们将这段代码称为:\n\n```cpp\n#include \"recipe07.h\"\n\nint main(void)\n{\n    the_answer is{42};\n    is.print();\n\n    return 0;\n}\n```\n\n输出如下:\n\n![](img/b30b50ca-8f48-4791-80c2-83b5886b15f3.png)\n\n如您所见，如果用显式类型定义类，而不是使用普通头文件的模板类，我们可以用同样的方式调用我们的类，普通头文件很小，没有完整的实现，允许编译器加快速度。"
  },
  {
    "path": "docs/adv-cpp-prog-cb/05.md",
    "content": "# 五、并发和同步\n\n在本章中，我们将学习如何在 C++ 中正确处理并发、同步和并行。在这里，您必须具备 C++ 和 C++ 线程的一般知识。本章很重要，因为使用 C++ 通常需要使用共享资源，如果没有正确实现线程安全，共享资源很容易被破坏。我们将从`std::mutexes`的广泛概述开始，它提供了一种同步 C++ 线程的方法。然后，我们将研究原子数据类型，它为安全处理并行性提供了另一种机制。\n\n本章有一些菜谱，演示了如何在使用 C++ 线程时处理不同的场景，包括处理`const &`、线程安全包装、阻塞与异步编程以及 C++ 承诺和未来。这一点很重要，因为这一知识在处理多个执行线程时至关重要。\n\n本章涵盖以下配方:\n\n*   使用互斥体\n*   使用原子数据类型\n*   理解`const &`可变性在多线程环境中的含义\n*   使类线程安全\n*   同步包装器以及如何实现它们\n*   阻塞操作与异步编程\n*   带着承诺和未来工作\n\n# 技术要求\n\n要编译和运行本章中的示例，您必须拥有运行 Ubuntu 18.04 的计算机的管理权限，并且具有功能性互联网连接。在运行这些示例之前，您必须安装以下内容:\n\n```cpp\n> sudo apt-get install build-essential git cmake\n```\n\n如果这安装在 Ubuntu 18.04 以外的任何操作系统上，则需要 GCC 7.4 或更高版本以及 CMake 3.6 或更高版本。\n\n# 使用互斥体\n\n在这个食谱中，我们将学习为什么以及如何在 C++ 中使用互斥体。在 C++ 中使用多线程时，建立线程间共享的资源是很常见的。正如我们将在本食谱中演示的那样，试图同时使用这些共享资源会导致能够破坏资源的竞争条件。\n\n互斥体(在 C++ 中，这被写成`std::mutex`)是一个用于保护共享资源的对象，确保多个线程可以以受控的方式访问共享资源。这可以防止它变得腐败。\n\n# 准备好\n\n在我们开始之前，请确保满足所有技术要求，包括安装 Ubuntu 18.04 或更高版本，并在终端窗口中运行以下内容:\n\n```cpp\n> sudo apt-get install build-essential git\n```\n\n这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后，打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。\n\n# 怎么做...\n\n您需要执行以下步骤来尝试此食谱:\n\n1.  从新的终端，运行以下命令下载源代码:\n\n```cpp\n> cd ~/\n> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git\n> cd Advanced-CPP-CookBook/chapter05\n```\n\n2.  要编译源代码，请运行以下命令:\n\n```cpp\n> cmake .\n> make recipe01_examples\n```\n\n3.  编译源代码后，您可以通过运行以下命令来执行该配方中的每个示例:\n\n```cpp\n> ./recipe01_example01\nThe answer is: 42\nThe answer is: 42\nThe answer is: 42\nThe\n answer is: 42\nThe answer is: 42\n...\n\n> ./recipe01_example02\nThe answer is: 42\nThe answer is: 42\nThe answer is: 42\nThe answer is: 42\nThe answer is: 42\n...\n\n> ./recipe01_example03\n...\n\n> ./recipe01_example04\nThe answer is: 42\n\n> ./recipe01_example05\nThe answer is: 42\nThe answer is: 42\nThe answer is: 42\nThe answer is: 42\nThe answer is: 42\n...\n\n> ./recipe01_example06\nThe answer is: 42\nThe answer is: 42\n\n> ./recipe01_example07\n\n> ./recipe01_example08\nlock acquired\nlock failed\n```\n\n在下一节中，我们将逐一介绍这些示例，并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。\n\n# 它是如何工作的...\n\n在本食谱中，我们将学习如何使用`std::mutex`来保护共享资源不被破坏。首先，让我们先回顾一下当多个线程同时访问资源时，资源是如何变得损坏的:\n\n```cpp\n#include <thread>\n#include <string>\n#include <iostream>\n\nvoid foo()\n{\n    static std::string msg{\"The answer is: 42\\n\"};\n    while(true) {\n        for (const auto &c : msg) {\n            std::clog << c;\n        }\n    }\n}\n\nint main(void)\n{\n    std::thread t1{foo};\n    std::thread t2{foo};\n\n    t1.join();\n    t2.join();\n\n    // Never reached\n    return 0;\n}\n```\n\n执行时，我们会得到以下输出:\n\n![](img/01192c95-b3c1-4df5-a5a4-b94be4b18090.png)\n\n在前面的例子中，我们创建了一个函数，在一个循环中输出到`stdout`。然后我们创建两个线程，每个线程执行前面定义的函数。正如您所看到的，当两个线程都执行时，结果输出会损坏。这是因为当一个线程正在将其文本输出到`stdout`时，另一个线程同时输出到`stdout`，导致一个线程的输出与另一个线程的输出混合。\n\n为了处理这个问题，我们必须确保，一旦其中一个线程试图将其文本输出到`stdout`，应该允许它在另一个线程能够输出之前完成其输出。换句话说，每个线程必须轮流输出到`stdout`。当一个线程正在输出时，另一个线程必须等待轮到它。为此，我们将利用一个`std::mutex`对象。\n\n# std::互斥\n\n互斥体是一个用于保护共享资源的对象，以确保共享资源的使用不会导致损坏。为此，`std::mutex`具有`lock()`功能和`unlock()`功能。锁定功能*获取对共享资源(有时称为关键部分)的*访问。`unlock()` *释放*这个之前获得的权限。在另一个线程已经执行了`lock()`之后，任何执行`lock()`功能的尝试都将导致该线程必须等待`unlock()`功能被执行。\n\n`std::mutex`如何实现取决于 CPU 的架构和操作系统；然而，一般来说，互斥体可以用一个简单的整数来实现。如果整数为`0`，`lock()`函数将整数设置为`1`并返回，告知互斥被获取。如果整数为`1`，表示互斥已经被获取，`lock()`函数会等待(即阻塞)直到整数变为`0`，然后将整数设置为`1`返回。如何实现这种等待取决于操作系统。比如`wait()`函数可以永远循环，直到整数变成`0`，称为**自旋锁**，也可以执行一个`sleep()`函数，等待一段时间，让其他线程和进程在互斥锁的同时执行。释放函数总是将整数设置为`0`，这意味着不再获取互斥体。确保互斥体正常工作的诀窍是确保使用原子操作读取/写入整数。如果使用非原子操作，整数本身将遭受互斥体试图防止的共享资源损坏。\n\n例如，考虑以下情况:\n\n```cpp\n#include <mutex>\n#include <thread>\n#include <string>\n#include <iostream>\n\nstd::mutex m{};\n\nvoid foo()\n{\n    static std::string msg{\"The answer is: 42\\n\"};\n    while(true) {\n        m.lock();\n        for (const auto &c : msg) {\n            std::clog << c;\n        }\n        m.unlock();\n    }\n}\n\nint main(void)\n{\n    std::thread t1{foo};\n    std::thread t2{foo};\n\n    t1.join();\n    t2.join();\n\n    // Never reached\n    return 0;\n}\n```\n\n该示例在运行时输出以下内容:\n\n![](img/a74a32c8-e166-46cc-b84f-774e905f34cb.png)\n\n在前面的例子中，我们创建了输出到`stdout`的相同函数。不同的是，在输出到`stdout`之前，我们通过执行`lock()`功能获取`std::mutex`。输出到`stdout`后，我们通过执行`unlock()`函数释放互斥体。`lock()`和`unlock()`功能之间的代码称为**临界区**。关键区域的任何代码在任何给定时间只能由一个线程执行，确保我们对`stdout`的使用不会被破坏。\n\n通过控制对共享资源的访问(例如，使用互斥体)来确保共享资源不会被破坏，这称为**同步**。尽管大多数需要线程同步的场景并不复杂，但有些场景可能会产生需要整个大学课程才能覆盖的线程同步方案。由于这个原因，线程同步被认为是计算机科学中非常难以正确编程的范例。\n\n在本食谱中，我们将介绍其中的一些场景。首先，让我们讨论一个叫做**僵局**的东西。当线程在调用`lock()`函数时进入无休止的等待状态时，就会发生死锁。死锁通常极难调试，并且是由以下几种原因造成的:\n\n*   由于程序员错误或获取互斥锁的线程崩溃，线程从未调用`unlock()`\n*   同一线程在调用`unlock()`之前多次调用`lock()`函数\n*   每个线程以不同的顺序锁定多个互斥体\n\n为了演示这一点，让我们看下面的例子:\n\n```cpp\n#include <mutex>\n#include <thread>\n\nstd::mutex m{};\n\nvoid foo()\n{\n    m.lock();\n}\n\nint main(void)\n{\n    std::thread t1{foo};\n    std::thread t2{foo};\n\n    t1.join();\n    t2.join();\n\n    // Never reached\n    return 0;\n}\n```\n\n在前面的例子中，我们创建了两个线程，这两个线程都试图锁定互斥体，但从不调用`unlock()`。因此，第一个线程获取互斥体，然后返回而不释放它。当第二个线程试图获取互斥体时，它会被迫等待第一个线程执行`unlock()`，但它永远不会这样做，从而导致死锁(即程序永远不会返回)。\n\n在这个例子中，死锁很容易识别和纠正；然而，在现实场景中，识别死锁要复杂得多。让我们看看下面的例子:\n\n```cpp\n#include <array>\n#include <mutex>\n#include <thread>\n#include <string>\n#include <iostream>\n\nstd::mutex m{};\nstd::array<int,6> numbers{4,8,15,16,23,42};\n\nint foo(int index)\n{\n    m.lock();\n    auto element = numbers.at(index);\n    m.unlock();\n\n    return element;\n}\n\nint main(void)\n{\n    std::cout << \"The answer is: \" << foo(5) << '\\n';\n    return 0;\n}\n```\n\n在前面的例子中，我们编写了一个函数，在给定索引的情况下返回数组中的一个元素。此外，我们获取了一个保护数组的互斥体，并在返回之前释放互斥体。这里的挑战是我们必须`unlock()`函数可以返回的互斥体，它不仅包括从函数返回的每个可能的分支，还包括可能引发异常的所有可能情况。在上例中，如果提供的索引大于数组，`std::array`对象将引发异常，导致函数在有机会调用`unlock()`之前返回，如果另一个线程共享该数组，将导致死锁。\n\n# 标准::锁定 _ 防护\n\nC++ 提供了一个`std::lock_guard`对象来简化`std::mutex`对象的使用，而不是在代码中乱丢`try` / `catch`块来防止死锁，死锁假设程序员甚至能够在不出错的情况下确定每个可能发生的情况。\n\n例如，考虑以下代码:\n\n```cpp\n#include <mutex>\n#include <thread>\n#include <iostream>\n\nstd::mutex m{};\n\nvoid foo()\n{\n    static std::string msg{\"The answer is: 42\\n\"};\n\n    while(true) {\n        std::lock_guard lock(m);\n        for (const auto &c : msg) {\n            std::clog << c;\n        }\n    }\n}\n\nint main(void)\n{\n    std::thread t1{foo};\n    std::thread t2{foo};\n\n    t1.join();\n    t2.join();\n\n    // Never reached\n    return 0;\n}\n```\n\n执行时，我们会看到以下内容:\n\n![](img/948ff65e-fbca-4f22-98f2-85b33fe28cea.png)\n\n如前例所示，当我们在互斥体上正常调用`lock()`时，使用`std::lock_guard`。`std::lock_guard`在互斥体创建时调用`lock()`函数，在互斥体销毁时调用`unlock()`(一个叫做**资源获取是初始化**或 **RAII** 的成语)。无论函数如何返回(无论是从正常返回还是从异常返回)，互斥体总是会被释放，确保死锁是不可能的，防止程序员必须准确地确定函数可能返回的每个可能的场景。\n\n虽然`std::lock_guard`能够在`unlock()`从未被调用的情况下防止死锁，但是在`unlock()`被调用之前同一线程多次调用`lock()`的情况下，它不能防止死锁的发生。为了处理这种情况，C++ 提供了`std::recursive_mutex`。\n\n# std::递归 _ 互斥\n\n每次同一线程调用`lock()`函数时，递归互斥体都会递增存储在互斥体内的整数，而不会导致`lock()`函数等待。比如释放互斥体(即互斥体中的整数为`0`，线程`#1`调用`lock()`函数时，互斥体中的整数设置为`1`。正常情况下，如果线程`#1`再次调用`lock()`功能，`lock()`功能会看到整数为`1`，进入等待状态，直到整数设置为`0`。相反，递归互斥体将确定哪个线程正在调用`lock()`函数，如果获取互斥体的线程是调用`lock()`函数的同一个线程，互斥体中的整数将使用原子操作再次递增(现在导致`2`)。对于要释放的互斥体，线程必须调用`unlock()`，使用原子操作递减整数，直到互斥体中的整数为`0`。\n\n递归互斥允许同一个线程想调用多少次`lock()`函数就调用多少次，防止多次调用`lock()`函数并导致死锁，代价是`lock()`和`unlock()`函数必须包含一个附加的函数调用来获取线程的`id()`实例，这样互斥就可以确定哪个线程在调用`lock()`和`unlock()`。\n\n例如，考虑以下代码片段:\n\n```cpp\n#include <mutex>\n#include <thread>\n#include <string>\n#include <iostream>\n\nstd::recursive_mutex m{};\n\nvoid foo()\n{\n    m.lock();\n    m.lock();\n\n    std::cout << \"The answer is: 42\\n\";\n\n    m.unlock();\n    m.unlock();\n}\n\nint main(void)\n{\n    std::thread t1{foo};\n    std::thread t2{foo};\n\n    t1.join();\n    t2.join();\n\n    return 0;\n}\n```\n\n前面的示例导致以下结果:\n\n![](img/fef09e3b-fb6a-479b-a600-6c482d4c8b94.png)\n\n在前面的例子中，我们定义了一个函数，它为递归互斥体调用`lock()`函数两次，输出到`stdout`，然后调用`unlock()`函数两次。然后我们创建两个执行这个函数的线程，导致`stdout`没有损坏，也没有死锁。\n\n# 标准::共享 _ 互斥\n\n到目前为止，我们的同步原语已经序列化了对共享资源的访问。也就是说，当访问关键区域时，每个线程必须一次执行一个。尽管这确保了不会发生损坏，但对于某些类型的场景来说，这是低效的。为了更好地理解这一点，我们必须首先研究是什么导致了腐败。\n\n让我们考虑一个由两个线程同时递增的整数变量。递增整数变量的过程如下:`i = i + 1`。\n\n让我们这样写:\n\n```cpp\nint i = 0;\n\nauto tmp = i;\ntmp++ ;\ni = tmp; // i == 1\n```\n\n为了防止损坏，我们使用互斥来确保如果两个线程递增整数，它们会同步递增:\n\n```cpp\nauto tmp_thread1 = i;\ntmp_thread1++ ;\ni = tmp_thread1; // i == 1\n\nauto tmp_thread2 = i;\ntmp_thread2++ ;\ni = tmp_thread2; // i == 2\n```\n\n当这些操作混合时(也就是说，当两个操作在不同的线程中同时执行时)，就会发生损坏。例如，考虑以下代码:\n\n```cpp\nauto tmp_thread1 = i; // 0\nauto tmp_thread2 = i; // 0\ntmp_thread1++ ; // 1\ntmp_thread2++ ; // 1\ni = tmp_thread1; // i == 1\ni = tmp_thread2; // i == 1\n```\n\n整数不是`2`，而是`1`，因为在第一个增量允许结束之前就读取了整数。这种情况是可能的，因为两个线程都试图写入同一个共享资源。我们称这些类型的线程为**生产者**。\n\n但是，如果我们创建一百万个线程同时读取共享资源，会怎么样。由于整数从不改变，无论线程以什么顺序执行，它们都将读取相同的值，因此不可能损坏。我们称这些线程为**消费者**。如果我们只有消费者，我们就不需要线程同步，因为损坏是不可能的。\n\n最后，如果我们有同样的 100 万消费者，但我们在组合中增加了一个生产者，会发生什么？现在，我们必须使用线程同步，因为生产者可能正在试图向消费者试图读取的整数写入一个值，这将导致一个损坏的结果。为了防止这种情况，我们必须使用互斥来保护整数。然而，如果我们使用`std::mutex`，所有 100 万消费者将不得不相互等待，即使消费者自己可以安全地同时执行而不用担心腐败。只有当制作人试图执行时，我们才会担心。\n\n为了处理这个明显的性能问题，C++ 提供了`std::shared_mutex`对象。例如，考虑以下代码:\n\n```cpp\n#include <mutex>\n#include <shared_mutex>\n#include <thread>\n#include <iostream>\n\nint count_rw{};\nconst auto &count_ro = count_rw;\n\nstd::shared_mutex m{};\n\nvoid reader()\n{\n    while(true) {\n        std::shared_lock lock(m);\n        if (count_ro >= 42) {\n            return;\n        }\n    }\n}\n\nvoid writer()\n{\n    while(true) {\n        std::unique_lock lock(m);\n        if (++ count_rw == 100) {\n            return;\n        }\n    }\n}\n\nint main(void)\n{\n    std::thread t1{reader};\n    std::thread t2{reader};\n    std::thread t3{reader};\n    std::thread t4{reader};\n    std::thread t5{writer};\n\n    t1.join();\n    t2.join();\n    t3.join();\n    t4.join();\n    t5.join();\n\n    return 0;\n}\n```\n\n在前面的例子中，我们创建了一个生产者函数(称为`reader`函数)和一个消费者函数(称为`writer`函数)。生产者使用`std::unique_lock()`锁定互斥，而消费者使用`std::shared_lock()`锁定互斥。每当使用`std::unique_lock()`锁定互斥体时，所有其他线程都必须等待(生产者和消费者都一样)。但是，如果使用`std::shared_lock()`锁定互斥体，则使用`std::shared_lock()`锁定互斥体的额外尝试不会导致线程等待。\n\n只有当`std::unique_lock()`被调用时，等待才会发生。这使得消费者可以在不等待对方的情况下执行。只有当生产者试图执行时，消费者才必须等待，防止消费者相互序列化，最终导致更好的性能(特别是如果消费者的数量是 100 万)。\n\n需要注意的是，我们使用`const`关键字来保证消费者不是生产者。这个简单的技巧确保了程序员不会意外地认为他们已经编程了一个消费者，而事实上，他们已经创建了一个生产者，因为如果发生这种情况，编译器会警告程序员。\n\n# 标准::定时互斥\n\n最后，我们还没有处理获取互斥锁的线程崩溃的场景。在这种情况下，任何试图获取相同互斥体的线程都将进入死锁状态，因为崩溃的线程永远没有机会调用`unlock()`。防止此问题的一种方法是使用`std::timed_mutex`。\n\n例如，考虑以下代码:\n\n```cpp\n#include <mutex>\n#include <thread>\n#include <iostream>\n\nstd::timed_mutex m{};\n\nvoid foo()\n{\n    using namespace std::chrono;\n\n    if (m.try_lock_for(seconds(1))) {\n        std::cout << \"lock acquired\\n\";\n    }\n    else {\n        std::cout << \"lock failed\\n\";\n    }\n}\n\nint main(void)\n{\n    std::thread t1{foo};\n    std::thread t2{foo};\n\n    t1.join();\n    t2.join();\n\n    return 0;\n}\n```\n\n执行此操作时，我们会得到以下结果:\n\n![](img/a606b9bb-6ef7-4885-93e6-344fc3bc06e7.png)\n\n在前面的例子中，我们告诉 C++ 线程只允许等待 1 秒钟。如果互斥体已经被获取，并且在 1 秒钟后没有被释放，`try_lock_for()`函数将退出并返回 false，允许线程优雅地退出并处理错误，而不会进入死锁。\n\n# 使用原子数据类型\n\n在这个食谱中，我们将学习如何在 C++ 中使用原子数据类型。原子数据类型提供了读写简单数据类型(即布尔或整数)的能力，而不需要线程同步(即使用`std::mutex`和 friends)。为了实现这一点，原子数据类型使用特殊的中央处理器指令来实现，这些指令确保当一个操作被执行时，它是作为单个原子操作来完成的。\n\n例如，递增一个整数可以写成如下形式:\n\n```cpp\nint i = 0;\n\nauto tmp = i;\ntmp++ ;\ni = tmp; // i == 1\n```\n\n原子数据类型确保执行该增量，使得没有其他同时增量整数的尝试会交错，从而导致损坏。CPU 是如何做到这一点的，不在本书讨论范围之内。这是因为这在现代超标量流水线式 CPU 中极其复杂，这些 CPU 支持在多个内核和套接字上并行、无序和推测地执行指令。\n\n# 准备好\n\n在我们开始之前，请确保满足所有技术要求，包括安装 Ubuntu 18.04 或更高版本，并在终端窗口中运行以下内容:\n\n```cpp\n> sudo apt-get install build-essential git\n```\n\n这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后，打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。\n\n# 怎么做...\n\n您需要执行以下步骤来尝试此食谱:\n\n1.  从新的终端，运行以下命令下载源代码:\n\n```cpp\n> cd ~/\n> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git\n> cd Advanced-CPP-CookBook/chapter05\n```\n\n2.  要编译源代码，请运行以下命令:\n\n```cpp\n> cmake .\n> make recipe02_examples\n```\n\n3.  编译源代码后，您可以通过运行以下命令来执行该配方中的每个示例:\n\n```cpp\n> ./recipe02_example01\ncount: 711\natomic count: 1000\n```\n\n在下一节中，我们将逐一介绍这些示例，并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。\n\n# 它是如何工作的...\n\n在这个食谱中，我们将学习如何使用 C++ 的原子数据类型。原子数据类型仅限于简单的数据类型，如整数，由于这些数据类型实现起来极其复杂，因此只支持简单的操作，如加、减、增和减。\n\n让我们看一个简单的例子，它不仅演示了如何在 C++ 中使用原子数据类型，还演示了为什么原子数据类型如此重要:\n\n```cpp\n#include <atomic>\n#include <thread>\n#include <iostream>\n\nint count{};\nstd::atomic<int> atomic_count{};\n\nvoid foo()\n{\n    do {\n        count++ ;\n        atomic_count++ ;\n    }\n    while (atomic_count < 99999);\n}\n\nint main(void)\n{\n    std::thread t1{foo};\n    std::thread t2{foo};\n\n    t1.join();\n    t2.join();\n\n    std::cout << \"count: \" << count << '\\n';\n    std::cout << \"atomic count: \" << atomic_count << '\\n';\n\n    return 0;\n}\n```\n\n当执行这段代码时，我们会得到以下结果:\n\n![](img/216f9a16-1893-4c5f-b259-da1e2d0b4bc0.png)\n\n在前面的例子中，我们有两个整数。第一个整数是普通的 C/C++ 整数类型，而第二个是原子数据类型(整数类型)。然后我们定义一个循环直到原子数据类型为`1000`的函数。最后，我们从两个线程执行这个函数，这意味着我们的全局整数同时增加了两个线程。\n\n如您所见，这个简单测试的输出显示，简单的 C/C++ 整数数据类型的值与原子数据类型的值不同，但两者的增量相同。其原因可以从该功能的组装中看出(在英特尔中央处理器上)，如下所示:\n\n![](img/b1e3de9e-b754-49b6-a53a-d4e0bfd9cc2f.png)\n\n要增加一个整数(未启用优化)，编译器必须将内存内容移入寄存器，将`1`添加到寄存器中，然后将寄存器的结果写回内存。由于该代码在两个不同的线程中同时执行，因此该代码会交错，从而导致损坏。原子数据类型不会遇到同样的问题。这是因为增加原子数据类型的过程发生在单个特殊指令中，中央处理器确保在其他中央处理器上执行该指令，而不会将其内部状态与其他指令的相同内部状态交错。\n\n原子数据类型通常用于实现同步原语，如`std::mutex`(尽管实际上`std::mutex`是使用测试和设置指令实现的，这些指令使用类似的原理，但执行速度往往比原子指令快)。这些数据类型也可以用来实现称为无锁数据结构的特殊数据结构，它能够在多线程环境中运行，而不需要`std::mutex`。无锁数据结构的好处是，在以更复杂的 CPU 硬件和其他类型的性能损失为代价处理线程同步时，没有等待状态(当 CPU 遇到原子指令时，硬件提供的大多数 CPU 优化不得不被暂时禁用)。所以，像计算机科学中的任何事情一样，他们有他们的时间和地点。\n\n# 理解常量和可变在多线程环境中的含义\n\n在本食谱中，我们将学习如何处理标记为`const`，但包含必须用于确保线程同步的`std::mutex`的对象。这个方法很重要，因为将`std::mutex`存储为类的私有成员很有用，但是，一旦这样做，将该对象的实例作为常量引用(即`const &`)传递将导致编译器错误。在这个食谱中，我们将演示为什么会出现这种情况，以及如何克服它。\n\n# 准备好\n\n在我们开始之前，请确保满足所有技术要求，包括安装 Ubuntu 18.04 或更高版本，并在终端窗口中运行以下内容:\n\n```cpp\n> sudo apt-get install build-essential git\n```\n\n这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后，打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。\n\n# 怎么做...\n\n您需要执行以下步骤来尝试此食谱:\n\n1.  从新的终端，运行以下命令下载源代码:\n\n```cpp\n> cd ~/\n> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git\n> cd Advanced-CPP-CookBook/chapter05\n```\n\n2.  要编译源代码，请运行以下命令:\n\n```cpp\n> cmake .\n> make recipe03_examples\n```\n\n3.  编译源代码后，您可以通过运行以下命令来执行该配方中的每个示例:\n\n```cpp\n> ./recipe03_example01\nThe answer is: 42\n\n> ./recipe03_example03\nThe answer is: 42\n```\n\n在下一节中，我们将逐一介绍这些示例，并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。\n\n# 它是如何工作的...\n\n在这个食谱中，我们将学习如何将`std::mutex`添加到一个类的私有成员中，同时仍然能够处理`const`场景。一般来说，有两种方法可以确保对象是线程安全的。第一种方法是将`std::mutex`置于全球层面。这样做可以确保对象可以作为常量引用传递，或者对象本身可以具有标记为`const`的功能。\n\n为此，请考虑以下代码示例:\n\n```cpp\n#include <mutex>\n#include <thread>\n#include <iostream>\n\nstd::mutex m{};\n\nclass the_answer\n{\npublic:\n    void print() const\n    {\n        std::lock_guard lock(m);\n        std::cout << \"The answer is: 42\\n\";\n    }\n};\n\nint main(void)\n{\n    the_answer is;\n    is.print();\n\n    return 0;\n}\n```\n\n在前面的例子中，我们创建了一个对象，当执行`print()`功能时，该对象输出到`stdout`。`print()`函数被标记为`const`，这告诉编译器`print()`函数不会修改任何类成员(也就是说，该函数是只读的)。由于`std::mutex`是全局的，对象的常量限定符被维护，代码编译和执行没有问题。\n\n全局`std::mutex`对象的问题在于，该对象的每个实例都必须使用相同的`std::mutex`对象。如果用户有意这样做，这很好，但是如果您希望对象的每个实例都有自己的`std::mutex`对象(例如，当对象的同一个实例可能由多个线程执行时)怎么办？\n\n为此，让我们使用以下示例来看看这是如何发生的:\n\n```cpp\n#include <mutex>\n#include <thread>\n#include <iostream>\n\nclass the_answer\n{\n    std::mutex m{};\n\npublic:\n    void print() const\n    {\n        std::lock_guard lock(m);\n        std::cout << \"The answer is: 42\\n\";\n    }\n};\n\nint main(void)\n{\n    the_answer is;\n    is.print();\n\n    return 0;\n}\n```\n\n如果我们试图对此进行编译，我们会得到以下结果:\n\n![](img/944a6bd9-fba1-4f70-b061-5dc7c7c4afba.png)\n\n在前面的例子中，我们所做的只是取前面的例子并将`std::mutex`作为私有成员移动到类内部。因此，当我们试图编译该类时，我们会得到一个编译器错误。这是因为`print()`函数被标记为`const`，这告诉编译器`print()`函数不会修改类的任何成员。问题是当你试图锁定`std::mutex`时，你必须修改它，导致编译器错误。\n\n为了克服这一点，我们必须告诉编译器通过将`std::mutex`标记为可变来忽略这个错误。将成员标记为可变告诉编译器，即使对象作为常量引用传递或者对象定义了常量函数，也允许修改该成员。\n\n例如，代码在标记为`mutable`的`const`上是这样出现的:\n\n```cpp\n#include <mutex>\n#include <thread>\n#include <iostream>\n\nclass the_answer\n{\n    mutable std::mutex m{};\n\npublic:\n    void print() const\n    {\n        std::lock_guard lock(m);\n        std::cout << \"The answer is: 42\\n\";\n    }\n};\n\nint main(void)\n{\n    the_answer is;\n    is.print();\n\n    return 0;\n}\n```\n\n正如您在前面的例子中所看到的，一旦我们将`std::mutex`标记为可变的，代码就会如我们所期望的那样编译和执行。需要注意的是`std::mutex`是少数可以接受使用可变的例子之一。可变关键字很容易被滥用，导致代码无法按预期编译或运行。\n\n# 使类线程安全\n\n在这个食谱中，我们将学习如何使一个类线程安全(也就是说，如何确保一个类的公共成员函数可以在任何时间被任何数量的线程同时调用)。大多数类，尤其是那些由 C++ 标准库提供的类，不是线程安全的，相反，假设用户将根据需要添加线程同步原语，如`std::mutex`对象。这种方法的问题在于，每个对象都有两个必须在代码中跟踪的实例:类本身及其`std::mutex`。用户还必须用使用`std::mutex`保护类的自定义版本包装每个对象的函数，导致不仅有两个对象必须被管理，而且还有一堆 C 风格的包装函数。\n\n这个方法很重要，因为它将演示如何通过创建一个线程安全类来解决代码中的这些问题，该类将所有内容组合成一个类。\n\n# 准备好\n\n在我们开始之前，请确保满足所有技术要求，包括安装 Ubuntu 18.04 或更高版本，并在终端窗口中运行以下内容:\n\n```cpp\n> sudo apt-get install build-essential git\n```\n\n这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后，打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。\n\n# 怎么做...\n\n您需要执行以下步骤来尝试此食谱:\n\n1.  从新的终端，运行以下命令下载源代码:\n\n```cpp\n> cd ~/\n> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git\n> cd Advanced-CPP-CookBook/chapter05\n```\n\n2.  要编译源代码，请运行以下命令:\n\n```cpp\n> cmake .\n> make recipe04_examples\n```\n\n3.  编译源代码后，您可以通过运行以下命令来执行该配方中的每个示例:\n\n```cpp\n> ./recipe04_example01\n```\n\n在下一节中，我们将逐一介绍这些示例，并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。\n\n# 它是如何工作的...\n\n在这个食谱中，我们将学习如何通过实现我们自己的线程安全堆栈来创建一个线程安全类。C++ 标准库不提供线程安全的数据结构，因此，如果希望将数据结构用作跨多个线程的全局资源，可以手动添加线程安全。这可以通过实现包装函数或创建包装类来实现。\n\n创建包装函数的优点是，对于全局对象，所需的代码量通常更小，更容易理解，而线程安全类的优点是，您可以创建类的多个实例，因为`std::mutex`是独立的。\n\n这可以通过下面的代码示例来尝试:\n\n```cpp\n#include <mutex>\n#include <stack>\n#include <iostream>\n\ntemplate<typename T>\nclass my_stack\n{\n    std::stack<T> m_stack;\n    mutable std::mutex m{};\n\npublic:\n\n    template<typename ARG>\n    void push(ARG &&arg)\n    {\n        std::lock_guard lock(m);\n        m_stack.push(std::forward<ARG>(arg));\n    }\n\n void pop()\n    {\n        std::lock_guard lock(m);\n        m_stack.pop();\n    }\n\n    auto empty() const\n    {\n        std::lock_guard lock(m);\n        return m_stack.empty();\n    }\n};\n```\n\n在前面的例子中，我们实现了自己的堆栈。这个栈有`std::stack`和`std::mutex`作为成员变量。然后我们重新实现`std::stack`提供的一些功能。每个函数首先尝试获取`std::mutex`，然后调用`std::stack`中的相关函数。在`push()`函数的情况下，我们利用`std::forward`来确保传递给`push()`函数的参数得到保留。\n\n最后，我们可以像使用`std::stack`一样使用自定义堆栈。例如，看看下面的代码:\n\n```cpp\nint main(void)\n{\n    my_stack<int> s;\n\n    s.push(4);\n    s.push(8);\n    s.push(15);\n    s.push(16);\n    s.push(23);\n    s.push(42);\n\n    while(s.empty()) {\n        s.pop();\n    }\n\n    return 0;\n}\n```\n\n如您所见，`std::stack`和我们的自定义堆栈唯一的区别是我们的堆栈是线程安全的。\n\n# 同步包装器以及如何实现它们\n\n在本食谱中，我们将学习如何制作线程安全的同步包装器。默认情况下，C++ 标准库不是线程安全的，因为不是所有的应用都需要这个功能。确保 C++ 标准库线程安全的一种机制是创建一个线程安全的类，它将您希望使用的数据结构以及`std::mutex`作为私有成员添加到类中，然后重新实现数据结构的函数，首先获取`std::mutex`，然后将函数调用转发到数据结构。这种方法的问题是，如果数据结构是一个全局资源，那么会有很多额外的代码添加到您的程序中，使得生成的代码难以阅读和维护。\n\n这个方法很重要，因为它将演示如何通过制作线程安全的同步包装来解决代码中的这些问题。\n\n# 准备好\n\n在我们开始之前，请确保满足所有技术要求，包括安装 Ubuntu 18.04 或更高版本，并在终端窗口中运行以下内容:\n\n```cpp\n> sudo apt-get install build-essential git\n```\n\n这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后，打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。\n\n# 怎么做...\n\n您需要执行以下步骤来尝试此食谱:\n\n1.  从新的终端，运行以下命令下载源代码:\n\n```cpp\n> cd ~/\n> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git\n> cd Advanced-CPP-CookBook/chapter05\n```\n\n2.  要编译源代码，请运行以下命令:\n\n```cpp\n> cmake .\n> make recipe05_examples\n```\n\n3.  编译源代码后，您可以通过运行以下命令来执行该配方中的每个示例:\n\n```cpp\n> ./recipe05_example01\n```\n\n在下一节中，我们将逐一介绍这些示例，并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。\n\n# 它是如何工作的...\n\n在本食谱中，我们将学习如何创建线程安全的同步包装器，它允许我们将线程安全添加到 C++ 标准库数据结构中，默认情况下，这些数据结构不是线程安全的。\n\n为此，我们将为打算使用的 C++ 标准库中的每个函数创建包装函数。这些包装函数将首先尝试获取`std::mutex`，然后将相同的函数调用转发到 C++ 标准库数据结构。\n\n为此，请考虑下面的代码示例:\n\n```cpp\n#include <mutex>\n#include <stack>\n#include <iostream>\n\nstd::mutex m{};\n\ntemplate<typename S, typename T>\nvoid push(S &s, T &&t)\n{\n    std::lock_guard lock(m);\n    s.push(std::forward<T>(t));\n}\n\ntemplate<typename S>\nvoid pop(S &s)\n{\n    std::lock_guard lock(m);\n    s.pop();\n}\n\ntemplate<typename S>\nauto empty(S &s)\n{\n    std::lock_guard lock(m);\n    return s.empty();\n}\n```\n\n在前面的例子中，我们已经为`push()`、`pop()`和`empty()`函数创建了一个包装函数。这些函数试图在调用数据结构之前获取我们的全局`std::mutex`对象，在本例中，数据结构是一个模板。模板的使用创造了所谓的概念。我们的包装函数可以被任何实现`push()`、`pop()`和`empty()`的数据结构使用。另外，请注意，我们在`push()`函数中使用`std::forward`来确保被推参数的 l 值和 CV 限定符保持不变。\n\n最后，我们可以像使用数据结构的函数一样使用包装器，唯一的区别是数据结构作为第一个参数传递。例如，看看下面的代码块:\n\n```cpp\nint main(void)\n{\n    std::stack<int> mystack;\n\n    push(mystack, 4);\n    push(mystack, 8);\n    push(mystack, 15);\n    push(mystack, 16);\n    push(mystack, 23);\n    push(mystack, 42);\n\n    while(empty(mystack)) {\n        pop(mystack);\n    }\n\n    return 0;\n}\n```\n\n正如您在前面的示例中看到的，我们的同步包装器的使用很简单，同时确保我们创建的堆栈现在是线程安全的。\n\n# 阻塞操作与异步编程\n\n在本食谱中，我们将学习阻塞操作和异步操作之间的区别。这个方法很重要，因为阻塞操作会序列化单个 CPU 上每个操作的执行。如果每个操作的执行必须以串行顺序执行，这通常没问题；但是，如果这些操作可以并行执行，异步编程可能是一种有用的优化，确保在一个操作等待的同时，其他操作仍然可以在同一个 CPU 上执行。\n\n# 准备好\n\n在我们开始之前，请确保满足所有技术要求，包括安装 Ubuntu 18.04 或更高版本，并在终端窗口中运行以下内容:\n\n```cpp\n> sudo apt-get install build-essential git\n```\n\n这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后，打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。\n\n# 怎么做...\n\n您需要执行以下步骤来尝试此食谱:\n\n1.  从新的终端，运行以下命令下载源代码:\n\n```cpp\n> cd ~/\n> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git\n> cd Advanced-CPP-CookBook/chapter05\n```\n\n2.  要编译源代码，请运行以下命令:\n\n```cpp\n> cmake .\n> make recipe06_examples\n```\n\n3.  编译源代码后，您可以通过运行以下命令来执行该配方中的每个示例:\n\n```cpp\n> time ./recipe06_example01\n999999\n999999\n999999\n999999\n\nreal 0m1.477s\n...\n\n> time ./recipe06_example02\n999999\n999999\n999999\n999999\n\nreal 0m1.058s\n...\n\n> time ./recipe06_example03\n999999\n999999\n999998\n999999\n\nreal 0m1.140s\n...\n```\n\n在下一节中，我们将逐一介绍这些示例，并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。\n\n# 它是如何工作的...\n\n阻塞操作是必须在下一个操作发生之前完成的操作。大多数程序是串行编写的，这意味着每条指令必须在下一条指令之前执行。然而，问题是有些操作可以并行执行(即并发或异步执行)。在最好的情况下，序列化这些操作会导致较差的性能，并且在某些情况下，如果阻塞的操作正在等待另一个永远没有机会执行的操作，则实际上会导致死锁(程序进入无休止的等待状态)。\n\n为了演示阻塞操作，让我们检查以下内容:\n\n```cpp\n#include <vector>\n#include <iostream>\n#include <algorithm>\n\nconstexpr auto size = 1000000;\n\nint main(void)\n{\n    std::vector<int> numbers1(size);\n    std::vector<int> numbers2(size);\n    std::vector<int> numbers3(size);\n    std::vector<int> numbers4(size);\n```\n\n前面的代码创建了一个主函数，其中有四个`int`类型的`std::vector`对象。在以下步骤中，我们将使用这些向量来演示阻塞操作:\n\n1.  首先，我们创建四个可以存储整数的向量:\n\n```cpp\n    std::generate(numbers1.begin(), numbers1.end(), []() {\n      return rand() % size;\n    });\n    std::generate(numbers2.begin(), numbers2.end(), []() {\n      return rand() % size;\n    });\n    std::generate(numbers3.begin(), numbers3.end(), []() {\n      return rand() % size;\n    });\n    std::generate(numbers4.begin(), numbers4.end(), []() {\n      return rand() % size;\n    });\n```\n\n2.  接下来，我们使用`std::generate`用随机数填充每个数组，这产生了一个带有数字和随机顺序的数组:\n\n```cpp\n    std::sort(numbers1.begin(), numbers1.end());\n    std::sort(numbers2.begin(), numbers2.end());\n    std::sort(numbers3.begin(), numbers3.end());\n    std::sort(numbers4.begin(), numbers4.end());\n```\n\n3.  接下来，我们对整数数组进行排序，这是本例的主要目标，因为执行此操作需要一段时间:\n\n```cpp\n    std::cout << numbers1.back() << '\\n';\n    std::cout << numbers2.back() << '\\n';\n    std::cout << numbers3.back() << '\\n';\n    std::cout << numbers4.back() << '\\n';\n\n    return 0;\n}\n```\n\n4.  最后，我们输出每个数组中的最后一个条目，通常是`999999`(但不一定是，因为数字是使用随机数生成器生成的)。\n\n前面例子的问题是操作可以并行执行，因为每个数组都是独立的。为了解决这个问题，我们可以异步执行这些操作，这意味着数组将被并行创建、填充、排序和输出。例如，考虑以下代码:\n\n```cpp\n#include <future>\n#include <thread>\n#include <vector>\n#include <iostream>\n#include <algorithm>\n\nconstexpr auto size = 1000000;\n\nint foo()\n{\n    std::vector<int> numbers(size);\n    std::generate(numbers.begin(), numbers.end(), []() {\n      return rand() % size;\n    });\n\n    std::sort(numbers.begin(), numbers.end());\n    return numbers.back();\n}\n```\n\n我们要做的第一件事是实现一个名为`foo()`的函数，该函数创建我们的向量，用随机数填充它，对列表进行排序，并返回数组中的最后一个条目(与前面的示例相同，只是我们一次只处理一个数组，而不是`4`):\n\n```cpp\nint main(void)\n{\n    auto a1 = std::async(std::launch::async, foo);\n    auto a2 = std::async(std::launch::async, foo);\n    auto a3 = std::async(std::launch::async, foo);\n    auto a4 = std::async(std::launch::async, foo);\n\n    std::cout << a1.get() << '\\n';\n    std::cout << a2.get() << '\\n';\n    std::cout << a3.get() << '\\n';\n    std::cout << a4.get() << '\\n';\n\n    return 0;\n}\n```\n\n然后我们使用`std::async`执行这个`foo()`函数四次，得到相同的四个数组，就像我们前面的例子一样。本例中的`std::async()`函数与手动执行四个线程的功能相同。`std::aync()`的结果是一个`std::future`对象，一旦函数完成执行，它就存储函数的结果。我们在这个例子中做的最后一件事是使用`get()`函数，一旦函数准备好，就返回它的值。\n\n如果我们对这些函数的结果进行计时，我们可以看到异步版本比阻塞版本更快。下面的代码显示了这一点(`real`时间是寻找的时间):\n\n![](img/46ef0e32-b06c-4bc6-9b92-5984d00d7432.png)\n\n`std::async()`函数也可以用来在同一个线程中异步执行我们的数组函数。例如，考虑以下代码:\n\n```cpp\nint main(void)\n{\n    auto a1 = std::async(std::launch::deferred, foo);\n    auto a2 = std::async(std::launch::deferred, foo);\n    auto a3 = std::async(std::launch::deferred, foo);\n    auto a4 = std::async(std::launch::deferred, foo);\n\n    std::cout << a1.get() << '\\n';\n    std::cout << a2.get() << '\\n';\n    std::cout << a3.get() << '\\n';\n    std::cout << a4.get() << '\\n';\n\n    return 0;\n}\n```\n\n正如你在前面的例子中看到的，我们将操作从`std::launch::async`改为`std::launch::deferred`，这导致每个函数在需要函数的结果时(即调用`get()`函数时)执行一次。如果您不确定函数是否需要首先执行(也就是说，只在需要时执行函数)，这很有用，缺点是程序的执行速度较慢，因为线程通常不被用作优化方法。\n\n# 带着承诺和未来工作\n\n在这个食谱中，我们将学习如何使用 C++ 承诺和未来。C++ `promise`是 C++ 线程的参数，而 C++ `future`是线程的返回值，可以用来手动实现`std::async`调用的相同功能。这个方法很重要，因为对`std::aync`的调用要求每个线程停止执行以获得其结果，而手动实现 C++ `promise`和 **`future`** 允许用户在线程仍在执行时获得线程的返回值。\n\n# 准备好\n\n在我们开始之前，请确保满足所有技术要求，包括安装 Ubuntu 18.04 或更高版本，并在终端窗口中运行以下内容:\n\n```cpp\n> sudo apt-get install build-essential git\n```\n\n这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后，打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。\n\n# 怎么做...\n\n您需要执行以下步骤来尝试此食谱:\n\n1.  从新的终端，运行以下命令下载源代码:\n\n```cpp\n> cd ~/\n> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git\n> cd Advanced-CPP-CookBook/chapter05\n```\n\n2.  要编译源代码，请运行以下命令:\n\n```cpp\n> cmake .\n> make recipe07_examples\n```\n\n3.  编译源代码后，您可以通过运行以下命令来执行该配方中的每个示例:\n\n```cpp\n> ./recipe07_example01\nThe answer is: 42\n\n> ./recipe07_example02\nThe answer is: 42\n```\n\n在下一节中，我们将逐一介绍这些示例，并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。\n\n# 它是如何工作的...\n\n在这个食谱中，我们将学习如何手动使用 C++ `promise`和`future`来提供一个与参数并行执行的函数，以及获取函数的返回值。首先，让我们用下面的代码来演示这是如何以最简单的形式完成的:\n\n```cpp\n#include <thread>\n#include <iostream>\n#include <future>\n\nvoid foo(std::promise<int> promise)\n{\n    promise.set_value(42);\n}\n\nint main(void)\n{\n    std::promise<int> promise;\n    auto future = promise.get_future();\n\n    std::thread t{foo, std::move(promise)};\n    t.join();\n\n    std::cout << \"The answer is: \" << future.get() << '\\n';\n\n    return 0;\n}\n```\n\n上面的示例在执行时会产生以下结果:\n\n![](img/5313a9ee-d6f1-449f-90df-069c182a2a80.png)\n\n正如您在前面的代码中看到的，C++ `promise`是线程化函数的参数。线程通过设置`promise`参数返回其值，该参数又设置了一个 C++ `future`，用户可以从它提供给线程的`promise`参数中获得该值。需要注意的是，我们使用`std::move()`来防止`promise`参数被复制(编译器会禁止，因为 C++ `promise`是一个只移动的类)。最后，我们使用`get()`函数获得线程的结果，就像使用`std::async`获得线程执行的结果一样。\n\n手动使用`promise`和`future`的好处之一就是可以在线程完成之前得到线程的结果，让线程继续做功。例如，看看以下内容:\n\n```cpp\n#include <thread>\n#include <iostream>\n#include <future>\n\nvoid foo(std::promise<int> promise)\n{\n    promise.set_value(42);\n    while (true);\n}\n\nint main(void)\n{\n    std::promise<int> promise;\n    auto future = promise.get_future();\n\n    std::thread t{foo, std::move(promise)};\n\n    future.wait();\n    std::cout << \"The answer is: \" << future.get() << '\\n';\n\n    t.join();\n\n    // Never reached\n    return 0;\n}\n```\n\n执行时会产生以下结果:\n\n![](img/af9f0ada-0fe3-4d17-9c75-52f61975d425.png)\n\n在前面的例子中，我们创建了同一个线程，但是我们在线程中永远循环，这意味着线程永远不会返回。然后我们以同样的方式创建线程，但是一旦线程准备好就输出 C++ `future`的结果，我们可以使用`wait()`函数来确定。"
  },
  {
    "path": "docs/adv-cpp-prog-cb/06.md",
    "content": "# 六、优化代码以提高性能\n\n优化代码以提高性能可以确保代码充分利用 C++ 所能提供的功能。与其他高级语言不同，C++ 能够在不牺牲性能的情况下提供高级语法自由，尽管不可否认这是以更高的学习曲线为代价的。\n\n这一章很重要，因为它将演示优化代码的更高级方法，包括如何在单元级别对软件进行基准测试，如何检查编译器为潜在优化生成的结果汇编代码，如何减少应用正在使用的内存资源数量，以及为什么像`noexcept`这样的编译器提示很重要。读完这一章，你将有能力写出更高效的 C++。\n\n在本章中，我们将介绍以下食谱:\n\n*   对你的代码进行基准测试\n*   查看汇编代码\n*   减少内存分配的数量\n*   声明 noexcept\n\n# 技术要求\n\n要编译和运行本章中的示例，您必须拥有运行 Ubuntu 18.04 的计算机的管理权限，并且具有功能性互联网连接。在运行这些示例之前，您必须安装以下内容:\n\n```cpp\n> sudo apt-get install build-essential git cmake valgrind\n```\n\n如果这安装在 Ubuntu 18.04 以外的任何操作系统上，则需要 GCC 7.4 或更高版本以及 CMake 3.6 或更高版本。\n\n# 对你的代码进行基准测试\n\n在这个食谱中，你将学习如何基准测试和优化你的源代码。优化源代码将产生更高效的 C++，从而延长电池寿命，提高性能，等等。这个方法很重要，因为优化源代码的过程始于确定您计划优化的资源，包括速度、内存甚至功耗。如果没有基准测试工具，就很难比较同一问题的不同方法。\n\nC++ 程序员可以使用无数的基准测试工具(任何衡量程序单一属性的工具)，包括诸如 Boost、Folly 和 Abseil 等 c++ API，以及英特尔的 vTune 等 CPU 专用工具。还有一些分析工具(任何有助于理解程序行为的工具)，如 valgrind 和 gprof。在这个食谱中，我们将重点介绍其中的两个:Hayai 和 Valgrind。Hayai 提供了一个简单的微基准测试的例子，而 Valgrind 提供了一个更完整的，虽然更复杂的，动态分析/剖析工具的例子。\n\n# 准备好\n\n开始之前，请确保满足所有技术要求，包括安装 Ubuntu 18.04 或更高版本，并在终端窗口中运行以下内容:\n\n```cpp\n> sudo apt-get install build-essential git valgrind cmake\n```\n\n这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后，打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。\n\n# 怎么做...\n\n执行以下步骤完成该配方:\n\n1.  从新的终端，运行以下命令下载源代码:\n\n```cpp\n> cd ~/\n> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git\n> cd Advanced-CPP-CookBook/chapter06\n```\n\n2.  要编译源代码，请运行以下命令:\n\n```cpp\n> cmake -DCMAKE_BUILD_TYPE=Debug .\n> make recipe01_examples\n```\n\n3.  编译完源代码后，您可以通过运行以下命令来执行该配方中的每个示例:\n\n```cpp\n> ./recipe01_example01\n[==========] Running 2 benchmarks.\n[ RUN ] vector.push_back (10 runs, 100 iterations per run)\n[ DONE ] vector.push_back (0.200741 ms)\n...\n[ RUN ] vector.emplace_back (10 runs, 100 iterations per run)\n[ DONE ] vector.emplace_back (0.166699 ms)\n...\n\n> ./recipe01_example02\n```\n\n在下一节中，我们将逐一介绍这些示例，并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。\n\n# 它是如何工作的...\n\n应用于 C++ 的最常见的优化是执行速度。为了优化 C++ 的速度，我们必须从开发解决同一问题的不同方法开始，然后对每个解决方案进行基准测试，以确定哪个解决方案执行速度最快。基准测试工具，如 GitHub 上基于 C++ 的基准测试库 Hayai，有助于做出这一决定。为了解释这一点，让我们看一个简单的例子:\n\n```cpp\n#include <string>\n#include <vector>\n#include <hayai.hpp>\n\nstd::vector<std::string> data;\n\nBENCHMARK(vector, push_back, 10, 100)\n{\n    data.push_back(\"The answer is: 42\");\n}\n\nBENCHMARK(vector, emplace_back, 10, 100)\n{\n    data.emplace_back(\"The answer is: 42\");\n}\n```\n\n当我们执行前面的代码时，我们得到以下输出:\n\n![](img/4b7883ec-9592-4fde-bb6f-bcc9465077c4.jpg)\n\n在前面的例子中，我们使用 Hayai 库来测试使用`push_back()`和`emplace_back()`向向量添加字符串之间的性能差异。`push_back()`和`emplace_back()`的区别在于`push_back()`创建对象，然后将其复制或移动到矢量中，而`emplace_back()`在矢量本身中创建对象，不需要临时对象和后续的复制/移动。也就是说，如果使用`push_back()`，必须构建一个对象，然后要么复制，要么移动到向量中。如果使用`emplace_back()`，对象构造简单。不出所料，`emplace_back()`的表现优于`push_back()`，这也是为什么铿锵-Tidy 等工具会尽可能推荐使用`emplace_back()`而不是`push_back()`的原因。\n\n像 Hayai 这样的基准库使用简单，在帮助程序员优化源代码方面非常有效，并且不仅能够对速度进行基准测试，还能够对资源使用进行基准测试。这些库的问题在于它们在*单元*级别得到更好的利用，而不是在*集成*和*系统*级别；也就是说，为了测试整个可执行文件，这些库不太适合帮助程序员，因为随着测试规模的增加，它们不能很好地扩展。为了分析一个完整的可执行文件而不是一个单独的函数，像 Valgrind 这样的工具是存在的，它们可以帮助您分析在优化方面哪些函数最需要关注。从那里，可以使用基准测试工具来分析最需要关注的功能。\n\nValgrind 是一个动态分析工具，能够检测内存泄漏并跟踪程序的执行。为了看到这一点，让我们来看看下面的例子:\n\n```cpp\nvolatile int data = 0;\n\nvoid foo()\n{\n data++ ;\n}\n\nint main(void)\n{\n for (auto i = 0; i < 100000; i++) {\n foo();\n }\n}\n```\n\n在前面的例子中，我们从名为`foo()`的函数中增加一个全局变量(标记为 volatile，以确保编译器不会优化掉该变量)，然后执行该函数`100,000`次。要分析这个例子，运行以下命令(使用`callgrind`输出每个函数在程序中被调用的次数):\n\n```cpp\n> valgrind --tool=callgrind ./recipe01_example02\n> callgrind_annotate callgrind.out.*\n```\n\n这将产生以下输出:\n\n![](img/f6aac4e5-2af5-426e-a176-9da59a0a379b.png)\n\n如我们所见，`foo()`函数列在前一个输出的顶部附近(动态链接器的`_dl_lookup_symbol_x()`函数被调用最多，用于在执行前链接程序)。需要注意的是，程序将`foo()`功能的指令总数列为`800,000`(在左侧)。这是由于`foo()`功能是`8`装配指令长并且被执行`100,000`次。例如，让我们看看使用`objdump`的`foo()`函数的汇编(一种能够输出可执行文件的编译汇编的工具)，如下所示:\n\n![](img/f3a341c8-cf01-4265-9e74-e2496c465733.png)\n\n使用 Valgrind，可以对可执行文件进行分析，以确定哪些函数执行时间最长。比如我们来看看`ls`:\n\n```cpp\n> valgrind --tool=callgrind ls\n> callgrind_annotate callgrind.out.*\n```\n\n这将产生以下输出:\n\n![](img/050bd8f5-9adc-4825-b4db-fc8970010080.png)\n\n我们可以看到，`strcmp`函数被调用了很多。这些信息可以与*单元*级别的基准应用编程接口相结合，以确定是否可以编写更快版本的`strcmp`(例如，使用手写汇编和特殊的中央处理器指令)。使用 Hayai 和 Valgrind 等工具，可以隔离出程序中哪些函数消耗了最多的 CPU、内存甚至电源，并重写它们以提供更好的性能，同时将精力集中在将提供最佳投资回报的优化上。\n\n# 查看汇编代码\n\n在本食谱中，我们将看看两种不同优化的结果程序集:循环展开和按引用传递参数。这个食谱很重要，因为它将教你如何更深入地研究编译器如何将 C++ 转换成可执行代码。这些信息将阐明为什么 C++ 规范(如 C++ 核心指南)会提出关于优化和性能的建议。当您试图编写更好的 C++ 代码时，这通常是至关重要的，尤其是当您想要优化它时。\n\n# 准备好\n\n开始之前，请确保满足所有技术要求，包括安装 Ubuntu 18.04 或更高版本，并在终端窗口中运行以下内容:\n\n```cpp\n> sudo apt-get install build-essential git cmake\n```\n\n这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后，打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。\n\n# 怎么做...\n\n执行以下步骤完成该配方:\n\n1.  从新的终端，运行以下命令下载源代码:\n\n```cpp\n> cd ~/\n> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git\n> cd Advanced-CPP-CookBook/chapter06\n```\n\n2.  要编译源代码，请运行以下命令:\n\n```cpp\n> cmake -DCMAKE_BUILD_TYPE=Debug .\n> make recipe02_examples\n```\n\n3.  编译完源代码后，您可以通过运行以下命令来执行该配方中的每个示例:\n\n```cpp\n> ./recipe02_example01\n\n> ./recipe02_example02\n\n> ./recipe02_example03\n\n> ./recipe02_example04\n\n> ./recipe02_example05\n```\n\n在下一节中，我们将逐一介绍这些示例，并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。\n\n# 它是如何工作的...\n\n学习如何优化 C++ 代码的最好方法之一是学习如何分析编译器在编译后生成的结果汇编代码。在这个食谱中，我们将通过观察两个不同的例子来了解这个分析是如何完成的:循环展开和通过引用传递参数。\n\n在我们看这些例子之前，让我们看一个简单的例子:\n\n```cpp\nint main(void)\n{ }\n```\n\n在前面的例子中，我们只有一个`main()`函数。我们没有包含任何 C 或 C++ 库，`main()`函数本身是空的。如果我们编译这个例子，我们会看到生成的二进制文件仍然很大:\n\n![](img/99ace8d4-1e3a-45be-a24f-c318040b45eb.png)\n\n在这种情况下，示例的大小为`22kb`。为了显示编译器为此代码生成的结果程序集，我们可以执行以下操作:\n\n```cpp\n> objdump -d recipe02_example01\n```\n\n前面命令的结果输出应该会令人惊讶，因为应用中有很多代码什么都不做。\n\n为了更好地了解代码的真实数量，我们可以使用`grep`来细化输出，这是一个允许我们从任何命令中过滤文本的工具。让我们看看代码中的所有函数:\n\n![](img/d17573c5-b2e5-4905-a7a5-29b065898a0d.png)\n\n正如我们所看到的，编译器会自动为您在代码中添加几个函数。这包括`_init()`、`_fini()`和`_start()`功能。我们也可以看一个特定的函数，比如我们的主函数，如下所示:\n\n![](img/1b25fad2-bea6-4f4c-a87b-adb0e85f399d.png)\n\n在前面的例子中，我们在`objdump`的输出中搜索`main>:`和`RETQ`。所有函数名都以`>:`结尾，每个函数的最后一条指令(通常)是英特尔 64 位系统上的`RETQ`。\n\n以下是生成的程序集:\n\n```cpp\n  401106: push %rbp\n  401107: mov %rsp,%rbp\n```\n\n首先，它将当前堆栈帧指针(`rbp`)存储到堆栈中，并为`main()`函数加载带有堆栈当前地址(`rsp`)的堆栈帧指针。\n\n这可以在每个函数中看到，称为函数的序言。`main()`执行的唯一代码是`return 0`，由编译器自动添加到代码中:\n\n```cpp\n  40110a: mov $0x0,%eax\n```\n\n最后，这个函数中的最后一个程序集包含函数的 epilog，它恢复堆栈帧指针并返回:\n\n```cpp\n\n  40110f: pop %rbp\n  401110: retq\n```\n\n现在，我们已经更好地理解了如何获取和读取编译后的 C++ 的结果程序集，让我们来看一个循环展开的示例，这是用没有循环的指令的等效版本替换指令循环的过程。为此，请使用以下命令进行配置，确保在发布模式下编译示例(即启用编译器优化):\n\n```cpp\n> cmake -DCMAKE_BUILD_TYPE=Release .\n> make\n```\n\n为了理解循环展开，让我们看看下面的代码:\n\n```cpp\nvolatile int data[1000];\n\nint main(void)\n{\n    for (auto i = 0U; i < 1000; i++) {\n        data[i] = 42;\n    }\n}\n```\n\n当编译器遇到循环时，它生成的结果程序集包含以下代码:\n\n![](img/4414aa74-9275-444b-bf7a-a8a150a4a426.png)\n\n让我们把它分解一下:\n\n```cpp\n  401020: xor %eax,%eax\n  401022: nopw 0x0(%rax,%rax,1)\n```\n\n前两条指令属于代码的`for (auto i = 0U;`部分。在这种情况下，`i`变量存储在`EAX`寄存器中，并使用`XOR`指令设置为`0`(英特尔的`XOR`指令比`MOV`指令更快地将寄存器设置为 0)。`NOPW`指令可以安全忽略。\n\n接下来的几条指令是交错的，如下所示:\n\n```cpp\n  401028: mov %eax,%edx\n  40102a: add $0x1,%eax\n  40102d: movl $0x2a,0x404040(,%rdx,4)\n```\n\n这些指令代表`i++ ;`和`data[i] = 42;`代码。第一条指令存储`i`变量的当前值，然后在将`42`存储到由`i`索引的存储地址之前，将其递增 1。方便的是，这个结果程序集展示了一个可能的优化机会，因为编译器可以使用以下内容实现相同的功能:\n\n```cpp\n movl $0x2a,0x404040(,%rax,4)\n add $0x1,%eax\n```\n\n前面的代码在执行`i++ `之前存储了值`42`，因此不需要以下内容:\n\n```cpp\n  mov %eax,%edx\n```\n\n有许多方法可以实现这种潜在的优化，包括使用不同的编译器或手写程序集。下一组指令执行我们的`for`循环的`i < 1000;`部分:\n\n```cpp\n  401038: cmp $0x3e8,%eax\n  40103d: jne 401028 <main+0x8>\n```\n\n`CMP`指令检查`i`变量是否为`1000`，如果不是，则使用`JNE`指令跳到函数顶部继续循环。否则，剩余的代码将执行:\n\n```cpp\n  40103f: xor %eax,%eax\n  401041: retq \n```\n\n为了了解循环展开是如何工作的，让我们将循环的迭代次数从`1000`更改为`4`，如下所示:\n\n```cpp\nvolatile int data[4];\n\nint main(void)\n{\n    for (auto i = 0U; i < 4; i++) {\n        data[i] = 42;\n    }\n}\n```\n\n正如我们所看到的，除了循环的迭代次数之外，代码是相同的。产生的组件如下:\n\n![](img/4414aa74-9275-444b-bf7a-a8a150a4a426.png)\n\n我们可以看到，`CMP`和`JNE`指令缺失。现在，下面的代码被编译(*但是还有更多！*):\n\n```cpp\n    for (auto i = 0U; i < 4; i++) {\n        data[i] = 42;\n    }\n```\n\n编译后的代码转换为以下代码:\n\n```cpp\n        data[0] = 42;\n        data[1] = 42;\n        data[2] = 42;\n        data[3] = 42;\n```\n\n`return 0;`显示在分配之间的装配中。这是允许的，因为函数的返回值与赋值无关(因为赋值指令从不接触`RAX`，这为 CPU 提供了额外的优化(因为它可以并行执行`return 0;`，尽管这是本书范围之外的话题)。应该注意的是，循环展开不需要使用少量的循环迭代。一些编译器会部分展开一个循环来实现优化(例如，一次以`4`而不是`1`为组执行循环)。\n\n我们的最后一个示例将关注按引用传递，而不是按值传递。要启动，请在调试模式下重新编译代码:\n\n```cpp\n> cmake -DCMAKE_BUILD_TYPE=Debug .\n> make\n```\n\n让我们看看下面的例子:\n\n```cpp\nstruct mydata {\n    int data[100];\n};\n\nvoid foo(mydata d)\n{\n    (void) d;\n}\n\nint main(void)\n{\n    mydata d;\n    foo(d);\n}\n```\n\n在这个例子中，我们创建了一个大的结构，并通过值传递给我们主函数中名为`foo()`的函数。主要功能的结果组合如下:\n\n![](img/45b46604-3410-4880-b1de-7ed7b6ecbd4b.png)\n\n上例中的重要说明如下:\n\n```cpp\n  401137: rep movsq %ds:(%rsi),%es:(%rdi)\n  40113a: callq 401106 <_Z3foo6mydata>\n```\n\n前面的指令将大结构复制到堆栈中，然后调用我们的`foo()`函数。发生复制是因为结构是通过值传递的，这意味着编译器必须执行复制。另外，如果您希望看到可读格式而不是损坏格式的输出，请在选项中添加`C`，如下所示:\n\n![](img/1e602d6c-13f7-4a30-9494-09b95deb705c.png)\n\n最后，让我们通过引用来看看由此带来的改进:\n\n```cpp\nstruct mydata {\n    int data[100];\n};\n\nvoid foo(mydata &d)\n{\n    (void) d;\n}\n\nint main(void)\n{\n    mydata d;\n    foo(d);\n}\n```\n\n正如我们所看到的，我们通过引用而不是通过值来传递结构。产生的组件如下:\n\n![](img/60ea97a9-3744-48ce-bc91-000a09a842b0.png)\n\n在这里，代码少得多，导致执行速度更快。正如我们所了解到的，如果我们希望了解编译器正在产生什么，检查编译器产生什么是有效的，因为这提供了更多关于您可以进行哪些潜在更改来编写更高效的 C++ 代码的信息。\n\n# 减少内存分配的数量\n\n当应用运行时，隐藏内存分配一直由 C++ 产生。这个食谱将教你如何确定 C++ 何时分配内存，以及如何在可能的情况下移除这些分配。了解如何移除内存分配很重要，因为像`new()`、`delete()`、`malloc()`和`free()`这样的函数不仅速度慢，而且它们提供的内存也是有限的。删除不需要的分配不仅可以提高应用的整体性能，还有助于降低其整体内存需求。\n\n# 准备好\n\n开始之前，请确保满足所有技术要求，包括安装 Ubuntu 18.04 或更高版本，并在终端窗口中运行以下内容:\n\n```cpp\n> sudo apt-get install build-essential git valgrind cmake\n```\n\n这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后，打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。\n\n# 怎么做...\n\n执行以下步骤完成该配方:\n\n1.  从新的终端，运行以下命令下载源代码:\n\n```cpp\n> cd ~/\n> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git\n> cd Advanced-CPP-CookBook/chapter06\n```\n\n2.  要编译源代码，请运行以下命令:\n\n```cpp\n> cmake .\n> make recipe03_examples\n```\n\n3.  编译完源代码后，您可以通过运行以下命令来执行该配方中的每个示例:\n\n```cpp\n> ./recipe03_example01\n\n> ./recipe03_example02\n\n> ./recipe03_example03\n\n> ./recipe03_example04\n\n> ./recipe03_example05\n\n> ./recipe03_example06\n\n> ./recipe03_example07\n```\n\n在下一节中，我们将逐一介绍这些示例，并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。\n\n# 它是如何工作的...\n\n在这个食谱中，我们将学习如何监控一个应用消耗了多少内存，以及 C++ 在幕后分配内存的不同方式。首先，让我们看一个简单的应用，它什么也不做:\n\n```cpp\nint main(void)\n{\n}\n```\n\n正如我们所看到的，这个应用什么也不做。要查看应用使用了多少内存，我们将使用动态分析工具 Valgrind，如下所示:\n\n![](img/4b394af7-4501-4c48-bfd4-20e1f216d0de.png)\n\n如上例所示，我们的应用已经分配了堆内存(即使用`new()` / `delete()`或`malloc()` / `free()`分配的内存)。为了确定这个分配发生在哪里，让我们再次使用 Valgrind，但是这一次，我们将启用一个名为 **Massif** 的工具，它将跟踪内存分配来自哪里:\n\n![](img/def07c68-3679-40b0-ae34-b2b5ebb1757c.png)\n\n要查看前面示例的输出，我们必须输出一个自动为我们创建的文件:\n\n```cpp\n> cat massif.out.*\n```\n\n这导致我们检索以下输出:\n\n![](img/2434cd14-83e9-4761-b2d4-a3f3616eb879.png)\n\n我们可以看到，动态链接器的`init()`函数正在执行分配，大小为`72,704`字节。为了进一步演示如何使用 Valgrind，让我们看一下这个简单的例子，在这里我们执行自己的分配:\n\n```cpp\nint main(void)\n{\n    auto ptr = new int;\n    delete ptr;\n}\n```\n\n要查看前面源代码的内存分配，我们需要再次运行 Valgrind:\n\n![](img/5ca77107-fafd-479a-8c84-a660ea785dd3.png)\n\n可以看到，我们已经分配了`72,708`字节。因为我们知道应用会自动为我们分配`72,704`字节，所以我们可以看到 Valgrind 已经成功检测到我们分配的`4`字节(在运行 Linux 的英特尔 64 位系统上的整数大小)。要了解这种分配发生在哪里，让我们再次使用 Massif:\n\n![](img/d1b7826e-721d-44ad-8c3a-dbc55d4cc5e1.png)\n\n正如我们所看到的，我们已经将`--threshold=0.1`添加到命令行选项中，因为这告诉 Valgrind，任何构成`.1%`分配的分配都应该被记录。让我们来看看结果(T3 程序只是将文件的内容回显到控制台):\n\n```cpp\n> cat massif.out.*\n```\n\n通过这样做，我们得到以下输出:\n\n![](img/8e0189c6-c5a1-44bb-a848-4d752d4bee3a.png)\n\n正如我们所看到的，Valgrind 已经检测到了来自`init()`函数以及我们的`main()`函数的内存分配。\n\n现在，我们已经知道如何分析我们的应用进行的内存分配，让我们看看一些不同的 c++ API，看看它们在幕后进行什么类型的内存分配。首先，我们来看一个`std::vector`，如下:\n\n```cpp\n#include <vector>\nstd::vector<int> data;\n\nint main(void)\n{\n    for (auto i = 0; i < 10000; i++) {\n        data.push_back(i);\n    }\n}\n```\n\n这里，我们创建了一个整数的全局向量，然后将`10,000`个整数添加到向量中。使用 Valgrind，我们得到以下输出:\n\n![](img/01e29595-e5db-4190-8321-93c0973e49cb.png)\n\n在这里，我们可以看到 16 个分配，总共有`203,772`字节。我们知道应用将为我们分配`72,704`字节，所以我们必须从总数中删除它，为我们留下`131,068`字节的内存。我们也知道我们分配了`10,000`整数，总共是`40,000`字节。那么，问题是，其他`91,068`字节是从哪里来的呢？\n\n答案在于`std::vector`是如何在引擎盖下运作的。`std::vector`必须确保始终连续查看内存，这意味着当发生插入并且`std::vector`空间不足时，它必须分配一个新的、更大的缓冲区，然后将旧缓冲区的内容复制到新缓冲区中。问题是`std::vector`不知道当所有插入完成时缓冲区的总大小是多少，所以当执行第一次插入时，它会创建一个小缓冲区以确保不会浪费内存，然后随着向量的增长以小增量增加`std::vector`的大小，从而导致几个内存分配和内存副本。\n\n为了防止这种分配的发生，C++ 提供了`reserve()`函数，该函数为用户提供了一个`std::vector`来估计用户认为他们需要多少内存。例如，考虑以下代码:\n\n```cpp\n#include <vector>\nstd::vector<int> data;\n\nint main(void)\n{\n    data.reserve(10000);  // <--- added optimization \n\n    for (auto i = 0; i < 10000; i++) {\n        data.push_back(i);\n    }\n}\n```\n\n上一个例子中的代码和上一个例子中的一样，不同的是我们增加了对`reserve()`函数的调用，它告诉`std::vector`我们认为向量会有多大。Valgrind 的输出如下:\n\n![](img/726ac6f0-8701-40ea-b53e-310902914389.png)\n\n我们可以看到，应用分配了`112,704`字节。如果我们移除应用默认创建的`72,704`字节，我们将剩下`40,000`字节，这是我们期望的确切大小(因为我们将`10,000`整数添加到向量中，每个整数都是`4`字节大小)。\n\n数据结构不是执行隐藏分配的唯一类型的 C++ 标准库 API。我们来看一个`std::any`，如下:\n\n```cpp\n#include <any>\n#include <string>\n\nstd::any data;\n\nint main(void)\n{\n    data = 42;\n    data = std::string{\"The answer is: 42\"};\n}\n```\n\n在这个例子中，我们创建了一个`std::any`并给它分配了一个整数和一个`std::string`。让我们看看 Valgrind 的输出:\n\n![](img/56140dbb-bf2e-4670-82e2-15eb7134ce6d.png)\n\n我们可以看到，`3`分配发生了。第一次分配默认发生，第二次分配由`std::string`产生。最后一次分配由`std::any`产生。出现这种情况是因为`std::any`必须调整其内部存储，以考虑其看到的任何新的随机数据类型。换句话说，为了处理一个*通用*数据类型，C++ 必须执行一个分配。如果我们不断改变数据类型，情况会变得更糟。例如，考虑以下代码:\n\n```cpp\n#include <any>\n#include <string>\n\nstd::any data;\n\nint main(void)\n{\n    data = 42;\n    data = std::string{\"The answer is: 42\"};\n    data = 42;                                 // <--- keep swapping\n    data = std::string{\"The answer is: 42\"};   // <--- keep swapping\n    data = 42;                                 // <--- keep swapping\n    data = std::string{\"The answer is: 42\"};   // ...\n    data = 42;\n    data = std::string{\"The answer is: 42\"};\n}\n```\n\n前面的代码与前面的示例相同，唯一的区别是我们在数据类型之间进行了交换。Valgrind 产生以下输出:\n\n![](img/1df2f995-397d-4929-adfd-d6847ce57abf.png)\n\n如我们所见，`9`分配代替`3`发生。要解决这个问题，我们需要用一个`std::variant`代替`std::any`，如下:\n\n```cpp\n#include <variant>\n#include <string>\n\nstd::variant<int, std::string> data;\n\nint main(void)\n{\n    data = 42;\n    data = std::string{\"The answer is: 42\"};\n}\n```\n\n`std::any`和`std::variant`的区别在于`std::variant`要求用户说明变量必须支持哪些类型，从而消除了分配时动态内存分配的需要。Valgrind 的输出如下:\n\n![](img/2a8432c6-3aa6-4f3e-9f2d-d83c733ea3d2.png)\n\n现在，我们只有`2`分配，正如预期的那样(默认分配和来自`std::string`的分配)。如本食谱所示，包括 C++ 标准库在内的库可以隐藏内存分配，这可能会降低代码的速度，并使用比预期更多的内存资源。像 Valgrind 这样的工具可以用来识别这些类型的问题，允许您创建更高效的 C++ 代码。\n\n# 声明 noexcept\n\nC++ 11 引入了`noexcept`关键字，除了简化异常的一般使用方式之外，它还包括一个更好的 C++ 异常实现，该实现消除了异常的一些性能影响。然而，这并不意味着例外情况不包括*开销*(即绩效处罚)。在本食谱中，我们将探讨异常如何增加应用的开销，以及`noexcept`关键字如何帮助减少这些损失(取决于编译器)。\n\n这个方法很重要，因为它将证明如果一个函数没有抛出异常，那么它应该被标记为异常，以防止关于应用总大小的额外开销，导致应用加载更快。\n\n# 准备好\n\n开始之前，请确保满足所有技术要求，包括安装 Ubuntu 18.04 或更高版本，并在终端窗口中运行以下内容:\n\n```cpp\n> sudo apt-get install build-essential git cmake\n```\n\n这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后，打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。\n\n# 怎么做...\n\n执行以下步骤完成该配方:\n\n1.  从新的终端，运行以下命令下载源代码:\n\n```cpp\n> cd ~/\n> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git\n> cd Advanced-CPP-CookBook/chapter06\n```\n\n2.  要编译源代码，请运行以下命令:\n\n```cpp\n> cmake .\n> make recipe04_examples\n```\n\n3.  编译完源代码后，您可以通过运行以下命令来执行该配方中的每个示例:\n\n```cpp\n> ./recipe04_example01 \n\n> ./recipe04_example02\n```\n\n在下一节中，我们将逐一介绍这些示例，并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。\n\n# 它是如何工作的...\n\n在这个食谱中，我们将了解为什么如果一个函数不应该抛出异常，那么将它标记为`noexcept`是如此重要。这是因为它为应用消除了额外的异常支持开销，这可以改善执行时间、应用大小甚至加载时间(这取决于编译器、您使用的标准库等等)。为了展示这一点，让我们创建一个简单的例子:\n\n```cpp\nclass myclass\n{\n    int answer;\n\npublic:\n    ~myclass()\n    {\n        answer = 42;\n    }\n};\n```\n\n我们需要做的第一件事是创建一个类，在析构时设置一个`private`成员变量，如下所示:\n\n```cpp\nvoid foo()\n{\n    throw 42;\n}\n\nint main(void) \n{\n    myclass c;\n\n    try {\n        foo();\n    }\n    catch (...) {\n    }\n}\n```\n\n现在，我们可以创建两个函数。第一个函数抛出一个异常，而第二个函数是我们的主函数。这个函数创建了我们类的一个实例，并在一个`try` / `catch`块中调用`foo()`函数。换句话说，`main()`函数在任何时候都不会抛出异常。如果我们查看主函数的程序集，我们将看到以下内容:\n\n![](img/e8b37484-b6b5-4a42-a49c-ace342254030.png)\n\n如我们所见，我们的主函数调用`_Unwind_Resume`，由异常解卷器使用。这种额外的逻辑是由于 C++ 必须在函数的末尾添加额外的异常逻辑。要删除这个额外的逻辑，告诉编译器`main()`函数没有被抛出:\n\n```cpp\nint main(void) noexcept\n{\n    myclass c;\n\n    try {\n        foo();\n    }\n    catch (...) {\n    }\n}\n```\n\n添加`noexcept`告诉编译器不能抛出异常。因此，该函数不再包含用于处理异常的额外逻辑，如下所示:\n\n![](img/b17102a6-7e31-4c82-8751-308a935b23f2.png)\n\n如我们所见，展开功能不再存在。需要注意的是，存在对 catch 函数的调用，这是由于`try` / `catch`块，而不是异常的开销。"
  },
  {
    "path": "docs/adv-cpp-prog-cb/07.md",
    "content": "# 七、调试和测试\n\n在本章中，您将学习如何正确测试和调试您的 C++ 应用。这一点很重要，因为如果没有良好的测试和调试，您的 C++ 应用很可能包含难以检测的错误，这会降低它们的整体可靠性、稳定性和安全性。\n\n本章将从单元测试的全面概述开始，单元测试是在单元级别测试代码的行为，本章还将研究如何利用现有的库来加快编写测试的过程。接下来，它将演示如何使用 ASAN 和瑞银动态分析工具来检查内存损坏和未定义的行为。最后，本章将以快速查看如何在您自己的代码中利用`NDEBUG`宏在尝试解决问题时添加调试逻辑来结束。\n\n本章包含以下配方:\n\n*   掌握单元测试\n*   和 ASAN 一起工作，地址消毒剂\n*   与未定义的行为消毒剂瑞银合作\n*   使用`#ifndef NDEBUG`有条件地执行附加检查\n\n# 技术要求\n\n要编译和运行本章中的示例，您必须拥有运行 Ubuntu 18.04 的计算机的管理权限，并且具有功能性互联网连接。在运行这些示例之前，您必须安装以下内容:\n\n```cpp\n> sudo apt-get install build-essential git cmake\n```\n\n如果这安装在 Ubuntu 18.04 以外的任何操作系统上，则需要 GCC 7.4 或更高版本以及 CMake 3.6 或更高版本。\n\n章节的代码文件可以在[https://github . com/packt publishing/Advanced-CPP-cook book/tree/master/chapter 07](https://github.com/PacktPublishing/Advanced-CPP-CookBook/tree/master/chapter07)找到。\n\n# 掌握单元测试\n\n在这个食谱中，我们将学习如何单元测试我们的 C++ 代码。有几种不同的方法可以确保您的 C++ 代码以可靠、稳定、安全和符合规范的方式执行。\n\n单元测试是在基本单元级别测试代码的行为，是任何测试策略的关键组成部分。这个食谱很重要，不仅因为它将教你如何对代码进行单元测试，还因为它将解释为什么单元测试如此关键，以及如何使用现有的库加快对 C++ 进行单元测试的过程。\n\n# 准备好\n\n开始之前，请确保满足所有技术要求，包括安装 Ubuntu 18.04 或更高版本，并在终端窗口中运行以下内容:\n\n```cpp\n> sudo apt-get install build-essential git cmake\n```\n\n这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后，打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。\n\n# 怎么做...\n\n按照以下步骤完成配方:\n\n1.  从新的终端，运行以下命令下载源代码:\n\n```cpp\n> cd ~/\n> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git\n> cd Advanced-CPP-CookBook/chapter07\n```\n\n2.  要编译源代码，请运行以下命令:\n\n```cpp\n> cmake .\n> make recipe01_examples\n```\n\n3.  编译源代码后，您可以通过运行以下命令来执行该配方中的每个示例:\n\n```cpp\n> ./recipe01_example01\n===========================================================================\nAll tests passed (1 assertion in 1 test case)\n\n> ./recipe01_example02\n===========================================================================\nAll tests passed (6 assertions in 1 test case)\n\n> ./recipe01_example03\n===========================================================================\nAll tests passed (8 assertions in 1 test case)\n\n> ./recipe01_example04\n===========================================================================\nAll tests passed (1 assertion in 1 test case)\n\n> ./recipe01_example05\n...\n===========================================================================\ntest cases: 1 | 1 passed\nassertions: - none -\n\n> ./recipe01_example06\n...\n===========================================================================\ntest cases: 5 | 3 passed | 2 failed\nassertions: 8 | 6 passed | 2 failed\n\n> ./recipe01_example07\n===========================================================================\ntest cases: 1 | 1 passed\nassertions: - none -\n\n> ./recipe01_example08\n===========================================================================\nAll tests passed (3 assertions in 1 test case)\n```\n\n在下一节中，我们将逐一介绍这些示例，并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。\n\n# 它是如何工作的...\n\n简单地编写 C++ 应用，并希望它不需要任何测试就能如预期那样工作，肯定会导致可靠性、稳定性和安全性相关的错误。这个方法很重要，因为在发布前测试应用可以确保应用按预期执行，最终为您节省时间和金钱。\n\n测试代码有几种不同的方法，包括系统级、集成、长期稳定性、静态和动态分析等。在本食谱中，我们将重点介绍**单元测试**。单元测试将一个应用分解成功能性的**单元**，并测试每个单元以确保它按预期执行。通常，在实践中，每个函数和对象(即类)都是一个应该独立测试的单元。\n\n关于如何执行单元测试，有几种不同的理论，整本书都是关于这个主题的。一些人认为应该测试函数或对象中的每一行代码，利用覆盖工具来确保符合性，而另一些人认为单元测试应该是需求驱动的，使用黑盒方法。一个被称为**测试驱动开发**的常见开发过程规定，所有的测试，包括单元测试，都应该在任何源代码被编写之前被编写，而**行为驱动开发**通过一个特定的、故事驱动的单元测试方法，将测试驱动开发向前推进了一步。\n\n每个测试模型都有它的优缺点，你选择哪种方法将取决于你正在编写的应用的类型，你坚持的软件开发过程的类型，以及你可能需要或不需要遵循的任何策略。不管这种选择如何，单元测试都可能是您的测试方案的一部分，这个方法将为如何单元测试您的 C++ 应用提供基础。\n\n虽然单元测试可以用标准的 C++ 来完成(例如`libc++ `就是这样进行单元测试的)，但是单元测试库有助于简化这个过程。在这个食谱中，我们将利用`Catch2`单元测试库，它可以在\n[https://github.com/catchorg/Catch2.git](https://github.com/catchorg/Catch2.git)找到。\n\n虽然我们将回顾 Catch2，但是正在讨论的原则适用于大多数可用的单元测试库，或者即使是标准的 C++，如果您选择不使用助手库的话。要利用 Catch2，只需执行以下步骤:\n\n```cpp\n> git clone https://github.com/catchorg/Catch2.git catch\n> cd catch\n> mkdir build\n> cd build\n> cmake ..\n> make\n> sudo make install\n```\n\n您也可以使用 CMake 的`ExternalProject_Add`，正如我们在 GitHub 上的例子中所做的那样，来利用库的本地副本。\n\n为了了解如何使用 Catch2，让我们看下面这个简单的例子:\n\n```cpp\n#define CATCH_CONFIG_MAIN\n#include <catch.hpp>\n\nTEST_CASE(\"the answer\")\n{\n   CHECK(true);\n}\n```\n\n运行时，我们会看到以下输出:\n\n![](img/82b7302a-7165-4cf8-92f1-83a6491e786f.png)\n\n在前面的例子中，我们从定义`CATCH_CONFIG_MAIN`开始。这告诉 Catch2 库，我们希望它为我们创建`main()`函数。这必须在我们包含 Catch2 `include`语句之前定义，我们在前面的代码中已经这样做了。\n\n下一步是定义一个测试用例。每一个单元都被分解成测试用例来测试有问题的单元。每个测试用例的粒度由您决定:一些人选择为每个被测试的单元拥有一个测试用例，而另一些人，例如，选择为每个被测试的功能拥有一个测试用例。`TEST_CASE()`接受一个字符串，该字符串允许您提供测试用例的描述，这在测试失败时很有帮助，因为 Catch2 将输出该字符串来帮助您识别测试代码中失败发生的位置。我们简单例子的最后一步是使用`CHECK()`宏。这个宏执行特定的测试。每个`TEST_CASE()`可能会有几个`CHECK()`宏，旨在为设备提供特定的输入，然后验证结果输出。\n\n一旦编译并执行，单元测试库将提供一些描述如何执行测试的输出文本。在这种情况下，库声明所有的测试都通过了，这就是期望的结果。\n\n为了更好地理解如何在自己的代码中利用单元测试，让我们看看下面这个更复杂的例子:\n\n```cpp\n#define CATCH_CONFIG_MAIN\n#include <catch.hpp>\n\n#include <vector>\n#include <iostream>\n#include <algorithm>\n\nTEST_CASE(\"sort a vector\")\n{\n    std::vector<int> v{4, 8, 15, 16, 23, 42};\n    REQUIRE(v.size() == 6);\n\n    SECTION(\"sort descending order\") {\n        std::sort(v.begin(), v.end(), std::greater<int>());\n\n        CHECK(v.front() == 42);\n        CHECK(v.back() == 4);\n    }\n\n    SECTION(\"sort ascending order\") {\n        std::sort(v.begin(), v.end(), std::less<int>());\n\n        CHECK(v.front() == 4);\n        CHECK(v.back() == 42);\n    }\n}\n```\n\n像前面的例子一样，我们用`CATCH_CONFIG_MAIN`宏包含 Catch2，然后用描述定义一个测试用例。在这个例子中，我们正在测试对向量进行排序的能力，所以这就是我们提供的描述。我们在测试中做的第一件事是创建一个带有预定义整数列表的整数向量。\n\n接下来我们要做的是使用`REQUIRE()`宏进行测试，确保向量中有`6`元素。`REQUIRE()`宏与`CHECK()`类似，两者都检查以确保宏中的语句是真实的。区别在于`CHECK()`宏将报告错误，然后继续执行，而`REQUIRE()`宏将停止执行，停止单元测试。这有助于确保单元测试是基于测试可能做出的任何假设而正确构建的。`REQUIRE()`的使用很重要，因为单元测试随着时间的推移而成熟，并且其他程序员添加和修改单元测试，确保随着时间的推移 bug 不会被引入单元测试，因为没有什么比测试和调试您的单元测试更糟糕的了。\n\n`SECTION()`宏用于用更好的描述进一步分解我们的测试，并提供为每个测试添加通用设置代码的能力。在前面的例子中，我们测试了向量的`sort()`函数。`sort()`函数可以向不同的方向排序，这是单元测试必须验证的。没有`SECTION()`宏，如果测试失败，很难知道失败是来自升序还是降序排序。此外，`SECTION()`宏确保每个测试不影响其他测试的结果。\n\n最后，我们使用`CHECK()`宏来确保`sort()`功能按预期工作。单元测试也应该检查异常。在下面的示例中，我们将确保正确抛出异常:\n\n```cpp\n#define CATCH_CONFIG_MAIN\n#include <catch.hpp>\n\n#include <vector>\n#include <iostream>\n#include <algorithm>\n\nvoid foo(int val)\n{\n    if (val != 42) {\n        throw std::invalid_argument(\"The answer is: 42\");\n    }\n}\n\nTEST_CASE(\"the answer\")\n{\n    CHECK_NOTHROW(foo(42));\n    REQUIRE_NOTHROW(foo(42));\n\n    CHECK_THROWS(foo(0));\n    CHECK_THROWS_AS(foo(0), std::invalid_argument);\n    CHECK_THROWS_WITH(foo(0), \"The answer is: 42\");\n\n    REQUIRE_THROWS(foo(0));\n    REQUIRE_THROWS_AS(foo(0), std::invalid_argument);\n    REQUIRE_THROWS_WITH(foo(0), \"The answer is: 42\");\n}\n```\n\n和前面的例子一样，我们定义`CATCH_CONFIG_MAIN`宏，添加我们需要的包含，并定义一个单独的`TEST_CASE()`。我们还定义了一个`foo()`函数，如果`foo()`函数的输入无效，就会抛出这个函数。\n\n在我们的测试案例中，我们首先用有效的输入测试`foo()`函数。由于`foo()`函数没有输出(也就是说，函数返回`void`，我们通过使用`CHECK_NOTHROW()`宏确保没有抛出异常来检查以确保函数已经正确执行。需要注意的是，和`CHECK()`宏一样，`CHECK_NOTHROW()`宏也有等效的`REQUIRE_NOTHROW()`，如果检查失败将会暂停执行。\n\n最后，我们确保`foo()`函数在其输入无效时抛出异常。有几种不同的方法可以做到这一点。`CHECK_THROWS()`宏只是确保抛出了一个异常。`CHECK_THROWS_AS()`宏确保不仅抛出了异常，而且该异常属于`std::runtime_error`类型。两者都必须为真，测试才能通过。最后，`CHECK_THROWS_WITH()`宏确保抛出了异常，并且`what()`字符串返回了我们期望的与异常匹配的结果。与其他版本的`CHECK()`宏一样，这些宏也有`REQUIRE()`版本。\n\n虽然 Catch2 库提供了宏，可以让您深入了解每种异常类型的具体细节，但应该注意的是，除非异常类型和字符串在您的 API 要求中有明确定义，否则应该使用通用的`CHECK_THROWS()`宏——例如，`at()`函数由规范定义，当索引无效时总是返回一个`std::out_of_range`异常。在这种情况下，应该使用`CHECK_THROWS_AS()`宏来确保`at()`功能与规范相匹配。该异常返回的字符串未被指定为规范的一部分，因此应避免使用`CHECK_THROWS_WITH()`。这很重要，因为编写单元测试时的一个常见错误是编写了过度指定的单元测试。当被测试的代码被更新时，过度指定的单元测试必须经常被更新，这不仅成本高，而且容易出错。\n\n单元测试应该足够详细，以确保单元按预期执行，但足够通用，以确保对源代码的修改不需要更新单元测试本身，除非应用编程接口的要求发生变化，导致一组单元测试老化良好，同时仍然为确保可靠性、稳定性、安全性甚至合规性提供必要的测试。\n\n一旦有了一组单元测试来验证每个单元都按预期执行，下一步就是确保每当代码被修改时，单元测试都被执行。这可以手动完成，也可以通过**持续集成** ( **CI** )服务器自动完成，如 TravisCI 但是，当您决定这样做时，请确保单元测试返回正确的错误代码。在前面的例子中，当单元测试通过时，单元测试本身以`EXIT_SUCCESS`退出，并打印一个简单的字符串，说明所有测试都通过了。对于大多数配置项来说，这已经足够了，但是在某些情况下，让 Catch2 以易于解析的格式输出结果可能会很有用。\n\n例如，考虑以下代码:\n\n```cpp\n#define CATCH_CONFIG_MAIN\n#include <catch.hpp>\n\nTEST_CASE(\"the answer\")\n{\n    CHECK(true);\n}\n```\n\n让我们用下面的代码来运行:\n\n```cpp\n> ./recipe01_example01 -r xml\n```\n\n如果我们这样做，我们会得到以下结果:\n\n![](img/181d1cbf-5814-44ae-8f95-b7577da6c8e5.png)\n\n在前面的示例中，我们创建了一个简单的测试用例(与本食谱中的第一个示例相同)，并指示 Catch2 使用`-r xml`选项将测试结果输出到 XML。Catch2 有几种不同的输出格式，包括 XML 和 JSON。\n\n除了输出格式之外，Catch2 还可以用来对我们的代码进行基准测试。例如，考虑以下代码片段:\n\n```cpp\n#define CATCH_CONFIG_MAIN\n#define CATCH_CONFIG_ENABLE_BENCHMARKING\n#include <catch.hpp>\n\n#include <vector>\n#include <iostream>\n\nTEST_CASE(\"the answer\")\n{\n    std::vector<int> v{4, 8, 15, 16, 23, 42};\n\n    BENCHMARK(\"sort vector\") {\n        std::sort(v.begin(), v.end());\n    };\n}\n```\n\n在前面的例子中，我们创建了一个简单的测试用例，用预定义的向量编号对向量进行排序。然后，我们在一个`BENCHMARK()`宏中对这个列表进行排序，执行时会产生以下输出:\n\n![](img/6ba0ee12-7624-4e52-897e-5182f5487f0e.png)\n\n如上图截图所示，Catch2 多次执行该函数，平均花费`197`纳秒对向量进行排序。`BENCHMARK()`宏有助于确保代码不仅在给定特定输入的情况下以正确的输出按预期执行，而且在给定特定时间内执行。与更详细的输出格式(如 XML 或 JSON)相结合，这种类型的信息可以用来确保随着源代码的修改，生成的代码在相同的时间内或更快地执行。\n\n为了更好地理解单元测试如何真正提高你的 C++，我们将通过两个额外的例子来结束这个食谱，这两个例子旨在提供更真实的场景。\n\n在第一个例子中，我们将创建一个**向量**。与`std::vector`不同，在 C++ 中`std::vector`是一个动态的 C 风格的数组，数学中的向量是 *n* 维空间中的一个点(在我们的例子中，我们将其限制在 2D 空间中)，其大小是该点和原点之间的距离(即 0，0)。我们在示例中实现了这个向量，如下所示:\n\n```cpp\n#define CATCH_CONFIG_MAIN\n#include <catch.hpp>\n\n#include <cmath>\n#include <climits>\n\nclass vector\n{\n    int m_x{};\n    int m_y{};\n```\n\n我们做的第一件事(除了通常的宏和 includes)是用`x`和`y`坐标定义一个类:\n\n```cpp\npublic:\n\n    vector() = default;\n\n    vector(int x, int y) :\n        m_x{x},\n        m_y{y}\n    { }\n\n    auto x() const\n    { return m_x; }\n\n    auto y() const\n    { return m_y; }\n\n    void translate(const vector &p)\n    {\n        m_x += p.m_x;\n        m_y += p.m_y;\n    }\n\n    auto magnitude()\n    {\n        auto a2 = m_x * m_x;\n        auto b2 = m_y * m_y;\n\n        return sqrt(a2 + b2);\n    }\n};\n```\n\n接下来，我们添加一些帮助函数和构造函数。当 *x* 和 *y* 被设置为原点时，默认构造函数会生成一个没有方向或大小的向量。为了创建具有方向和大小的向量，我们还提供了另一个构造函数，允许您提供向量的初始 *x* 和 *y* 坐标。为了得到向量的方向，我们提供了 getters 返回向量的 *x* 和 *y* 值。最后，我们提供了两个助手函数。第一个辅助函数**翻译**向量，在数学中这是另一个术语，用于改变给定另一个向量的向量的 *x* 和 *y* 坐标。最后一个辅助函数返回向量的大小，如果向量的 *x* 和 *y* 值被用来构造三角形(也就是说，我们必须使用毕达哥拉斯定理来计算向量的大小)，那么它就是向量斜边的长度。接下来，我们继续添加运算符，具体如下:\n\n```cpp\nbool operator== (const vector &p1, const vector &p2)\n{ return p1.x() == p2.x() && p1.y() == p2.y(); }\n\nbool operator!= (const vector &p1, const vector &p2)\n{ return !(p1 == p2); }\n\nconstexpr const vector origin;\n```\n\n我们增加了一些等价算子，可以用来检查两个向量是否相等。我们还定义了一个表示原点的向量，这个向量的 *x* 和 *y* 值为 0。\n\n为了测试这个向量，我们添加了以下测试:\n\n```cpp\nTEST_CASE(\"default constructor\")\n{\n    vector p;\n\n    CHECK(p.x() == 0);\n    CHECK(p.y() == 0);\n}\n\nTEST_CASE(\"origin\")\n{\n    CHECK(vector{0, 0} == origin);\n    CHECK(vector{1, 1} != origin);\n}\n\nTEST_CASE(\"translate\")\n{\n    vector p{-4, -8};\n    p.translate({46, 50});\n\n    CHECK(p.x() == 42);\n    CHECK(p.y() == 42);\n}\n\nTEST_CASE(\"magnitude\")\n{\n    vector p(1, 1);\n    CHECK(Approx(p.magnitude()).epsilon(0.1) == 1.4);\n}\n\nTEST_CASE(\"magnitude overflow\")\n{\n    vector p(INT_MAX, INT_MAX);\n    CHECK(p.magnitude() == 65536);\n}\n```\n\n第一个测试确保默认构造的向量实际上是原点。我们的下一个测试确保我们的全局**原点**向量是原点。这一点很重要，因为我们不应该假设原点是默认构造的——也就是说，未来有人可能会意外地将原点更改为`0,0`以外的其他东西。这个测试用例保证了原点实际上是`0,0`，这样以后如果有人不小心更改了这个，这个测试就会失败。由于原点必须导致 *x* 和 *y* 都为 0，因此该测试没有超出规定。\n\n接下来，我们测试平移和幅度函数。在幅度测试的情况下，我们使用`Approx()`宏。这是必要的，因为返回的幅度是一个浮点，其大小和精度取决于硬件，与我们的测试无关。`Approx()`宏允许我们陈述精度水平，我们希望验证`magnitude()`函数的结果，该函数使用`epsilon()`修改器来实际陈述精度。在这种情况下，我们只希望验证到小数点后一位。\n\n最后一个测试用例用于演示如何测试这些函数的所有输入。如果一个函数取整数，那么有效、无效和极端的输入都应该被测试。在这种情况下，我们正在通过 *x* 和 *y* 的`INT_MAX`。产生的`magnitude()`函数没有提供有效的结果。这是因为计算大小的过程溢出了整数类型。这种类型的错误要么应该在代码中考虑(也就是说，您应该检查可能的溢出并抛出异常)，要么 API 的规范应该调用这些类型的问题(也就是说，C++ 规范可能会声明这种类型的输入的结果是未定义的)。无论哪种方式，如果一个函数取一个整数，那么所有可能的整数值都应该被测试，并且这个过程应该对所有输入类型重复。\n\n该测试的结果如下:\n\n![](img/64400718-2c26-405a-9f0f-f9581dec0119.png)\n\n如前面的截图所示，该单元没有通过最后一次测试。如前所述，要解决这个问题，幅度函数应该更改为当发生溢出时抛出，找到防止溢出的方法，或者删除测试并声明此类输入未定义。\n\n在最后一个例子中，我们将演示如何处理不返回值的函数，而是处理输入。\n\n让我们通过创建一个写入文件的类和另一个使用第一个类将字符串写入所述文件的类来开始这个示例，如下所示:\n\n```cpp\n#define CATCH_CONFIG_MAIN\n#include <catch.hpp>\n\n#include <string>\n#include <fstream>\n\nclass file\n{\n    std::fstream m_file{\"test.txt\", std::fstream::out};\n\npublic:\n\n    void write(const std::string &str)\n    {\n        m_file.write(str.c_str(), str.length());\n    }\n};\n\nclass the_answer\n{\npublic:\n\n    the_answer(file &f)\n    {\n        f.write(\"The answer is: 42\\n\");\n    }\n};\n```\n\n如前面的代码所示，第一个类写入一个名为`test.txt`的文件，而第二个类将第一个类作为输入，并使用它向该文件写入一个字符串。\n\n我们测试第二类如下:\n\n```cpp\nTEST_CASE(\"the answer\")\n{\n    file f;\n    the_answer{f};\n}\n```\n\n前面测试的问题是我们没有任何`CHECK()`宏。这是因为，除了`CHECK_NOTHROW()`，我们没有什么可查的。在这个测试中，我们正在测试以确保`the_answer{}`类调用`file{}`类并且`write()`功能正常。我们可以打开`test.txt`文件并检查以确保它是用正确的字符串编写的，但是这是一项大量的工作。这种类型的检查也是过度指定的，因为我们不是在测试`file{}`类——我们只是在测试`the_answer{}`类。如果将来我们决定`file{}`类应该写入网络文件，而不是磁盘上的文件，单元测试将不得不改变。\n\n为了解决这个问题，我们可以利用一个叫做**嘲讽**的概念。一个`Mock`类是一个假装是输入的类的类，为单元测试提供**接缝**，允许单元测试验证测试结果。这与提供虚假输入的`Stub`不同。可悲的是，与其他语言相比，C++ 并不支持嘲讽。帮助程序库(如 GoogleMock)试图解决这个问题，但代价是要求您的所有可模拟类都包含一个 vTable(即继承纯虚拟基类)，并定义每个可模拟类两次(一次在代码中，第二次在测试中，使用 Google 定义的一组 API)。这远非最佳。像希波克拉底这样的库试图解决这些问题，但代价是一些只在特定环境下有效的虚拟黑魔法，并且在出现问题时几乎不可能调试。虽然希波莫克可能是最好的选择之一(也就是说，直到 C++ 启用本机嘲讽)，但以下示例是使用标准 C++ 进行嘲讽的另一种方法，唯一的缺点是冗长:\n\n```cpp\n#define CATCH_CONFIG_MAIN\n#include <catch.hpp>\n\n#include <string>\n#include <fstream>\n\nclass file\n{\n    std::fstream m_file{\"test.txt\", std::fstream::out};\n\npublic:\n    VIRTUAL ~file() = default;\n\n    VIRTUAL void write(const std::string &str)\n    {\n        m_file.write(str.c_str(), str.length());\n    }\n};\n\nclass the_answer\n{\npublic:\n    the_answer(file &f)\n    {\n        f.write(\"The answer is: 42\\n\");\n    }\n};\n```\n\n和前面的例子一样，我们创建了两个类。第一类写入文件，而第二类使用第一类将字符串写入所述文件。不同的是我们增加了`VIRTUAL`宏。当代码被编译到我们的应用中时，`VIRTUAL`被设置为无，这意味着它被编译器从代码中移除。然而，当代码在我们的测试中编译时，它被设置为`virtual`，这告诉编译器给类一个 vTable。因为这只是在我们的测试中完成的，所以增加的开销是可以接受的。\n\n既然我们的类在我们的测试用例中支持继承，我们可以创建我们的`file{}`类的子类版本，如下所示:\n\n```cpp\nclass mock_file : public file\n{\npublic:\n    void write(const std::string &str)\n    {\n        if (str == \"The answer is: 42\\n\") {\n            passed = true;\n        }\n        else {\n            passed = false;\n        }\n    }\n\n    bool passed{};\n};\n```\n\n前面的类定义了我们的模拟。我们的模拟检查不是写入文件，而是查看是否有特定的字符串被写入我们的假文件，并根据测试结果将全局变量设置为`true`或`false`。\n\n然后我们可以如下测试我们的`the_answer{}`类:\n\n```cpp\nTEST_CASE(\"the answer\")\n{\n    mock_file f;\n    REQUIRE(f.passed == false);\n\n    f.write(\"The answer is not: 43\\n\");\n    REQUIRE(f.passed == false);\n\n    the_answer{f};\n    CHECK(f.passed);\n}\n```\n\n执行此操作时，我们会得到以下结果:\n\n![](img/289a2554-23b1-4e41-9c45-6bc8b34eb163.png)\n\n如前面的截图所示，我们现在可以检查以确保我们的类按照预期写入文件。需要注意的是，在执行我们的测试之前，我们使用`REQUIRE()`宏来确保模拟处于`false`状态。这确保了如果我们的实际测试注册为已经通过，那么它实际上已经通过，而不是因为我们的测试逻辑中的错误而注册为通过。\n\n# 和 ASAN 一起工作，地址消毒剂\n\n在这个食谱中，我们将学习如何利用谷歌的**地址消毒剂**(**ASAN**)——这是一个动态分析工具——来检查我们代码中的内存损坏错误。这个方法很重要，因为它提供了一种简单的方法来确保您的代码既可靠又稳定，对构建系统的更改也很少。\n\n# 准备好\n\n开始之前，请确保满足所有技术要求，包括安装 Ubuntu 18.04 或更高版本，并在终端窗口中运行以下内容:\n\n```cpp\n> sudo apt-get install build-essential git cmake\n```\n\n这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后，打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。\n\n# 怎么做...\n\n按照配方执行以下步骤:\n\n1.  从新的终端，运行以下命令下载源代码:\n\n```cpp\n> cd ~/\n> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git\n> cd Advanced-CPP-CookBook/chapter07\n```\n\n2.  要编译源代码，请运行以下命令:\n\n```cpp\n> cmake -DCMAKE_BUILD_TYPE=ASAN ..\n> make recipe02_examples\n```\n\n3.  编译源代码后，您可以通过运行以下命令来执行该配方中的每个示例:\n\n```cpp\n> ./recipe02_example01\n...\n\n> ./recipe02_example02\n...\n\n> ./recipe02_example03\n...\n\n> ./recipe02_example04\n...\n\n> ./recipe02_example05\n...\n```\n\n在下一节中，我们将逐一介绍这些示例，并解释每个示例程序的功能，以及它与本食谱中所教授的课程之间的关系。\n\n# 它是如何工作的...\n\n谷歌的地址消毒剂是对 GCC 和 LLVM 编译器的一组修改，以及一组在测试时必须链接到您的应用中的库。为此，我们必须在编译测试代码时添加以下编译器标志(但不要将这些标志添加到生产版本中):\n\n```cpp\n-fsanitize=address \n-fno-optimize-sibling-calls \n-fsanitize-address-use-after-scope \n-fno-omit-frame-pointer \n-g -O1\n```\n\n这里要注意的最重要的标志是`-fsanitize=address`标志，它告诉编译器启用 ASAN。洗手液需要其余的彩旗才能正常工作，最引人注目的彩旗是`-g`和`-01`。`-g`标志启用调试，`-O1`标志将优化级别设置为 1，以提供一些性能改进。请注意，一旦启用 ASAN 工具，编译器将自动尝试链接到 ASAN 库，该库必须存在于您的计算机上。\n\n为了演示这种消毒剂的工作原理，让我们看几个例子。\n\n# 内存泄漏错误\n\n`AddressSanitizer`是一个动态分析工具，旨在识别内存损坏错误。它类似于 Valgrind，但直接构建在您的可执行文件中。演示这一点最简单的例子(也是最常见的错误类型之一)是内存泄漏，如以下代码所示:\n\n```cpp\nint main(void)\n{\n    new int;\n}\n```\n\n这将产生以下输出:\n\n![](img/4cdecfde-c17e-47ba-b109-8dd637af2a5d.png)\n\n在前面的例子中，我们使用`new`运算符在程序中分配一个整数，但是在退出程序之前，我们永远不会释放这个分配的内存。ASAN 工具能够检测到这个问题，并在应用完成执行时输出一个错误。\n\n# 内存删除了两次\n\n检测内存泄漏的能力非常有用，但这不是 ASAN 能够检测到的唯一类型的错误。另一种常见的错误是两次删除内存。例如，考虑以下代码片段:\n\n```cpp\nint main(void)\n{\n    auto p = new int;\n    delete p;\n\n    delete p;\n}\n```\n\n执行时，我们会看到以下输出:\n\n![](img/01d887f8-4fcc-4efc-8691-67831ec8b13a.png)\n\n在前面的例子中，我们使用`new`运算符分配一个整数，然后使用删除运算符分配`delete`整数。由于指向先前分配的内存的指针仍然在我们的`p`变量中，我们可以再次删除它，这是我们在退出程序之前做的。在某些系统上，这将产生分段错误，因为它是未定义的行为。ASAN 工具能够检测到此问题，并输出一条错误消息，说明出现了`double-free`错误。\n\n# 访问无效内存\n\n另一种类型的错误是试图访问从未分配的内存。这通常是由试图取消引用空指针的代码引起的，但当指针损坏时也会发生这种情况，如下所示:\n\n```cpp\nint main(void)\n{\n    int *p = (int *)42;\n    *p = 0;\n}\n```\n\n这将产生以下输出:\n\n![](img/ebd30496-bc91-49e1-b20c-1e5f580db297.png)\n\n在前面的例子中，我们创建了一个指向整数的指针，然后为它提供了一个损坏的值`42`(这不是一个有效的指针)。然后，我们尝试取消引用损坏的指针，这将导致分段错误。应该指出的是，ASAN 工具能够发现这个问题，但它不能提供任何有用的信息。这是因为 ASAN 工具是一个连接到内存分配例程的库，跟踪每个分配以及如何使用分配。如果一个分配从未发生，它将不会有任何关于发生了什么的信息，除了典型的 Unix 信号处理程序已经能够提供的信息之外，其他动态分析工具，如 Valgrind，更适合处理这些信息。\n\n# 删除后使用内存\n\n为了进一步演示地址消毒器是如何工作的，让我们看下面的例子:\n\n```cpp\nint main(void)\n{\n    auto p = new int;\n    delete p;\n\n    *p = 0;\n}\n```\n\n当我们执行此操作时，我们会看到以下内容:\n\n![](img/c01185c7-a10c-4464-be43-c60816cfcd63.png)\n\n前面的示例分配一个整数，然后删除该整数。然后，我们尝试使用之前删除的内存。因为这个内存位置最初是被分配的，所以 ASAN 缓存了这个地址。当取消对先前删除的内存的引用时，ASAN 能够检测到该问题为`heap-use-after-free`错误。它只能检测到这个问题，因为内存是以前分配的。\n\n# 删除从未分配的内存\n\n作为最后一个例子，让我们看看下面的内容:\n\n```cpp\nint main(void)\n{\n    int *p = (int *)42;\n    delete p;\n}\n```\n\n这将导致以下结果:\n\n![](img/ed7e5106-c3d4-478f-8085-45a6ed4f62fb.png)\n\n在前面的示例中，我们创建了一个指向指针的整数，然后再次为它提供了一个损坏的值。与前面的例子不同，在这个例子中，我们试图删除损坏的指针，这将导致分段错误。同样，ASAN 能够发现这个问题，但没有任何有用的信息，因为分配从未发生过。\n\n应该注意的是，C++ 核心指南——这是现代 C++ 的编码标准——在防止我们之前描述的问题类型方面非常有帮助。具体来说，《核心指南》规定`new()`、`delete()`、`malloc()`、`free()`和好友绝对不能直接使用，而应使用`std::unique_ptr`和`std::shared_ptr`进行*所有内存分配*。这些应用编程接口自动为您分配和释放内存。如果我们再看一下前面的例子，很容易看出使用这些 API 来分配内存，而不是手动使用`new()`和`delete()`可以防止这些类型的问题发生，因为前面的大多数例子都与无效使用`new()`和`delete()`有关。\n\n# 与未定义的行为消毒剂瑞银合作\n\n在本食谱中，我们将学习如何在我们的 C++ 应用中使用 UBSAN 动态分析工具，该工具能够检测未定义的行为。在我们的应用中可能会引入许多不同类型的错误，未定义的行为可能是最常见的类型，因为 C 和 C++ 规范定义了几种可能出现未定义行为的情况。\n\n这个食谱很重要，因为它将教你如何启用这个简单的功能，以及如何在你的应用中使用它。\n\n# 准备好\n\n开始之前，请确保满足所有技术要求，包括安装 Ubuntu 18.04 或更高版本，并在终端窗口中运行以下内容:\n\n```cpp\n> sudo apt-get install build-essential git cmake\n```\n\n这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后，打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。\n\n# 怎么做...\n\n按照以下步骤完成配方:\n\n1.  从新的终端，运行以下命令下载源代码:\n\n```cpp\n> cd ~/\n> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git\n> cd Advanced-CPP-CookBook/chapter07\n```\n\n2.  要编译源代码，请运行以下命令:\n\n```cpp\n> cmake -DCMAKE_BUILD_TYPE=UBSAN .\n> make recipe03_examples\n```\n\n3.  编译源代码后，您可以通过运行以下命令来执行该配方中的每个示例:\n\n```cpp\n> ./recipe03_example01\nFloating point exception (core dumped)\n\n> ./recipe03_example02\nSegmentation fault (core dumped)\n\n> ./recipe03_example03\nSegmentation fault (core dumped)\n\n> ./recipe03_example04\n\n```\n\n在下一节中，我们将逐一介绍这些示例，并解释每个示例程序的功能，以及它与本食谱中所教授的课程之间的关系。\n\n# 它是如何工作的...\n\nUBSAN 工具能够检测几种类型的未定义行为，包括:\n\n*   越界错误\n*   浮点错误\n*   除以零\n*   整数溢出\n*   空指针取消引用\n*   缺少退货\n*   有符号/无符号转换错误\n*   执行不到的代码\n\n在本食谱中，我们将看几个这样的例子，但是首先，我们必须在我们的应用中启用 UBSAN 工具。为此，我们必须在应用的构建系统中启用以下标志:\n\n```cpp\n-fsanitize=undefined\n```\n\n该标志将告诉 GCC 或 LLVM 使用 UBSAN 工具，该工具为我们的应用添加了额外的逻辑以及指向 UBSAN 库的链接。应该注意的是，随着时间的推移，UBSAN 工具的功能会不断增强。因此，海湾合作委员会和 LLVM 对瑞银集团的支持程度不同。为了充分利用这个工具，您的应用应该针对 GCC 和 LLVM 进行编译，并且您应该对这两者使用最新的编译器。\n\n# 除以零误差\n\n用 UBSAN 演示最简单的例子之一是被零除的误差，如下所示:\n\n```cpp\nint main(void)\n{\n    int n = 42;\n    int d = 0;\n\n    auto f = n/d;\n}\n```\n\n运行时，我们会看到以下内容:\n\n![](img/698ed489-e92b-4080-a0dc-fb224466ddf7.png)\n\n在前面的例子中，我们创建了两个整数(分子和分母)，分母设置为`0`。然后，我们对分子和分母进行除法运算，得到一个被零除的误差，当程序崩溃时，瑞银会检测并输出该误差。\n\n# 空指针取消引用\n\nC++ 中更常见的问题类型是空指针取消引用，如下所示:\n\n```cpp\nint main(void)\n{\n    int *p = 0;\n    *p = 42;\n}\n```\n\n这将导致以下结果:\n\n![](img/61d56d5b-161b-470f-8181-68dafa5ab7ab.png)\n\n在前面的例子中，我们创建了一个指向整数的指针，并将其设置为`0`(即`NULL`指针)。然后我们取消引用`NULL`指针并设置它的值，导致一个分段错误，当程序崩溃时，UBSAN 能够检测到这个错误。\n\n# 越界错误\n\n前面的两个例子都可能是使用 Unix 信号处理程序检测到的。在下一个示例中，我们将访问一个越界的数组，这在 C++ 规范中是未定义的，并且更难检测:\n\n```cpp\nint main(void)\n{\n    int numbers[] = {4, 8, 15, 16, 23, 42};\n    numbers[10] = 0;\n}\n```\n\n执行时，我们会得到以下结果:\n\n![](img/19eaa37d-90b8-4910-bc6a-bc96ec98f7dd.png)\n\n如前例所示，我们创建一个带有`6`元素的数组，然后尝试访问数组中的第 10 个元素，这个元素是不存在的。尝试访问数组中的这个元素不一定会产生分段错误。无论如何，瑞银能够检测到这种类型的错误，并在退出时将问题输出到`stderr`。\n\n# 溢出错误\n\n最后，我们还可以检测有符号整数溢出错误，这种错误在 C++ 中是未定义的，但极不可能产生崩溃，反而会导致程序进入损坏状态(通常会产生无限循环、越界错误等)。考虑以下代码:\n\n```cpp\n#include <climits>\n\nint main(void)\n{\n    int i = INT_MAX;\n    i++ ;\n}\n```\n\n这将导致以下结果:\n\n![](img/0db50c0b-0249-4600-9a72-c62b3fc591b0.png)\n\n如前面的例子所示，我们创建一个整数并将其设置为最大值。然后我们尝试增加这个整数，这通常会翻转整数的符号，这是瑞银能够检测到的一个错误。\n\n# 使用#ifndef NDEBUG 有条件地执行附加检查\n\n在这个食谱中，我们将学习如何利用`NDEBUG`宏，它代表*无调试*。这个方法很重要，因为大多数构建系统在编译*版本*或*产品*构建时会自动定义这个宏，这可以用来在创建这样的构建时禁用调试逻辑。\n\n# 准备好\n\n开始之前，请确保满足所有技术要求，包括安装 Ubuntu 18.04 或更高版本，并在终端窗口中运行以下内容:\n\n```cpp\n> sudo apt-get install build-essential git cmake\n```\n\n这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后，打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。\n\n# 怎么做...\n\n按照以下步骤完成配方:\n\n1.  从新的终端，运行以下命令下载源代码:\n\n```cpp\n> cd ~/\n> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git\n> cd Advanced-CPP-CookBook/chapter07\n```\n\n2.  要编译源代码，请运行以下命令:\n\n```cpp\n> cmake .\n> make recipe04_examples\n```\n\n3.  编译源代码后，您可以通过运行以下命令来执行该配方中的每个示例:\n\n```cpp\n> ./recipe04_example01\nThe answer is: 42\n\n> ./recipe04_example02\nrecipe04_example02: /home/user/book/chapter07/recipe04.cpp:45: int main(): Assertion `42 == 0' failed.\nAborted (core dumped)\n```\n\n在下一节中，我们将逐一介绍这些示例，并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。\n\n# 它是如何工作的...\n\n`NDEBUG`宏源于 C，用于改变`assert()`函数的行为。`assert()`功能可以编写如下:\n\n```cpp\nvoid __assert(int val, const char *str)\n{\n    if (val == 0) {\n        fprintf(stderr, \"Assertion '%s' failed.\\n\", str);\n        abort();\n    }\n}\n\n#ifndef NDEBUG\n    #define assert(a) __assert(a, #a)\n#else\n    #define assert(a)\n#endif \n```\n\n如前面的代码所示，如果给`__assert()`函数一个评估为`false`的布尔值(用 C 写，这是一个等于`0`的整数)，则向`stderr`输出一条错误消息，应用中止。然后使用`NDEBUG`宏来确定`assert()`函数是否存在，如果应用处于发布模式，那么所有断言逻辑都将被移除，从而减小应用的大小。使用 CMake 时，我们可以通过以下方式启用`NDEBUG`标志:\n\n```cpp\n> cmake -DCMAKE_BUILD_TYPE=Release ..\n```\n\n这将自动定义`NDEBUG`宏并启用优化。为了防止这个宏被定义，我们可以做相反的事情:\n\n```cpp\n> cmake -DCMAKE_BUILD_TYPE=Debug ..\n```\n\n前面的 CMake 代码将*而不是*定义`NDEBUG`宏，而是启用调试，并禁用大多数优化(尽管这取决于编译器)。\n\n在我们自己的代码中，`assert`宏可以如下使用:\n\n```cpp\n#include <cassert>\n\nint main(void)\n{\n    assert(42 == 0);\n}\n```\n\n这将导致以下结果:\n\n![](img/285a3fe5-641d-4c56-8521-9fe0e4ffbceb.png)\n\n如前例所示，我们创建了一个应用，该应用使用`assert()`宏来检查一个 false 语句，这会导致应用中止。\n\n虽然`assert()`功能使用了`NDEBUG`宏，但是您也可以自己使用，如下所示:\n\n```cpp\nint main(void)\n{\n#ifndef NDEBUG\n    std::cout << \"The answer is: 42\\n\";\n#endif\n}\n```\n\n如前代码所示，如果应用不是在*发布*模式下编译的(即编译时命令行上没有定义`NDEBUG`宏)，那么应用将输出到`stdout`。同样的逻辑可以在您的代码中使用，创建您自己的调试宏和函数，以确保您的调试逻辑在*发布*模式中被删除，允许您添加您需要的调试逻辑，而无需修改您交付给客户的最终应用。"
  },
  {
    "path": "docs/adv-cpp-prog-cb/08.md",
    "content": "# 八、创建和实现您自己的容器\n\n在本章中，您将学习如何通过利用 C++ 标准模板库已经提供的现有容器，在 C++ 中创建自己的自定义容器。这一章很重要，因为在很多情况下，您的代码会有在标准模板库容器上执行的常见操作，这些操作会在整个代码中重复(实现线程安全就是这种情况)。本章中的方法将教您如何轻松地将这些重复的代码封装到一个定制的容器中，而不必从头开始编写自己的容器，也不必用难以测试和验证的重复逻辑来乱丢代码。\n\n在本章中，您将学习实现自定义包装容器所需的技能，能够确保`std::vector`始终保持有序。第一个食谱将教你如何创建这个包装的基本知识。第二个方法将扩展第一个方法，教你如何根据容器的运行方式重新定义容器的接口。在这种情况下，由于容器总是按照排序的顺序，您将了解为什么提供一个`push_back()`函数没有意义，即使我们正在做的只是创建一个包装器(包装器的添加改变了容器本身的概念)。在第三个食谱中，你将学习使用迭代器的技巧，以及为什么在这个例子中，只能支持`const`迭代器。最后，我们将向容器中添加几个额外的 API，以提供完整的实现。\n\n本章中的配方如下:\n\n*   使用 std::vector 的简单包装\n*   添加标准::集合应用编程接口的相关部分\n*   使用迭代器\n*   添加标准::矢量应用编程接口的相关部分\n\n# 技术要求\n\n为了编译和运行本章中的示例，读者必须拥有运行 Ubuntu 18.04 的计算机的管理权限，并且具有功能性互联网连接。在运行这些示例之前，读者必须安装以下内容:\n\n```cpp\n> sudo apt-get install build-essential git cmake\n```\n\n如果这安装在 Ubuntu 18.04 以外的任何操作系统上，则需要 GCC 7.4 或更高版本以及 CMake 3.6 或更高版本。\n\n章节的代码文件可以在[https://github . com/packt publishing/Advanced-CPP-cook book/tree/master/chapter 08](https://github.com/PacktPublishing/Advanced-CPP-CookBook/tree/master/chapter08)找到。\n\n# 使用 std::vector 的简单包装\n\n在本食谱中，我们将学习如何通过包装现有的标准模板库容器来创建自己的自定义容器，以便根据需要提供自定义功能。在后面的食谱中，我们将在这个定制容器的基础上最终创建一个基于`std::vector`的完整容器。\n\n这个方法很重要，因为利用现有容器的代码通常伴随着公共逻辑，每次使用该容器时都会复制该逻辑。这个配方(以及这一整章)将教你如何将这个重复的逻辑封装到你自己的容器中，以便它可以被独立测试。\n\n# 准备好\n\n开始之前，请确保满足所有技术要求，包括安装 Ubuntu 18.04 或更高版本，并在终端窗口中运行以下内容:\n\n```cpp\n> sudo apt-get install build-essential git cmake\n```\n\n这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后，打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。\n\n# 怎么做...\n\n通过以下步骤来尝试配方:\n\n1.  从新的终端，运行以下命令下载源代码:\n\n```cpp\n> cd ~/\n> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git\n> cd Advanced-CPP-CookBook/chapter08\n```\n\n2.  要编译源代码，请运行以下命令:\n\n```cpp\n> cmake .\n> make recipe01_examples\n```\n\n3.  编译源代码后，您可以通过运行以下命令来执行该配方中的每个示例:\n\n```cpp\n> ./recipe01_example01\n1\n2\n3\n4\n5\n6\n7\n8\n\n> ./recipe01_example02\n1\n2\n3\n\n> ./recipe01_example03\n3\nelements: 4 42 \n3\nelements: 4 8 15 42 \n3\nelements: 4 8 15 16 23 42 \n```\n\n在下一节中，我们将逐一介绍这些例子，并解释每一个例子是做什么的，以及它与本食谱中所教授的课程之间的关系。\n\n# 它是如何工作的...\n\n在这个食谱中，我们将学习如何在`std::vector`周围创建一个简单的包装容器。大多数情况下，**标准模板库** ( **STL** )容器足以执行您的应用可能需要的任务，一般来说，应该避免创建您自己的容器，因为它们很复杂，很难正确处理。\n\n但是，有时您可能会发现自己在容器上重复执行相同的操作。当这种情况发生时，将这些常见的操作包装到一个包装器容器中通常是有帮助的，该容器可以独立地进行单元测试，以确保容器能够按预期工作。例如，STL 容器不是线程安全的。如果您需要一个容器在每次访问该容器时都具有线程安全功能，那么您首先需要确保您对该容器拥有独占访问权限(例如，通过锁定一个`std::mutex`)，然后才能进行容器操作。这种模式将在整个代码中重复出现，增加了进入死锁的机会。这个问题可以通过创建一个容器包装器来防止，该包装器为容器的每个公共成员添加一个`std::mutex`。\n\n在这个方法中，让我们考虑一个例子，在这个例子中，我们创建了一个向量(也就是说，连续内存中的元素数组，您必须能够直接访问它)，这个向量必须始终保持排序顺序。首先，我们需要一些标题:\n\n```cpp\n#include <vector>\n#include <algorithm>\n#include <iostream>\n```\n\n为了实现我们的容器，我们将利用`std::vector`。虽然我们可以从头开始实现自己的容器，但大多数时候这是不需要的，应该避免，因为这样的任务非常耗时和复杂。我们需要`std::sort`和`iostream`的`algorithm`表头进行测试。让我们补充如下:\n\n```cpp\ntemplate<\n    typename T,\n    typename Compare = std::less<T>,\n    typename Allocator = std::allocator<T>\n    >\nclass container\n{\n    using vector_type = std::vector<T, Allocator>;\n    vector_type m_v;\n\npublic:\n```\n\n容器的定义将从其模板定义开始，模板定义与`std::vector`的定义相同，只是增加了`Compare`类型，用于定义我们希望容器排序的顺序。默认情况下，容器将按升序排序，但这可以根据需要进行更改。最后，容器将有一个私有成员变量，它是这个容器包装的`std::vector`的一个实例。\n\n为了让容器与 C++ 实用程序、模板函数甚至一些关键的语言特性一起正常运行，容器将需要定义与`std::vector`相同的别名，如下所示:\n\n```cpp\n    using value_type = typename vector_type::value_type;\n    using allocator_type = typename vector_type::allocator_type;\n    using size_type = typename vector_type::size_type;\n    using difference_type = typename vector_type::difference_type;\n    using const_reference = typename vector_type::const_reference;\n    using const_pointer = typename vector_type::const_pointer;\n    using compare_type = Compare;\n```\n\n正如您所看到的，没有必要自己手动定义别名。相反，我们可以简单地从`std::vector`本身转发别名的声明。这种情况的例外是`compare_type`别名，因为这是我们添加到包装容器中的一个别名，它代表模板类用于最终将提供给`std::sort`的比较操作的类型。\n\n我们也不包括引用别名的非常数版本。原因是我们的容器必须始终保持`std::vector`有序。如果我们向用户提供对存储在`std::vector`中的元素的直接写访问，用户可以将`std::vector`置于无序状态，而我们的定制容器没有能力根据需要重新排序。\n\n接下来，让我们定义我们的构造函数(映射到`std::vector`提供的相同构造函数)。\n\n# 默认构造函数\n\n下面定义了我们的默认构造函数:\n\n```cpp\n    container() noexcept(noexcept(Allocator()))\n    {\n        std::cout << \"1\\n\";\n    }\n```\n\n由于`std::vector`的默认构造函数产生一个空向量，因此我们没有额外的逻辑必须添加，因为空向量是默认排序的。接下来，我们必须定义一个接受自定义分配器的构造函数。\n\n# 自定义分配器构造函数\n\n我们的自定义分配器构造函数定义如下:\n\n```cpp\n    explicit container(\n        const Allocator &alloc\n    ) noexcept :\n        m_v(alloc)\n    {\n        std::cout << \"2\\n\";\n    }\n```\n\n和前面的构造函数一样，这个构造函数创建了一个空向量，但是有一个已经存在的分配器。\n\n# 计算构造函数\n\n接下来的两个构造函数允许 API 的用户设置向量的最小大小，如下所示:\n\n```cpp\n    container(\n        size_type count,\n        const T &value,\n        const Allocator &alloc = Allocator()\n    ) :\n        m_v(count, value, alloc)\n    {\n        std::cout << \"3\\n\";\n    }\n\n    explicit container(\n        size_type count,\n        const Allocator &alloc = Allocator()\n    ) :\n        m_v(count, alloc)\n    {\n        std::cout << \"4\\n\";\n    }\n```\n\n第一个构造函数将创建`count`元素的向量，所有元素都用`value`的值初始化，而第二个构造函数将创建具有默认值的元素(例如，整数向量将被初始化为零)。\n\n# 复制/移动构造函数\n\n为了支持复制和移动容器的能力，我们需要实现如下的复制和移动构造函数:\n\n```cpp\n    container(\n        const container &other,\n        const Allocator &alloc\n    ) :\n        m_v(other.m_v, alloc)\n    {\n        std::cout << \"5\\n\";\n    }\n\n    container(\n        container &&other\n    ) noexcept :\n        m_v(std::move(other.m_v))\n    {\n        std::cout << \"6\\n\";\n    }\n```\n\n因为我们的自定义包装容器必须始终保持排序顺序，所以将一个容器复制或移动到另一个容器不会改变容器中元素的顺序，这意味着这些构造函数也不需要排序操作。然而，我们确实特别注意通过复制或移动我们的容器封装的内部`std::vector`来确保复制和移动正确发生。\n\n为了完整起见，我们还提供了一个移动构造函数，它允许我们像`std::vector`一样，在提供自定义分配器的同时进行移动，如下所示:\n\n```cpp\n    container(\n        container &&other,\n        const Allocator &alloc\n    ) :\n        m_v(std::move(other.m_v), alloc)\n    {\n        std::cout << \"7\\n\";\n    }\n```\n\n接下来，我们将提供一个接受初始化列表的构造函数。\n\n# 初始化列表构造函数\n\n最后，我们还将添加一个接受初始值设定项列表的构造函数，如下所示:\n\n```cpp\n    container(\n        std::initializer_list<T> init,\n        const Allocator &alloc = Allocator()\n    ) :\n        m_v(init, alloc)\n    {\n        std::sort(m_v.begin(), m_v.end(), compare_type());\n        std::cout << \"8\\n\";\n    }\n```\n\n如前面的代码所示，初始化列表可以以任何顺序为`std::vector`提供初始元素。因此，我们必须在向量初始化后对列表进行排序。\n\n# 使用\n\n让我们测试这个容器，以确保每个构造函数都按预期工作:\n\n```cpp\nint main(void)\n{\n    auto alloc = std::allocator<int>();\n\n    container<int> c1;\n    container<int> c2(alloc);\n    container<int> c3(42, 42);\n    container<int> c4(42);\n    container<int> c5(c1, alloc);\n    container<int> c6(std::move(c1));\n    container<int> c7(std::move(c2), alloc);\n    container<int> c8{4, 42, 15, 8, 23, 16};\n\n    return 0;\n}\n```\n\n如前面的代码块所示，我们通过调用每个构造函数来测试我们的构造函数，这将产生以下输出:\n\n![](img/d05b7686-5517-4965-80e9-17420fc8564b.png)\n\n如您所见，每个构造函数都按预期成功执行了。\n\n# 向我们的容器中添加元素\n\n有了我们的构造函数，我们还需要提供手动向容器添加数据的能力(例如，如果我们最初使用默认构造函数创建了容器)。\n\n首先，让我们关注一下`std::vector`提供的`push_back()`功能:\n\n```cpp\n    void push_back(const T &value)\n    {\n        m_v.push_back(value);\n        std::sort(m_v.begin(), m_v.end(), compare_type());\n\n        std::cout << \"1\\n\";\n    }\n\n    void push_back(T &&value)\n    {\n        m_v.push_back(std::move(value));\n        std::sort(m_v.begin(), m_v.end(), compare_type());\n\n        std::cout << \"2\\n\";\n    }\n```\n\n如前面的代码片段所示，`push_back()`函数具有与版本`std::vector`相同的函数签名，允许我们简单地将函数调用转发给`std::vector`。问题是，将一个值推到`std::vector`的末尾可能会导致`std::vector`进入无序状态，要求我们在每次推送时对`std::vector`进行重新排序(要求`std::vector`始终保持有序的结果)。\n\n解决这个问题的一种方法是向容器包装器添加另一个成员变量，该变量跟踪`std::vector`何时被污染。实现这些功能的另一种方法是按排序顺序添加元素(也就是说，遍历向量排序顺序并将元素放在适当的位置，根据需要移动剩余的元素)。如果元素很少被添加到`std::vector`中，那么这种方法可能会优于调用`std::sort`。然而，如果元素被大量添加到`std::vector`中，那么受污染的方法可能会表现得更好。\n\n创建容器包装器的一个主要好处是，这些类型的优化可以在不改变依赖于容器本身的代码的情况下实现和测试。两种实现(或其他)都可以被实现、测试和比较，以确定哪种优化最适合您的特定需求，而使用容器的代码永远不会改变。这不仅清理了代码，而且增加的封装触及了面向对象设计的核心，确保代码中的每个对象只有一个目的。在容器包装器的情况下，目的是封装以排序顺序维护`std::vector`的操作。\n\n为了完整起见，我们还会增加`push_back()`的`emplace_back()`版本，就像`std::vector`一样:\n\n```cpp\n    template<typename... Args>\n    void emplace_back(Args&&... args)\n    {\n        m_v.emplace_back(std::forward<Args>(args)...);\n        std::sort(m_v.begin(), m_v.end(), compare_type());\n\n        std::cout << \"3\\n\";\n    }\n```\n\n与等效的`std::vector`相比，`emplace_back()`函数的不同之处在于，我们的版本不返回对所创建元素的引用。这是因为排序会使引用无效，从而无法返回有效的引用。\n\n# 推送/定位的使用\n\n最后，让我们测试一下我们的`push_back()`和`emplace`函数，以确保它们被正确调用，如下所示:\n\n```cpp\nint main(void)\n{\n    int i = 42;\n    container<int> c;\n\n    c.push_back(i);\n    c.push_back(std::move(i));\n    c.emplace_back(42);\n\n    return 0;\n}\n```\n\n如前面的代码片段所示，我们调用每个版本的`push_back()`以及`emplace_back()`函数，以确保它们按照预期被正确调用，结果如下:\n\n![](img/51e13fb0-f3e4-460b-8109-137c99e246ed.png)\n\n我们可以更进一步，向我们的测试容器添加更好的测试数据，如下所示:\n\n```cpp\nint main(void)\n{\n    int i = 42;\n    container<int> c;\n\n    c.emplace_back(4);\n    c.push_back(i);\n    c.emplace_back(15);\n    c.push_back(8);\n    c.emplace_back(23);\n    c.push_back(std::move(16));\n\n    return 0;\n}\n```\n\n如前面的代码片段所示，我们将整数`4`、`42`、`15`、`8`、`23`和`16`添加到向量中。在下一个配方中，我们将从`std::set`中窃取 API，以向我们的容器提供更好的`push`和`emplace`API，以及一个输出函数，以更好地了解`std::vector`包含什么以及它包含元素的顺序。\n\n# 添加标准::集合应用编程接口的相关部分\n\n在这个食谱中，我们将学习如何将`std::set`中的 API 添加到我们在第一个食谱中创建的自定义容器中。具体来说，我们将了解为什么`std::vector::push_back()`和`std::vector::emplace_back()`在与我们的自定义容器一起使用时没有意义，该容器总是以排序顺序维护其内部元素。\n\n# 准备好\n\n开始之前，请确保满足所有技术要求，包括安装 Ubuntu 18.04 或更高版本，并在终端窗口中运行以下内容:\n\n```cpp\n> sudo apt-get install build-essential git cmake\n```\n\n这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后，打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。\n\n# 怎么做...\n\n通过以下步骤来尝试配方:\n\n1.  从新的终端，运行以下命令下载源代码:\n\n```cpp\n> cd ~/\n> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git\n> cd Advanced-CPP-CookBook/chapter08\n```\n\n2.  要编译源代码，请运行以下命令:\n\n```cpp\n> cmake .\n> make recipe02_examples\n```\n\n3.  编译源代码后，您可以通过运行以下命令来执行该配方中的每个示例:\n\n```cpp\n> ./recipe02_example01 \nelements: 4 \nelements: 4 42 \nelements: 4 15 42 \nelements: 4 8 15 42 \nelements: 4 8 15 23 42 \nelements: 4 8 15 16 23 42 \n```\n\n在下一节中，我们将逐一介绍这些示例，并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。\n\n# 它是如何工作的...\n\n在本章的第一个食谱中，我们创建了一个定制的容器包装器，它模仿了一个`std::vector`，但是它确保了向量中的元素始终保持有序的顺序，包括添加`std::vector::push_back()`函数和`std::vector::emplace_back()`函数。在本食谱中，我们将把`std::set::insert()`和`std::set::emplace()`功能添加到我们的定制容器中。\n\n因为我们的容器包装器总是确保`std::vector`是有序的，所以在向量的前面、后面或中间添加一个元素没有区别。无论元素添加到向量的哪个位置，都必须在访问向量之前对其进行排序，这意味着无论元素添加到哪个位置，元素的添加顺序都可能发生变化。\n\n这种对元素添加位置的不关心类似于`std::set`。`std::set`向集合中添加元素，然后返回`true`或`false`，这取决于被测试的元素是否是集合的成员。`std::set`提供`insert()`和`emplace()`功能，为器械包添加元素。让我们将这些相同的 API 添加到自定义容器中，如下所示:\n\n```cpp\n    void insert(const T &value)\n    {\n        push_back(value);\n    }\n\n    void insert(T &&value)\n    {\n        push_back(std::move(value));\n    }\n\n    template<typename... Args>\n    void emplace(Args&&... args)\n    {\n        emplace_back(std::forward<Args>(args)...);\n    }\n```\n\n正如您在前面的代码片段中看到的，我们添加了一个`insert()`函数(复制和移动)，以及一个`emplace()`函数，它只不过是调用它们的`push_back()`和`emplace_back()`等价物，确保传递给这些函数的参数被正确转发。这些应用编程接口和我们在之前的配方*中添加的应用编程接口之间的唯一区别是函数本身的名称，该配方使用了一个简单的 std::vector* 包装器。\n\n虽然这样的改变看起来微不足道，但这很重要，因为它重新定义了容器的 API 和用户之间的概念。`push_back()`和`emplace_back()`函数建议将元素添加到向量的后面，而事实上并不是。相反，它们被简单地添加到`std::vector`中，并且`std::vector`的顺序根据所添加元素的值而改变。因此，`push_back()`和`emplace_back()`功能是需要的，但是应该要么重命名，要么标记为私有，以确保用户只使用`insert()`和`emplace()`版本来正确管理期望。当编写您自己的容器(甚至是包装器)时，坚持最少惊讶的原则是很重要的，这确保了用户正在使用的应用编程接口将按照应用编程接口可能建议的方式工作。\n\n# 使用迭代器\n\n在本食谱中，我们将学习如何向我们在第一个食谱中开始的自定义容器添加迭代器支持，该容器包装了一个`std::vector`，确保其内容始终保持排序顺序。\n\n为了增加迭代器支持，我们将学习如何转发`std::vector`已经提供的迭代器(我们不会从头实现迭代器，因为这是本书范围之外的一个主题，因为从头实现容器非常困难)。\n\n# 准备好\n\n开始之前，请确保满足所有技术要求，包括安装 Ubuntu 18.04 或更高版本，并在终端窗口中运行以下内容:\n\n```cpp\n> sudo apt-get install build-essential git cmake\n```\n\n这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后，打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。\n\n# 怎么做...\n\n你需要通过以下步骤来尝试这个食谱:\n\n1.  从新的终端，运行以下命令下载源代码:\n\n```cpp\n> cd ~/\n> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git\n> cd Advanced-CPP-CookBook/chapter08\n```\n\n2.  要编译源代码，请运行以下命令:\n\n```cpp\n> cmake .\n> make recipe03_examples\n```\n\n3.  编译源代码后，您可以通过运行以下命令来执行该配方中的每个示例:\n\n```cpp\n> ./recipe03_example01 \nelements: 4 8 15 16 23 42 \n\n> ./recipe03_example02 \nelements: 4 8 15 16 23 42 \nelements: 4 8 15 16 23 42 \nelements: 42 23 16 15 8 4 \nelements: 1 4 8 15 16 23 42 \nelements: 4 8 15 16 23 42 \nelements: \n```\n\n在下一节中，我们将逐一介绍这些示例，并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。\n\n# 它是如何工作的...\n\n我们的定制容器包装的`std::vector`已经提供了迭代器的有效实现，可以用于我们的容器。然而，我们需要转发`std::vector`提供的 API 的特定部分，以确保迭代器正常工作，包括关键的 C++ 特性，例如基于范围的循环。\n\n首先，让我们将最后一个剩余的构造函数`std::vector`添加到我们的自定义容器中:\n\n```cpp\n    template <typename Iter>\n    container(\n        Iter first,\n        Iter last,\n        const Allocator &alloc = Allocator()\n    ) :\n        m_v(first, last, alloc)\n    {\n        std::sort(m_v.begin(), m_v.end(), compare_type());\n    }\n```\n\n如前面的代码片段所示，我们给定的迭代器类型没有定义。迭代器可以来自我们容器的另一个实例，也可以直接来自`std::vector`，它不按排序顺序存储元素。即使迭代器来自我们定制容器的实例，迭代器存储元素的顺序也可能与容器元素的顺序不同。因此，我们必须在`std::vector`初始化后对其进行排序。\n\n除了构造，重要的是我们的定制容器还包括`std::vector`提供的基于迭代器的别名，因为这些别名是容器正确使用 c++ API 所必需的。下面是它的示例代码片段:\n\n```cpp\n    using const_iterator = typename vector_type::const_iterator;\n    using const_reverse_iterator = typename vector_type::const_reverse_iterator;\n```\n\n如前面的代码片段所示，像第一个配方中定义的别名一样，我们只需要转发声明`std::vector`已经提供的别名，这样我们的定制容器也可以利用它们。不同的是，我们不包括这些迭代器别名的非常数版本。由于我们的自定义容器必须始终保持数据的排序顺序，因此我们必须限制用户直接修改迭代器内容的能力，因为这可能会导致容器元素的顺序发生变化，而我们的容器无法根据需要重新排序。相反，应通过使用`insert()`、`emplace()`和`erase()`对容器进行修改。\n\nC++ template-based functions rely on these aliases to properly implement their features, which also include range-based for loops.\n\n最后，`std::vector`提供的一系列基于迭代器的成员函数也应该通过我们的定制容器转发。下面的代码描述了这一点:\n\n```cpp\n    const_iterator begin() const noexcept\n    {\n        return m_v.begin();\n    }\n\n    const_iterator cbegin() const noexcept\n    {\n        return m_v.cbegin();\n    }\n```\n\n第一组成员函数是`begin()`函数，它提供了一个迭代器来表示`std::vector`中的第一个元素。与别名一样，我们不转发这些成员函数的非常数版本。此外，为了完整起见，我们包括了这些函数的`c`版本。在 C++ 17 中，这些是可选的，因为如果愿意，您可以使用`std::as_const()`来代替。下一组迭代器是`end()`迭代器，它提供了一个表示`std::vector`结尾的迭代器(不要与表示`std::vector`中最后一个元素的迭代器混淆)。下面的代码显示了这一点:\n\n```cpp\n    const_iterator end() const noexcept\n    {\n        return m_v.end();\n    }\n\n    const_iterator cend() const noexcept\n    {\n        return m_v.cend();\n    }\n```\n\n如前面的代码片段所示，和大多数成员函数一样，我们只需要将 API 转发到我们的定制容器封装的私有`std::vector`中。这个相同的过程可以对`rbegin()`和`rend()`重复，它们提供了与前面相同的 API，但是返回一个反向迭代器，它以相反的顺序遍历`std::vector`。\n\n接下来，我们实现基于迭代器的`emplace()`函数，如下所示:\n\n```cpp\n    template <typename... Args>\n    void emplace(const_iterator pos, Args&&... args)\n    {\n        m_v.emplace(pos, std::forward<Args>(args)...);\n        std::sort(m_v.begin(), m_v.end(), compare_type());\n    }\n```\n\n虽然提供`emplace()` API 提供了一个更完整的实现，但是应该注意的是，只有进一步优化以利用元素添加到容器的方式中的预期位置，它才会有用。这与对`std::vector`进行排序的更好方法相结合。\n\n虽然前面的实现是可行的，但是它的性能很可能类似于我们在第一个配方中实现的`emplace()`版本。由于自定义容器始终保持排序顺序，元素插入到`std::vector`的位置无关紧要，因为`std::vector`的新顺序将改变被添加元素的位置。这是当然的，除非 position 参数的添加为 API 提供了一些额外的支持来更好地优化添加，而我们的实现并没有这样做。因此，除非`pos`参数用于优化，否则前面的函数可能是多余和不必要的。\n\n与前面的`emplace()`函数一样，我们不试图返回表示添加到容器中的元素的迭代器，因为这个迭代器在排序后变得无效，并且没有足够的关于添加到`std::vector`中的内容的信息来重新定位迭代器(例如，如果存在重复，就无法知道哪个元素实际上刚刚被添加)。\n\n最后，我们实现`erase`功能，如下所示:\n\n```cpp\n    const_iterator erase(const_iterator pos)\n    {\n        return m_v.erase(pos);\n    }\n\n    const_iterator erase(const_iterator first, const_iterator last)\n    {\n        return m_v.erase(first, last);\n    }\n```\n\n与`emplace()`功能不同，从`std::vector`中移除元素不会改变`std::vector`的顺序，因此不需要排序。还需要注意的是，我们版本的`erase()`功能返回了`const`版本。这再次是因为我们不能支持迭代器的非常数版本。\n\n最后，现在我们已经能够访问存储在容器中的元素，让我们创建一些测试逻辑来确保我们的容器按照预期工作:\n\n```cpp\nint main(void)\n{\n    container<int> c{4, 42, 15, 8, 23, 16};\n```\n\n首先，我们将从初始值设定项列表中创建一个容器，其中包含没有顺序的整数。在这个容器被创建之后，存储这些元素的`std::vector`应该是有序的。为了证明这一点，让我们遍历容器并输出结果:\n\n```cpp\n    std::cout << \"elements: \";\n\n    for (const auto &elem : c) {\n        std::cout << elem << ' ';\n    }\n\n    std::cout << '\\n';\n```\n\n如前面的代码片段所示，我们首先向`stdout`输出一个标签，然后使用基于范围的 for 循环迭代我们的容器，一次输出一个元素。最后，在所有元素都输出到`stdout`后，我们输出一个新的行，结果如下:\n\n```cpp\nelements: 4 8 15 16 23 42\n```\n\n正如预期的那样，该输出按排序顺序排列。\n\n需要注意的是，我们的 for 循环范围必须将每个元素定义为`const`。这是因为我们不支持迭代器的非常数版本。任何使用这些迭代器的非常数版本的尝试都会导致编译器错误，如下例所示:\n\n```cpp\n    for (auto &elem : c) {\n        elem = 42;\n    }\n```\n\n前面的代码导致以下编译器错误(这是有意的):\n\n```cpp\n/home/user/book/chapter08/recipe03.cpp: In function ‘int main()’:\n/home/user/book/chapter08/recipe03.cpp:396:14: error: assignment of read-only reference ‘elem’\n  396 | elem = 42;\n```\n\n发生此编译器错误的原因是因为 for 循环的范围也可以写成如下形式:\n\n```cpp\n    std::cout << \"elements: \";\n\n    for (auto iter = c.begin(); iter != c.end(); iter++) {\n        auto &elem = *iter;\n        std::cout << elem << ' ';\n    }\n\n    std::cout << '\\n';\n```\n\n如前面的代码片段所示，元素没有被标记为`const`，因为 ranged for 循环使用了`begin()`和`end()`成员函数，从而产生了读写迭代器(除非您明确声明`const`)。\n\n我们还可以为新的`emplace()`函数创建一个测试，如下所示:\n\n```cpp\n    c.emplace(c.cend(), 1);\n\n    std::cout << \"elements: \";\n    for (const auto &elem : c) {\n        std::cout << elem << ' ';\n    }\n    std::cout << '\\n';\n```\n\n这将产生以下输出:\n\n```cpp\nelements: 1 4 8 15 16 23 42\n```\n\n如前面的输出所示，数字`1`按照排序顺序添加到我们的容器中，尽管我们告诉容器将我们的元素添加到`std::vector`的末尾。\n\n我们还可以颠倒前面的操作，验证我们的`erase()`功能是否正常工作，如下所示:\n\n```cpp\n    c.erase(c.cbegin());\n\n    std::cout << \"elements: \";\n    for (const auto &elem : c) {\n        std::cout << elem << ' ';\n    }\n    std::cout << '\\n';\n```\n\n这将产生以下输出:\n\n```cpp\nelements: 4 8 15 16 23 42\n```\n\n可以看到，新增的`1`已经成功移除。\n\n# 添加标准::矢量应用编程接口的相关部分\n\n在这个食谱中，我们将通过添加`std::vector`已经提供的剩余 API 来完成我们在本章前三个食谱中构建的定制容器。在这个过程中，我们将删除没有意义的或者我们不能支持的 API，因为我们的定制容器必须将`std::vector`中的元素保持在有序的顺序中。\n\n这个方法很重要，因为它将向您展示如何正确地创建一个包装容器，该容器可以用容器所需的逻辑(例如，线程安全，或者在我们的例子中，元素顺序)来封装现有的容器。\n\n# 准备好\n\n开始之前，请确保满足所有技术要求，包括安装 Ubuntu 18.04 或更高版本，并在终端窗口中运行以下内容:\n\n```cpp\n> sudo apt-get install build-essential git cmake\n```\n\n这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后，打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。\n\n# 怎么做...\n\n通过以下步骤来尝试配方:\n\n1.  从新的终端，运行以下命令下载源代码:\n\n```cpp\n> cd ~/\n> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git\n> cd Advanced-CPP-CookBook/chapter08\n```\n\n2.  要编译源代码，请运行以下命令:\n\n```cpp\n> cmake .\n> make recipe04_examples\n```\n\n3.  编译源代码后，您可以通过运行以下命令来执行该配方中的每个示例:\n\n```cpp\n> ./recipe04_example01 \nelements: 4 8 15 16 23 42 \nelements: 4 8 15 16 23 42 \nelements: 4 8 15 16 23 42 \nelements: 42 \nelements: 4 8 15 16 23 42 \nelements: 4 8 15 16 23 42 \nc1.at(0): 4\nc1.front(): 4\nc1.back(): 42\nc1.data(): 0xc01eb0\nc1.empty(): 0\nc1.size(): 6\nc1.max_size(): 2305843009213693951\nc1.capacity(): 42\nc1.capacity(): 6\nc1.size(): 0\nc1.size(): 42\nc1.size(): 0\nc1.size(): 42\nelements: 4 8 15 16 23 \n==: 0\n!=: 1\n <: 1\n<=: 1\n >: 0\n>=: 0\n```\n\n在下一节中，我们将逐一介绍这些示例，并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。\n\n# 它是如何工作的...\n\n目前，我们的定制容器能够被构建、添加、迭代和删除。然而，容器不支持直接访问容器的能力，也不支持简单的操作，例如`std::move()`或比较。为了解决这些问题，让我们从添加缺失的`operator=()`重载开始:\n\n```cpp\n    constexpr container &operator=(const container &other)\n    {\n        m_v = other.m_v;\n        return *this;\n    }\n\n    constexpr container &operator=(container &&other) noexcept\n    {\n        m_v = std::move(other.m_v);\n        return *this;\n    }    \n```\n\n第一个`operator=()`重载为复制分配提供支持，而第二个重载为移动分配提供支持。因为我们只有一个已经提供了适当的复制和移动语义的私有成员变量，所以我们不需要担心自我赋值(或移动)，因为`std::vector`函数的复制和移动实现将为我们处理这一点。\n\n如果您自己的定制容器有额外的私有元素，那么很可能需要自我赋值检查。例如，考虑以下代码:\n\n```cpp\n    constexpr container &operator=(container &&other) noexcept\n    {\n        if (&other == this) {\n            return *this;\n        }\n\n        m_v = std::move(other.m_v);\n        m_something = other.m_something;\n\n        return *this;\n    }\n```\n\n剩余的`operator=()`重载取一个初始化列表，如下所示:\n\n```cpp\n    constexpr container &operator=(std::initializer_list<T> list)\n    {\n        m_v = list;\n        std::sort(m_v.begin(), m_v.end(), compare_type());\n\n        return *this;\n    }\n```\n\n如前面的代码片段所示，像初始化列表构造函数一样，我们必须在赋值后对`std::vector`重新排序，因为初始化列表可以以任何顺序提供。\n\n接下来要实现的成员函数是`assign()`函数。下面的代码片段显示了这一点:\n\n```cpp\n    constexpr void assign(size_type count, const T &value)\n    {\n        m_v.assign(count, value);\n    }\n\n    template <typename Iter>\n    constexpr void assign(Iter first, Iter last)\n    {\n        m_v.assign(first, last);\n        std::sort(m_v.begin(), m_v.end(), compare_type());\n    }\n\n    constexpr void assign(std::initializer_list<T> list)\n    {\n        m_v.assign(list);\n        std::sort(m_v.begin(), m_v.end(), compare_type());\n    }\n```\n\n这些函数类似于`operator=()`重载，但不提供返回值或支持附加功能。让我们看看如何:\n\n*   第一个`assign()`功能用特定的`value`计数次数填充`std::vector`。由于该值从不改变，`std::vector`将始终按排序顺序排列，在这种情况下，无需对列表进行排序。\n*   第二个`assign()`函数取一个迭代器范围，类似于这个函数的构造器版本。像那个函数一样，传递给这个函数的迭代器可能来自原始的`std::vector`或者我们定制容器的另一个实例，但是排序顺序不同。为此，我们必须在作业后整理`std::vector`。\n*   最后，`assign()`函数还提供了一个初始化列表版本，与我们的`operator=()`重载相同。\n\n还需要注意的是，我们已经在每个功能中添加了`constexpr`。这是因为我们的定制容器中的大多数功能只不过是将一个调用从定制容器转发到`std::vector`，在某些情况下，还会调用`std::sort()`。`constexpr`的添加告诉编译器将代码视为编译时表达式，使其能够在启用优化时优化掉额外的函数调用(如果可能)，从而确保我们的自定义包装器具有尽可能小的开销。\n\n过去，这种类型的优化是使用`inline`关键字进行的。在 C++ 11 中添加的`constexpr`不仅能够为编译器提供`inline`提示，还能告诉编译器这个函数可以在编译时使用，而不是在运行时使用(这意味着编译器可以在编译代码时执行这个函数，以执行定制的编译时逻辑)。然而，在我们这里的例子中，运行时使用`std::vector`是不可能的，因为需要分配。因此，`constexpr`的使用只是为了优化，在大多数编译器上，`inline`关键字会提供类似的好处。\n\n还有很多`std::vector`也支持的附加功能，比如`get_allocator()`、`empty()`、`size()`、`max_size()`，都只是直接转发。让我们把重点放在我们的定制容器中到目前为止缺少的访问器上:\n\n```cpp\n    constexpr const_reference at(size_type pos) const\n    {\n        return m_v.at(pos);\n    }\n```\n\n我们提供的第一个直接访问`std::vector`的功能是`at()`功能。与我们的大多数成员职能一样，这是一个直接的前进。然而，与`std::vector`不同，我们没有计划增加`std::vector`提供的`operator[]()`过载。`at()`函数和`operator[]()`重载的区别在于`operator[]()`不检查以确保提供的索引在边界内(也就是说，它不访问`std::vector`边界外的元素)。\n\n`operator[]()`重载的功能类似于标准的 C 数组。这个操作符(被称为下标操作符)的问题是，缺少边界检查为可靠性和安全性缺陷进入您的程序打开了大门。因此，C++ 核心准则不鼓励使用下标运算符或任何其他形式的指针算法(任何试图通过使用指针计算数据位置而不进行显式边界检查的方法)。\n\n为了防止使用`operator[]()`过载，我们不包括它。\n\n像`std::vector`一样，我们也可以添加`front()`和`back()`访问器，如下所示:\n\n```cpp\n    constexpr const_reference front() const\n    {\n        return m_v.front();\n    }\n\n    constexpr const_reference back() const\n    {\n        return m_v.back();\n    }\n```\n\n前面的附加访问器为获取我们的`std::vector`中的第一个和最后一个元素提供了支持。与`at()`功能一样，我们只支持使用`std::vector`已经提供的这些功能的`const_reference`版本。\n\n现在我们来看代码片段`data()`函数:\n\n```cpp\n    constexpr const T* data() const noexcept\n    {\n        return m_v.data();\n    }\n```\n\n`data()`功能也是如此。我们只能支持这些成员函数的`const`版本，因为提供这些函数的非常量版本将为用户提供对`std::vector`的直接访问，允许他们插入无序数据，而容器没有能力根据需要重新排序。\n\n现在让我们关注比较运算符。我们首先将比较运算符的原型定义为容器的朋友。这是需要的，因为比较运算符通常实现为非成员函数，因此需要对容器的私有访问来比较它们包含的`std::vector`实例。\n\n例如，考虑以下代码片段:\n\n```cpp\n    template <typename O, typename Alloc>\n    friend constexpr bool operator==(const container<O, Alloc> &lhs,\n                                     const container<O, Alloc> &rhs);\n\n    template <typename O, typename Alloc>\n    friend constexpr bool operator!=(const container<O, Alloc> &lhs,\n                                     const container<O, Alloc> &rhs);\n\n    template <typename O, typename Alloc>\n    friend constexpr bool operator<(const container<O, Alloc> &lhs,\n                                    const container<O, Alloc> &rhs);\n\n    template <typename O, typename Alloc>\n    friend constexpr bool operator<=(const container<O, Alloc> &lhs,\n                                     const container<O, Alloc> &rhs);\n\n    template <typename O, typename Alloc>\n    friend constexpr bool operator>(const container<O, Alloc> &lhs,\n                                    const container<O, Alloc> &rhs);\n\n    template <typename O, typename Alloc>\n    friend constexpr bool operator>=(const container<O, Alloc> &lhs,\n                                     const container<O, Alloc> &rhs);\n```\n\n最后，我们按如下方式实现比较运算符:\n\n```cpp\ntemplate <typename O, typename Alloc>\nbool constexpr operator==(const container<O, Alloc> &lhs,\n                          const container<O, Alloc> &rhs)\n{\n    return lhs.m_v == rhs.m_v;\n}\n\ntemplate <typename O, typename Alloc>\nbool constexpr operator!=(const container<O, Alloc> &lhs,\n                          const container<O, Alloc> &rhs)\n{\n    return lhs.m_v != rhs.m_v;\n}\n```\n\n和成员函数一样，我们只需要将调用转发到`std::vector`，因为不需要实现自定义逻辑。这同样适用于其余的比较运算符。\n\n例如，我们可以如下实现`>`、`<`、`>=`和`<=`比较运算符:\n\n```cpp\ntemplate <typename O, typename Alloc>\nbool constexpr operator<(const container<O, Alloc> &lhs,\n                         const container<O, Alloc> &rhs)\n{\n    return lhs.m_v < rhs.m_v;\n}\n\ntemplate <typename O, typename Alloc>\nbool constexpr operator<=(const container<O, Alloc> &lhs,\n                          const container<O, Alloc> &rhs)\n{\n    return lhs.m_v <= rhs.m_v;\n}\n\ntemplate <typename O, typename Alloc>\nbool constexpr operator>(const container<O, Alloc> &lhs,\n                         const container<O, Alloc> &rhs)\n{\n    return lhs.m_v > rhs.m_v;\n}\n\ntemplate <typename O, typename Alloc>\nbool constexpr operator>=(const container<O, Alloc> &lhs,\n                          const container<O, Alloc> &rhs)\n{\n    return lhs.m_v >= rhs.m_v;\n}\n```\n\n就是这样！这就是如何通过利用现有的容器来实现自己的容器。\n\n正如我们所看到的，在大多数情况下，没有必要从头实现容器，除非您需要的容器不能使用 C++ 标准模板库已经提供的容器之一来实现。\n\n使用这种方法，不仅可以创建自己的容器，而且更重要的是，可以将代码中重复的功能封装到一个可以独立测试和验证的容器中。这不仅提高了应用的可靠性，还使它们更容易阅读和维护。\n\n在下一章中，我们将探讨如何在 C++ 中使用智能指针。"
  },
  {
    "path": "docs/adv-cpp-prog-cb/09.md",
    "content": "# 九、探索类型擦除\n\n在本章中，您将了解什么是类型擦除(也称为类型擦除)，以及如何在您自己的应用中使用它。本章很重要，因为类型擦除提供了处理不同类型对象的能力，而不需要对象共享一个公共基类。\n\n本章首先简单解释了类型擦除，解释了类型擦除在 C 语言中是如何工作的，以及如何使用继承在 C++ 中执行类型擦除。下一个方法将提供一种使用 C++ 模板进行类型擦除的不同方法，它将教您如何使用 C++ 概念来定义类型的规范，而不是类型本身。\n\n接下来，我们将介绍经典的 C++ 类型擦除模式。这个食谱将教你擦除类型信息的技巧，提供创建类型安全的通用代码的能力。最后，我们将以一个使用类型擦除来实现委托模式的综合示例来结束，这是一种提供包装任何类型的可调用对象的能力的模式，被像 ObjC 这样的语言大量使用。\n\n本章中的配方如下:\n\n*   如何删除具有继承性的类型\n*   使用 C++ 模板编写泛型函数\n*   学习 C++ 类型的橡皮擦模式\n*   实现委托模式\n\n# 技术要求\n\n要编译和运行本章中的示例，您必须拥有运行 Ubuntu 18.04 的计算机的管理权限，并且具有功能性互联网连接。在运行这些示例之前，您必须安装以下内容:\n\n```cpp\n> sudo apt-get install build-essential git cmake\n```\n\n如果这安装在 Ubuntu 18.04 以外的任何操作系统上，则需要 GCC 7.4 或更高版本以及 CMake 3.6 或更高版本。\n\n本章的代码文件可以在[https://github . com/packt publishing/Advanced-CPP-cook book/tree/master/chapter 09](https://github.com/PacktPublishing/Advanced-CPP-CookBook/tree/master/chapter09)找到。\n\n# 如何删除具有继承性的类型\n\n在这个食谱中，我们将学习如何使用继承来删除类型。当讨论类型擦除时，通常不考虑继承，但在现实中，它是 C++ 中使用的最常见的类型擦除形式。这个方法很重要，因为它将讨论什么是类型擦除，以及为什么它在日常应用中如此有用，而不仅仅是删除类型信息——这是 c 语言中常见的做法。\n\n# 准备好\n\n开始之前，请确保满足所有技术要求，包括安装 Ubuntu 18.04 或更高版本，并在终端窗口中运行以下内容:\n\n```cpp\n> sudo apt-get install build-essential git cmake\n```\n\n这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后，打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。\n\n# 怎么做...\n\n让我们按照以下步骤来尝试这个食谱:\n\n1.  从新的终端，运行以下命令下载源代码:\n\n```cpp\n> cd ~/\n> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git\n> cd Advanced-CPP-CookBook/chapter09\n```\n\n2.  要编译源代码，请运行以下命令:\n\n```cpp\n> cmake .\n> make recipe01_examples\n```\n\n3.  编译源代码后，您可以通过运行以下命令来执行该配方中的每个示例:\n\n```cpp\n> ./recipe01_example01 \n1\n0\n```\n\n在下一节中，我们将逐一介绍这些示例，并解释每个示例程序的功能，以及它与本食谱中所教授的课程之间的关系。\n\n# 它是如何工作的...\n\n类型擦除(或类型擦除)只是移除、隐藏或减少关于对象、功能等的类型信息的行为。在 C 语言中，类型擦除一直被使用。看看这个例子:\n\n```cpp\nint array[10];\nmemset(array, 0, sizeof(array));\n```\n\n在前面的例子中，我们创建了一个`10`元素的数组，然后我们使用`memset()`函数将数组清零。C 语言中的`memset()`函数看起来是这样的:\n\n```cpp\nvoid *memset(void *ptr, int value, size_t num)\n{\n    size_t i;\n    for (i = 0; i < num; i++) {\n        ((char *)ptr)[i] = value;    \n    }\n\n    return ptr;\n}\n```\n\n如前面的代码片段所示，`memset()`函数取的第一个参数是`void*`。然而，前面例子中的数组是整数数组。`memset()`函数实际上并不关心您提供了什么类型，只要您提供了一个指向该类型的指针和一个以字节为单位表示该类型总大小的大小。`memset()`函数然后将提供的指针类型转换为代表一个字节的类型(在 C 语言中，这通常是`char`或无符号的`char`，然后逐字节设置该类型的值。\n\n在 C 中使用`void*`是类型擦除的一种形式。在 C++ 中，这种类型(双关)的擦除通常是不鼓励的，因为获取类型信息的唯一方法是使用`dynamic_cast()`，这很慢(它需要运行时类型信息查找)。虽然在 C++ 中有很多方法可以执行类型擦除而不需要`void *`，但是让我们把重点放在继承上。\n\n在大多数文献中，继承通常不被描述为类型擦除，但它可能是最广泛使用的类型擦除形式。为了更好地探索这是如何工作的，让我们看一个常见的例子。假设我们正在创建一个有多个超级英雄可供用户选择的游戏。每个超级英雄在某个时刻都要攻击坏人，但是超级英雄如何攻击坏人，每个英雄都不一样。\n\n例如，考虑以下代码片段:\n\n```cpp\nclass spiderman\n{\npublic:\n    bool attack(int x, int) const\n    {\n        return x == 0 ? true : false;\n    }\n};\n```\n\n如前面的代码片段所示，我们的第一个英雄不在乎坏人是在地上还是在空中(也就是说，无论坏人的垂直距离如何，英雄都会成功击中坏人)，但是如果他们不在特定的水平位置，就会错过坏人。同样，我们也可能有另一个英雄如下:\n\n```cpp\nclass captain_america\n{\npublic:\n    bool attack(int, int y) const\n    {\n        return y == 0 ? true : false;\n    }\n};\n```\n\n第二个英雄与我们的第一个完全相反。这个英雄可以在地面上的任何地方成功击中坏人，但是如果坏人在地面上的任何地方，这个英雄就会错过(英雄可能够不到他们)。\n\n在下面的例子中，两个超级英雄同时在和坏人战斗:\n\n```cpp\n    for (const auto &h : heroes) {\n        std::cout << h->attack(0, 42) << '\\n';\n    }\n```\n\n虽然我们可以在战斗中一次给每个超级英雄打一个电话，但如果我们能在战斗中循环每个英雄，看看哪个英雄击中了坏人，哪个英雄错过了坏人，那就方便多了。\n\n在前面的例子中，我们有一个假设的英雄数组，我们循环遍历，检查哪个英雄命中，哪个英雄没有命中。在这个例子中，我们不关心英雄的类型(也就是说，我们不关心英雄具体是我们的第一个英雄还是第二个英雄)，我们只关心每个英雄实际上是一个英雄(而不是一个无生命的物体)，以及英雄有能力攻击坏人。换句话说，我们需要一种方法来删除每个超级英雄的类型，这样我们就可以将两个英雄放在一个数组中(除非每个英雄都是相同的，否则这是不可能的)。\n\n正如您可能已经猜到的，在 C++ 中实现这一点最常见的方法是使用继承(但是正如我们将在本章后面展示的，这不是唯一的方法)。首先，我们必须首先定义一个名为`hero`的基类，每个英雄都将继承这个基类，如下所示:\n\n```cpp\nclass hero\n{\npublic:\n    virtual ~hero() = default;\n    virtual bool attack(int, int) const = 0;\n};\n```\n\n在我们的例子中，每个英雄之间唯一的共同功能是他们都可以攻击坏人，`attack()`功能对所有英雄都是一样的。因此，我们创建了一个纯虚拟基类，其中有一个名为`attack()`的纯虚拟函数，每个英雄都必须实现它。还需要注意的是，一个类要成为纯虚函数，所有成员函数都必须设置为`0`，该类的析构函数必须显式标记为`virtual`。\n\n现在我们已经定义了什么是英雄，我们可以修改我们的英雄来继承这个纯虚拟基类，如下所示:\n\n```cpp\nclass spiderman : public hero\n{\npublic:\n    bool attack(int x, int) const override\n    {\n        return x == 0 ? true : false;\n    }\n};\n\nclass captain_america : public hero\n{\npublic:\n    bool attack(int, int y) const override\n    {\n        return y == 0 ? true : false;\n    }\n};\n```\n\n如图所示，两个英雄都继承了英雄的纯虚拟定义，并根据需要覆盖了`attack()`功能。有了这个修改，我们现在可以创建我们的英雄列表如下:\n\n```cpp\nint main(void)\n{\n    std::array<std::unique_ptr<hero>, 2> heros {\n        std::make_unique<spiderman>(),\n        std::make_unique<captain_america>()\n    };\n\n    for (const auto &h : heros) {\n        std::cout << h->attack(0, 42) << '\\n';\n    }\n\n    return 0;\n}\n```\n\n从前面的代码中，我们观察到以下情况:\n\n*   我们创建一个`hero`指针数组(使用`std::unique_ptr`来存储英雄的寿命，这个话题将在下一章讨论)。\n*   然后这个数组被初始化为包含两个英雄(每个英雄一个)。\n*   最后，我们循环每个英雄，看看英雄是成功攻击坏人还是失手。\n*   当调用`hero::attack()`函数时，调用会根据需要通过使用继承自动路由到正确的`spiderman::attack()`和`captain_america::attack()`函数。\n\n该阵列正在以类型安全的方式擦除每个英雄的类型信息，以将每个英雄放入一个容器中。\n\n# 使用 C++ 模板编写泛型函数\n\n在这个食谱中，我们将学习如何利用 C++ 模板擦除(或忽略)类型信息。您将学习如何使用 C++ 模板来实现 C++ 概念，以及如何在 C++ 标准库中使用这种类型的擦除。这个方法很重要，因为它将教会你如何更好地将你的 API 设计成不依赖于特定类型的规范(或者，换句话说，如何编写通用代码)。\n\n# 准备好\n\n开始之前，请确保满足所有技术要求，包括安装 Ubuntu 18.04 或更高版本，并在终端窗口中运行以下内容:\n\n```cpp\n> sudo apt-get install build-essential git cmake\n```\n\n这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后，打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。\n\n# 怎么做...\n\n让我们按照以下步骤来尝试这个食谱:\n\n1.  从新的终端，运行以下命令下载源代码:\n\n```cpp\n> cd ~/\n> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git\n> cd Advanced-CPP-CookBook/chapter09\n```\n\n2.  要编译源代码，请运行以下命令:\n\n```cpp\n> cmake .\n> make recipe02_examples\n```\n\n3.  编译源代码后，您可以通过运行以下命令来执行该配方中的每个示例:\n\n```cpp\n> ./recipe02_example01 \nhero won fight\nhero lost the fight :(\n```\n\n在下一节中，我们将逐一介绍这些示例，并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。\n\n# 它是如何工作的...\n\nC++ 最古老、使用最广泛的特性之一是 C++ 模板。像继承一样，C++ 模板通常不被描述为类型擦除的一种形式，但它们确实是。类型擦除只不过是删除或者在这种情况下忽略类型信息的行为。\n\n然而，与 C 语言不同的是，C++ 中的类型擦除通常试图避免移除类型信息，而倾向于在保留类型安全性的同时处理类型的严格定义。实现这一点的一种方法是通过使用 C++ 模板。为了更好地解释这一点，让我们从一个简单的 C++ 模板示例开始:\n\n```cpp\ntemplate<typename T>\nT pow2(T t)\n{\n    return t * t;\n}\n```\n\n在前面的例子中，我们创建了一个简单的函数，可以计算任何给定输入的 2 的幂。例如，我们可以这样调用这个函数:\n\n```cpp\nstd::cout << pow2(42U) << '\\n'\nstd::cout << pow2(-1) << '\\n'\n```\n\n当编译器看到使用`pow2()`函数时，它会自动为您生成以下代码(幕后):\n\n```cpp\nunsigned pow2(unsigned t)\n{\n    return t * t;\n}\n\nint pow2(int t)\n{\n    return t * t;\n}\n```\n\n如前面的代码片段所示，编译器创建了两个版本的`pow2()`函数:一个版本接受无符号值并返回无符号值，另一个版本接受整数并返回整数。编译器创建这两个版本是因为第一次使用`pow2()`函数时，我们为其提供了无符号值，而第二次使用`pow2()`函数时，我们为其提供了`int`。\n\n然而，就我们的代码而言，我们实际上并不关心函数提供的是什么类型，只要提供的类型能够成功执行`operator*()`。换句话说，`pow2()`函数的用户和`pow2()`函数的作者都在安全地忽略(或擦除)从概念角度传递给函数和从函数返回的类型信息。然而，编译器非常了解所提供的类型，并且必须根据需要安全地处理每种类型。\n\n这种类型擦除的形式按照 API 的规范执行擦除，在 C++ 中，这种规范被称为概念。与大多数规定输入和输出类型的应用编程接口不同(例如，`sleep()`函数接受一个无符号整数，并且只接受一个无符号整数)，一个概念特别忽略了类型，而是定义给定类型必须提供什么属性。\n\n例如，前面的`pow2()`功能有以下要求:\n\n*   提供的类型必须是整数类型或提供`operator *()`。\n*   提供的类型必须是可复制构造的或可移动构造的。\n\n如前面的代码片段所示，`pow2()`函数并不关心给它什么类型，只要提供的类型满足某些最低要求。让我们研究一个更复杂的例子，演示如何将 C++ 模板用作类型擦除的一种形式。假设我们有两个不同的英雄在和坏人战斗，每个英雄都提供了攻击坏人的能力，如下代码所示:\n\n```cpp\nclass spiderman\n{\npublic:\n    bool attack(int x, int) const\n    {\n        return x == 0 ? true : false;\n    }\n};\n\nclass captain_america\n{\npublic:\n    bool attack(int, int y) const\n    {\n        return y == 0 ? true : false;\n    }\n};\n```\n\n如前面的代码片段所示，每个英雄都提供了攻击坏人的能力，但是除了碰巧两个英雄都提供了具有相同函数签名的`attack()`函数之外，两个英雄都没有任何共同点。我们也没有能力给每个英雄增加继承(可能我们的设计无法处理继承增加的额外`vTable`开销，也可能英雄定义是提供给我们的)。\n\n现在假设我们有一个复杂的函数，必须为每个英雄调用`attack()`函数。我们可以为每个英雄编写相同的逻辑(也就是手动复制逻辑)，或者我们可以编写一个 C++ 模板函数来为我们处理这个问题，如下所示:\n\n```cpp\ntemplate<typename T>\nauto attack(const T &t, int x, int y)\n{\n    if (t.attack(x, y)) {\n        std::cout << \"hero won fight\\n\";\n    }\n    else {\n        std::cout << \"hero lost the fight :(\\n\";\n    }\n}\n```\n\n如前面的代码片段所示，我们可以利用 C++ 模板的类型擦除属性将攻击逻辑封装到单个模板函数中。前面的代码不关心它提供的是什么类型，只要该类型提供了一个`attack()`函数，该函数接受两个整数类型并返回一个整数类型(最好是`bool`，但任何整数都可以)。换句话说，只要所提供的类型符合一个约定的概念，这个模板函数就可以工作，为编译器提供了一种为我们处理特定类型逻辑的方法。\n\n我们可以如下调用前面的函数:\n\n```cpp\nint main(void)\n{\n    attack(spiderman{}, 0, 42);\n    attack(captain_america{}, 0, 42);\n\n    return 0;\n}\n```\n\n这将产生以下输出:\n\n![](img/70426f57-68a7-48bf-ac42-6ee95388297b.png)\n\n虽然这个例子展示了 C++ 模板如何被用作类型擦除的一种形式(至少对于创建概念的规范来说)，但是当讨论类型擦除时，有一种特定的模式被称为类型擦除模式或者仅仅是类型擦除。在下一个食谱中，我们将探索如何利用我们在前两个食谱中所学的知识来擦除类型信息，同时仍然支持容器等简单的东西。\n\n# 还有更多...\n\n在本食谱中，我们学习了如何使用概念来忽略(或擦除)特定于类型的知识，从而要求一个类型实现最少的一组特征。这些特性可以使用 SFINAE 来实现，这是我们在[第 4 章](04.html)、*使用通用编程模板*中详细讨论的主题。\n\n# 请参见\n\n在[第 13 章](13.html)、*奖励–使用 C++ 20 特性*中，我们还将讨论如何使用添加到 C++ 20 中的新特性来执行一个概念。\n\n# 学习 C++ 类型的橡皮擦模式\n\n在这个食谱中，我们将了解 C++ 中的类型擦除模式是什么，以及我们如何利用它来一般性地擦除类型信息，而不牺牲类型安全性或要求我们的类型继承纯虚拟基类。这个方法很重要，因为类型擦除模式在 C++ 标准库中被大量使用，它提供了一种简单的方法来封装没有任何共同点的数据类型，除了提供一组相似的 API，同时仍然支持容器之类的东西。\n\n# 准备好\n\n开始之前，请确保满足所有技术要求，包括安装 Ubuntu 18.04 或更高版本，并在终端窗口中运行以下内容:\n\n```cpp\n> sudo apt-get install build-essential git cmake\n```\n\n这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后，打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。\n\n# 怎么做...\n\n让我们按照以下步骤来尝试这个食谱:\n\n1.  从新的终端，运行以下命令下载源代码:\n\n```cpp\n> cd ~/\n> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git\n> cd Advanced-CPP-CookBook/chapter09\n```\n\n2.  要编译源代码，请运行以下命令:\n\n```cpp\n> cmake .\n> make recipe03_examples\n```\n\n3.  编译源代码后，您可以通过运行以下命令来执行该配方中的每个示例:\n\n```cpp\n> ./recipe03_example01 \n1\n0\n```\n\n在下一节中，我们将逐一介绍这些示例，并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。\n\n# 它是如何工作的...\n\n当我们通常想到 C++ 类型擦除时，这就是我们想到的例子。当我们必须利用一组对象时，类型擦除模式是需要的，就像它们是相关的一样，它们可能共享也可能不共享一个公共基类(也就是说，它们要么不使用继承，要么如果它们使用继承，它们可能不从同一组类继承)。\n\n例如，假设我们有以下类:\n\n```cpp\nclass spiderman\n{\npublic:\n    bool attack(int x, int) const\n    {\n        return x == 0 ? true : false;\n    }\n};\n\nclass captain_america\n{\npublic:\n    bool attack(int, int y) const\n    {\n        return y == 0 ? true : false;\n    }\n};\n```\n\n如前面的代码片段所示，每个类定义了不同类型的英雄。我们想做这样的事情:\n\n```cpp\nfor (const auto &h : heros) {\n    // something\n}\n```\n\n问题是，每个类都不是从相似的基类继承而来的，所以我们不能只创建每个类的一个实例并将它们添加到`std::array`中，因为编译器会抱怨类不一样。我们可以在`std::array`中存储每个类的原始`void *`指针，但是当需要使用`void *`时，我们必须`dynamic_cast()`回到每个类型来做任何有用的事情，如下所示:\n\n```cpp\n    std::array<void *, 2> heros {\n        new spiderman,\n        new captain_america\n    };\n\n    for (const auto &h : heros) {\n        if (ptr = dynamic_cast<spiderman>(ptr)) {\n            // something\n        }\n\n        if (ptr = dynamic_cast<captain_america>(ptr)) {\n            // something\n        }\n    }\n```\n\n`void *`的使用是类型擦除的一种形式，但这远远不理想，因为`dynamic_cast()`的使用很慢，我们添加的每一种新类型只会增加`if`语句的数量，并且这种实现与 C++ 核心指南相去甚远。\n\n然而，还有另一种方法可以解决这个问题。假设我们希望运行`attack()`函数，这个函数在每个英雄类之间恰好是相同的(也就是说，每个英雄类至少遵循一个共享的概念)。如果每个类都使用了下面的基类，我们可以只使用继承，如下所示:\n\n```cpp\nclass base\n{\npublic:\n    virtual ~base() = default;\n    virtual bool attack(int, int) const = 0;\n};\n```\n\n问题是，我们的英雄类不是从这个基类继承的。因此，让我们创建一个包装类，如下所示:\n\n```cpp\ntemplate<typename T>\nclass wrapper :\n    public base\n{\n    T m_t;\n\npublic:\n    bool attack(int x, int y) const override\n    {\n        return m_t.attack(x, y);\n    }\n};\n```\n\n如前面的代码片段所示，我们创建了一个继承自基类的模板包装类。这个包装器将一个实例存储到给定的任何类型，然后覆盖在纯虚拟基类中定义的`attack()`函数，该函数将对它的调用转发到包装器正在存储的实例。\n\n现在，我们可以按如下方式创建阵列:\n\n```cpp\n    std::array<std::unique_ptr<base>, 2> heros {\n        std::make_unique<wrapper<spiderman>>(),\n        std::make_unique<wrapper<captain_america>>()\n    };\n```\n\n`std::array`将`std::unique_ptr`存储到我们的基类中，然后我们用我们需要的每种类型创建我们的包装类(它继承了基类)，以存储在数组中。编译器为我们需要存储在数组中的每种类型创建一个包装器版本，由于包装器继承了基类，无论我们给包装器什么类型，数组总是可以根据需要存储结果包装器。\n\n现在，从这个数组中，我们可以执行以下操作:\n\n```cpp\n    for (const auto &h : heros) {\n        std::cout << h->attack(0, 42) << '\\n';\n    }\n```\n\n这就是:C++ 中的类型擦除。这种模式利用 C++ 模板赋予对象相同的继承属性，即使对象本身不直接使用继承。\n\n# 用类型擦除实现委托\n\n在这个食谱中，我们将学习如何实现委托模式，这是一个已经存在多年的模式(并且被其他一些语言大量使用，比如 ObjC)。这个方法很重要，因为它将教会你什么是委托，以及如何在你自己的应用中利用这个模式来提供更好的可扩展性，而不需要你的 API 使用继承。\n\n# 准备好\n\n开始之前，请确保满足所有技术要求，包括安装 Ubuntu 18.04 或更高版本，并在终端窗口中运行以下内容:\n\n```cpp\n> sudo apt-get install build-essential git cmake\n```\n\n这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后，打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。\n\n# 怎么做...\n\n让我们按照以下步骤来尝试这个食谱:\n\n1.  从新的终端，运行以下命令下载源代码:\n\n```cpp\n> cd ~/\n> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git\n> cd Advanced-CPP-CookBook/chapter09\n```\n\n2.  要编译源代码，请运行以下命令:\n\n```cpp\n> cmake .\n> make recipe04_examples\n```\n\n3.  编译源代码后，您可以通过运行以下命令来执行该配方中的每个示例:\n\n```cpp\n> ./recipe04_example01\n1\n0\n\n> ./recipe04_example02\n1\n0\n\n> ./recipe04_example03\n1\n0\n\n> ./recipe04_example04\n0\n1\n0\n```\n\n在下一节中，我们将逐一介绍这些示例，并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。\n\n# 它是如何工作的...\n\n如果你读过一本关于 C++ 的书，你可能已经看过苹果和橘子的例子，它演示了面向对象编程是如何工作的。想法如下:\n\n*   苹果是一种水果。\n*   橘子是一种水果。\n*   苹果不是橘子，但两者都是水果。\n\n这个例子旨在教你如何使用继承将代码组织成逻辑对象。苹果和橙子共享的逻辑被写入名为`fruit`的对象，而苹果或橙子特有的逻辑被写入从基础`fruit`对象继承的`apple`或`orange`对象。\n\n然而，这个例子也展示了如何扩展水果的功能。通过子类化一个水果，我可以创建一个苹果，它能做的比`fruit`基类更多。这种扩展类功能的想法在 C++ 中很常见，我们经常想到用继承来实现它。在这个食谱中，我们将探索如何做到这一点，而不需要苹果或橙子通过一种叫做委托的东西来利用继承。\n\n假设你正在创建一个游戏，你希望实现一个英雄和坏人战斗的战场。在你的代码中的某个时候，战斗中的每个英雄都需要攻击坏人。问题是英雄在战斗中来来去去，因为他们需要时间来恢复，所以你真的需要维护一个能够攻击坏人的英雄列表，你只需要遍历这个动态变化的英雄列表，看看他们的攻击是否成功。\n\n每个英雄可以存储一个英雄列表，这些英雄组成一个公共基类的子类，然后运行一个每个英雄覆盖的`attack()`函数，但是这需要使用继承，这可能是不希望的。我们也可以使用类型擦除模式包装每个英雄，然后存储指向我们包装器基类的指针，但是这将是我们的`attack()`函数所特有的，我们相信还会有其他需要这些类型扩展的情况。\n\n输入委托模式，它是类型擦除模式的扩展。使用委托模式，我们可以编写如下代码:\n\n```cpp\nint main(void)\n{\n    spiderman s;\n    captain_america c;\n\n    std::array<delegate<bool(int, int)>, 3> heros {\n        delegate(attack),\n        delegate(&s, &spiderman::attack),\n        delegate(&c, &captain_america::attack)\n    };\n\n    for (auto &h : heros) {\n        std::cout << h(0, 42) << '\\n';\n    }\n\n    return 0;\n}\n```\n\n如前面的代码片段所示，我们已经定义了两个不同类的实例，它们并不相同，然后我们创建了一个存储三个委托的数组。委托的模板参数采用`bool(int, int)`的函数签名，而委托本身似乎是由一个函数指针以及我们之前创建的类实例中的两个成员函数指针创建的。然后，我们能够遍历每个委托并调用它们，有效地独立调用函数指针和每个成员函数指针。\n\n委托模式提供了将不同的可调用对象封装成一个具有公共类型的单个对象的能力，只要这些对象共享相同的函数签名，就能够调用这些可调用对象。更重要的是，委托可以封装函数指针和成员函数指针，为应用编程接口的用户提供在需要时存储私有状态的能力。\n\n为了解释这是如何工作的，我们将从简单开始，然后以我们的例子为基础，直到最终实现。让我们从一个基类开始，如下所示:\n\n```cpp\ntemplate<\n    typename RET,\n    typename... ARGS\n    >\nclass base\n{\npublic:\n    virtual ~base() = default;\n    virtual RET func(ARGS... args) = 0;\n};\n```\n\n如前面的代码片段所示，我们已经创建了一个纯虚拟基类的模板。模板参数是`RET`(定义返回值)和`ARGS...`(定义变量列表的参数)。然后我们创建了一个名为`func()`的函数，它接受我们的参数列表并返回模板返回类型。\n\n接下来，让我们使用类型擦除模式定义一个从基类继承的包装器(如果您还没有阅读前面的方法，请现在阅读):\n\n```cpp\ntemplate<\n    typename T,\n    typename RET,\n    typename... ARGS\n    >\nclass wrapper :\n    public base<RET, ARGS...>\n{\n    T m_t{};\n    RET (T::*m_func)(ARGS...);\n\npublic:\n\n    wrapper(RET (T::*func)(ARGS...)) :\n        m_func{func}\n    { }\n\n    RET func(ARGS... args) override\n    {\n        return std::invoke(m_func, &m_t, args...);\n    }\n};\n```\n\n就像类型橡皮擦模式一样，我们有一个包装器类，它存储我们类型的一个实例，然后提供一个包装器可以调用的函数。不同之处在于，可以调用的函数不是静态定义的，而是由提供的模板参数定义的。此外，我们还存储了一个具有相同函数签名的函数指针，它由包装器的构造函数初始化，并使用`std::invoke`在`func()`函数中调用。\n\n与典型的类型擦除示例相比，这个额外的逻辑提供了定义我们希望从存储在包装器中的对象调用的任何函数签名的能力，而不是提前定义它(这意味着我们希望调用的函数可以在运行时而不是编译时确定)。\n\n然后，我们可以如下创建委托类:\n\n```cpp\ntemplate<\n    typename RET,\n    typename... ARGS\n    >\nclass delegate\n{\n    std::unique_ptr<base<RET, ARGS...>> m_wrapper;\n\npublic:\n\n    template<typename T>\n    delegate(RET (T::*func)(ARGS...)) :\n        m_wrapper{\n            std::make_unique<wrapper<T, RET, ARGS...>>(func)\n        }\n    { }\n\n    RET operator()(ARGS... args)\n    {\n        return m_wrapper->func(args...);\n    }\n};\n```\n\n与类型擦除模式一样，我们存储一个指向包装器的指针，它是从委托的构造函数创建的。这里需要认识的重要细节是`T`类型没有在委托本身中定义。相反，`T`类型仅在用于创建包装器实例化的委托的构造过程中是已知的。这意味着委托的每个实例都是相同的，即使委托存储了包装不同类型的包装。这允许我们如下使用委托。\n\n假设我们有两个不共享一个公共基础的英雄，但是提供了一个具有相同签名的`attack()`函数:\n\n```cpp\nclass spiderman\n{\npublic:\n    bool attack(int x, int)\n    {\n        return x == 0 ? true : false;\n    }\n};\n\nclass captain_america\n{\npublic:\n    bool attack(int, int y)\n    {\n        return y == 0 ? true : false;\n    }\n};\n```\n\n我们可以利用我们的委托类来存储英雄类的实例，并按照如下方式调用它们的攻击函数:\n\n```cpp\nint main(void)\n{\n    std::array<delegate<bool, int, int>, 2> heros {\n        delegate(&spiderman::attack),\n        delegate(&captain_america::attack)\n    };\n\n    for (auto &h : heros) {\n        std::cout << h(0, 42) << '\\n';\n    }\n\n    return 0;\n}\n```\n\n这将产生以下输出:\n\n![](img/36666375-3829-4923-ab93-fc4ef67966c3.png)\n\n虽然我们已经在创建委托方面取得了显著的进展(至少它是有效的)，但是这种早期实现存在一些问题:\n\n*   委托的签名是`bool, int, int`，这是误导性的，因为我们真的想要一个像`bool(int, int)`这样的函数签名，这样代码是自文档化的(委托的类型是单个函数签名，而不是三个不同的类型)。\n*   该委托不能处理标记为`const`的功能。\n*   我们必须在包装器中存储委托对象的一个实例，这防止我们为同一个对象创建多个函数的委托。\n*   我们不支持非成员函数。\n\n让我们逐一解决这些问题。\n\n# 向我们的委托添加函数签名\n\n虽然向我们的委托添加函数签名作为模板参数可以在不需要 C++ 17 的情况下完成，但是 C++ 17 中的用户定义类型推导使这个过程变得简单。下面的代码片段显示了这一点:\n\n```cpp\ntemplate<\n    typename T,\n    typename RET,\n    typename... ARGS\n    >\ndelegate(RET(T::*)(ARGS...)) -> delegate<RET(ARGS...)>;\n```\n\n如前面的代码片段所示，用户定义的类型推断告诉编译器如何获取我们的委托构造函数，并将其转换为我们希望使用的模板签名。如果没有这个用户定义的类型推导指南，`delegate(RET(T::*)(ARGS...))`构造函数将导致委托被推导为`delegate<RET, ARGS...>`，这不是我们想要的。相反，我们希望编译器推导出`delegate<RET(ARGS...)>`。我们的委托实现没有其他需要改变的地方。我们只需要告诉编译器如何执行类型推导。\n\n# 给我们的代表增加持续的支持\n\n我们的委托当前不能接受标记为`const`的成员函数，因为我们没有为委托提供能够这样做的包装。例如，我们的英雄的`attack()`功能目前是这样的:\n\n```cpp\nclass spiderman\n{\npublic:\n    bool attack(int x, int)\n    {\n        return x == 0 ? true : false;\n    }\n};\n```\n\n然而，我们希望我们的英雄`attack()`函数如下所示，因为它们不修改任何私有成员变量:\n\n```cpp\nclass spiderman\n{\npublic:\n    bool attack(int x, int) const\n    {\n        return x == 0 ? true : false;\n    }\n};\n```\n\n为了支持这一更改，我们必须创建一个支持这一更改的包装器，如下所示:\n\n```cpp\ntemplate<\n    typename T,\n    typename RET,\n    typename... ARGS\n    >\nclass wrapper_const :\n    public base<RET, ARGS...>\n{\n    T m_t{};\n    RET (T::*m_func)(ARGS...) const;\n\npublic:\n\n    wrapper_const(RET (T::*func)(ARGS...) const) :\n        m_func{func}\n    { }\n\n    RET func(ARGS... args) override\n    {\n        return std::invoke(m_func, &m_t, args...);\n    }\n};\n```\n\n如前所示，这个包装器与我们之前的包装器相同，区别在于我们存储的函数签名有一个添加的`const`实例。为了让委托使用这个附加的包装，我们还必须提供一个附加的委托构造函数，如下所示:\n\n```cpp\n    template<typename T>\n    delegate(RET (T::*func)(ARGS...) const) :\n        m_wrapper{\n            std::make_unique<wrapper_const<T, RET, ARGS...>>(func)\n        }\n    { }\n```\n\n这意味着我们还需要一个额外的用户定义类型推导指南，如下所示:\n\n```cpp\ntemplate<\n    typename T,\n    typename RET,\n    typename... ARGS\n    >\ndelegate(RET(T::*)(ARGS...) const) -> delegate<RET(ARGS...)>;\n```\n\n通过这些修改，我们现在可以支持标记有`const`的成员函数。\n\n# 为我们的代表增加一对多支持\n\n目前，我们的包装器为每种类型存储一个实例。这种方法通常用于类型擦除，但是在我们的例子中，它阻止了为同一个对象创建多个委托的能力(也就是说，不支持一对多)。为了解决这个问题，我们将在包装器中存储一个指向对象的指针，而不是对象本身，如下所示:\n\n```cpp\ntemplate<\n    typename T,\n    typename RET,\n    typename... ARGS\n    >\nclass wrapper :\n    public base<RET, ARGS...>\n{\n    const T *m_t{};\n    RET (T::*m_func)(ARGS...);\n\npublic:\n\n    wrapper(const T *t, RET (T::*func)(ARGS...)) :\n        m_t{t},\n        m_func{func}\n    { }\n\n    RET func(ARGS... args) override\n    {\n        return std::invoke(m_func, m_t, args...);\n    }\n};\n```\n\n如前所示，我们所做的唯一更改是存储一个指向我们正在包装的对象的指针，而不是对象本身，这也意味着我们需要在构造函数中初始化这个指针。要使用这个新的包装，我们必须修改我们的委托构造函数，如下所示:\n\n```cpp\n    template<typename T>\n    delegate(const T *t, RET (T::*func)(ARGS...)) :\n        m_wrapper{\n            std::make_unique<wrapper<T, RET, ARGS...>>(t, func)\n        }\n    { }\n```\n\n这反过来意味着我们必须更新用户定义的类型扣减指南，如下所示:\n\n```cpp\ntemplate<\n    typename T,\n    typename RET,\n    typename... ARGS\n    >\ndelegate(const T *, RET(T::*)(ARGS...)) -> delegate<RET(ARGS...)>;\n```\n\n通过这些修改，我们现在可以如下创建代理:\n\n```cpp\nint main(void)\n{\n    spiderman s;\n    captain_america c;\n\n    std::array<delegate<bool(int, int)>, 2> heros {\n        delegate(&s, &spiderman::attack),\n        delegate(&c, &captain_america::attack)\n    };\n\n    for (auto &h : heros) {\n        std::cout << h(0, 42) << '\\n';\n    }\n\n    return 0;\n}\n```\n\n如前所述，委托获取指向每个对象的指针，这意味着我们可以创建任意多的委托，包括在需要时创建指向其他成员函数指针的委托。\n\n# 向我们的委托添加对非成员函数的支持\n\n最后，我们需要修改委托来增加对非成员函数的支持。看看这个例子:\n\n```cpp\nbool attack(int x, int y)\n{\n    return x == 42 && y == 42 ? true : false;\n}\n```\n\n为此，我们只需添加另一个包装器，如下所示:\n\n```cpp\ntemplate<\n    typename RET,\n    typename... ARGS\n    >\nclass fun_wrapper :\n    public base<RET, ARGS...>\n{\n    RET (*m_func)(ARGS...);\n\npublic:\n\n    fun_wrapper(RET (*func)(ARGS...)) :\n        m_func{func}\n    { }\n\n    RET func(ARGS... args) override\n    {\n        return m_func(args...);\n    }\n};\n```\n\n如前所示，与我们的原始包装器一样，我们存储了指向我们希望调用的函数的指针，但是在这种情况下，我们不需要存储指向对象的指针，因为没有对象(因为这是非成员函数包装器)。要使用这个新包装，我们必须添加另一个委托构造函数，如下所示:\n\n```cpp\n    delegate(RET (func)(ARGS...)) :\n        m_wrapper{\n            std::make_unique<fun_wrapper<RET, ARGS...>>(func)\n        }\n    { }\n```\n\n这意味着我们还必须提供另一个用户定义的类型推导指南，如下所示:\n\n```cpp\ntemplate<\n    typename RET,\n    typename... ARGS\n    >\ndelegate(RET(*)(ARGS...)) -> delegate<RET(ARGS...)>;\n```\n\n经过所有的修改，我们终于能够使用本食谱开头定义的委托:\n\n```cpp\nint main(void)\n{\n    spiderman s;\n    captain_america c;\n\n    std::array<delegate<bool(int, int)>, 3> heros {\n        delegate(attack),\n        delegate(&s, &spiderman::attack),\n        delegate(&c, &captain_america::attack)\n    };\n\n    for (auto &h : heros) {\n        std::cout << h(0, 42) << '\\n';\n    }\n\n    return 0;\n}\n```\n\n执行此操作时，我们会得到以下输出:\n\n![](img/59994462-e91d-48fa-bcaf-1538fa6e4e37.png)\n\n该委托可以通过添加另一组包装器来进一步扩展以支持 lambda 函数，并且委托中的`std::unique_pointer`的需求可以被移除以支持放置新的，使用成员函数包装器大小的小缓冲区(或者，换句话说，移除动态内存分配)，这有时被称为小大小优化。"
  },
  {
    "path": "docs/adv-cpp-prog-cb/10.md",
    "content": "# 十、对动态分配的深入研究\n\n在本章中，您将学习如何使用动态内存分配。本章很重要，因为并非所有变量都可以全局定义或在堆栈上定义(即从函数内部定义)，因为应该尽可能避免使用全局内存，堆栈内存通常比堆内存(用于动态内存分配的内存)更有限。然而，堆内存的使用多年来导致了大量关于泄漏和悬空指针的错误。\n\n本章不仅将教您这种动态内存分配是如何工作的，还将教您如何以符合 C++ 核心指南的方式从堆中正确分配内存。\n\n从我们为什么使用智能指针以及它们之间的区别、转换和其他引用开始，我们将在这一章结束时简要解释一下在 Linux 下堆是如何工作的，以及为什么动态内存分配如此缓慢。\n\n在本章中，我们将介绍以下食谱:\n\n*   比较标准::共享 _ptr 和标准::唯一 _ptr\n*   从唯一 ptr 转换为共享 ptr\n*   使用循环引用\n*   使用智能指针进行类型转换\n*   显微镜下的堆\n\n# 技术要求\n\n要编译和运行本章中的示例，您必须拥有运行 Ubuntu 18.04 的计算机的管理权限，并且具有功能性互联网连接。在运行这些示例之前，必须使用以下命令安装 Valgrind:\n\n```cpp\n> sudo apt-get install build-essential git cmake valgrind \n```\n\n如果这安装在 Ubuntu 18.04 以外的任何操作系统上，则需要 GCC 7.4 或更高版本以及 CMake 3.6 或更高版本。\n\n本章的代码文件可以在[https://github . com/packt publishing/Advanced-CPP-cook book/tree/master/chapter 10](https://github.com/PacktPublishing/Advanced-CPP-CookBook/tree/master/chapter10)找到。\n\n# 比较标准::共享 _ptr 和标准::唯一 _ptr\n\n在本食谱中，我们将了解为什么 C++ 核心指南不鼓励使用手动调用 new 和 delete，以及为什么他们建议使用`std::unique_ptr`和`std::shared_ptr`。我们还将了解 a `std::unique_ptr`和 a `std::shared_ptr`之间的区别，以及为什么 a `std::shared_ptr`应该只在某些场景中使用(也就是为什么`std::unique_ptr`可能是您应该在大多数场景中使用的智能指针类型)。这个方法很重要，因为它将教你如何在现代 C++ 中正确分配动态(堆)内存。\n\n# 准备好\n\n开始之前，请确保满足所有技术要求，包括安装 Ubuntu 18.04 或更高版本，并在终端窗口中运行以下内容:\n\n```cpp\n> sudo apt-get install build-essential git cmake\n```\n\n完成后，打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。\n\n# 怎么做...\n\n按照以下步骤完成该配方:\n\n1.  从新的终端，运行以下命令下载源代码:\n\n```cpp\n> cd ~/\n> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git\n> cd Advanced-CPP-CookBook/chapter10\n```\n\n2.  要编译源代码，请运行以下命令:\n\n```cpp\n> cmake .\n> make recipe01_examples\n```\n\n3.  编译完源代码后，您可以通过运行以下命令来执行该配方中的每个示例:\n\n```cpp\n> ./recipe01_example01\n\n> ./recipe01_example02\nfree(): double free detected in tcache 2\nAborted (core dumped)\n\n> ./recipe01_example03\n\n> ./recipe01_example04\n\n> ./recipe01_example05\n\n> ./recipe01_example06\ncount: 42\n\n> ./recipe01_example07\ncount: 33320633\n\n> ./recipe01_example08\ncount: 42\n```\n\n在下一节中，我们将逐一介绍这些示例，并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。\n\n# 它是如何工作的...\n\n在 C++ 中，有三种不同的方法来声明变量:\n\n*   **全局变量**:这些是全局可访问的变量。在 Linux 上，这些通常存在于可执行文件的`.data`、`.rodata`或`.bss`部分。\n*   **栈变量**:这些变量是您在函数内部定义的，驻留在应用的栈内存中，由编译器管理。\n*   **堆变量**:这些是使用`malloc()` / `free()`或`new()` / `delete()`创建的变量，使用由动态内存管理算法管理的堆内存(例如，`dlmalloc`、`jemalloc`、`tcmalloc`等)。\n\n在本章中，我们将重点讨论后者，即堆风格的内存分配。您可能已经知道，在 C++ 中，内存是使用`new()`和`delete()`分配的，如下所示:\n\n```cpp\nint main(void)\n{\n    auto ptr = new int;\n    *ptr = 42;\n}\n```\n\n我们可以看到，分配了一个整数指针(即指向整数的指针)，然后设置为`42`。我们在 C++ 中使用`new()`而不是`malloc()`，原因如下:\n\n*   `malloc()`返回`void *`而不是我们关心的类型。这可能会导致分配不匹配的错误(也就是说，您打算分配一辆汽车，而不是分配一辆橙色汽车)。换句话说，`malloc()`不提供类型安全。\n*   `malloc()`需要一个尺寸参数。为了分配内存，我们需要知道为我们关心的类型分配多少字节。这可能会导致分配大小不匹配的错误(也就是说，您打算为一辆汽车分配足够的字节，但实际上您只为一辆橙色汽车分配了足够的字节)。\n*   `malloc()`出错时返回`NULL`，要求`NULL`对每次分配进行检查。\n\n`new()`操作员解决所有这些问题:\n\n*   `new()`返回`T*`。如上例所示，这甚至允许使用`auto`，防止冗余，因为 C++ 的类型系统有足够的信息来正确分配和跟踪所需的类型。\n*   `new()`不接受大小论证。相反，您告诉它您想要分配什么类型，它已经隐式地拥有了关于该类型的大小信息。再一次，通过简单地陈述你想要分配什么，你得到了你想要分配的，包括适当的指针和大小。\n*   `new()`如果分配失败，抛出异常。这防止了对`NULL`检查的需要。如果执行了下一行代码，就可以保证分配成功(假设没有禁用异常)。\n\n然而`new()`操作者还有一个问题；`new()`不跟踪所有权。像`malloc()`一样，`new()`操作符返回一个指针，这个指针可以在函数之间传递，而不知道谁真正拥有这个指针，这意味着它应该在不再需要的时候删除这个指针。\n\n这种所有权的概念是 C++ 核心指南(除了内存跨度之外)的一个关键组成部分，该指南试图解决 C++ 中导致不稳定、可靠性和安全性错误的常见错误。让我们看一个例子:\n\n```cpp\nint main(void)\n{\n    auto p = new int;\n    delete p;\n\n    delete p;\n}\n```\n\n在前面的例子中，我们分配了一个整数指针，然后删除该指针两次。在前面的例子中，我们从未在退出程序之前删除整数指针。现在，考虑以下代码块:\n\n```cpp\nint main(void)\n{\n    auto p = new int;\n    delete p;\n\n    *p = 42;\n}\n```\n\n在前面的例子中，我们分配一个整数指针，删除它，然后使用它。虽然这些例子看起来很简单，很容易避免，但是在大型复杂的项目中，这些类型的错误经常发生，以至于 C++ 社区开发了静态和动态分析工具来自动为我们识别这些类型的错误(尽管它们并不完美)，以及 C++ 核心指南本身，试图从一开始就防止这些类型的错误。\n\n在 C++ 11 中，标准委员会引入了`std::unique_ptr`来解决与`new()`和`delete()`的所有权问题。以下是它的工作原理:\n\n```cpp\n#include <memory>\n\nint main(void)\n{\n    auto ptr = std::make_unique<int>();\n    *ptr = 42;\n}\n```\n\n在前面的例子中，我们使用`std::make_unique()`函数分配了一个整数指针。这个函数创建一个`std::unique_ptr`，并给它一个使用`new()`分配的指针。这里，得到的指针(大部分)看起来和行为都像一个常规指针，除了当`std::unique_ptr`失去作用域时指针会被自动删除。也就是说，`std::unique_ptr`拥有使用`std::make_unique()`分配的指针，并对指针本身的生存期负责。在本例中，我们不需要手动运行`delete()`，因为`delete()`是在`main()`功能完成时为我们运行的(也就是当`std::unique_ptr`失去作用域时)。\n\n使用这个管理所有权的简单技巧，可以避免前面代码中显示的所有错误(大部分，我们将在后面讨论)。虽然下面的代码不符合 c++ Core guide(因为不建议使用下标运算符)，但是您也可以使用`std::unique_ptr`分配数组，如下所示:\n\n```cpp\n#include <memory>\n#include <iostream>\n\nint main(void)\n{\n    auto ptr = std::make_unique<int[]>(100);\n    ptr[0] = 42;\n}\n```\n\n如前面的代码所示，我们分配一个大小为`100`的 C 风格数组，然后设置数组中的第一个元素。一般来说，你唯一需要的指针类型是`std::unique_ptr`。然而，仍然会出现一些问题:\n\n*   未正确跟踪指针的生存期，例如，在函数中分配`std::unique_ptr`并返回结果指针。一旦函数返回，`std::unique_ptr`将失去作用域，从而删除刚刚返回的指针。`std::unique_ptr` *不*实行自动垃圾收集。您仍然需要了解指针的生存期以及它如何影响您的代码。\n*   永远不为`std::unique_ptr`提供失去作用域的机会，仍然有可能泄漏内存(尽管难度要大得多)；例如，将`std::unique_ptr`添加到全局列表中，或者在用`new()`手动分配的类中分配`std::unique_ptr`，然后泄漏。`std::unique_ptr` *又一次没有*实现自动垃圾回收，仍然需要你保证`std::unique_ptr`在需要的时候失去作用。\n*   `std::unique_ptr`也没有能力支持共享所有权。虽然这是一个问题，但这种情况很少发生。在大多数情况下，您只需要`std::unique_ptr`就可以确保正确的所有权。\n\n经常提出的一个问题是，*一旦分配了指针，我们如何安全地将这个指针传递给其他函数？*答案是，使用`get()`函数，将指针作为常规的 C 风格指针传递。`std::unique_ptr`定义的是所有权，而不是`NULL`的指针安全。`NULL`指针安全由带有`gsl::not_null`包装器和`expects()`宏的指南支持库提供。\n\n如何使用这些取决于您的指针哲学:\n\n*   一些人认为任何以指针为参数的函数都应该检查`NULL`指针。这种方法的优点是可以快速识别并安全处理`NULL`指针，缺点是您在代码中引入了额外的分支逻辑，这会降低性能和可读性。\n*   一些人认为应该检查以指针为参数的*公共*函数是否有`NULL`指针。这种方法的优点是提高了性能，因为并非所有函数都需要`NULL`指针检查。这种方法的缺点是公共接口仍然有额外的分支逻辑。\n*   一些人认为函数应该简单地记录它的期望(称为契约)。这种方法的好处是`assert()`和`expects()`宏可以用来在调试模式下检查`NULL`指针以强制执行该约定，而在发布模式下，没有性能损失。这种方法的缺点是，在释放模式下，所有赌注都被取消。\n\n您采取哪种方法将在很大程度上取决于您正在编写的应用的类型。如果你正在写下一个 Crush 游戏，你可能会更关心后一种方法，因为它表现最好。如果你正在编写一个自动驾驶飞机的应用，我们都希望你使用第一种方法。\n\n为了演示如何使用`std::unique_ptr`传递指针，让我们看下面的例子:\n\n```cpp\nstd::atomic<int> count;\n\nvoid inc(int *val)\n{\n    count += *val;\n}\n```\n\n假设您有一个作为线程执行的超关键函数，以整数指针作为参数，并将提供的整数添加到全局计数器中。这个线程的前一个实现是*下注*，祈祷最好的方法。该功能可以如下实现:\n\n```cpp\nvoid inc(int *val)\n{\n    if (val != nullptr) {\n        count += *val;\n    }\n    else {\n        std::terminate();\n    }\n}\n```\n\n如果提供的指针是`NULL`指针，前面的函数调用`std::terminate()`(不是一个非常容错的方法)。正如我们所看到的，这种方法很难理解，因为这里有很多额外的逻辑。我们可以这样实现:\n\n```cpp\nvoid inc(gsl::not_null<int *> val)\n{\n    count += *val;\n}\n```\n\n这与`NULL`指针检查做了同样的事情(取决于您如何定义`gsl::not_null`工作，因为这也可能引发异常)。您也可以如下实现:\n\n```cpp\nvoid inc(int *val)\n{\n    expects(val);\n    count += *val;\n}\n```\n\n前面的例子总是检查`NULL`指针，而前面的方法使用契约方法，允许在发布模式下取消检查。您也可以使用`assert()`(如果您没有使用 GSL...这开玩笑地说，当然，不应该是这种情况)。\n\n还应该注意的是，C++ 标准委员会正致力于通过使用 C++ 契约来添加`expects()`逻辑作为语言的核心组件，这一特性不幸地从 C++ 20 中删除了，但有望在标准的未来版本中添加，因为我们可能能够如下编写前面的函数(并告诉编译器我们希望使用哪种方法，而不是必须手动编写它):\n\n```cpp\nvoid inc(int *val) [[expects: val]]\n{\n    count += *val;\n}\n```\n\n我们可以如下使用这个函数:\n\n```cpp\nint main(void)\n{\n    auto ptr = std::make_unique<int>(1);\n    std::array<std::thread, 42> threads;\n\n    for (auto &thread : threads) {\n        thread = std::thread{inc, ptr.get()};\n    }\n\n    for (auto &thread : threads) {\n        thread.join();\n    }\n\n    std::cout << \"count: \" << count << '\\n';\n\n    return 0;\n}\n```\n\n从前面的代码示例中，我们可以观察到以下内容:\n\n*   我们使用`std::make_unique()`从堆中分配一个整数指针，返回`std::unique_ptr()`。\n*   我们创建一个线程数组并执行每个线程，将新分配的指针传递给每个线程。\n*   最后，我们等待所有线程完成并输出结果计数。由于`std::unique_ptr`的作用域是`main()`函数，我们必须确保线程在从`main()`函数返回之前完成。\n\n前面的示例产生以下输出:\n\n![](img/174f4cca-9a24-4400-8cc9-193f3b2d646b.png)\n\n正如我们前面提到的，前面的例子将`std::unique_ptr`定义为`main()`函数的范围，这意味着我们必须确保线程在`main()`函数返回之前完成。这种情况并不总是如此。让我们看看下面的例子:\n\n```cpp\nstd::atomic<int> count;\n\nvoid inc(int *val)\n{\n    count += *val;\n}\n```\n\n在这里，我们创建了一个函数，当给定一个整数指针时，该函数增加一个计数:\n\n```cpp\nint main(void)\n{\n    std::array<std::thread, 42> threads;\n\n    {\n        auto ptr = std::make_unique<int>(1);\n\n        for (auto &thread : threads) {\n            thread = std::thread{inc, ptr.get()};\n        }\n    }\n\n    for (auto &thread : threads) {\n        thread.join();\n    }\n\n    std::cout << \"count: \" << count << '\\n';\n\n    return 0;\n}\n```\n\n如前面的代码所示，`main()`函数也与我们前面的例子相同，只是`std::unique_ptr`是在自己的作用域中创建的，它是在线程需要完成之前释放的。这将产生以下输出:\n\n![](img/cf6bd321-786d-4be2-9694-3287c2c6229f.png)\n\n如前面的截图所示，当线程试图从已被删除的内存中读取时，结果输出是垃圾(也就是说，线程被赋予了一个悬空指针)。\n\n虽然这是一个简单的例子，但这种类型的场景可能发生在更复杂的场景中，问题的根源是共享所有权。在这个例子中，每个线程都拥有指针。换句话说，没有一个线程试图获得指针的唯一所有权(包括分配和执行其他线程的主线程)。虽然这种类型的问题通常发生在没有主线程设计的多线程应用中，但这也可能发生在异步逻辑中，在异步逻辑中，指针被分配，然后被传递给生命周期和执行点未知的多个异步作业。\n\n为了处理这些特定类型的问题，C++ 提供了`std::shared_ptr`。这是托管对象的包装。每次复制`std::shared_ptr`时，被管理对象都会增加一个内部计数器，用于跟踪指针(被管理对象存储的)有多少个所有者。每当`std::shared_ptr`失去作用域时，被管理对象减少内部计数器，并且一旦该计数达到`0`就删除指针。使用这种方法，`std::shared_ptr`能够支持一对多所有权模型，该模型可以处理我们之前定义的场景。\n\n让我们看看下面的例子:\n\n```cpp\nstd::atomic<int> count;\n\nvoid inc(std::shared_ptr<int> val)\n{\n    count += *val;\n}\n```\n\n如前面的代码所示，我们有相同的递增计数器的线程函数，但不同的是它采用`std::shared_ptr`而不是常规的整数指针。现在，我们可以如下实现前面的示例:\n\n```cpp\nint main(void)\n{\n    std::array<std::thread, 42> threads;\n\n    {\n        auto ptr = std::make_shared<int>(1);\n\n        for (auto &thread : threads) {\n            thread = std::thread{inc, ptr};\n        }\n    }\n\n    for (auto &thread : threads) {\n        thread.join();\n    }\n\n    std::cout << \"count: \" << count << '\\n';\n\n    return 0;\n}\n```\n\n如前面的代码所示，指针是在其自己的作用域中创建的，该作用域在线程需要完成之前被移除。但是，与前面的示例不同，这段代码会产生以下结果:\n\n![](img/510ee43e-1a06-46ff-8411-882af9ebb984.png)\n\n前面的代码正确执行的原因是指针的所有权在所有线程之间共享，指针本身在所有线程完成之前不会被删除(即使作用域丢失)。\n\n最后一个注意事项:当应该使用`std::unique_ptr`时，对所有指针类型使用`std::shared_ptr`可能很有诱惑力，因为它有很好的类型转换 API，并且在理论上确保函数有有效的指针。现实情况是，不管使用`std::shared_ptr`还是`std::unique_ptr`，一个函数都必须按照应用的需求执行其`NULL`检查，因为`std::shared_ptr`仍然可以被创建为`NULL`指针。\n\n`std::shared_ptr`也增加了开销，因为它必须在内部存储所需的删除程序。它还需要为托管对象分配额外的堆。`std::shared_ptr`和`std::unique_ptr`都定义了指针所有权。它们不提供自动垃圾收集(也就是说，它们不自动处理指针生存期)，也不保证某个指针不是`NULL`。`std::shared_ptr`应该只在多个事物必须拥有指针的生存期时使用，以确保应用的正确执行；否则，使用`std::unique_ptr`。\n\n# 从标准::唯一 _ptr 转换为标准::共享 _ptr\n\n在这个食谱中，我们将学习如何从`std::unique_ptr`转换成`std::shared_ptr`。这个配方很重要，因为当应用编程接口本身确实需要`std::shared_ptr`用于内部使用时，将应用编程接口定义为接受`std::unique_ptr`通常很方便。一个很好的例子是在创建图形用户界面应用编程接口时。您可能会将一个小部件传递给应用编程接口来存储和拥有，而不知道以后图形用户界面的实现是否需要添加线程，在这种情况下`std::shared_pointer`可能是一个更好的选择。该配方将为您提供将`std::unique_ptr`转换为`std::shared_ptr`的技能，如果需要的话，无需修改 API 本身。\n\n# 准备好\n\n开始之前，请确保满足所有技术要求，包括安装 Ubuntu 18.04 或更高版本，并在终端窗口中运行以下内容:\n\n```cpp\n> sudo apt-get install build-essential git cmake\n```\n\n完成后，打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。\n\n# 怎么做...\n\n按照以下步骤完成该配方:\n\n1.  从新的终端，运行以下命令下载源代码:\n\n```cpp\n> cd ~/\n> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git\n> cd Advanced-CPP-CookBook/chapter10\n```\n\n2.  要编译源代码，请运行以下命令:\n\n```cpp\n> cmake .\n> make recipe02_examples\n```\n\n3.  编译完源代码后，您可以通过运行以下命令来执行该配方中的每个示例:\n\n```cpp\n> ./recipe02_example01 \ncount: 42\n```\n\n在下一节中，我们将逐一介绍这些示例，并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。\n\n# 它是如何工作的...\n\n`std::shared_ptr`用于在多个事物必须拥有指针才能使应用正常执行时管理指针。但是，假设您提供了一个必须接受整数指针的 API，如下所示:\n\n```cpp\nvoid execute_threads(int *ptr);\n```\n\n前面的 API 建议调用这个函数的人拥有整数指针。也就是说，无论谁调用这个函数，都需要分配整数指针，并在函数完成后删除它。但是，如果我们打算让前面的应用编程接口拥有指针，那么我们真的应该这样编写这个应用编程接口:\n\n```cpp\nvoid execute_threads(std::unique_ptr<int> ptr);\n```\n\n这个 API 说，*请给我分配一个整数指针，但是它一旦通过，我就拥有它，并且会保证在需要的时候删除它。*现在，假设这个函数将在一对多所有权场景中使用这个指针。你是做什么的？您可以按如下方式编写您的应用编程接口:\n\n```cpp\nvoid execute_threads(std::shared_ptr<int> ptr);\n```\n\n然而，这将阻止您的应用编程接口在未来优化一对多关系(也就是说，如果您能够在未来删除这种关系，您仍然会被`std::shared_ptr`卡住，即使它是次优的，而不必修改应用编程接口的函数签名)。\n\n为了解决这个问题，c++ API 提供了将`std::unique_ptr`转换为`std::shared_ptr`的能力，如下所示:\n\n```cpp\nstd::atomic<int> count;\n\nvoid\ninc(std::shared_ptr<int> val)\n{\n    count += *val;\n}\n```\n\n假设我们有一个内部函数，就目前而言，将一个整数指针作为`std::shared_ptr`，使用它的值来递增`count`，并将其作为一个线程来执行。然后，我们为它提供一个公共 API 来使用这个内部函数，如下所示:\n\n```cpp\nvoid\nexecute_threads(std::unique_ptr<int> ptr)\n{\n    std::array<std::thread, 42> threads;\n    auto shared = std::shared_ptr<int>(std::move(ptr));\n\n    for (auto &thread : threads) {\n        thread = std::thread{inc, shared};\n    }\n\n    for (auto &thread : threads) {\n        thread.join();\n    }\n}\n```\n\n如前面的代码所示，我们的 API 声明了先前分配的整数指针的所有权。然后，它创建一系列线程，执行每个线程并等待每个线程完成。问题是，我们的内部函数需要一个`std::shared_ptr`(例如，可能这个内部函数在代码中的其他地方使用，那里有一对多的所有权场景，我们目前无法移除)。\n\n为了防止需要用`std::shared_ptr`定义我们的公共 API，我们可以通过将`std::unique_ptr`移动到新的`std::shared_ptr`中，然后从那里调用我们的线程，将`std::unique_ptr`转换为`std::shared_ptr`。\n\n`std::move()`是必需的，因为传递`std::unique_ptr`所有权的唯一方式是通过使用`std::move()`(因为在任何给定时间只有一个`std::unique_ptr`可以拥有指针)。\n\n现在，我们可以如下执行这个公共 API:\n\n```cpp\nint main(void)\n{\n    execute_threads(std::make_unique<int>(1));\n    std::cout << \"count: \" << count << '\\n';\n\n    return 0;\n}\n```\n\n这将产生以下输出:\n\n![](img/3662afb6-a730-4b83-a259-0d9182ad87de.png)\n\n将来，我们也许能够消除对`std::shared_ptr`的需求，并使用`get()`函数将`std::unique_ptr`传递给我们的内部函数，并且，当那个时候到来时，我们将不必修改公共 API。\n\n# 使用循环引用\n\n在这个食谱中，我们将学习如何使用循环引用。当我们使用多个`std::shared_ptr`时，循环引用发生，其中每个`std::shared_ptr`拥有对另一个的引用。这个方法很重要，因为当我们处理循环依赖对象时，这种类型的循环引用可能会发生(尽管这应该尽可能避免)。如果真的发生了，`std::shared_ptr`的共享特性会导致内存泄漏。本食谱将为您提供使用`std::weak_ptr`避免上述内存泄漏的技巧。\n\n# 准备好\n\n开始之前，请确保满足所有技术要求，包括安装 Ubuntu 18.04 或更高版本，并在终端窗口中运行以下内容:\n\n```cpp\n> sudo apt-get install build-essential git cmake valgrind \n```\n\n完成后，打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。\n\n# 怎么做...\n\n要使用循环引用，请执行以下步骤:\n\n1.  从一个新的终端，运行以下程序来下载该配方的源代码:\n\n```cpp\n> cd ~/\n> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git\n> cd Advanced-CPP-CookBook/chapter10\n```\n\n2.  要编译源代码，请运行以下命令:\n\n```cpp\n> cmake .\n> make recipe03_examples\n```\n\n3.  编译完源代码后，您可以通过运行以下命令来执行该配方中的每个示例:\n\n```cpp\n> valgrind ./recipe03_example01\n...\n==7960== HEAP SUMMARY:\n==7960== in use at exit: 64 bytes in 2 blocks\n==7960== total heap usage: 3 allocs, 1 frees, 72,768 bytes allocated\n...\n\n> valgrind ./recipe03_example02\n...\n==7966== HEAP SUMMARY:\n==7966== in use at exit: 64 bytes in 2 blocks\n==7966== total heap usage: 4 allocs, 2 frees, 73,792 bytes allocated\n...\n\n> valgrind ./recipe03_example03\n...\n==7972== HEAP SUMMARY:\n==7972== in use at exit: 0 bytes in 0 blocks\n==7972== total heap usage: 4 allocs, 4 frees, 73,792 bytes allocated\n...\n\n> valgrind ./recipe03_example04\n...\n==7978== HEAP SUMMARY:\n==7978== in use at exit: 0 bytes in 0 blocks\n==7978== total heap usage: 4 allocs, 4 frees, 73,792 bytes allocated\n...\n```\n\n在下一节中，我们将逐一介绍这些示例，并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。\n\n# 它是如何工作的...\n\n尽管应该避免循环引用，但是随着项目变得越来越复杂和庞大，循环引用很可能会出现。如果在这些循环引用发生时利用共享智能指针，可能会发生难以发现的内存泄漏。为了理解这是如何实现的，让我们看下面的例子:\n\n```cpp\nclass car;\nclass engine;\n```\n\n如前面的代码所示，我们从两个类原型开始。循环引用几乎总是以这种方式开始，因为一个类依赖于另一个类，反之亦然，需要使用类原型。\n\n让我们定义一个`car`如下:\n\n```cpp\nclass car\n{\n    friend void build_car();\n    std::shared_ptr<engine> m_engine;\n\npublic:\n    car() = default;\n};\n```\n\n如前面的代码所示，这是一个简单的类，它存储了一个指向`engine`的共享指针和一个名为`build_car()`的友元函数。现在，我们可以定义一个`engine`如下:\n\n```cpp\nclass engine\n{\n    friend void build_car();\n    std::shared_ptr<car> m_car;\n\npublic:\n    engine() = default;\n};\n```\n\n如前面的代码所示，一个`engine`类似于一个`car`，区别在于引擎存储了一个指向汽车的共享指针。不过，两者都有`build_car()`功能。两者都创建默认构造的共享指针，这意味着它们的共享指针在构造时是`NULL`指针。\n\n`build_car()`功能用于完成每个对象的构建，如下所示:\n\n```cpp\nvoid build_car()\n{\n    auto c = std::make_shared<car>();\n    auto e = std::make_shared<engine>();\n\n    c->m_engine = e;\n    e->m_car = c;\n}\n```\n\n如前面的代码所示，我们创建每个对象，然后设置汽车的引擎，反之亦然。由于汽车和发动机都在`build_car()`函数的范围内，我们预计一旦`build_car()`函数返回，这些指针将被删除。现在，我们可以如下执行这个`build_car()`功能:\n\n```cpp\nint main(void)\n{\n    build_car();\n    return 0;\n}\n```\n\n这看起来像一个简单的程序，但它有一个很难发现的内存泄漏。为了演示这一点，让我们在`valgrind`中运行这个应用，这是一个能够检测内存泄漏的动态内存分析工具:\n\n![](img/d039e626-58e3-4399-88f3-bb7b58a9d235.png)\n\n如前面截图所示，`valgrind`表示内存泄露。如果我们用`--leak-check=full`运行`valgrind`，它会告诉我们内存泄漏是汽车和发动机共享指针。发生这种内存泄漏的原因是汽车拥有对引擎的共享引用。同样的引擎拥有对汽车本身的共享引用。\n\n例如，考虑以下代码:\n\n```cpp\nvoid build_car()\n{\n    auto c = std::make_shared<car>();\n    auto e = std::make_shared<engine>();\n\n    c->m_engine = e;\n    e->m_car = c;\n\n    std::cout << c.use_count() << '\\n';\n    std::cout << e.use_count() << '\\n';\n}\n```\n\n如前面的代码所示，我们添加了对`use_count()`的调用，该调用输出`std::shared_ptr`包含的所有者数量。如果执行此操作，我们将看到以下输出:\n\n![](img/0fe8e3a7-4ec9-46f6-9e6f-14f91b815586.png)\n\n我们能看到两个车主的原因是因为`build_car()`函数在这里保存了对一辆车和一台发动机的引用:\n\n```cpp\n    auto c = std::make_shared<car>();\n    auto e = std::make_shared<engine>();\n```\n\n汽车第二次提到发动机是因为:\n\n```cpp\n    c->m_engine = e;\n```\n\n发动机和汽车也是如此。当`build_car()`功能完成时，以下内容首先失去作用域:\n\n```cpp\n    auto e = std::make_shared<engine>();\n```\n\n然而，引擎并没有被删除，因为汽车仍然保存着对引擎的引用。然后，汽车失去了作用范围:\n\n```cpp\n    auto c = std::make_shared<car>();\n```\n\n然而，汽车并没有被删除，因为引擎(还没有被删除)也保存着对汽车的引用。这导致`build_car()`返回时，汽车和引擎都没有被删除，因为两者仍然保持相互引用，没有办法告诉任何一个对象删除它们的引用。\n\n这种类型的循环内存泄漏虽然在我们的示例中很容易识别，但在复杂的代码中却非常难识别，这是应该避免共享指针和循环依赖的许多原因之一(通常更好的设计可以消除对两者的需求)。如果无法避免，可以使用`std::weak_ptr`代替，如下所示:\n\n```cpp\nclass car\n{\n    friend void build_car();\n    std::shared_ptr<engine> m_engine;\n\npublic:\n    car() = default;\n};\n```\n\n如前面的代码所示，我们仍然将我们的汽车定义为持有对引擎的共享引用。我们这样做是因为我们假设汽车的寿命更长(也就是说，在我们的模型中，你可以有一辆没有发动机的汽车，但你不能有一个没有汽车的发动机)。然而，发动机的定义如下:\n\n```cpp\nclass engine\n{\n    friend void build_car();\n    std::weak_ptr<car> m_car;\n\npublic:\n    engine() = default;\n};\n```\n\n如前面的代码所示，引擎现在存储了对汽车的弱引用。我们的`build_car()`功能定义如下:\n\n```cpp\nvoid build_car()\n{\n    auto c = std::make_shared<car>();\n    auto e = std::make_shared<engine>();\n\n    c->m_engine = e;\n    e->m_car = c;\n\n    std::cout << c.use_count() << '\\n';\n    std::cout << e.use_count() << '\\n';\n}\n```\n\n如前代码所示，`build_car()`功能不变。现在的不同之处在于，当我们使用`valgrind`执行这个应用时，我们会看到以下输出:\n\n![](img/0ac121f9-776c-48ff-a5e0-fdbe56d97fa4.png)\n\n如上图截图所示，没有内存泄漏，汽车的`use_count()`为`1`，而发动机的`use_count()`与上例相比仍为`2`。在引擎类中，我们使用`std::weak_ptr`，它可以访问`std::shared_ptr`管理的托管对象，但是在创建时不会增加托管对象的内部计数。这为`std::weak_ptr`提供了查询`std::shared_ptr`是否有效的能力，而不必持有对指针本身的强引用。\n\n内存泄漏被清除的原因是，当发动机失去作用域时，其使用次数从`2`减少到`1`。一旦汽车失去作用范围，只有`1`的使用计数，它被删除，这反过来减少发动机的使用计数到`0`，这导致发动机也被删除。\n\n我们在引擎中使用`std::weak_ptr`而不是 C 风格指针的原因是`std::weak_ptr`为我们提供了查询托管对象的能力，以查看指针是否仍然有效。例如，假设我们需要检查汽车是否仍然存在，如下所示:\n\n```cpp\nclass engine\n{\n    friend void build_car();\n    std::weak_ptr<car> m_car;\n\npublic:\n    engine() = default;\n\n    void test()\n    {\n        if (m_car.expired()) {\n            std::cout << \"car deleted\\n\";\n        }\n    }\n};\n```\n\n使用`expired()`功能，我们可以在使用前测试看看车是否还存在，这是 C 型指针无法做到的。现在，我们可以将我们的`build_car()`函数编写如下:\n\n```cpp\nvoid build_car()\n{\n auto e = std::make_shared<engine>();\n\n {\n auto c = std::make_shared<car>();\n\n c->m_engine = e;\n e->m_car = c;\n }\n\n e->test();\n}\n```\n\n在前面的例子中，我们创建了一个引擎，然后创建了一个新的范围来创建我们的汽车。然后，我们创建循环引用并失去作用域。这导致汽车如预期的那样被删除。不同的是，我们的引擎还没有被删除，因为我们仍然拥有对它的引用。现在，我们可以运行我们的测试函数，当它与`valgrind`一起运行时，会产生以下输出:\n\n![](img/37efcbf4-6ee5-4fef-b31a-150aa6c7b76f.png)\n\n如前面的截图所示，没有内存泄漏。`std::weak_ptr`成功去除了循环引用引入的鸡和蛋问题。因此，`std::shared_ptr`能够按预期运行，以正确的顺序释放内存。一般来说，应该尽可能避免循环引用和依赖关系，但是，如果无法避免，可以使用`std::weak_ptr`，如本食谱所示，来防止内存泄漏。\n\n# 使用智能指针进行类型转换\n\n在本食谱中，我们将学习如何使用`std::unique_ptr`和`std::shared_ptr`进行打字。类型转换允许您将一种类型转换成另一种类型。这个方法很重要，因为它展示了当试图转换智能指针的类型时(例如，当使用虚拟继承进行向上转换或向下转换时)使用`std::unique_ptr`和`std::shared_ptr`处理类型转换的正确方法。\n\n# 准备好\n\n开始之前，请确保满足所有技术要求，包括安装 Ubuntu 18.04 或更高版本，并在终端窗口中运行以下内容:\n\n```cpp\n> sudo apt-get install build-essential git cmake\n```\n\n完成后，打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。\n\n# 怎么做...\n\n要了解类型转换的工作原理，请执行以下步骤:\n\n1.  从新的终端，运行以下命令下载源代码:\n\n```cpp\n> cd ~/\n> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git\n> cd Advanced-CPP-CookBook/chapter10\n```\n\n2.  要编译源代码，请运行以下命令:\n\n```cpp\n> cmake .\n> make recipe04_examples\n```\n\n3.  编译完源代码后，您可以通过运行以下命令来执行该配方中的每个示例:\n\n```cpp\n> ./recipe04_example01\ndowncast successful!!\n\n> ./recipe04_example02\ndowncast successful!!\n```\n\n在下一节中，我们将逐一介绍这些示例，并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。\n\n# 它是如何工作的...\n\n使用智能指针进行类型转换并不像您预期的那样简单。\n\n为了更好地解释这一点，让我们看一个简单的例子，说明如何使用`std::unique_ptr`从基类向子类进行类型转换:\n\n```cpp\nclass base\n{\npublic:\n    base() = default;\n    virtual ~base() = default;\n};\n```\n\n让我们看看这是如何工作的:\n\n1.  我们从一个虚拟基类开始，如前面的代码所示，然后我们将基类子类化如下:\n\n```cpp\nclass subclass : public base\n{\npublic:\n    subclass() = default;\n    ~subclass() override = default;\n};\n```\n\n2.  接下来，我们在`main()`函数中创建一个`std::unique_ptr`，并将指针传递给一个`foo()`函数:\n\n```cpp\nint main(void)\n{\n    auto ptr = std::make_unique<subclass>();\n    foo(ptr.get());\n\n    return 0;\n}\n```\n\n`std::unique_ptr`只是拥有指针的生存期。指针的任何使用都需要使用`get()`函数，该函数从该点开始将`std::unique_ptr`转换为正常的 C 型指针。这是`std::unique_ptr`的预期用途，因为它不是为了确保指针安全而设计的，而是为了确保谁拥有指针被很好地定义，最终决定何时删除指针。\n\n3.  现在`foo()`功能可以定义如下:\n\n```cpp\nvoid foo(base *b)\n{\n    if (dynamic_cast<subclass *>(b)) {\n        std::cout << \"downcast successful!!\\n\";\n    }\n}\n```\n\n如前面的代码所示，`foo()`函数可以将指针视为普通的 C 风格指针，使用`dynamic_cast()`从基指针向下转换回原始子类。\n\n这种相同风格的类型转换是标准的 C++，不适用于`std::shared_ptr`。原因是因为需要类型转换版本的`std::shared_ptr`的代码可能还需要保存对指针的引用(也就是说，`std::shared_ptr`的副本以防止删除)。\n\n也就是说，不可能从`base *b`到`std::shared_ptr<subclass>`，因为`std::shared_ptr`没有指针的引用；相反，它保存对托管对象的引用，托管对象存储对实际指针的引用。由于`base *b`不存储托管对象，因此无法从中创建`std::shared_ptr`。\n\n然而，C++ 确实提供了`static_cast()`、`reinterpret_cast()`、`const_cast()`和`dynamic_cast()`的`std::shared_ptr`版本来执行共享指针的类型转换，这在类型转换时保留了托管对象。让我们看一个例子:\n\n```cpp\nclass base\n{\npublic:\n    base() = default;\n    virtual ~base() = default;\n};\n\nclass subclass : public base\n{\npublic:\n    subclass() = default;\n    ~subclass() override = default;\n};\n```\n\n如前面的代码所示，我们从相同的基类和子类开始。区别出现在我们的`foo()`函数中:\n\n```cpp\nvoid foo(std::shared_ptr<base> b)\n{\n    if (std::dynamic_pointer_cast<subclass>(b)) {\n        std::cout << \"downcast successful!!\\n\";\n    }\n}\n```\n\n不取`base *b`，取`std::shared_ptr<base>`。现在，我们可以使用`std::dynamic_pointer_cast()`功能代替`dynamic_cast()`将`std::shared_ptr<base>`降频至`std::shared_ptr<subclass>`。`std::shared_ptr`类型转换功能为我们提供了类型转换的能力，同时根据需要保持对`std::shared_ptr`的访问。\n\n产生的`main()`函数如下所示:\n\n```cpp\nint main(void)\n{\n    auto ptr = std::make_shared<subclass>();\n    foo(ptr);\n\n    return 0;\n}\n```\n\n这将产生以下输出:\n\n![](img/8b7d0aa2-1117-4b34-a220-0b1bba6777d6.png)\n\n应该注意的是，我们不需要显式上转换，因为这可以自动完成(类似于常规指针)。我们只需要显式向下转换。\n\n# 显微镜下的堆\n\n在这个食谱中，我们将学习堆在 Linux 中是如何工作的。我们将深入研究当您使用`std::unique_ptr`时，Linux 实际上是如何提供堆内存的。\n\n虽然这个方法是为那些拥有更高级功能的人准备的，但它很重要，因为它将教会你应用如何从堆中分配内存(也就是说，使用`new()` / `delete()`)，这反过来将向你展示为什么堆分配永远不应该从时间关键的代码中完成，因为它们很慢。当堆分配可以安全执行时，当应用中应该避免堆分配时，即使我们检查的一些汇编代码很难遵循，这个方法也会教你所需的技能。\n\n# 准备好\n\n开始之前，请确保满足所有技术要求，包括安装 Ubuntu 18.04 或更高版本，并在终端窗口中运行以下内容:\n\n```cpp\n> sudo apt-get install build-essential git cmake\n```\n\n完成后，打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。\n\n# 怎么做...\n\n要尝试本章的代码文件，请执行以下步骤:\n\n1.  从新的终端，运行以下命令下载源代码:\n\n```cpp\n> cd ~/\n> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git\n> cd Advanced-CPP-CookBook/chapter10\n```\n\n2.  要编译源代码，请运行以下命令:\n\n```cpp\n> cmake .\n> make recipe05_examples\n```\n\n3.  编译完源代码后，您可以通过运行以下命令来执行该配方中的每个示例:\n\n```cpp\n> ./recipe05_example01\n```\n\n在下一节中，我们将逐一介绍这些示例，并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。\n\n# 它是如何工作的...\n\n为了更好地理解代码必须执行到什么程度才能在堆上分配变量，我们将从下面的简单示例开始:\n\n```cpp\nint main(void)\n{\n    auto ptr = std::make_unique<int>();\n}\n```\n\n如前例所示，我们使用`std::unique_ptr()`分配一个整数。我们使用`std::unique_ptr()`作为我们的起点，因为这是大多数 C++ 核心指南代码在堆上分配内存的方式。\n\n`std::make_unique()`函数使用以下伪逻辑分配一个`std::unique_ptr`(这是一个简化的例子，因为它没有显示如何处理自定义删除程序):\n\n```cpp\nnamespace std\n{\n    template<typename T, typename... ARGS>\n    auto make_unique(ARGS... args)\n    {\n        return std::unique_ptr(new T(std::forward<ARGS>(args)...));\n    }\n}\n```\n\n如前面的代码所示，`std::make_unique()`函数创建了一个`std::unique_ptr`，并给它一个指针，该指针是用`new()`运算符分配的。一旦`std::unique_ptr`失去作用域，它将使用`delete()`删除指针。\n\n当编译器看到新的运算符时，它会用对运算符`new(unsigned long)`的调用来替换代码。要看到这一点，让我们看下面的例子:\n\n```cpp\nint main(void)\n{\n    auto ptr = new int;\n}\n```\n\n在前面的例子中，我们使用`new()`分配了一个简单的指针。现在，我们可以查看结果编译的程序集，可以在下面的截图中看到:\n\n![](img/b091f70c-4180-4aa8-a75b-8d820a238fbb.png)\n\n如下图截图所示，对`_Znwm`进行了一次调用，这是对`operator new(unsigned long)`进行了撕裂的 C++ 代码，很容易解缠:\n\n![](img/c027acee-fceb-4dbb-89c2-3bd138eecd8c.png)\n\n`new()`运算符本身看起来像下面的伪代码(注意，这没有考虑到禁用异常支持或为新处理程序提供支持的能力):\n\n```cpp\nvoid* operator new(size_t size)\n{\n    if (auto ptr = malloc(size)) {\n        return ptr;\n    }\n\n    throw std::bad_alloc();\n}\n```\n\n现在，我们可以看看新的操作符，看到`malloc()`被调用:\n\n![](img/837ddc0c-aa39-4480-a9dd-618520a38d5f.png)\n\n如前面截图所示，调用`malloc()`。如果得到的指针不是`NULL`，操作员返回；否则，它将进入错误状态，这涉及到调用新的处理程序并最终抛出`std::bad_alloc()`(至少在默认情况下)。\n\n对`malloc()`的调用本身要复杂得多。当一个应用本身启动时，它做的第一件事就是保留堆空间。操作系统给每个应用一个连续的虚拟内存块进行操作，Linux 上的堆是应用中的最后一块内存(即`new()`返回的内存来自应用内存空间的末端)。将堆放在这里为操作系统提供了一种根据需要向应用添加额外内存的方法(因为操作系统只是扩展了应用的虚拟内存)。\n\n应用本身使用`sbrk()`函数，在内存耗尽时向操作系统请求更多内存。调用此函数时，操作系统从其内部页面池中分配页面内存，并通过移动应用内存空间的末尾将此内存映射到应用中。映射过程本身很慢，因为操作系统不仅必须从池中分配页面，这需要某种搜索和保留逻辑，而且还必须遍历应用的页面表，以将这些额外的内存添加到其虚拟地址空间中。\n\n一旦`sbrk()`为应用提供了额外的内存，`malloc()`引擎就会接管。正如我们前面提到的，操作系统只是将内存页面映射到应用中。根据请求的不同，每个页面可以小到 4k 字节，大到 2 MB 甚至 1 GB。然而，在我们的例子中，我们分配了一个简单的整数，它的大小只有`4`字节。为了在不浪费内存的情况下将页面转换成小对象，`malloc()`本身有一种算法，可以将操作系统提供的内存分解成小块。该引擎还必须处理何时释放这些内存块，以便它们可以再次使用。这需要复杂的数据结构来管理应用的所有内存，对`malloc()`、`free()`、`new()`和`delete()`的每次调用都必须运用这一逻辑。\n\n使用`std::make_unique()`创建`std::unique_ptr`的简单调用必须使用从`new()`分配的内存创建`std::unique_ptr`，而`new()`实际上是调用`malloc()`，它必须在复杂的数据结构中搜索，以找到最终可以返回的空闲内存块，也就是说，假设`malloc()`有空闲内存，并且不必使用`sbrk()`向操作系统请求更多内存。\n\n换句话说，动态(即堆)内存很慢，应该只在需要的时候使用，理想情况下，不要在时间关键的代码中使用。"
  },
  {
    "path": "docs/adv-cpp-prog-cb/11.md",
    "content": "# 十一、C++ 中的常见模式\n\n在本章中，您将学习 C++ 中的各种设计模式。设计模式为正确解决不同类型的问题提供了一种通用的方法，通常，设计模式会在互联网上、会议上以及工作中的饮水机前讨论它们的优缺点。\n\n本章的目标是向您介绍一些更流行、不太流行甚至有争议的模式，让您了解设计模式试图解决的不同类型的问题。这是重要的一章，因为它将教会你解决困难问题的技巧，教会你解决别人过去经历过的常见问题的现有方法。当你在自己的应用中遇到问题时，学习这些设计模式的子集将为自己发现其他设计模式奠定基础。\n\n本章中的配方如下:\n\n*   学习工厂模式\n*   正确使用单一模式\n*   用装饰器模式扩展您的对象\n*   添加与观察者模式的通信\n*   利用静态多态性提高性能\n\n# 技术要求\n\n要编译和运行本章中的示例，您必须拥有运行 Ubuntu 18.04 的计算机的管理权限，并且具有功能性互联网连接。在运行这些示例之前，您必须安装以下内容:\n\n```cpp\n> sudo apt-get install build-essential git cmake \n```\n\n如果这安装在 Ubuntu 18.04 以外的任何操作系统上，则需要 GCC 7.4 或更高版本以及 CMake 3.6 或更高版本。\n\n本章的代码文件可以在[https://github . com/packt publishing/Advanced-CPP-cook book/tree/master/chapter 11](https://github.com/PacktPublishing/Advanced-CPP-CookBook/tree/master/chapter11)找到。\n\n# 学习工厂模式\n\n在这个食谱中，我们将学习工厂模式是什么，如何实现，以及何时使用。这个方法很重要，尤其是在单元测试中，因为工厂模式提供了添加接缝的能力(也就是说，代码中的有意位置提供了进行更改的机会)，能够更改另一个对象分配的对象类型，包括为测试分配假对象的能力。\n\n# 准备好\n\n开始之前，请确保满足所有技术要求，包括安装 Ubuntu 18.04 或更高版本，并在终端窗口中运行以下内容:\n\n```cpp\n> sudo apt-get install build-essential git cmake\n```\n\n这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后，打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。\n\n# 怎么做...\n\n执行以下步骤来尝试工厂模式的代码:\n\n1.  从新的终端，运行以下命令下载源代码:\n\n```cpp\n> cd ~/\n> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git\n> cd Advanced-CPP-CookBook/chapter11\n```\n\n2.  要编译源代码，请运行以下命令:\n\n```cpp\n> cmake .\n> make recipe01_examples\n```\n\n3.  编译源代码后，您可以通过运行以下命令来执行该配方中的每个示例:\n\n```cpp\n> ./recipe01_example01\n\n> ./recipe01_example02\n\n> ./recipe01_example03\ncorrect answer: The answer is: 42\n\n> ./recipe01_example04\nwrong answer: Not sure\n\n> ./recipe01_example05\ncorrect answer: The answer is: 42\n```\n\n在下一节中，我们将逐一介绍这些示例，并解释每个示例程序的功能，以及它与本食谱中所教授的课程之间的关系。\n\n# 它是如何工作的...\n\n工厂模式为分配资源的对象提供了一种改变对象分配的类型的方法。为了更好地理解这种模式是如何工作的，以及它为什么如此有用，让我们看下面的例子:\n\n```cpp\nclass know_it_all\n{\npublic:\n    auto ask_question(const char *question)\n    {\n        (void) question;\n        return answer(\"The answer is: 42\");\n    }\n};\n```\n\n如前面的代码所示，我们从一个名为`know_it_all`的类开始，当被问及一个问题时，它会提供一个答案。在这种特殊情况下，无论问什么问题，它总是返回相同的答案。答案定义如下:\n\n```cpp\nclass answer\n{\n    std::string m_answer;\n\npublic:\n    answer(std::string str) :\n        m_answer{std::move(str)}\n    { }\n};\n```\n\n如前所示，答案是一个简单的类，它被构造为一个字符串，并在内部存储该字符串。需要注意的是，在这种情况下，该 API 的用户实际上不能提取答案类存储的字符串，这意味着这些 API 的使用如下:\n\n```cpp\nint main(void)\n{\n    know_it_all universe;\n    auto ___ = universe.ask_question(\"What is the meaning of life?\");\n}\n```\n\n如前所示，我们可以问一个问题，并提供一个结果，但我们不确定实际提供了什么结果。这种类型的问题在面向对象编程中一直存在，测试这种逻辑是为什么整本书都以对象嘲讽为主题的众多原因之一。模拟是一个对象的假版本，专门设计用于验证测试的输出(不像假的，它只不过是一个提供测试输入的对象)。然而，在前面的例子中，模拟仍然需要一种方法来创建，以便验证函数的输出。输入工厂模式。\n\n让我们按如下方式修改答案类:\n\n```cpp\nclass answer\n{\n    std::string m_answer;\n\npublic:\n    answer(std::string str) :\n        m_answer{std::move(str)}\n    { }\n\n    static inline auto make_answer(std::string str)\n    { return answer(str); }\n};\n```\n\n如前面的代码所示，我们添加了一个静态函数，允许`answer`类创建自身的实例。我们没有改变这样一个事实，即`answer`类不提供提取其内容的能力，只是`answer`类是如何创建的。然后我们可以修改`know_it_all`类如下:\n\n```cpp\ntemplate<factory_t factory = answer::make_answer>\nclass know_it_all\n{\npublic:\n    auto ask_question(const char *question)\n    {\n        (void) question;\n        return factory(\"The answer is: 42\");\n    }\n};\n```\n\n如前面的代码所示，这里唯一的区别是`know_it_all`类为`factory_t`取了一个模板参数，并使用它来创建答案类，而不是直接创建`answer`类。`factory_t`定义如下:\n\n```cpp\nusing factory_t = answer(*)(std::string str);\n```\n\n这默认为我们添加到`answer`类的静态`make_answer()`函数。前面的例子以最简单的形式演示了工厂模式。我们不直接创建对象，而是将对象的创建委托给另一个对象。前面的实现没有改变这两个类的使用方式，如下所示:\n\n```cpp\nint main(void)\n{\n    know_it_all universe;\n    auto ___ = universe.ask_question(\"What is the meaning of life?\");\n}\n```\n\n如上图所示，`main()`逻辑保持不变，但这种新方法确保了`know_it_all`类专注于回答问题，而不用担心如何创建`answer`类本身，将该任务留给不同的对象。这种微妙变化背后的真正力量是，我们现在可以为`know_it_all`级提供不同的工厂，导致不同的`answer`级被退回。为了演示这一点，让我们创建一个新的`answer`类，如下所示:\n\n```cpp\nclass expected_answer : public answer\n{\npublic:\n    expected_answer(std::string str) :\n        answer{str}\n    {\n        if (str != \"The answer is: 42\") {\n            std::cerr << \"wrong answer: \" << str << '\\n';\n            exit(1);\n        }\n\n        std::cout << \"correct answer: \" << str << '\\n';\n    }\n\n    static inline answer make_answer(std::string str)\n    { return expected_answer(str); }\n};\n```\n\n如上图所示，我们已经创建了一个新的`answer`类，它对原来的`answer`类进行了子分类。这个新类检查它在构造过程中给出的值，并根据它提供的字符串输出成功或失败。我们可以如下使用这个新的`answer`类:\n\n```cpp\nint main(void)\n{\n    know_it_all<expected_answer::make_answer> universe;\n    auto ___ = universe.ask_question(\"What is the meaning of life?\");\n}\n```\n\n以下是结果输出:\n\n![](img/41a225db-9424-4bd1-aa9f-8be824d88b8d.png)\n\n使用前面的方法，我们不需要修改原始的`answer`类，就不能问不同的问题来查看`know_it_all`类是否提供了正确的答案。例如，假设`know_it_all`类是这样实现的:\n\n```cpp\ntemplate<factory_t factory = answer::make_answer>\nclass know_it_all\n{\npublic:\n    auto ask_question(const char *question)\n    {\n        (void) question;\n        return factory(\"Not sure\");\n    }\n};\n```\n\n我们测试了这个版本的`know_it_all`类，如下所示:\n\n```cpp\nint main(void)\n{\n    know_it_all<expected_answer::make_answer> universe;\n    auto ___ = universe.ask_question(\"What is the meaning of life?\");\n}\n```\n\n结果如下:\n\n![](img/3626806f-b78e-4c07-81cf-329507c18135.png)\n\n需要注意的是，工厂模式有几种实现方式。前面的方法使用模板参数来改变`know_it_all`类创建答案的方式，但是我们也可以使用运行时方法，如本例所示:\n\n```cpp\nclass know_it_all\n{\n    std::function<answer(std::string str)> m_factory;\n\npublic:\n    know_it_all(answer(*f)(std::string str) = answer::make_answer) :\n        m_factory{f}\n    { }\n\n    auto ask_question(const char *question)\n    {\n        (void) question;\n        return m_factory(\"The answer is: 42\");\n    }\n};\n```\n\n如上图所示，我们从一个自定义的`know_it_all`构造函数开始，该构造函数存储一个工厂函数的指针，同样默认为我们的`answer`类，但是如果我们选择的话，它提供了更改工厂的能力，如下图所示:\n\n```cpp\nint main(void)\n{\n    know_it_all universe(expected_answer::make_answer);\n    auto ___ = universe.ask_question(\"What is the meaning of life?\");\n}\n```\n\n如果我们愿意，我们还可以在这个类中添加一个 setter，以便在运行时更改这个函数指针。\n\n# 正确使用单一模式\n\n在这个食谱中，我们将学习如何在 C++ 11 和更高版本中正确地实现单例模式，以及什么时候使用单例模式是合适的。这个方法很重要，因为它将教你何时使用单例模式，该模式提供了单个全局资源的明确定义，确保资源保持全局，而不可能有多个副本。\n\n# 准备好\n\n开始之前，请确保满足所有技术要求，包括安装 Ubuntu 18.04 或更高版本，并在终端窗口中运行以下内容:\n\n```cpp\n> sudo apt-get install build-essential git cmake\n```\n\n这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后，打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。\n\n# 怎么做...\n\n执行以下步骤来尝试单例模式:\n\n1.  从新的终端，运行以下命令下载源代码:\n\n```cpp\n> cd ~/\n> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git\n> cd Advanced-CPP-CookBook/chapter11\n```\n\n2.  要编译源代码，请运行以下命令:\n\n```cpp\n> cmake .\n> make recipe01_examples\n```\n\n3.  编译源代码后，您可以通过运行以下命令来执行该配方中的每个示例:\n\n```cpp\n> ./recipe02_example01\nmemory: 0x4041a0\ni1: 0x4041a0\ni2: 0x4041a4\ni3: 0x4041a8\ni4: 0x4041ac\n\n> ./recipe02_example02\nmemory: 0x4041a0\ni1: 0x4041a0\ni2: 0x4041a4\ni3: 0x4041a0\ni4: 0x4041a4\n\n> ./recipe02_example03\nmemory: 0x4041a0\ni1: 0x4041a0\ni2: 0x4041a4\ni3: 0x4041a8\ni4: 0x4041ac\n\n> ./recipe02_example04\nmemory: 0x4041a0\ni1: 0x4041a0\ni2: 0x4041a4\ni3: 0x4041a8\ni4: 0x4041ac\n```\n\n在下一节中，我们将逐一介绍这些示例，并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。\n\n# 它是如何工作的...\n\n单例模式在 C++ 中已经存在了几年，可以说它是所有 C++ 中最有争议的模式之一，因为它的全局特性在您的应用中引入了耦合(类似于全局变量如何引入耦合)。单例模式实现了一个单一的全局资源。具体来说，它创建了一个保持全局范围的对象，同时确保不存在自身的副本。关于是否应该在您的代码中使用单例模式的争论不会在本书中得到回答，因为这取决于您的用例，但是让我们至少讨论一下这种模式的一些优点和缺点。\n\n**优势:**单体模式为只能包含单个实例的全局资源提供了一个明确定义的接口。不管我们喜欢与否，全局资源存在于我们所有的应用中(例如，堆内存)。如果需要这样的全局资源，并且您有一个处理耦合的机制(例如，像希波莫克这样的模仿引擎)，那么单例模式是确保全局资源得到正确管理的好方法。\n\n**缺点:**以下是缺点:\n\n*   单例模式定义了一个全局资源，像任何全局资源(例如，一个全局变量)一样，任何使用单例对象的代码都与单例紧密耦合。在面向对象的设计中，应该总是避免耦合，因为它防止了伪造代码可能依赖的资源的能力，这限制了测试时的灵活性。\n*   单一模式隐藏了依赖性。检查对象的接口时，无法确定对象的实现是否依赖于全局资源。大多数人认为这可以用好的文档来处理。\n*   单例模式在应用的整个生命周期中保持其状态。当单元测试作为单例的状态从一个单元测试进行到下一个单元测试时，尤其如此(也就是说，缺点是显而易见的)，大多数人认为这违反了什么是单元测试。\n\n总的来说，应该始终避免全球资源。句号。确保您的代码被正确地编写，以便在需要单个全局资源时强制执行单例设计模式。让我们讨论下面的例子。\n\n假设您正在为嵌入式设备编写应用，并且您的嵌入式设备有一个额外的内存池，可以映射到您的应用中(例如，视频或网络设备的设备内存)。现在，假设您只能拥有这些额外的内存池中的一个，并且您需要实现一组 API 来从这个池中分配内存。在我们的示例中，我们将使用以下内容实现这个内存池:\n\n```cpp\nuint8_t memory[0x1000] = {};\n```\n\n接下来，我们将实现一个内存管理器类来分配该池中的内存，如下所示:\n\n```cpp\nclass mm\n{\n    uint8_t *cursor{memory};\n\npublic:\n    template<typename T>\n    T *allocate()\n    {\n        if (cursor + sizeof(T) > memory + 0x1000) {\n            throw std::bad_alloc();\n        }\n\n        auto ptr = new (cursor) T;\n        cursor += sizeof(T);\n\n        return ptr;\n    }\n};\n```\n\n如前面的代码所示，我们已经创建了一个内存管理器类，该类存储了一个指向内存缓冲区的指针，该缓冲区包含我们的单个全局资源。然后，我们创建一个简单的分配函数，根据需要处理这些内存(没有释放能力，这使得算法非常简单)。\n\n由于这是一个全局资源，我们按如下方式全局创建该类:\n\n```cpp\nmm g_mm;\n```\n\n最后，我们可以使用新的内存管理器，如下所示:\n\n```cpp\nint main(void)\n{\n    auto i1 = g_mm.allocate<int>();\n    auto i2 = g_mm.allocate<int>();\n    auto i3 = g_mm.allocate<int>();\n    auto i4 = g_mm.allocate<int>();\n\n    std::cout << \"memory: \" << (void *)memory << '\\n';\n    std::cout << \"i1: \" << (void *)i1 << '\\n';\n    std::cout << \"i2: \" << (void *)i2 << '\\n';\n    std::cout << \"i3: \" << (void *)i3 << '\\n';\n    std::cout << \"i4: \" << (void *)i4 << '\\n';\n}\n```\n\n在前面的示例中，我们分配了四个整数指针，然后输出内存块的地址和整数指针的地址，以确保算法按预期工作，从而产生以下输出:\n\n![](img/4538b81d-108f-4a76-98a9-f759ccfadcff.png)\n\n如前所示，内存管理器会根据需要正确分配内存。\n\n前面实现的问题是，内存管理器只是一个类，就像任何其他类一样，这意味着它可以被创建任意多次，也可以被复制。为了更好地说明为什么这是一个问题，让我们看看下面的例子。让我们创建两个内存管理器，而不是创建一个:\n\n```cpp\nmm g_mm1;\nmm g_mm2;\n```\n\n接下来，让我们按如下方式使用这两种内存管理器:\n\n```cpp\nint main(void)\n{\n    auto i1 = g_mm1.allocate<int>();\n    auto i2 = g_mm1.allocate<int>();\n    auto i3 = g_mm2.allocate<int>();\n    auto i4 = g_mm2.allocate<int>();\n\n    std::cout << \"memory: \" << (void *)memory << '\\n';\n    std::cout << \"i1: \" << (void *)i1 << '\\n';\n    std::cout << \"i2: \" << (void *)i2 << '\\n';\n    std::cout << \"i3: \" << (void *)i3 << '\\n';\n    std::cout << \"i4: \" << (void *)i4 << '\\n';\n}\n```\n\n如前所示，唯一的区别是我们现在使用两个内存管理器，而不是一个。这将产生以下输出:\n\n![](img/8a5fd1a4-19c6-44c5-8250-18a3e61714ec.png)\n\n如前所示，内存已被双重分配，这可能会导致损坏和未定义的行为。出现这种情况的原因是内存缓冲区本身是一种全局资源，这是我们无法改变的。内存管理器本身没有做任何事情来确保这种情况不会发生，因此，该 API 的用户可能会意外地创建第二个内存管理器。请注意，在我们的示例中，我们显式地创建了第二个副本，但是第二个副本可能通过简单地传递内存管理器而出现，在此过程中无意中创建了副本。\n\n为了解决这个问题，我们必须处理两个特定的场景:\n\n*   创建多个内存管理器实例\n*   复制内存管理器\n\n为了解决这两个问题，现在让我们展示单例模式:\n\n```cpp\nclass mm\n{\n    uint8_t *cursor{memory};\n    mm() = default;\n```\n\n如上图所示，我们从标记为`private`的构造函数开始。将构造函数标记为`private`可以防止内存管理器创建自己的内存管理器实例。相反，要获得内存管理器的实例，我们将使用以下`public`函数:\n\n```cpp\n    static auto &instance()\n    {\n        static mm s_mm;\n        return s_mm;\n    }\n```\n\n前面的这个函数创建了内存管理器的静态(即全局)实例，然后返回对这个实例的引用。使用这个函数，API 的用户只能从这个函数获得内存管理器的一个实例，这个函数总是只返回对全局定义资源的引用。换句话说，没有编译器的抱怨，就无法创建额外的类实例。\n\n创建单例类的最后一步如下:\n\n```cpp\n    mm(const mm &) = delete;\n    mm &operator=(const mm &) = delete;\n    mm(mm &&) = delete;\n    mm &operator=(mm &&) = delete;\n```\n\n如前所示，复制和移动构造函数/运算符被显式删除。这解决了第二个问题。通过移除复制构造函数和运算符，无法创建全局资源的副本，从而确保该类仅作为单个全局对象存在。\n\n为了使用这个单例类，我们将执行以下操作:\n\n```cpp\nint main(void)\n{\n    auto i1 = mm::instance().allocate<int>();\n    auto i2 = mm::instance().allocate<int>();\n    auto i3 = mm::instance().allocate<int>();\n    auto i4 = mm::instance().allocate<int>();\n\n    std::cout << \"memory: \" << (void *)memory << '\\n';\n    std::cout << \"i1: \" << (void *)i1 << '\\n';\n    std::cout << \"i2: \" << (void *)i2 << '\\n';\n    std::cout << \"i3: \" << (void *)i3 << '\\n';\n    std::cout << \"i4: \" << (void *)i4 << '\\n';\n}\n```\n\n这将产生以下输出:\n\n![](img/cde66b36-11d4-4296-b84d-0a76d9e7da36.png)\n\n如果我们尝试自己创建内存管理器的另一个实例，我们会得到类似于下面的错误:\n\n```cpp\n/home/user/book/chapter11/recipe02.cpp:166:4: error: ‘constexpr mm::mm()’ is private within this context\n  166 | mm g_mm;\n```\n\n最后，由于 singleton 类是一个单一的全局资源，我们可以创建包装器来消除冗长，如下所示:\n\n```cpp\ntemplate<typename T>\nconstexpr T *allocate()\n{\n    return mm::instance().allocate<T>();\n}\n```\n\n这种变化可以如下使用:\n\n```cpp\nint main(void)\n{\n    auto i1 = allocate<int>();\n    auto i2 = allocate<int>();\n    auto i3 = allocate<int>();\n    auto i4 = allocate<int>();\n\n    std::cout << \"memory: \" << (void *)memory << '\\n';\n    std::cout << \"i1: \" << (void *)i1 << '\\n';\n    std::cout << \"i2: \" << (void *)i2 << '\\n';\n    std::cout << \"i3: \" << (void *)i3 << '\\n';\n    std::cout << \"i4: \" << (void *)i4 << '\\n';\n}\n```\n\n如前所示，`constexpr`包装器提供了一种简单的方法来消除我们的单例类的冗长性，如果内存管理器不是单例的话，这是很难做到的。\n\n# 用装饰器模式扩展您的对象\n\n在本食谱中，我们将学习如何实现装饰器模式，该模式提供了扩展类功能的能力，而不需要继承，继承本质上是静态的。这个方法很重要，因为继承不支持在运行时扩展类的能力，装饰器模式解决了这个问题。\n\n# 准备好\n\n开始之前，请确保满足所有技术要求，包括安装 Ubuntu 18.04 或更高版本，并在终端窗口中运行以下内容:\n\n```cpp\n> sudo apt-get install build-essential git cmake\n```\n\n这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后，打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。\n\n# 怎么做...\n\n执行以下步骤来尝试此食谱:\n\n1.  从新的终端，运行以下命令下载源代码:\n\n```cpp\n> cd ~/\n> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git\n> cd Advanced-CPP-CookBook/chapter11\n```\n\n2.  要编译源代码，请运行以下命令:\n\n```cpp\n> cmake .\n> make recipe03_examples\n```\n\n3.  编译源代码后，您可以通过运行以下命令来执行该配方中的每个示例:\n\n```cpp\n> ./recipe03_example01\nbutton width: 42\n\n> ./recipe03_example02\nbutton1 width: 10\nbutton2 width: 42\n\n> ./recipe03_example03\nbutton width: 74\n\n> ./recipe03_example04\nbutton width: 42\nbutton content width: 4\n```\n\n在下一节中，我们将逐一介绍这些示例，并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。\n\n# 它是如何工作的...\n\n在这个食谱中，我们将学习如何实现装饰器模式。首先，让我们看一个简单的例子:假设我们正在编写一个 C++ 应用，它将托管一个网站。在我们的网站中，我们需要定义一个用户可以点击的按钮，但我们需要计算按钮的宽度，给定一个额外的边距，增加按钮的总大小:\n\n```cpp\nclass margin\n{\npublic:\n    int width()\n    {\n        return 32;\n    }\n};\n```\n\n如前所示，我们已经创建了一个名为`margin`的类，它返回所讨论的边距的宽度(为了简化我们的示例，我们将只关注宽度)。然后，我们可以如下定义我们的按钮:\n\n```cpp\nclass button : public margin\n{\npublic:\n    int width()\n    {\n        return margin::width() + 10;\n    }\n};\n```\n\n如上图所示，我们按钮的总宽度是按钮本身的宽度加上边距的宽度。然后我们可以得到按钮的宽度，如下所示:\n\n```cpp\nint main()\n{\n    auto b = new button();\n    std::cout << \"button width: \" << b->width() << '\\n';\n}\n```\n\n这将产生以下输出:\n\n![](img/697bea8a-1cdb-45ac-8431-53344285e825.png)\n\n前面示例的问题是按钮必须始终有边距，因为按钮直接继承了边距类。有一些方法可以防止这种情况(例如，我们的按钮可以有一个配置选项来确定按钮是否返回有边距的宽度)，但是在这个食谱中，我们将使用装饰器模式来解决这个问题，允许我们创建两个按钮:一个有边距的按钮，一个没有边距的按钮。让我们试试这个:\n\n1.  首先，让我们定义如下纯虚拟基类:\n\n```cpp\nclass base\n{\npublic:\n    virtual int width() = 0;\n};\n```\n\n如上图所示，纯虚拟基类定义了`width`函数。\n\n2.  然后，我们可以如下实现我们的按钮:\n\n```cpp\nclass button : public base\n{\npublic:\n    int width() override\n    {\n        return 10;\n    }\n};\n```\n\n如上图所示，按钮继承基类并返回宽度`10`。利用前面的，我们可以开始`button`永远是`10`的宽度，按钮没有边距的概念。\n\n3.  要给按钮添加边距，我们首先必须创建一个装饰器类，如下所示:\n\n```cpp\nclass decorator : public base\n{\n    std::unique_ptr<base> m_base;\n\npublic:\n    decorator(std::unique_ptr<base> b) :\n        m_base{std::move(b)}\n    { }\n\n    int width()\n    {\n        return m_base->width();\n    }\n};\n```\n\n装饰器模式从指向`base`指针的私有成员开始，该指针在装饰器的构造函数中设置。装饰器还定义了`width`函数，但是将调用转发给基类。\n\n4.  现在，我们可以创建一个 margin 类，它是一个装饰器，如下所示:\n\n```cpp\nclass margin : public decorator\n{\npublic:\n    margin(std::unique_ptr<base> b) :\n        decorator{std::move(b)}\n    { }\n\n    int width()\n    {\n        return decorator::width() + 32;\n    }\n};\n```\n\n如上图所示，margin 类返回添加了附加的`32`的对象的宽度。\n\n5.  然后，我们可以如下创建两个按钮:\n\n```cpp\nint main()\n{\n    auto button1 = std::make_unique<button>();\n    auto button2 = std::make_unique<margin>(std::make_unique<button>());\n\n    std::cout << \"button1 width: \" << button1->width() << '\\n';\n    std::cout << \"button2 width: \" << button2->width() << '\\n';\n}\n```\n\n这将产生以下输出:\n\n![](img/c40ae265-8e79-4f31-8cea-5a48a0d65d00.png)\n\n装饰器模式的最大优势是它允许我们在运行时扩展一个类。例如，如果需要，我们可以创建一个带有两个边距的按钮:\n\n```cpp\nint main()\n{\n    auto b =\n        std::make_unique<margin>(\n            std::make_unique<margin>(\n                std::make_unique<button>()\n            )\n        );\n\n    std::cout << \"button width: \" << b->width() << '\\n';\n}\n```\n\n否则，我们可以创建另一个装饰器。为了演示这一点，让我们如下扩展我们的基类:\n\n```cpp\nclass base\n{\npublic:\n    virtual int width() = 0;\n    virtual int content_width() = 0;\n};\n```\n\n前面的基类现在定义了一个宽度和一个内容宽度(按钮内部我们可以实际使用的空间量)。现在，我们可以如下创建按钮:\n\n```cpp\nclass button : public base\n{\npublic:\n    int width() override\n    {\n        return 10;\n    }\n\n    int content_width() override\n    {\n        return width() - 1;\n    }\n};\n```\n\n如上图所示，我们的按钮有一个静态宽度，内容宽度等于宽度本身减去 1(为按钮的边框留出空间)。然后我们定义我们的装饰器如下:\n\n```cpp\nclass decorator : public base\n{\n    std::unique_ptr<base> m_base;\n\npublic:\n    decorator(std::unique_ptr<base> b) :\n        m_base{std::move(b)}\n    { }\n\n    int width() override\n    {\n        return m_base->width();\n    }\n\n    int content_width() override\n    {\n        return m_base->content_width();\n    }\n};\n```\n\n如前所示，唯一的区别是装饰器现在必须转发宽度和内容宽度函数。我们的利润装饰器如下所示:\n\n```cpp\nclass margin : public decorator\n{\npublic:\n    margin(std::unique_ptr<base> b) :\n        decorator{std::move(b)}\n    { }\n\n    int width() override\n    {\n        return decorator::width() + 32;\n    }\n\n    int content_width() override\n    {\n        return decorator::content_width();\n    }\n};\n```\n\n与 web 编程的情况一样，边距会增加对象的大小。它不会改变对象内部内容的空间，因此，边距返回内容宽度，不做任何修改。通过前面的更改，我们现在可以添加一个填充装饰器，如下所示:\n\n```cpp\nclass padding : public decorator\n{\npublic:\n    padding(std::unique_ptr<base> b) :\n        decorator{std::move(b)}\n    { }\n\n    int width() override\n    {\n        return decorator::width();\n    }\n\n    int content_width() override\n    {\n        return decorator::content_width() - 5;\n    }\n};\n```\n\n填充装饰器与边距装饰器相反。它不会改变对象的大小，它会减少给予对象内部内容的空间总量。因此，它不会改变宽度，但会减小内容的大小。\n\n要使用我们的新装饰器创建按钮，我们可以使用以下内容:\n\n```cpp\nint main()\n{\n    auto b =\n        std::make_unique<margin>(\n            std::make_unique<padding>(\n                std::make_unique<button>()\n            )\n        );\n\n    std::cout << \"button width: \" << b->width() << '\\n';\n    std::cout << \"button content width: \" << b->content_width() << '\\n';\n}\n```\n\n如前所示，我们创建了一个添加了边距和填充的按钮，这将产生以下输出:\n\n![](img/2d871265-d736-4585-83b8-30b74a6b04e9.png)\n\n装饰器模式提供了创建不同按钮的能力，而不需要编译时继承，这将要求我们对我们能想到的每一种可能的按钮类型都有不同的按钮定义。然而，应该注意的是，装饰模式是以增加分配和函数调用的重定向为代价的，所以这种运行时灵活性是有代价的。\n\n# 添加与观察者模式的通信\n\n在这个食谱中，我们将学习如何实现观察者模式。观察者模式为一个类提供了向另一个类注册的能力，以便在事件发生时接收通知。Qt 语言通过使用它的 singles 和 slots 机制来提供这个特性，同时需要 MOC 编译器来使它工作。这个方法很重要，因为我们将学习如何使用标准的 C++ 在不需要 Qt 的情况下实现观察者模式。\n\n# 准备好\n\n开始之前，请确保满足所有技术要求，包括安装 Ubuntu 18.04 或更高版本，并在终端窗口中运行以下内容:\n\n```cpp\n> sudo apt-get install build-essential git cmake\n```\n\n这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后，打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。\n\n# 怎么做...\n\n执行以下步骤来尝试此食谱:\n\n1.  从新的终端，运行以下命令下载源代码:\n\n```cpp\n> cd ~/\n> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git\n> cd Advanced-CPP-CookBook/chapter11\n```\n\n2.  要编译源代码，请运行以下命令:\n\n```cpp\n> cmake .\n> make recipe04_examples\n```\n\n3.  编译源代码后，您可以通过运行以下命令来执行该配方中的每个示例:\n\n```cpp\n> ./recipe04_example01 \nmom's phone received alarm notification\ndad's phone received alarm notification\n```\n\n在下一节中，我们将逐一介绍这些示例，并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。\n\n# 它是如何工作的...\n\n观察者模式为观察者提供了在事件发生时得到通知的能力。为了解释这是如何工作的，让我们从下面的纯虚拟基类开始:\n\n```cpp\nclass observer\n{\npublic:\n    virtual void trigger() = 0;\n};\n```\n\n如上图所示，我们已经定义了`observer`，它必须实现一个`trigger()`函数。然后，我们可以创建这个纯虚拟基类的两个不同版本，如下所示:\n\n```cpp\nclass moms_phone : public observer\n{\npublic:\n    void trigger() override\n    {\n        std::cout << \"mom's phone received alarm notification\\n\";\n    }\n};\n\nclass dads_phone : public observer\n{\npublic:\n    void trigger() override\n    {\n        std::cout << \"dad's phone received alarm notification\\n\";\n    }\n};\n```\n\n如前面的代码所示，我们已经创建了两个不同的类，这两个类都子类化了观察者纯虚拟类，覆盖了触发器函数。然后，我们可以实现一个类，该类生成观察者可能感兴趣的事件，如下所示:\n\n```cpp\nclass alarm\n{\n    std::vector<observer *> m_observers;\n\npublic:\n    void trigger()\n    {\n        for (const auto &o : m_observers) {\n            o->trigger();\n        }\n    }\n\n    void add_phone(observer *o)\n    {\n        m_observers.push_back(o);\n    }\n};\n```\n\n如前面的代码所示，我们从`std::vector`开始，它存储任意数量的观察者。然后我们提供一个触发函数，它代表我们的事件。当这个函数被执行时，我们循环通过所有的观察者，并通过调用他们的`trigger()`函数通知他们这个事件。最后，我们提供了一个函数，允许观察者订阅所讨论的事件。\n\n下面演示了如何使用这些类:\n\n```cpp\nint main(void)\n{\n    alarm a;\n    moms_phone mp;\n    dads_phone dp;\n\n    a.add_phone(&mp);\n    a.add_phone(&dp);\n\n    a.trigger();\n}\n```\n\n输出如下:\n\n![](img/381ac2b5-23b3-46d9-9c09-6eda2174b3b4.png)\n\n如前所示，当警报类被触发时，观察者会收到事件通知，并根据需要处理通知。\n\n# 利用静态多态性提高性能\n\n在这个食谱中，我们将学习如何在不需要虚拟继承的情况下创建多态性。相反，我们将使用编译时继承(称为静态多态性)。这个方法很重要，因为静态多态性不会导致与运行时、虚拟继承(因为不需要 vTable)相同的性能和内存使用损失，代价是可读性和无法利用虚拟子类化的运行时优势。\n\n# 准备好\n\n开始之前，请确保满足所有技术要求，包括安装 Ubuntu 18.04 或更高版本，并在终端窗口中运行以下内容:\n\n```cpp\n> sudo apt-get install build-essential git cmake\n```\n\n这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后，打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。\n\n# 怎么做...\n\n执行以下步骤来尝试配方:\n\n1.  从新的终端，运行以下命令下载源代码:\n\n```cpp\n> cd ~/\n> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git\n> cd Advanced-CPP-CookBook/chapter11\n```\n\n2.  要编译源代码，请运行以下命令:\n\n```cpp\n> cmake .\n> make recipe05_examples\n```\n\n3.  编译源代码后，您可以通过运行以下命令来执行该配方中的每个示例:\n\n```cpp\n> ./recipe05_example01\nsubclass1 specific\ncommon\nsubclass2 specific\ncommon\n> ./recipe05_example02\nsubclass1 specific\ncommon\nsubclass2 specific\ncommon\n```\n\n在下一节中，我们将逐一介绍这些示例，并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。\n\n# 它是如何工作的...\n\n多态性的主要目标之一是提供覆盖对象如何执行特定功能的能力，同时提供跨一组对象提供公共逻辑的能力。虚拟继承的问题是，如果您希望使用基类作为接口，重写的能力需要使用虚拟表(即虚拟表，这是处理虚拟继承所需的额外内存块)。\n\n例如，考虑以下代码:\n\n```cpp\nclass base\n{\npublic:\n    virtual void foo() = 0;\n\n    void common()\n    {\n        std::cout << \"common\\n\";\n    }\n};\n```\n\n让我们从之前定义的基类开始。它提供了一个纯粹的`foo()`函数(也就是说，子类必须实现这个函数)，同时还提供了自己的公共逻辑。然后，我们可以创建如下两个子类:\n\n```cpp\nclass subclass1 : public base\n{\npublic:\n    void foo() override\n    {\n        std::cout << \"subclass1 specific\\n\";\n    }\n};\n\nclass subclass2 : public base\n{\npublic:\n    void foo() override\n    {\n        std::cout << \"subclass2 specific\\n\";\n    }\n};\n```\n\n如前所示，我们将基类子类化，并用特定于子类的功能覆盖`foo()`函数。然后，我们可以从基类调用子类特定的`foo()`函数，如下所示:\n\n```cpp\nint main(void)\n{\n    subclass1 s1;\n    subclass2 s2;\n\n    base *b1 = &s1;\n    base *b2 = &s2;\n\n    b1->foo();\n    b1->common();\n\n    b2->foo();\n    b2->common();\n}\n```\n\n这将产生以下输出:\n\n![](img/523debdb-a99c-47b7-9ea3-d8aa453ca274.png)\n\n这种类型的运行时多态性需要使用 vTable，这不仅会增加每个对象的内存占用，还会导致性能损失，因为每个函数调用都需要 vTable 查找。如果不需要虚拟继承的运行时属性，静态多态性可以提供相同的功能，而不会带来损失。\n\n首先，让我们如下定义基类:\n\n```cpp\ntemplate<typename T>\nclass base\n{\npublic:\n    void foo()\n    { static_cast<T *>(this)->foo(); }\n\n    void common()\n    {\n        std::cout << \"common\\n\";\n    }\n};\n```\n\n像我们前面的例子一样，基类没有实现`foo()`函数，而是需要一个子类来实现这个函数(这使得静态转换可以将其转换为类型`T`)。\n\n然后，我们可以如下实现子类:\n\n```cpp\nclass subclass1 : public base<subclass1>\n{\npublic:\n    void foo()\n    {\n        std::cout << \"subclass1 specific\\n\";\n    }\n};\n\nclass subclass2 : public base<subclass2>\n{\npublic:\n    void foo()\n    {\n        std::cout << \"subclass2 specific\\n\";\n    }\n};\n```\n\n和前面的例子一样，子类只是实现了`foo()`函数。在这种情况下，不同之处在于继承需要使用模板参数，这消除了`foo()`函数重写的需要，因为基类从不使用虚函数。\n\n前面的静态多态性允许我们从基类执行`foo()`函数，如下所示:\n\n```cpp\ntemplate<typename T>\nvoid test(base<T> b)\n{\n    b.foo();\n    b.common();\n}\n```\n\n如上图所示，`test()`函数没有任何关于每个子类的信息。它只有关于基类(或接口)的信息。该`test()`功能可以如下执行:\n\n```cpp\nint main(void)\n{\n    subclass1 c1;\n    subclass2 c2;\n\n    test(c1);\n    test(c2);\n}\n```\n\n这再次导致相同的输出:\n\n![](img/18a35eb7-3016-43dc-9871-77f0e57eb78f.png)\n\n如上图所示，如果多态类型在编译时是已知的，静态多态可以用来消除对`virtual`的需求，从而消除对 vTable 的需求。这种类型的逻辑在使用模板类时特别有用，在模板类中，基类型是已知的，而子类类型是未知的(并且是提供的)，这使得模板函数只需要基接口。"
  },
  {
    "path": "docs/adv-cpp-prog-cb/12.md",
    "content": "# 十二、更仔细查看类型推导\n\n在本章中，您将学习 C++ 中类型推导的所有来龙去脉，包括 C++ 17 中的一些新增内容。这一章很重要，因为它将教会你所有的方法，编译器将试图为你自动推导类型信息。如果没有对 C++ 中类型推导的工作原理有一个坚定的理解，就有可能创建出不像预期那样工作的代码，尤其是在使用`auto`和模板编程的时候。从本章获得的知识将为您提供在自己的应用中适当利用类型推导的技能。\n\n本章中的配方如下:\n\n*   使用自动和类型推导\n*   学习`decltype`类型推导规则如何工作\n*   使用模板函数类型推导\n*   在 C++ 17 中利用模板类类型推导\n*   在 C++ 17 中使用用户定义的类型推导\n\n# 技术要求\n\n要编译和运行本章中的示例，您必须拥有运行 Ubuntu 18.04 的计算机的管理权限，并且具有功能性互联网连接。在运行这些示例之前，您必须安装以下内容:\n\n```cpp\n> sudo apt-get install build-essential git cmake \n```\n\n如果这安装在 Ubuntu 18.04 以外的任何操作系统上，则需要 GCC 7.4 或更高版本以及 CMake 3.6 或更高版本。\n\n本章的代码文件可以在[https://github . com/packt publishing/Advanced-CPP-cook book/tree/master/chapter 12](https://github.com/PacktPublishing/Advanced-CPP-CookBook/tree/master/chapter12)找到。\n\n# 使用自动和类型推导\n\n在这个食谱中，我们将学习编译器如何处理`auto`关键字，特别是类型推导。这个方法很重要，因为如何处理`auto`不是直观的，如果不清楚`auto`是如何工作的，你的代码很可能包含错误和性能问题。本食谱中包含的主题有`auto`的一般描述、类型推断、转发(或通用)参考、l 值和 r 值。\n\n# 准备好\n\n开始之前，请确保满足所有技术要求，包括安装 Ubuntu 18.04 或更高版本，并在终端窗口中运行以下内容:\n\n```cpp\n> sudo apt-get install build-essential git cmake\n```\n\n这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后，打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。\n\n# 怎么做...\n\n执行以下步骤来尝试配方:\n\n1.  从新的终端，运行以下命令下载源代码:\n\n```cpp\n> cd ~/\n> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git\n> cd Advanced-CPP-CookBook/chapter12\n```\n\n2.  要编译源代码，请运行以下命令:\n\n```cpp\n> cmake .\n> make recipe01_examples\n```\n\n3.  编译源代码后，您可以通过运行以下命令来执行该配方中的每个示例:\n\n```cpp\n> ./recipe01_example01\ni1 = int\ni2 = int\ni3 = std::initializer_list<int>\ni4 = std::initializer_list<int>\nc = char\nr = int\n\n> ./recipe01_example02\ni1 = int\ni2 = const int\ni3 = volatile int\ni4 = const volatile int\n\n> ./recipe01_example03\ni1 = int\ni2 = int&\na1 = int\na2 = int\na3 = int\na4 = int&\ni3 = int&&\na5 = int&\na6 = int&\na7 = int&\na8 = int&\na9 = int&&\na10 = int&&\n\n> ./recipe01_example04\ni1 = int\ni2 = const int&\ni3 = const int&&\n```\n\n在下一节中，我们将逐一介绍这些示例，并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。\n\n# 它是如何工作的...\n\n`auto`关键字是添加到 C++ 11 中的一个特性，称为**占位符类型说明符**。换句话说，`auto`关键字用来告诉编译器一个变量的类型将从它的初始化式中推导出来。与其他使用占位符类型的语言不同，`auto`关键字必须仍然遵循 C++ 严格的类型系统，这意味着`auto`不应与`std::any`混淆。\n\n例如`std::any`可能出现以下情况:\n\n```cpp\nstd::any i = 42;\ni = \"The answer is: 42\";\n```\n\n`auto`不允许出现以下情况:\n\n```cpp\nauto i = 42;\ni = \"The answer is: 42\";\n```\n\n在第一个例子中，我们定义`std::any`，它存储一个整数。然后我们用 C 型字符串替换`std::any`中的整数。关于`auto`，这是不可能的，因为一旦编译器在初始化时推导出变量的类型，该类型就不能改变(与 C++ 中的任何其他变量没有区别)。\n\n让我们看一个如何使用`auto`初始化变量的简单例子:\n\n```cpp\nint main(void)\n{\n    auto i1 = 42;\n    auto i2{42};\n    auto i3 = {42};\n    auto i4 = {4, 8, 15, 16, 23, 42};\n\n    show_type(i1);\n    show_type(i2);\n    show_type(i3);\n    show_type(i4);\n\n    char c = 0;\n    auto r = c + 42;\n\n    show_type(c);\n    show_type(r);\n}\n```\n\n运行此示例会产生以下输出:\n\n![](img/2a971f5a-11f4-4d46-a47e-a62cfab04dbe.png)\n\n如前面的代码所示，我们使用`auto`创建四个变量，初始化它们，然后使用名为`show_type()`的函数返回变量类型的输出。\n\nFor more information about how the `show_type()` function works, please see the code that comes with this chapter (the details of this function will make more sense after you finish reading this entire chapter).\n\n我们示例中的第一个变量`i1`被推导为整数。这是因为 C++ 中的数值类型总是被推导为整数，在我们的例子中我们也看到了`c`和`r`变量。原因是在编译过程中允许编译器增加任何变量的大小，也就是说，当编译器看到`c + 42`时，它做的第一件事就是在完成加法之前将`c`的值存储在一个临时整数中。\n\n在我们的例子中，第二个变量`i2`也被推导为整数，因为`{}`符号是 C++ 中任何类型的另一种初始化形式，带有一些额外的规则。具体来说，`i3`和`i4`被推导为整数的`std::initializer_list`，因为最后两个使用了`= {}`符号，这是由 C++ 规范定义的，在 C++ 17 中总是推导为`std::initializer_list`。需要注意的是，这是假设编译器遵循规范，在这个特定的例子中并不总是这样，这就是为什么像 AUTOSAR 这样的关键系统规范不允许这种类型的初始化。\n\n`auto`关键字也可以和 CV 限定词(即`const` / `volatile`)组合。看看这个例子:\n\n```cpp\nint main(void)\n{\n    auto i1 = 42;\n    const auto i2 = 42;\n    volatile auto i3 = 42;\n    const volatile auto i4 = 42;\n\n    show_type(i1);\n    show_type(i2);\n    show_type(i3);\n    show_type(i4);\n}\n```\n\n前面的示例导致以下输出:\n\n![](img/a4a02c94-dc87-4f79-b3bf-e2e8118cf556.png)\n\n如前面的截图所示，每个变量都用定义好的 CV 限定符修饰。\n\n到目前为止，在每个例子中，我们都可以简单地用`int`代替`auto`的使用，没有什么会改变，这就引出了一个问题，为什么首先要使用`auto`？原因有几个:\n\n*   使用除`auto`以外的东西意味着您的代码可能会指定一个变量的类型两次。例如，`int *ptr = new int;`声明`ptr`变量是整数两次:一次在变量声明中，第二次在变量初始化中。\n*   C++ 中有些类型真的很长(例如迭代器)，使用`auto`可以大大简化代码的冗长程度，例如`auto i = v.begin()`。\n*   编写模板代码时，要求`auto`正确处理引用类型，如转发引用。\n\n使用参考文献是`auto`的使用变得混乱的地方，也是大多数人犯错的地方。为了更好地解释，让我们看看下面的例子:\n\n```cpp\nint main(void)\n{\n    int i = 42;\n\n    int i1 = i;\n    int &i2 = i;\n\n    show_type(i1);\n    show_type(i2);\n\n    auto a1 = i1;\n    auto a2 = i2;\n\n    show_type(a1);\n    show_type(a2);\n}\n```\n\n这将产生以下输出:\n\n```cpp\ni1 = int\ni2 = int&\na1 = int\na2 = int\n```\n\n如前例所示，我们创建一个整数`i`，并将其设置为`42`。然后我们再创建两个整数:一个是`i`的副本，而第二个是`i`的引用。如输出所示，我们得到期望的类型，`int`和`int&`。使用`auto`关键字，我们可以预期，如果我们说类似`auto a = i2`的话，我们会得到一个`int&`类型，因为`i2`是对一个整数的引用，并且由于`auto`是根据它是如何初始化的来推导它的类型的，所以我们应该得到`int&`。问题是，我们没有。相反，我们得到`int`。\n\n这样做的原因是`auto`根据它是如何初始化的来获取它的类型，而不包括引用类型。换句话说，示例中`auto`的使用只是拾取`i2`类型，而不关注`i2`是否为整数或对整数的引用。要强制`auto`成为整数的引用，我们必须使用以下语法:\n\n```cpp\nauto a3 = i1;\nauto &a4 = i2;\n\nshow_type(a3);\nshow_type(a4);\n```\n\n这将产生以下输出:\n\n```cpp\na3 = int\na4 = int&\n```\n\n这个输出和预期的一样。同样的规则也适用于 r 值引用，但变得更加复杂。例如，考虑以下代码:\n\n```cpp\nint &&i3 = std::move(i);\nshow_type(i3);\n```\n\n这将产生以下输出:\n\n```cpp\ni3 = int&&\n```\n\n这个输出还是和预期的一样。根据我们已经了解到的情况，我们预计需要以下内容来获得 r 值参考:\n\n```cpp\nauto &&a5 = i3;\nshow_type(a6);\n```\n\n问题是这会导致以下输出:\n\n```cpp\na5 = int&\n```\n\n如前面的例子所示，我们没有得到预期的 r 值引用。任何在 C++ 中被标记为`auto &&`的东西都被认为是转发引用(这也被称为通用引用，一个由斯科特·迈耶斯创造的术语)。根据通用参考的初始化方式，通用参考将推导为 l 值或 r 值参考。\n\n例如，考虑以下代码:\n\n```cpp\nauto &&a6 = i1;\nshow_type(a6);\n```\n\n此代码导致以下结果:\n\n```cpp\na6 = int&\n```\n\n这是因为`i1`之前被定义为一个整数，所以`a6`变成了对`i1`的 l 值引用。以下也是事实:\n\n```cpp\nauto &&a7 = i2;\nshow_type(a7);\n```\n\n前面的代码导致以下结果:\n\n```cpp\na7 = int&\n```\n\n这是因为`i2`之前被定义为对整数的 l 值引用，这意味着通用引用也变成了对整数的 l 值引用。\n\n令人困惑的结果如下，如前面的代码片段所示:\n\n```cpp\nauto &&a8 = i3;\nshow_type(a8);\n```\n\n这再次导致以下结果:\n\n```cpp\na8 = int&\n```\n\n这里，`i3`早先被定义为对整数的 r 值引用(由结果输出证明)，但是通用引用没有从`i3`转发 r 值。这是因为，虽然`i3`被定义为 r 值参考，但一旦被使用，它就变成了 l 值参考。正如斯科特·迈耶过去所说，如果一个变量有名字(在我们的例子中是`i3`)，它就是一个 l 值，即使它是从 r 值开始的。另一种看待这个问题的方式是，一旦使用了一个变量(就像以任何方式访问一样)，这个变量就是一个 l 值。因此，前面的代码实际上正常工作。`i3`虽然被定义为 r 值，但它是 l 值，因此通用参考成为整数的 l 值参考，就像`i1`和`i2`一样。\n\n要使用`auto`获得 r 值参考，您必须做与不使用`auto`时相同的事情:\n\n```cpp\nauto &&a9 = std::move(i3);\nshow_type(a9);\n```\n\n这将导致以下结果:\n\n```cpp\na9 = int&&\n```\n\n如前面的代码片段所示，思考`auto`的最佳方式是简单地将单词`auto`替换为实际类型(在本例中为`int`，适用于实际类型的任何规则也适用于`auto`。不同的是，如果你试图写`int &&blah = i`，你会得到一个错误，因为编译器会识别出你试图从 l 值引用创建 r 值引用，这是不可能的(因为你只能从另一个 r 值引用创建 r 值引用)。\n\n前面例子如此重要的原因是`auto`不会产生编译器的抱怨。相反，当你打算创造一个 r 值时，它会产生一个 l 值，这可能导致效率低下或错误。了解`auto`的用法最重要的是，如果有名字，就是 l 值；否则，它是一个 r 值。\n\n例如，考虑以下代码:\n\n```cpp\nauto &&a10 = 42;\nshow_type(a10);\n```\n\n此代码导致以下结果:\n\n```cpp\na10 = int&&\n```\n\n由于数值`42`没有变量名，所以它是一个常数，因此，通用引用成为对整数的 r 值引用。\n\n还需要注意的是，`auto`的使用确实会在处理引用时混淆地继承 CV 限定符。看看这个例子:\n\n```cpp\nint main(void)\n{\n    const int i = 42;\n\n    auto i1 = i;\n    auto &i2 = i;\n    auto &&i3 = std::move(i);\n\n    show_type(i1);\n    show_type(i2);\n    show_type(i3);\n}\n```\n\n这将导致以下结果:\n\n![](img/638a9ffc-4c76-48bd-9656-a209fb3c43b6.png)\n\n如前面的截图所示，第一个整数仍然是`int`类型，因为`const int`的副本是`int`。然而`i2`和`i3`都成为`const int`的参考。如果我们将`auto`替换为`int`，我们会得到一个编译器错误，因为您不能创建对`const int`的非`const`引用，但是使用`auto`会很乐意为您将您的非`const`变量转换为`const`变量。这样做的问题是，当你试图修改你的变量时，你会得到奇怪的错误消息，抱怨变量是只读的，而事实上，你没有明确地将变量定义为`const`。一般来说，如果您期望`const`，最好总是将一个用`auto`定义的变量标记为`const`，否则标记为非`const`，以防止这些有时难以识别的错误。\n\n# 了解 decltype 类型扣减规则如何工作\n\n在本食谱中，我们将学习类型推导如何与`decltype()`和`decltype(auto)`配合使用，以及如何使用`decltype(auto)`来避免`auto`的参照性问题。\n\n这个配方很重要，因为`auto`在处理`decltype()`处理的引用时有一些奇怪的行为，这为 C++ 提供了一种更可预测地处理类型推断的方法，尤其是在使用 C++ 模板时。\n\n# 准备好\n\n开始之前，请确保满足所有技术要求，包括安装 Ubuntu 18.04 或更高版本，并在终端窗口中运行以下内容:\n\n```cpp\n> sudo apt-get install build-essential git cmake\n```\n\n这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后，打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。\n\n# 怎么做...\n\n执行以下步骤来尝试此食谱:\n\n1.  从新的终端，运行以下命令下载源代码:\n\n```cpp\n> cd ~/\n> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git\n> cd Advanced-CPP-CookBook/chapter12\n```\n\n2.  要编译源代码，请运行以下命令:\n\n```cpp\n> cmake .\n> make recipe02_examples\n```\n\n3.  编译源代码后，您可以通过运行以下命令来执行该配方中的每个示例:\n\n```cpp\n> ./recipe02_example01\ni = int\n\n> ./recipe02_example02\ni = short int\n\n> ./recipe02_example03\ni = short int\n\n> ./recipe02_example04\ni1 = int\ni2 = int\n\n> ./recipe02_example05\ni1 = int\ni2 = const int\ni3 = volatile int\ni4 = const volatile int\n\n> ./recipe02_example06\ni1 = int\ni2 = int&\ni3 = int&&\na1 = int\na2 = int\na3 = int\na4 = int\na5 = int&\na6 = int&&\nd1 = int\nd2 = int&\nd3 = int&&\n```\n\n在下一节中，我们将逐一介绍这些示例，并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。\n\n# 它是如何工作的...\n\nC++ 中的`auto`和`typename`都没有提供获取变量类型和使用该信息创建新类型的能力。为了更好地解释为什么您可能想要这样做，让我们看下面的例子:\n\n```cpp\ntemplate<typename FUNC>\nauto question(FUNC &&func)\n{\n    auto x = func() + 10;\n    return x;\n}\n```\n\n我们从一个函数开始我们的例子，这个函数将任何函数作为输入，并返回这个函数加上`10`的结果。然后，我们可以如下执行该函数:\n\n```cpp\nshort the_answer()\n{\n    return 32;\n}\n\nint main(void)\n{\n    auto i = question(the_answer);\n    show_type(i);\n}\n```\n\n如前例所示，我们向`question()`函数传递一个指向另一个返回`short`的函数的指针。在执行该函数时，我们存储结果，然后使用名为`show_type()`的函数，该函数被设计为输出所提供的类型是什么类型。这将导致以下结果:\n\n![](img/e8f08892-9ea4-4d72-9d86-197819b5e39f.png)\n\n这个例子的问题在于，返回的类型与我们得到的类型不同。C++ 允许根据需要增加任何变量的大小，并且通常在使用 short 时会这样做，尤其是当您试图用数值对 short 执行算术运算时，因为数值是以整数表示的。\n\n由于我们不知道所提供的函数在`question()`函数中的返回类型是什么，所以没有办法解决这个问题。进入`decltype()`。为了解释，让我们更新我们的示例来解决前面的问题:\n\n```cpp\ntemplate<typename FUNC>\nauto question(FUNC &&func)\n{\n    decltype(func()) x = func() + 10;\n    return x;\n}\n```\n\n如前例所示，我们将`auto`替换为`decltype(func())`。这告诉编译器获取`func()`的返回类型，并使用该类型定义`x`。因此，编译器将该模板转换为以下函数:\n\n```cpp\nshort question(short(*func)())\n{\n    short x = func() + 10;\n    return x;\n}\n```\n\n发生这种情况，而不是最初预期的以下情况:\n\n```cpp\nint question(short(*func)())\n{\n    int x = func() + 10;\n    return x;\n}\n```\n\n执行时会产生以下输出:\n\n![](img/74f2a492-4468-4399-82cf-1bcb78645046.png)\n\n如前一张截图所示，我们现在从`question()`函数中返回了正确的类型。使用 C++ 14，我们可以将这个例子更进一步，这样写:\n\n```cpp\ntemplate<typename FUNC>\nconstexpr auto question(FUNC &&func) -> decltype(func())\n{\n    return func() + 10;\n}\n```\n\n在前面代码片段的示例中，我们将`question()`函数转换为`constexpr`，这允许编译器优化函数调用，用`func() + 10`语句替换对`question()`的调用。通过使用`-> decltype()`函数返回语法明确告诉编译器我们希望函数返回什么类型，我们也消除了对基于堆栈的变量的需求。应该注意的是，这种语法是必需的，因为下面的语法不会编译:\n\n```cpp\ntemplate<typename FUNC>\nconstexpr decltype(func()) question(FUNC &&func)\n{\n    return func() + 10;\n}\n```\n\n前面的代码不会编译，因为编译器还没有`func()`的定义，因此不知道它的类型是什么。`->`语法通过将返回类型放在函数定义的末尾而不是前面来解决这个问题。\n\n`decltype()`说明符也可以用来代替`auto`，如下所示:\n\n```cpp\nint main(void)\n{\n    decltype(auto) i1 = 42;\n    decltype(auto) i2{42};\n\n    show_type(i1);\n    show_type(i2);\n}\n```\n\n这将产生以下输出:\n\n![](img/fb6593f6-0f30-46a3-9beb-ca5ecc548089.png)\n\n在这个例子中，我们使用`decltype(auto)`创建两个整数，并将它们初始化为`42`。在这种特殊情况下，`decltype(auto)`和`auto`的操作完全相同。两者都将占位符类型定义为整数，因为两者都使用数值进行初始化，默认情况下，该数值为`int`。\n\n和`auto`一样，可以用 CV 限定词(即`const` / `volatile`)修饰`decltype(auto)`如下:\n\n```cpp\nint main(void)\n{\n    decltype(auto) i1 = 42;\n    const decltype(auto) i2 = 42;\n    volatile decltype(auto) i3 = 42;\n    const volatile decltype(auto) i4 = 42;\n\n    show_type(i1);\n    show_type(i2);\n    show_type(i3);\n    show_type(i4);\n}\n```\n\n这将产生以下输出:\n\n![](img/ded88455-57d8-42c0-bf42-bf31d47ed56a.png)\n\n`decltype(auto)`的真正魔力在于它如何处理引用。为了演示这一点，让我们从下面的例子开始:\n\n```cpp\nint main(void)\n{\n    int i = 42;\n\n    int i1 = i;\n    int &i2 = i;\n    int &&i3 = std::move(i);\n\n    show_type(i1);\n    show_type(i2);\n    show_type(i3);\n}\n```\n\n执行时，我们会看到以下输出:\n\n```cpp\ni1 = int\ni2 = int&\ni3 = int&&\n```\n\n如前面的例子所示，我们已经创建了一个整数、一个对整数的 l 值引用和一个对整数的 r 值引用。让我们看看如果我们尝试使用`auto`而不是`int`会发生什么，如下所示:\n\n```cpp\nauto a1 = i1;\nauto a2 = i2;\nauto a3 = std::move(i3);\n\nshow_type(a1);\nshow_type(a2);\nshow_type(a3);\n```\n\n然后，我们会看到以下输出:\n\n```cpp\na1 = int\na2 = int\na3 = int\n```\n\n如前面的例子所示，我们只得到整数。所有引用都已删除。用`auto`获取引用的唯一方法是我们明确定义它们如下:\n\n```cpp\nauto a4 = i1;\nauto &a5 = i2;\nauto &&a6 = std::move(i3);\n\nshow_type(a4);\nshow_type(a5);\nshow_type(a6);\n```\n\n这将产生以下预期输出:\n\n```cpp\na4 = int\na5 = int&\na6 = int&&\n```\n\n必须添加额外的`&`运算符来显式定义引用类型的问题是，这假设在我们的模板代码中，我们实际上知道引用应该是什么。如果这些信息不可用，我们将无法编写模板函数，也无法知道是否可以创建 l 值或 r 值引用，很可能会产生一个副本。\n\n为了克服这一点，`decltype(auto)`不仅在初始化时继承了类型和 CV 限定符，还如下继承了引用:\n\n```cpp\ndecltype(auto) d1 = i1;\ndecltype(auto) d2 = i2;\ndecltype(auto) d3 = std::move(i3);\n\nshow_type(d1);\nshow_type(d2);\nshow_type(d3);\n```\n\n前面的代码在执行时会产生以下结果:\n\n```cpp\nd1 = int\nd2 = int&\nd3 = int&&\n```\n\n如前例所示，`decltype(auto)`可用于继承其正在初始化的值的所有类型信息，包括引用性。\n\n# 使用模板函数类型推导\n\n在这个食谱中，我们将学习模板函数类型推导是如何工作的。具体来说，这个食谱将教你模板函数类型推导如何与`auto`类型推导一样工作，以及函数类型推导如何与一些奇数类型(例如 C 风格数组)一起使用。\n\n这个方法很重要，因为它将教你如何正确地编写函数模板，消除了调用函数模板时显式定义类型信息的需要。\n\n# 准备好\n\n开始之前，请确保满足所有技术要求，包括安装 Ubuntu 18.04 或更高版本，并在终端窗口中运行以下内容:\n\n```cpp\n> sudo apt-get install build-essential git cmake\n```\n\n这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后，打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。\n\n# 怎么做...\n\n执行以下步骤来尝试此食谱:\n\n1.  从新的终端，运行以下命令下载源代码:\n\n```cpp\n> cd ~/\n> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git\n> cd Advanced-CPP-CookBook/chapter12\n```\n\n2.  要编译源代码，请运行以下命令:\n\n```cpp\n> cmake .\n> make recipe03_examples\n```\n\n3.  编译源代码后，您可以通过运行以下命令来执行该配方中的每个示例:\n\n```cpp\n> ./recipe03_example01 \nt = int\nt = int\n\n> ./recipe03_example02\nt = const int&\n\n> ./recipe03_example03\nt = int&\n\n> ./recipe03_example04\nt = int&\n\n> ./recipe03_example05\nt = int&&\n\n> ./recipe03_example06\nt = int&&\n\n> ./recipe03_example07\nt = const int&\n\n> ./recipe03_example08\nt = const int&&\n\n> ./recipe03_example09\nt = int (&&)[6]\n```\n\n在下一节中，我们将逐一介绍这些示例，并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。\n\n# 它是如何工作的...\n\n在 C++ 11 中，标准委员会增加了根据传递给函数的参数自动推断模板函数类型信息的能力。\n\n看看这个例子:\n\n```cpp\ntemplate<typename T>\nvoid foo(T t)\n{\n    show_type(t);\n}\n```\n\n前面的函数创建了一个标准的模板函数，该函数执行一个名为`show_type()`的函数，该函数旨在输出提供给它的类型信息。\n\n在 C++ 11 之前，我们将如下使用这个函数:\n\n```cpp\nint main(void)\n{\n    int i = 42;\n\n    foo<int>(i);\n    foo<int>(42);\n}\n```\n\n编译器已经知道模板应该将`T`类型定义为整数，因为这就是函数的用途。C++ 11 消除了这种冗余，允许以下情况:\n\n```cpp\nint main(void)\n{\n    int i = 42;\n\n    foo(i);\n    foo(42);\n}\n```\n\n执行时会产生以下输出:\n\n![](img/acf27d37-b223-47d6-8cca-91327b06bcef.png)\n\n然而，像`auto`一样，当使用 r 值引用时，这种类型的推导变得有趣起来，如下所示:\n\n```cpp\ntemplate<typename T>\nvoid foo(T &&t)\n{\n    show_type(t);\n}\n```\n\n前面的例子将`t`定义为转发引用(也称为通用引用)。通用引用采用它所传递的任何引用类型。例如，我们这样调用这个函数:\n\n```cpp\nint main(void)\n{\n    int i = 42;\n    foo(i);\n}\n```\n\n我们得到以下输出:\n\n![](img/55fe5e8f-d61e-4087-9c56-e55f50384a8a.png)\n\n前面的输出显示模板函数被赋予了一个整数的 l 值引用。这是因为`i`，在我们的主函数中，是一个 l 值，即使这个函数看起来是在请求一个 r 值引用。要获得 r 值参考，我们必须提供一个 r 值，如下所示:\n\n```cpp\nint main(void)\n{\n    int i = 42;\n    foo(std::move(i));\n}\n```\n\n执行时会产生以下输出:\n\n![](img/04ac53de-9f09-4000-afe6-8a9a9d78835b.png)\n\n如前面的截图所示，现在我们已经给了通用引用一个 r 值，我们得到了一个 r 值。应该注意的是，普遍参考文献只有以下签名:\n\n```cpp\ntemplate<typename T>\nvoid foo(T &&t)\n```\n\n例如，以下内容不是通用参考:\n\n```cpp\ntemplate<typename T>\nvoid foo(const T &&t)\n```\n\n以下也不是普遍参考:\n\n```cpp\nvoid foo(int &&t)\n```\n\n前面两个例子都是 r 值引用，因此需要提供一个 r 值(换句话说，这两个函数都定义了移动操作)。通用参考将接受 l 值和 r 值参考。虽然这看起来是一个优点，但它也有缺点，那就是有时很难知道您的模板函数是收到了 l 值还是 r 值。目前，确保模板函数充当 r 值引用而不是通用引用的最佳方法是使用 SFINAE:\n\n```cpp\nstd::is_rvalue_reference_v<decltype(t)>\n```\n\n最后，还可以对不太常见的类型(如 C 风格的数组)执行类型推导，如本例所示:\n\n```cpp\ntemplate<typename T, size_t N>\nvoid foo(T (&&t)[N])\n{\n    show_type(t);\n}\n```\n\n前面的函数声明我们希望将类型为`T`和大小为`N`的 C 风格数组传递给函数，然后在执行时输出其类型。我们可以如下使用这个函数:\n\n```cpp\nint main(void)\n{\n    foo({4, 8, 15, 16, 23, 42});\n}\n```\n\n这将自动推导为类型为`int`且大小为`6`的 C 型数组的 r 值引用。如本食谱所示，C++ 提供了几种机制，允许编译器确定模板函数中使用了哪些类型。\n\n# 在 C++ 17 中利用模板类类型推导\n\n在这个食谱中，我们将学习如何在 C++ 17 中使用类模板进行类类型推导。这个方法很重要，因为 C++ 17 增加了从构造函数推导模板类类型的能力，这减少了代码的冗长和冗余。\n\n从这个方法中获得的知识将为您提供编写 C++ 类的能力，这些类可以从类构造函数中正确地推导出它们的类型，而不需要显式的类型声明。\n\n# 准备好\n\n开始之前，请确保满足所有技术要求，包括安装 Ubuntu 18.04 或更高版本，并在终端窗口中运行以下内容:\n\n```cpp\n> sudo apt-get install build-essential git cmake\n```\n\n这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后，打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。\n\n# 怎么做...\n\n执行以下步骤来尝试此食谱:\n\n1.  从新的终端，运行以下命令下载源代码:\n\n```cpp\n> cd ~/\n> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git\n> cd Advanced-CPP-CookBook/chapter12\n```\n\n2.  要编译源代码，请运行以下命令:\n\n```cpp\n> cmake .\n> make recipe04_examples\n```\n\n3.  编译源代码后，您可以通过运行以下命令来执行该配方中的每个示例:\n\n```cpp\n> ./recipe04_example01\nt = int\nt = int\n\n> ./recipe04_example02\nt = int&\n\n> ./recipe04_example03\nt = int&&\nt = int&&\n\n> ./recipe04_example04\nt = int&&\nu = int&\n\n> ./recipe04_example05\nt = int&&\n\n> ./recipe04_example06\nt = const char (&)[16]\nu = int&&\n```\n\n在下一节中，我们将逐一介绍这些示例，并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。\n\n# 它是如何工作的...\n\n类模板类型推断是 C++ 17 中添加的一个新特性，它提供了从模板类的构造函数推断模板类类型的能力。假设我们有以下类模板:\n\n```cpp\ntemplate<typename T>\nclass the_answer\n{\n\npublic:\n    the_answer(T t)\n    {\n        show_type(t);\n    }\n};\n```\n\n如前面的代码片段所示，我们有一个简单的类模板，它在构造过程中采用类型`T`，并使用`show_type()`函数输出给定的任何类型。在 C++ 17 之前，这个类应该已经使用以下内容进行了实例化:\n\n```cpp\nint main(void)\n{\n    the_answer<int> is(42);\n}\n```\n\n使用 C++ 17，我们现在可以如下实例化这个类:\n\n```cpp\nint main(void)\n{\n    the_answer is(42);\n}\n```\n\n这样做的原因是类的构造函数以一个类型`T`作为参数。由于我们提供了一个数字整数作为参数，类的类型`T`被推导为一个整数。这种类型的推导也包括对引用的支持。看看这个例子:\n\n```cpp\ntemplate<typename T>\nclass the_answer\n{\n\npublic:\n    the_answer(T &t)\n    {\n        show_type(t);\n    }\n};\n```\n\n在前面的例子中，我们的类将`T&`作为类的构造函数中的参数，这允许我们如下实例化该类:\n\n```cpp\nint main(void)\n{\n    int i = 42;\n    the_answer is(i);\n}\n```\n\n执行时会产生以下结果:\n\n![](img/b10d531e-b7d8-49ce-b509-30aa6f695860.png)\n\n如前例所示，该类的类型`T`被推导为对整数的 l 值引用。大多数适用于函数模板的类型推导规则也适用于类模板，但也有一些例外。例如，类模板构造函数不支持转发引用(通用引用)。考虑以下代码:\n\n```cpp\ntemplate<typename T>\nclass the_answer\n{\n\npublic:\n    the_answer(T &&t)\n    {\n        show_type(t);\n    }\n};\n```\n\n前面的构造函数不是通用引用；这是一个 r 值参考，意味着我们不能执行以下操作:\n\n```cpp\nthe_answer is(i);\n```\n\n这是不可能的，因为它试图将 l 值绑定到 r 值，这是不允许的。相反，像任何其他 r 值引用一样，我们必须使用以下内容实例化该类:\n\n```cpp\nthe_answer is(std::move(i));\n```\n\n或者我们可以用以下内容绑定它:\n\n```cpp\nthe_answer is(42);\n```\n\n类模板类型推导不支持通用引用的原因是，类模板类型推导使用构造函数推导类型，然后根据推导出的类型为类的其余部分填充类型，这意味着在编译构造函数时，它看起来像这样:\n\n```cpp\nclass the_answer\n{\n\npublic:\n    the_answer(int &&t)\n    {\n        show_type(t);\n    }\n};\n```\n\n这定义了一个 r 值参考。\n\n要在构造函数或任何其他函数中获得通用引用，必须使用成员函数模板，该模板本身仍然支持类型推导，但不用于推导类的任何类型。看看这个例子:\n\n```cpp\ntemplate<typename T>\nclass the_answer\n{\n\npublic:\n\n    template<typename U>\n    the_answer(T &&t, U &&u)\n    {\n        show_type(t);\n        show_type(u);\n    }\n};\n```\n\n在前面的例子中，我们创建了一个类型为`T`的类模板，我们将构造函数定义为成员函数模板。建造师本身取`T &&t``U &&u`。然而，在这种情况下，`t`是一个 r 值参考，`u`是一个通用参考，尽管它们看起来相同。两者都可以由 C++ 17 编译器推导出来，如下所示:\n\n```cpp\nint main(void)\n{\n    int i = 42;\n    the_answer is(std::move(i), i);\n}\n```\n\n还应该注意的是，构造函数不一定要有任何特定顺序的类型才能进行推导。唯一的要求是所有类型都出现在构造函数的参数中。例如，考虑以下代码:\n\n```cpp\ntemplate<typename T>\nclass the_answer\n{\n\npublic:\n    the_answer(size_t size, T &&t)\n    {\n        show_type(t);\n    }\n};\n```\n\n前面的示例可以实例化如下:\n\n```cpp\nint main(void)\n{\n    the_answer is_2(42, 42);\n}\n```\n\n最后，类型推导也支持多个模板类型，如本例所示:\n\n```cpp\ntemplate<typename T, typename U>\nclass the_answer\n{\n\npublic:\n    the_answer(const T &t, U &&u)\n    {\n        show_type(t);\n        show_type(u);\n    }\n};\n```\n\n前面的示例创建了一个具有两种泛型类型的类模板。这个类的构造函数创建了一个对类型`T`的 l 值引用，同时也获取了一个对类型`U`的 r 值引用。这个类可以实例化如下:\n\n```cpp\nint main(void)\n{\n    the_answer is(\"The answer is: \", 42);\n}\n```\n\n这将产生以下输出:\n\n![](img/137fb1ac-46dc-4fe1-b8f4-6d95dcdbdfcf.png)\n\n如前例所示，`T`和`U`都推导成功。\n\n# 在 C++ 17 中使用用户定义的类型推导\n\n在本食谱中，我们将学习如何使用用户定义的推导指南来帮助编译器进行类模板类型的推导。大多数情况下，不需要用户定义的推导指南，但是在某些情况下，它们可能是为了确保编译器推导出正确的类型。这个方法很重要，因为没有用户定义的类型推导，某些类型的模板方案是不可能的，如下所示。\n\n# 准备好\n\n开始之前，请确保满足所有技术要求，包括安装 Ubuntu 18.04 或更高版本，并在终端窗口中运行以下内容:\n\n```cpp\n> sudo apt-get install build-essential git cmake\n```\n\n这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后，打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。\n\n# 怎么做...\n\n执行以下步骤来尝试此食谱:\n\n1.  从新的终端，运行以下命令下载源代码:\n\n```cpp\n> cd ~/\n> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git\n> cd Advanced-CPP-CookBook/chapter12\n```\n\n2.  要编译源代码，请运行以下命令:\n\n```cpp\n> cmake .\n> make recipe05_examples\n```\n\n3.  编译源代码后，您可以通过运行以下命令来执行该配方中的每个示例:\n\n```cpp\n> ./recipe05_example01\nt = unsigned int\nt = int\n\n> ./recipe05_example02\nt = unsigned int\n\n> ./recipe05_example03\nt = std::__cxx11::basic_string<char>\n```\n\n在下一节中，我们将逐一介绍这些示例，并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。\n\n# 它是如何工作的...\n\n类模板类型推断是 C++ 17 中非常需要的特性，因为它有助于减少 C++ 的冗余和冗长。然而，在某些情况下，编译器会推导出错误的类型——如果我们不依赖类型推导，这个问题是可以解决的。为了更好地理解这种类型的问题，让我们看下面的例子:\n\n```cpp\ntemplate<typename T>\nclass the_answer\n{\n\npublic:\n    the_answer(T t)\n    {\n        show_type(t);\n    }\n};\n```\n\n在前面的例子中，我们创建了一个简单的类模板，它的构造函数采用类型`T`，并使用`show_type()`函数输出给定的任何类型。现在假设我们希望使用这个类来实例化一个采用无符号整数的版本。有两种方法可以做到这一点:\n\n```cpp\nthe_answer<unsigned> is(42);\n```\n\n前面的方法是最明显的，因为我们明确地告诉编译器我们想要什么类型，而根本不使用类型推导。获取无符号整数的另一种方法是使用正确的数字文字语法，如下所示:\n\n```cpp\nthe_answer is(42U);\n```\n\n在前面的例子中，我们利用了类型推导，但是我们必须确保总是将`U`添加到整数中。这种方法的优点是代码是显式的。这种方法的缺点是，如果我们忘记添加`U`来声明我们希望有一个无符号整数，我们可能会无意中创建一个具有`int`类型而不是`unsigned`类型的类。\n\n为了防止这个问题，我们可以利用用户定义的类型推断来告诉编译器，如果它看到整数类型，我们实际上是指无符号类型，如下所示:\n\n```cpp\nthe_answer(int) -> the_answer<unsigned>;\n```\n\n前面的语句告诉编译器，如果它看到一个具有`int`类型的构造函数，`int`应该生成一个具有`unsigned`类型的类。\n\nThe left-hand side takes a constructor signature, while the right-hand side takes a class template signature.\n\n使用此方法，我们可以获取我们看到的任何构造函数签名，并将其转换为我们希望的类模板类型，如本例所示:\n\n```cpp\nthe_answer(const char *) -> the_answer<std::string>;\n```\n\n用户定义的类型推导指南告诉编译器，如果看到 C 风格的字符串，应该改为创建`std::string`。然后，我们可以用以下内容运行我们的示例:\n\n```cpp\nint main(void)\n{\n    the_answer is(\"The answer is: 42\");\n}\n```\n\n然后，我们得到以下输出:\n\n![](img/33a212a7-2507-4906-af8c-813b8dcb6bc0.png)\n\n如前面的截图所示，这个类是用`std::string`(或者至少是 GCC 对`std::string`的内部表示)构造的，而不是 C 风格的字符串。"
  },
  {
    "path": "docs/adv-cpp-prog-cb/13.md",
    "content": "# 十三、奖励——使用 C++ 20 特性\n\n在本章中，您将快速了解一些即将添加到 C++ 20 中的特性。这一章很重要，因为与 C++ 14 和 C++ 17 不同，C++ 20 为语言增加了几个改变游戏规则的特性，这些特性将永远改变 C++ 的面貌。\n\n它首先介绍了 C++ 20 概念，这是一种定义任意类型需求的新机制。C++ 20 Concepts 承诺改变我们使用模板和`auto`进行编程的方式，提供一种定义一个类型需要什么的机制。然后我们将转向 C++ 20 Modules，这是一个不再需要`#include`的新特性，改变了我们在 C++ 中定义接口的方式。C++ Modules 是对语言的巨大改变，需要对整个标准库以及我们的构建工具进行彻底的检查。接下来，我们将快速了解一下`std::span`和 C++ 范围。最后，我们将简要介绍 C++ 20 的另一个改变游戏规则的补充，叫做 Coroutines。\n\n本章中的配方如下:\n\n*   查看 C++ 20 中的概念\n*   使用 C++ 20 中的模块\n*   引入`std::span`，数组的新观点\n*   在 C++ 20 中使用范围\n*   学习如何在 C++ 20 中使用 Coroutines\n\n# 技术要求\n\n要编译和运行本章中的示例，您必须拥有运行 Ubuntu 19.04 的计算机的管理权限，并且具有功能性互联网连接。请注意，本书的其余部分使用 Ubuntu 18.04。由于我们将讨论仍在开发中的 C++ 20，因此在这一特定章节中，我们需要最新、最棒的 GCC 版本。在运行这些示例之前，您必须安装以下内容:\n\n```cpp\n> sudo apt-get install build-essential git cmake\n```\n\n如果这安装在 Ubuntu 18.04 以外的任何操作系统上，则需要 GCC 7.4 或更高版本以及 CMake 3.6 或更高版本。\n\n本章的代码文件可以在[//github . com/packt publishing/Advanced-CPP-cook book/tree/master/chapter 13](https://github.com/PacktPublishing/Advanced-CPP-CookBook/tree/master/chapter13https://github.com/PacktPublishing/Advanced-CPP-CookBook/tree/master/chapter13)找到。\n\n# 查看 C++ 20 中的概念\n\n在这个食谱中，我们将讨论 C++ 即将增加的内容，它有望彻底改变我们对称为 C++ 20 Concepts 的模板编程的看法。今天的 C++ 很大程度上依赖于使用 SFINAE 来约束适用于任何给定模板函数的类型。如[第 4 章](04.html)、*使用模板进行泛型编程*所见，SFINAE 很难写，读起来很混乱，编译速度也很慢。这个食谱很重要，因为 C++ 20 后的模板编程不仅更容易编码和调试，还将降低模板编程的人力成本，使其更容易阅读和理解。\n\n# 准备好\n\n开始之前，请确保满足所有技术要求，包括安装 Ubuntu 19.04 或更高版本，并在终端窗口中运行以下内容:\n\n```cpp\n> sudo apt-get install build-essential git cmake\n```\n\n这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后，打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。\n\n# 怎么做...\n\n您需要执行以下步骤来尝试配方:\n\n1.  从新的终端，运行以下命令下载源代码:\n\n```cpp\n> cd ~/\n> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git\n> cd Advanced-CPP-CookBook/chapter13\n```\n\n2.  要编译源代码，请运行以下命令:\n\n```cpp\n> cmake .\n> make recipe01_examples\n```\n\n3.  编译源代码后，您可以通过运行以下命令来执行该配方中的每个示例:\n\n```cpp\n> ./recipe01_example01\nThe answer is: 42\nThe answer is not: 43\n\n> ./recipe01_example02\nThe answer is: 42\nThe answer is not: 43\n```\n\n在下一节中，我们将逐一介绍这些示例，并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。\n\n# 它是如何工作的...\n\n为了最好地解释 C++ 20 Concepts 将如何帮助模板编程，我们今天将从一个用 C++ 编程接口的简单例子开始。接口定义了**应用编程接口** ( **应用编程接口**)的实现和应用编程接口的用户之间的契约，并在面向对象编程中大量使用，以将应用编程接口的接口从其实现细节中抽象出来。\n\n让我们从下面的纯虚拟界面开始:\n\n```cpp\nclass interface\n{\npublic:\n    virtual ~interface() = default;\n    virtual void foo() = 0;\n};\n```\n\nC++ 中前面的纯虚拟接口定义了一个`foo()`函数。这个 API 的客户端不需要知道`foo()`是如何实现的。他们只关心接口的定义和`foo()`的函数签名，来理解`foo()`应该如何表现。使用这个接口，我们可以定义这个接口的实现，如下所示:\n\n```cpp\nclass A :\n    public interface\n{\npublic:\n    void foo() override\n    {\n        std::cout << \"The answer is: 42\\n\";\n    }\n};\n```\n\n如前面的例子所示，我们创建了一个名为`A`的类，它继承了接口并覆盖了`foo()`函数来实现它。我们可以用另一个实现做同样的事情，如下所示:\n\n```cpp\nclass B :\n    public interface\n{\npublic:\n    void foo() override\n    {\n        std::cout << \"The answer is not: 43\\n\";\n    }\n};\n```\n\n如前面的例子所示，`B`类为接口提供了接口的替代实现。该接口的客户端可以按如下方式使用该接口:\n\n```cpp\nclass client\n{\n    interface &m_i;\n\npublic:\n    client(interface &i) :\n        m_i{i}\n    { }\n\n    void bar()\n    {\n        m_i.foo();\n    }\n};\n```\n\n客户实际上不需要知道任何关于`A`或`B`的事情。它只包括接口的定义，并使用接口来访问任何特定的实现。我们可以按如下方式使用该客户端:\n\n```cpp\nint main(void)\n{\n    A a;\n    B b;\n\n    client c1(a);\n    client c2(b);\n\n    c1.bar();\n    c2.bar();\n}\n```\n\n如前面的例子所示，我们首先创建`A`和`B`的实例，然后创建两个不同的客户端，分别为`A`和`B`提供接口实现。最后，我们为每个客户端执行`bar()`函数，得到如下输出:\n\n![](img/de930bf3-bf6d-44a0-8b41-446a2d7e0f6d.png)\n\n如前面的截图所示，客户端不知道接口是以两种不同的方式定义的，因为客户端只关心接口本身。这种技术在很多 C++ 文献中有所展示，特别是为了实现所谓的面向对象设计原则。标准操作规程设计原则代表以下内容:\n\n*   **单一责任原则**:这就保证了如果一个对象必须改变，它改变的原因只有一个(即一个对象不提供多个责任)。\n*   **打开-关闭原理**:这保证了一个对象可以在不被修改的情况下进行扩展。\n*   **利斯科夫替换原则**:这确保了当使用继承时，子类实现它们覆盖的函数的行为，而不仅仅是函数的签名。\n*   **接口隔离原则**:这样可以保证一个对象有尽可能小的接口，这样对象的客户端就不会被迫依赖不使用的 API。\n*   **依赖反转原理**:这保证了对象只依赖接口，不依赖实现。\n\n这些原则的结合旨在确保您在 C++ 中使用面向对象编程随着时间的推移更容易理解和维护。然而，现有的面向开源软件和 C++ 的文献的一个问题是，它提倡大量使用纯虚拟接口，这是有代价的。必须给每个类一个额外的虚拟表(即 vTable)，所有函数调用都会遇到虚拟函数重载的额外开销。\n\n解决这个问题的一种方法是使用静态接口(这在现有文献中并不经常讨论)。为了最好地解释这是如何工作的，让我们从接口的定义开始，如下所示:\n\n```cpp\n#include <iostream>\n\ntemplate<typename DERIVED>\nclass interface\n{\npublic:\n    constexpr void foo()\n    {\n        static_cast<DERIVED *>(this)->foo_override();\n    }\n};\n```\n\n如前面的例子所示，我们将利用静态多态性来实现我们的接口。前面的类采用名为`DERIVED`的类型，并将接口的一个实例强制转换为`DERIVED`类，调用已经被覆盖的`foo`函数的一个版本。`A`现在的实现是这样的:\n\n```cpp\nclass A :\n    public interface<A>\n{\npublic:\n    void foo_override()\n    {\n        std::cout << \"The answer is: 42\\n\";\n    }\n};\n```\n\n如前例所示，`A`现在继承的不是接口，而是`A`的接口。当调用界面中的`foo()`函数时，界面会将调用重定向到`A`的`foo_override()`函数。我们可以使用相同的方法实现`B`:\n\n```cpp\nclass B :\n    public interface<B>\n{\npublic:\n    void foo_override()\n    {\n        std::cout << \"The answer is not: 43\\n\";\n    }\n};\n```\n\n如前例所示，`B`能够提供自己的接口实现。需要注意的是，到目前为止，在这个设计模式中，我们还没有使用`virtual`，这意味着我们已经创建了一个接口和该接口的实现，而不需要虚拟继承，因此没有与这个设计相关的开销。事实上，编译器能够移除从`foo()`到`foo_override()`的调用重定向，确保抽象的使用与纯虚拟接口的使用相比不提供任何额外的运行时成本。\n\n`A`和`B`的客户端可以实现如下:\n\n```cpp\ntemplate<typename T>\nclass client\n{\n    interface<T> &m_i;\n\npublic:\n    client(interface<T> &i) :\n        m_i{i}\n    { }\n\n    void bar()\n    {\n        m_i.foo();\n    }\n};\n```\n\n如前面的代码片段所示，此示例中的客户端与上一示例中的客户端之间的唯一区别是，此客户端是一个模板类。静态多态性要求关于接口的信息类型在编译时是已知的。这在大多数设计中是没问题的，因为早期使用纯虚拟接口并不是因为我们想要执行运行时多态性和类型擦除的能力，而是为了确保客户端只遵守接口而不遵守实现。在这两种情况下，每个客户端的实现都是静态的，并且在编译时是已知的。\n\n为了使用客户端，我们可以使用一些 C++ 17 类类型的推导来保证我们的`main()`函数保持不变，如下所示:\n\n```cpp\nint main(void)\n{\n    A a;\n    B b;\n\n    client c1(a);\n    client c2(b);\n\n    c1.bar();\n    c2.bar();\n}\n```\n\n执行前面的示例会产生以下结果:\n\n![](img/417c5648-2521-4327-9b35-d507866b6af7.png)\n\n如前一张截图所示，代码执行相同的操作。这两种方法的唯一区别是，一种方法使用纯虚拟继承，这带来了运行时成本，而第二种方法使用静态多态性，这带来了人为成本。具体来说，前面的例子对于大多数初学者来说很难理解。在具有嵌套依赖关系的大型项目中，静态多态性的使用非常难以理解和阅读。\n\n前面例子的另一个问题是，当给出错误的类型时，编译器没有足够的关于接口和该接口的客户端的信息来提供合理的错误消息。看看这个例子:\n\n```cpp\nint main(void)\n{\n    client c(std::cout);\n}\n```\n\n这将导致以下编译器错误:\n\n```cpp\n/home/user/book/chapter13/recipe01.cpp: In function ‘int main()’:\n/home/user/book/chapter13/recipe01.cpp:187:23: error: class template argument deduction failed:\n  187 | client c(std::cout);\n      | ^\n/home/user/book/chapter13/recipe01.cpp:187:23: error: no matching function for call to ‘client(std::ostream&)’\n/home/user/book/chapter13/recipe01.cpp:175:5: note: candidate: ‘template<class T> client(interface<T>&)-> client<T>’\n  175 | client(interface<T> &i) :\n      | ^~~~~~\n\n...\n```\n\n前面的错误信息几乎没有用，尤其是对于初学者。为了克服这些问题，C++ 20 Concepts 承诺提供一个更清晰的模板编程实现。为了更好地解释这一点，让我们看看如何使用 C++ 20 概念实现接口:\n\n```cpp\ntemplate <typename T>\nconcept interface = requires(T t)\n{\n    { t.foo() } -> void;\n};\n```\n\n如前面的例子所示，我们已经定义了一个名为`interface`的 C++ 20 概念。给定一个类型`T`，这个概念要求`T`提供一个名为`foo()`的函数，该函数不接受输入也不返回输出。我们可以这样定义`A`:\n\n```cpp\nclass A\n{\npublic:\n    void foo()\n    {\n        std::cout << \"The answer is: 42\\n\";\n    }\n};\n```\n\n如前面的代码片段所示，`A`不再需要利用继承。它只是提供了一个`foo()`函数，给出了一个普通的 C++ 类定义。`B`也是这样实施的:\n\n```cpp\nclass B\n{\npublic:\n    void foo()\n    {\n        std::cout << \"The answer is not: 43\\n\";\n    }\n};\n```\n\n再一次，继承不再需要。该接口的客户端实现如下:\n\n```cpp\ntemplate<interface T>\nclass client\n{\n    T &m_i;\n\npublic:\n    client(T &i) :\n        m_i{i}\n    { }\n\n    void bar()\n    {\n        m_i.foo();\n    }\n};\n```\n\n如前面的例子所示，我们已经定义了一个采用模板类型`T`并调用其`foo()`函数的类。在前面的静态多态示例中，我们可以用完全相同的方式实现客户端。这种方法的问题在于，客户无法确定类型`T`是否符合界面。静态断言结合 SFINAE，例如`std::is_base_of()`，可以用来解决这个问题，但是每个依赖于接口的对象都必须包含这个逻辑。然而，使用 C++ 20 概念，这种简单性可以在不需要继承或任何复杂的模板技巧(如 SFINAE)的情况下实现。那么，让我们看看我们可以用什么来代替以下内容:\n\n```cpp\ntemplate<typename T>\n```\n\n可以使用以下内容来代替:\n\n```cpp\ntemplate<interface T>\n```\n\n今天 C++ 模板编程的问题是`typename`关键字没有告诉编译器任何关于类型本身的信息。SFINAE 提供了一种方法来解决这个问题，它以巨大的人力成本定义了一个类型的某些特征，因为 SFINAE 的理解更加复杂，当事情出错时，由此产生的编译器错误一点用都没有。C++ 20 Concepts 通过定义一个称为 Concept 的类型的属性来解决所有这些问题，然后使用该概念代替`typename`，为编译器提供确定给定类型是否符合该概念所需的所有信息。当出现问题时，编译器可以提供一个简单的错误消息，说明所提供的类型缺少什么。\n\nC++ 20 Concepts 是一个即将推出的令人兴奋的新功能，它有望彻底改变我们使用 C++ 模板编程的方式，以更复杂的编译器和 C++ 规范为代价，降低使用模板的总体人力成本。\n\n# 使用 C++ 20 中的模块\n\n在这个食谱中，我们将了解更多关于 C++ 20 的一个新特性，叫做模块。这个食谱很重要，因为 C++ 20 模块不再需要`#include`前进。今天的 C++ 代码通常分为头文件和源文件。每个源文件都是单独编译的，并且必须重新编译它包含的头文件(以及包含的头文件包含的任何头文件)，导致编译速度慢、依赖顺序问题以及 C 风格宏的过度使用。相反，可选地，库将使用 C++ 20 Modules 包括在内，改变我们编程的方式，甚至像“Hello World”这样的简单应用。\n\n# 准备好\n\n开始之前，请确保满足所有技术要求，包括安装 Ubuntu 19.04 或更高版本，并在终端窗口中运行以下内容:\n\n```cpp\n> sudo apt-get install build-essential git cmake\n```\n\n这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后，打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。\n\n# 怎么做...\n\n您需要执行以下步骤来尝试此食谱:\n\n1.  从新的终端，运行以下命令下载源代码:\n\n```cpp\n> cd ~/\n> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git\n> cd Advanced-CPP-CookBook/chapter13\n```\n\n2.  要编译源代码，请运行以下命令:\n\n```cpp\n> cmake .\n> make recipe02_examples\n```\n\n3.  编译源代码后，您可以通过运行以下命令来执行该配方中的每个示例:\n\n```cpp\n> ./recipe02_example01\nHello World\n\n> ./recipe02_example03\nThe answer is: 42\n```\n\n在下一节中，我们将逐一介绍这些示例，并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。应该注意的是，源代码中的示例 2 和示例 4 无法编译，因为在编写本文时，GCC 还不支持 C++ Modules。\n\n# 它是如何工作的...\n\nC++ 20 模块提供了一种新的方法来包含 C++ 中使用的 API 的定义。下面我们来看看如何用 C++ 编写一个简单的`Hello World`应用的例子:\n\n```cpp\n#include <iostream>\n\nint main(void)\n{\n    std::cout << \"Hello World\\n\";\n}\n```\n\n要使用 C++ 20 模块编写相同的应用，您需要执行以下操作:\n\n```cpp\nimport std.core;\n\nint main(void)\n{\n    std::cout << \"Hello World\\n\";\n}\n```\n\n虽然区别很细微，但在引擎盖下，已经发生了很多变化，使前面的代码成为可能。让我们看一个更复杂的例子，如下所示:\n\n```cpp\n#include <string>\n\ntemplate<size_t number>\nclass the_answer\n{\npublic:\n    auto operator()() const\n    {\n        return \"The answer is: \" + std::to_string(number);\n    }\n};\n\n#define CHECK(a) (a() == \"The answer is: 42\")\n```\n\n在前面的代码中，我们定义了一个头文件，它定义了一个名为`the_answer`的类模板。为了实现这个模板，我们必须包含`string`库。我们还在这个头部添加了一个宏来测试我们的类。我们可以如下使用这个标题:\n\n```cpp\n#include <iostream>\n#include \"header.h\"\n\nint main(void)\n{\n    the_answer<42> is;\n    std::cout << is() << '\\n';\n}\n```\n\n如前面的代码片段所示，我们包含了我们的头，创建了一个模板类的实例，并使用它来输出一条消息。执行时，我们会得到以下输出:\n\n![](img/bdf986f5-0d0d-4ff1-a0f9-edbf1528f356.png)\n\n虽然这是一个简单的例子，展示了一个实现 C++ functor 的类模板，但是这段代码有一些问题:\n\n*   `the_answer`的实现依赖于`string`库。这意味着，无论何时使用`header.h`，您不仅包括`the_answer`的定义，还包括`string`库的完整定义，包括其所有依赖项。这种类型的依赖链导致大量的构建时间成本。\n*   客户端也可以访问`CHECK()`宏。在 C++ 中，没有办法命名一个宏，导致宏冲突的可能性。\n*   前面的例子很小，因此很容易编译，但是假设我们的头是`30,000`行模板代码，其中混合了几个自己的包含。现在，假设我们必须在数百个源文件中包含我们的头。这种情况的结果将是非常长的编译时间，因为每次编译一个源文件，它必须一次又一次地重新编译同一个巨大的头文件。\n\n为了理解 C++ Modules 如何解决这些问题，让我们看看使用模块时同样的代码会是什么样子:\n\n```cpp\nimport std.string;\nexport module answers;\n\nexport\ntemplate<size_t number>\nclass the_answer\n{\npublic:\n    auto operator()() const\n    {\n        return \"The answer is: \" + std::to_string(number);\n    }\n};\n\n#define CHECK(a) (a() == \"The answer is: 42\")\n```\n\n如前面的代码片段所示，我们的自定义库包括字符串的定义，然后使用`export`模块创建一个名为`answers`的新 C++ 模块。然后我们用`export`定义来定义我们的类模板。每当编译一个头时(实际上，每当编译任何代码时)，编译器通常首先将人类可读的 C++ 语法转换成一种叫做**中间表示** ( **IR** )的东西。然后，这种红外线被转换成二进制组件。问题是头文件包含的代码(如宏和包含)无法转换为这种类型的表示，这意味着编译器每次看到头文件时，都必须将代码转换为 IR，然后每次都转换为二进制。\n\nC++ Modules 提供了一种语法和一组规则，使编译器能够将头文件转换为 IR，并将该 IR 的结果与生成的其他对象文件一起存储。编译器可以根据需要多次使用该红外，无需不断重复执行红外转换过程的代码。为了了解前面的代码是如何使用的，我们来看看下面的代码:\n\n```cpp\nimport answers;\nimport std.core;\n\nint main(void)\n{\n    the_answer<42> is;\n    std::cout << is();\n}\n```\n\n如这里所示，我们包括`std::cout`的定义和我们的`answers`模块。不同的是`main()`函数不需要将`answers`和`std.core`定义从 C++ 语法转换成编译器的 IR，减少了`main()`源文件的编译时间。`main()`源文件也可以创建一个名为`CHECK()`的宏，而不会与我们`answers`模块中的同一个宏发生冲突，因为宏是无法导出的。\n\n# 介绍 std::span，一种新的数组视图\n\n在这个食谱中，我们将学习如何使用`std::span`，这是 C++ 20 附带的一个新功能。这个配方很重要，因为`std::span`是指南支持库`gsl::span`的后代，该库是用于确保您的 C++ 符合核心指南的库的核心组件。在本食谱中，我们不仅将介绍`std::span`，还将解释如何在您自己的代码中使用它，以及为什么它有助于用数组的大小封装数组，并为一般使用数组提供方便的应用编程接口。\n\n# 准备好\n\n开始之前，请确保满足所有技术要求，包括安装 Ubuntu 19.04 或更高版本，并在终端窗口中运行以下内容:\n\n```cpp\n> sudo apt-get install build-essential git cmake\n```\n\n这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后，打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。\n\n# 怎么做...\n\n您需要执行以下步骤来尝试配方:\n\n1.  从新的终端，运行以下命令下载源代码:\n\n```cpp\n> cd ~/\n> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git\n> cd Advanced-CPP-CookBook/chapter13\n```\n\n2.  要编译源代码，请运行以下命令:\n\n```cpp\n> cmake .\n> make recipe03_examples\n```\n\n3.  编译源代码后，您可以通过运行以下命令来执行该配方中的每个示例:\n\n```cpp\n> ./recipe03_example01\n4 8 15 16 23 42 \n\n> ./recipe03_example02\n4 8 15 16 23 42 \n\n> ./recipe03_example03\n4 8 15 16 23 42 \n\n> ./recipe03_example04\n4 8 15 16 23 42 \n\n> ./recipe03_example05\nsize: 6\nsize (in bytes): 24\nsize: 6\nsize (in bytes): 24\nsize: 6\nsize (in bytes): 24\n\n> ./recipe03_example06\n42 \n```\n\n在下一节中，我们将逐一介绍这些示例，并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。\n\n# 它是如何工作的...\n\n在这个食谱中，我们将探索`std::span`是什么以及为什么需要它。在 C++ 中(甚至在 C 中)，为了将数组传递给函数，实现了以下操作:\n\n```cpp\nvoid foo(const int *array, size_t size)\n{\n    for (auto i = 0; i < size; i++) {\n        std::cout << array[i] << ' ';\n    }\n\n    std::cout << '\\n';\n}\n```\n\n如前例所示，我们创建了一个名为`foo()`的函数，该函数获取指向数组的指针以及数组的大小。然后我们使用这些信息将数组的内容输出到`stdout`。\n\n我们可以按如下方式执行该功能:\n\n```cpp\nint main(void)\n{\n    int array[] = {4, 8, 15, 16, 23, 42};\n    foo(array, sizeof(array)/sizeof(array[0]));\n}\n```\n\n这将产生以下输出:\n\n![](img/94681bc8-2828-4927-8427-fec635cdd27a.png)\n\n前面代码的问题是它不符合 C++ 核心指南。具体来说，我们被迫独立于数组本身存储数组的大小。如果阵列及其大小变得不同步，这可能会导致问题(这在大型项目中是可能的)。与数组相关的指针的使用也防止了范围内的`for`循环的使用，这意味着我们必须手动遍历数组，如果`for`循环没有被正确构造，这也可能导致潜在的稳定性问题。最后，我们需要使用`sizeof()`手工计算数组的大小，如图所示，这个操作很容易出错。\n\n解决这个问题的一种方法是使用模板函数，如下所示:\n\n```cpp\ntemplate<size_t N>\nvoid foo(const int (&array)[N])\n{\n    for (auto i = 0; i < N; i++) {\n        std::cout << array[i] << ' ';\n    }\n\n    std::cout << '\\n';\n}\n```\n\n如前面的代码片段所示，我们定义了一个模板函数，它引用了一个大小为`N`的整数数组。然后我们可以使用`N`遍历这个数组。我们甚至可以在数组上使用远程`for`循环，因为编译器在编译时知道数组的大小。该代码可以如下使用:\n\n```cpp\nint main(void)\n{\n    int array[] = {4, 8, 15, 16, 23, 42};\n    foo(array);\n}\n```\n\n如这里所示，我们做了一些改进。我们不再传递可能导致`NULL`指针违规的指针。我们不再使用`sizeof()`手工计算数组的大小，也不再需要独立于数组本身存储数组的大小。前面代码的问题是，每次数组的大小改变时，我们必须编译一个完全不同版本的`foo()`函数。如果`foo()`功能很大，这可能是个问题。这段代码也不支持动态分配的数组(换句话说，数组是否使用`std::unique_ptr`分配)。\n\n为了解决这个问题，C++ 20 增加了`std::span`类。看看这个例子:\n\n```cpp\nvoid foo(const std::span<int> &s)\n{\n    for (auto i = 0; i < s.size(); i++) {\n        std::cout << s[i] << ' ';\n    }\n\n    std::cout << '\\n';\n}\n```\n\n如前面的代码片段所示，我们已经使用`std::span`创建了`foo()`函数，它存储了一个整数数组。像大多数其他 C++ 容器一样，我们可以获得数组的大小，并且可以使用下标运算符来访问数组的单个元素。要使用这个函数，我们只需像使用模板函数一样调用它，如下所示:\n\n```cpp\nint main(void)\n{\n    int array[] = {4, 8, 15, 16, 23, 42};\n    foo(array);\n}\n```\n\n使用`std::span`，我们现在可以为不同大小的数组提供相同的`foo()`功能，我们甚至可以使用动态内存(换句话说，`std::unique_ptr`)分配数组，而不必重新实现`foo()`功能。远程`for`循环甚至工作如预期:\n\n```cpp\nvoid foo(const std::span<int> &s)\n{\n    for (const auto &elem : s) {\n        std::cout << elem << ' ';\n    }\n\n    std::cout << '\\n';\n}\n```\n\n要使用带有动态记忆的`foo()`，我们可以做以下操作:\n\n```cpp\nint main(void)\n{\n    auto ptr1 = new int[6]();\n    foo({ptr1, 6});\n    delete [] ptr1;\n\n    std::vector<int> v(6);\n    foo({v.data(), v.size()});\n\n    auto ptr2 = std::make_unique<int>(6);\n    foo({ptr2.get(), 6});\n}\n```\n\n如前面的例子所示，我们使用动态创建的三种不同类型的内存运行`foo()`函数。第一次运行`foo()`时，我们使用`new()` / `delete()`分配内存。如果您试图保持 C++ 核心指南的兼容性，您可能对这种方法不感兴趣。第二种和第三种方法使用`std::vector`或`std::unique_ptr`分配内存。两者都有其固有的缺点:\n\n*   `std::vector`存储自己的`size()`，但也存储容量，默认情况下，初始化内存。\n*   `std::unique_ptr`不存储自己的`size()`，也默认初始化内存。\n\n目前，C++ 没有一种数组类型能够分配未初始化内存的动态数组，同时还存储数组的大小(并且只存储其大小)。`std::span`但是，根据您的需要，可以结合使用上述方法来管理阵列。\n\n还需要注意的是，在前面的例子中我们创建`std::span`的时候，我们根据元素总数而不是字节总数传递给它数组的大小。`std::span`能够为您提供两者，如下所示:\n\n```cpp\nvoid foo(const std::span<int> &s)\n{\n    std::cout << \"size: \" << s.size() << '\\n';\n    std::cout << \"size (in bytes): \" << s.size_bytes() << '\\n';\n}\n```\n\n如果我们运行前面的`foo()`实现，通过前面提到的动态内存示例，我们会得到以下结果:\n\n![](img/5cd2b8ea-8b69-4e72-8ea6-39e36b056750.png)\n\n最后，我们可以使用跨度创建额外的子跨度，如下所示:\n\n```cpp\nvoid foo2(const std::span<int> &s)\n{\n    for (const auto &elem : s) {\n        std::cout << elem << ' ';\n    }\n\n    std::cout << '\\n';\n}\n```\n\n在前面的`foo2()`函数中，我们取一个跨度，并使用一个排列的`for`循环输出它的所有元素。然后，我们可以使用以下内容来创建子跨度:\n\n```cpp\nvoid foo1(const std::span<int> &s)\n{\n    foo2(s.subspan(5, 1));\n}\n```\n\n`subspan()`功能的结果是另一个`std::span`。不同的是，它内部存储的指针已经被`5`元素推进了，而`size()`存储的是现在的`1`。\n\n# 在 C++ 20 中使用范围\n\n在本食谱中，我们将学习如何使用 C++ Ranges，这是 C++ 20 附带的一个新功能集。Ranges 提供了方便的函数来处理任何模拟一系列对象或值的东西。例如，4、8、15、16、23、42 是一个整数范围。在今天的 C++ 中，根据您正在做的事情，处理范围可能会很麻烦。这个方法很重要，因为 C++ 范围消除了许多与使用范围相关的复杂性，确保您的代码随着时间的推移更容易阅读和维护。\n\n# 准备好\n\n开始之前，请确保满足所有技术要求，包括安装 Ubuntu 19.04 或更高版本，并在终端窗口中运行以下内容:\n\n```cpp\n> sudo apt-get install build-essential git cmake\n```\n\n这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后，打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。\n\n# 怎么做...\n\n要执行此配方，请执行以下步骤:\n\n1.  从新的终端，运行以下命令下载源代码:\n\n```cpp\n> cd ~/\n> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git\n> cd Advanced-CPP-CookBook/chapter13\n```\n\n2.  要编译源代码，请运行以下命令:\n\n```cpp\n> cmake .\n> make recipe04_examples\n```\n\n3.  编译源代码后，您可以通过运行以下命令来执行该配方中的每个示例:\n\n```cpp\n> ./recipe04_example01 \n1\n\n> ./recipe04_example02\n42\n\n> ./recipe04_example03\n42\n\n> ./recipe04_example04\n4 8 15 16 23 42 \n\n> ./recipe04_example05\n4 8 15 16 23 42 \n\n> ./recipe04_example06\n4 8 15 16 23 42 \n```\n\n在下一节中，我们将逐一介绍这些示例，并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。\n\n# 它是如何工作的...\n\nC++ Ranges 是对 C++ 20 的一个受欢迎的补充，因为它提供了一种处理任何对象或值列表的简单方法。为了最好地解释这是如何工作的，让我们看看下面的例子(注意，在这些食谱中，我们将使用 Ranges v3，同时等待 GCC 支持 Ranges，因为 v3 是 C++ 20 采用的实现):\n\n```cpp\n#include <iostream>\n#include <range/v3/algorithm/count.hpp>\n\nint main(void)\n{\n    auto list = {4, 8, 15, 16, 23, 42};\n    std::cout << ranges::count(list, 42) << '\\n';\n}\n```\n\n如前面的代码片段所示，我们创建了一个整数列表(在这个特定的例子中，我们创建了一个简单的初始化列表)。然后，我们使用`ranges::count()`函数来计算值`42`在列表中出现的总次数，得到以下输出:\n\n![](img/29bc917b-ce5c-4907-ac11-e21b6e099e03.png)\n\n范围也可用于搜索:\n\n```cpp\n#include <iostream>\n#include <range/v3/algorithm/find.hpp>\n\nint main(void)\n{\n    auto list = {4, 8, 15, 16, 23, 42};\n    if (auto i = ranges::find(list, 42); i != ranges::end(list)) {\n        std::cout << *i << '\\n';\n    }\n}\n```\n\n如前面的例子所示，我们已经创建了相同的整数初始化列表，并使用范围来返回迭代器。这个迭代器可以用来遍历列表或者获取定位值。初始值设定项列表已经支持迭代器，Ranges 所做的一件事是将这一功能扩展到其他类型，包括简单的 C 风格数组:\n\n```cpp\n#include <iostream>\n#include <range/v3/algorithm/find.hpp>\n\nint main(void)\n{\n    int list[] = {4, 8, 15, 16, 23, 42};\n    if (auto i = ranges::find(list, 42); i != ranges::end(list)) {\n        std::cout << *i << '\\n';\n    }\n}\n```\n\n前面的例子使用了一个 C 风格的数组而不是初始化列表，如图所示，Ranges 提供了一个迭代器来处理 C 风格的数组，这是目前不可能的。\n\nRanges 还提供了一些方便的算法。例如，考虑以下代码:\n\n```cpp\n#include <iostream>\n#include <range/v3/algorithm/for_each.hpp>\n\nint main(void)\n{\n    auto list = {4, 8, 15, 16, 23, 42};\n\n    ranges::for_each(list, [](const int &val){\n        std::cout << val << ' ';\n    });\n\n    std::cout << '\\n';\n}\n```\n\n在前面的例子中，我们已经创建了一个整数列表。然后我们在整数的整个范围内循环，并在这个列表上执行一个 lambda。虽然这可以使用传统的循环来完成，比如在 C++ 11 中添加的基于范围的循环，`for_each`可以简化您的逻辑(取决于您的用例)。\n\n范围还提供了将一个列表转换成另一个列表的能力。考虑以下示例:\n\n```cpp\n#include <iostream>\n#include <range/v3/view/transform.hpp>\n\nclass my_type\n{\n    int m_i;\n\npublic:\n    my_type(int i) :\n        m_i{i}\n    { }\n\n    auto get() const\n    {\n        return m_i;\n    }\n};\n```\n\n我们将从创建自己的类型开始这个例子。如前面的代码片段所示，我们有一个名为`my_type`的新类型，它由一个整数构成，并使用`get()`函数返回该整数。然后，我们可以扩展前面的示例，将整数列表转换为自定义类型列表，如下所示:\n\n```cpp\nint main(void)\n{\n    using namespace ranges::views;\n\n    auto list1 = {4, 8, 15, 16, 23, 42};\n    auto list2 = list1 | transform([](int val){\n        return my_type(val);\n    });\n\n    for(const auto &elem : list2) {\n        std::cout << elem.get() << ' ';\n    }\n\n    std::cout << '\\n';\n}\n```\n\n如前面的例子所示，我们创建了初始的整数列表，然后使用`ranges::views::transform`函数将该列表转换为第二个自定义类型列表。然后，我们可以使用传统的基于范围的`for`循环来迭代这个新列表。\n\n最后，Ranges 还提供了一些操作，允许您实际修改现有的范围。例如，考虑以下代码:\n\n```cpp\n#include <vector>\n#include <iostream>\n#include <range/v3/action/sort.hpp>\n\nint main(void)\n{\n    using namespace ranges;\n\n    std::vector<int> list = {4, 42, 15, 8, 23, 16};\n    list |= actions::sort;\n\n    for(const auto &elem : list) {\n        std::cout << elem << ' ';\n    }\n\n    std::cout << '\\n';\n}\n```\n\n在前面的例子中，我们使用`actions::sort`函数对向量列表进行排序，得到如下输出:\n\n![](img/2017152b-aba7-4295-99e7-bbbadae2dde7.png)\n\n如前面的例子所示，C++ 20 Ranges 为我们提供了一种使用管道操作符对`std::vector`进行排序的简单方法，而不是必须使用`std::sort`，明确定义了我们的开始和结束迭代器。\n\n# 学习如何在 C++ 20 中使用 Coroutines\n\n在这个食谱中，我们将简要地浏览一下 C++ 20 中一个即将推出的名为 Coroutines 的特性。与 C++ 20 中添加的其他一些特性不同，Coroutines 在今天的 C++ 中是不可能的。协同程序提供了暂停函数执行并产生结果的能力。一旦结果被使用，函数就可以在它停止的地方继续执行。这个配方很重要，因为 C++ 20 将在 C++ 中增加一流的支持(即新的关键字)来支持 Coroutines，并且这个新特性很可能在不久的将来开始出现在库和示例中。\n\n# 准备好\n\n开始之前，请确保满足所有技术要求，包括安装 Ubuntu 19.04 或更高版本，并在终端窗口中运行以下内容:\n\n```cpp\n> sudo apt-get install build-essential git cmake\n```\n\n这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后，打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。\n\n# 怎么做...\n\n要尝试此食谱，请执行以下步骤:\n\n1.  从新的终端，运行以下命令下载源代码:\n\n```cpp\n> cd ~/\n> git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git\n> cd Advanced-CPP-CookBook/chapter13\n```\n\n2.  要编译源代码，请运行以下命令:\n\n```cpp\n> cmake .\n> make recipe05_examples\n```\n\n3.  编译源代码后，您可以通过运行以下命令来执行该配方中的每个示例:\n\n```cpp\n> ./recipe05_example01 \n0 2 4 6 8 10 \n```\n\n在下一节中，我们将逐一介绍这些示例，并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。\n\n# 它是如何工作的...\n\n如前所述，Coroutines 提供了暂停和恢复函数执行的能力。为了演示这在 C++ 20 中是如何工作的，我们将简单地看一个简单的例子:\n\n```cpp\nauto\neven_numbers(size_t s, size_t e)\n{\n    std::vector<int> nums;\n\n    if (s % 2 != 0 || e % 2 != 0) {\n        std::terminate();\n    }\n\n    for (auto i = s; i <= e; i += 2) {\n        nums.push_back(i);\n    }\n\n    return nums;\n}\n```\n\n在前面的例子中，我们创建了一个名为`even_numbers()`的函数，在给定的范围内，返回偶数的`std::vector`。然后，我们可以如下使用该函数:\n\n```cpp\nint main(void)\n{\n    for (const auto &num : even_numbers(0, 10)) {\n        std::cout << num << ' ';\n    }\n\n    std::cout << '\\n';\n}\n```\n\n这将产生以下输出:\n\n![](img/e8884a7b-b906-4296-a575-a6701c548375.png)\n\n前面实现的问题是，这段代码需要使用`std::vector`来创建一个要迭代的数字范围。借助 Coroutines，我们将能够实现如下功能:\n\n```cpp\ngenerator<int>\neven_numbers(size_t s, size_t e)\n{\n    if (s % 2 != 0 || e % 2 != 0) {\n        std::terminate();\n    }\n\n    for (auto i = s; i < e; i += 2) {\n        co_yield i;\n    }\n\n    co_return e;\n}\n```\n\n从前面的代码中，我们可以看到以下内容:\n\n*   我们现在不返回`std::vector`，而是返回`generator<int>`。\n*   当我们循环遍历循环中的每个偶数值时，我们称之为`co_yield`。这使得`even_numbers()`函数返回所提供的值，同时保留其位置。\n*   一旦`even_numbers()`函数恢复，它将返回到最初执行`co_yield`的地方，这意味着该函数现在可以继续执行，产生下一个偶数。\n*   这个过程一直持续到`for`循环结束，Coroutine 返回最后一个偶数。\n\n要使用该功能，我们的`main()`代码不会改变:\n\n```cpp\nint main(void)\n{\n    for (const auto &num : even_numbers(0, 10)) {\n        std::cout << num << ' ';\n    }\n\n    std::cout << '\\n';\n}\n```\n\n不同的是，我们不是返回`std::vector`，而是返回 Coroutine 提供的整数。"
  },
  {
    "path": "docs/adv-cpp-prog-cb/README.md",
    "content": "# C++ 高级编程秘籍\n\n> 原书：[Advanced C++ Programming Cookbook](https://libgen.rs/book/index.php?md5=24E080E694C59B3F8E0220D0902724B0)\n> \n> 协议：[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)\n> \n> 阶段：机翻（1）\n> \n> 自豪地采用[谷歌翻译](https://translate.google.cn/)\n> \n> 让开源界提前感受入关。\n\n* [在线阅读](https://ccpp.apachecn.org)\n* [在线阅读（Gitee）](https://apachecn.gitee.io/apachecn-c-cpp-zh/)\n* [ApacheCN 学习资源](http://docs.apachecn.org/)\n\n## 贡献指南\n\n本项目需要校对，欢迎大家提交 Pull Request。\n\n> 请您勇敢地去翻译和改进翻译。虽然我们追求卓越，但我们并不要求您做到十全十美，因此请不要担心因为翻译上犯错——在大部分情况下，我们的服务器已经记录所有的翻译，因此您不必担心会因为您的失误遭到无法挽回的破坏。（改编自维基百科）\n\n## 联系方式\n\n### 负责人\n\n* [飞龙](https://github.com/wizardforcel): 562826179\n\n### 其他\n\n*   在我们的 [apachecn/apachecn-c-cpp-zh](https://github.com/apachecn/apachecn-c-cpp-zh) github 上提 issue.\n*   发邮件到 Email: `apachecn@163.com`.\n*   在我们的 [组织学习交流群](http://www.apachecn.org/organization/348.html) 中联系群主/管理员即可.\n\n## 赞助我们\n\n![](http://data.apachecn.org/img/about/donate.jpg)\n"
  },
  {
    "path": "docs/adv-cpp-prog-cb/SUMMARY.md",
    "content": "+   [C++ 高级编程秘籍](README.md)\n+   [零、前言](00.md)\n+   [一、库的开发入门](01.md)\n+   [二、将异常用于错误处理](02.md)\n+   [三、实现移动语义](03.md)\n+   [四、将模板用于泛型编程](04.md)\n+   [五、并发和同步](05.md)\n+   [六、优化代码以提高性能](06.md)\n+   [七、调试和测试](07.md)\n+   [八、创建和实现您自己的容器](08.md)\n+   [九、探索类型擦除](09.md)\n+   [十、对动态分配的深入研究](10.md)\n+   [十一、C++ 中的常见模式](11.md)\n+   [十二、更仔细查看类型推导](12.md)\n+   [十三、奖励——使用 C++ 20 特性](13.md)\n"
  },
  {
    "path": "docs/app-dev-qt-creator/00.md",
    "content": "# 零、前言\n\n这本最新版本的 Qt Creator 图书让您深入了解 Qt 的海量库及其功能。Qt 是一个强大的开发框架，可作为构建跨平台应用的完整工具集，帮助您缩短开发时间并提高工作效率。\n\n这本 Qt 编程书籍将带您了解 Qt Creator 的最新特性，例如 Qt Quick Controls 2、增强的 CMake 支持、一个新的 SCXML 图形编辑器和一个模型编辑器。 您将从设计用户界面开始，使用 Qt Quick 处理多媒体和传感器，最后使用 Qt Creator 为移动、物联网和嵌入式设备开发应用。 通读后，您将能够通过了解 Qt 的核心功能来在 Qt 中建立坚实的基础，并使用 Qt Creator 和 C++ 编程语言从头开始创建您自己的跨平台应用。\n\n# 这本书是写给谁的？\n\n本书面向想要使用强大的 Qt 开发工具和库进行 GUI 编程的初学者和有经验的程序员。 本书适合习惯于用 C++ 语言进行面向对象编程的核心程序员，也适合那些面向设计、只想学习如何使用 Qt Quick 创建美观直观的 GUI 的人。\n\n# 这本书涵盖了哪些内容\n\n[第 1 章](01.html)，*Qt Creator 入门*涵盖了开始下载 Qt Creator for Linux、MacOS X 和 Windows 所需的所有内容。 我们还将了解如何确保您的基本配置正在运行，并快速了解一个简单的 QtGui 应用以及一个 Qt Quick 应用。\n\n[第 2 章](02.html)，*使用 Qt Creator*构建应用，说明如何向项目添加文件，如何在项目中创建库，以及如何使用调试器和控制台记录器。\n\n[第 3 章](03.html)，*使用 Qt Designer 设计您的应用*，介绍了 Qt 的信号和槽的概念，解释了如何使用 Qt Designer 创建用户界面。 我们还将了解如何实例化表单、消息和对话框。\n\n[第 4 章](04.html)，*Qt Foundations*讨论了一些 Qt 核心类，您会发现这些类在编写应用时特别方便。 我们将从有用的数据类开始，看看 Qt 对多线程的支持，这是确保应用具有响应性的关键工具。 我们将介绍文件和 HTTP I/O，这是许多应用中的一个重要组件。 我们还将学习如何使用 Qt 的 XML 解析器创建网络应用以及从文件系统加载 XML 数据。\n\n[第 5 章](05.html)，*使用 Qt 小部件开发应用*，介绍了使用 Qt 小部件的 GUI 编程。 您将学习基本的应用管理、如何创建对话框和错误弹出窗口，以及其他主要的 GUI 元素。 我们还将了解 Qt 灵活的布局系统、模型-视图-控制器(Model-View-Controller)范例，以及如何在 Qt 中将其用于复杂控件(如列表和树视图)。 我们还将快速了解 Qt 对 WebKit 的支持。\n\n[第 6 章](06.html)，*使用 Qt*绘图，说明如何在 Qt 中绘制通用图。 我们将实现位图的屏幕外绘图的具体示例，以及创建与 Qt 小部件互操作的自定义小部件。 我们还将讨论 Qt 为图形管理提供的更新、更低级别的抽象--图形视图/图形场景架构。\n\n[第 7 章](07.html)，*使用 Qt Quick*做更多事情，更详细地介绍了 Qt Quick。 我们将研究用于显示形状、图像和文本的基本 Qt Quick 构造，以及如何管理用户事件。 您还将了解 Qt 快速转换框架和 SCXML 的新图形编辑器。 您还将学习如何将 C++ 与 Qt Quick 集成。\n\n[第 8 章](08.html)，*使用 Qt Quick 实现多媒体*，详细介绍了 Qt Quick 对多媒体的支持。 我们将介绍各种 Qt Quick 组件，它们提供音频和视频播放，以及如何访问摄像头。\n\n[第 9 章](09.html)，*传感器和 Qt Quick*介绍了 Qt 的传感器和定位框架，因为它们在 QML 中得到了支持。 您将学习如何确定设备在地球表面的位置，以及如何测量其机载传感器所报告的环境的其他特征。\n\n[第 10 章](10.html)，*使用 Qt 语言学家*本地化您的应用，解释了本地化任务，并讨论了 Qt 为本地化提供的各种工具。\n\n[第 11 章](11.html)，*使用 Qt Creator 优化性能*，介绍如何使用 QML 性能分析器执行 QML 应用的运行时评测，并说明如何读取它生成的报告。\n\n[第 12 章](12.html)，*使用 Qt Creator 开发移动应用*解释了如何编写移动应用，以及 Qt 如何为 iOS 和 Android 应用提供更好的支持。\n\n[第 13 章](13.html)，*使用 Qt Creator 进行嵌入式和物联网开发*介绍了如何创建专门为嵌入式设备设计的优化 Qt 应用。\n\n[第 14 章](14.html)，*Qt 提示和技巧*介绍了一组您在使用 Qt Creator 和 Qt 时应该熟悉的提示和技巧。\n\n# 为了最大限度地利用这本书\n\n虽然不需要具备 Qt 和 Qt Creator 的先验知识，但掌握 C++ 编程的基本知识会很有帮助。\n\n# 下载示例代码文件\n\n您可以从您的帐户[www.Packt.com](http://www.packt.com)下载本书的示例代码文件。 如果您在其他地方购买了本书，您可以访问[https://www.packtpub.com/support](https://www.packtpub.com/support)并注册，让文件直接通过电子邮件发送给您。\n\n您可以通过以下步骤下载代码文件：\n\n1.  登录或注册[www.Packt.com](http://www.packt.com)。\n2.  选择支持选项卡。\n3.  单击 Code Downloads(代码下载)。\n4.  在搜索框中输入图书名称，然后按照屏幕上的说明进行操作。\n\n下载文件后，请确保使用以下最新版本解压缩或解压缩该文件夹：\n\n*   WinRar/7-用于 Windows 的 Zip\n*   适用于 Mac 的 Zipeg/iZip/UnRarX\n*   Linux 版 7-Zip/PeaZip\n\n这本书的代码包也托管在 giHub 的 https://github.com/PacktPublishing/Application-Development-with-Qt-Creator-Third-Edition 上。如果代码有更新，它将在现有的 giHub 存储库中更新。\n\n我们还有来自我们丰富的图书和视频目录的其他代码包，请访问**[https://github.com/PacktPublishing/](https://github.com/PacktPublishing/)**。 看看他们！\n\n# 下载彩色图像\n\n我们还提供了一个 PDF 文件，其中包含本书中使用的屏幕截图/图表的彩色图像。 您可以在此处下载：[https://static.packt-cdn.com/downloads/9781789951752_ColorImages.pdf](https://static.packt-cdn.com/downloads/9781789951752_ColorImages.pdf)。\n\n# 使用的约定\n\n本书中使用了许多文本约定。\n\n`CodeInText`：指示文本中的代码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄。 这里有一个例子：“将`red`文件的可执行内容的`event`属性设置为`goGreen`，并将其延迟设置为`2s`。”\n\n代码块设置如下：\n\n```cpp\nWindow {\n    visible: true\n    width: 360\n    height: 360\n    Rectangle {\n```\n\n当我们希望您注意代码块的特定部分时，相关行或项将以粗体显示：\n\n```cpp\n#include <QQmlContext> \n#include \"nativeobject.h\" \n\nint main(int argc, char *argv[])\n{\n    QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);\n    QGuiApplication app(argc, argv);\n```\n\n任何命令行输入或输出都如下所示：\n\n```cpp\nqmake -project\n```\n\n**粗体**：表示您在屏幕上看到的新术语、重要单词或单词。 例如，菜单或对话框中的单词显示在文本中，如下所示。 下面是一个示例：“下面的屏幕截图向您展示了在”属性“窗口中设置值的位置。”\n\nWarnings or important notes appear like this. Tips and tricks appear like this.\n\n# 保持联系\n\n欢迎读者的反馈。\n\n**一般反馈**：如果您对本书的任何方面有疑问，请在邮件主题中提及书名，然后给我们发电子邮件至`customercare@packtpub.com`。\n\n**勘误表**：虽然我们已经竭尽全力确保内容的准确性，但错误还是会发生。 如果您在这本书中发现了错误，请向我们报告，我们将不胜感激。 请访问[https://www.packtpub.com/support/errata](https://www.packtpub.com/support/errata)，选择您的图书，单击勘误表提交表链接，然后输入详细信息。\n\n**盗版**：如果您在互联网上遇到任何形式的非法复制我们的作品，请您提供地址或网站名称，我们将不胜感激。 请拨打`copyright@packt.com`与我们联系，并提供该材料的链接。\n\n**如果您有兴趣成为一名作者**：如果有一个您擅长的主题，并且您有兴趣撰写或投稿一本书，请访问[Auths.Packtpub.com](http://authors.packtpub.com/)。\n\n# 评论\n\n请留下评论。 一旦你阅读并使用了这本书，为什么不在你购买它的网站上留下评论呢？ 这样，潜在读者就可以看到并使用您不偏不倚的意见来做出购买决定，我们 Packt 可以了解您对我们产品的看法，我们的作者也可以看到您对他们的书的反馈。 谢谢!\n\n有关 Packt 的更多信息，请访问[Packt.com](http://www.packt.com/)。"
  },
  {
    "path": "docs/app-dev-qt-creator/01.md",
    "content": "# 一、Qt Creator 入门\n\nQt Creator 是一个集成的软件开发环境，既支持传统的 C++ 应用开发，也支持使用 Qt 项目库(统称为**Qt**，发音为**CUIT**)的开发。\n\nQt 可以在商业许可下使用，也可以在 GPL v3 和 LGPL v2 下使用。 它的发展可以追溯到 1991 年。 在其生命的前 10 年，它是用于 Windows 和 X11 的跨平台工具包；到 2001 年，添加了对 MacOS X 的支持。\n\n在本章中，我们将介绍入门所需的所有内容，如下所示：\n\n*   从哪里下载 Qt Creator Linux、MacOS X 和 Windows 版\n*   最新版本 Qt 中的新功能\n*   如何确保基本配置正在运行\n*   快速了解简单的 Qt 小部件应用以及 Qt Quick 应用\n\n# 技术要求\n\n本章的技术要求包括 Qt 5.12.3 MinGW 64 位、Qt Creator 4.9.0 和 Windows 10。\n\n本章的代码可在[https://github.com/PacktPublishing/Application-Development-with-Qt-Creator-Third-Edition/tree/master/Chapter01](https://github.com/PacktPublishing/Application-Development-with-Qt-Creator-Third-Edition/tree/master/Chapter01)中找到。\n\n# 下载 Qt 和 Qt Creator\n\nQt 是 Qt Creator 背后的跨平台工具包，有着悠久而辉煌的历史。 目前是 Qt 公司的一个项目，它有自己的 url：[http://www.qt.io](http://www.qt.io)。 它还拥有商业和非商业许可证。 要开始免费使用非商业版本，请访问[http://www.qt.io/download-qt-installer](http://www.qt.io/download-qt-installer)。 您应该会看到类似以下屏幕截图的内容：\n\n![](img/3e801ddf-47a7-4c86-a851-5ec34ddf751d.png)\n\nOne of the most popular platforms for application development with Qt is Linux. On many Linux variants—notably Ubuntu, my personal favorite – you can get Qt Creator using the package manager. On my Ubuntu box, Qt Creator is just a `sudo apt-get install qtcreator` command away. You'll get a version of Qt that matches your flavor of Linux, although it might not be the latest and greatest build from The Qt Company. If you need to install the latest official version of Qt, it's recommended to download it from the preceding link.\n\n一些下载内容包括您需要的 C++ 编译器和链接器。 通过点击链接并下载 Qt，您现在应该已经拥有了 Qt、Qt Creator 和用于在 Windows 上开发软件的 MinGW 工具包。 如果您在 Linux 或 Mac 上进行开发，过程将类似，尽管它不会在您的开发中包含 MinGW。 在 Windows 上，有一个包含 MinGW 工具链的变体，因此您拥有构建应用所需的一切。\n\n但是，您也可以下载 Qt Creator for Windows，它使用 Microsoft Visual Studio 编译器。 因此，如果您更喜欢使用 Visual Studio 进行编译，使用 Qt Creator 作为 IDE，这也是一个选择。 在 MacOSX 上，您需要首先安装 Xcode 和命令行开发工具；您可以从 MacOSX 应用商店下载 Xcode，然后使用 Xcode 下载命令行开发工具。\n\n下载安装程序后，按常规方式运行它，它将启动适用于您的平台的安装向导。 根据您运行的是**在线安装程序**还是**离线安装程序**，后者的安装通常需要 3 到 4 分钟；但是，如果您运行的是在线安装程序，则可能需要几个小时。 这是因为当您运行安装过程时，在线安装程序会从 Qt 服务器下载每个未压缩的工具和库文件。 另一方面，离线安装程序包含所有工具和库，它们都是以高度压缩的格式打包在安装程序本身中的，这就是为什么安装过程相对更快、更简单的原因，但与在线安装程序相比，安装程序的尺寸更大。\n\n除此之外，您还需要有足够的磁盘空间。 Qt Creator 不会消耗那么多磁盘空间，但软件开发会消耗那么多磁盘空间；工具和库至少需要 500MB 的空闲空间，并且需要在主驱动器上预算几 GB 的空闲空间来存放源代码、中间目标文件、调试符号，当然还有编译后的应用。 (如果您在虚拟机上运行 Qt Creator，为此做好计划尤其重要；请确保虚拟机映像的虚拟硬盘驱动器有足够的磁盘空间。)\n\n您还应该确保您的开发箱有足够的 RAM；越多越好。 Qt Creator 在 2 GB 的 RAM 上运行得很愉快，但是如果有更多的 RAM 可用，Qt Creator 使用的编译器和链接器可以运行得更快。\n\n# Qt 中的新功能\n\nQt 开发人员不断向 Qt 添加新功能，同时修复影响其用户的关键错误。 这意味着我们可以期待 Qt 的每一次更新都有新功能，特别是在一个主要版本中。 在撰写本章时，Qt 的最新稳定版本是 5.12.3，这意味着它是其主要版本 5.12 的第三个小更新。\n\n自本书第二版以来，Qt 的一些重要变化如下：\n\n*   Qt WebView(WebKit)已被弃用，取而代之的是 Qt WebEngine(Chromium)。\n*   MinGW 64 位编译器现在包含在 Qt 的 Windows 安装程序中。\n*   移动平台增加了许多功能，包括支持应用内购买。\n*   Qt 脚本、Qt 快速控件 1 和 Qt Canvas 3D 已弃用。\n*   添加了对新平台的支持，如 twOS 和 watchOS。\n*   以前仅供商业使用的功能，如 Qt 图表、Qt 数据可视化、Qt 虚拟键盘、Qt 采购和 Qt Quick 2D 渲染器现在都是免费的。\n*   添加了对嵌入式平台的支持。\n*   增加了 Qt Automotive Suite。\n*   添加了 Python 的 Qt 绑定(使用 PySide 2 模块)。\n*   新的信号和插槽连接语法--现在您可以直接将信号连接到 C++ 11 的 lambda 函数。\n*   添加了对 JSON 格式的支持。\n*   增加了 Qt 3D Studio。\n*   在 Qt Creator 中添加了 SCXML 和状态机工具。\n\n...除此之外还有更多！\n\nTo learn more about the new features and changes in the latest Qt release, please check out the official introduction for Qt 5 at [https:/](https://doc.qt.io/qt-5/qt5-intro.html)[/doc.qt.io/qt-5/qt5-intro.html](https://doc.qt.io/qt-5/qt5-intro.html), or head over to the wiki page at [https://wiki.qt.io/Main](https://wiki.qt.io/Main).\n\n# 熟悉 Qt Creator\n\n下面的屏幕截图显示了第一次启动 Qt Creator 时您将看到的内容。 让我们仔细看看屏幕的每个部分：\n\n![](img/1516f84d-2563-4489-a736-5931764a1711.png)\n\n主窗口(当前显示 New Project 和 Open Project 的按钮)是您的工作区。 工作区还包括指向 Qt 项目、示例和教程的链接，以及 Qt 的开发人员文档，如其在线社区和博客。 在正常情况下，这将位于您将看到应用源代码的位置。 屏幕左侧有一系列图标，可让您选择应用中的各种视图。 这些建议如下：\n\n*   欢迎模式，显示有关 Qt Creator 的基本信息\n*   编辑模式，允许您编辑组成应用的文件\n*   设计模式，允许您使用 Qt Designer 来设计应用的用户界面\n*   调试模式，允许您在应用运行时对其进行调试，包括执行诸如查看内存和变量、设置断点以及单步执行应用等操作\n*   项目模式，该模式允许您调整项目的生成和链接设置\n*   分析模式，允许您分析应用的运行时性能\n*   帮助模式，提供有关 Qt Creator 和 Qt 框架的文档\n\n让我们使用 C++ 创建一个新项目。\n\n# 您的第一个应用-Hello World\n\n在 Qt Creator 中，从文件菜单中选择新建文件或项目向导。 Qt Creator 将向您显示 New File or Project 向导，该向导允许您选择要创建的项目类型，并为其命名，等等。 要创建第一个应用，请执行以下步骤：\n\n1.  如果尚未选择“新建文件”或“项目名称”，请选择“新建文件”或“项目名称”。\n2.  Qt Creator 向您展示了一个对话框，其中包含一系列令人眼花缭乱的项目选择。 选择应用，然后选择 Qt 控制台应用，然后单击选择...。\n3.  Qt Creator 要求您输入要存储项目文件的目录的名称和路径。 对于名称，请输入`HelloWorldConsole`，然后选择对您有意义的路径(或接受默认路径)。 然后，单击 Next(下一步)。\n4.  Qt Creator 会询问您要用于项目的构建系统。 如果您对此没有任何具体要求，只需保留默认选项：qmake。 然后，单击 Next(下一步)：\n\n![](img/c2ca53fb-14c4-4f68-94ba-acf9cacc9e55.png)\n\n5.  Qt Creator 可以支持各种工具包和库，您可以根据这些工具包和库构建应用。 选择桌面 Qt 工具包，默认情况下应已安装该工具包。 如果您在 Windows 上运行 Qt，请确保使用**MinGW**选择桌面 Qt 工具包，因为它是默认安装的。 如果选择桌面 Qt**MSVC**工具包，请确保事先安装了 Microsoft Visual Studio。 然后，单击 Next(下一步)。\n6.  下一步，Qt Creator 会提示您有关项目的版本控制系统。 Qt Creator 可以使用您安装的版本控制客户端来执行项目的更改跟踪。 目前，跳过此选项，并将“添加到版本控制”设置为“无”。 然后，单击 Finish。\n\nQt Creator 将创建您的项目并切换到编辑视图。 在`main.cpp`文件的源代码编辑器中，输入突出显示的代码：\n\n```cpp\n#include <QCoreApplication> \n#include <iostream> \n\nusing namespace std; \n\nint main(int argc, char *argv[]) \n{ \n    QCoreApplication a(argc, argv); \n    cout << \"Hello world!\"; \n\n    return a.exec(); \n} \n```\n\nYou can download the example code files for all Packt books you have purchased from your account at [http://www.packtpub.com](http://www.packtpub.com). If you purchased this book elsewhere, you can visit [http://www.packtpub.com/support](http://www.packtpub.com/support) and register to have the files emailed directly to you.\n\n`QCoreApplication`任务处理应用的整个系统启动，每个 Qt 控制台应用都需要创建一个并调用其`exec`方法作为`main`方法的一部分。 它设置 Qt 的事件处理程序，并提供一组移植帮助器来确定应用目录、库路径和其他详细信息。\n\n对于控制台应用，这就是您需要的全部内容；您可以自由地将 Qt 类与 C++ 标准库和**标准模板库**(**STL**)混合搭配，尽管一旦您掌握了 Qt 的基础类，许多 STL 构造可能会感觉有些受限。\n\n接下来，让我们编译并运行该应用。 有几种方法可以做到这一点：\n\n1.  单击左侧帮助视图按钮下面的绿色运行箭头以运行应用，如下所示：\n\n![](img/9d95adec-74e9-4e12-8a30-5add54f70977.png)\n\n2.  点击*F5*在调试器中构建并运行您的应用。\n3.  从 Debug(调试)菜单中单击 Start Debug(开始调试)，如下所示：\n\n![](img/c0039124-1df6-40a2-92a3-81c61f28be24.png)\n\n4.  单击绿色的 Run 箭头，并将 Bug 放在箭头上方，以便调试左侧的应用。\n5.  从 Build 菜单中选择 Run(或按*Ctrl*+*R*)。\n\nIf you only want to build the application, you can click on the hammer icon under the Run and Debug icons.\n\n应用启动后，您将看到 Hello world！ 控制台视图中的消息，如下所示：\n\n![](img/18617764-fa1e-4cc0-8728-b0b6093ec0a6.png)\n\n当您选择这些选项之一时，Qt Creator 将调用编译器和链接器来构建您的应用。 如果选择 Debug 选项，Qt Creator 会在启动应用时切换到 Debug 视图(我们将在[第 2 章](02.html)，*使用 Qt Creator*构建应用中详细讨论)。\n\n# 使用 Qt 小部件库的 Hello World\n\nQt 的优势之一是其丰富的 GUI 元素集合，您可以使用这些元素来创建有窗口的应用。 创建 GUI 应用在原则上类似于创建控制台应用；当您选择 New File 或 Project 时，从出现的 New 对话框中选择 Qt widgets Application，而不是选择 Qt 控制台应用。 现在就试试吧：\n\n1.  首先，通过单击文件菜单中的关闭所有项目和编辑器来关闭当前文件和项目。\n2.  接下来，再次单击 New File 或 Project，然后从向导的第一步单击 Qt Widgets Application。\n3.  再次浏览向导，将您的项目命名为`HelloWorldGui`。\n\n4.  然后，选择默认套件。 New project 向导将提示您输入实现主窗口的类的名称。 保留`QMainWindow`子类不变，名称为`MainWindow`。 跳过向导的生成系统和版本控制对话框部分。\n\nQt Creator 在`mainwindow.h`和`mainwindow.cpp`文件中创建提供平台基本窗口的类的默认子类，并创建包含应用窗口小部件的表单。\n\n下面的屏幕截图显示了您在 Qt Designer 中编辑的默认表单。 如果此时运行应用，您将看到一个空窗口。 相反，在 Qt Creator 的项目树(第二个窗格)中双击`Forms`文件夹，然后双击`mainwindow.ui`文件。 Qt Creator 切换到`Design`视图，您将看到类似以下屏幕截图的内容：\n\n![](img/99606097-d4fb-4fbe-a686-4257eabe4a4d.png)\n\n正如您从前面的屏幕截图中看到的，左侧是一个布局列表，您可以选择这些布局来组织小部件。 其中包括间隔符、视图、容器、按钮和其他小部件；除此之外，还有各种编辑和布局选项。 窗口中间是应用主窗口布局的预览。 再往右是一些窗格，它们显示主窗口中对象的层次结构以及您在主窗口中单击的任何项目的属性。\n\n# 在 Qt Designer 中放置小工具\n\n虽然我们将在[第 3 章](03.html)，*使用 Qt Designer 设计您的应用*中更多地探讨 Qt Designer，但是您可以通过构建一个简单的 UI 来体验它的使用。 首先确保您处于设计器模式，然后按以下步骤操作：\n\n1.  在显示“在此键入”的位置，单击鼠标右键并选择“删除菜单栏”。\n2.  拖动一个标签(在左侧窗格中的 Display Widgets 下)，并将其放入中间窗格的窗口预览中。\n3.  双击出现的标签，然后键入`Hello world!`。\n\n4.  抓住标签的一角并调整其大小，以便显示整个文本。 您也可以在窗口中移动它。\n5.  请注意，当您单击标签时，右下角窗格中的属性字段将更新以显示新标签的属性。\n6.  拖动一个按钮(位于左侧窗格中的按钮下)，并将其放入中间窗格的窗口预览中。\n7.  双击该按钮并将其文本更改为`Exit`。\n8.  选择新按钮后，将属性浏览器中的 objectName 字段更改为`exitButton`。 您必须遵循此处描述的名称，以便在添加 Slot 函数时生成的代码与下一个示例代码片段中显示的代码相同。\n9.  右键单击该按钮，然后选择 Go to Slot...(转到插槽...)。 将出现一个带有插槽列表的窗口(目前，您可以将插槽视为在某个操作上触发的东西；我们将在[第 2 章](02.html)，*使用 Qt Creator 构建应用*中详细讨论它们)。\n10.  从显示的列表中选择已单击()。\n11.  Qt Creator 返回到您的`mainwindow.cpp`文件的编辑视图。 将其更改为如下所示：\n\n```cpp\n#include \"mainwindow.h\" \n#include \"ui_mainwindow.h\" \n#include <QApplication> \nMainWindow::MainWindow(QWidget *parent) : \n    QMainWindow(parent), \n    ui(new Ui::MainWindow) \n{ \n    ui->setupUi(this); \n} \n\nMainWindow::~MainWindow() \n{ \n    delete ui; \n} \n\nvoidMainWindow::on_exitButton_clicked() \n{ \n    QApplication::exit(); \n}\n```\n\n在运行应用之前，让我们确保您了解`MainWindow`类的实现。 `MainWindow`类的构造函数加载主窗口的用户界面描述，并使用 Qt Creator 生成的`Ui::MainWindow`类设置它。 析构函数删除代码布局的实现，`on_exitButton_clicked`方法通过调用`QApplication`类实现的`exit`静态方法简单地终止应用。\n\n最后，我们必须将`on_exitButton_clicked`方法声明添加到`mainwindow.h`中(如果尚未添加)。 在左侧浏览器中双击此文件，并确保其内容如下所示：\n\n```cpp\n#ifndef MAINWINDOW_H \n#define MAINWINDOW_H \n\n#include <QMainWindow> \n\nnamespaceUi { \nclass MainWindow; \n} \n\nclass MainWindow : public QMainWindow \n{ \n    Q_OBJECT \n\npublic: \n    explicit MainWindow(QWidget *parent = 0); \n    ~MainWindow(); \n\nprivate slots: \n    void on_exitButton_clicked(); \n\nprivate: \n    Ui::MainWindow *ui; \n}; \n\n#endif // MAINWINDOW_H \n```\n\n您需要添加的关键行在前面的清单中突出显示。\n\n我们将在下一章学习更多关于信号和槽的内容；现在，您只需知道您声明的是一个私有函数，当您单击该按钮时将触发该函数。\n\n运行应用。 它应该会打开一个带有文本 Hello World！的窗口；单击窗口中的退出按钮(或右上角的关闭框按钮)应该会关闭应用：\n\n![](img/c58f4ae3-13b8-405a-9bd7-49fe2bf16c1e.png)\n\n此时，如果您想了解有关 Qt 小部件应用的更多信息，请继续尝试将其他 GUI 项拖到窗口，或者通过切换到帮助视图并单击帮助项列表中的 Qt GUI 来浏览 Qt 小部件应用的帮助文档。\n\n# Hello World Using Qt Quick\n\nQt Quick 是 Qt 较新的用户界面声明性框架。 有了这一点，创建具有动画过渡和流畅用户界面的流畅应用就变得非常容易。 使用 Qt Quick，您可以使用 QML 来描述您的用户界面，QML 是一种类似 JavaScript 的语言，允许您声明用户界面元素以及它们之间的关系；Qt Quick 运行时完成了应用实现中的大部分繁重任务。\n\n至此，您可以猜测如何创建一个 Qt Quick 项目。 从文件菜单中选择新建文件或项目，单击 Qt Quick Application-Empty，然后按照向导操作。\n\n向导将询问您一个额外的问题：要使用的 Qt 快速版本。 您只需选择最新版本即可。 完成向导后，您将得到一个简单的应用，该应用在其自己的窗口中实际显示 Hello World。 它提供的代码如下：\n\n```cpp\nimport QtQuick 2.12\nimport QtQuick.Window 2.12\n\nWindow {\n    visible: true\n    width: 640\n    height: 480\n    title: qsTr(\"Hello World\")\n}\n```\n\n如果您了解 JavaScript，那么它的语法可能看起来有点眼熟，但仍然是不同的。 前两行是`import`语句；它们指示哪些类应该可用于 QML 运行时。 至少，您的所有 Qt Quick 应用都必须导入`QtQuick`，就像本例所做的那样。\n\nQML 紧随其后。 它声明了一个 640x480 像素的窗口对象，该对象决定了应用窗口的大小。 在窗口内部，除了窗口大小之外，我们还可以看到另外两个属性：`visible`和`title`。`visible`属性只是表示是否要默认显示项目，在本例中我们是这样做的。 `title`属性就是：放置在应用窗口标题上的`Hello World`文本。 请注意，`title`属性的值实际上是对`qsTr`函数的函数调用的结果，该函数是 Qt 的内置本地化函数。 这将查看应用资源以返回`Hello World`的本地化版本(如果已提供)。\n\n值得一提的是，这里有必要使用这个函数`qsTr`；如果您希望您的应用支持多语言，则可以使用此函数。 函数的作用是根据所选的区域设置以不同的语言显示文本。 如果您不打算支持多种语言，可以忽略它。\n\n此时，您可以按常规方式运行应用，您将看到一个窗口，窗口标题为文本 Hello World。\n\n虽然基本概念和原理相似，但由于 Qt Quick Designer 的本质，Qt Quick Designer 实际上与 Qt Widgets Designer 有很大不同--Qt Quick Designer 专门为创建基于触摸的应用进行了优化，而 Qt Widget 则是为创建桌面程序而设计的。 在我们演示 Qt Quick Designer 之前，让我们先转到“文件”、“新建文件”或“项目”来创建一个 QtQuick UI 文件，然后在“Qt”类别下选择“QtQuick UI 文件”。 之后，使用`MyGui`的组件名称，并保留`MyGuiForm`的组件表单名称。 然后，按 Next(下一步)，然后按 Finish(完成)按钮。\n\n完成后，Qt Creator 会将`MyGui.ui.qml`和`MyGui.qml`添加到您的项目中，Qt Quick Designer 将自动启动。 请看下面的屏幕截图：\n\n![](img/e2c45814-d273-4e24-b105-531caa773804.png)\n\n它显示了可以添加到画布的内容列表，以及画布上对象的层次结构，以及各个对象的属性。\n\n然而，与 Qt 小部件相比，可以使用的 Qt Quick 小部件要少得多。 除此之外，Qt Quick 中的小部件与原生平台的外观和感觉几乎在相同程度上不匹配。 这是经过设计的；Qt 小部件用于通过使用具有本机外观的本机控件来构建与本机平台匹配的传统应用，而 Qt Quick 则用于创建具有自己外观的独立于设备的应用。 例如，您可能会使用 Qt 小部件编写企业数据收集应用，而使用 Qt Quick 创建媒体中心应用。\n\n但是，在这两种情况下使用设计器的方式是相同的。 让我们将`mouseArea`添加到主视图中，并让它有所作为：\n\n1.  从 Qt Creator 的文件列表中选择`MyGuiForm.ui.qml`，然后单击设计以查看设计视图。\n2.  在“库”窗格中，选择“QML 类型”并向下滚动，直到看到“矩形”。 将矩形拖到中央窗格，并将其放在名为 Item 的父对象下的某个位置。\n3.  在窗格中选择矩形后，在“颜色”下选择矩形的红色。\n4.  在选中矩形对象的情况下，单击属性选项卡中的布局选项卡，然后将鼠标悬停在布局上，直到您看到填充到父对象为止。 (这是 The Anchors 下的第五个图标，看起来像一个有边框的方框。)。 点击它。\n5.  现在，将一个 MouseArea 对象拖出 Library 窗格，并将其放到新矩形上。\n6.  选择 MouseArea 对象后，重复*步骤 4*使其填充父 Rectangle 对象。\n7.  单击 MouseArea 的`id`属性旁边带有箭头的小圆圈图标。 一旦点击图标就会变成红色。 这将允许其他 QML 脚本访问此对象。\n8.  对 Rectangle 对象也重复*步骤 7*。\n9.  返回到编辑视图并修改`main.qml`，使其与以下代码片段类似：\n\n```cpp\nimport QtQuick 2.12\nimport QtQuick.Window 2.12\n\nWindow {\n    visible: true\n    width: 640\n    height: 480\n    title: qsTr(\"Hello World\")\n\n    MyGuiForm {\n anchors.fill: parent\n mouseArea.onClicked: {\n rectangle.color = \"blue\";\n }\n }\n}\n```\n\n在较新版本的 Qt 中，QML 脚本分为两种格式：`.qml`和`.ui.qml`。 第一种格式(`.qml`)用于编写用户触发 GUI 事件时要执行的逻辑和操作。 第二种格式(`.ui.qml`)仅用于您的 GUI 的表面定义--对象的位置、对象的大小和颜色等。\n\n您可以看到，对`MyGui.ui.qml`文件所做的所有更改都是在 Design 视图中完成的；对于`main.qml`，我们必须使用文本编辑器来编写前面演示的逻辑代码。 您可以直接在`main.qml`中使用`MyGuiForm`类，并告诉它在用户按下鼠标区域按钮时要做什么。 您需要设置 MouseArea 的默认 ID，以便`onClicked`处理程序知道哪个对象将触发事件(在本例中，它使用默认名称：`mouseArea`)。 `id`属性还允许其他 QML 按名称访问矩形(在本例中，其名称只是默认名称)，当用户按下 MouseArea 项时，`onClicked`处理程序会将矩形项的 color 属性更改为蓝色。\n\n运行应用。 您将看到红色的矩形填充整个窗口，单击该矩形会将其颜色更改为蓝色。\n\n# 简略的 / 概括的 / 简易判罪的 / 简易的\n\n获取 Qt Creator 很简单；在大多数 Linux 平台上，可以通过 web 下载，也可以通过本地包管理器进行可选安装(尽管包管理器提供的版本可能比您从 Qt 项目网站获得的版本稍早)。\n\nQt Creator 在项目中为您组织其源代码；当您第一次启动它时，您可以创建一个默认项目，也可以创建一个新项目来包含应用的源代码和资源。 Qt Creator 中提供了编译和调试应用所需的所有选项。 此外，它还支持用于开发 Qt 小部件和 Qt Quick 应用的设计器工具。\n\n在下一章中，我们将深入研究如何配置 Qt Creator 来编译和编辑您的代码，包括如何向项目添加源文件、配置编译器和链接器选项、向第三方库添加依赖关系等等。"
  },
  {
    "path": "docs/app-dev-qt-creator/02.md",
    "content": "# 二、使用 Qt Creator 构建应用\n\n使用 Qt Creator 要做的第一件事是弄清楚如何添加源文件和构建(或调试)您的项目。 本章就是这样--我们将介绍如何向项目中添加文件，如何在项目中创建库，以及如何使用调试器和控制台记录器。 这些是使用 Qt 创建高质量应用所需的基本技能。 在本章结束时，您将驾驶 Qt Creator 像专业人员一样开发控制台应用。\n\n在本章中，我们将执行以下操作：\n\n*   了解我们的样本库\n*   查看构建菜单和`.pro`文件\n*   链接到我们的样本库\n*   调试 / 除错 / 除去窃听器\n*   建造我们的项目\n*   运行并调试我们的应用\n\n# 技术要求\n\n本章的技术要求包括 Qt 5.12.3 MinGW 64 位、Qt Creator 4.9.0 和 Windows 10。\n\n# 入门-我们的样本库\n\n本章的示例代码有两部分：定义公共函数的库和调用此函数的控制台应用。 库是分解应用的一种很好的方法，虽然这个示例很简单，但它也让我向您展示了如何创建库并将其包含在您的应用中。\n\n我将扩展您的想象力；让我们假设您负责设置一个数学函数库。 在本例中，我们将只编写一个函数，即阶乘。 您应该能够回忆起介绍性编程中的阶乘函数；它由一个！ 定义如下：\n\n*   0!。 是 1\n*   1！ 是 1\n*   不！ 是 n×(n-1)！\n\n这是一个递归定义，我们可以用以下方式对其进行编码：\n\n```cpp\nunsigned long factorial(unsigned int n) \n{ \n    switch(n)  \n    { \n        case 0: return 1; \n        case 1: return 1; \n        default: return n * factorial(n-1); \n    } \n} \n```\n\n另一种避免函数调用成本的定义如下所示：\n\n```cpp\nunsigned long factorial(unsigned int n) \n{ \n    unsigned long result = 1; \n    for(unsigned int i = n; i > 1; i--) \n    { \n        result *= i; \n    } \n    return result; \n}\n```\n\n让我们首先创建一个实现`factorial`函数的库。 为此，请执行以下步骤：\n\n1.  在 Qt Creator 中，从“文件”菜单中选择“新建文件”或“项目”。\n2.  在对话框的左侧窗格中选择 Library，然后从中间窗格中选择 C++ Library：\n\n![](img/b14d9dc6-6fd0-4c8c-a440-ff28b31cff01.png)\n\n3.  Qt Creator 可以创建可以在应用之间共享的**动态链接****库**(Windows 术语中的**DLL**)、静态库或插件。 我们将创建一个静态库，因此在出现的下一个窗口中，选择 Static Linked Library 并将其命名为`MathFunctions`。 为项目选择一条合理的路径。\n\nA statically linked library is included in your program binary and is part of your application. If multiple applications use a static library, each will have its own copy. A dynamically linked library is stored as a separate file and can be shared by multiple applications at runtime because each application loads the dynamically linked library. Qt also supports plugins, which are dynamic libraries loaded at runtime that can extend an application's functionality.\n\n4.  Qt Creator 构建的库可以依赖于 Qt 库。 让这个库依赖于 Qt 的核心数据结构`QtCore`。 在 Select Required Modules(选择所需的模块)窗口中，选中`QtCore`，然后单击 Next(下一步)。\n5.  在下一个窗口中，命名 Qt Creator 将添加到项目中的骨架文件。 单击 Next(下一步)。\n6.  在 Project Management 窗口中，选择<none>作为版本控制选项(我们不会对此项目使用版本控制)，然后单击 Finish。</none>\n7.  编辑`mathfunctions.h`以包含阶乘函数的静态方法声明：\n\n```cpp\n#ifndef MATHFUNCTIONS_H \n#define MATHFUNCTIONS_H \n\nclass MathFunctions \n{ \npublic: \n    MathFunctions(); \n    static unsigned long int factorial(unsigned int n); \n}; \n#endif // MATHFUNCTIONS_H  \n```\n\n8.  打开`mathfunctions.cpp`。 您可以通过以下方式之一完成此操作：在 Project 窗格中双击它，右键单击阶乘函数并选择 Switch Header/Source，或者只需按下*F4*键。 编写您的`factorial`函数；`mathfunctions.cpp`现在应该包含类似以下内容的内容：\n\n```cpp\n#include \"mathfunctions.h\" \n\nMathFunctions::MathFunctions() \n{ \n} \n\nunsigned long int MathFunctions::factorial(unsigned int n) \n{ \n    switch(n) \n    { \n        case 0: return 0; \n        case 1: return 1; \n        default: return n * factorial(n-1); \n    } \n} \n```\n\n9.  单击左侧的 Projects 按钮，通过编辑 General 下的 Build 目录行，将 Release 和 Debug 构建的输出路径更改为指向同一目录。 首先对生成执行此操作，然后对调试生成配置执行此操作。 为此，请从生成目录路径中删除目录路径的发布和调试部分。 这样，在构建库时，Qt Creator 会将库的发布版本和调试版本分别放在名为`release`和`debug`的文件夹中：\n\n![](img/d34af19f-d7ac-4eb2-8e61-4b3c8b5a67ed.png)\n\n在编写代码时，请注意，Qt Creator 会在不同阶段提示您使用自动建议(称为**autossuggest**)从标题中推断出的内容。 例如，一旦您键入`MathFunc`的类名，它就会自动完成类名或 C 预处理器保护；您可以使用鼠标或只需点击*r**eturn*来选择类名。\n\n类似地，输入双冒号会告诉 Qt Creator 您正试图在`MathFunctions`类中输入内容，它会提示您输入`MathFunctions`类成员；您可以使用箭头选择`factorial`并点击*Return*，然后它就会输入该内容。\n\n最后，键入左圆括号将提示 Qt Creator 您正在定义一个函数，并提示您使用在头文件中定义的函数的参数。 当您键入代码时，您会经常看到这种自动完成；这也是学习 Qt 的一个很好的方法，因为您可以键入类名或函数名的一部分，Qt Creator 会在整个过程中提示您一些有用的提示。 Qt Creator 还可以自动补全变量和方法名；开始键入函数名，然后按*Ctrl*+空格键查看可能补全的菜单。\n\n在继续之前，请确保在发布和调试配置中构建您的库。 最简单的方法是单击软件左下角的构建选择器，选择 Release 或 Debug，然后单击锤子图标执行构建，如以下屏幕截图所示：\n\n![](img/9c6b0748-778f-4822-a033-464887a66eeb.png)\n\nA combination of *Ctrl *+ *B* offers a mouse-free shortcut to the Build menu.\n\n# 了解环境-使用 Build 菜单和.pro 文件\n\n在上一章中，您了解了如何通过点击 Qt Creator 主窗口角落的锤子按钮或启动调试器来构建应用。 要只构建您的库或任何应用，您可以使用锤子图标，也可以使用 Build 菜单中的各种选项。 显而易见的选择是全部构建或全部重建。 选择全部生成将仅重新编译 Qt Creator 识别的那些需要重新生成的文件；选择全部生成将清除项目中的所有目标文件，并从头开始重新生成整个项目。\n\n在大多数情况下，选择 Build All 就足够了，这就是您想要做的，因为这样更快。 有时，您确实希望重新构建整个项目，尤其是在出现问题、Qt 的 make 系统无法协调所有依赖项(或者您错误地指定了这些依赖项)的情况下。 选择立即构建并等待其构建，同时我们讨论其他选项。*了解景观-构建菜单和`.pro`文件。*您还可以通过选择全部清理来清理您的项目(删除所有对象文件和其他自动生成的产品)。\n\n发布选项可用于某些外接程序工具包，这些外接程序工具包允许您将编译后的应用和库发布到应用存储区和存储库；您可以在任何 Qt Creator 外接程序的文档中找到有关这方面的更多详细信息。\n\n每个 Qt Creator 项目背后都有一个`.pro`文件；它的功能与 Makefile 相同，实际上，它是由 Qt 工具包的 qmake 命令处理的。\n\nA Makefile is a file that describes how your application can be built using the make utility. For more information, go to [https://www.techopedia.com/definition/16406/make](https://www.techopedia.com/definition/16406/make). Qt provides qmake, a utility that converts the `.pro` files to Makefiles; you'll work with the Qt Creator GUI most of the time to create the `.pro` files and ignore the resulting Makefile.\n\n这些文件是声明性的，因为您声明了组成您的应用的文件之间的关系，而 qmake 计算出如何从那里构建您的应用。 在大多数情况下，您只需对`.pro`文件进行少量更改或不更改，但了解它们的工作方式并不会有什么坏处。 双击`MathFunctions.pro`，您会发现：\n\n```cpp\nQT -= gui \nTARGET = MathFunctions \nTEMPLATE = lib \nCONFIG += staticlib \nDEFINES += QT_DEPRECATED_WARNINGS\nSOURCES += mathfunctions.cpp \nHEADERS += mathfunctions.h \nunix { \n    target.path = /usr/lib\n    INSTALLS += target \n}\n```\n\n`.pro`文件的基本语法是变量赋值；Qt Creator 生成的文件赋值以下变量：\n\n*   `QT`：此变量指示项目将链接到的 Qt 模块。 默认情况下，所有项目都包括`QtCore`和`QtGui`；还有太多其他模块可用，其中包括许多关键功能，如`WebEngine`的网页浏览引擎(`QtWebEngine`)和多媒体库(`QtMultimedia`)。 我们在这里的赋值表明我们使用默认的 Qt 模块，但不将它们链接到`QtGui`。\n*   `TARGET`：此变量是编译后的库或可执行文件的名称。\n*   `TEMPLATE`：此变量指示 qmake 应该使用哪种模板来生成二进制文件。 在我们的例子中，我们说它应该使用模板来创建一个`lib`文件--一个静态库。\n*   `CONFIG`：此变量将额外的配置传递给 qmake 的模板。 这里，我们说我们需要一个静态链接库。\n*   `DEFINES`：此变量指定应在整个构建过程中设置的预处理器(`-D`选项)。 在这种情况下，qmake 在检测到我们的项目中使用了不推荐使用的特性时会显示警告消息。\n*   `SOURCES`和`HEADERS`：这些变量包含组成我们项目的源文件和头文件的列表。\n*   `INSTALLS`：此变量指示生成的构建产品应该安装在哪里。 这里，它设置在`scope`中。 作用域允许您在 qmake 中指定条件选项；作用域的条件是变量或表达式(可能为真也可能为假)，如果变量为真，则执行后面的代码。 本文件末尾的范围是，*如果我们是为 Unix 变体构建的，请将*`target.path`*变量设置为*`/usr/lib`*，将*`INSTALLS`和*变量设置为*`target`*。*\n\n这些是您可以在几乎任何`.pro`文件中找到的基本变量。\n\nFor more information on qmake scopes that you can use to control conditional compilation, see [https://doc.qt.io/qt-5/qmake-advanced-usage.html](https://doc.qt.io/qt-5/qmake-advanced-usage.html).\n\n您可能想知道的另一个额外变量是`LIBS`。 现在，`LIBS`表示 Qt Creator 应该将您的项目链接到的其他库。\n\n请注意变量的管理方式：使用`=`进行赋值，使用`+=`将项添加到列表，使用`-=`从列表中删除项。\n\n既然我们已经了解了 Build 菜单和`.pro`文件，让我们继续学习如何将我们刚刚构建的库合并到另一个项目中。\n\n# 链接到我们的样本库\n\n现在，让我们创建一个依赖于我们的库的应用。 我们的应用将调用库中的阶乘函数，并静态链接到库以访问阶乘函数。 为此，您需要执行以下步骤：\n\n1.  从文件菜单中选择关闭所有项目和编辑器。\n2.  从文件菜单中选择新建文件或项目，然后使用向导创建名为`MathFunctionsTest`的新 Qt 控制台应用。\n3.  在项目窗格中右键单击`MathFunctionsTest`，然后单击添加库。 现在，您可以选择构建树中的库、构建树外部的库、系统上的外部库(如 Unix 数学库`fftmpeg`)或您创建的另一个库。 选择外部库，然后单击下一步。\n\n4.  通过单击标记为库文件的行旁边的 Browse...，浏览在上一节中构建的库文件。 它将位于项目文件夹中名为`build-MathFunctions-Desktop_Qt_5_12_2_MinGW_64bit-Debug`的文件夹中。 选择`release`或`debug`文件夹中的`MathFunctions`库；无论是哪一个都无关紧要。 该对话框应类似于以下屏幕截图：\n\n![](img/197c0725-3d6f-4f2a-89a2-a23c8704e09f.png)\n\n5.  单击 Include Path 旁边的 Browse...浏览您的库的 Include 文件。这是您放置库标题的目录。\n6.  选择静态链接。\n7.  将其他值设置为默认值，单击 Next，然后单击 Finish。\n\nQt Creator 将在您的`.pro`文件中发挥它的魔力，添加一个`LIBS`变量，该变量包含库构建的输出和库的头文件的包含路径。\n\n现在我们可以调用阶乘函数了。 编辑`main.cpp`以读取以下代码：\n\n```cpp\n#include <QCoreApplication>\n#include \"mathfunctions.h\"\n\nint main(int argc, char *argv[])\n{\n    QCoreApplication a(argc, argv);\n    qDebug(\"6! is %lu\", MathFunctions::factorial(6));\n    return a.exec(); \n}\n```\n\n这段代码首先包括我们的库头文件。 请注意，如果在仅添加`#include`声明之后编译应用，您将获得`MathFunctions`库中每个元素的自动建议帮助。 这段代码使用`qDebug`而不是 C 标准库来处理其控制台输出。\n\n`qDebug()` actually has a stream-savvy implementation too. I could have written the `qDebug` line as follows:\n`qDebug() << \"6! is\" << MathFunctions::factorial(6);`. The code would have generated the same output. To do this, you'll need to be sure to include the  `#include <QDebug>` line.\n\n现在在 Debug 模式下构建并运行应用；您应该会看到一个控制台窗口，其中包含文本 6！ 是 720。 现在，尝试在发布模式下构建和运行库...。 等等，为什么`qDebug`的调试输出还在？\n\n`qDebug`实际上不是调试日志；它是调试信息的输出流，与构建级别无关。 如果希望在发布版本中关闭其输出，则需要编辑`.pro`文件。 双击您的`.pro`文件并添加以下行：\n\n```cpp\nCONFIG(release, debug|release): DEFINES += QT_NO_DEBUG_OUTPUT\n```\n\n这是另一个范围；它表示如果您的构建配置是`release`，则将`QT_NO_DEBUG_OUTPUT`预处理器定义添加到项目的预处理器定义列表中。\n\n现在，如果您重新构建(不要选择 Build，但实际上选择了 Rebuild，因为您希望在整个系统中进行干净的构建)并在发布模式下运行，您将不会看到任何输出。\n\nQt actually defines four output streams. One is for debugging messages, another is for bonafide warnings; use `qDebug` for regular logging and `qWarning` to output messages of a higher priority. There's also `qCritical` and `qFatal` for high-priority log messages that will indicate critical failures or failures that cause the application to terminate. You can also turn off warnings in release builds in the same way; simply add the following to your `.pro` file:\n`CONFIG(release, debug|release): DEFINES += QT_NO_WARNING_OUTPUT.`\n\n如果要将文件添加到项目中，您将执行什么操作？ 您可以通过手动编辑`.pro`文件来实现这一点，如果您是一个优秀的打字员，编辑速度会更快，但是它也容易出错，如果您搞砸了，可能会导致奇怪的构建问题，或者您也可以通过右键单击您的项目并选择 Add New...来完成此操作。 或添加现有文件...。 添加新项...。 选项打开一个简短的向导，其中包含如下选项：\n\n*   Qt 设计器窗体，我们将在下一章中讨论\n*   Qt 资源文件，我们将在下一章中讨论\n*   **Qt 快速标记**30-文件\n*   JavaScript 文件(可以包含实现 Qt Quick 应用逻辑的代码)\n*   完整 OpenGL 或 OpenGL/ES 中的片段或顶点的 OpenGL 着色器\n*   文本文件(如项目的`Readme`文件)或临时文件，在完成编辑会话之前用作存放临时剪贴板项目的位置\n\n在我们进入调试这个重要主题之前，让我们再看一看`.pro`文件，即我们应用的`.pro`文件，如下所示：\n\n```cpp\nQT -= gui\n\n# No debug output\nCONFIG(release, debug|release): DEFINES += QT_NO_DEBUG_OUTPUT\n\nCONFIG += c++ 11 console\nCONFIG -= app_bundle\n\nDEFINES += QT_DEPRECATED_WARNINGS\n\nSOURCES += main.cpp\n\n# Default rules for deployment.\nqnx: target.path = /tmp/$${TARGET}/bin\nelse: unix:!android: target.path = /opt/$${TARGET}/bin\n!isEmpty(target.path): INSTALLS += target\n\n# Include library\nwin32:CONFIG(release, debug|release): LIBS += -L$$PWD/../build-MathFunctions-Desktop_Qt_5_12_3_MinGW_64_bit-Release/release/ -lMathFunctions\nelse:win32:CONFIG(debug, debug|release): LIBS += -L$$PWD/../build-MathFunctions-Desktop_Qt_5_12_3_MinGW_64_bit-Debug/debug/ -lMathFunctions\nelse:unix: LIBS += -L$$PWD/../build-MathFunctions-Desktop_Qt_5_12_3_MinGW_64_bit-Debug/ -lMathFunctions\n\nINCLUDEPATH += $$PWD/../MathFunctions\nDEPENDPATH += $$PWD/../MathFunctions\n\nwin32-g++ :CONFIG(release, debug|release): PRE_TARGETDEPS += $$PWD/../build-MathFunctions-Desktop_Qt_5_12_3_MinGW_64_bit-Release/release/libMathFunctions.a\nelse:win32-g++ :CONFIG(debug, debug|release): PRE_TARGETDEPS += $$PWD/../build-MathFunctions-Desktop_Qt_5_12_3_MinGW_64_bit-Debug/debug/libMathFunctions.a\nelse:win32:!win32-g++ :CONFIG(release, debug|release): PRE_TARGETDEPS += $$PWD/../build-MathFunctions-Desktop_Qt_5_12_3_MinGW_64_bit-Release/release/MathFunctions.lib\nelse:win32:!win32-g++ :CONFIG(debug, debug|release): PRE_TARGETDEPS += $$PWD/../build-MathFunctions-Desktop_Qt_5_12_3_MinGW_64_bit-Debug/debug/MathFunctions.lib\nelse:unix: PRE_TARGETDEPS += $$PWD/../build-MathFunctions-Desktop_Qt_5_12_3_MinGW_64_bit-Debug/libMathFunctions.a\n```\n\n哟！ 这是相当密集的。 让我们看看能不能解开它。 它首先告诉构建系统我们不使用 QtGui。\n\n接下来是在发布版本中禁用`qDebug`消息的指令，这在默认情况下是不会发生的。 其他的`CONFIG`选项一起说，我们正在构建一个支持`C++ 11`标准的控制台应用。 带有`SOURCES`选项的下一行表示我们有一个源文件，一个是`main.cpp`的源文件。\n\n下一组作用域表示我们的库的路径，并处理这样的事实：我们的库在 Windows 上的不同目录中，用于发布和调试。 这与 Unix 系统不同，Unix 系统中只有一个库的内部版本变体。 在此之后是`INCLUDEPATH`和`DEPENDPATH`变量，它们指示在`MathFunctions`目录中有库头，并且应用依赖于这些头。 因此，如果标头上的时间戳发生更改，则应该重新构建二进制文件。\n\n最后一个作用域指定了对输出库本身的相同依赖关系；如果库发生更改，则必须重新构建应用可执行文件。 这一点尤其重要，因为通过这种方式，我们可以运行 Qt Creator 的多个副本，分别编辑库和应用文件，并在它们更改后重新构建所需的位，以及计算出所有依赖项，并自动构建正确的位。\n\n我们已经学习了如何将 C++ 库链接到我们的 Qt 项目，但是我们还没有真正完成。 接下来，我们将看看如何通过支持 Qt 提供的工具来调试我们的应用。\n\n# 失物招领-调试\n\nQt Creator 有一个最先进的 GUI，它可以连接到**GNU 调试器**(**gdb**)或 Microsoft 的命令行调试器 CDB(如果您使用 Microsoft 工具)。\n\n如果您已经在 MacOS、Linux 或用于 Windows 的 Qt Creator 的 MinGW 版本上安装了 Qt Creator，那么您就拥有了开始调试应用所需的一切。 如果您已经安装了 Microsoft Visual Studio，然后又安装了使用 Microsoft 编译器的 Qt Creator 版本，则还需要安装 Microsoft 命令行调试器才能使用 Qt Creator 的调试功能。 下面介绍如何安装命令行调试器：\n\n1.  如果您使用的是 32 位版本的编译器和 Qt 创建器，请从[http://msdn.microsoft.com/en-us/windows/hardware/hh852365](http://msdn.microsoft.com/en-us/windows/hardware/hh852365)下载 Windows 调试工具，或者从[http://msdn.microsoft.com/en-us/windows/hardware/hh852365](http://msdn.microsoft.com/en-us/windows/hardware/hh852365)下载 64 位版本的编译器和 Qt 创建器。\n\n2.  配置调试符号服务器的方法是：转到“工具”菜单下的“选项”，选择左侧的“调试器”项，单击“CDB 路径”窗格，然后编辑“符号路径”行旁边的文本框。\n\nUsually, the debugger works out of the box with Qt Creator, unless you're using the Microsoft toolchain. However, if you encounter problems, consult the Qt documentation about setting up the debugger at [http://qt-project.org/doc/qt-5/debug.html](http://qt-project.org/doc/qt-5/debug.html).\n\n下面的屏幕截图显示了我们的测试项目中的调试器，在断点处停止。 让我们来详细看看下面的截图来了解一下方向：\n\n![](img/4767b225-93a0-4f88-92ef-a3c70ffde341.png)\n\n在屏幕截图中，您将看到以下组件(为便于参考而编号)：\n\n1.  左侧是 Qt Creator 中用于选择视图的常用按钮行。\n2.  按钮旁边是项目文件的视图和打开的文档列表。\n3.  在主编辑器窗格中，每个源代码行都有一个可点击的指示器，用于设置和清除断点。\n4.  调用堆栈(指示程序如何到达执行停止的行)显示在编辑器窗格下的窗格中。\n5.  右上角是变量检查器，您可以在其中查看当前堆栈帧中的变量值以及任何全局变量。\n6.  变量检查器下面是一个挂起的断点列表，因此您可以打开和关闭断点，而无需遍历代码。\n\n要生成您可以在前面的屏幕截图中看到的屏幕，我单击了第 6 行的左侧，放置了一个断点，然后在确保已在构建选择器中指定了 Debug 构建之后，单击了左侧的 Debug 按钮。 Qt Creator 在 Debug 模式下构建应用，启动应用，并让它运行到第 6 行的断点。\n\n# 设置断点和单步执行程序\n\n断点(如果您以前从未遇到过断点)就是执行中断的点，您可以检查程序的状态。 一旦在断点处停止执行，您就可以单步执行函数或跨行执行程序，一次执行一行以查看它的行为。 在 Debug 视图中单击行号左侧可以设置或清除断点。 在断点处停止时，编辑器窗格边距中的黄色箭头指示处理器即将执行的代码行。\n\n在断点时，调用堆栈窗格上方会出现几个按钮，使您可以控制程序流，如下面的屏幕截图所示：\n\n![](img/e0a4ec6f-e0d8-48cc-a653-077a55a8ee60.png)\n\n按钮定义如下(同样，为便于参考，请重新编号)：\n\n1.  绿色的“继续”按钮，它在箭头指示的行处继续执行。 您也可以通过按下*F5*功能键继续。\n2.  红色的停止按钮，用于完全停止调试。\n3.  跳过按钮，用于执行当前行并前进到下一行，然后再次停止。 您可以通过按*F10*跨过一行。\n4.  单步执行按钮，进入下一个要调用的函数并再次停止。 您可以通过按*F11*单步执行功能。\n5.  “单步执行”按钮，该按钮在再次停止之前在当前调用上下文中运行函数的其余部分。 按*Shift*+*F11*可退出当前功能。\n6.  指令式按钮(看起来像一个小屏幕)，用于在一次处理源码行和一次处理一条装配线之间切换调试器。\n7.  还有一个线程菜单，因此您可以看到哪个线程正在运行或已经停止。\n\n例如，(在前面的屏幕截图中)从第 7 行开始，如果我们跨过第 8 行(按*F10*)，然后按*F11*，我们将进入阶乘函数。 此时，如果我们再次进入该函数，我们将在右侧列中看到*n*的值发生变化，并且箭头前进指向第 9 行(同样，如屏幕截图中的编号所示)。 在这里，我们可以通过几种方式调试阶乘函数：\n\n*   我们可以通过查看右侧窗格中的变量来检查该变量的内容。 如果它位于当前调用框架之上的堆栈框架中，我们可以更改调用框架并在不同的调用框架中查看变量。\n*   我们可以通过单击变量的值并输入新值来修改该变量。\n*   使用一些调试器，我们可以将箭头移动到调用函数中的不同行以跳过一行或多行代码，或者倒回执行以再次运行某段代码。\n\n不幸的是，最后一个功能不能与 Microsoft 命令行调试器一起使用，它特别强大，因为我们可以单步执行程序、观察错误、修改变量以解决错误原因，并且无需重新编译代码并重新运行可执行文件即可继续测试代码。 或者，我可以跳过一些我知道需要一段时间才能运行的代码，方法是在相关变量中替换新状态，然后从当前调用帧中的新位置继续。\n\n此外，我们还可以做许多其他的事情，从我们如何调试应用到我们可以通过各种方式查看应用运行时的状态。 从主调试菜单中，我们可以执行以下操作：\n\n*   通过从调试程序菜单中选择[分离]，将调试器与正在运行的进程分离(如果调试器速度变慢，并且我们知道我们的部分代码不需要调试，这会很方便)。\n*   通过停止执行来中断程序执行，并通过从 Debug 菜单中选择 Interrupt 来检查当前状态(如果我们的应用似乎陷入了我们意想不到的长循环中并且看起来挂起了，这一点很有用)。\n*   停止时，通过选择 Run to Line(运行到行)或按*Ctrl*+*F10*，运行到光标所在的行。\n*   停止时，通过选择跳转到行跳到光标所在的行。 选择跳转到行使您可以跳过当前点和目标行之间的代码行。\n\n# 检查变量和内存\n\n“变量”窗格显示当前堆栈帧中所有变量的值。 结构显示其成员的值，因此您也可以遍历复杂的数据结构。 在变量窗格中，您还可以将变量名称和值复制到剪贴板或仅复制变量值。\n\n在变量窗格中，有一个非常有用的功能，称为表达式求值器，它允许您为代码中的变量构造代数表达式并查看结果。 例如，如果我在阶乘函数的开头停止，n 设置为 6，我可以右键单击 Variables 窗格，选择 Insert New Expression Evaluator，然后在出现的对话框中输入公式，如`n*(n-1)`。 因此，窗格中会出现一条新行，显示表达式和值`30`。 虽然这是一个相当做作的例子，但我也可以查看指针值和指针取消引用。\n\n我还可以在变量更改时有条件地中断执行；这称为条件断点或数据断点。 例如，让我们在`main`函数中加入一个循环，并在执行循环时中断。 要做到这一点，首先要更改`main`函数，使其读起来像下面的代码块：\n\n```cpp\n#include <QCoreApplication> \n#include <QDebug> \n#include \"mathfunctions.h\" \n\nint main(int argc, char *argv[]) \n{ \n    QCoreApplication a(argc, argv);\n\n    int values[] = { 6, 7, 8 }; \n    for(unsigned int i = 0; i < sizeof(values)/sizeof(int); i++)\n    {\n        qDebug() << values[i] << \"! = \" << \n          MathFunctions::factorial(values[i]);\n    }\n\n    return a.exec(); \n}\n```\n\n这将迭代整数数组值中存储的值，并打印每个值的计算阶乘。 再次开始调试，让我们在`i`上添加一个数据断点。 要执行此操作，请执行以下步骤：\n\n1.  在`main`的第一行设置断点，该行将初始化`QCoreApplication`。\n2.  跳过直到出现`for`循环，然后在右窗格中右键单击`i`，并从 Add Data Breakpoint 子菜单中选择 Add Data Breakpoint at Object‘s Address。\n3.  按*F5*或继续按钮继续。\n\n当`i`设置为`0`时，执行将在第 11 行(`for`循环的开始处)停止。 每次我点击*F5*继续，应用都会运行，直到`i`的值因`for`循环末尾的`i++ `语句而改变。\n\n您还可以通过单击变量检查器窗格中数组名称旁边的展开箭头来检查和更改变量检查器中数组的各个值。\n\n除了查看和更改变量值之外，您还可以查看和更改各个内存位置。 如果您正在调试二进制格式的解码器或编码器，例如，当您需要查看内存中的特定位置时，您可能希望这样做。 在变量窗格中，您可以选择几个选项来检查内存位置；其中几个选项如下所示：\n\n*   您可以右键单击给定的变量，然后在该变量的地址处打开一个内存窗口。\n*   您可以右键单击给定的变量，并在该变量指向的值处打开一个内存窗口(换句话说，取消引用指向内存位置的指针)。\n*   您可以在变量窗格上单击鼠标右键并在当前堆栈帧的开头打开内存浏览器。\n*   您可以右键单击变量窗格，并在内存中的任意位置打开内存浏览器。\n\n以下屏幕截图显示了包含数组值的内存查看器：\n\n![](img/191923c7-9a45-4696-95fb-cfc2714cd17c.png)\n\n该窗口在左侧显示内存地址，以 16 字节为一行显示内存值(首先是十六进制，然后是 ASCII)，并为打开窗口所选择的实际变量上色。 您可以选择一个值范围，然后右键单击它们以执行以下操作：\n\n*   复制 ASCII 或十六进制值\n*   在您选择的内存位置上设置数据断点\n*   将执行转移到您单击的地址(如果您正在查看数据，可能不会执行此操作！)\n\n# 检查调用堆栈\n\n调用堆栈是应用在某个时间点执行时的函数调用层次结构。 虽然实际流程有所不同(通常在代码中)，但它从`main`开始，尽管调用`main`的内容因平台而异。 调用堆栈的一个明显用途是当您单击中断按钮时提供上下文；如果您的程序只是在某个地方考虑它的循环，单击中断并查看调用堆栈可以给您一个线索，了解发生了什么。\n\n还记得我是如何根据阶乘函数本身定义阶乘函数的吗？ 如果您在阶乘中放置断点并调用它，然后在查看调用堆栈之前继续几次断点，就可以非常清楚地看到这一点。 您将看到类似于以下屏幕截图的内容：\n\n![](img/3a9f746c-43e7-4a65-9786-afe3bddb0af9.png)\n\n“调用堆栈”窗口中的字段从左到右依次是堆栈级别(从堆栈顶部开始编号，然后向下移动)、正在调用的函数、在其中定义函数的文件以及当前正在执行的函数的行号。 所以，这个堆栈框架说我们在`mathfunctions.cpp`中`MathFunctions::factorial`的第 9 行，由`MathFunctions::factorial`的第 13 行调用，这是由`MathFunctions::factorial`的第 13 行调用的……。 依此类推，直到它在我们的主函数和操作系统在此之前用来设置应用进程的系统启动代码中触底。\n\n如果右键单击调用堆栈窗格的一行，则可以执行以下操作：\n\n*   重新加载堆栈，以防显示器损坏。\n*   将调用堆栈的内容复制到剪贴板-非常适合错误报告。 如果您的应用在调试器中引发异常或崩溃，您可以复制调用堆栈并将其发送给负责该部分代码的开发人员(或者将其作为纪念品保留下来)。\n*   在调用堆栈中函数调用指示的代码行中的指令地址处打开内存编辑器。\n*   在调用堆栈中函数调用指示的代码行中的指令地址处打开反汇编程序。\n*   反汇编内存的一个区域或当前函数。\n*   调试时在“调用堆栈”窗口中显示程序的计数器地址。\n\n仅此而已；我们已经学习了如何使用调试工具来检查我们的应用。 接下来，让我们学习如何构建我们的项目！\n\n# 项目窗格和生成项目\n\n您已经看到了`.pro`文件如何影响项目的编译，但还有比这更多的影响。 如果您单击 Qt Creator 左侧的 Projects 按钮，您将看到该项目的选项，其中包括以下内容：\n\n*   新的生成和运行选项\n*   编辑器选项\n*   代码样式选项\n*   依附者 / 附属国 / 附属地区 / 从属物\n\n其中每一个都在自己的面板中。\n\nIn most cases, you won't need to monkey around with any of these settings, but you might have to tinker with the Build & Run settings, especially if you're targeting multiple platforms such as Windows and Linux with cross-compilers or Android. (I will write more about this exciting development in Qt later in this book.)\n\n您应该知道的最后一件事是构建和运行工具包选择器。 Qt 是当今可用的最好的跨平台工具包之一，您可以很容易地发现自己正在支持多个平台(如 Linux 和 Android)或多个版本的 Qt 的系统上工作。 为了支持这一点，Qt 有构建工具包的概念，它只是支持特定平台的头文件、库和相关的东西。 您可以安装多个构建工具包，并通过选择 Open Build and Run Kit Selector 选择要编译的构建工具包。 默认情况下，如果您按照上一章中的步骤安装 Qt Creator，您将安装一个构建工具包；从 Qt 网站，您可以选择其他构建工具包。\n\n项目模式下的不同类型设置如下：\n\n*   对于生成设置，有用于发布和调试生成的配置选项。 在构建版本设置编辑器中，您可以控制构建产品是否放置在它们自己的目录中(默认情况下，即所谓的影子构建，您的构建输出不会与源代码混合，而是放置在它们自己的目录中)、构建的 qmake 配置(并实际看到 Qt Creator 将如何调用 qmake)、Qt Creator 如何清理您的项目，以及您需要为构建设置的任何环境变量。\n*   运行设置编辑器允许您控制应用是在本地运行还是部署在远程主机上(并不总是受支持，但对于 Android 等平台通常是这样)，您想要传递给应用的任何命令行参数，以及性能分析器工具的设置，我将在[第 4 章](04.html)*，Qt Foundations*中讨论。\n*   在“编辑器”面板中，可以设置此项目的特定编辑器选项。 这些设置会覆盖全局 Qt Creator 默认值，您可以通过从“工具”菜单中选择“选项”并选择“文本编辑器”选项来设置该默认设置。 这些选项包括一些细节，比如格式化代码时是使用制表符还是空格(我强烈建议您使用空格；它们与任何地方的编辑器都兼容！)、每个制表位的空格数量、是否应该自动缩进、源文件应该如何编码等等。\n*   “代码样式”面板是对 Qt Creator 全局设置的另一个覆盖(这次是“选项”菜单中可用的“选项”对话框的“C++”和“Qt 快速”面板)。 在这里，您可以拾取默认样式或编辑样式。\n\nI strongly recommend that you pick a style that matches the existing source code you're editing; if you're starting from a blank page, the Qt default style is quite readable and is my favorite.\n\n*   如果项目文件包含多个子项目，则“依赖项”面板允许您设置生成顺序，以便以正确的顺序生成内容。 例如，我们可以选择同时打开我们的库项目和测试项目；如果这样做，我们将看到依赖项中列出了`MathFunctions`库，并且我们可以选择在构建测试应用之前构建项目。\n\n我们已经了解了如何构建我们的项目。 让我们退一步，再回顾一下整个过程。\n\n# 回顾-运行和调试您的应用\n\n您将花费大量时间在 Qt Creator 中编辑、编译和调试代码，因此明智的做法是记住以下基础知识：\n\n*   箭头键在没有调试器的情况下运行应用；若要调试应用，请选择上面有错误图标的箭头键。\n*   您可以通过单击左侧的 Edit 或 Debug 视图选项在应用的 Editor 视图和 Debug 视图之间切换；如果您调试应用，Qt Creator 将自动进入 Debug 视图。\n*   断点不仅仅停留在一行代码上！ 使用数据断点可以锁定仅偶尔出现的奇怪错误，或者快速跳过大型循环的前几个大项。\n*   “变量”窗格让您看到的不仅仅是变量的内容；您还可以添加由多个变量和算术组成的表达式，或者查看任意的内存位置。\n*   想要在调试会话期间破解错误吗？ 您可以在“变量”窗格中更改变量的值，然后继续运行，同时更改程序的状态。\n\n希望您已经通过我们在这里介绍的所有小节学习了如何构建和调试 Qt 应用。 最后，让我们总结一下从本章中学到的东西。\n\n# 简略的 / 概括的 / 简易判罪的 / 简易的\n\nQt Creator 的**集成开发环境**(**IDE**)包含一个编辑器和工具，用于启动编译器、链接器和调试器，以便构建和调试应用。 使用它，您可以启动和停止应用，在应用停止时放置断点，或者检查应用的变量或逻辑流。\n\n虽然 Qt Creator 会为您管理大部分项目，但有时您只需要处理一个`.pro`文件就可以了。 您可以使用作用域来处理条件编译(例如，何时为特定平台构建，或者文件是否应该包含在发布或调试模式中)。 `.pro`文件由作用域、变量和它们的值组成；通过设置`.pro`文件提供给 qmake 的变量，qmake 可以理解项目中的依赖关系，并神奇地创建一个 Makefile 来构建应用。\n\n在本章中，我们学习了如何构建自己的 C++ 库并将其链接到我们的 Qt 应用。 在那之后，我们还学习了如何使用 Qt 提供的工具构建和调试我们的应用。\n\n在下一章中，我们将从构建项目的机制出发，看看 Qt Creator 的 UI 设计器，并向您简要介绍 Qt 窗口小部件和 Qt Quick 的世界。"
  },
  {
    "path": "docs/app-dev-qt-creator/03.md",
    "content": "# 三、使用 Qt Designer 设计应用\n\nQt 最为人所知的可能是一个跨平台的用户界面工具包，直到最近几年，Qt Creator 才真正发展成为一个完整的软件开发环境。 然而，即使在其早期版本中，Qt 也有一个很好的工具，可以用 Qt Designer(现在是 Qt Creator 的一部分)来构建用户界面。 最近，构建 Qt 的开发人员添加了 Qt Quick，作为用户界面开发的第二种选择。 Qt Quick 扩展了 Qt Creator 的 Qt 库和 Qt 设计器功能，为触摸屏和**机顶盒**(**STB**)构建流畅的界面。 这得益于 Qt Quick 和**Qt 元对象语言**(**QML**)的声明性质。\n\n在本章中，我们将介绍以下主题：\n\n*   介绍信号和时隙\n*   使用 Qt Designer 创建用户界面\n*   在应用中实例化窗体、消息框和对话框\n*   连接 Qt 小部件应用逻辑\n*   介绍 Qt Quick 对声明式用户界面开发的支持\n*   了解 Qt 应用的构建\n*   创建 Qt 应用\n\n这些主题将让您深入了解使用 Qt 创建应用的基础知识，无论是在 C++ 中还是通过 QML 方法。 您将学习如何使用 Qt 窗体设计器和 Qt 快速设计器来设计您的第一个用户界面。 然后，您还将学习如何使用 Qt 提供的基本功能，例如实例化消息框以及通过信号和槽机制将用户界面交互与事件函数连接起来。\n\n在本章结束时，您将可以决定您的应用应该使用 Qt 窗口小部件还是 Qt Quick 来编写，并在 Qt Creator 附带的文档的帮助下构建您的应用。\n\n# 技术要求\n\n本章的技术要求包括 Qt 5.12.3 MinGW 64 位、Qt Creator 4.9.0 和 Windows 10。\n\n# 介绍信号和时隙\n\n在软件系统中，经常需要耦合不同的对象。 理想情况下，这种耦合应该是松散的-也就是说，不依赖于系统的编译时配置。 当您考虑用户界面时，这一点尤其明显-例如，按下按钮可能会调整文本小部件的内容，或者导致某些内容出现或消失。 许多系统将事件用于此目的；提供数据的组件将数据封装在事件中，事件循环(或最近的事件侦听器)捕获事件并执行某些操作。 这称为**事件驱动**编程或事件模型。\n\nQt 提供了*信号和槽*机制作为界面来管理事件，如单击事件、选择更改事件、文本输入事件等。 当用户执行某项操作并触发事件时，该对象(即按钮)会生成一个信号，该信号将被发送到 Qt 的事件系统。 然后，事件系统将通知链接到该信号的另一个对象这样的事件已经发生。 最终，作为对信号的反应，它将触发接收对象的`slot`函数(有点类似于回调函数)。 换句话说，当对象 A 被激发(即，单击按钮)时，它将向对象 B 发送信号，并通知它执行其`slot`函数(即，关闭窗口)。 Qt 对象可能会发出多个信号，并且信号可能带有参数；此外，多个 Qt 对象可以具有连接到同一信号的槽，这使得安排一对多通知变得容易。\n\nQt 提供了一个宏`connect`，它允许您将信号连接到插槽。 同样重要的是，如果没有对象对信号感兴趣，则可以安全地忽略该信号，并且不会将任何插槽连接到该信号。 同样重要的是，如果没有插槽连接到信号，它将被简单地忽略。 从 Qt 的对象基类`QObject`继承的任何对象都可以发出信号或提供用于连接到信号的插槽。 在幕后，Qt 提供了对 C++ 语法的扩展，以声明信号和槽。\n\n一个简单的例子将有助于阐明这一点。 您在 Qt 文档中找到的经典示例是一个很好的示例，我们将在这里再次使用它，并进行一些扩展。 假设您需要一个计数器，即一个容纳整数的容器。 在 C++ 中，您可能会编写类似以下代码块的代码：\n\n```cpp\nclass Counter \n{ \npublic: \n    Counter() {} \n\n    int getValue() { return myValue; } \n    void setValue(int value); \n\n private: \n    int myValue = 0; \n }; \n```\n\n`Counter`类有一个私有成员`myValue`，承载其值。 客户端可以调用`getValue`来获取计数器的值，或者通过使用新值调用`setValue`来设置其值。 请注意，在 C++ 11 中，您可以直接初始化类数据成员的默认值-在本例中，我们将`myValue`的默认值设置为`0`。\n\n在 Qt 中，我们可以这样编写类，使用信号和槽：\n\n```cpp\n#include <QObject> \n\nclass Counter : public QObject \n{ \nQ_OBJECT \n\npublic: \n    Counter(QObject *parent = 0) {}\n    int getValue() { return myValue; } \n\npublic slots: \n    void setValue(int value); \n    void increment(); \n    void decrement(); \n\nsignals: \n    void valueChanged(int newValue); \n\nprivate: \n    int m_value = 0; \n};\n```\n\n此`Counter`类从`QObject`继承所有 Qt 对象的基类。 要使`QObject`的所有功能(如信号槽机制)可用，`QObject`的子类必须包括声明`Q_OBJECT`作为其定义的第一个元素；此宏扩展到 Qt 代码，实现 Qt 对象和信号槽机制所需的子类特定粘合剂。 构造函数保持不变，将我们的私有成员初始化为 0。 同样，访问器方法值保持不变，返回计数器的当前值。 如果开头没有包含`Q_OBJECT`宏，则编译将失败。\n\n对象的槽必须是公共的，并且使用 C++ 公共槽的 Qt 扩展来声明。 该代码定义了三个时隙：`setValue`时隙，用于接受计数器的新值；以及`increment`和`decrement`时隙，用于递增和递减计数器的值。 槽可以接受参数，但不能返回它们；信号与其槽之间的通信是单向的：以信号开始，以连接到信号的槽终止。 这种机制是 Qt 开发人员故意设计的。\n\n计数器提供单一信号。 信号也是使用 C++ 信号的 Qt 扩展声明的。 `Counter`对象发出`valueChanged`信号的单个参数，这是计数器的新值。 信号是函数签名，而不是方法；Qt 对 C++ 的扩展使用信号和槽的类型签名，以确保信号槽连接之间的类型安全，这是信号和槽相对于其他解耦消息传递方案的关键优势。\n\n作为开发人员，使用任何有意义的应用逻辑实现类中的每个槽是我们的责任。 `Counter`类的插槽如下所示：\n\n```cpp\nvoid Counter::setValue(int newValue) \n{ \n    myValue = newValue; \n    emit valueChanged(newValue); \n} \n\nvoid Counter::increment() \n{ \n    setValue(getValue() + 1); \n} \n\nvoid Counter::decrement() \n{ \n    setValue(getValue() - 1); \n}\n```\n\n我们使用`setValue`槽的实现作为方法，这是所有槽的核心。 `setValue`槽接受一个新值，如果它们不相同，则将新值赋给`Counter`的私有成员变量。 然后，该信号使用 Qt 扩展`emit`发出`valueChanged`信号，该信号触发对连接到该信号的时隙的调用。\n\nThis is a common pattern for signals that handle object properties: testing the property to be set for equality with the new value, and only assigning and emitting a signal if the values are unequal.\n\n如果我们有一个按钮--比如说`QPushButton`--我们可以把它的点击信号连接到`increment`或`decrement`槽上，这样点击按钮就可以递增或递减计数器。 我会使用`QObject::connect`方法完成此操作，如下所示：\n\n```cpp\nQPushButton* button = new QPushButton(tr(\"Increment\"), this); \nCounter* counter = new Counter(this); \nQObject::connect(button, &QPushButton::clicked, counter, &Counter::increment); \n```\n\n我们首先创建`QPushButton`和`Counter`对象。 `QPushButton`构造函数接受一个字符串，即按钮的标签，我们将其表示为`Increment`字符串或其本地化的对应物。\n\n为什么我们要将它传递给每个构造函数？ Qt 在`QObject`对象及其子代之间提供了父子内存管理，在使用完对象后可以轻松地进行清理。 当您释放一个对象时，Qt 还会释放父对象的所有子对象，这样您就不必这样做了。 父子关系是在构造时设置的：这个信号告诉构造函数，当调用此代码的对象被释放时，`PushButton`和`counter`也可以被释放。 (当然，调用方法也必须是`QObject`的子类才能正常工作。)\n\n接下来，调用`QObject::connect`，首先传递源对象和要连接的信号，然后传递接收器对象和信号应该发送到的槽。\n\nEver since Qt version 5, you are not recommended to use the old `SIGNAL` and `SLOT` macros when calling `QObject::connect`.\n\n信号也可以连接到信号，当这种情况发生时，信号被链接并触发连接到下游信号的任何时隙。 例如，我可以编写以下代码：\n\n```cpp\nCounter a, b; \nQObject::QObject::connect(&a, &Counter::valueChanged, &b, &Counter::setValue); \n```\n\n这会将`b`计数器与`a`计数器连接起来，因此对`a`计数器的值的任何更改也会改变`b`计数器的值。\n\n除此之外，您还可以将信号直接连接到函数，如下所示：\n\n```cpp\nconnect(a, &Counter::valueChanged, someFunction);\n```\n\n如果您正在编写 C++ 11 代码，您甚至可以将您的信号连接到 lambda 函数，如下所示：\n\n```cpp\nconnect(a, &Counter::valueChanged, [=](const QString &newValue)\n{\n    b->updateValue(\"senderValue\", newValue);\n});\n```\n\nLambda 函数是我们所说的匿名函数，它不需要名称或标识符。 Lambda 函数总是被更高阶的函数立即触发，用外行的话说，它是在函数中声明的函数。 一旦它的任务完成，它将不复存在；因此，不需要标识符。\n\n在这种情况下，当`valueChanged`信号被触发时，我们直接在`connect`函数中声明一个 lambda 函数，该函数随后调用`updateValue`并将`newValue`变量传递给它。 当您不打算在其他地方重用相同的槽函数时，这很有帮助，因此，如果您以前编写过 JavaScript，您应该非常熟悉这一点。\n\n还有`disconnect`，它会中断信号和特定插槽之间的连接。 调用`disconnect`类似于调用`connect`，如下所示：\n\n```cpp\ndisconnect(&a, &Counter::valueChanged(int), &b, &Counter::setValue(int));\n```\n\n这会断开我们在上一个示例中建立的连接。\n\n信号和槽在整个 Qt 中使用，既用于用户界面元素，也用于处理异步操作，如网络套接字上的数据存在、HTTP 事务结果等。 在幕后，信号和槽非常高效，归根结底是函数分派操作，因此您应该毫不犹豫地在自己的设计中使用抽象。\n\nQt 提供了一个特殊的构建工具，元对象编译器，它编译 C++ 的扩展，是信号和槽所必需的。 它生成实现该机制所需的附加代码。\n\n使用这些工具，我们将在 Qt Designer 的帮助下创建一个示例表单。\n\n# 使用 Qt Designer 创建用户界面\n\n让我们使用 Qt Designer 和两个窗体创建一个简单的计算器应用：一个窗体接受算术运算的参数，第二个对话框窗体显示结果。\n\n请记住，我们也将在本章的后续部分使用这些表单。 在本章中，我们将做两次，第一次是展示如何使用 Qt 窗口小部件完成这项工作，第二次是使用 Qt Quick。 该示例是精心设计的，但将向您展示如何在这两种环境中创建多个用户界面窗体，并让您练习如何使用信号和插槽。\n\n*F1* is the keystroke you can use in Qt Creator to get help. As you write code in this and subsequent chapters, for any class or method you're curious about, select it and hit *F1*. You'll be taken to Qt's `Help` mode, with documentation about that class or method.\n\n# 创建主窗体\n\n在[章](01.html)，*Qt Creator 快速入门*中，您了解了 Qt 小部件设计器的基本元素，包括可以使用的小部件调色板、中心编辑窗格、对象树和属性视图。 下面的屏幕截图再次显示了 Qt 设计器：\n\n![](img/80272bbe-cb98-4b8f-879e-6418f25bdb67.png)\n\n从左到右排列，您看到的屏幕部分如下所示：\n\n1.  视图选择器，当前指示 Qt Designer 视图处于活动状态。\n2.  可以在表单上布局的可能小部件的调色板。\n3.  表单编辑器，位于连接编辑器上方，允许您在小部件之间连接信号和插槽。\n4.  对象树，通过使用嵌套列表，指示表单上已布局的所有对象，并显示它们的父子关系。\n\n5.  对象树下是属性编辑器，您可以在其中编辑在表单编辑器上选择的任何项的编译时属性。\n\n让我们首先创建一个新的 Qt 小部件项目(从 New File or Projects 对话框中单击 Qt Widgets Application)，并将该项目命名为`QtGuiCalculator`，然后执行以下步骤：\n\n1.  在项目的`Forms`文件夹中，双击`mainwindow.ui`文件。 设计器将打开。\n2.  从调色板中拖出垂直布局。\n3.  右键单击布局并选择 Layout(布局)，然后选择 Adjust Size...(调整大小...)。 布局将缩小到某一点。\n4.  拖动两个行编辑器并将其放到对象查看器(最右侧窗格)的垂直布局上。 您将看到垂直布局变大，以接受每个行编辑器。 您现在应该有类似以下屏幕截图的内容，其中布局现在有两个文本字段：\n\n![](img/7ec585e5-6ebd-4f36-9a9b-475121cdef79.png)\n\n5.  拖动水平布局，并将其放到对象查看器中的垂直布局上。\n6.  将四个按钮拖放到您刚刚添加的水平布局上。\n\n7.  调整包含窗口的大小，以便在窗口中显示整个布局。\n8.  使用屏幕右下角区域的属性浏览器重命名按钮`plusButton`、`minusButton`、`timesButton`和`divideButton`。 执行此操作时，向下滚动到 Text 属性(在 QAbstractButton 下)，并为每个按钮指定一个逻辑标签，如`+`、`-`、`*`和`/`。\n9.  选择顶行输入，并将其命名为`argument1Input`。\n10.  选择底线输入，并将其命名为`argument2Input`。\n\n下一个屏幕截图显示了到目前为止您应该在 Qt Designer 表单编辑器窗格中看到的内容。 您也可以通过打破布局并使用鼠标定位按钮来手动排列按钮，但这通常会降低布局对窗口大小调整的健壮性，通常不是一个好主意。 下面的屏幕截图描述了我们的计算器用户界面：\n\n![](img/3c173c0b-e0b8-4afc-b8e7-89b2207c5f14.png)\n\n到目前为止，这是相当简单的。 我们使用垂直布局和水平布局来布局各种控件；这利用了 Qt 对小部件布局和大小的动态约束。 所有小部件都有最小和最大大小属性，布局使用这些属性来确定小部件消耗的实际大小。 有些小部件是有弹性的；也就是说，它们可以伸展以填充其内容。 指定小工具的实际大小时，可以指定它在*x*和*y*轴的每个轴上采用下列值之一：\n\n*   小工具的最小大小。\n*   小工具的最大大小。\n*   介于其最小值和最大值之间的固定大小。\n*   扩展大小，扩展以适合小工具的内容。\n\nQt 提供了四种布局，您可以混合搭配，就像我们刚才所做的那样。 您已经遇到了垂直和水平布局；还有网格布局和表单布局，前者允许您在*m x n*网格中组织事物，后者组织小部件的方式类似于本机平台枚举表单上的字段的方式。\n\n现在，我们的布局有点杂乱无章。 让我们添加一些间隔符，以便更好地填充窗口中的空间，同时，为关于框添加一个按钮：\n\n1.  拖动一个垂直间隔符并将其放在输入行之间，并在包含按钮行的水平布局和输入行之间拖放第二个垂直间隔符。\n2.  将一个按钮拖到垂直布局，并在底线和按钮之间添加一个间隔符。\n3.  将最后一个按钮命名为`aboutButton`，并将其文本命名为`About`。 我们稍后将添加一个图标。\n\n下一个屏幕截图显示了我们在设计器中构建的应用。 如果您单击 Run(运行)按钮，您应该会看到以下内容：\n\n![](img/2981062f-209d-4070-bdb5-5bcd62136551.png)\n\n现在，让我们进行结果对话。 右键单击项目并选择 Add New...(添加新的...)；然后，执行以下步骤：\n\n1.  在出现的对话框中，选择左侧的 Qt，然后选择中间的 Qt Designer 窗体。 点击 Choose(选择)。\n2.  为您的对话框选择一种对话样式；选择底部带有按钮的对话框，然后单击下一步。\n3.  将文件命名为`resultdialog.ui`，然后单击 Next。\n4.  单击 Finish(完成)。\n5.  在出现的对话框中，拖出一个表单布局。 右键单击对话框窗口(不是窗体布局)，然后选择 Layout(布局)，然后选择 Layout(垂直布局)。\n6.  将标签添加到窗体布局。 将其文本更改为`Result`。\n7.  拖出另一个标签，并将其命名为`result`。 在`Property`下的`objectName`字段中，将名称更改为`result`。\n8.  调整窗口大小，使其更小，以使其看起来更好。\n\n您应该有一个类似以下屏幕截图的对话框：\n\n<q>![](img/b5eca8cd-3b2a-4b6f-96c1-3ebe30765e81.png)</q>\n\n现在是尝试布局和间隔符的好时机，并可以随心所欲地设置对话框样式。 要了解这是如何实现的，让我们看看如何使用这些资源。\n\n# 使用应用资源\n\n现在，让我们为应用添加一个用于 About 按钮的图标。 你可以画一个，或者在线下载一个免费的图标。 图标可以是**便携网络图形**(**PNG**)、**联合图像专家组**(**JPEG**)或其他格式；最好选择**可伸缩矢量图形**(**SVG**)格式，因为 SVG 图像是基于矢量的，并且可以正确缩放到不同的大小。 将资源文件放入项目目录，然后执行以下步骤：\n\n1.  在 Qt Creator 中选择编辑视图。\n2.  右键单击该项目，然后选择 Add New...(添加新的...)。 在打开的 New File 对话框中，单击 Files and Classes 下的 Qt，然后单击 Qt Resource file。\n3.  将文件命名为`resources`。\n4.  将其添加到当前项目。\n5.  如果`resources.qrc`尚未在编辑器中打开，请在解决方案窗格中双击它。 资源文件编辑器将出现。\n6.  单击 Add(添加)，选择 Add Prefix(添加前缀)，然后创建前缀`/`。\n7.  再次单击 Add(添加)，然后单击 Add Files(添加文件)，并选择您的图标。\n\n图标通过 Qt 资源编译器加载到应用的只读部分。 只要在资源的路径和名称前加上冒号，就可以在访问文件的任何位置访问它们。 例如，我们可以在应用资源中放置一个文本文件，然后打开该文件进行阅读，如下所示：\n\n```cpp\nQFile file(\":/folder/fileName.txt\"); \nfile.open(QIODevice::ReadOnly | QIODevice::Text); \n\nwhile (!file.atEnd()) { \n    QByteArray line = file.readLine(); \n    processLine(line); // Calling your own function\n} \n```\n\n应用资源适用于文本和小媒体文件，如图标或图像。 但是，您应该避免将它们用于较大的项目，如电影和较大的声音，因为它们会不必要地增加应用二进制文件的大小。 出于这些目的，最好将媒体文件与您的应用一起打包，然后直接从磁盘加载它们。\n\n在下一节中，当我们将 About 框添加到应用时，我们将使用您添加的资源。\n\n# 在应用中实例化窗体、消息框和对话框\n\nQt Designer 为您在 Designer 中创建的每个表单生成一个基于 XML 的布局文件(以`.ui`结尾)。 在编译时，Qt Creator 将布局编译为一个头文件，该文件为您的用户界面布局构造组件。 Qt 应用通常使用的模式是构造主类实例化的私有布局类。 下面是它在主窗口中的工作方式：\n\n```cpp\n#ifndef MAINWINDOW_H \n#define MAINWINDOW_H \n\n#include <QMainWindow> \n\nnamespace Ui { \n  class MainWindow; \n} \n\nclass MainWindow : public QMainWindow \n{ \n    Q_OBJECT \n\npublic: \n    explicit MainWindow(QWidget *parent = nullptr); \n    ~MainWindow(); \n\nprivate: \n    Ui::MainWindow *ui; \n}; \n\n#endif // MAINWINDOW_H \n```\n\n在`mainwindow.cpp`中，我们有以下内容：\n\n```cpp\n#include \"mainwindow.h\" \n#include \"ui_mainwindow.h\"\n#include \"resultdialog.h\" \n#include <QMessageBox> \n\nMainWindow::MainWindow(QWidget *parent) : \n    QMainWindow(parent), \n    ui(new Ui::MainWindow)\n{ \n    ui->setupUi(this); \n} \n\nMainWindow::~MainWindow()\n{\n    delete ui;\n}\n```\n\nWhy is the constructor declared explicitly? This prevents the C++ compiler from providing implicit casts so that callers can't use anything but the `MainWindow` instances when referring to `MainWindow`. Qt provides this by default.\n\n`Ui::MainWindow`类由 Qt Designer 自动构造；通过将其声明包含在`mainwindow.cpp`中，我们创建了它的一个实例，并将该实例分配给`ui`字段。 初始化后，我们调用它的`setupUi`函数，该函数创建您在 Qt Designer 中勾勒出的整个用户界面。\n\n我们在 Qt 设计器中布局的控件可以作为字段名访问。 例如，我们可以修改`mainwindow.cpp`来调用 About 框，方法是向`mainwindow.h`添加一个槽来处理单击 About 按钮时的情况，然后，我们可以在槽的实现中添加代码来调用 About 框。 为此，请执行以下步骤：\n\n1.  向`mainwindow.h`添加一个公共槽声明，以及名为`aboutClicked`的槽。 它现在读起来应该类似于以下代码：\n\n```cpp\nclass MainWindow : public QMainWindow \n{ \n    Q_OBJECT \n\npublic: \n    explicit MainWindow(QWidget *parent = 0); \n    ~MainWindow(); \n\npublic slots: \n    void aboutButtonClicked(); \n\nprivate: \n    Ui::MainWindow *ui; \n}; \n```\n\n2.  在`mainwindow.cpp`的顶部，为`QMessageBox`类添加一条`include`语句，如下所示：\n\n```cpp\n#include <QMessageBox>\n```\n\n3.  将`aboutClicked`槽的实现添加到`mainwindow.cpp`。 这段代码在堆栈上构造`QMessageBox`，并将其图标设置为您先前在资源中添加的图标，将对话框的文本设置为`Lorem ipsum`，将消息框的标题设置为`About`。 调用`QMessageBox`的`exec`方法打开消息框并阻塞应用流，直到您关闭消息框。 它应该读取类似以下代码的内容：\n\n```cpp\nvoid MainWindow::aboutButtonClicked() \n{ \n    QMessageBox messageBox; \n    messageBox.setIconPixmap(QPixmap(\":/icon.png\")); \n    messageBox.setText(\"Lorem ipsum.\"); \n    messageBox.setWindowTitle(\"About\"); \n    messageBox.exec(); \n} \n```\n\n4.  在`MainWindow`构造函数中，将信号从 About 按钮连接到您刚刚创建的插槽。 您的构造函数现在应该如下所示：\n\n```cpp\nMainWindow::MainWindow(QWidget *parent) : \n    QMainWindow(parent), \n    ui(new Ui::MainWindow), \n{ \n    ui->setupUi(this); \n    QObject::connect(ui->aboutButton, &QPushButton::clicked, \n                     this, &MainWindow::aboutButtonClicked); \n} \n```\n\n如果我们构建应用，我们现在有一个功能齐全的 About 框，包括您选择的应用图标。 `connect`调用就像我们前面看到的信号槽连接；它将主窗口用户界面的`aboutButton`单击信号连接到您的`aboutClicked`槽。\n\nA word on naming signals and slots before we continue: a signal is typically named a verb in its past tense, denoting the semantics of the event that just occurred that it's trying to signal. A slot should somehow match those semantics, preferably including more detail as to how the signal is being handled. So, Qt names the button's clicked signal logically, and I expand on this by giving a slot named `aboutClicked`. Of course, you can name your signals and slots whatever you like, but this is a good practice to follow.\n\n在连接其他按钮并实现计算器逻辑之前，我们需要为结果对话框设置类。 Qt 使用将表单编译成类的**用户界面编译器**(**UIC**)自动创建此类。 我们将遵循`MainWindow`类的模式，创建一个私有的`ui`成员，其中包含编译时生成的对象的一个实例，该对象构造结果对话框的用户界面。 通过右键单击项目，您可以使用可用的 New File 向导创建`ResultDialog`类；选择 C++ Class，并将其命名为`ResultDialog`。 类本身应该继承自`QDialog`。 头文件应如下所示：\n\n```cpp\n#ifndef RESULTDIALOG_H \n#define RESULTDIALOG_H \n\n#include <QDialog> \n\nnamespace Ui { \n    class Dialog; \n} \n\nclass ResultDialog : public QDialog \n{ \n    Q_OBJECT \npublic: \n    explicit ResultDialog(QWidget *parent = nullptr); \n    ~ResultDialog(); \n\nprivate: \n    Ui::Dialog *ui; \n\n}; \n\n#endif // RESULTDIALOG_H \n```\n\n我们需要做的第一件事是向前声明由 Qt Designer 创建的`Dialog`类；我们在`Ui`命名空间中这样做，这样它就不会与应用中的任何其他代码冲突，如下所示：\n\n```cpp\nnamespace Ui { \n    class Dialog; \n} \n```\n\n然后，我们需要将指向该类实例的指针声明为私有成员变量；我们将该指针命名为`ui`，就像对`MainWindow`类所做的那样。\n\n您可以猜测我们的`ResultDialog`实现是什么样子，如下所示：\n\n```cpp\n#include \"resultdialog.h\" \n#include \"ui_resultdialog.h\" \n\nResultDialog::ResultDialog(QWidget *parent) : \n    QDialog(parent), \n    ui(new Ui::Dialog) \n{ \n    ui->setupUi(this); \n} \n\nResultDialog::~ResultDialog() \n{ \n    delete ui; \n} \n```\n\n在构造时，它创建`Ui:Dialog`类的实例，然后调用其`setupUi`方法，在运行时创建用户界面的实例。\n\n除了使用表单文件(`.ui`)之外，您还可以仅使用 C++ 代码手动实例化表单小部件。 让我们试试看吧。 在开始之前，请查看[https://doc.qt.io/qt-5/qtwidgets-module.html](https://doc.qt.io/qt-5/qtwidgets-module.html)，了解您可以在应用中使用的所有 Qt 小部件类。\n\n假设您想要使用 C++ 手动将一个按钮实例化到您的程序中。 您必须先包含其标头，然后才能访问其属性和功能，如下所示：\n\n```cpp\n#include <QPushButton>\n```\n\n然后，您可以继续实例化按钮，如下所示：\n\n```cpp\nQPushButton * button = new QPushButton(\"Click me\", this);\n```\n\n在前面的代码中，我们声明了一个`QPushButton`对象，并将其标题设置为`Click me`。 第二个参数是按钮的父对象，我们使用`this`关键字来指示父对象是`MainWindow`对象。 如果我们希望它是其他的东西，我们可以这样做：\n\n```cpp\nQPushButton * button = new QPushButton(\"Click me\", this);\nQPushButton * button2 = new QPushButton(\"Click me\", button);\n```\n\n在前面的代码中，我们实例化了第二个按钮，并使第一个按钮成为其父按钮。 一旦小部件被赋予了父对象，它将跟随父对象到达任何地方，并且如果父对象被删除，它也将被删除。 在下面的代码块中可以看到这种情况的示例：\n\n```cpp\nQPushButton * button = new QPushButton(\"Click me\", this);\nQPushButton * button2 = new QPushButton(\"Click me\", button);\nbutton->deleteLater();\n```\n\n由于我们要删除`button`对象，它的子对象`button2`也将被删除。\n\n现在，发布此实例化，让我们看看如何连接 Qt 小部件逻辑以了解其工作原理。\n\n# 连接 Qt 小部件应用逻辑\n\n计算器的应用逻辑很简单：我们向`ResultDialog`添加一个属性设置器，允许我们设置对话框的结果字段，然后在`MainWindow`中连接一些算术、信号和槽，以进行实际计算并显示对话框。 让我们来看一下以下步骤：\n\n1.  首先，对`ResultDialog`进行以下更改：\n\n```cpp\nvoid ResultDialog::setResult(float r) \n{ \n    ui->result->setText(QString::number(r)); \n} \n```\n\n此方法接受一个浮点数(要在对话框中显示的值)，并使用 Qt 的默认格式将结果格式化为字符串。 Qt 是完全国际化的；如果您在讲英语的语言环境中执行此操作，它将使用小数点，而如果您在将区域设置为使用逗号作为小数点分隔符的区域中执行此操作，则它将使用逗号。 Number 方法非常方便，重载使用双精度和浮点数以及整数和参数来表示返回字符串的精度和求幂。\n\n2.  现在，我们转到修改后的`MainWindow`类。 首先，让我们看一下修改后的类声明，如下所示：\n\n```cpp\n#ifndef MAINWINDOW_H \n#define MAINWINDOW_H \n\n#include <QMainWindow> \n#include <QPair> \n\nnamespace Ui { \n    class MainWindow; \n} \n\nclass ResultDialog; \n\nclass MainWindow : public QMainWindow \n{ \n    Q_OBJECT \n\n    typedef QPair<float, float> Arguments; \npublic: \n    explicit MainWindow(QWidget *parent = nullptr); \n    ~MainWindow(); \n    Arguments arguments(); \nsignals: \n    void computed(float f); \n```\n\n3.  我们继续声明类的槽函数和私有变量，如下所示：\n\n```cpp\npublic slots: \n    void aboutClicked(); \n    void plusButtonClicked(); \n    void minusButtonClicked(); \n    void timesButtonClicked(); \n    void divideButtonClicked(); \n\n    void showResult(float r); \n\nprivate: \n    Ui::MainWindow *ui; \n    ResultDialog* results; \n}; \n\n#endif // MAINWINDOW_H\n```\n\n除了基类`QMainWindow`之外，我们现在还包括`QPair`，这是一个简单的 Qt 模板，允许我们传递成对的值。 我们将使用`QPair`模板(一种定义为`Arguments`的类型)传递算术运算的一对参数。\n\n4.  我们添加一个信号`computed`，该类在任何时候执行算术运算时都会触发该信号。 我们还为每个算术按钮单击添加槽：`plusButtonClicked`、`minusButtonClicked`、`timesButtonClicked`和`divideButtonClicked`。 最后，我们添加了一个`showResult`槽，它显示了发生计算时的结果。\n5.  现在，`MainWindow`构造函数需要为我们所有的按钮、信号和插槽执行一系列信号插槽连接。 将突出显示的代码部分添加到`mainwindow.cpp`中，如下所示：\n\n```cpp\nMainWindow::MainWindow(QWidget *parent) : \n    QMainWindow(parent), \n    ui(new Ui::MainWindow), \n    results(0) \n{ \n    ui->setupUi(this); \n    QObject::connect(ui->aboutButton, &QPushButton::clicked, \n                     this, &MainWindow::aboutButtonClicked); \n    QObject::connect(this, &MainWindow::computed, \n                     this, &MainWindow::showResult); \n    QObject::connect(ui->plusButton, &QPushButton::clicked, \n                     this, &MainWindow::plusButtonClicked); \n    QObject::connect(ui->minusButton, &QPushButton::clicked, \n                     this, &MainWindow::minusButtonClicked); \n    QObject::connect(ui->timesButton, &QPushButton::clicked, \n                     this, &MainWindow::timesButtonClicked); \n    QObject::connect(ui->divideButton, &QPushButton::clicked, \n                     this, &MainWindow::divideButtonClicked); \n}\n```\n\n6.  将 About 按钮连接到显示 About 对话框的插槽后，接下来我们将计算出的信号从`MainWindow`连接到它的`showResult`插槽。 请注意，此信号/槽带有一个参数，该参数是要显示的值。 剩下的四个连接将每个操作按钮与代码连接起来，以执行特定的算术运算。\n    如果还没有新的`ResultDialog`，则`showResult`槽创建一个新的`ResultDialog`，将其结果设置为传入的值，并调用该对话框，如以下代码块所示：\n\n```cpp\nvoid MainWindow::showResult(float r) \n{ \n    if (!results) \n    { \n        results = new ResultDialog(this); \n    } \n    results->setResult(r); \n    results->exec(); \n} \n```\n\n`arguments`方法是每个算术函数使用的辅助方法，它从每个输入行获取值，将它们从字符串转换为浮点数，并执行一些错误检查，以确保条目是有效的浮点数，如以下代码块所示：\n\n```cpp\nMainWindow::Arguments MainWindow::arguments() \n{ \n    bool ok1, ok2; \n    float a1 = ui->argument1Input->text().toFloat(&ok1); \n    float a2 = ui->argument2Input->text().toFloat(&ok2); \n    if (!ok1 || !ok2) \n    { \n        QMessageBox messageBox; \n        messageBox.setIconPixmap(QPixmap(\":/icon.png\")); \n        messageBox.setText(\"One of your entries is not a valid number.\"); \n        messageBox.setWindowTitle(\"Error\"); \n        messageBox.exec(); \n    } \n    return Arguments(a1, a2); \n}\n```\n\n`QString`的`toFloat`方法就是这样做的：它将字符串转换为浮点数，如果转换成功，则返回该数字并设置传递给`true`的布尔值，否则设置为`false`。 代码对两个参数输入行都执行此操作，然后检查生成的布尔值，如果其中一个参数格式错误，则在将参数的`QPair`类返回给调用方之前报告错误。\n\n7.  剩余的代码实际执行算术运算，表示在操作完成时发生了计算。 例如，以`plusButtonClicked`插槽为例，如下所示：\n\n```cpp\nvoid MainWindow::plusButtonClicked() \n{ \n    Arguments a = arguments(); \n    emit computed(a.first + a.second); \n} \n```\n\n这将使用`arguments`函数从输入行获取参数，计算总和，然后发出具有求和值的计算信号。 当我们将计算信号连接到`showResult`插槽时，这会触发对`showResult`的调用，这将在必要时创建`ResultDialog`，并显示带有计算结果的对话框。 `minusButtonClicked`、`timesButtonClicked`和`divideButtonClicked`方法都是相似的。\n\n最终结果类似于以下屏幕截图：\n\n![](img/23db2d6e-faab-471e-99fd-31792cc10d37.png)\n\n让我们拭目以待吧！\n\n# 了解有关 Qt 小工具的更多信息\n\n有很多关于使用 Qt 小部件集进行编程的书籍；它是一个非常丰富的小部件集，几乎包括构建普通 Mac、Windows 或 Linux 应用所需的一切，并且具有大多数计算机用户都熟悉的用户界面控件的优势。\n\n我们将在[第 5 章](05.html)，*使用 Qt 小部件*开发应用中详细讨论它，但是您也可以参考[https://doc.qt.io/qt-5/qtwidgets-index.html](https://doc.qt.io/qt-5/qtwidgets-index.html)上的 Qt 文档。\n\n接下来，我们将了解 Qt Quick 和语法如何在编程的代码间歇中工作。\n\n# 介绍 Qt Quick 对声明式用户界面开发的支持\n\n您在最低级别所做的大多数编程都是必需的：您要描述算法应该如何工作(取此值并将其平方；搜索此字符串的第一个匹配项并替换它；以此方式格式化此数据；依此类推)。 使用 Qt Quick，您的编程在很大程度上是声明性的：不是说*如何*，而是说*什么*。 例如，在使用 Qt 的 C++ 中，我们可能会编写如下代码来绘制一个矩形：\n\n```cpp\nQRect r(0, 0, 16, 16); \nQPainter p; \np.setBrush(QBrush(Qt::blue)); \np.drawRect(r); \n```\n\n此代码创建一个 16x16 像素的矩形，分配一个执行绘制的`QPainter`对象，告诉绘制程序其画笔应为蓝色，然后通知绘制程序绘制该矩形。 在 QML 中，我们只需按如下方式编写矩形代码：\n\n```cpp\nimport QtQuick 2.12 \nRectangle { \n    width: 16 \n    height: 16 \n    color: \"blue\" \n} \n```\n\n区别是显而易见的：我们只是说有一个 16x16 像素的蓝色矩形。 如何绘制矩形取决于 Qt Quick 运行时。\n\nQt Quick 的底层语言是 QML。 QML 在很大程度上是基于 JavaScript 的，事实上，您可以用 JavaScript 编写的大多数内容，您也可以用 QML 表达。 表达式语法本质上没有变化；赋值、算术等都是相同的，名称/值系统在功能上也是相同的，尽管对象框架前面可能有类型声明(正如您从我刚才展示的`Rectangle`示例中可以看到的那样)。\n\nA key exception to the *what works in JavaScript works in QML rule* is the lack of a **document object model** (**DO****M**), and things such as the document root for global variables because there's no root context or DOM on which other things hang. If you're porting a web application to QML, be prepared to refactor those parts of your application's architecture.\n\nQML 中的对象必须以树的方式作为父对象；每个 QML 文件都必须包含一个封装对象，然后可以有包含子对象的子对象。 但是，文件顶部的层次结构必须有一个根。 通常，这个根是一个`Rectangle`对象，它绘制一个显示其子元素的基本矩形，或者是一个`Item`对象，它是一个更复杂的用户界面元素的容器，实际上并不绘制任何内容。 每个项都可以有一个名称，该名称存储在它的`id`属性中。\n\n大多数可见的 QML 项都可以有状态，即当特定状态处于活动状态时应用的属性集合。 这使您可以执行一些操作，如声明按钮的休眠状态和按下状态之间的差异；按下按钮只需在状态之间切换，按钮的颜色、阴影等都可以更改，而不需要更改每个单独的属性。\n\nA key concept in QML that's not present in JavaScript is that of binding: if two QML object properties share the same value, changing one changes the other. Binding is a method that couples values with notifications about value changes; it's similar to how references work in C++, or how pass-by-reference works in other languages. This is very handy in coding things such as animations because you can use the value of one object as the value for another object, and when the underlying value changes in one place, both objects are updated.\n\nQML 文件可以相互依赖，也可以包含业务逻辑的 JavaScript 文件。 您已经在每个 QML 文件的顶部看到了一个这样的示例：import 指令指示运行库包括指定的文件和版本；因此，当我们编写`import QtQuick 2.12`时，运行库会找到 Qt Quick 模块版本 2.12 的声明，并在解析文件时包含其符号。 这就是封装功能的方式。 默认情况下，项目中包含 QML 文件，而您也可以包含 JavaScript 文件并将其分配给特定的 JavaScript 变量。\n\n例如，我们可以有一个实现计算器所有功能的 JavaScript 文件`calculatorLogic.js`。 在 QML 中，编写以下代码：\n\n```cpp\nimport QtQuick 2.12 \nimport \"calculatorLogic.js\" as CalculatorLogic \nItem { \n    // someplace in code \n    CalculatorLogic.add(argument1, argument2); \n} \n```\n\n最初的`import`语句加载 JavaScript 文件并将其值赋给 QML 对象`CalculatorLogic`；然后我们可以分派该对象的方法和访问属性，就像它是任何其他 QML 对象一样。\n\nQt Quick 声明了许多基本数据类型；这些数据类型与编写 C++ 代码时在 Qt 中找到的数据类型非常匹配，尽管语法可能不同。 您将遇到的一些最重要的类型如下：\n\n*   具有*x*和*y*属性的点。\n*   一个矩形，具有*x*、*y*、Width 和 Height 属性。\n*   具有宽度和高度属性的大小。\n*   颜色，它是 HTML ARGB 表示法中的引号字符串或 Qt 颜色词典中的命名颜色。 (您能想到的大多数颜色都有 QML 名称。)\n*   2D、3D 或 4D 矢量。\n*   基本类型，包括布尔值、字符串、整数和浮点数。\n\nThere are also a lot of visible types for user interface construction; in this chapter, there's only room to touch on a few. For a detailed list of all QML types and the documentation about those types, see [https://doc.qt.io/qt-5/qmlreference.html](https://doc.qt.io/qt-5/qmlreference.html).\n\nQML 是创建 Qt 应用的另一种方式，无需使用 C++ 编程语言。 如果您熟悉 JavaScript 等 Web 脚本语言，那么学习 QML 对您来说绝对是一件容易的工作。 接下来，让我们看看如何构建一个 Qt 应用。\n\n# 了解 Qt 应用的构建\n\n在[第 1 章](01.html)，*Qt Creator*快速入门中，您已经基本熟悉了 Qt Designer for Qt Quick Applications。 由于我们在前面的示例中使用 C++ 语言创建了一个计算器程序，作为比较，我们还使用 QML 创建了一个计算器程序，以便您可以了解其中的区别。\n\n在我们用 QML 重新创建计算器应用之前，让我们再看一看。 下面的屏幕截图显示了 Qt 快速窗口的 Qt 设计器：\n\n![](img/c2fe01be-65bd-42ad-9103-c1c5dc82a1f6.png)\n\n再次从左侧开始，我们有以下组件：\n\n1.  视图选择器，显示 Qt Designer 视图处于活动状态。\n2.  正在编辑的文件的对象层次结构，显示该文件中可见项之间的父子关系。\n3.  在对象层次结构的上方是一个调色板，其中包含可以拖到 QML 编辑器窗格上的项。\n4.  对象层次结构旁边是对象状态的摘要。\n5.  状态摘要上方是 QML 文件的对象编辑器。\n6.  最后，还有一个属性编辑器，它允许您调整当前选定的 QML 项的属性。\n\nPersonally, I find it easier to just write QML than to use the designer. The syntax takes a little getting used to, but what the designer is good for is previewing the QML you've written by hand, and making minor adjustments to its layout.\n\n说到布局，在我们详细查看示例代码之前，值得注意的是，QML 有一个丰富的动态布局系统。 可见项目具有锚定属性，您可以相对于其相邻视图或父视图锚定项目的侧面。 您在[章](01.html)，*《Qt Creator 入门》*中简要看到了这一点，其中我们创建了一个与其父对象一样大的`MouseArea`对象。 我们也将使用它来控制计算器参数输入行和操作符按钮的布局。\n\n现在让我们开始制作我们的示例代码，方法是从 File 菜单中单击 New File 或 Project，然后浏览向导以创建一个 Qt Quick2.3 应用。 将应用命名为`QtQuickCalculator`。\n\n# 创建 Qt 应用\n\n我们的计算器每一次操作都有一个按钮。 虽然我们可以将每个按钮设置为单独的矩形和`MouseArea`，但使单个 QML 按钮封装按钮的行为(包括按下按钮时外观的变化、按钮标签的位置等)要容易得多。\n\n右键单击项目并选择 Add New...创建一个新的 QML 文件，然后从 Qt 项中选择 QML 文件(Qt Quick2)。 按钮是一个`Rectangle`对象，它包含第二个`Rectangle`对象、按钮的`Text`标签和处理按钮单击的`MouseArea`对象。 将文件命名为`Button.qml`，并对其进行编辑，如下所示：\n\n```cpp\nimport QtQuick 2.12 \n\nRectangle { \n    id: button \n\n    width: 64 \n    height: 64 \n    property alias operation: buttonText.text \n    signal clicked \n\n    color: \"green\" \n\n    Rectangle { \n        id: shade \n        anchors.fill: button; \n        color: \"black\";\n        opacity: 0 \n    } \n\n    Text { \n        id: buttonText \n        anchors.centerIn: parent; \n        color: \"white\" \n        font.pointSize: 16 \n    } \n\n    MouseArea { \n        id: mouseArea \n        anchors.fill: parent \n        onClicked: { \n            button.clicked(); \n        } \n    } \n    states: State { \n        name: \"pressed\"; when: mouseArea.pressed == true \n          PropertyChanges\n        { target: shade; opacity: .4 }\n    } \n}\n```\n\n从代码的顶部开始，我们可以看到以下内容：\n\n*   在此文件的范围内，按钮的 ID 仅为`button`。\n*   它的宽度和高度都是 64 像素。\n*   该按钮有一个可由其客户端配置的属性，即`operation`属性。 该属性实际上是一个别名，这意味着它自动设置`buttonText`的`text`属性的值，而不是一个单独的字段。\n*   该按钮发出单一信号：`clicked`信号。\n*   按钮的颜色是绿色。\n*   有一个矩形填充黑色但不透明度为零的按钮，这意味着在正常使用中它是不可见的(透明)。 当按钮被按下时，我们调整这个矩形的不透明度，以使按钮在被按下时变得更暗。\n*   按钮的文本标签大小为 16 磅，白色，居中。\n*   接受按钮点击的`MouseArea`区域与按钮大小相同，并发出点击的信号。\n*   该按钮有两种状态：默认状态和当`mouseArea.pressed`属性为 true 时出现的第二种状态(因为您在`MouseArea`中按下了鼠标按钮)。 当按下状态时，我们请求一个`PropertyChanges`属性，稍微改变共享矩形的不透明度，使按钮上有阴影，使其变暗。\n\n如果您进入 Qt Designer，您实际上可以看到按钮的两种状态，如您在下一个屏幕截图中所看到的。 状态只是一个名称，一个`when`子句指示状态何时处于活动状态，而`PropertyChanges`属性的集合指示当状态处于活动状态时应更改哪些属性。 所有可见的 QML 项都有一个`state`属性，该属性只是当前活动状态的名称。\n\nQML uses signals and slots similar to Qt in C++, but there's no `emit` keyword. Instead, you declare the signal directly, using the signal keyword and the name of the signal, and then, you invoke the signal as if it were a function call. For each QML item's signal, the slot is named `on` and the signal name. Thus, when we use the button, we write an `onClicked` handler for the `clicked` signal. Note that this is different from when writing slots in C++, where you can name a slot anything you want and connect it to a signal with `connect`.\n\n# 计算器的主视图\n\n在上一节中，我们已经成功创建了自定义按钮对象。 现在，我们希望在计算器的用户界面中使用 Button 对象。 让我们回到编辑器，直接编辑`main.qml`。 我们将直接在代码中声明我们的输入行、结果行和四个操作按钮；如果愿意，您可以对设计器执行相同的操作，然后编辑代码以匹配以下内容：\n\n```cpp\nimport QtQuick 2.12 \nimport QtQuick.Window 2.12\n\nWindow { \n    visible: true\n    width: 360 \n    height: 200 \n    title: qsTr(\"Calculator\")\n    color: \"grey\" \n\n    TextInput { \n        id: argument1 \n        anchors.left: parent.left \n        width: 160 \n        anchors.top: parent.top \n        anchors.topMargin: 10 \n        anchors.leftMargin: 10 \n        anchors.rightMargin: 10 \n        text: \"2\" \n        font.pointSize: 18 \n    } \n\n    TextInput { \n        id: argument2 \n        anchors.right: parent.right \n        width: 160 \n        anchors.top: parent.top \n        anchors.topMargin: 10 \n        anchors.leftMargin: 10 \n        anchors.rightMargin: 10 \n        text: \"2\" \n        font.pointSize: 18 \n    } \n\n    Text { \n        id: result \n        anchors.left: parent.left \n        anchors.right: parent.right \n        anchors.top: argument2.bottom \n        anchors.topMargin: 10 \n        anchors.leftMargin: 10 \n        anchors.rightMargin: 10 \n        text: \"4\" \n        font.pointSize: 24 \n    } \n```\n\n然后，我们继续添加`Row`布局，并将操作按钮放置到布局中：\n\n```cpp\n     Row { \n        id: buttonRow \n        anchors.bottom: parent.bottom \n        anchors.bottomMargin: 20 \n        anchors.left: parent.left\n        anchors.leftMargin: 20\n        spacing: 20 \n        Button { \n            id: plusButton \n            operation: \"+\" \n            onClicked: result.text = parseFloat(argument1.text) + parseFloat(argument2.text) \n        } \n\n        Button { \n            id: minusButton \n            operation: \"-\" \n            onClicked: result.text = parseFloat(argument1.text) - parseFloat(argument2.text) \n        } \n\n        Button { \n            id: timesButton \n            operation: \"*\" \n            onClicked: result.text = parseFloat(argument1.text) * parseFloat(argument2.text) \n        } \n\n        Button { \n            id: divideButton \n            operation: \"/\" \n            onClicked: result.text = parseFloat(argument1.text) / parseFloat(argument2.text) \n        } \n    } \n}\n```\n\n该视图有两个文本输入行：只读文本结果行和操作按钮，包装在行项目中以提供水平布局。 计算器的基本视图是灰色的，这是我们用`color`属性设置的，它位于一个 360×200 像素的窗口中。 控件的位置如下所示：\n\n*   第一个输入行定位在父窗口的左上角，边距为 10 像素。 它有 160 像素长，默认高度为 18 磅的文本输入字段。\n*   第二个输入线锚定在父项的右侧，在顶部和右侧有 10 个像素的边距。 它也有 160 像素长，默认高度为 18 磅的文本输入域。\n*   结果输入行的顶部锚定在输入行的底部和父矩形的左侧。 它的两边各有 10 个像素的边距。\n*   按钮之间的间距为 20 像素，排成一行固定在父级的底部。\n\n如果您调整应用窗口的大小，这些锚点可以让视图很好地回流；输入线分布在窗口的宽度上，底部的按钮栏随着窗口的放大而向下移动。\n\n每个按钮都有一个`click`槽，该槽获得每个输入行的浮点解释并执行适当的算术运算。 它们每个都是`Button`的实例；QML 类是您在上一节中编写的类。 请注意，在`onClicked`处理程序中使用了 JavaScript 函数`parseFloat`。 正如您在前面提到的那样，QML 中的 JavaScript 运行时支持函数，因此我们可以直接调用 JavaScript 函数。\n\n以下屏幕截图显示了完整的计算器应用：\n\n![](img/9c16752d-db3b-41b4-b57e-b1778ba01809.png)\n\n请注意，在运行该应用时，如果您将鼠标悬停在一个按钮上并按下鼠标按钮，您将看到阴影变暗(这在屏幕截图中没有显示)。 这反映了我在上一节中向您展示的按钮中的两个状态。\n\n# 了解有关 Qt Quick 和 QML 的更多信息\n\nQt Quick 的设计初衷是创建流畅的应用，这些应用没有太多的小部件复杂性。 媒体集线器、照片查看器、电话拨号器、Web 浏览器和其他类型的应用不需要与主机平台的外观和感觉相匹配(或者在主机平台都是用 Qt Quick 编写的嵌入式系统上)，这些都是适合 Qt Quick 范例的应用的很好示例。\n\nFor more information about Qt Quick, with a plethora of examples that show you the breadth and power of the platform, see [https://doc.qt.io/qt-5/qtquick-touchinteraction-example.html](https://doc.qt.io/qt-5/qtquick-touchinteraction-example.html).\n\n# 简略的 / 概括的 / 简易判罪的 / 简易的\n\n在本章的前半部分，我们使用 C++ 编程语言成功创建了一个 Qt 应用。 然后，我们还学习了如何使用 Qt Quick 和 QML 脚本语言创建相同的计算器程序。 通过这两个示例，我们了解了这两种方法之间的区别，从而使我们能够决定哪种方法最适合我们的项目。\n\nQt 附带了不是一个而是两个互补的**图形用户界面**(**GUI**)工具包：Qt 小部件，它采用传统的基于小部件的方法进行 GUI 开发；Qt Quick，它提供了一种更适合于媒体盒、某些手机应用、汽车仪表盘和其他嵌入式环境的平台无关用户界面的声明性方法。 对于两者，Qt 都提供了 Qt Designer，这是一个拖放环境，允许您在构建应用时构建、配置和预览用户界面。\n\n接下来，我们了解了 Qt 的核心是信号和槽的概念，这是 Qt 对回调和事件的回答，用于处理当今 GUI 应用所需的后期绑定。 Qt 对象可以发出信号，这些信号是类型安全的函数声明，其他对象可以连接到这些信号，在发出信号时触发方法调用。\n\n在下一章中，您将暂停学习 Qt Creator 和 GUI 开发，专注于 Qt 提供的一些基本功能，如数据结构、文件 I/O、使用 HTTP 的联网和 XML 解析。"
  },
  {
    "path": "docs/app-dev-qt-creator/04.md",
    "content": "# 四、Qt 基础\n\nQt 确实是构建应用的最佳跨平台框架之一。 因此，它有大量的核心类来管理数据，以及围绕平台服务(如线程、文件系统、网络 I/O，当然还有图形)的包装器。\n\n在本章中，我们将讨论一些 Qt 的核心类，您会发现这些类在编写应用时特别方便。 在本讨论中，我们将重点介绍在为应用构建业务逻辑时特别有用的部分 Qt。 我们将从几个有用的数据类开始讨论。 之后，我们将了解 Qt 对多线程的支持，这是保持应用响应性的关键工具。 接下来，我们将讨论访问文件和 HTTP I/O，这是许多应用中的一个重要组件。 最后，我们将看一看 Qt 的 XML 解析器，您可以使用它来创建联网的应用或从文件系统加载 XML 数据。\n\n我们将在本章介绍以下主题：\n\n*   使用 Qt 的核心类表示数据\n*   Qt 中的多线程\n*   使用 Qt 访问文件\n*   使用 Qt 访问 HTTP 资源\n*   使用 Qt 解析 XML\n*   使用 Qt 解析 JSON\n\n# 技术要求\n\n本章的技术要求包括 Qt 5.12.3 MinGW 64 位、Qt Creator 4.9.0 和 Windows 10。\n\n# 使用 Qt 的核心类表示数据\n\n您将遇到的最常见的 Qt 核心类可能是 Qt 的字符串容器类：`QString`。 它具有与 C++ STL`std::wstring`类类似的功能。 与`wstring`一样，它是多字节的。 您可以从传统的 C 样式`char *`字符串或另一个`QString`构造一个。\n\n`QString`有很多辅助方法，其中一些如下所示：\n\n*   `append`：这会将一个`QString`类附加到另一个类上。\n*   `arg`：这用于构建格式化字符串(而不是`sprintf`)。\n*   `at`和`operator[]`：您可以使用它们来访问字符`QString`中的单个字符。\n*   `operator==`、`operator!=`、`operator<`、`operator>`、`operator<=`和`operator>=`：它们比较两个`QStrings`。\n*   `clear`：这将清空`QString`并将其设置为空字符串。\n*   `contains`：这将在一个字符串中搜索另一个字符串或正则表达式。\n*   `count`：此参数统计在`QString`中出现的子字符串或字符。\n*   `startsWith`和`endsWith`：如果`QString`分别以特定字符串开始或结束，则返回 TRUE。\n*   `indexOf`：这将返回子字符串在字符串中第一次出现的索引，如果字符串中不存在该子字符串，则返回`-1`。\n*   `insert`：这会在`QString`中的特定位置插入另一个`QString`。\n*   `lastIndexOf`：这返回子字符串在`QString`中的最后一个索引。\n*   `length`：这返回字符串的长度(以字符为单位)。\n*   `remove`：这将从`QString`中删除字符串的所有匹配项或多个字符。\n*   `setNum`：这会格式化一个数字，并用给定的数字替换`QString`的值。\n*   `split`：这将返回通过在特定分隔符拆分字符串而创建的`QString`个对象的列表(我们稍后将讨论 Qt 列表)。\n*   `toDouble`、`toFloat`、`toInt`和`toLong`：如果可以转换，它们将返回字符串的数字表示形式。\n*   `toLower`和`toUpper`：它们返回转换为\n    小写或大写的字符串的副本。\n*   `truncate`：这将在给定位置截断字符串。\n\nQt 也有许多模板集合类。 其中最通用的是`QList<T>`，它针对基于索引的快速访问以及快速插入和删除进行了优化。 还有`QLinkedList<T>`和`QVector<T>`，前者使用链表结构存储其值，后者将其元素存储在序列向量数组中，因此使用模板类进行索引访问最快，但调整大小的速度较慢。 Qt 还提供了`QStringList`，在所有目的和目的上都与`QList<QString>`相同。\n\n正如您可能想象的那样，这些类型提供了`operator[]`元素，因此您可以访问和分配列表中的任何元素。 您将发现的其他`QList<T>`方法包括：\n\n*   `append`：这会将一个项目追加到列表中。\n*   `at`：这将以只读目的访问列表中的单个元素。\n*   `clear`：这将清空列表。\n*   `contains`：这将在列表中搜索特定元素。\n*   `count`：此参数统计元素在列表中出现的次数。\n*   `empty`：如果列表为空，则返回 TRUE。\n*   `startsWith`和`endsWith`：如果列表以指定的元素开始或结束，则返回 TRUE。\n*   `first`和`last`：它们分别返回列表的第一个和最后一个元素。\n*   `indexOf`：如果某个项目在列表中，则返回该项目第一次出现的索引，如果没有找到该项目，则返回`-1`。\n*   `lastIndexOf`：如果项目在列表中，则返回项目的最后一个索引。\n*   `length`：此参数返回列表的长度。\n*   `prepend`：这会将项目添加到列表中。\n*   `push_back`和`push_front`：它们分别将元素推送到列表的末尾或开头。\n*   `removeAt`、`removeFirst`和`removeLast`：它们删除列表的第 i<sup>、第一个或最后一个元素。</sup>\n*   `replace`：这将替换列表中的一个元素。\n*   `swap`：这将在不同的索引处交换列表的两个元素。\n*   `toStdList`：这将返回`QList`的`std::list<T>`。\n*   `toVector`：这将返回列表的`QVector<T>`。\n\n对于列表，您想要做的一件常见的事情就是迭代它的元素。 `QList`提供与 C++ STL 类似的迭代器，因此您可以迭代列表的元素，如下所示：\n\n```cpp\nQList<QString> list; \nlist.append(\"January\"); \nlist.append(\"February\"); \n ... \nlist.append(\"December\"); \n\nQList<QString>::const_iterator i; \nfor (i = list.constBegin(); i != list.constEnd(); ++ i) \n    cout << *i << endl; \n```\n\n`QList<T>::const_iterator`和`QList<T>::iterator`在列表上提供只读和可变迭代器；您可以分别通过调用`constBegin`或`begin`来获得一个迭代器，并将其与`constEnd`或`end`进行比较，以查看何时位于列表的末尾。\n\n现在我们已经了解了核心类是如何工作的，让我们看看它们是如何使用键-值对的。\n\n# 使用键-值对\n\n键-值对(通常称为映射)是一种容器，它允许您轻松地排序和标识其中包含的数据。 如果你使用过其他语言，你可能知道它是字典或地图。地图中的每个元素都包含一对数据--一个键和一个值。 键是允许您快速查找元素的标识符，而值是将返回给您的实际数据。\n\n对于刚刚开始学习编程的初学者来说，您可能会发现很难掌握键-值对的用法。 让我们用一个简单的例子来说明键-值对的用处。 假设您正在为一所只有数千名学生的学校运行一个系统，并且您试图寻找一个名为“John Smith”的特定学生。 如果您要将这个人的名字与全名列表进行比较，您的程序将需要很长时间才能找到此人。 此外，可能有不止一个学生叫约翰·史密斯。\n\n为了方便起见，我们给每个学生分配了一个唯一的身份证号码。 当我们将所有学生放入键值对中时，ID 号将变为`key`，学生姓名将变为`value`。 因此，当我们尝试寻找特定的学生时，我们将改为搜索 ID 号，这比仅仅比较姓名要快得多，也更容易比较。 一旦我们找到了我们要找的 ID 号，我们就可以获得它的`value`，并确认它是否是我们要找的学生。 在搜索长列表时，键-值对非常高效。\n\nQt 为此提供了四个模板类：`QMap`、`QMultiMap`、`QHash`和`QMultiHash`。 它们共享接口，但是`QHash`提供更快的查找，尽管它的键必须提供`==`运算符和全局散列函数`qHash()`。 这是因为其底层数据结构使用哈希表作为其数据结构。 `QMap`另一方面，将键-值对以对的形式存储在列表中，因此查找速度较慢，但您在选择的键结构上有更大的灵活性。 `QMultiMap`和`QMultiHash`类允许您为单个键存储多个值，而`QMap`和`QHash`只为每个键存储单个值。 大多数情况下，可以使用`QMap`或`QMultiMap`；只有在管理大量键和值的情况下，`QHash`才能在访问性能方面取胜。\n\n下面是带有字符串键和数值的`QMap`示例：\n\n```cpp\nQMap<QString, int> map; \nmap[\"one\"] = 1; \nmap[\"two\"] = 2; \nmap[\"three\"] = 3; \n```\n\n您可以使用`operator[]`或`value`方法查找值。 如果要检查是否为给定键分配了值，请使用`contains`方法。 需要注意的一件事是，`operator[]`与`value`并不完全相同；如果您使用它，并且给定键没有值，它会自动插入您提供的键的默认值，这可能不是您想要的。 其他方法包括：\n\n*   `clear`：这将清除字典。\n*   `empty`：如果字典为空，则返回 TRUE。\n*   `insert`：这会将键-值对插入到字典中。\n*   `key`：这将返回与您传入的值匹配的第一个键。\n*   `keys`：这将返回一个键列表。\n*   `remove`：这将删除您为其提供密钥的元素。\n\n所有这些容器类(包括`QString`)都是轻量级的；当可以使用写入时复制实现时，它们通过引用来携带数据。 因此，让我们创建一个类的实例，并将其分配给第二个实例，如下所示：\n\n```cpp\nQString oneFish = \"red fish\"; \nQString twoFish = oneFish; \n```\n\n`oneFish`和`twoFish`都指向幕后的相同数据，只有当您开始通过其方法更改`twoFish`的值时，它才会获得自己的内存缓冲区。 这是这些类与 STL 类不同的一个重要方面，也是 Qt 在移动设备等低内存平台上获得更好内存性能的关键。\n\n您应该知道的另一个轻量级数据类是`QByteArray`，它抽象了内存中的字节集合，用于 I/O 或其他数据操作。 您可以通过调用`constData`方法来获取`QByteArray`的常量`char *`，或者通过调用其`data`方法来获取可变的`char *`指针。 如果您想知道它的大小，可以调用它的`length`方法。 与 a`QString`一样，a`QByteArray`有很多助手函数来搜索、追加数据、预先添加数据等。 因此，如果您需要操作原始的字节集合，最好在使用 C 样式函数执行自己的实现之前查看`QByteArray`。\n\nFor more about these and other Qt core container classes, see [https://doc.qt.io/qt-5/containers.html](https://doc.qt.io/qt-5/containers.html).\n\n我们已经了解了 Qt 提供的一些基本核心类。 通过了解如何使用这些核心类，我们将能够轻松地使用 Qt 中的所有其他类，因为它们都构建在核心类之上。\n\n在下一节中，我们将学习如何创建利用 Qt 的多线程特性的应用。\n\n# Qt 中的多线程\n\n**线程**是单个应用内的单行执行。 当今几乎所有的操作系统都是多线程的；也就是说，您的应用一次可以有多个并发执行行。 多线程是提高应用响应性的关键方法，因为当今的大多数处理器都可以并行执行多个线程，并且操作系统经过优化可以在多个线程之间共享资源。\n\nQt 通过三个关键类支持主机操作系统上的多线程：\n\n*   `QThread`\n*   `QSemaphore`\n*   `QMutex`\n\n第一个参数`QThread`表示单个执行线程，而后两个参数用于同步线程对数据结构的访问。\n\n根据设计，应用完全在用户线程上运行，用户线程是在应用启动时启动的单个执行线程。 您可以通过子类化`QThread`并覆盖`run`方法来创建新的执行线程(不能操作用户界面)。 然后，当您需要执行开销很大的操作时，只需创建`QThread`子类的一个实例并调用其`start`方法即可。 (如果您熟悉 Java 中的线程，这类似于 Java。)。 反过来，该方法调用您的`run`方法，线程一直运行到`run`退出。 一旦`run`退出，它将使用`finished`信号发出完成。\n\n您可以将插槽连接到该信号以观察任务完成情况。 当您使用线程执行后台工作(如网络事务)时，这尤其方便；您在线程的后台执行网络事务，您将知道 I/O 何时完成，因为您的线程将完成并发出`finished`方法。 在本章后面的*使用 Qt*访问 HTTP 资源一节中，我们将看到一个这样的例子。\n\n下面是最简单的线程示例：\n\n```cpp\nclass MyThread : public QThread \n{ \n    Q_OBJECT \n    void run() Q_DECL_OVERRIDE\n    { \n        /* perform the expensive operation */ \n    } \n}; \n\nvoid MyObject::startWorkInAThread() \n{ \n    MyThread *myThread = new MyThread(this); \n    connect(myThread, &MyObject::threadFinished, this, \n        MyObject::notifyThreadFinished); \n    connect(myThread, &MyThread::finished, myThread, \n        &QObject::deleteLater); \n    myThread->start(); \n}\n```\n\n在我们深入到代码之前，让我来解释一下我们为什么要这样做。 如果您正在处理繁重的计算、批量文件操作或繁重的网络 I/O，则应该将它们放在单独的线程中，而不是在主线程中运行。 这是因为这些操作非常“昂贵”，可能会在 CPU 处理应用时使其停止。 用外行的话说，“昂贵的”操作意味着 CPU 需要很长时间才能完成计算任务的繁重进程，这就是您不希望这些繁重进程在主线程上运行的原因。\n\n相反，我们创建第二个线程，并将这些代价高昂的操作放入第二个线程的`run`方法中。 只要执行`run`，线程就会运行；执行完成后，它将发出`finished`信号。 要启动其中一个执行线程，只需创建该线程的一个新实例并调用其`start`方法。 然后将两个信号处理程序连接到线程的`finished`方法；第一个信号处理程序在线程完成时简单地删除该线程，第二个信号处理程序(未显示)使用线程的执行结果更新 UI。\n\n多线程编程可能很棘手，因为您需要注意这样的情况：一个线程正在向数据结构写入数据，而另一个线程想要从该数据结构中读取数据。 如果不小心，可能会导致读取线程接收垃圾或更新了一半的数据，从而导致难以重现的编程错误。 Qt 提供了两个线程原语`QMutex`和`QSemaphore`，用于阻止线程在资源(如数据结构)上的执行，从而允许线程在线程运行时独占访问该资源。\n\n`QMutex`类有两个方法：\n\n*   `lock`\n*   `unlock`\n\n要确保一次只有一个线程可以访问一个代码块，请创建一个互斥锁并调用其`lock`方法。 当执行结束时，您必须调用`unlock`；否则，没有其他线程可以运行该代码。 还有`tryLock`方法，它尝试获取锁，如果在指定的超时时间内无法获取锁，则立即返回，让您执行其他操作，而不是等到线程锁定互斥锁。\n\n`QSemaphore`是`QMutex`的通用版本，允许您管理一个由*n*项组成的池；线程不会阻塞单个互斥锁上的执行，而是会一直阻塞，直到它能够获得您在调用其`acquire`方法时指定的资源数量。 当您使用完许多资源时，您可以调用`release`方法，指示您要释放的项目数。 `QSemaphore`还有一个`tryAcquire`方法和一个`available`方法，前者在资源获取在期望的超时失败时立即返回，后者返回当前可用的资源数量。\n\n下面是一些示例代码，说明如何使用`QSemaphore`类：\n\n```cpp\nconst int bufferSize = 10;\nQSemaphore sem(bufferSize); // sem.available() == 10\n\nsem.acquire(6); // sem.available() == 4\nsem.acquire(4); // sem.available() == 0\nsem.release(7); // sem.available() == 7\nsem.release(3); // sem.available() == 10\n\nsem.tryAcquire(2); // sem.available() == 8, returns true\nsem.tryAcquire(540); // sem.available() == 8, returns false\n```\n\n尽管 Qt 为我们提供了一些让多线程变得更容易的类，但多线程本身在编程学科中是一个相当高级的话题。 但是，您必须能够掌握多线程的概念，然后才能利用诸如`QThread`或`QMutex`之类的类将计算工作负载高效地分散到您的 CPU 线程上。 请注意，多线程并不是提高应用性能的绝对解决方案。 如果你做得不对，情况可能正好相反。\n\nSince version 5.3, Qt has also introduced some higher-level programming constructs with Qt Concurrent that are beyond the scope of this book. For more information on `QThread` and its supporting classes, or Qt Concurrent, consult the Qt documentation at [https://doc.qt.io/qt-5/threads-technologies.html](https://doc.qt.io/qt-5/threads-technologies.html).\n\n我们已经学习了如何通过使用诸如`QThread`、`QSemaphore`和`QMutex`这样的类来支持多线程，从而在 Qt 中运行繁重的操作。 接下来，我们将学习如何使用 Qt 访问本地存储中的文件。\n\n# 使用 Qt 访问文件\n\n文件基本上是以字节流的形式存储的数字信息，驻留在硬盘中的某个地方。 如果您的程序需要保存或加载数据，例如用于文字处理、图像编辑、媒体流或程序配置，则需要访问存储在本地硬盘上的文件。 Qt 为我们提供了允许我们轻松访问文件系统的类，而无需考虑操作系统的类型。\n\nQt 将更通用的字节流的概念封装在其`QIODevice`类(即`QFile`的父类)以及网络 I/O 类(如`QTcpSocket`)中。 当然，我们不会直接创建`QIODevice`实例，而是创建一个类似于`QFile`的子类，然后直接使用`QFile`实例对文件进行读写。\n\nFiles and network access usually take time, and thus your applications shouldn't work with them on the main thread. Consider creating a subclass of `QThread` to perform I/O operations such as reading from files or accessing the network.\n\n要开始使用文件，我们必须首先使用`open`方法打开它。 `open`方法接受单个参数，即打开文件的方式，它是以下各项的按位组合：\n\n*   `QIODevice::ReadOnly`：用于只读访问。\n*   `QIODevice::WriteOnly`：这用于只写访问。\n*   `QIODevice::ReadWrite`：用于读写访问。\n*   `QIODevice::Append`：此选项仅用于追加到文件。\n*   `QIODevice::Truncate`：用于截断文件，在写入之前丢弃所有先前的内容。\n*   `QIODevice::Text`：用于将文件视为文本，在读写过程中将换行符转换为平台表示形式。\n*   `QIODevice::Unbuffered`：这用于绕过输入和输出的任何缓冲。\n\n这些标志可以使用按位二进制运算符或*`|`*运算符组合在一起。 例如，读写文本文件的常见组合是`QIODevice::ReadWrite | QIODevice::Text`。 实际上，`QIODevice:ReadWrite`在内部定义为`QIODevice::Read | QIODevice::Write`。\n\n打开文件后，可以通过调用文件的`read`方法并传递要读取的字节数来读取一定数量的字节；生成的`QByteArray`对象包含要读取的数据。 类似地，您可以通过调用`write`来编写`QByteArray`，或者使用接受常量的重载`write`方法来编写`QByteArray``* char`。 在这两种情况下，`write`还需要写入字节数。 如果您只想将文件的全部内容读入单个缓冲区，则可以调用`readAll`，它将返回文件全部内容的`QByteArray`。\n\n某些`QIODevice`子类(如`QFile`)是可查找的；也就是说，您可以将读/写游标定位在文件中的任何位置，或确定其位置。 您可以使用`seek`方法将光标定位在文件中的特定位置，使用`pos`方法获取文件光标的当前位置。 请注意，其他`QIODevice`子类，如用于网络 I/O 的子类，不支持`seek`和`pos`方法，但如果您尝试使用它们，它们会正常失败。\n\n如果希望在不实际移动光标的情况下查看数据，可以调用`peek`并传递要返回的字节数；结果是`QByteArray`。 在`peek`之后调用`read`将返回相同的数据，因为`peek`不会使光标前进超过您所查看的数据。 当创建一个复杂的解析器需要了解其实现的多个位置的传入数据时，`peek`方法非常方便；您可以查看数据，决定如何解析数据，然后调用`read`来获取数据。\n\n要确定您是否在文件的末尾并且没有更多的数据要读取，可以调用`atEnd`，如果没有更多的数据要读取，则返回 TRUE。 如果您想知道文件中有多少字节，可以调用`bytesAvailable`，它返回可供读取的字节数(如果已知，当然，网络套接字可能不会携带该信息)。\n\n在处理文件时，我们通常也需要处理目录。 `QDir`类允许我们检查目录的内容，确定文件或目录是否存在，以及删除文件或目录。 需要注意的一件事是，无论主机平台使用什么目录分隔符路径，Qt 始终使用正斜杠--`/`*来表示目录，因此即使我们正在为 Windows 编写 Qt 程序，我们也会使用正斜杠，而不是反斜杠。 这使得编写在 Windows 和 Linux 上运行的跨平台兼容代码变得很容易，而不需要对目录处理代码使用特殊情况。\n\n首先，通过传递文件路径来创建`QDir`的实例；之后，您可以使用它执行以下操作：\n\n*   通过调用`absolutePath`获取目录的绝对路径。\n*   通过调用`cd`切换到其他有效目录。\n*   通过调用`entryInfoList`获取目录中的文件列表。\n*   通过调用`exists`确定特定文件或目录是否存在。\n*   通过调用`isRoot`确定该目录是否为文件系统的根目录。\n*   通过调用`remove`并将文件名传递给`remove`来删除文件。\n*   通过调用`rename`重命名文件。\n*   通过调用`rmdir`并将目录名称传递给`remove`来删除空目录。\n*   使用`==`和`!=`运算符比较两个目录。\n\n当然，文件的位置(如应用首选项和/或临时文件)因平台而异；`QStandardPaths`类有一个静态的`standardLocations`方法，该方法返回我们要查找的存储类型的路径。 要使用它，我们从`QStandardPaths::StandardLocation`枚举中传递一个值，该值如下所示：\n\n*   `DesktopLocation`：这将返回桌面目录。\n*   `DocumentsLocation`：这将返回文档目录。\n*   `MusicLocation`：此参数返回音乐在文件系统上的位置。\n*   `PicturesLocation`：此参数返回照片在文件系统上的位置。\n*   `TempLocation`：返回存储临时文件的路径。\n*   `HomeLocation`：这将返回当前用户主目录的路径。\n*   `DataLocation`：这将返回持久数据特定于应用的目录的路径。\n*   `CacheLocation`：这将返回应用可以缓存数据的路径。\n*   `ConfigLocation`：这将返回应用可以存储配置设置的路径。\n\n好了，理论部分说得够多了。 让我们开始着手编写一些代码吧！ 首先，让我们看看如何加载文本文件并读取其内容：\n\n```cpp\nQFile file(\"myFile.txt\");\nif (!file.open(QIODevice::ReadOnly | QIODevice::Text))\n{\n    return;\n}\nwhile (!file.atEnd())\n{\n    QByteArray line = file.readLine();\n    qDebug() << line;\n}\n```\n\n我们首先将一个`myFile.txt`文本文件的名称提供给一个`QFile`对象进行初始化。 然后，我们调用`open()`让 Qt 打开该文件，并告诉它作为文本文件只读(不写)。 如果文件已成功打开，`open()`函数将返回`true`，如果打开文件失败，则返回`false`。\n\n打开文件后，我们使用`while`循环检查读取过程是否已到达文本文件的末尾。 如果没有，那么我们重复调用`readLine()`来读取文本文件的每一行，直到它到达末尾。 然后，我们使用`qDebug()`显示刚刚阅读的文本。\n\n接下来，让我们尝试一些不同的东西：\n\n```cpp\nQString fileName = QFileDialog::getOpenFileName(this, \"Open Image\", \"\", \"Image Files (*.png *.jpg *.bmp)\", QStandardPaths::DesktopLocation);\nQImage image = QImage(fileName);\n```\n\n前面的代码只是打开一个文件选择对话框，让用户选择一个图像文件。 我们调用`getOpenFileName()`来启动文件选择对话框。 我们还将对话框的标题设置为`Open Image`，并将文件类型限制为 PNG、JPG 和 BMP。 然后，我们使用`DesktopLocation`枚举将默认位置设置为用户的桌面。 一旦用户选择了图像文件，图像文件的完整路径将存储在`fileName`变量中。 然后，我们可以使用`fileName`变量将图像数据转换为`QImage`对象。\n\nFor more information about files and network I/O, see the Qt documentation at [https://doc.qt.io/qt-5/io-functions.html](https://doc.qt.io/qt-5/io-functions.html).\n\n就是这样，我们已经明确了如何使用 Qt 类从本地存储加载文件。 让我们继续下一节，学习如何使用 Qt 访问 HTTP 资源。\n\n# 使用 Qt 访问 HTTP 资源\n\n在当今网络世界中，一件常见的事情是使用**超文本传输协议**(**HTTP**)来访问 Web 上的远程资源或服务。 要做到这一点，您的应用应该首先使用 Qt 的支持来选择一个承载网络来发出 HTTP 请求。 然后，它应该使用其对 HTTP 的支持，通过承载网络服务打开的网络连接发出一个或多个 HTTP 请求。\n\n首先，您需要编辑项目的(`.pro`)文件以包括以下内容，以确保在 Qt 声明中包含网络模块：\n\n```cpp\nQT += network\n```\n\n今天的计算设备支持多种方式访问网络。 例如，Android 平板电脑可以内置 4G 无线**广域网**(**WAN**)适配器和 Wi-Fi 无线电，为不同的接入点提供多种网络配置。 Android 平台包含复杂的代码，可根据以最佳成本提供最佳带宽的无线网络调出合适的网络接口，或提示用户选择所需的 Wi-Fi 网络。 各种 Linux 发行版都有类似的功能，Microsoft Windows 和 MacOS X 也是如此。为了封装此功能，Qt 提供了承载网络模块，该模块封装了平台的底层支持，以提示用户使用哪个网络连接、如何启动网络连接、停止网络连接等。\n\n如果您正在编写网络应用，以确定如何连接到网络，则在首次尝试使用网络之前，您需要使用此模块来提示用户。 要做到这一点，最简单的方法是使用如下代码片段：\n\n```cpp\nbool OpenNetworkConnection()\n{\n    QNetworkConfigurationManager manager;\n    const bool canStartIAP = (manager.capabilities() & QNetworkConfigurationManager::CanStartAndStopInterfaces); // Is there \n//  default access point, use it\n    QNetworkConfiguration cfg = manager.defaultConfiguration();\n    if (!cfg.isValid() || (!canStartIAP && cfg.state() != \n        QNetworkConfiguration::Active))\n    {\n        QMessageBox::information(this, tr(\"Network\"), tr(\"No Access \n            Point found.\")); \n        return false; \n    } \n    QNetworkSession* session = new QNetworkSession(cfg, this); \n    session->open(); \n    return session->waitForOpened(-1); \n} \n```\n\n此代码执行以下操作：\n\n*   它创建网络配置管理器的一个实例。\n*   它确定管理器是否可以启动和停止系统上的网络接口。\n*   它会获取网络连接的默认配置。\n*   如果默认配置无效，或者配置管理器无法启动网络配置且没有网络连接，则会弹出一个对话框，指示应用无法启动新的网络连接，并返回 False 以通知调用方没有建立网络连接。\n*   如果默认配置有效，代码将获取由默认配置配置的网络会话，将其打开，然后等待，直到连接打开。 在幕后，平台可能会提示用户进行所需的网络连接、管理一个或多个无线电等。 一旦配置了会话，您的应用就可以使用网络了。\n\n一旦建立了到网络的连接，您就可以发出网络请求，或者创建用于 TCP 或 UDP 访问的低级通信。 接下来，我们将了解当需要在线通信时 HTTP 请求是如何工作的。\n\nFurther information about low-level networking will not be discussed in this book; if you're curious, you can see the Qt documentation at [https://doc.qt.io/qt-5/qtnetwork-index.html](https://doc.qt.io/qt-5/qtnetwork-index.html)[.](https://doc.qt.io/qt-5/qtnetwork-index.html)\n\n# 执行 HTTP 请求\n\nQt 提供了三个关键类来执行 HTTP 请求：`QNetworkAccessManager`、`QNetworkRequest`和`QNetworkReply`。 HTTP 请求对于需要与服务器进行在线通信的应用非常重要，例如：用户登录、数据输入、新闻提要、通知等。\n\n我们使用`QNetworkAccessManager`请求来配置 HTTP 请求的语义，配置代理服务器以及实际发出请求。 最简单的做法是创建一个，将它的`finished`信号连接到您希望在请求完成时调用的槽，然后调用它的`get`方法，如下所示：\n\n```cpp\nmNetManager = new QNetworkAccessManager(this); \nconnect(mNetManager, &QNetworkAccessManager::finished, this, &MainWindow::handleNetFinished); \n\n// later, when you want to make a request \nQNetworkReply *reply = mNetManager->get(QNetworkRequest(QUrl(url))); \n```\n\n`QNetworkAccessManager`类有用于 HTTP 的每个方法`GET`、`POST`、`DELETE`、`HEAD`和`PUT`的方法，分别恰当地命名为`get`、`post`、`delete`、`head`和`put`。 对于大多数事务，要简单地获取数据，您将使用`get`，这是获取远程网络资源的标准 HTTP 方法。 如果要通过`put`方法触发远程过程调用，请调用`QNetworkAccessManager`类的参数`put`方法，传递一个`QNetworkRequest`对象和一个指向要放入服务器的数据的`QByteArray`或`QIODevice`指针。\n\nIf you need to configure a proxy server as part of your request, you can do so using the `setProxy` method of `QNetworkAccessManager`. Note that Qt will configure itself with whatever system the HTTP proxy is by default, so you should only need to override the proxy server settings if you're working with an application-specific proxy server for your application.\n\n`QNetworkAccessManager`类使用`QNetworkRequest`类来封装请求的语义，允许您通过调用其`setHeader`或`setRawHeader`方法来设置伴随请求的 HTTP 标头。 `setHeader`方法允许您设置特定的 HTTP 标头，如`User-Agent`标头，而`setRawHeader`方法允许您提供自定义的 HTTP 标头名称和值作为`QByteArray`值。\n\n发出请求后，`QNetworkAccessManager`类将接管，执行必要的网络 I/O 以查找远程主机、联系远程主机并发出请求。 当应答准备好时，它用`finished`信号通知您的代码，传递与请求相关联的`QNetworkReply`类。 使用`QNetworkReply`类，您可以通过调用`header`或`rawHeader`分别获取标准或自定义 HTTP 标头来访问与回复相关联的标头。 `QNetworkReply`继承自`QIODevice`，因此您只需使用`read`或`readAll`方法根据需要读取响应，如下所示：\n\n```cpp\nvoid MyClass::handleNetFinished(QNetworkReply* reply) \n{ \n    if (reply->error() == QNetworkReply::NoError)\n    { \n        QByteArray data = reply->readAll(); \n    }\n    else\n    { \n        qDebug() << QString(\"net error %1\").arg(reply->error()); \n    } \n} \n```\n\nFor more information about bearer network configuration, see the Qt documentation at [https://doc.qt.io/qt-5/bearer-management.html](https://doc.qt.io/qt-5/bearer-management.html). For more information about all of Qt's support for networking, see the Qt documentation at [https://doc.qt.io/qt-5/qtnetwork-index.html](https://doc.qt.io/qt-5/qtnetwork-index.html). There are also some good network samples at [https://doc.qt.io/qt-5/examples-network.html](https://doc.qt.io/qt-5/examples-network.html).\n\n我们已经学习了如何允许我们的应用通过 HTTP 请求与在线服务器通信。 接下来，我们将研究如何使用 Qt 解析 XML 数据。\n\n# 使用 Qt 解析 XML\n\nQt 的早期版本有许多 XML 解析器，每个解析器都适合不同的任务和不同的解析风格。每个 XML 解析器在旧版本的 Qt 中都用于不同的格式。 幸运的是，在 Qt5 中，这一点得到了简化；目前，您只需要一个 XML 解析器来解析 XML 数据。 在最新的 Qt 版本中使用的关键 xml 解析器是`QXmlStreamReader`https://doc.qt.io/qt-5/qxmlstreamreader.html 类(有关详细信息，请参阅[XML](https://doc.qt.io/qt-5/qxmlstreamreader.html)类)。 这个类从`QIODevice`子类读取，一次读取一个 XML 标记，允许您打开解析器遇到的标记类型。 因此，我们的解析器如下所示：\n\n```cpp\nQXmlStreamReader xml; \nxml.setDevice(input); \nwhile (!xml.atEnd())\n{ \n    QXmlStreamReader::TokenType type = xml.readNext(); \n    switch(type) \n    { \n        ... // do processing \n    } \n} \nif (xml.hasError())\n{ \n    ... // do error handling \n} \n```\n\n每次调用`readNext`方法时，`QXMLStreamReader`类依次读取 XML 的每个标记。 对于每次读取的标记，`readNext`返回读取的标记的类型，可以是以下类型之一：\n\n*   `StartDocument`：表示文档的开头。\n*   `EndDocument`：表示文档结束。\n*   `StartElement`：这表示元素的开始。\n*   `EndElement`：这表示元素的结束。\n*   `Characters`：这表示读取了某些字符。\n*   `Comment`：这表示已读取评论。\n*   `DTD`：这表示文档类型声明已读取。\n*   `EntityReference`：这表示读取了无法解析的实体引用。\n*   `ProcessingInstruction`：这表示读取了 XML 处理指令。\n\n了解了使用 Qt 解析 XML 的这些基础知识之后，让我们看看如何使用它们。\n\n# 将 XML 解析与 HTTP 结合使用\n\n让我们用一些示例代码将多线程、HTTP I/O 和 XML 解析结合在一起，该示例代码使用从远程服务器获取具有唯一标记的平面 XML 文档，并从 XML 解析选定的标记，将结果作为名称-值对存储在`QMap<QString, QString>`中。\n\n平面 XML 文件是没有嵌套元素的文件，即以下形式的 XML 文档：\n\n```cpp\n<?xml version=\"1.0\"?> \n<document> \n    <tag>Value</tag> \n    <tag2>Value 2</tag2> \n</document> \n```\n\n我们将从`WorkerThread`类头开始：\n\n```cpp\n#include <QMap> \n#include <QThread> \n#include <QXmlStreamReader>\n#include <QNetworkAccessManager>\n#include <QNetworkReply>\n\nclass WorkerThread : public QThread \n{ \n    Q_OBJECT \n\npublic: \n    WorkerThread(QObject* owner); \n    void run(); \n\n    void fetch(const QString& url); \n    void cancel(); \n\nsignals: \n    void error(const QString& error); \n    void finished(const QMap<QString, QString>&); \n\nprivate slots: \n    void handleNetFinished(QNetworkReply* reply); \n\nprivate: \n    bool mCancelled; \n    QNetworkAccessManager* mNetManager; \n    QNetworkReply* mReply; \n} \n```\n\n这个类扩展了`QThread`，所以它是一个`QObject`。 它的槽是私有的，因为它只在这个类的作用域内使用，不能作为其公共接口的一部分使用。 要使用它，您需要创建它并调用它的`fetch`方法，将 URL 传递给 FETCH。 它的作用是发出成功结果的信号，通过`finished`信号从 XML 传递名称-值对的字典，或者如果请求失败，则通过`error`信号传递带有错误消息的字符串。 如果我们启动一个请求，而用户想要取消它，我们只需调用`cancel`方法。\n\n该类携带的数据非常少：一个新的`mCancelled`取消标志，它用来执行 I/O 的`QNetworkAccessManager`实例`mNetManager`，以及来自请求的`QNetworkReply`请求`mReply`。 接下来，我们将了解如何实现`WorkerThread`来解析 XML。\n\n# 实现 WorkerThread\n\n现在我们已经了解了 XML 中的解析工作原理，我们可以看到`WorkerThread`核心的实现是什么样子，如下所示：\n\n1.  以下代码显示了`WorkerThread`的实现：\n\n```cpp\nWorkerThread::WorkerThread(QObject* owner)\n{\n    this->setParent(owner);\n    mNetManager = new QNetworkAccessManager(this);\n    connect(mNetManager, &QNetworkAccessManager::finished, this, &WorkerThread::handleNetFinished);\n}\n\nvoid WorkerThread::run() \n{ \n    QXmlStreamReader xml; \n    QXmlStreamReader::TokenType type; \n    QString fieldName; \n    QString value; \n    QString tag; \n    bool successful = false; \n    bool gotValue = false; \n    QMap<QString, QString> result; \n\n    xml.setDevice(mReply); \n```\n\n2.  然后，我们继续编写代码，并通过循环遍历文件并读取每个 XML 元素来开始解析 XML 数据：\n\n```cpp\n    while(!xml.atEnd()) \n    { \n        // If we've been cancelled, stop processing. \n        if (mCancelled) break; \n\n        type = xml.readNext(); \n        bool gotEntry = false; \n        switch( type ) \n        { \n            case QXmlStreamReader::StartElement: \n            {  \n                QString tag = xml.name().toString().toLower(); \n                fieldName = tag; \n                gotValue = false; \n                qDebug() << \"tag\" << tag;\n            } \n            break; \n```\n\n3.  我们继续检查 XML 元素的类型，并相应地保存其值：\n\n```cpp\n            case QXmlStreamReader::Characters: \n            // Save aside any text \n            if (!gotValue) \n            { \n                value = xml.text().toString().simplified(); \n                if (value != \"\")\n                {\n                    gotValue = true;\n                    qDebug() << \"value\" << value;\n                }\n            } \n            break; \n            case QXmlStreamReader::EndElement: \n            // Save aside this value \n            if (gotEntry && gotValue)\n            { \n                result[fieldName] = value; \n            }  \n            gotEntry = false; \n            gotValue = false; \n            break; \n            default: \n            break; \n        } \n    } \n```\n\n4.  然后，我们检查解析是否成功。 如果成功，我们触发`finished`信号，如果不成功，则调用`error`信号：\n\n```cpp\n    successful = xml.hasError() ? false : true; \n\n    if (!mCancelled && successful) { \n        emit finished(result); \n    } else if (!mCancelled) { \n        emit error(tr(\"Could not interpret the server's response.\")); \n    } \n} \n```\n\n5.  之后，我们编写`fetch`和`handleNetFinished`函数从服务器获取数据。 我们还编写了用于取消请求的`cancel`函数：\n\n```cpp\nvoid WorkerThread::fetch(const QString& url) \n{ \n    // Don't try to re-start if we're running \n    if (isRunning()) { this->cancel(); } \n\n    QNetworkReply *reply = mNetManager->get(QNetworkRequest\n        (QUrl(url))); \n\n    if (!reply) { emit error(tr(\"Could not contact the server.\")); } \n} \n\nvoid WorkerThread::cancel()\n{ \n    mCancelled = true; \n    wait(); \n}; \n\nvoid WorkerThread::handleNetFinished(QNetworkReply* reply) \n{ \n    // Start parse by starting the thread. \n    if (reply->error() == QNetworkReply::NoError)\n    { \n        if (!this->isRunning())\n        { \n              mReply = reply; \n              start(); \n        } \n    }\n    else\n    { \n        emit error(tr(\"A network error occurred.\")); \n        qDebug() << QString(\"net error %1\").arg(reply->error()); \n    } \n} \n```\n\n这里有很多代码(完整的类在本书附带的下载中显示)，所以让我们一个方法一个方法地学习：\n\n*   构造函数初始化每个成员字段，并将`QNetworkAccessManager`的`finished`信号连接到我们的`handleNetFinished`\n    槽。 (这里省略了构造器，但本书附带的示例代码\n    中提供了构造器。)\n*   `run`方法是该类的核心，负责读取和解析 XML 响应。 我们将`read`和 parse 放在`run`方法中，因为它可能会占用最多的时间，这样它就可以在后台线程上运行，这样它就不会阻塞用户界面。\n\n`run`方法执行以下操作：\n\n*   使用我们的网络响应`mReply`初始化`QXMLStreamReader`类。\n*   循环遍历它正在读取的 XML 文档中找到的标记。 对于每个标记：\n    *   如果标记是开始元素，则它获取标记的名称并注意到它已接收到新的开始元素。\n    *   如果标签是一个字符串，它会将该字符串保存下来，并注意到它有一个标签的值。\n    *   如果它是一个 XML 元素的末尾，并且它同时有一个标记名\n        和一个值，它会将该标记值分配给结果散列中指定的槽。\n\n*   一旦读取了所有标记或出现错误，代码将首先测试错误。\n*   如果解析没有取消并且成功，代码将发出`finished`信号，传递结果为`QMap`的 XML 文档中的名称和值。\n*   如果解析遇到错误，代码将发出错误信号。\n*   在使用`QNetworkAccessManager`发出 HTTP`GET`请求之前，如果有一个请求挂起，则`fetch`方法简单地取消该请求。\n*   `cancel`方法设置由`run`方法检查的取消标志，并等待线程完成，确保在`cancel`返回之前取消。\n*   当 HTTP`GET`请求返回时，`QNetworkAccessManager`调用`handleNetFinished`方法，保存生成的网络请求，启动从远程服务器读取的线程，并解析结果。 如果发生错误，它会用错误信号发出错误信号，并将 HTTP 错误消息记录到调试器控制台。\n\n现在我们已经了解了如何通过 HTTP web 请求获取 XML 数据，然后使用 Qt 解析 XML 数据。 接下来，我们将学习如何解析另一种称为 JSON 的流行格式。\n\n# 使用 Qt 解析 JSON\n\nJSON 是继 XML 之后流行的数据传输格式。 它于 2013 年首次标准化，自那以来已成为网络上最受欢迎的格式。 JSON 数据如下所示：\n\n```cpp\n{\n \"firstName\": \"John\",\n \"lastName\": \"Smith\",\n \"age\": 42,\n \"address\": {\n \"streetAddress\": \"14 2nd Street\",\n \"city\": \"New York\",\n \"state\": \"NY\",\n \"postalCode\": \"10021-3100\"\n },\n \"phoneNumbers\": [{\n \"type\": \"home\",\n \"number\": \"212 686-7890\"\n },\n {\n \"type\": \"mobile\",\n \"number\": \"321 456-7788\"\n }]\n}\n```\n\n如您所见，它与 XML 格式完全不同。 它如此受欢迎的主要原因是因为它非常便于人类阅读，而且与 XML 相比要短得多，没有所有的开始和结束标记。\n\n要使用 Qt 解析 JSON 数据，让我们开始编写一些代码：\n\n1.  首先，我们必须包括与 JSON 类相关的标头：\n\n```cpp\n#include <QJsonDocument>\n#include <QJsonObject>\n#include <QVariantMap>\n```\n\n2.  之后，我们将尝试解析前面的 JSON 数据，在转换为`QString`格式时如下所示：\n\n```cpp\nQString jsonString = \"{\\\"firstName\\\": \\\"John\\\",\\\"lastName\\\": \\\"Smith\\\",\\\"age\\\": 42,\\\"address\\\": {\\\"streetAddress\\\": \\\"14 2nd Street\\\",\\\"city\\\": \\\"New York\\\",\\\"state\\\": \\\"NY\\\",\\\"postalCode\\\": \\\"10021-3100\\\"},\\\"phoneNumbers\\\": [{\\\"type\\\": \\\"home\\\",\\\"number\\\": \\\"212 686-7890\\\"},{\\\"type\\\": \\\"mobile\\\",\\\"number\\\": \\\"321 456-7788\\\"}]}\";\n```\n\n3.  接下来，我们将通过首先将数据转换为`QJsonDocument`对象来解析数据，然后通过调用`QJsonDocument::fromJson()`函数获得`QJsonObject`对象。\n4.  然后，我们将`QJsonObject`转换为`QVariantMap`，然后才能从它获得所需的数据：\n\n```cpp\nQJsonDocument doc = QJsonDocument::fromJson(jsonString.toUtf8());\nQJsonObject obj = doc.object();           // Get the json object\nQVariantMap map = obj.toVariantMap();     // Convert json object to variant map\n\nqDebug() << map[\"firstName\"].toString();  // Obtain firstName data\n```\n\n5.  前面的代码会产生以下结果：\n\n```cpp\nJohn\n```\n\n如您所见，代码简单明了。 我们还利用从本章开头学到的有关键-值对的知识，从我们的 JSON 变体映射中获得了`firstName`数据。\n\n6.  接下来，我们将了解如何从 JSON 数组获取数据，例如：\n\n```cpp\n{\n\"name\":\"John\",\n\"age\":30,\n\"cars\":[ \"Ford\", \"BMW\", \"Fiat\" ]\n}\n```\n\n7.  `cars`数据由数组格式包装的三个单独的数据项组成。 让我们开始编写一些代码来解析 JSON 数据。 同样，我们将把前面的 JSON 文本转换为`QString`格式：\n\n```cpp\nQString jsonString = \"{\\\"name\\\":\\\"John\\\",\\\"age\\\":30,\\\"cars\\\":[ \\\"Ford\\\", \\\"BMW\\\", \\\"Fiat\\\" ]}\";\n```\n\n8.  然后，我们必须在代码的顶部添加两个新的头，即`QJsonArray`和`QJsonValue`：\n\n```cpp\n#include <QJsonArray>\n#include <QJsonValue>\n```\n\n9.  之后，我们开始解析 JSON 数据，如下所示：\n\n```cpp\nQJsonDocument doc = QJsonDocument::fromJson(jsonString.toUtf8());\nQJsonObject obj = doc.object();        // Get the json object\nQJsonValue value = obj.value(\"cars\");  // Get cars data in QJsonValue format\nQJsonArray array = value.toArray();    // Convert it to QJsonArray\n```\n\n10.  一旦将`cars`数据转换为`QJsonArray`格式，我们就可以像任何普通的 C++ 数组一样遍历它：\n\n```cpp\nfor (int i = 0; i < array.size(); i++)\n{\n    qDebug() << array.at(i).toString(); // Get data\n}\n```\n\n前面的代码会产生以下结果：\n\n```cpp\nFord\nBMW\nFiat\n```\n\n同样，代码非常简短和直观。 Qt 使通过以下`QJsonDocument`、`QJsonObject`、`QJsonValue`和`QJsonArray`类解析 JSON 数据变得非常容易。\n\nFor more information about JSON support in Qt, please refer to the link here: [https://doc.qt.io/qt-5/json.html](https://doc.qt.io/qt-5/json.html).\n\n在本章中，我们学习了如何使用 Qt 的核心功能。 我们还学习了如何使用 Qt 类解析 XML 数据和 JSON 数据。 在下一章中，我们将学习如何使用 Qt 小工具创建表单应用。\n\n# 简略的 / 概括的 / 简易判罪的 / 简易的\n\n我们在本章涵盖了很多内容，从数据结构到文件，再到网络。 您已经了解了如何使用基本的 Qt 核心和网络类来构建后端逻辑，这可以帮助您构建应用的业务逻辑。\n\n不仅如此，我们还学习了如何利用多线程将工作负载分散到不同的 CPU 线程，以加快进程。 如果您正在创建一个执行大量计算的应用，并且您不想让它在计算仍在进行时变得没有响应，这一点尤其有用。 这将严重影响用户体验，并最终影响您作为品牌或公司的声誉。\n\n除此之外，我们还了解了如何利用 HTTP 请求与远程服务器通信并从中获取数据。 Qt 提供了自己的类，这些类在后台得到了很好的实现，从而使这个过程变得更容易。 这对那些希望构建支持云架构并向其用户提供动态内容的现代应用的开发人员非常有利。\n\n我们还学习了如何解析不同类型的数据格式，如 XML 和 JSON，这两种格式在 Web 和桌面应用中都非常流行和常用。 通过将这些功能整合到您的应用中，您将能够使其与市场上的任何第三方系统兼容，从而提高其价值。\n\n在下一章中，我们将开始研究 Qt 支持来构建您的演示逻辑。 我们将暂时离开这些基础知识，回顾构建桌面应用的关键 Qt Widget 类。 您将了解可用于您的应用的大量基本 Qt Widget 类、Qt 对模型-视图-控制器范例的支持如何工作，以及如何使用 QWebEngineView-Qt 集成的基于 Web 引擎的浏览器在应用开发中呈现 Web 内容。"
  },
  {
    "path": "docs/app-dev-qt-creator/05.md",
    "content": "# 五、使用 Qt 小部件开发应用\n\nQt 在跨平台 GUI 开发方面有着悠久的历史。 由于 GUI 设计的各个方面的控件都非常类似于本机平台的控件(或者，在许多情况下，包装本机平台的控件)，因此对于任何跨平台开发项目来说，它都是一个多功能的选择。 对于许多人来说，开始使用 Qt 小部件的最佳方式是在 Qt Creator 的设计器窗格中闲逛，就像我们在[第 3 章](03.html)，*使用 Qt Designer 设计应用*中所做的那样。 如果你是那种喜欢在打开新玩具包装前阅读文档的人，这一章是为你准备的。\n\n在本章中，您将快速了解如何使用 Qt 小部件进行 GUI 编程。 这不是一个详尽的介绍，但将指导您了解 Qt Designer 和 Qt 文档，帮助您在开始构建应用时对可以做什么有一个更高层次的理解。 您将学习基本的应用管理、如何创建对话框和错误弹出窗口，并了解可以使用设计器创建的一些主要 GUI 元素。\n\n接下来，您将学习如何使用 Qt 的灵活布局系统来管理这些 GUI 元素的布局，如果您要在应用中使用多个屏幕尺寸，这是应用开发的重要部分。 之后，您将了解**Model-View-Controller**(**MVC**)范例，以及它如何在 Qt 中用于列表和树视图等复杂控件。 本章最后将快速浏览一下 Qt 对 WebEngine 的支持，它允许您使用`QWebEngineView`控件将丰富的 Web 内容集成到您的应用 UI 中。\n\n我们将介绍以下主题：\n\n*   您的主应用及其菜单\n*   创建简单的 Qt 小部件\n*   使用布局管理小部件布局\n*   用 Qt 进行 MVC 编程\n\n*   使用`QWebEngineView`呈现 Web 内容\n*   使用模型编辑器\n*   在 Qt Creator 上启用 LSP\n\n# 技术要求\n\n本章的技术要求包括 Qt 5.12.3、MinGW 64 位、Qt Creator 4.9.0 和 Windows 10。\n\n# 您的主应用及其菜单\n\n为了使用 Qt 小部件，您需要做两件事。 首先，您需要通过在项目的`.pro`文件中添加以下行来确保在项目中包含小部件模块：\n\n```cpp\nQT += widgets \n```\n\n其次，任何使用 Qt 小部件的文件都应该包括`QWidgets`头作为其头之一。 您可能还需要包括各个小部件的头文件，如`QButton`和`QMenuBar`：\n\n```cpp\n#include <QWidgets> \n```\n\nQt 提供`QGuiApplication`类(`QCoreApplication`的子类)来管理应用的生命周期，包括当今 GUI 平台所需的事件循环。 您已经看到了`QCoreApplication`，我们在[章](01.html)，*《Qt Creator 入门》*中将其用于我们的控制台应用。\n\n您可能不会对`QGuiApplication`做太多事情，但它提供了两个信号，有助于您了解：\n\n*   `QGuiApplication::applicationStateChanged()`：当应用状态更改时，它会发出`applicationStateChanged`信号，通知您应用是挂起、隐藏、非活动还是活动。 在移动平台上查看此信号是个好主意，在移动平台上，当您的应用处于隐藏或非活动状态时，您应该做最少的处理。\n*   `QGuiApplication::lastWindowClosed()`：当应用的主窗口或父窗口关闭并即将退出时，它会发出`lastWindowClosed`信号。\n\n除了这些信号之外，`QGuiApplication`还有一些静态方法可以很方便地确定应用的显示名称、区域设置模式是从左到右还是从右到左显示文本，以及平台名称。 有关完整列表，请参阅[https://doc.qt.io/qt-5/qguiapplication.html](https://doc.qt.io/qt-5/qguiapplication.html)上的界面文档或`QGuiApplication`的 Qt 创建者帮助文档。\n\n`QGuiApplication`的主窗口包括一个菜单栏，您可以在其中添加菜单项。 在 Microsoft Windows 上，菜单栏是窗口的一部分；在 MacOS X 上，它是屏幕顶部的菜单栏；而基于 X-Windows 的应用(如 Linux)将它们放在窗口管理器指定的位置。 Qt 提供了`QMenuBar`类来实现水平菜单栏的功能；该类有零个或多个与其关联的`QMenu`实例，每个实例都对应于一个菜单(如文件和编辑)。 菜单项本身表示为动作，在 Qt 中实现为`QAction`类的实例。 如果我们自下而上地工作，从操作到菜单再到菜单栏，理解流程是最容易的。\n\n`QAction`类是一个抽象的用户界面操作，可以嵌入到菜单等 Qt 小部件中。 操作可以具有以下属性：\n\n*   `enabled`：这是一个布尔标志，指示操作是否已启用(可选)。\n*   `font`：这用于显示与操作关联的任何文本。\n*   `icon`：这表示一个动作。\n*   `iconVisibleInMenu`：这是用于检查图标在菜单中是否可见的标志。\n*   `shortcut`：这是与操作关联的键盘快捷键。\n*   `text`：这是操作的文本描述。\n*   `toolTip`：这是操作的工具提示。\n*   `visible`：显示动作是否可见。\n\n操作有一个触发信号，该信号在触发操作时触发，例如当用户选择相应的菜单项时。 (它们的属性也有更改信号，因此您可以监视它们何时更改，但这不太常见。)。 如果需要让操作触发其事件，就像它被调用一样，请调用它的`activate`方法。 有关更多信息，请查看[https://doc.qt.io/qt-5/qaction.html](https://doc.qt.io/qt-5/qaction.html)上的文档。\n\n`QMenu`的实例将一个或多个逻辑上相关的操作分组；您可以将其用作下拉菜单、菜单栏的一部分或上下文菜单。 `QMenu`提供了以下方法，可用于构建菜单层次：\n\n*   `addAction`和`removeAction`：这两个方法分别添加和删除单个`QAction`实例。\n*   `clear`：此方法移除所有操作。\n*   `addSeparator`：此方法在两个操作之间添加菜单分隔符。\n*   `addMenu`：此方法向菜单添加子菜单。\n\n最后，`QMenuBar`对所有下拉菜单进行分组；它使用`addMenu`和`insertMenu`方法在其指定的菜单参数之前和之后添加和插入菜单。 您可以在[https://doc.qt.io/qt-5/qmenu.html](https://doc.qt.io/qt-5/qmenu.html)和`QMenuBar`在[https://doc.qt.io/qt-5/qmenubar.html](https://doc.qt.io/qt-5/qmenubar.html)查看关于`QMenu`的文档。\n\nOnce you add or insert a menu to a menu bar, you can't remove the menu itself. This is consistent with how most GUIs behave; they don't let you remove menus entirely from the main menu bar.\n\n实际上，所有这些都比听起来简单得多，因为您可以使用 Qt Creator Designer 来构建应用菜单。\n\n通过打开应用主视图(基于主窗口模板`MainWindow`)的用户界面表单，您可以单击 Type here 文本并创建一个新的`QMenu`实例。 单击新的菜单栏实例会导致菜单栏下拉，第一个菜单项被标记为 Type here。 要尝试此操作，请返回到[第 1 章](01.html)，*Qt Creator*入门(或上一章中的计算器示例)中的 Qt 小部件示例应用，并尝试单击 Qt Designer 中的菜单栏。 同样，在带有标签的菜单项上键入将创建一个菜单操作；然后您可以在属性编辑器中命名该操作。\n\n下面的屏幕截图显示了 Qt Designer 的此行为：\n\n![](img/5c35aa9f-9c25-49c8-b4bc-fccdfbc10df6.png)\n\n一旦你命名了一个动作，你只需要把它触发的信号连接到你的应用中的一个槽上。 通常，我们在主窗口的构造函数中执行此操作，如下所示：\n\n```cpp\nMainWindow::MainWindow(QWidget *parent) : \n    QMainWindow(parent), \n    ui(new Ui::MainWindow) \n{ \n    ui->setupUi(this); \n    connect(ui->actionAbout, &QAction::triggered, this, &MainWindow:\n      :handleAbout); \n} \n```\n\nFor more information about using menus, refer to Qt's menu sample at [https://doc.qt.io/qt-5/qtwidgets-mainwindows-menus-example.html](https://doc.qt.io/qt-5/qtwidgets-mainwindows-menus-example.html).\n\n我们已经学习了如何创建应用菜单，并使用信号和插槽机制将其链接到功能。 现在让我们继续下一节，学习不同的小部件！\n\n# 创建简单的 Qt 小部件\n\n使用 Qt Creator 中的小部件是体验可用小部件的最佳方式，但您可能最常使用的几个类值得一提。 我们已经讨论过菜单；接下来，让我们看看按钮、文本输入和组合框。 如果您对这些小部件中的任何一个都很好奇，可以启动 Qt Designer 并制作一个：\n\n![](img/2a730b25-d7fe-46e7-861e-b0c58c9e4f66.png)\n\n实现按钮、复选框和单选按钮的 Qt 按钮类都继承自`QAbstractButton`类。 您可以在 Qt Creator 的设计器中拖出`QAbstractButton`的任何具体子类，或者以编程方式实例化它们。 通过`QAbstractButton`，所有按钮都具有以下属性：\n\n*   `checkable`：这是一个布尔标志，指示按钮是否具有复选框行为。 默认情况下，此属性的值为 False。\n*   `checked`：表示该按钮当前是否处于选中状态。\n*   `down`：这是一种指示按钮当前是否处于按下状态的布尔值。\n*   `icon`：此属性保存按钮上显示的图标。\n*   `shortcut`：此属性保存与按钮关联的助记符。\n*   `text`：此属性保存按钮上显示的文本。\n\n按钮提供以下信号，您可以将这些信号连接到应用中的插槽，以检测用户输入：\n\n*   点击该按钮时，该按钮会发出`clicked`信号。\n*   当按钮被按下时(即，接收到鼠标或笔下事件)，该按钮发出`pressed`信号。\n*   释放按钮时，该按钮会发出`released`信号(即接收到鼠标或竖笔事件)。\n*   该按钮在状态从选中变为未选中或反之亦然时发出`toggled`信号。\n\n您可以对父容器中的多个按钮进行分组，如`QFrame`，以控制独占行为；这就是单选按钮的工作方式。 容器的一个很好的选择是`QGroupBox`小部件，它构成其内容的框架并为集合提供标题。 通过在单个`QFrame`容器中放置多个按钮(如`QRadioButtons`)并确保它们的`autoExclusive`属性为`true`(`QRadioButtons`的默认值)，单击一个单选按钮可选中该按钮，同时取消选中所有其他按钮。\n\n让我们看看如何使用 C++ 代码创建按钮：\n\n```cpp\nQPushButton* button = new QPushButton(\"Some default text\", ui->\n  centralWidget);\nbutton->setText(\"Click me!\");\n```\n\n前面的代码是创建按钮的最小代码。 您也可以通过简单地将按钮从 Qt Designer 拖到画布上来实现这一点。 结果如下所示：\n\n![](img/5e0ce104-36a2-4b70-9402-525dc5088640.png)\n\nDon't forget that you should use radio buttons for options that have exclusive behavior (that is, only one item can be selected) and checkboxes for items where multiple items can be selected.\n\n我现在已经提到图标两次了，一次是在讨论菜单，另一次是在讨论按钮，但没有真正描述它们在 Qt 中是如何工作的。 当然，您可以直接操作位图(我们将在[章 6](06.html)，*使用 Qt*绘制中进一步讨论这一点)，但对于大多数用户界面元素，您需要一个容器来表示处于各种模式和状态(如按下、释放和高亮显示)的图标。 在 Qt 中，容器是`QIcon`。\n\n使用`QIcon`非常简单；您可以简单地从一个像素图或资源实例化一个。 例如，要将按钮图标设置为应用资源中的特定图像，只需编写以下代码：\n\n```cpp\nbutton->setIcon(QIcon(\":/icon.png\")); \n```\n\n在幕后，`QIcon`类为您创建了三个用于不同州的附加图标。 这是因为图标实际上可以处于四种模式之一：正常、活动、禁用或选中。 您可以将多个图像应用于`QIcon`，不同的模式将显示不同的图像。 为了更好地理解这一点，我准备了一张图表，以供比较之用，如下所示：\n\n![](img/50f73cec-9d7f-4e3d-87c5-b474e05d75c4.png)\n\n每种模式可以处于两种状态：打开或关闭。\n\n此外，`QIcon`将缩放图标以适应与图标相关联的用户界面元素，因此您可以以用户界面所需的最大分辨率创建图标，并依靠`QIcon`进行缩放以适应界面的各种元素。\n\n让我们继续来看短信。 到目前为止，Qt 小部件中最常见、最易于使用的文本容器是`QLabel`类，它只是一个标签。 `QLabel`实际上可以显示文本或图像；您可以使用它的`setText`方法设置它的文本，使用它的`setPixmap`或`setPicture`方法设置它的图像。 文本可以是纯文本，也可以是富文本；富文本是 HTML4 标记的子集，因此您可以做一些简单的事情，比如设置粗体文本、带下划线的文本，甚至是超链接。 如果您使用带有`QLabel`类的超链接，您应该准备好通过连接到标签的`linkActivated`信号来捕捉用户点击，标签在用户单击链接时会发出该信号，并将该链接的 URL 作为信号的参数发送。\n\n默认情况下，标签显示左对齐、垂直居中的内容；您可以通过调用标签的`setAlignment`方法或通过设置 Qt Creator Designer 中的 Align 属性来更改此设置。 还可以通过调用`setWordWrap`并传递`true`启用换行，或传递`false`禁用换行来设置是否发生换行，从而控制标签的换行。\n\n让我们看看在 C++ 中创建`QLabel`的代码是什么样子：\n\n```cpp\nQLabel* label = new QLabel(\"This is some text\", ui->centralWidget);\n```\n\n结果如下所示：\n\n![](img/53eead50-4c5d-46ad-b627-61699d195c84.png)\n\n其次是文本输入；Qt 为单行文本输入提供了`QLineEdit`元素，为多行输入提供了`QTextEdit`元素。 无论主机平台是什么，都提供对编辑功能的支持，因此您通常可以使用`QLineEdit`和`QTextEdit`免费获得撤消和重做、复制和粘贴以及拖放功能。\n\n让我们先来谈谈`QLineEdit`，因为它稍微简单一些。 这是一个允许用户编辑单行文本的小部件。 `QLineEdit`具有以下属性，您可以使用 Qt Creator Designer 或在源代码中设置这些属性：\n\n*   `alignment`：此选项控制文本显示时的对齐方式。\n*   `cursorPosition`：表示当前光标位置。\n*   `displayText`：这将显示显示给用户的文本(根据`echoMode`属性的不同，文本可能会有所不同)。\n*   `echoMode`：这可用于控制密码空白或常规输入行行为。\n*   `hasSelectedText`：如果文本字段选择了文本，则此属性为`true`。\n*   `inputMask`：它控制输入验证(稍后我将详细介绍)。\n*   `maxLength`：指定输入行的最大长度(以字符为单位)。\n*   `placeholderText`：当文本字段为空时，该文本显示为灰色文本。\n*   `readOnly`：当为`true`时，该标志表示文本字段不可编辑。\n*   `selectedText`：它包含文本字段中当前选定的文本。\n*   `text`：此字段包含输入行的整个文本。\n\n`QLineEdit`具有以下信号：\n\n*   `cursorPositionChanged`：这由行编辑器在光标移动时发出。\n*   `editingFinished`：当用户编辑完文本字段并将焦点移到下一个可聚焦元素时，行编辑器会发出此消息。\n*   `returnPressed`：当用户按下键盘上的*Return*或*Enter*键时，行编辑器会发出此信息。\n*   `selectionChanged`：当所选文本更改时，行编辑器会发出此消息。\n*   `textChanged`：当字段中的文本发生更改时，行编辑器会发出此消息，传递字段的新文本。\n*   `textEdited`：当用户更改字段中的文本时(而不是当文本以编程方式更改时)，传递字段的新文本时，行编辑器会发出此消息。\n\n让我们来看看创建`QLineEdit`所需的最小 C++ 代码：\n\n```cpp\nQLineEdit* lineEdit = new QLineEdit(ui->centralWidget);\n```\n\n前面的代码产生以下结果：\n\n![](img/6836560a-f2c0-4df6-83de-cd680fb2792f.png)\n\n`QLineEdit`实例可以执行输入验证；您可以通过两种方式执行此操作：设置输入掩码或提供验证器。 输入掩码简单且适用于基本任务，例如验证 IP 地址或数字输入。 输入掩码是一个字符串，它指示字符串中每个位置允许的字符类别。 `QLineEdit`为输入掩码定义以下字符类：\n\n| 输入掩码中的字符 | 字符类 |\n| `A` | 需要 ASCII 字母字符：A-Z、a-z。 |\n| `a` | ASCII 字母字符是允许的，但不是必需的。 |\n| `N` | 需要 ASCII 字母数字字符：A-Z、a-z、0-9。 |\n| `n` | 允许使用 ASCII 字母数字字符，但不是必需的。 |\n| `X` | 任何必需的字符。 |\n| `x` | 任何字符都是允许的，但不是必需的。 |\n| `9` | 需要 ASCII 数字：0-9。 |\n| `0` | ASCII 数字是允许的，但不是必需的。 |\n| `D` | 需要 ASCII 数字：1-9。 |\n| `d` | ASCII 数字是允许的，但不是必需的(1-9)。 |\n| `#` | 允许使用 ASCII 数字或加号/减号，但不是必需的。 |\n| `H` | 需要十六进制字符：a-F、a-f、0-9。 |\n| `h` | 允许使用十六进制字符，但不是必需的。 |\n| `B` | 需要二进制字符：0-1。 |\n| `b` | 二进制字符是允许的，但不是必需的。 |\n| `>` | 此字符后面的所有字母字符均为大写字母。 |\n| `<` | 此字符后面的所有字母字符均为小写。 |\n| `!` | 关闭大小写转换。 |\n| `\\` | 使用`\\`转义前面列出的特殊字符，以将其用作分隔符。 |\n\n例如，您可以使用字符串`000.000.000.000;`设置 IP 地址的输入掩码。 这将输入限制为三组四位数字，并用句点括起来。\n\n对于更复杂的验证任务，您可以指定验证器，它是`QIntValidator`、`QDoubleValidator`或`QRegExpValidator`等类的实例，用户每次更改文本时都会调用`QLineEdit`来验证输入。 其中，最灵活的是`QRegExpValidator`，它接受正则表达式并根据正则表达式验证输入。\n\n对于较大的文本块，您需要使用`QTextEdit`实例。 不出所料，`QTextEdit`与`QLineEdit`有很多相同的接口。 不同之处包括：\n\n*   您不能像使用`QLineEdit`那样屏蔽密码输入。\n*   如果`QTextEdit`的`acceptRichText`标志为真，则该字段可以接受表示为 HTML4 子集的富文本。\n*   `QTextEdit`的富文本可作为 HTML 属性使用，而解析后的文本可作为文本属性使用。\n\n最后是`QComboBox`，它将输入行与下拉菜单相结合，提示用户选择预存文本。 正如您可能想象的那样，它的界面类似于菜单和输入行；您可以使用`addItem`和`insertItem`向组合框追加和插入文本项。 您需要连接到它的`highlighted`和`editTextChanged`信号，当用户选择菜单项或更改文本输入行时，它会发出这些信号。\n\n让我们来写一些代码吧！\n\n```cpp\nQComboBox* cbox = new QComboBox(ui->centralWidget);\ncbox->addItem(\"Option 1\");\ncbox->addItem(\"Option 2\");\ncbox->addItem(\"Option 3\");\n```\n\n前面的代码产生以下结果：\n\n![](img/55b908b7-f696-442e-9ccb-cdf07f998c99.png)\n\n到目前为止，我们已经学习了如何在 Qt 中使用 C++ 代码创建按钮、文本输入字段和组合框。 您可以使用类似的方法创建其他类型的小部件，并将它们放入您的应用中。 但是，如果没有布局，小部件将无法正确放置。 让我们继续下一节，学习如何使用布局管理小部件定位。\n\n# 使用布局管理小部件布局\n\nQt 小部件包括一个健壮的布局系统来控制小部件在显示器上的呈现。 布局基本上类似于小部件；它们可以放在应用上、命名、成为其他小部件的父级等等。\n\n但是，与小部件不同的是，它们的唯一目的是管理小部件及其在应用中的位置。 下面的屏幕截图说明了布局的目的。 请注意，我们在这里仅显示了一种布局类型(垂直布局)，稍后我们将讨论许多其他类型的布局：\n\n![](img/71ab40bb-0469-4077-b016-331a157c443b.png)\n\n在 Qt Creator Designer 中，您可以从以下布局中进行选择：\n\n*   `QBoxLayout`：这将水平或垂直地布局其视图子对象。\n*   `QHBoxLayout`：这将水平布局其视图子对象。\n*   `QVBoxLayout`：这将垂直布局其视图子对象。\n*   `QFormLayout`：这将并排布局成对的小部件(如标签和文本框)，然后垂直平铺这些成对的小部件，从而提供表单的外观。\n*   `QGridLayout`：这将在网格中布局小部件。\n*   `QStackedLayout`：这一次只显示一个小部件。\n\n使用这些布局中的一种很简单：只需在 Qt Creator Designer 中选择适当的布局并将其拖动到您正在构建的小部件或窗口即可。 如果要在代码中构建小部件的层次结构，请将小部件添加到布局并设置父小部件的布局，如下所示：\n\n```cpp\nQWidget *window = new QWidget(); \nQPushButton *button1 = new QPushButton(\"One\"); \nQPushButton *button2 = new QPushButton(\"Two\"); \nQPushButton *button3 = new QPushButton(\"Three\"); \n\nQHBoxLayout *layout = new QHBoxLayout; \nlayout->addWidget(button1); \nlayout->addWidget(button2); \nlayout->addWidget(button3); \n\nwindow->setLayout(layout); \nwindow->show(); \n```\n\n前面的代码生成如下所示的结果：\n\n![](img/7e536418-9581-4354-b461-019631cc8f63.png)\n\n布局与小部件的`sizePolicy`和`sizeHint`属性协同工作。 这些属性向布局和布局管理器提供有关小部件应该如何布局的信息。 `sizePolicy`属性是`QSizePolicy`类的实例，它控制布局首选项以在水平和垂直方向上的布局之间进行选择，在每个方向上提供以下选项：\n\n*   `QSizePolicy::Fixed`：这里，小部件的`sizeHint`属性是小部件应该达到的唯一大小。\n*   `QSizePolicy::Minimum`：在这里，小部件的大小是它所能达到的最小大小，它越大没有任何好处。\n*   `QSizePolicy::Maximum`：这里，小部件的大小是它可以达到的最大大小；它可以更小，但不能更大。\n*   `QSizePolicy::Preferred`：在这里，如果可以，将考虑小部件的`sizeHint`属性。\n*   `QSizePolicy::Expanding`：这用于指示`sizeHint`属性是建议的，但如果它可用，则可以使用更多空间。\n*   `QSizePolicy::MinimumExpanding`：这用于指示`sizeHint`是最小且足够的，但如果可用，则可以使用更多空间。\n\nQt 小部件中的小部件具有适用于目标平台的常规 UI 约束的大小策略，您通常不需要使用`QSizePolicy::setVerticalPolicy`或`QSizePolicy::setHorizontalPolicy`更改策略。\n\nUse the layout classes and their defaults as much as you can in your application to ensure cross-platform compatibility and proper layout on screens of different sizes. If you're worried about individual pixel placement for your widgets, you're likely doing something wrong and will end up with a user interface that doesn't look like what you expect on at least some systems some of the time.\n\n有关使用 Layout 类管理小部件布局的更多信息，请参阅[https://doc.qt.io/qt-5/layout.html](https://doc.qt.io/qt-5/layout.html)上的 Qt 文档。\n\n我们了解了如何在 Qt 应用中使用不同类型的布局来管理小部件。 让我们在下一节继续学习模型-视图-控制器编程。\n\n# 基于 Qt 的模型-视图-控制器编程\n\n编写软件是管理抽象的练习。 你能对你的软件系统进行越多的抽象推理，你的境况就越好。 自 20 世纪 70 年代以来在 GUI 世界中出现的一个关键抽象是**模型-视图-控制器**(**MVC**)范例。 我将在这里简要讨论 MVC，但是网上有很多关于 MVC 的文章，所以如果你对它是新的，你绝对应该去你最喜欢的搜索引擎去查找它。\n\n在 MVC 中，您将与用户界面相关的代码划分为三个逻辑组件：\n\n*   **模型**：它负责存储要显示给用户的数据。 它是某种类型的容器，不了解您的用户界面、应该如何绘制内容，或者用户在与您的应用交互时应该触发哪些事件或方法。\n*   **View**：这负责在显示器上绘制模型的内容。\n*   **控制器**：它负责操作模型和视图的内容，以响应各个用户操作。\n\n这些独立的逻辑组件中的每一个只通过定义良好的接口与下一个组件通信，如下所示：\n\n![](img/606c6b30-725b-4369-9e26-c002ea7c56c4.png)\n\n在 Qt 中，视图和控制器被简单地组合到视图中，这种排列称为**模型/视图**模式。 为了使用户界面开发尽可能通用，Qt 还引入了一个委托，它可以在共享相同视图和模型的同时轻松地进出不同的用户事件处理程序。 MVC 和模型/视图模式的共同点是数据和视图是分开的，这允许您对不同的视图使用相同的数据模型，或者将不同的模型用于相同的视图。 正如您所预期的那样，模型及其视图通过信号和插槽进行通信。\n\nQt 使用模型/视图模式来管理其更复杂的用户界面元素，如列表视图、表视图和树视图。 使用 Qt 模型类的视图类如下：\n\n*   `QListView`：这显示了项目的顺序列表。\n*   `QTreeView`：这显示了层次结构中项目的树状视图。\n*   `QTableView`：这显示了项目的表视图。\n\n所有这些视图都接受 Qt 的一个模型类来存储 Qt 呈现给用户的数据；Qt 提供的这些模型类都继承自以下抽象基类之一：\n\n*   `QAbstractItemModel`：这足够灵活，可以处理以表、列表和树的形式表示数据的视图。\n*   `QAbstractListModel`：这是一个更加专门化的模型超类，经过优化可以在列表视图中显示数据。\n*   `QAbstractTableModel`：这是一个更加专门化的模型超类，针对在表视图中显示数据进行了优化。\n\n大多数情况下，您不需要为您的应用创建自己的模型。 Qt 提供了几个对许多应用都足够好的具体模型实现：\n\n*   `QStringListModel`：这可用于存储字符串的顺序列表。\n*   `QStandardItemModel`：这可用于在任意树层次结构中存储项目。\n*   `QFileSystemModel`：这可以用作文件系统上的数据模型。\n*   `QSqlQueryModel`、`QSqlTableModel`和`QSqlRelationalTableModel`：它们可以在 SQL 数据库上使用。\n\nIf these classes don't meet your needs, you can implement a subclass of one of the abstract model classes and hook that to your view. Typically, you'd choose to do this for one of two reasons: either the existing implementation isn't performant enough for your needs (typically not a problem unless you're managing thousands of items in the model), or you're trying to put a model in front of a new data source other than memory, the filesystem, or SQL.\n\nFor example, if you were building a database browser over a MongoDB database, you might want to create a model that queries and updates the MongoDB database directly. If this is an option you need to pursue, be sure to see the Qt documentation on the topic at [https://doc.qt.io/qt-5/model-view-programming.html#creating-new-models](https://doc.qt.io/qt-5/model-view-programming.html#creating-new-models).\n\n# 分析一个具体的模型子类\n\n让我们花点时间看一下具体的模型子类，看看如何将数据移入移出它。 到目前为止，您将使用的最常见的模型是`QStandardItemModel`，它将其项存储为一个由`QStandardItem`实例组成的二维数组。 每个`QStandardItem`实例可以存储以下内容：\n\n*   与模型中的项关联的数据。 这通常是一个字符串，但也可以是数字或布尔值。 您可以使用`data`方法访问它，并使用`setData`方法设置它。\n*   用于在视图中呈现项的字体。 您可以使用`font`方法访问它，并使用`setFont`方法设置它。\n*   与项目关联的可选图标。 您可以使用`icon`方法访问它，并使用`setIcon`方法设置它。\n\n*   是否可以用复选标记标记该项目，如果可以，则该项目是否处于选中状态。 您可以使用`checkable`和`setCheckable`方法来指示是否可以标记该项，使用`checked`和`setChecked`来实际设置该项的检查状态。\n*   是否可以将项目拖放到其中。 您可以使用`dragEnabled`、`dropEnabled`、`setDragEnabled`和`setDropEnabled`方法来获取和设置这些选项。\n*   项目是否可编辑。 您可以使用`editable`和`setEditable`方法获取和设置项目的可编辑状态。\n*   该项是否可选。 您可以使用`selectable`和`setSelectable`方法来获取和设置项目的可选状态。\n*   该项的工具提示。 您可以使用`toolTip`和`setToolTip`方法来获取和设置项目的工具提示。\n\n每个`QStandardItem`方法还可以有自己的行和列，使其能够存储树结构。 您可以通过调用`appendRow`、`appendColumn`、`removeRow`或`removeColumn`来操作行和列。 例如，要创建表示树的简单模型，可以编写以下代码：\n\n```cpp\nQStandardItemModel model; \nQStandardItem *parentItem = model.invisibleRootItem(); \nfor (int i = 0; i < 4; ++ i)\n{ \n    QStandardItem *item = new QStandardItem(QString(\"node %0\").arg(i)); \n    parentItem->appendRow(item); \n    parentItem = item; \n} \n```\n\n这将创建一个具有根元素的模型，该模型具有四个子元素，每个子元素都在各自的行中。 有了这个模型，您就可以创建一个树视图来显示该模型，如下所示：\n\n```cpp\nQTreeView *treeView = new QTreeView(this); \ntreeView->setModel(myStandardItemModel); \n```\n\n当然，您需要知道什么时候单击某个项目；当用户单击某个项目时，`QTreeView`方法会发出`clicked`信号，因此您可以将此信号连接到类中的槽：\n\n```cpp\nconnect(treeView, &QTreeView::clicked, this, &MainWindow::clicked); \n```\n\nQt 模型/视图视图使用`QModelIndex`类来指示模型中项目的索引；`QStandardItem`从不在模型和视图之间传递。 `QModelIndex`提供`itemFromIndex`方法在模型中指定的位置返回`QStandardItem`。 请注意，您不应该在应用中缓存`QModelIndex`实例任何时间长度，因为如果模型在您下面更改，则给定数据的索引也会更改。 同样，如果您需要某项在其模型中的索引，请调用该项的`index`方法，该方法返回模型中该项的`QModelIndex`。\n\n# 在 Qt Creator 上使用 MVC 模型\n\n虽然前面的示例显示了如何在 C++ 中将 MVC 模型完全应用于树视图，但让我们看看如何在 Qt Designer 中使用小部件实现相同的功能。 Qt Creator 中有几个小部件可以与 MVC 设计模式一起使用；它们被称为基于模型的项目视图：\n\n*   列表视图\n*   树状视图\n*   表视图\n*   列视图\n*   撤消视图\n\n除了呈现给用户的方式不同之外，它们都非常相似。 这些小部件是控制器和视图的实现位置。 控制器(有时称为**Delegate**)是应用逻辑的地方，例如表示层(视图)和数据层(模型)之间的交互规则。 例如，我们可以通过委托使表可编辑，并在用户完成编辑时更新模型中的数据。 视图将向用户提供 UI，委托将处理视图呈现和编辑背后的逻辑。 最后，模型是存储数据的实际容器。 可以将同一模型应用于不同的代理和视图，因为它们是分开的。 这就是使用 MVC 设计模式的优势，它为您提供了很大的灵活性。\n\n在 Qt Designer 中，您可以在 Widget 框中找到内置的 Widget。 在本例中，我们不使用基于项目的小部件类别中的基于项目的小部件，因为这些小部件具有内置的模型，并且不能修改：\n\n![](img/800624a5-8047-4a2f-9769-d95031c33b70.png)\n\n对于 GUI 设计，它非常简单。 首先，我们删除菜单栏、工具栏和状态栏，因为我们不需要这些额外的面板。 然后，我们将一个表视图放到中心小部件上，如下所示：\n\n![](img/0ac96944-7000-4086-ae1e-e7a4f925b7c8.png)\n\n然后，选择中心小部件并单击位于画布上方的 Layout。 表视图现在将填满整个主窗口：\n\n![](img/4088ba11-1a47-4e30-8d1b-ed9f0cfd176f.png)\n\n由于`QTableView`已经处理了视图和委托，我们只需要自己处理模型。 Qt 支持的机型有多种类型，如下所示：\n\n*   `QStandardItemModel`\n*   `QStringListModel`\n*   `QSqlTableModel`\n*   `QDirModel`\n\n如果您愿意，甚至可以通过继承其中一个抽象模型类来创建自己的模型：\n\n*   `QAbstractProxyModel`\n*   `QAbstractListModel`\n*   `QAbstractTableModel`\n\n其中一些内置模型是为单列数据结构设计的，而另一些模型则支持多列甚至递归树状结构。 对于我们的例子，我们需要一个支持多列的模型来适应我们的表视图。\n\n因此，在我们的`MainWindow`类的构造函数中，我们创建了一个`QStandardItemModel`对象，它为为我们的表创建多列和多行提供了很大的灵活性。 出于测试目的，我们将列计数设置为`5`，将行计数设置为`10`：\n\n```cpp\nMainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow)\n{\n    ui->setupUi(this);\n\n    QStandardItemModel* model = new QStandardItemModel(this);\n    model->setColumnCount(5);\n    model->setRowCount(10);\n```\n\n之后，我们使用两个`for`个循环在标准模型中插入一些虚拟数据。 我们循环遍历每一列和每一行，然后创建一个带有单词`\"row #, column #\"`(`#`表示行号和列号)的`QStandardItem`对象，并将其附加到其各自的位置。 请注意，我在行号和列号中都添加了`1`，这样它们就不会从零开始：\n\n```cpp\n    for (int row = 0; row < model->rowCount(); row++)\n    {\n     for (int column = 0; column < model->columnCount(); column++)\n        {\n            QStandardItem *item = new QStandardItem(QString(\"row %0, \n              column %1\").arg(row + 1).arg(column + 1));\n            model->setItem(row, column, item);\n        }\n    }\n```\n\n最后，我们在表视图上调用`setModel`并将模型应用于它：\n\n```cpp\n    ui->tableView->setModel(model);\n}\n```\n\n此外，不要忘记在顶部包括所需的标头：\n\n```cpp\n#include <QStandardItemModel>\n#include <QMessageBox>\n```\n\n如果您现在构建并运行该程序，您应该看到如下所示：\n\n![](img/32ad997e-fc2b-46fb-ae99-bbb294dac10f.png)\n\n您甚至可以双击任何数据块并动态编辑数据。 这是`QTableView`类中委托的默认行为，可以通过 Qt 设计器(或 C++ 代码，如果愿意)禁用它。 让我们尝试通过选择表视图并将其 editTriggers 属性设置为 NoEditTriggers 来禁用它：\n\n![](img/ee51e515-c435-41cf-a5f1-4831fca2f83e.png)\n\n我们希望将双击行为改为其他行为。 让我们右键单击 Qt Designer 上的表视图，然后在弹出菜单中选择 Go to Slot。 然后，选择 DoubleClicked(QModelIndex)信号并按 OK：\n\n![](img/66f769a6-8939-4ad3-9311-c6b0c2526427.png)\n\n系统将为您创建一个插槽功能。 我们只需弹出一个消息框，并在双击时显示数据块中的数据：\n\n```cpp\nvoid MainWindow::on_tableView_doubleClicked(const QModelIndex &index)\n{\n    QMessageBox::information(this, \"Data\", index.data\n       (Qt::DisplayRole).toString());\n}\n```\n\n在前面的代码中，我们提示一个标题为`\"Data\"`的信息消息框，并将数据作为消息。 我们使用`DisplayRole`指定要获取文本，而不是 DecorationRole 图标或 ToolTipRole 工具提示。 下面的屏幕截图取自官方文档，清楚地显示了不同角色之间的区别：\n\n![](img/1cff5d8d-fa3c-4e99-9b60-2e54024992dd.png)\n\n如果我们再次构建并运行该程序，则在双击表时应该会得到以下结果：\n\n![](img/63f01f85-ef08-4c85-8bf1-3c3fc8abf2e1.png)\n\n从前面的示例中，我们了解了如何实现我们自己的模型，并将其应用于我们使用 Qt Designer 创建的表视图。 同样，这些都是 Qt 的内置 MVC 设计模式，它们总是试图通过从用户中移除一些控件来使事情变得更容易。 例如，Qt 的 MVC 只适用于一种数据结构，并且内置了控制器，具有更简单的视图结构。 如果您想为您的程序提供更合适的**和**MVC 设计模式，您可以随时编写自己的设计模式，老实说，这是一项相当复杂的任务。\n\nFor more information about Qt's application of the model/view pattern, see [https://doc.qt.io/qt-5/model-view-programming.html](https://doc.qt.io/qt-5/model-view-programming.html).\n\n我们已经学习了如何利用模型-视图-控制器编程来动态显示数据。 接下来，我们将学习如何使用 QWebEngine 显示 Web 内容。\n\n# 使用 QWebEngineView 呈现 Web 内容\n\nQt 在其 Qt WebEngine 模块中包含 WebEngine 端口，这是 Chromium 和几个开源浏览器背后的流行浏览器实现。 使用 Qt WebEngine 模块，您的应用可以显示丰富的 HTML，甚至是一个功能齐全的 Web 浏览器。 创建混合应用非常容易，它既包含本地应用的功能，又具有显示来自本地资源、本地文件系统或互联网的 Web 内容的能力。\n\nDo note that the Qt WebEngine module only works on MSVC compilers. You will get an error if you use other compilers.\n\n要使用 Qt WebEngine 模块，您必须通过将以下内容添加到 PRO 文件来将其包含在您的应用中：\n\n```cpp\nQT += webenginewidgets \n```\n\n访问 Qt WebEngine 小部件类的任何源文件还应包含带有以下`#include`语句的接口：\n\n```cpp\n#include <QtWebEngineWidgets>\n```\n\n此模块公开的网页表示的关键类是`QWebEngineView`；使用它就像在 Qt Creator Designer 中将其添加到布局中，然后告诉它打开文档一样简单，如下所示：\n\n```cpp\nQWebEngineView *view = new QWebEngineView(ui->centralWidget); \nview->load(QUrl(\"http://www.zhieng.com\")); \n```\n\n前面的代码产生以下结果：\n\n![](img/4b14b056-6b8a-4580-a1e4-fe3b0da4dfd5.png)\n\n`load`方法启动启动网络层的过程，解析 URL，获取内容，并呈现结果。 您还可以使用`QWebEngineView`的`setUrl`方法来设置它的`url`属性，该属性会触发相同的流，或者如果您在本地拥有 HTML(例如，您通过编程方式构建它或从资源中获取它)，则只需调用它的`setHtml`方法即可。\n\n当`QWebEngineView`正在加载页面时，它会发出三个信号：\n\n*   `loadStarted`：这是在页面开始加载时发出的。\n*   `loadProgress`：当 web 视图的每个元素完成加载时，都会发出此消息。\n*   `loadFinished`：页面加载完成后将发出此消息。\n\n`QWebEngineView`具有以下属性：\n\n*   `hasSelection`：如果用户在`QWebEngineView`中选择了区域，则为`true`。\n*   `icon`：这是`QWebEngineView`的图标。\n*   `selectedText`：这包含用户在`QWebEngineView`中选择的未标记文本。\n*   `title`：这包含文档的标题。\n*   `url`：这包含文档的 URL。\n*   `zoomFactor`：这是一个实数，指示页面在呈现时应该缩放多少。 默认值`1.0`表示不应进行缩放。\n\n`QWebEngineView`包含`QWebEnginePage`的一个实例，可通过`page`方法获得。 `QWebEnginePage`方法本身就是执行实际渲染的方法，它还有几个额外的信号，您可以监视它们来观察渲染引擎本身的行为：\n\n*   `loadStarted`：这是在`QWebEnginePage`开始加载文档时发出的。\n*   `loadFinished`：这是在`QWebEnginePage`完成页面加载时发出的。\n*   `urlChanged`：当`QWebEnginePage`要在加载新网页之前更改其 URL 时发出此消息。 该信号将新的 URL 传递到连接到该信号的插槽。\n\n在创建混合应用时，您可能希望从应用的 JavaScript 访问 C++ 应用中的应用数据。 `QWebChannel`类提供了`registerObject`方法，该方法允许您将`QObject`实例绑定到作为网页`window`对象的子级的槽。 例如，让我们编写以下代码片段：\n\n```cpp\nQWebEnginePage *page= myView->page(); \nQWebChannel *channel = new QWebChannel(page);\npage->setWebChannel(channel);\nchannel->registerObject(QString(\"TheNameOfTheObjectUsed\"), this);\n```\n\n在 JavaScript 中，对象的属性在公开的对象中以槽的形式提供，因此您可以跨 C++/JavaScript 边界共享数据。 桥还允许您通过在 JavaScript 中调用 JavaScript 对象上的信号函数来跨边界扩展脚本调用；这将导致连接到该信号的任何插槽执行。 类似地，您的 JavaScript 对象支持`connect`方法，该方法允许您将命名槽连接到 JavaScript 代码，因此从 C++ 调用信号将调用连接到该槽的 JavaScript 方法。\n\nFor more information about `QWebEngineView` and Qt's support for the WebEngine browser, refer to the Qt documentation at [https://doc.qt.io/qt-5/qtwebengine-overview.html](https://doc.qt.io/qt-5/qtwebengine-overview.html).\n\n我们已经学习了如何使用 QWebEngineView 显示网页内容，并创建了我们自己的迷你网页浏览器。 在下一节中，我们将学习如何在 Qt Creator 中使用模型编辑器。\n\n# 使用模型编辑器\n\n模型编辑器是 Qt 工具集中的新成员。 您可以使用它来创建**通用建模语言**(**UML)**样式的模型，以可视化您的应用的行为。 这是与您的程序员沟通或向您的团队展示您的想法的好方法之一。 目前，模型编辑器仍处于 Beta 测试阶段，并将在以后的版本中进行更改。 目前，您需要在 Qt Creator 中启用它，然后才能使用它。 要执行此操作，请尝试以下步骤：\n\n1.  转至 Help(帮助)|About Plugins(关于插件)，打开 Installed Plugins(已安装插件)窗口。 确保已选中 ModelEditor 选项。\n\n2.  之后，您可以转到文件|新建文件或项目，然后在文件和类|建模下选择文件模型或临时模型选项来创建新模型文件。 另一个选项，State Chart，用于状态机编辑，因此与我们当前的主题无关。 请参阅以下屏幕截图以更好地了解：\n\n![](img/c1cf92e7-1bb9-4ae5-8ca2-a3d9c8c40642.png)\n\n3.  一旦您创建了模型文件，让我们用 Qt Creator 打开它们。 您将看到专门为模型编辑设计的全新用户界面：\n\n![](img/5fdf1e1b-6c14-482d-8069-0ed6cad597b8.png)\n\n用户界面包含五个不同的部分，如下所述：\n\n1.  元素工具栏：您可以将元素从此工具栏拖放到模型编辑器中。\n2.  模型编辑器：这是模型可视化的地方。 您可以在此编辑器视图中编辑模型。\n3.  工具栏按钮：您可以在这里找到一些快捷按钮，如放大、缩小、缩放到原始大小、添加包、添加组件、添加类、添加画布图。\n\n4.  元素树：元素树以树形图的形式显示您添加到模型编辑器中的所有元素。 您可以在这里选择或删除元素，而不是在模型编辑器中进行选择或删除。\n5.  属性编辑器：在此编辑刚刚选择的元素的属性。\n\n就像任何其他 UML 建模软件一样，您可以将不同类型的图放置到编辑器视图中，并编辑其名称、颜色、角色和图元素的其他属性。 您还可以将不同的元素链接在一起，以可视化它们之间的关系。\n\n对结果满意后，您可以转到文件->|>导出图表，将图表另存为图像。 除此方法外，您还可以选择编辑器视图中的所有元素，然后按键盘上的*Ctrl*+*C*将其转换为 300dpi 图像，然后将其存储到剪贴板中。 您可以按*Ctrl*+*V*将图像粘贴到首选的文字处理软件或图像编辑软件上。\n\n我们已经学习了如何使用 Qt Creator 套件提供的模型编辑器。 让我们进入下一节，在您的 Qt Creator 上启用 LSP。\n\n# 在 Qt Creator 上启用 LSP\n\n**LSP**(**Language Server Protocol**的缩写)是最近版本中添加到 Qt 的最新功能之一。 LSP 通过添加对 C++ 和 QML 以外的其他编程语言的支持，使 Qt 变得更加强大。 通过为 LSP 提供客户端，Qt Creator 可以为支持 LSP 的编程语言提供以下功能：\n\n*   代码自动完成\n*   当符号发生鼠标悬停事件时突出显示该符号\n*   代码共享\n*   通过查看文档大纲检查代码\n*   集成来自语言服务器的诊断\n*   查找对符号的引用\n*   导航到符号定义\n\n这扩展了 Qt Creator 的用途，并消除了将 Qt 保持在 C++ 领域内的障碍。 您甚至可以使用 Qt Creator 编写 Python 项目，并在不离开 Qt 的情况下直接执行代码！ 因此，要在 Qt 创建者中启用 LSP，请转到 Help|About Plugins。 然后，在 Installed Plugins 中，查找 LanguageClient(实验性)选项，并确保选中该选项。 选中该选项后，可能需要重新启动 Qt Creator。 下面的屏幕截图显示了这一点：\n\n![](img/460f2a19-cc3d-4560-87e8-3bba866b9aae.png)\n\n在 Qt Creator 中使用替代语言之前，您需要做几件事。\n\n1.  首先，您必须先安装语言服务器，然后 Qt 才能使用它。 例如，如果您计划使用 Python，则必须从[https://www.python.org](https://www.python.org)下载安装程序并将其安装到您的计算机上。\n\n2.  完成后，您可以开始将语言服务器添加到您的 Qt Creator 中，方法是转到工具->->选项...--|语言客户端。\n3.  然后，单击 Add 按钮。 下面的屏幕截图显示了这一点：\n\n![](img/e39a514d-aa83-48b4-b754-91d823278184.png)\n\n在应用该语言之前，请填写该语言的信息：\n\n4.  完成后，单击确定。 您现在可以开始使用默认情况下 Qt 不支持的替代语言编写代码了！ 下面的屏幕截图就是这样一个例子：\n\n![](img/2c980663-b2ef-418d-a2dd-ae23eb8c49bb.png)\n\n前面的屏幕截图显示了使用 Qt Creator 编写 Python 时的代码完成和语法突出显示。\n\nDo note that currently, the LSP is still an experimental feature. It might not work very well on all languages since Python is the only language that has been fully tested. It is not recommended to use it on critical commercial projects.\n\n在本节中，我们了解了如何在 Qt Creator 上启用 LSP。 我希望你能发现这一点很有用，它能提高你的工作质量，提高工作效率。\n\n# 简略的 / 概括的 / 简易判罪的 / 简易的\n\n在本章中，我们对 Qt 窗口小部件模块进行了一次旋风式的浏览。 我们了解了一些可用的基本小部件，以及它们提供的信号、插槽和属性。 我们还了解了 Qt 对 MVC 范例的应用，以及对于像和树视图这样的复杂小部件，Qt 如何将关注点分离为模型和视图，从而允许我们为应用实现新的数据模型，或者基于这些数据模型创建新的视图。 然后，我们了解了 Qt 对 WebEngine 浏览器的支持，这让我们可以构建混合应用，将 JavaScript 和 HTML 的优点与 Qt 的优点结合起来。 最后，我们了解了 Qt 小部件的新功能，以及它们如何帮助应用开发和语言支持。\n\n在下一章中，我们将从窗口小部件转到底层绘图，我们可以使用它来实现我们自己的窗口小部件或基本的基于像素的渲染应用。"
  },
  {
    "path": "docs/app-dev-qt-creator/06.md",
    "content": "# 六、使用 Qt 绘图\n\n虽然许多应用只能使用内置小部件构建，但其他应用需要能够执行自定义绘图-例如，当您需要一两个自定义小部件时，或者您正在进行屏幕外呈现以编程方式在图形文件中创建图像时，或者您对构建完全不同的用户界面感兴趣时。 除了您可以使用 Qt Quick 执行的操作之外，Qt 还提供了对 C++ 中所有这些场景的支持。\n\n在本章中，我们将了解 Qt 中的一般绘图需要了解的内容。 我们首先讨论`QPainter`，以及它如何使用`QPaintDevice`实例来抽象绘图功能。 我们将大体上了解这是如何工作的，然后给出屏幕外绘制位图以及创建与 Qt 小部件互操作的自定义小部件的具体示例。 在本章的后半部分，我们将介绍 Qt 为图形管理提供的一个更新、更低级别的抽象：`QGraphicsView`和`QGraphicsScene`提供的图形视图/图形场景架构，以及如何使用它在 C++ 中构建与 Qt 窗口小部件类互操作的应用，同时包含复杂的可视化层次结构。\n\n在本章中，我们将介绍以下主题：\n\n*   开始在 Qt 中绘图\n*   在`QPaintDevice`个实例上使用`QPainter`绘制\n*   拉出屏幕\n*   创建自定义小部件\n*   图形视图框架简介\n\nThroughout the chapter, don't forget you can always ask for Qt Creator's help when encountering an unfamiliar class or method. Qt Creator also comes with a number of examples you can look at; these are in the `examples` directory, under the directory in which you installed Qt.\n\n# 技术要求\n\n本章的技术要求包括 Qt 5.13.1 MinGW 64 位、Qt Creator 4.10.0 和 Windows 10。\n\n代码文件可在以下链接中找到：[https://github.com/PacktPublishing/Application-Development-with-Qt-Creator-Third-Edition](https://github.com/PacktPublishing/Application-Development-with-Qt-Creator-Third-Edition)\n\n# 开始在 Qt 中绘图\n\n我们在本章中介绍的所有材料都依赖于 Qt GUI 模块，该模块是 Qt 的一部分。 即使您正在编写命令行工具(比如处理图像文件)，也需要通过将以下代码添加到`.pro`文件来将该模块包含在项目中：\n\n```cpp\nQT += gui widgets \n```\n\n当然，在您的 C++ 实现中，我们还需要包括我们正在使用的类的头文件。 例如，如果我们使用`QImage`、`QBitmap`和`QPainter`，请确保在 C++ 文件的顶部包含这些头文件，如下所示：\n\n```cpp\n#include <QImage> \n#include <QPainter> \n#include <QBitmap> \n```\n\n因为 Qt 的绘制实现使用底层窗口系统，所以任何执行图形操作的应用都必须使用`QGuiApplication`构建，它会将窗口系统作为启动的一部分进行初始化。\n\n由于我们已经向项目中添加了所需的模块和标题，并在 Qt 项目中启用了绘图功能，因此让我们开始在下一节中绘制一些内容！\n\n# 在 QPaintDevice 实例上使用 QPainter 绘图\n\n从本质上讲，图形绘画需要两样东西：会画的东西和能画的东西。 Qt 将`QPainter`类定义为前者，将`QPaintDevice`定义为后者的类的接口。 您很少实例化每个类，但是如果您正在进行图形编程，则会经常使用这两个类；通常，您会有一个`QPaintDevice`子类的实例，向它请求其相关的`QPainter`，然后使用`QPainter`执行绘图。 这可能发生在您编写小部件时；例如，当您需要绘制小部件的内容时，会向您传递一个`QPainter`子类。\n\n`QPaintDevice`有几个子类，如下所示：\n\n*   `QWidget`：此类及其子类由小部件层次结构使用。\n*   `QImage`：这是一个用于屏幕外图像的容器类，这些图像针对输入/输出和单个像素访问进行了优化。\n*   `QPixmap`：这是一个用于屏幕外图像的容器类，这些图像针对与屏幕的交互进行了高度优化。\n*   `QBitmap`：这是`QPixmap`的一个子类，位深度为 1，因此适用于单色图像。\n*   `QPicture`：这是一种涂装设备，可记录`QPainter`绘图操作并可回放。\n\n`QPaintDevice`子类有`width`和`height`方法，分别以像素为单位返回绘制设备的宽度和高度；相应的`widthMM`和`heightMM`方法以毫米为单位返回绘制设备的宽度和高度(如果已知)。 您还可以通过调用`depth`方法来获取`QPaintDevice`类的位深度。\n\n我们将在接下来的每一节中进一步讨论如何获取`QPaintDevice`子类，因为我们看到了要在其上绘制的内容(例如，屏幕外的位图或自定义小部件)。 让我们转到`QPainter`，它是我们用来执行绘图的类。\n\n`QPainter`通过使用绘制特定形状(点、线、多边形、椭圆、圆弧等)的方法以及控制`QPainter`如何执行所请求的实际绘制的许多设置来封装绘画的概念。\n\n设置包括以下内容：\n\n*   `brush`：这指示它应该如何填充形状。\n*   `backgroundMode`：这表示背景应该是不透明的还是透明的。\n*   `font`：表示绘制文本时应使用的字体。\n*   `pen`：这表示它应该如何绘制形状的轮廓。\n\n您还可以指定是否启用了视图变换，这使您可以设置`QPainter`实例在执行绘图时将应用的仿射变换。\n\nQt 允许您为`QPainter`实例指定可能在比例、旋转和原点方面与目标`QPaintDevice`类的坐标系不同的任意坐标系。 在执行此操作时，您可以将图形坐标系之间的仿射变换指定为变换矩阵，或通过单独的缩放、旋转和原点偏移参数来指定。 如您所料，默认设置为不转换。\n\nDiscussing transformations is beyond the scope of this section; for more details, see the Qt documentation on this topic at [https://doc.qt.io/qt-5/qpainter.html#coordinate-transformations](https://doc.qt.io/qt-5/qpainter.html#coordinate-transformations).\n\n`QBrush`和`QPen`类都使用`QColor`实例来指定颜色；使用`QColor`，您可以将颜色指定为 RGB、HSV 或 CMYK 值，或者通过颜色名称指定由**可缩放矢量图形**(**SVG**)颜色名称定义的颜色之一(请参阅[http://www.december.com/html/spec/colorsvg.html](http://www.december.com/html/spec/colorsvg.html)上的列表)。 除颜色外，`QBrush`实例还指定样式、渐变和纹理；`QPen`实例指定样式、宽度、画笔、笔帽样式(在笔的端点上使用)和连接样式(在两个笔划连接时使用)。 两者都有简单的构造函数来设置对象的各个字段，或者您可以通过 setter 和 getter 方法指定它们。 例如，我们可以使用以下代码行创建一支`green`笔，其宽度为`3`像素，由一条带有圆形大写和连接的虚线组成：\n\n```cpp\nQPen pen(Qt::green, 3, Qt::DashLine, Qt::RoundCap, Qt::RoundJoin); \n```\n\n同样，要创建用绿色实心填充的`QBrush`，可以编写以下代码：\n\n```cpp\nQBrush brush(Qt::green, Qt::SolidPattern);\n```\n\n`QFont`的操作类似，但当然，与画笔或钢笔相比，字体有更多的选项。 通常，将字体系列与字体大小和粗细一起传递给所需字体的构造函数。 Qt 有一种健壮的字体匹配算法，它试图将所需的字体系列与系统上实际可用的字体进行匹配，因为众所周知，一旦您放弃了常用的*Times New Roman*、*Helvetica*等常用字体，就很难预测哪些字体随处可用。 因此，您获得的字体可能不完全是您请求的字体；您可以通过从您创建的`QFont`创建一个`QFontInfo`方法来获取有关字体的信息，如下所示：\n\n```cpp\nQFont serifFont(\"Times\", 10); \nQFontInfo serifInfo(serifFont); \n```\n\n一旦设置了`QPainter`的画笔、钢笔和字体，绘制只需调用各种`QPainter`方法即可。 我不会一一列举它们，而是将您的注意力集中在这里的几个方面，然后向您展示一个使用其中一些的示例：\n\n*   `drawArc`：这将绘制一条圆弧，从一个角度开始，并跨越一个矩形中的一个角度。 角是以度的十六分之一为单位测量的。\n*   `drawConvexPolygon`：这将获取一个点列表，并绘制一个凸多边形。\n*   `drawEllipse`：这将在矩形中绘制一个椭圆(要绘制圆形，请将矩形变为正方形)。\n*   `drawImage`：这将绘制一幅图像(`QImage`)，其中包含目标矩形、图像和源矩形。\n*   `drawLine`：这将绘制一条线；`drawLines`将绘制一系列线。\n*   `drawPicture`：这将绘制一幅图(`QPicture`)。\n*   `drawPixmap`：这将绘制一个像素图(`QPixmap`)。\n*   `drawPoint`和`drawPoints`：它们绘制一个点或一组点。\n*   `drawPolygon`：这将绘制一个(可能是凹的)多边形，它的点可以是点数组，也可以是`QPolygon`。\n*   `drawPolyline`：这将绘制一条多段线。\n*   `drawRect`：这将绘制单个矩形；`drawRects`将绘制多个矩形。\n*   `drawText`：这将绘制一个文本字符串。\n*   `fillPath`：这将使用您传递的笔刷填充多边形路径。\n*   `fillRect`：这将使用您传递的画笔绘制一个实心矩形。\n\n为了方便这些方法，Qt 定义了助手容器类，包括`QPoint`、`QLine`和`QPolygon`。 它们采用整数坐标；如果在绘制时(比如，在使用转换绘制时)需要更大的位置，则可以使用浮点变量`QPointF`、`QLineF`和`QPolygonF`。\n\n让我们通过画一张脸来看看所有这些在实践中是如何堆叠起来的。 给定一个`QPainter`类，我们可以按如下方式编写它：\n\n```cpp\nvoid MainWindow::paintEvent(QPaintEvent *event)\n{\n    QPainter painter(this);\n    QPen pen(Qt::black, 2, Qt::SolidLine);\n    QBrush whiteBrush(Qt::white, Qt::SolidPattern);\n    QBrush blackBrush(Qt::black, Qt::SolidPattern);\n    QRect faceOutline(0, 0, 100, 100);\n    painter.setPen(pen);\n    painter.setBrush(whiteBrush);\n    painter.drawEllipse(faceOutline);\n```\n\n上述代码首先定义实心黑色钢笔和实心黑白画笔，并将钢笔设置为我们创建的钢笔。\n\n接下来，它创建一个每边 100 像素的正方形，并使用`drawEllipse`在其中绘制一个白色圆圈。 然后，我们画嘴巴，它是圆底部的半椭圆弧。 接下来，我们使用一个矩形绘制两个眼睛，每个眼睛都是一个实心圆。 最后，我们使用由三个点定义的两条线绘制鼻子，如下所示：\n\n```cpp\n    QRect mouth(30, 60, 40, 20);\n    painter.drawArc(mouth, 180 * 16, 180 * 16);  // Draw mouth\n    QRect eye(25, 25, 10, 10);\n    painter.setBrush(blackBrush);\n    painter.drawEllipse(eye);  // Draw left eye\n    eye = QRect(65, 25, 10, 10);\n    painter.drawEllipse(eye);  // Draw right eye\n    QPoint nosePoints[3] = {\n        QPoint(50, 45),\n        QPoint(40, 50),\n        QPoint(50, 50) };\n    painter.drawPolyline(nosePoints, 3);  // Draw nose\n}\n```\n\n您可以在下面的屏幕截图中看到结果：\n\n![](img/17021ef5-7e83-47a7-9e75-d9f1e68199c5.png)\n\n我们已经学会了如何在屏幕上画笑脸！ 现在，让我们看看如何使用`QPainter`来拉出屏幕。\n\n# 拉出屏幕\n\n您可能想要画出屏幕的原因有很多：您可能想要组成一个图像集合并一个接一个地显示它们(这称为**双缓冲**，您可以这样做以避免在屏幕上绘制时屏幕绘制闪烁)，或者编写一个直接生成图像文件的程序。\n\n正如我在上一节中提到的，Qt 提供了几个用于屏幕外绘制的类，每个类都有不同的优点和缺点。 这些类是`QImage`、`QPixmap`、`QBitmap`和`QPicture`。 在正常情况下，您需要在`QImage`和`QPixmap`之间进行选择。\n\n`QImage` is the class best suited for general-purpose drawing, where you're interested in loading the image from or saving the image to a file. If you're working with resources, combining multiple images, and doing a bit of drawing, `QImage` is the class you want to use.\n\n另一方面，如果您主要出于显示性能或双缓冲的目的使用屏幕外渲染，则需要使用`QPixmap`。 `QPixmap`被优化为在底层窗口系统中使用数据结构，并且比`QImage`更快地与本机窗口系统互操作。 `QBitmap`只是定义单色位图的`QPixmap`的一个方便的子类。\n\n`QPicture`是一个有趣的东西，它以与分辨率无关的格式记录绘图操作，您可以将其保存到文件中，稍后再重播。 如果要创建与平台无关的轻量级矢量图像，您可能需要这样做，但通常只使用适当分辨率的**便携网络图形**(**PNG**)格式可能更容易。\n\n要获得其中一个类的画笔，只需创建类的一个实例，然后传递一个指向`QPainter`构造函数实例的指针。 例如，要执行上一节中对屏幕外图像的绘制并将其保存为 PNG 文件，我们将从编写以下代码开始：\n\n```cpp\nQImage image(100, 100, QImage::Format_ARGB32); \nQPainter painter(&image); \n```\n\n第一行创建一个 100 像素正方形的图像，将每个像素编码为 32 位整数，红色、绿色和蓝色的每个不透明通道对应 8 位。 第二行创建一个可以在`QImage`实例上绘制的`QPainter`实例。 接下来，我们执行您在上一节中刚刚看到的绘图，完成后，我们将图像写入 PNG 文件，并显示以下行：\n\n```cpp\nimage.save(\"face.png\"); \n```\n\n`QImage`支持多种图像格式，包括 PNG 和 JPEG。 `QImage`还有一个`load`方法，可以从文件或资源加载图像。\n\n就是这样，我们不仅学习了如何在屏幕上绘制图像，还学习了如何将其绘制出屏幕并将其保存到图像文件中。 接下来，我们将继续学习如何在 Qt 中创建我们自己的自定义小部件。\n\n# 创建自定义小部件\n\n本质上，使用自定义小部件绘制与屏幕外绘制没有什么不同；您只需要一个小部件子类和一个指向小部件的绘图器，就可以了。 然而，你怎么知道什么时候该画呢？\n\nQt 的`QWidget`类定义了呈现系统用来将事件传递给小部件的接口：Qt 定义了`QEvent`类来封装有关事件的数据，而`QWidget`类定义了一个接口，Qt 的呈现系统使用该接口将事件传递给小部件进行处理。 Qt 不仅使用此事件系统来指示鼠标移动和键盘输入等内容，还使用它来请求绘制屏幕。\n\n我们先来看一下绘画。 QWidget 定义了`paintEvent`方法，Qt 的呈现系统通过传递`QPaintEvent`指针来调用该方法。 `QPaintEvent`指针包括需要重新绘制的区域和该区域的边界矩形，因为重新绘制整个矩形通常比重新绘制复杂区域更快。 当您使用`QPainter`绘制小部件的内容时，Qt 会对该区域执行必要的裁剪；但是，如果有用的话，您可以使用该信息作为需要重绘内容的提示。\n\n让我们看另一个绘画示例，这一次是一个模拟时钟小部件。 此示例来自 Qt 附带的示例代码；您可以在[https://doc.qt.io/qt-5/qtwidgets-widgets-analogclock-example.html](https://doc.qt.io/qt-5/qtwidgets-widgets-analogclock-example.html)中看到它。\n\n我在这里包含了实现模拟时钟的整个`QWidget`子类。 我们将把它分成几部分；首先是必须包含的标题，如下所示：\n\n```cpp\n#include \"analogclock.h\" \n```\n\n构造函数位于标头包含之后，如下所示：\n\n```cpp\nAnalogClock::AnalogClock(QWidget *parent) : QWidget(parent) \n{ \n    QTimer *timer = new QTimer(this); \n    connect(timer, &QTimer::timeout, this, &AnalogClock::update); \n    timer->start(1000); \n    resize(200, 200); \n} \n```\n\n构造函数创建一个`timer`对象，该对象每隔`1000`毫秒发出一个超时信号，并将该计时器连接到小部件的`update`槽。 `update`插槽强制小部件重新绘制；这就是小部件每秒更新自身的方式。 最后，它将小部件本身的大小调整为边上的`200`个像素。\n\n`timeout`信号触发`update`槽功能，基本上告知父`QWidget`类刷新屏幕，随后触发`paintEvent`功能，如下所示：\n\n```cpp\nvoid AnalogClock::update()\n{\n    QWidget::update();\n}\n```\n\n下一部分是 Paint 事件处理程序。 这是一个很长的方法，所以我们将把它分成几个部分来看。 该方法可以在以下代码块中看到：\n\n```cpp\nvoid AnalogClock::paintEvent(QPaintEvent *) \n{ \n    static const QPoint hourHand[3] = { \n        QPoint(7, 8), \n        QPoint(-7, 8), \n        QPoint(0, -40) \n    }; \n    static const QPoint minuteHand[3] = { \n        QPoint(7, 8), \n        QPoint(-7, 8), \n        QPoint(0, -70) \n    }; \n\n    QColor hourColor(127, 0, 127); \n    QColor minuteColor(0, 127, 127, 191); \n    int side = qMin(width(), height()); \n    QTime time = QTime::currentTime(); \n\n    QPainter painter(this); \n```\n\n在此之前是堆栈变量的声明，包括时针和分针的坐标数组和颜色，并获取用于绘制的`QPainter`实例。\n\n接下来是设置绘图器本身的代码。 我们请求一个抗锯齿图形，并使用 Qt 的支持来缩放和平移视图，以使我们的坐标计算更加简单，如下所示：\n\n```cpp\n    painter.setRenderHint(QPainter::Antialiasing); \n    painter.translate(width() / 2, height() / 2); \n    painter.scale(side / 200.0, side / 200.0); \n\n    painter.setPen(Qt::NoPen); \n    painter.setBrush(hourColor); \n```\n\n我们将原点平移到小部件的中间。 最后，我们设置钢笔和画笔；我们为钢笔选择`NoPen`，因此绘制的只有填充，并且我们最初将画笔设置为小时画笔颜色。\n\n在那之后，我们画时针。 此代码在渲染中使用 Qt 对旋转的支持，将视口旋转适当的量以放置时针(每小时需要 30 度)，并为指针本身绘制一个凸多边形。 下面的代码片段显示了这一点：\n\n```cpp\n    painter.save(); \n    painter.rotate(30.0 * ((time.hour() + time.minute() / 60.0))); \n    painter.drawConvexPolygon(hourHand, 3); \n    painter.restore(); \n```\n\n代码在旋转之前保存画笔的配置状态，然后在绘制时针之后恢复(未旋转)状态。\n\n当然，时针最好带有小时标记，所以我们循环 12 圈，为每个小时标记画一条线，如下所示：\n\n```cpp\n    painter.setPen(hourColor); \n\n    for (int i = 0; i < 12; ++ i) { \n        painter.drawLine(88, 0, 96, 0); \n        painter.rotate(30.0); \n    } \n```\n\n时针不挡道了，现在是画分针的时候了。 我们使用相同的旋转技巧将分针旋转到正确的位置，为分针绘制另一个凸多边形，如下所示：\n\n```cpp\n    painter.setPen(Qt::NoPen); \n    painter.setBrush(minuteColor); \n    painter.save(); \n    painter.rotate(6.0 * (time.minute() + time.second() / 60.0)); \n    painter.drawConvexPolygon(minuteHand, 3); \n    painter.restore(); \n```\n\n最后，我们在钟面周围画 60 个刻度线，每分钟一个刻度线，如下所示：\n\n```cpp\n    painter.setPen(minuteColor); \n\n    for (int j = 0; j < 60; ++ j) { \n        if ((j % 5) != 0) \n            painter.drawLine(92, 0, 96, 0); \n        painter.rotate(6.0); \n    } \n}\n```\n\n正如我前面所暗示的，自定义小部件也可以接受事件；`mousePressEvent`、`mouseReleaseEvent`和`mouseDoubleClick`事件指示用户在小部件边界内按下、释放或双击鼠标的时间。 还有`mouseMoveEventmouseMoveEvent`，每当鼠标在小部件中移动并按下鼠标按钮时，Qt 系统都会调用它。 该界面还指定了按键事件：有告诉您用户何时按下某个键的`keyPressEvent`，以及分别指示小部件何时获得和失去键盘焦点的`focusInEvent`和`focusOut`事件。\n\n头文件看起来要简单得多，我们可以在以下代码块中看到：\n\n```cpp\n#include <QWidget>\n#include <QTimer>\n#include <QTime>\n#include <QPainter>\n\nclass AnalogClock : public QWidget\n{\n    Q_OBJECT\npublic:\n    explicit AnalogClock(QWidget *parent = nullptr);\n    void paintEvent(QPaintEvent *);\nsignals:\npublic slots:\n    void update();\n};\n```\n\n下面的屏幕截图显示了运行中的钟面：\n\n![](img/5bd9cf34-2f68-41a0-a068-2bce9c7fc0c4.png)\n\nFor more information about the `QWidget` interface and creating custom widgets, see the `QWidget` documentation at [https://doc.qt.io/qt-5/qwidget.html](https://doc.qt.io/qt-5/qwidget.html) and the Qt event system documentation at [https://doc.qt.io/qt-5/eventsandfilters.html](https://doc.qt.io/qt-5/eventsandfilters.html).\n\n在本节中，我们学习了如何使用 Qt 的 Paint 事件创建实时模拟时钟显示。 让我们继续下一节，学习如何使用 Qt 的 Graphics View 框架创建一个简单的 2D 游戏！\n\n# 图形视图框架简介\n\nQt 提供了一个单独的视图框架，即 Graphics View 框架，可以一次绘制成百上千个相对轻量级的自定义项目。 如果您正在从头开始实现您自己的小部件集(尽管您可能也想考虑 Qt Quick 来实现这一点)，或者如果您有大量的项目要同时显示在屏幕上，每个项目都有自己的位置和数据，那么您可以选择 Graphics View 框架。 这对于处理和显示大量数据的应用尤其重要，例如地理信息系统或计算机辅助设计应用。\n\n在 Graphics View 框架中，Qt 定义场景，负责为大量项目提供快速界面。 (如果您还记得我们在上一章中对**Model-View-Controller**(**MVC**)的讨论，您可以将场景视为视图渲染器的模型。)。 该场景还将事件分布到它包含的项目，并管理场景中各个项目的状态。 `QGraphicsScene`是负责场景实现的 Qt 类。 您可以将`QGraphicsScene`看作一个可绘制项目的容器，每个项目都是`QGraphicsItem`的子类。\n\n您的`QGraphicsItem`子类可用于覆盖每个项目的绘图和事件处理，然后您可以通过调用`addItem`方法`QGraphicsScene`将您的自定义项目添加到您的`QGraphicsScene`类中。 `QGraphicsScene`提供了一个`items`方法，该方法返回点、矩形、多边形或常规矢量路径包含或相交的项的集合。 在幕后，`QGraphicsScene`使用二进制空间分区树(参见 Wikipedia 在[http://en.wikipedia.org/wiki/Binary_space_partitioning](http://en.wikipedia.org/wiki/Binary_space_partitioning)上关于 BSP 树的文章)，以便非常快速地按位置搜索条目层次结构(请参阅 Wikipedia 上关于 BSP 树的文章)。\n\n场景中有一个或多个`QGraphicsItem`子类实例，表示场景中的图形项；Qt 定义了一些用于渲染的简单子类，但您可能需要创建自己的子类。 Qt 提供以下功能：\n\n*   `QGraphicsRectItem`：这用于呈现矩形。\n*   `QGraphicsEllipseItem`：这是用来渲染椭圆的。\n*   `QGraphicsTextItem`：这用于呈现文本。\n\n让我们在这里逐一了解一下：\n\n`QGraphicsItem`提供一个可以在子类中重写的接口，用于管理鼠标和键盘事件、拖放、接口层次结构和冲突检测。 每个项目都驻留在其自己的局部坐标系中，辅助函数为您提供了项目坐标和场景坐标之间的快速转换。\n\n图形视图框架使用一个或多个`QGraphicsView`实例来显示`QGraphicsScene`类的内容。 可以将多个视图附加到同一场景，每个视图都有自己的平移和旋转，以查看场景的不同部分。 `QGraphicsView`小部件是一个滚动区域，因此您还可以将滚动条挂接到视图，让用户在视图中滚动。 视图接收来自键盘和鼠标的输入，为场景生成场景事件，并将这些场景事件调度到场景，然后场景将这些相同的事件调度到场景中的项目。\n\nGraphics View 框架非常适合于创建游戏，事实上，Qt 的示例源代码就是您可以在[https://wiki.qt.io/Towers_lasers_and_spacecrafts_example](https://wiki.qt.io/Towers_lasers_and_spacecrafts_example)上看到的塔楼和宇宙飞船示例应用。 如果你愿意的话，这个游戏很简单，由电脑来玩；静止的塔楼射击迎面而来的移动的宇宙飞船，正如你在下面的屏幕截图中所看到的：\n\n![](img/abaa4825-5233-4d0a-83d5-5ca95a397d2e.png)\n\n让我们看一下这个示例应用中的部分代码，以了解 Graphics View 框架的实际工作方式。\n\n游戏的核心是更新移动设备位置的游戏计时器；应用的入口点设置计时器`QGraphicsView`和负责跟踪状态的`QGraphicsScene`的子类。 为此，请考虑以下代码：\n\n```cpp\n#include \"mainwindow.h\"\n\n#include <QApplication>\n#include <QGraphicsView>\n#include \"scene.h\"\n#include \"simpletower.h\"\n\nint main(int argc, char *argv[])\n{\n    QApplication app(argc, argv);\n    Scene scene;\n    scene.setSceneRect(0,0,640,360);\n    QGraphicsView view(&scene);\n    QTimer timer;\n    QObject::connect(&timer, &QTimer::timeout, &scene, \n      &Scene::advance);\n    view.show();\n    timer.start(10);\n    return app.exec();\n}\n```\n\n计时器每 10 毫秒计时一次，并连接到场景的提前时段，负责推进游戏状态。 `QGraphicsView`类是整个场景的渲染窗口；它接受要从中渲染的`Scene`对象的一个实例。 应用的`main`函数初始化视图、场景和计时器，启动计时器，然后将控制传递给 Qt 的事件循环。\n\n`Scene`类有两个方法：一个是构造函数，它在场景中创建一些不移动的塔；另一个是`advance`方法，它推进场景的一次性计时，每次经过`main`函数中的计时器时都会触发该方法。 让我们首先看一下构造函数，如下所示：\n\n```cpp\n#include \"scene.h\"\n#include \"mobileunit.h\"\n#include \"simpletower.h\"\n#include <QDebug>\n\nScene::Scene(): QGraphicsScene(), ticTacTime(0)\n{\n    SimpleTower * simpleTower = new SimpleTower();\n    simpleTower->setPos(200.0, 100.0);\n    addItem(simpleTower);\n\n    simpleTower = new SimpleTower();\n    simpleTower->setPos(200.0, 180.0);\n    addItem(simpleTower);\n\n    simpleTower = new SimpleTower();\n    simpleTower->setPos(200.0, 260.0);\n    addItem(simpleTower);\n\n    simpleTower = new SimpleTower();\n    simpleTower->setPos(250.0, 050.0);\n    addItem(simpleTower);\n```\n\n我们继续创建更多的`SimpleTower`对象，并使用`setPos`和`addItem`对其进行初始化，如下所示：\n\n```cpp\n    simpleTower = new SimpleTower();\n    simpleTower->setPos(250.0, 310.0);\n    addItem(simpleTower);\n\n    simpleTower = new SimpleTower();\n    simpleTower->setPos(300.0, 110.0);\n    addItem(simpleTower);\n\n    simpleTower = new SimpleTower();\n    simpleTower->setPos(300.0, 250.0);\n    addItem(simpleTower);\n\n    simpleTower = new SimpleTower();\n    simpleTower->setPos(350.0, 180.0);\n    addItem(simpleTower);\n}\n```\n\n非常无聊-它只创建静态塔的实例并设置它们的位置，使用`addItem`方法将每个塔添加到场景中。 在查看`SimpleTower`类之前，让我们先看一下`Scene`类的`advance`方法，如以下代码块所示：\n\n```cpp\nvoid Scene::advance()\n{\n    ticTacTime++ ;\n\n    // delete killed objects\n    QGraphicsItem* item = nullptr;\n    MobileUnit* unit = nullptr;\n    int i = 0;\n\n    while (i < items().count())\n    {\n        item = items().at(i);\n        unit = dynamic_cast<MobileUnit*>(item);\n        if ((unit != nullptr) && (unit->getIsFinished() == true))\n        {\n            removeItem(item);\n            delete unit;\n        }\n        else\n            ++ i;\n    }\n```\n\n在此之后，我们每隔 20 个刻度添加一个新单位，如下所示：\n\n```cpp\n    // Add new units every 20 tictacs\n    if (ticTacTime % 20 == 0)\n    {\n        // qDebug() << \"add unit\";\n        MobileUnit* mobileUnit= new MobileUnit();\n        qreal h = static_cast<qreal>(qrand() % static_cast<int>\n          (height()));\n        mobileUnit->setPos(width(), h);\n        addItem(mobileUnit);\n    }\n\n    QGraphicsScene::advance();\n    update();\n}\n```\n\n从前面的代码中，我们可以看到该方法有两个关键部分，如下所述：\n\n*   第一部分删除由于某种原因(例如，它们的健康状况降至 10)而过期的所有移动单元。 这是通过循环遍历场景中的所有项目并测试每个项目是否都是`MobileUnit`实例来实现的。 如果是，则代码测试其`isFinished`函数，如果为真，则从场景中删除该项目并释放它。\n*   第二部分每隔 20 次通过`advance`方法运行一次，并创建一个新的`MobileUnit`对象，将其随机放置在显示屏的右侧。 最后，该方法调用继承的`advance`方法，该方法触发对场景中每个项目的提前调用，然后调用`update`，这触发场景的重绘。\n\n接下来让我们看一下`SimpleTower`的`QGraphicsItem`子类。 首先，让我们看一下`SimpleTower`构造函数，如以下代码块所示：\n\n```cpp\n#include <QPainter>\n#include <QGraphicsScene>\n#include \"simpletower.h\"\n#include \"mobileunit.h\"\n\nSimpleTower::SimpleTower(): QGraphicsRectItem()\n, detectionDistance(100.0), time(0, 0)\n, reloadTime(100), shootIsActive(false)\n, target(nullptr), towerImage(QImage(\":/lightTower.png\"))\n{\n    setRect(-15.0, -15.0, 30.0, 30.0);\n    time.start();\n}\n```\n\n构造函数设置塔楼的边界并启动计时器，用于确定塔楼向迎面而来的船只开火的时间间隔。\n\n`QgraphicsItem`实例在其`paint`方法中进行绘制；`paint`方法采用将用于呈现项的`QPainter`指针，以及指向项和层次结构中所属小部件的呈现选项的指针。 下面是`SimpleTower`的`paint`方法：\n\n```cpp\nvoid SimpleTower::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget* widget)\n{\n    painter->drawImage(-15, -15, towerImage);\n    if ((target != nullptr) && (shootIsActive))\n    { // laser beam\n        QPointF towerPoint = mapFromScene(pos());\n        QPointF theTarget = mapFromScene(target->pos());\n        painter->setPen(QPen(Qt::yellow,8.0,Qt::SolidLine));\n        painter->drawLine(towerPoint.x(), towerPoint.y(), \n          theTarget.x(), theTarget.y());\n        painter->setPen(QPen(Qt::red,5.0,Qt::SolidLine));\n        painter->drawLine(towerPoint.x(), towerPoint.y(), \n          theTarget.x(), theTarget.y());\n        painter->setPen(QPen(Qt::white,2.0,Qt::SolidLine));\n        painter->drawLine(towerPoint.x(), towerPoint.y(), \n          theTarget.x(), theTarget.y());\n        shootIsActive = false;\n    }\n}\n```\n\n`paint`方法必须绘制两个内容：塔本身，这是在构建时加载的静态图像(用`drawImage`绘制)，如果塔向目标射击，它会在塔和塔所针对的移动单元之间绘制彩色线条。\n\n接下来，我们将继续学习`advance`方法，如以下代码块所示：\n\n```cpp\nvoid SimpleTower::advance(int phase)\n{\n    if (phase == 0)\n    {\n        searchTarget();\n        if ((target != nullptr) && (time.elapsed() > reloadTime))\n            shoot();\n    }\n}\n```\n\n每次场景前进时，每个塔都会搜索一个目标，如果选择了一个，它就会向目标射击。 场景图为每次前进调用每个项目的`advance`方法两次，传递一个整数，指示场景中的项目是即将前进(当`phase`参数为`0`时表示)，还是场景中的项目已经前进(当`phase`段为`1`时表示)。\n\n`searchTarget`方法在检测距离内查找最近的目标，如果找到，则将塔的目标指针设置为范围内最近的单位，如下所示：\n\n```cpp\nvoid SimpleTower::searchTarget()\n{\n    target = nullptr;\n    QList<QGraphicsItem*> itemList = scene()->items();\n    int i = itemList.count() - 1;\n    qreal dx, dy, sqrDist;\n    qreal sqrDetectionDist = detectionDistance * detectionDistance;\n    MobileUnit* unit = nullptr;\n    while((i >= 0) && (nullptr == target) )\n    {\n        QGraphicsItem * item = itemList.at(i);\n        unit = dynamic_cast<MobileUnit*>(item);\n        if ((unit != nullptr) && (unit->getLifePoints() > 0))\n        {\n            dx = unit->x() - x();\n            dy = unit->y() - y();\n            sqrDist = dx * dx + dy * dy;\n            if (sqrDist < sqrDetectionDist)\n                target=unit;\n        }\n        --i;\n    }\n}\n```\n\n请注意，我们缓存指向目标单元的指针并调整其位置，因为在后续帧中，目标单元将移动。 最后，`shoot`方法简单地设置了`paint`用来指示应该绘制拍摄图形的布尔标志，它向目标指示它已被损坏。 下面的代码显示了这一点：\n\n```cpp\nvoid SimpleTower::shoot()\n{\n    shootIsActive=true;\n    target->touched(3);\n    time.restart();\n}\n```\n\n这将重新启动计时器，该计时器用于跟踪计时器拍摄的后续快照之间的时间。 最后，让我们看一下在场景中渲染单个移动宇宙飞船的`MobileUnit`类。 遵循以下步骤：\n\n1.  首先定义`include`指令，然后定义构造函数，如下所示：\n\n```cpp\n#include \"mobileunit.h\"\n#include <QPainter>\n#include <QGraphicsScene>\n#include <math.h>\n\nMobileUnit::MobileUnit(): QGraphicsRectItem()\n, lifePoints(10), alpha(0)\n, dirX(1.0), dirY(0.0)\n, speed(1.0), isFinished(false)\n, isExploding(false), explosionDuration(500)\n, redExplosion(0.0, 0.0, 20.0, 0.0, 0.0), time(0, 0)\n, spacecraftImage(QImage(\":/spacecraft00.png\") )\n{\n    alpha = static_cast<qreal>(qrand() % 90 + 60);\n    qreal speed = static_cast<qreal>(qrand()% 10 - 5);\n    dirY = cos(alpha / 180.0 * M_PI );\n    dirX = sin(alpha / 180.0 * M_PI);\n    alpha = -alpha * 180.0 ;\n    speed = 1.0 + speed * 0.1;\n    setRect(-10.0, -10.0, 20.0, 20.0);\n    time.start();\n\n    redExplosion.setColorAt(0.0, Qt::white);\n    redExplosion.setColorAt(0.2, QColor(255, 255, 100, 255));\n    redExplosion.setColorAt(0.4, QColor(255, 80, 0, 200));\n    redExplosion.setColorAt(1.0, QColor(255, 255, 255, 0));\n}\n```\n\n构造器比固定单元的构造器稍微复杂一些。 它需要为移动单元设置初始航向和速度。 然后，它设置单元和计时器的界限来控制自己的行为。 如果该单元被禁用，它将爆炸；我们将使用径向渐变中的同心圆绘制爆炸，因此我们需要在渐变中的各个点设置颜色。\n\n2.  接下来是`paint`方法，该方法在单位受损时绘制单位或单位爆炸，如以下代码块所示：\n\n```cpp\nvoid MobileUnit::paint(QPainter *painter, const QStyleOptionGraphicsItem* option, QWidget* widget)\n{\n    painter->setPen(Qt::NoPen);\n\n    if (!isExploding)\n    {\n        painter->rotate(alpha);\n        painter->drawImage(-15, -14, spacecraftImage);\n    }\n    else\n    {\n        painter->setBrush(QBrush(redExplosion));\n        qreal explosionRadius = 8.0 + time.elapsed() / 50;\n        painter->drawEllipse(-explosionRadius, -explosionRadius, 2.0 * explosionRadius, 2.0 * explosionRadius);\n    }\n}\n```\n\n这非常简单：如果单元没有爆炸，它只设置要绘制的图像的旋转并绘制图像；否则，它使用我们在构造函数中配置的径向渐变笔刷绘制圆形爆炸。\n\n3.  之后是`advance`方法，该方法负责将舰船从一个帧移动到下一个帧，并跟踪爆炸舰船的状态，如以下代码块所示：\n\n```cpp\nvoid MobileUnit::advance(int phase)\n{\n    if (phase==0)\n    {\n        qreal xx = x(); qreal yy = y();\n        if ( (xx < 0.0) || (xx > scene()->width() ) )\n        { // rebond\n            dirX = -dirX;\n            alpha = -alpha;\n        }\n        if ( (yy < 0.0) || (yy > scene()->height()))\n        { // rebond\n            dirY = -dirY;\n            alpha = 180 - alpha;\n        }\n        if (isExploding)\n        {\n            speed *= 0.98; // decrease speed\n            if (time.elapsed() > explosionDuration)\n                isFinished = true; // is dead\n        }\n        setPos(x() + dirX * speed, y() + dirY * speed);\n    }\n}\n```\n\n为简单起见，`advance`方法通过反转方向和方向使场景边缘的项目从页边距反弹。 如果物品正在爆炸，则其减速，并且如果定时器中经过的时间长于爆炸持续时间，则该方法设置指示在下一场景推进期间应当从场景中移除该物品的标志。 最后，该方法通过将方向和速度的乘积与每个坐标相加来更新项目的位置。\n\n4.  最后，`touched`方法将移动单元的健康点递减指定的量，如以下代码块所示：\n\n```cpp\nvoid MobileUnit::touched (int hurtPoints)\n{\n    lifePoints -= hurtPoints; // decrease life\n    if (lifePoints < 0)\n        lifePoints = 0;\n    if (lifePoints == 0)\n    {\n        time.start();\n        isExploding = true;\n    }\n}\n```\n\n如果该装置的生命值为零，它会启动爆炸定时器并设置爆炸标志。\n\nFor more documentation about the Graphics View framework, see the Qt documentation at [https://doc.qt.io/qt-5/graphicsview.html](https://doc.qt.io/qt-5/graphicsview.html).\n\n就是这样，我们已经成功地使用 Qt 的 Graphics View 框架创建了一个简单的游戏。 你可以进一步扩展这个项目，把它变成一个完整的游戏，也许还可以在 App Store 上发布它！\n\n# 简略的 / 概括的 / 简易判罪的 / 简易的\n\n在本章中，我们学习了如何使用`QPainter`类在屏幕上和屏幕外绘制图形。 我们还学习了如何在 Qt 中创建自己的自定义小部件。 然后，我们探索了 Graphics View 框架并创建了一个简单的游戏。\n\n贯穿本章，我们了解了 Qt 如何提供`QPaintDevice`接口和`QPainter`类来执行图形操作。 使用`QPaintDevice`子类(如`QWidget`、`QImage`和`QPixmap`)，您可以执行屏幕上和屏幕外绘制。 我们还了解了 Qt 如何通过 Graphics View 框架(由类`QGraphicsView`和`QGraphicsScene`以及`QGraphicsItem`支持)为大量轻量级对象提供单独的可视对象层次结构。\n\n在下一章中，我们将从 Qt 对 C++ 中 GUI 的支持转向 Qt Quick 的支持。 我们将学习基本的 Qt Quick 构造、在 Qt Quick 中执行动画和其他过渡，以及如何将 Qt Quick 与 C++ 应用集成。"
  },
  {
    "path": "docs/app-dev-qt-creator/07.md",
    "content": "# 七、使用 Qt Quick 实现更多功能\n\n正如您在[第 3 章](03.html)，*使用 Qt Designer 设计应用*中看到的，Qt Quick 是 Qt 用于应用开发的声明性环境。 Qt Quick 是流畅的动画用户界面的理想之选，在这些界面中，您可以更多地使用触摸和鼠标事件，并且应用的样式基于图形资源，而不需要镜像主机平台的窗口小部件集。\n\nQt Quick 提供了几个基本的图形元素，并提供了使用基于 JavaScript 的脚本语言将它们组合在一起的能力，使您能够利用网页设计的现有技能来创建用户体验，如果没有大量的额外工作，这些用户体验是不可能完全用 HTML 和 CSS 创建的。 但是，您可以通过使用 Qt Quick 的其他一些我们在上一章中没有探索的功能来做更多的事情。 让我们看看使用 Qt Quick 还能实现什么！\n\n在本章中，我们将比在[第 3 章](03.html)，*使用 Qt Designer 设计应用*中更详细地了解 Qt Quick。\n\n在本章中，我们将介绍以下内容：\n\n*   理解 Qt Quick 的基本概念\n*   在 Qt Quick 中使用状态和转换\n*   Qt Quick 与 C++ 的集成\n*   将所有这些放在一起-一个图片库应用\n*   介绍新的 Qt 快速控件 2\n*   了解 SCXML 的新图形编辑器\n\n# 技术要求\n\n本章的技术要求包括 Qt 5.13.1 MinGW 64 位、Qt Creator 4.10.0 和 Windows 10。\n\n本章的 GitHub 链接可在此处找到：\n\n[https：//github.com/PacktPublishing/Application-Development-with-Qt-Creator-Third-Edition/tree/master/Chapter07](https://github.com/PacktPublishing/Application-Development-with-Qt-Creator-Third-Edition/tree/master/Chapter07).\n\n# 理解 Qt Quick 的基本概念\n\n正如您在[第 3 章](03.html)，*使用 Qt Designer 设计应用*中看到的，Qt Quick 使您能够专注于声明屏幕上可见的内容以及它的行为方式，而不是创建对象。 Qt Quick 使用**Qt 建模语言**语言(**QML**)来实现这一点，它使用类似 JavaScript 的语法和树结构来定义可见对象及其关系，并编写它们的行为脚本。 对象具有严格的父子层次结构，该层次结构定义了它们的视觉关系；父对象在显示时包含其子对象。\n\n要创建新的 Qt Quick 项目，请转到文件|新建文件或项目|创建 Qt Quick Application-Empty。\n\n使用传统坐标系将对象放置在 Qt Quick 的主窗口中，其中(0，0)位于显示的左上角。 子对象可以使用相对于父对象左上角的坐标放置在其父对象中，也可以通过相对于对象边界的灵活锚点系统来放置。\n\n每个项目都有七条不可见的锚线：`left`、`horizontalCenter`、`right`、`top`、`verticalCenter`、`bottom`和`baseline`。 所有这些都是不言而喻的，除了`baseline`，它对应于文本所在的行。 (对于没有文本的项目，Qt 将基线定义为与`top`锚定线相同。)。 锚点是相对于其他对象的；每条锚定线都是锚定字段中的一个字段，您可以将其设置为另一个对象的锚定线。 例如，让我们写下以下内容：\n\n```cpp\nimport QtQuick 2.12\nimport QtQuick.Window 2.12\n\nWindow {\n    visible: true\n    width: 360\n    height: 360\n    Rectangle {\n        id: rect1\n        height: 100 \n        width: 100 \n        anchors.centerIn: parent \n        color: \"gray\" \n    }\n    Rectangle { \n        id: rect2 \n        height: 100 \n        width: 100 \n        anchors.left: rect1.right \n        anchors.top: rect1.top \n        color: \"black\" \n    } \n} \n```\n\n在前面的代码中，我们创建了两个`Rectangle`对象，即`rect1`和`rect2`。 `rect1`对象以灰色显示，并位于其父对象(即应用窗口)的中心。 然后，我们将`rect2`对象定位到`rect1`的右侧，并将其垂直位置与`rect1`对齐。\n\n这将产生以下输出：\n\n![](img/9e56c0b0-efa2-4ae7-b129-1bb3445277b1.png)\n\n此代码创建一个高度和宽度均为 360 像素的窗口和两个矩形。 Qt 将第一个矩形放置在父窗口的中心，并将第二个矩形放置在具有相同顶线的第一个矩形的右侧。\n\n锚定不仅会影响位置，还会影响大小。 请考虑以下 QML：\n\n```cpp\nimport QtQuick 2.12 \nimport QtQuick.Window 2.12\n\nWindow { \n    visible: true \n    width: 360 \n    height: 360 \n\n    Rectangle { \n        id: rect1 \n        height: 100 \n        width: 100 \n        anchors.left: parent.left; \n        anchors.verticalCenter: parent.verticalCenter \n        color: \"gray\" \n    } \n\n    Rectangle { \n        id: rect2 \n        anchors.left: rect1.right \n        anchors.top: rect1.top; \n        anchors.bottom: rect1.bottom \n        anchors.right: rect3.left \n        color: \"black\" \n    } \n\n    Rectangle { \n        id: rect3 \n        height: 100 \n        width: 100 \n        anchors.right: parent.right; \n        anchors.verticalCenter: parent.verticalCenter \n        color: \"gray\" \n    } \n} \n```\n\n这会在父窗口的两侧放置两个正方形，在这两个正方形之间拉伸一个矩形。 请注意，将`rect2`的`anchor.left`参数设置为`rect1.right`值，并将`rect2`的`anchor.right`设置为`rect3.left`值。 下面的屏幕截图显示了它的外观：\n\n![](img/1d9ebe5b-95b1-4c4b-a4a1-05fe73bcf893.png)\n\n对于更复杂的布局，Qt Quick 定义了四个定位元素：`Column`、`Row`、`Grid`和`Flow`。 这些代码的行为与您预期的大致相同：\n\n*   `Column`：这会在垂直列中布局其子项。\n*   `Row`：这会在水平条中布局其子项。\n*   `Grid`：这会在网格中布局其子对象。\n*   `Flow`：这将并排排列子元素，并在必要时包装元素。\n\n下面的代码使用`Flow`元素根据窗口大小均匀排列`Rectangle`对象：\n\n```cpp\nimport QtQuick 2.12\nimport QtQuick.Window 2.12\n\nWindow {\n    visible: true\n    width: 360\n    height: 360\n\n    Flow {\n        anchors.fill: parent\n        anchors.margins: 4\n        spacing: 10\n\n        Rectangle { height: 40; width: 40; color: \"gray\" }\n        Rectangle { height: 40; width: 40; color: \"black\" }\n        Rectangle { height: 40; width: 40; color: \"gray\" }\n        Rectangle { height: 40; width: 40; color: \"black\" }\n        Rectangle { height: 40; width: 40; color: \"gray\" }\n        Rectangle { height: 40; width: 40; color: \"black\" }\n        Rectangle { height: 40; width: 40; color: \"gray\" }\n        Rectangle { height: 40; width: 40; color: \"black\" }\n        Rectangle { height: 40; width: 40; color: \"gray\" }\n    }\n}\n```\n\n这将产生一个从左到右的矩形流，间隔为 10 个像素，左侧为 4 个像素，如下所示：\n\n![](img/7d4a0877-5ffa-468f-96c2-2272b9e05ce9.png)\n\n通常，只要您不能完全控制目标硬件，就应该使用锚定布局和更复杂的`Row`、`Column`、`Grid`和`Flow`元素。 相对间隔的布局让 Qt 在运行时做出布局决策，这样您就不会在大屏幕上出现奇怪的空间，或者布局会溢出小屏幕的边界。 当然，在设计时需要更多考虑，因为您需要确定各种项的相对间距，将最大空间分配给需要它的项。\n\n当然，除了`Rectangle`之外，Qt Quick 还定义了几个可见元素。 视觉类型包括以下内容：\n\n*   `Window`：这是应用的顶级窗口。\n*   `Item`：这是所有可见对象继承的基本视觉对象类型。\n*   `Image`：用于渲染位图。\n*   `AnimatedImage`：用于播放 GIF 动画图像。\n*   `AnimatedSprite`：用于播放存储为单个图像中的一系列精灵的动画图像。\n*   `Text`：用于在场景中绘制文本。\n\n所有这些都需要您导入`QtQuick`模块，除了需要`QtQuick.Window`的`Window`。 撰写本文时，此模块的当前版本是 2.2，但您可以查看文档以了解有关特定版本的信息。\n\n类(如`Image`类)有一个`source`属性，该属性从获取其内容的位置获取 URL。 通常，您的图像要么来自应用的资源段，要么来自 Web；如果您需要从其他来源加载它们或动态创建它们，您可以提供图像提供程序。 (我将在本章末尾的示例中向您展示如何做到这一点。)\n\n还有`WebEngineView`，这是一个使用 Qt WebEngine 构建的控件，您可以将其包含在 QML 中，并且可以呈现 Web 内容。 与其他可视化元素不同，您需要使用`import QtWebEngine`导入它，并启用项目文件中的`webengine`模块来访问`WebEngineView`。 QML 中的网页查看器非常简单，如下所示：\n\n```cpp\nimport QtQuick 2.12\nimport QtQuick.Window 2.12\nimport QtWebEngine 1.8\n\nWindow {\n    visible: true\n    width: 640\n    height: 480\n\n    WebEngineView {\n        anchors.top: parent.top;\n        anchors.left: parent.left\n        anchors.right: parent.right;\n        anchors.bottom: parent.bottom;\n        id: web\n        url: \"http://www.zhieng.com\"\n    }\n}\n```\n\n前面的代码产生以下结果：\n\n![](img/228bc35f-0ae6-49bf-90c4-59381c512084.png)\n\nNote that the current version of the Qt WebEngine module only works with MSVC compiler under windows and not any other compilers. This is because the Chromium engine in the Qt WebEngine module has not been ported to other compilers, so we can only use MSVC for now.\n\nQML 定义了一个接受输入的对象集合，而不是像 Qt 小部件那样通过事件方法处理用户输入。 这些对象包括以下对象：\n\n*   `MouseArea`：用于鼠标点击和移动。\n*   `Keys`：这是用来处理击键的。\n*   `KeyNavigation`：用于处理箭头导航。\n*   `Flickable`：用于处理鼠标轻击或触摸轻击以进行滚动。\n*   `PinchArea`：这用于处理捏到缩放或其他两指交互。\n*   `MultiPointTouchArea`：用于处理通用多点触控输入。\n*   `Drag`：这用于处理可视项的拖放事件。\n*   `DropArea`：此选项用于接受从拖动区放置的项目。\n*   `TextInput`：用于捕获简单输入字段中的自由格式文本输入。\n*   `TextEdit`：用于多行文本输入。\n\n正如您在[第 3 章](03.html)，*使用 Qt Designer 设计您的应用*中看到的那样，这些定义中的每一个都定义了您可以绑定 JavaScript 的信号。 例如，`MouseArea`定义了以下信号：\n\n*   `onClicked`：当用户单击鼠标区域时会发出此消息。\n*   `onDoubleClicked`：当用户双击鼠标区域时会发出此消息。\n*   `onEntered`：当用户将鼠标指针移入鼠标区域时会发出此消息。\n*   `onExited`：当用户将鼠标指针移出鼠标区域时发出。\n*   `onPressed`：当用户在鼠标区域按下鼠标时会发出此信息。\n*   `onReleased`：当用户在鼠标区域松开鼠标按键时会发出此消息。\n*   `onWheel`：当用户在鼠标区域操作鼠标滚轮时会发出此消息。\n\n信号伴随着一个包含信号细节的事件；例如，`MouseArea`元素将`MouseEvent`事件发送到信号处理程序。 这些不是绑定 JavaScript 的参数，而是作为特殊变量出现在 JavaScript 中。\n\nQt Quick 还为需要数据模型的用户交互定义了其他一些视图；这些视图如下所示：\n\n*   `ListView`：正如您可以想象的那样，`ListView`在垂直或水平列表中布局其项目。\n*   `GridView`：这将在`gridPathView`中布局其项目。`PathView`采用任意路径，并将其项目布局在您提供的路径上。\n\n所有这些都使用`ListModel`来包含它们的数据，遵循我们在上一章中讨论的 MVC 模式。 执行此操作时，使用 QML 绑定将视图绑定到模型，视图依赖于模型更改来显示模型中的更改。 您可以在 Qt Quick 中为`ListModel`容器提供项，也可以提供`QAbstractItemModel`的子类，并通过 Qt Quick C++ 绑定提供您自己的数据。 (我将在本章后面的*集成 Qt Quick 和 C++*一节中讨论如何绑定 QML 和 C++。)。 这些视图完全支持触摸，支持轻击和惯性滚动。 他们使用模型和委托来执行绘图；委托只是引用模型中的字段的 QML`Item`元素。 例如，此 QML 创建一个包含三个元素的列表，每个元素都有两个字段`name`和`number`：\n\n```cpp\nimport QtQuick 2.12\nimport QtQuick.Window 2.12\n\nWindow {\n    visible: true\n    width: 360\n    height: 360\n\n    ListModel {\n        id: model\n        ListElement {\n            name: \"Bill Smith\"\n            number: \"555 3264\"\n        }\n        ListElement {\n            name: \"John Brown\"\n            number: \"555 8426\"\n        }\n        ListElement {\n            name: \"Sam Wise\"\n            number: \"555 0473\"\n        }\n    }\n\n    ListView {\n        model: model\n        anchors.top: parent.top;\n        anchors.bottom: parent.bottom;\n        anchors.left: parent.left;\n        anchors.right: parent.right;\n        delegate: Item {\n            width: 180\n            height: 100\n            Column {\n                Text { text: name }\n                Text { text: number }\n            }\n        }\n    }\n}\n```\n\n`ListModel`容器就是：`ListView`的数据列表。 `ListView`委托是一个`Item`元素，其中`Column`包含两个文本字段。 请注意，每个字段的文本只是`ListModel`字段中的字段名称。 前面的代码产生以下结果：\n\n![](img/8e10bb3a-06e6-4878-85c9-19ba0bf19e6b.png)\n\n当然，我们可以将其分成三个文件。 我们可以将`ListModel`保存在名为`ContactModel.qml`的文件中，并将委托项保存在名为`ListModelDelegate.qml`的文件中，如下所示：\n\n```cpp\n// ContactModel.qml \nimport QtQuick 2.12 \n\nListModel { \n    id: model \n    ListElement { \n        name: \"Bill Smith\" \n        number: \"555 3264\" \n    } \n    ListElement { \n        name: \"John Brown\" \n        number: \"555 8426\" \n    } \n    ListElement { \n        name: \"Sam Wise\" \n        number: \"555 0473\" \n    } \n} \n\n// ListModelDelegate.qml \nimport QtQuick 2.12\n\nItem { \n    width: 180 \n    height: 100 \n    Column { \n        Text { text: name } \n        Text { text: number } \n    } \n} \n```\n\n然后，我们的主应用将类似于以下代码：\n\n```cpp\nimport QtQuick 2.12 \nimport QtQuick.Window 2.12 \n\nWindow { \n    visible: true \n    width: 360 \n    height: 360 \n\n    ListView { \n        model: ContactModel {} \n        anchors.top: parent.top; \n        anchors.bottom: parent.bottom; \n        anchors.left: parent.left; \n        anchors.right: parent.right; \n        delegate: ListModelDelegate {} \n    } \n} \n```\n\n如果您认为您将拥有一个可重用的组件，或者当您的代码太长而无法理解时，将您的 QML 划分为单独的文件是一个好主意。\n\n就是这样--我们已经学习了如何使用 Qt Quick 中的一些基本功能，比如定位元素、Web 引擎和列表视图。 这将使您能够更好地了解 Qt Quick 项目和 Qt 窗体项目之间的区别，同时为您的项目选择正确的项目。 在此之后，让我们继续下一节，了解如何为我们的 Qt Quick 项目创建状态机。\n\n# 在 Qt Quick 中使用状态和转换\n\n传统的 GUI 编程通常涉及易于编写的样板状态机，用于跟踪控件和应用状态。 例如，一个按钮可能有几种状态：当鼠标悬停在它上面时；当它被按下时；然后一旦按下，在复选框或按钮的情况下，一个单独的状态用于打开或关闭。 虽然这段代码并不难写，但它确实涉及到一些编写，而且更复杂的界面需要更多的代码。\n\nQt Quick 通过其`State`构造为此提供了抽象，该构造将状态名称、状态名称发生的条件以及对象的哪些属性应采用新值进行分组。 我们在[第 3 章](03.html)，*使用 Qt Designer 设计您的应用*中第一次看到了这一点，当时我们编写了自己的按钮组件，转载如下：\n\n```cpp\nimport QtQuick 2.12\nimport QtQuick.Window 2.12\n\nWindow {\n visible: true\n    width: 256\n    height: 256\n\n    Rectangle\n    {\n        id: button\n\n        anchors.fill: parent\n\n        property alias operation: buttonText.text\n        signal clicked\n\n        color: \"green\"\n\n        Rectangle {\n            id: shade\n            anchors.fill: button;\n            color: \"black\";\n            opacity: 0\n        }\n```\n\n我们继续在此处编写代码的其余部分：\n\n```cpp\n        Text {\n            id: buttonText\n            anchors.centerIn: parent;\n            color: \"white\"\n            font.pointSize: 16\n        }\n\n        MouseArea {\n            id: mouseArea\n            anchors.fill: parent\n            onClicked: {\n                button.clicked();\n            }\n        }\n\n        states: State {\n            name: \"pressed\";\n            when: mouseArea.pressed === true;\n            PropertyChanges {\n                target: shade; opacity: .4\n            }\n        }\n    }\n}\n\n```\n\n在这里，按钮有两种状态：默认状态，即阴影完全透明；当用户按下按钮(由按下状态表示)时，将阴影的`opacity`属性设置为`0.4`。 前面的代码生成以下结果-左侧为默认状态，右侧为按下状态：\n\n![](img/37b5fc0c-ae95-4bdb-a4d0-e56f8ad451cd.png)\n\n项的`states`字段实际上可以是一个数组；编写这些状态的另一种方法是显式声明这两种状态，如下所示：\n\n```cpp\nstates: [ \n    State { \n        name: \"pressed\"; when: mouseArea.pressed === true \n        PropertyChanges { target: shade; opacity: .4 } \n    }, \n    State { \n        name: \"released\"; when: mouseArea.pressed !== true \n        PropertyChanges { target: shade; opacity: 0.0 } \n    } \n] \n```\n\n每个`State`构造可以有多个`PropertyChanges`元素，每个元素都有自己的目标和属性，因此可以使用简单的声明创建复杂的状态转换。\n\n发生状态更改时，Qt Quick 使用过渡来控制属性的动画。 要创建过渡，需要定义动画；可以在任何可以更改的属性上应用动画，指定动画的起始值和结束值。 考虑一个简单的鼠标按下动画，如下所示：\n\n```cpp\nimport QtQuick 2.12\nimport QtQuick.Window 2.12\n\nWindow\n{\n    visible: true\n height: 360\n    width: 360\n\n    Rectangle {\n        id: rect1\n        width: 100; height: 100\n        color: \"red\"\n\n        MouseArea {\n            id: mouseArea\n            anchors.fill: parent\n        }\n\n        states: State {\n            name: \"moved\"; when: mouseArea.pressed\n            PropertyChanges { target: rect1; x: 100; y: 100 }\n        }\n\n        transitions: Transition {\n            NumberAnimation {\n                properties: \"x,y\"\n                easing.type: Easing.InOutQuad\n            }\n        }\n    }\n}\n```\n\n前面的代码产生以下结果-默认状态在左侧，按下状态在右侧：\n\n![](img/8a454081-cc63-4213-b054-b825f7b33cc6.png)\n\n在这里，矩形从父坐标系的原点开始，矩形中有`MouseArea`。 当您在矩形中按下鼠标时，矩形的状态将更改为`moved`，并且矩形的左上角将移动到父画布上的(100,100)。 但是，我们还在`x`和`y`属性上指定了动画`NumberAnimation`，因此属性更改将使用我们指定的缓动曲线(动画开始和结束时的二次曲线)设置为此值的动画。以下是我们可以用来为 Qt Quick 对象设置动画的一些动画类型：\n\n*   不同类型的属性有不同的动画。 您可以使用`NumberAnimation`来设置数字属性的动画，如您在此处所见。\n*   还有`ColorAnimation`，用于设置颜色动画，以及`RotationAnimation`，用于设置旋转动画。\n\n下面是单击一个红色矩形时将其旋转成半圆形的 QML：\n\n```cpp\nimport QtQuick 2.12\nimport QtQuick.Window 2.12\n\nWindow\n{\n    visible: true\n    height: 360\n    width: 360\n\n    Item {\n        width: 300; height: 300\n\n        Rectangle {\n            id: rect\n            width: 150; height: 100;\n            anchors.centerIn: parent\n            color: \"red\"\n            antialiasing: true\n\n            states: State {\n                name: \"rotated\"\n                PropertyChanges { target: rect; rotation: 180 }\n            }\n\n            transitions: Transition {\n                RotationAnimation {\n                    duration: 1000\n                    direction: RotationAnimation.Counterclockwise\n                }\n            }\n        }\n\n        MouseArea {\n            anchors.fill: parent;\n            onClicked: rect.state = \"rotated\"\n        }\n    }\n}\n```\n\n前面的代码产生以下结果-默认状态为左侧，按下状态为右侧：\n\n![](img/96512d10-f64c-4e1c-b09a-800490791eda.png)\n\n同样，模式是相同的：我们使用`PropertyChanges`定义状态，表示新属性值，然后使用动画(在本例中为`RotationAnimation`)指定在该属性上的转换。 在这里，转换具有以毫秒为单位指定的持续时间。 请注意，旋转仅发生一次，并且旋转角度由-360 度和 360 度的值限制。\n\nFor more information on the animation and transition framework in Qt Quick, see the Qt Quick documentation at [https://doc.qt.io/qt-5/qtquick-statesanimations-animations.html](https://doc.qt.io/qt-5/qtquick-statesanimations-animations.html) and the Qt Quick Animation samples at [https://doc.qt.io/qt-5/qtquick-animation-example.html](https://doc.qt.io/qt-5/qtquick-animation-example.html).\n\n我们已经学习了如何利用 Qt 提供的状态机来基于状态转换操作对象属性。 让我们继续下一节，学习如何将我们的 Qt Quick 项目与 C++ 编程语言集成。\n\n# Qt Quick 与 C++ 的集成\n\n您可以使用`qmlviewer`应用运行一个简单的 Qt Quick 应用，该应用是作为 Qt SDK 的一部分提供的。 但是，在大多数情况下，您将使用 Qt Creator 中的 Qt Quick 应用向导创建一个 Qt Quick 应用，这将导致一个混合应用，该混合应用由 Qt C++ 应用容器和 QML 文件组成，作为应用在运行时加载和播放的资源。\n\n如果我们查看使用此向导创建的项目中的`main.cpp`，我们将看到以下代码：\n\n```cpp\n#include <QGuiApplication>\n#include <QQmlApplicationEngine>\n\nint main(int argc, char *argv[])\n{\n    QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);\n\n    QGuiApplication app(argc, argv);\n\n    QQmlApplicationEngine engine;\n const QUrl url(QStringLiteral(\"qrc:/main.qml\"));\n QObject::connect(&engine, &QQmlApplicationEngine::objectCreated, &app, \n   [url](QObject *obj, const QUrl &objUrl)\n    {\n        if (!obj && url == objUrl)\n            QCoreApplication::exit(-1);\n    }, Qt::QueuedConnection);\n    engine.load(url);\n\n    return app.exec();\n}\n```\n\n这非常简单：它创建`QQmlApplicationEngine`的实例(继承自`QGuiApplication`的类)，并实例化包含 Qt 快速播放引擎`QQmlEngine`的单个实例的顶级窗口。 其中，`QQmlEngine`类保存一个称为根上下文的对象，它是 QML 环境中所有命名属性的列表。 我们可以访问这个根上下文，并向其中添加任何我们想要的 Qt 对象。\n\n例如，让我们创建一个具有属性的本机 C++ 对象，如下所示：\n\n```cpp\n#ifndef NATIVEOBJECT_H \n#define NATIVEOBJECT_H \n\n#include <QObject> \n\nclass NativeObject : public QObject \n{ \n    Q_OBJECT \n\npublic: \n    explicit NativeObject(QObject *parent = nullptr) : \n    QObject(parent) \n    { \n        m_text = \"Hello world!\"; \n    } \n\n    Q_PROPERTY(QString text READ text) \n    const QString& text() { return m_text; } \n\nprivate: \n    QString m_text; \n\n}; \n\n#endif // NATIVEOBJECT_H \n```\n\n我们可以在应用启动时将其中一个实例化并绑定到 QML 引擎的根上下文。 为此，我们只需创建一个，然后调用`rootContext`的`setContextProperty`方法，传递对象的一个实例和引擎应将其绑定到的名称，如下所示：\n\n```cpp\n#include <QGuiApplication>\n#include <QQmlApplicationEngine>\n#include <QQmlContext> \n\n#include \"nativeobject.h\" \n\nint main(int argc, char *argv[])\n{\n    QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);\n    QGuiApplication app(argc, argv);\n    QQmlApplicationEngine engine;\n\n    NativeObject object; \n engine.rootContext()->setContextProperty(\"object\", \n dynamic_cast<QObject*>(&object)); \n\n    const QUrl url(QStringLiteral(\"qrc:/main.qml\"));\n    QObject::connect(&engine, &QQmlApplicationEngine::objectCreated, \n      &app, [url](QObject *obj, const QUrl &objUrl)\n    {\n        if (!obj && url == objUrl)\n            QCoreApplication::exit(-1);\n    }, Qt::QueuedConnection);\n    engine.load(url);\n\n    return app.exec();\n}\n```\n\n然后，我们可以在 QML 中的任何位置使用`object`名称访问此对象，如下所示：\n\n```cpp\nimport QtQuick 2.12 \nimport QtQuick.Window 2.12\n\nWindow \n{ \n    visible: true \n    height: 360 \n    width: 360 \n\n    Text { \n        anchors.top: parent.top \n        anchors.horizontalCenter: parent.horizontalCenter \n        text: object.text \n    } \n} \n```\n\n在这里，我们对象的`Text`属性被 QML 引擎取消引用，并用于 QML 中文本字段的文本值。\n\n这对于列表模型特别方便；我们可以使用 C++ 中的文件系统或网络资源创建任意复杂的模型，然后将它们指定为`ListView`、`GridView`或`PathView`对象的模型，从而使我们能够在利用 Qt 的 MVC 和灵活的网络体系结构的同时，在 Qt Quick 中创建丰富的用户界面。 我们将在下一节创建自定义模型以在混合多媒体捕获应用中存储图像数据时执行此操作。\n\nFor more information about how Qt Quick binds with C++, see the documentation for the `QQmlContext` class at [https://doc.qt.io/qt-5/qqmlcontext.html](https://doc.qt.io/qt-5/qqmlcontext.html) and [https://doc.qt.io/qt-5/qtquick-modelviewsdata-cppmodels.html](https://doc.qt.io/qt-5/qtquick-modelviewsdata-cppmodels.html).\n\n# 把所有这些放在一起-一个图片库应用\n\n让我们将我们在前几章中讨论的内容应用到一个简单的图片库应用中，就像智能手机上的图片库一样。 我们将在网格中显示系统目录中的图像，让用户轻触即可滚动图像。 下面是我们的应用的外观：\n\n![](img/bc72d150-02f2-45d2-9e38-3d131c69030b.png)\n\n为此，我们需要以下组件：\n\n*   包含要显示图像的路径的模型\n*   负责创建模型的控制器\n*   可以从系统的图像目录加载图像的图像提供程序\n*   QML 用户界面\n\n让我们先来看看应用的 QML：\n\n```cpp\nimport QtQuick 2.12\nimport QtQuick.Window 2.12\n\nWindow {\n    visible: true\n    width: 1080 / 2\n    height: 1920 / 2\n\n    Item {\n        anchors.top: parent.top\n        anchors.left: parent.left\n        anchors.bottom: parent.bottom\n        anchors.right: parent.right\n        clip: true\n```\n\n在前面的代码中，我们创建了应用窗口和一个`Item`对象，该对象充当`GridView`对象的父对象。 之后，我们将定义`GridView`，它从我们定义的源位置加载图像文件，如下所示：\n\n```cpp\n\n        GridView {\n            id: grid\n            anchors.fill: parent;\n            cellHeight: 190\n            cellWidth: 250\n            model: ListModel {}\n            delegate: Image {\n                width: 240; height: 180\n                fillMode: Image.PreserveAspectFit\n                source: \"image://file/\" + path\n            }\n        }\n    }\n```\n\n然后，我们创建一个`Timer`对象，以在`10`毫秒后触发我们的 C++ 函数：\n\n```cpp\n    Timer {\n        interval: 10\n        running: true\n        repeat: false\n        onTriggered: {\n            controller.deferredInit()\n            grid.model = model\n        }\n    }\n}\n```\n\n有一个`GridView`和一个代表，它使用单个`Image`元素来显示库中的每个元素。 通过指定以`image`开头的 URL，我们告诉 Qt Quick 引擎我们希望使用自定义图像提供程序；在本例中，它是我们将在`main`函数中初始化时提供的文件图像提供程序。 网格中的图像将被缩放以适合网格单元格，同时保留图像的纵横比。 网格开始于一个空的列表模型，一旦计时器在启动后触发并指示控制器初始化该模型，我们就会将其替换为一个初始化的列表模型。\n\n计时器启动有点麻烦；原因是我们将使用一个依赖于 Qt 多线程的文件系统类来监控图像目录，并且直到我们进入应用的主循环之后，才会初始化 Qt 多线程系统。 因此，我们启动主循环，初始化 QML，然后初始化应用控制器，应用控制器将初始化模型并从目录中获取图像文件列表。\n\n应用的主入口点创建控制器和模型以及图像提供程序，并将它们绑定到 Qt Quick 引擎，如下所示：\n\n```cpp\n#include <QGuiApplication>\n#include <QQmlApplicationEngine>\n#include <QDir>\n#include <QQmlContext>\n\n#include \"imagegallerycontroller.h\"\n#include \"imagegallerymodel.h\"\n#include \"fileimageprovider.h\"\n\nint main(int argc, char *argv[])\n{\n    QGuiApplication app(argc, argv);\n    QQmlApplicationEngine engine;\n\n    ImageGalleryController *controller = new \n      ImageGalleryController(&engine);\n    ImageGalleryModel *model = controller->model();\n\n    QQmlContext *context = engine.rootContext();\n    context->setContextProperty(\"controller\", controller);\n    context->setContextProperty(\"model\", model);\n    engine.addImageProvider(QLatin1String(\"file\"),\n    new FileImageProvider(QQuickImageProvider::Pixmap));\n```\n\n在初始化自定义 C++ 类(如`ImageGalleryController`和`ImageGalleryModel`)之后(稍后我们将查看它们)，然后启动`main.qml`并运行应用：\n\n```cpp\n    const QUrl url(QStringLiteral(\"qrc:/main.qml\"));\n    QObject::connect(&engine, &QQmlApplicationEngine::objectCreated,\n    &app, [url](QObject *obj, const QUrl &objUrl) {\n        if (!obj && url == objUrl)\n            QCoreApplication::exit(-1);\n    }, Qt::QueuedConnection);\n    engine.load(url);\n\n    return app.exec();\n}\n```\n\n此代码创建控制器的一个实例，从控制器获取指向模型的指针，然后将它们绑定到 Qt Quick 引擎。 接下来，我们向引擎注册我们的自定义图像提供程序的一个实例，说明文件的基本路径引用的任何内容都应该调用我们的图像提供程序，获取像素图。\n\n图像提供程序可以返回像素图或`QImage`实例；像素图的绘制速度稍快一些。 您返回的是您的图像提供程序的函数；您的图像提供程序只需从磁盘加载图像或像素图，并对其进行缩放以适应显示。 以下代码用于执行此操作：\n\n```cpp\n#ifndef FILEIMAGEPROVIDER_H\n#define FILEIMAGEPROVIDER_H\n\n#include <QQuickImageProvider>\n\nclass FileImageProvider : public QQuickImageProvider\n{\npublic:\n    FileImageProvider(QQuickImageProvider::ImageType type);\n    QImage requestImage(const QString& id, QSize* size, const QSize& \n      requestedSize);\n    QPixmap requestPixmap(const QString& id, QSize* size, const QSize& \n      requestedSize);\n};\n\n#endif // FILEIMAGEPROVIDER_H\n```\n\n在声明了头文件中的函数之后，让我们在源文件中定义代码：\n\n```cpp\n#include \"fileimageprovider.h\"\n\nFileImageProvider::FileImageProvider(QQuickImageProvider::ImageType type) :\n  QQuickImageProvider(type)\n{\n}\n\nQImage FileImageProvider::requestImage(const QString& filename, QSize* size, const QSize& requestedSize)\n{\n    QImage image(filename);\n    QImage result;\n    if (requestedSize.isValid()) {\n        result = image.scaled(requestedSize, Qt::KeepAspectRatio);\n    } else {\n        result = image;\n    }\n    *size = result.size();\n    return result;\n}\n```\n\n`requestPixmap`函数与`requestImage`函数相同，但以不同的格式返回数据：\n\n```cpp\nQPixmap FileImageProvider::requestPixmap(const QString& filename, QSize* size, const QSize& requestedSize)\n{\n    QPixmap image(filename);\n    QPixmap result;\n\n    if (requestedSize.isValid()) {\n        result = image.scaled(requestedSize, Qt::KeepAspectRatio);\n    } else {\n        result = image;\n    }\n    *size = result.size();\n    return result;\n}\n```\n\n图像提供程序接口定义了两个方法，您需要为其中至少一个方法提供实现。 当您的方法返回图像时，该接口指定图像的 ID、所需的图像大小以及指向图像实际大小的指针，然后返回一个`QImage`实例或一个像素图。\n\n在本例中，我提供了这两种实现，以便让您了解如何做到这一点，尽管您只需要在映像提供程序中提供一种实现。\n\nFor more information on the interface, see the documentation at [https://doc.qt.io/qt-5/qquickimageprovider.html](https://doc.qt.io/qt-5/qquickimageprovider.html).\n\n我们的实现将所需的图像从磁盘加载为像素图或`QImage`，如果调用方提供了有效的大小，它将在不更改纵横比的情况下调整图像的大小。 缩放之后，我们确定图像的大小，将其赋给 size 参数，然后返回结果图像或像素图。\n\n我们的应用很简单，因此控制器的唯一目的是初始化数据模型；因此，控制器是一个非常简单的类：\n\n```cpp\n#ifndef IMAGEGALLERYCONTROLLER_H\n#define IMAGEGALLERYCONTROLLER_H\n\n#include <QObject>\n\nclass ImageGalleryModel;\nclass QAbstractItemModel;\n\nclass ImageGalleryController : public QObject\n{\n    Q_OBJECT\n\npublic:\n    explicit ImageGalleryController(QObject *parent = 0);\n    ImageGalleryModel* model() const;\n\nsignals:\n\npublic slots:\n    void deferredInit();\n\nprivate:\n    ImageGalleryModel *imageGalleryModel;\n\n};\n\n#endif // IMAGEGALLERYCONTROLLER_H\n```\n\n声明了类头之后，让我们在源文件中定义它的函数：\n\n```cpp\n#include <QDir>\n#include <QDesktopServices>\n#include <QStandardPaths>\n\n#include \"imagegallerycontroller.h\"\n#include \"imagegallerymodel.h\"\n\nImageGalleryController::ImageGalleryController(QObject *parent) :\n    QObject(parent)\n{\n    imageGalleryModel = new ImageGalleryModel();\n}\n\nImageGalleryModel *ImageGalleryController::model() const\n{\n    return imageGalleryModel;\n}\n\nvoid ImageGalleryController::deferredInit()\n{\n    imageGalleryModel->setRootPath(QStandardPaths::standardLocations(\n    QStandardPaths::PicturesLocation)[0]);\n}\n```\n\n这并不奇怪：控制器在构造时创建一个模型，其延迟的初始化将模型的根路径设置为系统图片标准位置中的第一个元素。 (这实际上有一点风险；有些平台可能没有定义这样的目录，但对于本例来说没问题。)\n\n模型是应用的大部分逻辑所在的位置。\n\n首先，我们来看一下模型的界面：\n\n```cpp\n#ifndef IMAGEGALLERYMODEL_H\n#define IMAGEGALLERYMODEL_H\n\n#include <QStandardItemModel>\n\nclass QFileSystemWatcher;\n\nclass ImageGalleryModel : public QStandardItemModel\n{\n  Q_OBJECT\npublic:\n    enum ImageGalleryRoles {\n        PathRole = Qt::UserRole + 1\n    };\n\n    explicit ImageGalleryModel(QObject *parent = nullptr);\n\n    QHash<int, QByteArray> roleNames() const;\n    void setRootPath(const QString& path);\nsignals:\n\npublic slots:\n    void onDirectoryChanged(const QString& path);\n\nprivate:\n    QString path;\n    QFileSystemWatcher *watcher;\n};\n\n#endif // IMAGEGALLERYMODEL_H\n```\n\n模型中的每个元素都可以存储由不同角色访问的不同数据片段；我们定义了一个角色`PathRole`，它将包含到单个图像的绝对路径。 为此，我们需要做两件事：为角色定义一个数字值(通过`ImageGalleryRoles`枚举)，然后提供一个`roleNames`方法，该方法返回由角色值索引的角色名的散列。 Qt Quick 使用返回的散列中的角色名称来确定如何访问数据模型的单个值中的各个角色：在散列中查找角色名称，然后获取角色值，最后调用具有所需角色的模型条目上的数据。\n\n模型类是应用中最大的类，尽管即使在这里，我们也使用`QStandardItemModel`来实际执行模型的大部分工作。 具体实现如下：\n\n```cpp\n#include \"imagegallerymodel.h\"\n#include <QDir>\n#include <QFile>\n#include <QFileSystemWatcher>\n\nImageGalleryModel::ImageGalleryModel(QObject *parent) :\n  QStandardItemModel(parent)\n{\n    watcher = new QFileSystemWatcher(this);\n    connect(watcher, SIGNAL(directoryChanged(QString)),\n        this, SLOT(onDirectoryChanged(QString)));\n}\n\nQHash<int, QByteArray> ImageGalleryModel::roleNames() const\n{\n    QHash<int, QByteArray> roles;\n    roles[PathRole] = \"path\";\n    return roles;\n}\n```\n\n然后我们声明`setRootPath`函数，该函数允许我们设置图片库的目录：\n\n```cpp\nvoid ImageGalleryModel::setRootPath(const QString &p)\n{\n    this->clear();\n\n    if (path != \"\") {\n        watcher->removePath(path);\n    }\n\n    path = p;\n    watcher->addPath(path);\n\n    // Sync the model\n    onDirectoryChanged(path);\n}\n```\n\n然后我们实现`onDirectoryChanged`槽函数，该函数有点长：\n\n```cpp\nvoid ImageGalleryModel::onDirectoryChanged(\n    const QString &path)\n{\n    QStringList nameFilters;\n    nameFilters << \"*.png\" << \"*.jpg\";\n    QDir directory(path);\n    directory.setNameFilters(nameFilters);\n    QStringList files = directory.entryList();\n\n    QHash<QString, int> fileIndexes;\n\n    // Sync the model with the list.\n\n    // Now delete anything in the model not on the filesystem\n    for(int i = 0; i < rowCount(); i++) {\n        QString absolutePath = data(index(i, 0), PathRole).toString();\n        QString name = directory.relativeFilePath(absolutePath);\n        if (!files.contains(name)) {\n            removeRow(i);\n            i--;\n            continue;\n        }\n        fileIndexes[absolutePath] = i;\n    }\n```\n\n我们继续实现`onDirectoryChanged`函数。 我们遍历位于源目录中的所有文件，并获得其绝对文件路径：\n\n```cpp\n    // Add anything to the model that's on disk\n    // and not in the model\n    foreach(const QString &file, files) {\n        QString absolutePath = directory.absoluteFilePath(file);\n        if (!fileIndexes.contains(absolutePath)) {\n            QStandardItem *item = new QStandardItem();\n            item->setData(absolutePath, PathRole);\n            appendRow(item);\n        }\n    }\n}\n```\n\n让我们一个方法地看这个方法：\n\n*   构造函数创建一个`QFileSystemWatcher`的实例，该实例在后台监视您给出的文件或目录更改的路径，分别为更改的目录或文件发出`directoryChanged`和`fileChanged`信号。 我们将使用`setPath`方法中所需的路径来初始化此观察器。\n*   `roleNames`方法建立数据角色的名称、路径和我们用来从模型获取此数据的枚举值之间的关系。 如前所述，Qt 在角色枚举及其文本名称的哈希表中期望这种关系。 Qt Quick 将使用散列中的值来确定适当的角色来获取数据，因此我可以简单地引用 QML 中的字段路径，并且在幕后，Qt Quick 引擎从相应的角色值获取值。\n*   `setRootPath`方法必须首先重置模型的内容，然后如果观察器正在监视某个目录，则在监视您经过的目录之前停止监视旧目录。 一旦代码初始化了观察器，它就会通过调用观察器所调用的相同槽来强制模型更新(还记得构造函数中的`connect`实例吗？)。 每当目录更改时。\n*   `onDirectoryChanged`槽获取它所传递的目录中的 PNG 和 JPEG 文件列表；然后，它通过首先删除模型中不在磁盘上的所有内容，然后将磁盘上的所有内容添加到模型中来同步模型。 当我们第一次遍历模型以处理任何删除操作时，我们为模型中的文件构建了一个哈希表；稍后，当我们向模型中添加项时，我们可以只检查哈希表中的条目。 这比顺序搜索模型或模型内容列表要快一点，因为哈希表搜索通常比顺序搜索快。\n\n我们已经成功地使用 Qt Quick 创建了图片库。 让我们继续下一节，了解 Qt Quick Control2 有哪些新功能。\n\n# 介绍新的 Qt 快速控件 2\n\n自从 Qt 5.6 以来，Qt Quick Controls 2 已经发布，取代了版本 1，后者存在性能问题和技术问题，这导致了它的弃用。 有了 Qt Quick Controls 2，一切都是从零开始重建的，并考虑到了性能。 Qt Quick Controls 2 不仅可以在 PC 上流畅运行，还可以在移动设备和嵌入式设备上流畅运行。\n\n要在项目中使用 Qt Quick Controls 2，请执行以下步骤：\n\n1.  将以下模块添加到您的 QML 文件中：\n\n```cpp\nimport QtQuick.Controls 2.12\n```\n\n2.  如果您需要在 C++ 代码中使用 Qt Quick Control 2，则必须首先将`quickcontrols2`模块添加到`qmake`项目文件中：\n\n```cpp\nQT += quickcontrols2\n```\n\n3.  之后，您可以在 C++ 源代码中包含`QtQuickControls2`头：\n\n```cpp\n#include <QtQuickControls2>\n```\n\n以下是 Qt Quick Control 2 中提供的一些 QML 类型：\n\n*   按钮-按钮允许用户点击或按下按钮并触发事件。\n*   复选框-可以打开或关闭复选框以表示启用或禁用选项。\n*   组合框-组合框是一个向下列出供用户选择的选项的选择框。\n*   TextField--文本字段是允许用户键入一行文本的最简单的文本输入。\n*   TextArea-与文本字段不同，文本区域是多行文本输入字段，也支持文本修饰。\n*   菜单栏-菜单栏是位于应用窗口边缘的下拉菜单数组。\n*   圆形按钮-圆形按钮与按钮相同，不同之处在于它支持圆角。\n*   滑块-滑块允许用户通过沿轨迹移动手柄来操纵值。\n*   SpinBox-数字调整框是一个整数选择框，带有向上和向下指示器按钮，用于操作整数值。\n\n比这里列出的要多得多。 有关 qml 类型的完整列表，请访问[https://doc.qt.io/archives/qt-5.11/qtquick-controls2-qmlmodule.html](https://doc.qt.io/archives/qt-5.11/qtquick-controls2-qmlmodule.html)。\n\n让我们看一下在我们的应用中应用 Qt Quick Control 2 的一些示例代码：\n\n```cpp\nimport QtQuick 2.13\nimport QtQuick.Window 2.13\nimport QtQuick.Controls 2.12\n\nApplicationWindow {\n    visible: true\n    width: 200\n    height: 200\n    title: qsTr(\"Hello World\")\n\n    Button {\n        text: \"Push Me\"\n        anchors.centerIn: parent\n    }\n}\n```\n\n在前面的代码中，我们将`QtQuick.Controls`模块导入到 QML 文件中，然后创建了一个标签为“Push Me”的`Button`对象。 结果如下所示：\n\n![](img/1a44026b-0bd1-4680-8d90-fdd3d0fc0710.png)\n\nQt Quick Control 2 自带`Default Style`，主要由灰色和黑色组成，如上图截图所示。\n\n您还可以选择其他几种样式来使您的应用与您正在运行的操作系统相匹配。 其他一些选项如下所示：\n\n*   **Fusion Style**：Fusion Style`Fusion Style`是一种看起来非常类似桌面操作系统原生 Widget 的风格，适合用于桌面应用。\n*   **IMAGINE STYLE：**`Imagine Style`依靠图像资产来显示小部件，这些小部件适用于需要更多定制外观的应用。\n*   **Material Style**：`Material Style`是专门为让您的应用看起来像原生 Android 应用而设计的，因为它完全遵循 Google 官方发布的材料设计指南。\n*   **通用样式**：`Universal Style`使您的应用看起来像是本机通用 Windows 平台应用，因为它遵循微软通用设计指南。\n\n有关此处列出的所有样式的更多信息，请查看[https://doc.qt.io/archives/qt-5.11/qtquickcontrols2-styles.html](https://doc.qt.io/archives/qt-5.11/qtquickcontrols2-styles.html)。\n\n要将样式应用于您的应用，需要执行以下几个步骤：\n\n1.  您需要创建一个名为`qtquickcontrols2.conf`的文件，并将其添加到您的项目资源中。 然后，将以下代码添加到文件中：\n\n```cpp\n[Controls]\nStyle=Material\n\n[Material]\nTheme=Light\nAccent=Teal\nPrimary=BlueGrey\n```\n\n2.  在`[Controls]`下，我们将 Qt Quick Control2 样式设置为`Material`。\n3.  之后，在`[Material]`下，我们设置材质样式的颜色。 如果我们再次运行我们的程序，您应该看到按钮已更改：\n\n![](img/80205e23-82e3-4fd9-bfae-7332291fcc52.png)\n\n我们的应用现在看起来像是在 Android 操作系统上运行！ 每种样式都有其独特的配置，因此您必须查看各自的文档才能了解更多信息。\n\nTo get information about various styles, please visit the following:\n\n*   有关`Fusion Style`，请查看[https://doc.qt.io/archives/qt-5.11/qtquickcontrols2-fusion.html](https://doc.qt.io/archives/qt-5.11/qtquickcontrols2-fusion.html)。\n*   对于`Imagine Style`，请查看[https://doc.qt.io/archives/qt-5.11/qtquickcontrols2-imagine.html](https://doc.qt.io/archives/qt-5.11/qtquickcontrols2-imagine.html)。\n*   有关`Material Style`，请查看[https://doc.qt.io/archives/qt-5.11/qtquickcontrols2-material.html](https://doc.qt.io/archives/qt-5.11/qtquickcontrols2-material.html)。\n*   最后，查看`Universal Style`的[https://doc.qt.io/archives/qt-5.11/qtquickcontrols2-universal.html](https://doc.qt.io/archives/qt-5.11/qtquickcontrols2-universal.html)。\n\n除了在`qtquickcontrols2.conf`文件中设置颜色之外，还可以在 QML 文件中覆盖它。 如果您希望特定的 QML 文件具有与其他文件不同的样式，这将非常有用。 下面的代码显示了这一点：\n\n```cpp\nApplicationWindow {\n    visible: true\n    width: 200\n    height: 200\n    title: qsTr(\"Hello World\")\n\n    Material.theme: Material.Dark\n    Material.accent: Material.Green\n\n    Button {\n        text: \"Push Me\"\n        anchors.centerIn: parent\n    }\n}\n```\n\n前面的代码产生以下结果：\n\n![](img/293152f3-50e4-436f-907c-f18f68f7c8da.png)\n\n您还可以覆盖每个单独 QML 对象的样式，如下所示：\n\n```cpp\nApplicationWindow {\n    visible: true\n    width: 200\n    height: 200\n    title: qsTr(\"Hello World\")\n\n    Material.theme: Material.Dark\n    Material.accent: Material.Green\n\n    Button {\n        text: \"Push Me\"\n        anchors.centerIn: parent\n        highlighted: true\n        Material.accent: Material.Blue\n        Material.background: Material.Orange\n        Material.foreground: Material.Pink\n    }\n}\n```\n\n由于按钮现在有自己的`Material.accent`属性，它将覆盖我们在顶部设置的属性。 该代码产生以下结果：\n\n![](img/be7e252c-3f71-411e-839b-13d48fe07028.png)\n\n最后，Qt 还提供了一个 C++ 方法，用于通过`QQuickStyle`类更改 Qt Quick Control2 样式。 您可以调用`QQuickStyle`下的`setStyle`函数来更改样式：\n\n```cpp\nQQuickStyle::setStyle(\"Material\");\n```\n\n我们已经了解了 Qt Quick Control2 的一些功能以及如何更改其外观。 在下一节中，我们将研究 SCXML 的新图形编辑器，以及如何使用它创建一个简单的状态机。\n\n# 了解 SCXML 的新图形编辑器\n\nQt 在 5.7 中引入了 SCXML 这一新特性，作为为应用构建复杂状态机的符号格式。 状态机是程序或程序的小部件根据您为其定义的当前状态更改其属性的一种机制。 我们已经看到当鼠标悬停在`Push Button`上或被用户按下时，`Push Button`如何改变其外观。 这是`Push Button`的不同状态，其行为由状态机确定和执行。\n\n使用 SCXML，您可以为程序定义更复杂的状态机，并将其保存为人类可读的 SCXML 文件格式，以便 Qt 进行解析和处理。 然后，Qt 将根据 SCXML 文件的内容生成 C++ 类，以驱动您先前定义的状态机。 Qt 还提供了一个图形编辑器，使您可以轻松定义状态机，而无需手动编写 SCXML 文件的内容。 编辑器如下所示：\n\n![](img/1f9369d4-bf08-4f60-8a15-e4b750dd7361.png)\n\n编辑器由四个不同部分组成：\n\n*   通用状态面板(A)：这是您挑选和选择要添加到状态编辑器的状态类型的位置。\n*   状态编辑器(B)：这是定义状态机的地方。 您可以从此处的 Common States 面板拖放不同类型的状态，并将它们链接在一起以形成状态转换。\n*   结构面板(C)：这里的面板以树列表的形式显示您的状态机的结构。 您放置在状态编辑器中的所有状态及其层次关系都将在此处显示。\n*   属性面板(D)：此窗口显示所选状态的属性，您可以根据需要查看和编辑这些属性。\n\n要创建 SCXML 文件，请执行以下操作：\n\n1.  转到文件|新建文件或项目。\n2.  然后，选择文件和类|建模下的状态图选项，然后按 Choose...(选择...)。 纽扣。\n3.  一旦您创建了该文件，Qt Creator 将使用 SCXML 编辑器自动将其打开。 您将看到一个空的 SCXML 文件，就像前面的屏幕截图所示。\n4.  您可以通过将`Initial`节点拖动到状态编辑器来启动状态机，从而开始创建您的第一个状态机。\n5.  之后，您可以将一些`State`节点拖到状态编辑器中，并通过首先选择其中一个节点并单击箭头图标将它们链接在一起。 然后，无论您何时移动，都会有一个虚线箭头跟随您的光标。\n6.  左键单击状态编辑器以创建断点以更改箭头的方向，或右键单击其他节点之一以建立与您已选择的上一个节点的过渡。当`State`节点与多个其他节点链接时，它会创建一个供状态机选择的选项。 状态机将通过遵循您稍后在代码中设置的条件来选择适当的路径。 现在，您只需定义状态机可以使用的状态。 下图显示了 SCXML 编辑器内状态机的外观示例：\n\n![](img/9934f767-604a-4efc-950b-3d465c6fd61c.png)\n\n上图显示了\nSCXML 部分的新图形编辑器，我们看到，从顶部开始，有一个`Initial`节点，它表示程序的初始状态，然后它将向下移动到名为 State_1 的`State`节点。从那里开始，它可能会转到不同的状态，具体取决于您在代码中设置的条件。 箭头指示国家可以移动的方向。 最后，状态最终将在第二`Final`节点结束，在该节点，状态机将停止其操作。\n\n7.  最后，放置一个`Final`节点来定义状态机可以达到的最终状态。 一旦达到此状态，状态机就完成了任务并停止运行。\n\n除此之外，如果满足您的需要，还可以使用`Parallel`节点。 与一次只转到一个其他节点的`State`节点不同，`Parallel`节点可以同时移动到多个其他状态，因此得名。\n\n`History`节点比其他节点复杂得多。 它表示以前保存的历史状态。 您可以使用此节点从未直接链接的遥远状态返回到以前的状态。 要将状态保存到历史堆栈，请创建一个`History`节点作为`State`节点的子节点。 当状态机在运行时检测到`History`节点的存在时，它将自动记录当前状态。 在转换期间，只要状态机到达`History`节点，它就会返回到此状态。 下图显示了如何使用`History`节点的示例：\n\n![](img/6e60cf02-cfeb-46b5-bf8d-322e8fd3856f.png)\n\n从上图中我们可以看到，当通过 Transition_20 从 State_1 状态移动到`History`状态时，您的程序的状态将返回到 State_5，这是另一个`History`节点位于 State_1 之前的位置。如果您的状态机中有多个`History`状态，则它将挑选它偶然发现的最后一个状态。\n\n除了将`History`节点添加到`State`节点之外，还可以将`State`节点添加到另一个`State`节点以建立父子关系。 像这样的递归状态会增加状态机的复杂性，使用这种方法时应该格外小心。\n\n对于每个`State`节点，可以对其应用其他元素，称为`Executable Content`。 以下是可以添加到您的州的`Executable Content`元素的列表：\n\n*   `raise`：引发事件。\n*   `send`：与外部实体沟通。\n*   `script`：运行脚本并对数据模型中的值执行某些操作。\n*   `assign`：在数据模型中添加或修改值。\n*   `cancel`：取消动作执行。\n*   `log`：将信息记录到日志中。\n*   `if`：根据条件执行操作。\n*   `foreach`：遍历一组值并对每个值执行操作。\n\n您可以将这些元素添加到状态，方法是右键单击其节点，然后从弹出菜单上的`onentry`或`onexit`选项中选择前述元素之一。 这些`Executable Content`元素中的大多数都是用于在进入或退出该州时操作该州的数据模型的方法。 因此，只能将这些元素添加到分配了`datamodel`的`State`节点或`Parallel`节点。 如果尚未执行此操作，请右键单击要应用的`State`节点，然后选择`datamodel`。\n\n要将`data`添加到`datamodel`，请在结构面板中右键单击它，然后在弹出菜单中选择`data`。 现在，您可以将数据分配给`datamodel`，并通过我们前面提到的`Executable Content`元素对其进行操作。\n\n让我们自己试一试吧：\n\n1.  首先，创建一个新的 Qt Quick 项目，并将`scxml`模块添加到我们的项目中：\n\n```cpp\nQT += quick scxml\n```\n\n2.  然后，按如下方式设置我们的 GUI：\n\n```cpp\nimport QtQuick 2.12\nimport QtQuick.Window 2.12\n\nWindow {\n    id: window\n    visible: true\n    width: 640\n    height: 480\n    title: qsTr(\"State Machine\")\n\n    Rectangle {\n        id: rectangle\n        width: 200\n        height: 200\n        color: \"red\"\n        anchors.verticalCenter: parent.verticalCenter\n        anchors.horizontalCenter: parent.horizontalCenter\n    }\n}\n```\n\n3.  这里没有什么花哨的东西，只是一个位于窗口中央的简单矩形：\n\n![](img/7c0ce6fb-6134-4655-93d8-f76f23e5e256.png)\n\n4.  之后，创建一个 SCXML 文件并将其命名为`statemachine.scxml`。\n5.  创建文件后，请确保也将此文件添加到项目资源中，因为我们稍后将在 QML 代码中使用它。\n6.  之后，使用 Qt Creator 打开该文件，并创建一个简单的状态机，如下所示：\n\n![](img/6e539835-20f5-4892-af79-a1570a7346fe.png)\n\n从上图可以看出，前面的状态机由三种不同的状态组成：**红色**、**绿色**和**蓝色**。 我们将使用这些状态来更改前面在 QML 代码中定义的矩形的颜色。 您可以从状态机中看到的其他名称(如`start`、`goGreen`、`goBlue`和`goRed`)是转换的名称。\n\n7.  完成后，右键单击每个`State`节点(**红色**、**绿色**和**蓝色**)，然后从弹出菜单中选择 onentry|Send。\n\n8.  将`red`的`send`可执行内容的`event`属性设置为`goGreen`，并将其延迟设置为`2s`。 这将允许状态机在进入`red`状态两秒后自动触发`goGreen`转换。 下面的屏幕截图显示了在属性窗口中设置值的位置：\n\n![](img/8c2a55ee-8846-46b3-ac1d-081d7a7f355c.png)\n\n对`green`和`blue`状态重复上述步骤，但将`event`属性分别更改为`goBlue`和`goRed`。 这意味着状态机将继续循环运行，直到我们停止程序。\n\n9.  接下来，打开我们的`main.qml`文件并将`QtScxml`模块导入到我们的 QML 代码中：\n\n```cpp\nimport QtScxml 5.8\n```\n\n10.  之后，在我们的窗口对象下添加以下代码：\n\n```cpp\nStateMachineLoader {\n    source: \"qrc:///statemachine.scxml\"\n    stateMachine.onReachedStableState: {\n        if (stateMachine.red)\n            rectangle.color = \"red\"\n        else if (stateMachine.green)\n            rectangle.color = \"green\"\n        else if (stateMachine.blue)\n            rectangle.color = \"blue\"\n    }\n}\n```\n\n我们创建了一个`StateMachineLoader`对象，该对象用于加载前面创建的 SCXML 文件，并自动为我们生成状态机代码。 但是，我们必须自己编写逻辑，这可以在`onReachedStableState`槽函数下看到。 当我们的状态机成功更改其状态时，将触发此函数。 然后，我们检查每个状态是否为当前状态(如果是当前状态，则返回 TRUE)，并相应地更改矩形形状的颜色。\n\n如果我们现在运行代码，我们将看到矩形每两秒更改一次颜色。 就是这样--我们已经了解了如何利用 Qt 提供的新的 SCXML 编辑器，并为我们的应用创建一个简单的状态机。\n\n# 简略的 / 概括的 / 简易判罪的 / 简易的\n\n在本章中，您快速了解了 Qt Quick，这是 Qt 用于应用开发的声明性框架。 您了解了 Qt Quick 提供的作为应用开发基础的基本可见项目，以及如何使用 Qt Quick 的布局系统定位项目，如何使用 Web 引擎组件创建简单的 Web 浏览器，以及如何使用 Qt Quick 的列表视图创建简单的列表。\n\n除此之外，您还了解了 Qt Quick 对动画和过渡的支持。 我们了解了如何利用状态构造根据对象的当前状态更改对象的属性，还了解了如何利用最新的 SCXML 编辑器创建更复杂的状态机。\n\n最后，我们了解了如何将 Qt Quick 和 C++ 链接起来，为您提供了 Qt 开发中的两全其美。 通过遵循本章中的示例，我们学习了如何从头开始创建图片库！\n\n在下一章中，我们将了解 Qt Quick 对多媒体录制和回放的支持。 敬请关注！"
  },
  {
    "path": "docs/app-dev-qt-creator/08.md",
    "content": "# 八、使用 Qt Quick 实现多媒体\n\n今天的应用越来越多地使用多媒体来增强其对用户的吸引力。 音效是大多数用户界面的关键部分，许多应用都包含视频教程或其他视频内容。 有些应用甚至使用许多设备上提供的摄像头；尤其是移动应用，大多数移动设备至少有一个摄像头(如果不是两个或更多摄像头的话)。\n\n在本章中，我们将看看 Qt Quick 对多媒体的支持。 我们将从概述可能的功能开始，这样您就可以理解使用 Qt 提供的与平台无关的多媒体支持可以构建什么，不可以构建什么。 接下来，我们将详细介绍提供音频和视频播放访问的 Qt Quick 组件，以及如何使用摄像头(如果支持)。\n\n在本章中，我们将介绍以下主题：\n\n*   在 Qt 中实现多媒体\n*   播放音频剪辑和音效\n*   播放视频剪辑\n*   访问摄像机\n\n# 技术要求\n\n本章的技术要求包括 Qt 5.13.1 MinGW 64 位、Qt Creator 4.10.0 和 Windows 10。\n\n本章的 GitHub 链接可在此处找到：\n\n[https：//github.com/PacktPublishing/Application-Development-with-Qt-Creator-Third-Edition/tree/master/Chapter08](https://github.com/PacktPublishing/Application-Development-with-Qt-Creator-Third-Edition/tree/master/Chapter08).\n\n# 在 Qt 中实现多媒体\n\n长期以来，Qt 通过包含其音频库，在它支持的 C++ 平台上提供了一些对多媒体的支持。 在 Qt 5.0 及更高版本中，Qt Quick 提供了几个对象来与 Qt 提供的本地支持和 Qt 提供的底层平台进行交互。 使用这些 QML 组件，您可以执行以下操作：\n\n*   在背景中播放声音剪辑和短音效\n*   播放视频内容\n*   显示相机取景器\n*   从摄像机捕获摄像机内容\n\nQt 实际支持的内容取决于目标平台；例如，如果硬件没有摄像头，您就无法显示摄像头取景器或拍照。 在实践中，支持的级别也各不相同；例如，在我撰写本文时，Windows 上的多媒体支持非常差。 此外，Qt 可以播放的音频和视频的实际格式取决于随 Qt 安装的库，而库本身又取决于目标平台。 Linux 等平台可能需要额外的库才能完全支持许多音频和视频文件使用的视听编码器/解码器(编解码器)。\n\n要使用我们将在本章中讨论的任何多媒体类型，您的 QML 实例必须导入`QtMultimedia`模块，如下所示：\n\n```cpp\nimport QtMultimedia 5.12 \n```\n\nIn this chapter, we'll focus on the Qt Quick multimedia interfaces. If you're interested in using the lower-level C++ APIs, see the Qt Multimedia documentation at [https://doc.qt.io/qt-5/multimediaoverview.html](https://doc.qt.io/qt-5/multimediaoverview.html).\n\n我们已经了解了 Qt 中多媒体模块的功能和用法。 让我们继续下一节，了解如何支持此模块并使用它播放音频剪辑。\n\n# 播放音频剪辑和音效\n\nQt Quick 提供了`SoundEffect`类型，可以在最小延迟的情况下播放短小的音效。 这对于按钮点击声音、虚拟键盘声音和提示音等内容尤其有用，因为它们是丰富且引人入胜的多媒体体验的一部分。 使用它很简单；您可以为该类型提供一个`source`字段，并调用它的`play`方法来开始播放，如下所示：\n\n```cpp\nimport QtQuick 2.12\nimport QtQuick.Window 2.12\nimport QtMultimedia 5.12\n\nWindow {\n    visible: true\n    width: 320\n    height: 240\n    title: qsTr(\"Play Sound\")\n\n    Text {\n        text: \"Click Me!\";\n        font.pointSize: 24;\n        width: 150; height: 50;\n\n        SoundEffect {\n            id: playSound\n            source: \"soundeffect.wav\"\n        }\n        MouseArea {\n            id: playArea\n            anchors.fill: parent\n            onPressed: { playSound.play() }\n        }\n    }\n}\n```\n\n前面的代码产生以下结果：\n\n![](img/033f4643-e2b9-46f5-92ee-2e2047aae201.png)\n\n在这里，当您单击鼠标区域时，`SoundEffect`将播放存储在应用资源中的`soundeffect.wav`文件的内容。 您可以通过调用音效的`stop`方法来停止音效，否则它会一直播放到完成。\n\n`SoundEffect`类型有一些更改音效播放方式的附加字段：\n\n*   `loops`字段指示调用`play`后声音在回放中循环的次数。\n*   `loopsRemaining`字段表示在播放声音时还剩下多少次播放循环。\n*   播放声音时，`playing`字段为真。\n*   `volume`字段表示音量，范围从`0.0`(无音频)到`1.0`(最大音量)。\n*   `status`字段是指示播放状态的枚举。 它可以具有下列值之一：\n    *   `SoundEffect.Null`：未设置声音。\n    *   `SoundEffect.Loading`：正在加载声音。\n    *   `SoundEffect.Ready`：声音已准备好播放。\n    *   `SoundEffect.Error`：加载或播放期间出错。\n\n对于较长的音频位，最好使用`Audio`类型。 `Audio`类型的用法与此类似；您可以给它一个源，指示音频应该来自哪里，然后调用它的`play`方法开始回放。 您可以通过调用其`pause`方法暂停播放，通过调用其`stop`方法停止播放，或者通过调用`seek`并传递 Time to Seek 中的偏移量来查找音频中的时间偏移量(假设音频源支持查找；某些源，如 Web 流，不支持查找)。 您可以按如下方式使用它：\n\n```cpp\nimport QtQuick 2.12\nimport QtQuick.Window 2.12\nimport QtMultimedia 5.12\n\nWindow {\n    visible: true\n    width: 320\n    height: 240\n    title: qsTr(\"Play Sound\")\n\n    Text {\n        text: \"Click Me!\";\n        font.pointSize: 24;\n        width: 150; height: 50;\n\n        Audio {\n            id: playMusic\n            source: \"music.wav\"\n        }\n        MouseArea {\n            id: playArea\n            anchors.fill: parent\n            onPressed: { playMusic.play() }\n        }\n    }\n}\n```\n\n`Audio`类型包括以下影响播放的属性：\n\n*   `autoLoad`：默认为`true`，并强制元素在初始化时开始加载媒体。\n*   `autoPlay`：默认为`false`，但是当它是`true`时，一旦元素在初始化时加载，它就开始播放。\n*   `bufferProgress`：这是`0.0`到`1.0`范围内的实数，表示播放缓冲区已满的程度。\n*   `duration`：这表示音频剪辑的持续时间。\n*   `error`和`errorString`：它们包含回放失败时的错误信息。\n*   `hasAudio`和`hasVideo`：它们分别指示剪辑是否有音频和视频。\n*   `loops`：表示音频应该播放的次数。\n*   `muted`：如果用户已将音频静音，则为`true`。\n*   `position`：表示音频播放中的位置。\n*   `seekable`：如果音频流支持查找，则此属性为`true`。\n*   `volume`：表示播放音量为从`0.0`(无音频)到`1.0`(满音量)的实数。\n\n`Audio`类型还有一个`metaData`属性，该属性包括有关正在播放的音频的信息(如果在流中编码的话)。 它包括`albumArtist`、`albumTitle`、`audioBitRate`、`category`、`comment`等字段。\n\n与`Audio`类型类似的是`MediaPlayer`类型，它既支持音频播放，也支持视频播放(我们将在下一节看到)。 对于音频播放，其用法与`Audio`类型相同。\n\n我们已经学习了如何使用`SoundEffect`对象播放音频剪辑和音效。 让我们进入下一节，学习如何播放视频剪辑！\n\n# 播放视频剪辑\n\n播放视频和播放音频一样简单；有一个`Video`类型支持播放视频并在显示器上显示视频。 这是一个视频播放器，当你点击它时，它会播放视频，当你按下空格键时，它会暂停并重新开始播放：\n\n```cpp\nimport QtQuick 2.12\nimport QtQuick.Window 2.12\nimport QtMultimedia 5.12\n\nWindow {\n    visible: true\n    width: 800\n    height: 600\n    title: qsTr(\"Play Video\")\n\n    Video {\n        id: video\n        width : 800\n        height : 600\n        source: \"video.avi\"\n\n        MouseArea {\n            anchors.fill: parent\n            onClicked: {\n                video.play()\n            }\n        }\n\n        focus: true\n        Keys.onSpacePressed:\n            video.playbackState == MediaPlayer.PlayingState ?\n                video.pause() :\n                video.play()\n    }\n}\n```\n\n前面的代码产生以下结果：\n\n![](img/d8d0475a-08f0-4286-ae06-5ef79cfd874d.png)\n\n`Keys`类型为按下的各种键发出信号；在这里，我们将`spacePressed`信号绑定到暂停并播放视频的脚本。\n\n除了没有`metaData`属性外，`Video`的大多数属性与`Audio`相同。 它是`Item`的子类，因此通常的定位属性(如`anchors`、`x`、`y`、`width`和`height`)可用于将项放置在其父项中。 请注意，由于性能原因，`Video`实例上的所有转换可能并不都可用；例如，通常不能自由旋转一个转换。\n\n您也可以使用`MediaPlayer`实例和`VideoOutput`实例播放视频内容。 与`Video`一样，`VideoOutput`类型也是`Item`的子类，本质上是与`MediaPlayer`实例关联的视频编解码器呈现视频的画布。 您可以通过在其`source`属性中指定一个`MediaPlayer`实例来使用它，如下所示：\n\n```cpp\nimport QtQuick 2.12\nimport QtQuick.Window 2.12\nimport QtMultimedia 5.12\n\nWindow {\n    visible: true\n    width: 800\n    height: 600\n    title: qsTr(\"Play Video\")\n\n    Rectangle {\n width: 800\n height: 600\n color: \"black\"\n\n MediaPlayer {\n id: player\n source: \"video.avi\"\n autoPlay: true\n }\n\n VideoOutput {\n id: videoOutput\n source: player\n anchors.fill: parent\n }\n }\n}\n```\n\n前面的代码会产生以下结果：\n\n![](img/78760d9c-72b2-4c3e-acac-218cab0c2d47.png)\n\n在这里，`MediaPlayer`实例加载后将立即播放`video.avi`，视频将出现在`VideoOutput`项中，该项的大小调整为填充父矩形。 通常，您只想使用`Video`实例，除非您需要有多个播放窗口，或者显示相机取景器，这将在下一步讨论。\n\n由于视频编解码器的性质，虽然`VideoOutput`是`Item`的子类，但并非所有变换都受支持；例如，您不能旋转视频播放器，也不能将项目放在其上面并期望它绘制子对象。 当您想到当今许多直接在主机系统的图形硬件上运行的编解码器时，这是有意义的。\n\n`VideoOutput`只有几个属性。 这些资料如下：\n\n*   `autoOrientation`：当`true`时，它使用屏幕方向来定位视频。\n*   `contentRect`：这表示应该在`VideoOutput`项中呈现视频的内容矩形。\n*   `fillMode`：这可以是渲染到内容矩形时的`Stretch`选项之一(表示视频是否应该拉伸)、`PreserveToFit`选项(保留其纵横比)或`PreserveAspectCrop`选项(通过裁剪图像来保留纵横比)。 (默认情况下，保留纵横比并使视频适合矩形。)\n*   `orientation`：这使您可以将视频的方向设置为 90 度增量。 当使用`VideoOutput`类作为摄影机取景器时，这是最有用的，我们将在下一节中讨论这一点。\n*   `sourceRect`：这指定应该从哪个源矩形考虑渲染视频。\n\n我们已经学习了如何使用 Qt Quick 及其多媒体模块在 Qt 中轻松播放视频剪辑。 要了解如何访问您的网络摄像头，让我们继续下一节。\n\n# 访问摄像机\n\n要在硬件和 Qt 多媒体支持时访问相机，请使用`Camera`类型及其关联类型来控制相机的拍摄行为、曝光、闪光灯、对焦和图像处理设置。 使用以下代码可以简单地使用相机显示取景器：\n\n```cpp\nimport QtQuick 2.12\nimport QtQuick.Window 2.12\nimport QtMultimedia 5.12\n\nWindow {\n    visible: true\n    width: 640\n    height: 480\n    title: qsTr(\"Webcam\")\n\n    Item {\n        width: 640\n        height: 480\n\n        Camera {\n            id: camera\n        }\n\n        VideoOutput {\n            source: camera\n            anchors.fill: parent\n        }\n    }\n}\n```\n\n前面的代码会产生以下结果：\n\n![](img/f67e8d36-243d-4132-b6d7-02083f9ef57e.png)\n\n简而言之，`Camera`类型就像`MediaPlayer`实例一样，充当视频的源。\n\n`Camera`类型提供了一些属性来控制其行为。 这些建议如下：\n\n*   `imageCapture`：这是`CameraCapture`的一个实例，它定义相机应该如何捕捉图像。\n*   `videoRecording`：这是`CameraRecorder`的一个实例，它定义了摄像机应该如何捕捉视频。\n*   `exposure`：这是`CameraExposure`的一个实例，它控制相机曝光模式的各种选项。\n*   `focus`：这是`CameraFocus`的一个实例，它控制自动和手动对焦行为。\n*   `flash`：这是控制相机闪光灯的`CameraFlash`实例。\n*   `imageProcessing`：这是`CameraImageProcessing`的一个实例，它控制实时图像处理流水线选项，如白平衡、饱和度和锐化。\n\n与这些字段关联的类型不能直接实例化。\n\n要让相机拍照，请指定`imageCapture`属性并调用其`capture`方法，如下所示：\n\n```cpp\nimport QtQuick 2.12\nimport QtQuick.Window 2.12\nimport QtMultimedia 5.12\n\nWindow {\n    visible: true\n    width: 640\n    height: 480\n    title: qsTr(\"Webcam\")\n\n    Item {\n        width: 640\n        height: 360\n\n        Camera {\n            id: camera\n\n            imageCapture {\n                onImageCaptured: {\n                    // Show the preview in an Image\n                    photoPreview.source = preview\n                }\n            }\n        }\n\n        VideoOutput {\n            source: camera\n            focus : visible // to receive focus and capture key events\n            width: 320\n            height: 180\n            anchors.top: parent.top\n            anchors.horizontalCenter: parent.horizontalCenter\n\n            MouseArea {\n                anchors.fill: parent;\n                onClicked: camera.imageCapture.capture();\n            }\n        }\n\n        Image {\n            id: photoPreview\n            width: 320\n            height: 180\n            anchors.bottom: parent.bottom\n            anchors.horizontalCenter: parent.horizontalCenter\n        }\n    }\n}\n```\n\n前面的代码会产生以下结果：\n\n![](img/be1fef7b-2b3f-459d-bfbd-811ce270255a.png)\n\n在这里，相机在顶部的`VideoOutput`项中显示取景器，在底部有一个`Image`项来显示捕获的图像。 当您触摸取景器时，QML 调用`imageCapture`的`capture`方法，该方法是`Camera`的一部分，捕获图像并更新底部图像。\n\n`Camera`项的`imageCapture`属性还有一个`capturedImagePath`属性，它是存储上次捕获的图像的路径的字符串。\n\n录制的工作方式与此类似；您可以指定录制的属性，例如在`videoRecording`属性中指定所需的编解码器，然后调用其`record`和`stop`方法来开始和停止录制。 生成的视频将存储在属性的`actualLocation`字段指示的位置。\n\nFor more information on the actual attributes available to applications using the `Camera` type, see the Qt Multimedia documentation for the `Camera` type at [https://doc.qt.io/qt-5/cameraoverview.html](https://doc.qt.io/qt-5/cameraoverview.html)[.](http://qt-project.org/doc/qt-5/cameraoverview.html)\n\n就是这样，我们已经成功地将我们的应用连接到网络摄像头，并且可以从其中检索图像。\n\n# 简略的 / 概括的 / 简易判罪的 / 简易的\n\n在本章中，您了解了 Qt Quick 提供的用于管理音频和视频媒体的类型，以及如何控制摄像机(如果存在)。 使用这些类型，您可以将音效和环境音频添加到您的应用中，这将使您的视频更令人愉悦，并帮助观众更好地了解正在播放的内容。 您还可以播放来自资源、文件系统或 Web 的视频。 此外，除了控制相机，如果有的话，你还可以用它捕捉静止和运动的图像。\n\n在下一章中，我们将了解 Qt 对访问硬件传感器的支持，例如与设备位置、方向和电源状态相关的支持。"
  },
  {
    "path": "docs/app-dev-qt-creator/09.md",
    "content": "# 九、传感器和 Qt Quick\n\n今天的任何设备都带有无数的传感器，包括一种确定设备位置和方向的方法，以及通过温度计、发光传感器、加速计、陀螺仪和其他传感器测量其周围环境特征的方法。 手机和其他便携式设备尤其如此。 要了解我们移动设备中所有可用的传感器的更多信息，请阅读该文章：[https://gizmodo.com/all-the-sensors-in-your-smartphone-and-how-they-work-1797121002](https://gizmodo.com/all-the-sensors-in-your-smartphone-and-how-they-work-1797121002)。\n\n在本章中，我们将看看 Qt 的传感器和定位框架，因为它们在 QML 中是受支持的。 你将学习如何确定设备在地球表面的位置，以及如何测量其机载传感器报告的环境的其他特征。\n\n在本章中，我们将介绍以下主题：\n\n*   访问 Qt 中的传感器\n*   确定设备位置\n*   获取设备的位置\n*   在地图视图上放置位置标记\n*   在 C++ 中访问传感器\n\n# 技术要求\n\n本章的技术要求如下：\n\n*   Qt 5.12.3 arm64-v8a\n*   Qt Creator 4.9.0\n*   Windows 10\n\n# 访问 Qt 中的传感器\n\n从 Qt Mobility 库开始，Qt 已经为设备传感器提供了一个强大的移植层已有数年之久，该库旨在促进手机软件的开发。 随着 Qt 的不断发展，Qt Quick 增加了对传感器的支持，支持的传感器列表也在增加。 目前，Qt 支持以下传感器：\n\n*   通过`Accelerometer`型支持加速度计。\n*   高度计通过`Altimeter`类型支持。\n*   通过`AmbientLightSensor`和`LightSensor`类型支持环境光传感器。\n*   通过`AmbientTemperatureSensor`类型支持环境温度传感器。\n*   指南针通过`Compass`类型被支持。\n*   陀螺仪通过`Gyroscope`型支撑。\n*   设备是否在皮套中是通过类型`Holster`来确定的。\n*   与设备屏幕的接近程度通过`IRProximitySensor`和`ProximitySensor`类型确定。\n*   通过`Magnetometer`类型支持环境磁场。\n*   通过`OrientationSensor`类型支持设备的方向。\n*   通过`RotationSensor`类型支持设备旋转。\n*   通过`TapSensor`类型支持设备外壳在其*x*、*y*和*z*轴上轻敲的方式。\n*   通过`TiltSensor`类型报告设备的倾斜情况。\n\n这些类型中的每一个都有包含读数的相应类型；例如，`Accelerometer`类型通过`AccelerometerReading`类型报告其当前值。\n\n要访问传感器库，必须将关键字`sensors`添加到`.pro`文件，如下所示：\n\n```cpp\nQT += quick sensors\n```\n\n使用所有这些类型所遵循的模式本质上是相同的：导入`QtSensors`模块、实例化传感器、激活或停用传感器，然后将脚本连接到其`readingChanged`插槽。 例如，要从加速度计读取数据，您需要编写以下代码：\n\n```cpp\nimport QtQuick 2.12 \nimport QtQuick.Window 2.12 \nimport QtSensors 5.12 \nWindow { \n    visible: true \n    width: 360 \n    height: 360 \n\n    Accelerometer { \n        id: accel \n        dataRate: 100 \n        active: true \n\n        onReadingChanged: { \n            // print out the x, y, and z values from accelerometer\n            console.log(x + \",\" + y + \",\" + z);\n        } \n    } \n} \n```\n\n传感器有三个您需要了解的关键属性：\n\n*   `dataRate`：这表示传感器的采样速率(以毫秒为单位)。\n*   `active`：这表示应用是否应该采样传感器(由`true`的值表示)。\n*   `onReadingChanged`：它包含处理传感器读数的脚本，该脚本通过访问您提供的脚本中的`reading`变量获得。\n\n每个传感器根据其用途返回不同类型的读数；对于此加速度计传感器，它为我们提供一组三维读数，即`x`、`y`和`z`。\n\n重要的是要认识到，尽管 Qt 有针对所有这些传感器的接口，但并不是每个平台都支持所有这些传感器，甚至在特定平台(比如 Android)上，不同的设备可能有不同的传感器。 例如，在 Qt 5.3 中，Qt 支持加速度计、环境温度传感器、陀螺仪、光传感器、磁力计、接近传感器和旋转传感器，但不支持任何其他传感器。 此外，并不是每台安卓设备都有这些传感器；我的安卓平板电脑没有磁力计。 在设计应用时，您需要考虑这两个事实：目标的 Qt 端口层支持哪些传感器，以及目标受众实际拥有的硬件上的传感器类型。 显示 Qt 支持的平台提供在[http://qt-project.org/doc/qt-5/compatmap.html](http://qt-project.org/doc/qt-5/compatmap.html)上可用的传感器的矩阵。\n\n正如我们将在[的第 12 章](12.html)，*使用 Qt Creator*开发移动应用中了解到的，传感器消耗电池电量，因此您的应用应该明智地使用它们。 当您需要通过设置 Active 属性进行测量时，将其打开，并在完成测量时将其关闭。\n\n在本节中，我们学习了如何从手机上的加速度计读取数据。 接下来，我们将了解如何通过读取 GPS 传感器的数据来确定设备的位置。\n\n# 确定设备位置\n\n许多设备支持位置确定，或者通过诸如**全球定位系统**(**GPS**)接收器之类的硬件或者通过诸如**网际协议**(**IP**)地理位置之类的网络资源来支持位置确定。 与其他传感器支持类似，此功能是在 Qt 4.x 中通过 Qt Mobility 模块引入 Qt 的，现在通过 Qt 定位模块提供支持。 它在许多移动设备上都受支持，包括 Android。\n\n要使用 Qt 定位模块，您需要在`.pro`文件中包含关键字`positioning`，如下所示：\n\n```cpp\nQT += quick network positioning \n```\n\nQt 定位模块提供了三种定位方式，您可以通过导入`QtPositioning`模块来访问：\n\n*   `PositionSource`：它以指定的速率提供位置更新，当位置更新可用时发出`positionChanged`信号。\n*   `Position`：在分配给`positionChanged`信号的槽中，您将收到一个`Position`实例。\n*   `Coordinate`：`Position`实例具有指定设备位置的`Coordinate`属性。\n\n`PositionSource`类型具有以下属性：\n\n*   `active`：如果为真，则向系统指示应激活定位系统，并将设备定位读数返回给您的应用。\n*   `name`：表示当前报告设备位置的定位插件的唯一名称。\n*   `preferredPositioningMethods`：这表示您的应用的定位首选项。 首选的定位方法可以是以下方法之一：\n    *   `PositionSource.NoPositioningMethods`：这表示没有首选的定位方法。\n    *   `PositionSource.SatellitePositioningMethods`：这表明应首选基于卫星的方法，如全球定位系统。\n    *   `PositionSource.NonSatellitePositioningMethods`：这表明应首选 IP 地理定位等非卫星方法。\n    *   `PositionSource.AllPositioningMethods`：表示可以接受任何定位方式：\n        *   `sourceError`：它保存上次使用`PositionSource`方法出现的错误。\n        *   `supportedPositioningMethods`：表示支持的可用定位方法。\n        *   `updateInterval`：此参数指定所需的更新间隔(以毫秒为单位)。\n        *   `valid`：指定定位系统是否已获得有效的后端插件来提供数据。\n\n下面是`PositionSource`的简单用法：\n\n```cpp\nPositionSource {\n    id: src\n    updateInterval: 1000\n    active: true\n\n    onPositionChanged: {\n        var coord = src.position.coordinate;\n        position.text =\n                Math.abs(coord.latitude) + (coord.latitude < 0 ? \" S \" \n                  : \" N \" ) + Math.abs(coord.longitude) +\n                  (coord.longitude < 0 ? \" W \" : \" E \" );\n    }\n}\n```\n\n您可以通过调用`start`方法在`PositionSource`上开始定位，通过调用它的`stop`方法停止更新。 除了这样做之外，您还可以通过调用它的`update`方法来请求一次定位报告。\n\n`Position`类型有许多可用于封装设备位置的属性。 这些资料如下：\n\n*   `altitudeValid`：这表示高度读数是否有效。\n*   `coordinate`：它包含一个包含纬度、经度和海拔的坐标。\n*   `directionValid`：表示方向是否有效。\n*   `horizontalAccuracy`：表示报告位置的水平精度程度。\n*   `horizontalAccuracyValid`：表示水平精度是否有效。\n*   `latitudeValid`和`longitudeValid`：表示纬度和经度是否有效。\n*   `speed`：这表示设备的速度。\n*   `speedValid`：表示设备的速度是否有效。\n*   `timestamp`：这表示测量的时间。\n*   `verticalAccuracy`：表示测量的垂直精度。\n*   `verticalAccuracyValid`：表示垂直精度是否有效。\n*   `verticalSpeed`：表示设备的垂直速度。\n*   `verticalSpeedValid`：表示垂直速度是否有效。\n\n所有距离和速度都以公制表示，而纬度和经度则以十进制度表示。 这是通过 WGS-84 基准完成的。\n\n除了提供设备的纬度、经度和海拔高度外，`Coordinate`类型还提供了`distanceTo`和`azimuthTo`方法，使您可以计算两个`Coordinate`实例之间的距离或从一个`Coordinate`实例到另一个实例的方向角。 它还提供了`atDistanceAndAzimuth`方法，用于在从坐标的纬度和经度以特定方位移动特定距离时计算目标点。 这些方法是地图制图中使用的所谓**大地正演问题**和**反大地测量问题**的解决方案。 请转到[http://www.ngs.noaa.gov/TOOLS/Inv_Fwd/Inv_Fwd.html](http://www.ngs.noaa.gov/TOOLS/Inv_Fwd/Inv_Fwd.html)了解如何计算这些参数的详细信息。\n\n下面的代码显示了`PositionSource`类型的用法示例。 但首先，让我们创建一个窗口和一些文本对象：\n\n```cpp\nimport QtQuick 2.12\nimport QtQuick.Window 2.12\nimport QtPositioning 5.12\n\nWindow {\n    visible: true\n    width: 360\n    height: 360\n\n    Text {\n        id: positionLabel\n        text: qsTr(\"Position:\")\n        anchors.top: parent.top\n        anchors.left: parent.left\n    }\n\n    Text {\n        id: position\n        text: qsTr(\"Hello World\")\n        anchors.horizontalCenter: parent.horizontalCenter\n        anchors.top: parent.top\n    }\n```\n\n之后，我们实现`PositionSource`对象，它向我们提供来自 GPS 传感器的数据：\n\n```cpp\n    PositionSource {\n        id: src\n        updateInterval: 1000\n        active: true\n\n        onPositionChanged: {\n            var coord = src.position.coordinate;\n            position.text =\n                    Math.abs(coord.latitude) + (coord.latitude < 0 ? \" \n                      S \" : \" N \" ) +\n                    Math.abs(coord.longitude) + (coord.longitude < 0 ? \n                       \" W \" : \" E \" );\n        }\n    }\n}\n```\n\n在这里，`PositionSource`类型在应用启动时处于活动状态，并且每秒更新一次。 当它接收到位置报告时，它会发出一个`positionChanged`信号，该信号会触发`onPositionChanged`脚本。 这将从该位置获取坐标，并对其进行格式化，以便在`position`文本字段中显示。\n\nLike other device sensors, obtaining position reports uses additional battery power over normal program execution. Generally, your application should only determine the device's position when it actually needs it, such as before submitting the position to a service in order to determine nearby points of interest or to tag the user's location in some way. In general, you should not run the positioning system all the time (unless you're building an application that needs it, such as a turn-by-turn navigation application), because doing so can greatly diminish battery life.\n\n在前面的示例中，我们了解了如何获取设备的 GPS 位置。 接下来，我们将深入研究我们的设备上可用的其他各种传感器，以及如何从它们获取各自的数据。\n\n# 获取设备的位置\n\n让我们继续看一个简单的示例，它返回设备的位置、加速计、陀螺仪、环境光和磁力计读数。 下面是我们的应用在华为手机上运行时的样子：\n\n![](img/d48c0ffa-44c2-4d69-95d9-9043be832144.jpg)\n\n请注意，我的华为手机没有磁力计，所以这些读数没有更新。 让我们看看这是如何工作的：\n\n1.  首先，我们需要确保在`.pro`文件中包含定位和传感器模块(如果没有，应用将编译，但无法启动)：\n\n```cpp\nQT += quick positioning sensors \n```\n\n2.  接下来，我们将继续讨论 QML 本身。 这很长，但很简单。 首先，我们将必要的模块添加到我们的项目中；然后，我们为我们的程序定义`Window`对象：\n\n```cpp\nimport QtQuick 2.12\nimport QtQuick.Window 2.12\nimport QtPositioning 5.12\nimport QtSensors 5.12\n\nWindow {\n    visible: true\n    width: 360\n    height: 360\n```\n\n3.  之后，我们创建一些`Text`对象来显示静态文本：\n\n```cpp\n    Text {\n        id: positionLabel\n        text: qsTr(\"Position:\")\n        anchors.top: parent.top\n        anchors.left: parent.left\n        color: position.valid ? \"red\" : \"black\"\n    }\n\n    Text {\n        id: position\n        text: qsTr(\"Hello World\")\n        anchors.horizontalCenter: parent.horizontalCenter\n        anchors.top: parent.top\n    }\n```\n\n4.  然后，我们创建一个`PositionSource`对象和一个`Accelerometer`对象来从定位传感器和加速度计传感器获取数据，如下所示：\n\n```cpp\n    PositionSource {\n        id: src\n        updateInterval: 1000\n        active: true\n\n        onPositionChanged: {\n            var coord = src.position.coordinate;\n            position.text =\n                    Math.abs(coord.latitude) + (coord.latitude < 0 ? \" S \" : \" N \" ) +\n                    Math.abs(coord.longitude) + (coord.longitude < 0 ? \" W \" : \" E \" );\n        }\n    }\n\n    LabelThreePart {\n        id: accelerometerReading\n        label: \"Accel\"\n        anchors.top: position.bottom\n        anchors.horizontalCenter: parent.horizontalCenter\n    }\n\n    Accelerometer {\n        id: accel\n        dataRate: 100\n        active:true\n\n        onReadingChanged: {\n            accelerometerReading.xValue = (reading.x).toFixed(2);\n            accelerometerReading.yValue = (reading.y).toFixed(2);\n            accelerometerReading.zValue = (reading.z).toFixed(2);\n        }\n    }\n\n    LabelThreePart {\n        id: gyroscopeReading\n        label: \"Gyro\"\n        anchors.top: accelerometerReading.bottom\n        anchors.horizontalCenter: parent.horizontalCenter\n    }\n```\n\n5.  紧接着，我们创建了一个`Gyroscope`旋转对象、一个旋转`LightSensor`旋转对象和一个旋转`Magnetometer`旋转对象，以从各自的传感器获得旋转运动、光敏感度和方向数据。 请考虑以下代码：\n\n```cpp\n\n    Gyroscope {\n        id: gyroscope\n        dataRate: 100\n        active: true\n\n        onReadingChanged: {\n            gyroscopeReading.xValue = (reading.x).toFixed(2);\n            gyroscopeReading.yValue = (reading.y).toFixed(2);\n            gyroscopeReading.zValue = (reading.z).toFixed(2);\n        }\n    }\n\n    Text {\n        id: lightSensorLabel\n        anchors.top: gyroscopeReading.bottom\n        anchors.right: lightSensorValue.left\n        text: qsTr(\"Light Sensor:\")\n    }\n\n    Text {\n        id: lightSensorValue\n        anchors.top: lightSensorLabel.top\n        anchors.horizontalCenter: parent.horizontalCenter\n        text: \"N/A\"\n    }\n\n    // Light Sensor\n    LightSensor {\n        id: lightSensor\n        dataRate: 100\n        active: true\n\n        onReadingChanged: {\n            lightSensorValue.text = (reading.illuminance).\n              toFixed(2);\n        }\n    }\n\n    // Magnetometer\n    LabelThreePart {\n        id: magnetometerReading\n        label: \"Mag\"\n        anchors.top: lightSensorValue.bottom\n        anchors.horizontalCenter: parent.horizontalCenter\n    }\n\n    Text {\n        id: magcLabel\n        anchors.right: magcValue.left\n        anchors.top: magnetometerReading.bottom\n        text: \"Mag Cal: \"\n    }\n    Text {\n        id: magcValue\n        anchors.horizontalCenter: parent.horizontalCenter\n        anchors.top: magcLabel.top\n        text: \"N/A\"\n    }\n\n    Magnetometer {\n        onReadingChanged: {\n            magnetometerReading.xValue = (reading.x).toFixed(2);\n            magnetometerReading.yValue = (reading.y).toFixed(2);\n            magnetometerReading.zValue = (reading.z).toFixed(2);\n            magcValue.text = (reading.calibrationLevel).toFixed(2);\n        }\n    }\n}\n```\n\n首先是 Position 标签和 Position 字段，如果`PositionSource`无法获得修复，则将其涂成红色；否则，它将显示为黑色。 生成此结果的代码如下所示：\n\n```cpp\n color: position.valid ? \"red\" : \"black\"\n```\n\n接下来是剩下的传感器。 在这里，我使用了我编写的一个小`LabelThreePart`控件，如下所示：\n\n```cpp\nimport QtQuick 2.12 \n\nRectangle { \n    property string label: \"Something\" \n    property alias xValue: xValue.text \n    property alias yValue: yValue.text \n    property alias zValue: zValue.text \n    width: parent.width \n    height: 32 \n\n    Text { \n        id: xLabel \n        anchors.left: parent.left \n        anchors.top: parent.top \n        text: label + \" X: \" \n    } \n    Text { \n        id: xValue \n        anchors.left: xLabel.right \n        anchors.top: parent.top \n        text: \"N/A\" \n    }\n```\n\n我们继续将其余的`text`对象添加到我们的 QML 代码中：\n\n```cpp\n    Text { \n        id: yLabel \n        anchors.right: yValue.left \n        anchors.top: parent.top \n        text: label + \" Y: \" \n    } \n    Text { \n        id: yValue \n        anchors.horizontalCenter: parent.horizontalCenter \n        anchors.top: parent.top \n        text: \"N/A\" \n    } \n    Text { \n        id: zLabel \n        anchors.right: zValue.left \n        anchors.top: parent.top \n        text: label + \" Z: \" \n    } \n    Text { \n        id: zValue \n        anchors.right: parent.right \n        anchors.top: parent.top \n        text: \"N/A\" \n    } \n} \n```\n\n从前面的代码中，我们可以看到这只是一个包含六个字段的矩形；它使用其`label`属性为要显示的`X`、`Y`和`Z`值创建有意义的标签，以及文本字段的属性别名(实际显示这些值)。\n\n确保在项目中包含 LabelThreePart.qml 文件；否则，您将无法编译：\n\n![](img/dbffafe2-2da5-42e2-9a08-96a349cc0a06.png)\n\n6.  在此之后，如果您正在为移动平台进行构建，还必须确保您已经启用了位置权限。 对于 Android 平台，请转到 Projects(项目)|>Build Steps(构建步骤)|创建 Android APK，然后单击 Create Templates(创建模板)按钮创建`AndroidManifest.xml`文件，然后您可以向该文件添加 android.permission.ACCESS_FINE_LOCATION 权限，然后才能访问手机的位置数据。 下面的屏幕截图显示了我们的应用所需的权限。 您可以通过从位于底部的选择框中选择所需的权限来添加更多权限。 然后，单击添加按钮继续：\n\n![](img/bd56c77e-0be4-480e-a0ea-27019f7dea96.jpg)\n\n对于 Android 6.0 及更高版本，您还必须在手机上启用位置权限才能正常工作。 每部手机都是不同的；对于我的手机，它位于设置|>应用和通知>|权限，如下所示：\n\n![](img/e3c8b777-373d-4754-8dba-3d2f31d13ccb.png)\n\n到目前为止，我们已经了解了如何从我们的设备和传感器获取位置数据。 让我们继续学习如何通过在地图视图上放置位置标记来可视化定位数据。\n\n# 在地图视图上放置位置标记\n\n从 5.0 版开始，Qt 为我们提供了一个地图视图组件，可以显示地球的地图或图像，类似于谷歌地图。 由于许可问题，Qt 地图视图不支持谷歌地图。 Qt Map View 的默认平铺地图服务提供商是社区地图项目**OpenStreetMap**(**OSM**)，因为它是免费的。 除了 OSM，您还可以使用其他商业服务提供商，例如 Mapbox、ArcGIS 和 HERE。\n\n与其他第三方地图解决方案不同，Qt 地图视图使用本地渲染引擎(由 Qt Quick 提供支持)渲染平铺地图，而不是将 Web 视图嵌入到应用中。 本机渲染可以提高性能，并使应用保持较小的大小，因为它不包含 Web 视图所需的所有不必要的资源。 然而，目前您不能使用 C++ 与地图视图交互，只能使用 QML。 我相信开发人员将在未来的版本中使其成为可能。\n\n让我们从用 QML 编写一个非常简单的地图视图应用开始：\n\n```cpp\nimport QtQuick 2.12\nimport QtQuick.Window 2.12\n\nimport QtLocation 5.12\nimport QtPositioning 5.12\n\nWindow {\n    id: window\n    visible: true\n    width: 640\n    height: 480\n    title: qsTr(\"Map View\")\n\n    Plugin {\n        id: mapPlugin\n        name: \"osm\"\n    }\n\n    Map {\n        id: map\n        anchors.fill: parent\n        plugin: mapPlugin\n        zoomLevel: 14\n        minimumZoomLevel: 6\n        maximumZoomLevel: 18\n        copyrightsVisible: false\n        gesture.enabled: true\n        gesture.acceptedGestures: MapGestureArea.PinchGesture | \n          MapGestureArea.PanGesture\n    }\n}\n```\n\n我们在这里添加的是`Map`对象，它显示平铺地图，旁边还有一个`Plugin`对象，它确定地图视图的服务提供者。 我们还可以定义缩放级别、最大和最小缩放级别、版权文本显示以及手势设置。 手势设置仅适用于触摸屏应用，因此我们无法在桌面上进行测试。\n\n除此之外，我们还必须为我们的 Qt 项目包括`location`模块：\n\n```cpp\nQT += quick location positioning\n```\n\n前面的代码生成以下输出：\n\n![](img/229076e6-d341-4a62-8286-2409095ffb40.png)\n\n很简单，不是吗？ 如果您看到一个空映射，请确保您已将 OpenSSL 库文件下载到您的计算机，因为 OSM 不再接受 HTTP 请求，而只接受 HTTPS 请求。 您可以从[https://indy.fulgan.com/SSL](https://indy.fulgan.com/SSL)下载最新的 openssl 版本。 对于 Windows 用户，还请确保`libcrypto`和`libssl`DLL 文件与您的可执行文件放在一起。\n\n如果您正在为 Android 开发应用，则必须在您的应用 APK 中包含`.so`个库，方法是在项目文件中使用以下设置：\n\n```cpp\ncontains(ANDROID_TARGET_ARCH,armeabi-v7a) {\n    ANDROID_EXTRA_LIBS = \\\n        $$PWD/android/32bit/libcrypto.so \\\n        $$PWD/android/32bit/libssl.so\n}\n\ncontains(ANDROID_TARGET_ARCH,arm64-v8a) {\n    ANDROID_EXTRA_LIBS = \\\n        $$PWD/android/64bit/libcrypto.so \\\n        $$PWD/android/64bit/libssl.so\n}\n```\n\nPlease make sure that you only include the 32-bit library if you are building a 32-bit application, and only include a 64-bit library if you're building a 64-bit application.\n\n让我们来看一下以下步骤：\n\n1.  之后，让我们创建另一个空的 QML 文件并将其命名为`marker.qml`。 我们将创建一个单独的 QML 文件，以便可以在我们的主 QML 文件或任何其他我们想要的 QML 文件中重用它。 现在我们已经创建了文件，我们可以添加以下代码来实现我们的地图标记：\n\n```cpp\nimport QtQuick 2.12\nimport QtQuick.Controls 2.12\nimport QtLocation 5.12\n\nMapQuickItem\n{\n    id: marker\n    anchorPoint.x: 0\n    anchorPoint.y: icon.height / 2\n\n    sourceItem: Item\n    {\n        Image\n        {\n            id: icon\n            source: \"img/mapmarker.png\"\n            sourceSize.width: 120\n            sourceSize.height: 120\n            width: 120\n            height: 120\n            anchors.centerIn: parent\n        }\n    }\n}\n```\n\n2.  接下来，我们需要在`Map`对象中添加以下代码：\n\n```cpp\nComponent.onCompleted: {\n    map.center = QtPositioning.coordinate(-37.8163521,144.9275631);\n    map.zoomLevel = 12;\n\n    var component = Qt.createComponent(\"marker.qml\");\n    var item = component.createObject(window, { coordinate: \n    QtPositioning.coordinate(-37.8163521,144.9275631) }); \n                                                    //Melbourne\n    map.addMapItem(item);\n}\n```\n\n在前面的代码中，当成功启动`Map`对象时，会调用`onCompleted`槽函数。 然后我们将地图的中心点重新定位到墨尔本的某个地方，并将其缩放级别设置为`12`。 之后，我们根据上一步中创建的`marker.qml`文件创建一个新组件，并将其放置在地图中心点所在的相同坐标上。\n\n3.  再次运行该程序。 现在，您应该看到如下所示：\n\n![](img/3e3e6058-0ed1-4104-9928-61c973c657d4.png)\n\n请注意，您需要将标记图像文件添加到项目资源中；否则，启动程序时它将不会显示在地图上。 如果您尚未创建 QT 资源文件，请转到文件|新建文件或项目，然后在文件和类|.Qt 下选择创建 Qt 资源文件选项。\n\n4.  之后，打开资源文件并添加前缀(其工作原理类似于文件夹)，以对不同的文件组进行分类。 创建前缀后，将标记图像文件添加到前缀并保存：\n\n![](img/0f2bc277-e1ae-4356-bef9-b3aeee96e24b.png)\n\n5.  最后，我们需要添加以下代码来获取设备的位置，并使用上一步中使用的地图标记显示其位置：\n\n```cpp\nproperty MapQuickItem item\n\nPositionSource {\n    id: src\n    updateInterval: 1000\n    active: false\n\n    onPositionChanged: {\n        var coord = src.position.coordinate;\n\n        map.center = coord;\n        item.coordinate = coord;\n    }\n}\n```\n\n请注意，我们已经将`item`对象变量从`onComplete`函数中移出，因为我们也将在`onPositionChanged`函数中使用它。 默认情况下，我们还将`active`属性设置为`false`，因为我们只希望它在映射成功初始化后才处于活动状态：\n\n```cpp\nComponent.onCompleted: {\n    map.center = QtPositioning.coordinate(-37.8163521,144.9275631);\n    map.zoomLevel = 12;\n\n    var component = Qt.createComponent(\"marker.qml\");\n    item = component.createObject(window, { coordinate: QtPositioning.\n      coordinate(-37.8163521,144.9275631) });\n    map.addMapItem(item);\n\n    src.active = true\n}\n```\n\n如果我们在移动设备上再次运行该应用，我们应该会看到标记已经从初始位置(墨尔本)移到了我们设备的位置。 如果我们开始带着我们的设备四处移动，记号笔也会每秒改变它的位置。 除此之外，我们还可以通过捏手指来放大地图：\n\n![](img/29b991f4-5fd4-4e09-b75f-f22895e14f5d.png)\n\n就是这样-在这一节中，我们学习了如何使用 Qt 提供的地图视图显示 OSM 地图，在地图视图上显示标记，以及获取设备的位置！\n\n接下来，我们将讨论如何使用 Qt 的 C++ 类访问移动设备上的相同传感器。\n\n# 用 C++ 访问传感器\n\n到目前为止，我们已经了解了如何通过 QML 脚本访问传感器数据。 但是，也可以通过 Qt 的各种 C++ 类访问传感器。 我们将介绍一些这方面的示例，并演示如何实现这一点。\n\n首先，创建一个空的 Qt 项目(不过，如果愿意，您可以从上一个示例继续)。 我们将打开`main.cpp`，而不是在`.qml`文件中编写代码。 之后，我们将包含一些头文件，以便可以访问这些类。 如果要创建新的 Qt 项目，请记住将`sensors`模块添加到您的项目文件(`.pro`)中：\n\n```cpp\n#include <QDebug>\n#include <QTimer>\n\n#include <QAccelerometer>\n#include <QAmbientLightSensor>\n#include <QProximitySensor>\n\n#include <QAccelerometerReading>\n#include <QAmbientLightReading>\n#include <QProximityReading>\n```\n\n然后，在`main`函数中添加以下代码：\n\n```cpp\nQAccelerometer *accSensor = new QAccelerometer;\naccSensor->start();\nQSensorReading *accReading = accSensor->reading();\n\nQTimer* timer = new QTimer;\nQObject::connect(timer, &QTimer::timeout, [=]{\n    qreal x = accReading->property(\"x\").value<qreal>();\n    qreal y = accReading->value(1).value<qreal>();\n    qDebug() << \"Accelerometer:\" << x << y;\n});\ntimer->start(1000);\n```\n\n在前面的代码中，我们创建了一个名为`accSensor`的`QAccelerometer`对象，该对象激活移动设备中的加速度计传感器。 然后，我们创建了一个名为`accReading`的`QSensorReading`对象，它帮助从`accSensor`检索数据。\n\n在那之后，我们创建了一个计时器，每 1000 毫秒(或 1 秒)触发 lambda 函数。 在 lambda 函数(每次计时器触发其`timeout`信号时都会调用该函数)中，我们从`accReading`对象获得了`x`和`y`属性，这是来自加速度计传感器的最新数据。 最后，我们使用`qDebug`函数打印出了`x`和`y`值。 请注意，我使用属性和值函数来分别获取`x`和`y`数据，只是为了向您展示两种不同的方法，您可以使用它们来实现相同的结果-第一种方法是通过引用属性名称(在本例中为`x`)，第二种方法是通过引用其在属性数组中的位置(在本例中为`1`，用于获取`y`值)。\n\n如果您现在在移动设备上构建并运行该应用，您应该会看到 Qt Creator 上的应用输出窗口上打印了一些值。 尝试旋转您的设备以查看`x`和`y`值的更大变化：\n\n```cpp\nD libSensorCPP.so: Accelerometer: 0.699519 0.850353\nD libSensorCPP.so: Accelerometer: -0.332152 1.04697\nD libSensorCPP.so: Accelerometer: -3.32985 2.8791\nD libSensorCPP.so: Accelerometer: 0.562877 9.5493\nD libSensorCPP.so: Accelerometer: 0.110468 9.82264\nD libSensorCPP.so: Accelerometer: 1.65357 -1.63815\nD libSensorCPP.so: Accelerometer: 0.759278 0.8346\n```\n\n一旦我们实现了这一点，我们就可以很容易地使用相同的方法来访问我们设备上的其他传感器。 让我们将以下内容添加到我们的代码中：\n\n```cpp\nQAmbientLightSensor *ambSensor = new QAmbientLightSensor;\nambSensor->start();\nQAmbientLightReading * ambReading = ambSensor->reading();\n\nQProximitySensor *proxSensor = new QProximitySensor;\nproxSensor->start();\nQProximityReading *proxReading = proxSensor->reading();\n```\n\n然后，将以下代码添加到我们之前创建的 lambda 函数中：\n\n```cpp\nqDebug() << \"Ambient Light:\" << ambReading->lightLevel();\nqDebug() << \"Proximity:\" << proxReading->close();\n```\n\n如果您现在构建并运行该项目，您应该会看到 Qt Creator 上显示的所有不同数据：\n\n```cpp\nD libSensorCPP.so: Accelerometer: 2.46714 0.646176\nD libSensorCPP.so: Ambient Light: 3\nD libSensorCPP.so: Proximity: false\nD libSensorCPP.so: Accelerometer: 2.47853 3.31007\nD libSensorCPP.so: Ambient Light: 3\nD libSensorCPP.so: Proximity: false\nD libSensorCPP.so: Accelerometer: 1.08533 3.96756\nD libSensorCPP.so: Ambient Light: 2\nD libSensorCPP.so: Proximity: false\n```\n\n不同传感器提供的数据各不相同，因此请阅读文档以了解每个传感器提供的数据(可从[https://doc.qt.io/qt-5/qtsensors-cpp.html#reading-classes](https://doc.qt.io/qt-5/qtsensors-cpp.html#reading-classes)获取)。除了我们在上一个示例中使用的类之外，还有许多其他 Qt 类允许您访问其各自的传感器，您可以在文档的同一页上找到这些类。\n\n在本节中，我们了解了如何通过 Qt 的 C++ 类访问设备的传感器，这为您在开发应用时提供了更多的选择。 您不仅可以编写 QML 脚本，还可以使用 C++ 实现相同的功能。\n\n# 简略的 / 概括的 / 简易判罪的 / 简易的\n\n在本章中，您学习了如何从设备传感器(包括设备的定位系统、加速度计和其他传感器)确定测量值。 您还了解了如何在地图上显示设备的位置，以便用户可以看到该位置及其上下文，而不仅仅是协调数字，因为定位帮助我们跟踪准确的位置。\n\n在下一章中，我们将看看如何使用 Qt 的本地化框架和工具来本地化我们的应用。"
  },
  {
    "path": "docs/app-dev-qt-creator/10.md",
    "content": "# 十、使用 Qt 语言学家本地化您的应用\n\n本地化是当今软件开发中一个重要但通常被忽视的部分。 大多数应用的作者，无论这些应用是商业应用还是开源应用，都希望为他们的应用赢得大量用户。 这意味着越来越多地需要在多个地区支持多种语言，而且通常需要在一个地区支持多种语言(可以将其视为在加拿大共存的法语和英语)。\n\n很长一段时间以来，Qt 都有一个使应用易于本地化的框架，其工具可以帮助您避免在应用中硬编码字符串，还有一个名为**Qt Languist**的 GUI 来帮助管理翻译。 在本章中，我们将介绍 Qt 的本地化策略，讨论 Qt 提供的三种工具(`lupdate`、`lrelease`和 Qt Languist)以及如何使用它们，以及在编写应用以利用 Qt 的本地化框架时需要做些什么。\n\n在本章中，我们将介绍以下主题：\n\n*   理解本地化的任务\n*   标记用于本地化的字符串\n*   使用 QLanguist 本地化您的应用\n*   使用 QLocale 本地化特殊参数-货币和日期\n\n# 技术要求\n\n本章的技术要求包括 Qt 5.12.3、MinGW 64 位、Qt Creator 4.9.0 和 Windows 10。\n\n# 理解本地化的任务\n\n如果您想要在全球销售您的产品，本地化是其重要功能。 当用户看到他们的母语在一款软件中被显示和使用时，他们更有可能会依恋它，成为你的忠实客户之一。 然而，本地化并不总是一个容易实现的特性，至少在 Qt 引入了一种更简单的方式之前是这样。\n\n本地化应用有几个阶段，在整个项目生命周期中通常会重叠。\n\n下图显示了这些阶段的交互方式：\n\n![](img/644d57f5-40bf-4e31-a0e8-d18fcdc420b2.png)\n\n这些阶段如下：\n\n1.  在编写应用时，将需要本地化的字符串放入特定函数中(请参见*步骤 5*)，以便 Qt 可以识别需要本地化的字符串。\n2.  定期提取应用中的所有字符串，并将它们交给翻译人员进行翻译。\n3.  翻译器为应用中的字符串提供翻译。\n4.  您可以使用要支持的每种语言的翻译字符串编译翻译文件。\n5.  C++ 和 QML 的`tr`和`qsTr`函数允许您识别应用中需要本地化的字符串。 Qt 提供了四个工具来简化这些阶段。\n6.  `lupdate`命令生成应用中需要本地化的字符串的列表。\n7.  翻译者使用 Qt Languist 为您的应用中的字符串提供翻译。\n8.  `lrelease`命令从 Qt Creator 获取翻译后的字符串，并将它们打包成供您的应用使用的格式。\n\n软件开发是迭代的，本地化也不例外。 小型项目可能更喜欢只本地化一次或两次，等到应用接近完成后再提交应用字符串进行本地化。 大型应用或拥有专职翻译人员的大型公司可能更喜欢迭代的方法，在整个应用开发过程中多次经历本地化周期。 Qt 支持这两种型号。\n\n我们已经了解了本地化的重要性以及如何在 Qt 中实施本地化。 在下一节中，我们将学习如何使用 C++ 方法完成此操作。\n\n# 标记用于本地化的字符串\n\n回到[第 1 章](01.html)，*Qt Creator 入门*，我告诉过您要始终使用`tr`和`qsTr`函数将字符串标记为本地化：`tr`用于 C++，`qsTr`用于 QML 字符串。 这样做有两个关键优势：\n\n*   它使 Qt 能够找到每个需要本地化的字符串。\n*   如果在应用中安装 Qt Translator 对象并提供翻译文件，则用这些函数包装的字符串将自动替换为它们的本地化等效项。\n\n让我们更详细地研究一下`tr`的用法。 所有在声明中包含`Q_OBJECT`宏的 Qt 对象都包含`tr`函数。 你已经看到了它的一个论点，如下所示：\n\n```cpp\nbutton = new QPushButton(tr(\"&Quit\"), this); \n```\n\n字符串中的前导`&`不是用于`tr`函数，而是用于键盘快捷键；您可以在字母前面加上`&`来指定键盘快捷键，它将获得默认系统(对于 Windows，*Alt*、对于 Apple，*cmd*和对于 Linux，*Alt*是一个组合键)。 如果该字符串的翻译版本未出现在应用的当前转换表中，`tr`函数将使用您在用户界面中作为字符串传递的字符串，或者使用当前转换表中的字符串(如果存在)。\n\n`tr`函数可以接受第二个参数，即`tr`用于可能需要不同翻译的同一字符串的消歧上下文。 如下所示：\n\n```cpp\ntr(\"&Copy\", \"Menu\"); \n```\n\n此函数还可以处理带复数的字符串，如下所示：\n\n```cpp\ntr(\"%n item(s) replaced\", \"\", count); \n```\n\n根据`count`的值和区域设置，返回不同的字符串。 因此，本地英语翻译可以返回`\"0 items replaced\"`、`\"1 item replaced\"`、`\"2 items replaced\"`等，而法语翻译可以返回`\"0 item remplacé\"`、`\"1 item remplacé\"`、`\"2 items remplacés\"`等。\n\n在 QML 中，您只需使用`qsTr`函数，该函数的工作方式与 C++ 中的`tr`方法完全相同。\n\n在本节中，我们了解了如何标记字符串，让 Qt 知道哪个字符串需要本地化。 接下来，我们将学习如何在 Qt 中运行本地化过程，并根据用户的语言偏好显示正确的语言。\n\n# 使用 QLanguist 本地化您的应用\n\n一旦使用`tr`或`qsTr`标记了字符串，就需要生成这些字符串的表，以便 Qt 语言学家进行本地化。 可以使用`lupdate`命令实现这一点，该命令获取`.pro`文件并遍历源代码以查找要本地化的字符串，并为 Qt 语言学家创建需要翻译的字符串的 XML 文件。 您需要为您想要支持的每种语言执行一次此操作。 在执行此操作时，最好系统地命名生成的文件；一种方法是使用项目文件的名称，后跟破折号，然后是该语言的 ISO-639-2 语言代码。\n\n下面是一个具体的例子。 本章使用`QtLinguistExample`；我们可以使用如下命令运行`lupdate`，以创建要转换为世界语(ISO-639-2 语言代码，ePO)的字符串列表：\n\n```cpp\n% lupdate -pro .\\QtLinguistExample.pro -ts .\\QtLinguistExample-epo.ts\n```\n\nDon't forget that the `%` character is the Command Prompt, which might differ from system to system.\n\n这里，`-pro`文件表示`.pro`文件包含要扫描以查找要翻译的字符串的源列表，而`-ts`参数表示要写入的翻译文件的名称。 当然，您需要在您的道路上使用`lupdate`。 如何设置路径将取决于您使用的是 Windows、MacOSX 还是 Linux，以及 Qt 的安装位置。 Qt 的某些安装可能会自动更新您的路径，而其他安装可能不会这样做。 例如，在我的 Windows 机器上，我可以在`C:\\Qt\\5.13.0\\msvc2017_64\\bin\\lupdate.exe`处找到`lupdate`。\n\n`.ts`文件是一个带有标记的 XML 文件，用于指示要翻译的字符串、它们在应用源代码中的上下文等。 Qt 语言学家会将翻译保存到它的输出文件中，该文件的名称也带有一个 QM 后缀，但是不要担心：`lupdate`足够聪明，如果您在提供一些翻译之后再次运行它，它不会覆盖现有的翻译。\n\n您还可以轻松地从 Qt Creator 运行命令提示符，方法是在项目面板中右键单击`.pro`命令文件，然后选择 Build Environment(构建环境)选项，以自动配置命令提示符相对于项目目录的路径，如下所示：\n\n![](img/30aa3ef6-fe62-4edc-b3d9-e69832cbd966.png)\n\n除了使用命令外，您还可以直接从 Qt Creator 中使用`lupdate`和`lrelease`，方法是转到工具|外部|语言学家|更新翻译(Lupdate)或发布翻译(Lrelease)。 请注意，这将更新或释放所有翻译文件。 如果您只想更新或发布特定的文件/语言，命令仍然是您最好的朋友。\n\n以下屏幕截图显示了`lupdate`工具在应用菜单上的位置：\n\n![](img/23ace585-985e-42f9-a90d-d4fe2440fb11.png)\n\nQt Language ist 是一个 GUI 应用；在启动该应用时，您将看到一个与下一个屏幕截图非常相似的屏幕：\n\n![](img/44da3244-6833-4d38-af5c-1b952b08c9ba.png)\n\n首先，您需要打开您通过导航到文件|打开文件并选择翻译文件生成的`.ts`文件。 系统将提示您输入目标语言，然后给出找到的字符串列表：\n\n![](img/14449d96-e8d4-4676-b2c4-6a751964424b.png)\n\n您或您的翻译人员只需遍历每个字符串，然后以翻译的语言输入相应的字符串。 这样做时，您可以在最右边的窗格中看到源代码中字符串的上下文；从中捕获字符串的源代码所在的行会高亮显示。\n\nQt 语言学家可以让您跟踪已翻译的字符串以及仍需翻译的字符串。 每个字符串左侧的图标可以是以下图标之一：\n\n*   黑色问号，表示字符串尚未翻译\n*   黄色问号，表示该字符串没有通过 Qt 语言学家的所有验证测试，但您忽略了这些失败\n*   一个感叹号，表示您提供的字符串没有通过 Qt 语言学家的验证测试\n*   黄色的复选框，表示您提供了翻译，但 Qt Creator 可能发现翻译有问题\n*   绿色的复选框，表示字符串已翻译完毕，可以开始使用了\n\nQt Languist 提供了一些简单的验证测试，比如确保具有`printf`样式参数的字符串在每次翻译中都有相同数量的参数。\n\nQt 语言学家还支持词汇书；您可以下载包含已本地化为目标语言的常用字符串的词汇书。\n\n在任何时候，您都可以通过运行`lrelease`来生成要包含在应用中的转换文件。 例如，要为世界语字符串创建一个，我们将使用`lrelease`，如下所示：\n\n```cpp\n% lrelease .\\QtLinguistExample-epo.ts .\\QtLinguistExample-epo.qm\n```\n\n这将获取传入的`.ts`文件，并生成包含字符串的`.qm`文件。 `.qm`文件是 Qt 在呈现应用的过程中直接使用的高度压缩的二进制文件。\n\n我们已经了解了如何使用 Qt Languist 将所有文本导出到列表中，并允许您和您的翻译人员进行翻译。 接下来，我们将继续在我们的应用中包含本地化文本。\n\n# 在应用中包含本地化字符串\n\n为了向应用中的`tr`和`qsTr`函数提供翻译后的字符串，您的应用需要包括一个`QTranslator`对象来读取`.qm`文件，并将提供给`tr`和`qsTr`的字符串替换为翻译后的对应字符串。\n\n我们可以在您的主要入口点函数中执行此操作，如下所示：\n\n```cpp\nQApplication a(argc, argv); \nQTranslator translator; \nbool result = translator.load(\"QtLinguistExample-epo.qm\"); \na.installTranslator(&translator); \n\n    // Other window setup stuff goes here \n\nreturn a.exec(); \n```\n\n此代码分配一个`QTranslator`对象，并在将其安装到`QApplication`对象之前将指定的翻译文件加载到转换器中。 在本例中，我们对语言进行了硬编码，以便将其本地化为世界语。\n\n请注意，如果您希望支持系统选择的区域设置，则可以选择这样做：\n\n```cpp\nQString locale = QLocale::system().name(); \nQTranslator translator; \ntranslator.load(QString(\"QtLinguistExample-\") + locale); \n```\n\n这里的`QLocale`类是用于管理系统区域设置的类。 在这里，我们使用它来确定系统的区域设置，然后尝试加载系统当前区域设置的本地化字符串文件。\n\n要实现这一点，应用的`.qm`文件需要可由应用定位。 它们应该在输出目录中；在开发过程中，一种方法是在 Qt Creator 中关闭影子构建，在项目窗格的 Build 和 Settings 下。 构建应用安装程序是一项特定于平台的任务，这超出了本书的范围，为此，您需要在应用二进制文件中包含`.qm`个文件。\n\nFor more information on Qt Linguist, refer to its manual at [https://doc.qt.io/qt-5/qtlinguist-index.html](https://doc.qt.io/qt-5/qtlinguist-index.html)[.](https://doc.qt.io/qt-5/qtlinguist-index.html)\n\n接下来，我们将了解如何本地化一些特殊参数以满足我们的要求。\n\n# 使用 QLocale 本地化特殊参数-货币和日期\n\n您可能需要做的一件常见的事情是本地化货币和日期。 Qt 使这一点变得简单，尽管解决方案在您仔细考虑之前并不明显。 让我们试试看吧。\n\n1.  您需要了解`QString arg`方法。 这将用其参数的格式化版本替换转义数字。 例如，如果我们编写以下内容：\n\n```cpp\nQString s = QString(\"%1 %2\").arg(\"a\").arg(\"b\"); \n```\n\n然后，`s`将包含字符串`\"a b\"`。\n\n2.  您需要了解`QLocale`的`toString`方法，该方法以特定于区域设置的方式格式化其参数。 因此，我们可以写下以下内容：\n\n```cpp\nQString currencyValue = QString(\"%1 %2\") \n    .arg(tr(\"$\")).arg(QLocale::toString(value, 'g', 2) \n```\n\n它使用`tr`本地化货币符号，并使用`QLocale`类的静态方法`toString`将货币值转换为带有区域设置特定小数分隔符(在美国和加拿大为句点，在欧洲为逗号)的字符串。\n\n日期格式类似；`QLocale`的`toString`方法重载了`QDateTime`、`QDate`和`QTime`参数，因此只需编写以下代码：\n\n```cpp\nQDateTime whenDateTime = QDateTime::currentDateTime(); \nQString when = QLocale::toString(whenDate); \n```\n\n这将获取当前日期和时间并将其存储在`whenDateTime`中，然后使用区域设置的默认格式将其转换为字符串。 `toString`方法可以采用第二个参数来确定输出格式；它是以下参数之一：\n\n*   `QLocale::LongFormat`：这使用长版本的月份和日期名称。\n*   `QLocale::ShortFormat`：这使用月和日名称的简短版本。\n*   `QLocale::NarrowFormat`：这提供了日期和时间的最窄格式。\n\n就这样。 我们不仅学习了如何使用 Qt 本地化文本，还学习了如何本地化货币和日期等特殊字符。\n\n# 简略的 / 概括的 / 简易判罪的 / 简易的\n\n使用 Qt 语言学家和 Qt 中的本地化框架，可以轻松地使用 Qt 本地化应用。 不过，要使用该框架，您必须将字符串标记为在源代码中使用`tr`或`qsTr`进行本地化，无论它们出现在哪里。 完成此操作后，您可以使用 Qt 的`lupdate`命令创建要使用 QLanguist 进行翻译的字符串的源文件，然后为每个字符串提供翻译。 提供翻译后，使用`lrelease`编译它们，然后通过在应用的主函数中安装`QTranslator`对象并加载由`lrelease`生成的翻译表，将它们包含在应用中。\n\n在本章中，我们学习了如何标记可翻译文本，以便 Qt 知道哪些文本需要本地化。 接下来，我们还学习了如何使用 Qt Language ist 将这些文本导出到列表中，以便您和您的翻译人员可以轻松地针对每种语言进行编辑。 然后，我们了解了如何将翻译后的文本重新加载到 Qt 应用中，并根据用户的喜好显示它们。 最后，我们还学习了如何本地化特殊字符，如货币和日期。\n\n在下一章中，我们将介绍 Qt Creator 支持的软件开发的另一个重要方面：使用 QML Profiler 和 Valgrind 进行性能分析。"
  },
  {
    "path": "docs/app-dev-qt-creator/11.md",
    "content": "# 十一、使用 Qt Creator 优化性能\n\n我们不是每天都使用性能分析工具，但我们很高兴在我们需要它们的时候它们就在我们身边。 商业工具，如 Microsoft Visual Studio 附带的工具，或独立的工具，如 IBM 的 Rational Rose Purify，可能会因为它们的复杂性和对初学者不友好的设计而让您感到困惑。 幸运的是，Qt Creator 拥有您需要的大部分内置支持，用于使用开源工具来帮助您分析应用的运行时和内存性能。\n\n在本章中，我们将了解如何使用 QML 性能分析器执行 QML 应用的运行时性能分析，并了解如何读取它生成的报告来识别性能问题。 然后，我们将把注意力转向使用 Qt Creator 的 Valgrind 进行内存性能分析，Qt Creator 是一个免费选项，可以帮助您查找 Linux 平台上的内存泄漏和堆损坏。\n\n在本章中，我们将介绍以下主题：\n\n*   介绍 QML 性能分析\n*   QML Profiler 简介\n*   使用 QML Profiler 执行更多操作\n*   实施测试集成\n*   增加了对测试集成的更好支持\n\n# 技术要求\n\n本章的技术要求包括 Qt 5.12.3、MinGW 64 位、Qt Creator 4.9.0 和 Windows 10。\n\n# QML 性能分析简介\n\nQt Quick 应用应该是快速的，用户界面流畅流畅。 在许多情况下，使用 QML 很容易做到这一点；QML 和 Qt Quick 运行时的贡献者花费了大量精力来创建一个在各种环境下都能很好地运行的环境。 然而，有时候，无论您如何尝试，您都会发现您无法从您的应用中挤出您想要的性能。 一些错误是显而易见的，例如：\n\n*   在状态更改或触发绘制操作的操作之间执行大量计算密集型任务\n*   同时显示数千个元素的过于复杂的视图层次结构\n*   在非常有限的硬件上运行(通常与前两个问题结合在一起)\n\nKnuth 说过一句名言，“*过早优化是万恶之源*，”他绝对是对的。 但是，有时您可能需要测量应用的性能，Qt Creator 为此提供了一个特殊的性能分析器。 这样，您就可以查看应用在每个 QML 方法中花费的时间，并测量应用处于控制边缘的关键方面，例如创建应用的视图层次结构需要多长时间。\n\n让我们仔细看看。\n\n# QtSlowButton--一个需要性能调优的 Qt Quick 应用\n\n让我们分析一下`QtSlowButton`的性能，这是我编写的一个性能不佳的示例程序。 `QtSlowButton`程序有两个 QML 组件：一个基于[第 3 章](03.html)中的计算器按钮的按钮、*使用 Qt Designer 设计应用(*创建 Qt 应用*部分下的*)的按钮，以及一个带有您可以按下的按钮的视图。 下面是按钮实现：\n\n```cpp\nimport QtQuick 2.12\n\nRectangle { \n    id: button \n\n    width: 128 \n    height: 64 \n\n    property alias label: buttonText.text \n    property int delay: 0 \n\n    color: \"green\" \n\n    Rectangle { \n        id: shade \n        anchors.fill: button; \n        color: \"black\"; opacity: 0 \n    } \n\n    Text { \n        id: buttonText \n        anchors.centerIn: parent; \n        color: \"white\" \n        font.pointSize: 16 \n    } \n\n    MouseArea { \n        id: mouseArea \n        anchors.fill: parent \n        onClicked: { \n            for(var i = 0; i < button.delay; i++); \n        } \n    } \n\n    states: [ \n        State { \n            name: \"pressed\"; when: mouseArea.pressed === true \n            PropertyChanges { target: shade; opacity: .4 } \n        } \n    ] \n} \n```\n\n当您按下每个按钮时，它只会运行一个`for`循环；它的`delay`属性控制循环的次数。 此外，每个按钮都有一个名为`buttonText`的`label`实例，该按钮在可点击区域的中心绘制该实例。\n\n该程序的主用户界面由`Column`中的三个按钮组成，标记为`fast`、`medium`、[0]和`slow`，延迟时间逐渐延长：\n\n```cpp\nimport QtQuick 2.12\nimport QtQuick.Window 2.12\n\nWindow {\n    visible: true\n    width: 180\n    height: 360\n    title: qsTr(\"Slow Button\")\n\n    Column\n    {\n        spacing: 20\n        Button    // custom Button from button.qml\n        {\n            delay: 10000;\n            label: \"fast\";\n        }\n        Button    // custom Button from button.qml\n        {\n            delay: 100000;\n            label: \"medium\";\n        }\n        Button    // custom Button from button.qml\n        {\n            delay: 300000;\n            label: \"slow\";\n        }\n    }\n}\n```\n\n对于本例，您可以加载本书附带的源项目，也可以创建一个新的 Qt Quick 项目，并使用此代码构建一个按钮和一个主视图。 现在构建并运行该程序。 您应该会看到如下所示：一个窗口，其中有三个标记为快速、中等和缓慢的按钮：\n\n![](img/bb217c21-1224-4220-a25c-e8b7ceae9c83.png)\n\n执行以下步骤以分析应用的性能：\n\n1.  通过单击左面板上的 Build 按钮构建应用。\n2.  从分析窗口中选择 QML Profiler。 应用将启动，Qt Creator 将切换到 Analyze 视图。\n3.  单击每个按钮几次。 预计在您单击按钮后会等待。\n4.  退出应用并查看 QML Profiler 窗口。\n\nQML Profiler uses TCP/IP to make a connection between the running application and the profiler, by default, on port `3768`. You might have to tinker with your host's firewall settings in order to get things to work correctly. On Windows, be sure to permit the connection in the Windows Firewall dialog that appears.\n\n以下屏幕截图显示了应用运行后的分析视图：\n\n![](img/9188e092-94e0-4c13-adbe-87abc42dfe09.png)\n\nQML Profiler 有以下三个选项卡，默认情况下显示第一个选项卡：\n\n*   第一个选项卡是 Timeline，它指示在应用的哪个时间点发生了什么，以及花费了多长时间。\n*   第二个选项卡 Flame Graph 列出了 QML 应用处理的事件以及每个事件花费的时间。\n*   第三个选项卡(Statistics)列出了程序在运行时遇到的 JavaScript 函数，以及应用在每个函数的总运行中花费的时间。\n\n我已经单击了处理信号行，以展开应用处理的信号。\n\n您可以看到，它处理了一个信号，即`onClicked`信号，总共处理了三次，每次花费的时间在图表上显示为不同的条。 显然，如果这是真正的工作，这里就会有提高性能的机会。\n\n下一个屏幕截图显示了此信息的不同视图，即您的应用的 JavaScript 运行时。 这表明直到数值精度极限，应用将其测量的所有时间都花在按钮的`onClicked`处理程序上-显然，在本例中是性能*热点*：\n\n![](img/1cf12c39-4f15-443d-8c95-d19610fa09ff.png)\n\n从前面的屏幕截图中，我们可以看到，有趣的是，我的 JavaScript 的每个事件都是在这里测量的，包括在按钮被按下时将不透明过滤器放在按钮前面的`$when`子句(所选行下面的三行)。 如果您需要从广义上观察应用中正在发生的事情，那么查看此视图会非常有帮助。\n\n性能极客可能对下一个屏幕截图最感兴趣，因为它显示了 QML 在运行应用时处理每个事件所花费的时间：\n\n![](img/fe3d2d46-d43f-4246-bb20-d94f575e4cac.png)\n\n我们可以看到，`onClicked`处理程序消耗了大部分处理器资源，但也显示了其他内容，例如为视图创建矩形和为按钮状态创建变量绑定。 通常，我们将使用 Statistics 视图来大致了解您的应用中的问题所在，而我们将使用 Flame Graph 视图来聚焦于特定的问题。\n\n在本节中，我们了解了如何通过查看 QML Profiler 中的详细信息来识别 Qt Quick 项目中的问题。 让我们继续下一节，看看如何在 Valgrind 的帮助下轻松检测 Qt 表单项目中的问题。 请注意，Valgrind 只支持 Linux，因此在此之后，我们还将研究如何检测其他操作系统上的问题。\n\n# QtLeakyButton-需要内存帮助的 Qt C++ 应用\n\n**QtLeakyButton**是一个做一件事的应用：显示一个按钮，当单击该按钮时，将分配 512KB 的 RAM。 下面是代码(您可以运行本书附带的示例代码，也可以创建一个 QtGUI 应用，只有一个按钮和一个标签，然后将此代码用于您的`MainWindow`类)。\n\n首先，我们来看一下`mainwindow.h`中的变化：\n\n```cpp\npublic slots: \n    void leakPressed(); \n\nprivate: \n    Ui::MainWindow *ui; \n    int m_count; \n```\n\n之后，我们来看看`mainwindow.cpp`中的更改：\n\n```cpp\nMainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), \n    ui(new Ui::MainWindow), m_count(0) \n{ \n    ui->setupUi(this); \n    connect(ui->leakButton, &QPushButton::clicked, this, &MainWindow::leakPressed); \n} \n\nvoid MainWindow::leakPressed() \n{ \n    void *p = new char[512 * 1024]; \n    m_count++ ; \n    ui->leakCount->setText(QString::number(m_count)); \n} \n```\n\n从代码中，我们可以观察到以下几点：\n\n*   `MainWindow`类有一个整数计数器和一个用于实例化形式的`ui`槽。\n*   `MainWindow`构造函数实例化此窗体，然后将`leakButton`单击的信号连接到`MainWindow::leakPressed`。\n*   `leakPressed`方法只是分配内存并颠簸计数器，用您按下按钮的次数更新计数器。\n\n# 使用 Valgrind 检测 Linux 上的内存泄漏\n\n要使用 Valgrind，我们需要向您的应用添加一个新的运行目标。 要完成此操作，请执行以下步骤：\n\n1.  单击左侧面板中的项目，然后单击运行。\n2.  当您进入运行设置页面时，您将看到 Valgrind 设置界面。 默认设置应该有效。 如果要设置其他设置，请确保您知道要做什么以及要从调试信息中查找什么。\n3.  如果要查找在程序退出之前可能已释放的指针，请在“内存分析选项”下启用“显示可到达和间接丢失的块”。\n\n4.  您还可以要求 Valgrind 通过为 Check for Leaks on Finish 下拉框选择不同的选项(即，仅摘要或完整)来显示应用运行时发生的不同数量的泄漏。 下面的屏幕截图显示了这一点：\n\n![](img/82204b85-6a5c-41ee-befd-24f4dd723214.png)\n\n现在，您可以为您的应用选择 Valgrind 运行目标。 您需要对调试版本执行此操作，因为 Valgrind 需要应用中的调试符号才能生成有意义的报告。 要使用 Valgrind，请启动应用并单击该按钮几次。 Valgrind 进程不断地输出信息，但是大部分输出都是在我们退出应用之后输出的。\n\nValgrind 会生成大量输出，这可能需要一些时间来整理。 我们正在查找泄漏摘要，它指示肯定丢失和间接丢失的字节数。 绝对丢失的块是您分配的未释放的内存；间接丢失的内存是因为被另一个指针引用而引用的指针没有释放而泄漏的内存。\n\n输出将类似于以下代码：\n\n```cpp\nX bytes in 1 blocks are definitely lost in loss record n of m at 0x........: function_name (filename:line number) \n```\n\n`X`表示泄漏的字节数，泄漏块的地址显示在第二行。 记录号表示应用的内存分配器使用的内部记录号，可能对您没有太大帮助。 下面的屏幕截图显示了它的实际效果：\n\n![](img/fa11ff42-7d68-4091-9138-001c000a32c3.png)\n\n我们应该关注应用中的泄漏，因为 Qt 本身也可能有泄漏。 Valgrind 支持抑制文件，这些文件指示应该忽略的泄漏；如果您可以找到并下载针对构建所依据的 Qt 版本的抑制文件，则可以通过修改参数行来包含对抑制文件的引用，如下所示：\n\n```cpp\n-q --tool=memcheck --leak-check=full --leak-resolution=low --suppressions=suppresion.txt ./[your-app-target-name] \n```\n\n# 使用可视化泄漏检测器检测 Windows 上的内存泄漏\n\n由于 Valgrind 只支持 Linux，让我们来看看如何检测 Windows 上的内存泄漏。 对于 Windows 平台，您可以使用的最佳工具是 Visual Studio 本身，它可以免费下载。 您还必须使用 Visual C++ 编译器而不是 MinGW 编译项目，以便 Visual Studio 检漏器正常工作。 然后，您需要下载 Visual Left Detector，这是一个在现有 Visual C++ 检测器之上添加功能的库。 您可以从[https://kinddragon.github.io/vld](https://kinddragon.github.io/vld)下载视觉检漏仪。\n\n安装可视检漏仪后，再次打开 Leaky Button 示例。 这一次，切换到一个支持 Visual Studio 编译器的工具包，然后打开项目文件(`.pro`)并向其中添加以下代码：\n\n```cpp\nINCLUDEPATH += \"C:/Program Files (x86)/Visual Leak Detector/include/\"\nLIBS += -L\"C:/Program Files (x86)/Visual Leak Detector/lib/Win64\" -lvld\n```\n\n前面的代码将把包含可视化泄漏检测器的头文件和库文件的文件夹与您的项目链接起来。 接下来，打开`main.cpp`并包含以下标题：\n\n```cpp\n#include <vld.h>\n```\n\n然后，将以下文件从 Visual 检漏仪安装文件夹复制到应用的 Build 文件夹：\n\n*   `dbghelp.dll`\n*   `Microsoft.DTfW.DHL.manifest`\n*   `vld_x64.dll`\n\n现在，构建并运行该程序，在调试模式下启动应用时，您应该在 Application Output 窗口中看到以下行：\n\n```cpp\nVisual Leak Detector read settings from: C:\\Program Files (x86)\\Visual Leak Detector\\vld.ini\nVisual Leak Detector Version 2.5.1 installed.\n```\n\n如果您看到前面的消息，祝贺您！ 已成功为您的项目实现可视泄漏检测器。 之后，按下泄漏钮。 每次按下按钮时，您将看到该数字仅增加 1`1`，因为视觉检漏仪已检测到泄漏并已停止操作。 现在，您可以关闭您的程序并查看告诉您可能面临的问题的长消息。 如果您逐行仔细查看，您可能会从消息中注意到类似以下内容：\n\n```cpp\nf:\\dd\\vctools\\crt\\vcstartup\\src\\startup\\exe_winmain.cpp (17): QtLeakyButton.exe!WinMainCRTStartup()\nKERNEL32.DLL!BaseThreadInitThunk() + 0x14 bytes\nntdll.dll!RtlUserThread ucrtbased.dll!malloc()\nf:\\dd\\vctools\\crt\\vcstartup\\src\\heap\\new_array.cpp (29): QtLeakyButton.exe!operator new[]()\nc:\\users\\leezh\\desktop\\qtleakybutton\\mainwindow.cpp (19): QtLeakyButton.exe!MainWindow::leakPressed() + 0xA bytes\n```\n\n第一行指示程序已启动。 然后是关键字`malloc`和`new`，这意味着问题与内存分配有关。 之后，我们还可以看到`leakPressed`，它正是导致内存泄漏的函数。 从这里，我们知道`leakPressed`函数出了问题，这与未使用的指针未被清除有关，因此我们可以在代码中修复该问题。 根据您面临的问题，调试消息看起来可能会有所不同，因此请仔细查看这些消息，确保不会遗漏任何重要的细节。\n\n查找应用中的内存泄漏部分是艺术，部分是科学。 在应用开发期间定期检查这是一个很好的练习，以确保在您最熟悉您正在运行的新代码的同时，快速发现您可能引入的漏洞。接下来，我们将继续学习如何使用 QML Profiler 做更多工作并优化我们现有的应用。\n\n# QML Profiler 简介\n\n在上一节*介绍 QML 性能分析*中，我们向您介绍了 QML Profiler 及其基本功能。 在本节中，我们将探索 QML Profiler 还可以提供哪些其他功能来使我们的调试过程更快、更有效。 我们还将学习如何检查 QML Profiler 中显示的数据以及这些数据表明了什么，以便我们可以确定应用速度减慢或崩溃的原因。\n\n为了演示这一点，让我们执行以下步骤：\n\n1.  让我们创建一个新的 Qt Quick 项目，并将`main.qml`代码更改为：\n\n```cpp\nimport QtQuick 2.12\nimport QtQuick.Window 2.12\n\nWindow {\n    id: window\n    visible: true\n    width: 640\n    height: 480\n    title: qsTr(\"Hello World\")\n\n    Component.onCompleted: {\n        for (var i = 0; i < 200; i++)\n        {\n            var x = Math.floor((Math.random() * window.width) + 1);\n            var y = Math.floor((Math.random() * window.height) + 1);\n            Qt.createQmlObject('import QtQuick 2.12; Rectangle \n              {color: \"red\"; width: 20; height: 20; x: ' + x + ';\n               y: ' + y + ';}', window, 'rect');\n        }\n    }\n}\n```\n\n在前面的代码中，我们为`Window`对象设置了一个名为`window`的`id`实例。 当创建`window`图形时，将调用`onCompleted`图形函数，我们告诉程序开始在屏幕上的任意位置生成 200 个矩形图形。 我们使用`Math.random`生成每个矩形的`x`和`y`两个值，随机生成器的范围从`1`到`width`或`height`的值`window`。\n\n2.  如果我们现在运行该程序，我们应该看到如下所示：\n\n![](img/d42add86-c683-4e66-8c83-24396fbdef5e.png)\n\n3.  之后，进入分析界面|QML Profiler 界面，打开 QML Profiler，开始分析应用。 一旦程序启动并生成矩形，您现在就可以关闭它并转到 QML Profiler 界面。\n4.  在访问 Timeline 或 Flame Graph 之前，最好先查看统计数据，因为这是以线性方式获得应用整体性能概览的最快方式：\n\n![](img/d117d10a-0af1-4a09-bc1d-e8b7b6d9dfe5.png)\n\n如你所见，在我的电脑上，`onCompleted`函数完成其任务所需的总时间是 1.45 秒，这是我们在屏幕上产生 200 个矩形时预计的结果。 您可以尝试将`i`的数量增加到 300 或 400，并查看在 QML Profiler 中生成所有这些矩形需要多少秒。 您可以双击列表上的单个项目以查看操作的细分详细信息，以进一步调查。 通过 Statistics 窗口，我们可以很容易地检查应用的性能，并发现降低程序速度的操作。\n\n5.  除此之外，您还可以查看脚本的左侧，以百分比为单位查看您自己的时间数据，该数据也可以在您的统计数据窗口中找到：\n\n![](img/f0e9178d-1c85-4fb1-8f03-5e081d327fdc.png)\n\n6.  接下来，我们来看一看火焰图表的窗口：\n\n![](img/803f7273-05b3-4cdd-bc3e-8bb261f4f822.png)\n\nFlame Graph 窗口主要用于以条形图的形式可视化应用的整体性能，包括编译和创建每个组件所花费的总时间、其内存使用情况以及函数执行的内存分配数量。\n\n7.  现在，让我们进入下一个时间线窗口。 时间线窗口基本上显示的数据与统计数据窗口中的数据相似。 但是，它还按照时间线列出所有操作，告诉您哪些操作发生在什么时间(从程序执行开始)，以及运行任何特定操作需要多长时间。 除此之外，它还显示每个操作的内存使用情况和场景图的渲染持续时间。\n8.  让我们来看看我们的示例程序的时间表，看看我们是否能发现性能问题。 如果你在时间线窗口中滚动，你会看到一大堆五颜六色的条形图，沿着这条线编辑和创建不同的部分。 这些条中的每一个都表示在我们的应用窗口中创建了一个单独的矩形形状：\n\n![](img/66b8e243-5e88-4e7a-b735-b9d79655209e.png)\n\n很快，我们注意到编译时间比创建时间长得多。 创建每个矩形只需要几纳秒，而编译则需要几毫秒。 这是因为我们要求我们的程序在每次尝试创建矩形时加载新的 QML 代码，因此它需要重复编译，即使每个矩形本质上具有相同的属性(除了`x`和`y`)。 在时间线窗口中，我们发现了一个可以进一步改进的性能问题。\n\n9.  要解决这个问题，让我们将矩形的 QML 代码移到一个单独的 QML 文件中，方法是转到文件|新建 QML 文件或项目。然后，在“文件和类”|“Qt”类别下选择“QML 文件(Qt Quick 2)(QML 文件(Qt Quick 2)”)。 之后，打开新创建的文件(这里我将其称为`myRectangle.qml`)，并将代码更改为以下代码：\n\n```cpp\nimport QtQuick 2.12;\n\nRectangle{\n    color: \"red\";\n    width: 20;\n    height: 20;\n    x: 0;\n    y: 0;\n}\n```\n\n前面的代码与我们在`main.qml`中使用的矩形代码基本相同，不同之处在于现在默认情况下将参数`x`和参数`y`的值设置为参数`0`。 在生成单个矩形之后，我们将稍后设置每个`x`和`y`的值。\n\n然后，在`main.qml`中，我们更改`onCompleted`函数中的代码：\n\n```cpp\nComponent.onCompleted: {\n    var component = Qt.createComponent(\"myRectangle.qml\");\n\n    for (var i = 0; i < 200; i++)\n    {\n        var x = Math.floor((Math.random() * window.width) + 1);\n        var y = Math.floor((Math.random() * window.height) + 1);\n\n        component.createObject(window, {x: x, y: y});\n    }\n}\n```\n\n我们不必在每次生成矩形形状时都编译新的 QML 代码，而是现在只需在`for`循环之前编译一次。 我们通过调用`Qt.createComponent`并加载存储矩形的 QML 代码的`myRectangle.qml`来做到这一点。 然后，我们调用`component.createObject`命令来生成矩形，并将其参数`x`和参数`y`的值更改为随机数。`createObject`将从内存中复制已编译的矩形组件，因此不需要额外的编译。\n\n如果我们再次运行 QML Profiler，我们应该会在时间轴窗口中看到完全不同的东西：\n\n![](img/8faaf751-8fe2-49bc-b30b-b6e5801ca778.png)\n\n现在我们只需要 8 毫秒，而不是花费大约 1.6 秒来编译所有的 200 个矩形形状，因为我们只需要编译一次，而不是 200 次。 结果，我们的程序得到了优化，并且执行得更好了！ 尽管 1 秒的改进在这里看起来并不重要，但是随着您的程序的增长，这里和那里会出现越来越多的性能问题，这些问题会滚雪球般地发展成性能噩梦。\n\n# 使用 QML Profiler 执行更多操作\n\n现在，让我们了解如何将 QML 分析器用于除普通 GUI 应用之外的其他应用。 在下面的示例中，我们将尝试使用 Qt Quick 创建一个简单的 3D 渲染器，并使用 QML Profiler 检查其性能。\n\n首先，让我们创建一个空的 Qt Quick 项目。 然后打开`main.qml`，添加以下模块：\n\n```cpp\nimport QtQuick 2.12 as QtQuick2\nimport QtQuick.Scene3D 2.0\nimport Qt3D.Core 2.0\nimport Qt3D.Render 2.0\nimport Qt3D.Extras 2.0\n```\n\n我们添加了以后使用所需的 3D 渲染模块。 我们还为`QtQuick`模块设置了别名`QtQuick2`，稍后我们将在代码中使用它。 之后，让我们删除默认的`Window`项并添加`Entity`。 我们将其称为`Entity`实例`sceneRoot`，它将充当 3D 场景中所有对象的父项。 我们还添加了一个`Camera`实例来设置渲染的视点：\n\n```cpp\nEntity {\n    id: sceneRoot\n\n    Camera {\n        id: camera\n        projectionType: CameraLens.PerspectiveProjection\n        fieldOfView: 45\n        aspectRatio: 16/9\n        nearPlane : 0.1\n        farPlane : 1000.0\n        position: Qt.vector3d(0, 0, -30)\n        upVector: Qt.vector3d(0, 1, 0)\n        viewCenter: Qt.vector3d(0, 0, 0)\n    }\n```\n\n我们将相机定位在`x:0``y:0``z:-30`，然后将其查看目标设置在原点。 之后，添加`ForwardRenderer`，它将场景从摄影机视图渲染到我们的 Qt 窗口中。 我们通过设置`clearColor`属性将背景颜色设置为浅蓝色：\n\n```cpp\n    components: [\n        RenderSettings {\n            activeFrameGraph: ForwardRenderer {\n                clearColor: Qt.rgba(0, 0.5, 1, 1)\n                camera: camera\n            }\n        }\n    ]\n```\n\n下一步则更为复杂。 我们创建另一个`Entity`实例，这一次它将携带一个 3D 立方体模型。 要实现这一点，我们需要三个不同的组件：网格、材质和变换：\n\n```cpp\n    PhongMaterial {\n        id: material\n    }\n\n    CuboidMesh {\n        id: mesh\n        xExtent: 10\n        yExtent: 10\n        zExtent: 10\n    }\n\n    Transform {\n        id: transform\n        property real userAngle: 0.0\n        matrix: {\n            var m = Qt.matrix4x4();\n            m.rotate(userAngle, Qt.vector3d(0, 1, 0));\n            m.translate(Qt.vector3d(0, 0, 0));\n            return m;\n        }\n    }\n\n    Entity {\n        id: entity\n        components: [mesh, material, transform]\n    }\n```\n\n网格本质上是包含所有顶点位置信息的 3D 数据。 在本例中，我们只需要一个简单的立方体模型，所以我们使用了`CuboidMesh`组件，它是一个类似于立方体的内置形状。 材质是三维网格曲面特性的定义，例如颜色、粗糙度和反射率。 在这种情况下，我们只需将标准 Phong 材质应用于 3D 网格。 变换组件是网格的位置、旋转和缩放，由矩阵值表示。 然后，我们将这些组件提供给`Entity`项以形成 3D 对象。\n\n最后，我们使用`NumberAnimation`动画类型无限旋转立方体，这样我们就可以在 3D 场景中看到一些运动。 我们使用开始时创建的`QtQuick2`别名来调用`NumberAnimation`，因为它属于`QtQuick`模块：\n\n```cpp\n    QtQuick2.NumberAnimation\n    {\n        target: transform\n        property: \"userAngle\"\n        duration: 10000\n        from: 0\n        to: 360\n\n        loops: QtQuick2.Animation.Infinite\n        running: true\n    }\n}\n```\n\n在构建我们的项目之前，让我们打开`main.cpp`并将整个`main`函数更改为：\n\n```cpp\n#include <QGuiApplication>\n#include <Qt3DQuickExtras/qt3dquickwindow.h>\n\nint main(int argc, char *argv[])\n{\n    QGuiApplication app(argc, argv);\n    Qt3DExtras::Quick::Qt3DQuickWindow view;\n    view.setSource(QUrl(\"qrc:/main.qml\"));\n    view.show();\n\n    return app.exec();\n}\n```\n\n我们将窗口类更改为`Qt3DQuickWindow`，因为我们不再需要普通的 GUI 窗口。 现在让我们构建并运行该程序。 您应该看到如下所示：\n\n![](img/29ef1f51-34c6-4a5b-a6b1-8be74cf48b7a.png)\n\n要分析该程序，请转到分析|QML Profiler。 从 QML Profiler 窗口，我们可以看到设置 3D 模型需要多长时间，渲染每个帧需要多长时间，等等。 此示例场景非常简单，因此一切看起来都很快，但如果您有一个包含许多高度多边形模型和复杂材质的复杂场景，则会注意到此处的瓶颈：\n\n![](img/021cd4be-63d2-440c-9852-ec988f3430b9.png)\n\n仅此而已-我们不仅使用 QML Profiler 来分析普通 GUI 程序的性能，而且这次我们还学习了如何使用 QML Profiler 来分析 3D 渲染器。\n\n接下来，我们将学习如何在我们的 Qt 应用中实现测试集成。\n\n# 实施测试集成\n\n单元测试是应用开发过程中的一个非常重要的阶段，但往往被开发人员，尤其是初学者忽视。 单元测试可确保应用的质量达到标准，并改善用户体验。 单元测试的方法之一是将自动测试集成到项目中。 在本节中，我们将学习如何在 Qt Creator 中实现不同类型的自动测试。\n\nQt 测试框架由两部分组成-Qt 测试和 Qt 快速测试，它们测试 C++、QML 和 GUI 特性，而其他两个框架只测试 C++ 特性。 选一个最适合你的项目。 我们将在这里逐一研究它们。\n\n# 创建 Qt 和 Qt 快速测试\n\nQt 和 Qt 快速测试内置于 Qt Creator 中，因此您不需要在项目中安装任何第三方组件。 按照下面给出的步骤了解如何创建 Qt 或 Qt 快速测试：\n\n1.  通过转到文件|新建文件或项目来创建新项目。\n2.  在“其他项目”类别下选择“自动测试项目”。\n3.  在 Project and Test Information(项目和测试信息)对话框中，为 Test framework 选项选择 Qt Test(Qt 测试)或 Qt Quick Test(Qt 快速测试)。\n4.  Qt Test 下还有两个额外的选项，分别是 GUI 应用和需要 QApplication 的选项。 如果您正在构建一个图形用户界面应用，您可以选中这两个选项。\n5.  在此之后，填写测试用例名称-任何东西都可以。\n6.  选择您的构建系统。 如果您不确定要做什么，请将其保留为 qmake。 只有在使用其他构建系统(如 CMake 或 QBS)时，才需要更改此选项。\n7.  按 Next(下一步)并完成该过程的其余部分。 下面的屏幕截图显示了这一点：\n\n![](img/01ecf896-7d08-415a-9588-ea07b83f1b81.png)\n\n创建项目后，打开`.pro`文件。 您将看到一些与我们在前面所有示例项目中使用的普通项目文件完全不同的内容：\n\n```cpp\nQT += testlib\nQT += gui\nCONFIG += qt warn_on depend_includepath testcase\n```\n\n如您所见，此项目默认附带`testlib`模块。 此模块包含我们在本节后面执行测试所需的所有功能。 在配置部分中，我们还包括了`warn_on`、`depend_includepath`和`testcase`，这对我们来说都是新的：\n\n*   `warn_on`：此选项告诉编译器输出尽可能多的警告。 我们需要此功能来显示自动测试期间发生的任何问题。\n*   `depend_includepath`：此选项告诉 Qt 追加`INCLUDEPATH`和`DEPENDPATH`的值，以便正确加载所有依赖项。\n*   `testcase`：我们必须在我们的项目中包含此选项，以指示它是自动测试。 在运行测试之前，检查目标将被添加到 Makefile。 如果没有此选项，Qt Creator 将不会将应用作为自动测试执行。\n\n之后，打开项目目录中的 cpp 文件。 CPP 文件中的类以您在创建项目期间插入的测试用例名称命名。 在类下为您创建了三个槽函数-`initTestCase`、`cleanupTestCase`和`test_case1`。\n\n*   `initTestCase`：这是一个内置插槽，不会被视为测试函数。 此函数将在测试开始前调用。 您可以使用它来初始化测试所需的变量或指针。\n*   `cleanupTestCase`：顾名思义，此函数用于在执行完所有测试后清除变量或指针。\n*   `test_case1`：这是默认情况下为您创建的示例测试函数。 您可以自己创建更多的槽函数，所有这些函数都将被视为测试函数。 您可以创建的测试函数数量没有限制，您可以对测试函数使用任何名称，只要它们不与`initTestCase`、`cleanupTestCase`、`init`和`cleanup`冲突。\n\n除了`initTestCase`和`cleanupTestCase`之外，还有另外两个槽函数`init`和`cleanup`不被视为测试函数；它们在前面已简要说明：\n\n*   `init`：与`initTestCase`不同，此槽函数在每个测试函数之间调用。 如果此功能失败，将跳过下面的测试功能，测试将进入下一个测试功能。\n*   `cleanup`：这类似于`cleanupTestCase`，但在每个测试函数之后运行。\n\n让我们来看看一些简单的代码，以及如何创建我们自己的自动测试：\n\n```cpp\nvoid Testing::test_case1()\n{\n    QString str = \"Testing\";\n    QVERIFY(str.toUpper() == \"TESTING\");\n}\n```\n\n如果我们现在运行该应用，我们应该看到类似以下内容：\n\n```cpp\n********* Start testing of Testing *********\nConfig: Using QtTest library 5.13.1, Qt 5.13.1 (x86_64-little_endian-llp64 shared (dynamic) debug build; by GCC 7.3.0)\nPASS : Testing::initTestCase()\nPASS : Testing::test_case1()\nPASS : Testing::cleanupTestCase()\nTotals: 3 passed, 0 failed, 0 skipped, 0 blacklisted, 1ms\n********* Finished testing of Testing *********\n```\n\n我们使用`QVERIFY`进行测试。 如果结果为`true`，则测试将通过。 否则，测试将失败。 现在，让我们更改代码中的某些内容，使其失败：\n\n```cpp\nvoid Testing::test_case1()\n{\n    QString str = \"Testing1234\";\n    QVERIFY(str.toUpper() == \"TESTING\");\n}\n```\n\n现在，让我们再运行一次程序。 现在，您应该会看到显示失败状态：\n\n```cpp\n********* Start testing of Testing *********\nConfig: Using QtTest library 5.13.1, Qt 5.13.1 (x86_64-little_endian-llp64 shared (dynamic) debug build; by GCC 7.3.0)\nPASS : Testing::initTestCase()\nFAIL! : Testing::test_case1() 'str.toUpper() == \"TESTING\"' returned FALSE. ()\n..\\autotest\\tst_testing.cpp(44) : failure location\nPASS : Testing::cleanupTestCase()\nTotals: 2 passed, 1 failed, 0 skipped, 0 blacklisted, 2ms\n********* Finished testing of Testing *********\n```\n\n您可以使用`QVERIFY`检查特定函数或变量。 只要结果返回`true`，测试就会通过并继续进行。 除了`QVERIFY`，您还可以使用`QCOMPARE`比较两个值。 让我们创建第二个测试函数，并在其中使用`QCOMPARE`：\n\n```cpp\nclass Testing : public QObject\n{\n    Q_OBJECT\npublic:\n    Testing();\n    ~Testing();\nprivate slots:\n    void initTestCase();\n    void cleanupTestCase();\n    void test_case1();\n    void test_case2();\n};\n\nvoid Testing::test_case2()\n{\n int a = 10;\n QCOMPARE(a, 10);\n}\n```\n\n`QCOMPARE`的工作方式与`QVERIFY`非常相似。 唯一的区别是它只接受两个输入，而不是一个。\n\nTo learn more about the functions and macros provided by the QTest namespace, please check out the documentation at [https://doc.qt.io/qt-5/qtest.html](https://doc.qt.io/qt-5/qtest.html).\n\n# 使用 QSignalSpy 测试信号和插槽\n\n我们还可以使用`QSignalSpy`类测试程序中的信号和插槽。 `QSignalSpy`是 Qt 提供的一个类，用于连接到任何物体的信号并记录其发射。 `QSignalSpy`然后将把信号的参数附加到`QVariant`列表中进行单元测试。\n\n让我们继续前面的代码，并添加一个名为`test_signalslot`的新槽函数：\n\n```cpp\nprivate slots:\n    void initTestCase();\n    void cleanupTestCase();\n    void test_case1();\n    void test_case2();\n    void test_signalslot();\n```\n\n接下来，在`Testing`类的构造函数中，我们创建一个新的`QObject`，并将其`objectNameChanged`信号连接到`test_signalslot`槽函数。 我们还将`objectNameChanged`信号连接到`QSignalSpy`对象，以便它可以记录发射。 之后，我们通过更改对象名称来触发信号，如下所示：\n\n```cpp\nTesting::Testing()\n{\n    QObject* object = new QObject(this);\n    connect(object, &QObject::objectNameChanged, this, \n      &Testing::test_signalslot);\n    spy = new QSignalSpy(object, &QObject::objectNameChanged);\n    object->setObjectName(\"New Name\");\n}\n```\n\n然后，我们实现`test_signalslot`函数。 首先，我们检查信号是否已经发出。 我们还考虑了`QSignalSpy`记录的所有参数，并检查第一个参数是否为`\"New Name\"`，这样我们就可以验证该信号就是我们刚才发出的信号：\n\n```cpp\nvoid Testing::test_signalslot()\n{\n    QCOMPARE(spy->count(), 1);\n    QList<QVariant> arguments = spy->takeFirst();\n    QVERIFY(arguments.at(0).toString() == \"New Name\");\n}\n```\n\n现在构建并运行该程序，您应该会看到如下所示：\n\n```cpp\n********* Start testing of Testing *********\nConfig: Using QtTest library 5.13.0, Qt 5.13.0 (x86_64-little_endian-llp64 shared (dynamic) debug build; by GCC 7.3.0)\nPASS : Testing::initTestCase()\nPASS : Testing::test_case1()\nPASS : Testing::test_case2()\nPASS : Testing::test_signalslot()\nPASS : Testing::cleanupTestCase()\nTotals: 5 passed, 0 failed, 0 skipped, 0 blacklisted, 1ms\n********* Finished testing of Testing *********\n```\n\n您可以更改`QCOMPARE`和`QVERIFY`中的值，以查看如果单元测试失败会发生什么情况。 如果您正在测试没有立即发出的定时器信号，您可以使用`wait`函数要求 Qt 在验证之前等待几毫秒：\n\n```cpp\nQVERIFY(spy->wait(1000));\n```\n\n就这样。 我们已经学习了如何利用 Qt 和 Qt 快速测试来自动检查我们的代码，并确保质量得到适当的维护。 我们还了解了如何通过`QSignalSpy`对 Qt 应用中的信号和槽进行单元测试。\n\nTo learn more about the `QSignalSpy` class, please visit [https://doc.qt.io/qt-5/qsignalspy.html](https://doc.qt.io/qt-5/qsignalspy.html).\n\n接下来，我们将转向第三方自动测试套件，并使我们的测试集成更加多样化和更好。\n\n# 增加了对测试集成的更好支持\n\n尽管 Qt 和 Qt Quick Testing 都是非常好的自动测试框架，但我们也可以集成其他一些第三方单元测试框架来测试不同的问题。 您还可以比较来自不同框架的结果，以确保没有误报，并确保您的产品质量处于最佳状态。 除了他们自己的 Qt 测试框架外，Qt Creator 还在编辑器中集成了其他几个不同的自动测试套件，如 Google C++ 测试框架和 Boost.Test，用于自动化单元测试。\n\n首先，我们将学习如何将 Google Test 集成到我们的项目中。\n\n# 创建 Google 测试\n\n在我们开始设置谷歌测试之前，让我们从 GitHub 链接下载谷歌 C++ 测试框架：[https://github.com/google/googletest](https://github.com/google/googletest)。 单击“克隆”或“下载”按钮，然后单击“下载 ZIP：\n\n![](img/0079b5c7-d1de-4ab3-a449-4f17ff1a6b6a.png)\n\n拥有所有必需的文件后，将这些文件解压缩到 PC 上的一个目录中，然后按照以下步骤创建一个测试项目：\n\n1.  通过转到“文件”|“新建文件”或“项目”来创建新项目。\n2.  在“其他项目”类别下选择“汽车测试项目”。\n3.  在 Project and Test Information(测试项目和测试信息)对话框中，为测试框架(Test Framework)选项选择`Google Test`选项。\n4.  之后，填写“测试套件名称”和“测试用例名称”字段。\n5.  如果要在测试中支持 C++ 11 功能，请选中“启用 C++ 11”复选框。\n6.  对于 Google 测试存储库字段，选择您刚刚从 GitHub 下载的文件解压的目录-例如，*`C:\\googletest-master`。\n7.  选择您的虚拟构建系统。 如果您不确定要做什么，请将其保留为 qmake。 只有在使用某些其他构建系统(如 CMake 或 QBS)时，才需要更改此选项。\n8.  按下下一步，完成剩下的流程。\n\n一旦创建了项目，您将看到我们刚才介绍的项目向导已经为您设置了几项内容。 如果打开`gtest_dependency.pri`，可以看到已经为您设置了`INCLUDEPATH`、`SOURCES`等设置。 包含测试函数的实际源文件位于*`tst_testscene.h`，如下所示：\n\n```cpp\n#ifndef TST_TESTCASE_H\n#define TST_TESTCASE_H\n\n#include <gtest/gtest.h>\n#include <gmock/gmock-matchers.h>\n\nusing namespace testing;\n\nTEST(TestSuite, TestCase)\n{\n    EXPECT_EQ(1, 1);\n    ASSERT_THAT(0, Eq(0));\n}\n\n#endif // TST_TESTCASE_H\n```\n\n与 Qt Test 类似，Google Test 也使用诸如`EXPECT_EQ`和`ASSERT_THAT`等宏来进行测试。 他们是这样做的：\n\n*   `EXPECT_EQ`：执行简单真/假条件测试的非致命断言，类似于`QCOMPARE`。 检测到故障时，任何以“`EXPECT_`”开头的宏都不会终止自动测试。 终端显示故障后进行测试。\n*   `ASSERT_THAT`：*此宏允许执行复杂得多的测试。 第一个输入是要用此断言测试的值，而第二个输入是测试的条件。 在前面的示例中，断言测试值`0`，条件等于`0`，这可以由*`Eq(0)`表示。 你可以用它来比较复杂得多的变量，比如地图或向量--例如，`ASSERT_THAT(v, ElementsAre(5, 10, 15));`。而关键字`ASSERT_`告诉我们，这是一个致命的断言，这意味着当检测到失败时，测试将立即停止。\n\n如果您现在构建并运行该项目，您应该会看到类似如下的结果：\n\n![](img/cca98eaf-d68f-4662-b6ec-b59f87d3ea64.png)\n\nTo learn more about the other macros available in Google C++ Testing Framework, visit [https://github.com/google/googletest/blob/master/googletest/docs/primer.md](https://github.com/google/googletest/blob/master/googletest/docs/primer.md).\n\n现在，让我们看看 Boost 测试是如何工作的。\n\n# 创建助推测试\n\n最后，我们将学习如何创建 Boost 测试。 顾名思义，Boost 测试需要一组名为 Boost 的第三方 C++ 库。 你可以从 Boost 官网下载，网址是：[https://www.boost.org](https://www.boost.org)。 下载完 ZIP 文件后，将其解压到您 PC 上的某个位置，因为我们稍后会用到它。 完成后，检查以下步骤以创建一个测试项目：\n\n1.  通过转到“文件”|“新建文件”或“项目”来创建新项目。\n2.  在“其他项目”类别下选择“汽车测试项目”。\n3.  在 Project and Test Information(测试项目和测试信息)对话框中，为 Test Framework(测试框架)选项选择 Boost Test(增强测试)。\n4.  之后，填写“测试套件名称”和“测试用例名称”字段。\n5.  然后，转到您解压下载的 Boost 目录，然后将目录路径复制到 Boost Include dir(可选)字段-例如，*`C:\\boost_1_71_0`。\n6.  选择您的虚拟构建系统。 如果您不确定要做什么，请将其保留为 qmake。 只有在使用某些其他构建系统(如 CMake 或 QBS)时，才需要更改此选项。\n7.  按下下一步，完成剩下的流程。\n\n创建项目后，打开`main.cpp`：\n\n```cpp\n#define BOOST_TEST_MODULE TestSuite\n#include <boost/test/included/unit_test.hpp>\n\nBOOST_AUTO_TEST_CASE( testCase )\n{\n    BOOST_TEST( true /* test assertion */ );\n}\n```\n\n我们可以看到不同单元测试套件之间的相似之处。 Boost 也使用宏，尽管它们以`BOOST_`开头，而且它的工作原理也非常类似于 Qt Test 和 Google Test。 您将使用的最常见的宏是`BOOST_TEST`，它适用于大多数测试用例。 例如，我们可以按如下方式更改`BOOST_AUTO_TEST_CASE`函数：\n\n```cpp\nBOOST_AUTO_TEST_CASE( testCase )\n{\n    int a = 5;\n    int b = 6;\n    BOOST_TEST(a != b - 1);\n}\n```\n\n正如您从前面的代码中看到的，我们有意将其设置为失败测试，只是为了看看应用的结果是什么。 现在让我们构建并运行该程序。 您应该会看到类似以下屏幕截图的内容：\n\n![](img/d9555709-88d5-49c2-82f6-7b2e226e00f9.png)\n\n除此之外，Boost 还支持不同严重级别的断言：\n\n*   `BOOST_TEST_WARN`：最低严重级别。 如果测试执行失败，这甚至不会增加错误计数。 如果当前测试失败，自动测试将继续执行下一个测试功能。\n*   `BOOST_TEST_CHECK`：中等严重级别。 这实际上与我们在上一个示例中使用的`BOOST_TEST`相同。 如果此测试执行失败，则会增加错误计数，但不会阻止自动测试继续进行。\n\n*   `BOOST_TEST_REQUIRE`：最高严重级别。 此断言既会增加错误计数，又会在测试返回失败结果时停止自动测试。\n\nThere are plenty of other macros that you can use to test various types of test cases. To learn more about this, please visit the Boost documentation at [https://www.boost.org/doc/libs/1_71_0/libs/test/doc/html](https://www.boost.org/doc/libs/1_71_0/libs/test/doc/html).\n\n在本节中，我们了解了如何使用各种类型的单元测试套件设置有效的自动测试。\n\n# 简略的 / 概括的 / 简易判罪的 / 简易的\n\nQt Creator 提供了 QML 分析器，它允许您执行 Qt 应用的运行时分析。 您可以看到应用如何运行的图形(在时间上)，还可以深入了解有关应用如何花费时间绘制、绑定到变量和执行 JavaScript 的详细信息。\n\nQt Creator 还可以很好地与 Linux 上的 Valgrind 集成，让您可以查找应用中的内存泄漏。 在 Linux 上使用 Valgrind，您可以查看已分配但未释放的块，更重要的是，可以查看它们有多大，以及它们在代码中的分配位置，这让您在确定它们未被释放的原因方面领先一步。 因为 Valgrind 只能在 Linux 上运行，所以我们还讨论了另一个内存泄漏检测器，称为 Visual Leak Detector，它在 Windows 上工作得很好。\n\n除此之外，我们还学习了如何使用 Qt Test、Google Test 和 Boost 测试实现自动化单元测试，以帮助提高应用的质量。 说到质量，我们还了解了如何使用 QML Profiler 通过检查内存使用和代码效率来提高应用的性能。\n\n在下一章中，我们将把注意力从 Qt Creator 的特定部分转移到它最激动人心的方面之一：使用 Qt Creator 编译和测试 Android 等移动平台上的应用的能力。"
  },
  {
    "path": "docs/app-dev-qt-creator/12.md",
    "content": "# 十二、使用 Qt Creator 开发移动应用\n\nQt 和手机开发由来已久。 Qt 的开端包括 90 年代末和本世纪初 Linux 个人数字助理的早期版本。 从那时起，它已经移植到了许多移动环境中，包括诺基亚发布的 Linux 的移动版本，如 MeeGo 和 Symbian。 虽然 Symbian 和 MeeGo 来来去去，但 Qt 对移动平台的接受依然存在，最近的一次是对 Android 的支持。\n\n在本章中，我们将简单介绍一下如何编写移动应用，然后学习如何设置 Qt Creator 来编写 Android 应用。 值得注意的是，虽然我们将在开发移动应用时利用您所学到的有关 Qt 开发的所有知识，但我们还需要了解移动软件运行的环境与传统台式机和笔记本电脑环境的不同之处，以及如何设计这些约束。 一旦我们了解了这些不同之处，用 Qt 为 Android 等移动平台编写软件就像打个响指一样简单！\n\n我们将在本章介绍以下主题：\n\n*   了解移动软件开发\n*   为 Android 设置 Qt Creator\n*   将应用部署到 Android 设备\n*   为 iOS 设置 Qt Creator\n*   改进对 iOS 和 Android 应用的支持\n\n# 技术要求\n\n本章的技术要求包括 Qt 5.12.3`arm64-v8a`、Qt Creator 4.9.0 和 Windows 10。\n\n# 了解移动软件开发\n\n在为任何移动平台(如手机或平板电脑)开发软件时，需要记住的关键一点是，每种资源都是溢价的。 该设备较小，这意味着：\n\n*   您的用户将较少关注您的应用，并在较短的时间内使用它。\n*   屏幕更小了，所以你可以在显示屏上显示更少的信息(不要被今天的显示器的高点距所愚弄；在 4 英寸的显示器上阅读六点字体并不好玩，不管像素密度高不好。)\n*   处理器和图形处理单元速度较慢。\n*   内存更少，图形内存也更少。\n*   应用数据的持久化存储较少。\n*   网络速度更慢，最高可达三个数量级。\n\n让我们更详细地看一下其中的每一个。\n\n# 用户关注度非常高\n\n你能一边走路一边嚼口香糖吗？ 我不能，但很多人一边走路，一边嚼口香糖，一边使用他们的移动设备(更糟糕的是，有些人甚至一边开车一边使用他们的设备)。 手机或平板电脑上的应用一次只吸引用户 100%的注意力超过几分钟，这是非常罕见的。 一个很好的经验法则是，设备越小，用户就越有可能在做其他事情时把它当作可以拿起来看一眼或使用的东西。\n\n您的用户对您的应用的关注有限有三个主要后果：\n\n*   **您的应用必须快速**：移动设备不能放置额外的进度条、旋转光标或冗长的闪屏。\n*   **您的应用必须简洁**：最好的移动应用只在一两个页面上显示数据，具有非常扁平的导航层次结构。 一种常见的结构是有一个带有信息的单一屏幕和一个带有首选项的单一屏幕，这些首选项允许您配置应该显示哪些信息(例如，您从中获取信息的位置)。 喜欢清晰的图标而不是冗长的文字--如果你不会画画，找一个会画的人，或者从 Noun Project([http://thenounproject.com/](http://thenounproject.com/))这样的网站上买图标。\n\n*   **你的应用必须是可访问的**：按钮应该很大(一个很好的指导原则是，你的应用中的任何点击目标都不应该小于你的指垫，大约一平方厘米)，如果可能的话，文本应该更大。\n\n出于这些原因，Qt Quick 对于您将要编写的大多数移动应用是更好的选择。 您可以创建流畅、响应迅速的应用，这些应用在视觉上令人愉悦，并且不会让您的用户不知所措。\n\n# 计算资源非常宝贵。\n\n移动设备必须随身携带电源：电池。 虽然电池在过去 20 年里有所改进，但它们并没有跟上摩尔定律；大部分改进都是在处理器方面做出的，因为处理器变得更小，在正常运行过程中散热更少。\n\n尽管如此，移动设备还是不如台式机或笔记本电脑快。 思考这一问题的一个好方法是，上一代的处理器设计可能对今天的移动设备具有很好的伸缩性。 这并不是说移动设备速度慢，只是说它们速度更慢。 需要考虑的同样重要的一点是，您无法在不严重影响电池寿命的情况下全速运行处理器或图形处理器。\n\nQt，特别是 Qt Quick，针对低功耗进行了优化，但您仍然可以做以下几件事来帮助您的移动应用获得最佳性能：\n\n*   **不要投票**：这可能是最重要的一点。 尽可能使用 Qt 的异步信号槽机制，如果需要在后台执行某些操作，请考虑使用`QThread`和 Qt 多线程环境的其余部分进行多线程。 您的应用休眠时间越长，电池续航时间就越长。\n*   **避免不必要的动画**：在当今的应用中，一定数量的动画既是惯例，也是重要的；精心设计的动画可以帮助用户了解它们在应用用户界面中的位置以及它们的去向。 然而，不要仅仅为了看到像素移动而闪烁、眨眼或以其他方式设置动画；在幕后，必须进行大量操作才能移动这些像素，这会消耗电池。\n*   **明智地使用网络**：大多数移动设备至少有两个无线电(蜂窝和 Wi-Fi)；有些甚至更多。 访问网络应该被视为一种必要的罪恶，因为无线电在发送和接收数据时会消耗电能。 另外，别忘了数据解析：如果您要解析大量数据，很可能会全速运行 CPU 来完成繁重的任务，这意味着电池续航时间较短。\n\n接下来，让我们来看看网络资源是如何影响设备的。\n\n# 网络资源非常宝贵\n\n你已经被警告过使用网络的电池费用很高。 雪上加霜的是，大多数移动设备运行的网络速度可能比台式机慢三个数量级；你的办公室台式机可能有千兆位以太网，但在世界上许多地方，每秒一兆位被认为是很快的。 随着网络运营商在各地部署蜂窝无线网络，如**长期演进**(**LTE**)和 Wi-Fi 热点，这种情况正在迅速改善，但这些网络绝不是统一可用的。 在最近一次去加州的旅行中，在 8 个小时的时间里，我的蜂窝网络连接吞吐量比我的有线调制解调器(运行速度为每秒 25 兆位)快了很多，低到令人恐惧的每秒兆位，这可能会让一个大网页爬行。\n\n对于大多数应用，您应该可以使用**Hypertext Transfer Protocol**(**HTTP**)；Qt 的`QNetworkAccessManager`类实现了 HTTP 和 HTTPS，使用 HTTP 意味着您可以构建 Web 服务来以标准方式支持您的后端。\n\n如果您正在开发游戏或自定义应用，则可能需要构建一个自定义协议。 考虑使用`QTcpSocket`或`QUdpSocket`作为您的网络协议，当然要记住 TCP 是一种可靠的协议。 但是，使用 UDP 不能保证您的数据到达目的地；可靠性取决于您。\n\nSomething to make a special note of is error handling in networked applications. Unlike a desktop, where network failures are likely to be rare because your computer is tethered to the network, wireless networks can suffer all sorts of transitory problems. These don't necessarily lead to logical failures; a short drop in network connectivity can result in **Domain Name Service** (**DNS**) problems, **Transport Layer Security **(**TLS**) timeouts, or retry timeouts.\n\nHandle errors in your application, and ensure that there are mechanisms to retry important network operations, such as data synchronization and content uploads. Be prepared for duplicate requests and uploads too, in cases where your device uploads something to a server, but doesn't get an acknowledgment from the server because of a network problem, and so tries again.\n\n接下来，我们来看看存储资源是如何工作的。\n\n# 存储资源非常宝贵\n\n移动设备通常都使用固态存储器。 尽管固态存储器的价格在过去几年里大幅下降，但它仍然没有构成大多数台式机和许多笔记本电脑磁盘驱动器的旋转磁存储器那么便宜。在过去的几年里，固态存储器的价格已经大幅下降，但它仍然没有构成大多数台式机和许多笔记本电脑磁盘驱动器的旋转磁存储器便宜。 因此，移动设备可能只有 8 GB 的闪存用于永久存储，或者如果你幸运的话，16 或 32 GB。 这是在系统和所有应用之间共享的；您的应用最多不应该使用超过几千兆字节的空间，而且只有在您的用户期望使用的情况下，比如播客应用才会使用。 这应该是应用大小的总和：它的静态资源，如音频和视频，以及它可能从网络下载和缓存的任何内容。\n\n同样重要的一点是，您的应用的运行时大小需要更小。 大多数移动设备有半 GB 到 2 GB 的动态 RAM 可用；系统在所有正在运行的应用之间共享这些内存，因此重要的是只分配您需要的，并在完成后释放它。\n\n最后，别忘了你的图形纹理和东西也会消耗宝贵的 GPU 内存。 当 Qt 为您管理 GPU 时，无论您使用的是 Qt 还是 Qt Quick，您都可以编写一个消耗设备所有纹理内存的应用，从而使本机操作系统很难或不可能在需要使用另一个应用或系统消息中断您的应用时呈现所需的内容。\n\n接下来，让我们讨论一下是否应该将我们的应用移植到不同的平台。\n\n# 去港口还是不去港口？\n\n套用这位不朽的吟游诗人的话，这就是问题所在。 由于 Qt 在众多平台上具有难以置信的灵活性，抢占现有应用并将其移植的诱惑可能是不可抗拒的，特别是在垂直市场中，您有一款用 Qt 编写的桌面定制软件，并且有一位客户想要*相同的东西*用于他们的移动员工的最新移动设备。 一般来说，我能提供给您的最好建议是避免移植 UI，只在移动设备上表现良好的应用中移植业务逻辑。\n\n从台式机或笔记本电脑环境移植的 UI 在移动设备上很难正常工作。 用户的操作模式太不同了：一个人坐在台式机或笔记本电脑前想做的事情，与他们站起来、四处走动或在会议室、食堂或咖啡馆里短暂冲刺时想做或能做的事情完全不同。 如果你正在从一台移动设备移植到另一台移动设备上，情况可能不会那么糟糕；例如，一个开发了 Android 版 Qt 应用的开发人员将他们的应用移植到 iOS 上应该不会有太大的困难。\n\n假设移植业务逻辑不会大量使用 CPU、网络或动态或静态存储，那么移植业务逻辑可能是更安全的选择。 Qt 通过`QtSql`为 SQLite 提供了包装器，许多企业应用将其用于本地存储。 这是一种合理的数据存储替代方案，而且大多数基于 HTTP 的网络应用在网络层上应该不会太难，只要它们有合理的缓存策略，并且不会太频繁地请求数据。 但是，如果应用使用大量存储或具有持久的网络连接，则需要重新架构和重写。\n\n# 一句关于测试的话\n\n测试任何应用都很重要，但移动应用需要额外的测试工作，尤其是 Android 应用。 市场上有各种各样的设备可供选择，用户希望您的应用在他们可能拥有的任何设备上都能很好地运行。\n\n如果您对商业发布应用感兴趣，那么您可以做的最重要的事情就是在真实的设备上测试您的应用--尽可能多地测试您能接触到的设备。 虽然 Qt Creator 使用的 Android SDK 附带了可以在台式机或笔记本电脑上运行 Android 应用的仿真器，但在仿真器上运行并不能替代在设备上运行。 很多东西都不一样，从硬件本身的尺寸到触摸屏，当然还有网络连接和原始处理能力。\n\n幸运的是，安卓设备并不是特别贵，而且市面上的安卓设备非常多。 如果你刚刚起步，eBay 或 Google Play 商店可能是一个购买便宜的二手或新设备的好地方。 如果你是一名学生或初露头角的企业家，别忘了很多家庭成员可能都有一部 Android 设备可以借给你；或者，你也可以使用你已经拥有的 Android 手机。\n\n你应该在什么时候测试什么？ 经常，还有所有的事！ 在一个为期数周的项目中，离在设备上运行的生成的时间永远不应该超过几天。 你花在编写没有在设备上测试的代码上的时间越长，你就会对设备的性能做出越多的假设，而这些假设中的许多都会被证明是错误的。\n\n请确保不仅在良好的环境下测试您的应用，也要在糟糕的环境下测试您的应用。 网络连接就是一个很好的例子；您应该在没有网络覆盖的情况下测试错误处理。 如果你工作的地方有良好的网络覆盖，你可以使用的一个诀窍是把设备放在金属饼干罐或油漆罐里；这种金属会衰减信号，并与现实世界中(比如在隧道或地铁中)失去的信号具有相同的效果。\n\n在下一节中，我们将学习如何设置我们的 Qt Creator 来创建 Android 应用。\n\n# 为 Android 设置 Qt Creator\n\nAndroid 的功能是以 API 级别划分的；Qt for Android 支持 Android 级别 16 及更高级别：即 Android 4.1，它是姜饼的变体。 幸运的是，今天市场上的大多数设备至少是棉花糖(Android 6.0)，这使得 Qt for Android 成为数百万设备的可行开发平台。\n\nQt 不需要 Java 编程语言来开发 Android 应用，因为它使用 Android NDK 工具集，该工具集支持开箱即用的 C++。 您可以像往常一样编写 C++ 或 QML 代码，而不必担心其他任何事情，因为 Qt 会为您处理这些问题。\n\n不用再费劲了，让我们开始下载项目所需的所有组件。\n\n# 正在下载所有的片段\n\n要开始使用 Qt Creator for Android，你需要下载很多东西。 让我们开始吧：\n\n1.  从发布适用于 Android 的 Qt 开始。 如果它不是您在[第 1 章](01.html)，*Qt Creator*快速入门中下载的 Qt 安装的一部分，您需要返回并从[https://www.qt.io/download](https://www.qt.io/download)下载它。\n\n2.  Android 开发人员工具需要当前版本的**Java Development Kit**(**JDK**)(不仅是运行时、Java Runtime Environment，而且是整个工具包和堆)；您可以从[http://www.oracle.com/technetwork/java/javase/downloads/jdk7-downloads-1880260.html](http://www.oracle.com/technetwork/java/javase/downloads/jdk7-downloads-1880260.html)下载。\n3.  您需要最新的 Android**软件开发工具包**(**SDK**)，它与 Android Studio 安装包一起提供。 您可以从[https://developer.android.com/studio](https://developer.android.com/studio)下载适用于 MacOSX、Linux 或 Windows 的 Android Studio。\n4.  您需要最新的 Android Native Development Kit(NDK)，可以从[https://developer.android.com/ndk/downloads](https://developer.android.com/ndk/downloads)下载。\n5.  与旧版本不同，你不再需要安装 Ant 来构建你的 Android 应用。 取而代之的是，Qt 使用与 Android SDK 一起提供的 Gradle Build 系统。\n\n按照给定的顺序下载、解压缩并安装其中的每一个。 在 Windows 上，我通过安装 Android Studio 来安装 Android SDK，然后通过将其解压缩到我的硬盘驱动器根目录来安装 NDK。 最后，我在提供的默认位置安装了 JDK。\n\n# 设置环境变量\n\n安装 JDK 后，需要确保已将`JAVA_HOME`环境变量设置为指向安装它的目录，以便 Qt Creator 可以自动检测 JDK 目录。 否则，您必须在 Qt Creator 中手动设置目录路径，稍后我也会解释。\n\n具体操作方式因平台而异；在 MacOS X 或 Linux 机器上，您可以编辑`.bashrc`、`.tcshrc`等；在 Windows 上，请转到系统属性，单击环境变量，然后添加`JAVA_HOME`变量。 路径应该指向 JDK 目录的基目录；对我来说，它是`C:\\Program Files\\Java\\jdk1.8.0_221\\`，不过您的路径将取决于您安装 JDK 的位置和安装的版本。\n\nMake sure you set the path with the trailing directory separator; the Android SDK is pretty fussy about that sort of thing.\n\n接下来，您需要更新您的`PATH`以指向您刚刚安装的所有内容。 同样，这是一个环境变量，您需要添加以下内容：\n\n*   JDK 的`bin`目录\n*   `Android\\Sdk\\tools`目录\n*   `Android\\Sdk\\platform-tools`目录\n\n对我来说，在我的 Windows 10 电脑上，我的`PATH`文件现在包括以下内容：\n\n```cpp\n...C:\\Program Files\\Java\\jdk1.8.0_221\\bin;C:\\Users\\YourName\\AppData\\Local\\Android\\Sdk\\tools;;C:\\Users\\YourName\\AppData\\Local\\Android\\Sdk\\platform-tools;... \n```\n\n不要忘记分隔符：在 Windows 上，它是分号(`;`)，而在 MacOSX 和 Linux 上，它是冒号(`:`)。 此外，请注意，Windows 中的 Android SDK 路径隐藏在`AppData`目录中，除非您打开了用户文件夹的查看属性下的隐藏项目选项，否则您不会立即看到该目录。\n\nAn environment variable is a variable maintained by your operating system that affects its configuration; see [http://en.wikipedia.org/wiki/Environment_variable](http://en.wikipedia.org/wiki/Environment_variable) for more details.\n\n此时，最好重新启动计算机(如果您运行的是 Windows)，或者注销并重新登录(在 Linux 或 MacOS X 上)，以确保所有这些设置都生效。 如果你使用的是 MacOS X 或 Linux 机器，你或许可以启动一个新的终端并获得相同的效果(或者重新加载你的外壳配置文件)，但是我喜欢现在重新启动的想法，以确保我下次启动所有东西时，它都能正常工作。\n\n# 完成 Android SDK 安装\n\n现在，我们需要使用 Android SDK 工具来确保您安装了至少一个 Android API 级别的完整版 SDK。 我们需要启动 Android Studio 并运行 Android SDK 管理器。 为此，请执行以下步骤：\n\n1.  Android Studio 启动后，查找配置按钮：(在下一个屏幕截图中圈出)：\n\n![](img/8695c743-098c-4aeb-90c4-1c68e2c13440.png)\n\n2.  点击弹出菜单中的 SDK 管理器按钮，打开 Android SDK 管理器。\n3.  确保您至少安装了一个高于 API 级别 16 的 Android API 级别，以及 Google USB 驱动程序(您需要此驱动程序才能在硬件上进行调试)。\n4.  退出 Android Studio。\n\n接下来，让我们看看**Android Debug Bridge**(**ADP**)-将可执行文件传输到 Android 设备并支持设备上调试的软件组件-是否正常工作。 启动 shell 提示符并键入`adb`。 如果您看到大量输出且没有错误，则网桥安装正确。 如果没有，请返回并检查您的`PATH`变量以确保它是正确的。\n\n同时，你也应该让你的 Android 设备支持开发者，这样它就可以和 ADB 一起工作了。 按照[http://bit.ly/1a29sal](http://bit.ly/1a29sal)提供的步骤操作。\n\n让我们进入下一节，开始配置我们的 Qt Creator。\n\n# 配置 Qt 创建器\n\n现在，是时候告诉 Qt Creator 您刚刚安装的所有东西了。 执行以下步骤：\n\n1.  启动 Qt Creator，但不创建新项目。\n2.  在 Tools(工具)菜单下，选择 Options(选项)，然后点击 Devices(设备)和 Android(安卓)。\n3.  填空，如下一个屏幕截图所示。 它们应该设置如下：\n    *   SDK 目录的路径，在您安装 Android SDK 的目录中\n    *   安装 Android NDK 的路径\n    *   选中自动为 Android 工具链创建工具包\n    *   安装 JDK 的目录(可能会自动从您的`JAVA_HOME`目录中获取)，如以下屏幕截图所示：\n\n![](img/e8848a67-9dce-47be-807c-56877f78dfe3.png)\n\n4.  单击确定关闭选项窗口。\n\n您现在应该能够为 Android 创建一个新的 Qt GUI 或 Qt Quick 应用了！ 这样做，并确保 Android 是向导中的目标选项，如下一个屏幕截图所示；确保至少为您的桌面环境选择一个 ARM 目标、一个 x86 目标和一个目标：\n\n![](img/6691fb92-ff31-4aee-bcb9-fe0b3b5ee83e.png)\n\n如果您想要将 Android 构建配置添加到现有项目中，则过程略有不同。 执行以下步骤：\n\n1.  像往常一样加载项目。\n2.  单击左侧窗格中的项目。 项目窗格将打开。\n3.  在 Build&Run(构建和运行)选项下点击所需的 Android(或其他)设备构建工具包，它将为您的项目启用。\n\n下面的屏幕截图显示了项目按钮和构建和运行选项在 Qt Creator 中的位置：\n\n![](img/a9fcab4e-3b60-4662-b056-fe169558f8cf.png)\n\n接下来，我们将继续学习如何构建和运行我们的 Android 应用。\n\n# 构建和运行您的应用\n\n正常编写和构建您的应用。 一个好主意是，在您进城进行大量更改之前，首先为 Android 构建 Qt Quick`Hello World`应用，然后通过为该设备编译来测试环境。 当您准备好在设备上运行时，请执行以下步骤：\n\n1.  导航到项目(在左侧)，然后选择 Android for ARM 工具包的 Run Settings。\n2.  在 Package Configurations 下，确保 Android SDK 级别设置为您安装的 SDK 级别。\n3.  确保包名称类似于`org.qtproject.example`，后跟您的项目名称。\n4.  使用 USB 电缆将 Android 设备连接到计算机。\n5.  选择 Android for ARM Run 目标，然后单击 Debug 或 Run 在设备上调试或运行您的应用。\n\n就这样!。 现在我们已经为 Android 设置了 Qt Creator，并了解了如何在它上构建和运行我们的应用。 接下来，我们将了解如何部署这些应用。\n\n除了支持 Android，Qt 还支持 iOS；未来可能还会支持其他平台。 有关 Qt 对移动平台支持的更多信息，请参阅[https://doc.qt.io/qt-5/mobiledevelopment.html](https://doc.qt.io/qt-5/mobiledevelopment.html)上的 Qt 文档。\n\n# 将应用部署到 Android 设备\n\n在本节中，我们将学习如何构建和部署专门用于 Android 设备的 Qt 应用。 部署到 Android 设备与桌面或 iOS 有很大不同。 在将该应用部署到 Android 设备之前，我们需要进行一些设置：\n\n1.  让我们通过单击显示位于左侧面板上的项目的扳手图标来打开 Build and Settings(构建和设置)界面。 请确保您已经选择了其中一个 Android 工具包(例如，用于 arm64-v8a 的 Android)：\n\n![](img/2478a8ee-9497-420a-9fe3-00d397bd3c75.png)\n\n2.  之后，您将在 Build Settings 界面上看到一些可用的附加设置，称为 Build Android APK，它仅适用于 Android 平台。 您将不会在其他生成平台上看到这些设置：\n\n![](img/9ab8c64c-b61d-4967-93d9-0fda53e4e0a0.png)\n\n让我们逐一来看一下设置：\n\n如果您尚未生成证书，请单击 Create...(创建...)。 按钮启动该过程。 将弹出一个名为 Create a KeyStore 和证书的窗口，您必须在按下保存按钮之前填写所有信息：\n\n![](img/c348de74-5d82-438d-9cde-148936ef215f.png)\n\n按下保存按钮后，将生成`.keystore`文件。 请确保此文件妥善保管，不要遗失。 如果你丢失了证书，你将无法再将你的应用上传到应用商店，除非你更改了你的应用的标识符，这被认为是应用商店上的新应用。\n\n如果你只是在开发阶段测试你的应用，你不需要给它签名。 但是，如果您要将应用发布到应用商店，则必须在构建应用之前对其进行签名。\n\n要为您的应用签名，请执行以下操作：\n\n3.  让我们继续到下一节关于构建 Android APK 的界面。 高级操作部分下的选项大多是可选的：\n    *   **构建后打开包位置**：成功构建后自动打开文件夹。\n    *   **详细输出**：输出每个包含的插件缺少的依赖项列表。\n    *   **使用 Ministro 服务安装 Qt**：Ministro 充当 Qt 库的中央存储库，允许 Qt 应用共享库，从而最小化您的应用大小。 不过，系统会要求用户安装 Ministro，然后才能运行您的应用。\n4.  之后，您将看到“高级操作”设置下的“创建模板”按钮。 如果您以前没有生成过模板，请现在通过单击按钮进行生成：\n\n![](img/616cbea5-3e70-4b40-b350-debcb32e64c0.png)\n\n5.  通过单击创建模板，将弹出一个窗口。 保留默认设置，然后单击 Finish 按钮：\n\n![](img/3401cdd4-0ac0-4367-9200-829b2f355bfb.png)\n\n然后，将在项目目录的`android`文件夹下为您创建一组文件，如下所示：\n\n![](img/a5186e53-c73c-441d-9a23-f283623b9962.png)\n\n包含单词 Gradle 的文件是 Android Gradle 构建工具包的一部分，该工具包管理依赖项和构建逻辑。 它类似于桌面上的 CMake，用于定制、配置和扩展构建过程。 我们现在不需要碰这些文件。\n\n更重要的文件是`AndroidManifest.xml`，我们需要打开并配置它：\n\n![](img/62651135-41fd-4646-910a-0755cdcc28eb.png)\n\n首先，在 Package 部分下，我们有以下选项：\n\n*   包名：这是我们应用的标识符。 根据需要将其设置为类似于`com.companyname.appname`的值。 在你将应用上载到应用商店后，无法更改此设置，因为应用商店使用包名称来标识你的应用。\n*   版本代码：这是您的应用的数字格式版本。 版本代码不会显示给用户，但由应用商店用来识别应用的实际构建版本。 每次将更新上载到应用商店时，此数字都必须递增。\n\n*   版本名称：这是用户在应用商店上看到的版本标签。 你可以设置任何你想要的东西，因为应用商店不依赖它来识别版本。\n*   最低 SDK 要求：这是用户设备必须支持的最低 Android SDK 版本。 如果用户的设备不符合最低要求，他们将无法安装这款应用。 请注意，如果您的应用依赖于仅在较高版本上支持的特定功能，则不能简单地将其设置为可用的最低版本。\n*   目标 SDK：这是您当前用来构建 Android 应用的实际 Android SDK 版本。 如果用户的设备运行的是较高版本，则可能会警告用户可能不兼容，甚至在系统更新后，您的应用可能会自动从用户的设备中删除。\n\n接下来，我们将介绍应用部分：\n\n*   应用名称：应用的名称，通常显示在应用图标下。\n*   活动名称：应用主要活动的名称，可以与应用名称相同。 用外行人的话说，Android 应用中的活动就像是应用中的单个进程，有自己的用户界面。 一个 Android 应用可以有许多不同的活动，这些活动可以做不同的事情。 例如，您可以触发一个从主活动捕获照片的相机活动，然后生成另一个将照片上传到服务器并显示上传进度的活动。\n*   Run：这是您要运行的应用的名称。 大多数项目只有一个应用，所以我们可以只保留默认值。\n*   应用图标：这是您为应用设置图标的位置。 您需要设置几种不同的大小-低 DPI、中 DPI 和高 DPI 图标。\n\n最后，我们将查看权限部分。 除了下面解释的两个选项(顾名思义)之外，底部的巨大空间是您向 Android 应用添加权限的地方。 某些功能需要获得用户的许可才能由您的应用使用，例如获取设备位置、访问文件存储和访问摄像头。 有关安卓系统可用权限的完整列表，请访问[https://developer.android.com/reference/android/Manifest.permission](https://developer.android.com/reference/android/Manifest.permission)。 您可以通过从组合框中选择权限，然后按添加按钮来向应用添加权限。 要删除权限，请在组合框上方的权限列表中选择该权限，然后单击删除按钮。\n\n一旦准备好了`AndroidManifest.xml`，现在就可以构建项目并生成 APK 文件了。 如果您打算将您的应用上传到应用商店，请记住对其进行签名。 要在我们的手机上运行这款应用，我们只需像往常一样点击 Run 按钮即可。 这一次，将弹出一个窗口，要求您选择要在其上运行的 Android 设备。 请确保您已使用 USB 电缆将电话连接到 PC，以便该设备将显示在此处的列表中：\n\n![](img/a5ddffbb-146e-4b75-b419-542637440cbf.png)\n\n按下 OK 按钮，等待构建过程完成。 一旦该应用成功构建，Qt 将自动在您的 Android 设备上启动该应用。\n\n请注意，您也可以在 Android 模拟器上运行您的应用，而不是在实际的物理设备上运行。 在将应用部署到 Android 仿真器之前，首先需要设置仿真器。 您可以打开 Android Studio，单击窗口右下角的配置按钮，然后选择 AVD 管理器：\n\n![](img/c902cb40-ea1a-43e9-8059-fd938a179532.png)\n\n将弹出一个窗口，显示可供部署的仿真器(Android 虚拟设备)列表。 默认情况下，您应该看到一个空列表。 您可以通过按 Create Virtual Device...(创建虚拟设备...)来创建新的仿真器。 位于窗口左下角的按钮：\n\n![](img/adaef631-b508-4d88-84c9-98a1314b6905.png)\n\n之后，选择要模拟的设备类别(即电话)，选择设备型号，然后单击下一步：\n\n![](img/79e6b337-6c4c-4d13-a164-0ba3e94355d5.png)\n\n然后，为您的模拟器选择 Android 版本。 如果您的计算机还没有 Android 镜像，请单击 Android 版本名称旁边的下载链接。 下载后，您可以选择版本并单击下一步：\n\n![](img/5153a4f7-bc86-4367-85a6-1c41c8f54d36.png)\n\n最后，为您的模拟器命名，设置其启动方向，然后单击 Finish：\n\n![](img/57d4833d-92cb-4741-8424-7de44eda2ac1.png)\n\n一旦设置了仿真器，您就可以测试运行仿真器并查看它的运行情况：\n\n![](img/512a58ca-57a6-4374-a936-28b97362c855.png)\n\n设置仿真器后，当您从 Qt Creator 运行 Android 应用时，该仿真器将出现在设备列表中：\n\n![](img/6e3fa980-f395-4a67-bc91-1ad1c7a1020c.png)\n\n请注意，Android 模拟器速度非常慢，不推荐用于关键产品。 使用实际的物理设备仍然是应用开发的最佳方式，无论是 Android 还是 iOS。\n\n在本节中，我们了解了如何设置 Qt 应用并将其部署到 Android 设备。 接下来，我们将学习如何在 iOS 和 Android 应用中实现原生功能，以更好地支持功能。\n\n# 为 iOS 设置 Qt Creator\n\n使用 Qt 开发 Android 和 iOS 之间的一个主要区别是，你根本不能在 Qt Creator 上构建和运行 iOS 应用，因为这是苹果不允许的。 因此，Qt Creator 只能用于开发应用和生成 Xcode 文件。 Xcode 是适用于所有 Apple 平台(包括 iOS)的*事实上的*编程工具和编译器。 一旦 Qt Creator 生成了所需的 Xcode 文件，您就必须在 Xcode 上完成其余的工作。\n\n让我们来看看如何为 iOS 开发设置我们的 Qt Creator。\n\n首先，当您创建一个新项目时，您将看到许多不同的工具包可供您使用。 带有单词 clang 的工具包是为 MacOS 开发的，在部署到 iOS 模拟器或设备之前，你也可以用它来测试你的应用的 GUI。 关键字为 iOS 的工具包用于部署到物理 iOS 设备(iPhone 或 iPad)，而关键字为的工具包用于部署到随 Xcode 安装一起提供的 iOS 模拟器：\n\n![](img/c055e409-3df2-4c07-a744-74cdfaf32679.png)\n\n因为你不能为 Qt Creator 构建和运行你的 iOS 应用，所以你只能通过构建|运行 qmake 来生成它的 Xcode 文件。 这将在您的 Build 文件夹中生成以下文件：\n\n![](img/7a2cb9e5-8296-49e6-9a6f-0b56f10f71c2.png)\n\n每次运行 qmake 时，它都会生成 make 文件、Xcode 项目文件、用于配置的 plist 文件以及一些用于链接 Qt 插件的 CPP 文件。 请注意，每次在这里，qmake 都会替换任何现有的文件，所以最好不要直接编辑这些文件，因为您所做的任何更改都将在下一次运行后消失。 但是，您可以通过将数据写入项目文件(`.pro`)来告诉 qmake 在生成这些文件时要替换哪些数据。 例如，请考虑以下内容：\n\n```cpp\nios {\n    QMAKE_TARGET_BUNDLE_PREFIX = com.mycompany\n    QMAKE_BUNDLE = myapp\n    QMAKE_INFO_PLIST = ios/Info.plist\n}\n```\n\n如您所见，我已经在一个`ios`宏中添加了代码，这样如果我们不是为 iOS 构建项目，它将被忽略。 首先，我们将应用的包前缀设置为`com.mycompany`，然后将包名称设置为`myapp`。 这将导致`com.mycompany.myapp`作为完整的捆绑包名称。 然后，我们复制由 qmake 生成的`Info.plist`文件，并将其放入项目目录内的`ios`文件夹(如果该文件夹不存在，则创建一个)。 然后，我们告诉 qmake 将特定的`Info.plist`文件复制到 build 目录，而不是生成一个全新的文件。 这样，我们所做的任何更改也将被复制。\n\n就这样。 让我们进入下一个主题，了解如何通过支持原生功能来改善对 iOS 和 Android 应用的支持。\n\n# 改进对 iOS 和 Android 应用的支持\n\nQt 不仅使得在 iOS 和 Android 上编译和部署应用变得容易，而且它还通过 Java(Android)和 Objective C++(IOS)编码支持原生功能。 在接下来的几节中，我们将同时研究这两个问题。\n\n# 从 Qt 调用 Android 函数\n\n要从 Qt 调用 Android 函数，需要执行以下几个步骤：\n\n1.  创建一个空的 Qt Quick 项目，我们已经做过无数次了。\n2.  打开项目文件(`.pro`)，将以下`androidextras`模块添加到项目中：\n\n```cpp\nQT += quick\nandroid: QT += androidextras\nCONFIG += c++ 11\n```\n\n3.  通过单击 Create Template 按钮创建`AndroidManifest.xml`和其他重要的 Android 文件，我们在前面的*将应用部署到 Android 设备的*一节中了解了这一点。\n4.  然后，在您的 Qt 项目目录中创建一个`MyToast.java`文件，并使用 Qt Creator 将其打开。 您必须将其放置在存储`AndroidManifest.xml`的 Android 文件夹中，并确保文件夹结构如以下屏幕截图所示，否则，您将无法在后面的步骤中从代码中调用它：\n\n![](img/080b0f58-767b-435d-b385-5cb9ef768059.png)\n\n5.  之后，重新打开我们的项目文件(`.pro`)，并添加以下文本以告诉 Qt 在目录中查找 Java 源代码：\n\n```cpp\ncontains(ANDROID_TARGET_ARCH,arm64-v8a) {\n    ANDROID_PACKAGE_SOURCE_DIR = \\\n    $$PWD/android\n    ANDROID_JAVA_SOURCES.path = $$PWD/android/src/org\n      /qtproject/example\n INSTALLS += ANDROID_JAVA_SOURCES\n}\n```\n\n`MyToast.java`的代码非常简单。 当调用`notify`函数时，我们只需在 Toast 小部件上显示一行消息：\n\n```cpp\npackage org.qtproject.example;\n\nimport android.content.Context;\nimport android.widget.Toast;\n\npublic class MyToast extends org.qtproject.qt5.android.bindings.QtActivity\n{\n    public MyToast() {}\n    public static void notify(Context context, String message)\n    {\n        int duration = Toast.LENGTH_SHORT;\n        Toast toast = Toast.makeText(context, message, duration);\n        toast.show();\n    }\n}\n```\n\n完成后，创建一个名为`MyToast`的 C++ 类，并包含`QObject`作为其父类。 头文件`mytoast.h`如下所示：\n\n```cpp\n#ifndef MYTOAST_H\n#define MYTOAST_H\n\n#include <QObject>\n#ifdef Q_OS_ANDROID\n  #include <QtAndroidExtras>\n  #include <QAndroidJniObject>\n#endif\n\nclass MyToast : public QObject\n{\n    Q_OBJECT\npublic:\n    explicit MyToast(QObject *parent = nullptr);\n    Q_INVOKABLE void callToast(QString message);\n};\n#endif // MYTOAST_H\n```\n\n在前面的代码中，如果我们通过检查`Q_OS_ANDROID`宏来编译 Android 平台的项目，那么我们只包含`QtAndroidExtras`和`QAndroidJniObject`头。 这将防止在为其他平台(如 Windows 或 iOS)编译项目时出现编译错误。 然后，我们使用`Q_INVOKABLE`宏声明一个`callToast`函数，以便稍后可以从 QML 调用它。\n\n下面的`mytoast.cpp`源文件看起来也非常简单，您可以在这里看到：\n\n```cpp\n#include \"mytoast.h\"\n\nMyToast::MyToast(QObject *parent) : QObject(parent) {}\n\nvoid MyToast::callToast(QString message)\n{\n    #ifdef Q_OS_ANDROID\n    QtAndroid::runOnAndroidThread([=]\n    {\n        QAndroidJniObject::callStaticMethod<void>\n          (\"org/qtproject/example/MyToast\",\n            \"notify\", \"(Landroid/content/Context;Ljava/lang/String;)V\",\n            QtAndroid::androidActivity().object(),\n            QAndroidJniObject::fromString(message).object<jstring>());\n    });\n    #endif\n}\n```\n\n在前面的代码中，我们实现了`callToast`函数，该函数通过`QAndroidJniObject`类触发 Java`notify`函数。 `QAndroidJniObject`类为我们提供了`callStaticMethod`函数，该函数可用于调用静态的 Java 本机函数。 我们在 Java 代码中声明了静态函数，因为静态函数不属于类的实例，而是类本身，类的所有实例都可以访问它，因此我们更容易以这种跨语言的方式实现。\n\n这个特定的`callStaticMethod`不需要返回，因此我们将其放在函数名后面。 至于函数输入，第一个变量是我们要查找的 Java 类，它是`org/qtproject/example/MyToast`，我们要调用的函数名是`notify`。\n\n然后，我们告诉 Java 函数应该从输入变量获得什么数据类型，第一个变量是`android/content/Contact`，后面是`java/lang/String`。 末尾的`V`是返回变量的数据类型，在本例中为`void`。 之后，我们提交`notify`函数所需的变量，第一个变量是我们应用的上下文，第二个变量是我们希望在 Toast 小部件上显示的消息。\n\n请注意，我们在 Android 线程中运行整个代码块，而不是在 Qt 线程中运行，因为 Toast 小部件是 Android UI 的一部分。 我们调用了`QtAndroid::runOnAndroidThread`并在 lambda 函数中运行了`notify`函数，这意味着只有在切换回 Android 线程之后才会调用`notify`。\n\n一旦您理解了前面的代码，让我们继续打开`main.cpp`。 这里我们要做的第一件事是包括`mytoast.h`和`QQmlContext`标头：\n\n```cpp\n#include <QQmlContext>\n#include \"mytoast.h\"\n```\n\n然后，我们将`MyToast`类注册为自声明的`MyLib 1.o`库下的 QML 类型。 您将在后面的步骤中看到这是如何实现的：\n\n```cpp\nQCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);\nQGuiApplication app(argc, argv);\nQQmlApplicationEngine engine;\n\nQQmlContext *context = engine.rootContext();\nqmlRegisterType<MyToast>(\"MyLib\", 1, 0, \"MyToast\");\n```\n\n之后，我们想要在点击屏幕时调用`callToast`函数。 因此，让我们打开`main.qml`并导入我们在上一步中声明的`MyLib 1.0`库：\n\n```cpp\nimport QtQuick 2.12\nimport QtQuick.Window 2.12\n\nimport MyLib 1.0\n```\n\n一旦我们导入库，我们现在就可以访问`MyToast`QML 类型。 让我们在 QML 代码中创建一个，并将其命名为`toast`：\n\n```cpp\nWindow {\n    visible: true\n    width: 640\n    height: 480\n    title: qsTr(\"Hello World\")\n\n MyToast {\n id: toast\n }\n```\n\n之后，为我们创建一个`MouseArea`对象来显示祝酒词消息。 然后，当触发`onClicked`事件时，我们调用`toast.callToast`：\n\n```cpp\n    MouseArea {\n        anchors.fill: parent\n        onClicked: toast.callToast(\"My message here. Hello World!\")\n    }\n}\n```\n\n就这样!。 让我们构建该应用并将其部署到您的 Android 设备上。 它在启动时看起来空空如也，但如果你点击屏幕，神奇的事情就会发生：\n\n![](img/48617cab-d448-4092-9b49-8c14d42a2d62.jpg)\n\n尽管 Qt 不支持一些原生 Android 特性，比如 Toast 小部件，但我们仍然可以通过 Java 自己实现它，并将其连接到我们的 Qt 代码。 这真是太棒了，让 Qt 成为一个非常强大的开发平台。\n\n# 从 Qt 调用 IOS 函数\n\n调用 iOS 函数与我们为 Android 调用的函数有很大不同。 第一个区别是 iOS 的编程语言，它是 Objective C++(带“加号”)，而不是 Java。 为了让我们在 Qt 项目中集成 iOS 函数，我们不能对将集成到 Qt 项目中的类使用通常的 Objective C 语言。 取而代之的是目标 C++ 语言，它在 MacOS 和 iOS 环境下与 C++ 兼容。 其次，没有像 Android 那样的**iOS Extras**模块。 这是因为 Objective C++ 与 C++ 兼容，因此我们只需在 Objective C++ 中创建一个`trampoline`参数函数并直接调用它，进而触发您想要调用的本机 iOS 函数。\n\n让我们用一个简单的例子来尝试一下。 首先，在 Qt 项目中创建一个名为`MyClass`的新类，并将`.cpp`文件的扩展名更改为`.mm`，这将告诉 Apple 编译器 Xcode 这是一个目标 C++ 源文件(使用目标 C 的`.m`扩展名)。 只有`.mm`扩展允许您在同一源文件中混合使用 Objective C 代码和您的 C++ 代码，这就是它被称为 Objective C++ 的原因。\n\n然后，打开您的 Qt 项目文件(`.pro`)，并确保只有在为 iOS 平台编译时这两个文件才会包含在您的项目中。 这是因为其他平台无法识别 Objective C++，在编译过程中会给您带来大量错误：\n\n```cpp\nios {\n    HEADERS += ios/MyClass.h\n    SOURCES += ios/MyClass.mm\n}\n```\n\n之后，打开`MyClass.h`，将`doSomethingT`功能添加到您的文件中：\n\n```cpp\n#ifndef MYCLASS_H\n#define MYCLASS_H\n#include <QObject>\n\nclass MyClass : public QObject\n{\n    Q_OBJECT\n\n    public:\n        MyClass(QObject *parent = NULL);\n void doSomethingT(QString arg1, QString arg2);\n\n    private:\n};\n\n#endif\n```\n\n正如您从前面的代码中看到的，它看起来与我们的普通 C++ Qt 代码一样。 我们将函数命名为`doSomethingT`，在函数名的末尾加上大写的`T`，这样我们就可以记住这只是一个蹦床函数。 然后，在`MyClass.mm`文件中，我们实现了我们的`MyClass`函数，该函数看起来与我们的普通 C++ 代码略有不同：\n\n```cpp\n#import <Foundation/Foundation.h>\n#include \"MyClass.h\"\n\n@interface MyObject : NSObject\n{\n    - (void) doSomething:(NSString*)arg1 :(NSString*)arg2\n}\n@end\n\n@implementation MyObject\n- (void) doSomething:(NSString*)arg1 :(NSString*)arg2\n{\n    NSLog(@\"The actual doSomething function being called!\");\n}\n@end\n\nMyClass::MyClass(QObject *parent) : QObject(parent) {}\n\nvoid MyClass::doSomethingT(QString arg1, QString arg2)\n{\n    [[MyObject alloc] doSomething:arg1.toNSString() \n      :arg2.toNSString()];\n    qDebug() << \"Doing something:\" << arg1 << \", \" << arg2;\n}\n```\n\n如果您不熟悉 Objective C 语法，最初在这里看起来可能有点奇怪。 我有意在前两行中同时使用了`#import`和`#include`，以向您展示您可以在一个完整的`.mm`源文件中将目标 C 和 C++ 代码混合在一起。 然后，我们创建一个名为`MyObject`的目标 C 类，它携带实际的`doSomething`函数。 我们首先在关键字`@interface`和`@end`之间声明类及其函数，然后在关键字`@implementation`和`@end`之间实现类及其函数。 `doSomething`函数接受两个`NSString`变量，即`arg1`和`arg2`，这与`doSomethingT`函数类似。\n\n之后，我们继续编写代码，但这一次是用 C++。 我们实现了我们的`MyClass`类和`doSomethingT`蹦床函数。 在`doSomethingT`函数中，我们初始化了目标 C`MyObject`类，并在这里调用了实际的`doSomething`代码。 幸运的是，Qt 在`QString`类下为我们提供了一个`toNSString()`函数，这使得数据类型转换变得无缝。 我们还使用了`qDebug`函数来确保成功调用了这个蹦床函数。 在`doSomething`函数中，我们使用了`NSLog`函数打印出一条调试消息，以确保它被成功调用。\n\n为了更好地理解这里讨论的整个过程，让我们看一下下图，以总结所有内容：\n\n![](img/9a7d9f0d-47a1-49d5-a7c1-2b4edccc7b92.png)\n\n我们使用了驻留在 C++ `MyClass`类中的`doSomethingT`蹦床函数来调用`MyObject`类中的实际目标 C 函数：`doSomething`。 然后，`doSomething`函数基本上可以用来调用您喜欢的任何其他本机 IOS 函数。 换句话说，我们通过一个简单的蹦床函数在两个不同的领域之间跳跃，并且充分利用了 Objective C++ 允许我们做的事情。\n\n就这样。\n\n在本节中，我们了解了如何从 C++ 源代码调用 IOS 函数。 这使得 Qt 成为一个真正强大的开发平台，因为我们现在可以将我们的应用部署到不同的平台，而无需创建不同的项目集。 取而代之的是，我们只使用一个 Qt 项目来管理所有这些项目。\n\n# 简略的 / 概括的 / 简易判罪的 / 简易的\n\nQt for Android 为你在移动开发方面提供了极好的帮助，但它并不是万能的。 如果您计划以移动设备为目标，则应该确保很好地了解应用用户的使用模式，以及移动应用必须在其上运行的 CPU、GPU、内存和网络方面的限制。\n\n然而，一旦我们理解了这些，我们使用 Qt Creator 和 Qt 的所有技能就会延续到移动领域。 要为 Android 进行开发，首先要安装 JDK、Android Studio、Android NDK，然后像往常一样开发应用：为设备编译并频繁地在设备上运行，以解决在此过程中出现的任何意想不到的问题。\n\n在我们的最后一章中，我们将了解 Qt Creator 和 Qt 中的一些零碎内容，这将使软件开发变得更加容易。"
  },
  {
    "path": "docs/app-dev-qt-creator/13.md",
    "content": "# 十三、使用 Qt Creator 开发嵌入式和物联网\n\n在上一章中，我们学习了如何使用 Qt 创建移动应用，这使得 Qt 成为一个真正强大的跨平台开发套件。 更进一步，Qt 已经开始在最近的版本中增加对嵌入式设备的支持，允许高性能应用在计算能力有限的小型硬件上运行。 这包括**IoT**(**Internet o****f Things**)设备、医疗设备、汽车显示器、制造机器等。 不幸的是，嵌入式设备的 Qt 只有在您持有 Qt 商业许可证的情况下才能使用。\n\n在本章中，我们将介绍以下主题：\n\n*   设置嵌入式 Linux 映像\n*   构建交叉编译的 Qt 应用\n*   为嵌入式项目配置 Qt\n*   编写您的第一个嵌入式程序\n*   将 Qt 应用部署到嵌入式系统\n\n在本章中，您将学习如何设置用于在嵌入式设备上运行的 Linux 映像，然后配置和构建 Qt 项目，使其支持您正在运行的设备。 最后，您还将学习如何编写第一个嵌入式程序并将其部署到硬件上。\n\n# 技术要求\n\n本章的技术要求包括 Qt(商业许可证)5.13.1、Boot2Qt Emulator、Qt Creator 4.10.1、Windows 10、GNU 工具链、Python 2.8、WinFLASHTool、Debian for Beaglebone(或其他系统)和 SSH 服务器。 您还需要物理嵌入式设备(如 Intel NUC 或 Raspberry PI)进行部署。\n\n您可以申请 30 天的试用期来试用各种功能。 在本章的下一节中，您将了解如何申请试用许可证。 请注意，您仍然可以在不使用商业许可的情况下为嵌入式设备开发 Qt 应用，但这将需要大量的手工工作和专业知识。 除此之外，您还需要极大的耐心才能在不使用 Qt 商业许可提供的自动化方法的情况下将您的程序部署到嵌入式系统上。\n\n# 设置嵌入式 Linux 映像\n\n嵌入式设备过去是由硬件制造商单独定制的，因为固件必须专门为焊接到设备上的芯片创建。 这阻止了小公司和业余爱好开发人员在没有与大制造商合作的情况下设计新产品，这是相当有挑战性的，特别是对业余爱好者来说。\n\n然而，近年来我们的技术有了很大的进步，这意味着运行我们个人电脑和手机的芯片已经变得几乎和大型制造商生产的专有智能设备一样强大。这导致大多数制造商为了使用 ARM 和 x86 芯片而改变立场，这样做可以降低他们的研发成本。这也让小公司或业余爱好开发人员可以很容易地在低预算嵌入式硬件(如 Intel NUC 或 Raspberry Pi)上制作软件原型，并仍然期望它能在实际生产硬件上工作。\n\n基于 Linux 的系统目前被许多嵌入式项目使用，因为它们的开源特性使得它们具有高度的可定制性。 嵌入式 Linux 系统实际上与我们通常看到的运行在台式机或服务器上的 Linux 系统有很大的不同。 对于嵌入式硬件来说，这些 Linux 操作系统太过庞大和复杂，而且由于计算能力有限，在性能方面也不会很好地运行。 因此，我们需要从 Linux 系统中去掉所有不必要的组件，并构建我们自己的 Linux 映像，它很小、很简单，并且针对嵌入式设备进行了优化。\n\n在设置我们的嵌入式 Linux 系统之前，让我们先使用商业许可安装 Qt。 请注意，您只能使用商业许可为嵌入式设备构建 Qt 应用。 开放源码许可是不允许的，您在开放源码包中找不到构建嵌入式应用所需的任何 Qt 组件。\n\n我们来看一下如何办理营业执照登记。\n\n# 注册 Qt 商用许可证 30 天试用期\n\n请按照以下步骤注册 30 天试用版 Qt 商业许可证：\n\n1.  转到[http://www.qt.i](http://www.qt.io)。\n2.  单击下载。 试一试。 买。 位于网页右上角的按钮。\n3.  然后，单击位于 Try Qt 列中的 Download Qt Now 按钮。 该按钮实际上不会启动任何下载，但会触发一个名为“立即请求您的免费试用”的注册表。\n4.  填写表格，然后单击立即提交我的请求以提交申请表。 请注意，您必须使用商务电子邮件地址进行注册，而不是使用免费电子邮件地址(如 Gmail、Yahoo！ 邮件、Hotmail 和许多其他邮件)，因为这是不允许的。 如果您这样做，Qt 公司可能会拒绝您的免费试用请求。\n5.  在 Qt 公司批准您的请求并向您发送激活链接之前，请等待几天。\n\n一旦您获得了 30 天的试用许可，恭喜您！ 您可以开始从 Qt 安装程序下载用于嵌入式开发的组件。\n\n# 安装 Qt 组件以创建设备\n\n一旦您使用您的商业帐户登录 Qt 安装程序，安装程序将在 Qt for Device Creation 部分显示选项。*如果您使用开源软件许可证，则以下屏幕截图可能是您以前从未见过的内容：\n\n![](img/da1509c0-8f63-4874-9727-e9253d51d307.png)\n\n正如您在前面的屏幕截图中看到的，默认情况下，Qt 为您提供了一组预构建的 Linux 映像，这些映像在流行的嵌入式开发板(如 Intel NUC、NVIDIA Jetson TX2、Raspberry PI 3 和 Toradex Colibri iMX7)上运行良好。 如果您使用的是 Qt 默认支持的这些硬件选项之一，那么您不必从头开始构建您自己的 Linux 映像-只需从 Qt 安装程序下载即可。\n\nQt 还为在 VirtualBox 上运行的仿真器提供了 Linux 映像。 如果您勾选了 Emulator 选项，并且您的 PC 上没有安装 VirtualBox，则会出现一个消息框，提示您在继续操作之前先安装此软件：\n\n![](img/94a93059-9312-497e-9537-e0d05539740d.png)\n\n要下载 VirtualBox，只需进入[https://www.virtualbox.org](https://www.virtualbox.org)即可。 然后，导航到 Downloads 页面，选择您正在运行的操作系统(例如 Windows 主机)的下载选项。 运行安装程序，您就可以运行了。 如果您不想在每次有一些小的更改时都将应用导出到设备上，那么在仿真器上运行是测试应用的最快方式。\n\n# 将嵌入式 Linux 映像写入存储设备\n\nQt 商业广告为我们提供了一个方便的工具，可以将预置的嵌入式 Linux 镜像写入拇指驱动器或 SD 卡。 这是一个名为引导至 QT 闪存向导的程序，或名为`b2qt-flashing-wizard.exe`，位于`Qt/Tools/b2qt`。 当您运行它时，系统会要求您选择正在运行的设备以及支持它的 Qt 版本。 下面的屏幕截图显示了这一点：\n\n![](img/ba44f9c4-3746-423f-b30b-ab51e645ade1.png)\n\n之后，系统将要求您选择要写入 Linux 镜像的存储设备：\n\n![](img/a73621d4-9f29-440b-a2e4-849eee6b312e.png)\n\n单击 Next 按钮后，Linux 映像将写入存储设备：\n\n![](img/033ff4d0-5a89-4961-85bb-40088501a8a8.png)\n\n一旦完成，您就可以将您的拇指驱动器或 SD 卡插入您的嵌入式设备，并开始启动您的 Linux 系统！ 您应该看到在 Linux 系统上默认启动了 Qt 演示。\n\n下面的照片显示了在我 6 岁的 Intel NUC 上运行的嵌入式 Qt 演示。 虽然我的机器现在已经很旧了，但性能还是不错的：\n\n![](img/51126045-b91a-4a01-9834-d3ffa21a00df.png)\n\n如果您的设备没有得到 Qt 公司的正式支持，那么您将需要构建自己的自定义 Linux 映像，这并不是一项简单的任务。 Qt 使用 Yockto 项目([https://www.yoctoproject.org](https://www.yoctoproject.org))中的配方来配置他们的 linux 镜像，所以如果您正在尝试构建自己的 linux 镜像，您也应该这样做。\n\n要了解如何使用 Yockto 项目提供的工具构建您自己的自定义 Linux 映像，请查看[https://doc.qt.io/QtForDeviceCreation/qtee-custom-embedded-linux-image.html](https://doc.qt.io/QtForDeviceCreation/qtee-custom-embedded-linux-image.html)上的文档。 请注意，您只能在 Linux 系统(如 Ubuntu)上从源代码构建 Linux 映像。 你不能在 Windows 或 MacOS 系统上做到这一点。 幸运的是，Yockto 项目还为我们提供了一个构建机器人，它允许我们在云上构建，而不是在我们自己的机器上构建。\n\n在本节中，我们了解了如何设置嵌入式 Linux 映像以在 Qt 上创建设备。 接下来，我们将学习如何构建交叉编译的 Qt 项目。\n\n# 构建交叉编译的 Qt 应用\n\n交叉编译是指您在一台计算机上编写代码，但在另一台运行不同操作系统或处理器的计算机上构建代码。 例如，您可以在 Windows 上开发应用，但为 Linux 计算机构建应用；或者，您可以在 x86 Linux 计算机上编写代码，但为 ARMv8 Linux 设备构建可执行文件。\n\n在下列情况下需要交叉编译：\n\n*   Qt 工具链或库在您正在运行的目标设备上不可用\n*   目标设备非常慢，不适合编译代码\n*   该设备没有任何显示或输入法\n\nQt 商业版使得交叉编译应用并将其部署到不同类型的嵌入式设备变得非常容易，因此除非 Qt 官方不支持该硬件，否则不推荐使用手动方式。\n\n# 使用 Qt Creator 自动交叉编译\n\n当您使用商业许可证在 Qt Creator 中创建项目时，您将在 Kit Selection 页面上看到比通常使用开源许可证时更多的选项。 这些工具包是您使用 Qt 安装程序选择并安装的工具包。 如果您在工具包选择页面上没有看到这些选项，请再次打开 Qt 安装程序并安装缺少的软件包。\n\n如果工具包可用，请选择与您的硬件匹配的工具包，或者如果要在不部署到设备的情况下测试应用，请选择仿真器选项，如下所示：\n\n![](img/957b399d-f1a4-4b0b-9b1d-3f118f1c8f89.png)\n\n如果您是在 Qt 上创建设备的新手，那么您可能还不能在实际的硬件上部署和运行您的应用。 接下来，我们将在*将 Qt 应用部署到嵌入式系统*一节中介绍这一点。 现在，让我们为仿真器交叉编译它。 如果您的硬件是官方支持的，则在 Qt Creator 中交叉构建时不需要额外的步骤。 只需点击位于左下角的 Build 或 Run 按钮即可。 当您第一次在 Qt Creator 上运行该程序时，Qt 启动器将在仿真器中启动，而不是在您的程序中启动。 这是因为 Qt 启动器是运行仿真器时要执行的默认程序。\n\nQt Launcher 实质上是一个演示库，它允许您尝试 Qt 支持的不同功能来创建设备：\n\n![](img/cc7194ff-6f8c-417f-a6ab-3b2f57703efc.png)\n\n一旦模拟器启动，您可以尝试从 Qt Creator 再次运行您的程序，现在它应该会显示在模拟器上。 请注意，由于需要与您的 PC 共享资源，与实际硬件相比，仿真器的性能可能较差。\n\n# 交叉编译手册\n\n除非硬件未得到 Qt 的正式支持，否则不建议手动交叉编译。 例如，创建新项目时，在套件选择页面上找不到 Beaglebone 板卡([http://beagleboard.org/bone](http://beagleboard.org/bone)[)](http://beagleboard.org/bone)。 在这种情况下，我们别无选择，只能自己手动完成。 这是因为在开始交叉编译您的项目之前，设置它是一个非常漫长而复杂的过程。\n\n如果像我一样，您是使用 Windows 系统开发应用的人，那么您需要从[http://gnutoolchains.com/beaglebone](http://gnutoolchains.com/beaglebone)下载 Beaglebone 的 GNU 工具链。 该软件包包括与 Beaglebone 板上运行的 LINUX 镜像兼容的 GCC 编译器、头文件和库。 您也可以从前面的链接下载 Linux 映像，而不是从 Yockto Project 网站下载。 还支持其他类型的架构，如 CubieBoard、Blackfin 和 AVR。\n\n下载安装程序后，运行它并将 GNU 工具链安装到本地目录：\n\n![](img/7e9639af-0875-454c-ab4f-8d7716718970.png)\n\n在为 Beaglebone 安装了 GNU 工具链之后，可以从[https://www.python.org/downloads](https://www.python.org/downloads)安装 Python2.8，因为我们稍后将需要它来编译 Qt 框架。 安装后，从[Linux](http://sysprogs.com/winflashtool/download)下载 WinFLASHTool 并将 http://sysprogs.com/winflashtool/download 映像写入 SD 卡，如下所示：\n\n![](img/60247bc6-c750-4f53-9d66-463bd88b3c8f.png)\n\n将 Linux 映像写入 SD 卡后，将其插入 Beaglebone 设备并启动。 默认情况下，Beaglebone 上的 Linux 系统应该安装并启用 SSH 服务器。 如果没有，您可以使用这里给出的终端命令自己完成(如果您使用的不是基于 Debian 的 Linux，这些命令可能会有所不同)：\n\n1.  首先，安装`openssl-server`：\n\n```cpp\nsudo apt-get install openssh-server\n```\n\n2.  然后，启用并启动 SSH 服务：\n\n```cpp\nsudo systemctl enable ssh\nsudo systemctl start ssh\n```\n\n3.  SSH 服务器启动后，您可以返回到 Windows 计算机并启动位于`C:\\SysGCC\\beaglebone\\tools`文件夹中的`UpdateSysroot.bat`批处理文件。 此工具用于从目标 Linux 计算机获取所有库文件，并将其复制到您的 Windows 计算机：\n\n![](img/33197e77-3c63-48e8-9e38-862a2d1d5b1d.png)\n\n4.  单击[选择...]按钮以连接到 Linux 计算机。 如果不能使用 username@devicename 格式连接，则使用本地 IPv4 IP 地址(例如，192.168.0.140)：\n\n![](img/a93e6b0d-7d4c-4803-af69-68f1a3ade3e1.png)\n\n5.  连接后，单击 Synchronize 按钮开始从 Linux 机器抓取所有库文件。 这可能需要一些时间才能完成：\n\n![](img/36f79dc2-8a96-4183-a7d2-c5130e1d00b0.png)\n\n就这样!。 现在，您可以在 Linux 设备和 Windows 计算机之间同步库文件。\n\n之后，我们将使用 GNU 工具链和刚刚从 Beaglebone 获取的库文件从源代码构建 Qt。 为此，让我们看一看需要什么：\n\n1.  首先，在使用 qmake 构建整个 Qt 框架之前，我们需要构建 qmake。\n2.  转到 Qt 的源文件夹，例如：`C:/Qt/5.12.5/Src`。 然后，转到`/qtbase/mkspecs`目录并复制`linux-arm-gnueabi-g++ `文件夹。\n3.  将复制的文件夹重命名为`linux-arm-gnueabihf-g++ `，然后打开文件夹内的`qmake.conf`文件。 将所有`-gnueabi-`前缀更改为`-gnueabihf-`，使其与文件夹名称匹配，如下所示：\n\n```cpp\nMAKEFILE_GENERATOR = UNIX\nCONFIG += incremental\nQMAKE_INCREMENTAL_STYLE = sublib\n\ninclude(../common/linux.conf)\ninclude(../common/gcc-base-unix.conf)\ninclude(../common/g++-unix.conf)\n\n# modifications to g++.conf\nQMAKE_CC = arm-linux-gnueabihf-gcc\nQMAKE_CXX = arm-linux-gnueabihf-g++\nQMAKE_LINK = arm-linux-gnueabihf-g++\nQMAKE_LINK_SHLIB = arm-linux-gnueabihf-g++\n\n# modifications to linux.conf\nQMAKE_AR = arm-linux-gnueabihf-ar cqs\nQMAKE_OBJCOPY = arm-linux-gnueabihf-objcopy\nQMAKE_NM = arm-linux-gnueabihf-nm -P\nQMAKE_STRIP = arm-linux-gnueabihf-strip\nload(qt_config)\n```\n\n由于我们从`linux-arm-gnueabi-g++ `文件夹复制了配置文件`qmake.conf`并将其重命名为`linux-arm-gnueabihf-g++ `，因此我们还必须重命名前缀，使其与跨工具链软件包名称相匹配。 关键字*`gnueabi`表示我们从中复制的文件夹使用用于`armel`体系结构的跨工具链包，而我们用于新文件夹的关键字*`gnueabihf`使用用于`armhf`体系结构的跨工具链包。 使用的跨工具链软件包的类型将基于您正在运行的硬件。\n\n4.  完成后，打开命令提示符，在源文件夹之外的某个位置创建一个 Build 文件夹，然后导航到 Build 文件夹：\n\n```cpp\nmkdir qt-build\ncd qt-build\n```\n\n5.  然后，我们可以使用以下命令开始构建过程：\n\n```cpp\nC:\\Qt\\5.12.5\\Src\\qtbase\\configure -platform win32-g++ -xplatform linux-arm-gnueabihf-g++ -release -opengl es2 -device linux-beaglebone-g++ -sysroot C:/SysGCC/beaglebone/arm-linux-gnueabihf/sysroot -prefix /usr/local/qt5\n```\n\n构建过程将需要一些时间，完成后，您应该看到`qmake.exe`现在位于`qt-build/qtbase/bin`内部。 在构建过程中可能会遇到一些错误消息，但只要已经构建了`qmake.exe`，就没有问题。 要验证这一点，请键入以下命令：\n\n```cpp\n..\\qtbase\\bin\\qmake -v\n```\n\n您应该看到 qmake 的版本号，这意味着它是一个成功的构建。 接下来，我们将构建整个 Qt 框架。 我们需要做的第一件事是再次打开 Qt 源目录中的`qtbase`文件夹中的`configure`文件，然后在显示`Creating qmake...`文本之前添加突出显示的代码：\n\n```cpp\n# build qmake\nif [ '!' -f \"$outpath/bin/qmake.exe\" ]; then\n    echo \"Creating qmake...\"\n    mkdir -p \"$outpath/qmake\" || exit\n```\n\n然后，运行以下命令开始配置 Qt 框架。 该命令类似于前一个命令，但具有`device-option`设置：\n\n```cpp\nC:\\qt-everywhere-src-5.13.1\\configure -platform win32-g++ -xplatform linux-arm-gnueabihf-g++ -release -opengl es2 -sysroot C:/SysGCC/beaglebone/arm-linux-gnueabihf/sysroot -prefix /usr/local/qt5 -device-option CROSS_COMPILE=C:/SysGCC/beaglebone/bin/arm-linux-gnueabihf- -qt-xcb\n```\n\n如果没有出错，则可以通过键入以下命令继续构建 Qt 框架：\n\n```cpp\nmake && make install\n```\n\n请注意，每个硬件都有非常不同的设置和配置，其工作方式可能与前面的示例不同。 但是，如果发生任何情况，命令提示符应该会告诉您问题所在，因此您可能需要一些耐心来调整配置，直到它适用于您的平台。 如果您的设备正式受 Qt 支持，请使用上一小节*使用 Qt Creator*自动交叉编译中描述的内置方法。\n\nQt 框架搭建成功后，打开位于`C:\\SysGCC\\beaglebone\\tools\\PortableSmartty`的`SmarTTY.exe`。 然后，使用 SmarTTY 通过 SSH 连接到您的设备，我们在前面的步骤中已经了解了这一点。 接入后，进入 SCP|上传目录，如下图所示：\n\n![](img/8e290021-a054-4a8b-8f43-cb1b3187353f.png)\n\n将弹出一个窗口，让您设置要上传的目录和远程设备上的目标位置。 将本地目录设置为`C:\\SysGCC\\beaglebone\\arm-linux-gnueabihf\\sysroot\\usr\\local\\qt5`，将远程目录设置为`/usr/local/qt5`：\n\n![](img/c9c4f2ef-0838-46b0-9a50-b0d480f75308.png)\n\n之后，按 Upload 按钮开始将文件上传到您的嵌入式设备。 就这样!。 您已经成功地将整个 Qt 库上传到嵌入式设备，然后可以在其上运行您的 Qt 应用。\n\n接下来，我们将学习如何为嵌入式设备手动编译 Qt 应用。 在开始编译应用之前，我们需要查看 Qt Creator 上的一些设置：\n\n1.  首先，打开 Qt Creator，进入工具|选项...，然后导航到工具包|编译器。\n2.  然后，点击位于右侧的添加按钮，选择 GCC|C++。 设置编译器设置，如以下屏幕截图所示：\n\n![](img/556d3f27-3201-4c4b-ae3e-5c2b2e0361d9.png)\n\n3.  对 GCC|C 重复上述步骤，并按如下方式设置设置：\n\n![](img/7815bd8d-3d77-4c82-890e-e8738c4f34d7.png)\n\n4.  之后，导航到 Debuggers(调试器)选项卡，然后单击 Add(添加)按钮。 将调试器命名为`Beaglebone`，其余设置如下：\n\n![](img/64bf262a-7fc8-4a97-88fc-a1640551a167.png)\n\n5.  然后，转到 Qt Versions 选项卡并单击 Add...。 纽扣。\n6.  选择我们在前面的步骤中构建的`qmake.exe`，并将其命名为`Qt Beaglebone %{Qt:Version} (build-qt)`。\n7.  最后，转到 Kits 选项卡并单击 Add 按钮。 将该工具包命名为`Beaglebone`，并链接我们在前面步骤中刚刚添加的编译器、调试器和 Qt 版本。\n\n现在您应该能够为您的嵌入式设备交叉编译 Qt 应用了。 您将无法将生成的可执行文件启动到 Windows 计算机，因为它是 Linux 可执行文件。 我们将在本章第二节*将 Qt 应用部署到嵌入式系统*一节中讨论如何手动将应用部署到您的设备上。\n\n在本节中，我们了解了如何在 Windows PC 上为 Linux 设备交叉编译 Qt 应用。 让我们继续下一节，学习如何为嵌入式项目配置我们的 Qt 框架。\n\n# 为嵌入式项目配置 Qt\n\n正如您可能已经知道的，Qt 框架附带了一组巨大的库，这些库加起来可能有几 GB 的存储空间。 即使在最低限度，这些库也可能占用高达 15 兆字节的存储空间，对于小型嵌入式设备来说，这太过分了。 为了解决这个问题，Qt 公司为我们提供了一个工具，用于在我们从嵌入式项目的源代码构建 Qt 框架之前配置我们的 Qt 包。 为了减小 Qt 库大小，我们可以挑选我们想要的特性，并丢弃不需要的特性。\n\n该工具名为 Qt Configuration Tool 或`qconfig-gui.exe`，位于`C:\\Qt\\Tools\\QtConfigGui`文件夹中，如下所示：\n\n![](img/515ce334-9532-4d07-b37a-c792b9ad53a7.png)\n\n按 Run Configure(运行配置)按钮开始配置过程，该过程需要一些时间才能完成。 完成后，您可以打开命令提示符，并使用以下`mingw32-make`命令从源代码构建 Qt 库：\n\n```cpp\ncd C:\\Qt\\5.12.5\nmingw32-make\n```\n\n构建过程根据您的配置而有所不同。您打开的功能越多，构建 Qt 库所需的时间就越长。 通常情况下，最多需要几个小时才能建成，因为 Qt 是一个非常庞大的库。 构建过程如下所示：\n\n![](img/23142e0e-752a-4ef8-9b27-a560c89bed53.png)\n\n完成后，不要忘记运行`install`命令：\n\n```cpp\nmingw32-make install\n```\n\n这会将所有已编译的库文件复制到适当的目录，如：`C:\\Qt\\Qt-5.12.5`。 如果您发现需要根据需要更改功能列表，可以重新配置和重新编译。\n\n在本节中，我们学习了如何配置 Qt 并使其更小以适合我们的嵌入式设备。 在下一节中，我们将继续使用 Qt Quick 编写我们的第一个嵌入式程序。\n\n# 编写您的第一个嵌入式程序\n\n在 Qt 中，为台式机、移动设备或嵌入式设备构建应用没有区别。 您可以像往常一样创建项目，但唯一的区别将是构建过程。 让我们通过转到文件|新建文件或项目管理并选择 Qt Quick Application-Empty 来创建一个 Qt Quick 项目。 确保在创建 Qt Quick 项目时勾选了 Use Virtual Keyboard(使用虚拟键盘)选项。\n\n完成后打开`main.qml`。 默认情况下，代码包含`InputPanel`项，即虚拟键盘：\n\n```cpp\nInputPanel {\n    id: inputPanel\n    z: 99\n    x: 0\n    y: window.height\n    width: window.width\n\n    states: State {\n        name: \"visible\"\n        when: inputPanel.active\n        PropertyChanges {\n            target: inputPanel\n            y: window.height - inputPanel.height\n        }\n    }\n    transitions: Transition {\n        from: \"\"\n        to: \"visible\"\n        reversible: true\n        ParallelAnimation {\n            NumberAnimation {\n                properties: \"y\"\n                duration: 250\n                easing.type: Easing.InOutQuad\n            }\n        }\n    }\n}\n```\n\n如果您现在在模拟器上构建并运行该程序，您将看不到任何东西，因为虚拟键盘从视图中隐藏。 让我们导入`QtQuick.Controls`模块并将`TextField`项添加到我们的程序中：\n\n```cpp\nimport QtQuick.Controls 2.12\n\nWindow {\n    id: window\n    visible: true\n    width: 640\n    height: 480\n    title: qsTr(\"Hello World\")\n\n    TextField {\n        id: textInput\n        x: 100\n        y: 100\n        placeholderText: qsTr(\"Enter text here\")\n    }\n```\n\n如果您再次在模拟器上运行该程序，您应该会在屏幕上看到文本字段。 单击文本字段以触发虚拟键盘：\n\n![](img/f9801f26-77ce-4f03-8bd3-d715a3dce5d0.png)\n\n就这样。 除了虚拟键盘，Qt Quick 提供的所有其他物品都可以像桌面和移动应用一样使用。 Qt 模糊了桌面、移动和嵌入式应用开发之间的界限，简化了开发流程，使开发人员的生活更轻松。\n\n在本节中，我们学习了如何使用 Qt Quick 开发我们的第一个嵌入式程序，以及如何启用虚拟键盘功能。 在下一节中，我们将学习如何将我们的应用部署到实际的嵌入式设备上，并使其成为默认的启动程序。\n\n# 将 Qt 应用部署到嵌入式系统\n\n最后，我们将学习如何将我们的应用部署到实际的物理设备上。 我们将介绍两种方法-自动方式和手动方式。 自动方式需要 Qt 商业许可证，此功能与 Qt Creator 一起提供。 另一方面，人工方式不需要商业许可，但它的过程比前者长得多。 让我们来看看每一个。\n\n# 从 Qt Creator 自动部署\n\nQt 商业许可证附带一个名为**Boot to Qt Flashing Wizard**的工具，我们在*设置嵌入式 Linux 映像*一节中讨论了该工具。 与我们在*构建交叉编译的 Qt 应用*一节中讨论的在 Qt 中手动交叉编译的方式不同，Qt 商业版附带的 Linux 镜像针对嵌入式开发进行了优化，并支持从您的 Qt Creator 进行远程部署。 这意味着你不需要付出任何额外的努力就可以让它工作。\n\n但是，在您可以轻松地部署嵌入式应用之前，您必须在 Qt Creator 中设置您的设备，以便它知道将应用导出到哪里。 这实际上与*构建交叉编译的 Qt 应用*一节中讨论的 SSH 方法非常相似，但是该功能与 Qt Creator 本身一起提供，因此这里不需要第三方程序。\n\n要设置您的设备，请转到工具|选项...。 |设备。 在 Devices(设备)选项卡下，单击 Add(添加)...。 位于窗口右侧的按钮。 之后，将弹出一个名为 Device Configuration Wizard Selection(设备配置向导选择)的窗口：\n\n![](img/d83bce8c-ceee-4f14-9fe4-5d2f543f72bb.png)\n\n选择 Boot2Qt Device 选项，然后单击 Start Wizard。 配置向导将引导您完成几个步骤来设置设备。 设置应与您看到的内容类似，如下所示：\n\n![](img/e7bed259-9006-4840-b6b5-21956f40b87a.png)\n\n如果无法使用`username@devicename`格式连接，则改用本地 IPv4 IP 地址(即 192.168.0.140)。 IP 地址可以在运行 Boot to Qt 闪存向导提供的 Linux 映像的嵌入式设备上的 Qt 启动器中找到。 它位于网络设置窗口下的网络部分。 在获取 IP 地址之前，您的设备必须先连接到网络：\n\n![](img/390caef2-36a4-4dac-bb4f-9e0a6e89ff6c.png)\n\n完成后，按 OK 按钮关闭选项...。 Qt Creator 中的窗口。 之后，确保您选择的是正确的套件(即 Boot2Qt 5.12.5 Intel NUC)，而不是仿真器。 之后，按 Run(运行)按钮并等待一段时间。 构建应用并将其发送到设备后，您应该会看到应用正在硬件上运行：\n\n![](img/afed0d75-15d5-446d-be14-14160e0dbd85.png)\n\n但是，如果您现在重新启动嵌入式设备，Qt 启动器将在启动时启动，而不是您的应用。 要覆盖默认启动应用，请单击左侧面板上的 Projects(项目)按钮，然后打开 Run Setting(运行设置)界面：\n\n![](img/de270ad3-ef1f-4e20-8b32-5d0eabd59378.png)\n\n在 Deployment 部分下，单击 Add Deploy Step 按钮并选择 Change Default Application：\n\n![](img/fea46daf-de18-46ed-976e-64c30e675edd.png)\n\n之后，选择将此应用设置为默认启动选项并保存您的项目。 再次从 Qt Creator 运行您的应用，以便在嵌入式设备上执行此步骤。 然后，重新启动您的设备，瞧！ 您的程序现在是打开设备时启动的默认应用。\n\n# 使用 SSH 手动部署\n\n手动将交叉编译的应用部署到嵌入式设备的方法是通过 SSH 方法，我们在*构建交叉编译的 Qt 应用*一节中已经讨论过。 在我们开始使用 SmarTTY 上传我们的 Linux 可执行文件之前，让我们先来看看 Qt 是如何做到这一点的。\n\n如果我们探索包含来自 Qt 的 Linux 镜像的 SD 卡或拇指驱动器，我们很快就会意识到它并不像我们想象的那样是某种黑魔法。 Qt 只是将我们的 Qt 应用存储在`opt`目录中，例如，存储在`/opt/myproject/bin/myproject`目录中，并从那里启动它。\n\n对于默认的启动应用，Qt 支持 Debian Linux 系统使用的`systemd`服务。 这些设置可以在`/lib/systemd/qtlauncher.service`中找到，如下所示：\n\n```cpp\n[Unit]\nDescription=B2Qt Launcher Demo\nAfter=systemd-user-sessions.service\nConditionPathExists=!/usr/bin/b2qt\n\n[Service]\nUser=root\nExecStart=-/usr/bin/appcontroller /usr/bin/qtlauncher --applications-root /data/usr/qt\n\n[Install]\nWantedBy=multi-user.target\n```\n\n这些设置基本上是告诉用户`systemd`在`systemd-user-sessions.service`启动后再启动这项服务。 然后，它将查找名为`b2qt`的快捷链接，该链接位于`/usr/bin/`。 `b2qt`快捷键实质上链接到我们的 Qt 应用(即`/opt/myproject/bin/myproject`)并启动它。\n\n如果快捷方式链接不存在，则会启动 Qt 启动器。 就是这么简单。 因此，如果我们想要模仿这种方法，我们可以首先使用 SmarTTY 将我们的应用上传到设备的`opt`目录，方法是转到 SCP|Upload a file：\n\n![](img/8fe0b72f-8cc0-4fc8-880b-80aa5b74438b.png)\n\n上传可执行文件后，打开`/lib/systemd`并创建一个扩展名为`.service`的新文件。 您可以复制前面的脚本并根据您的喜好进行更改，例如添加自定义描述名称、自定义目录路径等。 然后，运行以下命令以启用您的自定义服务：\n\n```cpp\nsudo systemctl enable yourservice.service\n```\n\n注意，这只适用于使用`systemd`的 Linux 系统，比如 Debian。 如果您使用的 Linux 系统运行其他一些启动管理器/初始化守护进程，如`initd`、`Runit`、`Upstart`或许多其他进程，则不能使用前面的方法。 互联网上有大量的资源可以教你如何与那些不同的初创企业经理一起做这件事。\n\n在本节中，我们了解了如何使用自动和手动方法将 Qt 应用部署到嵌入式设备。\n\n# 简略的 / 概括的 / 简易判罪的 / 简易的\n\n在这一章中，我们经历了一段漫长的旅程，学习如何增强 Qt 的跨平台能力，并创建了我们的第一个嵌入式设备应用。 我们了解了如何设置嵌入式 Linux 映像，并将其写入 SD 卡或拇指驱动器，以便在设备上运行。\n\n然后，我们学习了如何从 Windows 机器交叉编译 Qt 项目并将其导出到 Linux 设备。 这确实是一种节省时间的方法，因为嵌入式设备通常不足以编译源代码，因此对于我们来说，能够使用不同的、更强大的设置编译它并将其远程传送到我们的生产设备上，甚至不需要用 USB 电缆连接，这对我们来说真的很方便。\n\n除此之外，我们还了解了如何配置 Qt 框架并减小其大小，使其能够适应嵌入式设备。 这一点非常重要，因为嵌入式设备(即可穿戴设备、智能机器人、医疗设备等)通常体积非常小，存储空间非常有限。\n\n之后，我们还学习了如何在第一个嵌入式程序中触发虚拟键盘。 这一点也非常重要，因为大多数嵌入式设备没有用于用户输入的键盘或鼠标；因此，虚拟键盘在确保用户能够在不需要键盘的情况下键入信息方面发挥着重要作用。\n\n最后，本章最酷的部分是我们学习了如何在实际的嵌入式设备上部署我们的应用，并看到我们的辛勤工作取得了成果。 我真的希望越来越多的人可以通过使用这些信息来获得力量，并生产出真正令人惊叹的产品，或许可以让世界变得更好。\n\n在下一章中，您将学习一些提示和技巧，以帮助您改进项目的开发工作流程和生产力。"
  },
  {
    "path": "docs/app-dev-qt-creator/14.md",
    "content": "# 十四、QT 提示和技巧\n\n在前面的章节中，我们讨论了 Qt 对于软件开发的伟大之处：如何编辑、编译和调试应用；如何分析它们的执行和内存性能；如何针对世界不同地区本地化它们；以及如何制作在 Android 手机和平板电脑上运行的移动应用。\n\n在本章中，我们将讨论在使用 Qt Creator 和 Qt 时应该知道的一些提示和技巧，它们会让您像专业人士一样编写软件。\n\n我们将在本章介绍以下主题：\n\n*   使用 Qt Creator 编写控制台应用\n*   与版本控制系统集成\n*   配置编码样式和编码格式选项\n*   将新主题应用于 Qt Creator\n*   设置 Qt 快速窗口显示选项\n*   从 CMake 和命令行生成项目\n*   同时运行多个调试器\n*   了解有关 Qt 的更多信息\n\n# 技术要求\n\n本章的技术要求包括 Qt 5.12.3、MinGW 64 位、Qt Creator 4.9.0 和 Windows 10。\n\n# 使用 Qt Creator 编写控制台应用\n\n还记得[第 1 章](01.html)中的`Hello World`，*Qt Creator 入门*吗？ 这是一个控制台应用--几乎是您所能编写的最简单的控制台应用。 让我们回顾一下代码：我们创建了一个新的 Qt 控制台应用，并在`main.cpp`中编写了以下代码行：\n\n```cpp\n#include <QCoreApplication> \n#include <iostream> \n\nusing namespace std; \n\nint main(int argc, char *argv[]) \n{ \n    QCoreApplication a(argc, argv); \n\n    cout << \"Hello world!\"; \n\n    return a.exec(); \n} \n```\n\n任何有效的 C++ 文件在 Qt 应用中都是有效的，包括**标准模板库**(**STL**)代码。 如果您需要用 C++ 编写一个小工具，并且还没有学到很多关于 Qt 的知识--您知道的关于 C++(如果您愿意，甚至可以使用 C)的所有内容都可以在 Qt Creator 中访问，那么这就特别方便了。\n\n尽管 Qt 是最广为人知的 GUI 工具包，但值得一提的是，Qt Core 库是每个 Qt 应用(包括 Qt 控制台应用)的一部分，它包含大量实用程序和模板类，包括：\n\n*   集合类，包括`QList`、`QVector`、`QStack`和`QQueue`，用于保存列表和向量以及用于后进先出和先进先出数据存储\n*   字典类(也称为**哈希表**)，包括`QMap`和`QHash`\n\n*   具有`QFile`和`QDir`的跨平台文件的 I/O\n*   `QString`支持 Unicode 字符串\n\n为什么要选择 Qt 的类，而不是普通的 C++ 提供的类？ 原因有几个：\n\n*   **内存性能**：与 STL 集合不同，Qt 集合是基于引用的，并使用写入时复制来节省内存。 Qt 集合通常比其对应的 STL 集合占用更少的内存。\n*   **Iteration**：迭代 Qt 集合是安全的，有保护的访问以防止您走出集合的末尾。\n*   **可读性**：在整个应用中使用 Qt 代码和库可以提供统一的外观，从而使代码更易于维护。\n*   **可移植性**：在某些可用 Qt 的嵌入式平台上，STL 可能不存在。 然而，这几乎不是第一次编写 Qt 时的问题。\n\n值得注意的是，Qt 的集合通常比 STL 对应的集合稍微慢一些。 在使用 Qt 类处理数据时，您通常会牺牲内存性能来换取速度。 然而，在实践中，这很少是问题。\n\n`QFile`和`QDir`类值得特别提及，因为有一件事-可移植性。 即使是目录分隔符也是以可移植的方式处理的；无论您是在 MacOS、Linux 还是 Windows 上运行，目录始终由单个`/`分隔，这使得以与平台无关的方式编写代码并确保其在所有平台上运行都很容易。 在幕后，Qt 转换目录字符串，以便在访问文件时使用特定于平台的目录分隔符。\n\n接下来，我们将学习如何将我们的 Qt Creator 与版本控制系统集成。\n\n# Qt Creator 与版本控制系统的集成\n\n几乎所有的大型项目都需要某种版本控制来协调不同用户对同一文件所做的更改，并确保源代码库的更改和谐进行。 即使是单个开发人员也可以从使用版本控制中受益，因为版本控制提供了开发人员编辑的每个文件中更改内容的记录，并提供了一段时间以来项目的宝贵历史记录。 Qt Creator 支持以下版本控制系统：\n\n*   Bazaar(Qt Creator 2.2 版及更高版本支持)\n*   绒毛膜绒毛取样\n*   讨厌的人 / 卑鄙的人 / 饭桶\n*   Mercurial(Qt Creator 2.0 版及更高版本支持)\n*   PERFORCE(支持 PERFORCE 服务器版本 2006.1 和更高版本)\n*   颠覆\n\n除了这些，Qt Creator 还支持一些商业版本控制托管服务，包括：\n\n*   GitHub\n*   GitLab\n\n您需要做的第一件事是为您的项目设置一些版本控制软件。 如何做到这一点取决于您选择的版本控制系统(例如，它可能由您的组织规定，或者您可能对过去的项目有个人偏好)，并且会因系统而异。 因此，我们不会在这里深入讨论。 但是，您需要有一个存储库来存储源代码的版本，并在您的工作站上安装相应的版本控制软件，并在系统的`PATH`变量中包含版本控制二进制文件的相应目录，以便 Qt Creator 可以找到它们。 重要的是，您能够从系统的 Shell(如 PowerShell 或本地终端提示符)访问版本控制命令，因为 Qt Creator 以相同的方式访问它们。\n\n完成此操作后，您可以通过导航到 Tools|Options...来配置 Qt Creator 与版本控制的交互方式。 |版本控制。 以下屏幕截图显示了此窗口：\n\n![](img/cb337539-1517-4842-bd7a-e535f706a21c.png)\n\n有适用于您正在使用的任何版本控制系统的通用配置选项，然后还有针对 Qt 支持的每种版本控制风格的特定选项。 以下是常规选项：\n\n*   可在任何提交邮件上运行以确保邮件格式正确或包含正确信息的脚本\n*   源代码控制系统的名称和别名列表\n*   要包括在每封提交邮件中的字段列表\n*   在使用 SSH 访问版本控制系统时，用于提示您输入 SSH 密码的 SSH 提示符命令\n\n一些版本控制系统，如 Git 和 Mercurial，支持本地版本控制存储库。 如果您单独执行开发项目，并且只需要一个地方来备份您的更改(当然，请记住也要备份源代码存储库目录)，这将非常方便。 如果您使用的是其中一个系统，则可以使用 Qt 直接创建本地存储库目录，方法是选择 Tools(工具)下的 Create Repository(创建存储库)，或者转到 File|New File(新建文件)或 Project(项目)，然后转到最后一个项目管理页面。 当然，要做到这一点，您需要先安装版本控制软件。\n\nYou can find out more about how Qt Creator integrates with version control systems by taking a look at the Qt documentation at [https://doc.qt.io/qtcreator/creator-version-control.html](https://doc.qt.io/qtcreator/creator-version-control.html).\n\n如果安装和配置版本控制系统，则此系统提供的各种命令将添加到 Qt Creator 的工具菜单的子菜单中。 从那里，您可以执行以下步骤：\n\n*   导航到窗口|输出窗格|版本控制，查看版本控制命令输出。\n*   从您的版本控制系统中查看不同的输出(两个同名不同版本的文件之间的比较)，使您可以看到您正在编辑的文件中与存储库中的文件相比发生了哪些更改。\n*   通过选择日志或文件日志，查看受版本控制的文件的 ChangeLog。\n*   通过选择提交或提交将文件更改提交到系统。\n*   通过选择还原将更改还原到文件。\n*   单击 Update，使用版本控制系统的当前内容更新您的工作目录。\n*   使用附加的按版本控制命令来支持分支、存储和远程存储库。\n\nIf you're just starting out and need to choose a version control system, perhaps the best thing to do is to look at the comparison of various systems on Wikipedia, at [http://en.wikipedia.org/wiki/Comparison_of_revision_control_software](http://en.wikipedia.org/wiki/Comparison_of_revision_control_software), and get familiar with one. Personally, I prefer Git for my work, using both local repositories and hosted repositories such as GitHub. It's free, fast, has good support for branching, and is well supported by Qt Creator.\n\n接下来，我们将介绍如何为您的版本控制系统设置第三方托管服务，如 GitHub 和 GitLab。 实际上，这些服务非常简单。 您可以通过 Git 登录到这些服务，就像您的自托管存储库一样。\n\n# 设置您的 GitHub 存储库\n\n首先，如果你还没有 GitHub 账户，那就注册一个。 注册页面位于网站首页，地址为[https://github.com](https://github.com)。 然后，完成注册过程后，单击位于右上角的个人资料图片，然后单击下拉菜单上的 Your Repositories(您的存储库)选项：\n\n![](img/0ad61771-775b-4a7a-9624-878a1c0f96f1.png)\n\n在那里，您将看到绿色的 New 按钮。 按 New 按钮为您的项目创建一个新的 GitHub 存储库。 然后，系统将指示您创建一个新的存储库页面，在该页面中，您可以通过在所有者下拉框旁边插入存储库名称来创建新的存储库：\n\n![](img/c5f16386-4ee6-4dd9-b86e-948626b37a38.png)\n\n之后，您可以通过选中其中一个选项来设置项目是公共项目还是私人项目。 然后，选中 Initialize this pository with a readme 选项，因为您正在创建一个全新的存储库。 GitHub 创建的`README`文件将成为存储库的首页。\n\n您还可以设置`.gitignore`选项，该选项告诉 GitHub 忽略一些作为临时文件或用户特定文件的文件，例如`make`文件和用户的项目设置文件(`.pro.user`)。 在本例中选择 Qt 选项，因为我们在本书中开发的是 Qt 项目。 您还可以通过选择 Add a License 下拉框中的一个选项将许可证添加到您的项目中：\n\n![](img/63456c6b-ca1b-434e-b6a4-d375ac658a79.png)\n\n单击创建存储库按钮后，您将进入以下页面：\n\n![](img/1e081b95-649f-46e6-8267-9131ca5429ed.png)\n\n这些命令可用于将项目链接到 GitHub。 不过，您也可以在 Qt Creator 中执行此操作。 转到工具|Git|创建存储库...，然后选择您的项目文件夹并单击选择文件夹。 这将创建一个在您的 PC 上运行的本地存储库。 Git 系统能够链接到两个存储库-一个本地存储库和一个远程存储库。 您必须先将代码提交到本地存储库，然后才能将其推送到远程服务器。\n\n现在您已经创建了本地存储库，让我们继续远程存储库：\n\n1.  转到工具|Git|远程存储库|管理远程....。 将打开一个窗口，其中显示您已连接到的当前远程服务器，该窗口目前为空。\n2.  单击 Add...(添加...)。 按钮，并将您的 GitHub URL 添加到远程列表中。 URL 可以在快速设置网页上找到，如上一个屏幕截图所示。\n3.  一旦您链接了 GitHub 帐户，您就可以尝试并单击 FETCH 或按钮来验证连接：\n\n![](img/a7d0fdb1-0c02-4474-9f08-d4110f7836ca.png)\n\n如果 SSH 链接无法工作，您也可以通过单击快速设置网页上的 HTTPS 按钮切换到 HTTPS 链接。\n\n要将代码提交到本地存储库，只需转到工具|Git|本地存储库|提交...。 Qt Creator 上将出现一个窗口，如下所示：\n\n![](img/d2e14e37-f2b8-4d92-bcda-04da05ac82d3.png)\n\n选择要提交的文件，键入描述，然后单击提交按钮。 在此之后，您可以继续将代码推送到 GitHub 存储库。 如果转到工具|Git|远程存储库|推送，您可能会看到如下错误：\n\n```cpp\nfatal: No configured push destination.\nEither specify the URL from the command-line or configure a remote repository using\n\n    git remote add <name> <url>\n\nand then push using the remote name\n\n    git push <name>\n\nThe command \"C:\\Program Files\\Git\\cmd\\git.exe\" terminated with exit code 128.\n```\n\n这是因为 Git 不知道默认推送到哪台远程服务器，Qt Creator 中也没有解决方案，所以需要使用`git`命令来实现：\n\n```cpp\ngit push -u \"resp\" master\n```\n\n这里，`resp`是您的存储库名称，`master`是存储库的主分支。\n\n或者，您也可以在每次要推送项目时使用**Git Gui**工具。 转到工具|Git|Git Tools(Git 工具)|Git Gui(Git 图形用户界面)。 将出现如下所示的窗口：\n\n![](img/bc14fe83-a1d2-4a55-bf70-b9e254c400bb.png)\n\n你也可以在这里做出承诺和推送。 如果您进入远程推流|推流，会弹出一个窗口，您可以在该窗口中选择您要推送到的远程服务器：\n\n![](img/c684fd15-2240-4bc5-89e1-4029f943174a.png)\n\n单击按钮后，您将看到如下成功状态：\n\n![](img/56fe0fe9-8b36-4276-96c6-891996e00823.png)\n\n在本节中，我们了解了如何将 Qt Creator 链接到 GitHub 帐户。 接下来，我们将看看如何为 GitLab 做同样的事情。\n\n# 设置 GitLab 存储库\n\nGitLab 实际上与 GitHub 大同小异。 要创建帐户，请转到[http://gitlab.com](http://gitlab.com)，然后单击右上角的注册按钮。 创建帐户并登录 GitLab 后，您将看到以下屏幕：\n\n![](img/d84285a1-09e4-4aed-a859-4271103b5402.png)\n\n单击 Create a project 按钮，它将带您进入新的项目页面。 与 GitHub 类似，您需要填写项目名称和有关项目的其他信息：\n\n![](img/50c7171a-7f88-495f-90f0-663d7ea91254.png)\n\n完成后，单击 Create Project 按钮以完成该过程。 要获取 URL，请从顶部面板转到 Project 页面，然后单击您的存储库。 之后，点击 Clone 按钮，URL 将出现在弹出的面板上：\n\n![](img/23c1ad73-6736-4cb0-973c-b632e555f60f.png)\n\n之后，您可以按照 GitHub 示例中的步骤将 URL 应用到您的 Qt Creator。\n\n接下来，我们将了解如何在 Qt Creator 中配置编码样式和编码格式。\n\n# 配置编码样式和编码格式选项\n\n代码的可读性至关重要，而 Qt Creator 的默认编码风格是大多数人认为非常易读的样式。 但是，您可能正在使用不同的编码指导原则，或者您可能只是发现您无法忍受 Qt Creator 编辑器如何处理代码格式化的特定方面；可能是方括号的位置或 Switch 语句的格式化方式。 幸运的是，Qt Creator 具有极强的可配置性。 转到工具|选项...。 |C++ 并配置 Qt Creator 如何格式化您的代码，如下图所示：\n\n![](img/69ab4ee8-6c73-4623-a072-368dc4e95701.png)\n\nBasic 对话框允许您选择流行的格式样式，例如 Qt 的默认格式或大多数 GNU 代码使用的格式。 您还可以单击 Edit...，这将打开代码样式编辑器，您可以在下一个屏幕截图中看到：\n\n<q>![](img/4a17e559-0bb0-4bdd-886c-6d5cafee2d3c.png)</q>\n\n您需要首先复制内置样式并根据您的喜好对其进行编辑；从编辑代码样式对话框中，您可以选择制表符是空格还是制表符、每个制表位的空格数以及如何处理行连续。 每个窗格允许您调整代码格式的特定方面，如下所示：\n\n*   内容窗格允许您调整类主体的格式，包括公共声明、受保护声明和私有声明的间距。\n*   “花括号”窗格允许您控制与花括号相关的格式设置。\n*   “Switch”窗格允许您控制`switch`和`case`语句的格式。\n*   “对齐”窗格允许您控制代码如何在连续行之间对齐。\n*   “指针”窗格允许您控制指针声明周围的间距。\n\n人们很容易为所有这些选项而疯狂，但我劝你不要这么做；乍看起来不错的东西，当你日复一日地看到它时，往往会变得一团糟。 如果您刚刚开始使用 Qt，请坚持默认格式，并记住一句古老的格言：在编辑现有代码时“不做坏事”-与已有的格式相匹配。\n\n继续外观，让我们继续学习如何更改 Qt Creator 实例的外观。\n\n# 将新主题应用到 Qt Creator\n\n除了代码样式的配置，Qt Creator 还允许我们更改整个程序的配色方案和样式。 这有助于我们更改用户界面外观！ 要执行此操作，请转至工具|选项...|环境，您将在用户界面和设置下看到主题设置选项：\n\n![](img/e27a6339-f5dd-4f35-8f52-154a5e633001.png)\n\n默认情况下，QT Creator 使用的是扁平主题，这也是我们一直熟悉的。 Qt 还为我们提供了一些较新的选项，如平板深色、平板设计、平板深色和平板灯光。 一定要试一试，看看有没有适合你口味的！ 请注意，您可能需要重新启动 Qt Creator 才能进行更改。\n\n这里值得一提的是 Qt Creator 的其他一些第三方主题：\n\n*   德古拉主题：11-13[HTTPS：//draculaheme.com/qtcreator](https://draculatheme.com/qtcreator)\n*   蓝天主题：[https://github.com/foxoman/bluesky](https://github.com/foxoman/bluesky)\n*   伊尔达尔·吉尔马诺夫的黑暗与光明主题：[https://github.com/gilmanov-ildar/qtcreator-themes](https://github.com/gilmanov-ildar/qtcreator-themes)\n*   弧形主题：https://github.com/elmodos/qt-creator-arc-theme\n\n您可以下载这些主题，并将文件`.xml`复制到以下目录：\n\n*   **窗口**：↔`%APPDATA%\\QtProject\\qtcreator\\themes`\n*   **MacOS**：`~/.config/QtProject/qtcreator/themes/`\n*   **Linux**：11-13`~/.config/QtProject/qtcreator/themes/`\n\n然后，重新启动 Qt Creator，您应该可以在`Theme`选择框中找到主题名称。\n\n如果你对主题的配色方案不满意，你仍然可以通过转到“工具”|“选项”来对其进行一些调整……。 |文本编辑器|字体和颜色。 在这里，您将看到一个允许您更改代码编辑器配色方案的选择框：\n\n![](img/d81b42f9-e517-4747-900f-1ade7473a951.png)\n\n通过更改配色方案的每个可用选项，您可以更上一层楼。 但是，您不能更改原始设置，而是需要首先通过按下复制...按钮复制选定的配色方案。 然后，将弹出一个小窗口，要求您为新复制的配色方案插入一个名称。 一旦使用从上一个配色方案复制的属性创建了新的配色方案，您就可以开始胡乱操作并更改任何您喜欢的设置。 这就是我要说的！\n\nQt Creator 确实让我们可以灵活地定制工具的外观，让我们在使用它时有宾至如归的感觉。\n\n让我们来看看一些不同的东西，除了调整 Qt Creator 的主题之外，我们还可以让我们的 Qt Quick 窗口脱颖而出。 让我们来看看如何更改 Qt 快速窗口显示选项的设置。\n\n# 设置 Qt 快速窗口显示选项\n\nQt Quick 非常适合为机顶盒或汽车电脑等非传统计算环境构建应用。 通常，在使用 Qt Quick 时，您会希望应用在这些设置中的窗口内容周围没有所有常见的窗口(如关闭框)，因为您试图基于 Qt Quick 应用(而不是主机平台上的窗口工具箱)呈现统一的用户界面。\n\n您可以通过编辑 Qt Quick 项目中的`main.cpp`文件轻松设置窗口选项。 默认情况下，它类似于以下代码片段：\n\n```cpp\n#include <QGuiApplication>\n#include <QtQuick/QQuickView>\n\nint main(int argc, char *argv[])\n{\n  QGuiApplication a(argc, argv);\n\n  QQuickView view;\n  view.setSource(QUrl(\"qrc:/qml/main.qml\"));\n  view.show();\n\n  return a.exec();\n}\n```\n\n此代码创建一个 Qt Quick Application Viewer，将其主 QML 文件(要加载的第一个文件)设置为指定的文件，然后在启动应用的事件循环之前显示该文件。 幸运的是，`QQuickView`有一个参数`setFlags`参数方法，允许您将参数`Qt::Window`参数传递给它初始化的窗口，以便显示您的 Qt Quick 应用。 这些标志包括以下内容：\n\n*   `Qt::FramelessWindowHint`：这表示窗口应该是无边框的(这适用于 Linux 系统，但不适用于 Windows)。\n*   `Qt::Popup`：表示弹出窗口。 您可以在 Windows 上使用此选项来获得几乎无边框的窗口，并带有轻微的投射阴影。\n*   `Qt::WindowStaysOnTopHint`：这表示该窗口应位于所有其他窗口之上。\n*   `Qt::WindowStaysOnBottomHint`：这表示该窗口应位于所有其他窗口之下。\n*   `Qt::Desktop`：表示窗口应在桌面上运行。\n\n我们将向您展示如何在即将到来的示例 C++ 代码中使用这些标志。\n\nA complete list of the flags can be found in the Qt documentation at [https://doc.qt.io/qt-5.9/qt.html#WindowType-enum](https://doc.qt.io/qt-5.9/qt.html#WindowType-enum).\n\n您还可以使用`QQuickView`的方法调整窗口的不透明度。\n\n例如，假设我们想要一个没有边框的蓝色窗口，但是希望有一个 75%不透明度的轻微阴影悬停在 Qt Quick 应用的所有其他窗口上。 我们将 QML 更改为如下所示：\n\n```cpp\nimport QtQuick 2.12 \n\nRectangle { \n    width: 360 \n    height: 360 \n    color: \"blue\" \n    Text { \n        text: qsTr(\"Hello World\") \n        anchors.centerIn: parent \n        font.pointSize: 18 \n    } \n    MouseArea { \n        anchors.fill: parent \n        onClicked: { \n            Qt.quit(); \n        } \n    } \n} \n```\n\n请注意我们的顶级矩形的声明`color: blue`。 接下来，我们将把`main.cpp`公式修改如下：\n\n```cpp\n#include <QGuiApplication>\n#include <QtQuick/QQuickView>\n\nint main(int argc, char *argv[])\n{\n  QGuiApplication a(argc, argv);\n\n  QQuickView view;\n  view.setOpacity(0.75);\n  view.setFlags(Qt::Popup | Qt::WindowStaysOnTopHint);\n  view.setSource(QUrl(\"qrc:/qml/main.qml\"));\n  view.show();\n\n  return a.exec();\n}\n```\n\n这里的关键代码行恰好在`view.setSource`之前：`setOpacity`方法设置主窗口的不透明度，而`setFlags`方法将主窗口的标志设置为将位于所有其他窗口之上的弹出窗口。 在运行应用时，我们将看到类似以下屏幕截图的内容：\n\n![](img/3518a983-000d-43f8-9a72-52d8661f0cd9.png)\n\n您可以使用此技巧为 Qt Quick 应用的显示方式提供各种效果。\n\n我们已经了解了如何为 Qt Quick 应用设置窗口显示选项。因此，接下来，我们将学习如何直接从命令行构建我们的项目。\n\n# 从 CMake 和命令行构建项目\n\n有时，您需要从命令行构建项目。 可能您正在 Linux 上工作，只是在那里更舒服，或者您在开会时在桌面上运行了一个远程会话。 或者，您可能希望在构建服务器上自动构建，并且需要知道 Qt 如何为您的构建执行编译魔术。\n\n# 使用 qmake 构建\n\n诀窍在于 qmake：qt 的 meta-make 系统，它为您已经安装的编译器工具链管理 make 文件的生成。 Qmake 命令获取您在[第 2 章](02.html)，*使用 Qt Creator*构建应用中第一次看到的`.pro`文件，并生成工具链构建应用所需的`make`或`nmake`文件。 让我们看看这是如何工作的：\n\n1.  首先，确保您的系统路径中有编译器和 set 实用程序；如何做到这一点在不同的开发环境中有所不同。 接下来，请确保路径中有用于 Qt 构建系统的命令-如果您已使用包管理器在 Linux 上安装了 Qt，则默认情况下会发生这种情况；在 MacOS 或 Windows 上，只需编辑路径以包含先前安装的 Qt 工具中相应的`bin`目录，即可轻松完成此操作。\n2.  接下来，打开一个命令窗口并切换到包含您的项目的目录：您的`.pro`文件应该在此目录的根目录下。 键入`qmake`，然后输入`make`(如果您的构建系统使用 make)或`nmake`(如果您使用的是 Microsoft Windows 工具链)。 这就是一切！\n3.  如果您有一个 C++ 项目，无论是否有 Qt，并且您错过了`.pro`文件，qmake 可以使用以下命令为您创建该文件：\n\n```cpp\nqmake -project\n```\n\n使用此命令，qmake 可以浏览文件夹和子文件夹中的所有 C++ 文件，并写入一个通用的 PRO 文件。 然后，您可以编辑此文件以更改目标名称或添加一些`qt`模块，如下面的语句所示，但一般来说，您会得到一个好的结果：\n\n```cpp\nqt += qt network xml\n```\n\n我们已经了解了如何使用 qmake 构建我们的 Qt 项目。 接下来，我们将学习如何使用 CMake 构建我们的项目。\n\n# 使用 CMake 构建\n\nCMake 是一组跨平台的工具，用于为不同类型的项目生成 make 文件，例如 Visual Studio 和 Qt。 CMake 使用一个名为“`CMakeLists.txt`”的简单配置文件，可以很容易地进行修改以满足您的需要。 由于 CMake 是第三方工具，因此您无法从 Qt 的官方文档中获得太多信息。 相反，您可以在[https://cmake.org/documentation](https://cmake.org/documentation)上阅读 CMake 提供的文档。\n\n因此，如果您决定使用 CMake 而不是 Qt 提供的默认 qmake，则不再为 Qt 项目创建`.pro`项目文件。 相反，您可以将`CMakeLists.txt`创建为项目文件，并手动将源文件、库和编译器设置的路径添加到`CMakeLists.txt`文件中。 您也不能从 Qt Creator 创建新的 Qt 项目，因为默认情况下它附带了 qmake 项目文件(`.pro`)。\n\nQt Creator 支持 CMake 3.0 及更高版本，这意味着最新版本将在您的 Qt Creator 上开箱即用。 如果您已在计算机上安装了 CMake，Qt Creator 将通过`PATH`设置自动查找 CMake。 如果您还没有，请访问[https://cmake.org](https://cmake.org)并下载最新版本的 CMake。\n\n您可以通过转到工具|选项...在 Qt Creator 中设置 CMake。 然后选择套件|CMake：\n\n![](img/226ba88a-27c7-45ef-8315-5ac4259e059f.png)\n\n如果 Qt Creator 没有检测到您的 CMake 目录，您可以单击 Add 按钮手动设置目录路径。 选中自动运行 CMake 复选框后，Qt Creator 将在您对任何`CMakeLists.txt`文件进行任何更改时自动运行 CMake。\n\n之后，转到 Kits 选项卡，并确保您在项目上运行的工具包选择了 CMake 工具：\n\n![](img/3f95f9d7-b1c4-4eac-8aee-492289021a64.png)\n\n完成后，我们现在可以开始创建第一个运行 CMake 的 Qt 项目，方法是在所需的目录中创建一个`CMakeLists.txt`文件。 之后，使用 Qt Creator 打开该文件。 您需要添加到该文件的第一件事是最低 CMake 要求，以便在我们的计算机上安装的 CMake 低于该版本时通知我们：\n\n```cpp\ncmake_minimum_required(VERSION 3.0.0)\n```\n\n然后，设置您的项目名称并让 CMake 在构建目录中查找头文件：\n\n```cpp\nproject(MyProject)\nset(CMAKE_INCLUDE_CURRENT_DIR ON)\n```\n\n之后，启用以下三个选项，以便 CMake 在构建时通过扫描头文件和源文件自动处理`moc`、`uic`和`rcc`构建目标，这将使您的工作更轻松：\n\n```cpp\nset(CMAKE_AUTOMOC ON)\nset(CMAKE_AUTOUIC ON)\nset(CMAKE_AUTORCC ON)\n```\n\n然后，我们需要在我们的项目中包含 Qt5 组件，这样它才能被正确编译和运行。 我们可以通过两种方式添加这些组件-第一种是逐个添加：\n\n```cpp\nfind_package(Qt5Core REQUIRED)\nfind_package(Qt5Widgets REQUIRED)\n```\n\n不过，我更喜欢第二种方式，即一次添加所有必需的组件：\n\n```cpp\nfind_package(Qt5 COMPONENTS Core Widgets REQUIRED)\n```\n\n如果您有任何要添加到项目中的 C++ 源文件、Qt 快速文件(`.qml`)、Qt 表单文件(`.ui`)、Qt 资源文件(`.qrc`)等，可以使用`add_executable`命令进行添加。 例如，请考虑以下内容：\n\n```cpp\nadd_executable(MyProject main.cpp mainwindow.cpp mainwindow.ui resource.qrc)\n```\n\n保存`CMakeLists.txt`文件后，Qt Creator 现在将刷新项目并在左侧的项目面板上显示源文件。 您可能注意到，项目树看起来与 qmake 使用的普通 Qt 结构略有不同：\n\n![](img/2ed5d5f8-741b-4e47-89e7-2755510de207.png)\n\n最后，我们必须将 Qt 库链接到我们的项目，以便它可以正确编译和运行：\n\n```cpp\ntarget_link_libraries(MyProject Qt5::Widgets)\n```\n\n现在，我们的`CMakeLists.txt`文件看起来像这样，非常简短简单：\n\n```cpp\ncmake_minimum_required(VERSION 3.0.0)\nproject(MyProject)\nset(CMAKE_INCLUDE_CURRENT_DIR ON)\n\nset(CMAKE_AUTOMOC ON)\nset(CMAKE_AUTOUIC ON)\nset(CMAKE_AUTORCC ON)\n\nfind_package(Qt5 COMPONENTS Core Widgets REQUIRED)\nadd_executable(MyProject main.cpp mainwindow.cpp mainwindow.ui resource.qrc)\ntarget_link_libraries(MyProject Qt5::Widgets)\n```\n\n您现在可以点击 Qt Creator 上的 Run 按钮来启动构建过程。 如果构建过程中没有错误，您的程序应该会在几秒钟内自动运行。 当然，前面的示例只是在 CMake 中启动项目的最基本配置。 如果您需要了解更高级的设置，请访问[https://doc.qt.io/qt-5/cmake-manual.html](https://doc.qt.io/qt-5/cmake-manual.html)和[https://cmake.org/documentation](https://cmake.org/documentation)。\n\n就这样。 我们已经了解了如何在 Qt 项目中使用 CMake，并熟悉了`CMakeLists.txt`文件的基本配置。 接下来，我们将学习如何同时运行多个调试器。\n\n# 同时运行多个调试器\n\n通常，我们的项目不仅仅是一个仅运行单个可执行文件的简单`hello world`应用，还可以是同时在服务器、客户端计算机甚至移动设备上运行的不同类型的可执行文件的集合。 例如，您的最终用户可能正在运行您从 Qt 构建的应用，该应用在他们的移动设备上运行。 然后，您就有了一个服务器应用，该应用也是从 Qt 构建的，它处理从用户的应用发送的信息。 最后，向您的服务器管理员提供使用 Qt 构建的管理软件，该软件在他们的 PC 上运行。\n\n要构建和维护这样一个庞大的应用，我们需要确保它总是容易的，并且让事情保持在一起。 为了确保我们可以轻松地编辑、构建和调试所有不同的应用，我们使用名为`subdir`的东西将这些项目组合到 Qt 中的一个项目中。 为此，我们将描述的所有三个示例程序放到一个文件夹中，并在其中创建一个空的`.pro`文件。 该目录如下所示：\n\n![](img/abdb1365-a329-43b4-a623-7861e15f9bae.png)\n\n每个项目子文件夹就像一个普通的 Qt 项目文件夹，都有自己的项目文件(`.pro`)，但是我们将使用这些文件夹之外的主项目文件将它们组合到一个主项目中。 然后，打开主项目文件并添加以下代码：\n\n```cpp\nTARGET = Multiple_Projects\nTEMPLATE = subdirs\nCONFIG += ordered\n\nSUBDIRS += \\\n    Console \\\n    Widget \\\n    QtQuick \\\n```\n\n正如我们所看到的，它非常简短和简单。 `TARGET`设置只是给这个项目起了一个名字，在本例中，我只是简单地将其命名为`Multiple_Projects`。 我们通常将`TEMPLATE`设置为`app`，现在改为`subdirs`，因为我们只使用此项目文件将其他项目组合在一起，而不是构建实际的程序。 我们将关键字`ordered`添加到`CONFIG`设置中，以便按照给定的顺序处理后面列出的所有目录。 最后，通过将单个项目文件夹添加到`SUBDIRS`设置变量，将其添加到主项目文件中。 保存项目文件后，您将看到项目结构现在已更新。 现在，您将能够看到项目树中的所有子项目：\n\n![](img/5b04cf04-81c0-4c41-97e9-f7a01081861f.png)\n\n这样，您可以更轻松地编辑、生成和调试所有子项目，而无需单独打开每个项目。 现在，您甚至可以将它们放入相同的版本控制存储库中，并动态提交/更新整个项目。 这在与程序员团队协作时特别方便。\n\n您可能会注意到，当您尝试运行所有这些程序时，Qt Creator 可能会在运行第二个程序之前关闭第一个程序，依此类推。 可以通过转到工具|选项...更改此默认行为。 |Build&Run|常规，修改构建前停止应用选项为无：\n\n![](img/4421b2c0-0182-46fe-97ec-b809c534ec9b.png)\n\n这样，您可以同时运行和调试程序，并同时查看 Qt Creator 上所有程序的调试消息，从而使您的调试过程更简单、更快捷：\n\n![](img/9d43d1eb-eb0f-47c3-a5c1-05d526fd055e.png)\n\n最后一个提示是，如果您希望 Qt Creator 同时启动这三个程序，而不是逐个运行它们，您可以转到项目界面，然后通过单击首选工具包下的运行图标打开运行设置页面：\n\n![](img/d075197e-1a77-4ea4-8c3b-c58f92befc76.png)\n\n之后，通过选择 Custom Executable(自定义可执行文件)选项添加新的运行配置：\n\n![](img/db556e90-e0c2-4c17-8939-4175e2973ff6.png)\n\n然后，单击浏览...。 按钮，并将其链接到帮助您同时启动所有三个程序的批处理脚本。 您还需要通过单击 Browse...(浏览...)选择工作目录。 按钮的工作目录设置。 不推荐使用批处理脚本，但如果您真的想这样做，您可以这样做。\n\n我们已经了解了如何将不同的 Qt 项目分组在一起，并同时在这些项目上运行调试器。 接下来，我们将进一步了解 Qt 助手是什么，以及如何从我们的 Qt Creator 访问它。\n\n# 了解有关 Qt 的更多信息\n\n在前几章中，我向您介绍了 Qt Creator 的帮助面板，以及编辑代码时用于自动完成类成员的编辑器工具。 Qt Creator 的帮助视图实际上是**Qt 助手**的子视图，后者是 Qt 的完整文档。 如果您安装了所有的 Qt 安装，则默认情况下应该安装该文件；文档在本地打包为 HTML 文件。 这些文档中的大部分也可以在 Web 上找到，但通过这种方式访问要快得多。\n\n当我们从 Qt SDK 启动 Qt Assistant 时(或者从`assistant`的命令行启动，或者通过在安装的应用列表中找到它)，我们应该会看到类似以下屏幕截图的内容：\n\n![](img/80a1b511-69e5-465f-ae84-3a198981d68b.png)\n\nQt 助手是了解 Qt 的最佳场所。 在左侧的列中，您可以看到一个目录；最好的开始位置是 Qt Core，然后是 Qt GUI 或 Qt Quick，具体取决于您想要编写 GUI 还是 Qt Quick 应用。 右侧的主视图就像一个浏览器窗口，带有指向相关部分的超链接。\n\n此外，在 Qt 助手中，您可以为经常访问的页面添加书签，查看文档中所有术语的索引，并使用左侧列中的搜索选项卡快速搜索术语。 这是一种无价的资源，像电子书一样容易使用。\n\n最后，如果您更喜欢使用 Web 来了解一些事情，请不要忘记 Qt 的大量在线文档，可从[https://doc.qt.io](https://doc.qt.io)获取。 在[https://forum.qt.io](https://forum.qt.io)上还有 Qt 项目论坛。\n\n# 简略的 / 概括的 / 简易判罪的 / 简易的\n\nQt 和 Qt Creator 为应用开发提供了一个很好的环境，无论您是在编写控制台、GUI 还是 Qt Quick 应用。 您可以将标准 C++ 代码与 Qt 混合使用，从而最大限度地利用您现有的技能。 在执行此操作时，您可以将版本控制和命令行构建等功能添加到工具中，从而使您能够在大型团队中工作，并使用 Qt 执行大型项目的无人参与构建。 Qt 也有很棒的文档，既与 Qt Creator 捆绑在一起，也在网络上捆绑在一起。\n\n根据您在本书中学到的知识和可用的内容，您的应用开发目标是无穷无尽的！"
  },
  {
    "path": "docs/app-dev-qt-creator/README.md",
    "content": "# Qt Creator 应用开发\n\n> 原书：[Application Development with Qt Creator](https://libgen.rs/book/index.php?md5=B82379C12D929B1C043C3F08AF11F2EA)\n> \n> 协议：[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)\n> \n> 阶段：机翻（1）\n> \n> 自豪地采用[谷歌翻译](https://translate.google.cn/)\n> \n> 让开源界提前感受入关。\n\n* [在线阅读](https://ccpp.apachecn.org)\n* [在线阅读（Gitee）](https://apachecn.gitee.io/apachecn-c-cpp-zh/)\n* [ApacheCN 学习资源](http://docs.apachecn.org/)\n\n## 贡献指南\n\n本项目需要校对，欢迎大家提交 Pull Request。\n\n> 请您勇敢地去翻译和改进翻译。虽然我们追求卓越，但我们并不要求您做到十全十美，因此请不要担心因为翻译上犯错——在大部分情况下，我们的服务器已经记录所有的翻译，因此您不必担心会因为您的失误遭到无法挽回的破坏。（改编自维基百科）\n\n## 联系方式\n\n### 负责人\n\n* [飞龙](https://github.com/wizardforcel): 562826179\n\n### 其他\n\n*   在我们的 [apachecn/apachecn-c-cpp-zh](https://github.com/apachecn/apachecn-c-cpp-zh) github 上提 issue.\n*   发邮件到 Email: `apachecn@163.com`.\n*   在我们的 [组织学习交流群](http://www.apachecn.org/organization/348.html) 中联系群主/管理员即可.\n\n## 赞助我们\n\n![](http://data.apachecn.org/img/about/donate.jpg)\n"
  },
  {
    "path": "docs/app-dev-qt-creator/SUMMARY.md",
    "content": "+   [Qt Creator 应用开发](README.md)\n+   [零、前言](00.md)\n+   [第一部分：基础知识](sec1.md)\n\t+   [一、Qt Creator 入门](01.md)\n\t+   [二、使用 Qt Creator 构建应用](02.md)\n\t+   [三、使用 Qt Designer 设计应用](03.md)\n\t+   [四、Qt 基础](04.md)\n\t+   [五、使用 Qt 小部件开发应用](05.md)\n+   [第二部分：高级功能](sec2.md)\n\t+   [六、使用 Qt 绘图](06.md)\n\t+   [七、使用 Qt Quick 实现更多功能](07.md)\n\t+   [八、使用 Qt Quick 实现多媒体](08.md)\n\t+   [九、传感器和 Qt Quick](09.md)\n+   [第三部分：实际事项](sec3.md)\n\t+   [十、使用 Qt 语言学家本地化您的应用](10.md)\n\t+   [十一、使用 Qt Creator 优化性能](11.md)\n\t+   [十二、使用 Qt Creator 开发移动应用](12.md)\n\t+   [十三、使用 Qt Creator 开发嵌入式和物联网](13.md)\n\t+   [十四、QT 提示和技巧](14.md)\n"
  },
  {
    "path": "docs/app-dev-qt-creator/sec1.md",
    "content": "# 第一部分：基础知识\n\n在本书的第一部分，我们将探讨 Qt Creator 的基础知识，以及如何使用它构建和设计应用，并深入研究 Qt 的基础。 这些构成了我们未来高级主题的前提条件。\n\n本节包括以下章节：\n\n*   [第 1 章](01.html)，*Qt Creator 入门*\n*   [第 2 章](02.html)，*使用 Qt Creator 构建应用*\n*   [第 3 章](03.html)，*使用 Qt Designer 设计应用*\n*   [第 4 章](04.html)，*Qt 基础*\n*   [第 5 章](05.html)，*使用 Qt 小工具开发应用*"
  },
  {
    "path": "docs/app-dev-qt-creator/sec2.md",
    "content": "# 第二部分：高级功能\n\n在本书的第二部分，我们将探讨一些高级的 Qt 主题，这些主题将帮助我们更好地理解 Qt，除了一些使用 Qt 的工具外，还包括传感器的工作。\n\n本节包括以下章节：\n\n*   [第 6 章](06.html)，*使用 Qt*绘图\n*   [第 7 章](07.html)，*使用 Qt Quick 执行更多操作*\n*   [第 8 章](08.html)，*使用 Qt Quick 实现多媒体*\n*   [第 9 章](09.html)、*传感器和 Qt Quick*"
  },
  {
    "path": "docs/app-dev-qt-creator/sec3.md",
    "content": "# 第三部分：实际事项\n\n在本书的第三部分中，我们将应用全书中提供的信息，并了解 Qt 中应用的开发和实现。 此外，我们还将学习一些 Qt 的技巧和技巧。\n\n本节包括以下章节：\n\n*   [第 10 章](10.html)，*使用 Qt 语言学家本地化您的应用*\n*   [第 11 章](11.html)，*使用 Qt Creator 优化性能*\n*   [第 12 章](12.html)，*使用 Qt Creator 开发移动应用*\n*   [第 13 章](13.html)，*使用 Qt Creator 进行嵌入式和物联网开发*\n*   [第 14 章](14.html)，*Qt 提示和技巧*"
  },
  {
    "path": "docs/begin-cpp-game-prog/00.md",
    "content": "# 零、序言\n\n这本书是关于提供一个有趣的游戏编程世界介绍，C++，和 OpenGL 供电 SFML 使用五个有趣的，完全可玩的游戏，增加难度和先进的特点。这些游戏是一个上瘾的，疯狂的双键敲击游戏，一个乒乓游戏，一个多级僵尸生存射击游戏，一个分屏多人拼图平台，和一个射击游戏。\n\n通过这一改进和扩展的第二版，我们将从编程的基本知识开始，例如变量、循环和条件，当您通过关键的 C++ 主题时，每一个游戏都会变得更加熟练，如 Po.T0.面向对象编程 To1 T1（MultT2OOP ToeT3），C++ 指针，以及对**标准模板库**（**STL**的介绍。在构建这些游戏的同时，您还将学习令人兴奋的游戏编程概念，如粒子效果、定向声音（空间化）、OpenGL 可编程着色器、如何生成数千个对象等。\n\n# 这本书是给谁的\n\n这本书对你来说是完美的，如果你有任何一个 C++ 编程知识，或者你需要一个初级水平的进修课程，如果你想学习游戏或者仅仅用游戏作为一种吸引学习 C++ 的方法，如果你有一天想要发布一个游戏的愿望，也许是在 Steam 上，或者，如果你只是想玩得开心，用你的作品给朋友留下深刻印象。\n\n# 这本书涵盖的内容\n\n【参考译文】第 1 章，T3，T4，C++，SFML，VisualStudio，开始第一个游戏 Ty5T5，代表了相当大的第一章，但我们将学习我们所需要的一切，以使我们的第一个游戏的第一部分运行和运行。以下是我们要做的：了解我们将要建立的游戏，发现 C++，了解微软 Visual C++，探索 SFML 及其与 C++ 的关系，建立开发环境，计划和准备第一个游戏项目，木材！！！在书中写第一个 C++ 代码，制作一个可绘制背景的可运行游戏。\n\n【参考译文】T1，T2，T3，2，T4，5，5，变量，操作符和决策-动画精灵 StutyT7.，在屏幕上覆盖了更多的图形，为了实现这一点，我们需要学习 C++ 的一些基础知识。我们将学习如何使用变量来记忆和操纵值，我们还将开始在游戏中添加更多图形。随着本章的深入，我们将看到如何操纵这些值来设置图形动画。这些值称为变量。\n\n【引用】第 3 章，T2，T3，C + +字符串和 SFML 时间-玩家输入和 HUD Ont5，继续木材！！！游戏我们将用本章的一半时间学习如何操作文本并在屏幕上显示，另一半时间学习计时以及视觉时间条如何通知玩家并在游戏中产生紧迫感。我们将包括以下几点：暂停和重新启动游戏，C++ 字符串，SFML 文本和 SFML 字体类，添加一个 HUD 到木材！！！并为木材添加时间条！！！。\n\n【引用】第 4 章，第 3 章，第 3 章，第四章，循环，数组，开关，枚举，和函数-实现游戏力学，T5，也许比书中的其他章节有更多的 C++ 信息。它充满了将极大地提高我们理解力的基本概念。它还将开始揭示一些我们略过的模糊领域，比如函数和游戏循环。一旦我们探索了一个完整的 C++ 语言必需品列表，我们将使用我们所知道的一切来让主要游戏机制树干移动。到本章结束时，我们将为最后阶段和木材完工做好准备！！！。这就是我们将在本章中探讨的内容：循环、数组、使用开关进行决策、枚举、开始使用函数以及创建和移动树分支。\n\n[*第 5 章*](05.html#_idTextAnchor138)*碰撞、声音和结束条件——使游戏可玩*，构成第一个项目的最后阶段。本章结束时，您将完成第一个完整的游戏。一旦你有了木材！！！启动和运行时，请务必阅读本章的最后一节，因为它将提出使游戏更好的方法。在本章中，我们将介绍以下主题：添加其余精灵、处理玩家输入、设置飞行日志动画、处理死亡、添加音效、添加功能和改进木材！！！。\n\n[*第 6 章*](06.html#_idTextAnchor154)*面向对象编程——启动乒乓球游戏*包含了相当多的理论，但该理论将为我们提供知识，让我们开始使用 OOP 产生强大的效果。此外，我们不会浪费任何时间将该理论用于编写下一个项目，即乒乓球游戏。我们将在幕后了解如何通过编码一个类来创建用作对象的新类型。我们将首先看一个简化的乒乓球场景来学习一些基本的类知识，然后我们将重新开始，并使用我们所学的原理编写一个真实的乒乓球游戏。\n\n[*第 7 章*](07.html#_idTextAnchor175)*动态碰撞检测与物理——完成乒乓球游戏*解释了如何编写我们的第二节课。我们将看到，尽管球显然与球拍有很大不同，但我们将使用完全相同的技术将球的外观和功能封装在`Ball`类中，就像我们对球拍和`Bat`类所做的那样。然后，我们将通过编码一些动态碰撞检测和记分，为乒乓球游戏添加最后的润色。这听起来可能很复杂，但是，正如我们所预期的，SFML 将使事情变得比其他方式简单得多。\n\n[*第 8 章*](08.html#_idTextAnchor183)*，SFML 视图–启动僵尸射击游戏*，解释了该项目如何更多地使用 OOP，并产生强大的效果。我们还将探索 SFML`View`类。这个多才多艺的类将使我们能够轻松地将游戏划分为不同层面，以适应游戏的不同方面。在僵尸射手项目中，我们将有一个用于 HUD 的层和一个用于主游戏的层。这将是必要的，因为当玩家每次清除僵尸时，游戏世界都会扩大，最终，游戏世界将比屏幕更大，需要滚动。使用 View 类将防止 HUD 中的文本与背景一起滚动。在下一个项目中，我们将更进一步，创建一个协作的分屏游戏，SFML 视图类将完成大部分的艰苦工作。这就是我们在本章中要做的：规划僵尸竞技场游戏，编写`Player`类代码，了解 SFML 视图类，构建僵尸竞技场游戏引擎，让玩家类发挥作用。\n\n【引用】第 9 章，第 4 章，第 5 章，第 6 章，C++ 引用，SpRITE 表，以及顶点数组 AUT7，探索 C++ 引用，它允许我们研究变量或对象，而这些变量和对象在范围之外。此外，引用将帮助我们避免在函数之间传递大型对象，这是一个缓慢的过程。这是一个缓慢的过程，因为每次执行此操作时，都必须创建变量或对象的副本。有了这些关于引用的新知识，我们将看一看 SFML`VertexArray`类，它允许我们建立一个大图像，可以使用单个图像文件中的多个部分快速有效地绘制到屏幕上。到本章结束时，我们将拥有一个可伸缩、随机、滚动的背景，使用引用和`VertexArray`对象。\n\n【引用】第 10 章第 2 章：T3、3、4、指针、标准模板库和纹理管理（T5），首先介绍了指针的基本 C++ 主题。指针是保存内存地址的变量。通常，指针将保存另一个变量的内存地址。这听起来有点像一个参考，但我们将看到它们是如何更强大，我们将使用一个指针来处理不断扩大的僵尸群。我们还将了解 STL，它是一个类集合，允许我们快速轻松地实现常见的数据管理技术。一旦我们了解了 STL 的基本知识，我们将能够使用新获得的知识来管理游戏中的所有纹理，因为如果我们有 1000 个僵尸，我们真的不想为每个僵尸都将僵尸图形的副本加载到 GPU 中。我们还将深入研究 OOP 并使用静态函数，这是一个类的函数，可以在没有类实例的情况下调用。同时，我们将看到如何设计一个类以确保只有一个实例可以存在。当我们需要保证代码的不同部分将使用相同的数据时，这非常理想。\n\n[*第 11 章*](11.html#_idTextAnchor249)*碰撞检测、拾音器和子弹*解释了到目前为止我们是如何实现游戏的主要视觉方面的。我们有一个可控的角色在竞技场里到处跑，竞技场上到处都是追逐他的僵尸。问题是它们之间没有互动。僵尸可以在玩家身上奇迹般地穿行而不留下划痕。我们需要检测僵尸和玩家之间的冲突。如果僵尸能够伤害并最终杀死玩家，我们给玩家一些子弹来换取他的枪是公平的。然后我们需要确保子弹能够击中并杀死僵尸。同时，如果我们正在为子弹、僵尸和玩家编写碰撞检测代码，那么也应该为生命和弹药拾取添加一个类。\n\n[*第 12 章*](12.html#_idTextAnchor272)*分层视图和实现 HUD*是我们将看到 SFML 视图真正价值的一章。我们将添加大量 SFML`Text`对象并对其进行操作，就像我们之前在木材项目和 Pong 项目中所做的那样。新的是，我们将使用第二个视图实例绘制 HUD。这样，无论背景、玩家、僵尸和其他游戏对象在做什么，HUD 都将整齐地位于主要游戏动作的顶部。\n\n【参考译文】第 13 章 A2 T2，To.T4，声音效果，文件 I/O，以及完成游戏 Ty5，演示了如何使用 C++ 标准库轻松地操作硬盘上存储的文件，我们还将添加声音效果。当然，我们知道如何添加声音效果，但我们将讨论在代码中调用 play 函数的确切位置。我们还将把一些松散的结束，使游戏完整。在本章中，我们将执行以下操作：使用文件输入和文件输出保存和加载 hi 乐谱，添加声音效果以允许玩家升级，并创建永无止境的多个波。\n\n[*第 14 章*](14.html#_idTextAnchor292)*抽象和代码管理——更好地利用 OOP*，重点是启动 Thomas Was Lone 项目，特别是探索如何构建代码以更好地利用 OOP。以下是本章将涉及的主题的详细信息：介绍了最终项目 Thomas Was Late，包括游戏性功能和项目资产，并详细讨论了与以前的项目相比，我们将如何改进代码结构，代码 Thomas Was Late game engine，并实现分屏功能。\n\n[*第 15 章*](15.html#_idTextAnchor306)*高级 OOP–继承和多态性*通过查看稍微高级一些的继承和多态性概念，进一步扩展了我们对 OOP 的了解。然后，我们将能够使用这一新知识来实现我们游戏中的明星角色，托马斯和鲍勃。这是我们将在本章中介绍的内容：学习如何使用继承来扩展和修改类，使用多态性将类的对象视为多种类型的类，学习抽象类以及如何设计从不实例化的类，构建抽象`PlayableCharacter`类，将继承与`Thomas`和`Bob`类一起使用，并将 Thomas 和 Bob 添加到游戏项目中。\n\n[*第 16 章*](16.html#_idTextAnchor327)*构建可玩关卡和碰撞检测*可能是本项目最令人满意的章节之一。这样做的原因是，到比赛结束时，我们将有一个可玩的游戏。虽然仍有一些功能需要实现（声音、粒子效果、HUD 和着色器效果），但 Bob 和 Thomas 将能够奔跑、跳跃和探索世界。此外，通过在文本文件中创建平台和障碍物，您将能够创建任何大小或复杂度的自己的关卡设计。我们将通过以下主题来实现这一切：探索如何在文本文件中设计关卡，构建一个`LevelManager`类，该类将从文本文件中加载关卡，将其转换为游戏可以使用的数据，并跟踪关卡细节，如繁殖位置、当前关卡和允许的时间限制，更新游戏引擎以使用`LevelManager`，并编写一个多态函数来处理 Bob 和 Thomas 的碰撞检测。\n\n[*第 17 章*](17.html#_idTextAnchor340)*声音空间化和 HUD*添加了所有音效和 HUD。我们在之前的两个项目中都做过，但是这次我们会做一些不同的事情。我们将探讨声音空间化的概念，以及 SFML 如何使这一复杂的概念变得简单易懂。此外，我们将构建一个 HUD 类来封装将信息绘制到屏幕上的代码。我们将按照以下顺序完成任务：什么是空间化，SFML 如何处理空间化，构建`SoundManager`类，部署发射器，使用`SoundManager`类，构建然后使用 HUD 类。\n\n[*第 18 章*](18.html#_idTextAnchor356)*粒子系统和着色器*检查了什么是粒子系统，然后将其编码到我们的游戏中。我们将触及 OpenGL 着色器主题的表面，并了解如何使用另一种语言**OpenGL 着色语言**（**GLSL**）编写代码，该语言可以直接在图形卡上运行，从而产生平滑的图形效果，否则可能无法实现。像往常一样，我们也将使用我们的新技能和知识来加强当前项目。\n\n[*第 19 章*](19.html#_idTextAnchor372)*游戏编程设计模式——启动太空入侵者+*游戏，介绍最终项目。正如你现在所期待的，这个项目将在学习新的 C++ 技术方面迈出重要的一步。接下来的四章将讨论一些主题，包括智能指针、C++ 断言、使用 GAMEPAD 控制器、使用 VisualStudio 调试、将基类指针转换成特定派生类的指针、调试以及首先查看设计模式。作者推测，如果你打算在 C++ 中进行深度、大规模的游戏，那么设计模式将成为你未来几个月和几年学习议程的一个重要部分。为了介绍这个重要的主题，我选择了一个相对简单但有趣的游戏作为例子。让我们进一步了解一下太空入侵者++ 游戏，然后我们可以继续讨论设计模式以及为什么需要它们。在这一重要的章节中，我们将涵盖以下主题：了解太空入侵者++ 以及为什么选择它作为最终项目，了解什么是设计模式以及它们对游戏开发者的重要性，研究太空入侵者++ 项目中的设计模式，这些设计模式将在接下来的四章中使用，开始太空入侵者++ 项目，编写大量的类来充实游戏。\n\n[*第 20 章*](20.html#_idTextAnchor414)*游戏对象和组件*涵盖了我们在上一章开始讨论的与实体组件模式相关的所有编码。这意味着我们将对所有其他组件将从中派生的基本组件类进行编码。我们还将充分利用我们对智能指针的新知识，这样我们就不必关注我们为这些组件分配的内存。我们还将在本章中对`GameObject`类进行编码。以下是本章各节的列表：准备对组件进行编码、对组件基类进行编码、对碰撞器组件进行编码、对图形组件进行编码、对更新组件进行编码以及对`GameObject`类进行编码。\n\n[*第 21 章*](21.html#_idTextAnchor432)*文件 I/O 和游戏对象工厂*解释了`GameObject`如何进入游戏中使用的`m_GameObjects`向量。我们将看到如何在文本文件中描述单个对象和整个级别。我们将编写代码来解释文本，然后将值加载到一个类中，该类将成为游戏对象的蓝图。我们将编写一个名为`LevelManager`的类来监督整个过程，从最初请求加载`InputHandler`通过`ScreenManager`发送的关卡开始，一直到工厂模式类，工厂模式类从组件组装游戏对象，并将其交付给`m_GameObjects`向量中整齐包装的`LevelManager`类。\n\n[*第 22 章*](22.html#_idTextAnchor445)*使用游戏对象并构建游戏*，构成空间入侵者++ 项目的最后阶段。我们将学习如何使用 SFML 从游戏板接收输入来完成所有的艰苦工作，我们还将编写一个类来处理入侵者和`GameScreen`类之间的通信，以及玩家和`GameScreen`类之间的通信。该类将允许玩家和入侵者产生子弹，但完全相同的技术可用于您自己游戏不同部分之间需要的任何类型的通信，因此了解这一点很有用。游戏的最后一部分（和往常一样）将是碰撞检测和游戏本身的逻辑。一旦 Space Invaders++ 启动并运行，我们将学习如何使用 Visual Studio 调试器，当您设计自己的逻辑时，调试器将非常有用，因为它允许您一次一行地遍历代码，并查看变量的值。它也是一个有用的工具，用于研究我们在本项目过程中组装的模式的执行流程。\n\n[*第 23 章*](23.html#_idTextAnchor457)*在您离开之前。。。*结束了我们的旅程。当你第一次打开一本书的大门挡时，它的后页看起来好像离你很远。但我希望不是太难吧？关键是你现在在这里，希望你能很好地了解如何用 C++ 来构建游戏。你可能会惊讶地发现，即使在这几百页之后，我们也只能把脚趾浸到 C++ 中。即使是我们已经讨论过的话题，也可以更深入地讨论，还有很多，一些相当重要的话题，我们甚至没有提到。考虑到这一点，让我们来看看下一步会是什么。\n\n# 充分利用这本书\n\n需要满足以下要求：\n\n*   Windows 7 Service Pack 1、Windows 8 或 Windows 10\n*   1.6 GHz 或更快的处理器\n*   1 GB 内存（用于 x86）或 2 GB 内存（用于 x64）\n*   15 GB 的可用硬盘空间\n*   5400 RPM 硬盘驱动器\n*   支持 DirectX 9 的视频卡，以 1024 x 768 或更高的显示分辨率运行\n\n本书中使用的所有软件都是免费的。本书将逐步介绍如何获取和安装软件。本书始终使用 VisualStudioforWindows，但是有经验的 Linux 和 Mac 用户在使用他们喜欢的编程环境运行代码和遵循说明方面可能不会有问题。\n\n## 下载示例代码文件\n\n您可以从您的账户[www.packt.com](http://www.packt.com)下载本书的示例代码文件。如果您在其他地方购买了本书，您可以访问[www.packtpub.com/support](http://www.packtpub.com/support)并注册，将文件通过电子邮件直接发送给您。\n\n您可以通过以下步骤下载代码文件：\n\n1.  在“T0”登录或注册 http://www.packt.com T1-T1\n2.  选择**支持**选项卡。\n3.  点击**代码下载**。\n4.  在**搜索**框中输入图书名称，并按照屏幕上的说明进行操作。\n\n下载文件后，请确保使用以下最新版本解压或解压缩文件夹：\n\n*   WinRAR/7-Zip for Windows\n*   适用于 Mac 的 Zipeg/iZip/UnRarX\n*   适用于 Linux 的 7-Zip/PeaZip\n\n该书的代码包也托管在 GitHub 上的[https://github.com/PacktPublishing/Beginning-Cpp-Game-Programming-Second-Edition](https://github.com/PacktPublishing/Beginning-Cpp-Game-Programming-Second-Edition) 。如果代码有更新，它将在现有 GitHub 存储库中更新。\n\n我们的丰富书籍和视频目录中还有其他代码包，请访问[https://github.com/PacktPublishing/](https://github.com/PacktPublishing/) 。看看他们！\n\n## 下载彩色图片\n\n我们还提供了一个 PDF 文件，其中包含本书中使用的屏幕截图/图表的彩色图像。您可以在这里下载：[https://static.packt-cdn.com/downloads/9781838648572_ColorImages.pdf](https://static.packt-cdn.com/downloads/9781838648572_ColorImages.pdf) 。\n\n## 使用的约定\n\n本书中使用了许多文本约定。\n\n`CodeInText`：表示文本中的码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄。例如：“我的主要项目目录是`D:\\VS Projects\\Timber`\n\n代码块设置如下：\n\n```cpp\nint main()\n{\n    return 0;\n}\n```\n\n当我们希望提请您注意代码块的特定部分时，相关行或项目以粗体显示：\n\n```cpp\nint main()\n{\n    return 0;\n}\n```\n\n**粗体**：表示一个新术语、一个重要单词或您在屏幕上看到的单词。例如，菜单或对话框中的单词出现在文本中，如下所示。下面是一个例子：“点击**新建项目**按钮。”\n\n重要提示\n\n警告或重要提示如下所示。\n\n提示\n\n提示和技巧如下所示。\n\n# 联系\n\n我们欢迎读者的反馈。\n\n**一般反馈**：如果您对本书的任何方面有疑问，请在邮件主题中注明书名，并发送电子邮件至[customercare@packtpub.com](mailto:customercare@packtpub.com)。\n\n**勘误表**：尽管我们已尽一切努力确保内容的准确性，但还是会出现错误。如果您在本书中发现错误，如果您能向我们报告，我们将不胜感激。请访问[www.packtpub.com/support/errata](http://www.packtpub.com/support/errata)，选择您的书籍，单击 errata 提交表单链接，然后输入详细信息。\n\n**盗版**：如果您在互联网上发现我们作品的任何形式的非法复制品，请您提供我们的位置地址或网站名称，我们将不胜感激。请致电[与我们联系 copyright@packt.com](mailto:copyright@packt.com)带有指向该材料的链接。\n\n**如果您有兴趣成为一名作家**：如果您对某个主题有专业知识，并且您有兴趣撰写或贡献一本书，请访问[authors.packtpub.com](http://authors.packtpub.com)。\n\n## 审查\n\n请留下评论。一旦你阅读并使用了这本书，为什么不在你购买它的网站上留下评论呢？然后，潜在读者可以看到并使用您的无偏见意见做出购买决定，我们 Packt 可以了解您对我们产品的看法，我们的作者可以看到您对他们书籍的反馈。非常感谢。\n\n有关 Packt 的更多信息，请访问[Packt.com](http://packt.com)。"
  },
  {
    "path": "docs/begin-cpp-game-prog/01.md",
    "content": "# 一、C++，SFML，VisualStudio，并开始第一个游戏\n\n欢迎来到《开放 C++ 游戏编程》第 1 卷。我不会浪费任何时间让你开始写你的旅程，用 C++ 和 OpenGL 供电的 SFML 来为 PC 编写精彩的游戏。\n\n这是一个相当沉重的第一章，但我们绝对会学习我们所需要的一切，这样我们就有了第一场比赛的第一部分。以下是我们在本章中要做的事情：\n\n*   了解我们将创建的游戏\n*   满足 C++\n*   了解微软 Visual C++\n*   SFML 及其与 C++ 的关系探讨\n*   建立发展环境\n*   计划和准备第一个游戏项目，木材！！！\n*   写下这本书的第一个 C++ 代码，制作一个可以绘制背景的可运行游戏。\n\n# 我们将打造的游戏\n\n这段旅程将是顺利的，因为我们将一步一步地学习超高速 C++ 语言的基本原理，然后把新的知识用于添加我们将要构建的五个游戏的酷功能。\n\n以下是本书的五个项目。\n\n## 木材！！！\n\n第一个游戏是一个令人上瘾的，快节奏的，巨大成功的木材人的复制品，可以在[找到 http://store.steampowered.com/app/398710/](http://store.steampowered.com/app/398710/) 。我们的游戏，木材！！！，在我们构建一个真正的可玩游戏时，我们将介绍 C++ 的所有基本知识。以下是我们完成游戏后的版本，我们在最后一刻添加了一些增强功能：\n\n![](img/B14278_01_01.jpg)\n\n## 庞\n\nPong 是最早制作的视频游戏之一，你可以在这里了解它的历史：[https://en.wikipedia.org/wiki/Pong](https://en.wikipedia.org/wiki/Pong) 。这是一个很好的例子，说明了游戏对象动画和动态碰撞检测的基础知识是如何工作的。我们将构建这个简单的复古游戏来探索类和面向对象编程的概念。球员将使用屏幕底部的球棒，将球击回屏幕顶部：\n\n![](img/B14278_01_01b.jpg)\n\n## 僵尸竞技场\n\n下一步，我们将打造一款疯狂的僵尸生存射击游戏，与蒸汽打击*9000 多名僵尸没什么两样！*，您可以在[了解更多信息 http://store.steampowered.com/app/273500/](http://store.steampowered.com/app/273500/) 。玩家将拥有一把机关枪，必须击退不断增长的僵尸浪潮。所有这些都将发生在一个随机生成的滚动世界中。为了实现这一点，我们将学习面向对象编程如何使我们拥有易于编写和维护的大型**代码库**（大量代码）。期待激动人心的功能，如数百个敌人、快速射击武器、皮卡车和一个可以在每次浪潮后“升级”的角色：\n\n![](img/B14278_01_02_%281%29.jpg)\n\n## 托马斯迟到了\n\n第四款游戏将是一款时尚且富有挑战性的单人和合作拼图平台。它基于非常流行的游戏*托马斯是孤独的*（[http://store.steampowered.com/app/220780/](http://store.steampowered.com/app/220780/) ）。希望了解一些很酷的主题，如粒子效果、OpenGL 着色器和分屏协作多人游戏：\n\n![](img/B14278_01_03.jpg)\n\n提示\n\n如果您现在想玩任何游戏，您可以从`Runnable Games`文件夹中的下载包中进行。只需双击相应的`.exe`文件。请注意，在此文件夹中，您可以从任何章节运行已完成的游戏或处于部分完成状态的任何游戏。\n\n## 太空入侵者++\n\n最后的游戏将是太空入侵者的克隆。在某些方面，游戏本身并不重要。该项目将用于学习游戏编程模式。随着本书的深入，我们的代码将变得越来越长，越来越复杂。每个项目都会引入一种或多种技术来解决这个问题，但尽管有这些技术，我们的代码的复杂性和长度还是会不断地挑战我们。\n\n太空入侵者项目（称为太空入侵者++）将向我们展示如何从根本上重新组织我们的游戏代码，我们也可以一劳永逸地控制和正确管理我们的代码。这将为您提供规划和构建深度、复杂和创新游戏所需的所有知识，而不会陷入代码混乱。\n\n游戏还将引入屏幕、输入处理程序和实体组件系统等概念。它还将允许我们学习如何让玩家使用 GAMEPAD 而不是键盘，并介绍 C++ 指针的智能指针、转换、断言、断点调试，并教我们从整本书中最重要的一课：如何构建自己独特的游戏：\n\n![](img/B14278_01_36.jpg)\n\n让我们开始介绍 C++，Visual Studio 和 SFML！\n\n# 满足 C++\n\n现在我们知道我们将要构建什么游戏，让我们从 C++、VisualStudio 和 SFML 开始。你可能有一个问题，为什么要使用 C++ 语言？第二步：C++ 很快-非常快。我们编写的代码被直接翻译成机器可执行指令，这一事实使这一点成为现实。这些说明是游戏的关键。可执行游戏包含在一个`.exe`文件中，玩家只需双击即可运行该文件。\n\n在将代码更改为可执行文件的过程中，有几个步骤。首先，**预处理器**查看是否有*其他代码*需要包含在我们自己的代码中并添加它。接下来，所有代码由**编译器**程序**编译**成**目标文件**。最后，第三个程序称为**链接器**，将所有目标文件连接到游戏的可执行文件中。\n\n此外，C++ 是非常好建立的，同时也是最新的。C++ 是一种面向对象编程的编程语言 To1（Apple T2SooOutT3）语言，这意味着我们可以编写和组织我们的代码，使用测试良好的约定，使我们的游戏高效和可管理。当我们阅读本书时，它的好处和必要性将会显现出来。\n\n正如您可能猜到的，我提到的大多数*其他代码*是 SFML，我们将在一分钟内了解更多关于 SFML 的信息。我刚才提到的预处理器、编译器和链接器程序都是 Visual Studio**集成开发环境**（**IDE**的一部分。\n\n# 微软 Visual Studio\n\nVisualStudio 隐藏了预处理、编译和链接的复杂性。只需按下一个按钮，它就能把一切都包起来。除此之外，它还为我们提供了一个灵活的用户界面，可以将代码输入并管理大量的代码文件和其他项目资产。\n\n虽然 Visual Studio 的高级版本花费数百美元，但我们将能够在免费的“**Express 2019 for Community**版本中构建所有五款游戏。这是 Visual Studio 的最新免费版本。\n\n# \\12304；T0]SFML\n\n**SFML**是**简单快速媒体库**。它不是唯一的 C++ 游戏和多媒体库。使用其他库是可能的，但 SFML 似乎每次都能为我所接受。首先，使用面向对象 C++ 编写。面向对象 C++ 的好处是很多的，而且随着你在这本书中的进步，你会体验到它们。\n\nSFML 也很容易入门，因此如果您是初学者，它是一个不错的选择，但同时，如果您是专业人士，它有潜力构建最高质量的 2D 游戏。因此，初学者可以开始使用 SFML，而不必担心随着经验的增长必须重新开始使用新的语言/库。\n\n也许最大的好处是大多数现代 C++ 编程都使用面向对象编程。每个 C++ 初学者的指导我都读过使用和教 OOP。事实上，OOP 是几乎所有语言编码的未来（和现在）。那么，如果你从一开始就学习 C++，你会想用其他方法来做吗？\n\nSFML 有一个模块（代码），用于在 2D 游戏中执行任何操作。SFML 使用 OpenGL 工作，OpenGL 也可以制作 3D 游戏。当您希望在多个平台上运行游戏时，OpenGL 实际上可以免费使用图形库。当您使用 SFML 时，您将自动使用 OpenGL。\n\nSFML 允许您创建以下内容：\n\n*   2D 图形和动画，包括滚动游戏世界。\n*   音效和音乐播放，包括高品质的定向声音。\n*   使用键盘、鼠标和游戏板进行输入处理。\n*   在线多人游戏功能。\n*   同样的代码可以在所有主要的桌面操作系统和手机上编译和链接！。\n\n广泛的研究并没有发现任何更合适的方式来为 PC 甚至为专家开发 2D 游戏，尤其是如果你是初学者，并且想在有趣的游戏环境中学习 C++。\n\n在接下来的部分中，我们将设置开发环境，首先讨论在使用 Mac 或 Linux 操作系统时应该做什么。\n\n# 建立发展环境\n\n现在，您对我们将如何制作游戏有了更多的了解，现在是时候建立一个开发环境，以便我们可以进行编码了。\n\n## Mac 和 Linux 呢？\n\n我们将制作的游戏可以在 Windows、Mac 和 Linux 上运行！对于每个平台，我们使用的代码都是相同的。但是，每个版本都需要在其预期的平台上编译和链接，VisualStudio 将无法帮助我们使用 Mac 和 Linux。\n\n说这本书完全适合 Mac 和 Linux 用户是不公平的，特别是对初学者来说。虽然，我想，如果你是一个热情的 Mac 或 Linux 用户，并且对自己的操作系统感到满意，那么你很可能会成功。您将遇到的大多数额外挑战将出现在开发环境 SFML 的初始设置和第一个项目中。\n\n为此，我可以强烈推荐以下教程，它们将有望取代接下来的 10 页（大约），直到*计划书！！！*节，本书何时与所有操作系统相关。\n\n对于 Linux，请阅读本文以替换下面的几个部分：[https://www.sfml-dev.org/tutorials/2.5/start-linux.php](https://www.sfml-dev.org/tutorials/2.5/start-linux.php) 。\n\n在 Mac 上，阅读本教程开始：[https://www.sfml-dev.org/tutorials/2.5/start-osx.php](https://www.sfml-dev.org/tutorials/2.5/start-osx.php) 。\n\n## 安装 Visual Studio 2019 社区版\n\n要开始创建游戏，我们需要安装 VisualStudio2019。安装 VisualStudio 几乎可以像下载文件并单击几个按钮一样简单。我将一步一步地指导您完成安装过程。\n\n重要提示\n\n请注意，多年来，Microsoft 可能会更改用于获取 Visual Studio 的名称、外观和下载页面。他们可能会更改用户界面的布局，并使随后的说明过时。然而，我们为每个项目配置的设置是 C++ 和 SFML 的基础，因此即使在微软对 VisualStudio 进行一些激进的操作时，对本章后面的说明也可能进行仔细的解释。无论如何，在撰写本文时，VisualStudio2019 已经发布了两周，所以希望这一章在一段时间内是最新的。如果确实发生了一些重要的事情，那么我将在[上添加一个最新的教程 http://gamecodeschool.com](http://gamecodeschool.com) 我一发现这件事。\n\n让我们开始安装 Visual Studio：\n\n1.  您首先需要的是 Microsoft 帐户和登录详细信息。如果你有 Hotmail 或 MSN 电子邮件地址，那么你已经有了。如果没有，您可以在这里注册一个免费的：[https://login.live.com/](https://login.live.com/) 。\n2.  The next step is to visit [https://visualstudio.microsoft.com/vs/](https://visualstudio.microsoft.com/vs/) and find the download link for **Community 2019**. This is what it looks like at the time of writing:\n\n    ![](img/B14278_01_04.jpg)\n\n3.  将文件保存到您的计算机。\n4.  下载完成后，双击以运行下载。在撰写本文时，我的文件名为`vs_community__33910147.1551368984.exe`。根据当前版本的 Visual Studio，您的将有所不同。\n5.  After giving permission for Visual Studio to make changes to your computer, you will be greeted with the following window. Click **Continue**:\n\n    ![](img/B14278_01_05.jpg)\n\n6.  Wait for the installer program to download some files and set up the next stage of the installation. Shortly, you will be presented with the following window:\n\n    ![](img/B14278_01_05a.jpg)\n\n7.  如果要选择新位置来安装 Visual Studio，请找到**更改**选项并配置安装位置。最简单的方法是将文件保留在 VisualStudio 选择的默认位置。当您准备好时，用 C++ OutT3AY 选项定位 OLE T2 桌面开发并选择它。\n8.  接下来，点击**安装**按钮。抓取一些点心，因为这一步可能需要一些时间。\n9.  当这个过程完成时，您可以关闭所有打开的窗口，包括任何提示您启动新项目的窗口，因为在安装 SFML 之前，我们还没有准备好开始编码。\n\n现在，我们准备将注意力转向 SFML。\n\n## 设置 SFML\n\n本简短教程将指导您下载 SFML 文件，这些文件允许我们在项目中包含库中包含的功能。此外，我们将了解如何使用 SFML**DLL**文件，使我们编译的目标代码能够与 SFML 一起运行。要设置 SFML，请执行以下步骤：\n\n1.  Visit this link on the SFML website: [http://www.sfml-dev.org/download.php](http://www.sfml-dev.org/download.php). Click on the button that says **Latest stable version**, as shown here:\n\n    ![](img/B14278_01_06.jpg)\n\n2.  By the time you read this book, the latest version will almost certainly have changed. This won’t matter as long as you do the next step just right. We want to download the **32-bit version** of **Visual C++ 2017**. This might sound counter-intuitive because we have just installed Visual Studio 2019 and you probably (most commonly) have a 64-bit PC. The reason we chose to download the 32-bit version is that Visual C++ 2017 is part of Visual Studio 2019 (Visual Studio does more than C++) and we will be building games in 32-bit so that they can run on *both* 32- and 64-bit machines. Click the **Download** button that’s shown in the following screenshot:\n\n    ![](img/B14278_01_07.jpg)\n\n3.  下载完成后，在安装 Visual Studio 的同一驱动器的根目录下创建一个文件夹，并将其命名为`SFML`。另外，在安装 Visual Studio 的驱动器的根目录下创建另一个文件夹，并将其命名为`VS Projects`。\n4.  最后，解压缩 SFML 下载。在桌面上执行此操作。解压缩完成后，您可以删除.zip 文件夹。您的桌面上将只剩下一个文件夹。其名称将反映您下载的 SFML 版本。我的名字叫`SFML-2.5.1-windows-vc15-32-bit`。您的文件名可能会反映较新的版本。双击此文件夹查看其内容，然后再次双击进入下一个文件夹（我的文件夹名为`SFML-2.5.1`。下面的屏幕截图显示了我的`SFML-2.5.1` 文件夹的内容。你的应该看起来一样：\n5.  复制此文件夹的全部内容，并将所有文件和文件夹粘贴到您在*步骤 3*中创建的`SFML`文件夹中。在本书的其余部分，我将把这个文件夹简单地称为“您的`SFML`文件夹”。\n\n现在，我们已经准备好在 VisualStudio 中使用 C++ 和 SFML。\n\n## 创建新项目\n\n由于建立项目是一个复杂的过程，我们将一步一步地进行，以便我们能够开始习惯它：\n\n1.  Start Visual Studio in the same way you start any app: by clicking on its icon. The default installation options will have placed a **Visual Studio 2019** icon in the Windows start menu. You will see the following window:\n\n    ![](img/B14278_01_10.jpg)\n\n2.  Click on the **Create a new project** button, as highlighted in the preceding screenshot. You will see the **Create a new project** window, as shown in the following screenshot:\n\n    ![](img/B14278_01_11.jpg)\n\n3.  In the **Create a new project** window, we need to choose the type of project we will be creating. We will be creating a console app, so select **Console App**, as highlighted in the preceding screenshot, and click the **Next** button. You will then see the **Configure your new project** window. This following screenshot shows the **Configure your new project** window after the next three steps have been completed:\n\n    ![](img/B14278_01_12.jpg)\n\n4.  在**配置新项目**窗口中，在**项目****名称**字段中键入`Timber`。请注意，这会导致 Visual Studio 自动将**解决方案名称**字段配置为相同的名称。\n5.  在**位置**字段中，浏览到我们在上一教程中创建的`VS Projects`文件夹。这将是我们所有项目文件的保存位置。\n6.  选中选项**将解决方案和项目放在同一目录**中。\n7.  Note that the preceding screenshot shows what the window looks like when the previous three steps have been completed. When you have completed these steps, click **Create**. The project will be generated, including some C++ code. This following screenshot shows where we will be working throughout this book:\n\n    ![](img/B14278_01_13.jpg)\n\n8.  现在，我们将配置该项目以使用我们放在`SFML`文件夹中的 SFML 文件。从主菜单中选择**项目****木材属性…**。您将看到以下窗口：\n\n![](img/B14278_01_14.jpg)\n\n提示\n\n在前面的屏幕截图中，**确定**、**取消**和**应用**按钮未完全形成。这可能是 Visual Studio 没有正确处理我的屏幕分辨率的问题。你的身体有望完全成形。无论你的按钮是否像我的一样，继续学习教程都是一样的。\n\n接下来，我们将开始配置项目属性。由于这些步骤相当复杂，我将在新的步骤列表中介绍它们。\n\n## 配置项目属性\n\n在此阶段，您应该打开**木材属性页**窗口，如前一节末尾的屏幕截图所示。现在，我们将开始配置一些属性，同时使用以下带注释的屏幕截图作为指导：\n\n![](img/B14278_01_15.jpg)\n\n我们将在本节中添加一些相当复杂和重要的项目设置。这是一个艰苦的部分，但我们只需要做一次，每个项目。我们需要做的是告诉 VisualStudio 在哪里可以从 SFML 找到特殊类型的代码文件。我所指的特殊类型的文件是一个**头文件**。头文件是定义 SFML 代码格式的文件，因此当我们使用 SFML 代码时，编译器知道如何处理它。请注意，头文件与主要源代码文件不同，它们包含在具有`.hpp`文件扩展名的文件中。当我们最终开始在第二个项目中添加我们自己的头文件时，所有这些都会变得更加清晰。此外，我们需要告诉 VisualStudio 在哪里可以找到 SFML 库文件。在**木材属性页**窗口中，执行以下三个步骤，在前面的屏幕截图中编号：\n\n1.  首先（**1**，从**配置：**下拉列表中选择**所有配置**。\n2.  第二步（**2**，从左侧菜单中选择**C/C++**，然后选择**General**。\n3.  第三个（**3**，找到**附加包含目录**编辑框，键入 SFML 文件夹所在的驱动器号，然后键入`\\SFML\\include`。如果您在 D 驱动器上找到了`SFML`文件夹，则键入的完整路径如前一屏幕截图所示；也就是说，`D:\\SFML\\include`。如果在不同的驱动器上安装了 SFML，请更改路径。\n4.  点击**应用**保存您目前的配置。\n5.  现在，仍然在同一窗口中，执行以下步骤，这些步骤涉及以下带注释的屏幕截图。首先（**1**，选择**连接器**，然后选择**普通**。\n6.  Now, find the **Additional Library Directories** edit box (**2**) and type the drive letter where your `SFML` folder is, followed by `\\SFML\\lib`. So, the full path to type if you located your `SFML` folder on your D drive is, as shown in the following screenshot, `D:\\SFML\\lib`. Vary your path if you installed SFML to a different drive:\n\n    ![](img/B14278_01_16.jpg)\n\n7.  点击**应用**保存您目前的配置。\n8.  最后，对于这个阶段，仍然在同一个窗口中，执行以下步骤，这些步骤参考以下带注释的屏幕截图。切换**配置：**下拉菜单（**1**到**调试**，因为我们将在调试模式下运行和测试我们的游戏。\n9.  选择**连接器**，然后**输入**（**2**。\n10.  找到**附加依赖项**编辑框（**3**），点击最左侧。现在，复制并粘贴/键入以下内容：`sfml-graphics-d.lib;sfml-window-d.lib;sfml-system-d.lib;sfml-network-d.lib;sfml-audio-d.lib;`在指定位置。要格外小心，将光标准确地放置在正确的位置，不要覆盖已经存在的任何文本。\n11.  Click **OK**:\n\n    ![](img/B14278_01_16a.jpg)\n\n12.  点击**应用**，然后点击**确定**。\n\n呸；就这样！我们已经成功配置了 Visual Studio，可以继续规划木材！！！项目\n\n# 规划木材！！！\n\n每当你做游戏时，最好先用铅笔和纸开始。如果你不知道你的游戏在屏幕上是如何工作的，你怎么可能让它在代码中工作呢？\n\n提示\n\n在这一点上，如果你还没有，我建议你去看一段 Timberman 在行动的视频，这样你就可以看到我们的目标。如果你觉得你的预算可以应付，那就拿一份拷贝来试一试。它通常在 Steam 上以低于 1 美元的价格出售：[http://store.steampowered.com/app/398710/](http://store.steampowered.com/app/398710/) 。\n\n定义游戏性的游戏特征和对象称为**机制**。游戏的基本机制如下：\n\n*   时间总是不多了。\n*   你可以通过砍树得到更多的时间。\n*   砍树会导致树枝掉落。\n*   球员必须避免树枝掉落。\n*   重复此操作，直到时间用完或球员被压扁。\n\n希望你在这个阶段规划 C++ 代码显然有点傻。这当然是 C++ 初学者入门的第一章。但是，我们可以看看我们使用的所有资产，以及我们需要做什么来做我们的 C++ 代码。\n\n看看这个游戏的注释截图：\n\n![](img/B14278_01_17.jpg)\n\n您可以看到，我们有以下功能：\n\n*   **玩家得分：**玩家每次砍一根圆木，得一分。他们可以用向左或向右箭头（光标）键来砍木头。\n*   **玩家角色：**玩家每次砍树时，他们将移动到/停留在树的同一侧，相对于他们使用的光标键。因此，球员必须小心选择在哪一边砍。\n*   当玩家切碎时，玩家角色手中会出现一个简单的斧头图形。\n*   **收缩时间条：**每次玩家切碎时，会在不断收缩的时间条上增加少量时间。\n*   **致命的树枝：**玩家砍得越快，他们得到的时间就越多，但树枝向下移动的速度也越快，因此他们被压扁的可能性就越大。树枝在树顶随机繁殖，每砍一次就向下移动。\n*   当玩家被压扁时——他们会经常被压扁——墓碑图形就会出现。\n*   **切碎的原木：**当玩家切碎时，一个切碎的原木图形会呼啸而过，远离玩家。\n*   **只是为了装饰：**这里有三朵浮云，它们会以随机的高度和速度漂移，还有一只蜜蜂，它什么也不做，只是四处飞。\n*   **背景：**所有这些都发生在一个美丽的背景上。\n\n因此，简而言之，玩家必须疯狂地砍杀以获得分数，避免时间耗尽。有点反常，但有趣的结果是，他们砍得越快，就越有可能在泥泞中死去。\n\n我们现在知道了游戏是什么样子，如何玩，以及游戏机制背后的动机。现在，我们可以开始建造了。遵循以下步骤：\n\n1.  Now, we need to copy the SFML `.dll` files into the main project directory. My main project directory is `D:\\VS Projects\\Timber`. It was created by Visual Studio in the previous tutorial. If you put your `VS Projects` folder somewhere else, then perform this step there instead. The files we need to copy into the project folder are located in your `SFML\\bin` folder. Open a window for each of the two locations and highlight all the files in the `SFML\\bin` folder, as shown in the following screenshot:\n\n    ![](img/B14278_01_19.jpg)\n\n2.  现在，将突出显示的文件复制粘贴到项目文件夹中，即`D:\\VS Projects\\Timber`。\n\n该项目现已建立并准备就绪。您将能够看到以下屏幕。我对这个屏幕截图进行了注释，这样您就可以开始熟悉 VisualStudio 了。我们将很快重新审视所有这些领域和其他领域：\n\n![](img/B14278_01_20.jpg)\n\n您的布局可能与前面的屏幕截图略有不同，因为与大多数应用一样，VisualStudio 的窗口是可自定义的。花点时间找到右侧的**解决方案资源管理器**窗口，并对其进行调整，使其内容清晰明了，就像上一个屏幕截图一样。\n\n我们将很快回到这里开始编码。但首先，我们将探索我们将使用的项目资产。\n\n# 项目资产\n\n资产是你制作游戏所需要的任何东西。在我们的案例中，这些资产包括以下内容：\n\n*   在屏幕上书写的字体\n*   不同动作的音效，如斩、死和时间不足\n*   角色、背景、分支和其他游戏对象的图形\n\n本游戏所需的所有图形和声音都包含在本书的下载包中。它们可以在`Chapter 1/graphics`和`Chapter 1/sound`文件夹中找到。\n\n尚未提供所需的字体。这是因为我想避免关于许可证的任何可能的歧义。不过，这不会引起任何问题，因为我将向您展示为您自己选择和下载字体的确切位置和方式。\n\n虽然我将提供资产本身或从何处获取资产的信息，但您可能希望自己创建或获取这些资产。\n\n## 资产外包\n\n有许多网站允许你与艺术家、音响工程师甚至程序员签约。其中最大的一个是 Upwork（[www.Upwork.com](http://www.upwork.com)）。你可以免费加入这个网站并发布你的工作。你需要对你的要求写一个清晰的解释，并说明你准备支付多少。然后，你可能会得到一个很好的选择承包商投标做这项工作。然而，请注意，有许多不合格的承包商的工作可能令人失望，但如果你仔细选择，你可能会找到一位称职、热情、有价值的人或公司来做这项工作。\n\n## 制作自己的声音特效\n\n音效可以从 Freesound（[www.Freesound.org](http://www.freesound.org)等网站免费下载，但如果您正在销售游戏，许可证通常不允许您使用音效。另一种选择是使用来自[www.BFXR.net](http://www.bfxr.net)的名为 BFXR 的开源软件，它可以帮助您生成许多不同的音效，您可以根据自己的喜好保留和处理这些音效。\n\n## 将资产添加到项目中\n\n一旦决定了要使用哪些资产，就应该将它们添加到项目中。以下说明假设您正在使用本书下载包中提供的所有资产。当您使用自己的文件时，只需使用完全相同的文件名将相应的声音或图形文件替换为您自己的文件：\n\n1.  浏览到项目文件夹，即`D:\\VS Projects\\Timber`。\n2.  在此文件夹中创建三个新文件夹，并将其命名为`graphics`、`sound`和`fonts`。\n3.  从下载包中，将`Chapter 1/graphics` 的全部内容复制到`D:\\VS Projects\\Timber\\graphics`文件夹中。\n4.  从下载包中，将`Chapter 1/sound`的全部内容复制到`D:\\VS Projects\\Timber\\sound`文件夹中。\n5.  现在，请访问[http://www.1001freefonts.com/komika_poster.font 在您的网络浏览器中点击](http://www.1001freefonts.com/komika_poster.font)并下载**Komika 海报**字体。\n6.  提取压缩下载的内容并将`KOMIKAP_.ttf`文件添加到`D:\\VS Projects\\Timber\\fonts`文件夹中。\n\n让我们来看看这些资产，特别是图形，以便我们可以看到当我们在 C++ 代码中使用的时候发生了什么。\n\n## 探索资产\n\n图形资源构成了场景的一部分，即我们的木材！！！游戏如果你看一看图形资产，就应该清楚在我们的游戏中将在哪里使用它们：\n\n![](img/B14278_01_21.jpg)\n\n声音文件都是`.wav`格式。这些文件包含我们将在游戏中的某些事件中播放的声音效果。它们都是使用 BFXR 生成的，如下所示：\n\n*   `chop.wav`：有点像斧头（复古斧头）砍树的声音\n*   `death.wav`：有点像复古“失落”的声音\n*   `out_of_time.wav`：当玩家因时间不足而输球时发出的声音，而不是被压扁的声音\n\n我们已经看到了所有的资产，包括图形，所以现在我们将有一个关于屏幕分辨率的简短讨论，以及我们如何在屏幕上定位图形。\n\n# 了解屏幕和内部坐标\n\n在我们转到实际 C++ 代码之前，让我们来谈谈坐标。我们在显示器上看到的所有图像都是由像素构成的。像素是微小的光点，它们结合在一起形成我们看到的图像。\n\n有许多不同的监视器分辨率，但作为一个例子，考虑到一个相当典型的游戏监视器可能有 1920 个像素水平和 1080 个像素垂直。\n\n像素从屏幕左上角开始编号。如下图所示，我们的 1920 x 1080 示例在水平（x）轴上编号为 0 到 1919，在垂直（y）轴上编号为 0 到 1079：\n\n![](img/B14278_01_31.jpg)\n\n因此，可以通过 x 和 y 坐标确定特定和准确的屏幕位置。我们通过将游戏对象（如背景、角色、项目符号和文本）绘制到屏幕上的特定位置来创建游戏。这些位置由像素的坐标标识。看看下面这个假设的例子，我们可以在屏幕的大致中心坐标处进行绘制。在 1920 x 1080 屏幕的情况下，这将位于 960540 位置：\n\n![](img/B14278_01_32.jpg)\n\n除了屏幕坐标外，我们的游戏对象也将拥有各自相似的坐标系。与屏幕坐标系一样，它们的**内部**或**本地**坐标从左上角的 0,0 开始。\n\n在前面的图像中，我们可以看到在屏幕的 960540 处绘制了字符的 0,0。\n\n一个可视的 2D 游戏对象，比如一个角色或者一个僵尸，被称为**精灵**。精灵通常由图像文件制作。所有精灵都有所谓的**起源**。\n\n如果我们将精灵绘制到屏幕上的特定位置，则原点将位于该特定位置。精灵的 0,0 坐标是其原点。下图演示了这一点：\n\n![](img/B14278_01_33.jpg)\n\n因此，在显示绘制到屏幕上的字符的图像中，尽管我们在中心位置（960540）绘制了图像，但它看起来有点向右和向下。\n\n了解这一点很重要，因为它将帮助我们理解绘制所有图形时使用的坐标。\n\n重要提示\n\n请注意，在现实世界中，游戏玩家有各种各样的屏幕分辨率，我们的游戏需要使用尽可能多的分辨率。在第三个项目中，我们将看到如何使我们的游戏动态地适应几乎任何分辨率。在第一个项目中，我们需要假设屏幕分辨率为 1920 x 1080。如果您的屏幕分辨率更高，这将是好的。不要担心，如果你的屏幕低于这一点，因为我已经提供了一套单独的代码为每一章的木材！！！游戏除了在开头添加和交换几行代码之外，代码文件几乎完全相同。如果您的屏幕分辨率较低，那么只需按照本书中的代码进行操作，假设您的分辨率为 1920 x 1080。在试用游戏时，您可以根据需要复制并粘贴前五章`low res`文件夹中的代码文件。事实上，一旦从第一章中添加了额外的行，所有其余的代码都将是相同的，无论您的屏幕分辨率如何。为了方便起见，我为每一章提供了低分辨率代码。第三个项目将讨论这几行代码如何发挥其魔力（缩放屏幕）。可供选择的代码将在低至 960 x 540 的分辨率下工作，因此在几乎任何 PC 或笔记本电脑上都可以。\n\n现在，我们可以编写第一段 C++ 代码，并在动作中看到它。\n\n# 开始编写游戏代码\n\n如果 Visual Studio 尚未打开，请打开它。打开木材！！！通过在 Visual Studio 主窗口上的**最近**列表中左键单击项目。\n\n找到右侧的**解决方案资源管理器**窗口。在**源文件**文件夹下找到`Timber.cpp`文件。\n\n重要提示\n\n.CPP 用于 C Plus。\n\n删除“代码”窗口的全部内容并添加以下代码，以便您自己拥有相同的代码。你可以用与任何文本编辑器或文字处理器相同的方式来实现这一点；如果您愿意，您甚至可以复制并粘贴它。在您进行编辑后，我们可以讨论：\n\n```cpp\n// This is where our game starts from\nint main()\n{\n    return 0;\n}\n```\n\n这个简单的 C++ 程序是个好地方。让我们一行一行地看一遍。\n\n## 通过注释使代码更清晰\n\n第一行代码如下：\n\n```cpp\n// This is where our game starts from\n```\n\n任何以两个正斜杠（`//`开头的代码行都是注释，编译器会忽略它。因此，这行代码不起任何作用。它用于在以后返回代码时留下任何有用的信息。注释在行尾结束，因此下一行的任何内容都不是注释的一部分。还有另一种类型的注释称为**多行**或**c 风格**注释，可用于留下占用多行的注释。我们将在本章后面看到其中一些。在本书中，我将留下数百条注释，以帮助添加上下文并进一步解释代码。\n\n## 主要功能\n\n我们在代码中看到的下一行如下：\n\n```cpp\nint main()\n```\n\n**int**是一种被称为**型**的物质。C++ 有多种类型，它们代表不同类型的数据。`int`是**整数**或整数。等一下，我们马上就回来。\n\n`main()`部分是后面代码段的名称。代码部分在开始的花括号（`{`和下一个结束的花括号（`}`之间标出。\n\n因此，这些花括号`{...}`之间的所有内容都是`main`的一部分。我们将这样的代码段称为**函数**。\n\n每一个 C++ 程序都有一个函数，它是整个程序开始执行的一个地方。随着我们阅读本书，最终，我们的游戏将有许多代码文件。然而，只有一个`main`函数，无论我们编写什么代码，我们的游戏都将从`main`函数开头的花括号内的第一行代码开始执行。\n\n现在，不要担心函数名`()`后面的奇怪括号。我们将在[*第 4 章*](04.html#_idTextAnchor110)*【循环、数组、开关、枚举和函数——实现游戏机制*中进一步讨论它们，届时我们将以全新的、更有趣的角度看待函数。\n\n让我们仔细看看`main`函数中的一行代码。\n\n## 呈现和语法\n\n再看看我们的`main`功能的整体：\n\n```cpp\nint main()\n{\n    return 0;\n}\n```\n\n我们可以看到，在`Main`中，只有一行代码`return 0;`。在我们继续了解这行代码的作用之前，让我们先看看它是如何呈现的。这很有用，因为它可以帮助我们准备编写易于阅读且与代码的其他部分不同的代码。\n\n首先，请注意， `return 0;`向右缩进一个制表符。这清楚地表明它是`main`函数的内部。随着代码长度的增长，我们将看到缩进代码并留下空白对于保持可读性至关重要。\n\n接下来，注意这行末尾的标点符号。分号（`;`）告诉编译器它是指令的结尾，后面的任何内容都是新指令。我们将以分号结尾的指令称为 a`statement`。\n\n请注意，编译器并不关心是否在分号和下一个语句之间留下新行或空格。但是，如果没有为每个语句开始一行，则会导致代码极难读取，并且完全缺少分号将导致**语法错误**，游戏将无法编译或运行。\n\n一段代码加在一起，通常由其与该段其余部分的缩进表示，称为**块**。\n\n现在您已经熟悉了`main`函数的概念，缩进代码以保持整洁，并在每个语句的末尾加上分号，我们可以继续了解`return 0;`语句的实际功能。\n\n## 从函数返回值\n\n实际上，`return 0;`在我们的游戏环境中几乎什么都不做。然而，这一概念很重要。当我们使用`return`关键字时，无论是单独使用还是后跟一个值，它都是一条指令，用于程序执行跳转/移回最初启动函数的代码。\n\n通常，启动函数的代码将是代码中其他地方的另一个函数。然而，在这种情况下，是操作系统启动了`main`功能。因此，当执行`return 0;`时，`main`函数退出，整个程序结束。\n\n由于在`return`关键字后面有一个`0`，该值也会发送到操作系统。我们可以将 0 的值更改为其他值，该值将被发送回。\n\n我们说启动函数**的代码调用**函数，函数**返回**值。\n\n您还不需要完全掌握所有这些函数信息。在这里介绍它很有用。在我们继续之前，我将介绍函数的最后一件事。还记得`int main()`中的`int`吗？这告诉编译器从`main`返回的值的类型必须是`int`（整数/整数）。我们可以返回任何符合`int`条件的值；可能是 0、1999、6358 等等。如果我们尝试返回的不是`int,`或者 12.76，那么代码将无法编译，游戏也无法运行。\n\n函数可以返回大量不同类型的选择，包括我们为自己发明的类型！但是，必须以我们刚才看到的方式让编译器知道该类型。\n\n这一点关于函数的背景信息将使事情在我们前进的过程中变得更加顺利。\n\n## 运行游戏\n\n你甚至可以在这一点上运行游戏。通过单击 Visual Studio 快速启动栏中的**本地 Windows 调试器**按钮来执行此操作。或者，您可以使用*F5*快捷键：\n\n![](img/B14278_01_34.jpg)\n\n你只会得到一个黑屏。如果黑屏不能自动关闭，您可以点击任意键将其关闭。这个窗口是 C++ 控制台，我们可以用它调试我们的游戏。我们现在不需要这样做。我们的程序正在启动，从`main`的第一行即`return 0;`开始执行，然后立即退出回到操作系统。\n\n我们现在有了可能编码和运行的最简单的程序。我们现在将添加一些代码来打开一个窗口，游戏最终将出现在其中。\n\n# 使用 SFML 打开窗口\n\n现在，让我们再添加一些代码。下面的代码将使用 SFML 打开一个窗口！！！最终会发生冲突。窗口宽 1920 像素，高 1080 像素，全屏显示（无边框或标题）。\n\n将此处突出显示的新代码输入到现有代码中，然后我们将对其进行检查。键入（或复制粘贴）时，请尝试了解发生了什么：\n\n```cpp\n// Include important libraries here\n#include <SFML/Graphics.hpp>\n// Make code easier to type with “using namespace”\nusing namespace sf;\n// This is where our game starts from\nint main()\n{\n // Create a video mode object\n VideoMode vm(1920, 1080);\n // Create and open a window for the game\n RenderWindow window(vm, “Timber!!!”, Style::Fullscreen);\nreturn 0;\n}\n```\n\n## #包括 SFML 功能\n\n我们将在新代码中注意到的第一件事是`#include`指令。\n\n`#include`**指令**告诉 Visual Studio 在编译之前*包括*或添加另一个文件的内容。这样做的结果是，当我们运行程序时，一些我们自己没有编写的其他代码将成为程序的一部分。将其他文件中的代码添加到我们的代码中的过程称为**预处理**，并且可能毫不奇怪地由一个称为**预处理器**的程序执行。`.hpp`文件扩展名表示它是**头**文件。\n\n因此，`#include <SFML/Graphics.hpp>`告诉预处理器包含名为`SFML.`的文件夹中包含的`Graphics.hpp`文件的内容。它与我们在设置项目时创建的文件夹相同。\n\n这一行添加了上述文件中的代码，这使我们能够访问 SFML 的一些特性。当我们开始编写自己的独立代码文件并使用`#include`使用它们时，它是如何实现这一点的将变得更加清晰。\n\n我们将在本书中包括的主要文件是 SFML 头文件，它使我们能够访问所有酷的游戏编码特性。我们还将使用 AutoT0TAL 访问 OLE T1E.C++ 标准库 ALE T2A.头文件。这些头文件使我们能够访问 C++ 语言本身的核心特性。\n\n现在重要的是，如果我们添加这一行代码，就可以使用 SFML 提供的一整套新功能。\n\n下一条新线是`using namespace sf;`。我们将很快回到这条线的功能。\n\n## 面向对象、类和对象\n\n在阅读本书的过程中，我们将全面讨论 OOP、类和对象。下面是一个简短的介绍，以便我们了解正在发生的事情。\n\n我们已经知道 OOP 代表面向对象编程。OOP 是一种编程范式，即一种*编码方式*。OOP 在编程界几乎每种语言中都被公认为是编写代码的最好、甚至是唯一的专业方法。\n\nOOP 引入了很多编码概念，但它们的基础都是**类**和**对象**。在编写代码时，只要可能，我们都希望编写可重用、可维护和安全的代码。我们这样做的方式是将代码结构化为一个类。我们将在[*第 6 章*](06.html#_idTextAnchor154)*【面向对象编程——开始乒乓球游戏*中学习如何做到这一点。\n\n我们现在需要知道的关于类的一切是，一旦我们编码了类，我们就不只是在游戏中执行代码；相反，我们从类的创建可用对象*。*\n\n例如，如果我们想要 100 个僵尸**NPC**s（**非玩家角色**，我们可以仔细设计并编写一个名为`Zombie`的类，然后从这个类中创建我们喜欢的任意多个僵尸对象。每个僵尸对象都具有相同的功能和内部数据类型，但每个僵尸对象都是一个独立的实体。\n\n为了进一步说明假设的僵尸示例，但不显示`Zombie`类的任何代码，我们可以基于`Zombie`类创建一个新对象，如下所示：\n\n```cpp\nZombie z1;\n```\n\n`z1`对象现在是一个完全编码并运行的`Zombie`对象。我们可以这样做：\n\n```cpp\nZombie z2;\nZombie z3;\nZombie z4;\nZombie z5;\n```\n\n我们现在有五个独立的`Zombie`**实例**，但它们都基于一个精心编码的类。在我们回到刚才编写的代码之前，让我们再进一步。我们的僵尸可以包含行为（由函数定义）和数据，这些数据可能表示僵尸的健康状况、速度、位置或旅行方向。例如，我们可以编写我们的`Zombie`类，使我们能够使用`Zombie`对象，可能如下所示：\n\n```cpp\nz1.attack(player);\nz2.growl();\nz3.headExplode();\n```\n\n重要提示\n\n请再次注意，目前所有这些僵尸代码都是假设的。不要在 Visual Studio 中键入此代码–它只会产生一系列错误。\n\n我们将设计我们的类，以便我们能够以最合适的方式使用数据和行为，以符合我们游戏的目标。例如，我们可以设计类，以便在创建每个僵尸对象时为其分配数据值。\n\n假设我们需要在创建每个僵尸时指定一个唯一的名称和速度（以米/秒为单位）。仔细编写`Zombie`类可以让我们编写如下代码：\n\n```cpp\n// Dave was a 100 metre Olympic champion before infection \n// He moves at 10 metres per second\nZombie z1(“Dave”, 10);\n// Gill had both of her legs eaten before she was infected\n// She drags along at .01 metres per second\nZombie z2(“Gill”, .01);\n```\n\n关键是类几乎是无限灵活的，一旦我们对类进行了编码，我们就可以通过创建它们的对象/实例来使用它们。正是通过类和从中创建的对象，我们才能利用 SFML 的强大功能。是的，我们还将编写自己的类，包括一个`Zombie`类。\n\n让我们回到刚才编写的真实代码。\n\n## 使用名称空间 sf\n\n在我们继续深入研究`VideoMode`和`RenderWindow`之前，我们将了解`using namespace sf;` 行代码的作用，您可能已经猜到它们是由 SFML 提供的类。\n\n当我们创建一个类时，我们在一个**名称空间**中这样做。我们这样做是为了将我们的类与其他人编写的类区分开来。考虑一下这个类。在 Windows 这样的环境中，完全有可能已经有人编写了一个名为`VideoMode`的类。通过使用名称空间，我们和 SFML 程序员可以确保类的名称永远不会冲突。\n\n`VideoMode`类的完整使用方式如下：\n\n```cpp\nsf::VideoMode...\n```\n\n`using namespace sf;` 允许我们在代码中的任何地方省略`sf::`前缀。如果没有它，仅在这个简单的游戏中就有 100 多个`sf::`实例。它还使我们的代码更可读，也更短。\n\n## SFML 视频模式和渲染窗口\n\n在`main`函数中，我们现在有两条新注释和两行新的实际代码。实际代码的第一行是：\n\n```cpp\nVideoMode vm(1920, 1080);\n```\n\n此代码从名为`VideoMode`的类中创建一个名为`vm`的对象，并设置两个内部值`1920`和`1080`。这些值表示玩家屏幕的分辨率。\n\n下一行新代码如下所示：\n\n```cpp\nRenderWindow window(vm, “Timber!!!”, Style::Fullscreen);\n```\n\n在前一行代码中，我们正在从 SFML 提供的名为`RenderWindow`的类创建一个名为`window`的新对象。此外，我们正在窗口对象内设置一些值。\n\n首先，使用`vm`对象初始化`window`的一部分。起初，这似乎令人困惑。但是，请记住，类可以根据其创建者的意愿而变化和灵活。是的，一些类可以包含其他类的其他实例。\n\n提示\n\n只要您理解这个概念，就没有必要在此时完全理解它是如何工作的。我们编写一个类，然后从该类中生成可用的对象——有点像架构师绘制蓝图。你当然不能把你所有的家具、孩子和狗都搬到蓝图中去，但你可以从蓝图中建造一座房子（或许多房子）。在这个类比中，类就像蓝图，对象就像房子。\n\n接下来，我们使用`“Timber!!!”`值为窗口命名。然后，我们使用预定义的`Style::FullScreen`值使`window`对象全屏显示。\n\n提示\n\n`Style::FullScreen`是在 SFML 中定义的值。它很有用，因为我们不需要记住内部代码用来表示全屏的整数。此类值的编码术语为`constant`。常数和它们的密切 C++ 亲属，Po.T2 变量。\n\n让我们看一看正在运行的窗口对象。\n\n## 运行游戏\n\n此时您可以再次运行游戏。你会看到一个更大的黑屏闪烁，然后消失。这是我们刚刚编码的 1920 x 1080 全屏窗口。不幸的是，我们的程序仍然在启动，从`main`的第一行开始执行，创建一个很酷的新游戏窗口，然后进入`return 0;`并立即退出回到操作系统。\n\n接下来，我们将添加一些代码，这些代码将构成本书中每个游戏的基本结构。这就是所谓的游戏循环。\n\n# 主游戏循环\n\n我们需要一种方法，在玩家想要退出之前一直留在程序中。同时，我们应该清楚地标出代码的不同部分将在我们使用木材的过程中走向何处！！！。此外，如果我们要阻止我们的游戏退出，我们最好在玩家准备好时为他们提供退出的方式；否则，游戏将永远继续下去！\n\n将以下突出显示的代码添加到现有代码中，然后我们将浏览并讨论所有代码：\n\n```cpp\nint main()\n{\n// Create a video mode object\nVideoMode vm(1920, 1080);\n// Create and open a window for the game\nRenderWindow window(vm,\t“Timber!!!”, Style::Fullscreen);\n while (window.isOpen())\n {\n /*\n ****************************************\n Handle the players input\n ****************************************\n */\n if (Keyboard::isKeyPressed(Keyboard::Escape))\n {\n window.close();\n }\n /*\n ****************************************\n Update the scene\n ****************************************\n */\n /*\n ****************************************\n Draw the scene\n ****************************************\n */\n // Clear everything from the last frame\n window.clear();\n // Draw our game scene here\n // Show everything we just drew\n window.display();\n }\n    return 0;\n}\n```\n\n## While 循环\n\n我们在新代码中看到的第一件事如下：\n\n```cpp\nwhile (window.isOpen())\n{\n```\n\n我们在新代码中看到的最后一件事是结束`}`。我们已经创建了一个**while**循环。在`while`循环的开始（`{`和结束（`}`括号）之间的一切都将继续执行，一次又一次，可能永远。\n\n仔细观察`while`循环的括号`(...)`之间，如下所示：\n\n```cpp\nwhile (window.isOpen())\n```\n\n这段代码的完整解释必须等到我们在[*第 4 章*](04.html#_idTextAnchor110)*中讨论循环和条件，循环、数组、开关、枚举和函数——实现游戏机制*。现在重要的是，当`window`对象被设置为 closed 时，代码的执行将跳出`while`循环并转到下一条语句。窗户是如何关上的，很快就有人知道了。\n\n当然，下一句话是`return 0;`，它结束了我们的游戏。\n\n我们现在知道我们的`while`循环会一圈又一圈地旋转，重复执行其中的代码，直到我们的 window 对象被设置为 closed。\n\n## C 型代码注释\n\n就在`while`循环内部，我们可以看到乍一看可能有点像 ASCII 艺术：\n\n```cpp\n/*\n****************************************\nHandle the player’s input\n****************************************\n*/\n```\n\n重要提示\n\nASCII 艺术是一种利用计算机文本创建图像的利基但有趣的方式。您可以在此处阅读更多信息：[https://en.wikipedia.org/wiki/ASCII_art](https://en.wikipedia.org/wiki/ASCII_art) 。\n\n前面的代码只是另一种注释类型。这种类型的注释称为 C 风格的注释。注释以（`/*`开头，以（`*/`结尾）。介于两者之间的任何内容都只是为了提供信息，而不是编译。我已经使用了这个稍微复杂的文本来明确我们将在代码文件的每个部分中做什么。当然，您现在可以计算出，下面的任何代码都与处理玩家的输入有关。\n\n跳过几行代码，您将看到我们有另一个 C 风格的注释，宣布在代码的这一部分，我们将更新场景。\n\n如果你跳到下一个 C 风格的评论，我们将在哪里绘制所有的图形就很清楚了。\n\n## 输入、更新、绘制、重复\n\n尽管第一个项目使用了最简单的游戏循环版本，但每个游戏都需要代码中的这些阶段。让我们看一下步骤：\n\n1.  获取玩家的输入（如果有）。\n2.  根据人工智能、物理或玩家输入等内容更新场景。\n3.  绘制当前场景。\n4.  以足够快的速度重复这些步骤，以创建一个平滑、动画的游戏世界。\n\n现在，让我们看看在游戏循环中实际执行某些操作的代码。\n\n## 检测按键\n\n首先，在注释可识别的部分中，我们有以下代码：\n\n```cpp\nif (Keyboard::isKeyPressed(Keyboard::Escape))\n{\n window.close();\n}\n```\n\n此代码检查当前是否按下了*Esc*键。如果是，突出显示的代码使用`window`对象关闭自身。现在，下次`while`循环开始时，它将看到`window`对象已关闭，并在`while`循环的关闭花括号后立即跳转到代码，游戏将退出。我们将在[*第 2 章*](02.html#_idTextAnchor070)*中更全面地讨论 `if`语句、变量、运算符和决策–设置精灵动画*。\n\n## 清理并绘制场景\n\n目前，`Update the scene`部分中没有代码，所以让我们转到`Draw the scene`部分。\n\n我们要做的第一件事是使用以下代码擦去动画的前一帧：\n\n```cpp\nwindow.clear();\n```\n\n我们现在要做的是从游戏中抽出每一个物体。但是，我们没有任何游戏对象。\n\n下一行代码如下：\n\n```cpp\nwindow.display();\n```\n\n当我们绘制所有游戏对象时，我们正在将它们绘制到一个隐藏的曲面，以便显示。`window.display()`代码从先前显示的表面翻转到新更新（先前隐藏）的表面。这样，玩家将永远看不到绘制过程，因为曲面上添加了所有精灵。它还保证场景在翻转之前是完整的。这可以防止被称为**撕裂**的图形故障。这个过程称为**双缓冲**。\n\n还要注意，所有这些绘图和清除功能都是使用我们的`window`对象执行的，该对象是从 SFML`RenderWindow`类创建的。\n\n## 运行游戏\n\n运行游戏，您将获得一个空白的全屏窗口，该窗口将一直打开，直到您按下*Esc*键。\n\n这是一个良好的进展。在这个阶段，我们有一个正在执行的程序，它打开一个窗口并循环，等待玩家按*Esc*键退出。现在，我们可以继续绘制游戏的背景图像。\n\n# 绘制游戏背景\n\n现在，我们将在游戏中看到一些图形。我们需要做的是创建一个精灵。我们将创建的第一个将是游戏背景。然后我们可以在清除窗口和显示/翻转窗口之间绘制它。\n\n## 使用纹理准备雪碧\n\nSFML`RenderWindow`类允许我们创建`window`对象，该对象基本上负责游戏窗口所需的所有功能。\n\n现在我们来看另外两个 SFML 类，它们将负责将精灵绘制到屏幕上。也许毫不奇怪，其中一个类被称为`Sprite`。另一个类称为`Texture`。纹理是存储在存储器中的图形，在**图形处理单元**（**GPU**上）。\n\n由`Sprite`类生成的对象需要由`Texture`类生成的对象才能将自身显示为图像。添加以下突出显示的代码。试着弄清楚到底发生了什么。然后，我们将一行一行地通过它：\n\n```cpp\nint main()\n{\n    // Create a video mode object\n    VideoMode vm(1920, 1080);\n    // Create and open a window for the game\n    RenderWindow window(vm,\t“Timber!!!”, Style::Fullscreen);\n    // Create a texture to hold a graphic on the GPU\n    Texture textureBackground;\n    // Load a graphic into the texture\n    textureBackground.loadFromFile(“graphics/background.png”);\n    // Create a sprite\n    Sprite spriteBackground;\n    // Attach the texture to the sprite\n    spriteBackground.setTexture(textureBackground);\n    // Set the spriteBackground to cover the screen\n    spriteBackground.setPosition(0,0);\n    while (window.isOpen())\n    {\n```\n\n首先，我们从 SFML`Texture`类创建一个名为`textureBackground`的对象：\n\n```cpp\nTexture textureBackground;\n```\n\n完成后，我们可以使用`textureBackground`对象将图形从`graphics`文件夹加载到`textureBackground`，如下所示：\n\n```cpp\ntextureBackground.loadFromFile(“graphics/background.png”);\n```\n\n提示\n\n我们只需要指定`graphics/background`，因为路径是相对于我们创建文件夹并添加图像的 Visual Studio**工作目录**的。\n\n接下来，我们使用以下代码从 SFML`Sprite`类创建一个名为`spriteBackground`的对象：\n\n```cpp\nSprite spriteBackground;\n```\n\n然后，我们可以将`Texture`对象`backgroundTexture`与`Sprite`对象`backgroundSprite`关联起来，如下所示：\n\n```cpp\nspriteBackground.setTexture(textureBackground);\n```\n\n最后，我们可以将`spriteBackground`对象定位在`window`对象的`0,0`坐标处：\n\n```cpp\nspriteBackground.setPosition(0,0);\n```\n\n由于图形文件夹中的`background.png`图形宽 1920 像素，高 1080 像素，因此它将整齐地填充整个屏幕。请注意，前一行代码实际上并没有显示精灵。它只是设置它的位置，为显示它做好准备。\n\n`backgroundSprite`对象现在可用于显示背景图形。当然，你几乎肯定想知道为什么我们要以如此复杂的方式做事。原因在于图形卡和 OpenGL 的工作方式。\n\n纹理占用图形内存，而这个内存是一个有限的资源。此外，将图形加载到 GPU 内存中的过程非常缓慢——速度不会太慢，以至于你可以看到它发生，或者在它发生时你会看到你的电脑明显变慢，但速度足够慢，以至于你无法在游戏循环的每一帧都这样做。因此，将实际纹理（`textureBackground`与我们将在游戏循环期间操纵的任何代码分离是非常有用的。\n\n正如您将看到的，当我们开始移动图形时，我们将使用 sprite 进行移动。任何由`Texture`类生成的对象都会愉快地坐在 GPU 上，只需等待相关的`Sprite`对象告诉它在哪里显示自己。在以后的项目中，我们还将使用多个不同的`Sprite`对象重用相同的`Texture`对象，这将有效利用 GPU 内存。\n\n总之，我们可以陈述如下：\n\n*   纹理加载到 GPU 的速度非常慢。\n*   纹理一旦在 GPU 上就可以快速访问。\n*   我们将`Sprite`对象与纹理相关联。\n*   我们操纵`Sprite`对象的位置和方向（通常在`Update the scene` 部分）。\n\n我们绘制`Sprite`对象，它依次显示与其关联的`Texture`对象（通常在`Draw the scene` 部分）。所以，我们现在需要做的就是使用我们的双缓冲系统，这是由我们的`window`对象提供的，来绘制我们的新`Sprite`对象（`spriteBackground`，我们应该可以看到我们的游戏在运行。\n\n## 双缓冲背景精灵\n\n最后，我们需要在游戏循环中的适当位置绘制该精灵及其相关纹理。\n\n提示\n\n请注意，当我呈现来自同一块的代码时，我不会添加缩进，因为它减少了书籍文本中换行的实例。缩进是隐含的。查看下载包中的代码文件，了解缩进的充分使用。\n\n添加以下突出显示的代码：\n\n```cpp\n/*\n****************************************\nDraw the scene\n****************************************\n*/\n// Clear everything from the last run frame\nwindow.clear();\n// Draw our game scene here\nwindow.draw(spriteBackground);\n// Show everything we just drew\nwindow.display();\n```\n\n新的代码行简单地使用`window`对象来绘制`spriteBackground`对象，介于清除显示和显示新绘制的场景之间。\n\n我们现在知道了精灵是什么，我们可以将纹理与之关联，然后将其放置在屏幕上，最后绘制它。游戏已经准备好再次运行，这样我们就可以看到这段代码的结果了。\n\n## 运行游戏\n\n如果我们现在运行该程序，我们将看到第一个迹象，表明我们正在进行一场真正的游戏：\n\n![](img/B14278_01_35.jpg)\n\n在目前的状态下，它不会在 Steam 上获得年度独立游戏，但至少我们已经在路上了！\n\n让我们看看本章中可能出现的一些错误，以及在我们继续阅读本书时。\n\n# 处理错误\n\n你做的每一个项目都会有问题和错误。这是保证！问题越棘手，解决问题时就越令人满意。经过数小时的努力，当一个新的游戏功能终于出现时，它可能会引起真正的高潮。如果没有这场斗争，它将不知何故变得不值得。\n\n在这本书的某个时候，可能会有一些挣扎。保持冷静，相信你会克服它，然后开始工作。\n\n记住，不管你有什么问题，你很可能不是世界上第一个有同样问题的人。想一个简洁的句子来描述你的问题或错误，然后把它输入谷歌。你会惊讶地发现，别人已经很快、准确、经常地为你解决了问题。\n\n话虽如此，这里有一些指针（双关语；参见[*第 10 章*](10.html#_idTextAnchor214)*、指针、标准模板库和纹理管理*）可以帮助您开始，以防您在努力使第一章起作用。\n\n## 配置错误\n\n本章中出现问题的最可能原因是**配置错误**。正如您在设置 VisualStudio、SFML 和项目本身的过程中可能注意到的那样，有很多文件名、文件夹和设置需要恰到好处。仅仅一个错误的设置就可能导致一系列错误中的一个，这些错误的文本并不能确切地说明什么是错误的。\n\n如果您无法在黑屏工作的情况下获得空项目，那么重新开始可能会更容易。确保所有文件名和文件夹都适合您的特定设置，然后运行代码的最简单部分。这是屏幕闪烁黑色然后关闭的部分。如果您能够达到这个阶段，那么配置可能不是问题。\n\n## 编译错误\n\n编译错误可能是我们今后遇到的最常见的错误。检查您的代码是否与我的代码相同，尤其是行末尾的分号以及类和对象名称的大小写的细微变化。如果所有其他操作都失败，请打开下载包中的代码文件，然后复制并粘贴它。虽然本书中总是有可能出现代码输入错误，但代码文件是从实际工作项目中生成的——它们确实有效！\n\n## 链路错误\n\n链接错误很可能是由于缺少 SFML`.dll`文件造成的。你把它们都复制到项目文件夹了吗？\n\n## 虫子\n\nbug 是代码工作时发生的事情，但并不像您期望的那样。调试其实很有趣。你挤压的虫子越多，你的游戏就越好，你一天的工作也就越令人满意。解决 bug 的诀窍是尽早发现它们！要做到这一点，我建议您在每次实现新功能时运行和玩游戏。你越早发现这个 bug，你就越有可能对其原因记忆犹新。在本书中，我们将运行代码以查看每个可能阶段的结果。\n\n# 总结\n\n这是一个相当具有挑战性的章节，也许作为第一章有点刻薄。确实，配置一个 IDE 来使用 C++ 库可能有点尴尬和长。此外，众所周知，类和对象的概念对于不熟悉编码的人来说有点尴尬。\n\n现在，我们在这个阶段，但是，我们可以完全集中在 C++，SFML 和游戏。随着我们这本书的进步，我们将学习越来越多的 C++，以及实现越来越有趣的游戏功能。在这样做的同时，我们将进一步了解函数、类和对象等内容，以帮助它们更加神秘化。\n\n我们在本章中已经取得了大量的成果，包括概述了一个具有基本功能的 C++ 程序，构造了一个简单的游戏循环，它可以监听玩家的输入，并将精灵（连同其相关的纹理）绘制到屏幕上。\n\n在下一章中，我们将学习所有 C++，我们需要画更多的精灵并激活它们。\n\n# 常见问题\n\n以下是您可能想到的一些问题：\n\nQ） 到目前为止，我一直在为所呈现的内容而挣扎。我适合编程吗？\n\nA） 在本书中，设置一个开发环境并将 OOP 作为一个概念来考虑可能是最难的事情。只要游戏正常运行（绘制背景），就可以继续下一章。\n\nQ） 所有这些关于 OOP、类和对象的讨论太多了，有点破坏了整个学习体验。\n\nA） 别担心。我们将不断回到 OOP、类和对象。在[*第 6 章*](06.html#_idTextAnchor154)*面向对象编程——开始乒乓球游戏*中，我们将真正开始掌握整个 OOP。您现在需要了解的是，SFML 已经编写了大量有用的类，我们可以通过从这些类创建可用对象来使用这些代码。\n\nQ） 我真的不懂这个函数。\n\nA） 没关系；我们将不断地再次回到它，并将更彻底地了解函数。你只需要知道，当函数被调用时，它的代码被执行，当它完成（到达`return`语句）时，程序跳回调用它的代码。"
  },
  {
    "path": "docs/begin-cpp-game-prog/02.md",
    "content": "# 二、变量、运算符和决策——设置精灵动画\n\n在本章中，我们将在屏幕上做更多的绘制，为了实现这一点，我们将需要学习 C++ 的一些基础知识。我们将学习如何使用变量来记忆和操作值，并开始在游戏中添加更多图形。随着本章的深入，我们将了解如何操纵这些值来设置图形动画。这些值称为变量。\n\n以下是即将推出的产品：\n\n*   学习 C++ 变量的所有知识\n*   了解如何操作存储在变量中的值\n*   添加一个静态树图形，准备让玩家切掉\n*   绘制一只蜜蜂和三朵云并设置其动画\n\n# C++ 变量\n\n变量 T1 是我们的 C++ 游戏存储和操作值/数据的方式。如果我们想知道玩家有多健康，我们需要一个变量。也许你想知道在当前的浪潮中还有多少僵尸。这也是一个变量。如果你需要记住得到高分的球员的名字，你猜到了，我们需要一个变量。比赛结束了还是还在继续？是的，这也是一个变量。\n\n变量是 PC 内存中位置的命名标识符。PC 内存是计算机程序执行时存储的位置。因此，我们可以将一个变量命名为`numberOfZombies`，该变量可以引用内存中的一个位置，该位置存储一个值来表示当前 wave 中剩余的僵尸数量。\n\n计算机系统在内存中寻址位置的方式很复杂。编程语言使用变量为我们提供了一种人性化的方法来管理内存中的数据。\n\n我们刚才提到的少量变量意味着必须有不同的**类型**变量。\n\n## 变量类型\n\n有各种各样的 C++ 变量类型（参见下面几页关于变量在两页中的提示）。很容易用一整章的时间来讨论它们。下面是本书中最常用的变量类型表。然后，在下一节中，我们将了解如何使用这些变量类型：\n\n![](img/B14278_02_1.jpg)\n\n必须告诉编译器它是什么类型的变量，以便它能够为它分配适当的内存量。最好为您使用的每个变量使用最佳和最合适的类型。然而，在实践中，您通常可以通过提升变量而侥幸逃脱。也许您需要一个只有五个有效数字的浮点数？如果将其存储为`double`，编译器不会抱怨。但是，如果您尝试将`float`或`double`存储在`int`中，它将**更改**/**转换**值以适合`int`。随着本书的深入，我将明确在每种情况下使用的最佳变量类型是什么，我们甚至会看到一些在变量类型之间进行转换/强制转换的实例。\n\n上表中值得注意的一些额外细节包括所有`float`值旁边的`f`后缀。这个`f`后缀告诉编译器该值是`float`类型，而不是`double`。假定不带`f`前缀的浮点值为`double`。有关这方面的更多信息，请参阅下一个关于变量的提示。\n\n正如我们前面提到的，还有更多的类型。如果您想了解更多关于类型的信息，请参阅下一个关于变量的提示。\n\n### 用户定义类型\n\n用户定义的类型比我们刚才看到的类型要高级得多。当我们在 C++ 中讨论用户定义类型时，我们通常会谈论类。在上一章中，我们简要介绍了类及其相关对象。我们将在一个单独的文件中编写代码，有时是两个文件。然后我们可以声明、初始化和使用它们。我们将把如何定义/创建自己的类型留到[*第 6 章*](06.html#_idTextAnchor154)*【面向对象编程——开始乒乓游戏*。\n\n## 声明和初始化变量\n\n到目前为止，我们知道变量用于存储我们的游戏工作所需的数据/值。例如，一个变量表示一个玩家的生命数或玩家的名字。我们还知道，这些变量可以表示多种不同类型的值，例如`int`、`float`、`bool`等等。当然，我们还没有看到的是如何实际使用变量。\n\n创建和准备新变量分为两个阶段。这些阶段称为**声明**和**初始化**。\n\n### 声明变量\n\n我们可以在 C++ 中声明变量，如下所示：\n\n```cpp\n// What is the player's score?\nint playerScore;\n// What is the player's first initial\nchar playerInitial;\n// What is the value of pi\nfloat valuePi;\n// Is the player alive or dead?\nbool isAlive;\n```\n\n一旦我们编写了声明变量的代码，它就存在了，可以在代码中使用了。然而，我们通常希望给变量一个合适的值，这就是初始化的作用。\n\n### 初始化变量\n\n现在我们已经用有意义的名称声明了变量，我们可以用适当的值初始化这些变量，如下所示：\n\n```cpp\nplayerScore = 0;\nplayerInitial = 'J';\nvaluePi = 3.141f;\nisAlive = true;\n```\n\n此时，变量存在并持有特定值。很快，我们将看到如何改变、测试和响应这些值。接下来，我们将看到可以将声明和初始化合并到一个步骤中。\n\n### 一步声明和初始化\n\n如果适合我们，我们可以将声明和初始化步骤合并为一个步骤。有时，我们知道变量必须以什么值启动程序，一步声明和初始化是合适的。通常，我们不会，我们会先声明变量，然后在程序中初始化它，如下所示：\n\n```cpp\nint playerScore = 0;\nchar playerInitial = 'J';\nfloat valuePi = 3.141f;\nbool isAlive = true;\n```\n\n变量提示\n\n正如所承诺的，这里是关于变量的提示。如果您想查看 C++ 类型的完整列表，请查看此网页：http://www.tutorialspoint.com/cplusplus/cpp_data_types.htm 。如果您想对`float`、`double`和`f`后缀进行更深入的讨论，请阅读以下内容：[http://www.cplusplus.com/forum/beginner/24483/](http://www.cplusplus.com/forum/beginner/24483/) 。最后，如果您想知道 ASCII 字符代码的输入和输出，那么这里有更多信息：[http://www.cplusplus.com/doc/ascii/](http://www.cplusplus.com/doc/ascii/) 。请注意，这些链接是为特别好奇的读者准备的，为了继续，我们已经讨论了足够多的内容。\n\n### 常数\n\n有时，我们需要确保一个值永远不能更改。为此，我们可以使用`const`关键字声明并初始化**常量**：\n\n```cpp\nconst float PI = 3.141f;\nconst int PLANETS_IN_SOLAR_SYSTEM = 8;\nconst int NUMBER_OF_ENEMIES = 2000;\n```\n\n按照惯例，声明常量都是大写的。前面常量的值永远不能更改。我们将在[*第 4 章*](04.html#_idTextAnchor110)*中看到一些常量在起作用，循环、数组、开关、枚举和函数——实现游戏机制*。\n\n### 声明和初始化用户定义类型\n\n我们已经看到了如何声明和初始化一些 SFML 定义类型的示例。这是因为我们可以非常灵活地创建/定义这些类型（类），因此我们声明和初始化它们的方式也非常不同。下面是前一章中关于声明和初始化用户定义类型的几个提示。\n\n创建一个名为`vm`的`VideoMode`类型的对象，并使用两个`int`值`1920`和`1080`对其进行初始化：\n\n```cpp\n// Create a video mode object\nVideoMode vm(1920, 1080);\n```\n\n创建一个名为`textureBackground`的`Texture`类型的对象，但不进行任何初始化：\n\n```cpp\n// Create a texture to hold a graphic on the GPU\nTexture textureBackground;\n```\n\n请注意，尽管我们没有建议使用任何特定值来初始化`textureBackground`，但一些变量设置可能会在内部进行，这是可能的（事实上，很有可能的）。在这一点上，对象是否需要/具有提供初始化值的选项完全取决于类的编码方式，并且几乎是无限灵活的。这进一步表明，当我们开始编写自己的类时，会有一些复杂性。幸运的是，这也意味着我们将拥有强大的能力来设计我们的类型/类，使之成为我们制作游戏所需要的！将这种巨大的灵活性添加到 SFML 设计类的强大功能中，我们游戏的潜力几乎是无限的。\n\n在本章中，我们还将看到 SFML 提供的更多用户创建的类型/类，并在本书中加载更多。\n\n我们现在已经看到，变量是计算机内存中的一个命名位置，变量可以是一个简单的整数，也可以是一个更强大的对象。既然我们知道可以初始化这些变量，我们将看看如何操作它们所持有的值。\n\n# 操纵变量\n\n此时，我们确切地知道变量是什么，它们可以是什么主要类型，以及如何声明和初始化它们。然而，我们仍然不能对他们做那么多。我们需要操纵我们的变量；添加它们；把他们带走；然后进行乘法、除法和测试。\n\n首先，我们将讨论如何操纵它们，然后我们将研究如何以及为什么测试它们。\n\n## C++ 算法与赋值算子\n\n为了操纵变量，C++ 有一个范围的算术运算算子，即 T1 和 Ty2 T2 赋值算子 T3。幸运的是，大多数算术运算符和赋值运算符使用起来非常直观，而那些不直观的运算符则很容易解释。首先，让我们看看算术运算符表，然后是赋值运算符表，我们将在本书中经常使用这些运算符：\n\n![](img/B14278_02_2.jpg)\n\n现在，对于赋值运算符：\n\n![](img/B14278_02_3.jpg)\n\n重要提示\n\n从技术上讲，除了`=`、`--`和`++ `之外，所有这些运算符都被称为**复合赋值运算符**，因为它们包含多个运算符。\n\n现在我们已经看到了大量的算术运算符和赋值运算符，我们实际上可以看看如何通过组合运算符、变量和值来形成**表达式**来操作变量。\n\n## 用表情做事\n\n**表达式**是组合变量、运算符和值的结果。使用表达式，我们可以得到一个结果。此外，我们很快就会看到，我们可以在测试中使用表达式。这些测试可以用来决定我们的代码下一步应该做什么。首先，让我们看看一些可能在游戏代码中看到的简单表达式。下面是一个简单表达式的示例：\n\n```cpp\n// Player gets a new high score\nhiScore = score;\n```\n\n在前面的代码中，`score`变量中保存的值用于更改`hiScore`变量中的值。这两个变量现在具有相同的值，但请注意，它们仍然是独立的、不同的变量（内存中的位置）。这可能正是我们需要的球员击败高分。下面是另一个例子：\n\n```cpp\n// Set the score to 100\nscore = 100;\n```\n\n让我们看看加法运算符，它与赋值运算符一起使用：\n\n```cpp\n// Add to the score when an alien is shot\nscore = aliensShot + wavesCleared;\n```\n\n在前面的代码中，使用加法运算符将`aliensShot`和`wavesCleared`持有的值相加，然后将相加结果分配给`score`变量。现在，让我们来看看下面的代码：\n\n```cpp\n// Add 100 to whatever the score currently is\nscore = score + 100;\n```\n\n请注意，在运算符的两侧使用相同的变量是完全可以接受的。在前面的代码中，将 100 添加到`score`变量所持有的值中，然后将该新值分配回`score`。\n\n将减法运算符与赋值运算符结合使用。以下代码将减法运算符右侧的值与左侧的值相减。它通常与赋值运算符一起使用，可能类似于：\n\n```cpp\n// Uh oh lost a life\nlives = lives - 1;\n```\n\n它也可以这样使用：\n\n```cpp\n// How many aliens left at end of game\naliensRemaining = aliensTotal - aliensDestroyed;\n```\n\n接下来，我们将了解如何使用除法运算符。下面的代码将左侧的数字除以右侧的数字。同样，它通常与赋值运算符一起使用，如下所示：\n\n```cpp\n// Make the remaining hit points lower based on swordLevel\nhitPoints = hitPoints / swordLevel;\n```\n\n它也可以这样使用：\n\n```cpp\n// Give something, but not everything, back for recycling a block\nrecycledValueOfBlock = originalValue / .9f;\n```\n\n显然，在前面的示例中，`recycledValueOfBlock`变量必须是`float`类型，才能准确地存储类似计算的答案。\n\n也许毫不奇怪，我们可以这样使用乘法运算符：\n\n```cpp\n// answer is equal to 100, of course\nanswer = 10 * 10;\n```\n\n它也可以这样使用：\n\n```cpp\n// biggerAnswer = 1000, of course\nbiggerAnswer = 10 * 10 * 10;\n```\n\n重要提示\n\n另一方面，你有没有想过 C++ 的名字是怎么来的？C++ 是 C 语言的扩展。它的发明者，**比亚恩·斯特劳斯特鲁普**最初称它为“带类的 C”，但这个名字后来演变而来。如果你感兴趣的话，你可以阅读在 T2 T2 上的 C++ 故事。http://www.cplusplus.com/info/history/ 。\n\n现在，让我们看一下增量操作符的作用。这是一种巧妙的方法，可以将 1 添加到游戏变量的值中。\n\n请看下面的代码：\n\n```cpp\n// Add one to myVariable\nmyVariable = myVariable + 1;\n```\n\n上述代码给出的结果与以下代码相同：\n\n```cpp\n// Much neater and quicker\nmyVariable ++ ;\n```\n\n减量运算符`--`，你猜到了，是从某物中减去 1 的一种快速方法，如下所示：\n\n```cpp\nplayerHealth = playerHealth -1;\n```\n\n这与执行以下操作相同：\n\n```cpp\nplayerHealth --;\n```\n\n让我们看看更多的运营商的行动，然后我们可以回到建设木材！！！游戏加法、减法、乘法和除法运算符都有一个相关的运算符，该运算符将其主要功能（加法、减法等）与赋值相结合。当我们想要执行操作符的主要功能，然后是赋值时，它们允许我们使用更简洁的代码。请看以下四个示例（每个操作员一个）：\n\n```cpp\nsomeVariable = 10;\n// Multiply the variable by 10 and put the answer \n// back in the variable\nsomeVariable *= 10;\n// someVariable now equals 100\n// Divide someVariable by 5 put the answer back \n// into the variable\nsomeVariable /= 5;\n// someVariable now equals 20\n// Add 3 to someVariable and put the answer back \n// into the variable\nsomeVariable += 3;\n// someVariable now equals 23\n// Take 25 from someVariable and put the answer back \n// into the variable\nsomeVariable -= 25;\n// someVariable now equals -2\n```\n\n在前面的四个示例中，我们可以看到，当我们要使用四个算术运算符中的一个并后跟赋值时，可以使用`*=`、`/=`、`+=`和`-=`运算符来缩短语法。在这本书中，我们会做很多。\n\n是时候给我们的游戏添加更多的精灵了。\n\n# 添加云、树和嗡嗡叫的蜜蜂\n\n在本节中，我们将为我们的木材添加云、树和嗡嗡叫的蜜蜂！！！游戏首先，我们将添加一棵树。这很容易。这是因为树不会移动。我们将使用上一章绘制背景时使用的相同程序。蜜蜂和云层也会很容易在它们的起始位置绘制，但是我们需要结合我们刚刚学到的关于操纵变量和一些新的 C++ 主题来让它们移动的知识。\n\n## 准备树\n\n让我们准备好画这棵树吧！添加以下突出显示的代码。请注意未高亮显示的代码，这是我们已经编写的代码。这将帮助您确定新代码应该在设置背景位置之后，但在主游戏循环开始之前立即键入。在添加新代码后，我们将对其进行概述：\n\n```cpp\nint main()\n{\n    // Create a video mode object\n    VideoMode vm(1920, 1080);\n    // Create and open a window for the game\n    RenderWindow window(vm, \"Timber!!!\", Style::Fullscreen);\n    // Create a texture to hold a graphic on the GPU\n    Texture textureBackground;\n    // Load a graphic into the texture\n    textureBackground.loadFromFile(\"graphics/background.png\");\n    // Create a sprite\n    Sprite spriteBackground;\n    // Attach the texture to the sprite\n    spriteBackground.setTexture(textureBackground);\n    // Set the spriteBackground to cover the screen\n    spriteBackground.setPosition(0, 0);\n    // Make a tree sprite\n    Texture textureTree;\n    textureTree.loadFromFile(\"graphics/tree.png\");\n    Sprite spriteTree;\n    spriteTree.setTexture(textureTree);\n    spriteTree.setPosition(810, 0);\n\n    while (window.isOpen())\n    {\n```\n\n以下五行代码（不包括注释）就是这样做的：\n\n1.  首先，我们创建一个名为`textureTree`的`Texture`类型的对象。\n2.  接下来，我们从`tree.png`图形文件将图形加载到纹理中。\n3.  然后，我们声明一个名为`spriteTree`的`Sprite`类型的对象\n4.  之后，我们将`textureTree`与`spriteTree`关联。无论何时我们绘制`spriteTree`，它都会显示`textureTree`纹理，这是一个整洁的树图形。\n5.  最后，我们使用*x*轴上的坐标`810`和*y*轴上的坐标`0`设置树的位置。\n\n树精灵和树纹理已准备好绘制。让我们继续讨论 bee 对象，它的处理方式几乎相同。\n\n## 准备蜜蜂\n\n准备蜜蜂雪碧与准备树雪碧非常相似，但并不完全相同。下面的代码与树代码之间的差异很小，但很重要。由于蜜蜂需要移动，我们还声明了两个与蜜蜂相关的变量。添加以下突出显示的代码，看看您是否能够了解如何使用`beeActive`和`beeSpeed`变量：\n\n```cpp\n// Make a tree sprite\nTexture textureTree;\ntextureTree.loadFromFile(\"graphics/tree.png\");\nSprite spriteTree;\nspriteTree.setTexture(textureTree);\nspriteTree.setPosition(810, 0);\n// Prepare the bee\nTexture textureBee;\ntextureBee.loadFromFile(\"graphics/bee.png\");\nSprite spriteBee;\nspriteBee.setTexture(textureBee);\nspriteBee.setPosition(0, 800);\n// Is the bee currently moving?\nbool beeActive = false;\n// How fast can the bee fly\nfloat beeSpeed = 0.0f;\nwhile (window.isOpen())\n{\n```\n\n我们创建蜜蜂的方式与创建背景和树的方式相同。我们使用一个`Texture`，一个`Sprite`，并将两者关联起来。注意，在前面的 bee 代码中，有一些我们以前没有见过的新代码。有一个`bool`变量用于确定蜜蜂是否活跃。记住，`bool`变量可以是`true`或`false`。我们暂时将`beeActive`初始化为`false`。\n\n接下来，我们声明一个名为`beeSpeed`的新`float`变量。这将保持蜜蜂在屏幕上以每秒像素的速度飞行。\n\n很快，我们将看到如何使用这两个新变量来移动蜜蜂。在此之前，让我们以几乎相同的方式设置一些云。\n\n## 准备云彩\n\n添加以下突出显示的代码。研究新代码，并尝试找出它的作用：\n\n```cpp\n// Prepare the bee\nTexture textureBee;\ntextureBee.loadFromFile(\"graphics/bee.png\");\nSprite spriteBee;\nspriteBee.setTexture(textureBee);\nspriteBee.setPosition(0, 800);\n// Is the bee currently moving?\nbool beeActive = false;\n// How fast can the bee fly\nfloat beeSpeed = 0.0f;\n// make 3 cloud sprites from 1 texture\nTexture textureCloud;\n// Load 1 new texture\ntextureCloud.loadFromFile(\"graphics/cloud.png\");\n// 3 New sprites with the same texture\nSprite spriteCloud1;\nSprite spriteCloud2;\nSprite spriteCloud3;\nspriteCloud1.setTexture(textureCloud);\nspriteCloud2.setTexture(textureCloud);\nspriteCloud3.setTexture(textureCloud);\n// Position the clouds on the left of the screen\n// at different heights\nspriteCloud1.setPosition(0, 0);\nspriteCloud2.setPosition(0, 250);\nspriteCloud3.setPosition(0, 500);\n// Are the clouds currently on screen?\nbool cloud1Active = false;\nbool cloud2Active = false;\nbool cloud3Active = false;\n// How fast is each cloud?\nfloat cloud1Speed = 0.0f;\nfloat cloud2Speed = 0.0f;\nfloat cloud3Speed = 0.0f;\nwhile (window.isOpen())\n{\n```\n\n我们刚刚添加的代码中唯一一件看起来有点奇怪的事情是我们只有一个`Texture`类型的对象。多个`Sprite`对象共享一个纹理是完全正常的。一旦`Texture`存储在 GPU 内存中，它就可以很快与`Sprite`对象关联。只有在`loadFromFile`代码中的图形的初始加载是一个相对缓慢的操作。当然，如果我们想要三种不同形状的云，那么我们需要三种纹理。\n\n除了轻微的纹理问题，我们刚刚添加的代码与蜜蜂相比并不是什么新鲜事。唯一的区别是有三个云精灵，三个`bool` 变量用于确定每个云是否处于活动状态，三个`float`变量用于保持每个云的速度。\n\n在这个阶段，所有的精灵和变量都已经准备好了。现在我们可以继续绘制它们。\n\n## 画树、蜜蜂和云\n\n最后，通过在绘图部分添加以下突出显示的代码，我们可以将它们全部绘制到屏幕上：\n\n```cpp\n/*\n****************************************\nDraw the scene\n****************************************\n*/\n// Clear everything from the last run frame\nwindow.clear();\n// Draw our game scene here\nwindow.draw(spriteBackground);\n// Draw the clouds\nwindow.draw(spriteCloud1);\nwindow.draw(spriteCloud2);\nwindow.draw(spriteCloud3);\n// Draw the tree\nwindow.draw(spriteTree);\n// Draw the insect\nwindow.draw(spriteBee);\n// Show everything we just drew\nwindow.display();\n```\n\n绘制三朵云、蜜蜂和树的方式与绘制背景的方式相同。但是，请注意，我们在屏幕上绘制不同对象的顺序。我们必须在背景后绘制所有图形，否则它们将被覆盖，我们必须在树前绘制云，否则它们将看起来有点奇怪，在树前漂移。蜜蜂在树前或树后看起来都很好。我选择把蜜蜂画在树前，这样它就可以分散伐木工人的注意力，有点像真正的蜜蜂。\n\n跑木材！！！敬畏地凝视着那棵树，三朵云，还有一只蜜蜂……什么都不做！他们看起来像是在排队参加比赛；蜜蜂必须后退的比赛：\n\n![](img/B14278_02_01.jpg)\n\n利用我们对操作符的了解，我们可以尝试移动刚刚添加的图形，但有一个问题。问题是，真正的云和蜜蜂以不均匀的方式移动。它们没有固定的速度或位置，这些因素取决于风速或蜜蜂可能有多匆忙等因素。对于不经意的观察者来说，他们所走的道路和速度似乎是*随机*。\n\n# 随机数\n\n**随机数**在游戏中有很多用途，可能决定玩家使用哪张牌，或者从敌人的健康中减去一定范围内的伤害。现在我们将学习如何生成随机数，以确定蜜蜂和云的起始位置和速度。\n\n## 在 C++ 中生成随机数的方法\n\n为了生成随机数，我们将需要使用更多的 C++ 函数，更精确。不要在游戏中添加任何代码。让我们看看一些假设代码所需的语法和步骤。\n\n计算机不能真正地挑选随机数。他们只能使用**算法**/**计算**来挑选*看似*随机的数字。为了使该算法不会不断返回相同的值，我们必须**对**随机数生成器进行种子设定。种子可以是任何整数，但每次需要唯一的随机数时，它必须是不同的种子。请看以下代码，该代码为随机数生成器种子：\n\n```cpp\n// Seed the random number generator with the time\nsrand((int)time(0));\n```\n\n前面的代码使用`time`函数从 PC 获取时间，即`time(0)`。对`time`函数的调用随附为发送给`srand`函数的值。其结果是将当前时间用作种子。\n\n前面的代码看起来有点复杂，因为看起来有点不寻常的`(int)`语法。这样做的目的是将从`time`返回的值转换/强制转换为`int`。在这种情况下，`srand`功能需要这样做。\n\n重要提示\n\n用于描述从一种类型到另一种类型的转换的术语是**cast**。\n\n总之，前一行代码执行以下操作：\n\n*   使用`time`获取时间\n*   将其转换为`int`\n*   将此结果值发送到`srand`，由`srand`为随机数生成器种子\n\n当然，时间总是在变化。这使得`time`函数成为为随机数生成器播种的好方法。但是，想想如果我们多次给随机数生成器播种，并且以如此快速的连续方式，`time`返回相同的值，可能会发生什么。当我们为云设置动画时，我们将看到并解决这个问题。\n\n在这个阶段，我们可以在一个范围内创建随机数，并将其保存到一个变量中供以后使用，如下所示：\n\n```cpp\n// Get the random number & save it to a variable called number\nint number = (rand() % 100);\n```\n\n注意我们给`number`赋值的方式很奇怪。通过使用模运算符（`%`和`100`的值，我们要求将`rand`返回的数字除以 100，得到余数。当你除以 100 时，你可以得到的最大余数是 99。可能的最低数字是 0。因此，前面的代码将生成一个介于 0 和 99 之间（含 0 和 99）的数字。这些知识将有助于为我们的蜜蜂和云生成随机速度和起始位置。\n\n但是在我们实现随机蜜蜂和云之前，我们需要学习如何在 C++ 中做出决定。\n\n# 与 if 和 else 一起做出决策\n\n如果我们允许作出决定的话，我们可以做决定。在上一章中，我们已经看到了`if`在动作中，当我们检测到玩家是否在每一帧按下*Esc*键时：\n\n```cpp\nif (Keyboard::isKeyPressed(Keyboard::Escape))\n{\n    window.close();\n}\n```\n\n到目前为止，我们已经了解了如何使用算术和赋值运算符来创建表达式。现在，我们将介绍一些新的运营商。\n\n## 逻辑运算符\n\n**逻辑运算符**将通过构建可以测试值为真或假的表达式来帮助我们做出决策。起初，这似乎是一个非常狭窄的选择，对于高级 PC 游戏中可能需要的选择来说是不够的。一旦我们再深入一点，我们就会发现，我们只需要几个逻辑运算符就可以做出所需的所有决策。\n\n下面是最有用的逻辑运算符表。查看它们和相关示例，然后我们将了解如何使用它们：\n\n![](img/B14278_02_4.jpg)\n\n让我们来看看 C++ 的 To0T0T 和 OutT1 AES 关键字，这将允许我们把所有这些逻辑运算符好好利用。\n\n## C++ IF 和其他\n\n让我们让前面的例子不那么抽象。满足 C++ HOLT T1。我们将使用`if`和一些操作符以及一个小故事来演示它们的使用。接下来是一个虚构的军事局势，希望它不会像前面的例子那样抽象。\n\n## 如果他们过桥，开枪打死他们！\n\n上尉快要死了，他知道自己剩下的下属经验不足，所以决定写一个 C++ 程序来传达他死后的最后命令。部队在等待增援时必须守住桥的一侧。\n\n上尉想让他的部队明白的第一个命令是：\n\n“如果他们过桥，就开枪！”\n\n那么，我们如何模拟 C++ 中的这种情况呢？我们需要一个`bool`变量`isComingOverBridge`。以下代码位假定`isComingOverBridge`变量已声明并初始化为`true`或`false`。\n\n然后我们可以这样使用`if`：\n\n```cpp\nif(isComingOverBridge)\n{\n    // Shoot them\n}\n```\n\n如果`isComingOverBridge`变量等于`true`，则会运行打开和关闭花括号`{...}`内的代码。如果没有，程序将在`if`块之后继续运行，而不运行其中的代码。\n\n## 射杀他们……否则就这样做\n\n上尉还想告诉他的部队，如果敌人没有从桥上过来，就待在原地不动。\n\n现在，我们可以介绍另一个 C++ 关键字，当我们想要在`if`没有**求值为`true`的情况下显式地做某事时，我们可以使用`else`。**\n\n例如，如果敌人没有从桥上过来，我们可以编写以下代码来告诉部队待在原地：\n\n```cpp\nif(isComingOverBridge)\n{\n    // Shoot them\n}\nelse\n{\n    // Hold position\n}\n```\n\n船长随后意识到问题并不像他最初想的那么简单。如果敌人从桥上过来，但是军队太多怎么办？他的小队将被消灭。因此，他提出了以下代码（这次我们还将使用一些变量）：\n\n```cpp\nbool isComingOverBridge;\nint enemyTroops;\nint friendlyTroops;\n// Initialize the previous variables, one way or another\n// Now the if\nif(isComingOverBridge && friendlyTroops > enemyTroops)\n{\n    // shoot them\n}\nelse if(isComingOverBridge && friendlyTroops < enemyTroops) \n{\n    // blow the bridge\n}\nelse\n{\n    // Hold position\n}\n```\n\n前面的代码有三种可能的执行路径。首先，如果敌人正在过桥，而友军人数较多：\n\n```cpp\nif(isComingOverBridge && friendlyTroops > enemyTroops)\n```\n\n第二种情况发生在敌军越过桥梁，但人数超过友军时：\n\n```cpp\nelse if(isComingOveBridge && friendlyTroops < enemyTroops)\n```\n\n然后，第三个也是最后一个可能的结果（如果其他两个都不是`true`将执行）被最终`else`捕获，没有`if`条件。\n\n## 读者挑战\n\n你能用前面的代码发现一个缺陷吗？一个可能让一群没有经验的军队陷入完全混乱的人？敌军和友军人数完全相等的可能性尚未明确处理，因此将由最终`else`处理。最后的`else`是为了在没有敌军的情况下进行的。我想任何自尊的上尉都会希望他的部队在这种情况下作战。他可以更改第一个`if`语句以适应这种可能性，如下所示：\n\n```cpp\nif(isComingOverBridge && friendlyTroops >=  enemyTroops)\n```\n\n最后，上尉最后担心的是，如果敌人挥舞着投降的白旗跨过桥来并被迅速屠杀，那么他的士兵最终将成为战犯。需要的 C++ 代码是显而易见的。使用`wavingWhiteFlag`布尔变量，他编写了以下测试：\n\n```cpp\nif (wavingWhiteFlag)\n{\n    // Take prisoners\n}\n```\n\n但这段代码放在哪里还不太清楚。最后，机长选择了以下嵌套解决方案，并将`wavingWhiteFlag`的测试改为逻辑非，如下所示：\n\n```cpp\nif (!wavingWhiteFlag)\n{\n    // not surrendering so check everything else\t\n    if(isComingOverTheBridge && friendlyTroops >= enemyTroops)\n    {\n        // shoot them\n    }\n\n    else if(isComingOverTheBridge && friendlyTroops < enemyTroops) \n    {\n        // blow the bridge\n    }\n}\nelse\n{\n    // this is the else for our first if\n    // Take prisoners\n}\n// Holding position\n```\n\n这表明我们可以在彼此内部嵌套`if`和`else`语句，以创建相当深入和详细的决策。\n\n我们可以通过`if`和`else`继续做出越来越复杂的决策，但我们所看到的作为一个介绍已经足够了。也许值得一提的是，解决问题的方法往往不止一种。*正确的*方法通常是以最清晰、最简单的方式解决问题的方法。\n\n我们越来越接近所有的 C++ 知识，我们需要能够动画我们的云和蜜蜂。我们还有最后一个动画问题要讨论，然后我们可以回到游戏。\n\n# 定时\n\n在我们移动蜜蜂和云层之前，我们需要考虑时机。我们已经知道，主游戏循环会重复执行，直到玩家按下*退出*键。\n\n我们还知道 C++ 和 SFML 是非常快的。事实上，我老化的笔记本电脑以每秒 5000 次的速度执行一个简单的游戏循环（就像现在的循环一样）。\n\n## 帧速率问题\n\n让我们考虑蜜蜂的速度。为了便于讨论，我们可以假设它将以每秒 200 像素的速度移动。在 1920 像素宽的屏幕上，穿过整个宽度大约需要 10 秒，因为 10 x 200 等于 2000（接近 1920）。\n\n此外，我们知道我们可以用`setPosition(...,...)`定位任何精灵。我们只需要把 x 和 y 坐标放在括号里。\n\n除了设置精灵的位置外，我们还可以获取精灵的当前位置。例如，为了获得蜜蜂的水平 x 坐标，我们将使用以下代码：\n\n```cpp\nint currentPosition = spriteBee.getPosition().x;\n```\n\n蜜蜂的当前 x 坐标现在存储在`currentPosition`中。要将蜜蜂移动到右侧，我们需要将 200（我们的预期速度）除以 5000（我的笔记本电脑上的每秒大约帧数）的适当分数添加到`currentPosition`，如下所示：\n\n```cpp\ncurrentPosition += 200/5000;\n```\n\n现在，我们将使用`setPosition` 移动我们的蜜蜂。它将平滑地从左向右移动 200 除以每帧 5000 像素。但这种方法存在两大问题。\n\n帧速率是每秒处理游戏循环的次数。也就是说，我们处理玩家输入、更新游戏对象并将其绘制到屏幕上的次数。在本书的其余部分，我们将进一步讨论帧速率问题。\n\n我笔记本电脑上的帧速率可能并不总是恒定的。蜜蜂可能看起来像是间歇性地在屏幕上“助推”。\n\n当然，我们希望我们的游戏有更多的观众，而不仅仅是我的笔记本电脑！每台电脑的帧速率都会有所不同，至少略有不同。如果你有一台旧电脑，这只蜜蜂似乎被铅压得喘不过气来，如果你有最新的游戏装备，它可能是一只模糊的涡轮蜜蜂。\n\n幸运的是，每个游戏的问题都是一样的，SFML 提供了一个解决方案。理解此解决方案的最简单方法是实现它。\n\n## SFML 帧速率解决方案\n\n现在我们将测量并使用帧速率来控制我们的游戏。要开始实现此功能，请在主游戏循环之前添加以下代码：\n\n```cpp\n// How fast is each cloud?\nfloat cloud1Speed = 0;\nfloat cloud2Speed = 0;\nfloat cloud3Speed = 0;\n\n// Variables to control time itself\nClock clock;\nwhile (window.isOpen())\n{\n```\n\n在前面的代码中，我们声明了一个`Clock`类型的对象，并将其命名为`clock`。类名以大写字母开头，对象名（我们将实际使用）以小写字母开头。对象名称是任意的，但是`clock`似乎是时钟的合适名称。我们也将在这里添加更多与时间相关的变量。\n\n现在，在游戏代码的更新部分，添加以下突出显示的代码：\n\n```cpp\n/*\n****************************************\nUpdate the scene\n****************************************\n*/\n// Measure time\nTime dt = clock.restart();\n/*\n****************************************\nDraw the scene\n****************************************\n*/\n```\n\n正如您所料，`clock.restart()`功能会重新启动时钟。我们希望每一帧重新启动时钟，这样我们就可以计算出每一帧需要多长时间。但是，除此之外，它还返回自上次重新启动时钟以来经过的时间量。\n\n因此，在前面的代码中，我们声明了一个名为`dt`的`Time`类型的对象，并使用它存储`clock.restart()`函数返回的值。\n\n现在，我们有一个名为`dt`的`Time`对象，它保存自上次更新场景并重新启动时钟以来经过的时间量。也许你能看到这是怎么回事？我们将使用每帧经过的时间来控制我们移动蜜蜂和云的距离。\n\n让我们在游戏中添加更多的代码，并使用到目前为止我们所学到的关于操作变量、生成随机数、`if`关键字和`else`关键字的所有知识。然后，我们将看到如何使用`Clock`对象和`dt`来克服帧率问题。\n\n重要提示\n\n`dt`代表**增量时间**，是两次更新之间的时间。\n\n# 移动云和蜜蜂\n\n让我们使用从最后一帧开始经过的时间来为蜜蜂和云彩呼吸生命。这将解决在不同 PC 之间具有一致帧速率的问题。\n\n## 赋予蜜蜂生命\n\n我们要做的第一件事是将蜜蜂设置在一定的高度和速度。我们只想在蜜蜂不活动时这样做。因此，我们将以下代码包装在一个`if`块中。检查并添加以下突出显示的代码，然后我们将对其进行讨论：\n\n```cpp\n/*\n****************************************\nUpdate the scene\n****************************************\n*/\n// Measure time\nTime dt = clock.restart();\n// Setup the bee\nif (!beeActive)\n{\n    // How fast is the bee\n    srand((int)time(0));\n    beeSpeed = (rand() % 200) + 200;\n    // How high is the bee\n    srand((int)time(0) * 10);\n    float height = (rand() % 500) + 500;\n    spriteBee.setPosition(2000, height);\n    beeActive = true;\n}\n/*\n****************************************\nDraw the scene\n****************************************\n*/\n```\n\n现在，如果蜜蜂没有激活，就像游戏第一次启动时没有一样，`if(!beeActive)`将是`true`，前面的代码将按照以下顺序执行以下操作：\n\n*   为随机数生成器设定种子。\n*   获取一个介于 200 和 399 之间的随机数，并将结果分配给`beeSpeed`。\n*   再次为随机数生成器设定种子。\n*   获取一个介于 500 和 999 之间的随机数，并将结果分配给一个名为`height`的新`float`变量。\n*   将蜜蜂的位置设置为 x 轴上的`2000`（屏幕右侧）和 y 轴上的`height`相等值。\n*   Set `beeActive` to true.\n\n    重要提示\n\n    注意，`height`变量是我们在游戏循环中声明的第一个变量。此外，因为它是在`if`块内声明的，所以它实际上在`if`块外是“不可见的”。这对我们来说很好，因为一旦我们设定了蜜蜂的高度，我们就不再需要它了。这种影响变量的现象称为**范围**。我们将在[*第 4 章*](04.html#_idTextAnchor110)*【循环、数组、开关、枚举和函数——实现游戏机制*中对此进行更全面的探讨。\n\n如果我们运行游戏，蜜蜂还不会发生任何事情，但是现在蜜蜂处于活动状态，我们可以编写一些代码，在`beeActive`为`true`时运行。\n\n添加以下突出显示的代码，如您所见，无论何时`beeActive`为`true`都会执行。这是因为在`if(!beeActive)`块后面跟有`else`：\n\n```cpp\n// Set up the bee\nif (!beeActive)\n{\n    // How fast is the bee\n    srand((int)time(0) );\n    beeSpeed = (rand() % 200) + 200;\n    // How high is the bee\n    srand((int)time(0) * 10);\n    float height = (rand() % 1350) + 500;\n    spriteBee.setPosition(2000, height);\n    beeActive = true;\n}\nelse\n// Move the bee\n{\n    spriteBee.setPosition(\nspriteBee.getPosition().x - \n         (beeSpeed * dt.asSeconds()),\n        spriteBee.getPosition().y);\n    // Has the bee reached the left-hand edge of the screen?\n    if (spriteBee.getPosition().x < -100)\n    {\n        // Set it up ready to be a whole new bee next frame\n        beeActive = false;\n    }\n}\n/*\n****************************************\nDraw the scene\n****************************************\n*/\n```\n\n在`else`块中，发生了以下事情。\n\n蜜蜂的位置根据以下标准改变。`setPosition`函数使用`getPosition`函数获取蜜蜂当前的水平坐标。然后从该坐标中减去`beeSpeed * dt.asSeconds()`。\n\n`beeSpeed`变量值为每秒多个像素，在上一个`if` 块中随机分配。`dt.asSeconds()`的值将是 1 的一小部分，表示上一帧动画所用的时间。\n\n假设蜜蜂当前的水平坐标为**1000**。现在，假设一台基本 PC 以每秒 5000 帧的速度循环。这意味着`dt.asSeconds`将是**0.0002**。现在，我们还假设`beeSpeed`设置为每秒最大**399**像素。有了这些信息，我们可以说确定`setPosition`用于水平坐标的值的代码如下：\n\n```cpp\n1000 - 0.0002 x 399\n```\n\n因此，蜜蜂在水平轴上的新位置为 999.9202。我们可以看到蜜蜂非常非常平滑地向左漂移，每帧的像素数低于一个。如果帧速率波动，则公式将生成一个新的值以适合。如果我们在一台每秒只有 100 帧的电脑或一台每秒有一百万帧的电脑上运行相同的代码，蜜蜂将以相同的速度移动。\n\n`setPosition`功能使用`getPosition().y`使蜜蜂在整个活动周期内保持完全相同的垂直坐标。\n\n我们刚才添加的`else`块中代码的最后一部分如下：\n\n```cpp\n// Has the bee reached the right hand edge of the screen?\nif (spriteBee.getPosition().x < -100)\n{\n    // Set it up ready to be a whole new bee next frame\n    beeActive = false;\n}\n```\n\n该代码在每一帧中（当`beeActive`为`true`时）测试蜜蜂是否从屏幕左侧消失。如果`getPosition`函数返回的值小于-100，则肯定会在玩家的视野之外。当这种情况发生时，`beeActive`被设置为`false`，在下一帧，一只“新”蜜蜂将被设置为以新的随机高度和新的随机速度飞行。\n\n试着运行游戏，看着我们的蜜蜂尽职尽责地从右向左飞，然后以新的高度和速度再次回到右侧。每次都像一只新蜜蜂。\n\n提示\n\n当然，一只真正的蜜蜂会在你试图集中精力砍树时纠缠你很久。我们将在以后的项目中制作一些更聪明的游戏角色。\n\n现在，我们将让云以一种非常相似的方式移动。\n\n## 吹云\n\n我们要做的第一件事是在一定的高度和速度设置第一个云。我们只希望在云处于非活动状态时执行此操作。因此，我们将把下面的代码包装在一个`if`块中。在我们为蜜蜂添加的代码之后，检查并添加以下突出显示的代码，然后我们将讨论它。这与我们在 bee 上使用的代码几乎相同：\n\n```cpp\nelse\n// Move the bee\n{\n    spriteBee.setPosition(\n        spriteBee.getPosition().x - \n         (beeSpeed * dt.asSeconds()),\n        spriteBee.getPosition().y);\n    // Has the bee reached the right hand edge of the screen?\n    if (spriteBee.getPosition().x < -100)\n    {\n        // Set it up ready to be a whole new bee next frame\n        beeActive = false;\n    }\n}\n// Manage the clouds\n// Cloud 1\nif (!cloud1Active)\n{\n    // How fast is the cloud\n    srand((int)time(0) * 10);\n    cloud1Speed = (rand() % 200);\n    // How high is the cloud\n    srand((int)time(0) * 10);\n    float height = (rand() % 150);\n    spriteCloud1.setPosition(-200, height);\n    cloud1Active = true;\n}\n/*\n****************************************\nDraw the scene\n****************************************\n*/\n```\n\n我们刚刚添加的代码与蜜蜂相关代码之间的唯一区别是，我们使用不同的精灵，并对随机数使用不同的范围。此外，我们将`time(0)`返回的结果乘以 10（`* 10`），因此我们总是保证为每个云获得不同的种子。当我们接下来对另一个云运动进行编码时，您将看到我们分别使用了`* 20`和`* 30`。\n\n现在，我们可以在云活动时采取行动。我们将在`else`块中这样做。与`if`块一样，该代码与 bee 相关代码相同，只是所有代码都在云上工作，而不是在 bee 上工作：\n\n```cpp\n// Manage the clouds\nif (!cloud1Active)\n{\n    // How fast is the cloud\n    srand((int)time(0) * 10);\n    cloud1Speed = (rand() % 200);\n    // How high is the cloud\n    srand((int)time(0) * 10);\n    float height = (rand() % 150);\n    spriteCloud1.setPosition(-200, height);\n    cloud1Active = true;\n}\nelse\n{\n    spriteCloud1.setPosition(\nspriteCloud1.getPosition().x + \n        (cloud1Speed * dt.asSeconds()),\n        spriteCloud1.getPosition().y);\n    // Has the cloud reached the right hand edge of the screen?\n    if (spriteCloud1.getPosition().x > 1920)\n    {\n        // Set it up ready to be a whole new cloud next frame\n        cloud1Active = false;\n    }\n}\n/*\n****************************************\nDraw the scene\n****************************************\n*/\n```\n\n现在我们知道该做什么了，我们可以为第二个和第三个云复制相同的代码。在第一个云的代码之后添加以下高亮显示的代码，该代码处理第二个和第三个云：\n\n```cpp\n...\n// Cloud 2\nif (!cloud2Active)\n{\n    // How fast is the cloud\n    srand((int)time(0) * 20);\n    cloud2Speed = (rand() % 200);\n    // How high is the cloud\n    srand((int)time(0) * 20);\n    float height = (rand() % 300) - 150;\n    spriteCloud2.setPosition(-200, height);\n    cloud2Active = true;\n}\nelse\n{\n    spriteCloud2.setPosition(\nspriteCloud2.getPosition().x + \n         (cloud2Speed * dt.asSeconds()),\n        spriteCloud2.getPosition().y);\n    // Has the cloud reached the right hand edge of the screen?\n    if (spriteCloud2.getPosition().x > 1920)\n    {\n        // Set it up ready to be a whole new cloud next frame\n        cloud2Active = false;\n    }\n}\nif (!cloud3Active)\n{\n    // How fast is the cloud\n    srand((int)time(0) * 30);\n    cloud3Speed = (rand() % 200);\n    // How high is the cloud\n    srand((int)time(0) * 30);\n    float height = (rand() % 450) - 150;\n    spriteCloud3.setPosition(-200, height);\n    cloud3Active = true;\n}\nelse\n{\n    spriteCloud3.setPosition(\nspriteCloud3.getPosition().x + \n        (cloud3Speed * dt.asSeconds()),\n        spriteCloud3.getPosition().y);\n    // Has the cloud reached the right hand edge of the screen?\n    if (spriteCloud3.getPosition().x > 1920)\n    {\n        // Set it up ready to be a whole new cloud next frame\n        cloud3Active = false;\n    }\n}\n/*\n****************************************\nDraw the scene\n****************************************\n*/\n```\n\n现在，你可以运行游戏，云彩将随机地、连续地飘过屏幕。蜜蜂也会从右到左嗡嗡叫，然后再次从右侧重生。以下屏幕截图显示了我们在本章中取得的成就：\n\n![](img/B14278_02_02.jpg)\n\n提示\n\n所有这些云和蜜蜂的处理看起来有点重复吗？我们将看到如何节省大量的打字，使我们的代码更可读，因为在 C++ 中，有多种方法处理同一类型的变量或对象。其中一种方法叫做**数组**，我们将在[*第 4 章*](04.html#_idTextAnchor110)*中学习它们，循环、数组、开关、枚举和函数——实现游戏机制*。在本项目结束时，一旦我们了解了阵列，我们将讨论如何改进云代码。\n\n请看一些与本章主题相关的常见问题。\n\n# 总结\n\n在本章中，我们了解到变量是内存中的命名存储位置，我们可以在其中保存特定类型的值。类型包括`int`、`float`、`double`、`bool`、`String`和`char`。\n\n我们可以声明和初始化我们需要为游戏存储数据的所有变量。一旦我们有了变量，我们就可以使用算术运算符和赋值运算符来操作它们，也可以使用逻辑运算符在测试中使用它们。与`if`和`else`关键字结合使用，我们可以根据游戏中的当前情况对代码进行分支。\n\n利用所有这些新知识，我们制作了一些云和一只蜜蜂的动画。在下一章中，我们将更多地使用这些技能来添加**平视显示器**（**HUD**），并为玩家添加更多的输入选项，以及使用时间条直观地表示时间。\n\n# 常见问题\n\nQ） 当蜜蜂达到-100 时，我们为什么要将其设置为非活动状态？既然零是窗户的左边，为什么不干脆零呢？\n\nA） 蜜蜂图形的宽度为 60 像素，其原点位于左上角的像素处。因此，当蜜蜂的原点为 x 等于零时，整个蜜蜂图形仍在屏幕上供玩家查看。等到-100 时，我们可以确定它不在玩家的视野之内。\n\nQ） 我如何知道我的游戏循环有多快？\n\nA） 如果您有现代 NVIDIA 图形卡，您可能已经能够通过配置 GeForce Experience overlay 来显示帧速率。然而，为了使用我们自己的代码明确地度量这一点，我们需要学习更多的东西。我们将在[*第 5 章*](05.html#_idTextAnchor138)*中添加测量和显示当前帧速率的功能，碰撞、声音和结束条件–使游戏可玩*。"
  },
  {
    "path": "docs/begin-cpp-game-prog/03.md",
    "content": "# 三、C++ 字符串和 SFML 时间——玩家输入和 HUD\n\n在本章中，我们将继续介绍木材！！游戏我们将在本章中花大约一半的时间学习如何操作文本并在屏幕上显示，另一半时间学习计时以及视觉时间条如何告知玩家剩余时间并在游戏中产生紧迫感。\n\n我们将讨论以下主题：\n\n*   暂停并重新启动游戏\n*   C++ 字符串\n*   SFML 文本和 SFML 字体类\n*   为木材添加 HUD！！！\n*   为木材添加时间条！！！\n\n# 暂停并重新启动游戏\n\n当我们在接下来的三章中研究这个游戏时，代码显然会越来越长。因此，现在似乎是一个提前思考并为代码添加更多结构的好时机。我们将添加此结构，以便暂停并重新启动游戏。\n\n我们将添加代码，这样当游戏第一次运行时，它将处于暂停状态。然后，玩家可以按*回车*键开始游戏。然后，游戏将一直运行，直到玩家被压扁或时间用完为止。此时，游戏将暂停，等待玩家按下*回车*，以便重新开始游戏。\n\n让我们一步一步地设置它。\n\n首先，在主游戏循环外声明一个名为`paused`的新`bool`变量，并将其初始化为`true`：\n\n```cpp\n// Variables to control time itself\nClock clock;\n// Track whether the game is running\nbool paused = true;\nwhile (window.isOpen())\n{\n    /*\n    ****************************************\n    Handle the players input\n    ****************************************\n    */\n```\n\n现在，每当游戏运行时，我们都有一个`paused`变量，它将是`true`。\n\n接下来，我们将添加另一个`if`语句，其中表达式将检查*Enter*键当前是否被按下。如果正在按下，则将`paused`设置为`false`。在其他键盘处理代码之后添加以下突出显示的代码：\n\n```cpp\n/*\n****************************************\nHandle the players input\n****************************************\n*/\nif (Keyboard::isKeyPressed(Keyboard::Escape))\n{\n    window.close();\n}\n// Start the game\nif (Keyboard::isKeyPressed(Keyboard::Return))\n{\n paused = false; \n}\n/*\n****************************************\nUpdate the scene\n****************************************\n*/\n```\n\n现在，我们有一个名为`paused`的`bool`，它从`true`开始，但在玩家按下*回车*键时变为`false`。此时，我们必须根据`paused`的当前值做出适当的游戏循环响应。\n\n我们将这样做。我们将把代码的整个更新部分，包括我们在上一章中为移动蜜蜂和云编写的代码，包装在一个`if`语句中。\n\n在下面的代码中，请注意，`if`块只有在`paused`不等于`true`时才会执行。换句话说，游戏暂停时不会移动/更新。\n\n这正是我们想要的。仔细看看我们添加新的`if`语句及其相应的开始和结束花括号`{...}`的地方。如果把它们放错地方，事情就不会像预期的那样进行。\n\n添加以下突出显示的代码以包装代码的更新部分，并密切注意下面的上下文。我在几行上添加了`...`来表示隐藏代码。明显地不是真实的代码，不应该添加到游戏中。您可以通过其周围未高亮显示的代码来确定新代码（高亮显示）在起点和终点的放置位置：\n\n```cpp\n/*\n****************************************\nUpdate the scene\n****************************************\n*/\nif (!paused)\n{\n    // Measure time\n                ...\n        ...\n        ...\n\n        // Has the cloud reached the right hand edge of the screen?\n        if (spriteCloud3.getPosition().x > 1920)\n        {\n            // Set it up ready to be a whole new cloud next frame\n            cloud3Active = false;\n        }\n    }\n} // End if(!paused)\n/*\n****************************************\nDraw the scene\n****************************************\n*/\n```\n\n请注意，当您放置新`if`块的右大括号时，VisualStudio 会整齐地调整所有缩进以保持代码整洁。\n\n现在，您可以运行游戏，在按下*回车*键之前，一切都将是静态的。现在可以开始为我们的游戏添加功能了。我们只需要记住，当玩家死亡或时间用完时，我们需要将`paused`设置为`true`。\n\n在前一章中，我们首先研究 C++ 字符串。我们需要更多地了解它们，以便实现玩家的 HUD。\n\n# C++ 字符串\n\n在上一章中，我们简要地提到了字符串，并了解到字符串可以保存字母数字数据——从单个字符到整本书。我们没有考虑声明、初始化或操作字符串，所以现在让我们这样做。\n\n## 声明字符串\n\n声明字符串变量很简单。这与我们在前一章中用于其他变量的过程相同：我们声明类型，后跟名称：\n\n```cpp\nString levelName;\nString playerName;\n```\n\n一旦我们声明了一个字符串，我们就可以给它赋值。\n\n## 给字符串赋值\n\n要为字符串赋值，就像常规变量一样，我们只需输入名称，后跟赋值运算符，然后输入值：\n\n```cpp\nlevelName = \"DastardlyCave\";\nplayerName = \"John Carmack\";\n```\n\n请注意，这些值需要用引号括起来。与常规变量一样，我们也可以在一行中声明和赋值：\n\n```cpp\nString score = \"Score = 0\";\nString message = \"GAME OVER!!\";\n```\n\n在下一节中，我们将看到如何更改字符串变量的值。\n\n## 操纵弦\n\n我们可以使用`#include <sstream>`指令为字符串提供一些额外的操作选项。`sstream`类允许我们一起“添加”一些字符串。当我们一起添加字符串时，这被称为**串联**：\n\n```cpp\nString part1 = \"Hello \";\nString part2 = \"World\";\nsstream ss;\nss<< part1 << part2;\n// ss now holds \"Hello World\"\n```\n\n此外，通过使用`sstream`对象，字符串变量甚至可以与不同类型的变量连接。以下代码开始揭示字符串如何对我们有用：\n\n```cpp\nString scoreText = \"Score = \";\nint score = 0;\n// Later in the code\nscore ++ ;\nsstream ss;\nss<<scoreText<< score;\n// ss now holds \"Score = 1\"\n```\n\n在前面的代码中，`ss`用于将`scoreText`的内容与`score`的值连接起来。注意，尽管 score 持有一个`int`值，`ss`持有的最终值仍然是一个持有等价值的字符串；在本例中，“1”。\n\n提示\n\n`<<`运算符是位运算符之一。但是，C++ 允许您编写自己的类并覆盖特定操作符在类的上下文中所做的操作。`sstream`类这样做是为了让`<<`操作符按照它的方式工作 rk。复杂性隐藏在类中。我们可以使用它的功能，而不用担心它是如何工作的。如果您有冒险精神，可以在[上阅读有关操作员过载的信息 http://www.tutorialspoint.com/cplusplus/cpp_overloading.htm](http://www.tutorialspoint.com/cplusplus/cpp_overloading.htm) 。你不需要更多的信息来继续这个项目。\n\n现在我们知道了 C++ 字符串的基本原理以及如何使用 AutoT0T，我们将研究如何使用一些 SFML 类来在屏幕上显示它们。\n\n# SFML 的文本和字体类\n\n在我们继续将代码添加到游戏之前，让我们先讨论一下使用一些假设代码的`Text`和`Font`类。\n\n能够在屏幕上绘制文本的第一步是使用字体。在 T1 T1 中，第 1 章 AUTT3，Ty4T4，Ont5，C++，SFML，VisualStudio，并开始第一个游戏 To.T6.，我们在项目文件夹中添加了一个字体文件。现在，我们可以将字体加载到 SFML`Font`对象中，这样它就可以使用了。\n\n执行此操作的代码如下所示：\n\n```cpp\nFont font;\nfont.loadFromFile(\"myfont.ttf\");\n```\n\n在前面的代码中，我们首先声明`Font`对象，然后加载一个实际的字体文件。请注意，`myfont.ttf`是一种假设字体，我们可以使用项目文件夹中的任何字体。\n\n加载字体后，我们需要一个 SFML`Text`对象：\n\n```cpp\nText myText;\n```\n\n现在，我们可以配置我们的`Text`对象。这包括大小、颜色、屏幕上的位置、保存消息的字符串，当然还有将消息与`font`对象关联的行为：\n\n```cpp\n// Assign the actual message\nmyText.setString(\"Press Enter to start!\");\n// assign a size\nmyText.setCharacterSize(75);\n// Choose a color\nmyText.setFillColor(Color::White);\n// Set the font to our Text object\nmyText.setFont(font);\n```\n\n现在，我们可以创建和操作字符串值以及分配、声明和初始化 SFML`Text`对象，我们可以进入下一节，在这里我们将向木材添加 HUD！！！\n\n# 实施 HUD\n\n现在，我们已经对字符串、SFML`Text`和 SFML`Font`有了足够的了解，可以开始实现 HUD 了。**HUD**代表**平视显示器**。它可以像屏幕上的分数和文字信息一样简单，也可以包括更复杂的元素，如表示玩家角色所面对方向的时间条、迷你地图或指南针。\n\n要开始使用 HUD，我们需要在代码文件的顶部添加另一个`#include`指令，以添加对`sstream`类的访问。正如我们已经知道的，`sstream`类添加了一些非常有用的功能，用于将字符串和其他变量类型组合到字符串中。\n\n添加以下突出显示的代码行：\n\n```cpp\n#include <sstream>\n#include <SFML/Graphics.hpp>\nusing namespace sf;\nint main()\n{\n```\n\n接下来，我们将设置我们的 SFML`Text`对象：一个用来保存消息，我们将根据游戏的状态进行更改，另一个用来保存分数并需要定期更新。\n\n代码声明`Text`和`Font`对象，加载字体，将字体分配给`Text`对象，然后添加字符串消息、颜色和大小。从上一节的讨论来看，这应该很熟悉。此外，我们还添加了一个名为`score`的新`int`变量，我们可以对其进行操作，以便它保存玩家的分数。\n\n提示\n\n请记住，如果您选择了一个不同的字体从 AutoT0T，返回到 To.T3A.Ty4T.第 1 章 AutoT5，AutoT6A. Ty7T7，C++，SFML，VisualStudio，并开始第一个游戏 Ty8T8，您需要改变代码的那部分，以匹配在 Type T2AY 文件夹中的 OutT1 文件。\n\n通过添加以下突出显示的代码，我们将准备继续更新 HUD：\n\n```cpp\n// Track whether the game is running\nbool paused = true;\n// Draw some text\nint score = 0;\nText messageText;\nText scoreText;\n// We need to choose a font\nFont font;\nfont.loadFromFile(\"fonts/KOMIKAP_.ttf\");\n// Set the font to our message\nmessageText.setFont(font);\nscoreText.setFont(font);\n// Assign the actual message\nmessageText.setString(\"Press Enter to start!\");\nscoreText.setString(\"Score = 0\");\n// Make it really big\nmessageText.setCharacterSize(75);\nscoreText.setCharacterSize(100);\n// Choose a color\nmessageText.setFillColor(Color::White);\nscoreText.setFillColor(Color::White);\nwhile (window.isOpen())\n{\n    /*\n    ****************************************\n    Handle the players input\n    ****************************************\n    */\n```\n\n在上述代码中，我们实现了以下目标：\n\n*   声明一个变量来保存分数\n*   声明了一些 SFML`Text`和`Font`对象\n*   通过从文件加载字体初始化`Font`对象\n*   使用字体和一些字符串初始化`Text`对象\n*   使用`setCharacterSize`和`setFillColor`功能设置`Text`对象的大小和颜色\n\n下面的代码片段可能看起来有点复杂。然而，当你把它分解一点的时候，它是很简单的。检查并添加新突出显示的代码。我们将在这之后进行讨论：\n\n```cpp\n// Choose a color\nmessageText.setFillColor(Color::White);\nscoreText.setFillColor(Color::White);\n// Position the text\nFloatRect textRect = messageText.getLocalBounds();\nmessageText.setOrigin(textRect.left +\n textRect.width / 2.0f,\n textRect.top +\n textRect.height / 2.0f);\nmessageText.setPosition(1920 / 2.0f,\t1080 / 2.0f);\nscoreText.setPosition(20, 20);\nwhile (window.isOpen())\n{\n    /*\n    ****************************************\n    Handle the players input\n    ****************************************\n    */\n```\n\n我们将在屏幕上显示两个`Text`类型的对象。我们想用一点填充物将`scoreText`放置在左上角。这不是挑战；我们只需使用`scoreText.setPosition(20, 20)`，将其定位在左上角，水平和垂直填充为 20 像素。\n\n然而，定位`messageText`并不那么容易。我们想把它放在屏幕的正中间。起初，这似乎不是问题，但我们必须记住，我们绘制的所有内容的原点都在左上角。因此，如果我们简单地将屏幕的宽度和高度除以 2，并在`mesageText.setPosition...`中使用结果，那么文本的左上角将位于屏幕的中心，它将不整齐地向右侧展开。\n\n以下是为方便起见再次讨论的代码：\n\n```cpp\n// Position the text\nFloatRect textRect = messageText.getLocalBounds();\nmessageText.setOrigin(textRect.left +\n    textRect.width / 2.0f,\n    textRect.top +\n    textRect.height / 2.0f);\n```\n\n代码所做的是将`messageText`的*中心*设置到屏幕的中心。我们正在回顾的代码中相当复杂的部分将`messageText`的起源重新定位到了自身的中心。\n\n在前面的代码中，我们首先声明一个名为`textRect`的`FloatRect`类型的新对象。顾名思义，`FloatRect`对象持有一个带有浮点坐标的矩形。\n\n然后，代码使用`mesageText.getLocalBounds`函数以包裹`messageText`的矩形的坐标初始化`textRect`。\n\n下一行代码由于相当长而分散在四行上，使用`messageText.setOrigin`函数将原点（用于绘制的点）更改为`textRect`的中心。当然，`textRect`保存一个与包裹`messageText`的坐标相匹配的矩形。然后，执行以下代码行：\n\n```cpp\nmessageText.setPosition(1920 / 2.0f,\t1080 / 2.0f);\n```\n\n现在，`messageText`将整齐地定位在屏幕的正中央。每次更改`messageText`的文本时，我们都会使用此代码，因为更改消息会更改`messageText`的大小，因此其来源将需要重新计算。\n\n接下来，我们声明一个名为`ss`的`stringstream`类型的对象。注意，我们使用全名，包括名称空间，即`std::stringstream`。我们可以通过在代码文件的顶部添加`using namespace std`来避免这种语法。不过，我们不打算在这里讨论，因为我们很少使用它。看看下面的代码，并将其添加到游戏中；然后，我们可以更详细地了解它。由于我们只希望在游戏未暂停时执行此代码，请确保将其与其他代码一起添加到`if(!paused)`块中，如下所示：\n\n```cpp\nelse\n    {\n        spriteCloud3.setPosition(\n            spriteCloud3.getPosition().x +\n            (cloud3Speed * dt.asSeconds()),\n            spriteCloud3.getPosition().y);\n        // Has the cloud reached the right hand edge of the screen?\n        if (spriteCloud3.getPosition().x > 1920)\n        {\n            // Set it up ready to be a whole new cloud next frame\n            cloud3Active = false;\n        }\n    }\n // Update the score text\n std::stringstream ss;\n ss<< \"Score = \" << score;\n scoreText.setString(ss.str());\n}// End if(!paused)\n/*\n****************************************\nDraw the scene\n****************************************\n*/\n```\n\n我们使用`ss`和`<<`操作符提供的特殊功能，将变量连接成`stringstream`。在这里，`ss << \"Score = \" << score`具有使用`\"Score = \"`创建字符串的效果。无论`score`的值是什么，都会连接在一起。例如，当游戏第一次开始时，`score`等于零，因此`ss`将保留`\"Score = 0\"`值。如果`score`发生变化，`ss`将调整每个帧。\n\n下面的代码行只是将`ss`中包含的字符串设置为`scoreText`：\n\n```cpp\nscoreText.setString(ss.str());\n```\n\n现在可以将其绘制到屏幕上了。\n\n下面的代码同时绘制了`Text`对象（`scoreText`和`messageText`，但是绘制`messageText`的代码被包装在`if`语句中。此`if`语句导致`messageText` 仅在游戏暂停时绘制。\n\n添加以下突出显示的代码：\n\n```cpp\n// Now draw the insect\nwindow.draw(spriteBee);\n// Draw the score\nwindow.draw(scoreText);\nif (paused)\n{\n // Draw our message\n window.draw(messageText);\n}\n// Show everything we just drew\nwindow.display();\n```\n\n我们现在可以运行游戏，并看到我们的 HUD 正在屏幕上绘制。您将看到**分数=0**和**按 ENTER 键启动**消息。当您按*进入*时，后者将消失：\n\n![](img/B14278_03_01.jpg)\n\n如果您想看到分数更新，请在`while(window.isOpen)`循环中的任意位置添加一行临时代码`score ++ ;`。如果你加上这条临时线，你会看到分数上升得很快，非常快！\n\n如果您添加了临时代码，即`score ++ ;`，请确保在继续之前将其删除。\n\n# 增加时间条\n\n由于时间是游戏中的一个关键机制，因此有必要让玩家意识到它。他们需要知道分配给他们的六秒钟是否即将用完。当比赛接近尾声时，这会给他们一种紧迫感，如果他们表现得足够好，能够维持或增加剩余时间，就会给他们一种成就感。\n\n在屏幕上绘制剩余的秒数不容易阅读（当专注于树枝时），也不是实现目标的特别有趣的方法。\n\n我们需要的是时间条。我们的时间条将是一个简单的红色矩形，突出显示在屏幕上。它将开始很好和广泛，但随着时间的推移，迅速缩小。当玩家的剩余时间为零时，时间条将完全消失。\n\n在添加时间条的同时，我们将添加必要的代码来跟踪玩家的剩余时间，并在时间用完时做出响应。让我们一步一步地看一遍。\n\n找到前面的`Clock clock;`声明，并在其后面添加突出显示的代码，如下所示：\n\n```cpp\n// Variables to control time itself\nClock clock;\n// Time bar\nRectangleShape timeBar;\nfloat timeBarStartWidth = 400;\nfloat timeBarHeight = 80;\ntimeBar.setSize(Vector2f(timeBarStartWidth, timeBarHeight));\ntimeBar.setFillColor(Color::Red);\ntimeBar.setPosition((1920 / 2) - timeBarStartWidth / 2, 980);\nTime gameTimeTotal;\nfloat timeRemaining = 6.0f;\nfloat timeBarWidthPerSecond = timeBarStartWidth / timeRemaining;\n// Track whether the game is running\nbool paused = true;\n```\n\n首先，我们声明一个`RectangleShape`类型的对象，并将其命名为`timeBar`。`RectagleShape`是一个 SFML 类，非常适合绘制简单的矩形。\n\n接下来，我们将添加几个`float`变量`timeBarStartWidth`和`timeBarHeight`。我们分别将它们初始化为`400`和`80`。这些变量将帮助我们跟踪在每帧绘制`timeBar`所需的大小。\n\n接下来，我们使用`timeBar.setSize`函数设置`timeBar`的大小。我们不只是传递两个新的`float`变量。首先，我们创建一个`Vector2f`类型的新对象。然而，这里的不同之处在于，我们没有给新对象命名。相反，我们只需使用两个浮点变量初始化它，并将其直接传递给`setSize`函数。\n\n提示\n\n`Vector2f`是一个包含两个`float`变量的类。它还有一些其他功能，将在本书中介绍。\n\n之后，我们使用`setFillColor`功能将`timeBar`涂成红色。\n\n在前面的代码中，我们对`timeBar`所做的最后一件事是设置其位置。垂直坐标是完全直接的，但是我们设置水平坐标的方式有点复杂。计算结果如下：\n\n```cpp\n(1920 / 2) - timeBarStartWidth / 2\n```\n\n首先，法典将 1920 除以 2。然后，它将`timeBarStartWidth`除以 2。最后，它从前者中减去后者。\n\n结果是`timeBar`以水平的方式整齐地坐在屏幕中央。\n\n我们讨论的最后三行代码声明了一个名为`gameTimeTotal`的新`Time`对象，一个名为`timeRemaining`的新`float`对象，该对象被初始化为`6`，以及一个名为`timeBarWidthPerSecond`的奇怪声音`float`，我们将在下一步讨论。\n\n用`timeBarStartWidth`除以`timeRemaining`初始化`timeBarWidthPerSecond`变量。结果就是游戏中每秒钟`timeBar`需要缩小的像素数量。当我们在每一帧中调整`timeBar`的大小时，这将非常有用。\n\n显然，每次玩家开始新游戏时，我们都需要重置剩余时间。这样做的逻辑位置是按下*回车*键。我们也可以同时将`score`设置回零。现在让我们通过添加以下突出显示的代码来实现这一点\n\n```cpp\n// Start the game\nif (Keyboard::isKeyPressed(Keyboard::Return))\n{\n    paused = false;\n // Reset the time and the score\n score = 0;\n timeRemaining = 6;\n}\n```\n\n现在，我们必须减少每一帧的剩余时间，并相应地调整`timeBar`的大小。将以下突出显示的代码添加到更新部分，如下所示：\n\n```cpp\n/*\n****************************************\nUpdate the scene\n****************************************\n*/\nif (!paused)\n{\n    // Measure time\n    Time dt = clock.restart();\n // Subtract from the amount of time remaining\n timeRemaining -= dt.asSeconds();\n // size up the time bar\n timeBar.setSize(Vector2f(timeBarWidthPerSecond *\n timeRemaining, timeBarHeight));\n    // Set up the bee\n    if (!beeActive)\n    {\n        // How fast is the bee\n        srand((int)time(0) * 10);\n        beeSpeed = (rand() % 200) + 200;\n        // How high is the bee\n        srand((int)time(0) * 10);\n        float height = (rand() % 1350) + 500;\n        spriteBee.setPosition(2000, height);\n        beeActive = true;\n    }\n    else\n        // Move the bee\n```\n\n首先，我们用以下代码减去玩家剩余的时间，减去前一帧执行所需的时间：\n\n```cpp\ntimeRemaining -= dt.asSeconds();\n```\n\n然后，我们用以下代码调整了`timeBar`的大小：\n\n```cpp\ntimeBar.setSize(Vector2f(timeBarWidthPerSecond *\n        timeRemaining, timeBarHeight));\n```\n\n当乘以`timeRemaining`时，`Vector2F`的 x 值初始化为`timebarWidthPerSecond`。这就产生了正确的宽度，相对于玩家的剩余时间。高度保持不变，使用`timeBarHeight`时无需任何操作。\n\n当然，我们必须检测时间何时已过。现在，我们只需检测时间已过，暂停游戏，并更改`messageText`的文本。稍后，我们将在这里做更多的工作。在前面添加的代码之后添加以下突出显示的代码。我们将在后面详细介绍：\n\n```cpp\n// Measure time\nTime dt = clock.restart();\n// Subtract from the amount of time remaining\ntimeRemaining -= dt.asSeconds();\n// resize up the time bar\ntimeBar.setSize(Vector2f(timeBarWidthPerSecond *\n    timeRemaining, timeBarHeight));\nif (timeRemaining<= 0.0f) {\n\n // Pause the game\n paused = true;\n // Change the message shown to the player\n messageText.setString(\"Out of time!!\");\n //Reposition the text based on its new size\n FloatRect textRect = messageText.getLocalBounds();\n messageText.setOrigin(textRect.left +\n textRect.width / 2.0f,\n textRect.top +\n textRect.height / 2.0f);\n messageText.setPosition(1920 / 2.0f, 1080 / 2.0f);\n}\n// Set up the bee\nif (!beeActive)\n{\n    // How fast is the bee\n    srand((int)time(0) * 10);\n    beeSpeed = (rand() % 200) + 200;\n    // How high is the bee\n    srand((int)time(0) * 10);\n    float height = (rand() % 1350) + 500;\n    spriteBee.setPosition(2000, height);\n    beeActive = true;\n}\nelse\n    // Move the bee\n```\n\n让我们逐步浏览前面的代码：\n\n1.  首先，我们使用`if(timeRemaining<= 0.0f).`测试时间是否已用完\n2.  然后，我们将`paused`设置为`true`，因此这将是最后一次执行代码的更新部分（直到玩家再次按下*Enter*。\n3.  然后，我们更改`messageText`的消息，计算其新中心以设置为原点，并将其定位在屏幕中心。\n\n最后，对于这部分代码，我们需要绘制`timeBar`。这段代码中没有我们以前见过很多次的新内容。请注意，我们在树后绘制`timeBar`，这样它就不会被部分遮挡。添加以下高亮显示的代码以绘制时间条：\n\n```cpp\n// Draw the score\nwindow.draw(scoreText);\n// Draw the timebar\nwindow.draw(timeBar);\nif (paused)\n{\n    // Draw our message\n    window.draw(messageText);\n}\n// Show everything we just drew\nwindow.display();\n```\n\n现在，您可以运行游戏，按*回车*键开始游戏，然后看着时间条平滑地消失为零：\n\n![](img/Image85176.jpg)\n\n然后游戏暂停，**超时！！**信息将出现在屏幕中央：\n\n![](img/B14278_03_04.jpg)\n\n当然，您可以按*回车*再次开始游戏，并观看游戏从头开始运行。\n\n# 总结\n\n在本章中，我们学习了字符串、SFML`Text`和 SFML`Font`。在他们之间，他们允许我们在屏幕上绘制文本，这为玩家提供了一个 HUD。我们还使用了`sstream`，它允许我们连接字符串和其他变量来显示分数。\n\n我们还研究了 SFML`RectangleShape`类，它完全按照其名称进行操作。我们使用了一个`RectangleShape`类型的对象和一些精心规划的变量来绘制一个时间条，直观地向玩家显示他们还剩多少时间。一旦我们实施了可以压扁玩家的切碎和移动分支，时间条将提供视觉反馈，从而产生紧张和紧迫感。\n\n在下一章，我们将学习一系列新的 C++ 特性，包括循环、数组、切换、枚举和函数。这将允许我们移动树枝，跟踪它们的位置，并挤压玩家。\n\n# 常见问题\n\nQ） 我可以预见，将精灵放在左上角有时会很不方便。还有别的选择吗？\n\nA） 幸运的是，您可以选择精灵的哪个点用作定位/原点像素，就像我们使用`messageText`一样，使用`setOrigin`功能。\n\nQ） 代码越来越长，我正努力跟踪所有东西的位置。我们如何解决这个问题？\n\nA） 是的，我同意。在下一章中，我们将介绍几种可以组织代码并使其更具可读性的方法中的第一种。当我们学习 C++ 函数时，我们将对此进行研究。另外，当我们学习 C++ 数组时，我们将学习一种新的方法来处理同一类型的多个对象/变量（如云）。"
  },
  {
    "path": "docs/begin-cpp-game-prog/04.md",
    "content": "# 四、循环、数组、`switch`、枚举和函数——实现游戏机制\n\n这一章可能比这本书中的其他章节有更多的 C++ 信息。它充满了将极大地推动我们理解的基本概念。它还将开始揭示一些我们略过的模糊领域，比如函数和游戏循环。\n\n一旦我们探索了一个完整的 C++ 语言必需品列表，我们将使用我们所知道的一切，使主要的游戏机制，树枝移动。到本章结束时，我们将为最后阶段和木材完工做好准备！！！。\n\n在本章中，我们将介绍以下主题：\n\n*   循环\n*   阵列\n*   与`switch`一起做决策\n*   枚举\n*   函数入门\n*   创建和移动树枝\n\n# 回路\n\n在编程中，我们经常需要不止一次地做同样的事情。到目前为止，我们看到的一个明显的例子是游戏循环。去掉所有代码后，我们的游戏循环如下所示：\n\n```cpp\nwhile (window.isOpen())\n{\n}\n```\n\n有几种不同类型的循环，我们将在这里介绍最常用的循环。这种类型的循环的正确术语是**while**循环。\n\n## while 循环\n\n`while`循环非常简单。回想一下`if`语句及其计算为`true`或`false`的表达式。我们可以在`while`循环的条件表达式中使用完全相同的运算符和变量组合。\n\n与`if`语句类似，如果表达式为`true`，则执行代码。然而，与 ToeT2 循环不同的是，它内的 C++ 代码将重复执行，直到条件为假。看看下面的代码。\n\n```cpp\nint numberOfZombies = 100;\nwhile(numberOfZombies > 0)\n{\n    // Player kills a zombie\n    numberOfZombies--;\n    // numberOfZombies decreases each pass through the loop\n}\n// numberOfZombies is no longer greater than 0\n```\n\n让我们回顾一下前面代码中发生的事情。在`while`循环之外，`int numberOfZombies`被声明并初始化为`100`。然后，`while`循环开始。其条件表达式为`numberOfZombies > 0`。因此，`while`循环将继续在其体内的代码中循环，直到条件评估为`false`。这意味着前面的代码将执行 100 次。\n\n在第一次通过循环时，`numberOfZombies`等于 100，然后是 99，然后是 98，依此类推。但一旦`numberOfZombies`等于零，它当然不再*大于*。然后，代码将跳出`while`循环，并在关闭花括号后继续运行。\n\n就像`if`语句一样，`while`循环可能不会执行一次。请看下面的代码：\n\n```cpp\nint availableCoins = 10;\nwhile(availableCoins > 10)\n{\n    // more code here.\n    // Won't run unless availableCoins is greater than 10\n}\n```\n\n`while`循环中的上述代码将不会执行，因为条件为 false。\n\n请注意，表达式的复杂性或循环体中可以包含的代码量没有限制。考虑以下假设我们游戏循环的变化：\n\n```cpp\nint playerLives = 3;\nint alienShips = 10;\nwhile(playerLives !=0 && alienShips !=0 )\n{\n    // Handle input\n    // Update the scene\n    // Draw the scene\n}\n// continue here when either playerLives or alienShips equals 0\n```\n\n上一个`while`循环将继续执行，直到`playerLives`或`alienShips`等于零。一旦出现其中一种情况，表达式将计算为`false`，程序将从`while`循环后的第一行代码继续执行。\n\n值得注意的是，一旦循环体被输入，它将始终至少完成一次，即使表达式在中途的计算结果为 false，因为在代码尝试开始另一个过程之前不会再次测试它。让我们来看一个例子：\n\n```cpp\nint x = 1;\nwhile(x > 0)\n{\n    x--;\n    // x is now 0 so the condition is false\n    // But this line still runs\n    // and this one\n    // and me!\n}\n// Now I'm done!\n```\n\n前一个循环体将执行一次。我们还可以建立一个永远运行的`while`循环，毫不奇怪，它被称为**无限循环**。以下是一个例子：\n\n```cpp\nint y = 0;\nwhile(true)\n{\n    y++ ; // Bigger... Bigger...\n}\n```\n\n如果您发现前面的循环令人困惑，请仔细想想。循环在其条件为`true`时执行。嗯，`true`始终是`true`，因此将继续执行。\n\n### 打破 while 循环\n\n我们可以使用一个无限循环，这样我们就可以决定何时从循环体而不是表达式中退出循环。当我们准备离开循环体时，我们可以使用**break**关键字来完成此操作，可能如下所示：\n\n```cpp\nint z = 0;\nwhile(true)\n{\n    z++ ; // Bigger... Bigger...\n    break; // No you're not\n\n    // Code doesn't reach here\n}\n```\n\n在前面的代码中，循环内的代码将执行一次，直到并包括`break`语句，然后在关闭`while`循环的花括号后继续执行。\n\n正如您可能已经猜到的那样，我们可以结合任何 C++ 决策工具，如 AutoT0}，AuthT1，以及另一个我们将在不久的时间内知道的，如我们的 To.t3t 循环和其他循环类型。考虑下面的例子：\n\n```cpp\nint x = 0;\nint max = 10;\nwhile(true)\n{\n    x++ ; // Bigger... Bigger...\n    if(x == max){\n        break;\n    } // No you're not\n    // code reaches here only until max = 10\n}\n```\n\n在前面的代码中，`if`条件决定是否以及何时执行`break`语句。在这种情况下，代码将继续循环，直到`max`达到 10。\n\n我们可以在很长一段时间里看 C++ 的各种排列，但是，在某些时候，我们想回到游戏中去。那么，让我们转到另一种类型的循环：`for`循环。\n\n## 用于循环\n\n循环的**语法比`while`循环稍微复杂一些，因为它需要三个部分来设置一个循环。首先看一下下面的代码。我们将在以下时间将其拆分：**\n\n```cpp\nfor(int x = 0; x < 100; x ++)\n{\n    // Something that needs to happen 100 times goes here\n}\n```\n\n以下是`for`循环条件的所有部分所做的：\n\n```cpp\nfor(declaration and initialization; condition; change before each iteration)\n```\n\n为了进一步澄清这一点，这里有一个表格来解释三个关键部分中的每一个，如前一个`for`循环示例中所示：\n\n![](img/B14278_03_1.jpg)\n\n我们可以改变`for`循环，这样它们可以做更多的事情。下面是另一个从 10 开始倒数的简单示例：\n\n```cpp\nfor(int i = 10; i > 0; i--)\n{\n    // countdown\n}\n// blast off\n```\n\n`for`回路控制初始化、条件评估和控制变量。我们将在本章后面的游戏中使用`for`循环。\n\n现在，我们可以继续讨论 C++ 数组的主题，它帮助我们存储大量相关数据。\n\n# 阵列\n\n如果一个变量是一个盒子，我们可以在其中存储特定类型的值，例如`int`、`float`或`char`，那么我们可以将数组视为一行盒子。框的行几乎可以是任何大小和类型，包括由类生成的对象。但是，所有箱子必须是同一类型的。\n\n提示\n\n一旦我们在倒数第二个项目中学习到一些更先进的 C++，就可以在每一个框中使用相同类型的限制。\n\n这个阵列听起来像是在[*第 2 章*](02.html#_idTextAnchor070)*中对我们的云有用，变量、操作符和决策–设置精灵动画*。那么，我们如何创建和使用数组呢？\n\n## 声明一个数组\n\n我们可以声明一个`int`类型变量数组，如下所示：\n\n```cpp\nint someInts[10];\n```\n\n现在，我们有一个名为`someInts`的数组，它可以存储十个`int`值。然而，目前它是空的。\n\n## 初始化数组元素\n\n要向数组元素添加值，我们可以使用我们已经熟悉的语法类型，同时引入一些新语法，称为**数组表示法**。在下面的代码中，我们将`99`的值存储在数组的第一个**元素**中：\n\n```cpp\nsomeInts[0] = 99;\n```\n\n为了在第二个元素中存储值 999，我们需要使用以下代码：\n\n```cpp\nsomeInts[1] = 999;\n```\n\n我们可以在最后一个元素中存储值 3，如下所示：\n\n```cpp\nsomeInts[9] = 3;\n```\n\n请注意，数组的元素总是从零开始，直到数组的大小减 1 为止。与普通变量类似，我们可以操作存储在数组中的值。唯一的区别是我们将使用数组表示法，因为尽管我们的数组有一个名称-`someInts`，但单个元素没有。\n\n在下面的代码中，我们将第一个和第二个元素添加在一起，并将答案存储在第三个元素中：\n\n```cpp\nsomeInts[2] = someInts[0] + someInts[1];\n```\n\n数组还可以与常规变量无缝交互，例如：\n\n```cpp\nint a = 9999;\nsomeInts[4] = a;\n```\n\n有更多的方法可以初始化数组，现在让我们看一种方法。\n\n### 快速初始化数组元素\n\n我们可以按如下方式快速向元素添加值。本例使用一个`float`数组：\n\n```cpp\nfloat myFloatingPointArray[3] {3.14f, 1.63f, 99.0f};\n```\n\n现在，`3.14`、`1.63`和`99.0`值分别存储在第一、第二和第三位置。请记住，当使用数组表示法访问这些值时，我们将使用[0]、[1]和[2]。\n\n还有其他初始化数组元素的方法。这个稍微抽象的示例显示了如何使用`for`循环将值 0 到 9 放入`uselessArray`数组：\n\n```cpp\nfor(int i = 0; i < 10; i++)\n{\n    uselessArray[i] = i;\n}\n```\n\n前面的代码假设`uslessArray`之前已初始化为至少包含 10 个`int`变量。\n\n但是为什么我们需要阵列呢？\n\n## 这些阵列对我们的游戏到底有什么作用？\n\n我们可以在任何可以使用正则变量的地方使用数组–可能在这样的表达式中：\n\n```cpp\n// someArray[4] is declared and initialized to 9999\nfor(int i = 0; i < someArray[4]; i++)\n{\n    // Loop executes 9999 times\n}\n```\n\n在游戏代码中数组的最大好处之一在本节的开头已经提到。数组可以保存对象（类的实例）。让我们想象一下，我们有一个`Zombie`类，我们想要存储一大堆。我们可以这样做：\n\n```cpp\nZombie horde [5] {zombie1, zombie2, zombie3}; // etc...\n```\n\n`horde`数组现在保存`Zombie` 类的实例负载。每一个都是一个独立的、有生命的、呼吸的、自我决定的`Zombie`对象。然后我们可以循环通过`horde`阵列，每个阵列都通过游戏循环，移动僵尸，检查他们的头是否碰到斧头，或者他们是否成功抓住了玩家。\n\n阵列，如果我们当时知道的话，在[*第 2 章*](02.html#_idTextAnchor070)*变量、操作符和决策中处理我们的云是完美的*精灵动画。我们可以拥有我们想要的尽可能多的云，并且编写的代码比我们为三个微不足道的云所做的要少。\n\n提示\n\n要完整地查看这段改进的云代码，请查看 Timber 的增强版！！！（代码和可玩游戏）在下载包中。或者，在查看代码之前，您可以尝试自己使用数组实现云。\n\n了解所有这些数组内容的最佳方法是查看它的运行情况。我们将在实现树分支时执行此操作。\n\n现在，我们将保留我们的云代码，以便我们能够尽快回到游戏中添加功能。但首先，让我们做更多的 C++ 决策。\n\n# 用开关做决策\n\n我们已经看过了`if`，它允许我们根据表达式的结果决定是否执行代码块。但有时，C++ 中的决策可以用其他更好的方法来实现。\n\n当我们必须根据不涉及复杂组合或广泛价值范围的可能结果的明确列表做出决策时，通常应该选择`switch`。我们可以开始`switch`决定如下：\n\n```cpp\nswitch(expression)\n{\n    // More code here\n}\n```\n\n在前面的示例中，`expression`可以是一个实际表达式，也可以只是一个变量。然后，在大括号内，我们可以根据表达式的结果或变量的值做出决策。我们使用`case`和`break`关键字来实现这一点：\n\n```cpp\ncase x:\n    //code for x\n    break;\n\ncase y:\n    //code for y\n    break;\n```\n\n如您所见，每个`case`表示一个可能的结果，每个`break`表示该`case`的结束以及执行离开`switch`块的点。\n\n或者，我们也可以使用不带值的`default`关键字来运行一些代码，以防`case`语句的计算结果都不是`true`，如下所示：\n\n```cpp\ndefault: // Look no value\n    // Do something here if no other case statements are true\n    break;\n```\n\n作为最后一个抽象的例子，考虑一个复古文本冒险，玩家输入一个字母如“N”、“E”、“S”或“W”，以移动北、East、南或西。一个`switch`块可用于处理玩家的每个可能输入：\n\n```cpp\n// get input from user in a char called command\nswitch(command){\n    case 'n':\n        // Handle move here\n        break;\n    case 'e':\n        // Handle move here\n        break;\n    case 's':\n        // Handle move here\n        break;\n    case 'w':\n        // Handle move here\n        break;\n\n    // more possible cases\n    default:\n        // Ask the player to try again\n        break;\n}\n```\n\n要理解我们所看到的关于`switch`的一切，最好的方法是将其付诸行动，以及我们正在学习的所有其他新概念。\n\n接下来，我们将学习在编写更多代码之前需要了解的另一个 C++ 概念。让我们看看类枚举。\n\n# 类枚举\n\n**枚举**是逻辑集合中所有可能值的列表。C++ 枚举是一种很好的枚举事物的方法。例如，如果我们的游戏使用的变量只能在特定的值范围内，并且这些值在逻辑上可以形成一个集合或集合，那么枚举可能适合使用。它们将使您的代码更清晰，更不容易出错。\n\n为了在 C++ 中声明一个类枚举，我们可以使用这两个关键字，OUTT0}，加上枚举的名称，后面是枚举可以包含的值，用一对括号括起来。\n\n例如，检查以下枚举声明。请注意，按照惯例，枚举中的可能值应以大写形式声明：\n\n```cpp\nenum class zombieTypes {\n   REGULAR, RUNNER, \n   CRAWLER, SPITTER, BLOATER \n};\n```\n\n注意，在这里，我们没有声明任何`zombieType`的实例，只声明了类型本身。如果这听起来很奇怪，就这样想吧。SFML 创建了`Sprite`、`RectangleShape`和`RenderWindow`类，但要使用这些类中的任何一个，我们必须声明该类的对象/实例。\n\n此时，我们已经创建了一个名为`zombieTypes`的新类型，但我们没有它的实例。那么，让我们现在就这样做：\n\n```cpp\nzombieType jeremy = zombieTypes::CRAWLER;\nzombieType anna = zombieTypes::SPITTER;\nzombieType diane = zombieTypes::BLOATER;\n/*\n    Zombies are fictional creatures and any resemblance\n    to real people is entirely coincidental\n*/\n```\n\n接下来是我们即将添加到 Timber 中的代码类型的预览！！！。我们希望跟踪分支或玩家位于树的哪一侧，因此我们将声明一个名为`side`的枚举，如下所示：\n\n```cpp\nenum class side { LEFT, RIGHT, NONE };\n```\n\n我们可以将球员放在左边，如下所示：\n\n```cpp\n// The player starts on the left\nside playerSide = side::LEFT;\n```\n\n我们可以使分支位置数组的第四级（数组从零开始）完全没有分支，如下所示：\n\n```cpp\nbranchPositions[3] = side::NONE;\n```\n\n我们也可以在表达式中使用枚举：\n\n```cpp\nif(branchPositions[5] == playerSide)\n{\n    // The lowest branch is the same side as the player\n    // SQUISHED!!\n}\n```\n\n前面的代码测试数组位置[5]元素中的分支是否与玩家位于同一侧。\n\n我们将研究一个更重要的 C++ 主题，即函数，然后我们将回到编码游戏。当我们想划分一些做某件特定事情的代码时，我们可以使用函数。\n\n# 功能入门\n\nC++ 函数究竟是什么？函数是变量、表达式和**控制流语句**（循环和分支）的集合。事实上，到目前为止，我们在本书中学习的任何代码都可以在函数中使用。我们编写的函数的第一部分称为**签名**。下面是一个函数签名示例：\n\n```cpp\nvoid shootLazers(int power, int direction);\n```\n\n如果我们加上一对大括号`{...}`和函数执行的一些代码，我们就有了一个完整的函数，即**定义**：\n\n```cpp\nvoid shootLazers(int power, int direction)\n{\n    // ZAPP!\n}\n```\n\n然后，我们可以从代码的另一部分使用新函数，可能如下所示：\n\n```cpp\n// Attack the player\nshootLazers(50, 180) // Run the code in the function\n// I'm back again - code continues here after the function ends\n```\n\n当我们使用一个函数时，我们说我们**调用**它。在我们调用`shootLazers`时，程序的执行分支到该函数中包含的代码。该函数将一直运行到结束或被告知`return`。然后，代码将从函数调用后的第一行继续运行。我们已经在使用 SFML 提供的函数。这里不同的是，我们将学习编写和调用自己的函数。\n\n下面是函数的另一个示例，包含使函数返回到调用它的代码的代码：\n\n```cpp\nint addAToB(int a, int b)\n{\n    int answer = a + b;\n    return answer;\n}\n```\n\n我们可以使用前面函数的调用可能如下所示：\n\n```cpp\nint myAnswer = addAToB(2, 4);\n```\n\n显然，我们不需要编写函数来将两个变量添加到一起，但是这个示例帮助我们了解函数的工作方式。首先，我们传入值`2`和`4`。在函数签名中，值`2`被分配给`int a`，值`4`被分配给`int b`。\n\n在函数体中，`a`和`b`变量相加，用于初始化新变量`int answer`。`return answer;`线路就是这样做的。它将`answer`中存储的值返回给调用代码，导致`myAnswer`被值`6`初始化。\n\n请注意，前面示例中的每个函数签名都略有不同。原因是 C++ 函数签名非常灵活，允许我们精确地构建我们需要的函数。\n\n函数签名如何定义必须如何调用函数以及函数是否/如何返回值值得进一步讨论。让我们给签名的每一部分起一个名字，这样我们就可以把它分成几个部分并了解它们。\n\n这是一个函数签名，其部分由其正式/技术术语描述：\n\n```cpp\nreturn type | name of function | (parameters)\n```\n\n以下是我们可以用于这些部件的几个示例：\n\n*   返回类型 Po.T5：：To T0，，T1，，T2，等，或任何 C++ 类型或表达式。\n*   **功能名称**：`shootLazers`、`addAToB`等\n*   这和参数，参数，T1，T2 是一样的。\n\n现在，让我们依次看看每个部分，从返回类型开始。\n\n## 函数返回类型\n\n顾名思义，返回类型是将从函数返回到调用代码的值的类型：\n\n```cpp\nint addAToB(int a, int b){\n\n    int answer = a + b;\n    return answer;\n\n}\n```\n\n在我们前面看到的稍微单调但有用的`addAtoB`示例中，签名中的返回类型是`int`。`addAToB`函数向调用它的代码发回并返回一个适合`int`变量的值。返回类型可以是我们迄今所看到的任何 C++ 类型，也可以是我们尚未见过的 C++ 类型。\n\n但是，函数根本不必返回值。在这种情况下，签名必须使用`void`关键字作为返回类型。使用`void`关键字时，函数体不得尝试返回值，因为这将导致错误。但是，它可以使用没有值的`return`关键字。以下是返回类型和使用`return`关键字的有效组合：\n\n```cpp\nvoid doWhatever(){\n\n    // our code\n    // I'm done going back to calling code here\n    // no return is necessary\n\n}\n```\n\n另一种可能性如下：\n\n```cpp\nvoid doSomethingCool(){\n\n    // our code\n\n    // I can do this if I don't try and use a value\n    return;\n}\n```\n\n下面的代码是更多可能函数的示例。请务必阅读注释和代码：\n\n```cpp\nvoid doYetAnotherThing(){\n    // some code\n\n    if(someCondition){\n\n        // if someCondition is true returning to calling code\n        // before the end of the function body\n        return;\n    }\n\n    // More code that might or might not get executed\n\n    return;\n\n    // As I'm at the bottom of the function body\n    // and the return type is void, I'm\n    // really not necessary but I suppose I make it\n    // clear that the function is over.\n}\n\nbool detectCollision(Ship a, Ship b){\n\n    // Detect if collision has occurred\n    if(collision)\n    {\n        // Bam!!!\n        return true;\n    }\n    else\n    {\n        // Missed\n        return false;\n    }\n\n}\n```\n\n前一个代码中的最后一个函数示例，是针对 Stutt0 的，是对我们 C++ 代码的近期的一瞥，并且演示了我们也可以将用户定义的类型作为对象传递到函数中，这样我们就可以对它们执行计算。\n\n我们可以依次调用每个函数，如下所示：\n\n```cpp\n// OK time to call some functions\ndoWhatever();\ndoSomethingCool();\ndoYetAnotherThing();\n\nif (detectCollision(milleniumFalcon, lukesXWing))\n{\n    // The jedi are doomed!\n    // But there is always Leia.\n    // Unless she was on the Falcon?\n}\nelse\n{\n    // Live to fight another day\n}\n\n// Continue with code from here\n```\n\n不要担心关于`detectCollision`函数的奇怪语法；我们很快就会看到这样的代码。简单地说，我们使用返回值（`true`或`false`作为直接在`if` 语句中的表达式。\n\n## 函数名\n\n我们在设计自己的函数时使用的函数名几乎可以是任何东西。但最好使用词语，通常是动词，清楚地解释函数的作用。例如，查看以下函数：\n\n```cpp\nvoid functionaroonieboonie(int blibbityblob, float floppyfloatything)\n{\n    //code here\n}\n```\n\n前面的函数是完全合法的，可以工作，但下面的函数名更清楚：\n\n```cpp\nvoid doSomeVerySpecificTask()\n{\n    //code here\n}\n\nint getMySpaceShipHealth()\n{\n    //code here\n}\n\nvoid startNewGame()\n{\n    //code here\n}\n```\n\n使用前三个示例中的清晰且描述性的函数名是一种很好的做法，但是，正如我们从`functionaroonieboonie`函数中看到的，这不是编译器强制执行的规则。接下来，我们将进一步了解如何与函数共享一些值。\n\n## 功能参数\n\n我们知道函数可以将结果返回给调用代码。但是，如果我们需要与函数共享调用代码中的一些数据值，该怎么办？**参数**允许我们与函数共享值。在查看返回类型时，我们已经看到了参数示例。我们将看同一个例子，但更仔细一点：\n\n```cpp\nint addAToB(int a, int b)\n{ \n    int answer = a + b;\n    return answer; \n}\n```\n\n这里，参数是`int a`和`int b`。请注意，在函数体的第一行中，我们使用了`a + b`，就好像它们已经声明并初始化了变量一样。那是因为他们是。函数签名中的参数是它们的声明，调用函数的代码将初始化它们。\n\n重要术语注释\n\n请注意，我们将函数签名括号`(int a, int b)`中的变量称为参数。当我们将值从调用代码传递到函数中时，这些值称为参数。当参数到达时，参数使用这些参数初始化实际的可用变量，如：\n\n`int returnedAnswer = addAToB(10,5);`\n\n此外，正如我们在前面的示例中部分看到的，我们不必在参数中只使用`int`。我们可以使用任何 C++ 类型。我们也可以根据需要使用尽可能多的参数来解决问题，但最好保持参数列表尽可能短，因此尽可能易于管理。\n\n正如我们将在未来章节中看到的，我们已经从本教程中留下了一些较酷的函数用法，这样我们就可以在我们进一步讨论函数的主题之前了解相关的 C++ 概念。\n\n## 功能体\n\n正文是我们一直在回避的部分，有如下评论：\n\n```cpp\n// code here\n// some code\n```\n\n事实上，我们已经知道该怎么做了！到目前为止，我们所了解的任何 C++ 代码都将在函数的主体中工作。\n\n## 功能原型\n\n到目前为止，我们已经看到了如何编写函数，也看到了如何调用函数。然而，我们还需要做一件事，让它们发挥作用。所有功能必须有一个**原型**。原型让编译器知道我们的功能，如果没有原型，整个游戏将无法编译。幸运的是，原型很简单。\n\n我们可以简单地重复函数的签名，后跟分号。需要注意的是，原型必须在任何调用或定义函数的尝试之前出现。下面是一个完全可用的函数的最简单的例子。仔细查看注释和代码中函数不同部分出现的位置：\n\n```cpp\n// The prototype\n// Notice the semicolon on the end\nint addAToB(int a, int b);\n\nint main()\n{ \n    // Call the function\n    // Store the result in answer\n    int answer = addAToB(2,2);\n\n    // Called before the definition\n    // but that's OK because of the prototype\n\n    // Exit main\n    return 0;\n\n}// End of main\n\n// The function definition\nint addAToB(int a, int b)\n{\n    return a + b;\n}\n```\n\n前面的代码演示了以下内容：\n\n*   原型在`main`功能之前。\n*   使用该函数的调用正如我们所预期的，在`main`函数中。\n*   The definition is after/outside the `main` function.\n\n    重要提示\n\n    请注意，我们可以省略函数原型，当定义出现在函数使用之前时，直接转到定义。然而，随着我们的代码变得越来越长，并且分布在多个文件中，这几乎永远不会发生。我们将始终使用单独的原型和定义。\n\n让我们看看如何组织我们的职能。\n\n## 组织职能\n\n值得指出的是，如果我们有多个函数，特别是如果它们相当长，我们的`.cpp`文件将很快变得难以处理。这违背了功能的部分目标。我们将在下一个项目中看到的解决方案是，我们可以将所有功能原型添加到我们自己的头文件（`.hpp` 或 `.h`）。然后，我们可以在另一个`.cpp`文件中对所有函数进行编码，只需在主`.cpp`文件中添加另一个`#include...`指令即可。这样，我们就可以使用任意数量的函数，而无需将它们的任何代码（原型或定义）添加到主代码文件中。\n\n## 函数明白了！\n\n关于函数我们应该讨论的另一点是**范围**。如果我们在函数中直接或在其中一个参数中声明变量，则该变量在该函数之外不可用/不可见。此外，在其他函数中声明的任何变量都不能在函数中看到/使用。\n\n我们应该通过参数/参数和返回值在函数代码和调用代码之间共享值。\n\n当一个变量由于来自另一个函数而不可用时，称其超出范围。当它可用时，就说它在范围之内。\n\n重要提示\n\nC++ 中任何块内声明的变量仅在该块内的范围内！这也包括循环和`if`块。在`main`顶部声明的变量在`main`的任意范围内，在游戏循环中声明的变量仅在游戏循环的范围内，依此类推。在函数或其他块中声明的变量称为**局部**变量。我们编写的代码越多，这就越有意义。每次我们在代码中遇到关于范围的问题时，我都会进行讨论，以澄清问题。下一节将讨论一个这样的问题。也有更多的 C++ 主食把这个问题公开了。它们被称为 ORT T5。引用 To6T6 和 Posits T8，我们将在第 9 章中学习它们，第 10 章，第 11 章，第 13 章，C++ 引用，SpRITE 表，和顶点数组。标准模板库，纹理管理。\n\n## 更多关于功能的信息\n\n我们还可以了解更多的函数，但我们已经对它们有了足够的了解，可以实现游戏的下一部分。如果所有的技术术语，如参数、签名和定义还没有完全理解，也不用担心。当我们开始使用这些概念时，它们会变得更加清晰。\n\n## 关于函数的绝对定论——现在\n\n您可能注意到，我们一直在调用函数，特别是 SFML 函数，方法是在函数名之前添加对象名和句点，如下所示：\n\n```cpp\nspriteBee.setPosition...\nwindow.draw...\n// etc\n```\n\n然而，在我们对函数的整个讨论中，我们看到了在没有任何对象的情况下调用函数。我们可以将函数作为类的一部分编写，也可以简单地作为独立函数编写。当我们将函数作为类的一部分编写时，我们需要该类的一个对象来调用该函数，但是当我们有一个独立的函数时，我们就不需要了。\n\n我们将在一分钟内编写一个独立的函数，我们将从[*第 6 章*](06.html#_idTextAnchor154)、*面向对象编程开始编写函数类*。到目前为止，我们对函数的所有了解都与这两种情况相关。\n\n现在，我们可以回到编码的木材分支！！！游戏\n\n# 种植树枝\n\n接下来，正如我在过去 20 页中所承诺的，我们将使用我们所学的所有新的 C++ 技术来绘制和移动树上的一些分支。\n\n在`main`功能之外添加以下代码。为了明确起见，我的意思是之前的*是`int main()`的代码：*\n\n```cpp\n#include <sstream>\n#include <SFML/Graphics.hpp>\nusing namespace sf;\n// Function declaration\nvoid updateBranches(int seed);\nconst int NUM_BRANCHES = 6;\nSprite branches[NUM_BRANCHES];\n// Where is the player/branch?\n// Left or Right\nenum class side { LEFT, RIGHT, NONE };\nside branchPositions[NUM_BRANCHES];\nint main()\n```\n\n我们刚刚用新代码实现了很多东西：\n\n*   首先，我们为一个名为`updateBranches`的函数编写了一个函数原型。我们可以看到，它不返回值（`void`，并且它接受一个名为`seed`的`int`参数。我们将很快编写函数定义，然后我们将确切地了解它的功能。\n*   接下来，我们声明一个名为`NUM_BRANCHES`的`int`常量，并将其初始化为`6`。树上会有六根移动的树枝，我们很快就会看到`NUM_BRANCHES`对我们是多么有用。\n*   接下来，我们声明一个名为`branches`的`Sprite`对象数组，它可以容纳六个`Sprite`实例。\n*   之后，我们声明了一个名为`side`的新枚举，它有三个可能的值：`LEFT`、`RIGHT`和`NONE`。这将用于描述各个分支以及玩家在代码中的几个位置的位置。\n*   Finally, in the preceding code, we initialize an array of `side` types with a size of `NUM_BRANCHES` (6). To be clear about what this achieves, we will have an array called `branchPositions` with six values in it. Each of these values is of the `side` type and can be either `LEFT`, `RIGHT`, or `NONE`.\n\n    重要提示\n\n    当然，您真正想知道的是为什么常量、两个数组和枚举被声明在`main`函数的之外。通过在`main`之上声明它们，它们现在拥有**全局范围**。用另一种方式来描述这一点，常量、两个数组和枚举在整个游戏中都有作用域。这意味着我们可以在`main`功能和`updateBranches`功能中的任何位置访问和使用它们。注意，使所有变量尽可能地位于实际使用的地方是一种很好的做法。使所有内容都全球化似乎很有用，但这会导致代码难以阅读且容易出错。\n\n## 准备树枝\n\n现在，我们将准备六个`Sprite`对象并将它们加载到`branches`数组中。在游戏循环之前添加以下突出显示的代码：\n\n```cpp\n// Position the text\nFloatRect textRect = messageText.getLocalBounds();\nmessageText.setOrigin(textRect.left +\n    textRect.width / 2.0f,\n    textRect.top +\n    textRect.height / 2.0f);\nmessageText.setPosition(1920 / 2.0f, 1080 / 2.0f);\nscoreText.setPosition(20, 20);\n// Prepare 6 branches\nTexture textureBranch;\ntextureBranch.loadFromFile(\"graphics/branch.png\");\n// Set the texture for each branch sprite\nfor (int i = 0; i < NUM_BRANCHES; i++) {\n    branches[i].setTexture(textureBranch);\n    branches[i].setPosition(-2000, -2000);\n    // Set the sprite's origin to dead centre\n    // We can then spin it round without changing its position\n    branches[i].setOrigin(220, 20);\n}\nwhile (window.isOpen())\n```\n\n在前面的代码中，我们正在执行以下操作：\n\n1.  首先，我们声明一个 SFML`Texture`对象并将`branch.png`图形加载到其中。\n2.  接下来，我们创建一个`for`循环，将`i`设置为零，并在每次通过该循环时增加`i`，直到`i`不再小于`NUM_BRANCHES`。这是完全正确的，因为`NUM_BRANCHES`是 6，`branches`阵列的位置是 0 到 5。\n3.  在`for`循环中，我们使用`setTexture`为`branches`数组中的每个`Sprite`设置`Texture`，然后使用`setPosition`将其隐藏在屏幕外。\n4.  最后，我们使用`setOrigin`将原点（绘制精灵时用于定位精灵的点）设置为精灵的中心。很快，我们将旋转这些精灵。将原点置于中心意味着它们将很好地旋转，而不会将精灵移出位置。\n\n现在我们已经准备好了所有的分支，我们可以编写一些代码来在每一帧更新它们。\n\n## 每帧更新分支精灵\n\n在下面的代码中，我们将根据`branches`数组中所有精灵的位置以及`side`在相应`branchPositions`数组中的值来设置`branches`数组中所有精灵的位置。添加以下突出显示的代码，并在详细阅读之前先尝试理解它：\n\n```cpp\n    // Update the score text\n    std::stringstream ss;\n    ss << \"Score: \" << score;\n    scoreText.setString(ss.str());\n    // update the branch sprites\n    for (int i = 0; i < NUM_BRANCHES; i++)\n    {\n        float height = i * 150;\n        if (branchPositions[i] == side::LEFT)\n        {\n            // Move the sprite to the left side\n            branches[i].setPosition(610, height);\n            // Flip the sprite round the other way\n            branches[i].setRotation(180);\n        }\n        else if (branchPositions[i] == side::RIGHT)\n        {\n            // Move the sprite to the right side\n            branches[i].setPosition(1330, height);\n            // Set the sprite rotation to normal\n            branches[i].setRotation(0);\n        }\n        else\n        {\n            // Hide the branch\n            branches[i].setPosition(3000, height);\n        }\n    }\n} // End if(!paused)\n/*\n****************************************\nDraw the scene\n****************************************\n```\n\n我们刚才添加的代码是一个大的`for`循环，它将`i`设置为零，并在循环中每次增加`i`一，并一直持续到`i`不再小于 6。\n\n在`for`循环中，一个名为`height`的新`float`变量被设置为`i * 150`。这意味着第一个分支的高度为 0，第二个分支的高度为 150，第六个分支的高度为 750。\n\n接下来，我们有一个由`if`和`else`块组成的结构。看一看去掉代码的结构：\n\n```cpp\nif()\n{\n}\nelse if()\n{\n}\nelse\n{\n}\n```\n\n第一条`if`语句使用`branchPositions`数组查看当前分支是否应位于左侧。如果是，则将`branches`阵列中相应的`Sprite`设置为屏幕上的一个位置，适合左侧（610 像素）和当前`height`的任何位置。然后将精灵翻转 180 度，因为默认情况下，`branch.png`图形“挂起”在右侧。\n\n请注意，`else if`仅在分支不在左侧时执行。它使用相同的方法查看它是否在右侧。如果是，则在右侧绘制分支（1330 像素）。然后，将精灵旋转设置为零度，以防之前旋转为 180 度。如果 x 坐标看起来有点奇怪，请记住我们将分支精灵的原点设置为其中心。\n\n最后的`else`语句正确地假设当前`branchPosition`必须是`NONE`，并将分支隐藏在 3000 像素的屏幕外。\n\n此时，我们的分支机构已就位，准备就绪。\n\n## 画树枝\n\n在这里，我们将使用另一个`for`循环从 0 到 5 逐步遍历整个`branches`阵列，并绘制每个分支精灵。添加以下突出显示的代码：\n\n```cpp\n// Draw the clouds\nwindow.draw(spriteCloud1);\nwindow.draw(spriteCloud2);\nwindow.draw(spriteCloud3);\n// Draw the branches\nfor (int i = 0; i < NUM_BRANCHES; i++) {\n    window.draw(branches[i]);\n}\n// Draw the tree\nwindow.draw(spriteTree);\n```\n\n当然，我们还没有编写移动所有分支的函数。一旦我们编写了这个函数，我们还需要确定何时以及如何调用它。让我们解决第一个问题并编写函数。\n\n## 移动树枝\n\n我们已经在`main`函数之上添加了函数原型。现在，我们可以对函数的实际定义进行编码，该函数每次调用时都会将所有分支向下移动一个位置。我们将把这个函数分为两部分进行编码，以便我们可以轻松地检查正在发生的事情。\n\n在`main`功能的关闭曲括号后增加`updateBranches`功能*的第一部分：*\n\n```cpp\n// Function definition\nvoid updateBranches(int seed)\n{\n    // Move all the branches down one place\n    for (int j = NUM_BRANCHES-1; j > 0; j--) {\t\n        branchPositions[j] = branchPositions[j - 1];\n    }\n}\n```\n\n在函数的第一部分中，我们只需将所有分支向下移动一个位置，一次一个，从第六个分支开始。这是通过使`for`循环计数从 5 到 0 来实现的。请注意，`branchPositions[j] = branchPositions[j - 1];` 进行实际移动。\n\n在前面的代码中需要注意的另一点是，在我们将位置 4 的分支移动到位置 5 之后，然后将位置 3 的分支移动到位置 4，依此类推，我们需要在树的顶部位置 0 添加一个新分支。\n\n现在，我们可以在树的顶部生成一个新的分支。添加以下突出显示的代码，然后我们将讨论它：\n\n```cpp\n// Function definition\nvoid updateBranches(int seed)\n{\n    // Move all the branches down one place\n    for (int j = NUM_BRANCHES-1; j > 0; j--) {\t\n        branchPositions[j] = branchPositions[j - 1];\n    }\n    // Spawn a new branch at position 0\n    // LEFT, RIGHT or NONE\n    srand((int)time(0)+seed);\n    int r = (rand() % 5);\n    switch (r) {\n    case 0:\n        branchPositions[0] = side::LEFT;\n        break;\n    case 1:\n        branchPositions[0] = side::RIGHT;\n        break;\n    default:\n        branchPositions[0] = side::NONE;\n        break;\n    }\n}\n```\n\n在`updateBranches`函数的最后一部分，我们使用与函数调用一起传入的整数`seed`变量。我们这样做是为了保证随机数种子总是不同的。我们将在下一章中看到我们是如何得出这个值的。\n\n接下来，我们生成一个介于 0 和 4 之间的随机数，并将结果存储在名为`r`的`int`变量中。现在，我们使用`switch`，使用`r`作为表达式。\n\n`case`语句意味着，如果`r`等于零，那么我们将在树的左侧顶部添加一个新分支。如果`r`等于 1，则分支向右移动。如果`r`是其他内容（2、3 或 4），则`default`确保不会在顶部添加分支。这种左、右、无的平衡使树看起来很现实，游戏也很好地运行。您可以轻松地更改代码，使分支更频繁或更少。\n\n即使为我们的分支编写了所有这些代码，我们仍然无法在游戏中看到它们中的任何一个。这是因为在调用`updateBranches`函数之前，我们还有更多的工作要做。\n\n如果您现在想查看分支，您可以添加一些临时代码，每次在游戏循环之前使用唯一的种子调用函数五次：\n\n```cpp\nupdateBranches(1);\nupdateBranches(2);\nupdateBranches(3);\nupdateBranches(4);\nupdateBranches(5);\nwhile (window.isOpen())\n{\n```\n\n现在，您可以在适当的位置看到分支。但如果分支机构要实际移动，我们需要定期致电`updateBranches`：\n\n![](img/B14278_04_01.jpg)\n\n提示\n\n在继续之前，不要忘记删除临时代码。\n\n现在，我们也可以把注意力转移到玩家身上，真正调用`updateBranches`函数。我们将在下一章中这样做。\n\n# 总结\n\n虽然不是最长的，但这可能是我们迄今为止覆盖最多 C++ 的章节。我们研究了可以使用的不同类型的循环，例如`for`和`while`循环。然后我们研究了数组，我们可以使用它们来处理大量变量和对象，而不必费吹灰之力。我们还学习了枚举和`switch`。本章中最大的概念可能是函数，它允许我们组织和抽象游戏代码。在本书中，我们将更深入地了解更多地方的函数。\n\n现在我们有了一个完整的“工作”树，我们可以完成游戏了，我们将在本项目的下一章和最后一章中完成。\n\n# 常见问题\n\n你提到过还有几种类型的 C++ 循环。我在哪里可以找到他们？\n\nA） 是的，请看本教程和对`do while`循环的解释：[http://www.tutorialspoint.com/cplusplus/cpp_do_while_loop.htm](http://www.tutorialspoint.com/cplusplus/cpp_do_while_loop.htm) 。\n\nQ） 我可以假设我现在是阵列方面的专家吗？\n\nA） 就像本书中的许多主题一样，总有更多的东西需要学习。您对阵列的了解已经足够多，可以继续学习，但如果您还想了解更多，请参阅本更全面的阵列教程：[http://www.cplusplus.com/doc/tutorial/arrays/](http://www.cplusplus.com/doc/tutorial/arrays/) 。\n\nQ） 我可以假设我是函数方面的专家吗？\n\nA） 就像本书中的许多主题一样，总有更多的东西需要学习。您对函数了解得足够多，可以继续学习，但如果您想了解更多，请参阅本教程：[http://www.cplusplus.com/doc/tutorial/functions/](http://www.cplusplus.com/doc/tutorial/functions/) 。"
  },
  {
    "path": "docs/begin-cpp-game-prog/05.md",
    "content": "# 五、碰撞、声音和结束条件——使游戏可玩\n\n这是第一个项目的最后阶段。本章结束时，您将完成第一个完整的游戏。一旦你有了木材！！！启动和运行时，请务必阅读本章的最后一节，因为它将提出使游戏更好的方法。\n\n在本章中，我们将介绍以下主题：\n\n*   添加其余的精灵\n*   处理玩家输入\n*   设置飞行日志的动画\n*   处理死亡\n*   添加声音效果\n*   添加功能和改进木材！！！\n\n# 准备玩家（和其他精灵）\n\n让我们为玩家的精灵添加代码，同时添加一些精灵和纹理。下面这个相当大的代码块还添加了一个墓碑精灵，用于在玩家被压扁时使用，一个斧头精灵用于劈砍，以及一个可以在玩家每次劈砍时快速离开的原木精灵。\n\n注意，在`spritePlayer`对象之后，我们声明了一个`side`变量`playerSide`，以跟踪玩家当前的位置。此外，我们为`spriteLog` 对象添加了一些额外的变量，包括`logSpeedX`、`logSpeedY`和`logActive`，以存储日志移动的速度以及当前是否正在移动。`spriteAxe`还有两个相关的`float`常量变量，用于记住理想像素位置在左右两侧的位置。\n\n在`while(window.isOpen())`代码之前添加以下代码块，就像我们以前经常做的那样。请注意，下面代码块中的所有代码都是新的，而不仅仅是突出显示的代码。我没有为这段代码提供任何额外的上下文，因为`while(window.isOpen())`应该很容易识别。突出显示的代码就是我们刚才讨论的代码。\n\n在`while(window.isOpen())`行之前添加以下代码的全部内容，并记下我们简要讨论过的突出显示的行。这将使本章代码的其余部分更容易理解：\n\n```cpp\n// Prepare the player\nTexture texturePlayer;\ntexturePlayer.loadFromFile(\"graphics/player.png\");\nSprite spritePlayer;\nspritePlayer.setTexture(texturePlayer);\nspritePlayer.setPosition(580, 720);\n// The player starts on the left\nside playerSide = side::LEFT;\n// Prepare the gravestone\nTexture textureRIP;\ntextureRIP.loadFromFile(\"graphics/rip.png\");\nSprite spriteRIP;\nspriteRIP.setTexture(textureRIP);\nspriteRIP.setPosition(600, 860);\n// Prepare the axe\nTexture textureAxe;\ntextureAxe.loadFromFile(\"graphics/axe.png\");\nSprite spriteAxe;\nspriteAxe.setTexture(textureAxe);\nspriteAxe.setPosition(700, 830);\n// Line the axe up with the tree\nconst float AXE_POSITION_LEFT = 700;\nconst float AXE_POSITION_RIGHT = 1075;\n// Prepare the flying log\nTexture textureLog;\ntextureLog.loadFromFile(\"graphics/log.png\");\nSprite spriteLog;\nspriteLog.setTexture(textureLog);\nspriteLog.setPosition(810, 720);\n// Some other useful log related variables\nbool logActive = false;\nfloat logSpeedX = 1000;\nfloat logSpeedY = -1500;\n```\n\n在前面的代码中，我们添加了许多新变量。在我们真正使用它们之前，很难完整地解释它们，但下面是它们的用途概述。有一个名为`playerSide`的`side`枚举类型变量被初始化为`left`。这将跟踪玩家在树的哪一侧。\n\n有两个`const float`值决定了斧头将被划到的水平位置，这取决于玩家是在树的左侧还是右侧。\n\n还有三个变量有助于在原木被砍断并飞离树木时保持对原木的控制，`bool`用于确定原木是否在运动（`logActive`和两个`float`值用于保持原木的水平和垂直速度。\n\n现在，我们可以画所有的新精灵了。\n\n# 绘制玩家和其他精灵\n\n在我们添加代码来移动玩家并使用我们所有的新精灵之前，让我们先绘制它们。我们这样做是为了在添加代码以更新/更改/移动它们时，能够看到发生了什么。\n\n添加以下高亮显示的代码以绘制四个新精灵：\n\n```cpp\n// Draw the tree\nwindow.draw(spriteTree);\n// Draw the player\nwindow.draw(spritePlayer);\n// Draw the axe\nwindow.draw(spriteAxe);\n// Draw the flying log\nwindow.draw(spriteLog);\n// Draw the gravestone\nwindow.draw(spriteRIP);\n// Draw the bee\nwindow.draw(spriteBee);\n```\n\n前面的代码将我们的四个新精灵一个接一个地传递给`draw`函数。\n\n运行游戏，您将在场景中看到我们的新精灵：\n\n![](img/B14278_05_01.jpg)\n\n我们现在真的很接近一场比赛了。下一个任务是编写一些代码，让玩家控制发生的事情。\n\n# 处理玩家的输入\n\n一些不同的事情取决于玩家的移动，如下所示：\n\n*   什么时候展示斧头\n*   何时开始设置日志动画\n*   何时将所有分支向下移动\n\n因此，为正在切球的球员设置键盘操作是有意义的。完成后，我们可以将刚才提到的所有特性放在代码的同一部分中。\n\n让我们思考一下如何检测键盘按下。在每一帧中，我们测试当前是否按下了特定的键盘键。如果是，我们就采取行动。如果按下*Esc*键，我们退出游戏，如果按下*Enter*键，我们重新开始游戏。到目前为止，这已足以满足我们的需要。\n\n然而，当我们试图处理砍树时，这种方法存在一个问题。问题一直存在,；直到现在才有关系。根据你的电脑功能的不同，游戏循环每秒可以执行数千次。每次通过游戏循环，按下一个键，就会被检测到，相关代码就会执行。\n\n所以，实际上，每次你按*回车*重新启动游戏，你很可能会重新启动它超过一百次。这是因为即使是最简单的印刷机也只能持续很短的一秒钟。您可以通过运行游戏并按住*回车*键来验证这一点。请注意，时间条不会移动。这是因为游戏一次又一次地重新启动，每秒数百甚至数千次。\n\n如果我们不使用一种不同的方法来砍树，那么仅仅一次尝试砍树就会在短短的几秒钟内把整棵树砍倒。我们需要更成熟一点。我们要做的是允许玩家切碎，然后当他们这样做时，禁用检测按键的代码。然后，我们将检测玩家何时将手指从按键上移开，然后重新启用按键检测。以下是明确列出的步骤：\n\n1.  等待玩家使用左箭头键或右箭头键切碎原木。\n2.  当玩家切碎时，禁用按键检测。\n3.  等待玩家将手指从钥匙上取下。\n4.  可重入斩波检测。\n5.  重复步骤 1。\n\n这听起来可能很复杂，但在 SFML 的帮助下，这将很简单。现在，让我们一步一步地实现它。\n\n添加以下突出显示的代码行，它声明了一个名为`acceptInput`的`bool`变量，该变量将用于确定何时侦听印章以及何时忽略印章：\n\n```cpp\nfloat logSpeedX = 1000;\nfloat logSpeedY = -1500;\n// Control the player input\nbool acceptInput = false;\nwhile (window.isOpen())\n{\n```\n\n现在我们已经设置了布尔值，我们可以继续下一步了。\n\n## 处理设置新游戏\n\n为了准备好处理切块，请将以下突出显示的代码添加到启动新游戏的`if`块：\n\n```cpp\n/*\n****************************************\nHandle the players input\n****************************************\n*/\nif (Keyboard::isKeyPressed(Keyboard::Escape))\n{\n    window.close();\n}\n// Start the game\nif (Keyboard::isKeyPressed(Keyboard::Return))\n{\n    paused = false;\n    // Reset the time and the score\n    score = 0;\n    timeRemaining = 6;\n // Make all the branches disappear -\n // starting in the second position\n for (int i = 1; i < NUM_BRANCHES; i++)\n {\n branchPositions[i] = side::NONE;\n }\n // Make sure the gravestone is hidden\n spriteRIP.setPosition(675, 2000);\n // Move the player into position\n spritePlayer.setPosition(580, 720);\n acceptInput = true;\n}\n/*\n****************************************\nUpdate the scene\n****************************************\n*/\n```\n\n在前面的代码中，我们使用`for`循环来准备没有分支的树。这对玩家来说是公平的，因为如果游戏开始时树枝正好在他们头顶上方，这将被认为是不体育的。然后，我们只需将墓碑从屏幕上移开，玩家就可以进入左侧的起始位置。前面代码所做的最后一件事是将`acceptInput`设置为`true`。\n\n我们现在准备好接受切碎按键。\n\n## 检测玩家切球\n\n现在，我们可以处理左右光标按键。添加这个简单的`if`块，它只在`acceptInput`为`true`时执行：\n\n```cpp\n// Start the game\nif (Keyboard::isKeyPressed(Keyboard::Return))\n{\n    paused = false;\n    // Reset the time and the score\n    score = 0;\n    timeRemaining = 5;\n    // Make all the branches disappear\n    for (int i = 1; i < NUM_BRANCHES; i++)\n    {\n        branchPositions[i] = side::NONE;\n    }\n    // Make sure the gravestone is hidden\n    spriteRIP.setPosition(675, 2000);\n    // Move the player into position\n    spritePlayer.setPosition(675, 660);\n    acceptInput = true;\n}\n// Wrap the player controls to\n// Make sure we are accepting input\nif (acceptInput)\n{\n // More code here next...\n}\n/*\n****************************************\nUpdate the scene\n****************************************\n*/\n```\n\n现在，在我们刚刚编码的`if`块中，添加以下突出显示的代码，以处理玩家按下键盘上的右光标键时发生的情况：\n\n```cpp\n// Wrap the player controls to\n// Make sure we are accepting input\nif (acceptInput)\n{\n    // More code here next...\n\n // First handle pressing the right cursor key\n if (Keyboard::isKeyPressed(Keyboard::Right))\n {\n // Make sure the player is on the right\n playerSide = side::RIGHT;\n\n score ++ ;\n // Add to the amount of time remaining\n timeRemaining += (2 / score) + .15;\n spriteAxe.setPosition(AXE_POSITION_RIGHT,\n spriteAxe.getPosition().y);\n spritePlayer.setPosition(1200, 720);\n // Update the branches\n updateBranches(score);\n\n // Set the log flying to the left\n spriteLog.setPosition(810, 720);\n logSpeedX = -5000;\n logActive = true;\n acceptInput = false;\n }\n // Handle the left cursor key\n}\n```\n\n在前面的代码中发生了很多事情，所以让我们来看看：\n\n*   首先，我们检测玩家是否在树的右侧砍树。如果他们有，那么我们将`playerSide`设置为`side::RIGHT`。我们稍后将在代码中响应`playerSide`的值。然后，我们在分数上加上一个`score ++ `。\n*   下一行代码有点神秘，但所发生的一切是我们增加了剩余的时间。我们奖励球员采取行动。然而，对于玩家来说，问题是分数越高，额外增加的时间就越少。你可以使用这个公式使游戏更容易或更难。\n*   然后，用`spriteAxe.setPosition`将斧头移动到其右侧位置，玩家精灵也移动到其右侧位置。\n*   接下来，我们调用`updateBranches`将所有分支向下移动一个位置，并在树的顶部生成一个新的随机分支（或空间）。\n*   然后，`spriteLog`被移动到其起始位置，伪装成树，其`speedX`变量被设置为负数，以便向左呼啸。另外，`logActive`被设置为`true`，这样我们将很快编写的日志移动代码将在每一帧为日志设置动画。\n*   最后，`acceptInput`设置为`false`。此时，玩家不能再进行排骨。我们已经解决了压力机被频繁检测的问题，我们将看看如何尽快重新启动切碎。\n\n现在，仍然在我们刚刚编码的`if(acceptInput)`块中，添加以下突出显示的代码来处理当玩家按下键盘上的左光标键时发生的情况：\n\n```cpp\n    // Handle the left cursor key\n if (Keyboard::isKeyPressed(Keyboard::Left))\n {\n // Make sure the player is on the left\n playerSide = side::LEFT;\n score++ ;\n // Add to the amount of time remaining\n timeRemaining += (2 / score) + .15;\n spriteAxe.setPosition(AXE_POSITION_LEFT,\n spriteAxe.getPosition().y);\n spritePlayer.setPosition(580, 720);\n // update the branches\n updateBranches(score);\n // set the log flying\n spriteLog.setPosition(810, 720);\n logSpeedX = 5000;\n logActive = true;\n acceptInput = false;\n }\n}\n```\n\n前面的代码与处理右侧印章的代码相同，只是精灵的位置不同，`logSpeedX`变量设置为正值，以便日志向右旋转。\n\n现在，我们可以对释放键盘键时发生的情况进行编码。\n\n## 检测到钥匙被释放\n\n要使前面的代码在第一次切块之后工作，我们需要检测玩家何时释放一个键，然后将`acceptInput`设置回`true`。\n\n这与我们到目前为止看到的密钥处理略有不同。SFML 有两种不同的方法来检测来自玩家的键盘输入。我们在处理*回车*键时已经看到了第一种方法，它是动态的、瞬时的，这正是我们需要立即对按键做出响应的。\n\n下面的代码使用了检测何时释放钥匙的方法。在`Handle the players input`部分顶部输入以下突出显示的代码，然后我们将进行检查：\n\n```cpp\n/*\n****************************************\nHandle the players input\n****************************************\n*/\nEvent event;\nwhile (window.pollEvent(event))\n{\n if (event.type == Event::KeyReleased && !paused)\n {\n // Listen for key presses again\n acceptInput = true;\n // hide the axe\n spriteAxe.setPosition(2000,\n spriteAxe.getPosition().y);\n }\n}\nif (Keyboard::isKeyPressed(Keyboard::Escape))\n{\n    window.close();\n}\n```\n\n在前面的代码中，我们声明了一个名为`event`的`Event`类型的对象。然后，我们调用`window.pollEvent`函数，传入新对象`event`。`pollEvent`函数将数据放入描述操作系统事件的`event`对象中。这可能是按键、按键释放、鼠标移动、鼠标单击、游戏控制器动作，或者窗口本身发生的事情（调整大小、移动等等）。\n\n我们将代码包装在`while`循环中的原因是队列中可能存储了许多事件。`window.pollEvent`功能将一次加载一个到`event`中。通过循环的每次传递，我们将看到我们是否对当前事件感兴趣，如果感兴趣，我们将做出响应。当`window.pollEvent`返回`false`时，表示队列中不再有事件，`while`循环将退出。\n\n此`if`条件`(event.type == Event::KeyReleased && !paused)`在两个按键都已释放且游戏未暂停时执行。\n\n在`if`区块内，我们将`acceptInput`设置回`true`并将斧头精灵隐藏在屏幕外。\n\n现在，您可以运行游戏并敬畏地注视移动的树、摆动的斧头和动画玩家。然而，它不会压扁玩家，而且原木在切碎时也不会移动。\n\n让我们继续让原木移动。\n\n## 为砍下的原木和斧头制作动画\n\n当玩家切块时，`logActive`被设置为`true`，因此我们可以将一些代码包装在一个块中，该块仅在`logActive`为`true`时执行。此外，每一个印章都会将`logSpeedX`设置为正数或负数，因此原木可以开始以正确的方向从树上飞走。\n\n在更新分支精灵的位置之后添加以下高亮显示的代码：\n\n```cpp\n    // update the branch sprites\n    for (int i = 0; i < NUM_BRANCHES; i++)\n    {\n        float height = i * 150;\n        if (branchPositions[i] == side::LEFT)\n        {\n            // Move the sprite to the left side\n            branches[i].setPosition(610, height);\n            // Flip the sprite round the other way\n            branches[i].setRotation(180);\n        }\n        else if (branchPositions[i] == side::RIGHT)\n        {\n            // Move the sprite to the right side\n            branches[i].setPosition(1330, height);\n            // Flip the sprite round the other way\n            branches[i].setRotation(0);\n        }\n        else\n        {\n            // Hide the branch\n            branches[i].setPosition(3000, height);\n        }\n    }\n // Handle a flying log\n if (logActive)\n {\n spriteLog.setPosition(\n spriteLog.getPosition().x + \n (logSpeedX * dt.asSeconds()),\n\n spriteLog.getPosition().y + \n (logSpeedY * dt.asSeconds()));\n // Has the log reached the right hand edge?\n if (spriteLog.getPosition().x < -100 ||\n spriteLog.getPosition().x > 2000)\n {\n // Set it up ready to be a whole new log next frame\n logActive = false;\n spriteLog.setPosition(810, 720);\n }\n }\n} // End if(!paused)\n/*\n****************************************\nDraw the scene\n****************************************\n*/\n```\n\n该代码通过使用`getPosition`获取精灵当前的水平和垂直位置，然后分别使用`logSpeedX`和`logSpeedY`乘以`dt.asSeconds`来设置精灵的位置。\n\n在每帧移动日志精灵后，代码使用`if`块查看精灵在左侧或右侧是否消失在视野之外。如果有，则将原木移回起点，准备进行下一次切碎。\n\n如果您现在运行游戏，您将能够看到日志飞到屏幕的适当一侧：\n\n![](img/B14278_05_02.jpg)\n\n现在，让我们转到一个更敏感的话题。\n\n# 处理死亡\n\n每一场比赛的结局都必须很糟糕，要么是球员的时间不够（我们已经处理好了），要么是被树枝压扁。\n\n检测球员被压扁真的很简单。我们只想知道：`branchPositions`数组中的最后一个分支是否等于`playerSide`？如果是这样，玩家就死了。\n\n添加以下高亮显示的代码，当玩家被分支挤压时检测并执行该代码。我们将在稍后讨论：\n\n```cpp\n    // Handle a flying log\n    if (logActive)\n    {\n        spriteLog.setPosition(\n            spriteLog.getPosition().x + \n                (logSpeedX * dt.asSeconds()),\n\n        spriteLog.getPosition().y + \n            (logSpeedY * dt.asSeconds()));\n        // Has the log reached the right-hand edge?\n        if (spriteLog.getPosition().x < -100 ||\n            spriteLog.getPosition().x > 2000)\n        {\n            // Set it up ready to be a whole new cloud next frame\n            logActive = false;\n            spriteLog.setPosition(800, 600);\n        }\n    }\n // has the player been squished by a branch?\n if (branchPositions[5] == playerSide)\n {\n // death\n paused = true;\n acceptInput = false;\n\n // Draw the gravestone\n spriteRIP.setPosition(525, 760);\n // hide the player\n spritePlayer.setPosition(2000, 660);\n // Change the text of the message\n messageText.setString(\"SQUISHED!!\");\n // Center it on the screen\n FloatRect textRect = messageText.getLocalBounds();\n messageText.setOrigin(textRect.left +\n textRect.width / 2.0f,\n textRect.top + textRect.height / 2.0f);\n messageText.setPosition(1920 / 2.0f,\n 1080 / 2.0f);\n }\n} // End if(!paused)\n/*\n****************************************\nDraw the scene\n****************************************\n*/\n```\n\n在玩家死亡后，前面的代码所做的第一件事就是将`paused`设置为`true`。现在，循环将完成此帧，并且在玩家启动新游戏之前不会再次运行循环的更新部分。\n\n然后，我们将墓碑移动到靠近玩家站立的位置，并将玩家精灵隐藏在屏幕外。\n\n我们将`messageText`的字符串设置为`\"Squished!!\"`，然后使用通常的技术将其置于屏幕中央。\n\n您现在可以运行游戏并真正玩它。下面的屏幕截图显示了球员的最终得分和墓碑，以及**压扁的**信息：\n\n![](img/B14278_05_03.jpg)\n\n还有一个问题要处理。只是我，还是有点安静？\n\n# 简单的声音效果\n\n在本节中，我们将添加三种声音。每种声音都将在特定的游戏事件中播放，即，当玩家切碎时发出简单的砰砰声，当玩家用完时间时发出黯淡的失败声，当玩家被压死时发出复古的粉碎声。\n\n## SFML 声音是如何工作的\n\nSFML 使用两个不同的类播放声音效果。第一节课是`SoundBuffer`课。这个类保存声音文件中的实际音频数据。由`SoundBuffer`负责将`.wav`文件加载到 PC 的 RAM 中，其格式可以在无需进一步解码的情况下播放。\n\n当我们在一分钟内为声音效果编写代码时，我们将看到，一旦我们有一个`SoundBuffer`对象，其中存储了我们的声音，我们将创建另一个`Sound`类型的对象。然后我们可以将这个`Sound`对象与`SoundBuffer`对象关联起来。然后，在代码中的适当时刻，我们将能够调用适当的`Sound`对象的`play`函数。\n\n## 何时播放声音\n\n正如我们将很快看到，加载和播放声音的 C++ 代码非常简单。然而，我们需要考虑的是，当我们称为 TytT0 函数时，在我们的代码中，我们将函数调用设为 ORT T1？让我们看看：\n\n*   可通过按下左右光标键来调用印章声音。\n*   死亡之声可以从`if`模块中播放，该模块检测到一棵树损坏了玩家。\n*   可以从检测`timeRemaining`是否小于零的`if`块播放超时声音。\n\n现在，我们可以编写声音代码了。\n\n## 添加声音代码\n\n首先，我们将添加另一个`#include`指令，使 SFML 声音相关类可用。添加以下突出显示的代码：\n\n```cpp\n#include <sstream>\n#include <SFML/Graphics.hpp>\n#include <SFML/Audio.hpp>\nusing namespace sf;\n```\n\n现在，我们将声明三个不同的`SoundBuffer`对象，将三个不同的声音文件加载到其中，并将三个`Sound`类型的不同对象与`SoundBuffer`类型的相关对象相关联。添加以下突出显示的代码：\n\n```cpp\n// Control the player input\nbool acceptInput = false;\n// Prepare the sounds\n// The player chopping sound\nSoundBuffer chopBuffer;\nchopBuffer.loadFromFile(\"sound/chop.wav\");\nSound chop;\nchop.setBuffer(chopBuffer);\n// The player has met his end under a branch\nSoundBuffer deathBuffer;\ndeathBuffer.loadFromFile(\"sound/death.wav\");\nSound death;\ndeath.setBuffer(deathBuffer);\n// Out of time\nSoundBuffer ootBuffer;\nootBuffer.loadFromFile(\"sound/out_of_time.wav\");\nSound outOfTime;\noutOfTime.setBuffer(ootBuffer);\nwhile (window.isOpen())\n{\n```\n\n现在，我们可以播放我们的第一个音效。将以下单行代码添加到`if`块，该块检测到播放机已按下右光标键：\n\n```cpp\n// Wrap the player controls to\n// Make sure we are accepting input\nif (acceptInput)\n{\n    // More code here next...\n\n    // First handle pressing the right cursor key\n    if (Keyboard::isKeyPressed(Keyboard::Right))\n    {\n        // Make sure the player is on the right\n        playerSide = side::RIGHT;\n\n        score++ ;\n        timeRemaining += (2 / score) + .15;\n        spriteAxe.setPosition(AXE_POSITION_RIGHT,\n            spriteAxe.getPosition().y);\n        spritePlayer.setPosition(1120, 660);\n        // update the branches\n        updateBranches(score);\n\n        // set the log flying to the left\n        spriteLog.setPosition(800, 600);\n        logSpeedX = -5000;\n        logActive = true;\n        acceptInput = false;\n // Play a chop sound\n chop.play();\n    }\n```\n\n提示\n\n将完全相同的代码添加到以`if (Keyboard::isKeyPressed(Keyboard::Left))`开头的下一个代码块的末尾，当玩家在树的左侧砍树时发出劈啪声。\n\n查找处理玩家时间不足的代码，并添加以下突出显示的代码以播放与时间不足相关的音效：\n\n```cpp\nif (timeRemaining <= 0.f) {\n    // Pause the game\n    paused = true;\n    // Change the message shown to the player\n    messageText.setString(\"Out of time!!\");\n    //Reposition the text based on its new size\n    FloatRect textRect = messageText.getLocalBounds();\n    messageText.setOrigin(textRect.left +\n        textRect.width / 2.0f,\n        textRect.top +\n        textRect.height / 2.0f);\n    messageText.setPosition(1920 / 2.0f, 1080 / 2.0f);\n // Play the out of time sound\n outOfTime.play();\n}\n```\n\n最后，要在玩家被压扁时播放死亡之声，请将以下突出显示的代码添加到`if`块，该块在底部分支与玩家位于同一侧时执行：\n\n```cpp\n// has the player been squished by a branch?\nif (branchPositions[5] == playerSide)\n{\n    // death\n    paused = true;\n    acceptInput = false;\n\n    // Draw the gravestone\n    spriteRIP.setPosition(675, 660);\n    // hide the player\n    spritePlayer.setPosition(2000, 660);\n    messageText.setString(\"SQUISHED!!\");\n    FloatRect textRect = messageText.getLocalBounds();\n    messageText.setOrigin(textRect.left +\n        textRect.width / 2.0f,\n        textRect.top + textRect.height / 2.0f);\n    messageText.setPosition(1920 / 2.0f, 1080 / 2.0f);\n // Play the death sound\n death.play();\n}\n```\n\n就这样！我们已经完成了第一场比赛。在进入第二个项目之前，让我们讨论一些可能的增强。\n\n# 改进游戏和代码\n\n看看这些建议的木材增强！！！项目您可以在下载包的`Runnable`文件夹中看到正在运行的增强功能：\n\n*   **加快代码速度：**我们的代码中有一部分正在减慢我们的游戏速度。这对于这个简单的游戏来说并不重要，但是我们可以通过将`sstream`代码放入一个只偶尔执行的块中来加快速度。毕竟，我们不需要每秒更新数千次分数！\n*   **调试控制台：**让我们再添加一些文本，以便查看当前的帧速率。就像分数一样，我们不需要经常更新。每一百帧一次就可以了。\n*   **在背景中添加更多的树：**只需添加更多的树精灵，并在任何看起来合适的位置绘制它们（一些靠近相机，一些远离相机）。\n*   **提高 HUD 文本的可视性：**我们可以在分数和 FPS 计数器后面绘制简单的`RectangleShape`对象。带点透明度的黑色看起来很不错。\n*   **提高云代码的效率：**正如我们已经多次提到的，我们可以利用我们对数组的了解来缩短云代码。\n\n看一看游戏，游戏中有额外的树、云和透明的文本背景：\n\n![](img/B14278_05_04.jpg)\n\n要查看这些增强功能的代码，请查看下载包的`Timber Enhanced Version` 文件夹。\n\n# 总结\n\n在本章中，我们为木材添加了画龙点睛和图形！！！游戏如果在这本书之前，你从来没有编码过一行 C++，那么你可以在后面给自己一个大的拍子。在短短的五章中，你已经从零知识变成了一个有效的游戏。\n\n然而，我们不会恭喜自己太久，因为在下一章，我们将直接转向一些稍微多的硬核 C++。而下一个游戏，一个简单的乒乓球游戏，在某些方面比木材更简单！！，学习编写我们自己的课程将为我们构建更复杂、功能更丰富的游戏做好准备。\n\n# 常见问题\n\nQ） 我承认云的阵列解决方案更有效。但是我们真的需要三个单独的阵列吗？一个用于活动，一个用于速度，一个用于精灵本身？\n\nA） 如果我们查看各种对象的属性/变量，例如，`Sprite`对象，我们会发现它们数量众多。精灵还有位置、颜色、大小、旋转等。但如果他们有`active`、`speed`或者更多，那就太完美了。问题是，SFML 的编码人员不可能预测我们希望使用他们的`Sprite`类的所有方式。幸运的是，我们可以自己上课。我们可以创建一个名为`Cloud`的类，它的布尔值表示`active`和`int`表示速度。我们甚至可以给我们的`Cloud`类一个 SFML`Sprite`对象。然后我们可以进一步简化我们的云代码。我们将在下一章中介绍如何设计自己的类。"
  },
  {
    "path": "docs/begin-cpp-game-prog/06.md",
    "content": "# 六、面向对象编程——启动乒乓球游戏\n\n在本章中，有相当多的理论，但这些理论将为我们提供开始使用具有一定专业知识的**面向对象编程**（**OOP**）所需的知识。此外，我们不会浪费任何时间把这个理论很好地应用，因为我们将使用它来编写下一个项目，一个乒乓球游戏。我们将在幕后了解如何通过编码一个类来创建可以用作对象的新类型。首先，我们将看一个简化的乒乓球场景，这样我们可以了解一些基本的类，然后我们将再次开始，并使用我们所学的原则编写一个真实的乒乓球游戏。\n\n在本章中，我们将介绍以下主题：\n\n*   使用假设的`Bat`类了解 OOP 和类\n*   开始编写乒乓球游戏，编写一个真实的类来表示玩家的球拍\n\n# 哎呀\n\n面向对象编程是一种编程范式，我们可以认为它几乎是代码的标准方式。的确，有一些非面向对象的编码方式，甚至还有一些非面向对象的游戏编码语言/库。然而，因为我们是从零开始的，所以没有理由以任何其他方式做事情。\n\nOOP 将执行以下操作：\n\n*   使我们的代码更易于管理、更改或更新\n*   使我们的代码编写更快、更可靠\n*   使之能够轻松使用其他人的代码（就像我们使用 SFML 一样）\n\n我们已经看到了第三个好处。让我们讨论一下 OOP 到底是什么。\n\nOOP 是一种编程方式，它将我们的需求分解成比整体更易于管理的块。每个块都是自包含的，但可能被其他程序重用，同时与其他块一起作为一个整体工作。这些块就是我们所说的对象。\n\n当我们计划和编码一个对象时，我们使用**类**来完成。\n\n提示\n\n类可以被认为是对象的蓝图。\n\n我们实现了一个类的对象*。这称为类的**实例**。想想房子的蓝图。你不能住在里面，但你可以用它盖房子。您可以构建房子的一个实例。通常，当我们为游戏设计类时，我们编写类来表示现实世界中的*事物*。在下一个项目中，我们将为玩家控制的球拍和玩家可以用球拍在屏幕上弹起的球编写类。然而，OOP 不仅仅是这样。*\n\n提示\n\nOOP 是一种*做事方式，一种定义最佳实践的方法。*\n\nOOP 的三个核心原则是**封装**、**多态**和**继承**。这听起来可能很复杂，但是，一步一步来，这是相当简单的。\n\n## 封装\n\n**封装**意味着确保代码的内部工作不会受到使用它的代码的干扰。您可以通过只允许访问您选择的变量和函数来实现这一点。这意味着您的代码始终可以在不影响使用它的程序的情况下进行更新、扩展或改进，前提是仍然以相同的方式访问暴露的部分。\n\n例如，通过适当的封装，SFML 团队是否需要更新其`Sprite`类的工作方式无关紧要。如果函数签名保持不变，它们就不必担心内部发生了什么。我们在更新之前编写的代码在更新之后仍然有效。\n\n## 多态性\n\n**多态性**允许我们编写更少依赖于我们试图操纵的*类型*的代码。这将使我们的代码更清晰、更高效。多态性表示*不同的形式*。如果我们编写的对象可以是多种类型的东西，那么我们可以利用这一点。在这一点上，多态性听起来可能有点像黑魔法。我们将在第四个项目中使用多态性，我们将从[*第 14 章*](14.html#_idTextAnchor292)、*抽象和代码管理开始，更好地利用 OOP*。一切都会变得更加清晰。\n\n## 继承\n\n就像听起来一样，**继承**意味着我们可以利用其他人的类的所有特性和好处，包括封装和多态性，同时根据我们的情况进一步细化他们的代码。我们将在使用多态性的同时首次使用继承。\n\n## 为什么要使用 OOP？\n\n如果编写正确，OOP 允许您添加新功能，而不必担心它们如何与现有功能交互。当您确实需要更改一个类时，它的自包含（封装）特性意味着对程序的其他部分的影响较小，甚至可能为零。\n\n您可以使用其他人的代码（如 SFML 类），而不知道或甚至不关心其内部的工作方式。\n\n面向对象编程（OOP）和扩展的 SFML 允许您编写使用复杂概念的游戏，如多摄像头、多人游戏、OpenGL、定向声音等等，而且无需费吹灰之力。\n\n通过使用继承，您可以创建一个类的多个类似但不同的版本，而无需从头开始创建该类。\n\n由于多态性，您仍然可以将用于原始对象类型的函数用于新对象。\n\n所有这些都很有道理。正如我们所知，C++ 从一开始就设计了所有的 OOP。\n\n提示\n\n除了成功的决心外，OOP 和制作游戏（或任何其他类型的应用）的最终成功关键在于规划和设计。它不是仅仅“知道”所有 C++、SFML 和 OOP 主题，它们将帮助您编写伟大的代码，而是应用所有这些知识来编写结构良好/设计好的代码。本书中的代码以适合于学习游戏上下文中各种 C++ 主题的顺序和方式呈现。构建代码的艺术和科学称为**设计模式**。随着代码变得越来越长、越来越复杂，有效地使用设计模式将变得越来越重要。好消息是我们不需要自己发明这些设计模式。随着我们的项目变得越来越复杂，我们需要了解它们。随着我们的项目变得越来越复杂，我们的设计模式也将不断发展。\n\n在这个项目中，我们将学习和使用基本类和封装。随着本书的进展，我们将变得更大胆，并使用继承、多态性和其他面向对象编程相关的 C++ 特性。\n\n## 什么是课堂？\n\n类是一组代码，它可以包含函数、变量、循环以及其他我们已经了解到的 C++ 语法。每个新类将在其自己的`.h`代码文件中以与该类相同的名称声明，而其函数将在其自己的`.cpp`文件中定义。\n\n一旦我们编写了一个类，我们就可以使用它从中生成任意多的对象。记住，类就是蓝图，我们根据蓝图创建对象。房子不是蓝图，就像对象不是类一样。由类*制成的对象。*\n\n提示\n\n可以将对象视为变量，将类视为类型。\n\n当然，关于 OOP 和类的讨论，我们实际上还没有看到任何代码。我们现在来解决这个问题。\n\n# 乒乓球拍理论\n\n下面是一个假设性的讨论，我们可以通过编写一个 Bat 类来使用 OOP 开始 Pong 项目。不要在项目中添加任何代码，因为为了解释理论，下面的内容过于简化了。在本章后面，我们将对其进行真正的编码。当我们真正开始编写课程时，实际上会有很大的不同，但我们将在这里学习的原则将为我们的成功做好准备。\n\n我们将从探索变量和函数作为类的一部分开始。\n\n## 类变量和函数声明\n\n一个能弹起球的球棒是第一个优秀的班级候选人。\n\n提示\n\n如果你不知道什么是乒乓球，那么看看这个链接：[https://en.wikipedia.org/wiki/Pong](https://en.wikipedia.org/wiki/Pong) 。\n\n让我们来看看一个假设的 AutoT0x 文件：\n\n```cpp\nclass Bat\n{\n    private:\n        // Length of the pong bat\n        int m_Length = 100;\n        // Height of the pong bat\n        int m_Height = 10;\n        // Location on x axis\n        int m_XPosition;      \n        // Location on y axis\n        int m_YPosition;      \n    public:\n        void moveRight();\n        void moveLeft();\n};\n```\n\n乍一看，代码可能看起来有点复杂，但在对其进行解释后，我们会发现很少有概念没有涉及。\n\n首先要注意的是，使用`class`关键字声明一个新类，后跟该类的名称，整个声明用大括号括起来，后跟一个结束分号：\n\n```cpp\nclass Bat\n{\n    …\n    …\n};\n```\n\n现在，让我们看看变量声明及其名称：\n\n```cpp\n// Length of the pong bat\nint m_Length = 100; \n// Height of the pong bat\nint m_Height = 10;\n// Location on x axis\nint m_XPosition;      \n// Location on y axis\nint m_YPosition;\n```\n\n所有名称的前缀均为`m_`。这个`m_`前缀不是强制性的，但它是一个很好的惯例。声明为类的一部分的变量称为**成员变量**。在处理成员变量时，使用`m_`作为前缀可以让事情变得简单。当我们为类编写函数时，我们将开始看到局部（非成员）变量和参数。`m_`公约将证明自己是有用的。\n\n另外，请注意，所有变量都位于代码中以`private:`关键字为首的部分。浏览前面的代码，注意类代码主体分为两部分：\n\n```cpp\nprivate:\n    // more code here\npublic:\n    // More code here\n```\n\n`public`和`private`关键字控制我们类的封装。类的实例/对象的用户不能直接访问私有的任何内容。如果您正在设计一个供其他人使用的类，您不希望他们能够随意更改任何内容。请注意，成员变量不必是私有的，但只要有可能，就可以通过将其私有化来实现良好的封装。\n\n这意味着我们的四个成员变量（`m_Length`、`m_Height`、`m_XPosition`和`m_YPosition`）无法通过我们的游戏引擎从`main`函数直接访问。它们只能通过类的代码间接访问。这是实际的封装。对于`m_Length`和`m_Height`变量，只要我们不需要改变蝙蝠的大小，这是相当容易接受的。然而，需要访问`m_XPosition`和`m_YPosition`成员变量，或者我们究竟将如何移动蝙蝠？\n\n本规范`public:`部分解决了该问题，具体如下：\n\n```cpp\nvoid moveRight();\nvoid moveLeft();\n```\n\n该类提供两个公共函数，可用于`Bat`类型的对象。当我们查看这些函数的定义时，我们将看到这些函数是如何精确地操作私有变量的。\n\n总之，我们有一系列无法访问的（私有）变量，无法从`main`函数中使用。这很好，因为封装使我们的代码不那么容易出错，并且更易于维护。然后，我们通过提供两个公共函数来间接访问`m_XPosition`和`m_YPosition`变量，从而解决移动 bat 的问题。\n\n`main`函数中的代码可以使用类的实例调用这些函数，但函数中的代码可以精确控制变量的使用方式。\n\n让我们看一下函数定义。\n\n## 类函数定义\n\n我们将在本书中编写的函数定义将全部放在一个单独的文件中，以说明类和函数声明。我们将使用与类同名的文件和`.cpp`文件扩展名。例如，下面的代码将放在一个名为`Bat.cpp`的文件中。请看以下代码，其中只有一个新概念：\n\n```cpp\n#include \"Bat.h\"\nvoid Bat::moveRight()\n{\n    // Move the bat a pixel to the right\n    xPosition ++ ;\n}\nvoid Bat::moveLeft()\n{\n    // Move the bat a pixel to the left\n    xPosition --;\n}\n```\n\n首先要注意的是，我们必须使用 include 指令来包含来自`Bat.h`文件的类和函数声明。\n\n我们在这里看到的新概念是使用**范围解析运算符**、`::`。由于函数属于一个类，我们必须在函数名前面加上类名以及`::`来编写签名部分。`void Bat::moveLeft()`和`void Bat::moveRight`。\n\n重要提示\n\n实际上，我们之前已经简单地看到了作用域解析操作符，也就是说，每当我们声明一个类的对象时，我们之前没有使用过`using namespace..`。\n\n请注意，我们可以将函数定义和声明放在一个文件中，如下所示：\n\n```cpp\nclass Bat\n{\n    private:\n        // Length of the pong bat\n        int m_Length = 100; \n        // Length of the pong bat\n        int m_Height = 10;\n        // Location on x axis\n        int m_XPosition;      \n        // Location on y axis\n        int m_YPosition;      \n    public:\n        void Bat::moveRight()\n        {\n            // Move the bat a pixel to the right\n            xPosition ++ ;\n        }\n        void Bat::moveLeft()\n        {\n            // Move the bat a pixel to the left\n            xPosition --;\n        }\n};\n```\n\n但是，当我们的类变长时（就像我们的第一个 Zombie Arena 类一样），将函数定义分离到它们自己的文件中会更有条理。此外，头文件被认为是“公共的”，如果其他人将使用我们编写的代码，头文件通常用于文档目的。\n\n但是一旦我们编写了类，我们如何使用它呢？\n\n## 使用类的实例\n\n尽管我们看到了所有与类相关的代码，但实际上我们并没有使用该类。我们已经知道如何做到这一点，因为我们已经多次使用了 SFML 类。\n\n首先，我们将创建一个`Bat`类的实例，如下所示：\n\n```cpp\nBat bat;\n```\n\n`bat`对象包含我们在`Bat.h`中声明的所有变量。我们无法直接访问它们。然而，我们可以使用蝙蝠的公共功能移动蝙蝠，如下所示：\n\n```cpp\nbat.moveLeft();\n```\n\n或者我们可以这样移动它：\n\n```cpp\nbat.moveRight();\n```\n\n记住`bat`*是*`Bat`，因此它拥有所有成员变量和所有可用函数。\n\n稍后，我们可能会决定让我们的乒乓球游戏成为多人游戏。在`main`功能中，我们可以更改代码，使游戏有两个蝙蝠，可能如下所示：\n\n```cpp\nBat bat;\nBat bat2;\n```\n\n认识到`Bat`的每一个实例都是具有自己的变量集的独立对象，这一点至关重要。初始化类实例的方法还有很多，下一步，当我们将`Bat`类编码为 real 时，我们将看到一个例子。\n\n现在，我们可以开始真正的项目了。\n\n# 创建 Pong 项目\n\n由于建立一个项目是一个复杂的过程，我们将通过它一步一步，就像我们做了木材！！！项目我不会给你看我为木材做的截图！！！项目，但过程是相同的，所以翻转回到 Po.T0\\. Tyl T1 第 1 章 AUTT2。\n\n1.  启动 Visual Studio 并单击**创建新项目**按钮。或者，如果你还有木材！！！项目打开，可选择**文件****新建项目**。\n2.  在接下来显示的窗口中，选择**控制台应用**并点击**下一步**按钮。然后您将看到**配置新项目**窗口。\n3.  在**配置新项目**窗口中，在**项目****名称**字段中键入`Pong`。请注意，这会导致 Visual Studio 自动配置**解决方案名称**字段，使其具有相同的名称。\n4.  在**位置**字段中，浏览到我们在第 1 章中创建的`VS Projects`文件夹。喜欢木材！！！项目，这将是我们所有项目文件的保存位置。\n5.  选中选项**将解决方案和项目放在同一目录**中。\n6.  完成这些步骤后，单击**创建**。这个项目是由 VisualStudio 生成的，包括一些在前面的文件中的 C++ 代码。\n7.  现在，我们将配置该项目以使用我们放在`SFML`文件夹中的 SFML 文件。从主菜单中选择**项目****乒乓球属性…**。在此阶段，您应该打开**Pong 属性页**窗口。\n8.  在**Pong 属性页**窗口中，从**配置：**下拉列表中选择**所有配置**。\n9.  现在，从左侧菜单中选择**C/C++**，然后选择**General**。\n10.  之后，找到**附加包含目录**编辑框，键入 SFML 文件夹所在的驱动器号，然后键入`\\SFML\\include`。如果您在 D 驱动器上找到了`SFML`文件夹，则键入的完整路径为`D:\\SFML\\include`。如果在其他驱动器上安装了 SFML，请更改路径。\n11.  点击**应用**保存您目前的配置。\n12.  现在，仍在同一窗口中，执行以下步骤。从左侧菜单中选择**链接器**，然后选择**常规**。\n13.  现在，找到**附加库目录**编辑框，键入`SFML`文件夹所在的驱动器号，然后键入`\\SFML\\lib`。因此，如果您在 D 驱动器上找到了您的`SFML`文件夹，那么输入的完整路径是`D:\\SFML\\lib`。如果在其他驱动器上安装了 SFML，请更改路径。\n14.  点击**应用**保存您目前的配置。\n15.  接下来，仍然在同一窗口中，执行以下步骤。切换**配置：**下拉菜单至**调试**，因为我们将在调试模式下运行和测试 Pong。\n16.  选择**连接器**，然后**输入**。\n17.  找到**附加依赖项**编辑框，点击最左侧的编辑框。现在，复制并粘贴/键入以下内容：`sfml-graphics-d.lib;sfml-window-d.lib;sfml-system-d.lib;sfml-network-d.lib;sfml-audio-d.lib;`。请格外小心，将光标精确地放置在编辑框当前内容的开头，以免覆盖已有的任何文本。\n18.  点击**确定**。\n19.  点击**应用**，然后点击**确定**。\n20.  现在，我们需要将 SFML`.dll`文件复制到主项目目录中。我的主要项目目录是`D:\\VS Projects\\Pong`。它是由 VisualStudio 在前面的步骤中创建的。如果您将您的`VS Projects`文件夹放在其他地方，则改为在那里执行此步骤。我们需要复制到项目文件夹中的文件位于我们的`SFML\\bin`文件夹中。为两个位置中的每个位置打开一个窗口，并突出显示`SFML\\bin`文件夹中的所有文件。\n21.  现在，将突出显示的文件复制粘贴到项目文件夹中，即`D:\\VS Projects\\Pong`。\n\n我们现在已经配置了项目属性并准备就绪。\n\n我们将在这个游戏中为 HUD（平视显示器）显示一些文本，显示玩家的分数和剩余寿命。为此，我们需要一种字体。\n\n重要提示\n\n从[下载此免费个人使用字体 http://www.dafont.com/theme.php?cat=302](http://www.dafont.com/theme.php?cat=302) 并解压缩下载。或者可以随意使用您选择的字体。当我们加载字体时，您只需要对代码做一些小的更改。\n\n在`VS Projects\\Pong`文件夹中新建一个名为`fonts`的文件夹，并将`DS-DIGIT.ttf`文件添加到`VS Projects\\Pong\\fonts`文件夹中。\n\n我们现在准备好编码我们的第一个 C++ 类。\n\n# 编码蝙蝠类\n\n简单的 pongbat 示例是介绍类基础知识的一个好方法。类可以简单而简短，就像前面的`Bat`类一样，但它们也可以更长、更复杂，并且包含由其他类生成的其他对象。\n\n当涉及到制作游戏时，假设的`Bat`类中缺少了一些重要的东西。对于所有这些私有成员变量和公共函数来说，这可能很好，但是我们将如何绘制呢？我们的乒乓球拍需要一个精灵，在一些游戏中，它们也需要一个纹理。此外，我们需要一种方法来控制所有游戏对象的动画速率，就像我们在上一个项目中对蜜蜂和云所做的那样。我们可以在类中包含其他对象，其方式与我们在`main.cpp`文件中包含它们的方式完全相同。让我们为`Bat`类编写真实代码，这样我们就可以看到如何解决所有这些问题。\n\n## 编码 Bat.h\n\n首先，我们将对头文件进行编码。右键点击**解决方案浏览器**窗口中的**头文件**，选择**添加**|**新项目**。接下来，选择**头文件（.h）**选项并将新文件命名为`Bat.h`。点击**添加**按钮。我们现在已经准备好对文件进行编码。\n\n将以下代码添加到`Bat.h`：\n\n```cpp\n#pragma once\n#include <SFML/Graphics.hpp>\nusing namespace sf;\nclass Bat\n{\nprivate:\n    Vector2f m_Position;\n    // A RectangleShape object\n    RectangleShape m_Shape;\n    float m_Speed = 1000.0f;\n    bool m_MovingRight = false;\n    bool m_MovingLeft = false;\npublic:\n    Bat(float startX, float startY);\n    FloatRect getPosition();\n    RectangleShape getShape();\n    void moveLeft();\n    void moveRight();\n    void stopLeft();\n    void stopRight();\n    void update(Time dt);\n};\n```\n\n首先，注意文件顶部的`#pragma once`声明。这将防止编译器多次处理该文件。随着我们的游戏变得越来越复杂，可能有几十个类，这将加快编译时间。\n\n注意成员变量的名称以及函数的参数和返回类型。我们有一个名为`m_Position`的`Vector2f`，它将保持球员球棒的水平和垂直位置。我们还有一个 SFML`RectangleShape`，它将是屏幕上显示的实际蝙蝠。\n\n有两个布尔成员将跟踪蝙蝠当前移动的方向（如果有），我们有一个名为`m_Speed`的`float`，告诉我们当玩家决定向左或向右移动蝙蝠时，蝙蝠每秒可以移动的像素数。\n\n代码的下一部分需要一些解释，因为我们有一个名为`Bat`的函数；这与类的名称完全相同。这称为构造函数。\n\n## 构造函数\n\n当一个类被编码时，编译器会创建一个特殊的函数。我们在代码中没有看到这个函数，但它确实存在。它被称为构造函数。如果我们使用假设的`Bat`类示例，就会调用该函数。\n\n当我们需要编写一些代码来准备要使用的对象时，通常在构造函数中是一个很好的地方。当我们希望构造函数除了简单地创建一个实例之外做任何事情时，我们必须替换编译器提供的默认（看不见的）构造函数。这就是我们将使用`Bat`构造函数所做的。\n\n请注意，`Bat`构造函数接受两个`float`参数。当我们第一次创建`Bat`对象时，这非常适合初始化屏幕上的位置。还要注意，构造函数没有返回类型，甚至没有`void`。\n\n我们将很快使用构造函数`Bat`将此游戏对象置于其起始位置。请记住，此函数是在声明`Bat`类型的对象时调用的。\n\n## 继续 Bat.h 解释\n\n接下来是`getPosition`函数，它返回一个`FloatRect`，四个点定义一个矩形。然后，我们有`getShape`，它返回一个`RectangleShape`。这将用于我们可以返回到主游戏循环`m_Shape`，以便绘制。\n\n我们还有`moveLeft`、`moveRight`、`stopLeft`和`stopRight`功能，用于控制蝙蝠是否、何时以及朝哪个方向运动。\n\n最后，我们有`update`函数，它接受一个`Time`参数。此函数将用于计算如何在每帧移动球棒。由于球棒和球的移动方式都非常不同，因此将移动代码封装在类中是有意义的。在`main`函数中，我们将在游戏的每一帧调用`update`函数一次。\n\n提示\n\n您可能会猜到`Ball`类也会有一个`update`函数。\n\n现在，我们可以编写`Bat.cpp`，它将实现所有定义并使用成员变量。\n\n## 编码 Bat.cpp\n\n让我们创建文件，然后开始讨论代码。右键单击解决方案资源管理器窗口中的**源文件**文件夹。现在，选择 AuthT3 的 C++ 文件（.CPP）AUTT4，并在 AUTT5 的名称中输入 AutoT0}：点击**添加**按钮，将为我们创建新文件。\n\n我们将此文件的代码分为两部分，以使讨论更简单。\n\n首先，对`Bat`构造函数进行编码，如下所示：\n\n```cpp\n#include \"Bat.h\"\n\n// This the constructor and it is called when we create an object\nBat::Bat(float startX, float startY)\n{\n    m_Position.x = startX;\n    m_Position.y = startY;\n\n    m_Shape.setSize(sf::Vector2f(50, 5));\n    m_Shape.setPosition(m_Position);\n}\n```\n\n在前面的代码中，我们可以看到我们包含了`bat.h`文件。这使得我们可以使用之前在`bat.h`中声明的所有函数和变量。\n\n我们实现构造函数是因为我们需要做一些工作来设置实例，而编译器提供的默认的看不见的空构造函数是不够的。请记住，构造函数是我们初始化`Bat`实例时运行的代码。\n\n请注意，我们使用`Bat::Bat`语法作为函数名，以明确我们使用的是`Bat`类中的`Bat`函数。\n\n此构造函数接收两个`float`值`startX`和`startY`。接下来我们将这些值分配给`m_Position.x`和`m_Position.y`。名为`m_Position`的`Vector2f`现在保存传入的值，因为`m_Position`是一个成员变量，所以这些值可以在整个类中访问。但是，请注意，`m_Position`已声明为`private`，无论如何，在我们的`main`函数文件中无法直接访问。我们将看看如何尽快解决这个问题。\n\n最后，在构造函数中，我们通过设置其大小和位置来初始化名为`m_Shape`的`RectangleShape`。这与我们在乒乓球理论*一节中对假设的`Bat`类进行编码的方式不同。SFML`Sprite`类具有方便的大小和位置变量，我们可以使用`setSize`和`setPosition`函数访问这些变量，因此我们不再需要假设的`m_Length`和`m_Height`。*\n\n此外，请注意，我们需要改变初始化`Bat`类的方式（与假设的`Bat`类相比），以适合我们的自定义构造函数。\n\n我们需要实现`Bat`类的其余五个函数。在我们刚才讨论的构造函数之后，在`Bat.cpp`中添加以下代码：\n\n```cpp\nFloatRect Bat::getPosition()\n{\n    return m_Shape.getGlobalBounds();\n}\nRectangleShape Bat::getShape()\n{\n    return m_Shape;\n}\nvoid Bat::moveLeft()\n{\n     m_MovingLeft = true;\n}\nvoid Bat::moveRight()\n{\n    m_MovingRight = true;\n}\nvoid Bat::stopLeft()\n{\n    m_MovingLeft = false;\n}\nvoid Bat::stopRight()\n{\n    m_MovingRight = false;\n}\nvoid Bat::update(Time dt)\n{\n    if (m_MovingLeft) {\n        m_Position.x -= m_Speed * dt.asSeconds();\n    }\n    if (m_MovingRight) {\n        m_Position.x += m_Speed * dt.asSeconds();\n    }\n    m_Shape.setPosition(m_Position);\n}\n```\n\n让我们看一下刚才添加的代码。\n\n首先，我们有`getPosition`功能。它所做的只是向调用它的代码返回一个`FloatRect`。代码的`m_Shape.getGlobalBounds`行返回一个用`RectangleShape`四个角的坐标初始化的`FloatRect`，即`m_Shape`。当我们确定球是否击中球棒时，我们将从`main`函数调用此函数。\n\n接下来是`getShape`函数。此函数所做的只是将一份`m_Shape`传递给调用代码。这是必要的，以便我们可以在`main`函数中绘制蝙蝠。当我们编写公共函数的唯一目的是从类传回私有数据时，我们称之为 getter 函数。\n\n现在，我们可以看看`moveLeft`、`moveRight`、`stopLeft`和`stopRight`函数。他们所做的就是适当地设置`m_MovingLeft`和`m_MovingRight`布尔变量，以便跟踪玩家当前的意图。但是请注意，它们不会对决定位置的`RectangleShape`实例或`FloatRect`实例执行任何操作。这正是我们需要的。\n\n`Bat`类中的最后一个函数是`update`。我们将在游戏的每一帧调用此函数一次。随着我们的游戏项目变得越来越复杂，`update`功能将变得越来越复杂。现在，我们需要做的就是调整`m_Position`，这取决于玩家是向左移动还是向右移动。请注意，用于进行此调整的公式与我们用于更新木材中的蜜蜂和云的公式相同！！！项目代码将速度乘以增量时间，然后将其与位置相加或相减。这会导致蝙蝠相对于帧更新所用的时间移动。接下来，代码用`m_Position`中的最新值设置`m_Shape`的位置。\n\n在我们的`Bat`类中有`update`函数而不是`main`函数是封装。而不是更新所有游戏对象在`main`功能中的位置，就像我们在木材中做的一样！！！项目中，每个对象将负责更新自己。然而，正如我们接下来要做的，我们将从`main`函数调用这个`update`函数。\n\n# 使用 Bat 类并对主要功能进行编码\n\n切换到创建项目时自动生成的`main.cpp`文件。删除所有自动生成的代码并添加下面的代码。\n\n将`Pong.cpp`文件编码如下：\n\n```cpp\n#include \"Bat.h\"\n#include <sstream>\n#include <cstdlib>\n#include <SFML/Graphics.hpp>\nint main()\n{\n    // Create a video mode object\n    VideoMode vm(1920, 1080);\n    // Create and open a window for the game\n    RenderWindow window(vm, \"Pong\", Style::Fullscreen);\n    int score = 0;\n    int lives = 3;\n\n    // Create a bat at the bottom center of the screen\n    Bat bat(1920 / 2, 1080 - 20);\n    // We will add a ball in the next chapter\n    // Create a Text object called HUD\n    Text hud;\n    // A cool retro-style font\n    Font font;\n    font.loadFromFile(\"fonts/DS-DIGI.ttf\");\n    // Set the font to our retro-style\n    hud.setFont(font);\n    // Make it nice and big\n    hud.setCharacterSize(75);\n    // Choose a color\n    hud.setFillColor(Color::White);\n    hud.setPosition(20, 20);\n    // Here is our clock for timing everything\n    Clock clock;\n    while (window.isOpen())\n    {\n        /*\n        Handle the player input\n        ****************************\n        ****************************\n        ****************************\n        */\n        /*\n        Update the bat, the ball and the HUD\n        *****************************\n        *****************************\n        *****************************\n        */\n\n        /*\n        Draw the bat, the ball and the HUD\n        *****************************\n        *****************************\n        *****************************\n        */\n\n    }\n    return 0;\n}\n```\n\n在前面的代码中，结构类似于我们在木材中使用的结构！！！项目但是，第一个例外是，当我们创建`Bat`类的实例时：\n\n```cpp\n// Create a bat\nBat bat(1920 / 2, 1080 - 20);\n```\n\n前面的代码调用构造函数来创建`Bat`类的新实例。该代码传入所需的参数，并允许`Bat`类初始化其在靠近底部屏幕中心的位置。这是我们击球开始的最佳位置。\n\n还请注意，我使用了注释来指示代码的其余部分最终将放置在何处。这一切都在游戏循环中，就像它在森林里一样！！！项目下面是代码的其余部分，只是提醒您：\n\n```cpp\n      /*\n        Handle the player input\n        …\n        /*\n        Update the bat, the ball and the HUD\n        …\n\n        /*\n        Draw the bat, the ball and the HUD\n        …\n```\n\n接下来，将代码添加到`Handle the player input`部分，如下所示：\n\n```cpp\nEvent event;\nwhile (window.pollEvent(event))\n{\n    if (event.type == Event::Closed)\n        // Quit the game when the window is closed\n        window.close();\n}\n// Handle the player quitting\nif (Keyboard::isKeyPressed(Keyboard::Escape))\n{\n    window.close();\n}\n// Handle the pressing and releasing of the arrow keys\nif (Keyboard::isKeyPressed(Keyboard::Left))\n{\n    bat.moveLeft();\n}\nelse\n{\n    bat.stopLeft();\n}\nif (Keyboard::isKeyPressed(Keyboard::Right))\n{\n    bat.moveRight();\n}\nelse\n{\n    bat.stopRight();\n}\n```\n\n前面的代码通过按*退出*键来处理玩家退出游戏的情况，就像在木材中一样！！！项目接下来，有两个`if`–`else`结构处理球员移动球棒。让我们分析这两种结构中的第一种：\n\n```cpp\nif (Keyboard::isKeyPressed(Keyboard::Left))\n{\n    bat.moveLeft();\n}\nelse\n{\n    bat.stopLeft();\n}\n```\n\n上述代码将检测播放机是否按下键盘上的左箭头光标键。如果是，则对`Bat`实例调用`moveLeft`函数。调用此函数时，`true`值设置为`m_MovingLeft`私有布尔变量。但是，如果未按下左箭头键，则调用`stopLeft`函数，并将`m_MovingLeft`设置为`false`。\n\n然后在下一个`if`–`else`代码块中重复相同的过程，以处理玩家按下（或不按下）右箭头键。\n\n接下来，在`Update the bat the ball and the HUD`部分添加以下代码，如下所示：\n\n```cpp\n// Update the delta time\nTime dt = clock.restart();\nbat.update(dt);\n// Update the HUD text\nstd::stringstream ss;\nss << \"Score:\" << score << \"  Lives:\" << lives;\nhud.setString(ss.str());\n```\n\n在前面的代码中，我们使用了与木材完全相同的计时技术！！！project，只是这次，我们在`Bat`实例上调用`update`并传入增量时间。记住，当`Bat`类收到增量时间时，它将使用该值根据先前收到的玩家移动指令和所需的球棒速度移动球棒。\n\n接下来，在`Draw the bat, the ball and the HUD`部分添加以下代码，如下所示：\n\n```cpp\nwindow.clear();\nwindow.draw(hud);\nwindow.draw(bat.getShape());\nwindow.display();\n```\n\n在前面的代码中，我们清除屏幕，为 HUD 绘制文本，并使用`bat.getShape`函数从`Bat`实例中抓取`RectangleShape`实例并将其绘制到屏幕上。最后，我们调用`window.display`，就像我们在上一个项目中所做的一样，将 bat 绘制在其当前位置。\n\n在这个阶段，你可以运行游戏，你会看到 HUD 和蝙蝠。使用箭头/光标键可以平滑地左右移动球棒：\n\n![](img/Image86209.jpg)\n\n恭喜！这是第一类，所有的代码和部署。\n\n# 总结\n\n在本章中，我们了解了 OOP 的基础知识，例如如何编写和使用类，包括利用封装来控制类外的代码如何访问成员变量，但只能以我们希望的程度和方式访问。这就像 SFML 类一样，它允许我们创建和使用`Sprite`和`Text`实例，但只是按照它们设计的使用方式。\n\n如果关于 OOP 和类的一些细节不完全清楚，不要太担心自己。我之所以这样说，是因为我们将在本书余下的时间里对类进行编码，我们使用它们的次数越多，它们就会变得越清晰。\n\n此外，我们有一个工作蝙蝠和一个平视显示器为我们的乒乓球游戏。\n\n在下一章中，我们将对`Ball`类进行编码，并使其在屏幕上跳跃。然后，我们将能够添加碰撞检测并完成游戏。\n\n# 常见问题\n\n问：我已经学习了其他语言，而 OOP 在 C++ 中似乎更简单。这是正确的评估吗？\n\nA） 这是对 OOP 及其基本原理的介绍。还有比这更重要的事情。在本书中，我们将学习更多的 OOP 概念和细节。"
  },
  {
    "path": "docs/begin-cpp-game-prog/07.md",
    "content": "# 七、动态碰撞检测与物理——完成乒乓球游戏\n\n在本章中，我们将编写第二个类。我们将看到，尽管球显然与球棒有很大不同，但我们将使用完全相同的技术将球的外观和功能封装在`Ball`类中，就像我们对球棒和`Bat`类所做的一样。然后，我们将通过编写一些动态碰撞检测和记分代码，为乒乓球游戏添加最后的润色。这听起来可能很复杂，但正如我们所预期的，SFML 将使事情比其他方式简单得多。\n\n本章将介绍以下主题：\n\n*   编写 Ball 类的代码\n*   使用 Ball 类\n*   碰撞检测和计分\n*   运行游戏\n\n我们将从编码表示球的类开始。\n\n# 对 Ball 类进行编码\n\n首先，我们将对头文件进行编码。右键单击解决方案浏览器窗口中的**头文件**，选择**添加**|**新项目**。接下来，选择**头文件（.h）**选项并将新文件命名为`Ball.h`。点击**添加**按钮。现在，我们已经准备好对该文件进行编码。\n\n将以下代码添加到`Ball.h`：\n\n```cpp\n#pragma once\n#include <SFML/Graphics.hpp>\nusing namespace sf;\nclass Ball\n{\nprivate:\n    Vector2f m_Position;    \n    RectangleShape m_Shape;\n    float m_Speed = 300.0f;\n    float m_DirectionX = .2f;\n    float m_DirectionY = .2f;\npublic:\n    Ball(float startX, float startY);\n    FloatRect getPosition();\n    RectangleShape getShape();\n    float getXVelocity();\n    void reboundSides();\n    void reboundBatOrTop();\n    void reboundBottom();\n    void update(Time dt);\n};\n```\n\n您将注意到的第一件事是成员变量与`Bat`类的相似性。位置、外观和速度都有一个成员变量，就像球员的球拍一样，它们是相同的类型（`Vector2f`、`RectangleShape`和`float`）。他们甚至有相同的名字（分别是`m_Position`、`m_Shape`和`m_Speed`）。此类成员变量之间的区别在于，方向由两个`float`变量处理，这两个变量将跟踪水平和垂直运动。这些是`m_DirectionX`和`m_DirectionY`。\n\n请注意，我们将需要编写八个函数来激活这个球。有一个与类同名的构造函数，我们将使用它初始化一个`Ball`实例。有三个函数的名称和用法与`Bat`类相同。它们是`getPosition`、`getShape`和`update`。`getPosition`和`getShape`函数将与`main`函数共享球的位置和外观，并且`main`函数将调用`update`函数，以允许`Ball`类每帧更新一次其位置。\n\n其余功能控制球的移动方向。当检测到屏幕两侧发生碰撞时，将从`main`调用`reboundSides`函数；当球击中球员球棒或屏幕顶部时，将调用`reboundBatOrTop`函数；当球击中屏幕底部时，将调用`reboundBottom`函数。\n\n当然，这些仅仅是声明，所以让我们编写 C++，它实际上在 PosiT0x 文件中完成了工作。\n\n让我们创建文件，然后开始讨论代码。右键单击解决方案资源管理器窗口中的**源文件**文件夹。现在，选择 AuthT3 的 C++ 文件（.CPP）AUTT4，并在 AUTT5 的名称中输入 AutoT0}：点击**添加**按钮，将为我们创建新文件。\n\n将以下代码添加到`Ball.cpp`：\n\n```cpp\n#include \"Ball.h\"\n// This the constructor function\nBall::Ball(float startX, float startY)\n{\n    m_Position.x = startX;\n    m_Position.y = startY;\n    m_Shape.setSize(sf::Vector2f(10, 10));\n    m_Shape.setPosition(m_Position);\n}\n```\n\n在前面的代码中，我们为`Ball` 类头文件添加了所需的`include`指令。与类同名的构造函数接收两个`float`参数，用于初始化`m_Position`成员的`Vector2f`实例。然后使用`setSize`功能调整`RectangleShape`实例的大小，并使用`setPosition`进行定位。正在使用的大小是 10 像素宽和 10 像素高；这是武断的，但效果很好。当然，所使用的位置取自`m_Position Vector2f`实例。\n\n在`Ball.cpp`函数的构造函数下方添加以下代码：\n\n```cpp\nFloatRect Ball::getPosition()\n{\n    return m_Shape.getGlobalBounds();\n}\nRectangleShape Ball::getShape()\n{\n    return m_Shape;\n}\nfloat Ball::getXVelocity()\n{\n    return m_DirectionX;\n}\n```\n\n在前面的代码中，我们正在对`Ball`类的三个 getter 函数进行编码。它们各自向`main`函数返回一些内容。第一个`getPosition`使用`m_Shape`上的`getGlobalBounds`函数返回`FloatRect`实例。这将用于碰撞检测。\n\n`getShape`函数返回`m_Shape`，以便可以在游戏循环的每一帧中绘制它。`getXVelocity`函数告诉`main`函数球的运动方向，我们很快就会知道这对我们有多有用。因为我们不需要得到垂直速度，所以没有相应的`getYVelocity`函数，但是如果我们得到了，添加一个就很简单了。\n\n在刚才添加的代码下面添加以下函数：\n\n```cpp\nvoid Ball::reboundSides()\n{\n    m_DirectionX = -m_DirectionX;\n}\nvoid Ball::reboundBatOrTop()\n{\n    m_DirectionY = -m_DirectionY;\n}\nvoid Ball::reboundBottom()\n{\n    m_Position.y = 0;\n    m_Position.x = 500;\n    m_DirectionY = -m_DirectionY;\n}\n```\n\n在前面的代码中，名称以`rebound…`开头的三个函数处理球与不同位置碰撞时发生的情况。在`reboundSides`功能中，`m_DirectionX`将其值反转，这将产生使正值为负值和负值为正值的效果，从而反转（水平）球的移动方向。`reboundBatOrTop`的作用与`m_DirectionY`完全相同，但与`m_DirectionY`作用相同，其作用是将球垂直移动的方向反转。`reboundBottom`功能将球重新定位在屏幕的顶部中心并向下发送。这正是我们想要的，在球员错过了一个球并且球打到了屏幕底部之后。\n\n最后，对于`Ball`类，添加更新函数，如下所示：\n\n```cpp\nvoid Ball::update(Time dt)\n{\n    // Update the ball's position\n    m_Position.y += m_DirectionY * m_Speed * dt.asSeconds();\n    m_Position.x += m_DirectionX * m_Speed * dt.asSeconds();\n    // Move the ball \n    m_Shape.setPosition(m_Position);\n}\n```\n\n在前面的代码中，`m_Position.y`和`m_Position.x`使用适当的方向速度、速度和当前帧完成所需的时间进行更新。然后使用新更新的`m_Position`值更改`m_Shape RectangleShape`实例所在的位置。\n\n`Ball`课程结束了，让我们付诸行动吧。\n\n# 使用 Ball 类\n\n要将球付诸行动，请添加以下代码以使`main`函数中的`Ball`类可用：\n\n```cpp\n#include \"Ball.h\"\n```\n\n添加以下突出显示的代码行，以使用我们刚刚编写的构造函数声明和初始化`Ball`类的实例：\n\n```cpp\n// Create a bat\nBat bat(1920 / 2, 1080 - 20);\n// Create a ball\nBall ball(1920 / 2, 0);\n// Create a Text object called HUD\nText hud;\n```\n\n添加与突出显示位置完全相同的以下代码：\n\n```cpp\n/*\nUpdate the bat, the ball and the HUD\n****************************************************\n****************************************************\n****************************************************\n*/\n// Update the delta time\nTime dt = clock.restart();\nbat.update(dt);\nball.update(dt);\n// Update the HUD text\nstd::stringstream ss;\nss << \"Score:\" << score << \"    Lives:\" << lives;\nhud.setString(ss.str());\n```\n\n在前面的代码中，我们只需在`ball`实例上调用`update`。球将相应地重新定位。\n\n添加以下突出显示的代码以在游戏循环的每个帧上绘制球：\n\n```cpp\n/*\nDraw the bat, the ball and the HUD\n*********************************************\n*********************************************\n*********************************************\n*/\nwindow.clear();\nwindow.draw(hud);\nwindow.draw(bat.getShape());\nwindow.draw(ball.getShape());\nwindow.display();\n```\n\n在这个阶段，您可以运行游戏，球将在屏幕顶部生成，并开始向屏幕底部下降。然而，它会从屏幕底部消失，因为我们还没有检测到任何碰撞。我们现在来解决这个问题。\n\n# 碰撞检测与评分\n\n不同于木材！！！游戏当我们简单地检查最低位置的一个分支是否与玩家角色在同一侧时，在这个游戏中，我们需要从数学上检查球与球棒的交点，或者球与屏幕四个边中的任何一个的交点。\n\n让我们看看一些可以实现这一点的假设代码，以便了解我们正在做什么。然后，我们将转向 SFML 来为我们解决这个问题。\n\n测试两个矩形相交的代码如下所示。不要使用以下代码。仅用于演示目的：\n\n```cpp\nif(objectA.getPosition().right > objectB.getPosition().left\n    && objectA.getPosition().left < objectB.getPosition().right )\n{    \n    // objectA is intersecting objectB on x axis    \n    // But they could be at different heights    \n\n    if(objectA.getPosition().top < objectB.getPosition().bottom         \n        && objectA.getPosition().bottom > objectB.getPosition().top )\n        {       \n            // objectA is intersecting objectB on y axis as well \n            // Collision detected  \n        } \n}\n```\n\n我们不需要写这段代码；但是，我们将使用 SFML`intersects`函数，该函数适用于`FloatRect`对象。回想或回顾`Bat`和`Ball`课程；它们都有一个`getPosition`函数，返回对象当前位置的`FloatRect`。我们将了解如何使用`getPosition`和`intersects`进行所有碰撞检测。\n\n在主函数的更新部分末尾添加以下突出显示的代码：\n\n```cpp\n/*\nUpdate the bat, the ball and the HUD\n**************************************\n**************************************\n**************************************\n*/\n// Update the delta time\nTime dt = clock.restart();\nbat.update(dt);\nball.update(dt);\n// Update the HUD text\nstd::stringstream ss;\nss << \"Score:\" << score << \"    Lives:\" << lives;\nhud.setString(ss.str());\n// Handle ball hitting the bottom\nif (ball.getPosition().top > window.getSize().y)\n{\n // reverse the ball direction\n ball.reboundBottom();\n // Remove a life\n lives--;\n // Check for zero lives\n if (lives < 1) {\n // reset the score\n score = 0;\n // reset the lives\n lives = 3;\n }\n}\n```\n\n在前面的代码中，第一个`if`条件检查球是否击中屏幕底部：\n\n```cpp\nif (ball.getPosition().top > window.getSize().y)\n```\n\n如果球的顶部位置大于窗口的高度，则球已从球员视图的底部消失。作为响应，`ball.reboundBottom`函数被调用。请记住，在此功能中，球将重新定位在屏幕顶部。此时，玩家已失去一条生命，`lives`变量递减。\n\n第二个`if`条件检查玩家是否已耗尽生命（`lives < 1`。如果是这种情况，分数重置为 0，生命数重置为 3，游戏重新开始。在下一个项目中，我们将学习如何保持和显示玩家的最高分数。\n\n在前面的代码下面添加以下代码：\n\n```cpp\n// Handle ball hitting top\nif (ball.getPosition().top < 0)\n{\n    ball.reboundBatOrTop();\n    // Add a point to the players score\n    score++ ;\n}\n```\n\n在前面的代码中，我们检测到球的顶部撞击屏幕的顶部。当这种情况发生时，球员将获得一分并调用`ball.reboundBatOrTop`，这将反转垂直移动方向并将球送回屏幕底部。\n\n在前面的代码下面添加以下代码：\n\n```cpp\n// Handle ball hitting sides\nif (ball.getPosition().left < 0 || \n    ball.getPosition().left + ball.getPosition().width> window.getSize().x)\n{\n    ball.reboundSides();\n}\n```\n\n在前面的代码中，`if`条件检测到球的左侧与屏幕的左侧碰撞，或球的右侧（左+10）与屏幕的右侧碰撞。在任何一种情况下，都会调用`ball.reboundSides`函数，并反转水平行驶方向。\n\n添加以下代码：\n\n```cpp\n// Has the ball hit the bat?\nif (ball.getPosition().intersects(bat.getPosition()))\n{\n    // Hit detected so reverse the ball and score a point\n    ball.reboundBatOrTop();\n}\n```\n\n在前面的代码中，`intersects`功能用于确定球是否击中球棒。当这种情况发生时，我们使用与屏幕顶部碰撞相同的功能来反转球的垂直移动方向。\n\n# 运行游戏\n\n您现在可以运行游戏并在屏幕上弹起球。当你用球棒击球时，得分会增加，而当你错过它时，生命会减少。当`lives`为 0 时，分数将重置，`lives`将返回到 3，如下所示：\n\n![](img/Image86489.jpg)\n\n# 总结\n\n祝贺这是第二场比赛完成了！我们本可以为该游戏添加更多功能，如合作游戏、高分、音效等，但我只想用最简单的示例介绍类和动态碰撞检测。现在我们在游戏开发者的武库中已经有了这些主题，我们可以进入一个更激动人心的项目和更多的游戏开发主题。\n\n在下一章中，我们将规划僵尸竞技场游戏，了解 SFML`View`类，它作为虚拟摄像机进入我们的游戏世界，并编写更多的类。\n\n# 常见问题\n\nQ） 这场比赛不是有点安静吗？\n\nA） 我没有在这个游戏中添加音效，因为我想在使用我们的第一个类并学习利用时间平滑地设置所有游戏对象的动画时，使代码尽可能短。如果要添加声音效果，则只需将.wav 文件添加到项目中，使用 SFML 加载声音，并在每个碰撞事件中播放声音效果。我们将在下一个项目中这样做。\n\nQ） 游戏太简单了！我怎样才能使球加速一点？\n\nA） 有很多方法可以让游戏更具挑战性。一种简单的方法是在`Ball`类的`reboundBatOrTop`函数中添加一行代码，以提高速度。例如，以下代码将在每次调用函数时将球的速度提高 10%：\n\n```cpp\n// Speed up a little bit on each hit\nm_Speed = m_Speed * 1.1f;\n```\n\n球会很快变快。然后你需要设计一种方法，当玩家失去所有生命时，将速度重置回`300.0f`。您可以在`Ball`类中创建一个新函数，可能称为`resetSpeed`，并在代码检测到玩家已经失去了最后的生命时从`main`开始调用它。"
  },
  {
    "path": "docs/begin-cpp-game-prog/08.md",
    "content": "# 八、SFML 视图——开始僵尸射击游戏\n\n在这个项目中，我们将更多地使用**OOP**并产生强大的效果。我们还将探索 SFML`View`类。这个多才多艺的课程可以让我们轻松地将游戏划分为不同的层次，以适应游戏的不同方面。在僵尸射手项目中，我们将有一个用于 HUD 的层和一个用于主游戏的层。这是必要的，因为当玩家每次清除僵尸浪潮时，游戏世界都会扩大，最终，游戏世界将比屏幕更大，需要滚动。使用`View`类将防止 HUD 的文本与背景一起滚动。在下一个项目中，我们将更进一步，创建一个由 SFML`View`类完成大部分艰苦工作的合作分屏游戏。\n\n这是我们在本章中要做的：\n\n*   规划和启动僵尸竞技场游戏\n*   对`Player`类进行编码\n*   学习 SFML`View`课程\n*   构建僵尸竞技场游戏引擎\n*   让`Player`班开始工作\n\n# 规划并启动僵尸竞技场游戏\n\n在这一点上，如果你还没有，我建议你去看一段超过 9000 个僵尸的视频*（[http://store.steampowered.com/app/273500/](http://store.steampowered.com/app/273500/) 和*深红色土地*（[http://store.steampowered.com/app/262830/](http://store.steampowered.com/app/262830/) 。我们的游戏显然不会像这两个例子中的任何一个那样深入或先进，但我们也将拥有相同的基本功能和游戏机制，例如：*\n\n **   平视显示器（HUD），显示详细信息，如分数、高分和剪辑中的子弹、剩余子弹数、玩家健康状况以及剩余可杀死的僵尸数。\n*   玩家会在疯狂逃离僵尸的同时射杀僵尸。\n*   使用*WASD*键盘键在滚动世界中移动，同时使用鼠标对准枪。\n*   在每个关卡之间，玩家将选择一个“升级”关卡，该关卡将影响玩家获胜所需的游戏方式。\n*   玩家需要收集“拾起物”以恢复生命和弹药。\n*   每一波都会带来更多的僵尸和更大的竞技场，使其更具挑战性。\n\n将有三种类型的僵尸飞溅。它们将具有不同的属性，例如外观、健康和速度。我们称之为追逐者、膨胀者和爬虫。请查看游戏的以下注释屏幕截图，以查看游戏中的一些功能以及组成游戏的组件和资产：\n\n![](img/B14278_08_01.jpg)\n\n以下是关于每个编号点的更多信息：\n\n1.  乐谱和 hi 乐谱。这些以及 HUD 的其他部分将在一个单独的层中绘制，称为视图，并由`View`类的实例表示。hi 分数将被保存并加载到文件中。\n2.  将在竞技场周围建造一堵墙的纹理。该纹理包含在称为**精灵片**的单个图形中，以及其他背景纹理（点**3**、**5**和**6**）。\n3.  来自 sprite 表的两个泥纹理中的第一个。\n4.  这是一个“弹药收集”。当玩家得到这个，他们将得到更多的弹药。还有一个“健康提升”，玩家将从中获得更多的健康。玩家可以在僵尸浪潮之间选择升级这些皮卡。\n5.  草纹理，也来自雪碧表。\n6.  sprite 表中的第二个泥纹理。\n7.  曾经有僵尸的血迹。\n8.  HUD 的底部。从左到右，有一个图标表示弹药、剪辑中的子弹数、备用子弹数、健康栏、当前僵尸数量以及当前僵尸数量。\n9.  球员的性格。\n10.  玩家用鼠标瞄准的十字线。\n11.  一个行动缓慢但强壮的“胖”僵尸。\n12.  一个移动稍快但较弱的“爬虫”僵尸。还有一种“追逐者僵尸”，速度很快，力量也很弱。不幸的是，在他们全部被杀之前，我没能在截图中找到一个。\n\n所以，我们有很多事情要做，还有新的 C++ 技能。让我们从创建一个新项目开始。\n\n## 创建新项目\n\n由于创建项目是一个相对复杂的过程，我将再次详细介绍所有步骤。对于更多的细节和图像，请参阅 OutT0.在 OrtT2 中设置木材项目 AuthT1A.第 1 章，第 4 章，第 5 章，第 6 页，C++，SFML，VisualStudio，并开始第一个游戏 Ty7 T7。\n\n由于建立一个项目是一个复杂的过程，我们将一步一步地进行，就像我们为木材项目所做的那样。我不会给你展示和我一样的木材项目，但是这个过程是一样的，所以回溯到 TytT0。T1 章第 1 章，AtT2。让我们看一下以下步骤：\n\n1.  启动 Visual Studio 并单击**创建新项目**按钮。如果您打开了另一个项目，您可以选择**文件****新项目**。\n2.  在接下来显示的窗口中，选择**控制台应用**并点击**下一步**按钮。然后您将看到**配置新项目**窗口。\n3.  在**配置新项目**窗口中，在**项目****名称**字段中键入`Zombie Arena`。\n4.  在**位置**字段中，浏览到`VS Projects`文件夹。\n5.  选中选项**将解决方案和项目放在同一目录**中。\n6.  完成上述步骤后，单击**创建**。\n7.  现在，我们将配置该项目以使用我们放在`SFML`文件夹中的 SFML 文件。从主菜单中选择**项目****僵尸竞技场属性…**。在此阶段，您应该打开**僵尸竞技场属性页**窗口。\n8.  在**僵尸竞技场属性页**窗口中，执行以下步骤。从**配置：**下拉菜单中选择**所有配置**。\n9.  现在，从左侧菜单中选择**C/C++**，然后选择**General**。\n10.  接下来，找到**附加包含目录**编辑框，键入 SFML 文件夹所在的驱动器号，然后键入`\\SFML\\include`。如果您在 D 驱动器上找到了`SFML`文件夹，则键入的完整路径将为`D:\\SFML\\include`。如果在不同的驱动器上安装了 SFML，请更改路径。\n11.  点击**应用**保存到目前为止的配置。\n12.  现在，仍在同一窗口中，执行以下步骤。从左侧菜单中选择**链接器**，然后选择**常规**。\n13.  现在，找到**附加库目录**编辑框，键入`SFML`文件夹所在的驱动器号，然后键入`\\SFML\\lib`。因此，如果您在 D 驱动器上找到了您的`SFML`文件夹，那么输入的完整路径将是`D:\\SFML\\lib`。如果将 SFML 安装到其他驱动器，请更改路径。\n14.  点击**应用**保存到目前为止的配置。\n15.  接下来，仍然在同一窗口中，执行以下步骤。切换**配置：**下拉菜单到**调试**，因为我们将在调试模式下运行和测试 Pong。\n16.  选择**连接器**，然后**输入**。\n17.  找到**附加依赖项**编辑框并单击最左侧的它。现在复制并粘贴/键入以下内容：`sfml-graphics-d.lib;sfml-window-d.lib;sfml-system-d.lib;sfml-network-d.lib;sfml-audio-d.lib;`。要格外小心，将光标正好放在编辑框当前内容的开头，以免覆盖已有的任何文本。\n18.  点击**确定**。\n19.  点击**应用**，然后点击**确定**。\n\n现在，您已经配置了项目属性，几乎可以开始了。接下来，我们需要按照以下步骤将 SFML`.dll`文件复制到主项目目录中：\n\n1.  我的主要项目目录是`D:\\VS Projects\\Zombie Arena`。此文件夹是由 Visual Studio 在前面的步骤中创建的。如果您将`Projects`文件夹放在其他地方，请在您的目录中执行此步骤。我们需要复制到项目文件夹中的文件位于您的`SFML\\bin`文件夹中。为两个位置中的每一个打开一个窗口，并突出显示所有`.dll`文件。\n2.  现在，将高亮显示的文件复制并粘贴到项目中。\n\n该项目现已建立并准备就绪。接下来，我们将探索并添加项目资产。\n\n## 项目资产\n\n与之前的奥运会相比，本项目中的资产数量更多、种类更多。这些资产包括：\n\n*   屏幕上文本的字体\n*   不同动作的音效，如射击、重新加载或被僵尸击中\n*   角色、僵尸和各种背景纹理的精灵表的图形\n\n游戏所需的所有图形和声音效果都包含在下载包中。它们可以分别在`Chapter 8/graphics`和`Chapter 8/sound`文件夹中找到。\n\n尚未提供所需的字体。这样做是为了避免有关许可证的任何可能的歧义。这不会造成问题，因为将提供下载字体以及如何选择字体以及在何处选择字体的链接。\n\n## 探索资产\n\n图形资源构成了我们的僵尸竞技场游戏场景的一部分。查看以下图形资产；您应该清楚游戏中的资产将用于何处：\n\n![](img/B14278_08_03.jpg)\n\n然而，可能不太明显的是`background_sheet.png`文件，它包含四个不同的图像。这是我们前面提到的精灵表。我们将看到如何使用 SpRITE 表来保存内存并增加游戏的速度，在 Tyl T1 中，To.T2A.第 9 章 AutoT3。\n\n声音文件都是`.wav`格式。这些文件包含触发某些事件时将播放的声音效果。详情如下:\n\n*   `hit.wav`：僵尸与玩家接触时发出的声音。\n*   `pickup.wav`：当玩家碰撞或踏上（收集）健康提升（拾取）时播放的声音。\n*   `powerup.wav`：当玩家在每一波僵尸之间选择一个属性来增加力量（加电）时发出的声音。\n*   `reload.wav`：令人满意的点击，让玩家知道他们已经装载了新的弹药。\n*   `reload_failed.wav`：不太令人满意的声音，表示无法加载新子弹。\n*   `shoot.wav`：枪声。\n*   `splat.wav`：像僵尸被子弹击中的声音。\n\n一旦决定了要使用哪些资产，就应该将它们添加到项目中。\n\n## 将资产添加到项目中\n\n以下说明假设您使用的所有资产都是该书的下载包。当您使用自己的资源时，只需使用相同的文件名将相应的声音或图形文件替换为您自己的文件。让我们来看看这些步骤：\n\n1.  浏览至`D:\\VS Projects\\ZombieArena`。\n2.  在此文件夹中创建三个新文件夹，并将其命名为`graphics`、`sound`和`fonts`。\n3.  从下载包中，将`Chapter 8/graphics`的全部内容复制到`D:\\VS Projects\\ZombieArena\\graphics`文件夹中。\n4.  从下载包中，将`Chapter 6/sound`的全部内容复制到`D:\\VS Projects\\ZombieArena\\sound`文件夹中。\n5.  现在，请访问[http://www.1001freefonts.com/zombie_control.font 在 web 浏览器中下载](http://www.1001freefonts.com/zombie_control.font)并下载**僵尸控件**字体。\n6.  提取压缩下载的内容并将`zombiecontrol.ttf`文件添加到`D:\\VS Projects\\ZombieArena\\fonts`文件夹中。\n\n现在，是时候考虑 OOP 将如何帮助我们完成这个项目，然后我们可以开始为 Zombie Arena 编写代码。\n\n# OOP 与僵尸竞技场项目\n\n我们面临的最初问题是当前项目的复杂性。让我们考虑一下，只有一个僵尸；以下是我们需要使其在游戏中发挥作用的内容：\n\n*   它的水平和垂直位置\n*   它的大小\n*   它所面临的方向\n*   每种僵尸类型都有不同的纹理\n*   精灵\n*   每种僵尸类型的速度不同\n*   每种僵尸类型的运行状况不同\n*   跟踪每个僵尸的类型\n*   碰撞检测数据\n*   它的智能（追逐玩家），对于每种类型的僵尸略有不同\n*   僵尸是活的还是死的指示\n\n这意味着一个僵尸可能需要十几个变量，而管理一个僵尸部落则需要这些变量的整个数组。但是机关枪、皮卡和不同级别的皮卡上的子弹呢？简单的木材！！！而乒乓球游戏也开始变得有点难以管理，很容易猜测，这个更复杂的射击游戏会更糟糕很多倍！\n\n幸运的是，我们将把我们在前两章学到的所有 OOP 技能付诸实践，同时学习一些新的 C++ 技术。\n\n我们将用一个类来表示玩家，以此开始我们的项目编码。\n\n# 打造一流的玩家\n\n让我们想想我们的`Player`课程需要做什么，以及我们需要做什么。学员需要*知道*的移动速度、当前在游戏世界中的位置以及健康状况。在玩家眼中，`Player`类是一个 2D 图形字符，因此该类需要一个`Sprite`对象和一个`Texture`对象。\n\n此外，尽管目前原因可能并不明显，但我们的`Player`课程也将从了解游戏运行的总体环境的一些细节中获益。这些细节包括屏幕分辨率、组成竞技场的瓷砖大小以及当前竞技场的总体大小。\n\n由于`Player`类将全权负责在每个帧中更新自身（就像蝙蝠和球一样），因此它需要知道玩家在任何给定时刻的意图。例如，玩家当前是否按住键盘方向键？或者玩家当前是否按住多个键盘方向键？布尔变量用于确定*W*、*A*、*S*和*D*键的状态，并且是必需的。\n\n很明显，我们需要在新类中选择大量变量。在了解了 OOP 的所有知识之后，我们当然会将所有这些变量都私有化。这意味着我们必须在适当的情况下从`main`功能提供访问。\n\n我们将使用一大堆 getter 函数以及一些函数来设置我们的对象。这些功能相当多。这个类中有 21 个函数。一开始，这似乎有点令人畏惧，但我们将通读它们，并看到它们中的大多数只是设置或获取一个私有变量。\n\n只有几个深入的函数：`update`，它将从`main`函数中每帧调用一次，`spawn`，它将在每次生成玩家时初始化一些私有变量。然而，正如我们将看到的，没有什么复杂的东西，它们都将被详细描述。\n\n最好的方法是对头文件进行编码。这将使我们有机会查看所有私有变量并检查所有函数签名。\n\n提示\n\n请密切注意返回值和参数类型，因为这将使理解函数定义中的代码更加容易。\n\n## 对玩家类头文件进行编码\n\n右键点击**解决方案资源管理器**中的**头文件**并选择**添加【新项目】**。在**添加新项目**窗口中，突出显示**头文件（.h）**，然后在**名称**字段中键入`Player.h`。最后，点击**添加**按钮。我们现在准备为第一个类编写头文件。\n\n通过添加声明开始编码`Player`类，包括开头和结尾的大括号，后跟分号：\n\n```cpp\n#pragma once\n#include <SFML/Graphics.hpp>\nusing namespace sf;\nclass Player\n{\n};\n```\n\n现在，让我们在文件中添加所有私有成员变量。根据我们已经讨论过的内容，看看您是否能够计算出他们每个人将做什么。我们将在稍后逐一介绍：\n\n```cpp\nclass Player\n{\nprivate:\n const float START_SPEED = 200;\n const float START_HEALTH = 100;\n // Where is the player\n Vector2f m_Position;\n // Of course, we will need a sprite\n Sprite m_Sprite;\n // And a texture\n // !!Watch this space – Interesting changes here soon!!\n Texture m_Texture;\n // What is the screen resolution\n Vector2f m_Resolution;\n // What size is the current arena\n IntRect m_Arena;\n // How big is each tile of the arena\n int m_TileSize;\n // Which direction(s) is the player currently moving in\n bool m_UpPressed;\n bool m_DownPressed;\n bool m_LeftPressed;\n bool m_RightPressed;\n // How much health has the player got?\n int m_Health;\n // What is the maximum health the player can have\n int m_MaxHealth;\n // When was the player last hit\n Time m_LastHit;\n // Speed in pixels per second\n float m_Speed;\n// All our public functions will come next\n};\n```\n\n前面的代码声明了所有的成员变量。有些是正则变量，有些是对象。请注意，它们都在类的`private:`部分下，因此不能从类外直接访问。\n\n另外，请注意，我们使用的命名约定是在非常量变量的所有名称前加上前缀`m_`。`m_`前缀将提醒我们，在编码函数定义时，它们是成员变量，不同于我们将在某些函数中创建的局部变量，也不同于函数参数。\n\n使用的所有变量都很简单，例如`m_Position`、`m_Texture`和`m_Sprite`，分别用于玩家的当前位置、纹理和精灵。除此之外，每个变量（或变量组）都会被注释，以使其用法简单明了。\n\n然而，为什么需要它们，以及它们将在什么环境中使用，可能并不那么明显。例如，`m_LastHit`是`Time`类型的对象，用于记录玩家上次收到僵尸攻击的时间。不清楚*为什么*我们可能需要这些信息，但我们将很快进行讨论。\n\n当我们将游戏的其余部分拼凑在一起时，每个变量的上下文将变得更加清晰。现在，重要的是熟悉名称和数据类型，以便在项目的其余部分中轻松完成以下内容。\n\n提示\n\n您不需要记住变量名和类型，因为我们将在使用它们时讨论所有代码。然而，你确实需要花点时间去查看它们，并更加熟悉它们。此外，在我们继续的过程中，如果有什么不清楚的地方，可能值得参考这个头文件。\n\n现在，我们可以添加一个完整的函数列表。添加以下突出显示的代码，看看您是否能够了解这些代码的作用。请密切注意返回类型、参数和每个函数的名称。这是理解我们将在本项目其余部分编写的代码的关键。关于每个功能，他们告诉了我们什么？添加以下突出显示的代码，然后我们将对其进行检查：\n\n```cpp\n// All our public functions will come next\npublic:\n Player();\n void spawn(IntRect arena, Vector2f resolution, int tileSize);\n // Call this at the end of every game\n void resetPlayerStats();\n\n // Handle the player getting hit by a zombie\n bool hit(Time timeHit);\n // How long ago was the player last hit\n Time getLastHitTime();\n // Where is the player\n FloatRect getPosition();\n // Where is the center of the player\n Vector2f getCenter();\n // What angle is the player facing\n float getRotation();\n // Send a copy of the sprite to the main function\n Sprite getSprite();\n // The next four functions move the player\n void moveLeft();\n void moveRight();\n void moveUp();\n void moveDown();\n // Stop the player moving in a specific direction\n void stopLeft();\n void stopRight();\n void stopUp();\n void stopDown();\n // We will call this function once every frame\n void update(float elapsedTime, Vector2i mousePosition);\n // Give the player a speed boost\n void upgradeSpeed();\n // Give the player some health\n void upgradeHealth();\n // Increase the maximum amount of health the player can have\n void increaseHealthLevel(int amount);\n // How much health has the player currently got?\n int getHealth();\n};\n```\n\n首先，请注意，所有功能都是公共的。这意味着我们可以使用`main`函数中的类实例调用所有这些函数，代码如下：\n\n```cpp\nplayer.getSprite();\n```\n\n假设`player`是`Player`类的完全设置实例，前面的代码将返回`m_Sprite`的副本。将此代码放到真实的上下文中，我们可以在`main`函数中编写如下代码：\n\n```cpp\nwindow.draw(player.getSprite());\n```\n\n前面的代码将在正确的位置绘制玩家图形，就像精灵是在`main`函数中声明的一样。这就是我们在 Pong 项目中对`Bat`类所做的。\n\n在我们开始在相应的 To.T0.file 中实现这些函数（即，写定义）之前，让我们依次看看它们中的每一个：\n\n*   `void spawn(IntRect arena, Vector2f resolution, int tileSize)`：此函数按其名称执行。它将准备好对象以供使用，包括将其放在其起始位置（即生成它）。注意，它不返回任何数据，但有三个参数。接收一个名为`arena`的`IntRect`实例，该实例将是当前级别的大小和位置；一个`Vector2f`实例，包含屏幕分辨率；还有一个`int`，它将保持背景瓷砖的大小。\n*   `void resetPlayerStats`：一旦我们赋予玩家在波浪之间升级的能力，我们需要能够在新游戏开始时移除/重置这些能力。\n*   `Time getLastHitTime()`：这个函数只做一件事——它返回玩家最后一次被僵尸击中的时间。我们将在检测碰撞时使用此功能，它将允许我们确保玩家不会因为接触僵尸而频繁受到惩罚*。*\n**   `FloatRect getPosition()`：此函数返回一个`FloatRect`实例，描述矩形的水平和垂直浮点坐标，其中包含玩家图形。这对于碰撞检测也很有用。*   `Vector2f getCenter()`：这与`getPosition`略有不同，因为它是`Vector2f`类型，只包含玩家图形中心的*x*和*y*位置。*   `float getRotation()`：`main`函数中的代码有时需要知道玩家当前面对的方向（以度为单位）。3 点钟为 0 度，顺时针增加。*   `Sprite getSprite()`：如前所述，此函数返回代表玩家的精灵副本。*   `void moveLeft()`、`..Right()`、`..Up()`、`..Down()`：这四个功能没有返回类型和参数。当按下一个或多个*WASD*键时，`main`函数将调用它们，`Player`类将能够动作。*   `void stopLeft()`、`..Right()`、`..Up()`、`..Down()`：这四个功能没有返回类型和参数。它们将从`main`函数调用，`Player`类将能够在一个或多个*WASD*键被释放时进行操作。*   `void update(float elapsedTime, Vector2i mousePosition)`: This will be the only long function of the entire class. It will be called once per frame from `main`. It will do everything necessary to make sure the `player` object's data is updated so that it's ready for collision detection and drawing. Notice that it returns no data but receives the amount of elapsed time since the last frame, along with a `Vector2i` instance, which will hold the horizontal and vertical screen location of the mouse pointer/crosshair.\n\n    重要提示\n\n    请注意，这些是整数屏幕坐标，不同于浮点世界坐标。\n\n    *   `void upgradeSpeed()`：当玩家选择让玩家更快时，可以从升级屏幕调用该功能。*   `void upgradeHealth()`：当玩家选择让玩家更强壮（即更健康）时，可以从升级屏幕调用的另一个功能。*   `void increaseHealthLevel(int amount)`：与之前的功能有一个微妙但重要的区别，即此功能将增加玩家的生命值，达到当前设置的最大值。此功能将在玩家拾取健康拾取时使用。*   `int getHealth()`：由于健康水平是动态的，我们需要能够确定玩家在任何给定时刻的健康水平。此函数返回一个保存该值的`int`。*\n\n *与变量一样，现在应该清楚每个函数的用途。还有*为什么*以及使用其中一些函数的确切上下文，只有在我们进行项目时才会显示出来。\n\n提示\n\n您不需要记住函数名、返回类型或参数，因为我们将在使用它们时讨论代码。但是，您确实需要花时间查看它们，以及前面的解释，并更加熟悉它们。此外，在我们继续的过程中，如果有什么不清楚的地方，可能值得参考这个头文件。\n\n现在，我们可以进入函数的核心部分：定义。\n\n## 对玩家类函数定义进行编码\n\n最后，我们可以开始编写完成类工作的代码。\n\n*在**解决方案资源管理器**中**源文件**上点击*右键，选择**添加【新项目】。。。**。在 AuthT9.中添加新的条目，即“T10”窗口，突出显示（由 To.T11.左键单击 AdT12To on）AutoT13+c++ 文件（.CPP）AUT1414，然后，在 AUTT15 的名称 No.T1616 字段中，键入 TyT0.最后，点击**添加**按钮。\n\n提示\n\n从现在开始，我将简单地要求您创建一个新的类或头文件。因此，将前面的步骤提交到内存中，或者如果需要提醒，请返回此处。\n\n我们现在已经准备好为这个项目中的第一个类编写`.cpp`文件。\n\n下面是必要的 include 指令，后面是构造函数的定义。记住，当我们第一次实例化`Player`类型的对象时，将调用构造函数。将以下代码添加到`Player.cpp`文件中，然后我们可以仔细查看它：\n\n```cpp\n#include \"player.h\"\nPlayer::Player()\n{\n    m_Speed = START_SPEED;\n    m_Health = START_HEALTH;\n    m_MaxHealth = START_HEALTH;\n    // Associate a texture with the sprite\n    // !!Watch this space!!\n    m_Texture.loadFromFile(\"graphics/player.png\");\n    m_Sprite.setTexture(m_Texture);\n    // Set the origin of the sprite to the center, \n    // for smooth rotation\n    m_Sprite.setOrigin(25, 25);\n}\n```\n\n在构造函数中（当然，它与类同名且没有返回类型），我们编写代码开始设置`Player`对象，以备使用。\n\n要清楚；当我们从`main`函数编写以下代码时，将运行此代码：\n\n```cpp\nPlayer player;\n```\n\n现在不要添加前一行代码。\n\n我们在构造函数中所做的就是根据它们的相关常量初始化`m_Speed`、`m_Health`和`m_MaxHealth`。然后，我们将玩家图形加载到`m_Texture`中，将`m_Texture`与`m_Sprite`关联，并将`m_Sprite`的原点设置到中心`(25, 25)`。\n\n提示\n\n请注意隐晦的注释`// !!Watch this space!!`，这表示我们将返回到纹理的加载以及一些与之相关的重要问题。我们最终会改变我们如何处理这种纹理，一旦我们发现了问题，并学到了更多的 C++。我们将在[*第 10 章*](10.html#_idTextAnchor214)*、指针、标准模板库和纹理管理*中进行。\n\n接下来，我们将对`spawn`函数进行编码。我们将只创建一个`Player`类的实例。然而，我们需要为每一个波将其生成到当前级别。这就是`spawn`函数将为我们处理的内容。将以下代码添加到`Player.cpp`文件中，确保检查详细信息并阅读注释：\n\n```cpp\nvoid Player::spawn(IntRect arena, \n        Vector2f resolution, \n        int tileSize)\n{\n    // Place the player in the middle of the arena\n    m_Position.x = arena.width / 2;\n    m_Position.y = arena.height / 2;\n    // Copy the details of the arena \n    // to the player's m_Arena\n    m_Arena.left = arena.left;\n    m_Arena.width = arena.width;\n    m_Arena.top = arena.top;\n    m_Arena.height = arena.height;\n    // Remember how big the tiles are in this arena\n    m_TileSize = tileSize;\n    // Store the resolution for future use\n    m_Resolution.x = resolution.x;\n    m_Resolution.y = resolution.y;\n}\n```\n\n前面的代码首先将`m_Position.x`和`m_Position.y`值初始化为传入`arena`的高度和宽度的一半。这具有将玩家移动到关卡中心的效果，无论其大小。\n\n接下来，我们将传入的`arena`的所有坐标和维度复制到相同类型的成员对象`m_Arena`。当前竞技场的大小和坐标的细节被频繁使用，这样做是有意义的。我们现在可以使用`m_Arena`来完成一些任务，比如确保玩家不能穿墙。除此之外，出于同样的目的，我们将传入的`tileSize`实例复制到成员变量`m_TileSize`。我们将在`update`功能中看到`m_Arena`和`m_TileSize`的作用。\n\n前面代码的最后两行将屏幕分辨率从`Vector2f` `resolution`（是`spawn`的一个参数）复制到`m_Resolution`（是`Player`的一个成员变量）。我们现在可以在`Player`类中访问这些值。\n\n现在，添加非常简单的`resetPlayerStats`函数代码：\n\n```cpp\nvoid Player::resetPlayerStats()\n{\n    m_Speed = START_SPEED;\n    m_Health = START_HEALTH;\n    m_MaxHealth = START_HEALTH;\n}\n```\n\n当玩家死亡时，我们将使用它重置他们可能使用的任何升级。\n\n在项目即将完成之前，我们不会编写调用`resetPlayerStats`函数的代码，但它已经为我们需要它的时候做好了准备。\n\n在代码的下一部分中，我们将再添加两个函数。他们将处理玩家被僵尸击中时发生的事情。我们将能够在当前游戏时间内呼叫`player.hit()`并通过。我们还可以通过调用`player.getLastHitTime()`查询玩家最后一次被击中的时间。当我们有一些僵尸时，这些函数的实用性将变得非常明显。\n\n将两个新的定义添加到 HORT T0 文件中，然后再仔细检查 C++ 代码：\n\n```cpp\nTime Player::getLastHitTime()\n{\n    return m_LastHit;\n}\nbool Player::hit(Time timeHit)\n{\n    if (timeHit.asMilliseconds() \n        - m_LastHit.asMilliseconds() > 200)\n    {\n        m_LastHit = timeHit;\n        m_Health -= 10;\n        return true;\n    }\n    else\n    {\n        return false;\n    }\n}\n```\n\n`getLastHitTime()`的代码非常简单；它将返回`m_LastHit`中存储的任何值。\n\n`hit`功能更加深入细致。首先，`if`语句检查作为参数传入的时间是否比`m_LastHit`中存储的时间提前 200 毫秒。如果是，`m_LastHit`会随着时间的推移而更新，`m_Health`会从其当前值中扣除 10。`if`语句中的最后一行代码是`return true`。注意，`else`子句只是将`false`返回给调用代码。\n\n此功能的总体效果是，每秒最多只能从玩家身上扣除五次生命值。请记住，我们的游戏循环可能以每秒数千次迭代的速度运行。在这种情况下，如果没有此功能提供的限制，僵尸只需与玩家接触一秒钟，就会扣除数万点生命值。`hit`功能控制并限制此现象。它还通过返回`true`或`false`让调用代码知道是否注册了新的命中。\n\n这段代码意味着我们将在`main`函数中检测僵尸和玩家之间的冲突。然后我们将致电`player.hit()`决定是否扣除任何健康积分。\n\n接下来，对于`Player`类，我们将实现一系列 getter 函数。它们允许我们将数据整齐地封装在`Player`类中，同时将其值提供给`main`函数。\n\n在上一个块的后面添加以下代码：\n\n```cpp\nFloatRect Player::getPosition()\n{\n    return m_Sprite.getGlobalBounds();\n}\nVector2f Player::getCenter()\n{\n    return m_Position;\n}\nfloat Player::getRotation()\n{\n    return m_Sprite.getRotation();\n}\nSprite Player::getSprite()\n{\n    return m_Sprite;\n}\nint Player::getHealth()\n{\n    return m_Health;\n}\n```\n\n前面的代码非常简单。前面五个函数中的每一个都返回一个成员变量的值。仔细查看每一个函数，熟悉哪个函数返回哪个值。\n\n接下来的八个短函数启用键盘控件（我们将从`main`函数中使用），以便我们可以更改`Player`类型的对象中包含的数据。将以下代码添加到`Player.cpp`文件中，然后我们将总结其工作原理：\n\n```cpp\nvoid Player::moveLeft()\n{\n    m_LeftPressed = true;\n}\nvoid Player::moveRight()\n{\n    m_RightPressed = true;\n}\nvoid Player::moveUp()\n{\n    m_UpPressed = true;\n}\nvoid Player::moveDown()\n{\n    m_DownPressed = true;\n}\nvoid Player::stopLeft()\n{\n    m_LeftPressed = false;\n}\nvoid Player::stopRight()\n{\n    m_RightPressed = false;\n}\nvoid Player::stopUp()\n{\n    m_UpPressed = false;\n}\nvoid Player::stopDown()\n{\n    m_DownPressed = false;\n}\n```\n\n前面的代码有四个函数（`moveLeft`、`moveRight`、`moveUp`、`moveDown`），它们将相关的布尔变量（`m_LeftPressed`、`m_RightPressed`、`m_UpPressed`、`m_DownPressed`设置为`true`。其他四个函数（`stopLeft`、`stopRight`、`stopUp`和`stopDown`的作用正好相反，将相同的布尔变量设置为`false`。`Player`类的实例现在可以随时了解*WASD*键中哪些被按下，哪些没有被按下。\n\n下面的函数是完成所有艰苦工作的函数。`update`函数将在游戏循环的每一帧中调用一次。添加以下代码，然后我们将详细检查它。如果我们按照前面的八个功能进行操作，并且还记得我们是如何为木材制作云和蜜蜂的动画的！！！项目和乒乓球的球拍和球，我们可能会理解以下大部分代码：\n\n```cpp\nvoid Player::update(float elapsedTime, Vector2i mousePosition)\n{\n    if (m_UpPressed)\n    {\n        m_Position.y -= m_Speed * elapsedTime;\n    }\n    if (m_DownPressed)\n    {\n        m_Position.y += m_Speed * elapsedTime;\n    }\n    if (m_RightPressed)\n    {\n        m_Position.x += m_Speed * elapsedTime;\n    }\n    if (m_LeftPressed)\n    {\n        m_Position.x -= m_Speed * elapsedTime;\n    }\n    m_Sprite.setPosition(m_Position);\n    // Keep the player in the arena\n    if (m_Position.x > m_Arena.width - m_TileSize)\n    {\n        m_Position.x = m_Arena.width - m_TileSize;\n    }\n    if (m_Position.x < m_Arena.left + m_TileSize)\n    {\n        m_Position.x = m_Arena.left + m_TileSize;\n    }\n    if (m_Position.y > m_Arena.height - m_TileSize)\n    {\n        m_Position.y = m_Arena.height - m_TileSize;\n    }\n    if (m_Position.y < m_Arena.top + m_TileSize)\n    {\n        m_Position.y = m_Arena.top + m_TileSize;\n    }\n    // Calculate the angle the player is facing\n    float angle = (atan2(mousePosition.y - m_Resolution.y / 2,\n        mousePosition.x - m_Resolution.x / 2)\n        * 180) / 3.141;\n    m_Sprite.setRotation(angle);\n}\n```\n\n前面代码的第一部分移动玩家精灵。四个`if`语句检查哪些与运动相关的布尔变量（`m_LeftPressed`、`m_RightPressed`、`m_UpPressed`或`m_DownPressed`为真，并相应地更改`m_Position.x`和`m_Position.y`。还使用前两个项目中相同的公式计算移动量：\n\n**位置（+或-）速度*经过的时间。**\n\n在这四条`if`语句之后，调用`m_Sprite.setPosition`并传入`m_Position`。精灵现在已经被调整到一帧的正确数量。\n\n接下来的四个`if`语句检查`m_Position.x`或`m_Position.y`是否超出当前竞技场的任何边缘。请记住，当前竞技场的范围存储在`m_Arena`中的`spawn`函数中。让我们看看这四个`if`语句中的第一个，以便理解它们：\n\n```cpp\nif (m_Position.x > m_Arena.width - m_TileSize)\n{\n    m_Position.x = m_Arena.width - m_TileSize;\n}\n```\n\n前面的代码测试`m_position.x`是否大于`m_Arena.width`，减去瓷砖的大小（`m_TileSize`。我们将在创建背景图形时看到，此计算将检测到玩家误入墙中。\n\n当`if`语句为真时，`m_Arena.width - m_TileSize`计算用于初始化`m_Position.x`。这意味着玩家图形的中心永远无法偏离右侧墙的左侧边缘。\n\n接下来的三个`if`语句，即我们刚才讨论的那个语句之后的三个`if`语句，除了对其他三堵墙做同样的事情。\n\n前面代码中的最后两行计算并设置玩家精灵旋转的角度（即朝向）。这行代码看起来可能有点复杂，但它只是简单地使用十字线的位置（`mousePosition.x`和`mousePosition.y`）以及屏幕的中心（`m_Resolution.x`和`m_Resolution.y`）在一个经过尝试和测试的三角函数中。\n\n`atan`如何使用这些坐标以及 Pi（3.141）是相当复杂的，这就是为什么它被包装在一个方便的函数中。\n\n重要提示\n\n如果您想更详细地研究三角函数，可以在此处进行：[http://www.cplusplus.com/reference/cmath/](http://www.cplusplus.com/reference/cmath/) 。\n\n我们将为`Player`类添加的最后三个函数分别使玩家的速度提高 20%，使玩家的生命值增加 20%，并使玩家的生命值增加传入的数量。\n\n在`Player.cpp`文件的末尾添加以下代码，我们将仔细查看：\n\n```cpp\nvoid Player::upgradeSpeed()\n{\n    // 20% speed upgrade\n    m_Speed += (START_SPEED * .2);\n}\nvoid Player::upgradeHealth()\n{\n    // 20% max health upgrade\n    m_MaxHealth += (START_HEALTH * .2);\n}\nvoid Player::increaseHealthLevel(int amount)\n{\n    m_Health += amount;\n    // But not beyond the maximum\n    if (m_Health > m_MaxHealth)\n    {\n        m_Health = m_MaxHealth;\n    }\n}\n```\n\n在前面的代码中，`upgradeSpeed()`和`upgradeHealth()`函数分别增加`m_Speed`和`m_MaxHealth`中存储的值。通过将起始值乘以.2 并将其添加到当前值，这些值将增加 20%。当玩家在两个关卡之间选择他们想要改善的角色属性（即升级）时，这些函数将从`main`函数调用。\n\n`increaseHealthLevel()`函数从`amount`参数中的`main`获取`int`值。这个`int`值将由一个名为`Pickup`的类提供，我们将在[*第 11 章*](11.html#_idTextAnchor249)*、碰撞检测、皮卡和子弹*中写入。`m_Health`成员变量按传入值增加。然而，对于球员来说有一个陷阱。`if`语句检查`m_Health`是否超过`m_MaxHealth`，如果超过，则将其设置为`m_MaxHealth`。这意味着玩家不能简单地从拾取中获得无限的生命值。相反，他们必须小心地平衡他们在不同级别之间选择的升级。\n\n当然，我们的`Player`类不能做任何事情，除非我们实例化它并将其应用到我们的游戏循环中。在此之前，我们先来看看游戏摄像机的概念。\n\n# 用 SFML 视图控制游戏摄像机\n\n在我看来，SFML`View`类是最整洁的类之一。读完这本书后，当我们在不使用媒体/游戏库的情况下制作游戏时，我们会真正注意到`View`的缺失。\n\nAutoT0.类允许我们考虑我们的游戏在它自己的世界中发生，具有它自己的属性。我是什么意思？嗯，当我们创建一个游戏时，我们通常试图创建一个虚拟世界。虚拟世界很少，如果有的话，是以像素来衡量的，而且很少，如果有的话，这个世界的像素数将与玩家的显示器相同。我们需要一种方法来抽象我们正在构建的虚拟世界，以便它可以是我们喜欢的任何大小或形状。\n\n另一种看待 SFML`View`的方式是将其视为一个摄像头，玩家可以通过它查看虚拟世界的一部分。大多数游戏都会有一个以上的摄像头/世界视图。\n\n例如，考虑一个分屏游戏，两个玩家可以同时在世界的不同地方。\n\n或者，考虑一个游戏，其中有一个小区域的屏幕代表整个游戏世界，但在一个非常高的水平/放大，像一个迷你地图。\n\n即使我们的游戏比前两个例子简单得多，并且不需要分割屏幕或迷你地图，我们也可能希望创建一个比正在玩的屏幕更大的世界。当然，僵尸竞技场就是这样。\n\n此外，如果我们不断移动游戏摄像机来显示虚拟世界的不同部分（通常是跟踪玩家），HUD 会发生什么变化？如果我们绘制分数和其他屏幕上的 HUD 信息，然后我们滚动世界以跟随玩家，分数将相对于该相机移动。\n\nSFML`View`类可以轻松启用所有这些特性，并用非常简单的代码解决这个问题。诀窍是为每个摄像头创建一个`View`实例——可能是迷你地图的`View`实例，滚动游戏世界的`View`实例，然后是 HUD 的`View`实例。\n\n`View`的实例可以根据需要四处移动、调整大小和位置。因此，游戏后的主`View`实例可以跟踪玩家，迷你地图视图可以保持在屏幕的固定、缩小的小角落，HUD 可以覆盖整个屏幕，永远不会移动，尽管主`View`实例可以在玩家去的任何地方。\n\n让我们看一下使用几个`View`实例的一些代码。\n\n提示\n\n此代码用于介绍`View`类。不要将此代码添加到僵尸竞技场项目中。\n\n创建并初始化几个`View`实例：\n\n```cpp\n// Create a view to fill a 1920 x 1080 monitor\nView mainView(sf::FloatRect(0, 0, 1920, 1080));\n// Create a view for the HUD\nView hudView(sf::FloatRect(0, 0, 1920, 1080));\n```\n\n前面的代码创建了两个填充 1920 x 1080 监视器的`View`对象。现在，我们可以让`mainView`变魔术，同时让`hudView`完全独处：\n\n```cpp\n// In the update part of the game\n// There are lots of things you can do with a View\n// Make the view centre around the player                \nmainView.setCenter(player.getCenter());\n// Rotate the view 45 degrees\nmainView.rotate(45)\n// Note that hudView is totally unaffected by the previous code\n```\n\n当我们操纵`View`实例的属性时，我们是这样做的。当我们在视图中绘制精灵、文本或其他对象时，我们必须明确地**将**视图设置为窗口的当前视图：\n\n```cpp\n// Set the current view\nwindow.setView(mainView);\n```\n\n现在，我们可以在该视图中绘制我们想要的所有内容：\n\n```cpp\n// Do all the drawing for this view\nwindow.draw(playerSprite);\nwindow.draw(otherGameObject);\n// etc\n```\n\n玩家可能处于任何坐标；这并不重要，因为`mainView`以图形为中心。\n\n现在，我们可以将 HUD 绘制到`hudView`。请注意，就像我们从后面到前面在层中绘制单个元素（背景、游戏对象、文本等），我们也从后面到前面绘制视图。因此，在主游戏场景后绘制 HUD：\n\n```cpp\n// Switch to the hudView\nwindow.setView(hudView);\n// Do all the drawing for the HUD\nwindow.draw(scoreText);\nwindow.draw(healthBar);\n// etc\n```\n\n最后，我们可以用通常的方式绘制/显示当前帧的窗口及其所有视图：\n\n```cpp\nwindow.display();\n```\n\n提示\n\n如果您想进一步了解 SFML`View`而不是本项目所需的内容，包括如何实现分屏和迷你地图，那么最好的网站指南是 SFML 官方网站：[https://www.sfml-dev.org/tutorials/2.5/graphics-view.php](https://www.sfml-dev.org/tutorials/2.5/graphics-view.php) 。\n\n现在我们已经了解了`View`，我们可以开始编写僵尸竞技场`main`函数，并将我们的第一个`View`实例用于实际。在[*第 12 章*](12.html#_idTextAnchor272)*分层视图和实现 HUD*中，我们将为 HUD 引入第二个`View`实例，并将其分层到主`View`实例的顶部。\n\n# 启动僵尸竞技场游戏引擎\n\n在这个游戏中，我们需要在`main`中稍微升级一下游戏引擎。我们将有一个名为`state`的枚举，它将跟踪游戏的当前状态。然后，在整个`main`中，我们可以包装部分代码，以便在不同的状态下发生不同的事情。\n\n当我们创建项目时，VisualStudio 为我们创建了一个名为`ZombieArena.cpp`的文件。这将是包含我们的`main`函数和实例化和控制所有类的代码的文件。\n\n我们从现在熟悉的`main`函数和一些包含指令开始。注意为`Player`类添加了 include 指令。\n\n将以下代码添加到`ZombieArena.cpp`文件中：\n\n```cpp\n#include <SFML/Graphics.hpp>\n#include \"Player.h\"\nusing namespace sf;\nint main()\n{\n    return 0;\n}\n```\n\n前面的代码中没有什么新内容，除了`#include \"Player.h\"`行意味着我们现在可以在代码中使用`Player`类。\n\n让我们充实一下我们的游戏引擎。下面的代码做了很多工作。在添加代码时，请务必阅读注释，以了解正在发生的事情。然后我们将更详细地讨论它。\n\n在`main`功能开始处添加以下突出显示的代码：\n\n```cpp\nint main()\n{\n // The game will always be in one of four states\n enum class State { PAUSED, LEVELING_UP, \n GAME_OVER, PLAYING };\n\n // Start with the GAME_OVER state\n State state = State::GAME_OVER;\n // Get the screen resolution and \n // create an SFML window\n Vector2f resolution;\n resolution.x = \n VideoMode::getDesktopMode().width;\n resolution.y = \n VideoMode::getDesktopMode().height;\n RenderWindow window(\n VideoMode(resolution.x, resolution.y), \n \"Zombie Arena\", Style::Fullscreen);\n // Create a an SFML View for the main action\n View mainView(sf::FloatRect(0, 0, \n resolution.x, resolution.y));\n // Here is our clock for timing everything\n Clock clock;\n // How long has the PLAYING state been active\n Time gameTimeTotal;\n // Where is the mouse in \n // relation to world coordinates\n Vector2f mouseWorldPosition;\n // Where is the mouse in \n // relation to screen coordinates\n Vector2i mouseScreenPosition;\n // Create an instance of the Player class\n Player player;\n // The boundaries of the arena\n IntRect arena;\n // The main game loop\n while (window.isOpen())\n {\n\n }\n    return 0;\n}\n```\n\n让我们浏览一下我们输入的所有代码的每个部分。在`main`函数中，我们有以下代码：\n\n```cpp\n// The game will always be in one of four states\nenum class State { PAUSED, LEVELING_UP, GAME_OVER, PLAYING };\n// Start with the GAME_OVER state\nState state = State::GAME_OVER;\n```\n\n前面的代码创建了一个名为`State`的新枚举类。然后，代码创建一个名为`state`的`State`类实例。`state`枚举现在可以是声明中定义的四个值之一。这些值分别为`PAUSED`、`LEVELING_UP`、`GAME_OVER`和`PLAYING`。这四个值正是我们跟踪和响应游戏在任何给定时间可能处于的不同状态所需要的。请注意，`state`不可能一次保存多个值。\n\n紧接着，我们添加了以下代码：\n\n```cpp\n// Get the screen resolution and create an SFML window\nVector2f resolution;\nresolution.x = VideoMode::getDesktopMode().width;\nresolution.y = VideoMode::getDesktopMode().height;\nRenderWindow window(VideoMode(resolution.x, resolution.y), \n    \"Zombie Arena\", Style::Fullscreen);\n```\n\n前面的代码声明了一个名为`resolution`的`Vector2f`实例。我们通过调用`width`和`height`的`VideoMode::getDesktopMode`函数来初始化`resolution`（`x`和`y`的两个成员变量。`resolution`对象现在拥有游戏运行的监视器的分辨率。最后一行代码使用适当的分辨率创建一个名为`window`的新`RenderWindow`实例。\n\n下面的代码创建一个 SFML`View`对象。视图（最初）定位在监视器像素的精确坐标处。如果我们使用这个`View`在当前位置绘制一些图形，它将与绘制没有视图的窗口相同。然而，我们最终将开始将这一观点转移到玩家需要看到的游戏世界的各个部分。然后，当我们开始使用第二个`View`实例时，它保持不变（对于 HUD），我们将看到这个`View`实例如何跟踪动作，而另一个实例保持不变以显示 HUD：\n\n```cpp\n// Create a an SFML View for the main action\nView mainView(sf::FloatRect(0, 0, resolution.x, resolution.y));\n```\n\n接下来，我们创建了一个`Clock`实例来进行计时，并创建了一个名为`gameTimeTotal`的`Time`对象，该对象将保存已过的游戏总时间。随着项目的进展，我们还将引入更多变量和对象来处理时间安排：\n\n```cpp\n// Here is our clock for timing everything\nClock clock;\n// How long has the PLAYING state been active\nTime gameTimeTotal;\n```\n\n下面的代码声明了两个向量：一个包含两个`float`变量，称为`mouseWorldPosition`，另一个包含两个整数，称为`mouseScreenPosition`。鼠标指针有点反常，因为它存在于两个不同的坐标空间中。如果我们愿意，我们可以把它们想象成平行宇宙。首先，当玩家在世界各地移动时，我们需要跟踪十字线在那个世界的位置。这些将是浮点坐标，并将存储在`mouseWorldCoordinates`中。当然，监视器本身的实际像素坐标永远不会改变。对于水平分辨率-1，垂直分辨率-1，它们将始终为 0,0。我们将使用`mouseScreenPosition`中存储的整数跟踪鼠标指针相对于该坐标空间的位置：\n\n```cpp\n// Where is the mouse in relation to world coordinates\nVector2f mouseWorldPosition;\n// Where is the mouse in relation to screen coordinates\nVector2i mouseScreenPosition;\n```\n\n最后，我们开始使用`Player`类。这行代码将导致构造函数（`Player::Player`）执行。如果您想刷新有关此功能的记忆，请参阅`Player.cpp`：\n\n```cpp\n// Create an instance of the Player class\nPlayer player;\n```\n\n此`IntRect`对象将保持起始水平和垂直坐标，以及宽度和高度。一旦初始化，我们将能够访问当前竞技场的大小和位置详细信息，代码为`arena.left`、`arena.top`、`arena.width`和`arena.height`：\n\n```cpp\n// The boundaries of the arena\nIntRect arena;\n```\n\n当然，我们前面添加的代码的最后一部分是我们的游戏循环：\n\n```cpp\n// The main game loop\nwhile (window.isOpen())\n{\n}\n```\n\n我们可能已经注意到代码变得相当长。我们将在下一节讨论这一不便之处。\n\n# 管理代码文件\n\n使用类和函数进行抽象的优点之一是可以减少代码文件的长度（行数）。尽管我们将在这个项目中使用十几个代码文件，`ZombieArena.cpp`中的代码长度在接近结尾时仍然会变得有点笨拙。在最后一个项目“太空入侵者++”中，我们将研究更多抽象和管理代码的方法。\n\n现在，请使用此技巧使事情易于管理。请注意，Visual Studio 中代码编辑器的左侧有几个**+**和**-**符号，其中一个如下图所示：\n\n![](img/B14278_08_04.jpg)\n\n代码的每个块（`if`、`while`、`for`等）将有一个符号。您可以通过点击**+**和**-**标志来展开和折叠这些块。我建议保留所有当前未讨论的代码。这将使事情更加清楚。\n\n此外，我们可以创建自己的可折叠块。我建议在主游戏循环开始之前用所有代码制作一个可折叠块。要执行此操作，请高亮显示代码，然后*右键单击*并选择**概述****隐藏选择**，如下图所示：\n\n![](img/B14278_08_05.jpg)\n\n现在，您可以点击**-**和**+**标志来展开和折叠区块。每次我们在主游戏循环之前添加代码（这将是非常常见的），您可以展开代码，添加新行，然后再次折叠它。以下屏幕截图显示了代码折叠时的外观：\n\n![](img/B14278_08_06.jpg)\n\n这比以前更容易管理。现在，我们可以从主游戏循环开始。\n\n# 开始对主游戏循环进行编码\n\n如您所见，前面代码的最后一部分是游戏循环（`while (window.isOpen()){}`。我们现在将关注这一点。具体来说，我们将对游戏循环的输入处理部分进行编码。\n\n我们将要添加的代码相当长。不过，这并没有什么复杂的地方，我们将在稍后对其进行全面检查。\n\n将以下突出显示的代码添加到游戏循环中：\n\n```cpp\n// The main game loop\nwhile (window.isOpen())\n{\n /*\n ************\n Handle input\n ************\n */\n // Handle events by polling\n Event event;\n while (window.pollEvent(event))\n {\n if (event.type == Event::KeyPressed)\n { \n // Pause a game while playing\n if (event.key.code == Keyboard::Return &&\n state == State::PLAYING)\n {\n state = State::PAUSED;\n }\n // Restart while paused\n else if (event.key.code == Keyboard::Return &&\n state == State::PAUSED)\n {\n state = State::PLAYING;\n // Reset the clock so there isn't a frame jump\n clock.restart();\n }\n // Start a new game while in GAME_OVER state\n else if (event.key.code == Keyboard::Return &&\n state == State::GAME_OVER)\n {\n state = State::LEVELING_UP;\n }\n if (state == State::PLAYING)\n {\n }\n }\n }// End event polling\n}// End game loop\n```\n\n在前面的代码中，我们实例化了一个`Event`类型的对象。我们将使用`event`，就像在以前的项目中一样，对系统事件进行轮询。为此，我们将上一个块中的其余代码包装在一个带有`window.pollEvent(event)`条件的`while`循环中。这将继续循环每个帧，直到没有更多的事件要处理。\n\n在这个`while`循环中，我们处理我们感兴趣的事件。首先，我们测试`Event::KeyPressed`事件。如果在游戏处于`PLAYING`状态时按下*返回*键，则我们将`state`切换到`PAUSED`。\n\n如果在游戏处于`PAUSED`状态时按下*返回*键，则我们将`state`切换到`PLAYING`并重新启动`clock`对象。我们从`PAUSED`切换到`PLAYING`后重新启动`clock`的原因是，当游戏暂停时，经过的时间仍在累积。如果我们不重新启动时钟，我们的所有对象都会更新它们的位置，就好像帧刚刚花了很长时间一样。当我们充实此文件中的其余代码时，这一点将变得更加明显。\n\n然后我们有一个`else if`块来测试在游戏处于`GAME_OVER`状态时是否按下了`Return`键。如果是，则将`state`更改为`LEVELING_UP`。\n\n重要提示\n\n注意，`GAME_OVER`状态是显示主屏幕的状态。所以，`GAME_OVER`状态是玩家刚死后，玩家第一次运行游戏时的状态。玩家在每场比赛中首先要做的事情就是选择一个属性进行改进（即升级）。\n\n在前面的代码中，有一个最终的`if`条件来测试状态是否等于`PLAYING`。此`if`块为空，我们将在整个项目中向其添加代码。\n\n提示\n\n在整个项目中，我们将向该文件的许多不同部分添加代码。因此，花时间了解我们的游戏可能处于的不同状态以及我们在哪里处理它们是值得的。在适当的时候折叠和扩展不同的`if`、`else`和`while`块也是非常有益的。\n\n花些时间彻底熟悉我们刚刚编码的`while`、`if`和`else if`块。我们会定期转介他们。\n\n接下来，在前面的代码之后，仍然在游戏循环中，仍然在处理输入，添加下面突出显示的代码。请注意，现有代码（未突出显示）准确显示了新代码（突出显示）的位置：\n\n```cpp\n    }// End event polling\n // Handle the player quitting\n if (Keyboard::isKeyPressed(Keyboard::Escape))\n {\n window.close();\n }\n // Handle WASD while playing\n if (state == State::PLAYING)\n {\n // Handle the pressing and releasing of the WASD keys\n if (Keyboard::isKeyPressed(Keyboard::W))\n {\n player.moveUp();\n }\n else\n {\n player.stopUp();\n }\n if (Keyboard::isKeyPressed(Keyboard::S))\n {\n player.moveDown();\n }\n else\n {\n player.stopDown();\n }\n if (Keyboard::isKeyPressed(Keyboard::A))\n {\n player.moveLeft();\n }\n else\n {\n player.stopLeft();\n }\n if (Keyboard::isKeyPressed(Keyboard::D))\n {\n player.moveRight();\n }\n else\n {\n player.stopRight();\n }\n }// End WASD while playing\n}// End game loop\n```\n\n在前面的代码中，我们首先测试玩家是否按下了*退出*键。如果按下，游戏窗口将关闭。\n\n接下来，在一个大的`if(state == State::PLAYING)`区块内，我们依次检查每个*WASD*键。如果按下一个键，我们调用相应的`player.move...`函数。如果不是，我们调用相关的`player.stop...`函数。\n\n此代码确保在每一帧中，玩家对象将使用按下和未按下的*WASD*键进行更新。`player.move...`和`player.stop...`函数将信息存储在成员布尔变量（`m_LeftPressed`、`m_RightPressed`、`m_UpPressed`和`m_DownPressed`中。然后，`Player`类在`player.update`函数的每一帧中响应这些布尔值，我们将在游戏循环的更新部分调用这些函数。\n\n现在，我们可以处理键盘输入，让玩家在每场比赛开始时以及每场比赛之间升级。添加并研究以下突出显示的代码，然后我们将对其进行讨论：\n\n```cpp\n    }// End WASD while playing\n // Handle the LEVELING up state\n if (state == State::LEVELING_UP)\n {\n // Handle the player LEVELING up\n if (event.key.code == Keyboard::Num1)\n {\n state = State::PLAYING;\n }\n if (event.key.code == Keyboard::Num2)\n {\n state = State::PLAYING;\n }\n if (event.key.code == Keyboard::Num3)\n {\n state = State::PLAYING;\n }\n if (event.key.code == Keyboard::Num4)\n {\n state = State::PLAYING;\n }\n if (event.key.code == Keyboard::Num5)\n {\n state = State::PLAYING;\n }\n if (event.key.code == Keyboard::Num6)\n {\n state = State::PLAYING;\n }\n\n if (state == State::PLAYING)\n { \n // Prepare the level\n // We will modify the next two lines later\n arena.width = 500;\n arena.height = 500;\n arena.left = 0;\n arena.top = 0;\n // We will modify this line of code later\n int tileSize = 50;\n // Spawn the player in the middle of the arena\n player.spawn(arena, resolution, tileSize);\n\n // Reset the clock so there isn't a frame jump\n clock.restart();\n }\n }// End LEVELING up\n\n}// End game loop\n```\n\n在前面的代码中，我们处理键盘键*1*、*2*、*3*、*4*、*5*和*6*，测试`state`的当前值是否等于`LEVELING_UP`。在`if`块中，我们只需将`state`设置为`State::PLAYING`。我们将在后面的[*第 13 章*](13.html#_idTextAnchor279)*中添加一些代码来处理每个升级选项、音效、文件 I/O 和完成游戏*。\n\n此代码执行以下操作：\n\n1.  如果`state`等于`LEVELING_UP`，等待*1*、*2*、*3*、*4*、*5*或*6*按键按下。\n2.  按下时，将`state`更改为`PLAYING`。\n3.  当状态改变时，仍然在`if (state == State::LEVELING_UP)`块内，嵌套的`if(state == State::PLAYING)`块将运行。\n4.  在该块中，我们设置`arena`的位置和大小，将`tileSize`设置为`50`，将所有信息传递给`player.spawn`，并调用`clock.restart`。\n\n现在，我们有了一个实际生成的玩家对象，它知道自己的环境，可以响应按键。现在，我们可以在每次通过循环时更新场景。\n\n确保从游戏循环的输入处理部分整齐地折叠代码，因为我们现在已经完成了。下面的代码位于游戏循环的更新部分。添加并研究以下突出显示的代码，然后我们可以讨论它：\n\n```cpp\n    }// End LEVELING up\n /*\n ****************\n UPDATE THE FRAME\n ****************\n */\n if (state == State::PLAYING)\n {\n // Update the delta time\n Time dt = clock.restart();\n\n // Update the total game time\n gameTimeTotal += dt;\n\n // Make a decimal fraction of 1 from the delta time\n float dtAsSeconds = dt.asSeconds();\n // Where is the mouse pointer\n mouseScreenPosition = Mouse::getPosition();\n // Convert mouse position to world coordinates of mainView\n mouseWorldPosition = window.mapPixelToCoords(\n Mouse::getPosition(), mainView);\n // Update the player\n player.update(dtAsSeconds, Mouse::getPosition());\n // Make a note of the players new position\n Vector2f playerPosition(player.getCenter());\n\n // Make the view centre around the player \n mainView.setCenter(player.getCenter());\n }// End updating the scene\n\n}// End game loop\n```\n\n首先，注意前面的代码被包装在一个测试中，以确保游戏处于`PLAYING`状态。如果游戏已暂停、已结束或玩家正在选择升级内容，我们不希望此代码运行。\n\n首先，我们重新启动时钟并将前一帧的时间存储在`dt`变量中：\n\n```cpp\n// Update the delta time\nTime dt = clock.restart();\n```\n\n接下来，我们将前一帧的时间添加到游戏运行的累计时间中，如`gameTimeTotal`所示：\n\n```cpp\n// Update the total game time\ngameTimeTotal += dt;\n```\n\n现在，我们使用`dt.AsSeconds`函数返回的值初始化一个名为`dtAsSeconds`的`float`变量。对于大多数帧，这将是 1 的一小部分。这非常适合传递到`player.update`函数，用于计算移动玩家精灵的量。\n\n现在，我们可以使用`MOUSE::getPosition`函数初始化`mouseScreenPosition`。\n\n重要提示\n\n您可能想知道获取鼠标位置的语法有点不寻常。这称为一个**静态函数**。如果我们用 static 关键字在类中定义函数，我们可以使用类名调用该函数，而不需要类的实例。C++ OOP 有很多怪癖和规则。随着我们的进步，我们将看到更多。\n\n然后使用`window`上的 SFML`mapPixelToCoords`函数初始化`mouseWorldPosition`。我们在本章前面讨论`View`类时讨论了这个函数。\n\n此时，我们可以根据需要调用`player.update`并传入`dtAsSeconds`和鼠标的位置。\n\n我们将玩家的新中心存储在名为`playerPosition`的`Vector2f`实例中。目前，这是未使用的，但我们将在项目的后面使用它。\n\n然后，我们可以使用`mainView.setCenter(player.getCenter())`将视图集中在玩家最新位置的中心。\n\n我们现在可以把玩家拉到屏幕上了。添加以下突出显示的代码，将主游戏循环的平局部分拆分为不同的状态：\n\n```cpp\n        }// End updating the scene\n /*\n **************\n Draw the scene\n **************\n */\n if (state == State::PLAYING)\n {\n window.clear();\n // set the mainView to be displayed in the window\n // And draw everything related to it\n window.setView(mainView);\n // Draw the player\n window.draw(player.getSprite());\n }\n if (state == State::LEVELING_UP)\n {\n }\n if (state == State::PAUSED)\n {\n }\n if (state == State::GAME_OVER)\n {\n }\n window.display();\n    }// End game loop\n    return 0;\n}\n```\n\n在前面代码的`if(state == State::PLAYING)`部分中，我们清除屏幕，将窗口视图设置为`mainView`，然后用`window.draw(player.getSprite())`绘制玩家精灵。\n\n在处理完所有不同的状态后，代码以通常的方式用`window.display();`显示场景。\n\n你可以运行游戏，看到我们的玩家角色随着鼠标的移动而旋转。\n\n提示\n\n运行游戏时，需要按*键*开始游戏，然后选择*1*到*6*之间的数字，模拟选择升级选项。然后，游戏就开始了。\n\n您还可以在（空的）500 x 500 像素竞技场内移动玩家。您可以在屏幕中央看到我们的孤独玩家，如下所示：\n\n![](img/B14278_08_07.jpg)\n\n然而，你没有任何运动感，因为我们没有实现背景。我们将在下一章中这样做。\n\n# 总结\n\n呸！那是一个很长的问题。本章我们做了很多工作：我们为僵尸竞技场项目`Player`构建了第一个类，并将其用于游戏循环。我们还学习并使用了`View`类的一个实例，尽管我们还没有探讨它给我们带来的好处。\n\n在下一章中，我们将通过探索什么是精灵床单来构建竞技场背景。我们还将学习关于 C++ 的 ToR.T0}引用 To1-T1，它允许我们操纵变量，即使它们超出范围（也就是在另一个函数中）。\n\n# 常见问题\n\nQ） 我注意到我们已经编写了很多`Player`类的函数，但我们没有使用这些函数。为什么会这样？\n\nA） 我们没有继续回到`Player`类，而是添加了整个项目所需的所有代码。在[*第 13 章*](13.html#_idTextAnchor279)*音效、文件 I/O、游戏*结束时，我们将充分利用所有这些功能。**"
  },
  {
    "path": "docs/begin-cpp-game-prog/09.md",
    "content": "# 九、C++ 引用、精灵列表和顶点数组\n\n在[*第 4 章*](04.html#_idTextAnchor110)*循环、数组、开关、枚举和函数——实现游戏机制*中，我们讨论了范围。这是一个概念，即在函数或内部代码块中声明的变量在该函数或代码块中仅具有作用域（即，可以*看到*或使用）。只使用当前的 C++ 知识，这可能会导致问题。如果我们需要处理`main`函数中需要的一些复杂对象，我们该怎么办？这可能意味着所有代码都必须在`main`函数中。\n\n在本章中，我们将探讨 C++ OrthT0 引用引用 To1-T1，它允许我们研究变量或对象，这些变量和对象在范围之外。除此之外，这些引用将帮助我们避免在函数之间传递大型对象，这是一个缓慢的过程。它很慢，因为每次我们这样做时，都必须创建变量或对象的副本。\n\n有了这些新的参考知识，我们将研究 SFML`VertexArray`类，它允许我们建立一个大图像，可以使用单个图像文件中的多个部分快速有效地绘制到屏幕上。到本章结束时，我们将拥有一个可伸缩、随机、滚动的背景，它是使用引用和`VertexArray`对象制作的。\n\n在本章中，我们将讨论以下主题：\n\n*   C++ 引用\n*   SFML 顶点数组\n*   对随机滚动的背景进行编码\n\n# C++ 引用\n\n当我们将值传递给函数或从函数返回值时，这正是我们所做的-通过**值**传递/返回。所发生的事情是，创建变量所持有的值的副本，然后将其发送到函数，在函数中使用它。\n\n这具有双重意义：\n\n1.  如果我们希望函数对变量进行永久性更改，那么这个系统对我们没有好处。\n2.  当副本作为参数传入或从函数返回时，会消耗处理能力和内存。对于一个简单的`int`，甚至可能是一个`Sprite`，这是无关紧要的。然而，对于一个复杂的对象，可能是整个游戏世界（或背景），复制过程将严重影响我们游戏的性能。\n\n引用是这两个问题的解决方案。**参考**是一种特殊类型的变量。参考*指的是*另一个变量。下面是一个帮助您更好地理解这一点的示例：\n\n```cpp\nint numZombies = 100;\nint& rNumZombies = numZombies;\n```\n\n在前面的代码中，我们声明并初始化了一个名为`numZombies`的常规`int`。然后我们声明并初始化一个名为`rNumZombies`的`int`引用。类型后面的引用运算符`&`确定正在声明引用。\n\n提示\n\n引用名称前面的`r`前缀是可选的，但对于记住我们正在处理引用非常有用。\n\n现在，我们有一个名为`numZombies`的`int`变量，它存储值 100，还有一个名为`rNumZombies`的`int`引用，它表示`numZombies`。\n\n我们对`numZombies`所做的任何事情都可以通过`rNumZombies`看到，而我们对`rNumZombies`所做的任何事情实际上都是对`numZombies`所做的。请看下面的代码：\n\n```cpp\nint score = 10;\nint& rScore = score;\nscore ++ ;\nrScore ++ ;\n```\n\n在前面的代码中，我们声明了一个名为`score`的`int`。接下来，我们声明一个名为`rScore`的`int`引用，它引用`score`。记住，我们对`score`所做的任何事情都可以被`rScore`看到，而我们对`rScore`所做的任何事情实际上都是对`score`所做的。\n\n因此，考虑当我们这样增加增量 T0 时会发生什么：\n\n```cpp\nscore ++ ;\n```\n\n`score`变量现在存储值 11。除此之外，如果我们要输出`rScore`，它也会输出 11。下一行代码如下：\n\n```cpp\nrScore ++ ;\n```\n\n现在，`score`实际上保留值 12，因为我们对`rScore`所做的任何事情实际上都是对`score`所做的。\n\n提示\n\n如果您想知道*这是如何工作的，那么我们将在下一章讨论**指针**时介绍更多内容。但是简单地说，你可以考虑在计算机内存中存储一个位置/地址。内存中的位置与它引用的变量存储其值的位置相同。因此，对引用或变量的操作具有完全相同的效果。*\n\n现在，更重要的是讨论引用的*为什么*。使用引用有两个原因，我们已经提到了它们。这里再次总结了它们：\n\n1.  在另一个函数中更改/读取变量/对象的值，否则超出范围。\n2.  在不制作副本的情况下传递/返回函数（因此，效率更高）。\n\n研究以下代码，然后我们将对其进行讨论：\n\n```cpp\nvoid add(int n1, int n2, int a);\nvoid referenceAdd(int n1, int n2, int& a);\nint main()\n{\n    int number1 = 2;\n    int number2 = 2;\n    int answer = 0;\n\n    add(number1, number2, answer);\n    // answer still equals zero because it is passed as a copy\n    // Nothing happens to answer in the scope of main\n    referenceAdd(number1, number2, answer);\n    // Now answer equals 4 because it was passed by reference\n    // When the referenceAdd function did this:\n    // answer = num1 + num 2;\n    // It is actually changing the value stored by answer\n    return 0;\n}\n// Here are the two function definitions\n// They are exactly the same except that\n// the second passes a reference to a\nvoid add(int n1, int n2, int a)\n{\n    a = n1 + n2;\n    // a now equals 4\n    // But when the function returns a is lost forever\n}\nvoid referenceAdd(int n1, int n2, int& a)\n{\n    a = n1 + n2;\n    // a now equals 4\n    // But a is a reference!\n    // So, it is actually answer, back in main, that equals 4\n}\n```\n\n前面的代码以两个函数的原型开始：`add`和`referenceAdd`。`add`函数使用三个`int`变量，`referenceAdd`函数使用两个`int`变量和一个`int`引用。\n\n调用`add`函数并传入`number1`、`number2`和`answer`变量时，会制作一份值的副本，并操作`add`（即，`n1`、`n2`和`a`本地的新变量。因此，返回到`main`中的`answer`保持为零。\n\n调用`referenceAdd`函数时，`number1`和`number2`再次按值传递。但是，`answer`是通过引用传递的。当添加到`n2`的`n1`值被分配给参考`a`时，实际发生的情况是该值被分配回`main`函数中的`answer`。\n\n很明显，对于如此简单的事情，我们永远不需要使用引用。然而，它确实演示了通过引用传递的机制。\n\n让我们总结一下我们对参考资料的了解。\n\n## 引用综述\n\n前面的代码演示了如何使用引用在一个范围内使用另一个范围内的代码更改变量的值。除了非常方便之外，通过引用传递也非常有效，因为不需要复制。我们的示例，即使用对`int`的引用，有点含糊不清，因为`int`太小，没有实际的效率增益。在本章的后面，我们将使用一个参考来通过整个级别布局，效率的提高将是显著的。\n\n提示\n\n有一个有参考资料的！必须在创建变量时将引用指定给该变量。这意味着它不是完全灵活的。现在不要担心这个。在下一章中，我们将进一步探讨引用以及它们更灵活（稍微复杂）的关系，例如**指针**。\n\n这在很大程度上与`int`无关，但对于类的大型对象可能意义重大。我们将在本章后面实现僵尸竞技场游戏的滚动背景时使用这种精确的技术。\n\n# SFML 顶点数组和精灵表\n\n我们几乎准备好实现滚动背景。我们只需要了解 SFML 顶点数组和精灵表。\n\n## 什么是雪碧床单？\n\n**精灵表**是一组图像，可以是动画帧，也可以是包含在一个图像文件中的完全独立的图形。请仔细查看此精灵表，其中包含四个单独的图像，将用于在我们的僵尸竞技场游戏中绘制背景：\n\n![](img/B14278_09_01.jpg)\n\nSFML 允许我们加载一个精灵表作为一个常规纹理，就像我们在本书中迄今为止对每个纹理所做的那样。当我们将多个图像作为单个纹理加载时，GPU 可以更有效地处理它。\n\n提示\n\n事实上，一台现代 PC 可以处理这四种纹理，而无需使用精灵表。然而，这些技术值得学习，因为我们的游戏对硬件的要求越来越高。\n\n当我们从 sprite 表中绘制图像时，我们需要做的是确保参考我们需要的 sprite 表部分的精确像素坐标，如下所示：\n\n![](img/B14278_09_02.jpg)\n\n上一个图表使用坐标及其在精灵工作表中的位置标记每个零件/平铺。这些坐标称为**纹理坐标**。我们将在代码中使用这些纹理坐标来绘制所需的正确部分。\n\n## 什么是顶点数组？\n\n首先，我们需要问，什么是顶点？**顶点**是单个图形点，即坐标。该点由水平和垂直位置定义。顶点的复数形式是顶点。因此，顶点数组是一个完整的顶点集合。\n\n在 SFML 中，顶点数组中的每个顶点还具有一种颜色和一个相关的附加顶点（即一对坐标），称为**纹理坐标**。纹理坐标是我们想要在精灵表中使用的图像的位置。稍后，我们将看到如何定位图形并选择精灵表的一部分显示在每个位置，所有这些都使用单个顶点数组。\n\nSFML`VertexArray`类可以保存不同类型的顶点集。但每个`VertexArray`只能容纳一种类型的集合。我们使用适合这种场合的电视机。\n\n视频游戏中常见的场景包括但不限于以下**原语**类型：\n\n*   **点**：每个点一个顶点。\n*   **线条**：每组两个顶点，定义线条的起点和终点。\n*   **三角形**：每点三个顶点。对于复杂的 3D 模型，这是最常用的（以千计），或者成对地创建一个简单的矩形，例如精灵。\n*   **四边形**：每套四个顶点。这是从精灵图纸映射矩形区域的便捷方法。\n\n我们将在这个项目中使用四边形。\n\n## 用瓷砖搭建背景\n\n僵尸竞技场背景将由随机排列的方形图像组成。你可以把这种安排想象成地板上的瓷砖。\n\n在这个项目中，我们将使用带有**四元组**集合的顶点数组。每个顶点都是一组四个顶点（即四边形）的一部分。每个顶点将从我们的背景定义一个平铺的一个角，而每个纹理坐标将根据精灵工作表中的特定图像保留一个适当的值。\n\n让我们看一些代码来开始。这不是我们将在项目中使用的确切代码，但它非常接近，允许我们在继续使用实际实现之前研究顶点数组。\n\n## 构建顶点数组\n\n当我们创建一个类的实例时，我们声明我们的新对象。下面的代码声明了一个名为`background`的`VertexArray`类型的新对象：\n\n```cpp\n// Create a vertex array\nVertexArray background;\n```\n\n我们想让`VertexArray`的实例知道我们将使用哪种类型的原语。请记住，点、线、三角形和四边形都有不同数量的顶点。通过将`VertexArray`实例设置为保存特定类型，可以知道每个原语的开始。在我们的例子中，我们需要四个。以下是执行此操作的代码：\n\n```cpp\n// What primitive type are we using\nbackground.setPrimitiveType(Quads);\n```\n\n与常规 C++ 数组一样，需要将一个 AUT0T0L 实例设置为特定的大小。然而，`VertexArray`类比常规数组更灵活。它允许我们在游戏运行时更改其大小。可以在声明的同时配置大小，但是我们的背景需要随着每个 wave 而扩展。`VertexArray`类通过`resize`函数提供此功能。以下是将竞技场大小设置为 10 x 10 瓷砖大小的代码：\n\n```cpp\n// Set the size of the vertex array\nbackground.resize(10 * 10 * 4);\n```\n\n在前一行代码中，第一个`10`是宽度，第二个`10`是高度，4 是四边形中的顶点数。我们本可以通过 400 分，但这样的计算让我们清楚地知道我们在做什么。当我们将项目编码为 real 时，我们将更进一步，以帮助澄清，并为计算的每个部分声明变量。\n\n我们现在有一个`VertexArray`实例，可以配置它的数百个顶点。下面是我们如何设置前四个顶点（即第一个四边形）上的位置坐标：\n\n```cpp\n// Position each vertex in the current quad\nbackground[0].position = Vector2f(0, 0);\nbackground[1].position = Vector2f(49, 0);\nbackground[2].position = Vector2f(49,49);\nbackground[3].position = Vector2f(0, 49);\n```\n\n下面是我们如何将这些相同顶点的纹理坐标设置为精灵工作表中的第一个图像。图像文件中的这些坐标从 0,0（左上角）到 49,49（右下角）：\n\n```cpp\n// Set the texture coordinates of each vertex\nbackground[0].texCoords = Vector2f(0, 0);\nbackground[1].texCoords = Vector2f(49, 0);\nbackground[2].texCoords = Vector2f(49, 49);\nbackground[3].texCoords = Vector2f(0, 49);\n```\n\n如果我们想将纹理坐标设置为 sprite 工作表中的第二个图像，我们将编写如下代码：\n\n```cpp\n// Set the texture coordinates of each vertex\nbackground[0].texCoords = Vector2f(0, 50);\nbackground[1].texCoords = Vector2f(49, 50);\nbackground[2].texCoords = Vector2f(49, 99);\nbackground[3].texCoords = Vector2f(0, 99);\n```\n\n当然，如果我们像这样单独定义每个顶点，那么即使是一个简单的 10 乘 10 竞技场，我们也要花很长时间来配置。\n\n当我们实现真实背景时，我们将设计一组嵌套的`for`循环，循环通过每个四边形，选择一个随机背景图像，并指定适当的纹理坐标。\n\n代码需要相当智能。它需要知道何时是边缘平铺，以便可以使用“精灵”工作表中的墙图像。它还需要使用适当的变量，知道每个背景瓷砖在精灵表中的位置，以及所需竞技场的总体大小。\n\n我们将通过将所有代码放在一个单独的函数和一个单独的文件中来管理这种复杂性。我们将使用 C++ 引用使 ALE T0.实例在 ALE T1 中使用。\n\n我们稍后会研究这些细节。您可能已经注意到，我们从未将纹理（精灵表与顶点数组）关联起来。现在让我们看看怎么做。\n\n## 使用顶点数组进行绘制\n\n我们可以以加载任何其他纹理的相同方式加载精灵表作为纹理，如下代码所示：\n\n```cpp\n// Load the texture for our background vertex array\nTexture textureBackground;\ntextureBackground.loadFromFile(\"graphics/background_sheet.png\");\n```\n\n然后我们可以通过一次调用`draw`来绘制整个`VertexArray`：\n\n```cpp\n// Draw the background\nwindow.draw(background, &textureBackground);\n```\n\n前面的代码比将每个瓷砖作为单个精灵绘制要高效得多。\n\n重要提示\n\n在我们继续之前，请注意在`textureBackground`代码之前有点奇怪的`&`符号。你的直接想法可能是这与引用有关。这里发生的事情是我们传递的是`Texture`实例的地址，而不是实际的`Texture`实例。我们将在下一章中进一步了解这一点。\n\n我们现在能够使用我们对引用和顶点数组的知识来实现僵尸竞技场项目的下一阶段。\n\n# 创建随机生成的滚动背景\n\n在本节中，我们将创建一个函数，在单独的文件中创建背景。通过使用顶点数组引用，我们将确保`main`函数可以使用背景（在范围内）。\n\n当我们编写与`main`函数共享数据的其他函数时，我们将把它们都写在它们自己的`.cpp`文件中。我们将在一个新的头文件中为这些函数提供原型，我们将在`ZombieArena.cpp`中包含（带有`#include`指令）。\n\n为了实现这一点，让我们创建一个名为`ZombieArena.h`的新头文件。现在，我们已经准备好为新函数编写头文件。\n\n在这个新的`ZombieArena.h`头文件中，添加以下突出显示的代码，包括函数原型：\n\n```cpp\n#pragma once\nusing namespace sf;\nint createBackground(VertexArray& rVA, IntRect arena);\n```\n\n前面的代码允许我们编写名为`createBackground`的函数的定义。为了匹配原型，函数定义必须返回一个`int`值，并接收一个`VertexArray`引用和一个`IntRect`对象作为参数。\n\n现在，我们可以创建一个新的`.cpp`文件，在其中我们将对函数定义进行编码。创建一个名为`CreateBackground.cpp`的新文件。现在，我们已经准备好编写将创建我们的背景的函数定义。\n\n将以下代码添加到`CreateBackground.cpp`文件中，然后我们将对其进行查看：\n\n```cpp\n#include \"ZombieArena.h\"\nint createBackground(VertexArray& rVA, IntRect arena)\n{\n    // Anything we do to rVA we are really doing\n    // to background (in the main function)\n\n    // How big is each tile/texture\n    const int TILE_SIZE = 50;\n    const int TILE_TYPES = 3;\n    const int VERTS_IN_QUAD = 4;\n    int worldWidth = arena.width / TILE_SIZE;\n    int worldHeight = arena.height / TILE_SIZE;\n    // What type of primitive are we using?\n    rVA.setPrimitiveType(Quads);\n    // Set the size of the vertex array\n    rVA.resize(worldWidth * worldHeight * VERTS_IN_QUAD);\n    // Start at the beginning of the vertex array\n    int currentVertex = 0;\n    return TILE_SIZE;\n}\n```\n\n在前面的代码中，我们编写了函数签名以及标记函数体的开始和结束花括号。\n\n在函数体中，我们声明并初始化三个新的`int`常量，以保存在函数其余部分中需要引用的值。它们是`TILE_SIZE`、`TILE_TYPES`和`VERTS_IN_QUAD`。\n\n`TILE_SIZE`常数是指 sprite 表中每个瓷砖的大小（以像素为单位）。`TILE_TYPES`常数是指 sprite 表中不同瓷砖的数量。我们可以在 sprite 表中添加更多的块，并更改`TILE_TYPES`以匹配更改，我们将要编写的代码仍然可以工作。`VERTS_IN_QUAD`表示每个四边形中有四个顶点。与总是键入不太清晰的数字`4`相比，使用此常量不太容易出错。\n\n然后我们声明并初始化两个`int`变量：`worldWidth`和`worldHeight`。这些变量的用途可能显而易见。它们的名字背叛了它们，但值得指出的是，它们指的是世界的宽度和高度，是瓷砖的数量，而不是像素。`worldWidth`和`worldHeight`变量通过将进入竞技场的高度和宽度除以`TILE_SIZE`常数来初始化。\n\n接下来，我们将第一次使用我们的参考资料。记住，我们对`rVA`所做的任何事情，实际上都是对传入的变量所做的，它在`main`函数的作用域中（或者在编写代码时）。\n\n然后，我们使用`rVA.setType`准备顶点数组以使用四边形，然后通过调用`rVA.resize`使其大小刚好合适。对于`resize`函数，我们传入`worldWidth * worldHeight * VERTS_IN_QUAD`的结果，这正好等于我们准备完成顶点数组时将拥有的顶点数。\n\n最后一行代码声明并将`currentVertex`初始化为零。我们将使用`currentVertex`循环遍历顶点数组，初始化所有顶点。\n\n现在我们可以编写嵌套`for`循环的第一部分，该循环将准备顶点数组。添加以下突出显示的代码，并根据我们对顶点数组的了解，尝试了解它的功能：\n\n```cpp\n    // Start at the beginning of the vertex array\n    int currentVertex = 0;\n for (int w = 0; w < worldWidth; w++)\n {\n for (int h = 0; h < worldHeight; h++)\n {\n // Position each vertex in the current quad\n rVA[currentVertex + 0].position = \n Vector2f(w * TILE_SIZE, h * TILE_SIZE);\n\n rVA[currentVertex + 1].position =\n Vector2f((w * TILE_SIZE) + TILE_SIZE, h * TILE_SIZE);\n\n rVA[currentVertex + 2].position =\n Vector2f((w * TILE_SIZE) + TILE_SIZE, (h * TILE_SIZE) \n + TILE_SIZE);\n\n rVA[currentVertex + 3].position =\n Vector2f((w * TILE_SIZE), (h * TILE_SIZE) \n + TILE_SIZE);\n\n // Position ready for the next four vertices\n currentVertex = currentVertex + VERTS_IN_QUAD;\n }\n }\n    return TILE_SIZE;\n}\n```\n\n我们刚刚使用嵌套的`for`循环添加了通过顶点数组的步骤的代码，该循环首先通过前四个顶点进行步骤：`currentVertex + 1`、 `currentVertex + 2`等等。\n\n我们使用数组符号`rvA[currentVertex + 0]..`等访问数组中的每个顶点。使用数组表示法，我们调用`position`函数`rvA[currentVertex + 0].position...`。\n\n对于`position`函数，我们传递每个顶点的水平和垂直坐标。我们可以通过组合使用`w`、`h`和`TILE_SIZE`以编程方式计算出这些坐标。\n\n在上一个代码的末尾，我们定位了`currentVertex`，通过将嵌套的`for`循环向前推进四位（即添加四位），为下一次通过该循环做好准备，即`currentVertex = currentVertex + VERTS_IN_QUAD`。\n\n当然，所有这些都是设置顶点的坐标；它不会从“精灵”工作表中指定纹理坐标。这就是我们下一步要做的。\n\n为了明确新代码的去向，我在上下文中展示了它，以及我们刚才编写的所有代码。添加并研究以下突出显示的代码：\n\n```cpp\nfor (int w = 0; w < worldWidth; w++)\n    {\n        for (int h = 0; h < worldHeight; h++)\n        {\n            // Position each vertex in the current quad\n            rVA[currentVertex + 0].position = \n                Vector2f(w * TILE_SIZE, h * TILE_SIZE);\n\n            rVA[currentVertex + 1].position =\n                Vector2f((w * TILE_SIZE) + TILE_SIZE, h * TILE_SIZE);\n\n            rVA[currentVertex + 2].position =\n                Vector2f((w * TILE_SIZE) + TILE_SIZE, (h * TILE_SIZE) \n                + TILE_SIZE);\n\n            rVA[currentVertex + 3].position =\n                Vector2f((w * TILE_SIZE), (h * TILE_SIZE) \n                + TILE_SIZE);\n\n // Define the position in the Texture for current quad\n // Either grass, stone, bush or wall\n if (h == 0 || h == worldHeight-1 || \n w == 0 || w == worldWidth-1)\n {\n // Use the wall texture\n rVA[currentVertex + 0].texCoords = \n Vector2f(0, 0 + TILE_TYPES * TILE_SIZE);\n\n rVA[currentVertex + 1].texCoords = \n Vector2f(TILE_SIZE, 0 + \n TILE_TYPES * TILE_SIZE);\n\n rVA[currentVertex + 2].texCoords = \n Vector2f(TILE_SIZE, TILE_SIZE + \n TILE_TYPES * TILE_SIZE);\n\n rVA[currentVertex + 3].texCoords = \n Vector2f(0, TILE_SIZE + \n TILE_TYPES * TILE_SIZE);\n }\n\n            // Position ready for the next for vertices\n            currentVertex = currentVertex + VERTS_IN_QUAD;\n        }\n    }\n    return TILE_SIZE;\n}\n```\n\n前面突出显示的代码在精灵图纸内设置每个顶点相关的坐标。注意有点长的`if`条件。该条件检查当前四元组是竞技场中第一个四元组还是最后一个四元组。如果它是（第一个或最后一个），则表示它是边界的一部分。然后，我们可以使用一个简单的公式，使用`TILE_SIZE`和`TILE_TYPES`从 sprite 床单中瞄准墙壁纹理。\n\n依次为每个顶点初始化数组符号和`texCoords`成员，以指定精灵表中墙纹理的适当角。\n\n下面的代码被包装在一个`else`块中。这意味着，每当四边形不表示边框/墙砖时，它将通过嵌套的`for`循环运行。在现有代码中添加以下突出显示的代码，然后我们将对其进行检查：\n\n```cpp\n            // Define position in Texture for current quad\n            // Either grass, stone, bush or wall\n            if (h == 0 || h == worldHeight-1 ||\n                w == 0 || w == worldWidth-1)\n            {\n                // Use the wall texture\n                rVA[currentVertex + 0].texCoords = \n                    Vector2f(0, 0 + TILE_TYPES * TILE_SIZE);\n\n                rVA[currentVertex + 1].texCoords = \n                    Vector2f(TILE_SIZE, 0 + \n                    TILE_TYPES * TILE_SIZE);\n\n                rVA[currentVertex + 2].texCoords = \n                    Vector2f(TILE_SIZE, TILE_SIZE + \n                    TILE_TYPES * TILE_SIZE);\n\n                rVA[currentVertex + 3].texCoords = \n                    Vector2f(0, TILE_SIZE + \n                    TILE_TYPES * TILE_SIZE);\n            }\n else\n {\n // Use a random floor texture\n srand((int)time(0) + h * w - h);\n int mOrG = (rand() % TILE_TYPES);\n int verticalOffset = mOrG * TILE_SIZE;\n rVA[currentVertex + 0].texCoords = \n Vector2f(0, 0 + verticalOffset);\n\n rVA[currentVertex + 1].texCoords = \n Vector2f(TILE_SIZE, 0 + verticalOffset);\n\n rVA[currentVertex + 2].texCoords = \n Vector2f(TILE_SIZE, TILE_SIZE + verticalOffset);\n\n rVA[currentVertex + 3].texCoords = \n Vector2f(0, TILE_SIZE + verticalOffset);\n } \n            // Position ready for the next for vertices\n            currentVertex = currentVertex + VERTS_IN_QUAD;\n        }\n    }\n    return TILE_SIZE;\n}\n```\n\n前面突出显示的代码首先在随机数生成器中植入一个公式，该公式在循环的每个过程中都是不同的。然后，`mOrG`变量被初始化为一个介于 0 和`TILE_TYPES`之间的数字。这正是我们随机选择一种瓷砖类型所需要的。\n\n重要提示\n\n`mOrG`代表“泥或草”。名称是任意的。\n\n现在，我们通过将`mOrG`乘以`TileSize`来声明并初始化一个名为`verticalOffset`的变量。现在，在精灵表中有一个垂直参考点，指向当前四边形的随机选择纹理的起始高度。\n\n现在，我们使用一个涉及到`TILE_SIZE`和`verticalOffset`的简单公式将纹理每个角的精确坐标指定给适当的顶点。\n\n我们现在可以在游戏引擎中使用我们的新功能。\n\n# 使用背景\n\n我们已经做了一些棘手的事情，所以这很简单。有三个步骤，如下所示：\n\n1.  创建一个`VertexArray`。\n2.  在调平每个波浪后初始化它。\n3.  在每一帧中绘制它。\n\n添加以下突出显示的代码以声明名为`background`的`VertexArray`实例并将`background_sheet.png`文件作为纹理加载：\n\n```cpp\n// Create an instance of the Player class\nPlayer player;\n// The boundaries of the arena\nIntRect arena;\n// Create the background\nVertexArray background;\n// Load the texture for our background vertex array\nTexture textureBackground;\ntextureBackground.loadFromFile(\"graphics/background_sheet.png\");\n// The main game loop\nwhile (window.isOpen())\n```\n\n添加以下代码调用`createBackground`函数，传入`background`作为引用，并按值传入`arena`。注意，在突出显示的代码中，我们还修改了初始化`tileSize`变量的方式。添加高亮显示的代码，如图所示：\n\n```cpp\nif (state == State::PLAYING)\n{\n    // Prepare the level\n    // We will modify the next two lines later\n    arena.width = 500;\n    arena.height = 500;\n    arena.left = 0;\n    arena.top = 0;\n // Pass the vertex array by reference \n // to the createBackground function\n int tileSize = createBackground(background, arena);\n    // We will modify this line of code later\n // int tileSize = 50;\n    // Spawn the player in the middle of the arena\n    player.spawn(arena, resolution, tileSize);\n    // Reset the clock so there isn't a frame jump\n    clock.restart();\n}\n```\n\n请注意，我们已经替换了代码的`int tileSize = 50`行，因为我们直接从`createBackground`函数的返回值获取值。\n\n提示\n\n为了将来代码的清晰性，您应该删除代码的`int tileSize = 50`行及其相关注释。我只是对它进行了注释，以便为新代码提供更清晰的上下文。\n\n最后，是画图的时候了。这真的很简单。我们所做的就是调用`window.draw`并传递`VertexArray`实例，以及`textureBackground`纹理：\n\n```cpp\n/*\n **************\n Draw the scene\n **************\n */\nif (state == State::PLAYING)\n{\n    window.clear();\n    // Set the mainView to be displayed in the window\n    // And draw everything related to it\n    window.setView(mainView);\n // Draw the background\n window.draw(background, &textureBackground);\n    // Draw the player\n    window.draw(player.getSprite());\n}\n```\n\n提示\n\n如果你想知道在`textureBackground`前面的奇怪的`&`标志是怎么回事，那么在下一章中，所有的一切都会被弄清楚。\n\n您现在可以运行游戏了。您将看到以下输出：\n\n![](img/B14278_09_03.jpg)\n\n他 re，注意玩家精灵如何在竞技场的范围内平滑地滑行和旋转。尽管`main`函数中的当前代码绘制了一个小竞技场，`CreateBackground`函数可以创建任何大小的竞技场。我们将在[*第 13 章*](13.html#_idTextAnchor279)、*音效、文件 I/O 和游戏结束*中看到比屏幕更大的竞技场。\n\n# 总结\n\n在本章中，我们发现 C++ 引用，它们是作为另一变量别名的特殊变量。当我们通过引用而不是通过值传递变量时，我们对引用所做的任何操作都会发生在调用函数中的变量上。\n\n我们还学习了顶点数组，并创建了一个充满四边形的顶点数组，以从精灵表中绘制瓷砖作为背景。\n\n当然，房间里的大象是我们的僵尸游戏没有任何僵尸。我们将在下一章中通过学习 C++ 指针和 ALE T0 标准模板库 ALE T1（St2）来学习这一点。\n\n# 常见问题\n\n以下是您可能想到的一些问题：\n\nQ） 你能再总结一下这些参考资料吗？\n\nA） 必须立即初始化引用，并且不能将其更改为引用其他变量。将引用与函数一起使用，以便不处理副本。这有利于提高效率，因为它避免了复制，并帮助我们更容易地将代码抽象为函数。\n\nQ） 有没有一种简单的方法可以记住使用引用的主要好处？\n\na）为了帮助你记住引用的内容，请考虑这个简短的押韵：\n\n*移动大型物体会让我们的游戏变得波涛汹涌，*\n\n*引用传递比复制快。*"
  },
  {
    "path": "docs/begin-cpp-game-prog/10.md",
    "content": "# 十、指针、标准模板库、纹理管理\n\n在本章中，我们将学到很多东西，并在游戏方面做很多工作。首先，我们将学习关于 To.T0 指针的基本 C++ 主题。指针是保存内存地址的变量。通常，指针将保存另一个变量的内存地址。这听起来有点像一个参考，但我们将看到他们是如何更强大，并使用指针来处理不断扩大的僵尸群。\n\n我们还将了解**标准模板库**（**STL**），这是一个类集合，允许我们快速轻松地实现常见的数据管理技术。\n\n一旦我们了解了 STL 的基本知识，我们将能够使用这些新知识来管理游戏中的所有纹理，因为如果我们有 1000 个僵尸，我们并不想为每个僵尸都将僵尸图形的副本加载到 GPU 中。\n\n我们还将更深入地研究 OOP，并使用**静态**函数，这是一个类的函数，可以在没有类实例的情况下调用。同时，我们将看到如何设计一个类以确保只有一个实例可以存在。当我们需要保证代码的不同部分将使用相同的数据时，这非常理想。\n\n在本章中，我们将介绍以下主题：\n\n*   学习指针\n*   学习 STL\n*   使用静态函数和**单例**类实现`TextureHolder`类\n*   实现指向一群僵尸的指针\n*   编辑一些现有代码，为玩家和背景使用`TextureHolder`类\n\n# 学习指针\n\n指针可以是学习 C++ 代码时产生挫折的原因。然而，这个概念很简单。\n\n重要提示\n\n**指针**是一个保存内存地址的变量。\n\n就这样！没什么可担心的。让初学者感到沮丧的可能是我们用来处理指针的代码的语法。我们将逐步介绍使用指针的代码的每一部分。然后你可以开始掌握它们的持续过程。\n\n提示\n\n在本节中，我们将实际了解有关指针的更多信息，而不是本项目所需要的。在下一个项目中，我们将更多地使用指针。尽管如此，我们将只触及这个话题的表面。进一步的研究肯定是值得推荐的，我们将在最后一章讨论更多。\n\n我很少认为记忆事实、数字或语法是最好的学习方法。然而，记住与指针相关的简短但关键的语法可能是值得的。这将确保信息深入我们的大脑，我们永远不会忘记它。然后我们可以讨论为什么我们需要指针，并检查它们与引用的关系。指针类比可能有助于：\n\n提示\n\n如果一个变量是一个 house，它的内容是它所持有的值，那么指针就是 house 的地址。\n\n在上一章中，在讨论引用时，我们了解到，当我们向函数传递值或从函数返回值时，实际上我们正在创建一个全新的房子，但它与上一章完全相同。我们正在制作传递给函数或传递自函数的值的副本。\n\n此时，指针可能开始听起来有点像引用。那是因为它们有点像引用。然而，指针要灵活得多，功能强大得多，并且有自己独特的用途。这些特殊和独特的用途需要特殊和独特的语法。\n\n## 指针语法\n\n有两个主要运算符与指针关联。第一个是操作员的**地址：**\n\n```cpp\n&\n```\n\n第二个是**解引用**操作符：\n\n```cpp\n*\n```\n\n现在，我们将研究使用这些运算符和指针的不同方式。\n\n您将注意到的第一件事是操作符的地址与引用操作符的地址相同。为了增加一个有抱负的 C++ 游戏程序员的痛苦，操作员在不同的环境下做不同的事情。从一开始就知道这一点是很有价值的。如果您正盯着一些涉及指针的代码，并且看起来您快要发疯了，请了解以下几点：\n\n提示\n\n你完全清醒！您只需要查看上下文的细节。\n\n现在，你知道，如果有些事情不清楚，不明显，那不是你的错。指针并不清晰，也不明显，但仔细观察上下文会发现发生了什么。\n\n有了这些知识，您需要更加关注指针，而不是之前的语法，以及这两个运算符（的**地址和**解引用**地址），我们现在可以开始看一些真正的指针代码。**\n\n提示\n\n继续之前，请确保已记住这两个运算符。\n\n## 声明一个指针\n\n为了声明一个新指针，我们使用了去引用操作符，以及指针将保存其地址的变量类型。在我们进一步讨论指针之前，先看一下以下代码：\n\n```cpp\n// Declare a pointer to hold \n// the address of a variable of type int\nint* pHealth;\n```\n\n前面的代码声明了一个名为`pHealth`的新指针，它可以保存`int`类型变量的地址。注意，我说过*可以*保存`int`类型的变量。与其他变量一样，指针也需要用值初始化以正确使用它。\n\n名称`pHealth`与其他变量一样，是任意的。\n\n提示\n\n通常的做法是在作为指针的变量名称前加上前缀`p`。这样，当我们处理指针时，记忆起来就容易多了，并且可以将它们与常规变量区分开来。\n\n在引用引用运算符周围使用的空白是可选的，因为 C++ 很少关心语法中的空格。但是，建议使用它，因为它有助于可读性。看下面三行代码，它们都做相同的事情。\n\n在上一个示例中，我们刚刚看到了以下格式，类型旁边有解引用操作符：\n\n```cpp\nint* pHealth;\n```\n\n以下代码显示取消引用运算符任一侧的空白：\n\n```cpp\nint * pHealth;\n```\n\n以下代码显示指针名称旁边的取消引用运算符：\n\n```cpp\nint *pHealth;\n```\n\n了解这些可能性是值得的，这样当您阅读代码时，也许在 web 上，您将了解它们都是相同的。在本书中，我们将始终在类型旁边使用第一个选项和解引用操作符。\n\n就像正则变量只能成功地包含适当类型的数据一样，指针应该只包含适当类型的变量的地址。\n\n指向`int`类型的指针不应包含`String`、`Zombie`、`Player`、`Sprite`、`float`或除`int`之外的任何其他类型的地址。\n\n让我们看看如何初始化指针。\n\n## 初始化指针\n\n接下来，我们将看到如何将变量的地址转换成指针。请看下面的代码：\n\n```cpp\n// A regular int variable called health\nint health = 5;\n// Declare a pointer to hold the address of a variable of type int\nint* pHealth;\n// Initialize pHealth to hold the address of health,\n// using the \"address of\" operator\npHealth = &health;\n```\n\n在前面的代码中，我们声明了一个名为`health`的`int`变量，并将其初始化为`5`。虽然我们以前从未讨论过这个变量，但它一定在计算机内存中的某个地方，这是有道理的。它必须有一个内存地址。\n\n我们可以使用操作员的**地址访问此地址。仔细查看前面代码的最后一行。我们用地址`health`初始化`pHealth`，如下所示：**\n\n```cpp\n  pHealth = &health;\n```\n\n我们的`pHealth`指针现在持有常规`int`、`health`的地址。\n\n提示\n\n在 C++ 术语中，我们说，ORT T0 指向点 T1。\n\n我们可以通过将`pHealth`传递给函数来使用它，这样函数就可以处理`health`，就像我们处理引用一样。\n\n如果这就是我们要做的，那么我们就没有理由去做指针了，让我们看看重新初始化它们。\n\n## 重新初始化指针\n\n与引用不同，指针可以重新初始化以指向不同的地址。请看以下代码：\n\n```cpp\n// A regular int variable called health\nint health = 5;\nint score = 0;\n// Declare a pointer to hold the address of a variable of type int\nint* pHealth;\n// Initialize pHealth to hold the address of health\npHealth = &health;\n// Re-initialize pHealth to hold the address of score\npHealth = &score;\n```\n\n现在，`pHealth`指向`int`变量`score`。\n\n当然，我们的指针的名称`pHealth`现在是不明确的，可能应该被称为`pIntPointer`。这里要理解的关键是我们*可以*进行重新分配。\n\n在这个阶段，除了简单地指向（保存内存地址）之外，我们实际上没有使用指针。让我们看看如何访问指针指向的地址中存储的值。这将使它们真正有用。\n\n## 取消对指针的引用\n\n我们知道指针在内存中保存地址。如果我们在游戏中输出这个地址，也许在我们的 HUD 中，在它被声明和初始化之后，它可能看起来像这样：**9876**。\n\n它只是一个值–一个表示内存中地址的值。在不同的操作系统和硬件类型上，这些值的范围会有所不同。在本书的上下文中，我们永远不需要直接操纵地址。我们只关心指向的地址中存储的值是什么。\n\n变量使用的实际地址是在游戏执行时（运行时）确定的，因此在编写游戏代码时，无法知道变量的地址以及指针中存储的值。\n\n我们可以使用**解引用**运算符访问指针指向的地址处存储的值：\n\n```cpp\n*\n```\n\n下面的代码使用指针直接操作一些变量。尝试并遵循以下步骤，然后我们将完成：\n\n提示\n\n警告下面的代码毫无意义（双关语）。它只是演示如何使用指针。\n\n```cpp\n// Some regular int variables\nint score = 0;\nint hiScore = 10;\n// Declare 2 pointers to hold the addresses of int\nint* pIntPointer1;\nint* pIntPointer2;\n// Initialize pIntPointer1 to hold the address of score\npIntPointer1 = &score;\n// Initialize pIntPointer2 to hold the address of hiScore\npIntPointer2 = &hiScore;\n// Add 10 to score directly\nscore += 10;\n// Score now equals 10\n// Add 10 to score using pIntPointer1\n*pIntPointer1 += 10;\n// score now equals 20\\. A new high score\n// Assign the new hi score to hiScore using only pointers\n*pIntPointer2 = *pIntPointer1;\n// hiScore and score both equal 20\n```\n\n在前面的代码中，我们声明了两个`int`变量`score`和`hiScore`。然后分别用值 0 和 10 初始化它们。接下来，我们声明两个指向`int`的指针。这些是`pIntPointer1`和`pIntPointer2`。我们初始化它们的步骤与声明它们分别保存（指向）`score`和`hiScore`变量的地址相同。\n\n接下来，我们以通常的方式将 10 添加到`score``score += 10`。然后，我们可以看到，通过对指针使用解引用运算符，我们可以访问存储在指针指向的地址处的值。以下代码更改了由`pIntPointer1`指向的变量存储的值：\n\n```cpp\n// Add 10 to score using pIntPointer1\n*pIntPointer1 += 10;\n// score now equals 20, A new high score\n```\n\n前面代码的最后一部分取消引用两个指针，将`pIntPointer1`指向的值指定为`pIntPointer2`指向的值：\n\n```cpp\n// Assign the new hi-score to hiScore with only pointers\n*pIntPointer2 = *pIntPointer1;\n// hiScore and score both equal 20\n```\n\n`score`和`hiScore`现在都等于 20。\n\n## 指针用途广泛，功能强大\n\n我们可以用指针做更多的事情。以下是我们可以做的一些有用的事情。\n\n### 动态分配内存\n\n到目前为止，我们看到的所有指针都指向内存地址，这些地址的作用域仅限于创建它们的函数。因此，如果我们声明并初始化指向局部变量的指针，当函数返回时，指针、局部变量和内存地址都将消失。它们超出了范围。\n\n到目前为止，我们一直在使用固定数量的内存，这是在执行游戏之前决定的。此外，我们一直使用的内存是由操作系统控制的，当我们调用函数并从函数返回时，变量会丢失和创建。我们需要的是一种使用内存的方法，在我们完成之前，它总是在范围内。我们希望能够访问我们可以称之为自己的并负责的内存。\n\n当我们声明变量（包括指针）时，它们位于称为**堆栈**的内存区域中。还有另一个内存区域，虽然由操作系统分配和控制，但可以在运行时分配。另一个内存区域称为**空闲存储**，有时称为**堆**。\n\n提示\n\n堆上的内存没有特定函数的作用域。从函数返回不会删除堆上的内存。\n\n这给了我们巨大的力量。通过对内存的访问，我们可以使用大量的对象来规划游戏，而内存的访问仅受我们运行游戏的计算机资源的限制。在我们的例子中，我们想要一大群僵尸。然而，正如蜘蛛侠的叔叔毫不犹豫地提醒我们的那样，“强大的力量带来巨大的责任。”\n\n让我们看看如何使用指针来利用空闲存储上的内存，以及如何在使用完内存后将其释放回操作系统。\n\n要创建指向堆上某个值的指针，我们需要一个指针：\n\n```cpp\nint* pToInt = nullptr;\n```\n\n在前一行代码中，我们以与前面相同的方式声明指针，但由于我们没有将其初始化为指向变量，因此我们将其初始化为`nullptr`。我们这样做是因为这是良好的做法。当你甚至不知道指针指向的时候，考虑撤销指针（在它指向的地址上改变一个值）。这相当于去射击场，蒙住某人的眼睛，让他们旋转，然后告诉他们射击。通过将指针指向 nothing（`nullptr`），我们不会对其造成任何伤害。\n\n当我们准备在空闲存储器上请求内存时，我们使用`new`关键字，如下代码行所示：\n\n```cpp\npToInt = new int;\n```\n\n`pToInt`现在保存空闲存储空间的内存地址，其大小正好适合保存`int`值。\n\n提示\n\n程序结束时，将返回任何分配的内存。然而，重要的是要认识到，除非我们释放内存，否则内存永远不会被释放（在游戏执行过程中）。如果我们继续从免费商店获取内存而不归还，最终它将耗尽，游戏将崩溃。\n\n我们不太可能偶尔从免费存储中取出`int`大小的块来耗尽内存。但是如果我们的程序有一个请求内存的函数或循环，并且这个函数或循环在整个游戏中定期执行，那么最终游戏会变慢，然后崩溃。此外，如果我们在免费商店中分配了很多对象，并且没有正确地管理它们，那么这种情况很快就会发生。\n\n以下代码行将`pToInt`先前指向的空闲存储上的内存交回（删除）：\n\n```cpp\ndelete pToInt;\n```\n\n现在，`pToInt`之前指出的记忆不再是我们可以随心所欲的记忆；我们必须采取预防措施。虽然内存已经返回到操作系统，`pToInt`仍然保存着这个内存的地址，它不再属于我们。\n\n以下代码行确保`pToInt`不能用于尝试操作或访问此内存：\n\n```cpp\npToInt = nullptr;\n```\n\n提示\n\n如果指针指向无效的地址，则称为**野生**或**悬挂**指针。如果您尝试取消对悬空指针的引用，并且幸运的话，游戏将崩溃，并且您将获得内存访问冲突错误。如果你运气不好，你将创建一个难以发现的 bug。此外，如果我们在空闲存储上使用的内存将在函数的生命周期之外持续存在，那么我们必须确保保留一个指向它的指针，否则将导致内存泄漏。\n\n现在，我们可以声明指针并将它们指向空闲存储上新分配的内存。我们可以通过解引用操作和访问它们指向的内存。我们还可以在使用完内存后将其返回到空闲存储区，并且我们知道如何避免指针悬空。\n\n让我们看看指针的更多优点。\n\n### 向函数传递指针\n\n为了将指针传递给函数，我们需要编写一个在原型中具有指针的函数，如以下代码所示：\n\n```cpp\nvoid myFunction(int *pInt)\n{\n   // Dereference and increment the value stored \n   // at the address pointed to by the pointer\n   *pInt ++\n   return;\n}\n```\n\n前面的函数只是取消对指针的引用，并将 1 添加到存储在指向地址的值中。\n\n现在，我们可以使用该函数显式地将变量地址或另一个指针传递给变量：\n\n```cpp\nint someInt = 10;\nint* pToInt = &someInt;\nmyFunction(&someInt);\n// someInt now equals 11\nmyFunction(pToInt);\n// someInt now equals 12\n```\n\n如前一段代码所示，在函数中，我们正在从调用代码中操作变量，并且可以使用变量的地址或指向该变量的指针来操作变量，因为这两个操作是相同的。\n\n指针还可以指向类的实例。\n\n### 声明并使用指向对象的指针\n\n指针不仅仅用于正则变量。我们还可以声明指向用户定义类型（如类）的指针。这就是我们将如何声明指向`Player`类型对象的指针：\n\n```cpp\nPlayer player;\nPlayer* pPlayer = &Player;\n```\n\n我们甚至可以直接从指针访问`Player`对象的成员函数，如下代码所示：\n\n```cpp\n// Call a member function of the player class\npPlayer->moveLeft()\n```\n\n请注意微妙但至关重要的区别：使用指向对象的指针而不是直接使用->操作符访问函数。在这个项目中，我们不需要使用指向对象的指针，但在使用之前，我们将更仔细地研究它们，这将在下一个项目中进行。\n\n在我们谈论一些全新的东西之前，让我们再看一个新的指针主题。\n\n## 指针和数组\n\n数组和指针有一些共同点。数组的名称是内存地址。更具体地说，数组的名称是该数组中第一个元素的内存地址。另外，数组名指向数组的第一个元素。理解这一点的最佳方法是继续阅读并查看以下示例。\n\n我们可以创建指向数组持有的类型的指针，然后使用与数组完全相同的语法以相同的方式使用指针：\n\n```cpp\n// Declare an array of ints\nint arrayOfInts[100];\n// Declare a pointer to int and initialize it \n// with the address of the first\n// element of the array, arrayOfInts\nint* pToIntArray = arrayOfInts;\n// Use pToIntArray just as you would arrayOfInts\narrayOfInts[0] = 999;\n// First element of arrayOfInts now equals 999\npToIntArray[0] = 0;\n// First element of arrayOfInts now equals 0\n```\n\n这还意味着，具有接受指针的原型的函数也接受指针指向的类型的数组。当我们建造越来越多的僵尸群时，我们将利用这一事实。\n\n提示\n\n关于指针和引用之间的关系，编译器在实现引用时实际上使用指针。这意味着引用只是一个方便的工具（在“引擎盖下”使用指针）。您可以将参考视为一个自动变速箱，它可以很好地方便在城镇中驾驶，而指针是一个手动变速箱-更复杂，但如果使用正确，它们可以提供更好的结果/性能/灵活性。\n\n## 指针汇总\n\n指针有时有点烦琐。事实上，我们对指针的讨论只是对这个主题的介绍。与它们相处融洽的唯一方法就是尽可能多地使用它们。为了完成此项目，您需要了解指针的所有内容如下：\n\n*   指针是存储内存地址的变量。\n*   我们可以将指针传递给函数，以便在被调用函数内直接操作调用函数作用域中的值。\n*   数组名保存第一个元素的内存地址。我们可以将这个地址作为指针传递，因为它就是这样。\n*   We can use pointers to point to memory on the free store. This means we can dynamically allocate large amounts of memory while the game is running.\n\n    提示\n\n    还有更多的方法可以使用指针。一旦我们习惯了使用常规指针，我们将在最终项目中学习**智能指针**。\n\n在我们再次开始编写僵尸竞技场项目之前，还有一个主题需要讨论。\n\n# 标准模板库\n\n**标准模板库（STL）**是数据容器的集合，以及处理我们放入这些容器中的数据的方法。如果我们想更具体，它是一种存储和操作不同类型的 C++ 变量和类的方法。\n\n我们可以将不同的容器看作是定制的和更高级的阵列。STL 是 C++ 的一部分。它不是像 SFML 那样需要设置的可选内容。\n\nSTL 是 C++ 的一部分，因为它的容器和操作它们的代码对于许多应用需要使用的许多类型的代码来说是至关重要的。\n\n简而言之，STL 实现了我们和几乎每个 C++ 程序员几乎都需要的代码，至少在某个时候，并且可能是非常规则的。\n\n如果我们要编写自己的代码来包含和管理数据，那么我们不可能像编写 STL 的人那样高效地编写代码。\n\n因此，通过使用 STL，我们可以保证使用最好的编写代码来管理数据。甚至 SFML 也使用 STL。例如，在引擎盖下，`VertexArray`类使用 STL。\n\n我们需要做的就是从可用的容器中选择正确的容器类型。通过 STL 可用的容器类型包括：\n\n*   **向量**：这就像一个带助推器的阵列。它处理动态调整大小、排序和搜索。这可能是最有用的容器。\n*   **列表**：允许数据排序的容器。\n*   **映射**：允许用户以键/值对的形式存储数据的关联容器。在这里，一条数据是找到另一条数据的“关键”。地图也可以增长和收缩，也可以搜索。\n*   **Set**: A container that guarantees that every element is unique.\n\n    重要提示\n\n    有关 STL 容器类型的完整列表、它们的不同用途和说明，请查看以下链接：[http://www.tutorialspoint.com/cplusplus/cpp_stl_tutorial.htm](http://www.tutorialspoint.com/cplusplus/cpp_stl_tutorial.htm) 。\n\n在僵尸竞技场游戏中，我们将使用地图。\n\n提示\n\n如果您想了解 STL 为我们节省的复杂性，那么请看一下本教程，它实现了列表所能实现的功能。请注意，本教程只实现了列表的最简单的裸体实现：[http://www.sanfoundry.com/cpp-program-implement-single-linked-list/](http://www.sanfoundry.com/cpp-program-implement-single-linked-list/) 。\n\n我们可以很容易地看到，如果我们探索 STL，我们将节省大量的时间并最终得到一个更好的游戏。让我们更仔细地看看如何使用一个 AutoT0-实例，然后我们将看到它将如何在僵尸竞技场游戏中对我们有用。\n\n## 什么是地图？\n\n**映射**是一个可动态调整大小的容器。我们可以轻松地添加和删除元素。与 STL 中的其他容器相比，`map`类的特殊之处在于我们访问其中数据的方式。\n\n`map`实例中的数据成对存储。考虑一个登录到帐户的情况，可能有用户名和密码。地图非常适合查找用户名，然后检查相关密码的值。\n\n一张地图也正好可以显示账户名称和编号，或者公司名称和股价。\n\n注意，当我们使用 STL 中的`map`时，我们决定形成键值对的值的类型。值可以是`string`实例和`int`实例，如账号；`string`实例及用户名、密码等`string`实例；或用户定义的类型，如对象。\n\n下面是一些真实的代码，让我们熟悉`map`。\n\n## 申报地图\n\n这就是我们如何申报`map`：\n\n```cpp\nmap<string, int> accounts;\n```\n\n前一行代码声明了一个名为`accounts`的新`map`，它有一个`string`对象的键，每个对象都将引用一个`int`值。\n\n我们现在可以存储引用`int`类型值的`string`类型的键值对。我们将看看下一步如何做到这一点。\n\n## 向地图添加数据\n\n让我们继续向帐户添加一个键值对：\n\n```cpp\naccounts[\"John\"] = 1234567;\n```\n\n现在，地图中有一个条目可以使用 John 的密钥访问。以下代码向 accounts map 添加了另外两个条目：\n\n```cpp\naccounts[\"Smit\"] = 7654321;\naccounts[\"Larissa\"] = 8866772;\n```\n\n我们的地图上有三个条目。让我们看看如何访问帐号。\n\n## 在地图中查找数据\n\n我们将以与添加数据相同的方式访问数据：使用密钥。例如，我们可以将`Smit`键存储的值分配给新的`int``accountNumber`，如下所示：\n\n```cpp\nint accountNumber = accounts[\"Smit\"];\n```\n\n`int`变量`accountNumber`现在存储值`7654321`。我们可以对`map`实例中存储的值执行任何我们可以对该类型执行的操作。\n\n## 从地图中删除数据\n\n从地图中提取价值也很简单。以下代码行删除键`John`及其关联值：\n\n```cpp\naccounts.erase(\"John\");\n```\n\n让我们看看我们可以用`map`做的更多事情。\n\n## 检查地图的大小\n\n我们可能想知道地图中有多少个键值对。下面的代码行就是这样做的：\n\n```cpp\nint size = accounts.size();\n```\n\n`int`变量`size`现在保存`2`的值。这是因为`accounts`保存了 Smit 和 Larissa 的值，因为我们删除了 John。\n\n## 检查地图上的钥匙\n\n`map`最相关的特性是它能够使用键查找值。我们可以测试特定密钥是否存在，如下所示：\n\n```cpp\nif(accounts.find(\"John\") != accounts.end())\n{\n    // This code won't run because John was erased\n}\nif(accounts.find(\"Smit\") != accounts.end())\n{\n    // This code will run because Smit is in the map\n}\n```\n\n在前面的代码中，`!= accounts.end`值用于确定键何时存在或不存在。如果搜索到的键在`map`中不存在，则`accounts.end`将是`if`语句的结果。\n\n让我们看看如何通过循环映射来测试或使用映射中的所有值。\n\n## 循环/迭代映射的键值对\n\n我们已经了解了如何使用`for` 循环来循环/迭代数组的所有值。但是，如果我们想对地图做这样的事情呢？\n\n下面的代码显示了我们如何循环遍历帐户`map`的每个键值对，并向每个帐号添加一个键值对：\n\n```cpp\nfor (map<string,int>::iterator it = accounts.begin(); \n    it != accounts.end();  \n    ++ it)\n{\n    it->second += 1;\n}\n```\n\n`for`循环的条件可能是前面代码中最有趣的部分。条件的第一部分是最长的部分。`map<string,int>::iterator it = accounts.begin()`如果我们将其分解，则更容易理解。\n\n`map<string,int>::iterator`是一种类型。我们正在声明一个适用于键值对为`string`和`int`的`map`的`iterator`。迭代器的名称为`it`。我们将`accounts.begin()`返回的值赋给`it`。迭代器`it`现在保存`accounts`映射中的第一个键值对。\n\n`for`循环的其余条件如下所示。`it != accounts.end()`表示循环将继续，直到到达映射的末尾，`++ it`只需进入映射中的下一个键值对，每个键值对都会通过循环。\n\n在`for`循环中，`it->second`访问键值对的值，`+= 1`向该值添加一个。请注意，我们可以使用`it->first`访问密钥（这是密钥-值对的第一部分）。\n\n您可能已经注意到，通过映射设置循环的语法非常冗长。C++ 有办法减少这种冗长。\n\n## 自动关键字\n\n`for`循环条件下的代码非常冗长——尤其是在`map<string,int>::iterator`方面。C++ 提供了一个巧妙的方法来减少冗长的 Type T2AER 关键字。使用`auto`关键字，我们可以改进前面的代码：\n\n```cpp\nfor (auto it = accounts.begin(); it != accounts.end(); ++ it)\n{\n    it->second += 1;\n}\n```\n\n`auto`关键字指示编译器自动为我们推断类型。这对于我们编写的下一个类特别有用。\n\n## STL 汇总表\n\n正如我们在本书中所涵盖的几乎所有 C++ 概念一样，STL 是一个巨大的话题。整本书只涵盖 STL。然而，在这一点上，我们已经知道了足够的信息来构建一个使用 STL`map`存储 SFML`Texture`对象的类。然后，我们可以使用文件名作为键值对的键来检索/加载纹理。\n\n为什么我们会达到这种额外的复杂程度，而不是像目前一样继续使用`Texture`类，这一点在我们继续的过程中会变得很明显。\n\n# 质感老人班\n\n成千上万的僵尸代表着一个新的挑战。加载、存储和操作三种不同僵尸纹理的数千份副本不仅会占用大量内存，还会占用大量处理能力。我们将创建一个新类型的类来克服这个问题，并允许我们只存储每个纹理中的一个。\n\n我们还将以这样一种方式对该类进行编码，即该类只能有一个实例。这种类型的类称为**单例**。\n\n提示\n\n单件是一种设计模式。设计模式是一种构造我们的代码的方法，它被证明是有效的。\n\n此外，我们还将对该类进行编码，以便它可以直接通过类名在游戏代码中的任何位置使用，而无需访问实例。\n\n## 对 TextureHolder 头文件进行编码\n\n让我们创建一个新的头文件。在**解决方案资源管理器**中右键点击**头文件**，选择**添加【新增项目】。。。**。在**添加新项目**窗口中，高亮显示**头文件（.h）**，然后在**名称**字段中键入`TextureHolder.h`。\n\n将下面的代码添加到`TextureHolder.h`文件中，然后我们可以讨论它：\n\n```cpp\n#pragma once\n#ifndef TEXTURE_HOLDER_H\n#define TEXTURE_HOLDER_H\n#include <SFML/Graphics.hpp>\n#include <map>\nusing namespace sf;\nusing namespace std;\nclass TextureHolder\n{\nprivate:\n    // A map container from the STL,\n    // that holds related pairs of String and Texture\n    map<    string, Texture> m_Textures;\n    // A pointer of the same type as the class itself\n    // the one and only instance\n    static TextureHolder* m_s_Instance;\npublic:\n    TextureHolder();\n    static Texture& GetTexture(string const& filename);\n};\n#endif\n```\n\n在前面的代码中，请注意我们有一个来自 STL 的针对`map`的`include`指令。我们声明了一个`map`实例，该实例包含`string`类型和 SFML`Texture`类型，以及键值对。`map`被称为`m_Textures`。\n\n在前面的代码中，这一行紧随其后：\n\n```cpp\nstatic TextureHolder* m_s_Instance;\n```\n\n前一行代码非常有趣。我们正在声明一个静态指针，指向名为`m_s_Instance`的`TextureHolder`类型的对象。这意味着`TextureHolder`类有一个与自身类型相同的对象。不仅如此，因为它是静态的，所以可以通过类本身使用它，而不需要类的实例。当我们编写相关的`.cpp`文件时，我们将看到如何使用它。\n\n在类的`public`部分，我们有构造函数的原型`TextureHolder`。构造函数不接受参数，并且通常没有返回类型。这与默认构造函数相同。我们将用一个定义覆盖默认构造函数，该定义使我们的单例按我们希望的方式工作。\n\n我们还有另一个函数名为`GetTexture`。让我们再次查看签名并准确分析发生了什么：\n\n```cpp\nstatic Texture& GetTexture(string const& filename);\n```\n\n首先，请注意，该函数返回对`Texture`的引用。这意味着`GetTexture`将返回一个引用，这是有效的，因为它避免了复制可能是大型图形的内容。另外，请注意，函数被声明为`static`。这意味着该函数可以在没有类实例的情况下使用。该函数将`string`作为常量引用，作为参数。这有两方面的影响。首先，操作是有效的，其次，因为参考是恒定的，所以不能更改。\n\n## 对 TextureHolder 函数定义进行编码\n\n现在，我们可以创建一个新的`.cpp`文件，该文件将包含函数定义。这将使我们能够看到新类型函数和变量背后的原因。在**解决方案资源管理器**中右键点击**源文件**，选择**添加【新增项目】。。。**。在 ORT T8 中，添加新的条目“OT9”窗口，突出显示（通过左键单击）OutT10E.c++ 文件（.CPP）OUTT11T，然后在 ORT T12.名称 No.Tt13.字段中，键入 No.T1。最后，点击**添加**按钮。现在，我们已经准备好编写这个类的代码了。\n\n添加以下代码，然后我们可以讨论它：\n\n```cpp\n#include \"TextureHolder.h\"\n// Include the \"assert feature\"\n#include <assert.h>\nTextureHolder* TextureHolder::m_s_Instance = nullptr;\nTextureHolder::TextureHolder()\n{\n    assert(m_s_Instance == nullptr);\n    m_s_Instance = this;\n}\n```\n\n在前面的代码中，我们将`TextureHolder`类型的指针初始化为`nullptr`。在构造函数中，`assert(m_s_Instance == nullptr)`确保`m_s_Instance`等于`nullptr`。如果没有，游戏将退出执行。然后，`m_s_Instance = this`将指针分配给`this`实例。现在，考虑代码在何处发生。代码在构造函数中。构造函数是我们从类创建对象实例的方式。因此，实际上，我们现在有一个指向`TextureHolder`的指针，它指向自身的唯一实例。\n\n将代码的最后一部分添加到`TextureHolder.cpp`文件中。这里的注释多于代码。请检查以下代码，并在添加代码时阅读注释，然后我们可以浏览它：\n\n```cpp\nTexture& TextureHolder::GetTexture(string const& filename)\n{\n    // Get a reference to m_Textures using m_s_Instance\n    auto& m = m_s_Instance->m_Textures;\n    // auto is the equivalent of map<string, Texture>\n    // Create an iterator to hold a key-value-pair (kvp)\n    // and search for the required kvp\n    // using the passed in file name\n    auto keyValuePair = m.find(filename);\n    // auto is equivalent of map<string, Texture>::iterator\n\n    // Did we find a match?\n    if (keyValuePair != m.end())\n    {\n        // Yes\n        // Return the texture,\n        // the second part of the kvp, the texture\n        return keyValuePair->second;\n    }\n    else\n    {\n        // File name not found\n        // Create a new key value pair using the filename\n        auto& texture = m[filename];\n        // Load the texture from file in the usual way\n        texture.loadFromFile(filename);\n        // Return the texture to the calling code\n        return texture;\n    }\n}\n```\n\n关于前面的代码，您可能会注意到的第一件事是`auto`关键字。`auto`关键字已在上一节中解释。\n\n提示\n\n如果您想知道被`auto`替换的实际类型是什么，那么请在前面代码中每次使用`auto`之后立即查看注释。\n\n在代码的开头，我们得到了对`m_textures`的引用。然后，我们尝试获取由传入文件名（`filename`表示的键值对的迭代器。如果我们找到匹配的关键点，我们将返回带有`return keyValuePair->second`的纹理。否则，我们将纹理添加到贴图中，然后将其返回给调用代码。\n\n诚然，`TextureHolder`类引入了许多新概念（单例、`static`函数、常量引用、`this`和`auto`关键字）和语法。除此之外，我们刚刚了解了指针和 STL，本节的代码可能有点让人望而生畏。\n\n那么，这一切值得吗？\n\n## 我们使用 TextureHolder 取得了哪些成就？\n\n关键是，现在我们有了这个类，我们可以在代码中任意使用纹理，而不用担心内存耗尽或访问特定函数或类中的任何纹理。我们很快就会看到如何使用`TextureHolder`。\n\n# 建造一大群僵尸\n\n现在，我们装备了`TextureHolder`类，以确保我们的僵尸纹理很容易获得，并且只加载到 GPU 一次。然后，我们可以调查创建一个完整的部落。\n\n我们将在阵列中存储僵尸。由于构建和繁殖一大群僵尸的过程涉及到相当多的代码行，因此它是一个很好的抽象到单独函数的候选者。很快，我们将编写`CreateHorde`函数，但首先，当然，我们需要一个`Zombie`类。\n\n## 对 Zombie.h 文件进行编码\n\n构建类来表示僵尸的第一步是在头文件中编写成员变量和函数原型。\n\n在**解决方案资源管理器**中右键点击**头文件**，选择**添加【新增项目】。。。**。在**添加新项目**窗口中，高亮显示**头文件（.h）**，然后在**名称**字段中键入`Zombie.h`。\n\n将以下代码添加到`Zombie.h`文件中：\n\n```cpp\n#pragma once\n#include <SFML/Graphics.hpp>\nusing namespace sf;\nclass Zombie\n{\nprivate:\n    // How fast is each zombie type?\n    const float BLOATER_SPEED = 40;\n    const float CHASER_SPEED = 80;\n    const float CRAWLER_SPEED = 20;\n    // How tough is each zombie type\n    const float BLOATER_HEALTH = 5;\n    const float CHASER_HEALTH = 1;\n    const float CRAWLER_HEALTH = 3;\n    // Make each zombie vary its speed slightly\n    const int MAX_VARRIANCE = 30;\n    const int OFFSET = 101 - MAX_VARRIANCE;\n    // Where is this zombie?\n    Vector2f m_Position;\n    // A sprite for the zombie\n    Sprite m_Sprite;\n    // How fast can this one run/crawl?\n    float m_Speed;\n    // How much health has it got?\n    float m_Health;\n    // Is it still alive?\n    bool m_Alive;\n\n    // Public prototypes go here\n};\n```\n\n前面的代码声明了`Zombie`类的所有私有成员变量。在前面代码的顶部，我们有三个常量变量来控制每种僵尸的速度：一个非常慢的爬虫，一个稍微快一点的 Bloater 和一个稍微快一点的 Chaser。我们可以尝试这三个常数的值，以帮助平衡游戏的难度水平。这里还值得一提的是，这三个值仅用作每种僵尸类型速度的起始值。正如我们将在本章后面看到的，我们将根据这些值改变每个僵尸的速度一小部分。这可以防止同类僵尸在追逐玩家时聚集在一起。\n\n接下来的三个常量确定每种僵尸类型的运行状况级别。请注意，膨胀者是最难对付的，其次是爬虫。作为一种平衡，追击者僵尸将是最容易被杀死的。\n\n接下来，我们还有两个常数，`MAX_VARRIANCE`和`OFFSET`。这些将帮助我们确定每个僵尸的速度。我们将在编写`Zombie.cpp`文件时看到具体的编码方式。\n\n在这些常量之后，我们声明了一组看起来很熟悉的变量，因为我们的`Player` 类中有非常相似的变量。`m_Position`、`m_Sprite`、`m_Speed`和`m_Health`变量的名称意味着：僵尸对象的位置、精灵、速度和健康状况。\n\n最后，在前面的代码中，我们声明了一个名为`m_Alive`的布尔值，当僵尸活着并正在狩猎时，它将为真，但当它的生命值为零时，它将为假，并且它只是在我们原本漂亮的背景上的一点血迹。\n\n现在，我们可以完成`Zombie.h`文件了。添加以下代码中突出显示的函数原型，然后我们将讨论它们：\n\n```cpp\n    // Is it still alive?\n    bool m_Alive;\n\n    // Public prototypes go here    \npublic:\n\n    // Handle when a bullet hits a zombie\n    bool hit();\n    // Find out if the zombie is alive\n    bool isAlive();\n    // Spawn a new zombie\n    void spawn(float startX, float startY, int type, int seed);\n    // Return a rectangle that is the position in the world\n    FloatRect getPosition();\n    // Get a copy of the sprite to draw\n    Sprite getSprite();\n    // Update the zombie each frame\n    void update(float elapsedTime, Vector2f playerLocation);\n};\n```\n\n在前面的代码中，有一个`hit`函数，我们可以在每次僵尸被子弹击中时调用它。然后，该函数可以采取必要的步骤，例如从僵尸身上获取生命值（降低`m_Health`的值）或杀死僵尸（将`m_Alive`设置为 false）。\n\n`isAlive`函数返回一个布尔值，让调用代码知道僵尸是活的还是死的。我们不想执行碰撞检测，也不想让玩家在血溅上行走时失去生命。\n\n`spawn`函数采用一个起始位置、一个类型（Crawler、Bloater 或 Chaser，由`int`表示）以及一个种子，用于我们将在下一节中看到的一些随机数生成。\n\n就像我们在`Player`类中所做的一样，`Zombie`类具有`getPosition`和`getSprite`函数，以获得一个矩形，该矩形表示每帧可以绘制的僵尸和精灵所占据的空间。\n\n前面代码中的最后一个原型是`update`函数。我们可能已经猜到它将接收到自最后一帧以来经过的时间，但也注意到它接收到一个称为`playerLocation`的`Vector2f`向量。这个向量实际上就是玩家中心的精确坐标。我们将很快看到如何使用这个向量来追踪玩家。\n\n现在，我们可以在`.cpp`文件中对函数定义进行编码。\n\n## 对 Zombie.cpp 文件进行编码\n\n接下来，我们将编码`Zombie`类的功能——函数定义。\n\n创建一个新的`.cpp`文件，该文件将包含函数定义。在**解决方案资源管理器**中右键点击**源文件**，选择**添加【新增项目】。。。**。在 ORT T8 中，添加新的条目“OT9”窗口，突出显示（通过左键单击）OutT10E.c++ 文件（.CPP）OUTT11T，然后在 ORT T12.名称 No.Tt13.字段中，键入 No.T1。最后，点击**添加**按钮。现在，我们已经准备好编写这个类的代码了。\n\n将以下代码添加到`Zombie.cpp`文件中：\n\n```cpp\n#include \"zombie.h\"\n#include \"TextureHolder.h\"\n#include <cstdlib>\n#include <ctime>\nusing namespace std;\n```\n\n首先，我们添加必要的 include 指令，然后添加`using namespace std`。您可能还记得我们在对象声明前面加上`std::`前缀的几个例子。这个`using`指令意味着我们不需要对这个文件中的代码执行此操作。\n\n现在，添加以下代码，这是`spawn`函数的定义。添加代码后，请研究代码，然后我们将讨论它：\n\n```cpp\nvoid Zombie::spawn(float startX, float startY, int type, int seed)\n{\n\n    switch (type)\n    {\n    case 0:\n        // Bloater\n        m_Sprite = Sprite(TextureHolder::GetTexture(\n            \"graphics/bloater.png\"));\n        m_Speed = BLOATER_SPEED;\n        m_Health = BLOATER_HEALTH;\n        break;\n    case 1:\n        // Chaser\n        m_Sprite = Sprite(TextureHolder::GetTexture(\n            \"graphics/chaser.png\"));\n        m_Speed = CHASER_SPEED;\n        m_Health = CHASER_HEALTH;\n        break;\n    case 2:\n        // Crawler\n        m_Sprite = Sprite(TextureHolder::GetTexture(\n            \"graphics/crawler.png\"));\n        m_Speed = CRAWLER_SPEED;\n        m_Health = CRAWLER_HEALTH;\n        break;\n    }\n    // Modify the speed to make the zombie unique\n    // Every zombie is unique. Create a speed modifier\n    srand((int)time(0) * seed);\n    // Somewhere between 80 and 100\n    float modifier = (rand() % MAX_VARRIANCE) + OFFSET;\n    // Express this as a fraction of 1\n    modifier /= 100; // Now equals between .7 and 1\n    m_Speed *= modifier;\n\n    // Initialize its location\n    m_Position.x = startX;\n    m_Position.y = startY;\n    // Set its origin to its center\n    m_Sprite.setOrigin(25, 25);\n    // Set its position\n    m_Sprite.setPosition(m_Position);\n}\n```\n\n函数所做的第一件事是基于作为参数传入的`int`值的`switch`执行路径。在`switch`块中，每种类型的僵尸都有一个`case`。根据僵尸的类型，相应的纹理、速度和健康状况将初始化为相关的成员变量。\n\n提示\n\n我们可以对不同类型的僵尸使用枚举。项目完成后，请随时升级您的代码。\n\n有趣的是，我们使用静态`TextureHolder::GetTexture`函数来指定纹理。这意味着无论我们产生多少僵尸，GPU 内存中最多会有三个纹理。\n\n接下来的三行代码（不包括注释）执行以下操作：\n\n*   将作为参数传入的`seed`变量植入随机数生成器。\n*   使用`rand`函数和`MAX_VARRIANCE`和`OFFSET`常量声明并初始化`modifier`变量。结果是 0 到 1 之间的分数，可以用来使每个僵尸的速度唯一。我们之所以要这么做，是为了让僵尸们不会在彼此身上挤得太多。\n*   我们现在可以将`m_Speed`乘以`modifier`，我们将得到一个僵尸，其速度在为此类僵尸速度定义的常数`MAX_VARRIANCE`%之内。\n\n在解决速度问题后，我们将`startX`和`startY`中的已通过位置分别分配给`m_Position.x`和`m_Position.y`。\n\n上一个清单中的最后两行代码将精灵的原点设置为中心，并使用`m_Position`向量设置精灵的位置。\n\n现在，将`hit`函数的以下代码添加到`Zombie.cpp`文件中：\n\n```cpp\nbool Zombie::hit()\n{\n    m_Health--;\n    if (m_Health < 0)\n    {\n        // dead\n        m_Alive = false;\n        m_Sprite.setTexture(TextureHolder::GetTexture(\n            \"graphics/blood.png\"));\n        return true; \n    }\n    // injured but not dead yet\n    return false;\n}\n```\n\n`hit`功能好且简单：将`m_Health`减少一，然后检查`m_Health`是否低于零。\n\n如果它低于零，那么它将`m_Alive`设置为 false，将僵尸的纹理交换为血溅，并将`true`返回到调用代码，以便它知道僵尸现在已经死了。如果僵尸幸存下来，命中将返回`false`。\n\n添加以下三个 getter 函数，它们只向调用代码返回一个值：\n\n```cpp\nbool Zombie::isAlive()\n{\n    return m_Alive;\n}\nFloatRect Zombie::getPosition()\n{\n    return m_Sprite.getGlobalBounds();\n}\nSprite Zombie::getSprite()\n{\n    return m_Sprite;\n}\n```\n\n前面的三个函数都是不言自明的，可能除了`getPosition`函数之外，它使用`m_Sprite.getLocalBounds`函数获取`FloatRect`实例，然后返回给调用代码。\n\n最后，对于`Zombie`类，我们需要为`update`函数添加代码。仔细看一下下面的代码，然后我们将仔细阅读：\n\n```cpp\nvoid Zombie::update(float elapsedTime, \n    Vector2f playerLocation)\n{\n    float playerX = playerLocation.x;\n    float playerY = playerLocation.y;\n    // Update the zombie position variables\n    if (playerX > m_Position.x)\n    {\n        m_Position.x = m_Position.x + \n            m_Speed * elapsedTime;\n    }\n    if (playerY > m_Position.y)\n    {\n        m_Position.y = m_Position.y + \n            m_Speed * elapsedTime;\n    }\n\n    if (playerX < m_Position.x)\n    {\n        m_Position.x = m_Position.x - \n            m_Speed * elapsedTime;\n    }\n    if (playerY < m_Position.y)\n    {\n        m_Position.y = m_Position.y - \n            m_Speed * elapsedTime;\n    }\n    // Move the sprite\n    m_Sprite.setPosition(m_Position);\n    // Face the sprite in the correct direction\n    float angle = (atan2(playerY - m_Position.y,\n        playerX - m_Position.x)\n        * 180) / 3.141;\n    m_Sprite.setRotation(angle);\n}\n```\n\n在前面的代码中，我们将`playerLocation.x`和`playerLocation.y`复制到名为`playerX`和`playerY`的局部变量中。\n\n接下来，有四个`if`语句。他们测试僵尸是否在当前玩家位置的左侧、右侧、上方或下方。这四个`if`语句在计算为 true 时，使用通常的公式（即速度乘以自上一帧起的时间）适当调整僵尸的`m_Position.x`和`m_Position.y`值。更具体地说，代码是 `m_Speed * elapsedTime`。\n\n在四个`if` 语句之后，`m_Sprite`被移动到它的新位置。\n\n然后，我们使用之前用于玩家和鼠标指针的相同计算，但这次，我们用于僵尸和玩家。此计算将找到面向玩家的僵尸所需的角度。\n\n最后，对于这个函数和类，我们调用`m_Sprite.setRotation`来实际旋转僵尸精灵。请记住，此函数将在游戏的每一帧中为每个活僵尸调用。\n\n但是，我们想要一大群僵尸。\n\n## 使用僵尸类创建部落\n\n现在，我们有了一个类来创建一个活的、攻击的和可杀死的僵尸，我们想要繁殖一整群僵尸。\n\n为了实现这一点，我们将编写一个单独的函数，并使用一个指针，以便我们可以引用我们的部落，该部落将在`main`中声明，但配置在不同的范围内。\n\n在 Visual Studio 中打开`ZombieArena.h`文件并添加以下突出显示的代码行：\n\n```cpp\n#pragma once\n#include \"Zombie.h\"\nusing namespace sf;\nint createBackground(VertexArray& rVA, IntRect arena);\nZombie* createHorde(int numZombies, IntRect arena);\n```\n\n现在我们有了一个原型，我们可以编写函数定义了。\n\n创建一个新的`.cpp`文件，该文件将包含函数定义。在**解决方案资源管理器**中右键点击**源文件**，选择**添加【新增项目】。。。**。在 ORT T8 中，添加新的条目“OT9”窗口，突出显示（通过左键单击）OutT10E.c++ 文件（.CPP）OUTT11T，然后在 ORT T12.名称 No.Tt13.字段中，键入 No.T1。最后，点击**添加**按钮。\n\n将以下代码添加到`CreateHorde.cpp`文件中并进行研究。之后，我们将把它分成几个部分进行讨论：\n\n```cpp\n#include \"ZombieArena.h\"\n#include \"Zombie.h\"\nZombie* createHorde(int numZombies, IntRect arena) \n{\n    Zombie* zombies = new Zombie[numZombies];\n    int maxY = arena.height - 20;\n    int minY = arena.top + 20;\n    int maxX = arena.width - 20;\n    int minX = arena.left + 20;\n    for (int i = 0; i < numZombies; i++)\n    {\n\n        // Which side should the zombie spawn\n        srand((int)time(0) * i);\n        int side = (rand() % 4);\n        float x, y;\n        switch (side)\n        {\n        case 0:\n            // left\n            x = minX;\n            y = (rand() % maxY) + minY;\n            break;\n        case 1:\n            // right\n            x = maxX;\n            y = (rand() % maxY) + minY;\n            break;\n        case 2:\n            // top\n            x = (rand() % maxX) + minX;\n            y = minY;\n            break;\n        case 3:\n            // bottom\n            x = (rand() % maxX) + minX;\n            y = maxY;\n            break;\n        }\n        // Bloater, crawler or runner\n        srand((int)time(0) * i * 2);\n        int type = (rand() % 3);\n        // Spawn the new zombie into the array\n        zombies[i].spawn(x, y, type, i);\n\n    }\n    return zombies;\n}\n```\n\n让我们再看一次前面的所有代码，一小段一小段。首先，我们添加了现在熟悉的`include`指令：\n\n```cpp\n#include \"ZombieArena.h\"\n#include \"Zombie.h\"\n```\n\n接下来是函数签名。请注意，函数必须返回指向`Zombie`对象的指针。我们将创建一个`Zombie`对象数组。创建完部落后，我们将返回阵列。当我们返回数组时，实际上是返回数组第一个元素的地址。正如我们在本章前面关于指针的部分中所了解的，这与指针是一样的。签名还表明我们有两个参数。第一个，`numZombies`是当前部落需要的僵尸数量，第二个，`arena`是一个`IntRect`，它容纳了创建该部落的当前竞技场的规模。\n\n在函数签名之后，我们声明一个指向名为`zombies`的`Zombie`类型的指针，并使用数组第一个元素的内存地址对其进行初始化，我们在堆上动态分配该内存地址：\n\n```cpp\nZombie* createHorde(int numZombies, IntRect arena) \n{\n    Zombie* zombies = new Zombie[numZombies];\n```\n\n代码的下一部分只是将竞技场的末端复制到`maxY`、`minY`、`maxX`和`minX`。我们从右侧和底部减去 20 个像素，同时在顶部和左侧添加 20 个像素。我们使用这四个局部变量来帮助定位每个僵尸。我们进行了 20 像素的调整，以阻止僵尸出现在墙上：\n\n```cpp\nint maxY = arena.height - 20;\nint minY = arena.top + 20;\nint maxX = arena.width - 20;\nint minX = arena.left + 20;\n```\n\n现在，我们输入一个`for`循环，它将从零到`numZombies`循环`zombies`数组中的每个`Zombie`对象：\n\n```cpp\nfor (int i = 0; i < numZombies; i++)\n```\n\n在`for`循环中，代码要做的第一件事是给随机数生成器种子，然后生成一个介于 0 和 3 之间的随机数。此编号存储在`side`变量中。我们将使用`side`变量来决定僵尸是在竞技场的左侧、顶部、右侧还是底部产卵。我们还声明了两个`int`变量`x`和`y`。这两个变量将暂时保持当前僵尸的实际水平和垂直坐标：\n\n```cpp\n// Which side should the zombie spawn\nsrand((int)time(0) * i);\nint side = (rand() % 4);\nfloat x, y;\n```\n\n仍然在`for`循环中，我们有一个包含四个`case`语句的`switch`块。请注意，`case`语句用于`0`、`1`、`2`和`3`，并且`switch`语句中的参数是`side`。在每个`case`块中，我们使用一个预定值初始化`x`和`y`，该预定值可以是`minX`、`maxX`、`minY`或`maxY`以及一个随机生成的值。仔细观察每个预定值和随机值的组合。您将看到它们适用于在左侧、顶部、右侧或底部随机定位当前僵尸。这样做的效果是，每个僵尸都可以在竞技场外缘的任意位置随机产卵：\n\n```cpp\nswitch (side)\n{\n    case 0:\n        // left\n        x = minX;\n        y = (rand() % maxY) + minY;\n        break;\n    case 1:\n        // right\n        x = maxX;\n        y = (rand() % maxY) + minY;\n        break;\n    case 2:\n        // top\n        x = (rand() % maxX) + minX;\n        y = minY;\n        break;\n    case 3:\n        // bottom\n        x = (rand() % maxX) + minX;\n        y = maxY;\n        break;        \n}\n```\n\n仍然在`for`循环中，我们再次为随机数生成器播种，并生成一个介于 0 和 2 之间的随机数。我们将这个数字存储在`type`变量中。`type`变量将确定当前僵尸是追逐者、Bloater 还是爬虫。\n\n确定类型后，我们对`zombies`数组中的当前`Zombie`对象调用`spawn`函数。作为提醒，发送到`spawn`函数的参数决定了僵尸的起始位置和僵尸的类型。显然是任意的`i` 被传入，因为它被用作唯一的种子，在适当的范围内随机改变僵尸的速度。这就阻止了我们的僵尸“聚在一起”，变成一团而不是一个部落：\n\n```cpp\n// Bloater, crawler or runner\nsrand((int)time(0) * i * 2);\nint type = (rand() % 3);\n// Spawn the new zombie into the array\nzombies[i].spawn(x, y, type, i);\n```\n\n对于每个僵尸，`for` 循环自身重复一次，由`numZombies`中包含的值控制，然后我们返回数组。作为另一个提醒，数组只是其自身第一个元素的地址。数组是在堆上动态分配的，因此它在函数返回后仍然存在：\n\n```cpp\nreturn zombies;\n```\n\n现在，我们可以让僵尸复活了。\n\n## 让部落复活（复活）\n\n我们有一个`Zombie`类和一个函数来创建一个随机产卵的部落。我们有`TextureHolder`单体作为一种简洁的方式来保存三种纹理，可以用于几十甚至数千个僵尸。现在，我们可以在`main`中将部落添加到我们的游戏引擎中。\n\n添加以下突出显示的代码以包括`TextureHolder`类。然后，就在`main`里面，我们将初始化`TextureHolder`的一个也是唯一一个实例，它可以在我们游戏的任何地方使用：\n\n```cpp\n#include <SFML/Graphics.hpp>\n#include \"ZombieArena.h\"\n#include \"Player.h\"\n#include \"TextureHolder.h\"\nusing namespace sf;\nint main()\n{\n    // Here is the instance of TextureHolder\n    TextureHolder holder;\n    // The game will always be in one of four states\n    enum class State { PAUSED, LEVELING_UP, GAME_OVER, PLAYING };\n    // Start with the GAME_OVER state\n    State state = State::GAME_OVER;\n```\n\n下面几行突出显示的代码声明了一些控制变量，这些变量用于波形开始时的僵尸数量、仍将被杀死的僵尸数量，当然还有一个名为`zombies`的指向`Zombie`的指针，我们将其初始化为`nullptr`：\n\n```cpp\n// Create the background\nVertexArray background;\n// Load the texture for our background vertex array\nTexture textureBackground;\ntextureBackground.loadFromFile(\"graphics/background_sheet.png\");\n// Prepare for a horde of zombies\nint numZombies;\nint numZombiesAlive;\nZombie* zombies = nullptr;\n// The main game loop\nwhile (window.isOpen())\n```\n\n接下来，在嵌套在`LEVELING_UP`节中的`PLAYING`节中，我们添加了执行以下操作的代码：\n\n*   将`numZombies`初始化为`10`。随着项目的进展，这最终将是动态的，并基于当前波数。\n*   删除任何先前存在的已分配内存。否则，每次对`createHorde`的新呼叫都会逐渐占用更多内存，但不会释放先前部落的内存。\n*   然后调用`createHorde`并将返回的内存地址分配给`zombies`。\n*   我们也用`numZombies` 初始化`zombiesAlive`，因为我们现在还没有杀死任何人。\n\n添加我们刚才讨论过的以下突出显示的代码：\n\n```cpp\nif (state == State::PLAYING)\n{\n    // Prepare the level\n    // We will modify the next two lines later\n    arena.width = 500;\n    arena.height = 500;\n    arena.left = 0;\n    arena.top = 0;\n    // Pass the vertex array by reference \n    // to the createBackground function\n    int tileSize = createBackground(background, arena);\n    // Spawn the player in the middle of the arena\n    player.spawn(arena, resolution, tileSize);\n    // Create a horde of zombies\n    numZombies = 10;\n    // Delete the previously allocated memory (if it exists)\n    delete[] zombies;\n    zombies = createHorde(numZombies, arena);\n    numZombiesAlive = numZombies;\n    // Reset the clock so there isn't a frame jump\n    clock.restart();\n}\n```\n\n现在，将以下突出显示的代码添加到`ZombieArena.cpp`文件中：\n\n```cpp\n/*\n ****************\n UPDATE THE FRAME\n ****************\n */\nif (state == State::PLAYING)\n{\n    // Update the delta time\n    Time dt = clock.restart();\n    // Update the total game time\n    gameTimeTotal += dt;\n    // Make a decimal fraction of 1 from the delta time\n    float dtAsSeconds = dt.asSeconds();\n    // Where is the mouse pointer\n    mouseScreenPosition = Mouse::getPosition();\n    // Convert mouse position to world coordinates of mainView\n    mouseWorldPosition = window.mapPixelToCoords(\n        Mouse::getPosition(), mainView);\n    // Update the player\n    player.update(dtAsSeconds, Mouse::getPosition());\n    // Make a note of the players new position\n    Vector2f playerPosition(player.getCenter());\n    // Make the view centre around the player                \n    mainView.setCenter(player.getCenter());\n    // Loop through each Zombie and update them\n    for (int i = 0; i < numZombies; i++)\n    {\n        if (zombies[i].isAlive())\n        {\n            zombies[i].update(dt.asSeconds(), playerPosition);\n        }\n    }\n}// End updating the scene\n```\n\n前面的新代码所做的就是循环遍历僵尸数组，检查当前僵尸是否处于活动状态，如果是，则使用必要的参数调用其`update`函数。\n\n添加以下代码以绘制所有僵尸：\n\n```cpp\n/*\n **************\n Draw the scene\n **************\n */\nif (state == State::PLAYING)\n{\n    window.clear();\n    // set the mainView to be displayed in the window\n    // And draw everything related to it\n    window.setView(mainView);\n    // Draw the background\n    window.draw(background, &textureBackground);\n    // Draw the zombies\n    for (int i = 0; i < numZombies; i++)\n    {\n        window.draw(zombies[i].getSprite());\n    }\n    // Draw the player\n    window.draw(player.getSprite());\n}\n```\n\n前面的代码循环遍历所有僵尸，并调用`getSprite`函数以允许`draw`函数执行其工作。我们不检查僵尸是否还活着，因为即使僵尸已经死了，我们也要吸取血迹。\n\n在 main 函数的末尾，我们需要确保删除指针，因为这是一个很好的实践，而且通常是必不可少的。但是，从技术上讲，这并不是必需的，因为游戏即将退出，操作系统将回收在`return 0`语句之后使用的所有内存：\n\n```cpp\n    }// End of main game loop\n     // Delete the previously allocated memory (if it exists)\n    delete[] zombies;\n    return 0;\n}\n```\n\n你可以运行游戏，看到僵尸在竞技场边缘产卵。他们会立即以不同的速度直奔玩家。只是为了好玩，我增加了竞技场的大小，并将僵尸数量增加到 1000 个，正如您在下面的屏幕截图中所看到的：\n\n![](img/B14278_10_01.jpg)\n\n这将是一个糟糕的结局！\n\n请注意，由于我们在[*第 8 章*](08.html#_idTextAnchor183)、*SFML 视图中编写的代码，您还可以使用*回车*键暂停并恢复部落的进攻——开始僵尸射击游戏*。\n\n让我们修正一些类仍然直接使用`Texture`实例的事实，并将其修改为使用新的`TextureHolder`类。\n\n# 对所有纹理使用 TextureHolder 类\n\n既然我们有`TextureHolder`类，我们不妨保持一致，并使用它加载所有纹理。让我们对为背景精灵表和玩家加载纹理的现有代码进行一些非常小的修改。\n\n## 改变背景获取纹理的方式\n\n在`ZombieArena.cpp`文件中，找到以下代码：\n\n```cpp\n// Load the texture for our background vertex array\nTexture textureBackground;\ntextureBackground.loadFromFile(\"graphics/background_sheet.png\");\n```\n\n删除前面突出显示的代码，并将其替换为以下突出显示的代码，该代码使用我们新的`TextureHolder`类：\n\n```cpp\n// Load the texture for our background vertex array\nTexture textureBackground = TextureHolder::GetTexture(\n    \"graphics/background_sheet.png\");\n```\n\n让我们更新`Player`类获取纹理的方式。\n\n## 改变玩家获得纹理的方式\n\n在`Player.cpp`文件中，在构造函数内部，找到以下代码：\n\n```cpp\n#include \"player.h\"\nPlayer::Player()\n{\n    m_Speed = START_SPEED;\n    m_Health = START_HEALTH;\n    m_MaxHealth = START_HEALTH;\n    // Associate a texture with the sprite\n    // !!Watch this space!!\n    m_Texture.loadFromFile(\"graphics/player.png\");\n    m_Sprite.setTexture(m_Texture);\n    // Set the origin of the sprite to the centre, \n    // for smooth rotation\n    m_Sprite.setOrigin(25, 25);\n}\n```\n\n删除前面突出显示的代码，并将其替换为以下突出显示的代码，该代码使用我们新的`TextureHolder`类。另外，添加`include`指令，将`TextureHolder`头添加到文件中。新代码在上下文中突出显示，如下所示：\n\n```cpp\n#include \"player.h\"\n#include \"TextureHolder.h\"\nPlayer::Player()\n{\n    m_Speed = START_SPEED;\n    m_Health = START_HEALTH;\n    m_MaxHealth = START_HEALTH;\n    // Associate a texture with the sprite\n    // !!Watch this space!!\n    m_Sprite = Sprite(TextureHolder::GetTexture(\n            \"graphics/player.png\"));\n    // Set the origin of the sprite to the centre, \n    // for smooth rotation\n    m_Sprite.setOrigin(25, 25);\n}\n```\n\n提示\n\n从现在开始，我们将使用`TextureHolder`类加载所有纹理。\n\n# 总结\n\n在本章中，我们讨论了指针，并讨论了它们是保存特定类型对象的内存地址的变量。随着这本书的进展和指针的力量的显现，这本书的全部意义将开始显露出来。我们还使用了指针来创建一个庞大的僵尸群，可以使用指针访问这些僵尸，结果发现指针与数组的第一个元素是一样的。\n\n我们学习了 STL，尤其是`map`课程。我们实现了一个类，该类将存储所有纹理，并提供对它们的访问。\n\n你可能已经注意到僵尸看起来并不是很危险。它们只是在玩家身上漂移而不留下划痕。目前，这是一件好事，因为球员没有办法保护自己。\n\n在下一章中，我们将再创建两个类：一个用于弹药和生命拾取，另一个用于玩家可以射击的子弹。在我们完成这些之后，我们将学习如何检测碰撞，以便子弹和僵尸造成一定的伤害，并且玩家可以收集拾音器。\n\n# 常见问题\n\n以下是您可能想到的一些问题：\n\nQ） 指针和引用之间有什么区别？\n\nA） 指针就像带有助推器的引用。指针可以更改为指向不同的变量（内存地址），也可以指向空闲存储上动态分配的内存。\n\nQ） 数组和指针是怎么回事？\n\nA） 数组实际上是指向第一个元素的常量指针。\n\nQ） 你能提醒我关于`new`关键字和内存泄漏的事吗？\n\nA） 当我们使用`new`关键字在空闲存储上使用内存时，即使创建它的函数已返回且所有局部变量都已消失，它也会持续存在。当我们在空闲存储器上使用完内存后，我们必须释放它。因此，如果我们在空闲存储上使用内存，并且希望在函数的生命周期之后继续使用，那么我们必须确保保留一个指向它的指针，否则我们将泄漏内存。这就像把我们所有的东西都放在家里，然后忘记我们住在哪里！当我们从`createHorde`返回`zombies`数组时，就像将中继接力棒（内存地址）从`createHorde`传递到`main`。这就像说，*好吧，这是你的僵尸大军——他们现在是你的责任*。而且，我们不希望任何泄漏的僵尸在我们的内存中到处跑！所以，我们必须记住调用`delete`指向动态分配内存的指针。"
  },
  {
    "path": "docs/begin-cpp-game-prog/11.md",
    "content": "# 十一、碰撞检测，拾音器和子弹\n\n到目前为止，我们已经实现了游戏的主要视觉元素。 我们有一个可控制的角色在充满僵尸追逐的竞技场中奔跑。 问题是它们之间没有相互作用。 僵尸可以毫无痕迹地穿过玩家的身体。 我们需要检测僵尸和玩家之间的冲突。\n\n如果僵尸能够伤害并最终杀死玩家，那么我们就应该为玩家的枪支提供一些子弹。 然后我们需要确保子弹能击中并杀死僵尸。\n\n与此同时，如果我们正在为子弹、僵尸和玩家编写碰撞检测代码，那么就应该添加一个关于生命值和弹药拾取器的类。\n\n以下是我们将要做的事情，以及我们将在这一章中讨论的顺序:\n\n*   发射子弹\n*   添加十字线和隐藏鼠标指针\n*   产卵皮卡\n*   碰撞检测\n\n让我们从`Bullet`课开始。\n\n# 编写 Bullet 类\n\n我们将使用 SFML`RectangleShape`类直观地表示一个子弹。 我们将编码一个具有`RectangleShape`成员的`Bullet`类，以及其他成员数据和函数。 然后，我们将添加子弹到我们的游戏几个步骤，如下:\n\n1.  首先，我们将对`Bullet.h`文件进行编码。 这将揭示成员数据的所有细节和函数的原型。\n2.  接下来，我们将编码`Bullet.cpp`文件，当然，该文件将包含`Bullet`类的所有函数的定义。 当我们逐步进行时，我将准确地解释`Bullet`类型的对象如何工作和被控制。\n3.  最后，我们将在`main`函数中声明一个完整的数组。 我们还将实施射击控制方案，管理玩家的剩余弹药和重新装填。\n\n让我们从第一步开始。\n\n## 对 Bullet 头文件进行编码\n\n要创建新的头文件，右键单击**Solution Explorer**中的**header Files**，选择**Add | new Item…** 。 在**Add New Item**窗口中，高亮(通过左键点击)**Header File (.h)**，然后在**Name**字段中，键入`Bullet.h`。\n\n在`Bullet.h`文件中添加以下私有成员变量和`Bullet`类声明。 然后我们可以浏览它们并解释它们的用途:\n\n```cpp\n#pragma once\n#include <SFML/Graphics.hpp>\nusing namespace sf;\nclass Bullet\n{\nprivate:\n    // Where is the bullet?\n    Vector2f m_Position;\n    // What each bullet looks like\n    RectangleShape m_BulletShape;\n    // Is this bullet currently whizzing through the air\n    bool m_InFlight = false;\n    // How fast does a bullet travel?\n    float m_BulletSpeed = 1000;\n    // What fraction of 1 pixel does the bullet travel, \n    // Horizontally and vertically each frame?\n    // These values will be derived from m_BulletSpeed\n    float m_BulletDistanceX;\n    float m_BulletDistanceY;\n\n    // Some boundaries so the bullet doesn't fly forever\n    float m_MaxX;\n    float m_MinX;\n    float m_MaxY;\n    float m_MinY;\n// Public function prototypes go here\n};\n```\n\n在之前的代码中，第一个成员是名为`m_Position`的`Vector2f`，它将保存子弹在游戏世界中的位置。\n\n接下来，我们将`RectangleShape`命名为`m_BulletShape`，因为我们为每个子弹使用了一个简单的非纹理图像，有点像我们在《Timber!!》中设置的时间条。\n\n然后代码声明`Boolean`， `m_InFlight`，这将跟踪子弹当前是否在空中呼啸而过。 这将允许我们决定是否需要在每一帧调用它的`update`函数，以及是否需要运行碰撞检测检查。\n\n变量`float``m_BulletSpeed`将(你可能猜到)保持子弹将以像素每秒的速度飞行。 它被初始化为`1000`的值，这有点随意，但它工作得很好。\n\n接下来，我们有另外两个`float`变量，`m_BulletDistanceX`和`m_BulletDistanceY`。 因为移动子弹的计算比移动僵尸或玩家的计算要复杂一些，所以我们可以利用这两个变量进行计算。 它们将被用来决定水平和垂直的变化，在每一帧的子弹的位置。\n\n最后，我们还有 4 个`float`变量(`m_MaxX`、`m_MinX`、`m_MaxY`和`m_MinY`)，这些变量稍后将被初始化以保存子弹的最大和最小位置以及水平和垂直位置。\n\n其中一些变量的需求可能不会立即显现，但当我们在`Bullet.cpp`文件中看到它们的实际作用时，就会变得更加清楚。\n\n现在，将所有的公共函数原型添加到`Bullet.h`文件中:\n\n```cpp\n// Public function prototypes go here\npublic:\n // The constructor\n Bullet();\n // Stop the bullet\n void stop();\n // Returns the value of m_InFlight\n bool isInFlight();\n // Launch a new bullet\n void shoot(float startX, float startY,\n float xTarget, float yTarget);\n // Tell the calling code where the bullet is in the world\n FloatRect getPosition();\n // Return the actual shape (for drawing)\n RectangleShape getShape();\n // Update the bullet each frame\n void update(float elapsedTime);\n};\n```\n\n让我们依次运行每个函数，然后我们可以继续编写它们的定义。\n\n首先，我们有`Bullet`函数，它当然是构造函数。 在这个函数中，我们将设置每个`Bullet`实例，以便进行操作。\n\n`stop`函数将在子弹已经开始运行但需要停止时被调用。\n\n函数返回一个布尔值，用于测试子弹当前是否正在飞行。\n\n`shoot`函数的用途从它的名字就可以看出，但是它的工作原理值得我们进行一些讨论。 现在，只需注意它将传入四个`float`参数。 这 4 个值代表子弹的起点(即玩家所在位置)水平和垂直位置，以及目标的垂直和水平位置(即十字准星所在位置)。\n\n函数返回一个代表子弹位置的`FloatRect`。 这个函数将用于检测与僵尸的碰撞。 你可能还记得[*第 10 章*](10.html#_idTextAnchor214)、*指针、标准模板库和纹理管理*，僵尸也有`getPosition`函数。\n\n接下来是`getShape`函数，它返回一个`RectangleShape`类型的对象。 正如我们所讨论的，每颗子弹都由一个`RectangleShape`物体直观地表示。 因此，`getShape`函数将用于获取`RectangleShape`当前状态的一个副本，以便绘制它。\n\n最后，正如预期的那样，有一个`update`函数，它有一个`float`参数，该参数表示自上次`update`调用以来已经过的时间的几分之一秒。 `update`方法将改变每帧子弹的位置。\n\n让我们看看并编写函数定义。\n\n## 编码 Bullet 源文件\n\n现在，我们可以创建一个包含函数定义的新`.cpp`文件。 右键单击**Solution Explorer**中的**Source Files**，选择**Add | New Item…** 。 在**Add New Item**窗口中，高亮(通过左键点击)**c++ File (.cpp)**，然后在**Name**字段中，键入`Bullet.cpp`。 最后，点击**添加**按钮。 现在我们已经准备好编写类了。\n\n添加以下代码，用于 include 指令和构造函数。 我们知道它是一个构造函数，因为函数与类具有相同的名称:\n\n```cpp\n#include \"bullet.h\"\n// The constructor\nBullet::Bullet()\n{\n    m_BulletShape.setSize(sf::Vector2f(2, 2));\n}\n```\n\n`Bullet`构造函数唯一需要做的事情就是设置`m_BulletShape`的大小，也就是`RectangleShape`对象。 代码将大小设置为两个像素乘两个像素。\n\n接下来，我们将编码更重要的`shoot`函数。 将以下代码添加到`Bullet.cpp`文件中并研究它，然后我们就可以讨论它了:\n\n```cpp\nvoid Bullet::shoot(float startX, float startY,\n    float targetX, float targetY)\n{\n    // Keep track of the bullet\n    m_InFlight = true;\n    m_Position.x = startX;\n    m_Position.y = startY;\n    // Calculate the gradient of the flight path\n    float gradient = (startX - targetX) / (startY - targetY);\n    // Any gradient less than 1 needs to be negative\n    if (gradient < 0)\n    {\n        gradient *= -1;\n    }\n    // Calculate the ratio between x and y\n    float ratioXY = m_BulletSpeed / (1 + gradient);\n    // Set the \"speed\" horizontally and vertically\n    m_BulletDistanceY = ratioXY;\n    m_BulletDistanceX = ratioXY * gradient;\n\n    // Point the bullet in the right direction\n    if (targetX < startX)\n    {\n        m_BulletDistanceX *= -1;\n    }\n    if (targetY < startY)\n    {\n        m_BulletDistanceY *= -1;\n    }\n\n    // Set a max range of 1000 pixels\n    float range = 1000;\n    m_MinX = startX - range;\n    m_MaxX = startX + range;\n    m_MinY = startY - range;\n    m_MaxY = startY + range;\n\n    // Position the bullet ready to be drawn\n    m_BulletShape.setPosition(m_Position);\n}\n```\n\n为了揭开`shoot`函数的神秘面纱，我们将把它分解开来，并分块讨论刚刚添加的代码。\n\n首先，让我们提醒自己签名。 `shoot`函数接收子弹的起始位置和目标水平和垂直位置。 调用代码将基于玩家精灵的位置和十字准星的位置提供这些内容。 再来一遍:\n\n```cpp\nvoid Bullet::shoot(float startX, float startY,\n    float targetX, float targetY)\n```\n\n在`shoot`函数中，我们将`m_InFlight`设置为`true`，并使用`startX`和`startY`参数定位子弹。 下面是这段代码:\n\n```cpp\n// Keep track of the bullet\nm_InFlight = true;\nm_Position.x = startX;\nm_Position.y = startY;\n```\n\n现在，我们用一点三角函数来确定子弹的飞行梯度。 子弹在水平方向和垂直方向上的前进，必须根据子弹起始点和目标点之间的线的斜率而变化。 变化的速度不能相同，或者非常陡峭的镜头会在垂直位置之前到达水平位置，反之亦然。\n\n下面的代码根据直线方程推导出梯度。 然后，它检查梯度是否小于零，如果小于零，将其乘以-1。 这是因为传入的起始和目标坐标可以为正或负，而我们总是希望每一帧的进程量为正。 乘以-1 只是把负数变成正数，因为负数乘以负数得到正数。 实际的移动方向将在`update`函数中通过增加或减去我们在该函数中得到的正数来处理。\n\n接下来，我们通过将子弹的速度(`m_BulletSpeed`)除以 1，再加上坡度来计算水平距离与垂直距离的比值。 这将允许我们根据子弹所朝的目标，在每一帧中改变子弹的水平和垂直位置。\n\n最后，在这部分代码中，我们将值赋给`m_BulletDistanceY`和`m_BulletDistanceX`:\n\n```cpp\n// Calculate the gradient of the flight path\nfloat gradient = (startX - targetX) / (startY - targetY);\n// Any gradient less than zero needs to be negative\nif (gradient < 0)\n{\n    gradient *= -1;\n}\n// Calculate the ratio between x and y\nfloat ratioXY = m_BulletSpeed / (1 + gradient);\n// Set the \"speed\" horizontally and vertically\nm_BulletDistanceY = ratioXY;\nm_BulletDistanceX = ratioXY * gradient;\n```\n\n下面的代码简单得多。 我们只是简单地设置了子弹可以到达的最大水平和垂直位置。 我们不希望子弹永远持续下去。 在更新函数中，我们将看到子弹是否通过了它的最大或最小位置:\n\n```cpp\n// Set a max range of 1000 pixels in any direction\nfloat range = 1000;\nm_MinX = startX - range;\nm_MaxX = startX + range;\nm_MinY = startY - range;\nm_MaxY = startY + range;\n```\n\n下面的代码将代表子弹的精灵移动到它的起始位置。 我们使用`Sprite`的`setPosition`函数，就像我们以前经常做的那样:\n\n```cpp\n// Position the bullet ready to be drawn\nm_BulletShape.setPosition(m_Position);\n```\n\n接下来，我们有四个简单的函数。 添加`stop`、`isInFlight`、`getPosition`、`getShape`函数:\n\n```cpp\nvoid Bullet::stop()\n{\n    m_InFlight = false;\n}\nbool Bullet::isInFlight()\n{\n    return m_InFlight;\n}\nFloatRect Bullet::getPosition()\n{\n    return m_BulletShape.getGlobalBounds();\n}\nRectangleShape Bullet::getShape()\n{\n    return m_BulletShape;\n}\n```\n\n`stop`函数简单地将`m_InFlight`变量设置为`false`。 函数返回相同变量当前的值。 因此，我们可以看到，`shoot`设置子弹运行，`stop`设置它停止，而`isInFlight`告知我们当前的状态是什么。\n\n函数返回一个`FloatRect`。 我们将看到如何使用来自每个游戏对象的`FloatRect`来检测碰撞。\n\n最后，对于前面的代码，`getShape`返回一个`RectangleShape`，这样我们就可以在每一帧中绘制一次子弹。\n\n在开始使用`Bullet`对象之前，需要实现的最后一个函数是`update`。 添加以下代码，研究它，然后我们可以讨论它:\n\n```cpp\nvoid Bullet::update(float elapsedTime)\n{\n    // Update the bullet position variables\n    m_Position.x += m_BulletDistanceX * elapsedTime;\n    m_Position.y += m_BulletDistanceY * elapsedTime;\n    // Move the bullet\n    m_BulletShape.setPosition(m_Position);\n    // Has the bullet gone out of range?\n    if (m_Position.x < m_MinX || m_Position.x > m_MaxX ||\n        m_Position.y < m_MinY || m_Position.y > m_MaxY)\n    {\n        m_InFlight = false;\n    }\n}\n```\n\n在`update`函数中，我们使用`m_BulletDistanceX`和`m_BulletDistanceY`乘以自上一帧以来的时间来移动子弹。 记住，这两个变量的值是在`shoot`函数中计算出来的，它们代表了以正确的角度移动子弹所需的梯度(彼此之间的比率)。 然后，我们使用`setPosition`函数实际移动`RectangleShape`。\n\n我们在`update`中做的最后一件事是测试子弹是否已经超出了它的最大射程。 稍微有点复杂的`if`语句根据`shoot`函数中计算的最大值和最小值检查`m_Position.x`和`m_Position.y`。 这些最大值和最小值存储在`m_MinX`、`m_MaxX`、`m_MinY`和`m_MaxY`中。 如果测试为真，则将`m_InFlight`设置为`false`。\n\n`Bullet`课程结束。 现在，我们将看看如何在`main`函数中拍摄一些。\n\n# 让子弹飞\n\n我们将通过以下六个步骤使子弹可用:\n\n1.  为`Bullet`类添加必要的 include 指令。\n2.  添加一些控制变量和一个数组来保存一些`Bullet`实例。\n3.  让玩家按*R*重新加载。\n4.  手柄玩家按下鼠标左键发射子弹。\n5.  更新每一帧中所有正在飞行的子弹。\n6.  在每一帧中画出正在飞行的子弹。\n\n## 包括 Bullet 类\n\n添加 include 指令使 Bullet 类可用:\n\n```cpp\n#include <SFML/Graphics.hpp>\n#include \"ZombieArena.h\"\n#include \"Player.h\"\n#include \"TextureHolder.h\"\n#include \"Bullet.h\"\nusing namespace sf;\n```\n\n让我们进行下一步。\n\n## 控制变量和子弹数组\n\n这里有一些变量来记录弹夹大小、备用子弹、子弹、弹夹中剩余的子弹、当前的射击速度(开始时为每秒 1 颗)以及最后一颗子弹发射的时间。\n\n添加以下突出显示的代码。 然后，我们可以继续，并在接下来的部分中看到所有这些变量的作用:\n\n```cpp\n// Prepare for a horde of zombies\nint numZombies;\nint numZombiesAlive;\nZombie* zombies = NULL;\n// 100 bullets should do\nBullet bullets[100];\nint currentBullet = 0;\nint bulletsSpare = 24;\nint bulletsInClip = 6;\nint clipSize = 6;\nfloat fireRate = 1;\n// When was the fire button last pressed?\nTime lastPressed;\n// The main game loop\nwhile (window.isOpen())\n```\n\n接下来，让我们来处理当玩家按下*R*键盘键(用于重新加载剪辑)时会发生什么。\n\n## 重新装弹\n\n现在，我们将处理与射击子弹相关的玩家输入。 首先，我们将处理按下*R*键来装弹枪。 我们将使用一个 SFML 事件来做到这一点。\n\n添加以下突出显示的代码。 它通过大量的上下文来显示，以确保代码运行在正确的位置。 研究代码，然后我们可以讨论它:\n\n```cpp\n// Handle events\nEvent event;\nwhile (window.pollEvent(event))\n{\n    if (event.type == Event::KeyPressed)\n    {\n        // Pause a game while playing\n        if (event.key.code == Keyboard::Return &&\n            state == State::PLAYING)\n        {\n            state = State::PAUSED;\n        }\n        // Restart while paused\n        else if (event.key.code == Keyboard::Return &&\n            state == State::PAUSED)\n        {\n            state = State::PLAYING;\n            // Reset the clock so there isn't a frame jump\n            clock.restart();\n        }\n        // Start a new game while in GAME_OVER state\n        else if (event.key.code == Keyboard::Return &&\n            state == State::GAME_OVER)\n        {\n            state = State::LEVELING_UP;\n        }\n        if (state == State::PLAYING)\n        {\n // Reloading\n if (event.key.code == Keyboard::R)\n {\n if (bulletsSpare >= clipSize)\n {\n // Plenty of bullets. Reload.\n bulletsInClip = clipSize;\n bulletsSpare -= clipSize; \n }\n else if (bulletsSpare > 0)\n {\n // Only few bullets left\n bulletsInClip = bulletsSpare;\n bulletsSpare = 0; \n }\n else\n {\n // More here soon?!\n }\n }\n        }\n    }\n}// End event polling\n```\n\n前面的代码嵌套在游戏循环的事件处理部分(`while(window.pollEvent)`)中，并且嵌套在只在真正玩游戏时执行的块中(`if(state == State::Playing)`)。 很明显，我们不希望玩家在游戏结束或暂停时重新加载内容，然后包装我们所描述的新代码。\n\n在新代码本身中，我们要做的第一件事是测试用`if (event.key.code == Keyboard::R)`按下的*R*键。 一旦我们检测到*R*键被按下，剩下的代码将被执行。 以下是`if`、`else if`和`else`组块的结构:\n\n```cpp\nif(bulletsSpare >= clipSize)\n    ...\nelse if(bulletsSpare > 0)\n    ...\nelse\n    ...\n```\n\n前面的结构允许我们处理三种可能的场景，如下所示:\n\n*   玩家按下了`R`，他们的子弹比弹夹所能承受的多。 在这种情况下，弹夹被重新填充，备用子弹的数量减少。\n*   玩家有一些备用子弹，但不足以完全填满弹夹。 在这个场景中，玩家可以用尽可能多的备用子弹填充剪辑，而备用子弹的数量被设置为零。\n*   玩家已经按了*R*，但是他们没有多余的子弹。 对于这个场景，我们实际上不需要更改变量。 然而,我们将在这里扮演一个音效当我们实现声音[*第十三章*](13.html#_idTextAnchor279),*声音效果,文件 I / O,和完成游戏【显示】,所以我们将把空`else`块准备好了。*\n\n现在，让我们发射一颗子弹。\n\n## 射击子弹\n\n在这里，我们将处理点击鼠标左键发射子弹。 添加以下突出显示的代码，并仔细研究它:\n\n```cpp\n    if (Keyboard::isKeyPressed(Keyboard::D))\n    {\n        player.moveRight();\n    }\n    else\n    {\n        player.stopRight();\n    }\n // Fire a bullet\n if (Mouse::isButtonPressed(sf::Mouse::Left))\n {\n if (gameTimeTotal.asMilliseconds()\n - lastPressed.asMilliseconds()\n > 1000 / fireRate && bulletsInClip > 0)\n {\n // Pass the centre of the player \n // and the centre of the cross-hair\n // to the shoot function\n bullets[currentBullet].shoot(\n player.getCenter().x, player.getCenter().y,\n mouseWorldPosition.x, mouseWorldPosition.y);\n currentBullet++ ;\n if (currentBullet > 99)\n {\n currentBullet = 0;\n }\n lastPressed = gameTimeTotal;\n bulletsInClip--;\n }\n }// End fire a bullet\n}// End WASD while playing\n```\n\n所有前面的代码都包装在一个`if` 语句中，该语句在按下鼠标左键(即`if (Mouse::isButtonPressed(sf::Mouse::Left))`)时执行。 注意，代码将重复执行，即使玩家只是按住按钮。 我们现在要通过的代码控制着开火的速度。\n\n在前面的代码中,然后检查是否在游戏中运行的总时间(`gameTimeTotal`)减去时间玩家最后一球一颗子弹(`lastPressed`)大于 1000,除以当前火灾和球员至少有一颗子弹夹。 我们用 1000 是因为这是一秒的毫秒数。\n\n如果测试成功，将执行实际触发子弹的代码。 射击子弹很容易，因为我们在`Bullet`课上做了所有艰苦的工作。 我们只需从`bullets`数组中调用`shoot` 来处理当前子弹。 我们传递玩家和十字线当前的水平和垂直位置。 子弹将在飞行中通过`Bullet`类的`shoot`函数中的代码进行配置和设置。\n\n我们要做的就是跟踪子弹的排列。 我们增加了变量`currentBullet`。 然后，我们需要检查是否使用`if (currentBullet > 99)`语句发射了最后一颗子弹(99)。 如果这是最后一颗子弹，我们将`currentBullet`设为零。 如果这不是最后一颗子弹，那么只要射击速度允许，玩家按下鼠标左键，下一颗子弹就准备好了。\n\n最后，在前面的代码中，我们存储子弹射入`lastPressed`的时间并减少`bulletsInClip`。\n\n现在，我们可以更新每一颗子弹，每一帧。\n\n## 每帧更新子弹\n\n添加以下高亮显示的代码来循环子弹数组，检查子弹是否在飞行，如果是，调用它的 update 函数:\n\n```cpp\n    // Loop through each Zombie and update them\n    for (int i = 0; i < numZombies; i++)\n    {\n        if (zombies[i].isAlive())\n        {\n            zombies[i].update(dt.asSeconds(), playerPosition);\n        }\n    }\n // Update any bullets that are in-flight\n for (int i = 0; i < 100; i++)\n {\n if (bullets[i].isInFlight())\n {\n bullets[i].update(dtAsSeconds);\n }\n }\n}// End updating the scene\n```\n\n最后，我们将画出所有的子弹。\n\n## 每帧绘制子弹\n\n添加以下高亮显示的代码来循环遍历`bullets`数组，检查子弹是否在飞行，如果是，绘制它:\n\n```cpp\n/*\n **************\n Draw the scene\n **************\n */\nif (state == State::PLAYING)\n{\n    window.clear();\n    // set the mainView to be displayed in the window\n    // And draw everything related to it\n    window.setView(mainView);\n    // Draw the background\n    window.draw(background, &textureBackground);\n    // Draw the zombies\n    for (int i = 0; i < numZombies; i++)\n    {\n        window.draw(zombies[i].getSprite());\n    }\n for (int i = 0; i < 100; i++)\n {\n if (bullets[i].isInFlight())\n {\n window.draw(bullets[i].getShape());\n }\n }\n    // Draw the player\n    window.draw(player.getSprite());\n}\n```\n\n运行游戏来尝试子弹。 请注意，在按*R*重新装填之前，您可以发射六发子弹。 很明显，缺少的是弹夹中子弹数量和备用子弹数量的可视指示器。 另一个问题是，玩家可能会很快用光子弹，特别是因为子弹没有任何阻止力。 它们直接飞过僵尸。 此外，我们希望玩家瞄准的是鼠标指针而不是精确的十字准星，显然我们还有很多工作要做。\n\n在下一章中，我们将通过 HUD 提供视觉反馈。 接下来我们将用十字准星替换鼠标光标，然后生成一些拾音器来补充子弹和生命值。 最后，在本章中，我们将处理碰撞检测，使子弹和僵尸造成损害，并使玩家能够真正得到拾取。\n\n# 瞄准目标\n\n添加一个十字准星很简单，只需要一个新概念。 添加以下高亮显示的代码，然后我们可以运行它:\n\n```cpp\n// 100 bullets should do\nBullet bullets[100];\nint currentBullet = 0;\nint bulletsSpare = 24;\nint bulletsInClip = 6;\nint clipSize = 6;\nfloat fireRate = 1;\n// When was the fire button last pressed?\nTime lastPressed;\n// Hide the mouse pointer and replace it with crosshair\nwindow.setMouseCursorVisible(true);\nSprite spriteCrosshair;\nTexture textureCrosshair = TextureHolder::GetTexture(\"graphics/crosshair.png\");\nspriteCrosshair.setTexture(textureCrosshair);\nspriteCrosshair.setOrigin(25, 25);\n// The main game loop\nwhile (window.isOpen())\n```\n\n首先，我们在`window`对象上调用`setMouseCursorVisible`函数。 然后，我们加载一个`Texture`，声明一个`Sprite`实例，并按照通常的方式初始化它。 此外，我们将精灵的原点设置在它的中心，以便更方便和更简单地让子弹飞向中间，就像你所期望的那样。\n\n现在，我们需要用鼠标的世界坐标更新每一帧的十字准星。 添加以下高亮显示的代码行，它使用`mouseWorldPosition`向量来设置每一帧的准星位置:\n\n```cpp\n/*\n ****************\n UPDATE THE FRAME\n ****************\n */\nif (state == State::PLAYING)\n{\n    // Update the delta time\n    Time dt = clock.restart();\n    // Update the total game time\n    gameTimeTotal += dt;\n    // Make a decimal fraction of 1 from the delta time\n    float dtAsSeconds = dt.asSeconds();\n    // Where is the mouse pointer\n    mouseScreenPosition = Mouse::getPosition();\n    // Convert mouse position to world coordinates of mainView\n    mouseWorldPosition = window.mapPixelToCoords(\n        Mouse::getPosition(), mainView);\n // Set the crosshair to the mouse world location\n spriteCrosshair.setPosition(mouseWorldPosition);\n    // Update the player\n    player.update(dtAsSeconds, Mouse::getPosition());\n```\n\n接下来，正如你可能已经预料到的，我们可以在每一帧中绘制十字准星。 在显示的位置中添加以下高亮显示的代码行。 这一行代码无需解释，但它在所有其他游戏对象之后的位置很重要，所以它被绘制在最上面:\n\n```cpp\n/*\n **************\n Draw the scene\n **************\n */\nif (state == State::PLAYING)\n{\n    window.clear();\n    // set the mainView to be displayed in the window\n    // And draw everything related to it\n    window.setView(mainView);\n    // Draw the background\n    window.draw(background, &textureBackground);\n    // Draw the zombies\n    for (int i = 0; i < numZombies; i++)\n    {\n        window.draw(zombies[i].getSprite());\n    }\n    for (int i = 0; i < 100; i++)\n    {\n        if (bullets[i].isInFlight())\n        {\n            window.draw(bullets[i].getShape());\n        }\n    }\n    // Draw the player\n    window.draw(player.getSprite());\n //Draw the crosshair\n window.draw(spriteCrosshair);\n}\n```\n\n现在，你可以运行游戏，并将看到一个很酷的十字线，而不是鼠标光标:\n\n![](img/B14278_11_01.jpg)\n\n注意子弹是如何穿过十字准星中心的。 射击机制的运作方式类似于允许玩家选择从臀部射击或向下瞄准。 如果玩家将十字准星保持在中心附近，他们便可以快速开火并转向，但同时也必须小心地判断远处僵尸的位置。\n\n或者，玩家可以将十字准星直接悬停在远处的僵尸头上，并获得准确的命中; 然而，如果僵尸从另一个方向发动攻击，他们就有更多的时间将十字准星向后移动。\n\n游戏的一个有趣改进便是在每次射击中添加少量的随机误差。 这种不准确性也许可以通过两次浪潮之间的升级来缓解。\n\n# 为拾取程序编写类\n\n在本节中，我们将编码一个具有`Sprite`成员的`Pickup`类，以及其他成员数据和函数。 我们将在我们的游戏中添加拾取只需要几个步骤:\n\n1.  首先，我们将对`Pickup.h`文件进行编码。 这将揭示成员数据的所有细节和函数的原型。\n2.  然后，我们将编写`Pickup.cpp`文件，当然，该文件将包含`Pickup`类的所有函数的定义。 当我们逐步进行时，我将准确地解释`Pickup`类型的对象如何工作和被控制。\n3.  最后，我们将在`main`函数中使用`Pickup`类来生成、更新和绘制它们。\n\n让我们从第一步开始。\n\n## 编码皮卡头文件\n\n要创建新的头文件，右键单击**Solution Explorer**中的**header Files**，选择**Add | new Item…** 。 在**Add New Item**窗口中，高亮(通过左键点击)**Header File (.h)**，然后在**Name**字段中，键入`Pickup.h`。\n\n在`Pickup.h`文件中添加并研究以下代码，然后我们可以遍历它:\n\n```cpp\n#pragma once\n#include <SFML/Graphics.hpp>\nusing namespace sf;\nclass Pickup\n{\nprivate:\n    //Start value for health pickups\n    const int HEALTH_START_VALUE = 50;\n    const int AMMO_START_VALUE = 12;\n    const int START_WAIT_TIME = 10;\n    const int START_SECONDS_TO_LIVE = 5;\n\n    // The sprite that represents this pickup\n    Sprite m_Sprite;\n    // The arena it exists in\n    IntRect m_Arena;\n    // How much is this pickup worth?\n    int m_Value;\n\n    // What type of pickup is this? \n    // 1 = health, 2 = ammo\n    int m_Type;\n    // Handle spawning and disappearing\n    bool m_Spawned;\n    float m_SecondsSinceSpawn;\n    float m_SecondsSinceDeSpawn;\n    float m_SecondsToLive;\n    float m_SecondsToWait;    \n// Public prototypes go here\n};\n```\n\n前面的代码声明了`Pickup`类的所有私有变量。 虽然这些名称应该是非常直观的，但为什么需要这么多名称可能并不明显。 让我们从头到尾看一遍:\n\n*   `const int HEALTH_START_VALUE = 50`:此常量变量用于设置所有生命值拾取器的起始值。 该值将用于初始化变量`m_Value` ，该变量将在整个游戏过程中进行操作。\n*   `const int AMMO_START_VALUE = 12`:这个常量变量用来设置所有拾取弹药的起始值。 该值将用于初始化变量`m_Value`，该变量将在整个游戏过程中进行操作。\n*   `const int START_WAIT_TIME = 10`:这个变量决定拾音器在消失后等待多长时间才能重生。 它将用于初始化变量`m_SecondsToWait`，该变量可以在整个游戏中进行操作。\n*   `const int START_SECONDS_TO_LIVE = 5`:这个变量决定了拾音器在产卵和被取消产卵之间的持续时间。 与前三个常量一样，它也有一个可以在整个游戏过程中被操纵的非常量。 它用来初始化的非常量是`m_SecondsToLive`。\n*   `Sprite m_Sprite`:这是视觉上代表物体的精灵。\n*   `IntRect m_Arena`:这将保持当前竞技场的大小，以帮助拾音器在一个合理的位置产卵。\n*   这辆皮卡的生命值和弹药值多少? 当玩家升级生命值或拾取弹药值时，就会使用这个值。\n*   `int m_Type`:生命值和弹药值分别是 1 或 2。 我们本可以使用枚举类，但对于只有两个选项来说，这似乎有点多余。\n*   `bool m_Spawned`:皮卡现在已经生成了吗?\n*   `float m_SecondsSinceSpawn`:从皮卡产生到现在有多久了?\n*   `float m_SecondsSinceDeSpawn`:皮卡消失多久了?\n*   `float m_SecondsToLive`:这个拾音器应该停留多长时间产卵之前反产卵?\n*   `float m_SecondsToWait`: How long should this pickup stay de-spawned before respawning?\n\n    提示\n\n    请注意，这个类的大部分复杂性是由于变量生成时间和它的可升级性质。 如果拾音器只是在收集时重新生成并具有固定值，这将是一个非常简单的类。 我们需要让道具能够升级，这样玩家就会被迫制定策略，在波涛中前进。\n\n接下来，将以下公共函数原型添加到`Pickup.h`文件中。 请务必熟悉新代码，以便我们能够浏览它:\n\n```cpp\n// Public prototypes go here\npublic:\n Pickup::Pickup(int type);\n // Prepare a new pickup\n void setArena(IntRect arena);\n void spawn();\n // Check the position of a pickup\n FloatRect getPosition();\n // Get the sprite for drawing\n Sprite getSprite();\n // Let the pickup update itself each frame\n void update(float elapsedTime);\n // Is this pickup currently spawned?\n bool isSpawned();\n // Get the goodness from the pickup\n int gotIt();\n // Upgrade the value of each pickup\n void upgrade();\n};\n```\n\n让我们简要地讨论一下每个函数的定义。\n\n*   第一个函数是构造函数，以类命名。 注意，它只接受一个`int`参数。 这将用于初始化拾取的类型(生命值或弹药)。\n*   `setArena`函数接收到`IntRect`。 这个函数将在每个 wave 开始时对每个`Pickup`实例调用。 然后，`Pickup`对象将“知道”它们可以产卵的区域。\n*   当然，`spawn`函数将处理生成拾取。\n*   与`Player`、`Zombie`和`Bullet`类一样，`getPosition`函数将返回一个`FloatRect`实例，该实例表示对象在游戏世界中的当前位置。\n*   `getSprite`函数返回一个`Sprite`对象，该对象允许拾取每帧绘制一次。\n*   函数接收上一帧所花费的时间。 它使用这个值来更新它的私有变量，并决定何时生成和反生成。\n*   函数返回一个布尔值，让调用代码知道当前拾取是否已生成。\n*   当玩家检测到冲突时，将调用`gotIt`函数。 然后，`Pickup`类的代码可以为在适当的时间重生做好准备。 注意，它返回一个`int`值，以便调用代码知道拾取物在生命值或弹药上的“价值”。\n*   当玩家在游戏升级阶段选择升级拾音器的属性时，`upgrade`函数将被调用。\n\n现在我们已经了解了成员变量和函数原型，在编写函数定义时应该很容易理解。\n\n## 编写 Pickup 类函数定义\n\n现在，我们可以创建一个包含函数定义的新`.cpp`文件。 右键单击**Solution Explorer**中的**Source Files**，选择**Add | New Item…** 。 在**Add New Item**窗口中，高亮(通过左键点击)**c++ File (.cpp)**，然后在**Name**字段中，键入`Pickup.cpp`。 最后，点击**添加**按钮。 现在我们已经准备好编写类了。\n\n将以下代码添加到`Pickup.cpp`文件中。 一定要检查代码，以便我们可以讨论它:\n\n```cpp\n#include \"Pickup.h\"\n#include \"TextureHolder.h\"\nPickup::Pickup(int type)\n{\n    // Store the type of this pickup\n    m_Type = type;\n    // Associate the texture with the sprite\n    if (m_Type == 1)\n    {\n        m_Sprite = Sprite(TextureHolder::GetTexture(\n            \"graphics/health_pickup.png\"));\n        // How much is pickup worth\n        m_Value = HEALTH_START_VALUE;\n    }\n    else\n    {\n        m_Sprite = Sprite(TextureHolder::GetTexture(\n            \"graphics/ammo_pickup.png\"));\n        // How much is pickup worth\n        m_Value = AMMO_START_VALUE;\n    }\n    m_Sprite.setOrigin(25, 25);\n    m_SecondsToLive = START_SECONDS_TO_LIVE;\n    m_SecondsToWait = START_WAIT_TIME;\n}\n```\n\n在前面的代码中，我们添加了熟悉的 include 指令。 然后，我们添加了`Pickup`构造函数。 我们知道它是构造函数，因为它与类具有相同的名称。\n\n构造函数接收名为`type`的`int`，代码所做的第一件事就是将从`type`接收到的值赋给`m_Type`。 在此之后，有一个`if else`块检查`m_Type`是否等于 1。 如果是，`m_Sprite`与健康拾取纹理关联，`m_Value`设置为`HEALTH_START_VALUE`。\n\n如果`m_Type`不等于 1，`else`块将拾弹纹理与`m_Sprite`关联，并将`AMMO_START_VALUE`值赋给`m_Value`。\n\n在`if``else`块之后，代码使用`setOrigin`函数将`m_Sprite`的原点设置到中心，并分别将`START_SECONDS_TO_LIVE`和`START_WAIT_TIME`赋值给`m_SecondsToLive`和`m_SecondsToWait`。\n\n构造函数已经成功地准备了一个可以使用的`Pickup`对象。\n\n现在，我们将添加`setArena`函数。 在添加代码时检查代码:\n\n```cpp\nvoid Pickup::setArena(IntRect arena)\n{\n    // Copy the details of the arena to the pickup's m_Arena\n    m_Arena.left = arena.left + 50;\n    m_Arena.width = arena.width - 50;\n    m_Arena.top = arena.top + 50;\n    m_Arena.height = arena.height - 50;\n    spawn();\n}\n```\n\n我们刚刚编码的`setArena`函数只是简单地从传入的`arena`对象中复制值，但在左侧和顶部改变`+ 50`值，在右侧和底部改变`- 50`值。 对象现在知道它可以生成的区域。 然后，`setArena`函数调用自己的`spawn`函数为绘制和更新每一帧做最后的准备。\n\n接下来是`spawn`函数。 在`setArena`函数后添加以下代码:\n\n```cpp\nvoid Pickup::spawn()\n{\n    // Spawn at a random location\n    srand((int)time(0) / m_Type);\n    int x = (rand() % m_Arena.width);\n    srand((int)time(0) * m_Type);\n    int y = (rand() % m_Arena.height);\n    m_SecondsSinceSpawn = 0;\n    m_Spawned = true;\n    m_Sprite.setPosition(x, y);\n}\n```\n\nT0 函数完成所有准备拾取所需的工作。 首先，它为随机数生成器播种，并获得对象的水平和垂直位置的随机数。 注意，它使用了`m_Arena.width`和`m_Arena.height`变量作为可能的水平和垂直位置的范围。\n\n变量`m_SecondsSinceSpawn`被设置为零，以便在取消衍生之前允许的时间长度被重置。 变量`m_Spawned`被设置为`true`，这样当我们从`main`调用`isSpawned`时，我们将得到一个正响应。 最后，`m_Sprite`用`setPosition`移动到相应位置，准备被绘制到屏幕上。\n\n在下面的代码块中，我们有三个简单的 getter 函数。 `getPosition`函数返回当前位置的`FloatRect``m_Sprite`、`getSprite`返回一个副本`m_Sprite`,`isSpawned`,返回`true`或`false`,根据对象是否正在产生。\n\n添加并检查我们刚才讨论的代码:\n\n```cpp\nFloatRect Pickup::getPosition()\n{\n    return m_Sprite.getGlobalBounds();\n}\nSprite Pickup::getSprite()\n{\n    return m_Sprite;\n}\nbool Pickup::isSpawned()\n{\n    return m_Spawned;\n}\n```\n\n接下来，我们将对`gotIt`函数进行编码。 当玩家接触/碰撞拾取物品时，这个函数将从`main`开始调用。 在`isSpawned`功能后增加`gotIt`功能:\n\n```cpp\nint Pickup::gotIt()\n{\n    m_Spawned = false;\n    m_SecondsSinceDeSpawn = 0;\n    return m_Value;\n}\n```\n\n`gotIt`函数将`m_Spawned`设为`false`，这样我们就知道不再绘制和检查碰撞。 `m_SecondsSinceDespawn`设置为零，以便重新开始产卵倒计时。 然后将`m_Value`返回到调用代码，以便调用代码可以处理添加额外的弹药或生命值的问题。\n\n接下来，我们需要编写`update`函数，它将前面提到的许多变量和函数联系在一起。 添加并熟悉`update`函数，然后我们可以讨论它:\n\n```cpp\nvoid Pickup::update(float elapsedTime)\n{\n    if (m_Spawned)\n    {\n        m_SecondsSinceSpawn += elapsedTime;\n    }\n    else\n    {\n        m_SecondsSinceDeSpawn += elapsedTime;\n    }\n    // Do we need to hide a pickup?\n    if (m_SecondsSinceSpawn > m_SecondsToLive && m_Spawned)\n    {\n        // Remove the pickup and put it somewhere else\n        m_Spawned = false;\n        m_SecondsSinceDeSpawn = 0;\n    }\n    // Do we need to spawn a pickup\n    if (m_SecondsSinceDeSpawn > m_SecondsToWait && !m_Spawned)\n    {\n        // spawn the pickup and reset the timer\n        spawn();\n    }\n}\n```\n\n`update`函数被划分为四个块，在每一帧中执行:\n\n1.  如果`m_Spawned`为 true，则执行`if`块:`if (m_Spawned)`。 此代码块将此帧的时间添加到`m_SecondsSinceSpawned`，以跟踪生成拾取的时间。\n2.  一个对应的`else`块，如果`m_Spawned`为 false 则执行该块。 此块将此帧所花费的时间添加到`m_SecondsSinceDeSpawn`，以跟踪自上次去衍生(隐藏)以来拾取程序已经等待了多长时间。\n3.  另一个`if`块，在生成拾取的时间超过其正常时间时执行:`if (m_SecondsSinceSpawn > m_SecondsToLive && m_Spawned)`。 该块将`m_Spawned`设置为`false`并将`m_SecondsSinceDeSpawn`重置为零。 现在，block 2 将执行，直到该再次生成它为止。\n4.  最后一个`if`块，当从反生成开始等待的时间超过了必要的等待时间，并且拾取当前没有生成时执行:`if (m_SecondsSinceDeSpawn > m_SecondsToWait && !m_Spawned)`。 当执行此块时，是时候再次生成拾取，然后调用`spawn`函数。\n\n这四个测试控制隐藏和显示拾取。\n\n最后，添加`upgrade`函数的定义:\n\n```cpp\nvoid Pickup::upgrade()\n{\n    if (m_Type == 1)\n    {\n        m_Value += (HEALTH_START_VALUE * .5);\n    }\n    else\n    {\n        m_Value += (AMMO_START_VALUE * .5);\n    }\n    // Make them more frequent and last longer\n    m_SecondsToLive += (START_SECONDS_TO_LIVE / 10);\n    m_SecondsToWait -= (START_WAIT_TIME / 10);\n}\n```\n\n`upgrade`测试拾音器的类型(生命值或弹药)，然后在`m_Value`上添加 50%(适当的)起始值。 `if``else`方块后的下两行将增加拾取器的生成时间，并减少玩家在两次刷出之间等待的时间。\n\n当玩家选择在`LEVELING_UP`状态升级拾音器时，此函数将被调用。\n\n我们的`Pickup`班可以使用了。\n\n# 使用 Pickup 类\n\n在完成了所有实现`Pickup`类的艰苦工作之后，我们现在可以继续在游戏引擎中编写代码，将一些拾取道具放入游戏中。\n\n我们要做的第一件事是在`ZombieArena.cpp`文件中添加一个 include 指令:\n\n```cpp\n#include <SFML/Graphics.hpp>\n#include \"ZombieArena.h\"\n#include \"Player.h\"\n#include \"TextureHolder.h\"\n#include \"Bullet.h\"\n#include \"Pickup.h\"\nusing namespace sf;\n```\n\n在下面的代码中，我们将添加两个`Pickup`实例:一个名为`healthPickup`，另一个名为`ammoPickup`。 我们将值 1 和 2 分别传递给构造函数，以便将它们初始化为正确的拾取类型。 添加以下高亮显示的代码，我们刚刚讨论过:\n\n```cpp\n// Hide the mouse pointer and replace it with crosshair\nwindow.setMouseCursorVisible(true);\nSprite spriteCrosshair;\nTexture textureCrosshair = TextureHolder::GetTexture(\n         \"graphics/crosshair.png\");\nspriteCrosshair.setTexture(textureCrosshair);\nspriteCrosshair.setOrigin(25, 25);\n// Create a couple of pickups\nPickup healthPickup(1);\nPickup ammoPickup(2);\n// The main game loop\nwhile (window.isOpen())\n```\n\n在键盘处理的`LEVELING_UP`状态下，在嵌套的`PLAYING`代码块中添加以下高亮显示的行:\n\n```cpp\nif (state == State::PLAYING)\n{\n    // Prepare the level\n    // We will modify the next two lines later\n    arena.width = 500;\n    arena.height = 500;\n    arena.left = 0;\n    arena.top = 0;\n    // Pass the vertex array by reference \n    // to the createBackground function\n    int tileSize = createBackground(background, arena);\n    // Spawn the player in the middle of the arena\n    player.spawn(arena, resolution, tileSize);\n // Configure the pick-ups\n healthPickup.setArena(arena);\n ammoPickup.setArena(arena);\n    // Create a horde of zombies\n    numZombies = 10;\n    // Delete the previously allocated memory (if it exists)\n    delete[] zombies;\n    zombies = createHorde(numZombies, arena);\n    numZombiesAlive = numZombies;\n    // Reset the clock so there isn't a frame jump\n    clock.restart();\n}\n```\n\n前面的代码只是将`arena`传递给每个拾取的`setArena`函数。 拾音器现在知道他们可以产卵的地方。 这段代码对每个新 wave 执行，因此，随着舞台的大小增加，`Pickup`对象将得到更新。\n\n下面的代码简单地为每一帧上的每个`Pickup`对象调用`update`函数:\n\n```cpp\n// Loop through each Zombie and update them\n    for (int i = 0; i < numZombies; i++)\n    {\n        if (zombies[i].isAlive())\n        {\n            zombies[i].update(dt.asSeconds(), playerPosition);\n        }\n    }\n    // Update any bullets that are in-flight\n    for (int i = 0; i < 100; i++)\n    {\n        if (bullets[i].isInFlight())\n        {\n            bullets[i].update(dtAsSeconds);\n        }\n    }\n // Update the pickups\n healthPickup.update(dtAsSeconds);\n ammoPickup.update(dtAsSeconds);\n}// End updating the scene\n```\n\n下面的代码在游戏循环的绘制部分检查拾取当前是否生成，如果是，绘制它。 让我们将它添加:\n\n```cpp\n    // Draw the player\n    window.draw(player.getSprite());\n // Draw the pick-ups, if currently spawned\n if (ammoPickup.isSpawned())\n {\n window.draw(ammoPickup.getSprite());\n }\n\n if (healthPickup.isSpawned())\n {\n window.draw(healthPickup.getSprite());\n }\n    //Draw the crosshair\n    window.draw(spriteCrosshair);\n}\n```\n\n现在，你可以运行游戏，并看到拾音器产卵和反产卵。 然而，你还不能把它们捡起来:\n\n![](img/B14278_11_02.jpg)\n\n现在我们在游戏中拥有了所有的物体，是时候让它们相互作用(碰撞)了。\n\n# 检测碰撞\n\n我们只需要知道游戏中的特定物体何时接触到其他物体。 然后，我们可以以适当的方式响应该事件。 在我们的类中，我们已经添加了在对象碰撞时将被调用的函数。 它们如下:\n\n*   `Player`类具有`hit`功能。 当僵尸与玩家发生碰撞时，我们就会调用它。\n*   `Zombie`类具有`hit`功能。 当子弹撞到僵尸的时候我们就叫它。\n*   `Pickup`类具有`gotIt`功能。 当玩家与拾音器发生碰撞时，我们便会调用它。\n\n如果有必要，回顾一下这些函数是如何工作的。 我们现在需要做的就是检测冲突并调用相应的函数。\n\n我们将使用**矩形交点**来检测碰撞。 这种类型的碰撞检测非常简单(特别是使用 SFML 时)。 我们将使用与《Pong》游戏相同的技术。 下图展示了矩形如何合理准确地代表僵尸和玩家:\n\n![](img/B14278_11_03.jpg)\n\n我们将在三段代码中处理这个问题，这三段代码将依次进行。 它们都将在游戏引擎的更新部分结束时消失。\n\n对于每一帧，我们需要知道以下三个问题的答案:\n\n1.  僵尸中枪了吗?\n2.  玩家是否被僵尸碰过?\n3.  玩家是否碰过拾音器?\n\n首先，让我们为`score`和`hiscore`添加更多变量。 当僵尸被杀死时，我们可以改变它们。 添加以下代码:\n\n```cpp\n// Create a couple of pickups\nPickup healthPickup(1);\nPickup ammoPickup(2);\n// About the game\nint score = 0;\nint hiScore = 0;\n// The main game loop\nwhile (window.isOpen())\n```\n\n现在，让我们从检测僵尸是否与子弹相撞开始。\n\n## 僵尸中枪了吗?\n\n下面的代码可能看起来很复杂，但是当我们逐步执行它时，我们会发现它并不是我们以前没有见过的。 在调用之后添加以下代码来更新每一帧的拾取。 然后，我们可以看一下:\n\n```cpp\n// Update the pickups\nhealthPickup.update(dtAsSeconds);\nammoPickup.update(dtAsSeconds);\n// Collision detection\n// Have any zombies been shot?\nfor (int i = 0; i < 100; i++)\n{\n for (int j = 0; j < numZombies; j++)\n {\n if (bullets[i].isInFlight() && \n zombies[j].isAlive())\n {\n if (bullets[i].getPosition().intersects\n (zombies[j].getPosition()))\n {\n // Stop the bullet\n bullets[i].stop();\n // Register the hit and see if it was a kill\n if (zombies[j].hit()) \n {\n // Not just a hit but a kill too\n score += 10;\n if (score >= hiScore)\n {\n hiScore = score;\n }\n numZombiesAlive--;\n // When all the zombies are dead (again)\n if (numZombiesAlive == 0) {\n state = State::LEVELING_UP;\n }\n } \n\n }\n }\n }\n}// End zombie being shot\n```\n\n在下一节中，我们将再次看到所有僵尸和子弹碰撞检测代码。 我们将一次做一点，以便我们可以讨论它。 首先，注意前面代码中嵌套的`for`循环的结构(去掉了一些代码)，如下所示:\n\n```cpp\n// Collision detection\n// Have any zombies been shot?\nfor (int i = 0; i < 100; i++)\n{\n    for (int j = 0; j < numZombies; j++)\n    {\n        ...\n        ...\n        ...\n    }\n}\n```\n\n代码循环遍历每个僵尸(0 到< T0)的每个子弹(0 到 99)。\n\n在嵌套的`for`循环中，我们执行以下操作。\n\n我们使用以下代码检查当前子弹是否在飞行，当前僵尸是否仍然活着:\n\n```cpp\nif (bullets[i].isInFlight() && zombies[j].isAlive())\n```\n\n假设僵尸是活着的，子弹在飞行，我们测试矩形交叉与以下代码:\n\n```cpp\nif (bullets[i].getPosition().intersects(zombies[j].getPosition()))\n```\n\n如果当前的子弹和僵尸相撞了，那么我们将采取一些步骤，如下所述。\n\n用下面的代码停止子弹:\n\n```cpp\n// Stop the bullet\nbullets[i].stop();\n```\n\n通过调用当前僵尸的`hit`函数注册一个命中。 请注意，`hit`函数返回一个布尔值，让调用代码知道僵尸是否已经死亡。 这在下面的代码行中显示:\n\n```cpp\n// Register the hit and see if it was a kill\nif (zombies[j].hit()) {\n```\n\n在这个`if`块中，它可以检测到僵尸是死的，并且不只是伤害我们，执行以下操作:\n\n*   在`score`上加 10。\n*   如果玩家取得的分数超过(击败)`score`，则更改`hiScore`。\n*   将`numZombiesAlive`减少 1。\n*   检查`(numZombiesAlive == 0)`是否所有僵尸都死了，如果是，将`state`改为`LEVELING_UP`。\n\n下面是我们刚刚讨论过的`if(zombies[j].hit())`内部的代码块:\n\n```cpp\n// Not just a hit but a kill too\nscore += 10;\nif (score >= hiScore)\n{\n    hiScore = score;\n}\nnumZombiesAlive--;\n// When all the zombies are dead (again)\nif (numZombiesAlive == 0) \n{\n    state = State::LEVELING_UP;\n}\n```\n\n僵尸和子弹都解决了。 你现在可以运行游戏并看到血。 当然，除非我们在下一章中执行 HUD，否则你不会看到分数。\n\n## 玩家是否被僵尸碰过?\n\n这段代码比僵尸和子弹碰撞检测代码短得多，也简单得多。 在我们之前写的代码之后添加以下高亮显示的代码:\n\n```cpp\n}// End zombie being shot\n// Have any zombies touched the player \nfor (int i = 0; i < numZombies; i++)\n{\n if (player.getPosition().intersects\n (zombies[i].getPosition()) && zombies[i].isAlive())\n {\n if (player.hit(gameTimeTotal))\n {\n // More here later\n }\n if (player.getHealth() <= 0)\n {\n state = State::GAME_OVER; \n }\n }\n}// End player touched\n```\n\n在这里，我们通过使用`for`循环来检测僵尸是否与玩家发生碰撞。 对于每个活着的僵尸，代码使用`intersects`函数来测试是否与玩家发生碰撞。 当发生碰撞时，我们叫`player.hit`。 然后，我们通过调用`player.getHealth`来检查玩家是否已经死亡。 如果玩家的命值等于或小于 0，则将`state`改为`GAME_OVER`。\n\n你可以运行游戏，碰撞将被检测到。 然而，因为还没有 HUD 或音效，所以还不清楚是否会发生这种情况。 此外，我们还需要在玩家死亡后重新设置游戏，并开始新游戏。 所以，虽然游戏运行了，但现在的结果并不特别令人满意。 我们将在接下来的两章中对此进行改进。\n\n## 玩家是否碰过拾音器?\n\n这里显示的是玩家与两个拾音器之间的碰撞检测代码。 在我们之前添加的代码之后添加以下高亮显示的代码:\n\n```cpp\n    }// End player touched\n // Has the player touched health pickup\n if (player.getPosition().intersects\n (healthPickup.getPosition()) && healthPickup.isSpawned())\n {\n player.increaseHealthLevel(healthPickup.gotIt());\n\n }\n // Has the player touched ammo pickup\n if (player.getPosition().intersects\n (ammoPickup.getPosition()) && ammoPickup.isSpawned())\n {\n bulletsSpare += ammoPickup.gotIt();\n\n }\n}// End updating the scene\n```\n\n前面的代码使用两个简单的`if` 语句来查看`healthPickup`或`ammoPickup`是否被玩家触及。\n\n如果已经收集了生命值，那么`player.increaseHealthLevel`函数将使用`healthPickup.gotIt`函数返回的值来增加玩家的生命值。\n\n如果拾取了弹药，那么`bulletsSpare`将增加`ammoPickup.gotIt`返回的值。\n\n重要提示\n\n你现在可以运行游戏，杀死僵尸，收集皮卡! 注意，当你的生命值等于 0 时，游戏将进入`GAME_OVER`状态并暂停。 要重新启动它，您需要按`Enter`，然后按 1 到 6 之间的数字。 当我们执行 HUD、主屏幕和升级屏幕时，这些步骤对玩家来说将是直观和直接的。 我们将在下一章中这样做。\n\n# 总结\n\n这是一个忙碌的章节，但我们取得了很多成就。 我们不仅通过两个新职业在游戏中添加了子弹和拾取物，而且我们还通过检测物体之间的碰撞来让所有物体进行互动。\n\n除了这些成就，我们还需要做更多工作来设置每款新游戏，并通过 HUD 向玩家提供反馈。 在下一章中，我们将构建 HUD。\n\n# 常见问题解答\n\n以下是你可能会想到的一些问题:\n\nQ:有没有更好的碰撞检测方法?\n\n)是的。 有很多方法来做碰撞检测，包括但不限于以下。\n\n*   你可以将对象划分成多个矩形，以更好地适应精灵的形状。 对于 c++ 来说，每一帧检查数千个矩形是完全可以管理的。 当您使用诸如邻居检查等技术来减少每帧必需的测试数量时，情况尤其如此。\n*   对于圆形物体，可以使用半径重叠方法。\n*   对于不规则多边形，可以使用传递数算法。\n\n如果你愿意，你可以看看以下链接来回顾所有这些技巧:\n\n*   邻居检查:[http://gamecodeschool.com/essentials/collision-detection-neighbor-checking/](http://gamecodeschool.com/essentials/collision-detection-neighbor-checking/)\n*   半径重叠法:[http://gamecodeschool.com/essentials/collision-detection-radius-overlap/](http://gamecodeschool.com/essentials/collision-detection-radius-overlap/)\n*   交叉数算法:[http://gamecodeschool.com/essentials/collision-detection-crossing-number/](http://gamecodeschool.com/essentials/collision-detection-crossing-number/)"
  },
  {
    "path": "docs/begin-cpp-game-prog/12.md",
    "content": "# 十二、视图分层与 HUD 实现\n\n在本章中，我们将看到 SFML 视图的真正价值。 我们将添加一个大的 SFML`Text`对象数组，并对它们进行操作，就像我们之前在 Timber 中做的那样!! 项目和 Pong 项目。 新的是，我们将使用第二个视图实例来绘制 HUD。 通过这种方式，HUD 将保持在主要游戏动作的上方，而不管背景、玩家、僵尸和其他游戏对象在做什么。\n\n以下是本章的内容:\n\n*   添加文本和背景到主/游戏结束屏幕\n*   在升级屏幕上添加文本\n*   创建第二个视图\n*   添加一个住房和城市发展部\n\n# 添加所有文本和 HUD 对象\n\n我们将在这一章中处理一些字符串。 我们这么做是为了在 HUD 和升级屏幕上添加必要的文本。\n\n添加额外的`include`指令在下面的代码中高亮显示，这样我们就可以创建一些`sstream`对象来实现这一点:\n\n```cpp\n#include <sstream>\n#include <SFML/Graphics.hpp>\n#include \"ZombieArena.h\"\n#include \"Player.h\"\n#include \"TextureHolder.h\"\n#include \"Bullet.h\"\n#include \"Pickup.h\"\nusing namespace sf;\n```\n\n接下来，添加这段相当长但很容易解释的代码。 为了帮助确定应该添加代码的位置，新代码会高亮显示，而现有代码不会:\n\n```cpp\nint score = 0;\nint hiScore = 0;\n// For the home/game over screen\nSprite spriteGameOver;\nTexture textureGameOver = TextureHolder::GetTexture(\"graphics/background.png\");\nspriteGameOver.setTexture(textureGameOver);\nspriteGameOver.setPosition(0, 0);\n// Create a view for the HUD\nView hudView(sf::FloatRect(0, 0, resolution.x, resolution.y));\n// Create a sprite for the ammo icon\nSprite spriteAmmoIcon;\nTexture textureAmmoIcon = TextureHolder::GetTexture(\n \"graphics/ammo_icon.png\");\nspriteAmmoIcon.setTexture(textureAmmoIcon);\nspriteAmmoIcon.setPosition(20, 980);\n// Load the font\nFont font;\nfont.loadFromFile(\"fonts/zombiecontrol.ttf\");\n// Paused\nText pausedText;\npausedText.setFont(font);\npausedText.setCharacterSize(155);\npausedText.setFillColor(Color::White);\npausedText.setPosition(400, 400);\npausedText.setString(\"Press Enter \\nto continue\");\n// Game Over\nText gameOverText;\ngameOverText.setFont(font);\ngameOverText.setCharacterSize(125);\ngameOverText.setFillColor(Color::White);\ngameOverText.setPosition(250, 850);\ngameOverText.setString(\"Press Enter to play\");\n// LEVELING up\nText levelUpText;\nlevelUpText.setFont(font);\nlevelUpText.setCharacterSize(80);\nlevelUpText.setFillColor(Color::White);\nlevelUpText.setPosition(150, 250);\nstd::stringstream levelUpStream;\nlevelUpStream <<\n \"1- Increased rate of fire\" <<\n \"\\n2- Increased clip size(next reload)\" <<\n \"\\n3- Increased max health\" <<\n \"\\n4- Increased run speed\" <<\n \"\\n5- More and better health pickups\" <<\n \"\\n6- More and better ammo pickups\";\nlevelUpText.setString(levelUpStream.str());\n// Ammo\nText ammoText;\nammoText.setFont(font);\nammoText.setCharacterSize(55);\nammoText.setFillColor(Color::White);\nammoText.setPosition(200, 980);\n// Score\nText scoreText;\nscoreText.setFont(font);\nscoreText.setCharacterSize(55);\nscoreText.setFillColor(Color::White);\nscoreText.setPosition(20, 0);\n// Hi Score\nText hiScoreText;\nhiScoreText.setFont(font);\nhiScoreText.setCharacterSize(55);\nhiScoreText.setFillColor(Color::White);\nhiScoreText.setPosition(1400, 0);\nstd::stringstream s;\ns << \"Hi Score:\" << hiScore;\nhiScoreText.setString(s.str());\n// Zombies remaining\nText zombiesRemainingText;\nzombiesRemainingText.setFont(font);\nzombiesRemainingText.setCharacterSize(55);\nzombiesRemainingText.setFillColor(Color::White);\nzombiesRemainingText.setPosition(1500, 980);\nzombiesRemainingText.setString(\"Zombies: 100\");\n// Wave number\nint wave = 0;\nText waveNumberText;\nwaveNumberText.setFont(font);\nwaveNumberText.setCharacterSize(55);\nwaveNumberText.setFillColor(Color::White);\nwaveNumberText.setPosition(1250, 980);\nwaveNumberText.setString(\"Wave: 0\");\n// Health bar\nRectangleShape healthBar;\nhealthBar.setFillColor(Color::Red);\nhealthBar.setPosition(450, 980);\n// The main game loop\nwhile (window.isOpen())\n```\n\n前面的代码非常简单，没有什么新内容。 它基本上创建了一大堆 SFML`Text`对象。 它分配它们的颜色和大小，然后使用我们之前见过的函数格式化它们的位置。\n\n需要注意的最重要的一点是，我们创建了另一个名为`hudView`的`View`对象，并将其初始化以适应屏幕的分辨率。\n\n正如我们所看到的，主要的`View`对象随着玩家滚动。 相反，我们将永远不会移动`hudView`。 这样做的结果是，如果我们在绘制 HUD 元素之前切换到这个视图，我们将创造出允许游戏世界在下方滚动而玩家的 HUD 保持静止的效果。\n\n提示\n\n打个比方，你可以想象在电视屏幕上放一块透明的塑料，上面写着一些字。 电视将像往常一样播放移动的图像，而塑料薄膜上的文字将保持不变，不管下面发生了什么。 在下一个项目中，当我们分割屏幕并分离游戏世界的移动视图时，我们将进一步发展这一概念。\n\n然而，需要注意的另一件事是，高分数并没有以任何有意义的方式设置。 我们将需要等到下一章，当我们调查文件 I/O，保存和检索高分。\n\n另一点值得注意的是，我们声明并初始化名为`healthBar`的`RectangleShape`，这将是玩家剩余生命值的可视化表示。 这将以几乎相同的方式工作的时间条在木材!! 项目，但它将代表生命值而不是时间。\n\n在前面的代码中，有一个名为`ammoIcon`的新的`Sprite`实例，它为我们将在其旁边绘制的子弹和剪辑统计信息提供上下文，该实例位于屏幕的左下角。\n\n虽然我们刚刚添加的大量代码并没有什么新的或技术上的东西，但请务必熟悉这些细节——尤其是变量名——以便更容易理解本章的其余部分。\n\n# 更新 HUD\n\n如您所料，我们将在代码的更新部分更新 HUD 变量。 然而，我们不会每一个帧都这样做。 原因在于，这是不必要的，它也会减慢我们的游戏循环。\n\n举个例子，假设玩家杀死僵尸并获得更多积分。 保存分数的`Text`对象是否在千分之一秒、百分之一秒或甚至十分之一秒内更新都无关紧要。 玩家看不出有什么区别。 这意味着我们不需要在每一帧为`Text`对象重新构建字符串。\n\n因此，我们可以计算何时更新 HUD 以及更新频率。 添加以下突出显示的变量:\n\n```cpp\n// Debug HUD\nText debugText;\ndebugText.setFont(font);\ndebugText.setCharacterSize(25);\ndebugText.setFillColor(Color::White);\ndebugText.setPosition(20, 220);\nstd::ostringstream ss;\n// When did we last update the HUD?\nint framesSinceLastHUDUpdate = 0;\n// How often (in frames) should we update the HUD\nint fpsMeasurementFrameInterval = 1000;\n// The main game loop\nwhile (window.isOpen())\n```\n\n在之前的代码中，我们有变量来跟踪自上次 HUD 更新以来的帧数，而间隔(以帧为单位)我们希望在 HUD 更新之间等待。\n\n现在，我们可以使用这些新变量并更新每一帧 HUD。 然而，我们不会看到所有 HUD 元素发生改变，除非我们在下一章中开始操作最后的变量，如`wave`。\n\n在游戏循环的更新部分添加以下高亮代码，如下所示:\n\n```cpp\n    // Has the player touched ammo pickup\n    if (player.getPosition().intersects\n        (ammoPickup.getPosition()) && ammoPickup.isSpawned())\n    {\n        bulletsSpare += ammoPickup.gotIt();\n\n    }\n // size up the health bar\n healthBar.setSize(Vector2f(player.getHealth() * 3, 50));\n // Increment the number of frames since the previous update\n framesSinceLastHUDUpdate++ ;\n // re-calculate every fpsMeasurementFrameInterval frames\n if (framesSinceLastHUDUpdate > fpsMeasurementFrameInterval)\n {\n // Update game HUD text\n std::stringstream ssAmmo;\n std::stringstream ssScore;\n std::stringstream ssHiScore;\n std::stringstream ssWave;\n std::stringstream ssZombiesAlive;\n // Update the ammo text\n ssAmmo << bulletsInClip << \"/\" << bulletsSpare;\n ammoText.setString(ssAmmo.str());\n // Update the score text\n ssScore << \"Score:\" << score;\n scoreText.setString(ssScore.str());\n // Update the high score text\n ssHiScore << \"Hi Score:\" << hiScore;\n hiScoreText.setString(ssHiScore.str());\n // Update the wave\n ssWave << \"Wave:\" << wave;\n waveNumberText.setString(ssWave.str());\n // Update the high score text\n ssZombiesAlive << \"Zombies:\" << numZombiesAlive;\n zombiesRemainingText.setString(ssZombiesAlive.str());\n framesSinceLastHUDUpdate = 0;\n }// End HUD update\n}// End updating the scene\n```\n\n在新代码中，我们更新了`healthBar`精灵的大小，然后增加了`framesSinceLastHUDUpdate`变量。\n\n接下来，我们启动一个`if`块，测试`framesSinceLastHUDUpdate`是否大于存储在`fpsMeasurementFrameInterval`中的首选间隔。\n\n在这个`if`块中是所有动作发生的地方。 首先，为需要设置为`Text`对象的每个字符串声明一个`stringstream`对象。\n\n然后，我们依次使用这些`stringstream`对象，并使用`setString`函数将结果设置为相应的`Text`对象。\n\n最后，在退出`if`块之前，将`framesSinceLastHUDUpdate`设置回零，以便重新开始计数。\n\n现在，当我们重新绘制场景时，新的值将出现在玩家的 HUD 中。\n\n# 绘制 HUD、主页和升级屏幕\n\n以下三个代码块中的所有代码都处于游戏循环的绘制阶段。 我们所需要做的就是在主游戏循环的绘制部分中，在适当的状态下绘制适当的`Text`对象。\n\n在`PLAYING`状态下，添加以下突出显示的代码:\n\n```cpp\n    //Draw the crosshair\n    window.draw(spriteCrosshair);\n // Switch to the HUD view\n window.setView(hudView);\n // Draw all the HUD elements\n window.draw(spriteAmmoIcon);\n window.draw(ammoText);\n window.draw(scoreText);\n window.draw(hiScoreText);\n window.draw(healthBar);\n window.draw(waveNumberText);\n window.draw(zombiesRemainingText);\n}\nif (state == State::LEVELING_UP)\n{\n}\n```\n\n在前面的代码块中需要注意的重要事情是，我们将视图切换到 HUD 视图。 这将导致所有内容都在我们赋予 HUD 元素的精确屏幕位置上绘制。 他们永远不会移动。\n\n在`LEVELING_UP`状态下，添加以下突出显示的代码:\n\n```cpp\nif (state == State::LEVELING_UP)\n{\n window.draw(spriteGameOver);\n window.draw(levelUpText);\n}\n```\n\n在`PAUSED`状态下，添加以下突出显示的代码:\n\n```cpp\nif (state == State::PAUSED)\n{\n window.draw(pausedText);\n}\n```\n\n在`GAME_OVER`状态下，添加以下突出显示的代码:\n\n```cpp\nif (state == State::GAME_OVER)\n{\n window.draw(spriteGameOver);\n window.draw(gameOverText);\n window.draw(scoreText);\n window.draw(hiScoreText);\n}\n```\n\n现在，我们可以运行游戏并在游戏过程中看到 HUD 更新:\n\n![](img/B14278_12_01.jpg)\n\n下面的截图显示了主页/游戏结束界面的最高分和分数:\n\n![](img/B14278_12_02.jpg)\n\n接下来，我们看到文本告诉玩家他们的升级选项是什么，尽管这些选项还没有任何作用:\n\n![](img/B14278_12_03.jpg)\n\n在这里，我们可以在暂停屏幕上看到一个有用的信息:\n\n![](img/B14278_12_04.jpg)\n\n提示\n\nSFML 视图比这个简单的 HUD 所能展示的更强大。 要深入了解 SFML`View`类的潜力以及它们使用起来有多容易，请查看在[https://www.sfml-dev.org/tutorials/2.5/graphics-view.php](https://www.sfml-dev.org/tutorials/2.5/graphics-view.php)上关于`View`的 SFML 网站教程。\n\n# 总结\n\n这是一个快速而简单的章节。 我们了解了如何使用`sstream`显示不同类型变量所持有的值，然后学习了如何使用第二个 SFML`View`对象在主要游戏动作的顶部绘制它们。\n\n现在我们几乎完成了《僵尸竞技场》。 本章的所有截图都显示了一个小舞台，不能充分利用整个显示器。\n\n在下一章中，也就是这个项目的最后一章，我们将进行一些收尾工作，如升级、音效和保存高分。 然后，该竞技场可以扩大到与显示器相同的大小，甚至更大。\n\n# 常见问题解答\n\n这里有一个你可能会想到的问题:\n\n问:我在哪里可以看到更多的`View`的力量?\n\nA)看看《Zombie Arena》游戏的增强版。 您可以使用光标键盘键旋转和缩放游戏。 警告! 旋转场景会使控制变得笨拙，但你可以看到一些可以用 View 类完成的事情:\n\n![](img/B14278_12_05.jpg)\n\n在主游戏循环的输入处理部分，只需几行代码就可以实现缩放和旋转功能。 您可以在下载包的`Zombie Arena Enhanced Version`文件夹中看到代码，或者从`Runnable Games/Zombie Arena`文件夹中运行增强版本。"
  },
  {
    "path": "docs/begin-cpp-game-prog/13.md",
    "content": "# 十三、音效，文件 I/O，完成游戏\n\n我们快到了。 这个简短的章节将演示如何使用 c++ 标准库轻松地操作存储在硬盘上的文件，我们还将添加声音效果。 当然，我们知道如何添加声音效果，但我们将讨论对`play`函数的调用在代码中的确切位置。 我们也将解决一些松散的结束，使游戏完成。\n\n在本章中，我们将涵盖以下主题:\n\n*   使用文件输入和文件输出保存和加载高分\n*   添加声音效果\n*   允许玩家升级\n*   创造多个永不结束的波浪\n\n# 保存并加载高分\n\n文件**i/o**或**输入/输出**是一个相当技术性的主题。 幸运的是，由于这是编程中常见的需求，所以有一个库可以为我们处理所有这些复杂性。 与连接 HUD 中的字符串一样，c++ 标准库通过`fstream`提供了必要的功能。\n\n首先，我们以包含`sstream`的方式包含`fstream`:\n\n```cpp\n#include <sstream>\n#include <fstream>\n#include <SFML/Graphics.hpp>\n#include \"ZombieArena.h\"\n#include \"Player.h\"\n#include \"TextureHolder.h\"\n#include \"Bullet.h\"\n#include \"Pickup.h\"\nusing namespace sf;\n```\n\n现在，在`ZombieArena`文件夹中添加一个名为`gamedata`的新文件夹。 接下来，右键单击该文件夹并创建一个名为`scores.txt`的新文件。 在这个文件中，我们将保存玩家的高分。 您可以轻松地打开该文件并添加一个分数。 如果是，请确保它是一个相当低的分数，以便我们可以很容易地测试是否超过该分数会导致添加新的分数。 一定要关闭文件，一旦你完成它，否则游戏将无法访问它。\n\n在下面的代码中，我们将创建一个名为`inputFile`的`ifstream`对象，并将刚才创建的文件夹和文件作为参数发送给它的构造函数。\n\n`if(inputFile.is_open())`检查文件是否存在并准备好进行读取。 然后将该文件的内容放入`hiScore`中并关闭该文件。 添加以下突出显示的代码:\n\n```cpp\n// Score\nText scoreText;\nscoreText.setFont(font);\nscoreText.setCharacterSize(55);\nscoreText.setColor(Color::White);\nscoreText.setPosition(20, 0);\n// Load the high score from a text file\nstd::ifstream inputFile(\"gamedata/scores.txt\");\nif (inputFile.is_open())\n{\n // >> Reads the data\n inputFile >> hiScore;\n inputFile.close();\n}\n// Hi Score\nText hiScoreText;\nhiScoreText.setFont(font);\nhiScoreText.setCharacterSize(55);\nhiScoreText.setColor(Color::White);\nhiScoreText.setPosition(1400, 0);\nstd::stringstream s;\ns << \"Hi Score:\" << hiScore;\nhiScoreText.setString(s.str());\n```\n\n现在，我们可以保存一个可能的新分数了。 在处理玩家健康值小于或等于 0 的块中，我们需要创建一个名为`outputFile`的`ofstream`对象，将`hiScore`的值写入文本文件，然后关闭该文件，如下所示:\n\n```cpp\n// Have any zombies touched the player            \nfor (int i = 0; i < numZombies; i++)\n{\n    if (player.getPosition().intersects\n        (zombies[i].getPosition()) && zombies[i].isAlive())\n    {\n        if (player.hit(gameTimeTotal))\n        {\n            // More here later\n        }\n        if (player.getHealth() <= 0)\n        {\n            state = State::GAME_OVER;\n std::ofstream outputFile(\"gamedata/scores.txt\");\n // << writes the data\n outputFile << hiScore;\n outputFile.close();\n\n        }\n    }\n}// End player touched\n```\n\n你可以玩游戏，你的高分将被保存。 退出游戏，并注意到如果你再次玩游戏，你的高分仍然存在。\n\n让我们制造一些噪音。\n\n# 准备音效\n\n在本节中，我们将创建所有的`SoundBuffer`和`Sound`对象，我们需要为游戏添加一系列的声音效果。\n\n首先添加所需的 SFML`#include`语句:\n\n```cpp\n#include <sstream>\n#include <fstream>\n#include <SFML/Graphics.hpp>\n#include <SFML/Audio.hpp>\n#include \"ZombieArena.h\"\n#include \"Player.h\"\n#include \"TextureHolder.h\"\n#include \"Bullet.h\"\n#include \"Pickup.h\"\n```\n\n现在,继续加入七`SoundBuffer`和`Sound`对象加载和准备七个声音文件,我们准备在[*第八章*【5】*,SFML 观点——僵尸射击游戏开始*:](08.html#_idTextAnchor183)\n\n```cpp\n// When did we last update the HUD?\nint framesSinceLastHUDUpdate = 0;\n// What time was the last update\nTime timeSinceLastUpdate;\n// How often (in frames) should we update the HUD\nint fpsMeasurementFrameInterval = 1000;\n// Prepare the hit sound\nSoundBuffer hitBuffer;\nhitBuffer.loadFromFile(\"sound/hit.wav\");\nSound hit;\nhit.setBuffer(hitBuffer);\n// Prepare the splat sound\nSoundBuffer splatBuffer;\nsplatBuffer.loadFromFile(\"sound/splat.wav\");\nSound splat;\nsplat.setBuffer(splatBuffer);\n// Prepare the shoot sound\nSoundBuffer shootBuffer;\nshootBuffer.loadFromFile(\"sound/shoot.wav\");\nSound shoot;\nshoot.setBuffer(shootBuffer);\n// Prepare the reload sound\nSoundBuffer reloadBuffer;\nreloadBuffer.loadFromFile(\"sound/reload.wav\");\nSound reload;\nreload.setBuffer(reloadBuffer);\n// Prepare the failed sound\nSoundBuffer reloadFailedBuffer;\nreloadFailedBuffer.loadFromFile(\"sound/reload_failed.wav\");\nSound reloadFailed;\nreloadFailed.setBuffer(reloadFailedBuffer);\n// Prepare the powerup sound\nSoundBuffer powerupBuffer;\npowerupBuffer.loadFromFile(\"sound/powerup.wav\");\nSound powerup;\npowerup.setBuffer(powerupBuffer);\n// Prepare the pickup sound\nSoundBuffer pickupBuffer;\npickupBuffer.loadFromFile(\"sound/pickup.wav\");\nSound pickup;\npickup.setBuffer(pickupBuffer);\n// The main game loop\nwhile (window.isOpen())\n```\n\n现在，七个音效已经准备好了。 我们只需要找出对`play`函数的每个调用在代码中的位置。\n\n# 升级\n\n下面我们将添加的代码允许玩家在两次波之间升级。 正因为我们已经做了很多工作，所以这是很容易实现的。\n\n添加以下高亮代码到我们处理玩家输入的`LEVELING_UP`状态:\n\n```cpp\n// Handle the LEVELING up state\nif (state == State::LEVELING_UP)\n{\n    // Handle the player LEVELING up\n    if (event.key.code == Keyboard::Num1)\n    {\n // Increase fire rate\n fireRate++ ;\n        state = State::PLAYING;\n    }\n    if (event.key.code == Keyboard::Num2)\n    {\n // Increase clip size\n clipSize += clipSize;\n        state = State::PLAYING;\n    }\n    if (event.key.code == Keyboard::Num3)\n    {\n // Increase health\n player.upgradeHealth();\n        state = State::PLAYING;\n    }\n    if (event.key.code == Keyboard::Num4)\n    {\n // Increase speed\n player.upgradeSpeed();\n        state = State::PLAYING;\n    }\n    if (event.key.code == Keyboard::Num5)\n    {\n // Upgrade pickup\n healthPickup.upgrade();\n        state = State::PLAYING;\n    }\n    if (event.key.code == Keyboard::Num6)\n    {\n // Upgrade pickup\n ammoPickup.upgrade();\n        state = State::PLAYING;\n    }\n    if (state == State::PLAYING)\n    {\n```\n\n玩家现在可以在每次清除一波僵尸时升级。 然而，我们还不能增加僵尸的数量或关卡的大小。\n\n在`LEVELING_UP`状态的下一部分中，就在我们刚刚添加的代码之后，修改当状态从`LEVELING_UP`更改为`PLAYING`时运行的代码。\n\n下面是完整的代码。 我突出显示了新的或稍微修改过的行。\n\n添加或修改下列突出显示的代码:\n\n```cpp\n    if (event.key.code == Keyboard::Num6)\n    {\n        ammoPickup.upgrade();\n        state = State::PLAYING;\n    }\n    if (state == State::PLAYING)\n    {\n // Increase the wave number\n wave++ ;\n        // Prepare the level\n        // We will modify the next two lines later\n arena.width = 500 * wave;\n arena.height = 500 * wave;\n        arena.left = 0;\n        arena.top = 0;\n        // Pass the vertex array by reference \n        // to the createBackground function\n        int tileSize = createBackground(background, arena);\n        // Spawn the player in the middle of the arena\n        player.spawn(arena, resolution, tileSize);\n        // Configure the pick-ups\n        healthPickup.setArena(arena);\n        ammoPickup.setArena(arena);\n        // Create a horde of zombies\n numZombies = 5 * wave;\n        // Delete the previously allocated memory (if it exists)\n        delete[] zombies;\n        zombies = createHorde(numZombies, arena);\n        numZombiesAlive = numZombies;\n // Play the powerup sound\n powerup.play();\n        // Reset the clock so there isn't a frame jump\n        clock.restart();\n    }\n}// End LEVELING up\n```\n\n前面的代码从增加`wave`变量开始。 然后，修改代码，使僵尸的数量和竞技场的大小相对于新值`wave`。 最后，我们添加了`powerup.play()`的调用来播放升级音效。\n\n# 重启游戏\n\n我们已经通过变量`wave`的值确定了竞技场的大小和僵尸的数量。 我们还必须重置弹药和枪支相关变量，并在每款新游戏开始时将`wave`和`score`设为零。 在游戏循环的事件处理部分找到以下代码，并添加以下突出显示的代码:\n\n```cpp\n// Start a new game while in GAME_OVER state\nelse if (event.key.code == Keyboard::Return &&\n    state == State::GAME_OVER)\n{\n    state = State::LEVELING_UP;\n wave = 0;\n score = 0;\n // Prepare the gun and ammo for next game\n currentBullet = 0;\n bulletsSpare = 24;\n bulletsInClip = 6;\n clipSize = 6;\n fireRate = 1;\n // Reset the player's stats\n player.resetPlayerStats();\n}\n```\n\n现在，我们可以玩这个游戏了，玩家可以变得更加强大，而僵尸会在一个越来越大的竞技场中变得越来越多——直到他们死去。 然后，游戏又重新开始。\n\n# 播放其余的声音\n\n现在，我们将添加对`play`函数的其余调用。 我们将单独处理每一个球员，因为准确地定位他们的去向是在正确的时间和他们比赛的关键。\n\n## 在玩家重新加载时添加音效\n\n在三个地方添加以下高亮代码，以便当玩家按下*R*键试图重新装填枪支时，播放适当的`reload`或`reloadFailed`声音:\n\n```cpp\nif (state == State::PLAYING)\n{\n    // Reloading\n    if (event.key.code == Keyboard::R)\n    {\n        if (bulletsSpare >= clipSize)\n        {\n            // Plenty of bullets. Reload.\n            bulletsInClip = clipSize;\n            bulletsSpare -= clipSize;        \n reload.play();\n        }\n        else if (bulletsSpare > 0)\n        {\n            // Only few bullets left\n            bulletsInClip = bulletsSpare;\n            bulletsSpare = 0;                \n reload.play();\n        }\n        else\n        {\n            // More here soon?!\n reloadFailed.play();\n        }\n    }\n}\n```\n\n当玩家重新加载或尝试重新加载时，现在将得到一个声音响应。 让我们继续播放射击声音。\n\n## 发出射击的声音\n\n在处理玩家点击鼠标左键的代码末尾添加以下高亮显示的`shoot.play()`调用:\n\n```cpp\n// Fire a bullet\nif (sf::Mouse::isButtonPressed(sf::Mouse::Left))\n{\n    if (gameTimeTotal.asMilliseconds()\n        - lastPressed.asMilliseconds()\n        > 1000 / fireRate && bulletsInClip > 0)\n    {\n        // Pass the centre of the player and crosshair\n        // to the shoot function\n        bullets[currentBullet].shoot(\n            player.getCenter().x, player.getCenter().y,\n            mouseWorldPosition.x, mouseWorldPosition.y);\n        currentBullet++ ;\n        if (currentBullet > 99)\n        {\n            currentBullet = 0;\n        }\n        lastPressed = gameTimeTotal;\n shoot.play();\n        bulletsInClip--;\n    }\n}// End fire a bullet\n```\n\n游戏现在将播放一个令人满意的射击声音。 接下来，我们将播放玩家被僵尸击中时的声音。\n\n## 当玩家被击中时播放声音\n\n在下面的代码中，我们将对`hit.play`的调用封装在一个测试中，以查看`player.hit`函数是否返回 true。 记住，`player.hit` 函数测试是否在之前的 100 毫秒内记录了一次命中。 这将产生一个快速重复的重击声音的效果，但不会太快，以至于声音模糊成一个噪音。\n\n将调用添加到`hit.play`，如下代码中高亮显示:\n\n```cpp\n// Have any zombies touched the player            \nfor (int i = 0; i < numZombies; i++)\n{\n    if (player.getPosition().intersects\n        (zombies[i].getPosition()) && zombies[i].isAlive())\n    {\n        if (player.hit(gameTimeTotal))\n        {\n            // More here later\n hit.play();\n        }\n        if (player.getHealth() <= 0)\n        {\n            state = State::GAME_OVER;\n            std::ofstream OutputFile(\"gamedata/scores.txt\");\n            OutputFile << hiScore;\n            OutputFile.close();\n\n        }\n    }\n}// End player touched\n```\n\n当僵尸触碰他们时，玩家将听到一种不祥的砰砰声，如果僵尸继续触碰他们，这种声音将以每秒 5 次的速度重复出现。 其逻辑包含在`Player`类的`hit`函数中。\n\n## 拾取时播放声音\n\n当玩家拾取生命值时，我们会播放常规拾取的声音。 然而，当玩家拿到弹药时，我们会播放装弹音效。\n\n在适当的碰撞检测代码中添加两个调用来播放声音:\n\n```cpp\n// Has the player touched health pickup\nif (player.getPosition().intersects\n    (healthPickup.getPosition()) && healthPickup.isSpawned())\n{\n    player.increaseHealthLevel(healthPickup.gotIt());\n // Play a sound\n pickup.play();\n\n}\n// Has the player touched ammo pickup\nif (player.getPosition().intersects\n    (ammoPickup.getPosition()) && ammoPickup.isSpawned())\n{\n    bulletsSpare += ammoPickup.gotIt();\n // Play a sound\n reload.play();\n\n}\n```\n\n## 当僵尸被射杀时发出啪啪声\n\n在检测子弹与僵尸碰撞的代码部分末尾添加一个调用`splat.play`:\n\n```cpp\n// Have any zombies been shot?\nfor (int i = 0; i < 100; i++)\n{\n    for (int j = 0; j < numZombies; j++)\n    {\n        if (bullets[i].isInFlight() && \n            zombies[j].isAlive())\n        {\n            if (bullets[i].getPosition().intersects\n                (zombies[j].getPosition()))\n            {\n                // Stop the bullet\n                bullets[i].stop();\n                // Register the hit and see if it was a kill\n                if (zombies[j].hit()) {\n                    // Not just a hit but a kill too\n                    score += 10;\n                    if (score >= hiScore)\n                    {\n                        hiScore = score;\n                    }\n                    numZombiesAlive--;\n                    // When all the zombies are dead (again)\n                    if (numZombiesAlive == 0) {\n                        state = State::LEVELING_UP;\n                    }\n                }    \n // Make a splat sound\n splat.play();\n\n            }\n        }\n    }\n}// End zombie being shot\n```\n\n你现在可以玩完整的游戏，并看到僵尸的数量和竞技场增加每波。 仔细选择你的升级:\n\n![](img/B14278_13_01.jpg)\n\n恭喜你!\n\n# 总结\n\n我们已经完成了《Zombie Arena》游戏。 这是一段相当长的旅程。 我们已经学习了一大堆 c++ 基础知识，比如引用、指针、面向对象和类。 此外，我们还使用了 SFML 来管理摄像机(视图)、顶点数组和碰撞检测。 我们学习了如何使用精灵表来减少对`window.draw`的调用次数并提高帧率。 使用 c++ 指针、STL 和一点 OOP，我们构建了一个单例类来管理纹理。 在下一个项目中，我们将扩展这一理念并管理所有游戏资产。\n\n在本书的倒数第二个项目中，我们将发现粒子效果、定向声音和分屏合作游戏。 在 c++ 中，我们还会遇到继承、多态性和其他一些新概念。\n\n# 常见问题解答\n\n以下是你可能会想到的一些问题:\n\nQ)尽管使用类，我还是发现代码变得非常长，无法管理。\n\nA)最大的问题之一是我们代码的结构。 随着我们对 c++ 学习的深入，我们也将学习使代码更易于管理、通常更短的方法。 我们将在下一个项目和最后一个项目中这样做。 读完这本书，你就会知道一些管理代码的策略。\n\nQ:音效看起来有点平淡和不现实。 如何改进?\n\nA)有效改善玩家从声音中获得的感觉的一种方法是让声音具有方向性，并根据音源与玩家角色的距离改变音量。 在下一个项目中，我们将使用 SFML 的高级声音特性。"
  },
  {
    "path": "docs/begin-cpp-game-prog/14.md",
    "content": "# 十四、抽象和代码管理——更好地利用面向对象\n\n在这一章中，我们将首先看一下本书倒数第二个项目。我们将建立的项目将使用高级功能，如定向声音，其效果是相对于玩家的位置来看。它还将有分屏合作游戏。此外，本项目将引入**着色器**的概念，这是用另一种语言编写的直接在显卡上运行的程序。到第 18 章[](18.html#_idTextAnchor356)**粒子系统和着色器*结束时，你将拥有一个功能齐全的多人平台游戏，该游戏采用了热门经典游戏*托马斯独自一人*的风格。*\n\n *这一章的重点将是让项目开始，并探索如何构建代码，以更好地利用面向对象程序。以下是本章将涵盖的主题的详细信息:\n\n*   介绍最终项目，托马斯迟到了，包括游戏特性和项目资产\n*   与以前的项目相比，我们将如何改进代码结构的详细讨论\n*   托马斯迟到游戏引擎的编码\n*   实现分屏功能\n\n# 托马斯迟到的游戏\n\n小费\n\n在这一点上，如果你还没有，我建议你去看看托马斯在 http://store.steampowered.com/app/220780/的视频。\n\n请注意简单但美观的图形。视频还展示了多种玩法挑战，比如使用角色的不同属性(身高、跳跃、力量等)。为了保持我们的游戏简单而不失去挑战，我们将有比托马斯独自一人更少的拼图功能，但将有额外的挑战，创造两个玩家合作玩的需求。只是为了保证比赛不会太轻松，我们也会让球员们不得不抢着打卡，这也是为什么我们比赛的名字叫托马斯迟到了。\n\n## 托马斯迟到的特征\n\n我们的游戏将不会像我们试图模仿的杰作那样先进，但它将有许多令人兴奋的游戏功能，例如:\n\n*   从与关卡挑战相适应的时间开始倒计时的时钟。\n*   相对于玩家的位置发出吼声的火坑，如果他们掉进去，会在开始时让玩家重生。水坑有同样的效果，但没有定向音效。\n*   合作游戏。两位玩家都必须在规定的时间内让他们的角色到达目标。他们需要经常一起工作，这样个子较矮、跳得较低的鲍勃就需要站在他朋友(托马斯)的头上。\n*   玩家可以选择全屏和分屏，这样他们就可以自己控制两个角色。\n*   每个级别都将在文本文件中设计和加载。这将使设计多种多样的层次变得容易。\n\n看看下面这个带注释的游戏截图，看看一些正在运行的功能和组成游戏的组件/资产:\n\n![](img/B14278_14_01a.jpg)\n\n让我们看看这些特性中的每一个，并再描述几个:\n\n*   前面的截图显示了一个简单的平视显示器，详细说明了关卡编号和玩家失败并必须重启关卡之前剩余的秒数。\n*   您还可以清楚地看到分屏合作在运行。请记住，这是可选的。单个玩家可以全屏观看比赛，同时在托马斯和鲍勃之间切换镜头焦点。\n*   在前面的截图中不是很清楚(尤其是在印刷品中)，但是当一个角色死亡时，他们会爆发出类似星爆/烟花的粒子效果。\n*   水和火瓷砖可以有策略地放置，使关卡变得有趣，并迫使角色之间合作。更多这方面的内容将在 [*第 16 章*](16.html#_idTextAnchor327)*建筑可玩关卡和碰撞检测*中介绍。\n*   接下来，请注意托马斯和鲍勃。他们不仅身高不同，而且跳跃能力也大不相同。这意味着鲍勃依赖托马斯进行大跳跃，可以设计关卡迫使托马斯走特定路线，以避免他“敲打头部”。\n*   此外，火砖会发出咆哮的声音。这些将是相对于托马斯的位置。它们不仅是定向的，来自左或右扬声器，而且随着托马斯靠近或远离声源，它们也会变得越来越大声和安静。\n*   最后，在前面带注释的截图中，您可以看到背景。为什么不将它与`background.png`文件(本章下文显示)进行比较？你会看到它是完全不同的。我们将在 [*第 18 章*](18.html#_idTextAnchor356)*粒子系统和着色器*中使用 OpenGL 着色器效果，以实现背景中的移动、几乎冒泡的效果。\n\n所有这些特性都需要更多的截图，这样我们在编写 C++ 代码时就可以记住最终的产品。\n\n下面的截图显示了托马斯和鲍勃到达一个火坑，如果没有帮助，鲍勃没有机会跳过去:\n\n![](img/B14278_14_01b.jpg)\n\n下面的截图显示了鲍勃和托马斯合作清除不稳定的跳跃:\n\n![](img/B14278_14_01c.jpg)\n\n下面的截图展示了我们如何设计需要“信念飞跃”才能达到目标的谜题:\n\n![](img/B14278_14_01d.jpg)\n\n下面的截图展示了我们如何设计几乎任何尺寸的压迫性洞穴系统。我们还可以设计出鲍勃和托马斯被迫分开走不同路线的层次:\n\n![](img/B14278_14_01e.jpg)\n\n## 创建项目\n\n创建托马斯迟到项目将遵循我们在前面三个项目中使用的相同过程。由于创建项目是一个稍微复杂的过程，我将在这里再次详述所有步骤。有关更多细节和图像，请参考设置木材！！！ [*第一章*](01.html#_idTextAnchor017)*c++、SFML、Visual Studio、首发游戏*中的项目:\n\n1.  启动 Visual Studio，点击**新建项目**按钮。如果您打开了另一个项目，您可以选择**文件** | **新项目**。\n2.  在接下来显示的窗口中，选择**控制台应用**并点击**下一步**按钮。然后你会看到**配置你的新项目**窗口。\n3.  在**配置您的新项目**窗口中，在**项目** **名称**字段中键入`TWL`。\n4.  在**位置**字段中，浏览至`VS Projects`文件夹。\n5.  勾选**选项，将解决方案和项目放在同一个目录**中。\n6.  完成这些步骤后，点击**创建**。\n7.  我们现在将配置项目使用我们放在`SFML`文件夹中的 SFML 文件。从主菜单中，选择**项目** | **TWL 物业…** 。在此阶段，您应该打开 **TWL 物业页面**窗口。\n8.  在 **TWL 物业页面**窗口中，执行以下步骤。从**配置:**下拉菜单中选择**所有配置**。\n9.  现在，从左侧菜单中选择 **C/C++** ，然后选择**通用**。\n10.  现在，找到**附加包含目录**编辑框，键入您的 SFML 文件夹所在的驱动器号，然后是`\\SFML\\include`。如果你在硬盘上找到你的`SFML`文件夹，完整的打字路径是`D:\\SFML\\include`。如果您在不同的驱动器上安装了 SFML，请改变您的路径。\n11.  点击**应用**保存到目前为止的配置。\n12.  现在，仍然在同一个窗口中，执行以下步骤。从左侧菜单中，选择**链接器**，然后选择**通用**。\n13.  现在，找到**附加库目录**编辑框，键入您的`SFML`文件夹所在的驱动器号，然后是`\\SFML\\lib`。所以，如果你在硬盘上找到你的`SFML`文件夹，完整的输入路径是`D:\\SFML\\lib`。如果您将 SFML 安装到不同的驱动器，请改变您的路径。\n14.  点击**应用**保存到目前为止的配置。\n15.  接下来，仍然在同一个窗口中，执行以下步骤。将**配置:**下拉菜单切换到**调试**，因为我们将在调试模式下运行和测试 Pong。\n16.  选择**链接器**，然后**输入**。\n17.  找到**附加依赖**编辑框，点击最左侧。现在，复制并粘贴/键入以下内容:`sfml-graphics-d.lib;sfml-window-d.lib;sfml-system-d.lib;sfml-network-d.lib;sfml-audio-d.lib;`。格外小心地将光标放在编辑框当前内容的开头，这样就不会覆盖已经存在的任何文本。\n18.  点击**确定**。\n19.  点击**应用**，然后**确定**。\n\n这是已配置并准备好的项目属性。现在，我们需要按照以下步骤将 SFML `.dll`文件复制到主项目目录中:\n\n1.  我的主要项目目录是`D:\\VS Projects\\TWL`。此文件夹是由 Visual Studio 在前面的步骤中创建的。如果您将`Projects`文件夹放在其他地方，请在那里执行此步骤。我们需要复制到项目文件夹中的文件位于我们的`SFML\\bin`文件夹中。为这两个位置分别打开一个窗口，高亮显示所有`.dll`文件。\n2.  现在，将突出显示的文件复制并粘贴到项目中。\n\n这个项目现在已经准备好了。\n\n## 项目资产\n\n这个项目中的资产甚至比僵尸竞技场游戏还要多和多样。像往常一样，这些资产包括用于在屏幕上书写的字体、用于不同动作(如跳跃、到达目标或远处的轰鸣声)的音效，当然还有托马斯和鲍勃的图形以及用于所有背景图块的精灵表。\n\n该游戏所需的所有资产都包含在下载包中。它们可以在`Chapter 14/graphics`和`Chapter 14/sound`文件夹中找到。\n\n除了我们期待的图形、声音和字体，这款游戏还有两种新的资产类型。它们是关卡设计文件和 GLSL 着色器程序。让我们了解一下他们每个人。\n\n### 游戏级设计\n\n级别都是在文本文件中创建的。通过使用数字 0 到 3，我们可以构建关卡设计来挑战玩家。所有级别设计都与其他资产在同一目录下的`levels`文件夹中。现在可以随便看一下，但是我们将在*第 18 章*、*粒子系统和着色器*中详细讨论它们。\n\n除了这些级别设计资产，我们还有一种特殊类型的图形资产，称为**着色器**。\n\n### GLSL 着色器\n\n**着色器**是用 **GLSL** ( **图形库着色语言**编写的程序。不要担心必须学习另一种语言，因为我们不需要太深入来利用着色器。着色器是特殊的，因为它们是独立于我们的 C++ 代码的完整程序，由 GPU 每一帧执行。事实上，这些着色器程序中的一些是针对每一帧、每一个像素运行的！我们将在 [*第 18 章*](18.html#_idTextAnchor356)*粒子系统和着色器*中找到更多关于这些细节的信息。如果等不了那么久，看看下载包`Chapter 14/shaders`文件夹里的文件。\n\n### 图形资产关闭\n\n图形资源构成了我们游戏场景的一部分。如果您看一下图形资产，应该很清楚它们将在我们的游戏中的什么地方使用:\n\n![](img/B14278_14_03.jpg)\n\n如果`tiles_sheet`图形上的瓷砖看起来与游戏截图有点不同，这是因为它们是部分透明的，通过它们显示的背景会有一点变化。如果背景图形看起来与游戏截图中的实际背景完全不同，那是因为我们将要编写的着色器程序将操纵每一个像素、每一帧，以创建一种“熔化”效果。\n\n### 声音资产关闭\n\n声音文件都是`.wav`格式。这些文件包含我们将在整个游戏中的某些事件中播放的声音效果。它们如下:\n\n*   `fallinfire.wav`:玩家头部起火，玩家没有逃生机会时会发出的声音。\n*   `fallinwater.wav`:水和火有一样的终结效果:死亡。这个音效通知玩家，他们需要从关卡的开头开始。\n*   `fire1.wav`:这个音效是单声道录制的。它将以不同的音量播放，根据玩家与火砖的距离，以及根据玩家是在火砖的左边还是右边，从不同的扬声器播放。显然，我们还需要学习一些技巧来实现这个功能。\n*   `jump.wav`:玩家跳跃时发出的令人愉悦(略微可预测)的鸣响。\n*   `reachgoal.wav`:当玩家将两个角色(托马斯和鲍勃)都拿到目标牌时发出的令人愉悦的胜利声音。\n\n音效非常简单，你可以很容易地创建自己的。如果您打算替换`fire1.wav` 文件，请务必以单声道(非立体声)格式保存您的声音。原因将在 [*第 17 章*](17.html#_idTextAnchor340)*声音空间化和平显中解释。*\n\n### 将资产添加到项目\n\n一旦您决定了要使用哪些资产，就该将它们添加到项目中了。以下说明将假设您正在使用本书下载包中提供的所有资源。\n\n如果您使用自己的文件，只需用自己的文件替换适当的声音或图形文件，使用完全相同的文件名。让我们开始吧:\n\n1.  浏览至`D:\\VS Projects\\TWL`文件夹。\n2.  在此文件夹中创建五个新文件夹，并将其命名为`graphics`、`sound`、`fonts`、 `shaders,` 和 `levels`。\n3.  从下载包中，将`Chapter 14/graphics`的全部内容复制到`D:\\VS Projects\\TWL\\graphics`文件夹中。\n4.  从下载包中，将`Chapter 14/sound`的全部内容复制到`D:\\VS Projects\\TWL\\sound`文件夹中。\n5.  现在，在你的网络浏览器中访问[http://www.dafont.com/roboto.font](http://www.dafont.com/roboto.font)，下载**机器人之光**字体。\n6.  提取压缩下载的内容，并将`Roboto-Light.ttf`文件添加到`D:\\VS Projects\\TWL\\fonts`文件夹。\n7.  从下载包中，将`Chapter 12/levels` 的全部内容复制到`D:\\VS Projects\\TWL\\levels`文件夹中。\n8.  从下载包中，将`Chapter 12/shaders` 的全部内容复制到`D:\\VS Projects\\TWL\\shaders`文件夹中。\n\n现在我们有了一个新项目，以及整个项目所需的所有资产，我们可以讨论如何构建游戏引擎代码。\n\n# 构建托马斯迟到的代码\n\n尽管采取了一些措施来减少问题，但是一个问题随着项目的进行变得越来越糟糕，那就是代码变得多长，多笨拙。**面向对象编程** ( **OOP** )允许我们将我们的项目分成逻辑的和可管理的块，称为类。\n\n通过引入`Engine`类，我们将对这个项目中代码的可管理性进行很大的改进。在其他功能中，`Engine`类将有三个私有功能。这些是`input`、`update`和`draw`。这些听起来应该很熟悉。这些函数中的每一个都将保存先前在`main`函数中的一大块代码。这些功能中的每一个都将在自己的代码文件中，即分别为`Input.cpp`、`Update.cpp`和`Draw.cpp`。\n\n`Engine`类中还会有一个公共函数，可以用`Engine`的一个实例来调用。该功能为`run`，负责游戏每帧调用`input`、`update`、`draw`一次:\n\n![](img/B14278_14_06.jpg)\n\n此外，因为我们已经将游戏引擎的主要部分抽象到了`Engine`类，所以我们也可以将许多变量从`main`中移出，并使它们成为`Engine`的成员。我们所需要做的就是创建一个`Engine`的实例，并调用它的`run`函数。这里是超级简单的`main`功能的预览:\n\n```cpp\nint main()\n{\n    // Declare an instance of Engine\n    Engine engine;\n    // Start the engine\n    engine.run();\n    // Quit in the usual way when the engine is stopped\n    return 0;\n}\n```\n\n小费\n\n暂时不要添加前面的代码。\n\n为了使我们的代码更加易于管理和阅读，我们还将抽象出大任务的责任，例如将级别和冲突检测加载到单独的函数中(在单独的代码文件中)。这两个功能分别是`loadLevel`和`detectCollisions`。我们还将编写其他函数来处理托马斯迟到项目的一些新特性。当它们发生时，我们将详细介绍它们。\n\n为了进一步利用面向对象程序，我们将把游戏领域的责任完全委托给新的类。你可能还记得，在以前的项目中，声音和抬头显示器代码相当长。我们将构建一个`SoundManager`和`HUD`类，以更干净的方式处理这些方面。当我们实现它们时，将深入探索它们是如何工作的。\n\n游戏关卡本身也比以前的游戏深入很多，所以我们也将会编码一个`LevelManager`类。\n\n正如你所料，可玩的角色也将由职业组成。然而，对于这个项目，我们将学习更多的 C++ 并实现一个具有托马斯和鲍勃所有共同功能的`PlayableCharacter`类。然后，`Thomas`和`Bob`类将*继承*这个共同的功能，并实现自己独特的功能和能力。这种技术，也许不出所料，被称为**继承**。关于继承，我会在下一章详细介绍: [*第十五章*](15.html#_idTextAnchor306)*高级 OOP——继承与多态*。\n\n我们还将实现其他几个类来执行特定的职责。例如，我们将使用粒子系统进行一些简单的爆炸。你也许能猜到，为了做到这一点，我们将编码一个`Particle`类和一个`ParticleSystem`类。所有这些类都有属于`Engine`类的实例。这样做将使游戏的所有功能都可以从游戏引擎中访问，但是将细节封装到适当的类中。\n\n小费\n\n请注意，尽管有这些新技术来分离我们代码的不同方面，到这个项目结束时，我们仍然会有一些稍微笨拙的类。这本书的最后一个项目，虽然是一个简单得多的射击游戏，但将探索另一种组织代码的方式，使其易于管理。\n\n在我们继续查看将构成`Engine`类的实际代码之前，最后要提到的是，我们将重用`TextureHolder`类，而不做任何更改，该类是我们为僵尸竞技场游戏讨论和编码的。\n\n# 构建游戏引擎\n\n正如我们在上一节中所建议的，我们将编写一个名为`Engine`的类来控制和绑定托马斯迟到游戏的不同部分。\n\n我们要做的第一件事是在这个项目中提供上一个项目的`TextureHolder`类。\n\n## 重用 TextureHolder 类\n\n我们为僵尸竞技场游戏讨论和编码的`TextureHolder`类在这个项目中也很有用。虽然可以直接从上一个项目中添加文件(`TextureHolder.h`和`TextureHolder.cpp`)，而无需重新编码或重新创建文件，但我不想假设您没有直接跳到这个项目。接下来是非常简短的说明，以及我们需要的完整代码清单，以创建`TextureHolder`类。如果您想要解释类或代码，请参见 [*第 10 章*](10.html#_idTextAnchor214)*指针、标准模板库和纹理管理*。\n\n小费\n\n如果你已经完成了上一个项目，并且你*想从僵尸竞技场项目中添加一个职业，简单地做下面的事情。在**解决方案资源管理器**窗口中，右键单击**头文件**，选择**添加|现有项目...**。从上一个项目浏览到`TextureHolder.h`并选择。在**解决方案资源管理器**窗口中，右键单击**源文件**并选择**添加|现有项目...**。从上一个项目浏览到`TextureHolder.cpp`并选择它。现在你可以在这个项目中使用`TextureHolder`类。请注意，文件在项目之间共享，任何更改都将在两个项目中生效。*\n\n要从头创建`TextureHolder`类，右键单击**解决方案资源管理器**中的**头文件**，并选择**添加|新项目...**。在**添加新项目**窗口中，突出显示(通过左键单击)**头文件(。h)** ，然后在**名称**字段中，键入`TextureHolder.h`。最后，点击**添加**按钮。\n\n在`TextureHolder.h`中添加以下代码:\n\n```cpp\n#pragma once\n#ifndef TEXTURE_HOLDER_H\n#define TEXTURE_HOLDER_H\n#include <SFML/Graphics.hpp>\n#include <map>\nclass TextureHolder\n{\nprivate:\n    // A map container from the STL,\n    // that holds related pairs of String and Texture\n    std::map<std::string, sf::Texture> m_Textures;\n    // A pointer of the same type as the class itself\n    // the one and only instance\n    static TextureHolder* m_s_Instance;\npublic:\n    TextureHolder();\n    static sf::Texture& GetTexture(std::string const& filename);\n};\n#endif\n```\n\n右键单击**解决方案资源管理器**中的**源文件**，并选择**添加|新项目...**。在**添加新项目**窗口中，突出显示(通过左键单击) **C++ 文件(。cpp)** ，然后在**名称**字段中，键入`TextureHolder.cpp`。最后，点击**添加**按钮。\n\n在`TextureHolder.cpp`中添加以下代码:\n\n```cpp\n#include \"TextureHolder.h\"\n#include <assert.h>\nusing namespace sf;\nusing namespace std;\nTextureHolder* TextureHolder::m_s_Instance = nullptr;\nTextureHolder::TextureHolder()\n{\n    assert(m_s_Instance == nullptr);\n    m_s_Instance = this;\n}\nsf::Texture& TextureHolder::GetTexture(std::string const& filename)\n{\n    // Get a reference to m_Textures using m_S_Instance\n    auto& m = m_s_Instance->m_Textures;\n    // auto is the equivalent of map<string, Texture>\n    // Create an iterator to hold a key-value-pair (kvp)\n    // and search for the required kvp\n    // using the passed in file name\n    auto keyValuePair = m.find(filename);\n    // auto is equivalent of map<string, Texture>::iterator\n    // Did we find a match?\n    if (keyValuePair != m.end())\n    {\n        // Yes\n        // Return the texture,\n        // the second part of the kvp, the texture\n        return keyValuePair->second;\n    }\n    else\n    {\n        // File name not found\n        // Create a new key value pair using the filename\n        auto& texture = m[filename];\n        // Load the texture from file in the usual way\n        texture.loadFromFile(filename);\n        // Return the texture to the calling code\n        return texture;\n    }\n}\n```\n\n我们现在可以继续上新的`Engine`课了。\n\n## 编码引擎\n\n像往常一样，我们将从头文件开始，头文件保存函数声明和成员变量。请注意，我们将在整个项目中重新访问这个文件，以添加更多的函数和成员变量。在这个阶段，我们将只添加必要的代码。\n\n右键单击**解决方案资源管理器**中的**头文件**，选择**添加|新项目...**。在**添加新项目**窗口中，突出显示(通过左键单击)**头文件(。h)** 然后在**名称**字段中，输入`Engine.h`。最后，点击**添加**按钮。我们现在准备为`Engine`类编码头文件。\n\n添加以下成员变量以及函数声明。他们中的许多人我们在其他项目中已经见过，其中一些在*构建托马斯迟到代码*一节中讨论过。注意函数和变量名，以及它们是私有的还是公共的。将以下代码添加到`Engine.h`文件中，然后我们将讨论它:\n\n```cpp\n#pragma once\n#include <SFML/Graphics.hpp>\n#include \"TextureHolder.h\"\nusing namespace sf;\nclass Engine\n{\nprivate:\n    // The texture holder\n    TextureHolder th;\n    const int TILE_SIZE = 50;\n    const int VERTS_IN_QUAD = 4;\n    // The force pushing the characters down\n    const int GRAVITY = 300;\n    // A regular RenderWindow\n    RenderWindow m_Window;\n    // The main Views\n    View m_MainView;\n    View m_LeftView;\n    View m_RightView;\n    // Three views for the background\n    View m_BGMainView;\n    View m_BGLeftView;\n    View m_BGRightView;\n    View m_HudView;\n    // Declare a sprite and a Texture \n    // for the background\n    Sprite m_BackgroundSprite;\n    Texture m_BackgroundTexture;\n    // Is the game currently playing?\n    bool m_Playing = false;\n    // Is character 1 or 2 the current focus?\n    bool m_Character1 = true;\n    // Start in full screen (not split) mode\n    bool m_SplitScreen = false;\n    // Time left in the current level (seconds)\n    float m_TimeRemaining = 10;\n    Time m_GameTimeTotal;\n    // Is it time for a new/first level?\n    bool m_NewLevelRequired = true;\n\n    // Private functions for internal use only\n    void input();\n    void update(float dtAsSeconds);\n    void draw();\n\npublic:\n    // The Engine constructor\n    Engine();\n    // Run will call all the private functions\n    void run();\n};\n```\n\n这是所有私有变量和函数的完整运行记录。在适当的地方，我会花一点时间解释:\n\n*   `TextureHolder th`:唯一的`TextureHolder`类实例。\n*   `TILE_SIZE`:一个有用的常数，提醒我们子画面中的每个图块都是 50 像素宽，50 像素高。\n*   `VERTS_IN_QUAD`:一个有用的常数，让我们对`VertexArray`的操作不那么容易出错。事实上，一个四边形有四个顶点。现在，我们不能忘记它。\n*   `GRAVITY`:一个常量 int 值，表示每秒向下推动游戏角色的像素数。一旦游戏结束，这是一个非常有趣的玩法。我们在这里将其初始化为 300，因为这对于我们的初始级别设计很有效。\n*   `m_Window`:我们所有项目中常见的`RenderWindow`对象。\n*   SFML `View`对象、`m_MainView`、`m_LeftView`、`m_RightView`、`m_BGMainView` : `m_BGLeftView`、`m_BGRightView`和`m_HudView`:前三个`View`对象是游戏的全屏视图和左右分屏视图。我们也有一个单独的 SFML `View`对象为这三个，这将绘制背后的背景。最后一个`View`对象`m_HudView`将绘制在其他六个视图的适当组合之上，以显示分数、剩余时间以及给玩家的任何消息。拥有七个不同的`View`对象可能意味着复杂性，但是当你看到随着章节的进展我们如何处理它们时，你会发现它们非常简单。到本章结束时，我们将把整个分屏/全屏难题整理出来。\n*   `Sprite m_BackgroundSprite`和`Texture m_BackgroundTexture`:可以预见的是，这个 SFML 精灵和纹理的组合将用于显示和保存图形资源文件夹中的背景图形。\n*   `m_Playing`:该布尔值将告知游戏引擎关卡是否已经开始(通过按*回车*键)。玩家一旦开始游戏，就不能选择暂停游戏。\n*   `m_Character1`:屏幕满屏时，应该以托马斯(`m_Character1` `= true`)还是鲍勃(`m_Character1 = false`)为中心？最初，它被初始化为真，以托马斯为中心。\n*   `m_SplitScreen`:这个变量用来判断当前正在玩的游戏是否处于分屏模式。我们将使用这个变量来决定如何准确地使用我们在几个步骤前声明的所有视图对象。\n*   `m_TimeRemaining`变量:这个`float`变量保存了到达当前关卡目标的剩余时间(以秒为单位)。在前面的代码中，出于测试的目的，它被设置为 10，直到我们为每个级别设置一个特定的时间。\n*   `m_GameTimeTotal`变量:这个变量是一个 SFML `Time`对象。它记录游戏已经玩了多长时间。\n*   `m_NewLevelRequired`布尔变量:这个变量关注玩家是刚完成一关还是失败一关。然后，我们可以使用它来触发加载下一个级别或重新启动当前级别。\n*   `input`功能:这个功能会处理玩家的所有输入，在这个游戏中完全是来自键盘。乍一看，它似乎直接处理所有的键盘输入。然而，在这个游戏中，我们将在`Thomas`和`Bob`类中处理直接影响托马斯或鲍勃的键盘输入。该功能还将处理键盘输入，如退出，切换到分屏，以及任何其他键盘输入。\n*   `update`功能:该功能将完成我们之前在`main`功能的更新部分所做的所有工作。我们还将从`update`函数中调用一些其他函数，以保持代码的组织性。如果你回头看代码，你会看到它接收到一个`float`参数，该参数将保存从上一帧开始过去的几分之一秒。当然，这正是我们更新所有游戏对象所需要的。\n*   `draw`功能:该功能将保存以前项目中主功能的绘图部分中曾经进入的所有代码。然而，当我们考虑用 SFML 绘制的其他方法时，我们会有一些没有保存在这个函数中的绘图代码。当我们在 [*第 18 章*](18.html#_idTextAnchor356)*粒子系统和着色器*中了解粒子系统时，我们会看到这个新代码。\n\n现在，让我们运行所有的公共函数:\n\n*   `Engine`构造函数:正如我们已经预料到的，当我们第一次声明`Engine`的一个实例时，这个函数将被调用。它将完成类的所有设置和初始化。我们将在不久后对`Engine.cpp`文件进行编码时看到确切的内容。\n*   `run`函数:这是我们唯一需要调用的公共函数。会触发`input`、`update`、`draw`的执行，会为我们做所有的工作。\n\n接下来，我们将看到所有这些函数的定义以及一些正在运行的变量。\n\n## 编码引擎\n\n在我们之前的所有类中，我们已经将所有函数定义放入了以类名为前缀的`.cpp`文件中。因为我们这个项目的目标是让代码更容易管理，所以我们做的事情有点不同。\n\n在`Engine.cpp`文件中，我们将放置构造函数(`Engine`)和公共`run`函数。其余的功能将放在它们自己的`.cpp`文件中，该文件有一个名称，表明哪个功能放在哪里。如果我们在包含来自`Engine`类的函数定义的所有文件的顶部添加适当的 include 指令(`#include \"Engine.h\"` ) ，这对于编译器来说不是问题。\n\n让我们从编码`Engine`并在`Engine.cpp`中运行开始。在**解决方案资源管理器**中右键单击**源文件**，选择**添加|新项目...**。在**添加新项目**窗口中，突出显示(通过左键单击) **C++ 文件(。cpp)** 然后，在**名称**字段中，键入`Engine.cpp`。最后，点击**添加**按钮。我们现在准备为`Engine`类编码`.cpp`文件。\n\n### 编写引擎类构造函数定义\n\n这个函数的代码将放在我们最近创建的`Engine.cpp`文件中。\n\n添加以下代码，然后我们可以讨论它:\n\n```cpp\n#include \"Engine.h\"\nEngine::Engine()\n{\n    // Get the screen resolution \n    // and create an SFML window and View\n    Vector2f resolution;\n    resolution.x = VideoMode::getDesktopMode().width;\n    resolution.y = VideoMode::getDesktopMode().height;\n    m_Window.create(VideoMode(resolution.x, resolution.y),\n        \"Thomas was late\",\n        Style::Fullscreen);\n    // Initialize the full screen view\n    m_MainView.setSize(resolution);\n    m_HudView.reset(\n        FloatRect(0, 0, resolution.x, resolution.y));\n    // Initialize the split-screen Views\n    m_LeftView.setViewport(\n        FloatRect(0.001f, 0.001f, 0.498f, 0.998f));\n    m_RightView.setViewport(\n        FloatRect(0.5f, 0.001f, 0.499f, 0.998f));\n    m_BGLeftView.setViewport(\n        FloatRect(0.001f, 0.001f, 0.498f, 0.998f));\n    m_BGRightView.setViewport(\n        FloatRect(0.5f, 0.001f, 0.499f, 0.998f));\n\n    m_BackgroundTexture = TextureHolder::GetTexture(\n        \"graphics/background.png\");\n    // Associate the sprite with the texture\n    m_BackgroundSprite.setTexture(m_BackgroundTexture);\n}\n```\n\n我们以前见过很多这样的代码。例如，有通常的代码行来获得屏幕分辨率，以及创建`RenderWindow`。在前面代码的末尾，我们使用现在熟悉的代码加载一个纹理，并将其分配给一个 Sprite。在这种情况下，我们正在加载`background.png`纹理并将其分配给`m_BackgroundSprite`。\n\n需要解释的是对`setViewport`函数的四次调用之间的代码。`setViewport`功能将屏幕的一部分分配给 SFML `View`对象。然而，它不适用于像素坐标。它使用比率工作。这里，“1”是整个屏幕(宽度或高度)。每次调用`setViewport`的前两个值是起始位置(先水平后垂直)，后两个值是结束位置。\n\n请注意`m_LeftView`和`m_BGLeftView`被放置在完全相同的位置，也就是说，从屏幕的最左边(0.001)开始，到中间(0.498)的千分之二结束。\n\n`m_RightView`和`m_BGRightView`也处于彼此完全相同的位置，从前面两个`View`对象的右侧(0.5)开始，几乎延伸到最右侧(0.998)。\n\n此外，所有的视图在屏幕的顶部和底部都留有微小的缝隙。当我们在屏幕上绘制这些`View`物体时，在一个白色背景的上面，会有屏幕两边之间有一条细细的白线，以及边缘周围有一条细细的白色边框的屏幕分割效果。\n\n我试图在下图中表示这种效果:\n\n![](img/Image90695.jpg)\n\n理解它的最好方法是完成这一章，运行代码，并看到它在运行。\n\n### 运行函数定义的编码\n\n这个函数的代码将放在我们最近创建的`Engine.cpp`文件中。\n\n在前一个构造函数代码之后立即添加以下代码:\n\n```cpp\nvoid Engine::run()\n{\n    // Timing     \n    Clock clock;\n    while (m_Window.isOpen())\n    {\n        Time dt = clock.restart();\n        // Update the total game time\n        m_GameTimeTotal += dt;\n        // Make a decimal fraction from the delta time\n        float dtAsSeconds = dt.asSeconds();\n        // Call each part of the game loop in turn\n        input();\n        update(dtAsSeconds);\n        draw();\n    }\n}\n```\n\n`run`功能是我们发动机的中心；它启动所有其他部分。首先，我们声明一个`Clock`对象。接下来，我们有熟悉的`while(window.isOpen())`循环，它创建了游戏循环。在这个 while 循环中，我们执行以下操作:\n\n1.  重启`clock`并保存上一个循环在`dt.`中花费的时间\n2.  记录`m_GameTimeTotal.`中经过的总时间\n3.  声明并初始化一个`float`来表示在前一帧中经过的那一秒的分数。\n4.  呼叫`input.`\n5.  调用`update`，传入经过的时间(`dtAsSeconds`)。\n6.  呼叫`draw.`\n\n所有这些看起来应该都很熟悉。新的是包裹在`run`功能中。\n\n### 输入函数定义的编码\n\n正如我们之前解释的那样，`input`函数的代码将放在自己的文件中，因为它比构造函数或`run`函数更广泛。我们将使用`#include \"Engine.h\"`并将`Engine::`作为函数签名的前缀，以确保编译器知道我们的意图。\n\n右键单击**解决方案资源管理器**中的**源文件**，并选择**添加|新项目...**。在**添加新项目**窗口中，突出显示(通过左键单击) **C++ 文件(。cpp)** 然后，在**名称**字段中，键入`Input.cpp`。最后，点击**添加**按钮。我们现在准备对`input` 功能进行编码。\n\n添加以下代码:\n\n```cpp\nvoid Engine::input()\n{\n    Event event;\n    while (m_Window.pollEvent(event))\n    {\n        if (event.type == Event::KeyPressed)\n        {            \n            // Handle the player quitting\n            if (Keyboard::isKeyPressed(Keyboard::Escape))\n            {\n                m_Window.close();\n            }\n            // Handle the player starting the game\n            if (Keyboard::isKeyPressed(Keyboard::Return))\n            {\n                m_Playing = true;\n            }\n            // Switch between Thomas and Bob\n            if (Keyboard::isKeyPressed(Keyboard::Q))\n            {\n                m_Character1 = !m_Character1;\n            }\n            // Switch between full and split-screen\n            if (Keyboard::isKeyPressed(Keyboard::E))\n            {\n                m_SplitScreen = !m_SplitScreen;\n            }\n        }\n    }    \n}\n```\n\n像前面的项目一样，我们检查每一帧的`RenderWindow`事件队列。也像我们之前做的一样，我们使用`if (Keyboard::isKeyPressed...`检测特定的键盘按键。我们刚刚添加的代码中最相关的信息是密钥的作用:\n\n*   和往常一样， *Esc* 键关闭窗口，游戏将退出。\n*   *进入*键将`m_Playing`设置为真，最终这将具有开始关卡的效果。\n*   *Q* 键在真和假之间交替`m_Character1`的值。该键仅在全屏模式下有效。它将在托马斯和鲍勃之间切换，成为主`View`的中心。\n*   *E* 键盘键在真和假之间切换`m_SplitScreen`。这将具有在全屏和分屏视图之间切换的效果。\n\n到本章结束时，大部分键盘功能将完全正常工作。我们正在接近能够运行我们的游戏引擎。接下来，让我们对`update`函数进行编码。\n\n### 对更新函数定义进行编码\n\n正如我们之前所解释的，这个函数的代码将放在它自己的文件中，因为它比构造函数或`run`函数更广泛。我们将使用`#include \"Engine.h\"`并在函数签名前加上`Engine::`前缀，以确保编译器知道我们的意图。\n\n右键单击**解决方案资源管理器**中的**源文件**，并选择**添加|新项目...**。在**添加新项目**窗口中，突出显示(通过左键单击) **C++ 文件(。cpp)** 然后，在**名称**字段中，键入`Update.cpp`。最后，点击**添加**按钮。我们现在准备为`update` 函数编写一些代码。\n\n在`Update.cpp` 文件中添加以下代码来实现`update`功能:\n\n```cpp\n#include \"Engine.h\"\n#include <SFML/Graphics.hpp>\n#include <sstream>\nusing namespace sf;\nvoid Engine::update(float dtAsSeconds)\n{\n    if (m_Playing)\n    {\n        // Count down the time the player has left\n        m_TimeRemaining -= dtAsSeconds;\n        // Have Thomas and Bob run out of time?\n        if (m_TimeRemaining <= 0)\n        {\n            m_NewLevelRequired = true;\n        }\n    }// End if playing\n\n}\n```\n\n首先，注意`update`函数接收前一帧作为参数的时间。当然，这对于更新功能履行其职责是必不可少的。\n\n前面的代码在这个阶段没有实现任何可见的东西。它确实建立了我们在未来章节中需要的结构。它从`m_TimeRemaining`中减去前一帧所花费的时间，并检查时间是否已经用完。如果有，则将`m_NewLevelRequired`设置为`true`。所有这些代码都封装在一个`if`语句中，该语句仅在`m_Playing`为`true`时执行。这样做的原因是，和之前的项目一样，我们不希望游戏还没有开始的时候，时间就提前了，对象就更新了。\n\n随着项目的继续，我们将在此代码的基础上进行构建。\n\n### 绘制函数定义的编码\n\n正如我们之前所解释的，这个函数的代码将放在它自己的文件中，因为它比构造函数或`run`函数更广泛。我们将使用`#include \"Engine.h\"`并在函数签名前加上`Engine::`前缀，以确保编译器知道我们的意图。\n\n右键单击**解决方案资源管理器**中的**源文件**，并选择**添加|新项目...**。在**添加新项目**窗口中，突出显示(通过左键单击) **C++ 文件(。cpp)** 然后，在**名称**字段中，键入`Draw.cpp`。最后点击**添加**按钮。我们现在准备向`draw` 功能添加一些代码。\n\n在`Draw.cpp`文件中添加以下代码来实现`draw`功能:\n\n```cpp\n#include \"Engine.h\"\nvoid Engine::draw()\n{\n    // Rub out the last frame\n    m_Window.clear(Color::White);\n    if (!m_SplitScreen)\n    {\n        // Switch to background view\n        m_Window.setView(m_BGMainView);\n        // Draw the background\n        m_Window.draw(m_BackgroundSprite);\n        // Switch to m_MainView\n        m_Window.setView(m_MainView);        \n    }\n    else\n    {\n        // Split-screen view is active\n        // First draw Thomas' side of the screen\n        // Switch to background view\n        m_Window.setView(m_BGLeftView);\n        // Draw the background\n        m_Window.draw(m_BackgroundSprite);\n        // Switch to m_LeftView\n        m_Window.setView(m_LeftView);\n\n        // Now draw Bob's side of the screen\n        // Switch to background view\n        m_Window.setView(m_BGRightView);\n        // Draw the background\n        m_Window.draw(m_BackgroundSprite);\n        // Switch to m_RightView\n        m_Window.setView(m_RightView);\n\n    }\n    // Draw the HUD\n    // Switch to m_HudView\n    m_Window.setView(m_HudView);\n\n    // Show everything we have just drawn\n    m_Window.display();\n}\n```\n\n在前面的代码中，没有什么是我们以前没有见过的。像往常一样，代码从清除屏幕开始。在这个项目中，我们用白色清除屏幕。新的是不同的绘图选项被一个条件分开的方式，该条件检查屏幕当前是否被分割:\n\n```cpp\nif (!m_SplitScreen)\n{\n}\nelse\n{\n}\n```\n\n如果屏幕没有分屏，我们在后台`View` ( `m_BGView`)画背景精灵，然后切换到主全屏`View` ( `m_MainView`)。注意，目前我们在`m_MainView`不做任何绘图。\n\n另一方面，如果屏幕被分割，执行`else`块中的代码，我们用屏幕左侧的背景精灵绘制`m_BGLeftView`，然后切换到`m_LeftView`。\n\n然后，还是在`else`块，我们用屏幕右边的背景精灵画`m_BGRightView`，然后切换到`m_RightView`。\n\n在我们刚刚描述的`if` `else`结构之外，我们切换到`m_HUDView`。在这个阶段，我们实际上没有在`m_HUDView`中绘制任何东西。\n\n像另外两个(`input`、`update`)最显著的三个函数一样，我们会经常回到`draw`函数。我们将为我们的游戏添加需要绘制的新元素。您会注意到，每次我们这样做的时候，我们都会将代码添加到主部分、左侧部分和右侧部分。\n\n让我们快速回顾一下`Engine`课，然后我们可以启动它。\n\n## 迄今为止的发动机等级\n\n我们所做的是将曾经在`main`函数中的所有代码抽象成`input`、`update`和`draw`函数。这些功能的连续循环以及计时由`run`功能处理。\n\n考虑在 Visual Studio 中打开 **Input.cpp** 、 **Update.cpp** 和 **Draw.cpp** 选项卡，可能是按顺序组织的，如下图所示:\n\n![](img/B14278_14_09.jpg)\n\n我们将在整个项目过程中重新访问这些函数中的每一个，并添加更多的代码。现在我们已经有了`Engine`类的基本结构和功能，我们可以在`main`函数中创建它的一个实例，并看到它在运行。\n\n# 编码主要功能\n\n让我们将项目创建时自动生成的`TFL.cpp`文件重命名为`Main.cpp`。右键单击**解决方案资源管理器**中的`TFL`文件，并选择**重命名**。更名为`Main.cpp`。这将是包含我们的`main`函数和实例化`Engine`类的代码的文件。\n\n在`Main.cpp`中添加以下代码:\n\n```cpp\n#include \"Engine.h\"\nint main()\n{\n    // Declare an instance of Engine\n    Engine engine;\n    // Start the engine VRRrrrrmmm\n    engine.run();\n    // Quit in the usual way when the engine is stopped\n    return 0;\n}\n```\n\n我们所做的就是为`Engine`类添加一个`include`指令，声明一个`Engine`的实例，然后调用它的`run`函数。一切都将由`Engine`类处理，直到玩家退出，执行返回到`main`和`return 0`语句。\n\n那很简单。现在，我们可以运行游戏，看到空的背景，无论是全屏还是分屏，最终都会包含所有的动作。\n\n这是目前为止全屏模式下的游戏，只显示了背景:\n\n![](img/B14278_14_10.jpg)\n\n现在，点击 *E* 键。您将能够看到屏幕被整齐地分成两半，为分屏合作游戏做好准备:\n\n![](img/B14278_14_11.jpg)\n\n# 总结\n\n在这一章中，我们介绍了托马斯迟到游戏，并为项目的其余部分奠定了理解和代码结构的基础。的确，解决方案资源管理器中有很多文件，但是，如果我们理解每个文件的目的，我们会发现实现项目的其余部分非常容易。\n\n在下一章中，我们将学习两个更基本的 C++ 主题，继承和多态性。我们也将开始使用它们，建造三个职业来代表两个可玩的角色。\n\n# 常见问题\n\n你可能会想到一个问题:\n\n问)我不完全理解代码文件的结构。我该怎么办？\n\n答:的确，抽象会让我们的代码结构变得不那么清晰，但实际代码本身会变得容易得多。我们将把代码分成`Input.cpp`、`Update.cpp`和`Draw.cpp`，而不是像在前面的项目中那样把所有东西都塞进`main`函数。此外，我们将使用更多的类来将相关代码组合在一起。再次学习*构建托马斯迟到代码*部分，尤其是图表。*"
  },
  {
    "path": "docs/begin-cpp-game-prog/15.md",
    "content": "# 十五、高级 OOP——继承与多态\n\n在本章中，我们将通过查看**继承**和**多态性**的稍微高级的概念来进一步扩展我们对 OOP 的了解。然后，我们将能够使用这些新知识来实现我们游戏中的明星人物，托马斯和鲍勃。以下是我们将在本章中介绍的内容:\n\n*   了解如何使用继承来扩展和修改类\n*   通过使用多态性，将类的对象视为不止一种类型的类\n*   了解抽象类，以及设计从未实例化的类实际上有多有用\n*   建立一个抽象的`PlayableCharacter`类\n*   将继承用于`Thomas`和`Bob`类\n*   将托马斯和鲍勃添加到游戏项目中\n\n# 遗传\n\n我们已经看到了如何通过实例化来自 SFML 图书馆的类的对象来利用其他人的辛勤工作。但是这整个面向对象的事情甚至比这更进一步。\n\n如果有一个类中有很多有用的功能，但并不是我们想要的，该怎么办？在这种情况下，我们可以从另一个类**继承**。就像听起来的那样，**继承**意味着我们可以利用其他人的类的所有特性和好处，包括封装，同时进一步细化或扩展代码，使其专门适合我们的情况。在这个项目中，我们将继承和扩展一些 SFML 类；我们也会在自己的课堂上这样做。\n\n让我们看一些使用继承的代码。\n\n## 扩展一个类\n\n考虑到所有这些，让我们看一个示例类，看看我们如何扩展它，只看语法，作为第一步。\n\n首先，我们定义一个要继承的类。这与我们创建任何其他类的方式没有什么不同。看一下这个假设的`Soldier`类声明:\n\n```cpp\nclass Soldier\n{\n    private:\n        // How much damage can the soldier take\n        int m_Health;\n        int m_Armour;\n        int m_Range;\n        int m_ShotPower;\n\n    Public:\n        void setHealth(int h);\n        void setArmour(int a);    \n        void setRange(int r);\n        void setShotPower(int p);\n};\n```\n\n在前面的代码中，我们定义了一个`Soldier`类。它有四个私有变量:`m_Health`、`m_Armour`、`m_Range`和`m_ShotPower`。它还有四个公共功能:`setHealth`、`setArmour, setRange`和 `setShotPower`。我们不需要看到这些函数的定义；他们将简单地初始化适当的变量，这样他们的名字就显而易见了。\n\n我们也可以想象一个完全实现的`Soldier`类会比这个深入得多。它可能会有`shoot`、`goProne`等功能。如果我们在 SFML 项目中实现了一个`Soldier`类，它可能会有一个`Sprite`对象，以及一个`update`和一个`getPostion`函数。\n\n如果我们想了解继承，我们在这里介绍的简单场景是合适的。现在，让我们看看一些新的东西:从`Soldier`类继承。请看下面的代码，尤其是突出显示的部分:\n\n```cpp\nclass Sniper : public Soldier\n{\npublic:\n    // A constructor specific to Sniper\n    Sniper::Sniper();\n};\n```\n\n通过在`Sniper`类声明中添加`: public Soldier`，`Sniper`继承自`Soldier`。但这到底意味着什么？`Sniper` **是一个** `Soldier`。它拥有`Soldier`的所有变量和功能。然而，继承甚至不止于此。\n\n还要注意，在前面的代码中，我们声明了一个`Sniper`构造函数。这个构造器是`Sniper`独有的。我们不仅继承了`Soldier`；我们有**扩展** `Soldier`。`Soldier`类的所有功能(定义)将由`Soldier`类处理，但是`Sniper`构造函数的定义必须由`Sniper`类处理。\n\n假设的`Sniper`构造函数定义可能是这样的:\n\n```cpp\n// In Sniper.cpp\nSniper::Sniper()\n{\n    setHealth(10);\n    setArmour(10);    \n    setRange(1000);\n    setShotPower(100);\n}\n```\n\n我们可以继续写一堆其他类，它们是`Soldier`类的扩展，也许是`Commando`和`Infantryman`。每一个都有完全相同的变量和函数，但是每一个都可以有一个唯一的构造函数来初始化那些适合于特定类型`Soldier`的变量。`Commando`可能有很高的`m_Health`和`m_ShotPower`，但真的微不足道`m_Range`。`Infantryman`可能介于`Commando`和`Sniper`之间，每个变量的值都一般。\n\n好像 OOP 还不够有用，我们现在可以对现实世界的对象建模，包括它们的层次结构。我们可以通过子类化/扩展/继承其他类来实现这一点。\n\n我们在这里可能要学习的术语是，从扩展而来的类是**超类**，从超类继承而来的类是**子类**。我们也可以说**家长**和**孩子**班。\n\n小费\n\n你可能会问自己这个关于继承的问题:为什么？原因大概是这样的:我们可以编写一次通用代码；在父类中，我们可以更新公共代码，并且从它继承的所有类也被更新。此外，子类只能使用公共的和**受保护的**实例变量和函数。因此，如果设计得当，这也增强了封装的目标。\n\n你是说受保护吗？是的。有一个类变量和函数的访问说明符叫做**保护**。您可以将受保护变量视为介于公共变量和私有变量之间。下面是访问说明符的快速总结，以及关于`protected`说明符的更多细节:\n\n*   `Public`任何拥有该类实例的人都可以访问和使用变量和函数。\n*   `Private`变量和函数只能由类的内部代码访问/使用，不能直接从实例中访问/使用。这对于封装以及当我们需要访问/更改私有变量时是有好处的，因为我们可以提供公共的 getter 和 setter 函数(比如`getSprite`)。如果我们扩展一个有`private`变量和函数的类，子类**不能**直接访问其父类的私有数据。\n*   `Protected`变量和函数几乎和 private 一样。类的实例不能直接访问/使用它们。然而，它们**可以被任何扩展了它们所声明的类的类直接使用。所以，这就像他们是私人的，除了给孩子上课。**\n\n **为了充分理解什么是受保护的变量和函数，以及它们如何有用，让我们先看另一个主题。然后，我们会看到他们在行动。\n\n# 多态性\n\n**多态性**允许我们编写更少依赖于我们试图操作的类型的代码。这可以使我们的代码更加清晰和高效。多态性意味着多种形式。如果我们编码的对象可以是多种类型的东西，那么我们可以利用这一点。\n\n重要说明\n\n但是多态性对我们意味着什么呢？归结为最简单的定义，多态性意味着:任何子类都可以被用作使用超类的代码的一部分。这意味着我们可以编写更简单、更容易理解、也更容易修改或更改的代码。此外，我们可以为超类编写代码，并依赖于这样一个事实，即无论它被子类化多少次，在某些参数内，代码仍然会工作。\n\n我们来讨论一个例子。\n\n假设我们想使用多态性来帮助编写一个动物园管理游戏，在这个游戏中，我们必须喂养和照顾动物的需求。我们可能会想要一个像`feed`这样的功能。我们可能还想把要喂养的动物的一个实例传递给`feed`函数。\n\n当然，动物园里有很多动物，比如狮子、大象和三趾树懒。有了我们对 C++ 继承的新知识，编写一个`Animal`类并让所有不同类型的动物继承它是有意义的。\n\n如果我们想写一个函数(`feed`)，我们可以将`Lion`、`Elephant`和`ThreeToedSloth`作为参数传入，似乎我们需要为每种类型的`Animal`写一个`feed`函数。然而，我们可以用多态返回类型和参数编写多态函数。看看下面假设的 feed 函数的定义:\n\n```cpp\nvoid feed(Animal& a)\n{\n    a.decreaseHunger();\n}\n```\n\n前面的函数有一个`Animal`引用作为参数，这意味着从扩展`Animal`的类构建的任何对象都可以传递到其中。\n\n这意味着您可以在今天编写代码，并在一周、一个月或一年内创建另一个子类，并且相同的函数和数据结构仍然可以工作。此外，我们可以对子类强制执行一组规则，关于它们可以做什么和不可以做什么，以及它们如何做。所以，一个阶段的好设计可以影响到其他阶段。\n\n但是我们真的会想要实例化一个真实的动物吗？\n\n# 抽象类——虚拟和纯虚拟函数\n\n**抽象类**是一个不能实例化的类，因此不能成为对象。\n\n小费\n\n这里我们可能想了解的一些术语是*具体*类。一个**具体类**是任何不是抽象的类。换句话说，到目前为止，我们编写的所有类都是具体的类，可以实例化为可用的对象。\n\n那么，这是永远不会被使用的代码？但这就像花钱请建筑师来设计你的房子，然后永远不要建造它！\n\n如果我们，或者一个类的设计者，想强制它的用户在使用他们的类之前继承它，他们可以让一个类**抽象**。如果发生这种情况，我们就不能用它制造一个物体；因此，我们必须首先从它继承，并从子类中创建一个对象。\n\n为此，我们可以制作一个函数**纯虚**，不提供任何定义。然后，该函数必须在继承它的任何类中被**覆盖** ( **重写**)。\n\n我们来看一个例子；会有帮助的。我们可以通过添加抽象的`Animal`类这样的纯虚函数来做一个类抽象，只能执行`makeNoise`的泛型动作:\n\n```cpp\nClass Animal\n    private:\n        // Private stuff here\n    public:\n        void virtual makeNoise() = 0;\n        // More public stuff here\n};\n```\n\n如您所见，我们在函数声明之前添加了 C++ 关键字`virtual,` ，之后添加了`= 0`。现在，任何从`Animal`扩展/继承的类都必须覆盖`makeNoise` 函数。这可能是有道理的，因为不同类型的动物会发出非常不同的噪音。我们可以假设任何扩展`Animal`类的人都足够聪明，能够注意到`Animal`类不能发出声音，他们需要处理它，但是如果他们没有注意到呢？关键是通过制造一个纯虚函数，我们保证他们会，因为他们必须。\n\n抽象类也很有用，因为有时候，我们想要一个可以用作多态类型的类，但是我们需要保证它永远不能用作对象。例如，`Animal`本身并没有真正的意义。我们不谈动物；我们谈论动物的种类。我们不会说，“哦，看看那个可爱的，毛茸茸的，白色的动物！”，或者，“昨天我们去宠物店买了一只动物和一张动物床。”太抽象了。\n\n因此，抽象类有点像**模板**，任何扩展它的类都可以使用它(从它继承)。例如，如果我们正在构建一个玩家管理企业及其员工的*工业帝国*类型游戏，我们可能想要一个`Worker`类，并将其扩展为`Miner`、`Steelworker`、`OfficeWorker`，当然还有`Programmer`。但是普通的`Worker`到底是做什么的呢？为什么我们要实例化一个呢？\n\n答案是，我们不想实例化一个，但我们可能想将其用作多态类型，这样我们就可以在函数之间传递多个`Worker`子类，并拥有可以容纳所有类型工作者的数据结构。\n\n所有纯虚函数都必须被扩展包含纯虚函数的父类的任何类覆盖。这意味着抽象类可以提供一些在其所有子类中都可用的通用功能。例如，`Worker`类可能有`m_AnnualSalary`、`m_Productivity`和`m_Age`成员变量。它也可能有`getPayCheck`函数，它不是纯虚的，在所有子类中都是一样的，但是有一个`doWork`函数，它是纯虚的，必须被覆盖，因为所有不同类型的`Worker`将会有很大的不同。\n\n重要说明\n\n顺便说一下，**虚拟**相对于纯虚拟是一个可以被**随意覆盖的功能**。您可以用与纯虚函数相同的方式声明虚函数，但将`= 0`保留到最后。在目前的游戏项目中，我们将使用一个纯虚拟的功能。\n\n如果这些虚拟的、纯虚拟的或抽象的东西不清楚，使用它可能是理解它的最好方法。\n\n# 构建可玩角色类\n\n既然我们已经知道了继承、多态和纯虚函数的基础，我们将使用它们。我们将建立一个`PlayableCharacter`类，它拥有我们游戏中任何角色都需要的大部分功能。它将有一个纯虚拟功能，称为`handleInput`。`handleInput`函数需要在子类中有很大的不同，所以这是有意义的。\n\n由于`PlayableCharacter`将有一个纯虚函数，它将是一个抽象类，它的任何对象都是不可能的。然后我们将构建`Thomas`和`Bob`类，它们将从`PlayableCharacter`继承，实现纯虚拟函数的定义，并允许我们在游戏中实例化`Bob`和`Thomas`对象。直接实例化`PlayableCharacter`实例是不可能的，但是我们不想，因为它太抽象了。\n\n## 编码可播放字符\n\n像创建类时一样，我们将从包含成员变量和函数声明的头文件开始。新的是，在这个类中，我们将声明一些**受保护的**成员变量。请记住，在从具有受保护变量的类继承的类中，可以像使用公共变量一样使用受保护变量。\n\n右键单击**解决方案资源管理器**中的**头文件**，选择**添加|新项目...**。在**添加新项目**窗口中，突出显示(通过左键单击)**头文件(。h)** 然后在**名称**字段中，输入`PlayableCharacter.h`。最后，点击**添加**按钮。我们现在准备为`PlayableCharacter`类编码头文件。\n\n我们将分三节添加和讨论`PlayableCharacter.h`文件的内容。首先是`protected`段，然后是`private`，然后是`public`。\n\n将以下代码添加到`PlayableCharacter.h`文件中:\n\n```cpp\n#pragma once\n#include <SFML/Graphics.hpp>\nusing namespace sf;\nclass PlayableCharacter\n{\nprotected:\n    // Of course we will need a sprite\n    Sprite m_Sprite;\n    // How long does a jump last\n    float m_JumpDuration;\n    // Is character currently jumping or falling\n    bool m_IsJumping;\n    bool m_IsFalling;\n    // Which directions is the character currently moving in\n    bool m_LeftPressed;\n    bool m_RightPressed;\n    // How long has this jump lasted so far\n    float m_TimeThisJump;\n    // Has the player just initiated a jump\n    bool m_JustJumped = false;\n    // Private variables and functions come next\n```\n\n在我们刚刚写的代码中首先要注意的是所有的变量都是`protected`。这意味着，当我们从类继承时，我们刚刚编写的所有变量都可以被扩展它的类访问。我们将通过`Thomas`和`Bob`课程扩展该课程。\n\n小费\n\n术语*从*继承而来*延伸*在本书的大多数上下文中几乎是同义词。然而，有时一个似乎比另一个更合适。\n\n除了`protected`访问规范，之前的代码没有什么新的或者复杂的。然而，值得注意的是一些细节。如果我们这样做了，随着我们的进步，就很容易理解这门课是如何运作的。所以，让我们一次一个地来分析这些`protected`变量。\n\n我们有一些可预测的`Sprite`、`m_Sprite`。我们有一个名为`m_JumpDuration`的`float`变量，它将保存一个代表角色能够跳跃的时间的值。值越大，角色能够跳得越远/越高。\n\n接下来，我们有一个布尔`m_IsJumping`，当角色跳跃时是`true`，否则是`false`。这将有助于确保角色在半空中不能跳跃。\n\n`m_IsFalling`变量的用法与`m_IsJumping`相似。它将被用来知道一个角色何时倒下。\n\n接下来，我们有两个布尔值，如果字符的左或右键盘按钮当前被按下，则这两个布尔值将为真。这些相对取决于角色( *A* 和 *D* 分别代表托马斯，*左*和*右*箭头键分别代表鲍勃)。我们如何回应这些布尔人将在`Thomas`和`Bob`课程中看到。\n\n`m_TimeThisJump`浮动变量在`m_IsJumping`为`true`的每一帧更新。然后我们可以找出`m_JumpDuration`何时到达。\n\n最后的`protected`变量是`m_JustJumped`布尔值。如果在当前帧中开始跳转，这将是`true`。它将被用来让我们知道什么时候播放跳跃音效。\n\n接下来，将以下`private`变量添加到`PlayableCharacter.h`文件中:\n\n```cpp\nprivate:\n    // What is the gravity\n    float m_Gravity;\n    // How fast is the character\n    float m_Speed = 400;\n    // Where is the player\n    Vector2f m_Position;\n    // Where are the characters various body parts?\n    FloatRect m_Feet;\n    FloatRect m_Head;\n    FloatRect m_Right;\n    FloatRect m_Left;\n    // And a texture\n    Texture m_Texture;\n    // All our public functions will come next\n```\n\n在前面的代码中，我们有一些有趣的`private`变量。请记住，这些变量只能由`PlayableCharacter`类中的代码直接访问。`Thomas`和`Bob`类将无法直接访问它们。\n\n`m_Gravity`变量将保存字符每秒下降的像素数。`m_Speed`变量将保存字符每秒可以向左或向右移动的像素数。\n\n`Vector2f`、`m_Position`变量是世界上(不是屏幕上)角色中心所在的位置。\n\n接下来要讨论的四个`FloatRect`对象很重要。当我们在*僵尸竞技场*游戏中进行碰撞检测时，我们只是检查两个`FloatRect`物体是否相交。每个`FloatRect`物体代表一个完整的角色、一辆皮卡或一颗子弹。对于非矩形物体(僵尸和玩家)，这有点不准确。\n\n在这场比赛中，我们需要更加精确。`m_Feet`、`m_Head`、`m_Right`、`m_Left`和`FloatRect`对象将保存角色身体不同部分的坐标。这些坐标将在每帧更新。\n\n通过这些坐标，我们将能够准确地判断一个角色何时降落在平台上，何时在跳跃过程中撞到他们的头，何时用瓷砖擦肩膀。\n\n最后，我们有一个`Texture`。`Texture`是`private`，因为它不被`Thomas`或`Bob`班级直接使用。但是我们看到`Sprite`是`protected`因为是直接使用的。\n\n现在，将所有`public`功能添加到`PlayableCharacter.h`文件中。然后，我们将讨论它们:\n\n```cpp\npublic:\n    void spawn(Vector2f startPosition, float gravity);\n    // This is a pure virtual function\n    bool virtual handleInput() = 0;\n    // This class is now abstract and cannot be instantiated\n    // Where is the player\n    FloatRect getPosition();\n    // A rectangle representing the position \n    // of different parts of the sprite\n    FloatRect getFeet();\n    FloatRect getHead();\n    FloatRect getRight();\n    FloatRect getLeft();\n    // Send a copy of the sprite to main\n    Sprite getSprite();\n    // Make the character stand firm\n    void stopFalling(float position);\n    void stopRight(float position);\n    void stopLeft(float position);\n    void stopJump();\n    // Where is the center of the character\n    Vector2f getCenter();\n    // We will call this function once every frame\n    void update(float elapsedTime);\n\n};// End of the class\n```\n\n让我们来谈谈我们刚刚添加的每个函数声明。这将使编码定义更容易理解:\n\n*   `spawn`函数接收一个名为`startPosition`的`Vector2f`和一个名为`gravity`的`float`值。顾名思义，`startPosition`将是角色开始时所在级别的坐标，`gravity`将是角色每秒倒下的像素数。\n*   `bool virtual handleInput() = 0`当然是我们的纯虚函数。由于`PlayableCharacter`有这个函数，任何扩展它的类，如果我们想实例化它，必须为这个函数提供一个定义。所以，当我们一分钟写完`PlayableCharacter`的所有函数定义时，我们不会提供`handleInput`的定义。在`Thomas`和`Bob`两个等级中都需要有定义。\n*   `getPosition`函数返回一个代表整个字符位置的`FloatRect`对象。\n*   `getFeet()`功能以及`getHead`、`getRight`和`getLeft`返回一个表示角色身体特定部位位置的`FloatRect`对象。这正是我们进行详细碰撞检测所需要的。\n*   像往常一样，`getSprite`函数向调用代码返回一份`m_Sprite`的副本。\n*   `stopFalling`、`stopRight`、`stopLeft`和`stopJump`函数接收单个`float`值，该函数将使用该值来重新定位角色，并阻止其在实心瓷砖中行走或跳跃。\n*   `getCenter`函数向调用代码返回一个`Vector2f`对象，让它知道字符的中心到底在哪里。该值保存在`m_Position`中。正如我们将在后面看到的，它被`Engine`类用来将适当的`View`集中在适当的角色周围。\n*   我们之前已经多次看到`update`函数，像往常一样，它采用一个`float`参数，这是当前帧所用的几分之一秒。然而，这个`update`功能将需要比之前的`update`功能(来自我们的其他项目)做更多的工作。它将需要处理跳跃以及更新`FloatRect`对象，这些对象代表角色的头部、脚部以及左侧和右侧。\n\n现在，我们可以为所有函数编写定义，当然除了`handleInput`。\n\n## 编码可播放字符\n\n右键单击**解决方案资源管理器**中的**源文件**，并选择**添加|新项目...**。在**添加新项目**窗口中，突出显示(通过左键单击) **C++ 文件(。cpp)** 然后，在**名称**字段中，键入`PlayableCharacter.cpp`。最后，点击**添加**按钮。我们现在准备为`PlayableCharacter`类编码`.cpp`文件。\n\n我们将分解代码并分成几大块讨论。首先，添加包含指令和`spawn`函数的定义:\n\n```cpp\n#include \"PlayableCharacter.h\"\nvoid PlayableCharacter::spawn(\n        Vector2f startPosition, float gravity)\n{\n    // Place the player at the starting point\n    m_Position.x = startPosition.x;\n    m_Position.y = startPosition.y;\n    // Initialize the gravity\n    m_Gravity = gravity;\n    // Move the sprite in to position\n    m_Sprite.setPosition(m_Position);\n}\n```\n\n`spawn`功能用传入的位置初始化`m_Position`，同时也初始化`m_Gravity`。最后一行代码将`m_Sprite`移动到其起始位置。\n\n接下来，在前一个代码之后立即添加`update`函数的定义:\n\n```cpp\nvoid PlayableCharacter::update(float elapsedTime)\n{\n    if (m_RightPressed)\n    {\n        m_Position.x += m_Speed * elapsedTime;\n    }\n    if (m_LeftPressed)\n    {\n        m_Position.x -= m_Speed * elapsedTime;\n    }\n    // Handle Jumping\n    if (m_IsJumping)\n    {\n        // Update how long the jump has been going\n        m_TimeThisJump += elapsedTime;\n        // Is the jump going upwards\n        if (m_TimeThisJump < m_JumpDuration)\n        {\n            // Move up at twice gravity\n            m_Position.y -= m_Gravity * 2 * elapsedTime;\n        }\n        else\n        {\n            m_IsJumping = false;\n            m_IsFalling = true;\n        }\n    }\n    // Apply gravity\n    if (m_IsFalling)\n    {\n        m_Position.y += m_Gravity * elapsedTime;\n    }\n    // Update the rect for all body parts\n    FloatRect r = getPosition();\n\n    // Feet\n    m_Feet.left = r.left + 3;\n    m_Feet.top = r.top + r.height - 1;\n    m_Feet.width = r.width - 6;\n    m_Feet.height = 1;\n    // Head\n    m_Head.left = r.left;\n    m_Head.top = r.top + (r.height * .3);\n    m_Head.width = r.width;\n    m_Head.height = 1;\n    // Right\n    m_Right.left = r.left + r.width - 2;\n    m_Right.top = r.top + r.height * .35;\n    m_Right.width = 1;\n    m_Right.height = r.height * .3;\n    // Left\n    m_Left.left = r.left;\n    m_Left.top = r.top + r.height * .5;\n    m_Left.width = 1;\n    m_Left.height = r.height * .3;\n    // Move the sprite into position\n    m_Sprite.setPosition(m_Position);\n}\n```\n\n代码的前两部分检查`m_RightPressed`或`m_LeftPressed`是否为`true`。如果两者都是，则`m_Position`使用与前一项目相同的公式(经过时间乘以速度)进行更改。\n\n接下来，我们看看角色当前是否正在执行跳转。我们从`if(m_IsJumping)`就知道这一点。如果这个`if`语句是`true`，这些是代码要采取的步骤:\n\n1.  用`elapsedTime`更新`m_TimeThisJump`。\n2.  检查`m_TimeThisJump`是否仍小于`m_JumpDuration`。如果是，则通过 2x 重力乘以经过的时间来改变`m_Position`的 y 坐标。\n3.  在`m_TimeThisJump`不低于`m_JumpDuration`时执行的`else`条款中，`m_Falling`设置为`true`。接下来将会看到这样做的效果。另外，`m_Jumping`设置为`false`。这阻止了我们刚刚讨论的代码执行，因为`if(m_IsJumping)`现在是假的。\n\n`if(m_IsFalling)`块向下移动`m_Position`每帧。使用`m_Gravity`的当前值和经过的时间移动它。\n\n接下来的代码(剩余代码的大部分)相对于整个精灵的当前位置更新了角色的“身体部分”。请看下图，了解代码如何计算角色的虚拟头、脚以及左侧和右侧的位置:\n\n![](img/B14278_15_01.jpg)\n\n最后一行代码使用`setPosition`功能，在完成`update`功能的所有可能性后，将精灵移动到正确的位置。\n\n现在，在前一个代码之后立即添加`getPosition`、`getCenter`、`getFeet`、`getHead`、`getLeft`、`getRight`和`getSprite`功能的定义:\n\n```cpp\nFloatRect PlayableCharacter::getPosition()\n{\n    return m_Sprite.getGlobalBounds();\n}\nVector2f PlayableCharacter::getCenter()\n{\n    return Vector2f(\n        m_Position.x + m_Sprite.getGlobalBounds().width / 2,\n        m_Position.y + m_Sprite.getGlobalBounds().height / 2\n        );\n}\nFloatRect PlayableCharacter::getFeet()\n{\n    return m_Feet;\n}\nFloatRect PlayableCharacter::getHead()\n{\n    return m_Head;\n}\nFloatRect PlayableCharacter::getLeft()\n{\n    return m_Left;\n}\nFloatRect PlayableCharacter::getRight()\n{\n    return m_Right;\n}\nSprite PlayableCharacter::getSprite()\n{\n    return m_Sprite;\n}\n```\n\n`getPosition`函数返回一个包裹整个精灵的`FloatRect`，而`getCenter`返回一个包含精灵中心的`Vector2f`。请注意，我们将精灵的高度和宽度除以 2，以便动态地得出这个结果。这是因为托马斯和鲍勃会有不同的高度。\n\n`getFeet`、`getHead`、`getLeft`和`getRight`函数返回`FloatRect`对象，这些对象代表我们在`update`函数中每帧更新的角色的身体部分。我们将在下一章中编写使用这些函数的冲突检测代码。\n\n`getSprite`功能像往常一样，返回`m_Sprite`的副本。\n\n最后，对于`PlayableCharacter`类，添加`stopFalling`、`stopRight`、`stopLeft`和`stopJump`函数的定义。在上一个代码之后立即执行此操作:\n\n```cpp\nvoid PlayableCharacter::stopFalling(float position)\n{\n    m_Position.y = position - getPosition().height;\n    m_Sprite.setPosition(m_Position);\n    m_IsFalling = false;\n}\nvoid PlayableCharacter::stopRight(float position)\n{\n\n    m_Position.x = position - m_Sprite.getGlobalBounds().width;\n    m_Sprite.setPosition(m_Position);\n}\nvoid PlayableCharacter::stopLeft(float position)\n{\n    m_Position.x = position + m_Sprite.getGlobalBounds().width;\n    m_Sprite.setPosition(m_Position);\n}\nvoid PlayableCharacter::stopJump()\n{\n    // Stop a jump early \n    m_IsJumping = false;\n    m_IsFalling = true;\n}\n```\n\n前面的每个函数都接收一个值作为参数，用于重新定位子画面的顶部、底部、左侧或右侧。确切的这些值是什么以及它们是如何获得的将是下一章的内容。前面的每个功能也重新定位了精灵。\n\n最后一个功能是`stopJump`功能，也将用于碰撞检测。它为`m_IsJumping`和`m_IsFalling`设置必要的值来结束跳跃。\n\n# 建立托马斯和鲍勃班\n\n现在，我们可以真正使用继承。我们将为托马斯建一个班，为鲍勃建一个班。它们都将继承我们刚刚编码的`PlayableCharacter`类。然后他们将拥有`PlayableCharacter`类的所有功能，包括直接访问其`protected`变量。我们还将添加纯虚函数`handleInput`的定义。您会注意到`Thomas`和`Bob`的`handleInput`功能会有所不同。\n\n## 编码托马斯\n\n右键单击**解决方案资源管理器**中的**头文件**，选择**添加|新项目...**。在**添加新项目**窗口中，突出显示(通过左键单击)**头文件(。h)** 然后在**名称**字段中，输入`Thomas.h`。最后，点击**添加**按钮。我们现在准备为`Thomas`类编码头文件。\n\n将以下代码添加到`Thomas.h`类中:\n\n```cpp\n#pragma once\n#include \"PlayableCharacter.h\"\nclass Thomas : public PlayableCharacter\n{\npublic:\n    // A constructor specific to Thomas\n    Thomas::Thomas();\n    // The overridden input handler for Thomas\n    bool virtual handleInput();\n};\n```\n\n前面的代码很短很甜。我们可以看到我们有一个构造函数，我们要实现纯虚拟的`handleInput`函数。所以，我们现在就开始吧。\n\n## 编码 Thomas.cpp\n\n右键单击**解决方案资源管理器**中的**源文件**，并选择**添加|新项目...**。在**添加新项目**窗口中，突出显示(通过左键单击) **C++ 文件(。cpp)** 然后，在**名称**字段中，键入`Thomas.cpp`。最后，点击**添加**按钮。我们现在准备为`Thomas`类编码`.cpp`文件。\n\n将`Thomas`构造函数添加到`Thomas.cpp`文件，如下所示:\n\n```cpp\n#include \"Thomas.h\"\n#include \"TextureHolder.h\"\nThomas::Thomas()\n{\n    // Associate a texture with the sprite\n    m_Sprite = Sprite(TextureHolder::GetTexture(\n        \"graphics/thomas.png\"));\n    m_JumpDuration = .45;\n}\n```\n\n我们所需要做的就是加载`thomas.png`图形并将跳转的持续时间(`m_JumpDuration`)设置为。`45` (将近半秒)。\n\n添加`handleInput`函数的定义如下:\n\n```cpp\n// A virtual function\nbool Thomas::handleInput()\n{\n    m_JustJumped = false;\n    if (Keyboard::isKeyPressed(Keyboard::W))\n    {\n        // Start a jump if not already jumping\n        // but only if standing on a block (not falling)\n        if (!m_IsJumping && !m_IsFalling)\n        {\n            m_IsJumping = true;\n            m_TimeThisJump = 0;\n            m_JustJumped = true;\n        }\n    }\n    else\n    {\n        m_IsJumping = false;\n        m_IsFalling = true;\n    }\n    if (Keyboard::isKeyPressed(Keyboard::A))\n    {\n        m_LeftPressed = true;\n    }\n    else\n    {\n        m_LeftPressed = false;\n    }\n    if (Keyboard::isKeyPressed(Keyboard::D))\n    {\n        m_RightPressed = true;\n    }\n    else\n    {\n        m_RightPressed = false;\n    }\n    return m_JustJumped;\n}\n```\n\n这段代码看起来应该很熟悉。我们正在使用 SFML `isKeyPressed`功能查看是否有任何 *W* 、 *A* 或 *D* 键被按下。\n\n当按下 *W* 时，玩家试图跳跃。然后，该代码使用`if(!m_IsJumping && !m_IsFalling)`代码来检查字符是否已经跳跃，也没有下落。当这些测试都为真时，`m_IsJumping`设置为`true`，`m_TimeThisJump`设置为 0，`m_JustJumped`设置为`true`。\n\n前两次测试评估不到`true`时，执行`else`子句，`m_Jumping`设置为`false`，`m_IsFalling`设置为真。\n\n处理如何按下 *A* 和 *D* 键，就像将`m_LeftPressed`和/或`m_RightPressed`设置为`true`或 `false`一样简单。`update`功能现在可以处理移动角色。\n\n函数的最后一行代码返回`m_JustJumped`的值。这将让调用代码知道它是否需要播放跳跃音效。\n\n我们现在将对`Bob`类进行编码。它几乎和`Thomas`类一模一样，只是跳跃能力不同和`Texture`不同，键盘上使用的按键也不同。\n\n## 编码鲍勃\n\n`Bob`类在结构上与`Thomas`类相同。它继承了`PlayableCharacter`，它有一个构造函数，它提供了`handleInput`函数的定义。与`Thomas`的不同之处在于，我们初始化鲍勃的一些成员变量的方式不同，我们处理输入(在`handleInput`函数中)的方式也不同。让我们对类进行编码，并查看细节。\n\n右键单击**解决方案资源管理器**中的**头文件**，选择**添加|新项目...**。在**添加新项目**窗口中，突出显示(通过左键单击)**头文件(。h)** 然后在**名称**字段中，输入`Bob.h`。最后，点击**添加**按钮。我们现在准备为`Bob`类编码头文件。\n\n将以下代码添加到`Bob.h`文件中:\n\n```cpp\n#pragma once\n#include \"PlayableCharacter.h\"\nclass Bob : public PlayableCharacter\n{\npublic:\n    // A constructor specific to Bob\n    Bob::Bob();\n    // The overriden input handler for Bob\n    bool virtual handleInput();\n};\n```\n\n除了类名和构造函数名之外，前面的代码与`Thomas.h`文件相同。\n\n## 编码 Bob.cpp\n\n右键单击**解决方案资源管理器**中的**源文件**，并选择**添加|新项目...**。在**添加新项目**窗口中，突出显示(通过左键单击) **C++ 文件(。cpp)** 然后，在**名称**字段中，键入`Bob.cpp`。最后，点击**添加**按钮。我们现在准备为`Bob`类编码`.cpp`文件。\n\n将`Bob`构造函数的以下代码添加到`Bob.cpp`文件中。请注意，纹理是不同的(`bob.png`),并且`m_JumpDuration`被初始化为一个明显更小的值。鲍勃现在是他自己独特的自己:\n\n```cpp\n#include \"Bob.h\"\n#include \"TextureHolder.h\"\nBob::Bob()\n{\n    // Associate a texture with the sprite\n    m_Sprite = Sprite(TextureHolder::GetTexture(\n        \"graphics/bob.png\"));\n    m_JumpDuration = .25;\n}\n```\n\n在`Bob`构造函数后立即添加`handleInput`代码:\n\n```cpp\nbool Bob::handleInput()\n{\n    m_JustJumped = false;\n    if (Keyboard::isKeyPressed(Keyboard::Up))\n    {\n        // Start a jump if not already jumping\n        // but only if standing on a block (not falling)\n        if (!m_IsJumping && !m_IsFalling)\n        {\n            m_IsJumping = true;\n            m_TimeThisJump = 0;\n            m_JustJumped = true;\n        }\n    }\n    else\n    {\n        m_IsJumping = false;\n        m_IsFalling = true;\n    }\n    if (Keyboard::isKeyPressed(Keyboard::Left))\n    {\n        m_LeftPressed = true;\n    }\n    else\n    {\n        m_LeftPressed = false;\n    }\n    if (Keyboard::isKeyPressed(Keyboard::Right))\n    {\n        m_RightPressed = true;;\n    }\n    else\n    {\n        m_RightPressed = false;\n    }\n    return m_JustJumped;\n}\n```\n\n请注意，该代码与`Thomas`类的`handleInput`函数中的代码几乎相同。唯一不同的是，我们对不同的键(分别是左右移动的*左*箭头键和*右*箭头键，跳跃的*上*箭头键)做出反应。\n\n现在我们有了一个被`Bob`和`Thomas`类扩展的`PlayableCharacter`类，我们可以在游戏中添加一个`Bob`和一个`Thomas`实例。\n\n# 更新游戏引擎使用托马斯和鲍勃\n\n为了能够运行游戏并看到我们的新角色，我们必须声明它们的实例，调用它们的`spawn`函数，每帧更新它们，每帧绘制它们。我们现在就开始吧。\n\n## 更新 Engine.h 以添加鲍勃和托马斯的实例\n\n打开`Engine.h`文件，添加以下高亮显示的代码行:\n\n```cpp\n#pragma once\n#include <SFML/Graphics.hpp>\n#include \"TextureHolder.h\"\n#include \"Thomas.h\"\n#include \"Bob.h\"\nusing namespace sf;\nclass Engine\n{\nprivate:\n    // The texture holder\n    TextureHolder th;\n    // Thomas and his friend, Bob\n    Thomas m_Thomas;\n    Bob m_Bob;\n    const int TILE_SIZE = 50;\n    const int VERTS_IN_QUAD = 4;\n    ...\n    ...\n```\n\n现在，我们有一个源自`PlayableCharacter`的`Thomas`和`Bob`的实例。\n\n## 更新输入功能以控制托马斯和鲍勃\n\n现在，我们将添加控制这两个角色的能力。该代码将进入代码的输入部分。当然，对于这个项目，我们有专门的`input`功能。打开`Input.cpp`并添加以下高亮显示的代码:\n\n```cpp\nvoid Engine::input()\n{\n    Event event;\n    while (m_Window.pollEvent(event))\n    {\n        if (event.type == Event::KeyPressed)\n        {\n            // Handle the player quitting\n            if (Keyboard::isKeyPressed(Keyboard::Escape))\n            {\n                m_Window.close();\n            }\n            // Handle the player starting the game\n            if (Keyboard::isKeyPressed(Keyboard::Return))\n            {\n                m_Playing = true;\n            }\n            // Switch between Thomas and Bob\n            if (Keyboard::isKeyPressed(Keyboard::Q))\n            {\n                m_Character1 = !m_Character1;\n            }\n            // Switch between full and split-screen\n            if (Keyboard::isKeyPressed(Keyboard::E))\n            {\n                m_SplitScreen = !m_SplitScreen;\n            }\n        }\n    }\n    // Handle input specific to Thomas\n    if(m_Thomas.handleInput())\n    {\n        // Play a jump sound\n    }\n    // Handle input specific to Bob\n    if(m_Bob.handleInput())\n    {\n        // Play a jump sound\n    }\n}\n```\n\n注意前面的代码有多简单:所有的功能都包含在`Thomas`和`Bob`类中。所有代码必须做的就是为每个`Thomas`和`Bob`类添加一个 include 指令。然后，在`input`函数内，代码只调用`m_Thomas`和`m_Bob`上的纯虚`handleInput`函数。我们将每个调用包装在`if`语句中的原因是，它们会根据新的跳转是否刚刚成功启动而返回`true`或`false`。我们将在 [*第 17 章*](17.html#_idTextAnchor340)*声音空间化和 HUD* 中处理跳跃声音效果的播放。\n\n## 更新更新功能以产生和更新可播放角色实例\n\n这分为两部分。首先，我们需要在一个新的级别开始时产生鲍勃和托马斯，其次，我们需要每帧更新他们(通过调用他们的`update`函数)。\n\n### 催生托马斯和鲍勃\n\n随着项目的进展，我们需要在几个不同的地方调用我们的`Thomas`和`Bob`对象的`spawn`函数。最明显的是，我们需要在新的关卡开始时衍生出这两个角色。在下一章中，随着我们在一个关卡开始时需要执行的任务数量的增加，我们将编写一个`loadLevel`函数。现在，让我们在`update`功能中调用`m_Thomas`和`m_Bob`上的`spawn`，如下面的代码所示。添加以下代码，但请记住，它最终会被删除和替换:\n\n```cpp\nvoid Engine::update(float dtAsSeconds)\n{\n    if (m_NewLevelRequired)\n    {\n        // These calls to spawn will be moved to a new\n        // loadLevel() function soon\n        // Spawn Thomas and Bob\n        m_Thomas.spawn(Vector2f(0,0), GRAVITY);\n        m_Bob.spawn(Vector2f(100, 0), GRAVITY);\n        // Make sure spawn is called only once\n        m_TimeRemaining = 10;\n        m_NewLevelRequired = false;\n    }\n    if (m_Playing)\n    {\n        // Count down the time the player has left\n        m_TimeRemaining -= dtAsSeconds;\n        // Have Thomas and Bob run out of time?\n        if (m_TimeRemaining <= 0)\n        {\n            m_NewLevelRequired = true;\n        }\n    }// End if playing\n\n}\n```\n\n之前的代码只是简单的调用`spawn`并和重力一起通过游戏世界中的一个位置。代码被包装在`if`语句中，该语句检查是否需要新的级别。产卵代码将被移动到专用的`loadLevel`功能，但是`if`条件将是已完成项目的一部分。另外，`m_TimeRemaining`现在被设置为任意 10 秒。\n\n现在，我们可以在游戏循环的每一帧更新实例。\n\n### 每帧更新托马斯和鲍勃\n\n接下来，我们将更新托马斯和鲍勃。我们所需要做的就是调用它们的`update`函数，并传递这个帧所花费的时间。\n\n添加以下突出显示的代码:\n\n```cpp\nvoid Engine::update(float dtAsSeconds)\n{\n    if (m_NewLevelRequired)\n    {\n        // These calls to spawn will be moved to a new\n        // LoadLevel function soon\n        // Spawn Thomas and Bob\n        m_Thomas.spawn(Vector2f(0,0), GRAVITY);\n        m_Bob.spawn(Vector2f(100, 0), GRAVITY);\n        // Make sure spawn is called only once\n        m_NewLevelRequired = false;\n    }\n    if (m_Playing)\n    {\n        // Update Thomas\n        m_Thomas.update(dtAsSeconds);\n        // Update Bob\n        m_Bob.update(dtAsSeconds);\n        // Count down the time the player has left\n        m_TimeRemaining -= dtAsSeconds;\n        // Have Thomas and Bob run out of time?\n        if (m_TimeRemaining <= 0)\n        {\n            m_NewLevelRequired = true;\n        }\n    }// End if playing\n\n}\n```\n\n现在角色可以移动了，我们需要更新合适的`View`对象，以角色为中心，让他们成为关注的中心。当然，在我们的游戏世界中没有一些物体之前，实际运动的感觉是不会达到的。\n\n添加以下突出显示的代码:\n\n```cpp\nvoid Engine::update(float dtAsSeconds)\n{\n    if (m_NewLevelRequired)\n    {\n        // These calls to spawn will be moved to a new\n        // LoadLevel function soon\n        // Spawn Thomas and Bob\n        m_Thomas.spawn(Vector2f(0,0), GRAVITY);\n        m_Bob.spawn(Vector2f(100, 0), GRAVITY);\n        // Make sure spawn is called only once\n        m_NewLevelRequired = false;\n    }\n    if (m_Playing)\n    {\n        // Update Thomas\n        m_Thomas.update(dtAsSeconds);\n        // Update Bob\n        m_Bob.update(dtAsSeconds);\n        // Count down the time the player has left\n        m_TimeRemaining -= dtAsSeconds;\n        // Have Thomas and Bob run out of time?\n        if (m_TimeRemaining <= 0)\n        {\n            m_NewLevelRequired = true;\n        }\n    }// End if playing\n\n    // Set the appropriate view around the appropriate character\n    if (m_SplitScreen)\n    {\n        m_LeftView.setCenter(m_Thomas.getCenter());\n        m_RightView.setCenter(m_Bob.getCenter());\n    }\n    else\n    {\n        // Centre full screen around appropriate character\n        if (m_Character1)\n        {\n            m_MainView.setCenter(m_Thomas.getCenter());\n        }\n        else\n        {\n            m_MainView.setCenter(m_Bob.getCenter());\n        }\n    }\n}\n```\n\n前面的代码处理了两种可能的情况。首先，`if(mSplitScreen)`条件将左侧视图定位在`m_Thomas`周围，右侧视图定位在`m_Bob`周围。当游戏处于全屏模式时执行的`else`子句测试`m_Character1`是否为`true`。如果是，则全屏视图(`m_MainView`)以托马斯为中心，否则以鲍勃为中心。你可能还记得，玩家可以用 *E* 键切换分屏模式，用 *Q* 键切换全屏模式下的鲍勃和托马斯。我们在`Engine`类的`input`功能中对此进行了编码，回到 [*第 12 章*](12.html#_idTextAnchor272)*分层视图和实现 HUD* 。\n\n现在，我们可以在屏幕上绘制托马斯和鲍勃的图形。\n\n## 画鲍勃和托马斯\n\n确保`Draw.cpp`文件已打开，并添加以下高亮显示的代码:\n\n```cpp\nvoid Engine::draw()\n{\n    // Rub out the last frame\n    m_Window.clear(Color::White);\n    if (!m_SplitScreen)\n    {\n        // Switch to background view\n        m_Window.setView(m_BGMainView);\n        // Draw the background\n        m_Window.draw(m_BackgroundSprite);\n        // Switch to m_MainView\n        m_Window.setView(m_MainView);        \n        // Draw thomas\n        m_Window.draw(m_Thomas.getSprite());\n        // Draw bob\n        m_Window.draw(m_Bob.getSprite());\n    }\n    else\n    {\n        // Split-screen view is active\n        // First draw Thomas' side of the screen\n        // Switch to background view\n        m_Window.setView(m_BGLeftView);\n        // Draw the background\n        m_Window.draw(m_BackgroundSprite);\n        // Switch to m_LeftView\n        m_Window.setView(m_LeftView);\n        // Draw bob\n        m_Window.draw(m_Bob.getSprite());\n\n        // Draw thomas\n        m_Window.draw(m_Thomas.getSprite());\n\n        // Now draw Bob's side of the screen\n        // Switch to background view\n        m_Window.setView(m_BGRightView);\n        // Draw the background\n        m_Window.draw(m_BackgroundSprite);\n        // Switch to m_RightView\n        m_Window.setView(m_RightView);\n        // Draw thomas\n        m_Window.draw(m_Thomas.getSprite());\n        // Draw bob\n        m_Window.draw(m_Bob.getSprite());\n\n    }\n    // Draw the HUD\n    // Switch to m_HudView\n    m_Window.setView(m_HudView);\n\n    // Show everything we have just drawn\n    m_Window.display();\n}\n```\n\n请注意，我们为全屏幕、左和右绘制了托马斯和鲍勃。此外，请注意我们在分屏模式下绘制字符的方式的细微差别。在绘制屏幕左侧时，我们会切换绘制字符的顺序，并在鲍勃之后绘制托马斯。所以，托马斯永远在左边“顶上”，鲍勃永远在右边顶上。这是因为控制托马斯的玩家分别在左边和右边照顾鲍勃。\n\n您现在可以运行游戏，并在屏幕中央看到托马斯和鲍勃，如下所示:\n\n![](img/B14278_15_01b.jpg)\n\n如果您按下 *Q* 键将焦点从托马斯切换到鲍勃，您将看到`View`进行轻微调整。如果你向左或向右移动角色(托马斯用 *A* 和 *D* ，鲍勃用箭头键)，你会看到他们相对于彼此移动。\n\n试着按下 *E* 键在全屏和分屏之间切换。然后，再次尝试移动两个角色以查看效果。在下面的截图中，您可以看到 Thomas 总是位于左侧窗口的中心，Bob 总是位于右侧窗口的中心:\n\n![](img/B14278_15_02.jpg)\n\n如果你让游戏运行足够长的时间，角色会每 10 秒钟在原来的位置重生一次。这是我们完成游戏所需功能的开始。该行为是由`m_TimeRemaining`低于 0，然后将`m_NewLevelRequired`变量设置为`true`引起的。\n\n还要注意的是，在我们画出关卡的细节之前，我们无法看到运动的全部效果。事实上，虽然看不到，但两个字符都在以每秒 300 像素的速度连续下降。由于相机每一帧都以它们为中心，并且游戏世界中没有其他物体，因此我们无法看到这种向下的移动。\n\n如果你想自己看这个，就把通话改到`m_Bob.spawn`，如下:\n\n```cpp\nm_Bob.spawn(Vector2f(0,0), 0);\n```\n\n既然鲍勃没有引力效应，托马斯显然会离他而去。这显示在下面的截图中:\n\n![](img/B14278_15_03.jpg)\n\n我们会在下一章增加一些可玩的关卡进行互动。\n\n# 总结\n\n在本章中，我们学习了一些新的 C++ 概念，例如继承，它允许我们扩展一个类并获得它的所有功能。我们还了解到，我们可以将变量声明为受保护的，这将使子类能够访问它们，但是它们仍然会被封装(隐藏)在所有其他代码中。我们还使用了纯虚函数，这使得类变得抽象，这意味着类不能被实例化，因此必须从/扩展继承。我们也被介绍了多态性的概念，但是需要等到下一章才能在我们的游戏中使用它。\n\n在下一章中，我们将为游戏添加一些主要功能。在下一章的结尾，托马斯和鲍勃将会行走、跳跃和下落。他们甚至能够跳到对方的头上，并探索从文本文件中加载的一些关卡设计。\n\n# 常见问题\n\nq)我们了解了多态，但是为什么到目前为止我没有注意到游戏代码中有任何多态的地方？\n\na)当我们编写一个以`PlayerCharacter`为参数的函数时，我们会在下一章看到多态性在起作用。我们将看看如何将鲍勃和托马斯传递给这个新函数。对他们俩来说都一样。**"
  },
  {
    "path": "docs/begin-cpp-game-prog/16.md",
    "content": "# 十六、建造可玩关卡和碰撞检测\n\n这一章可能是这个项目最令人满意的章节之一。这样做的原因是，到最后，我们将有一个可玩的游戏。尽管仍有一些功能需要实现(声音、粒子效果、平视显示器和着色器效果)，鲍勃和托马斯将能够奔跑、跳跃和探索世界。此外，你将能够通过简单地在文本文件中制作平台和障碍物来创建任何大小或复杂程度的自己的关卡设计。\n\n我们将通过涵盖以下主题来实现这一切:\n\n*   探索如何设计文本文件中的级别\n*   建立一个`LevelManager`类，它将从文本文件中加载等级，将它们转换成我们的游戏可以使用的数据，并跟踪等级细节，如产卵位置、当前等级和允许的时间限制\n*   更新游戏引擎使用`LevelManager`\n*   编写多态函数来处理鲍勃和托马斯的冲突检测\n\n# 设计一些关卡\n\n还记得我们在 [*第 14 章*](14.html#_idTextAnchor292)*抽象和代码管理——更好地利用 OOP* 中介绍的精灵表吗？又来了，用数字标注，代表我们将从中构建所有级别的每个图块:\n\n![](img/B14278_16_01.jpg)\n\n图像被放置在灰色背景上，这样我们可以更好地看到精灵表的不同细节。方格背景代表透明度。所以，除了 1 号以外，所有的瓷砖都会露出它们背后的至少一点背景。我们现在来复习一下:\n\n*   图块 0 是完全透明的，将用于填充没有任何其他图块的间隙。\n*   瓷砖 1 是托马斯和鲍勃将要走过的平台。\n*   瓷砖 2 是防火瓷砖，3 是防水瓷砖。\n*   就图块 4 而言，您可能需要仔细查看才能看到它。它有一个白色的方形轮廓。这是关卡的目标，也是托马斯和鲍勃必须走到一起的地方。\n\n在我们讨论设计关卡时，请记住这张图片。\n\n我们将在文本文件中输入这些图块编号的组合来设计布局。一个例子会有帮助:\n\n```cpp\n0000000000000000000000000000000000000000000000\n0000000000000000000000000000000000000000000000\n0000000000000000000000000000000000000000000000\n0000000000000000000000000000000000000000000000\n0000000000000000000000000000000000000000000000\n0000000000000000000000000000000000000000000000\n1111111111000111111222222221111133111111111411\n0000000000000000001222222221000133100000001110\n0000000000000000001222222221000133100000000000\n0000000000000000001222222221000133100000000000\n0000000000000000001111111111000111100000000000\n```\n\n前面的代码转换为以下级别布局:\n\n![](img/B14278_16_02.jpg)\n\n请注意，为了获得上一张截图，我必须缩小`View`，并且图像已经被裁剪。该级别的实际开始如下所示:\n\n![](img/B14278_16_03.jpg)\n\n这些截图展示了两件事。首先，您可以看到如何使用简单自由的文本编辑器(如 Windows 记事本或 Notepad ++)快速构建关卡设计。只要确保你使用等间距字体，这样所有的数字都是相同的大小。这使得设计关卡变得更加容易。\n\n其次，这些截图展示了设计的游戏性。在关卡中从左到右，托马斯和鲍勃需要跳过一个小洞，否则他们会摔死(重生)。然后，他们有一大片火要穿越。鲍勃不可能跳过那么多瓷砖。玩家需要共同努力找到解决方案。鲍勃清除火砖的唯一方法是站在托马斯的头上，然后从那里跳下去，如下图所示:\n\n![](img/B14278_16_04.jpg)\n\n然后很容易达到目标，进入下一个阶段。\n\n小费\n\n我强烈建议你完成这一章，然后花一些时间设计你自己的水平。\n\n为了让我们开始，我加入了一些关卡设计。它们在我们在 [*第 14 章*](14.html#_idTextAnchor292)*抽象和代码管理-更好地利用 OOP* 中添加到项目中的`levels`文件夹中。\n\n那里有一些游戏的缩小视图，以及关卡设计的代码截图。代码的截图可能比复制文本内容更有用。如果需要检查代码，只需打开`levels`文件夹中的文件。\n\n代码是这样的:\n\n![](img/B14278_16_05.jpg)\n\n这是前面的代码将生成的级别布局:\n\n![](img/B14278_16_05b.jpg)\n\n这一级是我在 [*第十四章*](14.html#_idTextAnchor292)*抽象与代码管理——更好地利用 OOP* 中提到的“信念飞跃”级:\n\n![](img/B14278_16_06.jpg)\n\n游戏内平台的代码已经突出显示，因为它们在下面的缩小截图中不是很清楚:\n\n![](img/B14278_16_06b.jpg)\n\n提供的设计很简单。游戏引擎将能够处理非常大的设计，但我们有自由使用我们的想象力，并建立一些长期和具有挑战性的水平。\n\n当然，这些设计不会做任何事情，直到我们学会如何加载它们，并将文本转换成可玩的级别。此外，在我们实现碰撞检测之前，不可能站在任何平台上。\n\n首先，让我们处理加载关卡设计。\n\n# 构建级别管理器类\n\n这将需要几个阶段的编码，使我们的水平设计工作。\n\n我们首先要做的是对`LevelManager`头文件进行编码。这将允许我们查看和讨论`LevelManager`类中的成员变量和函数。\n\n接下来，我们将对`LevelManager.cpp`文件进行编码，其中将包含所有的函数定义。由于这是一个长文件，我们将把它分成几个部分来编码和讨论它们。\n\n一旦`LevelManager`类完成，我们将把它的一个实例添加到游戏引擎(`Engine`类)。我们还将在`Engine`类`loadLevel`中添加一个新的函数，每当需要一个新的级别时，我们都可以从`update`函数中调用这个函数。`loadLevel`功能将不仅使用`LevelManager`实例来加载适当的级别，它还将处理诸如生成玩家角色和准备时钟等方面。\n\n现在，让我们通过编码`LevelManager.h`文件来了解一下`LevelManager`的概况。\n\n## 编码级别管理器\n\n右键单击**解决方案资源管理器**中的**头文件**，选择**添加|新项目...**。在**添加新项目**窗口中，突出显示(通过左键单击)**头文件(。h)** 然后在**名称**字段中，输入`LevelManager.h`。最后，点击**添加**按钮。我们现在准备为`LevelManager`类编码头文件。\n\n添加以下包含指令和私有变量，然后我们将讨论它们:\n\n```cpp\n#pragma once\n#include <SFML/Graphics.hpp>\nusing namespace sf;\nusing namespace std;\nclass LevelManager\n{\nprivate:\n    Vector2i m_LevelSize;\n    Vector2f m_StartPosition;\n    float m_TimeModifier = 1;\n    float m_BaseTimeLimit = 0;\n    int m_CurrentLevel = 0;\n    const int NUM_LEVELS = 4;\n// public declarations go here\n```\n\n前面的代码声明了一个`Vector2i`、`m_LevelSize`来保存两个整数值，这两个整数值将保存当前地图包含的水平和垂直切片数。`Vector2f`、`m_StartPosition`包含了世界上鲍勃和托马斯应该诞生的坐标。请注意，这不是与`m_LevelSize`单位相关的平铺位置，而是水平和垂直像素位置。\n\n`m_TimeModifier`成员变量是一个浮点型变量，用于乘以当前级别的可用时间。我们想这样做的原因是，我们可以改变(减少)这个值，这样我们就可以缩短玩家每次尝试相同级别的可用时间。举个例子，如果玩家第一次尝试一级获得 60 秒，那么 60 乘以 1 当然就是 60。当玩家完成所有关卡并第二次回到 1 级时，`m_TimeModifier`将被降低 10%。然后，当可用时间乘以 0.9，玩家可用的时间将是 54 秒。这要少 10%。比赛会越来越难。\n\n`m_BaseTimeLimit`浮点变量保持我们刚刚讨论的原始的、未修改的时间限制。\n\n我们大概可以猜测`m_CurrentLevel`会持有当前正在玩的关卡号。\n\n当再次回到 1 级并降低`m_TimeModifier`值合适时，将使用`int`、`NUM_LEVELS`常量进行标记。\n\n现在，在我们之前添加的代码之后添加以下公共变量和函数声明:\n\n```cpp\npublic:\n    const int TILE_SIZE = 50;\n    const int VERTS_IN_QUAD = 4;\n    float getTimeLimit();\n    Vector2f getStartPosition();\n    int** nextLevel(VertexArray& rVaLevel);\n    Vector2i getLevelSize();\n    int getCurrentLevel();\n};\n```\n\n在前面的代码中，有两个常量`int`成员。`TILE_SIZE`是一个有用的常数，提醒我们子画面中的每个图块都有 50 像素宽和 50 像素高。`VERTS_IN_QUAD`是一个有用的常数，让我们对`VertexArray`的操作不那么容易出错。事实上，一个四边形有四个顶点。现在，我们不能忘记这一点。\n\n`getTimeLimit`、`getStartPosition`、`getLevelSize`和`getCurrentLevel`函数是简单的 getter 函数，返回我们在前面代码块中声明的私有成员变量的当前值。\n\n值得多谈的功能是`nextLevel`。这个函数接收一个`VertexArray`引用，就像我们在僵尸竞技场游戏中使用的一样。然后，该函数可以在`VertexArray`引用上工作，所有的更改都将出现在调用代码的`VertexArray`引用中。\n\n`nextLevel`函数返回一个指向指针的指针，这意味着我们可以返回一个二维数组`int`值的第一个元素的地址。我们将建立一个二维数组`int`的价值，将代表每个层次的布局。当然，这些 int 值将从关卡设计文本文件中读取。\n\n## 对 LevelManager.cpp 文件进行编码\n\n右键单击**解决方案资源管理器**中的**源文件**，并选择**添加|新项目...**。在**添加新项目**窗口中，突出显示(通过左键单击) **C++ 文件(。cpp)** 然后，在**名称**字段中，键入`LevelManager.cpp`。最后，点击**添加**按钮。我们现在准备为`LevelManager`类编码`.cpp`文件。\n\n由于这是一堂相当长的课，我们将把它分成六大块来讨论。前五个将覆盖`nextLevel`功能，第六个将覆盖其余功能。\n\n添加以下包含指令和`nextLevel`功能的第一(五)部分:\n\n```cpp\n#include <SFML/Graphics.hpp>\n#include <SFML/Audio.hpp>\n#include \"TextureHolder.h\"\n#include <sstream>\n#include <fstream>\n#include \"LevelManager.h\"\nusing namespace sf;\nusing namespace std;\nint** LevelManager::nextLevel(VertexArray& rVaLevel)\n{\n    m_LevelSize.x = 0;\n    m_LevelSize.y = 0;\n    // Get the next level\n    m_CurrentLevel++ ;\n    if (m_CurrentLevel > NUM_LEVELS)\n    {\n        m_CurrentLevel = 1;\n        m_TimeModifier -= .1f;\n    }\n    // Load the appropriate level from a text file\n    string levelToLoad;\n    switch (m_CurrentLevel)\n    {\n    case 1:\n        levelToLoad = \"levels/level1.txt\";\n        m_StartPosition.x = 100;\n        m_StartPosition.y = 100;\n        m_BaseTimeLimit = 30.0f;\n        break;\n    case 2:\n        levelToLoad = \"levels/level2.txt\";\n        m_StartPosition.x = 100;\n        m_StartPosition.y = 3600;\n        m_BaseTimeLimit = 100.0f;\n        break;\n    case 3:\n        levelToLoad = \"levels/level3.txt\";\n        m_StartPosition.x = 1250;\n        m_StartPosition.y = 0;\n        m_BaseTimeLimit = 30.0f;\n        break;\n    case 4:\n        levelToLoad = \"levels/level4.txt\";\n        m_StartPosition.x = 50;\n        m_StartPosition.y = 200;\n        m_BaseTimeLimit = 50.0f;\n        break;\n    }// End switch\n```\n\n在包含指令之后，代码将`m_LevelSize.x`和`m_LevelSize.y`变量初始化为零。\n\n接下来，`m_CurrentLevel`递增。接下来的`if`语句检查`m_CurrentLevel`是否大于`NUM_LEVELS`。如果是，那么`m_CurrentLevel`被设置回`1`并且`m_TimeModifier`被减少`0.1`以缩短所有级别的允许时间。\n\n然后代码根据`m_CurrentLevel`保存的值进行切换。每个`case`语句初始化保存关卡设计的文本文件的名称，托马斯和鲍勃的起始位置，以及`m_BaseTimeLimit`，这是所讨论关卡的未修改的时间限制。\n\n小费\n\n如果您设计了自己的级别，请在这里添加一个`case`语句和相应的值。另外，编辑`LevelManager.h`文件中的`NUM_LEVELS`常量。\n\n现在，添加`nextLevel`功能的第二部分，如下。在前一个代码之后立即添加此代码。添加代码时请仔细研究，以便我们可以讨论它:\n\n```cpp\n    ifstream inputFile(levelToLoad);\n    string s;\n    // Count the number of rows in the file\n    while (getline(inputFile, s))\n    {\n        ++ m_LevelSize.y;\n    }\n    // Store the length of the rows\n    m_LevelSize.x = s.length();\n```\n\n在前面(第二部分)的代码中，我们声明了一个名为`inputFile`的`ifstream`对象，该对象打开了一个指向包含在`levelToLoad`中的文件名的流。\n\n代码使用`getLine`循环遍历文件的每一行，但不记录其任何内容。它所做的只是通过增加`m_LevelSize.y`来计算行数。在`for`循环后，使用`s.length`功能将水平仪的宽度保存在`m_LevelSize.x`中。这意味着所有线的长度必须相同，否则我们会遇到麻烦。\n\n至此，我们知道并已经在`m_LevelSize`中保存了当前级别的长度和宽度。\n\n现在，添加`nextLevel`函数的第三部分，如下代码所示。将代码添加到前面代码的正下方。添加代码时请仔细研究，以便我们可以讨论它:\n\n```cpp\n    // Go back to the start of the file\n    inputFile.clear();\n    inputFile.seekg(0, ios::beg);\n    // Prepare the 2D array to hold the int values from the file\n    int** arrayLevel = new int*[m_LevelSize.y];\n    for (int i = 0; i < m_LevelSize.y; ++ i)\n    {\n        // Add a new array into each array element\n        arrayLevel[i] = new int[m_LevelSize.x];\n    }\n```\n\n首先，我们使用`clear`功能清除`inputFile`。使用`0, ios::beg`参数调用的`seekg`函数将文件光标的位置(从下一个字符开始读取的位置)移动到文件的开头。\n\n接下来，我们声明一个指向名为`arrayLevel`的指针的指针。请注意，这是使用`new`关键字在自由存储/堆上完成的。一旦我们初始化了这个二维数组，我们就可以将它的地址返回给调用代码，它将一直存在，直到我们删除它或者游戏关闭。\n\n`for`循环从 0 循环到`m_LevelSize.y -1`。在循环的每一遍中，它都会在堆上添加一个新的`int`值数组，以匹配`m_LevelSize.x`的值。我们现在有了一个完美配置的(当前级别的)二维数组。唯一的问题是里面还什么都没有。\n\n现在，添加`nextLevel`函数的第四部分，如下代码所示。在前一个代码之后立即添加此代码。添加代码时请仔细研究，以便我们可以讨论它:\n\n```cpp\n    // Loop through the file and store all \n   // the values in the 2d array\n    string row;\n    int y = 0;\n    while (inputFile >> row)\n    {\n        for (int x = 0; x < row.length(); x++) {\n            const char val = row[x];\n            arrayLevel[y][x] = atoi(&val);\n        }\n        y++ ;\n    }\n    // Close the file\n    inputFile.close();\n```\n\n首先，代码初始化一个名为`row`的`string`，它一次只保存一行关卡设计。我们还声明并初始化了一个名为`y`的`int`，它将帮助我们计算行数。\n\n`while`循环重复执行，直到`inputFile`经过最后一行。在`while`循环中，有一个`for`循环，它遍历当前行的每个字符，并将其存储在二维数组`arrayLevel`中。请注意，我们使用`arrayLevel[y][x]=`访问二维数组的右边元素。`atoi`功能将`char val`转换为`int`。这是必需的，因为我们有一个二维数组用于`int`，而不是用于`char`。\n\n现在，我们来添加`nextLevel`函数的第五部分，如下图所示。在前一个代码之后立即添加此代码。添加代码时请仔细研究，这样我们就可以讨论它:\n\n```cpp\n    // What type of primitive are we using?\n    rVaLevel.setPrimitiveType(Quads);\n    // Set the size of the vertex array\n    rVaLevel.resize(m_LevelSize.x * \n      m_LevelSize.y * VERTS_IN_QUAD);\n    // Start at the beginning of the vertex array\n    int currentVertex = 0;\n    for (int x = 0; x < m_LevelSize.x; x++)\n    {\n        for (int y = 0; y < m_LevelSize.y; y++)\n        {\n            // Position each vertex in the current quad\n            rVaLevel[currentVertex + 0].position = \n                Vector2f(x * TILE_SIZE, \n                y * TILE_SIZE);\n            rVaLevel[currentVertex + 1].position = \n                Vector2f((x * TILE_SIZE) + TILE_SIZE, \n                y * TILE_SIZE);\n            rVaLevel[currentVertex + 2].position = \n                Vector2f((x * TILE_SIZE) + TILE_SIZE, \n                (y * TILE_SIZE) + TILE_SIZE);\n            rVaLevel[currentVertex + 3].position = \n                Vector2f((x * TILE_SIZE), \n                (y * TILE_SIZE) + TILE_SIZE);\n            // Which tile from the sprite sheet should we use\n            int verticalOffset = arrayLevel[y][x] * TILE_SIZE;\n            rVaLevel[currentVertex + 0].texCoords = \n                Vector2f(0, 0 + verticalOffset);\n            rVaLevel[currentVertex + 1].texCoords = \n                Vector2f(TILE_SIZE, 0 + verticalOffset);\n            rVaLevel[currentVertex + 2].texCoords = \n                Vector2f(TILE_SIZE, TILE_SIZE + verticalOffset);\n            rVaLevel[currentVertex + 3].texCoords = \n                Vector2f(0, TILE_SIZE + verticalOffset);\n            // Position ready for the next four vertices\n            currentVertex = currentVertex + VERTS_IN_QUAD;\n        }\n    }\n    return arrayLevel;\n} // End of nextLevel function\n```\n\n虽然这是五段代码中最长的一段(我们把`nextLevel`一分为二)，但也是最直接的。这是因为我们在僵尸竞技场项目中看到了非常相似的代码。\n\n预编码的过程是嵌套的`for`循环从零到水平的宽度和高度循环。对于阵列中的每个位置，四个顶点被放入`VertexArray`中，并且从子画面表中分配四个纹理坐标。使用`currentVertex`变量、`TILE SIZE`和`VERTS_IN_QUAD`常数计算顶点和纹理坐标的位置。在内部`for`循环的每个循环结束时，`currentVertex`增加`VERTS_IN_QUAD`，很好地移动到下一个图块。\n\n重要说明\n\n关于`VertexArray`要记住的重要一点是它是通过引用传入`nextLevel`的。因此，`VertexArray`将在调用代码中可用。我们将从`Engine`类的代码中调用`nextLevel`。\n\n一旦调用了这个函数，`Engine`类将有一个`VertexArray`以图形方式表示该层，还有一个二维数组`int`值作为该层中所有平台和障碍物的数字表示。\n\n其余的`LevelManager`函数都是简单的 getter 函数，但是要花时间去熟悉哪个函数返回了哪个私有值。添加`LevelManager`类的剩余功能，如下所示:\n\n```cpp\nVector2i LevelManager::getLevelSize()\n{\n    return m_LevelSize;\n}\nint LevelManager::getCurrentLevel()\n{\n    return m_CurrentLevel;\n}\nfloat LevelManager::getTimeLimit()\n{\n    return m_BaseTimeLimit * m_TimeModifier;\n}\nVector2f LevelManager::getStartPosition()\n{\n    return m_StartPosition;\n}\n```\n\n现在`LevelManager`类已经完成，我们可以继续使用它了。为此，我们将在`Engine`类中编写另一个函数。\n\n# 对负载水平函数进行编码\n\n需要明确的是，这个函数是`Engine`类的一部分，尽管它会将其大部分工作委托给其他函数，包括我们刚刚构建的`LevelManager`类的那些函数。\n\n首先，让我们将新函数的声明以及一些其他新代码添加到`Engine.h`文件中。打开`Engine.h`文件，添加`Engine.h`文件的缩略快照中突出显示的代码行，如下所示:\n\n```cpp\n#pragma once\n#include <SFML/Graphics.hpp>\n#include \"TextureHolder.h\"\n#include \"Thomas.h\"\n#include \"Bob.h\"\n#include \"LevelManager.h\"\nusing namespace sf;\nclass Engine\n{\nprivate:\n    // The texture holder\n    TextureHolder th;\n    // Thomas and his friend, Bob\n    Thomas m_Thomas;\n    Bob m_Bob;\n // A class to manage all the levels\n LevelManager m_LM;\n    const int TILE_SIZE = 50;\n    const int VERTS_IN_QUAD = 4;\n    // The force pushing the characters down\n    const int GRAVITY = 300;\n    // A regular RenderWindow\n    RenderWindow m_Window;\n    // The main Views\n    View m_MainView;\n    View m_LeftView;\n    View m_RightView;\n    // Three views for the background\n    View m_BGMainView;\n    View m_BGLeftView;\n    View m_BGRightView;\n    View m_HudView;\n    // Declare a sprite and a Texture for the background\n    Sprite m_BackgroundSprite;\n    Texture m_BackgroundTexture;\n    // Is the game currently playing?\n    bool m_Playing = false;\n    // Is character 1 or 2 the current focus?\n    bool m_Character1 = true;\n    // Start in full screen mode\n    bool m_SplitScreen = false;\n    // How much time is left in the current level\n    float m_TimeRemaining = 10;\n    Time m_GameTimeTotal;\n    // Is it time for a new/first level?\n    bool m_NewLevelRequired = true;\n // The vertex array for the level tiles\n VertexArray m_VALevel;\n // The 2d array with the map for the level\n // A pointer to a pointer\n int** m_ArrayLevel = NULL;\n // Texture for the level tiles\n Texture m_TextureTiles;\n\n    // Private functions for internal use only\n    void input();\n    void update(float dtAsSeconds);\n    void draw();    \n // Load a new level\n void loadLevel();\n\npublic:\n    // The Engine constructor\n    Engine();\n    ...\n    ...        \n    ...\n```\n\n这是我们在前面的代码中可以看到的:\n\n*   我们包括了`LevelManager.h`文件。\n*   我们添加了一个名为`m_LM`的`LevelManager`实例。\n*   我们增加了一个`VertexArray`叫做`m_VALevel`。\n*   我们添加了一个指向 int 的指针，该指针将保存从`nextLevel`返回的二维数组。\n*   我们为精灵表添加了一个新的`Texture`对象。\n*   我们添加了现在将要编写的`loadLevel`函数的声明。\n\n右键单击**解决方案资源管理器**中的**源文件**，并选择**添加|新项目...**。在**添加新项目**窗口中，突出显示(通过左键单击) **C++ 文件(。cpp)** 然后，在**名称**字段中，键入`LoadLevel.cpp`。最后，点击**添加**按钮。我们现在准备对`loadLevel`功能进行编码。\n\n将`loadLevel`功能的代码添加到`LoadLevel.cpp`文件中。然后，我们可以讨论一下:\n\n```cpp\n#include \"Engine.h\"\nvoid Engine::loadLevel()\n{\n    m_Playing = false;\n    // Delete the previously allocated memory\n    for (int i = 0; i < m_LM.getLevelSize().y; ++ i)\n    {\n        delete[] m_ArrayLevel[i];\n    }\n    delete[] m_ArrayLevel;\n    // Load the next 2d array with the map for the level\n    // And repopulate the vertex array as well\n    m_ArrayLevel = m_LM.nextLevel(m_VALevel);\n    // How long is this new time limit\n    m_TimeRemaining = m_LM.getTimeLimit();\n    // Spawn Thomas and Bob\n    m_Thomas.spawn(m_LM.getStartPosition(), GRAVITY);\n    m_Bob.spawn(m_LM.getStartPosition(), GRAVITY);\n    // Make sure this code isn't run again\n    m_NewLevelRequired = false;\n}\n```\n\n首先，我们将`m_Playing`设置为假，以阻止部分`update`功能的执行。接下来，我们遍历`m_ArrayLevel`中的所有水平数组并删除它们。在`for`循环之后，我们删除`m_ArrayLevel`本身。\n\n`m_ArrayLevel = m_LM.nextLevel(m_VALevel)`调用`nextLevel`并准备`VertexArray` `m_VALevel`，以及被称为`m_ArrayLevel`的二维数组。关卡已经设置好，可以开始了。\n\n通过调用`getTimeLimit`初始化`m_TimeRemaining`，使用`spawn`函数产生托马斯和鲍勃，以及从`getStartPosition`返回的值。\n\n最后将`m_NewLevelRequired`设置为`false`。几页后我们会看到，`m_NewLevelRequired`被设置为`true`会导致`loadLevel`被调用。我们只想运行这个函数一次。\n\n# 更新发动机\n\n打开`Engine.cpp`文件，在引擎构造器的末尾添加以下高亮显示的代码来加载子画面纹理:\n\n```cpp\nEngine::Engine()\n{\n    // Get the screen resolution and create an SFML window and View\n    Vector2f resolution;\n    resolution.x = VideoMode::getDesktopMode().width;\n    resolution.y = VideoMode::getDesktopMode().height;\n    m_Window.create(VideoMode(resolution.x, resolution.y),\n        \"Thomas was late\",\n        Style::Fullscreen);\n    // Initialize the full screen view\n    m_MainView.setSize(resolution);\n    m_HudView.reset(\n        FloatRect(0, 0, resolution.x, resolution.y));\n    // Initialize the split-screen Views\n    m_LeftView.setViewport(\n        FloatRect(0.001f, 0.001f, 0.498f, 0.998f));\n    m_RightView.setViewport(\n        FloatRect(0.5f, 0.001f, 0.499f, 0.998f));\n    m_BGLeftView.setViewport(\n        FloatRect(0.001f, 0.001f, 0.498f, 0.998f));\n    m_BGRightView.setViewport(\n        FloatRect(0.5f, 0.001f, 0.499f, 0.998f));\n    // Can this graphics card use shaders?\n    if (!sf::Shader::isAvailable())\n    {\n        // Time to get a new PC\n        m_Window.close();\n    }\n    m_BackgroundTexture = TextureHolder::GetTexture(\n        \"graphics/background.png\");\n    // Associate the sprite with the texture\n    m_BackgroundSprite.setTexture(m_BackgroundTexture);\n // Load the texture for the background vertex array\n m_TextureTiles = TextureHolder::GetTexture(\n \"graphics/tiles_sheet.png\");\n}\n```\n\n我们在前面的代码中所做的就是将精灵表加载到`m_TextureTiles`中。\n\n打开`Update.cpp`文件，进行以下突出显示的更改和添加:\n\n```cpp\nvoid Engine::update(float dtAsSeconds)\n{\n    if (m_NewLevelRequired)\n    {\n        // These calls to spawn will be moved to a new\n        // loadLevel function soon\n        // Spawn Thomas and Bob\n //m_Thomas.spawn(Vector2f(0,0), GRAVITY);\n //m_Bob.spawn(Vector2f(100, 0), GRAVITY);\n        // Make sure spawn is called only once\n //m_TimeRemaining = 10;\n //m_NewLevelRequired = false;\n // Load a level\n loadLevel();\n\n    }\n```\n\n实际上，我们应该删除而不是注释掉我们不再使用的行。我刚刚用这种方式展示了它，这样变化就很明显了。在之前的`if`语句中应该有的只是对`loadLevel`的调用。\n\n最后，在我们可以看到本章到目前为止所做工作的结果之前，打开`Draw.cpp`文件，并进行以下突出显示的添加，以绘制代表级别的顶点数组:\n\n```cpp\nvoid Engine::draw()\n{\n    // Rub out the last frame\n    m_Window.clear(Color::White);\n    if (!m_SplitScreen)\n    {\n        // Switch to background view\n        m_Window.setView(m_BGMainView);\n        // Draw the background\n        m_Window.draw(m_BackgroundSprite);\n        // Switch to m_MainView\n        m_Window.setView(m_MainView);        \n // Draw the Level\n m_Window.draw(m_VALevel, &m_TextureTiles);\n        // Draw thomas\n        m_Window.draw(m_Thomas.getSprite());\n        // Draw bob\n        m_Window.draw(m_Bob.getSprite());\n    }\n    else\n    {\n        // Split-screen view is active\n        // First draw Thomas' side of the screen\n        // Switch to background view\n        m_Window.setView(m_BGLeftView);\n        // Draw the background\n        m_Window.draw(m_BackgroundSprite);\n        // Switch to m_LeftView\n        m_Window.setView(m_LeftView);\n // Draw the Level\n m_Window.draw(m_VALevel, &m_TextureTiles);\n\n        // Draw bob\n        m_Window.draw(m_Bob.getSprite());\n        // Draw thomas\n        m_Window.draw(m_Thomas.getSprite());\n\n        // Now draw Bob's side of the screen\n        // Switch to background view\n        m_Window.setView(m_BGRightView);\n        // Draw the background\n        m_Window.draw(m_BackgroundSprite);\n        // Switch to m_RightView\n        m_Window.setView(m_RightView);\n // Draw the Level\n m_Window.draw(m_VALevel, &m_TextureTiles);\n        // Draw thomas\n        m_Window.draw(m_Thomas.getSprite());\n        // Draw bob\n        m_Window.draw(m_Bob.getSprite());\n\n    }\n    // Draw the HUD\n    // Switch to m_HudView\n    m_Window.setView(m_HudView);\n\n    // Show everything we have just drawn\n    m_Window.display();\n}\n```\n\n请注意，我们需要为所有屏幕选项(全、左、右)绘制`VertexArray`。\n\n现在，你可以运行游戏了。然而，不幸的是，托马斯和鲍勃直接从我们精心设计的平台上跌落下来。由于这个原因，我们无法尝试并在关卡中前进，也无法战胜时间。\n\n# 碰撞检测\n\n我们将使用矩形交叉和 SFML `intersects`功能来处理碰撞检测。在这个项目中不同的是，我们将把碰撞检测代码抽象成它自己的功能。正如我们已经看到的，托马斯和鲍勃有多个矩形(`m_Head`、`m_Feet`、`m_Left`和`m_Right`)需要我们检查碰撞。\n\n## 对探测碰撞功能进行编码\n\n需要明确的是，这个函数是`Engine`类的一部分。打开`Engine.h`文件，添加一个名为`detectCollisions`的函数声明。这在下面的代码片段中突出显示:\n\n```cpp\n    // Private functions for internal use only\n    void input();\n    void update(float dtAsSeconds);\n    void draw();\n    // Load a new level\n    void loadLevel();\n bool detectCollisions(PlayableCharacter& character);\n\npublic:\n    // The Engine constructor\n    Engine();\n```\n\n从签名中注意到`detectCollision`函数将多态参数作为`PlayerCharacter`对象。我们知道，`PlayerCharacter`是抽象的，永远不可能被实例化。然而，我们通过`Thomas`和`Bob`类继承了它。我们将能够通过`m_Thomas`或`m_Bob`至`detectCollisions`。\n\n右键单击**解决方案资源管理器**中的**源文件**，并选择**添加|新项目...**。在**添加新项目**窗口中，突出显示(通过左键单击) **C++ 文件(。cpp)** 然后，在**名称**字段中，键入`DetectCollisions.cpp`。最后，点击**添加**按钮。我们现在准备对`detectCollisions` 功能进行编码。\n\n在`DetectCollisions.cpp`中添加以下代码。请注意，这只是该函数的第一部分:\n\n```cpp\n#include \"Engine.h\"\nbool Engine::detectCollisions(PlayableCharacter& character)\n{\n    bool reachedGoal = false;\n    // Make a rect for all his parts\n    FloatRect detectionZone = character.getPosition();\n    // Make a FloatRect to test each block\n    FloatRect block;\n    block.width = TILE_SIZE;\n    block.height = TILE_SIZE;\n    // Build a zone around thomas to detect collisions\n    int startX = (int)(detectionZone.left / TILE_SIZE) - 1;\n    int startY = (int)(detectionZone.top / TILE_SIZE) - 1;\n    int endX = (int)(detectionZone.left / TILE_SIZE) + 2;\n    // Thomas is quite tall so check a few tiles vertically\n    int endY = (int)(detectionZone.top / TILE_SIZE) + 3;\n    // Make sure we don't test positions lower than zero\n    // Or higher than the end of the array\n    if (startX < 0)startX = 0;\n    if (startY < 0)startY = 0;\n    if (endX >= m_LM.getLevelSize().x)\n        endX = m_LM.getLevelSize().x;\n    if (endY >= m_LM.getLevelSize().y)\n        endY = m_LM.getLevelSize().y;\n```\n\n我们首先要做的是声明一个名为`reachedGoal`的布尔值。这是`detectCollisions`函数返回给调用代码的值。它被初始化为`false`。\n\n接下来，我们声明一个名为`detectionZone`的`FloatRect`对象，并用代表字符子画面整个矩形的同一个矩形初始化它。请注意，我们实际上不会对这个矩形进行相交测试。之后，我们宣布另一个`FloatRect`叫做`block`。我们将`block`初始化为一个 50 乘 50 的游戏单位矩形。我们将很快看到`block`投入使用。\n\n接下来，我们将看看如何使用`detectionZone`。通过将`detectionZone`周围的区域扩大几个街区，我们初始化了四个`int`变量:`startX`、`startY`、`endX`和`endY`。在接下来的四个`if`语句中，我们检查了在不存在的图块上尝试并进行碰撞检测是不可能的。我们将通过确保从不检查小于零或大于`getLevelSize().x`或`.y`返回的值的位置来实现这一点。\n\n前面这段代码所做的就是创建一个区域来进行碰撞检测。在距离角色数百或数千像素的块上进行碰撞检测是没有意义的。另外，如果我们尝试在不存在数组位置的地方(小于零或大于`getLevelSize()...`)做碰撞检测，游戏会崩溃。\n\n接下来，添加以下代码，处理玩家掉出关卡的情况:\n\n```cpp\n    // Has the character fallen out of the map?\n    FloatRect level(0, 0, \n        m_LM.getLevelSize().x * TILE_SIZE, \n        m_LM.getLevelSize().y * TILE_SIZE);\n\n    if (!character.getPosition().intersects(level))\n    {\n        // respawn the character\n        character.spawn(m_LM.getStartPosition(), GRAVITY);\n    }\n```\n\n一个角色要想停止坠落，必须与一个平台发生碰撞。因此，如果玩家移出地图(没有平台的地方)，他们会不断掉落。之前的代码检查字符*是否没有*与`FloatRect`、`level`相交。如果没有，则它已经掉出水平，`spawn`功能将其发送回起点。\n\n添加以下内容，相当长。代码块，然后我们将了解它的功能:\n\n```cpp\n    // Loop through all the local blocks\n    for (int x = startX; x < endX; x++)\n    {\n        for (int y = startY; y < endY; y++)\n        {\n            // Initialize the starting position of the current block\n            block.left = x * TILE_SIZE;\n            block.top = y * TILE_SIZE;\n            // Has character been burnt or drowned?\n            // Use head as this allows him to sink a bit\n            if (m_ArrayLevel[y][x] == 2 || m_ArrayLevel[y][x] == 3)\n            {\n                if (character.getHead().intersects(block))\n                {\n                    character.spawn(m_LM.getStartPosition(), GRAVITY);\n                    // Which sound should be played?\n                    if (m_ArrayLevel[y][x] == 2)// Fire, ouch!\n                    {\n                        // Play a sound\n                    }\n                    else // Water\n                    {\n                        // Play a sound\n                    }\n                }\n            }\n\n            // Is character colliding with a regular block\n            if (m_ArrayLevel[y][x] == 1)\n            {\n                if (character.getRight().intersects(block))\n                {\n                    character.stopRight(block.left);\n                }\n                else if (character.getLeft().intersects(block))\n                {\n                    character.stopLeft(block.left);\n                }\n                if (character.getFeet().intersects(block))\n                {\n                    character.stopFalling(block.top);\n                }\n                else if (character.getHead().intersects(block))\n                {\n                    character.stopJump();\n                }\n            }\n\n            // More collision detection here once we have \n            // learned about particle effects\n            // Has the character reached the goal?\n            if (m_ArrayLevel[y][x] == 4)\n            {\n                // Character has reached the goal\n                reachedGoal = true;\n            }\n        }\n    }\n```\n\n前面的代码使用相同的技术做了三件事。它循环遍历包含在`startX`、`endX`和`startY`、`endY`之间的所有值。对于每次通过，它都会检查并执行以下操作:\n\n*   角色是被烧死还是淹死？`if (m_ArrayLevel[y][x] == 2 || m_ArrayLevel[y][x] == 3)`判断当前被检查的位置是火瓦还是水瓦。如果角色的头部与这些牌中的一个相交，玩家将被重新上牌。我们还编写了一个空的`if` / `else`块，准备在下一章中添加声音。\n*   角色接触过普通瓷砖吗？`code if (m_ArrayLevel[y][x] == 1)`确定正在检查的当前位置是否包含常规图块。如果它与代表角色不同身体部位的任何矩形相交，则相关函数被调用(`stopRight`、`stopLeft`、`stopFalling`或`stopJump`)。传递给这些函数的值以及函数如何使用该值来重新定位字符是非常微妙的。虽然没有必要仔细检查这些值来理解代码，但我们可能希望查看传入的值，然后参考上一章中`PlayableCharacter`类的适当函数。这将帮助你准确地理解正在发生的事情。\n*   角色有没有碰到球门瓷砖？这是由`if (m_ArrayLevel[y][x] == 4)`决定的。我们只需要将`reachedGoal`设置为`true`。`Engine`类的`update`功能将跟踪两个角色(托马斯和鲍勃)是否同时达到目标。我们将在一分钟后在`update`函数中编写这段代码。\n\n将以下代码行添加到`detectCollisions`功能中:\n\n```cpp\n    // All done, return, whether or \n   // not a new level might be required\n    return reachedGoal;\n}\n```\n\n前一行代码返回`reachedGoal`布尔值，这样如果两个字符同时达到目标，调用代码可以保持跟踪并做出适当的响应。\n\n我们现在需要做的就是每一个字符，每一帧调用一次`detectCollision`函数。在`if(m_Playing)`代码块内的`Update.cpp`文件中添加以下突出显示的代码:\n\n```cpp\nif (m_Playing)\n{\n    // Update Thomas\n    m_Thomas.update(dtAsSeconds);\n    // Update Bob\n    m_Bob.update(dtAsSeconds);\n // Detect collisions and see if characters \n // have reached the goal tile\n // The second part of the if condition is only executed\n // when thomas is touching the home tile\n if (detectCollisions(m_Thomas) && detectCollisions(m_Bob))\n {\n // New level required\n m_NewLevelRequired = true;\n // Play the reach goal sound \n }\n else\n {\n // Run bobs collision detection\n detectCollisions(m_Bob);\n }\n    // Count down the time the player has left\n    m_TimeRemaining -= dtAsSeconds;\n    // Have Thomas and Bob run out of time?\n    if (m_TimeRemaining <= 0)\n    {\n        m_NewLevelRequired = true;\n    }\n}// End if playing\n```\n\n前面的代码调用`detectCollision`函数，检查鲍勃和托马斯是否同时达到了目标。如果有，则通过将`m_NewLevelRequired`设置为`true`来准备下一级。\n\n你可以运行游戏，在平台上行走。你可以达到目标，开始一个新的水平。同样，第一次，跳转按钮( *W* 或 *Up* 箭头)将工作。\n\n如果你达到了目标，那么下一关就会载入。如果你达到了最后一关的目标，那么第一关将会减少 10%的时间限制。当然，目前还没有视觉反馈，因为我们还没有建立平视显示器。我们将在下一章这样做。\n\n然而，许多级别需要托马斯和鲍勃作为一个团队工作。更具体地说，托马斯和鲍勃需要能够爬到对方的头上。\n\n## 更多碰撞检测\n\n在`if (m_Playing)`部分的`Update.cpp`文件中添加了之前的代码之后，再添加以下代码:\n\n```cpp\nif (m_Playing)\n{\n    // Update Thomas\n    m_Thomas.update(dtAsSeconds);\n    // Update Bob\n    m_Bob.update(dtAsSeconds);\n    // Detect collisions and see if characters \n    // have reached the goal tile\n    // The second part of the if condition is only executed\n    // when thomas is touching the home tile\n    if (detectCollisions(m_Thomas) && detectCollisions(m_Bob))\n    {\n        // New level required\n        m_NewLevelRequired = true;\n        // Play the reach goal sound\n    }\n    else\n    {\n        // Run bobs collision detection\n        detectCollisions(m_Bob);\n    }\n // Let bob and thomas jump on each others heads\n if (m_Bob.getFeet().intersects(m_Thomas.getHead()))\n {\n m_Bob.stopFalling(m_Thomas.getHead().top);\n }\n else if (m_Thomas.getFeet().intersects(m_Bob.getHead()))\n {\n m_Thomas.stopFalling(m_Bob.getHead().top);\n }\n    // Count down the time the player has left\n    m_TimeRemaining -= dtAsSeconds;\n    // Have Thomas and Bob run out of time?\n    if (m_TimeRemaining <= 0)\n    {\n        m_NewLevelRequired = true;\n    }\n}// End if playing\n```\n\n你可以再次运行游戏，站在托马斯和鲍勃的头上，到达以前无法到达的难以到达的地方:\n\n![](img/B14278_16_09.jpg)\n\n# 总结\n\n这一章有相当多的代码。我们学习了如何从文件中读取并将文本字符串转换成`char`值，然后转换成 int `values`。一旦我们有了一个二维的`int`值数组，我们就能够填充一个`VertexArray`实例来显示屏幕上的级别。然后，我们使用相同的二维数组`int`值来实现碰撞检测。我们使用了矩形交叉，就像我们在僵尸竞技场项目中所做的那样，尽管这一次，为了更精确，我们给了每个角色四个碰撞区域——每个区域代表他们的头、脚、左侧和右侧。\n\n现在游戏已经完全可以玩了，我们需要在屏幕上表示游戏的状态(分数和时间)。在下一章中，我们将实现平视显示器，以及一些比我们迄今为止使用的更高级的音效。"
  },
  {
    "path": "docs/begin-cpp-game-prog/17.md",
    "content": "# 十七、声音空间化和平视显示器\n\n在本章中，我们将添加所有的声音效果和平视显示器。我们已经在前面的两个项目中做到了这一点，但是这次我们会做一些不同的事情。我们将探索声音**空间化**的概念，以及 SFML 如何让这个原本复杂的概念变得美好和简单。此外，我们将构建一个 HUD 类来封装我们将信息绘制到屏幕上的代码。\n\n我们将按以下顺序完成这些任务。\n\n*   什么是空间化？\n*   SFML 如何处理空间化\n*   构建声音管理器类\n*   部署发射器\n*   使用声音管理器类\n*   建一个`HUD`班\n*   使用`HUD`类\n\n# 什么是空间化？\n\n**空间化**是相对于某个事物是其一部分的空间或其内部来制造该事物的行为。在我们的日常生活中，自然世界中的一切，默认都是空间化的。如果一辆摩托车从左向右呼啸而过，我们会听到声音从一边到另一边从微弱到响亮。当它经过时，它会在另一只耳朵里变得更加突出，然后再次消失在远处。如果我们有一天早上醒来，世界不再空间化，那将是异常怪异的。\n\n如果我们能让我们的电子游戏更像真实世界一点，我们的玩家就能变得更沉浸其中。如果玩家能在远处隐约听到他们的声音，当他们从一个方向或另一个方向靠近时，他们非人的哭喊声会越来越大，我们的僵尸游戏就会有趣得多。\n\n很明显，空间化的数学很复杂。我们如何根据从演奏者(声音的听者)到发出声音的物体(发声者)的距离和方向来计算给定声音在特定扬声器中的音量？\n\n幸运的是，SFML 为我们完成了所有复杂的过程。我们所需要做的就是熟悉一些技术术语，然后我们可以开始使用 SFML 来使我们的声音效果空间化。\n\n## 发射器、衰减和监听器\n\n我们需要了解一些信息，以便向 SFML 提供它开展工作所需的信息。在我们的游戏世界中，我们需要知道声音来自哪里。这个声源被称为**发射器**。在一个游戏中，发射器可能是一个僵尸，一辆车，或者在我们当前项目的情况下，一个火砖。我们已经跟踪了游戏中物体的位置，所以给 SFML 发射器的位置会很简单。\n\n我们需要注意的下一个因素是**衰减**。衰减是波恶化的速度。你可以简化这种说法，让它专门针对声音，说衰减是声音音量降低的速度。这在技术上并不准确，但对于本章和我们的游戏来说，这是一个足够好的描述。\n\n我们需要考虑的最后一个因素是**听者**。当 SFML 把声音空间化时，它相对于哪里空间化；游戏的“耳朵”在哪里。？在大多数游戏中，合乎逻辑的做法是使用玩家角色。在我们的游戏中，我们将使用托马斯(我们的玩家角色)。\n\n# 使用 SFML 处理空间化\n\nSFML 有几个功能，允许我们处理发射器，衰减和听众。让我们假设性地看一下它们，然后我们将编写一些代码，将空间化的声音真实地添加到我们的项目中。\n\n我们可以设置一个准备播放的音效，就像我们已经经常做的那样，如下所示:\n\n```cpp\n// Declare SoundBuffer in the usual way\nSoundBuffer zombieBuffer;\n// Declare a Sound object as-per-usual\nSound zombieSound;\n// Load the sound from a file like we have done so often\nzombieBuffer.loadFromFile(\"sound/zombie_growl.wav\");\n// Associate the Sound object with the Buffer\nzombieSound.setBuffer(zombieBuffer);\n```\n\n我们可以使用如下代码所示的`setPosition`功能设置发射器的位置:\n\n```cpp\n// Set the horizontal and vertical positions of the emitter\n// In this case the emitter is a zombie\n// In the Zombie Arena project we could have used \n// getPosition().x and getPosition().y\n// These values are arbitrary\nfloat x = 500;\nfloat y = 500;\nzombieSound.setPosition(x, y, 0.0f);\n```\n\n正如前面代码的注释中所建议的，我们如何准确地获得发射器的坐标可能取决于游戏的类型。如前面的代码所示，这在僵尸竞技场项目中非常简单。当我们在这个项目中确定位置时，我们将有一些挑战需要克服。\n\n我们可以按如下方式设置衰减级别:\n\n```cpp\nzombieSound.setAttenuation(15);\n```\n\n实际衰减水平可能有点模糊。我们希望玩家得到的效果可能与精确的科学公式不同，该公式用于根据衰减来降低远距离音量。获得正确的衰减水平通常是通过实验来实现的。衰减级别越高，声音级别降低到静音的速度越快。\n\n此外，我们可能希望在发射器周围设置一个区域，使体积完全不衰减。如果该功能在特定范围之外不合适，或者如果我们有许多声源并且不想“过度”使用该功能，我们可能会这样做。为此，我们可以使用如下所示的`setMinimumDistance`功能:\n\n```cpp\nzombieSound.setMinDistance(150);\n```\n\n对于前一行代码，在收听者距离发射器 150 像素/单位之前，不会计算衰减。\n\nSFML 图书馆的其他一些有用的功能包括`setLoop`功能。当`true`作为参数传入时，该函数将告诉 SFML 不断播放声音，如以下代码所示:\n\n```cpp\nzombieSound.setLoop(true);\n```\n\n声音将继续播放，直到我们用以下代码结束:\n\n```cpp\nzombieSound.stop();\n```\n\n有时，我们会想知道声音的状态(播放或停止)。我们可以通过`getStatus`函数实现这一点，如下面的代码所示:\n\n```cpp\nif (zombieSound.getStatus() == Sound::Status::Stopped)\n{\n    // The sound is NOT playing\n    // Take whatever action here\n}\nif (zombieSound.getStatus() == Sound::Status::Playing)\n{\n    // The sound IS playing\n    // Take whatever action here\n}\n```\n\n在 SFML 使用声音空间化还有一个方面我们需要讨论。倾听者。听者在哪里？我们可以用下面的代码设置监听器的位置:\n\n```cpp\n// Where is the listener? \n// How we get the values of x and y varies depending upon the game\n// In the Zombie Arena game or the Thomas Was Late game\n// We can use getPosition()\nListener::setPosition(m_Thomas.getPosition().x, \n    m_Thomas.getPosition().y, 0.0f);\n```\n\n前面的代码将使所有声音相对于该位置播放。这正是我们对于远处的火瓦轰鸣声或者来袭的丧尸所需要的，但是对于像跳跃这样的常规音效来说，这是一个问题。我们可以开始处理玩家位置的发射器，但是 SFML 让事情变得简单了。每当我们想要播放一个“正常”的声音时，我们只需简单地调用`setRelativeToListener`，如下代码所示，然后以与目前完全相同的方式播放声音。以下是我们如何播放“正常”的非空间跳跃音效:\n\n```cpp\njumpSound.setRelativeToListener(true);\njumpSound.play();\n```\n\n我们所需要做的就是在播放任何空间化的声音之前再次调用`Listener::setPosition`。\n\n我们现在有一个广泛的 SFML 声音功能剧目，我们准备好了一些空间噪音的真实。\n\n# 构建声音管理器类\n\n您可能还记得上一个项目，所有的声音代码占用了相当多的代码行。现在，考虑一下，随着空间化，它会变得更长。为了使我们的代码易于管理，我们将编写一个类来管理所有正在播放的声音效果。此外，为了帮助我们进行空间化，我们还将向`Engine`类添加一个函数，但是我们将在本章稍后讨论这个问题。\n\n## 编码声音管理器\n\n让我们从编码和检查头文件开始。\n\n右键单击**解决方案资源管理器**中的**头文件**，选择**添加|新项目...**。在**添加新项目**窗口中，突出显示(通过左键单击)**头文件(。h)** 然后在**名称**字段中，输入`SoundManager.h`。最后，点击**添加**按钮。我们现在准备为`SoundManager`类编码头文件。\n\n添加并检查以下代码:\n\n```cpp\n#pragma once\n#include <SFML/Audio.hpp>\nusing namespace sf;\nclass SoundManager\n{\n    private:\n        // The buffers\n        SoundBuffer m_FireBuffer;\n        SoundBuffer m_FallInFireBuffer;\n        SoundBuffer m_FallInWaterBuffer;\n        SoundBuffer m_JumpBuffer;\n        SoundBuffer m_ReachGoalBuffer;\n        // The Sounds\n        Sound m_Fire1Sound;\n        Sound m_Fire2Sound;\n        Sound m_Fire3Sound;\n        Sound m_FallInFireSound;\n        Sound m_FallInWaterSound;\n        Sound m_JumpSound;\n        Sound m_ReachGoalSound;\n        // Which sound should we use next, fire 1, 2 or 3\n        int m_NextSound = 1;\n    public:\n        SoundManager();\n        void playFire(Vector2f emitterLocation, \n            Vector2f listenerLocation);\n        void playFallInFire();\n        void playFallInWater();\n        void playJump();\n        void playReachGoal();\n};\n```\n\n在我们刚刚添加的代码中没有什么棘手的地方。有五个`SoundBuffer`对象和八个`Sound`对象。三个`Sound`物体将会玩同样的`SoundBuffer`。这就解释了`Sound` / `SoundBuffer`物体数量不同的原因。我们这样做是为了让多个咆哮的声音效果同时播放，并带有不同的空间化参数。\n\n请注意`m_NextSound`变量，它将帮助我们记录下一步应该使用这些同步声音中的哪一个。\n\n有一个构造器，`SoundManager`，我们将在这里设置我们所有的音效，有五个功能将播放音效。其中四个功能只是播放“正常”的音效，它们的代码会更简单。\n\n其中一个功能`playFire`，会处理空间化的音效，会更深入一点。注意`playFire`功能的参数。它接收一个`Vector2f`，这是发射器的位置，和第二个`Vector2f`，这是听者的位置。\n\n## 对 SoundManager.cpp 文件进行编码\n\n现在，我们可以对函数定义进行编码。构造函数和`playFire`函数有大量的代码，所以我们将分别来看。其他功能都很简短，所以我们将一次性处理它们。\n\n右键单击**解决方案资源管理器**中的**源文件**，并选择**添加|新项目...**。在**添加新项目**窗口中，突出显示(通过左键单击) **C++ 文件(。cpp)** 然后，在**名称**字段中，键入`SoundManager.cpp`。最后，点击**添加**按钮。我们现在准备为`SoundManager`类编码`.cpp`文件。\n\n### 构造函数的编码\n\n将包含指令和构造函数的以下代码添加到`SoundManager.cpp`:\n\n```cpp\n#include \"SoundManager.h\"\n#include <SFML/Audio.hpp>\nusing namespace sf;\nSoundManager::SoundManager()\n{\n    // Load the sound in to the buffers\n    m_FireBuffer.loadFromFile(\"sound/fire1.wav\");\n    m_FallInFireBuffer.loadFromFile(\"sound/fallinfire.wav\");\n    m_FallInWaterBuffer.loadFromFile(\"sound/fallinwater.wav\");\n    m_JumpBuffer.loadFromFile(\"sound/jump.wav\");\n    m_ReachGoalBuffer.loadFromFile(\"sound/reachgoal.wav\");\n    // Associate the sounds with the buffers\n    m_Fire1Sound.setBuffer(m_FireBuffer);\n    m_Fire2Sound.setBuffer(m_FireBuffer);\n    m_Fire3Sound.setBuffer(m_FireBuffer);\n    m_FallInFireSound.setBuffer(m_FallInFireBuffer);\n    m_FallInWaterSound.setBuffer(m_FallInWaterBuffer);\n    m_JumpSound.setBuffer(m_JumpBuffer);\n    m_ReachGoalSound.setBuffer(m_ReachGoalBuffer);\n\n    // When the player is 50 pixels away sound is full volume\n    float minDistance = 150;\n    // The sound reduces steadily as the player moves further away\n    float attenuation = 15;\n    // Set all the attenuation levels\n    m_Fire1Sound.setAttenuation(attenuation);\n    m_Fire2Sound.setAttenuation(attenuation);\n    m_Fire3Sound.setAttenuation(attenuation);\n    // Set all the minimum distance levels\n    m_Fire1Sound.setMinDistance(minDistance);\n    m_Fire2Sound.setMinDistance(minDistance);\n    m_Fire3Sound.setMinDistance(minDistance);\n    // Loop all the fire sounds\n    // when they are played\n    m_Fire1Sound.setLoop(true);\n    m_Fire2Sound.setLoop(true);\n    m_Fire3Sound.setLoop(true);\n}\n```\n\n在前面的代码中，我们将五个声音文件加载到五个`SoundBuffer`对象中。接下来，我们将八个`Sound`对象与一个`SoundBuffer`对象相关联。注意`m_Fire1Sound`、`m_Fire2Sound`、`m_Fire3Sound`都是从同一个`SoundBuffer`、`m_FireBuffer`开始玩。\n\n接下来，我们设置三种火音的衰减和最小距离。\n\n小费\n\n分别通过实验得出`150`和`15`的值。一旦游戏开始运行，建议通过改变这些值并观察(或者更确切地说，听到)差异来尝试这些值。\n\n最后，对于构造函数，我们在每个与火相关的`Sound`对象上使用`setLoop`函数。现在，当我们叫`play`的时候，他们会一直打下去。\n\n### 对 playFire 函数进行编码\n\n如下添加`playFire`功能。然后，我们可以讨论一下:\n\n```cpp\nvoid SoundManager::playFire(\n    Vector2f emitterLocation, Vector2f listenerLocation)\n{\n    // Where is the listener? Thomas.\n    Listener::setPosition(listenerLocation.x, \n        listenerLocation.y, 0.0f);\n    switch(m_NextSound)\n    {\n    case 1:\n        // Locate/move the source of the sound\n        m_Fire1Sound.setPosition(emitterLocation.x, \n            emitterLocation.y, 0.0f);\n        if (m_Fire1Sound.getStatus() == Sound::Status::Stopped)\n        {\n            // Play the sound, if its not already\n            m_Fire1Sound.play();\n        }\n        break;\n    case 2:\n        // Do the same as previous for the second sound\n        m_Fire2Sound.setPosition(emitterLocation.x, \n            emitterLocation.y, 0.0f);\n        if (m_Fire2Sound.getStatus() == Sound::Status::Stopped)\n        {\n            m_Fire2Sound.play();\n        }\n        break;\n    case 3:\n        // Do the same as previous for the third sound\n        m_Fire3Sound.setPosition(emitterLocation.x, \n            emitterLocation.y, 0.0f);\n        if (m_Fire3Sound.getStatus() == Sound::Status::Stopped)\n        {\n            m_Fire3Sound.play();\n        }\n        break;\n    }\n    // Increment to the next fire sound\n    m_NextSound++ ;\n    // Go back to 1 when the third sound has been started\n    if (m_NextSound > 3)\n    {\n        m_NextSound = 1;\n    }\n}\n```\n\n我们要做的第一件事是调用`Listener::setPosition`并根据作为参数传入的`Vector2f`设置监听器的位置。\n\n接下来，代码进入测试`m_NextSound`值的`switch`块。每个`case`声明都做了完全相同的事情，但是要么是`m_Fire1Sound`、`m_Fire2Sound`要么是`m_Fire3Sound`。\n\n在每个`case`块中，我们通过`setPosition`功能使用传入的参数设置发射器的位置。每个`case`块中代码的下一部分检查声音当前是否停止，如果是，播放声音。很快，我们将看到如何到达传递给这个函数的发射器和接收器的位置。\n\n`playFire`函数的最后一部分递增`m_NextSound`，并确保它只能等于 1、2 或 3，如`switch`块所要求的。\n\n### 对 SoundManager 的其余功能进行编码\n\n添加这四个简单的函数:\n\n```cpp\nvoid SoundManager::playFallInFire()\n{\n    m_FallInFireSound.setRelativeToListener(true);\n    m_FallInFireSound.play();\n}\nvoid SoundManager::playFallInWater()\n{\n    m_FallInWaterSound.setRelativeToListener(true);\n    m_FallInWaterSound.play();\n}\nvoid SoundManager::playJump()\n{\n    m_JumpSound.setRelativeToListener(true);\n    m_JumpSound.play();\n}\nvoid SoundManager::playReachGoal()\n{\n    m_ReachGoalSound.setRelativeToListener(true);\n    m_ReachGoalSound.play();\n}\n```\n\n`playFallInFire`、`playFallInWater`和`playReachGoal`功能只做两件事。首先，他们各自调用`setRelativeToListener`使音效不空间化，使音效“正常”，不具有方向性，然后在合适的`Sound`对象上调用`play`。\n\n`SoundManager`课到此结束。现在，我们可以在`Engine`类中使用它。\n\n# 在游戏引擎中添加声音管理器\n\n打开`Engine.h`文件，添加新的`SoundManager`类的实例，如下图高亮显示的代码所示:\n\n```cpp\n#pragma once\n#include <SFML/Graphics.hpp>\n#include \"TextureHolder.h\"\n#include \"Thomas.h\"\n#include \"Bob.h\"\n#include \"LevelManager.h\"\n#include \"SoundManager.h\"\nusing namespace sf;\nclass Engine\n{\nprivate:\n    // The texture holder\n    TextureHolder th;\n    // Thomas and his friend, Bob\n    Thomas m_Thomas;\n    Bob m_Bob;\n    // A class to manage all the levels\n    LevelManager m_LM;\n    // Create a SoundManager\n    SoundManager m_SM;\n    const int TILE_SIZE = 50;\n    const int VERTS_IN_QUAD = 4;\n```\n\n此时，我们可以使用`m_SM`来调用各种`play...`函数。不幸的是，为了管理发射器(火砖)的位置，还有一点工作要做。\n\n# 填充声音发射器\n\n打开`Engine.h`文件，为`populateEmitters`函数添加一个新原型，并为`Vector2f`对象添加一个新的 STL `vector`:\n\n```cpp\n    ...\n    ...\n    ...\n    // Run will call all the private functions\n    bool detectCollisions(PlayableCharacter& character);\n    // Make a vector of the best places to emit sounds from\n    void populateEmitters(vector <Vector2f>& vSoundEmitters,\n        int** arrayLevel);\n    // A vector of Vector2f for the fire emitter locations\n    vector <Vector2f> m_FireEmitters;\n\npublic:\n    ...\n    ...\n    ...\n```\n\n`populateEmitters`函数以一个`Vector2f`对象的`vector`为参数，还有一个指向`int`的指针(二维数组)。`vector`会将每个发射器的位置保持在一个水平。数组是保存级别布局的二维数组。\n\n## 对人口发射器功能进行编码\n\n`populateEmitters`功能的工作是扫描`arrayLevel`的所有元素，并决定发射器放在哪里。它将把结果储存在`m_FireEmitters`。\n\n右键单击**解决方案资源管理器**中的**源文件**，并选择**添加|新项目...**。在**添加新项目**窗口中，突出显示(通过左键单击) **C++ 文件(。cpp)** 然后，在**名称**字段中，键入`PopulateEmitters.cpp`。最后，点击**添加**按钮。现在，我们可以对新函数`populateEmitters`进行编码了。\n\n完整地添加代码。请务必像您一样研究代码，以便我们可以讨论它:\n\n```cpp\n#include \"Engine.h\"\nusing namespace sf;\nusing namespace std;\nvoid Engine::populateEmitters(\n    vector <Vector2f>& vSoundEmitters, \n   int** arrayLevel)\n{\n    // Make sure the vector is empty\n    vSoundEmitters.empty();\n    // Keep track of the previous emitter\n    // so we don't make too many\n    FloatRect previousEmitter;\n    // Search for fire in the level\n    for (int x = 0; x < (int)m_LM.getLevelSize().x; x++)\n    {\n        for (int y = 0; y < (int)m_LM.getLevelSize().y; y++)\n        {\n            if (arrayLevel[y][x] == 2)// fire is present\n            {\n                // Skip over any fire tiles too \n                // near a previous emitter\n                if (!FloatRect(x * TILE_SIZE,\n                    y * TILE_SIZE,\n                    TILE_SIZE,\n                    TILE_SIZE).intersects(previousEmitter))\n                {\n                    // Add the coordinates of this water block\n                    vSoundEmitters.push_back(\n                        Vector2f(x * TILE_SIZE, y * TILE_SIZE));\n                    // Make a rectangle 6 blocks x 6 blocks,\n                    // so we don't make any more emitters \n                    // too close to this one\n                    previousEmitter.left = x * TILE_SIZE;\n                    previousEmitter.top = y * TILE_SIZE;\n                    previousEmitter.width = TILE_SIZE * 6;\n                    previousEmitter.height = TILE_SIZE * 6;\n                }\n            }\n        }\n    }\n    return;\n}\n```\n\n乍一看，有些代码可能看起来很复杂。理解我们用来选择发射器位置的技术会使这变得更简单。在我们的关卡中，有大量的火砖。例如，在一个级别中，一个组中有 30 多个火砖。代码确保在给定的矩形内只有一个发射器。该矩形存储在`previousEmitter`中，为 300 像素乘 300 像素(`TILE_SIZE * 6`)。\n\n代码设置了一个嵌套的`for`循环，循环通过`arrayLevel`，寻找火砖。当它找到一个时，它确保它不与`previousEmitter`相交。只有到那时，它才会使用`pushBack`功能向`vSoundEmitters`添加另一个发射器。这样做之后，它还会更新`previousEmitter`以避免获得大簇的声音发射器。\n\n让我们制造一些噪音。\n\n# 播放声音\n\n打开`LoadLevel.cpp`文件，将调用添加到新的`populateEmitters`函数中，如下代码所示:\n\n```cpp\nvoid Engine::loadLevel()\n{\n    m_Playing = false;\n    // Delete the previously allocated memory\n    for (int i = 0; i < m_LM.getLevelSize().y; ++ i)\n    {\n        delete[] m_ArrayLevel[i];\n    }\n    delete[] m_ArrayLevel;\n    // Load the next 2d array with the map for the level\n    // And repopulate the vertex array as well\n    m_ArrayLevel = m_LM.nextLevel(m_VALevel);\n    // Prepare the sound emitters\n    populateEmitters(m_FireEmitters, m_ArrayLevel);\n    // How long is this new time limit\n    m_TimeRemaining = m_LM.getTimeLimit();\n    // Spawn Thomas and Bob\n    m_Thomas.spawn(m_LM.getStartPosition(), GRAVITY);\n    m_Bob.spawn(m_LM.getStartPosition(), GRAVITY);\n    // Make sure this code isn't run again\n    m_NewLevelRequired = false;\n}\n```\n\n第一个要添加的声音是跳跃声。我们记得键盘处理代码是在`Bob`和`Thomas`类中的纯虚函数中，并且`handleInput`函数在成功启动跳转时返回`true`。\n\n打开`Input.cpp`文件，添加以下高亮显示的代码行，在托马斯或鲍勃成功开始跳跃时播放跳跃声音:\n\n```cpp\n// Handle input specific to Thomas\nif (m_Thomas.handleInput())\n{\n    // Play a jump sound\n    m_SM.playJump();\n}\n// Handle input specific to Bob\nif (m_Bob.handleInput())\n{\n    // Play a jump sound\n    m_SM.playJump();\n}\n```\n\n打开`Update.cpp`文件，添加以下高亮显示的代码行，当托马斯和鲍勃同时达到当前级别的目标时，播放成功声音:\n\n```cpp\n// Detect collisions and see if characters have reached the goal tile\n// The second part of the if condition is only executed\n// when Thomas is touching the home tile\nif (detectCollisions(m_Thomas) && detectCollisions(m_Bob))\n{\n    // New level required\n    m_NewLevelRequired = true;\n    // Play the reach goal sound\n    m_SM.playReachGoal();\n}\nelse\n{\n    // Run Bobs collision detection\n    detectCollisions(m_Bob);\n}\n```\n\n此外，在`Update.cpp`文件中，我们将添加代码来循环通过`m_FireEmitters`向量，并决定何时需要调用`SoundManager`类的`playFire`函数。\n\n仔细观察新突出显示的代码周围的少量上下文。在正确的位置添加这段代码非常重要:\n\n```cpp\n}// End if playing\n// Check if a fire sound needs to be played\nvector<Vector2f>::iterator it;\n// Iterate through the vector of Vector2f objects\nfor (it = m_FireEmitters.begin(); it != m_FireEmitters.end(); it++)\n{\n    // Where is this emitter?\n    // Store the location in pos\n    float posX = (*it).x;\n    float posY = (*it).y;\n    // is the emitter near the player?\n    // Make a 500 pixel rectangle around the emitter\n    FloatRect localRect(posX - 250, posY - 250, 500, 500);\n    // Is the player inside localRect?\n    if (m_Thomas.getPosition().intersects(localRect))\n    {\n        // Play the sound and pass in the location as well\n        m_SM.playFire(Vector2f(posX, posY), m_Thomas.getCenter());\n    }\n}\n\n// Set the appropriate view around the appropriate character\n```\n\n前面的代码有点像声音的碰撞检测。每当托马斯在火发射器周围 500×500 像素的矩形内游荡时，就会调用`playFire`函数，传递发射器和托马斯的坐标。`playFire`功能完成剩下的工作，播放空间化的循环音效。\n\n打开`DetectCollisions.cpp`文件，找到合适的地方，添加下面高亮显示的代码。当任一角色落入水中或火中时，这两行高亮显示的代码会触发声音效果:\n\n```cpp\n// Has character been burnt or drowned?\n// Use head as this allows him to sink a bit\nif (m_ArrayLevel[y][x] == 2 || m_ArrayLevel[y][x] == 3)\n{\n    if (character.getHead().intersects(block))\n    {\n        character.spawn(m_LM.getStartPosition(), GRAVITY);\n        // Which sound should be played?\n        if (m_ArrayLevel[y][x] == 2)// Fire, ouch!\n        {\n            // Play a sound\n            m_SM.playFallInFire();\n        }\n        else // Water\n        {\n            // Play a sound\n            m_SM.playFallInWater();\n        }\n    }\n}\n```\n\n玩这个游戏现在可以让你听到所有的声音，包括当你靠近一个火砖的时候，很酷的空间感。\n\n# 实现平显类\n\n平视显示器超级简单，与僵尸竞技场项目相比没有什么不同。不同的是，我们将把所有代码打包到一个新的`HUD`类中。如果我们将所有`Font`、`Text`和其他变量声明为这个新类的成员，那么我们可以在构造函数中初始化它们，并为它们的所有值提供 getter 函数。这将使`Engine`类免受大量声明和初始化的影响。\n\n## 编码平视显示器\n\n首先，我们将使用所有成员变量和函数声明对`HUD.h`文件进行编码。右键单击**解决方案资源管理器**中的**头文件**，选择**添加|新项目...**。在**添加新项目**窗口中，突出显示(通过左键单击)**头文件(。h)** 然后在**名称**字段中，输入`HUD.h`。最后，点击**添加**按钮。我们现在准备为`HUD`类编码头文件。\n\n在`HUD.h`中添加以下代码:\n\n```cpp\n#pragma once\n#include <SFML/Graphics.hpp>\nusing namespace sf;\nclass Hud\n{\nprivate:\n    Font m_Font;\n    Text m_StartText;\n    Text m_TimeText;\n    Text m_LevelText;\npublic:\n    Hud();\n    Text getMessage();\n    Text getLevel();\n    Text getTime();\n    void setLevel(String text);\n    void setTime(String text);\n};\n```\n\n在前面的代码中，我们添加了一个`Font`实例和三个`Text`实例。`Text`对象将用于显示提示用户开始、剩余时间和当前级别号的信息。\n\n公共功能更有趣。首先，有一个构造函数，大部分代码都将在这里运行。构造器将初始化`Font`和`Text`对象，并将它们相对于当前屏幕分辨率定位在屏幕上。\n\n三个 getter 函数`getMessage`、`getLevel`和`getTime`将向调用代码返回一个`Text`对象，以便它可以将它们绘制到屏幕上。\n\n`setLevel`和`setTime`功能将分别用于更新`m_LevelText`和`m_TimeText`中显示的文本。\n\n现在，我们可以为刚刚声明的函数编写所有的定义。\n\n## 对 HUD.cpp 文件进行编码\n\n右键单击**解决方案资源管理器**中的**源文件**，并选择**添加|新项目...**。在**添加新项目**窗口中，突出显示(通过左键单击) **C++ 文件(。cpp)** 然后，在**名称**字段中，键入`HUD.cpp`。最后，点击**添加**按钮。我们现在准备为`HUD`类编码`.cpp`文件。\n\n添加 include 指令和以下代码。然后，我们将讨论它:\n\n```cpp\n#include \"Hud.h\"\nHud::Hud()\n{\n    Vector2u resolution;\n    resolution.x = VideoMode::getDesktopMode().width;\n    resolution.y = VideoMode::getDesktopMode().height;\n    // Load the font\n    m_Font.loadFromFile(\"fonts/Roboto-Light.ttf\");\n    // when Paused\n    m_StartText.setFont(m_Font);\n    m_StartText.setCharacterSize(100);\n    m_StartText.setFillColor(Color::White);\n    m_StartText.setString(\"Press Enter when ready!\");\n    // Position the text\n    FloatRect textRect = m_StartText.getLocalBounds();\n    m_StartText.setOrigin(textRect.left +\n        textRect.width / 2.0f,\n        textRect.top +\n        textRect.height / 2.0f);\n    m_StartText.setPosition(\n        resolution.x / 2.0f, resolution.y / 2.0f);\n    // Time\n    m_TimeText.setFont(m_Font);\n    m_TimeText.setCharacterSize(75);\n    m_TimeText.setFillColor(Color::White);\n    m_TimeText.setPosition(resolution.x - 150, 0);\n    m_TimeText.setString(\"------\");\n    // Level\n    m_LevelText.setFont(m_Font);\n    m_LevelText.setCharacterSize(75);\n    m_LevelText.setFillColor(Color::White);\n    m_LevelText.setPosition(25, 0);\n    m_LevelText.setString(\"1\");\n}\n```\n\n首先，我们将水平和垂直分辨率存储在名为`resolution`的`Vector2u`中。接下来，我们从`fonts`目录加载字体，我们在 [*第 14 章*](14.html#_idTextAnchor292)*抽象和代码管理–更好地利用 OOP* 。\n\n接下来的四行代码设置`m_StartText`的字体、颜色、大小和文本。在这之后的代码块捕获包裹`m_StartText`的矩形的大小，并执行计算，以计算出如何将其定位在屏幕的中心。如果你想更彻底的解释这部分代码，那么参考 [*第三章*](03.html#_idTextAnchor098)*c++ 字符串和 SFML 时间–玩家输入和 HUD* 。\n\n在构造函数的最后两个代码块中，设置了`m_TimeText`和`m_LevelText`的字体、文本大小、颜色、位置和实际文本。稍后，我们将看到这两个`Text`对象将通过两个 setter 函数进行更新，无论何时需要。\n\n在我们刚刚添加的代码下面添加以下 getter 和 setter 函数:\n\n```cpp\nText Hud::getMessage()\n{\n    return m_StartText;\n}\nText Hud::getLevel()\n{\n    return m_LevelText;\n}\nText Hud::getTime()\n{\n    return m_TimeText;\n}\nvoid Hud::setLevel(String text)\n{\n    m_LevelText.setString(text);\n}\nvoid Hud::setTime(String text)\n{\n    m_TimeText.setString(text);\n}\n```\n\n前面代码中的前三个函数只是返回适当的`Text`对象，即`m_StartText`、`m_LevelText`或`m_TimeText`。当我们将平视显示器绘制到屏幕上时，我们将很快使用这些功能。最后两个函数`setLevel`和`setTime`使用`setString`函数更新相应的`Text`对象，其值将从`Engine`类的`update`函数传入，每 500 帧更新一次。\n\n所有这些完成后，我们可以让平视显示器类在我们的游戏引擎中工作。\n\n# 使用抬头显示器类\n\n打开`Engine.h`，为我们的新类添加一个 include，声明一个新的`HUD`类的实例，并声明和初始化两个新的成员变量，它们将跟踪我们更新 HUD 的频率。正如我们在前面的项目中学到的，我们不需要每一帧都更新 HUD。\n\n在`Engine.h`中添加以下高亮显示的代码:\n\n```cpp\n#pragma once\n#include <SFML/Graphics.hpp>\n#include \"TextureHolder.h\"\n#include \"Thomas.h\"\n#include \"Bob.h\"\n#include \"LevelManager.h\"\n#include \"SoundManager.h\"\n#include \"HUD.h\"\nusing namespace sf;\nclass Engine\n{\nprivate:\n    // The texture holder\n    TextureHolder th;\n    // Thomas and his friend, Bob\n    Thomas m_Thomas;\n    Bob m_Bob;\n    // A class to manage all the levels\n    LevelManager m_LM;\n    // Create a SoundManager\n    SoundManager m_SM;\n    // The Hud\n    Hud m_Hud;\n    int m_FramesSinceLastHUDUpdate = 0;\n    int m_TargetFramesPerHUDUpdate = 500;\n    const int TILE_SIZE = 50;\n```\n\n接下来，我们需要给`Engine`类的`update`函数添加一些代码。打开`Update.cpp`并添加以下高亮显示的代码，每 500 帧更新一次抬头显示器:\n\n```cpp\n    // Set the appropriate view around the appropriate character\n    if (m_SplitScreen)\n    {\n        m_LeftView.setCenter(m_Thomas.getCenter());\n        m_RightView.setCenter(m_Bob.getCenter());\n    }\n    else\n    {\n        // Centre full screen around appropriate character\n        if (m_Character1)\n        {\n            m_MainView.setCenter(m_Thomas.getCenter());\n        }\n        else\n        {\n            m_MainView.setCenter(m_Bob.getCenter());\n        }\n    }\n    // Time to update the HUD?\n// Increment the number of frames since \n   // the last HUD calculation\n    m_FramesSinceLastHUDUpdate++ ;\n    // Update the HUD every m_TargetFramesPerHUDUpdate frames\n    if (m_FramesSinceLastHUDUpdate > m_TargetFramesPerHUDUpdate)\n    {\n        // Update game HUD text\n        stringstream ssTime;\n        stringstream ssLevel;\n        // Update the time text\n        ssTime << (int)m_TimeRemaining;\n        m_Hud.setTime(ssTime.str());\n        // Update the level text\n        ssLevel << \"Level:\" << m_LM.getCurrentLevel();\n        m_Hud.setLevel(ssLevel.str());\n        m_FramesSinceLastHUDUpdate = 0;\n    }\n}// End of update function\n```\n\n在前面的代码中，`m_FramesSinceLastUpdate`每帧递增一次。当`m_FramesSinceLastUpdate`超过`m_TargetFramesPerHUDUpdate`时，执行进入`if`块。在`if`块中，我们使用`stringstream`对象来更新我们的`Text`，就像我们在之前的项目中所做的那样。在这个项目中，我们使用的是`HUD`类，所以我们通过传入`Text`对象需要设置的当前值来调用`setTime`和`setLevel`函数。\n\n`if`块的最后一步是将`m_FramesSinceLastUpdate`设置回零，以便它可以开始向下一次更新计数。\n\n最后，打开`Draw.cpp`文件，添加以下高亮显示的代码，绘制每一帧的 HUD:\n\n```cpp\n    else\n    {\n        // Split-screen view is active\n        // First draw Thomas' side of the screen\n        // Switch to background view\n        m_Window.setView(m_BGLeftView);\n        // Draw the background\n        m_Window.draw(m_BackgroundSprite);\n        // Switch to m_LeftView\n        m_Window.setView(m_LeftView);\n        // Draw the Level\n        m_Window.draw(m_VALevel, &m_TextureTiles);\n\n        // Draw thomas\n        m_Window.draw(m_Bob.getSprite());\n        // Draw thomas\n        m_Window.draw(m_Thomas.getSprite());\n\n        // Now draw Bob's side of the screen\n        // Switch to background view\n        m_Window.setView(m_BGRightView);\n        // Draw the background\n        m_Window.draw(m_BackgroundSprite);\n        // Switch to m_RightView\n        m_Window.setView(m_RightView);\n        // Draw the Level\n        m_Window.draw(m_VALevel, &m_TextureTiles);\n        // Draw thomas\n        m_Window.draw(m_Thomas.getSprite());\n        // Draw bob\n        m_Window.draw(m_Bob.getSprite());\n\n    }\n    // Draw the HUD\n    // Switch to m_HudView\n    m_Window.setView(m_HudView);\n    m_Window.draw(m_Hud.getLevel());\n    m_Window.draw(m_Hud.getTime());\n    if (!m_Playing)\n    {\n        m_Window.draw(m_Hud.getMessage());\n    }\n    // Show everything we have just drawn\n    m_Window.display();\n}// End of draw\n```\n\n上面的代码通过使用抬头显示器类中的 getter 函数来绘制抬头显示器。请注意，绘制提示玩家开始的消息的调用仅在游戏当前未玩`(!m_Playing)`时使用。\n\n运行游戏，玩几个关卡，看时间滴答下降，关卡滴答上升。当你再次回到 1 级时，注意你的时间比以前少了 10%。\n\n# 总结\n\n在这一章中，我们探讨了声音的空间化。我们的“托马斯迟到了”游戏现在不仅完全可以玩了，而且我们还增加了定向音效和一个简单但信息丰富的平视显示器。我们还可以轻松地添加新的级别。至此，我们可以收工了。\n\n多加一点火花就好了。在下一章中，我们将研究两个游戏概念。首先，我们将看看粒子系统，这是我们处理爆炸或其他特殊效果的方式。为了实现这一点，我们需要多学一点 C++。正因如此，才会引入多重继承的话题。\n\n之后，当我们了解 OpenGL 和可编程图形管道时，我们将为游戏添加最后的繁荣。然后，我们将能够接触到 **GLSL** 语言，它允许我们编写直接在 GPU 上执行的代码，这样我们就可以创建一些特殊效果。"
  },
  {
    "path": "docs/begin-cpp-game-prog/18.md",
    "content": "# 十八、粒子系统和着色器\n\n在这一章中，我们将看看什么是粒子系统，然后将它编码到我们的游戏中。我们将触及 OpenGL 着色器主题的表面，看看如何用另一种语言( **GLSL** )编写可以直接在显卡上运行的代码，以获得流畅的图形效果，否则这可能是不可能的。像往常一样，我们也将使用我们的新技能和知识来增强当前的项目。\n\n在本章中，我们将涵盖以下主题:\n\n*   构建粒子系统\n*   OpenGL 着色器和 GLSL\n*   在托马斯迟到游戏中使用着色器\n\n# 构建粒子系统\n\n在我们开始编码之前，看看我们到底想要实现什么是有帮助的。\n\n请看下图:\n\n![](img/B14278_18_01.jpg)\n\n上图是素色背景上粒子效果的截图。我们将在我们的游戏中使用这个效果。我们会在玩家每次死亡时产生一个这样的效果。\n\n我们实现这一效果的方式如下:\n\n1.  首先，我们在选定的像素位置生成 1000 个点(粒子)，一个在另一个之上。\n2.  游戏的每一帧以预定但随机的速度和角度向外移动 1000 个粒子中的每一个。\n3.  重复第二步两秒钟，然后让粒子消失。\n\n我们将使用一个`VertexArray`来绘制所有的点，并使用`Point`的原始类型来直观地表示每个粒子。此外，我们将继承 SFML `Drawable`类，这样我们的粒子系统就可以处理绘图本身。\n\n## 对粒子类进行编码\n\n`Particle`类将是一个简单的类，只代表一千个粒子中的一个粒子。让我们开始编码。\n\n### 编码粒子\n\n右键单击**解决方案资源管理器**中的**头文件**，选择**添加|新项目...**。在**添加新项目**窗口中，突出显示(通过左键单击)**头文件(。h)** 然后在**名称**字段中，输入`Particle.h`。最后，点击**添加**按钮。我们现在准备为`Particle`类编码头文件。\n\n将以下代码添加到`Particle.h`文件中:\n\n```cpp\n#pragma once\n#include <SFML/Graphics.hpp>\nusing namespace sf;\nclass Particle\n{\nprivate:\n    Vector2f m_Position;\n    Vector2f m_Velocity;\npublic:\n    Particle(Vector2f direction);\n    void update(float dt);\n    void setPosition(Vector2f position);\n    Vector2f getPosition();\n};\n```\n\n在前面的代码中，我们有两个`Vector2f`对象。一个将代表粒子的水平和垂直坐标，而另一个将代表水平和垂直速度。\n\n重要说明\n\n当您在多个方向上有变化率(速度)时，组合值也定义了一个方向。这叫做**速度**。因此`Vector2f`被称为`m_Velocity`。\n\n我们还有几个公共功能。首先是构造函数。它需要一个`Vector2f`并利用这个让它知道这个粒子会有哪个方向/速度。这意味着选择速度的是系统，而不是粒子本身。\n\n接下来是`update`功能，它占用前一帧所占用的时间。我们将使用它精确地移动粒子正确的量。\n\n最后两个函数`setPosition`和`getPosition`分别用于移动粒子的位置和找出其位置。\n\n当我们对这些函数进行编码时，它们都将完全有意义。\n\n### 对粒子文件进行编码\n\n右键单击**解决方案资源管理器**中的**源文件**，并选择**添加|新项目...**。在**添加新项目**窗口中，突出显示(通过左键单击) **C++ 文件(。cpp)** 然后，在**名称**字段中，键入`Particle.cpp`。最后，点击**添加**按钮。我们现在准备为`Particle`类编码`.cpp`文件。\n\n在`Particle.cpp`中添加以下代码:\n\n```cpp\n#include \"Particle.h\"\nParticle::Particle(Vector2f direction)\n{\n    // Determine the direction\n\n    m_Velocity.x = direction.x;\n    m_Velocity.y = direction.y;\n}\nvoid Particle::update(float dtAsSeconds)\n{\n    // Move the particle\n    m_Position += m_Velocity * dtAsSeconds;\n}\nvoid Particle::setPosition(Vector2f position)\n{\n    m_Position = position;\n}\nVector2f Particle::getPosition()\n{\n    return m_Position;\n}\n```\n\n所有这些函数都使用我们以前见过的概念。构造函数使用在`Vector2f`对象中传递的来设置`m_Velocity.x`和`m_Velocity.y`值。\n\n`update`功能通过将`m_Velocity`乘以经过的时间(`dtAsSeconds`)来移动粒子的水平和垂直位置。请注意，要实现这一点，我们只需将两个`Vector2f`对象添加在一起。不需要分别为 x 和 y 成员执行计算。\n\n如前所述，`setPosition`函数用传入的值初始化`m_Position`对象。`getPosition`功能返回`m_Position`到调用代码。\n\n我们现在有了一个功能齐全的`Particle`类。接下来，我们将编写一个`ParticleSystem`类来产生和控制粒子。\n\n## 对粒子系统类进行编码\n\n`ParticleSystem`类为我们的粒子效果做了大部分工作。我们将在`Engine`类中创建的实例就是这个类。然而，在此之前，让我们再多谈一谈面向对象编程和 SFML `Drawable`课程。\n\n# 探索 SFML 的可绘制类和面向对象程序设计\n\n`Drawable`类只有一个功能。它也没有变量。此外，它唯一的功能是纯虚拟的。这意味着，如果我们从`Drawable`继承，我们必须实现它唯一的功能。从第 14 章[](14.html#_idTextAnchor292)**抽象和代码管理——更好地利用 OOP* 中可以看出，我们可以将继承自`drawable`的类用作多态类型。更简单地说，SFML 允许我们用`Drawable`对象做的任何事情，我们都可以用从它继承的类来做。唯一的要求是我们必须为纯虚函数提供一个定义，`draw`。*\n\n *一些继承自`Drawable`的职业已经包括`Sprite`和`VertexArray`(以及其他)。每当我们使用`Sprite`或`VertexArray`时，我们都将其传递给`RenderWindow`类的`draw`功能。\n\n我们之所以能够在这整本书里画出我们曾经画过的每一个物体，是因为它们都是从`Drawable`继承而来的。我们可以利用这些知识。\n\n我们可以用任何喜欢的对象从`Drawable`继承，只要实现纯虚的`draw`功能。这也是一个简单的过程。考虑一个假设的`SpaceShip`类。从`Drawable`继承的`SpaceShip`类的头文件(`SpaceShip.h`)如下所示:\n\n```cpp\nclass SpaceShip : public Drawable\n{\nprivate:\n    Sprite m_Sprite;\n    // More private members\npublic:\n    virtual void draw(RenderTarget& target, \n        RenderStates states) const;\n    // More public members\n};\n```\n\n在前面的代码中，我们可以看到纯虚拟的`draw`函数和一个`Sprite`实例。请注意，在课堂之外没有办法进入私人的`Sprite`，甚至连`getSprite`功能都没有！\n\n`SpaceShip.cpp`文件看起来像这样:\n\n```cpp\nvoid SpaceShip::SpaceShip\n{\n    // Set up the spaceship\n}\nvoid SpaceShip::draw(RenderTarget& target, RenderStates states) const\n{\n    target.draw(m_Sprite, states);\n}\n// Any other functions\n```\n\n在前面的代码中，请注意`draw`函数的简单实现。参数超出了本书的范围。只需注意`target`参数用于调用`draw`，传入`m_Sprite`以及另一个参数`states`。\n\n小费\n\n虽然没有必要了解充分利用`Drawable`的参数，但在本书的上下文中，您可能会感兴趣。你可以在 SFML 网站上读到更多关于 SFML 的信息。\n\n在主游戏循环中，我们现在可以将`SpaceShip`实例视为`Sprite`或从`Drawable`继承的任何其他类，如下所示:\n\n```cpp\nSpaceShip m_SpaceShip;\n// create other objects here\n// ...\n// In the draw function\n// Rub out the last frame\nm_Window.clear(Color::Black);\n// Draw the spaceship\nm_Window.draw(m_SpaceShip);\n// More drawing here\n// ...\n// Show everything we have just drawn\nm_Window.display();\n```\n\n因为`SpaceShip` **是** `Drawable`所以我们可以把它当作`Sprite`或者`VertexArray`来对待，因为我们否决了纯虚的`draw`函数，所以一切都按照我们希望的那样运行。在本章中，您将使用这种方法来绘制粒子系统。\n\n在我们讨论面向对象程序设计的时候，让我们来看看将绘图代码封装到游戏对象中的另一种方法，我们将在下一个项目中使用它。\n\n## 从可绘制继承的替代方案\n\n也有可能通过实现我们自己的函数，在我们的类中，通过使用下面的代码，将所有的绘图功能保留在作为要绘制的对象的类中:\n\n```cpp\nvoid drawThisObject(RenderWindow window)\n{\n    window.draw(m_Sprite)\n}\n```\n\n前面的代码假设`m_Sprite`代表我们正在绘制的当前类的视觉外观，就像它在这个项目和上一个项目中一样。假设包含`drawThisObject`函数的类的实例被称为`playerHero`，并且进一步假设我们有一个名为`m_Window`的`RenderWindow`的实例，那么我们可以使用以下代码从主游戏循环中绘制对象:\n\n```cpp\n playerHero.draw(m_Window);\n```\n\n在这个解决方案中，我们将`RenderWindow`、`m_Window`作为参数传递到`drawThisObject`函数中。`drawThisObject` 功能然后使用`RenderWindow`绘制`Sprite`、`m_Sprite`。\n\n如果我们有一组更复杂的游戏对象，那么传递一个`RenderWindow`的引用给要绘制的对象，每一帧，这样它就可以自己绘制，是一个很好的战术。\n\n我们将在本书的最后一个项目中使用这种策略，我们将在下一章开始。让我们通过编码`ParticleSystem`类来完成粒子系统，这个类将继承自`Drawable`。\n\n### 编码粒子系统\n\n右键单击**解决方案资源管理器**中的**头文件**，选择**添加|新项目...**。在**添加新项目**窗口中，突出显示(通过左键单击)**头文件(。h)** 然后在**名称**字段中，输入`ParticleSystem.h`。最后，点击**添加**按钮。我们现在准备为`ParticleSystem`类编码头文件。\n\n将`ParticleSystem`类的代码添加到`ParticleSystem.h`中:\n\n```cpp\n#pragma once\n#include <SFML/Graphics.hpp>\n#include \"Particle.h\"\nusing namespace sf;\nusing namespace std;\nclass ParticleSystem : public Drawable\n{\nprivate:\n    vector<Particle> m_Particles;\n    VertexArray m_Vertices;\n    float m_Duration;\n    bool m_IsRunning = false;\npublic:\n    virtual void draw(RenderTarget& target, \n      RenderStates states) const;\n\n    void init(int count);\n    void emitParticles(Vector2f position);\n    void update(float elapsed);\n    bool running();\n};\n```\n\n让我们一点一点来看这个。首先，请注意我们继承了 SFML 的`Drawable`类。这将允许我们将我们的`ParticleSystem`实例传递给`m_Window.draw`，因为`ParticleSystem` **是一个** `Drawable`。而且，由于我们从`Drawable`继承，我们可以使用与`Drawable`类内部使用的相同的函数签名来覆盖`draw`函数。很快，当我们使用`ParticleSystem`类时，我们将看到以下代码。\n\n```cpp\nm_Window.draw(m_PS);\n```\n\n`m_PS`对象是我们的`ParticleSystem`类的一个实例，我们将它直接传递给`RenderWindow`类的`draw`函数，就像我们对`Sprite`、`VertexArray`和`RectangleShape`实例所做的一样。继承和多态的力量使这一切成为可能。\n\n小费\n\n暂时不要添加`m_Window.draw…`代码；我们首先还有一点工作要做。\n\n有一个名为`Particle`类型的`m_Particles`的向量。这个向量将保存`Particle`的每一个实例。接下来，我们有一个`VertexArray`叫做`m_Vertices`。这将用于以一整串`Point`图元的形式绘制所有粒子。\n\n`m_Duration`、`float`变量是每个效果持续的时间。我们将在构造函数中初始化它。\n\n`m_IsRunning`布尔变量将用于指示粒子系统当前是否正在使用。\n\n接下来，在公共部分，我们有纯虚拟函数`draw`，我们将很快实现它来处理当我们将`ParticleSystem`的实例传递给`m_Window.draw`时发生的事情。\n\n`init`功能将准备`VertexArray`和`vector`。它还将初始化所有`Particle`物体(由`vector`持有)的速度和初始位置。\n\n`update`函数将遍历`vector`中的每个`Particle`实例，并调用它们各自的`update`函数。\n\n`running`功能提供对`m_IsRunning`变量的访问，以便游戏引擎可以查询`ParticleSystem`当前是否在使用。\n\n让我们对函数定义进行编码，看看`ParticleSystem`里面发生了什么。\n\n### 粒子系统. cpp 文件的编码\n\n右键单击**解决方案资源管理器**中的**源文件**，并选择**添加|新项目...**。在**添加新项目**窗口中，突出显示(通过左键单击) **C++ 文件(。cpp)** 然后，在**名称**字段中，键入`ParticleSystem.cpp`。最后，点击**添加**按钮。我们现在准备为`ParticleSystem`类编码`.cpp`文件。\n\n我们将把这个文件分成五个部分，这样我们就可以更详细地编码和讨论它。添加第一段代码，如下所示:\n\n```cpp\n#include <SFML/Graphics.hpp>\n#include \"ParticleSystem.h\"\nusing namespace sf;\nusing namespace std;\nvoid ParticleSystem::init(int numParticles)\n{\n    m_Vertices.setPrimitiveType(Points);\n    m_Vertices.resize(numParticles);\n    // Create the particles\n    for (int i = 0; i < numParticles; i++)\n    {\n        srand(time(0) + i);\n        float angle = (rand() % 360) * 3.14f / 180.f;\n        float speed = (rand() % 600) + 600.f;\n        Vector2f direction;\n        direction = Vector2f(cos(angle) * speed,\n            sin(angle) * speed);\n        m_Particles.push_back(Particle(direction));\n    }\n}\n```\n\n经过必要的`includes`，我们有了 `init`函数的定义。我们称`setPrimitiveType`为`Points`作为参数，以便`m_VertexArray`知道它将处理什么类型的原语。我们用`numParticles`调整`m_Vertices`的大小，调用`init`函数时传递给了它。\n\n`for`循环为速度和角度创建随机值。然后，它使用三角函数将这些值转换成存储在`Vector2f`、`direction`中的向量。\n\n小费\n\n如果你想了解更多关于三角函数(`cos`和`sin`)如何将角度和速度转换成矢量的知识，那么你可以看看这篇文章系列:[http://gamecode school . com/essentials/computing-heading-in-2d-games-use-三角函数-part-1/](http://gamecodeschool.com/essentials/calculating-heading-in-2d-games-using-trigonometric-functions-part-1/) 。\n\n在`for`循环(和`init`函数)中发生的最后一件事是向量被传递到`Particle`构造器中。使用`push_back`功能，新的`Particle`实例存储在`m_Particles`中。因此，调用值为`1000`的`init`就意味着我们有 1000 个`Particle`的实例，随机速度，藏在`m_Particles`里，只等着爆炸！\n\n接下来，在`ParticleSysytem.cpp`中增加`update`功能:\n\n```cpp\nvoid ParticleSystem::update(float dt)\n{\n    m_Duration -= dt;\n    vector<Particle>::iterator i;\n    int currentVertex = 0;\n    for (i = m_Particles.begin(); i != m_Particles.end(); i++)\n    {\n        // Move the particle\n        (*i).update(dt);\n        // Update the vertex array\n        m_Vertices[currentVertex++ ].position = i->getPosition();\n    }\n    if (m_Duration < 0)\n    {\n        m_IsRunning = false;\n    }\n}\n```\n\n`update`功能比乍一看要简单。首先，`m_Duration`是通过时间流逝而减少的，`dt`。这样我们就能知道两秒钟过去了。向量迭代器`i`被声明为与`m_Particles`一起使用。\n\n`for`循环经过`m_Particles`中的每个`Particle`实例。对于每一个，它调用其`update`函数并传入`dt`。每个粒子都会更新它的位置。粒子自我更新后，`m_Vertices`中适当的顶点会使用粒子的`getPosition`功能进行更新。在每次通过`for`循环结束时，`currentVertex`递增，为下一个顶点做好准备。\n\n`for`循环完成编码后，`if(m_Duration < 0)`检查是否到了关闭效果的时间。如果两秒钟过去了，`m_IsRunning`被设置为`false`。\n\n接下来，添加`emitParticles`功能:\n\n```cpp\nvoid ParticleSystem::emitParticles(Vector2f startPosition)\n{\n    m_IsRunning = true;\n    m_Duration = 2;\n\n    int currentVertex = 0;\n    for (auto it = m_Particles.begin(); \n         it != m_Particles.end();\n         it++)\n    {\n        m_Vertices[currentVertex++ ].color = Color::Yellow;\n        it->setPosition(startPosition);\n    }\n}\n```\n\n这是我们将调用的启动粒子系统的函数。所以可以预见的是，我们将`m_IsRunning`设置为`true`，将`m_Duration`设置为`2`。我们声明一个`iterator`、`i`，来遍历`m_Particles`中的所有`Particle`对象，然后我们在`for`循环中这样做。\n\n在`for`循环中，我们将顶点数组中的每个粒子设置为黄色，并将每个位置设置为`startPosition`，作为参数传入。请记住，每个粒子在相同的位置开始生命，但它们各自被赋予不同的速度。\n\n接下来，添加纯虚`draw`函数定义:\n\n```cpp\nvoid ParticleSystem::\n       draw(RenderTarget& target, \n       RenderStates states) const\n{\n    target.draw(m_Vertices, states);\n}\n```\n\n在前面的代码中，我们简单地使用`target`来调用`draw`，传递`m_Vertices`和`states`作为参数。记住我们永远不会直接调用这个函数！很快，当我们声明一个`ParticleSystem`的实例时，我们将把这个实例传递给`RenderWindow draw`函数。我们刚刚编码的`draw`函数将从那里内部调用。\n\n最后，增加`running`功能:\n\n```cpp\nbool ParticleSystem::running()\n{\n    return m_IsRunning;\n}\n```\n\n`running`函数是一个简单的 getter 函数，返回`m_IsRunning`的值。我们将在本章中看到这在哪里有用，这样我们就可以确定粒子系统的当前状态。\n\n## 使用粒子系统对象\n\n把我们的粒子\n\n系统工作起来非常简单，尤其是因为我们继承了`Drawable`。\n\n### 向引擎类添加粒子系统对象\n\n打开`Engine.h`并添加一个`ParticleSystem`对象，如下图高亮显示的代码所示:\n\n```cpp\n#pragma once\n#include <SFML/Graphics.hpp>\n#include \"TextureHolder.h\"\n#include \"Thomas.h\"\n#include \"Bob.h\"\n#include \"LevelManager.h\"\n#include \"SoundManager.h\"\n#include \"HUD.h\"\n#include \"ParticleSystem.h\"\nusing namespace sf;\nclass Engine\n{\nprivate:\n    // The texture holder\n    TextureHolder th;\n    // create a particle system\n    ParticleSystem m_PS;\n    // Thomas and his friend, Bob\n    Thomas m_Thomas;\n    Bob m_Bob;\n```\n\n现在，我们需要初始化系统。\n\n### 初始化粒子系统\n\n打开`Engine.cpp`文件，在`Engine`构造函数的末尾添加短的高亮代码:\n\n```cpp\nEngine::Engine()\n{\n    // Get the screen resolution and create an SFML window and View\n    Vector2f resolution;\n    resolution.x = VideoMode::getDesktopMode().width;\n    resolution.y = VideoMode::getDesktopMode().height;\n    m_Window.create(VideoMode(resolution.x, resolution.y),\n        \"Thomas was late\",\n        Style::Fullscreen);\n    // Initialize the full screen view\n    m_MainView.setSize(resolution);\n    m_HudView.reset(\n        FloatRect(0, 0, resolution.x, resolution.y));\n    // Initialize the split-screen Views\n    m_LeftView.setViewport(\n        FloatRect(0.001f, 0.001f, 0.498f, 0.998f));\n    m_RightView.setViewport(\n        FloatRect(0.5f, 0.001f, 0.499f, 0.998f));\n    m_BGLeftView.setViewport(\n        FloatRect(0.001f, 0.001f, 0.498f, 0.998f));\n    m_BGRightView.setViewport(\n        FloatRect(0.5f, 0.001f, 0.499f, 0.998f));\n    // Can this graphics card use shaders?\n    if (!sf::Shader::isAvailable())\n    {\n        // Time to get a new PC\n        m_Window.close();\n    }\n    m_BackgroundTexture = TextureHolder::GetTexture(\n        \"graphics/background.png\");\n    // Associate the sprite with the texture\n    m_BackgroundSprite.setTexture(m_BackgroundTexture);\n    // Load the texture for the background vertex array\n    m_TextureTiles = TextureHolder::GetTexture(\n        \"graphics/tiles_sheet.png\");\n    // Initialize the particle system\n    m_PS.init(1000);\n}// End Engine constructor\n```\n\n`Particle`实例的`VertexArray`和`vector`已准备好采取行动。\n\n### 每帧更新粒子系统\n\n打开`Update.cpp`文件，添加以下高亮显示的代码。它可以在`update`功能结束时直接进入:\n\n```cpp\n    // Update the HUD every m_TargetFramesPerHUDUpdate frames\n    if (m_FramesSinceLastHUDUpdate > m_TargetFramesPerHUDUpdate)\n    {\n        // Update game HUD text\n        stringstream ssTime;\n        stringstream ssLevel;\n        // Update the time text\n        ssTime << (int)m_TimeRemaining;\n        m_Hud.setTime(ssTime.str());\n        // Update the level text\n        ssLevel << \"Level:\" << m_LM.getCurrentLevel();\n        m_Hud.setLevel(ssLevel.str());\n        m_FramesSinceLastHUDUpdate = 0;\n    }\n    // Update the particles\n    if (m_PS.running())\n    {\n        m_PS.update(dtAsSeconds);\n    }\n}// End of update function\n```\n\n前面的代码只需要调用`update`。请注意，它包含在检查中，以确保系统当前正在运行。如果它没有运行，更新它是没有意义的。\n\n### 启动粒子系统\n\n打开`DetectCollisions.cpp`文件，里面有`detectCollisions`功能。我们最初编码的时候在里面留了一个注释。\n\n根据上下文确定正确的位置，并添加以下突出显示的代码:\n\n```cpp\n// Is character colliding with a regular block\nif (m_ArrayLevel[y][x] == 1)\n{\n    if (character.getRight().intersects(block))\n    {\n        character.stopRight(block.left);\n    }\n    else if (character.getLeft().intersects(block))\n    {\n        character.stopLeft(block.left);\n    }\n    if (character.getFeet().intersects(block))\n    {\n        character.stopFalling(block.top);\n    }\n    else if (character.getHead().intersects(block))\n    {\n        character.stopJump();\n    }\n}\n// More collision detection here once \n// we have learned about particle effects\n// Have the characters' feet touched fire or water?\n// If so, start a particle effect\n// Make sure this is the first time we have detected this\n// by seeing if an effect is already running            \nif (!m_PS.running()) {\n    if (m_ArrayLevel[y][x] == 2 || m_ArrayLevel[y][x] == 3)\n    {\n        if (character.getFeet().intersects(block))\n        {\n            // position and start the particle system\n            m_PS.emitParticles(character.getCenter());\n        }\n    }\n}\n// Has the character reached the goal?\nif (m_ArrayLevel[y][x] == 4)\n{\n    // Character has reached the goal\n    reachedGoal = true;\n}\n```\n\n首先，代码检查粒子系统是否已经在运行。如果不是，它会检查正在检查的当前图块是水图块还是火图块。如果是这两种情况，它会检查角色的脚是否与它接触。当这些`if`语句都为真时，粒子系统通过调用`emitParticles`函数并传入角色中心的位置作为坐标来启动效果。\n\n### 绘制粒子系统\n\n这是最好的部分。看看画`ParticleSystem`有多容易。在检查粒子系统正在运行之后，我们将实例直接传递给`m_Window.draw`函数。\n\n打开`Draw.cpp`文件，在所有需要的地方添加以下高亮显示的代码:\n\n```cpp\nvoid Engine::draw()\n{\n    // Rub out the last frame\n    m_Window.clear(Color::White);\n    if (!m_SplitScreen)\n    {\n        // Switch to background view\n        m_Window.setView(m_BGMainView);\n        // Draw the background\n        m_Window.draw(m_BackgroundSprite);\n        // Switch to m_MainView\n        m_Window.setView(m_MainView);        \n        // Draw the Level\n        m_Window.draw(m_VALevel, &m_TextureTiles);\n        // Draw thomas\n        m_Window.draw(m_Thomas.getSprite());\n        // Draw bob\n        m_Window.draw(m_Bob.getSprite());\n        // Draw the particle system\n        if (m_PS.running())\n        {\n            m_Window.draw(m_PS);\n        }\n    }\n    else\n    {\n        // Split-screen view is active\n        // First draw Thomas' side of the screen\n        // Switch to background view\n        m_Window.setView(m_BGLeftView);\n        // Draw the background\n        m_Window.draw(m_BackgroundSprite);\n        // Switch to m_LeftView\n        m_Window.setView(m_LeftView);\n        // Draw the Level\n        m_Window.draw(m_VALevel, &m_TextureTiles);\n\n        // Draw bob\n        m_Window.draw(m_Bob.getSprite());\n        // Draw thomas\n        m_Window.draw(m_Thomas.getSprite());\n        // Draw the particle system\n        if (m_PS.running())\n        {\n            m_Window.draw(m_PS);\n        }\n\n        // Now draw Bob's side of the screen\n        // Switch to background view\n        m_Window.setView(m_BGRightView);\n        // Draw the background\n        m_Window.draw(m_BackgroundSprite);\n        // Switch to m_RightView\n        m_Window.setView(m_RightView);\n        // Draw the Level\n        m_Window.draw(m_VALevel, &m_TextureTiles);\n        // Draw thomas\n        m_Window.draw(m_Thomas.getSprite());\n        // Draw bob\n        m_Window.draw(m_Bob.getSprite());\n        // Draw the particle system\n        if (m_PS.running())\n        {\n            m_Window.draw(m_PS);\n        }\n\n    }\n\n    // Draw the HUD\n    // Switch to m_HudView\n    m_Window.setView(m_HudView);\n    m_Window.draw(m_Hud.getLevel());\n    m_Window.draw(m_Hud.getTime());\n    if (!m_Playing)\n    {\n        m_Window.draw(m_Hud.getMessage());\n    }\n\n    // Show everything we have just drawn\n    m_Window.display();\n}\n```\n\n请注意，我们必须在所有的左、右和全屏代码块中绘制粒子系统。\n\n运行游戏，将角色的一只脚移到火砖的边缘。注意粒子系统爆发出生命:\n\n![](img/B14278_18_02.jpg)\n\n不 w，是时候做一些新的事情了。\n\n# OpenGL、着色器和 GLSL\n\n**开放图形库** ( **OpenGL** )是一个处理 2D 和 3D 图形的编程库。OpenGL 适用于所有主要的桌面操作系统，还有一个版本适用于移动设备，称为 OpenGL ES。\n\nOpenGL 最初发布于 1992 年。经过二十多年的提炼和完善。此外，显卡制造商设计他们的硬件，使其与 OpenGL 配合良好。提到这一点并不是为了历史课，而是为了解释，试图改进 OpenGL 并在桌面上的 2D(和 3D 游戏)中使用它将是徒劳的，尤其是如果我们希望我们的游戏不仅仅在窗口上运行，这是显而易见的选择。我们已经在使用 OpenGL 了，因为 SFML 使用的是 OpenGL。着色器是在图形处理器本身上运行的程序。我们将在下一节中找到关于它们的更多信息。\n\n## 可编程管道和着色器\n\n通过 OpenGL，我们可以访问所谓的**可编程管道**。我们可以用`RenderWindow`实例的`draw`功能发送我们要绘制的图形，每一帧。在调用`draw`后，我们还可以编写运行在图形处理器上的代码，独立处理每个像素。这是一个非常强大的功能。\n\n这个运行在图形处理器上的额外代码被称为**着色器程序**。我们可以在**顶点着色器**中编写代码来操作图形的几何形状(位置)。我们还可以编写代码，在代码中单独操纵每个像素的外观。这就是所谓的**片段着色器**。\n\n虽然我们不会深入探索着色器，但我们将使用 **GL 着色器语言** ( **GLSL** )编写一些着色器代码，我们将一窥它提供的可能性。\n\n在 OpenGL 中，一切都是点、线或三角形。此外，我们可以将颜色和纹理附加到这个基本的几何图形上，我们还可以将这些元素结合起来，制作出我们在当今现代游戏中看到的复杂图形。这些统称为**原语**。我们可以通过 SFML 原语和`VertexArray`以及`Sprite`和`Shape`类访问 OpenGL 原语。\n\n除了图元，OpenGL 还使用矩阵。矩阵是一种执行算术的方法和结构。这种算法可以是极其简单的高中水平计算，例如移动(转换)坐标，也可以是相当复杂的，例如执行更高级的数学运算，例如将我们的游戏世界坐标转换成 GPU 可以使用的 OpenGL 屏幕坐标。幸运的是，SFML 在幕后为我们处理的正是这种复杂性。SFML 还允许我们直接处理 OpenGL。\n\n小费\n\n如果你想了解更多关于 OpenGL 的知识，可以从这里开始:[http://learnopengl.com/#!Introduction](http://learnopengl.com/#!Introduction)。如果你想直接用 OpenGL，旁边还有 SFML，可以看这篇文章了解更多:[https://www.sfml-dev.org/tutorials/2.5/window-opengl.php](https://www.sfml-dev.org/tutorials/2.5/window-opengl.php)。\n\n一个应用可以有许多着色器。然后我们可以*将*不同的着色器附加到不同的游戏对象上，以创建所需的效果。在这个游戏中，我们将只有一个顶点和一个片段着色器。我们将把它应用于每一帧，以及背景。\n\n但是，当您看到如何将着色器附加到`draw`调用时，很明显拥有更多着色器是微不足道的。\n\n我们将遵循以下步骤:\n\n1.  首先，我们需要将在 GPU 上执行的着色器的代码。\n2.  然后，我们需要编译这些代码。\n3.  最后，我们需要在我们游戏引擎的绘制函数中将着色器附加到适当的`draw`函数调用中。\n\nGLSL 是一种语言，它也有自己的类型，以及这些类型的变量，可以声明和使用。此外，我们可以通过 C++ 代码与着色器程序的变量进行交互。\n\n正如我们将看到的，GLSL 与 C++ 有一些语法上的相似之处。\n\n## 编码片段着色器\n\n这是来自`shaders`文件夹中`rippleShader.frag`文件的代码。我们不需要对此进行编码，因为这是我们在 [*第 14 章*](14.html#_idTextAnchor292)*抽象和代码管理–更好地利用 OOP* 中添加的资产:\n\n```cpp\n// attributes from vertShader.vert\nvarying vec4 vColor;\nvarying vec2 vTexCoord;\n// uniforms\nuniform sampler2D uTexture;\nuniform float uTime;\nvoid main() {\n    float coef = sin(gl_FragCoord.y * 0.1 + 1 * uTime);\n    vTexCoord.y +=  coef * 0.03;\n    gl_FragColor = vColor * texture2D(uTexture, vTexCoord);\n}\n```\n\n前四行(不包括注释)是片段着色器将使用的变量，但它们不是普通变量。我们能看到的第一种类型是`varying`。这些变量的范围介于`shaders`和`uniform`之间。接下来，我们有`uniform`变量。这些变量可以直接从我们的 C++ 代码中操作。我们将很快看到如何做到这一点。\n\n除了`varying`和`uniform`类型之外，每个变量还有一个定义实际数据的更常规的类型，如下所示:\n\n*   `vec4`是一个有四个值的向量。\n*   `vec2`是一个有两个值的向量。\n*   `sampler2d` 会持有一个纹理。\n*   `float`就像 C++ 中的一个`float data type`。\n\n执行`main`函数内部的代码。如果我们仔细观察`main`中的代码，我们会看到每个变量都在使用。这段代码的具体功能超出了本书的范围。然而，总之，纹理坐标(`vTexCoord`)和像素/片段的颜色(`glFragColor`)由几个数学函数和运算操纵。请记住，这是对我们游戏的每一帧中调用的`draw`函数所涉及的每个像素执行的。此外，请注意`uTime`是作为每个帧的不同值传入的。我们很快就会看到，结果将是一种涟漪效应。\n\n## 编码顶点着色器\n\n这是来自`vertShader.vert`文件的代码。你不需要编码。这是我们在第 14 章[](14.html#_idTextAnchor292)**抽象和代码管理-更好地利用 OOP* 中添加的资产:*\n\n```cpp\n//varying \"out\" variables to be used in the fragment shader\nvarying vec4 vColor;\nvarying vec2 vTexCoord;\n\nvoid main() {\n    vColor = gl_Color;\n    vTexCoord = (gl_TextureMatrix[0] * gl_MultiTexCoord0).xy;\n    gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;\n}\n```\n\n首先，注意两个`varying`变量。这些就是我们在片段着色器中操作的变量。在`main`函数中，代码操纵每个顶点的位置。代码如何工作超出了本书的范围，但是在幕后有一些相当深入的数学研究。如果你对它感兴趣，那么进一步探索 GLSL 将会很有趣。\n\n现在我们有了两个着色器(一个片段和一个顶点)，我们可以在游戏中使用它们。\n\n## 向引擎类添加着色器\n\n打开`Engine.h`文件。添加以下突出显示的代码行，它将名为`m_RippleShader`的 SFML `Shader`实例添加到`Engine`类中:\n\n```cpp\n// Three views for the background\nView m_BGMainView;\nView m_BGLeftView;\nView m_BGRightView;\nView m_HudView;\n// Declare a sprite and a Texture for the background\nSprite m_BackgroundSprite;\nTexture m_BackgroundTexture;\n// Declare a shader for the background\nShader m_RippleShader;\n// Is the game currently playing?\nbool m_Playing = false;\n// Is character 1 or 2 the current focus?\nbool m_Character1 = true;\n```\n\n引擎对象及其所有功能现在都可以访问`m_RippleShader`。请注意，SFML `Shader`对象将由两个着色器代码文件组成。\n\n## 加载着色器\n\n添加以下代码，检查玩家的 GPU 是否可以处理着色器。如果不能，游戏将退出。\n\n小费\n\n你必须有一台非常旧的电脑，否则就无法工作。如果你有一个不处理着色器的图形处理器，请接受我的道歉。\n\n接下来，我们将添加一个`else`子句，如果系统可以处理着色器的话，该子句将加载着色器。打开`Engine.cpp`文件，将此代码添加到构造函数中:\n\n```cpp\n// Can this graphics card use shaders?\nif (!sf::Shader::isAvailable())\n{\n    // Time to get a new PC\n    // Or remove all the shader related code L\n    m_Window.close();\n}\nelse\n{\n    // Load two shaders (1 vertex, 1 fragment)\n    m_RippleShader.loadFromFile(\"shaders/vertShader.vert\",\n        \"shaders/rippleShader.frag\");\n}\nm_BackgroundTexture = TextureHolder::GetTexture(\n    \"graphics/background.png\");\n```\n\n我们几乎准备好看到我们的连锁反应在行动。\n\n## 更新和绘制着色器\n\n打开`Draw.cpp`文件。正如我们在编码着色器时已经讨论过的，我们将在每一帧直接从我们的 C++ 代码中更新`uTime`变量。我们将通过`setParameter`功能来实现。\n\n在每个可能的绘制场景中，添加以下高亮显示的代码来更新着色器的`uTime`变量，并将对`m_BackgroundSprite`的调用更改为`draw`:\n\n```cpp\nvoid Engine::draw()\n{\n    // Rub out the last frame\n    m_Window.clear(Color::White);\n    // Update the shader parameters\nm_RippleShader.setUniform(\"uTime\", \n      m_GameTimeTotal.asSeconds());\n    if (!m_SplitScreen)\n    {\n        // Switch to background view\n        m_Window.setView(m_BGMainView);\n        // Draw the background\n        //m_Window.draw(m_BackgroundSprite);\n        // Draw the background, complete with shader effect\n        m_Window.draw(m_BackgroundSprite, &m_RippleShader);\n        // Switch to m_MainView\n        m_Window.setView(m_MainView);\n        // Draw the Level\n        m_Window.draw(m_VALevel, &m_TextureTiles);\n        // Draw thomas\n        m_Window.draw(m_Thomas.getSprite());\n        // Draw thomas\n        m_Window.draw(m_Bob.getSprite());\n        // Draw the particle system\n        if (m_PS.running())\n        {\n            m_Window.draw(m_PS);\n        }\n    }\n    else\n    {\n        // Split-screen view is active\n        // First draw Thomas' side of the screen\n        // Switch to background view\n        m_Window.setView(m_BGLeftView);\n        // Draw the background\n        //m_Window.draw(m_BackgroundSprite);\n        // Draw the background, complete with shader effect\n        m_Window.draw(m_BackgroundSprite, &m_RippleShader);\n        // Switch to m_LeftView\n        m_Window.setView(m_LeftView);\n        // Draw the Level\n        m_Window.draw(m_VALevel, &m_TextureTiles);\n\n        // Draw thomas\n        m_Window.draw(m_Bob.getSprite());\n        // Draw thomas\n        m_Window.draw(m_Thomas.getSprite());\n        // Draw the particle system\n        if (m_PS.running())\n        {\n            m_Window.draw(m_PS);\n        }\n\n        // Now draw Bob's side of the screen\n        // Switch to background view\n        m_Window.setView(m_BGRightView);\n        // Draw the background\n        //m_Window.draw(m_BackgroundSprite);\n        // Draw the background, complete with shader effect\n        m_Window.draw(m_BackgroundSprite, &m_RippleShader);\n        // Switch to m_RightView\n        m_Window.setView(m_RightView);\n        // Draw the Level\n        m_Window.draw(m_VALevel, &m_TextureTiles);\n        // Draw thomas\n        m_Window.draw(m_Thomas.getSprite());\n        // Draw bob\n        m_Window.draw(m_Bob.getSprite());\n        // Draw the particle system\n        if (m_PS.running())\n        {\n            m_Window.draw(m_PS);\n        }                \n    }\n\n    // Draw the HUD\n    // Switch to m_HudView\n    m_Window.setView(m_HudView);\n    m_Window.draw(m_Hud.getLevel());\n    m_Window.draw(m_Hud.getTime());\n    if (!m_Playing)\n    {\n        m_Window.draw(m_Hud.getMessage());\n    }    \n\n    // Show everything we have just drawn\n    m_Window.display();\n}\n```\n\n最好删除被注释掉的代码行。\n\n运行游戏，你会得到一种怪异的熔岩。尝试改变背景图像以获得一些乐趣:\n\n![](img/B14278_18_03.jpg)\n\n就这样！我们的第四场比赛结束了。\n\n# 总结\n\n在本章中，我们探讨了粒子系统和着色器的概念。虽然我们看了每个可能最简单的情况，我们仍然设法创造了一个简单的爆炸和一个怪异的熔融岩石效果。\n\n在接下来的四章中，我们将在构建太空入侵者游戏的同时，研究更多使用设计模式改进代码的方法。**"
  },
  {
    "path": "docs/begin-cpp-game-prog/19.md",
    "content": "# 十九、游戏编程设计模式——启动太空入侵者 ++ 游戏\n\n欢迎来到最终项目。正如您现在已经预料到的，这个项目将在学习新的 C++ 技术方面向前迈出重要的一步。接下来的四章将关注诸如**智能指针**、C++ **断言、**使用游戏手柄控制器、使用 Visual Studio 进行调试、**将基类的**指针转换为特定派生类的指针、调试以及首先关注**设计模式**等主题。\n\n我的猜测是，如果你打算用 C++ 制作深度、大规模的游戏，那么设计模式将成为你未来几个月和几年学习日程的一大部分。为了介绍这个至关重要的话题，我选择了一个相对简单但有趣的游戏作为例子。在这一章中，我们将了解更多关于太空入侵者++ 游戏的信息，然后我们可以进入设计模式以及我们为什么需要它们的话题。\n\n在这一章中，我们将涉及以下主题:\n\n*   了解太空入侵者++ 以及我们为什么选择它作为最终项目。\n*   了解什么是设计模式，以及为什么它们对游戏开发者很重要。\n*   研究太空入侵者++ 项目中的设计模式，该项目将在接下来的四章中使用。\n*   我们将开始太空入侵者++ 项目。\n*   编写大量的类来充实游戏。\n\n先说说游戏本身。\n\n# 太空入侵者++\n\n看看下面三张截图，直观地解释了我们需要了解的关于太空入侵者++ 的大部分内容。以防你还不知道，《太空入侵者》是最早的街机游戏之一，发行于 1978 年。如果你喜欢一点历史，可以在这里阅读维基百科太空入侵者游戏页面:[https://en.wikipedia.org/wiki/Space_Invaders](https://en.wikipedia.org/wiki/Space_Invaders)。\n\n第一张截图显示了我们游戏的简单开始屏幕。为了讨论下一步要做的屏幕，我们将其称为**选择屏幕**。玩家有两个选择:退出或玩。但是，到本章结束时，您将知道如何添加和切换任意多个屏幕:\n\n![](img/B14278_19_01.jpg)\n\n如你所见，在前面的截图中，有一个我们之前没有实现的新功能:可点击按钮。我们将很快更多地讨论按钮和它们的对应物，例如用户界面面板和屏幕。\n\n下面的截图显示了游戏的运行情况。玩起来很简单。为了讨论下一步要做的屏幕，我们将下面的截图称为**播放屏幕**。入侵者向玩家射击时从左向右移动。当他们到达屏幕边缘时，他们下降一点，加快速度，并返回左侧:\n\n![](img/B14278_19_02.jpg)\n\n玩家可以左右移动，也可以上下移动，但垂直移动仅限于屏幕的下半部分。\n\n重要说明\n\n最初的太空入侵者游戏只允许水平移动。\n\n下面的截图显示了玩家失去三条生命时的选项。他们可以选择再次播放或退出并返回选择屏幕:\n\n![](img/B14278_19_03.jpg)\n\n虽然空间入侵者++ 确实允许我们引入许多新的 C++ 主题，我已经在本章的介绍中提到过，以及一些更与游戏相关的主题，例如使用游戏手柄控制器，但与以前的项目相比，这确实在复杂性方面没有真正提高。那么，为什么选择这个作为最终项目呢？\n\n重要说明\n\n在这个项目中，有很多代码。我们以前见过的大部分，要么在同一个语境中，要么在不同的语境中。不可能解释每一行，因为新书需要这样做。我已经非常仔细地选择了要完整解释的代码，要提到的代码，以及我猜你能自己解决的代码。我建议您随着进度学习本书和下载包中的所有代码。然而，我将详细讨论代码的结构，因为这是这个项目真正的学习目标。此外，本书显示了所有的 C++ 代码，因此没有遗漏任何内容，尽管只显示了`level1.txt`文件的概述。\n\n## 为什么太空入侵者++？\n\n开始讨论之前，请考虑一下我写这本书的两个目标:\n\n1.  这本书的第一个目标是向你介绍使用视频游戏学习材料的 C++ 编程。我已经在几个场合和几个话题上承认，这只是一个介绍。C++ 和游戏开发太大了，不适合单独写这本书。\n2.  这本书的第二个目标是让你能够继续学习，同时仍然使用游戏作为学习材料。\n\n问题是，正如我们所看到的，每次我们构建一个比上一个功能更多的游戏时，我们最终得到的是更复杂的代码结构，代码文件也变得越来越长。在这本书里，我们学习了改进代码结构的新方法，在每个阶段，我们都取得了成功，但是游戏日益增加的复杂性似乎总是超过了我们学习的代码改进。\n\n这个项目旨在解决这个复杂的问题，并收回我们的源代码控制。尽管这个游戏没有前一个项目那么深入，但要处理的类会多得多。\n\n这显然意味着相当复杂的结构。然而，这也意味着，一旦你掌握了这个结构，你将能够在更复杂的游戏中重用它，而没有任何代码文件超过几百行代码。\n\n这个项目的目的是让你提出你自己的游戏想法，甚至是复杂的想法，并使用我们将在下一节讨论的设计模式立即开始。\n\n小费\n\n然而，请注意，我绝对不是建议我们将在这里学习的代码结构(设计模式)是您的游戏开发未来的最终解决方案；事实上，他们远非如此。你将学到的是解决方案，让你开始你的梦想项目，而不会让复杂性阻止你前进。在这个过程中，你仍然需要学习更多关于设计模式、C++ 和游戏开发的知识。\n\n那么，什么是设计模式呢？\n\n# 设计图案\n\n一个**设计模式**是一个编码问题的可重用解决方案。事实上，大多数游戏(包括这个)都会使用多种设计模式。设计模式的关键点在于:它们已经被证明为一个常见问题提供了一个好的解决方案。我们不会发明任何设计模式——我们只是使用一些已经存在的模式来解决我们不断扩展的代码的问题。\n\n许多设计模式相当复杂，如果你想开始学习它们，需要在本书的水平之外进一步学习。接下来是一些关键的游戏开发相关模式的简化，这将有助于实现本书的第二个目标。我们敦促你继续你的研究，以便更全面地实现它们，并与比这里将要讨论的更多的模式一起实现它们。\n\n让我们看看空间入侵者++ 项目中使用的设计模式。\n\n## 屏幕、输入栏、界面和按钮\n\n这个项目将比任何其他项目更进一步抽象一些概念。太空入侵者++ 将引入**屏幕**的概念。通过给出一些例子，屏幕的概念最容易理解。游戏可以有菜单屏幕、设置屏幕、高分屏幕和游戏屏幕。一个**屏幕**是游戏各部分的逻辑划分。每个屏幕都有一些与其他屏幕相同的地方，但是每个屏幕也需要自己独特的功能。例如，一个菜单屏幕可能有按钮，使玩家能够转换到另一个屏幕，以及一个整洁的图形图像，甚至是一个动态场景。当然，高分屏幕将有一个所有高分的列表，也许还有一个返回菜单屏幕的按钮。每个屏幕将有不同的布局，不同的按钮点击，不同的键盘按下反应，但它们都需要以 60 FPS 绘制，并以相同的方式与游戏引擎交互。\n\n在之前的项目中，我们将屏幕的概念塞进了一个地方。这意味着我们有大量长的`if`、`else`和`else if`代码块来处理更新、绘制和响应用户交互。我们的代码已经变得很难处理了。如果我们要构建更复杂的游戏，我们需要在这方面进行改进。屏幕的概念意味着我们可以创建一个类来处理每个屏幕发生的所有事情，例如更新、绘制和用户交互，然后为每种类型的屏幕创建一个派生类，即菜单、游戏、高分等等，它处理特定屏幕需要更新、绘制和响应用户的独特方式。\n\n在太空入侵者++ 中，我们将有一个`Screen`类。然后我们将继承`Screen`来处理两个屏幕，`SelectScreen`和`GameScreen`。此外，我们将有一个知道如何显示按钮的`Button`类，一个知道如何绘制文本的`UIPanel`类，`Button`实例以及一个知道如何检测键盘和游戏手柄交互的`InputHandler`类。然后，我们将能够从`UIPanel`和`InputHandler`派生，让所有不同的`Screen`实例按照要求精确地运行，而不需要多次编码屏幕、用户界面面板、输入处理程序或按钮的基础。你的游戏越大，屏幕越多，这样做的好处就越大。这也意味着每个屏幕的细节不会像我们到目前为止所做的那样被塞进长的`if`、`else`和`else if`结构中。\n\n这有点像我们如何对`PlayableCharacter`类进行编码并从中导出`Thomas`和`Bob`。然而，正如我们将看到的，我们这次在抽象上走得更远。请看下图，它展示了这个想法的一个表现，并且只显示了一个屏幕:\n\n![](img/B14278_19_04.jpg)\n\n在上图中，我们可以看到一个屏幕有一个或多个`UIPanel`实例可以选择性显示，`UIPanel`实例可以有零个或多个`Button`实例。每个`UIPanel`将有一个相关的`InputHandler`，因为每个`UIPanel`将有不同的按钮组合和布局。按钮通过`UIPanel`和`InputHandler`实例之间的指针共享。\n\n如果你想知道哪个类处理游戏循环的更新阶段，答案是`Screen`类。然而，一旦你了解了这个模式是如何工作的，添加让`UIPanel`实例也在更新阶段工作的能力将会很简单。例如，如果面板需要移动或者可能显示加载进度条，这可能会很有用。\n\n屏幕将决定哪些`UIPanel`(因此，`InputHandler`)实例当前可见并响应。但是，玩家一次只能看到一个屏幕。我们将编写一个`ScreenManager`类，它将是游戏引擎的一个基本部分，用于处理调用相应(当前)屏幕的关键功能。`ScreenManager`类还将为`InputHandler`实例提供一种在需要更改屏幕时通知我们的方式，例如，当玩家单击选择屏幕上的**播放**按钮进入播放屏幕时。\n\n`ScreenManager`会保存每个屏幕的一个实例，记住玩家当前所在的屏幕，在正确的屏幕上调用`update`、`draw`、`handleInput`，并在需要时切换屏幕。下图有望帮助您可视化这个概念，我们也将很快对其进行编码:\n\n![](img/B14278_19_05.jpg)\n\n请注意，这些图表和解释是我们将要编码的解决方案的简化，但是它们给出了一个很好的概述。\n\n如果您想在现有屏幕上添加高分屏幕或另一个`UIPanel`实例，您将在 [*第 22 章*](22.html#_idTextAnchor445)*使用游戏对象和构建游戏*结束时知道如何操作。当然，很可能你会想开始自己的游戏。您将能够根据需要将您的下一个游戏分割成尽可能多的屏幕，并提供专用的布局和输入处理。\n\n## 实体-组件模式\n\n我们现在将花五分钟时间沉浸在显然无法解决的混乱的痛苦中。然后，我们将看到实体-组件模式是如何拯救的。\n\n### 为什么许多不同的对象类型难以管理\n\n在前面的项目中，我们为每个对象编写了一个类。我们有蝙蝠、球、爬虫和托马斯等课程。然后，在`update`函数中，我们会更新它们，在`draw`函数中，我们会绘制它们。每个对象决定更新和绘制的方式。\n\n我们可以开始使用空间入侵者++ 的相同结构。这是可行的，但是我们正在尝试学习一些更容易管理的东西，这样我们的游戏就可以变得更加复杂。\n\n这种方法的另一个问题是我们不能利用继承。例如，所有的入侵者、子弹和玩家都以相同的方式绘制自己，但是除非我们改变做事的方式，否则我们最终会得到三个代码几乎相同的`draw`函数。如果我们改变调用`draw`函数的方式或处理图形的方式，我们将需要更新所有三个类。\n\n肯定有更好的办法。\n\n### 使用通用游戏对象来获得更好的代码结构\n\n如果每个对象、玩家、外星人和所有子弹都是一个通用类型，那么我们可以将它们打包成一个`vector`实例，并循环遍历它们的每个`update`函数，然后遍历它们的每个`draw`函数。\n\n我们已经知道了一种方法——继承。乍一看，继承似乎是一个完美的解决方案。我们可以创建一个抽象的`GameObject`类，然后用`Player`、`Invader`和`Bullet`类扩展它。\n\n在所有三个类中相同的`draw`函数可以保留在父类中，我们不会有浪费重复代码的问题。太好了。\n\n这种方法的问题在于——在某些方面——游戏对象的多样性。多样性不是一种力量；它是多样的。例如，所有对象类型的移动方式都不同。子弹向上或向下，入侵者向左或向右，偶尔会下降，玩家的船会对输入做出反应。\n\n我们如何将这种多样性放入`update`中，以便它能够控制这种运动？也许我们可以用这样的东西:\n\n```cpp\nupdate(){\n   switch(objectType){\n          case 1:\n                // All the player's logic\n                break;\n          case 2:\n                // All the invader's logic here\n                Break;\n          case 3:\n                // All the bullet's logic here\n                break;\n   }\n}\n```\n\n光是`update`功能就比整个`GameEngine`类都大！\n\n大家可能还记得 [*第 15 章*](15.html#_idTextAnchor306)*高级 OOP 继承和多态*中，当我们从一个类继承时，我们也可以覆盖特定的函数。这意味着我们可以对每种对象类型使用不同版本的`update`函数。然而不幸的是，这种方法也有一个问题。\n\n`GameEngine`引擎必须“知道”它正在更新哪种类型的对象，或者至少能够查询它正在更新的`GameObject`实例，以便调用正确版本的`update`函数。真正需要的是`GameObject`在内部选择需要哪个版本的更新`function`。\n\n不幸的是，即使是解决方案中看起来有效的部分，在仔细观察后也会分崩离析。我说过`draw`函数中的代码对于所有三个对象都是相同的，因此`draw`函数可以是父类的一部分，并且被所有子类使用，而不是我们必须编码三个单独的`draw`函数。那么，当我们引入一个需要以不同方式绘制的新对象时会发生什么，比如一个飞过屏幕顶部的动画 UFO？在这种情况下，平局解决方案也会分崩离析。\n\n现在，我们已经看到了当对象彼此不同，但却来自同一个父类时会出现的问题，现在是时候看看我们将在空间入侵者++ 项目中使用的解决方案了。\n\n我们需要的是一种新的思维方式来构建我们所有的游戏对象。\n\n## 重成分轻遗传\n\n重组合轻继承是指用其他对象组合对象的思想。\n\n重要说明\n\n这一概念最初是在以下出版物中提出的:\n\n*设计模式:可重用面向对象软件的元素*\n\n作者 Erich Gamma，Richard Helm 等人。\n\n如果我们可以编写一个处理对象绘制方式的类(而不是函数)会怎么样？然后对于所有以相同方式绘制自己的类，我们可以在`GameObject`中实例化这些特殊的绘制类中的一个，任何需要以不同方式绘制的对象都可以有不同的绘制对象。然后，当`GameObject`做了不同的事情时，我们只需用不同的绘图或更新相关的类来适应它。我们所有对象中的所有相似之处都可以从使用相同的代码中受益，而所有差异不仅可以从封装中受益，还可以从基类中抽象出来。\n\n请注意，本节的标题是组合而不是继承，不是组合而不是继承。组合并不能取代继承和你在 [*第 15 章*](15.html#_idTextAnchor306)*高级 OOP 继承和多态*中学到的一切，依然成立。但是，在可能的情况下，不要继承，而要作曲。\n\n`GameObject`类是实体，而它将由做诸如更新其位置并将其绘制到屏幕上的事情的类组成，这就是为什么它被称为实体-组件模式。\n\n请看下图，它以我们将在本项目中实现的形式表示实体-组件模式:\n\n![](img/B14278_19_06.jpg)\n\n在上图中，我们可以看到一个`GameObject`实例由多个`Component`实例组成。从`Component`类派生出多个不同的类，包括`UpdateComponent`和`GraphicsComponent`。此外，还可以有更多从它们派生的特定类。例如`BulletUpdateComponent`和`InvaderUpdateComponent`类将从`UpdateComponent`类派生而来。这些类将处理子弹和入侵者(分别)如何在游戏的每一帧中更新自己。这对于封装来说很棒，因为我们不需要大的`switch`块来区分不同的对象。\n\n当我们使用组合而不是继承来创建一组表示行为/算法的类时，正如我们将在这里看到的，这被称为**策略**模式。你可以利用你在这里学到的一切，并把它称为战略模式。实体组件是一个不太为人所知但更具体的实现，这就是为什么我们称之为。区别在于学术性，但如果你想进一步探索事物，请随时求助于谷歌。在 [*第 23 章*](23.html#_idTextAnchor457)*你走之前……*我会给大家展示一些这类详细研究的好资源。\n\n实体-组件模式，以及优先于继承的组合使用，乍看起来很棒，但也带来了一些问题。这意味着我们新的`GameObject`类需要知道游戏中所有不同类型的组件和每一种类型的对象。它将如何向自身添加所有正确的组件？\n\n让我们来看看解决方案。\n\n## 工厂模式\n\n的确，如果我们要拥有这个通用的`GameObject`类，它可以是我们想要的任何东西，无论是子弹、玩家、入侵者，还是其他任何东西，那么我们将不得不编写一些逻辑来“知道”如何构建这些超灵活的`GameObject`实例，并用正确的组件来组成它们。但是，将所有这些代码添加到类本身会使它异常笨拙，并首先否定使用实体-组件模式的全部理由。\n\n我们需要一个构造函数来完成类似于这个假设的`GameObject`代码:\n\n```cpp\nclass GameObject\n{\n   UpdateComponent* m_UpdateComponent;\n   GraphicsComponent* m_GraphicsComponent;\n   // More components\n   // The constructor\n   GameObject(string type){\n      if(type == \"invader\")\n      {\n            m_UpdateComp = new InvaderUpdateComponent();   \n            m_GraphicsComponent = new StdGraphicsComponent();\n      }\n      else if(type ==\"ufo\")\n       {\n              m_UpdateComponent = new \n                   UFOUpdateComponentComponent();\n              m_GraphicsComponent = new AnimGraphicsComponent();\n       }\n      // etc.\n      …\n   }\n};\n```\n\n`GameObject`类不仅需要知道哪些组件与哪个`GameObject`实例一起使用，还需要知道哪些不需要某些组件，例如用于控制玩家的输入相关组件。对于太空入侵者++ 项目，我们可以做到这一点，并且在复杂的环境中生存，但是仅仅生存并不是目标；我们想要完全控制。\n\n`GameObject`类也需要理解所有这些逻辑。在实体-组件模式中使用组合而不是继承所获得的任何好处或效率都将主要丧失。\n\n此外，如果我们决定想要一种新的入侵者，也许是一个“隐形人”外星人，它会传送到玩家附近，开枪，然后再次传送走呢？编写一个新的`GraphicsComponent`类是没问题的，也许是一个在可见和不可见时“知道”的`CloakingGraphicsComponent`，以及一个新的`UpdateComponent`，也许是一个以传统方式传送而不是移动的`CloakerUpdateComponent`，但是不太好的是我们将不得不向`GameObject`类构造器添加一大堆新的`if` 语句。\n\n事实上，情况甚至比这更糟。如果我们决定普通入侵者现在可以隐身呢？入侵者现在不仅仅需要一个不同类型的`GraphicsComponent`职业。我们必须回到`GameObject`类，再次编辑所有那些`if`语句。\n\n其实可以想象的场景更多，最终都是越来越大的`GameObject`类。**工厂**模式是这些`GameObject`类相关问题的解决方案，也是实体-组件模式的完美合作伙伴。\n\n重要说明\n\n工厂模式的这种实现是开始学习工厂模式的一种更简单的方式。完成这个项目后，为什么不在网上搜索工厂模式，看看如何改进？\n\n游戏设计者将为游戏中每一种类型的对象提供一个规范，程序员将提供一个工厂类，根据游戏设计者的规范构建`GameObject`实例。当游戏设计师为实体提出新的想法时，我们所需要做的就是要求一个新的规范。有时，这将涉及到在使用现有组件的工厂中增加一条新的生产线，有时，这将意味着编码新的组件或者更新现有组件。关键是游戏设计者有多有创造力并不重要-`GameObject`和`GameEngine`类保持不变。\n\n在工厂代码中，检查当前对象类型，并向其中添加适当的组件(类)。子弹、玩家和入侵者有相同的图形组件，但都有不同的更新组件。\n\n当我们使用合成时，会不太清楚哪个类负责记忆。是创建它的类，使用它的类，还是其他类？让我们学习更多的 C++ 来帮助我们更简单地管理内存。\n\n# C++ 智能指针\n\n**智能指针**是我们可以用来获得与常规指针相同功能的类，但有一个额外的特性——该特性是它们可以自行删除。到目前为止，在我们使用指针的有限方式中，删除我们自己的内存并不是一个问题，但是随着您的代码变得更加复杂，当您在一个类中分配新的内存但在另一个类中使用它时，当我们使用完它时，哪个类负责删除内存就变得不太清楚了。一个类或函数如何知道另一个类或函数是否已经用完了一些分配的内存？\n\n解决方案是智能指针。智能指针有几种类型；我们将在这里看两个最常用的。智能指针成功的关键是使用正确的类型。\n\n我们要考虑的第一种类型是**共享指针**。\n\n## 共享指针\n\n共享指针可以安全删除它所指向的内存的方法是记录内存区域中不同引用的数量。如果将指针传递给函数，计数将增加 1。如果你把一个指针放入一个向量中，计数就会增加一。如果函数返回，计数将减少 1。如果向量超出范围或调用了`clear`函数，智能指针会将引用计数减少 1。当引用计数为零时，不再有任何东西指向内存区域，智能指针类调用`delete`。所有智能指针类都是在幕后使用常规指针实现的。我们只是得到了这样的好处，不用担心在哪里或者什么时候给`delete`打电话。让我们看看使用共享智能指针的代码。\n\n下面的代码创建了一个名为`myPointer`的新共享智能指针，它将指向`MyClass`的一个实例:\n\n```cpp\nshared_ptr<MyClass> myPointer;\n```\n\n`shared_ptr<MyClass>`为类型，`myPointer`为名称。下面的代码是我们如何初始化`myPointer`:\n\n```cpp\n myPointer = make_shared<MyClass>();\n```\n\n对`make_shared`的调用在内部调用`new`来分配内存。括号`()`是构造函数括号。例如，如果`MyClass`类构造函数采用了一个`int`参数，那么前面的代码可能如下所示:\n\n```cpp\nmyPointer = make_shared<MyClass>(3);\n```\n\n前面代码中的`3`是一个任意的例子。\n\n当然，如果需要，可以在一行代码中声明和初始化共享智能指针，如以下代码所示:\n\n```cpp\nshared_ptr<MyClass> myPointer = make_shared<MyClass>();\n```\n\n正是因为`myPointer`是一个`shared_ptr`，所以它有一个内部引用计数，跟踪有多少引用指向它创建的内存区域。如果我们复制指针，引用计数就会增加。\n\n复制指针包括将指针传递给另一个函数，将其放入`vector`、`map`或其他结构中，或者简单地复制它。\n\n我们可以使用与常规指针相同的语法来使用智能指针。有时很容易忘记它不是一个常规指针。以下代码调用`myPointer`上的`myFunction`功能:\n\n```cpp\nmyPointer->myFunction();\n```\n\n通过使用共享智能指针，有一些性能和内存**开销**。开销，我的意思是我们的代码运行得更慢，使用更多的内存。毕竟，智能指针需要一个变量来跟踪引用计数，并且它必须在每次引用超出范围时检查引用计数的值。然而，这种开销很小，并且仅在最极端的情况下才是一个问题，因为大部分开销发生在创建智能指针的时候。通常，我们会在游戏循环之外创建智能指针。在智能指针上调用函数和常规指针一样有效。\n\n有时候，我们知道我们永远只想要一个对智能指针的引用，在这种情况下，**独特的** **指针**是最好的选择。\n\n## 唯一指针\n\n当我们知道我们只需要一个对内存区域的引用时，我们可以使用一个唯一的智能指针。唯一指针失去了我提到的共享指针的大部分开销。此外，如果你试图复制一个唯一的指针，编译器会警告我们，代码要么不编译，要么崩溃，给我们一个明确的错误。这是一个非常有用的特性，可以防止我们不小心复制了一个不应该被复制的指针。你可能想知道这个禁止复制规则是否意味着我们永远不能把它传递给一个函数，甚至不能把它放在一个数据结构中，比如`vector`。为了找到答案，让我们看看一些独特的智能指针的代码，并探索它们是如何工作的。\n\n下面的代码创建了一个名为`myPointer`的唯一智能指针，它指向`MyClass`的一个实例:\n\n```cpp\nunique_ptr<MyClass> myPointer = make_unique<MyClass>();\n```\n\n现在。假设我们想给`vector`添加一个`unique_ptr`。首先要注意的是`vector`必须是正确的类型。下面的代码声明了一个`vector`，它包含指向`MyClass`实例的唯一指针:\n\n```cpp\nvector<unique_ptr<MyClass>> myVector;\n```\n\n`vector`被称为`myVector`，你放入其中的任何东西都必须是`MyClass`的唯一指针类型。但是我不是说唯一指针不能复制吗？当我们知道我们将永远只想要对一个记忆区域的单一引用时，我们应该使用`unique_ptr`。然而，这并不意味着不能移动引用。这里有一个例子:\n\n```cpp\n// Use move() because otherwise \n// the vector has a COPY which is not allowed\nmVector.push_back(move(myPointer));\n// mVector.push_back(myPointer); // Won't compile!\n```\n\n在前面的代码中，我们可以看到`move`函数可以用来将一个唯一的智能指针放入一个`vector`中。请注意，当您使用`move`函数时，您并没有给予编译器打破规则并复制唯一指针的权限，而是将责任从`myPointer`变量转移到了`myVector`实例。如果您在此时之后尝试使用`myPointer`变量，代码将执行，游戏将崩溃，给您一个**空指针访问违规错误**。以下代码将导致崩溃:\n\n```cpp\nunique_ptr<MyClass> myPointer = make_unique<MyClass>();\nvector<unique_ptr<MyClass>> myVector;\n// Use move() because otherwise \n// the vector has a COPY which is not allowed\nmVector.push_back(move(myPointer));\n// mVector.push_back(myPointer); // Won't compile!\nmyPointer->myFunction();// CRASH!!\n```\n\n将唯一指针传递给函数时，也适用完全相同的规则；使用`move`功能传递责任。我们将再次查看所有这些场景，当我们在几页时间内完成项目时，还会查看更多的场景。\n\n# 铸造智能指针\n\n我们经常会希望将派生类的智能指针打包到基类的数据结构或函数参数中，比如所有不同的派生类`Component`中。这就是多态性的本质。智能指针可以使用强制转换来实现这一点。但是当我们以后需要访问派生类的功能或数据时会发生什么呢？\n\n一个很好的例子是，当我们处理游戏对象内部的组件时，这将是经常必要的。会有一个抽象的`Component`类，并由此衍生出`GraphicsComponent`、`UpdateComponent`等等。\n\n例如，我们希望在游戏循环的每一帧的所有`UpdateComponent`实例上调用`update`函数。但是如果所有的组件都存储为基类`Component`实例，那么我们似乎不能这样做。从基类到派生类的转换解决了这个问题。\n\n下面的代码将一个基类`Component`实例`myComponent`转换为一个`UpdateComponent`类实例，然后我们可以在上面调用`update`函数:\n\n```cpp\nshared_ptr<UpdateComponent> myUpdateComponent =\n                static_pointer_cast<UpdateComponent>(MyComponent);\n```\n\n在等号之前，声明一个新的`shared_ptr`到一个`UpdateComponent`实例。在等号后面，`static_pointer_cast`函数在尖括号`<UpdateComponent>`中指定要转换的类型，在圆括号`(MyComponent)`中指定要转换的实例。\n\n我们现在可以使用`UpdateComponent`类的所有功能，在我们的项目中包括`update`功能。我们将`update`函数称为:\n\n```cpp\nmyUpdateComponent->update(fps);\n```\n\n有两种方法可以将一个类智能指针转换为另一个类智能指针。一种是用`static_pointer_cast`，就像我们刚才看到的，另一种是用`dynamic_pointer_cast`。不同的是`dynamic_pointer_cast`可以用，如果你不确定剧组是否会工作。当您使用`dynamic_pointer_cast`时，您可以通过测试结果是否为空指针来检查它是否有效。当你确定结果是你想要的类型时，你就使用`static_pointer_class`。我们将在整个太空入侵者++ 项目中使用`static_pointer_cast`。\n\n我们将定期将`Component`实例转换为不同的派生类型。随着项目的进展，我们如何确定我们要铸造的类型是正确的类型将变得显而易见。\n\n# C++ 断言\n\n在这个项目中，我们将使用 C++ **断言**。像往常一样，这个话题比我们在这里讨论的要多，但是我们仍然可以做一些有用的事情，只需要一个介绍。\n\n我们可以在一个类中使用`#define`预处理器语句为整个项目定义一个值。我们使用以下代码来实现:\n\n```cpp\n#define debuggingOnConsole\n```\n\n这段代码将写在头文件的顶部。现在，在整个项目中，我们可以编写如下代码:\n\n```cpp\n#ifdef debuggingOnConsole\n    // C++ code goes here\n#endif\n```\n\n`#ifdef debuggingOnConsole`语句检查`#define` `debuggingOnConsole`语句是否存在。如果是，那么任何 C++ 代码直到`#endif`语句都将包含在游戏中。然后我们可以选择注释掉`#define`语句来打开或关闭我们的调试代码。\n\n通常，我们会在`#ifdef`块中包含如下代码:\n\n```cpp\n#ifdef debuggingOnConsole         \n        cout << \n            \"Problem x occurred and caused a crash!\" \n            << endl;\n#endif\n```\n\n前面的代码使用`cout`语句将调试信息打印到控制台窗口。\n\n这些断言相当于一种在开发过程中从游戏中获得反馈的方法，然后在`#define`语句前面快速添加`//`，当我们完成时，从游戏中剥离所有调试代码。\n\n# 创建太空入侵者++ 项目\n\n您可以在`Space Invaders ++ `文件夹中找到本章末尾代表项目的可运行代码。第 20 章、第 21 章和第 22 章都需要完成，才能使项目再次运行。在 [*第 22 章*](22.html#_idTextAnchor445) 、*使用游戏对象和构建游戏*的末尾，可以在`Space Invaders ++ 2`文件夹中找到可运行的代表项目的完整代码。\n\n使用我们在前面四个项目中使用的相同设置，在 Visual Studio 中创建新项目。调用新项目`Space Invaders ++ `。\n\n在`Space Invaders ++ `文件夹中，复制并粘贴下载包中的`fonts`、`graphics`和`sound`文件夹及其内容。正如你所料，`fonts`、`graphics`和`sound`文件夹包含了我们将在这个游戏中使用的字体、图形和音频资源。\n\n另外需要从[https://opengameart.org/content/background-night](https://opengameart.org/content/background-night)下载后台文件。\n\n重要说明\n\n这张图是[https://opengameart.org/users/alekei](https://opengameart.org/users/alekei)的作品。\n\n可以从[https://opengameart.org/content/background-night](https://opengameart.org/content/background-night)下载。\n\n你可以在[https://creativecommons.org/licenses/by/3.0/](https://creativecommons.org/licenses/by/3.0/)找到牌照。\n\n将刚下载的文件重命名为`background.png`，放入项目的`graphics`文件夹。\n\n现在，添加`world`文件夹，包括`level1.txt`文件。该文件包含所有游戏对象的布局，我们将在 [*第 21 章*](21.html#_idTextAnchor432)*文件 I/O 和游戏对象工厂*中进一步讨论。\n\n## 用过滤器组织代码文件\n\n接下来，我们将做一些新的事情。由于这个项目中的类文件比我们以前的项目多，所以我们在 Visual Studio 中会更有条理一些。我们将创建一系列**过滤器**。这些是我们用来创建文件结构的逻辑组织器。这将允许我们以更有条理的方式查看所有的头文件和源文件。\n\n右键单击**解决方案资源管理器**窗口中的**头文件**文件夹，并选择**新过滤器**。给过滤器取`Engine`的名字。我们将把所有的核心头文件添加到这个过滤器中。\n\n再次右键单击**头文件**，并添加另一个名为`FileIO`的过滤器。我们将添加所有在`level1.txt`之间读取文本的文件，以及一些支持类。\n\n在**头文件**中制作另一个名为`GameObjects`的新过滤器。与所有游戏对象相关的一切，包括`GameObject`类和所有`Component`类相关的头文件，都将放在这里。\n\n添加另一个名为`Screens`的过滤器。右键单击刚刚添加的**屏幕**过滤器，并在**屏幕**内创建一个名为`Select`的过滤器。现在，在**屏幕**内创建另一个名为`Game`的过滤器。我们会将`Screen`、`InputHandler`、`UIPanel`的所有衍生版本放在**游戏**或**选择**(视情况而定)并将所有基类放在**屏幕**中。\n\n现在，重复前面创建过滤器的所有步骤，在**源文件**文件夹中创建完全相同的结构。现在，您应该有一个如下所示的解决方案资源管理器布局:\n\n![](img/B14278_19_07.jpg)\n\n注 t 前面的布局只是为了我们组织的利益；它对代码或完成的游戏没有影响。事实上，如果你使用操作系统的文件浏览器查看`Space Invaders ++ `文件夹，你会发现没有额外的文件夹。随着这个项目的进展和新类的添加，我们将在特定的过滤器中添加它们，以使它们更有条理，不那么混乱。\n\n## 添加开发状态文件\n\n为了将调试数据输出到控制台，我们将创建`DevelopState`类，该类除了定义`debuggingOnConsole`什么也不做。\n\n在`Header Files/Engine`过滤器中创建`DevelopState.h`文件，并添加以下代码:\n\n```cpp\n#pragma once\n#define debuggingOnConsole\nclass DevelopState {};\n```\n\n我们可以在游戏运行时注释掉`#define debuggingOnConsole`，但是，当我们遇到无法解释的崩溃时，我们可以取消注释。如果我们在代码的各个部分添加断言，我们可以看到这些部分是否会导致游戏崩溃。\n\n## 编码太空入侵者++。卡片打印处理机（Card Print Processor 的缩写）\n\n接下来，将我们创建项目时自动生成的`SpaceInvaders ++.cpp`文件拖放到`Source Files/Engine`过滤器中。这不是必需的，只是为了保持事情有条不紊。这个文件是游戏的入口点，因此是一个核心文件，尽管很短。\n\n编辑`SpaceInvaders ++.cpp`使其只有以下代码:\n\n```cpp\n#include \"GameEngine.h\"\nint main()\n{\n    GameEngine m_GameEngine;\n    m_GameEngine.run();\n    return 0;\n}\n```\n\n前面的代码创建了一个`GameEngine`的实例，并调用了它的`run`函数。在我们对`GameEngine`类进行编码之前会有错误。我们下一步会这么做。请注意，在整个项目中，通常会有一个、多个甚至多个错误。这是由于类的相互依赖性。我通常会提到什么时候有错误，什么时候会处理，但也许不是每一个错误。到本章结束时，我们将有一个无错误的、可执行的项目，但是，在此之后，它将需要直到 [*第 22 章*](22.html#_idTextAnchor445) 、*使用游戏对象并构建一个游戏*，直到该项目再次无错误且可执行。\n\n## 编码游戏引擎类\n\n在名为`GameEngine.h`的`Header Files/Engine`过滤器中创建新的头文件，并添加以下代码:\n\n```cpp\n#pragma once\n#include <SFML/Graphics.hpp>\n#include \"ScreenManager.h\"\n#include \"SoundEngine.h\"\nusing namespace sf;\nclass GameEngine {\nprivate:\n    Clock m_Clock;\n    Time m_DT;\n    RenderWindow m_Window;\n    unique_ptr<ScreenManager> m_ScreenManager;\n    float m_FPS = 0;\n    Vector2f m_Resolution;\n    void handleInput();\n    void update();\n    void draw();\npublic:\n    SoundEngine m_SoundEngine;\n    GameEngine();\n    void run();\n};\n```\n\n学习前面的代码来熟悉它。新的是，我们第一次看到智能指针在起作用。我们有一个独特的指针`ScreenManager`类型。这意味着这个指针不会被传递给任何其他类，但是，如果它被传递了，那么所有权也将被传递。\n\n除了智能指针，没有什么是我们以前没有见过的。有一个`Clock`实例、`Time`实例、`RenderWindow`实例，以及跟踪帧速率和屏幕分辨率的变量。此外，我们还有处理输入、更新和绘制每一帧的功能。这也不是什么新鲜事。然而，我们在这些功能中所做的将是新的。我们还有一个`SoundEngine`实例，它将与我们在其他项目中处理声音的方式几乎相同。我们还有`run`功能，这是公共的，将启动所有的私人功能。\n\n有错误是因为我们需要实现`ScreenManager`和`SoundEngine`类。我们很快就会找到他们。\n\n在名为`GameEngine.cpp`的`Source Files/Engine`过滤器中创建新的源文件，并添加以下代码:\n\n```cpp\n#include \"GameEngine.h\"\nGameEngine::GameEngine()\n{\n    m_Resolution.x = VideoMode::getDesktopMode().width;\n    m_Resolution.y = VideoMode::getDesktopMode().height;\n    m_Window.create(VideoMode(m_Resolution.x, m_Resolution.y),\n        \"Space Invaders++\", Style::Fullscreen);\n    m_ScreenManager = unique_ptr<ScreenManager>(new ScreenManager(\n        Vector2i(m_Resolution.x, m_Resolution.y)));\n}\nvoid GameEngine::run()\n{\n    while (m_Window.isOpen())\n    {\n        m_DT = m_Clock.restart();\n        m_FPS = m_DT.asSeconds();\n        handleInput();\n        update();\n        draw();\n    }\n}\nvoid GameEngine::handleInput()\n{\n    m_ScreenManager->handleInput(m_Window);\n}\nvoid GameEngine::update()\n{\n    m_ScreenManager->update(m_FPS);\n}\nvoid GameEngine::draw()\n{\n    m_Window.clear(Color::Black);\n    m_ScreenManager->draw(m_Window);\n    m_Window.display();\n}\n```\n\n在`GameEngine`构造函数中，`RenderWindow`实例被初始化，指向`ScreenManager`实例的唯一智能指针使用`new`初始化，该指针将解析传递给`ScreenManager`构造函数。\n\n重要说明\n\n这是调用`make_unique`函数的替代方法。\n\n`run`功能应该看起来很熟悉；它重启时钟并存储时间，就像我们到目前为止在每个项目中所做的那样。然后调用`handleInput`、`update`和`draw`功能。\n\n在`handleInput`函数中，调用`ScreenManager`实例的`handleInput`函数。在`update`函数中，调用`ScreenManger`实例的`update`函数。最后在`draw`功能中，清除`RenderWindow`，调用`ScreenManager`实例的`draw`功能，显示`RenderWindow`实例的内容。\n\n我们已经成功地将处理输入、更新和绘制每一帧的全部责任交给了`ScreenManager`类。正如我们将在*编码屏幕管理器*部分看到的那样，`ScreenManager`类将进一步将所有这些任务的责任委托给从`Screen`类派生的适当类。\n\n和相关的`GameEngine.h`头文件一样，也有错误，因为我们需要实现`ScreenManager`和`SoundEngine`类。\n\n## 对声音引擎类进行编码\n\n在名为`SoundEngine.h`的`Header Files/Engine`过滤器中创建新的头文件，并添加以下代码:\n\n```cpp\n#pragma once\n#ifndef SOUND_ENGINE_H\n#define SOUND_ENGINE_H\n#include <SFML/Audio.hpp>\nusing namespace sf;\nclass SoundEngine\n{\nprivate:\n    SoundBuffer m_ShootBuffer;\n    SoundBuffer m_PlayerExplodeBuffer;\n    SoundBuffer m_InvaderExplodeBuffer;\n    SoundBuffer m_ClickBuffer;\n    Sound m_ShootSound;\n    Sound m_PlayerExplodeSound;\n    Sound m_InvaderExplodeSound;\n    Sound m_UhSound;\n    Sound m_OhSound;\n    Sound m_ClickSound;\npublic:\n    SoundEngine();\n    static void playShoot();\n    static void playPlayerExplode();\n    static void playInvaderExplode();\n    static void playClick();\n    static SoundEngine* m_s_Instance;\n};\n#endif\n```\n\n在名为`SoundEngine.cpp`的`Source Files/Engine`过滤器中创建新的源文件，并添加以下代码:\n\n```cpp\n#include <SFML/Audio.hpp>\n#include <assert.h>\n#include \"SoundEngine.h\"\nusing namespace std;\nusing namespace sf;\nSoundEngine* SoundEngine::m_s_Instance = nullptr;\nSoundEngine::SoundEngine()\n{\n    assert(m_s_Instance == nullptr);\n    m_s_Instance = this;\n    // Load the sound into the buffers\n    m_ShootBuffer.loadFromFile(\"sound/shoot.ogg\");\n    m_PlayerExplodeBuffer.loadFromFile(\"sound/playerexplode.ogg\");\n    m_InvaderExplodeBuffer.loadFromFile(\"sound/invaderexplode.ogg\");\n    m_ClickBuffer.loadFromFile(\"sound/click.ogg\");\n    // Associate the sounds with the buffers\n    m_ShootSound.setBuffer(m_ShootBuffer);\n    m_PlayerExplodeSound.setBuffer(m_PlayerExplodeBuffer);\n    m_InvaderExplodeSound.setBuffer(m_InvaderExplodeBuffer);\n    m_ClickSound.setBuffer(m_ClickBuffer);\n}\nvoid SoundEngine::playShoot()\n{\n    m_s_Instance->m_ShootSound.play();\n}\nvoid SoundEngine::playPlayerExplode()\n{\n    m_s_Instance->m_PlayerExplodeSound.play();\n}\nvoid SoundEngine::playInvaderExplode()\n{\n    m_s_Instance->m_InvaderExplodeSound.play();\n}\nvoid SoundEngine::playClick()\n{\n    m_s_Instance->m_ClickSound.play();\n}\n```\n\n`SoundEngine`类使用的策略与之前项目中的`SoundManager`类完全相同。事实上，`SoundEngine`比`SoundManager`稍微简单一点，因为我们没有使用空间化特征。有关`SoundEngine`课程如何运作的复习资料，请参考第 17 章*声音空间化和抬头显示器*。\n\n现在，我们可以进入`ScreenManager`课了。\n\n## 编写屏幕管理器类的代码\n\n在名为`ScreenManager.h`的`Header Files/Engine`过滤器中创建新的头文件，并添加以下代码:\n\n```cpp\n#pragma once\n#include <SFML/Graphics.hpp>\n#include <map>\n#include \"GameScreen.h\"\n#include \"ScreenManagerRemoteControl.h\"\n#include \"SelectScreen.h\"\n//#include \"LevelManager.h\"\n#include \"BitmapStore.h\"\n#include <iostream>\nusing namespace sf;\nusing namespace std;\nclass ScreenManager : public ScreenManagerRemoteControl {\nprivate:\n    map <string, unique_ptr<Screen>> m_Screens;\n    //LevelManager m_LevelManager;\nprotected:\n    string m_CurrentScreen = \"Select\";\npublic:\n    BitmapStore m_BS;\n    ScreenManager(Vector2i res);\n    void update(float fps);\n    void draw(RenderWindow& window);\n    void handleInput(RenderWindow& window);\n    /****************************************************\n    *****************************************************\n    From ScreenManagerRemoteControl interface\n    *****************************************************\n    *****************************************************/\n    void ScreenManagerRemoteControl::\n        SwitchScreens(string screenToSwitchTo)\n    {\n        m_CurrentScreen = \"\" + screenToSwitchTo;\n        m_Screens[m_CurrentScreen]->initialise();\n    }\n    void ScreenManagerRemoteControl::\n        loadLevelInPlayMode(string screenToLoad)\n    {\n        //m_LevelManager.getGameObjects().clear();\n        //m_LevelManager.\n            //loadGameObjectsForPlayMode(screenToLoad);\n        SwitchScreens(\"Game\");\n    }\n    //vector<GameObject>& \n        //ScreenManagerRemoteControl::getGameObjects()\n    //{\n        //return m_LevelManager.getGameObjects();\n    //}\n    //GameObjectSharer& shareGameObjectSharer()\n    //{\n        //return m_LevelManager;\n    //}\n};\n```\n\n在前面的代码中，有一些`#include`语句和一些函数被注释掉了。这是因为直到 [*第 21 章*](21.html#_idTextAnchor432)*文件输入输出和游戏对象工厂*我们才会对`LevelManager`类进行编码。\n\n接下来要注意的是`ScreenManager`继承自`ScreenManagerRemoteControl`。关于这门课的更多内容。\n\n我们用一对键值对`string`和一个指向`Screen`的唯一指针来编码`map`。这将允许我们通过使用相应的`string`来获取特定`Screen`实例的功能。接下来，我们声明名为`m_CurrentScreen`的`string`，并将其初始化为`Select`。\n\n接下来，我们声明一个名为`m_BS`的`BitmapStore`实例。这将是我们在前面两个项目中看到的`TextureHolder`类的一个稍加修改的版本。接下来我们将对`BitmapStore`类进行编码。\n\n请注意，`ScreenManager`的构造函数采用了一个`Vector2i`实例，这是我们在`GameEngine`类中初始化一个`ScreenManager`实例时所期望的。\n\n接下来是`update`、`draw`和`handleInput`函数原型，它们是从`GameEngine`类中调用的。\n\n接下来的两个函数是最有趣的。请注意，它们来自`ScreenManager`继承自的`ScreenManagerRemoteControl`类。这些是`ScreenManagerRemoteControl`中的纯虚函数，我们这样做是为了与其他类共享`ScreenManager`类的一些功能。我们将在几节时间内对`ScreenManagerRemoteControl`类进行编码。请记住，当您从具有纯虚函数的类继承时，如果您想要创建一个实例，就必须实现这些函数。此外，实现应该包含在声明类的同一个文件中。有四个函数，其中两个已经被注释掉了。直接感兴趣的两个功能是`SwitchScreens`和`loadLevelInPlayMode`。\n\n`SwitchScreen`函数改变`m_CurrentScreen`的值，而`loadLevelInPlayMode`函数有一些临时注释掉的代码和一行用`Game`的值调用`SwitchScreens`的活动代码。\n\n让我们进入`ScreenManager.cpp`文件，这样我们就可以查看所有的函数定义。\n\n在名为`ScreenManager.cpp`的`Source Files/Engine`过滤器中创建新的源文件，并添加以下代码:\n\n```cpp\n#include \"ScreenManager.h\"\nScreenManager::ScreenManager(Vector2i res)\n{\n    m_Screens[\"Game\"] = unique_ptr<GameScreen>(\n        new GameScreen(this, res));\n    m_Screens[\"Select\"] = unique_ptr<SelectScreen>(\n        new SelectScreen(this, res));\n}\nvoid ScreenManager::handleInput(RenderWindow& window)\n{\n    m_Screens[m_CurrentScreen]->handleInput(window);\n}\nvoid ScreenManager::update(float fps)\n{\n    m_Screens[m_CurrentScreen]->update(fps);\n}\nvoid ScreenManager::draw(RenderWindow& window)\n{\n    m_Screens[m_CurrentScreen]->draw(window);\n}\n```\n\n在前面的代码中，构造函数向`map`实例添加了两个`Screen`实例——首先是一个键为`\"Game\"`的`GameScreen`实例，然后是一个键为`\"Select\"`的`SelectScreen`实例。`handleInput`、`update`、`draw`三个函数，不管当前屏幕是什么，都使用对应的`Screen`实例，调用其`handleInput`、`update`和`draw`函数。\n\n第一次执行游戏时，会调用`SelectScreen`开始的这些函数的版本，但如果调用了`ChangeScreen`或`loadLevelInPlayMode`函数，那么就可以从`map`开始在`GameScreen`实例上调用`handleInput`、`update`和`draw`。您可以向地图添加任意多种不同类型的`Screen`实例。然而，我建议你在开始做你自己的定制或者开始你自己的游戏之前完成太空入侵者++ 项目。\n\n## 对位图存储类进行编码\n\n在名为`BitmapStore.h`的`Header Files/Engine`过滤器中创建新的头文件，并添加以下代码:\n\n```cpp\n#pragma once\n#ifndef BITMAP_STORE_H\n#define BITMAP_STORE_H\n#include <SFML/Graphics.hpp>\n#include <map>\nclass BitmapStore\n{\nprivate:\n    std::map<std::string, sf::Texture> m_BitmapsMap;\n    static BitmapStore* m_s_Instance;\npublic:\n    BitmapStore();\n    static sf::Texture& getBitmap(std::string const& filename);\n    static void addBitmap(std::string const& filename);\n};\n#endif\n```\n\n在名为`BitmapStore.cpp`的`Source Files/Engine`过滤器中创建新的源文件，并添加以下代码:\n\n```cpp\n#include \"BitmapStore.h\"\n#include <assert.h>\nusing namespace sf;\nusing namespace std;\nBitmapStore* BitmapStore::m_s_Instance = nullptr;\nBitmapStore::BitmapStore()\n{\n    assert(m_s_Instance == nullptr);\n    m_s_Instance = this;\n}\nvoid BitmapStore::addBitmap(std::string const& filename)\n{\n    // Get a reference to m_Textures using m_S_Instance\n    auto& bitmapsMap = m_s_Instance->m_BitmapsMap;\n    // auto is the equivalent of map<string, Texture>\n    // Create an iterator to hold a key-value-pair (kvp)\n    // and search for the required kvp\n    // using the passed in file name\n    auto keyValuePair = bitmapsMap.find(filename);\n    // auto is equivalent of map<string, Texture>::iterator\n    // No match found so save the texture in the map\n    if (keyValuePair == bitmapsMap.end())\n    {\n        // Create a new key value pair using the filename\n        auto& texture = bitmapsMap[filename];\n        // Load the texture from file in the usual way\n        texture.loadFromFile(filename);\n    }\n}\nsf::Texture& BitmapStore::getBitmap(std::string const& filename)\n{\n    // Get a reference to m_Textures using m_S_Instance\n    auto& m = m_s_Instance->m_BitmapsMap;\n    // auto is the equivalent of map<string, Texture>\n    // Create an iterator to hold a key-value-pair (kvp)\n    // and search for the required kvp\n    // using the passed in file name\n    auto keyValuePair = m.find(filename);\n    // auto is equivalent of map<string, Texture>::iterator    \n    // Did we find a match?\n    if (keyValuePair != m.end())\n    {\n        return keyValuePair->second;\n    }\n    else\n    {\n#ifdef debuggingOnConsole         \n        cout << \n            \"BitmapStore::getBitmap()Texture not found Crrrashh!\" \n            << endl;\n#endif\n        return keyValuePair->second;\n    }\n}\n```\n\n前面的代码几乎是从前面两个项目的`BitmapStore`类复制粘贴而来的，除了最后的`else`块。在最后的`else`块中，我们第一次使用 C++ 断言在没有找到纹理的情况下向控制台输出所请求纹理的名称。这只有在定义`debuggingOnConsole`时才会发生。请注意，这也会使游戏崩溃。\n\n## 对 ScreenManagerRemoteControl 类进行编码\n\n在名为`ScreenManagerRemoteControl.h`的`Header Files/Screens`过滤器中创建新的头文件，并添加以下代码:\n\n```cpp\n#pragma once\n#include <string>\n#include <vector>\n//#include \"GameObject.h\"\n//#include \"GameObjectSharer.h\"\nusing namespace std;\nclass ScreenManagerRemoteControl\n{\npublic:\n    virtual void SwitchScreens(string screenToSwitchTo) = 0;\n    virtual void loadLevelInPlayMode(string screenToLoad) = 0;\n    //virtual vector<GameObject>& getGameObjects() = 0;\n    //virtual GameObjectSharer& shareGameObjectSharer() = 0;\n};\n```\n\n注意在前面的代码中，有一些`#include`语句和一些函数被注释掉了。这是因为在下一章之前，我们不会对`GameObject`和`GameObjectSharer`类进行编码。\n\n代码的其余部分是与我们之前在`ScreenManager.h`文件中看到的定义相匹配的原型。正如您所期望的，所有的函数都是纯虚函数，因此必须由我们希望拥有实例的任何类来实现。\n\n在名为`ScreenManagerRemoteControl.cpp`的`Source Files/Screens`过滤器中创建新的源文件，并添加以下代码:\n\n```cpp\n/*********************************\n******THIS IS AN INTERFACE********\n*********************************/\n```\n\n该代码文件为空，因为所有代码都在`.h`文件中。其实你不需要创建这个文件，但是我总觉得它是一个方便的提醒，以防我忘记了类的所有函数都是纯虚的，浪费时间去找`.cpp`文件，这个文件是不存在的。\n\n# 我们现在在哪里？\n\n在这个阶段，代码中唯一剩下的错误是引用`SelectScreen`类和`GameScreen`类的错误。要消除这些错误并拥有一个可运行的程序需要相当多的工作。这样做的原因是`SelectScreen`和`GameScreen`来源于`Screen`，反过来`Screen`类也依赖于`InputHandler`、`UIPanel`和`Button`。我们接下来会去找他们。\n\n# 对屏幕类及其依赖项进行编码\n\n我们现在要做的是对所有与屏幕相关的类进行编码。此外，我们游戏中的每一个屏幕都将有它们自己对所有这些类的具体实现。\n\n接下来，我们将对所有的基类进行编码；`Screen`、`InputHandler`、`UIPanel`和`Button`。接下来，我们将完成这些类的`SelectScreen`派生的完整实现和`GameScreen`派生的部分实现。此时，我们将能够运行游戏并看到我们的屏幕、用户界面面板和按钮正在运行，并且还能够在屏幕之间切换。在下一章中，我们将对游戏进行适当的工作，并实现`GameObject`和`LevelManager`。在 [*第 22 章*](22.html#_idTextAnchor445)*使用游戏对象和构建游戏中，*我们将看到如何在`GameScreen`类中使用它们。\n\n## 对按钮类进行编码\n\n在名为`Button.h`的`Header Files/Screens`过滤器中创建新的头文件，并添加以下代码:\n\n```cpp\n#pragma once\n#include <SFML/Graphics.hpp>\nusing namespace sf;\nclass Button\n{\nprivate:\n    RectangleShape m_Button;\n    Text m_ButtonText;\n    Font m_Font;\npublic:\n    std::string m_Text;\n    FloatRect m_Collider;\n    Button(Vector2f position, \n        float width, float height, \n        int red, int green, int blue, \n        std::string text);\n    void draw(RenderWindow& window);\n};\n```\n\n从前面的代码中可以看到，一个按钮在视觉上由一个 SFML `RectangleShape`实例和一个 SFML `Text`实例表示。还要注意的是，有一个名为`m_Collider`的`FloatRect`实例将用于检测鼠标点击按钮。构造函数将接收参数来配置按钮的位置、大小、颜色和文本。该按钮将在游戏循环的每一帧绘制一次，并有一个`draw`功能，该功能接收一个`RenderWindow`引用以启用该功能。\n\n在名为`Button.cpp`的`Source Files/Screens`过滤器中创建新的源文件，并添加以下代码:\n\n```cpp\n#include \"Button.h\"\nButton::Button(Vector2f position, \n    float width, float height, \n    int red, int green, int blue, \n    std::string text)\n{\n    m_Button.setPosition(position);\n    m_Button.setFillColor(sf::Color(red, green, blue));\n    m_Button.setSize(Vector2f(width, height));\n    m_Text = \"\" + text;\n    float textPaddingX = width /10;\n    float textPaddingY= height / 10;\n    m_ButtonText.setCharacterSize(height * .7f);\n    m_ButtonText.setString(text);\n    m_Font.loadFromFile(\"fonts/Roboto-Bold.ttf\");\n    m_ButtonText.setFont(m_Font);\n    m_ButtonText.setPosition(Vector2f((position.x + textPaddingX),\n        (position.y + textPaddingY)));\n    m_Collider = FloatRect(position, Vector2f(width, height));\n}\nvoid Button::draw(RenderWindow& window)\n{\n    window.draw(m_Button);\n    window.draw(m_ButtonText);\n}\n```\n\n大多数动作发生在构造器中，在所有其他项目中，没有什么是我们在无数场合中没有见过的。按钮准备使用传递给构造函数的所有值来绘制。\n\n`draw`功能使用`RenderWindow`参照在先前配置的`RectangleShape`实例上绘制先前配置的`Text`实例。\n\n## 对 UIPanel 类进行编码\n\n在名为`UIPanel.h`的`Header Files/Screens`过滤器中创建新的头文件，并添加以下代码:\n\n```cpp\n#pragma once\n#include <SFML/Graphics.hpp>\n#include \"Button.h\"\nusing namespace std;\nclass UIPanel {\nprivate:\n    RectangleShape m_UIPanel;\n    bool m_Hidden = false;\n    vector<shared_ptr<Button>> m_Buttons;\nprotected:\n    float m_ButtonWidth = 0;\n    float m_ButtonHeight = 0;\n    float m_ButtonPadding = 0;\n    Font m_Font;\n    Text m_Text;\n    void addButton(float x, float y, int width, int height,\n        int red, int green, int blue,\n        string label);\n\npublic:\n    View m_View;\n    UIPanel(Vector2i res, int x, int y, \n        float width, float height, \n        int alpha, int red, int green, int blue);\n    vector<shared_ptr<Button>> getButtons();\n    virtual void draw(RenderWindow& window);\n    void show();\n    void hide();\n};\n```\n\n`UIPanel`类的`private`部分由一个`RectangleShape`和一个`vector`组成，前者将直观地表示面板的背景，后者将跟踪面板当前是否对玩家可见，后者将保存该面板的所有`Button`实例。请注意，智能指针是共享的，因此我们可以传递它们，并让`shared_pointer`类负责计算引用，并在必要时删除内存。\n\n在`protected`部分，有成员变量用于记住按钮的大小和间距，还有一个`Text`和一个`Font`实例用于在面板上绘制文本。这个项目中的所有面板只有一个`Text`实例，但是特定的派生类可以根据需要自由添加额外的成员。例如，一个`HighScoreUIPanel`类可能需要一个充满`vector`实例的`Text`来绘制最高分数的列表。\n\n还有一个`addButton`函数，就是这个函数将调用`Button`类构造函数，并将实例添加到`vector`中。\n\n在`public`部分，我们可以看到每个`UIPanel`实例都有自己的`View`实例。这使得每个面板和屏幕可以随意配置其`View`。所有`View`实例将被绘制并添加到图层中的`RenderWindow`中。\n\n`UIPanel`构造器接收所有必要的尺寸和颜色来配置其`RectangleShape`。`getButtons`功能共享`Button`实例的`vector`，以便其他类可以与按钮交互。例如，`InputHandler`类需要按钮来检测鼠标点击。这就是为什么我们使用共享智能指针。\n\n`draw`函数当然在游戏循环的每一帧都被调用一次，是`virtual`，所以它可以被派生类可选地覆盖和自定义。`show`和`hide`功能将切换`m_Hidden`的值，以跟踪该面板当前是否对玩家可见。\n\n在名为`UIPanel.cpp`的`Source Files/Screens`过滤器中创建新的源文件，并添加以下代码:\n\n```cpp\n#include \"UIPanel.h\"\nUIPanel::UIPanel(Vector2i res, int x, int y, \n    float width, float height, \n    int alpha, int red, int green, int blue)\n{\n    m_UIPanel.setFillColor(sf::Color(red, green, blue, alpha));\n    // How big in pixels is the UI panel\n    m_UIPanel.setSize(Vector2f(width, height));\n    // How big in pixels is the view\n    m_View.setSize(Vector2f(width, height));\n\n    // Where in pixels does the center of the view focus\n    // This is most relevant when drawing a portion \n   // of the game world\n    // width/2, height/2 ensures it is exactly centered around the\n    // RectangleShape, mUIPanel\n    m_View.setCenter(width / 2, height / 2);\n    // Where in the window is the view positioned?\n    float viewportStartX = 1.f / (res.x / x);\n    float viewportStartY = 1.f / (res.y / y);\n    float viewportSizeX = 1.f / (res.x / width);\n    float viewportSizeY = 1.f / (res.y / height);\n    // Params from left to right\n    // StartX as a fraction of 1, startY as a fraction of 1 \n    // SizeX as a fraction of 1\n    // SizeY as a fraction of 1\n    m_View.setViewport(FloatRect(viewportStartX, viewportStartY, \n        viewportSizeX, viewportSizeY));\n}\nvector<shared_ptr<Button>> UIPanel::getButtons()\n{\n    return m_Buttons;\n}\nvoid UIPanel::addButton(float x, float y, \n    int width, int height,\n    int red, int green, int blue,\n    string label)\n{\n    m_Buttons.push_back(make_shared<Button>(Vector2f(x, y), \n        width, height,\n        red, green, blue, \n        label));\n}\nvoid UIPanel::draw(RenderWindow & window)\n{\n    window.setView(m_View);\n    if (!m_Hidden) {\n        window.draw(m_UIPanel);\n        for (auto it = m_Buttons.begin(); \n            it != m_Buttons.end(); ++ it)\n        {\n            (*it)->draw(window);\n        }\n    }\n}\nvoid UIPanel::show()\n{\n    m_Hidden = false;\n}\nvoid UIPanel::hide()\n{\n    m_Hidden = true;\n}\n```\n\n在构造函数中，`RectangleShape`实例被缩放、着色和定位。`View`实例也会根据面板的大小进行缩放。`View`类的`setViewport`功能与一些额外的计算一起使用，以确保`View`占据屏幕相对于分辨率的正确比例，因此在不同分辨率的屏幕上看起来大致相同。\n\n`getButtons`功能只是将按钮的`vector`返回到调用代码。`addButtons`函数使用`make_shared`函数在堆上分配新的`Button`实例，并将它们放入`vector`中。\n\n`draw`功能使用`setView`功能使该面板的特定`View`实例成为被绘制的实例。接下来是`RectangleShape`，代表这个面板是画出来的。然后，`vector`中的每个按钮被循环穿过并画在`RectangleShape`的顶部。只有当`m_Hidden`为假时，才会出现这种情况。\n\n`show`和`hide`功能允许该类用户切换`m_Hidden`。\n\n## 对输入命令类进行编码\n\n在名为`InputHandler.h`的`Header Files/Screens`过滤器中创建新的头文件，并添加以下代码:\n\n```cpp\n#pragma once\n#include <SFML/Graphics.hpp>\n#include <vector>\n#include \"Button.h\"\n#include \"Screen.h\"\n#include \"ScreenManagerRemoteControl.h\"\nusing namespace sf;\nusing namespace std;\nclass Screen;\nclass InputHandler\n{\nprivate:\n    Screen* m_ParentScreen;\n    vector<shared_ptr<Button>> m_Buttons;\n    View* m_PointerToUIPanelView;\n    ScreenManagerRemoteControl* m_ScreenManagerRemoteControl;\npublic:\n    void initialiseInputHandler(\n        ScreenManagerRemoteControl* sw, \n        vector<shared_ptr<Button>>,\n        View* pointerToUIView, \n        Screen* parentScreen);\n    void handleInput(RenderWindow& window, Event& event);\n    virtual void handleGamepad();\n    virtual void handleKeyPressed(Event& event, \n        RenderWindow& window);\n    virtual void handleKeyReleased(Event& event, \n        RenderWindow& window);\n    virtual void handleLeftClick(string& buttonInteractedWith, \n        RenderWindow& window);\n    View* getPointerToUIView();\n    ScreenManagerRemoteControl* \n        getPointerToScreenManagerRemoteControl();\n    Screen* getmParentScreen();\n};\n```\n\n这个文件有错误，因为`Screen`类还不存在。\n\n首先，研究一下这个头文件的`private`部分。每个`InputHandler`实例都有一个指向其所在屏幕的指针。随着项目的继续，这在我们会遇到的一些情况下会很有用。还有一个共享智能指针的`vector`指向`Button`实例。这些都是我们刚刚编码的`UIPanel`中相同的`Button`实例。每个派生的`UIPanel`将有一个匹配的派生的`InputHandler`，与之共享一组`vector`按钮。\n\n`InputHandler`类还持有指向`UIPanel`中`View`实例的指针。当我们在`InputHandler.cpp`中对函数定义进行编码时，我们将看到我们如何获得这个指针以及它是如何有用的。\n\n还有一个指向`ScreenManagerRemoteControl`的指针。记得在`ScreenManager`类中，我们已经从`ScreenManagerRemoteControl`实现了一些功能。这将使我们能够使用`SwitchScreen`等功能。当您考虑到`InputHandler`是我们将检测按钮点击的类时，这非常有用。当然，我们需要看看如何初始化这个指针，使其可用。我们将很快在`InputHandler.cpp`文件中看到如何。\n\n在`public`部分，有一个`initialiseInputHandler`功能。这就是我们刚才谈到的`private`成员准备使用的地方。看参数；它们与私有成员的类型完全匹配。\n\n接下来是`handleInput`功能。记住`GameEngine`类每帧调用一次；`ScreenManager`在当前屏幕上调用它，而`Screen`类(编码在下一个)将依次在它持有的所有`InputHandler`实例上调用它。它接收一个`RenderWindow`和一个`Event`实例。\n\n接下来，有四个`virtual`函数，每个函数都是从`InputHandler`类派生的，如果需要，它可以选择覆盖这些函数。它们如下:\n\n*   `handleGamepad`\n*   `handleKeyPressed`\n*   `handleKeyReleased`\n*   `handleLeftClick`\n\n正如我们将很快看到的，在`InputHandler.cpp`文件中，`handleInput`函数将循环遍历`Event`中的数据，就像我们以前经常做的那样。但是，它不会像我们过去那样直接处理所有事件，而是将响应委托给四个虚拟函数之一。然后，派生类将只接收它们决定要处理的事件和数据。在`InputHandler.cpp`文件中提供了四个虚拟函数的默认和空定义。\n\n`getPointerToUIView`函数将指针返回到该`InputHandler`实例保存的面板`View`。我们将很快看到我们需要`View`来对按钮进行鼠标点击碰撞检测。\n\n`getPointerToScreenManagerRemoteControl`和`getmParentScreen`返回指向由函数名称建议的成员变量的指针。\n\n重要说明\n\n请注意，如果您使私有数据受到保护，那么派生的`InputHandler`类可以访问数据，而无需通过我们刚刚讨论的函数。项目完成后，如果您愿意，可以随时重新访问这一部分并进行更改。\n\n现在，我们可以对所有函数定义进行编码。\n\n在名为`InputHandler.cpp`的`Source Files/Screens`过滤器中创建新的源文件，并添加以下代码:\n\n```cpp\n#include <sstream>\n#include \"InputHandler.h\"\nusing namespace sf;\nusing namespace std;\nvoid InputHandler::initialiseInputHandler(\n    ScreenManagerRemoteControl* sw, \n    vector<shared_ptr<Button>> buttons,\n    View* pointerToUIView, \n    Screen* parentScreen)\n{\n    m_ScreenManagerRemoteControl = sw;\n    m_Buttons = buttons;\n    m_PointerToUIPanelView = pointerToUIView;\n    m_ParentScreen = parentScreen;\n}\nvoid InputHandler::handleInput(RenderWindow& window, \n    Event& event)\n{\n    // Handle any key presses\n    if (event.type == Event::KeyPressed)\n    {\n        handleKeyPressed(event, window);\n    }\n    if (event.type == Event::KeyReleased)\n    {\n        handleKeyReleased(event, window);\n    }\n    // Handle any left mouse click released\n    if (event.type == Event::MouseButtonReleased)\n    {\n        auto end = m_Buttons.end();\n        for (auto i = m_Buttons.begin();\n            i != end;\n            ++ i) {\n            if ((*i)->m_Collider.contains(\n                window.mapPixelToCoords(Mouse::getPosition(), \n                (*getPointerToUIView()))))\n            {\n                // Capture the text of the button that was interacted \n                // with and pass it to the specialised version \n                // of this class if implemented\n                handleLeftClick((*i)->m_Text, window);\n                break;\n            }\n        }\n    }\n    handleGamepad();    \n}\nvoid InputHandler::handleGamepad()\n{}// Do nothing unless handled by a derived class\nvoid InputHandler::handleKeyPressed(Event& event, \n    RenderWindow& window)\n{}// Do nothing unless handled by a derived class\nvoid InputHandler::handleKeyReleased(Event& event, \n    RenderWindow& window)\n{}// Do nothing unless handled by a derived class\nvoid InputHandler::handleLeftClick(std::\n    string& buttonInteractedWith, \n    RenderWindow& window)\n{}// Do nothing unless handled by a derived class\nView* InputHandler::getPointerToUIView()\n{\n    return m_PointerToUIPanelView;\n}\nScreenManagerRemoteControl* \n    InputHandler::getPointerToScreenManagerRemoteControl()\n{\n    return m_ScreenManagerRemoteControl;\n}\nScreen* InputHandler::getmParentScreen() {\n    return m_ParentScreen;\n}\n```\n\n`initialiseInputHandler`函数初始化私有数据，正如我们已经讨论过的，四个`virtual`函数是空的，正如预期的那样，getter 函数返回指向私有成员的指针，就像我们说的那样。\n\n有趣的函数定义是`handleInput`函数，让我们来浏览一下。\n\n有一系列`if`语句，从之前的项目看应该很熟悉。每个`if`语句测试不同类型的事件，例如一个键被按下或一个键被释放。然而，调用适当的`virtual`函数，而不是处理事件。如果派生的`InputHandler`类覆盖了`virtual`函数，它将接收数据并开始处理事件。如果没有，那么就调用空的默认函数定义，什么都不会发生。\n\n当`MouseButtonReleased`事件发生时，`vector`中的每个`Button`实例被测试以查看点击是否发生在按钮内。这是通过在每个按钮中使用碰撞器上的`contains`功能并通过鼠标点击的位置来实现的。注意按钮坐标是相对于面板`View`而不是屏幕坐标。为此，`mapPixelToCoords`功能用于将鼠标点击的屏幕坐标转换为`View`对应的坐标。\n\n当检测到碰撞时，调用`handleLeftClick virtual`功能，来自按钮的文本被传入。派生的`InputHandler`类将根据按钮的文本处理按钮点击时发生的事情。\n\n`handleInput`函数中的最后一行代码调用最后一个`virtual`函数`handleGamepad`。任何实现该功能的衍生`InputHandler`类都有机会用游戏手柄对玩家的动作做出反应。在这个项目中，只有`GameInputHandler`会关心游戏手柄在做什么。如果你想的话，你可以调整这个项目，让玩家可以使用游戏手柄来浏览其他屏幕的菜单。\n\n## 对屏幕类进行编码\n\n在名为`Screen.h`的`Header Files/Screens`过滤器中创建新的头文件，并添加以下代码:\n\n```cpp\n#pragma once\n#include <SFML/Graphics.hpp>\n#include <vector>\n#include \"InputHandler.h\"\n#include \"UIPanel.h\"\n#include \"ScreenManagerRemoteControl.h\"\nclass InputHandler;\nclass Screen {\nprivate:\n    vector<shared_ptr<InputHandler>> m_InputHandlers;\n    vector<unique_ptr<UIPanel>> m_Panels;\nprotected:\n    void addPanel(unique_ptr<UIPanel> p, \n        ScreenManagerRemoteControl* smrc, \n        shared_ptr<InputHandler> ih);\n\npublic:\n    virtual void initialise();\n    void virtual update(float fps);\n    void virtual draw(RenderWindow& window);\n    void handleInput(RenderWindow& window);\n    View m_View;\n};\n```\n\n在前面代码的`private`部分，有一个指向`InputHandler`实例的共享智能指针向量。这是我们将存储所有派生的`InputHandler`实例的地方。`SelectScreen`实际上只会有一个`InputHandler`，而`GameScreen`会有两个，但是你可以想吃多少就吃多少。例如，考虑一个假设的设置屏幕，其中您可能有图形、声音、控制器、游戏性等选项。然后可以单击这些选项中的每一个，以显示一个唯一的`UIPanel`实例和一个相关的`InputHandler`。因此，我们本可以避免在这个项目中使用`vector`，但任何重大项目最终几乎肯定都需要`vector`。智能指针属于共享类型，这表明我们将在某个时候通过函数传递内容。\n\n下一个成员是指向`UIPanel`实例的唯一智能指针的`vector`。这是所有衍生的`UIPanel`实例将去的地方。指针的独特多样性表明我们将不共享指针；如果我们这样做，我们将不得不转移责任。\n\n在受保护的部分是`addPanel`功能，这是一个`Screen`将传入一个新的`UIPanel`实例的所有细节的地方，包括它相关的`InputHandler`。注意接收`ScreenManagerRemoteControl`指针的参数；请记住，这是传递到`InputHandler`所必需的。\n\n还有一个`initialise`函数，我们将很快看到它的用途。最后三个函数是`virtual`函数，即`update`、`draw`和`handleInput`，派生的`Screen`类可以根据需要覆盖这些函数。\n\n最后，注意`View`实例。每个`Screen`实例也会有自己的`View`实例来绘制，就像每个`UIPanel`一样。\n\n让我们看看刚刚讨论的功能的实现。\n\n在名为`Screen.cpp`的`Source Files/Screens`过滤器中创建新的源文件，并添加以下代码:\n\n```cpp\n#include \"Screen.h\"\nvoid Screen::initialise(){}\nvoid Screen::addPanel(unique_ptr<UIPanel> uip, \n    ScreenManagerRemoteControl* smrc, \n    shared_ptr<InputHandler> ih)\n{\n    ih->initialiseInputHandler(smrc, \n        uip->getButtons(), &uip->m_View, this);\n    // Use move() because otherwise \n    // the vector has a COPY which is not allowed\n    m_Panels.push_back(move(uip));        \n    m_InputHandlers.push_back(ih);\n}\nvoid Screen::handleInput(RenderWindow& window)\n{\n    Event event;\n    auto itr = m_InputHandlers.begin();\n    auto end = m_InputHandlers.end();\n    while (window.pollEvent(event))\n    {\n        for (itr;\n            itr != end;\n            ++ itr)\n        {\n            (*itr)->handleInput(window, event);\n        }\n    }\n}\nvoid Screen::update(float fps){}\nvoid Screen::draw(RenderWindow& window)\n{    \n    auto itr = m_Panels.begin();\n    auto end = m_Panels.end();\n    for (itr;\n        itr != end;\n        ++ itr)\n    {\n        (*itr)->draw(window);\n    }    \n}\n```\n\n`initialise`功能为空。它被设计为被覆盖。\n\n我们已经知道，`addPanel`函数存储传递给它的`InputHandler`和`UIPanel`实例。当传入一个`InputHandler`时，调用`initialiseInputHandler`函数，传入三个东西。首先是`Button`实例的`vector`，其次是相关`UIPanel`实例的`View`实例，第三是`this`参数。在当前上下文中，`this`是指向`Screen`实例本身的指针。为什么不参考`InputHandler`类，验证这些论点是否正确，它们会发生什么？\n\n接下来，面板和输入处理器被添加到适当的`vector`中。然而，如果你仔细观察，会发现一些有趣的事情。再看看将名为`uip`的`UIPanel`实例添加到`m_Panels`向量的代码行:\n\n```cpp\nm_Panels.push_back(move(uip));\n```\n\n传递给`push_back`的参数包含在对`move`的调用中。这将唯一指针的责任转移到了`vector`中的`UIPanel`。在此点之后使用`uip`的任何尝试都将导致读取访问冲突，因为`uip`现在是空指针。然而`m_Panels`中的指针是好走的。您可能会同意这比使用常规指针并计算出在哪里删除它更简单。\n\n`handleInput`函数循环遍历每个事件，依次传递给每个`InputHandler`。\n\n`update`函数在基类中没有功能，为空。\n\n`draw`函数循环遍历每个`UIPanel`实例，并调用它们的`draw`函数。\n\n现在，我们准备好对所有的派生类进行编码。我们将从选择屏幕(`SelectScreen`)开始，然后进入游戏屏幕(`GameScreen`)。不过，我们会先增加一节快速课。\n\n# 添加 WorldState.h 文件\n\n在名为`WorldState.h`的`Header Files/Engine`过滤器中创建新的头文件，并添加以下代码:\n\n```cpp\n#pragma once\nclass WorldState\n{\npublic:\n    static const int WORLD_WIDTH = 100;\n    static int WORLD_HEIGHT;\n    static int SCORE;\n    static int LIVES;\n    static int NUM_INVADERS_AT_START;\n    static int NUM_INVADERS;\n    static int WAVE_NUMBER;\n};\n```\n\n这些变量是公共的和静态的。因此，它们在整个项目中都是可访问的，并且保证只有一个实例。\n\n# 为选择屏幕编码派生类\n\n到目前为止，我们已经对代表用户界面的基本类进行了编码，并对游戏进行了合理的屏幕划分。接下来，我们将为它们中的每一个编写具体的实现。请记住，太空入侵者++ 将有两个屏幕:选择和游戏。选择屏幕将由`SelectScreen`类表示，并将有一个`UIPanel`实例、一个`InputHandler`实例和两个按钮。播放屏幕将由`GameScreen`类表示，它将有两个`UIPanel`实例。一个叫做`GameUIPanel`，会显示分数、生命和入侵者波数。另一个叫做`GameOverUIPanel`，会显示两个按钮，让玩家可以选择返回选择画面或者再次播放。由于`GameScreen`类由两个`UIPanel`实例组成，因此它也将由两个`InputHandler`实例组成。\n\n## 编码选择屏幕类别\n\n在名为`SelectScreen.h`的`Header Files/Screens/Select`过滤器中创建新的头文件，并添加以下代码:\n\n```cpp\n#pragma once\n#include \"Screen.h\"\nclass SelectScreen : public Screen\n{\nprivate:\n    ScreenManagerRemoteControl* m_ScreenManagerRemoteControl;\n    Texture m_BackgroundTexture;\n    Sprite m_BackgroundSprite;\npublic:\n    SelectScreen(ScreenManagerRemoteControl* smrc, Vector2i res);\n    void virtual draw(RenderWindow& window);\n};\n```\n\n`SelectScreen`类继承自`Screen`。在前面代码的`private`部分，有一个用于切换屏幕的`ScreenManagerRemoteControl`指针，以及一个用于绘制背景的`Texture`实例和`Sprite`实例。\n\n在`public`部分，我们可以看到覆盖`draw`函数的构造函数和原型。`SelectScreen`类不需要覆盖`update`功能。\n\n在名为`SelectScreen.cpp`的`Source Files/Screens/Select`过滤器中创建新的源文件，并添加以下代码:\n\n```cpp\n#include \"SelectScreen.h\"\n#include \"SelectUIPanel.h\"\n#include \"SelectInputHandler.h\"\nSelectScreen::SelectScreen(\n    ScreenManagerRemoteControl* smrc, Vector2i res)\n{\n    auto suip = make_unique<SelectUIPanel>(res);\n    auto sih = make_shared<SelectInputHandler>();\n    addPanel(move(suip), smrc, sih);\n    m_ScreenManagerRemoteControl = smrc;\n    m_BackgroundTexture.loadFromFile(\"graphics/background.png\");\n    m_BackgroundSprite.setTexture(m_BackgroundTexture);\n    auto textureSize = m_BackgroundSprite.\n        getTexture()->getSize();\n\n    m_BackgroundSprite.setScale(float(\n        m_View.getSize().x) / textureSize.x,        \n        float(m_View.getSize().y) / textureSize.y);\n}\nvoid SelectScreen::draw(RenderWindow& window)\n{\n    // Change to this screen's view to draw\n    window.setView(m_View);\n    window.draw(m_BackgroundSprite);\n    // Draw the UIPanel view(s)\n    Screen::draw(window);\n}\n```\n\n在构造函数中，到目前为止所有编码的目的开始结合在一起。`make_unique`函数用于创建指向`SelectUIPanel`实例的唯一智能指针。我们将在几节时间内对`SelectUIPanel`进行编码。接下来，`make_shared`函数用于创建指向`SelectInputHandler`实例的共享智能指针。接下来我们将对`SelectInputHandler`类进行编码。现在我们有了适当形式的`UIPanel`和`InputHandler`，我们可以调用`addPanel`函数，并将它们都传入。注意，在对`addPanel`的调用中，`suip`被包装在对`move`的调用中。在此点之后，任何使用`suip`的操作都不可能不使程序崩溃，因为它现在是一个空指针，因为所有权已经转移到函数参数。请记住，在`Screen`类`addPanel`函数中，当指向`UIPanel`的唯一指针隐藏在`UIPanel`实例的`vector`中时，所有权会再次移动。\n\n随后，`ScreenManagerRemoteControl`指针被初始化，现在可以在需要时切换到另一个屏幕。\n\n构造函数中的最后几行代码创建并缩放一个使用`background.png`图像的`Sprite`实例，该图像将填充整个屏幕。\n\n在`draw`函数中，对`setView`函数的调用使该面板的`View`实例成为要绘制的实例，然后`Sprite`实例被绘制到`RenderWindow`实例。\n\n最后在基础`Screen`类上调用`draw`函数，绘制所有面板及其相关按钮。在这个具体的例子中，它只绘制了一个面板`SelectUIPanel`，我们将在编码完`SelectInputHandler`后立即对其进行编码。\n\n## 编码选择作者类\n\n在名为`SelectInputHandler.h`的`Header Files/Screens/Select`过滤器中创建新的头文件，并添加以下代码:\n\n```cpp\n#pragma once\n#include \"InputHandler.h\"\nclass SelectInputHandler : public InputHandler\n{\npublic:\n    void handleKeyPressed(Event& event, \n        RenderWindow& window) override;\n    void handleLeftClick(std::string& buttonInteractedWith, \n        RenderWindow& window) override;\n};\n```\n\n`SelectInputHandler`类继承自`InputHandler`并覆盖`handleKeyPressed`和`handleLeftClick`函数。让我们看看这些功能是如何实现的。\n\n在名为`SelectInputHandler.cpp`的`Source Files/Screens/Select`过滤器中创建新的源文件，并添加以下代码:\n\n```cpp\n#include \"SelectInputHandler.h\"\n#include \"SoundEngine.h\"\n#include \"WorldState.h\"\n#include <iostream>\nint WorldState::WAVE_NUMBER;\nvoid SelectInputHandler::handleKeyPressed(\n    Event& event, RenderWindow& window)\n{\n    // Quit the game\n    if (Keyboard::isKeyPressed(Keyboard::Escape))\n    {\n        window.close();\n    }\n}\nvoid SelectInputHandler::handleLeftClick(\n    std::string& buttonInteractedWith, RenderWindow& window)\n{\n    if (buttonInteractedWith == \"Play\") {\n        SoundEngine::playClick();\n        WorldState::WAVE_NUMBER = 0;\n        getPointerToScreenManagerRemoteControl()\n            ->loadLevelInPlayMode(\"level1\");\n    }\n    if (buttonInteractedWith == \"Quit\") {\n        SoundEngine::playClick();\n        window.close();\n    }\n}\n```\n\n`handleKeyPressed`功能只需一个键盘按键即可交互。按下*退出*键，游戏退出。\n\n在`handleLeftClick`功能中，有两个`if`语句。请记住，`InputHandler`类的`handleInputFunction`传递被点击的按钮的文本，以及对`RenderWindow`的引用。如果点击**播放**按钮，则播放一声点击声，`WAVE_NUMBER`变量置零，`ScreenManagerRemoteControl`指针调用`loadLevelInPlayMode`功能。`loadLevelInPlayMode`功能在`ScreenManagerClass`中有定义。最终，这个函数确实会从传入的文件名中加载一个级别，但现在，它只是将屏幕更改为播放屏幕。\n\n如果点击**退出**按钮，则退出游戏。\n\n重要说明\n\n在此阶段，尽管包含`WorldState.h`，但使用`WorldState::WaveNumber`时可能会出现错误。这很好；发生这种情况是因为 Visual Studio 解析类的顺序。当我们添加所有同样使用`WorldState.h`的游戏画面相关类时，也就是在这个文件之前解析的时候，错误就会消失。\n\n我们来编码`SelectUIPanel`。然后，我们可以继续进行`GameScreen`课。\n\n## 编码选择面板类\n\n在名为`SelectUIPanel.h`的`Header Files/Screens/Select`过滤器中创建新的头文件，并添加以下代码:\n\n```cpp\n#pragma once\n#include \"UIPanel.h\"\nclass SelectUIPanel : public UIPanel\n{\nprivate:\n    void initialiseButtons();\npublic:\n    SelectUIPanel(Vector2i res);\n    void virtual draw(RenderWindow& window);\n};\n```\n\n`SelectUIPanel`类继承自`UIPanel`并覆盖`draw`函数。在前面的头文件中，还可以看到有一个名为`initialiseButtons`的函数，以及一个构造函数。让我们对定义进行编码。\n\n在名为`SelectUIPanel.cpp`的源`Files/Screens/Select`过滤器中创建新的源文件，并添加以下代码:\n\n```cpp\n#include \"SelectUIPanel.h\"\n#include <iostream>\nSelectUIPanel::SelectUIPanel(Vector2i res) :\n    // Create a new UIPanel  \n    // by calling the super-class constructor\n    UIPanel(res,\n        (res.x / 10) * 2, // Start 2/10 across\n        res.y / 3, // 1/3 of the resolution from the top\n        (res.x / 10) * 6, // as wide as 6/10 of the resolution\n        res.y / 3, // and as tall as 1/3 of the resolution\n        50, 255, 255, 255) // a, r, g, b\n{\n    m_ButtonWidth = res.x / 20;\n    m_ButtonHeight = res.y / 20;\n    m_ButtonPadding = res.x / 100;\n    m_Text.setFillColor(sf::Color(0, 255, 0, 255));\n    m_Text.setString(\"SPACE INVADERS ++\");\n    //https://www.dafont.com/roboto.font\n    m_Font.loadFromFile(\"fonts/Roboto-Bold.ttf\");\n    m_Text.setFont(m_Font);\n    m_Text.setPosition(Vector2f(m_ButtonPadding,\n        m_ButtonHeight + (m_ButtonPadding * 2)));\n    m_Text.setCharacterSize(160);\n    initialiseButtons();\n}\nvoid SelectUIPanel::initialiseButtons()\n{\n    // Buttons are positioned relative to the top left \n    // corner of the UI panel(m_View in UIPanel)\n    addButton(m_ButtonPadding,\n        m_ButtonPadding,\n        m_ButtonWidth,\n        m_ButtonHeight,\n        0, 255, 0,\n        \"Play\");\n    addButton(m_ButtonWidth + (m_ButtonPadding * 2),\n        m_ButtonPadding,\n        m_ButtonWidth,\n        m_ButtonHeight,\n        255, 0, 0,\n        \"Quit\");\n}\nvoid SelectUIPanel::draw(RenderWindow& window)\n{    \n        show();\n        UIPanel::draw(window);\n        window.draw(m_Text);        \n}\n```\n\n构造函数接收屏幕分辨率，并立即使用该数据调用超类构造函数。通过计算`res`中存储的值，计算面板的起始位置和尺寸。重要的是，这个计算在这里进行，而不是在`UIPanel`类中进行，因为每个`UIPanel`都有不同的大小和不同的位置。如果您对每个特定计算的效果感兴趣，请查看前面代码中的注释。颜色也使用 alpha、红色、绿色和蓝色值传递。\n\n接下来，基类中决定按钮大小和间距的成员变量被初始化。`20`的值只是一个可以工作的任意值，但重要的是所有的值都是基于屏幕的分辨率，所以它们会在不同的屏幕分辨率上很好地缩放。\n\n接下来的几行代码准备了一个`Text`实例，准备在 draw 函数中显示。最后，在构造函数中，调用`initialiseButtons`函数。\n\n在`initialiseButtons`功能中，`addButton`功能被调用了两次，创建了一个上面有“播放”的绿色按钮和一个上面有“退出”的红色按钮。\n\n由于使用了`WorldState.h` 文件，可能会出现一些错误。这些可以忽略，因为它们会在我们接下来的几节课中自我纠正。\n\n现在，我们可以对所有与游戏屏幕相关的类进行编码。\n\n# 为游戏画面编码派生类\n\n所有这些类的结构都与选择屏幕相关的类相同。不过，我一定会指出它们的不同之处。然而，大多数显著的差异将在接下来的三章中讨论，因为那时我们将对所有游戏对象和组件进行编码，然后将它们放在`GameScreen`类中工作。\n\n第一个区别是`GameScreen`类有两个`UIPanel`实例和两个`InputHandler`实例。\n\n## 编写游戏屏幕类的代码\n\n在名为`GameScreen.h`的`Header Files/Screens/Game`过滤器中创建新的头文件，并添加以下代码:\n\n```cpp\n#pragma once\n#include \"Screen.h\"\n#include \"GameInputHandler.h\"\n#include \"GameOverInputHandler.h\"\nclass GameScreen : public Screen\n{\nprivate:\n    ScreenManagerRemoteControl* m_ScreenManagerRemoteControl;\n    shared_ptr<GameInputHandler> m_GIH;\n    Texture m_BackgroundTexture;\n    Sprite m_BackgroundSprite;\npublic:\n    static bool m_GameOver;\n    GameScreen(ScreenManagerRemoteControl* smrc, Vector2i res);\n    void initialise() override;\n    void virtual update(float fps);\n    void virtual draw(RenderWindow& window);    \n};\n```\n\n请注意，这不是完成的代码，我们将在下一章向该文件添加更多功能。这只是足够的代码，以便我们可以运行游戏，并在本章末尾看到一些基本功能。\n\n该代码是`SelectScreen`类所熟悉的。我们也覆盖了`initialise`和`update`功能。此外，我们还添加了一个名为`m_GameOver`的布尔值，它将跟踪游戏当前是否正在进行。\n\n让我们继续讨论函数实现。\n\n在名为`GameScreen.cpp`的`Source Files/Screens/Game`过滤器中创建新的源文件，并添加以下代码:\n\n```cpp\n#include \"GameScreen.h\"\n#include \"GameUIPanel.h\"\n#include \"GameInputHandler.h\"\n#include \"GameOverUIPanel.h\"\n#include \"WorldState.h\"\nint WorldState::WORLD_HEIGHT;\nint WorldState::NUM_INVADERS;\nint WorldState::NUM_INVADERS_AT_START;\nGameScreen::GameScreen(ScreenManagerRemoteControl* smrc,\n    Vector2i res)\n{\n    m_GIH = make_shared<GameInputHandler>();\n    auto guip = make_unique<GameUIPanel>(res);\n    addPanel(move(guip), smrc, m_GIH);\n    auto m_GOIH = make_shared<GameOverInputHandler>();\n    auto gouip = make_unique<GameOverUIPanel>(res);\n    addPanel(move(gouip), smrc, m_GOIH);\n    m_ScreenManagerRemoteControl = smrc;\n    float screenRatio = VideoMode::getDesktopMode().width /\n        VideoMode::getDesktopMode().height;\n    WorldState::WORLD_HEIGHT = WorldState::WORLD_WIDTH /\n        screenRatio;\n    m_View.setSize(\n        WorldState::WORLD_WIDTH, WorldState::WORLD_HEIGHT);\n    m_View.setCenter(Vector2f(WorldState::WORLD_WIDTH /\n        2, WorldState::WORLD_HEIGHT / 2));\n\n    m_BackgroundTexture.loadFromFile(\"graphics/background.png\");\n    m_BackgroundSprite.setTexture(m_BackgroundTexture);\n    auto textureSize = m_BackgroundSprite.getTexture()->getSize();\n    m_BackgroundSprite.setScale(float(m_View.getSize().x) / \n      textureSize.x,\n        float(m_View.getSize().y) / textureSize.y);\n}\nvoid GameScreen::initialise()\n{\n    m_GIH->initialize();\n    WorldState::NUM_INVADERS = 0;\n    m_GameOver = false;\n    if (WorldState::WAVE_NUMBER == 0)\n    {\n        WorldState::NUM_INVADERS_AT_START = \n            WorldState::NUM_INVADERS;\n\n        WorldState::WAVE_NUMBER = 1;\n        WorldState::LIVES = 3;\n        WorldState::SCORE = 0;\n    }\n}\nvoid GameScreen::update(float fps)\n{\n    Screen::update(fps);\n    if (!m_GameOver)\n    {\n        if (WorldState::NUM_INVADERS <= 0)\n        {\n            WorldState::WAVE_NUMBER++ ;\n            m_ScreenManagerRemoteControl->\n                loadLevelInPlayMode(\"level1\");\n        }\n        if (WorldState::LIVES <= 0)\n        {\n            m_GameOver = true;\n        }\n    }\n}\nvoid GameScreen::draw(RenderWindow& window)\n{\n    // Change to this screen's view to draw\n    window.setView(m_View);\n    window.draw(m_BackgroundSprite);\n    // Draw the UIPanel view(s)\n    Screen::draw(window);\n}\n```\n\n除了两个`UIPanel`实例和两个`InputHandler`实例，在`SelectScreen`类中发生的一切都在这里发生。下一个区别是`GameScreen`确实实现了`update`功能。这是所有游戏对象将在游戏的每一帧更新的地方。\n\n下一个区别是我们在`initialise`和`update`功能中加入了一些游戏的基本逻辑。\n\n重要说明\n\n很抱歉`initialise`和`initialize`功能的拼写不一致。在目前的制作阶段改变它们更有可能在书中引入错误，而不是帮助你。\n\n在`initialize`函数中，代码调用`GameInputHandler`类的`initialize`函数，我们接下来将对其进行编码。`NUM_INVADERS`变量设置为零，`m_GameOver`设置为假。接下来，测试`WAVE_NUMBER`变量，如果它等于零，则`WorldState`类初始化其静态变量，为新游戏做好准备。\n\n在`update` 功能中，`m_GameOver`变量用于确定游戏是否正在运行，如果正在运行，则再进行两次测试。第一个测试是否所有的入侵者都被消灭了。在这个发展阶段，因为没有任何入侵者，这具有不断增加波数的效果。\n\n第二个测试检查玩家是否已经耗尽生命，如果已经耗尽，则`m_GameOver`设置为真。\n\n## 对 GameInputHandler 类进行编码\n\n在名为`GameInputHandler.h`的`Header Files/Screens/Game`过滤器中创建新的头文件，并添加以下代码:\n\n```cpp\n#pragma once\n#include \"InputHandler.h\"\nclass GameScreen;\nclass GameInputHandler : public InputHandler\n{\npublic:\n    void initialize();\n    void handleGamepad() override;\n    void handleKeyPressed(Event& event,\n        RenderWindow& window) override;\n    void handleKeyReleased(Event& event,\n        RenderWindow& window) override;\n};\n```\n\n这个类的工作方式与`SelectInputHandler`相同，但是我们需要覆盖更多的函数。我们将在这里为`initialize`、`handleGamepad`、`handleKeyPressed`和`handleKeyReleased`功能添加代码。\n\n这还不是完成的代码——我们将在下一章中为这个文件添加更多的特性。这只是足够的代码，这样我们就可以运行游戏，并在本章末尾看到一些基本功能。\n\n在名为`GameInputHandler.cpp`的`Source Files/Screens/Game`过滤器中创建新的源文件，并添加以下代码:\n\n```cpp\n#include \"GameInputHandler.h\"\n#include \"SoundEngine.h\"\n#include \"GameScreen.h\"\nvoid GameInputHandler::initialize() {\n}\nvoid GameInputHandler::handleGamepad()\n{\n}\nvoid GameInputHandler::handleKeyPressed(\n    Event& event, RenderWindow& window)\n{\n    // Handle key presses\n    if (event.key.code == Keyboard::Escape)\n    {\n        SoundEngine::playClick();\n        getPointerToScreenManagerRemoteControl()->\n            SwitchScreens(\"Select\");\n    }    \n}\nvoid GameInputHandler::handleKeyReleased(\n    Event& event, RenderWindow& window)\n{\n}\n```\n\n现在，我们只想给`handleKeyPressed`函数添加代码，但是为什么不添加前面代码中显示的其他空函数呢？当玩家按下*退出*键时，`ScreenMangerRemoteControl`指针调用`SwitchScreen`功能返回选择画面。\n\n这还不是完成的代码——我们将在下一章中为这个文件添加更多的特性。这只是足够的代码，这样我们就可以运行游戏，并在本章末尾看到一些基本功能。\n\n## 编码游戏面板类\n\n在名为`GameUIPanel.h`的`Header Files/Screens/Game`过滤器中创建新的头文件，并添加以下代码:\n\n```cpp\n#pragma once\n#include \"UIPanel.h\"\nclass GameUIPanel : public UIPanel\n{\npublic:\n    GameUIPanel(Vector2i res);\n    void draw(RenderWindow& window) override;\n};\n```\n\n像前面的`UIPanel`子类一样，我们将覆盖`draw`函数，并实现构造函数。现在让我们对这些函数进行编码。\n\n在名为`GameUIPanel.cpp`的`Source Files/Screens/Game`过滤器中创建新的源文件，并添加以下代码:\n\n```cpp\n#include \"GameUIPanel.h\"\n#include <sstream>\n#include \"WorldState.h\"\nint WorldState::SCORE;\nint WorldState::LIVES;\nGameUIPanel::GameUIPanel(Vector2i res) :\n    UIPanel(res,\n        1, // The left\n        1, // The top\n        res.x / 3, // 1/3 width screen\n        res.y / 12, \n        50, 255, 255, 255) // a, r, g, b \n{\n    m_Text.setFillColor(sf::Color(0, 255, 0, 255));\n    m_Text.setString(\"Score: 0 Lives: 3 Wave: 1\");\n    m_Font.loadFromFile(\"fonts/Roboto-Bold.ttf\");\n    m_Text.setFont(m_Font);\n    m_Text.setPosition(Vector2f(15,15));\n    m_Text.setCharacterSize(60);    \n}\nvoid GameUIPanel::draw(RenderWindow& window)\n{\n    UIPanel::draw(window);\n    std::stringstream ss;\n    ss << \"Score: \" << WorldState::SCORE << \"  Lives: \" \n        << WorldState::LIVES << \" Wave: \" \n        << WorldState::WAVE_NUMBER;\n    m_Text.setString(ss.str());\n    window.draw(m_Text);\n}\n```\n\n构造函数和`SelectUIPanel`类一样，调用基类构造函数来配置面板的位置、大小和颜色。此外，在构造器中，准备了一个`Text`实例来绘制到屏幕上。\n\n在`draw`功能中，`stringstream`实例用于连接一串显示玩家得分、剩余生命和清除波数的文本。`RenderWindow`实例然后将`Text`实例传递给其`draw`功能。\n\n## 对 GameOverInputHandler 类进行编码\n\n请记住，游戏屏幕将有两个面板和两个输入处理类。当玩家失去最后一条生命时，将显示面板上的游戏。这就是我们现在要编码的内容。\n\n在名为`GameOverInputHandler.h`的`Header Files/Screens/Game`过滤器中创建新的头文件，并添加以下代码:\n\n```cpp\n#pragma once\n#include \"InputHandler.h\"\nclass GameOverInputHandler :\n    public InputHandler\n{\npublic:\n    void handleKeyPressed(Event& event, \n        RenderWindow& window) override;\n    void handleLeftClick(std::string& \n        buttonInteractedWith, RenderWindow& window) override;\n};\n```\n\n与前面两个`InputHandler`派生类的头文件相比，前面的代码没有什么不同。\n\n在名为`GameOverInputHandler.cpp`的`Source Files/Screens/Game`过滤器中创建新的源文件，并添加以下代码:\n\n```cpp\n#include \"GameOverInputHandler.h\"\n#include \"SoundEngine.h\"\n#include \"WorldState.h\"\n#include <iostream>\nvoid GameOverInputHandler::handleKeyPressed(Event& event, \n    RenderWindow& window)\n{\n    if (event.key.code == Keyboard::Escape)\n    {\n        SoundEngine::playClick();\n        getPointerToScreenManagerRemoteControl()->\n            SwitchScreens(\"Select\");\n    }\n}\nvoid GameOverInputHandler::handleLeftClick(\n    std::string& buttonInteractedWith, RenderWindow& window)\n{\n    if (buttonInteractedWith == \"Play\") {\n        SoundEngine::playClick();\n        WorldState::WAVE_NUMBER = 0;\n        getPointerToScreenManagerRemoteControl()->\n            loadLevelInPlayMode(\"level1\");\n    }\n    else if (buttonInteractedWith == \"Home\") {\n        SoundEngine::playClick();\n        getPointerToScreenManagerRemoteControl()->\n            SwitchScreens(\"Select\");\n    }\n}\n```\n\n前面的代码处理两种类型的事件。首先，如果按下*退出*键盘键，游戏切换到选择画面。\n\n在`handleLeftClick`功能中，有两个不同的按钮被处理。如果点击**播放**按钮，则通过调用`loadLevelInPlayMode`开始新的游戏，而如果点击**主页**按钮，则显示选择屏幕。\n\n## 对游戏面板类进行编码\n\n在名为`GameOverUIPanel.h`的`Header Files/Screens/Game`过滤器中创建新的头文件，并添加以下代码:\n\n```cpp\n#pragma once\n#include \"UIPanel.h\"\nclass GameOverUIPanel :\n    public UIPanel\n{\nprivate:    \n    void initialiseButtons();\npublic:\n    GameOverUIPanel(Vector2i res);\n    void virtual draw(RenderWindow& window);\n};\n```\n\n在前面的头文件中没有什么新内容，所以让我们看看函数的实现\n\n在名为`GameOverUIPanel.cpp`的`Source Files/Screens/Game`过滤器中创建新的源文件，并添加以下代码:\n\n```cpp\n#include \"GameOverUIPanel.h\"\n#include \"GameScreen.h\"\nbool GameScreen::m_GameOver;\nGameOverUIPanel::GameOverUIPanel(Vector2i res) :\n    UIPanel(res,\n        (res.x / 10) * 3, \n        res.y / 2, // 50% of the resolution from the top\n        (res.x / 10) * 3, // as wide as 1/3 of the resolution\n        res.y / 6, // and as tall as 1/6 of the resolution\n        50, 255, 255, 255) // a, r, g, b    \n{\n    m_ButtonWidth = res.x / 20;\n    m_ButtonHeight = res.y / 20;\n    m_ButtonPadding = res.x / 100;\n    m_Text.setFillColor(sf::Color(0, 255, 0, 255));// Green\n    m_Text.setString(\"GAME OVER!\");\n    m_Font.loadFromFile(\"fonts/Roboto-Bold.ttf\");\n    m_Text.setFont(m_Font);\n    m_Text.setPosition(Vector2f(m_ButtonPadding, \n        (m_ButtonPadding * 2)+ m_ButtonHeight));\n    m_Text.setCharacterSize(60);\n    initialiseButtons();\n}\nvoid GameOverUIPanel::initialiseButtons()\n{\n    addButton(m_ButtonPadding,\n        m_ButtonPadding,\n        m_ButtonWidth,\n        m_ButtonHeight,\n        0, 255, 0,\n        \"Play\");\n    addButton(m_ButtonWidth + (m_ButtonPadding * 2),\n        m_ButtonPadding,\n        m_ButtonWidth,\n        m_ButtonHeight,\n        255, 0, 0,\n        \"Home\");\n}\nvoid GameOverUIPanel::draw(RenderWindow& window) \n{\n    if (GameScreen::m_GameOver)\n    {\n        show();\n        UIPanel::draw(window);\n        window.draw(m_Text);\n    }\n    else\n    {\n        hide();\n    }\n}\n```\n\n前面的代码在屏幕中间配置了一个面板，文本为**游戏结束！**和两个按钮，允许玩家重启游戏或退出，并返回开始屏幕(主页/选择)。\n\n# 运行游戏\n\n如果你运行游戏，你会看到选择屏幕，如下图所示:\n\n![](img/B14278_19_08.jpg)\n\n按**播放**过渡到游戏画面:\n\n![](img/B14278_19_09.jpg)\n\n按**退出**退出，返回选择画面。\n\n退出游戏，在`GameScreen`类找到下面一行代码:\n\n```cpp\nif (WorldState::LIVES <= 0)\n```\n\n将其更改为以下内容:\n\n```cpp\nif (true)\n```\n\n现在，再次运行游戏并选择**播放**按钮。将显示“游戏结束”面板，并且可以与以下内容进行交互:\n\n![](img/B14278_19_10.jpg)\n\n现在，把`GameScreen`班的`if (true)`改回`if (WorldState::LIVES <= 0)`。\n\n让我们休息一下；那是很长的一章。\n\n# 总结\n\n你在这一章取得了很大的成就。你已经为太空入侵者++ 游戏打下了坚实的基础，你还编写了一个可重用的系统，几乎可以用于任何被分成不同“屏幕”的游戏。\n\n我们现在有了一个输入处理系统，可以检测键盘按压和鼠标点击，并将处理它们的责任分配给特定屏幕的特定面板。此外，屏幕概念的抽象允许我们设置任意多的不同游戏循环。`GameScreen`类将是处理这个游戏逻辑的主要类，但是，一旦你看到在接下来的几章中，你可以很容易地编写另一个屏幕来玩一个完全不同的游戏。当然，你最有可能做的事情是从自己的想法开始。\n\n在下一章中，我们将对游戏对象和组件进行编码，这是实体-组件模式实现的基础。"
  },
  {
    "path": "docs/begin-cpp-game-prog/20.md",
    "content": "# 二十、游戏对象和组件\n\n在这一章中，我们将进行与上一章开始时讨论的实体-组件模式相关的所有编码。这意味着我们将对基础`Component`类进行编码，所有其他组件都将从该类中派生出来。我们也将很好地利用我们关于智能指针的新知识，这样我们就不必关心我们为这些组件分配的内存。我们也将在本章中对`GameObject`类进行编码。\n\n我们将在本章中讨论以下主题:\n\n*   准备对组件进行编码\n*   组件基类的编码\n*   对对撞机组件进行编码\n*   编码图形组件\n*   编码更新组件\n*   编码游戏对象类\n\n在开始编码之前，让我们进一步讨论这些组件。请注意，在本章中，我将尝试并强调实体-组件系统是如何结合在一起的，以及所有组件是如何组成一个游戏对象的。我不会解释我们已经看过很多次的每一行甚至每一块逻辑或与 SFML 相关的代码。由你来研究这些细节。\n\n# 准备对组件进行编码\n\n当你完成这一章时，会有很多错误，其中一些看起来不符合逻辑。例如，当一个类是您已经编码的类之一时，您会得到错误，说它不存在。这样做的原因是，当一个类中有错误时，其他类也不能可靠地使用它而不出错。正是由于所有类的互连性，我们直到下一章接近尾声时才会摆脱所有的错误并再次拥有可执行代码。将代码以更小的块添加到不同的类中是可能的，并且项目会更频繁地没有错误。然而，逐渐做一些事情意味着不断地进出课堂。当你在构建自己的项目时，这有时是一个很好的方法，但是我认为为这个项目做的最有启发性的事情是帮助你尽快完成它。\n\n# 对组件基类进行编码\n\n在名为`Component.h`的`Header Files/GameObjects`过滤器中创建新的头文件，并添加以下代码:\n\n```cpp\n#pragma once\n#include \"GameObjectSharer.h\"\n#include <string>\nusing namespace std;\nclass GameObject;\nclass Component {\npublic:\n    virtual string getType() = 0;\n    virtual string getSpecificType() = 0;\n    virtual void disableComponent() = 0;\n    virtual void enableComponent() = 0;\n    virtual bool enabled() = 0;\n    virtual void start(GameObjectSharer* gos, GameObject* self) = 0;\n};\n```\n\n这是每个游戏对象中每个组件的基类。纯虚函数意味着一个组件永远不能被实例化，并且必须总是从第一个继承。函数允许访问组件的类型和特定类型。组件类型包括碰撞器、图形、转换和更新，但是根据游戏的要求可以添加更多的类型。具体类型包括标准图形、入侵者更新、玩家更新等等。\n\n有两种功能允许启用和禁用组件。这很有用，因为在使用组件之前，可以测试该组件当前是否已启用。例如，您可以调用`enabled`函数来测试组件的更新组件在调用其`update`函数之前是否已启用，或者测试图形组件在调用其`draw`函数之前是否已启用。\n\n`start`函数可能是最有趣的函数，因为它有一个新的类类型作为参数之一。`GameObjectSharer`类将在所有游戏对象用其所有组件实例化后，授予对所有游戏对象的访问权限。这将使每个游戏对象中的每个组件都有机会查询细节，甚至获得指向另一个游戏对象中特定数据的指针。举个例子，所有入侵者的更新组件都需要知道玩家转换组件的位置，这样它就知道什么时候发射子弹。绝对可以在`start`功能中访问任何对象的任何部分。关键是每个特定的组件将决定他们需要什么，并且在关键的游戏循环期间不需要开始查询另一个游戏对象的细节。\n\n包含该组件的`GameObject`也被传递给`start`函数，这样任何组件都可以找到更多关于自身的信息。例如，图形组件需要了解变换组件，以便它知道在哪里绘制自己。作为第二个例子，入侵者和玩家飞船的更新组件将需要一个指向他们自己的碰撞器组件的指针，这样他们就可以在移动时更新它的位置。\n\n随着我们的进展，我们将看到更多`start`函数的用例。\n\n在名为`Component.cpp`的`Source Files/GameObjects`过滤器中创建新的源文件，并添加以下代码:\n\n```cpp\n/*********************************\n******THIS IS AN INTERFACE********\n*********************************/\n```\n\n由于`Component`类永远无法实例化，所以我在`Component.cpp`中放了前面的注释作为提醒。\n\n# 对对撞机组件进行编码\n\n太空入侵者++ 游戏将只有一个简单类型的碰撞器。它将是一个围绕物体的矩形框，就像我们在僵尸启示录和乒乓游戏中看到的那样。然而，很容易想象你可能需要其他类型的对撞机；也许是一个圆形的对撞机，或者是一个不包容的对撞机，比如我们在《托马斯迟到了》游戏中用于托马斯和鲍勃头部、脚部和侧面的对撞机。\n\n为此，将有一个基础`ColliderComponent`类(继承自`Component`)来处理所有碰撞器的基本功能，还有`RectColliderComponent`，它将添加一个包罗万象的矩形碰撞器的特定功能。新的碰撞器类型可以根据游戏开发的需要添加。\n\n接下来是具体对撞机的基类，`ColliderComponent`。\n\n## 编码碰撞组件类\n\n在名为`ColliderComponent.h`的`Header Files/GameObjects`过滤器中创建新的头文件，并添加以下代码:\n\n```cpp\n#pragma once\n#include \"Component.h\"\n#include <iostream>\nclass ColliderComponent : public Component\n{\nprivate:\n    string m_Type = \"collider\";\n    bool m_Enabled = false;\npublic:\n    /****************************************************\n    *****************************************************\n    From Component interface\n    *****************************************************\n    *****************************************************/\n    string Component::getType() {\n        return m_Type;\n    }\n    void Component::disableComponent() {\n        m_Enabled = false;\n    }\n    void Component::enableComponent() {\n        m_Enabled = true;\n    }\n    bool Component::enabled() {\n        return m_Enabled;\n    }\n   void Component::start(GameObjectSharer* gos, GameObject* self)\n   {\n\n    }\n};\n```\n\n`ColliderComponent`类继承自`Component`类。在前面的代码中，可以看到`m_Type`成员变量被初始化为`\"collider\"`，而`m_Enabled`被初始化为`false`。\n\n在`public`部分，代码覆盖了`Component`类的纯虚函数。研究它们以熟悉它们，因为它们在所有组件类中都以非常相似的方式工作。`getType`功能返回`m_Type`。`disableComponent`功能将`m_Enabled`设置为`false`。`enableComponent`功能将`m_Enabled`设置为`true`。`enabled`功能返回`m_Enabled`的值。`start`函数没有代码，但将被许多更具体的基于组件的类覆盖。\n\n在名为`ColliderComponent.cpp`的`Source Files/GameObjects`过滤器中创建新的源文件，并添加以下代码:\n\n```cpp\n/*\nAll Functionality in ColliderComponent.h\n*/\n```\n\n我在`ColliderComponent.cpp`中添加了前面的注释，提醒自己所有的功能都在头文件中。\n\n## 编码矩形碰撞组件类\n\n在名为`RectColliderComponent.h`的`Header Files/GameObjects`过滤器中创建新的头文件，并添加以下代码:\n\n```cpp\n#pragma once\n#include \"ColliderComponent.h\"\n#include <SFML/Graphics.hpp>\nusing namespace sf;\nclass RectColliderComponent : public ColliderComponent\n{\nprivate:\n    string m_SpecificType = \"rect\";\n    FloatRect m_Collider;\n    string m_Tag = \"\";\npublic:\n    RectColliderComponent(string name);\n    string getColliderTag();\n    void setOrMoveCollider(\n        float x, float y, float width, float height);\n\n    FloatRect& getColliderRectF();\n    /****************************************************\n    *****************************************************\n    From Component interface base class\n    *****************************************************\n    *****************************************************/\n    string getSpecificType() {\n        return m_SpecificType;\n    }\n\n    void Component::start(\n        GameObjectSharer* gos, GameObject* self) {}\n};\n```\n\n`RectColliderComponent`类继承自`ColliderComponent`类。它有一个初始化为`\"rect\"`的`m_SpecificType`变量。现在可以查询通用`Component`实例向量中的任何`RectColliderComponent`实例，并确定其具有类型`\"collider\"`和特定类型`\"rect\"`。由于`Component`类的纯虚函数，所有基于组件的类都将具有该功能。\n\n还有一个名为`m_Collider`的`FloatRect`实例将存储这个对撞机的坐标。\n\n在`public`部分，我们可以查看构造函数。请注意，它收到一个`string`。传入的值将是标识此`RectColliderComponent`所附加的游戏对象类型的文本，例如入侵者、子弹或玩家的船。这样就有可能确定什么类型的物体相互碰撞。\n\n在被重写的函数之前还有三个函数；记下它们的名称和参数，然后我们将在编码它们的定义时讨论它们。\n\n注意`getSpecificType`函数定义返回`m_SpecificType`。\n\n在名为`RectColliderComponent.cpp`的`Source Files/GameObjects`过滤器中创建新的源文件，并添加以下代码:\n\n```cpp\n#include \"RectColliderComponent.h\"\nRectColliderComponent::RectColliderComponent(string name) {\n    m_Tag = \"\" + name;\n}\nstring RectColliderComponent::getColliderTag() {\n    return m_Tag;\n}\nvoid RectColliderComponent::setOrMoveCollider(\n    float x, float y, float width, float height) {\n\n    m_Collider.left = x;\n    m_Collider.top = y;\n    m_Collider.width = width;\n    m_Collider.height = height;\n}\nFloatRect& RectColliderComponent::getColliderRectF() {\n    return m_Collider;\n}\n```\n\n在构造函数中，传入的`string`值被赋给`m_Tag`变量，`getColliderTag`函数通过类的实例使该值可用。\n\n`setOrMoveCollider`函数将`m_Collider`定位在作为参数传入的坐标上。\n\n`getColliderRectF`功能返回对`m_Collider`的引用。这是使用`FloatRect`类的`intersects`功能与另一台对撞机进行碰撞测试的理想选择。\n\n我们的对撞机现在已经完成，我们可以继续处理图形了。\n\n# 对图形组件进行编码\n\n太空入侵者++ 游戏将只有一种特定类型的图形组件。叫做`StandardGraphicsComponent`。与碰撞器组件一样，如果我们愿意，我们将实现一个基本的`GraphicsComponent`类，以便于添加其他图形相关的组件。比如经典的街机版《太空入侵者》，入侵者用两帧动画上下拍打手臂。一旦你看到了`StandardGraphicsComponent`是如何工作的，你将能够很容易地为另一个类(也许是`AnimatedGraphicsComponent`)编码，这个类每半秒钟左右就用一个不同的`Sprite`实例来绘制自己。你也可以有一个图形组件，它有一个着色器(也许是`ShaderGraphicsComponent`)来实现快速和酷的效果。除了这些，还有更多的可能性。\n\n## 对图形组件类进行编码\n\n在名为`GraphicsComponent.h`的`Header Files/GameObjects`过滤器中创建新的头文件，并添加以下代码:\n\n```cpp\n#pragma once\n#include \"Component.h\"\n#include \"TransformComponent.h\"\n#include <string>\n#include <SFML/Graphics.hpp>\n#include \"GameObjectSharer.h\"\n#include <iostream>\nusing namespace sf;\nusing namespace std;\nclass GraphicsComponent : public Component {\nprivate:\n    string m_Type = \"graphics\";\n    bool m_Enabled = false;\npublic:\n    virtual void draw(\n        RenderWindow& window,\n        shared_ptr<TransformComponent> t) = 0;\n    virtual void initializeGraphics(\n        string bitmapName,\n        Vector2f objectSize) = 0;\n    /****************************************************\n    *****************************************************\n    From Component interface\n    *****************************************************\n    *****************************************************/\n    string Component::getType() {\n        return m_Type;\n    }\n    void Component::disableComponent() {\n        m_Enabled = false;\n    }\n    void Component::enableComponent() {\n        m_Enabled = true;\n    }\n    bool Component::enabled() {\n        return m_Enabled;\n    }\n    void Component::start(\n        GameObjectSharer* gos, GameObject* self) {}\n};\n```\n\n前面的大部分代码实现了`Component`类的纯虚函数。`GraphicsComponent`类的新功能是`draw`函数，它有两个参数。第一个参数是对`RenderWindow`实例的引用，以便组件可以自己绘制，而第二个参数是指向`GameObject`的`TransformComponent`实例的共享智能指针，以便在游戏的每一帧都可以访问位置和比例等重要数据。\n\n`GraphicsComponent`类还有一个新功能就是`initializeGraphics`函数，它也有两个参数。第一个是代表要使用的图形文件的文件名的`string`值，而第二个是代表游戏世界中对象大小的`Vector2f`实例。\n\n前面两个函数都是纯虚函数，使得`GraphicsComponent`类抽象。任何继承自`GraphicsComponent`的类都需要实现这些功能。在下一节中，我们将看到`StandardGraphicsComponent`是如何做到的。\n\n在名为`GraphicsComponent.cpp`的`Source Files/GameObjects`过滤器中创建新的源文件，并添加以下代码:\n\n```cpp\n/*\nAll Functionality in GraphicsComponent.h\n*/\n```\n\n前面的注释提醒我们代码都在相关的头文件中。\n\n## 对标准图形组件类进行编码\n\n在名为`StandardGraphicsComponent.h`的`Header Files/GameObjects`过滤器中创建新的头文件，并添加以下代码:\n\n```cpp\n#pragma once\n#include \"Component.h\"\n#include \"GraphicsComponent.h\"\n#include <string>\nclass Component;\nclass StandardGraphicsComponent : public GraphicsComponent {\nprivate:\n    sf::Sprite m_Sprite;\n    string m_SpecificType = \"standard\";\npublic:\n    /****************************************************\n    *****************************************************\n    From Component interface base class\n    *****************************************************\n    *****************************************************/\n    string Component::getSpecificType() {\n        return m_SpecificType;\n    }\n\n    void Component::start(\n        GameObjectSharer* gos, GameObject* self) {\n    }\n    /****************************************************\n    *****************************************************\n    From GraphicsComponent\n    *****************************************************\n    *****************************************************/\n    void draw(\n        RenderWindow& window,\n        shared_ptr<TransformComponent> t) override;\n    void initializeGraphics(\n        string bitmapName,\n        Vector2f objectSize) override;\n};\n```\n\n`StandardGraphicsComponent`类有一个`Sprite`成员。它不需要一个`Texture`实例，因为这将从`BitmapStore`类的每一帧中获得。该类还覆盖了来自`Component`和`GraphicsComponent`类的所需功能。\n\n让我们为两个纯虚函数`draw`和`initializeGraphics`的实现编码。\n\n在名为`StandardGraphicsComponent.cpp`的`Source Files/GameObjects`过滤器中创建新的源文件，并添加以下代码:\n\n```cpp\n#include \"StandardGraphicsComponent.h\"\n#include \"BitmapStore.h\"\n#include <iostream>\nvoid StandardGraphicsComponent::initializeGraphics(\n    string bitmapName,\n    Vector2f objectSize)\n{\n    BitmapStore::addBitmap(\"graphics/\" + bitmapName + \".png\");\n    m_Sprite.setTexture(BitmapStore::getBitmap(\n        \"graphics/\" + bitmapName + \".png\"));\n    auto textureSize = m_Sprite.getTexture()->getSize();\n    m_Sprite.setScale(float(objectSize.x) / textureSize.x, \n        float(objectSize.y) / textureSize.y);    \n    m_Sprite.setColor(sf::Color(0, 255, 0)); \n}\nvoid StandardGraphicsComponent::draw(\n    RenderWindow& window,\n    shared_ptr<TransformComponent> t)\n{\n    m_Sprite.setPosition(t->getLocation());\n    window.draw(m_Sprite);\n}\n```\n\n在`initializeGraphics`函数中，调用`BitmapStore`类的`addBitmap`函数，传入图像的文件路径，以及游戏世界中对象的大小。\n\n接下来，检索刚刚添加到`BitmapStore`类的`Texture`实例，并将其设置为`Sprite`的图像。接下来，两个函数`getTexture`和`getSize`被链接在一起以获得纹理的大小。\n\n下一行代码使用`setScale`函数使`Sprite`与纹理大小相同，而纹理又被设置为游戏世界中该对象的大小。\n\n`setColor`功能然后将绿色应用于`Sprite`。这给了它更多一点复古的感觉。\n\n在`draw`功能中，`Sprite`使用`setPosition`和`TransformComponent`的`getLocation`功能移动到位。接下来我们将对`TransformComponent`类进行编码。\n\n最后一行代码将`Sprite`绘制到`RenderWindow`。\n\n# 对转换组件类进行编码\n\n在名为`TransformComponent.h`的`Header Files/GameObjects`过滤器中创建新的头文件，并添加以下代码:\n\n```cpp\n#pragma once\n#include \"Component.h\"\n#include<SFML/Graphics.hpp>\nusing namespace sf;\nclass Component;\nclass TransformComponent : public Component {\nprivate:\n    const string m_Type = \"transform\";\n    Vector2f m_Location;\n    float m_Height;\n    float m_Width;\npublic:\n    TransformComponent(\n        float width, float height, Vector2f location);\n    Vector2f& getLocation();\n    Vector2f getSize();\n    /****************************************************\n    *****************************************************\n    From Component interface\n    *****************************************************\n    *****************************************************/\n    string Component::getType()\n    {\n        return m_Type;\n    }\n    string Component::getSpecificType()\n    {\n        // Only one type of Transform so just return m_Type\n        return m_Type;\n    }\n    void Component::disableComponent(){}\n    void Component::enableComponent(){}\n    bool Component::enabled()\n    {\n        return false;\n    }\n    void Component::start(GameObjectSharer* gos, GameObject* self)    {}\n};\n```\n\n这个类有一个`Vector2f`存储对象在游戏世界中的位置，一个`float`存储高度，还有一个`float`存储宽度。\n\n在`public`部分，有一个构造函数，我们将用来设置这个类的实例，还有两个函数，`getLocation`和`getSize`，我们将用来共享对象的位置和大小。我们在编写`StandardGraphicsComponent`类时已经使用了这些函数。\n\n`TransformComponent.h`文件中剩余的代码是`Component`类的实现。\n\n在名为`TransformComponent.cpp`的`Source Files/GameObjects`过滤器中创建新的源文件，并添加以下代码:\n\n```cpp\n#include \"TransformComponent.h\"\nTransformComponent::TransformComponent(\n    float width, float height, Vector2f location)\n{\n    m_Height = height;\n    m_Width = width;\n    m_Location = location;\n}\nVector2f& TransformComponent::getLocation() \n{\n    return m_Location;\n}\nVector2f TransformComponent::getSize() \n{\n    return Vector2f(m_Width, m_Height);\n}\n```\n\n实现这个类的三个功能很简单。构造函数接收大小和位置，并初始化适当的成员变量。当请求时，`getLocation`和`getSize`功能返回该数据。请注意，这些值是通过引用返回的，因此可以通过调用代码进行修改。\n\n接下来，我们将对所有与更新相关的组件进行编码。\n\n# 编码更新组件\n\n正如您现在可能期望的那样，我们将编写一个从`Component`类继承的`UpdateComponent`类。它将拥有每个`UpdateComponent`需要的所有功能，然后我们将编码从`UpdateComponent`派生的类。这些将包含特定于游戏中各个对象的功能。对于这场比赛，我们将有`BulletUpdateComponent`、`InvaderUpdateComponent`和`PlayerUpdateComponent`。当你在自己的项目中工作，并且你想要游戏中的一个对象以一种特定的独特方式表现时，只要为它编写一个新的基于更新的组件，你就可以开始了。基于更新的组件定义行为。\n\n## 对更新组件类进行编码\n\n在名为`UpdateComponent.h`的`Header Files/GameObjects`过滤器中创建新的头文件，并添加以下代码:\n\n```cpp\n#pragma once\n#include \"Component.h\"\nclass UpdateComponent : public Component\n{\nprivate:\n    string m_Type = \"update\";\n    bool m_Enabled = false;\npublic:\n    virtual void update(float fps) = 0;\n\n    /****************************************************\n    *****************************************************\n    From Component interface\n    *****************************************************\n    *****************************************************/\n    string Component::getType() {\n        return m_Type;\n    }\n    void Component::disableComponent() {\n        m_Enabled = false;\n    }\n    void Component::enableComponent() {\n        m_Enabled = true;\n    }\n    bool Component::enabled() {\n        return m_Enabled;\n    }\n    void Component::start(\n        GameObjectSharer* gos, GameObject* self) {\n    }\n};\n```\n\n`UpdateComponent`只带来一个功能:`update`功能。这个函数是纯虚拟的，所以它必须由任何渴望成为`UpdateComponent`的可用实例的类来实现。\n\n在名为`UpdateComponent.cpp`的`Source Files/GameObjects`过滤器中创建新的源文件，并添加以下代码:\n\n```cpp\n/*\nAll Functionality in UpdateComponent.h\n*/\n```\n\n这是一个有用的注释，提醒我们这个类的所有代码都在相关的头文件中。\n\n## 对 BulletUpdateComponent 类进行编码\n\n在名为`BulletUpdateComponent.h`的`Header Files/GameObjects`过滤器中创建新的头文件，并添加以下代码:\n\n```cpp\n#pragma once\n#include \"UpdateComponent.h\"\n#include \"TransformComponent.h\"\n#include \"GameObjectSharer.h\"\n#include \"RectColliderComponent.h\"\n#include \"GameObject.h\"\nclass BulletUpdateComponent : public UpdateComponent\n{\nprivate:\n    string m_SpecificType = \"bullet\";\n    shared_ptr<TransformComponent> m_TC;\n    shared_ptr<RectColliderComponent> m_RCC;\n    float m_Speed = 75.0f;\n\n    int m_AlienBulletSpeedModifier;\n    int m_ModifierRandomComponent = 5;\n    int m_MinimumAdditionalModifier = 5;\n    bool m_MovingUp = true;\npublic:\n    bool m_BelongsToPlayer = false;\n    bool m_IsSpawned = false;\n    void spawnForPlayer(Vector2f spawnPosition);\n    void spawnForInvader(Vector2f spawnPosition);\n    void deSpawn();\n    bool isMovingUp();\n    /****************************************************\n    *****************************************************\n    From Component interface base class\n    *****************************************************\n    *****************************************************/\n    string Component::getSpecificType() {\n        return m_SpecificType;\n    }\n\n    void Component::start(\n        GameObjectSharer* gos, GameObject* self) {        \n        // Where is this specific invader\n        m_TC = static_pointer_cast<TransformComponent>(\n            self->getComponentByTypeAndSpecificType(\n                \"transform\", \"transform\"));\n        m_RCC = static_pointer_cast<RectColliderComponent>(\n            self->getComponentByTypeAndSpecificType(\n                \"collider\", \"rect\"));\n    }\n    /****************************************************\n    *****************************************************\n    From UpdateComponent\n    *****************************************************\n    *****************************************************/\n    void update(float fps) override;\n};\n```\n\n如果你想理解子弹的行为/逻辑，你需要花一些时间学习成员变量的名称和类型，因为我不会精确地解释子弹的行为；这些话题我们已经讨论过很多次了。然而，我要指出的是，有一些变量可以覆盖基本的东西，比如移动，有助于在一定范围内随机化每颗子弹速度的变量，以及识别子弹是属于玩家还是入侵者的布尔变量。\n\n你还不知道但必须在这里学习的关键是，每个`BulletUpdateComponent`实例将持有一个指向所属游戏对象的`TransformComponent`实例的共享指针和一个指向所属游戏对象的`RectColliderComponent`实例的共享指针。\n\n现在，仔细观察被覆盖的`start`函数。在`start`函数中，上述共享指针被初始化。代码通过使用所属游戏对象(`self`)的`getComponentByTypeAndSpecificType`功能来实现这一点，该功能是指向所属游戏对象的指针。我们将在后面的章节中对`GameObject`类进行编码，包括这个函数。\n\n在名为`BulletUpdate.cpp`的`Source Files/GameObjects`过滤器中创建新的源文件，并添加以下代码:\n\n```cpp\n#include \"BulletUpdateComponent.h\"\n#include \"WorldState.h\"\nvoid BulletUpdateComponent::spawnForPlayer(\n    Vector2f spawnPosition)\n{\n    m_MovingUp = true;\n    m_BelongsToPlayer = true;\n    m_IsSpawned = true;\n\n    m_TC->getLocation().x = spawnPosition.x;\n    // Tweak the y location based on the height of the bullet \n    // The x location is already tweaked to the center of the player\n    m_TC->getLocation().y = spawnPosition.y - m_TC->getSize().y;\n    // Update the collider\n    m_RCC->setOrMoveCollider(m_TC->getLocation().x,\n        m_TC->getLocation().y, \n        m_TC->getSize().x, m_TC->getSize().y);\n}\nvoid BulletUpdateComponent::spawnForInvader(\n    Vector2f spawnPosition)\n{\n    m_MovingUp = false;\n    m_BelongsToPlayer = false;\n    m_IsSpawned = true;\n    srand((int)time(0));\n    m_AlienBulletSpeedModifier = (\n        ((rand() % m_ModifierRandomComponent)))  \n        + m_MinimumAdditionalModifier;    \n    m_TC->getLocation().x = spawnPosition.x;\n    // Tweak the y location based on the height of the bullet \n    // The x location already tweaked to the center of the invader\n    m_TC->getLocation().y = spawnPosition.y;\n    // Update the collider\n    m_RCC->setOrMoveCollider(\n        m_TC->getLocation().x, m_TC->\n        getLocation().y, m_TC->getSize().x, m_TC->getSize().y);\n}\nvoid BulletUpdateComponent::deSpawn()\n{\n    m_IsSpawned = false;\n}\nbool BulletUpdateComponent::isMovingUp()\n{\n    return m_MovingUp;\n}\nvoid BulletUpdateComponent::update(float fps)\n{\n    if (m_IsSpawned)\n    {    \n        if (m_MovingUp)\n        {\n            m_TC->getLocation().y -= m_Speed * fps;\n        }\n        else\n        {\n            m_TC->getLocation().y += m_Speed / \n                m_AlienBulletSpeedModifier * fps;\n        }\n        if (m_TC->getLocation().y > WorldState::WORLD_HEIGHT \n            || m_TC->getLocation().y < -2)\n        {\n            deSpawn();\n        }\n        // Update the collider\n        m_RCC->setOrMoveCollider(m_TC->getLocation().x, \n            m_TC->getLocation().y, \n            m_TC->getSize().x, m_TC->getSize().y);\n    }\n}\n```\n\n前两个功能是`BulletUpdateComponent`类独有的；他们是`spawnForPlayer`和`spawnForInvader`。这两个函数都为成员变量、转换组件和碰撞器组件准备动作。每个人的方式都略有不同。例如，对于玩家拥有的子弹，它准备从玩家飞船的顶部向上移动屏幕，而子弹准备让入侵者从入侵者的底部向下移动屏幕。需要注意的关键是，所有这些都可以通过指向转换组件和碰撞器组件的共享指针来实现。此外，请注意`m_IsSpawned`布尔设置为真，使该更新组件的`update`功能准备好调用游戏的每一帧。\n\n在`update`功能中，子弹以适当的速度在屏幕上上下移动。它被测试看它是否已经从屏幕的顶部或底部消失，碰撞器被更新以环绕当前位置，这样我们就可以测试碰撞。\n\n这是我们在这本书里看到的同样的逻辑；新的是我们用来与组成这个游戏对象的其他组件进行通信的共享指针。\n\n子弹只需要产生并测试碰撞；我们将在接下来的两章中看到如何做到这一点。现在，我们将对入侵者的行为进行编码。\n\n## 对入侵日期组件类进行编码\n\n在名为`InvaderUpdateComponent.h`的`Header Files/GameObjects`过滤器中创建新的头文件，并添加以下代码:\n\n```cpp\n#pragma once\n#include \"UpdateComponent.h\"\n#include \"TransformComponent.h\"\n#include \"GameObjectSharer.h\"\n#include \"RectColliderComponent.h\"\n#include \"GameObject.h\"\nclass BulletSpawner;\nclass InvaderUpdateComponent : public UpdateComponent\n{\nprivate:\n    string m_SpecificType = \"invader\";\n    shared_ptr<TransformComponent> m_TC;\n    shared_ptr < RectColliderComponent> m_RCC;\n    shared_ptr < TransformComponent> m_PlayerTC;\n    shared_ptr < RectColliderComponent> m_PlayerRCC;\n    BulletSpawner* m_BulletSpawner;\n    float m_Speed = 10.0f;\n    bool m_MovingRight = true;\n    float m_TimeSinceLastShot;\n    float m_TimeBetweenShots = 5.0f;\n    float m_AccuracyModifier;\n    float m_SpeedModifier = 0.05;\n    int m_RandSeed;\npublic:\n    void dropDownAndReverse();\n    bool isMovingRight();\n    void initializeBulletSpawner(BulletSpawner* \n        bulletSpawner, int randSeed);\n    /****************************************************\n    *****************************************************\n    From Component interface base class\n    *****************************************************\n    *****************************************************/\n    string Component::getSpecificType() {\n        return m_SpecificType;\n    }\n    void Component::start(GameObjectSharer* gos, \n        GameObject* self) {\n\n        // Where is the player?\n        m_PlayerTC = static_pointer_cast<TransformComponent>(\n            gos->findFirstObjectWithTag(\"Player\")\n            .getComponentByTypeAndSpecificType(\n                \"transform\", \"transform\"));\n        m_PlayerRCC = static_pointer_cast<RectColliderComponent>(\n            gos->findFirstObjectWithTag(\"Player\")\n            .getComponentByTypeAndSpecificType(\n                \"collider\", \"rect\"));\n        // Where is this specific invader\n        m_TC = static_pointer_cast<TransformComponent>(\n            self->getComponentByTypeAndSpecificType(\n                \"transform\", \"transform\"));\n        m_RCC = static_pointer_cast<RectColliderComponent>(\n            self->getComponentByTypeAndSpecificType(\n                \"collider\", \"rect\"));\n    }\n    /****************************************************\n    *****************************************************\n    From UpdateComponent\n    *****************************************************\n    *****************************************************/\n    void update(float fps) override;    \n};\n```\n\n在类声明中，我们可以看到编码入侵者行为所需的所有特性。有一个指向转换组件的指针，以便入侵者可以移动，还有一个指向碰撞器组件的指针，以便它可以更新其位置并被碰撞:\n\n```cpp\nshared_ptr<TransformComponent> m_TC;\nshared_ptr < RectColliderComponent> m_RCC;\n```\n\n有指向玩家变换和碰撞器的指针，这样入侵者就可以查询玩家的位置，并决定何时发射子弹:\n\n```cpp\nshared_ptr < TransformComponent> m_PlayerTC;\nshared_ptr < RectColliderComponent> m_PlayerRCC;\n```\n\n接下来，有一个`BulletSpawner`实例，我们将在下一章进行编码。`BulletSpawner`职业将允许入侵者或玩家产生子弹。\n\n接下来是一大堆变量，我们将使用它们来控制速度、方向、射速、入侵者瞄准的精度以及发射子弹的速度。熟悉它们，因为它们将用于函数定义中相当深入的逻辑中:\n\n```cpp\nfloat m_Speed = 10.0f;\nbool m_MovingRight = true;\nfloat m_TimeSinceLastShot;\nfloat m_TimeBetweenShots = 5.0f;\nfloat m_AccuracyModifier;\nfloat m_SpeedModifier = 0.05;\nint m_RandSeed;\n```\n\n接下来，我们可以看到三个新的公共函数，系统的不同部分可以调用它们来使入侵者向下移动一点并向另一个方向前进，测试行进方向，并分别向前面提到的`BulletSpawner`类传递一个指针:\n\n```cpp\nvoid dropDownAndReverse();\nbool isMovingRight();\nvoid initializeBulletSpawner(BulletSpawner* \n        bulletSpawner, int randSeed);\n```\n\n一定要学习`start`函数，在那里初始化了入侵者和玩家的智能指针。现在，我们将对函数定义进行编码。\n\n在名为`InvaderUpdate.cpp`的`Source Files/GameObjects`过滤器中创建新的源文件，并添加以下代码:\n\n```cpp\n#include \"InvaderUpdateComponent.h\"\n#include \"BulletSpawner.h\"\n#include \"WorldState.h\"\n#include \"SoundEngine.h\"\nvoid InvaderUpdateComponent::update(float fps)\n{\n    if (m_MovingRight)\n    {\n        m_TC->getLocation().x += m_Speed * fps;\n    }\n    else\n    {\n        m_TC->getLocation().x -= m_Speed * fps;\n    }\n    // Update the collider\n    m_RCC->setOrMoveCollider(m_TC->getLocation().x, \n        m_TC->getLocation().y, m_TC->getSize().x, m_TC-\n      >getSize().y);\n    m_TimeSinceLastShot += fps;\n\n    // Is the middle of the invader above the \n   // player +- 1 world units\n    if ((m_TC->getLocation().x + (m_TC->getSize().x / 2)) > \n        (m_PlayerTC->getLocation().x - m_AccuracyModifier) &&\n        (m_TC->getLocation().x + (m_TC->getSize().x / 2)) < \n        (m_PlayerTC->getLocation().x + \n        (m_PlayerTC->getSize().x + m_AccuracyModifier)))\n    {\n        // Has the invader waited long enough since the last shot\n        if (m_TimeSinceLastShot > m_TimeBetweenShots)\n        {\n            SoundEngine::playShoot();\n            Vector2f spawnLocation;\n            spawnLocation.x = m_TC->getLocation().x + \n                m_TC->getSize().x / 2;\n            spawnLocation.y = m_TC->getLocation().y + \n                m_TC->getSize().y;\n            m_BulletSpawner->spawnBullet(spawnLocation, false);\n            srand(m_RandSeed);\n            int mTimeBetweenShots = (((rand() % 10))+1) / \n                WorldState::WAVE_NUMBER;\n            m_TimeSinceLastShot = 0;            \n        }\n    }\n}\nvoid InvaderUpdateComponent::dropDownAndReverse()\n{\n    m_MovingRight = !m_MovingRight;\n    m_TC->getLocation().y += m_TC->getSize().y;\n    m_Speed += (WorldState::WAVE_NUMBER) + \n        (WorldState::NUM_INVADERS_AT_START \n       - WorldState::NUM_INVADERS) \n        * m_SpeedModifier;\n}\nbool InvaderUpdateComponent::isMovingRight()\n{\n    return m_MovingRight;\n}\nvoid InvaderUpdateComponent::initializeBulletSpawner(\n    BulletSpawner* bulletSpawner, int randSeed)\n{\n    m_BulletSpawner = bulletSpawner;\n    m_RandSeed = randSeed;\n    srand(m_RandSeed);\n    m_TimeBetweenShots = (rand() % 15 + m_RandSeed);\n    m_AccuracyModifier = (rand() % 2);\n    m_AccuracyModifier += 0 + static_cast <float> (\n        rand()) / (static_cast <float> (RAND_MAX / (10)));\n}\n```\n\n代码太多了。实际上，里面没有我们以前没有见过的 C++ 代码。控制入侵者的行为完全是逻辑。让我们概述一下它的全部功能，为了方便起见，部分代码被重新打印。\n\n### 解释更新功能\n\n第一个`if`和`else`块根据情况将入侵者向右或向左移动每一帧:\n\n```cpp\nvoid InvaderUpdateComponent::update(float fps)\n{\n    if (m_MovingRight)\n    {\n        m_TC->getLocation().x += m_Speed * fps;\n    }\n    else\n    {\n        m_TC->getLocation().x -= m_Speed * fps;\n    }\n```\n\n接下来，碰撞器被更新到新位置:\n\n```cpp\n    // Update the collider\n    m_RCC->setOrMoveCollider(m_TC->getLocation().x, \n        m_TC->getLocation().y, m_TC->getSize().x, m_TC \n      ->getSize().y);\n```\n\n这段代码跟踪这个入侵者最后一次开枪已经有多久了，然后测试玩家是在入侵者的左边还是右边一个世界单位(+或者–对于随机精度修改器，所以每个入侵者都有点不同):\n\n```cpp\n   m_TimeSinceLastShot += fps;\n\n    // Is the middle of the invader above the \n   // player +- 1 world units\n    if ((m_TC->getLocation().x + (m_TC->getSize().x / 2)) > \n        (m_PlayerTC->getLocation().x - m_AccuracyModifier) &&\n        (m_TC->getLocation().x + (m_TC->getSize().x / 2)) < \n        (m_PlayerTC->getLocation().x + \n        (m_PlayerTC->getSize().x + m_AccuracyModifier)))\n    {\n```\n\n在前面的`if`测试中，另一个测试确保入侵者从最后一枪开始已经等了足够长的时间。如果有，那就拍一张。播放声音，计算子弹的产卵位置，调用`BulletSpawner`实例的`spawnBullet`函数，并计算新的随机等待时间，然后可以拍摄另一个镜头:\n\n```cpp\n        // Has the invader waited long enough since the last shot\n        if (m_TimeSinceLastShot > m_TimeBetweenShots)\n        {\n            SoundEngine::playShoot();\n            Vector2f spawnLocation;\n            spawnLocation.x = m_TC->getLocation().x + \n                m_TC->getSize().x / 2;\n            spawnLocation.y = m_TC->getLocation().y + \n                m_TC->getSize().y;\n            m_BulletSpawner->spawnBullet(spawnLocation, false);\n            srand(m_RandSeed);\n            int mTimeBetweenShots = (((rand() % 10))+1) / \n                WorldState::WAVE_NUMBER;\n            m_TimeSinceLastShot = 0;            \n        }\n    }\n}\n```\n\n`BulletSpawner`类的细节将在下一章透露，但作为对未来的一瞥，它将是一个抽象类，有一个名为`spawnBullet`的函数，并将被`GameScreen`类继承。\n\n### 解释 dropDownAndReverse 函数\n\n在`dropDownAndReverse`功能中，方向反转，垂直位置增加入侵者的高度。此外，入侵者的速度相对于玩家清除了多少波以及还有多少入侵者有待消灭而言会有所增加。清除的波浪越多，剩下的入侵者越少，入侵者移动的速度就越快:\n\n```cpp\nvoid InvaderUpdateComponent::dropDownAndReverse()\n{\n    m_MovingRight = !m_MovingRight;\n    m_TC->getLocation().y += m_TC->getSize().y;\n    m_Speed += (WorldState::WAVE_NUMBER) + \n        (WorldState::NUM_INVADERS_AT_START \n      - WorldState::NUM_INVADERS) \n        * m_SpeedModifier;\n}\n```\n\n下一个函数很简单，但为了完整起见，包含了它。\n\n### 解释 isMovingRight 函数\n\n该代码只是提供了对当前行驶方向的访问:\n\n```cpp\nbool InvaderUpdateComponent::isMovingRight()\n{\n    return m_MovingRight;\n}\n```\n\n它将用于测试是在屏幕左侧(向左移动时)还是在屏幕右侧(向右移动时)寻找碰撞，并允许碰撞触发对`dropDownAndReverse`功能的调用。\n\n### 解释 initializeBulletSpawner 函数\n\n我已经提到了`BulletSpawner`类是抽象的，将由`GameScreen`类实现。当`GameScreen`类的`initialize`函数被调用时，这个`initializeBulletSpawner`函数将被每个入侵者调用。如您所见，第一个参数是指向`BulletSpawner`实例的指针。这赋予了每个`InvaderUpdateComponent`调用`spawnBullet`函数的能力:\n\n```cpp\nvoid InvaderUpdateComponent::initializeBulletSpawner(\n    BulletSpawner* bulletSpawner, int randSeed)\n{\n    m_BulletSpawner = bulletSpawner;\n    m_RandSeed = randSeed;\n    srand(m_RandSeed);\n    m_TimeBetweenShots = (rand() % 15 + m_RandSeed);\n    m_AccuracyModifier = (rand() % 2);\n    m_AccuracyModifier += 0 + static_cast <float> (\n        rand()) / (static_cast <float> (RAND_MAX / (10)));\n}\n```\n\n`initializeBulletSpawner`函数中的其余代码设置随机值，使每个入侵者的行为与其他入侵者略有不同。\n\n## 对 PlayerUpdateComponent 类进行编码\n\n在名为`PlayerUpdateComponent.h`的`Header Files/GameObjects`过滤器中创建新的头文件，并添加以下代码:\n\n```cpp\n#pragma once\n#include \"UpdateComponent.h\"\n#include \"TransformComponent.h\"\n#include \"GameObjectSharer.h\"\n#include \"RectColliderComponent.h\"\n#include \"GameObject.h\"\nclass PlayerUpdateComponent : public UpdateComponent\n{\nprivate:\n    string m_SpecificType = \"player\";\n    shared_ptr<TransformComponent> m_TC;\n    shared_ptr<RectColliderComponent> m_RCC;\n    float m_Speed = 50.0f;\n    float m_XExtent = 0;\n    float m_YExtent = 0;\n    bool m_IsHoldingLeft = false;\n    bool m_IsHoldingRight = false;\n    bool m_IsHoldingUp = false;\n    bool m_IsHoldingDown = false;\npublic:\n    void updateShipTravelWithController(float x, float y);\n    void moveLeft();\n    void moveRight();\n    void moveUp();\n    void moveDown();\n    void stopLeft();\n    void stopRight();\n    void stopUp();\n    void stopDown();\n    /****************************************************\n    *****************************************************\n    From Component interface base class\n    *****************************************************\n    *****************************************************/\n    string Component::getSpecificType() {\n        return m_SpecificType;\n    }\n    void Component::start(GameObjectSharer* gos, GameObject* self) {        \n        m_TC = static_pointer_cast<TransformComponent>(self->\n            getComponentByTypeAndSpecificType(\n                \"transform\", \"transform\"));\n        m_RCC = static_pointer_cast<RectColliderComponent>(self->\n            getComponentByTypeAndSpecificType(\n                \"collider\", \"rect\"));        \n    }\n    /****************************************************\n    *****************************************************\n    From UpdateComponent\n    *****************************************************\n    *****************************************************/\n    void update(float fps) override;\n};\n```\n\n在`PlayerUpdateComponent`类中，我们有跟踪玩家是否按下键盘键所需的所有布尔变量，以及可以切换这些布尔值的函数。我们以前没有见过像`m_XExtent`和`M_YExtent float`类型变量这样的东西，当我们在函数定义中看到它们的用法时，我们会解释它们。\n\n注意，就像`BulletUpdateComponent`和`InvaderUpdateComponent`类一样，我们共享了指向这个游戏对象的转换和碰撞器组件的指针。正如我们所料，这些共享指针是在`start`函数中初始化的。\n\n在名为`PlayerUpdate.cpp`的`Source Files/GameObjects`过滤器中创建新的源文件，并添加以下代码:\n\n```cpp\n#include \"PlayerUpdateComponent.h\"\n#include \"WorldState.h\"\nvoid PlayerUpdateComponent::update(float fps)\n{\n    if (sf::Joystick::isConnected(0))\n    {\n        m_TC->getLocation().x += ((m_Speed / 100) \n            * m_XExtent) * fps;\n        m_TC->getLocation().y += ((m_Speed / 100) \n            * m_YExtent) * fps;        \n    }\n    // Left and right\n    if (m_IsHoldingLeft)\n    {\n        m_TC->getLocation().x -= m_Speed * fps;\n    }\n    else if (m_IsHoldingRight)\n    {\n        m_TC->getLocation().x += m_Speed * fps;\n    }\n    // Up and down\n    if (m_IsHoldingUp)\n    {\n        m_TC->getLocation().y -= m_Speed * fps;\n    }\n    else if (m_IsHoldingDown)\n    {\n        m_TC->getLocation().y += m_Speed * fps;\n    }\n\n    // Update the collider\n    m_RCC->setOrMoveCollider(m_TC->getLocation().x, \n        m_TC->getLocation().y, m_TC->getSize().x, \n        m_TC->getSize().y);\n\n    // Make sure the ship doesn't go outside the allowed area\n    if (m_TC->getLocation().x >\n        WorldState::WORLD_WIDTH - m_TC->getSize().x) \n    {\n        m_TC->getLocation().x = \n            WorldState::WORLD_WIDTH - m_TC->getSize().x;\n    }\n    else if (m_TC->getLocation().x < 0)\n    {\n        m_TC->getLocation().x = 0;\n    }\n    if (m_TC->getLocation().y > \n        WorldState::WORLD_HEIGHT - m_TC->getSize().y)\n    {\n        m_TC->getLocation().y = \n            WorldState::WORLD_HEIGHT - m_TC->getSize().y;\n    }\n    else if (m_TC->getLocation().y < \n        WorldState::WORLD_HEIGHT / 2)\n    {\n        m_TC->getLocation().y = \n            WorldState::WORLD_HEIGHT / 2;\n    }\n}    \nvoid PlayerUpdateComponent::\n    updateShipTravelWithController(float x, float y)\n{\n    m_XExtent = x;\n    m_YExtent = y;\n}\nvoid PlayerUpdateComponent::moveLeft()\n{\n    m_IsHoldingLeft = true;\n    stopRight();\n}\nvoid PlayerUpdateComponent::moveRight()\n{\n    m_IsHoldingRight = true;\n    stopLeft();\n}\nvoid PlayerUpdateComponent::moveUp()\n{\n    m_IsHoldingUp = true;\n    stopDown();\n}\nvoid PlayerUpdateComponent::moveDown()\n{\n    m_IsHoldingDown = true;\n    stopUp();\n}\nvoid PlayerUpdateComponent::stopLeft()\n{\n    m_IsHoldingLeft = false;\n}\nvoid PlayerUpdateComponent::stopRight()\n{\n    m_IsHoldingRight = false;\n}\nvoid PlayerUpdateComponent::stopUp()\n{\n    m_IsHoldingUp = false;\n}\nvoid PlayerUpdateComponent::stopDown()\n{\n    m_IsHoldingDown = false;\n}\n```\n\n在更新功能的第一个`if`块中，条件是`sf::Joystick::isConnected(0)`。当玩家将游戏手柄插入 USB 端口时，这种情况会返回真。在`if`块内，变换组件的水平和垂直位置都被改变:\n\n```cpp\n…((m_Speed / 100) * m_YExtent) * fps;\n```\n\n前面的代码将目标速度除以 100，然后乘以`m_YExtent`。`The m_XExtent`和`m_YExtent`变量将在每一帧更新，以保存代表玩家在水平和垂直方向上移动游戏手柄拇指棒的程度的值。值的范围是从-100 到 100，因此前面的代码具有这样的效果:当指杆位于其全部范围中的任何一个时，以全速向任何方向移动变换组件，或者当指杆部分位于中心(完全不移动)和其全部范围之间时，以该速度的一部分移动变换组件。这意味着如果玩家选择使用游戏手柄而不是键盘，他们将更好地控制飞船的速度。\n\n我们将在 [*第 22 章*](22.html#_idTextAnchor445)*使用游戏对象和构建游戏*中看到更多关于游戏手柄操作的细节。\n\n`update`功能的其余部分响应布尔变量，代表玩家正在按下或已经释放的键盘按键。\n\n在游戏手柄和键盘操作后，碰撞器组件被移动到新的位置，一系列`if`块确保玩家船不能移动到屏幕之外或屏幕上的中途点上方。\n\n下一个功能是`updateShipTravelWithController`功能；当控制器被插入时，它将为每一帧更新拇指操纵杆移动或静止的程度。\n\n其余功能更新布尔值，指示是否使用键盘键来移动船只。请注意，更新组件不处理发射子弹。我们本可以从这里处理它，一些游戏可能有一个很好的理由这样做。在这个游戏中，处理从`GameInputHandler`类射出子弹稍微直接一点。我们将在 [*第 22 章*](22.html#_idTextAnchor445)*中看到的`GameInputHandler`类使用游戏对象并构建游戏*将调用所有让`PlayerUpdateComponent`类知道游戏手柄和键盘发生了什么的功能。我们在前一章的`GameInputHandler`课程中对键盘响应的基础进行了编码。\n\n现在，让我们编写`GameObject`类的代码，它将保存所有不同的组件实例。\n\n# 编码游戏对象类\n\n我将非常详细地介绍这个类中的代码，因为它是所有其他类如何工作的关键。但是，我认为您将受益于完整地查看代码并首先研究它。考虑到这一点，在名为`GameObject.h`的`Header Files/GameObjects`过滤器中创建新的头文件，并添加以下代码:\n\n```cpp\n#pragma once\n#include <SFML/Graphics.hpp>\n#include <vector>\n#include <string>\n#include \"Component.h\"\n#include \"GraphicsComponent.h\"\n#include \"GameObjectSharer.h\"\n#include \"UpdateComponent.h\"\nclass GameObject {\nprivate:\n    vector<shared_ptr<Component>> m_Components;\n    string m_Tag;\n    bool m_Active = false;\n    int m_NumberUpdateComponents = 0;\n    bool m_HasUpdateComponent = false;\n    int m_FirstUpdateComponentLocation = -1;\n    int m_GraphicsComponentLocation = -1;\n    bool m_HasGraphicsComponent = false;\n    int m_TransformComponentLocation = -1;\n    int m_NumberRectColliderComponents = 0;\n    int m_FirstRectColliderComponentLocation = -1;\n    bool m_HasCollider = false;\npublic:\n    void update(float fps);\n    void draw(RenderWindow& window);\n    void addComponent(shared_ptr<Component> component);\n    void setActive();\n    void setInactive();\n    bool isActive();\n    void setTag(String tag);\n    string getTag();\n    void start(GameObjectSharer* gos);\n    // Slow only use in init and start\n    shared_ptr<Component> getComponentByTypeAndSpecificType(\n        string type, string specificType);\n    FloatRect& getEncompassingRectCollider();\n    bool hasCollider();\n    bool hasUpdateComponent();\n    string getEncompassingRectColliderTag();\n    shared_ptr<GraphicsComponent> getGraphicsComponent();\n    shared_ptr<TransformComponent> getTransformComponent();\n    shared_ptr<UpdateComponent> getFirstUpdateComponent();\n};\n```\n\n在前面的代码中，一定要仔细检查变量、类型、函数名及其参数。\n\n在名为`GameObject.cpp`的`Source Files/GameObjects`过滤器中创建新的源文件，然后研究并添加以下代码:\n\n```cpp\n#include \"DevelopState.h\"\n#include \"GameObject.h\"\n#include <iostream> \n#include \"UpdateComponent.h\"\n#include \"RectColliderComponent.h\"\nvoid GameObject::update(float fps)\n{\n    if (m_Active && m_HasUpdateComponent)\n    {\n        for (int i = m_FirstUpdateComponentLocation; i < \n            m_FirstUpdateComponentLocation + \n            m_NumberUpdateComponents; i++) \n        {\n            shared_ptr<UpdateComponent> tempUpdate =\n                static_pointer_cast<UpdateComponent>(\n             m_Components[i]);\n            if (tempUpdate->enabled()) \n            {\n                tempUpdate->update(fps);\n            }\n        }\n    }\n}\nvoid GameObject::draw(RenderWindow& window)\n{\n    if (m_Active && m_HasGraphicsComponent)\n    {\n        if (m_Components[m_GraphicsComponentLocation]->enabled())\n        {\n            getGraphicsComponent()->draw(window, \n                getTransformComponent());\n        }\n    }\n}\nshared_ptr<GraphicsComponent> GameObject::getGraphicsComponent() \n{\n    return static_pointer_cast<GraphicsComponent>(\n        m_Components[m_GraphicsComponentLocation]);\n}\nshared_ptr<TransformComponent> GameObject::getTransformComponent() \n{\n    return static_pointer_cast<TransformComponent>(\n        m_Components[m_TransformComponentLocation]);\n}\nvoid GameObject::addComponent(shared_ptr<Component> component)\n{\n    m_Components.push_back(component);\n    component->enableComponent();\n\n   if (component->getType() == \"update\") \n    {\n        m_HasUpdateComponent = true;\n        m_NumberUpdateComponents++ ;\n        if (m_NumberUpdateComponents == 1) \n        {\n            m_FirstUpdateComponentLocation = \n                m_Components.size() - 1;\n        }\n    }\n    else if (component->getType() == \"graphics\") \n    {\n        // No iteration in the draw method required\n        m_HasGraphicsComponent = true;\n        m_GraphicsComponentLocation = m_Components.size() - 1;\n    }\n    else if (component->getType() == \"transform\") \n    {\n        // Remember where the Transform component is\n        m_TransformComponentLocation = m_Components.size() - 1;\n    }\n    else if (component->getType() == \"collider\" && \n        component->getSpecificType() == \"rect\") \n    {\n        // Remember where the collider component(s) is\n        m_HasCollider = true;\n        m_NumberRectColliderComponents++ ;\n        if (m_NumberRectColliderComponents == 1) \n        {\n            m_FirstRectColliderComponentLocation = \n                m_Components.size() - 1;\n        }\n    }    \n}\nvoid GameObject::setActive()\n{\n    m_Active = true;\n}\nvoid GameObject::setInactive()\n{\n    m_Active = false;\n}\nbool GameObject::isActive()\n{\n    return m_Active;\n}\nvoid GameObject::setTag(String tag)\n{\n    m_Tag = \"\" + tag;\n}\nstd::string GameObject::getTag()\n{\n    return m_Tag;\n}\nvoid GameObject::start(GameObjectSharer* gos) \n{\n    auto it = m_Components.begin();\n    auto end = m_Components.end();\n    for (it;\n        it != end;\n        ++ it)\n    {\n        (*it)->start(gos, this);\n    }\n}\n// Slow - only use in start function\nshared_ptr<Component> GameObject::\n   getComponentByTypeAndSpecificType(\n    string type, string specificType) {\n    auto it = m_Components.begin();\n    auto end = m_Components.end();\n    for (it;\n        it != end;\n        ++ it)\n    {\n        if ((*it)->getType() == type)\n        {\n            if ((*it)->getSpecificType() == specificType)\n            {\n                return  (*it);\n            }\n        }\n    }\n    #ifdef debuggingErrors        \n        cout << \n            \"GameObject.cpp::getComponentByTypeAndSpecificType-\" \n            << \"COMPONENT NOT FOUND ERROR!\" \n            << endl;\n    #endif\n        return m_Components[0];\n}\nFloatRect& GameObject::getEncompassingRectCollider() \n{\n    if (m_HasCollider) \n    {\n        return (static_pointer_cast<RectColliderComponent>(\n            m_Components[m_FirstRectColliderComponentLocation]))\n            ->getColliderRectF();\n    }\n}\nstring GameObject::getEncompassingRectColliderTag() \n{\n    return static_pointer_cast<RectColliderComponent>(\n        m_Components[m_FirstRectColliderComponentLocation])->\n        getColliderTag();\n}\nshared_ptr<UpdateComponent> GameObject::getFirstUpdateComponent()\n{\n    return static_pointer_cast<UpdateComponent>(\n        m_Components[m_FirstUpdateComponentLocation]);\n}\nbool GameObject::hasCollider() \n{\n    return m_HasCollider;\n}\nbool GameObject::hasUpdateComponent()\n{\n    return m_HasUpdateComponent;\n}\n```\n\n小费\n\n在继续之前，一定要学习前面的代码。下面的解释假设您对变量名和类型以及函数名、参数和返回类型有基本的了解。\n\n## 解释游戏对象类\n\n让我们一次浏览`GameObject`类的一个函数，并重新打印代码，以便于讨论。\n\n### 解释更新功能\n\n对于每个游戏对象，游戏循环的每一帧都调用一次`update`功能。像我们大多数其他项目一样，当前的帧速率是必需的。在`update`功能中，进行一个测试来查看这个`GameObject`实例是否是活动的并且有一个更新组件。一个游戏对象不一定要有更新组件，尽管这个项目中的所有游戏对象都有。\n\n接下来，`update`功能循环遍历它拥有的所有组件，从`m_FirstUpdateComponent`开始到`m_FirstUpdateComponent + m_NumberUpdateComponents`。这段代码意味着一个游戏对象可以有多个更新组件。这是为了让你可以设计具有行为层次的游戏对象。这种行为分层在 [*第 22 章*](22.html#_idTextAnchor445)*使用游戏对象和构建游戏*中有进一步的讨论。这个项目中所有的游戏对象都只有一个更新组件，所以你可以在`update`功能中简化(并加快)逻辑，但我建议你先看完 [*第 22 章*](22.html#_idTextAnchor445)*使用游戏对象并构建游戏*后再这样做。\n\n因为一个组件可能是许多类型中的一种，所以我们创建一个临时的更新相关组件(`tempUpdate`)，将组件从组件向量转换为`UpdateComponent`，并调用`update`函数。`UpdateComponent`类的具体推导没关系；它将实现`update`功能，因此`UpdateComponent`类型足够具体:\n\n```cpp\nvoid GameObject::update(float fps)\n{\n    if (m_Active && m_HasUpdateComponent)\n    {\n        for (int i = m_FirstUpdateComponentLocation; i < \n            m_FirstUpdateComponentLocation + \n            m_NumberUpdateComponents; i++) \n        {\n            shared_ptr<UpdateComponent> tempUpdate =\n                static_pointer_cast<UpdateComponent>(\n             m_Components[i]);\n            if (tempUpdate->enabled()) \n            {\n                tempUpdate->update(fps);\n            }\n        }\n    }\n}\n```\n\n当我们进入后面的`addComponent`函数时，我们将看到如何初始化各种控制变量，例如`m_FirstUpdateComponentLocation`和`m_NumberOfUpdateComponents`。\n\n### 解释绘图功能\n\n`draw`功能检查游戏对象是否活动，是否有图形组件。如果是，则检查图形组件是否已启用。如果所有这些测试成功，则调用`draw`功能:\n\n```cpp\nvoid GameObject::draw(RenderWindow& window)\n{\n    if (m_Active && m_HasGraphicsComponent)\n    {\n        if (m_Components[m_GraphicsComponentLocation]->enabled())\n        {\n            getGraphicsComponent()->draw(window, \n                getTransformComponent());\n        }\n    }\n}\n```\n\n`draw`功能的结构意味着不是每个游戏对象都要自己画。我在 [*第 19 章*](19.html#_idTextAnchor372)*游戏编程设计模式–启动太空入侵者++ 游戏*中提到，您可能希望永远看不到的游戏对象充当不可见的触发区域(没有图形组件)，当玩家经过它们或暂时保持不可见的游戏对象(暂时禁用，但有图形组件)时，它们会做出响应。在这个项目中，所有游戏对象都有一个永久启用的图形组件。\n\n### 解释 getGraphicsComponent 函数\n\n此函数返回指向图形组件的共享指针:\n\n```cpp\nshared_ptr<GraphicsComponent> GameObject::getGraphicsComponent() \n{\n    return static_pointer_cast<GraphicsComponent>(\n        m_Components[m_GraphicsComponentLocation]);\n}\n```\n\n`getGraphicsComponent`函数让任何拥有包含的游戏对象实例的代码都可以访问图形组件。\n\n### 解释 getTransformComponent 函数\n\n此函数返回一个指向转换组件的共享指针:\n\n```cpp\nshared_ptr<TransformComponent> GameObject::getTransformComponent() \n{\n    return static_pointer_cast<TransformComponent>(\n        m_Components[m_TransformComponentLocation]);\n}\n```\n\n`getTransformComponent`函数让任何拥有包含的游戏对象实例的代码都可以访问转换组件。\n\n### 解释添加组件功能\n\n我们将在下一章中编码的工厂模式类将使用`addComponent`函数。该函数接收指向`Component`实例的共享指针。函数内部发生的第一件事是将`Component`实例添加到`m_Components`向量中。接下来，使用`enabled`功能启用组件。\n\n接下来是一系列的`if`和`else if`语句，处理每种可能的组件类型。当组件的类型被识别时，各种控制变量被初始化，以使类的其余部分中的逻辑能够正确工作。\n\n例如，如果检测到更新组件，则初始化`m_HasUpdateComponent`、`m_NumberUpdateComponents`和`m_FirstUpdateComponentLocation`变量。\n\n作为另一个例子，如果检测到碰撞器组件以及`rect`特定类型，则`m_HasCollider`、`m_NumberRectColliderComponents`和`m_FirstRectColliderComponent`变量被初始化:\n\n```cpp\nvoid GameObject::addComponent(shared_ptr<Component> component)\n{\n    m_Components.push_back(component);\n    component->enableComponent();\n\n   if (component->getType() == \"update\") \n    {\n        m_HasUpdateComponent = true;\n        m_NumberUpdateComponents++ ;\n        if (m_NumberUpdateComponents == 1) \n        {\n            m_FirstUpdateComponentLocation = \n                m_Components.size() - 1;\n        }\n    }\n    else if (component->getType() == \"graphics\") \n    {\n        // No iteration in the draw method required\n        m_HasGraphicsComponent = true;\n        m_GraphicsComponentLocation = m_Components.size() - 1;\n    }\n    else if (component->getType() == \"transform\") \n    {\n        // Remember where the Transform component is\n        m_TransformComponentLocation = m_Components.size() - 1;\n    }\n    else if (component->getType() == \"collider\" && \n        component->getSpecificType() == \"rect\") \n    {\n        // Remember where the collider component(s) is\n        m_HasCollider = true;\n        m_NumberRectColliderComponents++ ;\n        if (m_NumberRectColliderComponents == 1) \n        {\n            m_FirstRectColliderComponentLocation = \n                m_Components.size() - 1;\n        }\n    }    \n}\n```\n\n请注意，`GameObject`类不参与实际组件本身的配置或设置。这些都是在工厂模式类中处理的，我们将在下一章进行编码。\n\n### 解释 getter 和 setter 函数\n\n下面的代码是一系列非常简单的获取器和设置器:\n\n```cpp\nvoid GameObject::setActive()\n{\n    m_Active = true;\n}\nvoid GameObject::setInactive()\n{\n    m_Active = false;\n}\nbool GameObject::isActive()\n{\n    return m_Active;\n}\nvoid GameObject::setTag(String tag)\n{\n    m_Tag = \"\" + tag;\n}\nstd::string GameObject::getTag()\n{\n    return m_Tag;\n}\n```\n\n前面的获取器和设置器提供了关于游戏对象的信息，比如它是否活动以及它的标签是什么。它们还允许您设置标签，并告诉我们游戏对象是否处于活动状态。\n\n### 解释启动功能\n\n`start`功能是一个重要的功能。正如我们在对所有组件进行编码时所看到的那样，`start`函数允许访问任何游戏对象中的任何组件以及任何其他游戏对象的组件。一旦所有的`GameObject`实例由它们的所有组件组成，就调用`start`函数。在下一章中，我们将看到这是如何发生的，以及何时在每个`GameObject`实例上调用`start`函数。如我们所见，在`start`函数中，它循环遍历每个组件并共享一个新的类实例，即`GameObjectSharer`实例。这个`GameObjectSharer`类将在下一章进行编码，并允许访问任何类中的任何组件。我们看到了入侵者如何需要知道玩家在哪里，以及当我们对各种组件进行编码时如何使用`GameObjectSharer`参数。当在每个组件上调用`start`时，`this`指针也被传入，以使每个组件易于访问其包含的`GameObject`实例:\n\n```cpp\nvoid GameObject::start(GameObjectSharer* gos) \n{\n    auto it = m_Components.begin();\n    auto end = m_Components.end();\n    for (it;\n        it != end;\n        ++ it)\n    {\n        (*it)->start(gos, this);\n    }\n}\n```\n\n让我们进入`getComponentByTypeAndSpecificType`功能。\n\n### 解释 getcomponentbyteyandspecifictype 函数\n\n`getComponentByTypeAndSpecificType`函数有一个嵌套的`for`循环，它寻找组件类型与第一个`string`参数的匹配，然后在第二个`string`参数中寻找特定组件类型的匹配。它返回一个指向基类`Component`实例的共享指针。这意味着调用代码需要确切地知道正在返回什么派生的`Component`类型，以便能够将其转换为所需的类型。这应该不是问题，因为，当然，他们同时请求类型和特定类型:\n\n```cpp\n// Slow only use in start\nshared_ptr<Component> GameObject::getComponentByTypeAndSpecificType(\n    string type, string specificType) {\n    auto it = m_Components.begin();\n    auto end = m_Components.end();\n    for (it;\n        it != end;\n        ++ it)\n    {\n        if ((*it)->getType() == type)\n        {\n            if ((*it)->getSpecificType() == specificType)\n            {\n                return  (*it);\n            }\n        }\n    }\n    #ifdef debuggingErrors        \n        cout << \n            \"GameObject.cpp::getComponentByTypeAndSpecificType-\" \n            << \"COMPONENT NOT FOUND ERROR!\" \n            << endl;\n    #endif\n        return m_Components[0];\n}\n```\n\n这个函数中的代码非常慢，因此打算在主游戏循环之外使用。在该功能结束时，如果已经定义了`debuggingErrors`，代码将向控制台写入一条错误消息。这样做的原因是因为，如果执行达到这一点，就意味着没有找到匹配的组件，游戏就会崩溃。控制台的输出应该使错误易于发现。崩溃的原因可能是为无效类型或特定类型调用了该函数。\n\n### 解释 getEncompassingRectCollider 函数\n\n`getEncompassingRectCollider`函数检查游戏对象是否有碰撞器，如果有，则返回调用代码:\n\n```cpp\nFloatRect& GameObject::getEncompassingRectCollider() \n{\n    if (m_HasCollider) \n    {\n        return (static_pointer_cast<RectColliderComponent>(\n            m_Components[m_FirstRectColliderComponentLocation]))\n            ->getColliderRectF();\n    }\n}\n```\n\n值得注意的是，如果你扩展这个项目来处理多种类型的碰撞器，那么这段代码也需要修改。\n\n### 解释 getEncompassingRectColliderTag 函数\n\n这个简单的函数返回碰撞器的标签。这将有助于确定测试碰撞的对象类型:\n\n```cpp\nstring GameObject::getEncompassingRectColliderTag() \n{\n    return static_pointer_cast<RectColliderComponent>(\n        m_Components[m_FirstRectColliderComponentLocation])->\n        getColliderTag();\n}\n```\n\n我们还有几个函数要讨论。\n\n### 解释 getFirstUpdateComponent 函数\n\n`getFirstUpdateComponent`使用`m_FirstUpdateComponent`变量定位更新组件，然后将其返回给调用代码:\n\n```cpp\nshared_ptr<UpdateComponent> GameObject::getFirstUpdateComponent()\n{\n    return static_pointer_cast<UpdateComponent>(\n        m_Components[m_FirstUpdateComponentLocation]);\n}\n```\n\n现在我们只需要看几个吸气剂，然后我们就完成了。\n\n### 解释最终的 getter 函数\n\n这两个剩余的函数返回一个布尔值(每个)来告诉调用代码游戏对象是否有碰撞器和/或更新组件:\n\n```cpp\nbool GameObject::hasCollider() \n{\n    return m_HasCollider;\n}\nbool GameObject::hasUpdateComponent()\n{\n    return m_HasUpdateComponent;\n}\n```\n\n我们已经对`GameObject`类进行了完整的编码。我们现在可以考虑让它(以及它将包含的所有组件)工作。\n\n# 总结\n\n在本章中，我们已经完成了将我们的游戏对象绘制到屏幕上、控制它们的行为并让它们通过碰撞与其他类交互的所有代码。本章要讲的最重要的事情不是任何特定的基于组件的类如何工作，而是实体-组件系统有多灵活。如果你想要一个有特定行为方式的游戏对象，创建一个新的更新组件。如果它需要了解游戏中的其他对象，可以在`start`功能中获取一个指向相应组件的指针。如果它需要以一种奇特的方式绘制，也许用一个着色器或者一个动画，编码一个图形组件来执行`draw`函数中的动作。如果你需要多个对撞机，就像我们在托马斯迟到项目中为托马斯和鲍勃做的那样，这是没有问题的:编写一个新的基于对撞机的组件。\n\n在下一章中，我们将对文件输入和输出系统以及类进行编码，该类将是构建所有游戏对象并用组件组成它们的工厂。"
  },
  {
    "path": "docs/begin-cpp-game-prog/21.md",
    "content": "# 二十一、文件输入输出和游戏对象工厂\n\n本章讲述了一个`GameObject`如何进入游戏中使用的`m_GameObjects vector`。我们将研究如何在文本文件中描述单个对象和整个级别。我们将编写代码来解释文本，然后将值加载到一个类中，该类将是一个游戏对象的蓝图。我们还将编写一个名为`LevelManager`的类来监督整个过程，从最初请求加载从`InputHandler`通过`ScreenManager`发送的关卡，一直到工厂模式类，工厂模式类从组件组装游戏对象并将其发送到`LevelManager`，整齐地打包在`m_GameObjects vector`中。\n\n以下是我们将在本章中经历的步骤:\n\n*   检查我们将如何在文本文件中描述游戏对象及其组件\n*   对`GameObjectBlueprint`类进行编码，文本文件中的数据将临时存储在该类中\n*   对`ObjectTags`类进行编码，以帮助一致且无错误地描述游戏对象\n*   代码`BluePrintObjectParser`，负责将文本文件中游戏对象描述的数据加载到`GameObjectBlueprint`实例中\n*   代码`PlayModeObjectLoader`，打开文本文件，从`BlueprintObjectParser`一次接收一个`GameObjectBlueprint`实例\n*   对`GameObjectFactoryPlayMode`类进行编码，该类将从`GameObjectBlueprint`实例构造`GameObject`实例\n*   对`LevelManager`类进行编码，该类在收到`ScreenManager`类的指令后监督整个过程\n*   将代码添加到`ScreenManager`类中，这样我们就可以开始使用我们将在本章中编码的新系统\n\n让我们从检查我们如何准确地描述一个游戏对象开始，比如一个空间入侵者或文本文件中的一颗子弹，更不用说一整波了。\n\n# 文件 I/O 和工厂类的结构\n\n请看下图，该图概述了我们将在本章中编码的类，以及`GameObject`实例的`vector`将如何与我们在 [*第 19 章*](19.html#_idTextAnchor372) 、*游戏编程设计模式中编码的`ScreenManager`类共享——启动空间入侵者++ 游戏*:\n\n![](img/Image93956.jpg)\n\n上图显示了四个类之间共享的`GameObject`实例的`vector`。这是通过引用在类的函数之间传递`vector`来实现的。然后，每个类都可以使用`vector`及其内容来执行其角色。当一个新的等级需要加载到`vector`中时，`ScreenManager`等级将触发`LevelManager`等级。单个`Screen`类及其`InputHandler`衍生类，正如我们在 [*第 19 章*](19.html#_idTextAnchor372)*游戏编程设计模式–启动太空入侵者++ 游戏*中看到的，可以通过`ScreenManagerRemoteControl`访问`ScreenManager`。\n\n`LevelManager`类最终负责创建和共享向量。`PlayModeObjectLoader`将使用`BlueprintObjectParser`创建`GameObjectBlueprint`实例。\n\n当`PlayModeObjectLoader`提示时，`GameObjectFactoryPlayMode`类将使用这些`GameObjectBlueprint`实例完成`GameObject`创建过程并将`GameObject`实例打包到`vector`中。\n\n那么，每个`GameObject`实例的不同组件、位置、尺寸和外观配置从何而来？\n\n我们还可以看到三个类可以访问一个`GameObjectBlueprint`实例。这个实例由`LevelManager`类创建，并通过引用传递。`BlueprintObjectParser`将读取`level1.txt`文件，其中包含每个游戏对象的所有细节。它将初始化`GameObjectBlueprint`类的所有变量。`PlayModeObjectLoader`随后将传递对`GameObject`实例的`vector`的引用，并将对完全配置的`GameObjectBlueprint`实例的引用传递给`GameObjectFactoryPlayMode`类。如此重复，直到所有`GameObject`实例都打包到`vector`中。\n\n你可能想知道为什么我使用了稍微麻烦的类名，比如`GameObjectFactoryPlayMode`和`PlayModeObjectLoader`。原因是，一旦您看到这个系统有多方便，您可能会喜欢构建工具，允许您通过在需要的地方拖放来以可视化的方式设计级别，然后让文本文件自动生成而不是键入。这并不特别复杂，但我不得不在某个时候停止向游戏中添加功能。因此，你很可能会得到一个`GameObjectFactoryDesignMode`和一个`DesignModeObjectLoader`。\n\n# 描述世界上的一个物体\n\n我们已经在 [*第 19 章*](19.html#_idTextAnchor372)*游戏编程设计模式-启动太空入侵者++ 游戏*的`world`文件夹中添加了`level1.txt`文件。让我们讨论它的用途，未来的预期用途，以及它的内容。\n\n首先，我想指出，射手游戏并不是演示如何在这样的文本文件中描述游戏世界的最佳方式。之所以会这样，是因为游戏对象只有很少几种，最常见的一种，入侵者，都像阅兵的士兵一样整齐划一地排着队。它们实际上会被更有效地编程描述，也许是在嵌套的`for`循环中。然而，这个项目的目的是展示想法，而不是学习如何制作太空入侵者克隆体。\n\n请看下面的文字，这是来自`world`文件夹中`level1.txt`文件的样本:\n\n```cpp\n[START OBJECT]\n[NAME]invader[-NAME]\n[COMPONENT]Standard Graphics[-COMPONENT]\n[COMPONENT]Invader Update[-COMPONENT]\n[COMPONENT]Transform[-COMPONENT]\n[LOCATION X]0[-LOCATION X]\n[LOCATION Y]0[-LOCATION Y]\n[WIDTH]2[-WIDTH]\n[HEIGHT]2[-HEIGHT]\n[BITMAP NAME]invader1[-BITMAP NAME]\n[ENCOMPASSING RECT COLLIDER]invader[-ENCOMPASSING_RECT COLLIDER]\n[END OBJECT]\n```\n\n前面的文字描述了游戏中的单个对象；在这种情况下，入侵者。该对象以下列文本开头:\n\n```cpp\n[START OBJECT]\n```\n\n这将通知我们将要编写的代码，一个新的对象正在被描述。在文本中，我们可以看到以下内容:\n\n```cpp\n[NAME]invader[-NAME]\n```\n\n这通知代码对象的类型是入侵者。这最终将被设置为`ColliderComponent`类的`m_Tag`。入侵者会被识别出来。接下来的文字如下:\n\n```cpp\n[COMPONENT]Standard Graphics[-COMPONENT]\n[COMPONENT]Invader Update[-COMPONENT]\n[COMPONENT]Transform[-COMPONENT]\n```\n\n这告诉我们的系统，这个对象将添加三个组件:一个`StandardGraphicsComponent`实例、一个`InvaderUpdateComponent`实例和一个`TransformComponent`实例。这意味着物体将以标准的方式绘制，并按照我们为入侵者编写的规则运行。这也将意味着它在游戏世界中有位置和规模。有可能对象没有任何组件或组件较少。一个不采取动作也不移动的对象将不需要更新组件，一个不可见的对象将不需要图形组件(也许只是一个触发某些动作的不可见碰撞器)，一个在世界上没有位置的对象(也许是一个调试对象)将不需要变换组件。\n\n对象的位置和比例由以下四行文本决定:\n\n```cpp\n[LOCATION X]0[-LOCATION X]\n[LOCATION Y]0[-LOCATION Y]\n[WIDTH]2[-WIDTH]\n[HEIGHT]2[-HEIGHT]\n```\n\n下面一行文本决定了什么图形文件将用于此对象的纹理:\n\n```cpp\n[BITMAP NAME]invader1[-BITMAP NAME]\n```\n\n下面一行表示物体可以碰撞。一个装饰性的物体，也许是浮云(或蜜蜂)，不需要对撞机:\n\n```cpp\n[ENCOMPASSING RECT COLLIDER]invader[-ENCOMPASSING_RECT COLLIDER]\n```\n\n文本的最后一行将通知我们的系统对象已经完成了对自身的描述:\n\n```cpp\n[END OBJECT]\n```\n\n现在，让我们来看看如何描述子弹物体:\n\n```cpp\n[START OBJECT]\n[NAME]bullet[-NAME]\n[COMPONENT]Standard Graphics[-COMPONENT]\n[COMPONENT]Transform[-COMPONENT]\n[COMPONENT]Bullet Update[-COMPONENT]\n[LOCATION X]-1[-LOCATION X]\n[LOCATION Y]-1[-LOCATION Y]\n[WIDTH]0.1[-WIDTH]\n[HEIGHT]2.0[-HEIGHT]\n[BITMAP NAME]bullet[-BITMAP NAME]\n[ENCOMPASSING RECT COLLIDER]bullet[-ENCOMPASSING_RECT COLLIDER]\n[SPEED]75.0[-SPEED]\n[END OBJECT]\n```\n\n这与入侵者非常相似，但又不相同。项目符号对象有附加数据，如设定速度。入侵者的速度在`InvaderUpdateComponent`类的逻辑中设定。我们也可以为了子弹的速度而这样做，但这表明你可以根据具体游戏设计的要求来描述物体的细节。此外，正如我们所料，子弹有一个`BulletUpdateComponent`和一个不同的`[BITMAP NAME]`元素值。请注意，项目符号的位置设置为-1，-1。这意味着游戏开始时子弹在可玩区域之外。在下一章中，我们将会看到一个入侵者，或者玩家，如何在需要的时候将他们变成行动。\n\n现在，研究以下描述玩家船的文本:\n\n```cpp\n[START OBJECT]\n[NAME]Player[-NAME]\n[COMPONENT]Standard Graphics[-COMPONENT]\n[COMPONENT]Transform[-COMPONENT]\n[COMPONENT]Player Update[-COMPONENT]\n[LOCATION X]50[-LOCATION X]\n[LOCATION Y]40[-LOCATION Y]\n[WIDTH]3.0[-WIDTH]\n[HEIGHT]2.0[-HEIGHT]\n[BITMAP NAME]playership[-BITMAP NAME]\n[ENCOMPASSING RECT COLLIDER]player[-ENCOMPASSING_RECT COLLIDER]\n[SPEED]10.0[-SPEED]\n[END OBJECT]\n```\n\n根据我们迄今为止的讨论，前面的案文可能是完全可以预见的。现在我们已经完成了这一步，我们可以开始对解释这些对象描述的系统进行编码，并将它们转换成可用的`GameObject`实例。\n\n# 编写游戏对象蓝图类\n\n在名为`GameObjectBlueprint.h`的`Header Files/FileIO`过滤器中创建新的头文件，并添加以下代码:\n\n```cpp\n#pragma once\n#include<vector>\n#include<string>\n#include<map>\nusing namespace std;\nclass GameObjectBlueprint {\nprivate:\n    string m_Name = \"\";\n    vector<string> m_ComponentList;\n    string m_BitmapName = \"\";\n    float m_Width;\n    float m_Height;\n    float m_LocationX;\n    float m_LocationY;\n    float m_Speed;\n    bool m_EncompassingRectCollider = false;\n    string m_EncompassingRectColliderLabel = \"\";    \npublic:\n    float getWidth();\n    void setWidth(float width);\n    float getHeight();\n    void setHeight(float height);\n    float getLocationX();\n    void setLocationX(float locationX);\n    float getLocationY();\n    void setLocationY(float locationY);\n    void setName(string name);\n    string getName();\n    vector<string>& getComponentList();\n    void addToComponentList(string newComponent);\n    string getBitmapName();\n    void setBitmapName(string bitmapName);    \n    string getEncompassingRectColliderLabel();\n    bool getEncompassingRectCollider();\n    void setEncompassingRectCollider(string label);\n};\n```\n\n`GameObjectBlueprint`对于每个可能进入游戏对象的属性都有一个成员变量。请注意，它没有按组件划分属性。例如，它只有宽度、高度和位置等变量；它不会麻烦地将这些识别为转换组件的一部分。这些细节在工厂里处理。它还提供了获取器和设置器，以便`BlueprintObjectParser`类可以打包掉`level1.txt`文件中的所有值，`GameObjectFactoryPlayMode`类可以提取所有值，实例化适当的组件，并将它们添加到`GameObject`的实例中。\n\n在名为`GameObjectBlueprint.cpp`的`Source Files/FileIO`过滤器中创建一个新的源文件，并添加以下代码，用于我们刚刚声明的函数的定义:\n\n```cpp\n#include \"GameObjectBlueprint.h\"\nfloat GameObjectBlueprint::getWidth() \n{\n    return m_Width;\n}\nvoid GameObjectBlueprint::setWidth(float width) \n{\n    m_Width = width;\n}\nfloat GameObjectBlueprint::getHeight() \n{\n    return m_Height;\n}\nvoid GameObjectBlueprint::setHeight(float height) \n{\n    m_Height = height;\n}\nfloat GameObjectBlueprint::getLocationX() \n{\n    return m_LocationX;\n}\nvoid GameObjectBlueprint::setLocationX(float locationX) \n{\n    m_LocationX = locationX;\n}\nfloat GameObjectBlueprint::getLocationY() \n{\n    return m_LocationY;\n}\nvoid GameObjectBlueprint::setLocationY(float locationY) \n{\n    m_LocationY = locationY;\n}\nvoid GameObjectBlueprint::setName(string name)\n{\n    m_Name = \"\" + name;\n}\nstring GameObjectBlueprint::getName()\n{\n    return m_Name;\n}\nvector<string>& GameObjectBlueprint::getComponentList()\n{\n    return m_ComponentList;\n}\nvoid GameObjectBlueprint::addToComponentList(string newComponent)\n{\n    m_ComponentList.push_back(newComponent);\n}\nstring GameObjectBlueprint::getBitmapName()\n{\n    return m_BitmapName;\n}\nvoid GameObjectBlueprint::setBitmapName(string bitmapName)\n{\n    m_BitmapName = \"\" + bitmapName;\n}\nstring GameObjectBlueprint::getEncompassingRectColliderLabel() \n{\n    return m_EncompassingRectColliderLabel;\n}\nbool GameObjectBlueprint::getEncompassingRectCollider() \n{\n    return m_EncompassingRectCollider;\n}\nvoid GameObjectBlueprint::setEncompassingRectCollider(\n    string label) \n{\n    m_EncompassingRectCollider = true;\n    m_EncompassingRectColliderLabel = \"\" + label;\n}\n```\n\n虽然这是一堂很长的课，但这里没有我们以前没有见过的东西。setter 函数接收复制到向量或变量中的值，而 getter 函数允许访问这些值。\n\n# 对对象标签类进行编码\n\n我们在`level1.txt`文件中描述游戏对象的方式需要精确，因为我们将在这个类之后编码的`BlueprintObjectParser`类将从文件中读取文本并寻找匹配。例如，`[START OBJECT]`标签将触发新对象的开始。如果那个标签拼错了，比如说`[START OBJECR]`，那么整个系统就会分崩离析，会出现各种各样的 bug，甚至在我们运行游戏的时候崩溃。为了避免这种情况发生，我们将为描述游戏对象所需的所有标签定义常量(以编程方式不可更改)`string`变量。我们可以使用这些`string`变量，而不是输入像`[START OBJECT]`这样的东西，出错的机会就少得多。\n\n在名为`ObjectTags.h`的`Header Files/FileIO`过滤器中创建新的头文件，并添加以下代码:\n\n```cpp\n#pragma once\n#include <string>\nusing namespace std;\nstatic class ObjectTags {\npublic:\n    static const string START_OF_OBJECT;\n    static const string END_OF_OBJECT;\n    static const string COMPONENT;\n    static const string COMPONENT_END;\n    static const string NAME;\n    static const string NAME_END;\n    static const string WIDTH;\n    static const string WIDTH_END;\n    static const string HEIGHT;\n    static const string HEIGHT_END;\n    static const string LOCATION_X;\n    static const string LOCATION_X_END;\n    static const string LOCATION_Y;\n    static const string LOCATION_Y_END;\n    static const string BITMAP_NAME;\n    static const string BITMAP_NAME_END;\n    static const string ENCOMPASSING_RECT_COLLIDER;\n    static const string ENCOMPASSING_RECT_COLLIDER_END;\n};\n```\n\n我们已经为每个用来描述游戏对象的标签声明了一个`const string`。现在，我们可以初始化它们。\n\n在名为`ObjectTags.cpp`的`Source Files/FileIO`过滤器中创建新的源文件，并添加以下代码:\n\n```cpp\n#include \"DevelopState.h\"\n#include \"objectTags.h\"\nconst string ObjectTags::START_OF_OBJECT = \"[START OBJECT]\";\nconst string ObjectTags::END_OF_OBJECT = \"[END OBJECT]\";\nconst string ObjectTags::COMPONENT = \"[COMPONENT]\";\nconst string ObjectTags::COMPONENT_END = \"[-COMPONENT]\";\nconst string ObjectTags::NAME = \"[NAME]\";\nconst string ObjectTags::NAME_END = \"[-NAME]\";\nconst string ObjectTags::WIDTH = \"[WIDTH]\";\nconst string ObjectTags::WIDTH_END = \"[-WIDTH]\";\nconst string ObjectTags::HEIGHT = \"[HEIGHT]\";\nconst string ObjectTags::HEIGHT_END = \"[-HEIGHT]\";\nconst string ObjectTags::LOCATION_X = \"[LOCATION X]\";\nconst string ObjectTags::LOCATION_X_END = \"[-LOCATION X]\";\nconst string ObjectTags::LOCATION_Y = \"[LOCATION Y]\";\nconst string ObjectTags::LOCATION_Y_END = \"[-LOCATION Y]\";\nconst string ObjectTags::BITMAP_NAME = \"[BITMAP NAME]\";\nconst string ObjectTags::BITMAP_NAME_END = \"[-BITMAP NAME]\";\nconst string ObjectTags::ENCOMPASSING_RECT_COLLIDER = \n    \"[ENCOMPASSING RECT COLLIDER]\";\n\nconst string ObjectTags::ENCOMPASSING_RECT_COLLIDER_END \n    = \"[-ENCOMPASSING_RECT COLLIDER]\";\n```\n\n以上就是初始化的所有`string`变量。我们现在可以在下一节课中使用它们，并确保我们一致地描述游戏对象。\n\n# 对 BlueprintObjectParser 类进行编码\n\n这个类将拥有从我们已经讨论过的`level1.txt`文件中实际读取文本的代码。它将一次解析一个对象，正如我们之前看到的开始和结束标签所标识的那样。\n\n在名为`BlueprintObjectParser.h`的`Header Files/FileIO`过滤器中创建新的头文件，并添加以下代码:\n\n```cpp\n#pragma once\n#include \"GameObjectBlueprint.h\"\n#include <string>\nusing namespace std;\nclass BlueprintObjectParser {\nprivate:\n    string extractStringBetweenTags(\n        string stringToSearch, string startTag, string endTag);\npublic:\n    void parseNextObjectForBlueprint(\n        ifstream& reader, GameObjectBlueprint& bp);\n};\n```\n\n`extractStringBetweenTags`私有函数将捕获两个标签之间的内容。参数是三个`string`实例。第一个`string`是从`level1.txt`开始的一整行文字，第二个和第三个是开始和结束标签，需要丢弃。然后，两个标记之间的文本返回给调用代码。\n\n`parseNextObjectForBlueprint`功能接收一个`ifstream`阅读器，就像我们在僵尸射手和托马斯迟到游戏中使用的那个一样。它用于读取文件。第二个参数是对`GameObjectBlueprint`实例的引用。该函数将使用从`level1.txt`文件中读取的值填充`GameObjectBlueprint`实例，然后可以在调用代码中使用这些值来创建实际的`GameObject`。当我们接下来对`PlayModeObjectLoader`类和之后的`GameObjectFactoryPlayMode`类进行编码时，我们将看到这是如何发生的。\n\n让我们对刚才讨论的定义进行编码。\n\n在名为`BlueprintObjectParser.cpp`的`Source Files/FileIO`过滤器中创建新的源文件，并添加以下代码:\n\n```cpp\n#include \"BlueprintObjectParser.h\"\n#include \"ObjectTags.h\"\n#include <iostream>\n#include <fstream>\nvoid BlueprintObjectParser::parseNextObjectForBlueprint(\n    ifstream& reader, GameObjectBlueprint& bp)\n{\n    string lineFromFile;\n    string value = \"\";\n    while (getline(reader, lineFromFile)) \n    {\n        if (lineFromFile.find(ObjectTags::COMPONENT) \n            != string::npos) \n          {\n            value = extractStringBetweenTags(lineFromFile, \n                ObjectTags::COMPONENT, \n                ObjectTags::COMPONENT_END);\n            bp.addToComponentList(value);\n        }\n        else if (lineFromFile.find(ObjectTags::NAME) \n            != string::npos) \n          {\n            value = extractStringBetweenTags(lineFromFile, \n                ObjectTags::NAME, ObjectTags::NAME_END);\n            bp.setName(value);\n        }\n        else if (lineFromFile.find(ObjectTags::WIDTH) \n            != string::npos) \n          {\n            value = extractStringBetweenTags(lineFromFile, \n                ObjectTags::WIDTH, ObjectTags::WIDTH_END);\n            bp.setWidth(stof(value));\n        }\n        else if (lineFromFile.find(ObjectTags::HEIGHT) \n            != string::npos) \n          {\n            value = extractStringBetweenTags(lineFromFile, \n                ObjectTags::HEIGHT, ObjectTags::HEIGHT_END);\n            bp.setHeight(stof(value));\n        }\n        else if (lineFromFile.find(ObjectTags::LOCATION_X) \n            != string::npos) \n          {\n            value = extractStringBetweenTags(lineFromFile, \n                ObjectTags::LOCATION_X, \n                ObjectTags::LOCATION_X_END);\n            bp.setLocationX(stof(value));\n        }\n        else if (lineFromFile.find(ObjectTags::LOCATION_Y) \n            != string::npos) \n          {\n            value = extractStringBetweenTags(\n                      lineFromFile, \n                      ObjectTags::LOCATION_Y, \n                      ObjectTags::LOCATION_Y_END);\n            bp.setLocationY(stof(value));\n        }\n        else if (lineFromFile.find(ObjectTags::BITMAP_NAME) \n            != string::npos) \n          {\n            value = extractStringBetweenTags(lineFromFile, \n             ObjectTags::BITMAP_NAME, \n             ObjectTags::BITMAP_NAME_END);\n            bp.setBitmapName(value);\n        }\n\n        else if (lineFromFile.find(\n            ObjectTags::ENCOMPASSING_RECT_COLLIDER) \n            != string::npos) \n          {\n            value = extractStringBetweenTags(lineFromFile, \n                ObjectTags::ENCOMPASSING_RECT_COLLIDER, \n                ObjectTags::ENCOMPASSING_RECT_COLLIDER_END);\n            bp.setEncompassingRectCollider(value);\n        }\n\n        else if (lineFromFile.find(ObjectTags::END_OF_OBJECT) \n            != string::npos) \n        {\n            return;\n        }\n    }\n}\nstring BlueprintObjectParser::extractStringBetweenTags(\n    string stringToSearch, string startTag, string endTag)\n{\n    int start = startTag.length();\n    int count = stringToSearch.length() - startTag.length() \n        - endTag.length();\n    string stringBetweenTags = stringToSearch.substr(\n        start, count);\n    return stringBetweenTags;\n}\n```\n\n`parseNextObjectForBlueprint`中的代码很长，但很简单。一系列`if`语句识别文本行开头的起始标记，然后将文本行传递给`extractStringBetweenTags`函数，该函数返回值，然后将该值加载到适当位置的`GameObjectBlueprint`引用中。请注意，当`GameObjectBlueprint`已经将所有数据加载到函数中时，函数退出。发现`ObjectTags::END_OF_OBJECT`时，识别出这一点。\n\n# 对 PlayModeObjectLoader 类进行编码\n\n这是将`GameObjectBlueprint`实例传递给`BlueprintObjectParser`的类。当它得到完整的蓝图时，它将把它们传递给`GameObjectFactoryPlayMode`类，后者将构建`GameObject`实例并将其打包到`vector`实例中。一旦所有的`GameObject`实例被建立和存储，责任将被交给`LevelManager`类，它将控制游戏引擎其他部分对向量的访问。这是一个非常小的类，只有一个函数，但它将许多其他类链接在一起。请参考本章开头的图表进行说明。\n\n在名为`PlayModeObjectLoader.h`的`Header Files/FileIO`过滤器中创建新的头文件，并添加以下代码:\n\n```cpp\n#pragma once\n#include <vector>\n#include <string>\n#include \"GameObject.h\"\n#include \"BlueprintObjectParser.h\"\n#include \"GameObjectFactoryPlayMode.h\"\nusing namespace std;\nclass PlayModeObjectLoader {\nprivate:\n    BlueprintObjectParser m_BOP;\n    GameObjectFactoryPlayMode m_GameObjectFactoryPlayMode;\npublic:\n    void loadGameObjectsForPlayMode(\n        string pathToFile, vector<GameObject>& mGameObjects);\n};\n```\n\n`PlayModeObjectLoader`类有一个我们编码的前一个类的实例，也就是`BluePrintObjectParser`类。它还有一个我们接下来要编码的类的实例，即`GameObjectFactoryPlayMode`类。它有一个单一的公共功能，接收对保存`GameObject`实例的`vector`的引用。\n\n现在，我们将对`loadGameObjectsForPlayMode`函数的定义进行编码。在名为`PlayModeObjectLoader.cpp`的`Source Files/FileIO`过滤器中创建新的源文件，并添加以下代码:\n\n```cpp\n#include \"PlayModeObjectLoader.h\"\n#include \"ObjectTags.h\"\n#include <iostream>\n#include <fstream>\nvoid PlayModeObjectLoader::\n    loadGameObjectsForPlayMode(\n        string pathToFile, vector<GameObject>& gameObjects)\n{\n    ifstream reader(pathToFile);\n    string lineFromFile;\n    float x = 0, y = 0, width = 0, height = 0;\n    string value = \"\";\n    while (getline(reader, lineFromFile)) {\n        if (lineFromFile.find(\n            ObjectTags::START_OF_OBJECT) != string::npos) {\n            GameObjectBlueprint bp;\n            m_BOP.parseNextObjectForBlueprint(reader, bp);\n            m_GameObjectFactoryPlayMode.buildGameObject(\n                bp, gameObjects);\n        }\n    }       \n}\n```\n\n该函数接收一个`string`，它是需要加载的文件的路径。这个游戏只有一个这样的文件，但是你可以添加更多不同布局的文件，不同数量的入侵者，或者完全不同的游戏对象，如果你想的话。\n\n一个`ifstream`实例用于从文件中一次读取一行。在`while`循环中，使用`ObjectTags::START_OF_OBJECT`识别起始标签，调用`BlueprintObjectParser`的`parseNextObjectForBlueprint`功能。您可能还记得`BlueprintObjectParser`课，当到达`ObjectTags::END_OF_OBJECT`时，已完成的蓝图被返回。\n\n下一行代码调用`GameObjectFactoryPlayMode`类的`buildGameObject`，并传入`GameObjectBlueprint`实例。我们现在将对`GameObjectFactory`类进行编码。\n\n# 编码游戏对象要素类型 layMode 类\n\n现在，我们将对我们的工厂进行编码，该工厂将从`GameObject`类和我们在上一章中编码的所有组件相关类中构造工作游戏对象。我们将广泛使用智能指针，所以当我们完成它时，我们不必担心删除内存。\n\n在名为`GameObjectFactoryPlayMode.h`的`Header Files/FileIO`过滤器中创建新的头文件，并添加以下代码:\n\n```cpp\n#pragma once\n#include \"GameObjectBlueprint.h\"\n#include \"GameObject.h\"\n#include <vector>\nclass GameObjectFactoryPlayMode {\npublic:\n    void buildGameObject(GameObjectBlueprint& bp, \n        std::vector <GameObject>& gameObjects);\n};\n```\n\n工厂类只有一个功能，`buildGameObject`。我们已经在之前为`PlayModeObjectLoader`类编写的代码中看到了调用该函数的代码。该函数接收对蓝图的引用，以及对`GameObject`实例的`vector`的引用。\n\n在名为`GameObjectFactoryPlayMode.cpp`的`Source Files/FileIO`过滤器中创建新的源文件，并添加以下代码:\n\n```cpp\n#include \"GameObjectFactoryPlayMode.h\"\n#include <iostream>\n#include \"TransformComponent.h\"\n#include \"StandardGraphicsComponent.h\"\n#include \"PlayerUpdateComponent.h\"\n#include \"RectColliderComponent.h\"\n#include \"InvaderUpdateComponent.h\"\n#include \"BulletUpdateComponent.h\"\nvoid GameObjectFactoryPlayMode::buildGameObject(\n    GameObjectBlueprint& bp, \n    std::vector<GameObject>& gameObjects)\n{\n    GameObject gameObject;\n    gameObject.setTag(bp.getName());\n    auto it = bp.getComponentList().begin();\n    auto end = bp.getComponentList().end();\n    for (it;\n        it != end;\n        ++ it)\n    {\n        if (*it == \"Transform\")\n        {\n            gameObject.addComponent(\n                make_shared<TransformComponent>(\n                bp.getWidth(),\n                bp.getHeight(),\n                Vector2f(bp.getLocationX(),\n                 bp.getLocationY())));\n        }\n        else if (*it == \"Player Update\")\n        {\n            gameObject.addComponent(make_shared\n                <PlayerUpdateComponent>());\n        }\n        else if (*it == \"Invader Update\")\n        {\n            gameObject.addComponent(make_shared\n                <InvaderUpdateComponent>());\n        }\n        else if (*it == \"Bullet Update\")\n        {\n            gameObject.addComponent(make_shared\n                <BulletUpdateComponent>());\n        }\n        else if (*it == \"Standard Graphics\")\n        {\n            shared_ptr<StandardGraphicsComponent> sgp =\n                make_shared<StandardGraphicsComponent>();\n            gameObject.addComponent(sgp);\n            sgp->initializeGraphics(\n                bp.getBitmapName(),\n                Vector2f(bp.getWidth(), \n                    bp.getHeight()));\n        }        \n    }\n    if (bp.getEncompassingRectCollider()) {\n        shared_ptr<RectColliderComponent> rcc = \n            make_shared<RectColliderComponent>(\n            bp.getEncompassingRectColliderLabel());\n        gameObject.addComponent(rcc);\n        rcc->setOrMoveCollider(bp.getLocationX(),\n            bp.getLocationY(),\n            bp.getWidth(),\n            bp.getHeight());\n    }   \n\n    gameObjects.push_back(gameObject);\n}\n```\n\n在`buildGameObject`函数中发生的第一件事是创建一个新的`GameObject`实例，并使用`GameObject`类的`setTag`函数传入正在构建的当前对象的名称:\n\n```cpp\nGameObject gameObject;\ngameObject.setTag(bp.getName());\n```\n\n接下来，`for`循环通过`m_Components vector`中的所有组件。对于找到的每个组件，一个不同的`if` 语句创建一个适当类型的组件。正如您所料，每个组件的创建方式各不相同，因为它们的编码方式也各不相同。\n\n下面的代码创建了一个指向`TransformComponent`实例的共享指针。您可以看到传递给构造函数的必要参数，即宽度、高度和位置。创建指向`TransformComponent`实例的新共享指针的结果被传递给`GameObject`类的`addComponent`函数。`GameObject`实例现在在世界上有其规模和地位:\n\n```cpp\nif (*it == \"Transform\")\n{\n    gameObject.addComponent(make_shared<TransformComponent>(\n        bp.getWidth(),\n        bp.getHeight(),\n        Vector2f(bp.getLocationX(), bp.getLocationY())));\n}\n```\n\n当需要`PlayerUpdateComponent`时，执行以下代码。同样，代码创建一个指向适当类的新共享指针，并将其传递给`GameObject`实例的`addComponent`函数:\n\n```cpp\nelse if (*it == \"Player Update\")\n{\n    gameObject.addComponent(make_shared\n        <PlayerUpdateComponent>());\n}\n```\n\n以下三个代码块使用完全相同的技术来添加`InvaderUpdateComponent`、`BulletUpdateComponent`或`StandardGraphicsComponent`实例。请注意添加调用`initialize`函数的`StandardGraphicsComponent`实例后的额外代码行，该实例将`Texture`实例(如果需要)添加到`BitmapStore`单例中，并准备要绘制的组件:\n\n```cpp\nelse if (*it == \"Invader Update\")\n{\n    gameObject.addComponent(make_shared\n        <InvaderUpdateComponent>());\n}\nelse if (*it == \"Bullet Update\")\n{\n    gameObject.addComponent(make_shared\n        <BulletUpdateComponent>());\n}\nelse if (*it == \"Standard Graphics\")\n{\n    shared_ptr<StandardGraphicsComponent> sgp =\n        make_shared<StandardGraphicsComponent>();\n    gameObject.addComponent(sgp);\n    sgp->initializeGraphics(\n        bp.getBitmapName(),\n        Vector2f(bp.getWidth(), \n            bp.getHeight()));\n}\n```\n\n最后的`if` 块，如下面的代码所示，处理添加`RectColliderComponent`实例。第一行代码创建共享指针，而第二行代码调用`addComponent`函数将实例添加到`GameObject`实例。第三行代码调用`setOrMoveCollider`并传递对象的位置和大小。在这个阶段，物体已经准备好被碰撞。显然，我们仍然需要编写测试冲突的代码。我们将在下一章中这样做:\n\n```cpp\nif (bp.getEncompassingRectCollider()) {\n        shared_ptr<RectColliderComponent> rcc = \n            make_shared<RectColliderComponent>(\n            bp.getEncompassingRectColliderLabel());\n        gameObject.addComponent(rcc);\n        rcc->setOrMoveCollider(bp.getLocationX(),\n            bp.getLocationY(),\n            bp.getWidth(),\n            bp.getHeight());\n}\n```\n\n该类中的下面一行代码将刚刚构建的`GameObject`实例添加到`vector`中，该实例将与`GameScreen`类共享，并用于使游戏变得生动起来:\n\n```cpp\ngameObjects.push_back(gameObject);\n```\n\n我们将编写的下一个类使得共享我们刚刚填充的围绕项目各个类的`vector`实例变得容易。\n\n# 编写游戏对象共享类的代码\n\n这个类将有两个与其他类共享`GameObject`实例的纯虚函数。\n\n在名为`GameObjectSharer.h`的`Header Files/FileIO`过滤器中创建新的头文件，并添加以下代码:\n\n```cpp\n#pragma once\n#include<vector>\n#include<string>\nclass GameObject;\nclass GameObjectSharer {\npublic:\n    virtual std::vector<GameObject>& getGameObjectsWithGOS() = 0;\n    virtual GameObject& findFirstObjectWithTag(\n             std::string tag) = 0;\n};\n```\n\n`getGameObjectsWithGOS`函数返回对整个`GameObject`实例向量的引用。`findFirstObjectWithTag`函数只返回一个`GameObject`引用。当我们接下来对`LevelManager`类进行编码时，我们将看到如何从`GameObjectSharer`继承这些函数。\n\n简而言之，在`LevelManager`类之前，在名为`GameObjectSharer.cpp`的`Source Files/FileIO`过滤器中创建新的源文件，并添加以下代码:\n\n```cpp\n/*********************************\n******THIS IS AN INTERFACE********\n*********************************/\n```\n\n同样，这只是一个占位符文件，所有功能都在继承自`GameObjectSharer`的任何类中；在这种情况下，`LevelManager`类。\n\n# 对级别管理器类进行编码\n\n`LevelManager`类是我们在 [*第 19 章*](19.html#_idTextAnchor372)*游戏编程设计模式–启动太空入侵者++ 游戏*中编码的内容与我们在本章中编码的所有内容之间的联系。`ScreenManager`类将拥有一个`LevelManager`类的实例，`LevelManager`类将发起加载级别(使用我们刚刚编码的所有类)并与任何需要它们的类共享`GameObject`实例。\n\n在名为`LevelManager.h`的`Header Files/Engine`过滤器中创建新的头文件，并添加以下代码:\n\n```cpp\n#pragma once\n#include \"GameObject.h\"\n#include <vector>\n#include <string>\n#include \"GameObjectSharer.h\"\nusing namespace std;\nclass LevelManager : public GameObjectSharer {\nprivate:\n    vector<GameObject> m_GameObjects;\n    const std::string WORLD_FOLDER = \"world\";\n    const std::string SLASH = \"/\";\n    void runStartPhase();\n    void activateAllGameObjects();\npublic:\n    vector<GameObject>& getGameObjects();\n    void loadGameObjectsForPlayMode(string screenToLoad);\n    /****************************************************\n    *****************************************************\n    From GameObjectSharer interface\n    *****************************************************\n    *****************************************************/\n    vector<GameObject>& GameObjectSharer::getGameObjectsWithGOS()\n    {\n        return m_GameObjects;\n    }\n    GameObject& GameObjectSharer::findFirstObjectWithTag(\n         string tag)\n    {\n        auto it = m_GameObjects.begin();\n        auto end = m_GameObjects.end();\n        for (it;\n            it != end;\n            ++ it)\n        {\n            if ((*it).getTag() == tag)\n            {\n                return (*it);\n            }\n        }\n\n#ifdef debuggingErrors        \n    cout << \n        \"LevelManager.h findFirstGameObjectWithTag() \" \n        << \"- TAG NOT FOUND ERROR!\" \n        << endl;\n#endif    \n        return m_GameObjects[0];\n    }\n};\n```\n\n这个类提供了两种不同的方法来使`vector`充满游戏对象。一种方法是通过对`getGameObjects`的简单调用，但另一种方法是通过`getGameObjectsWithGOS`功能。后者是来自`GameObjectSharer`类的纯虚拟函数的实现，并且将是一种传递对每个游戏对象的访问的方式，从而可以访问所有其他游戏对象。您可能还记得 [*第 20 章*](20.html#_idTextAnchor414)*游戏对象和组件*中，在`GameObject`类的`start`函数调用期间传入了一个`GameObjectSharer`实例。在这个功能中，入侵者可以访问玩家的位置。\n\n还有两个私有函数:`runStartPhase`，它循环遍历所有调用 start 的`GameObject`实例，`activateAllGameObjects`，它循环遍历所有`GameObject`实例并将其设置为活动状态。\n\n此外，`LevelManager`类的一部分是`loadGameObjectsForPlayMode`函数，它将触发本章其余部分描述的整个游戏对象创建过程。\n\n`LevelManger.h`文件中的最后一个函数是另一个`GameObjectSharer`纯虚函数`findFirstObjectWithTag`的实现。这允许任何具有`GameObjectSharer`实例的类使用其标签来追踪特定的游戏对象。该代码遍历`vector`中的所有`GameObject`实例，并返回第一个匹配项。请注意，如果没有找到匹配，将返回一个空指针并使游戏崩溃。我们使用`#ifdef`语句向控制台输出一些文本，告诉我们是什么导致了崩溃，这样，如果我们不小心搜索到一个不存在的标签，我们就不会在几个小时内摸不着头脑。\n\n我们现在可以对函数的实现进行编码。\n\n在名为`LevelManager.cpp`的`Source Files/Engine`过滤器中创建新的源文件，并添加以下代码:\n\n```cpp\n#include \"LevelManager.h\"\n#include \"PlayModeObjectLoader.h\"\n#include <iostream>\nvoid LevelManager::\n    loadGameObjectsForPlayMode(string screenToLoad)\n{\n    m_GameObjects.clear();\n    string levelToLoad = \"\" \n        + WORLD_FOLDER + SLASH + screenToLoad;\n    PlayModeObjectLoader pmol;\n    pmol.loadGameObjectsForPlayMode(\n        levelToLoad, m_GameObjects);\n    runStartPhase();\n}\nvector<GameObject>& LevelManager::getGameObjects()\n{\n    return m_GameObjects;\n}\nvoid LevelManager::runStartPhase()\n{\n    auto it = m_GameObjects.begin();\n    auto end = m_GameObjects.end();\n    for (it;\n        it != end;\n        ++ it)\n    {\n        (*it).start(this);\n    }\n    activateAllGameObjects();\n}\nvoid LevelManager::activateAllGameObjects()\n{\n    auto it = m_GameObjects.begin();\n    auto end = m_GameObjects.end();\n    for (it;\n        it != end;\n        ++ it)\n    {\n        (*it).setActive();\n    }\n}\n```\n\n`loadLevelForPlayMode`函数清除`vector`，实例化一个完成所有文件读取的`PlayModeObjectLoader`实例，并将`GameObject`实例打包到`vector`中。最后调用`runStartPhase`函数。在`runStartPhase`功能中，所有的`GameObject`实例都被传递一个`GameObjectSharer` ( `this`)并有机会进行自我设置，准备播放。请记住，在`start`函数的`GameObject`类中，每个派生的`Component`实例都可以访问`GameObjectSharer`。参考 [*第二十章*](20.html#_idTextAnchor414)*游戏对象和组件*，看看我们在对`Component`类进行编码时是怎么处理的。\n\n`runStartPhase`函数以调用`activateAllGameObjects`结束，它循环通过`vector`，在每个`GameObject`实例上调用`setActive`。\n\n`getGameObjects`函数传递对`GameObject`实例的`vector`的引用。\n\n现在我们已经编码了`LevelManager`类，我们可以更新它实现的`ScreenManager`和`ScreenManagerRemoteControl`类。\n\n# 更新屏幕管理器和屏幕管理器远程控制类\n\n打开`ScreenManagerRemoteControl.h`文件，取消注释所有内容，使代码如下所示。我强调了未注释的行:\n\n```cpp\n#pragma once\n#include <string>\n#include <vector>\n#include \"GameObject.h\"\n#include \"GameObjectSharer.h\"\nusing namespace std;\nclass ScreenManagerRemoteControl\n{\npublic:\n    virtual void SwitchScreens(string screenToSwitchTo) = 0;\n    virtual void loadLevelInPlayMode(string screenToLoad) = 0;\n virtual vector<GameObject>& getGameObjects() = 0;\n virtual GameObjectSharer& shareGameObjectSharer() = 0;\n};\n```\n\n接下来打开`ScreenManager.h`，实现这个接口，取消注释掉所有注释掉的代码。所述代码被缩写并突出显示如下:\n\n```cpp\n...\n#include \"SelectScreen.h\"\n//#include \"LevelManager.h\"\n#include \"BitmapStore.h\"\n...\n...\nprivate:\n    map <string, unique_ptr<Screen>> m_Screens;\n //LevelManager m_LevelManager;\nprotected:\n    ...\n    ...\n/****************************************************\n*****************************************************\nFrom ScreenManagerRemoteControl interface\n*****************************************************\n*****************************************************/\n    ...\n    ...\n //vector<GameObject>& \n //ScreenManagerRemoteControl::getGameObjects()\n //{\n //return m_LevelManager.getGameObjects();\n //}\n //GameObjectSharer& shareGameObjectSharer()\n //{\n //return m_LevelManager;\n //}\n    ...\n    ...\n```\n\n请务必取消对 include 指令、`m_LevelManager`实例以及两个函数的注释。\n\n`ScreenManager`和`ScreenManagerRemoteControl`类现在功能齐全，`getGameObjects`和`shareGameObjectSharer`功能可以被任何引用了`ScreenManager`类的类使用。\n\n# 我们现在在哪里？\n\n此时，我们的`GameObject`类中的所有错误，以及所有组件相关的类都消失了。我们正在取得良好的进展。\n\n此外，我们可以重新访问`ScreenManager.h`文件并取消注释所有注释掉的代码。\n\n打开`ScreenManager.h`并取消注释`#include`指令，如下所示:\n\n```cpp\n//#include \"LevelManager.h\"\n```\n\n将其更改为:\n\n```cpp\n#include \"LevelManager.h\"\n```\n\n对于在`ScreenManager.h`中实现的`ScreenManagerRemoteControl`界面的功能也是如此。它们看起来如下:\n\n```cpp\nvoid ScreenManagerRemoteControl::\n        loadLevelInPlayMode(string screenToLoad)\n    {\n        //m_LevelManager.getGameObjects().clear();\n        //m_LevelManager.\n            //loadGameObjectsForPlayMode(screenToLoad);\n        SwitchScreens(\"Game\");\n    }\n//vector<GameObject>& \n    //ScreenManagerRemoteControl::getGameObjects()\n//{\n    //return m_LevelManager.getGameObjects();\n//}\n```\n\n按如下方式进行更改:\n\n```cpp\nvoid ScreenManagerRemoteControl::\n    loadLevelInPlayMode(string screenToLoad)\n{\n    m_LevelManager.getGameObjects().clear();\n    m_LevelManager.\n        loadGameObjectsForPlayMode(screenToLoad);\n    SwitchScreens(\"Game\");\n}\nvector<GameObject>& \n    ScreenManagerRemoteControl::getGameObjects()\n{\n    return m_LevelManager.getGameObjects();\n}\n```\n\n然而，我们还没有完全准备好运行游戏，因为代码中仍然有一些缺失的类被使用，比如`InvaderUpdateComponent`类中的`BulletSpawner`。\n\n# 总结\n\n在这一章中，我们已经建立了一种描述游戏中某个关卡的方法，以及一个解释描述并构建可用`GameObject`实例的系统。工厂模式用于许多类型的编程，不仅仅是游戏开发。我们使用的实现是最简单的实现，我鼓励您将工厂模式放在您的模式列表中，以便进一步研究和开发。然而，如果您希望构建一些深度且有趣的游戏，我们使用的实现应该可以很好地为您服务。\n\n在下一章中，我们将通过添加碰撞检测、子弹产卵和游戏本身的逻辑，最终使游戏变得栩栩如生。"
  },
  {
    "path": "docs/begin-cpp-game-prog/22.md",
    "content": "# 二十二、使用游戏对象和构建游戏\n\n本章是太空入侵者++ 项目的最后阶段。我们将学习如何使用 SFML 从游戏手柄接收输入，并编写一个类来处理入侵者和`GameScreen`类以及玩家和`GameScreen`类之间的通信。该类将允许玩家和入侵者生成子弹，但完全相同的技术可以用于您自己游戏不同部分之间所需的任何类型的通信，因此了解这一点很有用。游戏的最后部分(像往常一样)将是碰撞检测和游戏本身的逻辑。一旦空间入侵者++ 启动并运行，我们将学习如何使用 Visual Studio 调试器，这在您设计自己的逻辑时将是无价的，因为它允许您一次一行地遍历代码，并查看变量的值。它也是一个有用的工具，用于研究我们在这个项目过程中组装的模式的执行流程。\n\n以下是我们在本章中要做的事情:\n\n*   为生成项目符号编写解决方案\n*   处理玩家的输入，包括用游戏手柄\n*   检测所有必要对象之间的冲突\n*   编写游戏的主要逻辑\n*   了解调试并了解执行流程\n\n让我们从产生子弹开始。\n\n# 产卵子弹\n\n我们需要一种从玩家和每个入侵者身上产生子弹的方法。两者的解决方案非常相似，但并不完全相同。我们需要一种方法，当按下键盘按键或游戏手柄按钮时，允许`GameInputHandler`产生子弹，我们需要`InvaderUpdateComponent`使用它已经存在的逻辑来产生子弹。\n\n`GameScreen`类有一个保存所有`GameObject`实例的`vector`，因此`GameScreen`是将子弹移动到位并设置其在屏幕上上下移动的理想候选，这取决于谁或什么触发了射击。我们需要一种方式让`GameInputHandler`类和`InvaderUpdateComponenet`类与`GameScreen`类进行沟通，但我们也需要将沟通限制在只是产卵子弹；我们不希望他们能够控制`GameScreen`类的任何其他部分。\n\n让我们编写一个`GameScreen`可以继承的抽象类。\n\n## 对 BulletSpawner 类进行编码\n\n在名为`BulletSpawner.h`的`Header Files/GameObjects`过滤器中创建新的头文件，并添加以下代码:\n\n```cpp\n#include <SFML/Graphics.hpp>\nclass BulletSpawner\n{\npublic:\n    virtual void spawnBullet(\n        sf::Vector2f spawnLocation, bool forPlayer) = 0;\n};\n```\n\n前面的代码创建了一个名为`BulletSpawner`的新类，它有一个名为`spawnBullet`的纯虚函数。`spawnBullet`功能有两个参数。第一个是`Vector2f`实例，它将确定产卵位置。事实上，我们很快就会看到，当子弹产生时，这个位置会稍微调整，这取决于子弹是在屏幕上(作为玩家子弹)还是在屏幕下(作为入侵者子弹)。第二个参数是一个布尔值，如果子弹属于玩家，则为真；如果子弹属于入侵者，则为假。\n\n在名为`BulletSpawner.cpp`的`Source Files/GameObjects`过滤器中创建新的源文件，并添加以下代码:\n\n```cpp\n/*********************************\n******THIS IS AN INTERFACE********\n*********************************/\n```\n\n小费\n\n和往常一样，这个`.cpp`文件是可选的。我只是想平衡一下源头。\n\n现在，转到`GameScreen.h`，因为这是我们要实现这个类的功能的地方。\n\n## 更新游戏画面\n\n首先，更新 include 指令和类声明，如下面的代码所示，使`GameScreen`类继承自`BulletSpawner`:\n\n```cpp\n#pragma once\n#include \"Screen.h\"\n#include \"GameInputHandler.h\"\n#include \"GameOverInputHandler.h\"\n#include \"BulletSpawner.h\"\nclass GameScreen : public Screen, public BulletSpawner\n{\n   …\n   …\n```\n\n接下来，向`GameScreen.h`添加一些额外的函数和变量声明，如下面的代码所示:\n\n```cpp\nprivate:\n    ScreenManagerRemoteControl* m_ScreenManagerRemoteControl;\n    shared_ptr<GameInputHandler> m_GIH;\n int m_NumberInvadersInWorldFile = 0;\n vector<int> m_BulletObjectLocations;\n int m_NextBullet = 0;\n bool m_WaitingToSpawnBulletForPlayer = false;\n bool m_WaitingToSpawnBulletForInvader = false;\n Vector2f m_PlayerBulletSpawnLocation;\n Vector2f m_InvaderBulletSpawnLocation;\n Clock m_BulletClock;\n    Texture m_BackgroundTexture;\n    Sprite m_BackgroundSprite;\npublic:\n    static bool m_GameOver;\n    GameScreen(ScreenManagerRemoteControl* smrc, Vector2i res);\n    void initialise() override;\n    void virtual update(float fps);\n    void virtual draw(RenderWindow& window);\n BulletSpawner* getBulletSpawner();\n```\n\n新的变量包括`int`值的`vector`，该值将保存`vector`中所有项目符号的位置，该位置保存所有游戏对象。它还有一些控制变量，这样我们就可以跟踪下一个要使用的子弹，子弹是给玩家的还是给入侵者的，以及产生子弹的位置。我们还声明了一个新的`sf::Clock`实例，因为我们想限制玩家的射速。最后，我们有`getBulletSpawner`函数，它将以`BulletSpawner`的形式返回一个指向这个类的指针。这将给予接收者对`spawnBullet`功能的访问权，但除此之外别无他法。\n\n现在，我们可以添加`spawnBullet`功能的实现。在所有其他代码的末尾，但在`GameScreen`类的右花括号内，向`GameScreen.h`添加以下代码:\n\n```cpp\n/****************************************************\n*****************************************************\nFrom BulletSpawner interface\n*****************************************************\n*****************************************************/\nvoid BulletSpawner::spawnBullet(Vector2f spawnLocation, \n    bool forPlayer)\n{\n    if (forPlayer)\n    {\n        Time elapsedTime = m_BulletClock.getElapsedTime();\n        if (elapsedTime.asMilliseconds() > 500) {\n            m_PlayerBulletSpawnLocation.x = spawnLocation.x;\n            m_PlayerBulletSpawnLocation.y = spawnLocation.y;\n            m_WaitingToSpawnBulletForPlayer = true;\n            m_BulletClock.restart();\n        }\n    }\n    else\n    {\n        m_InvaderBulletSpawnLocation.x = spawnLocation.x;\n        m_InvaderBulletSpawnLocation.y = spawnLocation.y;\n        m_WaitingToSpawnBulletForInvader = true;\n    }\n}\n```\n\n`spawnBullet`功能的实现是一个简单的`if`–`else`结构。如果玩家需要子弹，则执行`if`模块，如果入侵者需要子弹，则执行`else`模块。\n\n`if`块检查自请求最后一个项目符号以来至少已经过去了半秒钟，如果已经过去了，则将`m_WaitingToSpawnBulletForPlayer`变量设置为真，复制项目符号所在的位置，并重新启动时钟，准备测试玩家的下一个请求。\n\n`else`区块记录入侵者子弹的产卵位置并将`m_WaitingToSpawnBulletForInvader`设定为`true`。不需要与`Clock`实例交互，因为入侵者的射速是在`InvaderUpdateComponent`级控制的。\n\n`BulletSpawner`谜题的最后一部分，在我们真正产生子弹之前，是在`GameScreen.cpp`的结尾加上`getBulletSpawner`的定义。下面是要添加的代码:\n\n```cpp\nBulletSpawner* GameScreen::getBulletSpawner()\n{\n    return this;\n}\n```\n\n这将返回一个指向`GameScreen`的指针，这使我们可以访问`spawnBullet`功能。\n\n# 处理玩家的输入\n\n向`GameInputHandler.h`文件中添加更多的声明，以便您的代码与下面的内容相匹配。我强调了要添加的新代码:\n\n```cpp\n#pragma once\n#include \"InputHandler.h\"\n#include \"PlayerUpdateComponent.h\"\n#include \"TransformComponent.h\"\nclass GameScreen;\nclass GameInputHandler : public InputHandler\n{\nprivate:\n shared_ptr<PlayerUpdateComponent> m_PUC;\n shared_ptr<TransformComponent> m_PTC;\n bool mBButtonPressed = false;\npublic:\n    void initialize();\n    void handleGamepad() override;\n    void handleKeyPressed(Event& event, \n        RenderWindow& window) override;\n    void handleKeyReleased(Event& event, \n        RenderWindow& window) override;    \n};\n```\n\n`GameInputHandler`类现在可以访问玩家的更新组件和玩家的转换组件。这非常有用，因为这意味着我们可以告诉`PlayerUpdateComponent`实例和玩家的`TransformComponent`实例玩家正在操作什么键盘按键和游戏手柄。我们还没有看到这两个共享指针是如何初始化的——毕竟`GameObject`实例和它们的所有组件不是打包在`vector`中吗？你大概能猜到解决办法和`GameObjectSharer`有关。让我们继续编码，了解更多信息。\n\n在`GameInputHanldler.cpp`文件中，在 include 指令之后但在 initialize 函数之前添加一个`BulletSpawner`类的正向声明，如以下代码所示:\n\n```cpp\n#include \"GameInputHandler.h\"\n#include \"SoundEngine.h\"\n#include \"GameScreen.h\"\nclass BulletSpawner;\nvoid GameInputHandler::initialize() {\n…\n```\n\n在`GameInputHandler.cpp`文件中，将以下高亮显示的代码添加到`handleKeyPressed`功能中:\n\n```cpp\nvoid GameInputHandler::handleKeyPressed(\n    Event& event, RenderWindow& window)\n{\n    // Handle key presses\n    if (event.key.code == Keyboard::Escape)\n    {\n        SoundEngine::playClick();\n        getPointerToScreenManagerRemoteControl()->\n            SwitchScreens(\"Select\");\n    }\n\nif (event.key.code == Keyboard::Left)\n {\n m_PUC->moveLeft();\n }\n if (event.key.code == Keyboard::Right)\n {\n m_PUC->moveRight();\n }\n if (event.key.code == Keyboard::Up)\n {\n m_PUC->moveUp();\n }\n if (event.key.code == Keyboard::Down)\n {\n m_PUC->moveDown();\n }\n}\n```\n\n请注意，我们正在响应键盘按压，就像我们在本书中一直在做的那样。然而，在这里，我们从我们在 [*第 20 章*](20.html#_idTextAnchor414)*游戏对象和组件*中编码的`PlayerUpdateComponent`类调用函数，以便采取所需的操作。\n\n在`GameInputHandler.cpp`文件中，将以下高亮显示的代码添加到`handleKeyReleased`功能中:\n\n```cpp\nvoid GameInputHandler::handleKeyReleased(\n    Event& event, RenderWindow& window)\n{\n if (event.key.code == Keyboard::Left)\n {\n m_PUC->stopLeft();\n }\n else if (event.key.code == Keyboard::Right)\n {\n m_PUC->stopRight();\n }\n else if (event.key.code == Keyboard::Up)\n {\n m_PUC->stopUp();\n }\n else if (event.key.code == Keyboard::Down)\n {\n m_PUC->stopDown();\n }\n else if (event.key.code == Keyboard::Space)\n {\n // Shoot a bullet\n SoundEngine::playShoot();\n Vector2f spawnLocation;\n spawnLocation.x = m_PTC->getLocation().x + \n m_PTC->getSize().x / 2;\n spawnLocation.y = m_PTC->getLocation().y;\n static_cast<GameScreen*>(getmParentScreen())->\n spawnBullet(spawnLocation, true);\n }\n}\n```\n\n前面的代码也依赖于从`PlayerUpdateComponent`类调用函数来处理当玩家释放一个键盘键时发生的事情。`PlayerUpdateComponent`类然后可以停止在适当的方向上移动，这取决于哪个键盘键刚刚被释放。当释放*空间*键时，`getParentScreen`功能与`spawnBullet`功能连锁，触发子弹产生。请注意，产卵坐标(`spawnLocation`)是使用指向`PlayerTransformComponent`实例的共享指针计算的。\n\n让我们了解一下 SFML 如何帮助我们与游戏手柄进行交互，然后我们可以返回`PlayerInputHandler`类来添加更多的功能。\n\n## 使用游戏手柄\n\nSFML 让处理游戏手柄输入变得异常容易。游戏手柄(或操纵杆)输入由`sf::Joystick`类处理。SFML 可以处理多达八个游戏手柄的输入，但本教程将只坚持一个。\n\n你可以把拇指操纵杆的位置想象成一个 2D 图，从左上角的-100，-100 开始，到右下角的 100，100。因此，拇指操纵杆的位置可以用 2D 坐标来表示。下图用几个坐标示例说明了这一点:\n\n![](img/B14278_22_01.jpg)\n\n我们所需要做的就是抓取该值，并为游戏循环的每一帧将其报告给`PlayerUpdateComponent`类。捕捉位置就像下面两行代码一样简单:\n\n```cpp\nfloat x  = Joystick::getAxisPosition(0, sf::Joystick::X);\nfloat y = Joystick::getAxisPosition(0, sf::Joystick::Y);\n```\n\n零参数从主游戏手柄请求数据。您可以使用值 0 到 7 从八个游戏手柄获得输入。\n\n我们还需要考虑其他一些事情。大多数游戏垫，尤其是拇指棒，在机械上是不完美的，即使没有被触摸也会记录很小的值。如果我们将这些值发送到`PlayerUpdateComponent`类，那么飞船将在屏幕上漫无目的地漂移。为此，我们将创建一个**死区**。这是一个我们将忽略任何价值的运动范围。10%的运动范围效果相当好。因此，如果从`getAxisPosition`函数中检索到的值在任一轴上介于-10 和 10 之间，我们将忽略它们。\n\n要从游戏手柄的 B 按钮获取输入，我们使用以下代码行:\n\n//玩家是否按了 B 键？\n\n```cpp\nif (Joystick::isButtonPressed(0, 1))\n{\n    // Take action here\n}\n```\n\n前面的代码检测 Xbox One 游戏手柄上的 B 按钮何时被按下。其他控制器会有所不同。0，1 参数指的是主游戏手柄和 1 号按钮。为了检测按钮何时被释放，我们需要编写一些自己的逻辑代码。因为我们想在释放时而不是按下时发射子弹，所以我们将使用一个简单的布尔值来跟踪它。让我们对`GameInputHandler`类的其余部分进行编码，看看我们如何将刚刚学到的知识付诸行动。\n\n在`GameInputHandler.cpp`文件中，将以下高亮显示的代码添加到`handleGamepad`功能中:\n\n```cpp\nvoid GameInputHandler::handleGamepad()\n{\n float deadZone = 10.0f;\n float x  = Joystick::getAxisPosition(0, sf::Joystick::X);\n float y = Joystick::getAxisPosition(0, sf::Joystick::Y); \n\n if (x < deadZone && x > -deadZone)\n {\n x = 0;\n }\n if (y < deadZone && y > -deadZone)\n {\n y = 0;\n }\n m_PUC->updateShipTravelWithController(x, y); \n // Has the player pressed the B button?\n if (Joystick::isButtonPressed(0, 1))\n {\n mBButtonPressed = true;\n }\n // Has player just released the B button?\n if (!Joystick::isButtonPressed(0, 1) && mBButtonPressed)\n {\n mBButtonPressed = false;\n // Shoot a bullet\n SoundEngine::playShoot();\n Vector2f spawnLocation;\n spawnLocation.x = m_PTC->getLocation().x + \n m_PTC->getSize().x / 2;\n spawnLocation.y = m_PTC->getLocation().y;\n\n static_cast<GameScreen*>(getmParentScreen())->\n getBulletSpawner()->spawnBullet(\n spawnLocation, true);\n }\n}\n```\n\n我们首先定义一个 10 的死区，然后开始捕捉拇指棒的位置。接下来的两个`if`块测试拇指操纵杆位置是否在死区内。如果是，则适当的值被设置为零以避免船只漂移。然后，我们可以在`PlayerUpdateComponent`实例上调用`updateShipTravelWithController`函数。那是处理过的拇指棒。\n\n如果按下游戏手柄上的 B 按钮，下一条`if`语句会将布尔值设置为`true`。下一个`if`语句检测 B 按钮何时未被按下，布尔值被设置为`true`。这表明 B 按钮刚刚被释放。\n\n在`if`块内部，我们将布尔设置为`false`，准备处理下一个按钮释放，播放射击声音，获取子弹的产卵位置，通过链接`getmParentScreen`和`getBulletSpawner`功能调用`spawnBullet`功能。\n\n# 对物理引擎播放模式类进行编码\n\n这个类将完成所有的碰撞检测。在这个游戏中，我们要注意几个碰撞事件:\n\n*   入侵者到达了屏幕的左侧还是右侧？如果是这样的话，所有的入侵者都需要下降一排，向另一个方向返回。\n*   有入侵者和玩家相撞吗？随着入侵者越来越低，我们希望他们能够撞上玩家，导致一条生命损失。\n*   入侵者的子弹击中玩家了吗？每次入侵者的子弹打中玩家，我们都需要把子弹藏起来，准备再次使用，从玩家身上扣除一条命。\n*   玩家子弹击中入侵者了吗？玩家每打一个入侵者，入侵者就应该被消灭，子弹被隐藏(准备重复使用)，玩家的分数增加。\n\n这个类将有一个`GameScreen`类调用的`initialize`函数，为检测碰撞做准备，`GameScreen`类将在所有游戏对象更新后为每一帧调用一次`detectCollisions`函数，以及另外三个将从`detectCollisions`函数调用的函数，以分离出我刚才列出的检测不同碰撞的工作。\n\n这三个功能分别是`detectInvaderCollisions`、`detectPlayerCollisionsAndInvaderDirection`和`handleInvaderDirection`。希望这些函数的名字能清楚地说明每个函数中会发生什么。\n\n在名为`PhysicsEnginePlayMode.h`的`Header Files/Engine`过滤器中创建新的源文件，并添加以下代码:\n\n```cpp\n#pragma once\n#include \"GameObjectSharer.h\"\n#include \"PlayerUpdateComponent.h\"\nclass PhysicsEnginePlayMode\n{\nprivate:\n    shared_ptr<PlayerUpdateComponent> m_PUC;\n    GameObject* m_Player;\n    bool m_InvaderHitWallThisFrame = false;\n    bool m_InvaderHitWallPreviousFrame = false;\n    bool m_NeedToDropDownAndReverse = false;\n    bool m_CompletedDropDownAndReverse = false;\n    void detectInvaderCollisions(\n        vector<GameObject>& objects,\n        const vector<int>& bulletPositions);\n    void detectPlayerCollisionsAndInvaderDirection(\n        vector<GameObject>& objects,\n        const vector<int>& bulletPositions);\n    void handleInvaderDirection();\npublic:\n    void initilize(GameObjectSharer& gos);\n    void detectCollisions(\n        vector<GameObject>& objects,\n        const vector<int>& bulletPositions);\n};\n```\n\n研究前面的代码，记下传递给每个函数的参数。还要注意将在整个类中使用的四个成员布尔变量。此外，请注意，有一个指向正在声明的`GameObject`类型的指针，这将是对玩家船的永久引用，因此我们不需要在游戏循环的每一帧中不断找到代表玩家的`GameObject`。\n\n在名为`PhysicsEnginePlayMode.cpp`的`Source Files/Engine`过滤器中创建新的源文件，并添加以下包含指令和`detectInvaderCollisions`函数。研究代码，然后我们将讨论它:\n\n```cpp\n#include \"DevelopState.h\"\n#include \"PhysicsEnginePlayMode.h\"\n#include <iostream>\n#include \"SoundEngine.h\"\n#include \"WorldState.h\"\n#include \"InvaderUpdateComponent.h\"\n#include \"BulletUpdateComponent.h\"\nvoid PhysicsEnginePlayMode::\ndetectInvaderCollisions(\n    vector<GameObject>& objects, \n    const vector<int>& bulletPositions)\n{\nVector2f offScreen(-1, -1);\nauto invaderIt = objects.begin();\nauto invaderEnd = objects.end();\nfor (invaderIt;\n    invaderIt != invaderEnd;\n    ++ invaderIt)\n{\n    if ((*invaderIt).isActive()\n        && (*invaderIt).getTag() == \"invader\")\n    {\n        auto bulletIt = objects.begin();\n        // Jump to the first bullet\n        advance(bulletIt, bulletPositions[0]);\n        auto bulletEnd = objects.end();\n        for (bulletIt;\n            bulletIt != bulletEnd;\n            ++ bulletIt)\n        {\n            if ((*invaderIt).getEncompassingRectCollider()\n                .intersects((*bulletIt)\n                    .getEncompassingRectCollider())\n                && (*bulletIt).getTag() == \"bullet\"\n                && static_pointer_cast<\n                      BulletUpdateComponent>(\n                (*bulletIt).getFirstUpdateComponent())\n                ->m_BelongsToPlayer)\n            {\n                SoundEngine::playInvaderExplode();\n                (*invaderIt).getTransformComponent()\n                    ->getLocation() = offScreen;\n                (*bulletIt).getTransformComponent()\n                    ->getLocation() = offScreen;\n                WorldState::SCORE++ ;\n                WorldState::NUM_INVADERS--;\n                (*invaderIt).setInactive();\n            }\n        }\n    }\n}\n}\n```\n\n前面的代码遍历了所有的游戏对象。第一个`if`语句检查当前游戏对象是否是活动的和入侵者:\n\n```cpp\nif ((*invaderIt).isActive()\n        && (*invaderIt).getTag() == \"invader\")\n```\n\n如果是主动入侵者，则进入另一个循环，代表子弹的每个游戏对象循环通过:\n\n```cpp\nauto bulletIt = objects.begin();\n// Jump to the first bullet\nadvance(bulletIt, bulletPositions[0]);\nauto bulletEnd = objects.end();\nfor (bulletIt;\n    bulletIt != bulletEnd;\n    ++ bulletIt)\n```\n\n下一个`if` 语句检查当前入侵者是否与当前子弹相撞，以及该子弹是否由玩家发射(我们不希望入侵者自己射击自己):\n\n```cpp\nif ((*invaderIt).getEncompassingRectCollider()\n        .intersects((*bulletIt)\n        .getEncompassingRectCollider())\n        && (*bulletIt).getTag() == \"bullet\"\n        && static_pointer_cast<BulletUpdateComponent>(\n        (*bulletIt).getFirstUpdateComponent())\n        ->m_BelongsToPlayer)\n```\n\n当该测试为真时，播放声音，子弹移出屏幕，入侵者数量减少，玩家分数增加，入侵者设置为非活动状态。\n\n现在，我们将检测玩家碰撞和入侵者的行进方向。\n\n添加`detectPlayerCollisionsAndInvaderDirection`功能，如下:\n\n```cpp\nvoid PhysicsEnginePlayMode::\ndetectPlayerCollisionsAndInvaderDirection(\n    vector<GameObject>& objects, \n    const vector<int>& bulletPositions)\n{\nVector2f offScreen(-1, -1);\nFloatRect playerCollider = \n    m_Player->getEncompassingRectCollider();\nshared_ptr<TransformComponent> playerTransform = \n    m_Player->getTransformComponent();\nVector2f playerLocation = \n    playerTransform->getLocation();\nauto it3 = objects.begin();\nauto end3 = objects.end();\nfor (it3;\n    it3 != end3;\n    ++ it3)\n{\n    if ((*it3).isActive() &&\n        (*it3).hasCollider() &&\n        (*it3).getTag() != \"Player\")\n    {\n        // Get a reference to all the parts of \n        // the current game object we might need\n        FloatRect currentCollider = (*it3)\n            .getEncompassingRectCollider();\n        // Detect collisions between objects \n        // with the player\n        if (currentCollider.intersects(playerCollider))\n        {\n            if ((*it3).getTag() == \"bullet\")\n            {\n                SoundEngine::playPlayerExplode();\n                WorldState::LIVES--;\n                (*it3).getTransformComponent()->\n                    getLocation() = offScreen;\n            }\n            if ((*it3).getTag() == \"invader\")\n            {\n                SoundEngine::playPlayerExplode();\n                SoundEngine::playInvaderExplode();\n                WorldState::LIVES--;\n                (*it3).getTransformComponent()->\n                    getLocation() = offScreen;\n                WorldState::SCORE++ ;\n                (*it3).setInactive();\n            }\n        }\n        shared_ptr<TransformComponent> \n            currentTransform =\n            (*it3).getTransformComponent();\n        Vector2f currentLocation = \n            currentTransform->getLocation();\n        string currentTag = (*it3).getTag();\n        Vector2f currentSize = \n            currentTransform->getSize();\n        // Handle the direction and descent \n        // of the invaders\n        if (currentTag == \"invader\")\n        {\n            // This is an invader\n            if (!m_NeedToDropDownAndReverse && \n                !m_InvaderHitWallThisFrame)\n            {\n                // Currently no need to dropdown \n                // and reverse from previous frame \n                // or any hits this frame\n                if (currentLocation.x >= \n                    WorldState::WORLD_WIDTH – \n                            currentSize.x)\n                {\n                    // The invader is passed its \n                    // furthest right position\n                    if (static_pointer_cast\n                        <InvaderUpdateComponent>((*it3)\n                        .getFirstUpdateComponent())->\n                        isMovingRight())\n                    {\n                        // The invader is travelling \n                        // right so set a flag that\n                        // an invader has collided\n\n                        m_InvaderHitWallThisFrame \n                                         = true;\n                    }\n                }\n                else if (currentLocation.x < 0)\n                {\n                    // The invader is past its furthest \n                    // left position\n                    if (!static_pointer_cast\n                        <InvaderUpdateComponent>(        \n                            (*it3).getFirstUpdateComponent())\n                        ->isMovingRight())\n                    {\n                        // The invader is travelling \n                        // left so set a flag that an\n                        // invader has collided \n                        m_InvaderHitWallThisFrame \n                                         = true;\n                    }\n                }\n            }\n            else if (m_NeedToDropDownAndReverse \n                && !m_InvaderHitWallPreviousFrame)\n            {\n                // Drop down and reverse has been set\n                if ((*it3).hasUpdateComponent())\n                {\n                    // Drop down and reverse\n                    static_pointer_cast<\n                            InvaderUpdateComponent>(            \n                            (*it3).getFirstUpdateComponent())\n                    ->dropDownAndReverse();\n                }\n            }\n        }\n    }\n}\n}\n```\n\n前面的代码比前面的函数长，因为我们正在检查更多的条件。在代码遍历所有游戏对象之前，它会获取所有相关玩家数据的引用。这样我们就不必每次检查都这样做:\n\n```cpp\nFloatRect playerCollider = \n    m_Player->getEncompassingRectCollider();\nshared_ptr<TransformComponent> playerTransform = \n    m_Player->getTransformComponent();\nVector2f playerLocation = \n    playerTransform->getLocation();\n```\n\n接下来，循环遍历每个游戏对象。第一个`if`测试检查当前物体是否是活动的，有碰撞器，不是玩家。我们不想测试玩家与自己的碰撞:\n\n```cpp\nif ((*it3).isActive() &&\n    (*it3).hasCollider() &&\n    (*it3).getTag() != \"Player\")\n```\n\n下一个`if`测试进行实际碰撞检测，看当前游戏对象是否与玩家相交:\n\n```cpp\nif (currentCollider.intersects(playerCollider))\n```\n\n接下来，有两个嵌套的`if`语句:一个处理与属于入侵者的子弹的碰撞，一个处理与入侵者的碰撞。\n\n接下来，代码检查每一个入侵者的游戏对象，看它是否击中了屏幕的左侧或右侧。请注意，`m_NeedToDropDownAndReverse`和`m_InvaderHitWallLastFrame`布尔变量被使用，因为它不会总是击中屏幕侧面的向量中的第一个入侵者。因此，检测冲突并触发下拉和反转是在连续的帧中处理的，以保证所有入侵者都下拉和反转，而不管是哪一个触发它。\n\n最后，当两个条件都为`true`时，调用`handleInvaderDirection`。\n\n添加`handleInvaderDirection`功能，如下:\n\n```cpp\nvoid PhysicsEnginePlayMode::handleInvaderDirection()\n{\n    if (m_InvaderHitWallThisFrame) {\n        m_NeedToDropDownAndReverse = true;\n        m_InvaderHitWallThisFrame = false;\n    }\n    else {\n        m_NeedToDropDownAndReverse = false;\n    }\n}\n```\n\n该函数只是相应地设置和取消布尔，以便下一次通过`detectPlayerCollisionAndDirection`函数时，实际上会下拉入侵者并改变他们的方向。\n\n添加`initialize`功能修复动作类:\n\n```cpp\nvoid PhysicsEnginePlayMode::initilize(GameObjectSharer& gos) {\n    m_PUC = static_pointer_cast<PlayerUpdateComponent>(\n        gos.findFirstObjectWithTag(\"Player\")\n        .getComponentByTypeAndSpecificType(\"update\", \"player\"));\n    m_Player = &gos.findFirstObjectWithTag(\"Player\");\n}\n```\n\n在前面的代码中，指向`PlayerUpdateComponent`的指针以及指向玩家`GameObject`的指针被初始化。这将避免在游戏循环中调用这些相对较慢的函数。\n\n添加`detectCollisions`函数，每帧从`GameScreen`类调用一次:\n\n```cpp\nvoid PhysicsEnginePlayMode::detectCollisions(\n    vector<GameObject>& objects,\n    const vector<int>& bulletPositions)\n{\n    detectInvaderCollisions(objects, bulletPositions);\n    detectPlayerCollisionsAndInvaderDirection(\n        objects, bulletPositions);\n    handleInvaderDirection();    \n}\n```\n\n`detectCollisions`函数调用处理碰撞检测不同阶段的三个函数。你可以把所有的代码都集中到这个单一的函数中，但是那样会很笨拙。或者，您可以将这三个大功能分成它们自己的`.cpp`文件，就像我们在托马斯迟到游戏中对`update`和`draw`功能所做的那样。\n\n在下一节中，我们将创建一个`PhysicsEngineGameMode`类的实例，并在`GameScreen`类中使用它，让游戏变得生动起来。\n\n# 制作游戏\n\n在本节结束时，我们将有一个可玩的游戏。在这一节中，我们将向`GameScreen`类添加代码，以汇集我们在过去三章中编码的所有内容。首先，通过添加额外的 include 指令，向`GameScreen.h`添加一个`PhysicsEngineGameMode`实例，如下所示:\n\n```cpp\n#include \"PhysicsEnginePlayMode.h\"\n```\n\n然后，声明一个实例，如下面的代码所示:\n\n```cpp\nprivate:\n    ScreenManagerRemoteControl* m_ScreenManagerRemoteControl;\n    shared_ptr<GameInputHandler> m_GIH;\n PhysicsEnginePlayMode m_PhysicsEnginePlayMode;\n…\n…\n```\n\n现在，打开`GameScreen.cpp`文件，添加一些额外的 include 指令，并正向声明`BulletSpawner`类，如下代码所示:\n\n```cpp\n#include \"GameScreen.h\"\n#include \"GameUIPanel.h\"\n#include \"GameInputHandler.h\"\n#include \"GameOverUIPanel.h\"\n#include \"GameObject.h\"\n#include \"WorldState.h\"\n#include \"BulletUpdateComponent.h\"\n#include \"InvaderUpdateComponent.h\"\nclass BulletSpawner;\nint WorldState::WORLD_HEIGHT;\nint WorldState::NUM_INVADERS;\nint WorldState::NUM_INVADERS_AT_START;\n```\n\n接下来，在`GameScreen.cpp`文件中，通过在现有代码中添加以下高亮显示的代码来更新`initialize`功能:\n\n```cpp\nvoid GameScreen::initialise()\n{\n    m_GIH->initialize();\n m_PhysicsEnginePlayMode.initilize(\n m_ScreenManagerRemoteControl->\n shareGameObjectSharer());\n    WorldState::NUM_INVADERS = 0;\n // Store all the bullet locations and\n // Initialize all the BulletSpawners in the invaders\n // Count the number of invaders\n int i = 0;\n auto it = m_ScreenManagerRemoteControl->\n getGameObjects().begin();\n auto end = m_ScreenManagerRemoteControl->\n getGameObjects().end();\n for (it;\n it != end;\n ++ it)\n {\n if ((*it).getTag() == \"bullet\")\n {\n m_BulletObjectLocations.push_back(i);\n }\n if ((*it).getTag() == \"invader\")\n {\n static_pointer_cast<InvaderUpdateComponent>(\n (*it).getFirstUpdateComponent())->\n initializeBulletSpawner(\n getBulletSpawner(), i);\n WorldState::NUM_INVADERS++ ;\n }\n ++ i;\n }\n    m_GameOver = false;\n    if (WorldState::WAVE_NUMBER == 0)\n    {\n        WorldState::NUM_INVADERS_AT_START = \n            WorldState::NUM_INVADERS;\n\n        WorldState::WAVE_NUMBER = 1;\n        WorldState::LIVES = 3;\n        WorldState::SCORE = 0;\n    }\n}\n```\n\n`initialize`函数中的前一个代码初始化了将处理所有碰撞检测的物理引擎。接下来，它循环遍历所有游戏对象，并执行两个任务:每个`if`块中一个任务。\n\n第一`if`块测试当前游戏对象是否为子弹。如果是，则它在游戏对象向量中的整数位置存储在`m_BulletObjectLocations vector`中。还记得我们对物理引擎进行编码时，这个`vector`在碰撞检测时很有用。当玩家或入侵者想要射击时，这个向量也将在这个类中用来跟踪下一个要使用的子弹。\n\n第二个`if`块检测当前游戏对象是否是入侵者，如果是，则在其更新组件上调用`initializeBulletSpawner`函数，并通过调用`getBulletSpawner`函数传递指向`BulletSpawner`的指针。入侵者现在有能力制造子弹。\n\n现在，我们需要在`update`函数中添加一些代码来处理更新阶段游戏的每一帧中发生的事情。这在下面的代码中突出显示。所有新代码都进入已经存在的`if(!m_GameOver)`块:\n\n```cpp\nvoid GameScreen::update(float fps)\n{\n    Screen::update(fps);\n    if (!m_GameOver)\n    {\n if (m_WaitingToSpawnBulletForPlayer)\n {\n static_pointer_cast<BulletUpdateComponent>(\n m_ScreenManagerRemoteControl->\n getGameObjects()\n [m_BulletObjectLocations[m_NextBullet]].\n getFirstUpdateComponent())->\n spawnForPlayer(\n m_PlayerBulletSpawnLocation);\n\n m_WaitingToSpawnBulletForPlayer = false;\n m_NextBullet++ ;\n if (m_NextBullet == m_BulletObjectLocations\n .size())\n {\n m_NextBullet = 0;\n }\n }\n if (m_WaitingToSpawnBulletForInvader)\n {\n static_pointer_cast<BulletUpdateComponent>(\n m_ScreenManagerRemoteControl->\n getGameObjects()\n [m_BulletObjectLocations[m_NextBullet]].\n getFirstUpdateComponent())->\n spawnForInvader(\n m_InvaderBulletSpawnLocation);\n\n m_WaitingToSpawnBulletForInvader = false;\n m_NextBullet++ ;\n if (m_NextBullet == \n m_BulletObjectLocations.size())\n {\n m_NextBullet = 0;\n }\n }\n auto it = m_ScreenManagerRemoteControl->\n getGameObjects().begin();\n auto end = m_ScreenManagerRemoteControl->\n getGameObjects().end();\n for (it;\n it != end;\n ++ it)\n {\n (*it).update(fps);\n }\n\n m_PhysicsEnginePlayMode.detectCollisions(\n m_ScreenManagerRemoteControl->getGameObjects(), \n m_BulletObjectLocations);\n        if (WorldState::NUM_INVADERS <= 0)\n        {\n            WorldState::WAVE_NUMBER++ ;\n            m_ScreenManagerRemoteControl->\n                loadLevelInPlayMode(\"level1\");\n        }\n\n        if (WorldState::LIVES <= 0)\n        {\n            m_GameOver = true;            \n        }\n    }\n}\n```\n\n在前面的新代码中，第一个`if`块检查玩家是否需要新的子弹。如果它是下一个可用的项目符号，`GameObject`实例调用其`BulletUpdateComponent`实例的`spawnForPlayer`函数。使用带有`m_BulletObjectLocations`向量的`m_NextBulletObject`变量来标识要使用的特定`GameObject`实例。第一个`if`块中剩余的代码准备发射下一颗子弹。\n\n如果入侵者正在等待发射子弹，则执行第二个`if`块。使用完全相同的技术来激活子弹，除了使用`spawnForInvader`功能，将它设置为向下移动。\n\n接下来，有一个循环，循环通过每个游戏对象。这是一切的关键，因为在循环内部，每个`GameObject`实例都会调用`update`函数。\n\n前面新代码的最后一行代码调用`detectCollisions`函数，查看是否有任何`GameObject`实例(在其刚刚更新的位置)发生了冲突。\n\n最后，我们将在`GameScreen.cpp`中给`draw`函数添加一些代码。在下面的列表中，新代码在现有代码中突出显示:\n\n```cpp\nvoid GameScreen::draw(RenderWindow & window)\n{    \n    // Change to this screen's view to draw\n    window.setView(m_View);\n    window.draw(m_BackgroundSprite);\n // Draw the GameObject instances\n auto it = m_ScreenManagerRemoteControl->\n getGameObjects().begin();\n auto end = m_ScreenManagerRemoteControl->\n getGameObjects().end();\n for (it;\n it != end;\n ++ it)\n {\n (*it).draw(window);\n }\n    // Draw the UIPanel view(s)\n    Screen::draw(window);\n}\n```\n\n前面的代码只是依次调用每个`GameObject`实例上的`draw`函数。现在，你已经完成了太空入侵者++ 项目，可以运行游戏了。恭喜你！\n\n# 了解执行和调试流程\n\n最后四章的大部分内容都是关于代码结构的。对于哪个类实例化哪个实例，或者各种函数的调用顺序，您很可能仍然有疑问和不确定性。如果在`Space Invaders ++.cpp`文件中有一种方法可以执行项目，并遵循从`int main()`一直到`return 0;`的执行路径，那不是很有用吗？事实证明我们可以，下面是如何做到的。\n\n我们现在将探索 Visual Studio 中的调试工具，同时尝试理解项目的结构。\n\n打开`Space Invaders ++.cpp`文件，找到第一行代码，如下:\n\n```cpp\nGameEngine m_GameEngine;\n```\n\n前面的代码是执行的第一行代码。它声明了`GameEngine`类的一个实例，并启动了我们所有的努力工作。\n\n右键单击前一行代码，选择**断点** | **插入断点**。以下是屏幕的外观:\n\n![](img/B14278_22_02.jpg)\n\n请注意，代码行旁边有一个红色圆圈。这是一个断点。当您运行代码时，执行将在这一点上暂停，我们将有一些有趣的选项可供选择。\n\n以通常的方式运行游戏。当执行暂停时，箭头指示当前的执行行，如下图所示:\n\n![](img/B14278_22_03.jpg)\n\n如果您将鼠标悬停在`m_GameEngine`文本上，然后单击箭头(以下截图中的左上角)，您将预览`m_GameEngine`实例中的所有成员变量及其值:\n\n![](img/B14278_22_04.jpg)\n\n让我们来看看代码。在主菜单中，查找以下图标集:\n\n![](img/B14278_22_05.jpg)\n\n如果您单击上一个屏幕截图中突出显示的箭头图标，它将移动到下一行代码。该箭头图标是**进入**按钮。下一行代码将是`GameEngine`构造函数的顶部。您可以继续点击**进入**按钮，在任何阶段检查任何变量的值。\n\n如果你点击进入`m_Resolution`的初始化，那么你会看到代码跳转到 SFML 提供的`Vector2i`类。继续点击查看组成我们游戏的所有步骤的代码流进度。\n\n如果想跳到下一个功能，可以点击**步出**按钮，如下图截图所示:\n\n![](img/B14278_22_06.jpg)\n\n只要你感兴趣，就跟着执行的流程走。完成后，只需点击**停止**按钮，如下图截图所示:\n\n![](img/B14278_22_07.jpg)\n\n或者，如果你想在不单步执行代码的情况下运行游戏，可以点击如下截图所示的**继续**按钮。但是，请注意，如果断点位于循环内部，则每次执行流到达断点时，它都会停止:\n\n![](img/B14278_22_08.jpg)\n\n如果你想从不同的起点来检查代码的流程，而不想一开始就必须点进每一行或每一个函数，那么你所需要做的就是设置不同的断点。\n\n可以通过停止调试(用**停止**按钮)，右键单击红色圆圈，选择**删除断点**来删除断点。\n\n然后，您可以通过在`GameEngine.cpp`的`update`函数的第一行代码中设置断点来开始遍历游戏循环。您可以在任何地方放置一个断点，因此可以随意探索单个组件或其他任何地方的执行流程。代码中值得检查的关键部分之一是`GameScreen`类的*更新*函数中的执行流程。为什么不试试呢？\n\n虽然我们刚刚探索的内容是有用的和有指导意义的，但是 Visual Studio 提供的这些工具的真正目的是调试我们的游戏。每当您得到不符合您预期的行为时，只需在任何可能导致问题的行中添加一个断点，逐步执行，并观察变量值。\n\n# 重用代码制作不同的游戏，构建设计模式\n\n有几次，我们已经讨论过我们编码的这个系统可以被重用来制作一个完全不同的游戏的可能性。我只是觉得完全听取这个事实是值得的。\n\n制作不同游戏的方法如下。我已经提到过，您可以将游戏对象的外观编码到从`GraphicsComponent`类派生的新组件中，并且可以将新行为编码到从`UpdateComponent`类派生的类中。\n\n假设您想要一组具有重叠行为的游戏对象；考虑一个 2D 游戏，在这个游戏中，敌人追捕玩家，然后在一定距离向玩家射击。\n\n也许你可以有一个接近玩家并向玩家发射手枪的敌人类型和一个向玩家远距离射击的敌人类型，就像狙击手可能会做的那样。\n\n你可以编写一个`EnemyShooterUpdateComponent`类和一个`EnemySniperUpdateComponent`类。您可以在`start`函数中获得一个指向玩家转换组件的共享指针，并编写一个抽象类(如`BulletSpawner`)来触发玩家的产卵射击，您就完成了。\n\n然而，考虑到这两个游戏对象都有拍摄的代码和接近玩家的代码。然后考虑一下，在某个阶段，你可能想要一个“打架”的敌人，他试图打玩家。\n\n当前系统也可以有多个更新组件。然后你可以有一个`ChasePlayerUpdateComponent`类来接近玩家，并分离更新组件来打卡、射击或狙击玩家。打孔/射击/狙击组件将在追逐组件上强制执行一些关于何时停止和开始追逐的值，然后更具体的组件(打孔、射击或狙击)将在提示时间合适时攻击玩家。\n\n正如我们已经提到的，在多个不同的更新组件上调用`update`函数的能力已经内置在代码中，尽管它从未经过测试。如果你看一下`GameObject.cpp`中的`update`功能，你会看到这个代码:\n\n```cpp\n    for (int i = m_FirstUpdateComponentLocation; i < \n        m_FirstUpdateComponentLocation + \n        m_NumberUpdateComponents; i++) \n    {\n   …\n}\n```\n\n在前面的代码中，`update`函数将在存在的尽可能多的更新组件上被调用。你只需要将它们编码并添加到`level1.txt`文件中的特定游戏对象中。使用这个系统，一个游戏对象可以有任意多的更新组件，允许您封装非常具体的行为，并根据需要围绕所需的游戏对象共享它们。\n\n当您想要创建一个对象池时，就像我们为入侵者和子弹所做的那样，您可以比我们在太空入侵者++ 项目中更高效。为了向您展示如何在游戏世界中定位对象，我们单独添加了所有入侵者和子弹。在实际项目中，您只需设计一个代表子弹池的类型，也许是一个子弹盒，如下所示:\n\n```cpp\n[NAME]magazine of bullets[-NAME]\n```\n\n你可以对一队入侵者做同样的事情:\n\n```cpp\n[NAME]fleet of invaders[-NAME]\n```\n\n然后，您将对工厂进行编码，以处理一个杂志或舰队，可能带有一个`for`循环，稍微麻烦的文本文件将得到改进。当然，您可以跨多个文本文件设计的不同级别的数量没有限制。这些文本文件更可能的名称是`beach_level.txt`或`urban_level.txt`。\n\n你可能想知道一些类的名字，比如`PhysicsEnginePlayMode`或者`GameObjectFactoryPlayMode`。这意味着`…PlayMode`只是这些班级的一个选择。\n\n我在这里提出的建议是，即使你在你的关卡设计文件中使用了舰队/弹匣策略，随着它们的增长，它们仍然会变得笨重。如果您可以查看级别并在屏幕上编辑它们，然后将更改保存回文件，那就更好了。\n\n您肯定需要新的物理引擎规则(检测对象上的点击和拖动)、新的屏幕类型(不更新每一帧)以及可能用于从文本文件解释和构建对象的新类。然而，关键是实体-组件/屏幕/用户界面面板/输入处理系统可以保持不变。\n\n甚至没有任何东西可以阻止你设计一些全新的组件类型，例如，一个滚动的背景对象，可以检测玩家移动的方向并相应地移动，或者一个交互式的提升对象，可以检测玩家何时站在上面，然后接受输入上下移动。我们甚至可以有一扇门可以打开和关闭，或者一个传送对象，当玩家触摸它时，它会检测输入，并从另一个文本文件加载一个新的级别。这里的重点是，这些都是游戏机制，可以很容易地集成到同一个系统。\n\n我可以继续谈论这些可能性更长的时间，但你可能宁愿自己做游戏。\n\n# 总结\n\n在这一章中，我们最终完成了太空入侵者++ 游戏。我们为游戏对象编写了一种请求产生子弹的方法，学习了如何从游戏手柄接收输入，并加入了游戏的最终逻辑来实现它。\n\n然而，也许这一章最重要的是，最后四章的辛劳将如何帮助你开始下一个项目。\n\n这本书有最后一章，有点厚，我保证，这是一个简短的一章。"
  },
  {
    "path": "docs/begin-cpp-game-prog/23.md",
    "content": "# 二十三、结束之前\n\n当你第一次打开一本书的这个大的门挡时，后面的一页可能看起来很遥远。但我希望不是太难。\n\n关键是你现在在这里，希望你对如何使用 C++ 构建游戏有很好的见解。\n\n这一章的重点是祝贺你取得的杰出成就，但也指出这一页可能不应该是你旅程的终点。如果像我一样，每当你让一个新的游戏功能变得生动起来时，你都会感到一阵兴奋，那么你可能想了解更多。\n\n听到这可能会让你感到惊讶，即使在这几百页之后，我们也只是在 C++ 中摸索。即使是我们已经讨论过的主题也可以更深入地讨论，还有许多——有些非常重要的——我们甚至没有提到的主题。考虑到这一点，让我们看看接下来会发生什么。\n\n如果你一定要有正式的资格，那么唯一的方法就是接受正式的教育。这当然是既费钱又费时，我实在帮不上什么忙。\n\n另一方面，如果你想在工作中学习，也许在开始一个你最终会发布的游戏的工作时，接下来是一个关于你下一步可能想做什么的讨论。\n\n对于每个项目，我们面临的最艰难的决定可能是如何构建我们的代码。在我看来，关于如何构造你的 C++ 游戏代码的绝对最好的信息来源是[http://gameprogrammingpatterns.com/](http://gameprogrammingpatterns.com/)。有些讨论是围绕本书没有涉及的概念展开的，但大部分都是完全可以理解的。如果你了解类、封装、纯虚函数和单例，请进入这个网站。\n\n我已经在这本书里指出了 SFML 的网站。如果你还没有去过，请看看:[http://www.sfml-dev.org/](http://www.sfml-dev.org/)。\n\n当你遇到你不懂(甚至没听说过)的 C++ 话题时，最简洁有条理的 C++ 教程可以在[http://www.cplusplus.com/doc/tutorial/](http://www.cplusplus.com/doc/tutorial/)找到。\n\n除此之外，你可能还想看四本 SFML 的书。它们都是好书，但适合谁却千差万别。以下是从初级到高级的书籍列表:\n\n*   *SFML 精粹*作者:米尔乔·米尔切夫:[https://www.packtpub.com/game-development/sfml-essentials](https://www.packtpub.com/game-development/sfml-essentials)\n*   马克西姆·栗林诚一郎的 SFML 蓝图:[https://www.packtpub.com/game-development/sfml-blueprints](https://www.packtpub.com/game-development/sfml-blueprints)\n*   *SFML 游戏开发示例*作者:雷蒙达斯·普皮乌斯:[https://www . packtpub . com/Game-Development/sfml-Game-Development-示例](https://www.packtpub.com/game-development/sfml-game-development-example)\n*   *简·哈勒、亨里克·沃格柳斯·汉森和阿图尔·莫雷拉的 SFML 游戏开发*\n\n你也可以考虑在你的游戏中加入栩栩如生的 2D 物理学。SFML 与 Box2d 物理引擎完美配合。本网址为官方网站:[http://box2d.org/](http://box2d.org/)。以下网址将带您找到在 C++ 中使用它的最佳指南:[http://www.iforce2d.net/](http://www.iforce2d.net/)。\n\n最后，我要恬不知耻地为初学游戏的程序员们插上自己的网站:[http://gamecodeschool.com](http://gamecodeschool.com)。\n\n# 谢谢！\n\n最重要的是，非常感谢大家买了这本书，继续做游戏！"
  },
  {
    "path": "docs/begin-cpp-game-prog/README.md",
    "content": "# C++ 游戏编程入门手册\n\n> 原书：[Beginning C++ Game Programming](https://libgen.rs/book/index.php?md5=8B22C2649BDEC9FA4EE716AE82AE0BB1)\n> \n> 协议：[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)\n> \n> 阶段：机翻（1）\n> \n> 自豪地采用[谷歌翻译](https://translate.google.cn/)\n> \n> 让开源界提前感受入关。\n\n* [在线阅读](https://ccpp.apachecn.org)\n* [在线阅读（Gitee）](https://apachecn.gitee.io/apachecn-c-cpp-zh/)\n* [ApacheCN 学习资源](http://docs.apachecn.org/)\n\n## 贡献指南\n\n本项目需要校对，欢迎大家提交 Pull Request。\n\n> 请您勇敢地去翻译和改进翻译。虽然我们追求卓越，但我们并不要求您做到十全十美，因此请不要担心因为翻译上犯错——在大部分情况下，我们的服务器已经记录所有的翻译，因此您不必担心会因为您的失误遭到无法挽回的破坏。（改编自维基百科）\n\n## 联系方式\n\n### 负责人\n\n* [飞龙](https://github.com/wizardforcel): 562826179\n\n### 其他\n\n*   在我们的 [apachecn/apachecn-c-cpp-zh](https://github.com/apachecn/apachecn-c-cpp-zh) github 上提 issue.\n*   发邮件到 Email: `apachecn@163.com`.\n*   在我们的 [组织学习交流群](http://www.apachecn.org/organization/348.html) 中联系群主/管理员即可.\n\n## 赞助我们\n\n![](http://data.apachecn.org/img/about/donate.jpg)\n"
  },
  {
    "path": "docs/begin-cpp-game-prog/SUMMARY.md",
    "content": "+   [C++ 游戏编程入门手册](README.md)\n+   [零、序言](00.md)\n+   [一、C++，SFML，VisualStudio，并开始第一个游戏](01.md)\n+   [二、变量、运算符和决策——设置精灵动画](02.md)\n+   [三、C++ 字符串和 SFML 时间——玩家输入和 HUD](03.md)\n+   [四、循环、数组、`switch`、枚举和函数——实现游戏机制](04.md)\n+   [五、碰撞、声音和结束条件——使游戏可玩](05.md)\n+   [六、面向对象编程——启动乒乓球游戏](06.md)\n+   [七、动态碰撞检测与物理——完成乒乓球游戏](07.md)\n+   [八、SFML 视图——开始僵尸射击游戏](08.md)\n+   [九、C++ 引用、精灵列表和顶点数组](09.md)\n+   [十、指针、标准模板库、纹理管理](10.md)\n+   [十一、碰撞检测，拾音器和子弹](11.md)\n+   [十二、视图分层与 HUD 实现](12.md)\n+   [十三、音效，文件 I/O，完成游戏](13.md)\n+   [十四、抽象和代码管理——更好地利用面向对象](14.md)\n+   [十五、高级 OOP——继承与多态](15.md)\n+   [十六、建造可玩关卡和碰撞检测](16.md)\n+   [十七、声音空间化和平视显示器](17.md)\n+   [十八、粒子系统和着色器](18.md)\n+   [十九、游戏编程设计模式——启动太空入侵者 ++ 游戏](19.md)\n+   [二十、游戏对象和组件](20.md)\n+   [二十一、文件输入输出和游戏对象工厂](21.md)\n+   [二十二、使用游戏对象和构建游戏](22.md)\n+   [二十三、结束之前](23.md)\n"
  },
  {
    "path": "docs/begin-cpp-prog/00.md",
    "content": "# 零、前言\n\nC++ 已经使用了 30 年，在此期间，许多新的语言层出不穷，但 C++ 却经久不衰。 这本书背后的一个大问题是：为什么？ 为什么使用 C++？ 答案就在你面前的十章中，但作为一个搅局者，它是语言的灵活性和力量，以及丰富而广泛的标准库。\n\nC++ 一直是一种强大的语言，让您可以直接访问内存，同时提供高级功能，如创建新类型(类)和覆盖操作符以满足您的需要。 然而，更现代的 C++ 标准增加了这一点，通过模板进行泛型编程，通过函数对象和 lambda 表达式进行函数式编程。 您可以根据需要使用这些功能中的任意数量；可以使用抽象接口指针编写事件驱动代码，也可以编写类似 C 语言的过程代码。\n\n在本书中，我们将带您了解 2011 年的 C++ 标准的功能以及该语言提供的标准库。 本文通过简短的代码片段解释了如何使用这些功能，并且每章都有一个实际的示例来说明这些概念。 在本书的最后，您将了解该语言的所有功能以及 C++ 标准库可以实现的功能。 您将以初学者的身份开始这本书，然后在了解并准备好使用 C++ 的情况下读完这本书。\n\n# 这本书涵盖了哪些内容\n\n[第 1 章](01.html)，*从 C++*开始，解释了用于编写 C++ 应用的文件、文件依赖关系以及 C++ 项目管理的基础知识。\n\n[第 2 章](02.html)，*了解语言功能*，涵盖 C++ 语句和表达式、常量、变量、运算符，以及如何控制应用中的执行流。\n\n[第 3 章](03.html)，*探索 C++ 类型*，描述了 C++ 内置类型、聚合类型、类型别名、初始值设定项列表以及类型之间的转换。\n\n[第 4 章](04.html)，*使用内存、数组和指针*，介绍了如何在 C++ 应用中分配和使用内存，如何使用内置数组，C++ 引用的作用，以及如何使用 C++ 指针访问内存。\n\n[第 5 章](05.html)，*使用函数*解释了如何定义函数，如何使用可变数量的参数逐个引用和逐值传递参数，创建和使用指向函数的指针，以及定义模板函数和重载运算符。\n\n[第 6 章](06.html)，*类*描述了如何通过类和类中使用的各种特殊函数定义新类型，如何将类实例化为对象并销毁它们，以及如何通过指针访问对象以及如何编写模板类。\n\n[第 7 章](07.html)，*面向对象编程简介*解释了继承和组合，以及使用指向对象的指针和引用如何影响类成员的访问级别，以及它们如何影响继承的成员。 本章还通过虚方法解释了多态性，并通过抽象类解释了继承编程。\n\n[第 8 章](08.html)，*使用标准库容器*介绍了所有的 C++ 标准库容器类，以及如何将它们与迭代器和标准算法一起使用，以便您可以操作容器中的数据。\n\n[第 9 章](09.html)，*使用 Strings*描述了标准 C++ String 类的功能，包括在数字数据和字符串之间进行转换、国际化字符串以及使用正则表达式搜索和操作字符串。\n\n[第 10 章](10.html)，*诊断和调试*解释了如何准备代码以提供诊断并使其能够被调试，如何突然或正常地终止应用，以及如何使用 C++ 异常。\n\n# 这本书你需要什么？\n\n这本书涵盖了 C++ 11 标准，以及相关的 C++ 标准库。 对于本书的大部分内容，任何与 C++ 11 兼容的编译器都是合适的。 这包括来自英特尔、ibm、Sun、苹果和微软的编译器，以及开源的 GCC 编译器。\n\n本书使用 Visual C++ 2017 社区版，因为它是一个功能齐全的编译器和环境，并且可以免费下载。 这是作者的个人选择，但不应限制喜欢使用其他编译器的读者。 上一章中关于*诊断和调试*的某些部分描述了特定于 Microsoft 的功能，但这些部分有明确的标记。\n\n# 这本书是写给谁的？\n\n本书面向刚接触 C++ 的有经验的程序员。 读者应该了解高级语言的用途，以及模块化代码和控制执行流等基本概念。\n\n# 公约\n\n在本书中，您将发现许多区分不同类型信息的文本样式。 下面是这些风格的一些例子，并解释了它们的含义。\n\n文本、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄中的代码字如下所示：“我们可以通过使用`include`指令包括其他上下文。”\n\n代码块设置如下：\n\n```cpp\n    class point\n    {\n    public:\n        int x, y;\n    };\n```\n\n当我们希望您注意代码块的特定部分时，相关行或项将以粗体显示：\n\n```cpp\n    class point\n    {\n    public:\n        int x, y;\n        point(int _x, int _y) : x(_x), y(_y) {}\n    };\n```\n\n任何命令行输入或输出都如下所示：\n\n```cpp\nC:\\> cl /EHsc test.cpp\n```\n\n**新术语**和**重要单词**以粗体显示。 您在屏幕上看到的单词(例如，在菜单或对话框中)会出现在文本中，如下所示：“单击”下一步“按钮将转到下一个屏幕。”\n\nWarnings or important notes appear in a box like this. Tips and tricks appear like this.\n\n# 读者反馈\n\n欢迎读者的反馈。 让我们知道你对这本书的看法-你喜欢什么或不喜欢什么。 读者反馈对我们很重要，因为它可以帮助我们开发出真正能让您获得最大收益的图书。\n要向我们发送一般反馈，只需向`feedback@packtpub.com`发送电子邮件，并在邮件主题中提及书名。\n如果有一个您擅长的主题，并且您有兴趣撰写或投稿一本书，请参阅我们的作者指南，网址为：[www.Packtpub.com/Authors](http://www.packtpub.com/authors)。\n\n# 客户支持\n\n现在您已经成为 Packt 图书的拥有者，我们有很多东西可以帮助您从购买中获得最大价值。\n\n# 下载示例代码\n\n您可以从您的帐户[http://www.packtpub.com](http://www.packtpub.com)下载本书的示例代码文件。 如果您在其他地方购买了本书，您可以访问[http://www.packtpub.com/support](http://www.packtpub.com/support)并注册，以便将文件通过电子邮件直接发送给您。\n\n您可以通过以下步骤下载代码文件：\n\n1.  使用您的电子邮件地址和密码登录或注册我们的网站。\n2.  将鼠标指针悬停在顶部的支持选项卡上。\n3.  单击 Code Downloads&Errata(代码下载和勘误表)。\n4.  在搜索框中输入图书的名称。\n5.  选择要为其下载代码文件的图书。\n6.  从您购买本书的下拉菜单中选择。\n7.  单击 Code Download(代码下载)。\n\n下载文件后，请确保使用以下最新版本解压缩或解压缩该文件夹：\n\n*   WinRar/7-用于 Windows 的 Zip\n*   适用于 Mac 的 Zipeg/iZip/UnRarX\n*   Linux 版 7-Zip/PeaZip\n\n该书的代码包也托管在 giHub 的[https://github.com/PacktPublishing/Beginning-Cpp-Programming](https://github.com/PacktPublishing/Beginning-Cpp-Programming)上。 我们还在[https://github.com/PacktPublishing/](https://github.com/PacktPublishing/)上提供了我们丰富的图书和视频目录中的其他代码包。 看看他们！\n\n# 下载本书的彩色图片\n\n我们还为您提供了一个 PDF 文件，其中包含本书中使用的屏幕截图/图表的彩色图像。 彩色图像将帮助您更好地了解输出中的更改。 您可以从[https://www.packtpub.com/sites/default/files/downloads/BeginningCppProgramming_ColorImages.pdf](https://www.packtpub.com/sites/default/files/downloads/BeginningCppProgramming_ColorImages.pdf)下载此文件。\n\n# 错误 / 排错 / 勘误表\n\n虽然我们已经竭尽全力确保内容的准确性，但错误还是会发生。 如果您在我们的一本书中发现错误--可能是文本或代码中的错误--如果您能向我们报告，我们将不胜感激。 通过这样做，您可以将其他读者从挫折中解救出来，并帮助我们改进本书的后续版本。 如果您发现任何勘误表，请访问[http://www.packtpub.com/submit-errata](http://www.packtpub.com/submit-errata)进行报告，选择您的图书，单击勘误表提交表链接，然后输入勘误表的详细信息。 一旦您的勘误表被核实，您提交的勘误表将被接受，勘误表将被上传到我们的网站或添加到该书目勘误表部分下的任何现有勘误表列表中。\n\n要查看之前提交的勘误表，请转到[https://www.packtpub.com/books/content/support](https://www.packtpub.com/books/content/support)，并在搜索字段中输入图书名称。 所需信息将显示在勘误表部分下。\n\n# 海盗行为 / 剽窃 / 著作权侵害 / 非法翻印\n\n在互联网上盗版版权材料是所有媒体持续存在的问题。 在 Packt，我们非常重视版权和许可证的保护。 如果您在互联网上发现任何形式的非法复制我们的作品，请立即提供我们的位置、地址或网站名称，以便我们采取补救措施。\n\n请通过`copyright@packtpub.com`联系我们，并附上疑似盗版材料的链接。\n\n我们感谢您在保护我们的作者方面的帮助，以及我们为您提供有价值内容的能力。\n\n# 问题 / 不确定 / 异议 / 难题\n\n如果您对本书的任何方面有任何问题，可以通过`questions@packtpub.com`与我们联系，我们将尽最大努力解决问题。"
  },
  {
    "path": "docs/begin-cpp-prog/01.md",
    "content": "# 一、从 C++ 开始\n\n为什么是 C++？ 使用 C++ 的理由和本书的读者一样多。\n\n您选择 C++ 可能是因为您必须支持 C++ 项目。 在其生命周期的 30 年中，已经编写了数百万行 C++，大多数流行的应用和操作系统将主要用 C++ 编写，或者将使用 C++ 编写的组件和库。 几乎不可能找到一台不包含一些用 C++ 编写的代码的计算机。\n\n或者，您可能选择了 C++ 来编写新代码。 这可能是因为您的代码将使用用 C++ 编写的库，并且有数千个库可用：开放源码库、共享软件库和商业库。\n\n或者，这可能是因为您被 C++ 提供的功能和灵活性所吸引。 现代高级语言的设计目的是使程序员能够轻松地执行操作；虽然 C++ 具有这样的功能，但它也允许您尽可能地接近机器，从而赋予您直接内存访问(有时是危险的)功能。 通过类和重载等语言特性，C++ 是一种灵活的语言，允许您扩展该语言的工作方式并编写可重用的代码。\n\n无论您决定使用 C++ 的原因是什么，您都做出了正确的选择，这本书是正确的起点。\n\n# 在这一章中你会发现什么？\n\n由于这本书是一本动手手册，因此它包含您可以键入、编译和运行的代码。 要编译代码，您需要 C++ 编译器和链接器，在本书中，这指的是提供 Visual C++ 的 Visual Studio2017 社区版。 之所以选择这个编译器，是因为它是免费下载的，它符合 C++ 标准，并且它有非常广泛的工具来简化代码的编写。 Visual C++ 提供了与 C++ 11 兼容的语言功能以及几乎所有 C++ 14 和 C++ 17 的语言功能。Visual C++ 还提供了 C99 运行时库、C++ 11 标准库和 C++ 14 标准库。 所有这些提到**标准**意味着您在本书中学习编写的代码可以用所有其他标准 C++ 编译器编译。\n\n本章将从如何获取和安装 Visual Studio 2017 社区版的详细信息开始。 如果您已经有 C++ 编译器，您可以跳过这一节。 本书的大部分内容对编译器和链接器工具都是厂商中立的，但第 10 章*诊断和调试*将涵盖一些特定于 Microsoft 的功能。 Visual Studio 具有功能齐全的代码编辑器，因此即使您不使用它来管理项目，您也会发现编辑代码非常有用。\n\n在我们描述了安装之后，您将学习 C++ 的基础知识：如何构建源文件和项目，以及如何管理可能包含数千个文件的项目。\n\n最后，本章将以一个循序渐进的结构化示例结束。 在这里，您将学习如何编写使用标准 C++ 库和一种机制来管理项目中的文件的简单函数。\n\n# 什么是 C++？\n\nC++ 的前身是 C，由贝尔实验室的 Dennis Richie 设计，于 1973 年首次发布。 C 语言是一种广泛使用的语言，用于编写 Unix 和 Windows 的早期版本。 事实上，许多操作系统的库和软件开发库仍然是用 C 接口编写的。 C 是强大的，因为它可以用来编写编译成紧凑形式的代码，它使用静态类型系统(因此编译器执行类型检查工作)，并且该语言的类型和结构允许对计算机体系结构的直接内存访问。\n\n然而，C 是过程性的，基于函数，虽然它有记录类型(`struct`)来封装数据，但它没有类似对象的行为来作用于封装的状态。 显然，不仅需要 C 语言的强大功能，还需要面向对象类的灵活性和可扩展性：一种以 C 语言为基础，带有类的语言。 1983 年，Bjarne Stroustrup 发布了 C++。 ++ 来自 C 增量运算符`++ `。\n\nStrictly, when postfixed to a variable, the `++ ` operator means *increment the variable, but return the variable's value before it was incremented*. So the C statements `int c = 1; int d = c++ ;` will result in variable `d` having a value of 1 and variable `c` having a value of 2\\. This does not quite express the idea that C++ is an increment on C.\n\n# 安装 Visual C++\n\nMicrosoft 的 Visual Studio Community 2017 包含 Visual C++ 编译器、C++ 标准库和一组可用于编写和维护 C++ 项目的标准工具。 这本书不是关于如何编写 Windows 代码的，而是关于如何编写标准 C++ 以及如何使用 C++ 标准库的。 本书中的所有示例都将在命令行上运行。 之所以选择 visual Studio，是因为它是免费下载的(尽管您确实需要向 Microsoft 注册电子邮件地址)，而且它符合标准。 如果您已经安装了 C++ 编译器，则可以跳过本部分。\n\n# 设置\n\n在开始安装之前，您应该知道，作为作为 Microsoft 社区程序一部分安装 Visual Studio 的协议的一部分，您应该拥有 Microsoft 帐户。 第一次运行 Visual Studio 时，您可以选择创建一个 Microsoft 帐户，如果您跳过此阶段，您将获得 30 天的试用期。 Visual Studio 将在本月提供完整的功能，但如果您想在此之后使用 Visual Studio，您必须提供一个 Microsoft 帐户。 Microsoft 帐户不会强加给您任何义务，当您在登录后使用 Visual C++ 时，您的代码仍将保留在您的计算机上，没有义务将其传递给 Microsoft。\n\n当然，如果您在一个月内阅读了这本书，您将能够使用 Visual Studio，而无需使用您的 Microsoft 帐户登录；您可以将此视为一种激励，促使您勤奋地读完这本书！\n\n# 正在下载安装文件\n\n要下载 Visual Studio Community 2017 安装程序，请访问[https://www.visualstudio.com/vs/社区/](https://www.visualstudio.com/vs/%20community/)。\n\n当您单击 Download Community 2017 按钮时，您的浏览器将下载一个名为`vs_community__1698485341.1480883588.exe`的 1 MB 文件。 当您运行此应用时，它将允许您指定要安装的语言和库，然后它将下载并安装所有必要的组件。\n\n# 安装 Visual Studio\n\nVisual Studio 2017 将 Visual C++ 视为可选组件，因此您必须明确表示要通过自定义选项安装它。 首次执行安装程序时，您将看到以下对话框：\n\n![](img/1cdfc359-5864-4206-b198-0bd99041523a.png)\n\n当您单击“继续”按钮时，应用将设置安装程序，如下所示：\n\n![](img/a432ff33-a41f-4de9-a6f8-6cdf3e9cd6da.png)\n\n顶部有三个选项卡，分别标记为工作负载、单个组件和语言包。 确保您已经选择了 WorkLoads 选项卡(如屏幕截图所示)，并选中了名为 Desktop Development with C++ 的项目中的复选框。\n\n安装程序将检查您是否有足够的磁盘空间来安装所选选项。 Visual Studio 需要的最大空间量是 8 GB，但对于 Visual C++，您使用的空间要少得多。 当您选中 Desktop Development with C++ Item 时，您将看到对话框右侧更改为列出所选选项和所需的磁盘大小，如下所示：\n\n![](img/6eba5d15-f59e-4ab9-924a-739cfd5aa484.png)\n\n对于本书，保留安装程序选择的选项，然后单击右下角的安装按钮。 安装程序将下载所需的所有代码，并通过以下对话框让您随时了解进度：\n\n![](img/33e49bcf-ace3-4fcc-b91e-1d57b70d5c98.png)\n\n安装完成后，Visual Studio Community 2017 项将更改为有两个按钮，Modify 和 Launch，如下所示：\n\n![](img/752132af-3977-43f6-b00b-ec90f7746839.png)\n\nModify 按钮允许您添加更多组件。 单击启动以首次运行 Visual Studio。\n\n# 正在向 Microsoft 注册\n\n第一次运行 Visual Studio 时，它将通过以下对话框要求您登录到 Microsoft 服务：\n\n![](img/90fe6b32-c2f5-4468-8d32-7beb06f72678.png)\n\n您不必注册 Visual Studio，但如果您选择不注册，Visual Studio 将只工作 30 天。 向 Microsoft 注册不会给您带来任何义务。 如果您愿意注册，那么不妨现在就注册。 单击“登录”按钮提供您的 Microsoft 凭据，或者如果您没有帐户，则单击“注册”以创建帐户。\n\n当您单击启动按钮时，将打开一个新窗口，但安装程序窗口将保持打开状态。 您可能会发现安装程序窗口隐藏了欢迎窗口，因此请检查 Windows 任务栏以查看是否打开了另一个窗口。 一旦 Visual Studio 启动，您就可以关闭安装程序窗口。\n\n现在，您将能够使用 Visual Studio 编辑代码，并且在您的计算机上安装了 Visual C++ 编译器和库，因此您将能够在 Visual Studio 中或在命令行上编译 C++ 代码。\n\n# 检查 C++ 项目\n\nC++ 项目可以包含数千个文件，管理这些文件可能是一项任务。 在生成项目时，是否应该编译文件？如果是，使用哪种工具？ 文件应该按什么顺序编译？ 这些编译器将产生什么输出？ 编译后的文件应该如何组合才能生成可执行文件？\n\n编译器工具还将有大量选项，包括调试信息、优化类型、对不同语言功能的支持以及处理器功能。 编译器选项的不同组合将在不同的情况下使用(例如，发布版本和调试版本)。 如果从命令行编译，则必须确保选择正确的选项，并在编译的所有源代码中一致地应用这些选项。\n\n管理文件和编译器选项可能会变得非常复杂。 这就是为什么对于生产代码，您应该使用 make 工具。 有两个是随 Visual Studio 一起安装的：**MSBuild**和**nmake**。 在 Visual Studio 环境中生成 Visual C++ 项目时，将使用 MSBuild，编译规则将存储在 XML 文件中。 您还可以在命令行上调用 MSBuild，将 XML 项目文件传递给它。 Nmake 工具是 Microsoft 版本的程序维护实用程序，在许多编译器中通用。 在本章中，您将学习如何编写一个简单的**Makefile**，以便与 nmake 实用程序一起使用。\n\n在介绍项目管理的基础知识之前，我们首先要检查 C++ 项目中常见的文件，以及编译器将对这些文件做什么。\n\n# 编译器\n\nC++ 是一种高级语言，旨在为您提供丰富的语言工具，便于您和其他开发人员阅读。 计算机的处理器执行低级代码，编译器的目的是将 C++ 转换为处理器的机器码。 单个编译器可能能够面向几种类型的处理器，如果代码是标准 C++，则可以使用支持其他处理器的其他编译器进行编译。\n\n然而，编译器所做的远不止这些。 正如在[第 4 章](04.html)，*使用内存、数组和指针*中所解释的，C++ 允许您将代码拆分成函数，这些函数接受参数并返回值，因此编译器设置用于传递此数据的内存。 此外，函数可以声明仅在该函数内使用的变量([第 5 章](05.html)，*使用函数*，将提供更多详细信息)，并且仅在函数执行时存在。 编译器设置这个内存，称为**堆栈帧**。 您有关于如何创建堆栈帧的编译器选项；例如，Microsoft 编译器选项`/Gd`、`/Gr`和`/Gz`确定将函数参数压入堆栈的顺序，以及调用方函数或被调用函数是否在调用结束时从堆栈中删除参数。 当您编写要共享的代码时，这些选项非常重要(但出于本书的目的，应该使用默认的堆栈结构)。 这只是一个方面，但它应该会给您留下深刻的印象，即编译器设置为您提供了大量的功能和灵活性。\n\n编译器编译 C++ 代码，如果在代码中遇到错误，它将发出编译器错误。 这是代码的语法检查。 需要指出的是，从语法的角度来看，您编写的代码可以是完美的 C++ 代码，但也可能是无稽之谈。 编译器的语法检查是对代码的一项重要检查，但您应该始终使用其他检查。 例如，下面的代码声明一个整数变量并为其赋值：\n\n```cpp\n    int i = 1 / 0;\n```\n\n编译器将发出错误`C2124 : divide or mod by zero`。 但是，下面的代码将使用另一个变量执行相同的操作，该变量在逻辑上是相同的，但编译器不会发出错误：\n\n```cpp\n    int j = 0; \n    int i = 1 / j;\n```\n\n当编译器发出错误时，它将停止编译。 这意味着两件事。 首先，您得不到编译后的输出，因此错误不会进入可执行文件。 其次，这意味着，如果源代码中还有其他错误，只有在修复了当前错误并重新编译之后才能知道。 如果要执行语法检查并将编译留待以后进行，请使用`/Zs`开关。\n\n编译器还将生成警告消息。 警告意味着代码将进行编译，但代码中可能存在影响可执行文件运行方式的问题。 Microsoft 编译器定义了四个级别的警告：级别 1 是最严重的(应该解决)，级别 4 是信息性的。\n\n警告通常用于指示正在编译的语言功能可用，但它需要开发人员尚未使用的特定编译器选项。 在代码开发过程中，您通常会忽略警告，因为您可能要测试语言功能。 但是，当您接近生成生产代码时，您应该更多地关注警告。 默认情况下，Microsoft 编译器将显示 1 级警告，您可以使用带数字的`/W`选项来指示您希望看到的级别(例如，`/W2`表示您希望看到 2 级警告和 1 级警告)。 在生产代码中，您可以使用`/Wx`选项，该选项告诉编译器将警告视为错误，以便您必须修复问题才能编译代码。 您还可以使用`pragmas`命令编译器(`pragmas`将在后面解释)和编译器选项来隐藏特定警告。\n\n# 链接代码\n\n编译器将产生输出。 对于 C++ 代码，这将是目标代码，但您可能有其他编译器输出，如编译的资源文件。 这些文件本身无法执行；尤其是因为操作系统需要设置某些结构。 C++ 项目总是分为两个阶段：将代码编译成一个或多个目标文件，然后将目标文件链接到可执行文件。 这意味着您的 C++ 编译器将提供另一个称为链接器的工具。\n\n链接器还具有确定其工作方式以及指定其输出和输入的选项，并且还将发出错误和警告。 与编译器一样，Microsoft 链接器有一个选项`/WX`，可以将警告视为发布版本中的错误。\n\n# 源文件\n\n在最基本的级别上，C++ 项目将只包含一个文件：C++ 源文件，通常扩展名为`cpp`或`cxx`。\n\n# 一个简单的例子\n\n最简单的 C++ 程序如下所示：\n\n```cpp\n    #include <iostream> \n\n    // The entry point of the program \n    int main() \n    { \n        std::cout << \"Hello, world!n\"; \n    }\n```\n\n要说明的第一点是，以`//`开头的行是注释。 编译器将忽略行尾之前的所有文本。 如果希望有多行注释，则每行必须以`//`开头。 您也可以使用 C 注释。 C 注释以`/*`开头，以`*/`结束，这两个符号之间的所有内容都是注释，包括换行符。\n\nC 注释是注释掉部分代码的快捷方法。\n\n大括号`{}`表示代码块；在本例中，C++ 代码用于函数`main`。 我们知道这是一个函数，因为它的基本格式是：首先是返回值的类型，然后是带一对圆括号的函数名，它用于声明传递给函数的参数(及其类型)。 在本例中，该函数名为`main`，圆括号为空，表示该函数没有参数。 函数名(`int`)之前的标识符表示该函数将返回一个整数。\n\nC++ 的惯例是，名为`main`的函数是可执行文件的**入口点**，也就是说，当您从命令行调用可执行文件时，这将是代码中将被调用的第一个函数。\n\nThis simple example function immediately immerses you into an aspect of C++ that irritates programmers of other languages: the language may have rules, but the rules don't always appear to be followed. In this case, the `main` function is declared to return an integer, but the code returns no value. The rule in C++ is that, if the function declares that it returns a value, then it must return a value. However, there is a single exception to this rule: if the `main` function does not return a value, then a value of `0` will be assumed. C++ contains many quirks such as this, but you will soon learn what they are and get used to them.\n\n`main`函数只有一行代码；这是一个以`std`开头、以分号(`;`)结尾的语句。 C++ 可以灵活地使用空格(空格、制表符和换行符)，这将在下一章中解释。 但是，重要的是要注意，您必须小心使用文字字符串(就像这里使用的那样)，并且每个语句都用分号分隔。 忘记必需的分号是编译器错误的常见来源。 额外的分号只是一条空语句，因此对于初学者来说，分号太多对代码的致命影响可能比分号太少要小。\n\n这条语句将字符串`Hello, world!`(和换行符)打印到控制台。 您知道这是一个字符串，因为它用双引号(`″″`)括起来。 使用运算符`<<`将字符串*放入*流对象`std::cout`。 名称的`std`部分是一个**名称空间**，实际上是具有类似目的的代码集合，或者来自单一供应商的代码集合。 在本例中，`std`表示`cout`流对象是标准 C++ 库的一部分。 双冒号`::`是**作用域解析**操作符，表示您希望访问在`std`名称空间中声明的`cout`对象。 您可以定义自己的命名空间，在大型项目中应该定义自己的命名空间，因为这将允许您使用可能已在其他命名空间中声明的名称，并且此语法允许您消除符号的歧义。\n\n`cout`对象是`ostream`类的实例，在调用`main`函数之前已经为您创建了该实例。 `<<`表示调用名为`operator <<`的函数并向其传递字符串(它是一个由`char`个字符组成的数组)。 此函数将字符串中的每个字符打印到控制台，直到它达到`NUL`字符。\n这是 C++ 灵活性的一个例子，该特性称为**运算符重载**。 `<<`运算符通常与整数一起使用，也用于将整数中的位左移指定的位数；`x << y`将返回一个`x`中的每一位都左移了`y`位的值，实际上是返回一个乘以 2<sup>y</sup>的值。 然而，在前面的代码中，代替整数`x`的是流对象`std::cout`，代替左移索引的是字符串。 显然，这在`<<`操作符的 C++ 定义中没有任何意义。 当与左侧的`ostream`对象一起使用时，C++ 标准有效地重新定义了`<<`运算符的含义。 此外，此代码中的`<<`操作符将向控制台打印一个字符串，因此它在右侧获取一个字符串。 C++ 标准库定义了允许将其他数据类型打印到控制台的其他`<<`操作符。 它们的调用方式都相同；编译器根据使用的参数类型确定编译哪个函数。\n前面我们说过，已经将`std::cout`对象创建为`ostream`类的实例，但是没有说明这是如何发生的。 这将我们带到简单源文件的最后一部分，还没有解释：以`#include`开头的第一行。 这里的`#`有效地指示将向编译器提供某种类型的消息。 您可以发送各种类型的消息(有几种消息是`#define`、`#ifdef`、`#pragma`，我们将在本书的其他地方介绍这些消息)。 在本例中，`#include`告诉编译器在此时将指定文件的内容复制到源文件中，这实质上意味着该文件的内容也将被编译。 指定的文件称为**头文件**，在文件管理和通过库重用代码方面非常重要。\n\n文件`<iostream>`(注意，没有扩展名)是标准库的一部分，可以在 C++ 编译器提供的**include 目录**中找到。 尖括号(`<>`)表示编译器应该查找用于存储头文件的标准目录，但您可以使用双引号(`″″`)提供头文件的绝对位置(或相对于当前文件的位置)。 C++ 标准库使用不使用文件扩展名的约定。 在命名您自己的头文件时，应该使用扩展名`h`(或`hpp`，很少使用`hxx`)。 C 运行时库(C++ 代码也可以使用该库)也对其头文件使用扩展名`h`。\n\n# 正在创建源文件\n\n首先，在开始菜单上找到 Visual Studio 2017 文件夹，然后单击 VS2017 的 Developer Command Prompt 条目。 这将启动 Windows 命令提示符，并将环境变量设置为使用 Visual C++ 2017。 但是，非常无益的是，它还会将命令行保留在 Program Files 文件夹下的 Visual Studio 文件夹中。 如果您打算进行任何开发，您会希望从这个文件夹移到一个创建和删除文件不会有任何危害的文件夹。 在执行此操作之前，请移动到 Visual C++ 文件夹并列出文件：\n\n```cpp\nC:\\Program Files\\Microsoft Visual Studio\\2017\\Community>cd %VCToolsInstallDir%\nC:\\Program Files\\Microsoft Visual Studio\\2017\\Community\\VC\\Tools\\MSVC\\14.0.10.2517>dir\n```\n\n由于安装程序会将 C++ 文件放在包含编译器当前版本的文件夹中，因此使用环境变量`VCToolsInstallDir`比指定特定版本以使用最新版本(在本例中为 14.0.10.2517)更安全。\n有几件事需要注意。 首先，文件夹`bin`、`include`和`lib`：\n\n| **文件夹** | **说明** |\n| `bin` | 它间接包含 Visual C++ 的可执行文件。 `bin`文件夹将包含您正在使用的 CPU 类型的单独文件夹，因此您必须导航到该文件夹下方才能找到包含可执行文件的实际文件夹。 两个主要的可执行文件是`cl.exe`(它是 C++ 编译器)和`link.exe`(它是链接器)。 |\n| `include` | 此文件夹包含 C 运行时库和 C++ 标准库的头文件。 |\n| `lib` | 此文件夹包含 C 运行时库和 C++ 标准库的静态链接库文件。同样，CPU 类型将有单独的文件夹 |\n\n我们将在本章后面部分回顾这些文件夹。\n\n需要指出的另一件事是`VC\\Auxillary\\Build`文件夹下的文件`vcvarsall.bat`。 当您在开始菜单上单击 VS2017 的开发者命令提示符时，将运行此批处理文件。 如果您希望使用现有的命令提示符编译 C++ 代码，可以通过运行此批处理文件进行设置。 此批处理文件的三个最重要的操作是设置`PATH`环境变量以包含 bin 文件夹的路径，并设置`INCLUDE`和`LIB`环境变量分别指向 Include 和 lib 文件夹。\n\n现在导航到根目录并创建一个新文件夹`Beginning_C++ `，然后移动到该目录。 接下来，为本章创建一个名为`Chapter_01`的文件夹。 现在您可以切换到 Visual Studio；如果该程序尚未运行，请从“开始”菜单启动它。\n\n在 Visual Studio 中，单击文件菜单，然后单击新建，然后单击文件...。 菜单项以获得 New File 对话框，并在左侧的树状视图中，单击 Visual C++ 选项。 在中间面板中，您将看到两个选项：C++ 文件(.cpp)和头文件(.h)，以及`Open`文件夹的 C++ 属性，如以下屏幕截图所示：\n\n![](img/092029d9-7d82-4c20-88b6-2f29c506aa3a.png)\n\n前两种文件类型用于 C++ 项目，第三种类型创建一个 JSON 文件来帮助 Visual Studio IntelliSence(键入时帮助)，本书中不会使用。\n单击第一个选项，然后单击打开按钮。 这将创建一个名为 Source1.cpp 的新空文件，因此在单击 File 菜单，然后将 Source1.cpp 另存为，并导航到项目文件夹时，在单击 Save 按钮之前，将其作为 simple.cpp 保存到章节项目文件夹中，将 File name 框中的名称更改为 simple.cpp。\n\n现在您可以输入简单程序的代码，如下所示：\n\n```cpp\n    #include <iostream> \n\n    int main() \n    { \n        std::cout << \"Hello, world!n\"; \n    }\n```\n\n当您完成此代码的键入后，通过单击文件菜单，然后单击菜单中的保存 simple.cpp 选项来保存文件。 现在就可以编译代码了。\n\n# 编译代码\n\n转到命令提示符并键入`**cl /?**`命令。 由于`PATH`环境设置为包含到`bin`文件夹的路径，因此您将看到编译器的帮助页面。 您可以通过按 Return 键滚动浏览这些页面，直到返回到命令提示符。 这些选项中的大多数都超出了本书的范围，但下表显示了我们将讨论的一些选项：\n\n| **编译器开关** | **说明** |\n| `/c` | 仅编译，不链接。 |\n| `/D<symbol>` | 定义常量或宏<symbol>。</symbol> |\n| `/EHsc` | 启用 C++ 异常处理，但指示不处理来自`extern ″C″`函数(通常是操作系统函数)的异常。 |\n| `/Fe:<file>` | 提供要链接到的可执行文件的名称。 |\n| `/Fo:<file>` | 提供要编译到的目标文件的名称。 |\n| `/I <folder>` | 提供用于搜索包含文件的文件夹的名称。 |\n| `/link<linker options>` | 将<linker options=\"\">传递给链接器。 这必须在源文件名和用于编译器的任何开关之后。</linker> |\n| `/Tp <file>` | 将<file>编译为 C++ 文件，即使它的文件扩展名没有`.cpp`或`.cxx`。</file> |\n| `/U<symbol>` | 删除先前定义的<symbol>宏或常量。</symbol> |\n| `/Zi` | 启用调试信息。 |\n| `/Zs` | 仅限语法，不编译或链接。 |\n\n请注意，有些选项在开关和选项之间需要空格，有些选项不能有空格，而对于其他选项，空格是可选的。 通常，如果您有包含空格的文件或文件夹的名称，则应该用双引号将名称括起来。 在使用开关之前，最好查阅帮助文件，了解它是如何使用空格的。\n\n在命令行中，键入`**cl simple.cpp**`命令。 您会发现编译器将发出警告`**C4530**`和`**C4577**`。 原因是 C++ 标准库使用异常，而您没有指定编译器应该为异常提供必要的支持代码。 使用`/EHsc`开关可以很容易地克服这些警告。 在命令行中，键入`cl /EHsc simple.cpp`命令。 如果您正确地键入了代码，它应该会编译：\n\n```cpp\nC:\\Beginning_C++ \\Chapter_01>cl /EHsc simple.cpp\nMicrosoft (R) C/C++ Optimizing Compiler Version 19.00.25017 for x86\nCopyright (C) Microsoft Corporation.  All rights reserved\n\nsimple.cpp\n\nMicrosoft (R) Incremental Linker Version 14.10.25017.0\nCopyright (C) Microsoft Corporation.  All rights reserved.\n/out:simple.exe\n\nsimple.obj\n```\n\n默认情况下，编译器会将该文件编译为目标文件，然后将该文件传递给链接器以链接为命令行可执行文件，该命令行可执行文件与 C++ 文件同名，但扩展名为：`.exe`。 表示`/out:simple.exe`的行是由链接器生成的，而`/out`是链接器选项。\n\n列出文件夹的内容。 您将发现三个文件：`simple.cpp`，源文件；simple.obj，编译器的输出；以及`simple.exe`，链接器将目标文件与适当的运行时库链接后的输出。 现在，您可以通过在命令行中键入`simple`来运行可执行文件：\n\n```cpp\nC:\\Beginning_C++ \\Chapter_01>simple\nHello, World!\n```\n\n# 在命令行和可执行文件之间传递参数\n\n前面，您发现`main`函数返回一个值，默认情况下该值为零。 当应用完成时，您可以将错误代码返回到命令行；这样您就可以在批处理文件和脚本中使用可执行文件，并使用该值来控制脚本中的流。 同样，当您运行可执行文件时，您可以从命令行传递参数，这将影响可执行文件的行为方式。\n\n通过在命令行中键入`**simple**`命令来运行简单的应用。 在 Windows 中，错误码是通过伪环境变量`ERRORLEVEL`获取的，因此可以通过`**ECHO**`命令获取此值：\n\n```cpp\nC:\\Beginning_C++ \\Chapter_01>simple\nHello, World!\n\nC:\\Beginning_C++ \\Chapter_01>ECHO %ERRORLEVEL%\n0\n```\n\n要显示该值是由应用返回的，请更改`main`函数以返回非 0 的值(在本例中为 99，突出显示)：\n\n```cpp\n    int main() \n    { \n        std::cout << \"Hello, world!n\"; \n return 99; \n    }\n```\n\n编译并运行此代码，然后打印出前面所示的错误代码。 您会发现错误代码现在显示为 99。\n\n这是一种非常基本的通信机制：它只允许传递整数值，调用代码的脚本必须知道每个值的含义。 您更有可能将参数传递给应用，这些参数将通过`main`函数的参数通过您的代码传递。 将`main`函数替换为以下内容：\n\n```cpp\n        int main(int argc, char *argv[]) \n        { \n            std::cout << \"there are \" << argc << \" parameters\" <<  \n            std::endl; \n            for (int i = 0; i < argc; ++ i) \n            { \n                std::cout << argv[i] << std::endl; \n            } \n        }\n```\n\n当您编写`main`函数从命令行获取参数时，惯例是它具有这两个参数。\n\n第一个参数通常称为`argc`。 它是一个整数，表示传递给应用的参数数量。 *此参数非常重要。* 原因是您将要通过数组访问内存，而此参数给出了您的访问限制。 如果您访问超过此限制的内存，您将会遇到问题：最好的情况是访问未初始化的内存，但在最坏的情况下，您可能会导致访问冲突。\n\n重要的是，无论何时访问内存，您都要了解要访问的内存量，并将其保持在其限制范围内。\n\n第二个参数通常称为`argv`，是指向内存中 C 字符串的指针数组。 您将在[第 4 章](04.html)，*使用内存、数组和指针*中了解更多关于数组和指针的内容，在[第 9 章](09.html)，*中使用 Strings*了解更多关于字符串的内容，因此我们不在这里进行详细讨论。 方括号(`[]`)表示参数是一个数组，数组的每个成员的类型由`char *`给出。 `*`表示每一项都是指向内存的指针。 通常，这将被解释为指向给定类型的单个项的指针，但字符串不同：`char *`表示在内存中，指针将有零个或多个字符后跟`NUL`字符()。 字符串的长度是直到`NUL`字符的字符数。\n此处显示的第三行将传递给应用的字符串数打印到控制台。 在本例中，我们不使用换行符(`n`)来添加换行符，而是使用流：`std::endl`。 您可以使用几个操纵器，这些操纵器将在第 6 章*类*中讨论。 `std::endl`操纵器将把换行符放入输出流中，然后它将刷新流。 该行显示 C++ 允许您将`<<`PUT 操作符的使用链接到一个流中。 该行还显示`<<`PUT 操作符是重载的，也就是说，不同参数类型有不同版本的操作符(在本例中，有三个版本：一个接受整数，用于`argv`，一个接受字符串参数，另一个接受操纵器作为参数)，但调用这些操作符的语法完全相同。\n\n最后，有一个代码块用于打印`argv`数组中的每个字符串，如下所示：\n\n```cpp\n    for (int i = 0; i < argc; ++ i) \n    { \n        std::cout << argv[i] << std::endl; \n    }\n```\n\n`for`语句意味着将调用代码块，直到变量`i`小于`argc`的值，并且在每次循环成功迭代之后，变量`i`递增(使用前缀增量运算符`++ `)。 数组中的项通过方括号语法访问(`[]`)。 传递的值是数组中的*索引*。\n\n请注意，变量`i`的起始值为`0`，因此访问的第一个项是`argv[0]`，由于当变量`i`的值为`argc`时，`for`循环结束，这意味着访问的数组中的最后一个项是`argv[argc-1]`。 这是数组的典型用法：第一个索引为零，如果数组中有`n`个项目，则最后一个项目的索引为`n-1`。\n\n像以前一样编译并运行此代码，不带参数：\n\n```cpp\nC:\\Beginning_C++ \\Chapter_01>simple\nthere are 1 parameters\nsimple\n```\n\n请注意，虽然您没有提供参数，但程序认为有一个参数：程序可执行文件的名称。 事实上，这不仅仅是名称，它还是用于调用可执行文件的命令。 在本例中，您键入了`**simple**`命令(不带扩展名)，并将文件`simple`的值作为参数打印在控制台上。 重试此操作，但这一次使用其全名`simple.exe`调用该程序。 现在您会发现第一个参数是`simple.exe`。\n尝试使用一些实际参数调用代码。 在命令行中键入以下命令：\n\n```cpp\nC:\\Beginning_C++ \\Chapter_01>simple test parameters\nthere are 3 parameters\nsimple\ntest parameters\n```\n\n这一次，程序说有三个参数，并且使用空格字符对它们进行了分隔。 如果要在单个参数中使用空格，则应将整个字符串放在双引号中：\n\n```cpp\nC:\\Beginning_C++ \\Chapter_01>simple ″test parameters″\nthere are 2 parameters\nsimple\ntest parameters\n```\n\n请记住，`argv`是一个字符串指针数组，因此，如果您希望从命令行传入数值类型，并且希望在程序中将其用作数字，则必须从通过`argv`访问的字符串表示形式进行转换。\n\n# 预处理器和符号\n\nC++ 编译器需要几个步骤来编译源文件。 顾名思义，编译器预处理器是这个过程的开始。 预处理器定位头文件并将其插入源文件。 它还替换宏和定义的常量。\n\n# 定义常量\n\n通过预处理器定义常量主要有两种方式：通过编译器开关和在代码中定义。 要了解这是如何工作的，让我们更改`main`函数以打印出常量的值；下面突出显示两行重要内容：\n\n```cpp\n    #include <iostream>  \n #define NUMBER 4 \n\n    int main() \n    { \n std::cout << NUMBER << std::endl; \n    }\n```\n\n以`#define`开头的行是给预处理器的指令，它说明，无论文本中有确切的符号`NUMBER`，都应该替换为 4。这是一个文本搜索和替换，但它只替换整个符号(因此，如果文件中有一个名为`NUMBER99`的符号，则不会替换`NUMBER`部分)。 预处理器完成其工作后，编译器将看到以下内容：\n\n```cpp\n    int main() \n    { \n std::cout << 4 << std::endl; \n    }\n```\n\n编译并运行原始代码，并确认程序只是将 4 打印到控制台。\n\n预处理器的文本搜索和替换方面可能会导致一些奇怪的结果，例如，将`main`函数更改为声明一个名为`NUMBER`的变量，如下所示：\n\n```cpp\n    int main() \n    { \n int NUMBER = 99; \n        std::cout << NUMBER << std::endl; \n    }\n```\n\n现在编译代码。 您将从编译器收到一个错误：\n\n```cpp\nC:\\Beginning_C++ \\Chapter_01>cl /EHhc simple.cpp\nMicrosoft (R) C/C++ Optimizing Compiler Version 19.00.25017 for x86\nCopyright (C) Microsoft Corporation.  All rights reserved.\n\nsimple.cpp\nsimple.cpp(7): error C2143: syntax error: missing ';' before 'constant'\nsimple.cpp(7): error C2106: '=': left operand must be l-value\n```\n\n这表明第 7 行有错误，这是声明变量的新行。 但是，由于预处理器执行的搜索和替换，编译器看到的是如下所示的行：\n\n```cpp\n    int 4 = 99;\n```\n\n这不是正确的 C++！\n在您键入的代码中，很明显是什么导致了问题，因为您对同一文件中的符号有`#define`指令。 实际上，您将包括几个头文件，这些头文件可能包括文件本身，因此错误的`#define`指令可能位于多个文件中的一个文件中。 同样，您的常量符号可能与`#define`指令后包含的头文件中的变量同名，并可能被预处理器替换。\n\n使用`#define`作为定义全局常量的方法通常不是一个好主意，在 C++ 中还有更好的方法，正如您将在[第 3 章](03.html)，*探索 C++ 类型*中看到的那样。\n\n如果您认为问题来自于预处理器替换符号，可以在预处理器完成工作后查看传递给编译器的源文件来调查这一问题。 为此，请使用`/EP`开关进行编译。 这将取消实际编译，并将预处理器的输出发送到`stdout`(命令行)。 请注意，这可能会产生大量文本，因此通常更好的做法是将此输出定向到一个文件，然后使用 Visual Studio 编辑器检查该文件。\n\n提供预处理器使用的值的另一种方法是通过编译器开关传递它们。 编辑代码并删除以`#define`开头的行。 正常编译这段代码(`**cl /EHsc simple.cpp**`)，运行它，并确认控制台上打印的数字是 99，这是分配给变量的值。 现在使用下面的代码行重新编译代码：\n\n```cpp\ncl /EHsc simple.cpp /DNUMBER=4\n```\n\n请注意，`/D`开关和符号名称之间没有空格。 这会告诉预处理器用文本`4`替换每个`NUMBER`符号，这会导致与上面相同的错误，表明预处理器正在尝试用提供的值替换符号。\n\nVisual C++ 和 nmake 项目等工具将具有通过 C++ 编译器定义符号的机制。 `/D`开关用于仅定义一个符号，如果您想定义其他符号，它们将有自己的`/D`开关。\n\n您现在会感到奇怪，为什么 C++ 有这样一个奇怪的工具，它似乎只会导致令人困惑的错误。 一旦了解了预处理器正在做什么，定义符号就会变得非常强大。\n\n# 使用宏\n\n预处理器符号的一个有用功能是**宏**。 宏具有参数，预处理器将确保搜索和替换将宏中的符号替换为用作宏的参数的符号。\n\n编辑`main`函数，如下所示：\n\n```cpp\n    #include <iostream> \n\n    #define MESSAGE(c, v)  \n    for(int i = 1; i < c; ++ i) std::cout << v[i] << std::endl; \n\n    int main(int argc, char *argv[]) \n    { \n        MESSAGE(argc, argv); \n        std::cout << \"invoked with \" << argv[0] << std::endl; \n    }\n```\n\n`main`函数调用名为`MESSAGE`的宏并将命令行参数传递给它。 然后，该函数将第一个命令行参数(调用命令)打印到控制台。 `MESSAGE`不是一个函数，它是一个宏，这意味着预处理器将用前面定义的文本中的两个参数替换出现的每个`MESSAGE`，用作为宏的第一个参数传递的任何参数替换`c`参数，用作为第二个参数的任何参数替换`v`。 预处理器处理完文件后，`main`将如下所示：\n\n```cpp\n    int main(int argc, char *argv[]) \n    { \n        for(int i = 1; i < argc; ++ i)  \n            std::cout << argv[i] << std::endl; \n        std::cout << \"invoked with \" << argv[0] << std::endl; \n    }\n```\n\n请注意，在宏定义中，反斜杠()用作行续行符，因此您可以使用多行宏。 使用一个或多个参数编译并运行此代码，并确认`MESSAGE`打印出命令行参数。\n\n# 使用符号\n\n您可以在没有值的情况下定义符号，并且可以告诉预处理器测试是否定义了符号。 最明显的情况是为调试版本编译与发布版本不同的代码。\n\n编辑代码以添加此处突出显示的行：\n\n```cpp\n #ifdef DEBUG \n    #define MESSAGE(c, v)  \n    for(int i = 1; i < c; ++ i) std::cout << v[i] << std::endl; \n #else #define MESSAGE #endif\n```\n\n第一行告诉预处理器查找`DEBUG`符号。 如果定义了此符号(无论其值如何)，则将使用`MESSAGE`宏的第一个定义。 如果未定义符号(发布版本)，则定义了`MESSAGE`符号，但它什么也不做：实质上，将从代码中删除出现的带有两个参数的`MESSAGE`。\n\n编译此代码并使用一个或多个参数运行程序。 例如：\n\n```cpp\nC:\\Beginning_C++ \\Chapter_01>simple test parameters\ninvoked with simple\n```\n\n这表明代码编译时没有定义`DEBUG`，因此将`MESSAGE`定义为不执行任何操作。 现在再次编译这段代码，但这次使用/DDEBUG 开关来定义`DEBUG`符号。 再次运行该程序，您将看到控制台上打印了命令行参数：\n\n```cpp\nC:\\Beginning_C++ \\Chapter_01>simple test parameters\ntest parameters \ninvoked with simple\n```\n\n这段代码使用了宏，但您可以在 C++ 代码中的任何位置对符号使用条件编译。 以这种方式使用的符号允许您编写灵活的代码，并通过编译器命令行上定义的符号选择要编译的代码。 此外，编译器将自己定义一些符号，例如，`__DATE__`将具有当前日期，`__TIME__`将具有当前时间，而`__FILE__`将具有当前文件名。\n\nMicrosoft, and other compiler producers, defines a long list of symbols that you can access, and you are advised to look these up in the manual. A few that you may find useful are as follows: `__cplusplus` will be defined for C++ source files (but not for C files) so you can identify code that needs a C++ compiler; `_DEBUG` is set for debug builds (note the preceding underscore), and `_MSC_VER` has the current version of the Visual C++ compiler, so you can use the same source for various versions of the compiler.\n\n# 使用语用\n\n与符号和条件编译相关的是编译器指令`#pragma once`。 Pragma 是特定于编译器的指令，不同的编译器将支持不同的编译指示。 Visual C++ 定义了`#pragma once`来解决当您有多个头文件(每个头文件都包括相似的头文件)时出现的问题。 问题是，这可能会导致相同的项被多次定义，编译器会将其标记为错误。 有两种方法可以做到这一点，您接下来包含的`<iostream>`头文件将使用这两种技术。 您可以在 Visual C++ `include`文件夹中找到此文件。 在该文件的顶部，您可以找到以下内容：\n\n```cpp\n    // ostream standard header \n    #pragma once \n    #ifndef _IOSTREAM_ \n    #define _IOSTREAM_\n```\n\n在底部，您将看到以下行：\n\n```cpp\n    #endif /* _IOSTREAM_ */\n```\n\n首先是条件编译：第一次包含这个头文件时，将不会定义符号`_IOSTREAM_`，因此将定义该符号，然后将包括文件的其余部分，直到`#endif`行。\n\n这说明了使用条件编译时的良好实践。 对于每个`#ifndef`，必须有一个`#endif`，并且它们之间通常可能有数百行。 当您使用`#ifdef`或`#ifundef`时，最好使用相应的`#else`和`#endif`来提供注释，以指示它所引用的符号。\n\n如果再次包含该文件，则将定义符号`_IOSTREAM_`，因此将忽略`#ifndef`和`#endif`之间的代码。 但是，必须指出的是，即使定义了符号，头文件仍将被加载和处理，因为有关如何操作的指令包含在文件中。\n\n`#pragma once`执行与条件编译相同的操作，但它绕过了使用可能重复的符号的问题。 如果您将这一行添加到头文件的顶部，您就是在指示预处理器加载并处理此文件一次。 预处理器维护它已处理的文件的列表，如果后续标头尝试加载已处理的文件，则不会加载该文件，也不会处理该文件。 这减少了项目预处理所需的时间。\n在关闭`<iostream>`文件之前，请查看文件中的行数。 对于`<iostream>`版本 v6.50：0009，有 55 行。 这是一个小文件，但它包括`<istream>`(1,157 行)，其中包括`<ostream>`(1,036 行)，其中包括`<ios>`(374 行)，其中包括`<xlocnum>`(1,630 行)，依此类推。 预处理的结果可能意味着源文件中将包含数万行代码，即使对于只有一行代码的程序也是如此！\n\n# 依附者 / 附属国 / 附属地区 / 从属物\n\nC++ 项目将生成一个可执行文件或库，这将由链接器从目标文件构建。 可执行文件或库依赖于这些对象文件。 目标文件将从 C++ 源文件(以及可能的一个或多个头文件)编译而来。 目标文件依赖于这些 C++ 源文件和头文件。 了解依赖项很重要，因为它有助于您了解编译项目中文件的顺序，并且允许您通过只编译那些已更改的文件来更快地构建项目。\n\n# 图书馆 / 图书馆的藏书 / 资料室 / 文库\n\n当您在源文件中包含一个文件时，您的代码将可以访问该头文件中的代码。 您的包含文件可能包含整个函数或类定义(这些将在后面的章节中介绍)，但这将导致前面提到的问题：一个函数或类的多个定义。 相反，您可以声明一个类或**函数原型**，它指示调用代码将如何调用函数，而不实际*定义*它。 显然，代码必须在其他地方定义，这可以是源文件或库，但是编译器会很高兴，因为它只看到一个定义。\n\n库是已经定义的代码；它已经过充分的调试和测试，因此，用户不需要访问源代码。 C++ 标准库主要通过头文件共享，这有助于您调试代码，但您必须抵制编辑这些文件的诱惑。 其他库将作为编译后的库提供。\n\n基本上有两种编译的库：静态库和动态链接库。 如果使用静态库，则编译器将从静态库复制您使用的编译代码，并将其放入可执行文件中。 如果您使用动态链接(或共享)库，则链接器将添加在运行时(可能是在加载可执行文件时，甚至可能延迟到调用函数)期间使用的信息，以将共享库加载到内存中并访问函数。\n\nWindows uses the extension `lib` for static libraries and `dll` for dynamic link libraries. GNU **gcc** uses the extension `a` for static libraries and `so` for shared libraries.\n\n如果在静态或动态链接库中使用库代码，编译器将需要知道您正在正确调用函数-以确保您的代码使用正确的参数数量和正确的类型调用函数。 这就是函数原型的目的：它向编译器提供它需要知道的有关调用函数的信息，而不提供函数的实际主体，即函数定义。\n\n本书不会详细介绍如何编写库，因为它是特定于编译器的；也不会深入到调用库代码的细节，因为不同的操作系统有不同的代码共享方式。 通常，C++ 标准库将通过标准头文件包含在您的代码中。 C 运行时库(它为 C++ 标准库提供了一些代码)将是静态链接的，但是如果编译器提供动态链接版本，您将有一个编译器选项来使用它。\n\n# 预编译头\n\n当您将文件包含到源文件中时，预处理器将包含该文件的内容(在考虑了任何条件编译指令之后)，并递归地包含该文件包含的任何文件。 如前所述，这可能会导致数千行代码。 在开发代码时，通常会编译项目，以便测试代码。 每次编译代码时，也会编译头文件中定义的代码，即使库头文件中的代码没有更改。 对于大型项目，这可能会使编译花费很长时间。\n\n为了解决此问题，编译器通常提供预编译头文件的选项，这些头文件不会更改。 创建和使用预编译头是特定于编译器的。 例如，使用 GNU C++ 编译器 GCC，您可以将头文件编译为 C++ 源文件(使用`/x`开关)，编译器将创建一个扩展名为`gch`的文件。 当 GCC 编译使用头文件的源文件时，它将搜索`gch`文件，如果找到预编译头文件，它将使用该文件；否则，它将使用头文件。\n在 Visual C++ 中，这个过程稍微复杂一些，因为您必须明确地告诉编译器在编译源文件时查找预编译头文件。 Visual C++ 项目中的约定是有一个名为`stdafx.cpp`的源文件，它只有一行包含文件`stdafx.h`。 您将所有稳定的头文件包含放在`stdafx.h`中。 接下来，通过使用`/Yc`编译器选项编译`stdafx.cpp`来创建预编译头，以指定`stdafx.h`包含要编译的稳定头。 这将创建一个`pch`文件(通常，Visual C++ 将以您的项目命名它)，其中包含编译到包含`stdafx.h`头文件的代码。 其他源文件必须包括`stdafx.h`头文件作为第一个头文件，但也可以包括其他文件。 编译源文件时，使用`/Yu`开关指定稳定的头文件(`stdafx.h`)，编译器将使用预编译头文件`pch`而不是头文件。\n\n在检查大型项目时，您通常会发现使用了预编译头；正如您所看到的，它改变了项目的文件结构。 本章后面的示例将说明如何创建和使用预编译头。\n\n# 项目结构\n\n将代码组织到模块中以使您能够有效地维护它，这一点很重要。 [第 7 章](07.html)，*面向对象编程简介*解释了面向对象，这是组织和重用代码的一种方式。 但是，即使您正在编写类似 C 的过程代码(即，您的代码涉及以线性方式调用函数)，您也将从将其组织到模块中受益。 例如，您可能有操作字符串的函数和其他访问文件的函数，因此您可能决定将字符串函数的定义放在一个源文件`string.cpp`中，而将文件函数的定义放在另一个文件`file.cpp`中。 为了使项目中的其他模块可以使用这些文件，您必须在头文件中声明函数的原型，并在使用这些函数的模块中包含该头文件。\n\n语言中没有关于头文件和包含函数定义的源文件之间的关系的绝对规则。 对于`string.cpp`中的函数，您可能有一个名为`string.h`的头文件；对于`file.cpp`中的函数，您可能有一个名为`file.h`的头文件。 或者，您可能只有一个名为`utilities.h`的文件，其中包含两个文件中所有函数的声明。 您必须遵守的唯一规则是，在编译时，编译器必须能够通过头文件或函数定义本身访问当前源文件中的函数声明。\n编译器不会在源文件中*向前查找*，因此如果函数`A`调用同一源文件中的另一个函数`B`，则在函数`A`调用它之前必须已经定义了函数`B`，或者必须有原型声明。 这导致了一个典型的约定，即具有与每个源文件相关联的头文件，每个源文件包含源文件中的函数原型，并且源文件包括此头文件。 当您编写类时，此约定变得更加重要。\n\n# 管理依赖项\n\n使用生成工具生成项目时，将执行检查以查看生成的输出是否存在，如果不存在，则执行相应的生成操作。 常见的术语是，构建步骤的输出称为**目标**，构建步骤的输入(例如，源文件)是该目标的**依赖关系**。 每个目标的依赖项是用于创建它们的文件。 依赖项本身可能是构建操作的目标，并且具有自己的依赖项。\n\n例如，下图显示了项目中的依赖关系：\n\n![](img/0998d366-8f1f-4f98-a0a0-9fa730e71d3f.png)\n\n在该项目中，有三个源文件(`main.cpp`、`file1.cpp`和`file2.cpp`)。 这些文件中的每一个都包含相同的头文件`utils.h`，该头文件是预编译的(因此有第四个源文件`utils.cpp`，它只包含`utils.h`)。 所有源文件都依赖于`utils.pch`，而后者又依赖于`utils.h`。 源文件`main.cpp`具有`main`函数，并调用其他两个源文件(`file1.cpp`和`file2.cpp`)中的函数，并通过相关联的头文件`file1.h`和`file2.h`访问这些函数。\n\n在第一次编译时，构建工具将看到可执行文件依赖于这四个目标文件，因此它将查找构建每个目标文件的规则。 对于三个 C++ 源文件，这意味着编译`cpp`文件，但由于`utils.obj`用于支持预编译头文件，因此构建规则将与其他文件不同。 当构建工具生成这些目标文件后，它会将它们与任何库代码(此处未显示)链接在一起。\n\n随后，如果您更改`file2.cpp`并构建项目，构建工具将看到只有`file2.cpp`发生了更改，而且由于只有`file2.obj`依赖于`file2.cpp`，因此 Make 工具需要做的全部工作就是编译`file2.cpp`，然后将新的`file2.obj`与现有的对象文件链接起来创建可执行文件。 如果更改头文件`file2.h`，构建工具将看到有两个文件依赖于此头文件`file2.cpp`和`main.cpp`，因此构建工具将编译这两个源文件，并将新的两个目标文件`file2.obj`和`main.obj`与现有的目标文件链接以形成可执行文件。 但是，如果预编译头文件`util.h`改变，则意味着必须编译所有*个源文件*。\n\n对于小项目，依赖项很容易管理，正如您所看到的，对于单个源文件项目，您甚至不必担心调用链接器，因为编译器会自动调用链接器。 随着 C++ 项目变得越来越大，管理依赖项变得越来越复杂，这就是 Visual C++ 等开发环境变得至关重要的地方。\n\n# Makefiles\n\n如果你支持一个 C++ 项目，你很可能会遇到一个 Makefile。 这是一个文本文件，其中包含用于在项目中构建目标的目标、依赖项和规则。 Makefile 是通过 make 工具调用的，在 Windows 上是 nmake，在类 Unix 平台上是**make**。\n\nMakefile 是一系列规则，如下所示：\n\n```cpp\n targets : dependents \n        commands \n```\n\n目标是依赖于从属对象的一个或多个文件(可能是几个文件)，因此，如果一个或多个从属对象比一个或多个目标新(因此自上次构建目标以来发生了更改)，则需要重新构建目标，这可以通过运行命令来完成。 可能有多个命令，并且每个命令都在单独的行上，并以制表符为前缀。 目标可能没有依赖项，在这种情况下，将始终调用命令。\n\n例如，使用前面的示例，可执行文件`test.exe`的规则如下所示：\n\n```cpp\n    test.exe : main.obj file1.obj file2.obj utils.obj \n        link /out:test.exe main.obj file1.obj file2.obj utils.obj\n```\n\n由于`main.obj`目标文件依赖于源文件`main.cpp`、头文件`File1.h`和`File2.h`以及预编译头文件`utils.pch`，因此该文件的规则如下：\n\n```cpp\n    main.obj : main.cpp file1.h file2.h utils.pch \n        cl /c /Ehsc main.cpp /Yuutils.h\n```\n\n使用`/c`开关调用编译器，该开关指示代码已编译为目标文件，但编译器不应调用链接器。 编译器被告知通过带有`/Yu`开关的头文件`utils.h`使用预编译头文件`utils.pch`。 其他两个源文件的规则将类似。\n\n生成预编译头文件的规则如下：\n\n```cpp\n    utils.pch : utils.cpp utils.h \n        cl /c /EHsc utils.cpp /Ycutils.h\n```\n\n`/Yc`开关告诉编译器使用头文件`utils.h`创建预编译头。\n\nMakefile 通常比这复杂得多。 它们将包含对目标、从属对象或命令开关进行分组的宏。 它们将包含目标类型的一般规则，而不是此处显示的特定规则，并且它们将具有条件测试。 如果您需要支持或编写 Makefile，那么您应该在手册中查找该工具的所有选项。\n\n# 写一个简单的项目\n\n本项目将说明 C++ 的特性以及您在本章中学到的项目。 该项目将使用多个源文件，以便您可以看到依赖项的效果以及生成工具将如何管理对源文件的更改。 该项目很简单：它将要求您键入您的名字，然后它会将您的姓名以及时间和日期打印到命令行。\n\n# 项目结构\n\n该项目使用三个函数：`main`函数，它调用两个函数`print_name`和`print_time`。 它们位于三个单独的源文件中，由于`main`函数将调用其他源文件中的其他两个函数，这意味着`main`源文件必须具有这些函数的原型。 在本例中，这意味着每个文件都有一个头。 该项目还将使用预编译头文件，这意味着源文件和头文件。 总而言之，这意味着将使用三个头文件和四个源文件。\n\n# 创建预编译头\n\n代码将使用 C++ 标准库通过流进行输入和输出，因此它将使用`<iostream>`头。 代码将使用 C++ `string`类型来处理输入，因此它将使用`<string>`头。 最后，它访问 C 运行时时间和日期函数，因此代码将使用`<ctime>`头。 这些都是标准的头文件，在开发项目时不会更改，因此它们很适合进行预编译。\n\n在 Visual Studio 中，创建一个 C++ 头文件并添加以下行：\n\n```cpp\n    #include <iostream> \n    #include <string> \n    #include <ctime>\n```\n\n将文件另存为`utils.h`。\n\n现在创建一个 C++ 源文件，并添加一行以包含您刚刚创建的头文件：\n\n```cpp\n    #include ″utils.h″\n```\n\n将其另存为`utils.cpp`。 您将需要为项目创建一个 Makefile，因此在 New File 对话框中，选择 Text File 作为您的文件类型。 添加以下构建预编译头的规则：\n\n```cpp\n    utils.pch utils.obj :: utils.cpp utils.h \n        cl /EHsc /c utils.cpp /Ycutils.h\n```\n\n将此文件另存为`makefile.`，并附加句点。 由于您将此文件创建为文本文件，因此 Visual Studio 通常会自动为其提供扩展名`txt`，但由于我们不想要扩展名，因此您需要添加句点以表示没有扩展名。 第一行说明两个文件`utils.pch`和`utils.obj`取决于指定的源文件和头文件。 第二行(前缀为制表符)告诉编译器编译 C++ 文件，而不是调用链接器，并告诉编译器保存包含在`utils.h`中的预编译代码。 该命令将创建指定的两个目标`utils.pch`和`utils.obj`。\n\n当 make 实用程序看到有两个目标时，默认操作(当目标和依赖项之间使用单个冒号时)是为每个目标调用一次命令(有一些宏可用于确定正在构建哪个目标)。 这意味着同一编译器命令将被调用两次。 我们不希望出现这种行为，因为两个目标都是通过一次命令调用创建的。 双冒号`::`是一种变通方法：它告诉 nmake 不要使用为每个目标调用命令的行为。 结果是，当 make 实用程序调用了一次命令 make`utils.pch`时，它会尝试 make`utils.obj`，但是发现它已经做了，因此意识到它不需要再次调用该命令。\n\n现在试试看这个。 在命令行中，在包含项目的文件夹中键入`nmake`。\n\n如果您没有给出 Makefile 的名称，程序维护工具将自动使用名为`makefile`的文件(如果您想使用另一个名称的 Makefile，请使用`/f`开关提供名称)：\n\n```cpp\nC:\\Beginning_C++ \\Chapter_01\\Code>nmake\nMicrosoft (R) Program Maintenance Utility Version 14.00.24210.0\nCopyright (C) Microsoft Corporation.  All rights reserved.\n\ncl /EHsc /c utils.cpp /Ycutils.h\nMicrosoft (R) C/C++ Optimizing Compiler Version 19.00.24210 for x86\nCopyright (C) Microsoft Corporation.  All rights reserved.\n\nutils.cpp\n```\n\n列出目录以确认已创建`utils.pch`和`utils.obj`。\n\n# 创建主文件\n\n现在创建一个 C++ 源文件并添加以下代码：\n\n```cpp\n    #include \"utils.h\" \n    #include \"name.h\" \n    #include \"time.h\" \n\n    void main() \n    { \n        print_name(); \n        print_time(); \n    }\n```\n\n将此文件另存为`main.cpp`。\n\n第一个包含文件是标准库头的预编译头。 另外两个文件为在`main`函数中调用的两个函数提供函数原型声明。\n\n现在您需要将`main`文件的规则添加到 Makefile。 将以下突出显示的行添加到文件顶部：\n\n```cpp\n main.obj : main.cpp name.h time.h utils.pch cl /EHsc /c main.cpp /Yuutils.h \n\n    utils.pch utils.obj :: utils.cpp utils.h \n        cl /EHsc /c utils.cpp /Ycutils.h\n```\n\n这一新行说明`main.obj`目标依赖于两个头文件：源文件和预编译头文件`utils.pch`。 此时，`main.cpp`文件将不会编译，因为头文件还不存在。 这样我们就可以测试 Makefile，创建两个 C++ 头文件；在第一个头文件中，添加函数 Prototype：\n\n```cpp\n    void print_name();\n```\n\n将此文件另存为`name.h`。 在第二个头文件中，添加函数 Prototype：\n\n```cpp\n    void print_time();\n```\n\n将此文件另存为`time.h`。\n现在可以运行 make 实用程序，它将只编译`main.cpp`文件。 测试一下：通过在命令行中键入`del main.obj utils.obj utils.pch`删除所有目标文件，然后再次运行 make 实用程序。 这一次，您将看到 make 实用程序首先编译`utils.cpp`，然后编译`main.cpp`。 这样排序的原因是因为第一个目标是`main.obj`，但是因为这取决于`utils.pch`，所以 make 工具移动到下一个规则并使用它来生成预编译头，然后返回到规则来创建`main.obj`。\n\n注意，您没有定义`print_name`也没有定义`print_time`，但是编译器没有错误。 原因是编译器只创建目标文件，而链接器负责解析函数的链接。 头文件中的函数原型使编译器确信函数将在另一个目标文件中定义。\n\n# 使用输入流和输出流\n\n到目前为止，我们已经了解了如何通过`cout`对象将数据输出到控制台。 标准库还提供了一个`cin`流对象，允许您从命令行输入值。\n\n创建一个 C++ 源文件并添加以下代码：\n\n```cpp\n    #include \"utils.h\" \n    #include \"name.h\" \n\n    void print_name() \n    { \n        std::cout << \"Your first name? \"; \n        std::string name; \n        std::cin >> name; \n        std::cout << name; \n    }\n```\n\n将此文件另存为`name.cpp`。\n\n第一个包含文件是预编译头文件，它将包括两个标准库头`<iostream>`和`<string>`，因此您可以使用在这些文件中声明的类型。 函数的第一行打印字符串您的名字？ 在控制台上。 请注意，查询后有一个空格，因此光标将保持在同一行上，为输入做好准备。\n下一行声明一个 C++ `string`对象变量。 字符串是零个或多个字符，每个字符都会占用内存。 `string`类完成分配和释放字符串将使用的内存的所有工作。 这个类将在[章](08.html)，*中使用标准库容器*进行更详细的描述。 `cin`重载`>>`操作符以从控制台获取输入。 当您按 Enter 键时，`>>`运算符将返回您在`name`变量中键入的字符(将空格字符视为分隔符)。 然后，该函数将`name`变量的内容打印到控制台，而不换行。\n\n现在，将此源文件的规则添加到 Makefile；将以下行添加到文件的顶部：\n\n```cpp\n    name.obj : name.cpp name.h utils.pch \n        cl /EHsc /c name.cpp /Yuutils.h\n```\n\n保存此文件并运行 Make 工具以确认它将成为`name.obj`目标。\n\n# 使用时间函数\n\n最终的源文件将获得时间并在控制台上打印出来。 创建一个 C++ 源文件并添加以下行：\n\n```cpp\n    #include \"utils.h\" \n    #include \"time.h\" \n\n    void print_time() \n    { \n        std::time_t now = std::time(nullptr); \n        std::cout << \", the time and date are \" \n                  << std::ctime(&now) << std::endl; \n    }\n```\n\n`std::time`和`std::gmtime`这两个函数是 C 函数，`std::time_t`是 C 类型；都可以通过 C++ 标准库获得。 `std::time`函数用于获取自 1970 年 1 月 1 日午夜以来的秒数。 该函数返回一个`std::time_t`类型的值，它是一个 64 位整数。 如果您将指针传递到内存中存储变量的位置，则该函数可以选择将该值复制到另一个变量。 在本例中，我们不需要此工具，因此我们将 C++ `nullptr`传递给函数以指示不应执行复制。\n接下来，我们需要将秒数转换为以您可以理解的格式包含时间和日期的字符串。 这就是`std::ctime`函数的用途，该函数将指向保存秒数的变量的指针作为参数。 `now`变量具有秒数，`&`运算符用于获取该变量在内存中的地址。 内存和指针在[第 4 章](04.html)，*使用内存、数组和指针*中有更详细的介绍。 此函数返回一个字符串，但您尚未为该字符串分配任何内存，也不应尝试释放该字符串使用的内存。 函数的作用是：创建一个**静态分配的**内存缓冲区，该缓冲区将由当前执行线程上运行的所有代码使用。 每次在同一执行线程上调用`std::ctime`函数时，使用的内存位置将是相同的，尽管内存的内容可能会改变。\n\n此函数说明查看手册以了解谁负责分配和释放内存有多么重要。 [第 4 章](04.html)，*使用内存、数组和指针*更详细地介绍了内存分配。\n\n通过多次调用 put`<<`操作符来格式化输出，从`std::ctime`返回的字符串被打印到控制台。\n\n现在将构建规则添加到 Makefile。 将以下内容添加到文件顶部：\n\n```cpp\n    time.obj : time.cpp time.h utils.pch \n        cl /EHsc /c time.cpp /Yuutils.h\n```\n\n保存此文件并运行 Make 工具，并确认它构建了`time.obj`目标。\n\n# 生成可执行文件\n\n现在，您已经拥有了项目所需的所有对象文件，因此下一项任务是将它们链接在一起。 为此，请将以下行添加到 Makefile 的顶部：\n\n```cpp\n    time_test.exe : main.obj name.obj time.obj utils.obj \n        link /out:$@ $**\n```\n\n这里的目标是可执行文件，从属文件是四个目标文件。 生成可执行文件的命令调用链接工具并使用特殊语法。 Make 工具将`$@`符号解释为使用目标，因此`/out`开关实际上将是`/out:time_test.out`。 生成工具将`$**`符号解释为*使用所有从属关系*，以便链接所有从属关系。\n\n保存此文件并运行 make 实用程序。 您应该会发现，只有链接工具会被调用，它会将目标文件链接在一起来创建可执行文件。\n\n最后，添加清理项目的规则。 最好提供一种机制来删除编译过程创建的所有文件，并使项目保持干净，只保留源文件。 在链接目标文件的行之后，添加以下行：\n\n```cpp\n    time_test.exe : main.obj name.obj time.obj utils.obj \n        link /out:$@ $** \n clean : @echo Cleaning the project...    \n        del main.obj name.obj time.obj utils.obj utils.pch del time_test.exe\n```\n\n目标`clean`是一个伪目标：实际上没有创建任何文件，因此没有依赖关系。 这说明了 make 实用程序的一个特性：如果您使用目标的名称调用 nmake，该实用程序将只生成该目标。 如果未指定目标，则实用程序将创建 Makefile 中提到的第一个目标，在本例中为`time_test.exe`。\n\n`clean`伪目标有三个命令。 第一个命令将`Cleaning the project...`打印到控制台。 这里的`@`符号告诉 make 实用程序在不将命令打印到控制台的情况下运行命令。 第二个和第三个命令调用命令行工具`del`来删除文件。 现在，通过在命令行中键入`nmake clean`来清理项目，并确认目录中只有头文件、源文件和 Makefile。\n\n# 测试代码\n\n再次运行 make 实用程序，以便构建可执行文件。 在命令行上，通过键入*`**time_test**`命令运行该示例。 系统将要求您键入您的名字；执行此操作，然后按 Enter 键。 您会发现您的姓名、时间和日期打印在控制台上：\n\n```cpp\nC:\\Beginning_C++ \\Chapter_01>time_test\nYour first name? Richard\nRichard, the time and date are Tue Sep  6 19:32:23 2016\n```\n\n# 更改项目\n\n现在您已经有了基本的项目结构，有了 Makefile，您就可以对文件进行更改，并且可以放心，在重新构建项目时，只会编译更改过的文件。 为了说明这一点，请更改`name.cpp`中的`print_name`函数，以更礼貌的方式询问您的姓名。 更改函数体中的第一行，如下突出显示：\n\n```cpp\n    void print_name() \n    {\n std::cout << \"Please type your first name and press [Enter] \"; \n        std::string name;\n```\n\n保存文件，然后运行 make 实用程序。 这一次，只编译`name.cpp`源文件，结果文件`name.obj`与现有目标文件链接。\n\n现在更改`name.h`头文件，并在文件中添加注释：\n\n```cpp\n // More polite version \n    void print_name();\n```\n\n做好项目。 你发现了什么？ 这一次，编译了*两个*源文件`name.cpp`和`main.cpp`，并将它们与现有目标文件链接以创建可执行文件。 要了解编译这两个文件的原因，请看一下 Makefile 中的依赖项规则。 唯一更改的文件是`name.h`，该文件在`name.obj`和`main.obj`的依赖列表中命名，因此，这两个文件被重新构建。 由于这两个文件位于`time_test.exe`的依赖项列表中，因此也将重新构建可执行文件。\n\n# 简略的 / 概括的 / 简易判罪的 / 简易的\n\n本章是对 C++ 的温和而全面的介绍。 您了解了使用该语言的原因以及如何从一家供应商安装编译器。 您了解了 C++ 项目的结构、源文件和头文件，以及如何通过库共享代码。 您还学习了如何使用 makefile 维护项目，并且通过一个简单的示例，您获得了编辑和编译代码的实践经验。\n\n您已经有了一个编译器、一个编辑器和一个管理项目的工具，所以现在您已经准备好学习关于 C++ 的更多细节，从下一章的 C++ 语句开始，并控制应用中的执行流程。"
  },
  {
    "path": "docs/begin-cpp-prog/02.md",
    "content": "# 二、了解语言特性\n\n在上一章中，您安装了 C++ 编译器并开发了一个简单的应用。 您还探索了 C++ 项目的基本结构以及如何管理它们。 在本章中，您将更深入地学习语言，并学习控制代码流的各种语言功能。\n\n# 编写 C++\n\n在格式化和编写代码时，C++ 是一种非常灵活的语言。 它也是一种强类型语言，这意味着有声明变量类型的规则，您可以通过让编译器帮助您编写更好的代码来利用这些规则。 在本节中，我们将介绍如何格式化 C++ 代码以及声明变量和确定变量作用域的规则。\n\n# 使用空格\n\n除了字符串文字之外，您可以自由使用空格(空格、制表符、换行符)，并且可以根据自己的喜好使用任意多或少的空格。 C++ 语句用分号分隔，因此在下面的代码中有三个语句，它们将编译和运行：\n\n```cpp\n    int i = 4; \n    i = i / 2; \n    std::cout << \"The result is\" << i << std::endl;\n```\n\n整个代码可以写成如下所示：\n\n```cpp\n    int i=4;i=i/2; std::cout<<\"The result is \"<<i<<std::endl;\n```\n\n在某些情况下需要空格(例如，声明变量时，类型和变量名之间必须有空格)，但约定是尽可能明智地使代码可读。 虽然从语言角度讲，将所有语句放在一行(如 JavaScript)是完全正确的，但这会使代码几乎完全不可读。\n\nIf you are interested in some of the more creative ways of making code unreadable, have a look at the entries for the annual International Obfuscated C Code Contest ([http://www.ioccc.org/](http://www.ioccc.org/)). As the progenitor of C++, many of the lessons in C shown at IOCCC apply to C++ code too.\n\n请记住，如果您编写的代码是可行的，那么它可能会被使用几十年，这意味着您可能需要在编写代码数年后才能重新使用该代码，这意味着其他人也会支持您的代码。 使您的代码可读不仅是对其他开发人员的礼貌，而且不可读的代码总是可能被替换的目标。\n\n# 格式化代码\n\n不可避免的是，无论您为谁编写代码，都将决定您如何格式化代码。 例如，如果您使用某种形式的预处理来提取代码和定义来为代码创建文档，这有时是有意义的。 在很多情况下，强加给你的风格是别人的个人喜好。\n\nVisual C++ allows you to place XML comments in your code. To do this you use a three--slash comment (`///`) and then compile the source file with the `/doc` switch. This creates an intermediate XML file called an `xdc` file with a `<doc>` root element and containing all the three--slash comments. The Visual C++ documentation defines standard XML tags (for example, `<param>`, `<returns>` to document the parameters and return value of a function). The intermediate file is compiled to the final document XML file with the `xdcmake` utility.\n\n在 C++ 中有两种广泛的风格：**K&R**和**Allman**。\n\nKernighan 和 Ritchie(K&R)写了第一本也是最有影响力的关于 C 语言的书(Dennis Ritchie 是 C 语言的作者)。 K&R 样式用于描述该书中使用的格式样式。 通常，K&R 将代码块的左大括号放在最后一条语句的同一行。 如果您的代码有嵌套语句(通常是嵌套的)，那么这种风格可能会让人有点困惑：\n\n```cpp\n    if (/* some test */) { \n        // the test is true  \n        if (/* some other test */) { \n            // second test is true  \n        } else { \n            // second test is false    \n        } \n    } else { \n        // the test is false  \n    }\n```\n\n此样式通常用于 Unix(和类 Unix)代码。\n\nAllman 样式(以开发人员 Eric Allman 命名)将左大括号放在新行上，因此嵌套示例如下所示：\n\n```cpp\n        if (/* some test */)  \n        { \n            // the test is true  \n            if (/* some other test */)  \n            { \n                // second test is true   \n            }  \n            else  \n            { \n                // second test is false     \n            } \n        }  \n        else  \n        { \n           // the test is false  \n        }\n```\n\n微软通常使用 Allman 风格。\n\n请记住，您的代码不太可能出现在纸上，因此 K&R 更紧凑的事实不会拯救任何树。 如果你可以选择，你应该选择最具可读性的风格；对于这本书，这位作者的决定是，奥尔曼更具可读性。\n\n如果您有多个嵌套块，缩进可以让您了解代码驻留在哪个块中。 然而，评论可能会有所帮助。 特别是，如果代码块包含大量代码，注释代码块的原因通常很有帮助。 例如，在`if`语句中，将测试结果放入代码块中会很有帮助，这样您就可以知道该块中的变量值是什么。 在测试的结束支撑上加一条注释也很有用：\n\n```cpp\n    if (x < 0)  \n    { \n       // x < 0 \n       /* lots of code */ \n    }  // if (x < 0) \n\n    else  \n    { \n       // x >= 0 \n       /* lots of code */ \n    }  // if (x < 0)\n```\n\n如果您将测试放在右大括号上作为注释，这意味着您有一个可以用来查找导致代码块的测试的搜索词。 前面的几行使注释变得多余，但是当您的代码块包含数十行代码，并且具有多层嵌套时，这样的注释可能非常有用。\n\n# 编写报表\n\n语句可以是变量的声明、计算结果为值的表达式，也可以是类型的定义。 语句也可以是影响代码执行流的控制结构。\n\n语句以分号结束。 除此之外，关于如何格式化语句几乎没有什么规则。 您甚至可以单独使用分号，这称为 NULL 语句。 NULL 语句不执行任何操作，因此使用过多的分号通常是有益的。\n\n# 使用表达式\n\n表达式是一系列运算符和操作数(变量或文字)，它们产生一些值。 请考虑以下事项：\n\n```cpp\n    int i; \n    i = 6 * 7;\n```\n\n右侧的`6 * 7`是一个表达式，赋值(从左侧的`i`到右侧的分号)是一个语句。\n\n每个表达式都是**左值**或**右值**。 您最有可能在错误描述中看到这些关键字。 实际上，左值是指某个内存位置表达式。 赋值左侧的项目必须为左值。 但是，左值可以出现在赋值的左侧或右侧。 所有变量都是左值。 右值是一个临时项，其存在时间不超过使用它的表达式；它将有一个值，但不能被赋值，因此它只能存在于赋值的右侧。 文字是右值。 下面显示了一个左值和右值的简单示例：\n\n```cpp\n    int i; \n    i = 6 * 7;\n```\n\n在第二行中，`i`是左值，表达式`6 * 7`产生右值(`42`)。 以下代码将不会编译，因为左侧有一个右值：\n\n```cpp\n    6 * 7 = i;\n```\n\n一般说来，当您在后面附加分号时，表达式就变成了语句。 例如，以下两个语句都是：\n\n```cpp\n    42;\n    std::sqrt(2);\n```\n\n第一行是右值`42`，但是因为它是临时的，所以没有效果。 C++ 编译器将对其进行优化。 第二行调用标准库函数来计算`2`的平方根。 同样，结果是一个右值，并且没有使用该值，因此编译器将对其进行优化。 但是，它说明可以在不使用其返回值的情况下调用函数。 虽然`std::sqrt`并非如此，但许多函数除了它们的返回值之外，还有其他持久的影响。 实际上，函数的全部意义通常是做一些事情，返回值通常仅用于指示函数是否成功；开发人员通常假设函数将成功，而忽略返回值。\n\n# 使用逗号运算符\n\n运算符将在本章后面介绍；不过，在这里介绍逗号运算符很有用。 您可以将由逗号分隔的一系列表达式作为单个语句。 例如，以下代码在 C++ 中是合法的：\n\n```cpp\n    int a = 9;\n    int b = 4;\n    int c;\n    c = a + 8, b + 1;\n```\n\n作者打算键入`c = a + 8 / b + 1;`和`:`，他们按了逗号而不是`/`。 其目的是将`c`赋值为 9+2+1 或 12。此代码将编译并运行，变量`c`将赋值为 17(`a + 8`)。 原因是逗号将赋值的右侧分隔为两个表达式`a + 8`和`b + 1`，并使用第一个表达式的值来赋值`c`。 在本章的后面部分，我们将了解运算符的优先顺序。 但是，在这里值得一提的是，逗号的优先级最低，`+`的优先级高于`=`，因此语句按照加法的顺序执行：赋值，然后是逗号运算符(结果是`b + 1`被丢弃)。\n\n您可以使用圆括号对表达式进行分组，以更改优先顺序。 例如，输入错误的代码可能如下所示：\n\n```cpp\n    c = (a + 8, b + 1);\n```\n\n该语句的结果是：变量`c`被赋值为 5(或`b + 1`)。 原因是，使用逗号运算符时，表达式是从左到右执行的，因此该组表达式的值是最紧凑的。 例如，在某些情况下，在`for`循环的初始化或循环表达式中，您会发现逗号运算符很有用，但正如您在这里看到的那样，即使有意使用，逗号运算符也会生成难以阅读的代码。\n\n# 使用类型和变量\n\n类型将在下一章中详细介绍，但在此提供基本信息会很有用。 C++ 是一种强类型语言，这意味着您必须声明所使用的变量类型。 这样做的原因是，编译器需要知道要为变量分配多少内存，它可以通过变量的类型来确定这一点。 此外，如果变量尚未显式初始化，编译器需要知道如何初始化该变量，并且要执行此初始化，编译器需要知道变量的类型。\n\nC++ 11 provides the `auto` keyword, which relaxes this concept of strong typing, and it will be covered in the next chapter. However, the type checking of the compiler is so important that you should use type checking as much as possible.\n\nC++ 变量可以在代码中的任何位置声明，只要它们是在使用之前声明的。 *声明变量的*决定*如何使用它(这称为变量的**作用域**)。 通常，最好在最严格的范围内尽可能靠近您要使用的位置来声明变量。 这可以防止*名称冲突*，在这种情况下，您必须添加附加信息来消除两个或更多变量的歧义。*\n\n你可以，*，也应该*，给你的变量起描述性的名称。 这将使您的代码更具可读性，更易于理解。 C++ 名称必须以字母字符或下划线开头。 它们可以包含除空格以外的字母数字字符，但可以包含下划线。 因此，以下是有效名称：\n\n```cpp\n    numberOfCustomers \n    NumberOfCustomers \n    number_of_customers\n```\n\nC++ 名称区分大小写，前`2,048`字符很重要。 变量名可以以下划线开头，但不能使用两个下划线，也不能使用后跟大写字母的下划线(这些是由 C++ 保留的)。 C++ 还保留了关键字(例如，`while`和`if`)，显然您不能将类型名用作变量名，既不能使用内置类型名(`int`、`long`等)，也不能使用您自己的自定义类型。\n\n您可以在语句中声明变量，并以分号结尾。 声明变量的基本语法是指定变量的类型，然后指定名称，还可以指定变量的任何初始化。\n\n在使用内置类型之前，必须先对其进行初始化：\n\n```cpp\n    int i; \n    i++ ;           // C4700 uninitialized local variable 'i' used \n    std::cout << i;\n```\n\n基本上有三种初始化变量的方法。 您可以赋值，也可以调用类型构造函数(类的构造函数将在[第 6 章](06.html)、*类*中定义)，或者可以使用函数语法初始化变量：\n\n```cpp\n    int i = 1; \n    int j = int(2); \n    int k(3);\n```\n\n这三个都是合法的 C++，但在风格上第一个更好，因为它更明显：变量是一个整数，它被称为`i`，并且被赋值为 1。第三个看起来很混乱；它看起来像是一个函数的声明，而它实际上是声明一个变量。 下一章将展示使用初始化列表语法赋值的变体。 您想要这样做的原因将留到那一章。\n\n[第 6 章](06.html)，*类*将介绍类，即您自己的自定义类型。 可以将自定义类型定义为具有默认值，这意味着您可以决定在使用自定义类型的变量之前不对其进行初始化。 但是，这将导致较差的性能，因为编译器将使用默认值初始化变量，随后您的代码将赋值，从而导致执行两次赋值。\n\n# 使用常量和文字\n\n每种类型都有一个文字表示形式。 整数将是不带小数点的数字，如果它是带符号的整数，则文字也可以使用加号或减号来表示符号。 同样，实数可以具有包含小数点的文字值，您甚至可以使用包含指数的科学(或工程)格式。 在代码中指定文字时，C++ 有各种规则可供使用，这些规则将在下一章中介绍。 下面显示了一些文字的示例：\n\n```cpp\n    int pos = +1; \n    int neg = -1; \n    double micro = 1e-6; \n    double unit = 1.; \n    std::string name = \"Richard\";\n```\n\n请注意，对于`unit`变量，编译器知道文字是实数，因为该值有一个小数点。 对于整数，您可以在代码中提供十六进制文字，方法是在数字前面加上`0x`，这样`0x100`就是十进制的`256`。 默认情况下，输出流将以 10 为基数打印数值；但是，您可以在输出流中插入**操纵器**，以告知它使用不同的数字基数。 默认行为是`std::dec`，这意味着数字应该显示为基数 10，`std::oct`表示显示为八进制(基数为 8)，`std::hex`表示显示为十六进制(基数`16`)。 如果您希望打印前缀，则可以使用流操纵器`std::showbase`(更多详细信息将在[第 8 章](08.html)，*使用标准库容器*中给出)。\n\nC++ 定义了一些文字。 对于逻辑类型`bool`，有`true`和`false`常量，其中`false`是零，`true`是 1。还有`nullptr`常量，同样是零，它被用作任何指针类型的无效值。\n\n# 定义常量\n\n在某些情况下，您可能希望提供可在整个代码中使用的常量值。 例如，您可能决定为`π`声明一个常量。 不应允许更改此值，因为它会更改代码中的基础逻辑。 这意味着您应该将变量标记为常量。 执行此操作时，编译器将检查变量的使用情况，如果在更改变量值的代码中使用该变量，则编译器将发出错误：\n\n```cpp\n    const double pi = 3.1415; \n    double radius = 5.0; \n    double circumference = 2 * pi * radius;\n```\n\n在这种情况下，符号`pi`被声明为常量，因此不能更改。 如果您随后决定更改常量，编译器将发出错误：\n\n```cpp\n    // add more precision, generates error C3892 \n    pi += 0.00009265359;\n```\n\n一旦您声明了一个常量，您就可以放心，编译器将确保它保持不变。 您可以为常量指定一个表达式，如下所示：\n\n```cpp\n    #include <cmath> \n    const double sqrtOf2 = std::sqrt(2);\n```\n\n在这段代码中，使用`std::sqrt`函数声明了一个名为`sqrtOf2`的全局常量，并为其赋值。 由于该常量是在函数外部声明的，因此它对文件是全局的，并且可以在整个文件中使用。\n\n在上一章中，您了解到声明常量的一种方法是使用`#define`符号。 这种方法的问题在于，预处理器只需进行简单的替换。 对于用`const`声明的常量，C++ 编译器将执行类型检查，以确保正确使用常量。\n\n还可以使用`const`声明将用作**常量表达式**的常量。 例如，您可以使用方括号语法声明数组(更多详细信息将在[第 4 章](04.html)，*使用内存、数组和指针*中给出)：\n\n```cpp\n    int values[5];\n```\n\n这在堆栈上声明了一个由五个整数组成的数组，这些项通过`values`数组变量访问。 这里的`5`是一个常量表达式。 当您在堆栈上声明一个数组时，您必须为编译器提供一个常量表达式，以便它知道要分配多少内存，这意味着在编译时必须知道数组的大小。 (您可以分配大小仅在运行时已知的数组，但这需要动态内存分配，如[第 4 章](04.html)，*使用内存、数组和指针中所述。* )在 C++ 中，您可以声明一个常量来执行以下操作：\n\n```cpp\n    const int size = 5;  \n    int values[size];\n```\n\n在代码的其他地方，当您访问`values`数组时，可以使用`size`常量来确保不会访问超出数组末尾的项。 由于`size`变量仅在一个位置声明，因此如果您需要在稍后阶段更改数组的大小，则只有一个位置可以进行此更改。\n\n`const`关键字还可以用于指针和引用(参见[第 4 章](04.html)，*使用内存、数组和指针*)和对象(参见[第 6 章](06.html)，*CLASS*)；通常，您会看到它用于函数的参数(参见[第 5 章](05.html)，*使用函数*)。 这用于让编译器帮助确保正确使用指针、引用和对象，就像您希望的那样。\n\n# 使用常量表达式\n\nC++ 11 引入了一个称为`constexpr`的关键字。 这应用于表达式，并指示应在编译类型而不是在运行时计算该表达式：\n\n```cpp\n    constexpr double pi = 3.1415; \n    constexpr double twopi = 2 * pi;\n```\n\n这类似于初始化使用`const`关键字声明的常量。 但是，也可以将关键字`constexpr`应用于返回可在编译时计算的值的函数，因此这允许编译器优化代码：\n\n```cpp\n    constexpr int triang(int i) \n    { \n       return (i == 0) ? 0 : triang(i - 1) + i;\n    }\n```\n\n在本例中，函数`triang`递归计算三角数。 代码使用条件运算符。 在圆括号中，测试函数参数以查看它是否为零，如果是，则函数返回零，实际上结束递归并将函数返回给原始调用方。 如果该参数不为零，则返回值为该参数的和，用该参数调用的`triang`的返回值将递减。\n\n当在代码中使用文字调用此函数时，可以在编译时对其求值。 `constexpr`指示编译器检查函数的使用情况，以查看它是否可以在编译时确定参数。 在这种情况下，与在运行时调用函数相比，编译器可以更有效地计算返回值并生成代码。 如果编译器在编译时无法确定参数，则该函数将被调用为**Normal**。 标记有`constexpr`关键字的函数只能有一个表达式(因此在`triang`函数中使用条件运算符`?:`)。\n\n# 使用枚举\n\n提供常量的最后一种方法是使用`enum`变量。 实际上，`enum`是一组命名常量，这意味着您可以将`enum`用作函数的参数。 例如：\n\n```cpp\n    enum suits {clubs, diamonds, hearts, spades};\n```\n\n它定义了一个名为`suits`的枚举，其中包含一副纸牌中花色的命名值。 枚举是整数类型，默认情况下编译器将采用`int`，但您可以通过在声明中指定整数类型来更改此类型。 因为纸牌花色只有四个可能的值，所以使用`int`(通常是`4`字节)是浪费内存，我们可以使用`char`(单字节)：\n\n```cpp\n    enum suits : char {clubs, diamonds, hearts, spades};\n```\n\n当您使用枚举值时，可以只使用名称；但是，通常使用枚举的名称来确定其范围，从而使代码更具可读性：\n\n```cpp\n    suits card1 = diamonds; \n    suits card2 = suits::diamonds;\n```\n\n这两种形式都是允许的，但后一种形式更明确地表明该值是从枚举中获取的。 要强制开发人员指定范围，可以应用关键字`class`：\n\n```cpp\n    enum class suits : char {clubs, diamonds, hearts, spades};\n```\n\n使用此定义和前面的代码，可以编译声明`card2`的行，但不编译声明`card1`的行。 使用作用域为`enum`时，编译器将枚举视为新类型，并且没有从新类型到整数变量的内置转换。 例如：\n\n```cpp\n    suits card = suits::diamonds; \n    char c = card + 10; // errors C2784 and C2676\n```\n\n`enum`类型基于`char`，但是当您将`suits`变量定义为作用域(使用`class`)时，第二行将不会编译。 如果枚举被定义为未限定作用域(没有`class`)，则枚举值和`char`之间存在内置转换。\n\n默认情况下，编译器将为第一个枚举数赋值 0，然后为后续枚举数递增该值。 因此，`suits::diamonds`的值为 1，因为它是`suits`中的第二个值。 您可以自己赋值：\n\n```cpp\n    enum ports {ftp=21, ssh, telnet, smtp=25, http=80};\n```\n\n在这种情况下，`ports::ftp`的值为 21，`ports::ssh`的值为 22(递增 21)，`ports::telnet`为 22，`ports::smtp`为 25，`ports::http`为 80。\n\nOften the point of enumerations is to provide named symbols within your code and their values are unimportant. Does it matter what value is assigned to `suits::hearts`? The intention is usually to ensure that it is different from the other values. In other cases, the values are important because they are a way to provide values to other functions.\n\n枚举在`switch`语句中很有用(请参见后面)，因为命名的值比只使用整数更清楚。 您还可以将枚举用作函数的参数，从而限制通过该参数传递的值：\n\n```cpp\n    void stack(suits card) \n    { \n        // we know that card is only one of four values \n    }\n```\n\n# 声明指针\n\n由于我们讨论的是变量的使用，因此有必要解释一下用于定义指针和数组的语法，因为其中存在一些潜在的缺陷。 [第 4 章](04.html)，*使用内存、数组和指针*更详细地介绍了这一点，因此我们将只介绍语法，以便您熟悉它。\n\n在 C++ 中，您将使用类型化指针访问内存。 该类型指示保存在指向的存储器中的数据的类型。 因此，如果指针是(`4`字节)整数指针，它将指向可用作整数的四个字节。 如果整数指针递增，则它将指向下一个可用作整数的四个字节。\n\nDon't worry if you find pointers confusing at this point. [Chapter 4](04.html), *Working with Memory, Arrays, and Pointers*, will explain this in more detail. The purpose of introducing pointers at this time is to make you aware of the syntax.\n\n在 C++ 中，指针使用`*`符号声明，您可以使用`&`运算符访问内存地址：\n\n```cpp\n    int *p; \n    int i = 42; \n    p = &i;\n```\n\n第一行声明了一个变量`p`，该变量将用于保存整数的内存地址。 第二行声明一个整数并为其赋值。 第三行将一个值赋给指针`p`，使其成为刚刚声明的整数变量的地址。 需要强调的是，`p`和*的值不是*`42`；它将是存储`42`值的存储器地址。\n\n请注意，声明的变量名上有`*`。 这是普遍的惯例。 原因是，如果在一条语句中声明多个变量，则`*`仅适用于立即变量。 因此，例如：\n\n```cpp\n    int* p1, p2;\n```\n\n最初，这看起来像是声明了两个整数指针。 但是，这一行没有这样做；它只声明了一个指向名为`p1`的整数的指针。 第二个变量是一个称为`p2`的整数。 前面的行相当于下面的内容：\n\n```cpp\n    int *p1;  \n    int p2;\n```\n\n如果希望在一条语句中声明两个整数，则应执行以下操作：\n\n```cpp\n    int *p1, *p2;\n```\n\n# 使用名称空间\n\n命名空间为您提供了一种模块化代码的机制。 命名空间允许您用唯一的名称标记类型、函数和变量，这样，使用作用域解析操作符，您就可以给出一个*完全限定名称*。 这样做的好处是，您可以确切地知道将调用哪个项。 缺点是使用完全限定名实际上是为重载函数关闭了 C++ 的*参数相关查找*机制，编译器将根据传递给函数的参数选择最合适的函数。\n\n定义名称空间很简单：使用`namespace`关键字和为其指定的名称来修饰类型、函数和全局变量。 在下面的示例中，在`utilities`命名空间中定义了两个函数：\n\n```cpp\n    namespace utilities \n    { \n        bool poll_data() \n        { \n            // code that returns a bool \n        } \n        int get_data() \n        { \n            // code that returns an integer \n        } \n    }\n```\n\nDo not use semicolon after the closing bracket.\n\n现在，当您使用这些符号时，需要用名称空间限定名称：\n\n```cpp\n    if (utilities::poll_data()) \n    { \n        int i = utilities::get_data(); \n        // use i here... \n    }\n```\n\n名称空间声明可能只声明函数，在这种情况下，实际的函数必须在其他地方定义，并且您需要使用限定名称：\n\n```cpp\n    namespace utilities \n    { \n        // declare the functions \n        bool poll_data(); \n        int get_data(); \n    } \n\n    //define the functions \n    bool utilities::poll_data() \n    { \n        // code that returns a bool \n    } \n\n    int utilities::get_data() \n    { \n       // code that returns an integer \n    }\n```\n\n命名空间的一个用途是对代码进行版本化。 代码的第一个版本可能有一个副作用，该副作用不在您的功能规范中，从技术上讲是一个错误，但有些调用者会使用它并依赖它。 当您更新代码以修复错误时，您可以决定允许调用者选择使用旧版本，这样他们的代码就不会中断。 您可以使用命名空间执行此操作：\n\n```cpp\n    namespace utilities \n    { \n        bool poll_data(); \n        int get_data(); \n\n        namespace V2 \n        { \n            bool poll_data(); \n            int get_data(); \n            int new_feature(); \n        } \n    }\n```\n\n现在，需要特定版本的调用者可以调用完全限定名称，例如，调用者可以使用`utilities::V2::poll_data`使用较新的版本，使用`utilities::poll_data`使用较旧的版本。 当特定命名空间中的项调用同一命名空间中的项时，它不必使用限定名称。 因此，如果`new_feature`函数调用`get_data`，则会调用`utilities::V2::get_data`。 重要的是要注意，要声明嵌套的名称空间，您必须手动执行嵌套(如下所示)；您不能简单地声明一个名为`utilities::V2`的名称空间。\n\n编写了前面的示例，以便代码的第一个版本将使用命名空间`utilities`调用它。 C++ 11 提供了一种称为内联**内联**命名空间的工具，它允许您定义嵌套命名空间，但允许编译器在执行参数相关查找时将这些项视为父命名空间中的项：\n\n```cpp\n    namespace utilities \n    { \n        inline namespace V1 \n        { \n            bool poll_data(); \n            int get_data(); \n        } \n\n        namespace V2 \n        { \n            bool poll_data(); \n            int get_data(); \n            int new_feature(); \n        } \n    }\n```\n\n现在要调用`get_data`的第一个版本，可以使用`utilities::get_data`或`utilities::V1::get_data`。\n\n完全限定名可能会使代码难以阅读，尤其是在代码只使用一个命名空间的情况下。 要在这里提供帮助，您有几个选择。 您可以放置一条`using`语句来指示在指定名称空间中声明的符号可以在没有完全限定名称的情况下使用：\n\n```cpp\n    using namespace utilities; \n    int i = get_data(); \n    int j = V2::get_data();\n```\n\n您仍然可以使用完全限定名称，但此语句允许您放宽要求。 请注意，嵌套的命名空间是命名空间的成员，因此前面的`using`语句意味着您可以使用`utilities::V2::get_data`或`V2::get_data`调用`get_data`的第二个版本。 如果您使用非限定名称，则意味着您将调用`utilities::get_data`。\n\n一个命名空间可以包含许多项，您可能决定只使用其中的几个项来放宽对完全限定名的使用。 为此，请使用`using`并给出项目的名称：\n\n```cpp\n    using std::cout; \n    using std::endl; \n    cout << \"Hello, World!\" << endl;\n```\n\n这段代码说明，每当使用`cout`时，它都会引用`std::cout`。 您可以在函数中使用`using`，也可以将其作为文件作用域，并使意图对文件而言是全局的。\n\n您不必在一个位置声明命名空间，您可以在多个文件中声明它。 以下内容可能位于与先前`utilities`声明不同的文件中：\n\n```cpp\n    namespace utilities \n    { \n        namespace V2 \n        { \n            void print_data(); \n        } \n    }\n```\n\n`print_data`函数仍然是`utilities::V2`命名空间的一部分。\n\n您还可以将`#include`放入命名空间，在这种情况下，头文件中声明的项现在将成为命名空间的一部分。 前缀为`c`的标准库头文件(例如，`cmath`、`cstdlib`和`ctime`)通过在`std`名称空间中包含适当的 C 头来访问 C 运行时函数。\n\n命名空间的最大优点是能够使用可能常见但对其他不知道命名空间名称的代码隐藏的名称来定义项。 命名空间意味着您的代码仍然可以通过完全限定名使用这些项。 但是，这只有在使用唯一的名称空间名称时才有效，很可能名称空间名称越长，它就越可能是唯一的。 Java 开发人员通常使用 URI 命名他们的类，您也可以决定做同样的事情：\n\n```cpp\n    namespace com_packtpub_richard_grimes \n    { \n        int get_data(); \n    }\n```\n\n问题是完全限定名变得相当长：\n\n```cpp\n    int i = com_packtpub_richard_grimes::get_data();\n```\n\n您可以使用别名解决此问题：\n\n```cpp\n    namespace packtRG = com_packtpub_richard_grimes; \n    int i = packtRG::get_data();\n```\n\nC++ 允许您定义没有名称的名称空间，即**匿名**名称空间。 如前所述，命名空间允许您防止在多个文件中定义的代码之间发生名称冲突。 如果您只想在一个文件中使用这样的名称，您可以定义一个唯一的名称空间名称。 但是，如果您必须对多个文件执行此操作，这可能会变得单调乏味。 没有名称的命名空间具有特殊含义，即它具有**内部链接**，即项目只能在当前翻译单元、当前文件中使用，而不能在任何其他文件中使用。\n\n未在命名空间中声明的代码将是`global`命名空间的成员。 您可以在没有命名空间名称的情况下调用代码，但您可能希望使用没有命名空间名称的作用域解析操作符显式指示该项位于`global`命名空间中：\n\n```cpp\n    int version = 42; \n\n    void print_version() \n    { \n        std::cout << \"Version = \" << ::version << std::endl; \n    }\n```\n\n# C++ 变量作用域\n\n正如您在上一章中看到的，编译器会将源文件编译为称为**翻译单元**的单个项目。 编译器将确定您声明的对象和变量以及您定义的类型和函数，一旦声明，您就可以在声明范围内的后续代码中使用这些内容中的任何一个。 在最广泛的情况下，您可以通过在将由项目中的所有源文件使用的头文件中声明项来在全局范围内声明项。 如果您不使用名称空间，那么使用这样的全局变量将它们命名为全局名称空间的一部分通常是明智的做法：\n\n```cpp\n    // in version.h \n    extern int version; \n\n    // in version.cpp \n    #include \"version.h\"  \n    version = 17; \n\n    // print.cpp \n    #include \"version.h\" \n    void print_version() \n    { \n        std::cout << \"Version = \" << ::version << std::endl; \n    }\n```\n\n这段代码包含两个源文件(`version.cpp`和`print.cpp`)的 C++ 以及这两个源文件都包含的头文件(`version.h`)。 头文件声明全局变量`version`，这两个源文件都可以使用；它声明了变量，但没有定义它。 实际变量是在`version.cpp`中定义和初始化的；正是在这里，编译器将为变量分配内存。 头中声明中使用的`extern`关键字向编译器指示`version`具有**外部链接**，即该名称在定义变量的文件以外的其他文件中可见。 在`print.cpp`源文件中使用了`version`变量。 在此文件中，使用作用域解析操作符(`::`)时没有命名空间名称，因此表示变量`version`在全局命名空间中。\n\n您还可以声明仅在当前翻译单元中使用的项目，方法是在使用前在源文件中声明它们(通常在文件顶部)。 这会产生一定程度的模块化，并允许您对其他源文件中的代码隐藏实现细节。 例如：\n\n```cpp\n    // in print.h \n    void usage(); \n\n    // print.cpp \n    #include \"version.h\" \n    std::string app_name = \"My Utility\"; \n    void print_version() \n    { \n       std::cout << \"Version = \" << ::version << std::endl; \n    } \n\n    void usage() \n    { \n       std::cout << app_name << \" \"; \n       print_version(); \n    }\n```\n\n`print.h`头包含文件`print.cpp`中代码的接口。 只有在头文件中声明的函数才能被其他源文件调用。 调用者不需要知道`usage`函数的实现，正如您在这里看到的那样，它是通过调用名为`print_version`的函数实现的，该函数仅对`print.cpp`中的代码可用。 变量`app_name`是在文件范围内声明的，因此只有`print.cpp`中的代码才能访问它。\n\n如果另一个源文件声明了一个文件作用域的变量，称为`app_name`，并且也是一个`std::string`，则该文件将被编译，但链接器在尝试链接目标文件时会出现错误。 原因是链接器将看到在两个位置定义的相同变量，并且它不知道使用哪个变量。\n\n函数还定义了一个作用域；函数中定义的变量只能通过该名称访问。 函数的参数也作为变量包含在函数中，因此在声明其他变量时，必须使用不同的名称。 如果参数未标记为`const`，则可以在函数中更改该参数的值。\n\n只要在使用之前声明变量，就可以在函数内的任何位置声明变量。 大括号(`{}`)用于定义代码块，它们还定义局部作用域；如果在代码块内声明变量，则只能在那里使用它。 这意味着您可以在代码块外声明同名变量，编译器将使用最接近其访问范围的变量。\n\n在结束本部分之前，有必要提到 C++**存储类**的一个方面。 在函数中声明的变量意味着编译器将在为该函数创建的堆栈帧上为该变量分配内存。 当函数完成时，堆栈帧将被拆卸并回收内存。 这意味着，函数返回后，所有局部变量中的值都会丢失；再次调用该函数时，会重新创建该变量并再次初始化。\n\nC++ 提供`static`关键字来更改此行为。 关键字`static`表示在程序启动时分配变量，就像在全局作用域中声明的变量一样。 将`static`应用于函数中声明的变量意味着该变量具有内部链接，也就是说，编译器将对该变量的访问限制为该函数：\n\n```cpp\n    int inc(int i) \n    { \n        static int value; \n        value += i; \n        return value; \n    } \n\n    int main() \n    { \n        std::cout << inc(10) << std::endl; \n        std::cout << inc(5) << std::endl; \n    }\n```\n\n默认情况下，编译器会将静态变量初始化为`0`，但您可以提供初始化值，该值将在第一次分配变量时使用。 当该程序启动时，在调用`main`函数之前，`value`变量将被初始化为`0`。 第一次调用`inc`函数时，`value`变量递增到 10，由该函数返回并打印到控制台。 当`inc`函数返回时，将保留`value`变量，以便当再次调用`inc`函数时，将`value`变量递增`5`至值`15`。\n\n# 使用运算符\n\n运算符用于从一个或多个操作数计算值。 下表将具有相等*优先级*的所有运算符分组，并列出它们的*结合性*。 表中的位置越高，运算符在表达式中的执行优先级就越高。 如果表达式中有多个运算符，编译器将在执行较低优先级运算符之前执行较高优先级运算符。 如果表达式包含优先级相等的运算符，则编译器将使用结合性来决定操作数是将运算符分组在其左侧还是右侧。\n\nThere are some ambiguities in this table. A pair of parentheses can mean a function call or a cast and in the table these are listed as `function()` and `cast()`; in your code you will simply use `()`. The `+` and `-` symbols are either used to indicate sign (unary plus and unary minus, given in the table as `+x` and `-x`), or addition and subtraction (given in the table as `+` and `-`). The `&` symbol means either \"take the address of\" (listed in the table as `&x`) or bitwise `AND` (listed in the table as `&`). Finally, the postfix increment and decrement operators (listed in the table as `x++ ` and `x--`) have a higher precedence than the prefix equivalents (listed as `++ x` and `--x`).\n\n| **优先级和结合性** | **运算符** |\n| Колибри1Колибри1：无关联性 | `::` |\n| **2**：从左到右的关联性 | `.`或`-> [] function() {} x++ x-- typeid const_cast dynamic_cast reinterpret_cast static_cast` |\n| **3**：从右到左的关联性 | `sizeof ++ x --x ~ ! -x +x &x * new delete cast()` |\n| **4**：从左到右的关联性 | `.*`或`->*` |\n| **5**：从左到右的关联性 | `* / %` |\n| **6**：从左到右的关联性 | `+ -` |\n| **7**：从左到右的关联性 | `<< >>` |\n| **8**：从左到右的关联性 | `< > <= >=` |\n| **9**：从左到右的关联性 | `== !=` |\n| **10**：从左到右关联性 | `&` |\n| **11**：从左到右关联性 | `^` |\n| **12**：从左到右关联性 | `&#124;` |\n| **13**：从左到右关联性 | `&&` |\n| **14**：从左到右关联性 | `&#124;&#124;` |\n| **15**：从右到左的关联性 | `? :` |\n| **16**：从右到左的关联性 | `= *= /= %= += -= <<= >>= &= &#124;= ^=` |\n| **17**：从右到左的关联性 | `throw` |\n| **18**：从左到右的关联性 | `,` |\n\n例如，看一下下面的代码：\n\n```cpp\n    int a = b + c * d;\n```\n\n这被解释为先执行乘法，然后执行加法。 编写相同代码的一种更清晰的方法是：\n\n```cpp\n    int a = b + (c * d);\n```\n\n原因是`*`的优先级高于`+`，所以先执行乘法，然后执行加法：\n\n```cpp\n    int a = b + c + d;\n```\n\n在这种情况下，`+`运算符具有相同的优先级，高于赋值的优先级。 由于`+`具有从左到右的关联性，因此该语句的解释如下：\n\n```cpp\n    int a = ((b + c) + d);\n```\n\n也就是说，第一个操作是将`b`和`c`相加，结果与`d`相加，该结果用于分配`a`。 这看起来可能并不重要，但请记住，加法可以在函数调用之间进行(函数调用的优先级高于`+`)：\n\n```cpp\n    int a = b() + c() + d();\n```\n\n这意味着按照`b`、`c`、`d`的顺序调用这三个函数，然后根据从左到右的关联性对它们的返回值求和。 这可能很重要，因为`d`可能取决于由其他两个函数更改的全局数据。\n\n如果您通过使用圆括号对表达式进行分组来显式指定优先级，则会使您的代码更具可读性，也更易于理解。 写入`b + (c * d)`可以立即明确哪个表达式首先执行，而`b + c * d`意味着您必须知道每个运算符的优先顺序。\n\n内置运算符是重载的，也就是说，无论操作数使用哪种内置类型，都使用相同的语法。 操作数必须是同一类型；如果使用不同的类型，编译器将执行一些默认转换，但在其他情况下(特别是在操作不同大小的类型时)，您必须执行强制转换以明确表示您的意思。 下一章将更详细地解释这一点。\n\n# 探索内置运算符\n\nC++ 附带了广泛的内置运算符；大多数是算术或逻辑运算符，本节将介绍这些运算符。 强制转换操作符将在下一章中介绍；内存操作符将在[第 4 章](04.html)、*使用内存、数组和指针*中介绍，与对象相关的操作符将在[第 6 章](06.html)、*类*中介绍。\n\n# 算术运算符\n\n算术运算符`+`、`-`、`/`、`*`和`%`可能只需要除法和模运算符的解释。 除了只能与整数类型一起使用的`%`之外，所有这些运算符都作用于整数和实数类型。 如果您混合了这两种类型(例如，将整数加到浮点数上)，则编译器将执行自动转换，如下一章所述。 除法运算符`/`的行为与您对浮点变量的预期一样：它产生两个操作数的除法结果。 当您在两个整数`a / b`之间执行除法时，结果是被除数(`a`)中除数(`b`)的整数。 除法的余数由模数`%`得到。 因此，对于任何整数`b`(不是零)，可以说，整数`a`可以表示如下：\n\n```cpp\n    (a / b) * b + (a % b)\n```\n\n请注意，模运算符只能用于整数。 如果想要得到浮点除法的剩余部分，请使用标准函数`std:;remainder`。\n\n使用整数除法时要小心，因为小数部分会被丢弃。 如果需要小数部分，则可能需要将数字显式转换为实数。 例如：\n\n```cpp\n    int height = 480; \n    int width = 640; \n    float aspect_ratio = width / height;\n```\n\n这会在本应为`1.3333`(或`4 : 3`)的情况下提供`1`的纵横比。 要确保执行浮点除法，而不是整数除法，您可以将被除数或除数任意(或两者)转换为浮点数，如下一章所述。\n\n# 增量和减量运算符\n\n这些运算符有两个版本，前缀和后缀。 顾名思义，前缀意味着运算符放在操作数的左边(例如，`++ i`)，后缀运算符放在右边(`i++ `)。 `++ `运算符将递增操作数，`--`运算符将递减它。 前缀运算符的意思是“在运算之后返回值*”，后缀运算符的意思是“在*运算之前返回值*”。 因此，下面的代码将递增一个变量，并使用它为另一个变量赋值：*\n\n```cpp\n    a = ++ b;\n```\n\n这里，使用前缀运算符，因此变量`b`递增，并将变量`a`赋给`b`递增后的值。 表达这一点的另一种方式是：\n\n```cpp\n    a = (b = b + 1);\n```\n\n下面的代码使用后缀运算符赋值：\n\n```cpp\n    a = b++ ;\n```\n\n这意味着变量`b`递增，但变量`a`被赋给`b`递增之前的值。 表达这一点的另一种方式是：\n\n```cpp\n    int t; \n    a = (t = b, b = b + 1, t);\n```\n\nNote that this statement uses the comma operator, so `a` is assigned to the temporary variable `t` in the right-most expression.\n\n递增和递减运算符可以应用于整数和浮点数。 运算符还可以应用于指针，因为它们有特殊的含义。 当您递增指针变量时，它意味着*将指针递增运算符*所指向的类型的大小。\n\n# 位运算符\n\n整数可视为一系列位`0`或`1`。 与其他操作数中相同位置的位相比，按位运算符对这些位起作用。 有符号整数使用一位来表示符号，但按位运算符作用于整数中的每一位，因此通常只对无符号整数使用位运算符。 在下面，所有类型都被标记为`unsigned`，因此它们被视为没有符号位。\n\n`&`运算符是按位 AND 的，这意味着左操作数中的每一位都与右操作数中相同位置的位进行比较。 如果两者都为 1，则同一位置的结果位将为 1；否则，结果位为 0：\n\n```cpp\n    unsigned int a = 0x0a0a; // this is the binary 0000101000001010 \n    unsigned int b = 0x00ff; // this is the binary 0000000000001111 \n    unsigned int c = a & b;  // this is the binary 0000000000001010 \n    std::cout << std::hex << std::showbase << c << std::endl;\n```\n\n在本例中，将按位`&`与`0x00ff`一起使用与提供掩码以屏蔽除最低字节之外的所有字节具有相同的效果。\n\n如果同一位置中的任一位或两位均为 1，则按位 OR 运算符`|`将返回值 1；仅当两位均为 0 时，才返回值 0：\n\n```cpp\n    unsigned int a = 0x0a0a; // this is the binary 0000101000001010 \n    unsigned int b = 0x00ff; // this is the binary 0000000000001111 \n    unsigned int c = a & b;  // this is the binary 0000101000001111 \n    std::cout << std::hex << std::showbase << c << std::endl;\n```\n\n`&`运算符的一个用途是查看是否设置了特定的位(或特定的位集合)：\n\n```cpp\n    unsigned int flags = 0x0a0a; // 0000101000001010 \n    unsigned int test = 0x00ff;  // 0000000000001111 \n\n    // 0000101000001111 is (flags & test) \n    if ((flags & test) == flags)  \n    { \n        // code for when all the flags bits are set in test \n    } \n    if ((flags & test) != 0) \n    { \n        // code for when some or all the flag bits are set in test  \n    }\n```\n\n`flags`变量具有我们需要的位，而`test`变量是我们正在检查的值。 值`(flags & test)`将仅包含也在`flags`中设置的前`test`个变量中的那些位。 因此，如果结果为非零，则意味着`test`中至少有一位也被设置在`flags`中；如果结果与第二个`flags`中的变量完全相同，则`flags`中的所有位都被设置在`test`中。\n\n异或运算符`^`用于测试比特何时不同；如果操作数中的比特不同，则结果比特为`1`，如果它们相同，则结果比特为`0`。 异或可用于翻转特定位：\n\n```cpp\n    int value = 0xf1; \n    int flags = 0x02; \n    int result = value ^ flags; // 0xf3 \n    std::cout << std::hex << result << std::endl;\n```\n\n最后的按位运算符是按位补码`~`。 此运算符应用于单个整数操作数，并返回一个值，其中每一位都是操作数中相应位的补码；因此，如果操作数位为 1，则结果中的位为 0；如果操作数中的位为 0，则结果中的位为 1。请注意，将检查所有位，因此需要注意整数的大小。\n\n# 布尔运算符\n\n`==`运算符测试两个值是否完全相同。 如果测试两个整数，则测试是显而易见的；例如，如果`x`是 2，`y`是 3，那么`x == y`显然是`false`。 然而，即使你这么认为，两个实数也可能不一样：\n\n```cpp\n    double x = 1.000001 * 1000000000000; \n    double y = 1000001000000; \n    if (x == y) std::cout << \"numbers are the same\";\n```\n\n`double`类型是一个 8 字节的浮点类型，但对于这里使用的精度来说，这是不够的；存储在`x`变量中的值是`1000000999999.9999`(小数点后四位)。\n\n`!=`运算符测试是否有两个值不为真。 运算符`>`和`<`测试两个值以查看左侧操作数是否大于或小于右侧操作数，`>=`运算符测试左侧操作数是否大于或等于右侧操作数，`<=`运算符测试左侧操作数是否小于或等于右侧操作数。 这些运算符可以在`if`语句中使用，类似于前面示例中使用`==`的方式。 使用运算符的表达式返回`bool`类型的值，因此您可以使用它们为布尔变量赋值：\n\n```cpp\n    int x = 10; \n    int y = 11; \n    bool b = (x > y); \n    if (b) std::cout << \"numbers same\"; \n    else   std::cout << \"numbers not same\";\n```\n\n赋值运算符(`=`)比大于(`>=`)运算符具有更高的优先级，但我们使用了圆括号来明确表示，值在用于赋值之前进行了测试。 您可以使用`!`运算符对逻辑值求反。 因此，使用前面获得的`b`值，您可以编写以下代码：\n\n```cpp\n    if (!b) std::cout << \"numbers not same\"; \n    else    std::cout << \"numbers same\";\n```\n\n可以使用`&&`(AND)和`||`(OR)运算符组合两个逻辑表达式。 带有`&&`运算符的表达式只有在两个操作数都是`true`时才为真，而带有`||`运算符的表达式只有在其中一个或两个操作数都是`true`时才是`true`：\n\n```cpp\n    int x = 10, y = 10, z = 9; \n    if ((x == y) || (y < z)) \n        std::cout << \"one or both are true\";\n```\n\n这段代码涉及三个测试；第一个测试`x`和`y`变量是否具有相同的值，第二个测试变量`y`是否小于`z`，然后进行一个测试，看看前两个测试中是否有一个或两个都是`true`。\n\n在这样的`||`表达式中，第一个操作数(`x==y`)是`true`，无论右操作数(这里是`y < z`)的值是什么，总的逻辑表达式都是`true`。 因此，测试第二个表达式是没有意义的。 相应地，在`&&`表达式中，如果第一个操作数是`false`，则整个表达式必须是`false`，因此不需要测试表达式的右侧部分。 编译器将提供代码来执行此*短路*：\n\n```cpp\n    if ((x != 0) && (0.5 > 1/x))  \n    { \n        // reciprocal is less than 0.5 \n    }\n```\n\n此代码测试`x`的倒数是否小于 0.5(或者相反，`x`大于 2)。 如果`x`变量的值为 0，则测试`1/x`是错误的，但在这种情况下，表达式将永远不会执行，因为`&&`的左操作数是`false`。\n\n# 按位移位运算符\n\n按位移位运算符按指定方向将左操作数整数中的位移位右操作数中给定的指定位数。 向左移位 1 位将数字乘以 2，向右移位 1 位将除以 2。以下是 2 字节整数的移位：\n\n```cpp\n    unsigned short s1 = 0x0010; \n    unsigned short s2 = s1 << 8; \n    std::cout << std::hex << std::showbase; \n    std::cout << s2 << std::endl; \n    // 0x1000  \n    s2 = s2 << 3; \n    std::cout << s2 << std::endl; \n    // 0x8000\n```\n\n在本例中，`s1`变量设置了第五位(`0x0010`或 16)。 第二个`s2`变量有这个值，向左移位 8 位，因此单个位被移位到第 13 位，底部 8 位全部设置为 0(`0x10000`或 4,096)。 这意味着`0x0010`已乘以 2<sup>8</sup>，或 256，得到`0x1000`。 接下来，将该值再左移 3 位，结果为`0x8000`；设置最高位。\n\n运算符将丢弃所有溢出的位，因此如果您设置了顶部位并将整数左移一位，则该顶部位将被丢弃：\n\n```cpp\n    s2 = s2 << 1; \n    std::cout << s2 << std::endl; \n    // 0\n```\n\n最后向左移位一位将得到值 0。\n\n重要的是要记住，当与流一起使用时，运算符`<<`意味着*插入到流*中，而当与整数一起使用时，它意味着*位移位*。\n\n# 赋值运算符\n\n赋值运算符`=`在左边赋值(变量)，在右边赋值结果(变量或表达式)：\n\n```cpp\n    int x = 10; \n    x = x + 10;\n```\n\n第一行声明一个整数并将其初始化为 10。第二行通过在变量上再加上 10 来改变变量，因此现在变量`x`的值为 20。这是赋值。 C++ 允许您使用简短语法根据变量值更改变量值。 前面的几行可以写成如下：\n\n```cpp\n    int x = 10; \n    x += 10;\n```\n\n这样的递增运算符(和递减运算符)可以应用于整数和浮点类型。 如果运算符应用于指针，则操作数指示指针更改了多少整项地址。 例如，如果`int`是 4 字节，而您将`10`加到`int`指针上，则实际指针值会递增 40(10 乘以 4 字节)。\n\n除了递增(`+=`)和递减(`-=`)赋值之外，还可以对乘法(`*=`)、除法(`/=`)和余数(`%=`)赋值。 除了最后一个(`%=`)，所有这些都可以用于浮点类型和整数。 余数赋值只能用于整数。\n\n您还可以对整数执行按位赋值操作：左移位(`<<=`)、右移位(`>>=`)、按位 AND(`&=`)、按位 OR(`|=`)和按位异或(`^=`)。 通常只有将这些应用于无符号整数才有意义。 因此，乘以 8 可以由这两行执行：\n\n```cpp\n    i *= 8; \n    i <<= 3;\n```\n\n# 控制执行流程\n\nC++ 提供了多种方法来测试值和遍历代码。\n\n# 使用条件语句\n\n最常用的条件语句是`if`。 在其最简单的形式中，`if`语句采用一对圆括号中的逻辑表达式，后面紧跟条件为`true`时执行的语句：\n\n```cpp\n    int i; \n    std::cin >> i; \n    if (i > 10) std::cout << \"much too high!\" << std::endl;\n```\n\n当条件为`false`时，还可以使用`else`语句捕捉情况：\n\n```cpp\n    int i; \n    std::cin >> i; \n    if (i > 10) std::cout << \"much too high!\" << std::endl; \n    else        std::cout << \"within range\" << std::endl;\n```\n\n如果要执行多个语句，可以使用大括号(`{}`)来定义代码块。\n\n条件是一个逻辑表达式，C++ 将从数值类型转换为`bool`，其中 0 是`false`，任何不是 0 的都是`true`。 如果您不小心，这可能是错误的来源，不仅很难注意到，而且还可能产生意想不到的副作用。 考虑以下代码，它要求从控制台输入，然后测试用户是否输入-1：\n\n```cpp\n    int i; \n    std::cin >> i; \n    if (i == -1) std::cout << \"typed -1\" << endl; \n    std::cout << \"i = \" << i << endl;\n```\n\n这是人为设计的，但您可能会要求循环中的值，然后对这些值执行操作，除非用户输入-1，此时循环结束。 如果键入错误，可能会出现以下代码：\n\n```cpp\n    int i; \n    std::cin >> i; \n    if (i = -1) std::cout << \"typed -1\" << endl; \n    std::cout << \"i = \" << i << endl;\n```\n\n在这种情况下，使用赋值运算符(`=`)代替*相等*运算符(`==`)。 只有一个字符的区别，但是这段代码仍然是正确的 C++，编译器很乐意编译它。\n\n结果是，无论您在控制台输入什么，变量`i`都被赋给-1，由于-1 不是零，所以`if`语句中的条件是`true`，因此执行该语句的 TRUE 子句。 由于变量被赋值为-1，这可能会进一步改变代码中的逻辑。 避免这个错误的方法是利用赋值时左边必须是左值的要求。 按如下方式执行测试：\n\n```cpp\n    if (-1 == i) std::cout << \"typed -1\" << endl;\n```\n\n这里，逻辑表达式是`(-1 == i)`，由于`==`运算符是可交换的(操作数的顺序无关紧要；您会得到相同的结果)，这与您在前面的测试中的意图完全相同。 但是，如果您键入了错误的运算符，则会得到以下结果：\n\n```cpp\n    if (-1 = i) std::cout << \"typed -1\" << endl;\n```\n\n在这种情况下，赋值在左侧有一个右值，这将导致编译器发出错误(在 Visual C++ 中，这是`C2106 '=' : left operand must be l-value`)。\n\n允许在`if`语句中声明变量，并且变量的作用域在语句块中。 例如，可以按如下方式调用返回整数的函数：\n\n```cpp\n    if (int i = getValue()) {    \n        // i != 0    // can use i here  \n    } else {    \n        // i == 0    // can use i here  \n    }\n```\n\n虽然这是完全合法的 C++，但您没有什么理由要这样做。\n\n在某些情况下，可以使用条件运算符`?:`代替`if`语句。 运算符执行`?`运算符左侧的表达式，如果条件表达式为`true`，则执行`?`右侧的表达式。 如果条件表达式为`false`，则执行`:`右侧的表达式。 运算符执行的表达式提供条件运算符的返回值。\n\n例如，下面的代码确定两个变量`a`和`b`中的最大值：\n\n```cpp\n    int max; \n    if (a > b) max = a; \n    else       max = b;\n```\n\n这可以用以下一条语句来表达：\n\n```cpp\n    int max = (a > b) ? a : b;\n```\n\n主要的选择是代码中哪一个是最具可读性的。 显然，如果赋值表达式很大，最好在`if`语句中将它们分成几行。 但是，在其他语句中使用条件语句很有用。 例如：\n\n```cpp\n    int number;  \n    std::cin  >> number; \n    std::cout << \"there \" \n              << ((number == 1) ? \"is \" : \"are \")  \n              << number << \" item\"            \n              << ((number == 1) ? \"\" : \"s\") \n              << std::endl;\n```\n\n此代码确定变量`number`是否为 1，如果是，则在控制台`there is 1 item`上打印。 这是因为在这两个条件中，如果第一个`number`变量的值为 1，则测试为`true`，并使用第一个表达式。 请注意，整个运算符周围有一对圆括号。 原因是流`<<`运算符是重载的，您希望编译器选择接受字符串的版本，它是运算符返回的类型，而不是`bool`，它是表达式`(number == 1)`的类型。\n\n如果条件运算符返回的值是左值，则可以在赋值的左侧使用它。 这意味着您可以编写以下相当奇怪的代码：\n\n```cpp\n    int i = 10, j = 0; \n    ((i < j) ? i : j) = 7; \n    // i is 10, j is 7 \n\n    i = 0, j = 10; \n    ((i < j) ? i : j) = 7; \n    // i is 7, j is 10\n```\n\n条件运算符检查`i`是否小于`j`，如果小于`j`，则为`i`赋值；否则，为`j`赋值。 这段代码很简洁，但缺乏可读性。 在这种情况下，使用`if`语句要好得多。\n\n# 选择\n\n如果您想测试一个变量是否是几个值中的一个，那么使用多个`if`语句就会变得很麻烦。 C++ `switch`语句更好地实现了这一目的。 基本语法如下所示：\n\n```cpp\n    int i; \n    std::cin >> i; \n    switch(i) \n    { \n        case 1:  \n            std::cout << \"one\" << std::endl; \n            break; \n        case 2:  \n            std::cout << \"two\" << std::endl; \n            break; \n        default: \n            std::cout << \"other\" << std::endl; \n    }\n```\n\n如果所选变量是指定值，则每个`case`实质上是关于要运行的特定代码的标签。 `default`子句用于不存在`case`的值。 您不必有`default`子句，这意味着您只针对指定的情况进行测试。 `default`子句可以用于最常见的情况(在这种情况下，案例过滤掉不太可能的值)，也可以用于例外的值(在这种情况下，案例处理最可能的值)。\n\n`switch`语句只能测试整数类型(包括`enum`)，并且只能测试常量。 `char`类型是整数，这意味着您可以在`case`项中使用字符，但只能使用单个字符；您不能使用字符串：\n\n```cpp\n    char c; \n    std::cin >> c; \n    switch(c) \n    { \n        case 'a':  \n            std::cout << \"character a\" << std::endl; \n            break; \n        case 'z':   \n            std::cout << \"character z\" << std::endl; \n            break; \n        default: \n            std::cout << \"other character\" << std::endl; \n    }\n```\n\n`break`语句指示为`case`执行的语句的结束。 如果不指定，执行将通过执行*，并且将执行以下`case`语句，即使它们已为不同的情况指定：*\n\n```cpp\n    switch(i) \n    { \n        case 1:  \n            std::cout << \"one\" << std::endl; \n            // fall thru \n        case 2:  \n            std::cout << \"less than three\" << std::endl; \n            break; \n        case 3:  \n            std::cout << \"three\" << std::endl; \n            break; \n        case 4: \n            break; \n            default: \n            std::cout << \"other\" << std::endl; \n    }\n```\n\n此代码显示了`break`语句的重要性。 值 1 将把`one`和`less than three`都打印到控制台，因为执行*通过*到前面的`case`，即使那个`case`是针对另一个值的。\n\n对于不同的情况，通常会有不同的代码，因此最常见的情况是用`break`来结束`case`。 很容易错误地错过`break`，这将导致异常行为。 在故意遗漏`break`语句时记录代码是一种很好的做法，这样您就知道如果遗漏了`break`，很可能是一个错误。\n\n您可以为每个`case`提供零条或多条语句。 如果有多条语句，则会针对该特定情况执行所有语句。 如果没有提供语句(如本例中的`case 4`)，则意味着不会执行任何语句，即使是`default`子句中的语句也不会执行。\n\n`break`语句表示*脱离此代码块*，它在 LOOP 语句`while`和`for`中的行为也是这样的。 还有其他方法可以让你突破`switch`。 `case`可以调用`return`来完成声明了`switch`的函数；它可以调用`goto`来跳转到标签，或者可以调用`throw`来抛出异常，该异常将被异常处理程序捕获到`switch`之外，甚至在函数之外。\n\n到目前为止，这些案例都是按数字顺序排列的。 这不是必需的，但它确实使代码更具可读性，显然，如果您希望*跳过*前`case`语句(如这里的`case 1`)，则应该注意`case`项的顺序。\n\n如果需要在`case`处理程序中声明临时变量，则必须使用大括号定义代码块，这将使变量的范围仅局限于该代码块。 当然，您可以在任何`case`处理程序中使用在`switch`语句外部声明的任何变量。\n\n由于枚举常量是整数，因此可以在`switch`语句中测试`enum`：\n\n```cpp\n    enum suits { clubs, diamonds, hearts, spades }; \n\n    void print_name(suits card) \n    { \n        switch(card) \n        { \n            case suits::clubs: \n                std::cout << \"card is a club\"; \n                break; \n            default: \n                std::cout << \"card is not a club\"; \n        } \n    }\n```\n\n虽然这里的`enum`没有限定作用域(它既不是`enum class`也不是`enum struct`)，但它不需要在`case`中指定值的作用域，但它使代码更明显地说明了常量所指的是什么。\n\n# 环绕 / 绕行 / 打环 / 翻筋斗\n\n大多数程序都需要遍历一些代码。 C++ 提供了几种方法来实现这一点，要么使用索引值迭代，要么测试逻辑条件。\n\n# 使用迭代进行循环\n\n`for`语句有两个版本，迭代和基于范围。 后者是在 C++ 11 中引入的。迭代版本的格式如下：\n\n```cpp\n    for (init_expression; condition; loop_expression) \n        loop_statement;\n```\n\n您可以提供一个或多个循环语句，对于多个语句，应该使用大括号提供代码块。 循环的目的可能由 LOOP 表达式实现，在这种情况下，您可能不希望执行 LOOP 语句；在这里，您使用 NULL 语句`;`，这意味着*什么都不做*。\n\n括号内有三个用分号分隔的表达式。 第一个表达式允许您声明和初始化循环变量。 此变量的作用域为`for`语句，因此只能在`for`表达式或后面的循环语句中使用它。 如果需要多个循环变量，可以在此表达式中使用逗号运算符声明它们。\n\n当条件表达式为`true`时，`for`语句将循环；因此，如果使用循环变量，则可以使用此表达式检查循环变量的值。 在调用 LOOP 语句之后，在循环结束时调用第三个表达式；在此之后，调用条件表达式以确定循环是否应该继续。 最后一个表达式通常用于更新循环变量的值。 例如：\n\n```cpp\n    for (int i = 0; i < 10; ++ i)   \n    { \n        std::cout << i; \n    }\n```\n\n在此代码中，循环变量为`i`，并将其初始化为零。 接下来，检查条件，由于`i`将小于 10，因此将执行该语句(将值打印到控制台)。 下一个动作是循环表达式；调用`++ i`，它递增循环变量`i`，然后检查条件，依此类推。 由于条件为`i < 10`，这意味着该循环将运行 10 次，值`i`介于 0 和 9 之间(因此您将在控制台上看到 0123456789)。\n\n循环表达式可以是您喜欢的任何表达式，但通常它会递增或递减一个值。 您不必将循环变量值更改为 1；例如，您可以使用`i -= 5`作为循环表达式，在每个循环中将变量减去 5。 循环变量可以是您喜欢的任何类型；它不必是整数，甚至不必是数字(例如，它可以是指针，也可以是[章](08.html)，*中描述的使用标准库容器*的**迭代器对象**)，并且条件和循环表达式不必使用循环变量。 事实上，您根本不需要声明循环变量！\n\n如果不提供循环条件，则循环将是无限的，除非您在循环中提供检查：\n\n```cpp\nfor (int i = 0; ; ++ i)  \n{ \n   std::cout << i << std::endl; \n   if (i == 10) break; \n}\n```\n\n这使用了前面在`switch`语句中介绍的`break`语句。 它指示执行退出`for`循环，您也可以使用`return`、`goto`或`throw`。 您很少看到使用`goto`结束的语句；但是，您可能会看到以下内容：\n\n```cpp\nfor (;;)  \n{ \n   // code \n}\n```\n\n在这种情况下，没有循环变量，没有循环表达式，也没有条件。 这是一个永恒的循环，循环中的代码决定循环何时结束。\n\n`for`语句中的第三个表达式，循环表达式，可以是您喜欢的任何东西；唯一的属性是它在循环结束时执行。 您可以选择更改此表达式中的另一个变量，甚至可以提供几个用逗号操作符分隔的表达式。 例如，如果您有两个函数，一个名为`poll_data`的函数在有更多数据可用时返回`true`，当没有更多数据时返回`false`，另一个名为`get_data`的函数返回下一个可用数据项，您可以按如下方式使用`for`(请记住，这是一个人为的示例，目的是为了说明问题)：\n\n```cpp\nfor (int i = -1; poll_data(); i = get_data()) \n{ \n   if (i != -1) std::cout << i << std::endl; \n}\n```\n\n当`poll_data`返回`false`值时，循环将结束。 需要`if`语句，因为第一次调用循环时尚未调用`get_data`。 更好的版本如下：\n\n```cpp\nfor (; poll_data() ;) \n{ \n   int i = get_data();  \n   std::cout << i << std::endl; \n}\n```\n\n在下一节中，请牢记此示例。\n\n还可以在`for`循环中使用另一个关键字。 在许多情况下，您的`for`循环将有多行代码，在某个时刻，您可能会决定当前循环已经完成，并且希望开始下一个循环(或者，更具体地说，执行循环表达式，然后测试条件)。 为此，您可以调用`continue`：\n\n```cpp\nfor (float divisor = 0.f; divisor < 10.f; ++ divisor)  \n{ \n   std::cout << divisor; \n   if (divisor == 0)  \n   {  \n      std::cout << std::endl; \n      continue; \n   } \n   std::cout << \" \" << (1 / divisor) << std::endl; \n}\n```\n\n在这段代码中，我们打印数字 0 到 9 的倒数(`0.f`是一个 4 字节的浮点文字)。 `for`循环中的第一行打印循环变量，下一行检查变量是否为零。 如果是，则打印新行并继续，即不执行`for`循环中的最后一行。 原因是最后一行打印的是倒数，将任何数字除以零都是错误的。\n\nC++ 11 引入了另一种使用`for`循环的方法，该循环旨在与容器一起使用。 C++ 标准库包含容器类的**模板**。 这些类包含对象集合，并以标准方式提供对这些项的访问。 标准方法是使用**迭代器**对象遍历集合。 关于如何做到这一点的更多细节将在[章](08.html)，*使用标准库容器*中给出；语法要求理解指针和迭代器，所以我们不在这里讨论它们。 基于范围的`for`循环提供了一种简单的机制来访问容器中的项，而无需显式使用迭代器。\n\n语法很简单：\n\n```cpp\nfor (for_declaration : expression) loop_statement;\n```\n\n首先要指出的是，只有两个表达式，它们之间用冒号分隔(`:`)。 第一个表达式用于声明循环变量，该变量属于要循环访问的集合中的项的类型。 第二个表达式提供对集合的访问。\n\nIn C++ terms, the collections that can be used are those that define a `begin` and `end` function that gives access to iterators, and also to stack-based arrays (that the compiler knows the size of).\n\n标准库定义了一个名为`vector`的容器对象。 `vector`模板是一个包含尖括号(`<>`)中指定类型的项的类；在下面的代码中，`vector`以一种 C++ 11 中新的特殊方式初始化，称为**列表初始化**。 此语法允许您在大括号之间的列表中指定向量的初始值。 下面的代码创建并初始化`vector`，然后使用迭代`for`循环打印出所有值：\n\n```cpp\nusing namespace std; \nvector<string> beatles = { \"John\", \"Paul\", \"George\", \"Ringo\" }; \n\nfor (int i = 0; i < beatles.size(); ++ i)  \n{ \n   cout << beatles.at(i) << endl; \n}\n```\n\nHere a `using` statement is used so that the classes `vector` and `string` do not have to be used with fully qualified names.\n\n`vector`类有一个名为`size`的成员函数(通过`.`运算符调用，意思是“在此对象上调用此函数”)，它返回`vector`中的项目数。 使用传递项目索引的`at`函数访问每个项目。 这段代码的一个大问题是它使用随机访问，也就是说，它使用每个项目的索引来访问每个项目。 这是`vector`的一个属性，但其他标准库容器类型没有随机访问权限。 下面使用基于范围的`for`：\n\n```cpp\nvector<string> beatles = { \"John\", \"Paul\", \"George\", \"Ringo\" }; \n\nfor (string musician : beatles)  \n{ \n   cout << musician << endl; \n}\n```\n\n此语法适用于任何标准容器类型和堆栈上分配的数组：\n\n```cpp\nint birth_years[] = { 1940, 1942, 1943, 1940 }; \n\nfor (int birth_year : birth_years)  \n{ \n   cout << birth_year << endl; \n}\n```\n\n在这种情况下，编译器知道数组的大小(因为编译器已经分配了数组)，因此它可以确定范围。 基于范围的`for`循环将遍历容器中的所有项，但与前一个版本一样，您可以使用`break`、`return`、`throw`或`goto`离开`for`循环，并且可以使用`continue`语句指示应该执行下一个循环。\n\n# 条件循环\n\n在上一节中，我们给出了一个人为的示例，其中`for`循环中的条件轮询数据：\n\n```cpp\nfor (; poll_data() ;) \n{ \n   int i = get_data();  \n   std::cout << i << std::endl; \n}\n```\n\n在本例中，条件中没有使用循环变量。 这是`while`条件循环的候选项：\n\n```cpp\nwhile (poll_data()) \n{ \n   int i = get_data();  \n   std::cout << i << std::endl; \n}\n```\n\n该语句将继续循环，直到表达式(本例中为`poll_data`)的值为`false`。 与`for`一样，可以使用`break`、`return`、`throw`或`goto`退出`while`循环，并且可以使用`continue`语句指示应该执行下一个循环。\n\n第一次调用`while`语句时，会在执行循环之前测试条件；在某些情况下，您可能希望循环至少执行一次，然后测试条件(很可能取决于循环中的操作)，以查看是否应该重复循环。 执行此操作的方法是使用`do-while`循环：\n\n```cpp\nint i = 5; \ndo \n{ \n   std::cout << i-- << std::endl; \n} while (i > 0);\n```\n\n注意`while`子句后面的分号。 这是必需的。\n\n此循环将以相反的顺序打印 5 比 1。 原因是循环开始时`i`被初始化为 5。循环中的语句通过后缀运算符递减变量，这意味着递减之前的值被传递给流。 在循环结束时，`while`子句测试变量是否大于零。 如果此测试为`true`，则循环重复。 在将`i`赋值为 1 的情况下调用循环时，会将值 1 打印到控制台，并将变量减为零，`while`子句将测试一个为`false`的表达式，循环将结束。\n\n这两种类型的循环的不同之处在于，在`while`循环中执行循环之前会测试条件，因此可能不会执行该循环。 在`do-while`循环中，条件在循环之后调用，这意味着对于`do-while`循环，LOOP 语句始终至少被调用一次。\n\n# 跳;跃;跳跃;跳过;跃过;跨越;快速移动;突然移动\n\nC++ 支持跳转，并且在大多数情况下，有更好的方法来分支代码；然而，为了完整性，我们将在这里讨论该机制。 跳转有两个部分：要跳转到的标记语句和`goto`语句。 标签的命名规则与变量相同；声明时使用冒号作为后缀，并且必须在语句之前。 使用标签的名称调用`goto`语句：\n\n```cpp\n    int main() \n    { \n        for (int i = 0; i < 10; ++ i) \n        { \n            std::cout << i << std::endl; \n            if (i == 5) goto end; \n        } \n\n    end:\n        std::cout << \"end\"; \n    }\n```\n\n标签必须与调用`goto`具有相同的功能。\n\n跳转很少使用，因为它们鼓励您编写非结构化代码。 但是，如果您的例程包含高度嵌套的循环或`if`语句，那么使用`goto`跳转来清理代码可能更有意义，可读性也更好。\n\n# 使用 C++ 语言功能\n\n现在，让我们使用您在本章中学到的功能来编写应用。 本例是一个简单的命令行计算器；您键入一个表达式，如*6*7*，应用将解析输入并执行计算。\n\n启动 Visual C++ 并单击文件菜单，然后单击新建，最后单击新建文件...选项以获得新建文件对话框。 在左侧窗格中，单击 Visual C++，在中间窗格中，单击 C++ 文件(.cpp)，然后单击打开按钮。 在执行任何其他操作之前，请保存此文件。 使用 Visual C++ 控制台(具有 Visual C++ 环境的命令行)导航到您在上一章中创建的`Beginning_C++ `文件夹，并创建一个名为`Chapter_02`的新文件夹。 现在，在 Visual C++ 中的文件菜单上，单击将 Source1.cpp 另存为...。 并在另存文件为对话框中找到您刚刚创建的`Chapter_02`文件夹。 在文件名框中，键入 calc.cpp，然后单击保存按钮。\n\n应用将使用`std::cout`和`std::string`；因此，在文件的顶部，添加定义这两个名称的标头，并添加一条`using`语句，这样您就不必使用完全限定名称：\n\n```cpp\n    #include <iostream> \n    #include <string> \n\n    using namespace std;\n```\n\n您将通过命令行传递表达式，因此在文件底部添加一个接受命令行参数的`main`函数：\n\n```cpp\n    int main(int argc, char *argv[]) \n    { \n    }\n```\n\n应用处理`arg1 op arg2`形式的表达式，其中`op`是运算符，`arg1`和`arg2`是参数。 这意味着，当调用应用时，它必须有四个参数；第一个参数是用于启动应用的命令，后三个参数是表达式。 `main`函数中的第一个代码应确保提供正确数量的参数，因此在此函数的顶部添加一个条件，如下所示：\n\n```cpp\n    if (argc != 4) \n    { \n        usage(); \n        return 1; \n    }\n```\n\n如果使用多于或少于四个参数调用命令，则调用函数`usage`，然后返回`main`函数，停止应用。\n\n在`main`函数之前添加`usage`函数，如下所示：\n\n```cpp\n    void usage() \n    { \n        cout << endl; \n        cout << \"calc arg1 op arg2\" << endl; \n        cout << \"arg1 and arg2 are the arguments\" << endl; \n        cout << \"op is an operator, one of + - / or *\" << endl; \n    }\n```\n\n这里简单地解释了如何使用该命令，并解释了参数。 此时，您可以编译应用了。 由于您使用的是 C++ 标准库，因此编译时需要支持 C++ 异常，因此在命令行中键入以下内容：\n\n```cpp\nC:\\Beginning_C++ Chapter_02\\cl /EHsc calc.cpp\n```\n\n如果您键入的代码没有任何错误，文件应该会编译。 如果从编译器获得任何错误，请检查源文件以查看代码是否与前面的代码完全相同。 您可能会收到以下错误：\n\n```cpp\n'cl' is not recognized as an internal or external command,  \noperable program or batch file.\n```\n\n这意味着控制台未设置为 Visual C++ 环境，因此要么将其关闭，然后通过 Windows 开始菜单启动控制台，要么运行 vcvarsall.bat 批处理文件。 上一章给出了完成这两项任务的步骤。\n\n一旦代码编译完毕，您就可以运行它了。 首先使用正确的参数数(例如，`calc 6 * 7`)运行它，然后尝试使用错误的参数数(例如，`calc 6 * 7 / 3`)。 请注意，参数之间的空格很重要：\n\n```cpp\nC:\\Beginning_C++ Chapter_02>calc 6 * 7 \n\nC:\\Beginning_C++ Chapter_02>calc 6 * 7 / 3 \n\ncalc arg1 op arg2 \narg1 and arg2 are the arguments \nop is an operator, one of + - / or *\n```\n\n在第一种情况下，应用不执行任何操作，因此您看到的只是一个空行。 在第二个示例中，代码确定没有足够的参数，因此它将使用信息打印到控制台。\n\n接下来，您需要对参数进行一些简单的解析，以检查用户是否传递了有效值。 在`main`函数的底部添加以下内容：\n\n```cpp\n    string opArg = argv[2]; \n    if (opArg.length() > 1) \n    { \n        cout << endl << \"operator should be a single character\" << endl; \n        usage(); \n        return 1; \n    }\n```\n\n第一行使用第三个命令行参数初始化 C++ `std::string`对象，该参数应该是表达式中的运算符。 这个简单的示例只允许操作符使用单个字符，因此后续行检查以确保操作符是单个字符。 C++ `std::string`类有一个名为`length`的成员函数，它返回字符串中的字符数。\n\n`argv[2]`参数的长度至少为一个字符(没有长度的参数不会被视为命令行参数！)，因此我们必须检查用户键入的操作符是否超过一个字符。\n\n接下来，您需要测试以确保该参数是允许的受限集之一，如果用户键入另一个操作符，则打印错误并停止处理。 在`main`函数的底部添加以下内容：\n\n```cpp\n    char op = opArg.at(0); \n    if (op == 44 || op == 46 || op < 42 || op > 47) \n    { \n        cout << endl << \"operator not recognized\" << endl; \n        usage(); \n        return 1; \n    }\n```\n\n测试将在一个字符上进行，因此您需要从`string`对象中提取该字符。 此代码使用`at`函数，该函数将传递所需字符的索引。 ([第 8 章](08.html)，*使用标准库容器*将提供有关`std::string`类成员的更多详细信息。)。 下一行检查该字符是否不受支持。 代码依赖于我们支持的字符的下列值：\n\n| **字符** | **值** |\n| `+` | `42` |\n| `*` | `43` |\n| `-` | `45` |\n| `/` | `47` |\n\n正如您所看到的，如果字符小于`42`或大于`47`，它将是不正确的，但是在`42`和`47`之间，还有两个我们也想拒绝的字符：`,`(`44`)和`.`(`46`)。 这就是为什么我们有前面的条件：“如果字符小于 42 或大于`47`，或者它是`44`或`46`，则拒绝它。”\n\n`char`数据类型是整数，这就是测试使用整数文字的原因。 您可以使用字符文字，因此以下更改同样有效：\n\n```cpp\n if (op == ',' || op == '.' || op < '+' || op > '/') \n    { \n        cout << endl << \"operator not recognized\" << endl; \n        usage(); \n        return 1; \n    }\n```\n\n你应该使用你认为最易读的任何一个。 由于检查一个字符是否*大于另一个字符*意义不大，本书将使用前者。\n\n此时，您可以编译代码并对其进行测试。 首先尝试使用多于一个字符的运算符(例如，`**`)，并确认您收到的消息是运算符应该是单个字符。 其次，使用不是可识别操作符的字符进行测试；尝试除`+`、`*`、`-`或`/`之外的任何字符，但也值得尝试`.`和`,`。\n\n请记住，命令提示符对某些符号有特殊操作，如“`&`”和“`|`”，甚至在调用代码之前，命令提示符可能会通过解析命令行给您一个错误。\n\n接下来要做的是将参数转换为代码可以使用的形式。 命令行参数以字符串数组的形式传递给程序；但是，我们将其中一些参数解释为浮点数(实际上是双精度浮点数)。 C 运行时提供了一个名为`atof`的函数，该函数可通过 C++ 标准库获得(在本例中，`<iostream>`包括包含`<cmath>`的文件，其中声明了`atof`)。\n\nIt is a bit counter-intuitive to get access to a math function such as `atof` through including a file associated with stream input and output. If this makes you uneasy, you can add a line after the `include` lines to include the `<cmath>` file. As mentioned in the previous chapter, the C++ Standard Library headers have been written to ensure that a header file is only included once, so including `<cmath>` twice has no ill effect. This was not done in the preceding code, because it was argued that `atof` is a string function and the code includes the `<string>` header and, indeed, `<cmath>` is included via the files the `<string>` header includes.\n\n将以下行添加到`main`函数的底部。 前两行将第二个和第四个参数(记住，C++ 数组是从零开始编制索引的)转换为`double`值。 最后一行声明一个变量来保存结果：\n\n```cpp\n    double arg1 = atof(argv[1]); \n    double arg2 = atof(argv[3]); \n    double result = 0;\n```\n\n现在我们需要确定传递了哪个操作符并执行请求的操作。 我们将使用`switch`语句完成此操作。 我们知道`op`变量将是有效的，因此我们不必提供`default`子句来捕获我们没有测试过的值。 在函数的底部添加一条`switch`语句：\n\n```cpp\n    double arg1 = atof(argv[1]); \n    double arg2 = atof(argv[3]); \n    double result = 0; \n\n    switch(op) \n    { \n    }\n```\n\n前三种情况`+`、`-`和`*`很简单：\n\n```cpp\n    switch (op) \n    { \n case '+': result = arg1 + arg2; break; case '-': result = arg1 - arg2; break; case '*': result = arg1 * arg2; break; \n    }\n```\n\n同样，由于`char`是整数，您可以在`switch`语句中使用它，但 C++ 允许您检查字符值。 在这种情况下，使用字符而不是数字会使代码更具可读性。\n\n在`switch`之后，添加最终代码以打印结果：\n\n```cpp\n    cout << endl; \n    cout << arg1 << \" \" << op << \" \" << arg2; \n    cout << \" = \" << result << endl;\n```\n\n现在可以编译代码并使用涉及`+`、`-`和`*`的计算对其进行测试。\n\n除法是个问题，因为被零除是无效的。 要测试这一点，请将以下行添加到`switch`的底部：\n\n```cpp\n case '/': result = arg1 / arg2; break;\n```\n\n编译并运行代码，将零作为最后一个参数：\n\n```cpp\nC:\\Beginning_C++ Chapter_02>calc 1 / 0 \n1 / 0 = inf\n```\n\n代码成功运行，并打印出表达式，但它显示结果是`inf`的奇数值。 这是怎么回事？\n\n除以零将`result`赋给`NAN`的值，该值是`<math.h>`中定义的常量(通过`<cmath>`包含)，意思是“不是一个数字”。 `cout`对象的插入操作符的`double`重载测试数字是否具有有效值，如果数字具有值`NAN`，则打印字符串 inf。 在我们的应用中，我们可以测试零因子，并将传递零的用户操作视为错误。 因此，请将代码更改为如下所示：\n\n```cpp\n    case '/': \n if (arg2 == 0) { cout << endl << \"divide by zero!\" << endl; return 1; } else { \n        result = arg1 / arg2; \n } \n    break;\n```\n\n现在，当用户传递零作为除数时，您将得到一条`divide by zero!`消息。\n\n现在您可以编译完整的示例并对其进行测试。 该应用支持使用`+`、`-`、`*`和`/`运算符的浮点运算，并将处理除以零的情况。\n\n# 简略的 / 概括的 / 简易判罪的 / 简易的\n\n在本章中，您学习了如何格式化代码，以及如何标识表达式和语句。 您已经了解了如何标识变量的作用域，以及如何将函数和变量的集合分组到命名空间中，以便防止名称冲突。 您还学习了 C++ 中循环和分支代码的基本知识，以及内置操作符的工作方式。 最后，将所有这些放在一个简单的应用中，该应用允许您在命令行执行简单的计算。\n\n在下一章中，您将了解 C++ 类型以及如何将值从一种类型转换为另一种类型。"
  },
  {
    "path": "docs/begin-cpp-prog/03.md",
    "content": "# 三、探索 C++ 类型\n\n在最后两章中，您已经学习了如何编写 C++ 程序，了解了您使用的文件，以及控制执行流的方法。 本章介绍您将在程序中使用的数据：数据类型以及保存这些数据的变量。\n\n变量可以处理具有特定格式和特定行为的数据，这由变量的类型决定。 变量的类型决定了在用户输入或查看数据时可以对数据和数据格式执行的操作。\n\n实际上，您可以查看三种常见的类型类别：内置类型、自定义类型和指针。 指针一般将在下一章中介绍，自定义类型或类以及指向它们的指针将在[第 6 章](06.html)、*类*中介绍。 本章将介绍作为 C++ 语言一部分提供的类型。\n\n# 探索内置类型\n\nC++ 提供整数、浮点和布尔类型。 `char`类型是整数，但它可用于保存单个字符，因此其数据可视为数字或字符。 C++ 标准库提供了`string`类，允许您使用和操作字符串。 字符串将在[章](09.html)、*中使用字符串*进行深入介绍。\n\n顾名思义，整数类型包含没有小数部分的整数值。 如果您使用整数执行计算，除非您采取措施保留小数部分(例如，通过余数运算符`%`)，否则任何小数部分都将被丢弃。 浮点类型保存可能包含小数部分的数字；因为浮点类型可以保存尾数指数格式的数字，所以它们可以保存非常大的数字，也可以保存非常小的数字。\n\n变量是类型的实例；它是为保存该类型可以保存的数据而分配的内存。 可以修改整数和浮点变量声明，以告知编译器要分配多少内存，从而告知变量可以保存的数据的限制以及对变量执行的计算的精度。 此外，您还可以指示变量是否会在符号重要的地方保存一个数字。 如果数字用于保存位图(其中位不构成数字，但有其自己的单独含义)，则使用有符号类型通常没有意义。\n\n在某些情况下，您将使用 C++ 从文件或网络流解压缩数据，以便您可以对其进行操作。 在这种情况下，您需要知道数据是浮点型的还是整型的、有符号的还是无符号的、使用了多少字节以及这些字节的顺序。 字节的顺序(多字节数中的第一个字节是数字的低位还是高位)由您正在编译的处理器决定，在大多数情况下，您不需要担心这个问题。\n\n同样，有时您可能需要了解变量的大小以及它在内存中的对齐方式；特别是当您使用数据记录(在 C++ 中称为`structs`)时。 C++ 提供了`sizeof`运算符来提供用于保存变量的字节数，并提供了`alignof`运算符来确定内存中类型的对齐方式。 对于基本类型，`sizeof`和`alignof`运算符返回相同的值；只需在自定义类型上调用`alignof`运算符，它将返回类型中最大数据成员的对齐。\n\n# 整数\n\n顾名思义，整数保存整数数据，即没有小数部分的数字。 因此，在小数部分很重要的情况下，使用整数进行任何算术都没有什么意义；在这种情况下，您应该使用浮点数。 上一章给出了一个这样的例子：\n\n```cpp\n    int height = 480;  \n    int width = 640; \n    int aspect_ratio = width / height;\n```\n\n这给出的纵横比为 1，这显然是不正确的，也没有任何作用。 即使将结果赋给浮点数，也会得到相同的结果：\n\n```cpp\n    float aspect_ratio = width / height;\n```\n\n原因是算术是在表达式`width / height`中执行的，该表达式将对将丢弃结果的任何小数部分的整数使用除法运算符。 要使用浮点除法运算符，必须将一个或另一个操作数转换为浮点数，因此使用浮点运算符：\n\n```cpp\n    float aspect_ratio = width / (float)height;\n```\n\n这将为`aspect_ratio`变量赋值 1.3333(或 4：3)。 这里使用的 CAST 操作符是 C CAST 操作符，它强制将一种类型的数据用作另一种类型的数据。 (之所以使用它，是因为我们还没有引入 C++ CAST 运算符，并且 C CAST 运算符的语法很清楚。)。 此强制转换中没有类型安全。 C++ 提供了强制转换运算符，下面将讨论这些操作符，其中一些将以类型安全的方式强制转换，这在使用指向自定义类型的对象的指针时变得很重要。\n\nC++ 提供了各种大小的整数类型，如下表所述。 这是五种标准整数类型。 该标准规定，`int`是处理器的自然大小，将具有介于(包括)`INT_MIN`和`INT_MAX`(在`<climits>`头文件中定义)之间的值。 整数类型的大小至少与列表中它前面的那些类型一样大，因此`int`至少与`short int`和`long long int`个类型一样大，后者至少与`long int`类型一样大。 如果类型都相同，那么短语*至少和*一样大就没有多大用处了，所以`<climits>`头文件也定义了其他基本整数类型的范围。 具体需要多少字节来存储这些整数范围，具体取决于具体实现。 下表列出了 x86 32 位处理器上的基本类型和大小范围：\n\n| **类型** | **范围** | **字节大小** |\n| `signed char` | -128 至 127 | 1. |\n| `short int` | -32768 至 32767 | 2 个 |\n| `int` | -2147483648 至 2147483647 | 4. |\n| `long int` | -2147483648 至 2147483647 | 4. |\n| `long long int` | -9223372036854775808 至 9223372036854775807 | 8 个 |\n\n实际上，您使用的不是`short int`类型，而是`short`；对于`long int`，您将使用`long`；对于`long long int`，您通常将使用`long long`。 从本表可以看到，`int`和`long int`的类型大小相同，但它们仍然是两种不同的类型。\n\n除`char`类型外，默认情况下整数类型是有符号的，即它们既可以包含正数也可以包含负数(例如，类型为`short`的变量可以具有介于-32,768 和 32,767 之间的值)。 您可以使用`signed`关键字显式指示该类型是有符号的。 您还可以通过使用`unsigned`关键字来获得无符号的等价物，这将为您提供额外的位，但也意味着按位运算符和移位运算符将按照您的预期工作。 您可能会发现`unsigned`使用时没有类型，在这种情况下它指的是`unsigned int`。 类似地，不带类型使用的`signed`指的是`signed int`。\n\n`char`类型是与`unsigned char`和`signed char`不同的类型。 该标准规定，`char`中的每一位都用来保存字符信息，因此是否可以将`char`视为可以保存负数取决于实现。 如果你想让`char`保存一个有符号的数字，你应该特别使用`signed char`。\n\n该标准对标准整数类型的大小并不精确，如果您正在编写包含字节流的代码(例如，访问文件或网络流中的数据)，这可能是一个问题。 `<cstdlib>`头文件定义将保存特定数据范围的命名类型。 这些类型的名称具有范围内使用的位数(尽管实际类型可能需要更多位)。 因此，存在名为`int16_t`和`uint16_t`的类型，其中第一种类型是包含 16 位值范围的有符号整数，第二种类型是无符号整数。 还有为 8 位、32 位和 64 位值声明的类型。\n\n下面显示了 x86 计算机上由`sizeof`运算符确定的这些类型的实际大小：\n\n```cpp\n    // #include <cstdint> \n    using namespace std;               // Values for x86 \n    cout << sizeof(int8_t)  << endl;   // 1 \n    cout << sizeof(int16_t) << endl;   // 2 \n    cout << sizeof(int32_t) << endl;   // 4 \n    cout << sizeof(int64_t) << endl;   // 8\n```\n\n此外，`<cstdlib>`头文件使用与前面相同的命名方案定义了名称为`int_least16_t`和`uint_least16_t`的类型，并且定义了 8 位、16 位、32 位和 64 位版本。 名称的`least`部分表示该类型将保存至少具有指定位数的值，但可能会有更多位数。 还有一些名称为`int_fast16_t`和`uint_fast16_t`的类型，其版本为 8 位、16 位、32 位和 64 位，它们被认为是可以容纳该位数的最快类型。\n\n# 指定整型文字\n\n要为整数变量赋值，需要提供一个没有小数部分的数字。 编译器将以数字表示的最接近精度标识类型，并尝试赋值整数，如有必要则执行转换。\n\n要显式指定文字是`long`值，可以使用`l`或`L`后缀。 同样，对于`unsigned long`，您可以使用后缀：`ul`或`UL`。 对于`long long`值，可以使用`ll`或`LL`后缀，对`unsigned long long`使用`ull`或`ULL`。 `u`(或`U`)后缀用于`unsigned`(即`unsigned int`)，您不需要为`int`添加后缀。 下面使用大写后缀说明了这一点：\n\n```cpp\n    int i = 3; \n    signed s = 3; \n    unsigned int ui = 3U; \n    long l = 3L; \n    unsigned long ul = 3UL; \n    long long ll = 3LL; \n    unsigned long long ull = 3ULL;\n```\n\n使用以 10 为基数的数字系统来指定位图形式的数字是令人困惑和麻烦的。 位图中的位是 2 的幂，所以使用 2 的幂的数字系统更有意义。C++ 允许您提供八进制(以 8 为基数)或十六进制(以 16 为基数)的数字。 要以八进制形式提供文字，您需要在数字前面加上一个零字符(`0`)。 要提供十六进制的文字，需要在数字前面加上`0x`字符序列。 八进制数使用数字 0 到 7，但十六进制数字需要 16 位，这意味着 0 到 9 和 a 到 f(或 A 到 F)，其中 A 是 10 为基数的 10，F 是 10 为基数的 15：\n\n```cpp\n    unsigned long long every_other = 0xAAAAAAAAAAAAAAAA; \n    unsigned long long each_other  = 0x5555555555555555; \n    cout << hex << showbase << uppercase; \n    cout << every_other << endl; \n    cout << each_other  << endl;\n```\n\n在此代码中，为两个 64 位整数(在 Visual C++ 中)分配了位图值，每隔一位设置为 1。第一个变量从底位设置开始，第二个变量从未设置的底位开始，第二个变量从设置的第二位开始。 在插入数字之前，使用三个操纵器修改流。 第一个`hex`表示应在控制台上将整数打印为十六进制，`showbase`表示将打印前导`0x`。 默认情况下，字母数字(A 到 F)将以小写形式提供，要指定必须使用大写字母，请使用`uppercase`。 修改流后，该设置将一直保留，直到更改。 要随后将流更改为对字母十六进制数字使用小写，将`nouppercase`插入到流中，并打印不带基数的数字，请插入`noshowbase`操纵器。 要使用八进制数字，请插入`oct`操纵器，要使用小数，请插入`dec`操纵器。\n\n当您指定这样的大数字时，很难看到您是否指定了正确的位数。 您可以使用单引号(`'`)将数字组合在一起：\n\n```cpp\n    unsigned long long every_other = 0xAAAA'AAAA'AAAA'AAAA; \n    int billion = 1'000'000'000;\n```\n\n编译器会忽略引号；它只是一种直观的辅助工具。 在第一个示例中，引号将数字分组为两个字节组；在第二个示例中，引号将十进制数分组为千位和百万位。\n\n# 使用位集显示位模式\n\n没有操纵器可以告诉`cout`对象将整数打印为位图，但您可以使用`bitset`对象模拟该行为：\n\n```cpp\n    // #include <bitset> \n    unsigned long long every_other = 0xAAAAAAAAAAAAAAAA; \n    unsigned long long each_other  = 0x5555555555555555; \n    bitset<64> bs_every(every_other); \n    bitset<64> bs_each(each_other); \n    cout << bs_every << endl; \n    cout << bs_each << endl;\n```\n\n结果是：\n\n```cpp\n    1010101010101010101010101010101010101010101010101010101010101010    \n    0101010101010101010101010101010101010101010101010101010101010101\n```\n\n这里的`bitset`类是**参数化的，**表示您通过尖括号(`<>`)提供一个参数，在本例中使用 64，表示`bitset`对象可以容纳 64 位。 在这两种情况下，`bitset`对象的初始化都是使用看起来像函数调用的语法执行的(实际上，它确实调用了一个称为**构造函数**的函数)，这是初始化对象的首选方式。 将`bitset`对象插入流中，打印出从最高位开始的每一位。 (原因是定义了一个`operator <<`函数，它接受一个`bitset`对象，就像大多数标准库类一样)。\n\n作为使用位运算符的替代方法，`bitset`类对于访问和设置单个位很有用：\n\n```cpp\n    bs_every.set(0); \n    every_other = bs_every.to_ullong(); \n    cout << bs_every << endl; \n    cout << every_other << endl;\n```\n\n`set`函数将指定位置的位设置为值 1。`to_ullong`函数将返回`bitset`表示的`long long`数字。\n\n对`set`函数的调用和赋值具有相同的结果，如下所示：\n\n```cpp\n    every_other |= 0x0000000000000001;\n```\n\n# 确定整数字节顺序\n\n整数中的字节顺序取决于实现；它取决于处理器处理整数的方式。 在大多数情况下，您不需要知道。 但是，如果您以二进制模式从文件读取字节，或从网络流读取字节，并且需要将两个或更多字节解释为整数的一部分，则需要知道它们的顺序，并在必要时将它们转换为处理器可识别的顺序。\n\nC 网络库(在 Windows 上称为**Winsock**库)包含一个函数集合，用于将`unsigned short`和`unsigned long`类型从网络顺序转换为主机顺序(即当前计算机上处理器使用的顺序)，反之亦然。 网络订单是大端的。 **大端**表示第一个字节将是整数中的最高字节，而**小端**表示第一个字节是最小字节。 将整数传输到另一台计算机时，首先将源计算机的处理器使用的顺序(主机顺序)转换为网络顺序，接收机在使用数据之前将整数从网络顺序转换为接收机的主机顺序。\n\n改变字节顺序的功能是`ntohs`和`ntohl`；用于将`unsigned short`和`unsigned long`从网络命令转换为主机命令的函数，以及用于从主机命令转换为网络命令的函数`htons`和`htonl`。 当您在调试代码时查看内存时，了解字节顺序将非常重要(例如，如[第 10 章](10.html)、*诊断和调试*中所述)。\n\n很容易编写代码来颠倒字节顺序：\n\n```cpp\n    unsigned short reverse(unsigned short us)  \n    { \n        return ((us & 0xff) << 8) | ((us & 0xff00) >> 8); \n    }\n```\n\n这使用按位运算符将假定组成`unsigned short`的两个字节分隔成左移 8 位的低位字节和右移 8 位的高位字节，并使用按位 OR 运算符`|`将这两个数字重新组合为`unsigned short`。 为 4 字节和 8 字节整数编写此函数的版本非常简单。\n\n# 浮点类型\n\n有三种基本的浮点类型：\n\n*   `float`(单精度)\n*   `double`(双精度)\n*   `long double`(扩展精度)\n\n所有这些都是签字的。 内存中数字的实际格式和使用的字节数特定于 C++ 实现，但`<cfloat>`头文件给出了范围。 下表列出了 x86 32 位处理器上使用的正范围和字节数：\n\n| **类型** | **范围** | **字节大小** |\n| 漂浮 / 浮动 / 使漂浮 / 实行 | 1.175494351e-38 至 3.402823466e+38 | 4. |\n| 两倍物 / 成对物 / 两倍 / 双精度型 | 2.2250738585072014e-308 至 1.7976931348623158e+308 | 8 个 |\n| 长双倍 | 2.2250738585072014e-308 至 1.7976931348623158e+308 | 8 个 |\n\n如您所见，在 Visual C++ 中，`double`和`long double`具有相同的范围，但它们仍然是两个截然不同的类型。\n\n# 指定浮点文字\n\n用于初始化`double`的文字通过使用科学格式或简单地提供小数点来指定为浮点：\n\n```cpp\n    double one = 1.0; \n    double two = 2.; \n    double one_million = 1e6;\n```\n\n第一个示例指示变量`one`被赋给浮点值 1.0。 尾随零并不重要，如第二个变量`two`所示；但是，尾随零确实使代码更具可读性，因为句点很容易被忽略。 第三个例子使用科学记数法。 第一部分是尾数，可以签名，`e`后面的部分是指数。 指数是数字的 10 次方(可以是负数)。 将变量赋给尾数乘以 10 的值，并将其提升到指数。 虽然不建议这样做，但您可以编写以下内容：\n\n```cpp\n    double one = 0.0001e4; \n    double one_billion = 1000e6;\n```\n\n编译器将适当地解释这些数字。 第一个示例不合常理，但第二个示例有一定的意义；它在您的代码中显示 10 亿就是 10 亿。\n\n这些示例将双精度浮点值赋给`double`变量。 要为单精度变量指定值以便可以分配`float`变量，请使用`f`(或`F`)后缀。 同样，对于`long double`文字，请使用`l`(或`L`)后缀：\n\n```cpp\n    float one = 1.f; \n    float two = 2f; // error \n    long double one_million = 1e6L;\n```\n\n如果您使用这些后缀，您仍然需要以正确的格式提供数字。 文字`2f`不正确；您必须提供小数点`2.f`。 当您指定具有大量数字的浮点数时，可以使用单引号(`'`)对数字进行分组。 如前所述，这只是对程序员的视觉帮助：\n\n```cpp\n    double one_billion = 1'000'000'000.;\n```\n\n# 字符和字符串\n\n`string`类和 C 字符串函数将在[章](09.html)，*使用字符串*中介绍；本节介绍代码中字符变量的基本用法。\n\n# 字符类型\n\n`char`类型是整数，所以也存在`signed char`和`unsigned char`。 这是三种不同的类型；`signed char`和`unsigned char`类型应该被视为数值类型。 `char`类型用于保存实现的字符集中的单个字符。 在 Visual C++ 中，这是一个 8 位整数，可以容纳 ISO-8859 或 UTF-8 字符集的字符。 这些字符集能够表示英语和大多数欧洲语言中使用的字符。 来自其他语言的字符占用多于一个字节，C++ 提供`char16_t`类型来保存 16 位字符，`char32_t`类型来保存 32 位字符。\n\n还有一种名为`wchar_t`(宽字符)的类型，可以保存最大扩展字符集中的字符。 通常，当您看到带有`w`前缀的 C 运行时库或 C++ 标准库函数时，它将使用宽字符串，而不是`char`字符串。 因此，`cout`对象将允许您插入`char`字符串，`wcout`对象将允许您插入宽字符串。\n\nC++ 标准规定，`char`函数中的每一位都用来保存字符信息，因此是否可以将`char`视为可以保存负数取决于实现。 以下内容说明了这一点：\n\n```cpp\n    char c = '~'; \n    cout << c << \" \" << (signed short)c << endl; \n    c += 2; \n    cout << c << \" \" << (signed short)c << endl;\n```\n\n`signed char`的范围是-128 到 127，但此代码使用单独的类型`char`，并尝试以相同的方式使用它。 首先将变量`c`赋给 ASCII 字符`~`(126)。 当您将字符插入到输出流中时，它将尝试打印字符而不是数字，因此下一行将此字符打印到控制台，为了获得数字值，代码将变量转换为`signed short`整数。 (同样，为了清晰起见，使用了 C 造型。)。 接下来，变量递增 2，也就是说，字符在字符集中又增加了两个字符，这意味着扩展 ASCII 字符集中的第一个字符；结果如下所示：\n\n```cpp\n    ~ 126\n    C -128\n```\n\n扩展字符集中的第一个字符是 C-cedilla。\n\n很不直观的是，126 的值加上两个结果就是-128，这是由带符号类型的溢出计算产生的。 即使这是故意的，也最好避免这样做。\n\n在 Visual C++ 中，C-cedilla 字符被视为-128，因此您可以编写以下代码以达到相同的效果：\n\n```cpp\n    char c = -128;\n```\n\n这是特定于实现的，因此对于可移植代码，您不应该依赖它。\n\n# 使用字符宏\n\n`<cctype>`头包含各种宏，您可以使用这些宏检查 a`char`包含的字符类型。 这些是在`<ctype.h>`中声明的 C 运行时宏。 下表介绍了一些用于测试字符值的更有用的宏。 请记住，因为这些是 C 例程，所以它们不会返回`bool`值；相反，它们返回一个`int`，对于`true`返回值为非零值，对于`false`返回值为零。\n\n| 发文：2013-06-04 晚上 9：00 | **测试字符是否为：** |\n| `isalnum` | 字母数字字符，从 A 到 Z，从 a 到 z，从 0 到 9 |\n| `isalpha` | 字母字符，从 A 到 Z，从 a 到 z |\n| `isascii` | ASCII 字符，0x00 到 0x7f |\n| `isblank` | 空格或水平制表符 |\n| `iscntrl` | 控制字符，0x00 到 0x1f 或 0x7f |\n| `isdigit` | 十进制数字 0 到 9 |\n| `isgraph` | 空格以外的可打印字符，0x21 到 0x7e |\n| `islower` | 小写字符，a 到 z |\n| `isprint` | 可打印字符，0x20 到 0x7e |\n| `ispunct` | 标点符号，`! \" # $ % & ' ( ) * + , - . / : ; < = > ? @ [ ] ^ _ ` { &#124; } ~ \\` |\n| `isspace` | 一个空格 |\n| `isupper` | 大写字符，从 A 到 Z |\n| `isxdigit` | 十六进制数字，0 到 9，a 到 f，A 到 F |\n\n例如，下面的代码在从输入流中读取单个字符时循环(在每个字符之后，您需要按*Enter*键)。 当提供非数字值时，循环结束：\n\n```cpp\n    char c; \n    do \n    { \n       cin >> c \n    } while(isdigit(c));\n```\n\n还可以使用宏来更改字符。 同样，这些函数将返回`int`值，您应该将其转换为`char`。\n\n| 发文：2013-06-04 晚上 9：00 | **返回** |\n| `toupper` | 字符的大写版本 |\n| `tolower` | 字符的小写版本 |\n\n在以下代码中，将回显在控制台键入的字符，直到用户键入`q`或`Q`。 如果键入的字符是小写字符，则回显的字符将转换为大写：\n\n```cpp\n    char c; \n    do \n    { \n        cin >> c; \n        if (islower(c)) c = toupper(c); \n        cout << c << endl; \n    } while (c != 'Q');\n```\n\n# 指定字符文字\n\n您可以使用原义字符初始化`char`变量。 这将是支持的字符集中的字符。 ASCII 字符集包括一些无法打印的字符，为了使用这些字符，C++ 提供了两个使用反斜杠字符(`\\`)的字符序列。\n\n| **名称** | 加入时间：清华大学 2007 年 01 月 25 日下午 3：33 | **C++ 序列** |\n| 纽兰。 | 低频率 / 同 low frequency | `\\n` |\n| 水平选项卡 | 高电压 / 同 high tension . | `\\t` |\n| 垂直选项卡 | 佛蒙特州 / 同 Vermont | `\\v` |\n| 退格键 | 英国标准 / 理学士 / 外科学士 / 圣礼 | `\\b` |\n| 归还 / 返回 / 回车 / 同 return | 复活社 / 哥斯达黎加 | `\\r` |\n| 换页 | 极强的 / 非常嘹亮的 | `\\f` |\n| 提醒 / 警告 / 使警觉，使意识到 | 丰饶之神（古代腓尼基人和迦南人信奉的主神） | `\\a` |\n| 反斜杠 | \\ | `\\\\` |\n| 问号 / 疑问 | ？ | `\\?` |\n| 女性单人配额 | ‘ | `\\'` |\n| 双配额双配额双配额 | “ | `\\\"` |\n\n此外，您还可以将该字符的数值指定为八进制或十六进制数字。 要提供一个八进制数字，您可以将数字指定为三个字符(如果需要，可以加上一个或两个`0`字符作为前缀)，并以反斜杠为前缀。 对于十六进制数，您可以在其前面加上`\\x`。 字符`M`是十进制的字符数字 77、八进制的 115 和十六进制的 4d，因此可以通过三种方式使用`M`字符初始化字符变量：\n\n```cpp\n    char m1 = 'M'; \n    char m2 = '\\115'; \n    char m3 = '\\x4d';\n```\n\n为完整起见，值得指出的是，您可以将字符初始化为整数，因此以下代码也会将每个变量初始化为`M`字符：\n\n```cpp\n    char m4 = 0115; // octal \n    char m5 = 0x4d; // hexadecimal\n```\n\n所有这些方法都是有效的。\n\n# 指定字符串文字\n\n字符串由一个或多个字符组成，您也可以在字符串文字中使用转义字符：\n\n```cpp\n    cout << \"This is \\x43\\x2b\\05\\3n\";\n```\n\n这个相当难读的字符串将在控制台上打印为`This is C++ `，后跟一个换行符。 大写 C 是十六进制的 43，+符号是十六进制的 2b 和八进制的 53。 `\\n`字符是换行符。 转义字符对于打印不在 C++ 编译器使用的字符集中的字符以及某些不可打印的字符(例如，按`\\t`插入水平制表符)很有用。 `cout`对象在将字符写入输出流之前缓冲字符。 如果使用`\\n`作为换行符，则会将其视为缓冲区中的任何其他字符。 `endl`操纵器会将`\\n`插入缓冲区，然后将其刷新，因此字符会立即写入控制台。\n\n*空*或`NULL`字符是`\\0`。 这是一个重要的字符，因为它是不可打印的，除了标记字符串中字符序列的结尾外，它没有任何用处。 空字符串是`\"\"`，但是因为字符串是由`NULL`字符分隔的，所以用空字符串初始化的字符串变量占用的内存将有一个字符，即`\\0`。\n\n换行符允许您在字符串中放入换行符。 如果您要执行的唯一格式设置是段落，并且打印的是短段落，则此功能非常有用：\n\n```cpp\n    cout << \"Mary had a little lamb,n its fleece was white as snow.\"  \n         << endl;\n```\n\n这将在控制台上打印两行：\n\n```cpp\n Mary had a little lamb,\n its fleece was white as snow.\n```\n\n但是，您可能希望使用长字符序列来初始化字符串，并且您使用的编辑器的限制可能意味着您希望将字符串拆分成几行。 为此，您可以将字符串的每个片段放在双引号内：\n\n```cpp\n    cout << \"And everywhere that Mary went, \" \n            \"the lamb was sure to go.\"  \n         << endl;\n```\n\n您将在控制台上看到以下内容：\n\n```cpp\n And everywhere that Mary went, the lamb was sure to go.\n```\n\n除了在末尾用`endl`明确要求的行外，没有打印任何换行符。 此语法允许您在代码中使长字符串更具可读性；当然，您可以在此类字符串中使用换行符`\\n`。\n\n# Unicode 文字\n\n也可以用字符初始化`wchar_t`变量，编译器将通过使用字符的字节并将剩余的(较高)字节赋值为零来将字符提升为宽字符。 但是，使用宽字符为这样的变量赋值更有意义，您可以使用`L`前缀来实现这一点。\n\n```cpp\n    wchar_t dollar = L'$'; \n    wchar_t euro = L'\\u20a0'; \n    wcout << dollar;\n```\n\n请注意，此代码使用的不是`cout`对象，而是宽字符版本`wcout`。 在引号中使用`\\u`前缀的语法表示下面的字符是 Unicode 字符。\n\n请记住，要显示 Unicode 字符，您需要使用将显示 Unicode 字符的控制台，并且在默认情况下，Windows 控制台设置为**代码页 850**，它不会显示 Unicode 字符。 您可以通过在标准输出流`stdout`上调用`_setmode`(在`<io.h>`中定义)，指定 UTF-16 文件模式(使用在`<fcntl.h>`中定义的`_O_U16TEXT`)来更改输出控制台的模式：\n\n```cpp\n    _setmode(_fileno(stdout), _O_U16TEXT);\n```\n\nYou can find a list of all of the characters supported by Unicode at [http://unicode.org/charts/](http://unicode.org/charts/).\n\n也可以将 UTF-16 字符分配给`char16_t`变量，将 UTF-32 字符分配给`char32_t`变量。\n\n# 原始字符串\n\n当您使用原始字符串文字时，您基本上关闭了转义字符的含义。 您在原始字符串中键入的任何内容都将成为其内容，即使您使用包含换行符的空格。 原始字符串用`R\"(`和`)\"`分隔。 也就是说，字符串位于内括号之间。\n\n```cpp\n    cout << R\"(newline is \\n in C++ and \"quoted text\" use quotes)\";\n```\n\n请注意，`()`是语法的一部分，不是字符串的一部分。 前面的代码将以下内容打印到控制台：\n\n```cpp\n newline is \\n in C++ and \"quoted text\" use quotes\n```\n\n通常，字符串中的`\\n`是转义字符，将被翻译为换行符，但在原始字符串中，它不会被翻译，而是被打印为两个字符。\n\n在普通的 C++ 字符串中，您必须转义某些字符；例如，双引号必须转义为`\\\"`，反斜杠转义为`\\\\`。 在不使用原始字符串的情况下，下面的结果是相同的：\n\n```cpp\n    cout << \"newline is \\\\n in C++ and \\\"quoted text\\\" use quotes\";\n```\n\n您还可以在原始字符串中包含换行符：\n\n```cpp\n    cout << R\"(Mary had a little lamb,  \n                             its fleece was white as snow)\" \n    cout << endl;\n```\n\n在此代码中，逗号后的换行符将打印到控制台。 遗憾的是，控制台上将打印所有空格，因此假设在前面的代码中缩进为三个空格，而`cout`缩进一次，您将在控制台上看到以下内容：\n\n```cpp\n Mary had a little lamb,\n its fleece was white as snow\n```\n\n`its`前面有 14 个空格，因为在源代码中，`its`前面有 14 个空格。 因此，您应该谨慎使用原始字符串。\n\n也许，原始字符串的最佳用法是在 Windows 上使用文件路径初始化变量。 Windows 中的文件夹分隔符是反斜杠，这意味着对于表示文件路径的文字字符串，您必须对每个分隔符进行转义；因此，该字符串将有很多双反斜杠，可能会缺少一个。 对于原始字符串，这种转义不是必需的。 下面的两个字符串变量表示相同的字符串：\n\n```cpp\n    string path1 = \"C:\\\\Beginning_C++ \\\\Chapter_03\\\\readme.txt\"; \n    string path2 = R\"(C:\\Beginning_C++ \\Chapter_03\\readme.txt)\";\n```\n\n这两个字符串具有相同的内容，但第二个字符串的可读性更好，因为 C++ 文字字符串没有转义反斜杠。\n\n只有在代码中声明的文字字符串才需要转义反斜杠的要求；它是编译器如何解释字符的指示。 如果从函数(或通过`argv[0]`)获取文件路径，分隔符将是反斜杠。\n\n# 字符串字节顺序\n\n扩展字符集使用每个字符一个以上的字节。 如果这些字符存储在文件中，字节的顺序就变得很重要。 在这种情况下，角色的作者必须使用与潜在读者将使用的顺序相同的顺序。\n\n一种方法是使用**字节顺序标记**(**BOM**)。 这是具有已知模式的已知字节数，通常作为流中的第一项放置，以便流的读取器可以使用它来确定流中其余字符的字节顺序。 Unicode 将 16 位字符`\\uFEFF`和非字符`\\uFFFE`定义为字节顺序标记。 在`\\uFEFF`的情况下，除位 8 以外的所有位都被设置(如果最低位被标记为位 0)。 此 BOM 可以作为机器之间传递的数据的前缀。 目标机器可以将 BOM 读入 16 位变量并测试这些位。 如果位 8 为零，则意味着两台机器具有相同的字节顺序，因此可以按照流中的顺序将字符读取为两个字节值。 如果位 0 为零，则意味着目标计算机以与源相反的顺序读取 16 位变量，因此必须采取措施确保以正确的顺序读取具有 16 位字符的字节。\n\nUnicode 字节顺序标记(BOM)按如下方式序列化(十六进制)：\n\n| **字符集** | **字节顺序标记** |\n| UTF-8 | 电炉 BB 高炉 |\n| UTF-16 高位序 | FE FF |\n| UTF-16 小端字节序 | FF FE |\n| UTF-32 高位序 | 00 00 FE FF |\n| UTF-32 小端 | FF FE 00 00 |\n\n请记住，当您从文件中读取数据时。 字符序列 FE FF 在非 Unicode 文件中非常少见，因此如果您将它们读作文件中的前两个字节，则意味着该文件是 Unicode 文件。 由于`\\uFEFF`和`\\uFFFE`不是可打印的 Unicode 字符，这意味着以这两个字符中的任何一个开头的文件都有字节顺序标记，然后您可以使用 BOM 来确定如何解释文件中的其余字节。\n\n# 布尔代数学（或逻辑）体系的\n\n`bool`类型保存一个布尔值，即只包含以下两个值之一：`true`或`false`。 C++ 允许您将 0(零)视为`false`，将任何非零值视为`true`，但这可能会导致错误，因此最好养成显式检查值的习惯：\n\n```cpp\n    int use_pointer(int *p) \n    { \n        if (p)            { /* not a null pointer */ } \n        if (p != nullptr) { /* not a null pointer */ }   \n        return 0; \n    }\n```\n\n这两个中的第二个更可取，因为它更清楚您要比较的是什么。\n请注意，即使指针不是`nullptr`，它也可能不是有效的指针，但通常的做法是将指针赋给`nullptr`以传达一些其他含义，也许是说指针操作不合适。\n\n您可以将布尔值插入到输出流中。 但是，默认行为是将布尔值视为整数。 如果希望`cout`输出带有字符串名的`bool`值，则将操纵器`boolalpha`插入到流中；这将使流打印`true`或`false`到控制台。 默认行为可以通过使用`noboolalpha`操纵器来实现。\n\n# 无效的 / 空的 / 空无所有的 / 缺门的\n\n在某些情况下，需要指明函数没有参数或不返回值；在这两种情况下，都可以使用关键字`void`：\n\n```cpp\n    void print_message(void) \n    { \n        cout << \"no inputs, no return value\" << endl; \n    }\n```\n\n在参数列表中使用`void`是可选的；可以使用空的一对圆括号，最好是这样。 这是指示函数除了返回`void`之外不返回值的唯一方式。\n\n请注意，`void`不是真正的类型，因为您不能创建`void`变量；它是缺少类型。 您将在下一章中了解到，您可以创建类型为`void`的指针，但如果不强制转换为类型化指针，您将无法使用此类指针所指向的内存：要使用内存，您必须决定内存保存的数据的类型。\n\n# 初始化器\n\n初始化器在上一章中已经提到，但我们将在这里更深入地讨论。 对于内置类型，必须在使用变量之前对其进行初始化。 对于自定义类型，类型可以定义默认值，但这样做会出现一些问题，这将在[第 6 章](06.html)、*类*中介绍。\n\n在所有版本的 C++ 中，有三种方法初始化内置类型：赋值、函数语法或调用构造函数。 在 C++ 11 中引入了另一种初始化变量的方法：通过列表初始化器进行构造。 这四种方式如下所示：\n\n```cpp\n    int i = 1; \n    int j = int(2); \n    int k(3); \n    int m{4};\n```\n\n这三个中的第一个是最清楚的；它使用易于理解的语法显示变量正在被初始化为一个值。 第二个示例通过像调用函数一样调用类型来初始化变量。 第三个示例调用`int`类型的构造函数。 这是初始化自定义类型的典型方式，因此最好只为自定义类型保留此语法。\n\n第四种语法是 C++ 11 中的新语法，它使用花括号(`{}`)之间的初始化列表来初始化变量。 稍微混淆一下，您还可以使用与赋给单个项目列表相同的语法来初始化内置类型：\n\n```cpp\n    int n = { 5 };\n```\n\n这真是令人困惑，类型`n`是整数，而不是数组。 回想一下，在上一章中，我们创建了一个包含披头士出生日期的数组：\n\n```cpp\n    int birth_years[] = { 1940, 1942, 1943, 1940 };\n```\n\n这将创建一个由四个整数组成的数组；每一项的类型为`int`，但数组变量的类型为`int*`。 该变量指向保存四个整数的内存。 同样，您也可以将变量初始化为一项的数组：\n\n```cpp\n    int john[] = { 1940 };\n```\n\n这与 C++ 11 允许初始化单个整数的初始化代码完全相同。 此外，使用相同的语法来初始化记录类型(`structs`)的实例，增加了对语法含义的另一层潜在混淆。\n\n最好避免使用花括号语法进行变量初始化，而只将其用于初始化列表。 但是，这种用于强制转换的语法有一些优点，稍后将对此进行说明。\n\n大括号语法可用于为 C++ 标准库中的任何集合类以及 C++ 数组提供初始值。 即使在用于初始化集合对象时，也存在混淆的可能性。 例如，考虑`vector`集合类。 它可以容纳通过一对尖括号(`<>`)提供的类型的集合。 此类对象的容量可以随着向该对象添加更多项目而增加，但您可以通过指定初始容量来优化其使用：\n\n```cpp\n    vector<int> a1 (42); \n    cout << \" size \" << a1.size() << endl; \n    for (int i : a1) cout << i << endl;\n```\n\n这段代码的第一行是：创建一个可以容纳整数的`vector`对象，并从为 42 个整数预留空间开始，每个整数都被初始化为零值。 第二行将向量的大小打印到控制台(42)，第三行将数组中的所有项打印到控制台，它将打印 42 个零。\n\n现在考虑以下几点：\n\n```cpp\n    vector<int> a2 {42}; \n    cout << \" size \" << a2.size() << endl; \n    for (int i : a2) cout << i << endl;\n```\n\n这里只有一个更改：圆括号已更改为大括号，但这意味着初始化已完全更改。 第一行现在的意思是：创建一个可以容纳整数的`vector`，并用单个整数 42 对其进行初始化。 `a2`的大小为 1，最后一行将仅打印一个值 42。\n\nC++ 的强大之处在于，它应该很容易编写正确的代码，并说服编译器帮助您避免错误。 使用大括号进行单项初始化增加了很难找到错误的可能性。\n\n# 默认值\n\n内置类型的变量应在首次使用之前进行初始化，但在某些情况下，编译器会提供默认值。\n\n如果在文件范围内或在项目中全局声明变量，并且没有为其提供初始值，则编译器将为其提供默认值。 例如：\n\n```cpp\n    int outside; \n\n    int main() \n    { \n        outside++ ; \n        cout << outside << endl; \n    }\n```\n\n此代码将编译并运行，并打印值 1；编译器已将`outside`初始化为 0，然后将其递增为 1。以下代码将不会编译：\n\n```cpp\n    int main() \n    { \n        int inside; \n        inside++ ; \n        cout << inside << endl; \n    }\n```\n\n编译器将报告递增运算符正在未初始化的变量上使用。\n\n在上一章中，我们看到了编译器提供默认值`static`的另一个示例。\n\n```cpp\n    int counter() \n    { \n        static int count; \n        return ++ count; \n    }\n```\n\n这是一个维护计数的简单函数。 变量`count`用`static`存储类修饰符标记，这意味着该变量与应用具有相同的生存期(在代码启动时分配，在程序结束时释放)；但是，它有内部链接，这意味着该变量只能在声明它的范围内使用，即`counter`函数。 编译器将用默认值 0 来初始化`count`变量，以便在第一次调用`counter`函数时返回值 1。\n\nC++ 11 的新初始化列表语法为您提供了一种声明变量并指定希望由编译器将其初始化为该类型的默认值的方法：\n\n```cpp\n    int a {};\n```\n\n当然，在阅读这段代码时，您必须知道`int`的默认值是什么(它是零)。 同样，简单地将变量初始化为一个值要容易得多，也更显式：\n\n```cpp\n    int a = 0;\n```\n\n默认值的规则很简单：值为零。 整数和浮点数的默认值为 0，字符的默认值为`\\0`，`bool`的默认值为`false,`，指针的默认值为常量`nullptr`。\n\n# 没有类型的声明\n\nC++ 11 引入了一种机制，用于声明一个变量的类型应该根据它被初始化的数据来确定，也就是说，它是`auto`。\n\n这里有一点混淆，因为在 C++ 11 之前，`auto`键用于声明**自动**变量，即在函数中自动分配到堆栈上的变量。 除了在文件作用域声明的变量或声明为`static`的变量外，本书中到目前为止所有其他变量都是自动变量，自动变量是使用最广泛的**存储类**(稍后解释)。 由于`auto`关键字是可选的且适用于大多数变量，因此在 C++ 中很少使用，因此 C++ 11 利用了这一点，去掉了旧的含义，并赋予了`auto`新的含义。\n\n如果使用 C++ 11 编译器编译旧 C++ 代码，而旧代码使用`auto`，则会出现错误，因为新编译器将假定`auto`将与未指定类型的变量一起使用。 如果发生这种情况，只需搜索并删除`auto`的每个实例；它在 C++ 11 之前的 C++ 中是多余的，开发人员几乎没有理由使用它。\n\n关键字`auto`表示编译器应该使用分配给它的数据类型创建一个变量。 变量只能有一个类型，编译器决定的类型是分配给它的数据所需的类型，并且您不能在其他地方使用该变量来保存不同类型的数据。 因为编译器需要从初始值设定项确定类型，所以这意味着所有`auto`变量都必须初始化：\n\n```cpp\n    auto i  = 42;    // int \n    auto l  = 42l;   // long \n    auto ll = 42ll;  // long long \n    auto f  = 1.0f;  // float \n    auto d  = 1.0;   // double \n    auto c  = 'q';   // char \n    auto b  = true;  // bool\n```\n\n请注意，没有语法指定整数值是单字节还是双字节，因此不能以这种方式创建`unsigned char`变量或`short`变量。\n\n这是对`auto`关键字的简单使用，您不应该以这种方式使用它。 AUTO 的强大之处在于，当您使用容器时，可能会产生一些外观相当复杂的类型：\n\n```cpp\n    // #include <string> \n    // #include <vector> \n    // #include <tuple> \n\n    vector<tuple<string, int> > beatles; \n    beatles.push_back(make_tuple(\"John\", 1940)); \n    beatles.push_back(make_tuple(\"Paul\", 1942)); \n    beatles.push_back(make_tuple(\"George\", 1943)); \n    beatles.push_back(make_tuple(\"Ringo\", 1940)); \n\n    for (tuple<string, int> musician : beatles) \n    { \n        cout << get<0>(musician) << \" \" << get<1>(musician) << endl; \n    }\n```\n\n此代码使用我们以前使用过的`vector`容器，但它使用`tuple`存储两个值项。 `tuple`类很简单；您可以在尖括号之间的声明中声明`tuple`对象中的项类型列表。 因此，`tuple<string, int>`声明说明该对象将按该顺序保存一个字符串和一个整数。 `make_tuple`函数由 C++ 标准库提供，将创建包含这两个值的`tuple`对象。 函数`push_back`将项目放入向量容器。 在四次调用`push_back`函数之后，`beatles`变量将包含四个项目，每个项目都是一个带有姓名和出生年份的`tuple`。\n\n范围`for`遍历容器，并在每个循环中将`musician`变量赋给容器中的下一项。 `tuple`中的值在`for`循环中的语句中打印到控制台。 使用`get`参数化函数(来自`<tuple>`)访问`tuple`中的项，其中尖括号中的参数表示要从圆括号中作为参数传递的`tuple`对象中获取的项的索引(从零开始索引)。 在本例中，对`get<0>`的调用获取打印出来的名称，然后是一个空格，然后`get<1>`获取`tuple`中的年份项。 此代码的结果是：\n\n```cpp\n    John 1940 \n    Paul 1942 \n    George 1943 \n    Ringo 1940\n```\n\n此文本的格式较差，因为它没有考虑名称的长度。 这可以通过使用字符串在[章](09.html)、*中解释的操纵器来解决。*\n\n再看一看`for`循环：\n\n```cpp\n    for (tuple<string, int> musician : beatles) \n    { \n        cout << get<0>(musician) << \" \" << get<1>(musician) << endl; \n    }\n```\n\n音乐家的类型是`tuple<string, int>;`，这是一个相当简单的类型，当您更多地使用标准模板时，可能会得到一些复杂的类型(特别是当您使用**迭代器**时)。 这就是`auto`变得有用的地方。 以下代码相同，但更易于阅读：\n\n```cpp\n    for (auto musician : beatles) \n    { \n        cout << get<0>(musician) << \" \" << get<1>(musician) << endl; \n    }\n```\n\nMUSIC 变量仍然是类型化的，它是一个`tuple<string, int>`，但是`auto`意味着您不必显式地对其进行编码。\n\n# 存储类\n\n在声明变量时，您可以指定它的存储类，该存储类指示变量的生存期、链接(哪些其他代码可以访问它)和内存位置。\n\n您已经看到了一个存储类`static`，当它应用于函数中的变量时，意味着该变量只能在该函数内访问，但其生存期与程序相同。 但是，`static`可以用于在文件作用域声明的变量，在这种情况下，它指示变量只能在当前文件中使用，这称为**内部链接**。 如果在文件范围内定义的变量上省略了关键字*`static`，则它有一个**外部链接，**，这意味着该变量的名称对其他文件中的代码是可见的。 关键字`static`可以用在类的数据成员上，也可以用在类上定义的方法上，这两个关键字都有有趣的效果，将在[第 6 章](06.html)、*类*中描述。\n\n关键字`static`表示该变量只能在当前文件中使用。 关键字`extern`则相反；变量(或函数)具有外部链接，可以在项目中的其他文件中访问。 在大多数情况下，您将在一个源文件中定义一个变量，然后在头文件中将其声明为`extern`，以便同一变量可以在其他源文件中使用。\n\n最终的存储类说明符是`thread_local`。 这是 C++ 11 的新特性，仅适用于多线程代码。 本书不涉及线程化，因此这里只作简要说明。\n\n线程是执行和并发的单位。 一个程序中可以有多个线程运行，也可以有两个或多个线程同时运行相同的代码。 这意味着两个不同的执行线程可以访问和更改同一变量。 由于并发访问可能会产生不良影响，因此多线程代码通常需要采取措施来确保任何时候只有一个线程可以访问数据。 如果不小心编写这样的代码，就会有死锁的危险，线程的执行会因为独占访问变量而暂停(在最坏的情况下是无限期的)，从而抵消了使用线程的好处。\n\n`thread_local`存储类表示每个线程都有自己的变量副本。 因此，如果两个线程访问同一个函数，并且该函数中的一个变量被标记为`thread_local,`，这意味着每个线程只看到它所做的更改。\n\n您有时会看到旧 C++ 代码中使用的存储类`register`。 这一点现在已弃用。 它被用来提示编译器该变量对程序的性能有重要影响，并建议编译器如果可能的话，应该使用 CPU 寄存器来保存该变量。 编译器可以忽略此建议。 事实上，在 C++ 11 中，编译器确实忽略了关键字；带有`register`变量的代码编译时不会出现错误或警告，编译器会根据需要优化代码。\n\n尽管`volatile`关键字不是存储类说明符，但它对编译器代码优化有影响。 关键字`volatile`表示变量(可能通过对某些硬件的**直接内存访问**(**DMA**)可以通过外部操作进行更改，因此编译器*不应用任何优化非常重要。*\n\n还有另一个名为`mutable`的存储类修饰符。 这只能用于类成员，因此将在[第 6 章](06.html)、*类*中介绍。\n\n# 使用类型别名\n\n有时，类型的名称可能会变得相当繁琐。 如果使用嵌套命名空间，则类型的名称包括使用的所有命名空间。 如果定义参数化类型(本章到目前为止使用的示例是`vector`和`tuple`)，则参数会增加类型的名称。 例如，前面我们看到一个容器，里面放着音乐家的名字和出生年份：\n\n```cpp\n    // #include <string> \n    // #include <vector> \n    // #include <tuple> \n\n    vector<tuple<string, int> > beatles;\n```\n\n这里，容器是一个`vector`，它保存的项是`tuple`项，每个项都包含一个字符串和一个整数。 要使该类型更易于使用，您可以定义一个预处理器符号：\n\n```cpp\n    #define name_year tuple<string, int>\n```\n\n现在，您可以在代码中使用`name_year`而不是`tuple`，并且预处理器将在编译代码之前将符号替换为该类型：\n\n```cpp\n    vector<name_year> beatles;\n```\n\n但是，因为`#define`是一个简单的搜索和替换，所以可能会出现本书前面解释的问题。 C++ 提供`typedef`语句为类型创建别名：\n\n```cpp\n    typedef tuple<string, int> name_year_t; \n    vector<name_year_t> beatles;\n```\n\n这里，为`tuple<string, int>`创建了一个名为`name_year_t`的别名。\n\n对于`typedef,`，别名通常出现在行尾，前面是它的别名类型。 这与`#define,`的顺序相反，在`#define,`中，您要定义的符号在`#define`之后，然后是其定义。 另请注意，`typedef`以分号结尾。 使用函数指针会变得复杂得多，正如您将在[第 5 章](05.html)、*使用函数*中看到的那样。\n\n现在，无论您想在哪里使用`tuple`，都可以使用别名：\n\n```cpp\n    for (name_year_t musician : beatles) \n    { \n        cout << get<0>(musician) << \" \" << get<1>(musician) << endl; \n    }\n```\n\n您可以`typedef`别名：\n\n```cpp\n    typedef tuple<string, int> name_year_t; \n    typedef vector<name_year_t> musician_collection_t; \n    musician_collection_t beatles2;\n```\n\n`beatles2`变量的类型为`vector<tuple<string, int>>`。 需要注意的是，`typedef`会创建别名；它不会创建新类型，因此您可以在原始类型及其别名之间切换。\n\n关键字`typedef`是在 C++ 中创建别名的成熟方法。\n\nC++ 11 引入了另一种创建类型别名的方法，即`using`语句：\n\n```cpp\n    using name_year = tuple<string, int>;\n```\n\n同样，这不会创建新类型，它会为相同类型创建一个新名称，并且在语义上与`typedef`相同。 `using`语法比使用`typedef`更具可读性，而且它还允许您使用模板。\n\n创建别名的`using`方法比`typedef`更具可读性，因为赋值的使用遵循变量的约定，即左边的新名称用于`=`右边的类型。\n\n# 在记录类型中聚合数据\n\n通常，您将拥有相关且必须一起使用的数据：聚合类型。 这样的记录类型允许您将数据封装到单个变量中。 C++ 继承了 C`struct`和`union`，作为提供记录的方式。\n\n# 构筑物\n\n在大多数应用中，您需要将几个数据项关联在一起。 例如，您可能希望定义一个时间记录，该时间记录包含以下各项的整数：指定时间的小时、分钟和秒。 您可以这样声明它们：\n\n```cpp\n    // start work \n    int start_sec = 0; \n    int start_min = 30; \n    int start_hour = 8; \n\n    // end work \n    int end_sec = 0 \n    int end_min = 0; \n    int end_hour = 17;\n```\n\n这种方法变得相当麻烦且容易出错。 没有封装，也就是说，`_min`变量可以与其他变量隔离使用。 如果在没有它所指的小时的情况下使用，小时之后的*分钟是否有意义？ 您可以定义与以下项目相关联的结构：*\n\n```cpp\n    struct time_of_day \n    { \n        int sec; \n        int min; \n        int hour; \n    };\n```\n\n现在，您已将这三个值作为一条记录的一部分，这意味着您可以声明此类型的变量；尽管您可以访问单个项，但很明显数据与其他成员相关联：\n\n```cpp\n    time_of_day start_work; \n    start_work.sec = 0; \n    start_work.min = 30; \n    start_work.hour = 8; \n\n    time_of_day end_work; \n    end_work.sec = 0; \n    end_work.min = 0; \n    end_work.hour = 17; \n\n    print_time(start_work); \n    print_time(end_work);\n```\n\n现在我们有两个变量：一个表示开始时间，另一个表示结束时间。 `struct`的成员封装在`struct`中，也就是说，您可以通过`struct`的实例访问该成员。 为此，您可以使用点运算符。 在这段代码中，`start_work.sec`表示您正在访问名为`start_work`的`time_of_day`结构实例的`sec`成员。 默认情况下，结构的成员是`public`，也就是说，`struct`之外的代码可以访问这些成员。\n\nClasses and structures can indicate the level of member access, and [Chapter 6](06.html), *Classes*, will show how to do this. For example, it is possible to mark some members of a `struct` as `private`, which means that only code that is a member of the type can access the member.\n\n调用名为`print_time`的助手函数将数据打印到控制台：\n\n```cpp\n    void print_time(time_of_day time) \n    { \n        cout << setw(2) << setfill('0') << time.hour << \":\"; \n        cout << setw(2) << setfill('0') << time.min << \":\"; \n        cout << setw(2) << setfill('0') << time.sec << endl; \n    }\n```\n\n在这种情况下，`setw`和`setfill`操作符用于将下一个插入项的宽度设置为两个字符，并用零填充任何未填充的位置(更多详细信息将在[第 9 章](09.html)，*中使用字符串*给出；实际上，`setw`给出了下一个插入数据占据的列的大小，`setfill`指定了使用的填充字符)。\n\n[第 5 章](05.html)，*使用函数*将更详细地介绍将结构传递给函数的机制和最有效的方法，但出于本节的目的，我们将在这里使用最简单的语法。 重要的是，调用者使用`struct`将三项数据关联在一起，并且所有项都可以作为一个单元传递给函数。\n\n# 正在初始化\n\n有几种方法可以初始化结构的实例。 前面的代码显示了一种方法：使用点运算符访问成员，并为其赋值。 还可以通过专门提供的称为构造函数的函数为`struct`的实例赋值。 由于有关于如何命名构造函数以及可以在其中做什么的特殊规则，因此这将留到[第 6 章](06.html)、*CLASS*。\n\n还可以使用大括号(`{}`)使用列表初始化式语法来初始化结构。 大括号中的项应该按照声明的成员顺序匹配`struct`的成员。 如果提供的值少于成员的数量，则剩余的成员将初始化为零。 实际上，如果在花括号之间没有提供任何项，则所有成员都设置为零。 提供的初始值设定项多于成员数量是错误的。 因此，请使用前面定义的`time_of_day`记录类型：\n\n```cpp\n    time_of_day lunch {0, 0, 13}; \n    time_of_day midnight {}; \n    time_of_day midnight_30 {0, 30};\n```\n\n在第一个示例中，将`lunch`变量初始化为 1 PM。 请注意，因为`hour`成员被声明为类型中的第三个成员，所以它是使用初始化列表中的第三项进行初始化的。 在第二个示例中，所有成员都设置为零，当然，零小时是午夜。 第三个示例提供了两个值，因此它们用于初始化`sec`和`min`。\n\n您可以拥有`struct`的成员，该成员本身就是`struct`，这是使用嵌套大括号进行初始化的：\n\n```cpp\n    struct working_hours \n    { \n        time_of_day start_work; \n        time_of_day end_work; \n    }; \n\n    working_hours weekday{ {0, 30, 8}, {0, 0, 17} }; \n    cout << \"weekday:\" << endl; \n    print_time(weekday.start_work); \n    print_time(weekday.end_work);\n```\n\n# 结构场\n\n结构可以具有小到单个位的成员，称为**位字段**。 在本例中，您使用成员将占用的位数声明一个整数成员。 您可以声明未命名的成员。 例如，您可能有一个结构，其中包含有关项目长度以及项目是否已更改(脏)的信息。 此引用的项的最大大小为 1,023，因此您需要一个宽度至少为 10 位的整数来保存它。 您可以使用`unsigned short`来保存长度和脏信息：\n\n```cpp\n    void print_item_data(unsigned short item) \n    { \n        unsigned short size = (item & 0x3ff); \n        char *dirty = (item > 0x7fff) ? \"yes\" : \"no\"; \n\n        cout << \"length \" << size << \", \"; \n        cout << \"is dirty: \" << dirty << endl; \n    }\n```\n\n这段代码将这两段信息分开，然后将它们打印出来。 像这样的位图对代码非常不友好。 您可以使用 a`struct`保存此信息，使用 a`unsigned short`保存 10 位长度信息，使用 a`bool`保存脏信息。 使用位域可以定义如下结构：\n\n```cpp\n    struct item_length \n    { \n        unsigned short len : 10; \n        unsigned short : 5; \n        bool dirty : 1; \n    };\n```\n\n`len`成员被标记为`unsigned short`，但只需要 10 位，因此使用冒号语法来说明这一点。 类似地，布尔的 yes/no 值可以仅包含在一位中。 该结构表明这两个值之间有 5 位未使用，因此没有名称。\n\n字段只是为了方便。 尽管看起来`item_length`结构应该只占用 16 位(`unsigned short`)，但不能保证编译器会这样做。 如果您从文件或网络流接收到`unsigned short`，则必须自己解压比特：\n\n```cpp\n    unsigned short us = get_length(); \n    item_length slen; \n    slen.len = us & 0x3ff; \n    slen.dirty = us > 0x7fff;\n```\n\n# 使用结构名称\n\n在某些情况下，可能需要在实际定义类型之前使用该类型。 只要不使用成员，就可以在定义类型之前声明它：\n\n```cpp\n    struct time_of_day; \n    void print_day(time_of_day time);\n```\n\n这可以在标题中声明，在标题中说明在其他地方定义了一个函数，该函数获取`time_of_day`记录并将其打印出来。 为了能够声明`print_day`函数，您必须声明`time_of_day`名称。 在定义函数之前，必须在代码中的其他位置定义`time_of_day`结构，否则将出现*未定义类型*错误。\n\n但是，有一个例外：在完全声明类型之前，类型可以保存指向同一类型实例的指针。 这是因为编译器知道指针的大小，因此可以为成员分配足够的内存。 只有在定义了整个类型之后，才能创建该类型的实例。 这方面的经典例子是链表，但由于这需要使用指针和动态分配，因此将留待下一章讨论。\n\n# 确定路线\n\n结构的用途之一是，如果您知道数据是如何保存在内存中的，就可以将结构作为内存块来处理。 如果您有一个映射到内存的硬件设备，其中不同的内存位置指的是控制该设备的值或从该设备返回值，这将非常有用。 访问设备的一种方法是定义一个结构，该结构与设备对 C++ 类型的直接内存访问的内存布局相匹配。 此外，结构对于文件或需要通过网络传输的数据包也很有用：您可以操作结构，然后将结构占用的内存复制到文件或网络流中。\n\n结构的成员按照它们在类型中声明的顺序在内存中排列。 根据每种类型的需要，这些项目将至少占用*大小的内存。 一个成员可能会占用比类型要求更多的内存，其原因是一种称为**对齐**的机制。*\n\n *就内存使用或访问速度而言，编译器将以最高效的方式将变量放置在内存中。 各种类型将与路线边界对齐。 例如，32 位整数将与 4 字节边界对齐，如果下一个可用内存位置不在此边界上，编译器将跳过几个字节，并将该整数放在下一个对齐边界。 您可以使用传递类型名称的`alignof`运算符测试特定类型的对齐：\n\n```cpp\n    cout << \"alignment boundary for int is \"  0\n        << alignof(int) << endl;                     // 4 \n    cout << \"alignment boundary for double is \"  \n        << alignof(double) << endl;                  // 8\n```\n\n`int`的对齐方式为 4，这意味着将在内存中的下一个四字节边界放置一个`int`变量。 `double`的对齐方式是 8，这是有意义的，因为在 Visual C++ 中，a`double`占用 8 个字节。 到目前为止，`alignof`的结果看起来与`sizeof`相同；但事实并非如此。\n\n```cpp\n    cout << \"alignment boundary for time_of_day is \"  \n        << alignof(time_of_day) << endl;             // 4\n```\n\n此示例打印`time_of_day`字符串结构的对齐方式，我们之前将其定义为三个整数。 此`struct`的对齐方式为 4，即`struct`中最大项的对齐方式。 这意味着`time_of_day`的一个实例将被放置在 4 字节边界上；它没有说明`time_of_day`变量中的项将如何对齐。\n\n例如，考虑下面的`struct`，它有四个成员，分别占用一个、两个、四个和八个字节：\n\n```cpp\n    struct test \n    { \n        uint8_t  uc; \n        uint16_t us; \n        uint32_t ui; \n        uint64_t ull; \n    }\n```\n\n编译器会告诉您对齐方式是 8(最大项的对齐方式，`ull`)，但是大小是 16，这可能看起来有点奇怪。 如果每个项目都在 8 字节边界上对齐，则大小必须为 32(4 乘以 8)。 如果这些项存储在内存中并尽可能有效地打包，则大小将为 15。相反，发生的情况是第二个项在两个字节的边界上对齐，这意味着在`uc`和`us`之间有一个字节的未使用空间。\n\n![](img/7623b00b-908e-4290-9bc8-897ab73ff91e.png)\n\n如果您想要将内部项对齐到(比方说)与`uint32_t`变量使用的边界相同的边界上，您可以用`alignas`标记一个项并给出所需的对齐方式。 请注意，因为 8 大于 4，所以在 8 字节边界上对齐的任何项目也将在 4 字节边界上对齐：\n\n```cpp\n    struct test \n    { \n        uint8_t  uc; \n        alignas(uint32_t) uint16_t us; \n        uint32_t ui; \n        uint64_t ull; \n    }\n```\n\n`uc`项将在 4 字节边界上对齐(`alignof(test)`将为 8)，它将占用一个字节。 `us`成员是`uint16_t`，但它被标记为`alignas(uint32_t)`，也就是说，它应该以与`uint32_t`相同的方式对齐，即在 4 字节边界上对齐。 这意味着`uc`和`us`都将位于提供填充的 4 字节边界上。 当然，`ui`成员也将在 4 字节边界上对齐，因为它是`uint32_t`。\n\n如果`struct`只有这三个成员，那么大小应该是 12。但是，`struct`还有另一个成员，即 8 字节的`ull`成员。 这必须在 8 字节边界上对齐，这意味着从`struct`开始的 16 个字节，要做到这一点，在`ui`和`ull`之间需要有 4 个字节的填充。 因此，`test`的大小现在报告为 24：对于`uc`和`us`是 4 字节(因为下面的项`ui`必须在下一个 4 字节边界上对齐)，对于`ull`是 8 字节(因为它是 8 字节整数)，对于`ui`是 8 字节，因为下面的项(`ull`)必须在下一个 8 字节边界上。\n\n下图显示了`test`类型的各种成员在内存中的位置：\n\n![](img/87f6fc03-7112-4760-a585-5d112f9cc07c.png)\n\n不能使用`alignas`放宽对齐要求，因此不能将`uint64_t`变量标记为在两字节边界(也不是八字节边界)上对齐。\n\n在大多数情况下，您不需要担心对齐问题；但是，如果您要访问内存映射设备或来自文件的二进制数据，则如果您可以将此数据直接映射到`struct`会很方便，在这种情况下，您会发现您必须密切注意对齐。 这称为**普通旧数据**，您经常会看到称为**POD 类型**的结构。\n\nPOD is an informal description, and sometimes it is used to describe types that have a simple construction and do not have virtual members (see [Chapter 6](06.html), *Classes* and [Chapter 7](07.html), *Introduction to Object-Oriented Programming*). The standard library provides a function in `<type_traits>` called `is_pod` that tests a type for these members.\n\n# 使用联合将数据存储在同一内存中\n\n联合是一个结构，其中所有成员占用相同的内存。 这种类型的大小是最大成员的大小。 由于联合只能保存一项数据，因此它是一种以多种方式解释数据的机制。\n\n联合的一个例子是`VARIANT`类型，它用于在 Microsoft 的**组件对象模型**(**COM**)中的**对象链接和嵌入**(**OLE**)对象之间传递数据。 `VARIANT`类型可以保存 COM 能够在 OLE 对象之间传输的任何数据类型的数据。 有时 OLE 对象会在同一进程中，但它们可能在同一台计算机或不同计算机上的不同进程中。 COM 保证它可以在不需要开发者提供任何额外网络代码的情况下传输`VARIANT`数据。 结构很复杂，但编辑后的版本如下所示：\n\n```cpp\n    // edited version \n    struct VARIANT \n    { \n        unsigned short vt; \n        union \n        { \n            unsigned char bVal; \n            short iVal; \n            long lVal; \n            long long llVal; \n            float fltVal; \n            double dblVal; \n       }; \n    };\n```\n\n请注意，您可以使用没有名称的联合：这是一个匿名的`union`，从成员访问的角度来看，您可以访问该联合的成员，就像它是包含它的`VARIANT`的成员一样。 对于可以在 OLE 对象之间传输的每种类型，`union`都包含一个成员，而`vt`成员则指示使用哪种类型。 创建`VARIANT`实例时，必须将`vt`设置为适当的值，然后初始化相关成员：\n\n```cpp\n    enum VARENUM \n    { \n        VT_EMPTY = 0,  \n        VT_NULL = 1,  \n        VT_UI1 = 17,  \n        VT_I2 = 2,  \n        VT_I4 = 3,  \n        VT_I8 = 20, \n        VT_R4 = 4,  \n        VT_R8 = 5  \n    };\n```\n\n该记录确保只使用所需的内存，并且将数据从一个进程传输到另一个进程的代码将能够读取`vt`成员，以确定需要如何处理数据以便可以传输：\n\n```cpp\n    // pseudo code, real VARIANT should not be handled like this \n    VARIANT var {}; // clear all items \n    var.vt = VT_I4; // specify the type \n    var.lVal = 42;  // set the appropriate member \n    pass_to_object(var);\n```\n\n请注意，您必须遵守规则，并且只能初始化适当的成员。 当您的代码接收到`VARIANT,`时，您必须读取`vt`以查看应该使用哪个成员来访问数据。\n\n通常，在使用联合时，您应该只访问您初始化的项目：\n\n```cpp\n    union d_or_i {double d; long long i}; \n    d_or_i test; \n    test.i = 42; \n    cout << test.i << endl; // correct use \n    cout << test.d << endl; // nonsense printed\n```\n\n# 访问运行时类型信息\n\nC++ 提供了一个名为`typeid`的运算符，它将在运行时返回有关变量(或类型)的类型信息。 **运行时类型信息**(**RTTI**)在您使用可以以**多态**方式使用的自定义类型时非常重要；详细信息将留待后面章节讨论。 RTTI 允许您在运行时检查变量的类型，并相应地处理变量。 RTTI 通过`type_info`对象(在`<typeinfo>`头文件中)返回：\n\n```cpp\n    cout << \"int type name: \" << typeid(int).name() << endl; \n    int i = 42; \n    cout << \"i type name: \" << typeid(i).name() << endl;\n```\n\n在这两种情况下，您都会看到 int 被打印为文字。 `type_info`类定义比较运算符(`==`和`!=`)，以便您可以比较类型：\n\n```cpp\n    auto a = i; \n    if (typeid(a) == typeid(int)) \n    { \n        cout << \"we can treat a as an int\" << endl; \n    }\n```\n\n# 确定类型限制\n\n`<limits>`头包含一个名为`numeric_limits`的模板类，它通过为每个内置类型提供的专门化来使用。 使用这些类的方法是在尖括号中提供所需信息的类型，然后使用作用域解析操作符(`::`)调用类上的`static`成员。 (有关类的`static`函数的详细信息将在[章](06.html)、*类*中给出)。 以下命令将`int`类型的限制打印到控制台：\n\n```cpp\n    cout << \"The int type can have values between \"; \n    cout << numeric_limits<int>::min() << \" and  \"; \n    cout << numeric_limits<int>::max() << endl;\n```\n\n# 在类型之间转换\n\n即使您非常努力地在代码中使用正确的类型，有时您也会发现必须在类型之间进行转换。 例如，您可能正在使用返回特定类型的值的库函数，或者您可能正在从与例程类型不同的外部源读取数据。\n\n对于内置类型，有关于不同类型之间转换的标准规则，其中一些规则将是自动的。 例如，如果您有一个类似`a + b`的表达式，并且`a`和`b`是不同的类型，那么，如果可能的话，编译器会自动将一个变量的值转换为另一个变量的类型，并调用该类型的`+`运算符。\n\n在其他情况下，您可能需要强制将一种类型转换为另一种类型，以便调用正确的运算符，这将需要某种类型的强制转换。 C++ 允许您使用类似 C 的强制转换，但是这些类型没有运行时测试，所以使用 C++ 强制转换要好得多，它具有不同级别的运行时检查和类型安全。\n\n# 类型转换\n\n内置转换可能只有两种结果之一：提升或缩小。 升级是指将较小的类型升级为较大的类型，并且您不会丢失数据。 当将较大类型的值转换为可能会丢失数据的较小类型时，会发生缩小转换。\n\n# 促进转换\n\n在混合类型表达式中，编译器将尝试将较小的类型提升为较大的类型。 因此，可以在需要`int`的表达式中使用`char`或`short`，因为它可以升级为更大的类型，而不会丢失数据。\n\n考虑一个声明为接受`int`参数的函数：\n\n```cpp\n    void f(int i);\n```\n\n我们可以这样写：\n\n```cpp\n    short s = 42; \n    f(s); // s is promoted to int\n```\n\n在这里，变量`s`被静默转换为`int`。 有些情况可能看起来很奇怪：\n\n```cpp\n    bool b = true; \n    f(b); // b is promoted to int\n```\n\n同样，转换是沉默的。 编译器假设您知道自己在做什么，并且您的意图是希望将`false`视为 0，将`true`视为 1。\n\n# 缩小转换范围\n\n在某些情况下，会出现*变窄*。 请非常小心，因为它会丢失数据。 下面，我们尝试将`double`转换为`int`。\n\n```cpp\n    int i = 0.0;\n```\n\n这是允许的，但编译器将发出警告：\n\n```cpp\nC4244: 'initializing': conversion from 'double' to 'int', possible loss of data\n```\n\n此代码显然是错误的，但该错误不是错误，因为它可能是故意的。 例如，在下面的代码中，我们有一个函数，其参数是浮点数，在例程中，该参数用于初始化`int`：\n\n```cpp\n    void calculation(double d) \n    { \n        // code \n        int i = d; \n\n        // use i \n        // other code \n    }\n```\n\n这可能是故意的，但是因为会损失精确度，所以您应该记录下为什么要这样做。 至少，使用强制转换操作符，这样您就可以很明显地理解操作的结果。\n\n# 缩小到布尔值\n\n如前所述，指针、整数和浮点值可以隐式转换为`bool`，其中非零值转换为`true`，零值转换为`false`。 这可能会导致很难注意到的令人讨厌的错误：\n\n```cpp\n    int x = 0; \n    if (x = 1) cout << \"not zero\" << endl; \n    else       cout << \"is zero\" << endl;\n```\n\n在这里，编译器看到赋值表达式`x = 1`，这是一个错误；它应该是比较`x == 1`。 但是，这是有效的 C++，因为表达式的值是 1，而编译器很有帮助地将其转换为`bool`值`true`。 这段代码将在没有任何警告的情况下编译，它不仅会产生与预期相反的结果(您将在控制台上看到打印的`not zero`)，而且赋值还会更改在整个程序中传播错误的变量的值。\n\n通过养成总是构造比较的习惯，以便潜在赋值的 r 值在左边，很容易避免这个错误。 相比之下，将不会有右值或左值的概念，因此当赋值不是预期的时候，这将使用编译器来捕获赋值：\n\n```cpp\n    if (1 = x) // error \n    cout << \"not zero\" << endl;\n```\n\n# 转换带符号的类型\n\n签名到未签名的转换可能会发生，并可能导致意外的结果。 例如：\n\n```cpp\n    int s = -3; \n    unsigned int u = s;\n```\n\n将为`unsigned short`变量赋值`0xfffffffd`，即两个变量的补值为 3。这可能是您想要的结果，但这是一种奇怪的获取方式。\n\n有趣的是，如果您尝试比较这两个变量，编译器将发出警告：\n\n```cpp\n    if (u < s) // C4018 \n    cout << \"u is smaller than s\" << endl;\n```\n\n这里给出的 Visual C++ 警告 C4018 是`'<': signed/unsigned mismatch`，它说明不能比较有符号类型和无符号类型，为此需要强制转换。\n\n# 铸件，铸造品\n\n在某些情况下，您必须在类型之间进行转换。 例如，这可能是因为提供数据的类型与您用来处理数据的例程不同。 您可能有一个库将浮点数处理为`float`，但您的数据输入为`double`。 您知道转换将失去精度，但知道这对最终结果影响不大，因此您不希望编译器警告您。 您要做的是告诉编译器一种类型到另一种类型的强制是可以接受的。\n\n下表总结了您可以在 C++ 11 中使用的各种强制转换操作：\n\n| **名称** | **语法** |\n| 建造 / 建筑物 / 解释 / 造句 | `{}` |\n| 删除`const`要求 | `const_cast` |\n| 不带运行时检查的强制转换 | `static_cast` |\n| 类型的按位强制转换 | `reinterpret_cast` |\n| 类指针之间的强制转换，带有运行时检查 | `dynamic_cast` |\n| C 样式 | `()` |\n| 函数样式 | `()` |\n\n# 抛开恒久不变\n\n正如上一章所提到的，`const`说明符用于向编译器指示项不会更改，并且代码更改该项的任何尝试都是错误的。 还有另一种使用此说明符的方法，这将在下一章中介绍。 将`const`应用于指针时，表示该指针所指向的内存不能更改：\n\n```cpp\n    char *ptr = \"0123456\"; \n    // possibly lots of code \n    ptr[3] = '\\0'; // RUNTIME ERROR!\n```\n\n这段写得很糟糕的代码告诉编译器创建一个值为`0123456`的字符串常量，然后将该内存的地址放入字符串指针`ptr`。 最后一行尝试写入字符串。 这将进行编译，但会在运行时导致访问冲突。 将`const`应用于指针声明将确保编译器检查此类情况：\n\n```cpp\n    const char *ptr = \"0123456\";\n```\n\n更典型的情况是将`const`应用于作为函数参数的指针，其目的是相同的：它向编译器指示指针指向的数据应该是只读的。 但是，在某些情况下，您可能需要删除此类指针的`const`属性，这是使用`const_cast`运算符执行的：\n\n```cpp\n    char * pWriteable = const_cast<char *>(ptr); \n    pWriteable[3] = '\\0';\n```\n\n语法很简单。 尖括号(`<>`)中给出了要转换为的类型，括号中提供了变量(它是`const`指针)。\n\n还可以将指针*转换为*`const`指针。 这意味着您可以使用一个指针来访问内存，以便对其进行写入，然后在进行更改后，您可以创建一个指向内存的`const`指针，实际上是通过该指针使内存成为只读的。\n\n显然，一旦丢弃了指针的不变性，您就需要为写入内存所造成的损害负责，因此代码中的`const_cast`运算符是您在代码审查期间检查代码的一个很好的标记。\n\n# 在没有运行时检查的情况下进行强制转换\n\n大多数强制转换都是使用`static_cast`运算符执行的，这可用于将指针转换为相关指针类型以及在数值类型之间进行转换。 没有执行运行时检查，因此您应该确保转换是可接受的：\n\n```cpp\n    double pi = 3.1415; \n    int pi_whole = static_cast<int>(pi);\n```\n\n这里，a`double`被转换为`int`，这意味着小数部分被丢弃。 通常，编译器会发出数据丢失的警告，但`static_cast`操作符表明这是您的意图，因此没有给出警告。\n\n运算符通常用于将`void*`指针转换为类型化指针。 在下面的代码中，`unsafe_d`函数假定参数是指向内存中双精度值的指针，因此它可以将`void*`指针转换为`double*`指针。 与`pd`指针*一起使用的`*`运算符取消引用*指针，以给出它所指向的数据。 因此，`*pd`表达式将返回`double`。\n\n```cpp\n    void unsafe_d(void* pData) \n    { \n       double* pd = static_cast<double*>(pData); \n       cout << *pd << endl; \n    }\n```\n\n这是不安全的，因为您依赖调用方来确保指针实际指向`double`。 可以这样称呼它：\n\n```cpp\n    void main() \n    { \n       double pi = 3.1415; \n       unsafe_d(&pi);       // works as expected \n\n       int pi_whole = static_cast<int>(pi); \n       unsafe_d(&pi_whole); // oops! \n    }\n```\n\n`&`运算符将操作数的内存地址作为类型化指针返回。 在第一种情况下，获取`double*`指针并将其传递给`unsafe_d`函数。 编译器会自动将此指针转换为`void*`参数。 编译器自动执行此操作，而不检查指针是否在函数中正确使用。 第二次调用`unsafe_d`说明了这一点，其中将`int*`指针转换为`void*`参数，然后在`unsafe_d`函数中，即使指针指向`int`，它也会由`static_cast`强制转换为`double*`。 因此，取消引用将返回不可预测的数据，`cout`将打印无稽之谈。\n\n# 在不进行运行时检查的情况下强制转换指针\n\n`reinterpret_cast`运算符允许将指向一种类型的指针转换为另一种类型的指针，它可以从指针转换为整数，也可以将整数转换为指针：\n\n```cpp\n    double pi = 3.1415; \n    int i = reinterpret_cast<int>(&pi); \n    cout << hex << i << endl;\n```\n\n与`static_cast`不同，此运算符始终涉及指针：在指针之间转换，从指针转换为整型，或从整型转换为指针。 在本例中，指向`double`变量的指针被转换为`int`，并将值打印到控制台。 实际上，这会打印出变量的内存地址。\n\n# 使用运行时检查进行强制转换\n\n`dynamic_cast`运算符用于在相关类之间转换指针，因此将在[第 6 章](06.html)、*类*中进行说明。 该运算符涉及运行时检查，因此只有在操作数可以转换为指定类型时才执行转换。 如果无法进行转换，则操作符返回`nullptr`，使您有机会只使用指向该类型的实际对象的已转换指针。\n\n# 使用列表初始值设定项进行强制转换\n\nC++ 编译器将允许一些隐式转换；在某些情况下，它们可能是有意的，在某些情况下，它们可能不是。 例如，下面的代码类似于前面显示的代码：将变量初始化为`double`值，然后在代码中使用它来初始化`int`。 编译器将执行转换，并发出警告：\n\n```cpp\n    double pi = 3.1415; \n    // possibly loss of code \n    int i = pi;\n```\n\n如果忽略警告，则可能不会注意到这种精度损失，这可能会导致问题。 解决此问题的一种方法是使用大括号进行初始化：\n\n```cpp\n    int i = {pi};\n```\n\n在这种情况下，如果`pi`可以无损失地转换为`int`(例如，如果`pi`是`short`)，则代码将在没有任何警告的情况下编译。 但是，如果`pi`是不兼容的类型(在本例中为`double`)，则编译器将发出错误：\n\n```cpp\nC2397: conversion from 'double' to 'int' requires a narrowing conversion\n```\n\n这里有一个有趣的例子。 `char`类型是整数，但`osteam`类中`char`的`<<`运算符将`char`变量解释为字符，而不是数字，如下所示：\n\n```cpp\n    char c = 35; \n    cout << c << endl;\n```\n\n这将在控制台上打印`#`，而不是 35，因为 35 是“#”的 ASCII 代码。 要将变量作为数字处理，可以使用以下方法之一：\n\n```cpp\n    cout << static_cast<short>(c) << endl; \n    cout << short{ c } << endl;\n```\n\n如您所见，第二个版本(构造)同样具有可读性，但比第一个版本短。\n\n# 使用 C 语言强制转换\n\n最后，您可以使用 C 样式转换，但提供这些转换只是为了编译遗留代码。 您应该改用一个 C++ 强制转换。 为完整起见，下面显示了 C 样式的强制转换：\n\n```cpp\n    double pi = 3.1415; \n    float f1 = (float)pi; \n    float f2 = float(pi);\n```\n\n有两个版本：第一个强制转换操作符包含要强制转换到的类型周围的圆括号，而在第二个版本中，强制转换看起来像是一个函数调用。 在这两种情况下，最好使用`static_cast`，以便进行编译时检查。\n\n# 使用 C++ 类型\n\n在本章的最后一部分中，我们将开发一个命令行应用，它允许您以字母数字和十六进制混合格式打印文件内容。\n\n应用必须使用文件名运行，但您也可以指定要打印的行数。 应用将在控制台上打印文件内容，每行 16 字节。 在左边，它给出十六进制表示，在右边，它给出可打印的表示(如果字符不在可打印的非扩展 ASCII 范围内，则给出一个点)。\n\n![](img/39e2cc9a-8fb1-46fe-bb11-17960072dfb7.png)\n\n在`C:\\Beginning_C++ `下创建一个名为`Chapter_03`的新文件夹。 启动 Visual C++ 并创建一个 C++ 源文件，并将其保存到您刚刚创建的文件夹`hexdump.cpp`。 添加一个简单的`main`函数，该函数允许应用接受参数，并支持使用 C++ 流进行输入和输出：\n\n```cpp\n    #include <iostream> \n\n    using namespace std; \n\n    int main(int argc, char* argv[]) \n    { \n    }\n```\n\n应用最多有两个参数：第一个是文件名，第二个是要在命令行上打印的 16 字节块的数量。 这意味着您需要检查参数是否有效。 首先添加`usage`函数以提供应用参数，如果使用非空参数调用，则打印出一条错误消息：\n\n```cpp\n    void usage(const char* msg) \n    { \n        cout << \"filedump filename blocks\" << endl; \n        cout << \"filename (mandatory) is the name of the file to dump\"  \n            << endl; \n        cout << \"blocks (option) is the number of 16 byte blocks \" \n            << endl; \n        if (nullptr == msg) return; \n        cout << endl << \"Error! \"; \n        cout << msg << endl; \n    }\n```\n\n在`main`函数之前添加此函数，以便可以从那里调用它。 可以使用指向 C 字符串的指针或使用`nullptr`调用该函数。 该参数为`const`，向编译器表明该字符串不会在函数中更改，因此如果有人试图更改该字符串，编译器将生成错误。\n\n将以下行添加到`main`函数：\n\n```cpp\n    int main(int argc, char* argv[]) \n    { \n if (argc < 2) { usage(\"not enough parameters\"); return 1; } if (argc > 3) { usage(\"too many parameters\"); return 1; } // the second parameter is file name string filename = argv[1]; \n    }\n```\n\n编译文件并确认没有打字错误。 由于此应用使用 C++ 标准库，因此您必须使用`/EHsc`开关提供对 C++ 异常的支持：\n\n```cpp\ncl /EHsc hexdump.cpp\n```\n\n您可以使用 0、1、2 和 3 个参数测试从命令行调用它的应用。 确认应用只允许在命令行上使用一个或两个参数调用它(这实际上意味着两个或三个参数，因为`argc`和`argv`包括应用名称)。\n\n下一项任务是确定用户是否提供了一个数字来指示要转储到控制台的 16 字节块的数量，如果提供了，则将命令行提供的字符串转换为整数。 此代码将使用`istringstream`类执行从字符串到数字的转换，因此您需要包括定义该类的头文件。 将以下内容添加到文件顶部：\n\n```cpp\n    #include <iostream>\n #include <sstream>\n```\n\n在声明`filename`变量之后，添加以下突出显示的代码：\n\n```cpp\n    string filename = argv[1]; \n int blocks = 1;  // default value if (3 == argc) { // we have been passed the number of blocks istringstream ss(argv[2]); ss >> blocks; if (ss.fail() || 0 >= blocks) { // cannot convert to a number usage(\"second parameter: must be a number,\" \"and greater than zero\"); return 1; } }\n```\n\n默认情况下，应用将从文件中转储一行数据(最多 16 个字节)。 如果用户提供了不同的行数，则使用`istringstream`对象将字符串格式的数字转换为整数。 这是用参数初始化的，然后从流对象中提取数字。 如果用户键入的值为零，或者如果他们键入的值无法解释为字符串，则代码将打印一条错误消息。 错误字符串被分成两行，但它仍然是一个字符串。\n\n请注意，`if`语句使用短路；也就是说，如果表达式的第一部分(`ss.fail()`，表示转换失败)是`true`，则不会计算第二个表达式(`0 >= blocks`，即`blocks`必须大于零)。\n\n编译此代码并尝试多次。 例如：\n\n```cpp\n hexdump readme.txt\n hexdump readme.txt 10\n hexdump readme.txt 0\n hexdump readme.txt -1\n```\n\n前两个命令运行时应该没有错误；后两个命令应该会产生错误。\n\nDon't worry that `readme.txt` does not exist, as it is only here as a test parameter.\n\n接下来，您将添加打开文件并对其进行处理的代码。 由于您将使用`ifstream`类从文件输入数据，因此请将以下头文件添加到该文件的顶部：\n\n```cpp\n    #include <iostream> \n    #include <sstream> \n #include <fstream>\n```\n\n然后在`main`函数的底部添加打开文件的代码：\n\n```cpp\n    ifstream file(filename, ios::binary); \n    if (!file.good()) \n    { \n        usage(\"first parameter: file does not exist\"); \n        return; \n    } \n\n    while (blocks-- && read16(file) != -1);  \n    file.close();\n```\n\n第一行创建名为`file`的流对象，并将其附加到通过`filename`中给出的路径指定的文件。 如果找不到文件，`good`函数将返回`false`。 此代码使用`!`运算符对该值求反，以便如果文件*不存在*，则执行`if`后面大括号中的语句。 如果文件存在并且`ifstream`对象可以打开它，则在`while`循环中一次读取 16 个字节的数据。 注意，在这段代码的末尾，在`file`对象上调用了`close`函数。 使用完资源后显式关闭资源是一种良好的做法。\n\n文件将由`read16`函数逐字节访问，包括不可打印的字节，因此像`\\r`或`\\n`这样的控制字符没有特殊意义，仍然会被读入。 但是，STREAM 类以一种特殊的方式处理`\\r`字符：这被视为行尾，通常流将静默使用该字符。 为了防止出现这种情况，我们使用`ios::binary`以二进制模式打开文件。\n\n再次查看`while`语句：\n\n```cpp\n    while (blocks-- && read16(file) != -1);\n```\n\n这里有两个表达式。 第一个表达式递减`blocks`变量，该变量保存将打印的 16 字节块的数量。 后缀递减表示表达式的值是递减之前的变量*的值，因此如果在`blocks`为零时调用该表达式，则整个表达式将短路，`while`循环结束。 如果第一个表达式非零，则调用`read16`函数，如果返回-1 值(到达文件末尾)，则循环结束。 循环的实际工作发生在`read16`函数中，因此`while`LOOP 语句是空语句。*\n\n现在，您必须在`main`函数的正上方实现`read16`函数。 此函数将使用一个常量来定义每个块的长度，因此在文件顶部附近添加以下声明：\n\n```cpp\n    using namespace std; \n const int block_length = 16;\n```\n\n就在`main`函数之前，添加以下代码：\n\n```cpp\n    int read16(ifstream& stm) \n    { \n        if (stm.eof()) return -1; \n        int flags = cout.flags(); \n        cout << hex; \n\n        string line; \n\n        // print bytes \n\n        cout.setf(flags); \n        return line.length(); \n    }\n```\n\n这只是该函数的框架代码。 稍后您将添加更多代码。\n\n此函数一次最多读取 16 个字节，并将这些字节的内容打印到控制台。 返回值是读取的字节数，如果到达文件末尾，则返回值为-1。 请注意用于将流对象传递给函数的语法。 这是一个**引用**，是一种指向实际对象的指针类型。 使用引用的原因是，如果我们不这样做，函数将获得流的*副本*。 引用将在下一章中介绍，将对象引用用作函数参数将在[章](05.html)、*使用函数*中介绍。\n\n此函数测试的第一行是验证是否已到达文件末尾，如果已到达，则不能再进行任何处理，并返回-1 的值。 代码将操作`cout`对象(例如，插入`hex`操纵器)；这样您就可以始终知道该对象在函数外部的状态，该函数可以确保当它返回`cout`对象时，它与调用函数时处于相同的状态。 通过调用`flags`函数获得`cout`对象的初始格式化状态，该状态用于在函数返回之前通过调用`setf`函数重置`cout`对象。\n\n此函数不执行任何操作，因此可以安全地编译文件并确认没有输入错误。\n\n`read16`函数执行三项操作：\n\n1.  它逐个字节地读入，最多 16 个字节。\n2.  它打印出每个字节的十六进制值。\n3.  它打印出该字节的可打印值。\n\n这意味着每行都有两个部分：左侧的十六进制部分和右侧的可打印部分。 用突出显示的代码替换函数中的注释：\n\n```cpp\n    string line; \n for (int i = 0; i < block_length; ++ i) { // read a single character from the stream unsigned char c = stm.get(); if (stm.eof()) \n            break; // need to make sure that all hex are printed   \n        // two character padded with zeros cout << setw(2) << setfill('0'); cout << static_cast<short>(c) << \" \"; if (isprint(c) == 0) line += '.'; else                 line += c; }\n```\n\n`for`循环最多循环`block_length`次。 第一个语句从流中读取单个字符。 该字节作为原始数据读入。 如果`get`发现流中没有更多的字符，它将在流对象中设置一个标志，并通过调用`eof`函数来测试这一点。 如果`eof`函数返回`true`，则表示已到达文件末尾，因此`for`循环结束，但函数不会立即返回。 原因是可能已经读取了*个*个字节，因此必须执行更多处理。\n\n循环中的其余语句做两件事：\n\n*   控制台上有打印字符十六进制值的语句\n*   在`line`变量中有一条语句以可打印的形式存储字符\n\n我们已经将`cout`对象设置为输出十六进制值，但是如果字节小于 0x10，则不会以零为前缀打印值。 要获得这种格式，我们插入`setw`操纵器，表示插入的数据将占据两个字符位置，插入`setfill`，表示使用`0`字符填充字符串。 这两个操纵器位于`<iomanip>`标题中，因此请将它们添加到文件的顶部：\n\n```cpp\n    #include <fstream> \n #include <iomanip>\n```\n\n通常，当您将`char`插入到流中时，会显示字符值，因此`char`变量会转换为`short`，这样流就会打印十六进制数值。 最后，在每个项目之间打印一个空格。\n\n`for`循环中的最后几行如下所示：\n\n```cpp\n    if (isprint(c) == 0) line += '.'; \n    else                 line += c;\n```\n\n此代码使用`isprint`宏检查字节是否为可打印字符(从“”到“~”)，如果字符可打印，则将其附加到`line`变量的末尾。 如果字节不可打印，则在`line`变量的末尾追加一个点作为占位符。\n\n到目前为止，代码将一个接一个地将字节的十六进制表示形式打印到控制台，唯一的格式是字节之间的空格。 如果您想测试代码，可以编译以下代码并在源文件上运行：\n\n```cpp\nhexdump hexdump.cpp 5\n```\n\n您将看到一些难以理解的内容，如下所示：\n\n```cpp\n    C:\\Beginning_C++ \\Chapter_03>hexdump hexdump.cpp 5 \n23 69 6e 63 6c 75 64 65 20 3c 69 6f 73 74 72 65 61 6d 3e 0d 0a \n23 69 6e 63 6c 75 64 65 20 3c 73 73 74 72 65 61 6d 3e 0d 0a 23 \n69 6e 63 6c 75 64 65 20 3c 66 73 74 72 65 61 6d 3e 0d 0a 23 69 \n6e 63 6c 75 64 65 20 3c 69 6f 6d 61 6e 69 70 3e 0d\n```\n\n`23`值是#，`20`是空格，`0d`和`0a`是回车和换行符。\n\n现在我们需要打印`line`变量中的字符表示形式，执行一些格式化，并添加换行符。 在`for`循环之后，添加以下内容：\n\n```cpp\n    string padding = \" \"; \n    if (line.length() < block_length) \n    { \n        padding += string( \n            3 * (block_length - line.length()), ' '); \n    } \n\n    cout << padding; \n    cout << line << endl;\n```\n\n十六进制显示和字符显示之间将至少有*两个*空格。 一个空格来自`for`循环中打印出的最后一个字符，第二个空格在`padding`变量的初始化中提供。\n\n每行的最大字节数应为 16 字节(`block_length`)，因此控制台上打印 16 个十六进制值。 如果读取的字节数较少，则需要额外的填充，以便在连续的行上字符表示对齐。 实际读取的字节数将是通过调用`length`函数获得的`line`变量的长度，因此丢失的字节数是表达式`block_length - line.length()`。 由于每个十六进制表示占用三个字符(两个用于数字，一个用于空格)，因此所需的填充是丢失字节数的三倍。 要创建适当数量的空格，需要使用两个参数调用字符串构造函数：副本数和要复制的字符。\n\n最后，此填充字符串被打印到控制台，后跟字节的字符表示形式。\n\n此时，您应该能够编译代码，而不会出现错误或警告。 在源文件上运行代码时，您应该看到如下所示：\n\n```cpp\n    C:\\Beginning_C++ \\Chapter_03>hexdump hexdump.cpp 5 \n23 69 6e 63 6c 75 64 65 20 3c 69 6f 73 74 72 65  #include <iostre\n61 6d 3e 0d 0a 23 69 6e 63 6c 75 64 65 20 3c 73  am>..#include <s\n73 74 72 65 61 6d 3e 0d 0a 23 69 6e 63 6c 75 64  stream>..#includ\n65 20 3c 66 73 74 72 65 61 6d 3e 0d 0a 23 69 6e  e <fstream>..#in\n63 6c 75 64 65 20 3c 69 6f 6d 61 6e 69 70 3e 0d  clude <iomanip>.\n```\n\n现在字节变得更有意义了。 由于应用不会更改其转储的文件，因此对二进制文件(包括其自身)使用此工具是安全的：\n\n```cpp\n    C:\\Beginning_C++ \\Chapter_03>hexdump hexdump.exe 17 \n4d 5a 90 00 03 00 00 00 04 00 00 00 ff ff 00 00  MZ..............\nb8 00 00 00 00 00 00 00 40 00 00 00 00 00 00 00  ........@.......\n00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................\n00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 00  ................\n0e 1f ba 0e 00 b4 09 cd 21 b8 01 4c cd 21 54 68  ........!..L.!Th\n69 73 20 70 72 6f 67 72 61 6d 20 63 61 6e 6e 6f  is program canno\n74 20 62 65 20 72 75 6e 20 69 6e 20 44 4f 53 20  t be run in DOS\n6d 6f 64 65 2e 0d 0d 0a 24 00 00 00 00 00 00 00  mode....$.......\n2b c4 3f 01 6f a5 51 52 6f a5 51 52 6f a5 51 52  +.?.o.QRo.QRo.QR\ndb 39 a0 52 62 a5 51 52 db 39 a2 52 fa a5 51 52  .9.Rb.QR.9.R..QR\ndb 39 a3 52 73 a5 51 52 b2 5a 9a 52 6a a5 51 52  .9.Rs.QR.Z.Rj.QR\n6f a5 50 52 30 a5 51 52 8a fc 52 53 79 a5 51 52  o.PR0.QR..RSy.QR\n8a fc 54 53 54 a5 51 52 8a fc 55 53 2f a5 51 52  ..TST.QR..US/.QR\n9d fc 54 53 6e a5 51 52 9d fc 53 53 6e a5 51 52  ..TSn.QR..SSn.QR\n52 69 63 68 6f a5 51 52 00 00 00 00 00 00 00 00  Richo.QR........\n00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................\n50 45 00 00 4c 01 05 00 6b e7 07 58 00 00 00 00  PE..L...k..X....\n```\n\nMZ 表示这是 Microsoft 的**Portable Executable**(**PE**)文件格式的 DOS 头部分。 实际的 PE 标头从最下面一行开始，字符为 PE。\n\n# 简略的 / 概括的 / 简易判罪的 / 简易的\n\n在本章中，您已经了解了 C++ 中的各种内置类型，如何初始化它们，以及如何使用它们。 您还学习了如何使用强制转换操作符将变量转换为不同的类型。 本章还向您介绍了记录类型，该主题将在[第 6 章](06.html)、*类*中展开。 最后，您已经看到了指针的各种示例，这一主题将在下一章中更详细地讨论。*"
  },
  {
    "path": "docs/begin-cpp-prog/04.md",
    "content": "# 四、使用内存、数组和指针\n\nC++ 允许您通过指针直接访问内存。 这为您提供了很大的灵活性，并且潜在地允许您通过消除一些不必要的数据复制来提高代码的性能。 但是，它也提供了额外的错误来源；有些错误对您的应用可能是致命的，甚至更糟(是的，比致命还糟糕！)。 因为内存缓冲区使用不当会在代码中打开安全漏洞，从而允许恶意软件接管机器。 显然，指针是 C++ 的一个重要方面。\n\n在本章中，您将看到如何声明指针并将其初始化到内存位置，如何在堆栈和 C++ 空闲存储上分配内存，以及如何使用 C++ 数组。\n\n# 在 C++ 中使用内存\n\nC++ 使用与 C 相同的语法来声明指针变量并将其分配给内存地址，并且它具有类似于 C 的指针算法。 与 C 一样，C++ 也允许您在堆栈上分配内存，因此当堆栈帧被销毁时会自动清除内存，并且会进行动态分配(在 C++ 空闲存储上)，程序员有责任释放内存。 本节将介绍这些概念。\n\n# 使用 C++ 指针语法\n\n在 C++ 中访问内存的语法很简单。 运算符的作用是：返回对象的地址。 对象*可以是变量、内置类型或自定义类型的实例，甚至可以是函数(函数指针将在下一章介绍)。 该地址被分配一个类型化指针变量或一个`void*`指针。 应该将`void*`指针仅视为内存地址的存储，因为您不能访问数据，也不能在`void*`指针上执行指针算术(即，使用算术运算符操作指针值)。 指针变量通常使用类型和`*`符号声明。 例如：*\n\n```cpp\n    int i = 42; \n    int *pi = &i;\n```\n\n在这段代码中，变量`i`是一个整数，编译器和链接器将确定将该变量分配到哪里。 通常，函数中的变量将位于堆栈帧上，如后面部分所述。 在运行时，将创建堆栈(实质上将分配一大块内存)，并在堆栈内存中为变量`i`保留空间。 然后，程序将一个值(42)放入该存储器。 接下来，将分配给变量`i`的存储器地址放置在变量`pi`中。 上述代码的内存使用情况如下图所示：\n\n![](img/89bf3c10-6e3f-4ac5-8ef7-9e801286fa70.png)\n\n指针保存的值为`0x007ef8c`(请注意，最低字节存储在内存中的最低字节中；这适用于 x86 机器)。 存储单元`0x007ef8c`具有值`0x0000002a`，即，变量`i`的值 42。 因为`pi`也是一个变量，所以它也会占用内存中的空间，在这种情况下，编译器将指针*放在内存中比它所指向的数据更低的位置*，在这种情况下，这两个变量不是连续的。\n\n对于这样在堆栈上分配的变量，您不应该假设变量在内存中的位置，也不应该假设变量相对于其他变量的位置。\n\n此代码假定为 32 位操作系统，因此指针`pi`占用 32 位并包含 32 位地址。 如果操作系统为 64 位，则指针将为 64 位宽(但整数可能仍为 32 位)。 在本书中，为了方便起见，我们将使用 32 位指针，因为 32 位地址比 64 位地址需要更少的打字时间。\n\n类型化指针用`*`符号声明，我们将其称为`int*`指针，因为该指针指向保存`int`的内存。 声明指针时，惯例是将`*`放在变量名旁边，而不是类型旁边。 此语法强调指向的*类型是`int`。 但是，如果在一条语句中声明多个变量，使用此语法非常重要：*\n\n```cpp\n    int *pi, i;\n```\n\n很明显，第一个变量是`int*`指针，第二个变量是`int`。 以下几点不太清楚：\n\n```cpp\n    int* pi, i;\n```\n\n您可能会将其解释为两个变量的类型都是`int*`和*，但事实并非如此*，因为它声明了一个指针和一个`int`。 如果要声明两个指针，则对每个变量应用`*`：\n\n```cpp\n    int *p1, *p2;\n```\n\n在单独的行上声明这两个指针可能更好。\n\n当您将`sizeof`运算符应用于指针时，您将获得指针的大小，而不是它所指向的大小。 因此，在 x86 机器上，`sizeof(int*)`将返回 4；在 x64 机器上，它将返回 8。这是一个重要的观察结果，特别是当我们在后面的部分讨论 C++ 内置数组时。\n\n要访问指针指向的数据，必须使用`*`运算符**取消引用**它：\n\n```cpp\n    int i = 42; \n    int *pi = &i; \n    int j = *pi;\n```\n\n在赋值的右侧类似这样使用，取消引用的指针提供对指针指向的值的访问，因此`j`被初始化为 42。 将其与指针声明进行比较，在指针声明中也使用了`*`符号，但含义不同。\n\n取消引用操作符不仅仅提供对内存位置上的数据的读取访问权限。 只要指针不限制它(使用*`const`关键字；请参见后面)，您也可以取消引用指针以写入内存位置：\n\n```cpp\n    int i = 42; \n    cout << i << endl; \n    int *pi { &i }; \n    *pi = 99; \n    cout << i << endl;\n```\n\n在此代码中，指针`pi`指向变量`i`在内存中的位置(在本例中，使用花括号语法)。 分配解除引用的指针会将值分配给指针所指向的位置。 结果是，在最后一行，变量`i`的值将是 99，而不是 42。\n\n# 使用空指针\n\n指针可以指向计算机中安装的内存中的任何位置，通过取消引用的指针进行赋值意味着您可能会覆盖操作系统使用的敏感内存，或者(通过直接内存访问)写入计算机上的硬件使用的内存。 但是，操作系统通常会为可执行文件提供其可以访问的特定内存范围，尝试访问超出此范围的内存将导致操作系统内存访问冲突。\n\n因此，您几乎总是应该使用`&`运算符或通过调用操作系统函数来获取指针值。 您不应该为指针提供绝对地址。 唯一的例外是无效内存地址`nullptr`的 C++ 常量：\n\n```cpp\n    int *pi = nullptr; \n    // code \n    int i = 42; \n    pi = &i; \n    // code \n    if (nullptr != pi) cout << *pi << endl;\n```\n\n此代码将指针`pi`初始化为`nullptr`。 在代码的后面，指针被初始化为整数变量的地址。 在代码的后面，将使用指针，但不是立即调用它，而是首先检查指针以确保它已被初始化为非空值。 编译器将检查您是否要使用尚未初始化的变量，但如果您正在编写库代码，编译器将不知道代码的调用者是否会正确使用指针。\n\nThe type of constant `nullptr` is not an integer, it is `std::nullptr_t`. All pointer types can be implicitly converted to this type, so `nullptr` can be used to initialize variables of all pointer types.\n\n# 内存类型\n\n一般来说，您可以将内存视为以下四种类型之一：\n\n*   静态或全局\n*   字符串池\n*   自动或堆叠\n*   免费商店\n\n当您在全局级别声明变量时，或者如果您在函数中声明变量为`static`，那么编译器将确保变量是从与应用具有相同生存期的内存中分配的--该变量在应用启动时创建，在应用结束时删除。\n\n当您使用字符串文字时，数据实际上也将是一个全局变量，但存储在可执行文件的不同部分。 对于 Windows 可执行文件，字符串文字存储在可执行文件的`.rdata`PE/COFF 部分。 文件的`.rdata`部分用于只读的初始化数据，因此您不能更改数据。 Visual C++ 允许您更进一步，并为您提供了**字符串池**选项。 请考虑以下内容：\n\n```cpp\n    char *p1 { \"hello\" }; \n    char *p2 { \"hello\" }; \n    cout << hex; \n    cout << reinterpret_cast<int>(p1) << endl; \n    cout << reinterpret_cast<int>(p2) << endl;\n```\n\n在此代码中，使用字符串文本`hello`的地址初始化了两个指针。 在以下两行中，每个指针的地址都打印在控制台上。 由于`char*`的`<<`运算符将变量视为指向字符串的指针，因此它将打印字符串而不是指针的地址。 要解决此问题，我们调用`reinterpret_cast`运算符将指针转换为整数并打印该整数的值。\n如果您使用 Visual C++ 编译器在命令行编译代码，您将看到打印出两个不同的地址。 这两个地址位于`.rdata`部分，均为只读。 如果使用`/GF`开关编译此代码以启用字符串池(这是 Visual C++ 项目的默认设置)，编译器将看到两个字符串文字相同，并且只会在`.rdata`部分存储一个副本，因此此代码的结果将是在控制台上打印两次单个地址。\n\n在这段代码中，两个变量`p1`和`p2`是自动变量，也就是说，它们是在为当前函数创建的堆栈上创建的。 调用函数时，会为函数分配一块内存，其中包含传递给函数的参数和调用函数的代码的返回地址的空间，以及函数中声明的自动变量的空间。 当函数完成时，堆栈帧将被销毁。\n\nThe **calling convention** of the function determines whether the calling function or the called function has the responsibility to do this. In Visual C++, the default is the `__cdecl` calling convention, which means the calling function cleans up the stack. The `__stdcall` calling convention is used by Windows operating system functions and the stack clean up is carried out by the called function. More details will be given in the next chapter.\n\n自动变量只有在函数和此类变量的地址仅在函数内有意义时才有效。 在本章的后面部分，您将看到如何创建数据数组。 分配为自动变量的数组在堆栈上分配为编译时确定的固定大小。 对于大型数组，您可能会超出堆栈的大小，特别是对于递归调用的函数。 在 Windows 上，默认堆栈大小为 1 MB，在 x86 Linux 上，默认堆栈大小为 2 MB。 Visual C++ 允许您使用`/F`编译器开关(或`/STACK`链接器开关)指定更大的堆栈。 GCC 编译器允许您使用`--stack`开关更改默认堆栈大小。\n\n最后一种内存类型是在**空闲存储**上创建的**动态内存**，有时也称为**堆**。 这是使用内存的最灵活方式。 顾名思义，您可以在运行时分配在运行时确定的大小的内存。 空闲存储区的实现取决于 C++ 实现，但您应该将空闲存储区视为与应用具有相同的生存期，因此从空闲存储区分配的内存应该至少与您的应用一样长。\n\n然而，这里存在潜在的危险，特别是对于长期使用的应用。 从空闲存储区分配的所有内存在您使用完后都应该返回到空闲存储区，以便空闲存储区管理器可以重用该内存。 如果没有适当地返回内存，则空闲存储管理器可能会耗尽内存，这将提示它向操作系统请求更多内存，因此，应用的内存使用量将随着时间的推移而增加，从而由于内存分页而导致性能问题。\n\n# 指针运算\n\n指针指向内存，指针的类型确定可以通过指针访问的数据的类型。 因此，`int*`指针将指向内存中的一个整数，您可以取消引用该指针(`*`)以获得该整数。 如果指针允许(未标记为`const`)，则可以通过指针算法更改其值。 例如，您可以递增或递减指针。 内存地址值的变化取决于指针的类型。 由于类型化指针指向一个类型，因此任何指针算法都将以该类型的*大小*为单位更改指针。\n\n如果递增`int*`指针，它将指向内存中下一个*个*整数，内存地址的变化取决于该整数的大小。 这相当于数组索引，例如`v[1]`这样的表达式表示您应该从`v`中第一个项目的内存位置开始，然后在内存中进一步移动一个项目，并将该项目返回到那里：\n\n```cpp\n    int v[] { 1, 2, 3, 4, 5 };\n    int *pv = v;\n    *pv = 11;\n    v[1] = 12;\n    pv[2] = 13;\n    *(pv + 3) = 14;\n```\n\n第一行在堆栈上分配一个由五个整数组成的数组，并将这些值初始化为数字 1 到 5。在本例中，因为使用了初始化列表，所以编译器将为所需的项数创建空间，因此没有给出数组的大小。 如果给出方括号之间的数组大小，则初始化列表的项不能多于数组大小。 如果列表中的项较少，则数组中的其余项将初始化为默认值(通常为零)。\n\n此代码中的下一行获取指向数组中第一项的指针。 这一行很重要：数组名称被视为指向数组中第一项的指针。 以下几行以各种方式更改数组项。 其中第一个(`*pv`)通过取消引用指针并为其赋值来更改数组中的第一项。 第二个(`v[1]`)使用数组索引为数组中的第二项赋值。 第三个(`pv[2]`)使用索引，但这次使用指针，并为数组中的第三个值赋值。 最后一个示例(`*(pv + 3)`)使用指针算法来确定数组中第四项的地址(请记住，第一项的索引为 0)，然后取消对指针的引用，为该项赋值。 之后，数组包含值`{ 11, 12, 13, 14, 5 }`，内存布局如下所示：\n\n![](img/5daa49f6-aa94-46a5-b9e8-1e3f60e607a0.png)\n\n如果您有一个包含值的内存缓冲区(在本例中，是通过数组分配的)，并且您想要将每个值乘以 3，则可以使用指针算法来执行此操作：\n\n```cpp\n    int v[] { 1, 2, 3, 4, 5 }; \n    int *pv = v; \n    for (int i = 0; i < 5; ++ i) \n    { \n        *pv++ *= 3; \n    }\n```\n\nLOOP 语句很复杂，您需要重新参考[第 2 章](02.html)和*理解语言功能*中给出的运算符优先级。 后缀增量运算符的优先级最高，其次是取消引用运算符(`*`)，最后，`*=`运算符是这三个运算符中最低的，因此运算符按以下顺序运行：`++ `、`*`、`*=`。 后缀运算符在递增之前返回值*，因此尽管指针递增指向内存中的下一项，但表达式使用递增之前的地址。 然后，该地址被取消引用，该地址由赋值运算符分配，该赋值运算符将该项替换为值乘以 3。这说明了指针和数组名称之间的一个重要区别；您可以递增指针，但不能递增数组：*\n\n```cpp\n    pv += 1; // can do this \n    v += 1; // error\n```\n\n当然，您可以对数组名称和指针使用索引(使用`[]`)。\n\n# 使用数组\n\n顾名思义，C++ 内置数组是零个或多个相同类型的数据项。 在 C++ 中，方括号用于声明数组和访问数组元素：\n\n```cpp\n    int squares[4]; \n    for (int i = 0; i < 4; ++ i)  \n    { \n        squares[i] = i * i; \n    }\n```\n\n`squares`变量是一个整数数组。 第一行为*四个*整数分配足够的内存，然后`for`循环用前四个正方形初始化内存。 编译器从堆栈分配的内存是连续的，数组中的项是连续的，因此`squares[3]`的内存位置是`squares[2]`之后的`sizeof(int)`。 由于数组是在堆栈上创建的，因此数组的大小是对编译器的指令；这不是动态分配，因此大小必须是常量。\n\n这里有一个潜在的问题：数组的大小被提到了两次，一次是在声明中，一次是在`for`循环中。 如果使用两个不同的值，则可能会初始化太少的项，或者可能会访问数组外部的内存。 RANGED`for`语法允许您访问数组中的每一项；编译器可以确定数组的大小，并将在 RANGED`for`循环中使用它。 在下面的代码中，故意犯了一个错误，显示了数组大小问题：\n\n```cpp\n    int squares[5]; \n    for (int i = 0; i < 4; ++ i)  \n    { \n        squares[i] = i * i; \n    } \n    for(int i : squares) \n    { \n        cout << i << endl; \n    }\n```\n\n数组的大小和第一个`for`循环的范围不一致，因此最后一项将不会初始化。 然而，Range`for`循环将遍历所有五个项目，因此将打印出最后一个值的一些随机值。 如果使用了相同的代码，但是声明了`squares`数组有三个项目，该怎么办呢？ 这取决于您正在使用的编译器以及您是否正在编译调试版本，但显然您将写入分配给数组的内存之外的*。*\n\n有一些方法可以缓解这些问题。 第一个问题在前面的章节中已经提到：声明一个数组大小的常量，并在代码需要知道数组大小时使用该常量：\n\n```cpp\n    constexpr int sq_size = 4; \n    int squares[sq_size]; \n    for (int i = 0; i < sq_size; ++ i) \n    { \n        squares[i] = i * i; \n    }\n```\n\n数组声明必须有一个大小常量，该常量通过使用`sq_size`常量变量进行管理。\n\n您可能还需要计算已分配数组的大小。 `sizeof`运算符应用于数组时，返回*整个*数组的大小(以字节为单位)，因此您可以通过将该值除以单个项目的大小来确定数组的大小：\n\n```cpp\n    int squares[4]; \n    for (int i = 0; i < sizeof(squares)/sizeof(squares[0]); ++ i) \n    { \n        squares[i] = i * i; \n    }\n```\n\n这是更安全的代码，但显然是冗长的。 C 运行时库包含一个名为`_countof`的宏，用于执行此计算。\n\n# 函数参数\n\n如图所示，数组会自动转换为适当的指针类型，如果将数组传递给函数或从函数返回数组，就会发生这种情况。 这种退化为哑指针意味着其他代码不能假设数组大小。 指针可以指向堆栈上分配的内存，其中内存寿命由函数确定，或者指向全局变量，其中内存寿命是程序的内存寿命，或者它可以指向动态分配的内存，并且内存由程序员确定。 指针声明中没有任何内容指示内存类型或谁负责释放内存。 在哑指针中也没有任何关于该指针指向多少内存的信息。 当您使用指针编写代码时，您必须严格控制如何使用它们。\n\n函数可以有数组参数，但这意味着比看起来要少得多：\n\n```cpp\n    // there are four tires on each car \n    bool safe_car(double tire_pressures[4]);\n```\n\n此函数将检查数组的每个成员是否都有一个介于允许的最小值和最大值之间的值。 汽车上的任何时候都有四个轮胎在使用，因此函数*应该使用四个值的数组调用*。 问题是，尽管看起来编译器*应该*检查传递给函数的数组大小是否合适，但事实并非如此。\n\n```cpp\n    double car[4] = get_car_tire_pressures(); \n    if (!safe_car(car)) cout << \"take off the road!\" << endl; \n    double truck[8] = get_truck_tire_pressures(); \n    if (!safe_car(truck)) cout << \"take off the road!\" << endl;\n```\n\n当然，开发人员应该很清楚卡车不是汽车，所以这个开发人员不应该编写这些代码，但是编译语言的通常优点是编译器会为您执行一些*健全性检查*。 在数组参数的情况下，它不会。\n\n原因是该数组是作为指针传递的，因此尽管该参数看起来是一个内置数组，但您不能使用您习惯于与 Range`for`这样的数组一起使用的工具。 事实上，如果`safe_car`函数调用`sizeof(tire_pressures)`，它将获得双指针的大小，而不是四`int`数组的字节大小 16。\n\n这种*衰减为数组参数的指针*特性意味着，函数只有在显式地告诉数组参数的大小时才会知道它的大小。 您可以使用一对空的方括号来指示应向该项传递数组，但它实际上与指针相同：\n\n```cpp\n    bool safe_car(double tire_pressures[], int size);\n```\n\n这里，该函数有一个参数指示数组的大小。 前面的函数与将第一个参数声明为指针完全相同。 以下不是函数的重载；它是*相同的*函数：\n\n```cpp\n    bool safe_car(double *tire_pressures, int size);\n```\n\n重要的一点是，当您将数组传递给函数时，数组的*第一维*被视为指针。 到目前为止，数组是一维的，但它们可能有多维。\n\n# 多维数组\n\n数组可以是多维的，要添加另一个维度，需要添加另一组方括号：\n\n```cpp\n    int two[2]; \n    int four_by_three[4][3];\n```\n\n第一个示例创建一个包含两个整数的数组，第二个示例创建一个包含 12 个整数的二维数组，这些整数排列成四行三列。 当然，*行*和*列*是任意的，它们将二维数组视为传统的电子表格，但它有助于可视化数据在内存中的排列方式。\n\n请注意，每个维度周围都有方括号。 在这方面，C++ 与其他语言不同，因此 C++ 编译器会将声明`int x[10,10]`报告为错误。\n\n初始化多维数组涉及一对大括号和用于初始化维的顺序的数据：\n\n```cpp\n    int four_by_three[4][3] { 11,12,13,21,22,23,31,32,33,41,42,43 };\n```\n\n在本例中，具有最高位数的值反映最左边的索引，具有较低位数的值反映最右边的索引(在这两种情况下，都比实际索引多一位)。 显然，您可以将其分成几行，并使用空格将值分组在一起，以使其更具可读性。 也可以使用嵌套大括号。 例如：\n\n```cpp\n    int four_by_three[4][3] = { {11,12,13}, {21,22,23}, \n                                {31,32,33}, {41,42,43} };\n```\n\n如果您阅读从左到右的维度，就可以了解更深层次的嵌套的初始化。 它有四行，因此在外支撑内有四组嵌套的支撑。 有三列，因此在嵌套大括号中有三个初始化值。\n\n嵌套大括号不仅便于格式化 C++ 代码，因为如果您提供一对空的大括号，编译器将使用默认值：\n\n```cpp\n    int four_by_three[4][3] = { {11,12,13}, {}, {31,32,33}, {41,42,43} };\n```\n\n这里，第二行项目被初始化为 0。\n\n增加尺寸时，原则适用：增加最右侧尺寸的嵌套：\n\n```cpp\n    int four_by_three_by_two[4][3][2]  \n       = { { {111,112}, {121,122}, {131,132} }, \n           { {211,212}, {221,222}, {231,232} }, \n           { {311,312}, {321,322}, {331,332} }, \n           { {411,412}, {421,422}, {431,432} }  \n         };\n```\n\n这是四行三列对(如您所见，当维度增加时，术语**行**和**列**在很大程度上是任意的)。\n\n您可以使用相同的语法访问项目：\n\n```cpp\n    cout << four_by_three_by_two[3][2][0] << endl; // prints 431\n```\n\n就内存布局而言，编译器按以下方式解释语法。 第一个索引以六个整数(3*2)为单位确定距数组开头的偏移量，第二个索引以两个整数为单位表示这六个整数*个块中的一个块*自身的偏移量，第三个索引是以单个整数为单位的偏移量。 因此，`[3][2][0]`是从开头开始的*(3*6)+(2*2)+0=22*个整数，将第一个整数视为索引 0。\n\n多维数组被视为数组的数组，因此每个“行”的类型是`int[3][2]`，并且我们从声明中知道有四个数组。\n\n# 将多维数组传递给函数\n\n您可以将多维数组传递给函数：\n\n```cpp\n    // pass the torque of the wheel nuts of all wheels \n    bool safe_torques(double nut_torques[4][5]);\n```\n\n这段代码编译后，您可以以 4x5 数组的形式访问参数，假设这辆车有四个轮子，每个轮子上有五个螺母。\n\n如前所述，当您传递数组时，第一维将被视为指针，因此，虽然您可以将 4x5 数组传递给此函数，但您也可以传递 2x5 数组，编译器不会出错。 但是，如果传递 4x3 数组(即第二维与函数中声明的不同)，编译器将发出数组不兼容的错误。 参数可以更准确地描述为`double row[][5]`。 由于第一个维度的大小不可用，因此应该使用该维度的大小声明函数：\n\n```cpp\n    bool safe_torques(double nut_torques[][5], int num_wheels);\n```\n\n这说明`nut_torques`是一个或多个“行”，每行有五个项目。 由于该数组不提供有关其行数的信息，因此您应该提供该信息。 声明这一点的另一种方式是：\n\n```cpp\n    bool safe_torques(double (*nut_torques)[5], int num_wheels);\n```\n\n方括号在这里很重要，如果省略它们而使用`double *nut_torques[5]`，则意味着`*`将引用数组中的类型，也就是说，编译器将把`nut_torques`视为由`double*`指针组成的五元数组。 我们以前见过这样一个数组的示例：\n\n```cpp\n    void main(int argc, char *argv[]);\n```\n\n`argv`参数是一个由`char*`指针组成的数组。 您还可以将`argv`参数声明为`char**`，其含义相同。\n\n通常，如果要将数组传递给函数，最好使用自定义类型或使用 C++ 数组类型。\n\n将 Range`for`与多维数组一起使用比乍一看要复杂一些，需要使用引用，如本章后面的小节所述。\n\n# 使用字符数组\n\n字符串将在[章](09.html)，*中使用 Strings*进行更详细的介绍，但这里值得指出的是，C 字符串是字符数组，可以通过指针变量进行访问。 这意味着，如果要操作字符串，则必须操作指针所指向的内存，而不是操作指针本身。\n\n# 比较字符串\n\n以下代码分配两个字符串缓冲区，并调用`strcpy_s`函数以使用相同的字符串对每个缓冲区进行初始化：\n\n```cpp\n    char p1[6]; \n    strcpy_s(p1, 6, \"hello\"); \n    char p2[6]; \n    strcpy_s(p2, 6, p1); \n    bool b = (p1 == p2);\n```\n\n`strcpy_c`函数将字符从最后一个参数中给定的指针(直到终止的`NUL`)复制到第一个参数中给定的缓冲区中，其最大大小在第二个参数中给出。 这两个指针在最后一行进行比较，这将返回值`false`。 问题在于，比较函数比较的是指针的值，而不是指针指向的值。 这两个缓冲区具有相同的字符串，但指针不同，因此`b`将为`false`。\n\n比较字符串的正确方法是逐个字符比较数据，看它们是否相等。 C 运行时提供了`strcmp`来逐个字符比较两个字符串缓冲区，`std::string`类定义了一个名为`compare`的函数，该函数也将执行这样的比较；但是，要小心从这些函数返回的值：\n\n```cpp\n    string s1(\"string\"); \n    string s2(\"string\"); \n    int result = s1.compare(s2);\n```\n\n返回值不是指示两个字符串是否相同的`bool`类型；而是`int`类型。 这些比较函数执行字典序比较，如果此代码中的参数(`s2`)按字典顺序大于操作数(`s1`)，则返回负值，如果操作数大于参数，则返回正数。 如果两个字符串相同，则函数返回 0。 请记住，对于值 0，`bool`是`false`，对于非零值，`true`是`true`。 标准库为`std::string`的`==`运算符提供了重载，因此可以安全地编写如下代码：\n\n```cpp\n    if (s1 == s2) \n    { \n        cout << \"strings are the same\" << endl; \n    }\n```\n\n运算符将比较这两个变量中包含的字符串。\n\n# 防止缓冲区溢出\n\n用于操作字符串的 C 运行时库因允许缓冲区溢出而臭名昭著。 例如，`strcpy`函数将一个字符串复制到另一个字符串，您可以通过`<cstring>`标头(包含在`<iostream>`标头中)访问它。 你可能会忍不住写下这样的话：\n\n```cpp\n    char pHello[5];          // enough space for 5 characters \n    strcpy(pHello, \"hello\");\n```\n\n问题是，`strcpy`将复制所有字符，直到(包括终止的`NULL`字符)，因此您将把 6 个字符复制到一个仅有*5*空间的数组中。 您可能会从用户输入(例如，从网页上的文本框中)获取一个字符串，并认为您分配的数组足够大，但恶意用户可能会提供一个故意大于缓冲区的过长字符串，从而覆盖程序的其他部分。 这样的*缓冲区溢出*导致许多程序受到黑客控制服务器的攻击，以至于 C 字符串函数都被更安全的版本所取代。 实际上，如果您想输入前面的代码，您会发现`strcpy`是可用的，但是 Visual C++ 编译器会发出一个错误：\n\n```cpp\nerror C4996: 'strcpy': This function or variable may be unsafe. \nConsider using strcpy_s instead. To disable deprecation, use _CRT_SECURE_NO_WARNINGS. See online help for details.\n```\n\n如果您有使用`strcpy`的现有代码，并且需要编译该代码，则可以在`<cstring>`之前定义符号：\n\n```cpp\n    #define _CRT_SECURE_NO_WARNINGS \n    #include <iostream>\n```\n\n防止此问题的初始尝试是调用`strncpy`，它将复制特定数量的字符：\n\n```cpp\n    char pHello[5];             // enough space for 5 characters \n    strncpy(pHello, \"hello\", 5);\n```\n\n该功能将复制最多五个字符，然后停止。 问题是要复制的字符串有五个字符，因此结果是没有`NULL`终止。 此函数的安全版本有一个参数，您可以使用该参数来说明目标缓冲区有多大：\n\n```cpp\n    size_t size = sizeof(pHello)/sizeof(pHello[0]); \n    strncpy_s(pHello, size, \"hello\", 5);\n```\n\n在运行时，这仍然会导致问题。 您已经告诉函数缓冲区的大小是 5 个字符，它将确定缓冲区大小不足以容纳您要求它复制的 6 个字符。 更安全的字符串函数将调用名为**约束处理程序**的函数，而不是允许程序静默继续和缓冲区溢出导致问题，默认版本将关闭程序，理由是缓冲区溢出意味着程序受到危害。\n\nC 运行时库字符串函数最初是为了返回函数结果而编写的，现在更安全的版本返回错误值。 还可以告诉`strncpy_s`函数截断副本，而不是调用约束处理程序：\n\n```cpp\n    strncpy_s(pHello, size, \"hello\", _TRUNCATE);\n```\n\nC++ `string`类可以保护您免受此类问题的影响。\n\n# 在 C++ 中使用指针\n\n指针在 C++ 中显然非常重要，但与任何强大的功能一样，也存在问题和危险，因此值得指出一些主要问题。 指针指向内存中的单个位置，指针的类型指示应该如何解释该内存位置。 您最多可以假定的是，内存中该位置的字节数就是指针类型的大小。 就这样。 这意味着指针本质上是不安全的。 然而，在 C++ 中，它们是使进程中的代码能够访问大量数据的最快方式。\n\n# 越界访问\n\n当您分配一个缓冲区(无论是在堆栈上还是在空闲存储上)并获得一个指针时，几乎没有什么可以阻止您访问未分配的内存--无论是在缓冲区位置之前还是之后。 这意味着当您在数组上使用指针算法或索引访问时，要仔细检查是否要访问超出界限的数据。 有时错误可能不会立即显现：\n\n```cpp\n    int arr[] { 1, 2, 3, 4 }; \n    for (int i = 0; i < 4; ++ i)  \n    { \n        arr[i] += arr[i + 1]; // oops, what happens when i == 3? \n    }\n```\n\n在使用索引时，您必须不断提醒自己，数组是从零开始编制索引的，因此最高索引是数组大小减去 1。\n\n# 指向已释放内存的指针\n\n这适用于堆栈上分配的内存和动态分配的内存。 以下是一个编写不佳的函数，它返回函数中堆栈上分配的字符串：\n\n```cpp\n    char *get() \n    { \n        char c[] { \"hello\" };\n        return c;\n    }\n```\n\n前面的代码分配了一个 6 个字符的缓冲区，然后用字符串文字`hello`的 5 个字符和终止字符`NULL`对其进行初始化。 问题是，一旦函数完成，堆栈帧就会被拆除，以便可以重用内存，并且指针将指向可能被其他对象使用的内存。 此错误是由糟糕的编程引起的，但它可能不像本例中那样明显。 如果函数使用多个指针并执行指针赋值，您可能不会立即注意到返回了指向堆栈分配的对象的指针。 最好的做法就是不从函数返回原始指针，但是如果您确实想要使用这种风格的编程，请确保内存缓冲区是通过参数传入的(因此函数不拥有缓冲区)，或者是动态分配的，并且您正在将所有权传递给调用者。\n\n这就引出了另一个问题。 如果您在指针上调用`delete`，然后在代码中稍后尝试访问该指针，则您将访问可能正在被其他变量使用的内存。 要缓解此问题，您可以养成在删除指针时将指针分配给`null_ptr`的习惯，并在使用指针之前检查是否有`null_ptr`。 或者，您也可以使用智能指针对象，它将为您完成此操作。 智能指针将在[第](06.html)章*类*中介绍。\n\n# 转换指针\n\n您可以输入指针，也可以使用`void*`指针。 类型化指针将访问内存，就像它是指定的类型一样(当您对类进行继承时，这会产生有趣的结果，但这将留给[第 6 章](06.html)、*类*和[第 7 章](07.html)、*面向对象编程简介*)。 因此，如果您强制转换指向另一个类型的指针并取消对其的引用，则内存将被视为包含该强制转换类型。 这样做很少有意义。 无法取消引用`void*`指针，因此永远不能通过`void*`指针访问数据，要访问必须强制转换指针的数据。 `void*`指针类型的全部原因是它可以指向任何东西。 通常，只有当类型与该函数无关时才应使用`void*`指针。 例如，C`malloc`函数返回一个`void*`指针，因为该函数只分配内存；它并不关心内存将用于什么。\n\n# 常量指针\n\n指针可以声明为`const`，这取决于应用指针的位置，这意味着指针指向的内存通过指针是只读的，或者指针的值是只读的：\n\n```cpp\n    char c[] { \"hello\" }; // c can be used as a pointer \n    *c = 'H';             // OK, can write thru the pointer \n    const char *ptc {c};  // pointer to constant \n    cout << ptc << endl;  // OK, can read the memory pointed to \n    *ptc =  'Y';          // cannot write to the memory \n    char *const cp {c};   // constant pointer \n    *cp = 'y';            // can write thru the pointer \n    cp++ ;                 // cannot point to anything else\n```\n\n这里，`ptc`是指向常量`char`的指针，也就是说，虽然您可以更改`ptc`指向的内容，也可以读取它指向的内容，但不能使用它来更改内存。 另一方面，`cp`是一个常量指针，这意味着您可以读写指针所指向的内存，但不能更改它所指向的位置。 通常会传递指向函数的前`const char*`个指针，因为函数不知道字符串已分配到哪里或缓冲区的大小(调用者可能会传递一个无法更改的文字)。 请注意，没有`const*`运算符，因此`char const*`被视为指向常量缓冲区的指针`const char*`。\n\n可以使用强制转换将指针设置为常量、更改或移除。 为了证明这一点，下面对关键字`const`做了一些相当无意义的更改：\n\n```cpp\n    char c[] { \"hello\" }; \n    char *const cp1 { c }; // cannot point to any other memory \n    *cp1 = 'H';            // can change the memory \n    const char *ptc = const_cast<const char*>(cp1); \n    ptc++ ;                 // change where the pointer points to \n    char *const cp2 = const_cast<char *const>(ptc); \n    *cp2 = 'a';            // now points to Hallo\n```\n\n指针`cp1`和`cp2`可用于更改它们所指向的内存，但一旦分配，这两个指针都不能指向其他内存。 第一个`const_cast`去掉了指向一个指针的`const`-ness，该指针可以更改为指向其他内存，但不能用来更改该内存，`ptc`。 第二个`const_cast`去掉了`ptc`的`const`性质，以便可以通过指针`cp2`改变内存。\n\n# 更改指向的类型\n\n`static_cast`运算符用于转换时进行编译时检查，而不是运行时检查，因此这意味着指针必须是相关的。 `void*`指针可以转换为任何指针，因此下面的编译是有意义的：\n\n```cpp\n    int *pi = static_cast<int*>(malloc(sizeof(int))); \n    *pi = 42; \n    cout << *pi << endl; \n    free(pi);\n```\n\nC`malloc`函数返回一个`void*`指针，因此您必须转换它才能使用内存。 (当然，C++ `new`操作符消除了这种强制转换的需要。)。 内置类型的相关性不足以让`static_cast`在指针类型之间进行转换，因此不能使用`static_cast`将`int*`指针转换为`char*`指针，即使`int`和`char`都是整数类型。 对于通过继承关联的自定义类型，可以使用`static_cast`强制转换指针，但没有运行时检查强制转换是否正确。 要使用运行时检查进行强制转换，您应该使用`dynamic_cast`，更多细节将在[第 6 章](06.html)、*类*和[第 7 章](07.html)、*面向对象编程简介*中给出。\n\n`reinterpret_cast`操作符是强制转换操作符中最灵活、最危险的，因为它无需任何类型检查就可以在任何指针类型之间进行转换。 它本质上是不安全的。 例如，下面的代码使用文字初始化宽字符数组。 数组`wc`将有六个字符，`hello`后跟`NULL`。 `wcout`对象将`wchar_t*`指针解释为指向`wchar_t`字符串中第一个字符的指针，因此插入`wc`将打印该字符串(直到`NUL`的每个字符)。 要获取实际内存位置，必须将指针转换为整数：\n\n```cpp\n    wchar_t wc[] { L\"hello\" }; \n    wcout << wc << \" is stored in memory at \"; \n    wcout << hex; \n    wcout << reinterpret_cast<int>(wc) << endl;\n```\n\n同样，如果您将`wchar_t`插入到`wcout`对象中，它将打印字符，而不是数值。 因此，要打印出单个字符的代码，我们需要将指针转换为合适的整数指针。 此代码假设 a`short`与 a`wchar_t`大小相同：\n\n```cpp\n    wcout << \"The characters are:\" << endl; \n    short* ps = reinterpret_cast<short*>(wc); \n    do  \n    {  \n        wcout << *ps << endl;  \n    } while (*ps++);\n```\n\n# 在代码中分配内存\n\nC++ 定义了两个运算符`new`和`delete`，这两个运算符从空闲存储区分配内存，并将内存释放回空闲存储区。\n\n# 分配单个对象\n\n`new`运算符与分配内存的类型一起使用，它将返回指向该内存的类型化指针：\n\n```cpp\n    int *p = new int; // allocate memory for one int\n```\n\n`new`运算符将为它创建的每个对象调用自定义类型的*默认构造函数*(如[第 6 章](06.html)，*类*中所述)。 内置类型没有构造函数，因此将进行类型初始化，这通常会将对象初始化为零(在本例中为零整数)。\n\n通常，在未显式初始化内存的情况下，不应使用为内置类型分配的内存。 事实上，在 Visual C++ 中，`new`运算符的调试版本会将内存初始化为每个字节的值`0xcd`，以在调试器中直观地提醒您尚未初始化内存。 对于自定义类型，初始化分配的内存留给类型的作者。\n\n重要的是，当您用完内存后，应将其归还给空闲存储，以便分配器可以重用它。 为此，您可以调用`delete`运算符：\n\n```cpp\n    delete p;\n```\n\n删除指针时，将调用该对象的**析构函数**。 对于内置类型，此操作不起任何作用。 在删除指针之后初始化指向`nullptr`的指针是很好的做法，如果您使用在使用指针之前检查指针的值的约定，这将保护您不会使用已删除的指针。 C++ 标准规定，如果删除一个值为`nullptr`的指针，`delete`运算符将不起作用。\n\nC++ 允许您在调用`new`运算符时通过两种方式初始化值：\n\n```cpp\n    int *p1 = new int (42); \n    int *p2 = new int {42};\n```\n\n对于自定义类型，`new`运算符将调用该类型的构造函数；对于内置类型，最终结果是相同的，并通过将项初始化为提供的值来执行。 您还可以使用初始化的列表语法，如前面代码中的第二行所示。 重要的是要注意，初始化是指向的内存，而不是指针变量。\n\n# 分配对象数组\n\n您还可以使用`new`运算符在动态内存中创建对象数组。 您可以通过在一对方括号中提供要创建的项目数来实现这一点。 下面的代码为两个整数分配内存：\n\n```cpp\n    int *p = new int[2]; \n    p[0] = 1; \n    *(p + 1) = 2; \n    for (int i = 0; i < 2; ++ i) cout << p[i] << endl; \n    delete [] p;\n```\n\n运算符返回指向分配的类型的指针，您可以使用指针算术或数组索引来访问内存。 不能在`new`语句中初始化内存；必须在创建缓冲区后执行此操作。 使用`new`为多个对象创建缓冲区时，必须使用适当版本的`delete`运算符：`[]`用于指示删除了多个项目，并将调用每个对象的析构函数。 务必始终使用与用于创建指针的`new`版本相对应的正确版本的`delete`。\n\n自定义类型可以为单个对象定义自己的运算符`new`和运算符`delete`，也可以为对象数组定义运算符`new[]`和运算符`delete[]`。 自定义类型作者可以使用它们为其对象使用自定义内存分配方案。\n\n# 处理失败的分配\n\n如果`new`运算符无法为对象分配内存，它将抛出`std::bad_alloc`异常，返回的指针将为`nullptr`。 异常在[第 10 章](10.html)，*诊断和调试*中有介绍，因此这里只简要概述语法。 在生产代码中检查内存分配失败非常重要。 下面的代码显示如何保护分配，以便您可以捕获`std::bad_alloc`异常并处理它：\n\n```cpp\n    // VERY_BIG_NUMER is a constant defined elsewhere \n    int *pi; \n    try \n    { \n        pi = new int[VERY_BIG_NUMBER]; \n        // other code \n    } \n    catch(const std::bad_alloc& e)  \n    {  \n        cout << \"cannot allocate\" << endl;  \n        return; \n    } \n    // use pointer \n    delete [] pi;\n```\n\n如果`try`块中的任何代码抛出异常控制，它将被传递给`catch`子句，忽略任何其他尚未执行的代码。 `catch`子句检查异常对象的类型，如果它是正确的类型(在本例中是分配错误)，它会创建对该对象的引用，并将控制权传递给`catch`块，异常引用的范围就是这个块。 在本例中，代码只是打印一个错误，但您可以使用它来执行操作，以确保内存分配失败不会影响后续代码。\n\n# 使用新运算符的其他版本\n\n此外，自定义类型可以定义放置运算符`new`，这允许您向自定义`new`函数提供一个或多个参数。 放置`new`的语法是通过括号提供放置字段。\n\nC++ 标准库版的`new`运算符提供了一个可以接受常量`std::nothrow`作为放置字段的版本。 如果分配失败，本版本不会抛出异常，只能根据返回指针的值判断失败：\n\n```cpp\n    int *pi = new (std::nothrow) int [VERY_BIG_NUMBER]; \n    if (nullptr == pi)  \n    { \n        cout << \"cannot allocate\" << endl; \n    } \n    else \n    { \n        // use pointer \n        delete [] pi; \n    }\n```\n\n类型前的圆括号用于传递放置字段。 如果在类型后面使用圆括号，则在分配成功时，这些圆括号将给出一个值来初始化对象。\n\n# 内存寿命\n\n由`new`分配的内存将保持有效，直到您调用`delete`。 这意味着您可能拥有生命周期较长的内存，并且代码可能会在代码中传递各种函数。 请考虑以下代码：\n\n```cpp\n    int *p1 = new int(42); \n    int *p2 = do_something(p1); \n    delete p1; \n    p1 = nullptr; \n    // what about p2?\n```\n\n这段代码创建一个指针并初始化它所指向的内存，然后将该指针传递给函数，该函数本身返回一个指针。 由于不再需要`p1`指针，因此将其删除并分配给`nullptr`，以便不能再次使用。 这段代码看起来不错，但问题是如何处理函数返回的指针？ 假设该函数简单地操作指针指向的数据：\n\n```cpp\n    int *do_something(int *p) \n    { \n        *p *= 10; \n        return p; \n    }\n```\n\n实际上，调用`do_something`会创建指针的副本，但不会创建指针指向的对象的副本。 这意味着当删除`p1`指针时，它所指向的内存不再可用，因此指针`p2`指向无效内存。\n\n这个问题可以使用一种称为**资源获取是初始化**(**RAII**)的机制来解决，这意味着使用 C++ 对象的特性来管理资源。 C++ 中的 RAII 需要类，特别是复制构造函数和析构函数。 智能指针类可用于管理指针，以便在复制指针时，也会复制指针所指向的内存。 析构函数是当对象超出作用域时自动调用的函数，因此智能指针可以使用它来释放内存。 智能指针和析构函数将在[第 6 章](06.html)、*类*中介绍。\n\n# Windows SDK 和指针\n\n从函数返回指针有其固有的危险：内存的责任被传递给调用者，调用者必须确保适当地释放内存，否则这可能会导致内存泄漏并相应地降低性能。 在本节中，我们将了解 Windows 的**软件开发工具包**(**SDK**)提供内存缓冲区访问的一些方式，并学习 C++ 中使用的一些技术。\n\n首先，值得指出的是，Windows SDK 中任何返回字符串或具有字符串参数的函数都有两个版本。 以`A`为后缀的版本表示函数使用 ANSI 字符串，而`W`版本将使用宽字符串。 出于本讨论的目的，更容易使用 ANSI 函数。\n\n`GetCommandLineA`函数具有以下原型(考虑到 Windows SDK`typedef`)：\n\n```cpp\n    char * __stdcall GetCommandLine();\n```\n\n所有 Windows 函数都定义为使用`__stdcall`调用约定。 通常，您会看到`WINAPI`的`typedef`用于`__stdcall`调用约定。\n\n该函数可以按如下方式调用：\n\n```cpp\n    //#include <windows.h>\n    cout << GetCommandLineA() << endl;\n```\n\n请注意，我们没有采取任何措施来释放返回的缓冲区。 原因是指针指向在您的进程的生命周期中存在的内存，所以您*不应该*释放它。 事实上，如果你要发布它，你会怎么做？ 您不能保证函数是使用与您正在使用的相同编译器或库编写的，因此不能使用 C++ `delete`运算符或 C`free`函数。\n\n当函数返回缓冲区时，一定要查阅文档，了解是谁分配了缓冲区，以及谁应该释放它。\n\n另一个例子是`GetEnvironmentStringsA`：\n\n```cpp\n    char * __stdcall GetEnvironmentStrings();\n```\n\n这也会返回一个指向缓冲区的指针，但这一次文档很清楚，在使用缓冲区之后，您应该释放它。 SDK 提供了一个名为`FreeEnvironmentStrings`的函数来执行此操作。 对于形式为`name=value`的每个环境变量，缓冲区包含一个字符串，每个字符串以`NUL`字符结束。 缓冲区中的最后一个字符串只是一个`NUL`字符，也就是说，缓冲区末尾有两个`NUL`字符。 这些函数可以按如下方式使用：\n\n```cpp\n    char *pBuf = GetEnvironmentStringsA(); \n    if (nullptr != pBuf) \n    { \n        char *pVar = pBuf; \n        while (*pVar) \n        { \n            cout << pVar << endl; \n            pVar += strlen(pVar) + 1; \n        } \n\n        FreeEnvironmentStringsA(pBuf); \n    }\n```\n\n`strlen`函数是 C 运行时库的一部分，它返回字符串的长度。 您不需要知道`GetEnvironmentStrings`函数如何分配缓冲区，因为`FreeEnvironmentStrings`将调用正确的解除分配代码。\n\n在某些情况下，开发人员有责任分配缓冲区。 Windows SDK 提供了一个名为`GetEnvironmentVariable`的函数来返回命名环境变量的值。 当您调用此函数时，您不知道是否设置了环境变量，或者是否设置了环境变量，也不知道它的值有多大，因此这意味着您很可能需要分配一些内存。 该函数的原型是：\n\n```cpp\n    unsigned long __stdcall GetEnvironmentVariableA(const char *lpName,   \n        char *lpBuffer, unsigned long nSize);\n```\n\n有两个参数是指向 C 字符串的指针。 这里有一个问题，`char*`指针可能正在将中的*字符串传递给函数，或者它可能被用来传入缓冲区，以便将字符串*传出*。 你怎么知道`char*`指针的用途呢？*\n\n您将看到完整参数声明的提示。 `lpName`指针标记为`const`，因此函数不会更改它所指向的字符串；这意味着它是中的*参数。 此参数用于传入要获取的环境变量的名称。 另一个参数只是一个`char*`指针，因此它可以用来将*中的字符串*传递给函数或*传递给*，或者实际上，同时传递*中的*和*中的*。 了解如何使用此参数的唯一方法是阅读文档。 在本例中，它是一个*out*参数；如果变量存在，函数将在`lpBuffer`中返回环境变量的值，或者如果变量不存在，函数将保持缓冲区不变并返回值 0。 您有责任以您认为合适的方式分配此缓冲区，并在最后一个参数`nSize`中传递此缓冲区的大小。*\n\n函数的返回值有两个目的。 它用于指示发生了错误(只有一个值 0，这意味着您必须调用`GetLastError`函数来获取错误)，它还用于提供有关缓冲区`lpBuffer`的信息。 如果函数成功，则返回值是复制到缓冲区的字符数，不包括`NULL`终止字符。 但是，如果函数确定缓冲区太小(它从`nSize`参数知道缓冲区的大小)无法容纳环境变量值，则不会进行复制，并且函数将返回所需的缓冲区大小，即环境变量(包括`NULL`终止符)中的字符数。\n\n调用此函数的常见方法是调用两次，第一次使用零大小的缓冲区，然后在再次调用之前使用返回值分配缓冲区：\n\n```cpp\n    unsigned long size = GetEnvironmentVariableA(\"PATH\", nullptr, 0); \n    if (0 == size)  \n    { \n        cout << \"variable does not exist \" << endl; \n    } \n    else \n    { \n        char *val = new char[size]; \n        if (GetEnvironmentVariableA(\"PATH\", val, size) != 0) \n        { \n            cout << \"PATH = \";\n            cout << val << endl; \n        } \n        delete [] val; \n    }\n```\n\n通常，与所有库一样，您必须阅读文档以确定如何使用参数。 Windows 文档将告诉您指针参数是 In、Out 还是 In/Out。 它还会告诉您谁拥有内存，以及您是否负责分配和/或释放内存。\n\n无论何时看到函数的指针参数，都要特别注意检查文档，了解指针的用途以及内存是如何管理的。\n\n# 内存和 C++ 标准库\n\nC++ 标准库提供了各种类，允许您操作对象集合。 这些类称为**标准模板库**(**STL**)，提供了将项插入到集合对象中的标准方法，以及访问项和迭代整个集合的方法(称为迭代器)。 STL 定义了实现为队列、堆栈或具有随机访问的向量的集合类。 这些类将使用标准库容器深入讨论[第 8 章](08.html)，*，因此在本节中，我们将只讨论两个行为类似于 C++ 内置数组的类。*\n\n# 标准库阵列\n\nC+Standard Library 提供了两个容器，可以通过索引器随机访问数据。 这两个容器还允许您访问底层内存，并且由于它们保证在内存中按顺序和连续地存储项，因此当您需要提供指向缓冲区的指针时，可以使用它们。 这两种类型都是模板，这意味着您可以使用它们来保存内置类型和自定义类型。 这两个集合类是`array`和`vector`。\n\n# 使用基于堆栈的数组类\n\n`array`类在`<array>`头文件中定义。 该类允许您在堆栈上创建固定大小的数组，并且与内置数组一样，它们不能在运行时收缩或扩展。 因为它们是在堆栈上分配的，所以它们不需要在运行时调用内存分配器，但很明显，它们应该小于堆栈帧大小。 这意味着`array`对于较小的项目数组是一个很好的选择。 编译时必须知道`array`的大小，并将其作为模板参数传递：\n\n```cpp\n    array<int, 4> arr { 1, 2, 3, 4 };\n```\n\n在此代码中，尖括号(`<>`)中的第一个模板参数是数组中每个项的类型，第二个参数是项数。 此代码使用初始化列表初始化数组，但请注意，您仍然需要在模板中提供数组的大小。 此对象将使用 Range`for`作为内置数组(或者实际上是任何标准库容器)工作：\n\n```cpp\n    for (int i : arr) cout << i << endl;\n```\n\n原因是`array`实现了此语法所需的`begin`和`end`函数。 您还可以使用索引来访问项目：\n\n```cpp\n    for (int i = 0; i < arr.size(); ++ i) cout << arr[i] << endl;\n```\n\n`size`函数将返回数组的大小，方括号索引器提供对数组成员的随机访问。 您可以访问数组边界之外的内存，因此对于前面定义的具有四个成员的数组，您可以访问`arr[10]`。 这可能会导致运行时出现意外行为，甚至会出现某种内存故障。 为了防止这种情况，该类提供了一个函数`at`，它将执行范围检查，如果索引超出范围，该类将抛出 C++ 异常`out_of_range`。\n\n使用`array`对象的主要优点是，您可以在编译时进行检查，以查看是否无意中将对象作为哑指针传递给函数。 请考虑以下函数：\n\n```cpp\n    void use_ten_ints(int*);\n```\n\n在运行时，该函数不知道传递给它的缓冲区的大小，在这种情况下，文档说明您必须传递一个带有 10`int`类型变量的缓冲区，但正如我们已经看到的，C++ 允许将内置数组用作指针：\n\n```cpp\n    int arr1[] { 1, 2, 3, 4 }; \n    use_ten_ints(arr1); // oops will read past the end of the buffer\n```\n\n没有编译器检查，也没有任何运行时检查来捕获此错误。 `array`类不允许发生这样的错误，因为没有自动转换为哑指针：\n\n```cpp\n    array<int, 4> arr2 { 1, 2, 3, 4 };  \n    use_ten_ints(arr2); // will not compile\n```\n\n如果您真的坚持要获取哑指针，您可以这样做，并保证可以将数据作为一个连续的内存块进行访问，在该内存块中，项是按顺序存储的：\n\n```cpp\n    use_ten_ints(&arr2[0]);    // compiles, but on your head be it \n    use_ten_ints(arr2.data()); // ditto\n```\n\n该类不仅仅是内置数组的包装器，它还提供了一些附加功能。 例如：\n\n```cpp\n    array<int, 4> arr3; \n    arr3.fill(42);   // put 42 in each item \n    arr2.swap(arr3); // swap items in arr2 with items in arr3\n```\n\n# 使用动态分配的向量类\n\n标准库还在`<vector>`头中提供了`vector`类。 同样，该类是一个模板，因此您可以将其与内置和自定义类型一起使用。 但是，与`array`不同的是，内存是动态分配的，这意味着可以在运行时扩展或缩小`vector`。 这些项是连续存储的，因此您可以通过调用`data`函数或访问第一个项的地址来访问底层缓冲区(为了支持调整集合的大小，缓冲区可能会改变，因此此类指针只能临时使用)。 当然，与`array`一样，不会自动转换为哑指针。 `vector`类使用方括号语法提供索引随机访问，并使用`at`函数进行范围检查。 该类还实现了允许容器与标准库函数和 Range`for`一起使用的方法。\n\n`vector`类比`array`类更灵活，因为您可以插入项目和移动项目，但这确实会带来一些开销。 因为类的实例在运行时动态分配内存，所以使用分配器的成本很高，并且在初始化和销毁(当`vector`对象超出范围时)有一些额外的开销。 `vector`类的对象也比它保存的数据占用更多的内存。 因此，它不适用于数量较少的项目(当`array`是更好的选择时)。\n\n# 参考文献\n\n引用是对象的别名。 也就是说，它是对象的另一个名称，因此通过引用访问对象与通过对象的变量名访问对象是相同的。 引用是使用引用名称上的`&`符号声明的，其初始化和访问方式与变量完全相同：\n\n```cpp\n    int i = 42; \n    int *pi = &i;  // pointer to an integer \n    int& ri1 = i;  // reference to a variable \n    i = 99;        // change the integer thru the variable \n    *pi = 101;     // change the integer thru the pointer \n    ri1 = -1;      // change the integer thru the reference \n    int& ri2 {i};  // another reference to the variable \n    int j = 1000; \n    pi = &j;       // point to another integer\n```\n\n在此代码中，声明并初始化一个变量，然后初始化指向该数据的指针，并将引用初始化为变量的别名。 引用`ri1`使用赋值运算符进行初始化，而引用`ri2`使用初始化器列表语法进行初始化。\n\nThe pointer and reference have two different meanings. The reference is not initialized to the value of the variable, the variable's data; it is an alias for the variable name.\n\n无论在哪里使用变量，都可以使用引用；无论您对引用做什么，实际上都等同于对变量执行相同的操作。 指针指向数据，因此您可以通过取消引用指针来更改数据，但同样，您可以使指针指向任何数据，并通过取消引用指针来更改该数据(如前面代码的最后两行所示)。 一个变量可以有多个别名，每个别名都必须在声明时初始化为变量。 一旦声明，就不能使引用引用不同的对象。\n\n以下代码将无法编译：\n\n```cpp\n    int& r1;           // error, must refer to a variable \n    int& r2 = nullptr; // error, must refer to a variable\n```\n\n由于引用是另一个变量的别名，因此在未初始化为变量的情况下它无法存在。 同样，您不能将其初始化为变量名以外的任何值，因此不存在*空引用*的概念。\n\n一旦初始化，引用永远只是一个变量的别名。 实际上，当您使用引用作为对任何运算符的操作数时，将在变量上执行该操作：\n\n```cpp\n    int x = 1, y = 2;  \n    int& rx = x; // declaration, means rx is an alias for x \n    rx = y;      // assignment, changes value of x to the value of y\n```\n\n在这段代码中，`rx`是变量`x`的别名，因此最后一行中的赋值只是将值`y`赋给`x`：赋值是在带别名的变量上执行的。 此外，如果您获取引用的地址，则会返回它引用的变量的地址。 虽然您可以拥有对数组的引用，但不能拥有引用的数组。\n\n# 常量引用\n\n到目前为止使用的引用允许您更改作为其别名的变量，因此它具有左值语义。 还有`const`个左值引用，即对可以读取但不能写入的对象的引用。\n\n与`const`指针一样，您可以在左值引用上使用`const`关键字声明`const`引用。 这实际上使引用成为只读的：您可以访问变量的数据来读取它，但不能更改它。\n\n```cpp\n    int i = 42; \n    const int& ri = i; \n    ri = 99;           // error!\n```\n\n# 返回引用\n\n有时对象会被传递给函数，而函数的语义是应该返回该对象。 这方面的一个例子是与流对象一起使用的`<<`运算符。 对此运算符的调用是*链接的*：\n\n```cpp\n    cout << \"The value is \" << 42;\n```\n\n这实际上是对名为`operator<<`的函数的一系列调用，其中一个采用`const char*`指针，另一个采用`int`参数。 这些函数还具有将使用的流对象的`ostream`参数。 但是，如果这只是一个`ostream`参数，则意味着将复制该参数，并在该副本上执行插入。 流对象通常使用缓冲，因此对流对象副本的更改可能不会产生预期的效果。 此外，为了启用插入操作符的*链接*，插入函数将返回作为参数传递的流对象。 其目的是通过多个函数调用传递相同的流对象。 如果这样的函数返回一个对象，那么它将是一个副本，这不仅意味着一系列插入将涉及制作大量副本，而且这些副本也是临时的，因此对流的任何更改(例如，像`std::hex`这样的操纵器)都不会持久化。 为了解决这些问题，使用了参考文献。 这类函数的典型原型是：\n\n```cpp\n    ostream& operator<<(ostream& _Ostr, int _val);\n```\n\n显然，您必须小心返回引用，因为您必须确保对象生存期与引用一样长。 此`operator<<`函数将返回在第一个参数中传递的引用，但在下面的代码中，将返回对自动变量的引用：\n\n```cpp\n    string& hello() \n    { \n        string str (\"hello\"); \n        return str; // don't do this! \n    }   // str no longer exists at this point\n```\n\n在前面的代码中，`string`对象只存在于函数的生命周期内，因此此函数返回的引用将引用一个不存在的对象。 当然，您可以返回对函数中声明的`static`变量的引用。\n\n从函数返回引用是一种常见的习惯用法，但无论何时考虑这样做，都要确保别名变量的生存期不在函数的作用域之内。\n\n# 临时名词和参考文献\n\n左值引用必须引用一个变量，但是当涉及到堆栈上声明的`const`引用时，C++ 有一些奇怪的规则。 如果引用是`const`，编译器将在引用的生存期内延长临时的生存期。 例如，如果使用初始化列表语法，编译器将创建一个临时的：\n\n```cpp\n    const int& cri { 42 };\n```\n\n在此代码中，编译器将创建一个临时`int`，并将其初始化为一个值，然后将其别名为`cri`引用(重要的是，此引用为`const`)。 临时值在作用域内时可通过引用获得。 这看起来可能有点奇怪，但请考虑在此函数中使用`const`引用：\n\n```cpp\n    void use_string(const string& csr);\n```\n\n您可以使用`string`变量调用此函数，该变量将显式转换为`string`或使用`string`文字：\n\n```cpp\n    string str { \"hello\" }; \n    use_string(str);      // a std::string object \n    const char *cstr = \"hello\"; \n    use_string(cstr);     // a C string can be converted to a std::string \n    use_string(\"hello\");  // a literal can be converted to a std::string\n```\n\n在大多数情况下，您不希望对内置类型进行`const`引用，但是使用自定义类型(复制时会有开销)是有好处的，正如您在这里看到的那样，如果需要，编译器将后退到创建临时类型。\n\n# 右值引用\n\nC++ 11 定义了一种新的引用类型，右值引用。 在 C++ 11 之前，代码(如赋值操作符)无法判断传递给它的 r 值是否是临时对象。 如果向这样的函数传递对对象的引用，则该函数必须小心不要更改引用，因为这会影响它所引用的对象。 如果引用的是临时对象，则函数可以随心所欲地处理临时对象，因为该对象在函数完成后将不会存活。 C++ 11 允许您专门为临时对象编写代码，因此在赋值的情况下，临时对象的操作符只需*将数据从临时对象*移到正在赋值的对象中。 相反，如果引用不是对临时对象的引用，则必须*复制数据*。 如果数据很大，则这将防止潜在的昂贵分配和复制。 这启用了所谓的*移动语义*。\n\n考虑一下这段相当做作的代码：\n\n```cpp\n    string global{ \"global\" }; \n\n    string& get_global() \n    { \n        return global; \n    } \n\n    string& get_static() \n    { \n        static string str { \"static\" }; \n        return str; \n    } \n\n    string get_temp() \n    { \n        return \"temp\"; \n    }\n```\n\n这三个函数返回一个`string`对象。 在前两种情况下，`string`具有程序的生存期，因此可以返回引用。 在最后一个函数中，该函数返回字符串文字，因此构造了一个临时的`string`对象。 这三个参数都可以用来提供`string`值。 例如：\n\n```cpp\n    cout << get_global() << endl; \n    cout << get_static() << endl; \n    cout << get_temp() << endl;\n```\n\n这三个都可以提供可用于分配`string`对象的字符串。 重要的是，前两个函数返回一个活动对象，而第三个函数返回一个临时对象，但这两个对象可以使用相同的方式。\n\n如果这些函数返回对大型对象的访问，您不会希望将该对象传递给另一个函数，因此，在大多数情况下，您会希望将这些函数返回的对象作为引用进行传递。 例如：\n\n```cpp\n    void use_string(string& rs);\n```\n\nReference 参数可防止字符串的另一个副本。 然而，这只是故事的一半。 `use_string`函数可以操作字符串。 例如，下面的函数从参数创建一个新的`string`，但将字母 a、b 和 o 替换为下划线(表示没有这些字母的单词中的空白处，复制没有 A、B 和 O 血型捐献的生活是什么样子)。 简单的实现如下所示：\n\n```cpp\n    void use_string(string& rs) \n    { \n        string s { rs }; \n        for (size_t i = 0; i < s.length(); ++ i) \n        { \n            if ('a' == s[i] || 'b' == s[i] || 'o' == s[i])  \n            s[i] = '_'; \n        } \n        cout << s << endl; \n    }\n```\n\nString 对象有一个索引运算符(`[]`)，因此您可以将其视为一个字符数组，既读取字符值，又为字符位置赋值。 `string`的大小是通过`length`函数获得的，该函数返回一个`unsigned int`(`typedef`到`size_t`)。 由于该参数是一个引用，这意味着对`string`的任何更改都将反映在传递给函数的`string`中。 此代码的目的是保持其他变量不变，因此它首先复制参数。 然后在副本上，代码遍历所有字符，将`a`、`b`和`o`字符更改为下划线，然后打印出结果。\n\n这段代码显然有复制开销--从引用`rs`创建`string`，`s`；但如果我们想要将类似于`get_global`或`get_static`的字符串传递给该函数，这是必要的，因为否则会对实际的全局变量和`static`变量进行更改。\n\n然而，从`get_temp`返回的临时`string`是另一种情况。 此临时对象仅存在到调用`get_temp`的语句的末尾。 因此，在知道该变量不会影响其他任何内容的情况下，可以对其进行更改。 这意味着您可以使用移动语义：\n\n```cpp\n    void use_string(string&& s) \n    { \n        for (size_t i = 0; i < s.length(); ++ i) \n        { \n            if ('a' == s[i] || 'b' == s[i] || 'o' == s[i]) s[i] = '_'; \n        } \n        cout << s << endl; \n    }\n```\n\n这里只有两个变化。 首先，使用类型的`&&`后缀将参数标识为右值引用。 另一个更改是，更改是在引用引用的对象上进行的，因为我们知道它是临时的，更改将被丢弃，因此它不会影响其他变量。 请注意，现在有两个*个*函数，它们是同名重载：一个具有左值引用，另一个具有右值引用。 调用此函数时，编译器将根据传递给它的参数调用正确的函数：\n\n```cpp\n    use_string(get_global()); // string&  version \n    use_string(get_static()); // string&  version \n    use_string(get_temp());   // string&& version \n    use_string(\"C string\");   // string&& version \n    string str{\"C++ string\"}; \n    use_string(str);          // string&  version\n```\n\n回想一下，`get_global`和`get_static`返回对将存在于程序生命周期中的对象的引用，因此编译器选择接受左值引用的`use_string`版本。 更改是在函数内的临时变量上进行的，这会产生复制开销。 `get_temp`返回一个临时对象，因此编译器调用接受右值引用的`use_string`的重载。 此函数更改引用引用的对象，但这并不重要，因为该对象不会超过行尾的分号。 使用类似 C 的字符串文字调用`use_string`也是如此：编译器将创建一个临时的`string`对象，并调用具有 rvalue 引用参数的重载。 在这段代码的最后一个示例中，在堆栈上创建了一个 C++ `string`对象，并将其传递给`use_string`。 编译器发现该对象是一个左值，并且可能会被更改，因此它调用接受左值引用的重载，该引用的实现方式仅改变函数中的临时局部变量。\n\n此示例显示，C++ 编译器将检测参数何时为临时对象，并使用右值引用调用重载。 通常，在编写*复制构造函数*(用于从现有实例创建新的自定义类型的特殊函数)和赋值运算符时使用此工具，以便这些函数可以实现左值引用重载以从参数复制数据，以及实现右值引用重载以将数据从临时对象移动到新对象。 其他用途是编写*仅移动*的自定义类型，其中它们使用无法复制的资源，例如文件句柄。\n\n# 范围和参考文献\n\n作为可以对引用执行操作的示例，值得看一下 C++ 11 中的 Ranged`for`工具。下面的代码非常简单；数组`squares`是用 0 到 4 的平方初始化的：\n\n```cpp\n    constexpr int size = 4; \n    int squares[size]; \n\n    for (int i = 0; i < size; ++ i) \n    { \n        squares[i] = i * i; \n    }\n```\n\n编译器知道数组的大小，因此您可以使用 Range`for`打印出数组中的值。 在下面的每个迭代中，局部变量`j`是数组中项的副本。 作为副本，这意味着您可以读取值，但对变量所做的任何更改都不会反映到数组中。 因此，下面的代码按预期工作；它打印出数组的内容：\n\n```cpp\n    for (int j : squares) \n    { \n        cout << J << endl; \n    }\n```\n\n如果要更改数组中的值，则必须访问实际值，而不是副本。 在范围`for`中执行此操作的方法是使用引用作为循环变量：\n\n```cpp\n    for (int& k : squares) \n    { \n        k *= 2; \n    }\n```\n\n现在，在每次迭代中，`k`变量都是数组中实际成员的别名，因此您对`k`变量所做的任何操作实际上都是在数组成员上执行的。 在本例中，`squares`数组的每个成员都乘以 2。您不能使用`int*`作为`k`的类型，因为编译器发现数组中的项的类型是`int`，并将其用作范围`for`中的循环变量。 由于引用是变量的别名，编译器将允许引用作为循环变量，而且，由于引用是别名，您可以使用它来更改实际的数组成员。\n\nRANGED`for`对于多维数组来说变得很有趣。 例如，在下面的示例中，声明了一个二维数组，并尝试使用`auto`个变量使用嵌套循环：\n\n```cpp\n    int arr[2][3] { { 2, 3, 4 }, { 5, 6, 7} };   \n    for (auto row : arr) \n    { \n        for (auto col : row) // will not compile\n        { \n            cout << col << \" \" << endl; \n        } \n    }\n```\n\n由于二维数组是数组数组(每行都是一维数组)，因此目的是在外部循环中获取每一行，然后在内部循环中访问该行中的每一项。 这种方法有几个问题，但最直接的问题是此代码无法编译。\n\n编译器会抱怨内部循环，说它找不到类型`int*`的`begin`或`end`函数。 原因是 Range`for`使用迭代器对象，对于数组，它使用 C++ 标准库函数`begin`和`end,`来创建这些对象。 编译器将从外部范围内的`arr`数组中看到，每一项都是一个`int[3]`数组，因此在外部`for`循环中，循环变量将是每个元素的*副本*，在本例中是一个`int[3]`数组。 您不能像这样复制数组，因此编译器将提供指向第一个元素的指针，即`int*`，这将在内部`for`循环中使用。\n编译器将尝试获取`int*`的迭代器，但这是不可能的，因为`int*`不包含关于它指向多少项的信息。 有为`int[3]`(以及所有大小的数组)定义的`begin`和`end`版本，但没有为`int*`定义。\n\n一个简单的更改就可以编译此代码。 只需将`row`变量转换为引用：\n\n```cpp\n    for (auto& row : arr) \n    { \n        for (auto col : row) \n        { \n            cout << col << \" \" << endl; \n        } \n    }\n```\n\nReference 参数指示别名用于`int[3]`数组，当然，别名与元素相同。 使用`auto`隐藏了实际发生的事情的丑陋之处。 当然，内部循环变量是`int`，因为这是数组中项的类型。 外部循环变量实际上是`int (&)[3]`。 也就是说，它是对`int[3]`的引用(圆括号用于指示它引用的是`int[3]`，而不是`int&`的数组)。\n\n# 在实践中使用指针\n\n一个常见的要求是拥有一个可以是任意大小并且可以在运行时增大和缩小的集合。 C++ 标准库提供了各种类来允许您这样做，正如将在[章](08.html)，*中使用标准库容器*所描述的那样。 下面的示例说明了如何实现这些标准集合的一些原则。 通常，您应该使用 C++ 标准库类，而不是实现您自己的类。 此外，标准库类*将*代码封装在一个类中，由于我们还没有讨论类，下面的代码将使用可能被错误调用的函数。 因此，您应该将此示例视为示例代码。 链表是一种常见的数据结构。 它们通常用于项目顺序很重要的队列。 例如，先进先出队列，其中按任务插入队列的顺序执行任务。 在本例中，每个任务都表示为一个结构，该结构包含任务描述和指向要执行的下一个任务的指针。\n如果指向下一个任务的指针为`nullptr`，则表示当前任务是列表中的最后一个任务：\n\n```cpp\n    struct task \n    { \n        task* pNext; \n        string description; \n    };\n```\n\n回想上一章，您通过实例使用点运算符访问结构的成员：\n\n```cpp\n    task item; \n    item.descrription = \"do something\";\n```\n\n在本例中，编译器将创建一个用字符串文字`do something`初始化的`string`对象，并将其分配给名为`item`的实例的`description`成员。 您还可以使用`new`运算符在免费商店上创建`task`：\n\n```cpp\n    task* pTask = new task; \n    // use the object \n    delete pTask;\n```\n\n在这种情况下，必须通过指针访问对象的成员，而 C++ 提供了`->`操作符来为您提供此访问权限：\n\n```cpp\n    task* pTask = new task; \n    pTask->descrription = \"do something\"; \n    // use the object \n    delete pTask;\n```\n\n在这里，`description`成员被分配给字符串。 请注意，由于`task`是一种结构，因此没有访问限制，这一点对于类很重要，在[第 6 章](06.html)、*类*中进行了描述。\n\n# 正在创建项目\n\n在`C:\\Beginning_C++ `下创建一个名为`Chapter_04`的新文件夹。 启动 Visual C++ 并创建一个 C++ 源文件，并将其保存到您刚刚创建的文件夹中，名称为`tasks.cpp`。 添加一个不带参数的简单`main`函数，并使用 C++ 流支持输入和输出：\n\n```cpp\n    #include <iostream> \n    #include <string> \n    using namespace std; \n\n    int main() \n    {\n    }\n```\n\n在`main`函数上方，添加表示列表中任务的结构定义：\n\n```cpp\n    using namespace std;  \n struct task { task* pNext; string description; };\n```\n\n它有两个成员。 对象的内部是`description`项。 在我们的示例中，执行任务需要将`description`项打印到控制台。 在实际项目中，您很可能有许多与任务相关的数据项，甚至可能有执行任务的成员函数，但我们还没有讨论成员函数；这是[第 6 章](06.html)、*类*的主题。\n\n链表的管道是另一个成员`pNext`。 请注意，在声明`pNext`成员时，尚未完全定义`task`结构。 这不是问题，因为`pNext`是*指针*。 不能有未定义类型或部分定义类型的数据成员，因为编译器不知道要为它分配多少内存。 您可以拥有指向部分定义类型的指针成员，因为无论指针成员指向的是什么，其大小都是相同的。\n\n如果我们知道列表中的第一个链接，那么我们就可以访问整个列表，在我们的示例中，这将是一个全局变量。 在构造列表时，构造函数需要知道列表的末尾，以便它们可以将新链接附加到列表。 为方便起见，我们再次将其设置为全局变量。 在定义`task`结构后添加以下指针：\n\n```cpp\n task* pHead = nullptr; task* pCurrent = nullptr;  \n    int main() \n    {\n    }\n```\n\n按照目前的情况，代码什么也不做，但这是编译文件以测试是否没有输入错误的好机会：\n\n```cpp\ncl /EHsc tasks.cpp\n```\n\n# 将任务对象添加到列表\n\n提供代码的下一件事是向任务列表中添加一个新任务。 这需要创建一个新的`task`对象并适当地对其进行初始化，然后通过将列表中的最后一个链接更改为指向新链接来将其添加到列表中。\n\n在`main`函数上方添加以下函数：\n\n```cpp\n    void queue_task(const string& name) \n    { \n        ...\n    }\n```\n\n该参数是一个`const`引用，因为我们不会更改该参数，并且我们不希望产生复制的开销。 该函数必须做的第一件事是创建一个新链接，因此添加以下行：\n\n```cpp\n    void queue_task(const string& name) \n    { \n task* pTask = new task; pTask->description = name; pTask->pNext = nullptr; \n    }\n```\n\n第一行在免费商店上创建一个新链接，以下几行对其进行初始化。 这不一定是初始化此类对象的最佳方式，更好的机制-构造函数-将在[第 6 章](06.html)、*类*中介绍。 请注意，`pNext`项被初始化为`nullptr`；这表示链接将位于列表的末尾。\n\n此函数的最后一部分将链接添加到列表中，即使该链接成为列表中的最后一个链接。 但是，如果列表为空，则表示此链接也是列表中的第一个*个*个链接。 代码必须同时执行这两个操作。 将以下代码添加到该函数的末尾：\n\n```cpp\n    if (nullptr == pHead) \n    { \n        pHead = pTask; \n        pCurrent = pTask; \n    } \n    else \n    { \n        pCurrent->pNext = pTask; \n        pCurrent = pTask; \n    }\n```\n\n第一行检查列表是否为空。 如果`pHead`为`nullptr`，则表示没有其他链接，因此当前链接是第一个链接，因此`pHead`和`pCurrent`都被初始化为新的链接指针。 如果列表中有现有链接，则必须将链接添加到最后一个链接，因此在`else`子句中，第一行使最后一个链接指向新链接，第二行用新链接指针初始化`pCurrent`，使新链接成为列表中任何新插入的最后一个链接。\n\n通过在`main`函数中调用此函数将项目添加到列表中。 在本例中，我们将把任务排队到一个房间的墙纸上。 这包括去除旧墙纸，填满墙上的任何洞，调整墙的大小(用稀释的浆糊粉刷，使墙变得粘性)，然后将粘贴的墙纸挂在墙上。 您必须按此顺序执行这些任务，不能更改顺序，因此这些任务是链表的理想选择。 在`main`函数中添加以下行：\n\n```cpp\n    queue_task(\"remove old wallpaper\"); \n    queue_task(\"fill holes\"); \n    queue_task(\"size walls\"); \n    queue_task(\"hang new wallpaper\");\n```\n\n在最后一行之后，已经创建了列表。 `pHead`变量指向列表中的第一个项目，您只需跟随`pNext`成员从一个链接到下一个链接，即可访问列表中的任何其他项目。\n\n您可以编译代码，但没有输出。 更糟糕的是，正如代码所示，存在内存泄漏。 该程序没有代码来`delete`由`new`操作符在空闲存储器上创建的`task`对象所占用的内存。\n\n# 正在删除任务列表\n\n遍历列表很简单，您可以沿着`pNext`指针从一个链接到下一个链接。 在执行此操作之前，让我们先修复上一节中介绍的内存泄漏。 在`main`函数上方添加以下函数：\n\n```cpp\n    bool remove_head() \n    { \n        if (nullptr == pHead) return false; \n        task* pTask = pHead; \n        pHead = pHead->pNext; \n        delete pTask; \n        return (pHead != nullptr); \n    }\n```\n\n此函数将删除列表开头的链接，并确保`pHead`指针指向下一个链接，该链接将成为列表的新开始。 该函数返回一个`bool`值，指示列表中是否还有更多链接。 如果此函数返回`false`，则表示整个列表已被删除。\n\n第一行检查是否使用空列表调用了此函数。 一旦我们确信列表至少有一个链接，我们就会创建此指针的临时副本。 原因是这样做的目的是删除第一项并使`pHead`指向下一项，为此，我们必须反向执行这些步骤：使`pHead`指向下一项，然后删除`pHead`先前指向的项。\n\n要删除整个列表，需要遍历链接，这可以使用`while`循环来实现。 在`remove_head`函数下面添加以下内容：\n\n```cpp\n    void destroy_list() \n    { \n        while (remove_head()); \n    }\n```\n\n要删除整个列表并解决内存泄漏问题，请在 Main 函数的底部添加以下行\n\n```cpp\n destroy_list(); \n    }\n```\n\n现在可以编译代码并运行它。 但是，您将看不到任何输出，因为代码所做的全部工作就是创建一个列表，然后将其删除。\n\n# 迭代任务列表\n\n下一步是从每个`pNext`指针后面的第一个链接开始迭代列表，直到我们到达列表的末尾。 对于访问的每个链接，都应该执行任务。 首先编写一个函数，该函数通过打印任务的描述来执行，然后返回指向下一个任务的指针。 在`main`函数的正上方添加以下代码：\n\n```cpp\n    task *execute_task(const task* pTask) \n    { \n        if (nullptr == pTask) return nullptr; \n        cout << \"executing \" << pTask->description << endl; \n        return pTask->pNext; \n    }\n```\n\n这里的参数被标记为`const`，因为我们不会更改指针指向的`task`对象。 这向编译器表明，如果代码确实尝试更改对象，则存在问题。 第一行检查以确保没有使用空指针调用函数。 如果是，则下一行将取消引用无效指针并导致内存访问错误。 最后一行返回指向下一个链接的指针(对于列表中的最后一个链接，指针可以是`nullptr`)，这样就可以在循环中调用该函数。 在此函数之后，添加以下内容以迭代整个列表：\n\n```cpp\n    void execute_all() \n    { \n        task* pTask = pHead; \n        while (pTask != nullptr) \n        { \n            pTask = execute_task(pTask); \n        } \n    }\n```\n\n此代码从开头`pHead`开始，并对列表中的每个链接调用`execute_task`，直到函数返回`nullptr`。 在`main`函数末尾添加对此函数的调用：\n\n```cpp\n execute_all(); \n        destroy_list(); \n    }\n```\n\n现在可以编译和运行代码了。 结果将是：\n\n```cpp\n    executing remove old wallpaper\nexecuting fill holes\n executing size walls executing hang new wallpaper\n```\n\n# 插入项目\n\n链表的优点之一是，您只需分配一个新项并更改指向它的适当指针，并使其指向列表中的下一项，即可将项插入列表。 这与分配`task`个对象的数组形成对比；如果要在中间的某个位置插入新项，则必须为旧项和新项分配足够大的新数组，然后将旧项复制到新数组中，并将新项复制到正确位置。\n\n墙纸任务清单的问题是，房间里有一些油漆过的木头，正如任何装饰师都知道的那样，最好在挂墙纸之前，通常是在调整墙壁大小之前，先给木器上漆。 我们需要在填补任何洞和调整墙壁大小之间插入一项新任务。 此外，在你做任何装饰之前，你应该在做其他任何事情之前覆盖房间里的所有家具，所以你需要在开始时增加一个新的任务。\n\n第一步是找到我们想要把新任务放在哪里来粉刷木制品。 我们将在插入的任务之前查找我们想要的任务。 在`main`之前添加以下内容：\n\n```cpp\n    task *find_task(const string& name) \n    { \n        task* pTask = pHead; \n\n        while (nullptr != pTask) \n        { \n            if (name == pTask->description) return pTask; \n            pTask = pTask->pNext; \n        }  \n        return nullptr; \n    }\n```\n\n此代码在整个列表中搜索具有与参数匹配的`description`的链接。 这是通过使用`string`比较运算符的循环执行的，如果找到所需的链接，则返回指向该链接的指针。 如果比较失败，循环会将循环变量初始化为下一个链接的地址，如果该地址为`nullptr`，则表示所需的任务不在列表中。\n\n在 main 函数中创建列表后，添加以下代码以搜索`fill holes`任务：\n\n```cpp\n    queue_task(\"hang new wallpaper\"); \n\n // oops, forgot to paint \n    woodworktask* pTask = find_task(\"fill holes\"); if (nullptr != pTask) { // insert new item after pTask } \n    execute_all();\n```\n\n如果`find_task`函数返回一个有效的指针，那么我们可以在这一点上添加一个项。\n\n执行此操作的函数将允许您在传递给它的列表中的任何项之后添加一个新项，如果您传递`nullptr`，它会将新项添加到开头。 它被称为`insert_after`，但显然，如果你传递`nullptr`，它也意味着在开头之前插入*。 在`main`函数的正上方添加以下内容：*\n\n```cpp\n    void insert_after(task* pTask, const string& name) \n    { \n        task* pNewTask = new task; \n        pNewTask->description = name; \n        if (nullptr != pTask) \n        { \n            pNewTask->pNext = pTask->pNext; \n            pTask->pNext = pNewTask; \n        } \n    }\n```\n\n第二个参数是`const`引用，因为我们不会更改`string`，但第一个参数不是`const`指针，因为我们将更改它所指向的对象。 此函数创建一个新的`task`对象，并将`description`成员初始化为新的任务名称。 然后，它检查传递给函数的`task`指针是否为空。 如果不是，则可以在列表中的指定链接之后插入*新项。 为此，新链接`pNext`成员被初始化为列表中的下一项，前一链接的`pNext`成员被初始化为新链接的地址。*\n\n当函数传递`nullptr`作为后面要插入的项目时，在开头插入一个项目怎么样？ 添加以下`else`子句。\n\n```cpp\n    void insert_after(task* pTask, const string& name) \n    { \n        task* pNewTask = new task; \n        pNewTask->description = name; \n        if (nullptr != pTask) \n        { \n            pNewTask->pNext = pTask->pNext; \n            pTask->pNext = pNewTask; \n        } \n        else { pNewTask->pNext = pHead; pHead = pNewTask; } \n    }\n```\n\n在这里，我们使新项的`pNext`成员指向列表的旧开始，然后将`pHead`更改为指向新项。\n\n现在，在`main`函数中，您可以添加一个调用，以插入一个新任务来绘制木制品，而且由于我们还忘记指出，最好在所有家具上覆盖防尘布之后装饰房间，因此在列表中首先添加一个任务来完成此任务：\n\n```cpp\n    task* pTask = find_task(\"fill holes\"); \n    if (nullptr != pTask) \n    { \n insert_after(pTask, \"paint woodwork\"); \n    } \n insert_after(nullptr, \"cover furniture\");\n```\n\n现在可以编译代码了。 运行代码时，您应该看到按所需顺序执行的任务：\n\n```cpp\n executing cover furniture executing remove old wallpaper\nexecuting fill holes\nexecuting paint woodwork\nexecuting size walls\nexecuting hang new wallpaper \n```\n\n# 简略的 / 概括的 / 简易判罪的 / 简易的\n\n可以说，使用 C++ 的主要原因之一是您可以使用指针直接访问内存。 这是大多数其他语言的程序员被阻止执行的功能。 这意味着作为一名 C++ 程序员，您是一种特殊类型的程序员：在内存方面值得信赖的人。 在本章中，您已经了解了如何获取和使用指针，以及一些不恰当使用指针会导致代码严重错误的示例。\n\n在下一章中，我们将介绍函数，其中将包括对另一种类型指针的描述：函数指针。 如果你被信任拥有指向数据和函数指针的指针，那么你真的是一种特殊类型的程序员。*"
  },
  {
    "path": "docs/begin-cpp-prog/05.md",
    "content": "# 五、使用函数\n\n函数是 C++ 的基本基础设施；代码包含在函数中，要执行该代码，您必须调用函数。 C++ 在定义和调用函数的方式上非常灵活：您可以使用固定数量的参数或可变数量的参数定义函数；您可以编写泛型代码，以便相同的代码可以用于不同的类型；您甚至可以编写数量可变的类型的泛型代码。\n\n# 定义 C++ 函数\n\n在最基本的级别上，函数有参数，有操作参数的代码，并返回值。 C++ 为您提供了几种确定这三个方面的方法。 在下一节中，我们将从声明的左侧到右侧介绍 C++ 函数的这些部分。 函数也可以**模板化**，但这将留待后面的小节讨论。\n\n# 声明和定义函数\n\n一个函数必须只定义一次，但通过重载，您可以拥有许多名称相同但参数不同的函数。 使用函数的代码必须有权访问函数的名称，因此它需要有权访问函数定义(例如，函数在源文件中早先定义)或函数的声明(也称为函数原型)。 编译器使用原型来类型检查*调用代码*是否使用正确的类型调用函数。\n\n通常，库被实现为单独的编译后的库文件，并且库函数的原型在头文件中提供，以便许多源文件可以通过包括头文件来使用函数。 但是，如果您知道函数名、参数和返回类型，则可以自己在文件中键入原型。 无论执行哪种操作，您都只是为编译器提供信息，以便对调用函数的表达式进行类型检查。 链接器负责定位库中的函数，并将代码复制到可执行文件中，或者设置基础结构以使用共享库中的函数。 包含库的头文件并不意味着您将能够使用该库中的函数，因为在标准 C++ 中，头文件不包含有关包含函数的库的信息。\n\nVisual C++ 提供了一个名为`comment`的`pragma`，它可以与`lib`选项一起用作链接器的消息，以链接到特定库。 因此，头文件中的`#pragma comment(lib, \"mylib\")`将告诉链接器与`mylib.lib`链接。 通常，最好使用项目管理工具，如**nmake**或**MSBuild**，以确保项目中链接了正确的库。\n\n大多数 C 运行时库是这样实现的：函数在静态库或动态链接库中编译，函数原型在头文件中提供。 您可以在链接器命令行中提供库，并且通常会包括库的头文件，以便编译器可以使用函数原型。 只要链接器知道该库，您就可以在代码中键入原型(并将其描述为*外部链接*，以便编译器知道函数是在其他地方定义的)。 这可以使您避免在源文件中包含一些大文件，这些文件大多具有您不会使用的功能原型。\n\n但是，大部分 C++ 标准库都是在头文件中实现的，这意味着这些文件可能非常大。 您可以通过将这些头文件包含在预编译头文件中来节省编译时间，如[第 1 章](01.html)、*中从 C++*开始所述。\n\n到目前为止，在本书中，我们使用了一个源文件，因此所有函数都在使用它们的同一文件中定义，并且我们在调用函数之前定义了函数，也就是说，函数定义在调用它的代码之上*。 只要在调用函数之前定义了函数原型，就不必在使用函数之前定义函数：*\n\n```cpp\n    int mult(int, int); \n\n    int main() \n    { \n        cout << mult(6, 7) << endl; \n        return 0; \n    } \n\n    int mult(int lhs, int rhs) \n    { \n        return lhs * rhs; \n    }\n```\n\n`mult`函数在`main`函数之后定义，但此代码将编译，因为原型在`main`函数之前给定。 这称为**向前声明**。 原型不必具有参数名称。 这是因为编译器只需要知道参数的类型，而不需要知道它们的名称。 但是，由于参数名称应该是自我说明的，所以给出参数名称通常是一个好主意，这样您就可以看到函数的用途。\n\n# 指定链接\n\n在上例中，该函数是在同一源文件中定义的，因此存在*内部链接*。 如果函数是在另一个文件中定义的，则原型将具有*外部链接*，因此原型必须定义如下：\n\n```cpp\n    extern int mult(int, int);        // defined in another file\n```\n\n`extern`关键字是可以添加到函数声明的众多说明符之一，在前面的章节中我们已经看到了其他说明符。 例如，可以在原型上使用`static`说明符，以指示该函数具有内部链接，并且该名称只能在当前源文件中使用。 在前面的示例中，将原型中的函数标记为`static`比较合适。\n\n```cpp\n    static int mult(int, int);        // defined in this file\n```\n\n还可以将函数声明为`extern \"C\"`，这会影响函数名在目标文件中的存储方式。 这对图书馆很重要，我们很快就会讲到。\n\n# 内联\n\n如果函数计算的值可以在编译时计算，则可以在声明的左侧用`constexpr`标记该值，以指示编译器可以通过在编译时计算该值来优化代码。 如果函数值可以在编译时计算，这意味着函数调用中的参数在编译时必须是已知的，因此它们必须是文字。 该函数也必须为单行。 如果不满足这些限制，则编译器可以自由忽略该说明符。\n\nRelated 是`inline`说明符。 这可以放在函数声明的左侧，作为对编译器的建议：当其他代码调用该函数时，编译器应该将实际代码的副本放入调用函数中，而不是由编译器在内存中插入指向该函数的跳转(以及创建堆栈帧)。 同样，编译器可以随意忽略此说明符。\n\n# 确定返回类型\n\n可以编写函数来运行例程，而不返回值。 如果是这种情况，则必须指定该函数返回`void`。 在大多数情况下，函数将返回值，即使只是为了指示函数已正确完成。 不要求调用函数获取返回值或对其执行任何操作。 调用函数可以简单地忽略返回值。\n\n有两种方法可以指定返回类型。 第一种方法是在函数名之前指定类型。 这是迄今为止大多数示例中使用的方法。 第二种方法称为**尾随返回类型**，要求您将`auto`作为返回类型放在函数名之前，并使用`->`语法在参数列表之后提供实际的返回类型：\n\n```cpp\n    inline auto mult(int lhs, int rhs) -> int \n    { \n        return lhs * rhs; \n    }\n```\n\n这个函数非常简单，很适合内联。 左边的返回类型是`auto`，这意味着实际的返回类型是在参数列表之后指定的。 `-> int`表示返回类型为`int`。 此语法与在左侧使用`int`具有相同的效果。 当函数模板化并且返回类型可能不明显时，此语法非常有用。\n\n在这个简单的示例中，您可以完全省略返回类型，只需在函数名左侧使用`auto`即可。 此语法意味着编译器将从返回的实际值推断返回类型。 显然，编译器只知道函数体的返回类型，因此不能提供此类函数的原型。\n\n最后，如果一个函数根本没有返回(例如，如果它进入一个永无止境的循环来轮询某个值)，您可以用 C++ 11 属性`[[noreturn]]`来标记它。 编译器可以使用此属性编写更高效的代码，因为它知道不需要提供返回值的代码。\n\n# 命名函数\n\n通常，函数名对变量有相同的规则：它们必须以字母或下划线开头，并且不能包含空格或其他标点符号。 遵循自文档化代码的一般原则，您应该根据函数的功能来命名函数。 有一个例外，它们是用于为操作符(大多数是标点符号)提供重载的特殊函数。 这些函数的名称形式为`operatorx`，其中`x`是您将在代码中使用的运算符。 后面的部分将解释如何实现具有全局函数的运算符。\n\n运算符是重载的一个示例。 您可以重载任何函数，即使用相同的名称，但提供不同参数类型或不同数量的参数的实现。\n\n# 函数参数\n\n函数可以没有参数，在这种情况下，函数是用一对空括号定义的。 函数定义必须在圆括号内给出参数的类型和名称。 在许多情况下，函数将具有固定数量的参数，但您可以使用可变数量的参数编写函数。 您还可以使用某些参数的默认值定义函数，实际上，提供了一个根据传递给函数的参数数量重载自身的函数。 变量参数列表和默认参数将在后面介绍。\n\n# 指定例外情况\n\n还可以标记函数以指示它们是否会引发异常。 有关异常的更多详细信息将在[第 10 章](10.html)，*诊断和调试*中提供，但您需要了解两种语法。\n\nC++ 的早期版本允许您以三种方式在函数上使用`throw`说明符：第一，您可以提供函数中代码可能引发的异常类型的逗号分隔列表；第二，您可以提供省略号(`...`)，这意味着函数可能抛出任何异常；第三，您可以提供一对空圆括号，这意味着函数不会抛出异常。 语法如下所示：\n\n```cpp\n    int calculate(int param) throw(overflow_error) \n    { \n        // do something which potentially may overflow \n    }\n```\n\n在 C++ 11 中，`throw`说明符已被弃用，很大程度上是因为指示异常类型的功能没有用处。 然而，表示不会抛出异常的`throw`版本被发现是有用的，因为它使编译器能够通过不提供处理异常的代码基础设施来优化代码。 C++ 11 使用`noexcept`说明符保留了此行为：\n\n```cpp\n    // C++ 11 style: \n    int increment(int param) noexcept \n    { \n        // check the parameter and handle overflow appropriately \n    }\n```\n\n# 函数体\n\n在确定返回类型、函数名和参数之后，您需要定义函数体。 函数的代码必须出现在一对大括号(`{}`)之间。 如果函数返回值，则函数必须至少有一行(函数中的最后一行)带有`return`语句。 这必须返回适当的类型或可以隐式转换为函数的返回类型的类型。 如前所述，如果函数被声明为返回`auto`，那么编译器将推导出返回类型。 在这种情况下，所有`return`语句*必须*返回相同的类型。\n\n# 使用函数参数\n\n调用函数时，编译器会检查该函数的所有重载，以查找与调用代码中的参数匹配的重载。 如果没有完全匹配，则执行标准和用户定义的类型转换，因此调用代码提供的值可能是与参数不同的类型。\n\n默认情况下，参数按值传递并复制，这意味着参数被视为函数中的局部变量。 函数的编写者可以决定通过引用传递参数，可以通过指针传递，也可以通过 C++ 引用传递。 **按引用传递**意味着调用代码中的变量可以由函数更改，但这可以通过设置参数`const`来控制，在这种情况下，按引用传递的原因是为了防止复制(可能代价高昂)。 内置数组始终作为指向数组第一项的指针传递。 编译器将在需要时创建临时文件。 例如，当参数是`const`引用并且调用代码传递文本时，将创建一个临时对象，并且该对象仅可用于函数中的代码：\n\n```cpp\n    void f(const float&); \n    f(1.0);              // OK, temporary float created \n    double d = 2.0; \n    f(d);                // OK, temporary float created\n```\n\n# 传递初始值设定项列表\n\n如果初始值设定项列表可以转换为参数类型，则可以将该列表作为参数传递。 例如：\n\n```cpp\n    struct point { int x; int y; }; \n\n    void set_point(point pt); \n\n    int main() \n    { \n        point p; \n        p.x = 1; p.y = 1; \n        set_point(p); \n        set_point({ 1, 1 });  \n        return 0; \n    }\n```\n\n此代码定义了一个具有两个成员的结构。 在`main`函数中，将在堆栈上创建`point`的新实例，并通过直接访问成员对其进行初始化。 然后将该实例传递给具有`point`参数的函数。 因为`set_point`的参数是按值传递的，所以编译器会在函数堆栈上创建该结构的副本。 第二次调用`set_point`也做了同样的事情：编译器将在函数堆栈上创建一个临时的`point`对象，并使用初始化式列表中的值对其进行初始化。\n\n# 使用默认参数\n\n有些情况下，一个或多个参数的值使用频率很高，您希望将其视为参数的默认值，同时仍可以选择允许调用方在必要时提供不同的值。 为此，请在定义的参数列表中提供默认值：\n\n```cpp\n    void log_message(const string& msg, bool clear_screen = false) \n    { \n        if (clear_screen) clear_the_screen(); \n        cout << msg << endl; \n    }\n```\n\n在大多数情况下，此功能预计用于打印一条消息，但有时用户可能希望先清除屏幕(例如，对于第一条消息，或在预定行数之后)。 为了适应函数的这种使用，为`clear_screen`参数指定了默认值`false`，但调用方仍然可以选择传递一个值：\n\n```cpp\n    log_message(\"first message\", true); \n    log_message(\"second message\"); \n    bool user_decision = ask_user(); \n    log_message(\"third message\", user_decision);\n```\n\n请注意，默认值出现在函数定义中，而不是出现在函数原型中，因此如果在头文件中声明`log_message`函数，则原型应该是：\n\n```cpp\n    extern void log_message(const string& msg, bool clear_screen);\n```\n\n可以具有默认值的参数是最右侧的参数。\n\n您可以将具有默认值的每个参数视为表示函数的单独重载，因此从概念上讲，`log_message`函数应被视为两个函数：\n\n```cpp\n    extern void log_message(const string& msg, bool clear_screen); \n    extern void log_message(const string& msg); // conceptually\n```\n\n如果您定义的`log_message`函数只有一个`const string&`参数，那么编译器将不知道是调用该函数还是调用`clear_screen`被赋予默认值`false`的版本。\n\n# 可变数量的参数\n\n具有默认参数值的函数可以被视为具有可变数量的用户提供的参数，如果调用方选择不提供值，您可以在编译时知道参数的最大数量及其值。 C++ 还允许您在参数数量和传递给函数的值不太确定的情况下编写函数。\n\n有三种方法可以使用可变数量的参数：初始值设定项列表、C 风格的变量参数列表和可变模板化函数。 这三种方法中的后一种将在本章后面讨论，一旦介绍了模板化函数。\n\n# 初始值设定项列表\n\n到目前为止，在本书中，初始化器列表被视为一种 C++ 11 构造，有点像内置数组。 事实上，当您使用带大括号的初始值设定项列表语法时，编译器实际上会创建模板化`initialize_list`类的一个实例。 如果初始值设定项列表用于初始化另一个类型(例如，初始化`vector`)，编译器将使用大括号之间给出的值创建一个`initialize_list`对象，并使用`initialize_list`迭代器初始化容器对象。 这种从带括号的初始值设定项列表创建`initialize_list`对象的功能可用于为函数提供可变数量的参数，尽管所有参数必须属于同一类型：\n\n```cpp\n    #include <initializer_list> \n\n    int sum(initializer_list<int> values) \n    { \n        int sum = 0; \n        for (int i : values) sum += i; \n        return sum; \n    } \n\n    int main() \n    { \n        cout << sum({}) << endl;                       // 0 \n        cout << sum({-6, -5, -4, -3, -2, -1}) << endl; // -21 \n        cout << sum({10, 20, 30}) << endl;             // 60 \n        return 0; \n    }\n```\n\n`sum`函数只有一个参数`initializer_list<int>`，只能用整数列表进行初始化。 `initializer_list`类只有很少的函数，因为它只提供对带括号列表中的值的访问。 值得注意的是，它实现了一个返回列表中项目数的`size`函数，以及返回指向列表中第一个项目和最后一个项目之后位置的指针的`begin`和`end`函数。 这两个函数是提供对列表的迭代器访问所必需的，它使您能够将对象与 range-`for`语法一起使用。\n\nThis is typical in the C++ Standard Library. If a container holds data in a contiguous block of memory, then pointer arithmetic can use the pointer to the first item and a pointer immediately after the last item to determine how many items are in the container. Incrementing the first pointer gives sequential access to every item, and pointer arithmetic allows random access. All containers implement a `begin` and `end` function to give access to the container *iterators*.\n\n在本例中，`main`函数调用此函数三次，每次都使用带括号的初始值设定项列表，该函数将返回列表中项目的总和。\n\n显然，这种技术意味着*变量*参数列表中的每一项都必须是相同的类型(或者是可以转换为指定类型的类型)。 如果参数是`vector`，则会得到相同的结果；不同之处在于，`initializer_list`参数需要的初始化较少。\n\n# 参数列表\n\nC++ 继承了 C 的参数列表思想。 为此，您可以使用省略号语法(`...`)作为最后一个参数，以指示调用方可以提供零个或多个参数。 编译器将检查函数是如何调用的，并在堆栈上为这些额外参数分配空间。 要访问额外的参数，您的代码必须包括`<cstdarg>`头文件，该文件包含可用于从堆栈中提取额外参数的宏。\n\n这本质上是类型不安全的，因为编译器无法检查函数在运行时将从堆栈中取出的参数是否与调用代码放入堆栈中的参数类型相同。 例如，以下是对整数求和的函数的实现：\n\n```cpp\n    int sum(int first, ...) \n    { \n        int sum = 0;    \n        va_list args; \n        va_start(args, first); \n        int i = first; \n        while (i != -1) \n        { \n            sum += i; \n            i = va_arg(args, int); \n        } \n        va_end(args); \n        return sum; \n    }\n```\n\n函数的定义必须至少有一个参数，这样宏才能工作；在本例中，该参数称为`first`。 重要的是，您的代码使堆栈保持一致的状态，这是使用`va_list`类型的变量执行的。 该变量在函数开始时通过调用`va_start`宏来初始化，堆栈在函数结束时通过调用`va_end`宏来恢复到以前的状态。\n\n此函数中的代码简单地迭代参数列表，并维护一个和，当参数的值为-1 时，循环结束。 没有宏来提供堆栈上有多少参数的信息，也没有任何宏来指示堆栈上参数的类型。 您的代码必须假定变量的类型，并在`va_arg`宏中提供所需的类型。 在本例中，假设堆栈上的每个参数都是`int`，则调用`va_arg`。\n\n一旦从堆栈中读取了所有参数，代码就会在返回总和之前调用`va_end`。 该函数可以按如下方式调用：\n\n```cpp\n    cout << sum(-1) << endl;                       // 0 \n    cout << sum(-6, -5, -4, -3, -2, -1) << endl;   // -20 !!! \n    cout << sum(10, 20, 30, -1) << endl;           // 60\n```\n\n由于`-1`用于指示列表的末尾，这意味着要使参数之和为零，您必须传递至少一个参数，即`-1`。 此外，第二行显示您在传递负数列表时有问题(在本例中`-1`不能是参数)。 在该实现中，可以通过选择另一个*标记值*来解决该问题。\n\n另一种实现可以避免使用标记作为列表末尾，而是使用第一个必需的参数来给出后面的参数计数：\n\n```cpp\n    int sum(int count, ...) \n    { \n        int sum = 0; \n        va_list args; \n        va_start(args, count); \n        while(count--) \n        { \n            int i = va_arg(args, int); \n            sum += i; \n        } \n        va_end(args); \n        return sum; \n    }\n```\n\n这一次，第一个值是后面的*个参数*，因此例程将从堆栈中提取确切数量的整数并对它们求和。 代码的名称如下所示：\n\n```cpp\n    cout << sum(0) << endl;                         // 0 \n    cout << sum(6, -6, -5, -4, -3, -2, -1) << endl; // -21 \n    cout << sum(3, 10, 20, 30) << endl;             // 60\n```\n\n对于如何处理确定传递了多少参数的问题，没有约定。\n\n例程假定堆栈上的每一项都是`int`，但是在函数的原型中没有关于这一点的信息，因此编译器不能对实际用于调用函数的参数进行类型检查。 如果调用方提供了不同类型的参数，则可能会从堆栈中读取错误的字节数，从而使对`va_arg`的所有其他调用的结果无效。 请考虑以下内容：\n\n```cpp\n    cout << sum(3, 10., 20, 30) << endl;\n```\n\n同时按逗号和句点键很容易，这是在键入`10`参数之后发生的。 句点表示`10`是`double`，因此编译器将`double`值放入堆栈。 当函数使用`va_arg`宏从堆栈读取值时，它会将 8 字节的`double`读取为两个 4 字节的`int`值，对于 Visual C++ 生成的代码，这将导致总和为`1076101140`。 这说明了参数列表的类型不安全方面：无法让编译器对传递给函数的参数进行类型检查。\n\n如果您的函数传递了不同的类型，那么您必须实现某种机制来确定这些参数是什么。 参数列表的一个很好的例子是 C`printf`函数：\n\n```cpp\n    int printf(const char *format, ...);\n```\n\n该函数所需的参数是一个格式字符串，重要的是，它有一个变量参数及其类型的有序列表。 格式字符串提供了通过`<cstdarg>`宏不可用的信息：变量参数的数量和每个变量参数的类型。 `printf`函数的实现将遍历格式字符串，当它遇到参数的格式说明符(以`%`开头的字符序列)时，它将使用`va_arg`从堆栈中读取所需的类型。 应该清楚的是，C 样式的参数列表并不像乍一看那样灵活；而且，它们可能相当危险。\n\n# 功能特点\n\n函数是定义为应用一部分或库中的模块化代码片段。 如果一个函数是由另一个供应商编写的，那么重要的是您的代码以该供应商想要的方式调用该函数。 这意味着要了解使用的调用约定及其对堆栈的影响。\n\n# 调用堆栈\n\n调用函数时，编译器将为新函数调用创建堆栈帧，并将项推入堆栈。 放到堆栈上的数据取决于您的编译器以及代码是为调试版本还是发布版本编译的；但是，通常会有关于传递给函数的参数、返回地址(函数调用后的地址)以及函数中分配的自动变量的信息。\n\n这意味着，当您在运行时调用函数时，在函数运行之前创建堆栈帧会产生内存开销和性能开销，而在函数完成后进行清理会产生性能开销。 如果函数是内联的，则不会出现这种开销，因为函数调用将使用当前堆栈帧而不是新堆栈帧。 显然，内联函数应该很小，无论是代码还是堆栈上使用的内存。 编译器可以忽略`inline`说明符，并使用单独的堆栈框架调用函数。\n\n# 指定调用约定\n\n当您的代码使用您自己的函数时，您不需要注意*调用约定*，因为编译器将确保使用适当的约定。 但是，如果您编写的库代码可以被其他 C++ 编译器使用，甚至可以被其他语言使用，那么调用约定就变得很重要。 由于本书不是关于可互操作的代码，因此我们不会深入讨论，而是将从两个方面进行探讨：函数命名和堆栈维护。\n\n# 使用 C 链接\n\n当您为 C++ 函数命名时，这是您将在 C++ 代码中用来调用该函数的名称。 然而，在幕后，C++ 编译器将*用额外的返回类型和参数符号修饰*名称，以便重载的函数都有不同的名称。 对于 C++ 开发人员来说，这也称为**名称损坏**。\n\n如果需要通过共享库(在 Windows 中为**动态链接库**)导出函数，则必须使用其他语言可以使用的类型和名称。 为此，可以用`extern \"C\"`标记函数。 这意味着该函数具有 C 链接，并且编译器不会使用 C++ 名称损坏。 显然，您应该只在将由外部代码使用的函数上使用它，而不应该将它与具有使用 C++ 自定义类型的返回值和参数的函数一起使用。 但是，如果这样的函数确实返回 C++ 类型，编译器将只发出警告。 原因是 C 是一种灵活的语言，C 程序员将能够解决如何将 C++ 类型转换为有用的东西，但这样滥用它们是糟糕的做法！\n\nThe `extern \"C\"` linkage can also be used with global variables, and you can use it on a single item or (using braces) on many items.\n\n# 指定如何维护堆栈\n\nVisual C++ 支持可在函数上使用的六种调用约定。 `__clrcall`说明符表示函数应作为.NET 函数调用，并允许您编写混合了本机代码和托管代码的代码。 C++/CLR(Microsoft 对 C++ 的语言扩展以编写.NET 代码)超出了本书的范围。 其他五个用于指示如何将参数传递给函数(在堆栈上或使用 CPU 寄存器)，以及谁负责维护堆栈。 我们只介绍三个：`__cdecl`、`__stdcall`和`__thiscall`。\n\n您很少显式使用`__thiscall`；它是用于定义为自定义类型成员的函数的调用约定，并指示该函数有一个隐藏参数，该参数是指向可通过函数中的`this`关键字访问的对象的指针。 下一章将给出更多细节，但重要的是要认识到，此类成员函数具有不同的调用约定，特别是当您需要初始化函数指针时。\n\n默认情况下，C++ 全局函数将使用`__cdecl`调用约定。 堆栈由调用代码维护，因此在调用代码中，每个对`__cdecl`函数的调用后面都跟有清理堆栈的代码。 这会使每个函数调用稍大一些，但使用变量参数列表时需要这样做。 大多数 Windows SDK 函数都使用`__stdcall`调用约定，它表明被调用的函数清理了堆栈，因此不需要在调用代码中生成这样的代码。 显然，编译器知道函数使用`__stdcall`是很重要的，因为否则，它将生成代码来清理已经被函数清理的堆栈框架。 您通常会看到 Windows 函数标有`WINAPI,`，这是`__stdcall`的`typedef`。\n\n# 使用递归\n\n在大多数情况下，调用堆栈的内存开销并不重要。 但是，当您使用递归时，可能会构建一长串堆栈帧。 顾名思义，递归是指函数调用自身。 一个简单的例子是计算阶乘的函数：\n\n```cpp\n    int factorial(int n) \n    { \n        if (n > 1) return n ∗ factorial(n − 1); \n        return 1; \n    }\n```\n\n如果您为 4 调用此功能，则会进行以下调用：\n\n```cpp\n    factorial(4) returns 4 * factorial(3) \n        factorial(3) returns 3 * factorial(2) \n            factorial(2) returns 2 * factorial(1) \n                factorial(1) returns 1\n```\n\n重要的一点是，在递归函数中，必须至少有一种方法使函数不进行递归。 在这种情况下，它将是使用参数 1 调用`factorial`时。在实践中，这样的函数应该标记为`inline`，以避免创建任何堆栈帧。\n\n# 重载函数\n\n您可以有多个名称相同的函数，但参数列表不同(参数的数量和/或参数的类型)。 这是*重载*函数名。 调用此类函数时，编译器将尝试查找最符合所提供参数的函数。 如果没有合适的函数，编译器将尝试转换参数，以查看是否存在具有这些类型的函数。 编译器将从琐碎的转换开始(例如，将数组名称转换为指针，将类型转换为`const`类型)，如果转换失败，编译器将尝试将类型升级(例如，将`bool`升级为`int`)。 如果失败，编译器将尝试标准转换(例如，对类型的引用)。 如果这样的转换产生多个可能的候选对象，则编译器将发出函数调用不明确的错误。\n\n# 职能和范围\n\n在查找合适的函数时，编译器还会考虑函数的作用域。 您不能在函数中定义函数，但可以在函数的作用域内提供函数原型，编译器将尝试(如有必要，通过转换)首先调用具有此类原型的函数。 请考虑以下代码：\n\n```cpp\n    void f(int i)    { /*does something*/ } \n    void f(double d) { /*does something*/ } \n\n    int main() \n    { \n        void f(double d); \n        f(1); \n        return 0; \n    }\n```\n\n在此代码中，函数`f`使用一个版本重载，该版本采用`int`，另一个版本采用`double`。 通常，如果调用`f(1)`，则编译器将调用函数的第一个版本。 然而，在`main`中有一个采用`double`的版本的原型，并且可以在不丢失信息的情况下将`int`转换为`double`。 原型的作用域与函数调用的作用域相同，因此在此代码中，编译器将调用接受`double`的版本。 该技术实质上用`int`参数隐藏了版本。\n\n# 已删除的功能\n\n有一种比使用作用域更正式的方法来隐藏函数。 C++ 将尝试显式转换内置类型。 例如：\n\n```cpp\n    void f(int i);\n```\n\n您可以使用`int`或任何可以转换为`int`的值来调用它：\n\n```cpp\n    f(1); \n    f('c'); \n    f(1.0); // warning of conversion\n```\n\n在第二种情况下，a`char`是一个整数，因此它被提升为`int`并调用该函数。 在第三种情况下，编译器将发出转换可能导致数据丢失的警告，但这是一个警告，因此代码将进行编译。 如果你想阻止这种隐式转换，你可以*删除你不想让调用者使用的函数*。 为此，请提供一个原型并使用语法`= delete`：\n\n```cpp\n    void f(double) = delete; \n\n    void g() \n    { \n        f(1);   // compiles \n        f(1.0); // C2280: attempting to reference a deleted function \n    }\n```\n\n现在，当代码尝试使用`char`或`double`(或将隐式转换为`double`的`float`)调用函数时，编译器将发出错误。\n\n# 按值传递和按引用传递\n\n默认情况下，编译器将按值传递参数，即创建一个副本。 如果传递自定义类型，则会调用其*复制构造函数*来创建新对象。 如果将指针传递给内置类型或自定义类型的对象，则将通过值传递*指针*，即在函数堆栈上为参数创建一个新指针，并使用传递给函数的内存地址对其进行初始化。 这意味着，在函数中，您可以将指针更改为指向其他内存(如果要对该指针使用指针算法，这将非常有用)。 指针指向的数据将通过引用传递，也就是说，数据保留在函数外部的位置，但函数可以使用指针更改数据。 同样，如果在参数上使用引用，则意味着该对象由该引用传递。 显然，如果在指针或引用参数上使用`const`，则这将影响函数是否可以更改指向或引用的数据。\n\n在某些情况下，您可能希望从一个函数返回多个值，并且可以选择使用该函数的返回值来指示该函数是否正确执行。 为此，一种方法是将其中一个参数设置为*OUT*参数，也就是说，它要么是指向函数将更改的对象或容器的指针，要么是对该对象或容器的引用：\n\n```cpp\n    // don't allow any more than 100 items \n    bool get_items(int count, vector<int>& values) \n    { \n        if (count > 100) return false; \n        for (int i = 0; i < count; ++ i) \n        { \n            values.push_back(i); \n        } \n        return true; \n    }\n```\n\n要调用此函数，必须创建`vector`对象并将其传递给函数：\n\n```cpp\n    vector<int> items {}; \n    get_items(10, items); \n    for(int i : items) cout << i << ' '; \n    cout << endl\n```\n\n因为`values`参数是一个引用，所以这意味着当`get_values`调用`push_back`在`values`容器中插入一个值时，它实际上是在将该值插入到`items`容器中。\n\n如果 Out 参数是通过指针传递的，查看指针声明很重要。 单个`*`表示变量是指针，两个表示它是指向指针的指针。 以下函数通过 OUT 参数返回`int`：\n\n```cpp\n    bool get_datum(/*out*/ int *pi);\n```\n\n代码的名称如下所示：\n\n```cpp\n    int value = 0; \n    if (get_datum(&value)) { cout << \"value is \" << value << endl; } \n    else                   { cout << \"cannot get the value\" << endl;}\n```\n\n这种返回表示成功的值的模式经常使用，尤其是在跨进程或机器边界访问数据的代码中。 函数返回值可用于提供调用失败原因的详细信息(无法访问网络？、安全凭证无效？等)，并指示应丢弃 OUT 参数中的数据。\n\n如果 out 参数具有双精度`*`，则意味着返回值本身是指向单个值或数组的指针：\n\n```cpp\n    bool get_data(/*in/out*/ int *psize, /*out*/ int **pi);\n```\n\n在本例中，您使用第一个参数传入所需的缓冲区大小，并在返回时通过此参数(它是 In/Out)接收缓冲区的实际大小以及第二个参数中指向缓冲区的指针：\n\n```cpp\n    int size = 10; \n    int *buffer = nullptr; \n    if (get_data(&size, &buffer)) \n    { \n        for (int i = 0; i < size; ++ i) \n        { \n            cout << buffer[i] << endl; \n        } \n        delete [] buffer; \n    }\n```\n\n任何返回内存缓冲区的函数都必须记录谁负责释放内存。 在大多数情况下，通常是调用方，如本示例代码所假定的那样。\n\n# 设计功能\n\n通常，函数将作用于全局数据或调用方传入的数据。 重要的是，当函数完成时，它会使此数据保持一致状态。 同样重要的是，函数可以在访问数据之前对其进行假设。\n\n# 前置条件和后置条件\n\n函数通常会更改某些数据：传递给函数的值、函数返回的数据或某些全局数据。 在设计函数时，确定要访问和更改的数据，并记录这些规则，这一点很重要。\n\n函数将对它将使用的数据有前提条件和假设。 例如，如果向某个函数传递了一个文件名，目的是让该函数从该文件中提取一些数据，那么谁负责检查该文件是否存在呢？ 您可以让它由函数负责，因此前几行将检查名称是否为文件的有效路径，并调用操作系统函数来检查该文件是否存在。 但是，如果您有几个函数将对文件执行操作，那么您将在每个函数中复制此检查代码，并且最好将该责任放在调用代码上。 显然，这样的操作可能代价很高，因此避免调用代码和函数来执行检查非常重要。\n\n[第 10 章](10.html)，*诊断和调试*将介绍如何添加调试代码(称为**Asserts**)，您可以将这些代码放在函数中以检查参数值，以确保调用代码遵循您设置的前提规则。 断言是使用条件编译定义的，因此只会出现在**调试版本**中(即，使用调试信息编译的 C++ 代码)。 **发布版本**(将交付给最终用户的已完成代码)将有条件地编译断言；这会使代码更快，如果您的测试足够彻底，则可以确保满足前提条件。\n\n您还应该记录您的函数的后置条件。 也就是说，关于函数返回的数据的假设(通过函数返回值、输出参数或引用传递的参数)。 后置条件是调用代码将做出的假设。 例如，您可以返回带符号整数，其中函数返回正值，但负值用于指示错误。 如果函数失败，返回指针的函数通常会返回`nullptr`。 在这两种情况下，调用代码都知道它需要检查返回值，并且仅在返回值为正或不为`nullptr`时才使用它。\n\n# 使用不变量\n\n您应该小心记录函数如何使用函数外部的数据。 如果该函数的目的是更改外部数据，则应记录该函数将执行的操作。 如果您没有显式地记录函数对外部数据做了什么，那么您必须确保函数完成这些数据时保持不变。 原因是调用代码只假定您在文档中所说的内容，更改全局数据的副作用可能会导致问题。 有时需要存储全局数据的状态，并在函数返回之前将项返回到该状态。\n\n我们已经在[第 3 章](03.html)，*探索 C++ 类型*中看到了一个使用`cout`对象的例子。 `cout`对象对于您的应用是全局的，可以通过操纵器对其进行更改，使其以特定方式解释数字值。 如果在函数中对其进行更改(例如，通过插入`hex`操纵器)，则在函数外部使用`cout`对象时，此更改将保持不变。\n\n[第 3 章](03.html)，*探索 C++ 类型*展示了如何解决这个问题。 在本章中，您创建了一个名为`read16`的函数，该函数从文件中读取 16 个字节，并将值以十六进制形式打印到控制台，并将其解释为 ASCII 字符：\n\n```cpp\n    int read16(ifstream& stm) \n    { \n        if (stm.eof()) return -1;  \n\n        int flags = cout.flags(); \n        cout << hex; \n        string line; \n\n        // code that changes the line variable \n\n        cout.setf(flags); \n        return line.length(); \n    }\n```\n\n此代码将`cout`对象的状态存储在临时变量`flags`中。 `read16`函数可以以任何必要的方式更改`cout`对象，但因为我们有存储状态，这意味着对象可以在返回之前恢复到其原始状态。\n\n# 函数指针\n\n当应用运行时，它将调用的函数将存在于内存中的某个位置。 这意味着您可以获得函数的地址。 C++ 允许您使用函数调用运算符(包含参数`()`的一对圆括号)通过函数指针调用函数。\n\n# 记住括号！\n\n首先，我们来看一个简单的例子，说明函数指针如何导致很难注意到代码中的错误。 一个名为`get_status`的全局函数执行各种验证操作，以确定系统状态是否有效。 此函数返回零值，表示系统状态有效，大于零的值为错误代码：\n\n```cpp\n    // values over zero are error codes \n    int get_status() \n    { \n        int status = 0;  \n        // code that checks the state of data is valid \n        return status; \n    }\n```\n\n代码可以这样调用：\n\n```cpp\n    if (get_status > 0) \n    { \n        cout << \"system state is invalid\" << endl; \n    }\n```\n\n这是一个错误，因为开发人员错过了`()`，因此编译器不会将其视为函数调用。 相反，它将此视为对函数内存地址的测试，由于函数永远不会位于零的内存地址，因此比较将始终为`true`，即使系统状态有效，也会打印消息。\n\n# 声明函数指针\n\n最后一节重点介绍了获取函数地址是多么容易：只需使用不带括号的函数名即可：\n\n```cpp\n    void *pv = get_status;\n```\n\n指针`pv`只是个小问题；您现在知道了函数在内存中的存储位置，但是要打印这个地址，您仍然需要将其转换为整数。 要使指针有用，您需要能够声明一个指针，通过该指针可以调用函数。 要了解如何做到这一点，让我们回到函数原型：\n\n```cpp\n    int get_status()\n```\n\n函数指针必须能够调用函数，不传递任何参数，并且期望返回值为整数。 函数指针声明如下：\n\n```cpp\n    int (*fn)() = get_status;\n```\n\n`*`表示变量`fn`是一个指针；但是，它绑定到左边，因此如果没有`*fn`周围的圆括号，编译器会将其解释为该声明是针对`int*`指针的。 声明的其余部分指示如何调用此函数指针：不带参数并返回`int`。\n\n通过函数指针调用很简单：您可以在通常给出函数名称的位置给出指针的名称：\n\n```cpp\n    int error_value = fn();\n```\n\n再次注意圆括号有多重要；它们指示函数指针`fn`中保存的地址处的函数被调用。\n\n函数指针可能会使代码看起来相当混乱，特别是当您使用它们指向模板化函数时，因此代码通常会定义别名：\n\n```cpp\n    using pf1 = int(*)();\n    typedef int(*pf2)();\n```\n\n这两行声明调用`get_status`函数所需的函数指针类型的别名。 两者都是有效的，但`using`版本更具可读性，因为很明显`pf1`是正在定义的别名。 要了解原因，请考虑以下别名：\n\n```cpp\n    typedef bool(*MyPtr)(MyType*, MyType*);\n```\n\n类型别名称为`MyPtr`，它指向返回`bool`并接受两个`MyType`指针的函数。 在`using`中，这一点要清楚得多：\n\n```cpp\n    using MyPtr = bool(*)(MyType*, MyType*);\n```\n\n这里的指示符是`(*)`，它指示该类型是一个函数指针，因为您正在使用圆括号来断开`*`的关联性。 然后，您可以向外阅读以查看函数的原型：左侧查看返回类型，右侧查看参数列表。\n\n声明别名后，可以创建指向函数的指针并调用它：\n\n```cpp\n    using two_ints = void (*)(int, int); \n\n    void do_something(int l, int r){/* some code */} \n\n    void caller() \n    { \n        two_ints fn = do_something; \n        fn(42, 99); \n    }\n```\n\n请注意，因为`two_ints`别名被声明为指针，所以在声明此类型的变量时不使用`*`。\n\n# 使用函数指针\n\n函数指针只是一个指针。 这意味着您可以将其用作变量；您可以从函数返回它，也可以将其作为参数传递。 例如，您可能有一些代码执行一些冗长的例程，并且您希望在例程期间提供一些反馈。 为了灵活起见，您可以将函数定义为采用**回调指针**，并在例程中定期调用该函数以指示进度：\n\n```cpp\n    using callback = void(*)(const string&); \n\n    void big_routine(int loop_count, const callback progress) \n    { \n        for (int i = 0; i < loop_count; ++ i) \n        { \n            if (i % 100 == 0) \n            { \n                string msg(\"loop \"); \n                 msg += to_string(i); \n                 progress(msg); \n            } \n            // routine \n        } \n    }\n```\n\n这里`big_routine`有一个名为`progress`的函数指针参数。 该函数有一个循环，该循环将被调用多次，并且每 100 次循环调用回调函数，传递一个`string`，该`string`提供有关进度的信息。\n\nNote that the `string` class defines a `+=` operator that can be used to append a string to the end of the `string` in the variable and the `<string>` header file defines a function called `to_string` that is overloaded for each of the built-in types to return a `string` formatted with the value of the function parameter.\n\n此函数将函数指针声明为`const`，只是为了让编译器知道函数指针不应更改为指向此函数中另一个函数的指针。 代码可以这样调用：\n\n```cpp\n    void monitor(const string& msg) \n    { \n        cout << msg << endl; \n    } \n\n    int main() \n    { \n        big_routine(1000, monitor); \n        return 0; \n    }\n```\n\n`monitor`函数具有与`callback`函数指针所描述的相同的原型(例如，如果函数参数是`string&`而不是`const string&,`，则代码将不会编译)。 然后调用`big_routine`函数，将指向`monitor`函数的指针作为第二个参数传递。\n\n如果将回调函数传递给库代码，则必须注意函数指针的调用约定。 例如，如果将函数指针传递给 Windows 函数(如`EnumWindows`)，则它必须指向使用`__stdcall`调用约定声明的函数。\n\nC++ 标准使用另一种技术来调用在运行时定义的函数，即函数器。 我们很快就会讲到这一点。\n\n# 模板化函数\n\n在编写库代码时，您通常需要编写几个函数，这些函数只在传递给函数的类型之间有所不同；例程操作是相同的，只是类型发生了变化。 C++ 提供了*模板*以允许您编写更泛型的代码；您使用*泛型类型*编写例程，编译器将在编译时生成具有适当类型的函数。 模板化函数使用`template`关键字和尖括号(`<>`)中的参数列表进行标记，这些参数为将要使用的类型提供占位符。 重要的是要理解这些模板参数是类型，并且引用参数的类型(并返回函数的值)，这些参数的类型将被调用函数使用的实际类型替换。 它们不是函数的参数，您(通常)在调用函数时不会提供它们。\n\n最好用示例来解释模板函数。 可以这样编写一个简单的`maximum`函数：\n\n```cpp\n    int maximum(int lhs, int rhs) \n    { \n        return (lhs > rhs) ? lhs : rhs; \n    }\n```\n\n您可以使用其他整数类型调用此函数，较小类型(`short`、`char`、`bool`等)将提升为`int`，较大类型(`long long`)的值将被截断。 同样，`unsigned`类型的变量将转换为可能导致问题的`signed int`类型。 请考虑下面的函数调用：\n\n```cpp\n    unsigned int s1 = 0xffffffff, s2 = 0x7fffffff; \n    unsigned int result = maximum(s1, s2);\n```\n\n`result`变量的值是什么：`s1`或`s2`？ 它是`s2`。 原因是这两个值都转换为`signed int`，当转换为有符号类型时，`s1`将是值`-1`，`s2`将是值`2147483647`。\n\n要处理无符号类型，需要*重载*函数，并为有符号整数和无符号整数编写一个版本：\n\n```cpp\n    int maximum(int lhs, int rhs) \n    { \n        return (lhs > rhs) ? lhs : rhs; \n    } \n\n    unsigned maximum(unsigned lhs, unsigned rhs) \n    { \n        return (lhs > rhs) ? lhs : rhs; \n    }\n```\n\n套路是一样的，但类型不同了。 还有另一个问题--如果调用者混合了类型怎么办？ 以下表达式是否有意义：\n\n```cpp\n    int i = maximum(true, 100.99);\n```\n\n此代码将进行编译，因为可以将`bool`和`double`转换为`int`，并调用第一个重载。 由于这样的调用是无稽之谈，如果编译器捕捉到这个错误就更好了。\n\n# 定义模板\n\n返回到`maximum`函数的两个版本，这两个版本的例程是相同的；唯一改变的是类型。 如果您有一个泛型类型，让我们将其命名为`T`，其中`T`可以是实现`operator>`的任何类型，该例程可以用下面的伪代码来描述：\n\n```cpp\n    T maximum(T lhs, T rhs) \n    { \n        return (lhs > rhs) ? lhs : rhs; \n    }\n```\n\n这将不会编译，因为我们没有定义类型`T`。 模板允许您告诉编译器代码使用类型，并将由传递给函数的参数确定。 将编译以下代码：\n\n```cpp\n    template<typename T> \n    T maximum(T lhs, T rhs) \n    { \n        return (lhs > rhs) ? lhs : rhs; \n    }\n```\n\n模板声明使用`typename`标识符指定将使用的类型。 类型`T`是一个占位符；您可以使用您喜欢的任何名称，只要它不是在相同作用域的其他地方使用的名称，当然，它必须在函数的参数列表中使用。 你可以用`class`代替`typename`，但意思是一样的。\n\n您可以调用此函数，传递任何类型的值，编译器将为该类型创建代码，为该类型调用`operator>`。\n\nIt is important to realize that, the first time the compiler comes across a templated function, it will create a version of the function for the specified type. If you call the templated function for several different types, the compiler will create, or instantiate, a *specialized* function for each of these types.\n\n该模板的定义说明只会使用一种类型，所以只能使用两个相同类型的参数进行调用：\n\n```cpp\n    int i = maximum(1, 100);\n    double d = maximum(1.0, 100.0);\n    bool b = maximum(true, false);\n```\n\n所有这些都将编译，前两个将给出预期的结果。 最后一行将把`b`赋给值`true`，因为`bool`是一个整数，`true`的值是`1+`，`false`的值是`0`。 这可能不是您想要的，所以我们稍后再来讨论这个问题。 请注意，由于模板说明两个参数必须是同一类型，因此不会编译以下代码：\n\n```cpp\n    int i = maximum(true, 100.99);\n```\n\n原因是`template`参数列表只给出了一个类型。 如果要使用不同类型的参数定义函数，则必须向模板提供额外的参数：\n\n```cpp\n    template<typename T, typename U> \n    T maximum(T lhs, U rhs) \n    { \n        return (lhs > rhs) ? lhs : rhs; \n    }\n```\n\nThis is done to illustrate how templates work; it really does not make sense to define a maximum function that takes two different types.\n\n这个版本是为两种不同的类型编写的，模板声明提到了两种类型，这两种类型用于两个参数。 但请注意，该函数返回`T`，即第一个参数的类型。 该函数可以按如下方式调用：\n\n```cpp\n    cout << maximum(false, 100.99) << endl; // 1 \n    cout << maximum(100.99, false) << endl; // 100.99\n```\n\n第一行的输出为`1`(如果使用`bool alpha`操纵器，则为`true`)，第二行的结果为`100.99`。 原因并不是立竿见影的。 在这两种情况下，比较都将从函数返回`100.99`，但是因为返回值的类型是`T`，所以返回值类型将是第一个参数的类型。 在第一种情况下，首先将`100.99`转换为`bool`，由于`100.99`不是零，因此返回的值是`true`(或`1`)。 在第二种情况下，第一个参数是`double`，因此函数返回`double`，这意味着返回`100.99`。 如果将`maximum`的模板版本更改为返回`U`(第二个参数的类型)，则前面代码返回的值将反转：第一行返回`100.99`，第二行返回`1`。\n\n请注意，当您*调用*模板函数时，您不必给出模板参数的类型，因为编译器会推导出它们。 需要指出的是，这只适用于参数。 返回类型不是由调用方分配给函数值的变量类型决定的，因为可以在不使用返回值的情况下调用函数。\n\n尽管编译器会根据您调用函数的方式推断模板参数，但您可以显式提供被调用函数中的类型来调用函数的特定版本，并(如果需要)让编译器执行隐式转换：\n\n```cpp\n    // call template<typename T> maximum(T,T); \n    int i = maximum<int>(false, 100.99);\n```\n\n此代码将调用具有两个`int`参数的`maximum`版本，并返回`int`，因此返回值为`100`，即`100.99`转换为`int`。\n\n# 使用模板参数值\n\n到目前为止定义的模板都以类型作为模板的参数，但是您也可以提供整数值。 以下是一个相当做作的例子来说明这一点：\n\n```cpp\n    template<int size, typename T> \n    T* init(T t) \n    { \n        T* arr = new T[size]; \n        for (int i = 0; i < size; ++ i) arr[i] = t; \n        return arr; \n    }\n```\n\n有两个模板参数。 第二个参数提供类型的名称，其中`T`是用于函数参数类型的占位符。 第一个参数看起来像函数参数，因为它的用法类似。 参数`size`可以在函数中作为局部(只读)变量使用。 函数参数是`T`，因此编译器可以从函数调用中推导出第二个模板参数，但不能推导出第一个参数，因此您*必须*在调用中提供一个值。 以下是为`T`的`int`和`size`的值`10`调用此模板函数的示例：\n\n```cpp\n    int *i10 = init<10>(42); \n    for (int i = 0; i < 10; ++ i) cout << i10[i] << ' '; \n    cout << endl; \n    delete [] i10;\n```\n\n第一行使用`10`作为模板参数和`42`作为函数参数调用函数。 因为`42`是一个`int`，所以`init`函数将创建一个有 10 个成员的`int`数组，并且每个成员都被初始化为值`42`。 编译器推导出`int`作为第二个参数，但是这段代码可以用`init<10,int>(42)`调用函数来显式指示您需要一个`int`数组。\n\n非类型参数在编译时必须是常量：该值可以是整数(包括枚举)，但不能是浮点数。 您可以使用整数数组，但可以通过 Template 参数将其用作指针。\n\n虽然在大多数情况下，编译器无法推导出 Value 参数，但如果将该值定义为数组的大小，则可以推导出 Value 参数。 这可以用来使函数看起来可以确定内置数组的大小，但当然不能，因为编译器将为每个所需的大小创建函数的一个版本。 例如：\n\n```cpp\n    template<typename T, int N> void print_array(T (&arr)[N]) \n    { \n        for (int i = 0; i < N; ++ i) \n        { \n            cout << arr[i] << endl; \n        } \n    }\n```\n\n这里有两个模板参数：一个是数组的类型，另一个是数组的大小。 该函数的参数看起来有点奇怪，但它只是一个由引用传递的内置数组。 如果不使用圆括号，则参数为`T& arr[N]`，即对类型为`T`的对象的 N 个大小的内置引用数组，这不是我们想要的。 我们需要一个类型为`T`的 N 大小的内置数组对象。 此函数的调用方式如下：\n\n```cpp\n    int squares[] = { 1, 4, 9, 16, 25 }; \n    print_array(squares);\n```\n\n前面代码的有趣之处在于，编译器看到初始化式列表中有五个项目。 内置数组有五个项目，因此调用如下函数：\n\n```cpp\n    print_array<int,5>(squares);\n```\n\n如前所述，编译器将为代码调用的每个`T`和`N`组合实例化此函数。 如果模板函数有大量代码，那么这可能是个问题。 解决此问题的一种方法是使用帮助器函数：\n\n```cpp\n    template<typename T> void print_array(T* arr, int size) \n    { \n        for (int i = 0; i < size; ++ i) \n        { \n            cout << arr[i] << endl; \n        } \n    } \n\n    template<typename T, int N> inline void print_array(T (&arr)[N]) \n    { \n        print_array(arr, N); \n    }\n```\n\n这做了两件事。 首先，有一个版本的`print_array`，它接受一个指针和指针所指向的项数。 这意味着`size`参数是在运行时确定的，因此该函数的版本只在编译时针对所使用的数组类型实例化，而不是同时针对类型和数组大小实例化。 第二件要注意的事情是，使用数组大小模板化的函数被声明为`inline`，并且它调用函数的第一个版本。 尽管对于每种类型和数组大小的组合都有相应的版本，但实例化将是内联的，而不是完整的函数。\n\n# 专用模板\n\n在某些情况下，您可能有一个适用于大多数类型的例程(也是模板化函数的候选者)，但是您可能会发现某些类型需要不同的例程。 要处理此问题，可以编写专门的模板函数，即将用于特定类型的函数，当调用方使用符合此专门化的类型时，编译器将使用此代码。 作为示例，下面是一个相当无意义的函数；它返回一个类型的大小：\n\n```cpp\n    template <typename T> int number_of_bytes(T t) \n    { \n        return sizeof(T); \n    }\n```\n\n这适用于大多数内置类型，但如果您使用指针调用它，您将获得指针的大小，而不是指针所指向的大小。 因此，对于`char`数组的大小，`number_of_bytes(\"x\")`将返回 4(在 32 位系统上)，而不是 2。 您可能决定对使用 C 函数`strlen`的`char*`指针进行专门化，以计算字符串中直到`NUL`字符的字符数。 要做到这一点，您需要一个类似于模板化函数的原型，用实际类型替换模板参数，因为模板参数不是必需的，所以您忽略了这一点。 由于此函数是针对特定类型的，因此需要在函数名中添加专用类型：\n\n```cpp\n    template<> int number_of_bytes<const char *>(const char *str) \n    { \n        return strlen(str) + 1; \n    }\n```\n\n现在，当您调用`number_of_bytes(\"x\")`时，将调用专门化，它将返回值 2。\n\n前面，我们定义了一个模板化函数来返回最多两个相同类型的参数：\n\n```cpp\n    template<typename T> \n    T maximum(T lhs, T rhs) \n    { \n        return (lhs > rhs) ? lhs : rhs; \n    }\n```\n\n使用专门化，可以为未使用`>`运算符进行比较的类型编写版本。 由于查找最多两个布尔值没有任何意义，因此可以删除`bool`的专门化：\n\n```cpp\n    template<> bool maximum<bool>(bool lhs, bool rhs) = delete;\n```\n\n这现在意味着，如果代码使用`bool`参数调用`maximum`，编译器将生成错误。\n\n# 可变模板\n\n可变模板是指存在可变数量的模板参数。 语法类似于函数的变量参数；您使用省略号，但在参数列表中参数的左侧使用它们，参数列表将其声明为*参数包*：\n\n```cpp\n    template<typename T, typename... Arguments>  \n    void func(T t, Arguments... args);\n```\n\n`Arguments`模板参数是零个或多个类型，这些类型是函数的相应数目的参数`args`的类型。 在本例中，该函数至少有一个类型为`T`的参数，但是您可以有任意数量的固定参数，包括一个都没有。\n\n在函数中，需要解压参数包才能访问调用方传递的参数。 您可以使用特殊运算符`sizeof...`确定参数包中有多少项(请注意，省略号是名称的一部分)；与`sizeof`运算符不同，这是项计数，而不是以字节为单位的大小。 要解压参数包，需要使用参数包名称右侧的省略号(例如，`args...`)。 此时，编译器将展开参数包，用参数包的内容替换符号。\n\n但是，您不会在设计时知道有多少个参数或它们是什么类型，因此有一些策略可以解决这个问题。 第一种使用递归：\n\n```cpp\n    template<typename T> void print(T t) \n    { \n        cout << t << endl; \n    } \n\n    template<typename T, typename... Arguments>  \n    void print(T first, Arguments ... next) \n    { \n        print(first); \n        print(next...); \n    }\n```\n\n可变模板化`print`函数可以用`ostream`类可以处理的任何类型的一个或多个参数调用：\n\n```cpp\n    print(1, 2.0, \"hello\", bool);\n```\n\n调用此函数时，参数列表被分成两部分：第一个参数`first,`中的第一个参数(`1`)和其他三个参数放入参数包`next`中。 然后，函数体调用`print`的第一个版本，该版本将`first`参数输出到控制台。 然后，变量函数中的下一行在对`print`的调用中展开参数包，也就是说，它递归地调用自身。 在此调用中，`first`参数将为`2.0`，其余参数将放入参数包中。 这将继续进行，直到参数包扩展到不再有更多参数为止。\n\n另一种解包参数包的方法是使用初始值设定项列表。 在这种情况下，编译器将创建一个包含每个参数的数组：\n\n```cpp\n    template<typename... Arguments>  \n    void print(Arguments ... args) \n    { \n        int arr [sizeof...(args)] = { args... }; \n        for (auto i : arr) cout << i << endl; \n    }\n```\n\n数组`arr,`是用参数包的大小创建的，与初始值设定项大括号一起使用的 Unpack 语法将用参数填充数组。 尽管这适用于任意数量的参数，但所有参数都必须是相同类型的数组`arr`。\n\n其中一个技巧是使用逗号运算符：\n\n```cpp\n    template<typename... Arguments>  \n    void print(Arguments ... args) \n    { \n        int dummy[sizeof...(args)] = { (print(args), 0)... }; \n    }\n```\n\n这将创建一个名为`dummy`的虚拟数组。 除了在参数包的扩展中以外，不使用该数组。 该数组以`args`参数包的大小创建，省略号使用括号内的*表达式*展开参数包。 表达式使用逗号运算符，它将返回逗号的右侧。 由于这是一个整数，这意味着`dummy`的每个条目都有零值。 有趣的部分是逗号操作符的左侧。 这里，使用`args`参数包中的每一项调用带有单个模板化参数的`print`版本。\n\n# 重载运算符\n\n前面我们说过函数名不应该包含标点符号。 严格来说并非如此，因为如果要编写运算符，则*仅*在函数名中使用标点符号。 运算符在作用于一个或多个操作数的表达式中使用。 一元运算符有一个操作数，二元运算符有两个操作数，运算符返回运算结果。 显然，这描述了一个函数：返回类型、名称和一个或多个参数。\n\nC++ 提供关键字`operator`来指示函数没有与函数调用语法一起使用，而是使用与运算符相关的语法来调用(通常，一元运算符，第一个参数在运算符的右侧，而对于二元运算符，第一个参数在左边，第二个参数在右边，但也有例外)。\n\n通常，您将提供运算符作为自定义类型的一部分(因此运算符作用于该类型的变量)，但在某些情况下，您可以在全局范围内声明运算符。 两者都是有效的。 如果您正在编写自定义类型(类，如下一章所述)，则将运算符的代码封装为自定义类型的一部分是有意义的。 在本节中，我们将重点介绍定义运算符的另一种方式：将其定义为全局函数。\n\n您可以提供以下一元运算符的您自己的版本：\n\n```cpp\n    ! & + - * ++ -- ~\n```\n\n您还可以提供以下二元运算符的您自己的版本：\n\n```cpp\n    != == < <= > >= && ||\n    % %= + += - -= * *= / /= & &= | |= ^ ^= << <<= = >> =>>\n    -> ->* ,\n```\n\n您还可以编写函数调用运算符`()`、数组下标`[]`、转换运算符、强制转换运算符`(),`、`new`和`delete`的版本。 不能重新定义`.`、`.*`、`::`、`?:`、`#`或`##`运算符，也不能重新定义“命名”运算符`sizeof`、`alignof`或`typeid`。\n\n定义运算符时，编写一个函数，其中函数名为`operator*x*`，并且`*x*`是运算符符号(请注意，没有空格)。 例如，如果定义的`struct`具有定义笛卡尔点的两个成员，则可能需要比较两个点是否相等。 可以这样定义`struct`：\n\n```cpp\n    struct point \n    { \n        int x; \n        int y; \n    };\n```\n\n比较两个`point`对象很容易。 如果一个对象的`x`和`y`等于另一个对象中的相应值，则它们相同。 如果定义`==`运算符，则还应该使用相同的逻辑定义`!=`运算符，因为`!=`应该给出与`==`运算符完全相反的结果。 以下是这些运算符的定义方式：\n\n```cpp\n    bool operator==(const point& lhs, const point& rhs) \n    { \n        return (lhs.x == rhs.x) && (lhs.y == rhs.y); \n    } \n\n    bool operator!=(const point& lhs, const point& rhs) \n    { \n        return !(lhs == rhs); \n    }\n```\n\n这两个参数是运算符的两个操作数。 第一个参数是运算符左侧的操作数，第二个参数是运算符右侧的操作数。 这些被作为引用传递，这样就不会复制，并且它们被标记为`const`，因为操作符不会改变对象。 定义后，您可以使用`point`类型，如下所示：\n\n```cpp\n    point p1{ 1,1 }; \n    point p2{ 1,1 }; \n    cout << boolalpha; \n    cout << (p1 == p2) << endl; // true \n    cout << (p1 != p2) << endl; // false\n```\n\n您可以定义一对名为`equals`和`not_equals`的函数，并改为使用以下函数：\n\n```cpp\n    cout << equals(p1,p2) << endl;     // true \n    cout << not_equals(p1,p2) << endl; // false\n```\n\n但是，定义运算符可以提高代码的可读性，因为您可以像使用内置类型一样使用该类型。 运算符重载通常被称为*语法糖*，这是一种使代码更容易阅读的语法--但这使一项重要的技术变得微不足道。 例如，智能指针是一种涉及类**析构函数**来管理资源生存期的技术，它之所以有用，是因为您可以像调用指针一样调用这些类的对象。 之所以可以这样做，是因为智能指针类实现了`->`和`*`运算符。 另一个例子是**Functors**或 Function Objects，其中类实现了`()`运算符，因此可以像访问函数一样访问对象。\n\n在编写自定义类型时，您应该问问自己，重载类型的运算符是否有意义。 如果类型是数值类型，例如复数或矩阵，那么实现算术运算符是有意义的，但是由于类型没有逻辑方面，实现逻辑运算符有意义吗？ 很容易重新定义运算符的*表示*，以涵盖您的特定操作，但这会降低代码的可读性。\n\n通常，一元运算符被实现为接受单个参数的全局函数。 后缀递增和递减运算符是一个例外，允许实现与前缀运算符不同的实现。 前缀操作符将引用对象作为参数(操作符将递增或递减)，并返回对更改后的对象的引用。 但是，后缀运算符必须在递增或递减之前返回对象的值。 因此，操作符函数有两个参数：一个是对要更改的对象的引用，另一个是整数(值始终为 1)；它将返回原始对象的副本。\n\n二元运算符将有两个参数，并返回对象或对对象的引用。 例如，对于我们前面定义的`struct`，我们可以为`ostream`对象定义一个插入运算符：\n\n```cpp\n    struct point \n    { \n        int x; \n        int y; \n    }; \n\n    ostream& operator<<(ostream& os, const point& pt) \n    { \n        os << \"(\" << pt.x << \",\" << pt.y << \")\"; \n        return os; \n    }\n```\n\n这意味着您现在可以将`point`对象插入到`cout`对象，以便在控制台上打印它：\n\n```cpp\n    point pt{1, 1}; \n    cout << \"point object is \" << pt << endl;\n```\n\n# 函数对象\n\n函数对象或**函数**是实现函数调用运算符(`operator()`)的自定义类型。 这意味着可以以看起来像函数的方式调用函数运算符。 由于我们还没有讨论类，因此在本节中，我们将只探索标准库提供的函数对象类型以及如何使用它们。\n\n`<functional>`头文件包含可用作函数对象的各种类型。 下表列出了这些内容：\n\n| **目的** | **类型** |\n| 算术 / 计算 / 可用数字表示的某种情况 | `divides`，`minus`，`modulus`，`multiplies`，`negate`，`plus` |\n| 逐位，按位 | `bit_and`，`bit_not`，`bit_or`，`bit_xor` |\n| 比较 / 对照 / 类比 / 比喻 | `equal_to`，`greater`，`greater_equal`，`less`，`less_equals`，`not_equal_to` |\n| 逻辑的 / 符合逻辑的 / 自然而然的 / 逻辑学的 | `logical_and`，`logical_not`，`logical_or` |\n\n这些都是二元函数类，除了`bit_not`、`logical_not,`和`negate`之外，它们都是一元函数类。 二元函数对象作用于两个值并返回结果，一元函数对象作用于单个值并返回结果。 例如，您可以使用以下代码计算两个数字的模数：\n\n```cpp\n    modulus<int> fn; \n    cout << fn(10, 2) << endl;\n```\n\n这将声明一个名为`fn`的函数对象，该对象将执行模运算。 该对象在第二行中使用，它使用两个参数调用对象上的`operator()`函数，因此下面一行等同于前面的行：\n\n```cpp\n    cout << fn.operator()(10, 2) << endl;\n```\n\n结果是在控制台上打印出`0`的值。 函数`operator()`只对两个参数取模，在本例中为`10 % 2`。 这看起来并不太令人兴奋。 `<algorithm>`标头包含处理函数对象的函数。 大多数采用谓词(即逻辑函数对象)，但有一个(`transform`)采用执行操作的函数对象：\n\n```cpp\n    // #include <algorithm> \n    // #include <functional> \n\n    vector<int> v1 { 1, 2, 3, 4, 5 }; \n    vector<int> v2(v1.size()); \n    fill(v2.begin(), v2.end(), 2); \n    vector<int> result(v1.size()); \n\n    transform(v1.begin(), v1.end(), v2.begin(), \n        result.begin(), modulus<int>()); \n\n    for (int i : result) \n    { \n        cout << i << ' '; \n    } \n    cout << endl;\n```\n\n此代码将对两个向量中的值执行五个模数计算。 从概念上讲，它是这样做的：\n\n```cpp\n    result = v1 % v2;\n```\n\n也就是说，`result`中的每一项都是`v1`和`v2`中相应项的模数。 在代码中，第一行创建具有五个值的`vector`。 我们将使用`2`计算这些值的模数，因此第二行声明为空`vector`，但容量与第一行`vector`相同。 第二个`vector`通过调用`fill`函数来填充。 第一个参数是`vector`中第一个项目的地址，`end`函数返回`vector`中最后一个*项目之后的地址。 函数调用中的最后一项是从第一个参数指向的项开始直到(但不包括)第二个参数指向的项的每个项中将放入`vector`的值。*\n\n此时，第二个`vector`将包含五个项目，每个项目都是`2`。 接下来，为结果创建一个`vector`；同样，它的大小与第一个数组相同。 最后，计算由`transform`函数执行，如下所示：\n\n```cpp\n    transform(v1.begin(), v1.end(),  \n       v2.begin(), result.begin(), modulus<int>());\n```\n\n前两个参数给出了第一个`vector`的迭代器，由此可以计算出项数。 由于所有三个`vector`的大小相同，因此只需要`v2`和`result`的`begin`迭代器。\n\n最后一个参数是函数对象。 这是一个临时对象，仅在此语句期间存在；它没有名称。 这里使用的语法是对类的构造函数的显式调用；它是模板化的，因此需要给出模板参数。 `transform`函数将对此函数对象调用`operator(int,int)`函数，将`v1`中的每个项目作为第一个参数，`v2`中的相应项目作为第二个参数，并将结果存储在`result`中的相应位置。\n\n由于`transform`接受任何二元函数对象作为第二个参数，因此您可以传递`plus<int>`的实例以将值 2 加到`v1`中的每一项，或者传递`multiplies<int>`的实例以将`v1`中的每一项乘以 2。\n\n函数对象有用的一种情况是使用谓词执行多个比较。 谓词是比较值并返回布尔值的函数对象。 `<functional>`头包含几个类，允许您比较项目。 让我们看看`result`容器中有多少项是零。 为此，我们使用`count_if`函数。 这将遍历容器，将谓词应用于每一项，并计算谓词返回值`true`的次数。 有几种方法可以做到这一点。 第一个定义谓词函数：\n\n```cpp\n    bool equals_zero(int a) \n    { \n        return (a == 0); \n    }\n```\n\n然后可以将指向它的指针传递给`count_if`函数：\n\n```cpp\n    int zeros = count_if( \n       result.begin(), result.end(), equals_zero);\n```\n\n前两个参数指示要检查的值范围。 最后一个参数是指向用作谓词的函数的指针。 当然，如果要检查不同的值，可以使其更通用：\n\n```cpp\n    template<typename T, T value> \n    inline bool equals(T a) \n    { \n        return a == value; \n    }\n```\n\n这样称呼它：\n\n```cpp\n    int zeros = count_if( \n       result.begin(), result.end(), equals<int, 0>);\n```\n\n这段代码的问题在于，我们在其他地方定义操作，而不是在使用它的地方。 `equals`函数可以在另一个文件中定义；但是，使用谓词时，将执行检查的代码定义为靠近需要谓词的代码会更具可读性。\n\n`<functional>`头还定义了可用作函数对象的类。 例如，`equal_to<int>`，它比较两个值。 但是，`count_if`函数需要一个一元函数对象，它将向该对象传递单个值(请参阅前面描述的`equals_zero`函数)。 `equal_to<int>`是一个二元函数对象，比较两个值。 我们需要提供第二个操作数，为此，我们使用名为`bind2nd`的助手函数：\n\n```cpp\n    int zeros = count_if( \n       result.begin(), result.end(), bind2nd(equal_to<int>(), 0));\n```\n\n`bind2nd`将*将*参数`0`绑定到从`equal_to<int>`创建的函数对象。 使用这样的函数对象使谓词的定义更接近将使用它的函数调用，但是语法看起来相当混乱。 C++ 11 提供了一种机制，可以让编译器确定所需的函数对象，并将参数绑定到这些对象。 这些被称为 lambda 表达式。\n\n# 介绍 lambda 表达式\n\nLambda 表达式用于在将使用函数对象的位置创建匿名函数对象。 这将使您的代码更具可读性，因为您可以看到将要执行的内容。 乍一看，lambda 表达式看起来像是作为函数参数的就地函数定义：\n\n```cpp\n    auto less_than_10 = [](int a) {return a < 10; }; \n    bool b = less_than_10(4);\n```\n\n这样我们就不会像使用谓词的函数那样复杂，在这段代码中，我们为 lambda 表达式分配了一个变量。 这通常不是您使用它的方式，但它使描述更清晰。 Lambda 表达式开头的方括号称为**捕获列表**。 此表达式不捕获变量，因此括号为空。 您可以使用在 lambda 表达式外部声明的变量，这些变量必须被*捕获*。 捕获列表指示是通过引用(使用`[&]`)还是通过值(使用`[=]`)捕获所有此类变量。 您还可以命名要捕获的变量(如果有多个变量，请使用逗号分隔的列表)，如果它们是通过值捕获的，则只使用它们的名称。 如果他们是通过引用捕获的，则在其名称上使用`&`。\n\n通过引入在名为`limit`的表达式外部声明的变量，可以使前面的 lambda 表达式更加通用：\n\n```cpp\n    int limit = 99; \n    auto less_than = [limit](int a) {return a < limit; };\n```\n\n如果将 lambda 表达式与全局函数进行比较，捕获列表有点像标识全局函数可以访问的全局变量。\n\n在标题列表之后，您可以在括号中给出参数列表。 同样，如果将 lambda 与函数进行比较，则 lambda 参数列表等同于函数参数列表。 如果 lambda 表达式没有任何参数，则可以完全省略括号。\n\nLambda 的车身是用一对支架给出的。 它可以包含可以在函数中找到的任何内容。 Lambda 主体可以声明局部变量，甚至可以声明`static`个变量，这看起来很奇怪，但却是合法的：\n\n```cpp\n    auto incr = [] { static int i; return ++ i; }; \n    incr(); \n    incr(); \n    cout << incr() << endl; // 3\n```\n\nLambda 的返回值是从返回的项中推导出来的。 Lambda 表达式不必返回值，在这种情况下，表达式将返回`void`：\n\n```cpp\n    auto swap = [](int& a, int& b) { int x = a; a = b; b = x; }; \n    int i = 10, j = 20; \n    cout << i << \" \" << j << endl; \n    swap(i, j); \n    cout << i << \" \" << j << endl;\n```\n\nLambda 表达式的强大之处在于，您可以在需要函数对象或谓词的情况下使用它们：\n\n```cpp\n    vector<int> v { 1, 2, 3, 4, 5 }; \n    int less_than_3 = count_if( \n       v.begin(), v.end(),  \n       [](int a) { return a < 3; }); \n    cout << \"There are \" << less_than_3 << \" items less than 3\" << endl;\n```\n\n在这里，我们声明一个`vector`，并用一些值对其进行初始化。 `count_if`函数用于计算容器中有多少项小于 3。因此，前两个参数用于给出要检查的项的范围，第三个参数是执行比较的 lambda 表达式。 `count_if`函数将为通过 lambda 的`a`参数传入的范围内的每一项调用此表达式。 函数的作用是：记录 lambda 返回的次数`true`。\n\n# 在 C++ 中使用函数\n\n本章中的示例使用您在本章中学到的技术按文件大小顺序列出文件夹和子文件夹中的所有文件，并列出文件名及其大小。 该示例相当于在命令行中键入以下内容：\n\n```cpp\ndir /b /s /os /a-d folder\n```\n\n这里，`folder`是您列出的文件夹。 `/s`选项递归，`/a-d`从列表中删除文件夹，`/os`按大小排序。 问题是，如果没有`/b`选项，我们会获得有关每个文件夹的信息，但使用它会删除列表中的文件大小。 我们需要文件名(及其路径)的列表，它们的大小按最小的顺序排列在第一位。\n\n首先，在`Beginning_C++ `文件夹下为本章(`Chapter_05`)创建一个新文件夹。 在 Visual C++ 中，创建一个新的 C++ 源文件，并将其另存为这个新文件夹下的`files.cpp`。 该示例将使用基本输出和字符串。 它只接受一个命令行参数；如果传递了更多的命令行参数，我们只需使用第一个命令行参数。 在`files.cpp`中添加以下内容：\n\n```cpp\n    #include <iostream> \n    #include <string> \n    using namespace std; \n\n    int main(int argc, char* argv[]) \n    { \n        if (argc < 2) return 1; \n        return 0; \n    }\n```\n\n该示例将使用 Windows 函数`FindFirstFile`和`FindNextFile`来获取有关符合文件规范的文件的信息。 它们以`WIN32_FIND_DATAA`结构返回数据，其中包含有关文件名、文件大小和文件属性的信息。 这些函数还返回有关文件夹的信息，因此这意味着我们可以测试子文件夹和递归。 `WIN32_FIND_DATAA`结构以 64 位数字的形式给出了文件大小，分为两个部分：高 32 位和低 32 位。 我们将创建自己的结构来保存这些信息。 在文件顶部，在 C++ 包含文件之后添加以下内容：\n\n```cpp\n    using namespace std; \n\n    #include <windows.h> struct file_size { unsigned int high; unsigned int low; };\n```\n\n第一行是 Windows SDK 头文件，以便您可以访问 Windows 函数，该结构用于保存有关文件大小的信息。 我们想根据文件的大小来比较它们。 `WIN32_FIND_DATAA`结构在两个`unsigned long`成员中提供大小(一个具有高 4 个字节，另一个具有低 4 个字节)。 我们可以将其存储为 64 位数字，但是，为了有借口编写一些运算符，我们将大小存储在`file_size`结构中。 该示例将打印出文件大小并比较文件大小，因此我们将编写一个操作符来将`file_size`对象插入到输出流中；由于我们希望按大小对文件进行排序，因此需要一个操作符来确定一个`file_size`对象是否大于另一个。\n\n代码将使用 Windows 函数来获取有关文件的信息，特别是它们的名称和大小。 此信息将存储在`vector`中，因此在文件顶部添加以下两个突出显示的行：\n\n```cpp\n    #include <string> \n    #include <vector>\n #include <tuple>\n```\n\n需要`tuple`类，以便我们可以将`string`(文件名)和`file_size`对象存储为`vector`中的每一项。 要使代码更具可读性，请在结构定义后添加以下别名：\n\n```cpp\n    using file_info = tuple<string, file_size>;\n```\n\n然后，在`main`函数的正上方添加将在文件夹中获取文件的函数的框架代码：\n\n```cpp\n    void files_in_folder( \n       const char *folderPath, vector<file_info>& files) \n    { \n    }\n```\n\n此函数引用`vector`和文件夹路径。 代码将遍历指定文件夹中的每个项目。 如果它是一个文件，它将把详细信息存储在`vector`中；否则，如果该项是一个文件夹，它将调用自己来获取该子文件夹中的文件。 在`main`函数的底部添加对此函数的调用：\n\n```cpp\n    vector<file_info> files; \n    files_in_folder(argv[1], files);\n```\n\n代码已经检查到至少有一个命令行参数，我们将其用作要检查的文件夹。 `main`函数应该打印出文件信息，因此我们在堆栈上声明了一个`vector`，并通过引用将其传递给`files_in_folder`函数。 到目前为止，这段代码没有做任何事情，但是您可以编译代码以确保没有输入错误(请记住使用`/EHsc`参数)。\n\n大部分工作在`files_in_folder`函数中执行。 首先，将以下代码添加到此函数：\n\n```cpp\n    string folder(folderPath); \n    folder += \"*\"; \n    WIN32_FIND_DATAA findfiledata {}; \n    void* hFind = FindFirstFileA(folder.c_str(), &findfiledata); \n\n    if (hFind != INVALID_HANDLE_VALUE) \n    { \n       do \n       { \n       } while (FindNextFileA(hFind, &findfiledata)); \n       FindClose(hFind); \n    }\n```\n\n我们将使用 ASCII 版本的函数(因此结构和函数名称的后缀为`A`)。 函数`FindFirstFileA`接受搜索路径，在本例中，我们使用带有后缀`*`的文件夹的名称，这意味着*该文件夹中的所有内容*。 请注意，Windows 函数需要一个`const char*`参数，因此我们在`string`对象上使用`c_str`函数。 如果函数调用成功，并且找到了满足此条件的项，则函数将填充引用传递的`WIN32_FIND_DATAA`结构，并返回一个不透明的指针，该指针将用于对此搜索进行后续调用(您不需要知道它指向什么)。 代码检查调用是否成功，如果成功，则重复调用`FindNextFileA`以获取下一项，直到此函数返回 0，表示没有更多项。 不透明指针被传递给`FindNextFileA`，以便它知道正在检查哪个搜索。 搜索完成后，代码调用`FindClose`来释放 Windows 为搜索分配的任何资源。\n\n搜索将同时返回文件项和文件夹项；要以不同方式处理这两个项，我们可以测试`WIN32_FIND_DATAA`结构的`dwFileAttributes`成员。 在`do`循环中添加以下代码：\n\n```cpp\n    string findItem(folderPath); \n    findItem += \"\"; \n    findItem += findfiledata.cFileName; \n    if ((findfiledata.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0) \n    { \n        // this is a folder so recurse \n    } \n    else \n    { \n        // this is a file so store information \n    }\n```\n\n`WIN32_FIND_DATAA`结构只包含文件夹中项目的相对名称，因此前几行创建了一个绝对路径。 以下几行测试项目是文件夹(目录)还是文件。 如果项目是一个文件，那么我们只需将其添加到传递给函数的向量中。 在`else`子句中添加以下内容：\n\n```cpp\n    file_size fs{}; \n    fs.high = findfiledata.nFileSizeHigh; \n    fs.low = findfiledata.nFileSizeLow; \n    files.push_back(make_tuple(findItem, fs));\n```\n\n前三行使用大小数据初始化`file_size`结构，最后一行将带有文件名及其大小的`tuple`添加到`vector`。 为了查看对此函数的简单调用结果，请将以下内容添加到`main`函数的底部：\n\n```cpp\n    for (auto file : files) \n    { \n        cout << setw(16) << get<1>(file) << \" \"  \n            << get<0>(file) << endl; \n    }\n```\n\n这将遍历`files`向量中的项目。 每个项目都是一个`tuple<string, file_size>`对象，要获得`string`项目，可以使用标准库函数，`get,`使用 0 作为函数模板参数，并使用 1 作为函数模板参数调用`get`来获得`file_size`对象。 代码调用`setw`操纵器以确保文件大小始终打印在 16 个字符宽的列中。 要使用它，您需要在文件顶部添加`<iomanip>`的 Include。 请注意，`get<1>`将返回一个`file_size`对象，该对象被插入到`cout`中。 按照目前的情况，此代码将不会编译，因为没有操作符来执行此操作。 我们需要写一本。\n\n在定义结构之后，添加以下代码：\n\n```cpp\n    ostream& operator<<(ostream& os, const file_size fs) \n    { \n        int flags = os.flags(); \n        unsigned long long ll = fs.low + \n            ((unsigned long long)fs.high << 32); \n        os << hex << ll; \n        os.setf(flags); \n        return os; \n    }\n```\n\n此操作符将更改`ostream`对象，因此我们存储函数开始时的初始状态，并在结束时将对象恢复到此状态。 因为文件大小是 64 位数字，所以我们转换`file_size`对象的组成部分，然后将其打印为十六进制数字。\n\n现在您可以编译和运行此应用了。 例如：\n\n```cpp\nfiles C:windows\n```\n\n这将列出`windows`文件夹中文件的名称和大小。\n\n还需要做两件事--递归子文件夹和对数据进行排序。 两者都很容易实现。 在`files_in_folder`函数中，将以下代码添加到`if`语句的代码块中：\n\n```cpp\n    // this is a folder so recurse \n    string folder(findfiledata.cFileName); \n    // ignore . and .. directories \n    if (folder != \".\" && folder != \"..\") \n    { \n        files_in_folder(findItem.c_str(), files); \n    }\n```\n\n搜索将返回`.`(当前)文件夹和`..`(父)文件夹，因此我们需要检查并忽略它们。 下一个动作是递归调用`files_in_folder`函数来获取子文件夹中的文件。 如果您愿意，可以编译和测试应用，但这一次最好使用`Beginning_C++ `文件夹测试代码，因为递归列出 Windows 文件夹将生成大量文件。\n\n代码返回获得的文件列表，但我们希望按文件大小的顺序查看它们。 为此，我们可以在`<algorithm>`头中使用排序函数，因此在`<tuple>`的 Include 之后添加一个 Include。 在`main`函数中，在调用`files_in_folder,`之后添加以下代码：\n\n```cpp\n    files_in_folder(argv[1], files); \n\n    sort(files.begin(), files.end(), \n        [](const file_info& lhs, const file_info& rhs) { \n            return get<1>(rhs) > get<1>(lhs);    \n    } );\n```\n\n`sort`函数的前两个参数表示要检查的项目范围。 第三项是谓词，该函数将把`vector`中的两项传递给谓词。 如果两个参数顺序一致(第一个参数小于第二个参数)，则必须返回值`true`。\n\n谓词由 lambda 表达式提供。 由于没有捕获变量，因此表达式以`[]`开头，后跟由`sort`算法比较的项目的参数列表(通过`const`引用传递，因为它们不会改变)。 实际比较是在支撑之间进行的。 由于我们希望以升序列出文件，因此必须确保两个文件中的第二个大于第一个。 在这段代码中，我们在两个`file_size`对象上使用了`>`运算符。 为了编译这段代码，我们需要定义这个运算符。 在插入运算符之后添加以下内容：\n\n```cpp\n    bool operator>(const file_size& lhs, const file_size& rhs) \n    { \n        if (lhs.high > rhs.high) return true; \n        if (lhs.high == rhs.high) { \n            if (lhs.low > rhs.low) return true; \n        } \n        return false; \n    }\n```\n\n现在可以编译并运行该示例。 您应该会发现，指定文件夹和子文件夹中的文件按照文件大小的顺序列出。\n\n# 简略的 / 概括的 / 简易判罪的 / 简易的\n\n函数允许您将代码分段为逻辑例程，这使您的代码更具可读性，并使您能够灵活地重用代码。 C++ 提供了丰富的选项来定义函数，包括变量参数列表、模板、函数指针和 lambda 表达式。 但是，全局函数有一个主要问题：数据与函数是分开的。 这意味着函数必须通过全局数据项访问数据，或者每次调用函数时都必须通过参数将数据传递给函数。 在这两种情况下，数据都存在于函数之外，并且可以由其他与数据无关的函数使用。 下一章将给出这个问题的解决方案：类。 `class`允许您将数据封装在自定义类型中，并且您可以在该类型上定义函数，以便只有这些函数才能访问数据。"
  },
  {
    "path": "docs/begin-cpp-prog/06.md",
    "content": "# 六、类\n\nC++ 允许您创建自己的类型。 这些自定义类型可以有运算符，也可以转换为其他类型；实际上，它们可以像内置类型一样与您定义的行为一起使用。 该工具使用一种称为类的语言功能。 能够定义您自己的类型的好处是，您可以将数据封装在所选类型的对象中，并使用该类型来管理该数据的生存期。 您还可以定义可以对该数据执行的操作。 换句话说，您可以定义具有状态和行为的自定义类型，这是面向对象编程的基础。\n\n# 写作课\n\n当您使用内置类型时，任何有权访问该数据的代码都可以直接访问该数据。 C++ 提供了一种机制(`const`)来阻止写访问，但是任何代码都可以使用`const_cast`来丢弃`const`属性。 您的数据可能很复杂，例如指向映射到内存中的文件的指针，目的是让您的代码更改几个字节，然后将该文件写回磁盘。 这样的原始指针是危险的，因为其他有权访问该指针的代码可能会更改不应该更改的部分缓冲区。 需要一种机制来将数据封装到知道要更改哪些字节的类型中，并且只允许该类型访问数据。 这是课程背后的基本理念。\n\n# 审查结构\n\n我们已经在 C++ 中看到了一种封装数据的机制：`struct`。 结构允许您声明内置类型、指针或引用的数据成员。 当您从该`struct`创建变量时，您将创建该结构的一个**实例**，也称为**对象**。 您可以创建引用此对象的变量或指向该对象的指针。 您甚至可以将对象按值传递给一个函数，编译器将在该函数中复制对象(它将调用*复制构造函数*作为`struct`)。 我们已经看到，使用`struct`可以访问实例的任何代码(甚至通过指针或引用)都可以访问对象的成员(尽管这是可以更改的)。 这样使用，可以将状态`struct`视为包含状态的**聚合**类型。\n\n通过直接使用点运算符或通过指向对象的指针使用`->`运算符，可以初始化`struct`实例的成员。 我们还看到，您可以使用初始化式列表(用大括号括起来)来初始化`struct`的实例。 这是非常严格的，因为初始值设定项列表必须与`struct`中的数据成员相匹配。 在[第 4 章](04.html)，*使用内存、数组和指针*中，您看到可以将指针作为`struct`的成员，但是您必须显式地采取适当的操作来释放指针指向的内存；如果不这样做，则可能会导致内存泄漏。\n\nA`struct`是您可以在 C++ 中使用的类类型之一；另外两个是`union`和`class`。 定义为`struct`或`class`的自定义类型可以具有行为和状态，C++ 允许您定义一些特殊函数来控制如何创建和销毁、复制和转换实例。 此外，您可以在`struct`或`class`类型上定义运算符，这样就可以像在内置类型上使用运算符一样在实例上使用运算符。 `struct`和`class`之间有区别，我们将在后面讨论这一点，但总的来说，本章的其余部分都是关于类的，当提到`class`时，您通常可以假定`struct`也适用于`class`。\n\n# 定义类\n\n类在语句中定义，它将在一个块中定义其成员，其中多个语句用大括号`{}`括起来。 因为它是语句，所以必须在最后一个花括号后面加一个分号。 类可以在头文件中定义(与许多**C++ 标准库**类一样)，但您必须采取措施确保此类文件在源文件中只包含一次。 [第 1 章](01.html)，*从 C++*开始，描述了如何使用`#pragma once`、条件编译和预编译头文件来实现这一点。 但是，类中关于特定项的一些规则必须在源文件中定义，稍后将对此进行介绍。\n\n如果您仔细阅读 C++ 标准库，您会发现类包含成员函数，并且试图将类的所有代码放入单个头文件中，这会使代码难以阅读和理解。 对于由大量专业 C++ 程序员维护的库文件来说，这可能是合理的，但是对于您自己的项目来说，可读性应该是一个关键的设计目标。 因此，可以在 C++ 头文件(包括其成员函数)中声明 C++ 类，并且可以将函数的实际实现放在源文件中。 这使得头文件更易于维护，并且更具可重用性。\n\n# 定义类行为\n\n类可以定义只能通过类的实例调用的函数；这样的函数通常称为**方法**。 对象将具有状态；这由类定义的数据成员提供，并在创建对象时进行初始化。 对象上的方法定义对象的行为，通常作用于对象的状态。 在设计类时，您应该这样考虑方法：它们描述执行某些操作的对象。\n\n```cpp\n    class cartesian_vector \n    { \n    public: \n        double x; \n        double y; \n        // other methods \n        double get_magnitude() { return std::sqrt((x * x) + (y * y)); } \n    };\n```\n\n该类有两个数据成员`x`和`y`，它们表示在笛卡尔 x 和 y 方向上解析的二维向量的方向。 关键字`public`表示在此说明符之后定义的任何成员都可以由类外部定义的代码访问。 默认情况下，类的所有成员都是`private`，除非您另有说明。 这样的访问说明符将在下一章中更深入地介绍，但是`private`意味着该成员只能被类的其他成员访问。\n\nThis is the difference between a `struct` and a `class`: by default, members of a `struct` are `public` and by default, members of a `class` are `private`.\n\n该类有一个名为`get_magnituide`的方法，它将返回笛卡尔向量的长度。 此函数作用于类的两个数据成员，并返回值。 这是一种**访问器**方法；它提供对对象状态的访问。 这样的方法在`class`上是典型的，但不要求方法返回值。 与函数类似，方法也可以接受参数。 可以这样调用`get_magnituide`方法：\n\n```cpp\n    cartesian_vector vec { 3.0, 4.0 }; \n    double len = vec.get_magnitude(); // returns 5.0\n```\n\n这里在堆栈上创建了`cartesian_vector`对象，并使用列表初始化器语法将其初始化为表示向量`(3,4)`的值。 该向量的长度为 5，这是通过对对象调用`get_magnitude`返回的值。\n\n# 使用 this 指针\n\n类中的方法具有特殊的调用约定，在 Visual C++ 中称为`__thiscall`。 原因是类中的每个方法都有一个名为`this`的隐藏参数，它是指向当前实例的类类型的指针：\n\n```cpp\n    class cartesian_vector \n    { \n    public: \n        double x; \n        double y; \n        // other methods \n        double get_magnitude() \n        { \n             return std::sqrt((this->x * this->x) + (this->y * this->y)); \n        } \n    };\n```\n\n这里，`get_magnitude`方法返回`cartesian_vector`对象的长度。 通过`->`运算符访问对象的成员。 如前所述，可以在没有`this`指针的情况下访问类的成员，但它确实明确表示项是`class`的成员。\n\n您可以在`cartesian_vector`类型上定义允许您更改其状态的方法：\n\n```cpp\n    class cartesian_vector \n    { \n    public: \n        double x; \n        double y; \n        reset(double x, double y) { this->x = x; this->y = y; } \n        // other methods \n    };\n```\n\n`reset`方法的参数与类的数据成员具有相同的名称；但是，由于我们使用了`this`指针，编译器知道这不是二义性的。\n\n您可以使用`*`运算符取消引用`this`指针以访问该对象。 当成员函数必须返回对当前对象的引用时(就像我们稍后将看到的一些操作符将返回的那样)，并且您可以通过返回`*this`来实现这一点，这一点很有用。 类中的方法还可以将`this`指针传递给外部函数，这意味着它通过类型化指针通过引用传递当前对象。\n\n# 使用作用域解析操作符\n\n您可以在`class`语句中定义内联方法，但也可以将声明和实现分开，因此该方法在`class`语句中声明，但在其他地方定义。 在`class`语句之外定义方法时，需要使用作用域解析操作符为方法提供类型名称。 例如，使用前面的`cartesian_vector`示例：\n\n```cpp\n    class cartesian_vector \n    { \n    public: \n        double x; \n        double y; \n        // other methods \n        double magnitude(); \n    }; \n\n    double cartesian_vector::magnitude() \n    { \n        return sqrt((this->x * this->x) + (this->y * this->y)); \n    }\n```\n\n该方法是在类定义之外定义的；但是，它仍然是类方法，因此它有一个可用于访问对象成员的`this`指针。 通常，类将在具有方法原型的头文件中声明，而实际方法将在单独的源文件中实现。 在这种情况下，使用`this`指针访问类成员(方法和数据成员)会让您粗略查看源文件时清楚地看到，函数是类的方法。\n\n# 定义类状态\n\n您的类可以有内置类型作为数据成员，也可以有自定义类型。 这些数据成员可以在类中声明(并在构造类的实例时创建)，或者它们可以是指向在空闲存储中创建的对象的指针，也可以是指向在其他地方创建的对象的引用。 请记住，如果您有指向在免费商店中创建的项的指针，则需要知道释放指针所指向的内存的责任是谁。 如果您有对某个堆栈框架上创建的对象的引用(或指针)，则需要确保您的类的对象不会比该堆栈帧存在的时间更长。\n\n当您将数据成员声明为`public`时，这意味着外部代码可以对数据成员进行读写。 您可以决定只授予只读访问权限，在这种情况下，您可以通过访问器使成员`private`具有读取访问权限：\n\n```cpp\n    class cartesian_vector \n    { \n        double x; \n        double y; \n    public: \n        double get_x() { return this->x; } \n        double get_y() { return this->y; } \n        // other methods \n    };\n```\n\n当您将数据成员设置为`private`时，这意味着您不能使用初始值设定项列表语法来初始化对象，但我们将在稍后解决这一问题。 您可以决定使用访问器授予对数据成员的写访问权限，并使用此访问器检查该值。\n\n```cpp\n    void cartesian_vector::set_x(double d) \n    { \n        if (d > -100 && d < 100) this->x = d; \n    }\n```\n\n这适用于值范围必须介于(但不包括)-`100`和`100`之间的类型。\n\n# 创建对象\n\n您可以在堆栈或免费商店中创建对象。 使用上一个示例，如下所示：\n\n```cpp\n    cartesian_vector vec { 10, 10 }; \n    cartesian_vector *pvec = new cartesian_vector { 5, 5 }; \n    // use pvec \n    delete pvec\n```\n\n这是对象的**直接初始化**，并假设`cartesian_vector`的数据成员是`public`。 在堆栈上创建`vec`对象，并使用初始化器列表进行初始化。 在第二行中，在空闲存储中创建一个对象，并使用初始化列表进行初始化。 空闲存储上的对象必须在某个时刻被释放，这是通过删除指针来实现的。 `new`操作符将在空闲存储中为类的数据成员和类所需的任何基础设施分配足够的内存(如下一章所述)。\n\nC++ 11 的一个新特性是允许直接初始化以在类中提供默认值：\n\n```cpp\n    class point \n    { \n    public: \n        int x = 0; \n        int y = 0; \n    };\n```\n\n这意味着如果在没有任何其他初始化值的情况下创建`point`的实例，它将被初始化，以便`x`和`y`都为零。 如果数据成员是内置数组，则可以使用类中的初始化列表提供直接初始化：\n\n```cpp\n    class car \n    { \n    public: \n        double tire_pressures[4] { 25.0, 25.0, 25.0, 25.0 }; \n    };\n```\n\nC++ 标准库容器可以用初始化列表进行初始化，因此，在`tire_pressures`的这个类中，我们可以使用`vector<double>`或`array<double,4>`，并以相同的方式对其进行初始化，而不是将类型声明为`double[4]`。\n\n# 物体的构造\n\nC++ 允许您定义特殊方法来执行对象的初始化。 这些函数称为**构造函数**。 在 C++ 11 中，默认情况下会为您生成三个这样的函数，但如果您愿意，也可以提供自己的版本。 这三个构造函数以及其他三个相关函数如下所示：\n\n*   **默认构造函数：**-调用此函数以创建具有*默认值*值的对象。\n*   **复制构造函数：**-此函数用于基于现有对象的值创建新对象。\n*   **Move 构造函数：**-此函数用于使用从现有对象移动的数据创建新对象。\n*   **析构函数：**此函数用于清理对象使用的资源。\n*   **复制分配：**此操作将数据从一个现有对象复制到另一个现有对象。\n*   **移动分配：**这会将数据从一个现有对象移动到另一个现有对象。\n\n这些函数的编译器创建的版本将隐式为`public`；但是，您可以决定通过定义自己的版本并将其设置为`private`来阻止复制或赋值，或者可以使用`=delete`语法删除它们。 您还可以提供自己的构造函数，这些构造函数将接受您决定初始化新对象所需的任何参数。\n\n构造函数是与类型同名但不返回值的成员函数，因此如果构造失败，则无法返回值，这可能意味着调用方将接收部分构造的对象。 处理这种情况的唯一方法是抛出异常(在[第 10 章](10.html)，*诊断和调试*中解释)。\n\n# 定义构造函数\n\n当创建一个没有值的对象时，将使用默认构造函数，因此必须使用默认值初始化该对象。 前面声明的`point`可以这样实现：\n\n```cpp\n    class point \n    { \n        double x; double y; \n    public: \n        point() { x = 0; y = 0; } \n    };\n```\n\n这会显式地将这些项初始化为零值。 如果要使用默认值创建实例，请不要使用圆括号。\n\n```cpp\n    point p;   // default constructor called\n```\n\n请务必注意此语法，因为很容易错误地写出以下内容：\n\n```cpp\n    point p();  // compiles, but is a function prototype!\n```\n\n这将进行编译，因为编译器会认为您提供的是函数原型作为转发声明。 但是，当您尝试将符号`p`用作变量时，会出现错误。 您还可以使用带空大括号的初始化列表语法调用默认构造函数：\n\n```cpp\n    point p {};  // calls default constructor\n```\n\n虽然在这种情况下，数据成员是内置类型，这无关紧要，但像这样初始化构造函数主体中的数据成员涉及到调用成员类型的赋值运算符。 更有效的方法是对**成员列表**使用直接初始化。\n\n下面是一个接受两个参数的构造函数，它说明了一个成员列表：\n\n```cpp\n    point(double x, double y) : x(x), y(y) {}\n```\n\n圆括号外的标识符是类成员的名称，圆括号内的项是用于初始化该成员的表达式(在本例中是构造函数参数)。 本例使用`x`和`y`作为参数名称。 您不必这样做；这里给出这一点只是为了说明编译器将区分参数和数据成员。 还可以在构造函数的成员列表中使用带括号的初始值设定项语法：\n\n```cpp\n    point(double x, double y) : x{x}, y{y} {}\n```\n\n在创建如下对象时调用此构造函数：\n\n```cpp\n    point p(10.0, 10.0);\n```\n\n您还可以创建对象数组：\n\n```cpp\n    point arr[4];\n```\n\n这将创建四个`point`对象，可以通过索引`arr`数组来访问这些对象。 请注意，在创建对象数组时，会在项上调用*默认的*构造函数；无法调用任何其他构造函数，因此必须分别初始化每个构造函数。\n\n您还可以为构造函数参数提供默认值。 在下面的代码中，`car`类具有四个轮胎(前两个是前胎)和备用轮胎的值。 有一个构造函数具有用于前胎和后胎的必需值，以及一个用于备胎的可选值。 如果没有提供备用轮胎压力的值，则将使用默认值：\n\n```cpp\n    class car \n    { \n        array<double, 4> tire_pressures;; \n        double spare; \n    public: \n        car(double front, double back, double s = 25.0)  \n          : tire_pressures{front, front, back, back}, spare{s} {} \n    };\n```\n\n可以使用两个值或三个值调用此构造函数：\n\n```cpp\n    car commuter_car(25, 27); \n    car sports_car(26, 28, 28);\n```\n\n# 委托构造函数\n\n构造函数可以使用相同的成员列表语法调用另一个构造函数：\n\n```cpp\n    class car \n    { \n        // data members \n    public: \n        car(double front, double back, double s = 25.0)  \n           : tire_pressures{front, front, back, back}, spare{s} {} \n        car(double all) : car(all, all) {} \n    };\n```\n\n在这里，接受一个值的构造函数委托给接受三个参数的构造函数(在本例中使用备件的默认值)。\n\n# 复制构造函数\n\n当您按值传递对象(或按值返回)或基于另一个对象显式构造对象时，将使用复制构造函数。 下面两行代码的最后两行都从另一个`point`对象创建了一个`point`对象，并且在这两种情况下都调用了复制构造函数：\n\n```cpp\n    point p1(10, 10); \n    point p2(p1); \n    point p3 = p1;\n```\n\n最后一行看起来涉及赋值操作符，但实际上它调用了复制构造函数。 复制构造函数可以按如下方式实现：\n\n```cpp\n    class point \n    { \n        int x = 0;int y = 0; \n    public: \n        point(const point& rhs) : x(rhs.x), y(rhs.y) {} \n    };\n```\n\n初始化访问另一个对象(`rhs`)上的`private`数据成员。 这是可以接受的，因为构造函数参数与正在创建的对象的类型相同。 复制操作可能不会这么简单。 例如，如果类包含一个作为指针的数据成员，您很可能希望复制指针所指向的数据，这将涉及在新对象中创建新的内存缓冲区。\n\n# 在类型之间转换\n\n您还可以执行转换。 在数学中，您可以定义一个表示方向的向量，这样在两点之间绘制的直线就是一个向量。 在我们的代码中，我们已经定义了一个`point`类和一个`cartesian_vector`类。 您可以决定使用在原点和点之间创建向量的构造函数，在这种情况下，您要将`point`对象转换为`cartesian_vector`对象：\n\n```cpp\n    class cartesian_vector \n    { \n        double x; double y;  \n    public: \n        cartesian_vector(const point& p) : x(p.x), y(p.y) {} \n    };\n```\n\n这里有一个问题，我们稍后将解决这个问题。 可以这样调用转换：\n\n```cpp\n    point p(10, 10); \n    cartesian_vector v1(p); \n    cartesian_vector v2 { p }; \n    cartesian_vector v3 = p;\n```\n\n# 交朋友\n\n上述代码的问题在于，`cartesian_vector`类访问`point`类的`private`成员。 由于我们已经编写了这两个类，我们很乐意改变规则，因此我们将`cartesian_vector`类设为`point`类的`friend`类：\n\n```cpp\n    class cartesian_vector; // forward decalartion \n\n    class point \n    { \n        double x; double y; \n    public: \n        point(double x, double y) : x(x), y(y){} \n        friend class cartesian_point; \n    };\n```\n\n因为`cartesian_vector`类是在`point`类之后声明的，所以我们必须提供一个正向声明，该声明实质上告诉编译器即将使用名称`cartesian_vector`，并且它将在其他地方声明。 重要的一行以`friend`开头。 这表明整个类`cartesian_vector`的代码可以访问`point`类的私有成员(数据和方法)。\n\n您还可以声明`friend`函数。 例如，您可以声明一个运算符，以便可以将`point`对象插入到`cout`对象中，这样就可以将其打印到控制台。 您不能更改`ostream`类，但可以定义全局方法：\n\n```cpp\n    ostream& operator<<(ostream& stm, const point& pt) \n    { \n        stm << \"(\" << pt.x << \",\" << pt.y << \")\"; \n        return stm; \n    }\n```\n\n此函数访问`point`的`private`成员，因此您必须使用以下命令使该函数成为`point`类的`friend`：\n\n```cpp\n    friend ostream& operator<<(ostream&, const point&);\n```\n\n这样的`friend`声明必须在`point`类中声明，但它是放在`public`还是`private`部分中无关紧要。\n\n# 将构造函数标记为显式\n\n在某些情况下，您不希望允许在作为另一种类型的构造函数的参数传递的一种类型之间进行隐式转换。 为此，需要用`explicit`说明符标记构造函数。 这意味着调用构造函数的唯一方法是使用圆括号语法：*显式*调用构造函数。 在下面的代码中，不能将`double`隐式转换为`mytype`的对象：\n\n```cpp\n    class mytype  \n    { \n    public: \n        explicit mytype(double x); \n    };\n```\n\n现在，如果要使用`double`参数创建对象，则必须显式地*调用构造函数：*\n\n```cpp\n    mytype t1 = 10.0; // will not compile, cannot convert \n    mytype t2(10.0);  // OK\n```\n\n# 销毁对象\n\n当一个对象被销毁时，调用一个称为析构函数的特殊方法。 此方法的类名以`~`符号为前缀，并且不返回值。\n\n如果对象是堆栈上的自动变量，那么当变量超出作用域时，它将被销毁。 当通过值传递对象时，将在被调用函数的堆栈上创建一个副本，并在被调用函数完成时销毁该对象。 此外，函数如何完成并不重要，无论是显式调用`return`，还是到达最后一个大括号，或者是否抛出异常；在所有这些情况下，都会调用析构函数。 如果函数中有多个对象，则以与构造同一作用域中的对象相反的顺序调用析构函数。 如果创建对象数组，则会在声明数组的语句上为数组中的每个对象调用默认构造函数，并且所有对象都将被销毁--当数组超出作用域时，将调用每个对象上的析构函数。\n\n以下是类`mytype`的一些示例：\n\n```cpp\n    void f(mytype t) // copy created \n    { \n        // use t \n    }   // t destroyed \n\n    void g() \n    { \n        mytype t1; \n        f(t1); \n        if (true) \n        { \n            mytype t2; \n        }   // t2 destroyed \n\n        mytype arr[4]; \n    }  // 4 objects in arr destroyed in reverse order to creation \n       // t1 destroyed\n```\n\n当您返回一个对象时，会发生一个有趣的操作。 以下注释是您所期望的：\n\n```cpp\n    mytype get_object() \n    { \n        mytype t;               // default constructor creates t \n        return t;               // copy constructor creates a temporary \n    }                           // t destroyed \n\n    void h() \n    { \n        test tt = get_object(); // copy constructor creates tt \n    }                           // temporary destroyed, tt destroyed\n```\n\n事实上，这一过程更加流畅。 在调试版本中，编译器将看到在返回`get_object`函数时创建的临时对象是将用作变量`tt`的对象，因此在`get_object`函数的返回值上没有额外的副本。 该函数实际上如下所示：\n\n```cpp\n    void h() \n    { \n        mytype tt = get_object();  \n    }   // tt destroyed\n```\n\n但是，编译器能够进一步优化代码。 在发布版本中(启用了优化)，不会创建临时对象，调用函数中的对象`tt`将是在`get_object`中创建的实际对象`t`。\n\n当您显式删除指向分配在空闲存储区上的对象的指针时，该对象将被销毁。 在这种情况下，对析构函数的调用是确定性的：它是在代码调用`delete`时调用的。 同样，对于相同的类`mytype`，如下所示：\n\n```cpp\n    mytype *get_object() \n    { \n        return new mytype; // default constructor called \n    } \n\n    void f() \n    { \n        mytype *p = get_object(); \n        // use p \n        delete p;        // object destroyed \n    }\n```\n\n有时，您希望使用删除对象的确定性方面(可能有忘记调用`delete`的危险)，有时，您更希望确保对象将在适当的时间销毁(可能会在更晚的时间销毁)。\n\n如果类中的数据成员是带有析构函数的自定义类型，则当销毁包含对象时，也会调用包含对象上的析构函数。 尽管如此，请注意，只有当*对象*是类成员时，才会出现这种情况。 如果类成员是指向空闲存储区中对象的指针，则必须显式删除包含对象的析构函数中的指针。 但是，您需要知道指针所指向的对象的位置，因为如果该对象不在空闲存储中，或者该对象正被其他对象使用，则调用`delete`会导致问题。\n\n# 指定对象\n\n当已创建的*对象被赋给另一个对象的值时，将调用赋值运算符。 默认情况下，您将获得一个复制赋值运算符，该运算符将复制所有数据成员。 这不一定是您想要的，特别是如果对象的数据成员是指针，在这种情况下，您更有可能执行深度复制并复制指向的数据，而不是指针的值(在后一种情况下，两个*个*对象将指向相同的数据)。*\n\n *如果定义复制构造函数，则仍将获得默认的复制赋值运算符；但是，如果您认为编写自己的复制构造函数很重要，则还应该提供自定义的复制赋值运算符。 (同样，如果定义复制赋值运算符，则除非定义默认复制构造函数，否则将获得默认复制构造函数。)\n\n复制赋值操作符通常是类的`public`成员，它接受对将用于提供赋值的值的对象的`const`引用。 赋值运算符的语义是您可以链接它们，因此，例如，下面的代码对其中两个对象调用赋值运算符：\n\n```cpp\n    buffer a, b, c;              // default constructors called \n    // do something with them \n    a = b = c;                   // make them all the same value \n    a.operator=(b.operator=(c)); // make them all the same value\n```\n\n最后两行做同样的事情，但显然第一行更具可读性。 要启用这些语义，赋值操作符必须返回对已赋值对象的引用。 因此，类`buffer`将具有以下方法：\n\n```cpp\n    class buffer \n    { \n        // data members \n    public: \n        buffer(const buffer&);            // copy constructor \n        buffer& operator=(const buffer&); // copy assignment \n    };\n```\n\n尽管复制构造函数和复制赋值方法看起来做的事情相似，但有一个关键的区别。 复制构造函数创建调用前不存在的新对象。 调用代码知道，如果构造失败，则会引发异常。 使用赋值时，两个对象都已存在，因此您要将值从一个对象复制到另一个对象。 这应该被视为原子操作，并且应该执行所有复制；分配在中途失败是不可接受的，从而导致一个对象同时包含两个对象。 此外，在构造中，对象仅在构造成功后才存在，因此复制构造不能发生在对象本身上，但是代码将对象分配给自身是完全合法的(如果没有意义的话)。 副本分配需要检查此情况并采取适当的操作。\n\n有多种策略可以做到这一点，一种常见的策略被称为复制和交换习惯用法，因为它使用标记为`noexcept`的标准库`swap`函数，并且不会抛出异常。 这个习惯用法涉及在赋值的右侧创建对象的临时副本，然后将其数据成员与左侧的对象的数据成员交换。\n\n# 移动语义\n\nC++ 11 通过 Move 构造函数和 Move 赋值操作符提供了移动语义，当使用临时对象创建另一个对象或将其分配给现有对象时，会调用这两个函数。 在这两种情况下，因为临时对象不会存在于语句之外，所以可以将临时对象的内容移动到另一个对象，从而使临时对象处于无效状态。 编译器将通过将数据从临时对象移动到新创建的(或分配给)对象的默认操作为您创建这些函数。\n\n您可以编写自己的版本，为了表示移动语义，这些版本有一个参数是一个右值引用(`&&`)。\n\nIf you want the compiler to provide you with a default version of any of these methods, you can provide the prototype in the class declaration suffixed with `=default`. In most cases, this is self-documenting rather than being a requirement, but if you are writing a POD class you must use the default versions of these functions, otherwise `is_pod` will not return `true`.\n\n如果您只想使用 MOVE 而从不使用 COPY(例如，文件句柄类)，则可以*删除*COPY 功能：\n\n```cpp\n    class mytype \n    { \n        int *p; \n    public: \n        mytype(const mytype&) = delete;             // copy constructor \n        mytype& operator= (const mytype&) = delete; // copy assignment \n        mytype&(mytype&&);                          // move constructor \n        mytype& operator=(mytype&&);                // move assignment \n    };\n```\n\n该类有一个指针数据成员，并允许移动语义，在这种情况下，将使用对临时对象的引用来调用移动构造函数。 由于该对象是临时的，因此在调用 Move 构造函数后它将无法继续存在。 这意味着新对象可以*将临时对象的状态*移入其自身：\n\n```cpp\n    mytype::mytype(mytype&& tmp) \n    { \n        this->p = tmp.p; \n        tmp.p = nullptr; \n    }\n```\n\nMove 构造函数将临时对象的指针分配给`nullptr`，因此为该类定义的任何析构函数都不会尝试删除指针。\n\n# 声明静态成员\n\n您可以声明类的成员--数据成员或方法--`static`。 这在某些方面类似于在文件范围内声明的自动变量和函数上使用`static`关键字，但在类成员上使用该关键字时，它有一些重要且不同的属性。\n\n# 定义静态成员\n\n当您在类成员上使用`static`时，这意味着该项与类相关联，而不是与特定实例相关联。 在这种情况下，对于数据成员，这意味着有一个数据项由类的所有实例共享。 同样，`static`方法没有附加到对象，它不是`__thiscall`，也没有`this`指针。\n\n`static`方法是类命名空间的一部分，因此它可以为类创建对象并访问其`private`成员。 默认情况下，`static`方法具有`__cdecl`调用约定，但如果愿意，可以将其声明为`__stdcall`。 这意味着，您可以在类中编写一个方法，该方法可用于初始化许多库使用的类 C 指针。 请注意，`static`函数不能调用类上的非静态方法，因为非静态方法需要`this`指针，但非静态方法可以调用`static`方法。\n\n非静态方法通过对象调用，或者使用点运算符(对于类实例)，或者使用对象指针的`->`运算符。 `static`方法不需要关联的对象，但可以通过一个对象调用。 这提供了通过对象或通过`class`名称调用`static`方法的两种方式：\n\n```cpp\n    class mytype \n    { \n    public: \n        static void f(){} \n        void g(){ f(); } \n    };\n```\n\n在这里，该类定义了一个名为`f`的`static`方法和一个名为`g`的非静态方法。 非静态方法`g`可以调用`static`方法，但是`static`方法`f`不能调用非静态方法。 因为`static`方法`f`是`public`，所以`class`外部的代码可以调用它：\n\n```cpp\n    mytype c; \n    c.g();       // call the nonstatic method \n    c.f();       // can also call the static method thru an object \n    mytype::f(); // call static method without an object\n```\n\n虽然可以通过对象调用`static`函数，但调用它根本不需要创建任何对象。\n\n静态数据成员需要做更多的工作，因为当您使用`static`时，它表示数据成员不是对象的一部分，通常数据成员是在创建对象时分配的。 您必须在类之外定义`static`个数据成员：\n\n```cpp\n    class mytype \n    { \n    public: \n        static int i; \n        static void incr() { i++ ; } \n    }; \n\n    // in a source file \n    int mytype::i = 42;\n```\n\n数据成员在文件作用域的类外部定义。 它使用`class`名称命名，但请注意，还必须使用类型定义它。 在本例中，数据成员使用值进行初始化；如果不这样做，则在第一次使用变量时，它将具有该类型的默认值(在本例中为零)。 如果选择在头文件中声明类(这很常见)，则`static`数据成员的定义必须在源文件中。\n\n您还可以在为`static`的方法中声明变量。 在这种情况下，在所有对象中跨方法调用维护该值，因此它具有与`static class`成员相同的效果，但是您没有在类外部定义变量的问题。\n\n# 使用静态和全局对象\n\n全局函数中的`static`变量将在第一次调用该函数之前的某个点创建。 类似地，作为类成员的`static`对象将在首次被访问之前的某个时刻被初始化。\n\n静态和全局对象在调用`main`函数之前构造，并在`main`函数结束后销毁。 此初始化的顺序有一些问题。 C++ 标准规定，源文件中定义的`static`和全局对象的初始化将在使用该源文件中定义的任何函数或对象之前进行，如果源文件中有多个全局对象，则它们将按照*定义的顺序进行初始化*。 问题是如果您有多个源文件，每个源文件中都有`static`个对象。 不能保证这些对象的初始化顺序。 如果一个`static`对象依赖于另一个`static`对象，这就成了问题，因为您不能保证依赖对象将在它所依赖的对象之后创建。\n\n# 命名构造函数\n\n这是`public static`方法的一个应用。 其思想是，由于`static`方法是`class`的成员，这意味着它可以访问`class`实例的`private`成员，因此这样的方法可以创建一个对象，执行一些额外的初始化，然后将该对象返回给调用方。 这是**工厂方法**。 到目前为止使用的`point`类是使用笛卡尔点构造的，但我们也可以基于极坐标创建点，其中`(x, y)`笛卡尔坐标可以计算为：\n\n```cpp\n    x = r * cos(theta) \n    y = r * sin(theta)\n```\n\n这里`r`是向量到点的长度，`theta`是该向量与 x 轴的逆时针角度。 `point`类已经有一个接受两个`double`值的构造函数，因此我们不能使用它来传递极坐标；相反，我们可以使用`static`方法作为名为*的构造函数*：\n\n```cpp\n    class point \n    { \n        double x; double y; \n    public: \n        point(double x, double y) : x(x), y(y){} \n        static point polar(double r, double th) \n        { \n            return point(r * cos(th), r * sin(th)); \n        } \n    };\n```\n\n该方法可以按如下方式调用：\n\n```cpp\n    const double pi = 3.141529; \n    const double root2 = sqrt(2); \n    point p11 = point::polar(root2, pi/4);\n```\n\n对象`p11`是笛卡尔坐标为(1，1)的`point`。 在本例中，`polar`方法调用`public`构造函数，但它可以访问私有成员，因此可以编写相同的方法(效率较低)为：\n\n```cpp\n    point point::polar(double r, double th) \n    { \n        point pt; \n        pt.x = r * cos(th); \n        pt.y = r * sin(th); \n        return pt; \n    }\n```\n\n# 嵌套类\n\n您可以在类中定义类。 如果嵌套类声明为`public`，则可以在容器类中创建对象并将其返回给外部代码。 但是，通常情况下，您会希望声明一个由类使用的类，并且应该是`private`。 下面声明了一个`public`嵌套类：\n\n```cpp\n    class outer \n    { \n    public: \n        class inner  \n        { \n        public: \n            void f(); \n        }; \n\n        inner g() { return inner(); } \n    }; \n\n    void outer::inner::f() \n    { \n         // do something \n    }\n```\n\n请注意嵌套类的名称是如何以包含类的名称作为前缀的。\n\n# 访问常量对象\n\n到目前为止，您已经看到了许多使用`const`的示例，其中最常见的可能是将`const`作为函数参数应用于引用，以向编译器表明该函数对对象只有只读访问权限。 使用这样的`const`引用，以便通过引用传递对象，以避免在通过值传递对象时发生的复制开销。 `class`上的方法可以访问对象数据成员，并且可能会更改它们，因此如果通过`const`引用传递对象，编译器将只允许该引用调用不更改对象的方法。 前面定义的`point`类有两个访问器来访问类中的数据：\n\n```cpp\n    class point \n    { \n        double x; double y; \n    public: \n        double get_x() { return x; } \n        double get_y() { return y: } \n    };\n```\n\n如果您定义了一个采用`const`引用的函数，并尝试调用这些访问器，则会从编译器收到一个错误：\n\n```cpp\n    void print_point(const point& p) \n    { \n        cout << \"(\" << p.get_x() << \",\" << p.get_y() << \")\" << endl; \n    }\n```\n\n来自编译器的错误有点模糊：\n\n```cpp\ncannot convert 'this' pointer from 'const point' to 'point &'\n```\n\n这条消息是编译器抱怨对象是`const`，它是不可变的，它不知道这些方法是否会保留对象的状态。 解决方案很简单--向不更改对象状态的方法添加`const`关键字，如下所示：\n\n```cpp\n    double get_x() const { return x; } \n    double get_y() const { return y: }\n```\n\n这实际上意味着`this`指针是`const`。 关键字`const`是函数原型的一部分，因此该方法可以在此基础上重载。 可以有一个方法在`const`对象上调用时调用，另一个方法在非常数对象上调用。 这使您能够实现写入时复制模式，例如，`const`方法将返回对数据的只读访问，非常数方法将返回可写数据的*副本*。\n\n当然，标有`const`的方法不得更改数据成员，即使是临时更改也不行。 因此，这样的方法只能调用`const`个方法。 在极少数情况下，数据成员被设计为通过`const`对象进行更改；在这种情况下，成员的声明用`mutable`关键字标记。\n\n# 使用带有指针的对象\n\n可以在空闲存储上创建对象，并通过类型化指针进行访问。 这提供了更大的灵活性，因为将指针传递给函数是有效的，而且您可以显式确定对象的生存期，因为对象是通过调用`new`创建的，而通过调用`delete`销毁的。\n\n# 获取指向对象成员的指针\n\n如果需要通过实例访问类数据成员的地址(假设数据成员为`public`)，只需使用`&`运算符：\n\n```cpp\n    struct point { double x; double y; }; \n    point p { 10.0, 10.0 }; \n    int *pp = &p.x;\n```\n\n在本例中，`struct`用于声明`point`，因此默认情况下成员是`public`。 第二行使用初始化列表构造具有两个值的`point`对象，然后最后一行获得指向其中一个数据成员的指针。 当然，在销毁对象之后不能使用指针。 数据成员是在内存中分配的(在本例中是在堆栈上)，因此地址操作符只获得指向该内存的指针。\n\n函数指针则不同。 无论创建了多少个`class`实例，内存中都只有一个方法副本，但是因为方法是使用`__thiscall`调用约定(使用隐藏的`this`参数)调用的，所以您必须有一个函数指针，该指针可以用指向对象的指针来初始化，以提供`this`指针。 请考虑以下内容`class`：\n\n```cpp\n    class cartesian_vector \n    { \n    public: \n        // other items \n        double get_magnitude() const \n        { \n            return std::sqrt((this->x * this->x) + (this->y * this->y)); \n        }  \n    };\n```\n\n我们可以定义指向`get_magnitude`方法的函数指针，如下所示：\n\n```cpp\n    double (cartesian_vector::*fn)() const = nullptr; \n    fn = &cartesian_vector::get_magnitude;\n```\n\n第一行声明一个函数指针。 这类似于 C 函数指针声明，不同之处在于指针类型中包含了`class`名称。 这是必需的，以便编译器知道它必须在通过该指针的任何调用中提供`this`指针。 第二行获取指向该方法的指针。 请注意，没有涉及任何对象。 您不是在获取指向对象上的方法的函数指针；而是在获取指向必须通过对象调用的`class`上的方法的指针。 要通过此指针调用该方法，需要使用指向对象上的成员运算符`.*`的指针：\n\n```cpp\n    cartesian_vector vec(1.0, 1.0); \n    double mag = (vec.*fn)();\n```\n\n第一行创建一个对象，第二行调用该方法。 指向成员运算符的指针表示，*右侧*上的函数指针是通过*左侧*上的对象调用的。 调用方法时，左侧对象的地址用作`this`指针。 因为这是一个方法，所以我们需要提供一个参数列表，在本例中为空(如果您有参数，则它们应该在此语句右侧的圆括号中)。 如果您有一个对象指针，则语法类似，但您使用指向成员运算符的`->*`指针：\n\n```cpp\n    cartesian_vector *pvec = new cartesian_vector(1.0, 1.0); \n    double mag = (pvec->*fn)(); \n    delete pvec;\n```\n\n# 运算符重载\n\n类型的行为之一是您可以对其应用的操作。 C++ 允许您将 C++ 运算符作为类的一部分进行重载，因此运算符显然是作用于该类型。 这意味着对于一元运算符，成员方法应该没有参数，而对于二元运算符，您只需要一个参数，因为当前对象将位于运算符的左侧，因此方法参数是右侧的项。 下表总结了如何实现一元运算符和二元运算符，以及四个例外：\n\n| **表达式** | **名称** | **成员方法** | **非成员函数** |\n| +a/-a | 前缀一元 | 运算符() | 营运者(A)(A) |\n| a，b | 二进制的 / 由两部分组成的 / 双重的 / 二元的 | 操作员(B)(B) | 运算符(a，b) |\n| A+/a- | 后缀一元 | 运算符(0) | 运算符(a，0) |\n| A=b | 任务 / 归属 / 转让 / 分配 | 操作员=(B) |  |\n| A ( b ) | 函数调用 | 操作员()(B) |  |\n| A [ b ] | 标引 | 操作员[](B) |  |\n| A->-> | 指针访问 | 运算符->() |  |\n\n这里，符号用于表示除表中提到的四个运算符之外的任何可接受的一元运算符或二元运算符。\n\n对于运算符应该返回什么没有严格的规则，但如果自定义类型上的运算符的行为类似于内置类型上的运算符，则会有所帮助。 还必须有一些一致性。 如果实现`+`运算符将两个对象相加在一起，则应该对`+=`运算符使用相同的加号操作。 此外，您可能会争辩说，加号操作还将决定减号操作应该是什么样子，因此也就决定了`-`和`-=`运算符。 同样，如果您想定义`<`运算符，那么也应该定义`<=. >`、`>=`、`==`和`!=`。\n\n标准库的算法(例如，`sort`)只需要在自定义类型上定义`<`运算符。\n\n该表显示，您几乎可以将所有运算符实现为自定义类型类的成员或全局函数(除了列出的四个必须是成员方法的运算符)。 通常，最好将运算符实现为类的一部分，因为它维护封装：成员函数可以访问类的非公共成员。\n\n一元运算符的一个例子是一元负运算符。 这通常不会改变对象，但会返回一个新对象，该对象是该对象的负*。 对于我们的`point class`，这意味着将两个坐标都设为负值，这相当于直线*y=-x*中笛卡尔点的镜像：*\n\n```cpp\n    // inline in point \n    point operator-() const \n    { \n        return point(-this->x, -this->y); \n    }\n```\n\n运算符被声明为`const`，因为运算符显然不会更改对象，因此在`const`对象上调用它是安全的。 运算符可以这样调用：\n\n```cpp\n    point p1(-1,1); \n    point p2 = -p1; // p2 is (1,-1)\n```\n\n要理解我们为什么要实现这样的运算符，请查看一元运算符在应用于内置类型时会做些什么。 这里的第二个语句`int i, j=0; i = -j;`只会改变`i`，不会改变`j`，因此成员`operator-`应该不会影响对象的值。\n\n二元负运算符有不同的含义。 首先，它有两个操作数，第二，在本例中，结果与操作数的类型不同，因为结果是一个向量，它通过将一个点与另一个点分开来指示方向。 假设已经使用具有两个参数的构造函数定义了`cartesian_vector`，那么我们可以这样写：\n\n```cpp\n    cartesian_vector point::operator-(point& rhs) const \n    { \n        return cartesian_vector(this->x - rhs.x, this->y - rhs.y); \n    }\n```\n\n递增和递减运算符具有特殊的语法，因为它们是一元运算符，可以作为前缀或后缀，并且它们会改变它们应用到的对象。 这两个运算符之间的主要区别在于，后缀运算符在递增/递减操作之前返回对象*的值，因此必须创建一个临时运算符。 因此，前缀运算符几乎总是比后缀运算符具有更好的性能。 在类定义中，为了区分这两者，前缀运算符没有参数，后缀运算符有一个伪参数(在上表中，给出了 0)。 对于类`mytype`，如下所示：*\n\n```cpp\n    class mytype  \n    { \n    public: \n        mytype& operator++() \n        {  \n            // do actual increment \n            return *this; \n        } \n        mytype operator++(int) \n        { \n            mytype tmp(*this); \n            operator++(); // call the prefix code \n            return tmp; \n        } \n    };\n```\n\n实际的增量代码由前缀操作符实现，该逻辑由后缀操作符通过显式调用该方法来使用。\n\n# 定义函数类\n\n函数器是实现`()`运算符的类。 这意味着您可以使用与函数相同的语法调用对象。 请考虑以下内容：\n\n```cpp\n    class factor \n    { \n        double f = 1.0; \n    public: \n        factor(double d) : f(d) {} \n        double operator()(double x) const { return f * x; }  \n    };\n```\n\n可以这样调用此代码：\n\n```cpp\n    factor threeTimes(3);        // create the functor object \n    double ten = 10.0; \n    double d1 = threeTimes(ten); // calls operator(double) \n    double d2 = threeTimes(d1);  // calls operator(double)\n```\n\n这段代码显示，函数器对象不仅提供一些行为(在本例中，对参数执行操作)，而且还可以具有状态。 前面两行是通过对象上的`operator()`方法调用的：\n\n```cpp\n    double d2 = threeTimes.operator()(d1);\n```\n\n看看语法。 函数器对象被调用，就像它是如下声明的函数一样：\n\n```cpp\n    double multiply_by_3(double d) \n    { \n        return 3 * d;  \n    }\n```\n\n假设您想要传递一个指向一个函数的指针--也许您希望该函数的行为被外部代码改变。 为了能够使用函数器或方法指针，您需要重载函数：\n\n```cpp\n    void print_value(double d, factor& fn); \n    void print_value(double d, double(*fn)(double));\n```\n\n第一个函数引用一个函数器对象。 第二个函数有一个 C 型函数指针(您可以向其传递指向`multiply_by_3`的指针)，并且非常不可读。 在这两种情况下，在实现代码中以相同的方式调用`fn`参数，但是您需要声明两个函数，因为它们的类型不同。 现在，考虑一下函数模板的魔力：\n\n```cpp\n    template<typename Fn> \n    void print_value(double d, Fn& fn) \n    { \n        double ret = fn(d); \n        cout << ret << endl; \n    }\n```\n\n这是泛型代码；`Fn`类型可以是 C 函数指针或函数器`class`，编译器将生成适当的代码。\n\nThis code can be called by either passing a function pointer to a global function, which will have the `__cdecl` calling convention, or a functor object where the `operator()` operator will be called, which has a `__thiscall` calling convention.\n\n这只是一个实现细节，但这确实意味着您可以编写一个泛型函数，该函数可以接受类似 C 的函数指针或函数器对象作为参数。 C++ 标准库使用了这种魔力，这意味着它提供的算法可以使用*全局函数*或*函数器*或*lambda 表达式*来调用。\n\n标准库算法使用三种类型的函数类、生成器以及一元函数和二元函数；即，具有零个、一个或两个参数的函数。 此外，标准库调用返回`bool`和**谓词**的函数对象(一元或二进制)。 文档将告诉您是否需要谓词函数、一元函数或二元函数。 旧版本的标准库需要知道函数对象的返回值和参数(如果有的话)的类型才能工作，因此，函数式类必须基于标准类`unary_function`和`binary_function`(通过继承，将在下一章进行说明)。 在 C++ 11 中，这一要求已被删除，因此不需要使用这些类。\n\n在某些情况下，当需要一元函数器时，您会希望使用二元函数器。 例如，标准库定义了`greater`类，当用作函数对象时，它采用两个参数和一个`bool`来确定第一个参数是否大于第二个参数，使用这两个参数的类型定义的`operator>`。 这将用于需要二元函数器的函数，因此该函数将比较两个值；例如：\n\n```cpp\n    template<typename Fn>  \n    int compare_vals(vector<double> d1, vector<double> d2, Fn compare) \n    { \n        if (d1.size() > d2.size()) return -1; // error \n        int c = 0; \n        for (size_t i = 0; i < d1.size(); ++ i) \n        { \n            if (compare(d1[i], d2[i])) c++ ; \n        } \n        return c; \n    }\n```\n\n它接受两个集合，并使用作为最后一个参数传递的函数器比较相应的项。 可以这样称呼它：\n\n```cpp\n    vector<double> d1{ 1.0, 2.0, 3.0, 4.0 }; \n    vector<double> d2{ 1.0, 1.0, 2.0, 5.0 }; \n    int c = compare_vals(d1, d2, greater<double>());\n```\n\n`greater`函数类在`<functional>`头中定义，并使用为该类型定义的`operator>`比较两个数字。 如果您希望将容器中的项与固定值进行比较，也就是说，当调用函数器上的`operator()(double, double)`方法时，一个参数始终具有固定值，该怎么办呢？ 一种选择是定义一个有状态的函数器类(如前所述)，这样固定值就是函数器对象的成员。 另一种方法是用固定值填充另一个`vector`，然后继续比较两个`vector`(对于大的`vector`来说，这可能会相当昂贵)。\n\n另一种方法是重用函数器类，但是*将*一个值绑定到它的一个参数。 可以这样编写`compare_vals`函数的一个版本，即只取一个`vector`：\n\n```cpp\n    template<typename Fn>  \n    int compare_vals(vector<double> d, Fn compare) \n    { \n        int c = 0; \n        for (size_t i = 0; i < d.size(); ++ i) \n        { \n            if (compare(d[i]) c++ ; \n        } \n        return c; \n    }\n```\n\n编写代码的目的是只对一个值调用函数器参数，因为假定函数器对象包含要比较的另一个值。 这是通过将函数器类绑定到参数来实现的：\n\n```cpp\n    using namespace::std::placeholders; \n    int c = compare_vals(d1, bind(greater<double>(), _1, 2.0));\n```\n\n`bind`函数是可变的。 第一个参数是函数器对象，后跟将传递给函数器的`operator()`方法的参数。 向`compare_vals`函数传递一个**绑定器**对象，该对象将函数绑定到值。 在`compare_vals`函数中，对`compare(d[i])`中的函数器的调用实际上是对绑定器对象的`operator()`方法的调用，该方法将参数`d[i]`和绑定值转发给函数器的`operator()`方法。\n\n在对`bind`的调用中，如果提供了实际值(这里为`2.0`)，则该值将传递给函数调用中该位置的函数(这里，`2,0`传递给第二个参数)。 如果使用前面有下划线的符号，则它是**占位符**。 在`std::placeholders`名称空间中定义了 20 个这样的符号(`_1`到`_20`)。 占位符的意思是“将在此位置传递的值用于活页夹对象`operator()`方法调用，以调用占位符指示的函数器调用`operator()`方法。” 因此，此调用中的占位符表示“传递调用活页夹的第一个参数，并将其传递给`greater`函数`operator()`的第一个参数。”\n\n前面的代码将`vector`中的每一项与`2.0`进行比较，并保留大于`2.0`的项的计数。 您可以这样调用它：\n\n```cpp\n    int c = compare(d1, bind(greater<double>(), 2.0, _1));\n```\n\n参数列表被交换，这意味着`2.0`将与`vector`中的每个项目进行比较，并且函数将保留`2.0`大于该项目的次数的计数。\n\n`bind`函数和占位符是 C++ 11 新增的。在以前的版本中，您可以使用`bind1st`和`bind2nd`函数将值绑定到函数的第一个或第二个参数。\n\n# 定义转换运算符\n\n我们已经看到，如果您的自定义类型具有接受您要转换的类型的构造函数，则可以使用构造函数将其从另一个类型转换为您的自定义类型。 您还可以在另一个方向上执行转换：将对象转换为另一种类型。 为此，您需要为不带返回类型的运算符提供要转换到的类型的名称。 在这种情况下，您需要在`operator`关键字和名称之间留一个空格：\n\n```cpp\n    class mytype \n    { \n        int i; \n    public: \n        mytype(int i) : i(i) {} \n        explicit mytype(string s) : i(s.size()) {} \n        operator int () const { return i; } \n    };\n```\n\n此代码可以将`int`或`string`转换为`mytype`；在后一种情况下，只能通过显式提到构造函数。 最后一行允许您将对象转换回`int`：\n\n```cpp\n    string s = \"hello\"; \n    mytype t = mytype(s); // explicit conversion \n    int i = t;            // implicit conversion\n```\n\n您可以设置这样的转换运算符`explicit`，以便只有在使用显式强制转换时才会调用它们。 在许多情况下，您可能不想使用此关键字，因为当您希望将资源包装在类中并使用析构函数为您执行自动资源管理时，隐式转换非常有用。\n\n使用转换操作符的另一个示例是从有状态函数器返回值。 这里的想法是，`operator()`将执行一些操作，结果由函数器维护。 问题是如何获得函数器的这种状态，特别是当它们经常被创建为临时对象时？ 转换操作符可以提供此功能。\n\n例如，当您计算平均值时，分两个阶段进行：第一个阶段是累加值，第二个阶段是通过除以项目数来计算平均值。 下面的函数类通过在转换为`double`的过程中执行除法来实现这一点：\n\n```cpp\n    class averager \n    { \n        double total; \n        int count; \n    public: \n        averager() : total(0), count(0) {} \n        void operator()(double d) { total += d; count += 1; } \n        operator double() const \n        {        \n            return (count != 0) ? (total / count) : \n                numeric_limits<double>::signaling_NaN(); \n        } \n    };\n```\n\n这可以这样称呼：\n\n```cpp\n    vector<double> vals { 100.0, 20.0, 30.0 }; \n    double avg = for_each(vals.begin(), vals.end(), averager());\n```\n\n`for_each`函数为`vector`中的每一项调用函数器，而`operator()`只是对传递给它的项求和并维护计数。 有趣的是，在`for_each`函数迭代了`vector`中的所有项之后，它返回函数器，因此存在到`double`的隐式转换，后者调用计算平均值的转换操作符。\n\n# 管理资源\n\n我们已经看到一种需要仔细管理的资源：内存。 使用`new`分配内存，使用完内存后，必须使用`delete`释放内存。 释放内存失败将导致内存泄漏。 内存可能是最基本的系统资源，但大多数操作系统还有许多其他资源：文件句柄、图形对象句柄、同步对象、线程和进程。 有时，拥有这样的资源是独占的，会阻止其他代码访问通过该资源访问的资源。 因此，重要的是要在某个时候释放这些资源，并且通常要及时释放它们。\n\n在这里，类通过 C++ 的作者 Bjarne Stroustrup 发明的称为**Resource Acquisition is Initialization**(RAII)的机制提供帮助。 简而言之，资源在对象的构造函数中分配，在析构函数中释放，因此这意味着资源的生存期就是对象的生存期。 通常，这样的包装器对象是在堆栈上分配的，这意味着当对象超出作用域*时，您可以保证资源将被释放，无论这种情况是如何发生的*。\n\n因此，如果在代码块中为循环语句(`while`，`for`)声明了对象，则在每个循环结束时将调用每个对象的析构函数(按创建的相反顺序)，并且在循环重复时将再次创建该对象。 无论循环是因为已到达代码块的末尾而重复，还是通过调用`continue`重复循环，都会发生这种情况。 另一种退出代码块的方法是调用`break`、a`goto`，或者如果代码调用`return`退出函数。 如果代码引发异常(参见[第 10 章](10.html)，*诊断和调试*)，则当对象超出范围时将调用析构函数，因此如果代码由`try`块保护，则在调用`catch`子句之前将调用块中声明的对象的析构函数。 如果没有保护块，则在销毁函数堆栈和传播异常之前将调用析构函数。\n\n# 编写包装类\n\n在编写类来包装资源时，您必须解决几个问题。 构造函数将用于使用某个库函数(通常通过某种不透明的句柄访问)获取资源，或者将该资源作为参数。 此资源存储为数据成员，以便类上的其他方法可以使用它。 资源将使用您的库提供的任何函数在析构函数中释放。 这是最低限度。 此外，您还必须考虑如何使用对象。 通常，如果您可以像使用资源句柄一样使用实例，这样的包装类是最方便的。 这意味着您可以保持相同的编程风格来访问资源，但不必太担心资源的释放。\n\n您应该考虑是否希望能够在包装类和资源句柄之间进行转换。 如果您确实允许这样做，这意味着您可能必须考虑克隆资源，这样您就不会有句柄的两个副本--一个由类管理，另一个副本可以由外部代码释放。 您还需要考虑是否允许复制或分配对象，如果允许，则需要适当地实现复制构造函数、移动构造函数以及复制和移动赋值操作符。\n\n# 使用智能指针\n\nC++ 标准库提供了几个类来包装通过指针访问的资源。 为了防止内存泄漏，您必须确保在空闲存储上分配的内存在某个时候被释放。 智能指针的思想是将实例视为指针，因此使用`*`运算符取消引用以访问它所指向的对象，或使用`->`运算符访问包装对象的成员。 智能指针类将管理其包装的指针的生存期，并将适当地释放资源。\n\n标准库有三个智能指针类：`unique_ptr`、`shared_ptr`和`weak_ptr`。 每个函数处理如何以不同的方式释放资源，以及如何或是否可以复制指针。\n\n# 管理独占所有权\n\n`unique_ptr`类是用指向它将维护的对象的指针构造的。 该类提供操作符`*`来提供对对象的访问，取消对包装指针的引用。 它还提供了`->`运算符，因此如果指针指向某个类，则可以通过包装指针访问成员。\n\n以下命令在空闲存储上分配对象并手动维护其生存期：\n\n```cpp\n    void f1() \n    { \n       int* p = new int; \n       *p = 42; \n       cout << *p << endl; \n       delete p; \n    }\n```\n\n在本例中，您将获得一个指针，指向为`int`分配的空闲存储上的内存。 要访问内存--无论是写入内存还是从内存读取--使用`*`操作符取消对指针的引用。 使用完指针后，必须调用`delete`来释放内存并将其返回到空闲存储。 现在考虑相同的代码，但使用智能指针：\n\n```cpp\n    void f2() \n    { \n       unique_ptr<int> p(new int); \n       *p = 42; \n       cout << *p << endl; \n       delete p.release(); \n    }\n```\n\n两个主要区别在于，智能指针对象是通过调用构造函数显式构造的，该构造函数接受用作模板参数的类型的指针。 此模式强化了资源只应由智能指针管理的想法。\n\n第二个更改是通过调用智能指针对象上的`release`方法来获得包装指针的所有权，从而释放内存，这样我们就可以显式删除指针。\n\n考虑一下`release`方法，它将指针从智能指针的所有权中释放出来。 在此调用之后，智能指针不再包装资源。 `unique_ptr`类还有一个方法`get`，它将提供对包装指针的访问，但智能指针对象仍将保留所有权；*不要删除通过这种方式获得的指针*！\n\n请注意，`unique_ptr`对象包装了一个指针，并且只包装了该指针。 这意味着对象在内存中的大小与它包装的指针的大小相同。 到目前为止，智能指针添加的内容很少，所以让我们看看另一种释放资源的方法：\n\n```cpp\n    void f3() \n    { \n       unique_ptr<int> p(new int); \n       *p = 42; \n       cout << *p << endl; \n       p.reset(); \n    }\n```\n\n这是资源的*确定性*释放，意味着资源恰好在您希望它发生的时候释放，这与指针的情况类似。 这里的代码没有释放资源本身；它允许智能指针使用**删除器**来释放资源。 `unique_ptr`的默认删除器是一个名为`default_delete`的函数器类，它调用换行指针上的`delete`运算符。 如果您打算使用确定性销毁，`reset`是首选方法。 您可以通过将自定义函数器类的类型作为第二个参数传递给`unique_ptr`模板来提供您自己的删除器：\n\n```cpp\n    template<typename T> struct my_deleter \n    { \n        void operator()(T* ptr)  \n        { \n            cout << \"deleted the object!\" << endl; \n            delete ptr; \n        } \n    };\n```\n\n在代码中，您将指定需要自定义删除器，如下所示：\n\n```cpp\n    unique_ptr<int, my_deleter<int> > p(new int);\n```\n\n在删除指针之前，您可能需要执行额外的清理，或者指针可能是由`new`以外的机制获得的，因此您可以使用自定义删除器来确保调用适当的释放函数。 请注意，删除器是智能指针类的一部分，因此，如果您有两个不同的智能指针以这种方式使用两个不同的删除器，则即使它们包装相同类型的资源，智能指针类型也是不同的。\n\nWhen you use a custom deleter, the size of a `unique_ptr` object may be larger than the pointer wrapped. If the deleter is a functor object, each smart pointer object will need memory for this, but if you use a lambda expression, no more extra space will be required.\n\n当然，您最有可能允许智能指针为您管理资源生存期，要做到这一点，您只需允许智能指针对象超出作用域：\n\n```cpp\n    void f4() \n    { \n       unique_ptr<int> p(new int); \n       *p = 42; \n       cout << *p << endl; \n    } // memory is deleted\n```\n\n由于创建的指针是单个对象，这意味着您可以在适当的构造函数上调用`new`运算符来传递初始化参数。 向`unique_ptr`的构造函数传递一个指向已经构造的对象的指针，该类在此之后管理该对象的生存期。 虽然`unique_ptr`对象可以通过调用其构造函数直接创建，但不能调用复制构造函数，因此不能在构造期间使用初始化语法。 相反，标准库提供了一个名为`make_unique`的函数。 它有几个重载，因此它是基于此类创建智能指针的首选方式：\n\n```cpp\n    void f5() \n    { \n       unique_ptr<int> p = make_unique<int>(); \n       *p = 42; \n       cout << *p << endl; \n    } // memory is deleted\n```\n\n此代码将调用包装类型(`int`)上的默认构造函数，但您可以提供将传递给该类型的相应构造函数的参数。 例如，对于具有两个参数的构造函数的`struct`，可以使用以下内容：\n\n```cpp\n    void f6() \n    { \n       unique_ptr<point> p = make_unique<point>(1.0, 1.0); \n       p->x = 42; \n       cout << p->x << \",\" << p->y << endl; \n    } // memory is deleted\n```\n\n`make_unique`函数调用为成员分配非默认值的构造函数。 `->`操作符返回一个指针，编译器将通过该指针访问对象成员。\n\n对于数组，还有`unique_ptr`和`make_unique`的专门化。 此版本的`unique_ptr`的默认删除程序将在指针上调用`delete[]`，因此它将删除数组中的每个对象(并调用每个对象的析构函数)。 该类实现了索引器操作符(`[]`)，因此您可以访问数组中的每一项。 但是，请注意，没有范围检查，因此，就像内置的数组变量一样，您可以在数组末尾之后进行访问。 没有取消引用运算符(`*`或`->`)，因此只能使用数组语法访问基于数组的`unique_ptr`对象。\n\n`make_unique`函数有一个重载，允许您传递要创建的数组的大小，但您必须单独初始化每个对象：\n\n```cpp\n    unique_ptr<point[]> points = make_unique<point[]>(4);     \n    points[1].x = 10.0; \n    points[1].y = -10.0;\n```\n\n这将创建一个数组，其中四个`point`对象最初设置为默认值，以下几行将第二个点初始化为值`(10.0, -10.0)`。 使用`vector`或`array`来管理对象数组几乎总是比使用`unique_ptr`更好。\n\nEarlier versions of the C++ Standard Library had a smart pointer class called `auto_ptr`. This was a first attempt, and worked in most cases, but also had some limitations; for example, `auto_ptr` objects could not be stored in Standard Library containers. C++ 11 introduces rvalue references and other language features such as move semantics, and, through these, `unique_ptr` objects can be stored in containers. The `auto_ptr` class is still available through the `<new>` header, but only so that older code can still compile.\n\n关于`unique_ptr`类的重要一点是，它确保指针只有一个副本。 这一点很重要，因为类析构函数将释放资源，因此如果您*可以*复制`unique_ptr`对象，则意味着将有多个析构函数尝试释放资源。 `unique_ptr`的对象拥有*独占所有权*；实例总是拥有它所指向的东西。\n\n不能复制 ASSIGN`unique_ptr`智能指针(复制赋值运算符和复制构造函数已删除)，但可以通过将资源的所有权从源指针转移到目标指针来*移动*它们。 因此，函数可以返回`unique_ptr`，因为所有权是通过 Move 语义转移给被赋给函数值的变量的。 如果将智能指针放入容器中，则会有另一个移动。\n\n# 共享所有权\n\n在某些情况下，您需要共享一个指针：您可以创建多个对象，然后将指向单个对象的指针传递给每个对象，以便它们可以调用此对象。 通常，当一个对象具有指向另一个对象的指针时，该指针表示应该在销毁包含对象期间销毁的资源。 如果一个指针是共享的，这意味着当其中一个对象删除该指针时，所有其他对象中的指针都将无效(这称为**悬挂指针**，因为它不再指向某个对象)。 您需要一种机制，其中多个对象可以持有一个指针，该指针将一直保持有效，直到*所有使用该指针的*对象都表示它们将不再需要使用它。\n\nC++ 11 为该工具提供了`shared_ptr`类。 该类在资源上维护**个引用计数**，该资源的每个`shared_ptr`副本都会增加引用计数。 当该资源的`shared_ptr`的一个实例被销毁时，它将递减引用计数。 引用计数是共享的，因此它意味着非零值表示至少存在一个`shared_ptr`在访问资源。 当最后一个`shared_ptr`对象将引用计数递减到零时，就可以安全地释放资源了。 这意味着必须以原子方式管理引用计数才能处理多线程代码。\n\n由于引用计数是共享的，这意味着每个`shared_ptr`对象持有一个指向称为**控制块**的共享缓冲区的指针，这意味着它持有原始指针和指向控制块的指针，因此每个`shared_ptr`对象将比`unique_ptr`对象持有更多的数据。 控制块不仅仅用于参考计数。\n可以创建`shared_ptr`对象以使用自定义删除器(作为构造函数参数传递)，并且删除器存储在控制块中。 这一点很重要，因为这意味着自定义删除器不是智能指针类型的一部分，因此包装相同资源类型但使用不同删除器的几个`shared_ptr`对象仍然是同一类型，并且可以放入该类型的容器中。\n\n您可以从另一个`shared_ptr`对象创建`shared_ptr`对象，这将使用原始指针和指向控制块的指针来初始化新对象，*和*会递增引用计数。\n\n```cpp\n    point* p = new point(1.0, 1.0); \n    shared_ptr<point> sp1(p); // Important, do not use p after this! \n    shared_ptr<point> sp2(sp1); \n    p = nullptr; \n    sp2->x = 2.0; \n    sp1->y = 2.0; \n    sp1.reset(); // get rid of one shared pointer\n```\n\n这里，第一个共享指针是使用原始指针创建的。 这不是建议使用`shared_ptr`的方式。 第二个共享指针是使用第一个智能指针创建的，因此现在有两个指向同一资源的共享指针(将`p`分配给`nullptr`以防止其进一步使用)。 此后，可以使用`sp1`或`sp2`访问*相同的*资源。 在这段代码的末尾，一个共享指针被重置为`nullptr`；这意味着`sp1`不再具有对资源的引用计数，您不能使用它来访问资源。 但是，您仍然可以使用`sp2`访问资源，直到它超出范围，或者调用`reset`。\n\n在此代码中，智能指针是从单独的原始指针创建的。 因为共享指针现在已经接管了资源的生命周期管理，所以不再使用原始指针很重要，在本例中，它被分配给`nullptr`。 最好避免使用原始指针，标准库通过一个名为`make_shared`的函数来实现这一点，该函数的用法如下：\n\n```cpp\n    shared_ptr<point> sp1 = make_shared<point>(1.0,1.0);\n```\n\n该函数将使用对`new`的调用创建指定的对象，由于它接受数量可变的参数，因此您可以使用它来调用包装类上的任何构造函数。\n\n您可以从`unique_ptr`对象创建`shared_ptr`对象，这意味着指针将*移动*到新对象，并创建引用计数控制块。 由于资源现在将被共享，这意味着资源上不再有独占所有权，因此`unique_ptr`对象中的指针将成为`nullptr`。 这意味着您可以拥有一个工厂函数，该函数返回一个指向包装在`unique_ptr`对象中的对象的指针，并且调用代码可以确定它是使用`unique_ptr`对象来获得对资源的独占访问，还是使用`shared_ptr`对象来共享它。\n\n将`shared_ptr`用于对象数组几乎没有什么意义；存储对象集合(`vector`或`array`)有更好的方法。 在任何情况下，都有一个索引运算符(`[]`)，默认删除器调用的是`delete`，而不是`delete[]`。\n\n# 处理悬空指针\n\n在本书的前面，我们已经指出，当您删除资源时，应该将指针设置为`nullptr`，并且应该在使用指针之前检查它是否为`nullptr`。 这是为了使您不会为已删除的对象调用指向内存的指针：悬挂指针。\n\n在某些情况下，可能会故意出现悬空指针。 例如，*父*对象可以创建具有指向父对象的**向后指针**的*子*对象，以便子对象可以访问父对象。 (这方面的一个示例是创建子控件的窗口；子控件访问父窗口通常很有用。)。 在这种情况下使用共享指针的问题在于，父控件对每个子控件都有引用计数，而每个子控件对父控件都有引用计数，这就产生了循环依赖关系。\n\n另一个例子是，如果您有一个包含观察者对象的容器，目的是能够通过调用每个观察者对象上的一个方法，在事件发生时通知每个观察者对象。 维护此列表可能很复杂，特别是在可以删除观察者对象的情况下，因此在完全删除对象之前，您必须提供一种从容器中删除对象的方法(其中将有`shared_ptr`个引用计数)。 如果您的代码可以简单地将指向对象的指针添加到容器中，而不维护引用计数，但允许您检查何时使用指针(如果指针悬空或指向现有对象)，则会变得更容易。\n\n这样的指针称为**弱指针**，C++ 11 标准库提供了一个名为`weak_ptr`的类。 您不能直接使用`weak_ptr`对象，并且没有取消引用运算符。 相反，您可以从`shared_ptr`对象创建一个`weak_ptr`对象，当您想要访问资源时，可以从`weak_ptr`对象创建一个`shared_ptr`对象。 这意味着`weak_ptr`对象具有与`shared_ptr`对象相同的原始指针，并访问相同的控制块，但它不参与引用计数。\n\n创建后，`weak_ptr`对象将使您能够测试包装指针是指向现有资源还是指向已销毁的资源。 有两种方法可以做到这一点：要么调用成员函数`expired`，要么尝试从`weak_ptr`创建一个`shared_ptr`。 如果要维护`weak_ptr`个对象的集合，可以决定定期迭代该集合，对每个对象调用`expired`，如果该方法返回`true`，则从集合中删除该对象。 由于`weak_ptr`对象可以访问由原始`shared_ptr`对象创建的控制块，因此它可以测试以查看引用计数是否为零。\n\n测试`weak_ptr`对象是否悬挂的第二种方法是从它创建一个`shared_ptr`对象。 有两种选择。 您可以通过将弱指针传递给其构造函数来创建`shared_ptr`对象，如果该指针已过期，则构造函数将抛出一个`bad_weak_ptr`异常。 另一种方法是调用弱指针上的`lock`方法，如果弱指针已过期，则会将`shared_ptr`对象赋给`nullptr`，您可以对此进行测试。 这三种方式如下所示：\n\n```cpp\n    shared_ptr<point> sp1 = make_shared<point>(1.0,1.0); \n    weak_ptr<point> wp(sp1); \n\n    // code that may call sp1.reset() or may not \n\n    if (!wp.expired())  { /* can use the resource */} \n\n    shared_ptr<point> sp2 = wp.lock(); \n    if (sp2 != nullptr) { /* can use the resource */} \n\n    try \n    { \n        shared_ptr<point> sp3(wp); \n        // use the pointer \n    } \n    catch(bad_weak_ptr& e) \n    { \n        // dangling weak pointer \n    }\n```\n\n由于弱指针不会改变资源上的引用计数，这意味着您可以将其用于反向指针，以打破循环依赖(尽管，通常使用原始指针是有意义的，因为子对象在没有父对象的情况下不能存在)。\n\n# 模板\n\n类可以模板化，这意味着您可以编写泛型代码，编译器将使用代码使用的类型生成一个类。 参数可以是类型、常量整数值或可变版本(零个或多个参数，由使用该类的代码提供)。 例如：\n\n```cpp\n    template <int N, typename T> class simple_array \n    { \n        T data[N]; \n    public: \n        const T* begin() const { return data; } \n        const T* end() const { return data + N; } \n        int size() const { return N; } \n\n        T& operator[](int idx)  \n        { \n            if (idx < 0 || idx >= N) \n                throw range_error(\"Range 0 to \" + to_string(N)); \n            return data[idx]; \n        }  \n    };\n```\n\n下面是一个非常简单的数组类，它定义了基本的迭代器函数和索引运算符，因此您可以这样调用它：\n\n```cpp\n    simple_array<4, int> four; \n    four[0] = 10; four[1] = 20; four[2] = 30; four[3] = 40; \n    for(int i : four) cout << i << \" \"; // 10 20 30 40 \n    cout << endl; \n    four[4] = -99;            // throws a range_error exception\n```\n\n如果选择在`class`声明之外定义函数，则需要将模板及其参数作为`class`名称的一部分提供：\n\n```cpp\n    template<int N, typename T> \n    T& simple_array<N,T>::operator[](int idx) \n    { \n        if (idx < 0 || idx >= N) \n            throw range_error(\"Range 0 to \" + to_string(N)); \n        return data[idx]; \n    }\n```\n\n您还可以设置模板参数的默认值：\n\n```cpp\n    template<int N, typename T=int> class simple_array \n    { \n        // same as before \n    };\n```\n\n如果您认为应该为模板参数提供特定的实现，则可以提供该版本的代码作为模板的专门化：\n\n```cpp\n    template<int N> class simple_array<N, char> \n    { \n        char data[N]; \n    public: \n        simple_array<N, char>(const char* str)  \n        {  \n            strncpy(data, str, N);  \n        } \n        int size() const { return N; } \n        char& operator[](int idx) \n        { \n            if (idx < 0 || idx >= N) \n                throw range_error(\"Range 0 to \" + to_string(N)); \n            return data[idx]; \n        } \n        operator const char*() const { return data; } \n    };\n```\n\n请注意，使用专门化时，您不会从完全模板化的类中获得任何代码；您必须实现想要提供的所有方法，并且如此处所示，实现与专门化相关但在完全模板化的类中不可用的方法。 本例是**部分专门化**，这意味着它只专门化一个参数(`T`，数据类型)。 该类将用于`simple_array<n, char>`类型的声明变量，其中`n`是整数。 您可以自由拥有完全专门化的模板，在本例中，它将是固定大小和指定类型的专门化：\n\n```cpp\n    template<> class simple_array<256, char> \n    { \n        char data[256]; \n    public: \n        // etc \n    };\n```\n\n它在这种情况下可能没有什么用处，但其想法是，对于需要 256 个字符的变量，将有特殊的代码。\n\n# 使用类\n\n**资源获取是初始化**技术对于管理其他库(如 C 运行时库或 Windows SDK)提供的资源很有用。 它简化了您的代码，因为您不必考虑资源句柄将在哪里超出范围，并在每个点提供清理代码。 如果清理代码很复杂，在 C 代码中通常会看到它放在函数的末尾，函数中的每个出口点都会有一个`goto`跳转到该代码。 这会导致代码混乱。 在本例中，我们将用类包装 C 文件函数，以便自动维护文件句柄的生存期。\n\nC 运行时`_findfirst`和`_findnext`函数允许您搜索匹配模式(包括通配符)的文件或目录。 函数`_findfirst`返回与该搜索相关的`intptr_t`，并将其传递给`_findnext`函数以获得后续值。 此`intptr_t`是指向 C 运行时为搜索维护的资源的不透明指针，因此当您完成搜索时，您必须调用`_findclose`来清除与其相关的任何资源。 为了防止内存泄漏，调用`_findclose`很重要。\n\n在`Beginning_C++ `文件夹下，创建一个名为`Chapter_06`的文件夹。 在 Visual C++ 中，创建一个新的 C++ 源文件，将其保存到`Chapter_06`文件夹，并将其命名为`search.cpp`。 应用将使用 Standard Library 控制台和字符串，并将使用 C Runtime 文件函数，因此请将以下行添加到文件的顶部：\n\n```cpp\n    #include <iostream> \n    #include <string> \n    #include <io.h> \n    using namespace std;\n```\n\n将使用文件搜索模式调用应用，并且它将使用 C 函数搜索文件，因此您需要一个带参数的`main`函数。 将以下内容添加到文件底部：\n\n```cpp\n    void usage() \n    { \n        cout << \"usage: search pattern\" << endl; \n        cout << \"pattern is the file or folder to search for \" \n             << \"with or without wildcards * and ?\" << endl; \n    } \n\n    int main(int argc, char* argv[]) \n    { \n        if (argc < 2) \n        { \n            usage(); \n            return 1; \n        } \n    }\n```\n\n第一件事是为管理该资源的搜索句柄创建一个包装类。 在 Usage 函数上方，添加一个名为`search_handle`的类：\n\n```cpp\n    class search_handle \n    { \n        intptr_t handle; \n    public: \n        search_handle() : handle(-1) {} \n        search_handle(intptr_t p) : handle(p) {} \n        void operator=(intptr_t p) { handle = p; } \n        void close()  \n        { if (handle != -1) _findclose(handle); handle = 0; } \n        ~search_handle() { close(); } \n    };\n```\n\n这个类有一个单独的函数来释放句柄。 这是为了让该类的用户能够尽快释放包装资源。 如果在可能引发异常的代码中使用该对象，则不会直接调用`close`方法，而是调用析构函数。 包装器对象可以使用`intptr_t`值创建。 如果此值为-1，则句柄无效，因此只有在句柄没有此值时，Close 方法才会调用`_findclose`。\n\n我们希望该类的对象拥有句柄的独占所有权，因此通过将以下内容放入类的公共部分来删除复制构造函数和复制赋值：\n\n```cpp\n    void operator=(intptr_t p) { handle = p; } \n search_handle(search_handle& h) = delete; void operator=(search_handle& h) = delete;\n```\n\n如果移动了对象，则必须释放现有对象中的任何句柄，因此在刚添加的行后添加以下内容：\n\n```cpp\n    search_handle(search_handle&& h)  { close(); handle = h.handle; } \n    void operator=(search_handle&& h) { close(); handle = h.handle; }\n```\n\n包装器类将通过调用`_findfirst`来分配，并将传递给对`_findnext`的调用，因此包装器类需要两个运算符：一个用于转换为`intptr_t`，以便该类的对象可以在需要`intptr_t`的任何地方使用；另一个用于在需要`bool`时使用对象。 将这些内容添加到类的`public`部分：\n\n```cpp\n    operator bool() const { return (handle != -1); } \n    operator intptr_t() const { return handle; }\n```\n\n转换为`bool`后，您可以编写如下代码：\n\n```cpp\n    search_handle handle = /* initialize it */; \n    if (!handle) { /* handle is invalid */ }\n```\n\n如果您有一个返回指针的转换操作符，那么编译器将在转换到`bool`之前调用它。\n\n您应该能够编译这段代码(记住使用`/EHsc`开关)以确认没有输入错误。\n\n接下来，编写一个包装类来执行搜索。 在`search_handle`类下面添加一个`file_search`类：\n\n```cpp\n    class file_search \n    { \n        search_handle handle; \n        string search; \n    public: \n        file_search(const char* str) : search(str) {} \n        file_search(const string& str) : search(str) {} \n    };\n```\n\n这个类是用搜索条件创建的，我们可以选择传递一个 C 或 C++ 字符串。 该类有一个`search_handle`数据成员，由于默认析构函数将调用成员对象的析构函数，因此我们自己不需要提供析构函数。 但是，我们将添加一个`close`方法，以便用户可以显式释放资源。 此外，为了让类的用户可以确定搜索路径，我们需要一个访问器。 在类的底部，添加以下内容：\n\n```cpp\n    const char* path() const { return search.c_str(); } \n    void close() { handle.close(); }\n```\n\n我们不希望复制`file_search`对象的实例，因为这将意味着搜索句柄的两个副本。 您可以删除复制构造函数和赋值运算符，但没有必要。 试试这个：在`main`函数中，添加此测试代码(在哪里并不重要)：\n\n```cpp\n    file_search f1(\"\"); \n    file_search f2 = f1;\n```\n\n编译代码。 您将看到一个错误和一个解释：\n\n```cpp\n error C2280: 'file_search::file_search(file_search &)': attempting to reference a deleted function\n note: compiler has generated 'file_search::file_search' here\n```\n\n如果没有复制构造函数，编译器将生成一个复制构造函数(这是第二行)。 第一行有点奇怪，因为它说明您正在尝试调用编译器生成的已删除方法！ 实际上，错误是说生成的复制构造函数试图复制已删除的`handle`数据成员和`search_handle`复制构造函数。 因此，您可以在不添加任何其他代码的情况下防止复制`file_search`对象。 删除您刚刚添加的测试线。\n\n接下来，将以下行添加到`main`函数的底部。 这将创建一个`file_search`对象并将信息打印到控制台。\n\n```cpp\n    file_search files(argv[1]); \n    cout << \"searching for \" << files.path() << endl;\n```\n\n然后，您需要添加代码来执行搜索。 这里使用的模式将是一个具有 out 参数并返回`bool`的方法。 如果对该方法的调用成功，则在 out 参数中将返回找到的文件，并且该方法将返回`true`。 如果调用失败，则 out 参数保持不变，该方法返回`false`。 在`file_search`类的`public`部分，添加此函数：\n\n```cpp\n    bool next(string& ret) \n    { \n        _finddata_t find{}; \n        if (!handle) \n        { \n            handle = _findfirst(search.c_str(), &find); \n            if (!handle) return false; \n        } \n        else \n        { \n            if (-1 == _findnext(handle, &find)) return false; \n        } \n\n        ret = find.name; \n        return true; \n    }\n```\n\n如果这是第一次调用此方法，则`handle`将无效，因此将调用`_findfirst`。 这将用搜索结果填充`_finddata_t`结构，并返回`intptr_t`值。 将`search_handle`对象数据成员赋给此函数返回的值，如果`_findfirst`返回`-1`，则该方法返回`false`。 如果调用成功，则使用`_finddata_t`结构中的 C 字符串指针初始化 OUT 参数(对`string`的引用)。\n\n如果有更多的文件与模式匹配，则可以重复调用`next`函数，然后在这些后续调用中调用`_findnext`函数以获取下一个文件。 在本例中，`search_handle`对象被传递给函数，并通过类的转换操作符隐式转换为`intptr_t`。 如果`_findnext`函数返回`-1`，则表示搜索中没有更多的文件。\n\n在`main`函数的底部，添加以下行以执行搜索：\n\n```cpp\n    string file; \n    while (files.next(file)) \n    { \n        cout << file << endl; \n    }\n```\n\n现在，您可以编译代码并使用搜索条件运行它。 请记住，这受`_findfirst`/`_findnext`函数功能的限制，因此您可以执行的搜索将非常简单。 尝试在命令行中使用参数运行此命令，以搜索`Beginning_C++ `文件夹中的子文件夹：\n\n```cpp\n search Beginning_C++ Ch*\n```\n\n这将给出子文件夹的列表，这些子文件夹以`Ch`开头。 由于`search_handle`没有理由成为单独的类，因此将整个类移动到`search_handle`的`private`部分，位于`handle`数据成员声明的上方。 编译并运行代码。\n\n# 简略的 / 概括的 / 简易判罪的 / 简易的\n\n通过类，C++ 提供了一种强大而灵活的机制来封装数据，并提供了方法来提供作用于数据的行为。 您可以将此代码作为模板，这样您就可以编写泛型代码，并让编译器为您需要的类型生成代码。 在本例中，您已经看到类是面向对象的基础。 类封装数据，因此调用方只需要知道预期的行为(在本例中，获取搜索中的下一个结果)，而不需要知道类如何做到这一点的细节。 在下一章中，我们将进一步研究类的特性；特别是通过继承实现代码重用。***"
  },
  {
    "path": "docs/begin-cpp-prog/07.md",
    "content": "# 七、面向对象编程导论\n\n到目前为止，您已经了解了如何在函数中模块化代码，以及如何用类中的代码封装数据。 您还了解了如何使用模板编写泛型代码。 类和封装允许您将代码和数据组合为一个对象。 在本章中，您将学习如何通过继承和组合*重用*代码，以及如何使用类继承来编写面向对象的代码。\n\n# 传承与组合\n\n到目前为止，您看到的类都是完整的类：您可以在免费存储或堆栈上创建类的实例。 之所以可以这样做，是因为已经定义了类的数据成员，因此可以计算对象需要多少内存，并且已经提供了类的全部功能。 这些被称为**具体类**。\n\n如果您在一个类中有一个被证明有用的例程，并且您想在一个新的类中重用，那么您有几个选择。 第一种称为**合成**。 通过组合，您可以将实用程序类的实例添加为将使用该例程的类的数据成员。 一个简单的例子是`string`类--它提供了您想要从字符串中获得的所有功能。 它将根据必须存储的字符数分配内存，并在字符串对象被销毁时释放其使用的内存。 您的类使用字符串的功能，但它本身不是字符串，因此它将字符串作为数据成员。\n\n第二个选项是使用**继承**。 使用继承的方法有很多种，本章将提到其中的一些。 简而言之，继承是当一个类*扩展*另一个类时，被扩展的类称为**基类**、**父类**或**超类**，进行扩展的类称为**派生类**、**子类**或**子类**。 但是，对于继承，有一个重要的概念需要理解：派生类与基类的关系。 它通常以**IS-a**的形式给出。 如果派生类是基类的类型，则关系是继承。 一个 mp3 文件是一个操作系统文件，所以如果您有一个`os_file`类，那么您可以合法地从它派生出一个`mp3_file`类。\n\n派生类具有基类的功能和状态(尽管它可能不具有对它们的完全访问权限，稍后将对其进行解释)，因此它可以使用基类的功能。 在本例中，它类似于作曲。 然而，两者之间存在着显著的差异。 通常，在合成中，合成的对象由类使用，而不是直接向类的客户端公开。 通过继承，派生类的对象是基类的对象，因此客户端代码通常会看到基类功能。 但是，派生类可以隐藏基类的功能，因此客户端代码将看不到隐藏的基类成员，并且派生类可以重写基类方法并提供其自己的版本。\n\n对于应该使用继承还是组合来重用代码，C++ 社区中有很多分歧，而且每种方法都有优缺点。 两者都不是十全十美的，通常需要妥协。\n\n# 从类继承\n\n考虑一个包装操作系统的类。 这将提供许多方法来访问通过调用操作系统函数获得的文件的创建日期、修改日期和大小等信息。 它还可以提供打开文件、关闭文件、将文件映射到内存以及其他有用功能的方法。 以下是几位这样的成员：\n\n```cpp\n    class os_file \n    { \n        const string file_name; \n        int file_handle; \n        // other data members \n    public: \n        long get_size_in_bytes(); \n        // other methods \n    };\n```\n\nMp3 文件是操作系统文件，但有其他操作系统功能可以访问其数据。 我们可以决定创建一个派生自`os_file`的`mp3_file`类，使其具有操作系统文件的功能，并使用 mp3 文件的功能对其进行扩展：\n\n```cpp\n    class mp3_file : public os_file \n    { \n        long length_in_secs; \n        // other data members \n    public: \n        long get_length_in_seconds(); \n        // other methods \n    };\n```\n\n`mp3_file`类的第一行指示它使用*public**继承*(我们稍后将解释公共继承的含义，但值得指出的是，这是从类派生的最常见方式)。 派生类继承数据成员和方法，派生类的用户可以通过派生类使用基类的成员，但要遵守访问说明符。 在本例中，如果某些代码有`mp3_file`对象，它可以从`mp3_file`类调用`get_length_in_seconds`方法，也可以从基类调用`get_size_in_bytes`方法，因为该方法是`public`。\n\n基类方法最有可能访问基类数据成员，这说明了重要的一点：派生对象包含基类数据成员。 从概念上讲，在内存中，可以将派生对象视为具有派生对象中定义的额外数据成员的基类对象数据成员。 也就是说，派生对象是基类对象的扩展版本。 这一点如下图所示：\n\n![](img/92734943-7c75-4c41-8840-907b7ddd8b2b.png)\n\n在内存中，`os_file`对象有两个数据成员`file_name`和`file_handle`，而`mp3_file`对象有这两个数据成员和一个额外的数据成员`length_in_secs`。\n\n封装原则在 C++ 中很重要。 虽然`mp3_file`对象包含`file_name`和`file_handle`数据成员，但它们只能由基类方法更改。 在这段代码中，这是通过将它们设置为`private`到`os_file`类来强制执行的。\n\n当创建派生对象时，必须首先创建基对象(使用适当的构造函数)，类似地，当销毁派生对象时，首先销毁对象的派生部分(通过派生类的析构函数)，然后再调用基类析构函数。 使用前面文本中讨论的成员，考虑以下代码片段：\n\n```cpp\n    class os_file \n    { \n    public: \n        os_file(const string& name)  \n            : file_name(name), file_handle(open_file(name)) \n        {} \n        ~os_file() { close_file(file_handle); } \n    }; \n\n    class mp3_file : public os_file \n    { \n    public: \n        mp3_file(const string& name) : os_file(name) {} \n        ~mp3_file() { /* clean up mp3 stuff*/ } \n    };\n```\n\n`open_file`和`close_file`函数是一些操作系统函数，用于打开和关闭操作系统文件。\n\n派生类不再需要执行关闭文件的操作，因为基类析构函数`~os_file`是在调用派生类析构函数之后自动调用的。 `mp3_file`构造函数通过其构造函数成员列表调用基类构造函数。 如果没有显式调用基类构造函数，则编译器将调用基类的默认构造函数作为派生类构造函数的第一个操作。 如果成员列表初始化数据成员，则这些成员将在调用任何基类构造函数后初始化。\n\n# 重写方法和隐藏名称\n\n派生类继承基类的功能(取决于方法的访问级别)，因此可以通过派生类的对象调用基类方法。 派生类可以实现与基类方法具有相同原型的方法，在这种情况下，基类方法*被派生类方法重写*，并且派生类提供该功能。 派生类通常会重写基类方法以提供特定于派生类的功能；但是，它可以通过使用名称解析运算符调用方法来调用基类方法：\n\n```cpp\n    struct base \n    { \n        void f(){ /* do something */ } \n        void g(){ /* do something */ } \n    }; \n\n    struct derived : base \n    { \n        void f() \n        { \n            base::f(); \n            // do more stuff \n        } \n    };\n```\n\n请记住，结构是一种`class`类型，其中成员默认为`public`，继承默认为`public`。\n\n在这里，`base::f`和`base::g`方法将执行该类实例的用户可用的一些操作。 `derived`类继承了这两个方法，由于当`derived`类的实例调用`g`方法时，它不实现方法`g`，因此它们实际上将调用`base::g`方法。 `derived`类实现自己版本的`f`方法，因此当`derived`类的实例调用`f`方法时，它们将调用`derived::f`，而不是基类版本。 在此实现中，我们决定需要基类版本的一些功能，因此`derived::f`显式调用`base::f`方法：\n\n```cpp\n    derived d; \n    d.f(); // calls derived::f \n    d.g(); // calls base::g\n```\n\n在上一个示例中，该方法在提供自己的实现之前首先调用基类版本。 这里没有具体的惯例。 类库有时是专门为您实现的，以便从基类派生并使用类库代码。 类库的文档将说明您是否需要替换基类实现，或者是否需要添加到基类实现中，如果是，您将在代码之前还是之后调用基类方法。\n\n在此示例中，派生类提供了一个具有确切原型的方法，作为基类上的方法来重写它。 事实上，在基类中添加与方法同名的任何方法都会向使用派生实例的客户端代码隐藏该基类方法。 因此，考虑如下实现`derived`类：\n\n```cpp\n    struct derived : base \n    { \n        void f(int i) \n        { \n            base::f(); \n            // do more stuff with i \n        } \n    };\n```\n\n在这种情况下，`base::f`方法对创建`derived`对象的代码隐藏，即使该方法具有不同的原型：\n\n```cpp\n    derived d; \n    d.f(42); // OK \n    d.f();   // won't compile, derived::f(int) hides base::f\n```\n\n具有相同名称的基类方法被隐藏，因此最后一行将不会编译。 但是，您可以通过提供基类名称来显式调用该函数：\n\n```cpp\n    derived d; \n    d.derived::f(42); // same call as above \n    d.base::f();      // call base class method \n    derived *p = &d;  // get an object pointer \n    p->base::f();     // call base class method \n    delete p;\n```\n\n乍一看，这个语法看起来有点奇怪，但是一旦您知道`.`和`->`操作符提供对成员的访问，并且操作符后面的符号是成员的名称，在本例中，使用类名和作用域解析操作符显式指定。\n\n通常，到目前为止显示的代码称为**实现继承**，其中类从基类继承实现。\n\n# 使用指针和引用\n\n在 C++ 中，您可以使用`&`运算符获得指向对象(内置类型或自定义类型)在内存中所在位置的指针。 指针是类型化的，因此使用该指针的代码假定该指针指向该类型的对象的内存布局。 类似地，您可以获取对对象的引用，该引用是该对象的*别名*，也就是说，对该引用的操作发生在该对象上。 指向派生类实例的指针(或引用)可以隐式转换为指向基类对象的指针(或引用)。 这意味着您可以使用基类对象的行为编写作用于基类对象的函数，并且只要参数是指向基类的指针或引用，就可以将任何派生类对象传递给该函数。 该函数既不知道也不关心派生类功能。\n\n您应该将派生对象视为基类对象，并接受它可以用作基类对象。 显然，基类指针只能访问基类上的成员：\n\n![](img/3893edb8-394f-41f6-8150-a82d8a8a097d.png)\n\n如果派生类隐藏了基类的成员，这意味着指向派生类的指针将通过成员名称调用派生版本，但基类指针将只看到基类成员，而看不到派生版本。\n\n如果您有基类指针，则可以使用`static_cast`将其强制转换为派生类指针：\n\n```cpp\n    // bad code \n    void print_y(base *pb) \n    { \n       // be wary of this \n       derived *pd = static_cast<derived*>(pb); \n       cout << \"y = \" << pd->y << endl; \n    } \n\n    void f() \n    { \n       derived d; \n       print_y(&d); // implicit cast to base* \n    }\n```\n\n这里的问题是`print_y`函数如何保证基类指针作为参数传递给特定的派生对象？ 如果没有使用该函数的开发人员的约束，它就不能保证他们永远不会传递不同类型的派生类指针。 即使内存中不包含`derived`对象，`static_cast`运算符也会返回指向该对象的指针。 有一种机制可以对被强制转换的指针执行类型检查，我们将在本章后面介绍这一点。\n\n# 访问级别\n\n到目前为止，我们已经看到类成员的两个访问说明符：`public`和`private`。 在类*和*中声明的成员可以由类*和*中的代码访问，类外的代码可以在对象上访问，或者如果成员是`static`，则使用类名。 在`private`节中声明的成员只能由同一类中的其他成员访问。 派生类可以访问基类的`private`成员，但不能访问`private`成员。 还有第三种类型的成员访问：`protected`。 在`protected`部分中声明的成员可以由同一类中的方法访问，也可以由任何派生类中的方法和朋友访问，但不能由外部代码访问：\n\n```cpp\n    class base \n    { \n    protected: \n        void test(); \n    }; \n\n    class derived : public base \n    { \n    public: \n        void f() { test(); } \n    };\n```\n\n在此代码中，`test`方法可以由`derived`类中的成员调用，但不能由类外的代码调用：\n\n```cpp\n    base b; \n    b.test();  // won't compile \n    derived d; \n    d.f();     // OK \n    d.test();  // won't compile\n```\n\n如果您编写的基类只打算用作基类(客户端代码不应该创建它的实例)，那么使用析构函数`protected`是有意义的：\n\n```cpp\n    class base \n    { \n    public: \n        // methods available through the derived object \n        protected: \n        ~base(){} \n    };\n```\n\n编译器不允许您在空闲存储上创建此类对象，然后使用`delete`销毁，因为此操作符将调用析构函数。 同样，编译器不允许您在堆栈上创建对象，因为当对象超出作用域时，编译器将调用不可访问的析构函数。 此析构函数将通过派生类的析构函数调用，因此可以确保对基类进行正确的清理。 这种模式并不意味着您总是打算只使用指向派生类的指针通过调用`delete`操作符来销毁对象。\n\n# 通过继承更改访问级别\n\n重写派生类中的方法时，对该方法的访问由派生类定义。 因此，如果基类方法是`protected`或`public`，则派生类可以更改访问权限：\n\n```cpp\n    class base \n    { \n        protected: \n        void f(); \n    public: \n        void g(); \n    }; \n\n    class derived : public base \n    { \n    public: \n        void f(); \n        protected: \n        void g(); \n    };\n```\n\n在前面的示例中，`base::f`方法是`protected`，因此只有`derived`类可以访问它。 `derived`类覆盖此方法(如果使用完全限定名，则可以调用基类方法)并使其成为`public`。 类似地，`base::g`方法是`public`，但是`derived`类覆盖了该方法并使其成为`protected`(如果需要，还可以使该方法成为`private`)。\n\n还可以使用`using`语句将派生类中的`protected`基类公开为`public`成员：\n\n```cpp\n    class base \n    { \n    protected: \n        void f(){ /* code */}; \n    }; \n\n    class derived: public base \n    { \n    public: \n        using base::f; \n    };\n```\n\n现在，`derived::f`方法是`public`，没有派生类创建新方法。 此功能的更好用途是创建一个方法`private`，使其不可用于派生类(或者，如果它是`public`，则通过实例)，或者使其成为`protected`，以便外部代码无法访问该成员：\n\n```cpp\n    class base \n    { \n    public: \n        void f(); \n    }; \n\n    class derived: public base \n    { \n    protected: \n        using base::f; \n    };\n```\n\n前面的代码可以这样使用：\n\n```cpp\n    base b; \n    b.f(); // OK \n    derived d; \n    d.f(); // won't compile\n```\n\n最后一行不能编译，因为`f`方法是`protected`。 如果要使该方法仅在派生类中可用，而不能在可能从它派生的任何类中使用，则可以在派生类的`private`部分中使用`using`语句；这类似于删除基类方法：\n\n```cpp\n    class derived: public base \n    { \n    public: \n        void f() = delete; \n\n        void g() \n        { \n            base::f(); // call the base class method \n        } \n    };\n```\n\n`f`方法不能通过`derived`类使用，但该类可以调用`base`类方法。\n\n# 继承访问级别\n\n前面已经看到，要从类派生，需要提供基类名称并提供继承访问说明符；到目前为止，示例都使用了`public`继承，但也可以使用`protected`或`private`继承。\n\n这是 class 和 struct 之间的另一个区别。 对于类，如果遗漏了继承访问说明符，编译器将假定它是私有的；对于结构，如果遗漏了继承访问说明符，编译器将假定它是公共的。\n\n继承说明符应用了更多的访问限制，它不会放松这些限制。 访问说明符并不确定它对基类成员的访问权限，而是通过派生类更改这些成员的可访问性(即通过类的实例，或者如果另一个类从该类派生)。 如果基类有`private`成员，并且类使用`public`继承，则派生类仍然不能访问`private`成员；它只能访问派生类的`public`和`protected`成员，并且派生类的对象只能访问`public`成员，而从该类派生的类只能访问`public`和`protected`成员。\n\n如果派生类通过*受保护的继承*派生，则它对基类的访问权限仍与`public`和`protected`成员相同，但基类`public`和`protected`成员现在将通过派生类被视为`protected`，因此它们可以由另一个派生类访问，但不能通过实例访问。 如果类通过私有继承派生，则所有基类成员都将成为派生类中的`private`；因此，尽管派生类可以访问`public`和`protected`成员，但从它派生的类不能访问任何基类成员。\n\n看待受保护继承的一种方法是，如果派生类在类的`protected`部分中对基类的每个`public`成员都有一条`using`语句。 类似地，私有继承就像删除了基类的`public`和`protected`方法一样。\n\n一般来说，大多数继承将通过*公共继承*进行。 但是，当您希望访问基类中的某些功能但不希望其功能对从您的类派生的类可用时，*私有继承*很有用。 这有点像合成，在这种情况下，您正在使用功能，但不希望直接公开该功能。\n\n# 多重继承\n\nC++ 允许您从多个基类继承。 当与接口一起使用时，这是一个功能强大的工具，我们将在本章后面部分发现这一点。 它对实现继承很有用，但可能会导致一些问题。 语法很简单：提供要继承的类的列表：\n\n```cpp\n    class base1 { public: void a(); }; \n    class base2 { public: void b(); }; \n    class derived : public base1, public base2  \n    { \n    public: \n        // gets a and b \n    };\n```\n\n使用多个继承的一种方法是构建每个类都提供某些功能或服务的类库。 要将这些服务添加到您的类中，可以将库中的类添加到基类列表中。 这种通过实现继承创建类的*构建块*方法存在问题，我们稍后会看到，通常更好的方法是使用组合。\n\n当您考虑多重继承时，仔细检查您是否需要通过继承获得服务，或者组合是否更合适，这一点很重要。 如果某个类提供了一个您不希望实例使用的成员，并且您决定需要将其删除，则应该考虑组合是一个好兆头。\n\n如果两个类都有同名的成员，则存在潜在问题。 最明显的情况是基类有同名的数据成员：\n\n```cpp\n    class base1 { public: int x = 1; }; \n    class base2 { public: int x = 2; }; \n    class derived : public base1, public base2 {};\n```\n\n在前面的示例中，两个基类都有一个名为`x`的数据成员。 `derived`类从这两个类继承，因此这是否意味着它只获得一个名为`x`的数据成员？ 不是的。 如果是这样，那么这将意味着`base1`类将能够在不知道它正在影响另一个类的情况下更改`base2`类中的数据成员，类似地，`base2`类将发现其数据成员被`base1`类更改，即使那个类不是`friend`。 因此，当您从具有同名数据成员的两个类派生时，派生类将同时获得这两个数据成员。\n\n这再次说明了维护封装的重要性。 这样的数据成员应该是`private`，并且只能由基类更改。\n\n派生类(以及使用实例的代码，如果数据成员是可访问的)可以使用它们的全名来区分它们：\n\n```cpp\n    derived d; \n    cout << d.base1::x << endl; // the base1 version \n    cout << d.base2::x << endl; // the base2 version\n```\n\n这个类可以用下图来总结，说明了三个类`base1`、`base2`和`derived`占用的内存：\n\n![](img/05bc19b7-42ab-43b1-83a6-dd934527e416.png)\n\n如果保持封装并使数据成员`private`并且仅通过访问器方法授予访问权限，则派生类将无法直接访问数据成员，也不会出现此问题。 但是，同样的问题也会发生在方法上，但即使这些方法具有不同的原型，问题也会出现：\n\n```cpp\n    class base1 { public: void a(int); }; \n    class base2 { public: void a(); }; \n    class derived : public base1, public base2 {};\n```\n\n在本例中，这两个基类有一个同名的方法`a`，但具有不同的原型。 这在使用`derived`类时会导致问题，即使通过参数可以明显看出应该调用什么方法：\n\n```cpp\n    derived d; \n    d.a();          // should be a from base2, compiler still complains\n```\n\n这段代码不会编译，编译器会抱怨方法调用不明确。 同样，这个问题的解决方案很简单，您只需要指定要使用哪个基类方法：\n\n```cpp\n    derived d; \n    d.base1::a(42); // the base1 version \n    d.base2::a();   // the base2 version\n```\n\n多重继承可能会变得更加复杂。 如果您有两个派生自同一基类的类，然后创建另一个派生自这两个基类的类，则会出现问题。 新类是否获得最顶层基类成员的两个副本--通过其每个直接基类获得一个副本？\n\n![](img/cb441eea-fa33-4434-b4ba-56255cba5624.png)\n\n在第一级继承中，每个类(`base1`和`base2`)从最终基类继承数据成员(这里，数据成员都称为`base::x`，以说明它们是从最终基类`base`继承的)。 派生程度最高的类`derived`继承了*两个*数据成员，那么哪个是`base::x`呢？ 答案是，它们中只有一个是`base1::x`是`base::x`，因为它是继承列表中的第一个。 当`base`方法更改它时，将在`base1`到`base1::x`中看到更改。 `base2::x`成员是单独的数据成员，当`base`更改`base::x`时不受影响。 这可能是一个意想不到的结果：派生最多的类从两个父类继承`x`。\n\n这可能不是您想要的行为。 这个问题通常称为*钻石继承问题*，从上面的图表可以明显看出这个名称的由来。 解决方案很简单，将在本章后面介绍。\n\n# 对象切片\n\n在本章的前面部分，您已经看到，如果使用指向派生对象的基类指针，则只能安全地访问基类成员。 其他成员仍在那里，但它们只能通过适当的派生类指针进行访问。\n\n但是，如果将派生类对象强制转换为基类对象，则会发生其他事情：创建一个新对象，该对象就是基类对象，而只是基类对象。 您已强制转换的变量将只有基类对象的内存，因此结果只有派生对象的基类对象部分：\n\n```cpp\n    struct base { /*members*/ }; \n    struct derived : base { /*members*/ }; \n\n    derived d; \n    base b1 = d; // slicing through the copy constructor   \n    base b2; \n    b2 = d;      // slicing through assignment\n```\n\n这里，对象`b1`和`b2`是通过*分割`derived`类对象`d`中的额外数据*而创建的。 这段代码看起来有点不对劲，您不太可能编写它，但如果您按值将对象传递给函数，则很可能会发生这种情况：\n\n```cpp\n    void f(base b) \n    { \n        // can only access the base members \n    }\n```\n\n如果将`derived`对象传递给此函数，则将调用`base`复制构造函数来创建新对象，从而分割出`derived`类数据成员。 在大多数情况下，您不希望出现此行为。 如果基类具有虚方法并期望虚方法提供的多态功能(虚方法将在本章后面介绍)，则此问题也会出现意外行为。 通过引用传递对象几乎总是一个更好的主意。\n\n# 引入多态性\n\n多态来自希腊语*许多形状*。 到目前为止，您已经了解了多态性的基本形式。 如果使用指向对象的基类指针，则可以访问基类行为，如果有派生类指针，则会获得派生类行为。 这并不像看起来那么简单，因为派生类可以实现其自己版本的基类方法，因此您可以拥有该行为的不同实现。\n\n一个基类可以派生多个类：\n\n```cpp\n    class base { /*members*/ }; \n    class derived1 : public base { /*members*/ }; \n    class derived2 : public base { /*members*/ }; \n    class derived3 : public base { /*members*/ };\n```\n\n由于 C++ 是强类型的，这意味着指向一个派生类的指针不能用于指向另一个派生类。 因此不能使用`derived1*`指针访问`derived2`的实例，它只能指向类型为`derived1`的对象。 即使类具有相同的成员，它们仍然是不同的类型，它们的指针也不同。 但是，所有派生类都有一个共同点，那就是基类。 派生类指针可以隐式转换为基类指针，因此`base*`指针可以指向`base`、`derived1`、`derived2`或`derived3`的实例。 这意味着可以向接受`base*`指针作为参数的泛型函数传递指向这些类中任何一个的指针。 这是接口的基础，我们将在后面看到。\n\n多态方面是，通过指针(或引用)，类的实例可以被视为其继承层次结构中任何类的实例。\n\n# 虚拟方法\n\n仅提供对基类功能的访问的基类指针或引用，这是有意义的，但它是有限制的。 如果您有一个为汽车提供接口的`car`类、一个油门踏板和一个用于改变速度的刹车、一个方向盘和一个倒档来改变方向-您可以从这个类派生出各种其他类型的汽车：跑车、SUV 或家用轿车。 当你踩油门时，如果你的车是 SUV，你希望它有 SUV 的扭矩，如果你的车是跑车，你希望它有跑车的速度。 类似地，如果在`car`指针上调用`accelerate`方法，且该指针指向`suv`，则期望获得反映 SUV 扭矩的方法，如果`car`指针指向`sportscar`对象，则性能加速。 前面我们说过，如果您通过基类指针访问派生类实例，那么您将获得基类方法的实现。 这意味着，在指向`suv`或`sportscar`对象的`car`指针上调用`accelerate`方法时，仍然会得到`car::accelerate`的实现，而不是您想要的`suv::accelerate`或`sportscar::accelerate`。\n\n这种通过基类指针调用派生方法的行为称为**方法调度**。 通过基类指针调用方法的代码不知道指针指向的对象类型，但它仍然获得该对象的功能，因为调用了该对象上的方法。 默认情况下不应用此方法分派，因为它在内存和性能方面都会涉及一些额外成本。\n\n可以参与方法调度的方法在基类中用关键字`virtual`标记，因此通常称为**虚方法**。 当您通过基类指针调用此类方法时，编译器会确保调用实际对象的类上的方法。 由于每个方法都有一个`this`指针作为隐藏参数，因此方法调度机制必须确保在调用该方法时`this`指针是合适的。 请考虑以下示例：\n\n```cpp\n    struct base  \n    {  \n        void who() { cout << \"base \"; }  \n    }; \n    struct derived1 : base  \n    {  \n        void who() { cout << \"derived1 \"; }  \n    }; \n    struct derived2 : base \n    { \n        void who() { cout << \"derived2 \"; } \n    }; \n    struct derived3 : derived2 \n    { \n        void who() { cout << \"derived3 \"; } \n    }; \n\n    void who_is_it(base& r) \n    { \n        p.who(); \n    } \n\n    int main() \n    { \n        derived1 d1; \n        who_is_it(d1); \n        derived2 d2; \n        who_is_it(d2); \n        derived3 d3; \n        who_is_it(d3); \n        cout << endl; \n        return 0; \n    }\n```\n\n有一个基类和两个子类`derived1`和`derived2`。 通过`derived2`到称为`derived3`的类还有更深一层的继承。 基类实现了一个名为`who`的方法，该方法打印类名。 在每个派生类上都适当地实现了此方法，因此当在`derived3`的对象上调用此方法时，该方法将在控制台中打印`derived3`。 `main`函数创建每个派生类的实例，并通过引用调用`who`方法的名为`who_is_it`的函数来传递每个派生类。 该函数有一个引用`base`的参数，因为这是所有类的基类(对于`derived3`，它的直接基类是`derived2`)。 运行此代码时，结果如下所示：\n\n```cpp\n    base base base\n```\n\n此输出来自对`who_is_it`函数的三次调用，传递的对象是`derived1`、`derived2`和`derived3`类的实例。 由于该参数是对`base`的引用，这意味着调用了`base::who`方法。\n\n做一个简单的改变就会彻底改变这一行为：\n\n```cpp\n    struct base \n    { \n virtual void who() { cout << \"base \"; } \n    };\n```\n\n所有的改变都是在基类中的`who`方法中添加了`virtual`关键字，但结果是显著的。 运行此代码时，结果如下所示：\n\n```cpp\n     derived1 derived2 derived3\n```\n\n您没有更改`who_is_it`函数，也没有更改派生类上的方法，但是`who_is_it`的输出与以前大不相同。 `who_is_it`函数通过引用调用`who`方法，但是现在，调用引用别名的实际对象上的`who`方法而不是调用`base::who`方法。 `who_is_it`函数没有做任何额外的工作来确保调用派生类函数--它与前面的*完全相同。*\n\n *`derived3`类不是直接从`base`派生的，而是从`derived2`派生的，`derived2`本身就是`base`的子类。 即便如此，方法分派仍然适用于`derived3`类的实例。 这说明无论继承链`virtual`应用到哪一层，方法调度仍将作用于派生类的继承方法。\n\n需要指出的是，方法分派仅应用于基类中应用了`virtual`的方法*和*。 基类中未标记为`virtual`的任何其他方法都将在不进行方法调度的情况下被调用。 派生类将继承`virtual`方法并自动获得方法分派，它不必在其重写的任何方法上使用`virtual`关键字，但对于如何调用该方法是一个有用的可视指示。\n\n通过实现`virtual`方法的派生类，您可以使用单个容器来保存指向所有此类实例的指针，并调用它们的`virtual`方法，而无需调用代码知道对象的类型：\n\n```cpp\n    derived1 d1; \n    derived2 d2; \n    derived3 d3; \n\n    base *arr[] = { &d1, &d2, &d3 }; \n    for (auto p : arr) p->who(); \n    cout << endl;\n```\n\n在这里，`arr`内置数组保存指向这三种类型的对象的指针，Range`for`循环遍历数组并虚拟调用该方法。 这提供了预期的结果：\n\n```cpp\n     derived1 derived2 derived3\n```\n\n关于前面的代码，有三个要点：\n\n*   这里使用内置数组很重要；标准库容器(如`vector`)存在问题。\n*   重要的是，数组保存的是指针，而不是对象。 如果您有一个由`base`个对象组成的数组，它们将通过切片派生对象进行初始化。\n*   使用堆栈对象的地址也很重要。 这是因为析构函数存在问题。\n\n后面几节将介绍这三个问题。\n\n对于要使用方法调度调用的`virtual`方法，派生类方法必须在名称、参数和返回类型方面与基类的`virtual`方法匹配相同的签名。 如果其中任何一个是不同的(例如，不同的参数)，那么编译器会认为派生方法是一个新函数，因此当您通过基指针调用`virtual`方法时，您将获得基方法。 这是一个相当隐蔽的错误，因为代码可以编译，但您会得到错误的行为。\n\n最后一段的一个例外是，如果两个方法的返回类型不同，它们是**协变的**，也就是说，一种类型可以转换为另一种类型。\n\n# 虚拟方法表\n\n您只需要知道通过虚方法进行方法分派的行为，但是了解 C++ 编译器如何实现方法分派会很有帮助，因为它突出了`virtual`方法的开销。\n\n当编译器在类上看到`virtual`方法时，它将创建一个名为**vtable**的方法指针表，并在表中放置指向类中每个`virtual`方法的指针。 这门课将有一份`vtable`的复印件。 编译器还将在类的每个实例中添加一个指向该表的指针，称为**vptr**。 因此，当您将一个方法标记为`virtual`时，将会有一个在运行时为该类创建的`vtable`的内存开销，以及从该类创建的每个对象的额外数据成员`vptr`的内存开销。 通常，当客户端代码调用(非内联)方法时，编译器会将跳转放置到该方法的客户端代码中的函数。 当客户端代码调用`virtual`方法时，编译器必须取消引用`vptr`以获得`vtable`，然后使用存储在那里的适当地址。 显然，这涉及到额外的间接性。\n\n对于基类中的每个`virtual`方法，在`vtable`中都有一个单独的条目，按照它们被声明的顺序。 当您使用`virtual`方法从基类派生时，派生类也将具有`vptr`，但编译器将使其指向派生类的`vtable`，即编译器将使用派生类中的`virtual`方法实现的地址填充`vtable`。 如果派生类没有实现它继承的`virtual`方法，则`vtable`中的指针将指向基类方法。 这一点如下图所示：\n\n![](img/fc74ce55-48c9-4bd6-bd99-e02d38151193.png)\n\n在左侧，有两个类；基类有两个虚函数，派生类只实现其中一个。 右手边是内存布局的插图。 两个对象显示为`base`对象和`derived`对象。 每个对象都有一个`vptr`，后面跟着类的数据成员，数据成员的排列方式是先排列基类数据成员，然后排列派生类数据成员。 `vtable`指针包含指向`virtual`方法的方法指针。 在基类的情况下，方法指针指向在`base`类上实现的方法。 对于派生类，在`derived`类中只实现了第二个方法，因此该类的`vtable`具有指向`base`类中的一个虚方法和`derived`类中的另一个虚方法的指针。\n\n这就提出了一个问题：如果派生类引入了基类中不可用的新方法，并使其成为`virtual`，会发生什么情况？ 这并不是不可想象的，因为最终的基类可以只提供所需行为的一部分，而派生自它的类可以提供更多通过子类上的虚方法调度来调用的行为。 实现非常简单：编译器为类上的所有`virtual`方法创建一个`vtable`，因此如果派生类有额外的`virtual`方法，则这些方法的指针出现在指向从基类继承的`virtual`方法的指针之后的`vtable`中。 当通过基类指针调用对象时，无论该类在继承层次结构中的哪个位置，它都将只看到与其相关的`vtable`个条目：\n\n![](img/eccd155e-2d01-427f-96ed-3261ff940476.png)\n\n# 多重继承和虚方法表\n\n如果一个类从多个类派生，并且父类具有`virtual`方法，则派生类的 vtable 将是其父类的 vtable 的组合，按照父类在派生列表中列出的顺序排列：\n\n![](img/f3c6f103-1c95-4b1d-ac49-1b656fb4c8d6.png)\n\n如果通过基类指针访问对象，则`vptr`可以访问与该基类相关的`vtable`部分。\n\n# 虚拟方法、构造和销毁\n\n在构造函数完成之前，不会构造对象的派生类部分，因此如果调用`virtual`方法，则不会设置`vtable`条目来调用正确的方法。 类似地，在析构函数中，对象的派生类部分已经被销毁-包括它们的数据成员，因此派生类上的`virtual`方法不能被调用，因为它们可能试图访问不再存在的数据成员。 如果在这些情况下允许`virtual`方法调度，结果将是不可预测的。 不应在构造函数或析构函数中调用`virtual`方法，如果这样做，调用将解析为该方法的基类版本。\n\n如果期望使用`virtual`方法分派通过基类指针调用一个类，则应该将析构函数设置为`virtual`。 我们这样做是因为用户可能会删除基类指针，在这种情况下，您会希望调用派生析构函数。 如果析构函数不是`virtual`，并且基类指针被删除，则只调用基类析构函数，这可能会导致内存泄漏。\n\n通常，基类析构函数应该是`protected`非虚的，或者是`public`和`virtual`的。 如果打算通过基类指针使用类，则析构函数应该是`public`和`virtual`，以便调用派生类析构函数，但是如果基类打算用于提供只能通过派生类对象提供的服务，则不应该直接访问基类对象，因此析构函数应该是`protected`和非虚的。\n\n# 容器和虚拟方法\n\n`virtual`方法的一个优点是将基类相关的对象放入容器中；前面，我们看到了使用内置基类指针数组的特定情况，但是标准库容器呢？ 例如，假设您有一个类层次结构，其中有一个基类`base`和三个派生类`derived1`、`derived2`和`derived3`，每个类都实现了前面使用的`virtual`方法`who`。 将对象放入容器的一种尝试可能如下所示：\n\n```cpp\n    derived1 d1; \n    derived2 d2; \n    derived3 d3; \n    vector<base> vec = { d1, d2, d3 }; \n    for (auto b : vec) b.who(); \n    cout << endl;\n```\n\n问题是向量包含`base`个对象，因此当初始化列表中的项被放入容器中时，它们实际上用于初始化新的`base`对象。 因为`vec`的类型是`vector<base>`，所以`push_back`方法将对对象进行切片。 因此，对每个对象调用`who`方法的语句将打印一个字符串`base`。\n\n为了实现`virtual`方法分派，我们需要将整个对象放入容器中。 我们可以使用指针或引用来完成此操作。 要使用指针，只要`vector`不比容器中的对象存活时间长，就可以使用堆栈对象的地址。 如果您使用在堆上创建的对象，则需要确保适当地删除这些对象，您可以使用智能指针来实现这一点。\n\n您可能会想创建一个引用容器：\n\n```cpp\n    vector<base&> vec;\n```\n\n这将导致一系列错误；不幸的是，这些错误中没有一个完全指出了问题。 `vector`必须包含可复制、可构造和可赋值的类型。 引用则不是这样，因为它们是实际对象的别名。 有一个解决方案。 `<functional>`标头包含名为`reference_wrapper`的适配器类，该适配器类具有复制构造函数和赋值运算符。 该类将对象的引用转换为指向该对象的指针。 现在您可以编写以下内容：\n\n```cpp\n    vector<reference_wrapper<base> > vec = { d1, d2, d3 }; \n    for (auto b : vec) b.get().who(); \n    cout << endl;\n```\n\n使用`reference_wrapper`的缺点是，要调用包装对象(及其虚拟方法)，需要调用`get`方法，该方法将返回对包装对象的*引用*。\n\n# 朋友和遗产\n\n在 C++ 中，友谊不是继承的。 如果一个类使另一个类(或函数)成为朋友，这意味着该朋友可以访问其`private`和`protected`成员，就好像该朋友是该类的成员一样。 如果从`friend`类派生，则新类不是第一个类的朋友，并且它没有访问第一个类的成员的权限。\n在上一章中，我们了解了如何通过编写全局插入操作符并使其成为类的`friend`来将对象插入到`ostream`对象中以打印它。 在下面的示例中，`friend`函数是内联实现的，但它实际上是一个单独的全局函数，无需使用类名进行对象或名称解析即可调用：\n\n```cpp\n    class base \n    {\n        int x = 0; \n    public: \n        friend ostream& operator<<(ostream& stm, const base& b) \n        { \n            // thru b we can access the base private/protected members \n            stm << \"base: \" << b.x << \" \"; \n            return stm; \n        } \n    };\n```\n\n如果我们派生自`base`类，则需要实现一个`friend`函数来将派生对象插入到流中。 由于该函数是*朋友*，因此它可以访问派生类的`private`和`protected`成员，但不能访问基类的`private`成员。 这种情况意味着，作为派生类的*朋友*的插入操作符只能打印出对象的一部分。\n\n如果将`derived`类对象强制转换为`base`类，例如，在按引用传递时通过指针或引用，并且打印该对象，则将调用`base`版本的插入运算符。 插入运算符是`friend`函数，因此它可以访问类的非公共数据成员，但作为*朋友*不足以使其成为`virtual`方法，因此没有`virtual`方法调度。\n\n虽然`friend`函数不能作为`virtual`方法调用，但它可以调用`virtual`方法并得到方法调度：\n\n```cpp\n    class base \n    { \n        int x = 0;  \n        protected: \n        virtual void output(ostream& stm) const { stm << x << \" \"; } \n    public: \n        friend ostream& operator<<(ostream& stm, const base& b) \n        { \n            b.output(stm); \n            return stm; \n        } \n    }; \n\n    class derived : public base \n    { \n        int y = 0; \n    protected: \n        virtual void output(ostream& stm) const \n        { \n            base::output(stm); \n            stm << y << \" \"; \n        } \n    };\n```\n\n在这个版本中，只有一个插入运算符，它是为`base`类定义的。 这意味着可以使用此运算符打印任何可以转换为`base`类的对象。 打印输出对象的实际工作被委托给名为`output`的`virtual`函数。 此函数受保护，因为它仅供类或派生类使用。 它的`base`类版本打印出基类的数据成员。 `derived`类版本有两个任务：打印出`base`类中的数据成员，然后打印出特定于`derived`类的数据成员。 第一个任务通过使用基类名称限定名称来调用方法的`base`类版本来完成。 第二个任务很简单，因为它可以访问自己的数据成员。 如果要从`derived`派生另一个类，则其`output`函数的版本将类似，但它将调用`derived::output`。\n\n现在，当对象插入到类似`cout`的`ostream`对象中时，将调用插入操作符，并将对`output`方法的调用分派给适当的派生类。\n\n# 覆盖和最终\n\n如前所述，如果您输入了错误的派生`virtual`方法的原型，例如，使用了错误的参数类型，编译器将把该方法视为新方法并对其进行编译。 派生类不重写基类的方法是完全合法的；这是您经常想要使用的功能。 但是，如果您在键入派生`virtual`方法的原型时出错，则在您打算调用新版本时将调用基方法。 `override`说明符旨在防止此错误。 当编译器看到这个说明符时，它知道您打算重写从基类继承的`virtual`方法，并且它将搜索继承链以找到合适的方法。 如果找不到这样的方法，则编译器将发出错误：\n\n```cpp\n    struct base  \n    {  \n        virtual int f(int i);  \n    }; \n\n    struct derived: base  \n    {  \n        virtual int f(short i) override; \n    };\n```\n\n在这里，`derived::f`将不会编译，因为继承链中没有具有相同签名的方法。 `override`说明符让编译器执行一些有用的检查，因此在所有派生覆盖的方法上使用它是一个好习惯。\n\nC++ 11 还提供了一个名为`final`的说明符，您可以将其应用于方法以指示派生类不能重写它，也可以将其应用于类以指示您不能从其派生：\n\n```cpp\n    class complete final { /* code */ }; \n    class extend: public complete{}; // won't compile\n```\n\n你很少会想要用这个。\n\n# 虚拟继承\n\n早些时候，我们讨论了所谓的多重继承的*菱形*问题，即一个类通过两个基类从单个祖先类继承。 当一个类从另一个类继承时，它将获取父类的数据成员，以便将派生类的实例视为由基类数据成员和派生类数据成员组成。 如果父类派生自同一祖先类，则它们将各自获得祖先类的数据成员，从而导致最终派生类从每个父类获得祖先类的数据成员的副本：\n\n```cpp\n    struct base { int x = 0; }; \n    struct derived1 : base { /*members*/ }; \n    struct derived2 :  base { /*members*/ }; \n    struct most_derived : derived1, derived2 { /*members*/ };\n```\n\n创建`most_derived`类的实例时，对象中有两个`base`副本：分别来自`derived1`和`derived2`。 这意味着`most_derived`对象将拥有数据成员`x`的两个副本。 显然，目的是让派生类只获得祖先类的数据成员的一个副本，那么如何实现这一点呢？ 此问题的解决方案是**虚拟继承**：\n\n```cpp\n    struct derived1 : virtual base { /*members*/ }; \n    struct derived2 : virtual base { /*members*/ };\n```\n\n在没有虚拟继承的情况下，派生类只调用其直接父级的构造函数。 使用`virtual`继承时，`most_derived`类负责调用最顶层父类的构造函数，如果不显式调用基类构造函数，编译器将自动调用默认构造函数：\n\n```cpp\n    derived1::derived1() : base(){} \n    derived2::derived2() : base(){} \n    most_derived::most_derived() : derived1(), derived2(), base(){}\n```\n\n在前面的代码中，`most_derived`构造函数调用`base`构造函数，因为这是其父类虚拟继承的基类。 `virtual`基类总是在非虚拟基类之前创建。 尽管在`most_derived`构造函数中调用了`base`构造函数，但我们仍然必须在派生类中调用`base`构造函数。 如果我们进一步从`most_derived`派生，则该类还必须调用`base`的构造函数，因为将在那里创建`base`对象。 虚拟继承比单一或多重继承更昂贵。\n\n# 抽象类\n\n具有`virtual`方法的类仍然是**个具体类**--您可以创建该类的实例。 您可能决定只提供部分功能，目的是让用户*具有*从类派生并添加缺少的功能。\n\n要做到这一点，一种方法是提供一个没有代码的`virtual`方法。 这意味着您可以在类中调用`virtual`方法，并且在运行时将调用派生类中的方法版本。 然而，尽管这为您在代码中调用派生方法提供了一种机制，但它并不*强制*实现那些`virtual`方法。 相反，派生类将继承空的`virtual`方法，如果不覆盖它们，客户端代码将能够调用空方法。 您需要一种机制来*强制*派生类提供那些`virtual`方法的实现。\n\nC++ 提供了一种称为**纯虚方法**的机制，它指示该方法应该由派生类覆盖。 语法很简单，您可以用`= 0`标记该方法：\n\n```cpp\n    struct abstract_base \n    { \n virtual void f() = 0; \n        void g() \n        { \n            cout << \"do something\" << endl; \n            f(); \n        } \n    };\n```\n\n这是完整的类；这是该类为方法`f`的定义提供的全部内容。 即使方法`g`调用没有实现的方法，该类也会编译。 但是，以下代码将不会编译：\n\n```cpp\n    abstract_base b;\n```\n\n通过声明纯虚函数，您可以使类成为抽象的，这意味着您不能创建实例。 但是，您可以创建指向该类的指针或引用，并对其调用代码。 此函数将编译：\n\n```cpp\n    void call_it(abstract_base& r) \n    { \n        r.g(); \n    }\n```\n\n该函数只知道类的公共接口，并不关心它是如何实现的。 我们实现了方法`g`来调用方法`f`，以说明您可以在同一个类中调用纯虚方法。 实际上，您也可以在类外部调用纯虚函数；下面的代码同样有效：\n\n```cpp\n    void call_it2(abstract_base& r) \n    { \n        r.f(); \n    }\n```\n\n使用抽象类的唯一方法是从它派生并实现纯虚函数：\n\n```cpp\n    struct derived1 : abstract_base \n    { \n        virtual void f() override { cout << \"derived1::f\" << endl; } \n    }; \n\n    struct derived2 : abstract_base \n    { \n        virtual void f() override { cout << \"derived2::f\" << endl; } \n    };\n```\n\n下面是从抽象类派生的两个类，它们都实现纯虚函数。 这些是具体的类，您可以创建它们的实例：\n\n```cpp\n    derived1 d1; \n    call_it(d1); \n    derived2 d2; \n    call_it(d2);\n```\n\n抽象类用于指示特定功能必须由派生类提供，而`= 0`语法指示方法体不是由抽象类提供的。 事实上，它比这更微妙；类必须是派生的，派生类上调用的方法必须在派生类上定义，但抽象基类也可以为方法提供主体：\n\n```cpp\n    struct abstract_base \n    { \n        virtual int h() = 0 { return 42; } \n    };\n```\n\n同样，这个类不能实例化，您必须*从它派生*，并且您*必须*实现该方法才能实例化对象：\n\n```cpp\n    struct derived : abstract_base \n    { \n        virtual int h() override { return abstract_base::h() * 10; } \n    };\n```\n\n派生类可以调用抽象类中定义的纯虚函数，但当外部代码调用此类方法时，它总是(通过方法调度)调用派生类上的虚方法的实现。\n\n# 获取类型信息\n\nC++ 提供类型信息，也就是说，您可以获得该类型唯一的信息，以及标识该类型的信息。 C++ 是一种强类型语言，因此编译器将在编译时确定类型信息，并在变量类型之间进行转换时强制执行类型规则。 编译器执行的任何类型检查，您都可以作为开发人员执行。 根据一般经验，如果您需要使用`static_cast`、`const_cast`、`reinterpret_cast`或类似 C 的强制转换进行强制转换，那么您将使类型做一些它们不应该做的事情，因此您应该重新考虑重写代码。 编译器非常善于告诉您哪里有类型不对齐的地方，因此您应该将此作为重新评估代码的提示。\n\n*不强制转换*规则可能有点太严格，而且使用强制转换的代码通常更易于编写和阅读，但这样的规则确实会让您始终质疑是否需要强制转换。\n\n当您使用多态性时，您通常会得到一个指向与对象类型不同的类型的指针或引用，当您转到接口编程时尤其如此，在接口编程中，实际对象通常并不重要，因为它是重要的行为。 在某些情况下，您可能需要获取类型信息，而编译器在编译时无法帮助您。 C++ 提供了一种获取类型信息的机制，称为**Runtime Type Information**(**RTTI**)，因为您可以在运行时获取此信息。 此信息是使用对象上的`typeid`运算符获得的：\n\n```cpp\n    string str = \"hello\"; \n    const type_info& ti = typeid(str); \n    cout << ti.name() << endl;\n```\n\n在命令行中打印的结果如下所示：\n\n```cpp\n    class std::basic_string<char,struct std::char_traits<char>,\n class std::allocator<char> >\n```\n\n这反映了`string`类实际上是模板化类`basic_string`的`typedef`，具有`char`作为字符类型，具有由`char_traits`类的专门化描述的字符特征，以及一个分配器对象(用于维护字符串使用的缓冲区)，它是`allocator`类的专门化。\n\n`typeid`操作符返回对`type_info`对象的`const`引用，在本例中，我们使用`name`方法返回指向对象类型名称的`const char`指针。 这是类型名称的可读版本。 类型名实际上存储在紧凑的修饰名中，这是通过`raw_name`方法获得的，但如果您想根据对象的类型(例如，在字典对象中)存储对象，那么更有效的机制是使用从`hash_code`方法返回的 32 位整数，而不是修饰名。 在所有情况下，相同类型的所有对象的返回值都相同，但与其他类型的对象不同。\n\n`type_info`类没有复制构造函数或复制赋值运算符，因此此类的对象不能放入容器中。 如果要将`type_info`对象放入类似`map`的关联容器中，则有两个选择。 首先，您可以将指向`type_info`对象的指针放入容器中(指针可以从引用中获得)；在这种情况下，如果容器是有序的，则需要定义一个比较运算符。 `type_info`类有一个`before`方法，可用于比较两个`type_info`对象。\n\n第二个选项(在 C++ 11 中)是使用`type_index`类的对象作为关联容器的键，该类用于包装`type_info`对象。\n\n`type_info`类是只读的，创建实例的唯一方法是通过`typeid`运算符。 但是，您可以对`type_info`对象调用比较运算符`==`和`!=`，这意味着您可以在运行时比较对象的类型。\n\n由于您可以在变量和类型上应用`typeid`运算符，这意味着您可以使用该运算符执行安全的强制转换，这些类型不会被切片或强制转换为完全不相关的类型：\n\n```cpp\n    struct base {}; \n    struct derived { void f(); }; \n\n    void call_me(base *bp) \n    { \n        derived *dp = (typeid(*bp) == typeid(derived))  \n            ? static_cast<derived*>(bp) : nullptr; \n        if (dp != nullptr) dp->f(); \n    } \n\n    int main() \n    { \n        derived d; \n        call_me(&d); \n        return 0; \n    }\n```\n\n此函数可以接受从`base`类派生的任何类的指针。 第一行使用条件运算符，其中比较是函数参数指向的对象的类型信息与类`derived`的类型之间的比较。 如果指针指向`derived`对象，则强制转换将起作用。 如果指针指向另一个派生类型的对象，而不是`derived`类，则比较将失败，并且表达式的计算结果为`nullptr`。 只有当指针指向`derived`类的实例时，`call_me`函数才会调用`f`方法。\n\nC++ 提供了执行运行时的强制转换操作符，这种运行时的类型检查称为`dynamic_cast`。 如果可以将对象强制转换为请求的类型，则操作将成功并返回有效指针。 如果无法通过请求的指针访问对象，则强制转换失败，操作符返回`nullptr`。 这意味着无论何时使用`dynamic_cast`，都应该在使用之前检查返回的指针。 可以按如下方式重写`call_me`函数：\n\n```cpp\n    void call_me(base *bp) \n    { \n        derived *dp = dynamic_cast<derived*>(bp); \n        if (dp != nullptr) dp->f(); \n    }\n```\n\n这基本上与前面的代码相同；`dynamic_cast`操作符执行运行时类型检查并返回适当的指针。\n\n请注意，您既不能向下转换到`virtual`基类指针，也不能向下转换到通过`protected`或`private`继承派生的类。 `dynamic_cast`运算符可以用于除向下强制转换之外的强制转换；显然，它将用于向上强制转换(到基类，尽管不是必需的)，它可以用于横向强制转换：\n\n```cpp\n    struct base1 { void f(); }; \n    struct base2 { void g(); }; \n    struct derived : base1, base2 {};\n```\n\n这里有两个基类，因此如果通过其中一个基类指针访问派生对象，则可以使用`dynamic_cast`运算符强制转换为另一个基类的指针：\n\n```cpp\n    void call_me(base1 *b1)  \n    { \n        base2 *b2 = dynamic_cast<base2*>(b1); \n        if (b2 != nullptr) b2->g(); \n    }\n```\n\n# 智能指针和虚拟方法\n\n如果要使用动态创建的对象，则需要使用智能指针来管理其生存期。 好消息是`virtual`方法分派通过智能指针工作(它们只是对象指针的包装器)，坏消息是在使用智能指针时类关系会丢失。 让我们来研究一下原因。\n\n例如，以下两个类通过继承相关：\n\n```cpp\n    struct base  \n    {  \n        Virtual ~base() {} \n        virtual void who() = 0;  \n    }; \n\n    struct derived : base  \n    {  \n        virtual void who() { cout << \"derivedn\"; }  \n    };\n```\n\n这很简单：它实现了一个`virtual`方法，该方法指示对象的类型。 有一个`virtual`析构函数，因为我们要将生存期管理移交给一个智能指针对象，并且我们希望确保正确调用`derived`类析构函数。 您可以使用`make_shared`或`shared_ptr`类的构造函数在堆上创建对象：\n\n```cpp\n    // both of these are acceptable \n    shared_ptr<base> b_ptr1(new derived);  \n    shared_ptr<base> b_ptr2 = make_shared<derived>();\n```\n\n派生类指针可以转换为基类指针，这在第一条语句中是显式的：`new`返回`derived*`指针，该指针被传递给需要`base*`指针的`shared_ptr<base>`构造函数。 第二个声明中的情况稍微复杂一些。 函数的作用是：返回一个临时的`shared_ptr<derived>`对象，该对象被转换为一个`shared_ptr<base>`对象。 这是由`shared_ptr`类上的转换构造函数执行的，该构造函数调用名为`__is_convertible_to`的**编译器内部**，它确定是否可以将一种指针类型转换为另一种指针类型。 在本例中，存在向上转换，因此允许转换。\n\n编译器内部本质上是由编译器提供的函数。 在本例中，`__is_convertible_to(derived*, base*)`将返回`true`，`__is_convertible_to(base*, derived*)`将返回`false`。 除非您正在编写库，否则您几乎不需要了解内部函数。\n\n由于临时对象是使用`make_shared`函数在语句中创建的，因此使用第一个语句效率更高。\n\n`shared_ptr`对象上的`operator->`将提供对包装指针的直接访问，因此这意味着以下代码将按照预期执行`virtual`方法调度：\n\n```cpp\n    shared_ptr<base> b_ptr(new derived); \n    b_ptr->who(); // prints \"derived\"\n```\n\n智能指针将确保在`b_ptr`超出作用域时通过基类指针销毁派生对象，由于我们有`virtual`析构函数，因此将发生适当的销毁。\n\n如果你有多重继承，你可以使用`dynamic_cast`(和 RTTI)在指向基类的指针之间进行转换，这样你就可以只选择你需要的行为。 请考虑以下代码：\n\n```cpp\n    struct base1  \n    {  \n        Virtual ~base1() {} \n        virtual void who() = 0;  \n    }; \n\n    struct base2  \n    {  \n        Virtual ~base2() {} \n        virtual void what() = 0;  \n    }; \n\n    struct derived : base1, base2  \n    {  \n        virtual void who()  { cout << \"derivedn\"; }  \n        virtual void what() { cout << \"derivedn\"; }  \n    };\n```\n\n如果您有指向这两个基类之一的指针，则可以将一个基类转换为另一个基类：\n\n```cpp\n    shared_ptr<derived> d_ptr(new derived); \n    d_ptr->who(); \n    d_ptr->what(); \n\n    base1 *b1_ptr = d_ptr.get(); \n    b1_ptr->who(); \n    base2 *b2_ptr = dynamic_cast<base2*>(b1_ptr); \n    b2_ptr->what();\n```\n\n可以在`derived*`指针上调用`who`和`what`方法，因此可以在智能指针上调用它们。 以下几行获得一个基类指针，以便访问*特定的*行为。 在此代码中，我们调用`get`方法从智能指针获取原始指针。 此方法的问题在于，现在有一个指向不受智能指针生存期管理保护的对象的指针，因此代码可能会在指针`b1_ptr`或`b2_ptr`上调用`delete`，并在以后智能指针尝试删除该对象时导致问题。\n\n这段代码可以工作，并且在这段代码中对动态创建的对象进行了正确的生存期管理，但是像这样访问原始指针本质上是不安全的，因为不能保证原始指针不会被删除。 诱人之处在于使用智能指针：\n\n```cpp\n    shared_ptr<base1> b1_ptr(d_ptr.get());\n```\n\n问题是，即使类`base1`和`derived`相关，但类`shared_ptr<derived>`和`shared_ptr<base1>`不是*相关的，因此每个智能指针类型将使用不同的控制块，即使它们引用*相同的对象*。 `shared_ptr`类将使用控制块引用计数，并在引用计数降为零时删除对象。 拥有两个不相关的`shared_ptr`对象和同一个对象的两个控制块意味着它们将尝试彼此独立地管理`derived`对象的生命周期，这最终将意味着一个智能指针在另一个智能指针完成对象之前将其删除。*\n\n这里有三条消息：智能指针是指针周围的轻量级包装器，因此您可以使用方法分派来调用`virtual`方法；但是，在使用从智能指针获得的原始指针时要小心，请记住，尽管可以有多个`shared_ptr`对象指向同一对象，但它们必须是相同类型的，以便只使用一个控制块。\n\n# 接口\n\n纯虚函数和虚方法分派导致了一种非常强大的编写面向对象代码的方式，称为**接口**。 接口是没有功能的类；它只有纯虚函数。 接口的目的是定义行为。 从接口*派生的具体类必须*提供该接口上所有方法的实现，因此这使得该接口成为一种契约。 实现接口的对象的用户可以保证，具有该接口的对象将实现该接口的所有*个*个方法。 接口编程将行为与实现解耦。 客户端代码只对行为感兴趣，而对提供接口的实际类不感兴趣。\n\n例如，通过`IPrint`接口可以访问打印文档的行为(设置页面大小、方向、份数，并告诉打印机打印文档)。 通过`IScan`界面可以访问扫描纸张的行为(分辨率、灰度或颜色，以及旋转和裁剪等调整)。 这两个界面是两种不同的行为。 如果要打印文档，客户端代码将使用`IPrint`，如果要扫描文档，则使用`IScan`接口指针。 这样的客户端代码并不关心它是实现`IPrint`接口的`printer`对象还是同时实现`IPrint`和`IScan`接口的`printer_scanner`对象。 传递给`IPrint*`接口指针的客户端代码保证可以调用每个方法。\n\n在下面的代码中，我们定义了`IPrint`接口(`define`使我们更明显地将抽象类定义为接口)：\n\n```cpp\n    #define interface struct \n\n    interface IPrint \n    { \n        virtual void set_page(/*size, orientation etc*/) = 0; \n        virtual void print_page(const string &str) = 0; \n    };\n```\n\n类可以实现此接口：\n\n```cpp\n    class inkjet_printer : public IPrint \n    { \n    public: \n        virtual void set_page(/*size, orientation etc*/) override \n        { \n            // set page properties \n        } \n        virtual void print_page(const string &str) override \n        { \n            cout << str << endl; \n        } \n    }; \n\n    void print_doc(IPrint *printer, vector<string> doc);\n```\n\n然后，您可以创建`printer`对象并调用函数：\n\n```cpp\n    inkjet_printer inkjet; \n    IPrint *printer = &inkjet; \n    printer->set_page(/*properties*/); \n    vector<string> doc {\"page 1\", \"page 2\", \"page 3\"}; \n    print_doc(printer, doc);\n```\n\n我们的喷墨打印机也是扫描仪，所以我们可以让它实现`IScan`接口：\n\n```cpp\n    interface IScan \n    { \n        virtual void set_page(/*resolution etc*/) = 0; \n        virtual string scan_page() = 0; \n    };\n```\n\n下一版本的`inkject_printer`类可以使用多重继承来实现此接口，但请注意存在一个问题。 该类已经实现了一个名为`set_page`的方法，由于打印机的页面属性将不同于扫描仪的页面属性，因此我们希望为`IScan`接口使用不同的方法。 我们可以通过两种不同的方法来解决这个问题，并限定它们的名称：\n\n```cpp\n    class inkjet_printer : public IPrint, public IScan \n    { \n    public: \n        virtual void IPrint::set_page(/*etc*/) override { /*etc*/ } \n        virtual void print_page(const string &str) override \n        { \n            cout << str << endl; \n        } \n        virtual void IScan::set_page(/*etc*/) override { /*etc*/ } \n        virtual string scan_page() override \n        { \n            static int page_no; \n            string str(\"page \"); \n            str += to_string(++ page_no); \n            return str; \n        } \n    }; \n\n    void scan_doc(IScan *scanner, int num_pages);\n```\n\n现在，我们可以获取`inkjet`对象上的`IScan`接口，并将其称为 scanner：\n\n```cpp\n    inkjet_printer inkjet; \n    IScan *scanner = &inkjet; \n    scanner->set_page(/*properties*/); \n    scan_doc(scanner, 5);\n```\n\n由于`inkject_printer`类派生自`IPrinter`和`IScan`接口，因此您可以获取一个接口指针并通过`dynamic_cast`操作符强制转换为另一个接口指针，因为这将使用 RTTI 来确保强制转换是可能的。 因此，假设您有一个`IScanner`接口指针，您可以进行测试，看看是否可以将其转换为`IPrint`接口指针：\n\n```cpp\n    IPrint *printer = dynamic_cast<IPrint*>(scanner); \n    if (printer != nullptr) \n    { \n        printer->set_page(/*properties*/); \n        vector<string> doc {\"page 1\", \"page 2\", \"page 3\"}; \n        print_doc(printer, doc); \n    }\n```\n\n实际上，如果指针所指向的对象上另一个接口表示的行为不可用，则使用`dynamic_cast`运算符请求一个接口指针。\n\n接口是一种约定；一旦您定义了它，您就应该*永远不要*更改它。 这不会限制您更改类。 事实上，这是使用接口的优点，因为类实现可以完全更改，但只要它继续实现客户端代码使用的接口，类的用户就可以继续使用该类(尽管需要重新编译)。 有些情况下，您会发现您定义的接口不够用。 可能有一个输入错误的参数需要修复，或者可能需要添加其他功能。\n\n例如，假设您要告诉打印机对象一次打印整个文档，而不是一页。 方法是从需要更改的接口派生并创建一个新接口；接口继承：\n\n```cpp\n    interface IPrint2 : IPrint \n    { \n        virtual void print_doc(const vector<string> &doc) = 0; \n    };\n```\n\n接口继承意味着`IPrint2`有三个方法：`set_page`、`print_page`和`print_doc`。 由于`IPrint2`接口是`IPrint`接口，这意味着当您实现`IPrint2`接口时，您也实现了`IPrint`接口，因此您需要将类更改为从`IPrint2`接口派生以添加新功能：\n\n```cpp\n class inkjet_printer : public IPrint2, public IScan \n    { \n    public: \n virtual void print_doc(const vector<string> &doc) override { \n            /* code*/\n        } \n        // other methods \n    };\n```\n\n从实现`IPrint`接口开始，`IPrint2`接口上的另外两个方法已经存在于这个类中。 现在，客户端可以从该类的实例中获取`IPrint`指针和`IPrint2`指针。 您已经扩展了类，但旧的客户端代码仍将编译。\n\n微软的**组件对象模型**(**COM**)将这一概念更进一步。 COM 基于接口编程，因此 COM 对象只能通过接口指针访问。 额外的步骤是，可以使用动态加载库将此代码加载到您的进程中，或者加载到您的计算机或另一台计算机上的另一个进程中，而且由于您使用接口编程，因此无论对象位于什么位置，都会以完全相同的方式*访问这些对象。*\n\n *# 阶级关系\n\n继承似乎是重用代码的理想方式：以尽可能泛型的方式编写一次，然后从基类派生一个类并重用代码，必要时对其进行专门化。 然而，你会发现很多反对这一点的建议。 有些人会告诉您，继承是重用代码的最糟糕的方式，您应该使用组合。 事实上，情况介于两者之间：继承提供了一些好处，但不应被视为最佳或唯一的解决方案。\n\n设计类库是有可能的，而且有一个总的原则需要牢记：您编写的代码越多，您(或其他人)需要做的维护工作就越多。 如果更改一个类，则依赖它的所有其他类也会更改。\n\n在最高级别，您应该意识到要避免的三个主要问题：\n\n*   **刚性**：更改一个类太难了，因为任何更改都会影响太多其他类。\n*   **脆弱性**：当您更改类时，可能会导致其他类发生意外更改。\n*   **固定**：很难重用类，因为它太依赖于其他类。\n\n当您在类之间具有紧密耦合时，就会发生这种情况。 通常，您应该设计类来避免这种情况，接口编程是一种很好的方法，因为接口只是一种行为，而不是特定类的实例。\n\n当您有*个依赖倒置*，也就是说，使用组件的较高级别代码依赖于较低级别组件如何实现的细节时，就会出现这样的问题。 如果您的代码执行某些操作，然后在您编写日志记录以使用特定设备(比如`cout`对象)时记录结果，那么代码将严格耦合到该日志记录设备，并依赖于该日志记录设备，并且您将来没有更改到其他设备的选项。 如果您通常通过接口指针来抽象功能，那么您就打破了这种依赖，从而使代码能够在将来与其他组件一起使用。\n\n另一个原则是，一般来说，您应该将您的类设计为可扩展的。 继承是一种很强的扩展类的机制，因为您正在创建一个全新的类型。 如果只需要改进功能，那么继承可能是一种矫枉过正的做法。 改进算法的一种更轻量级的形式是将方法指针(或函数器)或接口指针传递给类的方法，以便该方法在适当的时间调用以改进其工作方式。\n\n例如，大多数排序算法要求您传递一个方法指针，以便对它正在排序的类型的两个对象执行比较。 排序机制是通用的，它以最有效的方式对对象进行排序，但它的基础是告诉它如何对两个对象进行排序。 由于大多数算法保持不变，为每种类型编写一个新类是过分的。\n\n# 使用 Mixin 类\n\n**Mixin**技术允许您为类提供可扩展性，而不会出现组合的生命周期问题或原始继承的重量级问题。 这里的想法是，您拥有一个具有特定功能的库，可以将其添加到对象中。 要做到这一点，一种方法是将其作为具有`public`方法的基类应用，因此如果派生类公开派生自该类，则它也将具有作为`public`方法的那些方法。 除非该功能要求派生类也在这些方法中执行某些功能，否则这种方法工作得很好，在这种情况下，库的文档将要求派生类重写该方法，调用基类实现，并将它们自己的代码添加到该方法以完成实现(基类方法可以在额外的派生类代码之前或之后调用，文档必须指定这一点)。 到目前为止，我们已经在本章中多次看到这一点，并且它是一些较老的类库使用的技术，例如，微软的**基础类库**(**MFC**)。 Visual C++ 使这一点变得更容易，因为它使用向导工具生成 MFC 代码，并且有关于开发人员应该将代码添加到何处的注释。\n\n这种方法的问题在于，它要求从基类派生的开发人员实现特定的代码并遵循规则。\n开发人员可能会编写编译和运行的代码，但由于它不是按照所需的规则编写的，因此在运行时会有错误的行为。\n\nMixin 类颠覆了这个概念。 与开发者从库提供的基类派生并扩展所提供的功能不同，库*提供的 Mixin 类是从开发者*提供的类派生的。 这解决了几个问题。 首先，开发人员必须提供文档要求的特定方法，否则 Mixin 类(将使用这些方法)将无法编译。 编译器强制执行类库作者的规则，要求使用库的开发人员提供特定代码。 其次，Mixin 类上的方法可以准确地在需要的地方调用基类方法(由开发人员提供)。 使用类库的开发人员不再获得有关如何开发代码的详细说明，除此之外，他们还必须实现某些方法。\n\n那么，如何才能做到这一点呢？ 类库作者不知道客户端开发人员将编写的代码，也不知道客户端开发人员将编写的类的名称，因此无法从此类类派生。 C++ 允许您通过模板参数提供类型，以便在编译时使用此类型实例化类。 对于 Mixin 类，通过模板参数传递的类型是将用作基类的类型的名称。 开发人员只需提供一个具有特定方法的类，然后使用它们的类作为模板参数创建 Mixin 类的专门化：\n\n```cpp\n    // Library code \n    template <typename BASE> \n    class mixin : public BASE \n    { \n    public: \n        void something() \n        { \n            cout << \"mixin do something\" << endl; \n            BASE::something(); \n            cout << \"mixin something else\" << endl; \n        } \n    }; \n\n    // Client code to adapt the mixin class \n    class impl  \n    { \n    public: \n        void something() \n        { \n            cout << \"impl do something\" << endl; \n        } \n    };\n```\n\n此类的用法如下：\n\n```cpp\n    mixin<impl> obj; \n    obj.something();\n```\n\n如您所见，`mixin`类实现了一个名为`something`的方法，它调用了一个名为`something`的基类方法。 这意味着使用 Mixin 类功能的客户端开发人员必须实现具有该名称和相同原型的方法，否则不能使用 Mixin 类。 编写`impl`类的客户端开发人员不知道如何或在哪里使用他们的代码，只知道他们必须提供具有特定名称和原型的方法。 在这种情况下，`mixin::something`方法在它提供的功能之间的代码中调用基类方法，`impl`类的编写者不需要知道这一点。 此代码的输出如下所示：\n\n```cpp\n    mixin do something\nimpl do something\nmixin something else\n```\n\n这表明`mixin`类可以在它认为合适的地方调用`impl`类。 `impl`类只需提供功能；`mixin`类决定如何使用它。 事实上，任何实现具有正确名称和原型的方法的类都可以作为参数提供给`mixin`类的模板-甚至是另一个 Mixin 类！\n\n```cpp\n    template <typename BASE> \n    class mixin2 : public BASE \n    { \n    public: \n        void something() \n        { \n            cout << \"mixin2 do something\" << endl; \n            BASE::something(); \n            cout << \"mixin2 something else\" << endl; \n        } \n    };\n```\n\n这可以像这样使用：\n\n```cpp\n    mixin2< mixin<impl> > obj; \n    obj.something();\n```\n\n结果如下：\n\n```cpp\n    mixin2 do something\nmixin do something\nimpl do something\nmixin something else \nmixin2 something else\n```\n\n请注意，除了实现了适当的方法之外，`mixin`和`mixin2`类对彼此一无所知。\n\n由于 Mixin 类不能在没有 Template 参数提供的类型的情况下使用，因此它们有时被称为抽象子类。\n\n如果基类只有一个默认构造函数，那么这种方法就可以很好地工作。 如果实现需要另一个构造函数，那么 Mixin 必须知道要调用哪个构造函数，并且必须有适当的参数。 此外，如果您链接了 Mixin，那么它们将通过构造函数进行耦合。 解决此问题的一种方法是使用两阶段构造，即提供一个命名方法(例如，`init`)，用于在构造后初始化对象中的数据成员。 Mixin 类仍将像前面一样使用其默认构造函数创建，因此类之间不存在耦合，也就是说，`mixin2`类将对`mixin`或`impl`的数据成员一无所知：\n\n```cpp\n    mixin2< mixin<impl> > obj; \n    obj.impl::init(/* parameters */);  // call impl::init \n    obj.mixin::init(/* parameters */); // call mixin::init \n    obj.init(/* parameters */);        // call mixin2::init \n    obj.something();\n```\n\n这是可行的，因为只要限定方法的名称，就可以调用公共基类方法。 这三个`init`方法中的参数列表可以不同。 然而，这确实带来了一个问题，即客户端现在必须初始化链中的所有基类。\n\n这是 Microsoft 的**ActiveX 模板库**(**ATL**)(现在是 MFC 的一部分)用来提供标准 COM 接口实现的方法。\n\n# 使用多态性\n\n在下面的示例中，我们将创建模拟 C++ 开发人员团队的代码。 代码将使用接口来分离类，这样就可以在不更改类的情况下更改类使用的服务。 在这个模拟中，我们有一个管理团队的经理，所以经理的一个属性就是他们的团队。 此外，每个员工，无论是经理还是团队成员，都有一些共同的属性和行为--他们都有自己的名字和工作岗位，都做着某种工作。\n\n为章节创建一个文件夹，并在该文件夹中创建一个名为`team_builder.cpp`的文件，由于此应用将使用`vector`、智能指针和文件，因此请在文件顶部添加以下行：\n\n```cpp\n    #include <iostream> \n    #include <string> \n    #include <vector> \n    #include <fstream> \n    #include <memory> \n    using namespace std;\n```\n\n应用将具有命令行参数，但目前只需提供`main`函数的空副本：\n\n```cpp\n    int main(int argc, const char *argv[]) \n    { \n        return 0;  \n    }\n```\n\n我们将定义接口，因此在`main`函数之前添加以下内容：\n\n```cpp\n    #define interface struct\n```\n\n这只是语法上的甜头，但它使代码更具可读性，以显示抽象类的用途。 在此下面，添加以下接口：\n\n```cpp\n    interface IWork \n    { \n        virtual const char* get_name() = 0; \n        virtual const char* get_position() = 0; \n        virtual void do_work() = 0; \n    }; \n\n    interface IManage \n    { \n        virtual const vector<unique_ptr<IWork>>& get_team() = 0; \n        virtual void manage_team() = 0; \n    }; \n\n    interface IDevelop  \n    { \n        virtual void write_code() = 0; \n    };\n```\n\n所有工人都将实现第一个接口，该接口提供对他们的姓名和工作职位的访问，以及一个告诉他们做一些工作的方法。 我们将定义两种类型的工作者，一种是通过安排时间来管理团队的经理，另一种是编写代码的开发人员。 管理器有一个由`IWork*`个指针组成的`vector`个指针，由于这些指针将指向在空闲存储上创建的对象，因此`vector`个成员是包装这些指针的智能指针。 这就是说，管理者维护这些对象的生命周期：当管理者对象存在时，他们的团队也会存在。\n\n第一个操作是创建一个帮助器类，它执行工人的基本工作。 这样做的原因将在后面的示例中一目了然。 此类将实现`IWork`接口：\n\n```cpp\n    class worker : public IWork \n    { \n        string name; \n        string position; \n    public: \n        worker() = delete; \n        worker(const char *n, const char *p) : name(n), position(p) {} \n        virtual ~worker() {} \n        virtual const char* get_name() override  \n        { return this->name.c_str(); } \n        virtual const char* get_position() override  \n        { return this->position.c_str(); } \n        virtual void do_work() override { cout << \"works\" << endl; } \n    };\n```\n\n必须使用名称和职位创建`worker`对象。 我们还将为一位经理提供一个助手类：\n\n```cpp\n    class manager : public worker, public IManage \n    { \n        vector<unique_ptr<IWork>> team; \n    public: \n        manager() = delete; \n        manager(const char *n, const char* p) : worker(n, p) {} \n        const vector<unique_ptr<IWork>>& get_team() { return team; } \n        virtual void manage_team() override  \n        { cout << \"manages a team\" << endl; } \n        void add_team_member(IWork* worker) \n        { team.push_back(unique_ptr<IWork>(worker)); } \n        virtual void do_work() override { this->manage_team(); } \n    };\n```\n\n请注意，`do_work`方法是根据虚函数`manage_team`实现的，这意味着派生类只需要实现`manage_team`方法，因为它将从其父函数继承`do_work`方法，而方法调度将意味着调用了正确的方法。 类的其余部分很简单，但请注意，构造函数调用基类构造函数来初始化名称和工作位置(经理毕竟是工人)，并且`manager`类具有将项添加到智能指针中共享的团队的函数。\n\n要测试这一点，我们需要创建一个管理开发人员的`manager`类：\n\n```cpp\n    class project_manager : public manager \n    { \n    public: \n        project_manager() = delete; \n        project_manager(const char *n) : manager(n, \"Project Manager\") \n        {} \n        virtual void manage_team() override  \n        { cout << \"manages team of developers\" << endl; } \n    };\n```\n\n这将覆盖对基类构造函数的调用，该基类构造函数传递项目经理的姓名和描述作业的文字。 该类还覆盖`manage_team`来说明管理器的实际工作。 此时，您应该能够创建`project_manager`并将一些成员添加到他们的团队中(使用`worker`对象，您将很快创建开发人员)。 在`main`函数中添加以下内容：\n\n```cpp\n    project_manager pm(\"Agnes\"); \n    pm.add_team_member(new worker(\"Bill\", \"Developer\")); \n    pm.add_team_member(new worker(\"Chris\", \"Developer\")); \n    pm.add_team_member(new worker(\"Dave\", \"Developer\")); \n    pm.add_team_member(new worker(\"Edith\", \"DBA\"));\n```\n\n此代码将进行编译，但在运行时不会输出，因此请创建一个方法来打印经理团队：\n\n```cpp\n    void print_team(IWork *mgr) \n    { \n        cout << mgr->get_name() << \" is \"  \n             << mgr->get_position() << \" and \"; \n        IManage *manager = dynamic_cast<IManage*>(mgr); \n        if (manager != nullptr) \n        { \n            cout << \"manages a team of: \" << endl; \n            for (auto team_member : manager->get_team()) \n            { \n                cout << team_member->get_name() << \" \" \n                     << team_member->get_position() << endl; \n            } \n        } \n        else { cout << \"is not a manager\" << endl; } \n    }\n```\n\n此函数显示接口有多有用。 您可以将任何工人传递给该函数，它将打印出与所有工人相关的信息(姓名和工作职位)。 然后，它通过请求`IManage`接口来询问对象是否为管理器。 如果对象实现此接口，则该函数只能获取经理行为(在本例中，拥有一个团队)。 在`main`函数结束时，在最后一次调用`program_manager`对象之后，调用此函数：\n\n```cpp\n    print_team(&pm)\n```\n\n编译此代码(记住使用`/EHsc`开关)并运行代码。 您将获得以下输出：\n\n```cpp\n Agnes is Project Manager and manages a team of:\n Bill Developer\n Chris Developer\n Dave Developer\n Edith DBA\n```\n\n现在我们将添加一个多态性级别，因此在`print_team`函数之前添加以下类：\n\n```cpp\n    class cpp_developer : public worker, public IDevelop \n    { \n    public: \n        cpp_developer() = delete; \n        cpp_developer(const char *n) : worker(n, \"C++ Dev\") {} \n        void write_code() { cout << \"Writing C++ ...\" << endl; } \n        virtual void do_work() override { this->write_code(); } \n    }; \n\n    class database_admin : public worker, public IDevelop \n    { \n    public: \n        database_admin() = delete; \n        database_admin(const char *n) : worker(n, \"DBA\") {} \n        void write_code() { cout << \"Writing SQL ...\" << endl; } \n        virtual void do_work() override { this->write_code(); } \n    };\n```\n\n您可以更改`main`函数，以便不使用`worker`对象，而是对 Bill、Chris 和 Dave 使用`cpp_developer`，对 Edith 使用`database_admin`：\n\n```cpp\n    project_manager pm(\"Agnes\"); \n    pm.add_team_member(new cpp_developer(\"Bill\")); \n    pm.add_team_member(new cpp_developer(\"Chris\")); \n    pm.add_team_member(new cpp_developer(\"Dave\")); \n    pm.add_team_member(new database_admin(\"Edith\")); \n    print_team(&pm);\n```\n\n现在，您可以编译和运行代码，并且可以看到，您不仅可以将不同类型的对象添加到经理团队中，还可以通过`IWork`界面打印适当的信息。\n\n下一个任务是添加代码来序列化和反序列化这些对象。 序列化意味着将对象的状态(和类型信息)写入流，反序列化将获取该信息并创建具有指定状态的适当类型的新对象。 为此，每个对象都必须有一个构造函数，该构造函数接受指向反序列化程序对象的接口指针，并且构造函数应该调用该接口来提取正在创建的对象的状态。 此外，此类类应该实现一个方法来序列化对象的状态并将其写入序列化程序对象。 让我们首先来看一下序列化。 在文件顶部，添加以下接口：\n\n```cpp\n    #define interface struct \n\n interface IWork; \n    // forward declaration interface ISerializer { virtual void write_string(const string& line) = 0; virtual void write_worker(IWork *worker) = 0; virtual void write_workers ( const vector<unique_ptr<IWork>>& workers) = 0; }; interface ISerializable { virtual void serialize(ISerializer *stm) = 0; };\n```\n\n因为`ISerializer`接口使用`IWork`接口，所以需要转发声明。 第一个接口`ISerializer`由提供序列化服务的对象实现。 这可以基于文件、网络套接字、数据库或您想要用来存储对象的任何东西。 底层存储机制对于该接口的用户来说并不重要；重要的是该接口可以存储字符串，并且它可以存储使用`IWork`接口指针或此类对象的集合传递的整个对象。\n\n可以序列化的对象必须实现`ISerializable`接口，该接口只有一个方法，该方法接受指向将提供序列化服务的对象的接口指针。 在定义接口之后，添加以下类：\n\n```cpp\n    class file_writer : public ISerializer \n    { \n        ofstream stm; \n    public: \n        file_writer() = delete; \n        file_writer(const char *file) { stm.open(file, ios::out); } \n        ~file_writer() { close(); } \n        void close() { stm.close(); } \n        virtual void write_worker(IWork *worker) override \n        { \n            ISerializable *object = dynamic_cast<ISerializable*>(worker); \n            if (object != nullptr) \n            { \n                ISerializer *serializer = dynamic_cast<ISerializer*>(this); \n                serializer->write_string(typeid(*worker).raw_name()); \n         object->serialize(serializer); \n            } \n        } \n        virtual void write_workers( \n        const vector<unique_ptr<IWork>>& workers) override \n        { \n            write_string(\"[[\"); \n            for (const unique_ptr<IWork>& member : workers) \n            { \n                write_worker(member.get()); \n            } \n            write_string(\"]]\"); // end marker of team \n        } \n        virtual void write_string(const string& line) override \n        { \n            stm << line << endl; \n        } \n    };\n```\n\n该类为文件提供了`ISerializer`接口，因此`write_string`方法使用`ifstream`插入操作符将字符串写入文件中的一行。 `write_worker`方法将 Worker 对象写入文件。 为此，它首先询问 Worker 对象是否可以通过封装`IWork`接口和`ISerializable`接口来序列化自己。 如果 Worker 对象实现此接口，则序列化程序可以通过将`ISerializer`接口指针传递给 Worker 对象上的`serialize`方法来要求 Worker 对象序列化自己。 由 Worker 对象决定必须序列化的信息。 Worker 对象除了`ISerializer`接口之外对`file_writer`类一无所知，而`file_writer`类除了实现`IWork`和`ISerializable`接口之外对 Worker 对象一无所知。\n\n如果 Worker 对象是可序列化的，`write_worker`方法做的第一件事就是获取有关该对象的类型信息。 `IWork`接口将位于类(`project_manager`、`cpp_developer`或`database_admin`)上，因此取消引用指针将使`typeid`操作符能够访问类类型信息。 我们将原始类型名存储在序列化程序中，因为它是紧凑的。 一旦类型信息被序列化，我们就要求对象通过调用其`ISerializable`接口上的`serialize`方法来序列化自己。 Worker 对象将存储它需要的任何信息。\n\n经理对象需要序列化他们的团队，他们通过将 Worker 对象的集合传递给`write_workers`方法来实现这一点。 这表明正在序列化的对象是一个数组，方法是将它们写入两个标记`[[`和`]]`。 请注意，因为容器有`unique_ptr`个对象，所以没有复制构造函数，因为这意味着共享所有权。 因此，我们通过索引操作符访问项，这将为我们提供对容器内的`unique_ptr`对象的引用。\n\n现在，对于每个可以序列化的类，您必须从`ISerializable`派生类并实现`serialize`方法。 类继承树意味着一种 Worker 类型的每个类都派生自`worker`类，因此我们只需要此类从`ISerializable`接口继承：\n\n```cpp\n    class worker : public IWork, public ISerializable\n```\n\n约定是，类只序列化自己的状态，并委托其基类序列化基类对象。 继承树的顶部是`worker`类，因此在该类的底部添加以下接口方法：\n\n```cpp\n    virtual void serialize(ISerializer *stm) override \n    { \n        stm->write_string(name); \n        stm->write_string(position); \n    }\n```\n\n这只是将姓名和工作位置序列化到序列化程序。 请注意，Worker 对象不知道序列化程序将如何处理此信息，也不知道哪个类提供`ISerializer`接口。\n\n在`cpp_developer`类的底部，添加此方法：\n\n```cpp\n    virtual void serialize(ISerializer* stm) override \n    { worker::serialize(stm); }\n```\n\n`cpp_developer`类没有任何附加状态，因此它将序列化委托给其父类。 如果 Developer 类有一个状态，那么它将在序列化基对象之后序列化该状态。 将完全相同的代码添加到`database_admin`类的底部。\n\n`project_manager`类也调用其基类，但这是`manager`，因此将以下内容添加到`project_manager`类的底部：\n\n```cpp\n    virtual void serialize(ISerializer* stm) override \n    { manager::serialize(stm); }\n```\n\n`manager::serialize`更加复杂，因为该类具有应该序列化的状态：\n\n```cpp\n    virtual void serialize(ISerializer* stm) override \n    { \n        worker::serialize(stm); \n        stm->write_workers(this->team); \n    }\n```\n\n第一个操作是序列化基类：`worker`对象。 然后，代码序列化`manager`对象的状态，这意味着通过将此集合传递给序列化程序来序列化`team`数据成员。\n\n为了能够测试序列化，请在`main`方法之上创建一个方法，将`project_manager`代码移到新方法中，然后添加代码以序列化对象：\n\n```cpp\n    void serialize(const char* file) \n    { \n        project_manager pm(\"Agnes\"); \n        pm.add_team_member(new cpp_developer(\"Bill\")); \n        pm.add_team_member(new cpp_developer(\"Chris\")); \n        pm.add_team_member(new cpp_developer(\"Dave\")); \n        pm.add_team_member(new database_admin(\"Edith\")); \n        print_team(&pm); \n\n        cout << endl << \"writing to \" << file << endl; \n\n        file_writer writer(file); \n        ISerializer* ser = dynamic_cast<ISerializer*>(&writer); \n        ser->write_worker(&pm); \n        writer.close(); \n    }\n```\n\n前面的代码为指定的文件创建一个`file_writer`对象，获取该对象的`ISerializer`接口，然后序列化项目管理器对象。 如果您有其他团队，则可以在关闭`writer`对象之前将它们序列化到文件中。\n\n`main`函数将接受两个参数。 第一个是文件名，第二个是字符`r`或`w`(读取或写入文件)。 添加以下代码以替换`main`函数：\n\n```cpp\n    void usage() \n    { \n        cout << \"usage: team_builder file [r|w]\" << endl; \n        cout << \"file is the name of the file to read or write\" << endl; \n        cout << \"provide w to file the file (the default)\" << endl; \n        cout << \"        r to read the file\" << endl; \n    } \n\n    int main(int argc, char* argv[]) \n    { \n        if (argc < 2) \n        { \n            usage(); \n            return 0; \n        } \n\n        bool write = true; \n        const char *file = argv[1]; \n        if (argc > 2) write = (argv[2][0] == 'w'); \n\n        cout << (write ? \"Write \" : \"Read \") << file << endl << endl; \n\n        if (write) serialize(file); \n        return 0; \n    }\n```\n\n现在，您可以编译并运行此代码，给出一个文件名：\n\n```cpp\n    team_builder cpp_team.txt w\n```\n\n这将创建一个名为`cpp_team.txt`的文件，其中包含有关团队的信息；在命令行中使用`**type cpp_team.txt**`键入该文件：\n\n```cpp\n    .?AVproject_manager@@ \n    Agnes \n    Project Manager \n    [[ \n    .?AVcpp_developer@@ \n    Bill \n    C++ Dev \n    .?AVcpp_developer@@ \n    Chris \n    C++ Dev \n    .?AVcpp_developer@@ \n    Dave \n    C++ Dev \n    .?AVdatabase_admin@@ \n    Edith \n    DBA \n    ]]\n```\n\n该文件不是供人读取的，但如您所见，它的每一行都有一条信息，并且每个序列化的对象前面都有类的类型。\n\n现在，您将编写反序列化对象的代码。 代码需要一个读取序列化数据并返回 Worker 对象的类。 此类与序列化程序类紧密耦合，但应该通过接口访问它，这样它就不会耦合到 Worker 对象。 在声明`ISerializable`接口之后，添加以下内容：\n\n```cpp\n    interface IDeserializer \n    { \n        virtual string read_string() = 0; \n        virtual unique_ptr<IWork> read_worker() = 0; \n        virtual void read_workers(vector<unique_ptr<IWork>>& team) = 0; \n    };\n```\n\n第一个方法获取序列化字符串，其他两个方法获取单个对象和对象集合。 由于这些工作对象将在空闲存储上创建，因此这些方法使用智能指针。 每个类都可以序列化自己，所以现在您将使每个可序列化类能够反序列化自己。 为此，对于实现`ISerializable`的每个类，添加一个接受`IDeserializer`接口指针的构造函数。 从`worker`类开始；添加以下公共构造函数：\n\n```cpp\n    worker(IDeserializer *stm) \n    { \n        name = stm->read_string(); \n        position = stm->read_string(); \n    }\n```\n\n本质上，这与`serialize`方法的作用相反，它从反序列化程序*读取名称和位置字符串，顺序与它们传递给序列化程序的顺序*相同。 由于`cpp_developer`和`database_admin`类没有状态，因此除了调用基类构造函数外，它们不需要执行任何其他反序列化工作。 例如，将以下公共构造函数添加到`cpp_developer`类：\n\n```cpp\n    cpp_developer(IDeserializer* stm) : worker(stm) {}\n```\n\n向`database_admin`类添加类似的构造函数。\n\n经理们有一个状态，所以需要做更多的工作来反序列化他们。 将以下内容添加到`manager`类：\n\n```cpp\n    manager(IDeserializer* stm) : worker(stm) \n    { stm->read_workers(this->team); }\n```\n\n初始值设定项列表构造基类，运行后，构造函数通过调用`IDeserializer`接口上的`read_workers`来使用零个或多个工作对象初始化`team`集合。 最后，`project_manager`类派生自`manager`类，但没有添加额外的状态，因此添加以下构造函数：\n\n```cpp\n    project_manager(IDeserializer* stm) : manager(stm) {}\n```\n\n现在，每个可序列化的类都可以反序列化自己，下一个操作是编写将读取文件的反序列化程序类。 在`file_writer`类之后添加以下内容(请注意，没有内联实现两个方法)：\n\n```cpp\n    class file_reader : public IDeserializer \n    { \n        ifstream stm; \n    public: \n        file_reader() = delete; \n        file_reader(const char *file) { stm.open(file, ios::in); } \n        ~file_reader() { close(); } \n        void close() { stm.close(); } \n        virtual unique_ptr<IWork> read_worker() override; \n        virtual void read_workers( \n            vector<unique_ptr<IWork>>& team) override; \n        virtual string read_string() override \n        { \n            string line; \n            getline(stm, line); \n            return line; \n        } \n    };\n```\n\n构造函数打开指定的文件，析构函数将其关闭。 `read_string`接口方法从文件中读取一行并将其作为字符串返回。 主要工作在这里没有实现的两个接口方法中执行。 `read_workers`方法将读取`IWork`对象的集合，并将它们放入通过引用传递的集合中。 此方法将为文件中的每个对象调用`read_worker`方法，并将其放入集合中，因此读取文件的主要工作在此方法中执行。 `read_worker`方法是类中唯一与可序列化类有耦合的部分，因此，它必须在 Worker 类的定义下定义。 在`serialize`全局函数上方添加以下内容：\n\n```cpp\n    unique_ptr<IWork> file_reader::read_worker() \n    { \n    } \n    void file_reader::read_workers(vector<unique_ptr<IWork>>& team) \n    { \n        while (true) \n        { \n            unique_ptr<IWork> worker = read_worker(); \n            if (!worker) break; \n            team.push_back(std::move(worker)); \n        } \n    }\n```\n\n`read_workers`方法将使用`read_worker`方法从文件中读取每个对象，该方法返回`unique_ptr`对象中的每个对象。 我们希望将此对象放入容器中，但因为指针应该具有独占所有权，所以我们需要将所有权移到容器中的对象中。 有两种方法可以做到这一点。 第一种方法是简单地使用对`read_worker`的调用作为`push_back`的参数。 `read_worker`方法返回一个临时对象，它是一个右值，因此编译器在容器中创建对象时将使用移动语义。 我们之所以不这样做，是因为`read_worker`方法可能返回`nullptr`(我们要测试它)，因此我们创建了一个新的`unique_ptr`对象(Move 语义将所有权传递给该对象)，一旦我们测试出该对象不是`nullptr`，我们就调用标准库函数`move`，将该对象复制到容器中。\n\n如果`read_worker`方法读取数组的结束标记，则它返回`nullptr`，因此`read_workers`方法循环，读取每个 Worker 并将其放入集合中，直到返回`nullptr`。\n\n按如下方式实现`read_worker`方法：\n\n```cpp\n    unique_ptr<IWork> file_reader::read_worker() \n    { \n        string type = read_string(); \n        if (type == \"[[\") type = read_string(); \n        if (type == \"]]\") return nullptr; \n        if (type == typeid(worker).raw_name()) \n        { \n            return unique_ptr<IWork>( \n            dynamic_cast<IWork*>(new worker(this))); \n        }    \n        return nullptr; \n    }\n```\n\n第一行从文件中读取 Worker 对象的类型信息，以便它知道要创建什么对象。 由于文件将具有指示团队成员数组的标记，因此代码必须检测这些标记。 如果检测到数组的开始，则忽略标记字符串，并读取下一行以获取组中第一个对象的类型。 如果读取了结束标记，则这是数组的末尾，因此返回`nullptr`。\n\n此处显示了`worker`对象的代码。 `if`语句测试以检查类型字符串是否与`worker`类的原始名称相同。 如果是，那么我们必须创建一个`worker`对象，并通过调用接受`IDeserializer`指针的构造函数来请求它反序列化自己。 在空闲存储上创建`worker`对象，并调用`dynamic_cast`操作符以获取`IWork`接口指针，然后使用该指针初始化智能指针对象。 `unique_ptr`的构造函数是`explicit`，所以您必须调用它。 现在为所有其他可序列化类添加类似的代码：\n\n```cpp\n    if (type == typeid(project_manager).raw_name()) \n    { \n        return unique_ptr<IWork>( \n        dynamic_cast<IWork*>(new project_manager(this))); \n    } \n    if (type == typeid(cpp_developer).raw_name()) \n    { \n        return unique_ptr<IWork>( \n        dynamic_cast<IWork*>(new cpp_developer(this))); \n    } \n    if (type == typeid(database_admin).raw_name()) \n    { \n        return unique_ptr<IWork>( \n        dynamic_cast<IWork*>(new database_admin(this))); \n    }\n```\n\n最后，您需要创建一个`file_reader`并反序列化一个文件。 在`serialize`函数之后，添加以下内容：\n\n```cpp\n    void deserialize(const char* file) \n    { \n        file_reader reader(file); \n        while (true) \n        { \n            unique_ptr<IWork> worker = reader.read_worker(); \n            if (worker) print_team(worker.get()); \n            else break; \n        } \n        reader.close(); \n    }\n```\n\n这段代码简单地创建了一个基于文件名的`file_reader`对象，然后从打印出该对象的文件中读取每个 Worker 对象，如果是`project_manager`，则打印出他们的团队。 最后，在`main`函数中添加一行以调用此函数：\n\n```cpp\n    cout << (write ? \"Write \" : \"Read \") << file << endl << endl; \n    if (write) serialize(file); \n else deserialize(file);\n```\n\n现在，您可以编译代码并使用它读入包含以下内容的序列化文件：\n\n```cpp\n    team_builder cpp_team.txt r\n```\n\n(请注意`r`参数。)。 代码应该打印出您序列化到文件中的对象。\n\n前面的示例表明，您可以编写不知道用于序列化的机制的可序列化对象。 如果要使用与平面文件不同的机制(例如，XML 文件或数据库)，则不需要更改任何 Worker 类。 相反，您需要编写一个适当的类来实现`ISerializer`接口和`IDeserailizer`接口。 如果需要创建另一个 Worker 类，只需更改`read_worker`方法来反序列化该类型的对象。\n\n# 简略的 / 概括的 / 简易判罪的 / 简易的\n\n在本章中，您了解了如何使用 C++ 继承重用代码并提供对象之间的 is-a 关系。 您还了解了如何使用它来实现多态性，其中相关对象可以被视为具有相同的行为，同时仍然可以保持调用每个对象的方法的能力，以及将行为分组在一起的接口。 在下一章中，您将看到 C++ 标准库的特性以及它提供的各种实用程序类。**"
  },
  {
    "path": "docs/begin-cpp-prog/08.md",
    "content": "# 八、使用标准库容器\n\n标准库提供了几种类型的容器；每种类型的容器都是通过模板化的类提供的，因此容器的行为可以用于任何类型的项。 有一些用于顺序容器的类，其中容器中项的顺序取决于项插入容器的顺序。 此外，还有已排序和未排序的关联容器，它们将值与键相关联，随后使用键访问值。\n\n虽然不是容器本身，但在本章中，我们还将介绍两个相关的类：`pair`，它将两个值链接在一个对象中；以及`tuple`，它可以在单个对象中保存一个或多个值。\n\n# 使用对和元组\n\n在许多情况下，您会希望将两个项关联在一起；例如，关联容器允许您创建一种类型的数组，其中使用数字以外的项作为索引。 `<utility>`头文件包含一个名为`pair`的模板化类，它有两个名为`first`和`second`的数据成员。\n\n```cpp\n    template <typename T1, typename T2> \n    struct pair \n    { \n        T1 first; \n        T2 second; \n        // other members \n    };\n```\n\n由于类是模板化的，这意味着您可以关联任何项，包括指针或引用。 访问成员很简单，因为它们是公共的。 您还可以使用`get`模板化函数，因此对于`pair`对象`p`，您可以调用`get<0>(p)`而不是`p.first`。 该类还有一个复制构造函数和一个移动构造函数，以便您可以从另一个对象创建对象。 还有一个名为`make_pair`的函数，它将从参数中推断成员的类型：\n\n```cpp\n    auto name_age = make_pair(\"Richard\", 52);\n```\n\n要小心，因为编译器将使用它认为最合适的类型；在本例中，创建的`pair`对象将是`pair<const char*, int>`，但如果希望`first`项是`string`，使用构造函数会更简单。 您可以比较`pair`个对象；对第一个成员执行比较，只有当它们相等时才对第二个成员进行比较：\n\n```cpp\n    pair <int, int> a(1, 1); \n    pair <int, int> a(1, 2); \n    cout << boolalpha; \n    cout << a << \" < \" << b << \" \" << (a < b) << endl;\n```\n\n参数可以是参照：\n\n```cpp\n    int i1 = 0, i2 = 0; \n    pair<int&, int&> p(i1, i2); \n    ++ p.first; // changes i1\n```\n\n`make_pair`函数将从参数中推导出类型。 编译器无法区分变量和对变量的引用。 在 C++ 11 中，您可以使用`ref`函数(在`<functional>`中)指定`pair`将用于引用：\n\n```cpp\n    auto p2 = make_pair(ref(i1), ref(i2)); \n    ++ p2.first; // changes i1\n```\n\n如果要从一个函数返回两个值，可以通过通过引用传递的参数来实现，但是代码的可读性较差，因为您希望返回值通过函数的返回而不是通过其参数来实现。 `pair`类允许您在一个对象中返回两个值。 `<algorithm>`中的`minmax`函数就是一个例子。 这将返回一个`pair`对象，其中包含的参数从最小到最小的顺序排列，如果不应该使用默认操作符`<`，则会有一个重载，您可以在其中提供谓词对象。 以下内容将打印`{10,20}`：\n\n```cpp\n    auto p = minmax(20,10);  \n    cout << \"{\" << p.first << \",\" << p.second << \"}\" << endl;\n```\n\n`pair`类关联两个项目。 标准库提供了具有类似功能的`tuple`类，但是由于模板是可变的，这意味着您可以拥有任意数量的任意类型的参数。 但是，数据成员并不像`pair`中那样命名，而是通过模板化的`get`函数访问它们：\n\n```cpp\n    tuple<int, int, int> t3 { 1,2,3 }; \n    cout << \"{\" \n        << get<0>(t3) << \",\" << get<1>(t3) << \",\" << get<2>(t3)  \n        << \"}\" << endl; // {1,2,3}\n```\n\n第一行创建包含三个`int`项的`tuple`，并使用初始化列表对其进行初始化(您可以使用构造函数语法)。 然后，通过使用模板参数指示项目索引的`get`函数版本访问对象中的每个数据成员，将`tuple`打印到控制台。 请注意，索引是模板参数，因此不能在运行时使用变量提供它。 如果这是您想要做的，那么它清楚地表明您需要使用像`vector`这样的容器。\n\n函数`get`返回一个引用，因此可以用它来更改项目的值。 对于`tuple t3`，此代码将第一项更改为`42`，将第二项更改为`99`：\n\n```cpp\n    int& tmp = get<0>(t3); \n    tmp = 42; \n    get<1>(t3) = 99;\n```\n\n您也可以使用`tie`函数一次调用提取所有项目：\n\n```cpp\n    int i1, i2, i3; \n    tie(i1, i2, i3) = t3; \n    cout << i1 << \",\" << i2 << \",\" << i3 << endl;\n```\n\n函数的作用是：返回一个`tuple`，其中每个参数都是一个引用，并被初始化为您作为参数传递的变量。 如果您这样编写，前面的代码更容易理解：\n\n```cpp\n    tuple<int&, int&, int&> tr3 = tie(i1, i2, i3); \n    tr3 = t3;\n```\n\n可以从`pair`对象创建`tuple`对象，因此也可以使用`tie`函数从`pair`对象中提取值。\n\n有一个名为`make_tuple`的帮助器函数，它将推断参数的类型。 与`make_pair`函数一样，您必须小心扣减，因此浮点数将被推断为`double`，整数将被演绎为`int`。 如果希望参数是对特定变量的引用，可以使用`ref`函数或`cref`函数引用`const`。\n\n您可以比较`tuple`个对象，只要有相等数量的项目和等效的类型即可。 编译器将拒绝编译具有不同项数的`tuple`个对象的比较，或者如果一个`tuple`对象的项的类型无法转换为另一个`tuple`对象的类型。\n\n# 集装箱\n\n标准库容器允许您将零个或多个相同类型的项组合在一起，并通过迭代器顺序访问它们。 每个这样的对象都有一个向第一个项目返回迭代器对象的`begin`方法和一个返回容器中最后一个项目之后的项目的迭代器对象的`end`函数。 迭代器对象支持类似指针的算法，因此`end() - begin()`将给出容器中的项数。 所有容器类型都将实现`empty`方法来指示容器中是否没有项，并且(除了`forward_list`)`size`方法是容器中的项数。 您很想遍历容器，就好像它是一个数组：\n\n```cpp\n    vector<int> primes{1, 3, 5, 7, 11, 13}; \n    for (size_t idx = 0; idx < primes.size(); ++ idx)  \n    { \n        cout << primes[idx] << \" \"; \n    } \n    cout << endl;\n```\n\n问题是，并非所有容器都允许随机访问，如果您认为使用另一个容器更有效，则必须更改访问容器的方式。 如果要使用模板编写泛型代码，则此代码也不能很好地工作。 前面的代码最好使用迭代器编写：\n\n```cpp\n    template<typename container> void print(container& items) \n    { \n        for (container::iterator it = items.begin();  \n        it != items.end(); ++ it) \n        { \n            cout << *it << \" \"; \n        } \n        cout << endl; \n    }\n```\n\n所有容器都有一个名为`iterator`的`typedef`成员，它给出了从`begin`方法返回的迭代器的类型。 迭代器对象的行为类似于指针，因此您可以使用取消引用操作符获取迭代器引用的项，并使用增量操作符移动到下一项。\n\n对于除`vector`之外的所有容器，可以保证即使删除了其他元素，迭代器也将保持有效。 如果插入项，则只有`lists`、`forward_lists`和关联的容器保证迭代器保持有效。 迭代器将在后面进行更深入的介绍。\n\n所有容器都必须具有名为`swap`的异常安全(Nothrot)方法，并且(有两个异常)它们必须具有*事务性*语义；也就是说，操作必须成功或失败。 如果操作失败，则容器的状态与调用操作之前相同。 对于每个容器，当涉及到多元素插入时，这一规则是宽松的。 例如，如果使用迭代器范围一次插入多个项，而该范围中的一项插入失败，则该方法将无法撤消以前的插入。\n\n需要指出的是，对象被复制到容器中，因此放入容器中的对象类型必须具有复制和复制赋值操作符。 另外，请注意，如果将派生类对象放入需要基类对象的容器中，则复制将对该对象进行切片，这意味着与派生类有关的任何操作都将被删除(数据成员和虚方法指针)。\n\n# 序列容器\n\n序列容器存储一系列项及其存储顺序，当您使用迭代器访问它们时，这些项将按照放入容器的顺序进行检索。 创建容器后，可以使用库函数更改排序顺序。\n\n# 表 / 清单 / 镶边 / 目录\n\n顾名思义，`list`对象是由双向链表实现的，其中每一项都有一个指向下一项和前一项的链接。 这意味着可以快速插入项(如[第 4 章](04.html)，*使用内存、数组和指针*中的示例，使用单链表显示)，但由于在链表中，项只能访问它前面和后面的项，所以不能使用`[]`索引运算符进行随机访问。\n该类允许您通过构造函数提供值，也可以使用成员方法。 例如，`assign`方法允许您使用初始值设定项列表在一个操作中填充容器，或者使用迭代器填充到另一个容器中的某个范围。 也可以使用`push_back`或`push_front`方法插入单个项目：\n\n```cpp\n    list<int> primes{ 3,5,7 }; \n    primes.push_back(11); \n    primes.push_back(13); \n    primes.push_front(2); \n    primes.push_front(1);\n```\n\n第一行创建一个包含`3`、`5`和`7`的`list`对象，然后(按该顺序)将`11`和`13`推到末尾，以便`list`包含`{3,5,7,11,13}`。 然后，代码将数字`2`和`1`推到前面，这样最终的`list`就是`{1,2,3,5,7,11,13}`。 不管名称如何，`pop_front`和`pop_back`方法只删除列表前面或后面的项，但不会返回该项。 如果要获取已移除的项目，必须先*通过`front`或`back`方法访问该项目：*\n\n```cpp\n    int last = primes.back(); // get the last item \n    primes.pop_back();        // remove it\n```\n\n`clear`方法将删除`list`中的所有项，`erase`方法将删除项。 有两个版本：一个具有标识单个项的迭代器，另一个具有指示范围的两个迭代器。 通过提供范围中的第一项和在范围之后的项*来指示范围。*\n\n```cpp\n    auto start = primes.begin(); // 1 \n    start++ ;                     // 2 \n    auto last = start;           // 2 \n    last++ ;                      // 3 \n    last++ ;                      // 5 \n    primes.erase(start, last);   // remove 2 and 3\n```\n\n这是迭代器和标准库容器的一般原则；范围由迭代器指示，由第一个项目和最后一个项目之后的项目*表示。 `remove`方法将删除具有指定值的所有项：*\n\n```cpp\n    list<int> planck{ 6,6,2,6,0,7,0,0,4,0 }; \n    planck.remove(6);            // {2,0,7,0,0,4,0}\n```\n\n还有一个方法`remove_if`，它接受一个谓词，只有在该谓词返回`true`时才会删除一项。 同样，您可以使用迭代器将项插入到列表中，并将该项插入到指定项之前：\n\n```cpp\n    list<int> planck{ 6,6,2,6,0,7,0,0,4,0 }; \n    auto it = planck.begin(); \n    ++ it; \n    ++ it; \n    planck.insert(it, -1); // {6,6,-1,2,6,0,7,0,0,4,0}\n```\n\n您还可以指定应在该位置多次插入项目(如果插入，则插入多少个副本)，并且可以提供要一次插入的多个项目。 当然，如果您传递的迭代器是通过调用`begin`方法获得的，则该项将插入到`list`的开头。 通过调用`push_front`方法也可以实现同样的目的。 类似地，如果迭代器是通过调用`end`方法获得的，则在`list`的末尾插入该项，这与调用`push_back`相同。\n\n当您调用`insert`方法时，您提供了一个对象，该对象将被复制到`list`或移动到`list`(通过右值语义)。 该类还提供了几个**emplace**方法(`emplace`、`emplace_front`和`emplace_back`)，它们将根据您提供的数据构造一个新对象，并将该对象插入到`list`中。 例如，如果您有一个可以从两个`double`值创建的`point`类，则可以通过提供两个`double`值来`insert`构造`point`对象或`emplace``point`对象：\n\n```cpp\n    struct point \n    { \n        double x = 0, y = 0; \n        point(double _x, double _y) : x(_x), y(_y) {} \n    }; \n\n    list<point> points; \n    point p(1.0, 1.0); \n    points.push_back(p); \n    points.emplace_back(2.0, 2.0);\n```\n\n一旦创建了`list`，就可以使用成员函数对其进行操作。 `swap`方法将合适的`list`对象作为参数，它将参数中的项移动到当前对象中，并将当前`list`中的项移动到参数中。 因为`list`对象是使用链表实现的，所以这个操作很快。\n\n```cpp\n    list<int> num1 { 2,7,1,8,2,8 }; // digits of Euler's number \n    list<int> num2 { 3,1,4,5,6,8 }; // digits of pi \n    num1.swap(num2);\n```\n\n此后，代码`num1`将包含`{3,1,4,5,6,8}`，`num2`将包含`{2,7,1,8,2,8}`，如下所示：\n\n![](img/0bf7a5fa-4bd5-47b5-a66f-797feb112b08.png)\n\n`list`将按照项插入容器的顺序保存项；但是，您可以通过调用`sort`方法对它们进行排序，默认情况下，该方法将使用`<`运算符对`list`容器中的项按升序排序。 您还可以为比较操作传递函数对象。 排序后，您可以通过调用`reverse`方法来颠倒项目的顺序。 可以合并两个排序的列表，这涉及到从参数列表中提取项目并将其插入调用列表中，其顺序如下：\n\n```cpp\n    list<int> num1 { 2,7,1,8,2,8 }; // digits of Euler's number \n    list<int> num2 { 3,1,4,5,6,8 }; // digits of pi \n    num1.sort();                    // {1,2,2,7,8,8} \n    num2.sort();                    // {1,3,4,5,6,8} \n    num1.merge(num2);               // {1,1,2,2,3,4,5,6,7,8,8,8}\n```\n\n合并两个列表可能会导致重复，可以通过调用`unique`方法将其删除：\n\n```cpp\n    num1.unique(); // {1,2,3,4,5,6,7,8}\n```\n\n# 转发列表\n\n顾名思义，`forward_list`类类似于`list`类，但它只允许在列表前面插入和删除项。 这还意味着与类一起使用的迭代器只能递增；编译器将拒绝允许您递减这样的迭代器。 该类有`list`方法的子集，因此它有`push_front`、`pop_front`和`emplace_front`方法，但没有相应的`_back`方法。 它还实现了一些其他方法，因为列表项只能向前访问，这意味着插入将发生在现有项之后，因此该类实现了`insert_after`和`emplace_after`。\n类似地，您可以删除列表开头(`pop_front`)或指定项(`erase_after`)后面的项，或者告诉类在列表中正向迭代并删除具有特定值的项(`remove`和`remove_if`)：\n\n```cpp\n    forward_list<int> euler { 2,7,1,8,2,8 }; \n    euler.push_front(-1);       // { -1,2,7,1,8,2,8 } \n    auto it = euler.begin();    // iterator points to -1 \n    euler.insert_after(it, -2); // { -1,-2,2,7,1,8,2,8 } \n    euler.pop_front();          // { -2,2,7,1,8,2,8 } \n    euler.remove_if([](int i){return i < 0;}); \n                                // { 2,7,1,8,2,8 }\n```\n\n在前面的代码中，用欧拉数的数字初始化`euler`，并将值`-1`推到前面。 接下来，获得一个迭代器，该迭代器指向容器中的第一个值；即指向`-1`的值的位置。 在迭代器的位置之后插入值`-2`；也就是说，在值`-1`之后插入`-2`。 最后两行显示如何删除项；`pop_front`删除容器前面的项，`remove_if`将删除满足谓词的项(在本例中，当项小于零时)。\n\n# 向量 / 载体 / 航线 / 带菌者\n\n`vector`类具有动态数组的行为；也就是说，可以对项进行索引随机访问，并且容器将随着向其中插入更多项而增长。 您可以使用初始化列表和指定数量的项目副本来创建`vector`对象。 还可以通过传递指示该容器中项范围的迭代器，使`vector`基于另一个容器中的值。 您可以通过提供一个 Capacity 作为构造函数参数来创建一个具有预定大小的向量，容器中将创建指定数量的默认项。 如果在以后需要指定容器大小，可以调用`reserve`方法指定最小大小，或者调用`resize`方法，这可能意味着删除多余的项或创建新项，具体取决于现有的`vector`对象是大于还是小于请求的大小。\n\n当您将项插入到`vector`容器中时，如果没有分配足够的内存，则容器将分配足够的内存。 这将涉及分配新内存、将现有项复制到新内存中、创建新项，最后销毁项的旧副本并重新分配旧内存。 显然，如果您知道项的数量，并且知道如果没有新的分配，`vector`容器将无法容纳它们，则应该通过调用`reserve`方法来指示需要多少空间。\n\n插入构造函数以外的项非常简单。 您可以使用`push_back`在末尾插入一个项目(这是一个快速操作，假设不需要分配)，也可以使用`pop_back`删除最后一个项目。 还可以使用`assign`方法清除整个容器并插入指定的项(相同项的倍数、项的初始值设定项列表或使用迭代器指定的另一个容器中的项)。 与`list`对象一样，您可以清除整个`vector`、在某个位置拭除项目或在指定位置插入项目。 但是，没有与`remove`方法等效的方法来删除具有特定值的项。\n\n使用`vector`类的主要原因是使用`at`方法或`[]`索引运算符获得随机访问：\n\n```cpp\n   vector<int> distrib(10); // ten intervals \n   for (int count = 0; count < 1000; ++ count) \n   { \n      int val = rand() % 10; \n      ++ distrib[val]; \n   } \n   for (int i : distrib) cout << i << endl;\n```\n\n第一行创建一个包含 10 个项目的`vector`，然后在循环中，每次调用 C 运行时函数`rand`1000 次，以获得 0 到 32767 之间的伪随机数。 模运算符用于获得大致介于 0 和 9 之间的随机数。该随机数随后被用作`distrib`对象的索引，以选择指定的项目，然后递增。 最后，打印出分发内容，正如您所预期的那样，这会给出每一项的值大约为 100。\n\n此代码依赖于这样一个事实，即`[]`操作符返回对项目的引用，这就是项目可以以这种方式递增的原因。 `[]`运算符可用于读取和写入容器中的项。 容器通过`begin`和`end`方法以及(因为容器适配器需要它们)`front`和`back`方法提供迭代器访问。\n\n`vector`对象可以包含任何具有复制构造函数和赋值运算符的类型，这意味着所有内置类型。 按照目前的情况，`bool`个项目的`vector`会浪费内存，因为布尔值可以存储为单个位，而编译器会将`bool`视为整数(32 位)。 标准库为`bool`提供了`vector`类的专门化，可以更高效地存储项目。 然而，尽管乍一看这个类看起来是个好主意，但问题是，因为容器以位的形式保存布尔值，这意味着`[]`操作符不返回对`bool`的引用(相反，它返回一个行为类似的对象)。\n如果您希望保存布尔值并对其进行操作，那么只要您在编译时知道有多少项，`bitset`类可能是更好的选择。\n\n# 让我告诉你一件事\n\n名称`deque`表示*双端队列*，这意味着它可以从两端增长，虽然您可以在中间插入项，但成本更高。 作为队列，这意味着项目是有序的，但是，因为项目可以从两端放入队列，所以顺序不一定与将项目放入容器的顺序相同。\n\n`deque`的接口类似于`vector`，因此您可以使用`at`函数和`[]`运算符进行迭代器访问和随机访问。 与`vector`一样，您可以使用`push_back`、`pop_back`和`back`方法从`deque`容器的末尾访问项，但与`vector`不同的是，您还可以使用`push_front`、`pop_front`和`front`方法访问`deque`容器的前面。 虽然`deque`类有一些方法允许您在容器内插入和擦除项，`resize`，但这些操作的开销很大，如果您需要使用它们，那么您应该重新考虑使用此容器类型。 此外，`deque`类没有预分配内存的方法，因此，当您向该容器添加项时，可能会导致内存分配。\n\n# 关联容器\n\n使用类似 C 的`array`或`vector`，每一项都与其数字索引相关联。 早些时候，在`vector`一节中的一个示例中就利用了这一点，在该示例中，索引提供了分布的小数，并且为了方便起见，以十进制数据编号的方式对分布进行了拆分。\n\n关联容器允许您提供非数字的索引；这些是键，您可以将值与其关联。 当您将键-值对插入到容器中时，将对它们进行排序，以便容器随后可以通过其键高效地访问值。 通常，此顺序对您来说应该无关紧要，因为您不会使用容器按顺序访问项，而是通过键访问值。 典型的实现将使用二叉树或哈希表，这意味着根据项的关键字查找项是一种快速操作。\n\n对于有序容器(如`map`)，将使用`<`(较少的谓词)在容器中的键和现有键之间进行比较。 默认谓词意味着比较键，如果这是一个智能指针，那么将比较和用于排序的将是智能指针对象，而不是它们包装的对象。 在这种情况下，您需要编写自己的谓词来执行适当的比较，并将其作为模板参数传递。\n\n这意味着插入或擦除项通常很昂贵，并且键被视为不可变的，因此您不能为项更改它。 对于所有关联容器，没有 Remove 方法，但有 Erase 方法。 但是，对于那些保持项目排序的容器，擦除项目可能会影响性能。\n\n关联容器有几种类型，主要区别在于它们如何处理重复键和出现的排序级别。 `map`类具有按唯一键排序的键值对，因此不允许重复键。 如果您希望允许重复键，则可以使用`multimap`类。 `set`类本质上是一个映射，其中键与值相同，同样不允许重复。 `multiset`类不允许重复。\n\n有一个键与值相同的关联类可能看起来很奇怪，但在本节中包含该类的原因是，与`map`类一样，`set`类也有一个类似的接口来查找值。 同样类似于`map`类，`set`类查找项目的速度也很快。\n\n# 贴图和多重贴图\n\n`map`容器存储两个不同的项，一个键和一个值，并根据键按排序顺序维护这些项。 排序的`map`表示快速定位项目。 该类具有与其他容器相同的接口来添加项：您可以通过构造函数将它们放入容器中，也可以使用成员方法`insert`和`emplace`。 您还可以通过迭代器访问项。 当然，迭代器提供对单个值的访问，因此使用映射将访问同时具有键和值的`pair`对象：\n\n```cpp\n    map<string, int> people; \n    people.emplace(\"Washington\", 1789); \n    people.emplace(\"Adams\", 1797); \n    people.emplace(\"Jefferson\", 1801); \n    people.emplace(\"Madison\", 1809); \n    people.emplace(\"Monroe\", 1817); \n\n    auto it = people.begin(); \n    pair<string, int> first_item = *it; \n    cout << first_item.first << \" \" << first_item.second << endl;\n```\n\n对`emplace`的调用将项放入`map`，其中关键字是`string`(总统的名字)，值是`int`(总统开始任期的年份)。 然后，代码获得容器中第一个项目的迭代器，并通过取消引用迭代器来访问该项目，从而给出一个`pair`对象。 由于项目按排序顺序存储在`map`中，因此第一个项目将设置为`\"Adams\"`。 您还可以使用`insert`方法将项作为`pair`对象插入，或者作为对象或通过迭代器插入到另一个容器中的`pair`对象。\n\n大多数`emplace`和`insert`方法将返回以下形式的`pair`对象，其中`iterator`类型与`map`相关：\n\n```cpp\n    pair<iterator, bool>\n```\n\n您可以使用此对象测试两件事。 首先，`bool`指示插入是否成功(如果容器中已经存在具有相同键的项，则插入将失败)。 其次，`pair`的`iterator`部分要么指示新项的位置，要么指示将不被替换的现有项的位置(并且将导致插入失败)。\n\n*失败*取决于*等价*，而不是*相等*。 如果有一个项的键与您尝试插入的项相同，则插入将失败。 等价性的定义取决于与`map`对象一起使用的比较器谓词。 因此，如果`map`使用谓词`comp`，则通过测试`!comp(a,b) && !comp(b,a)`来确定两个项`a`和`b`之间的等价性。 这与测试`(a==b)`不同。\n\n假设前面的`map`对象，您可以这样做：\n\n```cpp\n    auto result = people.emplace(\"Adams\", 1825); \n    if (!result.second) \n       cout << (*result.first).first << \" already in map\" << endl;\n```\n\n测试`result`变量中的第二项以查看插入是否成功，如果不成功，则第一项是对现有项`pair<string,int>`的迭代器，代码取消对迭代器的引用以获得`pair`对象，然后打印出第一项，即关键字(在本例中为人名)。\n\n如果您知道项目在`map`中的位置，则可以调用`emplace_hint`：\n\n```cpp\n    auto result = people.emplace(\"Monroe\", 1817); \n    people.emplace_hint(result.first, \"Polk\", 1845);\n```\n\n这里我们知道`Polk`在`Monroe`之后，所以我们可以将迭代器作为提示传递给`Monroe`。 该类通过迭代器提供对项的访问，因此您可以使用 Range`for`(它基于迭代器访问)：\n\n```cpp\n    for (pair<string, int> p : people) \n    { \n        cout << p.first << \" \" << p.second << endl; \n    }\n```\n\n此外，还可以使用`at`方法和`[]`运算符访问各个项目。 在这两种情况下，该类都将搜索具有所提供键的项，如果找到该项，则返回对该项的值的引用。 在没有具有指定键的项的情况下，`at`方法和`[]`运算符的行为不同。 如果键不存在，`at`方法将抛出异常；如果`[]`操作符找不到指定的键，它将使用键并调用值类型的默认构造函数来创建一个新项。 如果键存在，`[]`运算符将返回对值的引用，因此您可以编写如下代码：\n\n```cpp\n    people[\"Adams\"] = 1825; \n    people[\"Jackson\"] = 1829;\n```\n\n第二行的行为与您预期的一样：将没有键为`Jackson`的项，因此`map`将使用该键创建项，通过调用值类型的默认构造函数(`int`，因此值被初始化为零)对其进行初始化，然后返回对该值的引用，该值被赋值为`1829`。 但是，第一行将查找`Adams`，查看是否存在一个项，并返回对其值的引用，然后为其赋值`1825`。 与插入新项相反，没有指示项的值已更改。 在某些情况下，您可能想要这种行为，但这不是代码的目的，显然，需要允许重复键的关联容器(如`multimap`)。 此外，在这两种情况下，都会搜索键，返回引用，然后执行赋值。 请注意，虽然以这种方式插入项是有效的，但是在容器中放置一个新的键值对会更有效，因为您没有这个额外的赋值。\n\n填写`map`后，可以使用以下内容搜索值：\n\n*   `at`方法，传递一个键并返回对该键的值的引用\n*   `[]`运算符，当传递一个键时，它返回对该键的值的引用\n*   `find`函数将使用模板中指定的谓词(与后面提到的 global`find`函数不同)，它将为您提供作为`pair`对象的整个项的迭代器\n*   `begin`方法将为您提供第一个项目的迭代器，`end`方法将为您在最后一个项目之后提供一个迭代器[T2\n*   `lower_bound`方法将迭代器返回给键*等于**等于或大于作为参数传递的键*的项\n*   `upper_bound`方法返回映射中键*大于提供的键*的第一个项的迭代器\n*   `equal_range`方法返回`pair`对象中的下限值和上限值\n\n# 集合和多集\n\n集的行为就像它们是贴图，但关键点与值相同；例如，如下所示：\n\n```cpp\n    set<string> people{ \n       \"Washington\",\"Adams\", \"Jefferson\",\"Madison\",\"Monroe\",  \n       \"Adams\", \"Van Buren\",\"Harrison\",\"Tyler\",\"Polk\"}; \n    for (string s : people) cout << s << endl;\n```\n\n这将按字母顺序打印出*9 个*人，因为有两个名为`Adams`的项目，而`set`类将拒绝重复项。 当项目被插入到集合中时，它将被排序，在这种情况下，顺序由比较两个`string`对象的词典排序来确定。 如果您希望允许重复，以便在容器中放置 10 个人，那么您应该改用`multiset`。\n\n与`map`一样，您不能更改容器中项的键，因为键用于确定排序。 对于`set`，键与值相同，因此这意味着您根本不能更改该项。 如果要执行查找，那么使用排序的`vector`可能会更好。 A`set`将比 A`vector`具有更多的内存分配开销。 如果搜索是按顺序进行的，那么在`set`容器上查找可能会比在`vector`容器上查找更快，但是如果您调用`binary_search`(稍后将在*排序项*部分中解释)，它可能会比关联容器更快。\n\n`set`类的接口是`map`类的受限版本，因此您可以将容器中的`insert`和`emplace`项分配给另一个容器中的值，这样您就可以使用迭代器访问(`begin`和`end`方法)。\n由于没有不同的键，这意味着`find`方法查找的是值，而不是键(与 bound 方法相似；例如，`equal_range`)。 没有`at`方法，也没有`[]`运算符。\n\n# 无序集装箱\n\n`map`和`set`类允许您快速查找对象，这得益于这些按排序顺序保存项目的类。 如果您遍历这些项(从`begin`到`end`)，那么您将以排序的顺序获得这些项。 如果您想要选择键值范围内的对象，可以调用`lower_bound`和`upper_bound`方法，使迭代器指向适当的键范围。 这是这些关联容器的两个重要特性：查找和排序。 在某些情况下，值的实际顺序并不重要，您想要的行为是高效查找。 在这种情况下，您可以使用`map`和`set`类的`unordered_`版本。 因为顺序并不重要，所以这些都是使用哈希表实现的。\n\n# 特殊用途集装箱\n\n到目前为止所描述的容器是灵活的，可以用于各种目的。 标准库提供了有特定用途的类，但是因为它们是通过包装其他类来实现的，所以它们被称为**容器适配器**。 例如，`deque`对象可以用作**先进先出**(**FIFO**)队列，方法是将对象推到`deque`后面(使用`push_back`)，然后使用`front`方法从队列前面访问对象(并使用`pop_front`删除它们)。 标准库实现了一个名为`queue`的容器适配器，它具有这种 FIFO 行为，并且它基于`deque`类。\n\n```cpp\n    queue<int> primes; \n    primes.push(1); \n    primes.push(2); \n    primes.push(3); \n    primes.push(5); \n    primes.push(7); \n    primes.push(11); \n    while (primes.size() > 0) \n    { \n        cout << primes.front() << \",\"; \n        primes.pop(); \n    } \n    cout << endl; // prints 1,2,3,5,7,11\n```\n\n您将`push`项放入队列并使用`pop`移除它们，然后使用`front`方法访问下一项。 此适配器可以包装的标准库容器必须实现`push_back`、`pop_front`和`front`方法。 也就是说，项目被放入容器的一端，并从另一端访问(和移除)。\n\n**后进先出**(**LIFO**)容器将放入项目，并从同一端存取(和移除)项目。 同样，可以使用`deque`对象来实现此行为，方法是使用`push_back`推入项，使用`front`访问项，并使用`pop_back`方法删除它们。 标准库提供了一个名为`stack`的适配器类来提供此行为。 这有一个名为`push`的方法来将项推入容器，一个名为`pop`的方法来删除项，但是奇怪的是，您使用`top`方法访问下一个项，即使它是使用包装容器的`back`方法实现的。\n\n不管名称如何，适配器类`priority_queue`的用法与`stack`容器类似；也就是说，使用`top`方法访问项。 容器确保当项目被推入时，队列的顶部将始终是具有最高优先级的项目。 谓词(默认值为`<`)用于对队列中的项进行排序。 例如，我们可以有一个聚合类型，该聚合类型具有任务名称以及与其他任务相比您必须完成该任务的优先级：\n\n```cpp\n    struct task \n    { \n    string name; \n    int priority; \n    task(const string& n, int p) : name(n), priority(p) {} \n    bool operator <(const task& rhs) const { \n        return this->priority < rhs.priority; \n        } \n    };\n```\n\n聚合类型很简单；它有两个由构造函数初始化的数据成员。 为了对任务进行排序，我们需要能够比较两个任务对象。 一种选择(前面给出)是定义一个单独的谓词类。 在本例中，我们使用缺省谓词，文档中说它将是`less<task>`，这将基于`<`运算符比较项。 为了使用缺省谓词，我们为`task`类定义了`<`操作符。 现在，我们可以将任务添加到`priority_queue`容器：\n\n```cpp\n    priority_queue<task> to_do; \n    to_do.push(task(\"tidy desk\", 1)); \n    to_do.push(task(\"check in code\", 10)); \n    to_do.push(task(\"write spec\", 8)); \n    to_do.push(task(\"strategy meeting\", 8)); \n\n    while (to_do.size() > 0) \n    { \n        cout << to_do.top().name << \" \" << to_do.top().priority << endl; \n        to_do.pop(); \n    }\n```\n\n此代码的结果是：\n\n```cpp\n    check in code 10\nwrite spec 8\nstrategy meeting 8\ntidy desk 1\n```\n\n队列已经根据`priority`数据项对任务进行了排序，`top`和`pop`方法调用的组合按优先级顺序读取这些项，并将它们从队列中删除。 具有相同优先级的项目按其被推入的顺序放置在队列中。\n\n# 使用迭代器\n\n到目前为止，在本章中我们已经指出，容器通过迭代器提供对项的访问。 这意味着迭代器就是简单的指针，这是有意为之的，因为迭代器的行为*类似于*指针。 但是，它们通常是迭代器类的对象(参见`<iterator>`头)。 所有迭代器都有以下行为：\n\n| **操作员** | **行为** |\n| *** | 提供对当前位置的元素的访问权限 |\n| +++ | 向前移动到下一个元素(通常使用前缀操作符)(这仅在迭代器允许向前移动的情况下) |\n| - | 向后移动到上一个元素(通常使用前缀运算符)(仅当迭代器允许向后移动时) |\n| `==`和`!=` | 比较两个迭代器是否位于相同位置 |\n| == | 分配迭代器 |\n\n与 C++ 指针不同，C++ 指针假定数据在内存中是连续的，迭代器可以用于更复杂的数据结构，如链表，其中项可能不是连续的。 运算符`++ `和`--`按预期工作，与底层存储机制无关。\n\n`<iterator>`头声明将递增迭代器的`next`全局函数和将按指定位置更改迭代器的`advance`函数(向前或向后取决于参数是否为负以及迭代器允许的方向)。 还有一个`prev`函数可以将迭代器递减一个或多个位置。 函数`distance`可用于确定两个迭代器之间有多少项。\n\n所有容器都有一个`begin`方法，它返回第一个项目的迭代器，还有一个`end`方法，它在最后一个项目之后返回一个迭代器*。 这意味着您可以通过调用`begin`然后递增迭代器直到它具有从`end`返回的值来迭代容器中的所有项。 迭代器上的`*`操作符提供对容器中元素的访问，如果迭代器是读写的(如果从 Begin 方法返回就是这样)，这意味着该项可以更改。 容器还有`cbegin`和`cend`方法，它们将返回一个常量迭代器，该迭代器只提供对元素的只读访问：*\n\n```cpp\n    vector<int> primes { 1,2,3,5,7,11,13 }; \n    const auto it = primes.begin(); // const has no effect \n    *it = 42; \n    auto cit = primes.cbegin(); \n    *cit = 1;                       // will not compile\n```\n\n这里`const`没有效果，因为变量是`auto`，类型是从用于初始化变量的项推导出来的。 `cbegin`方法定义为返回`const`迭代器，因此不能更改它引用的项。\n\n`begin`和`cbegin`方法返回**个正向迭代器**，以便`++ `操作符将迭代器向前移动。 容器还可以支持**反向迭代器**，其中`rbegin`是容器中的最后一项(即，在由`end`返回的位置之前的项*)，`rend`是在*第一项之前的位置*。 (还有`crbegin`和`crend`，它们返回`const`个迭代器。)。 需要注意的是，反向迭代器的`++ `运算符向后移动*，如下例所示：**\n\n```cpp\n    vector<int> primes { 1,2,3,5,7,11,13 }; \n    auto it = primes.rbegin(); \n    while (it != primes.rend()) \n    { \n        cout << *it++ << \" \"; \n    } \n    cout << endl; // prints 13,11,7,5,4,3,2,1\n```\n\n`++ `运算符根据迭代器应用到的迭代器类型递增迭代器。 需要注意的是，这里使用`!=`运算符来确定循环是否应该结束，因为将在所有迭代器上定义`!=`运算符。\n\n这里的迭代器类型通过使用`auto`关键字被忽略。 事实上，所有容器都将使用`typedef`表示它们使用的所有迭代器类型，因此在前面的例子中，我们可以使用以下代码：\n\n```cpp\n    vector<int> primes { 1,2,3,5,7,11,13 }; \n    vector<int>::iterator it = primes.begin();\n```\n\n允许正向迭代的容器将具有`iterator`和`const_iterator`的`typedef`，而允许反向迭代的容器将具有`reverse_iterator`和`const_reverse_iterator`的`typedef`。 为了完整，容器还将为返回指向元素的指针的方法使用`typedef`表示`pointer`和`const_pointer`，对于返回对元素的引用的方法使用`reference`和`const_reference`表示。 这些类型定义使您能够在不知道容器中的类型的情况下编写泛型代码，但代码仍然能够声明正确类型的变量。\n\n尽管迭代器看起来像是指针，但迭代器通常是由类实现的。 这些类型可能只允许单向迭代：正向迭代器将只有`++ `运算符，反向迭代器将有`-`运算符，或者该类型可能允许双向迭代(双向迭代器)，因此它们同时实现`++ `和`--`运算符。 例如，`list`、`set`、`multiset`、`map`和`multimap`类上的迭代器是双向的。 `vector`、`deque`、`array`和`string`类具有允许随机访问的迭代器，因此这些迭代器类型具有与双向迭代器相同的行为，但也有类似算术的指针，因此它们可以一次更改多个项目位置。\n\n# 输入和输出迭代器\n\n顾名思义，输入迭代器将只向前移动并具有读取访问权限，而输出迭代器将仅向前移动但将具有写入访问权限。 这些迭代器没有随机访问，也不允许向后移动。 例如，输出流可以与输出迭代器一起使用：您为取消引用的迭代器分配一个数据项，以便将该数据项写入流。 类似地，输入流可以有一个输入迭代器，您可以取消对该迭代器的引用以访问流中的下一项。 此行为意味着，对于输出迭代器，取消引用运算符(`*`)的唯一有效用法是在赋值的左侧。 使用`!=`检查迭代器的值没有任何意义，并且无法检查通过输出迭代器赋值是否成功。\n\n例如，`transform`函数接受三个迭代器和一个函数。 前两个迭代器是输入迭代器，指示函数要转换的项的范围。 结果将放入一个项目范围(与输入迭代器的范围大小相同)中，第一个项目由第三个迭代器表示，这是一个输出迭代器。 执行此操作的一种方法如下：\n\n```cpp\n    vector<int> data { 1,2,3,4,5 }; \n    vector<int> results; \n    results.resize(data.size()); \n    transform( \n       data.begin(), data.end(),  \n       results.begin(), \n       [](int x){ return x*x; } );\n```\n\n在这里，`begin`和`end`方法返回`data`容器上的迭代器，这些迭代器可以安全地用作输入迭代器。 只有当容器有足够的已分配项时，`results`容器上的`begin`方法才能用作输出迭代器，这就是此代码中的情况，因为它们已与`resize`一起分配。 然后，该函数将通过将每个输入项传递给最后一个参数(仅返回值的平方)中给出的 lambda 函数来转换每个输入项。 重新评估这里发生的事情很重要；`transform`函数的第三个参数是输出迭代器，这意味着您应该期待该函数通过此迭代器写入值。\n\n这段代码可以工作，但是它需要额外的步骤来分配空间，并且您在容器中有额外的默认对象分配，这样您就可以覆盖它们。 值得一提的是，输出迭代器不必位于另一个容器中。 它可以指向相同的容器，只要它引用可以写入的范围：\n\n```cpp\n    vector<int> vec{ 1,2,3,4,5 }; \n    vec.resize(vec.size() * 2); \n    transform(vec.begin(), vec.begin() + 5, \n       vec.begin() + 5, [](int i) { return i*i; });\n```\n\n调整`vec`容器的大小，以便为结果留出空间。 要转换的值的范围是从开始项到第五项(`vec.begin() + 5`是下一项)，写入转换后的值的位置是第六项到第十项。 如果你打印出矢量，你会得到`{1,2,3,4,5,1,4,9,16,25}`。\n\n另一种类型的输出迭代器是插入器。 `back_inserter`用于带有`push_back`的容器，`front_inserter`用于带有`push_front`的容器。 顾名思义，插入器调用容器上的`insert`方法。 例如，您可以像这样使用`back_inserter`：\n\n```cpp\n    vector<int> data { 1,2,3,4,5 }; \n    vector<int> results; \n    transform( \n       data.begin(), data.end(),  \n       back_inserter(results), \n       [](int x){ return x*x; } ); // 1,4,9,16,25\n```\n\n转换的结果与从`back_inserter`类创建的临时对象一起插入到`results`容器中。 使用`back_inserter`对象可确保当`transform`函数通过迭代器写入时，使用`push_back`将项*插入*到包装容器中。 请注意，结果容器应该与源容器不同。\n\n如果希望值按相反顺序排列，则如果容器支持`push_front`(例如，`deque`)，则可以使用`front_inserter`。 `vector`类没有`push_front`方法，但它有反向迭代器，因此您可以改用它们：\n\n```cpp\n    vector<int> data { 1,2,3,4,5 }; \n    vector<int> results; \n    transform( \n data.rbegin(), data.rend(), \n       back_inserter(results), \n       [](int x){ return x*x; } ); // 25,16,9,4,1\n```\n\n要颠倒结果的顺序，只需将`begin`更改为`rbegin`，将`end`更改为`rend`。\n\n# 流迭代器\n\n这些是`<iterators>`中的适配器类，可用于从输入流读取项目或将项目写入输出流。 例如，到目前为止，我们已经通过 Range`for`循环使用迭代器来打印容器的内容：\n\n```cpp\n    vector<int> data { 1,2,3,4,5 }; \n    for (int i : data) cout << i << \" \"; \n    cout << endl;\n```\n\n相反，您可以基于`cout`创建一个输出流迭代器，以便使用流运算符`<<`通过此迭代器将`int`值写入`cout`流。 要打印出包含`int`值的容器，只需将容器复制到输出迭代器：\n\n```cpp\n    vector<int> data { 1,2,3,4,5 }; \n    ostream_iterator<int> my_out(cout, \" \"); \n    copy(data.cbegin(), data.cend(), my_out); \n    cout << endl;\n```\n\n`ostream_iterator`类的第一个参数是它将适应的输出流，可选的第二个参数是在每个项目之间使用的分隔符字符串。 `copy`函数(在`<algorithm>`中)将把作为前两个参数传递的输入迭代器所指示的范围内的项复制到作为最后一个参数传递的输出迭代器。\n\n类似地，还有一个`istream_iterator`类，它将包装一个输入流对象并提供一个输入迭代器。 该类将使用 STREAM`>>`运算符来提取指定类型的对象，这些对象可以通过流迭代器读取。 然而，从流中读取数据要比向流中写入数据复杂得多，因为必须检测何时输入流中没有更多数据可供迭代器读取(文件情况结束)。\n\n`istream_iterator`类有两个构造函数。 一个构造函数只有一个参数，即要读取的输入流，而另一个构造函数(默认构造函数)没有参数，用于创建**结束流迭代器**。 流结束迭代器用于指示流中没有更多数据：\n\n```cpp\n    vector<int> data; \n    copy( \n       istream_iterator<int>(cin), istream_iterator<int>(), \n       back_inserter(data)); \n\n    ostream_iterator<int> my_out(cout, \" \"); \n    copy(data.cbegin(), data.cend(), my_out); \n    cout << endl;\n```\n\n对`copy`的第一次调用提供了两个输入迭代器(作为第一个参数)和一个输出迭代器。 该函数将数据从第一个迭代器复制到最后一个参数中的输出迭代器。 由于最后一个参数是从`back_inserter`创建的，这意味着这些项被插入到`vector`对象中。 输入迭代器基于输入流(`cin`)，因此`copy`函数将从控制台读取`int`值(每个值由空格分隔)，直到没有更多的值可用(例如，如果按*CTRL*+*Z*结束流或键入非数字项)。 由于您可以使用迭代器给出的一系列值来初始化容器，因此可以使用`istream_iterator`作为构造函数参数：\n\n```cpp\n    vector<int> data {  \n       istream_iterator<int>(cin), istream_iterator<int>() };\n```\n\n在这里，构造函数是使用初始值设定项列表语法调用的；如果使用圆括号，编译器会将其解释为函数的声明！\n\n如前所述，`istream_iterator`将使用流的`>>`运算符从流中读取指定类型的对象，并且该运算符使用空格来分隔项目(因此它只忽略所有空格)。 如果您在包含`string`个对象的容器中进行读取，那么您在控制台上键入的每个单词都将是容器中的一个项目。 `string`是一个字符容器，也可以使用迭代器对其进行初始化，因此您可以尝试使用`istream_iterator`从控制台将数据输入到`string`：\n\n```cpp\n    string data { \n            istream_iterator<char>(cin), istream_iterator<char>() };\n```\n\n在本例中，流是`cin`，但它很容易成为文件的`ifstream`对象。 问题是，`cin`对象将去掉空格，因此`string`对象将包含您键入的除空格以外的所有内容，因此将没有空格和换行符。\n\n此问题是由使用流的`>>`运算符的`istream_iterator`引起的，只能通过使用另一个类`istreambuf_iterator`来避免：\n\n```cpp\n    string data { \n        istreambuf_iterator<char>(cin), istreambuf_iterator<char>() };\n```\n\n这个类从流中读取每个字符，并将每个字符复制到容器中，而不需要处理`>>`。\n\n# 在 C 标准库中使用迭代器\n\nC 标准库通常需要指向数据的指针。 例如，当 C 函数需要一个字符串时，它需要一个指向包含该字符串的字符数组的`const char*`指针。 C++ 标准库的设计允许您将其类与 C 标准库一起使用；实际上，C 标准库是 C++ 标准库的一部分。 对于`string`对象，解决方案很简单：当需要`const char*`指针时，只需在`string`对象上调用`c_str`方法。\n\n在连续内存(`array`、`string`或`data`)中存储数据的容器有一个名为`data`的方法，该方法允许以 C 数组的形式访问容器的数据。 此外，这些容器拥有对其数据的`[]`操作符访问权限，因此您还可以将第一项的地址视为`&container[0]`(其中`container`是容器对象)，就像您对 C 数组所做的那样。 但是，如果容器为空，则此地址将无效，因此在使用它之前应该调用`empty`方法。 这些容器中的项数是从`size`方法返回的，因此对于任何接受指向 C 数组开头及其大小的指针的 C 函数，都可以使用`&container[0]`和`size`方法的值来调用它。\n\n您可能想通过调用容器的`begin`函数来获得具有连续内存的容器的开头，但这将返回迭代器(通常是一个对象)。 因此，要获得指向第一个项目的 C 指针，您应该调用`&*begin`；也就是说，取消引用从`begin`函数返回的迭代器以获得第一个项目，然后使用地址操作符获得其地址。 坦率地说，`&container[0]`更简单，可读性更强。\n\n如果容器不将其数据存储在连续内存中(例如，`deque`和`list`)，则只需将数据复制到临时向量中即可获得 C 指针。\n\n```cpp\n    list<int> data; \n    // do some calculations and fill the list \n    vector<int> temp(data.begin(), data.end()); \n    size_t size = temp.size(); // can pass size to a C function \n    int *p = &temp[0];         // can pass p to a C function\n```\n\n在本例中，我们选择使用`list`，例程将操作`data`对象。 在例程的后面，这些值将被传递给一个 C 函数，因此`list`被用来初始化一个`vector`对象，并且这些值是从`vector`中获得的。\n\n# 算法\n\n标准库在`<algorithm>`头文件中有大量的泛型函数集合。 所谓泛型，我们指的是它们通过迭代器访问数据，而不知道迭代器指的是什么，因此这意味着您可以编写泛型代码来为任何适当的容器工作。 但是，如果您知道容器类型，并且该容器具有执行相同操作的成员方法，则应该使用该成员。\n\n# 项目的迭代\n\n`<algorithm>`中的许多例程将获取范围并在这些范围内迭代，以执行某些操作。 顾名思义，`fill`函数将用值填充容器。 该函数使用两个迭代器来指定将放入容器每个位置的范围和值：\n\n```cpp\n    vector<int> vec; \n    vec.resize(5); \n    fill(vec.begin(), vec.end(), 42);\n```\n\n由于将为范围调用`fill`函数，这意味着您必须将迭代器传递给已有值的容器，这就是此代码调用`resize`方法的原因。 此代码将把`42`的值放入容器的每个项中，因此当它完成时，`vector`包含`{42,42,42,42,42}`。 此函数还有另一个版本，称为`fill_n`，它通过单个迭代器指定到范围开始的范围和范围中项目的计数。\n\n`generate`函数类似，但它有一个函数，可以是函数、函数对象或 lambda 表达式，而不是单个值。 调用该函数是为了提供容器中的每个项目，因此它没有参数，并返回迭代器访问的类型的对象：\n\n```cpp\n    vector<int> vec(5); \n    generate(vec.begin(), vec.end(),  \n        []() {static int i; return ++ i; });\n```\n\n同样，您必须确保向`generate`函数传递一个已经存在的范围，此代码通过将初始大小作为构造函数参数传递来实现这一点。 在本例中，lambda 表达式有一个`static`变量，该变量随着每次调用而递增，因此这意味着在`generate`函数完成之后，`vector`包含`{1,2,3,4,5}`。 此函数还有另一个版本，称为`generate_n`，它通过单个迭代器指定到范围开始的范围和范围中项目的计数。\n\n`for_each`函数将遍历由两个迭代器提供的范围，并针对该范围中的每一项调用指定的函数。 此函数必须有一个与容器中的项类型相同的参数：\n\n```cpp\n    vector<int> vec { 1,4,9,16,25 }; \n    for_each(vec.begin(), vec.end(),  \n         [](int i) { cout << i << \" \"; }); \n    cout << endl;\n```\n\n`for_each`函数迭代迭代器指定的所有项(在本例中是整个范围)，取消对迭代器的引用，并将项传递给函数，此代码的效果是打印容器的内容。 该函数可以按值(如本例)或按引用获取项。 如果通过引用传递项，则函数可以更改该项：\n\n```cpp\n    vector<int> vec { 1,2,3,4,5 }; \n    for_each(vec.begin(), vec.end(),  \n         [](int& i) { i *= i; });\n```\n\n调用此代码后，`vector`中的项将替换为这些项的正方形。 如果使用函数器或 lambda 表达式，则可以传递容器来捕获函数的结果；例如：\n\n```cpp\n    vector<int> vec { 1,2,3,4,5 }; \n    vector<int> results; \n    for_each(vec.begin(), vec.end(),  \n         [&results](int i) { results.push_back(i*i); });\n```\n\n在这里，容器被声明为接受对 lambda 表达式的每次调用的结果，并且通过捕获变量来引用该表达式来传递该变量。\n\nRecall from [Chapter 5](05.html), *Using Functions*, that the square brackets contain the names of the captured variables declared outside the expression. Once captured, it means that the expression is able to access the object.\n\n在本例中，每个迭代的结果(`i*i`)被推入捕获的集合中，以便存储结果以备以后使用。\n\n`transform`函数有两种形式；它们都提供函数(指针、函数器或 lambda 表达式)，并且都有通过迭代器传递的容器中项目的输入范围。 在这方面，它们类似于`for_each`。 `transform`函数还允许您将迭代器传递给用于存储函数结果的容器。 该函数必须有一个与输入迭代器引用的类型的类型(或引用)相同的参数，并且它必须返回输出迭代器访问的类型。\n\n另一个版本的`transform`使用一个函数来组合两个范围内的值，因此这意味着该函数必须有两个参数(这将是两个迭代器中的对应项)，并返回输出迭代器的类型。 您只需要给出其中一个输入范围内的全部项目范围，因为假设另一个范围至少一样大，因此您只需要提供第二个范围的开始迭代器：\n\n```cpp\n    vector<int> vec1 { 1,2,3,4,5 }; \n    vector<int> vec2 { 5,4,3,2,1 }; \n    vector<int> results; \n    transform(vec1.begin(), vec1.end(), vec2.begin(), \n       back_inserter(results), [](int i, int j) { return i*j; });\n```\n\n# 获取信息\n\n一旦容器中有了值，就可以调用函数来获取有关这些项的信息。 `count`函数用于计算一定范围内具有指定值的项数：\n\n```cpp\n    vector<int> planck{ 6,6,2,6,0,7,0,0,4,0 }; \n    auto number = count(planck.begin(), planck.end(), 6);\n```\n\n此代码将返回值`3`，因为容器中有三个`6`副本。 函数的返回类型是容器的`difference_type``typedef`中指定的类型，在本例中它将是`int`。 `count_if`函数的工作方式与此类似，但是您要传递一个谓词，该谓词接受单个参数(容器中的当前项)，并返回一个`bool`，指定这是否是要计数的值。\n\n函数的作用是：统计特定值出现的次数。 如果希望聚合所有值，则可以使用`<numeric>`中的`accumulate`函数。 这将在范围内迭代，访问每个项目，并保存所有项目的运行总和。 求和将使用该类型的`+`运算符执行，但也有一个版本接受一个二元函数(容器类型的两个参数并返回相同的类型)，该函数指定当您将两个这样的类型相加时会发生什么。\n\n向`all_of`、`any_of`和`none_of`函数传递带有相同类型容器的单个参数的谓词；还有指定的迭代器，指示它们迭代的范围，用谓词测试每个项目。 仅当所有项目的谓词为`true`时，`all_of`函数才返回`true`；如果至少有一个项目的谓词为`true`，则`any_of`函数返回`true`；只有当所有项目的谓词为`false`时，`none_of`函数才返回`true`。\n\n# 比较容器\n\n如果您有两个数据容器，有多种方法可以比较它们。 对于每种容器类型，都定义了`<`、`<=`、`==`、`!=`、`>`和`>=`运算符。 `==`和`!=`运算符比较容器的数量和这些项的值。 因此，如果项具有不同的项数、不同的值或两者都有，则它们不相等。 其他比较更喜欢值而不是项目数：\n\n```cpp\n    vector<int> v1 { 1,2,3,4 }; \n    vector<int> v2 { 1,2 }; \n    vector<int> v3 { 5,6,7 }; \n    cout << boolalpha; \n    cout << (v1 > v2) << endl; // true \n    cout << (v1 > v3) << endl; // false\n```\n\n在第一个比较中，这两个向量有相似的项目，但是`v2`有更少的项目，所以`v1`“大于”`v2`。 在第二种情况下，`v3`的值比`v1`大，但数量较少，因此`v3`比`v1`大。\n\n您还可以使用`equal`函数比较范围。 这将传递两个范围(假设这两个范围大小相同，因此只需要一个到第二个范围开始的迭代器)，并使用迭代器访问的类型的`==`运算符或用户提供的谓词比较两个范围中的相应项。 只有当所有这样的比较都是`true`时，函数才会返回`true`。 类似地，`mismatch`函数比较两个范围内的相应项目。 但是，此函数返回一个`pair`对象，其中的迭代器位于第一个不同项的两个范围中的每一个范围内。 您还可以提供比较功能。 `is_permutation is`与`is_permutation is`类似，因为它比较两个范围内的值，但如果两个范围具有相同的值，但顺序不一定相同，则返回`true`。\n\n# 更改项目\n\n**reverse**函数作用于容器中的范围，并颠倒项目的顺序；这意味着迭代器必须是可写的。 `copy`和`copy_n`函数将每个项目从一个范围向前复制到另一个范围；对于`copy`，输入范围由两个输入迭代器给出；对于`copy_n`，范围是一个输入迭代器和项目计数。 `copy_backward`函数将从范围末尾开始复制项目，以便输出范围的项目与原始项目的顺序相同。 这意味着输出迭代器将指示要复制到的范围的*结束*。 您还可以仅在项满足谓词指定的某些条件时才能复制项。\n\n*   `reverse_copy`函数将以与输入范围相反的顺序创建副本；实际上，该函数向后迭代原始数据，并将项目向前复制到输出范围。\n*   尽管名称不同，但`move`和`move_backward`函数在语义上等同于`copy`和`copy_backward`函数。 因此，在下面的操作中，原始容器在操作后将具有相同的值：\n\n```cpp\n        vector<int> planck{ 6,6,2,6,0,7,0,0,4,0 }; \n        vector<int> result(4);          // we want 4 items \n        auto it1 = planck.begin();      // get the first position \n        it1 += 2;                       // move forward 2 places \n        auto it2 = it1 + 4;             // move 4 items \n        move(it1, it2, result.begin()); // {2,6,0,7}\n```\n\n*   此代码将从第三个位置的项目开始，将四个项目从第一个容器复制到第二个容器。\n*   `remove_copy`和`remove_copy_if`函数遍历源范围，并复制具有指定值以外的项。\n\n```cpp\n        vector<int> planck{ 6,6,2,6,0,7,0,0,4,0 }; \n        vector<int> result; \n        remove_copy(planck.begin(), planck.end(),  \n            back_inserter(result), 6);\n```\n\n*   这里，`planck`对象与以前相同，而`result`对象将包含`{2,0,7,0,0,4,0}`。 `remove_copy_if`函数的行为类似，但被赋予谓词而不是实际值。\n*   `remove`和`remove_if`函数并不完全像它们的名字所暗示的那样。 这些函数作用于单个范围并迭代查找特定值(`remove`)，或者将每个项传递给指示是否应该删除该项的谓词(`remove_if`)。 移除项时，容器中后面的项将前移，但容器的大小保持不变，这意味着末尾的项保持原样。 `remove`函数的行为是这样的，因为它们只知道如何通过迭代器读取和写入项(这对所有容器都是通用的)。 要擦除一个项目，函数需要有权访问容器的`erase`方法，而`remove`函数只有权访问迭代器。\n*   如果要删除末尾的项，则必须相应地调整容器的大小。 通常，这意味着在容器上调用合适的`erase`方法，这是因为`remove`方法将迭代器返回到新的结束位置：\n\n```cpp\n        vector<int> planck { 6,6,2,6,0,7,0,0,4,0 }; \n        auto new_end = remove(planck.begin(), planck.end(), 6); \n                                             // {2,0,7,0,0,4,0,0,4,0} \n        planck.erase(new_end, planck.end()); // {2,0,7,0,0,4,0}\n```\n\n*   `replace`和`replace_if`函数迭代单个范围，如果值是指定值(`replace`)或从谓词(`replace_if`)返回`true`，则用指定的新值替换该项。 还有两个函数`replace_copy`和`replace_copy_if`，它们不会更改原始范围，而会更改到另一个范围(类似于`remove_copy`和`remove_copy_if`函数)。\n*   `rotate`函数将范围视为结束连接到开始，因此您可以将项目向前移动，以便当项目从结束位置脱落时，将其放在第一个位置。 如果要将每个项目前移四个位置，可以执行以下操作：\n\n```cpp\n        vector<int> planck{ 6,6,2,6,0,7,0,0,4,0 }; \n        auto it = planck.begin(); \n        it += 4; \n        rotate(planck.begin(), it, planck.end());\n```\n\n*   这种旋转的结果是`{0,7,0,0,4,0,6,6,2,6}`。 `rotate_copy`函数执行相同的操作，但是它不会影响原始容器，而是将项目复制到另一个容器中。\n*   `unique`函数作用于一个范围并“移除”(按照前面解释的方式)相邻项的重复项，您可以为函数提供一个谓词，以测试两个项是否相同。 此函数只检查相邻的项目，因此容器中稍后将保留重复项。 如果要删除所有重复项，则应首先对容器进行排序，以便相似的项目相邻。\n*   只有当项目是唯一的时，`unique_copy`函数才会将项目从一个范围复制到另一个范围，因此删除重复项的一种方法是在临时容器上使用此函数，然后将原始项分配给临时容器：\n\n```cpp\n        vector<int> planck{ 6,6,2,6,0,7,0,0,4,0 }; \n        vector<int> temp; \n        unique_copy(planck.begin(), planck.end(), back_inserter(temp)); \n        planck.assign(temp.begin(), temp.end());\n```\n\n*   在此代码之后，`planck`容器将具有`{6,2,6,0,7,0,4,0}`。\n*   最后，`iter_swap`将交换由两个迭代器指示的项，`swap_ranges`函数将一个范围中的项交换到另一个范围(第二个范围由一个迭代器指示，并且假定引用与第一个相同大小的范围)。\n\n# 查找项目\n\n标准库具有多种搜索项目的功能：\n\n*   `min_element`函数将返回范围内最小项的迭代器，`max_element`函数将返回最大项的迭代器。 向这些函数传递要检查的项目范围的迭代器和从两个项目的比较中返回`bool`的预测器。 如果不提供预测器，则将使用该类型的`<`运算符。\n\n```cpp\n        vector<int> planck{ 6,6,2,6,0,7,0,0,4,0 }; \n        auto imin = min_element(planck.begin(), planck.end()); \n        auto imax = max_element(planck.begin(), planck.end()); \n        cout << \"values between \" << *imin << \" and \"<< *imax << endl;\n```\n\n*   `imin`和`imax`值是迭代器，这就是为什么它们被解除引用以获得值的原因。 如果您想一次性获得最小元素和最大元素，可以调用`minmax_element`，它将返回一个带有这些项的迭代器的`pair`对象。 顾名思义，`adjacent_find`函数将返回具有相同值的前两个项目的位置(并且您可以提供一个谓词来确定*相同值*意味着什么)。 这允许您搜索重复项并获取这些重复项的位置。\n\n```cpp\n        vector<int> vec{0,1,2,3,4,4,5,6,7,7,7,8,9}; \n        vector<int>::iterator it = vec.begin(); \n\n        do \n        { \n            it = adjacent_find(it, vec.end()); \n            if (it != vec.end()) \n            {  \n                cout << \"duplicate \" << *it << endl; \n                ++ it; \n            } \n        } while (it != vec.end());\n```\n\n*   这个代码有一个数字序列，其中有一些相邻的数字重复。 在这种情况下，有三个*个*相邻重复项：`4`后跟`4`，序列`7,7,7`是`7`，后跟`7`，`7`后跟`7`。 `do`循环重复调用`adjacent_find`，直到它返回`end`迭代器，表明它已经搜索了所有项。 当找到重复的对时，代码将打印值，然后递增开始位置以进行下一次搜索。\n*   `find`函数在容器中搜索单个值，如果找不到该值，则返回该项目的迭代器或`end`迭代器。 向`find_if`函数传递一个谓词，它将迭代器返回到它找到的第一个满足该谓词的项；类似地，`find_if_not`函数找到不满足该谓词的第一个项。\n*   有几个函数被赋予两个范围，一个是要搜索的范围，另一个是要查找的值。 不同的函数要么查找搜索条件中的一个项目，要么查找所有项目。 这些函数对容器包含的类型或谓词使用`==`运算符。\n*   函数的作用是：返回它在搜索列表中找到的第一个项目的位置。 `search`函数查找特定序列，它返回整个序列的第一个*个*位置，而`find_end`函数返回整个搜索序列的最后*个*位置。 最后，`search_n`函数查找在指定容器的范围内重复多次的值(给定值和重复数)的序列。\n\n# 对项目进行排序\n\n可以对序列容器进行排序，一旦完成此操作，您就可以使用方法来搜索项、合并容器或获取容器之间的差异。 `sort`函数将根据您提供的`<`运算符或谓词对某个范围内的项目进行排序。 如果在该范围内有相等的项，则不能保证排序后这些项的顺序；如果此顺序很重要，则应改为调用`stable_sort`函数。 如果希望保留输入范围并将排序后的项复制到另一个范围中，可以使用名称混乱的`partial_sort_copy`函数。 这不是部分排序。 向该函数传递输入范围的迭代器和输出范围的迭代器，因此必须确保输出范围具有合适的容量。\n\n您可以通过调用`is_sorted`函数来检查某个范围是否已排序，如果发现没有按排序顺序排序的项，则会遍历所有项并返回`false`，在这种情况下，您可以通过调用`is_sorted_until`函数找到第一个排序不正确的项。\n\n顾名思义，`partial_sort`函数并不是按照每个项目相对于其他项目的确切顺序放置每个项目。 相反，它将创建两个组或分区，其中第一个分区将具有最小的项目(不一定以任何顺序)，而另一个分区将具有最大的项目。 您可以保证最小的项目位于第一个分区中。 要调用此函数，您需要传递三个迭代器，其中两个是要排序的范围，第三个是介于其他两个之间的某个位置，该位置指示在其之前是最小值的边界。\n\n```cpp\n    vector<int> vec{45,23,67,6,29,44,90,3,64,18}; \n    auto middle = vec.begin() + 5; \n    partial_sort(vec.begin(), middle, vec.end()); \n    cout << \"smallest items\" << endl; \n    for_each(vec.begin(), middle, [](int i) {cout << i << \" \"; }); \n    cout << endl; // 3 6 18 23 29 \n    cout << \"biggest items\" << endl; \n    for_each(middle, vec.end(), [](int i) {cout << i << \" \"; }); \n    cout << endl; // 67 90 45 64 44\n```\n\n在本例中，有一个由 10 个项目组成的向量，因此我们将`middle`迭代器定义为从开头开始的 5 个项目(这只是一个选择，它可以是其他一些值，具体取决于您想要获得的项目数量)。 在本例中，您可以看到五个最小的项目已被排序到前半部分，而后半部分的项目最大。\n\n名称奇怪的`nth_element`函数的作用类似于`partial_sort`。 向第*n*元素提供迭代器，该函数确保范围内的前*n*项是最小的。 `nth_element`函数比`partial_sort`快，虽然可以保证第*n*元素之前的项小于或等于第*n*元素，但分区内的排序顺序没有其他保证。\n\n`partial_sort`和`nth_element`函数是分区排序函数的版本。 `partition`函数是一个更通用的版本。 您向此函数传递一个范围和一个谓词，用于确定项将放置在两个分区中的哪个分区中。 满足谓词的项将被放入范围的第一个分区中，其他项将被放入第一个分区之后的范围中。 第二个分区的第一项称为分割点，它从`partition`函数返回，但您可以稍后通过将迭代器传递给分区范围并将谓词传递给`partition_point`函数来计算它。 `partition_copy`函数还将对值进行分区，但它将保持原始范围不变，并将值放入已分配的范围中。 这些分区函数不能保证等价项的顺序，如果这个顺序很重要，那么您应该调用`stable_partitian`函数。 最后，您可以通过调用`is_partitioned`函数来确定容器是否被分区。\n\n函数`shuffle`将容器中的项目重新排列为随机顺序。 此函数需要来自`<random>`库的统一随机数生成器。 例如，下面的代码将用十个整数填充一个容器，然后按随机顺序放置它们：\n\n```cpp\n    vector<int> vec; \n    for (int i = 0; i < 10; ++ i) vec.push_back(i); \n    random_device rd; \n    shuffle(vec.begin(), vec.end(), rd);\n```\n\n堆是部分排序的序列，其中第一个项始终是最大的，并且项在对数时间内添加到堆中或从堆中删除。 堆基于序列容器，奇怪的是，您必须在现有容器上使用函数调用，而不是由标准库提供适配器类。 要从现有容器创建堆，需要将范围迭代器传递给`make_heap`函数，该函数将容器作为堆进行排序。 然后，您可以使用其`push_back`方法将新项添加到容器中，但每次执行此操作时，都必须调用`push_heap`来重新排序堆。 类似地，要从堆中获取项，您可以调用容器上的`front`方法，然后通过调用`pop_heap`函数删除该项，该函数可确保堆保持有序。 您可以通过调用`is_heap`来测试容器是否安排为堆，如果容器没有完全安排为堆，则可以通过调用`is_heap_until`获得不满足堆条件的第一个项的迭代器。 最后，可以使用`sort_heap`将堆排序为排序序列。\n\n对容器进行排序后，可以调用一些函数来获取有关序列的信息。 已经为容器描述了`lower_bound`和`upper_bound`方法，函数的行为方式相同：`lower_bound`返回值大于或等于提供的值的第一个元素的位置，`upper_bound`返回大于提供的值的下一项的位置。 函数的作用是：测试一个排序范围是否包含第二个排序范围内的项目。\n\n以`set_`开头的函数将把两个排序的序列组合成第三个容器。 `set_difference`函数将复制第一个序列中的项目，但不复制第二个序列中的项目。 这不是对称操作，因为它不包括第二个序列中但不包括第一个序列中的项目。 如果您想要对称的差异，那么您应该调用`set_symmetric_difference`函数。 `set_intersection`将复制这两个序列中的项目。 `set_union`函数将合并这两个序列。 还有一个函数可以组合两个序列，它是`merge`函数。 这两个函数的不同之处在于，使用`set_union`函数时，如果一个项同时位于两个序列中，则结果容器中将只有一个副本，而使用`merge`时，结果容器中将有两个副本。\n\n如果对某个范围进行了排序，则可以调用`equal_range`函数来获取与传递给函数或谓词的值相等的元素范围。 此函数返回一对迭代器，它们表示容器中的值范围。\n\n需要排序容器的最后一个方法是`binary_search`。 此函数用于测试值是否在容器中。 传递给函数的迭代器指示要测试的范围和一个值，如果在该范围内有一个项目等于该值，它将返回`true`(您可以提供一个谓词来执行此相等测试)。\n\n# 使用数字图书馆\n\n标准库有几个用于执行数值操作的类库。 在本节中，我们将介绍两个：使用`<ratio>`的编译时算术和使用`<complex>`的复数。\n\n# 编译时算法\n\n分数是一个问题，因为对于某些分数，没有足够的有效数字来准确地表示它们，导致在进一步的算术中使用它们时会失去准确性。 此外，计算机是二进制的，仅将十进制小数部分转换为二进制将会失去准确性。 `<ratio>`库提供的类允许您将小数表示为整数比率的对象，并以比率执行分数计算。 只有在执行完所有小数运算后，才能将数字转换为小数，这意味着精度的潜在损失将降至最低。 由`<ratio>`库中的类执行的计算是在*编译时*执行的，因此编译器将捕获除以零和溢出等错误。\n\n使用库很简单；您可以使用`ratio`类，并提供分子和分母作为模板参数。 分子和分母将被分解存储，您可以通过对象的`num`和`den`成员访问这些值：\n\n```cpp\n    ratio<15, 20> ratio; \n    cout << ratio.num << \"/\" << ratio.den << endl;\n```\n\n这将打印出`3/4`。\n\n分数运算是使用模板执行的(实际上，这些模板是`ratio`模板的专门化)。 乍一看可能有点奇怪，但你很快就会习惯的！\n\n```cpp\n    ratio_add<ratio<27, 11>, ratio<5, 17>> ratio; \n    cout << ratio.num << \"/\" << ratio.den << endl;\n```\n\n这将打印出`514/187`(您可能需要一些纸张并进行分数计算以确认这一点)。 数据成员实际上是`static`成员，因此创建变量意义不大。 此外，由于运算是使用*类型*而不是*变量*执行的，因此最好通过这些类型访问成员：\n\n```cpp\n    typedef ratio_add<ratio<27, 11>, ratio<5, 17>> sum; \n    cout << sum::num << \"/\" << sum::den << endl;\n```\n\n现在，您可以将 SUM 类型用作可以执行的任何其他操作的参数。 四个二进制算术运算使用`ratio_add`、`ratio_subtract`、`ratio_multiply`和`ratio_divide`执行。 通过`ratio_equal`、`ratio_not_equal`、`ratio_greater`、`ratio_greater_equal`、`ratio_less`和`ratio_less_equal`进行比较。\n\n```cpp\n    bool result = ratio_greater<sum, ratio<25, 19> >::value; \n    cout << boolalpha << result << endl;\n```\n\n此操作测试之前执行的计算(`514/187`)是否大于分数`25/19`(是)。 编译器将拾取被零除的错误和溢出，因此以下代码将不会编译：\n\n```cpp\n    typedef ratio<1, 0> invalid; \n    cout << invalid::num << \"/\" << invalid::den << endl;\n```\n\n但是，必须指出的是，当访问分母时，编译器将在第二行发出错误。 还有用于 SI 前缀的 Ratio 类型定义。 这意味着您可以以纳米为单位执行计算，当您需要以米为单位表示数据时，您可以使用`nano`类型来获取比率：\n\n```cpp\n    double radius_nm = 10.0; \n    double volume_nm = pow(radius_nm, 3) * 3.1415 * 4.0 / 3.0; \n    cout << \"for \" << radius_nm << \"nm \" \n        \"the volume is \" << volume_nm << \"nm3\" << endl; \n    double factor = ((double)nano::num / nano::den); \n    double vol_factor = pow(factor, 3); \n    cout << \"for \" << radius_nm * factor << \"m \" \n        \"the volume is \" << volume_nm * vol_factor << \"m3\" << endl;\n```\n\n这里，我们在**纳米**(**nm**)的球体上进行计算。 球体的半径为 10 nm，因此第一次计算得出的体积为 4188.67 Nm3。 第二个计算将纳米转换为米；系数由`nano`比率确定(请注意，对于体积，系数是立方体)。 您可以定义一个类来执行这样的转换：\n\n```cpp\n    template<typename units> \n    class dist_units \n    { \n        double data; \n        public: \n            dist_units(double d) : data(d) {} \n\n        template <class other> \n        dist_units(const dist_units<other>& len) : data(len.value() *  \n         ratio_divide<units, other>::type::den / \n         ratio_divide<units, other>::type::num) {} \n\n        double value() const { return data; } \n    };\n```\n\n该类是为特定类型的单元定义的，它将通过`ratio`模板的实例化来表示。 该类有一个构造函数用来为这些单位中的值初始化它，还有一个构造函数用来从其他单位转换，它只是将当前单位除以其他类型的单位。 此类可按如下方式使用：\n\n```cpp\n    dist_units<kilo> earth_diameter_km(12742); \n    cout << earth_diameter_km.value() << \"km\" << endl; \n    dist_units<ratio<1>> in_meters(earth_diameter_km); \n    cout << in_meters.value()<< \"m\" << endl; \n    dist_units<ratio<1609344, 1000>> in_miles(earth_diameter_km); \n    cout << in_miles.value()<< \"miles\" << endl;\n```\n\n第一个变量基于`kilo`，因此单位是公里。 要将其转换为米，第二个变量类型基于与`ratio<1,1>`相同的`ratio<1>`。 结果是，将`earth_diameter_km`中的值放入`in_meters`中时，会将其乘以 1000。 转换为英里的过程稍微复杂一些。 一英里有 1609.344 米。 用于`in_miles`变量的比率为 1609344/1,000 或 1609.344。 我们用`earth_diameter_km`来初始化变量，那么这个值是不是太大了 1000 倍呢？ 不，原因是`earth_diameter_km`的类型是`dist_units<kilo>`，所以公里和里程之间的换算将包含 1000 的因子。\n\n# 复数\n\n复数不仅对数学感兴趣，在工程和科学中也是至关重要的，因此`complex`类型是任何类型库的重要组成部分。 复数由两部分组成--实数部分和虚数部分。 顾名思义，虚数不是实数，不能视为实数。\n\n在数学中，复数通常表示为二维空间中的坐标。 如果实数可以被认为是 x 轴上无限多个点中的一个，那么虚数就可以被认为是 y 轴上无限多个点中的一个。 这两个数字之间唯一的交点是原点，因为零是零，什么也不是，所以它可以是零实数，也可以是零虚数。 复数既有实数部分，也有虚数部分，因此可以将其视作笛卡儿点。 实际上，将复数可视化的另一种方式是将其表示为极数，其中该点表示为与 x 轴上的位置(正实数轴)成指定角度的指定长度的向量。\n\n`complex`类基于浮点类型，并且有针对`float`、`double`和`long double`的专门化。 这个类很简单；它有一个构造函数，它有两个参数表示数字的实部和虚部，并且它定义了用于赋值、比较、`+`、`-`、`/`和`*`的运算符(成员方法和全局函数)，作用于实部和虚部。\n\nAn operation like `+` is simple for a complex number: you just add the real parts together and the imaginary parts together, and these two sums are the real and imaginary parts of the result. However, multiplication and division are a bit more, umm, complex. In multiplication, you get a quadratic: the aggregation of the two real parts multiplied, the two imaginary parts multiplied, the two values of the real part of the first multiplied with the imaginary part of the second, and the imaginary part of the first multiplied with the real part of the second. The complication is that two imaginary numbers multiplied is equivalent to the multiplication of two equivalent real numbers multiplied by -1\\. Furthermore, multiplying a real and an imaginary number results in an imaginary number that is equivalent in size to the multiplication of two equivalent real numbers.\n\n还有对复数执行三角运算的函数：`sin`、`cos`、`tan`、`sinh`、`cosh`和`tanh`；以及基本的数学运算，如`log`、`exp`、`log10`、`pow`和`sqrt`。 您还可以调用函数来创建复数并获取有关它们的信息。 因此，`polar`函数将接受两个浮点数，表示矢量长度和角度的极坐标。 如果您有一个`complex`Number 对象，您可以通过调用`abs`(获取长度)和`arg`(获取角度)来获得极坐标。\n\n```cpp\n    complex<double> a(1.0, 1.0); \n    complex<double> b(-0.5, 0.5); \n    complex<double> c = a + b; \n    cout << a << \" + \" << b << \" = \" << c << endl; \n    complex<double> d = polar(1.41421, -3.14152 / 4); \n    cout << d << endl;\n```\n\n要说明的第一点是，有一个为`complex`数字定义的`ostream`插入操作符，因此您可以将它们插入到`cout`流对象中。 此代码的输出如下所示：\n\n```cpp\n    (1,1) + (-0.5,0.5) = (0.5,1.5)\n(1.00002,-0.999979)\n```\n\n第二行显示了仅对 2 和-1/4pi 的平方根使用五位小数的限制，这个数字实际上是复数`(1, -1)`。\n\n# 使用标准库\n\n在本例中，我们将为**逗号分隔值**(**CSV**)文件开发一个简单的解析器。 我们将遵循的规则如下：\n\n*   每条记录将占用一行，换行符表示新记录\n*   记录中的字段用逗号分隔，除非它们位于带引号的字符串内\n*   字符串可以用单引号(`'`)或双引号(`\"`)引起来，在这种情况下，字符串可以包含逗号作为字符串的一部分\n*   立即重复的引号(`''`或`\"\"`)是文字，是字符串的一部分，而不是字符串的分隔符\n*   如果字符串用引号引起来，则字符串外部的空格将被忽略\n\n这是一个非常基本的实现，省略了引号字符串可以包含换行符的通常要求。\n\n在本例中，大部分操作将使用`string`对象作为单个字符的容器。\n\n首先，在本书的文件夹中创建一个名为`Chapter_08`的章节文件夹。 在该文件夹中，创建名为`csv_parser.cpp`的文件。 由于应用将使用控制台输出和文件输入，因此请在文件顶部添加以下行：\n\n```cpp\n    #include <iostream> \n    #include <fstream> \n\n    using namespace std;\n```\n\n该应用还将接受一个命令行参数，该参数是要解析的 CSV 文件，因此在该文件的底部添加以下代码：\n\n```cpp\n    void usage() \n    { \n        cout << \"usage: csv_parser file\" << endl; \n        cout << \"where file is the path to a csv file\" << endl; \n    } \n\n    int main(int argc, const char* argv[]) \n    { \n        if (argc <= 1) \n        { \n            usage(); \n            return 1; \n        } \n        return 0; \n    }\n```\n\n应用会将文件逐行读取到`string`个对象的`vector`个中，因此将`<vector>`添加到包含文件列表中。 为简化编码，请在`usage`函数上面定义以下内容：\n\n```cpp\n    using namespace std; \n    using vec_str = vector<string>;\n```\n\n`main`函数将逐行读取文件，最简单的方法是使用`getline`函数，因此将`<string>`头文件添加到包含文件列表中。 将以下行添加到`main`函数的末尾：\n\n```cpp\n    ifstream stm; \n    stm.open(argv[1], ios_base::in); \n    if (!stm.is_open()) \n    { \n        usage(); \n        cout << \"cannot open \" << argv[1] << endl; \n        return 1; \n    } \n\n    vec_str lines; \n    for (string line; getline(stm, line); ) \n    { \n        if (line.empty()) continue; \n        lines.push_back(move(line)); \n    } \n    stm.close();\n```\n\n前几行使用`ifstream`类打开文件。 如果找不到该文件，则打开该文件的操作将失败，并通过调用`is_open`进行测试。 接下来，声明`string`个对象的`vector`个，并用从文件中读取的行填充这些对象。 函数`getline`有两个参数：第一个是打开的文件流对象，第二个是包含字符数据的字符串。 此函数返回流对象，该对象具有`bool`转换操作符，因此`for`语句将循环，直到此流对象指示它无法读取更多数据。 当流到达文件末尾时，会设置一个内部文件结束标志，这会导致`bool`转换操作符返回值`false`。\n\n如果`getline`函数读取空行，那么将无法解析`string`，因此需要对此进行测试，并且不会存储此类空行。 每个合法的行都被推入`vector`，但是，由于在此操作之后将不会使用此`string`变量，因此我们可以使用移动语义，因此通过调用`move`函数来显式地实现这一点。\n\n这段代码现在可以编译和运行(尽管它不会产生任何输出)。 您可以在满足前面给定条件的任何 CSV 文件上使用它，但作为测试文件，我们使用了以下文件：\n\n```cpp\n    George Washington,1789,1797 \n    \"John Adams, Federalist\",1797,1801 \n    \"Thomas Jefferson, Democratic Republican\",1801,1809 \n    \"James Madison, Democratic Republican\",1809,1817 \n    \"James Monroe, Democratic Republican\",1817,1825 \n    \"John Quincy Adams, Democratic Republican\",1825,1829 \n    \"Andrew Jackson, Democratic\",1829,1837 \n    \"Martin Van Buren, Democratic\",1837,1841 \n    \"William Henry Harrison, Whig\",1841,1841 \n    \"John Tyler, Whig\",1841,1841 \n    John Tyler,1841,1845\n```\n\n这些是 1845 年以前的美国总统；第一个字符串是总统的名字和他们的从属关系，但当总统没有从属关系时，它就会被漏掉(华盛顿和泰勒)。 然后在名字后面加上他们任期的开始和结束年份。\n\n接下来，我们希望解析向量中的数据，并根据前面给出的规则将项目拆分成单独的字段(字段之间用逗号分隔，但使用引号)。 为此，我们将每行表示为`list`个字段，每个字段是一个`string`。 在文件顶部附近添加`<list>`的 Include。 在进行`using`声明的文件顶部，添加以下内容：\n\n```cpp\n    using namespace std; \n    using vec_str = vector<string>; \n    using list_str = list<string>;using vec_list = vector<list_str>;\n```\n\n现在，在`main`函数的底部添加：\n\n```cpp\n    vec_list parsed; \n    for (string& line : lines) \n    { \n        parsed.push_back(parse_line(line)); \n    }\n```\n\n第一行创建`list`个对象的`vector`个，`for`循环遍历调用一个名为`parse_line`的函数的每一行，该函数解析字符串并返回`list`个`string`个对象。 函数的返回值将是一个临时对象，因此是一个右值，因此这意味着将调用具有移动语义的`push_back`版本。\n\n在用法函数上方，添加`parse_line`函数的开头：\n\n```cpp\n    list_str parse_line(const string& line) \n    { \n        list_str data; \n        string::const_iterator it = line.begin(); \n\n        return data; \n    }\n```\n\n该函数将字符串视为字符容器，因此它将使用`const_iterator`迭代 line 参数。 解析将在`do`循环中执行，因此添加以下内容：\n\n```cpp\n    list_str data; \n    string::const_iterator it = line.begin(); \n    string item; bool bQuote = false; bool bDQuote = false; do{++ it; } while (it != line.end()); data.push_back(move(item)); \n    return data;\n```\n\n稍后将解释布尔变量。 `do`循环递增迭代器，当达到`end`值时，循环结束。 `item`变量将保存解析的数据(此时为空)，最后一行将把值放入`list`；这是为了在函数结束之前将任何未保存的数据存储在`list`中。 由于 Item 变量即将被销毁，因此调用`move`可确保将其内容移动到`list`中，而不是复制。 如果没有此调用，则在将项放入`list`时将调用字符串复制构造函数。\n\n接下来，您需要对数据进行解析。 为此，添加一个开关来测试以下三种情况：逗号(表示字段结尾)和引号或双引号表示带引号的字符串。 其思想是读取每个字段，并使用`item`变量逐个字符构建其值。\n\n```cpp\n    do \n    { \n        switch (*it) { case ''': break; case '\"': break; case ',': break; default: item.push_back(*it); }; \n        ++ it; \n    } while (it != line.end());\n```\n\n默认操作很简单：它将字符复制到临时字符串中。 如果字符是单引号，我们有两个选择。 要么引号在带双引号的字符串中(在这种情况下，我们希望将引号存储在`item`中)，要么引号是分隔符，在这种情况下，我们通过设置`bQuote`值来存储它是开始引号还是结束引号。 对于单报价的情况，请添加以下内容：\n\n```cpp\n    case ''': \n    if (bDQuote) item.push_back(*it); else { bQuote = !bQuote; if (bQuote) item.clear(); } \n    break;\n```\n\n这很简单。 如果这是双引号字符串(设置了`bDQuote`)，则存储引号。 如果没有，那么我们翻转`bQuote bool`，这样如果这是第一个引号，我们就注册该字符串是被引用的，否则我们注册它是一个字符串的末尾。 如果我们位于带引号的字符串的开头，则清除 Item 变量以忽略前一个逗号(如果有)和引号之间的任何空格。 但是，此代码没有考虑使用相邻的两个引号，这意味着引号是文字和字符串的一部分。 更改代码以添加针对此情况的检查：\n\n```cpp\n    if (bDQuote) item.push_back(*it); \n    else \n    { \n        if ((it + 1) != line.end() && *(it + 1) == ''') { item.push_back(*it); ++ it; } else \n        { \n            bQuote = !bQuote; \n            if (bQuote) item.clear(); \n        } \n    }\n```\n\n`if`语句进行检查，以确保如果我们递增迭代器，我们不会到达行尾(在本例中，短路将在这里开始，表达式的其余部分将不会被求值)。 我们可以测试下一项，然后查看下一项是否为单引号；如果是，则将其添加到`item`变量并递增迭代器，以便在循环中使用两个引号。\n\n双引号的代码类似，但切换了布尔变量并测试双引号：\n\n```cpp\n    case '\"': \n    if (bQuote) item.push_back(*it); else { if ((it + 1) != line.end() && *(it + 1) == '\"') { item.push_back(*it); ++ it; } else { bDQuote = !bDQuote; if (bDQuote) item.clear(); } } \n    break;\n```\n\n最后，我们需要代码来测试逗号。 同样，我们有两种情况：要么是带引号的字符串中的逗号，在这种情况下，我们需要存储字符；要么是字段的末尾，在这种情况下，我们需要完成对该字段的解析。 代码非常简单：\n\n```cpp\n    case ',': \n    if (bQuote || bDQuote)  item.push_back(*it); else                    data.push_back(move(item)); \n    break;\n```\n\n`if`语句测试我们是否在带引号的字符串中(在这种情况下，`bQuote`或`bDQuote`将为真)，如果是，则存储字符。 如果这是字段的末尾，我们将把`string`推入`list`，但我们使用`move`，这样变量数据就会移动，而`string`对象则处于未初始化状态。\n\n此代码将编译并运行。 但是，仍然没有输出，所以在我们纠正这个问题之前，请检查一下您编写的代码。 在`main`函数的末尾，您将拥有一个`vector`，其中每个项目都有一个`list`对象，表示 CSV 文件中的每一行，而`list`中的每个项目都是一个字段。 现在您已经解析了该文件，可以相应地使用该数据了。 为了让您看到数据已被解析，请在`main`函数的底部添加以下行：\n\n```cpp\n    int count = 0; \n    for (list_str row : parsed) \n    { \n        cout << ++ count << \"> \"; \n        for (string field : row) \n        { \n            cout << field << \" \"; \n        } \n        cout << endl; \n    }\n```\n\n现在可以编译代码(使用`/EHsc`开关)并运行传递 CSV 文件名称的应用。\n\n# 简略的 / 概括的 / 简易判罪的 / 简易的\n\n在本章中，您已经看到了 C++ 标准库中的一些主要类，并深入研究了容器类和迭代器类。 `string`类就是这样一个容器；这是一个非常重要的类，我们将在下一章更深入地介绍它。**"
  },
  {
    "path": "docs/begin-cpp-prog/09.md",
    "content": "# 九、使用字符串\n\n在某些情况下，您的应用将需要与人通信，这意味着使用文本；例如输出文本、接受文本形式的数据，然后将数据转换为适当的类型。 C++ 标准库具有丰富的类集合，用于操作字符串、在字符串和数字之间进行转换，以及获取针对指定语言和区域性进行本地化的字符串值。\n\n# 使用 String 类作为容器\n\nC++ 字符串基于`basic_string`模板类。 该类是一个容器，因此它使用迭代器访问和方法来获取信息，并且具有包含有关它所持有的字符类型的信息的模板参数。 特定字符类型有不同的`typedef`表示：\n\n```cpp\n    typedef basic_string<char,\n       char_traits<char>, allocator<char> > string; \n    typedef basic_string<wchar_t,\n       char_traits<wchar_t>, allocator<wchar_t> > wstring; \n    typedef basic_string<char16_t,\n       char_traits<char16_t>, allocator<char16_t> > u16string; \n    typedef basic_string<char32_t,\n       char_traits<char32_t>, allocator<char32_t> > u32string;\n```\n\n`string`类基于`char`，`wstring`基于`wchar_t`宽字符，`16string`和`u32string`类分别基于 16 位和 32 位字符。 在本章的其余部分，我们将只关注`string`类，但它同样适用于其他类。\n\n对于不同大小的字符，比较、复制和访问字符串中的字符需要不同的代码，而特征模板参数提供了实现。 对于`string`，这是`char_traits`类。 例如，当该类复制字符时，它会将此操作委托给`char_traits`类及其`copy`方法。 特征类也由流类使用，因此它们还定义了适用于文件流的文件结尾值。\n\n字符串本质上是一个由零个或多个字符组成的数组，它在需要时分配内存，并在`string`对象被销毁时重新分配内存。 在某些方面，它非常类似于`vector<char>`对象。 作为容器，`string`类通过`begin`和`end`方法提供迭代器访问：\n\n```cpp\n    string s = \"hellon\"; \n    copy(s.begin(), s.end(), ostream_iterator<char>(cout));\n```\n\n在这里，调用`begin`和`end`方法从`string,`中的项获取迭代器，这些迭代器从`<algorithm>`传递给`copy`函数，以便通过`ostream_iterator`临时对象将每个字符复制到控制台。 在这方面，`string`对象类似于`vector`，因此我们使用前面定义的`s`对象：\n\n```cpp\nvector<char> v(s.begin(), s.end()); \ncopy(v.begin(), v.end(), ostream_iterator<char>(cout));\n```\n\n这将使用`string`对象上的`begin`和`end`方法提供的字符范围填充`vector`对象，然后使用`copy`函数将这些字符打印到控制台，方式与我们之前使用的完全相同。\n\n# 获取有关字符串的信息\n\n`max_size`方法将给出计算机体系结构上指定字符类型的字符串的最大大小，这可能会令人惊讶地大。 例如，在具有 2 GB 内存的 64 位 Windows 计算机上，`string`对象的`max_size`将返回 40 亿个字符，而`wstring`对象的该方法将返回 20 亿个字符。 这显然超过了机器中的内存！ 其他 Size 方法返回更有意义的值。 `length`方法返回与`size`方法相同的值，即字符串中有多少项(字符)。 `capacity`方法根据字符数指示已经为字符串分配了多少内存。\n\n您可以通过调用`compare`方法将`string`与另一个进行比较。 这将返回`int`而不是`bool`(但请注意，可以将`int`静默转换为`bool`)，其中返回值`0`表示两个字符串相同。 如果它们不相同，则如果参数字符串大于操作数字符串，则此方法返回负值；如果参数小于操作数字符串，则返回正值。 在这方面，*大于*和*小于*将按字母顺序测试字符串的排序。 此外，还为`<`、`<=`、`==`、`>=`和`>`定义了用于比较字符串对象的全局运算符。\n\n通过`c_str`方法，可以像使用 C 字符串一样使用`string`对象。 返回的指针是`const`；您应该知道，如果更改了`string`对象，指针可能会失效，因此不应该存储此指针。 您不应该使用`&str[0]`来获取 C++ 字符串`str`的 C 字符串指针，因为不能保证 String 类使用的内部缓冲区会被`NUL`终止。 提供`c_str`方法是为了返回一个指针，该指针*可以*用作 C 字符串，因此`NUL`终止。\n\n如果要将数据从 C++ 字符串复制到 C 缓冲区，可以调用`copy`方法。 将目标指针和要复制的字符数作为参数传递(也可以是偏移量)，该方法将尝试最多将指定数量的字符复制到目标缓冲区：*，但不带空终止字符*。 此方法假定目标缓冲区足够大，可以容纳复制的字符(您应该采取措施确保这一点)。 如果您希望传递缓冲区的大小以便该方法为您执行此检查，请改为调用`_Copy_s`方法。\n\n# 更改字符串\n\nString 类具有标准的容器访问方法，因此您可以通过使用`at`方法和`[]`运算符的引用(读写访问)来访问单个字符。 您可以使用`assign`方法替换整个字符串，或者使用`swap`方法交换两个 String 对象的内容。 此外，可以使用`insert`方法在指定位置插入字符，使用`erase`方法删除指定字符，使用`clear`方法删除所有字符。 该类还允许您使用`push_back`和`pop_back`方法将字符推送到字符串末尾(并删除最后一个字符)：\n\n```cpp\n    string str = \"hello\"; \n    cout << str << \"n\"; // hello \n    str.push_back('!'); \n    cout << str << \"n\"; // hello! \n    str.erase(0, 1); \n    cout << str << \"n\"; // ello!\n```\n\n可以使用`append`方法或`+=`运算符在字符串末尾添加一个或多个字符：\n\n```cpp\n    string str = \"hello\"; \n    cout << str << \"n\";  // hello \n    str.append(4, '!'); \n    cout << str << \"n\";  // hello!!!! \n    str += \" there\"; \n    cout << str << \"n\";  // hello!!!! there\n```\n\n`<string>`库还定义了一个全局`+`运算符，该运算符将两个字符串连接成第三个字符串。\n\n如果要更改字符串中的字符，可以使用`[]`运算符通过索引访问该字符，并使用引用覆盖该字符。 还可以使用`replace`方法将指定位置的一个或多个字符替换为 C 字符串、C++ 字符串或通过迭代器访问的其他容器中的字符：\n\n```cpp\n    string str = \"hello\"; \n    cout << str << \"n\";    // hello \n    str.replace(1, 1, \"a\"); \n    cout << str << \"n\";    // hallo\n```\n\n最后，您可以将字符串的一部分提取为新字符串。 `substr`方法接受偏移量和可选计数。 如果省略字符数，则子字符串将从指定位置一直到字符串末尾。 这意味着您可以通过传递偏移量 0 和小于字符串大小的计数来复制字符串的左侧部分，也可以通过仅传递第一个字符的索引来复制字符串的右侧部分。\n\n```cpp\n    string str = \"one two three\"; \n    string str1 = str.substr(0, 3);  \n    cout << str1 << \"n\";          // one \n    string str2 = str.substr(8); \n    cout << str2 << \"n\";          // three\n```\n\n在此代码中，第一个示例将前三个字符复制到一个新字符串中。 在第二个示例中，复制从第八个字符开始，一直持续到末尾。\n\n# 搜索字符串\n\n`find`方法是使用字符、C 字符串或 C++ 字符串传递的，您可以提供初始搜索位置来开始搜索。 `find`方法返回搜索文本所在位置(而不是迭代器)，如果找不到文本，则返回值`npos`。 Offset 参数和`find`方法的成功返回值使您能够重复解析字符串以查找特定项目。 `find`方法正向搜索指定的文本，还有一个`rfind`方法执行反向搜索。\n\n请注意，`rfind`并不完全与`find`方法相反。 `find`方法将字符串中的搜索点向前移动，并在每个点将搜索字符串与来自搜索点的字符进行比较(因此，第一个搜索文本字符，然后是第二个字符，依此类推)。 `rfind`方法将搜索点*向后移动*，但仍向前*进行比较*。 因此，假设`rfind`方法没有给定偏移量，第一次比较将在距离字符串末尾的偏移量(搜索文本的大小)处进行。 然后，通过将搜索文本中的第一个字符与搜索到的字符串中的搜索点处的字符进行比较来进行比较，如果比较成功，则将搜索文本中的第二个字符与搜索点之后的字符进行比较。 因此，在与搜索点的移动方向相反的方向上进行比较。\n\n这一点很重要，因为如果您希望使用`find`方法的返回值作为偏移量来解析字符串，那么在每次搜索之后，您应该将搜索偏移量*向前移动**，然后将搜索偏移量*向前移动，并且对于`rfind`，您应该将它向后移动*。*\n\n *例如，要搜索以下字符串中`the`的所有位置，可以调用：\n\n```cpp\n    string str = \"012the678the234the890\"; \n    string::size_type pos = 0; \n    while(true) \n    { \n        pos++ ; \n        pos = str.find(\"the\",pos); \n        if (pos == string::npos) break; \n        cout << pos << \" \" << str.substr(pos) << \"n\"; \n    } \n    // 3 the678the234the890 \n    // 9 the234the890 \n    // 15 the890\n```\n\n这将在字符位置 3、9 和 15 处找到搜索文本。要向后搜索字符串，您可以调用：\n\n```cpp\n    string str = \"012the678the234the890\"; \n    string::size_type pos = string::npos; \n    while(true) \n    { \n        pos--; pos = str.rfind(\"the\",pos); \n        if (pos == string::npos) break; \n        cout << pos << \" \" << str.substr(pos) << \"n\"; \n    } \n    // 15 the890 \n    // 9 the234the890 \n    // 3 the678the234the890\n```\n\n突出显示的代码显示了应该进行的更改，显示您需要从末尾搜索并使用`rfind`方法。 当您有一个成功的结果时，您需要在下一次搜索之前减少位置。 与`find`方法类似，如果找不到搜索文本，`rfind`方法将返回`npos`。\n\n有四种方法允许您搜索几个单独字符中的一个。 例如：\n\n```cpp\n    string str = \"012the678the234the890\"; \n    string::size_type pos = str.find_first_of(\"eh\"); \n    if (pos != string::npos) \n    { \n        cout << \"found \" << str[pos] << \" at position \"; \n        cout << pos << \" \" << str.substr(pos) << \"n\"; \n    } \n    // found h at position 4 he678the234the890\n```\n\n搜索字符串是`eh`，当`find_first_of`在字符串中找到字符`e`或`h`时，它将返回。 在本例中，字符`h`首先在位置 4 处找到。您可以提供一个偏移量参数来开始搜索，因此您可以使用`find_first_of`的返回值来解析字符串。 `find_last_of`方法类似，但它在字符串中以相反的方向搜索搜索文本中的一个字符。\n\n还有两种搜索方法可以查找搜索文本中提供的字符以外的字符*：`find_first_not_of`和`find_last_not_of`。 例如：*\n\n```cpp\n    string str = \"012the678the234the890\"; \n    string::size_type pos = str.find_first_not_of(\"0123456789\"); \n    cout << \"found \" << str[pos] << \" at position \"; \n    cout << pos << \" \" << str.substr(pos) << \"n\"; \n    // found t at position 3 the678the234the890\n```\n\n此代码查找数字以外的字符，因此它在位置 3(第四个字符)找到`t`。\n\n没有库函数可以裁剪`string`中的空格，但是您可以通过使用 find 函数查找非空格来裁剪字符串左侧和右侧的空格，然后将其用作`substr`方法的适当索引。\n\n```cpp\n    string str = \"  hello  \"; \n    cout << \"|\" << str << \"|n\";  // |  hello  | \n    string str1 = str.substr(str.find_first_not_of(\" trn\")); \n    cout << \"|\" << str1 << \"|n\"; // |hello  | \n    string str2 = str.substr(0, str.find_last_not_of(\" trn\") + 1); \n    cout << \"|\" << str2 << \"|n\"; // |  hello|\n```\n\n在前面的代码中，创建了两个新字符串：一个是左修剪空格，另一个是右修剪空格。 第一次转发搜索第一个不是空格的字符，并将其用作子字符串的起始索引(没有提供计数，因为复制了所有剩余的字符串)。 在第二种情况下，反向搜索字符串以查找不是空格的字符，但返回的位置将是`hello`的最后一个字符；因为我们需要第一个字符的子字符串，所以我们递增此索引以获得要复制的字符数。\n\n# 国际化\n\n`<locale>`头包含用于本地化时间、日期和货币格式的类，并提供本地化的字符串比较和排序规则。\n\nThe C Runtime Library also has global functions to carry out localization. However, it is important in the following discussion that we distinguish between C functions and the C locale. The C locale is the default locale, including the rules for localization, used in C and C++ programs and it can be replaced with a locale for a country or culture. The C Runtime Library provides functions to change the locale, as does the C++ Standard Library.\n\n由于 C++ 标准库提供了用于本地化的类，这意味着您可以创建多个表示一个区域设置的对象。 区域设置对象可以在函数中创建并且只能在函数中使用，或者可以全局应用于线程并仅由在该线程上运行的代码使用。 这与 C 本地化函数不同，在 C 本地化函数中，更改区域设置是全局的，因此所有代码(以及所有执行线程)都会受到影响。\n\n`locale`类的实例要么通过类构造函数创建，要么通过类的静态成员创建。 C++ 流类将使用区域设置(稍后解释)，如果您想要更改区域设置，则调用流对象上的`imbue`方法。 在某些情况下，您可能希望直接访问这些规则之一，并且您可以通过 Locale 对象访问它们。\n\n# 使用镶嵌面\n\n国际化规则称为**方面**。 语言环境对象是多个方面的容器，您可以使用`has_facet`函数测试该语言环境是否有特定的方面；如果有，可以通过调用`use_facet`函数获取对该方面的`const`引用。 下表中按七种类别总结了六种类型的方面。 刻面类是`locale::facet`嵌套类的子类。\n\n| **刻面类型** | **说明** |\n| `codecvt`，`ctype` | 在一种编码方案与另一种编码方案之间转换，用于对字符进行分类并将其转换为大写或小写 |\n| `collate` | 控制字符串中字符的排序和分组，包括字符串的比较和散列 |\n| `messages` | 从目录中检索本地化消息 |\n| `money` | 将表示货币的数字与字符串相互转换 |\n| `num` | 将数字与字符串相互转换 |\n| `time` | 将数字形式的时间和日期与字符串相互转换 |\n\n刻面类用于将数据转换为字符串，因此它们都有一个用于所用字符类型的模板参数。 `money`、`num,`和`time`面分别由三个类表示。 后缀为`_get`的类处理解析字符串，而后缀为`_put`的类处理字符串格式。 对于`money`和`num`方面，有一个带有`punct`后缀的类，它包含标点符号的规则和符号。\n由于`_get`方面用于将字符序列转换为数字类型，因此类有一个模板参数，您可以使用该参数来指示`get`方法将用来表示字符范围的输入迭代器类型。 类似地，`_put`刻面类有一个模板参数，您可以使用该参数提供`put`方法将转换后的字符串写入的输出迭代器类型。 这两种迭代器类型都提供了默认类型。\n\n`messages`面用于与 POSIX 代码兼容。 该类旨在允许您为应用提供本地化字符串。 其思想是对用户界面中的字符串进行索引，并在运行时通过`messages`方面使用索引访问本地化字符串。 但是，Windows 应用通常使用使用**消息编译器**编译的消息资源文件。 也许正是由于这个原因，作为标准库的一部分提供的`messages`方面没有做任何事情，但是基础设施就在那里，您可以派生您自己的`messages`方面类。\n\n`has_facet`和`use_facet`函数是针对您想要的特定类型的面而模板化的。 所有刻面类都是`locale::facet`类的子类，但是通过这个模板参数，编译器将实例化一个返回您请求的特定类型的函数。 因此，例如，如果要格式化法语区域设置的时间和日期字符串，可以调用以下代码：\n\n```cpp\n    locale loc(\"french\"); \n    const time_put<char>& fac = use_facet<time_put<char>>(loc);\n```\n\n这里，`french`字符串标识区域设置，这是 C Runtime Library`setlocale`函数使用的语言字符串。 第二行获取将数字时间转换为字符串的方面，因此函数模板参数为`time_put<char>`。 该类有一个名为`put`的方法，您可以调用该方法来执行转换：\n\n```cpp\n    time_t t = time(nullptr); \n    tm *td = gmtime(&t); \n    ostreambuf_iterator<char> it(cout); \n    fac.put(it, cout, ' ', td, 'x', '#'); \n    cout << \"n\";\n```\n\n函数`time`(通过`<ctime>`)返回一个包含当前时间和日期的整数，并使用`gmtime`函数将其转换为`tm`结构。 `tm`结构包含年、月、日、小时、分钟和秒的各个成员。 函数`gmtime`将地址返回到函数中静态分配的结构，因此您不必删除它占用的内存。 该方面将通过作为第一个参数传递的输出迭代器将`tm`结构中的数据格式化为字符串。 在本例中，输出流迭代器是从`cout`对象构造的，因此 facet 会将格式流写入控制台(不使用第二个参数，但因为它是一个引用，所以您必须传递一些东西，所以在那里也使用了`cout`对象)。 第三个参数是分隔符(同样，没有使用它)。 第五个和(可选)第六个参数指示所需的格式。 这些字符与 C 运行库函数`strftime`中使用的格式字符相同，是两个单字符，而不是 C 函数使用的格式字符串。 在本例中，`x`用于获取日期，`#`用作修饰符，以获取字符串的长版本。\n\n代码将提供以下输出：\n\n```cpp\n    samedi 28 janvier 2017\n```\n\n请注意，单词不是大写的，也没有标点符号，还要注意顺序：星期名称、日期数字、月，然后是年。\n\n如果`locale`对象构造函数参数更改为`german`，则输出将为：\n\n```cpp\n    Samstag, 28\\. January 2017\n```\n\n这些项目的顺序与法语相同，但单词都是大写的，并使用了标点符号。 如果使用`turkish`，则结果为：\n\n```cpp\n    28 Ocak 2017 Cumartesi\n```\n\n在这种情况下，星期几在字符串的末尾。\n\n两个国家/地区使用相同的语言将产生两个不同的字符串，以下是`american`和`english-uk`的结果：\n\n```cpp\n    Saturday, January 28, 2017\n28 January 2017\n```\n\n这里使用时间作为示例，因为没有流，插入运算符用于`tm`结构，这是一种不寻常的情况。 对于其他类型，有插入操作符将它们放入流中，因此流可以使用区域设置来国际化其显示类型的方式。 例如，您可以将`double`插入到`cout`对象中，该值将打印到控制台。 默认的区域设置是美式英语，使用句点来分隔整数和小数部分，但在其他文化中使用逗号。\n\n`imbue`函数将更改本地化，直到随后调用该方法：\n\n```cpp\n    cout.imbue(locale(\"american\")); \n    cout << 1.1 << \"n\"; \n    cout.imbue(locale(\"french\")); \n    cout << 1.1 << \"n\"; \n    cout.imbue(locale::classic());\n```\n\n这里，流对象被本地化为美国英语，然后在控制台上打印浮点数`1.1`。 接下来，本地化更改为法语，这一次控制台将显示`1,1`。 在法语中，小数点是逗号。 最后一行通过传递从`static classic`方法返回的区域设置来重置流对象。 这将返回所谓的**C 语言环境**，这是 C 和 C++ 的默认设置，并且是美式英语。\n\n`static`方法`global`可用于设置每个流对象将用作默认值的区域设置。 当从流类创建对象时，它调用`locale::global`方法来获取默认区域设置。 流克隆此对象，以便它拥有自己的副本，而不依赖于随后通过调用`global`方法设置的任何本地副本。 请注意，`cin`和`cout`流对象是在调用`main`函数之前创建的，这些对象将使用默认的 C 语言环境，直到您注入另一个语言环境。 但是，重要的是要指出，一旦创建了流，`global`方法对流没有影响，而`imbue`是更改流使用的区域设置的唯一方法。\n\n`global`方法还将调用 C`setlocale`函数来更改 C 运行时库函数使用的区域设置。 这一点很重要，因为一些 C++ 函数(例如`to_string`、`stod`，如下文所述)将使用 C 运行时库函数来转换值。 但是，C 运行时库对 C++ 标准库一无所知，因此调用 C`setlocale`函数来更改默认区域设置不会影响后续创建的流对象。\n\n值得指出的是，`basic_string`类使用由模板参数指示的字符特征类比较字符串。 `string`类使用`char_traits`类，其版本的`compare`方法直接比较两个字符串中的对应字符。 这种比较没有考虑用于比较字符的文化规则。 如果要使用区域性规则进行比较，可以通过`collate`方面进行：\n\n```cpp\n    int compare( \n       const string& lhs, const string& rhs, const locale& loc) \n    { \n        const collate<char>& fac = use_facet<collate<char>>(loc); \n        return fac.compare( \n            &lhs[0], &lhs[0] + lhs.size(), &rhs[0], &rhs[0] + rhs.size()); \n    }\n```\n\n# 字符串和数字\n\n标准库包含用于在 C++ 字符串和数值之间进行转换的各种函数和类。\n\n# 将字符串转换为数字\n\nC++ 标准库包含名为`stod`和`stoi`的函数，用于将 C++ `string`对象转换为数值(`stod`转换为`double`，`stoi`转换为`integer`)。 例如：\n\n```cpp\n    double d = stod(\"10.5\"); \n    d *= 4; \n    cout << d << \"n\"; // 42\n```\n\n这将使用值`10.5`初始化浮点变量`d`，然后在计算中使用该值，并在控制台上打印结果。 输入字符串可能包含无法转换的字符。 如果是这种情况，则字符串的解析在该点结束。 您可以提供指向`size_t`变量的指针，该变量将被初始化为第一个无法转换的字符的位置：\n\n```cpp\n    string str = \"49.5 red balloons\"; \n    size_t idx = 0; \n    double d = stod(str, &idx); \n    d *= 2; \n    string rest = str.substr(idx); \n    cout << d << rest << \"n\"; // 99 red balloons\n```\n\n在前面的代码中，`idx`变量将被初始化为值`4`，表示`5`和`r`之间的空格是第一个不能转换为`double`的字符。\n\n# 将数字转换为字符串\n\n`<string>`库提供了`to_string`函数的各种重载，以将整数类型和浮点类型转换为`string`对象。 此函数不允许您提供任何格式详细信息，因此对于整数，您不能指示字符串表示的基数(例如，十六进制)，而对于浮点转换，您无法控制有效位数等选项。 `to_string`功能是一个设施有限的简单功能。 更好的选择是使用流类，如下节所述。\n\n# 使用流类\n\n您可以使用`cout`对象(`ostream`类的实例)将浮点数和整数打印到控制台或打印到具有`ofstream`实例的文件。 这两个类都将使用成员方法和操纵器将数字转换为字符串，以影响输出字符串的格式。 同样，`cin`对象(`istream`类的实例)和`ifstream`类可以从格式化的流中读取数据。\n\n操纵器是引用流对象并返回该引用的函数。 标准库有各种全局插入运算符，它们的参数是对流对象和函数指针的引用。 适当的插入操作符将使用流对象作为其参数来调用函数指针。 这意味着操纵器将有权访问并可以操纵它插入的流。 对于输入流，还有一些提取操作符，它们有一个函数参数，该参数将调用具有流对象的函数。\n\nC++ Streams 的体系结构意味着在代码中调用的流接口和获取数据的低级基础设施之间有一个缓冲区。 C++ 标准库提供了以 String 对象作为缓冲区的流类。 对于输出流，在流中插入项之后访问字符串，这意味着字符串将包含根据这些插入操作符格式化的项。 类似地，您可以提供一个带有格式化数据的字符串作为输入流的缓冲区，当您使用提取操作符从流中提取数据时，您实际上是在解析字符串并将字符串的一部分转换为数字。\n\n此外，流类有一个`locale`对象，流对象将调用此区域设置的转换方面将字符序列从一种编码转换为另一种编码。\n\n# 输出浮点数\n\n`<ios>`库具有改变流处理数字方式的操纵器。 默认情况下，对于范围在`0.001`到`100000,`之间的数字，输出流将以十进制格式打印浮点数，而对于超出此范围的数字，它将使用带有尾数和指数的科学格式。 此混合格式是`defaultfloat`操纵器的默认行为。 如果您总是想要使用科学记数法，那么您应该将`scientific`操纵器插入到输出流中。 如果只想使用小数格式(即小数点左侧的整数和右侧的派系部分)显示浮点数，则使用`fixed`操纵器修改输出流。 可以通过调用`precision`方法更改小数位数：\n\n```cpp\n    double d = 123456789.987654321; \n    cout << d << \"n\"; \n    cout << fixed; \n    cout << d << \"n\"; \n    cout.precision(9); \n    cout << d << \"n\"; \n    cout << scientific; \n    cout << d << \"n\";\n```\n\n上述代码的输出为：\n\n```cpp\n 1.23457e+08\n 123456789.987654\n 123456789.987654328\n 1.234567900e+08\n```\n\n第一行显示科学记数法用于大数。 第二行显示了`fixed`的默认行为，即将小数指定为 6 位小数。 通过调用`precision`方法给出 9 位小数(通过在流的`<iomanip>`库中插入`setprecision`操纵器可以达到相同的效果)，可以在代码中更改这一点。 最后，通过调用`precision`方法将格式转换为尾数有 9 位小数的科学格式。 默认情况下，指数由小写字母`e`标识。 如果愿意，可以使用`uppercase`操纵器将此设置为大写(使用`nouppercase`设置为小写)。 请注意，小数部分的存储方式意味着，在具有 9 个小数位的固定格式中，我们看到第九位数字是`8`，而不是预期的`1`。\n\n还可以指定是否为正数显示`+`符号；`showpos`操纵器将显示该符号，但默认的`noshowpos`操纵器将不显示该符号。 即使浮点数是整数，`showpoint`操纵器也会确保显示小数点。 默认值为`noshowpoint`，这意味着如果没有小数部分，则不显示小数点。\n\n`setw`操纵器(在`<iomanip>`标题中定义)可用于整数和浮点数。 实际上，此操纵器定义了在控制台上打印时放置在流中的下一个(且仅下一个)项将占用的最小空间宽度：\n\n```cpp\n    double d = 12.345678; \n    cout << fixed; \n    cout << setfill('#'); \n    cout << setw(15) << d << \"n\";\n```\n\n为了说明`setw`操纵器的效果，此代码调用`setfill`操纵器，它指示应该打印散列符号(`#`)而不是空格。 代码的其余部分说明数字应该使用固定格式(默认情况下为 6 位小数)打印在 15 个字符宽的空格中。 结果是：\n\n```cpp\n    ######12.345678\n```\n\n如果数字为负数(或使用`showpos`)，则默认情况下符号将与数字一起使用；如果使用`internal`操纵器(在`<ios>`中定义)，则符号将在为数字设置的空格中左对齐：\n\n```cpp\n    double d = 12.345678; \n    cout << fixed; \n    cout << showpos << internal; \n    cout << setfill('#'); \n    cout << setw(15) << d << \"n\";\n```\n\n上述代码的结果如下：\n\n```cpp\n    +#####12.345678\n```\n\n请注意，空格右侧的`+`符号由井号表示。\n\n`setw`操纵器通常用于允许您输出格式化列中的数据表：\n\n```cpp\n    vector<pair<string, double>> table \n    { { \"one\",0 },{ \"two\",0 },{ \"three\",0 },{ \"four\",0 } }; \n\n    double d = 0.1; \n    for (pair<string,double>& p : table) \n    { \n        p.second = d / 17.0; \n        d += 0.1; \n    } \n\n    cout << fixed << setprecision(6); \n\n    for (pair<string, double> p : table) \n    { \n        cout << setw(6)  << p.first << setw(10) << p.second << \"n\"; \n    }\n```\n\n这将用一个字符串和一个数字填充`vector`对。 用字符串值和零来初始化`vector`，然后在`for`循环中更改浮点数(这里的实际计算无关紧要；重点是创建一些有多个小数位的数字)。 数据分两列打印出来，数字打印有 6 位小数。 这意味着，包括前导零和小数点在内，每个数字将占据 8 个空格。 文本列被指定为 6 个字符宽，数字列被指定为 10 个字符宽。 默认情况下，当您指定列宽时，输出将右对齐，这意味着每个数字前面有两个空格，文本将根据字符串的长度进行填充。 输出如下所示：\n\n```cpp\n one  0.005882\n two  0.011765\n three  0.017647\n four  0.023529\n```\n\n如果希望列中的项目左对齐，则可以使用`left`操纵器。 这将影响所有列，直到使用`right`操纵器将对正更改为右对齐：\n\n```cpp\n    cout << fixed << setprecision(6) << left;\n```\n\n由此产生的输出将为：\n\n```cpp\n one   0.005882\n two   0.011765\n three 0.017647\n four  0.023529\n```\n\n如果希望这两列的对齐方式不同，则需要在打印值之前设置对齐方式。 例如，要左对齐文本，右对齐数字，请使用以下命令：\n\n```cpp\n    for (pair<string, double> p : table) \n    { \n        cout << setw(6) << left << p.first  \n            << setw(10) << right << p.second << \"n\"; \n    }\n```\n\n上述代码的结果如下：\n\n```cpp\n one     0.005882\n two     0.011765\n three   0.017647\n four    0.023529\n```\n\n# 输出整数\n\n整数也可以使用`setw`和`setfill`方法按列打印。 可以插入操纵器以打印基数 8(`oct`)、基数 10(`dec`)和基数 16(`hex`)中的整数。 (也可以使用`setbase`操纵器并传递要使用的基础，但只允许 8、10 和 16 个值。)。 数字可以用指定的基数打印(八进制以`0`为前缀，十六进制以`0x`为前缀)，或者不使用`showbase`和`noshowbase`操纵器。 如果使用`hex`，则`9`上方的数字是字母`a`到`f`，默认情况下这些数字为小写。 如果您希望这些字符为大写，则可以使用`uppercase`操纵器(使用`nouppercase`时为小写)。\n\n# 输出时间和金钱\n\n向`<iomanip>`中的`put_time`函数传递一个用时间、日期和格式字符串初始化的`tm`结构。 该函数返回`_Timeobj`类的实例。 顾名思义，您实际上并不需要创建该类的变量；相反，应该使用该函数将特定格式的时间/日期插入到流中。 有一个将打印`_Timeobj`对象的插入操作符。 该函数的用法如下：\n\n```cpp\n    time_t t = time(nullptr); \n    tm *pt = localtime(&t); \n    cout << put_time(pt, \"time = %X date = %x\") << \"n\";\n```\n\n由此产生的输出为：\n\n```cpp\n    time = 20:08:04 date = 01/02/17\n```\n\n该函数将使用流中的区域设置，因此如果您将区域设置灌输到流中，然后调用`put_time`，则时间/日期将使用格式字符串和区域设置的时间/日期本地化规则进行格式化。 格式字符串使用`strftime`的格式标记：\n\n```cpp\n    time_t t = time(nullptr); \n    tm *pt = localtime(&t); \n    cout << put_time(pt, \"month = %B day = %A\") << \"n\"; \n    cout.imbue(locale(\"french\")); \n    cout << put_time(pt, \"month = %B day = %A\") << \"n\";\n```\n\n上述代码的输出为：\n\n```cpp\n month = March day = Thursday\n month = mars day = jeudi\n```\n\n同样，`put_money`函数返回一个`_Monobj`对象。 同样，这只是传递给此函数的参数的容器，您不应该使用此类的实例。 相反，您需要将此函数插入到输出流中。 实际工作发生在获取当前语言环境中的货币方面的插入操作符中，它使用该操作符将数字格式化为适当的小数位数并确定小数点字符；如果使用千位分隔符，则在将其插入到适当的位置之前使用哪个字符。\n\n```cpp\n    Cout << showbase; \n    cout.imbue(locale(\"German\")); \n    cout << \"German\" << \"n\"; \n    cout << put_money(109900, false) << \"n\"; \n    cout << put_money(\"1099\", true) << \"n\"; \n    cout.imbue(locale(\"American\")); \n    cout << \"American\" << \"n\"; \n    cout << put_money(109900, false) << \"n\"; \n    cout << put_money(\"1099\", true) << \"n\";\n```\n\n上述代码的输出为：\n\n```cpp\n German\n 1.099,00 euros\n EUR10,99\n American\n $1,099.00\n USD10.99\n```\n\n您可以在`double`或字符串中以欧分或美分的形式提供数字，`put_money`函数使用适当的小数点(`,`表示德国，`.`表示美国)和适当的千分隔符(`.`表示德国，`,`表示美国)将数字格式化为欧元或美元。 将`showbase`操纵器插入输出流意味着`put_money`函数将显示货币符号，否则将只显示格式化的数字。 函数`put_money`的第二个参数指定使用货币字符(`false`)还是国际符号(`true`)。\n\n# 使用流将数字转换为字符串\n\n流缓冲类负责从适当的源(文件、控制台等)获取字符和写入字符，并从`<streambuf>`的抽象类`basic_streambuf`派生。 这个基类定义了两个虚方法`overflow`和`underflow,`，它们被派生类覆盖，以便(分别)向与派生类关联的设备写入字符和从与派生类关联的设备读取字符。 流缓冲区类执行获取项或将项放入流的基本操作，由于缓冲区处理字符，因此该类使用字符类型和字符特征的参数进行模板化。\n\n顾名思义，如果使用`basic_stringbuf`，则流缓冲区将是一个字符串，因此读取字符的源和写入字符的目标就是该字符串。 如果使用此类为流对象提供缓冲区，则意味着可以使用为流编写的插入或提取操作符将格式化数据写入字符串或从字符串中读取格式化数据。 `basic_stringbuf`缓冲区是可扩展的，因此当您在流中插入项时，缓冲区将相应地扩展。 有`typedef`，其中缓冲器是`string`(`stringbuf`)或`wstring`(`wstringbuf`)。\n\n例如，假设您已经定义了一个类，并且还定义了一个插入运算符，这样您就可以将此操作符与`cout`对象一起使用，以将值打印到控制台：\n\n```cpp\n    struct point \n    { \n        double x = 0.0, y = 0.0; \n        point(){} \n        point(double _x, double _y) : x(_x), y(_y) {} \n    }; \n\n    ostream& operator<<(ostream& out, const point& p) \n    { \n        out << \"(\" << p.x << \",\" << p.y << \")\"; \n        return out; \n    }\n```\n\n将其与`cout`对象一起使用非常简单--请考虑以下代码：\n\n```cpp\n    point p(10.0, -5.0); \n    cout << p << \"n\";         // (10,-5)\n```\n\n您可以使用`stringbuf`将格式化输出定向到字符串，而不是控制台：\n\n```cpp\n    stringbuf buffer;  \n    ostream out(&buffer); \n    out << p; \n    string str = buffer.str(); // contains (10,-5)\n```\n\n由于流对象处理格式化，这意味着您可以插入任何有插入操作符的数据类型，并且可以使用任何`ostream`格式化方法和任何操纵器。 所有这些方法和操纵器的格式化输出将被插入到缓冲区中的 String 对象中。\n\n另一种选择是使用`<sstream>`中的`basic_ostringstream`类。 此类以用作缓冲区的字符串的字符类型为模板(因此`string`版本为`ostringstream`)。 它派生自`ostream`类，因此您可以在任何需要使用`ostream`对象的地方使用实例。 格式化后的结果可以通过`str`方法访问：\n\n```cpp\n    ostringstream os; \n    os << hex; \n    os << 42; \n    cout << \"The value is: \" << os.str() << \"n\";\n```\n\n此代码获取十六进制(`2a`)中的`42`值；这是通过在流中插入`hex`操作器，然后插入整数来实现的。 格式化字符串是通过调用`str`方法获得的。\n\n# 使用流从字符串读取数字\n\n`cin`对象是`istream`类(在`<istream>`库中)的实例，它可以从控制台输入字符并将其转换为您指定的数字形式。 `ifstream`类(在`<ifstream>`库中)还允许您从文件中输入字符并将其转换为数字形式。 与输出流一样，您可以使用带有字符串缓冲区的流类，以便可以将字符串对象转换为数字值。\n\n`basic_istringstream`类(在`<sstream>`库中)派生自`basic_istream`类，因此您可以创建流对象并从这些对象中提取项目(数字和字符串)。 该类在一个 String 对象上提供这个流接口(`typedef`的关键字`istringstream`基于 a`string`，`wistringstream`基于 a`wstring`)。 在构造此类对象时，使用包含数字的`string`初始化对象，然后使用`>>`运算符提取基本内置类型的对象，就像使用`cin`从控制台提取这些项一样。\n\n需要重申的是，提取操作符将空格视为流中项目之间的分隔符，因此它们将忽略所有前导空格，读取直到下一个空格的非空格字符，并尝试将此子字符串转换为适当的类型，如下所示：\n\n```cpp\n    istringstream ss(\"-1.0e-6\"); \n    double d; \n    ss >> d;\n```\n\n这将用值`-1e-6`初始化`d`变量。 与`cin,`一样，您必须知道流中项目的格式；因此，如果不是从前面示例中的字符串中提取`double`，而是尝试提取一个整数，那么当到达小数点时，对象将停止提取字符。 如果部分字符串未转换，则可以将其余部分提取到字符串对象中：\n\n```cpp\n    istringstream ss(\"-1.0e-6\"); \n    int i; \n    ss >> i; \n    string str; \n    ss >> str; \n    cout << \"extracted \" << i << \" remainder \" << str << \"n\";\n```\n\n这将在控制台上打印以下内容：\n\n```cpp\n    extracted -1 remainder .0e-6\n```\n\n如果字符串中有多个数字，则可以通过多次调用`>>`运算符来提取这些数字。 该流还支持一些操纵器。 例如，如果字符串中的数字为`hex`格式，则可以使用`hex`操纵器通知流这种情况，如下所示：\n\n```cpp\n    istringstream ss(\"0xff\"); \n    int i; \n    ss >> hex; \n    ss >> i;\n```\n\n这表示字符串中的数字是十六进制格式，变量`i`的初始化值为 255。 如果字符串包含非数字值，则流对象仍会尝试将字符串转换为适当的格式。 在下面的代码片段中，您可以通过调用`fail`函数来测试这样的提取是否失败：\n\n```cpp\n    istringstream ss(\"Paul was born in 1942\"); \n    int year; \n    ss >> year; \n    if (ss.fail()) cout << \"failed to read number\" << \"n\";\n```\n\n如果您知道字符串包含文本，则可以将其提取到字符串对象中，但请记住，空格字符被视为分隔符：\n\n```cpp\n    istringstream ss(\"Paul was born in 1942\"); \n    string str; \n    ss >> str >> str >> str >> str; \n    int year; \n    ss >> year;\n```\n\n在这里，数字前面有四个单词，所以代码读取`string`四次。 如果您不知道数字在字符串中的位置，但知道字符串中有一个数字，则可以移动内部缓冲区指针，直到它指向一个数字：\n\n```cpp\n    istringstream ss(\"Paul was born in 1942\"); \n    string str;    \n    while (ss.eof() && !(isdigit(ss.peek()))) ss.get(); \n    int year; \n    ss >> year; \n    if (!ss.fail()) cout << \"the year was \" << year << \"n\";\n```\n\n`peek`方法返回当前位置的字符，但不移动缓冲区指针。 此代码检查此字符是否为数字，如果不是，则通过调用`get`方法移动内部缓冲区指针。 (此代码测试`eof`方法，以确保在缓冲区结束后不会尝试读取字符。)。 如果您知道数字从哪里开始，那么可以调用`seekg`方法将内部缓冲区指针移动到指定位置。\n\n`<istream>`库有一个名为`ws`的操纵器，它可以从流中删除空格。 回想一下前面的内容，我们说过没有从字符串中删除空格的函数。 这是正确的，因为`ws`操纵器从*流*而不是从*字符串*中删除空格，但是由于您可以使用字符串作为流的缓冲区，这意味着您可以使用此函数间接地从字符串中删除空格：\n\n```cpp\n    string str = \"  hello  \"; \n    cout << \"|\" << str1 << \"|n\"; // |  hello  | \n    istringstream ss(str); \n    ss >> ws; \n    string str1; \n    ss >> str1; \n    ut << \"|\" << str1 << \"|n\";   // |hello|\n```\n\n函数`ws`实质上是遍历输入流中的项，当字符不是空格时返回。 如果流是文件或控制台流，则`ws`函数将从这些流中读取字符；在这种情况下，缓冲区由已分配的字符串提供，因此它跳过字符串开头的空格。 请注意，流类将后续的空格视为流中的值之间的分隔符，因此在本例中，流将从缓冲区读取字符，直到出现空格，并且本质上是*向左**和向右*修剪字符串。 然而，这不一定是您想要的。 如果您有一个字符串，其中有几个单词用空格填充，则此代码将只提供第一个单词。\n\n`<iomanip>`库中的`get_money`和`get_time`操纵器允许您使用区域设置的钱和时间方面从字符串中提取钱和时间：\n\n```cpp\n    tm indpday = {}; \n    string str = \"4/7/17\"; \n    istringstream ss(str); \n    ss.imbue(locale(\"french\")); \n    ss >> get_time(&indpday, \"%x\"); \n    if (!ss.fail())  \n    { \n       cout.imbue(locale(\"american\")); \n       cout << put_time(&indpday, \"%x\") << \"n\";  \n    }\n```\n\n在前面的代码中，首先使用法语格式(日/月/年)的日期初始化流，然后使用区域设置的标准日期表示用`get_time`提取日期。 日期被解析成`tm`结构，然后使用`put_time`以美国地区的标准日期表示打印出来。 结果是：\n\n```cpp\n    7/4/2017\n```\n\n# 使用正则表达式\n\n正则表达式是文本的模式，正则表达式解析器可以使用这些模式在字符串中搜索与模式匹配的文本，如果需要，还可以用其他文本替换匹配的项目。\n\n# 定义正则表达式\n\n**正则表达式**(**regex**)由定义模式的字符组成。 该表达式包含对解析器有意义的特殊符号，如果您希望在表达式的搜索模式中使用这些符号，则可以使用反斜杠(`\\`)对它们进行转义。 您的代码通常会将表达式作为`string`对象传递给`regex`类的实例作为构造函数参数。 然后，该对象被传递给`<regex>`中的函数，该函数将使用该表达式解析文本以查找与模式匹配的序列。\n\n下表总结了*一些可以与`regex`类匹配的模式*。\n\n| **图案** | **说明** | **示例** |\n| 字面意思 | 完全匹配的字符 | `li`匹配`flip``lip``plier` |\n| [组] | 匹配组中的单个字符 | `[at]`与`cat`、`cat`、`top`、`pear`匹配 |\n| [^组] | 匹配不在组中的单个字符 | `[^at]`将**c**at，t**o**p 匹配到**p**，**p**ear，p**e**Ar，豌豆**r** |\n| [倒数第一名] | 匹配范围`first`到`last`中的任何字符 | `[0-9]`匹配数字**1**02、1**0**2、10**2** |\n| {n} | 该元素恰好匹配 n 次 | **91{2}**与**911**匹配 |\n| {n，} | 元素匹配 n 次或更多次 | `wel{1,}`匹配`well`和**，欢迎**到来 |\n| {n，m} | 元素匹配 n 到 m 次 | `9{2,4}`匹配`99`、`999`、`9999`、`9999`9，但不匹配 9 |\n| 。 | 通配符，除`n`以外的任何字符 | `a.e`与`ate`和`are`匹配 |\n| *** | 元素匹配零次或多次 | `d*.d`与`.1`、`0.1`、`10.1`匹配，但不匹配 10 |\n| ++ | 元素匹配一次或多次 | `d*.d`与`0.1`、`10.1`匹配，但与 10 或.1 不匹配 |\n| ？ | 元素匹配零次或一次 | `tr?ap`与`trap`和`tap`匹配 |\n| &#124;&#124;&#124; | 匹配由&#124;分隔的任何一个元素 | `th(e&#124;is&#124;at)`与`the`、`this`、`that`匹配 |\n| [[：class：]] | 匹配字符类 | `[[:upper:]]`匹配大写字符：`I`am`R`ichard |\n| 毫微 / 中性的 / 脚注 / 名词 | 匹配换行符 |  |\n| 南方 / 款 / 秒 / 先令 | 匹配任何单个空格 |  |\n| 双角动量的 / 女儿 / 一天 / 白天 | 匹配任何单个数字 | `d`是`[0-9]` |\n| 重量 / 广泛的 / 用 / 随着 | 匹配可以在单词中的字符(大写和小写字符) |  |\n| 由…击中 / 把…贮存入仓 | 在字母数字字符和非字母数字字符之间的边界处匹配 | `d{2}b`匹配 9`99`和 99`99 bd{2}`匹配`99`9 和`99`99 |\n| 美元 | 这条线的末尾 | `s$`匹配行尾的单个空格 |\n| ^ | 行首 | `^d`如果行以数字开头，则匹配 |\n\n您可以使用正则表达式来定义要匹配的模式--Visual C++ 编辑器允许您在搜索对话框中执行此操作(这是开发表达式的一个很好的试验台)。\n\n定义要匹配的模式比定义*而不是*要匹配的模式容易得多。 例如，表达式`w+b<w+>`将匹配字符串`\"vector<int>\"`，因为它有一个或多个单词字符，后跟一个非单词字符(`<`)，然后是一个或多个单词字符，最后是`>`。 此模式将与字符串`\"#include <regex>\"`不匹配，因为在`include`之后有一个空格，并且`b`表示字母数字字符和非字母数字字符之间存在边界。\n\n表中的`th(e|is|at)`示例显示，当您想要提供替代方案时，可以使用圆括号对模式进行分组。 然而，圆括号还有另一个用途--它们允许您捕获组。 因此，如果要执行替换操作，可以将模式作为一个组进行搜索，然后稍后将该组作为指名子组引用(例如，搜索`(Joe)`，以便可以将`Joe`替换为`Tom`)。 您还可以在表达式中引用由圆括号指定的子表达式(称为反向引用)：\n\n```cpp\n    ([A-Za-z]+) +1\n```\n\n此表达式表示：*搜索在 a 到 z 和 A 到 Z 范围内有一个或多个字符的单词；该单词名为 1，因此查找出现两次的单词，并在它们之间留一个空格*。\n\n# 标准库类\n\n要执行匹配或替换，您必须创建正则表达式对象。 这是类`basic_regex`的对象，它具有字符类型的模板参数和正则表达式特征类。 这个类有两个`typedef`：`regex`表示`char`，`wregex`表示宽字符，它们的特性由`regex_traits`和`wregex_traits`类描述。\n\n特征类确定 regex 类如何解析表达式。 例如，回想一下前面的文本，您可以使用`w`表示单词，使用`d`表示数字，使用`s`表示空格。 `[[::]]`语法允许您为字符类使用更具描述性的名称：`alnum`、`digit`、`lower`等。 由于这些是依赖于字符集的文本序列，因此特征类将具有适当的代码来测试表达式是否使用受支持的字符类。\n\n适当的 regex 类将解析该表达式，以使`<regex>`库中的函数能够使用该表达式识别某些文本中的模式：\n\n```cpp\n    regex rx(\"([A-Za-z]+) +1\");\n```\n\n这将使用反向引用搜索重复的单词。 请注意，正则表达式使用`1`作为反向引用，但在字符串中，反斜杠必须转义(`\\`)。 如果使用字符类，如`s`和`d`，则需要进行大量转义。 相反，您可以使用原始字符串(`R\"()\"`)，但请记住，引号内的第一组括号是原始字符串语法的一部分，并不构成正则表达式组：\n\n```cpp\n    regex rx(R\"(([A-Za-z]+) +1)\");\n```\n\n哪一个更具可读性完全取决于您；两者都在双引号中引入了额外的字符，这可能会使快速浏览正则表达式匹配的内容变得混乱。\n\n请记住，正则表达式本身本质上是一个程序，因此`regex`解析器将确定该表达式是否有效，如果它不是对象，则构造函数将抛出`regex_error`类型的异常。 异常处理将在下一章中解释，但必须指出的是，如果未捕获异常，将导致应用在运行时中止。 异常的`what`方法将返回错误的基本描述，`code`方法将返回`regex_constants`命名空间的`error_type`枚举中的一个常量。 没有表示表达式中出现错误的位置。 您应该在外部工具(例如 Visual C++ 搜索)中彻底测试您的表达式。\n\n可以使用一个字符串(C 或 C++)或一对迭代器调用该构造函数，以访问字符串(或其他容器)中的一系列字符，也可以传递一个初始化列表，其中列表中的每一项都是一个字符。 Regex 语言有多种风格；`basic_regex`类的默认值是**ECMAScript**。 如果需要不同的语言(基本 POSIX、扩展 POSIX、AWK、grep 或 egrep)，可以将`syntax_option_type`枚举中定义的一个常量作为构造函数参数传递给`regex_constants`命名空间(副本也可以作为`basic_regex`类中定义的常量)。 您只能指定一种语言风格，但您可以将其与其他一些`syntax_option_type`常量结合使用：`icase`指定不区分大小写，`collate`在匹配中使用区域设置，`nosubs`表示您不想捕获组，`optimize`优化匹配。\n\n该类使用方法`getloc`获取解析器使用的区域设置，并使用`imbue`重置区域设置。 如果您`imbue`一个区域设置，那么您将无法使用`regex`对象进行任何匹配，直到您使用`assign`方法将其重置。 这意味着有两种方法可以使用`regex`对象。 如果希望使用当前区域设置，则将正则表达式传递给构造函数：如果希望使用不同的区域设置，请使用默认构造函数创建一个空的`regex`对象，然后使用该区域设置调用`imbue`并使用`assign`方法传递正则表达式。 一旦解析了正则表达式，您就可以调用`mark_count`方法来获取表达式中的捕获组数量(假设您没有使用`nosubs`)。\n\n# 匹配表达式\n\n一旦构造了`regex`对象，就可以将其传递给`<regex>`库中的方法，以在字符串中搜索模式。 `regex_match`函数以字符串(C 或 C++)或迭代器的形式传递给容器和构造的`regex`对象中的一系列字符。 在其最简单的形式中，仅当存在精确匹配(即表达式与搜索字符串完全匹配)时，该函数才会返回`true`：\n\n```cpp\n    regex rx(\"[at]\"); // search for either a or t \n    cout << boolalpha; \n    cout << regex_match(\"a\", rx) << \"n\";  // true \n    cout << regex_match(\"a\", rx) << \"n\";  // true \n    cout << regex_match(\"at\", rx) << \"n\"; // false\n```\n\n在前面的代码中，搜索表达式是针对给定范围(`a`或`t`)中的单个字符，因此对`regex_match`的前两个调用返回`true`，因为搜索的字符串是一个字符。 最后一个调用返回`false`，因为匹配项与搜索的字符串不同。 如果删除正则表达式中的`[]`，则只有第三个调用返回`true`，因为您正在查找确切的字符串`at`。 如果正则表达式是`[at]+`，因此您要查找字符`a`和`t`中的一个或多个，则所有三个调用都将返回`true`。 您可以通过传递`match_flag_type`枚举中的一个或多个常量来更改确定匹配的方式。\n\n如果将对`match_results`对象的引用传递给此函数，则在搜索之后，该对象将包含有关位置和匹配字符串的信息。 `match_results`对象是`sub_match`对象的容器。 如果函数成功，则意味着整个搜索字符串与表达式匹配，在本例中，返回的第一个`sub_match`项将是整个搜索字符串。 如果表达式有子组(用圆括号标识的模式)，那么这些子组将是`match_results`对象中的附加`sub_match`对象。\n\n```cpp\n    string str(\"trumpet\"); \n    regex rx(\"(trump)(.*)\"); \n    match_results<string::const_iterator> sm; \n    if (regex_match(str, sm, rx)) \n    { \n        cout << \"the matches were: \"; \n        for (unsigned i = 0; i < sm.size(); ++ i)  \n        { \n            cout << \"[\" << sm[i] << \",\" << sm.position(i) << \"] \"; \n        } \n        cout << \"n\"; \n    } // the matches were: [trumpet,0] [trump,0] [et,5]\n```\n\n在这里，表达式是文字`trump`，后跟任意数量的字符。 整个字符串与该表达式匹配，并且有两个子组：文字字符串`trump`和删除`trump`后剩下的任何内容。\n\n`match_results`类和`sub_match`类都是在用于指示匹配项的迭代器类型上模板化的。 有`typedef`调用的`cmatch`和`wcmatch`，其中模板参数分别是`const char*`和`const wchar_t*`，以及`smatch`和`wsmatch`，其中参数分别是`string`和`wstring`对象中使用的迭代器(类似地，还有子匹配类：`csub_match`、`wcsub_match`、`ssub_match`和`wssub_match`)。\n\n`regex_match`函数可能非常严格，因为它查找模式和搜索字符串之间的精确匹配。 `regex_search`函数更灵活，因为如果搜索字符串中有与表达式匹配的子字符串，它将返回`true`。 请注意，即使搜索字符串中有多个匹配项，`regex_search`函数也只会查找第一个匹配项。 如果要解析字符串，则必须多次调用该函数，直到它指示不再有匹配为止。 这就是使用迭代器访问搜索字符串的重载变得有用的地方：\n\n```cpp\n    regex rx(\"bd{2}b\"); \n    smatch mr; \n    string str = \"1 4 10 42 100 999\"; \n    string::const_iterator cit = str.begin(); \n    while (regex_search(cit, str.cend(), mr, rx)) \n    { \n        cout << mr[0] << \"n\"; \n        cit += mr.position() + mr.length(); \n    }\n```\n\n在这里，表达式将匹配用空格括起来的两位数(`d{2}`)(两个`b`模式表示前后的边界)。 循环以指向字符串开头的迭代器开始，当找到匹配项时，该迭代器递增到该位置，然后递增匹配的长度。 进一步解释的`regex_iterator`对象包装了这个行为。\n\n`match_results`类提供对包含的`sub_match`对象的迭代器访问，因此您可以使用 Range`for`。 最初，容器似乎以一种奇怪的方式工作，因为它知道`sub_match`对象在搜索字符串中的位置(通过`position`方法，该方法获取子匹配对象的索引)，但是`sub_match`对象似乎只知道它引用的字符串。 然而，仔细检查`sub_match`类就会发现它派生自`pair`，其中两个参数都是字符串迭代器。 这意味着`sub_match`对象具有指定子字符串的原始字符串中的范围的迭代器。 `match_result`对象知道原始字符串的开始，并可以使用`sub_match.first`迭代器来确定子字符串开始的字符位置。\n\n`match_result`对象有一个`[]`运算符(和`str`方法)，它返回指定组的子字符串；这将是一个使用迭代器构造的字符串，指向原始字符串中的字符范围。 `prefix`方法返回匹配之前的字符串，`suffix`方法返回匹配之后的字符串。 因此，在前面的代码中，第一个匹配项将是`10`，前缀将是`1 4`，后缀将是`42 100 999`。 相反，如果您访问`sub_match`对象本身，它只知道它的长度和字符串，这是通过调用`str`方法获得的。\n\n`match_result`对象也可以通过`format`方法返回结果。 这将获取一个格式字符串，其中匹配组通过由`$`符号(`$1`、`$2`等)标识的编号占位符标识。 输出可以是流，也可以作为字符串从方法返回：\n\n```cpp\n    string str(\"trumpet\"); \n    regex rx(\"(trump)(.*)\"); \n    match_results<string::const_iterator> sm; \n    if (regex_match(str, sm, rx)) \n    { \n        string fmt = \"Results: [$1] [$2]\"; \n        cout << sm.format(fmt) << \"n\"; \n    } // Results: [trump] [et]\n```\n\n使用`regex_match`或`regex_search,`，您可以使用圆括号来标识子组。 如果模式匹配，则可以使用通过引用函数传递的适当`match_results`对象来获取这些子组。 如前所述，`match_results`对象是`sub_match`对象的容器。 子匹配可以与`<`、`!=`、`==`、`<=`、`>`和`>=`运算符进行比较，这些运算符比较迭代器指向的项目(即子字符串)。 此外，可以将`sub_match`个对象插入到流中。\n\n# 使用迭代器\n\n该库还为正则表达式提供了迭代器类，提供了一种不同的字符串解析方式。 由于该类将涉及字符串比较，因此它以元素类型和特征为模板。 该类将需要遍历字符串，因此第一个模板参数是字符串迭代器类型，可以从中推导出元素和特征类型。 `regex_iterator`类是一个正向迭代器，因此它有一个`++ `运算符，并且它提供了一个`*`运算符来访问`match_result`对象。 在前面的代码中，您看到一个`match_result`对象被传递给`regex_match`和`regex_search`函数，这两个函数使用它来包含它们的结果。 这就提出了一个问题，即哪些代码填充了通过`regex_iterator`访问的`match_result`对象。 答案在于迭代器的`++ `运算符：\n\n```cpp\n    string str = \"the cat sat on the mat in the bathroom\"; \n    regex rx(\"(b(.at)([^ ]*)\"); \n    regex_iterator<string::iterator> next(str.begin(), str.end(), rx); \n    regex_iterator<string::iterator> end; \n\n    for (; next != end; ++ next) \n    { \n        cout << next->position() << \" \" << next->str() << \", \"; \n    } \n    cout << \"n\"; \n    // 4 cat, 8 sat, 19 mat, 30 bathroom\n```\n\n在此代码中，将在字符串中搜索第二个和第三个字母为`at`的单词。 `b`表示模式必须位于单词的开头(`.`表示单词可以以任何字母开头)。 这三个字符周围有一个捕获组，空格以外的一个或多个字符有第二个捕获组。\n\n迭代器对象`next`由指向要搜索的字符串的迭代器和`regex`对象构成。 `++ `操作符实质上调用`regex_search`函数，同时保持要执行下一次搜索的位置。 如果搜索未找到模式，则操作符返回序列的**结尾**迭代器，这是由默认构造函数(此代码中的`end`对象)创建的迭代器。 此代码打印出完全匹配，因为我们对`str`方法(`0`)使用默认参数。 如果希望匹配实际的子字符串，请使用`str(1)`，结果将为：\n\n```cpp\n    4 cat, 8 sat, 19 mat, 30 bat\n```\n\n由于`*`(和`->`)运算符提供对`match_result`对象的访问，因此还可以访问`prefix`方法来获取匹配之前的字符串，而`suffix`方法将返回匹配之后的字符串。\n\n`regex_iterator`类允许您遍历匹配的子字符串，而`regex_token_iterator`则更进一步，因为它还允许您访问所有子匹配。 在使用中，除了在构造方面，这个类与`regex_iterator,`相同。 `regex_token_iterator`构造函数有一个参数，用于指示您希望通过`*`操作符访问哪个子匹配。 值`-1`表示需要前缀，值`0`表示需要整个匹配，值`1`或更高表示需要编号的子匹配项。 如果愿意，可以传递带有所需子匹配类型的`int vector`或 C 数组：\n\n```cpp\n    using iter = regex_token_iterator<string::iterator>; \n    string str = \"the cat sat on the mat in the bathroom\"; \n    regex rx(\"b(.at)([^ ]*)\");  \n    iter next, end; \n\n    // get the text between the matches \n    next = iter(str.begin(), str.end(), rx, -1); \n    for (; next != end; ++ next) cout << next->str() << \", \"; \n    cout << \"n\"; \n    // the ,  ,  on the ,  in the , \n\n    // get the complete match \n    next = iter(str.begin(), str.end(), rx, 0); \n    for (; next != end; ++ next) cout << next->str() << \", \"; \n    cout << \"n\"; \n    // cat, sat, mat, bathroom, \n\n    // get the sub match 1 \n    next = iter(str.begin(), str.end(), rx, 1); \n    for (; next != end; ++ next) cout << next->str() << \", \"; \n    cout << \"n\"; \n    // cat, sat, mat, bat, \n\n    // get the sub match 2 \n    next = iter(str.begin(), str.end(), rx, 2); \n    for (; next != end; ++ next) cout << next->str() << \", \"; \n    cout << \"n\"; \n    // , , , hroom,\n```\n\n# 替换字符串\n\n`regex_replace`方法与其他方法类似，因为它接受一个字符串(C 字符串或 C++ `string`对象，或某个字符范围的迭代器)、一个`regex`对象和可选标志。 此外，该函数有一个格式字符串，并返回一个`string`。 格式字符串实质上是从正则表达式的匹配结果传递给每个`results_match`对象的`format`方法。 然后，该格式化字符串被用作相应匹配子字符串的替换。 如果没有匹配项，则返回搜索字符串的副本。\n\n```cpp\n    string str = \"use the list<int> class in the example\"; \n    regex rx(\"b(list)(<w*> )\"); \n    string result = regex_replace(str, rx, \"vector$2\"); \n    cout << result << \"n\"; // use the vector<int> class in the example\n```\n\n在前面的代码中，我们说整个匹配的字符串(应该是`list<`，后跟一些文本，后跟`>`和一个空格)应该替换为`vector,`，然后是第二个子匹配(`<`，后跟一些文本，后跟`>`和一个空格)。 结果是`list<int>`将替换为`vector<int>`。\n\n# 使用字符串\n\n该示例将以文本文件的形式读入电子邮件并进行处理。 互联网邮件格式的电子邮件分为两部分：邮件头和邮件正文。 这是一个简单的处理过程，因此不会尝试处理 MIME 电子邮件正文格式(尽管此代码可以作为该操作的起点)。 电子邮件正文将在第一个空白行之后开始，互联网标准规定每行不能超过 78 个字符。 如果它们较长，则不能超过 998 个字符。 这意味着换行符(回车符、换行符对)用于维护此规则，段落末尾由空行表示。\n\n标头更加复杂。 在它们最简单的形式中，标题在一行上，格式为`name:value`。 标题名称与标题值之间用冒号分隔。 可以使用一种称为折叠空格的格式将标题拆分到多行，其中拆分标题的换行符放在空格(空格、制表符等)之前。 这意味着以空格开头的行是前一行标题的延续。 标头通常包含用分号分隔的`name=value`对，因此能够分隔这些子项非常有用。 有时这些子项没有值，也就是说，会有一个子项以分号结尾。\n\n本例将把一封电子邮件作为一系列字符串，使用这些规则将创建一个具有标题集合和包含正文的字符串的对象。\n\n# 正在创建项目\n\n为项目创建一个文件夹，并创建一个名为`email_parser.cpp`的 C++ 文件。 由于此应用将读取文件并处理字符串，因此为适当的库添加 Include，并添加代码以从命令行获取文件名：\n\n```cpp\n    #include <iostream> \n    #include <fstream> \n    #include <string> \n\n    using namespace std; \n\n    void usage() \n    { \n        cout << \"usage: email_parser file\" << \"n\"; \n        cout << \"where file is the path to a file\" << \"n\"; \n    } \n\n    int main(int argc, char *argv[]) \n    { \n        if (argc <= 1) \n        { \n            usage(); \n            return 1; \n        } \n\n        ifstream stm; \n        stm.open(argv[1], ios_base::in); \n        if (!stm.is_open()) \n        { \n            usage(); \n            cout << \"cannot open \" << argv[1] << \"n\"; \n            return 1; \n        } \n\n        return 0; \n    }\n```\n\n标题将具有名称和正文。 正文可以是单个字符串，也可以是一个或多个子项。 创建一个类来表示标头的主体，暂时将其视为一行。 在`usage`函数上方添加以下类：\n\n```cpp\n    class header_body \n    { \n        string body; \n    public: \n        header_body() = default; \n        header_body(const string& b) : body(b) {} \n        string get_body() const { return body; } \n    };\n```\n\n这只是将类包装在`string`周围；稍后我们将添加代码以分离出`body`数据成员中的子项。 现在创建一个类来表示电子邮件。 在`header_body`类之后添加以下代码：\n\n```cpp\n    class email \n    { \n        using iter = vector<pair<string, header_body>>::iterator; \n        vector<pair<string, header_body>> headers; \n        string body; \n\n    public: \n        email() : body(\"\") {} \n\n        // accessors \n        string get_body() const { return body; } \n        string get_headers() const; \n        iter begin() { return headers.begin(); } \n        iter end() { return headers.end(); } \n\n        // two stage construction \n        void parse(istream& fin); \n    private: \n        void process_headers(const vector<string>& lines); \n    };\n```\n\n`headers`数据成员将标头作为名称/值对保存。 这些项目存储在`vector`而不是`map`中，因为当电子邮件从一个邮件服务器传递到另一个邮件服务器时，每个服务器可能会添加电子邮件中已存在的标题，因此标题会重复。 我们可以使用`multimap`，但这样会丢失标题的顺序，因为`multimap`将按有助于搜索项目的顺序存储项目。\nA`vector`保持项在容器中插入的顺序，由于我们将按顺序解析电子邮件，这意味着`headers`数据成员将以与电子邮件中相同的顺序获得标题项。 添加适当的 Include 以便可以使用`vector`类。\n\n主体和标头具有作为单个字符串的访问器。 此外，还有从`headers`数据成员返回迭代器的访问器，这样外部代码就可以迭代通过`headers`数据成员(该类的完整实现将具有允许您按名称搜索头的访问器，但就本例而言，只允许迭代)。\n\n该类支持两阶段构造，其中大部分工作是通过将输入流传递给`parse`方法来执行的。 `parse`方法将电子邮件读入为`vector`对象中的一系列行，并调用私有函数`process_headers`将这些行解释为标题。\n\n`get_headers`方法很简单：它只迭代标题，并以`name: value`格式在每行放置一个标题。 添加内联函数：\n\n```cpp\n    string get_headers() const \n    { \n        string all = \"\"; \n        for (auto a : headers) \n        { \n            all += a.first + \": \" + a.second.get_body(); \n            all += \"n\"; \n        } \n        return all; \n    }\n```\n\n接下来，您需要从文件中读取电子邮件，并提取正文和标题。 `main`函数已经有了打开文件的代码，所以创建一个`email`对象并将文件的`ifstream`对象传递给`parse`方法。 现在使用存取器打印出解析后的电子邮件。 将以下内容添加到`main`函数的末尾：\n\n```cpp\n email eml; eml.parse(stm); cout << eml.get_headers(); cout << \"n\"; cout << eml.get_body() << \"n\"; \n\n        return 0; \n    }\n```\n\n在`email`类声明之后，添加`parse`函数的定义：\n\n```cpp\n    void email::parse(istream& fin) \n    { \n        string line; \n        vector<string> headerLines; \n        while (getline(fin, line)) \n        { \n            if (line.empty()) \n            { \n                // end of headers \n                break; \n            } \n            headerLines.push_back(line); \n        } \n\n        process_headers(headerLines); \n\n        while (getline(fin, line)) \n        { \n            if (line.empty()) body.append(\"n\"); \n            else body.append(line); \n        } \n    }\n```\n\n此方法很简单：它重复调用`<string>`库中的`getline`函数来读取`string`，直到检测到换行符。 在该方法的前半部分中，字符串存储在`vector`中，然后传递给`process_headers`方法。 如果读入的字符串为空，则表示已读取空行--在这种情况下，所有标头都已被读取。 在该方法的后半部分中，读入电子邮件的正文。 `getline`函数会将用于格式化电子邮件的换行符去掉为 78 个字符的行长，因此循环只将这些行附加为一个字符串。 如果读入空行，则表示段落结束，因此会在正文字符串中添加换行符。\n\n在`parse`方法之后，添加`process_headers`方法：\n\n```cpp\n    void email::process_headers(const vector<string>& lines) \n    { \n        string header = \"\"; \n        string body = \"\"; \n        for (string line : lines) \n        { \n            if (isspace(line[0])) body.append(line); \n            else \n            { \n                if (!header.empty()) \n                { \n                    headers.push_back(make_pair(header, body)); \n                    header.clear(); \n                    body.clear(); \n                } \n\n                size_t pos = line.find(':'); \n                header = line.substr(0, pos); \n                pos++ ; \n                while (isspace(line[pos])) pos++ ; \n                body = line.substr(pos); \n            } \n        } \n\n        if (!header.empty()) \n        { \n            headers.push_back(make_pair(header, body)); \n        } \n    }\n```\n\n这段代码遍历集合中的每一行，当它有一个完整的标题时，它将字符串拆分成冒号上的名称/正文对。 在循环中，第一行测试第一个字符是否为空格；如果不是，则检查`header`变量是否有值；如果有，则在清除`header`和`body`变量之前将名称/正文对存储在类`headers`数据成员中。\n\n下面的代码作用于从集合中读取的行。 这段代码假设这是标题行的开始，因此在此处搜索字符串以查找冒号并拆分。 标题的名称在冒号之前，标题的正文(去掉前导空格)在冒号之后。 由于我们不知道标题正文是否会折叠到下一行上，因此不存储名称/正文；相反，允许`while`循环再次重复，以便测试下一行的第一个字符是否为空格，如果是，则将其附加到正文中。 保持名称/正文对直到`while`循环的下一次迭代的操作意味着最后一行将不会存储在循环中，因此在方法的末尾有一个测试，以查看`header`变量是否为空，如果不为空，则存储名称/正文对。\n\n现在可以编译代码(记得使用`/EHsc`开关)来测试没有输入错误。 要测试代码，您应该将来自电子邮件客户端的电子邮件另存为文件，然后使用此文件的路径运行`email_parser`应用。 以下是 Internet 消息格式 RFC 5322 的示例电子邮件之一，您可以将其放入文本文件中以测试代码：\n\n```cpp\n    Received: from x.y.test\n by example.net\n via TCP\n with ESMTP\n id ABC12345\n for <mary@example.net>;  21 Nov 1997 10:05:43 -0600\nReceived: from node.example by x.y.test; 21 Nov 1997 10:01:22 -0600\nFrom: John Doe <jdoe@node.example>\nTo: Mary Smith <mary@example.net>\nSubject: Saying Hello\nDate: Fri, 21 Nov 1997 09:55:06 -0600\nMessage-ID: <1234@local.node.example>\n\nThis is a message just to say hello.\nSo, \"Hello\".\n```\n\n您可以使用电子邮件消息测试应用，以显示解析已经考虑了标题格式，包括折叠空格。\n\n# 正在处理表头子项\n\n下一步是将标题主体处理为子项。 为此，请将以下突出显示的声明添加到`header_body`类的`public`部分：\n\n```cpp\n    public: \n        header_body() = default; \n        header_body(const string& b) : body(b) {} \n        string get_body() const { return body; } \n        vector<pair<string, string>> subitems(); \n    };\n```\n\n每个子项将是一个名称/值对，由于子项的顺序可能很重要，因此子项存储在`vector`中。 更改`main`函数，删除对`get_headers`的调用，并分别打印出每个标题：\n\n```cpp\n    email eml; \n    eml.parse(stm); \n    for (auto header : eml) { cout << header.first << \" : \"; vector<pair<string, string>> subItems = header.second.subitems(); if (subItems.size() == 0) { cout << header.second.get_body() << \"n\"; } else { cout << \"n\"; for (auto sub : subItems) { cout << \"   \" << sub.first; if (!sub.second.empty()) \n                cout << \" = \" << sub.second;         \n                cout << \"n\"; } } } \n    cout << \"n\"; \n    cout << eml.get_body() << endl;\n```\n\n由于`email`类实现了`begin`和`end`方法，这意味着 Range`for`循环将调用这些方法来访问`email::headers`数据成员上的迭代器。 每个迭代器都将提供对`pair<string,header_body>`对象的访问，因此在此代码中，我们首先打印出标题名称，然后访问`header_body`对象上的子项。 如果没有子项，则标题仍有一些文本，但不会拆分成子项，因此我们调用`get_body`方法来打印字符串。 如果有子项，则这些子项将被打印出来。 有些物品会有身体，有些则没有。 如果项有正文，则子项以`name = value`的形式打印。\n\n最后一个动作是解析标题正文，将它们拆分成子项。 在`header_body`类下面，将该方法的定义添加到下面：\n\n```cpp\n    vector<pair<string, string>> header_body::subitems() \n    { \n        vector<pair<string, string>> subitems; \n        if (body.find(';') == body.npos) return subitems; \n\n        return subitems; \n    }\n```\n\n由于子项之间使用分号分隔，因此有一个简单的测试来查找`body`字符串中的分号。 如果没有分号，则返回空的`vector`。\n\n现在，代码必须重复解析字符串，提取子项。 有几个案例需要解决。 大多数子项将采用`name=value;,`的形式，因此必须提取此子项并在等号字符处拆分，并丢弃分号。\n有些子项没有值，格式为`name;`，在这种情况下，会丢弃分号，并使用子项值的空字符串存储项。 最后，标题中的最后一项不能以分号结尾，因此必须考虑到这一点。\n\n添加以下`while`循环：\n\n```cpp\n    vector<pair<string, string>> subitems; \n    if (body.find(';') == body.npos) return subitems; \n    size_t start = 0;\n size_t end = start; while (end != body.npos){}\n```\n\n顾名思义，`start`变量是子项的起始索引，`end`是子项的结束索引。 第一个操作是忽略任何空格，因此在`while`循环中添加：\n\n```cpp\n    while (start != body.length() && isspace(body[start])) \n    { \n        start++ ; \n    } \n    if (start == body.length()) break;\n```\n\n这只是在`start`索引引用空格字符时递增它，只要它没有到达字符串的末尾。 如果到达字符串的末尾，则意味着没有更多的字符，因此循环结束。\n\n接下来，添加以下内容以搜索`=`和`;`字符并处理其中一种搜索情况：\n\n```cpp\n    string name = \"\"; \n    string value = \"\"; \n    size_t eq = body.find('=', start); \n    end = body.find(';', start); \n\n    if (eq == body.npos) \n    { \n        if (end == body.npos) name = body.substr(start); \n        else name = body.substr(start, end - start); \n    } \n    else \n    {\n    } \n    subitems.push_back(make_pair(name, value)); \n    start = end + 1;\n```\n\n如果找不到搜索到的项目，`find`方法将返回`npos`值。 第一个调用查找`=`字符，第二个调用查找分号。 如果找不到`=`，则该项没有值，只有名称。 如果找不到分号，则表示`name`是从`start`索引到字符串末尾的整个字符串。 如果有分号，则`name`从`start`索引一直到`end`指示的索引(因此要复制的字符数是`end-start`)。 如果找到`=`字符，则此时需要拆分字符串，该代码将在稍后显示。 一旦给`name`和`value`变量赋值，这些值就被插入到`subitems`数据成员中，并且将`start`索引移动到`end`索引之后的字符。 如果`end`索引为`npos`，则`start`索引的值将无效，但这并不重要，因为`while`循环将测试`end`索引的值，如果索引为`npos`，则会中断循环。\n\n最后，您需要添加子项中有`=`字符时的代码。 添加以下突出显示的文本：\n\n```cpp\n    if (eq == body.npos) \n    { \n        if (end == body.npos) name = body.substr(start); \n        else name = body.substr(start, end - start); \n    } \n    else \n    { \n if (end == body.npos) { name = body.substr(start, eq - start); value = body.substr(eq + 1); } else { if (eq < end) { name = body.substr(start, eq - start); value = body.substr(eq + 1, end - eq - 1); } else { name = body.substr(start, end - start); } } \n    }\n```\n\n第一行测试分号搜索是否失败。 在本例中，名称是从`start`索引到等号前的字符，值是紧跟在等号之后直到字符串末尾的文本。\n\n如果等号和分号字符有有效索引，则还有一种情况需要检查。 等号字符的位置可能在分号之后，在这种情况下，这意味着该子项没有值，并且等号字符将用于后续子项。\n\n此时，您可以编译代码并使用包含电子邮件的文件对其进行测试。 程序的输出应该是电子邮件拆分成标题和正文，每个标题拆分成子项，子项可以是简单的字符串或`name=value`对。\n\n# 简略的 / 概括的 / 简易判罪的 / 简易的\n\n在本章中，您已经看到了各种支持字符串的 C++ 标准库类。 您已经了解了如何从流中读取字符串、如何将字符串写入流、如何在数字和字符串之间进行转换，以及如何使用正则表达式操作字符串。 当您编写代码时，不可避免地会花费时间运行代码以检查它是否按照您的规范工作。 这包括提供检查算法结果的代码、将中间代码记录到调试设备的代码，当然还有在调试器下运行代码。 下一章是关于调试代码的！*"
  },
  {
    "path": "docs/begin-cpp-prog/10.md",
    "content": "# 十、诊断和调试\n\n软件是复杂的；然而，无论是在代码开发的正常测试阶段，还是在发布错误报告时，您在设计代码时，有时都必须对其进行调试。 谨慎的做法是设计代码，使测试和调试尽可能简单。 这意味着添加跟踪和报告代码，确定不变量以及前置条件和后置条件，以便有一个起点来测试代码，并编写具有可理解且有意义的错误代码的函数。\n\n# 准备代码\n\nC++ 和 C 标准库具有广泛的函数，允许您应用跟踪和报告函数，以便您可以测试代码是否以预期的方式处理数据。 这些工具大多使用条件编译，因此报告只在调试版本中出现，但如果您为跟踪提供有意义的消息，它们将成为代码文档的一部分。 在您可以报告代码的行为之前，您首先必须知道它会带来什么。\n\n# 不变量和条件\n\n类不变式是条件，即对象状态，您知道它保持为真。 在方法调用期间，对象状态将更改，可能更改为使对象无效的状态，但一旦公共方法完成，对象状态必须保持一致状态。 不能保证用户调用类上的方法的顺序，甚至根本不能保证它们调用方法，所以无论用户调用什么方法，对象都必须是可用的。 对象的不变方面适用于方法调用级别：在方法调用之间，对象必须一致且可用。\n\n例如，假设您有一个表示日期的类：它包含一个介于 1 和 31 之间的日期数字，一个介于 1 和 12 之间的月份数字，以及一个年份数字。 类不变式是，无论您对 Date 类的对象执行什么操作，它都将始终保存有效日期。 这意味着用户可以安全地使用 Date 类的对象。 这还意味着类上的其他方法(例如，确定两个日期之间有多少天的方法`operator-`)可以假定 Date 对象中的值是有效的，因此这些方法不必检查它们操作的数据的有效性。\n\n但是，有效日期大于范围 1 到 31(天数)和 1 到 12(月数)，因为不是每个月都有 31 天。 因此，如果您有一个有效的日期，比如 1997 年 4 月 5 日，并且您调用`set_day`方法将天数设置为 31，那么就违反了类不变条件，因为 4 月 31 日不是一个有效的日期。 如果要更改 Date 对象中的值，唯一安全的方法是同时更改所有值：日、月和年，因为这是保持类不变性的唯一方法。\n\n一种方法是在调试构建中定义私有方法，该方法测试类的不变条件，并使用断言(参见后面)确保维护不变条件。 您可以在可公开访问的方法离开之前调用此类方法，以确保对象保持一致状态。 方法还应该定义前置条件和后置条件。 前置条件是在调用方法之前强制为真的条件，后置条件是在方法完成后保证为真的条件。 对于类上的方法，类不变量是前提条件(因为在调用方法之前对象的状态应该是一致的)，不变量也是后置条件(因为在方法完成之后，对象状态应该是一致的)。\n\n还有一些前提条件是方法的调用方负责的。 前提条件是调用者确保的有文档记录的责任。 例如，Date 类将有一个前提条件，即日期数字介于 1 和 31 之间。 这简化了类代码，因为接受天数的方法可以假设传递的值永远不会超出范围(尽管，因为有些月份的天数少于 31 天，所以值可能仍然无效)。 同样，在调试版本中，您可以使用断言来检查这些前提条件是否为真，并且断言中的测试将在发布版本中编译掉。 在方法的末尾将有后置条件，也就是说，将维护类不变量(并且对象的状态将是有效的)，并且返回值将是有效的。\n\n# 条件编译\n\n正如[第 1 章](01.html)、*从 C++*开始的解释，当编译 C++ 程序时，有一个预编译步骤，将 C++ 源文件中包含的所有文件整理成单个文件，然后编译该文件。 预处理器还展开宏，并根据符号的值包括一些代码和排除其他代码。\n\n在其最简单的形式中，条件编译用`#ifdef`和`#endif`括起代码(也可以选择使用`#else`)，以便只有在定义了指定符号的情况下才编译这些指令之间的代码。\n\n```cpp\n    #ifdef TEST \n       cout << \"TEST defined\" << endl;     \n    #else \n       cout << \"TEST not defined\" << endl; \n    #endif\n```\n\n您可以保证只编译其中的一行，并且保证至少编译其中的一行。 如果定义了符号`TEST`，则将编译第一行，而对于编译器而言，第二行不存在。 如果未定义符号`TEST`，则将编译第二行。 如果您想以相反的顺序键入这些行，可以使用`#ifndef`指令。 通过条件编译提供的文本可以是 C++ 代码，也可以使用当前翻译单元中的其他符号(使用`#define`)或未定义的现有符号(使用`#undef`)来定义。\n\n`#ifdef`指令只是确定符号是否存在：它不测试它的值。 `#if`指令允许您测试表达式。 您可以将符号设置为具有值，并根据该值编译特定代码。 表达式必须是整型的，因此单个`#if`块可以使用`#if`和多个`#elif`指令以及(最多)一个`#else`指令测试多个值：\n\n```cpp\n    #if TEST < 0 \n       cout << \"negative\" << endl; \n    #elif TEST > 0 \n       cout << \"positive\" << endl; \n    #else \n       cout << \"zero or undefined\" << endl; \n    #endif\n```\n\n如果未定义符号，则`#if`指令将该符号视为具有值`0`；如果要区分这些情况，可以使用`defined`运算符来测试是否定义了符号。 最多只编译`#if`/`#endif`块中的一个部分，如果值不匹配，则不会编译任何代码。 表达式可以是宏，在这种情况下，宏将在测试条件之前展开。\n\n定义符号有三种方法。 第一种方式不受您的控制：编译器将定义一些符号(通常带有`__`或`_`前缀)，为您提供有关编译器和编译过程的信息。 这些符号中的一些将在后面的部分中描述。 其他两种方式完全由您控制--您可以使用`#define`在源文件(或头文件)中定义符号，也可以使用`/D`开关在命令行中定义它们：\n\n```cpp\n    cl /EHsc prog.cpp /DTEST=1\n```\n\n这将编译源代码，并将符号`TEST`设置为值`1`。\n\n您通常会使用条件编译来提供不应在生产代码中使用的代码，例如，在调试模式或测试代码时使用额外的跟踪代码。 例如，假设您有从数据库返回数据的库代码，但您怀疑库函数中的 SQL 语句有问题，返回的值太多。 在这里，您可以决定测试，添加代码以记录返回的值的数量：\n\n```cpp\n    vector<int> data = get_data(); \n    #if TRACE_LEVEL > 0 \n    cout << \"number of data items returned: \" << data.size() << endl; \n    #endif\n```\n\n这样的跟踪消息会污染您的用户界面，您会希望在生产代码中避免它们。 但是，在调试过程中，它们在确定哪里发生问题方面可能是无价的。\n\n在调试模式下调用的任何代码，条件代码都应该是`const`方法(这里是`vector::size`)，也就是说，它们不应该影响任何对象或应用数据的状态。 您必须确保代码的逻辑在调试模式下与在发布模式下的逻辑*和*完全相同。\n\n# 使用语用\n\nPragma 是特定于编译器的，通常关注目标文件中代码段的技术细节。 有几个 Visual C++ 编译指示在调试代码时很有用。\n\n通常，您会希望在编译代码时尽可能少地出现警告。 Visual C++ 编译器的默认警告是`/W1`，这意味着只列出最严重的警告。 将该值增加到 2、3 或最大值 4 会逐渐增加编译期间给出的警告数。 使用`/Wall`将发出 4 级警告和默认禁用的警告。 最后一个选项，即使对于最简单的代码，也会产生一个充满警告的屏幕。 当您有数百个警告时，有用的错误消息将隐藏在大量不重要的警告之间。 由于 C++ 标准库很复杂，并且使用了一些几十年前的代码，因此编译器会警告您一些构造。 为防止这些警告污染生成的输出，已禁用选择性文件中的特定警告。\n\n如果您支持较旧的库代码，您可能会发现代码编译时会出现警告。 您可能会尝试使用编译器`/W`开关来降低警告级别，但这将抑制所有高于您启用的警告的警告，并且它同样适用于您的代码，就像您可能包含在项目中的库代码一样。 `warning`杂注为您提供了更大的灵活性。 有两种方式可以调用它--可以重置警告级别以覆盖编译器`/W`开关，还可以更改特定警告的警告级别或完全禁用警告报告。\n\n例如，在`<iostream>`标题的顶部是一行：\n\n```cpp\n    #pragma warning(push,3)\n```\n\n这表示存储当前警告级别，对于此文件的其余部分(或直到其更改)，将警告级别设置为 3。文件的底部为一行：\n\n```cpp\n    #pragma warning(pop)\n```\n\n这会将警告级别恢复到先前存储的级别。\n\n您还可以更改报告一个或多个警告的方式。 例如，在`<istream>`的顶部是：\n\n```cpp\n    #pragma warning(disable: 4189)\n```\n\n此`pragma`的第一部分是说明符`disable`，它指示禁用了警告类型(在本例中为 4189)的报告。 如果选择，可以使用警告级别(`1`、`2`、`3`或`4`)作为说明符来更改警告的警告级别。 此功能的一个用途是只降低您正在处理的一段代码的警告级别，然后在该代码之后将其返回到其默认级别。 例如：\n\n```cpp\n    #pragma warning(2: 4333) \n    unsigned shift8(unsigned char c)  \n    { \n        return c >> 8;  \n    } \n    #pragma warning(default: 4333)\n```\n\n此函数将字符右移 8 位，这将生成 1 级警告 4333(*右移过大，数据丢失*)。 这是一个问题，需要修复，但目前，您希望编译代码时不使用此代码中的警告，因此警告级别更改为级别 2。使用默认警告级别(`/W1`)将不会显示警告。 但是，如果使用更敏感的警告级别(例如，`/W2`)进行编译，则会报告此警告。 警告级别的这种更改只是暂时的，因为最后一行将警告级别重置回其默认值(即 1)。 在这种情况下，警告级别会增加，这意味着您只能在编译器上看到更敏感的警告级别。 您还可以降低警告级别，这意味着报告警告的可能性更大。 您甚至可以将警告级别更改为`error`，这样当代码中存在此类型的警告时，代码将不会编译。\n\n# 添加信息性消息\n\n在测试和调试代码时，您将不可避免地遇到一些地方，在这些地方您可以看到潜在的问题，但与您正在处理的内容相比，这些问题的优先级较低。 重要的是要记下这个问题，以便您可以在以后的阶段解决该问题。 在 Visual C++ 中，有两种方法可以实现这一点，一种是良性的，另一种是会产生错误的。\n\n第一种方式是添加`TODO:`注释，如下图所示：\n\n```cpp\n    // TODO: potential data loss, review use of shift8 function \n    unsigned shift8(unsigned char c)  \n    { \n        return c >> 8;  \n    }\n```\n\nVisual Studio 编辑器有一个名为**任务列表**的工具窗口。 这将列出项目中以某个预定任务开始的注释(缺省值为`TODO`、`HACK`和`UNDONE`)。\n\n如果任务列表窗口不可见，请通过视图菜单将其启用。 Visual Studio 2015 中的默认设置是启用 C++ 中的任务。 早期版本不是这种情况，但可以通过工具菜单、选项对话框，然后通过文本编辑器、C/C++、格式、查看方式将枚举注释任务设置为是来启用它。 任务标签列表可以在选项对话框中的环境、任务列表项下找到。\n\n任务列表列出了带有文件和行号的任务，您可以通过双击条目打开文件并找到注释。\n\n识别需要注意的代码的第二种方法是`message`杂注。 顾名思义，这只允许您在代码中放置一条信息性消息。 当编译器遇到这个杂注时，它只是将消息放在输出流上。 请考虑以下代码：\n\n```cpp\n    #pragma message(\"review use of shift8 function\") \n    unsigned shift8(unsigned char c)  \n    { \n        return c >> 8;  \n    }\n```\n\n如果使用此代码和`/W1`(默认)警告级别编译`test.cpp`命令文件，则输出将如下所示：\n\n```cpp\n Microsoft (R) C/C++ Optimizing Compiler Version 19.00.24215.1 for x86\nCopyright (C) Microsoft Corporation.  All rights reserved.\n\ntest.cpp\nreview the use of shift8 function\ntest.cpp(8): warning C4333: '>>': right shift by too large amount, data loss\n```\n\n正如您所看到的，字符串就像编译器看到的那样打印出来，与警告消息形成对比的是，没有文件或行号的指示。 有一些方法可以使用编译器符号来解决这个问题。\n\n如果条件很重要，则需要发出一个错误，其中一种方法是使用`#error`指令。 当编译器达到此指令时，它将发出一个错误。 这是一个严重的操作，因此只有在有其他选择时才会使用它。 您很可能希望将其与条件编译一起使用。 通常用于只能用 C++ 编译器编译的代码：\n\n```cpp\n    #ifndef __cplusplus \n    #error C++ compiler required. \n    #endif\n```\n\n如果使用`/Tc`开关编译包含此代码的文件，将代码编译为 C，则不会定义`__cplusplus`预处理器符号，并将生成错误。\n\nC++ 11 添加了一个名为`static_assert`的新指令。 它的调用方式类似于函数(*调用*以分号结尾)，但它不是函数，因为它只在编译时使用。 此外，该指令可以在不使用函数调用的地方使用。 该指令有两个参数：表达式和字符串文字。 如果表达式为`false`，则字符串文字将在编译时与源文件和行号一起输出，并将生成错误。 在最简单的级别上，您可以使用下面的代码来发出一条消息：\n\n```cpp\n    #ifndef __cplusplus \n    static_assert(false, \"Compile with /TP\"); \n    #endif \n    #include <iostream> // needs the C++ compiler\n```\n\n由于第一个参数是`false`，指令将在编译期间发出错误消息。 同样的事情也可以通过`#error`指令来实现。 `<type_traits>`库有各种用于测试类型属性的谓词。 例如，`is_class`模板类有一个简单的模板参数，该参数是一种类型，如果类型是`class`，则将`static`成员`value`设置为`true`。 如果您有一个只应为类实例化的模板化函数，则可以添加以下内容`static_assert`：\n\n```cpp\n    #include <type_traits> \n\n    template <class T> \n    void func(T& value) \n    { \n        static_assert(std::is_class<T>::value, \"T must be a class\"); \n        // other code \n    }\n```\n\n在编译时，编译器将尝试实例化函数，并使用`value`在该类型上实例化`is_class`，以确定编译是否应该继续。 例如，以下代码：\n\n```cpp\n    func(string(\"hello\")); \n    func(\"hello\");\n```\n\n第一行将正确编译，因为编译器将实例化一个函数`func<string>,`，而参数是`class`。 但是，第二行不会编译，因为实例化的函数是`func<const char*>`，而`const char*`不是`class`。 输出为：\n\n```cpp\nMicrosoft (R) C/C++ Optimizing Compiler Version 19.00.24215.1 for x86\nCopyright (C) Microsoft Corporation.  All rights reserved.\n\ntest.cpp\ntest.cpp(25): error C2338: T must be a class\ntest.cpp(39): note: see reference to function template instantiation \n\n'void func<const char*>(T)' being compiled\nwith\n[\n T=const char *\n]\n```\n\n`static_assert`在*行 25*上，因此会产生`T must be a class`的错误。 *第 39 行*是对`func<const char*>`的第一次调用，并给出了错误的上下文。\n\n# 用于调试的编译器开关\n\n要允许您使用调试器单步执行程序，您必须提供信息以允许调试器将机器码与源代码相关联。 这至少意味着关闭所有优化，因为在尝试优化代码时，C++ 编译器将重新排列代码。 优化在默认情况下是关闭的(因此使用`/Od`开关是多余的)，但显然，为了能够调试进程和单步执行 C++ 代码，您需要删除所有的`/O`优化开关。\n\n由于 C++ 标准库使用 C 运行库，因此您需要编译代码才能使用后者的调试版本。 您使用的开关取决于您是在构建进程还是**动态链接库**(**DLL**)，以及是静态链接 C 运行库还是通过 DLL 访问它。 如果您正在编译一个进程，您可以使用`/MDd`在 DLL 中获取 C 运行时的调试版本，如果您使用`/MTd`，您将获得静态链接的 C 运行时的调试版本。 如果您正在编写动态链接库，则除了 C 运行时开关之一(`/MTd`是默认开关)之外，还必须使用`/LDd`。 这些开关将定义一个称为`_DEBUG`的预处理器符号。\n\n调试器需要知道调试器符号信息--变量的名称和类型，以及与代码相关的函数名称和行号。 可以接受的方法是通过一个名为**程序数据库**的文件，扩展名为`pdb`。 您可以使用其中一个`/Z`开关生成`pdb`文件：`/Zi`或`/ZI`开关将创建两个文件，一个文件的名称以`VC`(例如`VC140.pdb`)开头，包含所有`obj`文件的调试信息；另一个文件的名称为包含进程调试的项目名称。 如果编译时没有链接(`/c`)，则只创建第一个文件。 默认情况下，Visual C++ 项目向导将使用`/Od /MDd /ZI`进行调试生成。 `/ZI`开关表示程序数据库的创建格式允许 Visual C++ 调试器执行`Edit`和`Continue`，也就是说，您可以更改一些代码并继续单步执行代码，而无需重新编译。 当您为发布版本进行编译时，向导将使用`/O2 /MD /Zi`开关，这意味着代码已针对速度进行了优化，但仍将创建程序数据库(没有`Edit`和`Continue`支持)。 代码不需要程序数据库来运行(实际上，您不应该将其与代码一起分发)，但如果您有崩溃报告并且需要在调试器下运行发布构建代码，则它很有用。\n\n这些`/Z`编译器开关假定链接器与`/debug`开关一起运行(如果编译器调用链接器，它将通过此开关)。 链接器将根据`VC`程序数据库文件中的调试信息创建项目程序数据库。\n\n这就提出了为什么发布构建文件需要程序数据库的问题。 如果在调试器下运行程序并查看调用堆栈，您通常会在操作系统文件中看到一长串堆栈帧。 这些名称通常是由 DLL 名称和一些数字和字符组成的相当无意义的名称。 可以安装 Windows 的符号(`pdb`文件)，如果没有安装，则指示 Visual C++ 调试器从网络上称为**符号服务器**的计算机下载正在使用的库的符号。 这些符号不是库的源代码，但它们确实为您提供了函数名称和参数类型，这为您提供了有关单步执行时调用堆栈状态的附加信息。\n\n# 预处理器符号\n\n要访问代码中的跟踪、断言和报告功能，必须启用调试运行时库，这可以通过使用`/MDd`、`/MTd`或`/LDd`编译器开关来完成，这些开关将定义`_DEBUG`预处理器符号。 `_DEBUG`预处理器符号启用了很多功能，相反，不定义该符号将有助于优化代码。\n\n```cpp\n    #ifdef _DEBUG \n       cout << \"debug build\" << endl; \n    #else \n       cout << \"release built\" << endl; \n    #endif\n```\n\nC++ 编译器还将通过一些标准的预处理器符号提供信息。 其中大多数只对库编写者有用，但也有一些您可能想要使用。\n\nANSI 标准规定，当编译器将代码编译为 C++(而不是 C)时，应定义`__cplusplus`符号，并指定`__FILE__`符号应包含文件名，而`__LINE__`符号将包含您访问它的点的行号。 `__func__`符号将具有当前函数名称。 这意味着您可以创建如下跟踪代码：\n\n```cpp\n    #ifdef _DEBUG \n    #define TRACE cout << __func__ << \" (\" << __LINE__ << \")\" << endl; \n    #else \n    #define TRACE \n    #endif\n```\n\n如果此代码是为调试而编译的(例如，`/MTd`)，则每当使用`TRACE`时，`cout`行将被内联；如果代码不是为调试而编译的，则`TRACE`将不执行任何操作。 `__func__`符号只是函数名，它没有限定，所以如果您在类方法中使用它，它将不会提供有关类的信息。\n\nVisual C++ 还定义了特定于 Microsoft 的符号。 `__FUNCSIG__`符号提供完整的签名，包括类名(和任何`namespace`名称)、返回类型和参数。 如果您只需要完全限定名称，则可以使用`__FUNCTION__`符号。 您将在 Windows 头文件中经常看到的一个符号是`_MSC_VER`。 它的编号是当前 C++ 编译器的版本，它与条件编译一起使用，因此新的语言功能只能用支持它们的编译器编译。\n\nVisual C++ 项目页面定义了名为`$(ProjectDir)`和`$(Configuration)`的*生成宏*。 它们仅由 MSBuild 工具使用，因此它们在编译期间不会自动出现在源文件中，但是，如果将预处理器符号设置为生成宏的值，则可以在编译时通过该符号获得该值。 系统环境变量也可以作为构建宏使用，因此可以使用它们来影响构建。 例如，在 Windows 上，系统环境变量`USERNAME`具有当前登录用户的名称，因此您可以使用它设置一个符号，然后在编译时访问该符号。\n\n在 Visual C++ 项目页中，您可以在名为的 C/C++ 预处理器项目页上添加**预处理器定义**：\n\n```cpp\n    DEVELOPER=\"$(USERNAME)\"\n```\n\n然后，可以在代码中使用以下符号添加一行：\n\n```cpp\n    cout << \"Compiled by \" << DEVELOPER << endl;\n```\n\n如果您使用的是 make 文件，或者只是从命令行调用`cl`，则可以添加一个开关来定义符号，如下所示：\n\n```cpp\n    /DDEVELOPER=\"$(USERNAME)\"\n```\n\n在这里转义双引号很重要，因为如果没有双引号，编译器就会吃掉双引号。\n\n前面，您看到了如何使用`#pragma message`和`#error`指令将消息放入编译器的输出流中。 在 Visual Studio 中编译代码时，编译器和链接器输出将出现在“输出”窗口中。 如果消息的格式为：\n\n```cpp\n    path_to_source_file(line) message\n```\n\n其中`path_to_source_file`是文件的完整路径，`line`是出现`message`的行号。 然后，当您在输出窗口中双击此行时，文件将被加载(如果尚未加载)，并将插入点放置在该行上。\n\n`__FILE__`和`__LINE__`符号为您提供了使`#pragma message`和`#error`指令更加有用所需的信息。 输出`__FILE__`很简单，因为它是一个字符串，并且 C++ 将连接字符串文字：\n\n```cpp\n    #define AT_FILE(msg) __FILE__ \" \" msg \n\n    #pragma message(AT_FILE(\"this is a message\"))\n```\n\n宏作为编译指示的一部分被调用，以正确格式化消息；但是，您不能从宏调用编译指示，因为`#`有特殊用途(稍后会用到)。 此代码的结果如下所示：\n\n```cpp\n    c:\\Beginning_C++ Chapter_10test.cpp this is a message\n```\n\n通过宏输出`__LINE__`需要更多的工作，因为它包含一个数字。 这个问题在 C 中很常见，所以有一个使用两个宏和字符串运算符`#`的标准解决方案。\n\n```cpp\n    #define STRING2(x) #x \n    #define STRING(x) STRING2(x) \n    #define AT_FILE(msg) __FILE__ \"(\" STRING(__LINE__) \") \" msg\n```\n\n`STRING`宏用来将`__LINE__`符号扩展为一个数字，而`STRING2`宏用来将数字串化。 `AT_FILE`宏用正确的格式设置整个字符串的格式。\n\n# 生成诊断消息\n\n诊断消息的有效使用是一个广泛的主题，因此本节将只向您介绍基本知识。 在设计代码时，应该使编写诊断消息变得容易，例如，提供转储对象内容的机制，并提供对测试类不变式以及前置条件和后置条件的代码的访问。 您还应该分析代码以确保记录适当的消息。 例如，在循环中发出诊断消息通常会填满日志文件，从而使读取日志文件中的其他消息变得困难。 但是，循环中的某项操作持续失败这一事实本身可能是一个重要的诊断信息，尝试执行失败操作的次数也可能是一个重要的诊断信息，因此您可能想要记录下来。\n\n对诊断消息使用`cout`具有将这些消息与您的用户输出集成在一起的优势，这样您就可以看到中间结果的最终效果。 缺点是诊断消息与用户输出集成在一起，由于通常有大量的诊断消息，这些消息将完全淹没程序的用户输出。\n\nC++ 有两个流对象，您可以使用它们来代替`cout`。 `clog`和`cerr`流对象会将字符数据写入标准错误流(C 流指针`stderr`)，这通常会在控制台上显示为您正在使用`cout`(输出到标准输出流，即 C 流指针`stdout`)，但您可以将其重定向到其他地方。 `clog`和`cerr`之间的区别在于`clog`使用缓冲输出，这可能比未缓冲的`cerr`性能更好。 但是，如果应用在未刷新缓冲区的情况下意外停止，则可能会丢失数据。\n\n由于`clog`和`cerr`流对象在发布版本和调试版本中都可用，因此您应该只将它们用于您希望最终用户看到的消息。 这使得它们不适合用于跟踪消息(稍后将介绍)。 相反，您应该将它们用于用户将能够处理的诊断消息(可能是找不到文件，或者进程没有执行操作的安全访问权限)。\n\n```cpp\n    ofstream file; \n    if (!file.open(argv[1], ios::out)) \n    { \n        clog << \"cannot open \" << argv[1] << endl; \n        return 1; \n    }\n```\n\n此代码分两步打开文件(而不是使用构造函数)，如果文件无法打开，`open`方法将返回`false`。 代码检查打开文件是否成功，如果失败，它将通过`clog`对象告诉用户，然后从包含该代码的任何函数返回，因为`file`对象现在是无效的，不能使用。 缓冲了`clog`对象，但在本例中，我们希望立即通知用户，这是由`endl`操纵器执行的，它在流中插入换行符，然后刷新流。\n\n默认情况下，`clog`和`cerr`流对象将输出到标准错误流，这意味着对于控制台应用，您可以通过重定向这些流来分离输出流和错误流。 在命令行上，可以使用`stdin`的值为 0、`stdout,`的值为 1、`stderr`的值为 2 和重定向操作符`>`来重定向标准流。 例如，应用`app.exe`可以在`main`函数中包含以下代码：\n\n```cpp\n    clog << \"clog\" << endl; \n    cerr << \"cerrn\"; \n    cout << \"cout\" << endl;\n```\n\n`cerr`对象没有缓冲，所以换行符是使用`n`还是`endl`都无关紧要。 当您在命令行上运行此命令时，您将看到类似以下内容：\n\n```cpp\nC:\\Beginning_C++ \\Chapter_10>app\nclog\ncerr\ncout\n```\n\n要将流重定向到文件，请将流句柄(1 表示`stdout`，2 表示`stderr`)重定向到该文件；控制台将打开该文件并将该流写入该文件：\n\n```cpp\nC:\\Beginning_C++ \\Chapter_10>app 2>log.txt\ncout\n\nC:\\Beginning_C++ \\Chapter_10>type log.txt\nclog\ncerr\n```\n\n正如上一章所展示的，C++ 流对象是分层的，因此将数据插入到流中的调用将根据流的类型将数据写入底层流对象，无论是否使用缓冲。 此流缓冲区对象是使用`rdbuf`方法获取和替换的。 如果希望应用将`clog`对象重定向到文件，可以编写如下代码：\n\n```cpp\n    extern void run_code(); \n\n    int main() \n    { \n        ofstream log_file; \n        if (log_file.open(\"log.txt\")) clog.rdbuf(log_file.rdbuf()); \n\n        run_code(); \n\n        clog.flush(); \n        log_file.close(); \n        clog.rdbuf(nullptr); \n        return 0; \n    }\n```\n\n在这段代码中，应用代码将在`run_code`函数中，其余代码设置`clog`对象以重定向到文件。\n\n请注意，当`run_code`函数返回(应用已完成)时，文件将显式关闭；这并不完全是因为`ofstream`析构函数将关闭文件，在本例中，这将在`main`函数返回时发生。 最后一行很重要。 标准流对象是在调用`main`函数之前创建的，它们将在`main`函数返回之后的某个时间被销毁，也就是说，在文件对象被销毁之后很久。 为了防止`clog`对象访问被销毁的文件对象，调用`rdbuf`方法传递`nullptr`，以指示没有缓冲区。\n\n# 使用 C 运行时跟踪消息\n\n通常，您会希望通过实时运行应用来测试代码，并输出*跟踪消息*来测试算法是否工作。 有时，您可能希望测试调用函数的顺序(例如，正确的分支出现在`switch`语句或`if`语句中)，而在其他情况下，您可能希望测试中间值，以查看输入数据是否正确以及对该数据的计算是否正确。\n\n跟踪消息可能会生成大量数据，因此将这些数据发送到控制台是不明智的。 非常重要的一点是，跟踪消息仅在调试版本中生成。 如果在产品代码中留下跟踪消息，可能会严重影响应用的性能(稍后将对此进行解释)。 此外，跟踪消息不太可能本地化，也不会检查它们是否包含可用于对算法进行反向工程的信息。 发布版本中跟踪消息的最后一个问题是，您的客户会认为您向他们提供的是未经完全测试的代码。 因此，重要的是，只有在定义了`_DEBUG`符号时，才会在调试版本中生成跟踪消息。\n\nC 运行时提供了一系列名称以`_RPT`开头的宏，当定义了`_DEBUG`时，这些宏可用于跟踪消息。 这些宏有`char`和宽字符版本，有些版本只报告跟踪消息，其他版本报告消息和消息的位置(源文件和行号)。 最终，这些宏将调用名为`_CrtDbgReport`的函数，该函数将使用在别处确定的设置生成消息。\n\n`_RPTn`宏(其中`n`是`0`、`1`、`2`、`3`、`4`或`5`)将接受一个格式字符串和 0 到 5 个参数，这些参数将在报告之前放入字符串中。 宏的第一个参数指示要报告的消息类型：`_CRT_WARN`、`_CRT_ERROR`或`_CRT_ASSERT`。 这两个类别中的最后两个是相同的，它们指的是断言，这将在后面的小节中介绍。 报告宏的第二个参数是格式字符串，其后将跟随所需数量的参数。 `_RPTFn`宏格式相同，但将报告源文件和行号以及格式化消息。\n\n默认操作是`_CRT_WARN`消息不会产生任何输出，`_CRT_ERROR`和`_CRT_ASSERT`消息将生成一个弹出窗口，允许您中止或调试应用。 您可以通过调用`_CrtSetReportMode`函数并提供类别和指示要采取的操作的值来更改对任何这些消息类别的响应。 如果使用`_CRTDBG_MODE_DEBUG`，则消息将写入调试器输出窗口。 如果使用`_CRTDBG_MODE_FILE`，则消息将写入一个文件，您可以打开该文件并将句柄传递给`_CrtSetReportFile`函数。 (您还可以使用`_CRTDBG_FILE_STDERR`或`_CRTDBG_FILE_STDOUT`作为文件句柄，将消息发送到标准输出或错误输出。)。 如果使用`_CRTDBG_MODE_WNDW`作为报告模式，则将使用中止/重试/忽略对话框显示该消息。 由于这将暂停当前执行线程，因此它应该仅用于断言消息(默认操作)：\n\n```cpp\n    include <crtdbg.h> \n\n    extern void run_code(); \n\n    int main() \n    { \n        _CrtSetReportMode(_CRT_WARN, _CRTDBG_MODE_DEBUG); \n        _RPTF0(_CRT_WARN, \"Application startedn\"); \n\n        run_code(); \n\n        _RPTF0(_CRT_WARN, \"Application endedn\"); \n        return 0; \n    }\n```\n\n如果您没有在消息中提供`n`，那么下一条消息将被附加到您的消息的末尾，在大多数情况下，这不是您想要的(尽管您可以证明对`_RPTn`宏的一系列调用是合理的，其中最后一条以`n`结束)。\n\nVisual Studio Output 窗口在编译项目时显示(要在调试时显示它，请选择 View 菜单中的 Show Output 选项)，顶部是一个标记为 Show Output From 的组合框，通常设置为 Build。 如果将其设置为 Debug，那么您将看到调试会话期间生成的调试消息。 其中包括有关加载调试符号的消息，以及从`_RPTn`宏重定向到“输出”窗口的消息。\n\n如果您希望将消息定向到文件，则需要使用 Win32`CreateFile`函数打开该文件，并在调用`_CrtSetReportFile`函数时使用该函数中的句柄。 为此，您需要包括 Windows 头文件：\n\n```cpp\n    #define WIN32_LEAN_AND_MEAN \n    #include <Windows.h> \n    #include <crtdbg.h>\n```\n\n`WIN32_LEAN_AND_MEAN`宏将减小包含的 Windows 文件的大小。\n\n```cpp\n    HANDLE file =  \n       CreateFileA(\"log.txt\", GENERIC_WRITE, 0, 0, CREATE_ALWAYS, 0, 0); \n    _CrtSetReportMode(_CRT_WARN, _CRTDBG_MODE_FILE); \n    _CrtSetReportFile(_CRT_WARN, file); \n    _RPTF0(_CRT_WARN, \"Application startedn\"); \n\n    run_code(); \n\n    _RPTF0(_CRT_WARN, \"Application endedn\"); \n    CloseHandle(file);\n```\n\n此代码将把警告消息定向到文本文件`log.txt`，每次运行应用时都会创建新的文本文件`log.txt`。\n\n# 使用 Windows 跟踪邮件\n\n函数的作用是：向调试器发送消息。 该函数通过名为`DBWIN_BUFFER`的*共享内存节*来完成此操作。 共享内存意味着任何进程都可以访问该内存，因此 Windows 提供了两个名为`DBWIN_BUFFER_READY`和`DBWIN_DATA_READY`的*事件对象*来控制对该内存的读写访问。 这些事件对象在进程之间共享，可以处于有信号或无信号状态。 调试器将通过发信号通知`DBWIN_BUFFER_READY`事件来指示它不再使用共享内存，此时`OutputDebugString`函数可以将数据写入共享内存。 调试器将等待`DBWIN_DATA_READY`事件，当`OutputDebugString`函数完成对存储器的写入并且可以安全地读取缓冲区时，该事件将由`OutputDebugString`函数发出信号。 写入内存节的数据将是调用`OutputDebugString`函数的进程的进程 ID，后跟最多 4KB 的数据字符串。\n\n问题是，当您调用`OutputDebugString`函数时，它将等待`DBWIN_BUFFER_READY`事件，这意味着当您使用此函数时，您会将应用的性能耦合到另一个进程的性能，该进程通常是调试器(但可能不是)。 编写一个进程来访问`DBWIN_BUFFER`共享内存节并访问相关的事件对象非常容易，因此您的生产代码可能会在运行此类应用的计算机上运行。 因此，使用条件编译非常重要，这样`OutputDebugString`函数只在调试版本中使用--这些代码永远不会发布给您的客户：\n\n```cpp\n    extern void run_code(); \n\n    int main() \n    { \n        #ifdef _DEBUG \n            OutputDebugStringA(\"Application startedn\"); \n        #endif \n\n        run_code(); \n\n        #ifdef _DEBUG \n           OutputDebugStringA(\"Application endedn\"); \n        #endif \n        return 0; \n    }\n```\n\n您需要包括`windows.h`头文件来编译此代码。 对于`_RPT`示例，您必须在调试器下运行此代码才能查看输出，或者运行像**DebugView**这样的应用(可从 Microsoft 的 TechNet 网站获得)。\n\nWindows 提供了`DBWinMutex`互斥对象来充当访问此共享内存和事件对象的总*键*。 顾名思义，当您拥有互斥体的句柄时，您将拥有对资源的互斥访问权限。 问题在于，进程不必拥有该互斥锁的句柄就可以使用这些资源，因此，如果您的应用认为它拥有独占访问权，那么您无法保证它是否真的拥有独占访问权。\n\n# 使用断言\n\n断言检查条件是否为真。 断言的意思就是：如果条件不为真，程序就不应该继续。 显然，发布代码中不应该调用断言，因此必须使用条件编译。 断言应该用来检查不应该发生的情况：永远不会发生事件。 由于这些条件不会发生，因此在发布版本中应该不需要断言。\n\nC 运行时提供可通过`<cassert>`头文件使用的`assert`宏。 除非定义了`NDEBUG`符号，否则宏以及作为其唯一参数传递的表达式中调用的任何函数都将被调用。 也就是说，您不必定义`_DEBUG`符号来使用断言，并且您应该采取额外的操作来显式阻止调用`assert`。\n\n这一点值得再重复一遍。 即使没有定义`_DEBUG`，也会定义`assert`宏，因此可以在发布代码中调用断言。 为了防止这种情况发生，您必须在发布版本中定义`NDEBUG`符号。 相反，您可以在调试版本中定义`NDEBUG`符号，以便可以使用跟踪，但不必使用断言。\n\n通常，您将在调试版本中使用断言来检查函数中是否满足前置条件和后置条件，以及是否满足类不变条件。 例如，您可能有一个二进制缓冲区，它在第十个字节位置有一个特定值，因此编写了一个函数来提取该字节：\n\n```cpp\n    const int MAGIC=9; \n\n    char get_data(char *p, size_t size) \n    { \n        assert((p != nullptr)); \n        assert((size >= MAGIC)); \n        return p[MAGIC]; \n    }\n```\n\n在这里，对`assert`的调用用于检查指针是否不是`nullptr`以及缓冲区是否足够大。 如果这些断言为真，则意味着通过指针访问第十个字节是安全的。\n\n虽然这在此代码中并不是严格必需的，但断言表达式放在圆括号中。 养成这样做的习惯是很好的，因为`assert`是宏，因此表达式中的逗号将被视为宏参数分隔符；圆括号不受此影响。\n\n由于默认情况下将在发布版本中定义`assert`宏，因此您必须通过在编译器命令行的 make 文件中定义`NDEBUG`来禁用它们，或者您可能希望显式使用条件编译：\n\n```cpp\n    #ifndef _DEBUG \n    #define NDEBUG \n    #endif\n```\n\n如果调用 Assert 但失败，则控制台会打印一条 Assert 消息以及源文件和行号信息，然后通过调用`abort`终止该进程。 如果该过程是使用发布版本标准库构建的，则过程`abort`很简单，但是，如果使用调试版本，则用户将看到标准的中止/重试/忽略消息框，其中的中止和忽略选项中止该过程。 重试选项将使用**实时**(**JIT**)调试将已注册的调试器附加到进程。\n\n相反，只有在定义了`_DEBUG`时才定义`_ASSERT`和`_ASSERTE`宏，因此这些宏在发布版本中将不可用。 这两个宏都接受一个表达式，并在表达式为`false`时生成一条断言消息。 `_ASSERT`宏的消息将包括源文件和行号，以及声明断言失败的消息。 `_ASSERTE`宏的消息类似，但包含失败的表达式。\n\n```cpp\n    _CrtSetReportMode(_CRT_ASSERT, _CRTDBG_MODE_FILE); \n    _CrtSetReportFile(_CRT_ASSERT, _CRTDBG_FILE_STDOUT); \n\n    int i = 99; \n    _ASSERTE((i > 100));\n```\n\n此代码设置报告模式，以便失败的断言将是在控制台上打印的消息(而不是默认的中止/重试/忽略对话框)。 由于变量明显小于 100，断言将失败，因此进程将终止，控制台上将打印以下消息：\n\n```cpp\n    test.cpp(23) : Assertion failed: (i > 100)\n```\n\n中止/重试/忽略对话框为测试应用的人员提供了将调试器附加到进程的选项。 如果您认为断言的失败是令人发指的，则可以通过调用`_CrtDbgBreak`强制调试器附加到进程。\n\n```cpp\n    int i = 99; \n    if (i <= 100) _CrtDbgBreak();\n```\n\n您不需要使用条件编译，因为在发布版本中，`_CrtDbgBreak`函数是无操作的。 在调试版本中，此代码将触发 JIT 调试，这为您提供了关闭应用或启动调试器的选项，如果选择后者，则将启动已注册的 JIT 调试器。\n\n# 应用终止\n\n`main`函数是应用的入口点。 但是，操作系统不会直接调用它，因为 C++ 将在调用`main`之前执行初始化。 这包括构造标准库全局对象(`cin`、`cout`、`cerr`、`clog,`和宽字符版本)，并且为支撑 C++ 库的 C 运行时库执行了一整套初始化。 此外，还有您的代码创建的全局和静态对象。 当`main`函数返回时，必须调用全局和静态对象的析构函数，并在 C 运行时执行清理。\n\n有几种方法可以故意停止进程。 最简单的方法是从`main`函数返回，但这假设有一条简单的路径返回到`main`函数，从您的代码想要完成该过程的那一点开始。 当然，进程终止必须是有序的，您应该避免在代码中的任何位置正常停止进程的地方编写代码。 但是，如果您遇到数据已损坏且无法恢复的情况，并且任何其他操作可能会损坏更多数据，则您可能别无选择，只能终止应用。\n\n`<cstdlib>`头文件提供对头文件的访问，以及允许您终止和处理应用终止的函数。 当 C++ 程序正常关闭时，C++ 基础结构将调用在`main`函数中创建的对象的析构函数(与其构造顺序相反)和`static`对象的析构函数(可能是在`main`函数以外的函数中创建的)。 `atexit`函数允许您注册在`main`函数完成并且调用了`static`对象析构函数之后调用的函数(没有参数和返回值)。 您可以通过多次调用此函数来注册多个函数，在终止时，这些函数的调用顺序将与它们的注册顺序相反。 调用用`atexit`函数注册的函数后，将调用任何全局对象的析构函数。\n\n还有一个名为`_onexit`的 Microsoft 函数，它还允许您注册要在正常终止期间调用的函数。\n\n`exit`和`_exit`函数执行进程的正常退出，即，它们在关闭进程之前清理 C 运行时并刷新所有打开的文件。 `exit`函数通过调用任何已注册的终止函数来执行额外的工作；`_exit`函数不调用这些终止函数，快速退出也是如此。 这些函数不会调用临时或自动对象的析构函数，因此如果使用堆栈对象来管理资源，则必须在调用`exit`之前显式调用析构函数代码。 但是，将调用静态和全局对象的析构函数。\n\n`quick_exit`函数导致正常关机，但它不调用任何析构函数，也不刷新任何流，因此不会进行资源清理。 向`atexit`注册的函数不会被调用，但您可以通过向`at_quick_exit`函数注册它们来注册调用终止函数。 在调用这些终止函数之后，`quick_exit`函数调用关闭进程的`_Exit`函数。\n\n您还可以调用`terminate`函数来关闭进程，而不进行清理。 此过程将调用已向`set_terminate`函数注册的函数，然后调用`abort`函数。 如果程序中发生异常而未被捕获--并因此传播到`main`函数--C++ 基础结构将调用`terminate`函数。 `abort`函数是终止进程的最严格的机制。 此函数将退出进程，而不调用对象的析构函数或执行任何其他清理。 该函数引发`SIGABORT`信号，因此可以使用`signal`函数注册函数，该函数将在进程终止之前被调用。\n\n# 误差值\n\n有些函数旨在执行某个操作并根据该操作返回一个值，例如，`sqrt`将返回一个数字的平方根。 其他函数执行更复杂的操作，并使用返回值指示函数是否成功。 此类错误值没有通用约定，因此如果函数返回一个简单整数，则不能保证一个库使用的值与从另一个库中的函数返回的值具有相同的含义。 这意味着您必须仔细检查您使用的任何库代码的文档。\n\nWindows 确实提供了公共错误值，可以在`winerror.h`头文件中找到，Windows**软件开发工具包**(**SDK**)中的函数仅返回此文件中的值。 如果您编写的库代码将仅在 Windows 应用中使用，请考虑使用此文件中的错误值，因为您可以使用 Win32`FormatMessage`函数来获取错误描述，如下节所述。\n\nC 运行时库提供了一个名为`errno`的全局变量(实际上，它是一个宏，您可以将其视为变量)。 C 函数将返回一个值来指示它们已失败，您可以访问`errno`值来确定错误是什么。 `<errno.h>`头文件定义标准 POSIX 错误值。 `errno`变量不表示成功，它只表示错误，所以您应该只在函数指示有错误时才访问它。 `strerror`函数将返回一个 C 字符串，其中包含您作为参数传递的错误值的描述；这些消息根据通过调用`setlocale`函数设置的当前 C 语言环境进行本地化。\n\n# 获取消息描述\n\n要在运行时获取 Win32 错误代码的描述，请使用 Win32`FormatMessage`函数。 这将获得系统消息或自定义消息的描述(在下一节中介绍)。 如果要使用自定义消息，则必须加载绑定了消息资源的可执行文件(或 DLL)，并将`HMODULE`句柄传递给`FormatMessage`函数。 如果您想要获取系统消息的描述，则不需要加载模块，因为 Windows 将为您执行此操作。 例如，如果调用 Win32`CreateFile`函数打开一个文件，但找不到该文件，则该函数将返回值`INVALID_HANDLE_VALUE,`，指示存在错误。 要获取错误的详细信息，可以调用`GetLastError`函数(它返回一个 32 位无符号值，有时称为`DWORD`或`HRESULT`)。 然后可以将错误值传递给`FormatMessage`：\n\n```cpp\n    HANDLE file = CreateFileA( \n        \"does_not_exist\", GENERIC_READ, 0, 0, OPEN_EXISTING, 0, 0); \n    if (INVALID_HANDLE_VALUE == file) \n    { \n        DWORD err = GetLastError(); \n        char *str; \n        DWORD ret = FormatMessageA( \n            FORMAT_MESSAGE_FROM_SYSTEM|FORMAT_MESSAGE_ALLOCATE_BUFFER, \n            0, err, LANG_USER_DEFAULT, reinterpret_cast<LPSTR>(&str),  \n            0, 0); \n        cout << \"Error: \"<< str << endl; \n        LocalFree(str); \n    } \n    else \n    { \n        CloseHandle(file); \n    }\n```\n\n此代码尝试打开一个不存在的文件，并获取与故障相关的错误值(该值将为`ERROR_FILE_NOT_FOUND`)。 然后，代码调用`FormatMessage`函数来获取描述错误的字符串。 函数的第一个参数是指示函数应该如何工作的标志；在本例中，`FORMAT_MESSAGE_FROM_SYSTEM`标志表示错误是系统错误，`FORMAT_MESSAGE_ALLOCATE_BUFFER`标志表示函数应该分配足够大的缓冲区来使用 Win32`LocalAlloc`函数保存字符串。\n\nIf the error is a custom value that you have defined then you should use the `FORMAT_MESSAGE_FROM_HMODULE` flag, open the file with `LoadLibrary` and use the resulting `HMODULE` as the parameter passed in through the second parameter.\n\n第三个参数是错误消息编号(来自`GetLastError`)，第四个参数是指示要使用的语言 ID 的`LANGID`(在本例中为`LANG_USER_DEFAULT`，以获取当前登录用户的语言 ID)。 `FormatMessage`函数将为错误值生成格式化的，该字符串可能有替换参数。 格式化的字符串在缓冲区中返回，您有两种选择：您可以分配一个字符缓冲区并将指针作为第五个参数传入，将长度作为第六个参数传入，或者您可以使用本例中的`LocalAlloc`函数请求函数分配缓冲区。 要访问函数分配的缓冲区，可以通过第五个参数传递指针变量的*地址*。\n\n请注意，第五个参数用于获取指向用户分配的缓冲区的指针，或者返回系统分配的缓冲区的地址，这就是在本例中必须强制转换指向指针的指针的原因。\n\n某些格式字符串可能有参数，如果有，这些值将通过第七个参数中的数组传入(在本例中，不传递任何数组)。 前面代码的结果是字符串：\n\n```cpp\n    Error: The system cannot find the file specified.\n```\n\n使用消息编译器、资源文件和`FormatMessage`，您可以提供一种机制来从函数返回错误值，然后根据当前区域设置将这些值转换为本地化字符串。\n\n# 使用消息编译器\n\n上一个示例显示，您可以获取 Win32 错误的本地化字符串，但也可以创建自己的错误并提供作为资源绑定到进程或库的本地化字符串。 如果您打算向最终用户报告错误，则必须确保描述已本地化。 Windows 提供了一个名为 Message Compiler(`mc.exe`)的工具，它将获取包含各种语言的消息条目的文本文件，并将它们编译成可以绑定到模块的二进制资源。\n\n例如：\n\n```cpp\n    LanguageNames = (British = 0x0409:MSG00409) \n    LanguageNames = (French  = 0x040c:MSG0040C) \n\n    MessageId       = 1 \n    SymbolicName    = IDS_GREETING \n    Language        = English \n    Hello \n    . \n    Language        = British \n    Good day \n    . \n    Language        = French \n    Salut \n    .\n```\n\n这为同一消息定义了三个本地化字符串。 这里的消息是简单的字符串，但是您可以使用可以在运行时提供的占位符来定义格式化消息。 *中立的*语言是美国英语，此外，我们还定义了英式英语和法语的字符串。 用于语言的名称在文件顶部的`LanguageNames`行中定义。 这些条目具有稍后将在文件中使用的名称、语言的代码页以及将包含消息资源的二进制资源的名称。\n\n`MessageId`是`FormatMessage`函数将使用的标识符，`SymbolicName`是将在头文件中定义的预处理器符号，因此您可以在 C++ 代码中使用此消息，而不是数字。 该文件通过将其传递给命令行实用程序`mc.exe`进行编译，该实用程序将创建五个文件：一个带有符号定义的头文件、三个二进制源(`MSG00001.bin`，默认情况下为中立语言创建，以及`MSG00409.bin`和`MSG0040C.bin,`，它们是由于`LanguageNames`行创建的)，以及一个资源编译器文件。 对于本例，资源编译器文件(扩展名为`.rc`)将包含：\n\n```cpp\n    LANGUAGE 0xc,0x1 \n    1 11 \"MSG0040C.bin\" \n    LANGUAGE 0x9,0x1 \n    1 11 \"MSG00001.bin\" \n    LANGUAGE 0x9,0x1 \n    1 11 \"MSG00409.bin\"\n```\n\n这是可由 Windows SDK 资源编译器(`rc.exe`)编译的标准资源文件，它会将消息资源编译为可绑定到可执行文件或 DLL 的`.res`文件。 绑定了类型为`11`的资源的进程或 DLL 可由`FormatMessage`函数用作描述性错误字符串源。\n\n通常，您不会使用消息 ID 1，因为它不太可能是唯一的，并且您可能希望利用*工具代码*和*严重性代码*(有关工具代码的详细信息，请查看`winerror.h`头文件)。 此外，要指示该消息不是 Windows，您可以在运行`mc.exe`时使用`/c`开关设置错误代码的客户位。 这意味着您的错误代码不会是像 1 这样的简单值，但这应该无关紧要，因为您的代码将使用头文件中定义的符号。\n\n# C++ 异常\n\n顾名思义，例外是针对异常情况的。 这不是正常情况。 它们不是你想要发生的条件，而是可能发生的条件。 任何异常情况通常都意味着您的数据将处于不一致的状态，因此使用异常意味着您需要从事务的角度考虑问题，也就是说，要么操作成功，要么对象的状态应该保持与尝试操作之前的状态相同。 当代码块中发生异常时，代码块中发生的所有内容都将无效。 如果代码块是更大的代码块的一部分(比方说，一个函数是由另一个函数调用的一系列函数)，则该另一个代码块中的工作将无效。 这意味着异常可能会向外传播到调用堆栈上方的其他代码块，从而使依赖于操作成功的对象无效。 在某一时刻，异常情况将是可恢复的，因此您需要防止异常进一步发展。\n\n# 例外规范\n\n异常规范在 C++ 11 中已弃用，但您可能会在早期代码中看到它们。 规范是通过应用于函数声明的`throw`表达式，给出可以从函数抛出的异常。 `throw`规范可以是省略号，这意味着函数可以抛出异常，但没有指定类型。 如果规范为空，则意味着函数不会抛出异常，这与在 C++ 11 中使用`noexcept`说明符相同。\n\n`noexcept`说明符告诉编译器不需要异常处理，因此如果函数中确实发生了异常，则异常不会从函数中冒泡出来，而会立即调用`terminate`函数。 在这种情况下，不能保证调用自动对象的析构函数。\n\n# C++ 异常语法\n\n在 C++ 中，异常情况是通过抛出异常对象来生成的。 该异常对象可以是您喜欢的任何对象：对象、指针或内置类型，但是因为异常可能由其他人编写的代码处理，所以最好对用于表示异常的对象进行标准化。 为此，标准库提供了`exception`类，它可以用作基类。\n\n```cpp\n    double reciprocal(double d) \n    { \n        if (d == 0)  \n        { \n            // throw 0; \n            // throw \"divide by zero\"; \n            // throw new exception(\"divide by zero\"); \n            throw exception(\"divide by zero\"); \n        } \n        return 1.0 / d; \n    }\n```\n\n此代码测试参数，如果参数为零，则抛出异常。 这里给出了四个示例，它们都是有效的 C++，但只有最后一个版本是可接受的，因为它使用了一个 Standard Library 类(或从 Standard Library 类派生的一个类)，并且它遵循按值抛出异常的约定。\n\n当抛出异常时，异常处理基础结构将接管。 执行将在当前代码块中停止，异常将向上传播到调用堆栈。 当异常通过代码块传播时，将销毁所有自动对象，但不会销毁在代码块中的堆上创建的对象。 这是一个称为**堆栈展开的过程，**，在异常移动到调用堆栈中它上面的堆栈帧之前，尽可能多地清理每个堆栈帧。 如果没有捕获到异常，它将向上传播到`main`函数，此时将调用`terminate`函数来处理该异常(因此它将终止该进程)。\n\n您可以保护代码以处理传播的异常。 代码受`try`块保护，并受关联的`catch`块捕获：\n\n```cpp\n    try  \n    { \n        string s(\"this is an object\"); \n        vector<int> v = { 1, 0, -1}; \n        reciprocal(v[0]); \n        reciprocal(v[1]); \n        reciprocal(v[2]); \n    } \n    catch(exception& e) \n    { \n        cout << e.what() << endl; \n    }\n```\n\n与 C++ 中的其他代码块不同，即使`try`和`catch`块包含单行代码，大括号也是必需的。 在前面的代码中，对`reciprocal`函数的第二次调用将引发异常。 异常将暂停块中任何更多代码的执行，因此不会发生对`reciprocal`函数的第三次调用。 相反，异常会传播出代码块。 `try`块是在大括号之间定义的对象的作用域，这意味着这些对象的析构函数将被调用(`s`和`v`)。 然后将控制权传递给相关的`catch`块，在本例中，只有一个处理程序。 `catch`块是一个独立于`try`块的块，因此您不能访问`try`块中定义的任何变量。 这是有意义的，因为当生成异常时，整个代码块被*污染*，因此您不能信任在该块中创建的任何对象。 此代码使用公认的约定，即通过引用捕获异常，因此捕获的是实际的异常对象，而不是副本。\n\n惯例是：抛出我的值，通过引用捕获。\n\n标准库提供了一个名为`uncaught_exception`的函数，如果抛出异常但尚未处理，该函数将返回`true`。 能够对此进行测试似乎很奇怪，因为当发生异常时，除了异常基础设施之外不会调用任何代码(例如，`catch`处理程序)，您应该将异常代码放在那里。 然而，当抛出异常时，还有*个*个代码可以调用：在堆栈清除期间销毁的自动对象的析构函数。 应在析构函数中使用`uncaught_exception`函数来确定对象是否由于异常而被销毁，而不是由于对象超出范围或被删除而导致的正常对象销毁。 例如：\n\n```cpp\n    class test \n    { \n        string str; \n    public: \n        test() : str(\"\") {} \n        test(const string& s) : str(s) {} \n        ~test() \n        { \n            cout << boolalpha << str << \" uncaught exception = \" \n             << uncaught_exception() << endl; \n        } \n    };\n```\n\n这个简单的对象指示它是否因为异常堆栈展开而被销毁。 它可以像这样测试：\n\n```cpp\n    void f(bool b) \n    { \n        test t(\"auto f\"); \n        cout << (b ? \"f throwing exception\" : \"f running fine\")  \n            << endl; \n        if (b) throw exception(\"f failed\"); \n    } \n\n    int main() \n    { \n        test t1(\"auto main\"); \n        try \n        { \n            test t2(\"in try in main\"); \n            f(false); \n            f(true); \n            cout << \"this will never be printed\"; \n        } \n        catch (exception& e) \n        { \n            cout << e.what() << endl; \n        } \n        return 0; \n    }\n```\n\n仅当使用`true`值调用`f`函数时，它才会引发异常。 `main`函数调用了两次`f`，第一次使用值`false`(因此异常不会在`f`中抛出)，第二次使用`true`。 输出为：\n\n```cpp\n f running fine\n auto f uncaught exception = false\n f throwing exception\n auto f uncaught exception = true\n in try in main uncaught exception = true\n f failed\n auto main uncaught exception = false\n```\n\n第一次调用`f`时，`test`对象被正常销毁，因此`uncaught_exception`将返回`false`。 第二次`f`被称为函数中的`test`对象在异常被捕获之前被销毁，因此`uncaught_exception`将返回`true`。 因为抛出异常，所以执行离开`try`块，因此`try`块中的`test`对象被销毁，`uncaught_exception`将返回`true`。 最后，当异常被处理并且控制在`catch`块之后返回到代码时，当`main`函数返回时，在`main`函数中的堆栈上创建的`test`对象将被销毁，因此`uncaught_exception`将返回`false`。\n\n# 标准异常类\n\n`exception`类是 C 字符串的一个简单容器：该字符串作为构造函数参数传递，并可通过`what`访问器使用。 标准库在`<exception>`库中声明 Exception 类，并鼓励您从中派生自己的 Exception 类。 标准库提供以下派生类；大多数在`<stdexcept>`中定义。\n\n| **类别** | **抛出** |\n| `bad_alloc` | 当`new`操作员无法分配内存时(在`<new>`中) |\n| `bad_array_new_length` | 当要求`new`运算符创建长度无效的数组时(在`<new>`中) |\n| `bad_cast` | 当引用类型的`dynamic_cast`失败时(在`<typeinfo>`中) |\n| `bad_exception` | 发生意外情况(在`<exception>`中) |\n| `bad_function_call` | 调用了空的`function`对象(在`<functional>`中) |\n| `bad_typeid` | 当`typeid`的参数为空时(在`<typeinfo>`中) |\n| `bad_weak_ptr` | 访问指向已销毁对象的弱指针时(在`<memory>`中) |\n| `domain_error` | 当尝试在定义操作的域外执行操作时 |\n| `invalid_argument` | 当参数使用无效值时 |\n| `length_error` | 尝试超出为对象定义的长度时 |\n| `logic_error` | 当存在逻辑错误时，例如，类不变量或前置条件 |\n| `out_of_range` | 尝试访问为对象定义的范围之外的元素时 |\n| `overflow_error` | 当计算结果的值大于目标类型时 |\n| `range_error` | 当计算结果的值超出该类型的范围时 |\n| `runtime_error` | 当错误发生在代码范围之外时 |\n| `system_error` | 包装操作系统错误的基类(在`<system_error>`中) |\n| `underflow_error` | 当计算导致下溢时 |\n\n上表中提到的所有类都有一个接受`const char*`或`const string&`参数的构造函数，而不是接受 C 字符串的`exception`类(因此，如果描述通过`string`对象传递，则使用`c_str`方法构造基类)。 没有宽字符版本，因此如果要从宽字符串构造异常描述，则必须对其进行转换。 还要注意，标准异常类只有一个构造函数参数，这可以通过继承的`what`访问器获得。\n\n关于异常可以保存的数据没有绝对规则。 您可以从`exception`派生一个类，并使用您想要为异常处理程序提供的任何值来构造它。\n\n# 按类型捕获异常\n\n每个`try`块可以有多个`catch`块，这意味着您可以根据异常类型定制异常处理。 `catch`子句中的参数类型将按照声明的顺序对照异常类型进行测试。 异常将由第一个与异常类型匹配或为基类的处理程序处理。 这突出了通过引用捕获异常对象的约定。 如果将其作为基类对象捕获，则会创建一个副本，对派生类对象进行切片。 在许多情况下，代码将抛出从`exception`类派生的类型的对象，因此这意味着`exception`的 Catch 处理程序将捕获所有异常。\n\n由于代码可以引发任何对象，因此异常可能会传播出处理程序。 C++ 允许您通过在`catch`子句中使用省略号来捕获所有内容。 显然，您应该从派生程度最高的到派生程度最低的顺序对`catch`处理程序进行排序，并且(如果您使用它)在末尾使用省略号处理程序：\n\n```cpp\n    try  \n    { \n        call_code(); \n    } \n    catch(invalid_argument& iva) \n    { \n        cout << \"invalid argument: \" << e.what() << endl; \n    } \n    catch(exception& exc) \n    { \n        cout << typeid(exc).name() << \": \" << e.what() << endl; \n    } \n    catch(...) \n    { \n        cout << \"some other C++ exception\" << endl; \n    }\n```\n\n如果受保护的代码没有抛出异常，则不会执行`catch`块。\n\n当您的处理程序检查异常时，它可能会决定不想取消该异常；这称为重新引发异常。 为此，您可以使用不带操作数的`throw`语句(只允许在`catch`处理程序中这样做)，这将重新抛出捕获的实际异常对象，而不是副本。\n\n异常是基于线程的，因此很难将异常传播到另一个线程。 `exception_ptr`类(在`<exception>`中)为任何类型的异常对象提供共享所有权语义。 您可以通过调用`make_exception_ptr`对象来获取异常对象的共享副本，甚至可以使用`current_exception`获取`catch`块中正在处理的异常的共享副本。 这两个函数都返回一个`exception_ptr`对象。 `exception_ptr`对象可以包含任何类型的异常，而不仅仅是派生自`exception`类的异常，因此从包装的异常中获取信息是特定于异常类型的。 `exception_ptr`对象对这些细节一无所知，因此您可以将其传递给要使用共享异常(另一个线程)的上下文中的`rethrow_exception`，然后捕获适当的异常对象。 在下面的代码中，有两个线程正在运行。 `first_thread`函数在一个线程上运行，`second_thread`函数在另一个线程上运行：\n\n```cpp\n    exception_ptr eptr = nullptr; \n\n    void first_thread() \n    { \n        try  \n        { \n            call_code(); \n        } \n        catch (...)  \n        { \n            eptr = current_exception();  \n        } \n        // some signalling mechanism ... \n    } \n\n    void second_thread() \n    { \n        // other code \n\n        // ... some signalling mechanism \n        if (eptr != nullptr)  \n        { \n            try \n            { \n                rethrow_exception(eptr); \n            } \n            catch(my_exception& e) \n            { \n                // process this exception \n            } \n            eptr = nullptr; \n        } \n        // other code \n    }\n```\n\n前面的代码看起来像是使用`exception_ptr`作为指针。 实际上，`eptr`被创建为全局对象，对`nullptr`的赋值使用复制构造函数创建一个空对象(其中包装的异常是`nullptr`)。 类似地，与`nullptr`的比较实际上测试了包装的异常。\n\n这本书不是关于 C++ 线程的，所以我们不会深入讨论两个线程之间的信令细节。 此代码显示异常的共享副本*任何异常*可以存储在一个上下文中，然后在另一个上下文中重新引发和处理。\n\n# 函数 try 块\n\n您可能决定使用`try`块来保护整个函数，在这种情况下，您可以编写如下代码：\n\n```cpp\n    void test(double d) \n    { \n        try \n        { \n            cout << setw(10) << d << setw(10) << reciprocal(d) << endl; \n        } \n\n        catch (exception& e) \n        { \n            cout << \"error: \" << e.what() << endl; \n        } \n    }\n```\n\n这使用了前面定义的`reciprocal`函数，如果参数为零，该函数将抛出`exception`。 另一种替代语法是：\n\n```cpp\n    void test(double d) \n    try \n    { \n        cout << setw(10) << d << setw(10) << reciprocal(d) << endl; \n    } \n    catch (exception& e) \n    { \n        cout << \"error: \" << e.what() << endl; \n    }\n```\n\n这看起来相当奇怪，因为函数原型后面紧跟着`try... catch`块，而且没有外部花括号。 函数体是`try`块中的代码；当此代码完成时，函数返回。 如果函数返回值，它必须在`try`块中返回值。 在大多数情况下，您会发现这种语法降低了代码的可读性，但有一种情况下它可能有用--对于构造函数中的初始值设定项列表。\n\n```cpp\n    class inverse \n    { \n        double recip; \n    public: \n        inverse() = delete; \n        inverse(double d) recip(reciprocal(d)) {} \n        double get_recip() const { return recip; } \n    };\n```\n\n在这段代码中，我们包装了一个`double`值，它只是传递给构造函数的参数的倒数。 通过调用初始化器列表中的`reciprocal`函数来初始化数据成员。 由于这在构造函数主体之外，因此此处发生的异常将直接传递给调用构造函数的代码。 如果您想做一些额外的处理，那么可以在构造函数体内调用倒数函数：\n\n```cpp\n    inverse::inverse(double d)  \n    {  \n        try { recip = reciprocal(d); } \n        catch(exception& e) { cout << \"invalid value \" << d << endl; } \n    }\n```\n\n需要注意的是，异常将被自动重新抛出，因为构造函数中的任何异常都意味着对象无效。 但是，如果需要，这确实允许您执行一些额外的处理。 此解决方案不适用于基对象构造函数中引发的异常，因为尽管您可以在派生构造函数体中调用基构造函数，但编译器将自动调用默认构造函数。 如果希望编译器调用默认构造函数以外的构造函数，则必须在初始值设定项列表中调用它。 在`inverse`构造函数中提供异常代码的另一种语法是使用函数`try`块：\n\n```cpp\n    inverse::inverse(double d)  \n    try \n        : recip (reciprocal(d)) {}  \n    catch(exception& e) { cout << \"invalid value \" << d << endl; }\n```\n\n这看起来有点杂乱无章，但构造函数体仍然在初始化式列表之后，为`recip`数据成员提供初始值。 调用`reciprocal`的任何异常都将被捕获，并在处理后自动重新抛出。 初始值设定项列表可以包含对基类和任何数据成员的调用，所有这些都将受到`try`块的保护。\n\n# 系统错误\n\n`<system_error>`库定义了一系列类来封装系统错误。 `error_category`类提供了一种将数值错误值转换为本地化描述性字符串的机制。 通过`<system_error>`中的`generic_category`和`system_category`函数可以获得两个对象，`<ios>`有一个名为`isostream_category`的函数；所有这些函数都返回一个`error_category`对象。 `error_category`类有一个名为`message`的方法，它返回作为参数传递的错误号的字符串描述。 从`generic_category`函数返回的对象将返回 POSIX 错误的描述性字符串，因此您可以使用它来获取`errno`值的描述。 从`system_category`函数返回的对象将使用`FORMAT_MESSAGE_FROM_SYSTEM`参数通过 Win32`FormatMessage`函数返回错误描述，因此这可用于获取`string`对象中 Windows 错误消息的描述性消息。\n\nNote that `message` has no extra parameters to pass in values for a Win32 error message that takes parameters. Consequently, in those situations you will get back a message that has formatting placeholders.\n\n尽管名称不同，`isostream_category`对象本质上返回的描述与`generic_category`对象相同。\n`system_error`异常是一个报告由`error_category`对象之一描述的值的类。 例如，这是早先针对`FormatMessage`使用的示例，但使用`system_error`重写：\n\n```cpp\n    HANDLE file = CreateFileA( \n       \"does_not_exist\", GENERIC_READ, 0, 0, OPEN_EXISTING, 0, 0); \n    if (INVALID_HANDLE_VALUE == file) \n    { \n        throw system_error(GetLastError(), system_category()); \n    } \n    else \n    { \n        CloseHandle(file); \n    }\n```\n\n这里使用的`system_error`构造函数将错误值作为第一个参数(从 Win32 函数`GetLastError`返回的`ulong`)和一个`system_category`对象，该对象用于在调用`system_error::what`方法时将错误值转换为描述性字符串。\n\n# 嵌套异常\n\n`catch`块可以通过调用不带任何操作数的`throw`来重新抛出当前异常，并且在调用堆栈到达下一个`try`块之前将进行堆栈展开。 您还可以重新抛出嵌套在另一个异常中的当前异常*。 这是通过调用`throw_with_nested`函数(在`<exception>`中)并传递新异常来实现的。 该函数调用`current_exception`并将异常对象与参数一起包装在嵌套异常中，然后抛出该参数。 调用堆栈上方的`try`块可以捕获此异常，但它只能访问外部异常；它不能直接访问内部异常。 相反，内部异常可以通过调用`rethrow_if_nested`抛出。 例如，以下是用于打开文件的另一个版本的代码：*\n\n```cpp\n    void open(const char *filename) \n    { \n        try  \n        { \n            ifstream file(filename); \n            file.exceptions(ios_base::failbit); \n            // code if the file exists \n        } \n        catch (exception& e)  \n        { \n            throw_with_nested( \n                system_error(ENOENT, system_category(), filename)); \n        } \n    }\n```\n\n代码打开一个文件，如果该文件不存在，则设置一个状态位(您可以稍后通过调用`rdstat`方法来测试这些位)。 下一行指示抛出异常的类应该处理的状态位的值，在本例中提供了`ios_base::failbit`。 如果构造函数无法打开文件，则将设置此位，因此`exceptions`方法将以抛出异常作为响应。 在本例中，异常被捕获并包装到嵌套异常中。 外部异常是一个`system_error`异常，它被初始化为错误值`ENOENT`(表示文件不存在)和一个`error_category`对象来解释它，并将文件名作为附加信息传递。\n\n此函数可按如下方式调用：\n\n```cpp\n    try \n    { \n        open(\"does_not_exist\"); \n    } \n    catch (exception& e) \n    { \n        cout << e.what() << endl; \n    }\n```\n\n可以访问此处捕获的异常，但它只提供有关外部对象的信息：\n\n```cpp\n does_not_exist: The system cannot find the file specified.\n```\n\n此消息由`system_error`对象使用传递给其构造函数的附加信息和来自 CATEGORY 对象的描述来构造。 要在嵌套异常中获取内部对象，您必须通过调用`rethrow_if_nested`告诉系统抛出内部异常。 因此，您可以调用如下函数，而不是打印出外部异常：\n\n```cpp\n    void print_exception(exception& outer) \n    { \n        cout << outer.what() << endl; \n        try { rethrow_if_nested(outer); } \n        catch (exception& inner) { print_exception(inner); } \n    }\n```\n\n这将打印外部异常的描述，然后调用`rethrow_if_nested,`，只有在异常是嵌套的情况下才会抛出该异常。 如果是，它抛出内部异常，然后捕获该异常并递归调用`print_exception`函数。 结果是：\n\n```cpp\n    does_not_exist: The system cannot find the file specified. \n    ios_base::failbit set: iostream stream error\n```\n\n最后一行是调用`ifstream::exception`方法时抛出的内部异常。\n\n# 结构化异常处理\n\nWindows 中的本机异常是**结构化异常处理**(**SEH**)，Visual C++ 有一个语言扩展来允许您捕获这些异常。 重要的是要理解，它们与 C++ 异常不同，C++ 异常被编译器认为是*同步的*，也就是说，编译器知道一个方法是否可能(或者明确地说，不会)抛出 C++ 异常，并且它在分析代码时使用该信息。 C++ 异常也按类型捕获。 SEH 不是一个 C++ 概念，因此编译器将结构化异常视为*异步*，这意味着它将受 SEH 保护的块中的任何代码视为潜在地引发结构化异常，因此编译器无法执行优化。 Seh 异常也由异常代码捕获。\n\nSEH 的语言扩展是对 Microsoft C/C++ 的扩展，也就是说，它们既可以在 C++ 中使用，也可以在 C++ 中使用，因此处理基础结构不知道对象析构函数。 此外，当您捕获 SEH 异常时，不会对堆栈或进程的任何其他部分的状态做出任何假设。\n\n尽管大多数 Windows 函数将以适当的方式捕获内核生成的 SEH 异常，但有些函数故意允许它们传播(例如，**远程过程调用**(**RPC**)函数，或用于内存管理的函数)。 使用某些 Windows 函数，您可以显式请求使用 SEH 异常处理错误。 例如，`HeapCreate`组函数将允许 Windows 应用创建私有堆，您可以传递`HEAP_GENERATE_EXCEPTIONS`标志来指示创建堆以及在私有堆中分配或重新分配内存时出现的错误将生成 SEH 异常。 这是因为调用这些函数的开发人员可能会认为故障非常严重，无法恢复，因此进程应该终止。 由于 SEH 是如此严重的情况，您应该仔细检查除了报告异常的详细信息并终止该过程之外，是否适合(并非完全不可能)做更多的事情。\n\nSEH 异常本质上是低级操作系统异常，但熟悉语法很重要，因为它看起来类似于 C++ 异常。 例如：\n\n```cpp\n    char* pPageBuffer; \n    unsigned long curPages = 0; \n    const unsigned long PAGESIZE = 4096; \n    const unsigned long PAGECOUNT = 10; \n\n    int main() \n    { \n        void* pReserved = VirtualAlloc( \n        nullptr, PAGECOUNT * PAGESIZE, MEM_RESERVE, PAGE_NOACCESS); \n        if (nullptr == pReserved)  \n        { \n            cout << \"allocation failed\" << endl; \n            return 1; \n        } \n\n        char *pBuffer = static_cast<char*>(pReserved); \n        pPageBuffer = pBuffer; \n\n        for (int i = 0; i < PAGECOUNT * PAGESIZE; ++ i) \n        { \n            __try { pBuffer[i] = 'X'; } __except (exception_filter(GetExceptionCode())) { cout << \"Exiting process.n\"; ExitProcess(GetLastError()); } \n        } \n        VirtualFree(pReserved, 0, MEM_RELEASE); \n        return 0; \n    }\n```\n\n此处突出显示 SEH 异常代码。 此代码使用 Windows`VirtualAlloc`函数保留若干页内存。 保留不会分配内存，该操作必须在称为**提交内存**的单独操作中执行。 Windows 将在称为**页**的块中保留(和提交)内存，并且在大多数系统上，正如这里假设的那样，页是 4096 字节。 对`VirtualAlloc`函数的调用表明它应该保留 10 个 4096 字节的页面，这些页面将在以后提交(和使用)。\n\n`VirtualAlloc`的第一个参数指示内存的位置，但由于我们是在保留内存，所以这并不重要，因此传递了`nullptr`。 如果保留成功，则向内存返回一个指针。 `for`循环每次只向内存写入一个字节的数据。 突出显示的代码通过结构化异常处理保护这种内存访问。 受保护的块以`__try`关键字开头。 当引发 SEH 时，执行转到`__except`块。 这与 C++ 异常中的`catch`块非常不同。 首先，`__except`异常处理程序接收三个值中的一个，以指示它应该如何行为。 只有当这是`EXCEPTION_EXECUTE_HANDLER`时，处理程序块中的代码才会运行(在此代码中，突然关闭进程)。 如果值为`EXCEPTION_CONTINUE_SEARCH`，则不识别异常，搜索将继续向上堆栈*，但不展开 C++ 堆栈*。 令人惊讶的值是`EXCEPTION_CONTINUE_EXECUTION,`，因为这会忽略异常，`__try`块中的执行将继续。 *对于 C++ 异常，您不能这样做*。 通常，SEH 代码将使用异常筛选器函数来确定`__except`处理程序需要执行什么操作。 在此代码中，此筛选器称为`exception_filter,`，向其传递通过调用 Windows 函数`GetExceptionCode`获得的异常代码。 此语法非常重要，因为此函数只能在`__except`上下文中调用。\n\n循环第一次运行时没有提交内存，因此写入内存的代码将引发异常：页面错误。 执行将传递到异常处理程序并传递到`exception_filter`：\n\n```cpp\n    int exception_filter(unsigned int code) \n    { \n        if (code != EXCEPTION_ACCESS_VIOLATION) \n        { \n            cout << \"Exception code = \" << code << endl; \n            return EXCEPTION_EXECUTE_HANDLER; \n        } \n\n        if (curPage >= PAGECOUNT) \n        { \n            cout << \"Exception: out of pages.n\"; \n            return EXCEPTION_EXECUTE_HANDLER; \n        } \n\n        if (VirtualAlloc(static_cast<void*>(pPageBuffer), PAGESIZE, \n         MEM_COMMIT, PAGE_READWRITE) == nullptr) \n        { \n            cout << \"VirtualAlloc failed.n\"; \n            return EXCEPTION_EXECUTE_HANDLER; \n        } \n\n        curPage++ ; \n        pPageBuffer += PAGESIZE; \n        return EXCEPTION_CONTINUE_EXECUTION; \n    }\n```\n\n在 SEH 代码中，重要的是只处理您知道的异常，并且只有在您知道条件已经完全解决的情况下才使用异常。 如果您访问尚未提交的 Windows 内存，操作系统会生成一个称为页面错误的异常。 在这段代码中，测试异常代码以确定它是否是页面错误，如果不是，筛选器返回，通知异常处理程序运行终止进程的异常处理程序块中的代码。 如果异常是页面错误，那么我们可以提交下一页。 首先，要测试页码是否在我们将使用的范围内(如果不在，则关闭该进程)。 然后，通过另一次调用`VirtualAlloc`来提交下一页，以标识要提交的页以及该页中的字节数。 如果函数成功，它将返回指向提交页的指针或空值。 只有在提交页面成功时，筛选器才会返回值`EXCEPTION_CONTINUE_EXECUTION`，这表示异常已得到处理，并且可以在引发异常的时间点继续执行。 此代码是使用`VirtualAlloc`的标准方式，因为它意味着只有在需要内存分页时才会提交它们。\n\nSEH 还有终止处理程序的概念。 当执行通过调用`return`离开`__try`代码块时，或者通过完成块中的所有代码，或者通过调用 Microsoft 扩展`__leave`指令，或者引发 SEH，则调用标记为`__finally`的终止处理程序代码块。 由于终止处理程序总是被调用，所以无论如何退出`__try`块，都可以将其用作释放资源的一种方式。 然而，由于 SEH 不执行 C++ 堆栈展开(也不调用析构函数)，这意味着您不能在具有 C++ 对象的函数中使用此代码。 事实上，编译器将拒绝编译具有 SEH 并创建了 C++ 对象的函数，无论是在函数堆栈上还是在堆上分配的。 (但是，您可以使用全局对象或在调用函数时分配并作为参数传入的对象。)。 `__try`/`__finally`结构看起来很有用，但受到不能与创建 C++ 对象的代码一起使用的要求的限制。\n\n# 编译器异常开关\n\n在这一点上，有必要解释一下为什么要使用`/EHsc`开关编译代码。 简单的答案是，如果您不使用此开关，编译器将从标准库代码发出警告，并且由于标准库使用异常，因此您必须使用`/EHsc`开关。 警告告诉您要这样做，所以这就是您要做的。\n\n长长的答案是，`/EH`开关有三个参数，您可以使用它们来影响异常的处理方式。 使用`s`参数告诉编译器为同步异常提供基础结构，即可能在`try`块中抛出并在`catch`块中处理的 C++ 异常，并且具有调用自动 C++ 对象析构函数的堆栈展开。 `c`参数指示`extern C`函数(即所有 Windows SDK 函数)从不抛出 C++ 异常(因此编译器可以执行更高级别的优化)。 因此，您可以使用`/EHs`或`/EHsc`编译标准库代码，但后者将生成更优化的代码。 还有一个额外的参数，其中`/EHa`表示代码将使用`try`/`catch`块捕获*同步和异步异常(SEH)。*\n\n# 混合使用 C++ 和 SEH 异常处理\n\n`RaiseException`Windows 函数将抛出 SEH 异常。 第一个参数是异常代码，第二个参数表示处理此异常后进程是否可以继续(`0`表示可以)。 第三个和第四个参数提供有关异常的附加信息。 第四个参数是指向具有这些附加参数的数组的指针，参数的数量在第三个参数中给出。\n\n使用`/EHa`，您可以编写如下代码：\n\n```cpp\n    try  \n    { \n        RaiseException(1, 0, 0, nullptr); \n    } \n    // legal code, but don't do it \n    catch(...) \n    { \n        cout << \"SEH or C++ exception caught\" << endl; \n    }\n```\n\n此代码的问题在于它处理所有 SEH 异常。 这相当危险，因为某些 SEH 异常可能指示进程状态已损坏，因此进程继续运行是危险的。 C 运行库提供了一个名为`_set_se_translator`的函数，该函数提供了一种机制来指示哪些 SEH 异常由`try`处理。 使用此原型编写的函数会向此函数传递一个指针：\n\n```cpp\n    void func(unsigned int, EXCEPTION_POINTERS*);\n```\n\n第一个参数是异常代码(将从`GetExceptionCode`函数返回)，第二个参数是从`GetExceptionInformation`函数返回的，并且具有与异常相关的任何附加参数(例如，通过`RaiseException`中的第三个和第四个参数传递的参数)。 您可以使用这些值在 SEH 的位置抛出 C++ 异常。 如果您提供此功能：\n\n```cpp\n    void seh_to_cpp(unsigned int code, EXCEPTION_POINTERS*) \n    { \n        if (code == 1) throw exception(\"my error\"); \n    }\n```\n\n现在，您可以在处理 SEH 异常之前注册该函数：\n\n```cpp\n    _set_se_translator(seh_to_cpp); \n    try  \n    { \n        RaiseException(1, 0, 0, nullptr); \n    } \n    catch(exception& e) \n    { \n        cout << e.what() << endl; \n    }\n```\n\n在这段代码中，`RaiseException`函数产生一个值为 1 的自定义 SEH。这个转换可能不是最有用的，但它说明了这一点。 `winnt.h`头文件定义了可以在 Windows 代码中引发的标准 SEH 异常的异常代码。 更有用的翻译功能是：\n\n```cpp\n    double reciprocal(double d) \n    { \n        return 1.0 / d; \n    } \n\n    void seh_to_cpp(unsigned int code, EXCEPTION_POINTERS*) \n    { \n        if (STATUS_FLOAT_DIVIDE_BY_ZERO == code || \n            STATUS_INTEGER_DIVIDE_BY_ZERO == code) \n        { \n            throw invalid_argument(\"divide by zero\"); \n        } \n    }\n```\n\n这允许您按如下方式调用倒数函数：\n\n```cpp\n    _set_se_translator(seh_to_cpp); \n    try  \n    { \n        reciprocal(0.0); \n    } \n    catch(invalid_argument& e) \n    { \n        cout << e.what() << endl; \n    }\n```"
  },
  {
    "path": "docs/begin-cpp-prog/README.md",
    "content": "# C++ 编程入门手册\n\n> 原书：[Beginning C++ Programming](https://libgen.rs/book/index.php?md5=8B22C2649BDEC9FA4EE716AE82AE0BB1)\n> \n> 协议：[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)\n> \n> 阶段：机翻（1）\n> \n> 自豪地采用[谷歌翻译](https://translate.google.cn/)\n> \n> 让开源界提前感受入关。\n\n* [在线阅读](https://ccpp.apachecn.org)\n* [在线阅读（Gitee）](https://apachecn.gitee.io/apachecn-c-cpp-zh/)\n* [ApacheCN 学习资源](http://docs.apachecn.org/)\n\n## 贡献指南\n\n本项目需要校对，欢迎大家提交 Pull Request。\n\n> 请您勇敢地去翻译和改进翻译。虽然我们追求卓越，但我们并不要求您做到十全十美，因此请不要担心因为翻译上犯错——在大部分情况下，我们的服务器已经记录所有的翻译，因此您不必担心会因为您的失误遭到无法挽回的破坏。（改编自维基百科）\n\n## 联系方式\n\n### 负责人\n\n* [飞龙](https://github.com/wizardforcel): 562826179\n\n### 其他\n\n*   在我们的 [apachecn/apachecn-c-cpp-zh](https://github.com/apachecn/apachecn-c-cpp-zh) github 上提 issue.\n*   发邮件到 Email: `apachecn@163.com`.\n*   在我们的 [组织学习交流群](http://www.apachecn.org/organization/348.html) 中联系群主/管理员即可.\n\n## 赞助我们\n\n![](http://data.apachecn.org/img/about/donate.jpg)\n"
  },
  {
    "path": "docs/begin-cpp-prog/SUMMARY.md",
    "content": "+   [C++ 编程入门手册](README.md)\n+   [零、前言](00.md)\n+   [一、从 C++ 开始](01.md)\n+   [二、了解语言特性](02.md)\n+   [三、探索 C++ 类型](03.md)\n+   [四、使用内存、数组和指针](04.md)\n+   [五、使用函数](05.md)\n+   [六、类](06.md)\n+   [七、面向对象编程导论](07.md)\n+   [八、使用标准库容器](08.md)\n+   [九、使用字符串](09.md)\n+   [十、诊断和调试](10.md)\n"
  },
  {
    "path": "docs/boost-asio-cpp-net-prog-2e/0.md",
    "content": "# 零、前言\n\n大约二十年前，网络应用不是很容易开发。但是多亏了 Boost.Asio 为我们提供了网络编程功能以及异步操作功能来编程网络应用，现在我们可以轻松地开发它们。由于网络上的数据传输可能需要很长时间，这意味着确认和错误可能没有发送或接收数据的函数执行得快，因此在网络应用编程中确实需要异步操作功能。在本书中，您将学习网络基础知识，以及如何使用 Boost 开发网络应用.Asio 库。\n\n# 这本书涵盖了什么\n\n[第一章](1.html#page \"Chapter 1. Simplifying Your Network Programming in C++\")、*用 C++ 简化你的网络编程*解释了一个 C++ 编译器的准备，这个编译器将用来编译本书所有的源代码。此外，它将告诉我们如何编译单个源代码并链接到多个源代码。\n\n[第二章](2.html#page \"Chapter 2. Understanding the Networking Concepts\")、*了解组网概念*，涵盖了网络参考模型，分别是 OSI 和 TCP/IP。它还提供了各种 TCP/IP 工具，我们将经常使用这些工具来检测我们的网络连接中是否发生了错误。\n\n[第 3 章](3.html#page \"Chapter 3. Introducing the Boost C++ Libraries\")、*介绍 Boost C++ 库*，说明如何设置编译器来编译包含 Boost 库的代码，以及如何构建我们必须单独编译的库的二进制文件。\n\n[第四章](4.html#page \"Chapter 4. Getting Started with Boost.Asio\")*助力入门.Asio* ，讲并发和非并发编程。它还讨论了输入/输出服务，该服务用于访问操作系统的资源，并在我们的程序和执行输入/输出请求的操作系统之间建立通信。\n\n[第五章](5.html#page \"Chapter 5. Delving into the Boost.Asio Library\")*钻研助推.Asio Library* ，为我们讲解如何序列化 I/O 服务的工作，以确保工作顺序与我们设计的顺序完全匹配。它还包括如何处理网络编程中的错误和异常以及造成时间延迟。\n\n[第 6 章](6.html#page \"Chapter 6. Creating a Client-server Application\")，*创建客户端-服务器应用*，讨论了开发能够从客户端发送和接收数据流量的服务器，以及如何创建客户端程序来接收数据流量。\n\n[第 7 章](7.html#page \"Chapter 7. Debugging the Code and Solving the Error\")、*调试代码和解决错误*涵盖了调试过程，以跟踪意外结果可能产生的错误，例如在程序执行过程中出现崩溃。阅读本章后，您将能够通过调试代码来解决各种错误。\n\n# 这本书你需要什么\n\n要浏览本书并成功编译所有源代码，您需要一台运行微软视窗 8.1(或更高版本)并包含以下软件的个人计算机:\n\n*   MinGW-w64 适用于 Windows，版本 4.9.2\n*   记事本++ 的最新版本\n*   1.58.0 版的 Boost C++ 库\n\n# 这本书是给谁的\n\n这本书是给有网络编程基础知识，但不知道如何使用 Boost 的 C++ 网络程序员看的。网络编程 Asio。\n\n# 惯例\n\n在这本书里，你会发现许多区分不同种类信息的文本样式。以下是这些风格的一些例子和对它们的意义的解释。\n\n文本中的码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、伪 URL、用户输入和 Twitter 句柄如下所示:“等待片刻，直到`mingw-w64-install.exe`文件完全下载完毕。”\n\n代码块设置如下:\n\n```cpp\n/* rangen.cpp */\n#include <cstdlib>\n#include <iostream>\n#include <ctime>\nint main(void) {\n```\n\n当我们希望将您的注意力吸引到代码块的特定部分时，相关的行或项目以粗体显示:\n\n```cpp\nint guessNumber;\nstd::cout << \"Select number among 0 to 10: \";\nstd::cin >> guessNumber;\n\n```\n\n任何命令行输入或输出都编写如下:\n\n```cpp\nrundll32.exe sysdm.cpl,EditEnvironmentVariables\n\n```\n\n**新名词**和**重要词语**以粗体显示。你在屏幕上看到的单词，例如，在菜单或对话框中，出现在这样的文本中:“你将受到**欢迎**对话框的欢迎。只需按下**下一步**按钮，进入**设置**对话框。”\n\n### 注\n\n警告或重要提示会出现在这样的框中。\n\n### 类型\n\n提示和技巧是这样出现的。\n\n# 读者反馈\n\n我们随时欢迎读者的反馈。让我们知道你对这本书的看法——你喜欢或不喜欢什么。读者反馈对我们来说很重要，因为它有助于我们开发出你真正能从中获益的标题。\n\n要给我们发送一般反馈，只需发送电子邮件`<[feedback@packtpub.com](mailto:feedback@packtpub.com)>`，并在您的邮件主题中提及书名。\n\n如果你对某个主题有专业知识，并且对写作或投稿感兴趣，请参见我们位于[www.packtpub.com/authors](http://www.packtpub.com/authors)的作者指南。\n\n# 客户支持\n\n现在，您已经自豪地拥有了一本书，我们有许多东西可以帮助您从购买中获得最大收益。\n\n## 下载示例代码\n\n您可以从您在[http://www.packtpub.com](http://www.packtpub.com)的账户下载您购买的所有 Packt Publishing 书籍的示例代码文件。如果您在其他地方购买了这本书，您可以访问[http://www.packtpub.com/support](http://www.packtpub.com/support)并注册，以便将文件直接通过电子邮件发送给您。\n\n## 勘误表\n\n尽管我们尽了最大努力来确保我们内容的准确性，但错误还是会发生。如果你在我们的某本书里发现了错误——可能是文本或代码中的错误——如果你能向我们报告，我们将不胜感激。通过这样做，你可以让其他读者免受挫折，并帮助我们改进这本书的后续版本。如果您发现任何勘误表，请访问[http://www.packtpub.com/submit-errata](http://www.packtpub.com/submit-errata)，选择您的书籍，点击**勘误表提交表**链接，并输入您的勘误表的详细信息。一旦您的勘误表得到验证，您的提交将被接受，勘误表将上传到我们的网站或添加到该标题勘误表部分下的任何现有勘误表列表中。\n\n要查看之前提交的勘误表，请前往[https://www.packtpub.com/books/content/support](https://www.packtpub.com/books/content/support)并在搜索栏中输入图书名称。所需信息将出现在**勘误表**部分。\n\n## 盗版\n\n互联网上版权材料的盗版是所有媒体的一个持续问题。在 Packt，我们非常重视版权和许可证的保护。如果您在互联网上遇到任何形式的我们作品的非法拷贝，请立即向我们提供位置地址或网站名称，以便我们寻求补救。\n\n请通过`<[copyright@packtpub.com](mailto:copyright@packtpub.com)>`联系我们，获取疑似盗版资料的链接。\n\n我们感谢您在保护我们的作者方面的帮助，以及我们为您带来有价值内容的能力。\n\n## 问题\n\n如果您对本书的任何方面有问题，可以在`<[questions@packtpub.com](mailto:questions@packtpub.com)>`联系我们，我们将尽最大努力解决问题。"
  },
  {
    "path": "docs/boost-asio-cpp-net-prog-2e/1.md",
    "content": "# 一、使用 C++ 简化您的网络编程\n\n我们可以从网上选择几个 C++ 编译器。为了让您更容易理解本书中的所有代码，我选择了一个编译器，它将使编程过程更简单——绝对是最简单的一个。在本章中，您将发现以下主题:\n\n*   设置 MinGW 编译器\n*   用 C++ 编译\n*   GCC C++ 中的故障排除\n\n# 设置 MinGW 编译器和文本编辑器\n\n这是最难的部分——我们必须选择一个编译器。尽管我意识到每个编译器都有自己的优点和缺点，但我想让您更容易地浏览本章中的所有代码。因此，我建议您应用与我们相同的环境，包括我们使用的编译器。\n\n我将使用 GNU 编译器集合 **GCC** ，因为它是广泛使用的开源。由于我的环境包括微软视窗作为操作系统，我将使用 T4【视窗的极简主义 GCC】(**MinGW**)作为我的 C++ 编译器。对于那些没有听说过 GCC 的人来说，它是一个 C/C++ 编译器，你可以在 Linux 操作系统中找到，它也包含在 Linux 发行版中。MinGW 是海湾合作委员会在视窗环境下的一个港口。因此，本书中的整个代码和示例适用于任何其他 GCC 风格。\n\n## 安装 MinGW-w64\n\n为了您的方便，并且由于我们使用的是 64 位的 Windows 操作系统，所以我们选择了 MinGW-w64，因为它可以用于 Windows 32 位和 64 位架构。要安装它，只需打开您的互联网浏览器，导航至[http://sourceforge.net/projects/mingw-w64/](http://sourceforge.net/projects/mingw-w64/)进入下载页面，点击**下载**按钮。稍等片刻，直到`mingw-w64-install.exe`文件完全下载完毕。参考以下截图找到**下载**按钮:\n\n![Installing MinGW-w64](img/00002.jpeg)\n\n现在，执行安装程序文件。将会出现**欢迎**对话框。只需按下**下一步**按钮，进入**设置**对话框。在该对话框中，选择最新的 GCC 版本(此时为 **4.9.2** ，其余选项可选，如下:\n\n![Installing MinGW-w64](img/00003.jpeg)\n\n点击**下一步**按钮继续，进入安装位置选项。在这里，您可以更改默认安装位置。我准备把安装位置改成`C:\\MinGW-w64`以便我们下一步的设置更容易，但是如果你愿意的话可以保留这个默认位置。\n\n![Installing MinGW-w64](img/00004.jpeg)\n\n点击**下一步**按钮进入下一步，等待片刻，直到文件下载完毕，安装过程完成。\n\n## 设置路径环境\n\n现在你的机器上安装了 C++ 编译器，但是你只能从它的安装目录中访问它。为了从系统中的任何目录访问编译器，您必须通过执行以下步骤来设置 **路径环境**:\n\n1.  按下*窗口* + *R* 键，以管理员身份运行命令提示符。在文本框中输入`cmd`，不按*回车*键，而是按*Ctrl*+*Shift*+*回车*在管理员模式下运行命令提示符。这时将出现**用户账户控制**对话框。选择**是**确认您打算在管理员模式下运行命令提示符。如果操作正确，你会得到一个标题栏，标签为**管理员:命令提示符**。如果您没有得到它，您可能没有管理员权限。在这种情况下，您必须联系计算机管理员。\n2.  在管理员模式下的命令提示符下键入以下命令:\n\n    ```cpp\n    rundll32.exe sysdm.cpl,EditEnvironmentVariables\n\n    ```\n\n3.  Press the *Enter* key and the command prompt will immediately run the **Environment Variables** window. Afterwards, go to **System variables**, select the variable named **Path**, click on the **Edit** button to open the **Edit System Variable** dialog box, and then append the last **Variable value** parameter with the following string:\n\n    ```cpp\n    ;C:\\MinGW-w64\\mingw64\\bin\n    ```\n\n    (否则，如果使用安装向导在上一步中给出的默认位置，则必须调整安装目录的路径)\n\n4.  点击**编辑系统变量**对话框中的**确定**按钮，在**环境变量**对话框中再次点击**确定**按钮保存这些更改。\n\n是时候尝试我们的环境变量设置了。在除`C:\\MinGW-w64`以外的任何活动目录中，以管理员或非管理员模式打开一个新的命令提示符窗口，并键入以下命令:\n\n```cpp\ng++ --version\n\n```\n\n如果您看到输出通知您以下内容，您已经配置了正确的设置:\n\n```cpp\ng++ (x86_64-posix-seh-rev2, Built by MinGW-W64 project) 4.9.2\n\n```\n\n如果显示不同的版本号，您的计算机上可能有另一个 GCC 编译器。要解决这个问题，您可以修改**环境变量**，并删除与其他 GCC 编译器相关联的所有路径环境设置，例如`C:\\StrawberryPerl\\c\\bin`。\n\n但是，如果您确信已经正确执行了所有步骤，但仍然收到错误消息，如以下代码片段所示，您可能需要重新启动计算机来设置新的系统设置:\n\n```cpp\n'g++' is not recognized as an internal or external command, operable program or batch file.\n\n```\n\n## 选择和安装文本编辑器\n\n微软 Windows 已经配备了**记事本**，一个创建纯文本文件的简单文本编辑器。您可以使用记事本创建一个 C++ 文件，其中文件必须只包含纯文本格式。当你想编辑你的代码时，你也可以求助于一个沉重的 **集成开发环境** ( **IDE** )，但是我更喜欢一个简单、轻量、可扩展的编程纯文本编辑器，所以我选择使用文本编辑器而不是 IDE。由于我在编写代码时需要语法高亮，以使更容易阅读和理解，所以我选择**记事本++** 作为我们的文本编辑器。只要将输出文件保存为纯文本，您就可以选择自己喜欢的文本编辑器。以下是记事本++ 中语法高亮显示的示例:\n\n![Choosing and installing the Text Editor](img/00005.jpeg)\n\n如果你和我一样决定使用 Notepad++ 的话，可以去[http://notepad-plus-plus.org/](http://notepad-plus-plus.org/)到抓取 Notepad++ 的最新版本。在主页面找到**下载**菜单，选择当前版本链接。在那里，您将找到下载安装程序文件的链接。使用**记事本++ 安装程序**文件代替程序包文件，按照安装程序向导上的所有说明，获得在您的机器上设置它的最简单方法。\n\n![Choosing and installing the Text Editor](img/00006.jpeg)\n\n# 使用 GCC C++ 编译器\n\n现在我们已经准备好了我们的开发，我们可以编写我们的第一个 C++ 程序了。为了保持干净，在 C 驱动器(`C:\\CPP`)中创建一个`CPP`文件夹来存储我们的示例代码。您可以在系统上有相同的目录位置，以便更方便地执行所有步骤。否则，如果您决定使用不同的目录位置，您将不得不做一些修改。\n\n## 编译一个 C++ 程序\n\n我们不会创造你好世界！第一个示例代码的程序。在我看来，这很无聊，到现在，你应该已经知道如何编写 Hello World 了！程序。我们将创建一个简单的随机数生成器。你可以用这个程序和你的朋友一起玩。他们必须猜测程序将显示哪个数字。如果答案不正确，你可以用记号笔划掉他/她的脸，继续玩，直到你再也认不出你朋友的脸。下面是创建该生成器的代码:\n\n```cpp\n/* rangen.cpp */\n#include <cstdlib>\n#include <iostream>\n#include <ctime>\nint main(void) {\n  int guessNumber;\n  std::cout << \"Select number among 0 to 10:\";\n  std::cin >> guessNumber;\n  if(guessNumber < 0 || guessNumber > 10) {\n    return 1;\n  }\n  std::srand(std::time(0));\n  int randomNumber = (std::rand() % (10 + 1));\n  if(guessNumber == randomNumber) {\n    std::cout << \"Congratulation, \" <<guessNumber<<\" is your lucky number.\\n\";\n  }\n  else {\n    std::cout << \"Sorry, I'm thinking about number \\n\" << randomNumber;\n  }\n  return 0;\n}\n```\n\n在文本编辑器中键入代码，并将其与文件名称`rangen.cpp`一起保存在`C:\\CPP`位置。然后，打开命令提示符，在命令提示符下键入以下命令，将活动目录指向`C:\\CPP`位置:\n\n```cpp\ncd C:\\CPP\n\n```\n\n接下来，在控制台中键入以下命令来编译代码:\n\n```cpp\ng++ -Wall rangen.cpp -o rangen\n\n```\n\n前面的命令用一个名为`rangen.exe`的可执行文件编译`rangen.cpp`文件，该文件包含一堆机器码(自动添加`exe`扩展名，表示该文件是微软 Windows 中的可执行文件)。使用`-o`选项指定机器代码的输出文件。如果使用该选项，还必须指定输出文件的名称；否则，编译器会给你一个丢失文件名的错误。如果省略`-o`选项和输出文件名，输出将被写入名为`a.exe`的默认文件。\n\n### 类型\n\n与当前目录中的编译源文件同名的现有可执行文件将被覆盖。\n\n我建议您使用`-Wall`选项，并使其成为一种习惯，因为该选项将打开所有最常用的编译器警告。如果该选项被禁用，GCC 不会给你任何警告。因为我们的随机数生成器代码是完全有效的，所以 GCC 在编译时不会给出任何警告。这就是为什么我们依赖编译器警告来确保我们的代码是有效的并且被干净地编译。\n\n要运行该程序，在控制台中以`C:\\CPP`位置作为活动目录键入`rangen`，会显示一个欢迎词:**从 0 到 10** 中选择一个数字。按照它的指示去做，在`0`到`10`之间选择一个数字。然后，按*进入*，程序会给出一个数字。和你自己的比较一下。如果两个数字相同，你会受到祝贺。但是，如果您选择的号码与代码生成的号码不同，您将收到相同的通知。程序的输出如下图所示:\n\n![Compiling a C++ program](img/00007.jpeg)\n\n不幸的是，在我尝试的三次中，我从未猜出正确的数字。事实上，要猜测`rand()`函数生成了哪个数字并不容易，即使每次生成数字时都使用新的种子。为了尽量减少混乱，我将剖析`rangen.cpp`代码，如下所示:\n\n```cpp\nint guessNumber;\nstd::cout << \"Select number among 0 to 10: \";\nstd::cin >> guessNumber;\n\n```\n\n我保留了一个名为`guessNumber`的变量来存储来自用户的整数，并使用`std::cin`命令来获取从控制台输入的数字。\n\n```cpp\nif(guessNumber < 0 || guessNumber > 10) {\n return 1;\n}\n\n```\n\n如果用户给出了一个超出范围的数字，请通知操作系统程序中出现了错误——我发送了错误 1，但实际上，您可以发送任何数字——并让它处理该错误。\n\n```cpp\nstd::srand(std::time(0));\nint randomNumber = (std::rand() % (10 + 1);\n\n```\n\n`std::srand`函数用于初始化种子，为了在每次调用`std::rand()`函数时生成一个不同的随机数，我们从表头`ctime`使用`std::time(0)`函数。要生成一系列随机数，我们使用`modulo`方法，如果您调用类似`std::rand() % n`的函数，该方法将生成从 0 到(n-1)的随机数。如果你想把 *n* 也包括在内，只需用`1`加上 *n* 。\n\n```cpp\nif(guessNumber == randomNumber) {\n std::cout << \"Congratulation ,\"<< guessNumber<<\" is your lucky number.\\n\";\n}\nelse {\n std::cout << \"Sorry, I'm thinking about number \" << randomNumber << \"\\n\";\n}\n\n```\n\n这里是好玩的部分，程序将用户的猜测数与生成的随机数进行比较。无论发生什么，程序都会通知用户结果。让我们看看下面的代码:\n\n```cpp\nreturn 0;\n\n```\n\n一个`0`返回告诉操作系统程序已经正常终止，不用担心。让我们看看下面的代码:\n\n```cpp\n#include <cstdlib>\n#include <iostream>\n#include <ctime>\n\n```\n\n不要忘记在前面的代码中包含前三个头，因为它们包含了我们在这个程序中使用的函数，例如`<ctime>`头中定义了`time()`函数，`srand()`函数和`rand()`函数在`<cstdlib>`头中定义，`cout()`和`cin()`函数在`<iostream>`头中定义。\n\n如果你发现很难猜出程序生成的数字，这是因为我们使用当前时间作为随机生成器种子，这样做的结果是，在程序的每次调用中，生成的数字总是不同的。以下是我在大约六到七次尝试后能够正确猜测生成的随机数的截图(对于所有的程序调用，除了最后一次尝试之外，我们都错误地猜测了该数字):\n\n![Compiling a C++ program](img/00008.jpeg)\n\n## 编译多个源文件\n\n有时，当我们的代码有错误时，我们必须修改它。如果我们只是制作一个包含所有代码行的单个文件，当我们想要修改源代码时，我们会感到困惑，或者我们很难理解程序的流程。为了解决这个问题，我们可以将代码分成多个文件，其中每个文件只包含两到三个函数，以便于理解和维护它们。\n\n我们已经能够生成随机数，所以现在，让我们看看密码生成器程序。我们将使用它来尝试编译多个源文件。我将创建三个文件来演示如何编译多个源文件，它们是`pwgen_fn.h`、`pwgen_fn.cpp`和`passgen.cpp`。我们将从`pwgen_fn.h`文件开始，其代码如下:\n\n```cpp\n/* pwgen_fn.h */\n#include <string>\n#include <cstdlib>\n#include <ctime>\nclass PasswordGenerator {\n  public:\n    std::string Generate(int);\n};\n```\n\n前面的代码用于声明类名。在这个例子中，类名是`PasswordGenerator`，在这种情况下它将做的是在实现存储在`.cpp`文件中的同时生成密码。以下是包含`Generate()`功能实现的`pwgen_fn.cpp`文件列表:\n\n```cpp\n/* pwgen_fn.cpp */\n#include \"pwgen_fn.h\"\nstd::string PasswordGenerator::Generate(int passwordLength) {\n  int randomNumber;\n  std::string password;\n  std::srand(std::time(0));\n  for(int i=0; i < passwordLength; i++) {\n    randomNumber = std::rand() % 94 + 33;\n    password += (char) randomNumber;\n  }\n  return password;\n}\n```\n\n主入口文件`passgen.cpp`包含一个使用`PasswordGenerator`类的程序:\n\n```cpp\n/* passgen.cpp */\n#include <iostream>\n#include \"pwgen_fn.h\"\nint main(void) {\n  int passLen;\n  std::cout << \"Define password length: \";\n  std::cin >> passLen;\n  PasswordGenerator pg;\n  std::string password = pg.Generate(passLen);\n  std::cout << \"Your password: \"<< password << \"\\n\";\n  return 0;\n}\n```\n\n根据前面三个源文件，我们将生成一个可执行文件。为此，请转到命令提示符并在其中键入以下命令:\n\n```cpp\ng++ -Wall passgen.cpp pwgen_fn.cpp -o passgen\n\n```\n\n我没有得到任何警告或错误，所以即使你不应该。前面的命令编译`passgen.cpp`和`pwgen_fn.cpp`文件，然后将它们链接到一个名为`passgen.exe`的可执行文件。`pwgen_fn.h`文件，由于是和源文件同名的头文件，不需要在命令中声明相同。\n\n以下是在控制台窗口中键入`passgen`命令运行程序会得到的结果；每次运行程序时，您将获得不同的密码:\n\n![Compiling multiple source files](img/00009.jpeg)\n\n现在，是时候让我们剖析前面的源代码了。我们将从`pwgen_fn.h`文件开始，该文件只包含函数声明，如下所示:\n\n```cpp\nstd::string Generate(int);\n\n```\n\n从声明中可以看到，`Generate()`函数将有一个带有`int`类型的参数，并将返回`std::string`函数。我们没有在头文件中为参数定义名称，因为它将自动与源文件匹配。\n\n打开`pwgen_fn.cpp`文件，看到如下语句:\n\n```cpp\nstd::string PasswordGenerator::Generate(int passwordLength)\n\n```\n\n在这里，我们可以指定参数名称，即`passwordLength`。在这种情况下，我们可以有两个或多个同名的函数，只要它们在不同的类中。让我们看看下面的代码:\n\n```cpp\nint randomNumber;\nstd::string password;\n\n```\n\n我保留了名为`randomNumber`的变量来存储由`rand()`函数生成的随机数，并保留了`password`参数来存储由随机数转换而来的 ASCII 码。让我们看看下面的代码:\n\n```cpp\nstd::srand(std::time(0));\n\n```\n\n种子随机`srand()`函数与我们在前面的代码中用来生成随机种子的函数相同。我们使用它是为了在每次调用`rand()`函数时产生不同的数字。让我们看看下面的代码:\n\n```cpp\nfor(int i=0; i < passwordLength; i++) {\n randomNumber = std::rand() % 94 + 33;\n password += (char) randomNumber;\n}\nreturn password;\n\n```\n\n`for`迭代取决于用户定义的`passwordLength`参数。使用随机数生成器语句`std::rand() % 94 + 33`，我们可以根据其代码从 33 到 126 生成代表 ASCII 可打印字符的数字。关于 ASCII 码表的更多详细信息，可以去[http://en.wikipedia.org/wiki/ASCII](http://en.wikipedia.org/wiki/ASCII)。让我们看看下面的代码:\n\n```cpp\n#include \"pwgen_fn.h\"\n\n```\n\n`#include`头文件的单行会调用`pwgen_fn.h`文件中包含的所有头文件，所以我们不需要在这个源文件中声明包含的头文件，如下所示:\n\n```cpp\n#include <string>\n#include <cstdlib>\n#include <ctime>\n\n```\n\n现在，我们转到存储在`passgen.cpp`文件中的主入口代码:\n\n```cpp\nint passLen;\nstd::cout << \"Define password length: \";\nstd::cin >> passLen;\n\n```\n\n首先，用户决定他/她想要多长时间的密码，并且程序将其存储在`passLen`变量中:\n\n```cpp\nPasswordGenerator pg;\nstd::string password = pg.Generate(passLen);\nstd::cout << \"Your password: \"<< password << \"\\n\";\n\n```\n\n然后，程序实例化`PasswordGenerator`类，并调用`Generate()`函数产生一个长度为用户之前定义的密码。\n\n再看一下`passgen.cpp`文件，会发现 include 语句`#include <iostream>`(带尖括号)和`#include \"pwgen_fn.h\"`(带引号)两种形式是有区别的。通过在`#include`头文件语句中使用尖括号，编译器将查找系统头文件目录，但默认情况下不会在当前目录中查找。`#include`头文件语句中带引号，编译器会在当前目录中搜索头文件，然后查找系统头文件目录。\n\n## 分别编译和链接程序\n\n我们可以把一个大程序拆分成一组源文件，分别编译。假设我们有许多小文件，我们只想在其中一个文件中编辑一行，如果我们编译所有文件，而只需要修改一个文件，这将非常耗时。\n\n通过使用`-c`选项，我们可以编译单独的源代码来生成扩展名为`.o`的目标文件。在第一阶段，编译文件时不创建可执行文件。然后，在第二阶段，目标文件通过一个称为链接器的独立程序链接在一起。链接器将所有目标文件组合在一起，创建一个可执行文件。使用前面的`passgen.cpp`、`pwgen_fn.cpp`和`pwgen_fn.h`源文件，我们将尝试创建两个目标文件，然后将它们链接在一起以生成一个可执行文件。使用以下两个命令执行相同的操作:\n\n```cpp\ng++ -Wall -c passgen.cpp pwgen_fn.cpp\ng++ -Wall passgen.o pwgen_fn.o -o passgen\n\n```\n\n第一个命令使用`-c`选项，将创建两个与源文件名同名但扩展名不同的目标文件。第二个命令将它们链接在一起，并生成输出可执行文件，该文件的名称在`-o`选项之后，即`passgen.exe`文件。\n\n如果需要编辑`passgen.cpp`文件而不接触另外两个文件，只需要编译`passgen.cpp`文件，如下:\n\n```cpp\ng++ -Wall -c passgen.cpp\n\n```\n\n然后，您需要像前面的第二个命令一样运行链接命令。\n\n## 在 C++ 程序中检测到警告\n\n正如我们之前讨论的，编译器警告是确保代码有效性的重要辅助。现在，我们将尝试从我们创建的代码中找到错误。下面是一个 C++ 代码，它包含一个未初始化的变量，这会给我们一个不可预测的结果:\n\n```cpp\n/* warning.cpp */\n#include <iostream>\n#include <string>\nint main (void) {\n  std::string name;\n  int age;\n  std::cout << \"Hi \" << name << \", your age is \" << age << \"\\n\";\n}\n```\n\n然后，我们将运行以下命令来编译前面的`warning.cpp`代码:\n\n```cpp\ng++ -Wall -c warning.cpp\n\n```\n\n有时，我们无法检测到这个错误，因为它在第一眼看起来并不明显。但是，通过启用`-Wall`选项，我们可以防止错误，因为如果我们在启用警告选项的情况下编译前面的代码，编译器将产生警告消息，如以下代码所示:\n\n```cpp\nwarning.cpp: In function 'int main()':\nwarning.cpp:7:52: warning: 'age' may be used uninitialized in this function [-Wmaybe-uninitialized]\nstd::cout << \"Hi \" << name << \", your age is \" << age << \"\\n\";]\n\n```\n\n警告信息显示第 7 行第 52 列的`warning.cpp`文件中的`age`变量未初始化。GCC 产生的消息总是有**文件:行号:列号:错误类型:消息**形式。错误类型区分阻止成功编译的错误消息和指示可能的问题(但不阻止程序编译)的警告消息。\n\n显然，不检查编译器警告就开发程序是非常危险的。如果有任何未正确使用的功能，它们会导致程序崩溃或产生不正确的结果。打开编译器警告选项后，`-Wall`选项会捕捉 C++ 编程中出现的许多常见错误。\n\n# 了解 GCC C++ 编译器中的其他重要选项\n\nGCC 支持 **ISO C++ 1998** 、 **C++ 2003** ，也支持 4.9.2 版**c++ 2011**T13】标准。在 GCC 中选择该标准是使用以下选项之一完成的:`-ansi`、`-std=c++ 98`、`-std=c++ 03`或`–std=c++ 11`。让我们看看下面的代码，给它起个名字`hash.cpp`:\n\n```cpp\n/* hash.cpp */\n#include <iostream>\n#include <functional>\n#include <string>\nint main(void) {\n  std::string plainText = \"\";\n  std::cout << \"Input string and hit Enter if ready: \";\n  std::cin >> plainText;\n  std::hash<std::string> hashFunc;\n  size_t hashText = hashFunc(plainText);\n  std::cout << \"Hashing: \" << hashText << \"\\n\";\n  return 0;\n}\n```\n\n如果你编译并运行程序，它会给你一个每个纯文本用户输入的散列数。然而，编译前面的代码有点棘手。我们必须定义我们想要使用的国际标准化组织标准。让我们看一下以下五个编译命令，并在命令提示符窗口中逐一尝试它们:\n\n```cpp\ng++ -Wall hash.cpp -o hash\ng++ -Wall -ansi hash.cpp -o hash\ng++ -Wall -std=c++ 98 hash.cpp -o hash\ng++ -Wall -std=c++ 03 hash.cpp -o hash\ng++ -Wall -std=c++ 11 hash.cpp -o hash\n\n```\n\n当我们运行前面的前四个编译命令时，我们应该会得到以下错误消息:\n\n```cpp\nhash.cpp: In function 'int main()':\nhash.cpp:10:2: error: 'hash' is not a member of 'std'\n std::hash<std::string> hashFunc;\nhash.cpp:10:23: error: expected primary-expression before '>' token\n std::hash<std::string> hashFunc;\nhash.cpp:10:25: error: 'hashFunc' was not declared in this scope\n std::hash<std::string> hashFunc;\n\n```\n\n上面说`std`班没有`hash`。实际上，这是不正确的，因为自 C++ 2011 年以来，散列已经在标题`<string>`中定义了。为了解决这个问题，我们可以运行前面最后一个编译命令，如果它不再抛出错误，那么我们可以通过在控制台窗口中键入`hash`来运行程序。\n\n![Knowing other important options in the GCC C++ compiler](img/00010.jpeg)\n\n正如你在前面的截图中看到的，我调用了程序两次，并给出了 **Packt** 和 **packt** 作为输入。虽然我只是改变了一个字符，但整个散列发生了巨大的变化。这就是为什么散列被用来检测数据或文件的任何变化，如果它们被传输，只是为了确保数据不被改变。\n\n有关 GCC 中可用的 ISO C++ 11 特性的更多信息，请前往[http://gcc.gnu.org/projects/cxx0x.html](http://gcc.gnu.org/projects/cxx0x.html)。要获得标准要求的所有诊断，还应指定`-pedantic`选项(如果要将警告作为错误处理，则指定`-pedantic-errors`选项)。\n\n### 注\n\n仅`-ansi`选项不会导致非 ISO 程序被无端拒绝。为此，除了`-ansi`选项外，还需要`-pedantic`选项或`-pedantic-errors`选项。\n\n# GCC c++ 编译器中的故障排除\n\nGCC 提供了几个帮助和诊断选项，以帮助解决编译过程中的问题。您可以用来简化故障排除过程的选项将在接下来的章节中介绍。\n\n## 命令行选项的帮助\n\n使用 `help`选项获取顶级 GCC 命令行选项的摘要。该命令如下:\n\n```cpp\ng++ --help\n\n```\n\n要显示 GCC 及其相关程序选项的完整列表，如 GNU 链接器和 GNU 汇编器，请使用前面的`help`选项和详细(`-v`)选项:\n\n```cpp\ng++ -v --help\n\n```\n\n由前面的命令产生的选项的完整列表非常长，您可能希望使用`more`命令浏览它，或者将输出重定向到一个文件以供参考，如下所示:\n\n```cpp\ng++ -v --help 2>&1 | more\n\n```\n\n## 版本号\n\n您可以使用`version`选项找到您安装的 GCC 安装的版本号，如下命令所示:\n\n```cpp\ng++ --version\n\n```\n\n在我的系统中，如果我运行前面的命令，我将得到如下输出:\n\n```cpp\ng++ (x86_64-posix-seh-rev2, Built by MinGW-W64 project) 4.9.2\n\n```\n\n这取决于您在安装过程中调整的设置。\n\n在调查编译问题时，版本号很重要，因为旧版本的 GCC 可能会缺少程序使用的一些功能。版本号有`major-version.minor-version`或`major-version.minor-version.micro-version`形式，其中附加的第三个“微”版本号(如前面的命令所示)用于一个发布系列中后续的 bug 修复发布。\n\n## 冗长的编译\n\n`-v`选项也可以用来显示用于编译和链接程序的命令的确切顺序的详细信息。下面是一个例子，向您展示了`hello.cpp`程序的详细编译:\n\n```cpp\ng++ -v -Wall rangen.cpp\n\n```\n\n在此之后，您将在控制台中看到类似这样的内容:\n\n```cpp\nUsing built-in specs.\nCOLLECT_GCC=g++\nCOLLECT_LTO_WRAPPER=C:/mingw-w64/bin/../libexec/gcc/x86_64-w64-mingw32/4.9.2/lto-wrapper.exe\nTarget: x86_64-w64-mingw32\nConfigured with: ../../../src/gcc-4.9.2/configure –\n...Thread model: posix\ngcc version 4.9.2 (x86_64-posix-seh-rev2, Built by MinGW-W64 project)\n...\n\n```\n\n每当编译过程本身出现问题时，`-v`选项产生的输出会很有用。它显示用于搜索头文件和库的完整目录路径、预定义的预处理器符号以及用于链接的对象文件和库。\n\n# 总结\n\n我们成功地准备了 C++ 编译器，您学习了如何编译您使用编译器创建的源代码文件。每次编译源代码时，不要忘记使用`-Wall`(警告全部)选项，因为避免警告和细微错误很重要。此外，使用`-ansi`和`-pedantic`选项很重要，这样你的源代码可以在任何编译器中编译，因为它将检查 ANSI 标准并拒绝非 ISO 程序。\n\n现在，我们可以进入下一章学习网络概念，以便您了解网络体系结构，从而简化您的网络应用编程过程。"
  },
  {
    "path": "docs/boost-asio-cpp-net-prog-2e/2.md",
    "content": "# 二、理解网络概念\n\n在开始编写网络应用之前，我们最好先了解一下网络是如何工作的。在本章中，我们将挖掘网络概念及其内容。我们将在本章中讨论的主题如下:\n\n*   区分现场视察模型和传输控制协议模型\n*   探索 IPv4 和 IPv6 中的 IP 地址\n*   使用各种工具排除 TCP/IP 问题\n\n# 网络系统介绍\n\n网络架构由层和协议构成。架构中的每一个 **层**都有自己的角色，而其主要目的是为更高层提供一定的服务，并与相邻层进行的沟通。然而，**协议**是所有通信方用来标准化通信过程的规则和约定的集合。例如，当一个设备中的层 *n* 与另一个设备中的另一层 *n* 通信时，为了进行通信，它们必须使用相同的协议。\n\n现在流行的网络架构有两种:**开放系统互连** ( **OSI** )和 **TCP/IP** 参考模型。我们将更深入地了解每个参考模型及其优缺点，以便决定在我们的网络应用中应该使用哪个模型。\n\n## 现场视察参考模型\n\nOSI 模型用于连接到开放的系统——这些系统是开放的，并与其他系统通信。通过使用这种模型，我们不再依赖于操作系统，因此我们可以与任何计算机上的任何操作系统进行通信。该模型包含七层，每层都有特定的功能，并定义了在某些不同的层上处理数据的方式。该模型包含的七层是 **物理层****数据链路层****网络层****传输层****会话层****表示层**应用层**。**\n\n **### 物理层\n\n这是 OSI 模型中的第一个层，包含网络物理规范的定义，包括物理介质(电缆和连接器)和基本设备(中继器和集线器)。该层负责将输入的原始比特传输数据流转换成 0，并负责通信信道上的 1。然后，它将数据放在物理介质上。它关注数据传输的完整性，并确保从一台设备发送的位与另一台设备接收的数据完全相同。\n\n### 数据链路层\n\n数据链路层的主要作用是为原始数据传输提供链路。在传输数据之前，数据被分解成数据帧，数据链路层连续传输这些数据帧。如果服务可靠，接收器将为每个已发送的帧发回一个*确认帧*。\n\n该层由两个子层组成:**逻辑链路控制** ( **LLC** )和**媒体访问控制** ( **MAC** )。LLC 子层负责传输错误检查并处理帧传输，而 MAC 子层定义如何从物理介质中检索数据或将数据存储在物理介质中。\n\n我们还可以在这一层找到 MAC 地址，也称为**物理地址**。媒体访问控制地址用于识别连接到网络的每个设备，因为它对每个设备都是唯一的。使用命令提示符，我们可以通过在控制台窗口中键入以下命令来获取地址:\n\n```cpp\nipconfig /all\n\n```\n\n在忽略除 **Windows IP 配置**和**无线局域网适配器 Wi-Fi** 以外的所有其他信息后，我们将获得控制台输出，如下所示。我们可以在**物理地址**部分找到 MAC 地址，我自己的环境是 **80-19-34-CB-BF-FB** 。您将获得不同的结果，因为每个设备的 MAC 地址都是唯一的:\n\n```cpp\nWindows IP Configuration\n\n Host Name . . . . . . . . . . . . : HOST1\n Primary Dns Suffix  . . . . . . . :\n Node Type . . . . . . . . . . . . : Hybrid\n IP Routing Enabled. . . . . . . . : No\n WINS Proxy Enabled. . . . . . . . : No\n\nWireless LAN adapter Wi-Fi:\n Connection-specific DNS Suffix  . :\n Description . . . . . . . . . . . : Intel(R) Wireless-N 7260\n Physical Address. . . . . . . . . : 80-19-34-CB-BF-FB\n DHCP Enabled. . . . . . . . . . . : Yes\n Autoconfiguration Enabled . . . . : Yes\n Link-local IPv6 Address . . . . . : fe80::f14e:d5e6:aa0a:5855%3 (Preferred)\n IPv4 Address. . . . . . . . . . . : 192.168.1.4(Preferred)\n Subnet Mask . . . . . . . . . . . : 255.255.255.0\n Default Gateway . . . . . . . . . : 192.168.1.254\n DHCP Server . . . . . . . . . . . : 192.168.1.254\n DHCPv6 IAID . . . . . . . . . . . : 58726708\n DHCPv6 Client DUID. . . . . . . . : 00-01-00-01-1C-89-E6-3E-68-F7- 28-1E-61-66\n DNS Servers . . . . . . . . . . . : 192.168.1.254\n NetBIOS over Tcpip. . . . . . . . : Enabled\n\n```\n\n媒体访问控制地址包含 12 个十六进制字符，其中两位数字相互配对。前六位数字代表**组织唯一标识符**，其余数字代表**制造商序列号**。如果你真的很想知道这个号码是什么意思，可以去[www.macvendorlookup.com](http://www.macvendorlookup.com)把我们的 MAC 地址填入文本框，了解更多。在我自己的系统中，我获得了英特尔公司作为供应商公司名称，这与我安装的网卡的品牌相同。\n\n### 网络层\n\n网络层负责定义将数据包从源设备路由到目的设备的最佳方式。它将使用**互联网协议** ( **IP** )作为路由协议生成路由表，该 IP 地址用于确保数据到达所需目的地的路由。现在的 IP 有两个版本: **IPv4** 和 **IPv6** 。在 IPv4 中，我们使用 32 位地址来寻址协议，在 IPv6 中，我们使用 128 位地址。在下一个主题中，您将了解有关互联网协议、IPv4 和 IPv6 的更多信息。\n\n### 传输层\n\n传输层负责将数据从源传输到目的地。它会将数据分成更小的部分，或者在这种情况下是**段**，然后将所有段连接起来，将数据恢复到其在目的地的初始形式。\n\n工作在这一层的主要协议有两个:**传输控制协议** ( **TCP** )和**用户数据报协议** ( **UDP** )。TCP 通过建立会话来提供数据传输。在建立会话之前，不会传输数据。TCP 也被称为**面向连接的协议**，这意味着在传输数据之前必须建立会话。UDP 是一种尽最大努力传递数据的方法，但它不能保证传递，因为它不能建立会话。因此，UDP 也被称为**无连接协议**。关于 TCP 和 UDP 的深入解释可以在下一个主题中找到。\n\n### 会话层\n\n会话层负责会话的建立、维护和终止。我们可以将会话类比为网络上两个设备之间的连接。例如，如果我们想将一个文件从一台计算机发送到另一台计算机，在发送文件之前，该层将首先建立连接。然后，这一层将确保连接仍然正常，直到文件完全发送。最后，如果不再需要，该层将终止连接。我们谈论的联系是会话。\n\n该层还确保来自不同应用的数据不会互换。例如，如果我们同时运行互联网浏览器、聊天应用和下载管理器，该层将负责为每个应用建立会话，并确保它们与其他应用保持分离。\n\n该层使用三种通信方法:**单工**、**半双工**或**全双工**方法。在单纯形法中，数据只能由一方传输，因此另一方不能传输任何数据。这种方法不再普遍使用，因为我们需要可以相互交互的应用。在半双工方法中，任何数据都可以传输到所有相关设备，但在完成发送过程后，一次只能有一台设备传输数据。然后，其他人也可以发送和传输数据。全双工方法可以同时向所有设备传输数据。为了发送和接收数据，该方法使用不同的路径。\n\n### 表示层\n\n呈现层角色用于确定已经发送的数据，将数据翻译成合适的格式，然后呈现。例如，我们通过网络发送一个 MP3 文件，该文件被分成几个部分。然后，使用段上的头信息，该层将通过翻译段来构建文件。\n\n此外，该层负责数据压缩和解压缩，因为通过互联网传输的所有数据都被压缩以节省带宽。该层还负责数据的加密和解密，以保证两台设备之间的通信安全。\n\n### 应用层\n\n应用层处理用户使用的计算机应用。只有连接到网络的应用才会连接到该层。该层包含用户需要的几种协议，如下所示:\n\n*   **域名系统** ( **域名系统**):该协议是找到一个 IP 地址的主机名的协议。有了这个系统，我们不再需要记住每个 IP 地址，只需要记住主机名。我们很容易记住主机名中的一个单词，而不是 IP 地址中的一堆数字。\n*   **超文本传输协议** ( **HTTP** ):该协议是通过互联网在网页上传输数据的协议。我们还有 HTTPS 格式，用于发送安全问题的加密数据。\n*   **文件传输协议** ( **文件传输协议**):该协议用于将文件从文件传输协议服务器传输到文件传输协议服务器。\n*   **琐碎的 FTP** ( **TFTP** ):这个协议类似于 FTP，用来发送更小的文件。\n*   **动态主机配置协议** ( **DHCP** ):该协议是动态分配 TCP/IP 配置的方法。\n*   **邮局协议** ( **POP3** ):该协议是一个电子邮件协议用于从 POP3 服务器上取回电子邮件。服务器通常由**互联网服务提供商** ( **互联网服务提供商**)托管。\n*   **简单邮件传输协议** ( **SMTP** ):该协议与 POP3 形成对比，用于发送电子邮件。\n*   **互联网消息访问协议** ( **IMAP** ):该协议用于接收电子邮件消息。有了这个协议，用户可以将他们的电子邮件保存在本地计算机的文件夹中。\n*   **简单网络管理协议** ( **SNMP** ):该协议用于管理网络设备(路由器和交换机)发现问题并在问题变得严重之前进行报告。\n*   **服务器消息块** ( **中小企业**):该协议是一种文件传输协议，在微软网络上主要用于文件和打印机共享。\n\n该层还决定是否有足够的网络资源可用于网络访问。例如，如果您想使用互联网浏览器上网，应用层决定是否可以使用 HTTP 访问互联网。\n\n让我们看下图来看看 OSI 层包括哪些协议:\n\n![The Application layer](img/00011.jpeg)\n\n我们可以把这七层都分成两段层:**上层**和**下层**。上层负责与用户交互，不太关心底层细节，而下层负责通过网络传输数据，如格式化和编码。\n\n数据传输的格式因层而异。物理层有位，数据链路层有帧，等等。\n\n## TCP/IP 参考模型\n\nTCP/IP 模型是在 OSI 模型之前创建的。该模型的工作方式类似于现场视察模型，只是它只包含四层。TCP/IP 模型上的每一层都对应于现场视察模型的各层。TCP/IP 应用层映射了现场视察模型的 5 层、6 层和 7 层。传输控制协议传输层映射现场视察模型的第 4 层。TCP/IP 互联网层映射了现场视察模型的第 3 层。TCP/IP 链路层映射了现场视察模型的第 1 层和第 2 层。让我们看下图了解更多细节:\n\n![The TCP/IP reference model](img/00012.jpeg)\n\n这些是 TCP/IP 模型中每一层的主要角色:\n\n*   链路层负责确定数据传输过程中使用的协议和物理设备。\n*   互联网层负责通过寻址数据包来确定数据传输过程的最佳路由。\n*   传输层负责建立两台设备之间的通信并发送数据包。\n*   应用层负责向运行在计算机上的应用提供服务。由于缺少会话和演示层，应用必须包含在任何所需的会话和演示功能中。\n\n以下是 TCP/IP 模型中涉及的协议和设备:\n\n<colgroup class=\"calibre16\"><col class=\"calibre17\"> <col class=\"calibre17\"> <col class=\"calibre17\"></colgroup> \n| \n\n层\n\n | \n\n草案\n\n | \n\n设备\n\n |\n| --- | --- | --- |\n| 应用 | HTTP、HTTPS、SMTP、POP3 和 DNS | 代理服务器和防火墙 |\n| 运输 | TCP 和 UDP | - |\n| 互联网 | IP 和 ICMP | 路由器 |\n| 环 | 以太网、令牌环网和帧中继 | 集线器、调制解调器和中继器 |\n\n# 了解 TCP 和 UDP\n\n正如我们在本章前面的*传输层*一节中所讨论的，TCP 和 UDP 是用于通过网络传输数据的主要协议。它们的传递机制各不相同。TCP 在传输数据过程中有确认、序列号和流量控制，以提供有保证的传递，而 UDP 不提供有保证的传递，而是尽最大努力提供传递。\n\n## 传输控制协议\n\n在协议建立会话之前，TCP 执行三方握手过程。这样做是为了保证交货。请参考下图了解三向握手过程:\n\n![Transmission Control Protocol](img/00013.jpeg)\n\n从上图中，假设卡罗尔的设备想要向布莱恩的设备传输数据，并且他们需要执行三方握手过程。首先，卡罗尔的设备向布莱恩的设备发送一个数据包，并启用**同步** ( **同步**)标志。一旦布莱恩的设备接收到数据包，它将通过发送另一个同时启用了同步和确认标志的数据包来回复。最后，卡罗尔的设备通过发送启用确认标志的第三个数据包来完成握手过程。现在，两个设备都建立了会话，并保证另一个设备正在工作。然后，在会话建立后，数据传输准备就绪。\n\n### 类型\n\n在安全领域，我们知道术语“SYN-Flood”，这是一种拒绝服务攻击，攻击者向目标系统发送一连串 SYN 请求，试图消耗足够的服务器资源，使系统对合法流量无响应。攻击者只是发送同步，而没有发送预期的确认，导致服务器将同步确认发送到一个伪造的 IP 地址，该地址不会发送确认，因为它“知道”它从未发送过同步。\n\nTCP 还将数据分割成更小的数据段，并使用序列号来跟踪这些数据段。每个分离的段被分配不同的序列号，例如 1 到 20。然后，目标设备接收每个数据段，并使用序列号根据序列的顺序重组文件。\n\n例如，考虑卡罗尔想要从布莱恩的设备下载一个 JPEG 图像文件。在三方握手过程中建立会话后，两台设备确定单个数据段有多大，以及在确认之间需要发送多少数据段。一次可以发送的段的总数称为 TCP **滑动窗口**。如果在传输中有一个位被破坏或丢失，该段中的数据将不再有效。TCP 使用**循环冗余** **检查** ( **CRC** )通过验证每个数据段中的数据是否完整来识别损坏或丢失的数据。如果传输中有损坏或丢失的段，卡罗尔的设备将发送**否定确认** ( **NACK** )数据包，然后请求损坏或丢失的段；否则，Carol 的设备将发送 ACK 数据包并请求下一个数据段。\n\n## 用户数据报协议\n\nUDP 在发送数据之前不执行任何握手过程。它只是将数据直接发送到目的设备；但是，它会尽最大努力转发消息。想象一下，我们正在等待朋友的消息。我们打他/她的电话来接收我们的信息。如果我们的电话没有被接听，我们可以发送电子邮件或短信通知我们的朋友。如果我们的朋友不回复我们的电子邮件或短信，我们可以定期发送电子邮件。然而，我们谈到的所有技术都不能保证我们的信息被接收到。但是，我们仍然尽最大努力转发这条信息，直到它起作用。这是我们发送电子邮件的最大努力类比，类似于 UDP 的最大努力术语。它将尽最大努力确保数据被接收方接收，即使不能保证数据被接收。\n\n那么，为什么即使不可靠也要用 UDP 呢？有时，我们需要一种高速数据传输的通信，即使有一点数据损坏。例如，流式音频、流式视频和**IP 语音** ( **VoIP** )使用 UDP 来确保数据传输速度快。尽管 UDP 肯定丢失了数据包，但我们仍然能够清楚地获得所有消息。\n\n然而，尽管 UDP 在传输数据之前不检查连接，但它实际上使用校验和来验证数据。校验和可以通过比较校验和值来检查接收的数据是否被改变。\n\n## 了解港口\n\n在计算机网络中，**端口**是发送或接收数据的端点。端口通过其包含 16 位数字的**端口号**来识别。TCP 和 UDP 都使用逻辑端口号来跟踪数据包的内容，并帮助 TCP/IP 获得应用或服务的数据包，当设备接收到该数据包时，该数据包将处理该数据。\n\n共有`65536` TCP 端口和`65536` UDP 端口。我们可以将 TCP 端口分为三个端口范围，它们是:\n\n*   从`0`到`1023`的知名端口是已经通过**互联网** **指定号码授权** ( **IANA** )注册的端口，用于与特定协议或应用相关联。\n*   从`1024`到`49151`的注册端口是 IANA 已经为特定协议注册的端口，但是该范围内未使用的端口可以由计算机应用分配。\n*   从`49152`到`65535`的动态端口是未注册的端口，可以为任何目的分配。\n\n### 注\n\n要了解更多关于 TCP 和 UDP 中所有端口的详细信息，我们可以去[en.wikipedia.org/wiki/List_of_TCP_and_UDP_port_numbers](http://en.wikipedia.org/wiki/List_of_TCP_and_UDP_port_numbers)。还有，想知道所有 IANA 注册的指定港口，去[www.iana.org/assignments/port-numbers](http://www.iana.org/assignments/port-numbers)。\n\n为了理解端口概念，假设我们的计算机中安装了一个电子邮件客户端，例如雷鸟或微软 Outlook。现在，我们想向 Gmail 服务器发送一封电子邮件，然后从服务器获取所有传入的电子邮件，并将其保存在本地计算机上。发送电子邮件的步骤如下:\n\n1.  我们的电脑会随机分配一个未使用的端口号，如`48127`，将邮件发送到 Gmail SMTP 服务器的端口`25`。\n2.  当电子邮件到达 SMTP 服务器时，它识别出数据来自端口`25`，然后将数据转发给处理服务的 SMTP。\n3.  一旦收到电子邮件，服务器将确认发送到我们计算机中的端口`48127`，通知计算机电子邮件已经收到。\n4.  我们的电脑从端口`48127`完全收到确认后，会向邮件客户端发送一封邮件，邮件客户端再将邮件从发件箱移动到“已发送”文件夹。\n\n类似于发送电子邮件的步骤，要接收电子邮件，我们必须处理一个端口。其步骤如下:\n\n1.  我们的电脑会随机分配一个未使用的端口号，比如`48128`，向 Gmail POP3 服务器发送请求到端口`110`。\n2.  当电子邮件到达 POP3 服务器时，它识别出数据来自端口`110`，然后将数据转发给处理服务的 POP3。\n3.  然后，POP3 服务器通过端口`48128`向我们的计算机发送一封电子邮件。\n4.  我们的电脑收到来自端口`48128`的邮件后，会将邮件发送到我们的邮件客户端，然后移动到收件箱文件夹。它还会自动将邮件保存到本地计算机。\n\n# 探索互联网协议\n\nIP 是一种主要的通信协议，用于通过网络传送数据报。数据报本身是与分组交换网络相关联的传输单元。IP 的作用是根据 IP 地址将数据包从主机传送到主机，IP 地址在数据包的报头中说明。现在常用的 IP 有两个版本，分别是 IPv4 和 IPv6。\n\n## 互联网协议版本 4–IP v4\n\n自 20 世纪 80 年代以来，IPv4 已经成为标准的 IP 地址，用于通过网络获取从一台计算机到另一台计算机的 TCP/IP 流量。对于通过互联网连接的每台设备，IP 地址都是唯一的，所有设备只要拥有有效的 IP 地址，就可以通过互联网相互通信。\n\n一个有效的 IP 地址由四个十进制数构成，用三个点隔开。地址只包含从`0`到`255`的十进制数。我们可以说`10.161.4.25`是一个有效的 IP 地址，因为它包含在`0`到`255`之间的四个十进制数，并且用三个点隔开，而`192.2.256.4`是一个无效的 IP 地址，因为它包含大于`255`的十进制数。\n\n十进制数字实际上是从 8 个二进制数字转换而来的结果。所以，对于最大的 8 位数字，我们将有 1111 1111 或 255 个十进制数。这就是为什么 IP 地址中十进制数的范围是从 0 (0000 0000)到 255 (1111 1111)。\n\n要了解我们的 IP 地址配置，我们可以在命令提示符窗口中再次使用`ipconfig /all`命令。然后，它将显示如下输出:\n\n```cpp\nWireless LAN adapter Wi-Fi:\n Connection-specific DNS Suffix  . :\n Link-local IPv6 Address . . . . . : fe80::f14e:d5e6:aa0a:5855%3\n IPv4 Address. . . . . . . . . . . : 10.1.6.165\n Subnet Mask . . . . . . . . . . . : 255.255.255.0\n Default Gateway . . . . . . . . . : 10.1.6.1\n\n```\n\n输出将显示 IPv4 地址和 IPv6 地址中的 IP 地址。我们还可以看到，在我的设备中，`10.1.6.1`被用作系统的默认网关。`Default Gateway`参数是计算机网络上的一个点，用于为不匹配的 IP 地址或子网提供路径。\n\n一个 IP 地址必须包含这两个部分:一个**网络标识**来标识计算机所在的子网，一个**主机标识**来标识该子网内的计算机。每个网络标识表示网络子网中的一组主机。具有相同网络标识的设备必须具有唯一的主机标识。如果两台或多台设备具有相同的主机标识和相同的网络标识(所有四个十进制数字的 IP 地址都相同)，则会出现 IP 地址冲突。\n\n对于本地网络，**子网掩码**用于标识网络标识和主机标识在 IP 地址中的部分。以下是一些常见的子网掩码:\n\n*   `255.0.0.0`\n*   `255.255.0.0`\n*   `255.255.255.0`\n\n假设我们有 IP 地址`190.23.4.51`和子网掩码`255.255.0.0`。现在，我们可以使用布尔`AND`逻辑为子网掩码对应的 IP 地址的每一位找到网络标识。下表将 IP 地址和子网掩码转换为二进制数字，然后使用布尔`AND`逻辑找出网络标识:\n\n<colgroup class=\"calibre16\"><col class=\"calibre17\"> <col class=\"calibre17\"> <col class=\"calibre17\"> <col class=\"calibre17\"> <col class=\"calibre17\"></colgroup> \n|   | \n\n第 1 个字节\n\n | \n\n第二个字节\n\n | \n\n3 个字节\n\n | \n\n八月四日\n\n |\n| --- | --- | --- | --- | --- |\n| 190.23.4.51 | 1011 1110 | 0001 0111 | 0000 0100 | 0011 0011 |\n| 255.255.0.0 | 1111 1111 | 1111 1111 | 0000 0000 | 0000 0000 |\n| **网络 ID:** | 1011 1110 | 0001 0111 | 0000 0000 | 0000 0000 |\n\n从上表我们可以得到网络 ID，为`190.23.0.0`。\n\n子网掩码中必须应用相邻的最大数量。这意味着，如果决定使用第一个零，其余的数字必须为零。所以`255.0.255.0`的子网掩码无效。子网掩码也不允许以零开头。这意味着`0.255.0.0`的子网掩码也无效。\n\nIPv4 可以分为三个主要地址类别:A 类、B 类和 c 类。地址的类别由 IP 地址中的第一个数字定义，子网掩码是为每个类别预定义的。以下是每个类别的三个范围:\n\n<colgroup class=\"calibre16\"><col class=\"calibre17\"> <col class=\"calibre17\"> <col class=\"calibre17\"> <col class=\"calibre17\"></colgroup> \n| \n\n班级\n\n | \n\n第一个数字\n\n | \n\nIP 地址的范围\n\n | \n\n子网掩码\n\n |\n| --- | --- | --- | --- |\n| 甲级 | 1 至 126 | 1.0.0.0 到 126.255.255.254 | 255.0.0.0 |\n| 乙类 | 128 至 191 | 128.0.0.0 到 191.255.255.254 | 255.255.0.0 |\n| 丙类 | 192 至 223 | 192.0.0.0 到 223.255.255.254 | 255.255.255.0 |\n\n在转换 IP 地址中的第一个十进制数后，我们的计算机只需查看前两位就能确定 IP 地址的类别。例如，在范围为 1 到 126 的 A 类中，二进制数字在 0000 0001 到 0111 1110 之间。前两位可能是 0 和 0 或 0 和 1。范围从 128 到 191 的 B 类具有从 1000 0000 到 1011 1111 的二进制数字范围。这意味着最高的第一位总是 1，第二位总是 0。范围从 192 到 223 的 C 类具有从 1100 0000 到 1101 1111 的二进制数字范围。前两位的位都是 1。参考下表，总结计算机如何通过检查 IP 地址的前两位来确定 IP 地址的类别(这里，X 被忽略，可以是任何十六进制字符):\n\n<colgroup class=\"calibre16\"><col class=\"calibre17\"> <col class=\"calibre17\"></colgroup> \n| \n\n班级\n\n | \n\n二进制数字中的第一个数字\n\n |\n| --- | --- |\n| 甲级 | 00XXXXXX01XXXXXX |\n| 乙类 | 10XXXXXX |\n| 丙类 | 11XXXXXX |\n\n通过对 IP 地址进行分类，我们还可以仅通过查看 IP 地址来确定子网掩码，因为每个类都有不同的子网掩码，如下所示:\n\n<colgroup class=\"calibre16\"><col class=\"calibre17\"> <col class=\"calibre17\"> <col class=\"calibre17\"></colgroup> \n| \n\n班级\n\n | \n\n范围\n\n | \n\n子网掩码\n\n |\n| --- | --- | --- |\n| 甲级地址 | 0 -126 | 255.0.0.0 |\n| 乙类地址 | 128 至 191 | 255.255.0.0 |\n| 丙类地址 | 192 至 223 | 255.255.255.0 |\n\n通过知道子网掩码，我们可以很容易地知道网络 ID。假设我们有这三个 IP 地址:\n\n*   `174.12.1.8`\n*   `192.168.1.15`\n*   `10.70.4.13`\n\n现在，我们可以如下确定网络标识:\n\n<colgroup class=\"calibre16\"><col class=\"calibre17\"> <col class=\"calibre17\"> <col class=\"calibre17\"> <col class=\"calibre17\"></colgroup> \n| \n\nIP 地址\n\n | \n\n班级\n\n | \n\n子网掩码\n\n | \n\n网络标识\n\n |\n| --- | --- | --- | --- |\n| 174.12.1.8 | 乙类 | 255.255.0.0 | 174.12.0.0 |\n| 192.168.1.15 | 丙类 | 255.255.255.0 | 192.168.1.0 |\n| 10.70.4.13 | 甲级 | 255.0.0.0 | 10.0.0.0 |\n\n子网掩码还可以引用一个称为**无类域间路由** ( **CIDR** )的指示器，该指示器是根据位数定义的。例如，子网掩码`255.0.0.0`使用 8 位(值为`0`的位被视为未使用的位)，因此它被称为/8。同样，子网掩码 255.255.0.0 使用 16 位，可以引用为/16，子网掩码 255.255.255.0 使用 24 位，可以引用为/24。这些是我们之前的 IP 地址示例的 CIDR 符号:\n\n<colgroup class=\"calibre16\"><col class=\"calibre17\"> <col class=\"calibre17\"> <col class=\"calibre17\"></colgroup> \n| \n\n国际电脑互联网地址\n\n | \n\n子网掩码\n\n | \n\ncid 符号\n\n |\n| --- | --- | --- |\n| 174.12.1.8 | 255.255.0.0 | 174.12.1.8 /16 |\n| 192.168.1.15 | 255.255.255.0 | 192.168.1.15 /24 |\n| 10.70.4.13 | 255.0.0.0 | 10.70.4.13 /8 |\n\n## 互联网协议版本 6–IPv6\n\nIPv6 包含 128 位，是为了改进 IPv4 而推出的，IP v4 只有 32 位。IPv4 中有 32 位，可以寻址 4，294，967，296 个地址。一开始这个数字很高，但现在已经不够了，因为有很多设备需要一个 IP 地址。创建 IPv6 是为了解决这个问题，因为它可以寻址超过 340，000，000，000，000，000，000，000，000，000，000，或大约 *3.4028e+38* ，这已经足够了——至少目前是这样。\n\n### 注\n\nIPv5 已经被开发成由 64 位组成，但它从未被采用，因为人们认为如果使用互联网，它会很快耗尽 IP 地址。\n\nIPv4 地址和 IPv6 地址的显著区别在于，IPv6 不是以十进制数字显示 IP 地址，而是以十六进制字符表示地址。我们只要看这个格式号，一眼就能确定是 IPv4 还是 IPv6。我们可以调用`ipconfig /all`命令来知道我们的 IPv6 地址，并在以太网适配器网络中看到它。我有`fe80::f14e:d5e6:aa0a:5855%3`，但你的肯定不一样。地址本身是`fe80::f14e:d5e6:aa0a:5855`，最后一个`%3`变量是一个区域索引，用来标识网络接口卡。第一个 IPv6 地址中的数字`fe80`表示为链路本地地址，这是一个在网络上自动分配的 IP 地址，因为它不是由 DHCP 自动配置的，或者尚未手动配置。\n\n众所周知，IPv6 实际上是一组 128 位，为了简化符号，它将其位转换为十六进制字符。假设我们有一组形成 IPv6 的二进制数字，如下所示:\n\n```cpp\n0010 0000 0000 0001 0000 0000 0000 0000\n0000 0000 0000 0000 0000 0000 0000 0000\n0000 0000 0100 1111 0000 1001 0111 0011\n1111 0101 1111 1110 1111 1000 1011 0110\n```\n\n如果我们将其转换为 IPv6 地址格式，就更容易了，而不是记住所有这些数字。首先，我们将每个四位数组转换为十六进制字符，我们将得到这些十六进制字符:\n\n```cpp\n2001000000000000004f0973f5fef8b6\n```\n\n其次，我们用冒号分隔每组四个字符，如下所示:\n\n```cpp\n2001:0000:0000:0000:004f:0973:f5fe:f8b6\n```\n\n第三，我们可以抛出每个四位数集合中的前导零，如下所示:\n\n```cpp\n2001:0:0:0:4f:973:f5fe:f8b6\n```\n\n第四，我们将连续的零组折叠成一个空组，如下所示:\n\n```cpp\n2001::4f:973:f5fe:f8b6\n```\n\n现在我们更容易记住这个 IPv6 地址。\n\n### 注\n\n一个由两个冒号(`::`)表示的空组意味着根据需要插入尽可能多的零来将该地址组成 128 位。IPv6 地址不允许有多个空组，因为我们很难确定每个空组中有多少个零。\n\n同样，对于 IPv4，它通过查看第一个数字(实际上是前两位)来对 IP 地址进行分类，IPv6 的类型也可以通过查看其**前缀**来识别。这就是我们如何写网络标识`2001:04fe`以 32 位前缀开头的所有地址:\n\n```cpp\n2001:04fe:: /32\n```\n\n这意味着所有地址的前 32 位是 0010 0000 0000 0001 000 0100 1111 1110。然而，为了便于阅读这个地址，我们使用十六进制字符代替。\n\n# 使用 TCP/IP 工具进行故障排除\n\n以下一些命令可用于跟踪任何 TCP/IP 错误。这些命令可用于检查是否有任何路由器关闭或是否建立了任何连接。这将有助于我们决定正确的解决方案。\n\n## ipconfig 命令\n\n我们之前使用了 `ipconfig`命令来识别媒体访问控制地址和 IP 地址。除此之外，我们还可以使用这个命令来检查 TCP/IP 配置。我们也可以使用这个命令，如下面几节所述。\n\n### 显示全部配置信息\n\n为了完整地显示配置信息，我们可以在控制台上调用以下命令:\n\n```cpp\nipconfig /all\n\n```\n\n所有关于网络适配器的配置信息都将为我们显示，例如网络接口卡、无线卡和以太网适配器，就像我们在本章的*数据链路层*部分寻找 MAC 地址时已经尝试过的那样。\n\n### 显示域名系统\n\n以下命令将使用以下选项显示域名解析器缓存的内容:\n\n```cpp\nipconfig /displaydns\n\n```\n\n通过调用前面的命令，我们将获得关于本地系统中的域名系统的信息，如下所示:\n\n```cpp\nWindows IP Configuration\n\n ipv4only.arpa\n ----------------------------------------\n Record Name . . . . . : ipv4only.arpa\n Record Type . . . . . : 1\n Time To Live  . . . . : 77871\n Data Length . . . . . : 4\n Section . . . . . . . : Answer\n A (Host) Record . . . : 192.0.0.170\n\n Record Name . . . . . : ipv4only.arpa\n Record Type . . . . . : 1\n Time To Live  . . . . : 77871\n Data Length . . . . . : 4\n Section . . . . . . . : Answer\n A (Host) Record . . . : 192.0.0.171\n\n ieonlinews.microsoft.com\n ----------------------------------------\n Record Name . . . . . : ieonlinews.microsoft.com\n Record Type . . . . . : 1\n Time To Live  . . . . : 307\n Data Length . . . . . : 4\n Section . . . . . . . : Answer\n A (Host) Record . . . : 131.253.34.240\n\n```\n\n显示域名系统输出中每个字段的含义如下:\n\n*   **记录名称**:这是要与 IP 地址关联的 DNS 的名称。\n*   **记录类型**:这是记录的类型，用数字表示。\n*   **生存时间**:这是缓存过期时间，单位为秒。\n*   **数据长度**:这是以字节为单位存储记录值文本的内存大小。\n*   **段**:如果值为`Answer`，则表示回复实际查询，但是如果值为`Additional`，则表示包含查找实际答案所需的信息。\n*   **A(主机)记录**:这个是实际值存放的地方。\n\n### 刷新域名系统\n\n以下命令用于删除解析的域名系统服务器项目，但不删除缓存中的项目。在命令提示符下键入以下命令:\n\n```cpp\nipconfig /flushdns\n\n```\n\n一旦它成功刷新了 DNS 解析器缓存，我们将在控制台中看到以下消息:\n\n```cpp\nSuccessfully flushed the DNS Resolver Cache.\n\n```\n\n如果我们再次调用`ipconfig /displaydns`命令，解析的 DNS 服务器已经被移除，剩下的是缓存中的项目。\n\n### 更新 IP 地址\n\n有两个命令可以用来更新一个 IP 地址，它们是:\n\n```cpp\nipconfig /renew\n\n```\n\n前面的命令将从 DHCP 服务器更新 IPv4 的租用过程，而下面的命令将更新 IPv6 的租用过程:\n\n```cpp\nipconfig /renew6\n\n```\n\n### 释放 IP 地址\n\n使用以下两个命令分别释放从 DHCP 服务器获取的 IPv4 和 IPv6 的租用过程:\n\n```cpp\nipconfig /release\nipconfig /release6\n\n```\n\n这些命令只影响 DHCP 分配的(自动分配的)IP 地址。\n\n## ping 命令\n\n`ping` 命令用于检查与其他计算机的连通性。它使用**互联网控制消息协议** ( **ICMP** )向目标计算机发送消息。我们可以使用 IP 地址和主机名 ping 目标。假设我们有一个主机名为`HOST1`的设备，要 ping 它自己，我们可以使用以下命令:\n\n```cpp\nping HOST1\n\n```\n\n然后，我们将在控制台窗口中获得以下输出:\n\n```cpp\nPinging HOST1 [fe80::f14e:d5e6:aa0a:5855%3] with 32 bytes of data:\nReply from fe80::f14e:d5e6:aa0a:5855%3: time<1ms\nReply from fe80::f14e:d5e6:aa0a:5855%3: time<1ms\nReply from fe80::f14e:d5e6:aa0a:5855%3: time<1ms\nReply from fe80::f14e:d5e6:aa0a:5855%3: time<1ms\n\nPing statistics for fe80::f14e:d5e6:aa0a:5855%3:\n Packets: Sent = 4, Received = 4, Lost = 0 (0% loss),\nApproximate round trip times in milli-seconds:\n Minimum = 0ms, Maximum = 0ms, Average = 0ms\n\n```\n\n如果我们获得了 IPv6 地址，并希望将其显示在 IPv4 地址中，我们可以使用`-4`选项强制使用 IPv4 地址，如以下代码所示:\n\n```cpp\nping HOST1 -4\n\n```\n\n然后，我们将获得如下输出:\n\n```cpp\nPinging HOST1 [10.1.6.165] with 32 bytes of data:\nReply from 10.1.6.165: bytes=32 time<1ms TTL=128\nReply from 10.1.6.165: bytes=32 time<1ms TTL=128\nReply from 10.1.6.165: bytes=32 time<1ms TTL=128\nReply from 10.1.6.165: bytes=32 time<1ms TTL=128\n\nPing statistics for 10.1.6.165:\n Packets: Sent = 4, Received = 4, Lost = 0 (0% loss),\nApproximate round trip times in milli-seconds:\n Minimum = 0ms, Maximum = 0ms, Average = 0ms\n\n```\n\n但是，如果我们显示的是 IPv4 地址，而我们需要进入 IPv6 地址呢？我们可以使用`-6`选项强制使用 IPv6 地址，如下所示:\n\n```cpp\nping HOST1 -6\n\n```\n\n从`ping`命令，有两点发生。首先，名为`HOST1`的计算机被解析为`10.1.6.165`的 IP 地址。如果主机名解析不起作用，我们将得到如下错误:\n\n```cpp\nPing request could not find host HOST1\\. Please check the name and try again.\n\n```\n\n第二，该命令向`HOST1`发送四个数据包，接收四个数据包。该回复表示名为`HOST1`的计算机工作正常，能够响应命令请求。如果`HOST1`无法响应请求或被禁用，我们将看到如下输出:\n\n```cpp\nPinging HOST1 [10.1.6.165] with 32 bytes of data:\nRequest timed out.\nRequest timed out.\nRequest timed out.\nRequest timed out.\nPing statistics for 192.168.1.112:\n Packets: Sent = 4, Received = 0, Lost = 4 (100% loss),\n\n```\n\n发送`ping`命令时，我们可能会遇到一些错误信息，其中一些如下:\n\n*   **目的主机不可达**:表示路由有问题。这可能是因为本地计算机或远程计算机中的默认网关配置错误。\n*   **TTL 在传输中过期**:这表示 ping 过程通过的路由器数量大于 TTL(生存时间)值。每次 ping 通过路由器时，TTL 值都会递减。如果 ping 必须通过的路由器总数超过 TTL 值，将显示此错误消息。\n\n我们可以在 ping 命令中使用的另一个选项是`–t`。有了这个选项，`ping`命令不再发送四个数据包，而是继续发送数据包，直到用户按下 *Ctrl* + *C* 停止发送。这通常在我们等待断开状态转为连接状态时使用。我们可以将命令发送到控制台，如下所示:\n\n```cpp\nping HOST1 -t\n\n```\n\n## tracert 命令\n\n当我们有多台路由器时，我们可以使用`tracert`命令来跟踪数据包经过的路径。`tracert` 命令类似于`ping`命令，只是`tracert`有源设备和目的设备之间路由器的信息。这是我用来追踪从我的设备到[google.com](http://google.com)的通信轨迹的命令:\n\n```cpp\ntracert google.com\n\n```\n\n我在控制台窗口中获得了以下输出:\n\n```cpp\nTracing route to google.com [173.194.126.32]\nover a maximum of 30 hops:\n\n 1     1 ms     1 ms     1 ms  254.1.168.192.in-addr.arpa [192.168.1.254]\n 2    23 ms    26 ms     *     125.166.200.1\n 3     *        *      331 ms  189.subnet125-160-11.speedy.telkom.net.id [125.1\n 60.11.189]\n 4   293 ms    76 ms    84 ms  73.171.94.61.in-addr.arpa [61.94.171.73]\n 5   504 ms   612 ms   612 ms  61.94.117.229\n 6   698 ms   714 ms   209 ms  42.193.240.180.in-addr.arpa [180.240.193.42]\n 7     *        *        *     Request timed out.\n 8     *        *        *     Request timed out.\n 9     *      668 ms   512 ms  190.221.14.72.in-addr.arpa [72.14.221.190]\n 10     *        *        *     Request timed out.\n 11     *        *      582 ms  136.142.85.209.in-addr.arpa [209.85.142.136]\n 12   184 ms   202 ms   202 ms  233.242.85.209.in-addr.arpa [209.85.242.233]\n 13     *        *      563 ms  241.251.85.209.in-addr.arpa [209.85.251.241]\n 14   273 ms    96 ms    83 ms  kul01s08-in-f0.1e100.net [173.194.126.32]\n\nTrace complete.\n\n```\n\n可以看到有 14 行，每行代表一个**跳**(一种`ping`命令通过路由器的情况)。如果我们将一行除以一列，例如第四行，我们将得到下表:\n\n<colgroup class=\"calibre16\"><col class=\"calibre17\"> <col class=\"calibre17\"> <col class=\"calibre17\"> <col class=\"calibre17\"> <col class=\"calibre17\"></colgroup> \n| \n\nHop #\n\n | \n\nRTT1\n\n | \n\nRTT2\n\n | \n\nRTT3\n\n | \n\n名称/IP 地址\n\n |\n| --- | --- | --- | --- | --- |\n| four | 293 毫秒 | 76 毫秒 | 84 毫秒 | 61.94.171.73 |\n\n每行的解释如下:\n\n*   **跳数**:这是第一列，只是路线沿途的跳数。\n*   **RTT 栏目**:这是我们的包裹到达目的地返回电脑的**往返时间** ( **RTT** )。RRT 分为三列，因为 `tracecert`命令发送三个独立的信号包。这是为了显示路线的一致性，或缺乏一致性。\n*   **域/IP 栏**:这是路由器的 IP 地址。如果域名可用，也将通知。\n\n## 路径选择命令\n\n`pathping`命令用于验证路由路径。它像`tracert`命令一样检查两台设备的路由，然后像`ping`命令一样检查每台路由器的连通性。 `pathping`命令向每台路由器发送 100 条请求命令，期望得到 100 条回复。对于每个没有被回复的请求，`pathping`命令将把它算作 1%的数据丢失。因此，例如，如果有十个请求没有回复，将会有 10%的数据丢失。数据丢失的百分比越小，我们的连接就越好。\n\n我们将尝试借助以下命令将`pathping`命令发送到[google.com](http://google.com):\n\n```cpp\npathping google.com\n\n```\n\n通过这样做，我们将获得如下输出:\n\n```cpp\nTracing route to google.com [173.194.126.67]\nover a maximum of 30 hops:\n 0  HOST1 [10.1.7.101]\n 1  10.1.7.1\n 2  ns.csl-group.net [192.168.2.4]\n 3  101.255.54.25\n 4  115.124.80.209\n 5  peer-Exch-D2-out.tachyon.net.id [115.124.80.73]\n 6  ip-sdi.net.id [103.11.31.1]\n 7  ip-31-253.sdi.net.id [103.11.31.253]\n 8  209.85.243.158\n 9  216.239.40.129\n 10  209.85.242.243\n 11  209.85.251.175\n 12  kul06s05-in-f3.1e100.net [173.194.126.67]\n\nComputing statistics for 300 seconds...\n Source to Here   This Node/Link\nHop  RTT    Lost/Sent = Pct  Lost/Sent = Pct  Address\n 0                                           HOST1 [10.1.7.101]\n 0/ 100 =  0%   |\n 1   33ms     1/ 100 =  1%     1/ 100 =  1%  10.1.7.1\n 0/ 100 =  0%   |\n 2   24ms     1/ 100 =  1%     1/ 100 =  1%  ns.csl-group.net [192.168.2.4]\n 0/ 100 =  0%   |\n 3   19ms     1/ 100 =  1%     1/ 100 =  1%  101.255.54.25\n 0/ 100 =  0%   |\n 4   18ms     1/ 100 =  1%     1/ 100 =  1%  115.124.80.209\n 0/ 100 =  0%   |\n 5   33ms     1/ 100 =  1%     1/ 100 =  1%  peer-Exch-D2-out.tachyon.net.id [115.124.80.73]\n 0/ 100 =  0%   |\n 6   53ms     0/ 100 =  0%     0/ 100 =  0%  ip-sdi.net.id [103.11.31.1]\n 0/ 100 =  0%   |\n 7   38ms     2/ 100 =  2%     2/ 100 =  2%  ip-31-253.sdi.net.id [103.11.31.253]\n 0/ 100 =  0%   |\n 8   44ms     1/ 100 =  1%     1/ 100 =  1%  209.85.243.158\n 0/ 100 =  0%   |\n 9   59ms     0/ 100 =  0%     0/ 100 =  0%  216.239.40.129\n 4/ 100 =  4%   |\n 10  ---     100/ 100 =100%    96/ 100 = 96%  209.85.242.243\n 0/ 100 =  0%   |\n 11  ---     100/ 100 =100%    96/ 100 = 96%  209.85.251.175\n 0/ 100 =  0%   |\n 12   62ms     4/ 100 =  4%     0/ 100 =  0%  kul06s05-in-f3.1e100.net [173.194.126.67]\n\nTrace complete.\n\n```\n\n在第 10 行和第 11 行，我们获得了 100%的数据包丢失，因为发送到网络的数据包丢失了 100 个。然而，这是不可能的，因为数据没有到达目的路由器，因为 ICMP 被路由器阻止。使用此命令，我们可以确定在哪个特定路由器中会遇到大比例的数据丢失，尤其是在连接了许多路由器的大型网络中。\n\n我们也可以使用`–q`选项改变发送到路由器的请求数量。我们只需要在选项后说明新的请求数量，如下所示:\n\n```cpp\npathping -q 10 google.com\n\n```\n\n这将向路由器发送十个请求，而不是 100 个请求，速度会更快。\n\n## netstat 命令\n\n`netstat`(代表**网络统计**)命令通过显示当前设备中 TCP/IP 连接的所有信息，来查看 TCP/IP 统计。它将显示网络中涉及的连接、端口和应用的信息。我们可以通过在控制台窗口中键入以下命令来使用该命令:\n\n```cpp\nnetstat\n\n```\n\n之后，我们将获得如下输出所示的内容:\n\n```cpp\nActive Connections\n\n Proto  Local Address          Foreign Address        State\n TCP    127.0.0.1:50239        HOST1:50240            ESTABLISHED\n TCP    127.0.0.1:50240        HOST1:50239            ESTABLISHED\n TCP    127.0.0.1:50242        HOST1:50243            ESTABLISHED\n TCP    127.0.0.1:50243        HOST1:50242            ESTABLISHED\n TCP    127.0.0.1:60855        HOST1:60856            ESTABLISHED\n TCP    127.0.0.1:60856        HOST1:60855            ESTABLISHED\n TCP    127.0.0.1:60845        HOST1:60846            ESTABLISHED\n TCP    127.0.0.1:60846        HOST1:60845            ESTABLISHED\n TCP    192.168.1.4:50257      a72-246-188-35:http    ESTABLISHED\n TCP    192.168.1.4:50258      a72-246-188-35:http    ESTABLISHED\n TCP    192.168.1.4:50259      a72-246-188-35:http    ESTABLISHED\n TCP    192.168.1.4:50260      a104-78-107-69:http    ESTABLISHED\n TCP    192.168.1.4:50261      a72-246-188-35:http    TIME_WAIT\n TCP    192.168.1.4:50262      a72-246-188-35:http    ESTABLISHED\n TCP    192.168.1.4:50263      151:http               SYN_SENT\n TCP    [::1]:12372            HOST1:49567            ESTABLISHED\n TCP    [::1]:49567            HOST1:12372            ESTABLISHED\n\n```\n\n我们可以看到`netstat`命令的输出中有四列。每一栏的解释如下:\n\n*   **Proto** :显示协议名称，是 TCP 还是 UDP。\n*   **本地地址**:显示本地计算机的 IP 地址和使用的端口号。如果服务器正在监听所有接口，星号(`*`)将显示为主机名。如果端口尚未建立，端口号也将显示为星号。\n*   **对外地址**:显示插座连接的远程计算机的 IP 地址和端口号。如果端口尚未建立，端口号将显示为星号(`*`)。\n*   **状态**:表示一个 TCP 连接的状态。我们可能得到的状态如下:\n    *   **SYN_SEND** :这表示主动开放系统。\n    *   **SYN_RECEIVED** :表示服务器刚从客户端收到 SYN 。\n    *   **已建立**:表示客户端收到服务器的 SYN，会话建立。\n    *   **LISTEN** :表示服务器准备接受连接。\n    *   **FIN_WAIT_1** :这表示主动关闭系统。\n    *   **TIMED_WAIT** :表示主动关闭后客户端进入此状态。\n    *   **CLOSE_WAIT** :表示被动关闭，表示服务器刚从客户端收到第一个 FIN。\n    *   **FIN_WAIT_2** :这表示客户端刚刚收到服务器对其第一个 FIN 的确认。\n    *   **LAST_ACK** :这表示服务器发送自己的 FIN 时处于这种状态。\n    *   **CLOSED** :表示服务器收到客户端的 ACK，连接现在关闭。\n\n关于这些状态的更多细节和信息，可以到[tools.ietf.org/html/rfc793](http://tools.ietf.org/html/rfc793)查阅[第三章](3.html#page \"Chapter 3. Introducing the Boost C++ Libraries\")、*功能规范*。\n\n## 远程登录命令\n\n`telnet`(代表**终端网络**)命令用于通过 TCP/IP 网络访问远程计算机。在窗口中，有两个远程登录功能，即远程登录服务器和远程登录客户端。前者用于配置 Windows，以便侦听传入的连接并允许其他人使用它。而后者用于通过远程登录与任何服务器连接。\n\n默认情况下，由于存在安全风险，Windows 系统上不安装远程登录。禁用远程登录更安全，因为攻击者可以使用远程登录检查系统上的开放端口。然而，没有人能阻止我们在系统中安装它。我们可以通过执行以下步骤来做到这一点:\n\n1.  按*窗口* + *R* 打开**运行**窗口，在文本框中输入`%SYSTEMROOT%\\System32\\OptionalFeatures.exe`，然后按**确定**按钮。此时将打开**窗口功能**窗口。\n2.  Check **Telnet Client** and **Telnet Server** options, and then press the **OK** button to confirm the change. The checked option will look like the following screenshot:\n\n    ![The telnet command](img/00014.jpeg)\n\nTelnet 现在应该已经安装在我们的计算机上了。打开命令提示符窗口，运行以下命令启动远程登录:\n\n```cpp\ntelnet\n\n```\n\n按下*进入*后，会出现如下输出，光标在最后闪烁:\n\n```cpp\nWelcome to Microsoft Telnet Client\nEscape Character is 'CTRL+]'\nMicrosoft Telnet>_\n\n```\n\n现在，远程登录准备接收我们的命令。为了测试它，我们可以在其中运行各种命令。在远程登录中可用的命令的完整列表可以在[windows.microsoft.com/en-us/windows/telnet-commands](http://windows.microsoft.com/en-us/windows/telnet-commands)找到。\n\n# 总结\n\n在这一章中，当我们谈到网络体系结构时，我们知道了每一层在 OSI 和 TCP/IP 模型中的主要作用。我们探索了互联网协议，并能够区分 IPv4 和 IPv6 之间的区别。我们还能够确定子网掩码并对 IP 地址进行分类。此外，我们能够使用各种 TCP/IP 工具来检测错误是否发生。\n\n在下一章中，我们将讨论 Boost C++ 库，它将使我们在 C++ 编程中更有效率。现在，让我们准备好我们的编程工具，进入下一章。**"
  },
  {
    "path": "docs/boost-asio-cpp-net-prog-2e/3.md",
    "content": "# 三、Boost C++ 库简介\n\n许多程序员使用库，因为这简化了编程过程。因为他们不再需要从头开始编写函数，所以使用库可以节省很多代码开发时间。在本章中，我们将了解 Boost C++ 库。让我们准备自己的编译器和文本编辑器来证明 Boost 库的强大。在此过程中，我们将讨论以下主题:\n\n*   介绍 C++ 标准模板库\n*   介绍增强库\n*   在 MinGW 编译器中准备 Boost C++ 库\n*   构建增强库\n*   编译包含 Boost C++ 库的代码\n\n# 介绍 C++ 标准模板库\n\nC++ **标准模板库** ( **STL** )是一个基于通用模板的库，提供通用容器等。程序员可以轻松地使用 STL 提供的算法，而不是处理动态数组、链表、二叉树或哈希表。\n\nSTL 由容器、迭代器和算法构成，它们的作用如下:\n\n*   **容器**:它们的主要作用是管理某类对象的集合，比如整数数组或者字符串链表。\n*   **迭代器**:它们的主要作用是遍历集合的元素。迭代器的工作类似于指针。我们可以使用`++ `操作符增加迭代器，使用`*`操作符访问值。\n*   **算法**:它们的主要作用是处理集合的元素。算法使用迭代器遍历所有元素。在迭代元素之后，它处理每个元素，例如，修改元素。它还可以在迭代完所有元素后搜索和排序元素。\n\n让我们通过创建以下代码来检查构成 STL 的三个元素:\n\n```cpp\n/* stl.cpp */\n#include <vector>\n#include <iostream>\n#include <algorithm>\n\nint main(void) {\n  int temp;\n  std::vector<int> collection;\n  std::cout << \"Please input the collection of integer numbers, input 0 to STOP!\\n\";\n  while(std::cin >> temp != 0) {\n    if(temp == 0) break;\n    collection.push_back(temp);\n  }\n  std::sort(collection.begin(), collection.end());\n  std::cout << \"\\nThe sort collection of your integer numbers:\\n\";\n  for(int i: collection) {\n    std::cout << i << std::endl;\n  }\n}\n```\n\n命名前面的代码`stl.cpp`，运行下面的命令进行编译:\n\n```cpp\ng++ -Wall -ansi -std=c++ 11 stl.cpp -o stl\n\n```\n\n在我们剖析这段代码之前，让我们运行它看看会发生什么。这个程序会要求用户输入任意数量的整数，然后对数字进行排序。要停止输入并要求程序开始排序，用户必须输入`0`。这意味着`0`将不包括在排序过程中。由于我们不阻止用户输入非整数数字，如 3.14，在用户输入非整数数字后，程序将很快停止等待下一个数字。该代码产生以下输出:\n\n![Introducing the C++ standard template library](img/00015.jpeg)\n\n我们输入了六个整数:`43`、`7`、`568`、`91`、`2240`、`56`。最后一个条目是`0`以停止输入过程。然后程序开始对数字进行排序，我们得到按顺序排序的数字:`7`、`43`、`56`、`91`、`568`和`2240`。\n\n现在，让我们检查代码，以识别包含在 STL 中的容器、迭代器和算法:\n\n```cpp\nstd::vector<int> collection;\n\n```\n\n前面的代码片段包含来自 STL 的容器。有几个容器，我们在代码中使用一个 **向量**。一个向量在一个动态数组中管理它的元素，它们可以通过相应的索引被随机和直接地访问。在我们的代码中，容器准备容纳整数，所以我们必须定义尖括号`<int>`内的值的类型。这些尖括号在 STL 中也被称为**泛型**:\n\n```cpp\ncollection.push_back(temp);\nstd::sort(collection.begin(), collection.end());\n\n```\n\n前面代码中的 `begin()`和`end()`函数是 STL 中的算法。它们扮演着处理容器中的数据的角色，这些数据用于获取容器中的第一个和最后一个元素。在此之前，我们可以看到`push_back()`函数，该函数用于向容器追加一个元素:\n\n```cpp\nfor(int i: collection) {\n std::cout << i << std::endl;\n}\n\n```\n\n前面的`for`块将迭代称为`collection`的整数的每个元素。每次迭代元素时，我们可以单独处理该元素。在前面的例子中，我们向用户展示了这个数字。STL 中的迭代器就是这样发挥作用的。\n\n```cpp\n#include <vector>\n#include <algorithm>\n\n```\n\n我们包括定义所有`vector`函数的向量定义和调用`sort()`函数的`algorithm`定义。\n\n# 介绍 Boost C++ 库\n\nBoost C++ 库是一组补充 C++ 标准库的库。这个集合包含了一百多个库，我们可以用它们来提高我们在 C++ 编程中的生产力。当我们的需求超出了 STL 中的可用范围时，也会用到它。它在 Boost Licence 下提供源代码，这意味着它允许我们免费使用、修改和分发库，甚至用于商业用途。\n\nBoost 的开发由 Boost 社区处理，该社区由来自世界各地的 C++ 开发人员组成。社区的使命是开发高质量的库，作为 STL 的补充。只有经验证的库才会添加到 Boost 库中。\n\n### 注\n\n有关 Boost 库的详细信息，请访问[www.boost.org](http://www.boost.org)。如果你想为 Boost 贡献开发库，你可以加入[lists.boost.org/mailman/listinfo.cgi/boost](http://lists.boost.org/mailman/listinfo.cgi/boost)的开发者邮件列表。\n\n库的全部源代码可以在 github.com/boostorg 的官方网站上找到。\n\n## Boost 库的优势\n\n众所周知，使用 Boost 库将提高程序员的工作效率。此外，通过使用 Boost 库，我们将获得以下优势:\n\n*   它是开源的，所以我们可以检查源代码，并在需要时进行修改。\n*   它的许可证允许我们开发开源和闭源项目。它还允许我们自由地将我们的软件商业化。\n*   它有很好的文档记录，我们可以找到所有解释过的库，以及来自官方网站的示例代码。\n*   它几乎支持任何现代操作系统，比如 Windows 和 Linux。它还支持许多流行的编译器。\n*   它是 STL 的补充，而不是替代。这意味着使用 Boost 库将简化那些还没有被 STL 处理的编程过程。事实上，Boost 的许多部分都包含在标准的 C++ 库中。\n\n# 为 MinGW 编译器准备 Boost 库\n\n在我们通过使用 Boost 库来编程我们的 C++ 应用之前，需要配置这些库，以便被 MinGW 编译器识别。在这里，我们将准备我们的编程环境，以便我们的编译器能够使用 Boost 库。\n\n## 下载增强库\n\n下载 Boost 的最佳来源是官方下载页面。我们可以通过将我们的网络浏览器指向 www.boost.org/users/download T2 来到达那里。在**当前版本**部分找到**下载**链接。在写作的时候，Boost 库的当前版本是 1.58.0，但是当你读这本书的时候，版本可能已经改变了。如果是这样，您仍然可以选择当前版本，因为较高版本必须与较低版本兼容。但是，您必须进行调整，因为我们稍后将讨论设置。否则，选择同一个版本会让你很容易按照这本书的所有说明去做。\n\n有四种文件格式可供下载；分别是`.zip`、`.tar.gz`、`.tar.bz2`和`.7z`。这四个文件之间没有区别，只是文件大小不同。最大的文件是 ZIP 格式，最小的是 7Z 格式。由于文件大小，Boost 建议我们下载 7Z 格式。对比见下图:\n\n![Downloading Boost libraries](img/00016.jpeg)\n\n从前面的图片中，我们可以看到 ZIP 版本的大小为 123.1 MB，而 7Z 版本的大小为 65.2 MB。这意味着 ZIP 版本的大小几乎是 7Z 版本的两倍。因此，他们建议您选择 7Z 格式，以减少下载和解压缩时间。让我们选择`boost_1_58_0.7z`进行下载并保存到我们的本地存储中。\n\n## 部署增强库\n\n当我们在本地存储中获得`boost_1_58_0.7z`后，使用 7ZIP 应用对其进行解压缩，并将解压缩文件保存到`C:\\boost_1_58_0`。\n\n### 注\n\n7ZIP 应用可以从[www.7-zip.org/download.html](http://www.7-zip.org/download.html)获取。\n\n该目录应该包含如下文件结构:\n\n![Deploying Boost libraries](img/00017.jpeg)\n\n### 注\n\n我们可以直接去[sourceforge.net/projects/boost/files/boost/1.58.0](http://sourceforge.net/projects/boost/files/boost/1.58.0)而不是浏览到 Boost 下载页面，手动搜索 Boost 版本。当 1.58.0 版本不再是当前版本时，这将非常有用。\n\n## 使用增强库\n\nBoost 中的大部分库都是**头文件**；这意味着所有函数的声明和定义，包括名称空间和宏，对编译器都是可见的，不需要单独编译它们。我们现在可以尝试使用程序的 Boost 将字符串转换为`int`值，如下所示:\n\n```cpp\n/* lexical.cpp */\n#include <boost/lexical_cast.hpp>\n#include <string>\n#include <iostream>\n\nint main(void) {\n  try \t{\n    std::string str;\n    std::cout << \"Please input first number: \";\n    std::cin >> str;\n    int n1 = boost::lexical_cast<int>(str);\n    std::cout << \"Please input second number: \";\n    std::cin >> str;\n    int n2 = boost::lexical_cast<int>(str);\n    std::cout << \"The sum of the two numbers is \";\n    std::cout << n1 + n2 << \"\\n\";\n    return 0;\n  }\n  catch (const boost::bad_lexical_cast &e) {\n    std::cerr << e.what() << \"\\n\";\n    return 1;\n  }\n}\n```\n\n打开记事本++ 应用，输入前面的代码，并将其保存为`C:\\CPP`中的`lexical.cpp`—我们在[第 1 章](1.html#page \"Chapter 1. Simplifying Your Network Programming in C++\")、*中创建的目录，用 C++ 简化您的网络编程*。现在打开命令提示符，将活动目录指向`C:\\CPP`，然后键入以下命令:\n\n```cpp\ng++ -Wall -ansi lexical.cpp –Ic:\\boost_1_58_0 -o lexical\n\n```\n\n我们这里有一个新的选项，就是`–I`(“包含”选项)。此选项与目录的完整路径一起使用，以通知编译器我们有另一个标题目录要包含在代码中。由于我们将 Boost 库存储在`c:\\ boost_1_58_0`中，因此我们可以使用`–Ic:\\boost_1_58_0`作为附加参数。\n\n在`lexical.cpp`中，我们应用`boost::lexical_cast`将`string`类型数据转换为`int`类型数据。程序会要求用户输入两个数字，然后自动找出两个数字的总和。如果用户输入了不合适的号码，它会通知他们发生了错误。\n\nBoost 提供了`Boost.LexicalCast`库，用于将一种数据类型转换为另一种数据类型(将数值类型如`int`、`double`或`floats`转换为`string`类型，反之亦然)。现在，让我们仔细分析一下`lexical.cpp`来更详细地了解它的功能:\n\n```cpp\n#include <boost/lexical_cast.hpp>\n#include <string>\n#include <iostream>\n\n```\n\n我们包括`boost/lexical_cast.hpp`以便能够调用`boost::lexical_cast`函数，因为该函数在`lexical_cast.hpp`中声明。我们还使用`string`表头应用`std::string`功能，以及`iostream`表头应用`std::cin`、`std::cout`和`std::cerr`功能。\n\n其他功能，比如`std::cin`、`std::cout`，在[第一章](1.html#page \"Chapter 1. Simplifying Your Network Programming in C++\")、*中已经讲过了，在 C++* 中简化你的网络编程，我们看到了它们的功能是什么，所以我们可以跳过那些行:\n\n```cpp\nint n1 = boost::lexical_cast<int>(str);\nint n2 = boost::lexical_cast<int>(str);\n\n```\n\n我们使用前面两个单独的行将用户提供的输入`string`转换为`int`数据类型。然后，在转换数据类型之后，我们总结了两个`int`值。\n\n我们还可以看到前面代码中的块。如果用户输入了一个不合适的数字，除了 0 到 9，它用来捕捉错误。\n\n```cpp\ncatch (const boost::bad_lexical_cast &e)\n{\n std::cerr << e.what() << \"\\n\";\n return 1;\n}\n\n```\n\n前面的代码片段将捕捉错误，并通过使用`boost::bad_lexical_cast`通知用户错误消息到底是什么。我们调用`e.what()`函数获取错误消息的字符串。\n\n现在让我们通过在命令提示符下键入`lexical`来运行应用。我们将获得如下输出:\n\n![Using Boost libraries](img/00018.jpeg)\n\n我把`10`作为第一次输入，`20`作为第二次输入。结果是`30`因为它只是把两个输入加起来。但是如果我输入一个非数值会发生什么，例如`Packt`。以下是尝试该条件的输出:\n\n![Using Boost libraries](img/00019.jpeg)\n\n一旦应用发现错误，将忽略下一条语句，直接进入`catch`块。通过使用`e.what()`功能，应用可以获得错误消息并将其显示给用户。在我们的示例中，我们将`bad lexical cast: source type value could not be interpreted`作为错误消息的目标，因为我们试图将`string`数据分配给`int`类型变量。\n\n## 建立助推库\n\n正如我们之前讨论的一样，Boost 中的大多数库都是只有头文件的，但不是全部。有些库必须单独建造。它们是:\n\n*   `Boost.Chrono`:这是用来显示时钟的种类，比如当前时间，两次之间的范围，或者计算过程中经过的时间。\n*   `Boost.Context`:这是用来创建更高级别的抽象，比如协同程序和协同线程。\n*   `Boost.Filesystem`:用于处理文件和目录，如获取文件路径或检查文件或目录是否存在。\n*   `Boost.GraphParallel`:这是**增强图形库** ( **BGL** )的扩展，用于并行和分布式计算。\n*   `Boost.IOStreams`:这是用来使用 stream 读写数据的。例如，它将文件内容加载到内存或以 GZIP 格式写入压缩数据。\n*   `Boost.Locale`:这是用来本地化应用，换句话说就是把应用界面翻译成用户的语言。\n*   `Boost.MPI`:这是用来开发并发执行任务的程序。 **MPI 本身代表消息传递接口**。\n*   `Boost.ProgramOptions`:用于解析命令行选项。它使用双减号(`--`)来分隔每个命令行选项，而不是使用`main`参数中的`argv`变量。\n*   `Boost.Python`:这是用来解析 C++ 代码中 Python 语言的。\n*   `Boost.Regex`:这是用来在我们的代码中应用正则表达式的。但是如果我们的开发支持 C++ 11，我们不再依赖`Boost.Regex`库，因为它在`regex`头文件中可用。\n*   `Boost.Serialization`:这是用来把对象转换成一系列的字节，可以保存，然后再次还原成同一个对象。\n*   `Boost.Signals`:这是用来创造信号的。该信号将触发一个事件，使在其上运行一个功能。\n*   `Boost.System`:这是用来定义错误。包含`system::error_code`、`system::error_category`、`system::error_condition`和`system::system_error`四个等级。所有这些类都在`boost`命名空间内。C++ 11 环境中也支持，但是因为很多 Boost 库都使用`Boost.System`，所以需要保持包含`Boost.System`。\n*   `Boost.Thread`:这是用来应用线程编程。它提供了同步多线程数据访问的类。在 C++ 11 环境中，`Boost.Thread`库提供了扩展，所以我们可以在`Boost.Thread`中中断线程。\n*   `Boost.Timer`:这是用时钟来衡量代码性能的。它根据通常的时钟和中央处理器时间来测量经过的时间，这表明执行代码花费了多少时间。\n*   `Boost.Wave`:这个提供了一个可重用的 C 预处理器，我们可以在我们的 C++ 代码中使用。\n\n还有一些库有可选的、单独编译的二进制文件。它们如下:\n\n*   `Boost.DateTime`:用于处理时间数据；例如，日历日期和时间。它有一个二进制组件，只有当我们使用`to_string`、`from_string`或序列化特性时才需要它。如果我们的应用以 Visual C++ 6.x 或 Borland 为目标，也需要它。\n*   `Boost.Graph`:是用来创建二维图形的。它有一个二进制组件，只有当我们打算解析`GraphViz`文件时才需要它。\n*   `Boost.Math`:用于处理数学公式。它具有用于`cmath`功能的二进制组件。\n*   `Boost.Random`:用于生成随机数。它有一个二进制组件，只有当我们想使用`random_device`时才需要它。\n*   `Boost.Test`:用于编写和组织测试程序及其运行时执行。它可以在只有头文件的模式下使用，也可以单独编译的模式下使用，但是严重的话建议单独编译。\n*   `Boost.Exception`:用于在异常抛出后给异常添加数据。它为 32 位`_MSC_VER==1310`和`_MSC_VER==1400`提供了`exception_ptr`的非侵入式实现，这需要单独编译的二进制文件。这是由`#define BOOST_ENABLE_NON_INTRUSIVE_EXCEPTION_PTR`启用的。\n\n让我们尝试重新创建我们在[第 1 章](1.html#page \"Chapter 1. Simplifying Your Network Programming in C++\")*中创建的随机数生成器程序，用 C++ 简化您的网络编程*。但是现在我们将使用`Boost.Random`库，而不是 C++ 标准函数中的`std::rand()`。让我们看看下面的代码:\n\n```cpp\n/* rangen_boost.cpp */\n#include <boost/random/mersenne_twister.hpp>\n#include <boost/random/uniform_int_distribution.hpp>\n#include <iostream>\n\nint main(void) {\n  int guessNumber;\n  std::cout << \"Select number among 0 to 10: \";\n  std::cin >> guessNumber;\n  if(guessNumber < 0 || guessNumber > 10) {\n    return 1;\n  }\n  boost::random::mt19937 rng;\n  boost::random::uniform_int_distribution<> ten(0,10);\n  int randomNumber = ten(rng);\n  if(guessNumber == randomNumber) {\n    std::cout << \"Congratulation, \" << guessNumber << \" is your lucky number.\\n\";\n  }\n  else {\n    std::cout << \"Sorry, I'm thinking about number \" << randomNumber << \"\\n\"; \n  }\n  return 0;\n}\n```\n\n我们可以使用以下命令编译前面的源代码:\n\n```cpp\ng++ -Wall -ansi -Ic:/boost_1_58_0 rangen_boost.cpp -o rangen_boost\n\n```\n\n现在，让我们运行程序。不幸的是，在我运行程序的三次中，我总是获得相同的随机数，如下所示:\n\n![Building Boost libraries](img/00020.jpeg)\n\n从这个例子中我们可以看到，我们总是得到数字 8。这是因为我们应用了 Mersenne Twister，一个**伪随机数发生器** ( **PRNG** )，它使用默认种子作为随机性的来源，所以每次程序运行时都会生成相同的数字。当然，这不是我们期望的计划。\n\n现在，我们将再次修改程序，只用两行。首先，找到下面一行:\n\n```cpp\n#include <boost/random/mersenne_twister.hpp>\n\n```\n\n更改如下:\n\n```cpp\n#include <boost/random/random_device.hpp>\n\n```\n\n接下来，找到下面一行:\n\n```cpp\nboost::random::mt19937 rng;\n\n```\n\n更改如下:\n\n```cpp\nboost::random::random_device rng;\n\n```\n\n然后，将文件保存为`rangen2_boost.cpp`，并使用我们编译`rangen_boost.cpp`时使用的命令编译`rangen2_boost.cpp`文件。该命令如下所示:\n\n```cpp\ng++ -Wall -ansi -Ic:/boost_1_58_0 rangen2_boost.cpp -o rangen2_boost\n\n```\n\n可悲的是，将会有一些错误，编译器将显示以下错误消息:\n\n```cpp\ncc8KWVvX.o:rangen2_boost.cpp:(.text$_ZN5boost6random6detail20generate_uniform_intINS0_13random_deviceEjEET0_RT_S4_S4_N4mpl_5bool_ILb1EEE[_ZN5boost6random6detail20generate_uniform_intINS0_13random_deviceEjEET0_RT_S4_S4_N4mpl_5bool_ILb1EEE]+0x24f): more undefined references to boost::random::random_device::operator()()' follow\ncollect2.exe: error: ld returned 1 exit status\n\n```\n\n这是因为，正如我们前面看到的，如果我们想使用`random_device`属性，那么`Boost.Random`库需要单独编译。\n\nBoost 库有一个自己编译或构建 Boost 的系统，叫做`Boost.Build`库。安装`Boost.Build`库需要完成两个步骤。首先，通过将命令提示符下的活动目录指向`C:\\boost_1_58_0`并键入以下命令来运行**引导程序**:\n\n```cpp\nbootstrap.bat mingw\n\n```\n\n我们使用我们在[第 1 章](1.html#page \"Chapter 1. Simplifying Your Network Programming in C++\")、*中安装的 MinGW 编译器，作为我们编译 Boost 库的工具集。稍等片刻，如果该过程成功，我们将获得以下输出:*\n\n```cpp\nBuilding Boost.Build engine\n\nBootstrapping is done. To build, run:\n\n    .\\b2\n\nTo adjust configuration, edit 'project-config.jam'.\nFurther information:\n\n    - Command line help:\n    .\\b2 --help\n\n    - Getting started guide:\n    http://boost.org/more/getting_started/windows.html\n\n    - Boost.Build documentation:\n    http://www.boost.org/build/doc/html/index.html\n```\n\n在这一步中，我们将在 Boost 库的根目录中找到四个新文件。它们是:\n\n*   `b2.exe`:这是一个构建 Boost 库的可执行文件\n*   `bjam.exe`:这个和`b2.exe`一模一样，不过是旧版\n*   `bootstrap.log`:包含`bootstrap`过程的日志\n*   `project-config.jam`:这包含了一个设置，当我们运行`b2.exe`时，这个设置将用于构建过程中\n\n我们还发现这一步在`C:\\boost_1_58_0\\tools\\build\\src\\engine\\bin.ntx86`中创建了一个新的目录，其中包含了一堆与需要编译的 Boost 库相关的`.obj`文件。\n\n之后，通过在命令提示符下键入以下命令来运行第二步:\n\n```cpp\nb2 install toolset=gcc\n\n```\n\n运行该命令后，给自己倒杯咖啡，因为根据您的系统规格，完成该过程大约需要 20 到 50 分钟。我们将获得的最后一个输出如下:\n\n```cpp\n...updated 12562 targets...\n\n```\n\n这意味着这个过程已经完成，我们现在已经构建了 Boost 库。如果我们签入我们的浏览器，`Boost.Build`库会添加`C:\\boost_1_58_0\\stage\\lib`，它包含了我们可以直接在程序中使用的静态和动态库的集合。\n\n### 注\n\n`bootstrap.bat`和`b2.exe`使用`msvc`(微软 Visual C++ 编译器)作为默认工具集，很多 Windows 开发者的机器上已经安装了`msvc`。由于我们已经安装了 GCC 编译器，我们在 Boost 的构建中设置了`mingw`和`gcc`工具集选项。如果您也安装了`mvsc`并想在 Boost 的构建中使用它，可以省略工具集选项。\n\n现在，让我们再次尝试编译`rangen2_boost.cpp`文件，但是现在使用以下命令:\n\n```cpp\nc:\\CPP>g++ -Wall -ansi -Ic:/boost_1_58_0 rangen2_boost.cpp -Lc:\\boost_1_58_0\\stage\\lib -lboost_random-mgw49-mt-1_58 -lboost_system-mgw49-mt-1_58 -o rangen2_boost\n\n```\n\n我们这里有两个新选项，分别是`–L`和`–l`。如果库文件不在活动目录中，则`-L`选项用于定义包含库文件的路径。`–l`选项用于定义库文件的名称，但省略了文件名前的第一个`lib`字。在这种情况下，原始库文件名为`libboost_random-mgw49-mt-1_58.a`，我们省略了`lib`短语和选项`-l`的文件扩展名。\n\n名为`rangen2_boost.exe`的新文件将在`C:\\CPP`中创建。但是在我们运行程序之前，我们必须确保程序安装的目录包含程序所依赖的两个库文件。这些是`libboost_random-mgw49-mt-1_58.dll`和`libboost_system-mgw49-mt-1_58.dll`，我们可以从库目录`c:\\boost_1_58_0_1\\stage\\lib`中找到它们。\n\n为了方便我们运行该程序，运行以下`copy`命令将两个库文件复制到`C:\\CPP`:\n\n```cpp\ncopy c:\\boost_1_58_0_1\\stage\\lib\\libboost_random-mgw49-mt-1_58.dll c:\\cpp\ncopy c:\\boost_1_58_0_1\\stage\\lib\\libboost_system-mgw49-mt-1_58.dll c:\\cpp\n\n```\n\n现在程序应该运行顺利了。\n\n为了创建一个网络应用，我们将使用`Boost.Asio`库。我们在非头文件库中找不到`Boost.Asio`——我们要用来创建网络应用的库。似乎我们不需要建立 Boost 库，因为`Boost.Asio`是只有头文件的库。这是真的，但是由于`Boost.Asio`依赖于`Boost.System`并且`Boost.System`需要在使用之前构建，所以在我们可以使用它来创建我们的网络应用之前，首先构建 Boost 是很重要的。\n\n### 类型\n\n对于选项`–I`和`–L`，编译器不在乎我们是否使用反斜杠(\\)或斜杠(/)来分隔路径中的每个目录名，因为编译器可以同时处理 Windows 和 Unix 路径样式。\n\n# 总结\n\n我们看到 Boost C++ 库是为了补充标准 C++ 库而开发的。我们还能够设置我们的 MinGW 编译器，以便编译包含 Boost 库的代码，并构建必须单独编译的库的二进制文件。下一章谈到`Boost.Asio`库(我们将要用来开发网络应用的库)，我们将具体研究 Boost 库。请记住，虽然我们可以使用`Boost.Asio`库作为仅头库，但最好使用`Boost.Build`库来构建所有的 Boost 库。我们将很容易使用所有的库，而不用担心编译失败。"
  },
  {
    "path": "docs/boost-asio-cpp-net-prog-2e/4.md",
    "content": "# 四、Boost.Asio 入门\n\n我们已经大致了解了 Boost C++ 库。现在是时候了解更多关于 Boost 的信息了.Asio，我们用来开发网络应用的库。助推.Asio 是用来异步处理数据的库的集合，因为 Asio 本身代表**异步 I/O** ( **输入输出**)。异步意味着程序中的特定任务将在不阻塞其他任务和 Boost 的情况下运行.Asio 将在完成任务后通知程序。换句话说，任务是并发执行的。\n\n在本章中，我们将讨论以下主题:\n\n*   区分并发和非并发编程\n*   了解输入/输出服务、Boost 的大脑和心脏.Asio\n*   将函数动态绑定到函数指针\n*   同步访问任何全局数据或共享数据\n\n# 越来越接近助推.Asio 库\n\n假设我们正在开发一个音频下载器应用，我们希望用户能够导航到应用中的所有菜单，即使下载过程正在进行中。如果我们不使用异步编程，应用将被下载过程阻止，用户将不得不等到文件下载完成。但是由于异步编程，用户不需要等到下载过程完成才能继续使用应用。\n\n换句话说，一个同步的过程就像是在剧场售票线上排队。只有当我们到达售票柜台时，我们才会得到服务，在此之前，我们必须等待排在我们前面的前顾客的所有流程完成。相比之下，我们可以想象异步过程就像在餐厅用餐，服务员不必等待顾客的订单由厨师准备。服务员可以去接其他顾客的订单，而不是耽误时间等厨师。\n\n`Boost`库也有用来并发执行任务的`Boost.Thread`库，但是`Boost.Thread`库是用来访问内部资源的，比如 CPU 核心资源，而`Boost.Asio`库是用来访问外部资源的，比如网络连接，因为数据是通过网卡收发的。\n\n让我们区分和并发和非并发编程。请看下面的代码:\n\n```cpp\n/* nonconcurrent.cpp */\n#include <iostream>\n\nvoid Print1(void) {\n  for(int i=0; i<5; i++) {\n    std::cout << \"[Print1] Line: \" << i << \"\\n\";\n  }\n}\n\nvoid Print2(void) {\n  for(int i=0; i<5; i++) {\n    std::cout << \"[Print2] Line: \" << i << \"\\n\";\n  }\n}\n\nint main(void) {\n  Print1();\n  Print2();\n  return 0;\n}\n```\n\n前面的代码是非电流程序。将代码保存为`nonconcurrent.cpp`，然后使用以下命令进行编译:\n\n```cpp\ng++ -Wall -ansi nonconcurrent.cpp -o nonconcurrent\n\n```\n\n运行`nonconcurrent.cpp`后，会有这样的输出显示在你面前:\n\n![Getting closer to the Boost.Asio library](img/00021.jpeg)\n\n我们要运行两个功能:`Print1()`和`Print2()`。在非电流编程中，应用首先运行`Print1()`功能，然后完成功能中的所有指令。程序继续调用`Print2()`功能，直到指令完全运行。\n\n现在，让我们比较非并发编程和并发编程。为此，请看下面的代码:\n\n```cpp\n/* concurrent.cpp */\n#include <boost/thread.hpp>\n#include <boost/chrono.hpp>\n#include <iostream>\n\nvoid Print1() {\n  for (int i=0; i<5; i++) {\n    boost::this_thread::sleep_for(boost::chrono::milliseconds{500});\n    std::cout << \"[Print1] Line: \" << i << '\\n';\n  }\n}\n\nvoid Print2() {\n  for (int i=0; i<5; i++) {\n    boost::this_thread::sleep_for(boost::chrono::milliseconds{500});\n    std::cout << \"[Print2] Line: \" << i << '\\n';\n  }\n}\n\nint main(void) {\n  boost::thread_group threads;\n  threads.create_thread(Print1);\n  threads.create_thread(Print2);\n  threads.join_all();\n}\n```\n\n将前面的代码保存为`concurrent.cpp`，并使用以下命令进行编译:\n\n```cpp\ng++ -ansi -std=c++ 11 -I ../boost_1_58_0 concurrent.cpp -o concurrent -L ../boost_1_58_0/stage/lib -lboost_system-mgw49-mt-1_58 -lws2_32 -l boost_thread-mgw49-mt-1_58 -l boost_chrono-mgw49-mt-1_58\n\n```\n\n运行程序，得到如下输出:\n\n![Getting closer to the Boost.Asio library](img/00022.jpeg)\n\n从前面的输出中我们可以看到`Print1()`和`Print2()`功能是并发运行的。`Print2()`函数不需要等待`Print1()`函数完成所有要调用的指令的执行。这就是为什么我们称之为并发编程。\n\n### 类型\n\n如果在代码中包含库，请不要忘记复制关联的动态库文件。例如，如果您使用`–l`选项包含`boost_system-mgw49-mt-1_58`，您必须复制`libboost_system-mgw49-mt-1_58.dll`文件并将其粘贴到与输出可执行文件相同的目录中。\n\n# 检查提升中的输入/输出服务.Asio 库\n\n`Boost::Asio`命名空间的核心对象是`io_service`。**输入/输出服务**是一个通道，用于访问操作系统资源，并在我们的程序和执行输入/输出请求的操作系统之间建立通信。还有一个 I/O 对象有提交 I/O 请求的作用。对于实例，`tcp::socket`对象将从我们的程序向操作系统提供套接字编程请求。\n\n## 使用和阻止 run()功能\n\n输入/输出服务对象中最常使用的功能之一是`run()`功能。用于运行`io_service`对象的事件处理循环。它将阻塞下一个语句程序，直到`io_service`对象中的所有工作完成，并且不再有要调度的处理程序。如果我们停止`io_service`对象，它将不再阻塞程序。\n\n### 注\n\n在编程中，`event`是程序检测到的动作或事件，将由程序使用`event handler`对象处理。`io_service`对象有一个或多个处理事件的实例，即`event processing loop`。\n\n现在，让我们看看下面的代码片段:\n\n```cpp\n/* unblocked.cpp */\n#include <boost/asio.hpp>\n#include <iostream>\n\nint main(void) {\n  boost::asio::io_service io_svc;\n\n  io_svc.run();\n\n  std::cout << \"We will see this line in console window.\" << std::endl;\n\n  return 0;\n}\n```\n\n我们将前面的代码保存为`unblocked.cpp`，然后运行以下命令进行编译:\n\n```cpp\ng++ -Wall -ansi -I ../boost_1_58_0 unblocked.cpp -o unblocked -L ../boost_1_58_0/stage/lib -l boost_system-mgw49-mt-1_58 -l ws2_32\n\n```\n\n当我们运行程序时，会显示以下输出:\n\n```cpp\nWe will see this line in console window.\n\n```\n\n然而，为什么我们仍然在控制台中获得文本行，尽管以前我们知道`run()`函数在被调用后会阻止下一个函数？这是因为我们没有给`io_service`对象任何工作。既然`io_service`没有工作要做，`io_service`对象就不应该屏蔽程序。\n\n现在，让我们给`io_service`对象一些工作要做。这方面的程序如下代码所示:\n\n```cpp\n/* blocked.cpp */\n#include <boost/asio.hpp>\n#include <iostream>\n\nint main(void) {\n  boost::asio::io_service io_svc;\n  boost::asio::io_service::work worker(io_svc);\n\n  io_svc.run();\n\n  std::cout << \"We will not see this line in console window :(\" << std::endl;\n\n  return 0;\n}\n```\n\n将前面的代码命名为`blocked.cpp`和，然后在我们的控制台窗口中键入以下命令进行编译:\n\n```cpp\ng++ -Wall -ansi -I ../boost_1_58_0 blocked.cpp -o blocked -L ../boost_1_58_0/stage/lib -l boost_system-mgw49-mt-1_58 -l ws2_32\n\n```\n\n如果我们通过在控制台中键入`blocked`来运行程序，我们将不再看到文本行，因为我们添加了以下代码行:\n\n```cpp\nboost::asio::io_service::work work(io_svc);\n\n```\n\n`work`类负责告诉`io_service`对象什么时候开始工作，什么时候结束工作。它将确保在工作进行期间`io_service`对象中的`run()`功能不会退出。此外，当没有未完成的工作时，它将确保`run()`功能确实退出。在我们前面的代码中，`work`类通知`io_service`对象它有工作要做，但是我们没有定义工作是什么。因此，程序将被无限阻塞，并且不会显示输出。之所以被屏蔽，是因为`run()`功能被调用，即使我们仍然可以通过按 *Ctrl* + *C* 来终止程序。\n\n## 使用非阻塞轮询()功能\n\n现在，我们暂时离开`run()`功能，尝试使用`poll()`功能。`poll()`功能用于运行准备好的处理程序，直到没有剩余的准备好的处理程序，或者直到`io_service`对象已经停止。但是，与`run()`功能相比，`poll()`功能不会阻塞程序。\n\n让我们键入以下使用`poll()`功能的代码，并将其保存为`poll.cpp`:\n\n```cpp\n/* poll.cpp */\n#include <boost/asio.hpp>\n#include <iostream>\n\nint main(void) {\n  boost::asio::io_service io_svc;\n\n  for(int i=0; i<5; i++) {\n    io_svc.poll();\n    std::cout << \"Line: \" << i << std::endl;\n  }\n\n  return 0;\n}\n```\n\n然后，使用以下命令编译`poll.cpp`:\n\n```cpp\ng++ -Wall -ansi -I ../boost_1_58_0 poll.cpp -o poll -L ../boost_1_58_0/stage/lib -l boost_system-mgw49-mt-1_58 -l ws2_32\n\n```\n\n因为没有`io_service`对象要做的工作，程序应该如下显示五行文本:\n\n![Using the non-blocking poll() function](img/00023.jpeg)\n\n然而，如果我们在使用`poll()`功能时给`io_service`对象做功呢？为了找到答案，让我们输入以下代码并将其保存为`pollwork.cpp`:\n\n```cpp\n/* pollwork.cpp */\n#include <boost/asio.hpp>\n#include <iostream>\n\nint main(void) {\n  boost::asio::io_service io_svc;\n  boost::asio::io_service::work work(io_svc);\n\n  for(int i=0; i<5; i++) {\n    io_svc.poll();\n    std::cout << \"Line: \" << i << std::endl;\n  }\n\n  return 0;\n}\n```\n\n要编译`pollwork.cpp`，使用以下命令:\n\n```cpp\ng++ -Wall -ansi -I ../boost_1_58_0 pollwork.cpp -o pollwork -L ../boost_1_58_0/stage/lib -l boost_system-mgw49-mt-1_58 -l ws2_32\n\n```\n\n`poll.cpp`文件和`pollwork.cpp`文件的区别只有以下几行:\n\n```cpp\nboost::asio::io_service::work work(io_svc);\n\n```\n\n但是，如果我们运行`pollwork.exe`，我们将获得与`poll.exe`相同的输出。这是因为，如我们之前所知，`poll()`功能不会在有更多工作要做时阻止程序。它将执行当前工作，然后返回值。\n\n## 移除工作对象\n\n我们也可以通过从`io_service`对象中移除`work`对象来解锁程序，但是我们必须在中使用指向`work`对象的指针来移除`work`对象本身。我们将使用`shared_ptr`指针，一个由`Boost`库提供的智能指针。\n\n让我们使用`blocked.cpp`的修改代码。这方面的代码如下:\n\n```cpp\n/* removework.cpp */\n#include <boost/asio.hpp>\n#include <boost/shared_ptr.hpp>\n#include <iostream>\n\nint main(void) {\n  boost::asio::io_service io_svc;\n  boost::shared_ptr<boost::asio::io_service::work> worker(\n    new boost::asio::io_service::work(io_svc)\n  );\n\n  worker.reset();\n\n  io_svc.run();\n\n  std::cout << \"We will not see this line in console window :(\" << std::endl;\n\n  return 0;\n}\n```\n\n将前面的代码保存为`removework.cpp`，并使用以下命令编译它:\n\n```cpp\ng++ -Wall -ansi -I ../boost_1_58_0 removework.cpp -o removework -L ../boost_1_58_0/stage/lib -l boost_system-mgw49-mt-1_58 -l ws2_32\n\n```\n\n当我们运行`removework.cpp`的时候，相比于`blocked.cpp`会无限的阻塞程序，下面一行文字会显示给我们:\n\n![Removing the work object](img/00024.jpeg)\n\n现在，让我们剖析一下代码。正如我们在前面的代码中看到的，我们使用了`shared_ptr`指针来实例化`work`对象。借助 Boost 提供的智能指针，我们不再需要手动删除内存分配来存储指针，因为它保证了当最后一个指针被破坏或重置时，所指向的对象将被删除。不要忘记在`boost`目录中包含`shared_ptr.hpp`，因为`shared_ptr`指针是在头文件中定义的。\n\n我们还添加了`reset()`函数，以便在准备后续的`run()`函数调用时重置`io_service`对象。在调用`run()`或`poll()`函数之前，必须调用`reset()`函数。它还会告诉`shared_ptr`指针自动销毁我们创建的指针。关于`share_ptr`指针的更多信息，可在[www.boost.org/doc/libs/1_58_0/libs/smart_ptr/shared_ptr.htm](http://www.boost.org/doc/libs/1_58_0/libs/smart_ptr/shared_ptr.htm)找到。\n\n前面的程序解释了我们已经成功地从`io_service`对象中移除了对象。我们可以使用这个功能，如果我们打算完成所有悬而未决的工作，即使它实际上还没有完成。\n\n## 处理多个线程\n\n到目前为止，我们只为一个`io_service`对象处理了一个线程。如果我们想在单个`io_service`对象中处理更多的线程，下面的代码将解释如何做到这一点:\n\n```cpp\n/* multithreads.cpp */\n#include <boost/asio.hpp>\n#include <boost/shared_ptr.hpp>\n#include <boost/thread.hpp>\n#include <iostream>\n\nboost::asio::io_service io_svc;\nint a = 0;\n\nvoid WorkerThread() {\n  std::cout << ++ a << \".\\n\";\n  io_svc.run();\n  std::cout << \"End.\\n\";\n}\n\nint main(void) {\n  boost::shared_ptr<boost::asio::io_service::work> worker(\n    new boost::asio::io_service::work(io_svc)\n  );\n\n  std::cout << \"Press ENTER key to exit!\" << std::endl;\n\n  boost::thread_group threads;\n  for(int i=0; i<5; i++)\n    threads.create_thread(WorkerThread);\n\n  std::cin.get();\n\n  io_svc.stop();\n\n  threads.join_all();\n\n  return 0;\n}\n```\n\n给前面的代码命名为`mutithreads.cpp`，然后使用以下命令编译它:\n\n```cpp\ng++ -Wall -ansi -I ../boost_1_58_0 multithreads.cpp -o multithreads -L ../boost_1_58_0/stage/lib -l boost_system-mgw49-mt-1_58 -l ws2_32 -l boost_thread-mgw49-mt-1_58\n\n```\n\n我们将包含在`thread.hpp`头文件中，这样我们就可以使用头文件中定义的对象。线程本身是一段可以独立运行的指令序列，所以我们可以同时运行多个线程。\n\n现在，在我们的控制台中运行`mutithreads.exe`。我通过运行它获得了以下输出:\n\n![Dealing with many threads](img/00025.jpeg)\n\n您可能会获得不同的输出，因为被设置为线程池的所有线程都是彼此等效的。`io_service`对象可以随机选择其中任意一个并调用其处理程序，所以我们不能保证`io_service`对象是否会顺序选择一个线程:\n\n```cpp\nfor(int i=0; i<5; i++)\n threads.create_thread(WorkerThread);\n\n```\n\n使用前面的代码片段，我们可以创建五个线程来显示文本行，如您在前面的截图中所见。对于本例来说，五行文本足以查看非电流的顺序:\n\n```cpp\nstd::cout << ++ a << \".\\n\";\nio_svc.run();\n\n```\n\n在创建的每个线程中，程序将调用`run()`函数来运行`io_service`对象的工作。调用一次`run()`函数是不够的，因为所有非工作人员都将在`run()`对象完成所有工作后被调用。\n\n创建五个线程后，程序运行`io_service`对象的工作:\n\n```cpp\nstd::cin.get();\n\n```\n\n所有工作运行完毕后，程序等待您使用前面的代码片段从键盘上按下*进入*键:\n\n```cpp\nio_svc.stop();\n\n```\n\n一旦用户按下*进入*键，程序将点击前面的代码片段。`stop()`功能会通知`io_service`对象所有工作都应该停止。这意味着程序将停止我们拥有的五个线程:\n\n```cpp\nthreads.join_all();\n\n```\n\n`join_all()`功能随后将继续处理所有未完成的线程，程序将等待，直到所有线程中的所有进程都完成。前面的代码片段将在`WorkerThread()`块中继续下面的语句:\n\n```cpp\nstd::cout << \"End.\\n\";\n\n```\n\n所以在我们按下*进入*键后，程序将完成它剩余的代码，我们将获得如下剩余的输出:\n\n![Dealing with many threads](img/00026.jpeg)\n\n# 了解助力。绑定库\n\n我们已经能够使用`io_service`对象并初始化`work`对象。在这之后我们应该知道的是如何给`io_service`对象一些工作。但是在我们向`io_service`服务提供工作之前，我们需要了解`boost::bind`库。\n\n`Boost.Bind`库用于简化函数指针的调用。它将语法从深奥和令人困惑的东西转换成易于理解的东西。\n\n## 包装函数调用\n\n让我们按照的顺序查看下面的代码，以了解如何包装函数调用:\n\n```cpp\n/* uncalledbind.cpp */\n#include <boost/bind.hpp>\n#include <iostream>\n\nvoid func() {\n  std::cout << \"Binding Function\" << std::endl;\n}\n\nint main(void) {\n  boost::bind(&func);\n  return 0;\n}\n```\n\n将前面的代码保存为`uncalledbind.cpp`，然后使用以下命令进行编译:\n\n```cpp\ng++ -Wall -ansi -I ../boost_1_58_0 uncalledbind.cpp -o uncalledbind\n\n```\n\n我们不会得到任何一行文本作为输出，因为我们只是创建了一个函数调用，但实际上并没有调用它。我们必须将其添加到`()`运算符中，以调用如下函数:\n\n```cpp\n/* calledbind.cpp */\n#include <boost/bind.hpp>\n#include <iostream>\n\nvoid func() {\n  std::cout << \"Binding Function\" << std::endl;\n}\n\nint main(void) {\n  boost::bind(&func)();\n  return 0;\n}\n```\n\n命名前面的代码`calledbind.cpp`并运行以下命令编译它:\n\n```cpp\ng++ -Wall -ansi -I ../boost_1_58_0 calledbind.cpp -o calledbind\n\n```\n\n现在，如果我们运行程序，我们将获得文本行作为输出，当然，我们将看到`bind()`函数作为输出:\n\n```cpp\nboost::bind(&func)();\n\n```\n\n正如我们在整个代码中所看到的，更改只发生在一行中，如前面的代码片段所示。\n\n现在，让我们使用带有参数的函数来传递。我们将在下面的代码中为此目的使用`boost::bind`:\n\n```cpp\n/* argumentbind.cpp */\n#include <boost/bind.hpp>\n#include <iostream>\n\nvoid cubevolume(float f) {\n  std::cout << \"Volume of the cube is \" << f * f * f << std::endl;\n}\n\nint main(void) {\n  boost::bind(&cubevolume, 4.23f)();\n  return 0;\n}\n```\n\n运行以下命令编译前面的`argumentbind.cpp`文件:\n\n```cpp\ng++ -Wall -ansi -I ../boost_1_58_0 argumentbind.cpp -o argumentbind\n\n```\n\n我们使用`boost::bind`成功调用了带有参数的函数，因此我们获得了以下输出:\n\n```cpp\nVolume of the cube is 75.687\n\n```\n\n您需要记住，如果函数有多个参数，我们必须精确匹配函数签名。下面的代码将对此进行更详细的解释:\n\n```cpp\n/* signaturebind.cpp */\n#include <boost/bind.hpp>\n#include <iostream>\n#include <string>\n\nvoid identity(std::string name, int age, float height) {\n  std::cout << \"Name   : \" << name << std::endl;\n  std::cout << \"Age    : \" << age << \" years old\" << std::endl;\n  std::cout << \"Height : \" << height << \" inch\" << std::endl;\n}\n\nint main(int argc, char * argv[]) {\n  boost::bind(&identity, \"John\", 25, 68.89f)();\n  return 0;\n}\n```\n\n使用以下命令编译`signaturebind.cpp`代码:\n\n```cpp\ng++ -Wall -ansi -I ../boost_1_58_0 signaturebind.cpp -o signaturebind\n\n```\n\n身份函数的签名是`std::string`、`int`和`float`。因此，我们必须分别用`std::string`、`int`和`float`填充`bind`参数。\n\n因为我们已经精确地匹配了函数签名，我们将获得如下输出:\n\n![Wrapping a function invocation](img/00027.jpeg)\n\n我们已经能够在`boost::bind`中调用`global()`函数。现在，让我们继续在`boost::bind`中调用类内的函数。这方面的代码如下:\n\n```cpp\n/* classbind.cpp */\n#include <boost/bind.hpp>\n#include <iostream>\n#include <string>\n\nclass TheClass {\npublic:\n  void identity(std::string name, int age, float height) {\n    std::cout << \"Name   : \" << name << std::endl;\n    std::cout << \"Age    : \" << age << \" years old\" << std::endl;\n    std::cout << \"Height : \" << height << \" inch\" << std::endl;\n  }\n};\n\nint main(void) {\n  TheClass cls;\n  boost::bind(&TheClass::identity, &cls, \"John\", 25, 68.89f)();\n  return 0;\n}\n```\n\n使用以下命令编译前面的`classbind.cpp`代码:\n\n```cpp\ng++ -Wall -ansi -I ../boost_1_58_0 classbind.cpp -o classbind\n\n```\n\n这个输出将与`signaturebind.cpp`代码完全相同，因为函数的内容也完全相同:\n\n```cpp\nboost::bind(&TheClass::identity, &cls, \"John\", 25, 68.89f)();\n\n```\n\n正如我们在前面的代码片段中看到的，我们必须传递带有类和函数名的`boost:bind`参数、类的对象和基于函数签名的参数。\n\n## 使用助推器。绑定库\n\n到目前为止，我们已经能够将`boost::bind`用于全局和类函数。但是，当将`io_service`对象与`boost::bind`一起使用时，我们会出现**不可复制的**错误，因为`io_service`对象无法复制。\n\n现在，我们再来看看`multithreads.cpp`。我们将修改代码来解释`io_service`对象的`boost::bind`的用法，我们仍然需要`shared_ptr`指针的帮助。让我们看看下面的代码片段:\n\n```cpp\n/* ioservicebind.cpp */\n#include <boost/asio.hpp>\n#include <boost/shared_ptr.hpp>\n#include <boost/thread.hpp>\n#include <boost/bind.hpp>\n#include <iostream>\n\nvoid WorkerThread(boost::shared_ptr<boost::asio::io_service> iosvc, int counter) {\n  std::cout << counter << \".\\n\";\n  iosvc->run();\n  std::cout << \"End.\\n\";\n}\n\nint main(void) {\n  boost::shared_ptr<boost::asio::io_service> io_svc(\n    new boost::asio::io_service\n  );\n\n  boost::shared_ptr<boost::asio::io_service::work> worker(\n    new boost::asio::io_service::work(*io_svc)\n  );\n\n  std::cout << \"Press ENTER key to exit!\" << std::endl;\n\n  boost::thread_group threads;\n  for(int i=1; i<=5; i++)\n    threads.create_thread(boost::bind(&WorkerThread, io_svc, i));\n\n  std::cin.get();\n\n  io_svc->stop();\n\n  threads.join_all();\n\n  return 0;\n}\n```\n\n我们将前面的代码命名为`ioservicebind.cpp`，并使用以下命令进行编译:\n\n```cpp\ng++ -Wall -ansi -I ../boost_1_58_0 ioservicebind.cpp -o ioservicebind –L ../boost_1_58_0/stage/lib -l boost_system-mgw49-mt-1_58 -l ws2_32 -l boost_thread-mgw49-mt-1_58\n\n```\n\n当我们运行`ioservicebind.exe`时，我们获得与`multithreads.exe`相同的输出，但当然，程序会随机化所有线程的顺序:\n\n```cpp\nboost::shared_ptr<boost::asio::io_service> io_svc(\n new boost::asio::io_service\n);\n\n```\n\n我们实例化`shared_ptr`指针中的`io_service`对象，使其**可复制**，这样我们就可以将其绑定到我们用作线程处理程序的工人`thread()`函数:\n\n```cpp\nvoid WorkerThread(boost::shared_ptr<boost::asio::io_service> iosvc, int counter)\n\n```\n\n前面的代码片段向我们展示了`io_service`对象可以被传递给函数。我们不需要像在`multithreads.cpp`代码片段中那样定义`int`全局变量，因为我们也可以将`int`参数传递给`WorkerThread()`函数:\n\n```cpp\nstd::cout << counter << \".\\n\";\n\n```\n\n现在，不是增加显示给用户的`int`变量。我们可以使用前面的代码片段，因为我们从`main`块中的`for`循环传递了计数器。\n\n如果我们看一下`create_thread()`函数，我们会看到它在`ioservicebind.cpp`和`multithreads.cpp`文件中得到不同的参数。我们可以传递一个指向`void()`函数的指针，该函数不接受任何参数作为`create_thread()`函数的参数，如我们在`multithreads.cpp`文件中所见。我们还可以将绑定函数作为参数传递给`create_thread()`函数，正如我们在`ioservicebind.cpp`文件中看到的那样。\n\n## 使数据访问与升压同步。互斥库\n\n你运行`multithreads.exe`或者`ioservicebind.exe`可执行文件的时候有没有得到过下面的输出？\n\n![Synchronizing data access with the Boost.Mutex library](img/00028.jpeg)\n\n我们可以在前面的截图中看到这里有一个格式问题。因为`std::cout`对象是一个全局对象，同时从不同的线程写入它可能会导致输出格式问题。为了解决这个问题，我们可以使用一个`mutex`对象，该对象可以在`thread`库提供的`boost::mutex`对象中找到。互斥体用于同步对任何全局数据或共享数据的访问。为了更好地理解 Mutex，请看下面的代码:\n\n```cpp\n/* mutexbind.cpp */\n#include <boost/asio.hpp>\n#include <boost/shared_ptr.hpp>\n#include <boost/thread.hpp>\n#include <boost/bind.hpp>\n#include <iostream>\n\nboost::mutex global_stream_lock;\n\nvoid WorkerThread(boost::shared_ptr<boost::asio::io_service> iosvc, int counter) {\n  global_stream_lock.lock();\n  std::cout << counter << \".\\n\";\n  global_stream_lock.unlock();\n\n  iosvc->run();\n\n  global_stream_lock.lock();\n  std::cout << \"End.\\n\";\n  global_stream_lock.unlock();\n}\n\nint main(void) {\n  boost::shared_ptr<boost::asio::io_service> io_svc(\n    new boost::asio::io_service\n  );\n\n  boost::shared_ptr<boost::asio::io_service::work> worker(\n    new boost::asio::io_service::work(*io_svc)\n  );\n\n  std::cout << \"Press ENTER key to exit!\" << std::endl;\n\n  boost::thread_group threads;\n  for(int i=1; i<=5; i++)\n    threads.create_thread(boost::bind(&WorkerThread, io_svc, i));\n\n  std::cin.get();\n\n  io_svc->stop();\n\n  threads.join_all();\n\n  return 0;\n}\n```\n\n将前面的代码保存为`mutexbind.cpp`和，然后使用以下命令进行编译:\n\n```cpp\ng++ -Wall -ansi -I ../boost_1_58_0 mutexbind.cpp -o mutexbind -L ../boost_1_58_0/stage/lib -l boost_system-mgw49-mt-1_58 -l ws2_32 -l boost_thread-mgw49-mt-1_58\n\n```\n\n现在，运行`mutexbind.cpp`文件，我们将不再面临格式问题:\n\n```cpp\nboost::mutex global_stream_lock;\n\n```\n\n我们实例化新的`mutex`对象，`global_stream_lock`。有了这个对象，我们可以调用`lock()`和`unlock()`函数。`lock()`函数将阻止访问相同函数的其他线程等待当前线程完成。如果只有当前线程调用了`unlock()`函数，其他线程可以访问相同的函数。需要记住的一点是，我们不应该递归调用`lock()`函数，因为如果`lock()`函数没有被`unlock()`函数解锁，那么就会出现线程死锁，从而冻结应用。所以，我们在使用`lock()`和`unlock()`功能时一定要小心。\n\n# 给输入输出服务一些工作\n\n现在，是时候给`io_service`对象一些工作了。更多的了解`boost::bind`和`boost::mutex`会帮助我们给`io_service`对象工作做。`io_service`对象中有两个成员函数:`post()`和`dispatch()`函数，我们将经常使用这两个函数。`post()`功能是在我们将所有工作排队后，请求`io_service`对象运行`io_service`对象的工作，所以不允许我们立即运行工作。而`dispatch()`功能也是用来向`io_service`对象发出请求运行`io_service`对象的工作，但它会马上执行工作而不排队。\n\n## 使用 post()函数\n\n让我们通过创建下面的代码来检查`post()`函数。我们将使用`mutexbind.cpp`文件作为我们的基础代码，因为我们将只修改源代码:\n\n```cpp\n/* post.cpp */\n#include <boost/asio.hpp>\n#include <boost/shared_ptr.hpp>\n#include <boost/thread.hpp>\n#include <boost/bind.hpp>\n#include <iostream>\n\nboost::mutex global_stream_lock;\n\nvoid WorkerThread(boost::shared_ptr<boost::asio::io_service> iosvc, int counter) {\n  global_stream_lock.lock();\n  std::cout << counter << \".\\n\";\n  global_stream_lock.unlock();\n\n  iosvc->run();\n\n  global_stream_lock.lock();\n  std::cout << \"End.\\n\";\n  global_stream_lock.unlock();\n}\n\nsize_t fac(size_t n) {\n  if ( n <= 1 ) {\n    return n;\n  }\n  boost::this_thread::sleep(\n    boost::posix_time::milliseconds(1000)\n  );\n  return n * fac(n - 1);\n}\n\nvoid CalculateFactorial(size_t n) {\n  global_stream_lock.lock();\n  std::cout << \"Calculating \" << n << \"! factorial\" << std::endl;\n  global_stream_lock.unlock();\n\n  size_t f = fac(n);\n\n  global_stream_lock.lock();\n  std::cout << n << \"! = \" << f << std::endl;\n  global_stream_lock.unlock();\n}\n\nint main(void) {\n  boost::shared_ptr<boost::asio::io_service> io_svc(\n    new boost::asio::io_service\n  );\n\n  boost::shared_ptr<boost::asio::io_service::work> worker(\n    new boost::asio::io_service::work(*io_svc)\n  );\n\n  global_stream_lock.lock();\n  std::cout << \"The program will exit once all work has finished.\" << std::endl;\n  global_stream_lock.unlock();\n\n  boost::thread_group threads;\n  for(int i=1; i<=5; i++)\n    threads.create_thread(boost::bind(&WorkerThread, io_svc, i));\n\n  io_svc->post(boost::bind(CalculateFactorial, 5));\n  io_svc->post(boost::bind(CalculateFactorial, 6));\n  io_svc->post(boost::bind(CalculateFactorial, 7));\n\n  worker.reset();\n\n  threads.join_all();\n\n  return 0;\n}\n```\n\n将前面的代码命名为`post.cpp`，并使用下面的命令进行编译:\n\n```cpp\ng++ -Wall -ansi -I ../boost_1_58_0 post.cpp -o post -L ../boost_1_58_0/stage/lib -l boost_system-mgw49-mt-1_58 -l ws2_32 -l boost_thread-mgw49-mt-1_58\n\n```\n\n在运行程序之前，让我们检查代码以了解其行为:\n\n```cpp\nsize_t fac(size_t n) {\n if (n <= 1) {\n return n;\n }\n boost::this_thread::sleep(\n boost::posix_time::milliseconds(1000)\n );\n return n * fac(n - 1);\n}\n\n```\n\n我们添加`fac()`函数来递归计算 *n* 阶乘。为了查看工作线程的工作情况，会有一个时间延迟来减慢进程:\n\n```cpp\nio_svc->post(boost::bind(CalculateFactorial, 5));\nio_svc->post(boost::bind(CalculateFactorial, 6));\nio_svc->post(boost::bind(CalculateFactorial, 7));\n\n```\n\n在`main`块中，我们使用`post()`函数在`io_service`对象上发布三个函数对象。我们在初始化五个工作线程之后就这样做了。但是，因为我们在每个线程内部调用`io_service`对象的`run()`函数，所以`io_service`对象的工作将运行。这意味着`post()`功能将发挥作用。\n\n现在，让我们运行`post.cpp`并看看这里发生了什么:\n\n![Using the post() function](img/00029.jpeg)\n\n正如我们在前面截图的输出中所看到的，程序运行线程池中的线程，完成一个线程后，从`io_service`对象调用`post()`函数，直到调用完所有三个`post()`函数和所有五个线程。然后，它计算每三个 *n* 数的阶乘。得到`worker.reset()`功能后，通知工作已经完成，然后通过`threads.join_all()`功能加入所有线程。\n\n## 使用调度()功能\n\n现在，让我们通过检查`dispatch()`功能来给`io_service`功能一些工作。我们仍将使用`mutexbind.cpp`文件作为我们的基本代码，我们将对其稍加修改，使其如下所示:\n\n```cpp\n/* dispatch.cpp */\n#include <boost/asio.hpp>\n#include <boost/shared_ptr.hpp>\n#include <boost/thread.hpp>\n#include <boost/bind.hpp>\n#include <iostream>\n\nboost::mutex global_stream_lock;\n\nvoid WorkerThread(boost::shared_ptr<boost::asio::io_service> iosvc) {\n  global_stream_lock.lock();\n  std::cout << \"Thread Start.\\n\";\n  global_stream_lock.unlock();\n\n  iosvc->run();\n\n  global_stream_lock.lock();\n  std::cout << \"Thread Finish.\\n\";\n  global_stream_lock.unlock();\n}\n\nvoid Dispatch(int i) {\n  global_stream_lock.lock();\n  std::cout << \"dispath() Function for i = \" << i <<  std::endl;\n  global_stream_lock.unlock();\n}\n\nvoid Post(int i) {\n  global_stream_lock.lock();\n  std::cout << \"post() Function for i = \" << i <<  std::endl;\n  global_stream_lock.unlock();\n}\n\nvoid Running(boost::shared_ptr<boost::asio::io_service> iosvc) {\n  for( int x = 0; x < 5; ++ x ) {\n    iosvc->dispatch(boost::bind(&Dispatch, x));\n    iosvc->post(boost::bind(&Post, x));\n    boost::this_thread::sleep(boost::posix_time::milliseconds(1000));\n  }\n}\n\nint main(void) {\n  boost::shared_ptr<boost::asio::io_service> io_svc(\n    new boost::asio::io_service\n  );\n\n  boost::shared_ptr<boost::asio::io_service::work> worker(\n    new boost::asio::io_service::work(*io_svc)\n  );\n\n  global_stream_lock.lock();\n  std::cout << \"The program will exit automatically once all work has finished.\" << std::endl;\n  global_stream_lock.unlock();\n\n  boost::thread_group threads;\n\n  threads.create_thread(boost::bind(&WorkerThread, io_svc));\n\n  io_svc->post(boost::bind(&Running, io_svc));\n\n  worker.reset();\n\n  threads.join_all();\n\n  return 0;\n}\n```\n\n将前面的代码命名为`dispatch.cpp`，使用以下命令编译它:\n\n```cpp\ng++ -Wall -ansi -I ../boost_1_58_0 dispatch.cpp -o dispatch -L ../boost_1_58_0/stage/lib -l boost_system-mgw49-mt-1_58 -l ws2_32 -l boost_thread-mgw49-mt-1_58\n\n```\n\n现在，让我们运行程序以获得以下输出:\n\n![Using the dispatch() function](img/00030.jpeg)\n\n与`post.cpp`文件不同的是，在`dispatch.cpp`文件中，我们只创建了一个工作线程。另外，我们添加了两个函数，`dispatch(),`和`post()`来理解这两个函数之间的区别:\n\n```cpp\niosvc->dispatch(boost::bind(&Dispatch, x));\niosvc->post(boost::bind(&Post, x));\n\n```\n\n如果我们在`Running()`函数中查看前面的代码片段，我们期望得到`dispatch()`和`post()`函数之间的有序输出。然而，当我们看到输出时，我们发现结果是不同的，因为首先调用`dispatch()`函数，然后调用`post()`函数。发生这种情况是因为`dispatch()`函数可以从当前工作线程调用，而`post()`函数必须等到工作线程的处理程序完成后才能调用。换句话说，`dispatch()`函数的事件可以从当前工作线程执行，即使有其他未决事件排队，而`post()`函数的事件必须等到处理程序完成执行后才能被允许执行。\n\n# 总结\n\n我们可以使用两个函数来获取为我们工作的`io_service`对象:`run()`和`poll()`成员函数。`run()`功能阻止程序，因为它必须等待我们分配给它的工作，而`poll()`功能不阻止程序。当我们需要对`io_service`对象进行一些工作时，我们只需根据需要使用`poll()`或`run()`功能，然后根据需要调用`post()`或`dispatch()`功能。`post()`函数用于命令`io_service`对象运行给定的处理程序，但不允许处理程序由`io_service`对象从该函数内部调用。而`dispatch()`函数用于调用当前正在调用`run()`或`poll()`函数的线程中的处理程序。`dispatch()`和`post()`功能的根本区别在于`dispatch()`功能只要有可能就马上完成工作，而`post()`功能总是将工作排队。\n\n我们发现了`io_service`对象，如何运行它，以及如何给它一些工作。现在，让我们进入下一章，了解更多关于`Boost.Asio`库的信息，我们离创建我们的网络编程又近了一步。"
  },
  {
    "path": "docs/boost-asio-cpp-net-prog-2e/5.md",
    "content": "# 五、深入研究 Boost.Asio 库\n\n现在我们已经能够运行`io_service`对象并给它一些工作要做，是时候在`Boost.Asio`库中找到更多关于其他对象的信息，以便开发网络应用了。我们之前使用的`io_service`对象的所有工作都是异步运行的，但不是以序列化的顺序运行的，这意味着我们无法确定`io_service`对象将要运行的工作的顺序。此外，我们必须考虑如果我们的应用在运行时遇到任何错误，我们将做什么，并考虑运行任何`io_service`对象工作的时间间隔。因此，在本章中，我们将讨论以下主题:\n\n*   连续执行`io_service`对象的工作\n*   捕捉异常并正确处理它们\n*   在期望的时间内完成工作\n\n# 序列化输入输出服务工作\n\n假设我们想把要做的工作排好队，但是顺序很重要。如果我们只是应用异步方法，我们将不知道我们将得到的工作顺序。我们需要确保工作的顺序是我们想要的，并且已经设计好了。例如，如果我们以这个顺序发布工作 A、工作 B 和工作 C，我们希望在运行时保持这个顺序。\n\n## 使用链函数\n\n**Strand** 是`io_service`对象中的一个类，提供处理程序执行序列化。它可以用来确保我们的工作将连续执行。让我们检查下面的代码，通过使用`strand`函数来理解序列化。但是首先，我们将在没有的情况下开始使用`strand()`和`lock()`功能:\n\n```cpp\n/* nonstrand.cpp */\n#include <boost/asio.hpp>\n#include <boost/shared_ptr.hpp>\n#include <boost/thread.hpp>\n#include <boost/thread/mutex.hpp>\n#include <boost/bind.hpp>\n#include <iostream>\n\nboost::mutex global_stream_lock;\n\nvoid WorkerThread(boost::shared_ptr<boost::asio::io_service> iosvc, int counter) {\n  global_stream_lock.lock();\n  std::cout << \"Thread \" << counter << \" Start.\\n\";\n  global_stream_lock.unlock();\n\n  iosvc->run();\n\n  global_stream_lock.lock();\n  std::cout << \"Thread \" << counter << \" End.\\n\";\nglobal_stream_lock.unlock();\n}\n\nvoid Print(int number) {\n  std::cout << \"Number: \" << number << std::endl;\n}\n\nint main(void) {\n  boost::shared_ptr<boost::asio::io_service> io_svc(\n    new boost::asio::io_service\n  );\n\n  boost::shared_ptr<boost::asio::io_service::work> worker(\n    new boost::asio::io_service::work(*io_svc)\n  );\n\n  global_stream_lock.lock();\n  std::cout << \"The program will exit once all work has finished.\\n\";\n  global_stream_lock.unlock();\n\n  boost::thread_group threads;\n  for(int i=1; i<=5; i++)\n    threads.create_thread(boost::bind(&WorkerThread, io_svc, i));\n\n  boost::this_thread::sleep(boost::posix_time::milliseconds(500));\n\n  io_svc->post(boost::bind(&Print, 1));\n  io_svc->post(boost::bind(&Print, 2));\n  io_svc->post(boost::bind(&Print, 3));\n  io_svc->post(boost::bind(&Print, 4));\n  io_svc->post(boost::bind(&Print, 5));\n\n  worker.reset();\n\n  threads.join_all();\n\n  return 0;\n}\n```\n\n将前面的代码保存为`nonstrand.cpp`，并使用以下命令进行编译:\n\n```cpp\ng++ -Wall -ansi -I ../boost_1_58_0 nonstrand.cpp -o nonstrand -L ../boost_1_58_0/stage/lib -l boost_system-mgw49-mt-1_58 -l ws2_32 -l libboost_thread-mgw49-mt-1_58\n\n```\n\n然后，在控制台窗口中键入`nonstrand`运行。我们将得到类似如下的输出:\n\n![Using the strand function](img/00031.jpeg)\n\n您可能会得到不同的输出，并且多次运行程序确实会产生不同顺序的结果。这是因为，正如我们在上一章中讨论的，如果没有`lock`对象，输出将是不同步的，如下所示。我们可以注意到结果看起来很混乱:\n\n```cpp\nNumber: Number: 1\nNumber: 5\nNumber: 3\n2\nNumber: 4\n\n```\n\n正如我们在下面的片段中看到的，我们没有使用`lock`对象来同步输出。这就是为什么我们会得到前面截图所示的输出。\n\n```cpp\nvoid Print(int number) {\n std::cout << \"Number: \" << number << std::endl;\n}\n\n```\n\n现在，让我们应用`strand`功能来同步程序的流程。输入以下代码并保存为`strand.cpp`:\n\n```cpp\n/* strand.cpp */\n#include <boost/asio.hpp>\n#include <boost/shared_ptr.hpp>\n#include <boost/thread.hpp>\n#include <boost/thread/mutex.hpp>\n#include <boost/bind.hpp>\n#include <iostream>\n\nboost::mutex global_stream_lock;\n\nvoid WorkerThread(boost::shared_ptr<boost::asio::io_service> iosvc, int counter) {\n  global_stream_lock.lock();\n  std::cout << \"Thread \" << counter << \" Start.\\n\";\n  global_stream_lock.unlock();\n\n  iosvc->run();\n\n  global_stream_lock.lock();\n  std::cout << \"Thread \" << counter << \" End.\\n\";\n  global_stream_lock.unlock();\n}\n\nvoid Print(int number) {\n  std::cout << \"Number: \" << number << std::endl;\n}\n\nint main(void) {\n  boost::shared_ptr<boost::asio::io_service> io_svc(\n    new boost::asio::io_service\n  );\n\n  boost::shared_ptr<boost::asio::io_service::work> worker(\n    new boost::asio::io_service::work(*io_svc)\n  );\n\n  boost::asio::io_service::strand strand(*io_svc);\n\n  global_stream_lock.lock();\n  std::cout << \"The program will exit once all work has finished.\\n\";\n  global_stream_lock.unlock();\n\n  boost::thread_group threads;\n  for(int i=1; i<=5; i++)\n    threads.create_thread(boost::bind(&WorkerThread, io_svc, i));\n\n  boost::this_thread::sleep(boost::posix_time::milliseconds(500));\n\n  strand.post(boost::bind(&Print, 1));\n  strand.post(boost::bind(&Print, 2));\n  strand.post(boost::bind(&Print, 3));\n  strand.post(boost::bind(&Print, 4));\n  strand.post(boost::bind(&Print, 5));\n\n  worker.reset();\n\n  threads.join_all();\n\n  return 0;\n}\n```\n\n使用以下命令编译前面的代码:\n\n```cpp\ng++ -Wall -ansi -I ../boost_1_58_0 strand.cpp -o strand -L ../boost_1_58_0/stage/lib -l boost_system-mgw49-mt-1_58 -l ws2_32 -l libboost_thread-mgw49-mt-1_58\n\n```\n\n从`nonstrand.cpp`到`strand.cpp`我们只是做了一点修改，但是影响还是比较大的。在运行程序之前，让我们区分一下`nonstrand.cpp`和`strand.cpp`的代码:\n\n```cpp\nio_svc->post(boost::bind(&Print, 1));\nio_svc->post(boost::bind(&Print, 2));\nio_svc->post(boost::bind(&Print, 3));\nio_svc->post(boost::bind(&Print, 4));\nio_svc->post(boost::bind(&Print, 5));\n\n```\n\n我们使用`io_service`对象中的`post()`函数使其工作。但是通过使用这种方法，程序的流程是不可预测的，因为它不是同步的:\n\n```cpp\nstrand.post(boost::bind(&Print, 1));\nstrand.post(boost::bind(&Print, 2));\nstrand.post(boost::bind(&Print, 3));\nstrand.post(boost::bind(&Print, 4));\nstrand.post(boost::bind(&Print, 5));\n\n```\n\n然后，我们使用`strand`对象将功赋予`io_service`对象。通过使用这种方法，我们将确保工作的顺序与我们在代码中声明的完全相同。为了证明这一点，让我们看看以下输出:\n\n![Using the strand function](img/00032.jpeg)\n\n工作的顺序与我们代码中的工作顺序相同。我们以数字顺序显示了工作的输出，即:\n\n```cpp\nNumber: 1\nNumber: 2\nNumber: 3\nNumber: 4\nNumber: 5\n\n```\n\n并且，如果你记得，我们继续从`Print()`功能中省略`lock()`功能，由于`strand`对象的使用，它仍然正常运行。现在，不管我们重新运行程序多少次，结果总是按升序排列。\n\n## 将处理程序缠绕在线束对象上\n\n`boost::asio::strand`中有一个函数叫做`wrap()`方法。基于官方的 Boost 文档，它创建了一个新的处理函数对象，当调用该对象时，它会自动将包装好的处理函数传递给`strand`对象的调度函数。让我们看下面的代码来解释它:\n\n```cpp\n/* strandwrap.cpp */\n#include <boost/asio.hpp>\n#include <boost/shared_ptr.hpp>\n#include <boost/thread.hpp>\n#include <boost/thread/mutex.hpp>\n#include <boost/bind.hpp>\n#include <iostream>\n\nboost::mutex global_stream_lock;\n\nvoid WorkerThread(boost::shared_ptr<boost::asio::io_service> iosvc, int counter) {\n  global_stream_lock.lock();\n  std::cout << \"Thread \" << counter << \" Start.\\n\";\n  global_stream_lock.unlock();\n\n  iosvc->run();\n\n  global_stream_lock.lock();\n  std::cout << \"Thread \" << counter << \" End.\\n\";\n  global_stream_lock.unlock();\n}\n\nvoid Print(int number) {\n  std::cout << \"Number: \" << number << std::endl;\n}\n\nint main(void) {\n  boost::shared_ptr<boost::asio::io_service> io_svc(\n    new boost::asio::io_service\n  );\n\n  boost::shared_ptr<boost::asio::io_service::work> worker(\n    new boost::asio::io_service::work(*io_svc)\n  );\n\n  boost::asio::io_service::strand strand(*io_svc);\n\n  global_stream_lock.lock();\n  std::cout << \"The program will exit once all work has finished.\" <<  std::endl;\n  global_stream_lock.unlock();\n\n  boost::thread_group threads;\n  for(int i=1; i<=5; i++)\n    threads.create_thread(boost::bind(&WorkerThread, io_svc, i));\n\n  boost::this_thread::sleep(boost::posix_time::milliseconds(100));\n  io_svc->post(strand.wrap(boost::bind(&Print, 1)));\n  io_svc->post(strand.wrap(boost::bind(&Print, 2)));\n\n  boost::this_thread::sleep(boost::posix_time::milliseconds(100));\n  io_svc->post(strand.wrap(boost::bind(&Print, 3)));\n  io_svc->post(strand.wrap(boost::bind(&Print, 4)));\n\n  boost::this_thread::sleep(boost::posix_time::milliseconds(100));\n  io_svc->post(strand.wrap(boost::bind(&Print, 5)));\n  io_svc->post(strand.wrap(boost::bind(&Print, 6)));\n\n  worker.reset();\n\n  threads.join_all();\n\n  return 0;\n}\n```\n\n给前面的代码命名为`strandwrap.cpp,`，然后使用以下命令编译它:\n\n```cpp\ng++ -Wall -ansi -I ../boost_1_58_0 strandwrap.cpp -o strandwrap -L ../boost_1_58_0/stage/lib -l boost_system-mgw49-mt-1_58 -l ws2_32 -l libboost_thread-mgw49-mt-1_58\n\n```\n\n现在，运行该程序，我们将获得以下输出:\n\n![Wrapping a handler through the strand object](img/00033.jpeg)\n\n然而，如果我们多次运行该程序，它可能会产生如下的随机输出:\n\n```cpp\nNumber: 2\nNumber: 1\nNumber: 3\nNumber: 4\nNumber: 6\nNumber: 5\n\n```\n\n虽然工作被保证是串行执行的，但是不能保证哪个工作的顺序实际上是由于内置的处理程序包装器而发生的。而且如果顺序真的很重要，我们在使用`strand`对象的时候就要看内置的处理程序包装器本身。\n\n# 处理异常和错误\n\n有时，我们的代码会在运行时抛出异常或错误。正如你可能还记得我们在[第 3 章](3.html#page \"Chapter 3. Introducing the Boost C++ Libraries\")*中对`lexical.cpp`的讨论，在介绍 Boost C++ 库*时，我们有时必须在代码中使用异常处理，现在我们将挖掘它来深入研究异常和错误处理。\n\n## 处理异常\n\n异常是一种方式，通过将控制权转移给处理程序来应对代码出现异常情况的情况。为了处理异常，我们需要在代码中使用`try-catch`块；然后，如果出现异常情况，将向异常处理程序抛出异常。\n\n现在，看看下面的代码，看看异常处理是如何使用的:\n\n```cpp\n/* exception.cpp */\n#include <boost/asio.hpp>\n#include <boost/shared_ptr.hpp>\n#include <boost/thread.hpp>\n#include <boost/thread/mutex.hpp>\n#include <boost/bind.hpp>\n#include <iostream>\n\nboost::mutex global_stream_lock;\n\nvoid WorkerThread(boost::shared_ptr<boost::asio::io_service> iosvc, int counter) {\n  global_stream_lock.lock();\n  std::cout << \"Thread \" << counter << \" Start.\\n\";\n  global_stream_lock.unlock();\n\n  try {\n    iosvc->run();\n\n    global_stream_lock.lock();\n    std::cout << \"Thread \" << counter << \" End.\\n\";\n    global_stream_lock.unlock();\n  }\n  catch(std::exception & ex) {\n    global_stream_lock.lock();\n    std::cout << \"Message: \" << ex.what() << \".\\n\";\n    global_stream_lock.unlock();\n  }\n}\n\nvoid ThrowAnException(boost::shared_ptr<boost::asio::io_service> iosvc, int counter) {\n  global_stream_lock.lock();\n  std::cout << \"Throw Exception \" << counter << \"\\n\" ;\n  global_stream_lock.unlock();\n\n  throw(std::runtime_error(\"The Exception !!!\"));\n}\n\nint main(void) {\n  boost::shared_ptr<boost::asio::io_service> io_svc(\n    new boost::asio::io_service\n  );\n\n  boost::shared_ptr<boost::asio::io_service::work> worker(\n    new boost::asio::io_service::work(*io_svc)\n  );\n\n  global_stream_lock.lock();\n  std::cout << \"The program will exit once all work has finished.\\n\";\n  global_stream_lock.unlock();\n\n  boost::thread_group threads;\n  for(int i=1; i<=2; i++)\n    threads.create_thread(boost::bind(&WorkerThread, io_svc, i));\n\n  io_svc->post(boost::bind(&ThrowAnException, io_svc, 1));\n  io_svc->post(boost::bind(&ThrowAnException, io_svc, 2));\n  io_svc->post(boost::bind(&ThrowAnException, io_svc, 3));\n  io_svc->post(boost::bind(&ThrowAnException, io_svc, 4));\n  io_svc->post(boost::bind(&ThrowAnException, io_svc, 5));\n\n  threads.join_all();\n\n  return 0;\n}\n```\n\n将前面的代码保存为`exception.cpp`并运行以下命令进行编译:\n\n```cpp\ng++ -Wall -ansi -I ../boost_1_58_0 exception.cpp -o exception -L ../boost_1_58_0/stage/lib -l boost_system-mgw49-mt-1_58 -l ws2_32 -l libboost_thread-mgw49-mt-1_58\n\n```\n\n然后，运行该程序，您应该会得到以下输出:\n\n![Handling an exception](img/00034.jpeg)\n\n正如我们可以看到的，由于异常，我们没有显示从`std::cout << \"Thread \" << counter << \" End.\\n\";`开始的线。当运行`io_service`对象的工作时，它总是使用`throw`关键字抛出异常，这样该异常将被`WorkerThread`功能内的`catch`块捕获，因为`iosvc->run()`功能在`try`块内。\n\n我们还可以看到，虽然我们为`io_service`对象发布了五次工作，但异常处理只处理了两个异常，因为一旦线程完成，线程中的`join_all()`函数将完成线程并退出程序。换句话说，我们可以说，一旦异常被处理，线程就会退出并加入调用。可能引发异常的附加代码将永远不会被调用。\n\n如果我们递归地放入`io_service`对象的工作调用呢？会不会导致一个无限运行的程序？让我们尝试无限地抛出异常。代码如下所示:\n\n```cpp\n/* exception2.cpp */\n#include <boost/asio.hpp>\n#include <boost/shared_ptr.hpp>\n#include <boost/thread.hpp>\n#include <boost/thread/mutex.hpp>\n#include <boost/bind.hpp>\n#include <iostream>\n\nboost::mutex global_stream_lock;\n\nvoid WorkerThread(boost::shared_ptr<boost::asio::io_service> iosvc, int counter) {\n  global_stream_lock.lock();\n  std::cout << \"Thread \" << counter << \" Start.\\n\";\n  global_stream_lock.unlock();\n\n  try {\n    iosvc->run();\n\n    global_stream_lock.lock();\n    std::cout << \"Thread \" << counter << \" End.\\n\";\n    global_stream_lock.unlock();\n  }\n  catch(std::exception &ex) {\n    global_stream_lock.lock();\n    std::cout << \"Message: \" << ex.what() << \".\\n\";\n    global_stream_lock.unlock();\n  }\n}\n\nvoid ThrowAnException(boost::shared_ptr<boost::asio::io_service> iosvc) {\n  global_stream_lock.lock();\n  std::cout << \"Throw Exception\\n\" ;\n  global_stream_lock.unlock();\n\n  iosvc->post(boost::bind(&ThrowAnException, iosvc));\n\n  throw(std::runtime_error(\"The Exception !!!\"));\n}\n\nint main(void) {\n  boost::shared_ptr<boost::asio::io_service> io_svc(\n    new boost::asio::io_service\n  );\n\n  boost::shared_ptr<boost::asio::io_service::work> worker(\n    new boost::asio::io_service::work(*io_svc)\n  );\n\n  global_stream_lock.lock();\n  std::cout << \"The program will exit once all work has finished.\\n\";\n  global_stream_lock.unlock();\n\n  boost::thread_group threads;\n  for(int i=1; i<=5; i++)\n    threads.create_thread(boost::bind(&WorkerThread, io_svc, i));\n\n  io_svc->post(boost::bind(&ThrowAnException, io_svc));\n\n  threads.join_all();\n\n  return 0;\n}\n```\n\n将前面的代码保存为`exception2.cpp`，并使用以下命令进行编译:\n\n```cpp\ng++ -Wall -ansi -I ../boost_1_58_0 exception2.cpp -o exception2 -L ../boost_1_58_0/stage/lib -l boost_system-mgw49-mt-1_58 -l ws2_32 -l libboost_thread-mgw49-mt-1_58\n\n```\n\n现在，让我们检查一下代码:\n\n```cpp\niosvc->post(boost::bind(&ThrowAnException, iosvc));\n\n```\n\n我们在`ThrowAnException`函数中添加了前面的代码片段。每次调用`ThrowAnException`函数，都会调用自己。那么，它应该是一个无限程序，因为有一个递归函数。让我们通过在控制台窗口中键入`exception2`命令来运行程序来证明这一点。输出如下所示:\n\n![Handling an exception](img/00035.jpeg)\n\n幸运的是，程序能够成功完成。发生这种情况是因为异常通过`run()`函数传播，并且工作线程退出。之后，所有线程结束，调用`join_all()`函数。这就是为什么程序退出，即使在`io_service`对象中还有工作。\n\n## 处理错误\n\n在我们之前的例子中，我们使用了没有任何参数的`run()`函数，但实际上，该函数有两个重载方法，`std::size_t run()`和`std::size_t run(boost::system::error_code & ec)`。后一种方法有一个错误代码参数，如果发生错误，将设置该参数。\n\n现在，让我们尝试在`run()`函数中使用一个错误代码作为输入参数。看看下面的代码:\n\n```cpp\n/* errorcode.cpp */\n#include <boost/asio.hpp>\n#include <boost/shared_ptr.hpp>\n#include <boost/thread.hpp>\n#include <boost/thread/mutex.hpp>\n#include <boost/bind.hpp>\n#include <iostream>\n\nboost::mutex global_stream_lock;\n\nvoid WorkerThread(boost::shared_ptr<boost::asio::io_service> iosvc, int counter) {\n  global_stream_lock.lock();\n  std::cout << \"Thread \" << counter << \" Start.\\n\";\n  global_stream_lock.unlock();\n\n  boost::system::error_code ec;\n  iosvc->run(ec);\n\n  if(ec) {\n    global_stream_lock.lock();\n    std::cout << \"Message: \" << ec << \".\\n\";\n    global_stream_lock.unlock();\n  }\n\n  global_stream_lock.lock();\n  std::cout << \"Thread \" << counter << \" End.\\n\";\n  global_stream_lock.unlock();\n}\n\nvoid ThrowAnException(boost::shared_ptr<boost::asio::io_service> iosvc) {\n  global_stream_lock.lock();\n  std::cout << \"Throw Exception\\n\" ;\n  global_stream_lock.unlock();\n\n  iosvc->post(boost::bind(&ThrowAnException, iosvc));\n\n  throw(std::runtime_error(\"The Exception !!!\"));\n}\n\nint main(void) {\n  boost::shared_ptr<boost::asio::io_service> io_svc(\n    new boost::asio::io_service\n  );\n\n  boost::shared_ptr<boost::asio::io_service::work> worker(\n    new boost::asio::io_service::work(*io_svc)\n  );\n\n  global_stream_lock.lock();\n  std::cout << \"The program will exit once all work has finished.\\n\";\n  global_stream_lock.unlock();\n\n  boost::thread_group threads;\n  for(int i=1; i<=5; i++)\n    threads.create_thread(boost::bind(&WorkerThread, io_svc, i));\n\n  io_svc->post(boost::bind(&ThrowAnException, io_svc));\n\n  threads.join_all();\n\n  return 0;\n}\n```\n\n将前面的代码保存为`errorcode.cpp`，使用以下命令编译代码:\n\n```cpp\ng++ -Wall -ansi -I ../boost_1_58_0 errorcode.cpp -o errorcode -L ../boost_1_58_0/stage/lib -l boost_system-mgw49-mt-1_58 -l ws2_32 -l libboost_thread-mgw49-mt-1_58\n\n```\n\n现在，通过在控制台中键入`errorcode`命令来运行程序。这样做的结果是，程序将崩溃。以下屏幕截图显示了输出:\n\n![Handling an error](img/00036.jpeg)\n\n我们打算通过使用以下代码来检索错误代码:\n\n```cpp\niosvc->run(ec);\n\n```\n\n我们可以通过使用`if`块来捕捉错误，如下所示:\n\n```cpp\nif(ec)\n\n```\n\n然而，在错误变量方法中，用户异常转化为`boost::asio`异常；因此，错误变量`ec`不会将用户异常解释为错误，因此异常不会被处理程序捕获。如果`Boost.Asio`库需要抛出一个错误，如果没有错误变量就会变成异常，或者转换成错误变量。如果我们继续使用`try-catch`块来捕捉任何异常或错误会更好。\n\n此外，我们必须检查异常的类型，它要么是系统故障，要么是上下文故障。如果是系统故障，那么我们必须调用`io_service`类中的`stop()`函数，以确保工作对象已经被销毁，以便程序能够退出。相反，如果异常是上下文失败，我们需要工作线程再次调用`run()`函数，以防止线程死亡。现在，看看下面的代码来理解这个概念:\n\n```cpp\n/* errorcode2.cpp */\n#include <boost/asio.hpp>\n#include <boost/shared_ptr.hpp>\n#include <boost/thread.hpp>\n#include <boost/thread/mutex.hpp>\n#include <boost/bind.hpp>\n#include <iostream>\n\nboost::mutex global_stream_lock;\n\nvoid WorkerThread(boost::shared_ptr<boost::asio::io_service> iosvc, int counter) {\n  global_stream_lock.lock();\n  std::cout << \"Thread \" << counter << \" Start.\\n\";\n  global_stream_lock.unlock();\n\n  while(true) {\n    try {\n      boost::system::error_code ec;\n      iosvc->run(ec);\n      if(ec) {\n        global_stream_lock.lock();\n        std::cout << \"Error Message: \" << ec << \".\\n\";\n        global_stream_lock.unlock();\n      }\n      break;\n    }\n    catch(std::exception &ex) {\n      global_stream_lock.lock();\n      std::cout << \"Exception Message: \" << ex.what() << \".\\n\";\n      global_stream_lock.unlock();\n    }\n  }\n\n  global_stream_lock.lock();\n  std::cout << \"Thread \" << counter << \" End.\\n\";\n  global_stream_lock.unlock();\n}\n\nvoid ThrowAnException(boost::shared_ptr<boost::asio::io_service> iosvc) {\n  global_stream_lock.lock();\n  std::cout << \"Throw Exception\\n\" ;\n  global_stream_lock.unlock();\n\n  iosvc->post(boost::bind(&ThrowAnException, iosvc));\n\n  throw(std::runtime_error(\"The Exception !!!\"));\n}\n\nint main(void) {\n  boost::shared_ptr<boost::asio::io_service> io_svc(\n    new boost::asio::io_service\n  );\n\n  boost::shared_ptr<boost::asio::io_service::work> worker(\n    new boost::asio::io_service::work(*io_svc)\n  );\n\n  global_stream_lock.lock();\n  std::cout << \"The program will exit once all work has finished.\\n\";\n  global_stream_lock.unlock();\n\n  boost::thread_group threads;\n  for(int i=1; i<=5; i++)\n    threads.create_thread(boost::bind(&WorkerThread, io_svc, i));\n\n  io_svc->post(boost::bind(&ThrowAnException, io_svc));\n\n  threads.join_all();\n\n  return 0;\n}\n```\n\n将前面的代码保存为`errorcode2.cpp`，然后通过执行以下命令进行编译:\n\n```cpp\ng++ -Wall -ansi -I ../boost_1_58_0 errorcode2.cpp -o errorcode2 -L ../boost_1_58_0/stage/lib -l boost_system-mgw49-mt-1_58 -l ws2_32 -l libboost_thread-mgw49-mt-1_58\n\n```\n\n如果运行程序，会看到它不会退出，需要按 *Ctrl* + *C* 才能停止程序:\n\n![Handling an error](img/00037.jpeg)\n\n如果我们看到下面的代码片段:\n\n```cpp\nwhile(true) {\n try {\n . . .\n iosvc->run(ec);\n if(ec)\n . . .\n }\n catch(std::exception &ex) {\n . . .\n }\n}\n\n```\n\n工作线程正在循环。当输出结果中出现异常时(由`Throw Exception`和`Exception Message: The Exception!!!`输出指示)，也是这种情况。再次调用`run()`函数，这样它会将新事件发布到队列中。当然，我们不希望在我们的应用中出现这种情况。\n\n# 使用定时器类为工作执行计时\n\nBoost C++ 库中有一个类，它提供了对定时器进行阻塞或异步等待直到定时器到期的能力，被称为**截止定时器**。截止时间计时器指示两种状态之一:过期或未过期。\n\n## 即将到期的计时器\n\n在这里，我们将创建一个将在 10 秒后到期的计时器。让我们看看下面的代码:\n\n```cpp\n/* timer.cpp */\n#include <boost/asio.hpp>\n#include <boost/shared_ptr.hpp>\n#include <boost/thread.hpp>\n#include <boost/thread/mutex.hpp>\n#include <boost/bind.hpp>\n#include <iostream>\n\nboost::mutex global_stream_lock;\n\nvoid WorkerThread(boost::shared_ptr<boost::asio::io_service> iosvc, int counter) {\n  global_stream_lock.lock();\n  std::cout << \"Thread \" << counter << \" Start.\\n\";\n  global_stream_lock.unlock();\n\n  while(true) {\n    try {\n      boost::system::error_code ec;\n      iosvc->run(ec);\n      if(ec) {\n        global_stream_lock.lock();\n        std::cout << \"Message: \" << ec << \".\\n\";\n        global_stream_lock.unlock();\n      }\n      break;\n    }\n    catch(std::exception &ex) {\n      global_stream_lock.lock();\n      std::cout << \"Message: \" << ex.what() << \".\\n\";\n      global_stream_lock.unlock();\n    }\n  }\n\n  global_stream_lock.lock();\n  std::cout << \"Thread \" << counter << \" End.\\n\";\n  global_stream_lock.unlock();\n}\n\nvoid TimerHandler(const boost::system::error_code & ec) {\n  if(ec) {\n    global_stream_lock.lock();\n    std::cout << \"Error Message: \" << ec << \".\\n\";\n    global_stream_lock.unlock();\n  }\n  else {\n    global_stream_lock.lock();\n    std::cout << \"You see this line because you have waited for 10 seconds.\\n\";\n    std::cout << \"Now press ENTER to exit.\\n\";\n    global_stream_lock.unlock();\n  }\n}\n\nint main(void) {\n  boost::shared_ptr<boost::asio::io_service> io_svc(\n    new boost::asio::io_service\n  );\n\n  boost::shared_ptr<boost::asio::io_service::work> worker(\n    new boost::asio::io_service::work(*io_svc)\n  );\n\n  global_stream_lock.lock();\n  std::cout << \"Wait for ten seconds to see what happen, \";\n  std::cout << \"otherwise press ENTER to exit!\\n\";\n  global_stream_lock.unlock();\n\n  boost::thread_group threads;\n  for(int i=1; i<=5; i++)\n    threads.create_thread(boost::bind(&WorkerThread, io_svc, i));\n\n  boost::asio::deadline_timer timer(*io_svc);\n  timer.expires_from_now(boost::posix_time::seconds(10));\n  timer.async_wait(TimerHandler);\n\n  std::cin.get();\n\n  io_svc->stop();\n\n  threads.join_all();\n\n  return 0;\n}\n```\n\n将前面的代码保存为`timer.cpp`并运行以下命令进行编译:\n\n```cpp\ng++ -Wall -ansi -I ../boost_1_58_0 timer.cpp -o timer -L ../boost_1_58_0/stage/lib -l boost_system-mgw49-mt-1_58 -l ws2_32 -l libboost_thread-mgw49-mt-1_58\n\n```\n\n现在，让我们在运行代码之前对其进行区分:\n\n```cpp\nboost::asio::deadline_timer timer(*io_svc);\ntimer.expires_from_now(boost::posix_time::seconds(10));\ntimer.async_wait(TimerHandler);\n\n```\n\n在程序调用`TimerHandler`函数之前，它必须等待 10 秒，因为我们使用的是来自`timer`对象的`expires_from_now`函数。`async_wait()`功能将一直等到定时器到期:\n\n```cpp\nvoid TimerHandler(const boost::system::error_code & ec) {\n if(ec)\n . . .\n}\nelse {\n global_stream_lock.lock();\n std::cout << \"You see this line because you have waited for 10 seconds.\\n\";\n std::cout << \"Now press ENTER to exit.\\n\";\n global_stream_lock.unlock();\n}\n\n```\n\n在定时器到期后，`TimerHandler`功能将被调用，由于没有错误，程序将执行`else`块内的代码。让我们运行程序来查看完整的输出:\n\n![An expiring timer](img/00038.jpeg)\n\n并且，由于我们使用了`async_wait()`功能，我们可以在看到线路之前点击*回车*键退出程序，**现在按回车键退出**。\n\n## 使用定时器和 boost::bind 功能\n\n让我们尝试创建一个循环计时器。我们必须初始化全局计时器对象，以便该对象成为共享对象。为了实现这一点，我们需要来自`shared_ptr`指针和`boost::bind`方法的帮助来确保线程安全，因为我们将使用一个共享对象:\n\n```cpp\n/* timer2.cpp */\n#include <boost/asio.hpp>\n#include <boost/shared_ptr.hpp>\n#include <boost/thread.hpp>\n#include <boost/thread/mutex.hpp>\n#include <boost/bind.hpp>\n#include <iostream>\n\nboost::mutex global_stream_lock;\n\nvoid WorkerThread(boost::shared_ptr<boost::asio::io_service> iosvc, int counter) {\n  global_stream_lock.lock();\n  std::cout << \"Thread \" << counter << \" Start.\\n\";\n  global_stream_lock.unlock();\n\n  while( true ) {\n    try {\n      boost::system::error_code ec;\n      iosvc->run(ec);\n      if(ec) {\n        global_stream_lock.lock();\n        std::cout << \"Message: \" << ec << \".\\n\";\n        global_stream_lock.unlock();\n      }\n      break;\n    }\n    catch(std::exception &ex) {\n      global_stream_lock.lock();\n      std::cout << \"Message: \" << ex.what() << \".\\n\";\n      global_stream_lock.unlock();\n    }\n  }\n\n  global_stream_lock.lock();\n  std::cout << \"Thread \" << counter << \" End.\\n\";\n  global_stream_lock.unlock();\n}\n\nvoid TimerHandler(\n  const boost::system::error_code &ec,\n  boost::shared_ptr<boost::asio::deadline_timer> tmr\n)\n{\n  if(ec) {\n    global_stream_lock.lock();\n    std::cout << \"Error Message: \" << ec << \".\\n\";\n    global_stream_lock.unlock();\n  }\n  else {\n    global_stream_lock.lock();\n    std::cout << \"You see this every three seconds.\\n\";\n    global_stream_lock.unlock();\n\n    tmr->expires_from_now( boost::posix_time::seconds(3));\n    tmr->async_wait(boost::bind(&TimerHandler, _1, tmr));\n  }\n}\n\nint main(void) {\n  boost::shared_ptr<boost::asio::io_service> io_svc(\n    new boost::asio::io_service\n  );\n\n  boost::shared_ptr<boost::asio::io_service::work> worker(\n    new boost::asio::io_service::work(*io_svc)\n  );\n\n  global_stream_lock.lock();\n  std::cout << \"Press ENTER to exit!\\n\";\n  global_stream_lock.unlock();\n\n  boost::thread_group threads;\n  for(int i=1; i<=5; i++)\n    threads.create_thread(boost::bind(&WorkerThread, io_svc, i));\n\n  boost::shared_ptr<boost::asio::deadline_timer> timer(\n    new boost::asio::deadline_timer(*io_svc)\n  );\n  timer->expires_from_now( boost::posix_time::seconds(3));\n  timer->async_wait(boost::bind(&TimerHandler, _1, timer));\n\n  std::cin.get();\n\n  io_svc->stop();\n\n  threads.join_all();\n\n  return 0;\n}\n```\n\n将前面的代码保存为`timer2.cpp`，运行以下命令进行编译:\n\n```cpp\ng++ -Wall -ansi -I ../boost_1_58_0 timer2.cpp -o timer2 -L ../boost_1_58_0/stage/lib -l boost_system-mgw49-mt-1_58 -l ws2_32 -l libboost_thread-mgw49-mt-1_58\n\n```\n\n现在，运行程序。我们会得到一个循环输出，点击*进入*键可以停止，如下所示:\n\n![Using the timer along with the boost::bind function](img/00039.jpeg)\n\n从输出中我们看到计时器每三秒勾选一次，用户按下*进入*键后，工作将停止。现在，让我们看看下面的代码片段:\n\n```cpp\ntimer->async_wait(boost::bind(&TimerHandler, _1, timer));\n\n```\n\n`boost::bind`功能帮助我们使用全局定时器对象。如果我们看得更深，我们可以使用`boost::bind`函数的`_1`参数。如果我们阅读`boost::bind`函数的文档，我们会发现`_1`参数是一个占位符参数，将被第一个输入参数替换。\n\n### 注\n\n有关使用占位符绑定的更多信息，请查看位于 www.boost.org/doc/libs/1_58_0/libs/bind/doc/html/bind.html 的官方 Boost 文档。\n\n有关占位符参数的更多信息，请参见[en.cppreference.com/w/cpp/utility/functional/placeholders](http://en.cppreference.com/w/cpp/utility/functional/placeholders)。\n\n## 使用定时器和增强::链功能\n\n由于定时器是异步执行的，因此定时器的执行可能不是在一个序列化的进程中。计时器可能在一个线程中执行，而另一个事件同时执行。如前所述，我们可以利用`strand`函数来序列化执行顺序。让我们看看下面的代码片段:\n\n```cpp\n/* timer3.cpp */\n#include <boost/asio.hpp>\n#include <boost/shared_ptr.hpp>\n#include <boost/thread.hpp>\n#include <boost/thread/mutex.hpp>\n#include <boost/bind.hpp>\n#include <iostream>\n\nboost::mutex global_stream_lock;\n\nvoid WorkerThread(boost::shared_ptr<boost::asio::io_service> iosvc, int counter) {\n  global_stream_lock.lock();\n  std::cout << \"Thread \" << counter << \" Start.\\n\";\n  global_stream_lock.unlock();\n\n  while( true ) {\n    try {\n      boost::system::error_code ec;\n      iosvc->run(ec);\n      if(ec) {\n        global_stream_lock.lock();\n        std::cout << \"Message: \" << ec << \".\\n\";\n        global_stream_lock.unlock();\n      }\n      break;\n    }\n    catch(std::exception &ex) {\n      global_stream_lock.lock();\n      std::cout << \"Message: \" << ex.what() << \".\\n\";\n      global_stream_lock.unlock();\n    }\n  }\n\n  global_stream_lock.lock();\n  std::cout << \"Thread \" << counter << \" End.\\n\";\n  global_stream_lock.unlock();\n}\n\nvoid TimerHandler(\n  const boost::system::error_code &ec,\n  boost::shared_ptr<boost::asio::deadline_timer> tmr,\n  boost::shared_ptr<boost::asio::io_service::strand> strand\n)\n{\n  if(ec) {\n    global_stream_lock.lock();\n    std::cout << \"Error Message: \" << ec << \".\\n\";\n    global_stream_lock.unlock();\n  }\n  else {\n    global_stream_lock.lock();\n    std::cout << \"You see this every three seconds.\\n\";\n    global_stream_lock.unlock();\n\n    tmr->expires_from_now( boost::posix_time::seconds(1));\n    tmr->async_wait(\n      strand->wrap(boost::bind(&TimerHandler, _1, tmr, strand))\n    );\n  }\n}\n\nvoid Print(int number) {\n  std::cout << \"Number: \" << number << std::endl;\n  boost::this_thread::sleep( boost::posix_time::milliseconds(500));\n}\n\nint main(void) {\n  boost::shared_ptr<boost::asio::io_service> io_svc(\n    new boost::asio::io_service\n  );\n\n  boost::shared_ptr<boost::asio::io_service::work> worker(\n    new boost::asio::io_service::work(*io_svc)\n  );\n  boost::shared_ptr<boost::asio::io_service::strand> strand(\n    new boost::asio::io_service::strand(*io_svc)\n  );\n\n  global_stream_lock.lock();\n  std::cout << \"Press ENTER to exit!\\n\";\n  global_stream_lock.unlock();\n\n  boost::thread_group threads;\n  for(int i=1; i<=5; i++)\n    threads.create_thread(boost::bind(&WorkerThread, io_svc, i));\n\n  boost::this_thread::sleep(boost::posix_time::seconds(1));\n\n  strand->post(boost::bind(&Print, 1));\n  strand->post(boost::bind(&Print, 2));\n  strand->post(boost::bind(&Print, 3));\n  strand->post(boost::bind(&Print, 4));\n  strand->post(boost::bind(&Print, 5));\n\n  boost::shared_ptr<boost::asio::deadline_timer> timer(\n    new boost::asio::deadline_timer(*io_svc)\n  );\n\n  timer->expires_from_now( boost::posix_time::seconds(1));\n  timer->async_wait( \n    strand->wrap(boost::bind(&TimerHandler, _1, timer, strand))\n  );\n\n  std::cin.get();\n\n  io_svc->stop();\n\n  threads.join_all();\n\n  return 0;\n}\n```\n\n将前面的代码保存为`timer3.cpp`，并通过运行以下命令进行编译:\n\n```cpp\ng++ -Wall -ansi -I ../boost_1_58_0 timer3.cpp -o timer3 -L ../boost_1_58_0/stage/lib -l boost_system-mgw49-mt-1_58 -l ws2_32 -l libboost_thread-mgw49-mt-1_58\n\n```\n\n现在，通过在控制台中键入`timer3`命令来运行程序，我们将获得以下输出:\n\n![Using the timer along with the boost::strand function](img/00040.jpeg)\n\n从的输出中，我们可以看到前五个`work`对象首先执行，因为它们必须串行执行，然后执行`TimerHandler()`功能。在执行定时器线程之前，必须首先完成`work`对象。如果我们去掉`strand`包装，程序的流程会很混乱，因为我们没有把`std::cout`功能锁定在`Print()`功能里面。\n\n# 总结\n\n我们已经使用`strand`对象成功序列化了`io_service`对象的工作，所以我们可以确保我们设计的工作顺序。我们还可以通过使用错误和异常处理来确保我们的程序平稳运行，没有任何崩溃。最后，在本章中，我们讨论了等待时间，因为这在创建网络应用时非常重要。\n\n现在，让我们进入下一章，讨论如何创建一个服务器-客户端应用，使服务器和客户端双方之间的通信成为可能。"
  },
  {
    "path": "docs/boost-asio-cpp-net-prog-2e/6.md",
    "content": "# 六、创建客户端——服务器应用\n\n在前一章中，我们深入研究了`Boost.Asio`库，这对开发网络应用非常重要。现在，我们将深入讨论一个**客户端-服务器**应用，它可以通过计算机网络在两台或多台计算机之间相互通信。其中一个叫**客户端**，另一个叫**服务器**。\n\n我们将讨论服务器的开发，它能够发送和接收来自客户端的数据流量，并创建一个客户端程序来接收数据流量。在本章中，我们将讨论以下主题:\n\n*   在客户端和服务器之间建立连接\n*   在客户端和服务器之间发送和接收数据\n*   包装最常用的代码，通过避免代码重用来简化编程过程\n\n# 建立连接\n\n我们在[第 2 章](2.html#page \"Chapter 2. Understanding the Networking Concepts\")、*了解网络概念*中讨论了两种类型的互联网协议。它们是传输控制协议和用户数据报协议。TCP 是面向连接的，这意味着数据可以在连接建立后立即发送。相比之下，UDP 是无连接的互联网协议，这意味着该协议只是将数据直接发送到目的设备。这一章我们只讲 TCP 因此，我们必须首先建立连接。只有在客户端和服务器这两方接受连接的情况下，才能建立连接。在这里，我们将尝试同步和异步建立连接。\n\n## 同步客户端\n\n我们从建立与远程主机的同步连接开始。它作为一个客户端，将打开与帕克特出版网站([www.packtpub.com](http://www.packtpub.com))的连接。我们将使用 TCP 协议，正如我们之前在[第 2 章](2.html#page \"Chapter 2. Understanding the Networking Concepts\")*中讨论的，理解网络概念*。下面是代码:\n\n```cpp\n/* connectsync.cpp */\n#include <boost/asio.hpp>\n#include <boost/shared_ptr.hpp>\n#include <boost/thread.hpp>\n#include <boost/thread/mutex.hpp>\n#include <boost/bind.hpp>\n#include <boost/lexical_cast.hpp>\n\nboost::mutex global_stream_lock;\n\nvoid WorkerThread(boost::shared_ptr<boost::asio::io_service> iosvc, int counter) {\n  global_stream_lock.lock();\n  std::cout << \"Thread \" << counter << \" Start.\\n\";\n  global_stream_lock.unlock();\n\n  while(true) {\n    try {\n      boost::system::error_code ec;\n      iosvc->run(ec);\n      if(ec) {\n        global_stream_lock.lock();\n        std::cout << \"Message: \" << ec << \".\\n\";\n        global_stream_lock.unlock();\n      }\n      break;\n    }\n    catch(std::exception &ex) {\n      global_stream_lock.lock();\n      std::cout << \"Message: \" << ex.what() << \".\\n\";\n      global_stream_lock.unlock();\n    }\n  }\n\n  global_stream_lock.lock();\n  std::cout << \"Thread \" << counter << \" End.\\n\";\n  global_stream_lock.unlock();\n}\n\nint main(void) {\n  boost::shared_ptr<boost::asio::io_service> io_svc(\n    new boost::asio::io_service\n  );\n\n  boost::shared_ptr<boost::asio::io_service::work> worker(\n    new boost::asio::io_service::work(*io_svc)\n  );\n  boost::shared_ptr<boost::asio::io_service::strand> strand(\n    new boost::asio::io_service::strand(*io_svc)\n  );\n\n  global_stream_lock.lock();\n  std::cout << \"Press ENTER to exit!\\n\";\n  global_stream_lock.unlock();\n\n  boost::thread_group threads;\n  for(int i=1; i<=2; i++)\n    threads.create_thread(boost::bind(&WorkerThread, io_svc, i));\n\n  boost::asio::ip::tcp::socket sckt(*io_svc);\n\n  try {\n    boost::asio::ip::tcp::resolver resolver(*io_svc);\n    boost::asio::ip::tcp::resolver::query query(\"www.packtpub.com\", \n      boost::lexical_cast<std::string>(80)\n    );\n    boost::asio::ip::tcp::resolver::iterator iterator = resolver.resolve(query);\n    boost::asio::ip::tcp::endpoint endpoint = *iterator;\n\n    global_stream_lock.lock();\n    std::cout << \"Connecting to: \" << endpoint << std::endl;\n    global_stream_lock.unlock();\n\n    sckt.connect(endpoint); \n    std::cout << \"Connected!\\n\";\n  }\n  catch(std::exception &ex) {\n    global_stream_lock.lock();\n    std::cout << \"Message: \" << ex.what() << \".\\n\";\n    global_stream_lock.unlock();\n  }\n\n  std::cin.get();\n\n  boost::system::error_code ec;\n  sckt.shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec);\n  sckt.close(ec);\n\n  io_svc->stop();\n\n  threads.join_all();\n\n  return 0;\n}\n```\n\n将前面的代码保存为`connectsync.cpp`，运行以下命令编译代码:\n\n```cpp\ng++ -Wall -ansi -I ../boost_1_58_0 connectsync.cpp -o connectsync -L ../boost_1_58_0/stage/lib -l boost_system-mgw49-mt-1_58 -l ws2_32 -l libboost_thread-mgw49-mt-1_58\n\n```\n\n在控制台输入`connectsync`运行程序，我们应该会得到如下输出:\n\n![A synchronous client](img/00041.jpeg)\n\n我们一按*进入*键，程序就会退出。\n\n现在，让我们分析一下代码。正如我们在前面的代码中所看到的，我们使用前面的示例代码并插入一行代码，以便能够建立连接。让我们注意我们插入的那一行:\n\n```cpp\nboost::asio::ip::tcp::socket sckt(*io_svc);\n\n```\n\n我们现在有了一个全局变量`socket`。该变量将用于提供套接字功能。它来自命名空间`boost::asio::ip::tcp`，因为我们使用 TCP 作为我们的协议:\n\n```cpp\nboost::asio::ip::tcp::resolver resolver(*io_svc);\nboost::asio::ip::tcp::resolver::query query(\"www.packtpub.com\",\n boost::lexical_cast<std::string>(80)\n);\nboost::asio::ip::tcp::resolver::iterator iterator =\nresolver.resolve(query);\n\n```\n\n我们也使用命名空间`boost::asio::ip::tcp::resolver`。它用于获取我们要连接的远程主机的地址。使用`query()`类，我们传递互联网地址和端口作为参数。但是因为我们使用整数类型作为端口号，所以我们必须使用`lexical_cast`将其转换为字符串。查询类用于描述可以传递给解析器的查询。然后，通过使用`iterator`类，我们将根据解析器返回的结果定义迭代器:\n\n```cpp\nboost::asio::ip::tcp::endpoint endpoint = *iterator;\n\n```\n\n迭代器创建成功后，我们将其赋予`endpoint`类型变量。端点将存储由`resolver`生成的`ip`地址列表:\n\n```cpp\nsckt.connect(endpoint);\n\n```\n\n然后，`connect()`成员函数将套接字连接到端点，这是我们之前指定的。如果一切正常运行，并且没有抛出错误或异常，那么现在就建立了连接:\n\n```cpp\nboost::system::error_code ec;\nsckt.shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec);\nsckt.close(ec);\n\n```\n\n要释放连接，我们必须先使用`shutdown()`成员函数禁用套接字上的数据发送和接收过程；然后，我们调用`close()`成员函数关闭套接字。\n\n当我们运行程序并获得像前面的图像一样的输出时，它会通知我们连接已经建立。例如，我们可以在`query()`类中将端口号更改为`110`，这是远程远程登录服务协议，如下所示:\n\n```cpp\nboost::asio::ip::tcp::resolver::query query(\"www.packtpub.com\",\n boost::lexical_cast<std::string>(110)\n);\n\n```\n\n然后，程序会抛出一个异常，输出如下:\n\n![A synchronous client](img/00042.jpeg)\n\n从的输出中，我们可以得出结论，连接已经被目标机器拒绝，因为我们计划连接到的端口已经关闭。这意味着通过使用端口`80`，也就是**超文本传输协议** ( **HTTP** )，我们可以与 Packt Publishing 网站建立连接。\n\n## 异步客户端\n\n我们已经能够同步建立连接。但是，如果我们需要异步连接到目标，以便程序在尝试连接时不会冻结，那该怎么办？让我们看看下面的代码来找到答案:\n\n```cpp\n/* connectasync.cpp */\n#include <boost/asio.hpp>\n#include <boost/shared_ptr.hpp>\n#include <boost/thread.hpp>\n#include <boost/thread/mutex.hpp>\n#include <boost/bind.hpp>\n#include <boost/lexical_cast.hpp>\n#include <iostream>\n#include <string>\n\nboost::mutex global_stream_lock;\n\nvoid WorkerThread(boost::shared_ptr<boost::asio::io_service> iosvc, int counter) {\n  global_stream_lock.lock();\n  std::cout << \"Thread \" << counter << \" Start.\\n\";\n  global_stream_lock.unlock();\n\n  while(true) {\n    try {\n      boost::system::error_code ec;\n      iosvc->run(ec);\n      if(ec) {\n        global_stream_lock.lock();\n        std::cout << \"Message: \" << ec << \".\\n\";\n        global_stream_lock.unlock();\n      }\n      break;\n    }\n    catch(std::exception &ex) {\n      global_stream_lock.lock();\n      std::cout << \"Message: \" << ex.what() << \".\\n\";\n      global_stream_lock.unlock();\n    }\n  }\n\n  global_stream_lock.lock();\n  std::cout << \"Thread \" << counter << \" End.\\n\";\n  global_stream_lock.unlock();\n}\n\nvoid OnConnect(const boost::system::error_code &ec) {\n  if(ec) {\n    global_stream_lock.lock();\n    std::cout << \"OnConnect Error: \" << ec << \".\\n\";\n    global_stream_lock.unlock();\n  }\n  else {\n    global_stream_lock.lock();\n    std::cout << \"Connected!.\\n\";\n    global_stream_lock.unlock();\n  }\n}\n\nint main(void) {\n  boost::shared_ptr<boost::asio::io_service> io_svc(\n    new boost::asio::io_service\n  );\n\n  boost::shared_ptr<boost::asio::io_service::work> worker(\n    new boost::asio::io_service::work(*io_svc)\n  );\n\n  boost::shared_ptr<boost::asio::io_service::strand> strand(\n    new boost::asio::io_service::strand(*io_svc)\n  );\n\n  global_stream_lock.lock();\n  std::cout << \"Press ENTER to exit!\\n\";\n  global_stream_lock.unlock();\n\n  boost::thread_group threads;\n  for(int i=1; i<=2; i++)\n    threads.create_thread(boost::bind(&WorkerThread, io_svc, i));\n\n  boost::shared_ptr<boost::asio::ip::tcp::socket> sckt(\n    new boost::asio::ip::tcp::socket(*io_svc)\n  );\n\n  try {\n    boost::asio::ip::tcp::resolver resolver(*io_svc);\n    boost::asio::ip::tcp::resolver::query query(\"www.packtpub.com\",\n      boost::lexical_cast<std::string>(80)\n    );\n    boost::asio::ip::tcp::resolver::iterator iterator = resolver.resolve( query );\n    boost::asio::ip::tcp::endpoint endpoint = *iterator;\n\n    global_stream_lock.lock();\n    std::cout << \"Connecting to: \" << endpoint << std::endl;\n    global_stream_lock.unlock();\n\n    sckt->async_connect(endpoint, boost::bind(OnConnect, _1));\n  }\n  catch(std::exception &ex) {\n    global_stream_lock.lock();\n    std::cout << \"Message: \" << ex.what() << \".\\n\";\n    global_stream_lock.unlock();\n  }\n\n  std::cin.get();\n\n  boost::system::error_code ec;\n  sckt->shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec);\n  sckt->close(ec);\n\n  io_svc->stop();\n\n  threads.join_all();\n\n  return 0;\n}\n```\n\n然后，将前面的代码保存为`connectasync.cpp`并运行以下命令编译代码:\n\n```cpp\ng++ -Wall -ansi -I ../boost_1_58_0 connectasync.cpp -o connectasync -L ../boost_1_58_0/stage/lib -l boost_system-mgw49-mt-1_58 -l ws2_32 -l libboost_thread-mgw49-mt-1_58\n\n```\n\n尝试运行程序，应该会得到如下输出:\n\n![An asynchronous client](img/00043.jpeg)\n\n正如我们在前面的代码中看到的，我们添加了`OnConnect()`函数。因为`socket`对象是不可复制的，并且我们需要确保它在处理程序等待调用时仍然有效，所以我们必须使用`boost::shared_ptr`命名空间。我们还使用`boost::bind`名称空间来调用处理程序，即`OnConnect()`函数。\n\n## 异步服务器\n\n我们已经知道如何同步和异步连接到远程主机。现在，我们将创建服务器程序，与之前创建的客户端程序进行对话。因为我们将处理`boost::asio`命名空间中的异步程序，所以我们将只讨论异步服务器中的客户端程序。让我们看看下面的代码:\n\n```cpp\n/* serverasync.cpp */\n#include <boost/asio.hpp>\n#include <boost/shared_ptr.hpp>\n#include <boost/thread.hpp>\n#include <boost/thread/mutex.hpp>\n#include <boost/bind.hpp>\n#include <boost/lexical_cast.hpp>\n#include <iostream>\n#include <string>\n\nboost::mutex global_stream_lock;\n\nvoid WorkerThread(boost::shared_ptr<boost::asio::io_service> iosvc, int counter) {\n  global_stream_lock.lock();\n  std::cout << \"Thread \" << counter << \" Start.\\n\";\n  global_stream_lock.unlock();\n\n  while(true) {\n    try {\n      boost::system::error_code ec;\n      iosvc->run(ec);\n      if(ec) {\n        global_stream_lock.lock();\n        std::cout << \"Message: \" << ec << \".\\n\";\n        global_stream_lock.unlock();\n      }\n      break;\n    }\n    catch(std::exception &ex) {\n      global_stream_lock.lock();\n      std::cout << \"Message: \" << ex.what() << \".\\n\";\n      global_stream_lock.unlock();\n    }\n  }\n\n  global_stream_lock.lock();\n  std::cout << \"Thread \" << counter << \" End.\\n\";\n  global_stream_lock.unlock();\n}\n\nvoid OnAccept(const boost::system::error_code &ec) {\n  if(ec) {\n    global_stream_lock.lock();\n    std::cout << \"OnAccept Error: \" << ec << \".\\n\";\n    global_stream_lock.unlock();\n  }\n  else {\n    global_stream_lock.lock();\n    std::cout << \"Accepted!\" << \".\\n\";\n    global_stream_lock.unlock();\n  }\n}\n\nint main(void) {\n  boost::shared_ptr<boost::asio::io_service> io_svc(\n    new boost::asio::io_service\n  );\n\n  boost::shared_ptr<boost::asio::io_service::work> worker(\n    new boost::asio::io_service::work(*io_svc)\n  );\n\n  boost::shared_ptr<boost::asio::io_service::strand> strand(\n    new boost::asio::io_service::strand(*io_svc)\n  );\n\n  global_stream_lock.lock();\n  std::cout << \"Press ENTER to exit!\\n\";\n  global_stream_lock.unlock();\n\n  boost::thread_group threads;\n  for(int i=1; i<=2; i++)\n    threads.create_thread(boost::bind(&WorkerThread, io_svc, i));\n\n  boost::shared_ptr< boost::asio::ip::tcp::acceptor > acceptor(\n    new boost::asio::ip::tcp::acceptor(*io_svc)\n  );\n\n  boost::shared_ptr<boost::asio::ip::tcp::socket> sckt(\n    new boost::asio::ip::tcp::socket(*io_svc)\n  );\n\n  try {\n    boost::asio::ip::tcp::resolver resolver(*io_svc);\n    boost::asio::ip::tcp::resolver::query query(\n      \"127.0.0.1\", \n      boost::lexical_cast<std::string>(4444)\n    );\n    boost::asio::ip::tcp::endpoint endpoint = *resolver.resolve(query);\n    acceptor->open(endpoint.protocol());\n    acceptor->set_option(\n      boost::asio::ip::tcp::acceptor::reuse_address(false));\n    acceptor->bind(endpoint);\n    acceptor->listen(boost::asio::socket_base::max_connections);\n    acceptor->async_accept(*sckt, boost::bind(OnAccept, _1));\n\n    global_stream_lock.lock();\n    std::cout << \"Listening on: \" << endpoint << std::endl;\n    global_stream_lock.unlock();\n  }\n  catch(std::exception &ex) {\n    global_stream_lock.lock();\n    std::cout << \"Message: \" << ex.what() << \".\\n\";\n    global_stream_lock.unlock();\n  }\n\n  std::cin.get();\n\n  boost::system::error_code ec;\n  acceptor->close(ec);\n\n  sckt->shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec);\n  sckt->close(ec);\n\n  io_svc->stop();\n\n  threads.join_all();\n\n  return 0;\n}\n```\n\n将前面的代码保存为`serverasync.cpp`，运行以下命令编译代码:\n\n```cpp\ng++ -Wall -ansi -I ../boost_1_58_0 serverasync.cpp -o serverasync -L ../boost_1_58_0/stage/lib -l boost_system-mgw49-mt-1_58 -l ws2_32 -l libboost_thread-mgw49-mt-1_58 –l mswsock\n\n```\n\n在运行程序之前，让我们区分代码。我们现在有了一个新的对象，那就是`tcp::acceptor`。此对象用于接受新的套接字连接。由于使用了`accept()`功能，我们需要在编译过程中添加`mswsock`库:\n\n```cpp\nacptor->open(endpoint.protocol());\nacptor->set_option\n(boost::asio::ip::tcp::acceptor::reuse_address(false));\nacptor->bind(endpoint);\nacptor->listen(boost::asio::socket_base::max_connections);\nacptor->async_accept(*sckt, boost::bind(OnAccept, _1));\n\n```\n\n从前面的代码片段中，我们可以看到程序调用`open()`函数，通过使用从`endpoint`变量中检索的协议来打开接受者。然后，通过使用`set_option`功能，我们在接受者上设置了一个不重用地址的选项。受体也使用`bind()`功能绑定到端点。之后，我们调用`listen()`函数，使接受者进入监听新连接的状态。最后，接受者将使用`async_accept()`功能接受新的连接，这将启动异步接受。\n\n现在，是运行程序的时候了。我们需要在这里打开两个控制台。第一个控制台用于程序本身，第二个控制台用于调用`telnet`命令与服务器建立连接。我们只需要在运行完`serverasync`程序后运行`telnet 127.0.0.1 4444`命令(可以参考[第二章](2.html#page \"Chapter 2. Understanding the Networking Concepts\")、*了解联网概念*，在命令提示符下调用`telnet`命令)。输出应该如下所示:\n\n![An asynchronous server](img/00044.jpeg)\n\n从上图可以看出，程序启动时正在监听端口`4444`，当我们调用`telnet`命令开始连接到端口`4444`后，程序接受连接。但是，因为我们只有一个 socket 对象，只调用`async_accept()`函数一次，程序只接受一个连接。\n\n# 读写插座\n\n我们正式能够建立客户端-服务器连接。现在，我们将读写插座，使连接更加有用。我们将修改我们的之前的代码，`serverasync.cpp`，并添加`basic_stream_socket`对象，它提供了面向流的套接字功能。\n\n### 注\n\n欲了解更多关于`basic_stream_socket`对象的详细信息，可登陆[www . boost . org/doc/libs/1 _ 58 _ 0/doc/html/boost _ asio/reference/basic _ stream _ socket . html](http://www.boost.org/doc/libs/1_58_0/doc/html/boost_asio/reference/basic_stream_socket.html)。\n\n现在，以为例看一下下面包含读取和写入套接字过程的代码:\n\n```cpp\n/* readwritesocket.cpp */\n#include <boost/asio.hpp>\n#include <boost/shared_ptr.hpp>\n#include <boost/thread.hpp>\n#include <boost/thread/mutex.hpp>\n#include <boost/bind.hpp>\n#include <boost/lexical_cast.hpp>\n#include <boost/cstdint.hpp>\n#include <boost/enable_shared_from_this.hpp>\n#include <iostream>\n#include <string>\n\nboost::mutex global_stream_lock;\n\nvoid WorkerThread(boost::shared_ptr<boost::asio::io_service> iosvc, int counter) {\n  global_stream_lock.lock();\n  std::cout << \"Thread \" << counter << \" Start.\\n\";\n  global_stream_lock.unlock();\n\n  while(true) {\n    try {\n      boost::system::error_code ec;\n      iosvc->run(ec);\n      if(ec) {\n        global_stream_lock.lock();\n        std::cout << \"Message: \" << ec << \".\\n\";\n        global_stream_lock.unlock();\n      }\n      break;\n    }\n    catch(std::exception &ex) {\n      global_stream_lock.lock();\n      std::cout << \"Message: \" << ex.what() << \".\\n\";\n      global_stream_lock.unlock();\n    }\n  }\n\n  global_stream_lock.lock();\n  std::cout << \"Thread \" << counter << \" End.\\n\";\n  global_stream_lock.unlock();\n}\n\nstruct ClientContext : public boost::enable_shared_from_this<ClientContext> {\n  boost::asio::ip::tcp::socket m_socket;\n\n  std::vector<boost::uint8_t> m_recv_buffer;\n  size_t m_recv_buffer_index;\n\n  std::list<std::vector<boost::uint8_t> > m_send_buffer;\n\n  ClientContext(boost::asio::io_service & io_service)\n  : m_socket(io_service), m_recv_buffer_index(0) {\n    m_recv_buffer.resize(4096);\n  }\n\n  ~ClientContext() {\n  }\n\n  void Close() {\n    boost::system::error_code ec;\n    m_socket.shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec);\n    m_socket.close(ec);\n  }\n\n  void OnSend(const boost::system::error_code &ec, std::list<std::vector<boost::uint8_t> >::iterator itr) {\n    if(ec) {\n      global_stream_lock.lock();\n      std::cout << \"OnSend Error: \" << ec << \".\\n\";\n      global_stream_lock.unlock();\n\n      Close();\n    }\n    else {\n      global_stream_lock.lock();\n      std::cout << \"Sent \" << (*itr).size() << \" bytes.\" << std::endl;\n      global_stream_lock.unlock();\n    }\n    m_send_buffer.erase(itr);\n\n    // Start the next pending send\n    if(!m_send_buffer.empty()) {\n      boost::asio::async_write(\n        m_socket,\n        boost::asio::buffer(m_send_buffer.front()),\n        boost::bind(\n          &ClientContext::OnSend,\n          shared_from_this(),\n          boost::asio::placeholders::error,\n          m_send_buffer.begin()\n        )\n      );\n    }\n  }\n\n  void Send(const void * buffer, size_t length) {\n    bool can_send_now = false;\n\n    std::vector<boost::uint8_t> output;\n    std::copy((const boost::uint8_t *)buffer, (const boost::uint8_t *)buffer + length, std::back_inserter(output));\n\n    // Store if this is the only current send or not\n    can_send_now = m_send_buffer.empty();\n\n    // Save the buffer to be sent\n    m_send_buffer.push_back(output);\n\n    // Only send if there are no more pending buffers waiting!\n    if(can_send_now) {\n      // Start the next pending send\n      boost::asio::async_write(\n        m_socket,\n        boost::asio::buffer(m_send_buffer.front()),\n        boost::bind(\n          &ClientContext::OnSend,\n          shared_from_this(),\n          boost::asio::placeholders::error,\n          m_send_buffer.begin()\n        )\n      );\n    }\n  }\n\n  void OnRecv(const boost::system::error_code &ec, size_t bytes_transferred) {\n    if(ec) {\n      global_stream_lock.lock();\n      std::cout << \"OnRecv Error: \" << ec << \".\\n\";\n      global_stream_lock.unlock();\n\n      Close();\n    }\n    else \t{\n      // Increase how many bytes we have saved up\n      m_recv_buffer_index += bytes_transferred;\n\n      // Debug information\n      global_stream_lock.lock();\n      std::cout << \"Recv \" << bytes_transferred << \" bytes.\" << std::endl;\n      global_stream_lock.unlock();\n\n      // Dump all the data\n      global_stream_lock.lock();\n      for(size_t x = 0; x < m_recv_buffer_index; ++ x) {\n\n        std::cout << (char)m_recv_buffer[x] << \" \";\n        if((x + 1) % 16 == 0) {\n          std::cout << std::endl;\n        }\n      }\n      std::cout << std::endl << std::dec;\n      global_stream_lock.unlock();\n\n      // Clear all the data\n      m_recv_buffer_index = 0;\n\n      // Start the next receive cycle\n      Recv();\n    }\n  }\n\n  void Recv() {\n    m_socket.async_read_some(\n      boost::asio::buffer(\n        &m_recv_buffer[m_recv_buffer_index],\n        m_recv_buffer.size() - m_recv_buffer_index),\n      boost::bind(&ClientContext::OnRecv, shared_from_this(), _1, _2)\n    );\n  }\n};\n\nvoid OnAccept(const boost::system::error_code &ec, boost::shared_ptr<ClientContext> clnt) {\n  if(ec) {\n    global_stream_lock.lock();\n    std::cout << \"OnAccept Error: \" << ec << \".\\n\";\n    global_stream_lock.unlock();\n  }\n  else {\n    global_stream_lock.lock();\n    std::cout << \"Accepted!\" << \".\\n\";\n    global_stream_lock.unlock();\n\n    // 2 bytes message size, followed by the message\n    clnt->Send(\"Hi there!\", 9);\n    clnt->Recv();\n  }\n}\n\nint main(void) {\n  boost::shared_ptr<boost::asio::io_service> io_svc(\n    new boost::asio::io_service\n  );\n\n  boost::shared_ptr<boost::asio::io_service::work> worker(\n    new boost::asio::io_service::work(*io_svc)\n  );\n\n  boost::shared_ptr<boost::asio::io_service::strand> strand(\n    new boost::asio::io_service::strand(*io_svc)\n  );\n\n  global_stream_lock.lock();\n  std::cout << \"Press ENTER to exit!\\n\";\n  global_stream_lock.unlock();\n\n  // We just use one worker thread \n  // in order that no thread safety issues\n  boost::thread_group threads;\n  threads.create_thread(boost::bind(&WorkerThread, io_svc, 1));\n\n  boost::shared_ptr< boost::asio::ip::tcp::acceptor > acceptor(\n    new boost::asio::ip::tcp::acceptor(*io_svc)\n  );\n\n  boost::shared_ptr<ClientContext> client(\n    new ClientContext(*io_svc)\n  );\n\n  try {\n    boost::asio::ip::tcp::resolver resolver(*io_svc);\n    boost::asio::ip::tcp::resolver::query query(\n      \"127.0.0.1\",\n      boost::lexical_cast<std::string>(4444)\n    );\n    boost::asio::ip::tcp::endpoint endpoint = *resolver.resolve(query);\n    acceptor->open(endpoint.protocol());\n    acceptor->set_option(boost::asio::ip::tcp::acceptor::reuse_address(false));\n    acceptor->bind(endpoint);\n    acceptor->listen(boost::asio::socket_base::max_connections);\n    acceptor->async_accept(client->m_socket, boost::bind(OnAccept, _1, client));\n\n    global_stream_lock.lock();\n    std::cout << \"Listening on: \" << endpoint << std::endl;\n    global_stream_lock.unlock();\n  }\n  catch(std::exception &ex) {\n    global_stream_lock.lock();\n    std::cout << \"Message: \" << ex.what() << \".\\n\";\n    global_stream_lock.unlock();\n  }\n\n  std::cin.get();\n\n  boost::system::error_code ec;\n  acceptor->close(ec);\n\n  io_svc->stop();\n\n  threads.join_all();\n\n  return 0;\n}\n```\n\n将前面的代码保存为`readwritesocket.cpp`，使用以下命令编译代码:\n\n```cpp\ng++ -Wall -ansi -I ../boost_1_58_0 readwritesocket.cpp -o readwritesocket -L ../boost_1_58_0/stage/lib -l boost_system-mgw49-mt-1_58 -l ws2_32 -l libboost_thread-mgw49-mt-1_58 -l mswsock\n\n```\n\n如果我们将`readwritesocket.cpp`文件的代码与`serverasync.cpp`文件进行比较，我们会发现我们添加了一个名为`ClientContext`的新类。它包含五个成员功能:`Send()`、`OnSend()`、`Recv()`、`OnRecv()`和`Close()`。\n\n## 发送()和结束()功能\n\n在`Send()`功能中，我们输入一组字符和它们的长度。在函数发送字符数组之前，它必须检查`m_send_buffer`参数是否为空。只有当缓冲区不为空时，发送过程才会发生。\n\n`boost::asio::async_write`命名空间写入套接字并调用`OnSend()`函数处理程序。然后，它擦除缓冲区，并发送下一个挂起的数据(如果有)。现在，每次我们按下`telnet`窗口中的任何键，它都会显示我们键入的内容，因为`readwritesocket`项目会将我们键入的内容发送回`telnet`窗口。\n\n## Recv()和 OnRecv()函数\n\n与到`Send()`函数相反，`Recv()`函数将调用`async_read_some()`函数来接收数据集，`OnRecv()`函数处理程序将接收到的数据格式化为十六进制格式。\n\n# 包装网络代码\n\n为了方便起见，让我们为网络应用创建一个包装器。在使用这个包装器时，我们不需要一次又一次地重用我们的代码；从而使我们的编程过程更简单、更高效。现在，只需创建两个名为`wrapper.h`和`wrapper.cpp`的文件，我们将在下一个代码中将其包含在编译过程中。因为源代码很长，不方便在这本书里打印，所以我把它们做成可下载的文件，你可以在这本书的知识库[www . packtpub . com/networking-and-servers/boost asio-c-network-programming-second edition](http://www.packtpub.com/networking-and-servers/boostasio-c-network-programming-second-edition)查阅。转到**代码文件**部分。\n\n# 开发客户端和服务器程序\n\n我们已经有了网络包装代码，通过使用“T0”库来简化开发网络应用的编程过程。现在，让我们使用包装代码创建一个客户端和服务器程序。\n\n## 创建一个简单的回送服务器\n\n我们将去创建一个服务器程序，它将回显从客户端检索的所有流量。在这种情况下，我们将使用`telnet`作为客户端，就像我们之前所做的那样。文件必须保存为`echoserver.cpp`，内容如下:\n\n```cpp\n/* echoserver.cpp */\n#include \"wrapper.h\"\n#include <conio.h>\n#include <boost/thread/mutex.hpp>\n\nboost::mutex global_stream_lock;\n\nclass MyConnection : public Connection {\nprivate:\n  void OnAccept(const std::string &host, uint16_t port) {\n    global_stream_lock.lock();\n    std::cout << \"[OnAccept] \" << host << \":\" << port << \"\\n\";\n    global_stream_lock.unlock();\n\n    Recv();\n  }\n\n  void OnConnect(const std::string & host, uint16_t port) {\n    global_stream_lock.lock();\n    std::cout << \"[OnConnect] \" << host << \":\" << port << \"\\n\";\n    global_stream_lock.unlock();\n\n    Recv();\n  }\n\n  void OnSend(const std::vector<uint8_t> & buffer) {\n    global_stream_lock.lock();\n    std::cout << \"[OnSend] \" << buffer.size() << \" bytes\\n\";\n    for(size_t x=0; x<buffer.size(); x++) {\n\n      std::cout << (char)buffer[x];\n      if((x + 1) % 16 == 0)\n        std::cout << std::endl;\n    }\n    std::cout << std::endl;\n    global_stream_lock.unlock();\n  }\n\n  void OnRecv(std::vector<uint8_t> &buffer) {\n    global_stream_lock.lock();\n    std::cout << \"[OnRecv] \" << buffer.size() << \" bytes\\n\";\n    for(size_t x=0; x<buffer.size(); x++) {\n\n      std::cout << (char)buffer[x];\n      if((x + 1) % 16 == 0)\n        std::cout << std::endl;\n    }\n    std::cout << std::endl;\n    global_stream_lock.unlock();\n\n    // Start the next receive\n    Recv();\n\n    // Echo the data back\n    Send(buffer);\n  }\n\n  void OnTimer(const boost::posix_time::time_duration &delta) {\n    global_stream_lock.lock();\n    std::cout << \"[OnTimer] \" << delta << \"\\n\";\n    global_stream_lock.unlock();\n  }\n\n  void OnError(const boost::system::error_code &error) {\n    global_stream_lock.lock();\n    std::cout << \"[OnError] \" << error << \"\\n\";\n    global_stream_lock.unlock();\n  }\n\npublic:\n  MyConnection(boost::shared_ptr<Hive> hive)\n    : Connection(hive) {\n  }\n\n  ~MyConnection() {\n  }\n};\n\nclass MyAcceptor : public Acceptor {\nprivate:\n  bool OnAccept(boost::shared_ptr<Connection> connection, const std::string &host, uint16_t port) {\n    global_stream_lock.lock();\n    std::cout << \"[OnAccept] \" << host << \":\" << port << \"\\n\";\n    global_stream_lock.unlock();\n\n    return true;\n  }\n\n  void OnTimer(const boost::posix_time::time_duration &delta) {\n    global_stream_lock.lock();\n    std::cout << \"[OnTimer] \" << delta << \"\\n\";\n    global_stream_lock.unlock();\n  }\n\n  void OnError(const boost::system::error_code &error) {\n    global_stream_lock.lock();\n    std::cout << \"[OnError] \" << error << \"\\n\";\n    global_stream_lock.unlock();\n  }\n\npublic:\n  MyAcceptor(boost::shared_ptr<Hive> hive)\n    : Acceptor(hive) {\n  }\n\n  ~MyAcceptor() {\n  }\n};\n\nint main(void) {\n  boost::shared_ptr<Hive> hive(new Hive());\n\n  boost::shared_ptr<MyAcceptor> acceptor(new MyAcceptor(hive));\n  acceptor->Listen(\"127.0.0.1\", 4444);\n\n  boost::shared_ptr<MyConnection> connection(new MyConnection(hive));\n  acceptor->Accept(connection);\n\n  while(!_kbhit()) {\n    hive->Poll();\n    Sleep(1);\n  }\n\n  hive->Stop();\n\n  return 0;\n}\n```\n\n然后，使用以下命令编译前面的代码。在这里，我们可以看到我们在编译过程中包含`wrapper.cpp`来利用我们的包装代码:\n\n```cpp\ng++ -Wall -ansi -I ../boost_1_58_0 wrapper.cpp echoserver.cpp -o echoserver -L ../boost_1_58_0/stage/lib -l boost_system-mgw49-mt-1_58 -l ws2_32 -l libboost_thread-mgw49-mt-1_58 -l mswsock\n\n```\n\n我们可以通过在控制台窗口中键入`echoserver`来尝试前面的程序；之后，我们应该得到如下输出:\n\n![Creating a simple echo server](img/00045.jpeg)\n\n我们第一次运行程序，它会在`localhost`中监听端口`4444`。我们可以在`main`块中看到，如果没有键盘敲击，程序会调用`Hive`类中的`poll()`函数。这意味着如果按下任何键，程序将关闭，因为它将调用`Hive`类中的`Stop()`功能，这将停止`io_service`对象。每隔 1000 毫秒，计时器就会滴答作响，因为`Acceptor`类的构造函数会启动计时器间隔 1000 毫秒。\n\n现在，打开另一个控制台窗口，输入命令`telnet 127.0.0.1 4444`使`telnet`成为我们的客户端。`echoserver`接受连接后，每次按下键盘上的字母数字选项，`echoserver`都会将字符发送回`telnet`。下图描述了`echoserver`和`telnet`服务器之间的验收连接:\n\n![Creating a simple echo server](img/00046.jpeg)\n\n当服务器接受来自客户端的连接时，`OnAccept()`函数处理程序将立即被调用。我在`telnet`窗口中分别按下了 *A* 、 *B* 、 *C* 键，然后`echoserver`收到了字符并发回客户端。`telnet`窗口还显示`A`、`B`和`C`。\n\n## 创建一个简单的客户端程序\n\n我们已经成功地创建了一个服务器端程序。现在，我们将继续开发客户端程序。它将通过`HTTP GET`命令接收 Packt Publishing 网站的内容，代码如下:\n\n```cpp\n/* clienthttpget.cpp */\n#include \"wrapper.h\"\n#include <conio.h>\n#include <boost/thread/mutex.hpp>\n\nboost::mutex global_stream_lock;\n\nclass MyConnection : public Connection {\nprivate:\n  void OnAccept(const std::string &host, uint16_t port) {\n    global_stream_lock.lock();\n    std::cout << \"[OnAccept] \" << host << \":\" << port << \"\\n\";\n    global_stream_lock.unlock();\n\n    // Start the next receive\n    Recv();\n  }\n\n  void OnConnect(const std::string &host, uint16_t port) {\n    global_stream_lock.lock();\n    std::cout << \"[OnConnect] \" << host << \":\" << port << \"\\n\";\n    global_stream_lock.unlock();\n\n    // Start the next receive\n    Recv();\n\n    std::string str = \"GET / HTTP/1.0\\r\\n\\r\\n\";\n\n    std::vector<uint8_t> request;\n    std::copy(str.begin(), str.end(), std::back_inserter(request));\n    Send(request);\n  }\n\n  void OnSend(const std::vector<uint8_t> &buffer) {\n    global_stream_lock.lock();\n    std::cout << \"[OnSend] \" << buffer.size() << \" bytes\\n\";\n    for(size_t x=0; x<buffer.size(); x++) {\n\n      std::cout << (char)buffer[x];\n      if((x + 1) % 16 == 0)\n        std::cout << \"\\n\";\n    }\n    std::cout << \"\\n\";\n    global_stream_lock.unlock();\n  }\n\n  void OnRecv(std::vector<uint8_t> &buffer) {\n    global_stream_lock.lock();\n    std::cout << \"[OnRecv] \" << buffer.size() << \" bytes\\n\";\n    for(size_t x=0; x<buffer.size(); x++) {\n\n      std::cout << (char)buffer[x];\n      if((x + 1) % 16 == 0)\n        std::cout << \"\\n\";\n    }\n    std::cout << \"\\n\";\n    global_stream_lock.unlock();\n\n    // Start the next receive\n    Recv();\n  }\n\n  void OnTimer(const boost::posix_time::time_duration &delta) {\n    global_stream_lock.lock();\n    std::cout << \"[OnTimer] \" << delta << std::endl;\n    global_stream_lock.unlock();\n  }\n\n  void OnError(const boost::system::error_code &error) {\n    global_stream_lock.lock();\n    std::cout << \"[OnError] \" << error << \"\\n\";\n    global_stream_lock.unlock();\n  }\n\npublic:\n  MyConnection(boost::shared_ptr<Hive> hive)\n    : Connection(hive) {\n  }\n\n  ~MyConnection() {\n  }\n};\n\nint main(void) {\n  boost::shared_ptr<Hive> hive(new Hive());\n\n  boost::shared_ptr<MyConnection> connection(new MyConnection(hive));\n  connection->Connect(\"www.packtpub.com\", 80);\n\n  while(!_kbhit()) {\n    hive->Poll();\n    Sleep(1);\n  }\n\n  hive->Stop();\n\n  return 0;\n}\n```\n\n将前面的代码保存为`clienthttpget.cpp`，并使用以下命令编译代码:\n\n```cpp\ng++ -Wall -ansi -I ../boost_1_58_0 wrapper.cpp clienthttpget.cpp -o clienthttpget -L ../boost_1_58_0/stage/lib -l boost_system-mgw49-mt-1_58 -l ws2_32 –l libboost_thread-mgw49-mt-1_58 -l mswsock\n\n```\n\n当我们运行程序时，将显示以下输出:\n\n![Creating a simple client program](img/00047.jpeg)\n\n就在连接建立之后，程序使用以下代码片段向[www.packtpub.com](http://www.packtpub.com)的端口`80` 发送`HTTP GET`命令:\n\n```cpp\nstd::string str = \"GET / HTTP/1.0\\r\\n\\r\\n\";\nstd::vector<uint8_t> request;\nstd::copy(str.begin(), str.end(), std::back_inserter(request));\nSend(request)\n\n```\n\n然后，它使用`wrapper.cpp`文件代码内的`Connection`类中的`Send()`函数向套接字发送请求。`Send()`功能的代码片段如下:\n\n```cpp\nm_io_strand.post(boost::bind(&Connection::DispatchSend, shared_from_this(), buffer));\n\n```\n\n如我们所见，我们使用`strand`对象来允许所有事件串行运行。此外，由于有了`strand`对象，我们不必在每次事件发生时都使用`lock`对象。\n\n发送请求后，程序将使用以下代码片段汇集传入的数据:\n\n```cpp\nm_io_service.poll();\n\n```\n\n然后，一旦数据到来，它将由`OnRecv()`功能处理器显示在控制台中，如我们在前面的图像中所见。\n\n# 总结\n\n开发网络应用有三个基本步骤。第一步包括在源和目标之间建立连接，这意味着客户端和服务器。我们可以配置`socket`对象和`acceptor`对象来建立连接。\n\n其次，我们通过读写套接字来交换数据。为此，我们可以使用`basic_stream_socket`函数集合。在前面的例子中，我们使用`boost::asio::async_write()`方法发送数据，使用`boost::asio::async_read()`方法接收数据。最后，最后一步是释放连接。通过使用`ip::tcp::socket`对象中的`shutdown()`方法，我们可以禁用套接字上的数据发送和接收。然后，在`shutdown()`功能后调用`close()`方法将关闭插座并释放处理器。我们也已经为所有函数创建了一个包装器，通过访问`Boost.Asio`库，它在网络应用编程中最常用。这意味着我们可以简单有效地开发网络应用，因为我们不需要一遍又一遍地重用代码。"
  },
  {
    "path": "docs/boost-asio-cpp-net-prog-2e/7.md",
    "content": "# 七、调试代码并解决错误\n\n在前一章中，我们成功地开发了一个服务器-客户端程序。我们还顺利地运行了我们创建的程序。然而，有时我们在运行应用时会面临一些问题，比如收到意外结果或应用在运行时崩溃。在这种情况下，调试工具有能力帮助我们解决这些问题。在本章讨论调试工具时，我们将讨论以下主题:\n\n*   选择我们使用的调试工具，并保持它的简单和轻量级\n*   设置调试工具，准备待调试的可执行文件\n*   熟悉调试工具中使用的命令\n\n# 选择调试工具\n\n身边很多调试工具都自带编程语言的**集成开发环境** ( **IDE** )。比如 **Visual Studio** 有一个调试工具针对 C、C++、C#、Visual Basic。或者，您可能听说过代码块和流血开发-C++，它们也有自己的调试工具。但是，如果你还记得我们在[第 1 章](1.html#page \"Chapter 1. Simplifying Your Network Programming in C++\")、*中讨论的用 C++ 简化你的网络编程*的话，我们决定不使用 IDE，因为它的重载不会给我们的计算机加载太多资源。我们需要一个轻量级的工具来开发我们的网络应用。\n\n我们选择的工具是 **GNU 调试器** ( **GDB** )。GDB 是一个基于命令行工具的强大调试工具；这意味着我们不需要复杂的**图形用户界面** ( **GUI** )。换句话说，我们只需要一个键盘，甚至不需要鼠标，所以系统也变得轻量级了。\n\nGDB 可以做四件主要的事情来帮助我们解决代码问题，如下所示:\n\n*   **逐行运行我们的代码**:当 GDB 运行我们的程序时，我们可以看到此刻正在执行哪一行\n*   **在特定的一行停止我们的代码**:当我们怀疑某一行导致了错误时，这很有用\n*   **检查可疑线**:当我们成功停在可疑线时，我们可以继续检查，例如，通过检查涉及的变量的值\n*   **改变变量的值**:如果我们发现了导致错误的意外变量值，我们可以用我们的期望值替换 GDB 运行时的值，以确保值的改变会解决问题\n\n## 安装调试工具\n\n幸运的是，如果您遵循了第 1 章、*中与安装 MinGW-w64 相关的所有步骤，您就不需要安装其他任何东西了，因为安装包中还包含了 GDB 工具。我们现在需要做的是在命令控制台中运行 GDB 工具，检查它是否正常运行。*\n\n在我们命令提示符的任何活动目录中，键入以下命令:\n\n```cpp\ngdb\n\n```\n\n我们应该在控制台窗口中获得以下输出:\n\n```cpp\nC:\\CPP>gdb\nGNU gdb (GDB) 7.8.1\nCopyright (C) 2014 Free Software Foundation, Inc.\nLicense GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>\nThis is free software: you are free to change and redistribute it.\nThere is NO WARRANTY, to the extent permitted by law.  Type \"show copying\"\nand \"show warranty\" for details.\nThis GDB was configured as \"x86_64-w64-mingw32\".\nType \"show configuration\" for configuration details.\nFor bug reporting instructions, please see:\n<http://www.gnu.org/software/gdb/bugs/>.\nFind the GDB manual and other documentation resources online at:\n<http://www.gnu.org/software/gdb/documentation/>.\nFor help, type \"help\".\nType \"apropos word\" to search for commands related to \"word\".\n(gdb)_\n\n```\n\n正如我们在控制台上看到的前面的输出，我们有 7.8.1 版本(这不是最新版本，因为我们刚刚从 MinGW-w64 安装程序包中获得它)。最后一行还有`(gdb)`，旁边有一个闪烁的光标；这意味着 GDB 已经准备好接受命令。但是，目前我们需要知道的命令是`quit`(或者，我们可以用`q`作为捷径)退出 GDB。只需键入`q`并按*进入*，您将返回命令提示符。\n\n## 准备调试文件\n\nGDB 至少需要一个可执行文件进行调试。出于这个目的，我们将回到上一章，从那里借用源代码。还记得我们在[第一章](1.html#page \"Chapter 1. Simplifying Your Network Programming in C++\")、*用 C++ 简化你的网络编程*中创建的一个游戏吗，在这个游戏中，我们要猜测电脑想到的随机数？如果你记得的话，我们有源代码，我们在第一章中保存为`rangen.cpp`，我们通过添加`Boost`库进行了修改，在[第三章](3.html#page \"Chapter 3. Introducing the Boost C++ Libraries\")*中保存为`rangen_boost.cpp`，介绍了 Boost C++ 库*。在下一节中，我们将使用`rangen_boost.cpp`源代码来演示 GDB 的使用。还有，对于那些已经忘记了源代码的人，我在这里为大家重写了一下:\n\n```cpp\n/* rangen_boost.cpp */\n#include <boost/random/mersenne_twister.hpp>\n#include <boost/random/uniform_int_distribution.hpp>\n#include <iostream>\n\nint main(void) {\n  int guessNumber;\n  std::cout << \"Select number among 0 to 10: \";\n  std::cin >> guessNumber;\n  if(guessNumber < 0 || guessNumber > 10) {\n    return 1;\n  }\n  boost::random::mt19937 rng;\n  boost::random::uniform_int_distribution<> ten(0,10);\n  int randomNumber = ten(rng);\n\n  if(guessNumber == randomNumber) {\n    std::cout << \"Congratulation, \" << guessNumber << \" is your lucky number.\\n\";\n  }\n  else {\n    std::cout << \"Sorry, I'm thinking about number \" << randomNumber << \"\\n\"; \n  }\n  return 0;\n}\n```\n\n我们将修改编译命令，以便在 GDB 使用。我们将使用`-g`选项，这样创建的可执行文件将包含 GDB 将读取的调试信息和符号。我们将使用以下命令从包含调试信息和符号的`rangen_boost.cpp`文件生成`rangen_boost_gdb.exe`可执行文件:\n\n```cpp\ng++ -Wall -ansi -I ../boost_1_58_0 rangen_boost.cpp -o rangen_boost_gdb -g\n\n```\n\n正如我们在前面的命令中看到的，我们在编译命令中添加了`-g`选项，以便在可执行文件中记录调试信息和符号。现在，我们的活动目录中应该有名为`rangen_boost_gdb.exe`的文件。在下一节中，我们将使用 GDB 调试它。\n\n### 类型\n\n我们只能调试使用`-g`选项编译的可执行文件。换句话说，如果没有调试信息和符号，我们将无法调试可执行文件。此外，我们无法调试源代码文件(`*.cpp`文件)或头文件(`*.h`文件)。\n\n# 在 GDB 的领导下运行程序\n\n准备好包含调试信息和符号的可执行文件后，让我们运行 GDB 从文件中读取所有符号并进行调试。运行以下命令启动调试过程:\n\n```cpp\ngdb rangen_boost_gdb\n\n```\n\n我们的输出如下:\n\n```cpp\nC:\\CPP>gdb rangen_boost_gdb\nGNU gdb (GDB) 7.8.1\nCopyright (C) 2014 Free Software Foundation, Inc.\nLicense GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>\nThis is free software: you are free to change and redistribute it.\nThere is NO WARRANTY, to the extent permitted by law.  Type \"show copying\"\nand \"show warranty\" for details.\nThis GDB was configured as \"x86_64-w64-mingw32\".\nType \"show configuration\" for configuration details.\nFor bug reporting instructions, please see:\n<http://www.gnu.org/software/gdb/bugs/>.\nFind the GDB manual and other documentation resources online at:\n<http://www.gnu.org/software/gdb/documentation/>.\nFor help, type \"help\".\nType \"apropos word\" to search for commands related to \"word\"...\nReading symbols from rangen_boost_gdb...done.\n(gdb)_\n\n```\n\n除了`(gdb)`之前的最后一行，我们得到了与之前的 GDB 输出相同的输出。这一行告诉我们，GDB 已经成功读取了所有调试符号，并准备启动调试过程。在这一步中，如果我们的程序需要，我们还可以指定参数。由于我们的程序不需要指定任何参数，我们现在可以忽略它。\n\n## 开始调试过程\n\n要启动调试过程，我们可以调用`run`或`start`命令。前者将在 GDB 下启动我们的程序，而后者将表现类似，但将逐行执行代码。不同的是，如果我们还没有设置断点，如果我们调用`run`命令，程序将照常运行，而如果我们用`start`命令启动，调试器将自动在主代码块中设置断点，如果断点到达该点，程序将停止。\n\n现在，让我们使用`start`命令进行调试。只需在 GDB 提示符下键入`start`，控制台将追加以下输出:\n\n```cpp\n(gdb) start\nTemporary breakpoint 1 at 0x401506: file rangen_boost.cpp, line 10.\nStarting program: C:\\CPP\\rangen_boost_gdb.exe\n[New Thread 10856.0x213c]\n\nTemporary breakpoint 1, main () at rangen_boost.cpp:10\n10              std::cout << \"Select number among 0 to 10: \";\n\n```\n\n调试过程开始。从输出中，我们可以发现在第 10 行的`main`块内自动创建了一个断点。当没有断点时，调试器将选择主块中的第一条语句。这就是为什么我们把`line 10`作为我们的自动断点。\n\n## 继续和步进调试过程\n\n在我们在 GDB 的带领下成功启动我们的计划之后，下一步就是继续和迈步。我们可以使用以下命令之一来继续并逐步执行调试过程:\n\n*   `continue`:这个命令将恢复程序的执行，直到我们的程序正常完成。如果找到断点，执行将在设置断点的行停止。\n*   `step`:这个命令将执行我们程序的最后一步。*步骤*可能意味着一行源代码或一条机器指令。如果它找到了函数的调用，它将进入函数并在函数内部运行一个步骤。\n*   `next`:这个命令的行为类似于`step`命令，但是它只延续到当前堆栈帧的下一行。换句话说，如果`next`命令发现一个函数的调用，它将不会进入该函数。\n\n现在，让我们使用`next`命令。在我们调用`start`命令后，在 GDB 提示符下键入`next`命令。我们应该得到以下输出:\n\n```cpp\n(gdb) next\nSelect number among 0 to 10: 11         std::cin >> guessNumber;\n\n```\n\nGDB 执行第 10 行，然后继续执行第 11 行。我们将再次调用`next`命令继续调试过程。然而，如果我们只是按下*进入*键，GDB 将执行我们之前的命令。这就是为什么我们现在只需要按下*进入*键，它会给我们一个闪烁的光标。现在，我们必须输入我们猜测要存储在`guessNumber`变量中的数字。我会输入号码`4`，但是你可以输入你喜欢的号码。再次按下*进入*键，根据需要继续调试多次，正常退出程序。将附加以下输出:\n\n```cpp\n(gdb)\n4\n12              if(guessNumber < 0 || guessNumber > 10)\n(gdb)\n17              boost::random::mt19937 rng;\n(gdb)\n19              boost::random::uniform_int_distribution<> ten(0,10);\n(gdb)\n20              int randomNumber = ten(rng);\n(gdb)\n22              if(guessNumber == randomNumber)\n(gdb)\n28                      std::cout << \"Sorry, I'm thinking about number \" << randomNumber << \"\\n\";\n(gdb)\nSorry, I'm thinking about number 8\n30              return 0;\n(gdb)\n31      }(gdb)\n0x00000000004013b5 in __tmainCRTStartup ()\n(gdb)\nSingle stepping until exit from function __tmainCRTStartup, which has no line number information.\n[Inferior 1 (process 11804) exited normally]\n\n```\n\n正如我们在前面的输出中所看到的，在我们输入猜测的号码后，程序执行的`if`语句，以确保我们输入的号码没有超出范围。如果我们的猜测数字有效，程序会继续生成一个随机数。然后将我们的猜测数字与程序生成的随机数进行比较。无论两个数字是否相同，程序都会给出不同的输出。不幸的是，我的猜测数字不同于随机数。如果您能够正确猜测数字，您可能会获得不同的输出。\n\n## 打印源代码\n\n有时，我们可能想在运行调试过程时检查我们的源文件。由于调试信息和符号都记录在我们的程序中，GDB 可以打印源代码，即使它是一个可执行文件。要打印源代码，我们可以在 GDB 提示符下键入`list`(或快捷方式为`l`命令)。默认情况下，每次调用命令时，GDB 都会打印十行。但是，我们可以使用`set listsize`命令更改该设置。另外，要知道`list`命令将显示的行数，我们可以调用`show listsize`命令。让我们看看下面的命令行输出:\n\n```cpp\n(gdb) show listsize\nNumber of source lines gdb will list by default is 10.\n(gdb) set listsize 20\n(gdb) show listsize\nNumber of source lines gdb will list by default is 20.\n(gdb)_\n\n```\n\n我们使用`list`命令增加要显示的行数。现在，每次调用`list`命令，输出都会显示二十行源代码。\n\n以下是最常见的几种`list`命令的形式:\n\n*   `list`:这个命令将显示列表大小定义的所有行的源代码。如果我们再次调用它，它将显示列表大小定义的剩余行数。\n*   `list [linenumber]`:该命令将显示以`linenumber`为中心的线条。命令`list 10`将显示第 5 行到第 14 行，因为第 10 行在中间。\n*   `list [functionname]`:该命令将显示以`functionname`变量开头为中心的行。命令`list main`将在列表中心显示`int main(void)`功能。\n*   `list [first,last]`:该命令将显示从第一行到最后一行。命令`list 15,16`将只显示第 15 行和第 16 行。\n*   `list [,last]`:该命令将显示以`last`结尾的行。命令`list ,5`将显示第 1 行到第 5 行。\n*   `list [first,]`:此命令将显示以指定行为第一行开始的所有行。如果行数超过指定的行数，命令`list 5,`将显示第 5 行到其余行。否则，它将显示与列表大小设置一样多的行。\n*   `list +`:该命令将显示最后显示的行之后的所有行。\n*   `list -`:该命令将显示最后显示的行之前的所有行。\n\n## 设置和删除断点\n\n如果我们怀疑某一行出错，我们可以在该行设置一个断点，以便调试器在该行停止调试过程。要设置断点，我们可以调用`break [linenumber]`命令。考虑我们想在第 20 行停止，它包含以下代码:\n\n```cpp\nint randomNumber = ten(rng);\n\n```\n\n在这里，我们必须在 GDB 下加载我们的程序之后调用`break 20`命令，以便在第 20 行设置断点。以下输出控制台说明了这一点:\n\n```cpp\n(gdb) break 20\nBreakpoint 1 at 0x401574: file rangen_boost.cpp, line 20.\n(gdb) run\nStarting program: C:\\CPP\\rangen_boost_gdb.exe\n[New Thread 1428.0x13f4]\nSelect number among 0 to 10: 2\n\nBreakpoint 1, main () at rangen_boost.cpp:20\n20              int randomNumber = ten(rng);\n(gdb) next\n22              if(guessNumber == randomNumber)\n(gdb)\n28                      std::cout << \"Sorry, I'm thinking about number \" << randomNumber << \"\\n\";\n(gdb)\nSorry, I'm thinking about number 8\n30              return 0;\n(gdb)\n31      }(gdb)\n0x00000000004013b5 in __tmainCRTStartup ()\n(gdb)\nSingle stepping until exit from function __tmainCRTStartup,\nwhich has no line number information.\n[Inferior 1 (process 1428) exited normally]\n(gdb)_\n\n```\n\n在前面的输出控制台中，就在我们的程序加载到 GDB 下之后，我们调用`break 20`命令。调试器然后在第 20 行设置一个新的断点。我们没有像以前那样调用`start`命令，而是调用`run`命令来执行程序，并让它在找到断点时停止。例如，在我们输入猜测数字`2`后，调试器停在第 20 行，也就是我们期望它停的那一行。然后，我们调用`next`命令继续调试器，并多次按下*进入*键，直到程序退出。\n\n如果我们想删除一个断点，只需使用`delete N`命令，其中`N`是设置所有断点的顺序。如果我们没有记住我们设置的断点的所有位置，我们可以调用`info break`命令来获得所有断点的列表。我们也可以使用`delete`命令(没有`N`，将删除所有断点)。\n\n## 打印变量值\n\n我们已经能够停在我们想要的线上了。我们还可以发现我们在程序中使用的变量的值。我们可以调用`print [variablename]`命令打印任意变量的值。使用上一个断点，我们将打印变量`randomNumber`的值。就在调试器遇到第 20 行的断点后，我们将调用 print `randomNumber`命令。然后，我们调用`next`命令，再次打印`randomNumber`变量。请看下面的命令调用示例:\n\n```cpp\n(gdb) break 20\nBreakpoint 1 at 0x401574: file rangen_boost.cpp, line 20.\n(gdb) run\nStarting program: C:\\CPP\\rangen_boost_gdb.exe\n[New Thread 5436.0x1b04]\nSelect number among 0 to 10: 3\n\nBreakpoint 1, main () at rangen_boost.cpp:20\n20              int randomNumber = ten(rng);\n(gdb) print randomNumber\n$1 = 0\n(gdb) next\n22              if(guessNumber == randomNumber)\n(gdb) print randomNumber\n$2 = 8\n(gdb)_\n\n```\n\n正如我们在前面的输出中看到的，下面一行是设置断点的地方:\n\n```cpp\nint randomNumber = ten(rng);\n\n```\n\n在该行被执行之前，我们查看`randomNumber`变量的值。变量的值为`0`。然后，我们调用`next`命令指示调试器执行该行。之后，我们再次窥视变量的值，这次是`8`。当然，在这个实验中，你可能会得到不同的值，而不是 8。\n\n## 修改变量值\n\n我们将通过修改其中一个变量的值来欺骗我们的程序。可以使用`set var [variablename]=[newvalue]`命令重新分配变量值。为了保证我们要修改的变量的类型，我们可以调用`whatis [variablename]`命令来获取所需的变量类型。\n\n现在，让我们在程序给变量赋值一个随机数后，改变`randomNumber`变量的值。我们将重新启动调试过程，删除所有已经设置的断点，在第 22 行设置一个新的断点，并通过键入`continue`命令继续调试过程，直到调试器命中第 22 行的断点。在这种情况下，我们可以重新分配`randomNumber`变量的值，使其与`guessNumber`变量的值完全相同。现在，再次调用`continue`命令。之后，我们会因为猜中了正确的数字而受到祝贺。\n\n有关更多详细信息，让我们看看下面的输出控制台，它将说明前面的步骤:\n\n```cpp\n(gdb) start\nThe program being debugged has been started already.\nStart it from the beginning? (y or n) y\n\nTemporary breakpoint 2 at 0x401506: file rangen_boost.cpp, line 10.\nStarting program: C:\\CPP\\rangen_boost_gdb.exe\n[New Thread 6392.0x1030]\n\nTemporary breakpoint 2, main () at rangen_boost.cpp:10\n10              std::cout << \"Select number among 0 to 10: \";\n(gdb) info break\nNum     Type           Disp Enb Address            What\n1       breakpoint     keep y   0x0000000000401574 in main()\n at rangen_boost.cpp:20\n(gdb) delete 1\n(gdb) info break\nNo breakpoints or watchpoints.\n(gdb) break 22\nBreakpoint 3 at 0x40158d: file rangen_boost.cpp, line 22.\n(gdb) continue\nContinuing.\nSelect number among 0 to 10: 5\n\nBreakpoint 3, main () at rangen_boost.cpp:22\n22              if(guessNumber == randomNumber)\n(gdb) whatis randomNumber\ntype = int\n(gdb) print randomNumber\n$3 = 8\n(gdb) set var randomNumber=5\n(gdb) print randomNumber\n$4 = 5\n(gdb) continue\nContinuing.\nCongratulation, 5 is your lucky number.\n[Inferior 1 (process 6392) exited normally]\n(gdb)_\n\n```\n\n正如我们在前面的输出中看到的，当我们调用`start`命令时，调试器要求我们停止前面的调试过程，因为它仍然在运行。只需输入 *Y* 键，按*回车*键即可回答查询。我们可以使用`info break`命令列出所有可用的断点，然后根据我们从`info break`命令获得的顺序删除所需的断点。我们调用`continue`命令恢复调试过程，当调试器到达断点时，我们用`guessNumber`变量的值重新分配`randomNumber`变量。我们继续调试过程，并在运行时成功修改`randomNumber`变量的值，因为我们受到了程序的祝贺。\n\n如果程序中有多个变量，不用逐个打印所有变量，可以使用`info locals`命令打印所有变量的值。\n\n## 调用命令提示符\n\n我偶尔会在 GDB 提示符里面调用 Windows shell 命令，比如`cls`命令到清除屏幕，`dir`命令列出活动目录的内容，甚至还有编译命令。如果还想执行 Windows shell 命令，可以使用的 GDB 命令是`shell [Windows shell command]`。它实际上只是在 Windows shell 命令之前添加`shell`命令，并在需要时添加参数。让我们看看下面的控制台输出，以了解在 GDB 提示符下执行 Windows shell 命令。让我们看看下面的输出:\n\n```cpp\nC:\\CPP>gdb\nGNU gdb (GDB) 7.8.1\nCopyright (C) 2014 Free Software Foundation, Inc.\nLicense GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>\nThis is free software: you are free to change and redistribute it.\nThere is NO WARRANTY, to the extent permitted by law.  Type \"show copying\"\nand \"show warranty\" for details.\nThis GDB was configured as \"x86_64-w64-mingw32\".\nType \"show configuration\" for configuration details.\nFor bug reporting instructions, please see:\n<http://www.gnu.org/software/gdb/bugs/>.\nFind the GDB manual and other documentation resources online at:\n<http://www.gnu.org/software/gdb/documentation/>.\nFor help, type \"help\".\nType \"apropos word\" to search for commands related to \"word\".\n(gdb) shell dir rangen_boost* /w\n Volume in drive C is SYSTEM\n Volume Serial Number is 8EA6-1DBE\n\n Directory of C:\\CPP\n\nrangen_boost.cpp       rangen_boost.exe       rangen_boost_gdb.exe\n 3 File(s)        190,379 bytes\n 0 Dir(s)  141,683,314,688 bytes free\n(gdb) shell g++ -Wall -ansi -I ../boost_1_58_0 rangen_boost.cpp -o rangen_boost_gdb_2 -g\n(gdb) shell dir rangen_boost* /w\n Volume in drive C is SYSTEM\n Volume Serial Number is 8EA6-1DBE\n\n Directory of C:\\CPP\n\nrangen_boost.cpp         rangen_boost.exe         rangen_boost_gdb.exe\nrangen_boost_gdb_2.exe\n 4 File(s)        259,866 bytes\n 0 Dir(s)  141,683,249,152 bytes free\n\n```\n\n在前面的控制台输出中，我们调用`dir`命令列出活动目录中所有以`rangen_boost`开头的文件。然后，我们调用编译命令在活动目录中生成`rangen_boost_gdb_2.exe`可执行文件。然后，我们再次调用`dir`命令，以确保`rangen_boost_gdb_2.exe`可执行文件已成功创建。\n\n### 类型\n\n您可以使用`apropos shell`命令获取更多关于 shell 命令的信息。\n\n# 解决错误\n\n在[第五章](5.html#page \"Chapter 5. Delving into the Boost.Asio Library\")*中，深入研究助推.Asio 库*，我们讨论了异常和错误的处理。如果我们遵循本书中所有的源代码，我们可能永远不会得到任何错误代码来迷惑我们。然而，如果我们试图修改源代码，即使只是一点点，可能会抛出一个错误代码，程序不会给我们任何描述。由于`Boost`库抛出的错误代码是基于 Windows 系统错误代码，超出了本书的范围，我们可以在[msdn . Microsoft . com/en-us/library/Windows/desktop/ms 681381% 28v = vs . 85% 29 . aspx](http://msdn.microsoft.com/en-us/library/windows/desktop/ms681381%28v=vs.85%29.aspx)的**微软开发者网** ( **MSDN** )网站上找到的描述。在这里，我们可以找到从错误 0 到 15999 的所有错误代码的翻译。使用 GDB 和来自 MSDN 的错误代码翻译将成为解决我们程序中出现的错误的有力工具。\n\n让我们回到[第 6 章](6.html#page \"Chapter 6. Creating a Client-server Application\")、*创建客户端-服务器应用*并运行`serverasync`程序。当程序运行时，它在端口`4444`上监听`127.0.0.1`中的客户端，在我们的示例中，这将通过 telnet 来模拟。但是，如果客户端没有响应，会发生什么？为了进一步了解，让我们不运行 telnet 运行`serverasync`程序。由于客户端没有响应，将显示以下错误:\n\n![Solving the error](img/00048.jpeg)\n\n我们得到了系统错误代码`995`。现在，有了这个错误代码，我们可以访问 MSDN 系统错误代码并找到错误描述，这就是**由于线程退出或应用请求，输入/输出操作已经中止。(错误 _ 操作 _ 中止)**。\n\n# 接下来是什么？\n\n我们熟悉基本的 GDB 命令。在 GDB 还有很多命令我们不能在这本书里讨论。GDB 有一个官方网站，我们可以在 www.gnu.org/software/gdb/documentation/访问。在这里，我们可以找到所有尚未讨论过的完整命令。\n\n### 注\n\n我们还可以在官方网站[【www.boost.org】](http://www.boost.org)上获得关于 Boost C++ 库的更多详细信息，尤其是`Boost.Asio`库文档，可在[www.boost.org/doc/libs/1_58_0/doc/html/boost_asio.html](http://www.boost.org/doc/libs/1_58_0/doc/html/boost_asio.html)上获得。\n\n# 总结\n\n调试过程是我们可以通过一步一步运行程序来分析程序的一个基本过程。当我们的程序产生意想不到的结果或在执行过程中崩溃时，除了运行调试过程，没有其他选择。GDB 是我们的选择，因为它与 C++ 语言兼容，因为它带有 MinGW-w64 安装软件包，并且在加载时很轻。\n\nGDB 只能运行使用`-g`选项编译的可执行文件。此选项将添加调试信息和符号，这在调试过程中很重要。如果没有`-g`选项，您将无法调试编译的可执行文件。\n\n在 GDB 下成功加载程序后，我们可以选择`run`或`start`命令来执行调试过程。`run`命令将照常执行我们的程序，但是如果调试器发现断点，它将停止，而`start`命令将在第一次执行时在程序的`main`块停止。\n\n当调试器在某一行停止时，我们必须决定是否继续调试过程。我们可以选择运行程序，直到它退出或者使用`continue`命令找到断点。或者，我们可以使用`next`命令逐步运行调试器。\n\n要使调试器在调试过程执行时停止，请调用`break [linenumber]`命令来设置断点。如果我们想确保设置正确的行号，可以调用`list`命令打印源代码。调用`delete N`命令将删除`N`可以找到`info break`命令的断点。\n\n当发现错误时，检索变量值也很重要。如果程序产生意外的输出，我们可以通过打印变量来跟踪它的值。我们可以通过使用`print [variablename]`命令来做到这一点。对于我们怀疑会导致错误的变量，我们可以使用`set var [variablename]=[newvalue]`命令为该变量重新分配一个新值。然后，我们可以再次运行调试器，直到获得预期的输出。当我们修复了所有的错误，并且确信一切都是完美的，我们可以通过使用`shell [Windows shell command]`命令在 GDB 提示符下调用编译命令来重新编译我们的程序。"
  },
  {
    "path": "docs/boost-asio-cpp-net-prog-2e/README.md",
    "content": "# Boost.Asio C++ 网络编程入门中文第二版\n\n> 原书：[Boost.Asio C++ Network Programming - Second Edition](https://libgen.rs/book/index.php?md5=5C82493B57CFF907E3BF6B58A1BE124D)\n> \n> 协议：[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)\n> \n> 阶段：机翻（1）\n> \n> 自豪地采用[谷歌翻译](https://translate.google.cn/)\n> \n> 让开源界提前感受入关。\n\n* [在线阅读](https://ccpp.apachecn.org)\n* [在线阅读（Gitee）](https://apachecn.gitee.io/apachecn-c-cpp-zh/)\n* [ApacheCN 学习资源](http://docs.apachecn.org/)\n\n## 贡献指南\n\n本项目需要校对，欢迎大家提交 Pull Request。\n\n> 请您勇敢地去翻译和改进翻译。虽然我们追求卓越，但我们并不要求您做到十全十美，因此请不要担心因为翻译上犯错——在大部分情况下，我们的服务器已经记录所有的翻译，因此您不必担心会因为您的失误遭到无法挽回的破坏。（改编自维基百科）\n\n## 联系方式\n\n### 负责人\n\n* [飞龙](https://github.com/wizardforcel): 562826179\n\n### 其他\n\n*   在我们的 [apachecn/apachecn-c-cpp-zh](https://github.com/apachecn/apachecn-c-cpp-zh) github 上提 issue.\n*   发邮件到 Email: `apachecn@163.com`.\n*   在我们的 [组织学习交流群](http://www.apachecn.org/organization/348.html) 中联系群主/管理员即可.\n\n## 赞助我们\n\n![](http://data.apachecn.org/img/about/donate.jpg)\n"
  },
  {
    "path": "docs/boost-asio-cpp-net-prog-2e/SUMMARY.md",
    "content": "+   [Boost.Asio C++ 网络编程入门中文第二版](README.md)\n+   [零、前言](0.md)\n+   [一、使用 C++ 简化您的网络编程](1.md)\n+   [二、理解网络概念](2.md)\n+   [三、Boost C++ 库简介](3.md)\n+   [四、Boost.Asio 入门](4.md)\n+   [五、深入研究 Boost.Asio 库](5.md)\n+   [六、创建客户端——服务器应用](6.md)\n+   [七、调试代码并解决错误](7.md)\n"
  },
  {
    "path": "docs/boost-cpp-app-dev-cb/00.md",
    "content": "# 零、前言\n\n如果你想利用 Boost 和 C++ 的真正力量，避免在什么情况下使用哪个库的困惑，那么这本书就是为你准备的。\n从 Boost C++ 的基础知识开始，您将继续学习 Boost 库如何简化应用开发。您将学习转换数据，例如字符串到数字、数字到字符串、数字到数字等等。管理资源将成为小菜一碟。您将看到在编译时可以做什么样的工作，以及 Boost 容器可以做什么。您将学到开发高质量、快速和可移植应用的所有知识。写一次程序，然后就可以在 Linux、Windows、macOS、安卓操作系统上使用了。从操作图像到图表、目录、计时器、文件和网络，每个人都会发现一个有趣的话题。\n注意，本书的知识不会过时，因为越来越多的 Boost 库成为 C++ 标准的一部分。\n\n# 这本书涵盖了什么\n\n[第 1 章](01.html#RL0A0-712b4ba1126a4c7c89e1d44de61b4bdd)*开始写你的应用*，告诉你日常使用的图书馆。我们将看到如何从不同的来源获得配置选项，以及使用 Boost 库作者引入的一些数据类型可以实现什么。\n\n[第 2 章](02.html#36VSO0-712b4ba1126a4c7c89e1d44de61b4bdd)、*管理资源*，处理由 Boost 库引入的数据类型，主要集中在处理指针上。我们将看到如何轻松管理资源，以及如何使用能够存储任何函数对象、函数和 lambda 表达式的数据类型。读完这一章，你的代码将变得更加可靠，内存泄漏将成为历史。\n\n[第 3 章](03.html#515F20-712b4ba1126a4c7c89e1d44de61b4bdd)、*转换和转换*，描述了如何将字符串、数字和用户定义的类型相互转换，如何安全地转换多态类型，以及如何在 C++ 源文件内部编写大小解析器。涵盖了日常使用和极少数情况下的多种数据转换方式。\n\n[第 4 章](04.html#6FSQK0-712b4ba1126a4c7c89e1d44de61b4bdd)、*编译时技巧*描述了 Boost 库的一些基本示例，这些示例可用于优化算法的编译时检查以及其他元编程任务。没有它，理解 Boost 源和其他类似 Boost 的库是不可能的。\n\n[第 5 章](05.html#7PRJC0-712b4ba1126a4c7c89e1d44de61b4bdd)、*多线程*，重点介绍多线程编程的基础知识以及与之相关的所有东西。\n\n[第 6 章](06.html#9KVM80-712b4ba1126a4c7c89e1d44de61b4bdd) *【操纵任务】*显示了将功能对象称为任务。这一章的主要思想是，我们可以将所有的处理、计算和交互分成函子(任务)，并且几乎独立地处理这些任务中的每一个。此外，我们可能不会阻止一些缓慢的操作(例如从套接字接收数据或等待超时)，而是提供一个回调任务并继续处理其他任务。一旦操作系统完成慢速操作，我们的回调将被执行。\n\n[第七章](07.html#BBB6A0-712b4ba1126a4c7c89e1d44de61b4bdd)、*操纵琴弦*，展示了改变、搜索和表现琴弦的不同方面。我们将看到如何使用 Boost 库轻松完成一些常见的字符串相关任务。它处理非常常见的字符串操作任务。\n\n[第八章](08.html#CL9V20-712b4ba1126a4c7c89e1d44de61b4bdd)、*元编程*，介绍了一些很酷很难理解的元编程方法。在本章中，我们将更深入地了解如何将多个类型打包成一个类似元组的类型。我们将创建操作类型集合的函数，我们将看到编译时集合的类型如何改变，以及编译时技巧如何与运行时混合。\n\n[第九章](09.html#E4VR60-712b4ba1126a4c7c89e1d44de61b4bdd)、*集装箱*，讲的是助推集装箱以及与之直接相连的东西。本章提供了关于可以在日常编程中使用的 Boost 类的信息，这将使您的代码更快，新应用的开发更容易。\n\n[第 10 章](10.html#FKLNA0-712b4ba1126a4c7c89e1d44de61b4bdd)、*收集平台和编译器信息*，描述了用于检测编译器、平台和 boost 特性的不同帮助宏——这些宏在 Boost 库中广泛使用，对于编写能够处理任何编译器标志的可移植代码至关重要。\n\n[第 11 章](11.html#GUKG20-712b4ba1126a4c7c89e1d44de61b4bdd)、*使用系统*，详细介绍了文件系统以及如何创建和删除文件。我们将看到数据如何在不同的系统进程之间传递，如何以最大速度读取文件，以及如何执行其他技巧。\n\n[第 12 章](12.html#IK1FI0-712b4ba1126a4c7c89e1d44de61b4bdd)、*摸着冰山一角*，专门介绍一些大图书馆，给大家一些基础知识作为开始。\n\n# 这本书你需要什么\n\n你需要一个现代的 C++ 编译器，Boost 库(任何版本都可以，推荐 1.65 或者更新的版本)，和 QtCreator/qmake，或者直接导航到[http://apolukhin.GitHub.io/Boost-Cookbook/](http://apolukhin.github.io/Boost-Cookbook/)在线运行和实验例子。\n\n# 这本书是给谁的\n\n这本书是为那些希望提高 Boost 知识，并希望简化应用开发过程的开发人员准备的。假设具有标准库的 C++ 知识和基础知识。\n\n# 部分\n\n在这本书里，你会发现几个经常出现的标题(准备，怎么做...，它是如何工作的...，还有更多...，另请参阅)。为了给出如何完成配方的明确说明，我们使用以下部分:\n\n# 准备好\n\n本节告诉您配方中的预期内容，并描述如何设置配方所需的任何软件或任何初步设置。\n\n# 怎么做…\n\n本节包含遵循配方所需的步骤。\n\n# 它是如何工作的…\n\n这一部分通常包括对前一部分发生的事情的详细解释。\n\n# 还有更多…\n\n本节包含关于配方的附加信息，以便读者更好地了解配方。\n\n# 请参见\n\n本节提供了该配方的其他有用信息的有用链接。\n\n# 约定\n\n在这本书里，你会发现许多区分不同种类信息的文本样式。以下是这些风格的一些例子和对它们的意义的解释。\n\n文本中的码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟网址、用户输入和推特句柄如下所示:\n\n“记住这个库不仅仅是一个头文件，所以你的程序必须链接到`libboost_program_options`库”。\n\n代码块设置如下:\n\n```cpp\n#include <boost/program_options.hpp> \n#include <iostream>\nnamespace opt = boost::program_options; \nint main(int argc, char *argv[])\n{\n```\n\n当我们希望将您的注意力吸引到代码块的特定部分时，相关的行或项目以粗体显示:\n\n```cpp\n#include <boost/program_options.hpp> \n#include <iostream>\nnamespace opt = boost::program_options; \nint main(int argc, char *argv[])\n```\n\n任何命令行输入或输出都编写如下:\n\n```cpp\n $ ./our_program.exe --apples=10 --oranges=20\nFruits count: 30\n```\n\n**新名词**和**重要词语**以粗体显示。\n\nWarnings or important notes appear in a box like this. Tips and tricks appear like this.\n\n# 读者反馈\n\n我们随时欢迎读者的反馈。让我们知道你对这本书的看法——你喜欢或不喜欢什么。读者反馈对我们来说很重要，因为它有助于我们开发出你真正能从中获益的标题。要给我们发送一般反馈，只需发送电子邮件`feedback@packtpub.com`，并在您的邮件主题中提及书名。如果您对某个主题有专业知识，并且对写作或投稿感兴趣，请参见我们位于[www.packtpub.com/authors](http://www.packtpub.com/authors)的作者指南。\n\n# 客户支持\n\n现在，您已经自豪地拥有了一本书，我们有许多东西可以帮助您从购买中获得最大收益。\n\n# 下载示例代码\n\n你可以从你在[http://www.packtpub.com](http://www.packtpub.com)的账户下载这本书的示例代码文件。如果您在其他地方购买了这本书，您可以访问[http://www.packtpub.com/support](http://www.packtpub.com/support)并注册，以便将文件直接通过电子邮件发送给您。您可以按照以下步骤下载代码文件:\n\n1.  使用您的电子邮件地址和密码登录或注册我们的网站。\n2.  将鼠标指针悬停在顶部的“支持”选项卡上。\n3.  点击代码下载和勘误表。\n4.  在搜索框中输入图书的名称。\n5.  选择要下载代码文件的书籍。\n6.  从您购买这本书的下拉菜单中选择。\n7.  点击代码下载。\n\n下载文件后，请确保使用最新版本的解压缩文件夹:\n\n*   视窗系统的 WinRAR / 7-Zip\n*   zipeg/izp/un ARX for MAC\n*   适用于 Linux 的 7-Zip / PeaZip\n\n这本书的代码包也托管在 GitHub 上[https://GitHub . com/packt publishing/Boost-Cpp-Application-Development-cook book-第二版](https://github.com/PacktPublishing/Boost-Cpp-Application-Development-Cookbook-Second-Edition)。我们还有来自丰富的图书和视频目录的其他代码包，可在[https://github.com/PacktPublishing/](https://github.com/PacktPublishing/)获得。看看他们！\n\n本烹饪书中给出的示例的源代码文件也托管在作者的 GitHub 存储库中。您可以在[https://GitHub.com/apolukhin/Boost-Cookbook](https://github.com/apolukhin/Boost-Cookbook)访问作者的存储库，获取最新版本的代码。 [](https://github.com/apolukhin/Boost-Cookbook) 。\n\n# 正误表\n\n尽管我们尽了最大努力来确保我们内容的准确性，但错误还是会发生。如果你在我们的某本书里发现了错误——可能是文本或代码中的错误——如果你能向我们报告，我们将不胜感激。通过这样做，你可以让其他读者免受挫折，并帮助我们改进这本书的后续版本。如果您发现任何勘误表，请访问[http://www.packtpub.com/submit-errata](http://www.packtpub.com/submit-errata)，选择您的书籍，点击勘误表提交表格链接，并输入您的勘误表的详细信息。一旦您的勘误表得到验证，您的提交将被接受，勘误表将上传到我们的网站或添加到该标题勘误表部分下的任何现有勘误表列表中。\n\n要查看之前提交的勘误表，请前往[https://www.packtpub.com/books/content/support](https://www.packtpub.com/books/content/support)并在搜索栏中输入图书名称。所需信息将出现在勘误表部分。\n\n# 海盗行为\n\n在互联网上盗版受版权保护的材料是一个贯穿所有媒体的持续问题。在 Packt，我们非常重视版权和许可证的保护。如果您在互联网上遇到任何形式的我们作品的非法拷贝，请立即向我们提供位置地址或网站名称，以便我们寻求补救。\n\n请通过`copyright@packtpub.com`联系我们，获取疑似盗版资料的链接。\n\n我们感谢您在保护我们的作者方面的帮助，以及我们为您带来有价值内容的能力。\n\n# 问题\n\n如果您对本书的任何方面有问题，可以在`questions@packtpub.com`联系我们，我们将尽最大努力解决问题。"
  },
  {
    "path": "docs/boost-cpp-app-dev-cb/01.md",
    "content": "# 一、开始编写应用\n\n在本章中，我们将介绍:\n\n*   获取配置选项\n*   在容器/变量中存储任何值\n*   在容器/变量中存储多个选择的类型\n*   使用更安全的方法来处理存储多个选定类型的容器\n*   如果没有值，则返回一个值或标志\n*   从函数返回数组\n*   将多个值组合成一个\n*   绑定和重新排序函数参数\n*   获取人类可读的类型名\n*   使用 C++ 11 移动仿真\n*   制作不可复制的类\n*   创建一个不可复制但可移动的类\n*   使用 C++ 14 和 C++ 11 算法\n\n# 介绍\n\n**Boost** 是 C++ 库的集合。每个库在被 Boost 接受之前都经过了很多专业程序员的审核。使用许多编译器和许多 C++ 标准库实现在多个平台上测试库。使用 Boost 时，您可以确信您使用的是最便携、最快速、最可靠的解决方案之一，该解决方案是根据适用于商业和开源项目的许可证分发的。\n\nBoost 的许多部分已经包含在 C++ 11、C++ 14 和 C++ 17 中。此外，Boost 库将包含在下一个 C++ 标准中。你会在这本书的每个食谱中找到 C++ 标准特定的注释。\n\n没有长篇大论的介绍，我们开始吧！\n\n在本章中，我们将看到一些日常使用的食谱。我们将看到如何从不同的来源获得配置选项，以及使用 Boost 库作者引入的一些数据类型可以实现什么。\n\n# 获取配置选项\n\n来看看一些控制台程序，比如 Linux 中的`cp`。他们都有一个奇特的帮助；它们的输入参数不依赖于任何位置，并且具有人类可读的语法。例如:\n\n```cpp\n$ cp --help\nUsage: cp [OPTION]... [-T] SOURCE DEST\n  -a, --archive           same as -dR --preserve=all\n  -b                      like --backup but does not accept an argument \n```\n\n您可以在 10 分钟内为您的程序实现相同的功能。你所需要的只是`Boost.ProgramOptions`图书馆。\n\n# 准备好\n\n这个食谱只需要 C++ 的基本知识。请记住，这个库不仅仅是一个头，所以你的程序必须链接到`libboost_program_options`库。\n\n# 怎么做...\n\n让我们从一个简单的程序开始，该程序接受`apples`和`oranges`的计数作为输入，并计算水果的总数。我们希望实现以下结果:\n\n```cpp\n $ ./our_program.exe --apples=10 --oranges=20 Fruits count: 30\n```\n\n请执行以下步骤:\n\n1.  包含`boost/program_options.hpp`头，为`boost::program_options`命名空间做一个别名(太长了，打不出来！).我们还需要一个`<iostream>`标题:\n\n```cpp\n#include <boost/program_options.hpp> \n#include <iostream> \n\nnamespace opt = boost::program_options; \n```\n\n2.  现在，我们准备在`main()`功能中描述我们的选项:\n\n```cpp\nint main(int argc, char *argv[])\n{\n    // Constructing an options describing variable and giving \n    // it a textual description \"All options\". \n    opt::options_description desc(\"All options\"); \n\n    // When we are adding options, first parameter is a name\n    // to be used in command line. Second parameter is a type\n    // of that option, wrapped in value<> class. Third parameter\n    // must be a short description of that option.\n    desc.add_options()\n        (\"apples\", opt::value<int>(), \"how many apples do \n                                       you have\")\n        (\"oranges\", opt::value<int>(), \"how many oranges do you \n                                        have\")\n        (\"help\", \"produce help message\")\n    ;\n```\n\n3.  让我们解析命令行:\n\n```cpp\n    // Variable to store our command line arguments.\n    opt::variables_map vm; \n\n    // Parsing and storing arguments.\n    opt::store(opt::parse_command_line(argc, argv, desc), vm); \n\n    // Must be called after all the parsing and storing.\n    opt::notify(vm);\n```\n\n4.  让我们添加一些代码来处理`help`选项:\n\n```cpp\n    if (vm.count(\"help\")) {\n        std::cout << desc << \"\\n\";\n        return 1;\n    }\n```\n\n5.  最后一步。水果计数可以通过以下方式实现:\n\n```cpp\n    std::cout << \"Fruits count: \"\n        << vm[\"apples\"].as<int>() + vm[\"oranges\"].as<int>()\n        << std::endl;\n\n} // end of `main`\n```\n\n现在，如果我们用`help`参数调用我们的程序，我们将得到以下输出:\n\n```cpp\nAll options: \n    --apples arg          how many apples do you have\n    --oranges arg        how many oranges do you have \n    --help                    produce help message \n```\n\n如您所见，我们没有为`help`选项的值提供类型，因为我们不期望任何值被传递给它。\n\n# 它是如何工作的...\n\n这个例子很容易从代码和注释中理解。运行它会产生预期的结果:\n\n```cpp\n $ ./our_program.exe --apples=100 --oranges=20 Fruits count: 120\n```\n\n# 还有更多...\n\nC++ 标准采用了很多 Boost 库；然而，即使在 C++ 17 中也找不到`Boost.ProgramOptions`。目前还没有计划将其采用到 C++ 2a 中。\n\n`ProgramOptions`库非常强大，有很多功能。以下是如何:\n\n*   将配置选项值直接解析为一个变量，并使该选项成为必需选项:\n\n```cpp\n    int oranges_var = 0;\n    desc.add_options()\n        // ProgramOptions stores the option value into \n        // the variable that is passed by pointer. Here value of \n        // \"--oranges\" option will be stored into 'oranges_var'.\n        (\"oranges,o\", opt::value<int>(&oranges_var)->required(), \n                                                \"oranges you have\")\n```\n\n*   获取一些强制字符串选项:\n\n```cpp\n        // 'name' option is not marked with 'required()',\n        // so user may not provide it.\n        (\"name\", opt::value<std::string>(), \"your name\")\n```\n\n*   添加苹果的简称，将`10`设为`apples`的默认值:\n\n```cpp\n        // 'a' is a short option name for apples. Use as '-a 10'.\n        // If no value provided, then the default value is used.\n        (\"apples,a\", opt::value<int>()->default_value(10),\n                                   \"apples that you have\");\n```\n\n*   从配置文件中获取缺少的选项:\n\n```cpp\n    opt::variables_map vm;\n\n    // Parsing command line options and storing values to 'vm'.\n    opt::store(opt::parse_command_line(argc, argv, desc), vm);\n\n    // We can also parse environment variables. Just use\n    // 'opt::store with' 'opt::parse_environment' function.\n\n    // Adding missing options from \"apples_oranges.cfg\" config file.\n    try {\n        opt::store(\n            opt::parse_config_file<char>(\"apples_oranges.cfg\", desc),\n            vm\n        );\n    } catch (const opt::reading_file& e) {\n        std::cout << \"Error: \" << e.what() << std::endl;\n    }\n```\n\nThe configuration file syntax differs from the command-line syntax. We do not need to place minuses before the options. So, our `apples_oranges.cfg` file must look like this:\n`oranges=20`\n\n*   验证是否设置了所有必需的选项:\n\n```cpp\n    try {\n        // `opt::required_option` exception is thrown if\n        // one of the required options was not set.\n        opt::notify(vm);\n\n    } catch (const opt::required_option& e) {\n        std::cout << \"Error: \" << e.what() << std::endl;\n        return 2;\n    }\n```\n\n如果我们将所有提到的提示组合成一个可执行文件，那么它的`help`命令将产生以下输出:\n\n```cpp\n$ ./our_program.exe --help\n All options:\n   -o [ --oranges ] arg          oranges that you have\n   --name arg                       your name\n   -a [ --apples ] arg (=10)  apples that you have\n   --help                              produce help message\n\n```\n\n在没有配置文件的情况下运行它将产生以下输出:\n\n```cpp\n$ ./our_program.exe\n Error: can not read options configuration file 'apples_oranges.cfg'\n Error: the option '--oranges' is required but missing \n```\n\n用配置文件中的`oranges=20`运行程序会生成++，因为苹果的默认值是`10`:\n\n```cpp\n$ ./our_program.exe\n Fruits count: 30\n```\n\n# 请参见\n\n*   Boost 的官方文档包含了更多的例子，告诉我们`Boost.ProgramOptions`更高级的特性，比如位置相关选项、非常规语法等等；这在[http://boost.org/libs/program_options](http://boost.org/libs/program_options)有售\n*   您可以在[http://apolukhin.github.io/Boost-Cookbook](http://apolukhin.github.io/Boost-Cookbook)在线修改并运行本书中的所有示例\n\n# 在容器/变量中存储任何值\n\n如果你一直在用 Java、C#或 Delphi 编程，你肯定会错过用 C++ 中的`Object`值类型创建容器的能力。这些语言中的`Object`类是几乎所有类型的基本类，因此您可以随时为它赋值。想象一下，在 C++ 中拥有这样一个特性有多好:\n\n```cpp\ntypedef std::unique_ptr<Object> object_ptr; \n\nstd::vector<object_ptr> some_values; \nsome_values.push_back(new Object(10)); \nsome_values.push_back(new Object(\"Hello there\")); \nsome_values.push_back(new Object(std::string(\"Wow!\"))); \n\nstd::string* p = dynamic_cast<std::string*>(some_values.back().get()); \nassert(p); \n(*p) += \" That is great!\\n\"; \nstd::cout << *p; \n```\n\n# 准备好\n\n我们将使用只有标题的库。这个食谱只需要 C++ 的基本知识。\n\n# 怎么做...\n\nBoost 提供了一个解决方案`Boost.Any`库，它有更好的语法:\n\n```cpp\n#include <boost/any.hpp> \n#include <iostream> \n#include <vector> \n#include <string> \n\nint main() { \n    std::vector<boost::any> some_values; \n    some_values.push_back(10); \n    some_values.push_back(\"Hello there!\"); \n    some_values.push_back(std::string(\"Wow!\"));\n\n    std::string& s = boost::any_cast<std::string&>(some_values.back()); \n    s += \" That is great!\"; \n    std::cout << s; \n} \n```\n\n很棒，不是吗？顺便说一下，它有一个空状态，可以使用`empty()`成员函数来检查(就像在标准库容器中一样)。\n\n您可以使用两种方法从`boost::any`获得该值:\n\n```cpp\nvoid example() {\n    boost::any variable(std::string(\"Hello world!\"));\n\n    // Following method may throw a boost::bad_any_cast exception\n    // if actual value in variable is not a std::string.\n    std::string s1 = boost::any_cast<std::string>(variable);\n\n    // Never throws. If actual value in variable is not a std::string\n    // will return an NULL pointer.\n    std::string* s2 = boost::any_cast<std::string>(&variable);\n}\n```\n\n# 它是如何工作的...\n\n`boost::any`类只是在其中存储任何值。为了实现这一点，它使用了**类型擦除**技术(接近于 Java 或 C#对所有类型的做法)。要使用这个库，你不需要知道它的内部实现细节，但是这里有一个好奇者类型擦除技术的快速浏览。\n\n在分配类型为`T`的某个变量时，`Boost.Any`实例化了一个`holder<T>`类型，该类型可以存储指定类型`T`的值，并且是从某个基类型`placeholder`中派生出来的:\n\n```cpp\ntemplate<typename ValueType>\nstruct holder : public placeholder {\n    virtual const std::type_info& type() const {\n         return typeid(ValueType);\n    }\n     ValueType held;\n};\n```\n\n一个`placeholder`类型具有虚拟功能，用于获取一个存储类型`T`的`std::type_info`和克隆一个存储类型:\n\n```cpp\nstruct placeholder {\n    virtual ~placeholder() {}\n    virtual const std::type_info& type() const = 0;\n};\n```\n\n`boost::any`存储`ptr` -指向`placeholder`的指针。使用`any_cast<T>()`时，`boost::any`检查调用`ptr->type()`使`std::type_info`等于`typeid(T)`并返回`static_cast<holder<T>*>(ptr)->held`。\n\n# 还有更多...\n\n这种灵活性从来不是没有代价的。复制构造、值构造、复制赋值、赋值给`boost::any`的实例进行动态内存分配；所有类型转换都进行**运行时类型信息** ( **RTTI** )检查；`boost::any`大量使用虚函数。如果你热衷于性能，下一个食谱会告诉你如何在没有动态分配和 RTTI 使用的情况下获得几乎相同的结果。\n\n`boost::any`使用**右值引用**，但不能在**常量**中使用。\n\n`Boost.Any`库被 C++ 17 接受。如果您的编译器兼容 C++ 17，并且您希望避免对`any`使用 Boost，只需将`boost`命名空间替换为命名空间`std`，并包含`<any>`而不是`<boost/any.hpp>`。如果您在`std::any`中存储微小的对象，您的标准库实现可能会稍微快一些。\n\n`std::any` has the `reset()` function instead of `clear()` and `has_value()` instead of `empty()`. Almost all exceptions in Boost derived from the `std::exception` class or from its derivatives, for example, `boost::bad_any_cast` is derived from `std::bad_cast`. It means that you can catch almost all Boost exceptions using `catch (const std::exception& e)`.\n\n# 请参见\n\n*   Boost 的官方文档可能会给你更多的例子；可以在[http://boost.org/libs/any](http://www.boost.org/libs/any)找到\n*   使用一种更安全的方法来处理存储多种选择类型的容器配方，以获得关于该主题的更多信息\n\n# 在容器/变量中存储多个选择的类型\n\nC++ 03 联合只能持有名为**普通旧数据** ( **POD** )的极其简单的类型。例如在 C++ 03 中，不能将`std::string`或`std::vector`存储在并集中。\n\n你知道 C++ 11 中**无限制并集**的概念吗？让我简单地告诉你这件事。C++ 11 放宽了对联合的要求，但是你必须自己管理非 POD 类型的构造和销毁。您必须调用就地构建/销毁，并记住存储在联合中的类型。工作量很大，不是吗？\n\n我们能在 C++ 03 中有一个像变量一样的不受限制的联合来管理对象的生存期并记住它的类型吗？\n\n# 准备好\n\n我们将使用简单易用的仅头库。这个食谱只需要 C++ 的基本知识。\n\n# 怎么做...\n\n让我给你介绍一下`Boost.Variant`图书馆。\n\n1.  `Boost.Variant`库可以存储编译时指定的任何类型。它还管理就地构建/销毁，甚至不需要 C++ 11 标准:\n\n```cpp\n#include <boost/variant.hpp>\n#include <iostream>\n#include <vector>\n#include <string>\n\nint main() {\n    typedef boost::variant<int, const char*, std::string> my_var_t;\n    std::vector<my_var_t> some_values;\n    some_values.push_back(10);\n    some_values.push_back(\"Hello there!\");\n    some_values.push_back(std::string(\"Wow!\"));\n\n    std::string& s = boost::get<std::string>(some_values.back());\n    s += \" That is great!\\n\";\n    std::cout << s;\n} \n```\n\n很棒，不是吗？\n\n2.  `Boost.Variant`没有空状态，但是有一个`empty()`功能，没有用，总是返回`false`。如果需要表示空状态，只需在`Boost.Variant`库支持的类型的第一个位置添加一些简单的类型即可。当`Boost.Variant`包含该类型时，将其解释为空状态。这里有一个例子，我们将使用`boost::blank`类型来表示空状态:\n\n```cpp\nvoid example1() {\n    // Default constructor constructs an instance of boost::blank.\n    boost::variant<\n        boost::blank, int, const char*, std::string\n    > var;\n\n    // 'which()' method returns an index of a type\n    // currently held by variant.\n    assert(var.which() == 0); // boost::blank\n\n    var = \"Hello, dear reader\";\n    assert(var.which() != 0);\n}\n```\n\n3.  您可以使用两种方法从变量中获取值:\n\n```cpp\nvoid example2() {\n    boost::variant<int, std::string> variable(0);\n\n    // Following method may throw a boost::bad_get\n    // exception if actual value in variable is not an int.\n    int s1 = boost::get<int>(variable);\n\n    // If actual value in variable is not an int will return NULL.\n    int* s2 = boost::get<int>(&variable);\n}\n```\n\n# 它是如何工作的...\n\n`boost::variant`类保存一个字节数组，并将值存储在该数组中。数组的大小在编译时通过应用`sizeof()`和函数来确定，以便与每个模板类型对齐。在赋值或构造`boost::variant`时，先前的值被就地析构，新的值被构造在字节数组的顶部，使用新的位置。\n\n# 还有更多...\n\n`Boost.Variant`变量通常不动态分配内存，也不需要启用 RTTI。`Boost.Variant`速度极快，被其他 Boost 库广泛使用。为了获得最大性能，请确保在第一个位置的支持类型列表中有一个简单类型。`boost::variant`利用 C++ 11 的右值引用，如果它们在你的编译器上可用的话。\n\n`Boost.Variant`是 C++ 17 标准的一部分。`std::variant`与`boost::variant`略有不同:\n\n*   `std::variant`在`<variant>`头文件中声明，而不是在`<boost.variant.hpp>`中声明\n*   `std::variant`从不分配内存\n*   `std::variant`可与 constexpr 一起使用\n*   不是写`boost::get<int>(&variable)`，你得为`std::variant`写`std::get_if<int>(&variable)`\n*   `std::variant`不能递归保持自身，错过了一些其他高级技术\n*   `std::variant`可以原地构造物体\n*   `std::variant`有`index()`而不是`which()`\n\n# 请参见\n\n*   使用一种更安全的方法来处理储存多种选择类型的配方的容器\n*   Boost 的官方文档包含了更多关于`Boost.Variant`其他一些特性的例子和描述，可以在:[http://boost.org/libs/variant](http://www.boost.org/libs/variant)找到\n*   在[http://apolukhin.github.io/Boost-Cookbook](http://apolukhin.github.io/Boost-Cookbook)在线试验代码\n\n# 使用更安全的方法来处理存储多个选定类型的容器\n\n假设您正在为某个 SQL 数据库接口创建一个包装器。您决定`boost::any`将完美地匹配数据库表的单个单元格的要求。\n\n其他一些程序员将使用您的类，他/她的任务是从数据库中获取一行，并计算一行中算术类型的总和。\n\n这就是这样一个代码的样子:\n\n```cpp\n#include <boost/any.hpp> \n#include <vector> \n#include <string> \n#include <typeinfo> \n#include <algorithm> \n#include <iostream> \n\n// This typedefs and methods will be in our header, \n// that wraps around native SQL interface.\ntypedef boost::any cell_t; \ntypedef std::vector<cell_t> db_row_t; \n\n// This is just an example, no actual work with database. \ndb_row_t get_row(const char* /*query*/) { \n    // In real application 'query' parameter shall have a 'const \n    // char*' or 'const std::string&' type? See recipe \"Type  \n    // 'reference to string'\" for an answer. \n    db_row_t row; \n    row.push_back(10); \n    row.push_back(10.1f); \n    row.push_back(std::string(\"hello again\")); \n    return row; \n} \n\n// This is how a user will use your classes \nstruct db_sum { \nprivate: \n    double& sum_; \npublic: \n    explicit db_sum(double& sum) \n        : sum_(sum) \n    {} \n\n    void operator()(const cell_t& value) { \n        const std::type_info& ti = value.type(); \n        if (ti == typeid(int)) { \n            sum_ += boost::any_cast<int>(value); \n        } else if (ti == typeid(float)) { \n            sum_ += boost::any_cast<float>(value); \n        } \n    } \n}; \n\nint main() { \n    db_row_t row = get_row(\"Query: Give me some row, please.\"); \n    double res = 0.0; \n    std::for_each(row.begin(), row.end(), db_sum(res)); \n    std::cout << \"Sum of arithmetic types in database row is: \"\n              << res << std::endl; \n} \n```\n\n如果您编译并运行这个示例，它将输出一个正确的答案:\n\n```cpp\nSum of arithmetic types in database row is: 20.1\n```\n\n你还记得自己在阅读`operator()`的实现时的想法吗？我猜他们是，*“那么双，长，短，无符号，和其他类型呢？”*同样的想法会出现在使用你的界面的程序员的脑海中。因此，您需要仔细记录由您的`cell_t`存储的值，或者使用更优雅的解决方案，如下节所述。\n\n# 准备好\n\n如果您还不熟悉`Boost.Variant`和`Boost.Any`库，强烈建议阅读前面两个食谱。\n\n# 怎么做...\n\n`Boost.Variant`库实现了访问存储数据的访问者编程模式，这比通过`boost::get<>`获取值安全得多。这种模式迫使程序员处理变体中的每一种类型，否则代码将无法编译。您可以通过`boost::apply_visitor`功能使用该模式，该功能将`visitor`功能对象作为第一个参数，将`variant`作为第二个参数。如果您使用的是预 C++ 14 编译器，那么`visitor`函数对象必须从`boost::static_visitor<T>`类派生，其中`T`是由`visitor`返回的类型。对于变量存储的每种类型，一个`visitor`对象必须有`operator()`的重载。\n\n让我们将`cell_t`类型更改为`boost::variant<int, float, string>`并修改我们的示例:\n\n```cpp\n#include <boost/variant.hpp> \n#include <vector> \n#include <string> \n#include <iostream> \n\n// This typedefs and methods will be in header, \n// that wraps around native SQL interface. \ntypedef boost::variant<int, float, std::string> cell_t; \ntypedef std::vector<cell_t> db_row_t; \n\n// This is just an example, no actual work with database. \ndb_row_t get_row(const char* /*query*/) { \n    // See recipe \"Type 'reference to string'\" \n    // for a better type for 'query' parameter. \n    db_row_t row; \n    row.push_back(10); \n    row.push_back(10.1f); \n    row.push_back(\"hello again\"); \n    return row; \n} \n\n// This is a code required to sum values. \n// We can provide no template parameter \n// to boost::static_visitor<> if our visitor returns nothing. \nstruct db_sum_visitor: public boost::static_visitor<double> { \n    double operator()(int value) const { \n        return value; \n    } \n    double operator()(float value) const { \n        return value; \n    } \n    double operator()(const std::string& /*value*/) const { \n        return 0.0; \n    } \n}; \n\nint main() { \n    db_row_t row = get_row(\"Query: Give me some row, please.\"); \n    double res = 0.0; \n    for (auto it = row.begin(), end = row.end(); it != end; ++ it) { \n        res += boost::apply_visitor(db_sum_visitor(), *it); \n    } \n\n    std::cout << \"Sum of arithmetic types in database row is: \"\n              << res << std::endl;\n}\n```\n\n# 它是如何工作的...\n\n在编译时，`Boost.Variant`库生成一个大的`switch`语句，每个语句从变量的类型列表中为一个类型调用一个`visitor`。运行时，使用`which()`检索存储类型的索引，并跳转到`switch`语句中的正确案例。将为`boost::variant<int, float, std::string>`生成类似这样的内容:\n\n```cpp\nswitch (which()) \n{ \ncase 0 /*int*/: \n    return visitor(*reinterpret_cast<int*>(address())); \ncase 1 /*float*/: \n    return visitor(*reinterpret_cast<float*>(address())); \ncase 2 /*std::string*/: \n    return visitor(*reinterpret_cast<std::string*>(address())); \ndefault: assert(false); \n} \n```\n\n这里，`address()`函数返回一个指向`boost::variant<int, float, std::string>`内部存储器的指针。\n\n# 还有更多...\n\n如果我们将这个例子与这个食谱中的第一个例子进行比较，我们会看到`boost::variant`的以下优点:\n\n*   我们知道变量可以存储哪些类型\n*   如果一个 SQL 接口的库编写器添加或修改了一个`variant`持有的类型，我们会得到一个编译时错误，而不是不正确的行为\n\n来自 C++ 17 的`std::variant`也支持访问。只写`std::visit`而不是`boost::apply_visitor`就完事了。\n\nYou can download the example code files for all Packt books that you have purchased from your account at [http://www.PacktPub.com](http://www.PacktPub.com). If you purchased this book elsewhere, you can visit [http://www.PacktPub.com/](http://www.PacktPub.com/)support, and register to have the files emailed directly to you.\n\n# 请参见\n\n*   在阅读了[第 4 章](04.html#6FSQK0-712b4ba1126a4c7c89e1d44de61b4bdd)、*编译时技巧*中的一些方法后，即使底层类型发生变化，您也能够制作出正常工作的通用`visitor`对象\n*   Boost 的官方文档包含了更多的例子和对`Boost.Variant`其他一些特性的描述；可通过以下链接获得:[http://boost.org/libs/variant](http://www.boost.org/libs/variant)\n\n# 如果没有值，则返回一个值或标志\n\n想象一下，我们有一个函数，它不会抛出异常并返回一个值或指示发生了错误。在 Java 或 C#编程语言中，这种情况是通过比较函数值的返回值和`null`指针来处理的。如果功能返回`null`，则出现错误。在 C++ 中，从函数返回指针会让库用户感到困惑，并且通常需要缓慢的动态内存分配。\n\n# 准备好\n\n这个食谱只需要 C++ 的基础知识。\n\n# 怎么做...\n\n女士们先生们，让我用下面的例子向你们介绍`Boost.Optional`图书馆:\n\n`try_lock_device()`函数试图获取一个设备的锁，并可能成功或失败，这取决于不同的条件(在我们的例子中，它取决于一些`try_lock_device_impl()`函数调用):\n\n```cpp\n#include <boost/optional.hpp>\n#include <iostream>\n\nclass locked_device {\n    explicit locked_device(const char* /*param*/) {\n        // We have unique access to device.\n        std::cout << \"Device is locked\\n\";\n    }\n\n    static bool try_lock_device_impl();\n\npublic:\n    void use() {\n        std::cout << \"Success!\\n\";\n    }\n\n    static boost::optional<locked_device> try_lock_device() {\n        if (!try_lock_device_impl()) {\n            // Failed to lock device.\n            return boost::none;\n        }\n\n        // Success!\n        return locked_device(\"device name\");\n    }\n\n    ~locked_device(); // Releases device lock.\n};\n```\n\n该函数返回可转换为`bool`的`boost::optional`变量。如果返回值等于`true`，则获取锁，并且可以通过对返回的可选变量解引用来获得使用该设备的类的实例:\n\n```cpp\nint main() { \n    for (unsigned i = 0; i < 10; ++ i) { \n        boost::optional<locked_device> t\n            = locked_device::try_lock_device(); \n\n        // optional is convertible to bool.\n        if (t) { \n            t->use(); \n            return 0; \n        } else { \n            std::cout << \"...trying again\\n\"; \n        } \n    } \n\n    std::cout << \"Failure!\\n\"; \n    return -1; \n} \n```\n\n该程序将输出以下内容:\n\n```cpp\n...trying again\n...trying again\nDevice is locked\nSuccess!\n```\n\nThe default constructed `optional` variable is convertible to `false` and must not be dereferenced, because such an `optional` does not have an underlying type constructed.\n\n# 它是如何工作的...\n\n引擎盖下的`boost::optional<T>`有一个正确对齐的字节数组，其中`T`类型的对象可以是就地构造的。它还有一个`bool`变量来记住物体的状态(它是否被构造？).\n\n# 还有更多...\n\n`Boost.Optional`类不使用动态分配，也不需要基础类型的默认构造函数。当前的`boost::optional`实现可以使用 C++ 11 右值引用，但是不能用于 constexpr。\n\nIf you have a class `T` that has no empty state but your program logic requires an empty state or uninitialized `T`, then you have to come up with some workaround. Traditionally, users create some smart pointer to the class `T`, keep a `nullptr` in it, and dynamically allocate `T` if non empty state is required. Stop doing that! Use `boost::optional<T>` instead. It's a much faster and more reliable solution.\n\nC++ 17 标准包括`std::optional`类。只需将`<boost/optional.hpp>`替换为`<optional>`，将`boost::`替换为`std::`即可使用该类的标准版本。`std::optional`可与 constexpr 一起使用。\n\n# 请参见\n\nBoost 的官方文档包含了更多的例子，并描述了`Boost.Optional`的高级特性(比如就地建造)。该文档可通过以下链接获得:[http://boost.org/libs/optional.](http://www.boost.org/libs/optional)\n\n# 从函数返回数组\n\n让我们玩一个猜谜游戏！关于下面的功能，你能说些什么？\n\n```cpp\nchar* vector_advance(char* val); \n```\n\n程序员是否应该释放返回值？该函数是否试图取消分配输入参数？输入参数应该以零结尾，还是函数应该假设输入参数具有指定的宽度？\n\n现在，让我们把任务变得更难！请看下面一行:\n\n```cpp\nchar ( &vector_advance( char (&val)[4] ) )[4];\n```\n\n别担心。在了解这里发生的事情之前，我也挠了半个小时的头。`vector_advance`是接受并返回一个四元素数组的函数。有没有办法把这样的函数写清楚？\n\n# 准备好\n\n这个食谱只需要 C++ 的基础知识。\n\n# 怎么做...\n\n我们可以这样重写函数:\n\n```cpp\n#include <boost/array.hpp>\n\ntypedef boost::array<char, 4> array4_t;\narray4_t& vector_advance(array4_t& val);\n```\n\n这里，`boost::array<char, 4>`只是一个简单的包装器，围绕着一个由四个`char`元素组成的数组。\n\n这段代码回答了我们第一个示例中的所有问题，并且比第二个示例中的代码可读性更好。\n\n# 它是如何工作的...\n\n`boost::array`是固定大小的数组。`boost::array`的第一个模板参数是元素类型，第二个是数组的大小。如果需要在运行时更改数组大小，请改用`std::vector`、`boost::container::small_vector`、`boost::container::stack_vector`或`boost::container::vector`。\n\n`boost::array<>`类没有手写的构造函数，它的所有成员都是公共的，所以编译器会将其视为 POD 类型。\n\n# 还有更多...\n\n我们再来看一些`boost::array`用法的例子:\n\n```cpp\n#include <boost/array.hpp> \n#include <algorithm> \n\ntypedef boost::array<char, 4> array4_t; \n\narray4_t& vector_advance(array4_t& val) {\n    // C++ 11 lambda function\n    const auto inc = [](char& c){ ++ c; };\n\n    // boost::array has begin(), cbegin(), end(), cend(),\n    // rbegin(), size(), empty() and other functions that are\n    // common for standard library containers.\n    std::for_each(val.begin(), val.end(), inc);\n    return val;\n}\n\nint main() { \n    // We can initialize boost::array just like an array in C++ 11: \n    // array4_t val = {0, 1, 2, 3}; \n    // but in C++ 03 additional pair of curly brackets is required. \n    array4_t val = {{0, 1, 2, 3}}; \n\n    array4_t val_res;               // it is default constructible\n    val_res = vector_advance(val);  // it is assignable\n\n    assert(val.size() == 4); \n    assert(val[0] == 1); \n    /*val[4];*/ // Will trigger an assert because max index is 3 \n\n    // We can make this assert work at compile-time. \n    // Interested? See recipe 'Check sizes at compile-time' \n    assert(sizeof(val) == sizeof(char) * array4_t::static_size); \n} \n```\n\n`boost::array`最大的优点之一是不分配动态内存，提供与普通 C 数组完全相同的性能。C++ 标准委员会的人也喜欢它，所以它被接受为 C++ 11 标准。尝试包括`<array>`标题，并检查`std::array`的可用性。`std::array`自 C++ 17 以来，对 constexpr 的使用有了更好的支持。\n\n# 请参见\n\n*   Boost 的官方文档给出了`Boost.Array`方法的完整列表，并描述了方法的复杂性和抛出行为。可在以下链接获得:[http://boost.org/libs/array.](http://www.boost.org/libs/array)\n*   `boost::array`功能广泛应用于各种食谱；例如，参考*绑定一个值作为功能参数*配方。\n\n# 将多个值组合成一个\n\n有一份非常好的礼物送给喜欢的人。Boost 有一个名为`Boost.Tuple`的库。它就像`std::pair`一样，但是它也可以处理三元组、四元组，甚至更大的类型集合。\n\n# 准备好\n\n这个食谱只需要 C++ 的基础知识和一个标准库。\n\n# 怎么做...\n\n执行以下步骤将多个值合并为一个:\n\n1.  要开始使用元组，您需要包含一个适当的头并声明一个变量:\n\n```cpp\n#include <boost/tuple/tuple.hpp> \n#include <string> \n\nboost::tuple<int, std::string> almost_a_pair(10, \"Hello\");\nboost::tuple<int, float, double, int> quad(10, 1.0f, 10.0, 1);\n```\n\n2.  获取特定值是通过`boost::get<N>()`函数实现的，其中`N`是所需值的从零开始的索引:\n\n```cpp\n#include <boost/tuple/tuple.hpp>\n\nvoid sample1() {\n    const int i = boost::get<0>(almost_a_pair); \n    const std::string& str = boost::get<1>(almost_a_pair); \n    const double d = boost::get<2>(quad);\n}\n```\n\n`boost::get<>`功能有许多过载，在 Boost 中广泛使用。我们已经在*中看到了它如何与其他库一起使用，在容器/变量*配方中存储多个选择的类型。\n\n3.  您可以使用`boost::make_tuple()`函数构造元组，该函数编写起来更短，因为您不需要完全限定元组类型:\n\n```cpp\n#include <boost/tuple/tuple.hpp>\n#include <boost/tuple/tuple_comparison.hpp>\n#include <set>\n\nvoid sample2() {\n    // Tuple comparison operators are\n    // defined in header \"boost/tuple/tuple_comparison.hpp\"\n    // Don't forget to include it!\n    std::set<boost::tuple<int, double, int> > s;\n    s.insert(boost::make_tuple(1, 1.0, 2));\n    s.insert(boost::make_tuple(2, 10.0, 2));\n    s.insert(boost::make_tuple(3, 100.0, 2));\n\n    // Requires C++ 11\n    const auto t = boost::make_tuple(0, -1.0, 2);\n    assert(2 == boost::get<2>(t));\n    // We can make a compile time assert for type\n    // of t. Interested? See chapter 'Compile time tricks'\n}\n```\n\n4.  另一个让生活更轻松的功能是`boost::tie()`。它几乎和`make_tuple`一样工作，但是为每个传递的类型添加了一个非引用。这样的元组可以用来从另一个元组中获取变量的值。从下面的例子可以更好地理解:\n\n```cpp\n#include <boost/tuple/tuple.hpp>\n#include <cassert>\n\nvoid sample3() {\n    boost::tuple<int, float, double, int> quad(10, 1.0f, 10.0, 1); \n    int i; \n    float f; \n    double d; \n    int i2; \n\n    // Passing values from 'quad' variables \n    // to variables 'i', 'f', 'd', 'i2'.\n    boost::tie(i, f, d, i2) = quad; \n    assert(i == 10); \n    assert(i2 == 1); \n}\n```\n\n# 它是如何工作的...\n\n有些读者可能想知道，当我们总是可以用更好的名字编写自己的结构时，为什么我们需要一个元组；例如，我们可以创建一个结构，而不是写`boost::tuple<int, std::string>`:\n\n```cpp\nstruct id_name_pair { \n    int id; \n    std::string name; \n}; \n```\n\n嗯，这个结构肯定比`boost::tuple<int, std::string>`清晰。元组库背后的主要思想是简化模板编程。\n\n# 还有更多...\n\n元组的工作速度与`std::pair`一样快(它不在堆上分配内存，也没有虚函数)。C++ 委员会发现这个类非常有用，它被包含在标准库中。你可以在头文件`<tuple>`的 C++ 11 兼容实现中找到它(别忘了用`std::`替换所有的`boost::`名称空间)。\n\ntuple 的标准库版本必须具有多个微优化，并且通常提供稍好的用户体验。但是，元组元素的构造顺序没有保证，所以，如果你需要一个从第一个开始构造其元素的元组，你必须使用`boost::tuple`:\n\n```cpp\n#include <boost/tuple/tuple.hpp>\n#include <iostream>\n\ntemplate <int I>\nstruct printer {\n    printer() { std::cout << I; }\n};\n\nint main() {\n    // Outputs 012\n    boost::tuple<printer<0>, printer<1>, printer<2> > t;\n}\n```\n\n元组的当前 Boost 实现不使用变量模板，不支持右值引用，不支持 C++ 17 结构化绑定，并且不能与 constexpr 一起使用。\n\n# 请参见\n\n*   Boost 的官方文档包含了更多的例子，关于`Boost.Tuple`的性能和能力的信息。可在链接[http://boost.org/libs/tuple](http://www.boost.org/libs/tuple)获得。\n*   [第八章](08.html#CL9V20-712b4ba1126a4c7c89e1d44de61b4bdd)、*元编程、*中的*将所有元组元素转换为字符串*配方展示了元组的一些高级用法。\n\n# 绑定和重新排序函数参数\n\n如果你经常使用标准库并使用`<algorithm>`头，你肯定会写很多功能对象。在 C++ 14 中，您可以为此使用泛型 lambdas。在 C++ 11 中，您只有非泛型 lambdas。在 C++ 标准的早期版本中，可以使用`bind1st`、`bind2nd`、`ptr_fun`、`mem_fun`、`mem_fun_ref`等适配器函数构造函数对象，也可以手工编写(因为适配器函数看起来很吓人)。这里有一些好消息:`Boost.Bind`可以用来代替难看的适配器函数，并且它提供了一种更具可读性的语法。\n\n# 准备好\n\n了解标准库函数和算法会有所帮助。\n\n# 怎么做...\n\n让我们看看一些使用`Boost.Bind`和 C++ 11 lambda 类的例子:\n\n1.  所有示例都需要以下标题:\n\n```cpp\n// Contains boost::bind and placeholders.\n#include <boost/bind.hpp>\n\n// Utility stuff required by samples.\n#include <boost/array.hpp>\n#include <algorithm>\n#include <functional>\n#include <string>\n#include <cassert>\n```\n\n2.  计数值大于 5，如以下代码所示:\n\n```cpp\nvoid sample1() {\n    const boost::array<int, 12> v = {{\n        1, 2, 3, 4, 5, 6, 7, 100, 99, 98, 97, 96\n    }};\n\n    const std::size_t count0 = std::count_if(v.begin(), v.end(),\n        [](int x) { return 5 < x; }\n    );\n    const std::size_t count1 = std::count_if(v.begin(), v.end(), \n        boost::bind(std::less<int>(), 5, _1)\n    ); \n    assert(count0 == count1); \n}\n```\n\n3.  我们可以这样计算空字符串:\n\n```cpp\nvoid sample2() {\n    const boost::array<std::string, 3> v = {{\n        \"We \", \"are\", \" the champions!\"\n    }}; \n\n    const std::size_t count0 = std::count_if(v.begin(), v.end(),\n        [](const std::string& s) { return s.empty(); }\n    );\n    const std::size_t count1 = std::count_if(v.begin(), v.end(), \n        boost::bind(&std::string::empty, _1)\n    ); \n    assert(count0 == count1); \n} \n```\n\n4.  现在，让我们计算长度小于`5`的字符串:\n\n```cpp\nvoid sample3() {\n    const boost::array<std::string, 3> v = {{\n        \"We \", \"are\", \" the champions!\"\n    }};\n\n    const std::size_t count0 = std::count_if(v.begin(), v.end(), \n        [](const std::string& s) {  return s.size() < 5; }\n    ); \n    const std::size_t count1 = std::count_if(v.begin(), v.end(), \n        boost::bind(\n            std::less<std::size_t>(),\n            boost::bind(&std::string::size, _1),\n            5\n        )\n    ); \n    assert(count0 == count1);  \n} \n```\n\n5.  比较字符串:\n\n```cpp\nvoid sample4() {\n    const boost::array<std::string, 3> v = {{\n        \"We \", \"are\", \" the champions!\"\n    }}; \n    std::string s(\n        \"Expensive copy constructor is called when binding\"\n    );\n\n    const std::size_t count0 = std::count_if(v.begin(), v.end(),\n        [&s](const std::string& x) {  return x < s; }\n    ); \n    const std::size_t count1 = std::count_if(v.begin(), v.end(), \n        boost::bind(std::less<std::string>(), _1, s)\n    ); \n    assert(count0 == count1); \n} \n```\n\n# 它是如何工作的...\n\n`boost::bind`函数返回一个函数对象，该函数对象存储绑定值的副本和原始函数对象的副本。当执行对`operator()`的实际调用时，存储的参数与调用时传递的参数一起传递给原始功能对象。\n\n# 还有更多...\n\n看看前面的例子。当我们绑定值时，我们将一个值复制到一个函数对象中。对于某些类，这种操作很昂贵。有没有办法绕过抄袭？\n\n是的，有！`Boost.Ref`图书馆会在这里帮助我们！它包含两个函数，`boost::ref()`和`boost::cref()`，第一个函数允许我们传递一个参数作为引用，第二个函数传递参数作为常量引用。`ref()`和`cref()`函数只是构造一个类型为`reference_wrapper<T>`或`reference_wrapper<const T>`的对象，它可以隐式转换为引用类型。让我们改变最后的例子:\n\n```cpp\n#include <boost/ref.hpp> \n\nvoid sample5() {\n    const boost::array<std::string, 3> v = {{\n        \"We \", \"are\", \" the champions!\"\n    }}; \n    std::string s(\n        \"Expensive copy constructor is NOT called when binding\"\n    );  \n\n    const std::size_t count1 = std::count_if(v.begin(), v.end(), \n        boost::bind(std::less<std::string>(), _1, boost::cref(s))\n    ); \n    // ...\n} \n```\n\n您也可以使用`bind`重新排序、忽略和复制功能参数:\n\n```cpp\nvoid sample6() {\n    const auto twice = boost::bind(std::plus<int>(), _1, _1);\n    assert(twice(2) == 4);\n\n    const auto minus_from_second = boost::bind(std::minus<int>(), _2, _1);\n    assert(minus_from_second(2, 4) == 2);\n\n    const auto sum_second_and_third = boost::bind(\n        std::plus<int>(), _2, _3\n    );\n    assert(sum_second_and_third(10, 20, 30) == 50);\n}\n```\n\n函数`ref`、`cref`和`bind`被 C++ 11 标准接受，并在`std::`命名空间的`<functional>`头中定义。所有这些函数都不动态分配内存，也不使用虚函数。它们返回的对象对于一个好的编译器来说很容易优化。\n\n这些函数的标准库实现可能有额外的优化来减少编译时间，或者只有编译器特定的优化。您可以将标准库版本的`bind`、`ref`、`cref`功能与任何 Boost 库一起使用，甚至可以混合使用 Boost 和标准库版本。\n\n如果你使用的是 C++ 14 编译器，那么就用泛型 lambdas 来代替`std::bind`和`boost::bind`，因为它们不那么晦涩难懂，也更容易理解。与`std::bind`和`boost::bind`不同，C++ 17 lambdas 可以与 constexpr 一起使用。\n\n# 请参见\n\n官方文档包含了更多的例子和 http://boost.org/libs/bind.的高级特性描述\n\n# 获取人类可读的类型名\n\n通常需要在运行时获得可读的类型名:\n\n```cpp\n#include <iostream>\n#include <typeinfo>\n\ntemplate <class T>\nvoid do_something(const T& x) {\n    if (x == 0) {\n        std::cout << \"Error: x == 0\\. T is \" << typeid(T).name() \n        << std::endl;\n    }\n    // ...\n}\n```\n\n然而，前面的例子不是很容易移植。当 RTTI 被禁用时，它不起作用，并且它并不总是产生一个好的人类可读的名字。在一些平台上，早期的代码将只输出`i`或`d`。\n\n如果我们需要一个类型名而不剥离`const`、`volatile`和引用，情况会变得更糟:\n\n```cpp\nvoid sample1() {\n    auto&& x = 42;\n    std::cout << \"x is \"\n              << typeid(decltype(x)).name()\n              << std::endl;\n}\n```\n\n不幸的是，前面的代码在最好的情况下输出`int`，这不是我们所期望的。\n\n# 准备好\n\n这个食谱需要 C++ 的基础知识。\n\n# 怎么做\n\n在第一种情况下，我们需要一个没有限定符的人类可读的类型名。`Boost.TypeIndex`图书馆将帮助我们:\n\n```cpp\n#include <iostream>\n#include <boost/type_index.hpp>\n\ntemplate <class T>\nvoid do_something_again(const T& x) {\n    if (x == 0) {\n        std::cout << \"x == 0\\. T is \" << boost::typeindex::type_id<T>()\n                  << std::endl;\n    }\n    // ...\n}\n```\n\n在第二种情况下，我们需要保留限定符，因此我们需要从同一个库中调用一个稍微不同的函数:\n\n```cpp\n#include <boost/type_index.hpp>\n\nvoid sample2() {\n    auto&& x = 42;\n    std::cout << \"x is \"\n              << boost::typeindex::type_id_with_cvr<decltype(x)>()\n              << std::endl;\n}\n```\n\n# 它是如何工作的...\n\n`Boost.TypeIndex`库对不同的编译器有很多变通方法，并且知道为类型产生一个人类可读名称的最有效的方法。如果您提供一个类型作为模板参数，库保证所有可能的类型相关计算将在编译时执行，即使 RTTI 被禁用，代码也将工作。\n\n`boost::typeindex::type_id_with_cvr`中的`cvr`代表`const`、`volatile`，参考。这确保了该类型不会腐烂。\n\n# 还有更多...\n\n所有`boost::typeindex::type_id*`函数返回`boost::typeindex::type_index`的实例。它离`std::type_index`很近；但是，它另外还有一个获取原始类型名的`raw_name()`方法和一个获取人类可读类型名的`pretty_name()`。\n\n即使在 C++ 17 中，`std::type_index`和`std::type_info`也返回平台特定的类型名称表示，这些表示很难解码或方便使用。\n\n与标准库的`typeid()`不同，`Boost.TypeIndex`中的一些类可以与 constexpr 一起使用。这意味着如果你使用一个特定的`boost::typeindex::ctti_type_index`类，你可以在编译时得到你的类型的文本表示。\n\n用户可以使用`Boost.TypeIndex`库发明他们自己的 RTTI 实现。这对于嵌入式开发人员和需要针对特定类型进行高效 RTTI 调优的应用可能非常有用。\n\n# 请参见\n\n关于高级特性和更多示例的文档可在[http://boost.org/libs/type_index.](http://www.boost.org/libs/type_index)获得\n\n# 使用 C++ 11 移动仿真\n\nC++ 11 标准最大的特点之一是右值引用。这个特性允许我们修改临时对象，从它们那里窃取资源。正如您所猜测的，C++ 03 标准没有右值引用，但是使用`Boost.Move`库，您可以编写一个可移植的代码来模拟它们。\n\n# 准备好\n\n强烈建议您至少熟悉 C++ 11 右值引用的基础知识。\n\n# 怎么做...\n\n1.  假设您有一个包含多个字段的类，其中一些字段是标准的库容器:\n\n```cpp\nnamespace other { \n    class characteristics{}; \n} \n\nstruct person_info {\n    std::string name_; \n    std::string second_name_; \n    other::characteristics characteristic_; \n    // ...\n}; \n```\n\n2.  是时候添加移动赋值和移动构造函数了！请记住，在 C++ 03 标准库中，容器既没有移动运算符，也没有移动构造函数。\n3.  移动分配的正确实现是构造一个对象并将其与`this`交换的相同移动。移动构造函数的正确实现接近默认构造和`swap`。那么，让我们从`swap`会员功能开始:\n\n```cpp\n#include <boost/swap.hpp> \n\nvoid person_info::swap(person_info& rhs) {\n    name_.swap(rhs.name_);\n    second_name_.swap(rhs.second_name_);\n    boost::swap(characteristic_, rhs.characteristic_);\n} \n```\n\n4.  现在，将以下宏放入`private`部分:\n\n```cpp\n    BOOST_COPYABLE_AND_MOVABLE(person_info) \n```\n\n5.  编写一个复制构造函数。\n6.  写一个拷贝赋值，取参数为:`BOOST_COPY_ASSIGN_REF(person_info)`。\n\n7.  写一个`move`构造函数和一个移动赋值，参数为`BOOST_RV_REF(person_info)`:\n\n```cpp\nstruct person_info {\n    // Fields declared here\n    // ...\nprivate:\n    BOOST_COPYABLE_AND_MOVABLE(person_info)\npublic:\n    // For the simplicity of example we will assume that\n    // person_info default constructor and swap are very\n    // fast/cheap to call.\n    person_info();\n\n    person_info(const person_info& p)\n        : name_(p.name_)\n        , second_name_(p.second_name_)\n        , characteristic_(p.characteristic_)\n    {}\n\n    person_info(BOOST_RV_REF(person_info) person) {\n        swap(person);\n    }\n\n    person_info& operator=(BOOST_COPY_ASSIGN_REF(person_info) person) {\n        person_info tmp(person);\n        swap(tmp);\n        return *this;\n    }\n\n    person_info& operator=(BOOST_RV_REF(person_info) person) {\n        person_info tmp(boost::move(person));\n        swap(tmp);\n        return *this;\n    }\n\n    void swap(person_info& rhs);\n};\n```\n\n8.  现在，我们有了`person_info`类的移动分配和移动构造操作符的便携式快速实现。\n\n# 它是如何工作的...\n\n以下是如何使用移动分配的示例:\n\n```cpp\nint main() {\n    person_info vasya;\n    vasya.name_ = \"Vasya\";\n    vasya.second_name_ = \"Snow\"; \n\n    person_info new_vasya(boost::move(vasya)); \n    assert(new_vasya.name_ == \"Vasya\"); \n    assert(new_vasya.second_name_ == \"Snow\"); \n    assert(vasya.name_.empty()); \n    assert(vasya.second_name_.empty()); \n\n    vasya = boost::move(new_vasya); \n    assert(vasya.name_ == \"Vasya\"); \n    assert(vasya.second_name_ == \"Snow\"); \n    assert(new_vasya.name_.empty()); \n    assert(new_vasya.second_name_.empty()); \n}\n```\n\n`Boost.Move`库以非常高效的方式实现。当使用 C++ 11 编译器时，所有用于右值仿真的宏都被扩展为 C++ 11 特定的特性，否则(在 C++ 03 编译器上)，右值被仿真。\n\n# 还有更多...\n\n你注意到`boost::swap`呼叫了吗？这是一个非常有用的实用函数，它首先在变量的名称空间中搜索一个`swap`函数(在我们的例子中，它是名称空间`other::`，如果没有匹配的交换函数，它就使用`std::swap`。\n\n# 请参见\n\n*   更多关于仿真实现的信息可以在 Boost 网站和位于[http://boost.org/libs/move.](http://www.boost.org/libs/move)的`Boost.Move`图书馆的资源中找到\n*   `Boost.Utility`库是包含`boost::swap`的库，它有很多有用的函数和类。参考[http://boost.org/libs/utility](http://www.boost.org/libs/utility)的文件。\n*   [第二章](02.html#36VSO0-712b4ba1126a4c7c89e1d44de61b4bdd)、*资源管理中*通过派生*配方的成员初始化基类。*\n*   制作不可复制类的食谱。\n*   在*制作不可复制但可移动的类*配方中，有更多关于`Boost.Move`的信息，以及一些关于我们如何以便携有效的方式使用容器中的可移动物体的例子。\n\n# 制作不可复制的类\n\n您几乎肯定会遇到某些情况，其中一个类拥有一些资源，由于技术原因，这些资源不能被复制:\n\n```cpp\nclass descriptor_owner { \n    void* descriptor_; \n\npublic: \n    explicit descriptor_owner(const char* params); \n\n    ~descriptor_owner() { \n        system_api_free_descriptor(descriptor_); \n    } \n}; \n```\n\n前面例子中的 C++ 编译器生成了一个复制构造函数和一个赋值操作符，因此`descriptor_owner`类的潜在用户将能够创建以下可怕的东西:\n\n```cpp\nvoid i_am_bad() {\n    descriptor_owner d1(\"O_o\");   \n    descriptor_owner d2(\"^_^\"); \n\n    // Descriptor of d2 was not correctly freed \n    d2 = d1; \n\n    // destructor of d2 will free the descriptor \n    // destructor of d1 will try to free already freed descriptor \n}\n```\n\n# 准备好\n\n这个食谱只需要非常基本的 C++ 知识。\n\n# 怎么做...\n\n为了避免这种情况，发明了`boost::noncopyable`类。如果从中派生自己的类，C++ 编译器就不会生成复制构造函数和赋值运算符:\n\n```cpp\n#include <boost/noncopyable.hpp> \n\nclass descriptor_owner_fixed : private boost::noncopyable { \n    // ... \n```\n\n现在，用户将无法做坏事:\n\n```cpp\nvoid i_am_good() {\n    descriptor_owner_fixed d1(\"O_o\"); \n    descriptor_owner_fixed d2(\"^_^\"); \n\n    // Won't compile \n    d2 = d1; \n\n    // Won't compile either \n    descriptor_owner_fixed d3(d1); \n}\n```\n\n# 它是如何工作的...\n\n一个精致的读者会注意到，我们可以通过以下方式获得完全相同的结果:\n\n*   将`descriptor_owning_fixed`的复制构造函数和赋值运算符设为私有\n*   在没有实际实现的情况下定义它们\n*   使用 C++ 11 语法明确删除它们`= delete;`\n\n是的，你是正确的。根据编译器的能力，`boost::noncopyable`类选择使该类不可复制的最佳方式。\n\n`boost::noncopyable`也是你上课的一个很好的文档。它从不提出诸如“复制构造函数体是否在其他地方定义了？”或者“它是否有非标准的复制构造函数(带有非常量引用的参数)？”\n\n# 请参见\n\n*   制作一个不可复制但可移动的类的方法会给你一些想法，告诉你如何在 C++ 03 中通过移动资源来唯一拥有它\n*   你可以在 http://boost.org/libs/core 的`Boost.Core`图书馆官方文档中找到很多有用的函数和类\n*   [第二章](02.html#36VSO0-712b4ba1126a4c7c89e1d44de61b4bdd)、*资源管理*中*派生*配方成员初始化基类\n*   使用 C++ 11 的*移动仿真*配方\n\n# 创建一个不可复制但可移动的类\n\n现在，想象一下下面的情况:我们有一个无法复制的资源，应该在析构函数中正确释放，我们希望从函数中返回它:\n\n```cpp\ndescriptor_owner construct_descriptor() \n{ \n    return descriptor_owner(\"Construct using this string\"); \n} \n```\n\n实际上，您可以使用`swap`方法来解决这种情况:\n\n```cpp\nvoid construct_descriptor1(descriptor_owner& ret) \n{ \n    descriptor_owner(\"Construct using this string\").swap(ret); \n} \n```\n\n但是，这样的变通方法不允许我们在容器中使用`descriptor_owner`。对了，看起来很可怕！\n\n# 准备好\n\n强烈建议您至少熟悉 C++ 11 右值引用的基础知识。还建议使用 C++ 11 移动仿真读取*配方。*\n\n# 怎么做...\n\n那些使用 C++ 11 的读者，已经知道了只动类(比如`std::unique_ptr`或者`std::thread`)。使用这样的方法，我们可以只动一动`descriptor_owner`类:\n\n```cpp\nclass descriptor_owner1 {\n    void* descriptor_;\n\npublic:\n    descriptor_owner1()\n        : descriptor_(nullptr)\n    {}\n\n    explicit descriptor_owner1(const char* param);\n\n    descriptor_owner1(descriptor_owner1&& param)\n        : descriptor_(param.descriptor_)\n    {\n        param.descriptor_ = nullptr;\n    }\n\n    descriptor_owner1& operator=(descriptor_owner1&& param) {\n        descriptor_owner1 tmp(std::move(param));\n        std::swap(descriptor_, tmp.descriptor_);\n        return *this;\n    }\n\n    void clear() {\n        free(descriptor_);\n        descriptor_ = nullptr;\n    }\n\n    bool empty() const {\n        return !descriptor_;\n    }\n\n    ~descriptor_owner1() {\n        clear();\n    }\n};\n\n// GCC compiles the following in C++ 11 and later modes.\ndescriptor_owner1 construct_descriptor2() {\n    return descriptor_owner1(\"Construct using this string\");\n}\n\nvoid foo_rv() {\n    std::cout << \"C++ 11n\";\n    descriptor_owner1 desc;\n    desc = construct_descriptor2();\n    assert(!desc.empty());\n} \n```\n\n这仅适用于 C++ 11 兼容编译器。那是`Boost.Move`的正确时刻！让我们修改我们的示例，以便它可以在 C++ 03 编译器上使用。\n\n根据文档，要用可移植语法编写可移动但不可复制的类型，我们需要遵循以下简单步骤:\n\n1.  将`BOOST_MOVABLE_BUT_NOT_COPYABLE(classname)`宏放入`private`部分:\n\n```cpp\n#include <boost/move/move.hpp>\n\nclass descriptor_owner_movable {\n    void* descriptor_;\n\n    BOOST_MOVABLE_BUT_NOT_COPYABLE(descriptor_owner_movable\n```\n\n2.  编写移动构造函数和移动赋值，参数为`BOOST_RV_REF(classname)`:\n\n```cpp\npublic:\n    descriptor_owner_movable()\n        : descriptor_(NULL)\n    {}\n\n    explicit descriptor_owner_movable(const char* param)\n        : descriptor_(strdup(param))\n    {}\n\n    descriptor_owner_movable(\n        BOOST_RV_REF(descriptor_owner_movable) param\n    ) BOOST_NOEXCEPT\n        : descriptor_(param.descriptor_)\n    {\n        param.descriptor_ = NULL;\n    }\n\n    descriptor_owner_movable& operator=(\n        BOOST_RV_REF(descriptor_owner_movable) param) BOOST_NOEXCEPT\n    {\n        descriptor_owner_movable tmp(boost::move(param));\n        std::swap(descriptor_, tmp.descriptor_);\n        return *this;\n    }\n\n    // ...\n};\n\ndescriptor_owner_movable construct_descriptor3() {\n    return descriptor_owner_movable(\"Construct using this string\");\n} \n```\n\n# 它是如何工作的...\n\n现在，我们有了一个可移动但不可复制的类，它甚至可以在 C++ 03 编译器和`Boost.Containers`中使用:\n\n```cpp\n#include <boost/container/vector.hpp> \n#include <your_project/descriptor_owner_movable.h>\n\nint main() {\n    // Following code will work on C++ 11 and C++ 03 compilers \n    descriptor_owner_movable movable; \n    movable = construct_descriptor3(); \n    boost::container::vector<descriptor_owner_movable> vec; \n    vec.resize(10); \n    vec.push_back(construct_descriptor3()); \n\n    vec.back() = boost::move(vec.front()); \n}\n```\n\n不幸的是，C++ 03 标准库容器仍然不能使用它(这就是为什么我们在前面的例子中使用了来自`Boost.Containers`的向量)。\n\n# 还有更多...\n\n如果你想在 C++ 03 编译器上使用`Boost.Containers`，但在 C++ 11 编译器上使用标准库容器，你可以做以下简单的技巧。将包含以下内容的头文件添加到项目中:\n\n```cpp\n// your_project/vector.hpp \n// Copyright and other stuff goes here \n\n// include guards \n#ifndef YOUR_PROJECT_VECTOR_HPP \n#define YOUR_PROJECT_VECTOR_HPP \n\n// Contains BOOST_NO_CXX11_RVALUE_REFERENCES macro.\n#include <boost/config.hpp>\n\n#if !defined(BOOST_NO_CXX11_RVALUE_REFERENCES) \n// We do have rvalues \n#include <vector> \n\nnamespace your_project_namespace { \n  using std::vector; \n} // your_project_namespace \n\n#else \n// We do NOT have rvalues \n#include <boost/container/vector.hpp> \n\nnamespace your_project_namespace { \n  using boost::container::vector; \n} // your_project_namespace \n\n#endif // !defined(BOOST_NO_CXX11_RVALUE_REFERENCES) \n#endif // YOUR_PROJECT_VECTOR_HPP \n```\n\n现在，您可以包含`<your_project/vector.hpp>`并使用来自命名空间`your_project_namespace`的向量:\n\n```cpp\nint main() {\n    your_project_namespace::vector<descriptor_owner_movable> v; \n    v.resize(10); \n    v.push_back(construct_descriptor3()); \n    v.back() = boost::move(v.front()); \n}\n```\n\n但是，要小心编译器和标准库实现特定的问题！例如，仅当您用`noexcept`或`BOOST_NOECEPT`标记移动构造函数、析构函数和移动赋值运算符时，此代码才会在 C++ 11 模式下在 GCC 4.7 上编译。\n\n# 请参见\n\n*   [第 10 章](10.html#FKLNA0-712b4ba1126a4c7c89e1d44de61b4bdd)、*收集平台和编译器信息*中的*在 C++ 11* 配方中减少代码大小和提高用户定义类型的性能，提供了关于`noexcept`和`BOOST_NOEXCEPT`的更多信息。\n*   更多关于`Boost.Move`的信息可以在 Boost 的网站[http://boost.org/libs/move.](http://www.boost.org/libs/move)上找到\n\n# 使用 C++ 14 和 C++ 11 算法\n\nC++ 11 在`<algorithm>`头有一堆新的很酷的算法。C++ 14 有更多的算法。如果你坚持 C++ 11 之前的编译器，你必须从头开始写。例如，如果您希望输出 65 到 125 个代码点的字符，您必须在 C++ 11 之前的编译器上编写以下代码:\n\n```cpp\n#include <boost/array.hpp>\n\nboost::array<unsigned char, 60> chars_65_125_pre11() {\n    boost::array<unsigned char, 60> res;\n\n    const unsigned char offset = 65;\n    for (std::size_t i = 0; i < res.size(); ++ i) {\n        res[i] = i + offset;\n    }\n\n    return res;\n}\n```\n\n# 准备好\n\n这个食谱需要 C++ 的基础知识以及`Boost.Array`库的基础知识。\n\n# 怎么做...\n\n`Boost.Algorithm`库有所有新的 C++ 11 和 C++ 14 算法。使用它，您可以用以下方式重写前面的示例:\n\n```cpp\n#include <boost/algorithm/cxx11/iota.hpp>\n#include <boost/array.hpp>\n\nboost::array<unsigned char, 60> chars_65_125() {\n    boost::array<unsigned char, 60> res;\n    boost::algorithm::iota(res.begin(), res.end(), 65);\n    return res;\n}\n```\n\n# 它是如何工作的...\n\n您可能已经知道，`Boost.Algorithm`每个算法都有一个头文件。只需包含头文件并使用所需的函数。\n\n# 还有更多...\n\n拥有一个只实现 C++ 标准算法的库是很无聊的。这不是创新；这不是助推方式！这就是为什么你可以在`Boost.Algorithm`中找到不属于 C++ 的函数。例如，这里有一个将输入转换为十六进制表示的函数:\n\n```cpp\n#include <boost/algorithm/hex.hpp>\n#include <iterator>\n#include <iostream>\n\nvoid to_hex_test1() {\n    const std::string data = \"Hello word\";\n    boost::algorithm::hex(\n        data.begin(), data.end(),\n        std::ostream_iterator<char>(std::cout)\n    );\n}\n```\n\n上述代码输出以下内容:\n\n```cpp\n48656C6C6F20776F7264\n```\n\n更有趣的是，所有的函数都有额外的重载，接受一个范围作为第一个参数，而不是两个迭代器。**系列**是来自**系列 TS** 的概念。具有`.begin()`和`.end()`功能的数组和容器满足范围概念。有了这些知识，前面的例子可以缩短:\n\n```cpp\n#include <boost/algorithm/hex.hpp>\n#include <iterator>\n#include <iostream>\n\nvoid to_hex_test2() {\n    const std::string data = \"Hello word\";\n    boost::algorithm::hex(\n        data,\n        std::ostream_iterator<char>(std::cout)\n    );\n}\n```\n\nC++ 17 将从`Boost.Algorithm`开始有搜索算法。`Boost.Algorithm`库将很快扩展新算法和 C++ 20 特性，如 constexpr 可用算法。关注这个库，因为有一天，它可能会为你正在处理的问题提供一个现成的解决方案。\n\n# 请参见\n\n*   `Boost.Algorithm`的官方文档包含了在[http://boost.org/libs/algorithm](http://boost.org/libs/algorithm)的功能的完整列表和简短描述\n*   在线实验新算法:[http://apolukhin.github.io/Boost-Cookbook](http://apolukhin.github.io/Boost-Cookbook)"
  },
  {
    "path": "docs/boost-cpp-app-dev-cb/02.md",
    "content": "# 二、管理资源\n\n在本章中，我们将涵盖以下主题:\n\n*   管理指向不离开作用域的类的本地指针\n*   跨函数使用的类指针的引用计数\n*   管理指向不超出范围的数组的本地指针\n*   跨函数使用的数组指针的引用计数\n*   将任何功能对象存储在变量中\n*   在变量中传递函数指针\n*   在变量中传递 C++ 11 lambda 函数\n*   指针容器\n*   在示波器出口处进行！\n*   由派生类的成员初始化基类\n\n# 介绍\n\n在本章中，我们将继续讨论 Boost 库引入的数据类型，主要集中在处理指针上。我们将看到如何轻松管理资源，如何使用能够存储任何函数对象、函数和 lambda 表达式的数据类型。读完这一章，你的代码将变得更加可靠，内存泄漏将成为历史。\n\n# 管理指向不离开作用域的类的本地指针\n\n有时，我们需要动态分配内存，并在内存中构造一个类。这就是麻烦的开始。看看下面的代码:\n\n```cpp\nbool foo1() { \n    foo_class* p = new foo_class(\"Some data\"); \n\n    const bool something_else_happened = some_function1(*p);  \n    if (something_else_happened) { \n        delete p; \n        return false; \n    } \n\n    some_function2(p); \n\n    delete p; \n    return true; \n}\n```\n\n乍一看，这段代码是正确的。但是，如果`some_function1()`或`some_function2()`抛出异常怎么办？那样的话，`p`就不会被删除了。让我们用下面的方法修复它:\n\n```cpp\nbool foo2() { \n    foo_class* p = new foo_class(\"Some data\"); \n    try { \n        const bool something_else_happened = some_function1(*p); \n        if (something_else_happened) { \n            delete p; \n            return false; \n        } \n       some_function2(p); \n    } catch (...) { \n        delete p; \n        throw; \n    } \n    delete p; \n    return true; \n}\n```\n\n现在代码是正确的，但是很难看，很难读。我们能做得更好吗？\n\n# 入门指南\n\n需要 C++ 和异常期间代码行为的基本知识。\n\n# 怎么做...\n\n看看`Boost.SmartPtr`图书馆就知道了。有一门`boost::scoped_ptr`课可能会帮到你:\n\n```cpp\n#include <boost/scoped_ptr.hpp> \n\nbool foo3() { \n    const boost::scoped_ptr<foo_class> p(new foo_class(\"Some data\")); \n\n    const bool something_else_happened = some_function1(*p); \n    if (something_else_happened) { \n       return false; \n    } \n    some_function2(p.get()); \n    return true; \n}\n```\n\n现在，资源没有泄露的可能，源代码更加清晰。\n\nIf you have control over `some_function2(foo_class*)`, you may wish to rewrite it to take a reference to `foo_class` instead of a pointer. An interface with references is more intuitive than an interface with pointers unless you have a special agreement in your company that output parameters are taken only by pointer.\n\n顺便说一下，`Boost.Move`还有一个`boost::movelib::unique_ptr`你可以用来代替`boost::scoped_ptr`:\n\n```cpp\n#include <boost/move/make_unique.hpp> \n\nbool foo3_1() { \n    const boost::movelib::unique_ptr<foo_class> p\n        = boost::movelib::make_unique<foo_class>(\"Some data\"); \n\n    const bool something_else_happened = some_function1(*p); \n    if (something_else_happened) { \n       return false; \n    } \n    some_function2(p.get()); \n    return true; \n}\n```\n\n# 它是如何工作的...\n\n`boost::scoped_ptr<T>`和`boost::movelib::unique_ptr`是典型的 **RAII** 类。当抛出异常或变量超出范围时，堆栈被展开，析构函数被调用。在析构函数中，`scoped_ptr<T>`和`unique_ptr<T>`为它们存储的指针调用`delete`。因为这两个类默认都调用`delete`，所以如果基类的析构函数是虚拟的，那么通过指向`base`类的指针来保存`derived`类是安全的:\n\n```cpp\n#include <iostream>\n#include <string>\n\nstruct base {\n    virtual ~base(){}\n};\n\nclass derived: public base {\n    std::string str_;\n\npublic:\n    explicit derived(const char* str)\n        : str_(str)\n    {}\n\n    ~derived() /*override*/ {\n        std::cout << \"str == \" << str_ << '\\n';\n    }\n};\n\nvoid base_and_derived() {\n    const boost::movelib::unique_ptr<base> p1(\n        boost::movelib::make_unique<derived>(\"unique_ptr\")\n    );\n\n    const boost::scoped_ptr<base> p2(\n        new derived(\"scoped_ptr\")\n    );\n}\n```\n\n运行`base_and_derived()`功能将产生以下输出:\n\n```cpp\nstr == scoped_ptr\nstr == unique_ptr\n```\n\nIn C++, destructors for objects are called in the reverse construction order. That's why the destructor of `scoped_ptr` was called before the destructor of `unique_ptr`.\n\n`boost::scoped_ptr<T>`类模板既不可复制也不可移动。`boost::movelib::unique_ptr`类是一个只移动的类，它在 C++ 11 之前的编译器上使用移动仿真。这两个类都存储一个指向它们拥有的资源的指针，并且不要求`T`是一个完整的类型(`T`可以被正向声明)。\n\n某些编译器在删除不完整的类型时不会发出警告，这可能会导致难以检测的错误。幸运的是，在这种情况下，具有特定编译时断言的 Boost 类不是这种情况。这使得`scoped_ptr`和`unique_ptr`非常适合实现**皮条客**成语:\n\n```cpp\n// In header file:\nstruct public_interface {\n    // ...\nprivate:\n    struct impl; // Forward declaration.\n    boost::movelib::unique_ptr<impl> impl_;\n};\n```\n\n# 还有更多...\n\n那些课非常快。编译器将使用`scoped_ptr`和`unique_ptr`的代码优化为机器代码，与手写的手动内存管理代码相比，这不涉及额外的开销。\nC++ 11 有一个`std::unique_ptr<T, D>`类，它唯一拥有资源，并且行为与`boost::movelib::unique_ptr<T, D>`完全一样。\nc++ 标准库没有`boost::scoped_ptr<T>`，但是你可以用`const std::unique_ptr<T>`来代替。唯一不同的是`boost::scoped_ptr<T>`和`const std::unique_ptr<T>`不一样，还是可以叫`reset()`。\n\n# 请参见\n\n*   `Boost.SmartPtr`库的文档包含了许多关于所有智能指针类的例子和其他有用信息。你可以在[http://boost.org/libs/smart_ptr.](http://boost.org/libs/smart_ptr)读到他们\n*   如果你使用`boost::movelib::unique_ptr` [和](http://boost.org/libs/move)的移动模拟，这些`Boost.Move`文档可能会帮助你\n\n# 跨函数使用的类指针的引用计数\n\n假设您有一些包含数据的动态分配的结构，并且您希望在不同的执行线程中处理它。这样做的代码如下:\n\n```cpp\n#include <boost/thread.hpp> \n#include <boost/bind.hpp> \n\nvoid process1(const foo_class* p); \nvoid process2(const foo_class* p); \nvoid process3(const foo_class* p); \n\nvoid foo1() { \n    while (foo_class* p = get_data()) // C way \n    { \n        // There will be too many threads soon, see \n        // recipe 'Parallel execution of different tasks' \n        // for a good way to avoid uncontrolled growth of threads \n        boost::thread(boost::bind(&process1, p)) \n            .detach(); \n        boost::thread(boost::bind(&process2, p)) \n            .detach(); \n        boost::thread(boost::bind(&process3, p)) \n            .detach(); \n\n        // delete p; Oops!!!! \n    } \n}\n```\n\n我们不能在`while`循环结束时释放`p`，因为它仍然可以被运行`process`函数的线程使用。这些`process`函数不能删除`p`，因为它们不知道其他线程不再使用它了。\n\n# 准备好\n\n本食谱使用`Boost.Thread`库，它不是一个只包含标题的库。您的程序必须链接到`boost_thread`、`boost_chrono`和`boost_system`库。在进一步阅读之前，确保你理解线程的概念。参考*参见*部分，了解描述螺纹的配方。\n\n你还需要一些关于`boost::bind`或者`std::bind`的基础知识，基本都差不多。\n\n# 怎么做...\n\n您可能已经猜到，Boost(和 C++ 11)中有一个类可能会帮助您处理这个问题。叫做`boost::shared_ptr`。它可以如下使用:\n\n```cpp\n#include <boost/shared_ptr.hpp> \n\nvoid process_sp1(const boost::shared_ptr<foo_class>& p); \nvoid process_sp2(const boost::shared_ptr<foo_class>& p); \nvoid process_sp3(const boost::shared_ptr<foo_class>& p); \n\nvoid foo2() { \n    typedef boost::shared_ptr<foo_class> ptr_t; \n    ptr_t p; \n    while (p = ptr_t(get_data())) // C way \n    { \n        boost::thread(boost::bind(&process_sp1, p)) \n            .detach(); \n        boost::thread(boost::bind(&process_sp2, p)) \n            .detach(); \n        boost::thread(boost::bind(&process_sp3, p)) \n            .detach(); \n\n        // no need to anything \n    } \n}\n```\n\n这方面的另一个例子如下:\n\n```cpp\n#include <string> \n#include <boost/smart_ptr/make_shared.hpp> \n\nvoid process_str1(boost::shared_ptr<std::string> p); \nvoid process_str2(const boost::shared_ptr<std::string>& p); \n\nvoid foo3() { \n    boost::shared_ptr<std::string> ps = boost::make_shared<std::string>( \n        \"Guess why make_shared<std::string> \" \n        \"is faster than shared_ptr<std::string> \" \n        \"ps(new std::string('this string'))\" \n    ); \n\n    boost::thread(boost::bind(&process_str1, ps)) \n            .detach(); \n    boost::thread(boost::bind(&process_str2, ps)) \n            .detach(); \n}\n```\n\n# 它是如何工作的...\n\n`shared_ptr`类内部有一个原子引用计数器。复制时，引用计数器递增，调用其`destructor`时，引用计数器递减。当引用计数等于零时，`shred_ptr`指向的对象调用`delete`。\n\n现在，让我们来看看`boost::thread (boost::bind(&process_sp1, p))`的情况下发生了什么。函数`process_sp1`以一个参数作为参考，那么当我们退出`while`循环时，为什么不释放它呢？答案很简单。`bind()`返回的功能对象包含一个`shared`指针的副本，这意味着`p`指向的数据在功能对象被销毁之前不会被解除分配。功能对象被复制到线程中，并保持活动状态，直到线程执行。\n\n回到`boost::make_shared`，我们来看看`shared_ptr<std::string> ps(new int(0))`。在这种情况下，我们有两个电话打给`new`:\n\n*   通过`new int(0)`构造一个整数指针时\n*   当构造在堆上分配的`shared_ptr`类内部引用计数器时\n\n使用`make_shared<T>`只能对`new`进行一次呼叫。一个`make_shared<T>`分配一个内存块，并在该内存块中构造一个原子计数器和`T`对象。\n\n# 还有更多...\n\n原子引用计数器保证了线程间`shared_ptr`的正确行为，但是你必须记住原子操作没有非原子操作快。`shared_ptr`在赋值、复制构造和销毁未被移走的`shared_ptr`时触及原子变量。这意味着在 C++ 11 兼容的编译器上，您可以尽可能使用移动构造和移动赋值来减少原子操作的数量。如果不再使用`p`变量，就使用`shared_ptr<T> p1(std::move(p))`。如果不打算修改指向值，建议做成`const`。只需将`const`添加到智能指针的模板参数中，编译器会确保您不会修改内存:\n\n```cpp\nvoid process_cstr1(boost::shared_ptr<const std::string> p);\nvoid process_cstr2(const boost::shared_ptr<const std::string>& p);\n\nvoid foo3_const() {\n    boost::shared_ptr<const std::string> ps\n        = boost::make_shared<const std::string>(\n            \"Some immutable string\"\n        );\n\n    boost::thread(boost::bind(&process_cstr1, ps))\n            .detach();\n    boost::thread(boost::bind(&process_cstr2, ps))\n            .detach();\n\n    // *ps = \"qwe\"; // Compile time error, string is const!\n}\n```\n\nConfused with `const`? Here's a mapping of smart pointer constness to simple pointer constness:\n\n| `shared_ptr<T>` | `T*`  |\n| `shared_ptr<const T>` | `const T*`  |\n| `const shared_ptr<T>` | `T* const` |\n| `const shared_ptr<const T>` | `const T* const` |\n\n`shared_ptr`调用和`make_shared`函数是 C++ 11 的一部分，它们在`std::`命名空间的头`<memory>`中声明。它们具有与 Boost 版本几乎相同的特性。\n\n# 请参见\n\n*   有关`Boost.Thread`和原子操作的更多信息，请参考[第 5 章](05.html#7PRJC0-712b4ba1126a4c7c89e1d44de61b4bdd)**【多线程】*。*\n**   有关`Boost.Bind`的更多信息，请参考[第 1 章](01.html#RL0A0-712b4ba1126a4c7c89e1d44de61b4bdd)*开始编写应用*中的*绑定和重新排序功能参数*配方。*   有关如何将`shared_ptr<U>`转换为`shared_ptr<T>`的信息，请参考[第 3 章](03.html#515F20-712b4ba1126a4c7c89e1d44de61b4bdd)、配方*转换智能指针*。*   `Boost.SmartPtr`库的文档包含了许多关于所有智能指针类的例子和其他有用信息。参考链接[http://boost.org/libs/smart_ptr](http://boost.org/libs/smart_ptr)了解他们。*\n\n *# 管理指向不超出范围的数组的指针\n\n我们已经在*中看到了如何管理指向资源的指针，管理指向不离开*范围的类的指针。但是，当我们处理数组时，我们需要调用`delete[]`而不是简单的`delete`。否则，会出现内存泄漏。看看下面的代码:\n\n```cpp\nvoid may_throw1(char ch); \nvoid may_throw2(const char* buffer); \n\nvoid foo() { \n    // we cannot allocate 10MB of memory on stack, \n    // so we allocate it on heap \n    char* buffer = new char[1024 * 1024 * 10]; \n\n    // Oops. Here comes some code, that may throw.\n    // It was a bad idea to use raw pointer as the memory may leak!!\n    may_throw1(buffer[0]); \n    may_throw2(buffer); \n\n    delete[] buffer; \n}\n```\n\n# 准备好\n\n这个食谱需要 C++ 异常和模板的知识。\n\n# 怎么做...\n\n`Boost.SmartPointer`图书馆不仅有`scoped_ptr<>`班，还有一个`scoped_array<>`班:\n\n```cpp\n#include <boost/scoped_array.hpp> \n\nvoid foo_fixed() { \n    // We allocate array on heap \n    boost::scoped_array<char> buffer(new char[1024 * 1024 * 10]); \n\n    // Here comes some code, that may throw, \n    // but now exception won't cause a memory leak \n    may_throw1(buffer[0]); \n    may_throw2(buffer.get()); \n\n    // destructor of 'buffer' variable will call delete[] \n}\n```\n\n`Boost.Move`库的`boost::movelib::unique_ptr<>`类也可以处理数组。您只需要通过在模板参数的末尾提供`[]`来指示它正在存储一个数组:\n\n```cpp\n#include <boost/move/make_unique.hpp> \n\nvoid foo_fixed2() { \n    // We allocate array on heap \n    const boost::movelib::unique_ptr<char[]> buffer \n        = boost::movelib::make_unique<char[]>(1024 * 1024 * 10); \n\n    // Here comes some code, that may throw, \n    // but now exception won't cause a memory leak \n    may_throw1(buffer[0]); \n    may_throw2(buffer.get()); \n\n    // destructor of 'buffer' variable will call delete[] \n}\n```\n\n# 它是如何工作的...\n\n`scoped_array<>`的工作方式和`scoped_ptr<>`类完全一样，但是在析构函数中调用`delete[]`而不是`delete`。`unique_ptr<T[]>`做同样的事情。\n\n# 还有更多...\n\n`scoped_array<>`级和`scoped_ptr<>`级有相同的保证和设计。它既没有额外的内存分配，也没有虚函数调用。它不能被复制，也不是 C++ 11 的一部分。`std::unique_ptr<T[]>`是 C++ 11 的一部分，具有与`boost::movelib::unique_ptr<T[]>`类相同的保证和性能。\n\nActually, `make_unique<char[]>(1024)` is not the same as `new char[1024]`, because the first one does value initialization and the second one does the default initialization. The equivalent function for default-initialization is `boost::movelib::make_unique_definit`.\n\n请注意，Boost 版本也可以在 C++ 11 之前的编译器上工作，甚至可以在其上模拟右值，使`boost::movelib::unique_ptr`成为只移动的类型。如果您的标准库不提供`std::make_unique`，那么`Boost.SmartPtr`可能会帮助您。它提供了在标题`boost/smart_ptr/make_unique.hpp`中返回一个`std::unique_ptr`的`boost::make_unique`。它还在同一标题中提供`boost::make_unique_noinit`进行默认初始化。C++ 17 没有`make_unique_noinit`函数。\n\nUsing `new` for memory allocation and manual memory management in C++ is a bad habit. Use `make_unique` and `make_shared` functions wherever possible.\n\n# 请参见\n\n*   `Boost.SmartPtr`库的文档包含了许多关于所有智能指针类的例子和其他有用的信息，你可以在[http://boost.org/libs/smart_ptr.](http://boost.org/libs/smart_ptr)上读到它们\n*   如果你想使用`boost::movelib::unique_ptr`的移动模拟，这些`Boost.Move`文档可能会帮助你，在[http://boost.org/libs/move.](http://boost.org/libs/move)T4 阅读它们\n\n# 跨函数使用的数组指针的引用计数\n\n我们继续处理指针，下一个任务是引用计数数组。让我们看一下从流中获取一些数据并在不同线程中处理它的程序。这样做的代码如下:\n\n```cpp\n#include <cstring> \n#include <boost/thread.hpp> \n#include <boost/bind.hpp> \n\nvoid do_process(const char* data, std::size_t size); \n\nvoid do_process_in_background(const char* data, std::size_t size) { \n    // We need to copy data, because we do not know, \n    // when it will be deallocated by the caller.\n    char* data_cpy = new char[size]; \n    std::memcpy(data_cpy, data, size); \n\n    // Starting thread of execution to process data.\n    boost::thread(boost::bind(&do_process, data_cpy, size)) \n            .detach(); \n    boost::thread(boost::bind(&do_process, data_cpy, size)) \n            .detach();\n\n    // Oops!!! We cannot delete[] data_cpy, because \n    // do_process() function may still work with it.\n}\n```\n\n与*引用计数跨函数使用的类的指针*配方中出现的问题相同。\n\n# 准备好\n\n这个食谱使用`Boost.Thread`库，它不是一个只有标题的库，所以你的程序需要链接到`boost_thread`、`boost_chrono`和`boost_system`库。在进一步阅读之前，确保你理解线程的概念。\n\n你还需要一些关于`boost::bind`或`std::bind`的基本知识，这几乎是一样的。\n\n# 怎么做...\n\n有四种解决方案。它们之间的主要区别在于`data_cpy`变量的类型和构造。所有这些解决方案都做了与本食谱开头所述完全相同的事情，但没有内存泄漏。解决方案如下:\n\n*   第一种解决方案适用于在编译时已知数组大小的情况:\n\n```cpp\n#include <boost/shared_ptr.hpp>\n#include <boost/make_shared.hpp>\n\ntemplate <std::size_t Size>\nvoid do_process_shared(const boost::shared_ptr<char[Size]>& data);\n\ntemplate <std::size_t Size>\nvoid do_process_in_background_v1(const char* data) {\n    // Same speed as in 'First solution'.\n    boost::shared_ptr<char[Size]> data_cpy\n        = boost::make_shared<char[Size]>();\n    std::memcpy(data_cpy.get(), data, Size);\n\n    // Starting threads of execution to process data.\n    boost::thread(boost::bind(&do_process_shared<Size>, data_cpy))\n            .detach();\n    boost::thread(boost::bind(&do_process_shared<Size>, data_cpy))\n            .detach();\n\n    // data_cpy destructor will deallocate data when\n    // reference count is zero.\n}\n```\n\n*   由于 Boost 1.53，`shared_ptr`本身可以处理未知界的数组。第二种解决方案:\n\n```cpp\n#include <boost/shared_ptr.hpp>\n#include <boost/make_shared.hpp>\n\nvoid do_process_shared_ptr(\n        const boost::shared_ptr<char[]>& data,\n        std::size_t size);\n\nvoid do_process_in_background_v2(const char* data, std::size_t size) {\n    // Faster than 'First solution'.\n    boost::shared_ptr<char[]> data_cpy = boost::make_shared<char[]>(size);\n    std::memcpy(data_cpy.get(), data, size);\n\n    // Starting threads of execution to process data.\n    boost::thread(boost::bind(&do_process_shared_ptr, data_cpy, size))\n            .detach();\n    boost::thread(boost::bind(&do_process_shared_ptr, data_cpy, size))\n            .detach();\n\n    // data_cpy destructor will deallocate data when\n    // reference count is zero.\n}\n```\n\n*   第三种解决方案:\n\n```cpp\n#include <boost/shared_ptr.hpp>\n\nvoid do_process_shared_ptr2(\n        const boost::shared_ptr<char>& data,\n        std::size_t size);\n\nvoid do_process_in_background_v3(const char* data, std::size_t size) {\n    // Same speed as in 'First solution'.\n    boost::shared_ptr<char> data_cpy(\n                new char[size],\n                boost::checked_array_deleter<char>()\n    );\n    std::memcpy(data_cpy.get(), data, size);\n\n    // Starting threads of execution to process data.\n    boost::thread(boost::bind(&do_process_shared_ptr2, data_cpy, size))\n            .detach();\n    boost::thread(boost::bind(&do_process_shared_ptr2, data_cpy, size))\n            .detach();\n\n    // data_cpy destructor will deallocate data when\n    // reference count is zero.\n}\n```\n\n*   自 Boost 1.65 以来，最后一种解决方案已被弃用，但在古董 Boost 版本中可能有用:\n\n```cpp\n#include <boost/shared_array.hpp>\n\nvoid do_process_shared_array(\n        const boost::shared_array<char>& data,\n        std::size_t size);\n\nvoid do_process_in_background_v4(const char* data, std::size_t size) {\n    // We need to copy data, because we do not know, when it will be\n    // deallocated by the caller.\n    boost::shared_array<char> data_cpy(new char[size]);\n    std::memcpy(data_cpy.get(), data, size);\n\n    // Starting threads of execution to process data.\n    boost::thread(\n        boost::bind(&do_process_shared_array, data_cpy, size)\n    ).detach();\n    boost::thread(\n        boost::bind(&do_process_shared_array, data_cpy, size)\n    ).detach();\n\n    // No need to call delete[] for data_cpy, because\n    // data_cpy destructor will deallocate data when\n    // reference count is zero.\n}\n```\n\n# 它是如何工作的...\n\n在所有示例中，**智能指针**类对引用进行计数，并在引用计数等于零时调用`delete[]`获取指针。第一个和第二个例子很简单。在第三个例子中，我们为一个`shared`指针提供了一个自定义的`deleter`对象。当智能指针决定释放资源时，调用智能指针的`deleter`对象。当智能指针在没有显式`deleter`的情况下被构造时，默认的`deleter`被构造为根据智能指针的模板类型调用`delete`或`delete[]`。\n\n# 还有更多...\n\n第四个解决方案是最保守的，因为在 Boost 1.53 之前，第二个解决方案的功能没有在`shared_ptr`中实现。第一个和第二个解决方案是最快的，因为它们只使用一个内存分配调用。第三种解决方案可以用于较旧版本的 Boost 和 C++ 11 标准库的`std::shared_ptr<>`(只是别忘了将`boost::checked_array_deleter<T>()`改为`std::default_delete<T[]>()`)。\n\nActually, `boost::make_shared<char[]>(size)` is not the same as `new char[size]`, because it involves value-initialization of all elements. The equivalent function for default-initialization is `boost::make_shared_noinit`.\n\n当心！`std::shared_ptr`的 C++ 11 和 C++ 14 版本不能用数组！只是既然 C++ 17 `std::shared_ptr<T[]>`必须正常工作。如果您计划编写可移植代码，请考虑使用`boost::shared_ptr`、`boost::shared_array`，或者明确地将一个`deleter`传递给`std::shared_ptr`。\n\n`boost::shared_ptr<T[]>`, `boost::shared_array`, and C++ 17 `std::shared_ptr<T[]>` have `operator[](std::size_t index)` that allows you to access elements of shared array by index. `boost::shared_ptr<T>` and `std::shared_ptr<T>` with custom `deleter` have no `operator[]`, which makes them less useful.\n\n# 请参见\n\n`Boost.SmartPtr`库的文档包含了许多关于所有智能指针类的例子和其他有用的信息。你可以在[http://boost.org/libs/smart_ptr](http://boost.org/libs/smart_ptr)看到。\n\n# 将任何功能对象存储在变量中\n\n当您开发一个在头文件中声明了 API 并在源文件中实现的库时，请考虑这种情况。该库应具有接受任何功能对象的功能。看看下面的代码:\n\n```cpp\n// making a typedef for function pointer accepting int \n// and returning nothing \ntypedef void (*func_t)(int); \n\n// Function that accepts pointer to function and \n// calls accepted function for each integer that it has.\n// It cannot work with functional objects :( \nvoid process_integers(func_t f); \n\n// Functional object \nclass int_processor { \n   const int min_; \n   const int max_; \n   bool& triggered_; \n\npublic: \n    int_processor(int min, int max, bool& triggered) \n        : min_(min) \n        , max_(max) \n        , triggered_(triggered) \n    {} \n\n    void operator()(int i) const { \n        if (i < min_ || i > max_) { \n            triggered_ = true; \n        } \n    } \n};\n```\n\n如何改变`process_integers`功能接受任何功能对象？\n\n# 准备好\n\n建议在开始使用此配方之前，阅读[第 1 章](01.html#RL0A0-712b4ba1126a4c7c89e1d44de61b4bdd)、*中的【容器/变量】配方中存储的任何值*，开始编写您的应用*。*\n\n# 怎么做...\n\n有一个解决方案，叫做`Boost.Function`库。它允许您存储任何函数、成员函数或函数对象，如果它的签名与模板参数中描述的匹配:\n\n```cpp\n#include <boost/function.hpp> \n\ntypedef boost::function<void(int)> fobject_t; \n\n// Now this function may accept functional objects \nvoid process_(const fobject_t& f); \n\nint main() { \n    bool is_triggered = false; \n    int_processor fo(0, 200, is_triggered); \n    process_integers(fo); \n    assert(is_triggered); \n}\n```\n\n# 它是如何工作的...\n\n`fobject_t`对象本身存储功能对象，并删除它们的确切类型。将`boost::function`用于有状态对象是安全的:\n\n```cpp\nbool g_is_triggered = false; \nvoid set_functional_object(fobject_t& f) {\n    // Local variable\n    int_processor fo( 100, 200, g_is_triggered); \n\n    f = fo;\n    // now 'f' holds a copy of 'fo'\n\n    // 'fo' leavs scope and will be destroyed,\n    // but it's OK to use 'f' in outer scope.\n}\n```\n\n`boost::function`是否回忆起`boost::any`班？这是因为它使用相同的技术**类型擦除**来存储任何功能对象。\n\n# 还有更多...\n\n`boost::function`类有一个默认的构造函数，并且有一个空状态。检查空/默认构造状态可以这样完成:\n\n```cpp\nvoid foo(const fobject_t& f) { \n    // boost::function is convertible to bool \n    if (f) { \n        // we have value in 'f' \n        // ... \n    } else { \n        // 'f' is empty \n        // ... \n    } \n}\n```\n\n`Boost.Function`库有大量疯狂的优化。它可以存储小的功能对象，而无需额外的内存分配，并且具有优化的移动分配操作符。它被接受为 C++ 11 标准库的一部分，并在`std::`命名空间的`<functional>`头中定义。\n\n`boost::function`对存储在其中的对象使用 RTTI。如果禁用 RTTI，该库将继续工作，但会显著增加编译后的二进制文件的大小。\n\n# 请参见\n\n*   `Boost.Function`的官方文档包含更多示例、性能度量和类参考文档。参考链接[http://boost.org/libs/function](http://boost.org/libs/function)了解。\n*   变量配方中的*传递函数指针。*\n*   在变量配方中传递 c++ 11λ函数。\n\n# 在变量中传递函数指针\n\n我们继续前面的例子，现在我们想在我们的`process_integers()`方法中传递一个指向函数的指针。我们应该仅仅为函数指针添加一个重载，还是有更好的方法？\n\n# 准备好\n\n这个食谱延续了上一个食谱。你必须先阅读之前的食谱。\n\n# 怎么做...\n\n不需要做任何事情，因为`boost::function<>`也可以从函数指针中构造:\n\n```cpp\nvoid my_ints_function(int i); \n\nint main() { \n    process_integers(&my_ints_function); \n}\n```\n\n# 它是如何工作的...\n\n指向`my_ints_function`的指针将存储在`boost::function`类中，对`boost::function`的调用将被转发到存储的指针。\n\n# 还有更多...\n\n`Boost.Function`库为指向函数的指针提供了良好的性能，不会在堆上分配内存。标准库`std::function`在存储函数指针方面也很有效。从 Boost 1.58 开始，`Boost.Function`库可以存储带有右值引用的调用签名的函数和函数对象:\n\n```cpp\nboost::function<int(std::string&&)> f = &something;\nf(std::string(\"Hello\")); // Works\n```\n\n# 请参见\n\n*   `Boost.Function`的官方文档包含更多示例、性能度量和类参考文档。跟随[http://boost.org/libs/function](http://boost.org/libs/function)阅读相关内容。\n*   在变量配方中传递 c++ 11λ函数。\n\n# 在变量中传递 C++ 11 lambda 函数\n\n我们继续前面的例子，现在我们想在我们的`process_integers()`方法中使用一个 lambda 函数。\n\n# 准备好\n\n这个食谱是前两个系列的延续。你必须先读它们。您还需要一个兼容 C++ 11 的编译器，或者至少一个支持 C++ 11 lambda 的编译器。\n\n# 怎么做...\n\n不需要做任何事情，因为`boost::function<>`也可以用于任何难度的λ函数:\n\n```cpp\n#include <deque>\n//#include \"your_project/process_integers.h\"\n\nvoid sample() {\n    // lambda function with no parameters that does nothing \n    process_integers([](int /*i*/){}); \n\n    // lambda function that stores a reference \n    std::deque<int> ints; \n    process_integers([&ints](int i){ \n        ints.push_back(i); \n    }); \n\n    // lambda function that modifies its content \n    std::size_t match_count = 0; \n    process_integers([ints, &match_count](int i) mutable { \n        if (ints.front() == i) { \n           ++ match_count; \n        } \n        ints.pop_front(); \n    });\n}\n```\n\n# 还有更多...\n\n`Boost.Functional`中 lambda 函数存储的性能与其他情况相同。虽然由 lambda 表达式生成的函数对象足够小，可以放入`boost::function`的实例中，但不会执行动态内存分配。调用存储在`boost::function`中的对象的速度接近指针调用函数的速度。复制`boost::function`仅当初始`boost::function`有一个存储对象不适合它而没有分配时才分配堆内存。移动实例不会分配和释放内存。\n\n请记住`boost::function`意味着编译器的优化障碍。这意味着:\n\n```cpp\n    std::for_each(v.begin(), v.end(), [](int& v) { v += 10; });\n```\n\n通常比以下更能被编译器优化:\n\n```cpp\n    const boost::function<void(int&)> f0(\n        [](int& v) { v += 10; }\n    ); \n    std::for_each(v.begin(), v.end(), f0);\n```\n\n这就是为什么在实际不需要使用`Boost.Function`时，要尽量避免使用的原因。在某些情况下，C++ 11 `auto`关键字反而很方便:\n\n```cpp\n    const auto f1 = [](int& v) { v += 10; }; \n    std::for_each(v.begin(), v.end(), f1);\n```\n\n# 请参见\n\n关于性能和`Boost.Function`的更多信息可以在位于[http://www.boost.org/libs/function](http://www.boost.org/libs/function)的官方文档页面上找到。\n\n# 指针容器\n\n当我们需要在容器中存储指针时，就会出现这种情况。例如:将多态数据存储在容器中，强制容器中数据的快速复制，以及对容器中数据操作的严格异常要求。在这种情况下，C++ 程序员有以下选择:\n\n*   将指针存储在容器中，并使用`delete`处理它们的析构:\n\n```cpp\n#include <set>\n#include <algorithm>\n#include <cassert>\n\ntemplate <class T>\nstruct ptr_cmp {\n    template <class T1>\n    bool operator()(const T1& v1, const T1& v2) const {\n        return operator ()(*v1, *v2);\n    }\n\n    bool operator()(const T& v1, const T& v2) const {\n        return std::less<T>()(v1, v2);\n    }\n};\n\nvoid example1() {\n    std::set<int*, ptr_cmp<int> > s;\n    s.insert(new int(1));\n    s.insert(new int(0));\n\n    // ...\n    assert(**s.begin() == 0);\n    // ...\n\n    // Oops! Any exception in the above code leads to\n    // memory leak.\n\n    // Deallocating resources.\n    std::for_each(s.begin(), s.end(), [](int* p) { delete p; });\n}\n```\n\n这种方法容易出错，需要大量的编写工作\n\n*   在容器中存储 C++ 11 智能指针:\n\n```cpp\n#include <memory>\n#include <set>\n\nvoid example2_cpp11() {\n    typedef std::unique_ptr<int> int_uptr_t;\n    std::set<int_uptr_t, ptr_cmp<int> > s;\n    s.insert(int_uptr_t(new int(1)));\n    s.insert(int_uptr_t(new int(0)));\n\n    // ...\n    assert(**s.begin() == 0);\n    // ...\n\n    // Resources will be deallocated by unique_ptr<>.\n}\n```\n\n这个解决方案很好，但是不能用在 C++ 03 中，还需要写一个比较器函数对象。\n\nC++ 14 has a `std::make_unique` function for construction of `std::uniue_ptr`s. Using it instead of `new` is a good coding style!\n\n*   在容器中使用`Boost.SmartPtr`:\n\n```cpp\n#include <boost/shared_ptr.hpp>\n#include <boost/make_shared.hpp>\n\nvoid example3() {\n    typedef boost::shared_ptr<int> int_sptr_t;\n    std::set<int_sptr_t, ptr_cmp<int> > s;\n    s.insert(boost::make_shared<int>(1));\n    s.insert(boost::make_shared<int>(0));\n\n    // ...\n    assert(**s.begin() == 0);\n    // ...\n\n    // Resources will be deallocated by shared_ptr<>.\n}\n```\n\n这个解决方案是可移植的，但是它增加了性能损失(原子计数器需要额外的内存，并且它的递增/递减不如非原子操作快)，并且您仍然需要编写比较器。\n\n# 准备好\n\n为了更好地理解这个配方，需要标准库容器的知识。\n\n# 怎么做...\n\n`Boost.PointerContainer`库提供了一个很好的便携式解决方案:\n\n```cpp\n#include <boost/ptr_container/ptr_set.hpp> \n\nvoid correct_impl() { \n    boost::ptr_set<int> s; \n    s.insert(new int(1)); \n    s.insert(new int(0)); \n\n    // ... \n    assert(*s.begin() == 0); \n    // ... \n\n    // Resources will be deallocated by container itself.\n}\n```\n\n# 它是如何工作的...\n\n`Boost.PointerContainer`库有类`ptr_array`、`ptr_vector`、`ptr_set`、`ptr_multimap`等。这些类根据需要释放指针，并简化对指针所指向的数据的访问(不需要在`assert(*s.begin() == 0);`中进行额外的取消引用)。\n\n# 还有更多...\n\n当我们想要克隆一些数据时，需要在被克隆对象的命名空间中定义一个独立的函数`T*new_clone(const T& r)`。此外，如果包含头文件`<boost/ptr_container/clone_allocator.hpp>`，可以使用默认的`T* new_clone(const T& r)`实现，如下面的代码所示:\n\n```cpp\n#include <boost/ptr_container/clone_allocator.hpp>\n#include <boost/ptr_container/ptr_vector.hpp>\n#include <cassert>\n\nvoid theres_more_example() {\n    // Creating vector of 10 elements with values 100\n    boost::ptr_vector<int> v;\n    int value = 100;\n    v.resize(10, &value); // Beware! No ownership of pointer!\n\n    assert(v.size() == 10);\n    assert(v.back() == 100);\n}\n```\n\nC++ 标准库没有指针容器，但是使用`std::unique_ptr`的容器可以实现同样的功能。对了，从 Boost 1.58 开始，有一个`boost::movelib::unique_ptr`类可以在 C++ 03 中使用。您可以将其与来自`Boost.Container`库的容器混合，以获得存储指针的 C++ 11 功能:\n\n```cpp\n#include <boost/container/set.hpp>\n#include <boost/move/make_unique.hpp>\n#include <cassert>\n\nvoid example2_cpp03() { \n    typedef boost::movelib::unique_ptr<int> int_uptr_t; \n    boost::container::set<int_uptr_t, ptr_cmp<int> > s; \n    s.insert(boost::movelib::make_unique<int>(1)); \n    s.insert(boost::movelib::make_unique<int>(0)); \n    // ... \n    assert(**s.begin() == 0); \n}\n```\n\nNot all the developers know the Boost libraries well. It is more developer-friendly to use functions and classes that have C++ standard library alternatives, as the developers usually are more aware of the standard library features. So if there's no big difference for you, use `Boost.Container` with `boost::movelib::unique_ptr`.\n\n# 请参见\n\n*   官方文档包含每堂课的详细参考，点击链接[http://boost.org/libs/ptr_container](http://boost.org/libs/ptr_container)阅读。\n*   本章的前四个食谱给你一些关于智能指针用法的例子。\n*   [第 9 章](09.html#E4VR60-712b4ba1126a4c7c89e1d44de61b4bdd) *、容器*中的多个食谱描述了`Boost.Container`库的特点。看看那一章，寻找酷的、有用的、快速的容器。\n\n# 在示波器出口处进行！\n\n如果你在处理语言，如 Java、C#或 Delphi，你显然是在使用`try {} finally{}`结构。让我简单地给你描述一下这些语言结构是做什么的。\n\n当程序通过返回或异常离开当前范围时，执行`finally`块中的代码。这种机制用于替代 RAII 模式:\n\n```cpp\n// Some pseudo code (suspiciously similar to Java code) \ntry { \n    FileWriter f = new FileWriter(\"example_file.txt\"); \n    // Some code that may throw or return \n    // ... \n} finally { \n    // Whatever happened in scope, this code will be executed \n    // and file will be correctly closed \n    if (f != null) { \n        f.close() \n    } \n}\n```\n\n在 C++ 中有没有办法做到这样的事情？\n\n# 准备好\n\n这个食谱需要基本的 C++ 知识。了解抛出异常期间的代码行为将受到赞赏。\n\n# 怎么做...\n\nC++ 使用 RAII 模式而不是`try {} finally{}`构造。`Boost.ScopeExit`库被设计成允许用户在函数体中定义 RAII 包装:\n\n```cpp\n#include <boost/scope_exit.hpp> \n#include <cstdlib> \n#include <cstdio> \n#include <cassert> \n\nint main() { \n    std::FILE* f = std::fopen(\"example_file.txt\", \"w\"); \n    assert(f); \n\n    BOOST_SCOPE_EXIT(f) { \n    // Whatever happened in outer scope, this code will be executed \n    // and file will be correctly closed. \n        std::fclose(f); \n    } BOOST_SCOPE_EXIT_END \n\n    // Some code that may throw or return. \n    // ... \n}\n```\n\n# 它是如何工作的...\n\n`f`通过`BOOST_SCOPE_EXIT(f)`传递给数值。当程序离开执行范围时，`BOOST_SCOPE_EXIT(f) {`和`} BOOST_SCOPE_EXIT_END`之间的代码被执行。如果我们希望通过引用传递该值，请使用`BOOST_SCOPE_EXIT`宏中的`&`符号。如果我们希望传递多个值，只需用逗号分隔它们。\n\nPassing references to a pointer does not work well on some compilers. The `BOOST_SCOPE_EXIT(&f)` macro cannot be compiled there, which is why we do not capture it by reference in the example.\n\n# 还有更多...\n\n为了在成员函数中捕捉这一点，我们将使用一个特殊的符号`this_`:\n\n```cpp\nclass theres_more_example { \npublic: \n    void close(std::FILE*); \n\n    void theres_more_example_func() { \n        std::FILE* f = 0; \n        BOOST_SCOPE_EXIT(f, this_) { // Capturing `this` as 'this_' \n            this_->close(f); \n        } BOOST_SCOPE_EXIT_END \n    } \n};\n```\n\n`Boost.ScopeExit`库不在堆上分配额外的内存，也不使用虚函数。使用默认语法，不要定义`BOOST_SCOPE_EXIT_CONFIG_USE_LAMBDAS`，因为否则将使用`boost::function`实现作用域退出，这可能会分配额外的内存并暗示优化障碍。通过指定自定义的`deleter`，使用`boost::movelib::unique_ptr`或`std::unique_ptr`可以获得接近`BOOST_SCOPE_EXIT`的结果:\n\n```cpp\n#include <boost/move/unique_ptr.hpp>\n#include <cstdio>\n\nvoid unique_ptr_example() {\n    boost::movelib::unique_ptr<std::FILE, int(*)(std::FILE*)> f(\n        std::fopen(\"example_file.txt\", \"w\"), // open file\n        &std::fclose  // specific deleter\n    );\n    // ...\n}\n```\n\nIf you write two or more similar bodies for `BOOST_SCOPE_EXIT`, then it's time to think about some refactoring and moving the code to a fully functional RAII class.\n\n# 请参见\n\n官方文档包含更多的例子和用例。你可以在[http://boost.org/libs/scope_exit.](http://boost.org/libs/scope_exit)看到\n\n# 由派生类的成员初始化基类\n\n让我们看看下面的例子。我们有一些基类，它有虚函数，必须参照`std::ostream`对象进行初始化:\n\n```cpp\n#include <boost/noncopyable.hpp> \n#include <sstream> \n\nclass tasks_processor: boost::noncopyable { \n    std::ostream& log_; \n\nprotected: \n    virtual void do_process() = 0; \n\npublic: \n    explicit tasks_processor(std::ostream& log) \n        : log_(log) \n    {} \n\n    void process() { \n        log_ << \"Starting data processing\"; \n        do_process(); \n    } \n};\n```\n\n我们还有一个派生类，它有一个`std::ostream`对象并实现了`do_process()`函数:\n\n```cpp\nclass fake_tasks_processor: public tasks_processor { \n    std::ostringstream logger_; \n\n    virtual void do_process() { \n        logger_ << \"Fake processor processed!\"; \n    } \n\npublic: \n    fake_tasks_processor() \n        : tasks_processor(logger_) // Oops! logger_ does not exist here \n        , logger_() \n    {} \n};\n```\n\n这在编程中并不是一个很常见的情况，但是当这样的错误发生时，想要绕过它并不总是那么简单。有些人试图通过改变`logger_`的顺序和基类型初始化来绕过它:\n\n```cpp\n    fake_tasks_processor() \n        : logger_() // Oops! It is still constructed AFTER tasks_processor. \n        , tasks_processor(logger_) \n    {}\n```\n\n它不会像预期的那样工作，因为直接基类是在非静态数据成员之前初始化的，不管成员初始值设定项的顺序如何。\n\n# 入门指南\n\n这个食谱需要 C++ 的基础知识。\n\n# 怎么做...\n\n`Boost.Utility`库为这种情况提供了快速解决方案。解决方案称为`boost::base_from_member`模板。要使用它，您需要执行以下步骤:\n\n1.  包括`base_from_member.hpp`标题:\n\n```cpp\n#include <boost/utility/base_from_member.hpp>\n```\n\n2.  从`boost::base_from_member<T>`派生你的类，其中`T`是必须在基类之前初始化的类型(注意基类的顺序；`boost::base_from_member<T>`必须放在使用`T`的班级前面):\n\n```cpp\nclass fake_tasks_processor_fixed\n    : boost::base_from_member<std::ostringstream>\n    , public tasks_processor\n```\n\n3.  正确地，按如下方式编写构造函数:\n\n```cpp\n{\n    typedef boost::base_from_member<std::ostringstream> logger_t;\n\n    virtual void do_process() {\n        logger_t::member << \"Fake processor processed!\";\n    }\n\npublic:\n    fake_tasks_processor_fixed()\n        : logger_t()\n        , tasks_processor(logger_t::member)\n    {}\n};\n```\n\n# 它是如何工作的...\n\n直接基类在非静态数据成员之前初始化，并且按照它们出现在基说明符列表中的声明顺序初始化。如果我们需要用*某物*初始化基类`B`，我们需要使那个*某物*成为在`B`之前声明的基类`A`的一部分。换句话说，`boost::base_from_member`只是一个简单的类，它将其模板参数保存为非静态数据成员:\n\n```cpp\ntemplate < typename MemberType, int UniqueID = 0 >\nclass base_from_member {\nprotected:\n    MemberType  member;\n    //      Constructors go there...\n};\n```\n\n# 还有更多...\n\n如您所见，`base_from_member`有一个整数作为第二个模板参数。这是针对我们需要多个相同类型的`base_from_member`类的情况:\n\n```cpp\nclass fake_tasks_processor2 \n    : boost::base_from_member<std::ostringstream, 0> \n    , boost::base_from_member<std::ostringstream, 1> \n    , public tasks_processor \n{ \n    typedef boost::base_from_member<std::ostringstream, 0> logger0_t;\n    typedef boost::base_from_member<std::ostringstream, 1> logger1_t;\n\n    virtual void do_process() { \n        logger0_t::member << \"0: Fake processor2 processed!\"; \n        logger1_t::member << \"1: Fake processor2 processed!\"; \n    } \n\npublic: \n    fake_tasks_processor2() \n        : logger0_t() \n        , logger1_t() \n        , tasks_processor(logger0_t::member) \n    {} \n};\n```\n\n`boost::base_from_member`类既不应用额外的动态内存分配，也没有虚拟函数。如果您的编译器支持的话，当前的实现确实支持**完美转发**和**变量模板**。\nC++ 标准库没有`base_from_member`。\n\n# 请参见\n\n*   `Boost.Utility`库包含很多有用的类和函数；获取更多信息的文档位于[http://boost.org/libs/utility](http://boost.org/libs/utility)\n*   在[第 1 章](01.html#RL0A0-712b4ba1126a4c7c89e1d44de61b4bdd)*开始编写你的应用*中的*制作非可复制类*食谱包含了更多来自`Boost.Utility`的类的例子\n*   此外，[第 1 章](01.html#RL0A0-712b4ba1126a4c7c89e1d44de61b4bdd)、*开始编写应用*中的*使用 C++ 11 移动仿真*方法包含更多来自`Boost.Utility`的类示例*"
  },
  {
    "path": "docs/boost-cpp-app-dev-cb/03.md",
    "content": "# 三、类型转换\n\n在本章中，我们将介绍:\n\n*   将字符串转换为数字\n*   将数字转换为字符串\n*   将数字转换成数字\n*   将用户定义的类型转换为字符串或从字符串转换\n*   转换智能指针\n*   转换多态对象\n*   解析简单输入\n*   解析复杂输入\n\n# 介绍\n\n现在，我们知道了一些基本的 Boost 类型，是时候了解数据转换功能了。在本章中，我们将看到如何将字符串、数字、指针和用户定义的类型相互转换，如何安全地转换多态类型，以及如何在 C++ 源文件中编写大小解析器。\n\n# 将字符串转换为数字\n\n用 C++ 把字符串转换成数字会让很多人因为效率低和用户不友好而沮丧。查看字符串`100`如何转换为`int`:\n\n```cpp\n#include <sstream> \n\nvoid sample1() {\n    std::istringstream iss(\"100\");\n    int i;\n    iss >> i;\n\n    // ...\n}\n```\n\n最好不要想，从早期的转换过程中，有多少不必要的操作、虚函数调用、原子操作和内存分配发生了。顺便说一下，我们不再需要`iss`变量了，但是它会一直存在到作用域结束。\n\nc 方法也好不到哪里去:\n\n```cpp\n#include <cstdlib> \n\nvoid sample2() {\n    char * end;\n    const int i = std::strtol (\"100\", &end, 10);\n\n    // ...\n}\n```\n\n是把整数值转换成`int`还是停在中间某处？为了理解这一点，我们必须检查`end`变量的内容。之后，我们将有一个无用的`end`变量挡道，直到范围结束。我们想要一辆`int`，但是`strtol`返回`long int`。换算值是否符合`int`？\n\n# 准备好\n\n这个食谱只需要 C++ 和标准库的基础知识。\n\n# 怎么做...\n\nBoost 中有一个库，可以帮助你应对字符串到数字转换的令人沮丧的困难。它被称为`Boost.LexicalCast`，由一个`boost::bad_lexical_cast`异常类和几个`boost::lexical_cast`和`boost::conversion::try_lexical_convert`函数组成:\n\n```cpp\n#include <boost/lexical_cast.hpp> \n\nvoid sample3() {\n    const int i = boost::lexical_cast<int>(\"100\");\n    // ...\n}\n```\n\n它甚至可以用于非零结尾的字符串:\n\n```cpp\n#include <boost/lexical_cast.hpp>\n\nvoid sample4() {\n    char chars[] = {'x', '1', '0', '0', 'y' }; \n    const int i = boost::lexical_cast<int>(chars + 1, 3); \n    assert(i == 100); \n}\n```\n\n# 它是如何工作的...\n\n`boost::lexical_cast`函数接受字符串作为输入，并将其转换为三角括号中指定的类型。`boost::lexical_cast`功能甚至会为您检查边界:\n\n```cpp\n#include <boost/lexical_cast.hpp>\n#include <iostream>\n\nvoid sample5() {\n    try {\n        // short usually may not store values greater than 32767\n        const short s = boost::lexical_cast<short>(\"1000000\");\n        assert(false); // Must not reach this line.\n    } catch (const boost::bad_lexical_cast& e) {\n        std::cout << e.what() << '\\n';\n    }\n}\n```\n\n前面的代码输出:\n\n```cpp\nbad lexical cast: source type value could not be interpreted as target.\n```\n\n它还检查输入的语法是否正确，如果输入错误，将引发异常:\n\n```cpp\n#include <boost/lexical_cast.hpp>\n#include <iostream>\n\nvoid sample6() {\n    try {\n        const int i = boost::lexical_cast<int>(\"This is not a number!\");\n        assert(false); // Must not reach this line.\n    } catch (const boost::bad_lexical_cast& /*e*/) {}\n}\n```\n\n从 Boost 1.56 开始，有一个`boost::conversion::try_lexical_convert`函数，通过返回代码报告错误。在经常出现错误输入的性能关键的地方，它可能很有用:\n\n```cpp\n#include <boost/lexical_cast.hpp>\n#include <cassert>\n\nvoid sample7() {\n    int i = 0;\n    const bool ok = boost::conversion::try_lexical_convert(\"Bad stuff\", i);\n    assert(!ok);\n}\n```\n\n# 还有更多...\n\n`lexical_cast`和所有的`std::stringstream`类一样，使用`std::locale`并可以转换本地化的数字，但是对于 **C 语言环境**和没有数字分组的语言环境也有一套令人印象深刻的优化:\n\n```cpp\n#include <boost/lexical_cast.hpp>\n#include <locale> \n\nvoid sample8() {\n    try {\n        std::locale::global(std::locale(\"ru_RU.UTF8\"));\n        // In Russia coma sign is used as a decimal separator.\n        float f = boost::lexical_cast<float>(\"1,0\");\n        assert(f < 1.01 && f > 0.99);\n        std::locale::global(std::locale::classic()); // Restoring C locale\n    } catch (const std::runtime_error&) { /* locale is not supported */ }\n} \n```\n\nC++ 标准库没有`lexical_cast`，但是由于 C++ 17 有`std::from_chars`函数，可以用来创建速度惊人的通用转换器。请注意，这些转换器根本不使用区域设置，因此它们的功能略有不同。`std::from_chars`函数被设计成不分配内存，不抛出异常，并且没有原子或其他一些繁重的操作。\n\n# 请参见\n\n*   有关`boost::lexical_cast`表演的信息，请参考*将数字转换为字符串*配方。\n*   `Boost.LexicalCast`的官方文档包含一些示例、绩效衡量标准和常见问题的答案。在[http://boost.org/libs/lexical_cast](http://boost.org/libs/lexical_cast)有售。\n\n# 将数字转换为字符串\n\n在本食谱中，我们将继续讨论词汇转换，但现在我们将使用`Boost.LexicalCast`将数字转换为字符串。像往常一样，`boost::lexical_cast`将提供一种非常简单的方法来转换数据。\n\n# 准备好\n\n这个食谱只需要 C++ 的基础知识和一个标准库。\n\n# 怎么做...\n\n让我们使用`boost::lexical_cast`将整数`100`转换为`std::string`:\n\n```cpp\n#include <cassert>\n#include <boost/lexical_cast.hpp> \n\nvoid lexical_cast_example() {\n    const std::string s = boost::lexical_cast<std::string>(100);\n    assert(s == \"100\");\n}\n```\n\n将其与传统的 C++ 转换方法进行比较:\n\n```cpp\n#include <cassert>\n#include <sstream> \n\nvoid cpp_convert_example() {\n    std::stringstream ss;  // Slow/heavy default constructor.\n    ss << 100;\n    std::string s;\n    ss >> s;\n\n    // Variable 'ss' will dangle all the way, till the end\n    // of scope. Multiple virtual methods and heavy \n    // operations were called during the conversion.\n    assert(s == \"100\");\n}\n```\n\n并对照 C 转换方法:\n\n```cpp\n#include <cassert>\n#include <cstdlib> \n\nvoid c_convert_example() {\n     char buffer[100];\n     std::sprintf(buffer, \"%i\", 100);\n\n     // You will need an unsigned long long int type to \n     // count how many times errors were made in 'printf' \n     // like functions all around the world. 'printf' \n     // functions are a constant security threat! \n\n     // But wait, we still need to construct a std::string.\n     const std::string s = buffer;\n     // Now we have a 'buffer' variable that is not used.\n\n     assert(s == \"100\");\n}\n```\n\n# 它是如何工作的...\n\n`boost::lexical_cast`函数也可以接受数字作为输入，并将其转换为指定为模板参数的字符串类型(在三角括号中)。这与我们在之前的食谱中所做的非常接近。\n\n# 还有更多...\n\n细心的读者会注意到，在`lexical_cast`的情况下，我们有一个额外的调用来字符串复制构造函数，这样的调用会降低性能。这是真的，但只适用于老的或不好的编译器。现代编译器实现了一个名为**的返回值优化** ( **NRVO** )，它消除了对复制构造函数和析构函数的不必要的调用。即使 C++ 11 兼容的编译器没有检测到 NRVO，也使用`std::string`的移动构造函数，快速高效。`Boost.LexicalCast`文档的*性能*部分显示了不同类型的不同编译器的转换速度。在大多数情况下，`lexical_cast`比`std::stringstream`和`printf`功能更快。\n\n如果将`boost::array`或`std::array`作为输出参数类型传递给`boost::lexical_cast`，则动态内存分配会更少(或者根本没有内存分配，这取决于`std::locale`的实现)。\nC++ 11 有`std::to_string`和`std::to_wstring`功能，在`<string>`头中声明。这些函数使用区域设置，行为分别非常接近`boost::lexical_cast<std::string>`和`boost::lexical_cast<std::wstring>`。C++ 17 有`std::to_chars`功能，可以以惊人的速度将数字转换成字符数组。`std::to_chars`不分配内存，不抛出异常，可能会使用错误代码报错。如果你需要真正快速的不使用语言环境的转换功能，那就使用`std::to_chars`。\n\n# 请参见\n\n*   Boost 的官方文档包含将`lexical_cast`性能与其他转换方法进行比较的表格。在大多数情况下，`lexical_cast`比其他方法[http://boost.org/libs/lexical_cast](http://boost.org/libs/lexical_cast)更快。\n*   将字符串转换为数字的*方法。*\n*   将用户定义的类型转换成字符串的方法。\n\n# 将数字转换成数字\n\n您可能还记得编写以下代码的情况:\n\n```cpp\nvoid some_function(unsigned short param); \nint foo(); \n\nvoid do_something() {\n    // Some compilers may warn, that int is being converted to  \n    // unsigned short and that there is a possibility of loosing  \n    // data.\n    some_function(foo());\n} \n```\n\n通常，程序员只是通过隐式转换到`unsigned short`数据类型来忽略这些警告，如下面的代码片段所示:\n\n```cpp\n// Warning suppressed.\nsome_function( \n    static_cast<unsigned short>(foo()) \n); \n```\n\n但是，如果`foo()`返回不符合`unsigned short`的数字怎么办？这导致难以检测的错误。这种错误可能会在代码中存在多年，然后才会被发现并修复。看看`foo()`的定义:\n\n```cpp\n// Returns -1 if error occurred.\nint foo() { \n    if (some_extremely_rare_condition()) { \n        return -1; \n    } else if (another_extremely_rare_condition()) { \n        return 1000000; \n    } \n    return 42; \n}\n```\n\n# 准备好\n\n这个食谱只需要 C++ 的基础知识。\n\n# 怎么做...\n\n库`Boost.NumericConversion`为这种情况提供了解决方案。把`static_cast`换成`boost::numeric_cast`就可以了。当源值不能存储在目标中时，后者将引发异常:\n\n```cpp\n#include <boost/numeric/conversion/cast.hpp> \n\nvoid correct_implementation() { \n    // 100% correct.\n    some_function( \n        boost::numeric_cast<unsigned short>(foo()) \n    ); \n} \n\nvoid test_function() {\n    for (unsigned int i = 0; i < 100; ++ i) {\n        try {\n            correct_implementation();\n        } catch (const boost::numeric::bad_numeric_cast& e) {\n            std::cout << '#' << i << ' ' << e.what() << std::endl;\n        }\n    }\n}\n```\n\n现在，如果我们运行`test_function()`，它将输出以下内容:\n\n```cpp\n#47 bad numeric conversion: negative overflow \n#58 bad numeric conversion: positive overflow \n```\n\n我们甚至可以检测特定的溢出类型:\n\n```cpp\nvoid test_function1() { \n   for (unsigned int i = 0; i < 100; ++ i) { \n       try { \n           correct_implementation(); \n       } catch (const boost::numeric::positive_overflow& e) { \n           // Do something specific for positive overflow. \n           std::cout << \"POS OVERFLOW in #\" << i << ' '\n                     << e.what() << std::endl; \n       } catch (const boost::numeric::negative_overflow& e) { \n           // Do something specific for negative overflow. \n           std::cout <<\"NEG OVERFLOW in #\" << i << ' '\n                     << e.what() << std::endl; \n       } \n   } \n} \n```\n\n`test_function1()`功能将输出以下内容:\n\n```cpp\nNEG OVERFLOW in #47 bad numeric conversion: negative overflow \nPOS OVERFLOW in #59 bad numeric conversion: positive overflow \n```\n\n# 它是如何工作的...\n\n`boost::numeric_cast`检查输入参数的值是否适合新类型而不丢失数据，如果在转换过程中丢失了什么，则抛出异常。\n\n`Boost.NumericConversion`库的实现速度非常快。它可以在编译时做很多工作，比如转换成更大范围的类型时，只需通过`static_cast`就可以将源转换成目标类型。\n\n# 还有更多...\n\n`boost::numeric_cast`功能通过`boost::numeric::converter`实现，可以调整为使用不同的溢出、范围检查和舍入策略。但通常情况下，`numeric_cast`正是你所需要的。\n\n这里有一个小例子，演示了如何为`boost::numeric::cast`制作自己的溢出处理程序:\n\n```cpp\ntemplate <class SourceT, class TargetT> \nstruct mythrow_overflow_handler {\n    void operator() (boost::numeric::range_check_result r) { \n        if (r != boost::numeric::cInRange) { \n            throw std::logic_error(\"Not in range!\"); \n        } \n    } \n}; \n\ntemplate <class TargetT, class SourceT> \nTargetT my_numeric_cast(const SourceT& in) { \n    typedef boost::numeric::conversion_traits<\n        TargetT, SourceT\n    > conv_traits; \n    typedef boost::numeric::converter < \n        TargetT, \n        SourceT, \n        conv_traits, // default conversion traits\n        mythrow_overflow_handler<SourceT, TargetT> // !!! \n    > converter; \n\n    return converter::convert(in); \n} \n```\n\n以下是如何使用我们的自定义转换器:\n\n```cpp\nvoid example_with_my_numeric_cast() {\n    short v = 0;\n    try {\n        v = my_numeric_cast<short>(100000);\n    } catch (const std::logic_error& e) {\n        std::cout << \"It works! \" << e.what() << std::endl;\n    }\n}\n```\n\n上述代码输出以下内容:\n\n```cpp\nIt works! Not in range!\n```\n\n即使是 C++ 17 也没有安全的数字转换工具。然而，这方面的工作正在进行中。2020 年后，我们完全有机会看到这样的设施。\n\n# 请参见\n\nBoost 的官方文档包含数值转换器所有模板参数的详细描述；可在以下链接获得:[http://boost.org/libs/numeric/conversion](http://boost.org/libs/numeric/conversion)\n\n# 将用户定义的类型转换为字符串或从字符串转换\n\n`Boost.LexicalCast`中有一个功能，可以让用户用`lexical_cast`使用自己的类型。该功能要求用户为该类型编写正确的`std::ostream`和`std::istream`运算符。\n\n# 怎么做...\n\n1.  你只需要提供`operator<<`和`operator>>`流操作符。如果您的类已经可以流式传输，则无需执行任何操作:\n\n```cpp\n#include <iostream>\n#include <stdexcept>\n\n// Negative number that does not store minus sign.\nclass negative_number {\n    unsigned short number_; \n\npublic:\n    explicit negative_number(unsigned short number = 0)\n        : number_(number)\n    {} \n\n    // ...\n    unsigned short value_without_sign() const {\n        return number_;\n    }\n}; \n\ninline std::ostream&\n    operator<<(std::ostream& os, const negative_number& num)\n{\n    os << '-' << num.value_without_sign();\n    return os;\n}\n\ninline std::istream& operator>>(std::istream& is, negative_number& num)\n{\n    char ch;\n    is >> ch;\n    if (ch != '-') {\n        throw std::logic_error(\n            \"negative_number class stores ONLY negative values\"\n        );\n    }\n\n    unsigned short s;\n    is >> s;\n    num = negative_number(s);\n    return is;\n}\n```\n\n2.  现在，我们可以使用`boost::lexical_cast`来进行往返于`negative_number`类的转换。这里有一个例子:\n\n```cpp\n#include <boost/lexical_cast.hpp>\n#include <boost/array.hpp>\n#include <cassert>\n\nvoid example1() {\n    const negative_number n\n        = boost::lexical_cast<negative_number>(\"-100\");\n    assert(n.value_without_sign() == 100);\n\n    const int i = boost::lexical_cast<int>(n);\n    assert(i == -100);\n\n    typedef boost::array<char, 10> arr_t;\n    const arr_t arr = boost::lexical_cast<arr_t>(n);\n    assert(arr[0] == '-');\n    assert(arr[1] == '1');\n    assert(arr[2] == '0');\n    assert(arr[3] == '0');\n    assert(arr[4] == 0);\n} \n```\n\n# 它是如何工作的...\n\n`boost::lexical_cast`功能可以检测并使用流操作符转换用户定义的类型。\n\n`Boost.LexicalCast`库有许多针对基本类型的优化，当用户定义类型转换为基本类型或基本类型转换为用户定义类型时，都会触发这些优化。\n\n# 还有更多...\n\n`boost::lexical_cast`函数也可以转换为宽字符串，但是需要正确的`basic_istream`和`basic_ostream`运算符重载:\n\n```cpp\ntemplate <class CharT> \nstd::basic_ostream<CharT>& \n    operator<<(std::basic_ostream<CharT>& os, const negative_number& num)\n{ \n    os << static_cast<CharT>('-') << num.value_without_sign(); \n    return os; \n} \n\ntemplate <class CharT> \nstd::basic_istream<CharT>& \n    operator>>(std::basic_istream<CharT>& is, negative_number& num)\n{ \n    CharT ch; \n    is >> ch; \n    if (ch != static_cast<CharT>('-')) { \n        throw std::logic_error(\n            \"negative_number class stores ONLY negative values\"\n        ); \n    } \n    unsigned short s; \n    is >> s; \n    num = negative_number(s); \n    return is; \n} \n\nvoid example2() { \n    const negative_number n = boost::lexical_cast<negative_number>(L\"-1\"); \n    assert(n.value_without_sign() == 1); \n\n    typedef boost::array<wchar_t, 10> warr_t; \n    const warr_t arr = boost::lexical_cast<warr_t>(n); \n    assert(arr[0] == L'-'); \n    assert(arr[1] == L'1'); \n    assert(arr[2] == 0); \n} \n```\n\n`Boost.LexicalCast`库不是 C++ 的一部分。很多 Boost 库都在使用它，我希望它也能让你的生活更轻松。\n\n# 请参见\n\n*   `Boost.LexicalCast`文档包含一些示例、性能度量和常见问题的答案；它在[http://boost.org/libs/lexical_cast](http://boost.org/libs/lexical_cast)有售\n*   将字符串转换为数字的*方法*\n*   将数字转换为字符串的*方法*\n\n# 转换智能指针\n\n这里有一个问题:\n\n1.  你有一个名为`some_class`的班级:\n\n```cpp\nstruct base {\n    virtual void some_methods() = 0;\n    virtual ~base();\n};\n\nstruct derived: public base {\n    void some_methods() /*override*/;\n    virtual void derived_method() const;\n\n    ~derived() /*override*/;\n};\n```\n\n2.  您有一个第三方应用编程接口，它通过共享指向`base`的指针返回构造的`derived`，并在其他函数中接受共享指向`const derived`的指针:\n\n```cpp\n#include <boost/shared_ptr.hpp>\nboost::shared_ptr<const base> construct_derived();\nvoid im_accepting_derived(boost::shared_ptr<const derived> p);\n```\n\n3.  您必须使以下代码工作:\n\n```cpp\nvoid trying_hard_to_pass_derived() {\n    boost::shared_ptr<const base> d = construct_derived();\n\n    // Oops! Compile time error:\n    // ‘const struct base; has no member named ‘derived_method;.\n    d->derived_method();\n\n    // Oops! Compile time error:\n    // could not convert ‘d; to ‘boost::shared_ptr<const derived>;.\n    im_accepting_derived(d);\n}\n```\n\n你如何以一种友好的方式解决这个问题？\n\n# 入门指南\n\n这个食谱需要 C++ 和智能指针的基本知识。\n\n# 怎么做...\n\n解决方案是对智能指针使用特殊的强制转换。在这种特殊情况下，我们需要使用`dynamic_cast`功能，所以我们使用`boost::dynamic_pointer_cast`:\n\n```cpp\nvoid trying_hard_to_pass_derived2() {\n    boost::shared_ptr<const derived> d\n        = boost::dynamic_pointer_cast<const derived>(\n            construct_derived()\n        );\n\n    if (!d) {\n        throw std::runtime_error(\n            \"Failed to dynamic cast\"\n        );\n    }\n\n    d->derived_method();\n    im_accepting_derived(d);\n} \n```\n\n# 它是如何工作的...\n\nBoost 库有很多智能指针转换的功能。它们都接受一个智能指针和一个模板参数`T`，其中`T`是智能指针想要的模板类型。这些转换运算符模仿内置转换的行为，同时正确管理引用计数和其他智能指针内部:\n\n*   `boost::static_pointer_cast<T>(p)` -做`static_cast<T*>(p.get())`\n*   `boost::dynamic_pointer_cast<T>(p)` -做`dynamic_cast<T*>(p.get())`\n*   `boost::const_pointer_cast<T>(p)` -做`const_cast<T*>(p.get())`\n*   `boost::reinterpret_pointer_cast<T>(p)` -做`reinterpret_cast<T*>(p.get())`\n\n# 还有更多...\n\n所有的`boost::*_pointer_cast`函数都可以使用标准库中的智能指针和 C 指针，如果你包括`<boost/pointer_cast.hpp>`。\n在 C++ 11 中，标准库在`<memory>`头中定义了`std::static_pointer_cast`、`std::dynamic_pointer_cast`和`std::const_pointer_cast`，但是，它只针对`std::shared_ptr`。C++ 17 标准库有`std::reinterpret_pointer_cast`，但只针对`std::shared_ptr`。\n\n# 请参见\n\n*   `Boost.SmartPointer`库文档包含更多关于位于[http://boost.org/libs/smart_ptr/pointer_cast.html](http://boost.org/libs/smart_ptr/pointer_cast.html)的标准库的指针强制转换的示例\n*   `boost::shared_ptr`的铸件参考可在[http://boost.org/libs/smart_ptr/shared_ptr.htm](http://boost.org/libs/smart_ptr/shared_ptr.htm)获得\n*   本章中的*转换多态对象*配方将向您展示一种更好的动态转换方法\n\n# 转换多态对象\n\n想象一下，一些程序员设计了这样一个糟糕的接口，如下所示(这是一个很好的例子，说明了接口不应该如何编写):\n\n```cpp\nstruct object { \n    virtual ~object() {} \n}; \n\nstruct banana: public object { \n    void eat() const {} \n    virtual ~banana(){} \n}; \n\nstruct penguin: public object { \n    bool try_to_fly() const {\n        return false; // penguins do not fly\n    }\n    virtual ~penguin(){} \n}; \n\nobject* try_produce_banana(); \n```\n\n我们的任务是创建一个吃香蕉的函数，如果有不同的东西来代替香蕉，就会抛出异常(`try_produce_banana()`可能会返回`nullptr`),所以如果我们不检查就取消引用它返回的值，我们就有取消引用空指针的危险。\n\n# 入门指南\n\n这个食谱需要 C++ 的基础知识。\n\n# 怎么做...\n\n因此，我们需要编写以下代码:\n\n```cpp\nvoid try_eat_banana_impl1() { \n    const object* obj = try_produce_banana(); \n    if (!obj) { \n        throw std::bad_cast(); \n    } \n    dynamic_cast<const banana&>(*obj).eat(); \n} \n```\n\n很丑，不是吗？`Boost.Conversion`提供了稍微好一点的解决方案:\n\n```cpp\n#include <boost/cast.hpp> \n\nvoid try_eat_banana_impl2() { \n    const object* obj = try_produce_banana(); \n    boost::polymorphic_cast<const banana*>(obj)->eat(); \n} \n```\n\n# 它是如何工作的...\n\n`boost::polymorphic_cast`函数只是包装第一个例子的代码，仅此而已。它检查输入是否为空，然后尝试进行动态转换。这些操作中的任何错误都将引发`std::bad_cast`异常。\n\n# 还有更多...\n\n`Boost.Conversion`库还有一个`polymorphic_downcast`功能，应该只用于肯定会成功的向下转换。在调试模式下(当`NDEBUG`未定义时)，它将使用`dynamic_cast`检查正确的下变频。当`NDEBUG`被定义时，`polymorphic_downcast`功能将只做一个`static_cast`操作。在性能关键的部分使用它是一个很好的功能，仍然保留了在调试编译中检测错误的能力。\n从 Boost 1.58 开始，`Boost.Conversion`库中就有了`boost::polymorphic_pointer_downcast`和`boost::polymorphic_pointer_cast`功能。这些功能允许您安全地转换智能指针，并且具有与`boost::polymorphic_cast`和`boost::polymorphic_downcast`相同的特性。\nc++ 标准库缺少`polymorphic_cast`和`polymorphic_downcast`。\n\n# 请参见\n\n*   最初，`polymorphic_cast`的想法是在《C++ 编程语言》*比雅尼·斯特劳斯特鲁普*一书中提出的。关于不同主题的更多信息和一些好主意，请参考这本书。\n*   官方文件可能也有帮助；在[http://boost.org/libs/conversion](http://boost.org/libs/conversion)有售。\n*   有关转换智能指针的更多信息，请参考之前的配方。\n\n# 解析简单输入\n\n解析小文本是一项常见的任务。这样的情况总是让人左右为难:是用一些第三方专业的好的解析工具，比如 Bison 或者 ANTLR，还是只用 C++ 和标准库来尝试手工编写？第三方工具很适合处理复杂文本的解析，使用它们编写解析器很容易，但是它们需要额外的工具来从语法中创建 C++ 或 C 代码，并为项目添加更多的依赖项。\n\n手写解析器通常很难维护，但是除了 C++ 编译器，它们什么都不需要。\n\n![](img/00005.jpeg)\n\n让我们从一个非常简单的任务开始，解析 ISO 格式的日期，如下所示:\n\n```cpp\nYYYY-MM-DD \n```\n\n以下是可能的输入示例:\n\n```cpp\n2013-03-01 \n2012-12-31  // (woo-hoo, it almost a new year!) \n```\n\n我们需要来自以下链接[http://www.ietf.org/rfc/rfc333:](http://www.ietf.org/rfc/rfc3339.txt:)的解析器语法\n\n```cpp\n   date-fullyear   = 4DIGIT \n   date-month      = 2DIGIT  ; 01-12 \n   date-mday       = 2DIGIT  ; 01-28, 01-29, 01-30, 01-31 based on \n                             ; month/year \n   full-date       = date-fullyear \"-\" date-month \"-\" date-mday \n```\n\n# 准备好\n\n确保您熟悉占位符的概念，或者阅读[第 1 章](01.html#RL0A0-712b4ba1126a4c7c89e1d44de61b4bdd)、*开始编写应用*中的*重新排序函数参数*和*将值绑定为函数参数*的方法。解析工具的基础知识会很好。\n\n# 怎么做...\n\n让我给你介绍一个`Boost.Spirit`图书馆。它允许直接用 C++ 代码编写解析器(以及 lexers 和生成器)，这些解析器可以立即执行(不需要额外的工具来生成 C++ 代码)。`Boost.Spirit`的语法非常接近**扩展的巴克斯-诺尔形式** ( **EBNF** )，它被用于以多种标准表达语法，并被其他流行的解析器所理解。本章开头的语法是在 EBNF:\n\n1.  我们需要包含以下标题:\n\n```cpp\n#include <boost/spirit/include/qi.hpp>\n#include <boost/spirit/include/phoenix_core.hpp>\n#include <boost/spirit/include/phoenix_operator.hpp>\n#include <cassert>\n```\n\n2.  现在，是时候创建一个`date`结构来保存解析后的数据了:\n\n```cpp\nstruct date {\n    unsigned short year;\n    unsigned short month;\n    unsigned short day;\n};\n```\n\n3.  让我们看看解析器(在下一节中可以找到它如何工作的逐步描述):\n\n```cpp\n// See recipe \"Type 'reference to string'\" for a better type\n// than std::string for parameter 's'\ndate parse_date_time1(const std::string& s) {\n    using boost::spirit::qi::_1;\n    using boost::spirit::qi::ushort_;\n    using boost::spirit::qi::char_;\n    using boost::phoenix::ref;\n\n    date res;\n    const char* first = s.data();\n    const char* const end = first + s.size();\n    const bool success = boost::spirit::qi::parse(first, end,\n\n        // Implementation of 'full-date' rule from EBNF grammar.\n        ushort_[ ref(res.year) = _1 ] >> char_('-')\n           >> ushort_[ ref(res.month) = _1 ] >> char_('-')\n           >> ushort_[ ref(res.day) = _1 ]\n\n    ); \n\n    if (!success || first != end) {\n        throw std::logic_error(\"Parsing failed\");\n    }\n    return res;\n}\n```\n\n4.  现在，我们可以在任何地方使用这个解析器:\n\n```cpp\nint main() {\n    const date d = parse_date_time1(\"2017-12-31\");\n    assert(d.year == 2017);\n    assert(d.month == 12);\n    assert(d.day == 31);\n}\n```\n\n# 它是如何工作的...\n\n这是一个非常简单的实现；它不检查数字的位数。解析发生在`boost::spirit::qi::parse`函数中。让我们稍微简化一下，去掉成功解析的动作:\n\n```cpp\nconst bool success = boost::spirit::qi::parse(first, end, \n     ushort_ >> char_('-') >> ushort_ >> char_('-') >> ushort_ \n); \n```\n\n`first`参数指向要解析的数据的开头。它必须是非恒定变量，因为`parse`函数会将其修改为指向解析序列的末尾。\n参数`end`指向要解析的最后一个元素之后的位置。`first`和`end`应为迭代器或指针。\n\n函数的第三个参数是解析规则。它看起来完全像 EBNF 规则:\n\n```cpp\ndate-fullyear \"-\" date-month \"-\" date-md \n```\n\n我们刚刚用`>>`运算符替换了空格。\n\n`parse`功能成功返回`true`。如果我们想确保整个字符串被成功解析，我们需要检查解析器的返回值以及`end`和修改后的`first`迭代器的相等性。\n\n现在，我们需要处理成功解析的动作，这个方法就结束了。`Boost.Spirit`中的语义动作写在`[]`内部，可以使用函数指针、函数对象、`boost::bind`、`std::bind`(或其他`bind()`实现)或 C++ 11 lambda 函数来编写。\n\n因此，您也可以使用 C++ 11 lambda 为`YYYY`编写一个规则:\n\n```cpp\nconst auto y = [&res](unsigned short s) { res.year = s; };\n// ...\n\nushort_[y] >> char_('-') >> // ...\n```\n\nYou cannot put the lambda definition directly inside the `[]` because the C++ compiler will think that it's an attribute. As a workaround, you can make an `auto` variable with the lambda function in it and use that variable in parser rule description (just like it was done in the preceding code snippet).\n\n现在，我们来仔细看看月的语义动作:\n\n```cpp\nushort_[ ref(res.month) = _1 ] \n```\n\n对于从头读起这本书的人来说，前面的代码提醒了关于`boost::bind`、`boost::ref`和占位符。`ref(res.month)`表示将`res.month`作为可修改的引用传递，`_1`表示第一个输入参数，它将是一个数字(解析`ushort_`的结果)。\n\n# 还有更多...\n\n现在让我们修改我们的解析器，这样它可以处理数字计数。为此，我们将采用`unit_parser`类模板并设置正确的参数:\n\n```cpp\ndate parse_date_time2(const std::string& s) { \n    using boost::spirit::qi::_1; \n    using boost::spirit::qi::uint_parser; \n    using boost::spirit::qi::char_; \n    using boost::phoenix::ref; \n\n    date res; \n\n    // Use unsigned short as output type; require Radix 10 and\n    // from 2 to 2 digits.\n    uint_parser<unsigned short, 10, 2, 2> u2_; \n\n    // Use unsigned short as output type; require Radix 10 and\n    // from 4 to 4 digits.\n    uint_parser<unsigned short, 10, 4, 4> u4_; \n\n    const char* first = s.data(); \n    const char* const end = first + s.size(); \n    const bool success = boost::spirit::qi::parse(first, end, \n\n        u4_ [ ref(res.year) = _1 ] >> char_('-') \n            >> u2_ [ ref(res.month) = _1 ] >> char_('-')\n            >> u2_ [ ref(res.day) = _1 ] \n\n    ); \n    if (!success || first != end) { \n        throw std::logic_error(\"Parsing failed\"); \n    } \n    return res; \n} \n```\n\n如果那些例子看起来很复杂，不要担心。第一次我也是被`Boost.Spirit`吓到的，但现在真的简化了我的生活。如果这段代码没有吓到你，你是非常勇敢的。\n\n不要在头文件中编写解析器，因为这会增加项目的编译时间。在源文件中编写解析器，并隐藏该文件中的所有`Boost.Spirit`内部。如果我们调整前面的示例以遵循该规则，那么头文件将如下所示:\n\n```cpp\n// Header file.\n#ifndef MY_PROJECT_PARSE_DATE_TIME\n#define MY_PROJECT_PARSE_DATE_TIME\n\n#include <string>\n\nstruct date { \n    unsigned short year; \n    unsigned short month; \n    unsigned short day; \n}; \n\ndate parse_date_time2(const std::string& s);\n\n#endif // MY_PROJECT_PARSE_DATE_TIME\n```\n\n还要注意传递给`boost::spirit::parse`函数的迭代器类型。使用的迭代器类型越少，得到的二进制文件就越小。\n\n如果您现在认为使用标准库手工实现解析日期更简单，那么您是对的！但只是现在。看看下一个食谱，它会给你更多关于`Boost.Spirit`用法的例子，并将这个例子扩展为案例，当手工编写解析器比使用`Boost.Spirit`更难的时候。\n\n`Boost.Spirit`库不是 C++ 的一部分，在最近的将来也不会被提议包含在内。但是，它与现代 C++ 特性配合得非常好，所以如果您的编译器支持 C++ 11，请使用它们:\n\n```cpp\ndate parse_date_time2_cxx(const std::string& s) {\n    using boost::spirit::qi::uint_parser; \n    using boost::spirit::qi::char_; \n\n    date res; \n\n    uint_parser<unsigned short, 10, 2, 2> u2_; \n    uint_parser<unsigned short, 10, 4, 4> u4_; \n\n    const auto y = [&res](unsigned short s) { res.year = s; };\n    const auto m = [&res](unsigned short s) { res.month = s; };\n    const auto d = [&res](unsigned short s) { res.day = s; };\n\n    const char* first = s.data(); \n    const char* const end = first + s.size();\n    const bool success = boost::spirit::qi::parse(first, end, \n        u4_[y] >> char_('-') >> u2_[m] >> char_('-') >> u2_[d] \n    );\n\n    if (!success || first != end) { \n        throw std::logic_error(\"Parsing failed\"); \n    } \n    return res;\n}\n```\n\n# 请参见\n\n*   [第一章](01.html#RL0A0-712b4ba1126a4c7c89e1d44de61b4bdd)、*中*重新排序功能*配方参数开始编写应用*。\n*   *绑定一个值作为功能参数*配方。\n*   `Boost.Spirit`是一个巨大的只有标题的库。关于它，可能会写一本单独的书。欢迎在[http://boost.org/libs/spirit.](http://boost.org/libs/spirit)使用其文档\n\n# 解析复杂输入\n\n在前面的食谱中，我们编写了一个简单的日期解析器。想象一下，一段时间过去了，任务变了。现在，我们需要编写一个支持多种输入格式和区域偏移量的日期时间解析器。我们的解析器必须理解以下输入:\n\n```cpp\n2012-10-20T10:00:00Z      // date time with zero zone offset \n2012-10-20T10:00:00       // date time without zone offset \n2012-10-20T10:00:00+09:15 // date time with zone offset \n2012-10-20-09:15          // date time with zone offset \n10:00:09+09:15            // time with zone offset \n```\n\n# 准备好\n\n我们将使用`Boost.Spirit`库，这在*解析简单输入*食谱中有描述。在接触这个食谱之前，请先阅读它。\n\n# 怎么做...\n\n1.  让我们从编写一个保存解析结果的日期时间结构开始:\n\n```cpp\n#include <stdexcept>\n#include <cassert>\n\nstruct datetime {\n    enum zone_offsets_t {\n        OFFSET_NOT_SET,\n        OFFSET_Z,\n        OFFSET_UTC_PLUS,\n        OFFSET_UTC_MINUS\n    };\n\nprivate:\n    unsigned short year_;\n    unsigned short month_;\n    unsigned short day_;\n\n    unsigned short hours_;\n    unsigned short minutes_;\n    unsigned short seconds_;\n\n    zone_offsets_t zone_offset_type_;\n    unsigned int zone_offset_in_min_;\n\n    static void dt_assert(bool v, const char* msg) {\n        if (!v) {\n            throw std::logic_error(\n                \"Assertion failed in datetime: \" + std::string(msg)\n            );\n        }\n    }\n\npublic:\n    datetime()\n        : year_(0), month_(0), day_(0)\n        , hours_(0), minutes_(0), seconds_(0)\n        , zone_offset_type_(OFFSET_NOT_SET), zone_offset_in_min_(0)\n    {}\n\n    // Getters: year(), month(), day(), hours(), minutes(),\n    // seconds(), zone_offset_type(), zone_offset_in_min()\n    // ...\n\n    // Setters: set_year(unsigned short), set_day(unsigned short), ...\n    //\n    // void set_*(unsigned short val) {\n    //     Some dt_assert.\n    //     Setting the '*_' to 'val'.\n    // }\n    // ...\n\n}; \n```\n\n2.  现在，让我们编写一个函数来设置区域偏移:\n\n```cpp\nvoid set_zone_offset(datetime& dt, char sign, unsigned short hours\n    , unsigned short minutes)\n{\n    dt.set_zone_offset(\n        sign == '+'\n        ? datetime::OFFSET_UTC_PLUS\n        : datetime::OFFSET_UTC_MINUS\n    );\n    dt.set_zone_offset_in_min(hours * 60 + minutes);\n}\n```\n\n3.  编写解析器可以分为编写几个简单的解析器。我们从编写一个区域偏移解析器开始:\n\n```cpp\n// Default includes for Boost.Spirit.\n#include <boost/spirit/include/qi.hpp>\n#include <boost/spirit/include/phoenix_core.hpp>\n#include <boost/spirit/include/phoenix_operator.hpp>\n\n// We'll use bind() function from Boost.Spirit,\n// because it interates better with parsers.\n#include <boost/spirit/include/phoenix_bind.hpp>\n\ndatetime parse_datetime(const std::string& s) {\n    using boost::spirit::qi::_1;\n    using boost::spirit::qi::_2;\n    using boost::spirit::qi::_3;\n    using boost::spirit::qi::uint_parser;\n    using boost::spirit::qi::char_;\n    using boost::phoenix::bind;\n    using boost::phoenix::ref;\n\n    datetime ret;\n\n    // Use unsigned short as output type; require Radix 10 and\n    // from 2 to 2 digits.\n    uint_parser<unsigned short, 10, 2, 2> u2_;\n\n    // Use unsigned short as output type; require Radix 10 and\n    // from 4 to 4 digits.\n    uint_parser<unsigned short, 10, 4, 4> u4_;\n\n    boost::spirit::qi::rule<const char*, void()> timezone_parser\n        = -( // unary minus means optional rule\n\n            // Zero offset\n            char_('Z')[ bind(\n                &datetime::set_zone_offset, &ret, datetime::OFFSET_Z\n            ) ]\n\n            | // OR\n\n            // Specific zone offset\n            ((char_('+')|char_('-')) >> u2_ >> ':' >> u2_) [\n                bind(&set_zone_offset, ref(ret), _1, _2, _3)\n            ]\n        );\n```\n\n4.  让我们通过编写剩余的解析器来结束我们的示例:\n\n```cpp\n    boost::spirit::qi::rule<const char*, void()> date_parser =\n           u4_ [ bind(&datetime::set_year, &ret, _1) ] >> '-'\n        >> u2_ [ bind(&datetime::set_month, &ret, _1) ] >> '-'\n        >> u2_ [ bind(&datetime::set_day, &ret, _1) ]; \n\n    boost::spirit::qi::rule<const char*, void()> time_parser =\n            u2_ [ bind(&datetime::set_hours, &ret, _1) ] >> ':'\n         >> u2_ [ bind(&datetime::set_minutes, &ret, _1) ] >> ':'\n         >> u2_ [ bind(&datetime::set_seconds, &ret, _1) ]; \n\n    const char* first = s.data();\n    const char* const end = first + s.size();\n    const bool success = boost::spirit::qi::parse(first, end,\n        (\n            (date_parser >> 'T' >> time_parser)\n            | date_parser\n            | time_parser\n        )\n        >> timezone_parser\n    );\n\n    if (!success || first != end) {\n        throw std::logic_error(\"Parsing of '\" + s + \"' failed\");\n    }\n    return ret;\n} // end of parse_datetime() function\n```\n\n# 它是如何工作的...\n\n这里一个非常有趣的变量是`boost::spirit::qi::rule<const char*, void()>`。它删除了生成的解析器的确切类型，并允许您为递归语法编写解析器。它还允许您在源文件中编写解析器，并将它们导出到头文件，而不会显著影响项目的编译时间。例如:\n\n```cpp\n// Somewhere in header file \nclass example_1 { \n    boost::spirit::qi::rule<const char*, void()> some_rule_; \npublic: \n    example_1(); \n}; \n\n// In source file \nexample_1::example_1() { \n    some_rule_ = /* ... a lot of parser code ... */; \n} \n```\n\n请注意，这个类暗示了编译器的优化障碍，所以不需要的时候不要使用它。\n\n有时候我们用`>> ':'`代替`>> char_(':')`。第一种方法的局限性更大:您不能将动作绑定到它，也不能仅通过组合字符来创建新的规则(例如，您根本不能在不使用`char_`的情况下编写`char_('+')|char_('-')`。但是为了获得更好的性能，请使用第一种方法，因为有些编译器可能会稍微优化一下。\n\n# 还有更多...\n\n我们可以通过移除进行类型擦除的`rule<>`对象来使我们的示例稍微快一点。只需用 C++ 11 `auto`关键字替换即可。\n\n`Boost.Spirit`库生成非常快的解析器；在官方网站上有一些性能指标。官方文档包含编写更快解析器的高级建议。\n\n`boost::phoenix::bind`的用法不是强制性的，但是如果没有它，解析`timezone_parser`中特定区域偏移的规则将处理\n\n`boost::fusion::vector<char, unsigned short, unsigned short>`型。使用`bind(&set_zone_offset, ref(ret), _1, _2, _3)`似乎是一个更方便读者的解决方案。\n\n解析大文件时，考虑阅读[第 11 章](11.html#GUKG20-712b4ba1126a4c7c89e1d44de61b4bdd)、*中的*最快读取文件的方法*，使用系统*，因为不正确的文件处理可能比解析更能降低程序的速度。\n\n编译使用`Boost.Spirit`(或`Boost.Fusion`)库的代码可能会花费大量时间，因为模板实例数量巨大。当实验`Boost.Spirit`库使用现代编译器时，它们提供了更好的编译时间。\n\n# 请参见\n\n`Boost.Spirit`图书馆值得单独写一本书，不可能在几个食谱中描述它的所有特征，所以参考文档会帮助你获得更多关于它的信息。在[http://boost.org/libs/spirit](http://boost.org/libs/spirit)有售。在那里，您将找到更多的例子、现成的解析器，以及如何使用 Boost 在 C++ 11 代码中直接编写 lexers 和生成器的信息。"
  },
  {
    "path": "docs/boost-cpp-app-dev-cb/04.md",
    "content": "# 四、编译时技巧\n\n在本章中，我们将介绍以下内容:\n\n*   编译时检查大小\n*   启用积分类型的函数模板使用\n*   禁用真实类型的函数模板使用\n*   从数字创建类型\n*   实现类型特征\n*   为模板参数选择最佳运算符\n*   在 C++ 03 中获取表达式类型\n\n# 介绍\n\n在本章中，我们将看到一些基本的例子，介绍如何在编译时检查、优化算法以及其他元编程任务中使用 Boost 库。\n\n有些读者可能会问，*“我们为什么要关心编译时的事情？”*那是因为发布的版本程序编译一次，运行多次。我们在编译时做的越多，留给运行时的工作就越少，从而产生更快、更可靠的程序。只有当带有检查的部分代码被执行时，运行时检查才会被执行。编译时检查会阻止程序编译，最好是有意义的编译器错误消息。\n\n这一章可能是最重要的一章。没有它，理解 Boost 源和其他类似 Boost 的库是不可能的。\n\n# 编译时检查大小\n\n假设我们正在编写一些序列化函数，将值存储在指定大小的缓冲区中:\n\n```cpp\n#include <cstring> \n#include <boost/array.hpp> \n\n// C++ 17 has std::byte out of the box!\n// Unfortunately this is as C++ 03 example. \ntypedef unsigned char byte_t;\n\ntemplate <class T, std::size_t BufSizeV> \nvoid serialize_bad(const T& value, boost::array<byte_t, BufSizeV>& buffer) { \n    // TODO: check buffer size.\n    std::memcpy(&buffer[0], &value, sizeof(value)); \n}\n```\n\n该代码存在以下问题:\n\n*   没有检查缓冲区的大小，因此它可能会溢出\n*   该功能可用于**不可复制的**类型，这会导致不正确的行为\n\n我们可以通过添加一些断言来部分修复它，例如:\n\n```cpp\ntemplate <class T, std::size_t BufSizeV> \nvoid serialize_bad(const T& value, boost::array<byte_t, BufSizeV>& buffer) {  \n    // TODO: think of something better.\n    assert(BufSizeV >= sizeof(value));\n    std::memcpy(&buffer[0], &value, sizeof(value)); \n}\n```\n\n但是，这是一个糟糕的解决方案。如果没有调用函数，运行时检查不会在调试模式下的测试期间触发断言。运行时检查甚至可以在发布模式下进行优化，因此可能会发生非常糟糕的事情。\n`BufSizeV`和`sizeof(value)`值在编译时是已知的。这意味着，如果缓冲区太小，我们可以强制这段代码编译失败，而不用运行时断言。\n\n# 准备好\n\n这个食谱需要一些 C++ 模板和`Boost.Array`库的知识。\n\n# 怎么做...\n\n让我们使用`Boost.StaticAssert`和`Boost.TypeTraits`库来纠正解决方案。以下是如何:\n\n```cpp\n#include <boost/static_assert.hpp> \n#include <boost/type_traits/has_trivial_copy.hpp> \n\ntemplate <class T, std::size_t BufSizeV> \nvoid serialize(const T& value, boost::array<byte_t, BufSizeV>& buffer) { \n    BOOST_STATIC_ASSERT(BufSizeV >= sizeof(value)); \n    BOOST_STATIC_ASSERT(boost::has_trivial_copy<T>::value); \n\n    std::memcpy(&buffer[0], &value, sizeof(value)); \n}\n```\n\n# 它是如何工作的...\n\n只有在编译时可以计算断言表达式并且可以隐式转换为`bool`时，才能使用`BOOST_STATIC_ASSERT`宏。这意味着您只能在`BOOST_STATIC_ASSERT`中使用`sizeof()`、静态常量、常量表达式变量、带有编译时已知参数的常量表达式函数以及其他常量表达式。如果断言表达式计算为`false`，则`BOOST_STATIC_ASSERT`将停止编译。在`serialize`函数的情况下，如果第一次静态断言失败，这意味着用户误用了`serialize`函数，并提供了非常小的缓冲区。\n\n下面是更多的例子:\n\n```cpp\nBOOST_STATIC_ASSERT(3 >= 1); \n\nstruct some_struct { enum enum_t { value = 1}; }; \nBOOST_STATIC_ASSERT(some_struct::value); \n\ntemplate <class T1, class T2> \nstruct some_templated_struct \n{ \n    enum enum_t { value = (sizeof(T1) == sizeof(T2))}; \n}; \nBOOST_STATIC_ASSERT((some_templated_struct<int, unsigned int>::value));\n\ntemplate <class T1, class T2>\nstruct some_template { \n    BOOST_STATIC_ASSERT(sizeof(T1) == sizeof(T2));\n};\n```\n\nIf the `BOOST_STATIC_ASSERT` macro's assert expression has a comma sign in it, we must wrap the whole expression in additional brackets.\n\n最后一个例子非常接近我们在`serialize()`函数第二行看到的。所以现在，是时候去发现更多关于`Boost.TypeTraits`图书馆的事情了。这个库提供了大量的编译时元函数，允许我们获取关于类型的信息和修改类型。元功能用法看起来像`boost::function_name<parameters>::value`或`boost::function_name<parameters>::type`。仅当`T`是简单的可复制类型时，`boost::has_trivial_copy<T>::value`元功能才返回`true`。\n\n让我们再看一些例子:\n\n```cpp\n#include <iostream> \n#include <boost/type_traits/is_unsigned.hpp> \n#include <boost/type_traits/is_same.hpp> \n#include <boost/type_traits/remove_const.hpp> \n\ntemplate <class T1, class T2> \nvoid type_traits_examples(T1& /*v1*/, T2& /*v2*/)  { \n    // Returns true if T1 is an unsigned number \n    std::cout << boost::is_unsigned<T1>::value; \n\n    // Returns true if T1 has exactly the same type, as T2 \n    std::cout << boost::is_same<T1, T2>::value; \n\n    // This line removes const modifier from type of T1\\. \n    // Here is what will happen with T1 type if T1 is: \n    // const int => int \n    // int => int \n    // int const volatile => int volatile \n    // const int& => const int& \n    typedef typename boost::remove_const<T1>::type t1_nonconst_t; \n}\n```\n\nSome compilers may compile this code even without the `typename` keyword, but such behavior violates the C++ standard, so it is highly recommended to write `typename`.\n\n# 还有更多...\n\n`BOOST_STATIC_ASSSERT`宏有一个更详细的变体叫做`BOOST_STATIC_ASSSERT_MSG`，如果断言失败，它会尽力在编译器日志(或 IDE 窗口)中输出一条错误消息。看看下面的代码:\n\n```cpp\ntemplate <class T, std::size_t BufSizeV> \nvoid serialize2(const T& value, boost::array<byte_t, BufSizeV>& buf) { \n    BOOST_STATIC_ASSERT_MSG(boost::has_trivial_copy<T>::value, \n        \"This serialize2 function may be used only \" \n        \"with trivially copyable types.\" \n    ); \n\n    BOOST_STATIC_ASSERT_MSG(BufSizeV >= sizeof(value), \n        \"Can not fit value to buffer. \" \n        \"Make the buffer bigger.\" \n    ); \n\n    std::memcpy(&buf[0], &value, sizeof(value)); \n} \n\nint main() { \n    // Somewhere in code: \n    boost::array<unsigned char, 1> buf; \n    serialize2(std::string(\"Hello word\"), buf);\n}\n```\n\n上述代码将在 C++ 11 模式下在 g++ 编译器上编译时给出以下结果:\n\n```cpp\nboost/static_assert.hpp:31:45: error: static assertion failed: This serialize2 function may be used only with trivially copyable types.\n #     define BOOST_STATIC_ASSERT_MSG( ... ) static_assert(__VA_ARGS__)\n ^\nChapter04/01_static_assert/main.cpp:76:5: note: in expansion of macro ‘BOOST_STATIC_ASSERT_MSG;\n BOOST_STATIC_ASSERT_MSG(boost::has_trivial_copy<T>::value,\n ^~~~~~~~~~~~~~~~~~~~~~~\n\nboost/static_assert.hpp:31:45: error: static assertion failed: Can not fit value to buffer. Make the buffer bigger.\n #     define BOOST_STATIC_ASSERT_MSG( ... ) static_assert(__VA_ARGS__)\n ^\nChapter04/01_static_assert/main.cpp:81:5: note: in expansion of macro ‘BOOST_STATIC_ASSERT_MSG;\n BOOST_STATIC_ASSERT_MSG(BufSizeV >= sizeof(value),\n ^~~~~~~~~~~~~~~~~~~~~~~\n```\n\n无论是`BOOST_STATIC_ASSSERT`，还是`BOOST_STATIC_ASSSERT_MSG`，或者任何类型特征实体都不意味着运行时损失。所有这些函数都是在编译时执行的，不会向生成的二进制文件中添加一条汇编指令。C++ 11 标准的`static_assert(condition, \"message\")`相当于 Boost 的`BOOST_STATIC_ASSSERT_MSG`。在编译时不需要用户提供消息就可以断言的`BOOST_STATIC_ASSERT`功能在 C++ 17 中以`static_assert(condition)`的形式提供。您不必包含头文件就可以使用内置的编译器`static_assert`。\n\n`Boost.TypeTraits`库被部分接受为 C++ 11 标准。因此，你可以在`std::`名字空间的`<type_traits>`标题中找到特征。C++ 11 `<type_traits>`有一些在`Boost.TypeTraits`中不存在的功能，但是其他一些元功能只存在于 Boost 中。名称以`has_`开头的元功能在标准库中被重命名为名称以`is_`开头的元功能。这样，`has_trivial_copy`就变成了`is_trivially_copyable`等等。\n\nC++ 14 和 Boost 1.65 对所有有`::type`成员的类型特征都有快捷键。那些快捷方式允许你写`remove_const_t<T1>`而不是`typename remove_const<T1>::type`。请注意，在 Boost 1.65 的情况下，快捷键需要 C++ 11 兼容的编译器，因为它们只能使用**类型别名**来实现:\n\n```cpp\ntemplate <class T>\nusing remove_const_t = typename remove_const<T>::type;\n```\n\nC++ 17 为具有`::value`的类型特征增加了`_v`快捷键。从 C++ 17 开始，可以只写`std::is_unsigned_v<T1>`不写`std::is_unsigned<T1>::value`。这一招通常使用`variable templates`来实现:\n\n```cpp\ntemplate <class T>\ninline constexpr bool is_unsigned_v = is_unsigned<T>::value;\n```\n\n当 Boost 和标准库中有类似的特性时，如果您正在编写一个必须在 C++ 11 之前的编译器上工作的项目，请选择 Boost 版本。否则，在极少数情况下，标准库版本可能会稍微好一些。\n\n# 请参见\n\n*   本章的下一个食谱将会给你更多关于静态断言和类型特征如何被使用的例子和想法。\n*   阅读`Boost.StaticAssert`的官方文档，了解更多示例:\n\n[http://boost.org/libs/static_assert.](http://boost.org/libs/static_assert)\n\n# 启用积分类型的函数模板使用\n\n当我们有一个实现某些功能的类模板时，这是一种常见的情况:\n\n```cpp\n// Generic implementation.\ntemplate <class T> \nclass data_processor { \n    double process(const T& v1, const T& v2, const T& v3); \n};\n```\n\n现在，假设我们有该类的两个附加版本，一个用于整型，另一个用于实型:\n\n```cpp\n// Integral types optimized version. \ntemplate <class T>\nclass data_processor_integral {\n    typedef int fast_int_t;\n    double process(fast_int_t v1, fast_int_t v2, fast_int_t v3);\n}; \n\n// SSE optimized version for float types.\ntemplate <class T>\nclass data_processor_sse {\n    double process(double v1, double v2, double v3);\n};\n```\n\n现在的问题是:如何让编译器自动为指定的类型选择正确的类？\n\n# 准备好\n\n这个食谱需要一些 C++ 模板的知识。\n\n# 怎么做...\n\n我们将使用`Boost.Core`和`Boost.TypeTraits`来解决问题:\n\n1.  让我们从包含标题开始:\n\n```cpp\n#include <boost/core/enable_if.hpp>\n#include <boost/type_traits/is_integral.hpp>\n#include <boost/type_traits/is_float.hpp>\n```\n\n2.  让我们在通用实现中添加一个带有默认值的附加模板参数:\n\n```cpp\n// Generic implementation.\ntemplate <class T, class Enable = void>\nclass data_processor {\n    // ...\n};\n```\n\n3.  按照以下方式修改优化版本，以便编译器现在将它们视为模板部分专门化:\n\n```cpp\n// Integral types optimized version.\ntemplate <class T>\nclass data_processor<\n    T,\n    typename boost::enable_if_c<boost::is_integral<T>::value >::type\n>\n{\n    // ...\n};\n\n// SSE optimized version for float types.\ntemplate <class T>\nclass data_processor<\n    T,\n    typename boost::enable_if_c<boost::is_float<T>::value >::type\n>\n{\n    // ...\n};\n```\n\n4.  就这样！现在，编译器将自动选择正确的类:\n\n```cpp\ntemplate <class T>\ndouble example_func(T v1, T v2, T v3) {\n    data_processor<T> proc;\n    return proc.process(v1, v2, v3);\n}\n\nint main () {\n    // Integral types optimized version\n    // will be called.\n    example_func(1, 2, 3);\n    short s = 0;\n    example_func(s, s, s);\n\n    // Real types version will be called.\n    example_func(1.0, 2.0, 3.0);\n    example_func(1.0f, 2.0f, 3.0f);\n\n    // Generic version will be called.\n    example_func(\"Hello\", \"word\", \"processing\");\n}\n```\n\n# 它是如何工作的...\n\n`boost::enable_if_c`模板是一个棘手的模板。它利用了**替换失败不是错误** ( **SFINAE** )原理，该原理在**模板实例化**中使用。这就是原理的工作原理；如果在函数或类模板的实例化过程中形成了无效的参数或返回类型，则实例化将从重载解析集中移除，并且不会导致编译错误。现在棘手的部分，`boost::enable_if_c<true>`有一个可通过`::type`访问的成员类型，但是`boost::enable_if_c<false>`没有`::type`。让我们回到我们的解决方案，看看 SFINAE 如何处理作为`T`参数传递给`data_processor`类的不同类型。\n\n如果我们传递一个`int`作为`T`类型，首先编译器将尝试从*步骤 3* 实例化模板部分专门化，然后使用我们的非专门化泛型版本。当它试图实例化一个`float`版本时，`boost::is_float<T>::value`元功能返回`false`。`boost::enable_if_c<false>::type`元功能不能正确实例化，因为`boost::enable_if_c<false>`没有`::type`，那是 SFINAE 作用的地方。因为类模板不能被实例化，这必须被解释为不是一个错误，编译器跳过这个模板专门化。下一个部分专门化是为整型优化的专门化。`boost::is_integral<T>::value`元功能返回`true`，`boost::enable_if_c<true>::type`可以实例化，使得整个`data_processor`特殊化的实例化成为可能。编译器找到了匹配的部分专门化，因此它不需要尝试实例化非专门化的方法。\n\n现在，让我们尝试传递一些非算术类型(例如，`const char *`)，让我们看看编译器会做什么。首先，编译器尝试实例化模板部分专门化。带有`is_float<T>::value`和`is_integral<T>::value`的专门化未能实例化，因此编译器尝试实例化我们的泛型版本并成功。\n\n没有`boost::enable_if_c<>`，对于任何类型，所有部分专门化的版本都可能同时被实例化，导致歧义和编译失败。\n\nIf you are using templates and compiler reports that cannot choose between two template classes of methods, you probably need `boost::enable_if_c<>`.\n\n# 还有更多...\n\n这个方法的另一个版本叫做`boost::enable_if`，末尾没有`_c`。两者的区别在于`enable_if_c`接受常量作为模板参数；短版本接受具有`value`静态成员的对象。比如`boost::enable_if_c<boost::is_integral<T>::value >::type`等于`boost::enable_if<boost::is_integral<T> >::type`。\n\nBefore Boost 1.56 the `boost::enable_if` metafunctions were defined in the header `<boost/utility/enable_if.hpp>` instead of `<boost/core/enable_if.hpp>`.\n\nC++ 11 在`<type_traits>`头中定义了`std::enable_if`，其行为与`boost::enable_if_c`完全一样。它们之间没有区别，只是 Boost 的版本也可以在非 C++ 11 编译器上工作，提供了更好的可移植性。\n\nC++ 14 有一个快捷方式`std::enable_if_t`，没有`typename`和`::type`必须使用:\n\n```cpp\ntemplate <class T> \nclass data_processor<\n    T, std::enable_if_t<boost::is_float<T>::value >\n>;\n```\n\n所有的使能函数只在编译时执行，不会在运行时增加性能开销。然而，添加额外的模板参数可能会在`typeid(your_class).name()`中产生更大的类名，并在某些平台上比较两个`typeid()`结果时增加极其微小的性能开销。\n\n# 请参见\n\n*   接下来的食谱会给你更多`enable_if`用法的例子。\n*   您也可以查阅`Boost.Core`的官方文件。它包含许多例子和许多有用的类(在本书中被广泛使用)。跟随链接[http://boost.org/libs/core](http://boost.org/libs/core)阅读。\n*   您也可以在[http://msdn . Microsoft . com/en-us/library/3967 w96f % 28v = vs . 110% 29 . aspx](http://msdn.microsoft.com/en-us/library/3967w96f%28v=vs.110%29.aspx)上阅读一些关于模板部分专门化的文章。\n\n# 禁用真实类型的函数模板使用\n\n我们继续使用 Boost 元编程库。在前面的食谱中，我们看到了如何将`enable_if_c`用于类；现在是时候看看它在模板函数中的用法了。\n\n想象一下，在您的项目中，您有一个可以处理所有可用类型的模板函数:\n\n```cpp\ntemplate <class T> \nT process_data(const T& v1, const T& v2, const T& v3);\n```\n\n那个功能存在很久了。您已经编写了大量使用它的代码。突然，你想出了一个优化版本的`process_data`功能，但只针对有`T::operator+=(const T&)`的类型:\n\n```cpp\ntemplate <class T> \nT process_data_plus_assign(const T& v1, const T& v2, const T& v3);\n```\n\n您有一个庞大的代码库，对于拥有正确运算符的类型，手动将`process_data`更改为`process_data_plus_assign`可能需要几个月的时间。因此，您不想更改已经编写的代码。相反，如果可能的话，您希望强制编译器自动使用优化的函数来代替默认函数。\n\n# 准备好\n\n阅读之前的食谱，了解`boost::enable_if_c`的作用，并了解 SFINAE 的概念。仍然需要模板的基本知识。\n\n# 怎么做...\n\n模板魔术可以使用 Boost 库来完成。让我们看看怎么做:\n\n1.  我们需要`boost::has_plus_assign<T>`元功能和`<boost/enable_if.hpp>`标题:\n\n```cpp\n#include <boost/core/enable_if.hpp>\n#include <boost/type_traits/has_plus_assign.hpp>\n```\n\n2.  现在，我们使用`plus assign`运算符禁用类型的默认实现:\n\n```cpp\n// Modified generic version of process_data\ntemplate <class T>\ntypename boost::disable_if_c<boost::has_plus_assign<T>::value,T>::type\n    process_data(const T& v1, const T& v2, const T& v3);\n```\n\n3.  使用`plus assign`操作符启用类型的优化版本:\n\n```cpp\n// This process_data will call a process_data_plus_assign.\ntemplate <class T>\ntypename boost::enable_if_c<boost::has_plus_assign<T>::value, T>::type\n    process_data(const T& v1, const T& v2, const T& v3)\n{\n    return process_data_plus_assign(v1, v2, v3);\n}\n```\n\n4.  现在，尽可能使用优化版本:\n\n```cpp\nint main() {\n    int i = 1;\n    // Optimized version.\n    process_data(i, i, i);\n\n    // Default version.\n    // Explicitly specifing template parameter.\n    process_data<const char*>(\"Testing\", \"example\", \"function\");\n}\n```\n\n# 它是如何工作的...\n\n如果`bool_value`等于`true`，则`boost::disable_if_c<bool_value>::type`元功能禁用该方法。它的工作原理和`boost::enable_if_c<!bool_value>::type`一样。\n\n在替换成功的情况下，作为第二个参数传递给`boost::enable_if_c`或`boost::disable_if_c`的类通过`::type`返回。换句话说，`boost::enable_if_c<true, T>::type`和`T`是一样的。\n\n让我们一步一步来看`process_data(i, i, i)`的情况。我们传递一个`int`作为`T`类型，编译器搜索函数`process_data(int, int, int)`。因为没有这样的函数，下一步就是实例化`process_data`的模板版本。不过有两个模板`process_data`功能。例如，编译器从我们的第二个(优化的)版本开始实例化模板；在这种情况下，它成功地评估了`typename boost::enable_if_c<boost::has_plus_assign<T>::value, T>::type`表达式，并获得了`T`返回类型。但是，编译器并没有停止；它继续实例化尝试，并尝试实例化我们的函数的第一个版本。在替换`typename boost::disable_if_c<boost::has_plus_assign<T>::value`的过程中，出现故障，根据 SFINAE 规则，该故障不被视为错误。不再有模板`process_data`函数，所以编译器停止实例化。如您所见，如果没有`enable_if_c`和`disable_if_c`，编译器将能够实例化这两个模板，并且会有歧义。\n\n# 还有更多...\n\n与`enable_if_c`和`enable_if`的情况一样，禁用功能有一个`disable_if`版本:\n\n```cpp\n// First version \ntemplate <class T> \ntypename boost::disable_if<boost::has_plus_assign<T>, T>::type \n    process_data2(const T& v1, const T& v2, const T& v3); \n\n// process_data_plus_assign \ntemplate <class T> \ntypename boost::enable_if<boost::has_plus_assign<T>, T>::type \n    process_data2(const T& v1, const T& v2, const T& v3);\n```\n\nC++ 11 既没有`disable_if_c`也没有`disable_if`，但是你可以自由使用`std::enable_if<!bool_value>::type`来代替。\n\nBefore Boost 1.56 the `boost::disable_if` metafunctions were defined in the header `<boost/utility/enable_if.hpp>` instead of `<boost/core/enable_if.hpp>`.\n\n正如在前面的配方中提到的，所有启用和禁用功能都只在编译时执行，不会增加运行时的性能开销。\n\n# 请参见\n\n*   从头开始阅读本章，以获得更多编译时技巧的示例。\n*   考虑阅读`Boost.TypeTraits`官方文档，了解更多示例和位于[http://boost.org/libs/type_traits.](http://boost.org/libs/type_traits)的元功能的完整列表\n*   `Boost.Core`库可能会给你提供更多`boost::enable_if`用法的例子；在[http://boost.org/libs/core.](http://boost.org/libs/core)阅读\n\n# 从数字创建类型\n\n我们现在已经看到了如何使用`boost::enable_if_c`在函数之间进行选择的例子。让我们忘记这一章的技巧，使用不同的方法。考虑以下示例，其中我们有一个处理 POD 数据类型的通用方法:\n\n```cpp\n#include <boost/static_assert.hpp> \n#include <boost/type_traits/is_pod.hpp> \n\n// Generic implementation. \ntemplate <class T> \nT process(const T& val) { \n    BOOST_STATIC_ASSERT((boost::is_pod<T>::value)); \n    // ... \n}\n```\n\n我们还针对 1、4 和 8 字节的大小优化了一些处理功能。我们如何重写`process`函数，使其能够将调用分派给优化的处理函数？\n\n# 准备好\n\n强烈建议至少阅读本章的第一个食谱，这样你就不会被这里发生的所有事情所迷惑。模板和元编程不会吓到你(或者只是准备看很多)。\n\n# 怎么做...\n\n我们将看到模板类型的大小如何转换成某种类型的变量，以及该变量如何用于推导正确的函数重载。\n\n1.  让我们定义`process_impl`函数的通用和优化版本:\n\n```cpp\n#include <boost/mpl/int.hpp> \n\nnamespace detail {\n    // Generic implementation.\n    template <class T, class Tag>\n    T process_impl(const T& val, Tag /*ignore*/) {\n        // ...\n    }\n\n    // 1 byte optimized implementation.\n    template <class T>\n    T process_impl(const T& val, boost::mpl::int_<1> /*ignore*/) {\n        // ...\n    }\n\n    // 4 bytes optimized implementation.\n    template <class T>\n    T process_impl(const T& val, boost::mpl::int_<4> /*ignore*/) {\n        // ...\n    }\n\n    // 8 bytes optimized implementation.\n    template <class T>\n    T process_impl(const T& val, boost::mpl::int_<8> /*ignore*/) {\n        // ...\n    }\n} // namespace detail\n```\n\n2.  现在，我们准备编写一个流程函数:\n\n```cpp\n// Dispatching calls:\ntemplate <class T>\nT process(const T& val) {\n    BOOST_STATIC_ASSERT((boost::is_pod<T>::value));\n    return detail::process_impl(val, boost::mpl::int_<sizeof(T)>());\n}\n```\n\n# 它是如何工作的...\n\n这里最有意思的是`boost::mpl::int_<sizeof(T)>()`。`sizeof(T)`在编译时执行，所以它的输出可以作为模板参数。类`boost::mpl::int_<>`只是一个保存整型编译时值的空类。在`Boost.MPL`库中，这样的类被称为**积分常数**。它可以按照下面的代码实现:\n\n```cpp\ntemplate <int Value> \nstruct int_ { \n    static const int value = Value; \n    typedef int_<Value> type; \n    typedef int value_type; \n};\n```\n\n我们需要这个类的一个实例，这就是为什么我们在`boost::mpl::int_<sizeof(T)>()`的末尾有一个圆括号。\n\n现在，让我们仔细看看编译器将如何决定使用哪个`process_impl`函数。首先，编译器试图匹配具有非模板第二参数的函数。如果`sizeof(T)`是 4，编译器会尝试搜索带有像`process_impl(T, boost::mpl::int_<4>)`这样的签名的函数，并从`detail`命名空间中找到我们的 4 字节优化版本。如果`sizeof(T)`是 34，编译器找不到签名像`process_impl(T, boost::mpl::int_<34>)`的函数，使用模板函数`process_impl(const T& val, Tag /*ignore*/)`。\n\n# 还有更多...\n\n`Boost.MPL`库有几种元编程的数据结构。在这个食谱中，我们只触及了冰山一角。您可能会发现 MPL 中的以下积分常数类很有用:\n\n*   `bool_`\n*   `int_`\n*   `long_`\n*   `size_t`\n*   `char_`\n\n所有的`Boost.MPL`函数(除了`for_each`运行时函数)都是在编译时执行的，不会增加运行时开销。\nT2 库不是 C++ 的一部分。然而，C++ 重用了该库中的许多技巧。头文件`type_traits`中的 C++ 11 有一个`std::integral_constant<type, value>`类，可以用与前面例子相同的方式使用。您甚至可以使用它定义自己的**类型别名**:\n\n```cpp\ntemplate <int Value>\nusing int_ = std::integral_constant<int, Value>;\n```\n\n# 请参见\n\n*   [第八章](08.html#CL9V20-712b4ba1126a4c7c89e1d44de61b4bdd)、*元编程*中的食谱会给你更多`Boost.MPL`库用法的例子。如果你有信心，你也可以尝试阅读 http://boost.org/libs/mpl 链接的图书馆文档。\n*   在[http://boost . org/libs/type _ traits/doc/html/boost _ type traits/examples/fill . html](http://boost.org/libs/type_traits/doc/html/boost_typetraits/examples/fill.html)和[http://boost . org/libs/type _ traits/doc/html/boost _ type traits/examples/copy . html](http://boost.org/libs/type_traits/doc/html/boost_typetraits/examples/copy.html)阅读更多标签用法示例。\n\n# 实现类型特征\n\n我们需要实现一个类型特征，如果将`std::vector`类型作为模板参数传递给它，则返回`true`，否则返回`false`。\n\n# 准备好\n\n需要一些`Boost.TypeTrait`或标准库类型特征的基本知识。\n\n# 怎么做...\n\n让我们看看如何实现类型特征:\n\n```cpp\n#include <vector> \n#include <boost/type_traits/integral_constant.hpp> \n\ntemplate <class T> \nstruct is_stdvector: boost::false_type {}; \n\ntemplate <class T, class Allocator> \nstruct is_stdvector<std::vector<T, Allocator> >: boost::true_type  {};\n```\n\n# 它是如何工作的...\n\n几乎所有的工作都是由`boost::true_type`和`boost::false_type`班完成的。`boost::true_type`类中有一个布尔`::value`静态常数，等于`true`。`boost::false_type`类中有一个布尔`::value`静态常数等于`false`。这两个班也有一些`typedef` s 配合好`Boost.MPL`库。\n\n我们的第一个`is_stdvector`结构是一个通用结构，当没有找到这种结构的模板专用版本时，它将一直被使用。我们的第二个`is_stdvector`结构是针对`std::vector`类型的模板专门化(注意它是从`true_type`派生的)。因此，当我们将`std::vector`类型传递给`is_stdvector`结构时，编译器会选择一个模板专用版本。如果我们传递除`std::vector`以外的数据类型，则使用从`false_type`派生的通用版本。\n\nThere is no public keyword before `boost::false_type` and, `boost::true_type` in our trait, because we use `struct` keyword, and by default, it uses public inheritance.\n\n# 还有更多...\n\n使用 C++ 11 兼容编译器的读者可以使用在`<type_traits>`头中声明的`true_type`和`false_type`类型来创建他们自己的类型特征。从 C++ 17 开始，标准库有一个`bool_constant<true_or_false>`类型别名，为了方便您可以使用。\n像往常一样，类和函数的 Boost 版本更具可移植性，因为它们可以在 C++ 11 之前的编译器上使用。\n\n# 请参见\n\n*   这一章几乎所有的食谱都使用类型特征。更多示例和信息请参考【http://boost.org/libs/type_traits】的`Boost.TypeTraits`文档\n*   查看之前的配方，了解更多关于积分常数的信息，以及如何从头开始实现`true_type`和`false_type`。\n\n# 为模板参数选择最佳运算符\n\n想象一下，我们正在使用来自不同供应商的类，这些类实现不同数量的算术运算，并具有整数构造函数。我们确实希望创建一个函数，通过传递给它的任何一个类来递增。还有，我们希望这个功能有效！看看下面的代码:\n\n```cpp\ntemplate <class T> \nvoid inc(T& value) { \n    // TODO:\n    // call ++ value \n    // or call value ++ \n    // or value += T(1); \n    // or value = value + T(1); \n}\n```\n\n# 准备好\n\n需要一些 C++ 模板的基础知识，以及`Boost.TypeTrait`或标准库类型特征。\n\n# 怎么做...\n\n所有的选择都可以在编译时完成。这可以使用`Boost.TypeTraits`库来实现，如以下步骤所示:\n\n1.  让我们从制作正确的功能对象开始:\n\n```cpp\nnamespace detail {\n    struct pre_inc_functor {\n    template <class T>\n        void operator()(T& value) const {\n           ++ value;\n        }\n    };\n\n    struct post_inc_functor {\n    template <class T>\n        void operator()(T& value) const {\n            value++ ;\n        }\n    };\n\n    struct plus_assignable_functor {\n    template <class T>\n        void operator()(T& value) const {\n            value += T(1);\n        }\n    };\n\n    struct plus_functor {\n    template <class T>\n        void operator()(T& value) const {\n            value = value + T(1);\n        }\n    };\n}\n```\n\n2.  之后，我们将需要一堆类型特征:\n\n```cpp\n#include <boost/type_traits/conditional.hpp>\n#include <boost/type_traits/has_plus_assign.hpp>\n#include <boost/type_traits/has_plus.hpp>\n#include <boost/type_traits/has_post_increment.hpp>\n#include <boost/type_traits/has_pre_increment.hpp>\n```\n\n3.  我们准备推导出正确的函子并使用它:\n\n```cpp\ntemplate <class T>\nvoid inc(T& value) {\n    // call ++ value\n    // or call value ++\n    // or value += T(1);\n    // or value = value + T(1);\n\n    typedef detail::plus_functor step_0_t;\n\n    typedef typename boost::conditional<\n      boost::has_plus_assign<T>::value,\n      detail::plus_assignable_functor,\n      step_0_t\n    >::type step_1_t; \n\n    typedef typename boost::conditional<\n      boost::has_post_increment<T>::value,\n      detail::post_inc_functor,\n      step_1_t\n    >::type step_2_t;\n\n    typedef typename boost::conditional<\n      boost::has_pre_increment<T>::value,\n      detail::pre_inc_functor,\n      step_2_t\n    >::type step_3_t;\n\n    step_3_t() // Default construction of the functor.\n        (value); // Calling operator() of the functor.\n}\n```\n\n# 它是如何工作的...\n\n所有的魔法都是通过`conditional<bool Condition, class T1, class T2>`元功能完成的。当`true`作为第一个参数传入元功能时，它通过`::type` `typedef`返回`T1`。当`false`作为第一个参数传入元功能时，通过`::type` `typedef`返回`T2`。它的行为类似于某种编译时`if`语句。\n\n所以，`step0_t`持有`detail::plus_functor`元功能，`step1_t`持有`step0_t`或`detail::plus_assignable_functor`。`step2_t`型保持`step1_t`或`detail::post_inc_functor`。`step3_t`型适用于`step2_t`或`detail::pre_inc_functor`。每个`step*_t` `typedef`持有的是用类型性状推导出来的。\n\n# 还有更多...\n\n这个函数有一个 C++ 11 版本，可以在`std::`命名空间的`<type_traits>`头中找到。Boost 在不同的库中有这个函数的多个版本；例如，`Boost.MPL`有功能`boost::mpl::if_c`，作用和`boost::conditional`一模一样。它还有一个版本`boost::mpl::if_`(结尾没有`c`)，第一个模板参数叫`::type`；如果它是从`boost::true_type`派生的，它会在`::type`调用期间返回它的第二个参数。否则，它将返回最后一个模板参数。我们可以重写我们的`inc()`函数来使用`Boost.MPL`，如下面的代码所示:\n\n```cpp\n#include <boost/mpl/if.hpp> \n\ntemplate <class T> \nvoid inc_mpl(T& value) { \n    typedef detail::plus_functor step_0_t;\n\n    typedef typename boost::mpl::if_<\n      boost::has_plus_assign<T>,\n      detail::plus_assignable_functor,\n      step_0_t\n    >::type step_1_t;\n\n    typedef typename boost::mpl::if_<\n      boost::has_post_increment<T>,\n      detail::post_inc_functor,\n      step_1_t\n    >::type step_2_t;\n\n    typedef typename boost::mpl::if_<\n      boost::has_pre_increment<T>,\n      detail::pre_inc_functor,\n      step_2_t\n    >::type step_3_t;\n\n    step_3_t()   // Default construction of the functor.\n        (value); // Calling operator() of the functor.\n}\n```\n\nC++ 17 有一个`if constexpr`结构，使得前面的例子简单得多:\n\n```cpp\ntemplate <class T> \nvoid inc_cpp17(T& value) { \n    if constexpr (boost::has_pre_increment<T>()) {\n        ++ value;\n    } else if constexpr (boost::has_post_increment<T>()) {\n        value++ ;\n    } else if constexpr(boost::has_plus_assign<T>()) {\n        value += T(1);\n    } else {\n        value = value + T(1);\n    }\n}\n```\n\nIntegral constants in the standard library, `Boost.MPL` and `Boost.TypeTraits` have a constexpr conversion operator. For example, it means that an instance of `std::true_type` can be converted to `true` value. In the preceding example, `boost::has_pre_increment<T>` denotes a type, appending `()`, or C++ 11 curly brackets `{}` make an instance of that type, that is convertible to `true` or `false` values.\n\n# 请参见\n\n*   配方*启用积分类型的模板功能使用。*\n*   配方*禁用真实类型的模板功能使用。*\n*   `Boost.TypeTraits`文档有可用元功能的完整列表。跟随链接[http://boost.org/libs/type_traits](http://boost.org/libs/type_traits)阅读。\n*   [第八章](08.html#CL9V20-712b4ba1126a4c7c89e1d44de61b4bdd)、*元编程*中的食谱会给你更多`Boost.MPL`库用法的例子。如果你有信心，你也可以试着在[http://boost.org/libs/mpl](http://boost.org/libs/mpl)链接阅读它的文档。\n\n# 在 C++ 03 中获取表达式类型\n\n在之前的食谱中，我们看到了一些`boost::bind`用法的例子。它在 C++ 11 字之前可能是一个有用的工具，但是在 C++ 03 中很难将`boost::bind`结果存储为变量。\n\n```cpp\n#include <functional> \n#include <boost/bind.hpp> \n\nconst ??? var = boost::bind(std::plus<int>(), _1, _1);\n```\n\n在 C++ 11 中，我们可以用`auto`关键字代替`???`，这样就可以了。C++ 03 有没有办法做到？\n\n# 准备好\n\nC++ 11 `auto`和`decltype`关键词的知识可能会帮助你理解这个食谱。\n\n# 怎么做...\n\n我们需要一个`Boost.Typeof`库来获取表达式的返回类型:\n\n```cpp\n#include <boost/typeof/typeof.hpp>\n\nBOOST_AUTO(var, boost::bind(std::plus<int>(), _1, _1));\n```\n\n# 它是如何工作的...\n\n它只是创建了一个名为`var`的变量，表达式的值作为第二个参数传递。`var`的类型是从表达类型中检测出来的。\n\n# 还有更多...\n\n一个有经验的 C++ 读者会注意到，在 C++ 11 中有更多的关键字用于检测表达式的类型。也许`Boost.Typeof`对他们也有宏指令。让我们看看下面的 C++ 11 代码:\n\n```cpp\ntypedef decltype(0.5 + 0.5f) type;\n```\n\n使用`Boost.Typeof`，前面的代码可以按照如下方式编写:\n\n```cpp\ntypedef BOOST_TYPEOF(0.5 + 0.5f) type;\n```\n\nC++ 11 版本的`decltype(expr)`推导并返回`expr`的类型。\n\n```cpp\ntemplate<class T1, class T2> \nauto add(const T1& t1, const T2& t2) ->decltype(t1 + t2) { \n    return t1 + t2; \n};\n```\n\n使用`Boost.Typeof`，前面的代码可以这样写:\n\n```cpp\n// Long and portable way:\ntemplate<class T1, class T2>\nstruct result_of {\n    typedef BOOST_TYPEOF_TPL(T1() + T2()) type;\n};\n\ntemplate<class T1, class T2>\ntypename result_of<T1, T2>::type add(const T1& t1, const T2& t2) {\n    return t1 + t2;\n};\n\n// ... or ...\n\n// Shorter version that may crush some compilers.\ntemplate<class T1, class T2>\nBOOST_TYPEOF_TPL(T1() + T2()) add(const T1& t1, const T2& t2) {\n    return t1 + t2;\n};\n```\n\nC++ 11 has a special syntax for specifying return type at the end of the function declaration. Unfortunately, this cannot be emulated in C++ 03, so we cannot use `t1` and `t2` variables in a macro.\n\n您可以在模板和任何其他编译时表达式中自由使用`BOOST_TYPEOF()`函数的结果:\n\n```cpp\n#include <boost/static_assert.hpp> \n#include <boost/type_traits/is_same.hpp> \nBOOST_STATIC_ASSERT((\n    boost::is_same<BOOST_TYPEOF(add(1, 1)), int>::value\n));\n```\n\n然而不幸的是，没有帮助，这种魔力并不总是有效的。例如，用户定义的类并不总是被检测到，因此以下代码在某些编译器上可能会失败:\n\n```cpp\nnamespace readers_project { \n    template <class T1, class T2, class T3> \n    struct readers_template_class{}; \n} \n\n#include <boost/tuple/tuple.hpp> \n\ntypedef \n    readers_project::readers_template_class<int, int, float> \nreaders_template_class_1; \n\ntypedef BOOST_TYPEOF(boost::get<0>( \n    boost::make_tuple(readers_template_class_1(), 1) \n)) readers_template_class_deduced; \n\nBOOST_STATIC_ASSERT(( \n    boost::is_same< \n        readers_template_class_1, \n        readers_template_class_deduced \n    >::value \n));\n```\n\n在这种情况下，你可以向`Boost.Typeof`伸出援助之手，注册一个模板:\n\n```cpp\nBOOST_TYPEOF_REGISTER_TEMPLATE( \n        readers_project::readers_template_class /*class name*/, \n        3 /*number of template classes*/ \n)\n```\n\n然而，即使没有`BOOST_TYPEOF_REGISTER_TEMPLATE`和没有 C++ 11，三个最流行的编译器也能正确检测出类型。\n\n# 请参见\n\n`Boost.Typeof`的官方文档有更多的例子。跟随链接[http://boost.org/libs/typeof](http://boost.org/libs/typeof)阅读。"
  },
  {
    "path": "docs/boost-cpp-app-dev-cb/05.md",
    "content": "# 五、多线程操作\n\n在本章中，我们将介绍:\n\n*   创建执行线程\n*   同步对公共资源的访问\n*   使用原子快速访问公共资源\n*   创建工作队列类\n*   多读取器单写入器锁\n*   创建每个线程唯一的变量\n*   打断一根线\n*   操纵一组线程\n*   安全初始化共享变量\n*   锁定多个互斥体\n\n# 介绍\n\n在这一章中，我们将关注线程和所有与它们相关的东西。鼓励掌握多线程的基本知识。\n\n**多线程**是指单个进程内存在多个执行线程。线程可以共享进程资源并拥有自己的资源。这些执行线程可以在不同的 CPU 上独立运行，从而产生更快、更负责任的程序。`Boost.Thread`库提供跨操作系统接口的统一，以便与线程一起工作。它不是一个只有标题的库，所以本章的所有例子都需要链接到`libboost_thread`和`libboost_system`库。\n\n# 创建执行线程\n\n在现代多核编译器上，为了获得最大的性能(或者只是为了提供良好的用户体验)，程序通常使用多线程执行。这里有一个激励性的例子，我们需要在绘制用户界面的线程中创建并填充一个大文件:\n\n```cpp\n#include <cstddef> // for std::size_t\n\nbool is_first_run(); \n\n// Function that executes for a long time.\nvoid fill_file(char fill_char, std::size_t size, const char* filename);\n\n// Called in thread that draws a user interface:\nvoid example_without_threads() {\n    if (is_first_run()) {\n        // This will be executing for a long time during which\n        // users interface freezes...\n        fill_file(0, 8 * 1024 * 1024, \"save_file.txt\");\n    }\n}\n```\n\n# 准备好\n\n这个食谱需要了解`boost::bind`或`std::bind`。\n\n# 怎么做...\n\n启动一个执行线程从来没有这么容易:\n\n```cpp\n#include <boost/thread.hpp> \n\n// Called in thread that draws a user interface:\nvoid example_with_threads() {\n    if (is_first_run()) {\n        boost::thread(boost::bind(\n            &fill_file,\n            0,\n            8 * 1024 * 1024,\n            \"save_file.txt\"\n        )).detach();\n    }\n}\n```\n\n# 它是如何工作的...\n\n`boost::thread`变量接受一个可以在没有参数的情况下调用的函数对象(我们使用`boost::bind`提供了一个)，并创建一个单独的执行线程。这个函数对象被复制到一个构造好的执行线程中，并在那里运行。函数对象的返回值被忽略。\n\n![](img/00006.jpeg)\n\nWe are using version 4 of the `Boost.Thread` in all recipes (defined `BOOST_THREAD_VERSION` to `4`). Important differences between `Boost.Thread` versions are highlighted.\n\n之后，我们调用`detach()`函数，该函数执行以下操作:\n\n*   执行线程与`boost::thread`变量分离，但继续执行\n*   `boost::thread`变量开始保持`Not-A-Thread`状态\n\nWithout a call to `detach()`, the destructor of `boost::thread` will notice that it still holds an OS thread and will call `std::terminate`. It terminates our program without calling destructors, freeing up resources and without other cleanup.\n\n默认构造的线程也有一个`Not-A-Thread`状态，它们不会创建一个单独的执行线程。\n\n# 还有更多...\n\n如果我们想确保在做其他工作之前创建并编写了一个文件，该怎么办？在这种情况下，我们需要以下列方式连接线程:\n\n```cpp\nvoid example_with_joining_threads() {\n    if (is_first_run()) {\n        boost::thread t(boost::bind(\n            &fill_file,\n            0,\n            8 * 1024 * 1024,\n            \"save_file.txt\"\n        ));\n\n        // Do some work.\n        // ...\n\n        // Waiting for thread to finish.\n        t.join();\n    }\n} \n```\n\n线程连接后，`boost::thread`变量保持`Not-A-Thread`状态，其析构函数不调用`std::terminate`。\n\nRemember that the thread must be joined or detached before its destructor is called. Otherwise, your program will terminate!\n\n定义了`BOOST_THREAD_VERSION=2`后，`boost::thread`的析构函数调用`detach()`，并不会导致`std::terminate`。但是这样做会破坏与`std::thread`的兼容性，并且，有一天，当你的项目转移到 C++ 标准库线程时，或者当`BOOST_THREAD_VERSION=2`不被支持时，这会给你很多惊喜。`Boost.Thread`的版本 4 更显更强，在 C++ 语言中通常更可取。\n\nBeware that `std::terminate()` is called when any exception that is not of type `boost::thread_interrupted` leaves a boundary of the functional object that was passed to the `boost::thread` constructor.\n\n有一个非常有用的包装器，它作为线程周围的 RAII 包装器，允许您模拟`BOOST_THREAD_VERSION=2`行为；它被称为`boost::scoped_thread<T>`，其中`T`可以是以下类别之一:\n\n*   `boost::interrupt_and_join_if_joinable`:在破坏时中断并加入一个线程\n*   `boost::join_if_joinable`:在毁灭时连接一条线\n*   `boost::detach`:破坏时脱离一根线\n\n这里有一个简短的例子:\n\n```cpp\n#include <boost/thread/scoped_thread.hpp> \n\nvoid some_func(); \n\nvoid example_with_raii() { \n    boost::scoped_thread<boost::join_if_joinable> t( \n        boost::thread(&some_func) \n    ); \n\n    // 't' will be joined at scope exit.\n} \n```\n\n`boost::thread`类被接受为 C++ 11 标准的一部分，您可以在`std::`命名空间的`<thread>`头中找到它。Boost 的 Version 4 和 C++ 11 标准库版本的`thread`类没有太大区别。但是，`boost::thread`在 C++ 03 编译器上是可用的，所以它的用法更加通用。\n\nThere is a very good reason for calling `std::terminate` instead of joining by default! C and C++ languages are often used in life critical software. Such software is controlled by other software, called **watchdog**. Those watchdogs can easily detect that an application has terminated but can not always detect deadlocks or detect them with longer delays. For example, for defibrillator software, it's safer to terminate, than hang on `join()` for a few seconds waiting for a watchdog reaction. Keep that in mind when designing such applications.\n\n# 请参见\n\n*   本章所有食谱均使用`Boost.Thread`。你可以继续阅读以获得更多关于图书馆的信息。\n*   官方文档有一个完整的`boost::thread`方法列表，以及关于它们在 C++ 11 标准库中可用性的注释。按照链接[http://boost.org/libs/thread](http://boost.org/libs/thread)获取其官方文档。\n*   *中断一个线程*食谱会让你知道`boost::interrupt_and_join_if_joinable`类做什么。\n\n# 同步对公共资源的访问\n\n既然我们知道了如何启动执行线程，我们希望能够从不同的线程访问一些公共资源:\n\n```cpp\n#include <cassert> \n#include <cstddef> \n#include <iostream>\n\n// In previous recipe we included \n// <boost/thread.hpp>, which includes all \n// the classes of Boost.Thread.\n// Following header includes only boost::thread. \n#include <boost/thread/thread.hpp> \n\nint shared_i = 0;\n\nvoid do_inc() {\n    for (std::size_t i = 0; i < 30000; ++ i) {\n        const int i_snapshot = ++ shared_i;\n        // Do some work with i_snapshot.\n        // ...\n    }\n}\n\nvoid do_dec() {\n    for (std::size_t i = 0; i < 30000; ++ i) {\n        const int i_snapshot = --shared_i;\n        // Do some work with i_snapshot.\n        // ...\n    }\n}\n\nvoid run() {\n    boost::thread t1(&do_inc);\n    boost::thread t2(&do_dec);\n\n    t1.join();\n    t2.join();\n\n    assert(global_i == 0); // Oops!\n    std::cout << \"shared_i == \" << shared_i;\n}\n```\n\n这个`Oops!`不是偶然写在那里的。对于有些人来说，可能是惊喜，但是`shared_i`不等于`0`的可能性很大:\n\n```cpp\n    shared_i == 19567\n```\n\nModern compilers and processors have a huge number of different tricky optimizations that can break the preceding code. We won't discuss them here, but there is a useful link in the *See also* section of the document that briefly describes them.\n\n当一个公共资源是非平凡类时，情况会变得更糟；分段错误和内存泄漏可能(也将)发生。\n\n我们需要更改代码，以便在一个时刻只有一个线程修改`shared_i`变量，并绕过所有导致多线程代码的处理器和编译器优化。\n\n# 准备好\n\n这个食谱推荐螺纹的基本知识。\n\n# 怎么做...\n\n让我们看看如何修复前面的例子，并在运行结束时使`shared_i`相等:\n\n1.  首先，我们需要创建一个**互斥体**:\n\n```cpp\n#include <boost/thread/mutex.hpp> \n#include <boost/thread/locks.hpp> \n\nint shared_i = 0; \nboost::mutex i_mutex; \n```\n\n2.  将所有修改或从`shared_i`变量中获取数据的操作置于以下之间:\n\n```cpp\n      {   // Critical section begin \n          boost::lock_guard<boost::mutex> lock(i_mutex); \n```\n\n以及以下内容:\n\n```cpp\n      }   // Critical section end \n```\n\n它应该是这样的:\n\n```cpp\nvoid do_inc() {\n    for (std::size_t i = 0; i < 30000; ++ i) {\n        int i_snapshot;\n        { // Critical section begin.\n            boost::lock_guard<boost::mutex> lock(i_mutex);\n            i_snapshot = ++ shared_i;\n        } // Critical section end.\n\n        // Do some work with i_snapshot.\n        // ...\n    }\n}\n\nvoid do_dec() {\n    for (std::size_t i = 0; i < 30000; ++ i) {\n        int i_snapshot;\n        { // Critical section begin.\n            boost::lock_guard<boost::mutex> lock(i_mutex);\n            i_snapshot = -- shared_i;\n        } // Critical section end.\n\n        // Do some work with i_snapshot.\n        // ...\n    }\n} \n```\n\n# 它是如何工作的...\n\n`boost::mutex`类负责所有的同步工作。当一个线程试图通过`boost::lock_guard<boost::mutex>`变量锁定它，并且没有其他线程持有锁时，它成功地获得对代码段的唯一访问，直到锁被解锁或销毁。如果某个其他线程已经持有锁，则尝试获取锁的线程会等待，直到另一个线程解锁锁。所有的锁定/解锁操作都暗示了特定的指令，以便所有线程都可以看到关键部分中所做的更改。此外，您不再需要:\n\n*   确保修改后的资源值对所有内核可见\n*   确保值不只是在处理器的寄存器中被修改\n*   强制处理器不要对指令重新排序\n*   强制编译器不要对指令重新排序\n*   强制编译器不删除对未读取的存储的写入\n*   一堆其他讨厌的编译器/架构特定的东西\n\nIf you have a variable that is used from different threads and at least one thread modifies that variable, usually, all the code that uses it must be treated as a critical section and secured by a mutex.\n\n`boost::lock_guard`类是一个非常简单的 RAII 类，它存储对互斥体的引用，将其锁定在单参数构造函数中，并在析构函数中解锁。\n\n在前面例子中的花括号用法中，`lock`变量是在它们内部构造的，这样在到达`// Critical section end.`结束括号时，`lock`变量的析构函数被调用，互斥体被解锁。即使在关键部分发生了一些异常，互斥体也是正确解锁的。\n\n![](img/00007.jpeg)\n\nIf you initialize a common variable and then construct threads that only read it, then no mutex or other synchronization is required.\n\n# 还有更多...\n\n锁定互斥可能是一个非常缓慢的操作，这可能会使您的代码停止很长时间，直到其他线程释放锁。尽量使关键部分尽可能小；尝试在代码中减少它们。\n\n让我们来看看一些操作系统如何处理多核 CPU 上的锁定。当运行在 CPU 1 上的`thread #1`试图锁定已经被另一个线程锁定的互斥体时，`thread #1`被操作系统停止，直到锁被释放。停止的线程不会消耗处理器资源，因此操作系统会在 CPU 1 上执行其他线程。现在，我们有一些线程在 CPU 1 上运行；一些其他线程释放锁，现在操作系统必须恢复执行一个`thread #1`。因此，它在当前空闲的 CPU 上恢复执行，例如，CPU2。\n\n这将导致 CPU 缓存未命中，因此在释放互斥体后，代码运行稍慢。通常情况下，事情没有那么糟糕，因为一个好的操作系统会努力在它之前使用的同一个中央处理器上恢复线程。不幸的是，这种特定于操作系统的优化并不总是可行的。减少关键部分的数量及其大小，以减少线程挂起和缓存未命中的机会。\n\n不要试图在同一个线程中锁定一个`boost::mutex`变量两次；这会导致**僵局**。如果需要从单个线程多次锁定互斥体，请使用`<boost/thread/recursive_mutex.hpp>`头中的`boost::recursive_mutex`代替。多次锁定它不会导致死锁。`boost::recursive_mutex`只有在每次`lock()`调用`unlock()`一次后才会解除锁定。不需要的时候避免使用`boost::recursive_mutex`，因为比`boost::mutex`慢，通常表示代码流设计不好。\n\n`boost::mutex`、`boost::recursive_mutex`和`boost::lock_guard`类被 C++ 11 标准库接受，您可以在`std::`命名空间的`<mutex>`头中找到它们。Boost 和标准库版本之间没有太大区别。Boost 版本可能有一些扩展(在官方文档中标记为 *EXTENSION* )并提供更好的可移植性，因为它们甚至可以在 C++ 11 之前的编译器上使用。\n\n# 请参见\n\n*   下一个食谱将告诉你如何让这个例子更快(更短)。\n*   阅读本章第一个食谱，了解更多关于`boost::thread`课程的信息。http://boost.org/libs/thread 的官方文件也可能对你有所帮助。\n*   要了解第一个例子失败的原因以及多处理器如何使用公共资源的更多信息，请参见[上的*内存障碍:软件黑客的硬件视图*。注意，这是一个很难的话题。](http://www.rdrop.com/users/paulmck/scalability/paper/whymb.2010.07.23a.pdf)\n\n# 使用原子快速访问公共资源\n\n在前面的食谱中，我们看到了如何从不同的线程安全地访问公共资源。但是在这个配方中，我们进行了两次系统调用(锁定和解锁`mutex`)来获取一个整数的值:\n\n```cpp\n{   // Critical section begin.\n    boost::lock_guard<boost::mutex> lock(i_mutex); \n    i_snapshot = ++ shared_i; \n}   // Critical section end.\n```\n\n这看起来又跛又慢！我们能让之前食谱的代码更好吗？\n\n# 准备好\n\n阅读第一份食谱是你从这份食谱开始所需要的。多线程的一些基本知识将是受欢迎的。\n\n# 怎么做...\n\n让我们看看如何改进前面的例子:\n\n1.  现在，我们需要不同的标题:\n\n```cpp\n#include <cassert> \n#include <cstddef> \n#include <iostream>\n\n#include <boost/thread/thread.hpp> \n#include <boost/atomic.hpp> \n```\n\n2.  需要改变`shared_i`的类型:\n\n```cpp\nboost::atomic<int> shared_i(0); \n```\n\n3.  移除所有`boost::lock_guard`变量:\n\n```cpp\nvoid do_inc() {\n    for (std::size_t i = 0; i < 30000; ++ i) {\n        const int i_snapshot = ++ shared_i;\n\n        // Do some work with i_snapshot.\n        // ...\n    }\n}\n\nvoid do_dec() {\n    for (std::size_t i = 0; i < 30000; ++ i) {\n        const int i_snapshot = -- shared_i;\n\n        // Do some work with i_snapshot.\n        // ...\n    }\n}\n```\n\n4.  就这样！现在，它起作用了:\n\n```cpp\nint main() {\n    boost::thread t1(&do_inc);\n    boost::thread t2(&do_dec);\n\n    t1.join();\n    t2.join();\n\n    assert(shared_i == 0);\n    std::cout << \"shared_i == \" << shared_i << std::endl;\n\n    assert(shared_i.is_lock_free());\n}\n```\n\n# 它是如何工作的...\n\n处理器提供特定的**原子操作**，不能被其他处理器或处理器内核干扰。对于系统来说，这些操作似乎是瞬间发生的。`Boost.Atomic`提供围绕系统特定原子操作的类，与编译器合作禁用可能破坏多线程工作的优化，并提供统一的可移植接口来处理原子操作。如果在同一个内存位置上的两个原子操作从不同的线程同时开始，其中一个操作会一直等待，直到另一个操作完成，然后再使用前一个操作的结果。\n\n![](img/00008.jpeg)\n\n换句话说，同时使用来自不同线程的`boost::atomic<>`变量是安全的。系统将原子变量上的每个操作视为单个事务。系统将原子变量上的一系列操作视为一系列独立的事务:\n\n```cpp\n--shared_i;    // Transaction #1 \n// Some other may change value of `shared_i`!!\n++ shared_i;    // Transaction #2 \n```\n\nNever ever avoid synchronization for a variable that is modified from multiple threads. Even if the variable is a `bool` and all you do is read or write `true`/`false` to it! The compiler has all the rights to optimize away all the stores and reads, breaking your code in a million ways that nobody can even imagine. Guess whom a good employer would punish for such breakage? (The compiler is not the right answer to that question!)\n\n# 还有更多...\n\n`Boost.Atomic`库只能使用 POD 类型；否则，行为未定义。某些平台/处理器不为某些类型提供原子操作，因此`Boost.Atomic`使用`boost::mutex`模拟原子行为。如果类型特定宏设置为`2`，原子类型不使用`boost::mutex`:\n\n```cpp\n#include <boost/static_assert.hpp> \nBOOST_STATIC_ASSERT(BOOST_ATOMIC_INT_LOCK_FREE == 2); \n```\n\n`boost::atomic<T>::is_lock_free`成员函数依赖于运行时，因此它不适用于编译时检查，但当运行时检查足够时，它可能会提供更可读的语法:\n\n```cpp\nassert(shared_i.is_lock_free()); \n```\n\n原子操作比互斥操作快得多，但仍然比非原子操作慢得多。如果我们比较一个使用互斥体的配方的执行时间(0:00.08 秒)和这个配方中前一个例子的执行时间(0:00.02 秒)，我们会看到差异(在 30，000 次迭代中测试)。\n\nAll the known to the book author standard library implementations had issues with atomic operations. All of them! Do not write your own atomics. If you think that your own implementation of atomics would be better and you wish to waste some time--write it, check it using special tools, and think again. Repeat until you understand that you're wrong.\n\nC++ 11 兼容编译器应该在`std::`命名空间的`<atomic>`头中拥有所有原子类、`typedefs`和宏。如果编译器正确支持 C++ 11 内存模型，并且经过专门训练来优化`std::atomic`变量，那么`std::atomic`的编译器特定实现可能比 Boost 的版本工作得更快。\n\n# 请参见\n\n[http://boost.org/libs/atomic](http://boost.org/libs/atomic)的官方文档可能会给你更多关于这个主题的例子和一些理论信息。\n\n# 正在创建工作队列类\n\n让我们调用不带参数的函数对象(简称任务)。\n\n```cpp\ntypedef boost::function<void()> task_t; \n```\n\n现在，想象一下我们有发布任务的线程和执行发布任务的线程的情况。我们需要设计一个可以被这两种类型的线程安全使用的类。此类必须具有以下功能:\n\n*   获取任务或等待任务直到它被另一个线程发布\n*   如果我们有任务，检查并获取一个任务(如果没有剩余任务，返回一个空任务)\n*   发布任务\n\n# 准备好\n\n确保您对`boost::thread`或`std::thread`感到舒适，了解互斥体的基础知识，并且知道`boost::function`或`std::function`。\n\n# 怎么做...\n\n我们要实现的类在功能上接近`std::queue<task_t>`，但也有线程同步。让我们开始吧:\n\n1.  我们需要以下标题和成员:\n\n```cpp\n#include <deque>\n#include <boost/function.hpp>\n#include <boost/thread/mutex.hpp>\n#include <boost/thread/locks.hpp>\n#include <boost/thread/condition_variable.hpp>\n\nclass work_queue {\npublic:\n    typedef boost::function<void()> task_type;type;\n\nprivate:\n    std::deque<task_type> tasks_;\n    boost::mutex tasks_mutex_;\n    boost::condition_variable cond_;\n```\n\n2.  将任务放入队列的函数必须如下所示:\n\n```cpp\npublic:\n    void push_task(const task_type& task) {\n        boost::unique_lock<boost::mutex> lock(tasks_mutex_);\n        tasks_.push_back(task);\n        lock.unlock();\n\n        cond_.notify_one();\n    }\n```\n\n3.  用于获取推送任务或空任务(如果没有剩余任务)的非阻塞功能:\n\n```cpp\n    task_type try_pop_task() {\n        task_type ret;\n        boost::lock_guard<boost::mutex> lock(tasks_mutex_);\n        if (!tasks_.empty()) {\n            ret = tasks_.front();\n            tasks_.pop_front();\n        }\n\n        return ret;\n    }\n```\n\n4.  阻止功能，用于获取推送的任务或在任务被另一个线程推送时进行阻止:\n\n```cpp\n    task_type pop_task() {\n        boost::unique_lock<boost::mutex> lock(tasks_mutex_);\n        while (tasks_.empty()) {\n            cond_.wait(lock);\n        }\n\n        task_type ret = tasks_.front();\n        tasks_.pop_front();\n\n        return ret;\n    }\n}; \n```\n\n以下是如何使用`work_queue`类:\n\n```cpp\n#include <boost/thread/thread.hpp>\n\nwork_queue g_queue;\n\nvoid some_task();\nconst std::size_t tests_tasks_count = 3000 /*000*/;\n\nvoid pusher() {\n    for (std::size_t i = 0; i < tests_tasks_count; ++ i) {\n        g_queue.push_task(&some_task);\n    }\n}\n\nvoid popper_sync() {\n    for (std::size_t i = 0; i < tests_tasks_count; ++ i) {\n        work_queue::task_type t = g_queue.pop_task();\n        t();         // Executing task.\n    }\n}\n\nint main() {\n    boost::thread pop_sync1(&popper_sync);\n    boost::thread pop_sync2(&popper_sync);\n    boost::thread pop_sync3(&popper_sync);\n\n    boost::thread push1(&pusher);\n    boost::thread push2(&pusher);\n    boost::thread push3(&pusher);\n\n    // Waiting for all the tasks to push.\n    push1.join();\n    push2.join();\n    push3.join();\n    g_queue.flush(); \n\n    // Waiting for all the tasks to pop.\n    pop_sync1.join();\n    pop_sync2.join();\n    pop_sync3.join();\n\n    // Asserting that no tasks remained,\n    // and falling though without blocking.\n    assert(!g_queue.try_pop_task());\n\n    g_queue.push_task(&some_task);\n\n    // Asserting that there is a task,\n    // and falling though without blocking.\n    assert(g_queue.try_pop_task());\n}\n```\n\n# 它是如何工作的...\n\n在这个例子中，我们看到了一个新的 RAII 类`boost::unique_lock`。它只是一个`boost::lock_guard`类，具有显式解锁和锁定互斥锁的附加功能。\n\n回到我们的`work_queue`班。先说`pop_task()`功能。首先，我们获取一个锁，并检查可用的任务。如果有任务，我们就退回去；否则，`cond_.wait(lock)`就叫。此方法自动解锁锁，并暂停执行线程，直到其他线程通知当前线程。\n\n现在，我们来看看`push_task`法。在其中，我们还获取一个锁，在`tasks_.queue`中推送一个任务，解锁锁，调用`cond_.notify_one()`，唤醒`cond_.wait(lock)`中等待的线程(如果有)。因此，在此之后，如果某个线程正在等待一个`pop_task()`方法中的条件变量，该线程将继续执行，在`cond_.wait(lock)`内部调用`lock.lock()`，并在`while`中检查`tasks_empty()`。因为我们刚刚在`tasks_`中添加了一个任务，我们将从`while`循环中退出，解锁`<mutex>`(变量`lock`超出范围)，并返回一个任务。\n\n![](img/00009.jpeg)\n\nYou must check conditions in a loop, not just in an `if` statement! The `if` statement leads to errors, as the operating system sometimes may wake up the threads without any notify calls from the user.\n\n# 还有更多...\n\n注意，我们在调用`notify_one()`之前明确解锁了互斥体。然而，没有解锁，我们的例子仍然有效。\n\n但是，在这种情况下，已经唤醒的线程可能会在试图调用`cond_wait(lock)`内部的`lock.lock()`时再次被阻塞，这将导致更多的上下文切换和更差的性能。\n\n将`tests_tasks_count`设置为`3000000`并且不进行显式解锁，本示例运行 7 秒钟:\n\n```cpp\n $time -f E ./work_queue\n 0:07.38\n```\n\n通过显式解锁，此示例运行 5 秒钟:\n\n```cpp\n $ time -f E ./work_queue \n 0:05.39\n```\n\n您也可以使用`cond_.notify_all()`通知所有等待特定条件变量的线程。\n\nSome extremely exotic operating systems had an extremely rare issue with calling `notify_one()` outside the critical section (without holding a lock) on Boost before version 1.64 [https://github.com/boostorg/thread/pull/105](https://github.com/boostorg/thread/pull/105). It's doubtful that you will ever work with those. But, anyway, to avoid issues on those platforms, you may add a `flush()` function to the `work_queue` class that holds a lock and calls `notify_all()`:\n`void flush() {` `boost::lock_guard<boost::mutex> lock(tasks_mutex_);` `cond_.notify_all();` `}`\nCall `flush()` when you are done with pushing tasks in a queue to force the wakeup of all the threads.\n\nC++ 11 标准在`<condition_variable>`头中声明了`std::condition_variable`，在`<mutex>`头中声明了`std::unique_lock`。如果您使用 C++ 03 编译器或仅使用 Boost 的一些扩展，请使用 Boost 版本。\n\nThe `work_queue` class could be significantly improved by adding support for **rvalue references** and calling `std::move(tasks_.front())`. This will make the code in the critical section much faster, resulting in less threads, suspends, and wakeups, less cache misses and a much better performance.\n\n# 请参见\n\n*   本章的前三个食谱提供了很多关于`Boost.Thread`的有用信息\n*   官方文档可能会给你更多关于这个主题的例子和一些理论信息；可以在[http://boost.org/libs/thread](http://boost.org/libs/thread)找到\n\n# 多读取器单写入器锁\n\n想象一下，我们正在开发一些在线服务。我们有一个无序的注册用户地图，每个用户都有一些属性。这个集合被许多线程访问，但是很少被修改。具有以下设置的所有操作都是以线程安全的方式完成的:\n\n```cpp\n#include <unordered_map> \n#include <boost/thread/mutex.hpp> \n#include <boost/thread/locks.hpp> \n\nstruct user_info {\n    std::string address;\n    unsigned short age;\n\n    // Other parameters\n    // ...\n};\n\nclass users_online {\n    typedef boost::mutex mutex_t;\n\n    mutable mutex_t                             users_mutex_;\n    std::unordered_map<std::string, user_info>  users_;\n\npublic:\n    bool is_online(const std::string& username) const {\n        boost::lock_guard<mutex_t> lock(users_mutex_);\n        return users_.find(username) != users_.end();\n    }\n\n    std::string get_address(const std::string& username) const {\n        boost::lock_guard<mutex_t> lock(users_mutex_);\n        return users_.at(username).address;\n    }\n\n    void set_online(const std::string& username, user_info&& data) {\n        boost::lock_guard<mutex_t> lock(users_mutex_);\n        users_.emplace(username, std::move(data));\n    }\n\n    // Other methods:\n    // ...\n};\n```\n\n不幸的是，我们的在线服务有点慢，分析人员显示问题出在`users_online`类。任何操作都会在`mutex_`变量上获得一个唯一的锁，所以即使获得资源也会导致等待一个锁定的互斥体。由于一些资源很难复制，关键部分消耗了大量时间，减慢了`users_online`类的任何操作。\n\n不幸的是，项目需求不允许我们重新设计类。我们能在不改变界面的情况下加快速度吗？\n\n# 准备好\n\n确保您对`boost::thread`或`std::thread`感到舒适，并了解互斥体的基本知识。\n\n# 怎么做...\n\n这可能会有所帮助:\n\n将`boost::mutex`替换为`boost::shared_mutex`。对于不修改数据的方法，将`boost::unique_locks`替换为`boost::shared_lock`:\n\n```cpp\n#include <boost/thread/shared_mutex.hpp> \n\nclass users_online {\n    typedef boost::shared_mutex mutex_t;\n\n    mutable mutex_t                             users_mutex_;\n    std::unordered_map<std::string, user_info>  users_;\n\npublic:\n    bool is_online(const std::string& username) const {\n        boost::shared_lock<mutex_t> lock(users_mutex_);\n        return users_.find(username) != users_.end();\n    }\n\n    std::string get_address(const std::string& username) const {\n        boost::shared_guard<mutex_t> lock(users_mutex_);\n        return users_.at(username).address;\n    }\n\n    void set_online(const std::string& username, user_info&& data) {\n        boost::lock_guard<mutex_t> lock(users_mutex_);\n        users_.emplace(username, std::move(data));\n    }\n\n    // Other methods:\n    // ...\n};\n```\n\n# 它是如何工作的...\n\n如果多个线程不修改数据，我们可以允许同时从这些线程获取数据。只有当我们要修改互斥体保护的数据时，我们才需要唯一地拥有互斥体。在所有其他情况下，允许同时访问数据。这就是`boost::shared_mutex`的设计目的。它允许共享锁定(读锁定)，这允许多个同时访问资源。\n\n当我们尝试唯一锁定共享锁定的资源时，操作将被阻止，直到没有剩余的读锁定，并且仅在该资源被唯一锁定后，强制新的共享锁定等待，直到唯一锁定被释放。`boost::shared_lock`读写锁定比通常的`boost::mutex`锁定慢很多。不要使用`boost::shared_lock`，除非你确定没有好的方法来重新设计你的代码，并且你确定`boost::shared_lock`会加快速度。\n\nSome readers may see the `mutable` keyword for the first time. This keyword can be applied to non-static and non-constant class members. The `mutable` data member can be modified in the constant member functions and is usually used for mutexes and other helper variables that are not directly related to the class logic.\n\n# 还有更多...\n\n当你确实只需要唯一的锁时，不要使用`boost::shared_mutex`，因为它比通常的`boost::mutex`类慢。\n\n在 C++ 14 之前，共享互斥体在 C++ 中是不可用的。`shared_timed_mutex`和`shared_lock`在`std::`命名空间的`<shared_mutex>`头中定义。它们具有与 Boost 版本相近的性能特征，因此应用前面所有的性能注释。\n\nC++ 17 的`shared_mutex`可能比`shared_timed_mutex`稍快，因为它没有提供定时锁定的方法。这可能会为你节省几个宝贵的纳秒。\n\n# 请参见\n\n*   还有一个`boost::upgrade_mutex`类，对于共享锁需要提升为唯一锁的情况可能有用。更多信息请参见[http://boost.org/libs/thread](http://boost.org/libs/thread)的`Boost.Thread`文档。\n*   有关可变关键字的更多信息，请参考[http://herbsutter . com/2013/01/01/video-you-know-const-and-mutatable/](http://herbsutter.com/2013/01/01/video-you-dont-know-const-and-mutable/)。\n\n# 创建每个线程唯一的变量\n\n我们来看一下食谱*创建* *工作队列类*。那里的每个任务都可以在众多线程中的一个线程中执行，我们不知道在哪个线程中执行。假设我们希望使用某种连接发送已执行任务的结果:\n\n```cpp\n#include <boost/noncopyable.hpp>\n\nclass connection: boost::noncopyable {\npublic:\n    // Opening a connection is a slow operation\n    void open();\n\n    void send_result(int result);\n\n    // Other methods\n    // ...\n};\n```\n\n我们有以下解决方案:\n\n*   当我们需要发送数据时，打开一个新的连接(这非常慢)\n*   为所有线程建立一个单一连接，并将它们包装在互斥体中(这也很慢)\n*   拥有一个连接池，以线程安全的方式从其中获取一个连接，并使用它(需要大量的编码，但是这个解决方案很快)\n*   每个线程只有一个连接(实现起来又快又简单)\n\n那么，我们如何实现最后一个解决方案呢？\n\n# 准备好\n\n需要线程的基本知识。\n\n# 怎么做...\n\n是时候创建一个线程局部变量了。在头文件`connection`类定义后声明一个函数:\n\n```cpp\nconnection& get_connection();\n```\n\n使您的源文件如下所示:\n\n```cpp\n#include <boost/thread/tss.hpp>\nboost::thread_specific_ptr<connection> connection_ptr;\n\nconnection& get_connection() {\n    connection* p = connection_ptr.get();\n    if (!p) {\n        connection_ptr.reset(new connection);\n        p = connection_ptr.get();\n        p->open();\n    }\n\n    return *p;\n}\n```\n\n完成了。使用线程特定的资源从来没有这么容易过:\n\n```cpp\nvoid task() {\n    int result;\n    // Some computations go there.\n    // ...\n\n    // Sending the result:\n    get_connection().send_result(result);\n}\n```\n\n# 它是如何工作的...\n\n`boost::thread_specific_ptr`变量为每个线程保存一个单独的指针。最初，这个指针等于`nullptr`；这就是为什么我们检查`!p`并打开一个连接，如果它是`nullptr`。\n\n因此，当我们从已经启动指针的线程进入`get_connection()`时，`!p`返回值`false`，我们返回已经打开的连接。\n\n`delete`对于存储在`connection_ptr`变量里面的指针是在线程退出的时候调用的，所以我们不需要担心内存泄漏。\n\n# 还有更多...\n\n您可以提供自己的清理函数，该函数将在线程退出时被调用而不是`delete`。清理功能必须有`void (*cleanup_function)(T*)`签名，并且必须在`boost::thread_specific_ptr`施工期间通过。\n\nC++ 11 有一个特殊的关键字`thread_local`，用来声明带有线程本地存储持续时间的变量。C++ 11 没有`thread_specific_ptr`类，但是您可以使用`thread_local T`或`thread_local std::unique_ptr<T>`在支持`thread_local`的编译器上实现相同的行为。`boost::thread_specific_ptr`在 C++ 11 之前的编译器上工作，不像`thread_local`。\n\nC++ 17 有`inline`变量，可以用`thread_local`搭配`inline`在头文件中声明线程局部变量。\n\n# 请参见\n\n*   `Boost.Thread`文档给出了很多不同案例的好例子；可以在[http://boost.org/libs/thread](http://boost.org/libs/thread)找到\n*   在[http://stackoverflow . com/questions/13106049/c11-gcc-4-8-Thread-local-performance-pension . html](http://stackoverflow.com/questions/13106049/c11-gcc-4-8-thread-local-performance-penalty.html)阅读本主题，在[http://gcc . GNU . org/online docs/gcc-3 . 3 . 1/gcc/Thread-local . html](http://gcc.gnu.org/onlinedocs/gcc-3.3.1/gcc/Thread-Local.html)阅读关于 GCCs `__thread`关键字，可能会让你对`thread_local`在编译器中的实现方式和速度有所了解\n\n# 打断一根线\n\n有时，我们需要终止一个消耗了太多资源或者执行时间过长的线程。例如，一些解析器在一个线程中工作(并且主动使用`Boost.Thread`，但是我们已经从它那里获得了所需的数据量，所以解析可以停止。这是存根:\n\n```cpp\nint main() {\n    boost::thread parser_thread(&do_parse);\n\n    // ...\n\n    if (stop_parsing) {\n        // No more parsing required.\n        // TODO: Stop the parser!\n    }\n\n    // ...\n\n    parser_thread.join();\n}\n```\n\n我们怎么做？\n\n# 准备好\n\n这个食谱几乎不需要任何东西。你只需要至少对线程有一个基本的了解。\n\n# 怎么做...\n\n我们可以通过中断线程来停止它:\n\n```cpp\nif (stop_parsing) { \n    // No more parsing required. \n    parser_thread.interrupt(); \n}\n```\n\n# 它是如何工作的...\n\n`Boost.Thread`提供一些预定义的**中断点**，通过`interrupt()`调用检查线程是否被中断。如果线程被中断，则抛出异常`boost::thread_interrupted`。当异常通过`do_parse()`内部传播时，它调用所有资源的析构函数，就像典型的异常一样。`boost::thread_interrupted`异常由`Boost.Thread`库专门处理，对于该异常，允许离开线程函数(在我们的示例中为`do_parse()`)。当异常离开线程函数时，它被`boost::thread`内部捕获，并被视为取消线程的请求。\n\n`boost::thread_interrupted` is not derived from `std::exception`! Interruptions work well if you catch exceptions by their type or by references to `std::exception`. But if you catch an exception by `catch (...)` and do not rethrow it, the interruptions won't work.\n\n正如我们从本章的第一个食谱中所知道的，如果传递到线程中的函数没有捕获到异常，并且该异常离开了函数边界，应用就会终止。`boost::thread_interrupted`是那条规则的唯一例外；它可能会离开函数边界，并且不会`std::terminate()`应用。\n\n# 还有更多...\n\n官方文件中列出了`Boost.Thread`库的中断点。根据经验，所有阻挡的东西都会检查是否有干扰。\n\n我们也可以在任何地方手动添加中断点。我们所需要的就是呼叫`boost::this_thread::interruption_point()`:\n\n```cpp\nvoid do_parse() {\n    while (not_end_of_parsing) {\n        // If current thread was interrupted, the following\n        // line will throw an boost::thread_interrupted.\n        boost::this_thread::interruption_point();\n\n        // Some parsing goes here.\n        // ...\n    }\n}\n```\n\n如果一个项目不需要中断，定义`BOOST_THREAD_DONT_PROVIDE_INTERRUPTIONS`可以稍微提高性能，并且完全禁止线程中断。\n\nC++ 11 没有线程中断，但是您可以使用原子操作来部分模拟它们:\n\n*   创建一个原子`bool`变量\n*   检查线程中的原子变量，如果它已经改变，就抛出一个异常\n*   不要忘记捕捉传递给线程的函数中的异常(否则应用将终止)\n\n但是，如果代码在条件变量或 sleep 方法中的某个地方等待，这将对您没有帮助。\n\n# 请参见\n\n*   `Boost.Thread`的官方文档在[提供了预定义的中断点列表](http://www.boost.org/doc/libs/1_64_0/doc/html/thread/thread_management.html#thread.thread_management.tutorial.interruption.predefined_interruption_points)\n*   作为练习，查看本章中的其他方法，并考虑在哪些地方增加中断点可以改进代码\n*   阅读`Boost.Thread`文档的其他部分可能会有用；前往[http://boost.org/libs/thread](http://boost.org/libs/thread)\n\n# 操纵一组线程\n\n那些试图自己重复所有例子的读者，或者那些正在尝试线程的读者，一定已经厌倦了编写下面的代码来启动和连接线程:\n\n```cpp\n#include <boost/thread.hpp>\n\nvoid some_function();\n\nvoid sample() {\n    boost::thread t1(&some_function);\n    boost::thread t2(&some_function);\n    boost::thread t3(&some_function);\n\n    // ... \n\n    t1.join();\n    t2.join();\n    t3.join();\n} \n```\n\n也许有更好的方法做到这一点？\n\n# 准备好\n\n对于这个食谱来说，基本的线程知识就足够了。\n\n# 怎么做...\n\n我们可以使用`boost::thread_group`类操纵一组线程。\n\n1.  构造一个`boost::thread_group`变量:\n\n```cpp\n#include <boost/thread.hpp>\n\nint main() {\n    boost::thread_group threads;\n```\n\n2.  在前面的变量中创建线程:\n\n```cpp\n    // Launching 10 threads.\n    for (unsigned i = 0; i < 10; ++ i) {\n        threads.create_thread(&some_function);\n    }\n```\n\n3.  现在，您可以为`boost::thread_group`内的所有线程调用函数:\n\n```cpp\n    // Joining all threads.\n    threads.join_all();\n\n    // We can also interrupt all of them\n    // by calling threads.interrupt_all();\n}\n```\n\n# 它是如何工作的...\n\n`boost::thread_group`变量只保存所有构造或移动到它的线程，并可能向所有线程发送一些调用。\n\n# 还有更多...\n\nC++ 11 没有`thread_group`类；它是特定于 Boost 的。\n\n# 请参见\n\n`Boost.Thread`的官方文档可能会给你带来很多本章没有描述的其他有用的类；前往[http://boost.org/libs/thread](http://boost.org/libs/thread)。\n\n# 安全初始化共享变量\n\n假设我们正在设计一个安全关键类，该类从多个线程使用，从服务器接收答案，对它们进行后处理，并输出响应:\n\n```cpp\nstruct postprocessor {\n    typedef std::vector<std::string> answer_t;\n\n    // Concurrent calls on the same variable are safe.\n    answer_t act(const std::string& in) const {\n        if (in.empty()) {\n            // Extremely rare condition.\n            return read_defaults();\n        }\n\n        // ...\n    }\n};\n```\n\n注意`return read_defaults();`线。由于网络问题或其他一些问题，可能会出现服务器没有响应的情况。在这些情况下，我们尝试从文件中读取默认值:\n\n```cpp\n// Executes for a long time.\nstd::vector<std::string> read_defaults();\n```\n\n从前面的代码中，我们遇到了这个问题:服务器可能在一段明显的时间内不可访问，在这段时间里，我们将在每次`act`调用时重新读取文件。这极大地影响了性能。\n\n我们可以尝试通过在类中存储`default_`来修复它:\n\n```cpp\nstruct postprocessor {\n    typedef std::vector<std::string> answer_t;\n\nprivate:\n    answer_t default_;\n\npublic:\n    postprocessor()\n        : default_(read_defaults())\n    {}\n\n    // Concurrent calls on the same variable are safe.\n    answer_t act(const std::string& in) const {\n        if (in.empty()) {\n            // Extremely rare condition.\n            return default_;\n        }\n\n        // ...\n    }\n};\n```\n\n这也不是一个完美的解决方案:我们不知道`postprocessor`类有多少实例是由用户构建的，我们在运行期间可能不需要的默认值上浪费了内存。\n\n因此，我们必须在第一次远程服务器故障时并发安全地读取和存储当前实例中的数据，并且在下一次故障时不再读取。有很多方法可以做到这一点，但让我们看看最正确的方法。\n\n# 准备好\n\n对于这个食谱来说，线程的基础知识已经足够了。\n\n# 怎么做...\n\n1.  我们必须添加变量来存储初始化默认值的信息，并添加变量来存储默认值:\n\n```cpp\n#include <boost/thread/once.hpp>\n\nstruct postprocessor {\n    typedef std::vector<std::string> answer_t;\n\nprivate:\n    mutable boost::once_flag default_flag_;\n    mutable answer_t default_;\n```\n\n变量是`mutable`，因为我们要在`const`成员函数中修改它们。\n\n2.  让我们初始化变量:\n\n```cpp\npublic:\n    postprocessor()\n        : default_flag_(BOOST_ONCE_INIT)\n        , default_()\n    {}\n```\n\n3.  最后，让我们更改`act`功能:\n\n```cpp\n    // Concurrent calls on the same variable are safe.\n    answer_t act(const std::string& in) const {\n        answer_t ret;\n        if (in.empty()) {\n            // Extremely rare condition.\n            boost::call_once(default_flag_, [this]() {\n                this->default_ = read_defaults();\n            });\n            return default_;\n        }\n\n        // ...\n        return ret;\n    }\n};\n```\n\n# 它是如何工作的...\n\n简而言之，`boost::call_once`和`boost::once_flag`确保作为第二个参数传递的函数只执行一次。\n\n`boost::call_once`函数同步对作为第二个参数传递的函数 *F* 的调用。`boost::call_once`和`boost::once_flag`如果在同一个`once_flag`上有两个或更多并发调用，确保只对功能 *F* 进行一次调用，并确保只对 *F* 进行一次成功调用。\n\n如果对函数 *F* 的调用没有抛出离开 *F* 主体的异常，则`boost::call_once`假设调用成功，并将该信息存储在`boost::once_flag`中。任何随后用相同的`boost::once_flag`打给`boost::call_once`的电话都不起作用。\n\nDo not forget to initialize the `boost::once_flag` with the `BOOST_ONCE_INIT` macro.\n\n# 还有更多..\n\n`boost::call_once`可以将参数传递给要调用的函数:\n\n```cpp\n#include <iostream>\n\nvoid once_printer(int i) {\n    static boost::once_flag flag = BOOST_ONCE_INIT;\n    boost::call_once(\n        flag,\n        [](int v) { std::cout << \"Print once \" << v << '\\n'; },\n        i // <=== Passed to lambda from above.\n    );\n\n    // ...\n}\n```\n\n现在，如果我们在循环中调用`once_printer`函数:\n\n```cpp\nint main() {\n    for (unsigned i = 0; i < 10; ++ i) {\n        once_printer(i);\n    }\n}\n```\n\n输出中只有一行:\n\n```cpp\nPrint once 0\n```\n\nC++ 11 在`<mutex>`头中有一个`std::call_once`和`std::once_flag`。与 Boost 版本不同的是，`once_flag`的标准库版本不需要通过宏进行初始化，它有一个 constexpr 构造函数。像往常一样，Boost 版本在 C++ 11 之前的编译器上是可用的，所以如果你必须支持旧的编译器，就使用它。\n\nVisual Studio before 2015 was shipping a suboptimal `std::call_once` implementation more than ten times slower than the Boost's version. Stick to the `boost::call_once` if you're not using modern compilers.\n\n# 请参见\n\n`Boost.Thread`文档给出了很多不同案例的好例子。可以在[http://boost.org/libs/thread.](http://boost.org/libs/thread)找到\n\n# 锁定多个互斥体\n\n接下来的几段，你会成为写游戏的人之一。恭喜你，可以上班玩了！\n\n你正在开发一个服务器，你必须为两个用户之间交换战利品编写代码:\n\n```cpp\nclass user {\n    boost::mutex        loot_mutex_;\n    std::vector<item_t> loot_;\npublic:\n    // ...\n\n    void exchange_loot(user& u);\n};\n```\n\n服务器上的不同线程可以同时处理每个用户操作，因此您必须通过互斥来保护资源。初级开发人员试图解决这个问题，但他的解决方案不起作用:\n\n```cpp\nvoid user::exchange_loot(user& u) {\n    // Terribly wrong!!! ABBA deadlocks.\n    boost::lock_guard<boost::mutex> l0(loot_mutex_);\n    boost::lock_guard<boost::mutex> l1(u.loot_mutex_);\n    loot_.swap(u.loot_);\n}\n```\n\n前面代码中的问题是一个众所周知的 **ABBA 死锁**问题。想象一下*线程 1* 锁定*互斥体 A**线程 2* 锁定*互斥体 B* 。现在*线程 1* 试图锁定已经锁定的*互斥体 B* ，*线程 2* 试图锁定已经锁定的*互斥体 A* 。这导致两个线程被彼此无限期锁定，因为它们需要一个被另一个线程锁定的资源来继续，而另一个线程等待当前线程拥有的资源。\n\n现在，如果用户 1 和用户 2 同时为对方呼叫`exchange_loot`，那么我们可能会出现`user1.exchange_loot(user2)`呼叫锁定`user1.loot_mutex_``user2.exchange_loot(user1)`呼叫锁定`user2.loot_mutex_`的情况。`user1.exchange_loot(user2)`等待无穷大试图锁定`user2.loot_mutex_`，`user2.exchange_loot(user1)`等待无穷大试图锁定`user1.loot_mutex_`。\n\n# 准备好\n\n线程和互斥锁的基本知识对于这个食谱来说已经足够了。\n\n# 怎么做...\n\n这个问题有两种现成的解决方案:\n\n1.  需要编译器提供可变模板支持的简短示例:\n\n```cpp\n#include <boost/thread/lock_factories.hpp>\n\nvoid user::exchange_loot(user& u) {\n    typedef boost::unique_lock<boost::mutex> lock_t;\n\n    std::tuple<lock_t, lock_t> l = boost::make_unique_locks(\n        loot_mutex_, u.loot_mutex_\n    );\n\n    loot_.swap(u.loot_);\n}\n```\n\n相同的代码使用使用`auto`:\n\n```cpp\n#include <boost/thread/lock_factories.hpp>\n\nvoid user::exchange_loot(user& u) {\n    auto l = boost::make_unique_locks(\n        loot_mutex_, u.loot_mutex_\n    );\n\n    loot_.swap(u.loot_);\n}\n```\n\n2.  便携式解决方案:\n\n```cpp\n#include <boost/thread/locks.hpp>\n\nvoid user::exchange_loot(user& u) {\n    typedef boost::unique_lock<boost::mutex> lock_t;\n\n    lock_t l0(loot_mutex_, boost::defer_lock);\n    lock_t l1(u.loot_mutex_, boost::defer_lock);\n    boost::lock(l0, l1);\n\n    loot_.swap(u.loot_);\n}\n```\n\n# 它是如何工作的...\n\n核心思想是以某种方式对互斥体进行排序，并始终按照该特定顺序锁定它们。在这种情况下，就不存在 ABBA 问题了，因为所有线程总是会在 *B* 之前锁定互斥体 *A* 。通常，使用其他死锁避免算法，但是为了简单起见，我们假设使用互斥体的顺序。\n\n在第一个例子中，我们使用了`boost::make_unique_locks`，它总是以某种特定的顺序锁定线程，并返回一个持有锁的元组。\n\n在第二个例子中，我们手动创建了锁，但是由于传递了`boost::defer_lock`参数，没有锁定它们。实际的锁定发生在`boost::lock(l0, l1)`调用中，它以某种预定义的顺序锁定互斥体。\n\n现在，如果`user1`和`user2`同时为对方呼叫`exchange_loot`，那么`user1.exchange_loot(user2)`和`user2.exchange_loot(user1)`呼叫都会先尝试锁定`user1.loot_mutex_`或者先尝试锁定`user2.loot_mutex_`。这取决于运行时。\n\n# 还有更多...\n\n`boost::make_unique_locks`和`boost::lock`函数可能接受 2 个以上的锁或互斥体，因此您可以在更高级的情况下使用它们，其中必须同时锁定两个以上的互斥体。\n\nC++ 11 在标题`<mutex>`中定义了一个`std::lock`函数，其行为与`boost::lock`函数完全一样。\n\nC++ 17 有一个更漂亮的解决方案:\n\n```cpp\n#include <mutex>\n\nvoid user::exchange_loot(user& u) {\n    std::scoped_lock l(loot_mutex_, u.loot_mutex_);\n    loot_.swap(u.loot_);\n}\n```\n\n在前面的代码中，`std::scoped_lock`是一个接受可变数量锁的类。它有可变的模板参数，这些参数是从 C++ 17 推导指南中自动推导出来的。上例中`std::scoped_lock`的实际类型是:\n\n```cpp\nstd::scoped_lock<std::mutex, std::mutex>\n```\n\n`std::scoped_lock`持有在构造期间传递的所有互斥锁的锁，并避免死锁。换句话说，它的工作方式与第一个示例类似，但看起来稍好一些。\n\n# 请参见\n\n`Boost.Thread`的官方文档可能会给你带来很多本章没有描述的其他有用的类；前往[http://boost.org/libs/thread.](http://boost.org/libs/thread)"
  },
  {
    "path": "docs/boost-cpp-app-dev-cb/06.md",
    "content": "# 六、操作任务\n\n在本章中，我们将介绍:\n\n*   为任意数据类型处理注册任务\n*   将计时器和处理计时器事件作为任务\n*   作为任务的网络通信\n*   接受传入连接\n*   并行执行不同的任务\n*   流水线任务处理\n*   制造非阻塞屏障\n*   存储异常并从中创建任务\n*   获取和处理系统信号作为任务\n\n# 介绍\n\n这一章是关于任务的。我们将功能对象称为*任务*，因为它更短，更好地反映了它将做什么。这一章的主要思想是，我们可以将所有的处理、计算和交互分解为函子(任务)，并且几乎独立地处理这些任务中的每一个。此外，我们可能不会阻止一些缓慢的操作，例如从套接字接收数据或等待超时，而是提供一个回调任务并继续处理其他任务。一旦操作系统完成这个缓慢的操作，我们的回调就被执行了。\n\nThe best way to understand the example is to play with it by modifying, running, and extending it. The site, [http://apolukhin.github.io/Boost-Cookbook/](http://apolukhin.github.io/Boost-Cookbook/), has all the examples from this chapter, and you can even play with some of them online.\n\n# 开始之前\n\n本章要求至少具备第一、第二和第五章的基本知识。需要 C++ 11 右值引用和 lambdas 的基础知识。\n\n# 为任意数据类型处理注册任务\n\n首先，让我们关注保存所有任务并为其执行提供方法的类。我们已经在[第 5 章](05.html#7PRJC0-712b4ba1126a4c7c89e1d44de61b4bdd) *【多线程，创建工作队列类】*食谱中做了类似的事情，但是以下一些问题没有得到解决:\n\n*   一个`work_queue`类只是存储和返回任务，但是我们还需要执行现有的任务。\n*   任务可能会引发异常。如果异常离开了任务边界，我们需要捕捉并处理它们。\n*   任务可能不会注意到线程中断。同一线程上的下一个任务可能会被中断。\n*   我们需要一种方法来停止任务的处理。\n\n# 准备好\n\n该配方需要与`boost_system`和`boost_thread`库链接。还需要具备`Boost.Thread`的基本知识。\n\n# 怎么做...\n\n在这个食谱中，我们用`boost::asio::io_service`代替了上一章的`work_queue`。这样做是有原因的，我们会在下面的食谱中看到。\n\n1.  让我们从围绕用户任务的结构开始:\n\n```cpp\n#include <boost/thread/thread.hpp>\n#include <iostream>\n\nnamespace detail {\n\ntemplate <class T>\nstruct task_wrapped {\nprivate:\n    T task_unwrapped_;\n\npublic:\n    explicit task_wrapped(const T& f)\n        : task_unwrapped_(f)\n    {}\n\n    void operator()() const {\n        // Resetting interruption.\n        try {\n            boost::this_thread::interruption_point();\n        } catch(const boost::thread_interrupted&){}\n\n        try {\n            // Executing task.\n            task_unwrapped_();\n        } catch (const std::exception& e) {\n            std::cerr<< \"Exception: \" << e.what() << '\\n';\n        } catch (const boost::thread_interrupted&) {\n            std::cerr<< \"Thread interrupted\\n\";\n        } catch (...) {\n            std::cerr<< \"Unknown exception\\n\";\n        }\n    }\n};\n\n} // namespace detail\n```\n\n2.  为了便于使用，我们将创建一个从用户的函子中产生`task_wrapped`的函数:\n\n```cpp\nnamespace detail {\n\ntemplate <class T>\ntask_wrapped<T> make_task_wrapped(const T& task_unwrapped) {\n    return task_wrapped<T>(task_unwrapped);\n}\n\n} // namespace detail\n```\n\n3.  现在，我们准备写`tasks_processor`课:\n\n```cpp\n#include <boost/asio/io_service.hpp> \n\nclass tasks_processor: private boost::noncopyable {\nprotected:\n    static boost::asio::io_service& get_ios() {\n        static boost::asio::io_service ios;\n        static boost::asio::io_service::work work(ios);\n\n        return ios;\n    }\n```\n\n4.  让我们添加`push_task`方法:\n\n```cpp\npublic:\n    template <class T>\n    static void push_task(const T& task_unwrapped) {\n        get_ios().post(detail::make_task_wrapped(task_unwrapped));\n    }\n```\n\n5.  让我们通过添加用于启动和停止任务执行循环的成员函数来结束这个类:\n\n```cpp\n    static void start() {\n        get_ios().run();\n    }\n\n    static void stop() {\n        get_ios().stop();\n    }\n}; // tasks_processor\n```\n\n搞定了。现在，是时候测试我们的班级了:\n\n```cpp\nint func_test() {\n    static int counter = 0;\n    ++ counter;\n    boost::this_thread::interruption_point();\n\n    switch (counter) {\n    case 3:\n        throw std::logic_error(\"Just checking\");\n\n    case 10:\n        // Emulation of thread interruption.\n        // Caught inside task_wrapped and does not stop execution.\n        throw boost::thread_interrupted();\n\n    case 90:\n        // Stopping the tasks_processor.\n        tasks_processor::stop();\n    }\n\n    return counter;\n}\n```\n\n`main`功能可能如下所示:\n\n```cpp\nint main () {\n    for (std::size_t i = 0; i < 100; ++ i) {\n        tasks_processor::push_task(&func_test);\n    }\n\n    // Processing was not started.\n    assert(func_test() == 1);\n\n    // We can also use lambda as a task.\n    // Counting 2 + 2 asynchronously.\n    int sum = 0;\n    tasks_processor::push_task(\n        [&sum]() { sum = 2 + 2; }\n    );\n\n    // Processing was not started.\n    assert(sum == 0);\n\n    // Does not throw, but blocks till\n    // one of the tasks it is owning\n    // calls tasks_processor::stop().\n    tasks_processor::start();\n    assert(func_test() == 91);\n}\n```\n\n# 它是如何工作的...\n\n`boost::asio::io_service`变量可以存储和执行发布给它的任务。但是我们可能不会直接向它发布用户的任务，因为他们可能会收到针对其他任务的中断或抛出异常。这就是为什么我们用`detail::task_wrapped`结构包装用户的任务。它通过调用以下命令重置所有先前的中断:\n\n```cpp\ntry { \n    boost::this_thread::interruption_point(); \n} catch(const boost::thread_interrupted&){}\n```\n\n`detail::task_wrapped`执行`try{ } catch()`块中的任务，确保没有异常离开`operator()`边界。\n\n看一下`start()`功能。`boost::asio::io_service::run()`开始处理发布到`io_service`变量的任务。如果不调用`boost::asio::io_service::run()`，则不执行发布的任务(这可以在`main()`功能中看到)。可以通过调用`boost::asio::io_service::stop()`来停止任务处理。\n\n如果没有剩余任务，则`boost::asio::io_service`类从`run()`函数返回，因此我们使用`boost::asio::io_service::work`的实例强制它继续执行:\n\n```cpp\nstatic boost::asio::io_service& get_ios() {\n    static boost::asio::io_service ios;\n    static boost::asio::io_service::work work(ios);\n\n    return ios;\n}\n```\n\nThe `iostream` classes and variables, such as `std::cerr` and `std::cout` are not thread safe on pre C++ 11 compilers and may produce interleaved characters on C++ 11 compatible compilers. In real projects, additional synchronization must be used to get readable output. For the simplicity of an example, we do not do that.\n\n# 还有更多...\n\nC++ 17 标准库没有`io_service`。然而，`Boost.Asio`库的很大一部分被提议作为网络**技术规范** ( **TS** )作为 C++ 的补充。\n\n# 请参见\n\n*   本章以下食谱将向您展示为什么我们选择`boost::asio::io_service`而不是使用我们来自[第 5 章](05.html#7PRJC0-712b4ba1126a4c7c89e1d44de61b4bdd) **【多线程】** 的手写代码\n*   您可以参考`Boost.Asio`的文档，在[http://boost.org/libs/asio](http://boost.org/libs/asio)获取一些示例、教程和类参考\n*   您也可以阅读 *Boost。Asio C++ 网络编程*一书，对`Boost.Asio`的介绍比较流畅，涵盖了一些本书没有涉及的细节\n\n# 将计时器和处理计时器事件作为任务\n\n以特定的时间间隔检查某物是一项常见的任务。例如，我们需要每 5 秒钟检查一次活动的某些会话。对于这样的问题，有一些流行的解决方案:\n\n*   坏的解决方案会创建一个线程来执行检查，然后休眠 5 秒钟。这是一个蹩脚的解决方案，消耗了大量的系统资源，并且扩展性很差。\n*   正确的解决方案使用系统特定的 API 来异步操作计时器。这是一个更好的解决方案，需要一些工作，不便携，除非你使用`Boost.Asio`。\n\n# 准备好\n\n你必须知道如何使用 C++ 11 右值引用和`unique_ptr`。\n\n该配方基于上一个配方的代码。参见本章第一个食谱，获取`boost::asio::io_service`和`task_queue`课程的信息。\n\n将该配方与`boost_system`和`boost_thread`库链接。定义`BOOST_ASIO_DISABLE_HANDLER_TYPE_REQUIREMENTS`绕过限制性的库检查。\n\n# 怎么做...\n\n我们只是通过添加新的方法来修改`tasks_processor`类，以便在某个指定的时间运行任务。\n\n1.  让我们为我们的`tasks_processor`类添加一个延迟运行任务的方法:\n\n```cpp\nclass tasks_processor {\n    // ...\npublic:\n    template <class Time, class Func>\n    static void run_delayed(Time duration_or_time, const Func& f) {\n        std::unique_ptr<boost::asio::deadline_timer> timer(\n            new boost::asio::deadline_timer(\n                get_ios(), duration_or_time\n            )\n        );\n\n        timer_ref.async_wait(\n            detail::timer_task<Func>(\n                std::move(timer),\n                f\n            )\n        );\n    }\n};\n```\n\n2.  作为最后一步，我们创建一个`timer_task`结构:\n\n```cpp\n#include <boost/asio/io_service.hpp>\n#include <boost/asio/deadline_timer.hpp>\n#include <boost/system/error_code.hpp>\n#include <memory>  // std::unique_ptr\n#include <iostream>\n\nnamespace detail {\n\n    template <class Functor>\n    struct timer_task {\n    private:\n        std::unique_ptr<boost::asio::deadline_timer> timer_;\n        task_wrapped<Functor> task_;\n\n    public:\n        explicit timer_task(\n                std::unique_ptr<boost::asio::deadline_timer> timer,\n                const Functor& task_unwrapped)\n            : timer_(std::move(timer))\n            , task_(task_unwrapped)\n        {}\n\n        void operator()(const boost::system::error_code& error) const {\n            if (!error) {\n                task_();\n            } else {\n                std::cerr << error << '\\n';\n            }\n        }\n    };\n\n} // namespace detail\n```\n\n这就是我们使用新功能的方式:\n\n```cpp\nint main () {\n    const int seconds_to_wait = 3;\n    int i = 0;\n\n    tasks_processor::run_delayed(\n        boost::posix_time::seconds(seconds_to_wait),\n        test_functor(i)\n    );\n\n    tasks_processor::run_delayed(\n        boost::posix_time::from_time_t(time(NULL) + 1),\n        &test_func1\n    );\n\n    assert(i == 0);\n\n    // Blocks till one of the tasks\n    // calls tasks_processor::stop().\n    tasks_processor::start();\n}\n```\n\n其中`test_functor`是定义了`operator()`的结构，`test_func1`是函数:\n\n```cpp\nstruct test_functor {\n    int& i_;\n\n    explicit test_functor(int& i);\n\n    void operator()() const {\n        i_ = 1;\n        tasks_processor::stop();\n    }\n};\n\nvoid test_func1();\n```\n\n# 它是如何工作的...\n\n简而言之，当经过指定的时间后，`boost::asio::deadline_timer`将任务推送到`boost::asio::io_service`类的实例中执行。\n\n所有讨厌的东西都在`run_delayed`功能中:\n\n```cpp\n    template <class Time, class Functor>\n    static void run_delayed(Time duration_or_time, const Functor& f) {\n        std::unique_ptr<boost::asio::deadline_timer> \n        timer( /* ... */ );\n\n        boost::asio::deadline_timer& timer_ref = *timer;\n\n        timer_ref.async_wait(\n            detail::timer_task<Functor>(\n                std::move(timer),\n                f\n            )\n        );\n    }\n```\n\n`tasks_processor::run_delayed`函数接受超时和超时后要调用的函子。其中，创建了一个指向`boost::asio::deadline_timer`的唯一指针。`boost::asio::deadline_timer`保存特定于平台的东西，用于异步执行任务。\n\n`Boost.Asio` does not manage memory out of the box. The library user has to take care of managing resources usually by keeping them in the task. So if we need a timer and want some function to execute after the specified timeout, we have to move the timer's unique pointer into the task, get a reference to the timer, and pass a task to the timer.\n\n我们得到了对这一行中`deadline_timer`的引用:\n\n```cpp\nboost::asio::deadline_timer& timer_ref = *timer;\n```\n\n现在，我们创建一个`detail::timer_task`对象，它存储一个函子并获得`unique_ptr<boost::asio::deadline_timer>`的所有权:\n\n```cpp\n            detail::timer_task<Functor>(\n                std::move(timer),\n                f\n            )\n```\n\n`boost::asio::deadline_timer`在被触发之前不能被破坏，将其移入`timer_task`函子保证了这一点。\n\n最后，当请求的时间过去后，我们指示`boost::asio::deadline_timer`将`timer_task`函子发布到`io_service`:\n\n```cpp\ntimer_ref.async_wait( /* timer_task */ )\n```\n\n对`io_service`变量的引用保存在`boost::asio::deadline_timer`变量中。这就是为什么它的构造器需要一个对`io_service`的引用来存储它，并且一旦超时就将任务发布给它。\n\n`detail::timer_task::operator()`方法接受`boost::system::error_code`，如果等待时发生了不好的事情，则包含错误描述。如果没有错误发生，我们调用用户的 functor，它被包装以捕捉异常(我们重复使用第一个配方中的`detail::task_wrapped`结构)。\n\n`boost::asio::deadline_timer::async_wait`等待超时时不消耗 CPU 资源或执行线程。您可以简单地将一些进一步推入`io_service`中，它们将在操作系统保持超时的同时开始执行:\n\n![](img/00010.jpeg)\n\nAs a rule of thumb: all the resources that are used during the `async_*` calls must be stored in the task.\n\n# 还有更多...\n\n一些奇特/古老的平台没有 API 来很好地实现定时器，因此`Boost.Asio`库使用每个`io_service`的额外执行线程来模拟异步定时器的行为。没别的办法了。\n\nC++ 17 里面没有`Boost.Asio`类；然而，网络终端服务有`async_wait`和`timer`两个等级。\n\n# 请参见\n\n*   阅读本章的第一个食谱将教会你`boost::asio::io_service`的基础知识。以下食谱将为您提供更多`io_service`用法的例子，并向您展示如何使用`Boost.Asio`处理网络通信、信号和其他功能。\n*   您可以参考`Boost.Asio`的文档，在[http://boost.org/libs/asio](http://boost.org/libs/asio)网站获取一些示例、教程和课程参考。\n\n# 作为任务的网络通信\n\n通过网络接收或发送数据是一项缓慢的操作。当机器接收数据包时，当操作系统验证数据包并将数据复制到用户指定的缓冲区时，可能需要几秒钟。\n\n我们可以做很多工作而不是等待！让我们修改我们的`tasks_processor`类，使它能够以异步方式发送和接收数据。在非技术术语中，我们要求它从远程主机接收至少 *N* 字节，完成后，调用我们的函子。顺便说一句，不要阻止这个电话。那些了解**libev****libevent**或者 Node.js 的读者可能会在这个食谱中找到很多熟悉的东西。\n\n# 准备好\n\n这个食谱是基于前面两个食谱。参见本章第一个食谱，获取`boost::asio::io_service`和`task_queue`课程的信息。参见第二个配方，回顾异步处理的基础。\n\n将该配方与`boost_system`和`boost_thread`库链接。定义`BOOST_ASIO_DISABLE_HANDLER_TYPE_REQUIREMENTS`绕过限制性的图书馆检查。\n\n# 怎么做...\n\n让我们通过添加创建连接的方法来扩展上一个方法的代码。\n\n1.  一个连接将由一个`connection_with_data`类来表示。这个类保存到远程主机的套接字和一个用于接收和发送数据的`std::string`:\n\n```cpp\n#include <boost/asio/ip/tcp.hpp>\n#include <boost/core/noncopyable.hpp>\n\nstruct connection_with_data: boost::noncopyable {\n    boost::asio::ip::tcp::socket socket;\n    std::string data;\n\n    explicit connection_with_data(boost::asio::io_service& ios)\n        : socket(ios) \n    {}\n\n    void shutdown() {\n        if (!socket.is_open()) {\n            return;\n        }\n\n        boost::system::error_code ignore;\n        socket.shutdown(\n            boost::asio::ip::tcp::socket::shutdown_both,\n            ignore\n        );\n        socket.close(ignore);\n    }\n\n    ~connection_with_data() {\n        shutdown();\n    }\n};\n```\n\n2.  就像在前面的方法中一样，类主要由指向它的唯一指针使用。为了简单起见，我们添加一个`typedef`:\n\n```cpp\n#include <memory> // std::unique_ptr\n\ntypedef std::unique_ptr<connection_with_data> connection_ptr;\n```\n\n3.  上一个配方中的`tasks_processor`类拥有`boost::asio::io_service`对象。让它成为构建连接的工厂似乎是合理的:\n\n```cpp\nclass tasks_processor {\n    // ...\npublic:\n    static connection_ptr create_connection(\n        const char* addr,\n        unsigned short port_num)\n    {\n        connection_ptr c( new connection_with_data(get_ios()) );\n\n        c->socket.connect(boost::asio::ip::tcp::endpoint(\n            boost::asio::ip::address_v4::from_string(addr),\n            port_num\n        ));\n\n        return c;\n    }\n};\n```\n\n4.  以下是将数据异步写入远程主机的方法:\n\n```cpp\n#include <boost/asio/write.hpp>\n\ntemplate <class T>\nstruct task_wrapped_with_connection;\n\ntemplate <class Functor>\nvoid async_write_data(connection_ptr&& c, const Functor& f) {\n    boost::asio::ip::tcp::socket& s = c->socket;\n    std::string& d = c->data;\n\n    boost::asio::async_write(\n        s,\n        boost::asio::buffer(d),\n        task_wrapped_with_connection<Functor>(std::move(c), f)\n    );\n}\n```\n\n5.  以下是从远程主机异步读取数据的方法:\n\n```cpp\n#include <boost/asio/read.hpp>\n\ntemplate <class Functor>\nvoid async_read_data(\n    connection_ptr&& c,\n    const Functor& f,\n    std::size_t at_least_bytes)\n{\n    c->data.resize(at_least_bytes);\n    c->data.resize(at_least_bytes);\n\n    boost::asio::ip::tcp::socket& s = c->socket;\n    std::string& d = c->data;\n    char* p = (d.empty() ? 0 : &d[0]);\n\n    boost::asio::async_read(\n        s,\n        boost::asio::buffer(p, d.size()),\n        task_wrapped_with_connection<Functor>(std::move(c), f)\n    );\n}\n\ntemplate <class Functor>\nvoid async_read_data_at_least(\n    connection_ptr&& c,\n    const Functor& f,\n    std::size_t at_least_bytes,\n    std::size_t at_most)\n{\n    std::string& d = c->data;\n    d.resize(at_most);\n    char* p = (at_most == 0 ? 0 : &d[0]);\n\n    boost::asio::ip::tcp::socket& s = c->socket;\n\n    boost::asio::async_read(\n        s,\n        boost::asio::buffer(p, at_most),\n        boost::asio::transfer_at_least(at_least_bytes),\n        task_wrapped_with_connection<Functor>(std::move(c), f)\n    );\n}\n```\n\n6.  最后一部分是`task_wrapped_with_connection`类定义:\n\n```cpp\ntemplate <class T>\nstruct task_wrapped_with_connection {\nprivate:\n    connection_ptr c_;\n    T task_unwrapped_;\n\npublic:\n    explicit task_wrapped_with_connection\n    (connection_ptr&& c, const T& f)\n        : c_(std::move(c))\n        , task_unwrapped_(f)\n    {}\n\n    void operator()(\n        const boost::system::error_code& error,\n        std::size_t bytes_count)\n    {\n        c_->data.resize(bytes_count);\n        task_unwrapped_(std::move(c_), error);\n    }\n};\n```\n\n搞定了。现在，库用户可以像这样使用前面的类发送数据:\n\n```cpp\nvoid send_auth() {\n    connection_ptr soc = tasks_processor::create_connection(\n        \"127.0.0.1\", g_port_num\n    );\n    soc->data = \"auth_name\";\n\n    async_write_data(\n        std::move(soc),\n        &on_send\n    );\n}\n```\n\n用户也可以这样使用它来接收数据:\n\n```cpp\nvoid receive_auth_response(\n    connection_ptr&& soc,\n    const boost::system::error_code& err)\n{\n    if (err) {\n        std::cerr << \"Error on sending data: \" \n        << err.message() << '\\n';\n        assert(false);\n    }\n\n    async_read_data(\n        std::move(soc),\n        &process_server_response,\n        2\n    );\n}\n```\n\n库用户可以这样处理接收到的数据:\n\n```cpp\nvoid process_server_response(\n        connection_ptr&& soc,\n        const boost::system::error_code& err)\n{\n    if (err && err != boost::asio::error::eof) {\n        std::cerr << \"Client error on receive: \"\n        << err.message() << '\\n';\n        assert(false);\n    }\n\n    if (soc->data.size() != 2) {\n        std::cerr << \"Wrong bytes count\\n\";\n        assert(false);\n    }\n\n    if (soc->data != \"OK\") {\n        std::cerr << \"Wrong response: \" << soc->data << '\\n';\n        assert(false);\n    }\n\n    soc->shutdown();\n    tasks_processor::stop();\n}\n```\n\n# 它是如何工作的...\n\n`Boost.Asio`库不管理开箱即用的资源和缓冲区。因此，如果我们想要一些简单的接口来读写数据，最简单的解决方案是将发送/接收数据的套接字和缓冲区绑定在一起。这就是`connection_with_data`班的工作。它包含一个`boost::asio::ip::tcp::socket`，这是一个围绕本地套接字的`Boost.Asio`包装器和一个我们用作缓冲区的`std::string`变量。\n\n一个`boost::asio::ip::tcp::socket`类的构造函数接受`boost::asio::io_service`作为`Boost.Asio`的几乎所有类。创建套接字后，它必须连接到某个远程端点:\n\n```cpp\n        c->socket.connect(boost::asio::ip::tcp::endpoint(\n            boost::asio::ip::address_v4::from_string(addr),\n            port_num\n        ));\n```\n\n看一下书写功能。它接受指向`connection_with_data`类和函子`f`的唯一指针:\n\n```cpp\n#include <boost/asio/write.hpp>\n\ntemplate <class Functor>\nvoid async_write_data(connection_ptr&& c, const Functor& f) {\n```\n\n在其中，我们获得了对套接字和缓冲区的引用:\n\n```cpp\nboost::asio::ip::tcp::socket& s = c->socket;\nstd::string& d = c->data;\n```\n\n然后，我们要求异步写入:\n\n```cpp\n    boost::asio::async_write(\n        s,\n        boost::asio::buffer(d),\n        task_wrapped_with_connection<Functor>(std::move(c), f)\n    );\n}\n```\n\n所有有趣的事情都发生在`boost::asio::async_write`函数中。就像定时器一样，异步调用立即返回，而不执行函数。它只告诉在一些操作完成后将回调任务发布到`boost::asio::io_service`(在我们的例子中，它正在向套接字写入数据)。`boost::asio::io_service`在一个调用`io_service::run()`方法的线程中执行我们的函数。下图说明了这一点:\n\n![](img/00011.jpeg)\n\n现在，来看看`task_wrapped_with_connection::operator()`。它接受`const boost::system::error_code& error`和`std::size_t bytes_count`，因为`boost::asio::async_write`和`boost::asio::async_read`函数都在异步操作完成时传递这些参数。对`c_->data.resize(bytes_count);`的调用会调整缓冲区的大小，使其仅包含接收/写入的数据。最后，我们调用最初传递给`async`函数并存储为`task_unwrapped_`的回调。\n\n那是怎么回事？这就是发送数据的简单方法！现在，我们有了一个`async_write_data`函数，它将数据从缓冲区异步写入套接字，并在操作完成时执行回调:\n\n```cpp\nvoid on_send(connection_ptr&& soc, const boost::system::\nerror_code& err);\n\nvoid connect_and_send() {\n    connection_ptr s = tasks_processor::create_connection\n    (\"127.0.0.1\", 80);\n\n    s->data = \"data_to_send\";\n    async_write_data(\n        std::move(s),\n        &on_send\n    );\n}\n```\n\n`async_read_data`离`async_write_data`很近。它调整缓冲区的大小，创建一个`task_wrapped_with_connection`函数，并在异步操作完成时将其推入`is_service`。\n\n注意`async_read_data_at_least`功能。在它的身体里，对`boost::asio::async_read`有一个稍微不同的称呼:\n\n```cpp\nboost::asio::async_read(\n    s,\n    boost::asio::buffer(p, at_most),\n    boost::asio::transfer_at_least(at_least_bytes),\n    task_wrapped_with_connection<Functor>(std::move(c), f)\n);\n```\n\n里面有一个`boost::asio::transfer_at_least(al_least_bytes)`。`Boost.Asio`有很多自定义读写的函子。这一个函子说，*在调用回调之前至少传输* `at_least_bytes` *字节。更多字节是可以的，直到它们适合缓冲区*。\n\n最后，让我们看一下其中一个回调:\n\n```cpp\nvoid process_server_response(\n        connection_ptr&& soc,\n        const boost::system::error_code& err);\n```\n\n在这个例子中，回调必须接受`connection_ptr`和一个`boost::system::error_code`变量。一个`boost::system::error_code`变量保存关于错误的信息。它有一个到`bool`运算符的显式转换，所以检查错误的简单方法就是编写`if (err) { ... }`。如果遥控器结束传输并关闭插座，`err`可能包含`boost::asio::error::eof`错误代码。这并不总是坏事。在我们的示例中，我们将其视为非错误行为:\n\n```cpp\n    if (err && err != boost::asio::error::eof) {\n        std::cerr << \"Client error on receive: \" \n        << err.message() << '\\n';\n        assert(false);\n    }\n```\n\n因为我们已经将套接字和缓冲区绑定在一起，所以您可以从`soc->data`获得接收到的数据:\n\n```cpp\nif (soc->data.size() != 2) {\n    std::cerr << \"Wrong bytes count\\n\";\n    assert(false);\n}\n\nif (soc->data != \"OK\") {\n    std::cerr << \"Wrong response: \" << soc->data << '\\n';\n    assert(false);\n}\n```\n\nThe `soc->shutdown()` call is optional, because when `soc` goes out of scope, the destructor for it is called. Destructor of `unique_ptr<connection_with_data>` calls `~connection_with_data` that has a `shutdown()` in its body.\n\n# 还有更多...\n\n我们的`task_wrapped_with_connection::operator()`不够好！用户提供了`task_unwrapped_`回调我的抛出异常，并且可能被不属于该特定任务的`Boost.Thread`中断所中断。修复方法是将回调包装到第一个配方的类中:\n\n```cpp\nvoid operator()(\n    const boost::system::error_code& error,\n    std::size_t bytes_count)\n{\n    const auto lambda = [this, &error, bytes_count]() {\n        this->c_->data.resize(bytes_count);\n        this->task_unwrapped_(std::move(this->c_), error);\n    };\n\n    const auto task = detail::make_task_wrapped(lambda);\n    task();\n}\n```\n\n在`task_wrapped_with_connection::operator()`中，我们创建了一个名为`lambda`的λ函数。执行时，`lambda`将`connection_with_data`类中的数据调整为`bytes_count`并调用最初传递的回调。最后，我们将第一个配方中的`lambda`包装到我们的安全执行任务中，然后执行它。\n\n你可能会在网上看到很多`Boost.Asio`的例子。其中许多人使用`shared_ptr`而不是`unique_ptr`来保存数据。用`shared_ptr`的方法更容易实现；然而，它有两大缺点:\n\n*   效率:`shared_ptr`内部有一个原子计数器，从不同的线程修改它可能会显著降低性能。在接下来的一个食谱中，您将看到如何在多个线程中处理任务，这就是在高负载的情况下差异可能很明显的地方。\n*   显而易见:使用`unique_ptr`，您总是可以看到连接的所有权被转移到了某个地方(您可以在代码中看到`std::move`)。使用`shared_ptr`，你不能从界面上理解这个函数是获取了所有权还是仅仅使用了对一个对象的引用。\n\n但是，如果根据应用的逻辑，所有权必须同时在多个任务之间共享，您可能会被迫使用`shared_ptr`。\n\n`Boost.Asio`不是 C++ 17 的一部分，但它很快将作为 Networking TS 发货，并被纳入即将推出的 C++ 标准之一。\n\n# 请参见\n\n*   更多示例、教程、http://boost.org/libs/asio 的完整参考以及如何使用 UDP 或 ICMP 协议的示例，请参见`Boost.Asio`的官方文档。\n*   您也可以阅读 *Boost。Asio C++ 网络编程*一书，更详细地描述了`Boost.Asio`\n\n# 接受传入连接\n\n使用网络的服务器端通常看起来像一个序列，我们首先获得新的连接，读取数据，然后处理它，然后发送结果。想象一下，我们正在创建某种授权服务器，它必须每秒处理大量请求。在这种情况下，我们需要在多个线程中接受、接收、异步发送和处理任务。\n\n在这个食谱中，我们将看到如何扩展我们的`tasks_processor`类来接受和处理传入的连接，在下一个食谱中，我们将看到如何使它多线程化。\n\n# 准备好\n\n这个食谱需要对本章第一个食谱中描述的`boost::asio::io_service`基础知识有很好的了解。一些关于网络通信的知识会对你有所帮助。还需要了解`boost::function`和至少两个以前食谱的信息。将此食谱与`boost_system`和`boost_thread`库链接。定义`BOOST_ASIO_DISABLE_HANDLER_TYPE_REQUIREMENTS`绕过限制性的图书馆检查。\n\n# 怎么做...\n\n就像之前的食谱一样，我们给`tasks_processor`类增加了新的方法。\n\n1.  我们从在`tasks_processor`中添加一些`typedefs`开始:\n\n```cpp\nclass tasks_processor {\n    typedef boost::asio::ip::tcp::acceptor acceptor_t;\n\n    typedef boost::function<\n        void(connection_ptr, const boost::system::error_code&)\n    > on_accpet_func_t;\n```\n\n2.  让我们添加一个类，它将新的传入连接的套接字、要监听的套接字和用户提供的用于处理新连接的回调绑定在一起:\n\n```cpp\nprivate:\n    struct tcp_listener {\n        acceptor_t              acceptor_;\n        const on_accpet_func_t  func_;\n        connection_ptr          new_c_;\n\n        template <class Functor>\n        tcp_listener(\n                boost::asio::io_service& io_service,\n                unsigned short port,\n                const Functor& task_unwrapped)\n            : acceptor_(io_service, boost::asio::ip::tcp::endpoint(\n                boost::asio::ip::tcp::v4(), port\n            ))\n            , func_(task_unwrapped)\n        {}\n    };\n\n    typedef std::unique_ptr<tcp_listener> listener_ptr;\n```\n\n3.  我们需要添加一个在指定端口开始监听的函数:\n\n```cpp\npublic:  \n   template <class Functor>\n    static void add_listener(unsigned short port_num, const Functor& f) {\n        std::unique_ptr<tcp_listener> listener(\n            new tcp_listener(get_ios(), port_num, f)\n        );\n\n        start_accepting_connection(std::move(listener));\n    }\n```\n\n4.  开始接受传入连接的函数:\n\n```cpp\nprivate:\n   static void start_accepting_connection(listener_ptr&& listener) {\n        if (!listener->acceptor_.is_open()) {\n            return;\n        }\n\n        listener->new_c_.reset(new connection_with_data(\n            listener->acceptor_.get_io_service()\n        ));\n\n        boost::asio::ip::tcp::socket& s = listener->new_c_->socket;\n        acceptor_t& a = listener->acceptor_;\n        a.async_accept(\n            s,\n            tasks_processor::handle_accept(std::move(listener))\n        );\n    }\n```\n\n5.  我们还需要一个处理新连接的函子:\n\n```cpp\nprivate:\n    struct handle_accept {\n        listener_ptr listener;\n\n        explicit handle_accept(listener_ptr&& l)\n            : listener(std::move(l))\n        {}\n\n        void operator()(const boost::system::error_code& error) {\n            task_wrapped_with_connection<on_accpet_func_t> task(\n                std::move(listener->new_c_), listener->func_\n            );\n\n            start_accepting_connection(std::move(listener));\n            task(error, 0);\n        }\n    };\n```\n\n搞定了。现在，我们可以通过以下方式接受连接:\n\n```cpp\nclass authorizer {\npublic:\n    static void on_connection_accpet(\n        connection_ptr&& connection,\n        const boost::system::error_code& error)\n    {\n        assert(!error);\n        // ...\n    }\n};\n\nint main() {\n    tasks_processor::add_listener(80, &authorizer::on_connection_accpet);\n    tasks_processor::start();\n}\n```\n\n# 它是如何工作的...\n\n函数`add_listener`构建了新的`tcp_listener`，它保存了接受连接所需的所有东西。就像任何异步操作一样，我们需要在操作执行时保持资源活跃。一个独特的指向`tcp_listener`的指针可以完成这项工作。\n\n当我们构造指定端点的`boost::asio::ip::tcp::acceptor`(参见*步骤 3* )时，它在指定的地址打开一个套接字，并准备接受连接。\n\n在*步骤 4* 中，我们创建一个新的套接字，并为该新套接字调用`async_accept`。当一个新的连接到来时，`listener->acceptor_`将这个连接绑定到一个套接字，并将`tasks_processor::handle_accept`回调推入`boost::asio::io_service`。从之前的食谱中我们了解到，所有的`async_*`电话都会立即返回，`async_accept`并不是特例。\n\n让我们仔细看看我们的`handle_accept::operator()`。在其中，我们从之前的配方中创建一个`task_wrapped_with_connection`函子，并在其中移动一个新的连接。现在，我们的`listener_ptr`在`new_c_`中没有插座，因为它属于函子。我们调用函数`start_accepting_connection(std::move(listener))`，它在`listener->new_c_`中创建新的套接字，并启动异步接受。异步接受操作不会阻塞，因此程序继续执行，从`start_accepting_connection(std::move(listener))`函数返回，并通过连接`task(error, 0)`执行函子。\n\nYou've made everything as shown in the example, but the performance of the server is not good enough. That's because the example is simplified and many optimizations left behind at the scene. The most significant one is to keep a separate small buffer in `connection_with_data` and use it for all the internal `Boost.Asio`'s callback related allocations. See *Custom memory allocation example* in the official documentation of the `Boost.Asio` library for more information on this optimization topic.\n\n当调用`boost::asio::io_service`的析构函数时，调用所有回调的析构函数。这使得`tcp_connection_ptr`的析构函数被调用，释放了资源。\n\n# 还有更多...\n\n我们没有使用`boost::asio::ip::tcp::acceptor`类的所有特性。如果我们提供特定的`boost::asio::ip::tcp::endpoint`，它可以绑定到特定的 IPv6 或 IPv4 地址。您也可以通过`native_handle()`方法获得一个本地套接字，并使用一些特定于操作系统的调用来调整行为。您可以通过呼叫`set_option`为`acceptor_`设置一些选项。例如，您可以这样强制`acceptor_`重用地址:\n\n```cpp\nboost::asio::socket_base::reuse_address option(true); \nacceptor_.set_option(option); \n```\n\nReusing the address provides an ability to restart the server quickly after it was terminated without correct shutdown. After the server was terminated, a socket may be opened for some time, and you won't be able to start the server on the same address without the `reuse_address` option.\n\nC++ 17 没有从`Boost.Asio`开始的类，但是带有大部分功能的联网 TS 即将到来。\n\n# 请参见\n\n*   从头开始这一章是一个好主意，可以获得更多关于`Boost.Asio`的信息\n*   参见`Boost.Asio`的官方文档，了解更多示例、教程和在[http://boost.org/libs/asio](http://boost.org/libs/asio)的完整参考\n\n# 并行执行不同的任务\n\n现在，是时候让我们的`tasks_processor`在多个线程中处理任务了。这能有多难？\n\n# 入门指南\n\n你需要阅读本章的第一个食谱。还需要一些多线程的知识，尤其是阅读*操纵一组线程*的食谱。\n\n将该配方与`boost_system`和`boost_thread`库链接。定义`BOOST_ASIO_DISABLE_HANDLER_TYPE_REQUIREMENTS`绕过限制性的库检查。\n\n# 怎么做...\n\n我们所需要做的就是将`start_multiple`方法添加到我们的`tasks_processor`类中:\n\n```cpp\n#include <boost/thread/thread.hpp> \n\nclass tasks_processor {\npublic:\n    // Default value will attempt to guess optimal count of threads.\n    static void start_multiple(std::size_t threads_count = 0) {\n        if (!threads_count) {\n            threads_count = (std::max)(static_cast<int>(\n                boost::thread::hardware_concurrency()), 1\n            );\n        }\n\n        // First thread is the current thread.\n        -- threads_count;\n\n        boost::asio::io_service& ios = get_ios();\n        boost::thread_group tg;\n        for (std::size_t i = 0; i < threads_count; ++ i) {\n            tg.create_thread([&ios]() { ios.run(); });\n        }\n\n        ios.run();\n        tg.join_all();\n    }\n};\n```\n\n现在，我们可以做更多的工作，如下图所示:\n\n![](img/00012.jpeg)\n\n# 它是如何工作的...\n\n`boost::asio::io_service::run`方法是线程安全的。我们只需要从不同的线程运行`boost::asio::io_service::run`方法。\n\nIf you are executing tasks that modify a common resource, you need to add mutexes around that resources, or organize your application in a way, that the common resource is not used simultaneously by different tasks. It is safe to use resource from different tasks without concurrent access to the resource because `boost::asio::io_service` takes care of additional synchronization between tasks and forces the modification results of one task to be seen by another task.\n\n参见对`boost::thread::hardware_concurrency()`的调用。它返回可以在当前硬件上并发运行的线程数。但是，这只是一个提示，有时它可能会返回一个`0`值，这就是为什么我们要为它调用`std::max`函数。`std::max`确保`threads_count`至少存储值`1`。\n\nWe wrapped `std::max` in parentheses because some popular compilers define the `min()` and `max()` macros, so we need additional tricks to work around this.\n\n# 还有更多...\n\n`boost::thread::hardware_concurrency()`函数是 C++ 11 的一部分；您可以在`std::`名称空间的`<thread>`标题中找到它。\n\n所有的`boost::asio`类都不是 C++ 17 的一部分，但是它们将很快作为联网 TS 提供。\n\n# 请参见\n\n*   有关 http://boost.org/libs/asio 不同班级的更多示例和信息，请参见`Boost.Asio`文档\n*   [第五章](05.html#7PRJC0-712b4ba1126a4c7c89e1d44de61b4bdd)、*多线程*的食谱，(特别是最后一个食谱叫做*操纵一组线程*)会给你关于`Boost.Thread`用法的信息\n*   关于【http://boost.org/libs/thread】的`boost::thread_group`和`boost::threads`的信息，请参见`Boost.Thread`文档\n\n# 流水线任务处理\n\n有时，需要在指定的时间间隔内处理任务。与以前的食谱相比，我们试图按照任务在队列中出现的顺序来处理任务，这是一个很大的不同。\n\n考虑一个例子，我们正在编写一个连接两个子系统的程序，其中一个子系统产生数据包，另一个子系统将修改后的数据写入磁盘(类似这样的情况可以在摄像机、录音机和其他设备中看到)。我们需要按照指定的顺序一个接一个地处理数据包，平滑且抖动很小，并且在多个线程中进行。\n\n天真的方法在这里不起作用:\n\n```cpp\n#include <boost/thread/thread.hpp>\n\nsubsystem1 subs1;\nsubsystem2 subs2;\n\nvoid process_data() {\n    while (!subs1.is_stopped()) {\n        data_packet data = subs1.get_data();\n        decoded_data d_decoded = decode_data(data);\n        compressed_data c_data = compress_data(d_decoded);\n        subs2.send_data(c_data);\n    }\n}\n\nvoid run_in_multiple_threads() {\n    boost::thread t(&process_data);\n    process_data();\n\n    t.join();\n}\n```\n\n在多线程环境中，我们可以在第一个线程中获得*包#1* ，然后在第二个执行线程中获得*包#2* 。由于处理时间不同，操作系统上下文切换和调度*数据包#2* 可能在*数据包#1* 之前处理。数据包和处理顺序没有保证。让我们解决这个问题！\n\n# 准备好\n\n理解这个例子需要[第五章](05.html#7PRJC0-712b4ba1126a4c7c89e1d44de61b4bdd)、*多线程*中的*制作工作队列*食谱。代码必须链接到`boost_thread`和`boost_system`库。\n\n需要 C++ 11 的基础知识，尤其是 lambda 函数。\n\n# 怎么做...\n\n本食谱基于[第 5 章](05.html#7PRJC0-712b4ba1126a4c7c89e1d44de61b4bdd)、*多线程*的*制作工作队列*食谱中`work_queue`类的代码。我们将进行一些修改，并将使用该类的一些实例。\n\n1.  让我们从为数据解码、数据压缩和数据发送创建单独的队列开始:\n\n```cpp\nwork_queue decoding_queue, compressing_queue, sending_queue;\n```\n\n2.  现在，是时候重构`process_data`并将其拆分为多个功能了:\n\n```cpp\nvoid start_data_accepting();\nvoid do_decode(const data_packet& packet);\nvoid do_compress(const decoded_data& packet);\n\nvoid start_data_accepting() {\n    while (!subs1.is_stopped()) {\n        data_packet packet = subs1.get_data();\n\n        decoding_queue.push_task(\n            [packet]() {\n                do_decode(packet);\n            }\n        );\n    }\n}\n\nvoid do_decode(const data_packet& packet) {\n    decoded_data d_decoded = decode_data(packet);\n\n    compressing_queue.push_task(\n        [d_decoded]() {\n            do_compress(d_decoded);\n        }\n    );\n}\n\nvoid do_compress(const decoded_data& packet) {\n    compressed_data c_data = compress_data(packet);\n\n    sending_queue.push_task(\n        [c_data]() {\n            subs2.send_data(c_data);\n        }\n    );\n}\n```\n\n3.  我们的`work_queue`类来自[第 5 章](05.html#7PRJC0-712b4ba1126a4c7c89e1d44de61b4bdd)、*多线程*，获得了一些停止和运行任务的界面变化:\n\n```cpp\n#include <deque>\n#include <boost/function.hpp>\n#include <boost/thread/mutex.hpp>\n#include <boost/thread/locks.hpp>\n#include <boost/thread/condition_variable.hpp>\n\nclass work_queue {\npublic:\n    typedef boost::function<void()> task_type;\n\nprivate:\n    std::deque<task_type>       tasks_;\n    boost::mutex                mutex_;\n    boost::condition_variable   cond_;\n    bool                        is_stopped_;\n\npublic:\n    work_queue()\n        : is_stopped_(false)\n    {}\n\n    void run();\n    void stop();\n\n    // Same as in Chapter 5, but with\n    // rvalue references support.\n    void push_task(task_type&& task);\n};\n```\n\n4.  `work_queue``stop()``run()`功能的实现必须是这样的:\n\n```cpp\nvoid work_queue::stop() {\n    boost::lock_guard<boost::mutex> lock(mutex_);\n    is_stopped_ = true;\n    cond_.notify_all();\n}\n\nvoid work_queue::run() {\n    while (1) {\n        boost::unique_lock<boost::mutex> lock(mutex_);\n        while (tasks_.empty()) {\n            if (is_stopped_) {\n                return;\n            }\n            cond_.wait(lock);\n        }\n\n        task_type t = std::move(tasks_.front());\n        tasks_.pop_front();\n        lock.unlock();\n\n        t();\n    }\n}\n```\n\n5.  仅此而已！现在，我们只需要启动管道:\n\n```cpp\n#include <boost/thread/thread.hpp> \nint main() {\n    boost::thread t_data_decoding(\n        []() { decoding_queue.run(); }\n    );\n    boost::thread t_data_compressing(\n        []() { compressing_queue.run(); }\n    );\n    boost::thread t_data_sending(\n        []() { sending_queue.run(); }\n    );\n\n    start_data_accepting();\n```\n\n6.  管道可以这样停止:\n\n```cpp\n    decoding_queue.stop();\n    t_data_decoding.join();\n\n    compressing_queue.stop();\n    t_data_compressing.join();\n\n    sending_queue.stop();\n    t_data_sending.join();\n```\n\n# 它是如何工作的...\n\n诀窍是将单个数据包的处理分成一些同样小的子任务，并在不同的`work_queues`中逐一处理。在本例中，我们可以将数据处理分为数据解码、数据压缩和数据发送。\n\n理想情况下，六个数据包的处理如下所示:\n\n| **时间** | **接收** | **解码** | **压缩** | **发送** |\n| --- | --- | --- | --- | --- |\n| 勾选 1: | 数据包#1 |  |  |  |\n| 勾选 2: | 数据包#2 | 数据包#1 |  |  |\n| 勾选 3: | 数据包#3 | 数据包#2 | 数据包#1 |  |\n| 打勾 4: | 数据包#4 | 数据包#3 | 数据包#2 | 数据包#1 |\n| 勾选 5: | 数据包#5 | 数据包#4 | 数据包#3 | 数据包#2 |\n| 勾选 6: | 数据包#6 | 数据包#5 | 数据包#4 | 数据包#3 |\n| 打勾 7: | - | 数据包#6 | 数据包#5 | 数据包#4 |\n| 打勾 8: | - | - | 数据包#6 | 数据包#5 |\n| 打勾 9: | - | - | - | 数据包#6 |\n\n然而，我们的世界并不理想，所以有些任务可能比其他任务完成得更快。例如，接收可能比解码更快，在这种情况下，解码队列将保存一组要完成的任务。为了避免队列溢出，请努力使每个后续任务比前一个任务稍快。\n\n在我们的例子中，我们没有使用`boost::asio::io_service`，因为它不能保证发布的任务按照它们的邮资顺序执行。\n\n# 还有更多...\n\n在这个例子中，所有用来创建管道的工具都可以在 C++ 11 中使用，所以没有什么能阻止你在 C++ 11 兼容的编译器上创建同样的东西。但是，Boost 使您的代码在 C++ 11 之前的编译器上更加可移植和可用。\n\n# 请参见\n\n*   这种技术为处理器开发人员所熟知和使用。见[http://en.wikipedia.org/wiki/Instruction_pipeline](http://en.wikipedia.org/wiki/Instruction_pipeline)。在这里，您可能会发现对管道所有特征的简要描述。\n*   *来自[第五章](05.html#7PRJC0-712b4ba1126a4c7c89e1d44de61b4bdd)、*多线程*的【制作工作队列】*食谱将为您提供更多关于该食谱中使用的方法的信息。\n\n# 制造非阻塞屏障\n\n在多线程编程中，有一个抽象叫做**障碍**。它会停止到达它的执行线程，直到请求的线程数没有被阻塞。之后，所有线程都被释放，并继续执行。考虑以下可以使用它的示例。\n\n我们希望在不同的线程中处理数据的不同部分，然后发送数据:\n\n```cpp\n#include <boost/array.hpp>\n#include <boost/thread/barrier.hpp>\n#include <boost/thread/thread.hpp>\n\ntypedef boost::array<std::size_t, 10000> vector_type;\ntypedef boost::array<vector_type, 4> data_t;\n\nvoid fill_data(vector_type& data);\nvoid compute_send_data(data_t& data);\n\nvoid runner(std::size_t thread_index, boost::barrier& barrier, data_t& data) {\n    for (std::size_t i = 0; i < 1000; ++ i) {\n        fill_data(data.at(thread_index));\n        barrier.wait();\n\n        if (!thread_index) {\n            compute_send_data(data);\n        }\n        barrier.wait();\n    }\n}\n\nint main() {\n    // Initing barrier.\n    boost::barrier barrier(data_t::static_size);\n\n    // Initing data.\n    data_t data;\n\n    // Run on 4 threads.\n    boost::thread_group tg;\n    for (std::size_t i = 0; i < data_t::static_size; ++ i) {\n        tg.create_thread([i, &barrier, &data] () {\n            runner(i, barrier, data);\n        });\n    }\n\n    tg.join_all();\n}\n```\n\n`data_barrier.wait()`方法阻塞，直到所有线程填满数据。之后，所有线程都被释放。索引为`0`的线程使用`compute_send_data(data)`计算要发送的数据，而其他线程再次在栅栏处等待，如下图所示:\n\n![](img/00013.jpeg)\n\n看起来很蹩脚，不是吗？\n\n# 准备好\n\n这个食谱需要本章第一个食谱的知识。还需要`Boost.Thread`的知识。这个食谱的代码需要链接到`boost_thread`和`boost_system`库。\n\n# 怎么做...\n\n我们根本不需要封锁！让我们仔细看看这个例子。我们需要做的就是发布四个`fill_data`任务，最后完成的任务叫`compute_send_data(data)`。\n\n1.  我们需要第一个食谱中的`tasks_processor`类；无需对其进行任何更改。\n2.  我们将使用原子变量来代替屏障:\n\n```cpp\n#include <boost/atomic.hpp> \ntypedef boost::atomic<unsigned int> atomic_count_t; \n```\n\n3.  我们新的 runner 函数将如下所示:\n\n```cpp\nvoid clever_runner(\n        std::size_t thread_index,\n        std::size_t iteration,\n        atomic_count_t& counter,\n        data_t& data)\n{\n    fill_data(data.at(thread_index));\n\n    if (++ counter != data_t::static_size) {\n        return;\n    }\n\n    compute_send_data(data);\n\n    if (++ iteration == 1000) {\n        // Exiting, because 1000 iterations are done.\n        tasks_processor::stop();\n        return;\n    }\n\n    counter = 0;\n    for (std::size_t i = 0; i < data_t::static_size; ++ i) {\n        tasks_processor::push_task([i, iteration, &counter, &data]() {\n            clever_runner( \n                i, \n                iteration,\n                counter,\n                data\n            );\n        });\n    }\n}\n```\n\n4.  `main`功能需要一个微小的改变:\n\n```cpp\n    // Initing counter.\n    atomic_count_t counter(0);\n\n    // Initing data.\n    data_t data;\n\n    // Run 4 tasks.\n    for (std::size_t i = 0; i < data_t::static_size; ++ i) {\n        tasks_processor::push_task([i, &counter, &data]() {\n            clever_runner( \n                i, \n                0, // first iteration\n                counter,\n                data\n            );\n        });\n    }\n\n    tasks_processor::start();\n```\n\n# 它是如何工作的...\n\n我们一点也不阻拦。我们没有阻塞，而是计算完成填充数据的任务。这是通过`counter`原子变量完成的。最后剩余的任务将有一个等于`data_t::static_size`的`counter`变量。只有该任务必须计算和发送数据。\n\n之后，我们检查退出条件(完成了 1000 次迭代)，并通过将任务推入队列来发布新数据。\n\n# 还有更多...\n\n这是不是更好的解决方案？首先，它的伸缩性更好:\n\n![](img/00014.jpeg)\n\n这种方法对于程序做很多不同工作的情况也更有效。因为没有线程在屏障中等待，当其中一个线程计算和发送数据时，空闲线程可能会执行一些其他任务。\n\n这个方法可以在没有 Boost 库的 C++ 11 中实现。你只需要从第五章、*多线程*用`work_queue`替换`tasks_processor`里面的`io_service`。但是像往常一样，Boost 提供了更好的可移植性，并且可以让这个例子在使用 Boost 库的 C++ 11 之前的编译器上运行。你只需要用`boost::bind`和`boost::ref`替换 lambda 函数。\n\n# 请参见\n\n*   `Boost.Asio`的官方文档可能会给你更多关于[http://boost.org/libs/asio](http://boost.org/libs/asio)的`io_service`用法的信息\n*   参见[第 2 章](02.html#36VSO0-712b4ba1126a4c7c89e1d44de61b4bdd)、*管理资源*的所有`Boost.Function`相关食谱，以及[http://boost.org/libs/function](http://boost.org/libs/function)的官方文档，了解任务如何工作的概述\n*   参见[第 1 章](01.html#RL0A0-712b4ba1126a4c7c89e1d44de61b4bdd)、*开始编写应用*中与`Boost.Bind`相关的食谱，了解更多关于`boost::bind`功能的信息，或者参见[http://boost.org/libs/bind](http://boost.org/libs/bind)的官方文档\n\n# 存储异常并从中创建任务\n\n处理异常并不总是微不足道的，可能会消耗大量时间。考虑异常必须由网络序列化和发送的情况。这可能需要几毫秒和几千行代码。异常被捕获后，并不总是处理它的最佳时间和地点。\n\n我们可以存储异常并延迟它们的处理吗？\n\n# 准备好\n\n这个食谱需要熟悉`boost::asio::io_service`，这在本章的第一个食谱中有描述。\n\n该配方需要与`boost_system`和`boost_thread`库链接。\n\n# 怎么做...\n\n我们所需要的是能够存储异常，并像普通变量一样在线程之间传递它们。\n\n1.  让我们从存储和处理异常的函数开始:\n\n```cpp\n#include <boost/exception_ptr.hpp>\n\nstruct process_exception {\n    boost::exception_ptr exc_;\n\n    explicit process_exception(const boost::exception_ptr& exc)\n        : exc_(exc)\n    {}\n\n    void operator()() const;\n};\n```\n\n2.  该函子的`operator()`只是将异常输出到控制台:\n\n```cpp\n#include <boost/lexical_cast.hpp>\nvoid func_test2(); // Forward declaration.\n\nvoid process_exception::operator()() const  {\n    try {\n        boost::rethrow_exception(exc_);\n    } catch (const boost::bad_lexical_cast& /*e*/) {\n        std::cout << \"Lexical cast exception detected\\n\" << std::endl;\n\n        // Pushing another task to execute.\n        tasks_processor::push_task(&func_test2);\n    } catch (...) {\n        std::cout << \"Can not handle such exceptions:\\n\" \n            << boost::current_exception_diagnostic_information() \n            << std::endl;\n\n        // Stopping.\n        tasks_processor::stop();\n    }\n}\n```\n\n3.  让我们编写一些函数来演示异常是如何工作的:\n\n```cpp\n#include <stdexcept>\nvoid func_test1() {\n    try {\n        boost::lexical_cast<int>(\"oops!\");\n    } catch (...) {\n        tasks_processor::push_task(\n            process_exception(boost::current_exception())\n        );\n    }\n}\n\nvoid func_test2() {\n    try {\n        // ...\n        BOOST_THROW_EXCEPTION(std::logic_error(\"Some fatal logic error\"));\n        // ...\n    } catch (...) {\n        tasks_processor::push_task(\n            process_exception(boost::current_exception())\n        );\n    }\n}\n```\n\n现在，如果我们这样运行这个例子:\n\n```cpp\n  tasks_processor::get().push_task(&func_test1); \n  tasks_processor::get().start(); \n```\n\n我们将获得以下输出:\n\n```cpp\nLexical cast exception detected\n\nCan not handle such exceptions:\nmain.cpp(48): Throw in function void func_test2()\nDynamic exception type: boost::exception_detail::clone_impl<boost::exception_detail::error_info_injector<std::logic_error> >\nstd::exception::what: Some fatal logic error  \n```\n\n# 它是如何工作的...\n\n`Boost.Exception`库提供了存储和重新抛出异常的能力。`boost::current_exception()`方法只能从`catch()`块内部调用，它返回一个类型为`boost::exception_ptr`的对象。\n\n在前面的`func_test1()`例子中，`boost::bad_lexical_cast`异常被抛出。`boost::current_exception()`归还；从该异常创建一个`process_exception`任务。\n\n从`boost::exception_ptr`恢复异常类型的唯一方法是使用`boost::rethrow_exception(exc)`功能重新抛出。这就是`process_exception`功能的作用。\n\nThrowing and catching exceptions is a heavy operation. Throwing may dynamically allocate memory, touch cold memory, lock mutex, compute a bunch of addresses, and do other stuff. Do not throw exception in performance critical paths without very good reasons to do so!\n\n在`func_test2`中，我们使用`BOOST_THROW_EXCEPTION`宏抛出了一个`std::logic_error`异常。这个宏做了很多有用的工作；它检查我们的异常是否来自`std::exception`，向我们的异常添加关于源文件名、函数名和引发异常的代码行号的信息。当我们的`std::logic_error`异常被重新扔进`process_exception::operator()`时，它被`catch(...)`抓住。`boost::current_exception_diagnostic_information()`输出尽可能多的关于抛出异常的信息。\n\n# 还有更多...\n\n通常，`exception_ptr`用于在线程间传递异常。例如:\n\n```cpp\nvoid run_throw(boost::exception_ptr& ptr) {\n    try {\n        // A lot of code goes here.\n    } catch (...) {\n        ptr = boost::current_exception();\n    }\n}\n\nint main () {\n    boost::exception_ptr ptr;\n\n    // Do some work in parallel.\n    boost::thread t(\n        &run_throw,\n        boost::ref(ptr)\n    );\n\n    // Some code goes here.\n    // ...\n\n    t.join();\n\n    // Checking for exception.\n    if (ptr) {\n        // Exception occurred in thread.\n        boost::rethrow_exception(ptr);\n    }\n}\n```\n\n`boost::exception_ptr`类可以多次通过堆分配内存，使用原子，并通过重新抛出和捕获异常来实现一些操作。没有实际需要，尽量不要使用。\n\nC++ 11 采用了`boost::current_exception`、`boost::rethrow_exception`和`boost::exception_ptr`。你可以在`std::`命名空间的`<exception>`中找到它们。`BOOST_THROW_EXCEPTION`和`boost::current_exception_diagnostic_information()`功能不在 C++ 17 中。\n\n# 请参见\n\n*   http://boost.org/libs/exception 的`Boost.Exception`官方文档包含了很多关于实施和限制的有用信息。您可能还会发现一些本食谱中没有涉及的信息(例如，如何向已经抛出的异常添加附加信息)。\n*   本章的第一个食谱为你提供了`tasks_processor`课程的信息。*将字符串转换为数字 r* 来自[第 3 章](03.html#515F20-712b4ba1126a4c7c89e1d44de61b4bdd)、*转换和转换*的 ecipe 描述了本食谱中使用的`Boost.LexicalCast`库。\n\n# 获取和处理系统信号作为任务\n\n当编写一些服务器应用(尤其是 Linux 操作系统)时，需要捕获和处理信号。通常，所有的信号处理程序都是在服务器启动时设置的，在应用执行期间不会改变。\n\n这个食谱的目标是让我们的`tasks_processor`类能够处理信号。\n\n# 准备好\n\n我们需要本章第一个食谱的代码。还需要对`Boost.Function`有很好的了解。\n\n该配方需要与`boost_system`和`boost_thread`库链接。\n\n# 怎么做...\n\n这个食谱类似于本章 *2* 到 *4* 的食谱:我们有`async`信号等待功能，一些`async`信号处理程序，还有一些支持代码。\n\n1.  让我们从包含以下标题开始:\n\n```cpp\n#include <boost/asio/signal_set.hpp> \n#include <boost/function.hpp> \n```\n\n2.  现在，我们向`tasks_processor`类添加一个用于信号处理的成员:\n\n```cpp\nprotected:\n    static boost::asio::signal_set& signals() {\n        static boost::asio::signal_set signals_(get_ios());\n        return signals_;\n    }\n\n    static boost::function<void(int)>& signal_handler() {\n        static boost::function<void(int)> users_signal_handler_;\n        return users_signal_handler_;\n    }\n```\n\n3.  信号捕获时将调用的功能如下:\n\n```cpp\n    static void handle_signals(\n            const boost::system::error_code& error,\n            int signal_number)\n    {\n        signals().async_wait(&tasks_processor::handle_signals);\n\n        if (error) {\n            std::cerr << \"Error in signal handling: \" << error << '\\n';\n        } else {\n            boost::function<void(int)> h = signal_handler();\n            h(signal_number);\n        }\n\n    }\n```\n\n4.  现在我们需要一个注册信号处理器的函数:\n\n```cpp\npublic:\n\n    // This function is not thread safe!\n    // Must be called before all the `start()` calls.\n    // Function can be called only once.\n    template <class Func>\n    static void register_signals_handler(\n            const Func& f,\n            std::initializer_list<int> signals_to_wait)\n    {\n        // Making sure that this is the first call.\n        assert(!signal_handler()); \n\n        signal_handler() = f;\n        boost::asio::signal_set& sigs = signals();\n\n        std::for_each(\n            signals_to_wait.begin(),\n            signals_to_wait.end(),\n            [&sigs](int signal) { sigs.add(signal); }\n        );\n\n        sigs.async_wait(&tasks_processor::handle_signals);\n    }\n```\n\n仅此而已。现在，我们准备处理信号。以下是测试程序:\n\n```cpp\nvoid accept_3_signals_and_stop(int signal) {\n    static int signals_count = 0;\n    assert(signal == SIGINT);\n\n    ++ signals_count;\n    std::cout << \"Captured \" << signals_count << \" SIGINT\\n\"; \n    if (signals_count == 3) {\n        tasks_processor::stop();\n    }\n}\n\nint main () {\n    tasks_processor::register_signals_handler(\n        &accept_3_signals_and_stop,\n        { SIGINT, SIGSEGV }\n    );\n\n    tasks_processor::start();\n}\n```\n\n这将产生以下输出:\n\n```cpp\nCaptured 1 SIGINT\nCaptured 2 SIGINT\nCaptured 3 SIGINT\nPress any key to continue . . .\n```\n\n# 它是如何工作的...\n\n这里没有什么是困难的(与本章之前的一些食谱相比)。`register_signals_handler`功能将待处理的信号编号相加。这是通过调用`signals_to_wait`的每个元素的`boost::asio::signal_set::add`函数来完成的。\n\n接下来，`sigs.async_wait`启动`async`等待信号，并调用信号捕捉上的`tasks_processor::handle_signals`功能。`tasks_processor::handle_signals`函数立即开始异步等待下一个信号，检查错误，如果没有错误，则调用提供信号号码的回调。\n\n# 还有更多...\n\n我们可以做得更好！我们可以从第一个方法将用户提供的回调包装到我们的类中，以正确处理异常，并从第一个方法中做其他好事:\n\n```cpp\nboost::function<void(int)> h = signal_handler();\n\ndetail::make_task_wrapped([h, signal_number]() {\n    h(signal_number);\n})(); // make and run task_wrapped\n```\n\n当需要线程安全的动态添加和移除信号时，我们可以修改这个例子，使其看起来像*中的`detail::timer_task`一样，将定时器和处理定时器事件作为本章的任务*配方。当多个`boost::asio::signal_set`对象注册等待相同的信号时，每个`signal_set`的一个处理程序在一个信号上被调用。\n\n长期以来，C++ 一直能够使用`<csignal>`头中的`signal`函数来处理信号。网络终端服务可能不具备`signal_set`功能。\n\n# 请参见\n\n*   在变量配方中存储任何功能对象的*来自[第 2 章](02.html#36VSO0-712b4ba1126a4c7c89e1d44de61b4bdd)**管理资源*，提供了关于`boost::function`的信息**\n**   参见`Boost.Asio`的官方文档，了解更多关于`boost::asio::signal_set`的信息和例子，以及这个位于[http://boost.org/libs/asio](http://boost.org/libs/asio)的伟大图书馆的其他特征*"
  },
  {
    "path": "docs/boost-cpp-app-dev-cb/07.md",
    "content": "# 七、操纵字符串\n\n在本章中，我们将介绍:\n\n*   更改大小写和不区分大小写的比较\n*   使用正则表达式匹配字符串\n*   使用正则表达式搜索和替换字符串\n*   使用类似 printf 的安全函数格式化字符串\n*   替换和擦除字符串\n*   用两个迭代器表示一个字符串\n*   使用对字符串类型的引用\n\n# 介绍\n\n这一整章致力于改变、搜索和表示字符串的不同方面。我们将看到如何使用 Boost 库轻松完成一些常见的字符串相关任务。这一章足够轻松；它处理非常常见的字符串操作任务。那么，让我们开始吧！\n\n# 更改大小写和不区分大小写的比较\n\n这是一个相当常见的任务。我们有两个非 Unicode 或 ANSI 字符串:\n\n```cpp\n#include <string> \nstd::string str1 = \"Thanks for reading me!\"; \nstd::string str2 = \"Thanks for reading ME!\"; \n```\n\n我们需要以不区分大小写的方式比较它们。有很多方法可以做到这一点，让我们看看 Boost 的。\n\n# 准备好\n\n`std::string`基础知识是我们这里需要的全部。\n\n# 怎么做...\n\n以下是进行不区分大小写比较的不同方法:\n\n1.  最简单的一个是:\n\n```cpp\n#include <boost/algorithm/string/predicate.hpp> \n\nconst bool solution_1 = (\n     boost::iequals(str1, str2)\n);\n```\n\n2.  使用 Boost 谓词和标准库方法:\n\n```cpp\n#include <boost/algorithm/string/compare.hpp> \n#include <algorithm> \n\nconst bool solution_2 = (\n    str1.size() == str2.size() && std::equal(\n        str1.begin(),\n        str1.end(),\n        str2.begin(),\n        boost::is_iequal()\n    )\n);\n```\n\n3.  制作两个字符串的小写副本:\n\n```cpp\n#include <boost/algorithm/string/case_conv.hpp> \n\nvoid solution_3() {\n    std::string str1_low = boost::to_lower_copy(str1);\n    std::string str2_low = boost::to_lower_copy(str2);\n    assert(str1_low == str2_low);\n}\n```\n\n4.  制作原始字符串的大写副本:\n\n```cpp\n#include <boost/algorithm/string/case_conv.hpp> \n\nvoid solution_4() {\n    std::string str1_up = boost::to_upper_copy(str1);\n    std::string str2_up = boost::to_upper_copy(str2);\n    assert(str1_up == str2_up);\n}\n```\n\n5.  将原始字符串转换为小写:\n\n```cpp\n#include <boost/algorithm/string/case_conv.hpp> \n\nvoid solution_5() {\n    boost::to_lower(str1);\n    boost::to_lower(str2);\n    assert(str1 == str2);\n}\n```\n\n# 它是如何工作的...\n\n第二种方法并不明显。在第二种方法中，我们比较字符串的长度。如果它们具有相同的长度，我们使用`boost::is_iequal`谓词的一个实例逐个字符地比较字符串。`boost::is_iequal`谓词以不区分大小写的方式比较两个字符。\n\nThe `Boost.StringAlgorithm` library uses `i` in the name of a method or class, if this method is case-insensitive. For example, `boost::is_iequal`, `boost::iequals`, `boost::is_iless`, and others.\n\n# 还有更多...\n\n处理案例的`Boost.StringAlgorithm`库的每个功能和功能对象都接受`std::locale`。默认情况下(在我们的例子中)，方法和类使用默认构造的`std::locale`。如果我们经常处理字符串，那么构造一个`std::locale`变量并将其传递给所有方法可能是一个很好的优化。另一个很好的优化是通过`std::locale::classic()`使用 *C* 语言环境(如果您的应用逻辑允许的话):\n\n```cpp\n  // On some platforms std::locale::classic() works \n  // faster than std::locale().\n  boost::iequals(str1, str2, std::locale::classic()); \n```\n\nNobody forbids you to use both optimizations.\n\n可惜 C++ 17 没有`Boost.StringAlgorithm`开始的字符串函数。所有的算法都是快速可靠的，所以不要害怕在代码中使用它们。\n\n# 请参见\n\n*   助推字符串算法库的官方文档可以在[http://boost.org/libs/algorithm/string](http://boost.org/libs/algorithm/string)找到\n*   参见安德烈·亚历山德雷斯库和赫伯·萨特的《C++ 编码标准》一书，了解如何用几行代码创建不区分大小写的字符串\n\n# 使用正则表达式匹配字符串\n\n让我们做点有用的事吧！用户的输入必须使用一些**正则表达式**来检查，这是很常见的情况。问题是有很多正则表达式语法，使用一种语法编写的表达式没有被其他语法很好地处理。另一个问题是长正则表达式不太容易写。\n\n因此，在这个食谱中，我们将编写一个程序，支持不同的正则表达式语法，并检查输入字符串是否与指定的正则表达式匹配。\n\n# 入门指南\n\n这个食谱需要标准库的基础知识。正则表达式语法的知识可能会有所帮助。\n\n需要根据`boost_regex`库链接示例。\n\n# 怎么做...\n\n这个正则表达式匹配器示例由`main()`函数中的几行代码组成:\n\n1.  为了实现它，我们需要以下头:\n\n```cpp\n#include <boost/regex.hpp> \n#include <iostream> \n```\n\n2.  在程序开始时，我们需要输出可用的正则表达式语法:\n\n```cpp\nint main() { \n    std::cout  \n        << \"Available regex syntaxes:\\n\" \n        << \"\\t[0] Perl\\n\" \n        << \"\\t[1] Perl case insensitive\\n\" \n        << \"\\t[2] POSIX extended\\n\" \n        << \"\\t[3] POSIX extended case insensitive\\n\" \n        << \"\\t[4] POSIX basic\\n\" \n        << \"\\t[5] POSIX basic case insensitive\\n\\n\" \n        << \"Choose regex syntax: \"; \n```\n\n3.  现在，根据选择的语法正确设置标志:\n\n```cpp\n    boost::regex::flag_type flag;\n    switch (std::cin.get()) \n    {\n    case '0': flag = boost::regex::perl;\n        break;\n\n    case '1': flag = boost::regex::perl|boost::regex::icase;\n        break;\n\n    case '2': flag = boost::regex::extended;\n        break;\n\n    case '3': flag = boost::regex::extended|boost::regex::icase;\n        break;\n\n    case '4': flag = boost::regex::basic;\n        break;\n\n    case '5': flag = boost::regex::basic|boost::regex::icase;\n        break;\n    default:\n        std::cout << \"Incorrect number of regex syntax. Exiting...\\n\";\n        return 1;\n    }\n\n    // Disabling exceptions.\n    flag |= boost::regex::no_except;\n```\n\n4.  我们现在在循环中请求正则表达式模式:\n\n```cpp\n    // Restoring std::cin.\n    std::cin.ignore();\n    std::cin.clear();\n\n    std::string regex, str;\n    do {\n        std::cout << \"Input regex: \";\n        if (!std::getline(std::cin, regex) || regex.empty()) {\n            return 0;\n        }\n\n        // Without `boost::regex::no_except`flag this\n        // constructor may throw.\n        const boost::regex e(regex, flag);\n        if (e.status()) {\n            std::cout << \"Incorrect regex pattern!\\n\";\n            continue;\n        }\n```\n\n5.  循环得到一个`String to match:`:\n\n```cpp\n        std::cout << \"String to match: \";\n        while (std::getline(std::cin, str) && !str.empty()) {\n```\n\n6.  对其应用正则表达式并输出结果:\n\n```cpp\n            const bool matched = boost::regex_match(str, e);\n            std::cout << (matched ? \"MATCH\\n\" : \"DOES NOT MATCH\\n\");\n            std::cout << \"String to match: \";\n        } // end of `while (std::getline(std::cin, str))`\n```\n\n7.  我们将通过恢复`std::cin`并请求新的正则表达式模式来完成我们的示例:\n\n```cpp\n        // Restoring std::cin.\n        std::cin.ignore();\n        std::cin.clear();\n    } while (1);\n} // int main() \n```\n\n现在，如果我们运行前面的示例，我们将获得以下输出:\n\n```cpp\n Available regex syntaxes:\n```\n\n```cpp\n [0] Perl\n [1] Perl case insensitive\n [2] POSIX extended\n [3] POSIX extended case insensitive\n [4] POSIX basic\n [5] POSIX basic case insensitive\n```\n\n```cpp\nChoose regex syntax: 0\n Input regex: (\\d{3}[#-]){2}\n String to match: 123-123#\n MATCH\n String to match: 312-321-\n MATCH\n String to match: 21-123-\n DOES NOT MATCH\n String to match: ^Z\n Input regex: \\l{3,5}\n String to match: qwe\n MATCH\n String to match: qwert\n MATCH\n String to match: qwerty\n DOES NOT MATCH\n String to match: QWE\n DOES NOT MATCH\n String to match: ^Z\n\n Input regex: ^Z\n Press any key to continue . . .\n```\n\n# 它是如何工作的...\n\n所有匹配都由`boost::regex`类完成。它构造了一个能够进行正则表达式解析和编译的对象。使用`flag`输入变量将附加配置选项传递给类。\n\n如果正则表达式不正确，`boost::regex`抛出异常。如果通过了`boost::regex::no_except`标志，它会报告一个错误，在`status()`调用中返回一个非零值(就像我们的例子一样):\n\n```cpp\n        if (e.status()) {\n            std::cout << \"Incorrect regex pattern!\\n\";\n            continue;\n        }\n```\n\n这将导致:\n\n```cpp\nInput regex: (incorrect regex(\nIncorrect regex pattern!\n```\n\n正则表达式匹配通过调用`boost::regex_match`函数来完成。如果匹配成功，则返回`true`。额外的标志可以传递给`regex_match`，但是为了示例的简洁，我们避免了它们的使用。\n\n# 还有更多...\n\nC++ 11 包含了几乎所有的`Boost.Regex`类和标志。它们可以在`std::`命名空间的`<regex>`头中找到(而不是`boost::`)。官方文档提供了关于 C++ 11 和`Boost.Regex`的区别的信息。它还包含一些性能指标，表明`Boost.Regex`速度很快。一些标准库存在性能问题，因此在 Boost 和标准库版本之间进行明智的选择。\n\n# 请参见\n\n*   通过正则表达式搜索和替换字符串的*方法将为您提供更多关于`Boost.Regex`用法的信息*\n*   您也可以考虑官方文档来获取更多关于标志、性能度量、正则表达式语法和 C++ 11 一致性的信息，这些信息可以在[http://boost.org/libs/regex](http://boost.org/libs/regex)获得\n\n# 使用正则表达式搜索和替换字符串\n\n我妻子非常喜欢正则表达式匹配字符串的食谱。但是，她想要更多，并告诉我，在我推广食谱以便能够根据正则表达式匹配替换部分输入字符串之前，我不会得到任何食物。\n\n好了，来了。每个匹配的子表达式(括号中正则表达式的一部分)必须获得一个从 1 开始的唯一数字；这个数字将用于创建一个新的字符串。\n\n更新后的程序应该是这样工作的:\n\n```cpp\n Available regex syntaxes:\n```\n\n```cpp\n [0] Perl\n [1] Perl case insensitive\n [2] POSIX extended\n [3] POSIX extended case insensitive\n [4] POSIX basic\n [5] POSIX basic case insensitive\n```\n\n```cpp\n\n Choose regex syntax: 0\n Input regex: (\\d)(\\d)\n String to match: 00\n MATCH: 0, 0,\n Replace pattern: \\1#\\2\n RESULT: 0#0\n String to match: 42\n MATCH: 4, 2,\n Replace pattern: ###\\1-\\1-\\2-\\1-\\1###\n RESULT: ###4-4-2-4-4###\n```\n\n# 准备好\n\n我们将重用通过正则表达式匹配字符串的代码。建议在拿到这个之前先看一下。\n\n需要根据`boost_regex`库链接一个示例。\n\n# 怎么做...\n\n这个食谱是基于前一个食谱的代码。让我们看看必须改变什么:\n\n1.  不应包含额外的标题。但是，我们需要一个额外的字符串来存储替换模式:\n\n```cpp\n    std::string regex, str, replace_string;\n```\n\n2.  我们将`boost::regex_match`替换为`boost::regex_find`，并输出匹配结果:\n\n```cpp\n        std::cout << \"String to match: \";\n        while (std::getline(std::cin, str) && !str.empty()) {\n            boost::smatch results;\n            const bool matched = regex_search(str, results, e);\n            if (matched)  {\n                std::cout << \"MATCH: \";\n                std::copy(\n                    results.begin() + 1, \n                    results.end(), \n                    std::ostream_iterator<std::string>(std::cout, \", \")\n                );\n```\n\n3.  之后，我们需要获取替换模式并应用它:\n\n```cpp\n                std::cout << \"\\nReplace pattern: \";\n                if (\n                        std::getline(std::cin, replace_string)\n                        && !replace_string.empty())\n                {\n                    std::cout << \"RESULT: \" << \n                        boost::regex_replace(str, e, replace_string)\n                    ; \n                } else {\n                    // Restoring std::cin.\n                    std::cin.ignore();\n                    std::cin.clear();\n                }\n            } else { // `if (matched) `\n                std::cout << \"DOES NOT MATCH\";\n            }\n```\n\n就这样！每个人都很开心，我也吃饱了。\n\n# 它是如何工作的...\n\n`boost::regex_search`函数不仅返回`true`或`false`值(与`boost::regex_match`函数不同)，还存储匹配的零件。我们使用以下结构输出匹配的零件:\n\n```cpp\n    std::copy( \n        results.begin() + 1,  \n        results.end(),  \n        std::ostream_iterator<std::string>( std::cout, \", \") \n    ); \n```\n\n请注意，我们通过跳过第一个结果(`results.begin() + 1`)来输出结果，这是因为`results.begin()`包含整个正则表达式匹配。\n\n`boost::regex_replace`函数完成所有替换并返回修改后的字符串。\n\n# 还有更多...\n\n`regex_*`函数有不同的变体，有些接收双向迭代器而不是字符串，有些向迭代器提供输出。\n\n`boost::smatch`是`boost::match_results<std::string::const_iterator>`的`typedef`。如果您正在使用一些其他的双向迭代器而不是`std::string::const_iterator`，您应该使用您的双向迭代器的类型作为`boost::match_results`的模板参数。\n\n`match_results`有一个格式函数，所以我们可以用它来调整我们的例子，而不是:\n\n```cpp\nstd::cout << \"RESULT: \" << boost::regex_replace(str, e, replace_string); \n```\n\n我们可以使用以下内容:\n\n```cpp\nstd::cout << \"RESULT: \" << results.format(replace_string); \n```\n\n顺便说一下，`replace_string`支持多种格式:\n\n```cpp\nInput regex: (\\d)(\\d)\n String to match: 12\n MATCH: 1, 2,\n Replace pattern: $1-$2---$&---$$\n RESULT: 1-2---12---$\n```\n\n这个配方中的所有类和函数都存在于 C++ 11 的`<regex>`头的`std::`命名空间中。\n\n# 请参见\n\n`Boost.Regex`的官方文档将在[http://boost.org/libs/regex](http://boost.org/libs/regex)为您提供更多关于性能、C++ 11 标准兼容性和正则表达式语法的示例和信息。*正则表达式匹配字符串*食谱将告诉你`Boost.Regex`的基本知识。\n\n# 使用类似 printf 的安全函数格式化字符串\n\n`printf`系列功能是对安全的威胁。允许用户将自己的字符串作为类型并格式化说明符是一个非常糟糕的设计。那么，当需要用户定义的格式时，我们该怎么办呢？下面这个类的`std::string to_string(const std::string& format_specifier) const;`成员函数该如何实现？\n\n```cpp\nclass i_hold_some_internals \n{\n    int i;\n    std::string s;\n    char c;\n    // ...\n}; \n```\n\n# 准备好\n\n标准库的基础知识对这个食谱来说绰绰有余。\n\n# 怎么做...\n\n我们希望允许用户为字符串指定他们自己的输出格式:\n\n1.  为了以安全的方式做到这一点，我们需要以下标题:\n\n```cpp\n#include <boost/format.hpp>\n```\n\n2.  现在，我们为用户添加一些注释:\n\n```cpp\n    // `fmt` parameter may contain the following:\n    // $1$ for outputting integer 'i'.\n    // $2$ for outputting string 's'.\n    // $3$ for outputting character 'c'.\n    std::string to_string(const std::string& fmt) const {\n```\n\n3.  是时候让所有部分都发挥作用了:\n\n```cpp\n        boost::format f(fmt);\n        unsigned char flags = boost::io::all_error_bits;\n        flags ^= boost::io::too_many_args_bit;\n        f.exceptions(flags);\n        return (f % i % s % c).str();\n    }\n```\n\n仅此而已。看看这段代码:\n\n```cpp\nint main() {\n    i_hold_some_internals class_instance;\n\n    std::cout << class_instance.to_string(\n        \"Hello, dear %2%! \"\n        \"Did you read the book for %1% %% %3%\\n\"\n    );\n\n    std::cout << class_instance.to_string(\n        \"%1% == %1% && %1%%% != %1%\\n\\n\"\n    );\n}\n```\n\n想象一下`class_instance`有一个等于`100`的成员`i`、`s`有一个等于`\"Reader\"`的成员，还有一个等于`'!'`的成员`c`。然后，程序将输出以下内容:\n\n```cpp\n Hello, dear Reader! Did you read the book for 100 % !\n 100 == 100 && 100% != 100\n```\n\n# 它是如何工作的...\n\n`boost::format`类接受指定结果字符串格式的字符串。使用`operator%`将参数传递给`boost::format`。指定字符串格式的值`%1%`、`%2%`、`%3%`、`%4%`等被传递给`boost::format`的参数替换。\n\n对于格式字符串包含的参数少于传递给`boost::format`的参数的情况，我们也禁用例外:\n\n```cpp\n    boost::format f(format_specifier);\n    unsigned char flags = boost::io::all_error_bits;\n    flags ^= boost::io::too_many_args_bit;\n```\n\n这样做是为了允许像这样的一些格式:\n\n```cpp\n    // Outputs 'Reader'.\n    std::cout << class_instance.to_string(\"%2%\\n\\n\");\n```\n\n# 还有更多...\n\n如果格式不正确会发生什么？\n\n没什么可怕的，一个异常被抛出:\n\n```cpp\n    try {\n        class_instance.to_string(\"%1% %2% %3% %4% %5%\\n\");\n        assert(false);\n    } catch (const std::exception& e) {\n        // boost::io::too_few_args exception is catched.\n        std::cout << e.what() << '\\n';\n    }\n```\n\n前面的代码片段将以下几行输出到控制台:\n\n```cpp\n boost::too_few_args: format-string referred to more arguments than\n    were passed\n```\n\nC++ 17 没有`std::format`。`Boost.Format`图书馆不是一个很快的图书馆。尽量不要在性能关键部分大量使用它。\n\n# 请参见\n\n官方文档包含更多关于`Boost.Format`库性能的信息。更多类似扩展 printf 格式的示例和文档可在[http://boost.org/libs/format.](http://boost.org/libs/format)获得\n\n[](http://boost.org/libs/format)\n\n# 替换和擦除字符串\n\n我们需要删除字符串中的某些内容，替换字符串的一部分，或者删除一些子字符串的第一次或最后一次出现的情况非常常见。标准库允许我们在这方面做更多的工作，但是它通常涉及太多的代码编写。\n\n我们在*变化案例和不区分大小写比较*配方中看到`Boost.StringAlgorithm`库正在运行。让我们看看，当我们需要修改一些字符串时，如何使用它来简化我们的生活:\n\n```cpp\n#include <string> \nconst std::string str = \"Hello, hello, dear Reader.\"; \n```\n\n# 准备好\n\n这个例子需要 C++ 的基础知识。\n\n# 怎么做...\n\n该配方显示了`Boost.StringAlgorithm`库中不同的字符串擦除和替换方法是如何工作的:\n\n1.  擦除需要`#include <boost/algorithm/string/erase.hpp>`标题:\n\n```cpp\n#include <boost/algorithm/string/erase.hpp>\n\nvoid erasing_examples() {\n    namespace ba = boost::algorithm;\n    using std::cout;\n\n    cout << \"\\n erase_all_copy :\" << ba::erase_all_copy(str, \",\");\n    cout << \"\\n erase_first_copy:\" << ba::erase_first_copy(str, \",\");\n    cout << \"\\n erase_last_copy :\" << ba::erase_last_copy(str, \",\");\n    cout << \"\\n ierase_all_copy :\" << ba::ierase_all_copy(str, \"hello\");\n    cout << \"\\n ierase_nth_copy :\" << ba::ierase_nth_copy(str, \",\", 1);\n}\n```\n\n该代码输出以下内容:\n\n```cpp\n erase_all_copy   :Hello hello dear Reader.\n erase_first_copy :Hello hello, dear Reader.\n erase_last_copy  :Hello, hello dear Reader.\n ierase_all_copy   :, , dear Reader.\n ierase_nth_copy  :Hello, hello dear Reader.\n```\n\n2.  更换需要`<boost/algorithm/string/replace.hpp>`表头:\n\n```cpp\n#include <boost/algorithm/string/replace.hpp>\n\nvoid replacing_examples() {\n    namespace ba = boost::algorithm;\n    using std::cout;\n\n    cout << \"\\n replace_all_copy :\" \n        << ba::replace_all_copy(str, \",\", \"!\");\n\n    cout << \"\\n replace_first_copy :\"\n        << ba::replace_first_copy(str, \",\", \"!\");\n\n    cout << \"\\n replace_head_copy :\"\n        << ba::replace_head_copy(str, 6, \"Whaaaaaaa!\");\n}\n```\n\n该代码输出以下内容:\n\n```cpp\n replace_all_copy :Hello! hello! dear Reader.\n replace_first_copy :Hello! hello, dear Reader.\n replace_head_copy :Whaaaaaaa! hello, dear Reader.\n```\n\n# 它是如何工作的...\n\n所有的例子都是自我记录的。唯一不明显的是`replace_head_copy`功能。它接受要替换的字节数作为第二个参数，接受替换字符串作为第三个参数。因此，在前面的例子中，`Hello`被替换为`Whaaaaaaa!`。\n\n# 还有更多...\n\n还有就地修改字符串的方法。它们只是不会在`_copy`结束，然后返回`void`。所有不区分大小写的方法(以`i`开头的方法)都接受`std::locale`作为最后一个参数，并使用默认构造的区域设置作为默认参数。\n\n你是否经常使用不区分大小写的方法，并且需要更好的性能？只需创建一个持有`std::locale::classic()`的`std::locale`变量，并将其传递给所有算法。在小弦上，大部分时间被`std::locale`结构吃掉，而不是被算法吃掉:\n\n```cpp\n#include <boost/algorithm/string/erase.hpp>\n\nvoid erasing_examples_locale() {\n    namespace ba = boost::algorithm;\n\n    const std::locale loc = std::locale::classic();\n\n    const std::string r1\n        = ba::ierase_all_copy(str, \"hello\", loc);\n\n    const std::string r2\n        = ba::ierase_nth_copy(str, \",\", 1, loc);\n\n    // ...\n}\n```\n\nC++ 17 没有`Boost.StringAlgorithm`方法和类。但是，它有一个`std::string_view`类，可以在不分配内存的情况下使用子字符串。在本章接下来的两个食谱中，你可以找到更多关于`std::string_view`类的信息。\n\n# 请参见\n\n*   官方文件包含了很多例子和所有方法在[http://boost.org/libs/algorithm/string](http://boost.org/libs/algorithm/string)的完整参考\n*   有关`Boost.StringAlgorithm`库的更多信息，请参见本章中的*改变大小写和不区分大小写的比较*配方\n\n# 用两个迭代器表示一个字符串\n\n有些情况下，我们需要将一些字符串拆分为子字符串，并对这些子字符串做一些事情。在这个方法中，我们希望将字符串拆分成句子、计数字符和空格，当然，我们希望使用 Boost 并尽可能高效。\n\n# 准备好\n\n对于这个食谱，你需要一些标准库算法的基础知识。\n\n# 怎么做...\n\n借助 Boost，这很容易做到:\n\n1.  首先，包括正确的标题:\n\n```cpp\n#include <iostream>\n#include <boost/algorithm/string/split.hpp>\n#include <boost/algorithm/string/classification.hpp>\n#include <algorithm>\n```\n\n2.  现在，让我们定义我们的测试字符串:\n\n```cpp\nint main() {\n    const char str[] =\n        \"This is a long long character array.\"\n        \"Please split this character array to sentences!\"\n        \"Do you know, that sentences are separated using period, \"\n        \"exclamation mark and question mark? :-)\"\n    ;\n```\n\n3.  我们为拆分迭代器制作了一个`typedef`:\n\n```cpp\n    typedef boost::split_iterator<const char*> split_iter_t;\n```\n\n4.  构造迭代器:\n\n```cpp\n    split_iter_t sentences = boost::make_split_iterator(str,\n        boost::algorithm::token_finder(boost::is_any_of(\"?!.\"))\n    );\n```\n\n5.  现在，我们可以在匹配项之间迭代:\n\n```cpp\n    for (unsigned int i = 1; !sentences.eof(); ++ sentences, ++ i) {\n        boost::iterator_range<const char*> range = *sentences;\n        std::cout << \"Sentence #\" << i << \" : \\t\" << range << '\\n';\n```\n\n6.  计算字符数:\n\n```cpp\n        std::cout << range.size() << \" characters.\\n\";\n```\n\n7.  数一数空白:\n\n```cpp\n        std::cout \n            << \"Sentence has \" \n            << std::count(range.begin(), range.end(), ' ') \n            << \" whitespaces.\\n\\n\";\n    } // end of for(...) loop\n} // end of main()\n```\n\n就这样。现在，如果我们运行一个示例，它将输出:\n\n```cpp\n Sentence #1 : This is a long long character array\n 35 characters.\n Sentence has 6 whitespaces.\n\n Sentence #2 : Please split this character array to sentences\n 46 characters.\n Sentence has 6 whitespaces.\n\n Sentence #3 : Do you know, that sentences are separated using dot,\n exclamation mark and question mark\n 90 characters.\n Sentence has 13 whitespaces.\n\n Sentence #4 : :-)\n 4 characters.\n Sentence has 1 whitespaces.\n```\n\n# 它是如何工作的...\n\n这个食谱的主要思想是我们不需要从子串中构造`std::string`。我们甚至不需要一次性标记整个字符串。我们所需要做的就是找到第一个子串，并将其作为一对迭代器返回到子串的开头和结尾。如果我们需要更多的子串，找到下一个子串，并为该子串返回一对迭代器。\n\n![](img/00015.jpeg)\n\n现在，让我们仔细看看`boost::split_iterator`。我们使用`boost::make_split_iterator`函数构建了一个，该函数将`range`作为第一个参数，将二进制查找器谓词(或二进制谓词)作为第二个参数。当`split_iterator`被取消引用时，它将第一个子字符串作为`boost::iterator_range<const char*>`返回，该子字符串只保存一对指针，并且有一些方法可以使用它们。当我们增加`split_iterator`时，它会尝试寻找下一个子串，如果没有找到子串，`split_iterator::eof()`会返回`true`。\n\nDefault constructed split iterator represents an `eof()`. So we could rewrite the loop condition from `!sentences.eof()` to `sentences != split_iter_t()`. You could also use the split iterators with algorithms, for example: `std::for_each(sentences, split_iter_t(), [](auto range){ /**/ });`.\n\n# 还有更多...\n\n`boost::iterator_range`类广泛应用于所有的 Boost 库。在必须返回一对迭代器或者函数应该接受/使用一对迭代器的情况下，您可能会发现它甚至对您自己的代码也很有用。\n\n`boost::split_iterator<>`和`boost::iterator_range<>`类接受前向迭代器类型作为模板参数。因为我们正在处理前面例子中的字符数组，所以我们提供了`const char*`作为迭代器。如果我们使用`std::wstring`，我们将需要使用`boost::split_iterator<std::wstring::const_iterator>`和`boost::iterator_range<std::wstring::const_iterator>`类型。\n\nC++ 17 既没有`iterator_range`也没有`split_iterator`。然而，关于接受像“T2”这样的名字为“T3”的班级的讨论仍在继续。\n\n`boost::iterator_range`类没有虚函数，没有动态内存分配，快速高效。然而，它的输出流操作符`<<`没有针对字符数组的特定优化，因此流可能会很慢。\n\n`boost::split_iterator`类中有一个`boost::function`类，所以为大函子构建它可能会很慢。迭代只会增加很小的开销，即使在性能关键的部分你也感觉不到。\n\n# 请参见\n\n*   下一个食谱将告诉你`boost::iterator_range<const char*>`的一个不错的替代品\n*   `Boost.StringAlgorithm`的官方文档可能会在[http://boost.org/libs/algorithm/string](http://boost.org/libs/algorithm/string)给你提供更多关于课程的详细信息和一大堆例子\n*   更多关于`boost::iterator_range`的信息可以在这里找到:[http://boost.org/libs/range](http://boost.org/libs/range)；这是`Boost.Range`图书馆的一部分，在本书中没有描述，但你可能希望自己研究一下\n\n# 使用对字符串类型的引用\n\n这个食谱是本章最重要的食谱！让我们来看一个非常常见的情况，我们编写一些函数来接受一个字符串，并在`starts`和`ends`参数中传递的字符值之间返回字符串的一部分:\n\n```cpp\n#include <string>\n#include <algorithm>\n\nstd::string between_str(const std::string& input, char starts, char ends) {\n    std::string::const_iterator pos_beg \n        = std::find(input.begin(), input.end(), starts);\n    if (pos_beg == input.end()) {\n        return std::string();\n    }\n    ++ pos_beg;\n\n    std::string::const_iterator pos_end \n        = std::find(pos_beg, input.end(), ends);\n\n    return std::string(pos_beg, pos_end);\n}\n```\n\n你喜欢这个实现吗？在我看来，这很糟糕。考虑以下对它的调用:\n\n```cpp\nbetween_str(\"Getting expression (between brackets)\", '(', ')'); \n```\n\n在这个例子中，一个临时的`std::string`变量是由`\"Getting expression (between brackets)\"`构造的。字符数组足够长，所以很有可能在`std::string`构造函数内部调用动态内存分配，并将字符数组复制到其中。然后，在`between_str`函数内部的某个地方，新的`std::string`正在构建，这也可能导致另一个动态内存分配和复制。\n\n所以，这个简单的函数可能，而且在大多数情况下会:\n\n*   调用动态内存分配(两次)\n*   复制字符串(两次)\n*   释放内存(两次)\n\n我们能做得更好吗？\n\n# 准备好\n\n这个食谱需要标准库和 C++ 的基础知识。\n\n# 怎么做...\n\n我们在这里并不真正需要`std::string`类，我们只需要一些轻量级的类，它不管理资源，只有一个指向字符数组和数组大小的指针。Boost 为此提供了`boost::string_view`类。\n\n1.  要使用`boost::string_view`类，请包含以下标题:\n\n```cpp\n#include <boost/utility/string_view.hpp>\n```\n\n2.  更改方法的签名:\n\n```cpp\nboost::string_view between(\n    boost::string_view input,\n    char starts,\n    char ends)\n```\n\n3.  将功能体内各处的`std::string`改为`boost::string_view:`:\n\n```cpp\n{\n    boost::string_view::const_iterator pos_beg \n        = std::find(input.cbegin(), input.cend(), starts);\n    if (pos_beg == input.cend()) {\n        return boost::string_view();\n    }\n    ++ pos_beg;\n\n    boost::string_view::const_iterator pos_end \n        = std::find(pos_beg, input.cend(), ends);\n    // ...\n```\n\n4.  `boost::string_view`构造函数接受大小作为第二个参数，所以我们需要稍微修改一下代码:\n\n```cpp\n    if (pos_end == input.cend()) {\n        return boost::string_view(pos_beg, input.end() - pos_beg);\n    }\n\n    return boost::string_view(pos_beg, pos_end - pos_beg);\n}\n```\n\n就这样！现在我们可以称之为`between(\"Getting expression (between brackets)\", '(', ')')`，它将在没有任何动态内存分配和字符复制的情况下工作。我们仍然可以将其用于`std::string`:\n\n```cpp\n   between(std::string(\"(expression)\"), '(', ')')\n```\n\n# 它是如何工作的...\n\n如前所述，`boost::string_view`只包含一个指向字符数组和数据大小的指针。它有很多构造函数，可以用不同的方式初始化:\n\n```cpp\n    boost::string_view r0(\"^_^\");\n\n    std::string O_O(\"O__O\");\n    boost::string_view r1 = O_O;\n\n    std::vector<char> chars_vec(10, '#');\n    boost::string_view r2(&chars_vec.front(), chars_vec.size());\n```\n\n`boost::string_view`类具有`container`类所需的所有方法，因此它可用于标准库算法和 Boost 算法:\n\n```cpp\n#include <boost/algorithm/string/case_conv.hpp>\n#include <boost/algorithm/string/replace.hpp>\n#include <boost/lexical_cast.hpp>\n#include <iterator>\n#include <iostream>\n\nvoid string_view_algorithms_examples() {\n    boost::string_view r(\"O_O\");\n    // Finding single symbol.\n    std::find(r.cbegin(), r.cend(), '_');\n\n    // Will print 'o_o'.\n    boost::to_lower_copy(std::ostream_iterator<char>(std::cout), r);\n    std::cout << '\\n';\n\n    // Will print 'O_O'.\n    std::cout << r << '\\n';\n\n    // Will print '^_^'.\n    boost::replace_all_copy(\n        std::ostream_iterator<char>(std::cout), r, \"O\", \"^\"\n    );\n    std::cout << '\\n';\n\n    r = \"100\";\n    assert(boost::lexical_cast<int>(r) == 100);\n}\n```\n\nThe `boost::string_view` class does not really own string, so all its methods return constant iterators. Because of that, we cannot use it in methods that modify data, such as `boost::to_lower(r)`.\n\n在使用`boost::string_view`时，我们必须额外关注它所引用的数据；它必须存在并在引用它的`boost::string_view`变量的整个生命周期内有效。\n\nBefore Boost 1.61 there was no `boost::string_view` class, but the `boost::string_ref` class was used instead. Those classes are really close. `boost::string_view` closer follows the C++ 17 design and has better constexpr support. Since Boost 1.61, `boost::string_ref` is deprecated.\n\n`string_view`类快速高效，因为从不分配内存，没有虚函数！尽可能使用它们。它们被设计为`const std::string&`和`const char*`参数的替代产品。这意味着您可以替换以下三个功能:\n\n```cpp\nvoid foo(const std::string& s);\nvoid foo(const char* s);\nvoid foo(const char* s, std::size_t s_size);\n```\n\n只有一个:\n\n```cpp\nvoid foo(boost::string_view s);\n```\n\n# 还有更多...\n\n`boost::string_view`类是一个 C++ 17 类。如果你的编译器是 C++ 17 兼容的，你可以在`std::`名字空间的`<string_view>`头中找到它。\n\nBoost's and standard library's version support constexpr usage of `string_view`s; however, `std::string_view` currently has more functions marked with constexpr.\n\n请注意，我们已经通过值而不是常数引用接受了`string_view`变量。这是通过`boost::string_view`和`std::string_view`的推荐方式，因为:\n\n*   `string_view`是一个小类，里面有琐碎的类型。通过值传递它通常会导致更好的性能，因为间接寻址更少，并且它允许编译器进行更多的优化。\n*   在其他情况下，当没有性能差异时，写`string_view val`比写`const string_view& val`短。\n\n就像 C++ 17 的`std::string_view`一样，`boost::string_view`类实际上是一个`typedef`:\n\n```cpp\ntypedef basic_string_view<char, std::char_traits<char> > string_view; \n```\n\n您可能还会发现以下类型定义对`boost::`和`std::`名称空间中的宽字符很有用:\n\n```cpp\ntypedef basic_string_view<wchar_t,  std::char_traits<wchar_t> > wstring_view; \n\ntypedef basic_string_view<char16_t, std::char_traits<char16_t> > u16string_view; \n\ntypedef basic_string_view<char32_t, std::char_traits<char32_t> > u32string_view; \n```\n\n# 请参见\n\n`string_ref`和`string_view`的增强文档可以在[http://boost.org/libs/utility](http://boost.org/libs/utility)找到。"
  },
  {
    "path": "docs/boost-cpp-app-dev-cb/08.md",
    "content": "# 八、元编程\n\n在本章中，我们将介绍:\n\n*   使用类型的类型向量\n*   操纵类型向量\n*   在编译时获取函数的结果类型\n*   制作高阶元功能\n*   懒洋洋地评估元功能\n*   将所有元组元素转换为字符串\n*   拆分元组\n*   在 C++ 14 中操作异构容器\n\n# 介绍\n\n这一章致力于介绍一些很酷也很难理解的元编程方法。这些方法不是日常使用的，但是它们可能对泛型库的开发有真正的帮助。\n\n[第 4 章](04.html#6FSQK0-712b4ba1126a4c7c89e1d44de61b4bdd)*编译时技巧*已经介绍了元编程的基础知识。为了更好的理解，建议阅读它。在这一章中，我们将更深入地了解如何将多个类型打包到一个类似类型的元组中。我们将制作用于操作类型集合的函数，我们将看到编译时集合的类型可能如何改变，以及编译时技巧如何与运行时混合。所有这些都是元编程。\n\n系好安全带，准备好，我们走...！\n\n# 使用类型的类型向量\n\n有些情况下，处理所有模板参数就像在容器中一样非常好。想象我们正在写一些东西，比如`Boost.Variant`:\n\n```cpp\n#include <boost/mpl/aux_/na.hpp>\n\n// boost::mpl::na == n.a. == not available\ntemplate <\n    class T0 = boost::mpl::na,\n    class T1 = boost::mpl::na,\n    class T2 = boost::mpl::na,\n    class T3 = boost::mpl::na,\n    class T4 = boost::mpl::na,\n    class T5 = boost::mpl::na,\n    class T6 = boost::mpl::na,\n    class T7 = boost::mpl::na,\n    class T8 = boost::mpl::na,\n    class T9 = boost::mpl::na\n    >\nstruct variant;\n```\n\n前面的代码是以下所有有趣任务开始发生的地方:\n\n*   我们如何从所有类型中移除常量和变量限定符？\n*   我们如何删除重复的类型？\n*   我们如何得到所有类型的尺寸？\n*   怎样才能得到输入参数的最大值？\n\n所有这些任务都可以使用`Boost.MPL`轻松解决。\n\n# 准备好\n\n本食谱需要[第 4 章](04.html#6FSQK0-712b4ba1126a4c7c89e1d44de61b4bdd)*编译时技巧*的基本知识。阅读前积累一些勇气——这个食谱中会有很多元编程。\n\n# 怎么做...\n\n我们已经看到了如何在编译时操作一个类型。为什么我们不能更进一步，在一个数组中组合多种类型，并对该数组的每个元素执行操作呢？\n\n1.  首先，让我们将所有类型打包到`Boost.MPL`类型的容器中:\n\n```cpp\n#include <boost/mpl/vector.hpp>\n\ntemplate <\n    class T0, class T1, class T2, class T3, class T4,\n    class T5, class T6, class T7, class T8, class T9\n>\nstruct variant {\n    typedef boost::mpl::vector<\n        T0, T1, T2, T3, T4, T5, T6, T7, T8, T9\n    > types;\n};\n```\n\n2.  让我们让我们的例子不那么抽象，看看如果我们指定类型，它是如何工作的:\n\n```cpp\n#include <string>\nstruct declared{ unsigned char data[4096]; };\nstruct non_declared;\n\ntypedef variant<\n    volatile int, \n    const int, \n    const long, \n    declared, \n    non_declared, \n    std::string\n>::types types;\n```\n\n3.  我们可以在编译时检查一切。让我们断言类型不是空的:\n\n```cpp\n#include <boost/static_assert.hpp> \n#include <boost/mpl/empty.hpp> \n\nBOOST_STATIC_ASSERT((!boost::mpl::empty<types>::value)); \n```\n\n4.  我们也可以检查，例如，`non_declared`类型仍然在索引`4`位置:\n\n```cpp\n#include <boost/mpl/at.hpp>\n#include <boost/type_traits/is_same.hpp>\n\nBOOST_STATIC_ASSERT((boost::is_same<\n    non_declared, \n    boost::mpl::at_c<types, 4>::type\n>::value));\n```\n\n5.  最后一种仍然是`std::string`:\n\n```cpp\n#include <boost/mpl/back.hpp>\n\nBOOST_STATIC_ASSERT((boost::is_same<\n    boost::mpl::back<types>::type,\n    std::string\n>::value));\n```\n\n6.  我们可能会进行一些改造。让我们从移除常量和变量限定词开始:\n\n```cpp\n#include <boost/mpl/transform.hpp>\n#include <boost/type_traits/remove_cv.hpp>\n\ntypedef boost::mpl::transform<\n    types,\n    boost::remove_cv<boost::mpl::_1>\n>::type noncv_types;\n```\n\n7.  下面是我们删除重复类型的方法:\n\n```cpp\n#include <boost/mpl/unique.hpp>\n\ntypedef boost::mpl::unique<\n    noncv_types, \n    boost::is_same<boost::mpl::_1, boost::mpl::_2>\n>::type unique_types;\n```\n\n8.  我们可以检查向量只包含`5`类型:\n\n```cpp\n#include <boost/mpl/size.hpp>\n\nBOOST_STATIC_ASSERT((boost::mpl::size<unique_types>::value == 5));\n```\n\n9.  下面是我们如何计算每个元素的大小:\n\n```cpp\n// Without this we'll get an error:\n// \"use of undefined type 'non_declared'\"\nstruct non_declared{};\n\n#include <boost/mpl/sizeof.hpp>\ntypedef boost::mpl::transform<\n    unique_types, \n    boost::mpl::sizeof_<boost::mpl::_1>\n>::type sizes_types;\n```\n\n10.  这是如何从`sizes_type`类型中获得最大尺寸:\n\n```cpp\n#include <boost/mpl/max_element.hpp>\n\ntypedef boost::mpl::max_element<sizes_types>::type max_size_type;\n```\n\n我们可以断言，类型的最大大小等于结构的声明大小，这一定是我们示例中最大的一个:\n\n```cpp\n  BOOST_STATIC_ASSERT(max_size_type::type::value == sizeof(declared)); \n```\n\n# 它是如何工作的...\n\n`boost::mpl::vector`类是一个保存类型的编译时容器。更准确地说，它是一个保存类型的类型。我们不做它的例子；相反，我们只是在`typedef` s 中使用它。\n\n与标准库容器不同，`Boost.MPL`容器没有成员方法。相反，方法在单独的头中声明。所以要使用一些方法，我们需要:\n\n1.  包括正确的标题。\n2.  通常通过将容器指定为第一个参数来调用该方法。\n\n我们已经在[第四章](04.html#6FSQK0-712b4ba1126a4c7c89e1d44de61b4bdd)、*编译时技巧*中看到了元功能。我们使用了一些熟悉的`Boost.TypeTraits`库中的元功能(比如`boost::is_same`)。\n\n所以，在*步骤**3**步骤* *4* ，以及*步骤* *5* 中，我们只是在为我们的容器类型调用元功能。\n\n最难的部分来了！\n\n占位符被`Boost.MPL`库广泛用于组合元功能:\n\n```cpp\ntypedef boost::mpl::transform<\n    types,\n    boost::remove_cv<boost::mpl::_1>\n>::type noncv_types;\n```\n\n这里，`boost::mpl::_1`是一个占位符，整个表达式的意思是，对于`types`中的每个类型，做`boost::remove_cv<>::type`并将该类型推回到结果向量。通过`::type`返回结果向量。\n\n让我们移至*步骤* *7* 。这里，我们使用`boost::is_same<boost::mpl::_1, boost::mpl::_2>`模板参数为`boost::mpl::unique`指定一个比较元函数，其中`boost::mpl::_1`和`boost::mpl::_2`是占位符。你可能会发现它类似于`boost::bind(std::equal_to(), _1, _2)`，而*步骤* *7* 中的整个表达式类似于下面的伪代码:\n\n```cpp\nstd::vector<type> t; // 't' stands for 'types'. \nstd::unique(t.begin(), t.end(), boost::bind(std::equal_to<type>(), _1, _2)); \n```\n\n在*第*步*第*步中，有一些有趣的东西需要更好的理解。在前面的代码中，`sizes_types`不是一个值的向量，而是一个整数常量的向量——代表数字的类型。`sizes_types typedef`实际上是以下类型:\n\n```cpp\nstruct boost::mpl::vector<\n    struct boost::mpl::size_t<4>,\n    struct boost::mpl::size_t<4>,\n    struct boost::mpl::size_t<4096>,\n    struct boost::mpl::size_t<1>,\n    struct boost::mpl::size_t<32> \n>\n```\n\n最后一步现在必须明确。它只是从`sizes_types` `typedef`中获取最大元素。\n\nWe may use the `Boost.MPL` metafunctions at any place where typedefs are allowed.\n\n# 还有更多...\n\n`Boost.MPL`库的使用导致更长的编译时间，但是让你能够用类型做任何你想做的事情。它不会增加运行时开销，甚至不会向生成的二进制文件中添加一条指令。C++ 17 没有`Boost.MPL`类，`Boost.MPL`没有使用现代 C++ 的特性，比如变量模板。这使得`Boost.MPL`的编译时间在 C++ 11 编译器上不会尽可能短，但是使得该库在 C++ 03 编译器上可用。\n\n# 请参见\n\n*   有关元编程的信息基础，请参见第 4 章、*编译时技巧*\n*   *操纵类型向量*配方将为您提供更多关于元编程和`Boost.MPL`库的信息\n*   更多示例和完整参考请参见`Boost.MPL`官方文档，位于[http://boost.org/libs/mpl](http://boost.org/libs/mpl)\n\n# 操纵类型向量\n\n该配方的任务是根据第二个`boost::mpl::vector`功能的内容修改一个`boost::mpl::vector`功能的内容。我们将第二个向量称为修饰符向量，每个修饰符可能有以下类型:\n\n```cpp\n// Make unsigned.\nstruct unsigne; // Not a typo: `unsigned` is a keyword, we can not use it.\n\n// Make constant.\nstruct constant;\n\n// Otherwise we do not change type.\nstruct no_change;\n```\n\n那么，我们从哪里开始呢？\n\n# 准备好\n\n需要`Boost.MPL`的基础知识。阅读*使用类型的类型向量*配方和[第 4 章](04.html#6FSQK0-712b4ba1126a4c7c89e1d44de61b4bdd)，*编译时技巧，*可能会有所帮助。\n\n# 怎么做...\n\n这个方法与前一个类似，但是它也使用条件编译时语句。准备好，这不容易！\n\n1.  我们将从标题开始:\n\n```cpp\n// We'll need this at step 3.\n#include <boost/mpl/size.hpp>\n#include <boost/type_traits/is_same.hpp>\n#include <boost/static_assert.hpp>\n\n// We'll need this at step 4.\n#include <boost/mpl/if.hpp>\n#include <boost/type_traits/make_unsigned.hpp>\n#include <boost/type_traits/add_const.hpp>\n\n// We'll need this at step 5.\n#include <boost/mpl/transform.hpp>\n```\n\n2.  现在，让我们将所有元编程的魔力都放在结构中，以便更简单地重用:\n\n```cpp\ntemplate <class Types, class Modifiers>\nstruct do_modifications {\n```\n\n3.  最好检查传递的向量是否具有相同的大小:\n\n```cpp\n    BOOST_STATIC_ASSERT((boost::is_same<\n        typename boost::mpl::size<Types>::type, \n        typename boost::mpl::size<Modifiers>::type \n    >::value));\n```\n\n4.  现在，让我们来处理修改元功能:\n\n```cpp\n    typedef boost::mpl::if_<\n        boost::is_same<boost::mpl::_2, unsigne>,\n        boost::make_unsigned<boost::mpl::_1>,\n        boost::mpl::if_<\n            boost::is_same<boost::mpl::_2, constant>,\n            boost::add_const<boost::mpl::_1>,\n            boost::mpl::_1\n        >\n    > binary_operator_t;\n```\n\n5.  最后一步:\n\n```cpp\n    typedef typename boost::mpl::transform<\n        Types,\n        Modifiers,\n        binary_operator_t\n    >::type type;\n};\n```\n\n我们现在将运行一些测试，并确保我们的元功能运行良好:\n\n```cpp\n#include <boost/mpl/vector.hpp>\n#include <boost/mpl/at.hpp>\n\ntypedef boost::mpl::vector<\n    unsigne, no_change, constant, unsigne\n> modifiers;\n\ntypedef boost::mpl::vector<\n    int, char, short, long\n> types;\n\ntypedef do_modifications<types, modifiers>::type result_type;\n\nBOOST_STATIC_ASSERT((boost::is_same<\n    boost::mpl::at_c<result_type, 0>::type,\n    unsigned int\n>::value));\n\nBOOST_STATIC_ASSERT((boost::is_same<\n    boost::mpl::at_c<result_type, 1>::type,\n    char\n>::value));\n\nBOOST_STATIC_ASSERT((boost::is_same<\n    boost::mpl::at_c<result_type, 2>::type,\n    const short\n>::value));\n\nBOOST_STATIC_ASSERT((boost::is_same<\n    boost::mpl::at_c<result_type, 3>::type,\n    unsigned long\n>::value));\n```\n\n# 它是如何工作的...\n\n在步骤 *3 中，*我们断言大小是相等的，但是我们以一种不同寻常的方式做到了。`boost::mpl::size<Types>::type`元函数实际上返回一个整数常量`struct boost::mpl::long_<4>`，所以在静态断言中，我们实际上比较的是两种类型，而不是两个数字。这可以用一种更熟悉的方式改写:\n\n```cpp\n    BOOST_STATIC_ASSERT((\n        boost::mpl::size<Types>::type::value\n        ==\n        boost::mpl::size<Modifiers>::type::value\n    ));\n```\n\nNotice the `typename` keyword we use. Without it, the compiler won't be able to decide if `::type` is actually a type or some variable. Previous recipes did not require it, because parameters for the metafunction were fully known at the point where we were using them. But in this recipe, parameter for the metafunction is a template.\n\n我们先来看看*第五步*，再来处理*第四步*。在*步骤 5* 中，我们提供了从*步骤 4* 到`boost::mpl::transform`元功能的`Types`、`Modifiers`和`binary_operator_t`参数。这个元函数相当简单——对于每个传递的向量，它接受一个元素并将其传递给第三个参数——二进制元函数。如果我们用伪代码重写它，它将如下所示:\n\n```cpp\nvoid boost_mpl_transform_pseoudo_code() {\n    vector result;\n    for (std::size_t i = 0; i < Types.size(); ++ i) {\n        result.push_back(\n            binary_operator_t(Types[i], Modifiers[i])\n        );\n    }\n    return result;\n}\n```\n\n*第四步*可能会使某人头部受伤。在这一步中，我们编写一个元函数，为来自`Types`和`Modifiers`向量的每一对类型调用(参见前面的伪代码):\n\n```cpp\n    typedef boost::mpl::if_<\n        boost::is_same<boost::mpl::_2, unsigne>,\n        boost::make_unsigned<boost::mpl::_1>,\n        boost::mpl::if_<\n            boost::is_same<boost::mpl::_2, constant>,\n            boost::add_const<boost::mpl::_1>,\n            boost::mpl::_1\n        >\n    > binary_operator_t;\n```\n\n我们已经知道，`boost::mpl::_2`和`boost::mpl::_1`是占位符。在本食谱中，`_1`是来自`Types`向量的类型的占位符，`_2`是来自`Modifiers`向量的类型的占位符。\n\n所以，整个元功能是这样工作的:\n\n1.  将传递给它的第二个参数(通过`_2`)与`unsigned`类型进行比较。\n2.  如果类型相等，使传递给它的第一个参数(通过`_1`)无符号并返回该类型。\n3.  否则，它会将传递给它的第二个参数(通过`_2`)与常量类型进行比较。\n4.  如果类型相等，它会使传递给它的第一个参数(通过`_1`)保持不变，并返回该类型。\n5.  否则，返回传递给它的第一个参数(通过`_1`)。\n\n我们在构建这个元功能时需要非常小心。应额外注意不要在结尾调用`::type`:\n\n```cpp\n>::type binary_operator_t; // INCORRECT! \n```\n\n如果我们调用`::type`，编译器会在此时尝试对二进制运算符求值，这将导致编译错误。在伪代码中，这样的尝试看起来像这样:\n\n```cpp\nbinary_operator_t foo; \n// Attempt to call binary_operator_t::operator() without parameters, \n// when it has only two parameters overloads. \nfoo(); \n```\n\n# 还有更多...\n\n使用元功能需要一些练习。即使你卑微的仆人也不能从第一次尝试就正确地编写一些函数(尽管第二次和第三次尝试也不好)。不要害怕或困惑去实验！\n\n`Boost.MPL`库不是 C++ 17 的一部分，不使用现代 C++ 特性，但可以与 C++ 11 变量模板一起使用:\n\n```cpp\ntemplate <class... T> \nstruct vt_example { \n    typedef typename boost::mpl::vector<T...> type; \n}; \n\nBOOST_STATIC_ASSERT((boost::is_same< \n    boost::mpl::at_c<vt_example<int, char, short>::type, 0>::type, \n    int \n>::value)); \n```\n\n和往常一样，元函数不会向生成的二进制文件中添加一条指令，也不会使性能变差。然而，使用它们可以使您的代码更好地适应特定的情况。\n\n# 请参见\n\n*   从头开始阅读本章，以获得更多简单的`Boost.MPL`用法示例\n*   参见[第 4 章](04.html#6FSQK0-712b4ba1126a4c7c89e1d44de61b4bdd)、*编译时技巧*，特别是*为模板参数*配方选择最佳运算符，其中包含类似于`binary_operator_t`元功能的代码\n*   `Boost.MPL`的官方文档在[http://boost.org/libs/mpl](http://boost.org/libs/mpl)有更多的例子和完整的目录\n\n# 在编译时获取函数的结果类型\n\nC++ 11 增加了许多好的特性来简化元编程。一个这样的特性是替代函数语法。它允许推导模板函数的结果类型。这里有一个例子:\n\n```cpp\ntemplate <class T1, class T2>\nauto my_function_cpp11(const T1& v1, const T2& v2)\n    -> decltype(v1 + v2)\n{\n    return v1 + v2;\n}\n```\n\n它允许我们更容易地编写通用函数:\n\n```cpp\n#include <cassert>\n\nstruct s1 {};\nstruct s2 {};\nstruct s3 {};\n\ninline s3 operator + (const s1& /*v1*/, const s2& /*v2*/) {\n    return s3();\n}\n\ninline s3 operator + (const s2& /*v1*/, const s1& /*v2*/) {\n    return s3();\n}\n\nint main() {\n    s1 v1;\n    s2 v2;\n\n    s3 res0 = my_function_cpp11(v1, v2);\n    assert(my_function_cpp11('\\0', 1) == 1);\n}\n```\n\n但是，Boost 有很多这样的功能，它不需要 C++ 11 就能工作。这怎么可能，我们怎么才能做出 C++ 03 版本的`my_function_cpp11`函数？\n\n# 准备好\n\n这个食谱需要 C++ 和模板的基本知识。\n\n# 怎么做...\n\nC++ 11 极大地简化了元编程。必须用 C++ 03 编写大量代码，以使其接近替代函数语法:\n\n1.  我们必须包含以下标题:\n\n```cpp\n#include <boost/type_traits/common_type.hpp>\n```\n\n2.  现在，让我们在`result_of`命名空间中为任何类型创建一个元函数:\n\n```cpp\nnamespace result_of {\n\n    template <class T1, class T2>\n    struct my_function_cpp03 {\n        typedef typename boost::common_type<T1, T2>::type type;\n    };\n```\n\n3.  并将其专门化为`s1`和`s2`类型:\n\n```cpp\n    template <> \n    struct my_function_cpp03<s1, s2> {\n        typedef s3 type;\n    };\n\n    template <>\n    struct my_function_cpp03<s2, s1> {\n        typedef s3 type;\n    };\n} // namespace result_of\n```\n\n4.  现在我们准备编写`my_function_cpp03`函数:\n\n```cpp\ntemplate <class T1, class T2>\ntypename result_of::my_function_cpp03<T1, T2>::type\n    my_function_cpp03(const T1& v1, const T2& v2)\n{\n    return v1 + v2;\n}\n```\n\n就这样！现在，我们可以像使用 C++ 11 一样使用这个函数:\n\n```cpp\nint main() {\n    s1 v1;\n    s2 v2;\n\n    s3 res1 = my_function_cpp03(v1, v2);\n    assert(my_function_cpp03('\\0', 1) == 1);\n}\n```\n\n# 它是如何工作的...\n\n这个方法的主要思想是，我们可以制作一个特殊的元函数来推导结果类型。这样的技术在 Boost 库中随处可见，例如在`Boost.Variant`的`boost::get<>`实现中，或者在`Boost.Fusion`的几乎任何功能中。\n\n现在，让我们一步一步来。`result_of`命名空间只是某种传统，但你可以使用自己的命名空间，这并不重要。`boost::common_type<>`元功能推导出几种类型中常见的一种类型，所以我们将其用于一般情况。我们还为`s1`和`s2`类型添加了两个模板专门化的`result_of::my_function_cpp03`结构。\n\nThe disadvantage of writing metafunctions in C++ 03 is that sometimes we are required to write a lot. Compare the amount of code for `my_function_cpp11` and `my_function_cpp03` including the `result_of` namespace to feel the difference.\n\n当元函数准备好了，我们可以不用 C++ 11 推导出结果类型:\n\n```cpp\ntemplate <class T1, class T2>\ntypename result_of::my_function_cpp03<T1, T2>::type\n    my_function_cpp03(const T1& v1, const T2& v2);\n```\n\n# 还有更多...\n\n这种技术不会增加运行时开销，但可能会稍微降低编译速度。您也可以在现代 C++ 编译器上使用它。\n\n# 请参见\n\n*   食谱*启用积分类型的模板化函数*、*禁用实数类型的模板化函数*和*从[第 4 章](04.html#6FSQK0-712b4ba1126a4c7c89e1d44de61b4bdd)、*编译时技巧*中为模板参数*选择最佳运算符，将为您提供更多关于`Boost.TypeTraits`和元编程的信息\n*   考虑`Boost.TypeTraits`的官方文档，了解更多关于在[http://boost.org/libs/type_traits](http://boost.org/libs/type_traits)准备好的元功能的信息\n\n# 制作高阶元功能\n\n接受其他函数作为输入参数的函数或返回其他函数的函数称为**高阶**函数。例如，以下函数是高阶函数:\n\n```cpp\ntypedef void(*function_t)(int);\n\nfunction_t higher_order_function1();\nvoid higher_order_function2(function_t f);\nfunction_t higher_order_function3(function_t f); f); \n```\n\n我们已经在本章使用类型的类型向量的食谱*和*操作*类型的向量的食谱中看到了更高阶的元功能，我们使用了`boost::mpl::transform`。*\n\n在这个食谱中，我们将尝试制作我们自己的高阶元功能`coalesce`，它接受两种类型和两种元功能。`coalesce`元功能将第一个类型参数应用于第一个元功能，并将结果类型与`boost::mpl::false_`类型进行比较。如果结果类型是`boost::mpl::false_`类型，则返回将第二个 type-参数应用于第二个图元函数的结果，否则返回第一个结果类型:\n\n```cpp\ntemplate <class Param1, class Param2, class Func1, class Func2>\nstruct coalesce;\n```\n\n# 准备好\n\n这个食谱(和章节)是一个棘手的。强烈建议从头开始阅读这一章。\n\n# 怎么做...\n\n`Boost.MPL`元功能实际上是可以作为模板参数轻松传递的结构。最难的是正确使用它:\n\n1.  我们需要以下标题来编写高阶元函数:\n\n```cpp\n#include <boost/mpl/apply.hpp>\n#include <boost/mpl/if.hpp>\n#include <boost/type_traits/is_same.hpp>\n```\n\n2.  下一步是评估我们的功能:\n\n```cpp\ntemplate <class Param1, class Param2, class Func1, class Func2>\nstruct coalesce {\n    typedef typename boost::mpl::apply<Func1, Param1>::type type1;\n    typedef typename boost::mpl::apply<Func2, Param2>::type type2;\n```\n\n3.  现在，我们需要选择正确的结果类型:\n\n```cpp\n    typedef typename boost::mpl::if_<\n        boost::is_same< boost::mpl::false_, type1>,\n        type2,\n        type1\n    >::type type;\n};\n```\n\n就这样！我们已经完成了高阶元功能！现在，我们可以这样使用它:\n\n```cpp\n#include <boost/static_assert.hpp>\n#include <boost/mpl/not.hpp>\n#include <boost/mpl/next.hpp>\n\nusing boost::mpl::_1;\nusing boost::mpl::_2;\n\ntypedef coalesce<\n    boost::mpl::true_,\n    boost::mpl::int_<5>,\n    boost::mpl::not_<_1>,\n    boost::mpl::next<_1>\n>::type res1_t;\nBOOST_STATIC_ASSERT((res1_t::value == 6));\n\ntypedef coalesce<\n    boost::mpl::false_,\n    boost::mpl::int_<5>,\n    boost::mpl::not_<_1>,\n    boost::mpl::next<_1>\n>::type res2_t;\nBOOST_STATIC_ASSERT((res2_t::value));\n```\n\n# 它是如何工作的...\n\n编写高阶元函数的主要问题是如何处理占位符。这就是为什么我们不直接叫`Func1<Param1>::type`的原因。相反，我们必须使用`boost::mpl::apply`元函数，它接受一个函数和最多五个传递给这个函数的参数。\n\nYou may configure `boost::mpl::apply` to accept even more parameters, defining the `BOOST_MPL_LIMIT_METAFUNCTION_ARITY` macro to the required amount of parameters, for example, to 6.\n\n# 还有更多...\n\nC++ 11 没有任何接近`Boost.MPL`库的东西来应用一个元函数。\n\n现代 C++ 有一大堆可能帮助你实现`Boost.MPL`功能的特性。例如，C++ 11 有一个`<type_traits>`头和**基本** **常量表达式**支持。C++ 14 有一个**扩展常量表达式**支持，在 C++ 17 中有一个`std::apply`函数可以处理元组，并且可以在常量表达式中使用。此外，在 C++ 17 中，lambdas 默认为 constexpr，并且有一个 **if constexpr** (expr)。\n\nWriting your own solution would waste a lot of time and probably would not work on older compilers. So, `Boost.MPL` still remains one of the most suitable solutions for metaprogramming.\n\n# 请参见\n\n请参阅官方文档，尤其是*教程*部分，了解更多关于[http://boost.org/libs/mpl](http://boost.org/libs/mpl)的`Boost.MPL`信息。\n\n# 懒洋洋地评估元功能\n\n懒惰求值意味着在我们真正需要它的结果之前，不会调用函数。为了写出好的元功能，强烈推荐了解这个方法。懒惰评估的重要性将在下面的例子中展示。\n\n假设我们正在编写一些元函数，接受一个函数`Func`、一个参数`Param`和一个条件`Cond`。如果将`Cond`应用于`Param`返回`false`，则该函数的结果类型必须是`fallback`类型，否则结果必须是应用于`Param`的`Func`:\n\n```cpp\nstruct fallback;\n\ntemplate <\n        class Func,\n        class Param,\n        class Cond,\n        class Fallback = fallback>\nstruct apply_if; \n```\n\n这个元功能是我们离不开懒评价的地方，因为如果不满足`Cond`可能就无法将`Func`应用到`Param`上。这样的尝试总是会导致编译失败，`Fallback`永远不会返回。\n\n# 准备好\n\n强烈推荐阅读[第 4 章](04.html#6FSQK0-712b4ba1126a4c7c89e1d44de61b4bdd)*编译时技巧*。然而，良好的元编程知识应该就足够了。\n\n# 怎么做...\n\n盯紧小细节，比如不要在例子中调用`::type`:\n\n1.  我们需要以下标题:\n\n```cpp\n#include <boost/mpl/apply.hpp>\n#include <boost/mpl/eval_if.hpp>\n#include <boost/mpl/identity.hpp>\n```\n\n2.  函数的开头很简单:\n\n```cpp\ntemplate <class Func, class Param, class Cond, class Fallback>\nstruct apply_if {\n    typedef typename boost::mpl::apply<\n        Cond, Param\n    >::type condition_t;\n```\n\n3.  我们在这里要小心:\n\n```cpp\n    typedef boost::mpl::apply<Func, Param> applied_type; \n```\n\n4.  计算表达式时，必须格外小心:\n\n```cpp\n    typedef typename boost::mpl::eval_if_c<\n        condition_t::value,\n        applied_type,\n        boost::mpl::identity<Fallback>\n    >::type type;\n};\n```\n\n就这样！现在我们可以像这样自由使用它:\n\n```cpp\n#include <boost/static_assert.hpp>\n#include <boost/type_traits/is_integral.hpp>\n#include <boost/type_traits/make_unsigned.hpp>\n#include <boost/type_traits/is_same.hpp>\n\nusing boost::mpl::_1;\nusing boost::mpl::_2;\n\ntypedef apply_if<\n    boost::make_unsigned<_1>,\n    int,\n    boost::is_integral<_1>\n>::type res1_t;\n\nBOOST_STATIC_ASSERT((\n    boost::is_same<res1_t, unsigned int>::value\n));\n\ntypedef apply_if<\n    boost::make_unsigned<_1>,\n    float,\n    boost::is_integral<_1>\n>::type res2_t;\n\nBOOST_STATIC_ASSERT((\n    boost::is_same<res2_t, fallback>::value\n));\n```\n\n# 它是如何工作的...\n\n这个食谱的主要思想是，如果条件为`false`，我们就不能执行元功能，因为当条件为`false`时，有可能该类型的元功能不能应用:\n\n```cpp\n// Will fail with static assertion somewhere deeply in the implementation\n// of boost::make_unsigned<_1> if we do not evaluate the function lazily.\ntypedef apply_if<\n    boost::make_unsigned<_1>,\n    float,\n    boost::is_integral<_1>\n>::type res2_t;\n\nBOOST_STATIC_ASSERT((\n    boost::is_same<res2_t, fallback>::value\n));\n```\n\n那么，我们如何懒懒地评价一个元功能呢？\n\n如果无法访问元函数的内部类型或值，编译器会查看元函数内部。换句话说，当我们试图通过`::`获取元函数的一个成员时，编译器试图编译该元函数。这可以是打给`::type`或`::value`的电话。这就是`apply_if`的错误版本的样子:\n\n```cpp\ntemplate <class Func, class Param, class Cond, class Fallback>\nstruct apply_if {\n    typedef typename boost::mpl::apply<\n        Cond, Param\n    >::type condition_t;\n\n    // Incorrect: metafunction is evaluated when `::type` called.\n    typedef typename boost::mpl::apply<Func, Param>::type applied_type;\n\n    typedef typename boost::mpl::if_c<\n        condition_t::value,\n        applied_type,\n        boost::mpl::identity<Fallback>\n    >::type type;\n};\n```\n\n这与我们的例子不同，我们没有在*第 3 步*调用`::type`，而是使用`eval_if_c`实现了*第 4 步*，只为其中一个参数调用`::type`。`boost::mpl::eval_if_c`元功能是这样实现的:\n\n```cpp\ntemplate<bool C, typename F1, typename F2>\nstruct eval_if_c {\n    typedef typename if_c<C,F1,F2>::type f_;\n    typedef typename f_::type type; // call `::type` only for one parameter\n};\n```\n\n因为`boost::mpl::eval_if_c`调用`::type`表示成功条件，`fallback`没有`::type`，所以我们需要将`fallback`包装到`boost::mpl::identity`类中。这个类非常简单，但是很有用，它通过一个`::type`调用返回它的模板参数，并且不做任何事情:\n\n```cpp\ntemplate <class T> \nstruct identity { \n    typedef T type; \n}; \n```\n\n# 还有更多...\n\n正如我们已经提到的，C++ 11 没有`Boost.MPL`类，但是我们可以像`boost::mpl::identity<T>`一样用单个参数使用`std::common_type<T>`。\n\n和往常一样，元函数不会在输出二进制文件中添加一行，您可以根据需要多次使用元函数。编译时做得越多，留给运行时的时间就越少。\n\n# 请参见...\n\n*   `boost::mpl::identity`类型可用于禁用模板功能的**参数相关查找** ( **ADL** )。参见`<boost/implicit_cast.hpp>`标题中的`boost::implicit_cast`来源。\n*   从头开始读这一章，在[http://boost.org/libs/mpl](http://boost.org/libs/mpl)阅读`Boost.MPL`的官方文件可能会有帮助。\n\n# 将所有元组元素转换为字符串\n\n这个食谱和下一个食谱致力于混合编译时和运行时特性。我们将使用`Boost.Fusion`库，看看它能做什么。\n\n还记得我们在第一章中讨论的元组和数组吗？现在，我们想编写一个单一的函数，可以将元组和数组的元素流式传输到字符串中。\n\n![](img/00016.jpeg)\n\n# 准备好\n\n您应该知道`boost::tuple`和`boost::array`类以及`boost::lexical_cast`函数。\n\n# 怎么做...\n\n我们已经知道了几乎所有将在这个食谱中使用的函数和类。我们只需要把它们都聚集在一起:\n\n1.  我们需要编写一个将任何类型转换为字符串的函子:\n\n```cpp\n#include <boost/lexical_cast.hpp>\n#include <boost/noncopyable.hpp>\n\nstruct stringize_functor: boost::noncopyable {\nprivate:\n    std::string& result;\n\npublic:\n    explicit stringize_functor(std::string& res)\n        : result(res)\n    {}\n\n    template <class T>\n    void operator()(const T& v) const {\n        result += boost::lexical_cast<std::string>(v);\n    }\n};\n```\n\n2.  现在是代码的棘手部分:\n\n```cpp\n#include <boost/fusion/include/for_each.hpp>\n\ntemplate <class Sequence>\nstd::string stringize(const Sequence& seq) {\n    std::string result;\n    boost::fusion::for_each(seq, stringize_functor(result));\n    return result;\n}\n```\n\n仅此而已！现在，我们可以转换任何我们想要的字符串:\n\n```cpp\n#include <iostream>\n#include <boost/fusion/include/vector.hpp>\n#include <boost/fusion/adapted/boost_tuple.hpp>\n#include <boost/fusion/adapted/std_pair.hpp>\n#include <boost/fusion/adapted/boost_array.hpp>\n\nstruct cat{};\n\nstd::ostream& operator << (std::ostream& os, const cat& ) {\n    return os << \"Meow! \";\n}\n\nint main() {\n    boost::fusion::vector<cat, int, std::string> tup1(cat(), 0, \"_0\");\n    boost::tuple<cat, int, std::string> tup2(cat(), 0, \"_0\");\n    std::pair<cat, cat> cats;\n    boost::array<cat, 10> many_cats;\n\n    std::cout << stringize(tup1) << '\\n' \n        << stringize(tup2) << '\\n'\n        << stringize(cats) << '\\n'\n        << stringize(many_cats) << '\\n';\n}\n```\n\n前面的示例输出了以下内容:\n\n```cpp\n Meow! 0_0\n Meow! 0_0\n Meow! Meow! \n Meow! Meow! Meow! Meow! Meow! Meow! Meow! Meow! Meow! Meow!\n```\n\n# 它是如何工作的...\n\n`stringize`函数的主要问题是`boost::tuple`和`std::pair`都没有`begin()`和`end()`方法，所以我们不能调用`std::for_each`。这就是`Boost.Fusion`介入的地方。\n\n`Boost.Fusion`库包含了许多很棒的算法，可以在编译时操纵结构。\n\n`boost::fusion::for_each`函数迭代序列的元素，并为每个元素应用一个函子。\n\n请注意，我们包括:\n\n```cpp\n#include <boost/fusion/adapted/boost_tuple.hpp> \n#include <boost/fusion/adapted/std_pair.hpp> \n#include <boost/fusion/adapted/boost_array.hpp> \n```\n\n这是必需的，因为默认情况下`Boost.Fusion`只对自己的类起作用。`Boost.Fusion`有自己的元组类`boost::fusion::vector`，和`boost::tuple`相当接近:\n\n```cpp\n#include <string>\n#include <cassert>\n\n#include <boost/tuple/tuple.hpp>\n\n#include <boost/fusion/include/vector.hpp>\n#include <boost/fusion/include/at_c.hpp>\n\nvoid tuple_example() {\n    boost::tuple<int, int, std::string> tup(1, 2, \"Meow\");\n    assert(boost::get<0>(tup) == 1);\n    assert(boost::get<2>(tup) == \"Meow\");\n}\n\nvoid fusion_tuple_example() {\n    boost::fusion::vector<int, int, std::string> tup(1, 2, \"Meow\");\n    assert(boost::fusion::at_c<0>(tup) == 1);\n    assert(boost::fusion::at_c<2>(tup) == \"Meow\");\n}\n```\n\n但是`boost::fusion::vector`并不像`boost::tuple`那么简单。我们将在*分裂元组*配方中看到差异。\n\n# 还有更多...\n\n`boost::fusion::for_each`和`std::for_each`有一个根本区别。`std::for_each`函数内部包含一个循环，并在运行时决定必须进行多少次迭代。然而，`boost::fusion::for_each()`在编译时知道迭代次数并完全展开循环。对于`boost::tuple<cat, int, std::string> tup2`来说，`boost::fusion::for_each(tup2, functor)`呼叫相当于以下代码:\n\n```cpp\n    functor(boost::fusion::at_c<0>(tup2));\n    functor(boost::fusion::at_c<1>(tup2));\n    functor(boost::fusion::at_c<2>(tup2));\n```\n\nC++ 11 不包含`Boost.Fusion`类。`Boost.Fusion`的所有方法都很有效。他们在编译时尽可能地做，并且有一些非常高级的优化。\n\nC++ 14 增加了`std::integer_sequence`和`std::make_integer_sequence`来简化变量模板。使用这些实体，可以手动编写`boost::fusion::for_each`功能并实现`stringize`功能，而无需`Boost.Fusion`:\n\n```cpp\n#include <utility>\n#include <tuple>\n\ntemplate <class Tuple, class Func, std::size_t... I>\nvoid stringize_cpp11_impl(const Tuple& t, const Func& f, std::index_sequence<I...>) {\n    // Oops. Requires C++ 17 fold expressions feature.\n    // (f(std::get<I>(t)), ...);\n\n    int tmp[] = { 0, (f(std::get<I>(t)), 0)... };\n    (void)tmp; // Suppressing unused variable warnings.\n}\n\ntemplate <class Tuple>\nstd::string stringize_cpp11(const Tuple& t) {\n    std::string result;\n    stringize_cpp11_impl(\n        t,\n        stringize_functor(result),\n        std::make_index_sequence< std::tuple_size<Tuple>::value >()\n    );\n    return result;\n}\n```\n\n正如您可能看到的那样，许多代码都是为此而编写的，这样的代码并不容易阅读和理解。\n\nC++ 标准化工作组讨论了在 C++ 20 标准中增加类似于`constexpr for`的内容的想法。有了这个特性，有一天我们可以编写以下代码(语法可能会改变！):\n\n```cpp\ntemplate <class Tuple>\nstd::string stringize_cpp20(const Tuple& t) {\n    std::string result;\n    for constexpr(const auto& v: t) {\n        result += boost::lexical_cast<std::string>(v);\n    }\n    return result;\n}\n```\n\n在此之前，`Boost.Fusion`似乎是最便携、最简单的解决方案。\n\n# 请参见\n\n*   *分裂元组*配方将给出更多关于`Boost.Fusion`真实力量的信息\n*   `Boost.Fusion`的官方文档包含一些有趣的例子和完整的参考资料，可以在[http://boost.org/libs/fusion](http://boost.org/libs/fusion)找到\n\n# 拆分元组\n\n这个食谱将展示`Boost.Fusion`库的一小部分能力。我们将把一个元组分成两个元组，一个包含算术类型，另一个包含所有其他类型。\n\n![](img/00017.jpeg)\n\n# 准备好\n\n这个食谱需要了解`Boost.MPL`、占位符和`Boost.Tuple`。建议从头开始阅读这一章。\n\n# 怎么做...\n\n这可能是本章最难的食谱之一。结果类型在编译时确定，这些类型的值在运行时填充:\n\n1.  为了实现这种混合，我们需要以下头:\n\n```cpp\n#include <boost/fusion/include/remove_if.hpp>\n#include <boost/type_traits/is_arithmetic.hpp>\n```\n\n2.  现在，我们准备创建一个返回非算术类型的函数:\n\n```cpp\ntemplate <class Sequence>\ntypename boost::fusion::result_of::remove_if<\n    const Sequence, \n    boost::is_arithmetic<boost::mpl::_1> \n>::type get_nonarithmetics(const Sequence& seq) \n{\n    return boost::fusion::remove_if< \n        boost::is_arithmetic<boost::mpl::_1> \n    >(seq);\n}\n```\n\n3.  和一个返回算术类型的函数:\n\n```cpp\ntemplate <class Sequence>\ntypename boost::fusion::result_of::remove_if<\n    const Sequence, \n    boost::mpl::not_< boost::is_arithmetic<boost::mpl::_1> >\n>::type get_arithmetics(const Sequence& seq) \n{\n    return boost::fusion::remove_if< \n        boost::mpl::not_< boost::is_arithmetic<boost::mpl::_1> >\n    >(seq);\n}\n```\n\n就这样！现在，我们能够完成以下任务:\n\n```cpp\n#include <boost/fusion/include/vector.hpp>\n#include <cassert>\n#include <boost/fusion/include/at_c.hpp>\n#include <boost/blank.hpp>\n\nint main() {\n    typedef boost::fusion::vector<\n        int, boost::blank, boost::blank, float\n    > tup1_t;\n    tup1_t tup1(8, boost::blank(), boost::blank(), 0.0);\n\n    boost::fusion::vector<boost::blank, boost::blank> res_na\n        = get_nonarithmetics(tup1);\n    boost::fusion::vector<int, float> res_a = get_arithmetics(tup1);\n    assert(boost::fusion::at_c<0>(res_a) == 8);\n}\n```\n\n# 它是如何工作的...\n\n`Boost.Fusion`背后的思想是编译器在编译时知道结构布局，无论编译器在编译时知道什么，我们都可能同时改变。`Boost.Fusion`允许我们修改不同的序列，添加和删除字段，以及更改字段类型。这就是我们在*第二步*和*第三步*中所做的；我们从元组中移除了非必填字段。\n\n现在，让我们仔细看看`get_nonarithmetics`。首先，它的结果类型是使用以下结构推导出来的:\n\n```cpp\ntypename boost::fusion::result_of::remove_if<\n    const Sequence, \n    boost::is_arithmetic<boost::mpl::_1> \n>::type\n```\n\n这一定是我们熟悉的。在本章的【编译时获取函数结果类型】配方中，我们看到了类似这样的内容。`Boost.MPL`的占位符`boost::mpl::_1`与返回新序列类型的`boost::fusion::result_of::remove_if`元功能配合良好。\n\n现在，让我们进入函数内部，观察下面的代码:\n\n```cpp\n    return boost::fusion::remove_if< \n        boost::is_arithmetic<boost::mpl::_1> \n    >(seq);\n```\n\n请记住，编译器在编译时知道`seq`的所有类型。这意味着`Boost.Fusion`可以为`seq`的不同元素应用元功能，并为它们获取元功能结果。这也意味着`Boost.Fusion`知道如何将必填字段从旧结构复制到新结构。\n\nHowever, `Boost.Fusion` tries not to copy fields as long as possible.\n\n*步骤 3* 中的代码与*步骤 2* 中的代码非常相似，但是它有一个用于移除非必需类型的否定谓词。\n\n我们的功能可以用于`Boost.Fusion`支持的任何类型，而不仅仅是`boost::fusion::vector`。\n\n# 还有更多...\n\n您可以对`Boost.Fusion`容器使用`Boost.MPL`功能。你只需要包括`#include <boost/fusion/include/mpl.hpp>`:\n\n```cpp\n#include <boost/fusion/include/mpl.hpp>\n#include <boost/mpl/transform.hpp>\n#include <boost/type_traits/remove_const.hpp>\n\ntemplate <class Sequence>\nstruct make_nonconst: boost::mpl::transform<\n    Sequence,\n    boost::remove_const<boost::mpl::_1>\n> {};\n\ntypedef boost::fusion::vector<\n    const int, const boost::blank, boost::blank\n> type1;\ntypedef make_nonconst<type1>::type nc_type;\n\nBOOST_STATIC_ASSERT((boost::is_same<\n    boost::fusion::result_of::value_at_c<nc_type, 0>::type,\n    int\n>::value));\n\nBOOST_STATIC_ASSERT((boost::is_same<\n    boost::fusion::result_of::value_at_c<nc_type, 1>::type,\n    boost::blank\n>::value));\n\nBOOST_STATIC_ASSERT((boost::is_same<\n    boost::fusion::result_of::value_at_c<nc_type, 2>::type,\n    boost::blank\n>::value));\n```\n\nWe used `boost::fusion::result_of::value_at_c` instead of `boost::fusion::result_of::at_c` because `boost::fusion::result_of::at_c` returns the exact return type of the `boost::fusion::at_c` call, which is a reference. `boost::fusion::result_of::value_at_c` returns type without a reference.\n\n`Boost.Fusion`和`Boost.MPL`库不是 C++ 17 的一部分。`Boost.Fusion`速度极快。它有很多优化。\n\n值得一提的是，我们只看到了`Boost.Fusion`能力的极小一部分。关于它可以单独写一本书。\n\n# 请参见\n\n*   `Boost.Fusion`的良好教程和完整文档可在[http://boost.org/libs/fusion](http://boost.org/libs/fusion)获得\n*   您也可能希望在 http://boost.org/libs/mpl 看到`Boost.MPL`的官方文档\n\n# 在 C++ 14 中操作异构容器\n\n我们在本章中看到的大多数元编程技巧早在 C++ 11 之前就已经发明了。可能，你已经听说过一些。\n\n全新的怎么样？用一个把元编程颠倒过来并让你的眉毛上扬的库来实现 C++ 14 中以前的食谱怎么样？系好安全带，我们正在潜入`Boost.Hana`的世界。\n\n# 准备好\n\n这个食谱需要 C++ 11 和 C++ 14 的知识，尤其是 lambdas。您将需要一个真正的 C++ 14 兼容编译器来编译这个例子。\n\n# 怎么做...\n\n现在，让我们用`Boost.Hana`的方式来做一切:\n\n1.  从包含标题开始:\n\n```cpp\n#include <boost/hana/traits.hpp>\n```\n\n2.  我们创建一个`is_arithmetic_`功能对象:\n\n```cpp\nconstexpr auto is_arithmetic_ = [](const auto& v) {\n    auto type = boost::hana::typeid_(v);\n    return boost::hana::traits::is_arithmetic(type);\n};\n```\n\n3.  现在，我们实现`get_nonarithmetics`功能:\n\n```cpp\n#include <boost/hana/remove_if.hpp>\n\ntemplate <class Sequence>\nauto get_nonarithmetics(const Sequence& seq)  {\n    return boost::hana::remove_if(seq, [](const auto& v) {\n        return is_arithmetic_(v);\n    });\n}\n```\n\n4.  让我们反过来定义`get_arithmetics`。只是为了好玩！\n\n```cpp\n#include <boost/hana/filter.hpp>\n\nconstexpr auto get_arithmetics = [](const auto& seq) {\n    return boost::hana::filter(seq, is_arithmetic_);\n};\n```\n\n就这样。现在，我们可以使用这些功能:\n\n```cpp\n#include <boost/hana/tuple.hpp>\n#include <boost/hana/integral_constant.hpp>\n#include <boost/hana/equal.hpp>\n#include <cassert>\n\nstruct foo {\n    bool operator==(const foo&) const { return true; }\n    bool operator!=(const foo&) const { return false; }\n};\n\nint main() {\n    const auto tup1\n        = boost::hana::make_tuple(8, foo{}, foo{}, 0.0);\n\n    const auto res_na = get_nonarithmetics(tup1);\n    const auto res_a = get_arithmetics(tup1);\n\n    using boost::hana::literals::operator \"\"_c;\n    assert(res_a[0_c] == 8);\n\n    const auto res_na_expected = boost::hana::make_tuple(foo(), foo());\n    assert(res_na == res_na_expected);\n}\n```\n\n# 它是如何工作的...\n\n乍一看，代码似乎很简单，但事实并非如此。`Boost.Hana`把元编程反过来了！在前面的食谱中，我们直接使用类型，但是`Boost.Hana`创建了一个保存类型的变量，并且大多数时候使用变量。\n\n看看第二步中的`typeid_`呼叫:\n\n```cpp\nauto type = boost::hana::typeid_(v);\n```\n\n它实际上返回一个变量。关于类型的信息现在隐藏在`type`变量中，可以通过调用`decltype(type)::type`来提取。\n\n但是让我们一行一行地移动。在*步骤 2 中，*我们将通用λ存储到`is_arithmetic_`变量中。从这一点来看，我们可以将该变量用作功能对象。在 lambda 中，我们创建了一个`type`变量，它现在保存了关于`v`类型的信息。下一行是围绕`std::is_arithmetic`的特殊包装，它从`type`变量中提取关于`v`类型的信息，并将其传递给`std::is_arithmetic`特征。调用的结果是一个布尔积分常数。\n\n现在，神奇的部分！存储在`is_arithmetic_`变量中的 Lambda 实际上从未被`boost::hana::remove_if`和`boost::hana::filter`函数调用过。所有使用它的`Boost.Hana`函数只需要 lambda 函数的结果类型，而不需要它的主体。我们可以放心地更改定义，整个示例将继续运行良好:\n\n```cpp\nconstexpr auto is_arithmetic_ = [] (const auto& v) {\n    assert(false);\n    auto type = boost::hana::typeid_(v);\n    return boost::hana::traits::is_arithmetic(type);\n};\n```\n\n在*步骤 3* 和 *4 中，*我们分别调用`boost::hana::remove_if`和`boost::hana::filter`函数。在*步骤 3 中，*我们在λ内部使用了`is_arithmetic_`。在*第四步，*我们直接用了。你可以使用任何你喜欢的语法，这只是习惯问题。\n\n最后在`main()`中，我们检查一切都如预期的那样工作，并且索引 0 所代表的元组中的元素等于`8`:\n\n```cpp\n    using boost::hana::literals::operator \"\"_c;\n    assert(res_a[0_c] == 8);\n```\n\nThe best way to understand the `Boost.Hana` library is to experiment with it. You can do it online at [http://apolukhin.github.io/Boost-Cookbook/](http://apolukhin.github.io/Boost-Cookbook/).\n\n# 还有更多...\n\n有一个小细节没有描述。`operator[]`的元组访问是如何工作的？不可能有一个函数返回不同的类型！\n\n如果你第一次遇到这个把戏，这很有趣。`Boost.Hana`的`operator \"\"_c`处理文字，并根据文字构造不同的类型:\n\n*   如果写`0_c`，则返回`integral_constant<long long, 0>`\n*   如果写`1_c`，则返回`integral_constant<long long, 1>`\n*   如果写`2_c`，则返回`integral_constant<long long, 2>`\n\n`boost::hana::tuple`类实际上有很多`operator[]`重载，接受不同类型的`integral_constant`。根据整型常量的值，返回正确的元组元素。例如，如果你写`some_tuple[1_c]`，那么`tuple::operator[](integral_constant<long long, 1>)`被索引称为元素`1`被返回。\n\n`Boost.Hana`不是 C++ 17 的一部分。然而，该库的作者参加了 C++ 标准化会议，并提出了不同的有趣的东西来纳入 C++ 标准。\n\n如果你期望从`Boost.Hana`比从`Boost.MPL`获得数量级更好的编译时间，那么不要。目前，编译器没有很好地处理`Boost.Hana`方法。这可能有一天会改变。\n\nIt's worth looking at the source codes of the `Boost.Hana` library to discover new interesting ways of using C++ 14 features. All the Boost libraries could be found at GitHub [https://github.com/boostorg.](https://github.com/boostorg)\n\n# 请参见\n\n官方文档有更多的例子，一个完整的参考部分，一些更多的教程，和一个编译时性能部分。在 http://boost.org/libs/hana.享受`Boost.Hana`图书馆"
  },
  {
    "path": "docs/boost-cpp-app-dev-cb/09.md",
    "content": "# 九、容器\n\n在本章中，我们将介绍:\n\n*   在序列容器中存储一些元素\n*   在序列容器中存储最多 N 个元素\n*   以超快的方式比较字符串\n*   使用无序集和映射\n*   制作地图，其中值也是一个关键\n*   使用多索引容器\n*   获得单一链表和内存池的好处\n*   使用平面关联容器\n\n# 介绍\n\n本章专门介绍 Boost 容器以及与它们直接相关的东西。它提供了关于可以在日常编程中使用的 Boost 类的信息，这将使您的代码更快，新应用的开发更容易。\n\n容器的不同不仅在于功能，还在于它的一些成员的效率(复杂性)。关于复杂性的知识对于编写快速应用至关重要。本章不仅仅向您介绍一些新的容器，它还为您提供了何时以及何时不使用特定类型的容器或其方法的提示。\n\n那么，让我们开始吧！\n\n# 在序列容器中存储一些元素\n\n在过去的二十年里，C++ 程序员使用`std::vector`作为默认的序列容器。它是一个快速的容器，不做大量的分配，以一种对 CPU 缓存友好的方式存储元素，并且因为容器像函数一样连续存储元素`std::vector::data()`允许与纯 C 函数交互操作。\n\n但是，我们想要更多！有些情况下，我们确实知道要存储在向量中的典型元素数，我们需要通过完全消除这种情况下的内存分配来提高向量的性能。\n\n想象一下，我们正在编写一个处理银行交易的高性能系统。**事务**是一系列操作，如果其中至少一个操作失败，这些操作必须全部成功或失败。我们知道 99%的事务包含 8 个或更少的操作，并希望加快速度:\n\n```cpp\n#include <vector>\n\nclass operation;\n\ntemplate <class T>\nvoid execute_operations(const T&);\n\nbool has_operation();\noperation get_operation();\n\nvoid process_transaction_1() {\n    std::vector<operation> ops;\n    ops.reserve(8); // TODO: Memory allocation. Not good!\n\n    while (has_operation()) {\n        ops.push_back(get_operation());\n    }\n\n    execute_operations(ops);\n    // ...\n}\n```\n\n# 准备好\n\n这个食谱只需要标准库和 C++ 的基础知识。\n\n# 怎么做...\n\n这将是本书最简单的任务，感谢`Boost.Container`图书馆:\n\n1.  包括适当的标题:\n\n```cpp\n#include <boost/container/small_vector.hpp>\n```\n\n2.  将`std::vector`替换为`boost::container::small_vector`并放弃`reserve()`呼叫:\n\n```cpp\nvoid process_transaction_2() {\n    boost::container::small_vector<operation, 8> ops;\n\n    while (has_operation()) {\n        ops.push_back(get_operation());\n    }\n\n    execute_operations(ops);\n    // ...\n}\n```\n\n# 它是如何工作的...\n\n`boost::container::small_vector`的第二个模板参数是要在堆栈上预分配的元素计数。所以如果很多时候我们要在向量中存储 8 个或者更少的元素，我们只需要把`8`作为第二个模板参数。\n\n如果我们必须在容器中存储 8 个以上的元素，那么`small_vector`的行为与`std::vector`完全一样，并动态分配一大块内存来存储 8 个以上的元素。就像`std::vector`一样，`small_vector`是一个带有**随机访问迭代器**的序列容器，它一致地存储元素。\n\n总而言之，`boost::container::small_vector`是一个行为与`std::vector`完全相同的容器，但是允许为编译时指定数量的元素避免内存分配。\n\n# 还有更多...\n\n使用`small_vector`的一个缺点是我们的元素计数假设泄漏到接受`small_vector`作为参数的函数签名中。因此，如果我们有三个分别专用于`4`、`8`和`16`元素的函数，并且所有这些函数都使用前面示例中的`execute_operations`处理事务，那么我们将得到`execute_operations`函数的多个实例化:\n\n```cpp\nvoid execute_operations(\n    const boost::container::small_vector<operation, 4>&);\n\nvoid execute_operations(\n    const boost::container::small_vector<operation, 8>&);\n\nvoid execute_operations(\n    const boost::container::small_vector<operation, 16>&);\n```\n\n那可不好！现在，我们的可执行文件中有多个函数，它们做完全相同的事情，并且由几乎完全相同的机器代码组成。这导致了更大的二进制文件，更长的可执行文件启动时间，更长的编译和链接时间。一些编译器可能会消除冗余，但可能性很低。\n\n不过，解决办法很简单。`boost::container::small_vector`来源于独立于预分配元素计数的`boost::container::small_vector_base`类型:\n\n```cpp\nvoid execute_operations(\n    const boost::container::small_vector_base<operation>& ops\n);\n```\n\n就这样！现在，我们可以在任何`boost::container::small_vector`上使用新的`execute_operations`函数，而没有膨胀二进制大小的风险。\n\nC++ 17 没有`small_vector`这样的类。有人提议将`small_vector`纳入将于 2020 年左右推出的下一个 C++ 标准。\n\n# 请参见\n\n*   http://boost.org/libs/container 图书馆有许多有趣课程的完整参考文献\n*   `small_vector`来自 **LLVM** 项目的 Boost 您可以在始发地[http://llvm . org/docs/programmersmanual . html # llvm-ADT-small vector-h](http://llvm.org/docs/ProgrammersManual.html#llvm-adt-smallvector-h)阅读关于集装箱的信息\n\n# 在序列容器中最多存储 N 个元素\n\n这里有一个问题:如果我们知道序列从来没有超过 *N* 元素和 *N* 不大，我们应该用什么容器从函数返回序列。例如，我们必须如何编写最多返回五个事件的`get_events()`函数:\n\n```cpp\n#include <vector>\nstd::vector<event> get_events();\n```\n\n`std::vector<event>`分配内存，所以之前的代码不是一个好的解决方案。\n\n```cpp\n#include <boost/array.hpp>\nboost::array<event, 5> get_events();\n```\n\n`boost::array<event, 5>`不分配内存，而是构造所有的五行。少于五个元素没办法返回。\n\n```cpp\n#include <boost/container/small_vector.hpp>\nboost::container::small_vector<event, 5> get_events();\n```\n\n`boost::container::small_vector<event, 5>`不为五个或更少的元素分配内存，允许我们返回五个以下的元素。但是，解决方案并不完美，因为从函数接口来看，它从不返回超过五个元素并不明显。\n\n# 准备好\n\n这个食谱只需要标准库和 C++ 的基础知识。\n\n# 怎么做...\n\n`Boost.Container`有一个容器可以完美满足我们的需求:\n\n```cpp\n#include <boost/container/static_vector.hpp>\nboost::container::static_vector<event, 5> get_events();\n```\n\n# 它是如何工作的...\n\n`boost::container::static_vector<T, N>`是一个不分配内存的容器，只能容纳编译时指定数量的元素。想象一下`boost::container::small_vector<T, N>`不能动态分配内存，任何存储超过 *N 个*元素的尝试都会导致`std::bad_alloc`异常:\n\n```cpp\n#include <cassert>\n\nint main () {\n    boost::container::static_vector<event, 5> ev = get_events();\n    assert(ev.size() == 5);\n\n    boost::container::static_vector<int, 2> ints;\n    ints.push_back(1);\n    ints.push_back(2);\n    try {\n        // The following line always throws:\n        ints.push_back(3);\n    } catch (const std::bad_alloc& ) {\n        // ...\n    }\n}\n```\n\n就像`Boost.Container`库的所有容器一样，`static_vector`支持**移动语义**并使用 Boost 模拟右值引用。如果编译器不支持右值，请移动库。\n\n# 还有更多...\n\n如果用户插入一个元素，并且无法将新值放入已经分配的内存中，则`std::vector`会分配更大的内存块。在这种情况下，`std::vector`将元素从旧位置移动到新位置，如果这些元素不是行移动可构造的。否则，`std::vector`将元素复制到新的位置，然后为旧位置的每个元素调用析构函数。\n\n正因为如此，行为`std::vector`对于许多成员函数来说具有不变的复杂性。`static_vector`从不分配内存，因此它不必将元素从旧位置移动或复制到新位置。正因为如此，对于`std::vector`而言具有**摊销的 O(1)** 复杂度的操作对于`boost::container::static_vector`而言具有真正的 O(1)复杂度。这对于一些实时应用来说可能很方便；不过，要小心例外！\n\nSome people still prefer to pass output parameters by reference instead of returning them: `void get_events(static_vector<event, 5>& result_out)`. They think that this way, there's a guarantee that no copying of result happens. Don't do that, it makes things worse! C++ compilers have a whole bunch of optimizations, such as **Return Value Optimization** (**RVO**) and **Named Return Value Optimization** (**NRVO**); different platforms have agreements nailed down in ABI that code with `retun something;` does not result in an unnecessary copy and so forth. No copying happens already. However, when you pass a value, the reference compiler just does not see where the value came from and may assume that it aliases some other value in the scope. This may significantly degrade performance.\n\nC++ 17 没有`static_vector`类，目前也没有计划将其加入 C++ 20。\n\n# 请参见\n\n`Boost.Container`的官方文档有一个详细的参考部分，描述了`boost::container::static_vector`类的所有成员函数。参考[http://boost.org/libs/container.](http://boost.org/libs/container)\n\n[](http://boost.org/libs/container)\n\n# 以超快的方式比较字符串\n\n操纵字符串是一项常见的任务。在这里，我们将看到如何使用一些简单的技巧快速完成字符串比较操作。这个配方是下一个配方的蹦床，这里描述的技术将用于实现恒定的时间复杂度搜索。\n\n因此，我们需要创建一个能够快速比较字符串是否相等的类。我们将制作一个模板函数来测量比较的速度:\n\n```cpp\n#include <string>\n\ntemplate <class T>\nstd::size_t test_default() {\n    // Constants\n    const std::size_t ii_max = 200000;\n    const std::string s(\n        \"Long long long string that \"\n        \"will be used in tests to compare \"\n        \"speed of equality comparisons.\"\n    );\n\n    // Making some data, that will be \n    // used in comparisons.\n    const T data1[] = {\n        T(s),\n        T(s + s),\n        T(s + \". Whooohooo\"),\n        T(std::string(\"\"))\n    };\n\n    const T data2[] = {\n        T(s),\n        T(s + s),\n        T(s + \". Whooohooo\"),\n        T(std::string(\"\"))\n    };\n\n    const std::size_t data_dimensions = sizeof(data1) / sizeof(data1[0]);\n\n    std::size_t matches = 0u;\n    for (std::size_t ii = 0; ii < ii_max; ++ ii) {\n        for (std::size_t i = 0; i < data_dimensions; ++ i) {\n            for (std::size_t j = 0; j < data_dimensions; ++ j) {\n                if (data1[i] == data2[j]) {\n                    ++ matches;\n                }\n            }\n        }\n    }\n\n    return matches;\n}\n```\n\n# 准备好\n\n这个食谱只需要标准库和 C++ 的基础知识。\n\n# 怎么做...\n\n我们将使`std::string`成为我们自己类中的一个公共字段，并将所有的比较代码添加到我们的类中，而不需要编写助手方法来处理存储的`std::string`，如下步骤所示:\n\n1.  为此，我们需要以下标题:\n\n```cpp\n#include <boost/functional/hash.hpp>\n```\n\n2.  现在，我们可以创建我们的`fast comparison_`类:\n\n```cpp\nstruct string_hash_fast {\n    typedef std::size_t comp_type;\n\n    const comp_type     comparison_;\n    const std::string   str_;\n\n    explicit string_hash_fast(const std::string& s)\n        : comparison_(\n            boost::hash<std::string>()(s)\n        )\n        , str_(s)\n    {}\n};\n```\n\n3.  不要忘记定义`equality comparisons`操作符:\n\n```cpp\ninline bool operator == (\n    const string_hash_fast& s1, const string_hash_fast& s2)\n{\n    return s1.comparison_ == s2.comparison_ && s1.str_ == s2.str_;\n}\n\ninline bool operator != (\n    const string_hash_fast& s1, const string_hash_fast& s2)\n{\n    return !(s1 == s2);\n}\n```\n\n4.  就这样！现在，我们可以使用以下代码运行测试并查看结果:\n\n```cpp\n#include <iostream> \n#include <iostream>\n#include <cassert>\n\nint main(int argc, char* argv[]) {\n    if (argc < 2) {\n        assert(\n            test_default<string_hash_fast>()\n            ==\n            test_default<std::string>()\n        );\n        return 0;\n    }\n\n    switch (argv[1][0]) {\n    case 'h':\n        std::cout << \"HASH matched: \"\n                  << test_default<string_hash_fast>();\n        break;\n\n    case 's':\n        std::cout << \"STD matched: \"\n                  << test_default<std::string>();\n        break;\n\n    default:\n        return 2;\n    }\n}\n```\n\n# 它是如何工作的...\n\n字符串的比较很慢，因为如果字符串长度相等，我们需要逐个比较字符串的所有字符。相反，我们用整数的比较代替字符串的比较。这是通过`hash`函数实现的，该函数对字符串进行短固定长度的表示。\n\n我们来谈谈苹果的`hash`值。想象一下，你有两个带标签的苹果，如下图所示，你希望检查这两个苹果的品种是否相同。比较这些苹果最简单的方法是通过标签进行比较。否则，您将失去大量时间来根据颜色、大小、形状和其他参数比较苹果。哈希类似于反映对象价值的标签。\n\n![](img/00018.jpeg)\n\n现在，让我们一步一步来。\n\n在*步骤 1* 中，我们包括了包含`hash`函数定义的头文件。在*步骤 2* 中，我们声明了新的`string`类，它包含`str_`，这是字符串的原始值，`comparison_`，这是计算出的`hash`值。注意结构:\n\n```cpp\n    boost::hash<std::string>()(s) \n```\n\n这里，`boost::hash<std::string>`是一个结构，一个功能对象，就像`std::negate<>`一样。这就是为什么我们需要第一个括号——我们构造这个函数对象。内有`s`的第二个括号是对`std::size_t operator()(const std::string& s)`的调用，它计算`hash`值。\n\n现在，看看*第三步*，我们在这里定义`operator==`:\n\n```cpp\n    return s1.comparison_ == s2.comparison_ && s1.str_ == s2.str_; \n```\n\n额外注意表达式的第二部分。哈希操作会丢失信息，这意味着可能有多个字符串产生完全相同的`hash`值。这意味着如果哈希值不匹配，可以 100%保证字符串不匹配；否则，我们需要使用传统方法比较字符串。\n\n是时候比较一下数字了。如果我们使用默认的比较方法来测量执行时间，它会给我们 819 毫秒；然而，我们的散列比较工作快了将近两倍，并且在 475 毫秒内完成。\n\n# 还有更多...\n\nC++ 11 有`hash`功能对象；您可以在`std::`名称空间的`<functional>`标题中找到它。Boost 和标准库中的哈希算法快速可靠。它不分配额外的内存，也没有虚拟功能。\n\n您可以为自己的类型专门散列。在 Boost 中，这是通过在自定义类型的名称空间中专门化`hash_value`函数来完成的:\n\n```cpp\n// Must be in the namespace of string_hash_fast class.\ninline std::size_t hash_value(const string_hash_fast& v) {\n    return v.comparison_;\n}\n```\n\n这与`std::hash`的标准库专门化不同，标准库专门化要求您对`std::`命名空间中的`hash<>`结构进行模板专门化。\n\nBoost 中的哈希是为所有基本类型(如`int`、`float`、`double`和`char`)定义的，为数组定义的，也为所有标准库容器定义的，包括`std::array`、`std::tuple`和`std::type_index`。一些库也提供散列专门化，例如`Boost.Variant`库可以散列任何`boost::variant`类。\n\n# 请参见\n\n*   阅读本章中的*使用无序集和映射*方法，了解更多关于散列函数用法的信息。\n*   `Boost.Functional/Hash`的官方文档会告诉你如何组合多个哈希，并提供更多例子；在[http://boost.org/libs/functional/hash](http://boost.org/libs/functional/hash)阅读。\n\n# 使用无序集和映射\n\n在前面的配方中，我们看到了如何使用哈希优化字符串比较。读完之后，可能会出现以下问题:我们能否制作一个容器来缓存散列值，以便更快地进行比较？\n\n答案是肯定的，我们可以做得更多。我们可以实现几乎恒定的元素搜索、插入和移除时间。\n\n# 准备好\n\n需要 C++ 和 STL 容器的基本知识。阅读之前的食谱也会有所帮助。\n\n# 怎么做...\n\n这将是所有食谱中最简单的:\n\n1.  如果你想使用地图，你只需要包含`<boost/unordered_map.hpp>`标题。如果我们希望使用器械包，请包含`<boost/unordered_set.hpp>`标题。\n2.  现在，您可以自由使用`boost::unordered_map`代替`std::map`、`boost::unordered_set`代替`std::set`:\n\n```cpp\n#include <boost/unordered_set.hpp>\n#include <string>\n#include <cassert>\n\nvoid example() {\n    boost::unordered_set<std::string> strings;\n\n    strings.insert(\"This\");\n    strings.insert(\"is\");\n    strings.insert(\"an\");\n    strings.insert(\"example\");\n\n    assert(strings.find(\"is\") != strings.cend());\n}\n```\n\n# 它是如何工作的...\n\n无序容器存储值并记住每个值的散列。现在，如果您希望在其中找到一个值，他们将计算该值的散列，并在容器中搜索该散列。在找到散列之后，容器检查找到的值和搜索到的值是否相等。然后，返回值或容器末尾的迭代器。\n\n因为容器可能会搜索恒定宽度的整数哈希值，所以它可能会使用一些仅适用于整数的优化和算法。当传统的`std::set`和`std::map`提供更差的复杂度 O(log(N))，其中 *N* 是容器中的元素数量时，这些算法保证了恒定的搜索复杂度 O(1)。这就导致了一种情况，传统的`std::set`或`std::map`中的元素越多，它的工作速度就越慢。然而，无序容器的性能不依赖于元素数量。\n\n如此出色的表现从来都不是免费的。在无序容器中，值是无序的(你并不惊讶，是吗？).这意味着我们将是容器的元素，从`begin()`到`end()`将是输出，如下所示:\n\n```cpp\ntemplate <class T>\nvoid output_example() {\n    T strings;\n\n    strings.insert(\"CZ\");\n    strings.insert(\"CD\");\n    strings.insert(\"A\");\n    strings.insert(\"B\");\n\n    std::copy(\n        strings.begin(),\n        strings.end(),\n        std::ostream_iterator<std::string>(std::cout, \"  \")\n    );\n}\n```\n\n我们将得到`std::set`和`boost::unordered_set`的以下输出:\n\n```cpp\n boost::unordered_set<std::string> : B A CD CZ\n std::set<std::string> : A B CD CZ\n```\n\n那么，性能相差多少？通常，这取决于实施质量。我有以下数字:\n\n```cpp\nFor 100 elements:\nBoost: map is 1.69954 slower than unordered map\nStd: map is 1.54316 slower than unordered map\n\nFor 1000 elements:\nBoost: map is 4.13714 slower than unordered map\nStd: map is 2.12495 slower than unordered map\n\nFor 10000 elements:\nBoost: map is 2.04475 slower than unordered map\nStd: map is 2.23285 slower than unordered map\n\nFor 100000 elements:\nBoost: map is 1.67128 slower than unordered map\nStd: map is 1.68169 slower than unordered map\n```\n\n使用以下代码块测量性能:\n\n```cpp\n    T map;\n\n    for (std::size_t ii = 0; ii < ii_max; ++ ii) {\n        map[s + boost::lexical_cast<std::string>(ii)] = ii;\n    }\n\n    // Asserting.\n    for (std::size_t ii = 0; ii < ii_max; ++ ii) {\n        assert(map[s + boost::lexical_cast<std::string>(ii)] == ii);\n    }\n```\n\nThe code contains a lot of string constructions, so it is not 100% correct to measure the speedup using this test. It is here to show that unordered containers are usually faster than ordered ones.\n\n有时，当我们需要在无序容器中使用用户定义的类型时，可能会出现一个任务:\n\n```cpp\nstruct my_type { \n    int         val1_; \n    std::string val2_; \n}; \n```\n\n为此，我们需要为该类型编写一个比较运算符:\n\n```cpp\ninline bool operator == (const my_type& v1, const my_type& v2) {\n    return v1.val1_ == v2.val1_ && v1.val2_ == v2.val2_;\n} \n```\n\n我们还需要专门化该类型的散列函数。如果类型由多个字段组成，我们通常只需要将参与`equality comparisons`的所有字段的哈希值进行组合即可:\n\n```cpp\nstd::size_t hash_value(const my_type& v) { \n    std::size_t ret = 0u; \n\n    boost::hash_combine(ret, v.val1_); \n    boost::hash_combine(ret, v.val2_); \n    return ret; \n} \n```\n\nIt is highly recommended to combine hashes using the `boost::hash_combine` function.\n\n# 还有更多...\n\n容器也有多版本，在`<boost/unordered_set.hpp>`头中定义`boost::unordered_multiset`，在`<boost/unordered_map.hpp>`头中定义`boost::unordered_multimap`。就像标准库一样，容器的多版本能够存储多个相等的键值。\n\n所有无序容器都允许您指定自己的散列函数，而不是默认的`boost::hash`。它们也允许你专门化你自己的相等比较函子，而不是默认的`std::equal_to`。\n\nC++ 11 拥有 Boost 库的所有无序容器。您可以在标题中找到它们:`<unordered_set>`和`<unordered_map>`，在`std::`命名空间中，而不是`boost::`。Boost 和标准库版本的性能可能不同，但必须以相同的方式工作。然而，Boost 的无序容器即使在 C++ 03/C++ 98 编译器上也是可用的，并且利用了`Boost.Move`的右值引用仿真，所以即使在 C++ 11 之前的编译器上，您也可以将这些容器用于只移动类。\n\nC++ 11 没有`hash_combine`函数，所以你得自己写:\n\n```cpp\ntemplate <class T> \ninline void hash_combine(std::size_t& seed, const T& v) \n{ \n    std::hash<T> hasher; \n    seed ^= hasher(v) + 0x9e3779b9 + (seed<<6) + (seed>>2); \n} \n```\n\n或者直接用`boost::hash_combine`。\n\n自 Boost 1.64 以来，Boost 中的无序容器具有 C++ 17 的提取和插入节点的功能。\n\n# 请参见\n\n*   食谱*使用 C++ 11 移动仿真*在[第 1 章](01.html#RL0A0-712b4ba1126a4c7c89e1d44de61b4bdd)、*开始编写你的应用*，了解更多关于`Boost.Move`的右值参考仿真的细节\n*   更多关于无序集装箱的信息可在官方网站[http://boost.org/libs/unordered](http://boost.org/libs/unordered)获得\n*   有关组合散列和计算范围散列的更多信息，请访问[http://boost.org/libs/functional/hash](http://boost.org/libs/functional/hash)\n\n# 制作地图，价值也是关键\n\n一年中有几次，我们需要可以存储和索引一对值的东西。此外，我们需要使用第二个获得配对的第一部分，并使用第一个获得第二部分。迷茫？我给你举个例子。我们创建了一个词汇班。当用户将值放入其中时，类必须返回标识符，当用户将标识符放入其中时，类必须返回值。\n\n更实际的是，用户将登录名放在我们的词汇表中，并希望从中获得唯一的标识符。他们还希望获得标识符的所有登录信息。\n\n让我们看看如何使用 Boost 实现它。\n\n# 准备好\n\n本食谱需要标准库和模板的基本知识。\n\n# 怎么做...\n\n这个食谱是关于`Boost.Bimap`库的能力的。让我们看看如何使用它来实现这个任务:\n\n1.  我们需要以下内容:\n\n```cpp\n#include <iostream>\n#include <boost/bimap.hpp>\n#include <boost/bimap/multiset_of.hpp>\n```\n\n2.  现在，我们准备制作我们的词汇结构:\n\n```cpp\nint main() {\n    typedef boost::bimap<\n        std::string,\n        boost::bimaps::multiset_of<std::size_t>\n    > name_id_type;\n\n    name_id_type name_id;\n```\n\n3.  可以使用以下语法填充:\n\n```cpp\n    // Inserting keys <-> values\n    name_id.insert(name_id_type::value_type(\n        \"John Snow\", 1\n    ));\n\n    name_id.insert(name_id_type::value_type(\n        \"Vasya Pupkin\", 2\n    ));\n\n    name_id.insert(name_id_type::value_type(\n        \"Antony Polukhin\", 3\n    ));\n\n    // Same person as \"Antony Polukhin\"\n    name_id.insert(name_id_type::value_type(\n        \"Anton Polukhin\", 3\n    ));\n```\n\n4.  我们可以像处理地图一样处理它的左边部分:\n\n```cpp\n    std::cout << \"Left:\\n\";\n\n    typedef name_id_type::left_const_iterator left_const_iterator;\n    const left_const_iterator lend = name_id.left.end();\n\n    for (left_const_iterator it = name_id.left.begin();\n         it!= lend;\n         ++ it)\n    {\n        std::cout << it->first << \" <=> \" << it->second << '\\n';\n    }\n```\n\n5.  右边部分和左边几乎一样:\n\n```cpp\n    std::cout << \"\\nRight:\\n\";\n\n    typedef name_id_type::right_const_iterator right_const_iterator;\n    const right_const_iterator rend = name_id.right.end();\n\n    for (right_const_iterator it = name_id.right.begin();\n         it!= rend;\n         ++ it)\n    {\n        std::cout << it->first << \" <=> \" << it->second << '\\n';\n    }\n```\n\n6.  我们还需要确保词汇中有这样一个人:\n\n```cpp\n    assert(\n        name_id.find(name_id_type::value_type(\n            \"Anton Polukhin\", 3\n        )) != name_id.end()\n    );\n} /* end of main() */\n```\n\n就是这样，现在如果我们把所有的代码(除了 includes)放在`int main()`里面，我们会得到如下输出:\n\n```cpp\n Left:\n Anton Polukhin <=> 3\n Antony Polukhin <=> 3\n John Snow <=> 1\n Vasya Pupkin <=> 2\n\n Right:\n 1 <=> John Snow\n 2 <=> Vasya Pupkin\n 3 <=> Antony Polukhin\n 3 <=> Anton Polukhin\n```\n\n# 它是如何工作的...\n\n在*步骤 2* 中，我们定义了`bimap`类型:\n\n```cpp\n    typedef boost::bimap< \n        std::string, \n        boost::bimaps::multiset_of<std::size_t> \n    > name_id_type; \n```\n\n第一个模板参数告诉第一个键必须有类型`std::string`，应该作为`std::set`工作。第二个模板参数告诉第二个键必须有类型`std::size_t`。多个第一键可以有一个第二键值，就像`std::multimap`一样。\n\n我们可以使用来自`boost::bimaps::`命名空间的类来指定`bimap`的底层行为。我们可以使用哈希映射作为第一个键的基础类型:\n\n```cpp\n#include <boost/bimap/unordered_set_of.hpp> \n#include <boost/bimap/unordered_multiset_of.hpp> \n\ntypedef boost::bimap< \n    boost::bimaps::unordered_set_of<std::string>,  \n    boost::bimaps::unordered_multiset_of<std::size_t>  \n> hash_name_id_type; \n```\n\n当我们不指定键的行为而只指定其类型时，`Boost.Bimap`使用`boost::bimaps::set_of`作为默认行为。就像在我们的示例中一样，我们可以尝试使用标准库来表达以下代码:\n\n```cpp\n#include <boost/bimap/set_of.hpp> \n\ntypedef boost::bimap< \n    boost::bimaps::set_of<std::string>,  \n    boost::bimaps::multiset_of<std::size_t>  \n> name_id_type; \n```\n\n使用标准库，它看起来像是以下两个变量的组合:\n\n```cpp\n    std::map<std::string, std::size_t> key1;      // == name_id.left\n    std::multimap<std::size_t, std::string> key2; // == name_id.right\n```\n\n从前面的评论中我们可以看到，对`name_id.left`(在*第 4 步*中)的调用返回了对某个界面接近`std::map<std::string, std::size_t>`的东西的引用。从*第 5 步*调用`name_id.right`会返回一个界面接近`std::multimap<std::size_t, std::string>`的东西。\n\n在*第 6 步*中，我们使用一个整体`bimap`，搜索一对密钥并确保它们在容器中。\n\n# 还有更多...\n\n可惜 C++ 17 没有什么接近`Boost.Bimap`的东西。以下是其他一些坏消息:\n\n`Boost.Bimap`不支持右值引用，在某些编译器上，会显示大量的警告。请参考您的编译器文档，以获取有关抑制特定警告的信息。\n\n好消息是`Boost.Bimap`通常比两个标准库容器使用更少的内存，并且搜索速度和标准库容器一样快。它内部没有虚函数调用，而是使用动态分配。\n\n# 请参见\n\n*   下一个食谱*使用多索引容器*，将会给你更多关于多索引的信息，以及关于可以代替`Boost.Bimap`使用的 Boost 库的信息\n*   阅读官方文档，了解更多关于 http://boost.org/libs/bimap 的示例和信息\n\n# 使用多索引容器\n\n在前面的食谱中，我们制作了某种词汇，当我们需要与人合作时，这很好。但是，如果我们需要更高级的索引呢？让我们做一个程序来索引人:\n\n```cpp\nstruct person {\n    std::size_t     id_;\n    std::string     name_;\n    unsigned int    height_;\n    unsigned int    weight_;\n\n    person(std::size_t id, const std::string& name,\n                unsigned int height, unsigned int weight)\n        : id_(id)\n        , name_(name)\n        , height_(height)\n        , weight_(weight)\n    {}\n};\n\ninline bool operator < (const person& p1, const person& p2) {\n    return p1.name_ < p2.name_;\n}\n```\n\n我们将需要很多索引，例如，姓名、身份证、身高和体重。\n\n# 准备好\n\n需要关于标准库容器和无序地图的基本知识。\n\n# 怎么做...\n\n所有的索引都可以由一个单独的`Boost.Multiindex`容器来构建和管理。\n\n1.  为此，我们需要大量的包括:\n\n```cpp\n#include <iostream>\n#include <boost/multi_index_container.hpp>\n#include <boost/multi_index/ordered_index.hpp>\n#include <boost/multi_index/hashed_index.hpp>\n#include <boost/multi_index/identity.hpp>\n#include <boost/multi_index/member.hpp>\n```\n\n2.  最难的是构造`multi-index`类型:\n\n```cpp\nvoid example_main() {\n    typedef boost::multi_index::multi_index_container<\n        person,\n        boost::multi_index::indexed_by<\n            // names are unique\n            boost::multi_index::ordered_unique<\n                boost::multi_index::identity<person>\n            >,\n\n            // IDs are not unique, but we do not need them ordered\n            boost::multi_index::hashed_non_unique<\n                boost::multi_index::member<\n                    person, std::size_t, &person::id_\n                >\n            >,\n\n            // Height may not be unique, but must be sorted\n            boost::multi_index::ordered_non_unique<\n                boost::multi_index::member<\n                    person, unsigned int, &person::height_\n                >\n            >,\n\n            // Weight may not be unique, but must be sorted\n            boost::multi_index::ordered_non_unique<\n                boost::multi_index::member<\n                    person, unsigned int, &person::weight_\n                >\n            >\n        > // closing for `boost::multi_index::indexed_by<`\n    > indexes_t;\n```\n\n3.  现在，我们可以在我们的`multi-index`中插入值:\n\n```cpp\n    indexes_t persons;\n\n    // Inserting values:\n    persons.insert(person(1, \"John Snow\", 185, 80));\n    persons.insert(person(2, \"Vasya Pupkin\", 165, 60));\n    persons.insert(person(3, \"Antony Polukhin\", 183, 70));\n    // Same person as \"Antony Polukhin\".\n    persons.insert(person(3, \"Anton Polukhin\", 182, 70));\n```\n\n4.  让我们构造一个函数来打印索引内容:\n\n```cpp\ntemplate <std::size_t IndexNo, class Indexes>\nvoid print(const Indexes& persons) {\n    std::cout << IndexNo << \":\\n\";\n\n    typedef typename Indexes::template nth_index<\n            IndexNo\n    >::type::const_iterator const_iterator_t;\n\n    for (const_iterator_t it = persons.template get<IndexNo>().begin(),\n         iend = persons.template get<IndexNo>().end();\n         it != iend;\n         ++ it)\n    {\n        const person& v = *it;\n        std::cout \n            << v.name_ << \", \" \n            << v.id_ << \", \" \n            << v.height_ << \", \" \n            << v.weight_ << '\\n'\n        ;\n    }\n\n    std::cout << '\\n';\n} \n```\n\n5.  按如下方式打印所有索引:\n\n```cpp\n    print<0>(persons);\n    print<1>(persons);\n    print<2>(persons);\n    print<3>(persons);\n```\n\n6.  也可以使用之前配方中的一些代码:\n\n```cpp\n    assert(persons.get<1>().find(2)->name_ == \"Vasya Pupkin\");\n    assert(\n        persons.find(person(\n            77, \"Anton Polukhin\", 0, 0\n        )) != persons.end()\n    );\n\n    // Won't compile:\n    //assert(persons.get<0>().find(\"John Snow\")->id_ == 1);\n```\n\n现在，如果我们运行我们的示例，它将输出索引的内容:\n\n```cpp\n0:\nAnton Polukhin, 3, 182, 70\nAntony Polukhin, 3, 183, 70\nJohn Snow, 1, 185, 80\nVasya Pupkin, 2, 165, 60\n\n1:\nJohn Snow, 1, 185, 80\nVasya Pupkin, 2, 165, 60\nAnton Polukhin, 3, 182, 70\nAntony Polukhin, 3, 183, 70\n\n2:\nVasya Pupkin, 2, 165, 60\nAnton Polukhin, 3, 182, 70\nAntony Polukhin, 3, 183, 70\nJohn Snow, 1, 185, 80\n\n3:\nVasya Pupkin, 2, 165, 60\nAntony Polukhin, 3, 183, 70\nAnton Polukhin, 3, 182, 70\nJohn Snow, 1, 185, 80\n```\n\n# 它是如何工作的...\n\n这里最难的是使用`boost::multi_index::multi_index_container`构造多指标类型。第一个模板参数是我们要索引的类。在我们的情况下，就是`person`。第二个参数是类型`boost::multi_index::indexed_by`，所有的索引都必须描述为该类的模板参数。\n\n现在，让我们看看第一个索引描述:\n\n```cpp\n            boost::multi_index::ordered_unique< \n                boost::multi_index::identity<person> \n            > \n```\n\n`boost::multi_index::ordered_unique`类的用法意味着索引必须像`std::set`一样工作，并且拥有它的所有成员。`boost::multi_index::identity<person>`类意味着索引必须使用`person`类的`operator <`进行排序。\n\n下表显示了`Boost.MultiIndex`类型和 **STL 容器**之间的关系:\n\n| `Boost.MultiIndex`类型 | STL 容器 |\n| `boost::multi_index::ordered_unique` | `std::set` |\n| `boost::multi_index::ordered_non_unique` | `std::multiset` |\n| `boost::multi_index::hashed_unique` | `std::unordered_set` |\n| `boost::multi_index::hashed_non_unique` | `std::unordered_mutiset` |\n| `boost::multi_index::sequenced` | `std::list` |\n\n看看第二个指数:\n\n```cpp\n            boost::multi_index::hashed_non_unique< \n                boost::multi_index::member< \n                    person, std::size_t, &person::id_ \n                > \n            > \n```\n\n`boost::multi_index::hashed_non_unique`类型意味着索引的工作方式类似于`std::set`，`boost::multi_index::member<person, std::size_t, &person::id_>`意味着索引必须将哈希函数仅应用于人员结构的单个成员字段，而不是`person::id_`。\n\n剩下的指数现在不会有麻烦了；所以让我们来看看`print`函数中索引的用法。获取特定索引的迭代器类型是使用以下代码完成的:\n\n```cpp\n    typedef typename Indexes::template nth_index< \n            IndexNo \n    >::type::const_iterator const_iterator_t; \n```\n\n这看起来有点过于复杂，因为`Indexes`是一个模板参数。如果我们能在`indexes_t`的范围内编写这段代码，这个例子会更简单:\n\n```cpp\ntypedef indexes_t::nth_index<0>::type::const_iterator const_iterator_t;\n```\n\n`nth_index`成员元函数使用从零开始的索引数。在我们的例子中，索引 1 是标识的索引，索引 2 是高度的索引，以此类推。\n\n现在，我们来看看如何使用`const_iterator_t`:\n\n```cpp\nfor (const_iterator_t it = persons.template get<IndexNo>().begin(), \n         iend = persons.template get<IndexNo>().end(); \n         it != iend; \n         ++ it) \n    { \n        const person& v = *it; \n        // ... \n```\n\n这也可以简化为`indexes_t`在范围内:\n\n```cpp\n    for (const_iterator_t it = persons.get<0>().begin(), \n         iend = persons.get<0>().end(); \n         it != iend; \n         ++ it) \n    { \n        const person& v = *it; \n        // ... \n```\n\n函数`get<indexNo>()`返回索引。我们可以像使用 STL 容器一样使用该索引。\n\n# 还有更多...\n\nC++ 17 没有多索引库。`Boost.MultiIndex`是一个不使用虚函数的快速库。`Boost.MultiIndex`的官方文档包含性能和内存使用度量，表明该库在大多数情况下比基于标准库的手写代码使用更少的内存。不幸的是，`boost::multi_index::multi_index_container`不支持 C++ 11 特性，也没有使用`Boost.Move`的右值引用仿真。\n\n# 请参见\n\n`Boost.MultiIndex`的官方文档包含教程、性能度量、示例和其他`Boost.Multiindex`库对有用特性的描述。在[http://boost.org/libs/multi_index.](http://boost.org/libs/multi_index)阅读\n\n# 获得单一链表和内存池的好处\n\n如今，当我们需要非关联和非有序的容器时，我们通常会使用`std::vector`。这是*安德烈·亚历山德雷斯库*和*赫伯萨特*在 *C++ 编码标准*一书中推荐的。即使是那些没有读过书的用户，通常也会使用`std::vector`。为什么呢？嗯，`std::list`比`std::vector`更慢，使用的资源也更多。`std::deque`容器与`std::vector`非常接近，但不连续存储数值。\n\n如果我们需要一个容器，其中擦除和插入元素不会使迭代器无效，那么我们被迫选择一个缓慢的`std::list`。\n\n但是等等，我们可能会用 Boost 组装一个更好的解决方案！\n\n# 准备好\n\n理解引言部分需要对标准库容器有很好的了解。之后，只需要 C++ 和标准库容器的基础知识。\n\n# 怎么做...\n\n在这个食谱中，我们将同时使用两个 Boost 库:`Boost.Pool`和一个来自`Boost.Container`的链表。\n\n1.  我们需要以下标题:\n\n```cpp\n#include <boost/pool/pool_alloc.hpp>\n#include <boost/container/slist.hpp>\n#include <cassert>\n```\n\n2.  现在，我们需要描述列表的类型。这可以按照下面的代码来完成:\n\n```cpp\ntypedef boost::fast_pool_allocator<int> allocator_t;\ntypedef boost::container::slist<int, allocator_t> slist_t;\n```\n\n3.  我们可以像使用`std::list`一样使用单个链表:\n\n```cpp\ntemplate <class ListT>\nvoid test_lists() {\n    typedef ListT list_t;\n\n    // Inserting 1000000 zeros.\n    list_t  list(1000000, 0);\n\n    for (int i = 0; i < 1000; ++ i) {\n        list.insert(list.begin(), i);\n    }\n\n    // Searching for some value.\n    typedef typename list_t::iterator iterator;\n    iterator it = std::find(list.begin(), list.end(), 777);\n    assert(it != list.end());\n\n    // Erasing some values.\n    for (int i = 0; i < 100; ++ i) {\n        list.pop_front();\n    }\n\n    // Iterator is still valid and points to the same value.\n    assert(it != list.end());\n    assert(*it == 777);\n\n    // Inserting more values\n    for (int i = -100; i < 10; ++ i) {\n        list.insert(list.begin(), i);\n    }\n\n    // Iterator is still valid and points to the same value\n    assert(it != list.end());\n    assert(*it == 777);\n}\n\nvoid test_slist() {\n    test_lists<slist_t>();\n}\n\nvoid test_list() {\n    test_lists<std::list<int> >();\n}\n```\n\n4.  一些特定于列表的功能:\n\n```cpp\nvoid list_specific(slist_t& list, slist_t::iterator it) {\n    typedef slist_t::iterator iterator;\n\n    // Erasing element 776\n    assert( *(++ iterator(it)) == 776);\n    assert(*it == 777);\n\n    list.erase_after(it);\n\n    assert(*it == 777);\n    assert( *(++ iterator(it)) == 775);\n```\n\n5.  必须使用以下代码释放内存:\n\n```cpp\n    // Freeing memory: slist rebinds allocator_t and allocates\n    // nodes of the slist, not just ints.\n\n    boost::singleton_pool<\n        boost::fast_pool_allocator_tag,\n        sizeof(slist_t::stored_allocator_type::value_type)\n    >::release_memory();\n} // end of list_specific function\n```\n\n# 它是如何工作的...\n\n当我们使用`std::list`时，我们可能会注意到速度变慢，因为列表的每个节点都需要单独分配。这意味着通常当我们在`std::list`中插入 10 个元素时，容器会调用`new` 10 次。此外，分配的节点通常随机位于内存中，这不利于 CPU 缓存。\n\n这就是为什么我们从`Boost.Pool`开始使用 Boost `::fast_pool_allocator<int>`。这个分配器试图分配更大的内存块，以便在稍后的阶段，无需多次调用`new`，就可以构建多个节点。\n\n`Boost.Pool`库有一个缺点——它使用内存来满足内部需求。通常，每个元素使用额外的`sizeof(void*)`。为了解决这个问题，我们使用了一个来自`Boost.Containers`的链接列表。\n\n`boost::container::slist`类更紧凑，但是它的迭代器只能向前迭代。*步骤 3* 对于那些了解标准图书馆容器的读者来说很简单，所以我们转到*步骤 4* 来看看一些`boost::container::slist`的具体特性。由于单个链表迭代器只能向前迭代，传统的插入和删除算法需要线性时间 O(N)。这是因为当我们擦除或插入时，必须修改列表的前一个元素。为了解决这个问题，单一链表有两种方法`erase_after`和`insert_after`，它们的工作时间都是 O(1)。这些方法在迭代器的当前位置之后插入或删除元素。\n\nHowever, erasing and inserting values at the beginning of a single linked lists makes no big difference.\n\n仔细看看下面的代码:\n\n```cpp\n    boost::singleton_pool<\n        boost::fast_pool_allocator_tag,\n        sizeof(slist_t::stored_allocator_type::value_type)\n    >::release_memory();\n```\n\n之所以需要，是因为`boost::fast_pool_allocator`不释放内存，所以一定要手工做。在范围出口[第二章](02.html#36VSO0-712b4ba1126a4c7c89e1d44de61b4bdd)*资源管理*中的*在范围出口*做某事的方法可能有助于释放`Boost.Pool`。\n\n让我们看看执行时间，感受一下不同之处:\n\n```cpp\n$ TIME=\"Runtime=%E RAM=%MKB\" time ./07_slist_and_pool l\nstd::list: Runtime=0:00.08 RAM=34224KB\n\n$ TIME=\"Runtime=%E RAM=%MKB\" time ./07_slist_and_pool s\nslist_t:   Runtime=0:00.04 RAM=19640KB\n```\n\n我们可以看到，`slist_t`使用了一半的内存，比`std::list`类快了一倍。\n\n# 还有更多...\n\n`Boost.Container`库实际上有一个现成的解决方案，叫做`boost::container::stable_vector`。后者允许随机访问元素，具有随机访问迭代器，但是具有`std::list`的大部分性能和内存使用缺点。\n\nC++ 11 有`std::forward_list`，非常接近`boost::containers::slist`。它也有`*_after`法，但没有`size()`法。C++ 11 和 Boost 版本的单链表性能相同，都没有虚函数。然而，Boosts 版本也可以在 C++ 03 编译器上使用，甚至通过`Boost.Move`支持右值引用仿真。\n\n`boost::fast_pool_allocator`不在 C++ 17 中。不过 C++ 17 有更好的解决方案！标题`<memory_resource>`包含使用多态分配器的有用内容，在这里你可以找到`std::pmr::synchronized_pool_resource`、`std::pmr::unsynchronized_pool_resource`和`std::pmr::monotonic_buffer_resource`。用这些进行实验，以获得更好的性能。\n\nGuessing why `boost::fast_pool_allocator` does not free the memory by itself? That's because C++ 03 has no stateful allocators, so the containers are not copying and storing allocators. That makes it impossible to implement a `boost::fast_pool_allocator` function that deallocates memory by itself.\n\n# 请参见\n\n*   `Boost.Pool`的官方文档包含更多使用内存池的例子和类。跟随链接[http://boost.org/libs/pool](http://boost.org/libs/pool)阅读。\n*   *使用平面联想容器*食谱将从`Boost.Container`开始给你介绍更多的课程。您也可以在[http://boost.org/libs/container](http://boost.org/libs/container)阅读`Boost.Container`的官方文档，自行研究该图书馆或获取其课程的完整参考文档。\n*   *Vector vs List* ，C++ 编程语言的发明者*比雅尼·斯特劳斯特鲁普*的其他有趣话题可以在[http://channel 9 . msdn . com/Events/going native/going native-2012/Keynote-Bjarne-Stroustrup-CPP 11-Style](http://channel9.msdn.com/Events/GoingNative/GoingNative-2012/Keynote-Bjarne-Stroustrup-Cpp11-Style)网站找到。\n\n# 使用平面关联容器\n\n读完前面的食谱，有些读者可能会开始到处使用快速池分配器；尤其是对于`std::set`和`std::map`。好吧，我不会阻止你这么做，但至少让我们看看一个替代方案:平面关联容器。这些容器在传统向量容器的基础上实现，并存储有序的值。\n\n# 准备好\n\n需要标准库关联容器的基本知识。\n\n# 怎么做...\n\n扁平容器是`Boost.Container`库的一部分。在之前的食谱中，我们已经看到了如何使用它的一些容器。在本食谱中，我们将使用一个`flat_set`关联容器:\n\n1.  我们只需要包含一个头文件:\n\n```cpp\n#include <boost/container/flat_set.hpp>\n```\n\n2.  之后，我们可以自由构建扁平容器并进行实验:\n\n```cpp\n#include <algorithm>\n#include <cassert>\n\nint main() {\n    boost::container::flat_set<int> set;\n```\n\n3.  为元素保留空间:\n\n```cpp\n    set.reserve(4096);\n```\n\n4.  填充容器:\n\n```cpp\n    for (int i = 0; i < 4000; ++ i) {\n        set.insert(i);\n    }\n```\n\n5.  现在，我们可以像使用`std::set`一样使用它:\n\n```cpp\n    // 5.1\n    assert(set.lower_bound(500) - set.lower_bound(100) == 400);\n\n    // 5.2\n    set.erase(0);\n\n    // 5.3\n    set.erase(5000);\n\n    // 5.4\n    assert(std::lower_bound(set.cbegin(), set.cend(), 900000) == set.cend());\n\n    // 5.5\n    assert(\n        set.lower_bound(100) + 400 \n        == \n        set.find(500)\n    );\n} // end of main() function\n```\n\n# 它是如何工作的...\n\n*步骤 1* 和 *2* 简单，但是*步骤 3* 需要注意。这是使用平面关联容器和`std::vector`时最重要的步骤之一。\n\n`boost::container::flat_set`类存储其在向量中排序的值，这意味着任何不在容器末端的元素的插入或删除都需要线性时间 O(N)，就像`std::vector`的情况一样。这是必然的罪恶。但为此，我们获得了每个元素少三倍的内存使用，更多的处理器缓存友好存储，以及随机访问迭代器。看看*第 5 步*、`5.1`，在这里我们得到了调用`lower_bound`成员函数返回的两个迭代器之间的距离。用平集求距离需要常数时间 O(1)，而对`std::set`的迭代器进行同样的运算需要线性时间 O(N)。在`5.1`的情况下，使用`std::set`获取距离将比获取平置集装箱的距离慢 400 倍。\n\n回到*第三步*。如果不保留内存，元素的插入有时会变得更慢，内存效率也会降低。`std::vector`类分配所需的内存块，然后在该块上就地构造元素。当我们在没有保留内存的情况下插入一些元素时，有可能在预分配的内存块上没有剩余的可用空间，因此`std::vector`分配了更大的内存块。之后，`std::vector`将元素从第一个块复制或移动到第二个块，删除第一个块的元素，并释放第一个块。只有在那之后，插入才会发生。在插入过程中，这种复制和释放可能会发生多次，大大降低了速度。\n\nIf you know the count of elements that `std::vector` or any flat container must store, reserve the space for those elements before insertion. This speeds up the program in most cases!\n\n*第四步*很简单，我们在这里插入元素。请注意，我们正在插入有序元素。这不是必需的，但建议加快插入速度。在`std::vector`的末尾插入元素比在中间或开头便宜得多。\n\n在*第五步*中，`5.2`和`5.3`除了执行速度不同，没有太大区别。擦除元素的规则与插入元素的规则基本相同。解释见上一段。\n\nMay be I'm telling you simple things about containers, but I saw some very popular products that use features of C++ 11, have insane amount of optimizations and lame usage of standard library containers, especially `std::vector`.\n\n在*第 5 步*中，`5.4`向您展示了`std::lower_bound`函数使用`boost::container::flat_set`比使用`std::set`更快，因为随机访问迭代器。\n\n在*第 5 步*中，`5.5`也向您展示了随机访问迭代器的好处。\n\nWe did not use the `std::find` function here. This is because that function takes liner time O(N), while the member `find` functions take logarithmic time O(log(N)).\n\n# 还有更多...\n\n我们什么时候应该使用扁平容器，什么时候应该使用普通容器？好吧，这取决于你，但这里有一个不同于`Boost.Container`官方文档的列表，将帮助你做出决定:\n\n*   比标准关联容器更快的查找\n*   比标准关联容器快得多的迭代\n*   小对象的内存消耗更少(如果使用`shrink_to_fit`，则大对象的内存消耗更少)\n*   提高缓存性能(数据存储在连续内存中)\n*   非稳定迭代器(迭代器在插入和删除元素时无效)\n*   不能存储不可复制和不可移动的值类型\n*   与标准关联容器相比，异常安全性较弱(复制/移动构造函数在擦除和插入中转移值时会引发异常)\n*   与标准关联容器(特别是不可移动的类型)相比，插入和擦除速度更慢\n\n不幸的是，C++ 17 没有平面容器。Boost 的平面容器速度快，有很多优化，不使用虚函数。来自`Boost.Containers`的类通过`Boost.Move`支持右值引用仿真，所以你甚至可以在 C++ 03 编译器上自由使用它们。\n\n# 请参见\n\n*   有关`Boost.Container`的更多信息，请参考*获取单链表和内存池的好处*配方。\n*   在[第一章](01.html#RL0A0-712b4ba1126a4c7c89e1d44de61b4bdd)、*中使用 C++ 11 移动仿真*开始编写你的应用*的食谱将为你提供 C++ 03 兼容编译器上仿真值引用的基础知识。*\n*   `Boost.Container`的官方文档中包含了很多关于`Boost.Container`的有用信息，以及每个类的完整参考。在[http://boost.org/libs/container.](http://boost.org/libs/container)阅读"
  },
  {
    "path": "docs/boost-cpp-app-dev-cb/10.md",
    "content": "# 十、收集平台和编译器信息\n\n在本章中，我们将介绍:\n\n*   检测操作系统和编译器\n*   检测 int128 支持\n*   检测并旁路禁用的 RTTI\n*   使用更简单的方法编写元函数\n*   在 C++ 11 中减少代码大小并提高用户定义类型的性能\n*   导出和导入函数和类的可移植方式\n*   检测 Boost 版本并获取最新功能\n\n# 介绍\n\n不同的项目和公司有不同的编码要求。其中一些禁止异常或 RTTI，而一些禁止 C++ 11。如果你愿意写可移植的代码，可以被广泛的项目使用，这一章是为你准备的。\n\n想让你的代码尽可能快，使用最新的 C++ 特性吗？您肯定需要一个工具来检测编译器特性。\n\n有些编译器有独特的功能，可能会大大简化你的生活。如果您的目标是单一编译器，您可以节省很多时间并使用这些功能。不需要从头实现它们的类似物！\n\n本章专门介绍用于检测编译器、平台和 Boost 特性的不同助手宏。这些宏在 Boost 库中被广泛使用，对于编写能够处理任何编译器标志可移植代码是必不可少的。\n\n# 检测操作系统和编译器\n\n我猜你已经看到了一堆丑陋的宏来检测编译代码的编译器。类似这样的事情是 C 语言中的典型做法:\n\n```cpp\n#include <something_that_defines_macros>\n#if !defined(__clang__) \\\n    && !defined(__ICC) \\\n    && !defined(__INTEL_COMPILER) \\\n    && (defined(__GNUC__) || defined(__GNUG__))\n\n// GCC specific\n\n#endif\n```\n\n现在，试着想出一个好的宏来检测 GCC 编译器。尽量缩短宏的使用时间。\n\n看看下面的食谱来验证你的猜测。\n\n# 准备好\n\n只需要 C++ 的基础知识。\n\n# 怎么做...\n\n食谱很简单，由一个标题和一个宏组成。\n\n1.  标题:\n\n```cpp\n#include <boost/predef/compiler.h>\n```\n\n2.  宏:\n\n```cpp\n#if BOOST_COMP_GNUC\n\n// GCC specific\n\n#endif\n```\n\n# 它是如何工作的...\n\n标题`<boost/predef/compiler.h>`知道所有可能的编译器，并且为每个编译器都有一个宏。所以如果当前编译器是 GCC，那么宏`BOOST_COMP_GNUC`定义为`1`，其他编译器的所有其他宏定义为`0`。如果我们不在 GCC 编译器上，那么`BOOST_COMP_GNUC`宏被定义为`0`。\n\n由于这种方法，您不需要检查正在定义的宏本身:\n\n```cpp\n#if defined(BOOST_COMP_GNUC) // Wrong!\n\n// GCC specific\n\n#endif\n```\n\n`Boost.Predef`库的宏总是被定义的，这样你就不用在`#ifdef`中输入`defined()`或`def`。\n\n# 还有更多...\n\n`Boost.Predef`库还有检测 os、架构、标准库实现的宏，以及一些硬件能力。使用始终定义的宏；这允许您编写更短的复杂表达式:\n\n```cpp\n#include <boost/predef/os.h>\n#include <boost/predef/compiler.h>\n\n#if BOOST_COMP_GNUC && BOOST_OS_LINUX && !BOOST_OS_ANDROID\n\n// Do something for non Android Linux.\n\n#endif\n```\n\n现在，最精彩的部分。`Boost.Predef`库可在 C、C++ 和 Objective-C 编译器上使用。如果你喜欢，在你的非 C++ 项目中使用它。\n\nC++ 17 没有`Boost.Predef`库功能。\n\n# 请参见\n\n*   阅读`Boost.Predef`的官方文档，了解更多关于其在[http://boost.org/libs/predef](http://boost.org/libs/predef)的能力的信息\n*   下一个食谱将向你介绍`Boost.Config`库，那是很有秩序的，稍微不那么漂亮，但是功能要多得多\n\n# 检测 int128 支持\n\n一些编译器支持扩展算术类型，如 128 位浮点或整数。让我们快速浏览一下如何使用 Boost 来使用它们。\n\n我们将创建一个接受三个参数并返回这些方法的乘积值的方法。如果编译器支持 128 位整数，那么我们就使用它们。如果编译器支持`long long`，那么我们就用它；否则，我们需要发出编译时错误。\n\n# 准备好\n\n只需要 C++ 的基础知识。\n\n# 怎么做...\n\n我们需要什么来处理 128 位整数？显示它们可用的宏和一些跨平台具有可移植类型名称的`typedefs`。\n\n1.  包括标题:\n\n```cpp\n#include <boost/config.hpp>\n```\n\n2.  现在，我们需要检测 int128 支持:\n\n```cpp\n#ifdef BOOST_HAS_INT128\n```\n\n3.  增加一些`typedefs`并按如下方法执行:\n\n```cpp\ntypedef boost::int128_type int_t;\ntypedef boost::uint128_type uint_t;\n\ninline int_t mul(int_t v1, int_t v2, int_t v3) {\n    return v1 * v2 * v3;\n}\n```\n\n4.  对于不支持 int128 类型且没有`long long`的编译器，我们可能会产生编译时错误:\n\n```cpp\n#else // #ifdef BOOST_HAS_INT128\n\n#ifdef BOOST_NO_LONG_LONG\n#error \"This code requires at least int64_t support\"\n#endif\n```\n\n5.  现在，我们需要使用`int64`为不支持 int128 的编译器提供一些实现:\n\n```cpp\nstruct int_t { boost::long_long_type hi, lo; };\nstruct uint_t { boost::ulong_long_type hi, lo; };\n\ninline int_t mul(int_t v1, int_t v2, int_t v3) {\n    // Some hand written math.\n    // ...\n}\n\n#endif // #ifdef BOOST_HAS_INT128\n```\n\n# 它是如何工作的...\n\n标题`<boost/config.hpp>`包含大量描述编译器和平台特性的宏。在本例中，我们使用`BOOST_HAS_INT128`检测对 128 位整数的支持，使用`BOOST_NO_LONG_LONG`检测对 64 位整数的支持。\n\n从例子中我们可以看到，Boost 对于 64 位有符号和无符号整数有`typedefs`:\n\n```cpp\nboost::long_long_type  \nboost::ulong_long_type  \n```\n\n128 位有符号和无符号整数也有`typedefs`:\n\n```cpp\nboost::int128_type \nboost::uint128_type \n```\n\n# 还有更多...\n\nC++ 11 通过`long long int`和`unsigned long long int`内置类型支持 64 位类型。不幸的是，并不是所有的编译器都支持 C++ 11，所以`BOOST_NO_LONG_LONG`可能对你有用。\n\n128 位整数不是 C++ 17 的一部分，所以`typedefs`和宏来自 Boost 是编写可移植代码的方式之一。\n\nC++ 标准化委员会正在进行一项关于添加编译时指定宽度的整数的工作。当这项工作完成时，您将能够创建 128 位、512 位甚至 8388608 位(1 MB 大)整数。\n\n# 请参见\n\n*   有关`Boost.Config`的更多信息，请阅读食谱*检测并绕过禁用的 RTTI* 。\n*   阅读【http://boost.org/libs/config】的`Boost.Config`官方文档，了解更多关于其能力的信息。\n*   Boost 中有一个库，允许构造无限精度的类型。跟着链接[http://boost.org/libs/multiprecision](http://boost.org/libs/multiprecision)去看看`Boost.Multiprecision`图书馆。\n\n# 检测并旁路禁用的 RTTI\n\n一些公司和库对他们的 C++ 代码有特定的要求，比如没有 RTTI 的成功编译。\n\n在这个小食谱中，我们不仅要检测禁用的 RTTI，还要从头开始编写一个类似 Boost 的库，存储关于类型的信息，并在运行时比较类型，即使没有`typeid`。\n\n# 准备好\n\n这个食谱需要 C++ RTTI 用法的基本知识。\n\n# 怎么做...\n\n检测禁用的 RTTI、存储关于类型的信息以及在运行时比较类型是在 Boost 库中广泛使用的技巧。\n\n1.  为此，我们首先需要包含以下标题:\n\n```cpp\n#include <boost/config.hpp> \n```\n\n2.  让我们首先看看启用了 RTTI 并且 C++ 11 `std::type_index`类可用的情况:\n\n```cpp\n#if !defined(BOOST_NO_RTTI) \\\n    && !defined(BOOST_NO_CXX11_HDR_TYPEINDEX)\n\n#include <typeindex>\nusing std::type_index;\n\ntemplate <class T>\ntype_index type_id() {\n    return typeid(T);\n}\n```\n\n3.  否则，我们需要构建自己的`type_index`类:\n\n```cpp\n#else\n\n#include <cstring>\n#include <iosfwd> // std::basic_ostream\n#include <boost/current_function.hpp>\n\nstruct type_index {\n    const char * name_;\n\n    explicit type_index(const char* name)\n        : name_(name)\n    {}\n\n    const char* name() const { return name_; }\n};\n\ninline bool operator == (type_index v1, type_index v2) {\n    return !std::strcmp(v1.name_, v2.name_);\n}\n\ninline bool operator != (type_index v1, type_index v2) {\n    return !(v1 == v2);\n}\n```\n\n4.  最后一步是定义`type_id`功能:\n\n```cpp\ntemplate <class T>\ninline type_index type_id() {\n    return type_index(BOOST_CURRENT_FUNCTION);\n}\n\n#endif\n```\n\n5.  现在，我们可以比较类型:\n\n```cpp\n#include <cassert>\n\nint main() {\n    assert(type_id<unsigned int>() == type_id<unsigned>());\n    assert(type_id<double>() != type_id<long double>());\n}\n```\n\n# 它是如何工作的...\n\n如果禁用了 RTTI，则定义宏`BOOST_NO_RTTI`，如果编译器没有`<typeindex>`头，也没有`std::type_index`类，则定义宏`BOOST_NO_CXX11_HDR_TYPEINDEX`。\n\n上一节*第三步*手写的`type_index`结构只保存了某个字符串的指针；这里没什么有趣的。\n\n看一下`BOOST_CURRENT_FUNCTION`宏。它返回当前函数的全名，包括模板参数、参数和返回类型。\n例如，`type_id<double>()`表示如下:\n\n```cpp\n type_index type_id() [with T = double]\n```\n\n因此，对于任何其他类型，`BOOST_CURRENT_FUNCTION`返回不同的字符串，这就是为什么示例中的`type_index`变量与它不相等。\n\n恭喜你！我们刚刚重新发明了大部分`Boost.TypeIndex`库功能。删除*步骤 1 至 4* 中的所有代码，并稍微更改*步骤 5* 中的代码以使用`Boost.TypeIndex`库:\n\n```cpp\n#include <boost/type_index.hpp>\n\nvoid test() {\n    using boost::typeindex::type_id;\n\n    assert(type_id<unsigned int>() == type_id<unsigned>());\n    assert(type_id<double>() != type_id<long double>());\n}\n```\n\n# 还有更多...\n\n当然`Boost.TypeIndex`略多于此；它允许你以一种独立于平台的方式获得人类可读的类型名，解决与平台相关的问题，允许你发明自己的 RTTI 实现，拥有一个 constexpr RTTI，以及其他东西。\n\n不同的编译器有不同的宏来获取完整的函数名。使用 Boost 中的宏是最便携的解决方案。`BOOST_CURRENT_FUNCTION`宏在编译时返回名称，因此它意味着最小的运行时损失。\n\nC++ 11 有一个`__func__`魔法标识符，它被评估为当前函数的名称。但是`__func__`的结果只是函数名，而`BOOST_CURRENT_FUNCTION`也尽量显示函数参数，包括模板参数。\n\n# 请参见\n\n*   阅读即将到来的食谱，了解更多关于`Boost.Config`的信息\n*   浏览[http://github.com/boostorg/type_index](http://github.com/boostorg/type_index)查看`Boost.TypeIndex`库的源代码\n*   在[http://boost.org/libs/config](http://boost.org/libs/config)阅读`Boost.Config`的官方文件\n*   在 http://boost.org/libs/type_index 阅读`Boost.TypeIndex`图书馆的官方文件\n*   食谱*获取人类可读的类型名称*在[第 01 章](01.html#RL0A0-712b4ba1126a4c7c89e1d44de61b4bdd)、*开始编写您的应用*将向您介绍`Boost.TypeIndex`的一些其他功能\n\n# 使用更简单的方法编写元函数\n\n[第 4 章](04.html#6FSQK0-712b4ba1126a4c7c89e1d44de61b4bdd)、*编译时技巧*和[第 8 章](08.html#CL9V20-712b4ba1126a4c7c89e1d44de61b4bdd)、*元编程*都致力于元编程。如果你试图使用那些章节中的技巧，你可能已经注意到写一个元功能会花费很多时间。因此，在编写一个可移植的实现之前，使用更用户友好的方法来尝试元功能可能是一个好主意，比如 C++ 11 `constexpr`。\n\n在这个食谱中，我们来看看如何检测`constexpr`支持。\n\n# 准备好\n\n`constexpr`函数是可以在编译时计算的函数。对于这个食谱，我们只需要知道这些。\n\n# 怎么做...\n\n让我们看看如何检测编译器对`constexpr`特性的支持:\n\n1.  就像本章的其他食谱一样，我们从以下标题开始:\n\n```cpp\n#include <boost/config.hpp> \n```\n\n2.  编写`constexpr`功能:\n\n```cpp\n#if !defined(BOOST_NO_CXX11_CONSTEXPR) \\\n    && !defined(BOOST_NO_CXX11_HDR_ARRAY)\n\ntemplate <class T>\nconstexpr int get_size(const T& val) {\n    return val.size() * sizeof(typename T::value_type);\n}\n```\n\n3.  如果缺少 C++ 11 特性，让我们打印一个错误:\n\n```cpp\n#else\n#error \"This code requires C++ 11 constexpr and std::array\"\n#endif\n```\n\n4.  就这样。现在，我们可以自由编写如下代码:\n\n```cpp\n#include <array>\n\nint main() {\n    std::array<short, 5> arr;\n    static_assert(get_size(arr) == 5 * sizeof(short), \"\");\n\n    unsigned char data[get_size(arr)];\n}\n```\n\n# 它是如何工作的...\n\n当 C++ 11 `constexpr`可用时定义`BOOST_NO_CXX11_CONSTEXPR`宏。\n\n`constexpr`关键字告诉编译器，如果该函数的所有输入都是编译时常数，则可以在编译时计算该函数。C++ 11 对`constexpr`函数的功能施加了很多限制。C++ 14 消除了一些限制。\n\n当 C++ 11 `std::array`类和`<array>`头可用时，定义`BOOST_NO_CXX11_HDR_ARRAY`宏。\n\n# 还有更多...\n\n但是`constexpr`也有其他有用且有趣的宏，如下所示:\n\n*   `BOOST_CONSTEXPR`宏扩展到`constexpr`或不扩展\n*   `BOOST_CONSTEXPR_OR_CONST`宏扩展到`constexpr`或`const`\n*   `BOOST_STATIC_CONSTEXPR`宏与`static BOOST_CONSTEXPR_OR_CONST`相同\n\n使用这些宏，可以编写利用 C++ 11 常量表达式特性的代码(如果它们可用的话):\n\n```cpp\ntemplate <class T, T Value> \nstruct integral_constant { \n    BOOST_STATIC_CONSTEXPR T value = Value; \n\n    BOOST_CONSTEXPR operator T() const { \n        return this->value; \n    } \n}; \n```\n\n现在，我们可以使用`integral_constant`了，如下代码所示:\n\n```cpp\nchar array[integral_constant<int, 10>()]; \n```\n\n在示例中，调用`BOOST_CONSTEXPR operator T()`来获取数组大小。\n\nC++ 11 常量表达式可以提高编译速度，并在出现错误时提供诊断信息。这是一个很好的功能。如果你的函数需要 C++ 14 中的**宽松常量**，那么你可以使用`BOOST_CXX14_CONSTEXPR`宏。只有当宽松的常量表达式可用时，它才会扩展到`constexpr`，否则不会扩展到任何内容。\n\n# 请参见\n\n*   更多关于`constexpr`用法的信息可以在[http://en.cppreference.com/w/cpp/language/constexpr](http://en.cppreference.com/w/cpp/language/constexpr)阅读\n*   阅读`Boost.Config`官方文档，了解更多关于[http://boost.org/libs/config](http://boost.org/libs/config)宏的信息\n\n# 在 C++ 11 中减少代码大小并提高用户定义类型的性能\n\n在标准库容器中使用**用户定义类型** ( **UDTs** )时，C++ 11 有非常具体的逻辑。只有当移动构造函数不抛出异常或没有复制构造函数时，一些容器才使用移动赋值和移动构造。\n\n让我们看看如何确保编译器输出类`move_nothrow`有一个非抛出`move`赋值运算符和一个非抛出`move`构造函数。\n\n# 准备好\n\n这个食谱需要 C++ 11 数值参考的基础知识。标准库容器的知识也将很好地为您服务。\n\n# 怎么做...\n\n让我们看看如何使用 Boost 改进我们的 C++ 类。\n\n1.  我们只需要用`BOOST_NOEXCEPT`宏标记`move_nothrow`赋值运算符和`move_nothrow`构造函数:\n\n```cpp\n#include <boost/config.hpp>\n\nclass move_nothrow {\n    // Some class class members go here.\n    // ...\n\npublic:\n    move_nothrow() BOOST_NOEXCEPT;\n    move_nothrow(move_nothrow&&) BOOST_NOEXCEPT\n        // Members initialization goes here.\n        // ...\n    {}\n\n    move_nothrow& operator=(move_nothrow&&) BOOST_NOEXCEPT {\n        // Implementation goes here.\n        // ...\n        return *this;\n    }\n\n    move_nothrow(const move_nothrow&);\n    move_nothrow& operator=(const move_nothrow&);\n};\n```\n\n2.  现在，我们可以在 C++ 11 中使用带有`std::vector`的类，无需任何修改:\n\n```cpp\n#include <vector>\n\nint main() {\n    std::vector<move_nothrow> v(10);\n    v.push_back(move_nothrow());\n}\n```\n\n3.  如果我们从`move`构造函数中移除`BOOST_NOEXCEPT`，我们将得到以下错误，因为我们没有为复制构造函数提供定义:\n\n```cpp\n undefined reference to `move_nothrow::move_nothrow(move_nothrow \n    const&)  \n```\n\n# 它是如何工作的...\n\n在支持它的编译器上，`BOOST_NOEXCEPT`宏扩展到`noexcept`。标准库容器使用类型特征来检测构造函数是否抛出异常。类型特征主要基于`noexcept`说明符做出决定。\n\n为什么没有`BOOST_NOEXCEPT`我们会得到一个错误？编译器的类型特性返回`move_nothrow`抛出的，所以`std::vector`尝试使用`move_nothrow`的复制构造函数，没有定义。\n\n# 还有更多...\n\n不管`noexcept`函数或方法的定义是否在单独的源文件中，`BOOST_NOEXCEPT`宏也会减小二进制文件的大小。\n\n```cpp\n// In header file.\nint foo() BOOST_NOEXCEPT; \n\n// In source file.\nint foo() BOOST_NOEXCEPT { \n    return 0; \n} \n```\n\n这是因为在后一种情况下，编译器知道函数不会抛出异常，因此没有必要生成处理它们的代码。\n\nIf a function marked as `noexcept` does throw an exception, your program will terminate without calling destructors for the constructed objects.\n\n# 请参见\n\n*   描述为什么允许`move`构造函数抛出异常以及容器必须如何移动对象的文档可在[http://www . open-STD . org/JT C1/sc22/wg21/docs/papers/2010/n 3050 . html](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2010/n3050.html)上获得\n*   阅读`Boost.Config`的官方文档，了解更多`BOOST_NOEXCEPT`的示例，例如[http://boost.org/libs/config](http://boost.org/libs/config)的 Boost 中存在的宏\n\n# 导出和导入函数和类的可移植方式\n\n几乎所有现代语言都有能力创建具有定义良好的接口的库、类集合和方法。C++ 也不例外。我们有两种类型的库:运行时(也称为共享或动态)和静态。但是，在 C++ 中编写库并不是一项简单的任务。不同的平台有不同的方法来描述哪些符号必须从共享库中导出。\n\n让我们看看如何使用 Boost 以可移植的方式管理符号可见性。\n\n# 准备好\n\n创建动态和静态库的经验可能对这个方法有用。\n\n# 怎么做...\n\n这个食谱的代码由两部分组成。第一部分是图书馆本身。第二部分是使用该库的代码。这两个部分使用相同的头，其中声明了库方法。使用 Boost 以可移植的方式管理符号可见性很简单，可以通过以下步骤完成:\n\n1.  在头文件中，我们需要来自以下头文件的定义:\n\n```cpp\n#include <boost/config.hpp> \n```\n\n2.  以下代码也必须添加到头文件中:\n\n```cpp\n#if defined(MY_LIBRARY_LINK_DYNAMIC)\n#   if defined(MY_LIBRARY_COMPILATION)\n#       define MY_LIBRARY_API BOOST_SYMBOL_EXPORT\n#   else\n#       define MY_LIBRARY_API BOOST_SYMBOL_IMPORT\n#   endif\n#else\n#   define MY_LIBRARY_API\n#endif\n```\n\n3.  现在，所有的声明都必须使用`MY_LIBRARY_API`宏:\n\n```cpp\nint MY_LIBRARY_API foo();\n\nclass MY_LIBRARY_API bar {\npublic:\n    /* ... */ \n    int meow() const;\n};\n```\n\n4.  异常必须用`BOOST_SYMBOL_VISIBLE`声明；否则，只能在使用库的代码中使用`catch(...)`来捕获它们:\n\n```cpp\n#include <stdexcept>\n\nstruct BOOST_SYMBOL_VISIBLE bar_exception\n    : public std::exception \n{};\n```\n\n5.  库源文件必须包括头文件:\n\n```cpp\n#define MY_LIBRARY_COMPILATION\n#include \"my_library.hpp\"\n```\n\n6.  方法的定义也必须在库的源文件中:\n\n```cpp\nint MY_LIBRARY_API foo() {\n    // Implementation goes here.\n    // ...\n    return 0;\n}\n\nint bar::meow() const {\n    throw bar_exception();\n}\n```\n\n7.  现在，我们可以使用如下代码所示的库:\n\n```cpp\n#include \"../06_A_my_library/my_library.hpp\"\n#include <cassert>\n\nint main() {\n    assert(foo() == 0);\n    bar b;\n    try {\n        b.meow();\n        assert(false);\n    } catch (const bar_exception&) {}\n}\n```\n\n# 它是如何工作的...\n\n所有工作都在*步骤 2* 中完成。在这里，我们定义了宏`MY_LIBRARY_API`，它应用于我们希望从库中导出的类和方法。在*步骤 2* 中，我们检查`MY_LIBRARY_LINK_DYNAMIC`。如果没有定义，我们是在建一个静态库，没有必要定义`MY_LIBRARY_API`。\n\nThe developer must take care of `MY_LIBRARY_LINK_DYNAMIC`! It will not define itself. If we are making a dynamic library, we need to make our build system to define it,\n\n如果`MY_LIBRARY_LINK_DYNAMIC`被定义，我们正在构建一个运行时库，这就是变通方法的开始。作为开发人员，您必须告诉编译器，我们现在正在向用户导出函数。用户必须告诉编译器他/她正在从库中导入方法。要使导入和导出库都有一个头文件，我们使用以下代码:\n\n```cpp\n#if defined(MY_LIBRARY_COMPILATION) \n#    define MY_LIBRARY_API BOOST_SYMBOL_EXPORT \n#else \n#    define MY_LIBRARY_API BOOST_SYMBOL_IMPORT \n#endif \n```\n\n导出库(或者说编译库)时，必须定义`MY_LIBRARY_COMPILATION`。这导致`MY_LIBRARY_API`被定义为`BOOST_SYMBOL_EXPORT`。例如，见*第 5 步*，我们在包括`my_library.hpp`之前定义了`MY_LIBRARY_COMPILATION`。如果`MY_LIBRARY_COMPILATION`没有定义，表头是用户包含的，用户对那个宏一无所知。而且，如果用户包含标题，则必须从库中导入符号。\n\n`BOOST_SYMBOL_VISIBLE`宏必须只用于那些没有导出但被 RTTI 使用的类。这种类的例子是异常和使用`dynamic_cast`进行转换的类。\n\n# 还有更多...\n\n有些编译器默认导出所有符号，但提供标志来禁用这种行为。比如 Linux 上的 GCC 和 Clang 提供`-fvisibility=hidden`。强烈建议使用这些标志，因为这样可以缩小二进制文件的大小，加快动态库的加载速度，并改善二进制文件的逻辑结构。当导出的符号更少时，一些过程间优化可以执行得更好。C++ 17 没有描述可见性的标准方法。希望有一天，一种可移植的可视化工作方式会出现在 C++ 中，但是在那之前，我们必须使用 Boost 中的宏。\n\n# 请参见\n\n*   从头开始阅读本章，获取更多`Boost.Config`用法的示例\n*   考虑阅读`Boost.Config`的官方文档，了解`Boost.Config`宏的完整列表及其在[http://boost.org/libs/config](http://boost.org/libs/config)的描述\n\n# 检测 Boost 版本并获取最新功能\n\nBoost 正在积极开发中，因此每个版本都包含新的特性和库。有些人希望拥有为不同版本的 Boost 编译的库，也希望使用新版本的一些功能。\n\n我们来看看`boost::lexical_cast`变更日志。根据它，Boost 1.53 有一个`lexical_cast(const CharType* chars, std::size_t count)`功能过载。我们的任务是在新版本的 Boost 中使用这个函数重载，在旧版本中解决这个缺失的函数重载。\n\n# 准备好\n\n只需要 C++ 和`Boost.LexicalCast`库的基础知识。\n\n# 怎么做...\n\n好吧，我们需要做的就是获取关于 Boost 版本的信息，并使用它来编写最佳代码。这可以按照以下步骤完成:\n\n1.  我们需要包括包含 Boost 版本和`boost::lexical_cast`的标题:\n\n```cpp\n#include <boost/version.hpp>\n#include <boost/lexical_cast.hpp>\n```\n\n2.  如果可用，我们使用`Boost.LexicalCast`的新功能:\n\n```cpp\n#if (BOOST_VERSION >= 105200)\n\nint to_int(const char* str, std::size_t length) {\n    return boost::lexical_cast<int>(str, length);\n}\n```\n\n3.  否则，我们需要先将数据复制到`std::string`:\n\n```cpp\n#else\n\nint to_int(const char* str, std::size_t length) {\n    return boost::lexical_cast<int>(\n        std::string(str, length)\n    );\n}\n\n#endif\n```\n\n4.  现在，我们可以使用如下所示的代码:\n\n```cpp\n#include <cassert>\n\nint main() {\n    assert(to_int(\"10000000\", 3) == 100);\n}\n```\n\n# 它是如何工作的...\n\n`BOOST_VERSION`宏包含以以下格式编写的 Boost 版本:一个数字代表主版本，后面是三个数字代表次版本，然后是两个数字代表补丁级别。例如，Boost 1.73.1 将包含`BOOST_VERSION`宏中的`107301`号。\n\n所以，我们在*第二步*中查看 Boost 版本，根据`Boost.LexicalCast`的能力选择`to_int`功能的正确实现。\n\n# 还有更多...\n\n拥有版本宏是大型库的常见做法。某些 Boost 库允许您指定要使用的库版本；参见`Boost.Thread`及其`BOOST_THREAD_VERSION`宏示例。\n\n顺便说一下，C++ 也有版本宏。`__cplusplus`宏的值允许您区分 C++ 11 之前的版本和 C++ 11，C++ 11 和 C++ 14，或者 C++ 17。目前可以定义为以下值之一:`199711L`、`201103L`、`201402L`或`201703L`。当委员会批准标准时，宏观价值代表年和月。\n\n# 请参见\n\n*   阅读[第五章](05.html#7PRJC0-712b4ba1126a4c7c89e1d44de61b4bdd)、*多线程*中的*创建执行线程*的配方，了解更多关于`BOOST_THREAD_VERSION`及其如何影响`Boost.Thread`库的信息，或者阅读[http://boost.org/libs/thread](http://boost.org/libs/thread)的文档\n*   从头开始读本章或者考虑在[http://boost.org/libs/config](http://boost.org/libs/config)阅读`Boost.Config`的官方文件"
  },
  {
    "path": "docs/boost-cpp-app-dev-cb/11.md",
    "content": "# 十一、使用系统\n\n在本章中，我们将介绍:\n\n*   列出目录中的文件\n*   擦除和创建文件和目录\n*   编写和使用插件\n*   获取回溯-当前调用序列\n*   将数据从一个进程快速传递到另一个进程\n*   同步进程间通信\n*   在共享内存中使用指针\n*   读取文件的最快方法\n*   共同诉讼-拯救国家和推迟执行\n\n# 介绍\n\n每个操作系统都有许多系统调用。这些调用从一个操作系统到另一个操作系统是不同的，同时做非常接近的事情。Boost 为这些调用提供了可移植的安全包装。包装器的知识对于编写好的程序是必不可少的。\n\n本章专门介绍如何使用操作系统。我们已经在[第 6 章](06.html#9KVM80-712b4ba1126a4c7c89e1d44de61b4bdd)、*操控任务*中看到了如何处理网络通信和信号。在这一章中，我们将详细了解文件系统、创建和删除文件。我们将看到数据如何在不同的系统进程之间传递，如何以最大速度读取文件，以及如何执行其他技巧。\n\n# 列出目录中的文件\n\n有标准的库函数和类来读写数据到文件。但是在 C++ 17 之前，没有列出目录中的文件、获取文件类型或获取文件访问权限的函数。\n\n让我们看看如何使用 Boost 修复这样的不公正。我们将编写一个程序，列出当前目录中文件的名称、写访问和类型。\n\n# 准备好\n\nC++ 的一些基础知识对于使用这个方法来说已经足够了。\n\n该配方需要链接到`boost_system`和`boost_filesystem`库。\n\n# 怎么做...\n\n这个食谱和下一个食谱是关于使用文件系统的可移植包装器的:\n\n1.  我们需要包含以下两个标题:\n\n```cpp\n#include <boost/filesystem/operations.hpp> \n#include <iostream> \n```\n\n2.  现在，我们需要指定一个目录:\n\n```cpp\nint main() { \n    boost::filesystem::directory_iterator begin(\"./\"); \n```\n\n3.  指定目录后，遍历其内容:\n\n```cpp\n    boost::filesystem::directory_iterator end; \n    for (; begin != end; ++ begin) { \n```\n\n4.  下一步是获取文件信息:\n\n```cpp\n        boost::filesystem::file_status fs = \n            boost::filesystem::status(*begin);\n```\n\n5.  现在，输出文件信息:\n\n```cpp\n        switch (fs.type()) { \n        case boost::filesystem::regular_file: \n            std::cout << \"FILE       \";  \n            break; \n        case boost::filesystem::symlink_file: \n            std::cout << \"SYMLINK    \";  \n            break; \n        case boost::filesystem::directory_file: \n            std::cout << \"DIRECTORY  \";  \n            break; \n        default: \n            std::cout << \"OTHER      \";  \n            break; \n        } \n        if (fs.permissions() & boost::filesystem::owner_write) { \n            std::cout << \"W \"; \n        } else { \n            std::cout << \"  \"; \n        } \n```\n\n6.  最后一步是输出文件名:\n\n```cpp\n        std::cout << *begin << '\\n'; \n    } /*for*/ \n} /*main*/ \n```\n\n就是这样；现在，如果我们运行程序，它将输出如下内容:\n\n```cpp\nFILE W \"./main.o\" \nFILE W \"./listing_files\" \nDIRECTORY W \"./some_directory\" \nFILE W \"./Makefile\" \n```\n\n# 它是如何工作的...\n\n`Boost.Filesystem`的函数和类只是包装系统特定的函数来处理文件。\n\n注意*第二步*中`/`的用法。POSIX 系统使用斜线来指定路径；默认情况下，Windows 使用反斜杠。不过 Windows 也懂正斜杠，所以`./`会在所有流行的操作系统上工作，也就是当前目录。\n\n看看*第 3 步*，我们默认构建`boost::filesystem::directory_iterator`类。它就像一个`std::istream_iterator`类一样工作，当默认构造时，它充当一个`end`迭代器。\n\n*第四步*是一个棘手的步骤，不是因为这个函数很难理解，而是因为很多转换正在发生。取消引用`begin`迭代器返回`boost::filesystem::directory_entry`，该迭代器隐式转换为`boost::filesystem::path`，用作`boost::filesystem::status`函数的参数。事实上，我们可以做得更好:\n\n```cpp\nboost::filesystem::file_status fs = begin->status(); \n```\n\nRead the reference documentation carefully to avoid unrequired implicit conversions.\n\n*第 5 步*是显而易见的，所以我们移至*第 6 步*，在此再次发生到路径的隐式转换。更好的解决方案如下:\n\n```cpp\nstd::cout << begin->path() << '\\n'; \n```\n\n这里，`begin->path()`返回对包含在`boost::filesystem::directory_entry`中的`boost::filesystem::path`变量的常量引用。\n\n# 还有更多...\n\n；`Boost.Filesystem`是 C++ 17 的一部分。C++ 17 中的所有内容都位于名称空间`std::filesystem`中的一个头文件`<filesystem>`中。文件系统的标准库版本与 Boost 版本略有不同，主要是通过使用作用域枚举(`enum class`),其中`Boost.Filesystem`使用的是未作用域的`enum`。\n\nThere is a class ; `directory_entry`. That class provides caching of filesystem information, so if you work a lot with filesystem and query different information, try using `directory_entry` for a better performance.\n\n就像其他 Boost 库一样，`Boost.Filesystem`在 C++ 17 之前的编译器上工作，甚至在 C++ 11 之前的编译器上工作。\n\n# 请参见\n\n*   *擦除和创建文件和目录*的方法将显示另一个使用`Boost.Filesystem`的例子\n*   阅读助推的官方文档`Boost.Filesystem`获得更多关于其能力的信息；可通过以下链接获得:[http://boost.org/libs/filesystem](http://boost.org/libs/filesystem)\n*   你可以在[http://www . open-std.org/jtc1/sc22/wg21/docs/papers/2017/n4659.pdf](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/n4659.pdf)找到 C++ 17 草稿\n\n# 擦除和创建文件和目录\n\n让我们考虑下面几行代码:\n\n```cpp\n    std::ofstream ofs(\"dir/subdir/file.txt\"); \n    ofs << \"Boost.Filesystem is fun!\"; \n```\n\n在这几行中，我们试图在`dir/subdir`目录中向`file.txt`写一些东西。如果没有这样的目录，此尝试将会失败。使用文件系统的能力是编写好的工作代码所必需的。\n\n在这个食谱中，我们将构造一个目录和一个子目录，将一些数据写入一个文件，并尝试创建`symlink`。如果符号链接创建失败，请擦除创建的实体。我们还应该避免使用异常作为错误报告机制，更喜欢某种返回代码。\n\n让我们看看如何使用 Boost 以优雅的方式实现这一点。\n\n# 准备好\n\n这个食谱需要 C++ 和`std::ofstream`类的基础知识。\n\n；`Boost.Filesystem`不是一个只有标题的库，所以这个食谱中的代码需要链接到`boost_system`和`boost_filesystem`库。\n\n# 怎么做...\n\n我们继续处理文件系统的可移植包装器，在这个食谱中，我们将看到如何修改目录内容:\n\n1.  一如既往，我们需要包含一些标题:\n\n```cpp\n#include <boost/filesystem/operations.hpp> \n#include <cassert> \n#include <fstream> \n```\n\n2.  现在，我们需要一个变量来存储错误(如果有的话):\n\n```cpp\nint main() { \n    boost::system::error_code error; \n```\n\n3.  如果需要，我们还将创建目录，如下所示:\n\n```cpp\n    boost::filesystem::create_directories(\"dir/subdir\", error); \n    assert(!error); \n```\n\n4.  然后，我们将数据写入文件:\n\n```cpp\n    std::ofstream ofs(\"dir/subdir/file.txt\");\n    ofs << \"Boost.Filesystem is fun!\";\n    assert(ofs);\n    ofs.close();\n```\n\n5.  我们需要尝试创建`symlink`:\n\n```cpp\n    boost::filesystem::create_symlink(\n        \"dir/subdir/file.txt\", \"symlink\", error);\n```\n\n6.  然后，我们需要检查文件是否可以通过`symlink`访问:\n\n```cpp\n    if (!error) {\n        std::cerr << \"Symlink created\\n\";\n        assert(boost::filesystem::exists(\"symlink\"));\n```\n\n7.  如果`symlink`创建失败，我们将删除创建的文件:\n\n```cpp\n    } else {\n        std::cerr << \"Failed to create a symlink\\n\";\n\n        boost::filesystem::remove_all(\"dir\", error);\n        assert(!error);\n\n        boost::filesystem::remove(\"symlink\", error);\n        assert(!error);\n    } /*if (!error)*/\n} /*main*/\n```\n\n# 它是如何工作的...\n\n我们在[第六章](06.html#9KVM80-712b4ba1126a4c7c89e1d44de61b4bdd)、*操纵任务*的几乎所有食谱中都看到了`boost::system::error_code`在起作用。它可以存储关于错误的信息，并广泛应用于所有的 Boost 库。\n\nIf you do not provide an instance of `boost::system::error_code` to the `Boost.Filesystem` functions, the code will compile well. In that case, when an error occurs, a `boost::filesystem::filesystem_error` exception is thrown.\n\n仔细看看*第三步*。我们使用的是`boost::filesystem::create_directories`功能，而不是`boost::filesystem::create_directory`，因为后者无法创建子目录。`boost::filesystem::remove_all``boost::filesystem::remove`也是如此。第一个可以删除包含文件和子目录的非空目录。第二个删除单个文件。\n\n剩下的步骤很容易理解，应该不会引起任何麻烦。\n\n# 还有更多...\n\n`boost::system::error_code`类是 C++ 11 的一部分，可以在`std::`命名空间的`<system_error>`头中找到。`Boost.Filesystem`的类是 C++ 17 的一部分。\n\n最后，这里给打算用`Boost.Filesystem`的人一个小小的推荐。当错误在文件系统期间发生时，操作是例行的或者应用需要高的责任/性能，为此，使用`boost::system::error_codes`。否则，捕捉异常更可取、更可靠。\n\n# 请参见\n\n目录中的*列表文件也包含`Boost.Filesystem`的信息。阅读 http://boost.org/libs/filesystem 的 Boost 官方文档，获取更多信息和示例。*\n\n# 编写和使用插件\n\n这里有一个棘手的问题:我们希望允许用户编写程序功能的扩展，但我们不想给他们源代码。换句话说，我们想说，“*写一个函数 X，打包到一个共享库中。我们可能会将您的功能与其他用户的功能一起使用！”*\n\nYou meet this technique in everyday life: your browser uses it to allow third-party plugins, your text editor may use it for syntax highlighting, games use **dynamic library loading** for **downloadable content** (**DLC**s) and for adding gamer's content, web pages are returned by servers that use modules/plugins for encryption/authentication and so forth.\n\n对一个用户的函数有什么要求，我们如何在某个时候使用该函数而不将其链接到共享库？\n\n# 准备好\n\n这个食谱需要 C++ 的基础知识。阅读[第 10 章](10.html#FKLNA0-712b4ba1126a4c7c89e1d44de61b4bdd)中的*导出和导入功能和类的可移植方式*是一个要求。\n\n# 怎么做...\n\n首先，你必须和你的用户达成协议:\n\n1.  记录插件接口的需求。例如，你可能会说所有的插件必须导出一个名为`greet`的函数，该函数必须接受`const std::string&`并返回`std::string`。\n\n2.  之后，用户可以通过以下方式开始编写插件/共享库:\n\n```cpp\n#include <string>\n#include <boost/config.hpp>\n\n#define API extern \"C\" BOOST_SYMBOL_EXPORT\n\nAPI std::string greeter(const std::string& name) {\n    return \"Good to meet you, \" + name + \".\";\n}\n```\n\n3.  您用于加载共享库的程序代码必须包含来自`Boost.DLL`的标题:\n\n```cpp\n#include <boost/dll/shared_library.hpp>\n```\n\n4.  加载库的代码必须如下:\n\n```cpp\nint main() {\n    boost::filesystem::path plugin_path = /* path-to-pligin */;\n\n    boost::dll::shared_library plugin(\n        plugin_path,\n        boost::dll::load_mode::append_decorations\n    );\n```\n\n5.  获取用户的功能必须如下所示:\n\n```cpp\n    auto greeter = plugin.get<std::string(const std::string&)>(\"greeter\");\n```\n\n6.  完成了。现在，您可以使用该函数:\n\n```cpp\n    std::cout << greeter(\"Sally Sparrow\");\n}\n```\n\n根据加载的插件，您会得到不同的结果:\n\n`plugin_hello`:\n\n```cpp\nGood to meet you, Sally Sparrow.\n```\n\n`plugin_do_not`:\n\n```cpp\nThey are fast. Faster than you can believe. Don't turn \n\nyour back, don't look away, and don't blink. Good luck, Sally Sparrow.\n```\n\n# 它是如何工作的...\n\n*第二步*有一个小技巧。当你声明一个函数为`extern \"C\"`时，意味着编译器不能**篡改**(改变)函数名。换句话说，在*步骤 2* 中，我们只需创建一个名为`greet`的函数，并从共享库中以该确切名称导出。\n\n在*步骤 4 中，*我们创建一个名为`plugin`的`boost::dll::shared_library`变量。该变量的构造函数通过指定路径将共享库加载到当前可执行文件的地址空间中。在*步骤 5 中，*我们在`plugin`中搜索名为`greet`的函数。我们还指定该函数具有签名`std::string(const std::string&)`，并将指向该函数的指针存储在变量`greet`中。\n\n就这样！从现在开始，我们可以使用`greet`变量作为函数，只要不破坏`plugin`变量及其所有副本。\n\n您可以从共享库中导出多个函数；您甚至可以导出变量。\n\nBe careful! Always link C and C++ libraries dynamically to the plugin and your main executable, because otherwise your application will crash. Always use the same or ABI compatible versions of C and C++ libraries in your plugins and in your application. Otherwise your application will crash. Read the docs for typical missuses!\n\n# 还有更多...\n\n`Boost.DLL`是一个新的图书馆；它出现在 Boost 1.61 中。该库我最喜欢的部分是能够在共享库名称中添加特定于平台的装饰。例如，根据平台的不同，以下代码将尝试加载`\"./some/path/libplugin_name.so\"`、`\"./some/path/plugin_name.dll\"`或`\"./some/path/libplugin_name.dll\"`:\n\n```cpp\nboost::dll::shared_library lib(\n    \"./some/path/plugin_name\",\n    boost::dll::load_mode::append_decorations\n);\n```\n\nC++ 17 没有类似`boost::dll::shared_library`的类。但是，工作正在进行中，有一天我们可能会在 C++ 标准中看到它。\n\n# 请参见\n\n官方文档包含多个例子，更重要的是，还有图书馆[http://boost.org/libs/dll](http://boost.org/libs/dll)网站的典型问题/失误。\n\n# 获取回溯-当前调用序列\n\n当报告错误或失败时，更重要的是报告导致错误的步骤，而不是错误本身。考虑一下天真的交易模拟器:\n\n```cpp\nint main() {\n    int money = 1000;\n    start_trading(money);\n}\n```\n\n它报告的只有一行:\n\n```cpp\nSorry, you're bankrupt!\n```\n\n那是不行的。我们想知道它是怎么发生的，导致破产的步骤是什么！\n\n好吧。让我们修复以下函数，并让它报告导致破产的步骤:\n\n```cpp\nvoid report_bankruptcy() {\n    std::cout << \"Sorry, you're bankrupt!\\n\";\n\n    std::exit(0);\n}\n```\n\n# 入门指南\n\n你需要一个 Boost 1.65 或更新的配方。C++ 基础知识也是要求。\n\n# 怎么做...\n\n对于这个配方，我们只需要构造一个类并输出它:\n\n```cpp\n#include <iostream>\n#include <boost/stacktrace.hpp>\n\nvoid report_bankruptcy() {\n    std::cout << \"Sorry, you're bankrupt!\\n\";\n    std::cout << \"Here's how it happened:\\n\" \n        << boost::stacktrace::stacktrace();\n\n    std::exit(0);\n}\n```\n\n完成了。现在`report_bankruptcy()`输出类似如下的内容(从下往上读):\n\n```cpp\nSorry, you're bankrupt!\nHere's how it happened:\n 0# report_bankruptcy()\n 1# loose(int)\n 2# go_to_casino(int)\n 3# go_to_bar(int)\n 4# win(int)\n 5# go_to_casino(int)\n 6# go_to_bar(int)\n 7# win(int)\n 8# make_a_bet(int)\n 9# loose(int)\n10# make_a_bet(int)\n11# loose(int)\n12# make_a_bet(int)\n13# start_trading(int)\n14# main\n15# 0x00007F79D4C48F45 in /lib/x86_64-linux-\n\ngnu/libc.so.6\n16# 0x0000000000401F39 in ./04_stacktrace\n```\n\n# 它是如何工作的...\n\n所有的魔法都在`boost::stacktrace::stacktrace`等级之内。在构建时，它会将当前调用堆栈快速存储在自身中。`boost::stacktrace::stacktrace`是可复制和可移动的，所以一个存储的 a 调用序列可以传递给其他函数，复制到异常类中，甚至存储在某个文件中。你爱怎么用就怎么用！\n\n输出上的`boost::stacktrace::stacktrace`实例，解码存储的调用序列并尝试获得人类可读的函数名。这就是你在前面的例子中看到的:导致`report_bankruptcy()`函数调用的调用序列。\n\n`boost::stacktrace::stacktrace`你需要迭代存储的地址，将单个地址解码成人类可读的名字。如果不喜欢跟踪的默认输出格式，可以编写自己的函数，以自己喜欢的方式进行输出。\n\n请注意，回溯有用性取决于多种因素。程序的发布版本可能包含内联函数，导致可读性较差的跟踪:\n\n```cpp\n 0# report_bankruptcy()\n 1# go_to_casino(int)\n 2# win(int)\n 3# make_a_bet(int)\n 4# make_a_bet(int)\n 5# make_a_bet(int)\n 6# main\n```\n\n构建没有调试符号的可执行文件可能会产生没有很多函数名的跟踪。\n\nRead the *Configuration and Build* section of the official documentation for more information about different compilation flags and macros that may affect trace readability.\n\n# 还有更多...\n\n一个`Boost.Stacktrace`库对于大项目有一个非常简洁的特性。您可以在链接程序时禁用所有跟踪。这意味着您不需要重建所有的源文件。只需为整个项目定义`BOOST_STACKTRACE_LINK`宏即可。现在，如果你链接到`boost_stacktrace_noop`图书馆，空的痕迹将被收集。与`boost_stacktrace_windbg` / `boost_stacktrace_windbg_cached` / `boost_stacktrace_backtrace` / `... libraries`链接，获得不同可读性的痕迹。\n\n`Boost.Stacktrace`是新图书馆；它出现在 1.65 年的 Boost 中。\n\n`boost::stacktrace::stacktrace`收集当前通话序列非常快；它只是动态地分配一大块内存，并将一堆地址复制到内存中。解码地址要慢得多；它使用多个特定于平台的调用，可以分叉进程，并且可以初始化和使用 **COM** 。\n\nC++ 17 没有`Boost.Stacktrace`功能。正在努力将其添加到下一个 C++ 标准中。\n\n# 请参见\n\nhttp://boost.org/libs/stacktrace/的官方文档中有一些异步信号安全堆栈跟踪的例子，以及所有`Boost.Stacktrace`能力[的详细描述。](http://boost.org/libs/stacktrace)\n\n# 将数据从一个进程快速传递到另一个进程\n\n有时候，我们编写程序，经常互相交流。当程序在不同的机器上运行时，使用套接字是最常见的通信技术。但是如果多个进程在一台机器上运行，我们可以做得更好！\n\n让我们看看如何使用`Boost.Interprocess`库使单个内存片段从不同的进程中可用。\n\n# 准备好\n\n这个食谱需要 C++ 的基础知识。原子变量的知识也是必需的(查看*并参见*部分了解更多关于原子的信息)。有些平台要求链接运行时库`rt`。\n\n# 怎么做...\n\n在本例中，我们将在进程之间共享一个原子变量，使其在新进程启动时递增，在进程终止时递减:\n\n1.  我们需要包含以下进程间通信的标题:\n\n```cpp\n#include <boost/interprocess/managed_shared_memory.hpp> \n```\n\n2.  在标题`typedef`之后，一个检查将帮助我们确保原子可以用于这个例子:\n\n```cpp\n#include <boost/atomic.hpp> \n\ntypedef boost::atomic<int> atomic_t; \n#if (BOOST_ATOMIC_INT_LOCK_FREE != 2) \n#error \"This code requires lock-free boost::atomic<int>\" \n#endif \n```\n\n3.  创建或获取共享内存段:\n\n```cpp\nint main() {\n    boost::interprocess::managed_shared_memory \n        segment(boost::interprocess::open_or_create, \"shm1-cache\", 1024);\n```\n\n4.  获取或构造一个`atomic`变量:\n\n```cpp\n    atomic_t& atomic \n        = *segment.find_or_construct<atomic_t> // 1\n            (\"shm1-counter\")                   // 2\n            (0)                                // 3\n    ;\n```\n\n5.  以通常的方式使用`atomic`变量:\n\n```cpp\n    std::cout << \"I have index \" << ++ atomic \n        << \". Press any key...\\n\";\n    std::cin.get();\n```\n\n6.  销毁`atomic`变量:\n\n```cpp\n    const int snapshot = --atomic;\n    if (!snapshot) {\n        segment.destroy<atomic_t>(\"shm1-counter\");\n        boost::interprocess::shared_memory_object\n                ::remove(\"shm1-cache\");\n    }\n} /*main*/ \n```\n\n仅此而已！现在，如果我们同时运行这个程序的多个实例，我们会看到每个新实例都会增加其索引值:\n\n```cpp\nI have index 1\\. Press any key...\nI have index 2\\. \n\nPress any key...\nI have index 3\\. Press any key...\nI have index 4\\. Press any key...\nI have index 5\\. \n\nPress any key...\n```\n\n# 它是如何工作的...\n\n这个方法的主要思想是获取一段对所有进程都可见的内存，并在其中放置一些数据。让我们来看看*第三步*，在这里我们检索到这样一段记忆。这里，`shm1- cache`是片段的名称(不同的片段名称不同)。你可以给这些片段起任何名字。第一个参数是`boost::interprocess::open_or_create`，它告诉`boost::interprocess::managed_shared_memory`必须打开一个名为`shm1- cache`的现有线段，或者构造它。最后一个参数是线段的大小。\n\nThe size of the segment must be big enough to fit the `Boost.Interprocess` library-specific data in it. That's why we used `1024` and not `sizeof(atomic_t)`. But actually, the operating system rounds this value to the nearest bigger supported value, which is usually equal to or bigger than 4 kilobytes.\n\n*第 4 步*是一个棘手的步骤，因为我们在这里同时执行多个任务。在这一步的`2`部分，我们在线段中找到或构造一个名为`shm1-counter`的变量。在*第 4 步*的`3`部分，我们提供了一个参数，用于变量的初始化，如果在*第 2 步*中没有找到的话。仅当找不到变量并且必须构造变量时，才使用该参数，否则将忽略该参数。仔细看看第二行(部分`1`)。参见对取消引用操作符`*`的调用。我们这样做是因为`segment.find_or_construct<atomic_t>`返回一个指向`atomic_t`的指针，在 C++ 中使用裸指针是一种不好的风格。\n\nWe are using atomic variables in shared memory! This is required, because two or more processes may simultaneously work with the same `shm1-counter` atomic variable.\n\n使用共享内存中的对象时，必须非常小心；别忘了摧毁它们！在*步骤 6* 中，我们正在使用对象和片段的名称销毁它们。\n\n# 还有更多...\n\n仔细看看*第二步*，我们正在检查`BOOST_ATOMIC_INT_LOCK_FREE != 2`。我们正在检查`atomic_t`是否不使用互斥体。这非常重要，因为通常的互斥锁在共享内存中不起作用。所以如果`BOOST_ATOMIC_INT_LOCK_FREE`不等于`2`，我们得到一个未定义的行为。\n\n不幸的是，C++ 11 没有进程间类，而且据我所知，`Boost.Interprocess`并没有被提议包含在 C++ 20 中。\n\nOnce a managed segment is created, it cannot increase in size automatically! Make sure that you are creating segments big enough for your needs, or take a look at the *See also* section for information about increasing managed segments.\n\n共享内存是进程通信的最快方式，但适用于可能共享内存的进程。这通常意味着进程必须运行在同一台主机上或一个**对称多处理** ( **SMP** )集群上。\n\n# 请参见\n\n*   *同步进程间通信*配方将告诉您更多关于共享内存、进程间通信以及同步访问微程内存中的资源的信息\n*   关于原子的更多信息，使用原子配方快速访问公共资源\n*   Boost 的`Boost.Interprocess`官方文档可能也有帮助；在[http://boost.org/libs/interprocess](http://boost.org/libs/interprocess)有售\n*   如何增加管理的细分市场在[http://boost.org/libs/interprocess](http://boost.org/libs/interprocess)的*增长管理的细分市场*中有描述\n\n# 同步进程间通信\n\n在前面的食谱中，我们看到了如何创建共享内存以及如何在其中放置一些对象。现在，是时候做点有用的事情了。我们从[第五章](05.html#7PRJC0-712b4ba1126a4c7c89e1d44de61b4bdd)、*多线程*中的*制作工作队列*食谱来举个例子，让它可以为多个进程工作。在这个例子的最后，我们将得到一个类，它可以存储不同的任务，并在进程之间传递它们。\n\n# 准备好\n\n这个食谱使用了前一个食谱的技术。您还需要阅读[第 5 章](05.html#7PRJC0-712b4ba1126a4c7c89e1d44de61b4bdd)、*多线程*中的*制作工作队列*食谱，并了解其主要思想。该示例需要链接到某些平台上的运行时库`rt`。\n\n# 怎么做...\n\n人们认为，生成单独的子进程而不是线程会使程序更加可靠，因为子进程的终止不会终止主进程。我们在这里不反对这个假设，只是看看如何实现流程之间的数据共享。\n\n1.  该配方需要很多标题:\n\n```cpp\n#include <boost/interprocess/managed_shared_memory.hpp> \n#include <boost/interprocess/containers/deque.hpp> \n#include <boost/interprocess/allocators/allocator.hpp> \n#include <boost/interprocess/sync/interprocess_mutex.hpp> \n#include <boost/interprocess/sync/interprocess_condition.hpp> \n#include <boost/interprocess/sync/scoped_lock.hpp> \n\n#include <boost/optional.hpp> \n```\n\n2.  现在，我们需要定义我们的结构`task_structure`，它将用于存储任务:\n\n```cpp\nstruct task_structure { \n    // ... \n}; \n```\n\n3.  让我们开始写`work_queue`课:\n\n```cpp\nclass work_queue { \npublic: \n    typedef boost::interprocess::managed_shared_memory  \n            managed_shared_memory_t; \n\n    typedef task_structure task_type; \n    typedef boost::interprocess::allocator< \n        task_type,  \n        boost::interprocess::managed_shared_memory::segment_manager \n    > allocator_t; \n```\n\n4.  将`work_queue`的成员写如下:\n\n```cpp\nprivate: \n    managed_shared_memory_t segment_; \n    const allocator_t       allocator_; \n\n    typedef boost::interprocess::deque<task_type, allocator_t> deque_t; \n    deque_t&        tasks_; \n\n    typedef boost::interprocess::interprocess_mutex mutex_t; \n    mutex_t&        mutex_; \n\n    typedef boost::interprocess::interprocess_condition condition_t; \n    condition_t&    cond_; \n\n    typedef boost::interprocess::scoped_lock<mutex_t> scoped_lock_t;\n```\n\n5.  成员的初始化必须如下所示:\n\n```cpp\npublic: \n    explicit work_queue()\n        : segment_(\n              boost::interprocess::open_or_create,\n              \"work-queue\",\n              1024 * 1024 * 32\n        )\n        , allocator_(segment_.get_segment_manager())\n        , tasks_(\n            *segment_.find_or_construct<deque_t>\n              (\"work-queue:deque\")(allocator_)\n        )\n        , mutex_(\n            *segment_.find_or_construct<mutex_t>\n              (\"work-queue:mutex\")()\n        )\n        , cond_(\n            *segment_.find_or_construct<condition_t>\n              (\"work-queue:condition\")()\n        )\n    {}\n```\n\n6.  我们需要对`work_queue`的成员函数做一些小的改动，比如使用`scoped_lock_t`，而不是原来的唯一锁:\n\n```cpp\n    boost::optional<task_type> try_pop_task() { \n        boost::optional<task_type> ret; \n        scoped_lock_t lock(mutex_); \n        if (!tasks_.empty()) { \n            ret = tasks_.front(); \n            tasks_.pop_front(); \n        } \n        return ret; \n    }\n```\n\n7.  不要忘记资源清理:\n\n```cpp\n    void cleanup() {\n        segment_.destroy<condition_t>(\"work-queue:condition\");\n        segment_.destroy<mutex_t>(\"work-queue:mutex\");\n        segment_.destroy<deque_t>(\"work-queue:deque\");\n\n        boost::interprocess::shared_memory_object\n            ::remove(\"work-queue\");\n    }\n```\n\n# 它是如何工作的...\n\n在这个食谱中，我们所做的事情几乎与[第 5 章](05.html#7PRJC0-712b4ba1126a4c7c89e1d44de61b4bdd)、*多线程*中【制作工作队列】 *类*食谱完全相同，但是我们在共享内存中分配数据。\n\nTake additional care when storing the shared memory objects that have pointers or references as member fields. We'll see how to cope with pointers in the next recipe.\n\n看看*第二步*。我们没有使用`boost::function`作为任务类型，因为它里面有指针，所以它在共享内存中不起作用。\n\n*第三步*因`allocator_t`而有趣。如果没有从共享内存段分配内存，则其他进程可以使用它；这就是为什么需要一个特定的容器分配器。`allocator_t`是一个有状态的分配器，这意味着它是和容器一起复制的。此外，它不能被默认构造。\n\n*第四步*很简单，除了我们只提到`tasks_`、`mutex_`和`cond_`。这样做是因为对象本身是在共享内存中构造的。所以，`work_queue`可能只在里面存储引用。\n\n在*步骤 5 中，*我们正在初始化成员。这段代码你一定很熟悉。我们在之前的食谱中做了完全相同的事情。\n\nWe are providing an instance of the allocator to `tasks_` while constructing it. That's because `allocator_t` cannot be constructed by the container itself. Shared memory is not destructed at the exit event of a process, so we may run the program once, post the tasks to a work queue, stop the program, start some other program, and get tasks stored by the first instance of the program. Shared memory is destroyed only at restart, or if you explicitly call `segment.deallocate(\"work-queue\");`.\n\n# 还有更多...\n\n正如在前面的食谱中已经提到的，C++ 17 没有从`Boost.Interprocess`开始的类。此外，您不能在共享内存段中使用 C++ 17 或 C++ 03 容器。其中一些容器可能可以工作，但是这种行为是不可移植的。\n\n如果你查看一些`<boost/interprocess/containers/*.hpp>`头，你会发现它们只是使用了`Boost.Containers`库中的容器:\n\n```cpp\nnamespace boost { namespace interprocess { \n    using boost::container::vector; \n}} \n```\n\n`Boost.Interprocess`的容器拥有`Boost.Containers`库的所有优点，包括右值引用及其在旧编译器上的模拟。\n\nA `Boost.Interprocess`是在同一台机器上运行的进程通信的最快解决方案。\n\n# 请参见\n\n*   使用共享内存中指针的*配方*\n*   阅读[第五章](05.html#7PRJC0-712b4ba1126a4c7c89e1d44de61b4bdd)**多线程*，了解更多关于同步原语和多线程的信息*\n**   更多示例和信息，请参考 Boost 官方文档`Boost.Interprocess`库；可通过以下链接获得:[http://boost.org/libs/interprocess](http://boost.org/libs/interprocess)*\n\n *# 使用共享内存中的指针\n\n很难想象写出一些没有指针的低级 C++ 核心类。指针和引用在 C++ 中无处不在，它们在共享内存中不起作用！因此，如果我们在共享内存中有这样的结构，并且将共享内存中某个整数变量的地址分配给`pointer_`，那么`pointer_`在其他进程中将无效:\n\n```cpp\nstruct with_pointer { \n    int* pointer_; \n    // ... \n    int value_holder_; \n}; \n```\n\n我们如何解决这个问题？\n\n# 准备好\n\n理解这个食谱需要前面的食谱。该示例需要链接到某些平台上的运行时系统库`rt`。\n\n# 怎么做...\n\n修复它非常简单；我们只需要将指针替换为`offset_ptr<>`:\n\n```cpp\n#include <boost/interprocess/offset_ptr.hpp> \n\nstruct correct_struct { \n    boost::interprocess::offset_ptr<int> pointer_; \n    // ... \n    int value_holder_; \n}; \n```\n\n现在，我们可以像使用普通指针一样自由使用它:\n\n```cpp\nint main() {\n    boost::interprocess::managed_shared_memory \n        segment(boost::interprocess::open_or_create, \"segment\", 4096);\n\n    correct_struct* ptr =\n        segment.find<correct_struct>(\"structure\").first;\n\n    if (ptr) {\n        std::cout << \"Structure found\\n\";\n        assert(*ptr->pointer_ == ethalon_value);\n        segment.destroy<correct_struct>(\"structure\");\n    }\n}\n```\n\n# 它是如何工作的...\n\n我们不能在共享内存中使用指针，因为当一块共享内存被映射到一个进程的地址空间时，它的地址只对该进程有效。当我们得到一个变量的地址时，它只是该进程的一个本地地址。其他进程将共享内存映射到不同的基址，因此，变量地址也不同。\n\n![](img/00019.jpeg)\n\n那么，我们如何处理一个总是在变化的地址呢？有一招！由于指针和结构在同一个共享内存段中，它们之间的距离不会改变。`boost::interprocess::offset_ptr`背后的想法是记住`offset_ptr`和尖值之间的距离。出于尊重，`offset_ptr`将距离值添加到`offset_ptr`变量的进程相关地址。\n\n偏移指针模仿指针的行为，所以它是一个可以快速应用的插入替换。\n\nDo not place the classes that may have pointers or references into shared memory!\n\n# 还有更多...\n\n偏移指针的工作速度比普通指针稍慢，因为每次取消引用时，都需要计算地址。但是，这种差异通常不是你应该担心的。\n\nC++ 17 没有偏移指针。\n\n# 请参见\n\n*   Boost 的官方文档包含很多例子和更高级的`Boost.Interprocess`特性；在[http://boost.org/libs/interprocess](http://boost.org/libs/interprocess)有售\n*   阅读文件的最快方法食谱包含一些关于`Boost.Interprocess`库的非传统用法的信息\n\n# 读取文件的最快方法\n\n在互联网上，人们都在问*“最快的文件读取方式是什么？”*。让我们让这个食谱的任务变得更加困难:什么是读取二进制文件的最快和可移植的方法？\n\n# 准备好\n\n这个食谱需要 C++ 和`std::fstream`的基础知识。\n\n# 怎么做...\n\n该配方中的技术被对输入和输出性能至关重要的应用广泛使用。这是读取文件最快的方法:\n\n1.  我们需要包含来自`Boost.Interprocess`库的两个标题:\n\n```cpp\n#include <boost/interprocess/file_mapping.hpp> \n#include <boost/interprocess/mapped_region.hpp> \n```\n\n2.  现在，我们需要打开一个文件:\n\n```cpp\nconst boost::interprocess::mode_t mode = boost::interprocess::read_only; \nboost::interprocess::file_mapping fm(filename, mode); \n```\n\n3.  这个方法的主要部分是将所有文件映射到内存:\n\n```cpp\nboost::interprocess::mapped_region region(fm, mode, 0, 0);\n```\n\n4.  获取指向文件中数据的指针:\n\n```cpp\nconst char* begin = static_cast<const char*>(\n    region.get_address()\n);\n```\n\n就这样！现在，我们可以像处理普通内存一样处理文件:\n\n```cpp\nconst char* pos = std::find(\n    begin, begin + region.get_size(), '\\1'\n);\n```\n\n# 它是如何工作的...\n\n所有流行的操作系统都能够将文件映射到进程的地址空间。完成这种映射后，该过程可以像使用普通内存一样使用这些地址。操作系统负责所有文件操作，如缓存和预读。\n\n为什么它比传统的读/写更快？这是因为在大多数情况下，读/写被实现为内存映射，并将数据复制到用户指定的缓冲区。所以，read 通常比 memory map 做的多一点。\n\n就像标准库的`std::fstream`的情况一样，我们在打开文件时必须提供一个打开模式。参见我们提供`boost::interprocess::read_only`模式的*步骤 2* 。\n\n参见第 3 步第 3 步第 1 步，我们一次映射了整个文件。这个操作实际上非常快，因为操作系统不从磁盘读取数据，而是等待对映射区域的一部分的请求。在请求了映射区域的一部分后，操作系统将文件的该部分从磁盘加载到内存中。正如我们可能看到的，内存映射操作是懒惰的，映射区域的大小不会影响性能。\n\nHowever, a 32-bit OS cannot memory-map big files, so you have to map them by pieces. POSIX (Linux) operating systems require the `_FILE_OFFSET_BITS=64` macro to be defined for the whole project to work with big files on a 32-bit platform. Otherwise, the OS won't be able to map parts of the file that are beyond 4 GB.\n\n现在，是时候衡量性能了:\n\n```cpp\n    $ TIME=\"%E\" time ./reading_files m\n    mapped_region: 0:00.08\n\n    $ TIME=\"%E\" time ./reading_files r\n    ifstream: 0:00.09\n\n    $ TIME=\"%E\" time ./reading_files a\n    C: 0:00.09\n```\n\n正如预期的那样，内存映射文件比传统读取稍快。我们可能还会看到，纯 C 方法的性能与 C++ `std::ifstream`类相同，所以不要在 C++ 中使用与`FILE*`相关的函数。它们只是为了 C，而不是为了 C++！\n\n为了`std::ifstream`的最佳性能，不要忘记以二进制模式打开文件并按块读取数据:\n\n```cpp\nstd::ifstream f(filename, std::ifstream::binary); \n// ... \nchar c[kilobyte]; \nf.read(c, kilobyte); \n```\n\n# 还有更多...\n\n不幸的是，内存映射文件的类不是 C++ 17 的一部分，看起来它们也不会出现在 C++ 20 中。\n\n写入内存映射区域也是一个非常快速的操作。操作系统缓存写入内容，不会立即刷新对光盘的修改。操作系统和`std::ofstream`数据缓存是有区别的。如果`std::ofstream`数据被应用缓存，如果应用终止，缓存的数据可能会丢失。当数据被操作系统缓存时，应用的终止不会导致数据丢失。在这两种情况下，电源故障和操作系统崩溃都会导致数据丢失。\n\n如果多个进程映射单个文件，并且其中一个进程修改了映射的区域，则其他进程可以立即看到更改(即使没有实际将数据写入磁盘！现代 OS 非常聪明！).\n\n# 请参见\n\n`Boost.Interprocess`库包含了很多有用的功能来配合系统工作；这本书并没有涵盖所有的内容。你可以在官方网站上读到更多关于这个伟大图书馆的信息:http://boost.org/libs/interprocess。\n\n# 共同诉讼-拯救国家和推迟执行\n\n如今，许多嵌入式设备仍然只有一个内核。开发人员为这些设备编写程序，试图从这些设备中获取最大性能。\n\n对此类设备使用`Boost.Threads`或其他一些线程库是无效的。操作系统将被迫为执行、管理资源等安排线程，因为硬件无法并行运行它们。\n\n那么，如何在等待主体部分某个资源的同时，强制程序切换到子程序的执行呢？此外，如何控制子程序的执行时间？\n\n# 准备好\n\n这个食谱需要 C++ 和模板的基本知识。阅读一些关于`Boost.Function`的食谱也可能有所帮助。\n\n# 怎么做...\n\n这个食谱是关于允许多个入口点的**子程序**或**子程序**的。多个入口点使我们能够在特定位置暂停和恢复程序的执行，切换到其他子程序。\n\n1.  `Boost.Coroutine2`图书馆几乎什么都管。我们只需要包括它的标题:\n\n```cpp\n#include <boost/coroutine2/coroutine.hpp> \n```\n\n2.  使用所需的输入参数类型创建一个协同类型:\n\n```cpp\ntypedef boost::coroutines2::asymmetric_coroutine<std::size_t> corout_t;\n```\n\n3.  创建一个类，代表一个子程序:\n\n```cpp\nstruct coroutine_task {\n    std::string& result;\n\n    coroutine_task(std::string& r)\n        : result(r)\n    {}\n\n    void operator()(corout_t::pull_type& yield);\n\nprivate:\n    std::size_t ticks_to_work;\n    void tick(corout_t::pull_type& yield);\n};\n```\n\n4.  让我们来创建花冠本身:\n\n```cpp\nint main() {\n    std::string result;\n    coroutine_task task(result);\n    corout_t::push_type coroutine(task);\n```\n\n5.  现在，我们可以在等待主程序中的某个事件时执行子程序:\n\n```cpp\n    // Somewhere in main():\n\n    while (!spinlock.try_lock()) {\n        // We may do some useful work, before\n        // attempting to lock a spinlock once more.\n        coroutine(10); // 10 is the ticks count to run.\n    }\n    // Spinlock is locked.\n    // ...\n\n    while (!port.block_ready()) {\n        // We may do some useful work, before\n        // attempting to get block of data once more.\n        coroutine(300); // 300 is the ticks count to run.\n\n        // Do something with `result` variable.\n    }\n```\n\n6.  协同方法可能是这样的:\n\n```cpp\nvoid coroutine_task::operator()(corout_t::pull_type& yield) {\n    ticks_to_work = yield.get();\n\n    // Prepare buffers.\n    std::string buffer0;\n\n    while (1) {\n        const bool requiers_1_more_copy = copy_to_buffer(buffer0);\n        tick(yield);\n\n        if (requiers_1_more_copy) {\n            std::string buffer1;\n            copy_to_buffer(buffer1);\n            tick(yield);\n\n            process(buffer1);\n            tick(yield);\n        }\n\n        process(buffer0);\n        tick(yield);\n    }\n}\n```\n\n7.  `tick()`功能可以这样实现:\n\n```cpp\nvoid coroutine_task::tick(corout_t::pull_type& yield) {\n    if (ticks_to_work != 0) {\n        --ticks_to_work;\n    }\n\n    if (ticks_to_work == 0) {\n        // Switching back to main.\n        yield();\n\n        ticks_to_work = yield.get();\n    }\n}\n```\n\n# 它是如何工作的...\n\n在*步骤 2* 中，我们使用`std::size_t`作为模板参数来描述子程序的输入参数。\n\n*第三步*非常简单，除了`corout_t::pull_type& yield`参数。我们将在几秒钟内看到它的运行。\n\n当我们在*步骤 5* 中调用`coroutine(10)`时，我们正在制作一个协同程序来执行。执行跳转到`coroutine_task::operator()`，调用`yield.get()`返回输入参数`10`。执行继续，并且`coroutine_task::tick`功能测量经过的刻度。\n\n最有趣的部分来了！\n\n在*步骤 7 中，*如果在函数`coroutine_task::tick`中`ticks_to_work`变量变得等于`0`，则在`yield()`暂停执行协同程序，`main()`继续执行。在下一次调用`coroutine(some_value)`时，从`tick`功能的中间继续执行协同程序，就在`yield()`旁边的行。在该行中，执行`ticks_to_work = yield.get();`，并且`ticks_to_work`变量开始保持新的输入值`some_value`。\n\n这意味着我们可以在函数的多个地方暂停/继续协同。所有的函数状态和变量都被恢复:\n\n![](img/00020.jpeg)\n\n让我描述一下协同程序和线程之间的主要区别。当一个协同任务被执行时，主任务什么也不做。当主任务被执行时，协同任务什么也不做。线程没有这样的保证。使用 coroutines，您可以显式指定何时启动子任务以及何时挂起它。在单核环境中，线程随时可能切换；你无法控制这种行为。\n\n# 还有更多...\n\n在切换线程的时候，OS 做了很多工作，所以不是一个很快的操作。但是，使用 coroutines，您可以完全控制切换任务；此外，您不需要做一些特定于操作系统的内部内核工作。切换协同程序比切换线程要快得多，尽管没有调用`boost::function`快。\n\n`Boost.Coroutine2`库关心在协同任务中为变量调用析构函数，所以不需要担心泄漏。\n\nCoroutines use the `boost::coroutines2::detail::forced_unwind` exception to free resources that are not derived from `std::exception`. You must take care to not catch that exception in coroutine tasks.\n\n你不能复制`Boost.Coroutine2`花冠，但你可以`std::move`它们。\n\n有一个`Boost.Coroutine`库(末尾没有`2`！)，这不需要 C++ 11 兼容的编译器。但是该库已被弃用，并且有一些不同之处(例如，它不传播来自 coroutines 的异常)。当心差异！`Boost.Coroutine`在 Boost 1.56 中也显著改变了界面。\n\nC++ 17 没有协同程序。但是 **Coroutines TS** 几乎已经准备好了，所以下一个 C++ 标准将它们开箱即用的可能性很大。\n\n花冠 TS 不同于`Boost.Coroutine2`！Boost 提供**stack ful**coroutine，也就是说你不需要特意用宏/关键词修饰你的代码来使用它们。但这也意味着 Boost 协同程序更难被编译器优化，它们可能会分配更多的内存。协同程序 TS 提供了**无堆栈**协同程序，这意味着编译器可以精确计算协同程序所需的内存，甚至优化整个协同程序。但是，这种方法需要更改代码，采用起来可能会稍微困难一些。\n\n# 请参见\n\n*   Boost 的官方文档包含了更多的例子、性能说明、限制以及`Boost.Coroutines2`库的用例；可通过以下链接获得[http://boost.org/libs/coroutine2](http://boost.org/libs/coroutine2)\n*   从[第 2 章](02.html#36VSO0-712b4ba1126a4c7c89e1d44de61b4bdd)、*管理资源*、[第 5 章](05.html#7PRJC0-712b4ba1126a4c7c89e1d44de61b4bdd)、*多线程*来看一下菜谱，得到`Boost.Coroutine`、`Boost.Thread`和`Boost.Function`库的区别\n*   对 Coroutines TS 感兴趣？以下是作者 *CppCon 2016: Gor Nishanov 关于它们的实现的有趣的谈话。C++ 协同程序:在[https://www.youtube.com/watch?v=8C8NnE1Dg4A](https://www.youtube.com/watch?v=8C8NnE1Dg4A)的封面下**"
  },
  {
    "path": "docs/boost-cpp-app-dev-cb/12.md",
    "content": "# 十二、Boost 的冰山一角\n\n在本章中，我们将介绍:\n\n*   使用图形\n*   可视化图形\n*   使用真随机数发生器\n*   使用便携式数学函数\n*   编写测试用例\n*   在一个测试模块中组合多个测试用例\n*   操纵图像\n\n# 介绍\n\nBoost 是一个庞大的图书馆集合。其中一些图书馆很小，适合日常使用，而另一些图书馆需要一本单独的书来描述它们的所有功能。这一章专门介绍了一些大型图书馆，并提供了对它的基本理解。\n\n前两个食谱将解释`Boost.Graph`的用法。这是一个拥有数量惊人的算法的大图书馆。我们将看到一些基础知识，可能也是开发中最重要的部分——图形可视化。\n\n我们还将看到一个非常有用的生成真随机数的方法。这是编写安全密码系统的一个非常重要的方法。\n\n一些 C++ 标准库缺少数学函数。我们将看看如何使用 Boost 修复这个问题。但是，这本书的格式没有留下描述所有功能的空间。\n\n编写测试用例在*编写测试用例*和*中描述，在一个测试模块*中组合多个测试用例。这对任何生产质量系统都很重要。\n\n最后一个食谱是关于一个图书馆，它在我大学期间帮助了我很多课程。使用它可以创建和修改图像。我个人使用它来可视化不同的算法，在图像中隐藏数据，在图像上签名，以及生成纹理。\n\n不幸的是，即使是这一章也不能告诉你所有的 Boost 库。也许有一天，我会再写一本书，然后，再写几本。\n\n# 使用图形\n\n有些任务需要将数据表示为图形。`Boost.Graph`是一个库，旨在提供一种在内存中构建和表示图形的灵活方式。它还包含许多处理图的算法，例如拓扑排序、广度优先搜索、深度优先搜索和 Dijkstra 最短路径。\n\n好了，我们用`Boost.Graph`来执行一些基本任务吧！\n\n# 准备好\n\n这个食谱只需要 C++ 和模板的基础知识。\n\n# 怎么做...\n\n在这个食谱中，我们将描述一个图形类型，创建一个该类型的图形，给图形添加一些顶点和边，并搜索一个特定的顶点。这应该足以从`Boost.Graph`开始。\n\n1.  我们从描述图形类型开始:\n\n```cpp\n#include <boost/graph/adjacency_list.hpp> \n#include <string> \n\ntypedef std::string vertex_t; \ntypedef boost::adjacency_list< \n    boost::vecS \n    , boost::vecS \n    , boost::bidirectionalS \n    , vertex_t \n> graph_type; \n```\n\n2.  现在，我们构建它:\n\n```cpp\nint main() {\n    graph_type graph; \n```\n\n3.  让我们来实施一些加快图形构建的未记录的技巧:\n\n```cpp\n    static const std::size_t vertex_count = 5; \n    graph.m_vertices.reserve(vertex_count); \n```\n\n4.  现在，我们准备向图中添加顶点:\n\n```cpp\n    typedef boost::graph_traits<\n        graph_type\n    >::vertex_descriptor descriptor_t;\n\n    descriptor_t cpp\n        = boost::add_vertex(vertex_t(\"C++\"), graph);\n    descriptor_t stl\n        = boost::add_vertex(vertex_t(\"STL\"), graph);\n    descriptor_t boost\n        = boost::add_vertex(vertex_t(\"Boost\"), graph);\n    descriptor_t guru\n        = boost::add_vertex(vertex_t(\"C++ guru\"), graph);\n    descriptor_t ansic\n        = boost::add_vertex(vertex_t(\"C\"), graph);\n```\n\n5.  现在是连接顶点和边的时候了:\n\n```cpp\n    boost::add_edge(cpp, stl, graph); \n    boost::add_edge(stl, boost, graph); \n    boost::add_edge(boost, guru, graph); \n    boost::add_edge(ansic, guru, graph); \n} // end of main()\n```\n\n6.  我们可以做一个搜索顶点的函数:\n\n```cpp\ninline void find_and_print(\n    const graph_type& graph, boost::string_ref name)\n{\n```\n\n7.  接下来是一个获取所有顶点迭代器的代码:\n\n```cpp\n    typedef typename boost::graph_traits<\n        graph_type\n    >::vertex_iterator vert_it_t;\n\n    vert_it_t it, end;\n    boost::tie(it, end) = boost::vertices(graph);\n```\n\n8.  是时候搜索所需的顶点了:\n\n```cpp\n    typedef typename boost::graph_traits<\n        graph_type\n    >::vertex_descriptor desc_t;\n\n    for (; it != end; ++ it) {\n        const desc_t desc = *it;\n        const vertex_t& vertex = boost::get(\n            boost::vertex_bundle, graph\n        )[desc];\n\n        if (vertex == name.data()) {\n            break;\n        }\n    }\n\n    assert(it != end);\n    std::cout << name << '\\n';\n} /* find_and_print */\n```\n\n# 它是如何工作的...\n\n在*第 1 步*中，我们描述了我们的图形必须是什么样子，以及它必须基于什么类型。`boost::adjacency_list`是一个将图形表示为二维结构的类，其中第一维包含顶点，第二维包含该顶点的边。`boost::adjacency_list`必须是表示图形的默认选择，因为它适合大多数情况。\n\n第一个模板参数`boost::adjacency_list`描述了用于表示每个顶点的边列表的结构。第二个描述了存储顶点的结构。我们可以使用特定的选择器为这些结构选择不同的标准库容器，如下表所示:\n\n| 选择器 | 标准库容器 |\n| `boost::vecS` | `std::vector` |\n| `boost::listS` | `std::list` |\n| `boost::slistS` | `std::slist` |\n| `boost::setS` | `std::set` |\n| `boost::multisetS` | `std::multiset` |\n| `boost::hash_setS` | `std::hash_set` |\n\n第三个模板参数用于制作间接、有向或双向图。分别使用`boost::undirectedS`、`boost::directedS`和`boost::bidirectionalS`选择器。\n\n第五个模板参数描述了用作顶点的数据类型。在我们的例子中，我们选择`std::string`。我们还可以支持边的数据类型，并将其作为模板参数提供。\n\n*第 2 步*和*第 3 步*很简单，但是在*第 4 步*中，你可能会看到一些没有记录的方法来加快图形的构建。在我们的例子中，我们使用`std::vector`作为存储顶点的容器，所以我们可以强制它为所需数量的顶点保留内存。这导致在向图中插入顶点的过程中，内存分配/解除分配和复制操作减少。这个步骤不是很便携，在未来版本的 Boost 中可能会中断，因为这个步骤高度依赖于`boost::adjacency_list`的当前实现和所选择的存储顶点的容器类型。\n\n在*步骤 4* 中，我们看到了如何将顶点添加到图形中。注意`boost::graph_traits<graph_type>`是如何被使用的。`boost::graph_traits`类用于获取特定于图形类型的类型。我们将在本章后面看到它的用法和一些特定于图形的类型的描述。*第五步*展示了我们需要什么来连接顶点和边。\n\nIf we had provided some datatype for the edges, adding an edge would look as follows: `boost::add_edge(ansic, guru, edge_t(initialization_parameters), graph)`\n\n在*步骤 6 中，*图形类型是一个`template`参数。建议这样做，以实现更好的代码重用性，并使该函数与其他图形类型一起工作。\n\n在*步骤 7* 中，我们看到如何迭代图的所有顶点。顶点迭代器的类型是从`boost::graph_traits`接收的。函数`boost::tie`是`Boost.Tuple`的一部分，用于从元组中获取变量的值。因此，调用`boost::tie(it, end) = boost::vertices(g)`会将`begin`迭代器放入`it`变量，将`end`迭代器放入`end`变量。\n\n这可能会让你感到惊讶，但是取消顶点迭代器的引用并不会返回顶点数据。相反，它返回顶点描述符`desc`，可以在`boost::get(boost::vertex_bundle, g)[desc]`中使用它来获取顶点数据，就像我们在*步骤 8* 中所做的那样。顶点描述符类型用于许多`Boost.Graph`函数。我们已经在*步骤 5* 中看到了它在边缘构建功能中的使用。\n\nAs already mentioned, the `Boost.Graph` library contains implementations of many algorithms. You may find many search policies implemented, but we won't discuss them in this book. We limit this recipe just to the basics of the graph library.\n\n# 还有更多...\n\n`Boost.Graph`库不是 C++ 17 的一部分，也不会是下一个 C++ 标准的一部分。当前的实现不支持像右值引用这样的 C++ 11 特性。如果我们使用难以复制的顶点，我们可以通过以下技巧获得速度:\n\n```cpp\n vertex_descriptor desc = boost::add_vertex(graph);\n boost::get(boost::vertex_bundle, g_)[desc] = std::move(vertex_data);\n```\n\n它避免了在`boost::add_vertex(vertex_data, graph)`内的复制构造，而是使用带有移动赋值的默认构造。\n\n`Boost.Graph`的效率取决于多种因素，例如底层容器类型、图形表示、边和顶点数据类型。\n\n# 请参见\n\n阅读*可视化图表*食谱可以帮助您轻松处理图表。您也可以通过以下链接阅读其官方文档:[http://boost.org/libs/graph](http://boost.org/libs/graph)\n\n# 可视化图形\n\n由于可视化的问题，制作操纵图形的程序从来都不容易。当我们使用标准的库容器如`std::map`和`std::vector`时，我们总是可以打印容器的内容，看看里面发生了什么。但是当我们处理复杂的图形时，很难清晰地可视化内容；文本表示对人类不友好，因为它通常包含太多的顶点和边。\n\n在这个食谱中，我们将使用 **Graphviz** 工具来看一下`Boost.Graph`的可视化。\n\n# 准备好\n\n要可视化图形，您需要一个 Graphviz 可视化工具。还需要了解前面的配方。\n\n# 怎么做...\n\n可视化分两个阶段完成。在第一阶段，我们让我们的程序以适合 Graphviz 的文本格式输出图形的描述。在第二阶段，我们将第一步的输出导入可视化工具。这个食谱中编号的步骤都是关于第一阶段的。\n\n1.  让我们按照前面的方法为`graph_type`编写`std::ostream`运算符:\n\n```cpp\n#include <boost/graph/graphviz.hpp>\n\nstd::ostream& operator<<(std::ostream& out, const graph_type& g) {\n    detail::vertex_writer<graph_type> vw(g);\n    boost::write_graphviz(out, g, vw);\n\n    return out;\n}\n```\n\n2.  前一步中使用的`detail::vertex_writer`结构必须定义如下:\n\n```cpp\n#include <iosfwd>\n\nnamespace detail {\n    template <class GraphT>\n    class vertex_writer {\n        const GraphT& g_;\n\n    public:\n        explicit vertex_writer(const GraphT& g)\n            : g_(g)\n        {}\n\n        template <class VertexDescriptorT>\n        void operator()(\n            std::ostream& out,\n            const VertexDescriptorT& d) const\n        {\n            out << \" [label=\\\"\"\n                << boost::get(boost::vertex_bundle, g_)[d] \n                << \"\\\"]\"; \n        }\n    }; // vertex_writer\n} // namespace detail\n```\n\n仅此而已。现在，如果我们使用`std::cout << graph;`命令可视化上一个配方的图形，输出可以使用`dot`命令行工具创建图形图片:\n\n```cpp\n    $ dot -Tpng -o dot.png\n\n    digraph G {\n    0 [label=\"C++\"];\n    1 [label=\"STL\"];\n    2 [label=\"Boost\"];\n    3 [label=\"C++ guru\"];\n    4 [label=\"C\"];\n    0->1 ;\n    1->2 ;\n    2->3 ;\n    4->3 ;\n    }\n\n```\n\n下图描述了前面命令的输出:\n\n![](img/00021.gif)\n\n如果命令行让你害怕，我们也可以使用 **Gvedit** 或 **XDot** 程序进行可视化。\n\n# 它是如何工作的...\n\n`Boost.Graph`库包含以 Graphviz (DOT)格式输出图形的功能。如果我们在*步骤 1* 中用两个参数编写`boost::write_graphviz(out, g)`，该函数将输出一个顶点编号为`0`的图形图片。那不是很有用，所以我们提供了一个输出顶点名称的手写`vertex_writer`类的实例。\n\n正如我们在*步骤 2* 中看到的，Graphviz 工具理解 DOT 格式。如果你想为你的图表输出更多的信息，那么你可能需要阅读 Graphviz 文档来获得更多关于 DOT 格式的信息。\n\n如果您希望在可视化过程中向边缘添加一些数据，我们需要向`boost::write_graphviz`提供一个边缘可视化器的实例作为第四个参数。\n\n# 还有更多...\n\nC++ 17 不包含`Boost.Graph`或者图形可视化的工具。但是你不需要担心，因为有很多其他的图形格式和可视化工具`Boost.Graph`可以使用它们。\n\n# 请参见\n\n*   使用图形的*配方包含关于`Boost.Graphs`构造的信息*\n**   在[http://www.graphviz.org/](http://www.graphviz.org/)你会发现很多关于 DOT 格式和 Graphviz 的信息*\n\n **   Boost 的官方文档`Boost.Graph`库包含多个例子和有用的信息，可以在[http://boost.org/libs/graph](http://boost.org/libs/graph)找到\n\n# 使用真随机数发生器\n\n我知道许多商业产品使用不正确的方法获取随机数的例子。遗憾的是，一些公司仍然在密码学和银行软件中使用`rand()`。\n\n让我们看看如何使用适用于银行软件的`Boost.Random`获得完全随机的**均匀分布**。\n\n# 入门指南\n\n这个食谱需要 C++ 的基础知识。了解不同类型的发行版也会有所帮助。该配方中的代码需要链接到`boost_random`库。\n\n# 怎么做...\n\n要创建一个真正的随机数，我们需要操作系统或处理器的帮助。这是如何使用 Boost 完成的:\n\n1.  我们需要包含以下标题:\n\n```cpp\n#include <boost/config.hpp> \n#include <boost/random/random_device.hpp> \n#include <boost/random/uniform_int_distribution.hpp>\n```\n\n2.  高级随机位提供商在不同平台下有不同的名称:\n\n```cpp\nint main() {\n    static const std::string provider = \n#ifdef BOOST_WINDOWS \n        \"Microsoft Strong Cryptographic Provider\" \n#else \n        \"/dev/urandom\" \n#endif \n    ; \n```\n\n3.  现在，我们准备用`Boost.Random`初始化发电机:\n\n```cpp\n    boost::random_device device(provider); \n```\n\n4.  让我们得到一个均匀分布，它返回一个介于`1000`和`65535`之间的值:\n\n```cpp\n    boost::random::uniform_int_distribution<unsigned short> random(1000);\n```\n\n就这样。现在，我们可以使用`random(device)`调用获得真正的随机数。\n\n# 它是如何工作的...\n\n为什么`rand()`功能不适合银行？因为它会生成伪随机数，这意味着黑客可能会预测下一个生成的数字。这是所有伪随机数算法的问题。有些算法更容易预测，有些更难，但这仍然是可能的。\n\n这就是为什么，我们在这个例子中使用`boost::random_device`(见*第三步*)。该设备从操作系统中收集关于随机事件的信息，以产生不可预测的统一随机位。这种事件的例子是按键之间的延迟、某些硬件中断之间的延迟以及内部中央处理器的随机位发生器。\n\n操作系统可能有不止一种这样的随机位生成器。在我们的 POSIX 系统示例中，我们使用了`/dev/urandom`而不是更安全的`/dev/random`，因为后者会一直处于阻塞状态，直到操作系统捕获到足够多的随机事件。等待熵可能需要几秒钟，这通常不适合应用。长寿命**GPG**/**SSL**/**SSH**钥匙使用`/dev/random`。\n\n现在我们已经完成了发电机，是时候进入*第 4 步*并讨论分配类了。如果生成器只是生成均匀分布的位，则分布类会根据这些位生成一个随机数。在*第 4 步*中，我们做了一个均匀分布，返回一个`unsigned short`类型的随机数。参数`1000`意味着分布必须返回大于或等于`1000`的数字。我们还可以提供最大值作为第二个参数，默认情况下，它等于返回类型中可存储的最大值。\n\n# 还有更多...\n\n`Boost.Random`有大量的真/伪随机位发生器和分布，以满足不同的需求。避免复制发行版和生成器。这可能是一个昂贵的手术。\n\nC++ 11 支持不同的分发类和生成器。您可以在`std::`命名空间的`<random>`头中找到这个例子中的所有类。`Boost.Random`库不使用 C++ 11 特性，也不是该库真正需要的。应该使用 Boost 实现还是标准库？Boost 提供了跨系统的更好的可移植性。然而，一些标准库可能有程序集优化的实现，并且可能提供一些有用的扩展。\n\n# 请参见\n\n官方文档包含了一个完整的生成器和发行版的列表以及描述。可在以下链接获得:[http://boost.org/libs/random.](http://boost.org/libs/random)\n\n# 使用便携式数学函数\n\n有些项目需要特定的三角函数，这是一个用于数值求解常微分方程以及处理分布和常数的库。`Boost.Math`的所有这些部分即使放在单独的书中也很难适应。单一的食谱肯定是不够的。所以，让我们把重点放在非常基本的日常使用的函数上来处理浮点类型。\n\n我们将编写一个便携式函数，检查输入值是无穷大还是**不是**-**a**-**Number**(**NaN**)值，如果值为负，则更改符号。\n\n# 准备好\n\n这个食谱需要 C++ 的基础知识。了解 C99 标准的人会发现这个食谱有很多共同点。\n\n# 怎么做...\n\n执行以下步骤检查输入值是否为无穷大和 NaN 值，如果值为负，则更改符号:\n\n1.  我们需要以下标题:\n\n```cpp\n#include <boost/math/special_functions.hpp> \n#include <cassert> \n```\n\n2.  断言无穷大和 NaN 可以这样做:\n\n```cpp\ntemplate <class T> \nvoid check_float_inputs(T value) { \n    assert(!boost::math::isinf(value)); \n    assert(!boost::math::isnan(value)); \n```\n\n3.  使用以下代码更改标志:\n\n```cpp\n    if (boost::math::signbit(value)) { \n        value = boost::math::changesign(value); \n    } \n\n    // ... \n} // check_float_inputs \n```\n\n就这样！现在，我们可以检查`check_float_inputs(std::sqrt(-1.0))`和`check_float_inputs(std::numeric_limits<double>::max() * 2.0)`是否会触发断言。\n\n# 它是如何工作的...\n\n实类型具有无法使用相等运算符检查的特定值。例如，如果变量`v`包含 NaN，`assert(v != v)`可能通过，也可能不通过，这取决于编译器。\n\n对于这种情况，`Boost.Math`提供了可以可靠地检查无穷大和 NaN 值的函数。\n\n*第三步*包含`boost::math::signbit`功能，需要澄清。该函数返回一个有符号位，当数字为负时为`1`，当数字为正时为`0`。换句话说，如果值为负，则返回`true`。\n\n看*第三步，*有些读者可能会问，为什么不能直接乘以`-1`而不叫`boost::math::changesign`？我们可以。但是，乘法运算可能比`boost::math::changesign`慢，并且不能保证在特殊值下有效。例如，如果您的代码可以使用`nan`，则*第 3 步*中的代码可以更改`-nan`的符号并将`nan`写入变量。\n\nThe `Boost.Math` library maintainers recommend wrapping math functions from this example in round parentheses to avoid collisions with C macro. It is better to write `(boost::math::isinf)(value)` instead of `boost::math::isinf(value)`.\n\n# 还有更多...\n\nC99 包含本配方中描述的所有功能。为什么我们在 Boost 中需要它们？嗯，有些编译器厂商认为程序员不需要 C99 的全面支持，所以你不会在至少一个非常流行的编译器中找到那些函数。另一个原因是`Boost.Math`函数可能用于行为类似数字的类。\n\n`Boost.Math`是一个非常快速、便携、可靠的库。**数学特殊函数**是`Boost.Math`库的一部分，一些数学特殊函数被 C++ 17 接受。然而，`Boost.Math`提供了更多，并且有高可用性的循环版本，具有更好的复杂性和更好的适合一些任务(像数值积分)。\n\n# 请参见\n\nBoost 的官方文档包含了很多有趣的例子和教程，可以帮助你习惯`Boost.Math`。浏览[http://boost.org/libs/math](http://boost.org/libs/math)阅读相关内容。\n\n# 编写测试用例\n\n这个食谱和下一个食谱致力于使用`Boost.Test`库进行自动测试，许多 Boost 库都使用这个库。让我们动手为自己的班级写一些测试:\n\n```cpp\n#include <stdexcept> \nstruct foo { \n    int val_; \n\n    operator int() const; \n    bool is_not_null() const; \n    void throws() const; // throws(std::logic_error) \n}; \n```\n\n# 准备好\n\n这个食谱需要 C++ 的基础知识。要编译这个配方的代码，定义`BOOST_TEST_DYN_LINK`宏并链接到`boost_unit_test_framework`和`boost_system`库。\n\n# 怎么做...\n\n老实说，Boost 中不止有一个测试库。我们来看看最实用的。\n\n1.  要使用它，我们需要定义宏并包含以下标题:\n\n```cpp\n#define BOOST_TEST_MODULE test_module_name \n#include <boost/test/unit_test.hpp> \n```\n\n2.  每组测试都必须写在测试用例中:\n\n```cpp\nBOOST_AUTO_TEST_CASE(test_no_1) { \n```\n\n3.  检查`true`结果的某些功能必须按如下方式进行:\n\n```cpp\n    foo f1 = {1}, f2 = {2}; \n    BOOST_CHECK(f1.is_not_null());\n```\n\n4.  检查非质量必须以下列方式进行:\n\n```cpp\n    BOOST_CHECK_NE(f1, f2); \n```\n\n5.  检查抛出的异常必须如下所示:\n\n```cpp\n    BOOST_CHECK_THROW(f1.throws(), std::logic_error); \n} // BOOST_AUTO_TEST_CASE(test_no_1) \n```\n\n就这样！在编译和链接之后，我们将有一个自动测试`foo`并以人类可读的格式输出测试结果的二进制文件。\n\n# 它是如何工作的...\n\n编写单元测试很容易。你知道函数是如何工作的，在特定情况下会产生什么结果。因此，您只需检查预期结果是否与函数的实际输出相同。这就是我们在*步骤 3* 中所做的。我们知道`f1.is_not_null()`返回`true`，我们已经检查过了。在*第 4 步*我们确实知道`f1`不等于`f2`，所以我们也检查了一下。对`f1.throws()`的调用产生`std::logic_error`异常，我们检查预期类型的异常是否被抛出。\n\n在*第 2 步*，我们正在做一个测试用例——一组验证`foo`结构正确行为的检查。我们可能在一个源文件中有多个测试用例。例如，如果我们添加以下代码:\n\n```cpp\nBOOST_AUTO_TEST_CASE(test_no_2) { \n    foo f1 = {1}, f2 = {2}; \n    BOOST_REQUIRE_NE(f1, f2); \n    // ... \n} // BOOST_AUTO_TEST_CASE(test_no_2) \n```\n\n这段代码将与`test_no_1`测试用例一起运行。\n\n传递给`BOOST_AUTO_TEST_CASE`宏的参数只是测试用例的唯一名称，在出现错误时会显示出来。\n\n```cpp\nRunning 2 test cases... \nmain.cpp(15): error in \"test_no_1\": check f1.is_not_null() failed \nmain.cpp(17): error in \"test_no_1\": check f1 != f2 failed [0 == 0] \nmain.cpp(19): error in \"test_no_1\": exception std::logic_error is expected \nmain.cpp(24): fatal error in \"test_no_2\": critical check f1 != f2 failed [0 == 0] \n\n*** 4 failures detected in test suite \"test_module_name\" \n```\n\n`BOOST_REQUIRE_*`和`BOOST_CHECK_*`宏有一点区别。如果`BOOST_REQUIRE_*`宏检查失败，当前测试用例的执行停止，`Boost.Test`运行下一个测试用例。然而，失败`BOOST_CHECK_*`并不会停止当前测试用例的执行。\n\n*第 1 步*需要额外的注意。注意`BOOST_TEST_MODULE`宏定义。该宏必须在包含`Boost.Test`头之前定义；否则，链接程序将失败。更多信息可在本食谱的*部分找到。*\n\n# 还有更多...\n\n有些读者可能会疑惑，为什么我们在*第四步*写`BOOST_CHECK_NE(f1, f2)`而不是`BOOST_CHECK(f1 != f2)`？答案很简单:*第 4 步*中的宏提供了一个更易读和详细的`Boost.Test`库旧版本的输出。\n\nC++ 17 缺乏对单元测试的支持。但是`Boost.Test`库可以用来测试 C++ 17 和 C++ 11 之前的代码。\n\n记住，测试越多，得到的代码就越可靠！\n\n# 请参见\n\n*   在一个测试模块中组合多个测试用例的*配方包含更多关于测试和`BOOST_TEST_MODULE`宏的信息。*\n**   有关测试宏的完整列表和`Boost.Test`高级功能的信息，请参考位于[http://boost.org/libs/test](http://boost.org/libs/test)的 Boost 官方文档*\n\n *# 在一个测试模块中组合多个测试用例\n\n编写自动测试对你的项目有好处。然而，当项目很大并且许多开发人员都在做的时候，管理测试用例是很困难的。在这个食谱中，我们将看看如何运行单个测试，以及如何在单个模块中组合多个测试用例。\n\n让我们假设两个开发人员正在测试在`foo.hpp`头中声明的`foo`结构，我们希望给他们单独的源文件来编写测试。在这种情况下，两个开发人员不会互相打扰，可能会并行工作。但是，默认的测试运行必须执行两个开发人员的测试。\n\n# 准备好\n\n这个食谱需要 C++ 的基础知识。该配方部分重用了之前配方中的代码，并且还需要定义`BOOST_TEST_DYN_LINK`宏并与`boost_unit_test_framework`和`boost_system`库链接。\n\n# 怎么做...\n\n这个配方使用了前一个配方的代码。这是测试大项目非常有用的方法。不要低估它。\n\n1.  在上一个配方的`main.cpp`中的所有标题中，只留下这两行:\n\n```cpp\n#define BOOST_TEST_MODULE test_module_name \n#include <boost/test/unit_test.hpp> \n```\n\n2.  让我们把前面例子中的测试用例移到两个不同的源文件中:\n\n```cpp\n// developer1.cpp \n#include <boost/test/unit_test.hpp> \n#include \"foo.hpp\" \nBOOST_AUTO_TEST_CASE(test_no_1) { \n    // ... \n} \n// developer2.cpp \n#include <boost/test/unit_test.hpp> \n#include \"foo.hpp\" \nBOOST_AUTO_TEST_CASE(test_no_2) { \n    // ... \n} \n```\n\n就这样！因此，编译和链接所有的源代码和两个测试用例将在程序执行中起作用。\n\n# 它是如何工作的...\n\n所有的魔法都是由`BOOST_TEST_MODULE`宏完成的。如果是在`<boost/test/unit_test.hpp>`之前定义的，`Boost.Test`认为这个源文件是主要的，所有的辅助测试基础设施都必须放在里面。否则，从`<boost/test/unit_test.hpp>`只包含测试宏。\n\n如果您将所有`BOOST_AUTO_TEST_CASE`测试与包含`BOOST_TEST_MODULE`宏的源文件相链接，它们将会运行。当处理一个大项目时，每个开发人员可能只允许编译和链接他们自己的源代码。这使其独立于其他开发人员，并提高了开发速度——无需在调试时编译外来源代码和运行外来测试。\n\n# 还有更多...\n\n`Boost.Test`库很好，因为它有选择性地运行测试的能力。我们可以选择运行什么测试，并将它们作为命令行参数传递。例如，以下命令仅运行`test_no_1`测试用例:\n\n```cpp\n    ./testing_advanced -run=test_no_1\n```\n\n以下命令运行两个测试用例:\n\n```cpp\n    ./testing_advanced -run=test_no_1,test_no_2\n```\n\n可惜 C++ 17 标准没有内置测试支持，看起来 C++ 20 也不会采用`Boost.Test`的类和方法。\n\n# 请参见\n\n*   *编写测试用例*方法包含更多关于`Boost.Test`库的信息。有关`Boost.Test`的更多信息，请阅读[http://boost.org/libs/test](http://boost.org/libs/test)的 Boost 官方文档。\n*   勇敢的人可能会试着看看 Boost 库中的一些测试用例。那些测试用例被分配在位于`boost`文件夹中的`libs`子文件夹中。例如，`Boost.LexicalCast`测试用例在`boost_1_XX_0/libs/lexical_cast/test`分配。\n\n# 操纵图像\n\n我给你留下了一些真正美味的甜点——Boost 的通用图像库或只是`Boost.GIL`，它允许你在不太担心图像格式的情况下操纵图像。\n\n让我们用它做一些简单有趣的事情。例如，让我们制作一个否定任何图片的程序。\n\n# 准备好\n\n这个食谱需要 C++、模板和`Boost.Variant`的基础知识。该示例需要链接到`png`库。\n\n# 怎么做...\n\n为了简单起见，我们将只处理 PNG 图像。\n\n1.  让我们从包含头文件开始:\n\n```cpp\n#include <boost/gil/gil_all.hpp> \n#include <boost/gil/extension/io/png_dynamic_io.hpp> \n#include <string> \n```\n\n2.  现在，我们需要定义希望使用的图像类型:\n\n```cpp\nint main(nt argc, char *argv[]) {\n    typedef boost::mpl::vector<\n            boost::gil::gray8_image_t,\n            boost::gil::gray16_image_t,\n            boost::gil::rgb8_image_t\n    > img_types;\n```\n\n3.  打开现有的 PNG 图像可以这样实现:\n\n```cpp\n    std::string file_name(argv[1]); \n    boost::gil::any_image<img_types> source; \n    boost::gil::png_read_image(file_name, source);\n```\n\n4.  我们需要对图片进行如下操作:\n\n```cpp\n    boost::gil::apply_operation( \n        view(source), \n        negate() \n    ); \n```\n\n5.  以下代码行将帮助您编写图像:\n\n```cpp\n    boost::gil::png_write_view(\"negate_\" + file_name, const_view(source)); \n```\n\n6.  让我们看一下修改操作:\n\n```cpp\nstruct negate { \n    typedef void result_type; // required \n\n    template <class View> \n    void operator()(const View& source) const { \n        // ... \n    } \n}; // negate \n```\n\n7.  `operator()`的主体包括获取通道类型:\n\n```cpp\ntypedef typename View::value_type value_type; \ntypedef typename boost::gil::channel_type<value_type>::type channel_t; \n```\n\n8.  它还遍历像素:\n\n```cpp\nconst std::size_t channels = boost::gil::num_channels<View>::value; \nconst channel_t max_val = (std::numeric_limits<channel_t>::max)(); \n\nfor (unsigned int y = 0; y < source.height(); ++ y) { \n    for (unsigned int x = 0; x < source.width(); ++ x) { \n        for (unsigned int c = 0; c < channels; ++ c) { \n            source(x, y)[c] = max_val - source(x, y)[c]; \n        } \n    } \n} \n```\n\n现在让我们看看我们计划的结果:\n\n![](img/00022.gif)\n\n上图是下图的底片:\n\n![](img/00023.gif)\n\n# 它是如何工作的...\n\n在*步骤 2* 中，我们描述了我们希望处理的图像类型。这些图像是每像素 8 位和 16 位的灰度图像，以及每像素 8 位的 RGB 图像。\n\n`boost::gil::any_image<img_types>`类是一种`Boost.Variant`，它可能持有一个`img_types`变量的图像。您可能已经猜到了，`boost::gil::png_read_image`将图像读入图像变量。\n\n第 4 步中的`boost::gil::apply_operation`功能几乎等同于`Boost.Variant`库中的`boost::apply_visitor`。注意`view(source)`的用法。`boost::gil::view`函数在图像周围构建一个光包装，将图像解释为二维像素阵列。\n\n你还记得在`Boost.Variant`的时候，我们从`boost::static_visitor`吸引游客吗？当我们使用 GIL 版本的变体时，我们需要在`visitor`内部制作一个`result_type` typedef。你可以在*第六步*看到。\n\n一点理论:图像由称为**像素**的点组成。一幅图像有相同类型的像素。然而，对于单个通道，不同图像的像素在通道数和颜色位上可能不同。通道代表原色。在 RGB 图像的情况下，我们有一个由三个通道组成的像素——红色、绿色和蓝色。在灰色图像的情况下，我们有一个代表灰色的单一通道。\n\n回到我们的形象。在*步骤 2* 中，我们描述了我们希望处理的图像类型。在*步骤 3* 中，从文件中读取这些图像类型之一，并将其存储在源变量中。在*步骤 4* 中，`negate`访问者的`operator()`方法被实例化为所有图像类型。\n\n在*步骤 7* 中，我们可以看到如何从图像视图中获取通道类型。\n\n在*第 8 步*，我们迭代像素和通道并否定它们。通过`max_val - source(x, y)[c]`进行否定，结果写回图像视图。\n\n我们在第 5 步写回图像。\n\n# 还有更多...\n\nC++ 17 没有处理图像的内置方法。将 2D 绘图添加到 C++ 标准库中的工作正在进行中，尽管这是一种正交功能。\n\n`Boost.GIL`库快速高效。编译器很好地优化了它的代码，我们甚至可以使用一些`Boost.GIL`方法来帮助优化器展开循环。但是这一章只讨论了一些图书馆的基础知识，所以是时候停下来了。\n\n# 请参见\n\n*   更多关于`Boost.GIL`的信息可以在[http://boost.org/libs/gil](http://boost.org/libs/gil)的 Boost 官方文档中找到\n*   有关`Boost.Variant`库的更多信息，请参见[第 1 章](01.html#RL0A0-712b4ba1126a4c7c89e1d44de61b4bdd)、*开始编写应用*中的*在变量/容器中存储多个选择的类型*配方\n*   更多关于 C++ 的消息，请参见[https://isocpp.org/](https://isocpp.org/)\n*   看看[https://stdcpp.ru/](https://stdcpp.ru/)讨论关于俄语的 C++ 提案**"
  },
  {
    "path": "docs/boost-cpp-app-dev-cb/README.md",
    "content": "# Boost C++ 应用开发秘籍\n\n> 原书：[Boost C++ Application Development Cookbook](https://libgen.rs/book/index.php?md5=7D962636A0EEAFE24E8F810E007FCE09)\n> \n> 协议：[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)\n> \n> 阶段：机翻（1）\n> \n> 自豪地采用[谷歌翻译](https://translate.google.cn/)\n> \n> 让开源界提前感受入关。\n\n* [在线阅读](https://ccpp.apachecn.org)\n* [在线阅读（Gitee）](https://apachecn.gitee.io/apachecn-c-cpp-zh/)\n* [ApacheCN 学习资源](http://docs.apachecn.org/)\n\n## 贡献指南\n\n本项目需要校对，欢迎大家提交 Pull Request。\n\n> 请您勇敢地去翻译和改进翻译。虽然我们追求卓越，但我们并不要求您做到十全十美，因此请不要担心因为翻译上犯错——在大部分情况下，我们的服务器已经记录所有的翻译，因此您不必担心会因为您的失误遭到无法挽回的破坏。（改编自维基百科）\n\n## 联系方式\n\n### 负责人\n\n* [飞龙](https://github.com/wizardforcel): 562826179\n\n### 其他\n\n*   在我们的 [apachecn/apachecn-c-cpp-zh](https://github.com/apachecn/apachecn-c-cpp-zh) github 上提 issue.\n*   发邮件到 Email: `apachecn@163.com`.\n*   在我们的 [组织学习交流群](http://www.apachecn.org/organization/348.html) 中联系群主/管理员即可.\n\n## 赞助我们\n\n![](http://data.apachecn.org/img/about/donate.jpg)\n"
  },
  {
    "path": "docs/boost-cpp-app-dev-cb/SUMMARY.md",
    "content": "+   [Boost C++ 应用开发秘籍](README.md)\n+   [零、前言](00.md)\n+   [一、开始编写应用](01.md)\n+   [二、管理资源](02.md)\n+   [三、类型转换](03.md)\n+   [四、编译时技巧](04.md)\n+   [五、多线程操作](05.md)\n+   [六、操作任务](06.md)\n+   [七、操纵字符串](07.md)\n+   [八、元编程](08.md)\n+   [九、容器](09.md)\n+   [十、收集平台和编译器信息](10.md)\n+   [十一、使用系统](11.md)\n+   [十二、Boost 的冰山一角](12.md)\n"
  },
  {
    "path": "docs/cpp-dsal-design-principle/00.md",
    "content": "# 零、前言\n\n## 大约\n\n本节简要介绍作者、本书的内容、入门所需的技术技能，以及完成所有附带活动和练习所需的硬件和软件要求。\n\n## 关于书\n\nC++ 是一种成熟的多范例编程语言，使您能够编写对硬件具有高度控制的高级代码。如今，软件基础设施的重要部分，包括数据库、浏览器、多媒体框架和图用户界面工具包，都是用 C++ 编写的。\n\n这本书首先介绍了 C++ 数据结构，以及如何使用链表、数组、栈和队列来存储数据。在后面的章节中，这本书解释了基本的算法设计范式，例如贪婪方法和分治方法，它们被用来解决各种各样的计算问题。最后，您将学习动态编程的高级技术，以开发书中讨论的几种算法的优化实现。\n\n到本书结束时，您将学会如何用高效且可扩展的 C++ 14 代码实现标准数据结构和算法。\n\n### 关于作者\n\n**约翰·凯里**\n\n作为一名作曲家和钢琴家，约翰·凯里的正规教育几乎完全基于音乐领域。在他的艺术努力中广泛使用了计算机和其他形式的技术后，他在编程和数学领域投入了多年的自学，现在是一名专业的软件工程师。他认为，他不寻常的背景为他提供了一个独特的、相对非学术的软件开发视角。他目前在 Hydratec Industries 公司工作，该公司主要为消防喷淋系统设计人员开发计算机辅助设计软件，用于对提议的设计进行水力计算，以确定其有效性和合法性。\n\n**Shreyans Doshi**\n\nShreyans 毕业于艾哈迈达巴德的 Nirma 大学，获得计算机工程技术学士学位。毕业后，他加入金融行业，使用尖端的 C++ 应用开发超低延迟交易系统。在过去的三年里，他一直在用 C++ 设计交易基础设施。\n\n**板栗**\n\n帕亚斯毕业于 NIT 阿拉哈巴德大学，获得计算机科学技术学士学位。后来，他加入了三星印度研究公司，在那里他帮助开发了蒂森设备的多媒体框架。目前在加州大学河滨分校攻读地理空间数据库和路线规划算法博士学位期间，他担任教学和研究助理，十年来一直在使用 C++ 创建应用。\n\n### 学习目标\n\n本书结束时，您将能够:\n\n*   使用哈希表、字典和集合构建应用\n*   使用布隆过滤器实现网址缩短服务\n*   对字符串数据类型应用常用算法，如 heapsort 和 merge-sort\n*   使用 C++ 模板元编程来编写代码库\n*   探索现代硬件如何影响程序的实际运行时性能\n*   使用适当的现代 C++ 习惯用法，如`std::array`，而不是 C 风格的数组\n\n### 观众\n\n这本书是为那些想重温基本数据结构和算法设计技术的开发人员或学生准备的。虽然不需要数学背景，但复杂性类和大 O 符号的一些基本知识，以及算法课程的资格，将帮助你从这本书中获得最大收益。假设熟悉 C++ 14 标准。\n\n### 进场\n\n这本书使用了一种实用的实践方法来解释各种概念。通过练习，这本书表明，理论上表现相似的不同数据结构在现代计算机上的表现却大不相同。这本书没有深入研究任何理论分析，而是专注于基准测试和实际结果。\n\n### 硬件要求\n\n为了获得最佳的学生体验，我们推荐以下硬件配置:\n\n*   任何带有 Windows、Linux 或 macOS 的入门级 PC/Mac 都足够了\n*   处理器:英特尔酷睿 2 双核、阿思龙 X2 或更好\n*   内存:4 GB 内存\n*   存储:10 GB 可用空间\n\n### 软件需求\n\n您还需要提前安装以下软件:\n\n*   操作系统:Windows 7 SP1 32/64 位，Windows 8.1 32/64 位，或 Windows 10 32/64 位，Ubuntu 14.04 或更高版本，或 macOS Sierra 或更高版本\n*   浏览器:谷歌 Chrome 还是 Mozilla 火狐\n*   任何支持 C++ 14 标准的现代编译器和 IDE(可选)。\n\n### 安装和设置\n\n在开始阅读本书之前，请安装本书中使用的以下库。您可以在这里找到安装这些的步骤:\n\n**安装 Boost 库:**\n\n书中的一些练习和活动需要 Boost C++ 库。您可以在以下链接上找到库和安装说明:\n\nwindows:[https://www . boost . org/doc/libs/1 _ 71 _ 0/more/入门/windows.html](https://www.boost.org/doc/libs/1_71_0/more/getting_started/windows.html)\n\nLinux/macOS:[https://www . boost . org/doc/libs/1 _ 71 _ 0/more/入门/unix-variants.html](https://www.boost.org/doc/libs/1_71_0/more/getting_started/unix-variants.html)\n\n### 安装代码包\n\n将该类的代码包复制到`C:/Code`文件夹。\n\n### 附加资源\n\n本书的代码包也托管在 GitHub 上，网址为[https://GitHub . com/trainingypbackt/CPP-数据-结构-算法-设计-原理](https://github.com/TrainingByPackt/CPP-Data-Structures-and-Algorithm-Design-Principles)。\n\n我们还有来自 https://github.com/PacktPublishing/丰富的书籍和视频目录的其他代码包。看看他们！"
  },
  {
    "path": "docs/cpp-dsal-design-principle/01.md",
    "content": "# 一、列表、栈和队列\n\n## 学习目标\n\n本章结束时，您将能够:\n\n*   描述在任何应用中使用正确数据结构的重要性\n*   根据问题，实现各种内置数据结构，使应用开发更加容易\n*   如果 C++ 提供的数据结构对于用例来说不够好，那么实现适合给定情况的定制线性数据结构\n*   分析实际问题，其中不同类型的线性数据结构是有帮助的，并决定哪一种最适合给定的用例\n\n本章描述了在任何应用中使用正确数据结构的重要性。我们将学习如何在 C++ 中使用一些最常见的数据结构，以及使用这些结构的内置和定制容器。\n\n## 简介\n\n在设计任何应用时，数据管理都是需要牢记的最重要的考虑因素之一。任何应用的目的都是获取一些数据作为输入，对其进行处理或操作，然后提供合适的数据作为输出。例如，让我们考虑一个医院管理系统。在这里，我们可以有关于不同医生、病人和档案记录等的数据。医院管理系统应该允许我们执行各种操作，如接收病人，更新不同专业医生的加入和离开。虽然面向用户的界面将以与医院管理员相关的格式呈现信息，但在内部，系统将管理不同的记录和项目列表。\n\n一个程序员可以使用几种结构来保存内存中的任何数据。为保存数据选择正确的结构，也称为**数据结构**，对于确保可靠性、性能和实现应用中所需的功能至关重要。除了正确的数据结构，正确选择算法来访问和操作数据对于应用的最佳行为也是必要的。这本书将使你有能力为你的应用设计实现正确的数据结构和算法，以使你能够开发良好优化和可扩展的应用。\n\n本章介绍 C++ 中提供的基本和常用的线性数据结构。我们将看看他们各自的设计、优缺点。我们还将在练习的帮助下实现所述结构。了解这些数据结构将有助于您以更高性能、标准化、可读和可维护的方式管理任何应用中的数据。\n\n线性数据结构可以大致分为连续或链接结构。让我们理解两者之间的区别。\n\n## 连续与链接数据结构\n\n在任何应用中处理数据之前，我们必须决定如何存储数据。这个问题的答案取决于我们想要对数据执行什么样的操作以及操作的频率。我们应该选择在延迟、内存或任何其他参数方面为我们提供最佳性能的实现，而不影响应用的正确性。\n\n确定要使用的数据结构类型的一个有用指标是算法复杂度，也称为**时间复杂度**。时间复杂度表示执行某项操作所需的相对时间，与数据大小成比例。因此，时间复杂度显示了如果我们改变数据集的大小，时间将如何变化。对任何数据类型进行不同操作的时间复杂度取决于数据在其中的存储方式。\n\n数据结构可以分为两种类型:连续数据结构和链接数据结构。在接下来的章节中，我们将仔细研究这两者。\n\n### 连续数据结构\n\n如前所述，**连续数据结构**将所有元素存储在单个内存块中。下图显示了数据如何存储在连续的数据结构中:\n\n![](img/C14498_01_01.jpg)\n\n###### 图 1.1:连续数据结构的图示\n\n在上图中，将较大的矩形视为存储所有元素的单个内存块，而较小的矩形表示为每个元素分配的内存。这里需要注意的一点是，所有的元素都是同一类型的。因此，它们都需要相同的内存量，如`sizeof(type)`所示。第一个元素的地址也被称为**基址** ( **BA** )。因为它们都是同一类型，所以下一个元素出现在`BA + sizeof(type)`位置，然后一个出现在`BA + 2 * sizeof(type)`位置，以此类推。因此，要访问索引`i`中的任何元素，我们可以使用通用公式:`BA + i * sizeof(type)`。\n\n在这种情况下，我们总是可以使用公式立即访问任何元素，而不管数组的大小如何。因此，访问时间总是恒定的。这在大 O 符号中由 *O(1)* 表示。\n\n数组的两种主要类型是静态和动态的。静态数组只在其声明块中有生存期，但是动态数组提供了更好的灵活性，因为程序员可以确定何时应该分配它，何时应该取消分配它。我们可以根据需要选择其中的任何一种。两者对于不同的操作具有相同的性能。由于这个数组是在 C 语言中引入的，所以它也被称为 C 风格的数组。以下是这些数组的声明方式:\n\n*   静态数组被声明为`int arr[size];`。\n*   C 中的一个动态数组被声明为`int* arr = (int*)malloc(size * sizeof(int));`。\n*   一个动态数组在 C++ 中被声明为`int* arr = new int[size];`。\n\n静态数组是聚合的，这意味着它是在栈上分配的，因此当流退出函数时会被解除分配。另一方面，一个动态数组被分配在一个堆上，并保持在那里，直到手动释放内存。\n\n由于所有元素都是相邻的，因此当访问其中一个元素时，它旁边的一些元素也会被带入缓存。因此，如果您想访问这些元素，这是一个非常快速的操作，因为数据已经存在于缓存中。此属性也称为缓存局部性。虽然它不影响任何操作的渐近时间复杂度，但在遍历数组时，它可以在实践中为连续数据提供令人印象深刻的优势。由于遍历需要按顺序遍历所有元素，因此在获取第一个元素后，可以直接从缓存中检索接下来的几个元素。因此，该阵列被称为具有良好的缓存局部性。\n\n### L 链接的数据结构\n\n链接数据结构将数据保存在多个内存块(也称为节点)中，这些内存块可能位于内存中的不同位置。下图显示了数据如何存储在链接数据结构中:\n\n![](img/C14498_01_02.jpg)\n\n###### 图 1.2:链接的数据结构\n\n在链表的基本结构中，每个节点包含要存储在该节点中的数据和指向下一个节点的指针。最后一个节点包含一个`NULL`指针来指示列表的结束。要到达任何元素，我们必须从链表的开头，也就是头开始，然后跟随下一个指针，直到到达预期的元素。因此，要到达索引`i`处的元素，我们需要遍历链表并迭代`i`次。因此，我们可以说访问元素的复杂度是*O(n)*；也就是说，时间随着节点的数量成比例地变化。\n\n如果我们想要插入或删除任何元素，并且如果我们有一个指向该元素的指针，那么与数组相比，对于链表来说，这个操作非常小而且非常快。让我们来看看在链表中插入一个元素是如何工作的。下图说明了在链表的两个元素之间插入一个元素的情况:\n\n![](img/C14498_01_03.jpg)\n\n###### 图 1.3:将元素插入链表\n\n对于插入，一旦我们构造了要插入的新节点，我们只需要重新排列链接，使得前面元素的下一个指针 *(i = 1)* 指向新元素 *(i = 2)* 而不是其当前元素 *(i = 3)* ，新元素的下一个指针 *(i = 2)* 指向当前元素的下一个元素 *(i = 3)* 。这样，新节点就成为链表的一部分。\n\n类似地，如果我们想删除任何元素，我们只需要重新排列链接，以便要删除的元素不再与任何列表元素相连。然后，我们可以解除分配该元素，或者对其采取任何其他适当的操作。\n\n链表根本不能提供缓存局部性，因为元素不是连续存储在内存中的。因此，如果不实际访问存储在当前元素中的指针，就没有办法将下一个元素带入缓存。因此，尽管理论上，它的遍历时间复杂度与数组相同，但实际上，它的性能很差。\n\n下一节提供了连续数据结构和链接数据结构的比较摘要。\n\n### Com 型坯\n\n下表概括总结了链接数据结构和连续数据结构之间的重要区别:\n\n![](img/C14498_01_04.jpg)\n\n###### 图 1.4:比较连续和链接数据结构的表格\n\n下表包含有关各种参数的数组和链表的性能摘要:\n\n![](img/C14498_01_05.jpg)\n\n###### 图 1.5:显示数组和链表的一些操作的时间复杂性的表格\n\n对于任何应用，我们可以根据不同操作的要求和频率，选择数据结构或两者的组合。\n\n数组和链表非常常见，广泛用于任何存储数据的应用。因此，这些数据结构的实现必须尽可能没有错误和高效。为了避免重新发明代码，C++ 提供了各种各样的结构，比如`std::array`、`std::vector`和`std::list`。我们将在接下来的章节中更详细地看到其中的一些。\n\n### C 型数组的极限\n\n虽然 C 风格的数组可以完成这项工作，但它们并不常用。有许多限制表明需要更好的解决方案。其中一些主要限制如下:\n\n*   内存分配和释放必须手动处理。解除分配失败会导致内存泄漏，即内存地址变得不可访问。\n*   `operator[]`函数不检查参数是否大于数组的大小。如果使用不当，这可能会导致分段错误或内存损坏。\n*   嵌套数组的语法变得非常复杂，导致代码不可读。\n*   深度复印不是默认功能。它必须手动实现。\n\n为了避免这些问题，C++ 在名为`std::array`的 C 风格数组上提供了一个非常薄的包装器。\n\n## 标准::数组\n\n`std::array`自动分配和解除分配内存。`std::array`是一个模板化的类，它接受两个参数——元素的类型和数组的大小。\n\n在下面的例子中，我们将声明大小为`10`的`int`的`std::array`，设置任何元素的值，然后打印该值以确保其有效:\n\n```cpp\nstd::array<int, 10> arr;        // array of int of size 10\narr[0] = 1;                    // Sets the first element as 1\nstd::cout << \"First element: \" << arr[0] << std::endl;\nstd::array<int, 4> arr2 = {1, 2, 3, 4};\nstd::cout << \"Elements in second array: \";\n  for(int i = 0; i < arr.size(); i++)\n    std::cout << arr2[i] << \" \";\n```\n\n该示例将产生以下输出:\n\n```cpp\nFirst element: 1\nElements in second array: 1 2 3 4 \n```\n\n我们可以看到，`std::array`提供了`operator[]`，和 C 风格的数组一样，避免了检查索引是否小于数组大小的开销。此外，它还提供了一个名为`at(index)`的函数，如果参数无效，它会抛出一个异常。这样，我们可以以适当的方式处理异常。因此，如果我们有一段代码，其中我们将访问具有一点不确定性的元素，例如依赖于用户输入的数组索引，我们总是可以使用异常处理来捕获错误，如下例所示。\n\n```cpp\ntry\n{\n    std::cout << arr.at(4);    // No error\n    std::cout << arr.at(5);    // Throws exception std::out_of_range\n}\ncatch (const std::out_of_range& ex)\n{\n    std::cerr << ex.what();\n}\n```\n\n除此之外，将`std::array`传递给另一个函数类似于传递任何内置数据类型。我们可以通过值或引用传递它，有或没有`const`。此外，该语法不涉及任何指针相关操作或引用和去引用操作。因此，与 C 风格的数组相比，可读性要好得多，即使对于多维数组也是如此。下面的示例演示如何按值传递数组:\n\n```cpp\nvoid print(std::array<int, 5> arr)\n{\n    for(auto ele: arr)\n    {\n        std::cout << ele << \", \";\n    }\n}\nstd::array<int, 5> arr = {1, 2, 3, 4, 5};\nprint(arr);\n```\n\n该示例将产生以下输出:\n\n```cpp\n1, 2, 3, 4, 5\n```\n\n我们不能为这个函数传递任何其他大小的数组，因为数组的大小是函数参数的数据类型的一部分。所以，比如我们通过`std::array<int, 10>`，编译器会返回一个错误，说它不能匹配函数参数，也不能从一个转换到另一个。然而，如果我们想有一个通用函数，可以使用任何大小的`std::array`，我们可以为该函数模板化数组的大小，它将为数组的所有所需大小生成代码。因此，签名将如下所示:\n\n```cpp\ntemplate <size_t N>\nvoid print(const std::array<int, N>& arr)\n```\n\n除了可读性，在传递`std::array`的同时，默认情况下会将所有元素复制到一个新数组中。因此，执行自动深度复制。如果我们不想要那个特性，我们可以一直使用其他类型，比如 reference 和`const` reference。因此，它为程序员提供了更大的灵活性。\n\n实际上，对于大多数操作来说，`std::array`提供了与 C 风格数组相似的性能，因为它只是一个薄薄的包装器，以减少程序员的工作量，并使代码更加安全。`std::array`提供两种不同的功能来访问数组元素–`operator[]`和`at()`。`operator[]`，类似于 C 风格的数组，不执行任何索引检查。但是，`at()`函数对索引进行检查，如果索引超出范围就会抛出异常。正因为如此，在实践中稍慢一些。\n\n如前所述，迭代数组是一种非常常见的操作。`std::array`在循环和迭代器范围的帮助下，提供了一个非常好的界面。因此，打印数组中所有元素的代码如下所示:\n\n```cpp\nstd::array<int, 5> arr = {1, 2, 3, 4, 5};\nfor(auto element: arr)\n{\n    std::cout << element << ' ';\n}\n```\n\n该示例将显示以下输出:\n\n```cpp\n1 2 3 4 5 \n```\n\n在前面的例子中，当我们演示打印出所有的元素时，我们使用了一个索引变量进行迭代，在这里我们必须确保根据数组的大小正确使用它。因此，与此示例相比，它更容易出现人为错误。\n\n我们能够使用基于范围的循环迭代`std::array`的原因是由于迭代器。`std::array`有名为`begin()`和`end()`的成员函数，返回一种访问第一个和最后一个元素的方式。为了从一个元素移动到下一个元素，它还提供了算术运算符，如增量运算符(`++ `)和加法运算符(`+`)。因此，基于范围的`for`循环从`begin()`开始，在`end()`结束，使用增量操作符(`++ `)一步步前进。迭代器为所有可动态迭代的 STL 容器提供了统一的接口，例如`std::array`、`std::vector`、`std::map`、`std::set`和`std::list`。\n\n除了迭代器，我们需要在容器中指定位置的所有函数都是基于迭代器的；例如，在特定位置插入、删除范围内或特定位置的元素以及其他类似的功能。这使得代码更加可重用、可维护和可读。\n\n#### 注意\n\n对于 C++ 中所有借助迭代器指定范围的函数来说，`start()`迭代器通常是包含的，`end()`迭代器通常是排他的，除非另有说明。\n\n因此，`array::begin()`函数返回一个指向第一个元素的迭代器，但是`array::end()`在最后一个元素之后返回一个迭代器。因此，基于范围的循环可以写成如下形式:\n\n```cpp\nfor(auto it = arr.begin(); it != arr.end(); it++)\n{\n    auto element = (*it);\n    std::cout << element << ' ';\n}\n```\n\n还有一些其他形式的迭代器，比如`const_iterator`和`reverse_iterator`，也是相当有用的。`const_iterator`是普通迭代器的`const`版本。如果数组被声明为`const`，其与迭代器相关的函数，如`begin()`和`end()`，返回`const_iterator`。\n\n`reverse_iterator`允许我们反向遍历数组。所以，它的函数，比如增量运算符(`++ `)和`advance`，是普通迭代器的逆运算。\n\n除了`operator[]`和`at()`功能，`std::array`还提供了其他访问器，如下表所示:\n\n![](img/C14498_01_06.jpg)\n\n###### 图 1.6:显示 std::数组的一些访问器的表格\n\n下面的代码片段演示了如何使用这些函数:\n\n```cpp\nstd::array<int, 5> arr = {1, 2, 3, 4, 5};\nstd::cout << arr.front() << std::endl;       // Prints 1\nstd::cout << arr.back() << std::endl;        // Prints 5\nstd::cout << *(arr.data() + 1) << std::endl; // Prints 2\n```\n\n`std::array`提供的另一个有用的功能是用于深度比较的关系运算符和用于深度复制的复制分配运算符。所有尺寸运算符(`<`、`>`、`<=`、`>=`、`==`、`!=`)都是为`std::array`定义的，以比较两个数组，前提是也为底层类型的`std::array`提供相同的运算符。\n\nc 风格的数组也支持所有的关系运算符，但是这些运算符实际上并不比较数组内部的元素；事实上，他们只是比较指针。因此，只是将元素的地址作为整数进行比较，而不是对数组进行深度比较。这也叫一个**浅比较**，实际用处不大。同样，赋值也不会创建已赋值数据的副本。相反，它只是生成一个指向相同数据的新指针。\n\n#### 注意\n\n关系运算符仅适用于相同大小的`std::array`。这是因为数组的大小是数据类型本身的一部分，它不允许比较两种不同数据类型的值。\n\n在下面的例子中，我们将看到如何包装一个 C 风格的数组，它的大小由用户定义。\n\n### 练习 1:实现动态大小的数组\n\n让我们写一个小应用来管理学校的学生记录。一个班级的学生人数和他们的详细情况将作为输入给出。编写一个类似数组的容器来管理数据，这样也可以支持动态调整大小。我们还将实现一些实用函数来合并不同的类。\n\n执行以下步骤完成练习:\n\n1.  首先，包含所需的标题:\n\n    ```cpp\n    #include <iostream>\n    #include <sstream>\n    #include <algorithm>\n    ```\n\n2.  现在，让我们编写一个名为`dynamic_array`的基本模板化结构，以及主数据成员:\n\n    ```cpp\n    template <typename T>\n    class dynamic_array\n    {\n        T* data;\n        size_t n;\n    ```\n\n3.  现在，让我们添加一个构造函数，它采用数组的大小并复制它:\n\n    ```cpp\n    public:\n    dynamic_array(int n)\n    {\n        this->n = n;\n        data = new T[n];\n    }\n        dynamic_array(const dynamic_array<T>& other)\n      {\n        n = other.n;\n        data = new T[n];\n        for(int i = 0; i < n; i++)\n        data[i] = other[i];\n      }\n    ```\n\n4.  现在，让我们在`public`访问器中添加`operator[]`和`function()`，以支持直接访问数据，类似于`std::array` :\n\n    ```cpp\n    T& operator[](int index)\n    {\n        return data[index];\n    }\n    const T& operator[](int index) const\n    {\n        return data[index];\n    }\n    T& at(int index)\n    {\n        if(index < n)\n        return data[index];\n        throw \"Index out of range\";\n    }\n    ```\n\n5.  现在，让我们添加一个名为`size()`的函数来返回数组的大小，以及一个析构函数来避免内存泄漏:\n\n    ```cpp\n    size_t size() const\n    {\n        return n;\n    }\n    ~dynamic_array()\n    {\n        delete[] data;   // A destructor to prevent memory leak\n    }\n    ```\n\n6.  现在，让我们添加迭代器函数来支持基于范围的循环来迭代`dynamic_array` :\n\n    ```cpp\n    T* begin()\n    {\n        return data;\n    }\n    const T* begin() const\n    {\n        return data;\n    }\n    T* end()\n    {\n        return data + n;\n    }\n    const T* end() const\n    {\n        return data + n;\n    }\n    ```\n\n7.  现在，让我们添加一个函数，使用`+`运算符将一个数组追加到另一个数组中。让我们保持它作为一个`friend`功能，以获得更好的可用性:\n\n    ```cpp\n    friend dynamic_array<T> operator+(const dynamic_array<T>& arr1, dynamic_array<T>& arr2)\n    {\n        dynamic_array<T> result(arr1.size() + arr2.size());\n        std::copy(arr1.begin(), arr1.end(), result.begin());\n        std::copy(arr2.begin(), arr2.end(), result.begin() + arr1.size());\n        return result;\n    }\n    ```\n\n8.  现在，让我们添加一个`to_string`函数，该函数以分隔符作为参数，默认值为“`,`”:\n\n    ```cpp\n    std::string to_string(const std::string& sep = \", \")\n    {\n      if(n == 0)\n        return \"\";\n      std::ostringstream os;\n      os << data[0];\n      for(int i = 1; i < n; i++)\n        os << sep << data[i];\n      return os.str();\n    }\n    };\n    ```\n\n9.  现在，让我们为学生添加一个`struct`。为了简单起见，我们只保留名字和标准(即学生学习的年级/班级)，并添加`operator<<`以正确打印:\n\n    ```cpp\n    struct student\n    {\n        std::string name;\n        int standard;\n    };\n    std::ostream& operator<<(std::ostream& os, const student& s)\n    {\n        return (os << \"[Name: \" << s.name << \", Standard: \" << s.standard << \"]\");\n    }\n    ```\n\n10.  现在，让我们添加一个`main`函数来使用这个数组:\n\n    ```cpp\n    int main()\n    {\n        int nStudents;\n        std::cout << \"Enter number of students in class 1: \";\n        std::cin >> nStudents;\n    dynamic_array<student> class1(nStudents);\n    for(int i = 0; i < nStudents; i++)\n    {\n        std::cout << \"Enter name and class of student \" << i + 1 << \": \";\n        std::string name;\n        int standard;\n        std::cin >> name >> standard;\n        class1[i] = student{name, standard};\n    }\n    // Now, let's try to access the student out of range in the array\n    try\n    {\n        class1[nStudents] = student{\"John\", 8};  // No exception, undefined behavior\n        std::cout << \"class1 student set out of range without exception\" << std::endl;\n        class1.at(nStudents) = student{\"John\", 8};  // Will throw exception\n    }\n    catch(...)\n    {\n    std::cout << \"Exception caught\" << std::endl;\n    }\n    auto class2 = class1;  // Deep copy\n        std::cout << \"Second class after initialized using first array: \" << class2.to_string() << std::endl;\n        auto class3 = class1 + class2;\n        // Combines both classes and creates a bigger one\n        std::cout << \"Combined class: \";\n        std::cout << class3.to_string() << std::endl;\n        return 0;\n    }\n    ```\n\n11.  Execute the preceding code with three students – `Raj(8)`, `Rahul(10)`, and `Viraj(6)` as input. The output looks like the following in the console:\n\n    ```cpp\n    Enter number of students in class 1 : 3\n    Enter name and class of student 1: Raj 8\n    Enter name and class of student 2: Rahul 10\n    Enter name and class of student 3: Viraj 6\n    class1 student set out of range without exception\n    Exception caught\n    Second class after initialized using first array : [Name: Raj, Standard: 8], [Name: Rahul, Standard: 10], [Name: Viraj, Standard: 6]\n    Combined class : [Name: Raj, Standard: 8], [Name: Rahul, Standard: 10], [Name: Viraj, Standard: 6], [Name: Raj, Standard: 8], [Name: Rahul, Standard: 10], [Name: Viraj, Standard: 6]\n    ```\n\n    这里提到的大部分功能都有类似于`std::array`的实现。\n\n既然我们已经看到了各种容器，我们将在下面的练习中学习如何实现一个可以接受任何类型的数据并以通用形式存储它的容器。\n\n### 练习 2:通用快速数据存储容器构建器\n\n在本练习中，我们将编写一个函数，该函数接受任意类型的任意数量的元素，这些元素又可以转换为一个公共类型。该函数还应该返回一个容器，该容器将所有元素转换为通用类型，并且遍历速度应该很快:\n\n1.  让我们从包含所需的库开始:\n\n    ```cpp\n    #include <iostream>\n    #include <array>\n    #include <type_traits>\n    ```\n\n2.  First, we'll try to build the signature of the function. Since the return type is a container that is fast to traverse, we'll go ahead with `std::array`. To allow any number of parameters, we'll use variadic templates:\n\n    ```cpp\n    template<typename ... Args>\n    std::array<?,?> build_array(Args&&... args)\n    ```\n\n    考虑到返回类型的容器应该快速遍历的要求，我们可以选择数组或向量。由于元素的数量是在编译时根据函数的参数数量已知的，我们可以继续`std::array`。\n\n3.  现在，我们必须为`std::array`提供元素的类型和元素的数量。我们可以使用`std::common_type`模板找出`std::array`内部的元素类型。由于这取决于参数，我们将提供函数的返回类型作为尾随类型:\n\n    ```cpp\n    template<typename ... Args>\n    auto build_array(Args&&... args) -> std::array<typename std::common_type<Args...>::type, ?>\n    {\n        using commonType = typename std::common_type<Args...>::type;\n        // Create array\n    }\n    ```\n\n4.  如前面的代码所示，我们现在需要弄清楚两件事——元素的数量，以及如何用`commonType` :\n\n    ```cpp\n    template< typename ... Args>\n    auto build_array(Args&&... args) -> std::array<typename std::common_type<Args...>::type, sizeof...(args)>\n    {\n        using commonType = typename std::common_type<Args...>::type;\n        return {std::forward<commonType>(args)...};\n    }\n    ```\n\n    创建数组\n5.  现在，让我们编写`main`函数，看看我们的函数是如何工作的:\n\n    ```cpp\n    int main()\n    {\n        auto data = build_array(1, 0u, 'a', 3.2f, false);\n        for(auto i: data)\n            std::cout << i << \" \";\n        std::cout << std::endl;\n    }\n    ```\n\n6.  Running the code should give the following output:\n\n    ```cpp\n    1 0 97 3.2 0\n    ```\n\n    正如我们所看到的，所有的最终输出都是以 float 的形式，因为一切都可以转换成 float。\n\n7.  To test this further, we can add the following inside the `main` function and test the output:\n\n    ```cpp\n    auto data2 = build_array(1, \"Packt\", 2.0);\n    ```\n\n    通过这种修改，我们应该会得到一个错误，说所有的类型都不能转换成一个公共类型。确切的错误消息应该提到模板推导失败。这是因为没有单一的类型可以同时转换字符串和数字。\n\n构建 r 函数，比如我们在本练习中创建的函数，可以在您不确定数据类型时使用，但您需要优化效率。\n\n有很多有用的特性和实用功能`std::array`没有提供。这样做的一个主要原因是，与 C 风格的阵列相比，保持类似或更好的性能和内存要求。\n\n为了更高级的特性和灵活性，C++ 提供了另一种称为`std::vector`的结构。我们将在下一节研究这是如何工作的。\n\n## 标准::矢量\n\n正如我们前面看到的，`std::array`是对 C 风格数组的一个很好的改进。但是`std::array`也有一些局限性，它在编写应用的时候缺少一些常用用例的功能。以下是`std::array`的一些主要缺点:\n\n*   `std::array`的大小必须是常量，在编译时提供，并且是固定的。所以，我们不能在运行时改变它。\n*   由于大小限制，我们无法在数组中插入或移除元素。\n*   `std::array`不能自定义分配。它总是使用栈内存。\n\n在大多数实际应用中，数据是动态的，而不是固定的大小。例如，在我们前面的医院管理系统的例子中，我们可以有更多的医生加入医院，我们可以有更多的紧急病人，等等。因此，提前知道数据的大小并不总是可能的。所以，`std::array`并不总是最好的选择，我们需要有动态大小的东西。\n\n现在，我们来看看`std::vector`是如何为这些问题提供解决方案的。\n\n### 标准::v 矢量–可变长度阵列\n\n正如标题所示，`std::vector`解决了数组最突出的问题之一——固定大小。`std::vector`初始化时不需要我们提供它的长度。\n\n以下是初始化向量的一些方法:\n\n```cpp\nstd::vector<int> vec;\n// Declares vector of size 0\nstd::vector<int> vec = {1, 2, 3, 4, 5};\n// Declares vector of size 5 with provided elements\nstd::vector<int> vec(10);\n// Declares vector of size 10\nstd::vector<int> vec(10, 5);\n// Declares vector of size 10 with each element's value = 5\n```\n\n正如我们从第一次初始化中看到的，提供大小不是强制性的。如果我们没有显式地指定大小，并且如果我们没有通过指定它的元素来推断它，则根据编译器的实现，用元素的容量来初始化向量。术语“大小”是指向量中实际存在的元素的数量，这可能与其容量不同。因此，对于第一次初始化，大小将为零，但容量可能是一些小数字或零。\n\n我们可以使用`push_back`或`insert`函数在向量中插入元素。`push_back`将在末尾插入元素。`insert`将迭代器作为位置的第一个参数，可以用来在任意位置插入元素。`push_back`是一个非常常用的向量函数，因为它的性能。`push_back`算法的伪代码如下:\n\n```cpp\npush_back(val):\n    if size < capacity\n    // If vector has enough space to accommodate this element\n    - Set element after the current last element = val\n    - Increment size\n    - return; \n    if vector is already full\n    - Allocate memory of size 2*size\n    - Copy/Move elements to newly allocated memory\n    - Make original data point to new memory\n    - Insert the element at the end\n```\n\n实际的实现可能有点不同，但是逻辑保持不变。我们可以看到，如果有足够的空间，只需要 *O(1)* 时间就可以在后面插入东西。然而，如果没有足够的空间，它将不得不复制/移动所有元素，这将花费 *O(n)* 时间。每次容量用完时，大多数实现都会将向量的大小增加一倍。因此， *O(n)* 时间操作在 n 个元素之后进行。因此，平均而言，它只需多走一步，使其平均时间复杂度更接近 *O(1)* 。实际上，这提供了相当好的性能，因此，它是一个高使用率的容器。\n\n对于`insert`函数，除了将给定迭代器之后的元素向右移动之外，您别无选择。`insert`功能为我们做到了这一点。每当需要时，它还负责重新分配。由于需要移动元素，需要 *O(n)* 时间。以下示例演示如何实现向量插入函数。\n\n考虑一个具有前五个自然数的向量:\n\n```cpp\nstd::vector<int> vec = {1, 2, 3, 4, 5};\n```\n\n#### 注意\n\n矢量没有`push_front`功能。它具有通用的`insert`函数，该函数将迭代器作为位置的参数。\n\n通用的`insert`功能可以用来在前面插入一个元素，如下所示:\n\n```cpp\nvec.insert(int.begin(), 0);\n```\n\n让我们再看几个`push_back`和`insert`函数的例子:\n\n```cpp\nstd::vector<int> vec;\n// Empty vector {}\nvec.push_back(1);\n// Vector has one element {1}\nvec.push_back(2);\n// Vector has 2 elements {1, 2}\nvec.insert(vec.begin(), 0);\n// Vector has 3 elements {0, 1, 2}\nvec.insert(find(vec.begin(), vec.end(), 1), 4);\n// Vector has 4 elements {0, 4, 1, 2}\n```\n\n如前面的代码所示，`push_back`在末尾插入一个元素。此外，`insert`功能将插入位置作为参数。它采用迭代器的形式。因此，`begin()`功能允许我们在开头插入一个元素。\n\n现在我们已经了解了正常的插入函数，让我们来看看一些更好的替代方法，与`push_back`和`insert`函数相比，矢量可以使用。`push_back`和`insert`的缺点之一是，它们首先构造元素，然后将元素复制或移动到向量缓冲区内的新位置。这个操作可以通过在新位置调用新元素的构造函数来优化，这可以通过`emplace_back`和`emplace`函数来完成。为了获得更好的性能，建议您使用这些函数而不是普通的插入函数。因为我们正在构造元素，我们只需要传递构造函数参数，而不是构造值本身。然后，该函数将负责将参数转发到适当位置的构造函数。\n\n`std::vector`还提供了`pop_back`和`erase`功能，可以移除其中的元素。`pop_back`从向量中移除最后一个元素，有效地将大小减少一。`erase`有两个重载——移除迭代器提供的指向它的单个元素，以及移除迭代器提供的元素范围，其中该范围是通过定义要移除的第一个元素(包含)和要移除的最后一个元素(排除)来定义的。C++ 标准不需要这些函数来减少向量的容量。这完全取决于编译器的实现。`pop_back`不需要任何元素的重新排列，因此可以很快完成。其复杂度为 *O(1)* 。然而，`erase`需要移动元素，因此需要 *O(n)* 时间。在下面的练习中，我们将看到这些功能是如何实现的。\n\n现在，让我们看一下关于以不同方式从向量中移除元素的示例:\n\n考虑一个有 10 个元素的向量–`{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}`:\n\n```cpp\nvec.pop_back();\n// Vector has now 9 elements {0, 1, 2, 3, 4, 5, 6, 7, 8}\nvec.erase(vec.begin());\n// vector has now 7 elements {1, 2, 3, 4, 5, 6, 7, 8}\nvec.erase(vec.begin() + 1, vec.begin() + 4);\n// Now, vector has 4 elements {1, 5, 6, 7, 8}\n```\n\n现在，让我们看看其他一些有用的功能:\n\n*   `clear()`:这个函数只是通过移除所有元素来清空向量。\n*   `reserve(capacity)`:该功能用于指定矢量的容量。如果指定为参数的值大于当前容量，它将重新分配内存，新容量将等于参数。但是，对于所有其他情况，它不会影响向量的容量。这个函数不修改向量的大小。\n*   `shrink_to_fit()`:这个功能可以用来释放多余的空间。调用此函数后，大小和容量变得相等。当我们不期望向量的大小进一步增加时，可以使用这个函数。\n\n### 为标准向量分配参数:\n\n`std::vector`通过允许我们在数据类型之后传递一个分配器作为模板参数，解决了`std::array`关于自定义分配器的缺点。\n\n为了使用定制分配器，我们遵循某些概念和接口。由于向量在与内存访问相关的大多数行为中使用分配器函数，我们需要将这些函数作为分配器的一部分提供–`allocate`、`deallocate`、`construct`和`destroy`。这个分配器必须负责内存分配、解除分配和处理，以免损坏任何数据。对于依赖自动内存管理的高级应用来说，机制可能太昂贵，并且应用有自己的内存池或类似的资源，必须使用它们来代替默认的堆内存，客户分配器非常方便。\n\n因此矿石，`std::vector`是`std::array`的一个很好的替代品，在大小、生长和其他方面提供了更多的灵活性。渐近地，阵列的所有相似函数都具有与向量相同的时间复杂度。我们通常只为额外的功能支付额外的性能成本，这是相当合理的。对于一般情况，向量的性能离数组不远。因此，在实践中，`std::vector`是 C++ 中最常用的 STL 容器之一，因为它具有灵活性和性能。\n\n## 标准::f 前进 _ 列表\n\n到目前为止，我们只看到了类似数组的结构，但是，正如我们所看到的，数据结构中间的插入和删除对于连续的数据结构来说是非常低效的操作。这就是类似链表的结构出现的地方。许多应用需要频繁地在数据结构中间插入和删除。例如，任何具有多个选项卡的浏览器都可以在任何时间点和任何位置添加额外的选项卡。同样，任何音乐播放器都会有一个循环播放的歌曲列表，你也可以在中间插入任何歌曲。在这种情况下，我们可以使用链表结构来获得良好的性能。我们将在*活动 1* 、*实现歌曲播放列表*中看到音乐播放器的用例。现在，让我们探索 C++ 为我们提供了什么样的容器。\n\n链表的基本结构要求我们有一个指针，并使用`new`和`delete`运算符手动管理内存分配和解除分配。虽然不难，但会导致难以追踪的 bug。因此，就像`std::array`为 C 风格的数组提供了一个薄包装一样，`std::forward_list`为基本链表提供了一个薄包装。\n\n`std::forward_list`的目的是提供一些额外的功能，与基本的链表相比不会影响性能。为了保持性能，它不提供函数来获取列表的大小或直接获取第一个元素以外的任何元素。因此，它有一个名为`front()`的函数来获取对第一个元素的引用，但没有像`back()`那样访问最后一个元素。它确实为常见操作提供了功能，如插入、删除、反向和拼接。这些函数不会影响基本链表的内存需求或性能。\n\n另外，和`std::vector`一样，`std::forward_list`如果需要也可以将自定义分配器作为第二个模板参数。因此，我们可以轻松地将其用于受益于自定义内存管理的高级应用。\n\n### 在转发列表中插入和删除元素\n\n`std:: forward_list`提供`push_front`和`insert_after`功能，可用于在链表中插入元素。与向量的插入函数相比，这两者略有不同。`push_front`用于在前面插入元素。由于`forward_list`不能直接访问最后一个元素，所以它不提供`push_back`功能。对于特定位置的插入，我们使用`insert_after`代替`insert`。这是因为在链表中插入一个元素需要更新元素的下一个指针，之后我们要插入一个新元素。如果我们只提供迭代器，在那里我们想要插入一个新元素，我们不能快速访问前一个元素，因为在`forward_list`中不允许向后遍历。\n\n由于这是一种基于指针的机制，我们并不需要在插入过程中移动元素。因此，与任何基于数组的结构相比，这两个插入函数都要快得多。这两个函数只是修改指针，以便在指定位置插入新元素。该操作不依赖于列表的大小，因此时间复杂度为 *O(1)* 。我们将在下面的练习中研究这些功能的实现。\n\n现在，让我们看看如何在链表中插入元素:\n\n```cpp\nstd::forward_list<int> fwd_list = {1, 2, 3};\nfwd_list.push_front(0);\n// list becomes {0, 1, 2, 3}\nauto it = fwd_list.begin();\nfwd_list.insert_after(it, 5);\n// list becomes {0, 5, 1, 2, 3}\nfwd_list.insert_after(it, 6);\n// list becomes {0, 6, 5, 1, 2, 3}\n```\n\n`forward_list`还提供了`emplace_front`和`emplace_after`，类似于矢量的`emplace`。这两个函数的作用与插入函数相同，但通过避免额外的复制和移动，效率更高。\n\n`forward_list`还具有删除元素的`pop_front`和`erase_after`功能。`pop_front`，顾名思义，去掉了第一个元素。由于不需要任何换挡，实际操作起来相当快，时间复杂度为 *O(1)* 。`erase_after`有两个重载——移除单个元素(通过对其前一个元素使用迭代器)，以及移除一个范围内的多个元素(通过对该范围第一个元素之前的元素使用迭代器，对最后一个元素使用另一个迭代器)。\n\n`erase_after`函数的时间复杂度与被擦除的元素数量成线性关系，因为元素的删除不能通过释放单个内存块来完成。由于所有的节点分散在内存中的随机位置，函数需要分别释放它们。\n\n现在，让我们看看如何从列表中删除这些元素:\n\n```cpp\nstd::forward_list<int> fwd_list = {1, 2, 3, 4, 5};\nfwd_list.pop_front();\n// list becomes {2, 3, 4, 5}\nauto it = fwd_list.begin();\nfwd_list.erase_after(it);\n// list becomes {2, 4, 5}\nfwd_list.erase_after(it, fwd_list.end());\n// list becomes {2}\n```\n\n让我们在下一节中探索我们可以使用`forward_list`进行哪些其他操作。\n\n### 转发列表上的其他操作\n\n除了`erase`函数根据迭代器确定的位置删除元素外，`forward_list`还提供了`remove`和`remove_if`函数根据元素的值删除元素。`remove`函数接受一个参数——要删除的元素的值。它会根据为值类型定义的相等运算符，移除与给定元素匹配的所有元素。如果没有相等运算符，编译器不允许我们调用该函数，并抛出编译错误。由于`remove`只删除基于等式运算符的元素，所以不能使用它来删除基于其他条件的元素，因为我们不能在定义一次等式运算符后更改它。对于有条件的移除，`forward_list`提供`remove_if`功能。它以谓词为参数，谓词是以值类型的元素为参数的函数，以布尔值为返回值。因此，谓词返回 true 的所有元素都将从列表中删除。在最新的 C++ 版本中，我们也可以很容易地用 lambdas 指定谓词。下面的练习将帮助您理解如何实现这些功能。\n\n### 练习 3:使用 remove_if 从链接列表中有条件地移除元素\n\n在本练习中，我们将使用一些印度公民在选举期间的样本信息，并根据他们的年龄从选民名册中删除不符合资格的公民。为了简单起见，我们只存储公民的姓名和年龄。\n\n我们将数据存储在链表中，并使用`remove_if`移除所需的元素，这提供了一种移除满足特定条件的元素的方法，而不是定义要移除的元素的位置:\n\n1.  让我们首先包含所需的标题，并添加`struct citizen` :\n\n    ```cpp\n    #include <iostream>\n    #include <forward_list>\n    struct citizen\n    {\n        std::string name;\n        int age;\n    };\n    std::ostream& operator<<(std::ostream& os, const citizen& c)\n    {\n        return (os << \"[Name: \" << c.name << \", Age: \" << c.age << \"]\");\n    }\n    ```\n\n2.  现在，让我们编写一个`main`函数，并在一个`std::forward_list`中初始化几个公民。我们还将复制它，以避免再次初始化:\n\n    ```cpp\n    int main()\n    {\n      std::forward_list<citizen> citizens = {{\"Raj\", 22}, {\"Rohit\", 25}, {\"Rohan\", 17}, {\"Sachin\", 16}};\n      auto citizens_copy = citizens;\n      std::cout << \"All the citizens: \";\n      for (const auto &c : citizens)\n          std::cout << c << \" \";\n      std::cout << std::endl;\n    ```\n\n3.  Now, let's remove all of the ineligible citizens from the list:\n\n    ```cpp\n    citizens.remove_if(\n        [](const citizen& c)\n        {\n            return (c.age < 18);\n        });\n    std::cout << \"Eligible citizens for voting: \";\n    for(const auto& c: citizens)\n        std::cout << c << \" \";\n    std::cout << std::endl;\n    ```\n\n    `remove_if`函数移除给定谓词为真的所有元素。这里，我们提供了一个λ，因为条件非常简单。如果这是一个复杂的条件，我们还可以编写一个普通的函数，该函数接受底层类型列表的一个参数并返回一个布尔值。\n\n4.  Now, let's find out who'll be eligible for voting next year:\n\n    ```cpp\n    citizens_copy.remove_if(\n        [](const citizen& c)\n        {\n        // Returns true if age is less than 18\n            return (c.age != 17);\n        });\n    std::cout << \"Citizens that will be eligible for voting next year: \";\n    for(const auto& c: citizens_copy)\n        std::cout << c << \" \";\n    std::cout << std::endl;\n    }\n    ```\n\n    如你所见，我们只保留那些 17 岁的公民。\n\n5.  进行练习。你应该得到这样的输出:\n\n    ```cpp\n    All the citizens: [Name: Raj, Age: 22] [Name: Rohit, Age: 25] [Name: Rohan, Age: 17] [Name: Sachin, Age: 16] \n    Eligible citizens for voting: [Name: Raj, Age: 22] [Name: Rohit, Age: 25] \n    Citizens that will be eligible for voting next year: [Name: Rohan, Age: 17] \n    ```\n\n`remove_if`函数的时间复杂度为 *O(n)* ，因为它只需遍历列表一次，同时根据需要移除所有元素。如果我们想要移除具有特定值的元素，我们可以使用另一个版本的`remove`，它只需获取对象的一个参数，并从列表中移除与给定值匹配的所有对象。它还要求我们为给定的类型实现`==`运算符。\n\n`forward_list`还提供了`sort`功能对数据进行排序。所有与数组相关的结构都可以通过一个通用函数`std::sort(first iterator, last iterator)`进行排序。然而，它不能被基于链表的结构使用，因为我们不能随机访问任何数据。这也使得`forward_list`提供的迭代器不同于数组或向量的迭代器。我们将在下一节更详细地了解这一点。作为`forward_list`的一部分提供的`sort`功能有两个过载–`sort`基于小于运算符(`<`)和`sort`基于作为参数提供的比较器。默认的`sort`功能使用`std::less<value_type>`进行比较。如果第一个参数小于第二个参数，它只返回`true`，因此，需要我们为自定义类型定义小于运算符(`<`)。\n\n除此之外，如果我们想基于一些其他参数进行比较，我们可以使用参数重载，它采用二进制谓词。这两种过载都具有线性时间复杂度-*0(n×log n)*。以下示例演示了`sort`的两种重载:\n\n```cpp\nstd::forward_list<int> list1 = {23, 0, 1, -3, 34, 32};\nlist1.sort();\n// list becomes {-3, 0, 1, 23, 32, 34}\nlist1.sort(std::greater<int>());\n// list becomes {34, 32, 23, 1, 0, -3}\n```\n\n这里，`greater<int>`是标准本身提供的一个谓词，它是大于运算符(`>`)的包装器，将元素按降序排序，从列表的值中我们可以看到这一点..\n\n`forward_list`提供的其他功能有`reverse`和`unique`。`reverse`功能只是在与列表中存在的元素数量成线性关系的持续时间内颠倒元素的顺序，即时间复杂度为 *O(n)* 。`unique`函数仅保留列表中的唯一元素，并移除除第一个之外的所有重复值函数。因为它依赖于元素的相等性，所以它有两个重载——第一个重载不带参数，对值类型使用相等运算符，而第二个重载使用带有两个值类型参数的二元谓词。`unique`函数的时间复杂度是线性的。因此，它不会将每个元素与其他元素进行比较。相反，它只比较连续的元素是否相等，如果后者与基于默认或自定义二进制谓词的前者相同，则删除后者。因此，要使用`unique`函数从列表中移除所有唯一的元素，我们需要在调用函数之前对元素进行排序。在给定谓词的帮助下，`unique`会将所有元素与其相邻元素进行比较，如果谓词返回`true`，则移除后面的元素。\n\n现在让我们看看如何使用列表的`reverse`、`sort`和`unique`功能:\n\n```cpp\nstd::forward_list<int> list1 = {2, 53, 1, 0, 4, 10};\nlist1.reverse();\n// list becomes {2, 53, 1, 0, 4, 10}\nlist1 = {0, 1, 0, 1, -1, 10, 5, 10, 5, 0};\nlist1.sort();\n// list becomes {-1, 0, 0, 0, 1, 1, 5, 5, 10, 10}\nlist1.unique();\n// list becomes {-1, 0, 1, 5, 10}\nlist1 = {0, 1, 0, 1, -1, 10, 5, 10, 5, 0};\nlist1.sort();\n// list becomes {-1, 0, 0, 0, 1, 1, 5, 5, 10, 10}\n```\n\n如果元素比先前有效的元素至少大 2，则以下示例将移除这些元素:\n\n```cpp\nlist1.unique([](int a, int b) { return (b - a) < 2; });\n// list becomes {-1, 1, 5, 10}\n```\n\n#### 注意\n\n在调用`unique`函数之前，程序员必须确保数据已经排序。因此，我们在它前面调用`sort`函数。`unique`功能将元素与已经满足条件的前一个元素进行比较。此外，它总是保留原始列表的第一个元素。因此，总有一个可以比较的因素。\n\n在下一节中，我们将看看`forward_list`迭代器与向量/数组迭代器有何不同。\n\n## 迭代〔t0〕之外\n\n您可能已经注意到，在一些数组和向量的例子中，我们给迭代器添加了数字。迭代器就像指针，但是它们也为 STL 容器提供了一个公共接口。对这些迭代器的操作严格基于迭代器的类型，这取决于容器。向量和数组的迭代器在功能上是最灵活的。由于数据的连续性质，我们可以使用`operator[]`基于其位置直接从容器中访问任何元素。这个迭代器也被称为随机访问迭代器。但是对于`forward_list`来说，没有直接的方法可以遍历回去，甚至从一个节点到它的前一个节点，而不从头开始。因此，唯一允许的算术运算符是增量。这个迭代器也被称为前向迭代器。\n\n根据迭代器的类型，我们还可以使用其他实用函数，如`advance`、`next`和`prev`。`next`和`prev`取一个迭代器和一个距离值，然后返回指向与给定迭代器有给定距离的元素的迭代器。如果给定的迭代器支持该操作，这将按预期工作。例如，如果我们试图将`prev`函数与`forward`迭代器一起使用，它将抛出一个编译错误，因为这个迭代器是向前迭代器，只能向前移动。这些函数花费的时间取决于所用迭代器的类型。所有这些都是随机访问迭代器的常数时间函数，因为加法和减法是常数时间操作。对于其余的迭代器，它们都与需要向前或向后遍历的距离成线性关系。我们将在下面的练习中使用这些迭代器。\n\n### 练习 4:探索不同类型的迭代器\n\n假设我们有过去几年新加坡 F1 大奖赛的获胜者名单。借助向量迭代器，我们将发现如何从这些数据中检索有用的信息。之后，我们将尝试用`forward_list`做同样的事情，看看它与向量迭代器有什么不同:\n\n1.  让我们首先包括标题:\n\n    ```cpp\n    #include <iostream>\n    #include <forward_list>\n    #include <vector>\n    int main()\n    {\n    ```\n\n2.  让我们写一个带有赢家列表的向量:\n\n    ```cpp\n    std::vector<std::string> vec = {\"Lewis Hamilton\", \"Lewis Hamilton\", \"Nico Roseberg\", \"Sebastian Vettel\", \"Lewis Hamilton\", \"Sebastian Vettel\", \"Sebastian Vettel\", \"Sebastian Vettel\", \"Fernando Alonso\"};\n    auto it = vec.begin();       // Constant time\n    std::cout << \"Latest winner is: \" << *it << std::endl;\n    it += 8;                    // Constant time\n    std::cout << \"Winner before 8 years was: \" << *it << std::endl;\n    advance(it, -3);            // Constant time\n    std::cout << \"Winner before 3 years of that was: \" << *it << std::endl;\n    ```\n\n3.  让我们对`forward_list`迭代器进行同样的尝试，看看它们与向量迭代器有什么不同:\n\n    ```cpp\n    std::forward_list<std::string> fwd(vec.begin(), vec.end());\n    auto it1 = fwd.begin();\n    std::cout << \"Latest winner is: \" << *it << std::endl;\n    advance(it1, 5);   // Time taken is proportional to the number of elements\n    std::cout << \"Winner before 5 years was: \" << *it << std::endl;\n    // Going back will result in compile time error as forward_list only allows us to move towards the end.\n    // advance(it1, -2);      // Compiler error\n    }\n    ```\n\n4.  运行本练习应产生以下输出:\n\n    ```cpp\n    Latest winner is : Lewis Hamilton\n    Winner before 8 years was : Fernando Alonso\n    Winner before 3 years of that was : Sebastian Vettel\n    Latest winner is : Sebastian Vettel\n    Winner before 5 years was : Sebastian Vettel\n    ```\n\n5.  Now, let's see what happens if we add a number to this iterator by putting the following line inside the `main` function at the end:\n\n    ```cpp\n    it1 += 2;\n    ```\n\n    我们将收到类似如下的错误消息:\n\n    ```cpp\n    no match for 'operator+=' (operand types are std::_Fwd_list_iterator<int>' and 'int')\n    ```\n\n我们在本练习中探索的各种迭代器对于从数据集中轻松获取任何数据非常有用。\n\n正如我们所看到的，`std::array`是 C 风格数组上的一个薄包装器，`std::forward_list`只不过是一个单链表上的一个薄包装器。它提供了一个简单且不易出错的界面，不会影响性能或内存。\n\n除此之外，因为我们可以直接访问向量中的任何元素，所以向量迭代器上的加减运算是 *O(1)* 。另一方面，`forward_list`只支持通过遍历元素来访问元素。因此，它的迭代器的加法运算是 *O(n)* ，其中 n 是我们前进的步数。\n\n在下面的练习中，我们将制作一个自定义容器，其工作方式类似于`std::forward_list`，但有一些改进。我们将定义许多等同于`forward_list`函数的函数。它还应该帮助你理解这些功能是如何在引擎盖下工作的。\n\n### 练习 5:构建基本定制容器\n\n在本练习中，我们将实现一个带有一些改进的`std::forward_list`等效容器。我们将从一个名为`singly_ll`的基本实现开始，并逐渐改进:\n\n1.  让我们添加所需的头，然后从单个节点的基本实现`singly_ll`开始:\n\n    ```cpp\n    #include <iostream>\n    #include <algorithm>\n    struct singly_ll_node\n    {\n        int data;\n        singly_ll_node* next;\n    };\n    ```\n\n2.  现在，我们将实现实际的`singly_ll`类，它将节点包装起来以便更好地接口:\n\n    ```cpp\n    class singly_ll\n    {\n    public:\n        using node = singly_ll_node;\n        using node_ptr = node*;\n    private:\n        node_ptr head;\n    ```\n\n3.  现在，让我们添加`push_front`和`pop_front`，就像在`forward_list`中一样:\n\n    ```cpp\n    public:\n    void push_front(int val)\n    {\n        auto new_node = new node{val, NULL};\n        if(head != NULL)\n            new_node->next = head;\n        head = new_node;\n    }\n    void pop_front()\n    {\n        auto first = head;\n        if(head)\n        {\n            head = head->next;\n            delete first;\n        }\n        else\n            throw \"Empty \";\n    }\n    ```\n\n4.  现在让我们为我们的`singly_ll`类实现一个基本的迭代器，带有构造函数和访问函数:\n\n    ```cpp\n    struct singly_ll_iterator\n    {\n    private:\n        node_ptr ptr;\n    public:\n        singly_ll_iterator(node_ptr p) : ptr(p)\n        {\n    }\n    int& operator*()\n    {\n        return ptr->data;\n    }\n    node_ptr get()\n    {\n        return ptr;\n    }\n    ```\n\n5.  让我们添加增量前和增量后的`operator++ `功能:\n\n    ```cpp\n    singly_ll_iterator& operator++()     // pre-increment\n    {\n            ptr = ptr->next;\n            return *this;\n    }\n    singly_ll_iterator operator++(int)    // post-increment\n    {\n        singly_ll_iterator result = *this;\n    ++(*this);\n    return result;\n    }\n    ```\n\n6.  让我们添加等式运算作为`friend`函数:\n\n    ```cpp\n        friend bool operator==(const singly_ll_iterator& left, const singly_ll_iterator& right)\n        {\n            return left.ptr == right.ptr;\n        }\n        friend bool operator!=(const singly_ll_iterator& left, const singly_ll_iterator& right)\n        {\n            return left.ptr != right.ptr;\n        }\n    };\n    ```\n\n7.  让我们跳回我们的链表类。现在我们已经有了迭代器类，让我们实现`begin`和`end`函数来简化遍历。我们还将为两者添加`const`版本:\n\n    ```cpp\n    singly_ll_iterator begin()\n    {\n        return singly_ll_iterator(head);\n    }\n    singly_ll_iterator end()\n    {\n        return singly_ll_iterator(NULL);\n    }\n    singly_ll_iterator begin() const\n    {\n        return singly_ll_iterator(head);\n    }\n    singly_ll_iterator end() const\n    {\n        return singly_ll_iterator(NULL);\n    }\n    ```\n\n8.  让我们实现一个默认构造函数，一个用于深度复制的复制构造函数，以及一个带有`initializer_list` :\n\n    ```cpp\n    singly_ll() = default;\n    singly_ll(const singly_ll& other) : head(NULL)\n    {\n        if(other.head)\n            {\n                head = new node;\n                auto cur = head;\n                auto it = other.begin();\n                while(true)\n                {\n                    cur->data = *it;\n                    auto tmp = it;\n                    ++ tmp;\n                    if(tmp == other.end())\n                        break;\n                    cur->next = new node;\n                    cur = cur->next;\n                    it = tmp;\n                }\n            }\n    }\n    singly_ll(const std::initializer_list<int>& ilist) : head(NULL)\n    {\n        for(auto it = std::rbegin(ilist); it != std::rend(ilist); it++)\n                push_front(*it);\n    }\n    };\n    ```\n\n    的构造函数\n9.  让我们编写一个`main`函数来使用前面的函数:\n\n    ```cpp\n    int main()\n    {\n        singly_ll sll = {1, 2, 3};\n        sll.push_front(0);\n        std::cout << \"First list: \";\n        for(auto i: sll)\n            std::cout << i << \" \";\n        std::cout << std::endl;\n\n        auto sll2 = sll;\n        sll2.push_front(-1);\n        std::cout << \"Second list after copying from first list and inserting -1 in front: \";\n        for(auto i: sll2)\n            std::cout << i << ' ';  // Prints -1 0 1 2 3\n        std::cout << std::endl;\n        std::cout << \"First list after copying - deep copy: \";\n    for(auto i: sll)\n            std::cout << i << ' ';  // Prints 0 1 2 3\n        std::cout << std::endl;\n    }\n    ```\n\n10.  运行本练习应产生以下输出:\n\n    ```cpp\n    First list: 0 1 2 3\n    Second list after copying from first list and inserting -1 in front: -1 0 1 2 3 \n    First list after copying - deep copy: 0 1 2 3\n    ```\n\n正如我们在前面的例子中看到的，我们能够使用`std::initializer_list`初始化我们的列表。我们可以调用`push`、`pop_front`和`back`功能。如我们所见，`sll2.pop_back`只移除了`sll2`的元素，而没有移除`sll`。`sll`依然完好，五行全无。因此，我们也可以执行深度复制。\n\n### 活动 1:实现歌曲播放列表\n\n在本练习中，我们将研究一些双链表不够或不方便的应用。我们将构建一个适合应用的调整版本。我们经常会遇到必须定制默认实现的情况，例如在音乐播放器中循环播放歌曲时，或者在多个玩家一个接一个转圈的游戏中。\n\n这些应用有一个共同的特性——我们以循环方式遍历序列的元素。因此，在遍历列表时，最后一个节点之后的节点将是第一个节点。这被称为循环链表。\n\n我们将以音乐播放器为例。它应该支持以下功能:\n\n1.  使用多首歌曲创建播放列表。\n2.  将歌曲添加到播放列表。\n3.  从播放列表中移除歌曲。\n4.  Play songs in a loop (for this activity, we will print all the songs once).\n\n    #### 注意\n\n    你可以参考*练习 5* 、*构建一个基本的定制容器*，在这里我们从头构建了一个支持类似功能的容器。\n\n以下是解决问题的步骤:\n\n1.  首先，设计一个支持循环数据表示的基本结构。\n2.  之后，在结构中实现`insert`和`erase`功能，支持各种操作。\n3.  我们必须编写一个自定义迭代器。这有点棘手。重要的是确保我们能够使用基于范围的循环方法遍历容器。因此，`begin()`和`end()`应该返回不同的地址，尽管结构是圆形的。\n4.  After building the container, build a wrapper over it, which will store different songs in the playlist and perform relevant operations, such as `next`, `previous`, `print all`, `insert`, and `remove`.\n\n    #### 注意\n\n    这个活动的解决方案可以在第 476 页找到。\n\n`std::forward_list`有几个限制。`std::list`展示了一个更加灵活的列表实现，并有助于克服`forward_list`的一些缺点。\n\n## STD::l〔t0〕是\n\n如前一节中的 n 所示，`std::forward_list`只是基本链表的一个又好又薄的包装器。它不提供在末尾插入元素、向后遍历或获取列表大小等有用操作的功能。该功能仅限于节省内存和保持快速性能。除此之外，`forward_list`的迭代器只能支持很少的操作。在任何应用的大多数实际情况下，诸如在末尾插入一些东西和获取容器大小的功能都非常有用，并且经常使用。因此，`std::forward_list`并不总是需要快速插入的理想容器。为了克服对`std::forward_list`的一些限制，C++ 提供了`std::list`，由于它是一个双向链表，也称为双链表，所以有几个额外的特性。但是，请注意，这是以额外的内存需求为代价的。\n\n双链表的普通版本如下所示:\n\n```cpp\nstruct doubly_linked_list\n{\n    int data;\n    doubly_linked_list *next, *prev;\n};\n```\n\n如您所见，它有一个额外的指针指向前一个元素。因此，它为我们提供了一种向后遍历的方式，我们还可以存储大小和最后一个元素来支持快速`push_back`和`size`操作。还有，就像`forward_list`一样，也可以支持客户分配器作为模板参数。\n\n### 标准列表的常用功能\n\n`std::list`的大部分功能与`std::forward_list`的功能相同或相似，只是有一些小的改动。其中一个调整是以`_after`结尾的函数名有它们的等价物，没有`_after`。因此，`insert_after`和`emplace_after`简单的变成了`insert`和`emplace`。这是因为，使用`std::list`迭代器，我们也可以向后遍历，因此不需要提供前面元素的迭代器。相反，我们可以提供要执行操作的确切元素的迭代器。除此之外，`std::list`还为`push_back`、`emplace_back`、`pop_back`提供快速操作。以下练习演示了插入和删除功能在`std::list`中的使用。\n\n### 练习 6:标准::列表的插入和删除功能\n\n在本练习中，我们将使用`std::list`创建一个简单的整数列表，并探索在其中插入和删除元素的各种方法:\n\n1.  首先，让我们包含所需的标题:\n\n    ```cpp\n    #include <iostream>\n    #include <list>\n    int main()\n    {\n    ```\n\n2.  Then, initialize a list with a few elements and experiment on it with various insertion functions:\n\n    ```cpp\n    std::list<int> list1 = {1, 2, 3, 4, 5};\n    list1.push_back(6);\n    // list becomes {1, 2, 3, 4, 5, 6}\n    list1.insert(next(list1.begin()), 0);\n    // list becomes {1, 0, 2, 3, 4, 5, 6}\n    list1.insert(list1.end(), 7);\n    // list becomes {1, 0, 2, 3, 4, 5, 6, 7}\n    ```\n\n    可以看到，`push_back`函数在末尾插入了一个元素。`insert`功能在第一个元素后插入`0`，用`next(list1.begin())`表示。之后，我们在最后一个元素后插入`7`，用`list1.end()`表示。\n\n3.  现在，让我们来看看移除功能`pop_back`，该功能在`forward_list`中不存在:\n\n    ```cpp\n    list1.pop_back();\n    // list becomes {1, 0, 2, 3, 4, 5, 6}\n    std::cout << \"List after insertion & deletion functions: \";\n    for(auto i: list1)\n        std::cout << i << \" \";\n    }\n    ```\n\n4.  Running this exercise should give the following output:\n\n    ```cpp\n    List after insertion & deletion functions: 1 0 2 3 4 5 6\n    ```\n\n    这里，我们移除了刚刚插入的最后一个元素。\n\n    #### 注意\n\n    虽然`push_front`、`insert`、`pop_front`、`erase`的时间复杂度与`forward_list`的等效函数相同，但这些对于`std::list`来说稍微贵一些。原因是列表的每个节点都有两个指针，而不是像`forward_list`那样只有一个。所以，我们必须保持两个指针值的有效性。因此，当我们重新输入这些变量时，我们需要付出比单链表几乎两倍的努力。\n\n之前，我们看到了一个单链表的插入。现在让我们在下图中演示双链表的指针操作是什么样子的:\n\n![](img/C14498_01_07.jpg)\n\n###### 图 1.7:在双向链表中插入一个元素\n\n可以看到，即使在`std::list`的情况下，运算次数也是恒定的；然而，与`forward_list`相比，我们必须修正`prev`和`next`指针，以维护双链表，与`forward_list`相比，这在内存和性能方面花费了我们几乎两倍的成本。类似的想法也适用于其他功能。\n\n其他功能，如`remove`、`remove_if`、`sort`、`unique`和`reverse`提供了与`std::forward_list`相同的功能。\n\n### 双向迭代器\n\n在*迭代器*部分，我们看到了基于数组的随机访问迭代器和基于`forward_list`的前向迭代器的灵活性之间的区别。`std::list::iterator`的灵活性介于两者之间。与前向迭代器相比，它更灵活，因为它允许我们向后遍历。因此，`std::list`也支持反向遍历的功能，在操作被反转的地方公开反向迭代器。话虽如此，它不如随机访问迭代器灵活。虽然我们可以通过任意数量的移动向任何方向前进，但是由于这些移动必须通过逐个遍历元素来完成，而不是直接跳到所需的元素，所以时间复杂度仍然是线性的，而不是常数，就像随机访问迭代器的情况一样。因为这些迭代器可以在任一方向上移动，所以它们被称为双向迭代器。\n\n### 不同容器的失效迭代\n\n到目前为止，我们已经看到迭代器为我们提供了从任何容器中访问、遍历、插入和删除元素的统一方式。但是在某些情况下，迭代器在修改容器后变得无效，因为迭代器是基于绑定到内存地址的指针来实现的。因此，如果任何节点或元素的内存地址因为容器中的修改而改变，它将使迭代器无效，无论如何使用它都会导致未定义的行为。\n\n例如，一个非常基本的例子是`vector::push_back`，它只是在末尾添加了一个新元素。然而，正如我们前面看到的，在某些情况下，它还需要将所有元素移动到一个新的缓冲区。因此，所有迭代器、指针，甚至对任何现有元素的引用都将失效。同样，如果`vector::insert`功能导致重新分配，所有元素都需要移动。因此，所有的迭代器、指针和引用都是无效的。如果没有，函数将使指向插入位置右侧元素的所有迭代器无效，因为这些元素将在过程中被移动。\n\n与向量不同，基于链表的迭代器对于插入和删除操作更安全，因为元素不会被移动。因此，`std::list`或`forward_list`的插入函数都不会影响迭代器的有效性。一个例外是与删除相关的操作会使被删除元素的迭代器失效，这是显而易见的，也是合理的。它不影响其余元素的迭代器的有效性。以下示例显示了不同迭代器的迭代器无效:\n\n```cpp\nstd::vector<int> vec = {1, 2, 3, 4, 5};\nauto it4 = vec.begin() + 4;\n// it4 now points to vec[4]\nvec.insert(vec.begin() + 2, 0);\n// vec becomes {1, 2, 0, 3, 4, 5}\n```\n\n`it4`现在无效，因为它出现在插入位置之后。访问它将导致未定义的行为:\n\n```cpp\nstd::list<int> lst = {1, 2, 3, 4, 5};\nauto l_it4 = next(lst.begin(), 4);\nlst.insert(next(lst.begin(), 2), 0);\n// l_it4 remains valid\n```\n\n正如我们所知 aw，`std::list`比`std::forward_list`灵活得多。提供了`size`、`push_back`、`pop_back`等大量操作，操作的时间复杂度为 *O(1)* 。因此，与`std::forward_list`相比，`std::list`的使用频率更高。`forward_list`如果我们对内存和性能有非常严格的限制，并且我们确定不想向后遍历，那么它是一个更好的选择。所以，大多数情况下，`std::list`是比较安全的选择。\n\n### 活动 2:模拟纸牌游戏\n\n在本练习中，我们将分析给定的情况，并尝试提出最合适的数据结构来实现最佳性能。\n\n我们将尝试模拟一个纸牌游戏。游戏有 4 个玩家，每个玩家从 13 张随机牌开始。然后，我们将尝试从每个玩家手中随机挑选一张牌。这样，我们将有 4 张卡片进行比较。之后，我们将从这 4 张卡片中移除匹配的卡片。剩下的牌，如果有的话，会被放出来的玩家收回。如果有多个匹配对，其中只有一个可以删除，我们可以选择其中任何一个。如果没有配对，玩家可以洗牌。\n\n现在，我们需要一遍又一遍地继续这个过程，直到其中至少有一个没有牌。第一个扔掉所有牌的人赢得比赛。然后，我们将在最后打印获胜者。\n\n执行以下步骤来解决活动:\n\n1.  首先，确定哪个容器最适合存储每个玩家的卡片。我们应该有四个容器，里面有一套牌——每个玩家一张。\n2.  编写一个函数来初始化和洗牌。\n3.  写一个函数，在四个玩家中随机发牌。\n4.  写一个匹配的函数。该功能将从每个玩家中挑选一张牌，并根据游戏规则的要求进行比较。然后，它会移除必要的卡片。我们必须明智地选择卡，这样移除它会更快。在决定容器时也应该考虑这个参数。\n5.  现在，让我们写一个函数，看看我们是否有赢家。\n6.  Finally, we'll write the core logic of the game. This will simply call the matching function until we have a winner based on the function written in the previous step.\n\n    #### 注意\n\n    这个活动的解决方案可以在第 482 页找到。\n\n## 标准::deque–标准::矢量的特殊版本\n\n到目前为止，我们已经看到了基于数组和基于链表的容器。`std::deque`将两者混合，并在一定程度上结合各自的优势。正如我们所看到的，虽然 vector 是一个可变长度的数组，但是它的一些功能，比如`push_front`和`pop_front`，是非常昂贵的操作。`std::deque`可以帮助我们克服这一点。Deque 是双端队列的缩写。\n\n### 德格的结构\n\nC++ 标准只定义容器的行为，而不定义实现。到目前为止，我们所看到的容器足够简单，我们可以预测它们的实现。然而，德奎比这稍微复杂一点。因此，我们将首先看看它的需求，然后我们将尝试深入一点实现。\n\nC++ 标准为 deque 的不同操作保证了以下时间复杂性:\n\n*   *0(1)*代表`push_front`、`pop_front`、`push_back`和`pop_back`\n*   *O(1)* 用于随机访问所有元素\n*   在中间插入或删除的情况下 *N/2* 步的最大值，其中 *N* =德格的大小\n\n从需求来看，我们可以说容器应该能够非常快速地在任一方向上增长，并且仍然能够提供对所有元素的随机访问。因此，该结构必须有点像矢量，但仍然可以从前面和后面扩展。插入和删除的要求给出了一个轻微的提示，我们将移动元素，因为我们只被允许进行 *N/2* 步。这也验证了我们之前关于类似于向量的行为的假设。由于容器可以向任何一个方向快速增长，我们不必每次都将元素向右移动。相反，我们可以将元素移向最近的一端。这将给我们一个最大 *N/2* 步的时间复杂度，因为最近的末端离容器内的任何插入点不能超过 *N/2* 个节点。\n\n现在，让我们关注前面的随机访问和插入。该结构不能存储在单个内存块中。相反，我们可以有多个相同大小的内存块。这样，基于块的索引和大小(或者每个块的元素数量)，我们可以决定我们想要哪个块的索引元素。只有当我们将指向所有内存块的指针存储在一个连续的位置时，这才能帮助我们在 *O(1)* 时间内实现随机访问。因此，可以假设该结构类似于数组的向量。\n\n当我们想在前面插入一些东西，而第一个内存块没有足够的空间时，我们必须分配另一个块，并在前面的指针向量中插入它的地址。这可能需要重新分配指针向量，但实际数据不会被移动。为了优化这种重新分配，我们可以从向量的中间块开始插入，而不是从第一个块开始。这样，我们在一定数量的前端插入时是安全的。我们可以在重新分配指针向量时遵循同样的方法。\n\n#### 注意\n\n因为 deque 不像本章中讨论的其他容器那么简单，所以实际实现可能会有所不同，或者可能比我们讨论的有更多的优化，但是基本思想保持不变。也就是说，我们需要多个连续内存块来实现这样一个容器。\n\ndeque 支持的函数和操作更多的是向量和列表支持的函数的组合；因此，我们有`push_front`、`push_back`、`insert`、`emplace_front`、`emplace_back`、`emplace`、`pop_front`、`pop_back`、`erase`等等。我们也有向量的功能，如`shrink_to_fit`，来优化容量，但我们没有一个名为`capacity`的功能，因为这高度依赖于实现，因此预计不会被暴露。正如您所料，它像向量一样提供随机访问迭代器。\n\n让我们看看如何在 deque 上使用不同的插入和删除操作:\n\n```cpp\nstd::deque<int> deq = {1, 2, 3, 4, 5};\ndeq.push_front(0);\n// deque becomes {0, 1, 2, 3, 4, 5}\ndeq.push_back(6);\n// deque becomes {0, 1, 2, 3, 4, 5, 6}\ndeq.insert(deq.begin() + 2, 10);\n// deque becomes {0, 1, 10, 2, 3, 4, 5, 6}\ndeq.pop_back();\n// deque becomes {0, 1, 10, 2, 3, 4, 5}\ndeq.pop_front();\n// deque becomes {1, 10, 2, 3, 4, 5}\ndeq.erase(deq.begin() + 1);\n// deque becomes {1, 2, 3, 4, 5}\ndeq.erase(deq.begin() + 3, deq.end());\n// deque becomes {1, 2, 3}\n```\n\n这种结构可用于航班登机排队等情况。\n\n容器之间唯一不同的是性能和内存需求。Deque 将为前端和后端的插入和删除提供非常好的性能。中间的插入和删除平均来说也比向量快一点，虽然，渐近来说，它和向量是一样的。\n\n除此之外，deque 还允许我们像向量一样拥有客户分配器。我们可以在初始化时将其指定为第二个模板参数。这里需要注意的一点是，分配器是类型的一部分，而不是对象的一部分。这意味着我们不能比较两个对象的两个德格或两个向量，其中每个都有不同类型的分配器。同样，我们不能对不同类型的分配器对象进行其他操作，如赋值或复制构造函数。\n\n正如我们看到的 aw，`std::deque`与之前我们检查过的其他容器相比，结构稍微复杂一些。事实上，它是唯一一个提供高效随机访问以及快速`push_front`和`push_back`功能的容器。Deque 被用作其他容器的底层容器，我们将在下一节中看到。\n\n## 包含 er 适配器\n\n我们现在看到的容器都是从零开始建造的。在本节中，我们将看看构建在其他容器之上的容器。在现有容器上提供包装有多种原因，例如为代码提供更多的语义含义，限制某人仅仅因为可用就意外使用非预期的函数，以及提供特定的接口。\n\n一个这样的具体用例是**栈**数据结构。栈遵循**后进先出** ( **后进先出**结构来访问和处理数据。在功能方面，它只能在容器的一端插入和删除，除了在突变端，它不能更新甚至访问任何元素。这一端称为栈顶。我们可以很容易地使用任何其他容器，比如 vector 或 deque，因为默认情况下它可以满足这些要求。然而，这样做有一些基本问题。\n\n下面的示例显示了栈的两种实现:\n\n```cpp\nstd::deque<int> stk;\nstk.push_back(1);  // Pushes 1 on the stack = {1}\nstk.push_back(2);  // Pushes 2 on the stack = {1, 2}\nstk.pop_back();    // Pops the top element off the stack = {1}\nstk.push_front(0); // This operation should not be allowed for a stack\nstd::stack<int> stk;\nstk.push(1);       // Pushes 1 on the stack = {1}\nstk.push(2);       // Pushes 2 on the stack = {1, 2}\nstk.pop();         // Pops the top element off the stack = {1}\nstk.push_front(0); // Compilation error\n```\n\n正如我们在这个例子中看到的，使用 deque 的栈的第一个块仅通过变量的名称来提供语义含义。对数据进行操作的函数仍然不会强制程序员添加不应该被允许的代码，比如`push_front`。此外，`push_back`和`pop_back`函数会暴露不必要的细节，这些细节应该是默认的，因为它是一个栈。\n\n与此相比，如果我们看第二个版本，它看起来更准确地表明了它的功能。最重要的是，它不允许任何人做任何意想不到的事情，即使是偶然的。\n\n栈的第二个版本只不过是前一个容器 deque 的包装器，它为用户提供了一个良好的受限界面。这被称为容器适配器。C++ 提供了三种容器适配器:`std::stack`、`std::queue`和`std::priority_queue`。现在让我们简单地看一下它们。\n\n### 标准::st ack\n\n如前所述，适配器只是重复使用其他容器，如 deque、vector 或任何其他容器。默认情况下，`std::stack`将`std::deque`作为其底层容器。它提供了一个仅与栈相关的接口–`empty`、`size`、`top`、`push`、`pop`和`emplace`。这里，`push`只是为底层容器调用`push_back`函数，`pop`只是调用`pop_back`函数。`top`从底层容器调用`back`函数，获取最后一个元素，也就是栈顶。因此，它将用户操作限制为后进先出，因为它只允许我们更新底层容器一端的值。\n\n这里，我们使用 deque 作为底层容器，而不是向量。其背后的原因是，与 vector 不同，deque 不需要您在重新分配期间移动所有元素。因此，与矢量相比，使用 deque 更有效。但是，如果对于某些场景，任何其他容器更有可能提供更好的性能，stack 为我们提供了将容器作为模板参数的功能。因此，我们也可以使用向量或列表来构建栈，如下所示:\n\n```cpp\nstd::stack<int, std::vector<int>> stk;\nstd::stack<int, std::list<int>> stk;\n```\n\n一个栈的所有操作的时间复杂度为 *O(1)* 。通常没有将调用转发到底层容器的开销，因为编译器可以通过优化内联所有内容。\n\n### STD::qu〔t0〕新\n\n就像`std::stack`一样，我们有另一个容器适配器来处理很多应用中频繁出现的 **FIFO** ( **先进先出**)的场景，这个结构是由一个名为`std::queue`的适配器提供的。它的功能集几乎和栈一样，但是为了遵循先进先出而不是后进先出，意义和行为是不同的。对于`std::queue`，`push`就是`push_back`的意思，就像一叠，但是`pop`就是`pop_front`。代替`pop`，由于队列应该是暴露两端进行读取，所以它有`front`和`back`功能。\n\n下面是`std::queue`用法的一个小例子:\n\n```cpp\nstd::queue<int> q;\nq.push(1);  // queue becomes {1}\nq.push(2);  // queue becomes {1, 2}\nq.push(3);  // queue becomes {1, 2, 3}\nq.pop();    // queue becomes {2, 3}\nq.push(4);  // queue becomes {2, 3, 4}\n```\n\n如本例所示，首先，我们按顺序插入`1`、`2`和`3`。之后，我们将从队列中弹出一个元素。由于`1`是先推的，所以先从队列中移除。然后，下一次推送将`4`插入队列的后面。\n\n`std::queue`也使用`std::deque`作为底层容器，原因和 stack 一样，对于这里显示的所有方法，它也有 *O(1)* 的时间复杂度。\n\n### std::pr 优先级 _ 队列\n\n优先级队列通过其接口提供了一个非常有用的结构**堆**。众所周知，堆数据结构可以从容器中快速访问最小(或最大)元素。获取最小/最大元素是一个时间复杂度为 *O(1)* 的操作。插入具有 *O(对数 n)* 时间复杂度，而删除只能对最小/最大元素执行，该元素始终位于顶部。\n\n这里需要注意的一点是，我们只能让 min 或 max 函数快速可用，而不能同时使用这两个函数。这是由提供给容器的比较器决定的。与栈和队列不同，优先级队列默认基于向量，但是如果需要，我们可以更改它。同样，默认情况下，比较器是`std::less`。因为这是一个堆，所以得到的容器是一个最大堆。这意味着默认情况下，最大元素将位于顶部。\n\n这里，由于插入需要确保我们可以立即访问顶部元素(根据比较器的不同，最小或最大)，所以它不是简单地将调用转发到底层容器。相反，它使用比较器根据需要将数据向上冒泡，从而实现了数据堆积的算法。该操作需要的时间长度与容器的大小成对数关系，因此*0(对数 n)* 的时间复杂性。在用多个元素初始化不变量时，也需要维护它。但是这里`priority_queue`构造函数并不是简单的为每个元素调用插入函数；相反，它在 *O(n)* 中应用不同的堆化算法来加快速度。\n\n### 适配器的迭代器\n\n到目前为止，我们所看到的所有适配器都只公开了实现其语义所需的功能。从逻辑上考虑，遍历栈、队列和优先级队列没有意义。在任何时候，我们应该只能看到前面的元素。因此，STL 不为此提供迭代器。\n\n## 钳工 工作\n\n正如我们已经看到的，不同的容器有各种利弊，没有一个容器是每种情况下的完美选择。有时，对于给定的场景，多个容器可能会给出相似的平均性能。在这种情况下，标杆就是我们的朋友。这是一个根据统计数据确定更好方法的过程。\n\n考虑一个场景，其中我们希望将数据存储在连续的内存中，访问它，并使用各种函数对它进行操作。可以说要么用`std::vector`，要么用`std::deque`。但是我们不确定哪一个是最好的。乍一看，他们两个似乎都为这种情况给出了很好的表现。在不同的操作中，例如访问、插入、`push_back`和修改特定元素，有些支持`std::vector`，有些支持`std::deque`。那么，我们应该如何进行呢？\n\n想法是创建一个实际模型的小原型，并使用`std::vector`和`std::deque`来实现它。然后，在原型上测量两者的性能。根据性能测试的结果，我们可以选择整体效果更好的一个。\n\n最简单的方法是测量两者执行不同操作所需的时间，并进行比较。然而，在不同的运行期间，相同的操作可能需要不同的时间，因为还有其他因素，例如操作系统调度、缓存和中断等。这些参数会导致我们的结果严重偏离，因为执行一次任何操作都需要几百纳秒。为了克服这一点，我们可以多次执行操作(也就是说，我们的意思是几百万次)，直到我们在两次测量之间获得相当大的时间差。\n\n有一些我们可以使用的基准测试工具，比如 quic[k-bench.com](http://k-bench.com)，它们为我们提供了一种运行基准测试的简单方法。您可以尝试在 vector 和 deque 上运行前面提到的操作，以快速比较性能差异。\n\n### 活动 3:模拟办公室中共享打印机的队列\n\n在本练习中，我们将模拟办公室中共享打印机的队列。在任何公司办公室，打印机通常在打印室的整个楼层共享。这个房间里的所有电脑都连接到同一台打印机上。但是打印机在任何时间点只能做一个打印作业，完成任何作业也需要一定的时间。同时，一些其他用户可以发送另一个打印请求。在这种情况下，打印机需要将所有待处理的作业存储在某个地方，以便在当前任务完成后可以处理它们。\n\n执行以下步骤来解决活动:\n\n1.  创建一个名为`Job`的类(包括作业的标识、提交作业的用户的姓名和页数)。\n2.  创建一个名为`Printer`的类。这将提供一个添加新作业的界面，并处理到目前为止添加的所有作业。\n3.  要实现`printer`类，它需要存储所有未完成的作业。我们将实施一个非常基本的策略——先到先得。谁先提交工作，谁就先完成工作。\n4.  Finally, simulate a scenario where multiple people are adding jobs to the printer, and the printer is processing them one by one.\n\n    #### 注意\n\n    这个活动的解决方案可以在第 487 页找到。\n\n## 总结\n\n在这一章中，我们学习了如何通过选择存储数据的方式，根据应用的需求来设计应用。我们解释了我们可以对数据执行的不同类型的操作，这些操作可以根据操作的频率用作多个数据结构之间比较的参数。我们了解到容器适配器提供了一种非常有用的方式来表明我们在代码中的意图。我们看到，使用作为适配器提供的更具限制性的容器，而不是使用提供更多功能的主容器，在可维护性方面更有效，并且还减少了人为错误。我们详细讲解了各种数据结构–`std::array`、`std::vector`、`std::list`、`std::forward_list`，它们在任何应用开发过程中都是非常频繁的，它们的接口默认由 C++ 提供。这有助于我们编写高效的代码，而无需重新设计整个周期，并使过程更快。\n\n在本章中，我们看到的所有结构在逻辑上都是线性的，也就是说，我们可以从任何元素前进或后退。在下一章中，我们将探索使用这些结构无法轻松解决的问题，并实现新类型的结构来解决这些问题。"
  },
  {
    "path": "docs/cpp-dsal-design-principle/02.md",
    "content": "# 二、树、堆和图\n\n## 学习目标\n\n本章结束时，您将能够:\n\n*   分析和识别可以使用非线性数据结构的地方\n*   实现和操作树结构来表示数据和解决问题\n*   使用各种方法遍历树\n*   实现一个图结构来表示数据和解决问题\n*   基于给定的场景，使用不同的方法表示图\n\n在本章中，我们将研究两种非线性数据结构，即树和图，以及它们如何用于表示现实场景和解决各种问题。\n\n## 简介\n\n在前一章中，我们实现了不同类型的线性数据结构，以线性方式存储和管理数据。在线性结构中，我们最多可以沿两个方向前进或后退。然而，这些结构的范围非常有限，不能用于解决高级问题。在这一章中，我们将探讨一类更高级的问题。我们将看到，我们之前实现的解决方案不够好，无法直接使用。因此，我们将扩展这些数据结构，以制作更复杂的结构，用于表示非线性数据。\n\n看完这些问题后，我们将讨论使用**树**数据结构的基本解决方案。我们将实现不同类型的树来解决不同类型的问题。之后，我们将看一看一种特殊类型的树，叫做**堆**，以及它可能的实现和应用。接下来，我们将看看另一个复杂的结构-**图表**。我们将实现图的两种不同表示。这些结构有助于将现实世界的场景转化为数学形式。然后，我们将应用我们的编程技能和技术来解决与这些场景相关的问题。\n\n对树和图的深刻理解是理解更高级问题的基础。数据库(B-trees)、数据编码/压缩(Huffman tree)、图着色、分配问题、最小距离问题以及许多其他问题都是使用树和图的某些变体来解决的。\n\n现在，让我们看一些不能用线性数据结构表示的问题的例子。\n\n## 非线性问题\n\n借助线性数据结构无法表示的两种主要情况是层次问题和循环依赖。让我们仔细看看这些案例。\n\n### 等级问题\n\n让我们看几个本来就有层次属性的例子。以下是一个组织的结构:\n\n![Figure 2.1: Organization structure](img/C14498_02_01.jpg)\n\n###### 图 2.1:组织结构\n\n我们可以看到，CEO 是公司的负责人，管理副总监。副主任领导另外三名官员，以此类推。\n\n数据本质上是分层的。这种类型的数据很难用简单的数组、向量或链表来管理。为了巩固我们的理解，让我们看看另一个用例；也就是一个大学课程的结构，如下图所示:\n\n![](img/C14498_02_02.jpg)\n\n###### 图 2.2:大学课程结构中的课程层次结构\n\n上图显示了一所假设大学中某些课程的课程依赖关系。我们可以看到，要学习《高等物理 II》，学生必须顺利完成以下课程:高等物理和高等数学。同样，许多其他课程也有自己的先决条件。\n\n给定这样的数据，我们可以有不同类型的查询。例如，我们可能想找出哪些课程需要成功完成，这样我们就可以学习《高等数学》。\n\n这类问题可以用一种叫做树的数据结构来解决。所有的对象都被称为树的节点，而从一个节点到另一个节点的路径被称为边。我们将在本章稍后的*图表*部分对此进行更深入的研究。\n\n### 循环依赖\n\n让我们看看另一个复杂的现实场景，它可以用非线性结构更好地表示。下图代表了几个人之间的友谊:\n\n![Figure 2.3: A network of friends](img/C14498_02_03.jpg)\n\n###### 图 2.3:朋友网络\n\n这种结构称为图。人名或元素被称为节点，它们之间的关系被表示为边。这样的结构通常被各种社交网络用来代表他们的用户和他们之间的联系。我们可以观察到，爱丽丝是查理的朋友，查理是艾德的朋友，艾德是格蕾丝的朋友，等等。我们还可以推断爱丽丝、鲍勃和查理彼此认识。我们也可以推断，艾德对于格蕾丝来说是一级连接，查理是二级连接，爱丽丝和鲍勃是三级连接。\n\n图表有用的另一个领域是当我们想要表示城市之间的道路网络时，正如您将在本章后面的*图表*部分看到的那样。\n\n## tree–颠倒了！\n\n正如我们在上一节中所讨论的，树只不过是通过某种层次关系连接到其他节点的一些对象或节点。如果我们以图的方式显示这个层次，它看起来像一棵树，而不同的边看起来像它的分支。主节点不依赖于任何其他节点，也称为根节点，通常在顶部表示。所以，不像真正的树，这棵树是颠倒的，根在它的顶部！\n\n让我们尝试为一个非常基本的组织层次结构构建一个结构。\n\n### 练习 7:创建组织结构\n\n在本练习中，我们将实现在本章介绍中看到的组织树的基本版本。让我们开始吧:\n\n1.  首先，让我们包含所需的标题:\n\n    ```cpp\n    #include <iostream>\n    #include <queue>\n    ```\n\n2.  For simplicity, we'll assume that any person can have, at most, two subordinates. We'll see that this is not difficult to extend to resemble real-life situations. This kind of tree is also known as a **binary tree**. Let's write a basic structure for that:\n\n    ```cpp\n    struct node\n    {\n        std::string position;\n        node *first, *second;\n    };\n    ```\n\n    正如我们所看到的，任何节点都有两个到其他节点的链接——都是它们的下属。通过这样做，我们可以显示数据的递归结构。我们现在只存储这个位置，但是我们可以很容易地扩展到包括这个位置的名字，甚至包括这个位置的人的所有信息的整个结构。\n\n3.  我们不希望最终用户处理这种原始数据结构。所以，让我们用一个叫做`org_tree` :\n\n    ```cpp\n    struct org_tree\n    {\n        node *root;\n    ```\n\n    的界面来包装它\n4.  Now, let's add a function to create the root, starting with the highest commanding officer of the company:\n\n    ```cpp\n    static org_tree create_org_structure(const std::string& pos)\n    {\n        org_tree tree;\n        tree.root = new node{pos, NULL, NULL};\n        return tree;\n    }\n    ```\n\n    这是一个静态函数，只是为了创建树。现在，让我们看看如何扩展树。\n\n5.  Now, we want to add a subordinate of an employee. The function should take two parameters – the name of the already existing employee in the tree and the name of the new employee to be added as a subordinate. But before that, let's write another function that will help us find a particular node based on a value to make our insertion function easier:\n\n    ```cpp\n    static node* find(node* root, const std::string& value)\n    {\n        if(root == NULL)\n            return NULL;\n        if(root->position == value)\n            return root;\n        auto firstFound = org_tree::find(root->first, value);\n        if(firstFound != NULL)\n            return firstFound;\n        return org_tree::find(root->second, value);\n    }\n    ```\n\n    当我们遍历树寻找一个元素时，这个元素要么是我们所在的节点，要么是在右边或左边的子树中。\n\n    因此，我们需要首先检查根节点。如果它不是所需的节点，我们将尝试在左侧子树中找到它。最后，如果我们没有成功做到这一点，我们将查看正确的子树。\n\n6.  Now, let's implement the insertion function. We'll make use of the `find` function in order to reuse the code:\n\n    ```cpp\n    bool addSubordinate(const std::string& manager, const std::string& subordinate)\n    {\n        auto managerNode = org_tree::find(root, manager);\n        if(!managerNode)\n        {\n            std::cout << \"No position named \" << manager << std::endl;\n            return false;\n        }\n        if(managerNode->first && managerNode->second)\n        {\n            std::cout << manager << \" already has 2 subordinates.\" << std::endl;\n            return false;\n        }\n        if(!managerNode->first)\n            managerNode->first = new node{subordinate, NULL, NULL};\n        else\n            managerNode->second = new node{subordinate, NULL, NULL};\n        return true;\n    }\n    };\n    ```\n\n    如我们所见，该函数返回一个布尔值，指示我们是否可以成功插入节点。\n\n7.  Now, let's use this code to create a tree in the `main` function:\n\n    ```cpp\n    int main()\n    {\n        auto tree = org_tree::create_org_structure(\"CEO\");\n        if(tree.addSubordinate(\"CEO\", \"Deputy Director\"))\n            std::cout << \"Added Deputy Director in the tree.\" << std::endl;\n        else\n            std::cout << \"Couldn't add Deputy Director in the tree\" << std::endl;\n        if(tree.addSubordinate(\"Deputy Director\", \"IT Head\"))\n            std::cout << \"Added IT Head in the tree.\" << std::endl;\n        else\n            std::cout << \"Couldn't add IT Head in the tree\" << std::endl;\n        if(tree.addSubordinate(\"Deputy Director\", \"Marketing Head\"))\n            std::cout << \"Added Marketing Head in the tree.\" << std::endl;\n        else\n            std::cout << \"Couldn't add Marketing Head in the tree\" << std::endl;\n        if(tree.addSubordinate(\"IT Head\", \"Security Head\"))\n            std::cout << \"Added Security Head in the tree.\" << std::endl;\n        else\n            std::cout << \"Couldn't add Security Head in the tree\" << std::endl;\n        if(tree.addSubordinate(\"IT Head\", \"App Development Head\"))\n            std::cout << \"Added App Development Head in the tree.\" << std::endl;\n        else\n            std::cout << \"Couldn't add App Development Head in the tree\" << std::endl;\n    if(tree.addSubordinate(\"Marketing Head\", \"Logistics Head\"))\n            std::cout << \"Added Logistics Head in the tree.\" << std::endl;\n        else\n            std::cout << \"Couldn't add Logistics Head in the tree\" << std::endl;\n        if(tree.addSubordinate(\"Marketing Head\", \"Public Relations Head\"))\n            std::cout << \"Added Public Relations Head in the tree.\" << std::endl;\n        else\n            std::cout << \"Couldn't add Public Relations Head in the tree\" << std::endl;\n        if(tree.addSubordinate(\"Deputy Director\", \"Finance Head\"))\n            std::cout << \"Added Finance Head in the tree.\" << std::endl;\n        else\n            std::cout << \"Couldn't add Finance Head in the tree\" << std::endl;\n    }\n    ```\n\n    在执行前面的代码时，您应该会得到以下输出:\n\n    ```cpp\n    Added Deputy Director in the tree.\n    Added IT Head in the tree.\n    Added Marketing Head in the tree.\n    Added Security Head in the tree.\n    Added App Development Head in the tree.\n    Added Logistics Head in the tree.\n    Added Public Relations Head in the tree.\n    Deputy Director already has 2 subordinates.\n    Couldn't add Finance Head in the tree\n    ```\n\n下图显示了该输出:\n\n![Figure 2.4: Binary family tree based on an organization’s hierarchy](img/C14498_02_04.jpg)\n\n###### 图 2.4:基于组织层次结构的二叉树\n\n到目前为止，我们只是插入了元素。现在，我们来看看如何穿过这棵树。虽然我们已经看到了如何使用`find`函数遍历，但这只是我们可以做到的方法之一。我们可以用许多其他方式遍历树，所有这些我们将在下一节中讨论。\n\n### 穿越树木\n\n一旦我们有了一棵树，就有各种方法可以遍历它并到达我们需要的节点。让我们简单看一下各种遍历方法:\n\n*   **Preorder traversal**: In this method, we visit the current node first, followed by the left child of the current node, and then the right child of the current node in a recursive fashion. Here, the prefix \"pre\" indicates that the parent node is visited before its children. Traversing the tree shown in *figure 2.4* using the preorder method goes like this:\n\n    ```cpp\n    CEO, Deputy Director, IT Head, Security Head, App Development Head, Marketing Head, Logistics Head, Public Relations Head,\n    ```\n\n    正如我们所看到的，我们总是先访问父节点，然后是左子节点，然后是右子节点。我们这样做不仅仅是为了根，也是为了相对于它的子树的任何节点。我们使用这样一个函数来实现前序遍历:\n\n    ```cpp\n    static void preOrder(node* start)\n    {\n        if(!start)\n            return;\n        std::cout << start->position << \", \";\n        preOrder(start->first);\n        preOrder(start->second);\n    }\n    ```\n\n*   **In-order traversal**: In this type of traversal, first we'll visit the left node, then the parent node, and finally the right node. Traversing the tree that's shown in *figure 2.4* goes like this:\n\n    ```cpp\n    Security Head, IT Head, App Development Head, Deputy Director, Logistics Head, Marketing Head, Public Relations Head, CEO, \n    ```\n\n    我们可以用这样的函数来实现:\n\n    ```cpp\n    static void inOrder(node* start)\n    {\n        if(!start)\n            return;\n        inOrder(start->first);\n    std::cout << start->position << \", \";\n        inOrder(start->second);\n    }\n    ```\n\n*   **Post-order traversal**: In this traversal, we first visit both the children, followed by the parent node. Traversing the tree that's shown in *figure 2.4* goes like this:\n\n    ```cpp\n    Security Head, App Development Head, IT Head, Logistics Head, Public Relations Head, Marketing Head, Deputy Director, CEO, \n    ```\n\n    我们可以用这样的函数来实现:\n\n    ```cpp\n    static void postOrder(node* start)\n    {\n        if(!start)\n            return;\n        postOrder(start->first);\n        postOrder(start->second);\n        std::cout << start->position << \", \";\n    }\n    ```\n\n*   **Level order traversal**: This requires us to traverse the tree level by level, from top to bottom, and from left to right. This is similar to listing the elements at each level of the tree, starting from the root level. The results of such a traversal are usually represented as per the levels, as shown here:\n\n    ```cpp\n    CEO, \n    Deputy Director, \n    IT Head, Marketing Head, \n    Security Head, App Development Head, Logistics Head, Public Relations Head, \n    ```\n\n    在下面的练习中演示了这种遍历方法的实现。\n\n### 练习练习 8:演示层级顺序遍历\n\n在本练习中，我们将在我们在*练习 7* 、*创建组织结构*中创建的组织结构中实现层级顺序遍历。与前面的遍历方法不同，这里我们不遍历直接连接到当前节点的节点。这意味着遍历在没有递归的情况下更容易实现。我们将扩展在*练习 7* 中显示的代码来演示这个遍历。让我们开始吧:\n\n1.  First, we'll add the following function inside the `org_tree` structure from *Exercise 7*:\n\n    ```cpp\n    static void levelOrder(node* start)\n    {\n        if(!start)\n            return;\n        std::queue<node*> q;\n        q.push(start);\n        while(!q.empty())\n        {\n            int size = q.size();\n            for(int i = 0; i < size; i++)\n            {\n                auto current = q.front();\n                q.pop();\n                std::cout << current->position << \", \";\n                if(current->first)\n                    q.push(current->first);\n                if(current->second)\n                    q.push(current->second);\n            }\n            std::cout << std::endl;\n        }\n    }\n    ```\n\n    如前面的代码所示，首先，我们遍历根节点，然后遍历其子节点。在访问孩子时，我们会在当前级别完成后将他们的孩子推到队列中进行处理。其思想是从第一级开始排队，然后将下一级的节点添加到队列中。我们将继续这样做，直到队列为空，这表明下一级中没有更多节点。\n\n2.  我们的输出应该是这样的:\n\n    ```cpp\n    CEO, \n    Deputy Director, \n    IT Head, Marketing Head, \n    Security Head, App Development Head, Logistics Head, Public Relations Head, \n    ```\n\n## 树木的变种\n\n在前面的练习中，我们主要看了**二叉树**，这是最常见的一种树。在二叉树中，每个节点最多可以有两个子节点。然而，普通的二叉树并不总是服务于这个目的。接下来，我们将看看二叉树的一个更专业的版本，叫做二叉查找树。\n\n### 比娜里搜索树\n\nA **二叉查找树** ( **BST** )是二叉树的流行版本。BST 只不过是一棵二叉树，具有以下属性:\n\n*   父节点的值≥左子节点的值\n*   父节点的值≤右子节点的值\n\n简而言之，左子≤父≤右子。\n\n这就引出了一个有趣的特点。在任何时间点，我们总是可以说，所有小于或等于父节点的元素都将在左侧，而大于或等于父节点的元素将在右侧。因此，搜索一个元素的问题在每一步都在减少一半，就搜索空间而言。\n\n如果 BST 的构造方式是，除了最后一级的元素外，所有元素都有两个子元素，那么树的高度将是 *log n* ，其中 *n* 是元素的数量。因此，搜索和插入的时间复杂度为 *O(对数 n)* 。这种类型的二叉树也被称为**完全二叉树**。\n\n**在基站中搜索**\n\n让我们看看如何在二叉查找树中搜索、插入和删除元素。考虑一个具有唯一正整数的 BST，如下图所示:\n\n![Figure 2.5: Searching for an element in a binary search tree](img/C14498_02_05.jpg)\n\n###### 图 2.5:在二叉查找树中搜索元素\n\n假设我们必须搜索 7。从上图中箭头所代表的步骤中我们可以看到，在将值与当前节点的数据进行比较后，我们选择了边。正如我们已经提到的，左边的所有节点总是小于当前节点，右边的所有节点总是大于当前节点。\n\n因此，我们从比较根节点和 7 开始。如果它大于 7，我们移动到左边的子树，因为那里的所有元素都小于父节点，反之亦然。我们比较每个子节点，直到我们偶然发现 7，或者一个小于 7 的节点没有正确的节点。在这种情况下，到达节点 4 会到达我们的目标 7。\n\n如我们所见，我们没有穿越整棵树。相反，每次当前节点不是所需节点时，我们都会将范围缩小一半，这是通过选择左侧或右侧来实现的。这类似于线性结构的二分搜索法，我们将在*章节**4**分治*中了解。\n\n**在 BST 中插入新元素**\n\n现在，让我们看看插入是如何工作的。步骤如下图所示:\n\n![Figure 2.6: Inserting an element into a binary search tree](img/C14498_02_06.jpg)\n\n###### 图 2.6:将元素插入二叉查找树\n\n如您所见，首先，我们必须找到要插入新值的父节点。因此，我们必须采取类似于我们搜索的方法；也就是说，从根节点开始，通过将每个节点与我们的新元素进行比较来确定方向。最后一步，18 大于 17，但是 17 没有合适的孩子。因此，我们在那个位置插入 18。\n\n**从 BST 中删除元素**\n\n现在，让我们看看删除是如何工作的。考虑以下英国标准时间:\n\n![Figure 2.7: Binary search tree rooted at 12](img/C14498_02_07.jpg)\n\n###### 图 2.7:12 岁的二叉查找树\n\n我们将删除树中的根节点 12。让我们看看如何删除任何值。这比插入要复杂一点，因为我们需要找到被删除节点的替换，以便 BST 的属性保持真实。\n\n第一步是找到要删除的节点。之后，有三种可能:\n\n*   该节点没有子节点:只需删除该节点。\n*   该节点只有一个子节点:将父节点的相应指针指向唯一存在的子节点。\n*   该节点有两个子节点:在这种情况下，我们用它的后继节点替换当前节点。\n\n后继节点是当前节点之后的下一个最大数字。或者，换句话说，后继元素是所有元素中大于当前元素的最小元素。因此，我们将首先去右边的子树，它包含所有比当前元素大的元素，并找到其中最小的元素。找到最小的节点意味着尽可能地去子树的左边，因为左边的子节点总是比它的父节点少。在*图 2.7* 所示的树中，12 的右子树从 18 开始。所以，我们从那里开始看，然后试着向下移动到左边 15 岁的孩子。但是 15 号没有留下孩子，另一个孩子 16 号比 15 号大。因此，15 岁应该是这里的接班人。\n\n要将 12 替换为 15，首先，我们将在删除 12 的同时复制根处的后继值，如下图所示:\n\n![Figure 2.8: Successor copied to the root node](img/C14498_02_08.jpg)\n\n###### 图 2.8:复制到根节点的后继节点\n\n接下来，我们需要从右侧子树的旧位置删除后继节点 15，如下图所示:\n\n![Figure 2.9: Successor deleted from its old place](img/C14498_02_09.jpg)\n\n###### 图 2.9:从原来位置删除的继任者\n\n在最后一步，我们删除节点 15。我们对这个删除也使用相同的过程。由于 15 岁只有一个孩子，我们用 15 岁的孩子代替了 18 岁的左孩子。因此，以 16 为根的整个子树成为 18 的左子树。\n\n#### 注意\n\n后续节点最多只能有一个子节点。如果它有一个左子节点，我们会选择该子节点而不是当前节点作为后继节点。\n\n### 树上操作的时间复杂性\n\n现在，让我们看看这些函数的时间复杂性。理论上，我们可以说我们每次都将搜索范围缩小了一半。因此，搜索具有 *n 个*节点的基站所需的时间是 *T(n) = T(n / 2) + 1* 。该方程导致时间复杂度为 *T(n) = O(对数 n)* 。\n\n但这其中有蹊跷。如果我们仔细观察插入函数，插入的顺序实际上决定了树的形状。而且我们总是会把搜索范围缩小一半也不一定是真的，就像前面公式中 *T(n/2)* 描述的那样。因此，复杂性 *O(对数 n)* 并不总是准确的。我们将在*平衡树*部分更深入地研究这个问题及其解决方案，在这里我们将看到如何更准确地计算时间复杂度。\n\n现在，让我们实现刚刚在 C++ 中看到的操作。\n\n### 练习 9:实现二叉查找树\n\n在本练习中，我们将实现*图 2.7* 中所示的 BST，并添加一个`find`函数来搜索元素。我们还将尝试插入和删除元素，如前面小节所述。让我们开始吧:\n\n1.  首先，让我们包含所需的标题:\n\n    ```cpp\n    #include <iostream>\n    ```\n\n2.  现在，让我们写一个节点。这将类似于我们之前的练习，除了我们将有一个整数而不是字符串:\n\n    ```cpp\n    struct node\n    {\n        int data;\n        node *left, *right;\n    };\n    ```\n\n3.  现在，让我们在节点上添加一个包装器来提供一个干净的接口:\n\n    ```cpp\n    struct bst\n    {\n        node* root = nullptr;\n    ```\n\n4.  Before writing the insertion function, we'll need to write the `find` function:\n\n    ```cpp\n    node* find(int value)\n    {\n        return find_impl(root, value);\n    }\n        private:\n    node* find_impl(node* current, int value)\n    {\n        if(!current)\n        {\n            std::cout << std::endl;\n            return NULL;\n        }\n        if(current->data == value)\n        {\n            std::cout << \"Found \" << value << std::endl;\n            return current;\n        }\n        if(value < current->data)  // Value will be in the left subtree\n        {\n            std::cout << \"Going left from \" << current->data << \", \";\n            return find_impl(current->left, value);\n        }\n        if(value > current->data) // Value will be in the right subtree\n        {\n            std::cout << \"Going right from \" << current->data << \", \";\n            return find_impl(current->right, value);\n        }\n    }\n    ```\n\n    由于这是递归的，我们将实现保存在一个单独的函数中，并使其私有，以防止有人直接使用它。\n\n5.  Now, let's write an `insert` function. It will be similar to the `find` function, but with small tweaks. First, let's find the parent node, which is where we want to insert the new value:\n\n    ```cpp\n    public:\n    void insert(int value)\n    {\n        if(!root)\n            root = new node{value, NULL, NULL};\n        else\n            insert_impl(root, value);\n    }\n    private:\n    void insert_impl(node* current, int value)\n    {\n        if(value < current->data)\n        {\n            if(!current->left)\n                current->left = new node{value, NULL, NULL};\n            else\n                insert_impl(current->left, value);\n        }\n        else\n        {\n            if(!current->right)\n                current->right = new node{value, NULL, NULL};\n                else\n                    insert_impl(current->right, value);\n        }\n    }\n    ```\n\n    正如我们所看到的，我们正在检查该值应该插入左还是右子树。如果期望的一侧什么都没有，我们直接在那里插入节点；否则，我们递归调用该侧的`insert`函数。\n\n6.  现在，让我们编写一个`inorder`遍历函数。当应用于 BST 时，有序遍历提供了一个重要的优势，正如我们将在输出中看到的:\n\n    ```cpp\n    public:\n    void inorder()\n    {\n        inorder_impl(root);\n    }\n    private:\n    void inorder_impl(node* start)\n    {\n        if(!start)\n            return;\n        inorder_impl(start->left);        // Visit the left sub-tree\n        std::cout << start->data << \" \";  // Print out the current node\n        inorder_impl(start->right);       // Visit the right sub-tree\n    }\n    ```\n\n7.  Now, let's implement a utility function to get the successor:\n\n    ```cpp\n    public:\n    node* successor(node* start)\n    {\n        auto current = start->right;\n        while(current && current->left)\n            current = current->left;\n        return current;\n    }\n    ```\n\n    这遵循了我们在*删除 BST* 小节中讨论的逻辑。\n\n8.  现在来看看`delete`的实际实现。因为删除需要重新初始化父节点，所以我们将通过每次返回新节点来实现。我们将通过放置一个更好的接口来隐藏这种复杂性。我们将命名接口`deleteValue`，因为`delete`是一个保留的关键字，按照 C++ 标准:\n\n    ```cpp\n    void deleteValue(int value)\n    {\n        root = delete_impl(root, value);\n    }\n    private:\n    node* delete_impl(node* start, int value)\n    {\n        if(!start)\n            return NULL;\n        if(value < start->data)\n            start->left = delete_impl(start->left, value);\n        else if(value > start->data)\n            start->right = delete_impl(start->right, value);\n        else\n        {\n            if(!start->left)  // Either both children are absent or only left child is absent\n            {\n                auto tmp = start->right;\n                delete start;\n                return tmp;\n            }\n            if(!start->right)  // Only right child is absent\n            {\n                auto tmp = start->left;\n                delete start;\n                return tmp;\n            }\n            auto succNode = successor(start);\n            start->data = succNode->data;\n            // Delete the successor from right subtree, since it will always be in the right subtree\n            start->right = delete_impl(start->right, succNode->data);\n        }\n        return start;\n    }\n    };\n    ```\n\n9.  Let's write the `main` function so that we can use the BST:\n\n    ```cpp\n    int main()\n    {\n        bst tree;\n        tree.insert(12);\n        tree.insert(10);\n        tree.insert(20);\n        tree.insert(8);\n        tree.insert(11);\n        tree.insert(15);\n        tree.insert(28);\n        tree.insert(4);\n        tree.insert(2);\n        std::cout << \"Inorder: \";\n        tree.inorder();  // This will print all the elements in ascending order\n        std::cout << std::endl;\n        tree.deleteValue(12);\n        std::cout << \"Inorder after deleting 12: \";\n        tree.inorder();  // This will print all the elements in ascending order\n        std::cout << std::endl;\n        if(tree.find(12))\n            std::cout << \"Element 12 is present in the tree\" << std::endl;\n        else\n            std::cout << \"Element 12 is NOT present in the tree\" << std::endl;\n    }\n    ```\n\n    执行上述代码时的输出应该如下所示:\n\n    ```cpp\n    Inorder: 2 4 8 10 11 12 15 20 28 \n    Inorder after deleting 12: 2 4 8 10 11 15 20 28 \n    Going left from 15, Going right from 10, Going right from 11, \n    Element 12 is NOT present in the tree\n    ```\n\n观察前面的顺序遍历的结果。按顺序将首先访问左边的子树，然后访问当前节点，然后递归地访问右边的子树，如代码片段中的注释所示。因此，根据 BST 属性，我们将首先访问所有小于当前值的值，然后是当前值，之后，我们将访问所有大于当前值的值。由于这是递归发生的，我们将按照升序对数据进行排序。\n\n### 平衡树\n\n在我们理解平衡树之前，让我们从一个 BST 的例子开始，其插入顺序如下:\n\n```cpp\nbst tree;\ntree.insert(10);\ntree.insert(9);\ntree.insert(11);\ntree.insert(8);\ntree.insert(7);\ntree.insert(6);\ntree.insert(5);\ntree.insert(4);\n```\n\n在下图的帮助下，该基站可以可视化:\n\n![Figure 2.10: Skewed binary search tree](img/C14498_02_10.jpg)\n\n###### 图 2.10:倾斜的二叉查找树\n\n如上图所示，几乎整棵树都向左侧倾斜。如果调用`find`函数，即`bst.find(4)`，步骤如下:\n\n![Figure 2.11: Finding an element in a skewed binary search tree](img/C14498_02_11.jpg)\n\n###### 图 2.11:在倾斜的二叉查找树中找到一个元素\n\n我们可以看到，步骤的数量几乎等于元素的数量。现在，让我们用不同的插入顺序再次尝试同样的事情，如下所示:\n\n```cpp\nbst tree;\ntree.insert(7);\ntree.insert(5);\ntree.insert(9);\ntree.insert(4);\ntree.insert(6);\ntree.insert(10);\ntree.insert(11);\ntree.insert(8);\n```\n\n查找元素 4 所需的 BST 和步骤现在将如下所示:\n\n![Figure 2.12: Finding an element in a balanced tree](img/C14498_02_12.jpg)\n\n###### 图 2.12:在平衡树中找到一个元素\n\n如我们所见，这棵树不再倾斜了。或者，换句话说，树是平衡的。这种配置大大减少了查找 4 的步骤。因此，`find`的时间复杂度不仅取决于元素的数量，还取决于它们在树中的配置。如果我们仔细看台阶，我们总是朝着树的底部走一步，同时寻找一些东西。最后，我们到达叶节点(没有任何子节点的节点)。这里，我们根据元素的可用性返回所需的节点或空值。所以，我们可以说台阶的数量总是小于 BST 中的最大层数，也就是 BST 的高度。所以，寻找一个元素的实际时间复杂度是 O(高度)。\n\n为了优化时间复杂度，我们需要优化树的高度。这也叫*平衡一棵树*。其思想是在插入/删除后重新组织节点，以减少树的偏斜度。生成的树被称为高度平衡的 BST。\n\n有各种方法可以做到这一点，并获得不同类型的树，如 AVL 树、红黑树等。AVL 树背后的想法是执行一些旋转来平衡树的高度，同时仍然保持 BST 属性。考虑下图所示的示例:\n\n![Figure 2.13: Rotating a tree](img/C14498_02_13.jpg)\n\n###### 图 2.13:旋转树\n\n正如我们所看到的，右边的树比左边的树更平衡。轮换不在本书的讨论范围之内，因此我们不会冒险讨论这个例子的细节。\n\n### N-和树\n\n到目前为止，我们主要看到了二叉树或它们的变种。对于 N 元树，每个节点可以有 *N 个*子节点。由于 *N* 在这里是任意的，我们将把它存储在一个向量中。最后的结构看起来像这样:\n\n```cpp\nstruct nTree\n{\n    int data;\n    std::vector<nTree*> children;\n};\n```\n\n如我们所见，每个节点可以有任意数量的子节点。因此，整棵树是完全任意的。然而，就像普通的二叉树一样，普通的 N 元树也不是很有用。因此，我们必须为不同类型的应用构建一个不同的树，其中层次比二叉树的层次更高。在*图 2.1* 中显示的例子，代表了一个组织的层次结构，是一个 N 元树。\n\n在计算机世界中，有两种非常好的、众所周知的 N 元树实现，如下所示:\n\n*   计算机中的文件系统结构:从 Linux 中的`root` ( `/`)或 Windows 中的驱动器开始，我们可以在任何文件夹中拥有任意数量的文件(终端节点)和任意数量的文件夹。我们将在*活动 1，为文件系统*创建数据结构中更详细地了解这一点。\n*   编译器:大多数编译器基于用于源代码的标准定义的语法来构建抽象语法树。编译器通过解析 AST 生成低级代码。\n\n### 活动 4:为文件系统创建数据结构\n\n使用 N 元树为支持以下操作的文件系统创建数据结构:转到目录、查找文件/目录、添加文件/目录和列出文件/目录。我们的树将保存文件系统中所有元素(文件和文件夹)的信息和文件夹层次结构(路径)。\n\n执行以下步骤来解决此活动:\n\n1.  创建一个 N 元树，在一个节点中包含两个数据元素——目录/文件的名称和一个指示它是目录还是文件的标志。\n2.  添加一个数据成员来存储当前目录。\n3.  用单个根目录(`/`)初始化树。\n4.  添加查找目录/文件功能，该功能采用单个参数–`path`。`path`可以是绝对的(从`/`开始)也可以是相对的。\n5.  添加功能以添加文件/目录并列出位于给定路径的文件/目录。\n6.  Similarly, add a function to change the current directory.\n\n    #### 注意\n\n    这个活动的解决方案可以在第 490 页找到。\n\n我们已经打印了前面带有`d`的目录，以区别于前面带有“`–`”(连字符)的文件。您可以尝试创建更多具有绝对或相对路径的目录和文件。\n\n到目前为止，我们还没有支持某些 Linux 约定，比如用单点寻址任何目录，用双点寻址父目录。这可以通过扩展我们的节点来实现，也可以保存一个指向其父节点的指针。这样，我们可以非常容易地在两个方向上遍历。还有各种其他可能的扩展，例如添加符号链接，以及使用“`*`”扩展各种文件/目录名称的 globing 运算符。这个练习为我们提供了一个基础，这样我们就可以根据我们的需求自己构建一些东西。\n\n## 堆\n\n在前一章中，我们简单介绍了堆以及 C++ 如何通过 STL 提供堆。在这一章中，我们将深入研究堆。简单回顾一下，以下是预期的时间复杂性:\n\n*   *O(1)* :立即访问最大元素\n*   *O(log n)* :插入任何元素\n*   *0(对数 n)* :删除最大元素\n\n为了实现 *O(log n)* 的插入/删除，我们将使用一棵树来存储数据。但在这种情况下，我们将使用完整的树。一个**完全树**被定义为这样一个树，其中除了最后一级之外的所有级别的节点都有两个子节点，并且最后一级在左侧具有尽可能多的元素。例如，考虑下图所示的两棵树:\n\n![Figure 2.14: Complete versus non-complete tree](img/C14498_02_14.jpg)\n\n###### 图 2.14:完整树与非完整树\n\n因此，只要有足够的空间，可以通过在最后一层插入元素来构建一个完整的树。如果没有，我们将在新级别的最左边位置插入它们。这给了我们一个很好的机会，用数组一级一级地存储这个树。因此，树的根将是数组/向量的第一个元素，然后是它的左子元素，然后是右子元素，依此类推。与其他树不同，这是一种非常高效的内存结构，因为不需要额外的内存来存储指针。要从父节点转到它的子节点，我们可以很容易地使用数组的索引。如果父节点是第 *i* *第*节点，则其子节点将始终是 *2*i + 1* 和 *2*i + 2* 索引。同样，我们可以通过使用*(I–1)/2*来获取 *i* *第*子节点的父节点。我们也可以从上图中证实这一点。\n\n现在，让我们看看每次插入/删除时需要维护的不变量(或条件)。第一个要求是即时访问 max 元素。为此，我们需要确定它的位置，以便每次都能立即访问它。我们将始终将 max 元素保持在顶部–根位置。现在，为了保持这一点，我们还需要保持另一个不变量——父节点必须大于它的两个子节点。这样的堆也称为**最大堆**。\n\n正如您可能猜到的，快速访问最大元素所需的属性可以很容易地反转，以便快速访问最小元素。我们所需要做的就是在执行堆操作时反转我们的比较函数。这种堆被称为 **min 堆**。\n\n### 堆操作\n\n在本节中，我们将看到如何在堆上执行不同的操作。\n\n**将元素插入堆中**\n\n作为插入的第一步，我们将保留最重要的不变量，这为我们提供了一种将这个结构表示为数组的方法——一个完整的树。这可以很容易地通过在末尾插入新元素来完成，因为它将代表最后一级中的元素，就在所有现有元素之后，或者如果当前最后一级已满，则作为新级别中的第一个元素。\n\n现在，我们需要保留另一个不变量——如果可用，所有节点的值都必须大于它们的两个子节点。假设我们当前的树已经遵循这个不变量，在最后一个位置插入新元素之后，唯一一个不变量可能失败的元素将是最后一个元素。为了解决这个问题，如果父元素比元素小，我们就用它的父元素交换元素。即使父元素已经有另一个元素，它也会比新元素小(新元素>父元素>子元素)。\n\n因此，通过将新元素视为根而创建的子树满足所有不变量。但是，新元素可能仍然大于它的新父元素。因此，我们需要继续交换节点，直到整个树满足不变量。由于一棵完整的树的高度最多为 *O(log n)* ，整个操作最多需要 *O(log n)* 的时间。下图说明了向树中插入元素的操作:\n\n![Figure 2.15: Inserting an element into a heap with one node](img/C14498_02_15.jpg)\n\n###### 图 2.15:将一个元素插入具有一个节点的堆中\n\n如上图所示，在插入 11 之后，树不再具有 heap 属性。因此，我们将交换 10 和 11，使其再次成为堆。这个概念在下面的例子中更清晰，这个例子有更多的层次:\n\n![Figure 2.16: Inserting an element into a heap with several nodes](img/C14498_02_16.jpg)\n\n###### 图 2.16:将一个元素插入到具有几个节点的堆中\n\n**从堆中删除元素**\n\n首先要注意的是，我们只能删除 max 元素。我们不能直接接触任何其他元素。max 元素始终出现在根处。因此，我们将移除根元素。但是我们也需要决定谁来承担它的责任。为此，我们首先需要用最后一个元素交换根，然后移除最后一个元素。这样，我们的根将被删除，但它将打破每个父节点大于其子节点的不变性。为了解决这个问题，我们将把根与其两个子节点进行比较，然后用更大的子节点替换它。现在，不变量在其中一个子树处被打破。我们在整个子树中递归地继续交换过程。这样，不变量的断点就沿着树向下冒泡了。就像插入一样，我们遵循这个规则，直到遇到不变量。所需的最大步数将等于树的高度，即 *O(log n)* 。下图说明了这一过程:\n\n![Figure 2.17: Deleting an element in a heap](img/C14498_02_17.jpg)\n\n###### 图 2.17:删除堆中的元素\n\n**堆的初始化**\n\n现在，让我们看看最重要的步骤之一——堆的初始化。与向量、列表、deq 等不同，堆的初始化并不简单，因为我们需要维护堆的不变量。一个简单的解决方案是从一个空堆开始逐个插入所有元素。但这需要的时间是 *O(n * log(n))* ，效率不高。\n\n然而，有一个**堆积**算法可以在*0(n)*时间内完成。这背后的想法非常简单:我们不断更新树，以自下而上的方式匹配较小子树的堆属性。首先，最后一级已经具有堆的属性。接下来，我们一级一级地向根前进，使每个子树一个接一个地遵循堆属性。这个过程只有 *O(n)* 的时间复杂度。幸运的是，C++ 标准已经为此提供了一个名为`std::make_heap`的函数，它可以接受任何数组或向量迭代器，并将它们转换成堆。\n\n### 练习 10:流中位数\n\n在本练习中，我们将解决一个有趣的问题，这个问题经常出现在与数据分析相关的应用中，包括机器学习。想象一下，某个数据源一次给我们一个连续的数据元素(数据流)。我们需要在接收到每一个元素之后，找到到目前为止已经接收到的元素的中间值。一种简单的方法是每次有新元素进来时对数据进行排序，并返回中间的元素。但由于排序，这将有一个时间复杂度。根据传入元素的速率，这可能非常耗费资源。然而，我们将在堆的帮助下对此进行优化。让我们开始吧:\n\n1.  让我们首先包含所需的标题:\n\n    ```cpp\n    #include <iostream>\n    #include <queue>\n    #include <vector>\n    ```\n\n2.  现在，让我们编写一个容器来存储到目前为止收到的数据。我们将数据存储在两个堆中——一个最小堆和一个最大堆。我们将把较小的元素的前半部分存储在一个最大堆中，把较大的或者另一半存储在一个最小堆中。因此，在任何时候，都可以仅使用堆的顶部元素来计算中位数，这些元素很容易访问:\n\n    ```cpp\n    struct median\n    {\n        std::priority_queue<int> maxHeap;\n        std::priority_queue<int, std::vector<int>, std::greater<int>> minHeap;\n    ```\n\n3.  现在，让我们编写一个`insert`函数，以便插入新到达的数据:\n\n    ```cpp\n    void insert(int data)\n    {\n        // First element\n        if(maxHeap.size() == 0)\n        {\n            maxHeap.push(data);\n            return;\n        }\n        if(maxHeap.size() == minHeap.size())\n        {\n            if(data <= get())\n                maxHeap.push(data);\n            else\n                minHeap.push(data);\n            return;\n        }\n        if(maxHeap.size() < minHeap.size())\n        {\n            if(data > get())\n            {\n                maxHeap.push(minHeap.top());\n                minHeap.pop();\n                minHeap.push(data);\n            }\n            else\n                maxHeap.push(data);\n            return;\n        }\n        if(data < get())\n        {\n            minHeap.push(maxHeap.top());\n            maxHeap.pop();\n            maxHeap.push(data);\n        }\n        else\n            minHeap.push(data);\n    }\n    ```\n\n4.  现在，让我们写一个`get`函数，这样我们就可以从容器中得到中位数:\n\n    ```cpp\n    double get()\n    {\n        if(maxHeap.size() == minHeap.size())\n            return (maxHeap.top() + minHeap.top()) / 2.0;\n        if(maxHeap.size() < minHeap.size())\n            return minHeap.top();\n        return maxHeap.top();\n    }\n    };\n    ```\n\n5.  Now, let's write a `main` function so that we can use this class:\n\n    ```cpp\n    int main()\n    {\n        median med;\n        med.insert(1);\n        std::cout << \"Median after insert 1: \" << med.get() << std::endl;\n        med.insert(5);\n        std::cout << \"Median after insert 5: \" << med.get() << std::endl;\n        med.insert(2);\n        std::cout << \"Median after insert 2: \" << med.get() << std::endl;\n        med.insert(10);\n        std::cout << \"Median after insert 10: \" << med.get() << std::endl;\n        med.insert(40);\n        std::cout << \"Median after insert 40: \" << med.get() << std::endl;\n        return 0;\n    }\n    ```\n\n    前面程序的输出如下:\n\n    ```cpp\n    Median after insert 1: 1\n    Median after insert 5: 3\n    Median after insert 2: 2\n    Median after insert 10: 3.5\n    Median after insert 40: 5\n    ```\n\n这样，我们只需要插入任何新到达的元素，其时间复杂度仅为 *O(log n)* ，而如果我们用每个新元素对元素进行排序，则时间复杂度为 *O(n log n)* 。\n\n### 活动 5:使用堆的 K 路合并\n\n考虑一个与遗传学相关的生物医学应用，用于处理大型数据集。它需要以排序的方式排列 DNA 来计算相似性。但是由于数据集很大，它不能放在一台机器上。因此，它在分布式集群中处理和存储数据，每个节点都有一组排序值。主处理引擎要求所有数据以排序的方式存储在一个流中。因此，基本上，我们需要将多个排序数组合并成一个排序数组。借助向量模拟这种情况。\n\n执行以下步骤来解决此活动:\n\n1.  最小的数字将出现在所有列表的第一个元素中，因为所有列表已经被单独排序。为了更快地获得最小值，我们将构建一堆这些元素。\n2.  从堆中获取最小元素后，我们需要移除它，并用它所属的同一列表中的下一个元素替换它。\n3.  The heap node must contain information about the list so that it can find the next number from that list.\n\n    #### 注意\n\n    这个活动的解决方案可以在第 495 页找到。\n\n现在，让我们计算前面算法的时间复杂度。如果有 *k* 列表可用，我们的堆大小将为 k，所有的堆操作将为 *O(log k)* 。建筑堆将是 *O(k 原木 k)* 。之后，我们将不得不对结果中的每个元素执行堆操作。总元素为 *n × k* 。因此，总复杂度将为 *O(nk log k)* 。\n\n这个算法的奇妙之处在于，考虑到我们前面描述的现实场景，它实际上并不需要同时存储所有的 *n × k* 元素；它只需要在任意时间点存储 *k* 元素，其中 *k* 是集群中列表或节点的数量。正因如此， *k* 的值永远不会太大。在堆的帮助下，我们可以一次生成一个数字，并立即处理该数字，或者将其流式传输到其他地方进行处理，而无需实际存储它。\n\n## 图表\n\n虽然树 i 是表示分层数据的一种很好的方式，但是我们不能表示树中的循环或循环依赖关系，因为我们总是有一个单一且唯一的路径从一个节点到另一个节点。然而，有更复杂的场景具有固有的循环结构。例如，考虑一个道路网络。从一个地方到另一个地方可以有多种方式(地方可以表示为节点)。这样一组场景可以用图表更好地表示。\n\n与树不同，图必须存储节点以及节点之间的边的数据。例如，在任何道路网络中，对于每个节点(地点)，我们必须存储关于它连接到哪些其他节点(地点)的信息。这样，我们就可以形成一个包含所有需要的节点和边的图。这叫做**未加权图**。我们可以为每个边添加*权重*或更多信息。对于我们的道路网络示例，我们可以添加每个边(路径)从一个节点(位置)到另一个节点(位置)的距离。这种表示法被称为“T4”加权图“T5”，它包含了解决诸如寻找一个地方与另一个地方之间距离最小的路径等问题所需的道路网络的所有信息。\n\n有两种类型的图——无向图和有向图。**无向图**表示边是双向的。双向表示双边或可交换的属性。对于道路网络示例，点 A 和 B 之间的双向边意味着我们可以从 A 到 B，以及从 B 到 A。但是假设我们有一些单向限制的道路–我们需要使用**有向图**来表示。在有向图中，每当我们需要指出我们可以朝任何一个方向前进时，我们都使用两条边——从 A 点到 B 点，以及从 B 点到 A 点。我们将主要关注双向图，但是我们在这里将学习的关于结构和遍历方法的内容对于有向图也同样适用。唯一的变化是我们如何给图添加边。\n\n因为一个图可以有循环边和从一个节点到另一个节点的多条路径，我们需要唯一地识别每个节点。为此，我们可以为每个节点分配一个标识符。为了表示图的数据，我们并不真的需要像在树中那样以编程方式构建类似节点的结构。事实上，我们可以通过组合`std`容器来存储整个图。\n\n### 将图表示为邻接矩阵\n\n这里有一个最简单的理解图的方法——考虑一组节点，其中任何节点都可以直接连接到组中的任何其他节点。这意味着我们可以使用一个 2D 数组来表示这一点，对于一个具有 *N* 节点的图来说，这个数组的大小为 *N × N* 。每个单元格中的值将根据单元格的索引指示相应节点之间的边的权重。因此，`data[1][2]`将指示节点 1 和节点 2 之间的边的权重。这种方法被称为**邻接矩阵**。我们可以使用-1 的权重来表示没有边。\n\n考虑下图所示的加权图，它代表几个主要国际城市之间的航空网络，带有假设的距离:\n\n![Figure 2.18: Aviation network between some cities](img/C14498_02_18.jpg)\n\n###### 图 2.18:部分城市间的航空网络\n\n如上图所示，我们可以从伦敦经伊斯坦布尔或直接去迪拜。从一个地方到另一个地方有多种方式，而树木不是这样。此外，我们可以从一个节点遍历到另一个节点，并通过一些不同的边回到原始节点，这在树中也是不可能的。\n\n让我们为上图所示的图实现矩阵表示方法。\n\n### 练习 11:实现一个图并将其表示为邻接矩阵\n\n在本练习中，我们将实现一个代表上图所示城市网络的图，并演示如何将其存储为邻接矩阵。让我们开始吧:\n\n1.  首先，让我们包含所需的标题:\n\n    ```cpp\n    #include <iostream>\n    #include <vector>\n    ```\n\n2.  现在，让我们添加一个`enum`类，以便存储城市名称:\n\n    ```cpp\n    enum class city: int\n    {\n        LONDON,\n        MOSCOW,\n        ISTANBUL,\n        DUBAI,\n        MUMBAI,\n        SEATTLE,\n        SINGAPORE\n    };\n    ```\n\n3.  让我们也为`city`枚举添加一个`<<`运算符:\n\n    ```cpp\n    std::ostream& operator<<(std::ostream& os, const city c)\n    {\n        switch(c)\n        {\n            case city::LONDON:\n                os << \"LONDON\";\n                return os;\n            case city::MOSCOW:\n                os << \"MOSCOW\";\n                return os;\n            case city::ISTANBUL:\n                os << \"ISTANBUL\";\n                return os;\n            case city::DUBAI:\n                os << \"DUBAI\";\n                return os;\n            case city::MUMBAI:\n                os << \"MUMBAI\";\n                return os;\n            case city::SEATTLE:\n                os << \"SEATTLE\";\n                return os;\n            case city::SINGAPORE:\n                os << \"SINGAPORE\";\n                return os;\n            default:\n                return os;\n        }\n    }\n    ```\n\n4.  让我们写`struct graph`，它将封装我们的数据:\n\n    ```cpp\n    struct graph\n    {\n        std::vector<std::vector<int>> data;\n    ```\n\n5.  现在，让我们添加一个构造函数，该构造函数将创建一个具有给定节点数的空图(没有任何边的图):\n\n    ```cpp\n    graph(int n)\n    {\n        data.reserve(n);\n        std::vector<int> row(n);\n        std::fill(row.begin(), row.end(), -1);\n        for(int i = 0; i < n; i++)\n        {\n            data.push_back(row);\n        }\n    }\n    ```\n\n6.  现在，让我们添加最重要的功能–`addEdge`。需要三个参数——要连接的两个城市和边缘的重量(距离):\n\n    ```cpp\n    void addEdge(const city c1, const city c2, int dis)\n    {\n        std::cout << \"ADD: \" << c1 << \"-\" << c2 << \"=\" << dis << std::endl;\n        auto n1 = static_cast<int>(c1);\n        auto n2 = static_cast<int>(c2);\n        data[n1][n2] = dis;\n        data[n2][n1] = dis;\n    }\n    ```\n\n7.  现在，让我们添加一个函数，这样我们就可以从图中移除一条边:\n\n    ```cpp\n    void removeEdge(const city c1, const city c2)\n    {\n        std::cout << \"REMOVE: \" << c1 << \"-\" << c2 << std::endl;\n        auto n1 = static_cast<int>(c1);\n        auto n2 = static_cast<int>(c2);\n        data[n1][n2] = -1;\n        data[n2][n1] = -1;\n    }\n    };\n    ```\n\n8.  现在，让我们编写`main`函数，以便使用这些函数:\n\n    ```cpp\n    int main()\n    {\n        graph g(7);\n        g.addEdge(city::LONDON, city::MOSCOW, 900);\n        g.addEdge(city::LONDON, city::ISTANBUL, 500);\n        g.addEdge(city::LONDON, city::DUBAI, 1000);\n        g.addEdge(city::ISTANBUL, city::MOSCOW, 1000);\n        g.addEdge(city::ISTANBUL, city::DUBAI, 500);\n        g.addEdge(city::DUBAI, city::MUMBAI, 200);\n        g.addEdge(city::ISTANBUL, city::SEATTLE, 1500);\n        g.addEdge(city::DUBAI, city::SINGAPORE, 500);\n        g.addEdge(city::MOSCOW, city::SEATTLE, 1000);\n        g.addEdge(city::MUMBAI, city::SINGAPORE, 300);\n        g.addEdge(city::SEATTLE, city::SINGAPORE, 700);\n        g.addEdge(city::SEATTLE, city::LONDON, 1800);\n        g.removeEdge(city::SEATTLE, city::LONDON);\n        return 0;\n    }\n    ```\n\n9.  在执行这个程序时，我们应该得到如下输出:\n\n    ```cpp\n    ADD: LONDON-MOSCOW=900\n    ADD: LONDON-ISTANBUL=500\n    ADD: LONDON-DUBAI=1000\n    ADD: ISTANBUL-MOSCOW=1000\n    ADD: ISTANBUL-DUBAI=500\n    ADD: DUBAI-MUMBAI=200\n    ADD: ISTANBUL-SEATTLE=1500\n    ADD: DUBAI-SINGAPORE=500\n    ADD: MOSCOW-SEATTLE=1000\n    ADD: MUMBAI-SINGAPORE=300\n    ADD: SEATTLE-SINGAPORE=700\n    ADD: SEATTLE-LONDON=1800\n    REMOVE: SEATTLE-LONDON\n    ```\n\n如我们所见，我们将数据存储在向量的向量中，两个维度都等于节点数。因此，该表示所需的总空间与 *V2* 成比例，其中 *V* 是节点的数量。\n\n### 将一个图表示为邻接表\n\n图的矩阵表示的一个主要问题是所需的内存量与节点数的平方成正比。正如您可能想象的那样，随着节点数量的增加，这种情况会迅速增加。让我们看看如何改进这一点，以便使用更少的内存。\n\n在任何图中，我们都有固定数量的节点，每个节点都有固定的最大连接节点数，等于总节点数。在矩阵中，我们必须存储所有节点的所有边，即使两个节点没有直接连接在一起。相反，我们将只存储每行中节点的标识，指示哪些节点直接连接到当前节点。这种表示也称为**邻接表**。\n\n让我们看看与前面的练习相比，实现有何不同。\n\n### 练习 12:实现一个图并将其表示为邻接表\n\n在本练习中，我们将实现一个代表图 2.18 中所示的城市网络的图，并演示如何将其存储为邻接表。让我们开始吧:\n\n1.  在本练习中，我们将实现邻接表表示。让我们从标题开始，像往常一样:\n\n    ```cpp\n    #include <iostream>\n    #include <vector>\n    #include <algorithm>\n    ```\n\n2.  现在，让我们添加一个`enum`类，以便存储城市名称:\n\n    ```cpp\n    enum class city: int\n    {\n        MOSCOW,\n        LONDON,\n        ISTANBUL,\n        SEATTLE,\n        DUBAI,\n        MUMBAI,\n        SINGAPORE\n    };\n    ```\n\n3.  让我们也为`city`枚举添加`<<`运算符:\n\n    ```cpp\n    std::ostream& operator<<(std::ostream& os, const city c)\n    {\n        switch(c)\n        {\n            case city::MOSCOW:\n                os << \"MOSCOW\";\n                return os;\n            case city::LONDON:\n                os << \"LONDON\";\n                return os;\n            case city::ISTANBUL:\n                os << \"ISTANBUL\";\n                return os;\n            case city::SEATTLE:\n                os << \"SEATTLE\";\n                return os;\n            case city::DUBAI:\n                os << \"DUBAI\";\n                return os;\n            case city::MUMBAI:\n                os << \"MUMBAI\";\n                return os;\n            case city::SINGAPORE:\n                os << \"SINGAPORE\";\n                return os;\n            default:\n                return os;\n        }\n    }\n    ```\n\n4.  让我们写`struct graph`，它将封装我们的数据:\n\n    ```cpp\n    struct graph\n    {\n        std::vector<std::vector<std::pair<int, int>>> data;\n    ```\n\n5.  Let's see how our constructor defers from a matrix representation:\n\n    ```cpp\n    graph(int n)\n    {\n        data = std::vector<std::vector<std::pair<int, int>>>(n, std::vector<std::pair<int, int>>());\n    }\n    ```\n\n    如我们所见，我们正在用 2D 向量初始化数据，但是所有的行最初都是空的，因为在开始处没有边。\n\n6.  让我们为此实现`addEdge`功能:\n\n    ```cpp\n    void addEdge(const city c1, const city c2, int dis)\n    {\n        std::cout << \"ADD: \" << c1 << \"-\" << c2 << \"=\" << dis << std::endl;\n        auto n1 = static_cast<int>(c1);\n        auto n2 = static_cast<int>(c2);\n        data[n1].push_back({n2, dis});\n        data[n2].push_back({n1, dis});\n    }\n    ```\n\n7.  现在，让我们写下`removeEdge`，这样我们就可以从图中删除一条边:\n\n    ```cpp\n    void removeEdge(const city c1, const city c2)\n    {\n        std::cout << \"REMOVE: \" << c1 << \"-\" << c2 << std::endl;\n        auto n1 = static_cast<int>(c1);\n        auto n2 = static_cast<int>(c2);\n        std::remove_if(data[n1].begin(), data[n1].end(), [n2](const auto& pair)\n            {\n                return pair.first == n2;\n            });\n        std::remove_if(data[n2].begin(), data[n2].end(), [n1](const auto& pair)\n            {\n                return pair.first == n1;\n            });\n    }\n    };\n    ```\n\n8.  Now, let's write the `main` function so that we can use these functions:\n\n    ```cpp\n    int main()\n    {\n        graph g(7);\n        g.addEdge(city::LONDON, city::MOSCOW, 900);\n        g.addEdge(city::LONDON, city::ISTANBUL, 500);\n        g.addEdge(city::LONDON, city::DUBAI, 1000);\n        g.addEdge(city::ISTANBUL, city::MOSCOW, 1000);\n        g.addEdge(city::ISTANBUL, city::DUBAI, 500);\n        g.addEdge(city::DUBAI, city::MUMBAI, 200);\n        g.addEdge(city::ISTANBUL, city::SEATTLE, 1500);\n        g.addEdge(city::DUBAI, city::SINGAPORE, 500);\n        g.addEdge(city::MOSCOW, city::SEATTLE, 1000);\n        g.addEdge(city::MUMBAI, city::SINGAPORE, 300);\n        g.addEdge(city::SEATTLE, city::SINGAPORE, 700);\n        g.addEdge(city::SEATTLE, city::LONDON, 1800);\n        g.removeEdge(city::SEATTLE, city::LONDON);\n        return 0;\n    }\n    ```\n\n    在执行这个程序时，我们应该得到以下输出:\n\n    ```cpp\n    ADD: LONDON-MOSCOW=900\n    ADD: LONDON-ISTANBUL=500\n    ADD: LONDON-DUBAI=1000\n    ADD: ISTANBUL-MOSCOW=1000\n    ADD: ISTANBUL-DUBAI=500\n    ADD: DUBAI-MUMBAI=200\n    ADD: ISTANBUL-SEATTLE=1500\n    ADD: DUBAI-SINGAPORE=500\n    ADD: MOSCOW-SEATTLE=1000\n    ADD: MUMBAI-SINGAPORE=300\n    ADD: SEATTLE-SINGAPORE=700\n    ADD: SEATTLE-LONDON=1800\n    REMOVE: SEATTLE-LONDON\n    ```\n\n由于我们为每个节点存储了一个相邻节点列表，因此这种方法被称为邻接表。这个方法也使用一个向量的一个向量来存储数据，就像前面的方法一样。但是内向量的维数不等于节点数；相反，它取决于边的数量。根据我们的`addEdge`函数，对于图中的每条边，我们将有两个条目。这种表示所需的内存与 E 成正比，其中 E 是边数。\n\n到目前为止，我们只看到了如何构建一个图表。我们需要遍历一个图，以便能够在使用它时执行任何操作。有两种广泛使用的方法——广度优先搜索(BFS)和深度优先搜索(DFS)，我们将在*第 6 章*、*图算法 I* 中讨论这两种方法。\n\n## 总结\n\n在这一章中，我们研究了与前一章相比更高级的一类问题，这有助于我们描述更广泛的现实场景。我们研究并实现了两种主要的数据结构——树和图。我们还研究了在不同情况下可以使用的各种类型的树。然后，我们研究了以编程方式为这些结构表示数据的不同方式。在本章的帮助下，您应该能够应用这些技术来解决类似的现实问题。\n\n现在我们已经了解了线性和非线性数据结构，在下一章中，我们将了解一个非常具体但广泛使用的概念，称为查找，其目标是将值存储在容器中，以便搜索速度超快。我们还将研究散列背后的基本思想，以及如何实现这样一个容器。"
  },
  {
    "path": "docs/cpp-dsal-design-principle/03.md",
    "content": "# 三、哈希表和布隆过滤器\n\n## 学习目标\n\n本章结束时，您将能够:\n\n*   在任何大型应用中轻松识别与查找相关的问题\n*   评估问题是否适合确定性或非确定性查找解决方案\n*   根据场景实施高效的查找解决方案\n*   在大型应用中实现作为 C++ STL 的一部分提供的通用解决方案\n\n在本章中，我们将研究快速查找的问题。我们将了解解决这个问题的各种方法，并了解哪种方法可以用于特定的情况。\n\n## 简介\n\n查找只不过是检查容器中是否存在元素，或者为容器中的键找到相应的值。在前面几章提到的学生数据库系统和医院管理系统的例子中，一个常见的操作是从存储在系统中的大量数据中获取特定的记录。从字典中获取单词的意思、根据一组记录检查一个人是否被允许进入某个设施(访问控制)以及许多其他应用时，也会出现类似的问题。\n\n对于这些场景中的大多数，仅仅线性地遍历所有元素并匹配这些值将会非常耗时，尤其是考虑到存储的大量记录。让我们举一个在字典中查找单词的简单例子。英语词典中大约有 17 万个单词。最简单的方法之一是线性遍历字典，并将给定的单词与字典中的所有单词进行比较，直到找到该单词，或者到达字典的末尾。但是这个太慢了，会有 *O(n)* 的时间复杂度，其中 n 是字典中的字数，不仅庞大而且还在逐日增加。\n\n因此，我们需要更有效的算法，以便更快地进行查找。在本章中，我们将研究两种高效的结构，即哈希表和布隆过滤器。我们将实现它们，并比较它们的优缺点。\n\n## 哈希表\n\n让我们看看查字典这个最基本的问题。《牛津英语词典》大约有 17 万个单词。正如我们在引言中提到的，线性搜索将花费 *O(n)* 时间，其中 *n* 是字数。存储数据的更好方法是将其存储在高度平衡的树中，该树具有与 BST 相似的属性。这使得它比线性搜索快得多，因为它的时间复杂度只有 *O(对数 n)* 。但是对于需要大量这种查询的应用来说，这仍然不是一个足够好的改进。想想包含数百万或数十亿条记录的数据需要多长时间，比如神经科学数据或遗传数据。在数据中找到一些东西需要几天的时间。对于这些情况，我们需要更快的东西，比如**哈希表**。\n\n哈希表的组成部分之一是**哈希**。这背后的想法是用一个可能唯一的键来表示每个值，然后根据用例，使用同一个键来检查键的存在或检索相应的值。从给定数据中导出唯一键的函数称为哈希函数。让我们通过看一些例子来看看如何存储和检索数据，让我们了解为什么我们需要这样一个函数。\n\n### 散列\n\n让我们先举一个简单的例子，然后再进入散列。假设我们有一个存储整数的容器，我们想尽快知道一个特定的整数是否是容器的一部分。最简单的方法是使用布尔数组，每个位代表一个与其索引相同的值。当我们想要插入一个元素时，我们会将该元素对应的布尔值设置为 *0* 。要插入 *x* ，我们只需设置*数据【x】=真*。检查一个特定的整数 *x* 是否在容器内也很简单——我们只需检查*数据【x】*是否为*真*。因此，我们的插入、删除和搜索功能变成了 *O(1)* 。存储从 *0* 到 *9* 的整数的简单哈希表如下所示:\n\n![Figure 3.1: A simple hash table](img/C14498_03_01.jpg)\n\n###### 图 3.1:一个简单的散列表\n\n但是，这种方法存在一些问题:\n\n*   如果数据是浮点数呢？\n*   如果数据不仅仅是一个数字呢？\n*   如果数据的范围太高怎么办？也就是说，如果我们有十亿个数字，那么我们需要一个十亿大小的布尔数组，这并不总是可行的。\n\n为了解决这个问题，我们可以实现一个函数，将任何数据类型的任何值映射到所需范围内的整数。我们可以选择范围，使其布尔数组具有可行的大小。这个函数被称为**散列函数**，正如我们在上一节中提到的。它将把一个数据元素作为输入，并在提供的范围内提供相应的输出整数。\n\n对于大范围内的整数，最简单的散列函数是模函数(由 *%* 表示)，它将元素除以指定的整数( *n* )并返回余数。因此，我们将简单地拥有一个大小为 *n* 的数组。\n\n如果我们想插入一个给定值， *x* ，我们可以对其应用模函数( *x % n* ，我们将总是得到一个介于 *0* 和(*n–1*之间的值，包括这两个值)。现在可以在 *(x % n)* 位置插入 *x* 。这里，通过应用散列函数获得的数字被称为**散列值**。\n\n我们可能会遇到的一个主要问题是，两个元素可能具有相同的模函数输出。一个例子是( *9 % 7* )和( *16 % 7* ，这两者都产生一个哈希值 *2* 。因此，如果对应于 *2* 的槽是*真*(或 *1* 为布尔型)，我们将不知道 *2* 、 *9* 、 *16* 或返回 *x % 7 = 2* 的任何其他整数中的哪一个存在于我们的容器中。这个问题被称为冲突，因为应用哈希函数后，多个键具有相同的值，而不是唯一的值。\n\n如果我们在哈希表中存储实际值而不是布尔整数，我们将知道我们有哪个值，但是我们仍然不能存储具有相同哈希值的多个值。我们将在下一节讨论如何处理这个问题。但是首先，让我们在下面的练习中看看一组整数的基本字典的实现。\n\n### 练习 13:整数基础词典\n\n在本练习中，我们将实现无符号整数哈希映射的基本版本。让我们开始吧:\n\n1.  首先，让我们包含所需的标题:\n\n    ```cpp\n    #include <iostream>\n    #include <vector>\n    ```\n\n2.  现在，让我们添加`hash_map`类。我们将别名`unsigned int`以避免写长名字:\n\n    ```cpp\n    using uint = unsigned int;\n    class hash_map\n    {\n        std::vector<int> data;\n    ```\n\n3.  Now, let's add a constructor for this, which will take the size of the data or hash map:\n\n    ```cpp\n    public:\n    hash_map(size_t n)\n    {\n        data = std::vector<int>(n, -1);\n    }\n    ```\n\n    如图所示，我们使用`–1`来表示元素的缺失。这是我们将用作数据的唯一负值。\n\n4.  Let's add the `insert` function:\n\n    ```cpp\n    void insert(uint value)\n    {\n        int n = data.size();\n        data[value % n] = value;\n        std::cout << \"Inserted \" << value << std::endl;\n    }\n    ```\n\n    正如我们所看到的，我们并没有真正检查是否有一个值已经存在相同的哈希值。如果已经存在任何值，我们只是简单地覆盖。因此，对于给定的哈希值，将只存储最新插入的值。\n\n5.  Let's write a lookup function to see whether an element is present in the map or not:\n\n    ```cpp\n    bool find(uint value)\n    {\n        int n = data.size();\n        return (data[value % n] == value);\n    }\n    ```\n\n    我们将简单地检查该值是否出现在基于哈希值计算的索引中。\n\n6.  让我们实现一个`remove`函数:\n\n    ```cpp\n    void erase(uint value)\n    {\n        int n = data.size();\n        if(data[value % n] == value)\n        {\n    data[value % n] = -1;\n            std::cout << \"Removed \" << value << std::endl;\n    }\n    }\n    };\n    ```\n\n7.  让我们在`main`中写一个小的 lambda 函数来打印查找的状态:\n\n    ```cpp\n    int main()\n    {\n        hash_map map(7);\n        auto print = [&](int value)\n            {\n                if(map.find(value))\n                    std::cout << value << \" found in the hash map\";\n                else\n                    std::cout << value << \" NOT found in the hash map\";\n                std::cout << std::endl;\n            };\n    ```\n\n8.  让我们使用地图上的`insert`和`erase`功能:\n\n    ```cpp\n        map.insert(2);\n        map.insert(25);\n        map.insert(290);\n        print(25);\n        print(100);\n        map.insert(100);\n        print(100);\n        map.erase(25);\n    }\n    ```\n\n9.  程序输出如下:\n\n    ```cpp\n    Inserted 2\n    Inserted 25\n    Inserted 290\n    25 found in the hash map\n    100 NOT found in the hash map\n    Inserted 100\n    100 found in the hash map\n    Removed 25\n    ```\n\n正如我们所看到的，我们能够像预期的那样找到我们之前插入的大部分值，除了最后一种情况，`100`被`0`覆盖，因为它们具有相同的哈希值。正如我们之前所描述的，这被称为碰撞。在接下来的部分中，我们将看到如何避免这种问题，以使我们的结果更加准确。\n\n下图展示了与上一练习不同的功能，应该会使这一点更加清晰:\n\n![Figure 3.2: Basic operations in a hash table](img/C14498_03_02.jpg)\n\n###### 图 3.2:哈希表中的基本操作\n\n![Figure 3.3: Basic operations in a hash table (continued)](img/C14498_03_03.jpg)\n\n###### 图 3.3:哈希表中的基本操作(续)\n\n如上图所示，我们不能插入两个哈希值相同的元素；我们必须扔掉其中一个。\n\n现在，正如我们前面提到的，哈希表的一个主要用途是找到对应于某个键的值，而不仅仅是检查该键是否存在。这可以简单地通过存储键值对来实现，而不仅仅是数据中的键。因此，我们的插入、删除和查找函数仍然会根据我们的键计算哈希值，但是一旦我们找到数组中的位置，我们就会将我们的值作为该对的第二个参数。\n\n## 散列表中的列\n\n在前面几节中，我们看了哈希表如何帮助我们存储大量的密钥，使得查找任何所需的密钥变得容易。但是，我们也遇到了一个问题，多个密钥具有相同的哈希值，也称为**冲突**。在*练习 13* 、*整数基础字典*中，我们通过简单地重写密钥并保留对应于给定哈希值的最新密钥来处理这个问题。然而，这并不允许我们存储所有的密钥。在接下来的子主题中，我们将看几个方法来帮助我们克服这个问题，并允许我们在哈希表中保留所有的键值。\n\n### Clo se 寻址–链接\n\n到目前为止，我们只为任何哈希值存储了一个元素。如果我们已经有了特定哈希值的元素，我们别无选择，只能丢弃新值或旧值。**链接**的方法是我们保留两种价值观的一种方式。在这种方法中，我们将为每个索引存储一个链表，而不是在哈希表中存储一个键。因此，每当我们遇到碰撞问题时，我们只需在列表的末尾插入新的密钥。因此，本质上，我们可以存储任意多的元素，而不是一个元素。为每个索引选择链表而不是向量(新元素使用`push_back`)的原因是为了能够从任何位置快速移除元素。让我们在下面的练习中实现这一点。\n\n### 练习 14:带链接的哈希表\n\n在本练习中，我们将实现一个哈希表，并使用链接来处理冲突。让我们开始吧:\n\n1.  首先，让我们包含所需的标题:\n\n    ```cpp\n    #include <iostream>\n    #include <vector>\n    #include <list>\n    #include <algorithm>\n    ```\n\n2.  现在，让我们添加`hash_map`类。我们将别名`unsigned int`以避免写长名字:\n\n    ```cpp\n    using uint = unsigned int;\n    class hash_map\n    {\n        std::vector<std::list<int>> data;\n    ```\n\n3.  现在，让我们为`hash_map`添加一个构造函数，它将采用数据或哈希映射的大小:\n\n    ```cpp\n    public:\n    hash_map(size_t n)\n    {\n        data.resize(n);\n    }\n    ```\n\n4.  Let's add an `insert` function:\n\n    ```cpp\n    void insert(uint value)\n    {\n        int n = data.size();\n        data[value % n].push_back(value);\n        std::cout << \"Inserted \" << value << std::endl;\n    }\n    ```\n\n    正如我们所看到的，我们总是在数据中插入值。一种替代方法是搜索该值，并仅在该值不存在时插入它。\n\n5.  Let's write the lookup function to see whether an element is present in the map:\n\n    ```cpp\n    bool find(uint value)\n    {\n        int n = data.size();\n        auto& entries = data[value % n];\n        return std::find(entries.begin(), entries.end(), value) != entries.end();\n    }\n    ```\n\n    正如我们所看到的，我们的查找似乎比传统方法更快，但不如以前那么快。这是因为现在，它也依赖于数据，以及`n`的值。在这个练习之后，我们将再次回到这一点。\n\n6.  让我们实现一个移除元素的函数:\n\n    ```cpp\n    void erase(uint value)\n    {\n        int n = data.size();\n        auto& entries = data[value % n];\n        auto iter = std::find(entries.begin(), entries.end(), value);\n\n        if(iter != entries.end())\n        {\n    entries.erase(iter);\n            std::cout << \"Removed \" << value << std::endl;\n    }\n    }\n    };\n    ```\n\n7.  让我们编写与上一个练习相同的`main`函数，并看看不同之处:\n\n    ```cpp\n    int main()\n    {\n        hash_map map(7);\n        auto print = [&](int value)\n            {\n                if(map.find(value))\n                    std::cout << value << \" found in the hash map\";\n                else\n                    std::cout << value << \" NOT found in the hash map\";\n                std::cout << std::endl;\n            };\n    ```\n\n8.  Let's use the `insert` and `erase` functions on `map`:\n\n    ```cpp\n        map.insert(2);\n        map.insert(25);\n        map.insert(290);\n        map.insert(100);\n        map.insert(55);\n        print(100);\n        map.erase(2);\n    }\n    ```\n\n    下面是我们程序的输出:\n\n    ```cpp\n    Inserted 2\n    Inserted 25\n    Inserted 290\n    Inserted 100\n    Inserted 55\n    100 found in the hash map\n    Removed 2\n    ```\n\n正如我们所看到的，这些值不会被覆盖，因为我们可以在列表中存储任意数量的值。因此，我们的输出完全准确可靠。\n\n下图说明了如何对数据集执行不同的操作:\n\n![Figure 3.4: Basic operations on a hash table with chaining](img/C14498_03_04.jpg)\n\n###### 图 3.4:带链接的哈希表的基本操作\n\n![Figure 3.5: Basic operations on a hash table with chaining (continued)](img/C14498_03_05.jpg)\n\n###### 图 3.5:带链接的哈希表的基本操作(续)\n\n正如我们所看到的，我们将具有相同哈希值的元素附加到节点中的列表中，而不是单个元素。\n\n现在，让我们考虑这些操作的时间复杂性。我们看到，插入功能仍然是 *O(1)* 。虽然`push_back`可能比仅仅设置一个值慢一点，但并没有明显的慢。考虑到这种方法解决的问题，这是一个小代价。但是根据哈希表的大小和数据集，查找和删除可能会慢很多。例如，如果所有键都具有相同的哈希值，则搜索所需的时间将为 O(n)，因为它将简单地变成链表中的线性搜索。\n\n如果哈希表与要存储的密钥数量相比非常小，将会有很多冲突，并且列表平均会更长。另一方面，如果我们保留一个非常大的哈希表，我们可能最终会有非常稀疏的数据，并最终浪费内存。因此，哈希表的大小应该根据应用的上下文和场景进行优化。我们也可以用数学来定义这些东西。\n\n**加载因子**表示哈希表中每个列表的平均键数。可以使用以下公式计算:\n\n![Figure 3.6: Load factor](img/C14498_03_06.jpg)\n\n###### 图 3.6:负载系数\n\n如果键的数量等于我们的哈希表大小，加载因子将是 *1* 。这是一个理想的场景；所有的操作我们都会靠近 *O(1)* ，所有的空间都会得到适当的利用。\n\n如果该值小于 *1* ，这意味着我们甚至没有为每个列表存储一个键(假设我们希望每个索引都有一个列表)，这实际上浪费了一些空间。\n\n如果该值大于 *1* ，这意味着我们的列表的平均长度大于 1，因此我们的查找和删除功能平均会慢一点。\n\n负载系数的值可以随时在 *O(1)* 中计算。一些高级散列表实现利用该值来修改散列函数(也称为再散列)，如果该值在 1 的任一侧超过某些阈值。散列函数被修改，使得负载系数更接近 1。然后，哈希表的大小可以根据我们的负载系数和基于更新的哈希函数重新分配的值进行更新。再次清洗是一项昂贵的操作，因此不应过于频繁。但是，如果采用适当的策略，我们可以在平均时间复杂度方面取得非常好的结果。\n\n然而，负载系数并不是决定这项技术性能的唯一因素。考虑以下场景:我们有一个大小为 *7* 的哈希表，它有七个元素。然而，它们都具有相同的哈希值，因此它们都存在于单个桶中。因此，搜索将始终花费 *O(n)* 而不是 *O(1)* 时间。然而，负载系数将是 1，这是一个绝对理想的值。这里，实际的问题是散列函数。散列函数的设计应该使不同的键尽可能均匀地分布在所有可能的索引中。基本上，最小铲斗尺寸和最大铲斗尺寸之间的差异不应该很大(在这种情况下为 7)。如果哈希函数的设计方式是所有七个元素得到不同的哈希值，那么所有的搜索函数调用都会产生 *O(1)* 复杂度和即时结果。这是因为最小和最大铲斗尺寸之差将为 *0* 。然而，这通常不会在哈希表实现中完成。它应该由哈希函数本身处理，因为哈希表不依赖于哈希函数的实现。\n\n### 开放寻址\n\n另一种解决冲突的方法是**开放寻址**。在这个方法中，我们将所有元素存储在哈希表中，而不是将元素链接到哈希表。因此，为了容纳所有元素，哈希表的大小必须大于元素的数量。其思想是探查对应于特定哈希值的单元是否已经被占用。我们可以通过多种方式来探索这个值，正如我们将在下面的副主题中看到的。\n\n**线性探测**\n\n这是一种简单的探测技术。如果在一个特定的哈希值上有冲突，我们可以简单地查看一个空单元格的后续哈希值，并在找到空间后插入我们的元素。如果 *hash(x)* 处的单元格已满，那么我们需要检查 *hash(x + 1)* 处的单元格是否为空。如果也是满的，看*哈希(x + 2)* 等等。\n\n下图说明了线性探测的工作原理:\n\n![Figure 3.7: Basic operations on a hash table with linear probing](img/C14498_03_07.jpg)\n\n###### 图 3.7:带有线性探测的哈希表的基本操作\n\n![Figure 3.8: Unable to insert elements after hash table fills up](img/C14498_03_08.jpg)\n\n###### 图 3.8:哈希表填满后无法插入元素\n\n如我们所见，如果对应于其哈希值的位置已经被占用，我们将在下一个可用的槽中插入一个元素。插入前三个元素后，我们可以看到它们聚集在一起。如果在同一个范围内插入了更多的元素，那么所有的元素都将连续出现在簇的末尾，从而使簇增长。现在，当我们试图搜索一个值时，该值不存在于哈希函数首先计算的位置，而是存在于一个大簇的末尾，我们必须线性搜索簇中的所有键。因此，搜索变得非常慢。\n\n因此，如果数据密集地聚集在一起，我们就会遇到一个大问题。我们可以说，如果数据以这样一种方式分布，即存在一些值出现频率非常高的组，则数据是密集聚集的。例如，假设在 *100* 的哈希表中有很多哈希值为 *3* 到 *7* 的键。之后，所有的键将被连续探测到一些值，这将大大降低我们的搜索速度。\n\n**二次探测**\n\n正如我们所看到的，线性探测的主要问题是聚类。这背后的原因是，在碰撞的情况下，我们是线性运动的。这个问题很大程度上可以用二次方程代替线性方程来解决。这就是二次探测所提供的。\n\n首先，我们尝试在位置*散列(x)* 处插入值 *x* 。如果那个位置已经被占用了，我们就去位置*hash(x+1**2**)*，然后*hash(x+2**2**)*以此类推。因此，我们以二次方式增加偏移量，从而降低创建小数据簇的概率。\n\n这两种探测技术还有一个优点——一个元素的位置可能会受到其他不具有相同哈希值的元素的影响。因此，基本上，即使只有一个具有特定哈希值的键，它也会发生冲突，因为该位置存在一些其他元素，而链接则不是这种情况。例如，在线性探测中，如果我们有两个哈希值为 4 的键，其中一个将插入位置 4，另一个将插入位置 5。接下来，如果我们需要插入一个哈希值为 5 的密钥，它将需要在 6 处插入。该密钥受到影响，即使它与任何其他密钥没有相同的哈希值。\n\n### 完美散列–布谷鸟散列\n\n正如标题所示，**布谷鸟哈希**是完美的哈希技术之一。我们之前提到的方法在最坏的情况下不能保证 *O(1)* 的时间复杂度，但是如果实施得当，布谷鸟哈希可以实现这一点。\n\n在布谷鸟哈希中，我们保留了两个大小相同的哈希表，每个哈希表都有自己唯一的哈希函数。任何元素都可以出现在任意一个哈希表中，其位置基于相应的哈希函数。\n\n布谷鸟哈希与我们以前的哈希技术有两个主要不同之处:\n\n*   任何元素都可以出现在两个哈希表的任何一个中。\n*   任何元素都可以在将来移动到另一个位置，即使是在插入之后。\n\n早期的散列技术不允许在插入后移动元素，除非我们做了完全的重新散列，但是布谷鸟散列却不是这样，因为任何元素都可能有两个可能的位置。我们仍然可以通过增加任何元素的可能位置的数量来增加度数，以便获得更好的结果，并且减少重复使用的频率。但是，在本章中，我们将只查看具有两个可能位置(哈希表)的版本，因为它更容易理解。\n\n对于查找，我们只需要查看两个位置来确定元素是否存在。因此，查找总是需要 *O(1)* 时间。\n\n然而，插入功能可能需要更长的时间。在这种情况下，插入函数首先检查是否有可能在第一个哈希表中插入新元素，比如说 *A* 。如果是这样，它会在那里插入元素，我们就完成了。但是如果那个位置被一个预先存在的元素占据，比如说 *B* ，我们仍然继续插入 *A* 并将 *B* 移动到第二个哈希表。如果第二个哈希表中的这个新位置也被占用了，比如说通过元素 *C* ，我们再次将 *B* 插入到那里，并将 *C* 移动到第一个表中。我们可以递归地继续下去，直到找到所有元素的空槽。下图说明了这一过程:\n\n![Figure 3.9: Cuckoo hashing](img/C14498_03_09.jpg)\n\n###### 图 3.9:布谷鸟散列\n\n一个主要问题是，我们可能会陷入一个循环，递归可能会导致无限循环。对于上一段中的例子，考虑有一个元素 *D* ，我们希望在这里插入 *C* ，但是如果我们试图移动 *D* ，它会转到 *A* 的位置。因此，我们处在一个无限循环中。下图应该有助于您直观地看到这一点:\n\n![Figure 3.10: A cycle formed during cuckoo hashing](img/C14498_03_10.jpg)\n\n###### 图 3.10:杜鹃杂凑过程中形成的循环\n\n为了解决这个问题，一旦我们确定了周期，我们就需要用新的散列函数来重新散列一切。用新的哈希函数创建的哈希表可能仍然存在相同的问题，因此我们可能需要重新散列并尝试不同的哈希函数。然而，有了聪明的策略和明智选择的散列函数，我们可以高概率地实现摊销 *O(1)* 的性能。\n\n就像开放寻址一样，我们不能存储超过哈希表总大小的元素。为了确保良好的性能，我们应该确保我们的负载系数小于 50%，也就是说，元素的数量应该小于可用容量的一半。\n\n在下面的练习中，我们将看看布谷鸟散列的实现。\n\n### 练习 15:布谷鸟杂凑\n\n在本练习中，我们将实现布谷鸟哈希来创建哈希表，并在其中插入各种元素。我们还将跟踪操作如何进行，这将使我们能够了解插入是如何工作的。让我们开始吧:\n\n1.  让我们像往常一样从包含所需的头开始:\n\n    ```cpp\n    #include <iostream>\n    #include <vector>\n    ```\n\n2.  Let's add a class for the hash map. We'll also store size separately this time:\n\n    ```cpp\n    class hash_map\n    {\n        std::vector<int> data1;\n        std::vector<int> data2;\n        int size;\n    ```\n\n    如我们所见，我们使用两张桌子。\n\n3.  Now, let's add the corresponding hash functions:\n\n    ```cpp\n    int hash1(int key) const\n    {\n        return key % size;\n    }\n    int hash2(int key) const\n    {\n        return (key / size) % size;\n    }\n    ```\n\n    在这里，我们保持了这两个功能非常简单，但这些功能可以根据需要进行调整。\n\n4.  Now, let's add a constructor that will set our data for initialization:\n\n    ```cpp\n    public:\n    hash_map(int n) : size(n)\n    {\n        data1 = std::vector<int>(size, -1);\n        data2 = std::vector<int>(size, -1);\n    }\n    ```\n\n    如我们所见，我们只是将两个数据表初始化为空(由`–1`表示)。\n\n5.  Let's write a `lookup` function first:\n\n    ```cpp\n    std::vector<int>::iterator lookup(int key)\n    {\n        auto hash_value1 = hash1(key);\n        if(data1[hash_value1] == key)\n        {\n            std::cout << \"Found \" << key << \" in first table\" << std::endl;\n            return data1.begin() + hash_value1;\n        }\n        auto hash_value2 = hash2(key);\n        if(data2[hash_value2] == key)\n        {\n            std::cout << \"Found \" << key << \" in second table\" << std::endl;\n            return data2.begin() + hash_value2;\n        }\n        return data2.end();\n    }\n    ```\n\n    我们试图在两个表中找到键，如果找到了，就返回相关的迭代器。我们并不总是需要迭代器，但是我们将在删除函数中使用它来使事情变得更容易。如果没有找到元素，我们将返回`data2`表的末尾。如我们所见，查找的时间复杂度为 *O(1)* ，并且执行速度非常快。\n\n6.  Let's implement a delete function:\n\n    ```cpp\n    void erase(int key)\n    {\n        auto position = lookup(key);\n        if(position != data2.end())\n        {\n            *position = -1;\n            std::cout << \"Removed the element \" << key << std::endl;\n        }\n        else\n        {\n            std::cout << \"Key \" << key << \" not found.\" << std::endl;\n        }\n    }\n    ```\n\n    我们可以看到，大部分工作都是通过调用`lookup`函数来完成的。我们只需要验证结果并重置值，将其从表中删除。\n\n7.  For insertion, we shall implement the actual logic in a different function because it will be recursive. One more thing we want to do is avoid cycles. However, keeping a record of all the values that are visited can be costly. To avoid that, we will simply stop the function once it is called more than n times. Since the threshold of the recursion depth of n is dependent on our memory (or hash table size), this gives good performance:\n\n    ```cpp\n    void insert(int key)\n    {\n        insert_impl(key, 0, 1);\n    }\n    void insert_impl(int key, int cnt, int table)\n    {\n        if(cnt >= size)\n        {\n            std::cout << \"Cycle detected, while inserting \" << key << \". Rehashing required.\" << std::endl;\n            return;\n        }\n        if(table == 1)\n        {\n    int hash = hash1(key);\n            if(data1[hash] == -1)\n            {\n                std::cout << \"Inserted key \" << key << \" in table \" << table << std::endl;\n                data1[hash] = key;\n            }\n            else\n            {\n                int old = data1[hash];\n                data1[hash] = key;\n                std::cout << \"Inserted key \" << key << \" in table \" << table << \" by replacing \" << old << std::endl;\n                insert_impl(old, cnt + 1, 2);\n            }\n        }\n        else\n        {\n    int hash = hash2(key);\n            if(data2[hash] == -1)\n            {\n                std::cout << \"Inserted key \" << key << \" in table \" << table << std::endl;\n                data2[hash] = key;\n            }\n            else\n            {\n                int old = data2[hash];\n                data2[hash] = key;\n                std::cout << \"Inserted key \" << key << \" in table \" << table << \" by replacing \" << old << std::endl;\n                insert_impl(old, cnt + 1, 2);\n            }\n        }\n    }\n    ```\n\n    正如我们所看到的，实现需要三个参数——键、我们想要插入键的表以及递归调用栈的计数，以跟踪我们已经改变位置的元素的数量。\n\n8.  现在，让我们编写一个实用函数来打印哈希表中的数据。虽然这不是真正必要的，也不应该公开，但我们会这样做，以便更好地理解我们的 insert 函数是如何在内部管理数据的:\n\n    ```cpp\n    void print()\n    {\n        std::cout << \"Index: \";\n        for(int i = 0; i < size; i++)\n            std::cout << i << '\\t';\n        std::cout << std::endl;\n        std::cout << \"Data1: \";\n        for(auto i: data1)\n            std::cout << i << '\\t';\n        std::cout << std::endl;\n        std::cout << \"Data2: \";\n        for(auto i: data2)\n            std::cout << i << '\\t';\n        std::cout << std::endl;\n    }\n    };\n    ```\n\n9.  现在，让我们编写`main`函数，这样我们就可以使用这个散列图:\n\n    ```cpp\n    int main()\n    {\n        hash_map map(7);\n        map.print();\n        map.insert(10);\n        map.insert(20);\n        map.insert(30);\n        std::cout << std::endl;\n        map.insert(104);\n        map.insert(2);\n        map.insert(70);\n        map.insert(9);\n        map.insert(90);\n        map.insert(2);\n        map.insert(7);\n        std::cout << std::endl;\n        map.print();\n        std::cout << std::endl;\n        map.insert(14);  // This will cause cycle.\n    }\n    ```\n\n10.  您应该会看到以下输出:\n\n    ```cpp\n    Index: 0    1    2    3    4    5    6    \n    Data1: -1    -1    -1    -1    -1    -1    -1    \n    Data2: -1    -1    -1    -1    -1    -1    -1    \n    Inserted key 10 in table 1\n    Inserted key 20 in table 1\n    Inserted key 30 in table 1\n    Inserted key 104 in table 1 by replacing 20\n    Inserted key 20 in table 2\n    Inserted key 2 in table 1 by replacing 30\n    Inserted key 30 in table 2\n    Inserted key 70 in table 1\n    Inserted key 9 in table 1 by replacing 2\n    Inserted key 2 in table 2\n    Inserted key 90 in table 1 by replacing 104\n    Inserted key 104 in table 2 by replacing 2\n    Inserted key 2 in table 1 by replacing 9\n    Inserted key 9 in table 2\n    Inserted key 2 in table 1 by replacing 2\n    Inserted key 2 in table 2 by replacing 104\n    Inserted key 104 in table 1 by replacing 90\n    Inserted key 90 in table 2\n    Inserted key 7 in table 1 by replacing 70\n    Inserted key 70 in table 2\n    Index: 0    1    2    3    4    5     6\n    Data1: 7   -1    2    10  -1   -1     104\n    Data2: 2    9    20   70   30   90   -1\n    Inserted key 14 in table 1 by replacing 7\n    Inserted key 7 in table 2 by replacing 9\n    Inserted key 9 in table 1 by replacing 2\n    Inserted key 2 in table 2 by replacing 2\n    Inserted key 2 in table 1 by replacing 9\n    Inserted key 9 in table 2 by replacing 7\n    Inserted key 7 in table 1 by replacing 14\n    Cycle detected, while inserting 14\\. Rehashing required.\n    ```\n\n正如我们所看到的，输出显示了两个表是如何在内部维护的完整轨迹。我们已经打印了内部步骤，因为一些值正在移动。`14`的最后一次插入导致了一个循环，从痕迹中我们可以看到。插入深度已经超过`7`。同时，我们还可以看到两张桌子几乎都坐满了。我们已经从`14`中填充了`11`元素，因此替换值的机会在每一步都在增加。我们也在周期前打印了表格。\n\n此外，删除一个元素需要花费*0(1)*的时间，因为它只使用`lookup`函数，如果找到的话，会删除该元素。所以，唯一昂贵的功能是插入。因此，如果插入的数量比任何应用中的查找数量都少，这是一个理想的实现。\n\n让我们使用以下视觉辅助工具，以便更好地理解这一点:\n\n![Figure 3.11: Inserting elements in a hash table that uses cuckoo hashing](img/C14498_03_11.jpg)\n\n###### 图 3.11:在使用布谷鸟哈希的哈希表中插入元素\n\n![Figure 3.12: Handling collisions in a hash table using cuckoo hashing](img/C14498_03_12.jpg)\n\n###### 图 3.12:使用布谷鸟哈希处理哈希表中的冲突\n\n![Figure 3.13: Handling collisions in a hash table using cuckoo hashing (continued)](img/C14498_03_13.jpg)\n\n###### 图 3.13:使用布谷鸟哈希处理哈希表中的冲突(续)\n\n![Figure 3.14: Finding values in a hash table that uses cuckoo hashing](img/C14498_03_14.jpg)\n\n###### 图 3.14:在使用布谷鸟哈希的哈希表中查找值\n\n![Figure 3.15: Erasing values in a hash table that uses cuckoo hashing](img/C14498_03_15.jpg)\n\n###### 图 3.15:擦除使用布谷鸟哈希的哈希表中的值\n\n从前面的一系列图中我们可以看到，首先，我们尝试在第一个表中插入元素。如果已经有了另一个元素，我们覆盖它，并在另一个表中插入先前存在的元素。我们重复这个过程，直到可以安全地插入最后一个元素。\n\n## C++ 散列表\n\n正如我们前面提到的，在大多数应用中，查找操作非常频繁。然而，我们可能并不总是遇到正整数，它们很容易散列。大多数情况下，您可能会遇到字符串。考虑我们之前考虑的英语词典的例子。我们可以通过使用单词作为关键字和单词定义作为值来存储字典数据。另一个例子是我们在*第 1 章*、*列表、栈和队列*中考虑的医院记录数据库，其中患者的姓名可以用作关键字，其他相关信息可以作为值存储。\n\n我们之前用来计算整数哈希值的简单模函数不适用于字符串。一个简单的选择是计算所有字符的 ASCII 值之和的模。然而，一个字符串中所有字符的排列将会非常庞大，这将会产生很多冲突。\n\nC++ 提供了一个名为`std::hash<std::string>(std::string)`的函数，我们可以用它来生成字符串的哈希值。它有一个内置的算法来处理散列函数。同样，C++ 为所有基本类型的数据提供了这样的函数。\n\n现在，看看我们在*练习 14* 、*带链接的哈希表*中实现的哈希表，似乎很明显，我们可以简单地基于数据类型对其进行模板化，并制定一个通用解决方案来为任何给定类型的数据提供哈希函数。STL 为此提供了几个解决方案:`std::unordered_set<Key>`和`std::unordered_map<Key, Value>`。无序集合只能存储一组键，而无序映射可以存储键及其值。因此，每个唯一键在容器中都有一个对应的值。\n\n这两个容器都是以相同的方式实现的——使用带有链接的哈希表。哈希表中的每一行都是一个存储键(以及映射的值)的向量。这些行被称为**桶**。因此，在计算一个键的哈希值后，它将被放入一个桶中。每个桶也是一个支持链接的列表。\n\n默认情况下，这些集装箱的最大装载系数为 *1* 。一旦元素数量超过哈希表的大小，哈希函数将被更改，哈希值将被重新计算(重新散列)，并且将重建一个更大的哈希表来降低负载系数。我们也可以使用`rehash`功能手动完成。使用`max_load_factor(float)`功能可以更改 *1* 的负载系数默认最大限值。一旦负载系数超过定义的最大限值，这些值将被重新刷新。\n\n这些容器提供了常用的功能，如`find`、`insert`和`erase`。它们还提供了迭代所有元素的迭代器，以及使用其他容器(如向量和数组)创建无序集和映射的构造函数。无序映射还提供`operator[]`，以便它可以返回已知键的值。\n\n在下面的练习中，我们将研究无序集和映射的实现。\n\n### 练习 16:STL 提供的哈希表\n\n在本练习中，我们将实现无序集和映射，并在这些容器上应用插入、删除和查找等操作。让我们开始吧:\n\n1.  包括所需的标题:\n\n    ```cpp\n    #include <iostream>\n    #include <unordered_map>\n    #include <unordered_set>\n    ```\n\n2.  现在，让我们编写一些简单的`print`函数，使我们的`main`函数更易读:\n\n    ```cpp\n    void print(const std::unordered_set<int>& container)\n    {\n        for(const auto& element: container)\n            std::cout << element << \" \";\n        std::cout << std::endl;\n    }\n    void print(const std::unordered_map<int, int>& container)\n    {\n        for(const auto& element: container)\n            std::cout << element.first << \": \" << element.second << \", \";\n        std::cout << std::endl;\n    }\n    ```\n\n3.  同样，在`find`函数上添加包装器以保持代码整洁:\n\n    ```cpp\n    void find(const std::unordered_set<int>& container, const auto& element)\n    {\n        if(container.find(element) == container.end())\n            std::cout << element << \" not found\" << std::endl;\n        else\n            std::cout << element << \" found\" << std::endl;\n    }\n    void find(const std::unordered_map<int, int>& container, const auto& element)\n    {\n        auto it = container.find(element);\n        if(it == container.end())\n            std::cout << element << \" not found\" << std::endl;\n        else\n            std::cout << element << \" found with value=\" << it->second << std::endl;\n    }\n    ```\n\n4.  现在，编写`main`函数，这样我们就可以使用`unordered_set`和`unordered_map`，然后对其进行各种操作。我们将找到、插入和删除元素:\n\n    ```cpp\n    int main()\n    {\n        std::cout << \"Set example: \" << std::endl;\n        std::unordered_set<int> set1 = {1, 2, 3, 4, 5};\n        std::cout << \"Initial set1: \";\n        print(set1);\n        set1.insert(2);\n        std::cout << \"After inserting 2: \";\n        print(set1);\n        set1.insert(10);\n        set1.insert(351);\n        std::cout << \"After inserting 10 and 351: \";\n        print(set1);\n        find(set1, 4);\n        find(set1, 100);\n        set1.erase(2);\n        std::cout << \"Erased 2 from set1\" << std::endl;\n        find(set1, 2);\n        std::cout << \"Map example: \" << std::endl;\n        std::unordered_map<int, int> squareMap;\n        squareMap.insert({2, 4});\n        squareMap[3] = 9;\n        std::cout << \"After inserting squares of 2 and 3: \";\n        print(squareMap);\n        squareMap[30] = 900;\n        squareMap[20] = 400;\n        std::cout << \"After inserting squares of 20 and 30: \";\n        print(squareMap);\n        find(squareMap, 10);\n        find(squareMap, 20);\n        std::cout << \"Value of map[3]=\" << squareMap[3] << std::endl;\n        std::cout << \"Value of map[100]=\" << squareMap[100] << std::endl;\n    }\n    ```\n\n5.  这个程序的一个可能输出如下。集合和地图中元素的顺序可以不同，因此称为*无序*集合/地图:\n\n    ```cpp\n    Set example: \n    Initial set1: 5 4 3 2 1 \n    After inserting 2: 5 4 3 2 1 \n    After inserting 10 and 351: 351 10 1 2 3 4 5 \n    4 found\n    100 not found\n    Erased 2 from set1\n    2 not found\n    Map example: \n    After inserting squares of 2 and 3: 3: 9, 2: 4, \n    After inserting squares of 20 and 30: 20: 400, 30: 900, 2: 4, 3: 9, \n    10 not found\n    20 found with value=400\n    Value of map[3]=9\n    Value of map[100]=0\n    ```\n\n正如我们所看到的，我们可以从两个容器中插入、查找和删除元素。这些操作正在按预期进行。如果我们将这些操作与其他容器(如 vector、list、array、deque 等)进行比较，这里的性能会快得多。\n\n我们可以存储键值对，并使用`operator[]`访问任何给定键的值，如本练习所示。它返回一个引用，因此也允许我们设置值，而不仅仅是检索它。\n\n#### 注意\n\n由于`operator[]`返回一个引用，如果找不到关键字，它会将默认值添加到条目中。\n\n在最后一行，我们得到了`map[100] = 0`，即使`100`从未插入地图。这是因为`operator[]`正在返回默认值。\n\n如果我们想跟踪桶的数量，因为它们基于重新散列而改变，我们可以使用`bucket_count()`函数来实现。还有其他功能可以获取其他内部参数的详细信息，如`load_factor`、`max_bucket_count`等。我们也可以使用`rehash`功能手动重洗。\n\n因为这些容器是使用链接实现的，所以它们实际上是将键/值对存储在不同的桶中。因此，在搜索任何桶中的键时，我们需要比较它们是否相等。因此，我们需要为键类型定义相等运算符。或者，我们可以将其作为另一个模板参数传递。\n\n正如我们在本练习中看到的，无序集和映射不允许重复键。如果需要存储重复值，可以使用`unordered_multiset`或`unordered_multimap`。为了支持多个值，insert 函数不会检查容器中是否已经存在该键。此外，它还支持一些额外的函数来检索带有特定键的所有项目。我们不会再看更多关于这些容器的细节，因为它不在本书的讨论范围之内。\n\nSTL 为 C++ 支持的所有基本数据类型提供散列函数。因此，如果我们想要一个定制的类或结构作为任何上述容器的键类型，我们需要在`std`命名空间内实现一个散列函数。或者，我们可以将其作为模板参数传递。然而，每次我们自己编写一个散列函数并不是一个好主意，因为性能很大程度上取决于它。设计散列函数需要对手头的问题进行大量的研究和理解，以及数学技能。因此，我们将它排除在本书的范围之外。出于我们的目的，我们可以简单地使用`boost`库中提供的`hash_combine`功能，如下例所示:\n\n```cpp\n#include <boost/functional/hash.hpp>\nstruct Car\n{\n    std::string model;\n    std::string brand;\n    int buildYear;\n};\nstruct CarHasher\n{\n    std::size_t operator()(const Car& car) const\n    {\n        std::size_t seed = 0;\n        boost::hash_combine(seed, car.model);\n        boost::hash_combine(seed, car.brand);\n        return seed;\n    }\n};\nstruct CarComparator\n{\n    bool operator()(const Car& car1, const Car& car2) const\n    {\n    return (car1.model == car2.model) && (car1.brand == car2.brand);\n    }\n};\n// We can use the hasher as follows:\nstd::unordered_set<Car, CarHasher, CarComparator> carSet;\nstd::unordered_map<Car, std::string, CarHasher, CarComparator> carDescriptionMap;\n```\n\n正如我们所看到的，我们已经用`operator()`定义了一个散列结构，它将被无序容器使用。我们还用`operator()`定义了比较器结构来支持相关功能。我们将这些结构作为模板参数传递。这也允许我们对不同的对象使用不同类型的比较器和散列器。\n\n除了简单的散列函数(如模)之外，还有一些复杂的散列函数，称为加密散列函数，如 MD5、SHA-1 和 SHA-256。这些算法非常复杂，它们可以将任何类型的数据(甚至是文件)作为输入值。密码函数的一个重要特征是很难从给定的散列值(也称为反向散列)中确定实际数据，因此它们被用于一些最安全的系统中。例如，比特币区块链使用 SHA-256 算法来存储交易记录真实性的重要证明。区块链中的每个*块*包含其先前链接块的 SHA-256 哈希值，当前块的哈希值包含在后续块中。非法修改任何块都会使从该块开始的整个区块链失效，因为现在修改后的块的哈希值将与存储在下一个块中的值不匹配。即使有一些世界上最快的超级计算机，也需要数百年才能打破这种局面，创造伪造的交易记录。\n\n### 活动 6:将长网址映射到短网址\n\n在本活动中，我们将创建一个程序来实现类似于[https://tinyurl.com/](https://tinyurl.com/)的服务。它可以取一个很长的网址，并将其映射到一个小的网址，方便共享。每当我们输入短网址时，它应该检索原始网址。\n\n我们需要以下功能:\n\n*   高效地存储用户提供的原始网址和相应的较小网址\n*   如果找到，根据给定的较小网址检索原始网址；否则，返回一个错误\n\n这些高级步骤应该有助于您解决此活动:\n\n1.  创建一个包含`unordered_map`作为主要数据成员的类。\n2.  添加插入值的函数。这个函数应该有两个参数:原始网址和较小版本的网址。\n3.  Add a function to find the actual URL based on a given small URL if present.\n\n    #### 注意\n\n    这个活动的解决方案可以在第 498 页找到。\n\n## 布隆过滤器\n\n与哈希表相比，Bloom 过滤器非常节省空间，但代价是答案的确定性；也就是说，我们得到的答案是不确定的。只保证不会有假阴性，但可能有假阳性。换句话说，如果我们得到一个正命中，元素可能存在，也可能不存在；但是如果我们得到一个负数，那么元素肯定不存在。\n\n就像布谷鸟散列一样，我们将在这里使用多个散列函数。然而，我们将保留三个函数，因为两个函数不能达到足够的精度。基本思想是，我们不存储实际值，而是存储一个布尔数组，指示一个值是否(可能)存在。\n\n为了插入一个元素，我们计算所有散列函数的值，并将数组中所有三个散列值对应的位设置为 *1* 。对于查找，我们计算所有散列函数的值，并检查是否所有对应的位都设置为 *1* 。如果是，我们返回*真*；否则，我们返回 *false* (元素不存在)。\n\n显而易见的问题是——为什么查找是不确定的？原因是任何位都可以由多个元素设置。因此，一个特定值(称之为 *x* )的所有相关位被设置为 *1* 的概率相对较大，因为之前插入了一些其他元素，尽管 *x* 根本没有被插入。在这种情况下，查找功能仍将返回*真*。因此，我们可以预期一些假阳性。我们插入的元素越多，误报的几率就越高。但是，如果 *x* 的其中一个位没有设置，那么我们可以说该元素不存在。所以，假阴性不可能。\n\n当布尔数组中的所有位都设置为 *1* 时，数组将饱和。因此，查找功能将始终返回*真*，而插入功能将不会有任何影响，因为所有的位已经设置为 *1* 。\n\n下图更清楚地说明了这一点:\n\n![Figure 3.16: Inserting elements in a bloom filter](img/C14498_03_16.jpg)\n\n###### 图 3.16:在布隆过滤器中插入元素\n\n![Figure 3.17: Finding elements in a bloom filter](img/C14498_03_17.jpg)\n\n###### 图 3.17:在布隆过滤器中查找元素\n\n![Figure 3.18: Finding elements in a bloom filter (continued)](img/C14498_03_18.jpg)\n\n###### 图 3.18:在布隆过滤器中查找元素(续)\n\n如前面的图表所示，我们根据散列函数设置相关的位，对于插入，我们进行逐位`AND`来查找元素，正如我们前面解释的那样。\n\n在下面的练习中，我们将在 C++ 中实现一个布隆过滤器。\n\n### 练习 17:创建布隆过滤器\n\n在本练习中，我们将创建一个布隆过滤器，并尝试一些基本操作。我们还将测试查找中的误报。让我们开始吧:\n\n1.  让我们包括所需的标题:\n\n    ```cpp\n    #include <iostream>\n    #include <vector>\n    ```\n\n2.  现在，让我们为布隆过滤器创建一个类，并添加所需的数据成员:\n\n    ```cpp\n    class bloom_filter\n    {\n        std::vector<bool> data;\n        int nBits;\n    ```\n\n3.  Now, let's add the required hash functions. Again, we'll use very basic hash functions:\n\n    ```cpp\n    int hash(int num, int key)\n    {\n        switch(num)\n        {\n        case 0:\n            return key % nBits;\n        case 1:\n            return (key / 7) % nBits;\n        case 2:\n            return (key / 11) % nBits;\n        }\n        return 0;\n    }\n    ```\n\n    如您所见，我们使用单个函数，用一个名为`num`的参数来确定散列函数，以避免其他函数中不必要的`if` - `else`块。这也很容易扩大；我们只需要为每个散列函数添加一个 case。\n\n4.  让我们为布隆过滤器添加一个构造函数:\n\n    ```cpp\n    public:\n    bloom_filter(int n) : nBits(n)\n    {\n        data = std::vector<bool>(nBits, false);\n    }\n    ```\n\n5.  Now, let's add a `lookup` function:\n\n    ```cpp\n    void lookup(int key)\n    {\n        bool result = data[hash(0, key)] & data[hash(1, key)] & data[hash(2, key)];\n        if(result)\n        {\n            std::cout << key << \" may be present.\" << std::endl;\n        }\n        else\n        {\n            std::cout << key << \" is not present.\" << std::endl;\n        }\n    }\n    ```\n\n    `lookup`功能果然很简单。检查是否所有需要的位都设置为`1`。如果有可变数量的散列函数，我们总是可以循环所有的散列函数来检查是否所有对应的位都被设置为`1`。为了使我们的话更准确，我们还说由于误报的可能性，一个关键*可能存在*。另一方面，如果`lookup`返回否定，我们完全确定一个键不存在。\n\n6.  甚至插入函数也同样简单:\n\n    ```cpp\n    void insert(int key)\n    {\n        data[hash(0, key)] = true;\n        data[hash(1, key)] = true;\n        data[hash(2, key)] = true;\n        std::cout << key << \" inserted.\" << std::endl;\n    }\n    };\n    ```\n\n7.  让我们添加`main`函数，这样我们就可以使用这个类:\n\n    ```cpp\n    int main()\n    {\n    bloom_filter bf(11);\n    bf.insert(100);\n    bf.insert(54);\n    bf.insert(82);\n    bf.lookup(5);\n    bf.lookup(50);\n    bf.lookup(2);\n    bf.lookup(100);\n    bf.lookup(8);\n    bf.lookup(65);\n    }\n    ```\n\n8.  您应该会看到以下输出:\n\n    ```cpp\n    100 inserted.\n    54 inserted.\n    82 inserted.\n    5 may be present.\n    50 is not present.\n    2 is not present.\n    100 may be present.\n    8 is not present.\n    65 may be present.\n    ```\n\n我们可以看到，有几个假阳性，但没有假阴性。\n\n与以前的技术不同，这种结构只需要 11 位来存储这些信息，这一点我们可以从 Bloom 过滤器的构造函数中看到。因此，我们可以轻松地增加过滤器的大小，并相应地更新散列函数，以获得更好的结果。例如，我们可以将数组的大小增加到 1，000(因为 1，023 是一个素数，所以使用频率很高)，我们将仍然使用不到 130 个字节，这比大多数其他技术要少得多。随着哈希表大小的增加，我们的哈希函数也会变成 *%1023* 或者类似的，会提供更好的结果和更好的数字分布。\n\n这里需要注意的一个要点是，由于我们没有将实际数据存储在容器中，因此我们可以将其用作异构结构；也就是说，只要我们的散列函数足够好，我们就可以在同一个 Bloom 过滤器中同时插入不同类型的数据，例如整数、字符串和双精度值。\n\n这在现实生活中有一些非常好的用例，尤其是当数据量太大，即使使用哈希表也无法搜索时，一些误报是可以接受的。例如，当使用电子邮件提供商(如 Gmail 或 Outlook)创建新的电子邮件地址时，会检查该电子邮件地址是否已经存在。数据库中有数十亿个电子邮件地址，对这样一个基本且频繁的查询进行精确检查将非常昂贵。幸运的是，即使电子邮件地址还没有被占用，有时说它被占用也没关系，因为它不会造成任何伤害。用户将简单地选择其他东西。在这种情况下，使用布隆过滤器是一个可行的选择。我们将在*活动 7* 、*电子邮件地址验证器*中看到这一点。\n\n另一个例子是用于显示服务使用的新广告的推荐算法，例如脸书广告。每次你查看订阅源时，它都会显示一个新广告。它可以简单地将你看过的广告的标识存储在一个布隆过滤器中。然后，在您的提要中显示特定广告之前，可以对照它检查其标识。如果该检查返回您已经观看了特定的广告，即使您没有观看(假阳性)，它也不会显示该广告。然而，这很好，因为你不会知道它，因为你没有看到那个广告。这样，你每次都可以通过快速查找获得新广告。\n\n### 活动 7:电子邮件地址验证器\n\n在本活动中，我们将为电子邮件创建一个验证器，类似于我们在注册时在许多电子邮件服务中发现的(如 Gmail 和 Outlook)。我们将使用布隆过滤器来检查某个电子邮件地址是否已经被其他人占用。\n\n这些高级步骤应该有助于您完成本活动:\n\n1.  创建一个`BloomFilter`类，它可以接受多个散列函数和布隆的大小。\n2.  对于哈希，使用 OpenSSL 库中的 MD5 算法来生成给定电子邮件的哈希值。MD5 是一种 128 位哈希算法。对于多个散列函数，我们可以将每个字节用作单独的散列值。\n3.  要在布隆过滤器中添加一封电子邮件，我们需要将来自我们在*步骤 2* 中计算的哈希值的每个字节的所有位设置为 *true* 。\n4.  To find any email, we need to check whether all the relevant bits are *true* based on the hash value we calculated in *step 2*.\n\n    #### 注意\n\n    这个活动的解决方案可以在 503 页找到。\n\n## 总结\n\n正如我们在介绍中提到的，在大多数应用中都会遇到这样或那样的查找问题。我们可以根据需要使用确定性和概率性解决方案。在本章中，我们实现并看到了如何使用它们。最后，我们还看了一个 C++ 中用于散列的内置容器的例子。当我们编写应用时，这些容器非常有用，因为我们不需要每次都为每种类型自己实现它们。一个简单的经验法则是这样的:如果我们可以看到对容器的`find`函数的大量函数调用，我们应该选择基于查找的解决方案。\n\n到目前为止，我们已经看到了如何在各种类型的数据结构中存储数据并执行一些基本操作。在接下来的章节中，我们将研究各种类型的算法设计技术，以便我们可以优化这些操作，从分治开始。"
  },
  {
    "path": "docs/cpp-dsal-design-principle/04.md",
    "content": "# 四、分治法\n\n## 学习目标\n\n本章结束时，您将能够:\n\n*   描述各个击破的设计范例\n*   实现标准的分治算法，如合并排序、快速排序和线性时间选择\n*   使用 MapReduce 编程模型解决问题\n*   了解如何使用多线程 C++ MapReduce 实现\n\n在本章中，我们将研究分治算法设计范式，并学习如何使用它来解决计算问题。\n\n## 简介\n\n在前一章中，我们研究了一些常用的数据结构。数据结构是不同形式的数据组织，数据结构支持并控制对存储在其中的数据的访问成本。然而，使软件有用的不仅仅是以各种 us 格式存储和检索数据的能力，而是对数据进行转换以解决计算问题的能力。对于一个给定的问题，数据转换的精确定义和顺序是由一系列被称为**算法**的指令决定的。\n\n算法接受一组定义问题实例的输入，应用一系列转换，并输出一组结果。如果这些结果是手头计算问题的正确解，我们的算法就说是*正确*。一个算法的*优秀程度*取决于它的效率，或者说算法需要执行多少条指令才能产生正确的结果:\n\n![Figure 4.1: Scaling of steps taken by an algorithm with respect to the size of the input](img/C14498_04_01.jpg)\n\n###### 图 4.1:算法相对于输入大小所采取的步骤的比例\n\n上图显示了算法所需步骤数的增长，作为输入大小的函数。更复杂的算法随着输入的大小增长得更快，如果输入足够大，即使在现代计算机系统上，它们也可能变得不可行。例如，让我们假设我们有一台每秒可以执行一百万次操作的计算机。对于大小为 50 的输入，需要 *N log(N)* 步的算法需要 283 微秒才能完成；一个需要 *N* *2* 步的算法需要 2.5 毫秒；以及一个需要 *N 的算法！*(N*的阶乘*)步数大约需要 9，637，644，561，599，544，267，027，654，516，581，964，749，586，575，812，734.82 **个世纪**才能跑完！\n\n*如果对于输入 N 的大小，算法以 N 的多项式的多个步骤来解决问题，则算法被认为是有效的*\n\n将**多项式时间算法**表示为解的问题，据说也属于计算复杂度的类 *P* (多项式)。还有其他几个计算复杂性问题可以划分，这里给出了几个例子:\n\n*   **NP** ( **非确定性多项式时间**)问题有可以在多项式时间内验证的解，但没有任何已知的多项式时间解。\n*   **EXPTIME** ( **指数时间**)问题的解决方案的运行时间与输入的大小成指数关系。\n*   **PSPACE** ( **多项式空间**)问题需要多项式量的空间。\n\n找出 *P* 中的问题集是否与 *NP* 中的问题集完全相同，就是著名的 *P = NP* 问题，经过几十年的努力，这个问题依然没有解决，甚至为任何能解决它的人带来 100 万美元的奖金。我们再来看看*第 9 章*、*动态规划二*中的 *P* 和 *NP* 类问题。\n\n数十年来，计算机科学家一直将算法作为数学对象进行研究，并且已经确定了一套设计高效算法的通用方法(或**范例**)，可用于解决各种各样的问题。最广泛应用的算法设计范例之一叫做*分而治之*，这将是我们本章研究的主题。\n\nA **分而治之**型算法将给定的问题分解成更小的部分，尝试为每个部分求解问题，最后将每个部分的解组合成整个问题的解。几个广泛使用的算法属于这一类别，例如，二分搜索法，快速排序，合并排序，矩阵乘法，快速傅立叶变换，以及天际线算法。这些算法几乎出现在今天使用的所有主要应用中，包括数据库、网络浏览器，甚至语言运行时，如 Java 虚拟机和 V8 JavaScript 引擎。\n\n在本章中，我们将向您展示使用分治法解决问题意味着什么，以及您如何确定您的问题是否适合这样的解决方案。接下来，我们将练习递归思维，并向您展示现代 C++ 标准库提供的工具，以便您可以使用分治法解决问题。我们将通过查看 MapReduce 来结束这一章，包括讨论它为什么和如何扩展，以及您如何使用相同的范例来使用 CPU 级和机器级并行化来扩展您的程序。\n\n让我们深入研究一种使用分治法的基本算法——二分搜索法算法。\n\n## 二分搜索法\n\n让我们从标准的搜索问题开始:假设给我们一个正整数的排序序列，并要求我们找出序列中是否存在一个数字 *N* 。有几个地方搜索问题自然会出现；例如，接待员在一组按客户 id 排序的文件中查找客户的文件，或者教师在他们的学生登记册中查找学生获得的分数。实际上，它们都在解决搜索问题。\n\n现在，我们可以用两种不同的方法来解决这个问题。在第一种方法中，我们迭代整个序列，检查每个元素是否等于 *N* 。这称为**线性搜索**，如以下代码所示:\n\n```cpp\nbool linear_search(int N, std::vector<int>& sequence)\n{\n    for (auto i : sequence)\n    {\n        if (i == N)\n            return true;      // Element found!\n    }\n\n    return false;\n}\n```\n\n这种方法的一个好处是，它适用于所有数组，无论是排序的还是未排序的。但是，这种方法效率低下，并且没有考虑给定数组的排序。就其算法复杂度而言，是一种 *O(N)* 算法。\n\n利用序列排序的另一种解决方案如下:\n\n1.  从`range`中的整个序列开始。\n2.  比较当前`range`和 *N* 的中间元素。让这个中间元素成为 *M* 。\n3.  如果 *M = N* ，我们已经在序列中找到了 *N* ，因此搜索停止。\n4.  Otherwise, we modify the `range` according to two rules:\n\n    –如果 *N < M* ，这意味着如果 *N* 出现在`range`中，它将在 *M* 的左侧，因此，我们可以安全地从`range`中移除 *M* 右侧的所有元素。\n\n    –如果 *N > M* ，算法将从`range`中移除 *M* 左侧的所有元素。\n\n5.  如果`range`中剩余 1 个以上的元素，进入*步骤 2* 。\n6.  否则，序列中不存在 *N* ，搜索停止。\n\n为了说明这个算法，我们将展示二分搜索法是如何工作的，其中 *S* 是从 *1* 到 9 和 *N = 2* 的有序整数序列:\n\n1.  The algorithm starts with putting all the elements of *S* in range. The middle element in this step is found to be *5*. We compare *N* and *5*:\n\n    ![Figure 4.2: Binary search algorithm – step 1](img/C14498_04_02.jpg)\n\n    ###### 图 4.2:二进制搜索算法–步骤 1\n\n2.  Since *N < 5*, if *N* was present in the sequence, it would have to be to the left of *5*. Therefore, we can safely discard all the elements of the sequence lying toward the right of *5* from our search. Our range now has elements only between *1* and *5*, and the middle element is now *3*. We can now compare *N* and *3*:\n\n    ![Figure 4.3: Binary search algorithm – step 2](img/C14498_04_03.jpg)\n\n    ###### 图 4.3:二进制搜索算法–步骤 2\n\n3.  我们发现当前中间元素 *3* 仍然大于 *N* ，范围可以进一步删减，只包含 *1* 和 *3* 之间的元素。新的中间元素现在是 *2* ，等于 *N* ，搜索终止:\n\n![Figure 4.4: Binary search algorithm – step 3](img/C14498_04_04.jpg)\n\n###### 图 4.4:二进制搜索算法–步骤 3\n\n在下面的练习中，我们将研究二分搜索法算法的实现。\n\n### 练习 18:二分搜索法基准\n\n在本练习中，我们将编写一个二分搜索法实现并对其进行基准测试。按照以下步骤完成本练习:\n\n1.  首先添加以下标题:\n\n    ```cpp\n    #include <iostream>\n    #include <vector>\n    #include <chrono>\n    #include <random>\n    #include <algorithm>\n    #include <numeric>\n    ```\n\n2.  像这样添加线性搜索代码:\n\n    ```cpp\n    bool linear_search(int N, std::vector<int>& S)\n    {\n            for (auto i : S)\n            {\n                if (i == N)\n                    return true;       // Element found!\n            }\n\n            return false;\n    }\n    ```\n\n3.  添加此处显示的二分搜索法代码:\n\n    ```cpp\n    bool binary_search(int N, std::vector<int>& S)\n    {\n        auto first = S.begin();\n        auto last = S.end();\n        while (true)\n        {\n            // Get the middle element of current range\n            auto range_length = std::distance(first, last);\n            auto mid_element_index = first + std::floor(range_length / 2);\n            auto mid_element = *(first + mid_element_index);\n            // Compare the middle element of current range with N\n            if (mid_element == N)\n                return true;\n            else if (mid_element > N)\n                std::advance(last, -mid_element_index);\n            if (mid_element < N)\n                std::advance(first, mid_element_index);\n            // If only one element left in the current range\n            if (range_length == 1)\n                return false;\n        }\n    }\n    ```\n\n4.  为了评估二分搜索法的表现，我们将实现两个功能。首先，写小测试:\n\n    ```cpp\n    void run_small_search_test()\n    {\n        auto N = 2;\n        std::vector<int> S{ 1, 3, 2, 4, 5, 7, 9, 8, 6 };\n        std::sort(S.begin(), S.end());\n        if (linear_search(N, S))\n            std::cout << \"Element found in set by linear search!\" << std::endl;\n        else\n            std::cout << \"Element not found.\" << std::endl;\n        if (binary_search(N, S))\n            std::cout << \"Element found in set by binary search!\" << std::endl;\n        else\n            std::cout << \"Element not found.\" << std::endl;\n    }\n    ```\n\n5.  现在，添加大测试功能，如下:\n\n    ```cpp\n    void run_large_search_test(int size, int N)\n    {\n        std::vector<int> S;\n        std::random_device rd;\n        std::mt19937 rand(rd());\n          // distribution in range [1, size]\n        std::uniform_int_distribution<std::mt19937::result_type> uniform_dist(1, size); \n        // Insert random elements\n        for (auto i=0;i<size;i++)\n            S.push_back(uniform_dist(rand));\n        std::sort(S.begin(), S.end());\n        // To measure the time taken, start the clock\n        std::chrono::steady_clock::time_point begin = std::chrono::steady_clock::now();\n\n        bool search_result = binary_search(111, S);\n        // Stop the clock\n        std::chrono::steady_clock::time_point end = std::chrono::steady_clock::now();\n\n        std::cout << \"Time taken by binary search = \" << \n    std::chrono::duration_cast<std::chrono::microseconds>\n    (end - begin).count() << std::endl;\n\n        if (search_result)\n            std::cout << \"Element found in set!\" << std::endl;\n        else\n            std::cout << \"Element not found.\" << std::endl;\n    }\n    ```\n\n6.  最后，添加以下驱动代码，在随机生成的不同大小的向量中搜索数字`36543`:\n\n    ```cpp\n    int main()\n    {\n        run_small_search_test();\n        run_large_search_test(100000, 36543);\n        run_large_search_test(1000000, 36543);\n        run_large_search_test(10000000, 36543);\n        return 0;\n    }\n    ```\n\n7.  在 x64 调试模式下编译程序并运行。输出应该如下所示:\n\n![Figure 4.5: Binary search with debugging enabled](img/C14498_04_05.jpg)\n\n###### 图 4.5:启用调试的二分搜索法\n\n请注意，三个输入数组中的每一个都比前一个数组大 10 倍，因此第三个数组比第一个数组大 100 倍，第一个数组本身包含 10 万个元素。尽管如此，使用二分搜索法搜索阵列所花费的时间仅增加了 10 微秒。\n\n在之前的测试中，我们不允许任何编译器优化，并使用附加到程序的调试器运行。现在，让我们看看当我们的编译器被允许在没有附加调试器的情况下优化 C++ 代码时会发生什么。尝试在 x64-发布模式下编译*练习 18* 、*二分搜索法基准测试*中的代码并运行。输出应该如下所示:\n\n![Figure 4.6: Binary search with compiler optimizations turned on](img/C14498_04_06.jpg)\n\n###### 图 4.6:打开编译器优化的二分搜索法\n\n在所有三种情况下，二分搜索法所用时间大致相等，即使向量大小有很大差异！\n\n请注意，我们的二分搜索法实现使用迭代器和 C++ 标准库函数，如`std::distance()`和`std::advance()`。这在现代 C++ 中被认为是好的实践，因为它有助于保持我们的代码与底层数据类型无关，并且不会出现索引越界错误。\n\n现在，假设我们想在浮点数的向量上执行搜索。在前面的练习中，我们将如何修改我们的函数？答案非常简单。我们可以如下修改函数签名:\n\n```cpp\nbool linear_search(float N, std::vector<float>& S)\nbool binary_search(float N, std::vector<float>& S)\n```\n\n搜索函数内部的其余代码仍然可以保持完全相同，因为它完全独立于底层数据类型，并且只依赖于容器数据类型的行为。**核心算法逻辑与算法运行的底层数据类型的分离是在现代 C++ 中编写可重用代码的基石。**我们将在本书中看到几个这样分离的例子，并深入研究标准库提供的更多功能，这些功能可以帮助我们编写可重用和健壮的代码。\n\n### 活动 8:接种疫苗\n\n想象一下，现在是流感季节，卫生部门官员正计划访问一所学校，以确保所有入学的儿童都注射了流感疫苗。然而，有一个问题:一些孩子已经接种了流感疫苗，但不记得他们是否接种了卫生官员计划为所有学生接种的特定类别的流感疫苗。寻找官方记录，该部门能够找到已经接种疫苗的学生名单。此处显示了该列表的一小部分:\n\n![Figure 4.7: Excerpt of vaccination records](img/C14498_04_07.jpg)\n\n###### 图 4.7:疫苗接种记录摘录\n\n假设所有的名字都是正整数，并且给定的列表是排序的。你的任务是编写一个程序，可以在列表中查找给定学生的疫苗接种状态，并向官员输出该学生是否需要接种疫苗。学生在两种情况下需要接种疫苗:\n\n*   如果它们不在列表中\n*   如果他们在名单上，但没有注射流感疫苗\n\n因为这个列表可以有大量的学生，你的程序应该尽可能的快速和高效。程序的最终输出应该如下所示:\n\n![Figure 4.8: Sample output of Activity 8](img/C14498_04_08.jpg)\n\n###### 图 4.8:活动 8 的示例输出\n\n**高级步骤**\n\n本练习的解决方案使用了二分搜索法算法的稍加修改的版本。让我们开始吧:\n\n1.  将每个学生表示为`Student`类的对象，可以定义如下:\n\n    ```cpp\n     class Student\n    {\n        std::pair<int, int> name;\n        bool vaccinated;\n    }\n    ```\n\n2.  重载`Student`类所需的操作符，以便使用标准库的`std::sort()`功能对学生向量进行排序。\n3.  用二分搜索法看看这个学生是否在名单上。\n4.  如果学生不在列表中，您的函数应该返回 *true* ，因为学生需要接种疫苗。\n5.  否则，如果学生在列表中，但尚未接种疫苗，则返回 *true* 。\n6.  Else, return *false*.\n\n    #### 注意\n\n    这个活动的解决方案可以在 506 页找到。\n\n## 理解分治法\n\n分治法的核心是一个简单而直观的想法:如果你不知道如何解决一个问题的大实例，那就找出你能解决的问题的一小部分，然后解决它。然后，迭代更多这样的部分，一旦你解决了所有的部分，将结果组合成一个大的连贯的原始问题的解决方案。使用分治法解决问题有三个步骤:\n\n1.  **划分**:取原问题，分成几个部分，这样每个部分都需要解决同一个问题。\n2.  **征服**:解决各部分问题。\n3.  **组合**:取不同部位的解决方案，组合成原问题的解决方案。\n\n在前一节中，我们看了一个使用分治法在序列中搜索的例子。在每一步中，二分搜索法试图只搜索序列的一部分，这部分被标记为`range`。当找到元素或不再有方法将`range`进一步分成更小的部分时，搜索终止。然而，搜索问题与大多数分治算法的不同之处在于:在搜索问题中，如果一个元素可以在序列的较小的`range`中找到，那么它也肯定存在于完整的序列中。换句话说，在序列的一小部分中对问题的解决给了我们整个问题的解决方案。因此，该解决方案不需要实施一般各个击破方法的组合步骤。不幸的是，这一特性并没有在大多数可以用分治法解决的计算问题中得到体现。在下一节中，我们将深入探讨并查看更多使用分治法解决问题的示例。\n\n## 使用分治法进行排序\n\n我们现在将探讨在解决另一个标准问题——排序时，如何实现分治法。拥有高效的排序算法的重要性怎么强调都不为过。在 20 世纪 60 年代计算的早期，计算机制造商估计，他们机器中 25%的 CPU 周期用于对数组元素进行排序。尽管计算环境在过去几年中发生了显著变化，但排序在今天仍然被广泛研究，并且仍然是几个应用中的基本操作。例如，这是数据库中索引背后的关键思想，它允许使用类似于二分搜索法的对数时间搜索来快速访问存储的数据。\n\n排序算法实现的一般要求如下:\n\n*   该实现应该能够处理任何数据类型。它应该能够对整数、浮点小数甚至 C++ 结构或类进行排序，在这些结构或类中可以定义不同元素之间的顺序。\n*   排序算法应该能够处理大量数据，也就是说，相同的算法应该能够处理比计算机主内存更大的数据。\n*   排序算法应该是快速的，渐进的和实际的。\n\n虽然列出的三个目标都是可取的，但在实践中，很难同时实现第二个和第三个目标。第二个目标需要外部排序，即对不在计算机主内存中的数据进行排序。外部排序算法可以在执行过程中的任何时候只将整个数据的一小部分保存在内存中。\n\n在本节中，我们将介绍两种排序算法:合并排序和快速排序。合并排序是一种外部排序算法，因此实现了我们的第二个目标，而快速排序，顾名思义，是实践中已知最快的排序算法之一，并作为 C++ 标准库的`std::sort()`函数的一部分出现。\n\n### 合并排序\n\n**合并排序**是已知最古老的排序算法之一，出现在 20 世纪 40 年代末的报告中。当时的计算机有几百字节的主存储器，经常用于复杂的数学分析。因此，排序算法能够工作是至关重要的，即使要操作的所有数据不能保存在主存储器中。合并排序通过利用一个简单的想法解决了这个问题——对一大组元素进行排序与对元素的一个小子集进行排序相同，然后合并排序后的子集，以保持元素的递增或递减顺序:\n\n![Figure 4.9: Merge sort](img/C14498_04_09.jpg)\n\n###### 图 4.9:合并排序\n\n上图显示了使用合并排序对整数数组进行排序的示例。首先，该算法将原始阵列分成子阵列，直到每个子阵列仅由一个元素组成(*步骤 1* 至*步骤 4* )。在随后的所有步骤中，算法将元素合并到更大的数组中，保持每个子数组中的元素以递增的顺序排列。\n\n### 练习 19:合并排序\n\n在本练习中，我们将实现合并排序算法。步骤如下:\n\n1.  导入以下标题:\n\n    ```cpp\n    #include <iostream>\n    #include <vector>\n    #include <chrono>\n    #include <random>\n    #include <algorithm>\n    #include <numeric>\n    ```\n\n2.  The C++ code for the merge operation on two vectors is as follows. Write the `merge()` function like so:\n\n    ```cpp\n    template <typename T>\n    std::vector<T> merge(std::vector<T>& arr1, std::vector<T>& arr2)\n    {\n        std::vector<T> merged;\n        auto iter1 = arr1.begin();\n        auto iter2 = arr2.begin();\n        while (iter1 != arr1.end() && iter2 != arr2.end())\n        {\n            if (*iter1 < *iter2)\n            {\n                merged.emplace_back(*iter1);\n                iter1++ ;\n            }\n            else\n            {\n                merged.emplace_back(*iter2);\n                iter2++ ;\n            }\n        }\n        if (iter1 != arr1.end())\n        {\n            for (; iter1 != arr1.end(); iter1++)\n                merged.emplace_back(*iter1);\n        }\n        else\n        {\n            for (; iter2 != arr2.end(); iter2++)\n                merged.emplace_back(*iter2);\n        }\n        return merged;\n    }\n    ```\n\n    templated`merge()`函数接受对两个类型为`T`的向量的引用，并返回一个包含输入数组中元素的新向量，但按递增顺序排序。\n\n3.  我们现在可以使用合并操作来编写递归合并排序实现，如下所示:\n\n    ```cpp\n    template <typename T>\n    std::vector<T> merge_sort(std::vector<T> arr)\n    {\n        if (arr.size() > 1)\n        {\n            auto mid = size_t(arr.size() / 2);\n            auto left_half = merge_sort<T>(std::vector<T>(arr.begin(), arr.begin() + mid));\n            auto right_half = merge_sort<T>(std::vector<T>(arr.begin() + mid, arr.end()));\n            return merge<T>(left_half, right_half);\n        }\n\n        return arr;\n    }\n    ```\n\n4.  添加以下函数打印矢量:\n\n    ```cpp\n    template <typename T>\n    void print_vector(std::vector<T> arr)\n    {\n        for (auto i : arr)\n            std::cout << i << \" \";\n\n        std::cout << std::endl;\n    }\n    ```\n\n5.  下面的函数允许我们测试合并排序算法的实现:\n\n    ```cpp\n    void run_merge_sort_test()\n    {\n        std::vector<int>    S1{ 45, 1, 3, 1, 2, 3, 45, 5, 1, 2, 44, 5, 7 };\n        std::vector<float>  S2{ 45.6f, 1.0f, 3.8f, 1.01f, 2.2f, 3.9f, 45.3f, 5.5f, 1.0f, 2.0f, 44.0f, 5.0f, 7.0f };\n        std::vector<double> S3{ 45.6, 1.0, 3.8, 1.01, 2.2, 3.9, 45.3, 5.5, 1.0, 2.0,  44.0, 5.0, 7.0 };\n        std::vector<char>   C{ 'b','z','a','e','f','t','q','u','y' };\n        std::cout << \"Unsorted arrays:\" << std::endl;\n        print_vector<int>(S1);\n        print_vector<float>(S2);\n        print_vector<double>(S3);\n        print_vector<char>(C);\n        std::cout << std::endl;\n        auto sorted_S1 = merge_sort<int>(S1);\n        auto sorted_S2 = merge_sort<float>(S2);\n        auto sorted_S3 = merge_sort<double>(S3);\n        auto sorted_C = merge_sort<char>(C);\n        std::cout << \"Arrays sorted using merge sort:\" \n                    << std::endl;\n        print_vector<int>(sorted_S1);\n        print_vector<float>(sorted_S2);\n        print_vector<double>(sorted_S3);\n        print_vector<char>(sorted_C);\n        std::cout << std::endl;\n    }\n    int main()\n    {\n        run_merge_sort_test();\n        return 0;\n    }\n    ```\n\n6.  编译并运行程序。输出应该如下所示:\n\n![Figure 4.10: Sorting by merge sort](img/C14498_04_10.jpg)\n\n###### 图 4.10:按合并排序排序\n\n在本练习中，我们对合并排序的实现延续了我们的主题，即不将算法的实现与底层数据类型捆绑在一起，而只依赖于容器公开的函数。\n\n### 快速排序\n\n虽然合并排序的目标是对大量数据进行排序，但 quicksort 试图减少平均运行时间。quicksort 中的基本思想也与 merge sort 相同——将原始输入数组分成更小的子数组，对子数组进行排序，并将结果合并得到排序后的数组。但是，快速排序使用的基本操作是**划分**而不是合并。\n\n**分区操作的工作**\n\n给定一个数组和一个**枢轴元素**、 *P* ，在数组中，**分区操作**做两件事:\n\n1.  它将原始阵列分成两个子阵列， *L* 和 *R* ，其中 *L* 包含给定阵列中小于或等于 *P* 的所有元素， *R* 包含给定阵列中大于 *P* 的所有元素。\n2.  它按照 *L* 、 *P* 、 *R* 的顺序重组数组中的元素。\n\n下图显示了应用于未排序数组的分区结果，其中第一个元素被选为轴心:\n\n![Figure 4.11: Selecting a pivot and partitioning the vector around it](img/C14498_04_11.jpg)\n\n###### 图 4.11:选择一个枢轴并围绕它分割向量\n\n分割操作的一个有用的特性是，在应用它之后，向量中枢轴的新位置 *P* 变成了如果向量被排序的话 *P* 将具有的位置。例如，元素 *5* 在我们应用分区操作后出现在数组中的第 5 个位置，如果数组按升序排序，该位置与元素 *5* 的位置相同。\n\n前面的属性也是 quicksort 算法背后的核心思想，其工作原理如下:\n\n1.  如果输入数组 *A* 中有 1 个以上的元素，则对 *A* 进行分区操作。这就产生了子阵 *L* 和 *R* 。\n2.  使用 *L* 作为*步骤 1* 的输入。\n3.  使用 *R* 作为*步骤 1* 的输入。\n\n*步骤 2* 和 *3* 是对数组上的分区操作的递归调用，这些调用由分区操作生成并应用于原始输入数组。分区操作的这种简单递归应用导致以递增的顺序对元素进行排序。由于快速排序递归树可以很快变深，下图显示了在六个元素的小数组上应用快速排序的示例， *{5，6，7，3，1，9}* :\n\n![Figure 4.12: Visualization of the quicksort algorithm](img/C14498_04_12.jpg)\n\n###### 图 4.12:快速排序算法的可视化\n\n算法的每一次迭代都显示了分区操作的结果，该结果被应用到使用突出显示的枢轴在上一步中生成的子阵列。应该注意的是，我们选择数组的第一个元素作为轴是任意的。数组中的任何元素都可以被选为轴心，而不会影响快速排序算法的正确性。\n\n### 练习 20:快速排序\n\n在本练习中，我们将实现并测试快速排序的实现。让我们开始吧:\n\n1.  导入以下标题:\n\n    ```cpp\n    #include <iostream>\n    #include <vector>\n    #include <chrono>\n    #include <random>\n    #include <algorithm>\n    #include <numeric>\n    ```\n\n2.  The C++ code for the partition operation is as follows. Write the `partition()` function as shown here:\n\n    ```cpp\n    template <typename T>\n    auto partition(typename std::vector<T>::iterator begin,\n                typename std::vector<T>::iterator last)\n    {\n          // Create 3 iterators, \n          // one pointing to the pivot, one to the first element and \n          // one to the last element of the vector.\n        auto pivot_val = *begin;\n        auto left_iter = begin+1;\n        auto right_iter = last;\n        while (true)\n        {\n            // Starting from the first element of vector, find an element that is greater than pivot.\n            while (*left_iter <= pivot_val && \n                       std::distance(left_iter, right_iter) > 0)\n                left_iter++ ;\n            // Starting from the end of vector moving to the beginning, find an element that is lesser than the pivot.\n            while (*right_iter > pivot_val && \n                       std::distance(left_iter, right_iter) > 0)\n                right_iter--;\n            // If left and right iterators meet, there are no elements left to swap. Else, swap the elements pointed to by the left and right iterators\n            if (left_iter == right_iter)\n                break;\n            else\n                std::iter_swap(left_iter, right_iter);\n        }\n        if (pivot_val > *right_iter)\n            std::iter_swap(begin, right_iter);\n\n        return right_iter;\n    }\n    ```\n\n    这里显示的实现只接收底层容器对象上的迭代器，并返回指向数组中分区索引的另一个迭代器。这意味着向量的所有元素都大于右分区中的轴，所有小于或等于轴的元素都在左分区中。\n\n3.  快速排序算法递归使用分区操作，如下面的代码所示:\n\n    ```cpp\n    template <typename T>\n    void quick_sort(typename std::vector<T>::iterator begin, \n            typename std::vector<T>::iterator last)\n    {\n        // If there are more than 1 elements in the vector\n        if (std::distance(begin, last) >= 1)\n        {\n            // Apply the partition operation\n            auto partition_iter = partition<T>(begin, last);\n\n            // Recursively sort the vectors created by the partition operation\n            quick_sort<T>(begin, partition_iter-1);\n            quick_sort<T>(partition_iter, last);\n        }\n    }\n    ```\n\n4.  `print_vector()`用于向控制台打印矢量，实现如下:\n\n    ```cpp\n    template <typename T>\n    void print_vector(std::vector<T> arr)\n    {\n        for (auto i : arr)\n            std::cout << i << \" \";\n\n        std::cout << std::endl;\n    }\n    ```\n\n5.  从*练习 19* 、*合并排序*中改编驾驶员代码，如下所示:\n\n    ```cpp\n    void run_quick_sort_test()\n    {\n        std::vector<int> S1{ 45, 1, 3, 1, 2, 3, 45, 5, 1, 2, 44, 5, 7 };\n        std::vector<float>  S2{ 45.6f, 1.0f, 3.8f, 1.01f, 2.2f, 3.9f, 45.3f, 5.5f, 1.0f, 2.0f, 44.0f, 5.0f, 7.0f };\n        std::vector<double> S3{ 45.6, 1.0, 3.8, 1.01, 2.2, 3.9, 45.3, 5.5, 1.0, 2.0,  44.0, 5.0, 7.0 };\n        std::vector<char> C{ 'b','z','a','e','f','t','q','u','y'};\n        std::cout << \"Unsorted arrays:\" << std::endl;\n        print_vector<int>(S1);\n        print_vector<float>(S2);\n        print_vector<double>(S3);\n        print_vector<char>(C);\n        std::cout << std::endl;\n        quick_sort<int>(S1.begin(), S1.end() - 1);\n        quick_sort<float>(S2.begin(), S2.end() - 1);\n        quick_sort<double>(S3.begin(), S3.end() - 1);\n        quick_sort<char>(C.begin(), C.end() - 1);\n        std::cout << \"Arrays sorted using quick sort:\" << std::endl;\n        print_vector<int>(S1);\n        print_vector<float>(S2);\n        print_vector<double>(S3);\n        print_vector<char>(C);\n        std::cout << std::endl;\n    }\n    ```\n\n6.  写一个`main()`函数，调用`run_quick_sort_test()` :\n\n    ```cpp\n    int main()\n    {\n        run_quick_sort_test();\n        return 0;\n    }\n    ```\n\n7.  您的最终输出应该如下所示:\n\n![Figure 4.13: Sorting by quicksort](img/C14498_04_13.jpg)\n\n###### 图 4.13:按快速排序排序\n\n然而，quicksort 的运行时间确实取决于我们对 pivot 的选择有多“好”。快速排序的最佳情况是任何一步的轴都是当前数组的中间元素；在这种情况下，快速排序能够在每一步将元素划分成大小相等的向量，因此，递归树的深度正好是 *log(n)* 。如果没有选择中间值作为枢轴，就会导致分区大小不平衡，从而导致更深的递归树和更长的运行时间。\n\n快速排序和合并排序的渐近复杂性如下所示:\n\n![Figure 4.14: Asymptotic complexity of quicksort and merge sort](img/C14498_04_14.jpg)\n\n###### 图 4.14:快速排序和合并排序的渐近复杂性\n\n### 活动 9:部分排序\n\n在最后两个练习中，我们实现了**全排序**算法，该算法以递增(或递减)的顺序对向量的所有元素进行排序。然而，在几个问题实例中，这可能会有些过分。例如，假设给你一个包含地球上所有人类年龄的向量，并要求你找出人口中最年长的 10%的年龄中位数。\n\n这个问题的一个天真的解决方案是对年龄向量进行排序，从向量中提取年龄最大的 10%的人的年龄，然后找到提取的向量的中值。然而，这种解决方案是浪费的，因为它所做的远远超过计算解决方案所严格需要的，也就是说，它对整个数组进行排序，最终只使用排序数组的 10%作为所需的解决方案。\n\n通过将合并排序、快速排序等全排序算法专门化为**部分排序算法**，可以得出此类问题的更好解决方案。部分排序算法只对给定向量中指定数量的元素进行排序，而对向量的其余部分不进行排序。\n\n部分快速排序描述如下:\n\n1.  假设给我们一个向量 *V* ，我们需要创建一个 *k* 元素的排序子向量。\n2.  在 *V* 上应用分区操作，假设 *V* 的第一个元素为枢轴(同样，这个选择完全是任意的)。分割操作的结果是两个向量， *L* 和 *R* ，其中 *L* 包含小于枢轴的 *V* 的所有元素， *R* 包含大于枢轴的所有元素。此外，透视的新位置是透视在排序数组中的“正确”位置。\n3.  使用 *L* 作为*步骤 1* 的输入。\n4.  如果*步骤 2* 中枢轴的新位置小于 *k* ，则使用 *R* 作为*步骤 1* 的输入。\n\n在本练习中，您的任务是实现部分快速排序算法，该算法使用随机生成的数组来测试算法的输出。具有大小为 *100* 和 *k = 100* 的向量的最终输出应该如下所示:\n\n![Figure 4.15: Sample output of Activity 9](img/C14498_04_15.jpg)\n\n###### 图 4.15:活动 9 的示例输出\n\n#### 注意\n\n这个活动的解决方案可以在第 510 页找到。\n\n### 线性时间选择\n\n在前一节中，我们看了一些简单的算法示例，这些算法使用了分治的模式，并被引入到了分区和合并操作中。到目前为止，我们对分治算法的看法仅限于递归地将每个中间步骤分成两个子步骤的算法。然而，存在某些问题，将每一步分成更多的子部分可以产生实质性的好处。在下一节中，我们将研究一个这样的问题——线性时间选择。\n\n想象一下，你负责为你的学校组织游行乐队。为了确保所有乐队成员看起来都一样，学生的身高保持一致是很重要的。此外，所有年级的学生都必须参加。为了解决这些问题，你提出了以下解决方案——你将只选择每个年级第 15 名最矮的学生参加游行。问题可以形式化为:给定一组随机排序的元素， *S* ，要求你在 *S* 中找到*I*T4 的最小元素。一个简单的解决方案是对输入进行排序，然后选择第 *i* *第*元素。但是这个解决方案的算法复杂度是 *O(n log n)* 。在本节中，我们将通过一个分治的解决方案来解决 *O(n)* 中的问题。\n\n我们的解决方案取决于正确使用分区操作。我们在前一小节中介绍的分区操作接收一个向量和一个轴，然后将向量分成两部分，一部分包含小于轴的所有元素，另一部分包含大于轴的所有元素。最终算法的工作原理如下:\n\n1.  假设给我们一个输入向量 *V* ，我们需要找到具有最小元素的*。*\n2.  将输入向量 *V* 划分为向量 *V* *1* 、 *V* *2* 、 *V* *3* 、*……*、 *V* *n/5* ，每个向量包含五个元素(如果需要，最后一个向量可以少于五个元素)。\n3.  接下来，我们对每个 *V* *i* 进行排序。\n4.  For each *V**i*, find the median, *m**i*, and collect all medians into a set, *M*, as shown here:\n\n    ![Figure 4.16: Finding the medians of each subvector](img/C14498_04_16.jpg)\n\n    ###### 图 4.16:找到每个子向量的中间\n\n5.  Find the median element, *q*, of *M*:\n\n    ![Figure 4.17: Finding the median of a set of medians](img/C14498_04_17.jpg)\n\n    ###### 图 4.17:求一组中位数的中位数\n\n6.  Use the partition operation on *V* using *q* as the pivot to get two vectors, *L* and *R*:\n\n    ![Figure 4.18: Partitioning the whole vector](img/C14498_04_18.jpg)\n\n    ###### 图 4.18:分割整个向量\n\n7.  By the definition of the partition operation, *L* contains all the elements less than *q* and *R* contains all the elements greater than *q*. Let's say *L* has *(k – 1)* elements:\n\n    –如果 *i = k* ，那么 *q* 就是 *V* 中的*I*T6 第个元素。\n\n    –如果 *i < k* ，设置 *V = L* ，进入*步骤 1* 。\n\n    –如果 *i > k* ，设置 *V = R* 和*I = I–k*，进入*步骤 1* 。\n\n下面的练习演示了这个算法在 C++ 中的实现。\n\n### 练习 21:线性时间选择\n\n在本练习中，我们将实现线性时间选择算法。让我们开始吧:\n\n1.  导入以下标题:\n\n    ```cpp\n    #include <iostream>\n    #include <vector>\n    #include <chrono>\n    #include <random>\n    #include <algorithm>\n    #include <numeric>\n    ```\n\n2.  编写这里显示的助手函数:\n\n    ```cpp\n    template<typename T>\n    auto find_median(typename std::vector<T>::iterator begin, typename std::vector<T>::iterator last)\n    {\n        // Sort the array\n        quick_sort<T>(begin, last);\n\n        // Return the middle element, i.e. median\n        return begin + (std::distance(begin, last)/2); \n    }\n    ```\n\n3.  在*练习 20* 、*快速排序*中，我们的分区函数假设给定向量中的第一个元素始终是要使用的轴。我们现在需要一种更通用的分区操作形式，它可以处理任何枢轴元素:\n\n    ```cpp\n    template <typename T>\n    auto partition_using_given_pivot(\n    typename std::vector<T>::iterator begin, \n    typename std::vector<T>::iterator end, \n    typename std::vector<T>::iterator pivot)\n    {\n            // Since the pivot is already given,\n            // Create two iterators pointing to the first and last element of the vector respectively\n        auto left_iter = begin;\n        auto right_iter = end;\n        while (true)\n        {\n            // Starting from the first element of vector, find an element that is greater than pivot.\n            while (*left_iter < *pivot && left_iter != right_iter)\n                left_iter++ ;\n            // Starting from the end of vector moving to the beginning, find an element that is lesser than the pivot.\n            while (*right_iter >= *pivot && \n                      left_iter != right_iter)\n                right_iter--;\n            // If left and right iterators meet, there are no elements left to swap. Else, swap the elements pointed to by the left and right iterators.\n            if (left_iter == right_iter)\n                break;\n            else\n                std::iter_swap(left_iter, right_iter);\n        }\n        if (*pivot > *right_iter)\n            std::iter_swap(pivot, right_iter);\n        return right_iter;\n    }\n    ```\n\n4.  使用以下代码实现我们的线性时间搜索算法:\n\n    ```cpp\n    // Finds ith smallest element in vector V\n    template<typename T>\n    typename std::vector<T>::iterator linear_time_select(\n    typename std::vector<T>::iterator begin,\n    typename std::vector<T>::iterator last, size_t i)\n    {\n        auto size = std::distance(begin, last);\n        if (size > 0 && i < size) {\n            // Get the number of V_i groups of 5 elements each\n            auto num_Vi = (size+4) / 5; \n            size_t j = 0;\n            // For each V_i, find the median and store in vector M\n            std::vector<T> M;\n            for (; j < size/5; j++)\n            {\n                auto b = begin + (j * 5);\n                auto l = begin + (j * 5) + 5;\n                M.push_back(*find_median<T>(b, l));\n            }\n            if (j * 5 < size)\n            {\n                auto b = begin + (j * 5);\n                auto l = begin + (j * 5) + (size % 5);\n                M.push_back(*find_median<T>(b, l));\n            }\n            // Find the middle element ('q' as discussed)\n               auto median_of_medians = (M.size() == 1)? M.begin():\n          linear_time_select<T>(M.begin(), \n                                M.end()-1, M.size() / 2);\n\n             // Apply the partition operation and find correct position 'k' of pivot 'q'.\n            auto partition_iter = partition_using_given_pivot<T>(begin, last, median_of_medians);\n            auto k = std::distance(begin, partition_iter)+1;\n            if (i == k)\n                return partition_iter;\n            else if (i < k)\n                return linear_time_select<T>(begin, partition_iter - 1, i);\n            else if (i > k)\n                return linear_time_select<T>(partition_iter + 1, last, i-k);\n        }\n        else {\n            return begin;\n        }\n    }\n    ```\n\n5.  添加下面代码中显示的合并排序实现。我们将使用排序算法来证明我们实现的正确性:\n\n    ```cpp\n    template <typename T>\n    std::vector<T> merge(std::vector<T>& arr1, std::vector<T>& arr2)\n    {\n        std::vector<T> merged;\n        auto iter1 = arr1.begin();\n        auto iter2 = arr2.begin();\n        while (iter1 != arr1.end() && iter2 != arr2.end())\n        {\n            if (*iter1 < *iter2)\n            {\n                merged.emplace_back(*iter1);\n                iter1++ ;\n            }\n            else\n            {\n                merged.emplace_back(*iter2);\n                iter2++ ;\n            }\n        }\n        if (iter1 != arr1.end())\n        {\n            for (; iter1 != arr1.end(); iter1++)\n                merged.emplace_back(*iter1);\n        }\n        else\n        {\n            for (; iter2 != arr2.end(); iter2++)\n                merged.emplace_back(*iter2);\n        }\n        return merged;\n    }\n    template <typename T>\n    std::vector<T> merge_sort(std::vector<T> arr)\n    {\n        if (arr.size() > 1)\n        {\n            auto mid = size_t(arr.size() / 2);\n            auto left_half = merge_sort(std::vector<T>(arr.begin(),\n                arr.begin() + mid));\n            auto right_half = merge_sort(std::vector<T>(arr.begin() + mid,\n                arr.end()));\n            return merge<T>(left_half, right_half);\n        }\n        return arr;\n    }\n    ```\n\n6.  最后，添加以下驱动和测试功能:\n\n    ```cpp\n    void run_linear_select_test()\n    {\n        std::vector<int> S1{ 45, 1, 3, 1, 2, 3, 45, 5, 1, 2, 44, 5, 7 };\n        std::cout << \"Original vector:\" << std::endl;\n        print_vector<int> (S1);\n        std::cout << \"Sorted vector:\" << std::endl;\n        print_vector<int>(merge_sort<int>(S1));\n        std::cout << \"3rd element: \" \n                     << *linear_time_select<int>(S1.begin(), S1.end() - 1, 3) << std::endl;\n        std::cout << \"5th element: \" \n                     << *linear_time_select<int>(S1.begin(), S1.end() - 1, 5) << std::endl;\n        std::cout << \"11th element: \" \n                     << *linear_time_select<int>(S1.begin(), S1.end() - 1, 11) << std::endl;\n    }\n    int main()\n    {\n        run_linear_select_test();\n        return 0;\n    }\n    ```\n\n7.  编译并运行代码。您的最终输出应该如下所示:\n\n![Figure 4.19: Finding the 3rd, 5th, and 11th elements using linear time selection](img/C14498_04_19.jpg)\n\n###### 图 4.19:使用线性时间选择查找第 3、5 和 11 个元素\n\n虽然对给定算法的详细理论分析超出了本章的范围，但该算法的运行时间值得讨论。前面算法工作的基本思想是，每次用输入 *V* 调用`linear_time_select()`时，应用一个分区操作，然后函数只在其中一个分区上递归调用自己。在每个递归步骤中，问题的大小至少减少 30%。因为找到五个元素的中值是一个恒定的时间操作，所以通过前面的算法得到的递归方程可以使用归纳法来求解，从而看到运行时间确实是 *O(n)* 。\n\n#### 注意\n\n线性时间选择算法的一个有趣的性质是，当 *V* 被分成每个都是五个元素的子向量时，它的众所周知的渐近复杂性(线性)被实现。找到一个恒定大小的子向量，从而产生更好的渐近复杂度，这仍然是一个公开的问题。\n\n## C++ 分而治之的标准库工具\n\n在前一节中，我们手动实现了各个击破算法的必要功能。然而，C++ 标准库附带了一大组预定义的函数，可以在编程时为我们节省大量工作。下表提供了在实现使用分治模式的算法时使用的最常用函数的便捷列表。我们将简要描述这些功能以供参考，但为了简洁起见，详细的实现不在本章讨论范围之内。请随意探索关于这些功能的更多信息；您应该能够根据我们在本章中介绍的概念来理解它们:\n\n![Figure 4.20: Some useful STL functions for algorithms](img/C14498_04_20_1.jpg)\n\n![](img/C14498_04_20_2.jpg)\n\n###### 图 4.20:算法的一些有用的 STL 函数\n\n## 在更高的抽象层次上划分和征服——MapReduce\n\n到目前为止，在本章中，我们已经将分治看作是一种算法设计技术，并使用它来解决我们的问题，使用一组预定义的分治-合并步骤。在本节中，我们将稍微绕一下路，看看当我们需要将软件扩展到单台机器的计算能力之外，并使用计算机集群来解决问题时，将问题分成更小的部分并分别解决每个部分的相同原理会有什么特别的帮助。\n\n原 **MapReduce** 论文开始如下:\n\n*“MapReduce 是一个用于处理和生成大型数据集的编程模型和相关实现。用户指定一个映射函数来处理键值对以生成一组中间键/值对，指定一个缩减函数来合并与同一中间键相关联的所有中间值。”*\n\n#### 注意\n\n你可以参考 Jeffrey Dean 和 Sanjay Ghemawat 在 2004 年发表的关于 MapReduce 模型的原始研究论文，这里:[https://static . googleuser content . com/media/research . Google . com/en/us/archive/MapReduce-osdi 04 . pdf](https://static.googleusercontent.com/media/research.google.com/en/us/archive/mapreduce-osdi04.pdf)。\n\n自从最初的论文出现以来，已经出现了几种 MapReduce 编程模型的开源实现，其中最引人注目的是 Hadoop。Hadoop 为用户提供了一个编程工具包，用于编写映射和简化功能，这些功能可以应用于存储在称为 Hadoop 分布式文件系统(HDFS)的分布式文件系统中的数据。由于 HDFS 可以很容易地扩展到通过网络连接的几千台机器的集群，因此 MapReduce 程序能够随着集群的大小进行扩展。\n\n然而，在这一节中，我们感兴趣的不是 Hadoop，而是作为编程范式的 MapReduce，以及它与手头主题的关联，即各个击破技术。我们将不再使用 Hadoop，而是坚持使用 MapReduce 的开源单机实现，该实现使用多线程来模拟任务并行化的原始工作者模型。\n\n### 映射和简化抽象\n\n术语*映射*和*简化*起源于函数式编程语言，如 Lisp。\n\n**映射**是一个操作，它接收一个容器 *C* ，并对 *C* 的每个元素应用一个给定的函数 *f(x)* 。使用 *f(x) = x* *2* 的例子如下图所示:\n\n![Figure 4.21: Mapping the values of a container](img/C14498_04_21.jpg)\n\n###### 图 4.21:映射容器的值\n\n**Reduce** 是一个操作，通过对 *C* 的每个元素 *x* 应用给定的函数 *f(acc，x)* ，来聚合容器 *C* 中的值，并返回单个值。如下图所示:\n\n![Figure 4.22: Reducing the values of a container](img/C14498_04_22.jpg)\n\n###### 图 4.22:减少容器的值\n\nC++ 标准库包含映射和约简操作，即分别为`std::transform()`和`std::accumulate()`(`std::reduce()`在 C++ 17 中也有)。\n\n#### 注意\n\n`std::accumulate()`是仅使用加法函数的 reduce 运算的受限形式。较新的编译器还提供了`std::reduce()`，它更通用，可以并行化。\n\n下面的练习演示了使用 C++ 标准库实现 MapReduce。\n\n### 练习 22:在 C++ 标准库中映射和减少\n\n在本练习中，我们将了解如何使用这些函数来进一步理解地图并减少操作。让我们开始吧:\n\n1.  导入以下标题:\n\n    ```cpp\n    #include <iostream>\n    #include <vector>\n    #include <chrono>\n    #include <random>\n    #include <algorithm>\n    #include <numeric>\n    ```\n\n2.  首先用随机元素创建一个数组:\n\n    ```cpp\n    void transform_test(size_t size)\n    {\n        std::vector<int> S, Tr;\n        std::random_device rd;\n        std::mt19937 rand(rd());\n        std::uniform_int_distribution<std::mt19937::result_type> uniform_dist(1, size);\n        // Insert random elements\n        for (auto i = 0; i < size; i++)\n            S.push_back(uniform_dist(rand));\n        std::cout << \"Original array, S: \";\n        for (auto i : S)\n            std::cout << i << \" \";\n        std::cout << std::endl;\n        std::transform(S.begin(), S.end(), std::back_inserter(Tr), \n                          [](int x) {return std::pow(x, 2.0); });\n        std::cout << \"Transformed array, Tr: \";\n        for (auto i : Tr)\n            std::cout << i << \" \";\n        std::cout << std::endl;\n        // For_each\n        std::for_each(S.begin(), S.end(), [](int &x) {x = std::pow(x, 2.0); });\n        std::cout << \"After applying for_each to S: \";\n        for (auto i : S)\n                std::cout << i << \" \";\n        std::cout << std::endl;\n    }\n    ```\n\n3.  The `transform_test()` function randomly generates a vector of a given size and applies a transformation, *f(x) = x**2*, to the vector.\n\n    #### 注意\n\n    `std::transform()`不改变原始向量，而是以单独的向量返回结果，而`std::for_each()`修改输入向量。两者的另一个区别是`std::transform()`不保证输入函数 *f* 会从容器的第一个元素应用到最后一个元素；也就是说，函数应用的顺序不一定与元素的顺序匹配。从 C++ 17 开始，`std::transform()`也支持本机并行化，接受`ExecutionPolicy`作为第一个参数。\n\n    减少操作在 C++ 标准库中实现为`std::accumulate()`和`std::reduce()`(仅在 C++ 17 及更高版本中可用):\n\n    ```cpp\n    void reduce_test(size_t size)\n    {\n        std::vector<int> S;\n        std::random_device rd;\n        std::mt19937 rand(rd());\n        std::uniform_int_distribution<std::mt19937::result_type> uniform_dist(1, size);\n        // Insert random elements\n        for (auto i = 0; i < size; i++)\n            S.push_back(uniform_dist(rand));\n        std::cout << std::endl << \"Reduce test== \" << std::endl << \"Original array, S: \";\n        for (auto i : S)\n            std::cout << i << \" \";\n        std::cout << std::endl;\n        // Accumulate\n        std::cout<<\"std::accumulate() = \" << std::accumulate(S.begin(), S.end(), 0, [](int acc, int x) {return acc+x; });\n        std::cout << std::endl;\n    }\n    ```\n\n4.  添加以下驱动代码:\n\n    ```cpp\n    int main() \n    {\n        transform_test(10);\n        reduce_test(10);\n        return 0;\n    }\n    ```\n\n5.  编译并运行代码。您的输出应该如下所示:\n\n![Figure 4.23: Mapping and reducing an array](img/C14498_04_23.jpg)\n\n###### 图 4.23:映射和缩减数组\n\n### 集成零件–使用 MapReduce 框架\n\n要使用 MapReduce 模型编写程序，我们必须能够在一系列两个阶段中表达我们想要的计算: **Map** (也称为 **Partition** )，其中程序读取输入并创建一组中间的 *<键、值>* 对，以及 **Reduce** ，其中中间的 *<键、值>* 对然后以所需的方式进行组合以生成最终结果。下图说明了这个想法:\n\n![Figure 4.24: Generalized MapReduce framework](img/C14498_04_24.jpg)\n\n###### 图 4.24:广义 MapReduce 框架\n\nHadoop 等框架为 MapReduce 编程模型增加的主要价值在于，它们使映射和减少操作变得分布式和高度可扩展，从而使计算在机器集群上运行，并减少了总耗时。\n\n在下面的练习中，我们将使用 MapReduce 框架来执行一个示例任务。\n\n#### 注意\n\n以下练习和活动需要在您的系统上安装 Boost C++ 库。按照以下链接获取 Boost 库:\n\nwindows:[https://www . boost . org/doc/libs/1 _ 71 _ 0/more/入门/windows.html](https://www.boost.org/doc/libs/1_71_0/more/getting_started/windows.html)\n\nLinux/macOS:[https://www . boost . org/doc/libs/1 _ 71 _ 0/more/入门/unix-variants.html](https://www.boost.org/doc/libs/1_71_0/more/getting_started/unix-variants.html)\n\n### 练习 23:使用 MapReduce 检查素数\n\n给定一个正整数 *N* ，我们希望找出 *1* 和 *N* 之间的素数。在本练习中，我们将了解如何使用 MapReduce 编程模型实现这一点，并使用多线程解决问题。让我们开始吧:\n\n1.  让我们首先包含所需的库，并定义一个函数，使用质因数分解来检查给定的数是否是质数:\n\n    ```cpp\n    #include <iostream>\n    #include \"mapreduce.hpp\"\n    namespace prime_calculator {\n        bool const is_prime(long const number)\n        {\n            if (number > 2)\n            {\n                if (number % 2 == 0)\n                    return false;\n                long const n = std::abs(number);\n                long const sqrt_number = static_cast<long>(std::sqrt(\n    static_cast<double>(n)));\n                for (long i = 3; i <= sqrt_number; i += 2)\n                {\n                    if (n % i == 0)\n                        return false;\n                }\n            }\n            else if (number == 0 || number == 1)\n                return false;\n            return true;\n        }\n    ```\n\n2.  下面的类用于生成连续数字之间具有给定差值的数字范围(也称为**步长** ):\n\n    ```cpp\n        template<typename MapTask>\n        class number_source : mapreduce::detail::noncopyable\n        {\n        public:\n            number_source(long first, long last, long step)\n                : sequence_(0), first_(first), last_(last), step_(step)\n            {\n            }\n            bool const setup_key(typename MapTask::key_type& key)\n            {\n                key = sequence_++ ;\n                return (key * step_ <= last_);\n            }\n            bool const get_data(typename MapTask::key_type const& key, typename MapTask::value_type& value)\n            {\n                typename MapTask::value_type val;\n                val.first = first_ + (key * step_);\n                val.second = std::min(val.first + step_ - 1, last_);\n                std::swap(val, value);\n                return true;\n            }\n        private:\n            long sequence_;\n            long const step_;\n            long const last_;\n            long const first_;\n        };\n    ```\n\n3.  以下功能定义了地图阶段要执行的步骤:\n\n    ```cpp\n        struct map_task : public mapreduce::map_task<long, std::pair<long, long> >\n        {\n            template<typename Runtime>\n            void operator()(Runtime& runtime, key_type const& key, \n    value_type const& value) const\n            {\n                for (key_type loop = value.first; \n                    loop <= value.second; loop++)\n                runtime.emit_intermediate(is_prime(loop), loop);\n            }\n        };\n    ```\n\n4.  Now, let's implement the reduce stage:\n\n    ```cpp\n        struct reduce_task : public mapreduce::reduce_task<bool, long>\n        {\n            template<typename Runtime, typename It>\n            void operator()(Runtime& runtime, key_type const& key, It it, It ite) const\n            {\n                if (key)\n                    std::for_each(it, ite, std::bind(&Runtime::emit, \n    &runtime, true, std::placeholders::_1));\n            }\n        };\n        typedef\n            mapreduce::job<\n                prime_calculator::map_task,\n                prime_calculator::reduce_task,\n                mapreduce::null_combiner,\n                prime_calculator::number_source<prime_calculator::map_task>> job;\n    } // namespace prime_calculator\n    ```\n\n    前面的命名空间有三个功能:首先，它定义了一个检查给定数字是否是质数的函数；其次，它定义了一个函数，该函数在给定的范围内生成一系列数字；第三，它定义了地图并减少了任务。如前所述，映射函数发出 *< k，v >* 对，其中 *k* 和 *v* 都属于`long`类型，如果 *v* 是素数，则 *k* 是 *1* ，如果 *v* 不是素数，则 *0* 。然后，reduce 函数充当过滤器，仅在 *k = 1* 时输出 *< k，v >* 对。\n\n5.  The following driver code then sets the relevant parameters and starts the MapReduce computation:\n\n    ```cpp\n    int main()\n    {\n        mapreduce::specification spec;\n        int prime_limit = 1000;\n        // Set number of threads to be used\n        spec.map_tasks = std::max(1U, std::thread::hardware_concurrency());\n        spec.reduce_tasks = std::max(1U, std::thread::hardware_concurrency());\n        // Set the source of numbers in given range\n        prime_calculator::job::datasource_type datasource(0, prime_limit, prime_limit / spec.reduce_tasks);\n        std::cout << \"\\nCalculating Prime Numbers in the range 0 .. \" << prime_limit << \" ...\" << std::endl;\n\n    std::cout << std::endl << \"Using \"\n            << std::max(1U, std::thread::hardware_concurrency()) << \" CPU cores\";\n        // Run mapreduce\n        prime_calculator::job job(datasource, spec);\n        mapreduce::results result;\n        job.run<mapreduce::schedule_policy::cpu_parallel<prime_calculator::job> >(result);\n\n        std::cout << \"\\nMapReduce finished in \" \n    << result.job_runtime.count() << \" with \" \n    << std::distance(job.begin_results(), job.end_results()) \n    << \" results\" << std::endl;\n\n    // Print results\n        for (auto it = job.begin_results(); it != job.end_results(); ++ it)\n            std::cout << it->second << \" \";\n        return 0;\n    }\n    ```\n\n    驱动程序代码设置 MapReduce 框架所需的参数，运行计算，从 Reduce 函数收集结果，最后输出结果。\n\n6.  编译并运行前面的代码。您的输出应该如下所示:\n\n![](img/C14498_04_25.jpg)\n\n###### 图 4.25:使用 MapReduce 框架计算素数\n\n使用 MapReduce 模型编程的主要好处是，它产生了可大规模扩展的软件。我们在本练习中使用的 MapReduce 框架仅在单台机器上使用多线程来实现并行化。但是，如果它能够支持分布式系统，我们在这里编写的相同代码就可以在大型服务器集群上运行，使计算能够扩展到大规模。将前面的代码移植到诸如 Hadoop 之类的系统在 Java 中是一个微不足道的练习，但是超出了本书的范围。\n\n### 活动 10:在 MapReduce 中实现字数统计\n\n在本章中，我们已经看到了分治技术背后的思想作为一种非常有用的算法设计技术，以及在提供处理大型复杂计算的有用工具方面是多么强大。在本练习中，我们将练习使用上一节中介绍的 MapReduce 模型，将一个大问题分成更小的部分，求解更小的部分，并合并后续的结果。\n\n我们的问题定义取自原始的 MapReduce 论文，给出如下:给定一组包含文本的文件，找到每个单词在文件中出现的频率。例如，假设给你两个文件，内容如下:\n\n文件 1:\n\n```cpp\nThe quick brown fox jumps over a rabbit\n```\n\n文件 2:\n\n```cpp\nThe quick marathon runner won the race\n```\n\n考虑到输入文件，我们的程序应该输出以下结果:\n\n```cpp\nThe         2\nquick       2\na           1\nbrown       1\nfox         1\njumps       1\nmarathon    1\nover        1\nrabbit      1\nrace        1\nrunner      1\nthe         1\nwon         1\n```\n\n这样的问题经常出现在索引工作负载中，也就是说，当您被给予一个大的文本语料库并且需要索引内容以便可以更快地对文本进行后续搜索时。谷歌和必应等搜索引擎大量使用此类索引。\n\n在本练习中，您需要实现映射并减少字数问题的阶段。由于这涉及到特定于我们库的很大一部分代码，所以在`mapreduce_wordcount_skeleton.cpp`中已经为您提供了样板代码。\n\n**活动指南:**\n\n1.  Read through and understand the given code in `mapreduce_wordcount_skeleton.cpp`. You will notice that we need to import the Boost libraries in the header. Another thing to note is that the map stage in the given code creates *< k, v >* pairs, where *k* is a string and *v* is set to *1*. For example, say your set of input files contained a random combination of words, *w**1*, *w**2*, *w**3*, …, *w**n*. If so, the map stage should output *< k, 1>* pairs with *k = {w**1**, w**2**, w**3**, …, w**n**}*, as illustrated in the following diagram:\n\n    ![Figure 4.26: Mapping stage](img/C14498_04_26.jpg)\n\n    ###### 图 4.26:映射阶段\n\n2.  地图阶段的骨架代码如下:\n\n    ```cpp\n    struct map_task : public mapreduce::map_task<\n        std::string,                            // MapKey (filename)\n        std::pair<char const*, std::uintmax_t>> // MapValue (memory mapped file               \n                                                   // contents)\n    {\n    template<typename Runtime>\n        void operator()(Runtime& runtime, key_type const& key, \n                                             value_type& value) const\n        {\n            // Write your code here.\n            // Use runtime.emit_intermediate() to emit <k,v> pairs\n        }\n    };\n    ```\n\n3.  Since the map stage of the problem generated *< k, 1 >* pairs, the reduce task of our program should now combine the pairs with matching values of *k*, as shown here:\n\n    ![Figure 4.27: Reducing stage](img/C14498_04_27.jpg)\n\n    ###### 图 4.27:缩小阶段\n\n4.  在给定的代码中，reduce 任务接受两个迭代器，可以用来迭代具有相同键的元素，即`it`和`ite`之间的所有元素保证具有相同的键。然后，您的 reduce 阶段应该创建一个新的 *< k，v >* 对，其中 *k* 设置为输入对的键，而 *v* 等于输入对的数量:\n\n    ```cpp\n    template<typename KeyType>\n    struct reduce_task : public mapreduce::reduce_task<KeyType, unsigned>\n    {\n        using typename mapreduce::reduce_task<KeyType, unsigned>::key_type;\n        template<typename Runtime, typename It>\n        void operator()(Runtime& runtime, key_type const& key, It it, It const ite) const\n        {\n            // Write your code here.\n            // Use runtime.emit() to emit the resulting <k,v> pairs\n        }\n    };\n    ```\n\n5.  `testdata/`给你一组测试数据。编译并运行您的代码。输出应该如下所示:\n\n![Figure 4.28: Getting the frequency of words in the given input files](img/C14498_04_28.jpg)\n\n###### 图 4.28:获取给定输入文件中的词频\n\n#### 注意\n\n这个活动的解决方案可以在第 514 页找到。\n\n## 总结\n\n在这一章中，我们以两种不同的方式讨论了分而治之:首先是作为一种算法设计范式，然后是它在设计帮助我们扩展软件的其他工具中的使用。我们介绍了一些标准的分治算法(合并排序和快速排序)。我们还看到了像**划分**这样的简单操作是如何成为解决不同问题的基础的，例如部分排序和线性时间选择。\n\n在实践中实现这些算法时要记住的一个重要思想是将保存数据的数据结构与算法本身的实现分离。使用 C++ 模板通常是实现这种分离的好方法。我们看到 C++ 标准库附带了一大组可用于实现分治算法的原语。\n\n分而治之背后的基本思想的简单性使它成为解决问题的一个非常有用的工具，并允许创建并行化框架，如 MapReduce。我们还看到了一个使用 MapReduce 编程模型在给定范围内查找素数的例子。\n\n在下一章中，我们将介绍贪婪算法的设计范式，这种设计范式产生了像 Dijkstra 算法这样的解决方案来寻找图中的最短路径。"
  },
  {
    "path": "docs/cpp-dsal-design-principle/05.md",
    "content": "# 五、贪婪算法\n\n## 学习目标\n\n本章结束时，您将能够:\n\n*   描述算法设计的贪婪方法\n*   识别问题的最优子结构和贪婪选择性质\n*   实现贪婪算法，如分数背包和贪婪图着色\n*   使用不相交集数据结构实现克鲁斯卡尔最小生成树算法\n\n在这一章中，我们将看看各种“贪婪”的算法设计方法，看看它们如何应用于解决现实世界的问题。\n\n## 简介\n\n在前一章中，我们讨论了分治算法设计技术，该技术通过将输入分成较小的子问题，求解每个子问题，然后合并结果来解决给定的问题。继续我们算法设计范例的主题，我们现在将看看我们的下一个主题:**贪婪方法**。\n\n在每次迭代中，贪婪算法会选择“看似最好”的选项。换句话说，问题的贪婪解由一系列局部最优解组成给定问题的全局最优解。例如，下面的截图显示了汽车从华盛顿州 DC 的杜勒斯国际机场到东河谷镇办公楼的最短路径。自然，对于路径上不是起点和终点的任意两点，所示路径也是最短的:\n\n![Figure 5.1: A route from an airport to an office in Washington DC (Source: project-osrm.org)](img/C14498_05_01.jpg)\n\n###### 图 5.1:从机场到 DC 华府办公室的路线(来源:project-osrm.org)\n\n因此，我们可以推断，整个最短路径 P 实际上是沿着 P 的道路网络的顶点之间的几条最短路径的串联。因此，如果要求我们设计一个最短路径算法，一种可能的策略如下:从原点顶点开始，绘制一条到尚未探索的最近顶点的路径，然后重复，直到我们到达目的顶点。恭喜您–您刚刚使用迪克斯特拉算法解决了最短路径问题，该算法与为谷歌地图和必应地图等商业软件提供动力的算法相同！\n\n令人期待的是，贪婪算法所采取的简单方法使得它们只适用于算法问题的一小部分。然而，贪婪方法的简单性通常使其成为“第一次攻击”的优秀工具，通过它我们可以了解潜在问题的属性和行为，然后可以使用其他更复杂的方法来解决这些问题。\n\n在这一章中，我们将研究给定问题适合贪婪解的条件——最优子结构和贪婪选择性质。我们将会看到，当一个问题可以被证明具有这两个性质时，一个贪婪的解决方案肯定会产生正确的结果。我们还将看到一些在实践中使用贪婪解决方案的现实问题的例子，我们将在这一章的最后讨论最小生成树问题，它通常出现在电信和供水网络、电网和电路设计的情况下。但是首先，让我们先来看一些更简单的问题，这些问题可以使用贪婪算法来解决。\n\n## 基本贪婪算法\n\n在本节中，我们将研究两个标准问题，可以使用贪婪方法解决:**最短作业优先调度**和**分数背包**问题。\n\n### 最短作业优先调度\n\n假设你在银行排队。今天很忙，排队的人有 *N* 人，但是银行只有一个柜台开着(也是真的倒霉的一天！).假设一个人，*p*T4I*a*T8】I 的时间量在柜台得到服务。由于排队的人都很理性，所以大家都同意重新排序自己在队列中的位置，这样排队的每个人的*平均等待时间*就最小化了。你的任务是找到一种方法来重新排序队列中的人。你会如何解决这个问题？\n\n![Figure 5.2: The original queue](img/C14498_05_02.jpg)\n\n###### 图 5.2:原始队列\n\n为了进一步讨论这个问题，我们来看一个例子。上图为原始队列示例，其中 *A* *i* 显示服务时间， *W* *i* 显示第 *i* *个*人的等待时间。离柜台最近的人可以立即开始接受服务，所以他们的等待时间是 0。排在第二的人必须等到第一个人完成，所以他们必须等待*a**1**= 8*个时间单位才能得到服务。以类似的方式继续，第 *i* *个*人的等待时间等于队列中排在他们前面的所有*I–1*人的服务时间总和。\n\n解决这个问题的一个线索是这样的:既然我们在寻求最小化*平均等待时间*，我们就必须想办法尽可能减少最大可能人群的等待时间。减少所有人等待时间的一个方法是以最快的速度完成工作。通过对队列中的所有人重复这个想法，我们的解决方案得到了以下重新排序的队列:\n\n![Figure 5.3: The reordered queue with the minimum average waiting time](img/C14498_05_03.jpg)\n\n###### 图 5.3:平均等待时间最短的重新排序队列\n\n请注意，我们的重新排序队列的平均等待时间为 8.87 个单位，而原始排序的平均等待时间为 15.25 个单位，这大约是原来的 2 倍。\n\n### 练习 24:最短作业优先调度\n\n在本练习中，我们将通过采用上图所示的类似示例来实现最短作业优先调度解决方案。我们将考虑 10 个人排队，并尽量减少所有人的平均等待时间。让我们开始吧:\n\n1.  首先添加所需的标题，并创建计算等待时间和输入/输出的函数:\n\n    ```cpp\n    #include <iostream>\n    #include <algorithm>\n    #include <vector>\n    #include <random>\n    #include <numeric>\n    // Given a set of service times, computes the service times for all users\n    template<typename T>\n    auto compute_waiting_times(std::vector<T>& service_times)\n    {\n        std::vector<T> W(service_times.size());\n        W[0] = 0;\n\n        for (auto i = 1; i < service_times.size(); i++)\n            W[i] = W[i - 1] + service_times[i - 1];\n        return W;\n    }\n    // Generic function to print a vector\n    template<typename T>\n    void print_vector(std::vector<T>& V)\n    {\n        for (auto& i : V)\n            std::cout << i << \" \";\n        std::cout << std::endl;\n    }\n    template<typename T>\n    void compute_and_print_waiting_times(std::vector<T>& service_times)\n    {\n        auto waiting_times = compute_waiting_times<int>(service_times);\n\n        std::cout << \"Service times: \" << std::endl;\n        print_vector<T>(service_times);\n        std::cout << \"Waiting times: \" << std::endl;\n        print_vector<T>(waiting_times);\n        std::cout << \"Average waiting time = \"\n            << std::accumulate(waiting_times.begin(),            waiting_times.end(), 0.0) /\n            waiting_times.size();\n        std::cout<< std::endl;\n    }\n    ```\n\n2.  添加主解算器和驱动代码，如下图:\n\n    ```cpp\n    void shortest_job_first(size_t size)\n    {\n        std::vector<int> service_times;\n        std::random_device rd;\n        std::mt19937 rand(rd());\n        std::uniform_int_distribution<std::mt19937::result_type> uniform_dist(1, size);\n        // Insert random elements as service times\n        service_times.reserve(size);\n        for (auto i = 0; i < size; i++)\n            service_times.push_back(uniform_dist(rand));\n        compute_and_print_waiting_times<int>(service_times);\n        // Reorder the elements in the queue\n        std::sort(service_times.begin(), service_times.end());\n        compute_and_print_waiting_times<int>(service_times);\n    }\n    int main(int argc, char* argv[])\n    {\n        shortest_job_first(10);\n    }\n    ```\n\n3.  编译并运行代码！您的输出应该如下所示:\n\n![Figure 5.4: Output of the program to schedule the shortest job first](img/C14498_05_04.jpg)\n\n###### 图 5.4:首先安排最短作业的程序输出\n\n## 背包问题\n\n在本节中，我们将讨论标准的**背包问题**，也称为 0-1 背包问题，已知它是 NP 完全的，因此不允许我们有任何多项式时间的解。然后，我们将把我们的讨论转向背包问题的一个版本，称为**分数背包问题**，它可以使用贪婪的方法来解决。我们在这一部分的重点是演示如何即使是问题定义之间的细微差异也能导致解决方案策略的巨大变化。\n\n### 背包问题\n\n假设给你一组对象， *O = {O* *1* *，O* *2* *，…，O* *n* *}* ，每一个都有一定的权重， *W* *i* ，值为 *V* *i* *。*还会给你一个只能装总重量 T 单位的包(或背包)。现在，假设你的任务是找出一套放在包里的东西，这样总重量就小于或等于 T，这些东西的总价值就是它可能的最大值。\n\n这个问题的一个真实例子可以理解，如果你想象一个旅行交易者在所有交易中赚取固定的百分比利润。他们希望携带最大价值的货物，以使他们的利润最大化，但他们的车辆(或背包)最多只能容纳 T 个重量单位。交易者知道每件物品的确切重量和价值。他们应该携带哪一组物品，以使交易中携带的物品的总价值最大化？\n\n![Figure 5.5: The knapsack problem](img/C14498_05_05.jpg)\n\n###### 图 5.5:背包问题\n\n上图中出现的问题是众所周知的背包问题，并且已经被证明是 NP 完全的。换句话说，这个问题没有已知的多项式时间解决方案。因此，我们必须查看对象的所有可能组合，以找到在总重量仅为 *T* 单位的情况下具有最大值的组合。上图显示了容量为 8 个单位的背包的两种填充方式。灰色的物体是那些被选择放入背包的物体。我们可以看到第一组对象的总值为 40，第二组对象的总值为 37，两种情况下的总重量都是 8 个单位。因此，第二组对象是比第一组对象更好的选择。为了找到最佳可能的对象集，我们必须列出所有可能的组合，并选择具有最大值的组合。\n\n### 分数背包问题\n\n现在，我们将对上一小节中给出的背包问题进行一个小的修改:假设我们现在被允许将每个对象分成我们需要的多个部分，然后我们可以选择每个对象中我们想要保留在背包中的部分。\n\n就现实世界的类比而言，假设我们前面类比中的交易者正在交易石油、谷物和面粉等物品。交易者可以选择任何较小的重量单位。\n\n与标准背包的 NP 完全性相反，分数背包问题有一个简单的解决方案:根据元素的单位重量比的值来排序，并“贪婪地”选择尽可能多的具有最大比例的对象。下图显示了当背包容量设置为 8 个单位时，给定对象集的最佳选择。请注意，所选对象是单位重量比值最高的对象:\n\n![Figure 5.6: The fractional knapsack problem](img/C14498_05_06.jpg)\n\n###### 图 5.6:分数背包问题\n\n我们将在下面的练习中实现这个解决方案。\n\n### 练习 25:分数背包问题\n\n在本练习中，我们将考虑 10 个项目，并尝试最大化我们背包中的价值，该背包最大可容纳 25 个单位的重量。让我们开始吧:\n\n1.  First, we will begin by adding the required headers and defining an `Object` struct that will represent one object in our solution:\n\n    ```cpp\n    #include <iostream>\n    #include <algorithm>\n    #include <vector>\n    #include <random>\n    #include <numeric>\n    template <typename weight_type, \n        typename value_type, \n        typename fractional_type>\n    struct Object\n    {\n        using Wtype = weight_type;\n        using Vtype = value_type;\n        using Ftype = fractional_type;\n        Wtype weight;\n        Vtype value;\n        Ftype value_per_unit_weight;\n        // NOTE: The following overloads are to be used for std::sort() and I/O\n        inline bool operator< (const Object<Wtype,Vtype,Ftype>& obj) const\n        {\n            // An object is better or worse than another object only on the\n            // basis of its value per unit weight\n            return this->value_per_unit_weight < obj.value_per_unit_weight;\n        }\n        inline bool operator== (const Object<Wtype, Vtype, Ftype>& obj) const\n        {\n            // An object is equivalent to another object only if \n            // its value per unit weight is equal\n            return this->value_per_unit_weight == obj.value_per_unit_weight;\n        }\n        // Overloads the << operator so an object can be written directly to a stream\n        // e.g. Can be used as std::cout << obj << std::endl;\n        template <typename Wtype,\n            typename Vtype,\n            typename Ftype>\n        friend std::ostream& operator<<(std::ostream& os, \n                             const Object<Wtype,Vtype,Ftype>& obj);\n    };\n    template <typename Wtype,\n        typename Vtype,\n        typename Ftype>\n    std::ostream& operator<<(std::ostream& os, const Object<Wtype,Vtype,Ftype>& obj)\n    {\n        os << \"Value: \"<<obj.value \n        << \"\\t Weight: \" << obj.weight\n            <<\"\\t Value/Unit Weight: \" << obj.value_per_unit_weight;\n        return os;\n    }\n    ```\n\n    请注意，我们已经重载了`<`和`==`运算符，因为我们将在`objects`向量上使用`std::sort()`。\n\n2.  The code for the fractional knapsack solver is as follows:\n\n    ```cpp\n    template<typename weight_type, \n        typename value_type, \n        typename fractional_type>\n    auto fill_knapsack(std::vector<Object<weight_type, value_type,fractional_type>>& objects, \n                        weight_type knapsack_capacity)\n    {\n\n        std::vector<Object<weight_type, value_type, fractional_type>> knapsack_contents;\n        knapsack_contents.reserve(objects.size());\n\n        // Sort objects in the decreasing order\n        std::sort(objects.begin(), objects.end());\n        std::reverse(objects.begin(), objects.end());\n        // Add the 'best' objects to the knapsack\n        auto current_object = objects.begin();\n        weight_type current_total_weight = 0;\n        while (current_total_weight <= knapsack_capacity && \n    current_object != objects.end())\n        {\n            knapsack_contents.push_back(*current_object);\n\n            current_total_weight += current_object->weight;\n            current_object++ ;\n        }\n        // Since the last object overflows the knapsack, adjust weight\n        auto weight_of_last_obj_to_remove = current_total_weight - knapsack_capacity;\n        knapsack_contents.back().weight -= weight_of_last_obj_to_remove;\n        knapsack_contents.back().value -= knapsack_contents.back().value_per_unit_weight * \n                            weight_of_last_obj_to_remove;\n        return knapsack_contents;\n    }\n    ```\n\n    前面的函数按照价值/重量比的降序对对象进行排序，然后挑选能够放入背包的对象的所有部分，直到背包满为止。\n\n3.  Finally, to test our implementation, add the following test and driver code:\n\n    ```cpp\n    void test_fractional_knapsack(unsigned num_objects, unsigned knapsack_capacity)\n    {\n        using weight_type = unsigned;\n        using value_type = double;\n        using fractional_type = double;\n        // Initialize the Random Number Generator\n        std::random_device rd;\n        std::mt19937 rand(rd());\n        std::uniform_int_distribution<std::mt19937::result_type> \n    uniform_dist(1, num_objects);\n\n        // Create a vector of objects\n        std::vector<Object<weight_type, value_type, fractional_type>> objects;\n        objects.reserve(num_objects);\n        for (auto i = 0; i < num_objects; i++)\n        {\n            // Every object is initialized with a random weight and value\n            auto weight = uniform_dist(rand);\n            auto value = uniform_dist(rand);\n            auto obj = Object<weight_type, value_type, fractional_type> { \n                static_cast<weight_type>(weight), \n                static_cast<value_type>(value), \n                static_cast<fractional_type>(value) / weight \n            };\n            objects.push_back(obj);\n        }\n        // Display the set of objects\n        std::cout << \"Objects available: \" << std::endl;\n        for (auto& o : objects)\n            std::cout << o << std::endl;\n        std::cout << std::endl;\n        // Arbitrarily assuming that the total knapsack capacity is 25 units\n        auto solution = fill_knapsack(objects, knapsack_capacity);\n        // Display items selected to be in the knapsack\n        std::cout << \"Objects selected to be in the knapsack (max capacity = \"\n            << knapsack_capacity<< \"):\" << std::endl;\n        for (auto& o : solution)\n            std::cout << o << std::endl;\n        std::cout << std::endl;\n    }\n    int main(int argc, char* argv[])\n    {\n        test_fractional_knapsack(10, 25);\n    }\n    ```\n\n    前面的函数创建对象，并用 STL 随机数生成器中的随机数据初始化它们。接下来，它调用我们的分数背包求解器的实现，然后显示结果。\n\n4.  编译并运行这段代码！您的输出应该如下所示:\n\n![Figure 5.7: Output of Exercise 25](img/C14498_05_07.jpg)\n\n###### 图 5.7:练习 25 的输出\n\n请注意解算器是如何获取分数的，也就是说，按重量计算，最后一个对象的 5 个单位中只有 4 个。这是一个如何在选择对象保存在背包中之前对其进行分区的示例，这将分数背包与 0-1(标准)背包问题区分开来。\n\n### 活动 11:区间调度问题\n\n想象一下，你的待办事项清单上有一组任务(洗碗、去超市买杂货、为统治世界的秘密项目工作，以及其他类似的杂务)。每个任务由一个标识来标识，并且只能在特定的开始和结束时间之间完成。假设您希望完成最大数量的任务。在什么子集上，以什么顺序，你应该完成你的任务来实现你的目标？假设在任何时间点，您只能处理一项任务。\n\n例如，考虑下图所示的问题实例。我们被赋予了四个不同的任务，我们可以花时间去完成它们(矩形框代表任务可以完成的时间间隔):\n\n![Figure 5.8: Given task schedules](img/C14498_05_08.jpg)\n\n###### 图 5.8:给定的任务时间表\n\n下图显示了任务的最佳计划，它最大化了已完成的任务总数:\n\n![Figure 5.9: Optimal selection of tasks](img/C14498_05_09.jpg)\n\n###### 图 5.9:任务的最佳选择\n\n请注意，不完成任务 3 如何让我们完成任务 1 和 2，从而增加已完成任务的总数。在本练习中，您将需要实现这个贪婪的间隔调度解决方案。\n\n解决此活动的高级步骤如下:\n\n1.  假设每个任务都有一个开始时间、一个结束时间和一个标识。创建描述任务的结构。我们将用这个结构的不同实例来表示不同的任务。\n2.  实现一个创建 N 个任务的`std::list`的函数，从 1 到 N 顺序设置它们的标识，并使用随机数生成器的值作为开始和结束时间。\n3.  Implement the scheduling function as follows:\n\n    a.按照任务结束时间的递增顺序对任务列表进行排序。\n\n    b.贪婪地选择以最早的结束时间完成任务。\n\n    c.删除与当前所选任务重叠的所有任务(当前任务结束前开始的所有任务)。\n\n    d.如果任务仍在列表中，转到*步骤 b* 。否则，返回选择的任务向量。\n\n您的最终输出应该如下所示:\n\n![Figure 5.10: Expected output of Activity 11](img/C14498_05_10.jpg)\n\n###### 图 5.10:活动 11 的预期产出\n\n#### 注意\n\n这个活动的解决方案可以在第 516 页找到。\n\n### 对贪婪算法的要求\n\n在前一节中，我们看了贪婪方法给出最优解的问题的例子。然而，当且仅当一个问题具有两个性质时，可以使用贪婪方法最优地解决该问题:**最优子结构**性质和**贪婪选择**性质。在本节中，我们将尝试理解这些属性，并向您展示如何识别问题是否表现出这些属性。\n\n**最优子结构**:当一个给定问题的最优解 P 由它的子问题的最优解组成时，那么 P 被称为具有最优子结构。\n\n**贪婪选择**:当一个给定问题的最优解 P 可以通过在每次迭代中选择局部最优解来达到时，P 被称为具有贪婪选择性质。\n\n为了理解最优子结构和贪婪选择性质，我们将实现 Kruskal 的最小生成树算法。\n\n### 最小生成树问题\n\n最小生成树问题可以表述如下:\n\n*“给定一个图，G = < V，E >，其中 V 是顶点集，E 是边集，每个边都与一个边权重相关联，找到一棵树 T，它跨越 V 中的所有顶点，并且具有最小的总权重。”*\n\nMST 问题的一个实际应用是供水和运输网络的设计，因为设计者通常希望最小化所使用的管道或所创建的道路的总长度，并且仍然确保服务到达所有指定的用户。让我们试着用下面的例子把这个问题分开。\n\n假设给你一张地图上 12 个村庄的位置，并要求你找出需要修建的道路的最小总长度，以便所有的村庄都可以相互到达，并且道路不会形成一个循环。假设每条路都可以沿任一方向穿越。这个问题中村庄的一种自然表示是使用图数据结构。让我们假设下图的顶点 *G* 代表 12 个给定村庄的位置， *G* 的边代表顶点之间的距离:\n\n![Figure 5.11: Graph G representing the villages and distances between them](img/C14498_05_11.jpg)\n\n###### 图 5.11:代表村庄和村庄之间距离的曲线图\n\n构造最小生成树 T 的简单贪婪算法如下:\n\n1.  将 *G* 的所有边加入一个最小堆， *H* 。\n2.  从 *H* 开始，弹出一个边， *e* 。自然， *e* 在 *H* 中所有边中成本最小。\n3.  如果 *e* 的两个顶点都已经在 *T* 中，这意味着添加 *e* 将在 *T* 中创建一个循环。因此，丢弃 *e* ，转到步骤 2。否则，继续下一步。\n4.  将 *e* 插入最小生成树， *T* 。\n\n让我们花点时间来思考一下为什么这个策略有效。在第 2 步和第 3 步循环的每次迭代中，我们取成本最低的边，检查它是否给我们的解增加了顶点。这存储在最小生成树 *T* 中。如果有，我们给 *T* 加边；否则，我们丢弃该边并选择另一条具有最小值的边。我们的算法是贪婪的，因为在每次迭代中，它选择最小的边权重添加到解中。前面的算法发明于 1956 年，被称为**克鲁斯卡尔最小生成树算法**。将该算法应用到图 5.11*所示的图中，得到以下结果:*\n\n![Figure 5.12: Graph G showing the minimum spanning tree, T (with red edges)](img/C14498_05_12.jpg)\n\n###### 图 5.12:显示最小生成树 T(红色边)的图 G\n\n最小生成树中边的总权重 T 为 *(2 × 1) + (3 × 2) + (2 × 3) = 14* 个单位。因此，我们的问题的答案是，至少需要建造 12 个单位的道路。\n\n我们如何知道我们的算法确实是正确的？我们需要回到最优子结构和贪婪选择的定义，并证明 MST 问题表现出这两个性质。虽然这些性质的严格数学证明超出了本书的范围，但这些证明背后的直观思想如下:\n\n**最优子结构**:我们将用矛盾来证明这一点。让我们假设 MST 问题没有表现出最优子结构；也就是说，最小生成树不是由一组较小的最小生成树组成的:\n\n1.  假设给我们一个最小生成树 *T* ，在图的顶点 *G* 上，让我们从 *T* 移除任何边 *e* 。去除 *e* 将 *T* 分解成更小的树，*T**1**T**2*。\n2.  由于我们假设 MST 问题不表现出最优子结构，因此在 *T* *1* 的顶点上一定存在总权重较小的生成树。取此生成树，加上边*e**T**2*。这棵新树将是 *T'* 。\n3.  现在，由于 *T'* 的总重量小于 *T* 的总重量，这与我们最初的假设 *T* 是一个 MST 相矛盾。因此，最小二乘问题必须表现出最优子结构性质。\n\n**贪婪选择**:如果 MST 问题表现出贪婪选择属性，那么对于一个顶点， *v* ，连接 *v* 到图的其余部分的最小权边， *G* ，应该始终是最小生成树的一部分， *T* 。我们可以通过矛盾来证明这个假设，如下:\n\n1.  假设一条边 *(u，v)* 是连接 *v* 到 *G* 中任何其他顶点的最小权重边。假设 *(u，v)* 不是 *T* 的一部分。\n2.  如果 *(u，v)* 不是 *T* 的一部分，那么 *T* 必须由将 *v* 连接到其余 *G* 的一些其他边组成。设此边为 *(x，v)* 。由于 *(u，v)* 是最小重量边，根据定义， *(x，v)* 的重量大于 *(u，v)* 的重量。\n3.  A tree with a lesser total weight than *T* can be obtained if *(x, v)* is replaced with *(u, v)* in *T*. This contradicts our assumption that *T* is the minimum spanning tree. Therefore, the MST problem must exhibit the greedy choice property.\n\n    #### 注意\n\n    正如我们前面提到的，我们也可以采取严格的数学方法来证明 MST 问题表现出最优子结构性质，并且适用于贪婪选择性质。你可以在这里找到:[https://OCW . MIT . edu/courses/electric-engineering-and-computer-science/6-046j-design-and-analysis-of-algorithms-spring-2015/讲义/MIT6_046JS15_lec12.pdf](https://ocw.mit.edu/courses/electrical-engineering-and-computer-science/6-046j-design-and-analysis-of-algorithms-spring-2015/lecture-notes/MIT6_046JS15_lec12.pdf) 。\n\n让我们思考一下如何实现 Kruskal 算法。我们在*第 2 章*、*树、堆和图*中介绍了图和堆数据结构，因此我们知道如何实现步骤 1 和 2。第三步有点复杂。我们需要一个数据结构来存储图的边，并告诉我们添加新的边是否会创建一个循环，其中已经存储了边的任何可能组合。这个问题可以通过使用不相交集数据结构来解决。\n\n### 不相交集(或联合查找)数据结构\n\n一个**不相交集数据结构**由一个元素森林(一组树)组成，其中每个元素由一个数字标识表示，有一个“等级”，并包含一个指向其父元素的指针。当数据结构初始化时，它以等级为 0 的 *N* 个独立元素开始，每个元素都是只包含元素本身的树的一部分。数据结构支持另外两种操作:\n\n*   对树的`find`操作返回该树的根元素\n*   应用于两棵树的`union`操作将较小的树合并成较大的树，其中树的大小存储为其根的等级。\n\n更准确地说，分离集数据结构支持以下操作:\n\n*   `Make-Set`:这用 N 个元素初始化数据结构，将每个元素的秩设置为 0，父指针指向自身。下图显示了用五个元素初始化的不相交集 *DS* 的示例。圆圈内的数字表示元素标识，括号内的数字表示等级，箭头表示指向根元素的指针:\n\n![Figure 5.13: Initializing disjoint set with five elements](img/C14498_05_13.jpg)\n\n###### 图 5.13:用五个元素初始化不相交集\n\n在这个阶段，数据结构由五棵树组成，每棵树由一个元素组成。\n\n*   `Find`:从给定元素 *x* 开始，`find`操作跟随元素的父指针，直到到达树的根。根元素的父元素是根本身。在上一个例子中，每个元素都是树的根，因此这个操作将返回树中唯一的元素。\n*   `Union`:给定两个元素 *x* 和 *y* ，`union`运算找到 *x* 和 *y* 的根。如果两个根相同，这意味着 *x* 和 *y* 属于同一棵树。因此，它什么也不做。否则，它将具有较高等级的根设置为具有较低等级的根的父级。下图显示了在 *DS* 上执行`Union(1, 2)`和`Union(4, 5)`操作的结果:\n\n![Figure 5.14: Merging 1,2 and 4,5](img/C14498_05_14.jpg)\n\n###### 图 5.14:合并 1，2 和 4，5\n\n随着后续联合操作的应用，更多的树合并成更少(但更大)的树。下图为应用`Union(2, 3)`后 *DS* 中的树木:\n\n![Figure 5.15: Merging 2,3](img/C14498_05_15.jpg)\n\n###### 图 5.15:合并 2，3\n\n下图为应用`Union(2, 4)`后 *DS* 中的树木:\n\n![Figure 5.16: Merging 2,4](img/C14498_05_16.jpg)\n\n###### 图 5.16:合并 2，4\n\n现在，让我们理解不相交集数据结构如何帮助我们实现 Kruskal 算法。在算法开始时，在步骤 1 之前，我们初始化一个不相交集数据结构，其中 *N* 等于图中的顶点数， *G* 。然后，步骤 2 从最小堆中取出一条边，步骤 3 检查所考虑的边是否形成一个循环。请注意，循环检查可以使用 *DS* 上的`union`操作来实现，该操作应用于边的两个顶点。如果`union`操作成功合并了两棵树，则边缘被添加到 MST 中；否则，可以安全地丢弃该边缘，因为它会在 MST 中引入一个周期。以下说明的步骤解释了这一逻辑:\n\n1.  First, we begin by initializing a disjoint-set data structure, *DS*, containing all of the given vertices in the graph:\n\n    ![Figure 5.17: Step 1 of Kruskal’s algorithm – initialization](img/C14498_05_17.jpg)\n\n    ###### 图 5.17:克鲁斯卡尔算法的第一步——初始化\n\n2.  Let's proceed to add the edge with the lowest weight to our MST. As you can see from the following figure, as we add *edge (2,4)*, we also apply `Union(2,4)` to the elements in *DS*:\n\n    ![](img/C14498_05_18.jpg)\n\n    ###### 图 5.18:在对不相交集应用并集(2，4)之后，向 MST 添加边(2，4)\n\n3.  As we proceed with adding edges as per the algorithm, we reach *edge (1,5)*. As you can see, in *DS*, the corresponding elements are in the same tree. Hence, we cannot add that edge. As you can see from the following graph, adding that would have created a cycle:\n\n    ![](img/C14498_05_19.jpg)\n\n###### 图 5.19:尝试向 MST 添加边(1，5)失败，因为顶点 1 和 5 在 DS 中的同一棵树上\n\n在下面的练习中，我们将使用不相交集数据结构实现 Kruskal 的最小生成树算法。\n\n### 练习 26:克鲁斯卡尔的 MST 算法\n\n在本练习中，我们将实现不相交集数据结构和 Kruskal 算法，以在图中找到一个 MST。让我们开始吧:\n\n1.  首先添加以下标题并声明`Graph`数据结构:\n\n    ```cpp\n    #include<iostream>\n    #include<vector>\n    #include<algorithm>\n    #include<queue>\n    #include<map>\n    template <typename T> class Graph;\n    ```\n\n2.  首先，我们将实现不相交集:\n\n    ```cpp\n    template<typename T>\n    class SimpleDisjointSet\n    {\n    private:\n        struct Node\n        {\n            T data;\n            Node(T _data) : data(_data)\n            {}\n            bool operator!=(const Node& n) const\n            {\n                return this->data != n.data;\n            }\n        };\n        // Store the forest\n        std::vector<Node> nodes;\n        std::vector<size_t> parent;\n        std::vector<size_t> rank;\n    ```\n\n3.  添加类的构造函数，实现`Make-set`和`Find`操作，如下图:\n\n    ```cpp\n    public:\n        SimpleDisjointSet(size_t N)\n        {\n            nodes.reserve(N);\n            parent.reserve(N);\n            rank.reserve(N);\n        }\n        void add_set(const T& x)\n        {\n            nodes.emplace_back(x);\n            parent.emplace_back(nodes.size() - 1);    // the parent is the node itself\n            rank.emplace_back(0);        // the initial rank for all nodes is 0\n        }\n        auto find(T x)\n        {\n            // Find the node that contains element 'x'\n            auto node_it = std::find_if(nodes.begin(), nodes.end(), \n                [x](auto n) \n                {return n.data == x; });\n            auto node_idx = std::distance(nodes.begin(), node_it);\n            auto parent_idx = parent[node_idx];\n            // Traverse the tree till we reach the root\n            while (parent_idx != node_idx)\n            {\n                node_idx = parent_idx;\n                parent_idx = parent[node_idx];\n            }\n            return parent_idx;\n        }\n    ```\n\n4.  接下来，我们将在不相交集合中的两棵树之间执行`Union`操作，如下所示:\n\n    ```cpp\n        // Union the sets X and Y belong to\n        void union_sets(T x, T y)\n        {\n            auto root_x = find(x);\n            auto root_y = find(y);\n            // If both X and Y are in the same set, do nothing and return\n            if (root_x == root_y)\n            {\n                return;\n            }\n            // If X and Y are in different sets, merge the set with lower rank \n            // into the set with higher rank\n            else if (rank[root_x] > rank[root_y]) \n            {\n                parent[root_y] = parent[root_x];\n                rank[root_x]++ ;\n            }\n            else \n            {\n                parent[root_x] = parent[root_y];\n                rank[root_y]++ ;\n            }\n        }\n    };\n    ```\n\n5.  Now that our implementation of the disjoint set is complete, let's start implementing the graph. We will use an edge-list representation. The `edge` struct is defined as follows:\n\n    ```cpp\n    template<typename T>\n    struct Edge \n    {\n        size_t src;\n        size_t dest;\n        T weight;\n        // To compare edges, only compare their weights,\n        // and not the source/destination vertices\n        inline bool operator< (const Edge<T>& e) const\n        {\n            return this->weight < e.weight;\n        }\n        inline bool operator> (const Edge<T>& e) const\n        {\n            return this->weight > e.weight;\n        }\n    };\n    ```\n\n    因为我们的边的实现是模板化的，所以边权重可以是实现`<`和`>`运算的任何数据类型。\n\n6.  以下函数允许图被序列化并输出到流:\n\n    ```cpp\n    template <typename T>\n    std::ostream& operator<<(std::ostream& os, const Graph<T>& G)\n    {\n        for (auto i = 1; i < G.vertices(); i++)\n        {\n            os << i <<\":\\t\";\n            auto edges = G.edges(i);\n            for (auto& e : edges)\n                os << \"{\" << e.dest << \": \" << e.weight << \"}, \";\n\n            os << std::endl;\n        }\n\n        return os;\n    }\n    ```\n\n7.  The graph data structure can now be implemented with the following code:\n\n    ```cpp\n    template<typename T>\n    class Graph\n    {\n    public:\n        // Initialize the graph with N vertices\n        Graph(size_t N): V(N)\n        {}\n        // Return number of vertices in the graph\n        auto vertices() const\n        {\n            return V;\n        }\n        // Return all edges in the graph\n        auto& edges() const\n        {\n            return edge_list;\n        }\n        void add_edge(Edge<T>&& e)\n        {\n            // Check if the source and destination vertices are within range\n            if (e.src >= 1 && e.src <= V && e.dest >= 1 && e.dest <= V)\n                edge_list.emplace_back(e);\n            else\n                std::cerr << \"Vertex out of bounds\" << std::endl;\n        }\n        // Returns all outgoing edges from vertex v\n        auto edges(size_t v) const\n        {\n            std::vector<Edge<T>> edges_from_v;\n            for(auto& e:edge_list)\n            {\n                if (e.src == v)\n                    edges_from_v.emplace_back(e);\n            }\n            return edges_from_v;\n        }\n        // Overloads the << operator so a graph be written directly to a stream\n        // Can be used as std::cout << obj << std::endl;\n        template <typename T>\n        friend std::ostream& operator<< <>(std::ostream& os, const Graph<T>& G);\n    private: \n        size_t V;        // Stores number of vertices in graph\n        std::vector<Edge<T>> edge_list;\n    };\n    ```\n\n    #### 注意\n\n    我们的图实现不允许在创建图后更改图中的顶点数量。此外，尽管我们可以根据需要添加任意多的边，但由于在本练习中不需要删除边，因此没有实现删除边。\n\n8.  现在，我们可以这样实现克鲁斯卡尔算法:\n\n    ```cpp\n    // Since a tree is also a graph, we can reuse the Graph class\n    // However, the result graph should have no cycles\n    template<typename T>\n    Graph<T> minimum_spanning_tree(const Graph<T>& G)\n    {\n        // Create a min-heap for the edges\n        std::priority_queue<Edge<T>, \n            std::vector<Edge<T>>, \n            std::greater<Edge<T>>> edge_min_heap;\n        // Add all edges in the min-heap\n        for (auto& e : G.edges()) \n            edge_min_heap.push(e);\n        // First step: add all elements to their own sets\n        auto N = G.vertices();\n        SimpleDisjointSet<size_t> dset(N);\n        for (auto i = 0; i < N; i++)\n            dset.add_set(i);\n\n        // Second step: start merging sets\n        Graph<T> MST(N);\n        while (!edge_min_heap.empty())\n        {\n            auto e = edge_min_heap.top();\n            edge_min_heap.pop();\n    // Merge the two trees and add edge to the MST only if the two vertices of the edge belong to different trees in the MST\n            if (dset.find(e.src) != dset.find(e.dest))\n            {\n                MST.add_edge(Edge <T>{e.src, e.dest, e.weight});\n                dset.union_sets(e.src, e.dest); \n            }\n        }\n        return MST;\n    }\n    ```\n\n9.  最后，添加此处显示的驱动程序代码:\n\n    ```cpp\n     int main()\n    {\n        using T = unsigned;\n        Graph<T> G(9);\n        std::map<unsigned, std::vector<std::pair<size_t, T>>> edges;\n        edges[1] = { {2, 2}, {5, 3} };\n        edges[2] = { {1, 2}, {5, 5}, {4, 1} };\n        edges[3] = { {4, 2}, {7, 3} };\n        edges[4] = { {2, 1}, {3, 2}, {5, 2}, {6, 4}, {8, 5} };\n        edges[5] = { {1, 3}, {2, 5}, {4, 2}, {8, 3} };\n        edges[6] = { {4, 4}, {7, 4}, {8, 1} };\n        edges[7] = { {3, 3}, {6, 4} };\n        edges[8] = { {4, 5}, {5, 3}, {6, 1} };\n\n        for (auto& i : edges)\n            for(auto& j: i.second)\n                G.add_edge(Edge<T>{ i.first, j.first, j.second });\n\n        std::cout << \"Original Graph\" << std::endl;\n        std::cout << G;\n        auto MST = minimum_spanning_tree(G);\n        std::cout << std::endl << \"Minimum Spanning Tree\" << std::endl;\n        std::cout << MST;\n        return 0;\n    }\n    ```\n\n10.  最后，运行程序！您的输出应该如下所示:\n\n![Figure 5.20: Getting an MST from a given graph](img/C14498_05_20.jpg)\n\n###### 图 5.20:从一个给定的图中获取一个 MST\n\n验证我们算法的输出确实是*图 5.12 所示的 MST。*\n\n不使用不相交集的 Kruskal 算法的复杂度为 *O(E log E)* ，其中 E 为图中的边数。然而，对于不相交的集合，总复杂性归结为*O(E**α**(V)*，其中 *α* *(v)* 是阿克曼函数的逆函数。由于逆阿克曼函数的增长速度比对数函数慢得多，所以对于具有几个顶点的图来说，这两种实现的性能差异很小，但是对于更大的图实例来说，差异可能特别大。\n\n## 顶点着色问题\n\n顶点着色问题可以表述如下:\n\n*“给定一个图 G，给图的每个顶点分配一种颜色，这样就不会有两个相邻的顶点具有相同的颜色。”*\n\n例如，下图显示了在*图 5.11* 中显示的图的有效着色:\n\n![Figure 5.21: Coloring an uncolored graph](img/C14498_05_21.jpg)\n\n###### 图 5.21:给未着色的图着色\n\n图着色在解决现实世界中的各种各样的问题方面都有应用——为出租车制定时间表、解决数独难题和为考试制定时间表，这些都可以映射到找到问题的有效着色，建模为图。然而，找到产生有效顶点着色所需的最小颜色数(也称为色数)是一个 NP 完全问题。因此，问题本质上的一个微小变化就能对它的复杂性产生巨大的影响。\n\n作为图着色问题应用的一个例子，让我们考虑数独解算器的情况。数独是一个数字布局难题，目标是用 1 到 9 的数字填充一个 9 × 9 的盒子，每行不重复数字。每列是一个 3 × 3 的块。这里显示了一个数独谜题的例子:\n\n![Figure 5.22: (Left) a sudoku puzzle, (Right) its solution](img/C14498_05_22.jpg)\n\n###### 图 5.22:(左)一个数独难题，(右)它的解法\n\n我们可以将这个难题的一个实例建模为图着色问题，如下所示:\n\n*   用图 *G* 中的一个顶点表示拼图中的每个单元格。\n*   在同一列、行或同一 3 × 3 块中的顶点之间添加边。\n*   然后 *G* 的一个有效着色给了我们一个原始数独难题的解决方案。\n\n我们将在下面的练习中研究图着色的实现。\n\n### 练习 27:贪婪图着色\n\n在本练习中，我们将实现一个贪婪算法，当可以使用的最大颜色数为 6 时，该算法为图 5.21*中所示的图生成图着色。让我们开始吧:*\n\n1.  首先包含所需的头文件并声明`Graph`数据结构，我们将在本练习的后面部分实现:\n\n    ```cpp\n    #include <unordered_map>\n    #include <set>\n    #include <map>\n    #include <string>\n    #include <vector>\n    #include <iostream>\n    template <typename T> class Graph;\n    ```\n\n2.  下面的结构在我们的图中实现了一条边:\n\n    ```cpp\n    template<typename T>\n    struct Edge\n    {\n        size_t src;\n        size_t dest;\n        T weight;\n        // To compare edges, only compare their weights,\n        // and not the source/destination vertices\n        inline bool operator< (const Edge<T>& e) const\n        {\n            return this->weight < e.weight;\n        }\n        inline bool operator> (const Edge<T>& e) const\n        {\n            return this->weight > e.weight;\n        }\n    };\n    ```\n\n3.  以下函数允许我们将图直接写入输出流:\n\n    ```cpp\n    template <typename T>\n    std::ostream& operator<<(std::ostream& os, const Graph<T>& G)\n    {\n        for (auto i = 1; i < G.vertices(); i++)\n        {\n            os << i << \":\\t\";\n            auto edges = G.outgoing_edges(i);\n            for (auto& e : edges)\n                os << \"{\" << e.dest << \": \" << e.weight << \"}, \";\n            os << std::endl;\n        }\n        return os;\n    }\n    ```\n\n4.  将图实现为边列表，如下图所示:\n\n    ```cpp\n    template<typename T>\n    class Graph\n    {\n    public:\n        // Initialize the graph with N vertices\n        Graph(size_t N) : V(N)\n        {}\n        // Return number of vertices in the graph\n        auto vertices() const\n        {\n            return V;\n        }\n        // Return all edges in the graph\n        auto& edges() const\n        {\n            return edge_list;\n        }\n        void add_edge(Edge<T>&& e)\n        {\n            // Check if the source and destination vertices are within range\n            if (e.src >= 1 && e.src <= V &&\n                e.dest >= 1 && e.dest <= V)\n                edge_list.emplace_back(e);\n            else\n                std::cerr << \"Vertex out of bounds\" << std::endl;\n        }\n        // Returns all outgoing edges from vertex v\n        auto outgoing_edges(size_t v) const\n        {\n            std::vector<Edge<T>> edges_from_v;\n            for (auto& e : edge_list)\n            {\n                if (e.src == v)\n                    edges_from_v.emplace_back(e);\n            }\n            return edges_from_v;\n        }\n        // Overloads the << operator so a graph be written directly to a stream\n        // Can be used as std::cout << obj << std::endl;\n        template <typename T>\n        friend std::ostream& operator<< <>(std::ostream& os, const Graph<T>& G);\n    private:\n        size_t V;        // Stores number of vertices in graph\n        std::vector<Edge<T>> edge_list;\n    };\n    ```\n\n5.  下面的散列图存储了我们的着色算法将使用的颜色列表:\n\n    ```cpp\n    // Initialize the colors that will be used to color the vertices\n    std::unordered_map<size_t, std::string> color_map = {\n        {1, \"Red\"},\n        {2, \"Blue\"},\n        {3, \"Green\"},\n        {4, \"Yellow\"},\n        {5, \"Black\"},\n        {6, \"White\"}\n    };\n    ```\n\n6.  接下来，让我们实现一个助手函数，打印分配给每个顶点的颜色:\n\n    ```cpp\n    void print_colors(std::vector<size_t>& colors)\n    {\n        for (auto i=1; i<colors.size(); i++)\n        {\n            std::cout << i << \": \" << color_map[colors[i]] << std::endl;\n        }\n    }\n    ```\n\n7.  以下函数实现了我们的着色算法:\n\n    ```cpp\n    template<typename T>\n    auto greedy_coloring(const Graph<T>& G)\n    {\n        auto size = G.vertices();\n        std::vector<size_t> assigned_colors(size);\n        // Let us start coloring with vertex number 1\\. \n        // Note that this choice is arbirary.\n        for (auto i = 1; i < size; i++)\n        {\n            auto outgoing_edges = G.outgoing_edges(i);\n            std::set<size_t> neighbour_colors;\n            for (auto e : outgoing_edges)\n            {\n                auto dest_color = assigned_colors[e.dest];\n                neighbour_colors.insert(dest_color);\n            }\n            // Find the smallest unassigned color \n            // that is not currently used by any neighbor\n            auto smallest_unassigned_color = 1;\n            for (; \n                smallest_unassigned_color <= color_map.size();\n                smallest_unassigned_color++)\n            {\n              if (neighbour_colors.find(smallest_unassigned_color) == \n                  neighbour_colors.end())\n                  break;\n            }\n            assigned_colors[i] = smallest_unassigned_color;\n        }\n        return assigned_colors;\n    }\n    ```\n\n8.  最后添加驱动代码，如下图:\n\n    ```cpp\n    int main()\n    {\n        using T = size_t;\n        Graph<T> G(9);\n        std::map<unsigned, std::vector<std::pair<size_t, T>>> edges;\n        edges[1] = { {2, 2}, {5, 3} };\n        edges[2] = { {1, 2}, {5, 5}, {4, 1} };\n        edges[3] = { {4, 2}, {7, 3} };\n        edges[4] = { {2, 1}, {3, 2}, {5, 2}, {6, 4}, {8, 5} };\n        edges[5] = { {1, 3}, {2, 5}, {4, 2}, {8, 3} };\n        edges[6] = { {4, 4}, {7, 4}, {8, 1} };\n        edges[7] = { {3, 3}, {6, 4} };\n        edges[8] = { {4, 5}, {5, 3}, {6, 1} };\n        for (auto& i : edges)\n            for (auto& j : i.second)\n                G.add_edge(Edge<T>{ i.first, j.first, j.second });\n        std::cout << \"Original Graph: \" << std::endl;\n        std::cout << G << std::endl;\n        auto colors = greedy_coloring<T>(G);\n        std::cout << \"Vertex Colors: \" << std::endl;\n        print_colors(colors);\n        return 0;\n    }\n    ```\n\n9.  运行实现！您的输出应该如下所示:\n\n![](img/C14498_05_23.jpg)\n\n###### 图 5.23:图着色实现的输出\n\n我们的实现总是从顶点标识 1 开始给顶点着色。但是，这种选择是任意的，即使在同一个图上用不同的顶点启动贪婪着色算法，也很可能导致需要不同颜色数的不同图着色。\n\n图着色的质量通常通过它用来给图着色的颜色数量来衡量。虽然找到使用最少可能颜色数的最佳图着色是 NP 完全的，但是贪婪图着色通常用作有用的近似。例如，当设计一个编译器时，图着色被用来将中央处理器寄存器分配给正在编译的程序的变量。贪婪着色算法与一组试探法一起使用，以获得问题的“足够好”的解决方案，这在实践中是可取的，因为我们需要编译器快速才能有用。\n\n### 活动 12:威尔士-鲍威尔算法\n\n对用固定顶点标识开始贪婪着色的简单方法的改进是，按照入射到顶点上的边的数量的递减顺序(或按照顶点的度数的递减顺序)对顶点着色。\n\n该算法的工作原理如下:\n\n1.  按照度数递减的顺序对所有顶点进行排序，并将它们存储在一个数组中。\n2.  取排序后的数组中第一个未着色的顶点，给它分配第一个没有分配给它的任何邻居的颜色。让这个颜色为 *C* 。\n3.  遍历排序后的数组，将颜色 *C* 分配给每个没有任何已经分配了 *C* 的邻居的未着色顶点。\n4.  如果数组中还有未着色的顶点，请转到步骤 2。否则，结束程序。到目前为止分配给顶点的颜色是最终输出。\n\n以下是算法的四次迭代的示例，需要这四次迭代来找到图的有效着色，如图 5.21*所示:*\n\n *1.  Here is the graph that we start with:\n\n    ![Figure 5.24: Starting with an uncolored graph](img/C14498_05_24.jpg)\n\n    ###### 图 5.24:从未着色的图开始\n\n2.  Next, we sort by decreasing order of vertices, and start by coloring red:\n\n    ![Figure 5.25: Coloring red](img/C14498_05_25.jpg)\n\n    ###### 图 5.25:红色\n\n3.  In the next round, we start coloring blue:\n\n    ![](img/C14498_05_26.jpg)\n\n    ###### 图 5.26:蓝色\n\n4.  在最后一轮中，我们将颜色涂成绿色:\n\n![](img/C14498_05_27.jpg)\n\n###### 图 5.27:绿色\n\n完成本活动的高级步骤如下:\n\n1.  假设图的每条边都有源顶点标识、目标顶点标识和边权重。实现一个表示图的边的结构。我们将使用这个结构的实例在我们的图表示中创建不同的边。\n2.  使用边列表表示实现一个图。\n3.  实现一个函数，该函数实现威尔士-鲍威尔图着色并返回一个颜色向量。矢量中索引 *i* 处的颜色应该是分配给顶点标识 *i* 的颜色。\n4.  根据需要添加驱动程序和输入/输出代码，创建图*图 5.24* 所示的图。可以假设着色总是从顶点标识 *1* 开始。\n\n您的输出应该如下所示:\n\n![Figure 5.28: Expected output of Activity 12](img/C14498_05_28.jpg)\n\n###### 图 5.28:活动 12 的预期产出\n\n#### 注意\n\n这个活动的解决方案可以在第 518 页找到。\n\n## 总结\n\n贪婪的方法很简单:在算法的每次迭代中，从所有可能的选择中挑选看似最好的选择。换句话说，当在每次迭代中选择局部“最佳”的替代方案导致问题的全局最优解时，问题的贪婪解是适用的。\n\n在这一章中，我们看了一些问题的例子，其中贪婪方法是最优的，并导致给定问题的正确解决方案；也就是最短作业优先调度。我们还讨论了诸如 0-1 背包和图着色问题等 NP 完全问题的轻微修改版本如何具有简单的贪婪解。这使得贪婪方法成为解决困难问题的重要算法设计工具。对于有贪婪解决方案的问题，很可能是最简单的解决方法；即使对于没有贪婪解决方案的问题，它也可以经常用于解决实际中可能“足够好”的问题的宽松版本(例如，在编程语言编译器中将寄存器分配给变量时使用贪婪图着色)。\n\n接下来，我们讨论了贪婪选择和最优子结构性质，并看了一个证明给定问题表现出这些性质的例子。我们用最小生成树问题的两个解决方案来结束这一章:克鲁斯卡尔算法和威尔士-鲍威尔算法。我们对 Kruskal 算法的讨论也引入了不相交集数据结构。\n\n在下一章中，我们将重点介绍图算法，从广度优先和深度优先搜索开始，然后介绍 Dijkstra 的最短路径算法。我们还将研究最小生成树问题的另一种解决方案:Prim 算法。*"
  },
  {
    "path": "docs/cpp-dsal-design-principle/06.md",
    "content": "# 六、图算法 1\n\n## 学习目标\n\n本章结束时，您将能够:\n\n*   描述图在解决各种现实问题中的效用\n*   选择并实现正确的遍历方法来查找图中的元素\n*   用 Prim 算法求解最小生成树问题\n*   确定何时使用 Prim 和 Kruskal 算法来解决 MST 问题\n*   使用迪克斯特拉算法寻找图中两个顶点/节点之间的最短路径\n\n在这一章中，我们将研究基本的和最常用的算法来解决问题，可以用图的形式来表示，这将在下一章中进一步讨论。\n\n## 简介\n\n在前两章中，我们讨论了两种算法设计范式:分治法和贪婪方法，这两种方法使我们对广泛使用和重要的计算问题(如排序、搜索和寻找图上的最小权重生成树)找到了众所周知的解决方案。在本章中，我们将讨论一些特别适用于图数据结构的算法。\n\n一个**图**被定义为一组连接一对顶点的**顶点**和**边**。数学上，这通常写成 *G = < V，E >* ，其中 *V* 表示顶点集， *E* 表示构成图的边集。从一个节点指向另一个节点的边称为*有向*，而没有方向的边称为*无向*。正如我们在*第二章*、*树、堆和图*中看到的那样，边也可能与*权重*相关联，或者是*未加权的*。\n\n#### 注意\n\n当我们谈论图时，术语“节点”和“顶点”可以互换使用。在这一章，我们将坚持“顶点”\n\n图是一些最通用的数据结构，以至于其他链接数据结构，如树和链表，都被认为是图的特例。使图有用的是它们是*对象*(表示为**节点**)之间*关系*(表示为**边**)的一般表示。图可以在同一对节点之间有多条边，甚至在一条边上有多条边权重，节点也可以有从自身到自身的边(也称为自边)。下图中显示的图表显示了这些功能在图表中的表现方式。称为“超图”的图的变体也允许具有连接多个节点的边，并且另一组称为“混合图”的变体也允许在同一图中具有有向和无向边:\n\n![Figure 6.1: A graph with multiple edge weights, self edges (also called loops), and both directed and undirected edges](img/C14498_06_01.jpg)\n\n###### 图 6.1:具有多重边权重、自边(也称为循环)以及有向和无向边的图\n\n由于图提供的高度通用性，它们在几个应用中找到了用途。理论计算机科学家使用图来建模有限状态机和自动机，人工智能和机器学习专家使用图来从不同种类网络的结构随时间的变化中提取信息，交通工程师使用图来研究通过道路网络的交通流。\n\n在本章中，我们将仅限于研究使用加权有向图的算法，如果需要，还将研究正边权重。我们将首先研究**图遍历问题**，并涵盖两种解决方案:**广度优先搜索** ( **BFS** )和**深度优先搜索** ( **DFS** )。接下来，我们将回到上一章介绍的最小生成树问题，并提供一个不同的解决方案，称为 Prim 算法。最后，我们将讨论单一来源的最短路径问题，该问题为谷歌地图和 OSRM 路线规划等导航应用提供了动力。\n\n让我们先来看看遍历一个图的基本问题。\n\n## 图的遍历问题\n\n想象一下，你最近搬进了一个新社区的公寓。当你遇到你的新邻居并结交新朋友时，人们经常推荐你去附近的餐馆吃饭。你希望参观所有推荐的餐馆，所以你拿出一张附近的地图，并在地图上标记所有的餐馆和你的家，地图上已经标记了所有的道路。如果我们将每个餐厅和你的家表示为一个顶点，将连接餐厅的道路表示为图中的边，那么当从给定的顶点开始时，访问图中所有顶点的问题称为图遍历问题。\n\n在下图中，蓝色的数字是假设的顶点标识。顶点 *1* 为*家*，餐厅标注从 *R1* 到 *R7* 。没有一条边有箭头，因为这些边被认为是双向的，也就是说，你可以在道路上沿任一方向行驶:\n\n![Figure 6.2: Representing a neighborhood map as a graph](img/C14498_06_02.jpg)\n\n###### 图 6.2:将邻域图表示为图\n\n在数学记数法中，给定一个图， *G = < V，E >* ，图的遍历问题是访问所有*V**∑**V*从给定顶点开始， *s* 。图遍历问题也被称为**图搜索问题**，因为它可以用来“找到”图中的一个顶点。不同的图遍历算法给出了访问图中顶点的不同顺序。\n\n### 广度优先搜索\n\n图的“广度优先”搜索或广度优先遍历开始于将起始顶点添加到由一组先前访问过的顶点组成的**边界**，然后迭代探索与当前边界相邻的顶点。下面举例说明的步骤应该有助于您理解这个想法:\n\n1.  First, the *Home* vertex, which is the starting point, is visited. *R1* and *R2* are the neighbors of the vertices in the current frontier, which is represented by a blue dotted line in the following figure:\n\n    ![Figure 6.3: Initialization of the BFS frontier](img/C14498_06_03.jpg)\n\n    ###### 图 6.3:BFS 边界的初始化\n\n2.  The following figure shows BFS after visiting *R1* and *R1*, either of which can be visited before the other. The order of visiting vertices that are at the same distance from the source vertex is irrelevant; however, the vertices with lower distance from the source are always visited first:\n\n    ![Figure 6.4: The BFS frontier after visiting the R1 and R2 vertices](img/C14498_06_04.jpg)\n\n    ###### 图 6.4:访问 R1 和 R2 顶点后的 BFS 边界\n\n3.  下图为访问 *R3* 、 *R5* 、 *R6* 后的 BFS 状态。这基本上是遍历整个图之前的倒数第二个阶段:\n\n![Figure 6.5: The BFS frontier after visiting R3, R5, and R6](img/C14498_06_05.jpg)\n\n###### 图 6.5:访问 R3、R5 和 R6 后的 BFS 边境\n\nBFS 的一个有用的性质是，对于每个被访问的顶点，它的所有子顶点都在任何子顶点之前被访问。但是，在实现 BFS 时，通常不会在单独的数据结构中明确维护边界。相反，使用顶点标识队列来确保更靠近源顶点的顶点总是在更远的顶点之前被访问。在下面的练习中，我们将用 C++ 实现 BFS。\n\n### 练习 28:实施 BFS\n\n在本练习中，我们将使用图的边列表表示来实现广度优先搜索算法。为此，请执行以下步骤:\n\n1.  添加需要的头文件并声明图，如下:\n\n    ```cpp\n    #include <string>\n    #include <vector>\n    #include <iostream>\n    #include <set>\n    #include <map>\n    #include <queue>\n    template<typename T> class Graph;\n    ```\n\n2.  Write the following struct, which represents an edge in our graph:\n\n    ```cpp\n    template<typename T>\n    struct Edge\n    {\n        size_t src;\n        size_t dest;\n        T weight;\n        // To compare edges, only compare their weights,\n        // and not the source/destination vertices\n        inline bool operator< (const Edge<T>& e) const\n        {\n            return this->weight < e.weight;\n        }\n        inline bool operator> (const Edge<T>& e) const\n        {\n            return this->weight > e.weight;\n        }\n    };\n    ```\n\n    因为我们对边的定义使用模板，所以可以很容易地使边具有所需的任何数据类型的边权重。\n\n3.  接下来，重载`Graph`数据类型的`<<`运算符，以显示图的内容:\n\n    ```cpp\n    template <typename T>\n    std::ostream& operator<<(std::ostream& os, const Graph<T>& G)\n    {\n        for (auto i = 1; i < G.vertices(); i++)\n        {\n            os << i << \":\\t\";\n            auto edges = G.outgoing_edges(i);\n            for (auto& e : edges)\n                os << \"{\" << e.dest << \": \" << e.weight << \"}, \";\n            os << std::endl;\n        }\n        return os;\n    }\n    ```\n\n4.  编写一个类来定义我们的图数据结构，如下图所示:\n\n    ```cpp\n    template<typename T>\n    class Graph\n    {\n    public:\n        // Initialize the graph with N vertices\n        Graph(size_t N) : V(N)\n        {}\n        // Return number of vertices in the graph\n        auto vertices() const\n        {\n            return V;\n        }\n        // Return all edges in the graph\n        auto& edges() const\n        {\n            return edge_list;\n        }\n        void add_edge(Edge<T>&& e)\n        {\n            // Check if the source and destination vertices are within range\n            if (e.src >= 1 && e.src <= V &&\n                e.dest >= 1 && e.dest <= V)\n                edge_list.emplace_back(e);\n            else\n                std::cerr << \"Vertex out of bounds\" << std::endl;\n        }\n        // Returns all outgoing edges from vertex v\n        auto outgoing_edges(size_t v) const\n        {\n            std::vector<Edge<T>> edges_from_v;\n            for (auto& e : edge_list)\n            {\n                if (e.src == v)\n                    edges_from_v.emplace_back(e);\n            }\n            return edges_from_v;\n        }\n        // Overloads the << operator so a graph be written directly to a stream\n        // Can be used as std::cout << obj << std::endl;\n        template <typename T>\n        friend std::ostream& operator<<(std::ostream& os, const Graph<T>& G);\n    private:\n        size_t V;        // Stores number of vertices in graph\n        std::vector<Edge<T>> edge_list;\n    };\n    ```\n\n5.  For this exercise, we shall test our implementation of BFS on the following graph:\n\n    ![Figure 6.6: Graph for implementing BFS traversal in Exercise 28 ](img/C14498_06_06.jpg)\n\n    ###### 图 6.6:练习 28 中实现 BFS 遍历的图\n\n    我们需要一个函数来创建和返回所需的图。请注意，虽然图中的每条边都分配了边权重，但这不是必需的，因为 BFS 算法不需要使用边权重。按如下方式实现该功能:\n\n    ```cpp\n    template <typename T>\n    auto create_reference_graph()\n    {\n        Graph<T> G(9);\n        std::map<unsigned, std::vector<std::pair<size_t, T>>> edges;\n        edges[1] = { {2, 2}, {5, 3} };\n        edges[2] = { {1, 2}, {5, 5}, {4, 1} };\n        edges[3] = { {4, 2}, {7, 3} };\n        edges[4] = { {2, 1}, {3, 2}, {5, 2}, {6, 4}, {8, 5} };\n        edges[5] = { {1, 3}, {2, 5}, {4, 2}, {8, 3} };\n        edges[6] = { {4, 4}, {7, 4}, {8, 1} };\n        edges[7] = { {3, 3}, {6, 4} };\n        edges[8] = { {4, 5}, {5, 3}, {6, 1} };\n        for (auto& i : edges)\n            for (auto& j : i.second)\n                G.add_edge(Edge<T>{ i.first, j.first, j.second });\n        return G;\n    }\n    ```\n\n6.  像这样实现广度优先搜索:\n\n    ```cpp\n    template <typename T>\n    auto breadth_first_search(const Graph<T>& G, size_t dest)\n    {\n        std::queue<size_t> queue;\n        std::vector<size_t> visit_order;\n        std::set<size_t> visited;\n        queue.push(1); // Assume that BFS always starts from vertex ID 1\n        while (!queue.empty())\n        {\n            auto current_vertex = queue.front();\n            queue.pop();\n            // If the current vertex hasn't been visited in the past\n            if (visited.find(current_vertex) == visited.end())\n            {\n                visited.insert(current_vertex);\n                visit_order.push_back(current_vertex);\n                for (auto e : G.outgoing_edges(current_vertex))\n                    queue.push(e.dest);\n            }\n        }\n        return visit_order;\n    }\n    ```\n\n7.  添加以下创建参考图的测试和驱动程序代码，从顶点 *1* 开始运行 BFS，并输出结果:\n\n    ```cpp\n    template <typename T>\n    void test_BFS()\n    {\n        // Create an instance of and print the graph\n        auto G = create_reference_graph<unsigned>();\n        std::cout << G << std::endl;\n        // Run BFS starting from vertex ID 1 and print the order\n        // in which vertices are visited.\n        std::cout << \"BFS Order of vertices: \" << std::endl;\n        auto bfs_visit_order = breadth_first_search(G, 1);\n        for (auto v : bfs_visit_order)\n            std::cout << v << std::endl;\n    }\n    int main()\n    {\n        using T = unsigned;\n        test_BFS<T>();\n        return 0;\n    }\n    ```\n\n8.  运行前面的代码。您的输出应该如下所示:\n\n![Figure 6.7: Expected output of Exercise 28 ](img/C14498_06_07.jpg)\n\n###### 图 6.7:练习 28 的预期输出\n\n下图显示了我们的 BFS 实现访问的顶点顺序。请注意，搜索从顶点 *1* 开始，然后逐渐访问离源更远的顶点。在下图中，红色的整数表示顺序，箭头表示我们的 BFS 实现访问图的顶点的方向:\n\n![Figure 6.8: BFS implementation in Exercise 28 ](img/C14498_06_08.jpg)\n\n###### 图 6.8:练习 28 中的 BFS 实现\n\nBFS 的时间复杂度为 *O(V + E)* ，其中 *V* 为顶点数， *E* 为图中边数。\n\n### 深度优先搜索\n\n当 BFS 从源顶点开始并逐渐向外扩展搜索到更远的顶点时，DFS 从源顶点开始并沿着某个路径迭代访问尽可能远的顶点，返回到更早的顶点以沿着图中不同的路径探索顶点。这种搜索图的方法也叫做**回溯**。以下图示步骤显示了 DFS 的工作原理:\n\n1.  Naturally, we begin our traversal by visiting the *Home* vertex, as shown in the following figure:\n\n    ![Figure 6.9: DFS initialization](img/C14498_06_09.jpg)\n\n    ###### 图 6.9: DFS 初始化\n\n2.  Next, we visit vertex *R2*. Note that *R2* is chosen arbitrarily over *R1* since both are adjacent to *Home*, and either could have been chosen without affecting the correctness of the algorithm:\n\n    ![Figure 6.10: DFS after visiting R2](img/C14498_06_10.jpg)\n\n    ###### 图 6.10:访问 R2 后的 DFS\n\n3.  Next, we visit vertex *R3*, as shown in the following figure. Again, either of *R3* or *R1* could have been chosen arbitrarily, as both are adjacent to *R2*:\n\n    ![Figure 6.11: DFS after visiting R3](img/C14498_06_11.jpg)\n\n    ###### 图 6.11:访问 R3 后的 DFS\n\n4.  通过在每次迭代中访问任意未访问的相邻顶点来继续搜索。在 *R1* 被访问之后，搜索尝试寻找下一个未访问的顶点。由于没有剩余，搜索终止:\n\n![Figure 6.12: DFS after visiting all the vertices in the graph](img/C14498_06_12.jpg)\n\n###### 图 6.12:访问图中所有顶点后的 DFS\n\n在实现 BFS 时，我们使用队列来跟踪未访问的顶点。由于队列是**先进先出** ( **先进先出**)的数据结构，其中顶点从队列中移除的顺序与它们被添加到队列中的顺序相同，因此 BFS 算法使用它来确保更靠近起始顶点的顶点在更远的顶点之前被访问。实现 DFS 与实现 BFS 非常相似，除了一个区别:我们现在可以使用栈，而算法的其余部分保持不变，而不是使用队列作为要访问的顶点列表的容器。这种方法之所以有效，是因为在每次迭代中，DFS 都会访问当前顶点的一个未访问的邻居，这可以很容易地使用栈进行跟踪，栈是一种**后进先出** ( **后进先出**)数据结构。\n\n### 练习 29:实施 DFS\n\n在本练习中，我们将用 C++ 实现 DFS 算法，并在*图 6.2* 所示的图上进行测试。步骤如下:\n\n1.  包括需要的头文件，如下:\n\n    ```cpp\n    #include <string>\n    #include <vector>\n    #include <iostream>\n    #include <set>\n    #include <map>\n    #include <stack>\n    template<typename T> class Graph;\n    ```\n\n2.  Write the following struct in order to implement an edge in our graph:\n\n    ```cpp\n    template<typename T>\n    struct Edge\n    {\n        size_t src;\n        size_t dest;\n        T weight;\n        // To compare edges, only compare their weights,\n        // and not the source/destination vertices\n        inline bool operator< (const Edge<T>& e) const\n        {\n            return this->weight < e.weight;\n        }\n        inline bool operator> (const Edge<T>& e) const\n        {\n            return this->weight > e.weight;\n        }\n    };\n    ```\n\n    同样，由于我们的实现使用了结构的模板化版本，它允许我们分配任何所需数据类型的边权重。然而，出于 DFS 的目的，我们将使用空值作为边缘权重的占位符。\n\n3.  接下来，重载图的`<<`操作符，以便使用以下功能打印出来:\n\n    ```cpp\n     template <typename T>\n    std::ostream& operator<<(std::ostream& os, const Graph<T>& G)\n    {\n        for (auto i = 1; i < G.vertices(); i++)\n        {\n            os << i << \":\\t\";\n            auto edges = G.outgoing_edges(i);\n            for (auto& e : edges)\n                os << \"{\" << e.dest << \": \" << e.weight << \"}, \";\n            os << std::endl;\n        }\n        return os;\n    }\n    ```\n\n4.  实现使用边列表表示的图数据结构，如下所示:\n\n    ```cpp\n    template<typename T>\n    class Graph\n    {\n    public:\n        // Initialize the graph with N vertices\n        Graph(size_t N) : V(N)\n        {}\n        // Return number of vertices in the graph\n        auto vertices() const\n        {\n            return V;\n        }\n        // Return all edges in the graph\n        auto& edges() const\n        {\n            return edge_list;\n        }\n        void add_edge(Edge<T>&& e)\n        {\n            // Check if the source and destination vertices are within range\n            if (e.src >= 1 && e.src <= V &&\n                e.dest >= 1 && e.dest <= V)\n                edge_list.emplace_back(e);\n            else\n                std::cerr << \"Vertex out of bounds\" << std::endl;\n        }\n        // Returns all outgoing edges from vertex v\n        auto outgoing_edges(size_t v) const\n        {\n            std::vector<Edge<T>> edges_from_v;\n            for (auto& e : edge_list)\n            {\n                if (e.src == v)\n                    edges_from_v.emplace_back(e);\n            }\n            return edges_from_v;\n        }\n        // Overloads the << operator so a graph be written directly to a stream\n        // Can be used as std::cout << obj << std::endl;\n        template <typename T>\n        friend std::ostream& operator<< <>(std::ostream& os, const Graph<T>& G);\n    private:\n        size_t V;        // Stores number of vertices in graph\n        std::vector<Edge<T>> edge_list;\n    };\n    ```\n\n5.  现在，我们需要一个函数来为我们的图执行 DFS。执行如下:\n\n    ```cpp\n     template <typename T>\n    auto depth_first_search(const Graph<T>& G, size_t dest)\n    {\n        std::stack<size_t> stack;\n        std::vector<size_t> visit_order;\n        std::set<size_t> visited;\n        stack.push(1); // Assume that DFS always starts from vertex ID 1\n        while (!stack.empty())\n        {\n            auto current_vertex = stack.top();\n            stack.pop();\n            // If the current vertex hasn't been visited in the past\n            if (visited.find(current_vertex) == visited.end())\n            {\n                visited.insert(current_vertex);\n                visit_order.push_back(current_vertex);\n                for (auto e : G.outgoing_edges(current_vertex))\n                {    \n                    // If the vertex hasn't been visited, insert it in the stack.\n                    if (visited.find(e.dest) == visited.end())\n                    {\n                        stack.push(e.dest);\n                    }\n                }\n            }\n        }\n        return visit_order;\n    }\n    ```\n\n6.  We shall test our implementation of the DFS on the graph shown here:\n\n    ![Figure 6.13: Graph for implementing DFS traversal in Exercise 29 ](img/C14498_06_13.jpg)\n\n    ###### 图 6.13:练习 29 中实现 DFS 遍历的图\n\n    使用以下函数创建并返回图表:\n\n    ```cpp\n    template <typename T>\n    auto create_reference_graph()\n    {\n        Graph<T> G(9);\n        std::map<unsigned, std::vector<std::pair<size_t, T>>> edges;\n        edges[1] = { {2, 0}, {5, 0} };\n        edges[2] = { {1, 0}, {5, 0}, {4, 0} };\n        edges[3] = { {4, 0}, {7, 0} };\n        edges[4] = { {2, 0}, {3, 0}, {5, 0}, {6, 0}, {8, 0} };\n        edges[5] = { {1, 0}, {2, 0}, {4, 0}, {8, 0} };\n        edges[6] = { {4, 0}, {7, 0}, {8, 0} };\n        edges[7] = { {3, 0}, {6, 0} };\n        edges[8] = { {4, 0}, {5, 0}, {6, 0} };\n        for (auto& i : edges)\n            for (auto& j : i.second)\n                G.add_edge(Edge<T>{ i.first, j.first, j.second });\n        return G;\n    }\n    ```\n\n    请注意边缘权重使用空值，因为 DFS 不需要边缘权重。图的更简单的实现可以完全省略边权重，而不影响我们的 DFS 算法的行为。\n\n7.  最后，添加以下测试和驱动程序代码，运行我们的 DFS 实现并打印输出:\n\n    ```cpp\n    template <typename T>\n    void test_DFS()\n    {\n        // Create an instance of and print the graph\n        auto G = create_reference_graph<unsigned>();\n        std::cout << G << std::endl;\n        // Run DFS starting from vertex ID 1 and print the order\n        // in which vertices are visited.\n        std::cout << \"DFS Order of vertices: \" << std::endl;\n        auto dfs_visit_order = depth_first_search(G, 1);\n        for (auto v : dfs_visit_order)\n            std::cout << v << std::endl;\n    }\n    int main()\n    {\n        using T = unsigned;\n        test_DFS<T>();\n        return 0;\n    }\n    ```\n\n8.  编译并运行前面的代码。您的输出应该如下所示:\n\n![Figure 6.14: Expected output of Exercise 29 ](img/C14498_06_14.jpg)\n\n###### 图 6.14:练习 29 的预期输出\n\n下图显示了我们的 DFS 实现访问顶点的顺序:\n\n![Figure 6.15: The order of vertices visited and the direction of DFS](img/C14498_06_15.jpg)\n\n###### 图 6.15:访问顶点的顺序和 DFS 的方向\n\nBFS 和 DFS 的时间复杂度都是 *O(V + E)* 。然而，这两种算法之间有几个重要的区别。下面的列表总结了两者之间的差异，并指出了其中一个优于另一个的一些情况:\n\n*   BFS 更适合寻找离源顶点更近的顶点，而 DFS 通常更适合寻找离源更远的顶点。\n*   一旦在 BFS 访问了一个顶点，从源点到顶点的路径保证是最短路径，而 DFS 则不存在这样的保证。这就是为什么所有单源和多源最短路径算法都使用某种 BFS 变体的原因。这将在本章接下来的章节中探讨。\n*   当 BFS 访问与当前边界相邻的所有顶点时，BFS 创建的搜索树又短又宽，需要相对较多的内存，而 DFS 创建的搜索树又长又窄，需要相对较少的内存。\n\n### 活动 13:使用 DFS 找出一个图是否是二分图\n\n二部图是这样一种图，其中顶点可以被分成两组，这样图中的任何边都必须将一组中的一个顶点连接到另一组中的一个顶点。\n\n二分图可以用来建模几种不同的实际用例。例如，如果给我们一个学生列表和一个班级列表，学生和班级之间的关系可以建模为一个包含学生和班级之间的边的二部图，如果学生注册了那个班级。正如你所想象的，从一个学生到另一个学生，或者从一个学科到另一个学科的优势是没有意义的。因此，这样的边在二部图中是不允许的。下图说明了这样一个模型:\n\n![Figure 6.16: A sample bipartite graph representing student enrollment in different classes](img/C14498_06_16.jpg)\n\n###### 图 6.16:代表不同班级学生注册情况的二分图示例\n\n一旦像这里所示的模型已经准备好了，它就可以用来创建一个课程表，这样同一学生注册的两个班级就不会重叠。例如，如果 Jolene 注册了*数学*和*计算机科学*，这两门课就不应该同时安排，以免发生冲突。通过解决图中的最大流问题，可以使时间表中的冲突最小化。对于最大流问题，有几种标准算法是已知的:福特-富尔克森算法、迪尼奇算法和推-重新标记算法就是一些例子。然而，这样的算法通常很复杂，因此超出了本书的范围。\n\n使用二分图建模实体之间关系的另一个用例是观众和大型视频流平台(如网飞和 YouTube)维护的电影列表之间的关系。\n\n二部图的一个有趣的性质是，对于一般图来说是 *NP-complete* 的一些运算，如寻找最大匹配和顶点覆盖，对于二部图来说可以在多项式时间内求解。因此，确定给定的图是否是二分图是有用的。在本练习中，您需要实现一个 C++ 程序来检查给定的图 *G* 是否是二分图。\n\n二分检查算法使用稍微修改的 DFS 版本，其工作原理如下:\n\n1.  假设 DFS 从顶点 *1* 开始。将顶点标识 *1* 添加到栈中。\n2.  如果栈上仍有未访问的顶点，请从栈中弹出一个顶点，并将其设置为当前顶点。\n3.  如果分配给父顶点的颜色是蓝色，则将当前顶点分配为红色；否则，将当前顶点指定为蓝色。\n4.  将当前顶点的所有未访问的相邻顶点添加到栈中，并将当前顶点标记为已访问。\n5.  重复*步骤 2* 、*步骤 3* 和*步骤 4* ，直到所有顶点都被指定了颜色。如果算法终止时所有顶点都是彩色的，则给定的图是二分图。\n6.  如果在运行*步骤 2* 时，搜索遇到一个已经被访问过的顶点，并且该顶点被分配了一种不同于其在*步骤 3* 中被分配的颜色(在搜索树中被分配给其父顶点的颜色的倒数)，则算法立即终止，并且给定的图不是二分图。\n\n下图说明了前面算法的工作原理:\n\n![Figure 6.17: Initialization](img/C14498_06_17.jpg)\n\n###### 图 6.17:初始化\n\n![Figure 6.18: Since vertex 1 was assigned blue, we color vertex 2 red](img/C14498_06_18.jpg)\n\n###### 图 6.18:由于顶点 1 被指定为蓝色，我们将顶点 2 涂成红色\n\n![](img/C14498_06_19.jpg)\n\n###### 图 6.19:由于顶点 2 被涂成红色，我们把顶点 8 涂成蓝色。\n\n从前面的一组图中可以观察到，该算法在图中曲折前进，为每个访问过的顶点分配交替的颜色。如果所有的顶点都可以这样着色，那么这个图就是二分图。如果 DFS 到达两个已经被分配了相同颜色的顶点，则可以安全地声明该图不是二分图。\n\n使用图 6.17*中的图作为输入，您的最终输出应该如下所示:*\n\n![Figure 6.20: Expected output of Activity 13 ](img/C14498_06_20.jpg)\n\n###### 图 6.20:活动 13 的预期产出\n\n#### 注意\n\n这个活动的解决方案可以在第 524 页找到。\n\n## Prim 的 MST 算法\n\nMST 问题在*第 5 章*、*贪婪算法*中介绍，定义如下:\n\n*“给定一个图，G = < V，E >，其中 V 是顶点集，E 是边集，每个边都与一个边权重相关联，找到一个树 T，它跨越 V 中的所有顶点，并且具有最小的总权重。”*\n\n在*第 5 章*、*贪婪算法*中，我们讨论了 MST 问题和 Kruskal 算法的实际应用，该算法在给定的图中找到一个 MST。Kruskal 的算法将图的所有边添加到最小堆中，并贪婪地将最小成本边添加到 MST 中，检查每次添加时树中没有形成循环。\n\nPrim 算法(也称为 Jarvik 算法)背后的思想与 BFS 相似。该算法首先将起始顶点添加到一个*边界*，该边界由一组先前访问过的顶点组成，然后迭代探索与当前边界相邻的顶点。然而，在每次迭代中选择要访问的顶点时，从边界选择具有最低成本边的顶点。\n\n在实现 Prim 的算法时，我们在图的每个顶点上附加了一个*标签*，它存储了它与起始顶点的距离。该算法的工作原理如下:\n\n1.  First, it initializes the labels on all the vertices and sets all the distances to infinity. Since the distance from the starting vertex to itself is *0*, it sets the label of the starting vertex to *0*. Then, it adds all the labels to a min-heap, *H*.\n\n    下图中，红色显示的数字表示距起始顶点的估计距离，假设为顶点*1*；黑色数字表示边缘权重:\n\n    ![](img/C14498_06_21.jpg)\n\n    ###### 图 6.21:初始化 Prim 的 MST 算法\n\n2.  接下来，从 *H* 弹出一个顶点 *U* 。自然地， *U* 是与起始顶点距离最小的顶点。\n3.  For all vertices, *V*, adjacent to *U*, if the label of *V* > edge weight of *(U, V)*, set the label of *V* = edge weight of *(U, V)*. This step is called *settling* or *visiting* vertex *U*:\n\n    ![Figure 6.22: The status of the graph after visiting vertex 1](img/C14498_06_22.jpg)\n\n    ###### 图 6.22:访问顶点 1 后图的状态\n\n4.  While unvisited vertices remain in the graph, go to *step 2*. The following figure shows the state of the graph after visiting vertex *2*, where the edge shown in green is the sole edge in our MST so far:\n\n    ![](img/C14498_06_23.jpg)\n\n    ###### 图 6.23:访问顶点 2 后图的状态\n\n5.  此处显示了所有顶点都已确定后的最终 MST:\n\n![Figure 6.24: MST for our graph](img/C14498_06_24.jpg)\n\n###### 图 6.24:我们图表的最大似然时间\n\n### 练习 30:普里姆算法\n\n在本练习中，我们将实施 Prim 算法，在*图 6.22* 所示的图中找到 MST。按照以下步骤完成本练习:\n\n1.  添加需要的头文件，如下图:\n\n    ```cpp\n    #include <set>\n    #include <map>\n    #include <queue>\n    #include <limits>\n    #include <string>\n    #include <vector>\n    #include <iostream>\n    ```\n\n2.  使用以下结构实现图中的边:\n\n    ```cpp\n    template<typename T> class Graph;\n    template<typename T>\n    struct Edge\n    {\n        size_t src;\n        size_t dest;\n        T weight;\n        // To compare edges, only compare their weights,\n        // and not the source/destination vertices\n        inline bool operator< (const Edge<T>& e) const\n        {\n            return this->weight < e.weight;\n        }\n        inline bool operator> (const Edge<T>& e) const\n        {\n            return this->weight > e.weight;\n        }\n    };\n    ```\n\n3.  使用以下函数重载`Graph`类的`<<`运算符，以便我们可以将图输出到 C++ 流:\n\n    ```cpp\n     template <typename T>\n    std::ostream& operator<<(std::ostream& os, const Graph<T>& G)\n    {\n        for (auto i = 1; i < G.vertices(); i++)\n        {\n            os << i << \":\\t\";\n            auto edges = G.outgoing_edges(i);\n            for (auto& e : edges)\n                os << \"{\" << e.dest << \": \" << e.weight << \"}, \";\n            os << std::endl;\n        }\n        return os;\n    }\n    ```\n\n4.  添加一个基于边列表的图实现，如下图:\n\n    ```cpp\n    template<typename T>\n    class Graph\n    {\n    public:\n        // Initialize the graph with N vertices\n        Graph(size_t N) : V(N)\n        {}\n        // Return number of vertices in the graph\n        auto vertices() const\n        {\n            return V;\n        }\n        // Return all edges in the graph\n        auto& edges() const\n        {\n            return edge_list;\n        }\n        void add_edge(Edge<T>&& e)\n        {\n            // Check if the source and destination vertices are within range\n            if (e.src >= 1 && e.src <= V &&\n                e.dest >= 1 && e.dest <= V)\n                edge_list.emplace_back(e);\n            else\n                std::cerr << \"Vertex out of bounds\" << std::endl;\n        }\n        // Returns all outgoing edges from vertex v\n        auto outgoing_edges(size_t v) const\n        {\n            std::vector<Edge<T>> edges_from_v;\n            for (auto& e : edge_list)\n            {\n                if (e.src == v)\n                    edges_from_v.emplace_back(e);\n            }\n            return edges_from_v;\n        }\n        // Overloads the << operator so a graph be written directly to a stream\n        // Can be used as std::cout << obj << std::endl;\n        template <typename T>\n        friend std::ostream& operator<< <>(std::ostream& os, const Graph<T>& G);\n    private:\n        size_t V;        // Stores number of vertices in graph\n        std::vector<Edge<T>> edge_list;\n    };\n    ```\n\n5.  使用以下代码创建并返回图 6.22 所示的图:\n\n    ```cpp\n     template <typename T>\n    auto create_reference_graph()\n    {\n        Graph<T> G(9);\n        std::map<unsigned, std::vector<std::pair<size_t, T>>> edges;\n        edges[1] = { {2, 2}, {5, 3} };\n        edges[2] = { {1, 2}, {5, 5}, {4, 1} };\n        edges[3] = { {4, 2}, {7, 3} };\n        edges[4] = { {2, 1}, {3, 2}, {5, 2}, {6, 4}, {8, 5} };\n        edges[5] = { {1, 3}, {2, 5}, {4, 2}, {8, 3} };\n        edges[6] = { {4, 4}, {7, 4}, {8, 1} };\n        edges[7] = { {3, 3}, {6, 4} };\n        edges[8] = { {4, 5}, {5, 3}, {6, 1} };\n        for (auto& i : edges)\n            for (auto& j : i.second)\n                G.add_edge(Edge<T>{ i.first, j.first, j.second });\n        return G;\n    }\n    ```\n\n6.  接下来，我们将实现`Label`结构，它的一个实例被分配给图中的每个顶点，以便存储它与边界的距离。使用以下代码进行操作:\n\n    ```cpp\n    template<typename T>\n    struct Label\n    {\n        size_t vertex_ID;\n        T distance_from_frontier;\n        Label(size_t _id, T _distance) :\n            vertex_ID(_id),\n            distance_from_frontier(_distance)\n        {}\n        // To compare labels, only compare their distances from source\n        inline bool operator< (const Label<T>& l) const\n        {\n            return this->distance_from_frontier < l.distance_from_frontier;\n        }\n        inline bool operator> (const Label<T>& l) const\n        {\n            return this->distance_from_frontier > l.distance_from_frontier;\n        }\n        inline bool operator() (const Label<T>& l) const\n        {\n            return this > l;\n        }\n    };\n    ```\n\n7.  编写一个函数来实现 Prim 的 MST 算法，如下所示:\n\n    ```cpp\n    template <typename T>\n    auto prim_MST(const Graph<T>& G, size_t src)\n    {\n        std::priority_queue<Label<T>, std::vector<Label<T>>, std::greater<Label<T>>> heap;\n        std::set<int> visited;\n        std::vector<T> distance(G.vertices(), std::numeric_limits<T>::max());\n        std::vector<size_t> MST;\n        heap.emplace(src, 0);\n        // Search for the destination vertex in the graph\n        while (!heap.empty())\n        {\n            auto current_vertex = heap.top();\n            heap.pop();\n            // If the current vertex hasn't been visited in the past\n            if (visited.find(current_vertex.vertex_ID) == visited.end())\n            {\n                std::cout << \"Settling vertex ID \" \n    << current_vertex.vertex_ID << std::endl;\n                MST.push_back(current_vertex.vertex_ID);\n            // For each outgoing edge from the current vertex, \n            // create a label for the destination vertex and add it to the heap\n                for (auto e : G.outgoing_edges(current_vertex.vertex_ID))\n                {\n                    auto neighbor_vertex_ID = e.dest;\n                    auto new_distance_to_frontier = e.weight;\n            // Check if the new path to the vertex is shorter\n            // than the previously known best path. \n            // If yes, update the distance \n                    if (new_distance_to_frontier < distance[neighbor_vertex_ID])\n                    {\n    heap.emplace(neighbor_vertex_ID,  new_distance_to_frontier);\n                        distance[e.dest] = new_distance_to_frontier;\n                    }\n                }\n                visited.insert(current_vertex.vertex_ID);\n            }\n        }\n        return MST;\n    }\n    ```\n\n8.  最后，添加以下代码，运行我们对 Prim 算法的实现并输出结果:\n\n    ```cpp\n    template<typename T>\n    void test_prim_MST()\n    {\n        auto G = create_reference_graph<T>();\n        std::cout << G << std::endl;\n        auto MST = prim_MST<T>(G, 1);\n        std::cout << \"Minimum Spanning Tree:\" << std::endl;\n        for (auto v : MST)\n            std::cout << v << std::endl;\n        std::cout << std::endl;\n    }\n    int main()\n    {\n        using T = unsigned;\n        test_prim_MST<T>();\n        return 0;\n    }\n    ```\n\n9.  运行程序。您的输出应该如下所示:\n\n![Figure 6.25: Output of Exercise 30 ](img/C14498_06_25.jpg)\n\n###### 图 6.25:练习 30 的输出\n\n当使用二进制最小堆和邻接表来存储 MST 时，Prim 算法的时间复杂度是*O(E+V log V)*，当使用一种称为“斐波那契最小堆”的堆时，可以将其改进为 *O(E + V log V)*\n\n虽然 Prim 和 Kruskal 都是贪婪算法的例子，但它们在重要方面有所不同，其中一些总结如下:\n\n![Figure 6.26: Table comparing Kruskal’s and Prim’s algorithms](img/C14498_06_26.jpg)\n\n###### 图 6.26:比较克鲁斯卡尔和普里姆算法的表格\n\n## 迪克斯特拉最短路径算法\n\n每当用户在谷歌地图等路线规划应用或汽车内置的导航软件中请求路线时，图上的单源最短路径问题就会得到解决。问题定义如下:\n\n*“给定一个有向图，G - < V，E >，其中 V 是顶点集，E 是边集，每个边都与边权重、源顶点和目的顶点相关联，找到从源到目的地的最小成本路径。”*\n\nDijkstra 的算法适用于非负边权重的图，只是对 Prim 的 MST 算法稍作修改，有两个主要变化:\n\n*   Dijkstra 的算法不是将每个顶点上的标签设置为距离边界的最小距离，而是将每个顶点上的标签设置为距离源顶点的总距离。\n*   如果从堆中弹出目标顶点，Dijkstra 的算法就会终止，而 Prim 的算法只有在堆中没有更多顶点需要处理时才会终止。\n\n该算法的工作过程如下所示:\n\n1.  First, it initializes the labels on all the vertices and sets all the distances to infinity. Since the distance from the starting vertex to itself is *0*, it sets the label of the starting vertex to *0*. Then, it adds all the labels to a min-heap, *H*.\n\n    在下图中，红色数字表示从源(顶点 *2* )到目的地(顶点 *6* )的当前已知最佳距离:\n\n    ![Figure 6.27: Initializing Dijkstra’s algorithm](img/C14498_06_27.jpg)\n\n    ###### 图 6.27:初始化迪克斯特拉算法\n\n2.  然后，从 *H* 弹出一个顶点 *U* 。自然， *U* 是距离起始顶点距离最小的顶点。如果 *U* 是需要的目的地，我们已经找到了最短路径，算法终止。\n3.  For all vertices, *V*, adjacent to *U*, if the label of *V* > (label of *U* + edge weight of *(U, V)*), we have found a path to *V* that is shorter than the previously known minimum-cost path. Therefore, set the label of *V* to (label of *U* + edge weight of *(U, V)*). This step is called **settling** or **visiting** the vertex *U*:\n\n    ![](img/C14498_06_28.jpg)\n\n    ###### 图 6.28:解决顶点 1 后的算法状态\n\n4.  While unvisited vertices remain in the graph, go to *step 2*. The following figure shows the state of the graph after settling vertex *2*:\n\n    ![](img/C14498_06_29.jpg)\n\n    ###### 图 6.29:解决顶点 2 后算法的状态\n\n5.  当从 *H* 弹出目的顶点(顶点标识 *6* )时，算法终止。算法从 *1* 到 *6* 找到的最短路径如下图所示。另外，其他固定顶点上的标签显示了从 *1* 到该顶点的最短距离:\n\n![Figure 6.30: The shortest path from 1 to 6](img/C14498_06_30.jpg)\n\n###### 图 6.30:从 1 到 6 的最短路径\n\n### 练习 31:实现迪克斯特拉算法\n\n在本练习中，我们将实现 Dijkstra 算法，以在*图 6.28* 所示的图中找到最短路径。按照以下步骤完成本练习:\n\n1.  包含需要的头文件，声明图数据结构，如下图:\n\n    ```cpp\n    #include <string>\n    #include <vector>\n    #include <iostream>\n    #include <set>\n    #include <map>\n    #include <limits>\n    #include <queue>\n    template<typename T> class Graph;\n    ```\n\n2.  编写以下结构来实现我们的图实现中的一条边:\n\n    ```cpp\n    template<typename T>\n    struct Edge\n    {\n        size_t src;\n        size_t dest;\n        T weight;\n        // To compare edges, only compare their weights,\n        // and not the source/destination vertices\n        inline bool operator< (const Edge<T>& e) const\n        {\n            return this->weight < e.weight;\n        }\n        inline bool operator> (const Edge<T>& e) const\n        {\n            return this->weight > e.weight;\n        }\n    };\n    ```\n\n3.  为`Graph`类重载`<<`运算符，以便可以使用流输出，如下所示:\n\n    ```cpp\n     template <typename T>\n    std::ostream& operator<<(std::ostream& os, const Graph<T>& G)\n    {\n        for (auto i = 1; i < G.vertices(); i++)\n        {\n            os << i << \":\\t\";\n            auto edges = G.outgoing_edges(i);\n            for (auto& e : edges)\n                os << \"{\" << e.dest << \": \" << e.weight << \"}, \";\n            os << std::endl;\n        }\n        return os;\n    }\n    ```\n\n4.  实现图，如下图所示:\n\n    ```cpp\n    template<typename T>\n    class Graph\n    {\n    public:\n        // Initialize the graph with N vertices\n        Graph(size_t N) : V(N)\n        {}\n        // Return number of vertices in the graph\n        auto vertices() const\n        {\n            return V;\n        }\n        // Return all edges in the graph\n        auto& edges() const\n        {\n            return edge_list;\n        }\n        void add_edge(Edge<T>&& e)\n        {\n            // Check if the source and destination vertices are within range\n            if (e.src >= 1 && e.src <= V &&\n                e.dest >= 1 && e.dest <= V)\n                edge_list.emplace_back(e);\n            else\n                std::cerr << \"Vertex out of bounds\" << std::endl;\n        }\n        // Returns all outgoing edges from vertex v\n        auto outgoing_edges(size_t v) const\n        {\n            std::vector<Edge<T>> edges_from_v;\n            for (auto& e : edge_list)\n            {\n                if (e.src == v)\n                    edges_from_v.emplace_back(e);\n            }\n            return edges_from_v;\n        }\n        // Overloads the << operator so a graph be written directly to a stream\n        // Can be used as std::cout << obj << std::endl;\n        template <typename T>\n        friend std::ostream& operator<< <>(std::ostream& os, const Graph<T>& G);\n    private:\n        size_t V;        // Stores number of vertices in graph\n        std::vector<Edge<T>> edge_list;\n    };\n    ```\n\n5.  使用`Graph`类编写一个函数来创建*图 6.28* 所示的参考图，如下所示:\n\n    ```cpp\n    template <typename T>\n    auto create_reference_graph()\n    {\n        Graph<T> G(9);\n        std::map<unsigned, std::vector<std::pair<size_t, T>>> edges;\n        edges[1] = { {2, 2}, {5, 3} };\n        edges[2] = { {1, 2}, {5, 5}, {4, 1} };\n        edges[3] = { {4, 2}, {7, 3} };\n        edges[4] = { {2, 1}, {3, 2}, {5, 2}, {6, 4}, {8, 5} };\n        edges[5] = { {1, 3}, {2, 5}, {4, 2}, {8, 3} };\n        edges[6] = { {4, 4}, {7, 4}, {8, 1} };\n        edges[7] = { {3, 3}, {6, 4} };\n        edges[8] = { {4, 5}, {5, 3}, {6, 1} };\n        for (auto& i : edges)\n            for (auto& j : i.second)\n                G.add_edge(Edge<T>{ i.first, j.first, j.second });\n        return G;\n    }\n    ```\n\n6.  Implement Dijkstra's algorithm, as shown here:\n\n    ```cpp\n    template <typename T>\n    auto dijkstra_shortest_path(const Graph<T>& G, size_t src, size_t dest)\n    {\n        std::priority_queue<Label<T>, std::vector<Label<T>>, std::greater<Label<T>>> heap;\n        std::set<int> visited;\n        std::vector<size_t> parent(G.vertices());\n        std::vector<T> distance(G.vertices(), std::numeric_limits<T>::max());\n        std::vector<size_t> shortest_path;\n        heap.emplace(src, 0);\n        parent[src] = src;\n        // Search for the destination vertex in the graph\n        while (!heap.empty()) {\n            auto current_vertex = heap.top();\n            heap.pop();\n            // If the search has reached the destination vertex\n            if (current_vertex.vertex_ID == dest) {\n                std::cout << \"Destination \" << \n    current_vertex.vertex_ID << \" reached.\" << std::endl;\n                break;\n            }\n            if (visited.find(current_vertex.vertex_ID) == visited.end()) {\n                std::cout << \"Settling vertex \" << \n    current_vertex.vertex_ID << std::endl;\n                // For each outgoing edge from the current vertex, \n                // create a label for the destination vertex and add it to the heap\n                for (auto e : G.outgoing_edges(current_vertex.vertex_ID)) {\n                    auto neighbor_vertex_ID = e.dest;\n                    auto new_distance_to_dest=current_vertex.distance_from_source \n    + e.weight;\n                    // Check if the new path to the destination vertex \n    // has a lower cost than any previous paths found to it, if // yes, then this path should be preferred \n                    if (new_distance_to_dest < distance[neighbor_vertex_ID]) {\n                        heap.emplace(neighbor_vertex_ID, new_distance_to_dest);\n                        parent[e.dest] = current_vertex.vertex_ID;\n                        distance[e.dest] = new_distance_to_dest;\n                    }\n                }\n                visited.insert(current_vertex.vertex_ID);\n            }\n        }\n        // Construct the path from source to the destination by backtracking \n        // using the parent indexes\n        auto current_vertex = dest;\n        while (current_vertex != src) {\n            shortest_path.push_back(current_vertex);\n            current_vertex = parent[current_vertex];\n        }\n        shortest_path.push_back(src);\n        std::reverse(shortest_path.begin(), shortest_path.end());\n        return shortest_path;\n    }\n    ```\n\n    我们的实现分两个阶段工作——它从源开始搜索目的顶点，并使用回溯阶段，其中最短路径是通过跟踪从目的点到源的父指针找到的。\n\n7.  最后，通过在图中找到顶点 *1* 和 *6* 之间的最短路径，添加以下代码来测试我们对 Dijkstra 算法的实现:\n\n    ```cpp\n     template<typename T>\n    void test_dijkstra()\n    {\n        auto G = create_reference_graph<T>();\n        std::cout << \"Reference graph:\" << std::endl;\n        std::cout << G << std::endl;\n        auto shortest_path = dijkstra_shortest_path<T>(G, 1, 6);\n        std::cout << \"The shortest path between 1 and 6 is:\" << std::endl;\n        for (auto v : shortest_path)\n            std::cout << v << \" \";\n        std::cout << std::endl;\n    }\n    int main()\n    {\n        using T = unsigned;\n        test_dijkstra<T>();\n        return 0;\n    }\n    ```\n\n8.  运行程序。您的输出应该如下所示:\n\n![Figure 6.31: Output of Exercise 31 ](img/C14498_06_31.jpg)\n\n###### 图 6.31:练习 31 的输出\n\n正如您在前面的输出中所看到的，我们的程序沿着顶点 *1* 和 *6* 之间的最短路径跟踪顶点。当使用 Fibonacci 最小堆时，Dijkstra 算法最著名的运行时间是 *O(E + V log V)* 。\n\n### 活动 14:纽约最短路径\n\n在本练习中，您需要用 C++ 实现 Dijkstra 算法，以便它可以用来在给定的纽约道路网络中找到最短路径。我们的道路图由 264，326 个顶点和 733，846 条有向边组成，边权重是顶点之间的欧氏距离。此活动的步骤如下:\n\n1.  Download the road graph file from the following link: [https://raw.githubusercontent.com/TrainingByPackt/CPP-Data-Structures-and-Algorithm-Design-Principles/master/Lesson6/Activity14/USA-road-d.NY.gr](https://raw.githubusercontent.com/TrainingByPackt/CPP-Data-Structures-and-Algorithm-Design-Principles/master/Lesson6/Activity14/USA-road-d.NY.gr).\n\n    #### 注意\n\n    如果文件不是自动下载的，而是在您的浏览器中打开的，请通过右键单击任意空白处并选择“**另存为…** ”来下载文件\n\n2.  If you're running Windows, move the downloaded file to `<project directory>/out/x86-Debug/Chapter6`.\n\n    如果你运行的是 Linux，把下载的文件移到`<project directory>/build/Chapter6`。\n\n    #### 注意\n\n    目录结构可能因您的集成开发环境而异。该文件需要放在与编译的二进制文件相同的目录中。或者，您可以调整实现以接受文件的路径。\n\n3.  The road graph is a text file with three different kinds of rows:\n\n    ![Figure 6.32: Table describing the road graph file for New York](img/C14498_06_32.jpg)\n\n    ###### 图 6.32:描述纽约道路图文件的表格\n\n4.  实现一个加权边图。假设一旦创建了图，就不能在图中添加或删除顶点，这是可以接受的。\n5.  实现一个函数来解析道路图文件并填充该图。\n6.  实现 Dijkstra 算法，通过寻找顶点`913`和`542`之间的最短路径来测试你的实现。您的输出应该如下所示:\n\n![Figure 6.33: Expected output of Activity 14 ](img/C14498_06_33.jpg)\n\n###### 图 6.33:活动 14 的预期产出\n\n#### 注意\n\n这个活动的解决方案可以在第 530 页找到。\n\n## 总结\n\n本章我们讨论了三个主要的图问题:首先，图遍历问题，介绍了两种解决方案，广度优先搜索(BFS)和深度优先搜索(DFS)。其次，我们重新讨论了最小生成树问题，并使用 Prim 算法进行了求解。我们还将其与 Kruskal 算法进行了比较，并讨论了一个算法优于另一个算法的条件。最后，我们引入了单源最短路径问题，该问题在图中寻找最小代价的最短路径，并覆盖了 Dijkstra 最短路径算法。\n\n然而，Dijkstra 算法只适用于边权重为正的图。在下一章中，我们将寻求放松这一限制，并引入一个最短路径算法，可以处理负边缘权重。我们还将推广最短路径问题，以找到图中所有顶点对之间的最短路径。"
  },
  {
    "path": "docs/cpp-dsal-design-principle/07.md",
    "content": "# 七、图算法 2\n\n## 学习目标\n\n本章结束时，您将能够:\n\n*   描述 Dijkstra 算法的固有问题，并演示如何对其进行修改和/或与其他算法相结合来规避这些问题\n*   使用贝尔曼-福特和约翰逊算法寻找图中的最短路径\n*   描述 garaph 中强连接组件的重要性\n*   使用 Kosaraju 算法寻找图中的强连通分支\n*   描述有向图和无向图连通性的区别\n*   对复杂问题实施深度优先搜索\n*   评估图中的负权重循环\n\n本章在前一章的基础上，介绍了一些更高级的图算法。您还将学习如何处理负权重和处理负权重周期的异常。\n\n## 简介\n\n到目前为止，我们已经探索了各种常见的编程结构和范例。现在，我们将深入研究几项技术，这些技术扩展了我们之前讨论过的主题，从一系列高级图问题开始，然后将重点转移到动态编程的扩展主题上。\n\n在这一章中，我们将讨论三种著名的算法，即贝尔曼-福特算法、约翰逊算法和科萨拉朱算法。所有这些算法都与我们在本书中已经介绍过的算法有着明显的相似之处，但是以各种方式扩展和组合它们，以比次优实现所允许的更高的效率来解决潜在的复杂问题。除了学习这些特定的技术，本章还应该增加你对基本图相关技术的使用的总体熟悉程度，并提供更多关于如何将这些基础应用于各种不同问题的见解。\n\n## 再谈最短路径问题\n\n我们之前讨论了寻找图中两个节点之间最短路径的几种方法。我们从探索最标准的图遍历形式开始，即深度优先搜索和广度优先搜索，并最终讨论了如何处理包含加权边的图的更有问题的情况。我们演示了如何使用 Dijkstra 算法，根据立即可用的最佳选项贪婪地对遍历中的每一步进行优先级排序，从而有效地找到加权图中的最短距离。然而，尽管 Dijkstra 算法在性能上有所提高，但它并不适用于所有情况。\n\n考虑正在通过网络广播的无线信号；当它超过最初传播的点时，它的强度可能会受到许多因素的影响，例如它行进的距离以及它必须穿过的墙和其他障碍物的数量。如果您想确定信号到达每个目的地的路径，从而最大限度地减少信号恶化，您可以创建一个加权图，其中网络中的每个点由一个节点表示，任意两点之间的信号损失程度由加权边表示。然后，您可以使用迪克斯特拉算法计算图中的最短距离，以确定网络中成本最低的路径。\n\n现在，假设在网络中安装了一个中继器/增强器，以增加特定点的信号强度，这种增加如何在您的图表中表示？最明显的方法是将升压器节点的输出边缘权重设置为负值(相当于它增加信号强度的程度)，这将减少通过它的任何路径的总距离/恶化。如果我们在网络图中使用迪克斯特拉算法，这将如何影响我们的结果？\n\n正如我们在上一章中所讨论的，Dijkstra 的算法在遍历中如何选择每个顶点方面采取了贪婪的方法。在每一步中，它都会找到最近的未访问顶点，并将其添加到访问集中，从而将其排除在进一步的考虑之外。迪杰斯特拉算法的假设是，到目前为止考虑的每个顶点的最短路径已经找到，所以寻找更好的替代方案是没有意义的。然而，在包含负边权重的图中，如果它们在遍历的早期阶段产生较高的和，这种方法将不会探索导致最优解的可能性。\n\n考虑一个具有负边权重的图，如下图所示:\n\n![Figure 7.1: Applying Dijkstra's algorithm to a graph with a negative weight ](img/C14498_07_01.jpg)\n\n###### 图 7.1:将迪克斯特拉算法应用于负权重图\n\n在上图中，迪克斯特拉算法穿过的路径用红色表示。假设我们从顶点 A 开始，第一次从节点 *A* 移动到节点 *B* 后会有两个潜在的选项: *B — > C* ，其边权重为 *5* ， *B — > D* ，其边权重为 *10* 。由于 Dijkstra 的贪婪方法， *C* 将被选为最短路径中的下一个节点，但我们可以清楚地看到，另一个选项(*B—>D—>C = 10+–7 = 3*)实际上是最优选择。\n\n当面对负边缘权重时，Dijkstra 算法中固有的优化使其具有高水平的效率，最终导致其垮台。谢天谢地，对于这样的图，我们可以采用一种替代方法，这种方法与 Dijkstra 的算法非常相似，并且可以说实现起来更简单。\n\n## 贝尔曼-福特算法\n\n我们可以使用**贝尔曼-福特算法**来处理负权重的图。它用另一种方法取代了迪克斯特拉的贪婪选择方法，即迭代图中的每条边*V–1*次(其中 *V* 等于顶点的总数)，并在每次迭代中逐渐找到从源节点开始的最佳距离值。自然地，这使得它比迪克斯特拉算法具有更高的渐近复杂度，但是它也允许它为迪克斯特拉算法会误解的图产生正确的结果。下面的练习展示了如何实现贝尔曼-福特算法。\n\n### 练习 32:实现贝尔曼-福特算法(第一部分)\n\n在本练习中，我们将使用基本的贝尔曼-福特算法来寻找负权重图中的最短距离。让我们开始吧:\n\n1.  首先，通过包含必要的库(以及方便起见的`namespace std`)来设置您的代码:\n\n    ```cpp\n    #include <iostream>\n    #include <vector>\n    #include <climits>\n    using namespace std;\n    ```\n\n2.  让我们从定义图中边的表示开始，这将需要三个变量:源节点的索引、目标节点的索引以及在它们之间遍历的成本:\n\n    ```cpp\n    struct Edge\n    {\n        int start;    // The starting vertex\n        int end;      // The destination vertex\n        int weight;   // The edge weight\n        // Constructor\n        Edge(int s, int e, int w) : start(s), end(e), weight(w) {}\n    };\n    ```\n\n3.  为了实现贝尔曼-福特算法，我们需要对我们的图进行一些表示。为了简单起见，让我们假设我们的图可以用整数`V`、图中顶点的总数和向量`edges`(定义图的邻接的“边”对象的指针集合)来表示。让我们也定义一个整数常数`UNKNOWN`，我们可以将其设置为某个任意的高值，该值总是大于图中任何边权重子集的总和(在`climits`中定义的`INT_MAX`常数适用于此目的):\n\n    ```cpp\n    const int UNKNOWN = INT_MAX;\n    vector<Edge*> edges;   // Collection of edge pointers\n    int V;                 // Total number of vertices in the graph\n    int E;                 // Total number of edges in the graph\n    ```\n\n4.  让我们也编写一些代码来收集图数据作为用户输入:\n\n    ```cpp\n    int main()\n    {\n        cin >> V >> E;\n        for(int i = 0; i < E; i++)\n        {\n            int node_a, node_b, weight;\n            cin >> node_a >> node_b >> weight;\n            // Add a new edge using the defined constructor\n            edges.push_back(new Edge(node_a, node_b, weight));\n        }\n        // Choose a starting node\n        int start;\n        cin >> start;\n        // Run the Bellman-Ford algorithm on the graph for \n        // the chosen starting vertex \n        BellmanFord(start);\n        return 0;\n    }\n    ```\n\n5.  现在，我们可以开始实现贝尔曼-福特算法本身。出于我们的目的，让我们创建一个名为`BellmanFord()`的函数，该函数接受一个参数—`start`(我们希望从中找到图中最短路径的起始节点)—并返回`void`。然后，我们将定义一个大小为`V`的距离数组，每个元素都初始化为`UNKNOWN`，除了起始节点，其索引初始化为`0` :\n\n    ```cpp\n        void BellmanFord(int start)\n        {\n            vector<int> distance(V, UNKNOWN);\n            distance[start] = 0;\n    ```\n\n6.  大部分工作在下一步完成，我们定义一个循环，持续`V – 1`次迭代，并在每次重复时遍历整个边集。对于每条边，我们检查其源节点的当前距离值是否不等于`UNKNOWN`(在第一次迭代中，仅适用于起始节点)。假设这是真的，那么我们将它的目的节点的当前距离值与源节点的距离和边的权重相比较。如果将边缘权重添加到当前节点的距离的结果小于目标节点的存储距离，我们用新的总和替换它在距离数组中的值:\n\n    ```cpp\n    // Perform V - 1 iterations\n    for(int i = 0; i < V; i++)\n    {\n        // Iterate over entire set of edges\n        for(auto edge : edges)\n        {\n            int u = edge->start;\n            int v = edge->end;\n            int w = edge->weight;\n            // Skip nodes which have not yet been considered\n            if(distance[u] == UNKNOWN)\n            {\n                continue;\n            }\n            // If the current distance value for the destination\n            // node is greater than the sum of the source node's\n            // distance and the edge's weight, change its distance\n            // to the lesser value.\n            if(distance[u] + w < distance[v])\n            {\n                distance[v] = distance[u] + w;\n            }\n        }\n    }\n    ```\n\n7.  在函数的最后，我们现在可以遍历`distance`数组，并输出从源到图中每隔一个节点的最短距离:\n\n    ```cpp\n    cout << \"DISTANCE FROM VERTEX \" << start << \":\\n\"\n    for(int i = 0; i < V; i++)\n    {\n        cout << \"\\t\" << i << \": \";\n        if(distance[i] == UNKNOWN)\n        {\n            cout << \"Unvisited\" << endl;\n            continue;\n        }\n        cout << distance[i] << endl;\n    }\n    ```\n\n8.  现在，我们可以返回到我们的`main()`方法，并调用我们新实现的`BellmanFord()`函数。让我们在来自*图 7.1* 的示例图上测试我们的实现。为此，我们应该运行代码并输入以下输入:\n\n    ```cpp\n    5 5\n    0 1 3\n    1 2 5\n    1 3 10\n    3 2 -7\n    2 4 2\n    0\n    ```\n\n9.  我们的程序应该输出以下内容:\n\n    ```cpp\n    DISTANCE FROM VERTEX 0:\n        0: 0\n        1: 3\n        2: 6\n        3: 13\n        4: 8\n    ```\n\n如我们所见，贝尔曼-福特避免了会导致迪克斯特拉算法错误评估最短路径的陷阱。然而，还有另一个重要的问题需要解决，我们将在下一节中讨论。\n\n## 贝尔曼-福特算法(第二部分)——负权重循环\n\n考虑下图所示的图表:\n\n![](img/C14498_07_02.jpg)\n\n###### 图 7.2:负权重循环的图表\n\n用红色突出显示的边表示负权重循环或图中的循环，其中合并的边权重产生负总和。在这种情况下，这种循环会被反复考虑，最终结果会有偏差。\n\n为了比较，考虑一个只有正边权重的图。这样一个图中的循环永远不会被考虑在解决方案中，因为到循环中第一个节点的最短距离已经被找到。为了演示这一点，假设上图中节点 *B* 和 *D* 之间的边缘权重为正。从节点 *A* 开始，通过边的第一次迭代将确定到节点 *B* 的最短距离等于 *3* 。再经过两次迭代，我们也会知道 *A* 到*C*(*A—>B—>D—>C*)的最短距离，等于 *14* ( *3 + 8 + 3* )。\n\n显然，14 加不出小于 3 的正数。由于在每个节点只被访问一次的任何图遍历中最多只能有*| V–1 |*个步骤，我们可以确定通过图的边的*| V–1 |*次迭代足以确定每个可能的最短距离。推而广之，我们可以得出结论，在*| V–1 |*迭代之后，更短的路径能够存在的唯一方式是重新访问一个节点，并且导致它的边权重是负的。因此，贝尔曼-福特算法的最后一步包括通过边缘执行一次迭代，以检查这种循环的存在。\n\n我们可以使用与寻找最短路径相同的逻辑来实现这一点:通过检查每个边的权重与其源节点的距离值之和是否小于当前存储的到其目的节点的距离。如果在此步骤中发现较短的路径，我们将终止算法并报告负循环的存在。\n\n我们将在下面的练习中探索算法的这种实现。\n\n### 练习 33:实现贝尔曼-福特算法(第二部分)\n\n在本练习中，我们将修改*练习 32* 、*中实现贝尔曼-福特算法(第一部分)*的实现，以处理具有负权重循环的图。让我们开始吧:\n\n1.  我们基本上可以逐字复制上一步的代码。但是，这一次，我们将在确定是否找到了更短路径的条件下，用某种指示图包含负循环的输出来替换代码，从而使其无效:\n\n    ```cpp\n        // Iterate through edges one last time\n        for(auto edge : edges)\n        {\n            int u = edge->start;\n            int v = edge->end;\n            int w = edge->weight;\n\n            if(distance[u] == UNKNOWN)\n            {\n                continue;\n            }\n    ```\n\n2.  如果我们仍然能找到比我们已经找到的路径更短的路径，那么这个图一定包含一个负循环。让我们用以下`if`语句来检查负重量循环:\n\n    ```cpp\n            if(distance[u] + w < distance[v])\n            {\n                cout << \"NEGATIVE CYCLE FOUND\" << endl;\n                return;\n            }\n        }\n    ```\n\n3.  现在，让我们在第一个`for`循环结束和第一个输出行\n\n    ```cpp\n    void BellmanFord(int start)\n    {\n        vector<int> distance(V, UNKNOWN);\n        distance[start] = 0;\n        for(int i = 1; i < V; i++)\n        {\n            for(auto edge : edges)\n            {\n                int u = edge->start;\n                int v = edge->end;\n                int w = edge->weight;\n                if(distance[u] == UNKNOWN)\n                {\n                    continue;\n                } \n                if(distance[u] + w < distance[v])\n                {\n                    distance[v] = distance[u] + w;\n                }\n            }\n        }\n        for(auto edge : edges)\n        {\n            int u = edge->start;\n            int v = edge->end;\n            int w = edge->weight;\n            if(distance[u] == UNKNOWN)\n            {\n                continue;\n            }\n            if(distance[u] + w < distance[v])\n            {\n                cout << \"NEGATIVE CYCLE FOUND\" << endl;\n                return;\n            }\n        }\n        cout << \"DISTANCE FROM VERTEX \" << start << \":\\n\";\n        for(int i = 0; i < V; i++)\n        {\n            cout << \"\\t\" << i << \": \";\n            if(distance[i] == UNKNOWN)\n            {\n                cout << \"Unvisited\" << endl;\n                continue;\n            }\n            cout << distance[i] << endl;\n        }\n    }\n    ```\n\n    之间插入这段代码\n4.  为了测试我们添加的逻辑，让我们在以下输入上运行算法:\n\n    ```cpp\n    6 8\n    0 1 3\n    1 3 -8\n    2 1 3\n    2 5 5\n    3 2 3\n    2 4 2\n    4 5 -1\n    5 1 8\n    0\n    ```\n\n5.  我们的程序应该输出以下内容:\n\n    ```cpp\n    NEGATIVE CYCLE FOUND\n    ```\n\n### 活动 15:贪婪机器人\n\n你正在开发一种寻路机器人，它必须找到通过障碍跑道的最有效路径。出于测试目的，您设计了几门课程，每门都是正方形网格。你的机器人能够穿越它遇到的任何障碍，但这也需要更大的动力消耗。假设您的机器人从网格的左上角开始，可以在四个基本方向(北、南、东和西)中的任何一个方向上移动，您必须实现一个算法来确定您的机器人可以完成课程的最大能量。\n\n由于执行这种遍历所需的能量可能很高，所以您在整个电网中散布了发电站，您的机器人可以使用这些发电站为自己充电。不幸的是，看起来你的机器人在能源消耗方面相当贪婪——如果它可以多次到达一个能源站而不必回溯，它将不断返回同一个位置，直到它不可避免地过度充电并爆炸！正因为如此，你需要预测你的机器人是否会在灾难发生前重新访问发电站并中止穿越尝试。\n\n**输入**\n\n*   第一行包含单个整数`N`，是球场的高度和宽度。\n*   下一个`N` `2` `- 1`行各包含`directions`字符串和一个名为`power`的整数。每组`N`行对应一行，从网格顶部开始，其中每个单元格的数据从左到右定义(例如，在 *3 x 3* 网格中，*0—>【0，0】，1—>【0，1】，2—>【0，2】，3—>【1，0】，4—>【1，1】*等等)。\n*   `directions`包含集合{“`N`”、“`S`”、“`E`”、“`W`”中的 0-3 个字符，代表您的机器人可以从每个点访问的细胞。因此，如果`directions`弦是`SW`，那么机器人可以从该点向南或向西移动。`power`代表穿过细胞所需的能量消耗。`power`的正值表示充电站位于电池内。\n\n**输出**\n\n*   如果穿越路线导致机器人爆炸，打印一行–`TRAVERSAL ABORTED`。\n*   否则，打印你的机器人到达课程右下角单元格时所能拥有的最大能量，相对于它开始时的能量。例如，如果机器人能以比开始多 10 个单位的能量完成迷宫，打印`10`；如果它以比开始时少 10 个单位的能量完成迷宫，打印`-10`。\n\n**例**\n\n假设我们有以下输入:\n\n```cpp\n3\nSE -10\nSE -8\nS -6\nS 7\nE -10\nS 20\nE -1\nNE 5\n```\n\n网格的布局如下所示:\n\n![Figure 7.3: Grid for the robot's traversal ](img/C14498_07_03.jpg)\n\n###### 图 7.3:机器人遍历的网格\n\n到达右下角能量最大的单元的路径如下:\n\n```cpp\n0 —> 3 (-10)\n3 —> 6 (+7)\n6 —> 7 (-1)\n7 —> 4 (+5)\n4 —> 5 (-10)\n5 —> 8 (+20)\n(-10) + 7 + (-1) + 5 + (-10) + 20 \n= 11 more units of energy\n```\n\n因此，你的程序应该输出`11`。\n\n**测试用例**\n\n以下测试用例应该有助于您更好地理解这个问题:\n\n![Figure 7.4: Test case 1 for Activity 15 ](img/C14498_07_04.jpg)\n\n###### 图 7.4:活动 15 的测试用例 1\n\n![Figure 7.5: Test case 2 for Activity 15 ](img/C14498_07_05.jpg)\n\n###### 图 7.5:活动 15 的测试用例 2\n\n![Figure 7.6: Test case 3 for Activity 15 ](img/C14498_07_06.jpg)\n\n###### 图 7.6:活动 15 的测试用例 3\n\n![Figure 7.7: Test case 4 for Activity 15 ](img/C14498_07_07.jpg)\n\n###### 图 7.7:活动 15 的测试用例 4\n\n![Figure 7.8: Test case 5 for Activity 15 ](img/C14498_07_08.jpg)\n\n###### 图 7.8:活动 15 的测试用例 5\n\n**活动指南**\n\n*   不需要超出*练习 33* 、*实施贝尔曼-福特算法(第二部分)*中所述的算法。\n*   您可能需要重新解释一些输入，以便它对应于您试图解决的实际问题。\n*   There is no need to represent the grid as two-dimensional.\n\n    #### 注意\n\n    这个活动的解决方案可以在第 537 页找到。\n\n我们现在已经确定，Bellman-Ford 比 Dijkstra 算法更通用，因为它具有在 Dijkstra 算法会产生不正确结果的情况下产生正确解的能力。然而，如果我们正在考虑的图不包含任何负边权重，Dijkstra 算法是两者之间的明显选择，因为其贪婪方法提供了潜在的显著效率优势。现在，我们将探索贝尔曼-福特如何与迪克斯特拉算法结合使用，以便它可以用于负权重的图。\n\n## 约翰逊算法\n\n比较了 Bellman-Ford 算法和 Dijkstra 算法的相对优缺点后，我们现在将讨论一种算法，该算法将两者结合起来检索图中每对顶点之间的最短路径。**约翰逊算法**为我们提供了这样的优势:能够利用 Dijkstra 算法的效率，同时仍然为负边权重的图产生正确的结果。\n\n约翰逊算法背后的概念相当新颖——为了应对 Dijkstra 在处理负权重时的局限性，约翰逊算法只是对图中的边进行重新加权，使它们一致非负。这是通过贝尔曼-福特的创造性运用，结合一些特别优雅的数学逻辑来实现的。\n\n约翰逊算法的第一步是向图中添加一个新的“虚拟”顶点，该顶点随后通过零加权边与每隔一个顶点相连。然后，贝尔曼-福特被用来寻找新顶点和其余顶点之间的最短路径，这些距离被存储起来以备后用。\n\n考虑添加这个新顶点的含义:因为它有一条 0 加权边连接到图中的每个其他节点，所以它的最短路径距离都不会是正的。此外，它与图中每个节点的连通性确保了它的距离值在所有潜在的遍历路径上保持恒定的关系，这使得这些值和它们对应的边权重形成的和“伸缩”，换句话说，序列中的后续项相互抵消，使得总和等于第一项和最后一项的差。请看下图:\n\n![Figure 7.9: Applying Johnson's algorithm on a graph with negative weights ](img/C14498_07_09.jpg)\n\n###### 图 7.9:在负权重的图上应用约翰逊算法\n\n在上图中，标记为`S`的菱形节点表示虚拟顶点，黑色括号数字表示边权重，红色文本表示从`S`到每个节点的最短路径，橙色箭头表示从`S`经过的最佳路径，蓝色箭头表示从`S`分支的 0 权重边，这些边不包含在任何`S`的最短路径中。\n\n让我们获取新的距离值，并根据它们在图的遍历中的外观按顺序排列–`A --> B --> C --> A --> D --> E`:\n\n![Figure 7.10: Distance for traversing at each node ](img/C14498_07_10.jpg)\n\n###### 图 7.10:穿越每个节点的距离\n\n如果我们将原始边权重插入到它们所连接的节点的距离值之间，序列将如下:\n\n![Figure 7.11: Calculating the distance that's been traversed  ](img/C14498_07_11.jpg)\n\n###### 图 7.11:计算穿越的距离\n\n现在，让我们将以下公式应用于边值:\n\n```cpp\nW(uv) = w(uv) + d[s, u] - d[s, v]\n```\n\n这里，`w(uv)`表示节点`u`和`v`之间的原始边缘权重，`d[s, u]`和`d[s, v]`表示`S`和`u/v`之间的最短路径距离，`W(uv)`表示变换后的边缘权重值。应用此公式会产生以下结果:\n\n```cpp\nAB —> (-7) +   0  – (-7) = 0\nBC —> (-2) + (-7) – (-9) = 0\nCA —>  10  + (-9) –   0  = 1\nAD —> (-5) +   0  – (-5) = 0\nDE —>   4  + (-5) – (-1) = 0\n```\n\n请注意，在后续迭代中，表达式中的第三项总是被中间项抵消；这证明了公式的“伸缩”特性。由于这个性质，下面两个表示节点 A 和 E 之间距离的表达式是等价的:\n\n```cpp\n(w(AB) + d[s, A] - d[s, B]) + (w(BC) + d[s, B] - d[s, C]) + … + (w(DE) + d[s, D] - d[s, E])\n(w(AB) + w(BC) + w(CA) + w(AD) + w(DE)) + d[s, A] - d[s, E]\n```\n\n这意味着添加到图中任何路径的权重等于添加到其子路径的权重。我们知道，将这些值相加的结果总是非负的，因为贝尔曼-福特返回的距离数组确保我们对任何一对都有`d[s, u] + weight(u, v) >= d[s, v]``u,v`。因此，`w(u, v) + d[s, u] - d[s, v]`的值永远不能小于 0。\n\n作为所应用的变换的结果，图中任何最短路径将穿过的每条边将被重新加权为零，这给我们留下了非负权重值，非常值得注意的是，这些值仍然保留了它们原来的最短路径排序！我们现在可以使用这些新的权重值在图上执行 Dijkstra 算法，以有效地检索每对节点的最短路径。\n\n我们将在下面的练习中探索约翰逊算法的实现。\n\n### 练习 34:实现约翰逊算法\n\n在本练习中，我们将实现约翰逊算法，以找到负权重图中每个节点到每个其他节点的最短距离。让我们开始吧:\n\n1.  我们可以重用前面练习中的大部分代码，包括我们的`Edge`结构、`UNKNOWN`常量和图数据:\n\n    ```cpp\n    #include <iostream>\n    #include <vector>\n    #include <climits>\n    using namespace std;\n    struct Edge\n    {\n        int start;\n        int end;   \n        int weight;\n        Edge(int s, int e, int w) : start(s), end(e), weight(w) {}\n    };\n    const int UNKNOWN = INT_MAX;\n    vector<Edge*> edges;\n    int V;             \n    int E;             \n    ```\n\n2.  我们应该修改 Bellman-Ford 的函数声明，使其接受两个参数(一个整数，`V`，和一个向量或`Edge`指针，`edges`，并返回一个整数向量。我们也可以删除`start`参数:\n\n    ```cpp\n    vector<int> BellmanFord(int V, vector<Edge*> edges)\n    ```\n\n3.  我们将从向图中添加虚拟顶点`S`开始。因为`S`本质上对图的其余部分没有影响，所以这就像将距离数组的大小增加到 *| V + 1 |* 并在`S`和每隔一个节点\n\n    ```cpp\n    vector<int> distance(V + 1, UNKNOWN);\n    int s = V;\n    for(int i = 0; i < V; i++)\n    {\n        edges.push_back(new Edge(s, i, 0));\n    }\n    distance[s] = 0;\n    ```\n\n    之间添加一条边一样简单\n4.  我们继续将 Bellman-Ford 的标准实现应用于修改后的图，使用`S`作为源节点:\n\n    ```cpp\n    for(int i = 1; i < V; i++)\n    {\n        for(auto edge : edges)\n        {\n            int u = edge->start;\n            int v = edge->end;\n            int w = edge->weight;\n            if(distance[u] == UNKNOWN)\n            {\n                continue;\n            }\n            if(distance[u] + w < distance[v])\n            {\n                distance[v] = distance[u] + w;\n            }\n        }\n    }\n    ```\n\n5.  这一次，让我们将负周期的最终检查移动到它自己的功能中:\n\n    ```cpp\n    bool HasNegativeCycle(vector<int> distance, vector<Edge*> edges)\n    {\n        for(auto edge : edges)\n        {\n            int u = edge->start;\n            int v = edge->end;\n            int w = edge->weight;\n            if(distance[u] == UNKNOWN) continue;\n            if(distance[u] + w < distance[v])\n            {\n                return true;\n            }\n        }\n        return false;\n    }\n    ```\n\n6.  现在，我们可以在原始函数的末尾调用它，如果发现负循环，就返回一个空数组:\n\n    ```cpp\n    if(HasNegativeCycle(distance, edges))\n    {\n        cout << \"NEGATIVE CYCLE FOUND\" << endl;\n        return {};\n    }\n    ```\n\n7.  在确保图没有负循环之后，我们可以将距离值的结果集返回给调用函数，并将重新加权公式应用于图中的每条边。但是首先，让我们实现迪克斯特拉的算法:\n\n    ```cpp\n    vector<int> Dijkstra(int V, int start, vector<Edge*> edges)\n    ```\n\n8.  现在，让我们声明一个整数向量`distance`和一个布尔向量`visited`。和往常一样，`distance`的每个索引都会被初始化为`UNKNOWN`(除了起始顶点)，而`visited`的每个索引都会被初始化为假:\n\n    ```cpp\n    vector<int> distance(V, UNKNOWN);\n    vector<bool> visited(V, false);\n    distance[start] = 0;\n    ```\n\n9.  我们的迪克斯特拉算法的实现将利用一个简单的迭代方法，使用`for`循环。您可能会从前面的章节中回忆起，Dijkstra 的算法需要在遍历的每一步找到具有最小距离值的节点。虽然这通常是通过优先级队列来完成的，但是我们将通过编码另一个短函数`GetMinDistance()`来完成，该函数将距离和访问的数组作为参数，并返回具有最短路径值的节点的索引:\n\n    ```cpp\n    // Find vertex with shortest distance from current position and\n    // return its index\n    int GetMinDistance(vector<int> &distance, vector<bool> &visited)\n    {\n        int minDistance = UNKNOWN;\n        int result;\n        for(int v = 0; v < distance.size(); v++)\n        {            \n            if(!visited[v] && distance[v] <= minDistance)\n            {\n                minDistance = distance[v];\n                result = v;\n            }\n        }\n        return result;\n    }\n    ```\n\n10.  我们现在可以完成实现迪克斯特拉的算法:\n\n    ```cpp\n    for(int i = 0; i < V - 1; i++)\n    {\n        // Find index of unvisited node with shortest distance\n        int curr = GetMinDistance(distance, visited);\n        visited[curr] = true;\n        // Iterate through edges\n        for(auto edge : edges)\n        {\n            // Only consider neighboring nodes\n            if(edge->start != curr) continue;\n            // Disregard if already visited\n            if(visited[edge->end]) continue;\n            if(distance[curr] != UNKNOWN && distance[curr] + edge->weight < distance[edge->end])\n            {\n            distance[edge->end] = distance[curr] + edge->weight;\n            }\n        }\n    }\n    return distance;\n    ```\n\n11.  我们现在有了执行约翰逊算法所需的一切。让我们声明一个新的函数`Johnson()`，它也以`V`和`edges`为参数:\n\n    ```cpp\n    void Johnson(int V, vector<Edge*> edges)\n    ```\n\n12.  我们首先创建一个整数向量`h`，并将其设置为`BellmanFord()` :\n\n    ```cpp\n    // Get distance array from modified graph\n    vector<int> h = BellmanFord(V, edges);\n    ```\n\n    的输出\n13.  我们检查`h`是否为空。如果是，我们终止函数:\n\n    ```cpp\n    if(h.empty()) return; \n    ```\n\n14.  否则，我们应用重新加权公式:\n\n    ```cpp\n    for(int i = 0; i < edges.size(); i++)\n    {\n        edges[i]->weight += (h[edges[i]->start] - h[edges[i]->end]);\n    }\n    ```\n\n15.  为了存储每对节点的最短路径距离，我们用`V`行初始化一个矩阵(这样每对二维索引`[i, j]`代表顶点`i`和顶点`j`之间的最短路径)。然后我们执行对迪克斯特拉算法的`V`调用，该算法返回每个起始节点的`distance`数组:\n\n    ```cpp\n    // Create a matrix for storing distance values\n    vector<vector<int>> shortest(V);\n    // Retrieve shortest distances for each vertex\n    for(int i = 0; i < V; i++)\n    {\n        shortest[i] = Dijkstra(V, i, edges);\n    }\n    ```\n\n16.  不出所料，我们在这一步积累的结果相当不准确。由于我们的重新加权操作，每个距离值现在都是正值。然而，这可以通过对每个结果反向应用相同的公式来非常简单地纠正:\n\n    ```cpp\n    // Reweight again in reverse to get original values\n    for(int i = 0; i < V; i++)\n    {\n        cout << i << \":\\n\";\n        for(int j = 0; j < V; j++)\n        {\n            if(shortest[i][j] != UNKNOWN)\n            {\n                shortest[i][j] += h[j] - h[i];\n                cout << \"\\t\" << j << \": \" << shortest[i][j] << endl;\n            }\n        }\n    }\n    ```\n\n17.  现在，让我们回到我们的`main()`函数，实现处理输入的代码。在我们收集了输入图的边之后，我们只需要对`Johnson()`执行一次调用，我们的工作就完成了:\n\n    ```cpp\n    int main()\n    {\n        int V, E;\n        cin >> V >> E;\n        vector<Edge*> edges;\n        for(int i = 0; i < E; i++)\n        {\n            int node_a, node_b, weight;\n            cin >> node_a >> node_b >> weight;\n            edges.push_back(new Edge(node_a, node_b, weight));\n        }\n        Johnson(V, edges);\n        return 0;\n    }\n    ```\n\n18.  让我们使用以下输入来测试我们的算法:\n\n    ```cpp\n    7 9\n    0 1 3\n    1 2 5\n    1 3 10\n    1 5 -4\n    2 4 2\n    3 2 -7\n    4 1 -3\n    5 6 -8\n    6 0 12\n    ```\n\n19.  The output should be as follows:\n\n    ```cpp\n    0:\n        0: 0\n        1: 3\n        2: 6\n        3: 13\n        4: 8\n        5: -1\n        6: -9\n    1:\n        0: 0\n        1: 0\n        2: 3\n        3: 10\n        4: 5\n        5: -4\n        6: -12\n    2:\n        0: -1\n        1: -1\n        2: 0\n        3: 9\n        4: 2\n        5: -5\n        6: -13\n    4:\n        0: -3\n        1: -3\n        2: 0\n        3: 7\n        4: 0\n        5: -7\n        6: -15\n    5:\n        0: 4\n        1: 7\n        2: 10\n        3: 17\n        4: 12\n        5: 0\n        6: -8\n    6:\n        0: 12\n        1: 15\n        2: 18\n        3: 25\n        4: 20\n        5: 11\n        6: 0\n    ```\n\n    从前面的输出中可以看到，我们已经成功地打印了从每个节点到每个其他节点的最短距离。\n\n### 活动 16:随机化图统计\n\n你是一家知名软件公司的开发人员，该公司每年都会收到大量新的求职申请。因此，要求每个员工都参与技术面试过程。每次面试前，都会给你一套三个编程问题，每个问题包含一个简短的描述，以及两到三个难度不断增加的测试用例。\n\n最近有消息引起你的注意，一些受访者设法提前获得了某些面试问题的测试用例。因此，每隔几周，被调用的能力就会要求你创建新的测试用例集。为大多数问题生成像样的测试用例并不是特别有挑战性，除了关于图论的问题。您已经注意到，设计一个既有效又与问题相关的图表的过程可能有点耗时，因此您已经下定决心要自动化这个过程。\n\n贵公司使用的最常见的与图相关的面试问题是全对最短路径问题，该问题要求受访者在一个有加权边的有向图中找到每对顶点之间的最短距离。由于这个问题的性质，您希望生成器实用程序生成的图表有助于评估受访者对问题的理解。您已经决定，如果图表符合以下标准，它将对技术面试有用:\n\n*   它是一个有向图，可以包含正边权重和负边权重。\n*   任何一对节点之间都应该只有一条边，任何节点都不应该有自己的边。\n*   每个节点应该至少有一个传入或传出边缘。\n*   任何边权重的绝对值都应该小于 100。\n\n该实用程序应接受以下输入:\n\n*   `seed`:随机数生成的种子值\n*   `iterations`:要生成的图数量\n*   `V`:顶点数\n*   `E`:边数\n\n实用程序应该使用对`std::rand()`的调用来处理每个边的生成。如果它试图在同一对节点之间创建第二条边，它应该停止生成新边，直到找到有效的边对。\n\n图生成应如下进行:\n\n1.接收输入(`seed`、`iterations`、`V`和`E`)\n\n2.设置随机数生成器的种子值\n\n3 对于每次迭代，请执行以下操作:\n\n*   Set `i = 0`\n\n    -尝试通过对`rand()`执行三次调用来创建边，以便生成源节点、目标节点和边权重的值(按此顺序)。\n\n    -检查`rand()`生成的下一个值是否能被`3;`整除，如果能，则使边缘权重为负。\n\n*   If an edge between the source and destination nodes already exists, try again:\n\n    -将`edge(source, destination, weight)`添加到边集中，并增加`i`。\n\n    -如果在`E`边创建之后，有一个节点不是边的一部分，则该图被认为是无效的。\n\n如果生成的图是有效的，您应该找到图中每对节点之间的最短路径，就像我们在面试中所期望的那样。对于图中的每个节点，您希望找到其所有路径的平均最短距离(即距离值之和除以可到达的节点数)。图表的平均距离将被定义为这些值的平均值。\n\n您还对哪组值倾向于产生最多数量的“有趣”图感兴趣。当图的平均距离小于最高值边权重的一半时，你认为图是有趣的。因此，您的算法应该输出感兴趣的图与有效图总数的百分比(四舍五入到两位小数)。请注意，出于这一特殊目的，您认为具有负权重循环的连通图是有效的，但并不有趣。\n\n**输入格式**\n\n一行包含四个整数；即分别为`seed`、`iterations`、`V`和`E`。\n\n**输出格式**\n\n两行，第一行包含`INVALID:` 字符串，后面是无效图的数量，第二行包含`PERCENT INTERESTING:` 字符串，后面是感兴趣图与有效图的比率，显示为四舍五入到两位小数的百分比。\n\n**活动指南**\n\n对`std::rand()`的调用不一定会在每个环境中产生相同的值。为了确保一致性，您可以将以下代码复制/粘贴到您的程序中(取自 C 标准):\n\n```cpp\nstatic unsigned long int randNext = 1;\nint rand(void) // RAND_MAX assumed to be 32767\n{\n    randNext = randNext * 1103515245 + 12345;\n    return (unsigned int)(randNext/65536) % 32768\n}\nvoid srand(unsigned int seed)\n{\n    randNext = seed;\n}\n```\n\n实现图生成实用程序时，请确保按照问题描述中描述的确切顺序执行这些步骤。\n\n**测试用例**\n\n以下是一些示例输入和输出，应该可以帮助您更好地理解问题:\n\n![Figure 7.12: Test cases for Activity 16 ](img/C14498_07_12.jpg)\n\n###### 图 7.12:活动 16 的测试用例\n\n#### 注意\n\n这个活动的解决方案可以在第 541 页找到。\n\n## 强连通分量\n\n在前几章中，我们讨论了图的几种分类。描述一个图的特征的最常见的方法是说明它是有向的还是无向的。后者定义了默认情况下边是双向的图(如果节点 A 有一条边连接到节点 B，则节点 B 有一条边连接到节点 A)，而前者描述了边朝向特定“方向”的图。\n\n假设你是一家视频托管网站的员工，负责统计各种渠道的用户之间的共性。贵公司特别感兴趣的是发现订阅某些频道的个人和频道各自所有者的订阅之间的模式，希望更深入地了解他们的定向广告服务应该如何定向。你的公司提供的服务最近变得相当广泛，所以你需要一种方法来组织相关的数据，以一种足够清晰的方式来产生有用的统计信息。\n\n让我们将网站每个用户的频道可视化为有向图中的节点，它们之间的邻接关系代表他们订阅的另一个频道的各自所有者。我们可能会注意到，即使是在共享同一频道订阅的大量用户群体中，他们的所有单个订阅集的多样性也将使我们发现它们之间任何明显相似之处的能力变得非常复杂。理想情况下，我们希望解开图中大量杂乱的连接，并将数据放入不同的组中，在这些组中，每个用户的订阅都与其他用户的订阅有某种关联。\n\n我们可以通过观察有向图共有的某些特征来解开这个特殊问题的复杂性。因为有向图的边不能保证是双向的，所以我们可以从逻辑上得出结论，对图的某些部分的访问可能会受到限制，这取决于您从哪个节点开始遍历。如果将一个图分成不同的集合，使得同一集合中的任意一对顶点之间都有一条连接路径，则得到的组将代表该图的强连通分量。\n\n### 有向图和无向图的连通性\n\n无向图的连通分支可以被描述为包含主图的最大尺寸子图的集合，其中同一组中的每个节点都“连接”到其他节点(也就是说，单个分支中任意两个节点之间的访问不受限制)。在连通图中，无论遍历从哪里开始，每个节点都可以到达，因此我们可以推导出这样的图由单个连通分量(整个图)组成。相反，任何限制从一点到另一点的访问的图都被描述为断开的。\n\n另一方面，所谓的“强”连通性是有向图独有的特征。要比较理解“强连通性”的定义差异，请观察无向图的以下示例:\n\n![](img/C14498_07_13.jpg)\n\n###### 图 7.13:具有不同连接组件的图表\n\n三个彩色子图各自代表一个独立的连接组件。如前所述，它们的连通性是由这样一个事实定义的，即每个顶点都有一条路径将其连接到同一组中的其他顶点。此外，一个组件的顶点没有连接到另一个组件的路径。从上图中，我们可以看到无向图的连通分支被分成明显独立的组，其中任何分支的节点集和边集都与其他分支完全分离。\n\n相比之下，强连接的组件不需要与图中的其他组件完全隔离，也就是说，组件之间可能存在重叠的路径:\n\n![Figure 7.14: Graph with different strongly connected components ](img/C14498_07_14.jpg)\n\n###### 图 7.14:具有不同强连接组件的图\n\n在上图中，我们可以看到有四个强连接的组件: *A* 、 *B* 、 *CEFG* 、 *DHI* 。注意节点 *A* 和 *B* 是各自集合中唯一的成员。通过对节点 *A* 的进一步研究，我们可以看到虽然 *A* 有一条通往 *DHI* 集合中每个节点的路径，但是 *DHI* 集合中没有一个节点有任何通往节点 *A* 的路径。\n\n回到我们的视频托管网站示例，我们可以将网络图的强连接组件定义为组，在这些组中，通过导航与同一组中其他用户的频道相关联的订阅的“路径”，可以找到每个频道。以这种方式分解潜在的大量数据可能有助于将相关的图关系集与那些没有明显相似之处的图关系集隔离开来:\n\n![Figure 7.15: Example dataset represented as a graph with different strongly connected components ](img/C14498_07_15.jpg)\n\n###### 图 7.15:示例数据集表示为具有不同强连接组件的图\n\n## 小泽一郎算法\n\n寻找图的强连通分支的最常见且概念上容易掌握的方法之一是 Kosaraju 算法。Kosaraju 的算法通过执行两组独立的 DFS 遍历来工作，首先探索原始形式的图，然后对其转置进行同样的操作。\n\n#### 注意\n\n虽然 DFS 是 Kosaraju 算法中典型使用的遍历类型，但 BFS 也是一个可行的选择。然而，对于本章包含的解释和练习，我们将坚持传统的基于 DFS 的方法。\n\n图的转置基本上与原始图相同，除了其每个边中的源/目标顶点被交换(即，如果原始图中存在从节点 *A* 到节点 *B* 的边，转置图将具有从节点 *B* 到节点 *A* 的边):\n\n![Figure 7.16: Transpose of a graph ](img/C14498_07_16.jpg)\n\n###### 图 7.16:图的转置\n\n算法的第一步(初始化后)是遍历图的顶点并执行 DFS 遍历，从上一次遍历中尚未访问的每个节点开始。在 DFS 中每个点的开始，当前节点被标记为已访问，然后探索其所有未访问的邻居。在研究了每个当前节点的邻接之后，在当前递归子树终止之前，它被添加到栈的顶部。\n\n在探索原始图中的每个顶点后，从栈顶部弹出的每个未访问节点开始，对其转置进行同样的操作。此时，在具有唯一起始点的每个后续 DFS 遍历期间遇到的节点集代表图的强连通部分。\n\n就如何直观地简化一个潜在的复杂问题而言，Kosaraju 的算法相当有效，将它简化为一个相当简单的实现。另外，假设输入图具有邻接表表示，它也是相当有效的，因为它具有线性渐近复杂度 *O(V + E)* 。\n\n#### 注意\n\n不建议在该算法中使用邻接矩阵，因为在遍历中寻找每个顶点的邻居需要大量的额外迭代。\n\n我们将在下面的练习中研究 Kosarju 算法的实现。\n\n### 练习 35:实现小泽一郎的算法\n\n在本练习中，我们将使用 Kosaraju 的算法找到图中的强连通分量。让我们开始吧:\n\n1.  为了实现 Kosaraju 的算法，我们需要包含以下标题:\n\n    ```cpp\n    #include <iostream>\n    #include <vector>\n    #include <stack>\n    ```\n\n2.  让我们定义一个名为`Kosaraju()`的函数，该函数接受两个参数——一个整数，`V`(顶点数)和一个整数向量的向量，`adj`(图的邻接表表示)——并返回一个整数向量的向量，该向量表示输入图的每个强连通分量中的节点索引集合:\n\n    ```cpp\n    vector<vector<int>> Kosaraju(int V, vector<vector<int>> adj)\n    ```\n\n3.  我们的第一步是声明我们的栈容器和访问数组(每个索引初始化为`false`)。然后，我们遍历图的每个节点，从每个尚未标记为`visited` :\n\n    ```cpp\n    vector<bool> visited(V, false);\n    stack<int> stack;\n    for(int i = 0; i < V; i++)\n    {\n        if(!visited[i])    \n        {\n            FillStack(i, visited, adj, stack);\n        }\n    }\n    ```\n\n    的索引开始我们的 DFS 遍历\n4.  我们的第一个 DFS 函数`FillStack()`采用四个参数:一个整数节点(遍历中当前点的顶点索引)，一个名为`visited`的布尔向量(先前遍历的节点集)，以及两个整数向量`adj`(图的邻接表)和`stack`(访问节点索引列表，根据它们被探索的时间排序)。最后三个参数将通过调用函数的引用传递。DFS 以标准方式实现，除了在每个函数调用结束时将当前节点的索引推送到栈的附加步骤:\n\n    ```cpp\n    void FillStack(int node, vector<bool> &visited,\n    vector<vector<int>> &adj, stack<int> &stack)\n    {\n        visited[node] = true;\n        for(auto next : adj[node])\n        {\n            if(!visited[next])\n            {\n                FillStack(next, visited, adj, stack);\n            }\n        }\n        stack.push(node);\n    }\n    ```\n\n5.  现在，让我们定义另一个函数`Transpose()`，它以原始图的参数为参数，返回其转置的邻接表:\n\n    ```cpp\n    vector<vector<int>> Transpose(int V, vector<vector<int>> adj)\n    {\n        vector<vector<int>> transpose(V);\n        for(int i = 0; i < V; i++)\n        {\n            for(auto next : adj[i])\n            {\n                transpose[next].push_back(i);\n            }\n        }\n        return transpose;\n    }\n    ```\n\n6.  为了准备下一组遍历，我们声明邻接表转置(初始化为我们的`Transpose()`函数的输出)，并将我们访问的数组重新初始化为`false` :\n\n    ```cpp\n        vector<vector<int>> transpose = Transpose(V, adj);\n\n        fill(visited.begin(), visited.end(), false);\n    ```\n\n7.  对于算法的后半部分，我们将需要定义我们的第二个 DFS 函数`CollectConnectedComponents()`，它采用与`FillStack()`相同的参数，只是第四个参数现在被替换为对整数向量分量的引用。这个向量分量是我们存储图中每个强连通分量的节点索引的地方。遍历的实现也几乎与`FillStack()`函数相同，除了我们删除了将节点推送到栈的行。取而代之的是，我们在函数的开头包含一行，用于收集组件向量中遍历的节点:\n\n    ```cpp\n    void CollectConnectedComponents(int node, vector<bool> &visited,\n    vector<vector<int>> &adj, vector<int> &component)\n    {\n        visited[node] = true;\n        component.push_back(node);\n        for(auto next : adj[node])\n        {\n            if(!visited[next])\n            {\n                CollectConnectedComponents(next, visited, adj, component);\n            }\n        }\n    }\n    ```\n\n8.  回到我们的`Kosaraju()`函数，我们定义一个称为`connectedComponents`的整数向量向量，在这里我们将存储我们对转置执行的每个遍历的结果。然后，我们在一个`while`循环中迭代地从栈中弹出元素，再次从未访问的节点开始每次 DFS 遍历。在每次调用 DFS 函数之前，我们声明`CollectConnectedComponents()`引用的分量向量，然后在遍历完成后将其推送到`connectedComponents`。栈空时算法完成，之后返回`connectedComponents` :\n\n    ```cpp\n    vector<vector<int>> connectedComponents;\n    while(!stack.empty())\n    {\n        int node = stack.top();\n        stack.pop();\n        if(!visited[node])\n        {\n            vector<int> component;\n            CollectConnectedComponents(node, visited, transpose, component);\n            connectedComponents.push_back(component);\n        }\n    }\n    return connectedComponents;\n    ```\n\n9.  从我们的`main()`函数中，我们现在可以通过在单独的行上打印每个向量的值来输出每个强连通分量的结果:\n\n    ```cpp\n    int main()\n    {\n        int V;\n        vector<vector<int>> adj;\n        auto connectedComponents = Kosaraju(V, adj);\n        cout << \"Graph contains \" << connectedComponents.size() << \" strongly connected components.\" << endl;\n        for(auto component : connectedComponents)\n        {\n            cout << \"\\t\";\n            for(auto node : component)\n            {\n                cout << node << \" \";\n            }\n            cout << endl;\n        }\n    }\n    ```\n\n10.  To test the functionality of our newly implemented algorithm, let's create an adjacency list representation based on the following graph:\n\n    ![Figure 7.17: Graphical representation of sample input data ](img/C14498_07_17.jpg)\n\n    ###### 图 7.17:样本输入数据的图表示\n\n11.  在`main()`中，`V`和`adj`的定义如下:\n\n    ```cpp\n    int V = 9;\n    vector<vector<int>> adj =\n    {\n        { 1, 3 },\n        { 2, 4 },\n        { 3, 5 },\n        { 7 },\n        { 2 },\n        { 4, 6 },\n        { 7, 2 },\n        { 8 },\n        { 3 } \n    };\n    ```\n\n12.  执行我们的程序时，应显示以下输出:\n\n    ```cpp\n    Graph contains 4 strongly connected components.\n        0 \n        1 \n        2 4 5 6 \n        3 8 7\n    ```\n\n### 活动 17:迷宫-传送游戏\n\n你正在设计一个游戏，其中多个玩家被随机放置在迷宫般的房间里。每个房间都包含一个或多个传送设备，玩家可以使用它们在迷宫的不同部分之间旅行。每个传送点都有一个相关的值，这个值会被添加到任何使用它的玩家的分数中。玩家轮流轮流穿越迷宫，直到每个房间都至少被参观一次，此时回合结束，得分最低的玩家获胜。\n\n你已经实现了一个系统，在每个游戏开始的时候，这个系统都会按程序生成一个新的迷宫。不幸的是，你最近发现一些生成的迷宫包含循环，玩家可以使用这些循环来无休止地降低他们的分数。你还注意到，玩家经常有不公平的优势，这取决于他们开始的房间。最糟糕的是，传送点经常以这样一种方式分散，玩家最终可能会在整个回合中与迷宫的其他部分隔绝。\n\n您希望实现一个测试过程，以确保生成的迷宫是公平和适当平衡的。你的测试应该首先确定迷宫是否包含一条可以用来不断降低玩家分数的路径。如果是，应该输出`INVALID MAZE`。如果迷宫有效，你应该从每个起点找到可以达到的最低分数并上报(或者`DEAD END`，在房间没有传送点的情况下)。\n\n此外，你想防止被困在迷宫某个特定区域的可能性，所以你的测试也应该输出玩家无法进入迷宫其他部分的任何房间组。\n\n**预期输入**\n\n每个测试都应该接收以下输入:\n\n*   迷宫中的房间数量\n*   迷宫中传送点的数量\n*   源房间、目的房间以及与每个传送点相关的点数\n\n**预期输出**\n\n对于每一个测试，程序首先要确定迷宫中是否有可以用来无限降低玩家分数的路径。如果是，应该打印一行:`INVALID MAZE`。\n\n如果迷宫有效，你的程序应该输出能达到的最低分，从每个房间开始(或者`DEAD END`，如果房间没有传送点的话)，假设至少移动一次，整个迷宫只能穿越一次。最后，你的程序应该列出玩家可以“卡住”的任何房间组(也就是说，他们完全被限制进入迷宫的其他部分)；对于每一个这样的组，你的程序应该在单独的行上打印每个组中所有房间的索引。\n\n**样本输入和输出**\n\n以下是一些示例输入，可以帮助您更好地理解这个问题:\n\n![Figure 7.18: Test case 1 for Activity 17 ](img/C14498_07_18.jpg)\n\n###### 图 7.18:活动 17 的测试用例 1\n\n![Figure 7.19: Test case 2 for Activity 17 ](img/C14498_07_19.jpg)\n\n###### 图 7.19:活动 17 的测试用例 2\n\n![Figure 7.20: Test case 3 for Activity 17 ](img/C14498_07_20.jpg)\n\n###### 图 7.20:活动 17 的测试用例 3\n\n![Figure 7.21: Test case 4 for Activity 17 ](img/C14498_07_21.jpg)\n\n###### 图 7.21:活动 17 的测试用例 4\n\n![Figure 7.22: Test case 5 for Activity 17 ](img/C14498_07_22.jpg)\n\n###### 图 7.22:活动 17 的测试用例 5\n\n![Figure 7.23: Test case 6 for Activity 3 ](img/C14498_07_23a.jpg)\n\n![Figure 7.23: Test case 6 for Activity 17 ](img/C14498_07_23b.jpg)\n\n###### 图 7.23:活动 17 的测试用例 6\n\n![Figure 7.24: Test case 7 for Activity 17 ](img/C14498_07_24.jpg)\n\n###### 图 7.24:活动 17 的测试用例 7\n\n**活动指南**\n\n*   不要被无关的信息分散注意力。问问自己具体需要完成什么。\n*   问题的第一个条件(确定迷宫是否包含可以无限降低我们分数的路径)也可以表示为:如果迷宫表示为一个加权图，那么在任何产生负和的路径上是否存在循环？显然，这是一个我们完全有能力解决的问题！您可能还认识到第二个条件(找到玩家从给定点开始可以获得的最低分数)与第一个条件密切相关。\n*   最后一个条件有点挑战性。根据我们在本章中讨论的图术语，考虑如何重新定义“被困”在迷宫的某个部分。有这个属性的迷宫会是什么样子？\n*   Consider drawing one or several of the input graphs on paper. What characterizes the groups of rooms in which a player can get stuck?\n\n    #### 注意\n\n    这个活动的解决方案可以在第 550 页找到。\n\n## 选择正确的方法\n\n到目前为止，很可能很明显，很少有一种“完美”的方法来实现图结构。我们所代表的数据的特征，加上我们试图解决的问题的细节，可能会使某些方法不合理地低效，尽管事实上它们在不同的条件下可能是完全可以接受的。\n\n每当你试图确定是否使用邻接表对矩阵，类/结构对简单数组，贝尔曼-福特对约翰逊算法，BFS 对 DFS 等等，最终的决定应该主要取决于数据的细节和你打算如何使用它。例如，如果你想找到图中每对节点之间的最短距离，约翰逊算法将是一个很好的选择。然而，如果您只需要偶尔找到单个起始节点的最短距离，约翰逊算法将执行相当多的不必要的工作，而对贝尔曼-福特的一次调用就足够了。\n\n尝试使用不同形式的图表示来编写我们在本章中讨论的每个算法是一个有益的练习。例如，贝尔曼-福特可以通过用邻接表和二维边权重矩阵替换我们在第一个练习中使用的`Edge`指针的向量来轻松实现。在某些情况下，一个实现提供的效率潜力可能只比另一个略好；在其他时候，差异可能相当大。然后，有时候，某种方法的价值更多的是与简单性和可读性有关，而不是任何可衡量的性能基准。比较各种算法的性能如何在不同的数据集和场景中扩展可以提供非常丰富的信息，并且通常是现实世界开发中的基本实践。\n\n在您努力加深对图论及其实现的理解的过程中，我们提供以下建议:\n\n*   抵制使用“复制粘贴”方法实现新算法的冲动。如果你不理解算法工作背后的基本原理，你就很有可能错误地使用它。此外，即使它按照您希望的方式运行，也要记住图实现是高度特定于上下文的，这一点很重要。盲目使用任何算法意味着您将缺乏在不同参数集之间扩展解决方案功能所必需的理解。\n*   当将新的概念付诸实践时，避免完全依赖抽象的、非上下文的实现。在纯理论数据上使用某种算法后，尝试修改它以适合某种实际数据模型(即使该数据本身是假设的)。想象真实的场景，在其中你可以使用你新获得的算法知识，这将增加你知道何时以及如何在工作中使用它的概率。\n\n在您真正考虑以下事项之前，避免实现您的图表:\n\n*   它的基本目的和实现该目的所需的基本功能(即，它描述的数据、它需要执行的查询类型、它需要有多动态等等)\n*   它需要表示与问题相关的信息的最基本的组件\n\n未能评估这些关键想法可能会导致混乱和过于冗长的代码，充斥着不必要的数据和函数，对实际的解决方案毫无价值。在编写任何代码之前，规划好图表的必要组成部分，可能会为您节省大量的混乱和繁琐的重构工作。\n\n最终，发展对图编程的全面理解是一项技能，它远远超出了简单学习所有正确算法的范围。一个简单的网络搜索与任何非琐碎的绘图问题相关，将导致大量深入分析的研究文章，对不同方法的比较评估，以及尚未发现合理实现的推测解决方案。一如既往，一致的实践是掌握任何编程技能的最佳方法；而图论作为一门庞大而充满活力的学科，当然也不例外！\n\n## 总结\n\n到目前为止，我们已经相当详细地介绍了图表。现在，您应该对图论在软件开发中的一些基本用途有了一个坚实的理解，并且对基于图的解决方案如何被用来封装复杂的数据，使我们能够相对容易地查询和操作它有了一个了解。在*第 6 章*、*图算法 I* 中学习了图结构和遍历的基础，然后在本章中扩展它们来解决更高级的问题，现在您应该已经准备好在未来探索更深入的图实现，因为这些基本概念是它们的核心。\n\n虽然这一章没有完全结束我们对本书中图算法的讨论，但我们现在将从图中休息一下，来探索现代开发人员技能中最强大、最具挑战性的编程技术之一。像图算法一样，我们接下来要讨论的主题是如此广泛和概念抽象，以至于它将跨越两个独立的章节。然而，由于它的有用性(以及它的难度)，它是许多软件公司在技术面试时的最爱。"
  },
  {
    "path": "docs/cpp-dsal-design-principle/08.md",
    "content": "# 八、动态规划一\n\n## 学习目标\n\n本章结束时，您将能够:\n\n*   分析动态规划方法是否可以应用于给定的问题\n*   比较并选择记忆和列表的正确方法\n*   使用记忆选择合适的缓存解决方案\n*   使用简单的暴力方法分析问题\n*   通过实施逐步优化的算法来开发动态编程解决方案\n\n在本章中，将向您介绍动态编程方法。本章将指导您实现这种方法来解决计算机科学中一些众所周知的问题。\n\n## 简介\n\n受到许多程序员同样程度的喜爱和恐惧，**动态编程** ( **DP** )是各个击破范式的概念扩展，属于特定的问题类别。动态编程问题涉及的困难是多方面的，通常需要创造力、耐心和可视化抽象概念的能力。然而，这些问题带来的挑战往往有优雅而令人惊讶的简单解决方案，这可以为程序员提供远远超出当前任务范围的见解。\n\n在前一章中，我们讨论了几种技术，例如各个击破和贪婪方法。这些方法虽然在正确的情况下相当有效，但在某些情况下不会产生最佳结果。例如，在前一章中，我们讨论了 Dijkstra 算法如何不能为具有负边权重的图产生最优结果，而 Bellman-Ford 算法却产生了最优结果。对于可以递归解决但无法使用上述技术解决的问题，动态规划解决方案通常可能是最好的方法。\n\nDP 问题也在各种各样的情况下遇到。这里只是几个广泛的例子:\n\n*   组合学(计算符合特定标准的序列组合/排列的数量)\n*   字符串/数组(编辑距离、最长公共子序列、最长递增子序列等)\n*   图(最短路径问题)\n*   机器学习(语音/人脸识别)\n\n让我们从了解动态编程的基本思想开始。\n\n## 什么是动态规划？\n\n回答这个问题的最好方法是举个例子。为了说明动态编程的目的，让我们考虑斐波那契数列:\n\n```cpp\n{ 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, … }\n```\n\n通过观察前面的序列，我们可以看到，从第三个元素开始，每个项都等于前面两个项的总和。这可以简单地用下面的公式表示:\n\n```cpp\nF(0) = 0\nF(1) = 1\n…\nF(n) = F(n-1) + F(n-2)\n```\n\n我们可以清楚地看到，这个序列的项具有递归关系——当前项 *F(n)* 是基于之前项 *F(n-1)* 和 *F(n-2)* 的结果，因此前面的等式，即 *F(n) = F(n-1) + F(n-2)* ，被描述为序列的**递归关系**。初始术语 *F(0)* 和 *F(1)* 被描述为**基本情况**，或者产生溶液而不需要进一步递归的点。这些操作如下图所示:\n\n![Figure 8.1: Computing the nth term in the Fibonacci sequence](img/C14498_08_01.jpg)\n\n###### 图 8.1:计算斐波那契数列中的第 n 项\n\n用英语描述前面的图可能是这样的:\n\n```cpp\nF5 is equal to: \n    F4 + F3, where F4 is equal to:\n    .    F3 + F2, where F3 is equal to:\n    .    .    F2 + F1, where F2 is equal to:\n    .    .    .    F1 + F0, where F1 = 1 and F0 = 0.\n    .    .    …and F1 = 1.\n    .    …and F2 is equal to:\n    .        F1 + F0, where F1 = 1 and F0 = 0.\n    …and F3 is equal to:\n        F2 + F1, where F2 is equal to:\n        .    F1 + F0, where F1 = 1 and F0 = 0\n        …and F1 = 1.\n```\n\n我们将前面的方法描述为**自顶向下的解决方案**，因为它从递归树(即解决方案)的顶部开始，向下遍历其分支，直到到达基本用例。在 C++ 中，这可以用下面的递归函数来编写:\n\n```cpp\n    int Fibonacci(int n)\n    {\n        if(n < 2)\n        {\n            return n;\n        }\n        return Fibonacci(n – 1) + Fibonacci(n – 2);\n    }\n```\n\n通过进一步观察树，我们可以看到几个**子问题**，或者说为了找到最终解而必须解决的中间问题，必须解决不止一次。比如 *F(2)* 的解必须找到才能得到*F(4)【F(3)+F(2)】*和*F(3)【F(2)+F(1)】*的解。因此，据说斐波那契数列表现出一种称为**重叠子问题**的性质。这是将标准的分治问题与动态编程问题分开的决定性特征之一；在前一种情况下，子问题往往是唯一的，而在后一种情况下，相同的子问题必须重复求解。\n\n我们还可以看到，几个解分支彼此完全相同。例如，寻找 *F(2)* 的解将需要相同的一组计算，而不管你是需要它来求解 *F(4)* 还是 *F(3)* 。这说明了动态规划问题的第二个定义特征，即最优子结构。当整个问题的最优解可以通过其子问题的最优解的某种组合而形成时，一个问题被称为表现出**最优子结构**。\n\n要用动态规划解决一个问题，它必须具备这两个性质。由于子问题的重叠性质，随着输入的增加，这些问题的复杂性趋于指数增长；然而，利用最佳子结构特性可以显著降低复杂性。因此，本质上，DP 的目的是设计一种缓存先前解决方案的方法，以避免重复计算先前解决的子问题。\n\n## 记忆化——自上而下的方法\n\n不，这不是“记忆”，尽管这也能相当准确地描述这种技术。使用记忆化，我们可以重新制定我们之前描述的自顶向下的解决方案，以利用斐波那契序列展示的最佳子结构属性。我们的程序逻辑将与以前基本相同，只是现在，在每一步都找到解决方案后，我们将结果缓存在一个数组中，根据 *n* 的当前值进行索引(在这个问题中， *n* 代表**状态**或定义当前递归分支的一组参数)。在每次函数调用的最开始，我们将检查缓存中是否有状态 *F(n)* 的可用解决方案。如果是这样，我们将简单地返回缓存值:\n\n```cpp\nconst int UNKNOWN = -1;\nconst int MAX_SIZE = 100000;\nvector<int> memo(MAX_SIZE, UNKNOWN);\nint Fibonacci(int n)\n{\n    if(n < 2)\n    {\n        return n;\n    }\n    if(memo[n] != UNKNOWN)\n    {\n        return memo[n];\n    }\n    int result = Fibonacci(n - 1) + Fibonacci(n - 2);\n    memo[n] = result;\n    return result;\n}\n```\n\n递归树现在如下所示:\n\n![Figure 8.2: Computing the nth term in the Fibonacci sequence using cached solutions](img/C14498_08_02.jpg)\n\n###### 图 8.2:使用缓存的解计算斐波那契序列中的第 n 项\n\n通过这样做，我们已经消除了相当多的多余工作。这种以自上而下的方式递归缓存解决方案的技术被称为**记忆**，并且基本上可以用于任何 DP 问题，假设以下情况成立:\n\n1.  您可以设计一个缓存方案，利用不同状态的相似性，同时保留它们的唯一性。\n2.  在超出可用的栈空间之前，您可以积累必要的子问题的解决方案。\n\n第一点意味着为以后使用的结果编制索引的方法应该既有效又有用。为了使缓存方案有效，它必须仅被认为是状态的匹配，这些状态的解是从相同系列的子问题导出的；为了使它有用，它不能是特定于状态的，以至于不能被有效地使用(例如，如果每个子问题在缓存中都被分配了一个唯一的索引，条件“`if(memo[KEY] != UNKNOWN)`”将永远不会为真)。\n\n第二点指的是导致栈溢出错误的可能性，如果递归调用的数量可能非常高，这是任何自顶向下方法的基本限制。当程序超过调用栈上可用的分配内存量时，就会发生栈溢出。根据给定问题的性质，所需的递归深度可能会阻止记忆成为可行的选择；一如既往，在选择方法之前评估手头任务的潜在复杂性是非常有益的。\n\n记忆化通常是一种不错的动态规划优化方法。然而，在许多情况下，有更好的选择，我们将在下一节中研究。\n\n## 列表-自下而上的方法\n\n动态规划的*核心*是制表，这是记忆的逆方法。事实上，虽然术语*动态编程*有时被用于记忆和制表，但它的使用通常被认为是专指后者。\n\n制表的标准实现包括存储基本情况的解决方案，然后用每个子问题的解决方案迭代填充一个表，然后可以重用该表来寻找其他子问题的解决方案。列表式解决方案通常被认为比记忆式解决方案更难概念化，因为每个子问题的状态必须以一种可以迭代表达的方式来表示。\n\n计算斐波那契数列的列表解决方案如下:\n\n```cpp\nint Fibonacci(int n)\n{\n        vector<int> DP(n + 1, 0);\n        DP[1] = 1;\n        for(int i = 2; i <= n; i++)\n        {\n            DP[i] = DP[i-1] + DP[i-2];\n        }\n        return DP[n];\n} \n```\n\n在斐波那契例子中，状态非常简单，因为它是一维的和无条件的——公式总是认为，对于大于 *1* 的任何 n， *F(n) = F(n-1) + F(n-2)* 。但是，动态规划问题通常包含定义给定状态的几个维度，并且可能有多个条件影响状态之间的转换。在这种情况下，除了全面理解问题之外，确定如何表示当前状态可能还需要相当多的创造力。\n\n然而，列表的优势是显著的。除了列表式解决方案往往在内存方面更高效这一事实之外，它们还会生成包含每个给定状态的完整查找表。因此，如果您可能会收到关于问题任何状态的查询，列表可能是您的最佳选择。\n\n有趣的是，任何可以用记忆解决的问题，理论上都可以重新表述为列表式解决方案，反之亦然。使用前者通常可以为如何接近后者提供巨大的洞察力。在接下来的几节中，我们将探索动态编程问题的几个经典示例，并演示使用多种方法(从简单的暴力开始)如何让您达到表格解决方案所需的理解水平。\n\n## 子集和问题\n\n假设您正在实现数字收银机的逻辑。每当客户需要零钱时，您希望显示一条消息，告诉收银员当前在收银机中的钱是否可以以某种方式组合，以便其总和等于所需的零钱金额。例如，如果一个产品花费 7.50 美元，客户支付 10.00 美元，消息将报告收银机中的钱是否可以用来产生正好 2.50 美元的零钱。\n\n假设登记簿目前包含十个 25 分硬币(10 x 0.25 美元)、四个一角硬币(4 x 0.10 美元)和六个五分硬币(6 x 0.05 美元)。我们可以很容易地得出结论，2.50 美元的目标金额可以通过以下方式形成:\n\n```cpp\n10 quarters                    -> $2.50\n9 quarters, 2 dimes, 1 nickel  -> $2.25 + $0.20 + $0.05\n9 quarters, 1 dime,  3 nickels -> $2.25 + $0.10 + $0.15\n9 quarters, 5 nickels          -> $2.25 + $0.25\n8 quarters, 4 dimes, 2 nickels -> $2.00 + $0.40 + $0.10\n8 quarters, 3 dimes, 4 nickels -> $2.00 + $0.30 + $0.20\n8 quarters, 2 dimes, 6 nickels -> $2.00 + $0.20 + $0.30\n```\n\n有了这些参数，问题就变得相当简单了，只需尝试所有可用的货币组合，直到找到与 2.50 美元匹配的金额，就可以解决这个问题。但是，如果需要的零钱是 337.81 美元，并且收银机包含 100 张钞票/硬币，分为 20 美元、10 美元、5 美元、1 美元、0.25 美元、0.10 美元、0.05 美元和 0.01 美元的面额，该怎么办？我们可以清楚地看到，随着复杂性的增加，尝试每一个可能的总和变得非常不切实际。这是一个被称为子集和问题的经典问题的例子。\n\n**子集和问题**最基本的形式是问:给定一组非负整数`S`和一个整数`x`，是否有一个`S`元素的子集和等于`x`？看看下面的例子:\n\n```cpp\nS = { 13, 79, 45, 29 }\nx = 42 —> True (13 + 29)\nx = 25 —> False \n```\n\n以前面的集合为例，我们可以找到以下 16 个子集:\n\n```cpp\n{ }\n{ 13 }\n{ 79 }\n{ 45 }\n{ 29 }\n{ 13, 79 }\n{ 13, 45 }\n{ 13, 29 }\n{ 79, 45 }\n{ 79, 29 }\n{ 45, 29 }\n{ 13, 79, 45 }\n{ 13, 79, 29 }\n{ 13, 45, 29 }\n{ 79, 45, 29 }\n{ 13, 79, 45, 29 }\n```\n\n通过列出可以为不同大小的集合生成的子集总数，我们得到以下数字:\n\n```cpp\n0: 1\n1: 2\n2: 4\n3: 8\n4: 16\n5: 32\n6: 64\n7: 128\n…\n```\n\n从这个列表中，我们可以推导出从一组大小`n`可以形成的子集总数等于*2*T3】n，这证明了要考虑的子集数量随着 *n* 的大小呈指数增长。假设 *S* 中的元素数量很少，比如说 10 个或更少的元素，这个问题的强力方法可以很快找到解决方案；但是如果我们重新考虑包含 100 种不同纸币/硬币的收银机的例子， *S* 的大小将等于 100，这将需要探索 1，267，650，600，228，229，401，496，703，205，376 个子集！\n\n### 解决子集和问题–步骤 1:评估数据保护的需求\n\n当面临这样的问题时，我们的第一步是确定它是否可以(和/或应该)用 DP 解决。重申一下，如果 DP 具有以下特性，问题是可以解决的:\n\n*   **重叠子问题**:和标准的各个击破法一样，通过某种方式将较小子问题的解进行组合，就可以推导出最终的解；然而，与分而治之相反，某些子问题会遇到多次。\n*   **最优子结构**:给定问题的最优解可以由其子问题的最优解产生。\n\n让我们从是否具备这些特征的角度来分析前面的例子:\n\n![Figure 8.3: Optimal substructure and overlapping subproblems ](img/C14498_08_03.jpg)\n\n###### '\n\n###### 图 8.3:最优子结构和重叠子问题\n\n如图所示，重新格式化子集集合清楚地说明了大小为 n 的每个新子集是如何通过向大小为`n - 1`的子集添加单个新元素而形成的。这是构建新子集的最佳方法，对于大小大于 0 的每个子集都适用。因此，子集和问题有一个**最优子结构**。我们还可以看到，几个子集都是由同一个“子子集”派生而来的(例如 *{ 13 79 45 }* 和 *{ 13 79 29 }* 都是基于 *{ 13 79 }* )。所以问题也有**重叠子问题**。\n\n满足这两个标准后，我们可以得出结论，这个问题可以用动态规划来解决。\n\n### 步骤 2–定义状态和基本案例\n\n在确定这是一个发展伙伴关系问题之后，我们现在必须确定在这个问题的背景下什么构成了一种状态。换句话说，就我们试图回答的问题而言，是什么让一个可能的解决方案与另一个不同？\n\n虽然在过程的早期考虑问题的这些方面通常是明智的，但是在没有清楚地理解最终结果是如何形成的情况下，定义 DP 问题的状态通常是非常困难的，因此以最直接的方式实施解决方案通常是非常有帮助的。因此，我们将通过两种不同的方式来解决子集和问题的基本情况和状态，这两种方式更容易实现。\n\n在我们对动态编程的探索中，我们将考虑每一个问题总共四种不同的方法:**蛮力**、**回溯**、**记忆**和**制表**。与任何 DP 问题一样，所有这些方法都能够产生正确的结果，但前三种方法会随着输入规模的增加而迅速证明其局限性。然而，当处理任何动态编程问题时，以这种方式实现逐步优化的解决方案可以发挥很大的作用。\n\n### 第二步:暴力\n\n尽管效率低下，暴力解决方案在发展对手头问题的理解方面还是很有帮助的。实施强力方法可能是形成动态规划解决方案过程中必不可少的一步，原因如下:\n\n*   **简单性**:编写一个解决方案而不考虑其效率的简单性可以是一个很好的方式来发展对问题基本方面的理解；它还可以导致对问题本质的洞察，否则在没有足够背景的情况下，试图理解其复杂性的行为可能会错过这些洞察。\n*   **解决方案正确性的确定性**:通常情况下，当问题被更好地理解时，一个特别复杂的 DP 解决方案将需要相当多的重新设计。因此，有一种方法将您的解决方案输出与正确答案进行比较是非常重要的。\n*   **可视化子问题的能力**:强力解决方案会生成每个潜在的解决方案，然后选择符合问题标准的解决方案。这为可视化如何形成正确的解决方案提供了一种有效的方法，然后可以检查其基本模式，以便在以后的方法中使用。\n\n下面的练习演示了暴力方法的实现。\n\n### 练习 36:使用蛮力方法解决子集和问题\n\n在本练习中，我们将使用蛮力方法找到子集和问题的解决方案。让我们开始吧:\n\n1.  让我们从包含以下标题(为了方便起见还有`std`名称空间)开始:\n\n    ```cpp\n    #include <iostream>\n    #include <vector>\n    #include <algorithm>\n    using namespace std;\n    ```\n\n2.  另外，让我们定义一个名为`DEBUG`的预处理器常量和一个名为`PRINT`的宏，只有当`DEBUG`不为零时才会打印到`stderr`:\n\n    ```cpp\n    #define DEBUG 0\n    #if DEBUG\n    #define PRINT(x) cerr << x\n    #else\n    #define PRINT(x) \n    #endif\n    ```\n\n3.  我们现在将声明一个新的函数`SubsetSum_BruteForce()`，它接受两个参数——一个整数数组`set`和一个整数`sum`，并返回一个布尔值:\n\n    ```cpp\n    bool SubsetSum_BruteForce(vector<int> set, int sum)\n    {\n        ……\n    }\n    ```\n\n4.  现在，让我们声明另一个函数，`GetAllSubsets()`，它接受四个参数——两个整数向量，`set`和`subset`；一个整数；`index`；和一个名为`allSubsets`的三维整数向量(通过引用传递)。我们将使用该函数递归生成 *S* 的所有子集:\n\n    ```cpp\n    void GetAllSubsets(vector<int> set, vector<int> subset, int index, vector<vector<vector<int>>> &allSubsets)\n    {    \n        // Terminate if the end of the set is reached\n        if(index == set.size()) \n        {\n            // Add the accumulated subset to the results, indexed by size\n            allSubsets[subset.size()].push_back(subset);\n            return;\n        }\n        // Continue without adding element to subset\n        GetAllSubsets(set, subset, index + 1, allSubsets);\n        // Add element to subset\n        subset.push_back(set[index]);\n        GetAllSubsets(set, subset, index + 1, allSubsets);\n    }\n    ```\n\n5.  回到我们的`SubsetSum_BruteForce()`函数，我们现在可以声明`allSubsets`并调用函数:\n\n    ```cpp\n    bool SubsetSum_BruteForce(vector<int> set, int target)\n    {\n        vector<vector<vector<int>>> allSubsets(set.size() + 1);\n\n        GetAllSubsets(set, {}, 0, allSubsets);\n\n        ……\n    ```\n\n6.  现在，我们可以遍历每个子集，并将其总和与`target`进行比较，如果找到匹配，则返回`true`:\n\n    ```cpp\n    for(int size = 0; size <= set.size(); size++)\n    {\n        PRINT(\"SIZE = \" << size << endl);\n        for(auto subset : allSubsets[size])\n        {\n            int sum = 0;\n            PRINT(\"\\t{ \");\n            for(auto number : subset)\n            {\n                    PRINT(number << \" \");\n                    sum += number;\n            }\n            PRINT(\"} = \" << sum << endl);\n            if(sum == target) return true;\n        }\n    }\n    ```\n\n7.  如果在检查每个子集后没有找到匹配的和，我们返回`false` :\n\n    ```cpp\n        ……\n        return false;\n    }\n    ```\n\n8.  现在，在`main()`功能中，让我们定义我们的集合和目标如下:\n\n    ```cpp\n    int main()\n    {\n        vector<int> set = { 13, 79, 45, 29 };\n        int target = 58;\n        ……\n    }\n    ```\n\n9.  我们现在可以这样调用`SubsetSum_BruteForce()`输入:\n\n    ```cpp\n    bool found = SubsetSum_BruteForce(set, target);\n    if(found)\n    {\n        cout << \"Subset with sum \" << target << \" was found in the set.\" << endl;\n    }\n    else \n    {\n        cout << \"Subset with sum \" << target << \" was not found in the set.\" << endl;\n    }\n    ```\n\n10.  运行上述代码后，您应该会看到以下输出:\n\n    ```cpp\n    Subset with sum 58 was found in the set.\n    ```\n\n11.  现在，让我们将`target`设置为集合中找不到的总和:\n\n    ```cpp\n    int target = 1000000;\n    ```\n\n12.  再次运行程序应产生以下输出:\n\n    ```cpp\n    Subset with sum 1000000 was not found in the set.\n    ```\n\n13.  最后，让我们将`DEBUG`常数重新定义为 1:\n\n    ```cpp\n    #define DEBUG 1\n    ```\n\n14.  现在运行程序将产生以下输出:\n\n    ```cpp\n    SIZE = 0\n        { } = 0\n    SIZE = 1\n        { 29 } = 29\n        { 45 } = 45\n        { 79 } = 79\n        { 13 } = 13\n    SIZE = 2\n        { 45 29 } = 74\n        { 79 29 } = 108\n        { 79 45 } = 124\n        { 13 29 } = 42\n        { 13 45 } = 58\n        { 13 79 } = 92\n    SIZE = 3\n        { 79 45 29 } = 153\n        { 13 45 29 } = 87\n        { 13 79 29 } = 121\n        { 13 79 45 } = 137\n    SIZE = 4\n        { 13 79 45 29 } = 166\n    Subset with sum 1000000 was not found in the set.\n    ```\n\n因此，我们能够使用蛮力方法找到所需的子集。请注意，为了找到解决方案，我们基本上在尝试各种可能性。在下一节中，我们将对其进行一层优化。\n\n### 步骤 2.b:优化我们的方法——回溯\n\n显然，暴力方法还有很多不足之处。就性能而言，它已经尽可能地低效了。通过不加区别地检查每个可能的子集，我们在确定选项永远不会导致解决方案(例如，总和超过目标的子集)之后很久才考虑选项。为了改进我们的算法，我们可以利用**回溯**来排除保证无效的子问题的所有分支。\n\n在尝试使用 DP 之前实现回溯解决方案的主要优点是，它要求我们确定问题的基本情况和中间递归状态。正如我们在本章前面定义的，基本情况是递归函数中的一个条件，它不依赖于进一步的递归来产生答案。为了进一步说明，考虑计算一个数的阶乘的问题(一个数的阶乘， *n* ，相当于*n *(n-1)*(n-2)*(n-3)……* 1*)。我们可以编写一个 C++ 函数，如下所示:\n\n```cpp\nint Factorial(int n)\n{\n    // Base case — stop recursing\n    if(n == 1)\n    {\n        return 1;\n    }\n    // Recurse until base case is reached\n    return n * Factorial(n - 1);\n}\n```\n\n这个递归函数的结构可以这样说明:\n\n![Figure 8.4: Recursively calculating the Nth factorial](img/C14498_08_04.jpg)\n\n###### 图 8.4:递归计算第 n 个阶乘\n\n`n = 1`条件是基本情况，因为这是解可以返回而不进一步递归的点。\n\n在子集和问题中，定义基本情况的一种方法如下:\n\n```cpp\nIf sum of a given subset is equal to target : TRUE\n\nOtherwise:\n    — If sum is greater than target : FALSE\n    — If end of set is reached : FALSE\n```\n\n既然我们已经建立了基本案例，我们需要定义我们的中间状态。使用我们的蛮力算法的输出作为参考，我们可以分析每个大小组的子集是如何形成的，以绘制出我们的状态转换:\n\n```cpp\nBase case —> { } [SUM = 0]\n{ } —> { 13 } [0 + 13 = 13]\n       { 79 } [0 + 79 = 79]\n       { 45 } [0 + 45 = 45]\n       { 29 } [0 + 29 = 29]\n```\n\n当然大小`0`和大小`1`状态是最容易理解的。我们从一个空集合开始，我们可以向其中添加任何元素，以便创建大小为 1 的所有子集。\n\n```cpp\n{ 13 } —> { 13 79 } [13 + 79 = 92]\n          { 13 45 } [13 + 45 = 58]\n          { 13 29 } [13 + 29 = 42]\n{ 79 } —> { 79 45 } [79 + 45 = 124]\n          { 79 29 } [79 + 29 = 108]\n{ 45 } —> { 45 29 } [45 + 29 = 74]\n```\n\n对于大小为 2 的子集，我们可以遵循同样的逻辑。只需取大小为 1 的每个子集，并追加索引大于该子集中索引最高的元素的每个元素。这基本上是我们在强力实现中采用的方法；但是，这一次，我们将在处理它们时考虑每个子集的总和，并在当前总和超过目标时终止它们:\n\n![Figure 8.5: Eliminating values that exceed the target](img/C14498_08_05.jpg)\n\n###### 图 8.5:消除超出目标的值\n\n当`target`等于`58`时，我们可以看到大小为 3 或 4 的子集都不需要考虑。因此，我们可以如下描述我们的中间状态转换:\n\n```cpp\nfor element of set at index i and subset ss:\n    If sum of ss with set[i] is less than or equal to target: \n        1) Append set[i] to ss\n        2) Increment i \n        Next state —> (i = i + 1, ss = ss ∪ set[i])\n    In any case: \n        1) Do not append set[i] to ss\n        2) Increment i\n        Next state —> (i = i + 1, ss = ss)\n```\n\n现在，我们应该问以下问题:\n\n*   表示这种状态所需的最小数据量是多少？\n*   我们如何重新表述前面的逻辑来删除不必要的信息？\n\n考虑我们试图解决的具体问题:寻找元素的子集是否存在于和等于目标的集合中。根据问题描述，我们的任务不要求我们产生实际的子集，而只要求它们的和。因此，我们的伪代码可以更简洁地表达如下:\n\n```cpp\nfor element of set at index i and its sum as sum:\n    If sum plus set[i] is less than or equal to target: \n        1) Add value of set[i] to sum\n        2) Increment i \n        Next state —> (i = i + 1, sum = sum + set[i])\n    In any case: \n        1) Do not add value of set[i] to sum\n        2) Increment i\n        Next state —> (i = i + 1, sum = sum)\n```\n\n使用这种新的方法，我们基本上可以用两个整数`sum`和`i`来表示每个状态转换，消除了在最坏的情况下存储 *2* *n* 子集数组的需要。此外，我们可以通过反转问题来消除跟踪目标值的需要(即，从`target`开始，在每一步减去`set[i]`)。作为最后的优化，我们可以在调用函数之前对集合进行排序，这样一旦总和超过目标，我们就可以确定没有其他有效的可能性。我们将在下面的练习中用 C++ 实现这一点。\n\n### 练习 37:用回溯法解决子集和问题\n\n在本练习中，我们将解决一个类似于*练习 36* 、*中演示的问题，使用蛮力方法*解决子集和问题，但使用回溯方法和更复杂的输入来突出差异。让我们开始吧:\n\n1.  为了实现子集和问题的回溯解，我们定义了一个名为`SubsetSum_Backtracking()`的函数，如下所示:\n\n    ```cpp\n    bool SubsetSum_Backtracking(vector<int> &set, int sum, int i) \n    {\n        ……\n    }\n    ```\n\n2.  正如递归函数中经常出现的情况，我们在最开始定义我们的基本情况:\n\n    ```cpp\n    // The sum has been found\n    if(sum == 0)\n    {\n        return true;\n    }\n    // End of set is reached, or sum would be exceeded beyond this point\n    if(i == set.size() || set[i] > sum)\n    {\n        return false;\n    }\n    ```\n\n3.  在每一步，我们的选择是将当前元素的值添加到总和中，或者保持总和不变。我们可以把这个逻辑浓缩成一行，比如:\n\n    ```cpp\n    // Case 1: Add to sum\n    // Case 2: Leave as-is \n    return SubsetSum_Backtracking(set, sum – set[i], i + 1) \n        || SubsetSum_Backtracking(set, sum, i + 1); \n    ```\n\n4.  回到`main`，让我们对集合进行排序，并将我们的呼叫添加到`SubsetSum_BruteForce()`呼叫下面的`SubsetSum_Backtracking()`:\n\n    ```cpp\n    sort(set.begin(), set.end());\n    bool found;\n\n    found = SubsetSum_BruteForce(set, target);\n    found = SubsetSum_Backtracking(set, target, 0); \n    ```\n\n5.  为了测试，我们将实现一个函数，显示每种方法找到解决方案所需的时间。首先，我们需要包括`<time.h>`和`<iomanip>`标题:\n\n    ```cpp\n    #include <iostream>\n    #include <vector>\n    #include <algorithm> \n    #include <time.h>\n    #include <iomanip>\n    ```\n\n6.  我们还将定义一个名为`types`的字符串数组，我们将使用它来标记每种方法的结果:\n\n    ```cpp\n    vector<string> types = \n    {\n        \"BRUTE FORCE\",\n        \"BACKTRACKING\",\n        \"MEMOIZATION\",\n        \"TABULATION\"\n    };\n    const int UNKNOWN = INT_MAX;\n    ```\n\n7.  现在，我们将编写另一个函数`GetTime()`，该函数引用一个名为`timer`的`clock_t`对象和一个`string type`，然后返回`void` :\n\n    ```cpp\n    void GetTime(clock_t &timer, string type)\n    {\n        // Subtract timer value from current time to get time elapsed\n        timer = clock() - timer;\n        // Display seconds elapsed\n        cout << \"TIME TAKEN USING \" << type << \": \" << fixed << setprecision(5) << (float)timer / CLOCKS_PER_SEC << endl; \n\n        timer = clock(); // Reset timer \n    }\n    ```\n\n8.  现在，让我们重写`main()`函数，这样我们就可以按顺序执行每个函数调用，并比较每种方法花费的时间:\n\n    ```cpp\n    int main()\n    {\n        vector<int> set = { 13, 79, 45, 29 };\n        int target = 58;\n        int tests = 2;\n        clock timer = clock();\n        sort(set.begin(), set.end());\n        for(int i = 0; i < tests; i++)\n        {\n            bool found;\n            switch(i)\n            {\n                case 0: found = SubsetSum_BruteForce(set, target); break;\n                case 1: found = SubsetSum_Backtracking(set, target, 0); break;\n            }\n            if(found)\n            {\n                cout << \"Subset with sum \" << target << \" was found in the set.\" << endl;\n            }\n            else \n            {\n                cout << \"Subset with sum \" << target << \" was not found in the set.\" << endl;\n            }    \n            GetTime(timer, types[i]);\n            cout << endl;\n        }\n        return 0;\n    }\n    ```\n\n9.  最后，让我们重新定义我们的输入，以突出两种方法之间的效率差异:\n\n    ```cpp\n    vector<int> set = { 16, 1058, 22, 13, 46, 55, 3, 92, 47, 7, 98, 367, 807, 106, 333, 85, 577, 9, 3059 };\n    int target = 6076;\n    ```\n\n10.  Your output will produce something along the lines of the following:\n\n    ```cpp\n    Subset with sum 6076 was found in the set.\n    TIME TAKEN USING BRUTE FORCE: 0.89987\n    Subset with sum 6076 was found in the set.\n    TIME TAKEN USING BACKTRACKING: 0.00078\n    ```\n\n    #### 注意\n\n    所用时间的实际值会因您的系统而异。请注意数值的差异。\n\n正如您所看到的，在这个特殊的例子中，使用回溯方法找到答案的速度快了 1000 多倍。在下一节中，我们将通过使用缓存来进一步优化这个解决方案。\n\n### 第三步:记忆\n\n尽管回溯解决方案明显优于暴力破解，但仍远非理想。考虑一种情况，其中目标和很高，并且不在集合中——如果目标大于或等于集合中每个元素的和，我们可以通过预先计算总数并检查目标是否在有效范围内来轻松确定结果。然而，如果目标总和只是稍微低于这个数量，我们的算法仍然会被迫在完成之前探索几乎每一种可能性。\n\n为了演示这种差异，请尝试使用`6799`作为目标运行上一练习中的代码(正好比集合中所有元素的总和少 1)。在作者的机器上，回溯解决方案平均花费大约 0.268 秒产生结果——比练习中使用的目标值花费的平均时间长近 350 倍。\n\n值得庆幸的是，我们已经拥有了在利用记忆的同时设计自上而下的解决方案所需的所有信息。更好的是，我们几乎不需要修改我们以前的方法来实现它！\n\n### 设计缓存方案\n\n使用内存化最重要的方面是定义缓存方案。内存化解决方案的缓存结果可以通过多种方式完成，但最常见的方式如下:\n\n*   简单数组，状态由数字索引表示\n*   哈希表/映射，状态由描述性字符串表示，使用内置语言功能进行哈希处理\n*   哈希表/映射，其状态由使用原始哈希公式创建的哈希值表示\n\n这里的选择在很大程度上取决于上下文，但这里有一些一般的指导原则:\n\n*   由数字索引访问的数组/向量往往比映射快得多，后者必须在映射中定位给定的键，以确定它是否已经被缓存。\n*   即使状态可以表示为整数，如果缓存键非常大，那么一个数组的内存需求大到足以包含它们可能是不合理的。在这种情况下，地图是更好的选择。\n*   哈希表(例如，`std::unordered_map`)在定位和检索关键字方面往往比标准的映射/字典结构快得多(但仍然比数组慢)。\n*   `std::map`在哪些类型的数据可以用作密钥方面比`std::unordered_map`通用得多。虽然`std::unordered_map`在技术上可以提供相同的功能，但是它需要程序员为数据类型创建他们自己的散列函数，默认情况下，它不能存储为密钥。\n\n您可能还记得本章的介绍，缓存方案应该如下:\n\n*   **有效**:缓存键的表示方式必须避免不同状态之间的冲突，这些状态不用于解决同一组子问题。\n*   **有价值/有用**:如果你的缓存方案非常具体，以至于它实际上从未产生任何“命中”，那么它基本上什么也没有完成。\n\n在子集和问题中，我们可能会错误地认为，无法从具有给定`sum`值的状态中找到目标意味着不可能从具有相同和的任何其他状态中获得真实结果。因此，我们可以决定仅基于`sum`(即`if(memo[sum] != UNKNOWN) return memo[sum];`)的值来缓存每个解决方案。这是一个无效缓存方案的例子，因为它没有考虑到在同一个集合中可能有多种方法可以达到相同的总和，如下所示:\n\n```cpp\n{ 1 5 6 2 3 9 } \nSum of { 1 5 } = 6\nSum of { 6 } = 6\nSum of { 1 2 3 } = 6\n```\n\n假设目标值是前面例子中的`8`。如果首先遇到第三种情况，`memo[6]`将被设置为`false`，这显然是不正确的，因为通过包含第四个元素(`2`)可以从其他两种情况中达到目标。\n\n无用记忆方案的一个例子是这样一种方案，其中关键字等于子集的索引，因为每个可能的状态将包含一个完全唯一的关键字；因此，由同一组子问题形成的状态不会触发缓存命中。\n\n如果您不确定缓存方案的有效性，存储一个在每次缓存命中时递增的计数器可能会很有用。如果该计数器的最终值等于`0`，或者相对于您必须考虑的状态数量非常低，您可以得出您的缓存方案需要修订的结论。\n\n我们将探索使用向量缓存来实现记忆化。\n\n### 练习 38:用记忆法解决子集和问题\n\n在本练习中，我们将尝试实现与我们在*练习 37* 、*中通过使用回溯*解决子集和问题相同的解决方案，但是增加了记忆。让我们开始吧:\n\n1.  我们现在将创建另一个名为`SubsetSum_Memoization()`的函数。该函数的定义将与`SubsetSub_Backtracking()`相同，除了它将包括对一个名为`memo` :\n\n    ```cpp\n    bool SubsetSum_Memoization(vector<int> &set, int sum, int         i, vector<vector<int>> &memo)\n    {\n        ……\n    }\n    ```\n\n    的二维整数向量的引用\n2.  这个函数的大部分代码看起来与回溯方法非常相似。例如，我们的基本案例将像以前一样定义:\n\n    ```cpp\n    if(sum == 0)\n    {\n        return true;\n    }\n    if(i == set.size() || set[i] > sum)\n    {\n        return false;\n    }\n    ```\n\n3.  关键的区别在于，在基本情况之后，我们不是立即调查接下来的两个状态，而是检查`memo`表中的缓存结果:\n\n    ```cpp\n    // Is this state's solution cached?\n    if(memo[i][sum] == UNKNOWN)\n    {\n        // If not, find the solution for this state and cache it\n        bool append = SubsetSum_Memoization(set, sum - set[i], i + 1, memo);\n        bool ignore = SubsetSum_Memoization(set, sum, i + 1, memo);\n        memo[i][sum] = append || ignore;\n    }\n    // Return cached value\n    return memo[i][sum];\n    ```\n\n4.  现在，我们应该在`main()`函数中插入对`SubsetSum_Memoization()`的调用:\n\n    ```cpp\n    int tests = 3;\n    for(int i = 0; i < tests; i++)\n    {\n        bool found;\n        switch(i)\n        {\n            case 0: found = SubsetSum_BruteForce(set, target); break;\n            case 1: found = SubsetSum_Backtracking(set, target, 0); break;\n            case 2:\n            {\n                // Initialize memoization table\n                vector<vector<int>> memo(set.size(), vector<int>(7000, UNKNOWN));\n                found = SubsetSum_Memoization(set, target, 0, memo);\n                break;\n            }\n        }\n\n        if(found)\n        {\n            cout << \"Subset with sum \" << target << \" was found in the set.\" << endl;\n        }\n        else\n        {\n            cout << \"Subset with sum \" << target << \" was not found in the set.\" << endl;\n        }\n        GetTime(timer, types[i]);\n        cout << endl;\n    }\n    ```\n\n5.  Now, let's define `target` as `6799` and run our code. You should see an output similar to this:\n\n    ```cpp\n    Subset with sum 6799 was not found in the set.\n    TIME TAKEN USING BRUTE FORCE: 1.00100\n    Subset with sum 6799 was not found in the set.\n    TIME TAKEN USING BACKTRACKING: 0.26454\n    Subset with sum 6799 was not found in the set.\n    TIME TAKEN USING MEMOIZATION: 0.00127\n    ```\n\n    #### 注意\n\n    所用时间的实际值会因您的系统而异。请注意数值的差异。\n\n从输出中我们可以看到，缓存以指数因子优化了我们的问题。\n\n### 第四步:制表\n\n到目前为止，我们已经实现了三种不同的算法方法来解决子集和问题，每种方法都比前一种方法有显著的改进。然而，假设我们想要一个给定集合中所有可能子集和的列表。我们必须对每个和重复运行我们的算法，从 1 到整个集合的总和。对于这种情况，列表往往是唯一有效的选择。\n\n实现这样一个问题的迭代列表解决方案通常很难概念化。尽管问题的递归公式非常适合多维状态和分支条件，但列表解决方案必须使用标准的`for` / `while`循环，以某种方式将复杂的层次浓缩成一组简单的迭代:\n\n![Figure 8.6: Depiction of how the complexity of the subset sum problem’s recursive structure is reduced in the tabulated DP solution](img/C14498_08_06.jpg)\n\n###### 图 8.6:描述了子集和问题的递归结构的复杂性是如何在表格化的 DP 解决方案中降低的\n\n有几种方法可以解决这种减少，但最终它倾向于归结为你是否足够好地理解这个问题，以做出正确的概括。\n\n与记忆一样，在定义问题的基本情况和状态后，第一个目标是开发一个方案来存储不同状态的解决方案。通常，列表方法使用简单的数组/向量来达到这个目的。在斐波那契数列的计算中，我们已经看到了一个非常简单的 DP 表的例子:\n\n```cpp\nF[n] = F[n – 1] + F[n – 2];\n```\n\n在本章前面，我们还讨论了如何递归计算阶乘。一种自下而上的方法来填充该问题的表格，如下所示:\n\n```cpp\nfactorial[n] = factorial[n – 1] * n;\n```\n\n这些都是非常简单的例子，因为它们只包含一个维度，没有条件逻辑。每个州从头到尾都有一个一致的、可预测的公式。\n\n这些例子和子集和问题之间的主要区别在于，在后者中唯一表示每个状态的最小方式需要两个维度——集合中的索引和当前和。\n\n让我们更深入地考虑一下我们在这个问题上获得的一些见解:\n\n*   每个可能的尺寸子集`k`可以通过获取新元素并将其附加到每个尺寸子集`k – 1`上来形成。\n*   如果在索引`i`处找到了一个解，其和值为`x`，那么任何最终导致相同条件集的状态转换序列都将产生相同的结果:\n\n![Figure 8.7: Multiple paths with the same sum value on the same index value](img/C14498_08_07.jpg)\n\n###### 图 8.7:同一索引值上具有相同和值的多条路径\n\n这两个递归路径在用红色表示的状态下具有等于`8`的和值和等于`3`的索引值，由于子集和问题的最优子结构，这意味着该状态的解只需要被找到一次——无论之前发生了什么，它的结果都将是相同的。\n\n考虑到这些事实，我们可以从本质上颠倒自上而下的方法来开发自下而上的方法。\n\n**自上而下逻辑:**\n\n1.  从目标总和和集合的第一个索引开始。\n2.  Iterate through the set:\n\n    -如果总和减为零，结果为`TRUE`。\n\n    -如果达到设定的终点或超过目标，结果为`FALSE`。\n\n    -否则，您可以从总和中减去当前值或忽略它。\n\n3.  如果可以从状态`S`找到目标，其中和等于`x`，指数等于`i`，那么也可以从任何更早的状态找到目标，最终导致状态`S`。\n\n**自下而上逻辑:**\n\n1.  从等于`0`的总和和索引值开始。\n2.  Iterate through the set:\n\n    -如果在指数`0`和`i`之间可以找到等于`x`的和，那么在指数`0`和`i+1`之间也可以找到等于`x`的和。\n\n    -如果在指数`0`和`i`之间可以找到等于`x`的和，那么在指数`0`和`i+1`之间可以找到等于`x + set[i]`的和。\n\n    就如何填充表格而言，自上而下的方法可以描述如下:\n\n    在 S1 州，如果总和等于`x`且指数等于`i`，则出现以下任一情况时，`memo(i, x) = true`的值为:\n\n    -目标可以从 S2 州找到(这里总和等于`x – set[i]`，指数等于`i + 1`)，或者…\n\n    -可以从 S3 州找到目标(其中总和等于`x`，指数等于`i + 1`)\n\n    -否则，`memo(i, x) = false`。\n\n    这种逻辑的自下而上版本如下:\n\n    如果总和等于`x`且指数等于`i`，则出现以下任一情况时，`DP(i, x) = true`的值为:\n\n    - `x`小于`set[i]`和`DP(i-1, x) = true`的值\n\n    - `x`大于或等于`set[i]`和`DP(i-1, sum) = true OR DP(i-1, sum – set[i]) = true`的值\n\n    -否则，`DP(i, x) = false`。\n\n换句话说，如果我们已经确定在指数`0`和`i`(含)之间可以形成和`x`，那么很明显，在指数`0`和`i + 1`之间可以形成等于`x`和`x + set[i]`的和。在下面的练习中，我们将看一下这一点的实现。\n\n### 练习 39:用列表法解决子集和问题\n\n在本练习中，我们将修改*练习 38* 、*使用记忆*解决子集和问题的解决方案，以便我们可以通过将逻辑从自上而下转换为自下而上来使用制表。让我们开始吧:\n\n1.  我们将定义一个名为`SubsetSum_Tabulation()`的新函数，它以一个名为`set`的整数向量作为参数，并返回一个二维布尔向量:\n\n    ```cpp\n    vector<vector<bool>> SubsetSum_Tabulation(vector<int> set)\n    {\n        ……\n    }\n    ```\n\n2.  我们声明一个叫做`DP`的二维布尔向量。第一个维度的大小应该等于`set`的长度，第二个维度的大小应该等于集合中最高可能的子集和(即所有元素的总和)加 1。DP 的每个值都应该初始化为`false`，除了基本情况(即总和等于零):\n\n    ```cpp\n    int maxSum = 0;\n    for(auto num : set) \n    {\n        maxSum += num;\n    }\n    vector<vector<bool>> DP(set.size() + 1, vector<bool>(maxSum + 1, false));\n    for(int i = 0; i < set.size(); i++)\n    {\n        // Base case — a subset sum of 0 can be found at any index\n        DP[i][0] = true;\n    }\n    ```\n\n3.  现在，我们迭代两个嵌套的`for`循环，对应于`DP`表的第一个和第二个维度:\n\n    ```cpp\n    for(int i = 1; i <= set.size(); i++)\n    {\n        for(int sum = 1; sum <= maxSum; sum++)\n        {\n            ……\n        }\n    }\n    ```\n\n4.  现在，要填充表格，请使用以下代码:\n\n    ```cpp\n    for(int i = 1; i <= set.size(); i++)\n    {\n        for(int sum = 1; sum <= maxSum; sum++)\n        {\n            if(sum < set[i-1])\n            {\n                DP[i][sum] = DP[i-1][sum];\n            }\n            else\n            {\n                DP[i][sum] = DP[i-1][sum]\n                        || DP[i-1][sum – set[i-1]];\n            }\n        }\n    }\n    return DP;\n    ```\n\n5.  现在，我们再次修改`main()`函数以包含我们的列表解决方案:\n\n    ```cpp\n    int main()\n    {\n        vector<int> set = { 16, 1058, 22, 13, 46, 55, 3, 92, 47, 7, 98, 367, 807, 106, 333, 85, 577, 9, 3059 };\n        int target = 6076\n        int tests = 4;\n        clock_t timer = clock();\n        sort(set.begin(), set.end());\n        for(int i = 0; i < tests; i++)\n        {\n            bool found;\n            switch(i)\n            {\n                ……\n                case 3:\n                {\n                    vector<vector<bool>> DP = SubsetSum_Tabulation(set);\n                    found = DP[set.size()][target];\n                    break;\n                }\n            }\n        }\n        ……\n    }\n    ```\n\n6.  You should see an output something like the one shown here:\n\n    ```cpp\n    Subset with sum 6076 was found in the set.\n    TIME TAKEN USING BRUTE FORCE: 0.95602\n    Subset with sum 6076 was found in the set.\n    TIME TAKEN USING BACKTRACKING: 0.00082\n    Subset with sum 6076 was found in the set.\n    TIME TAKEN USING MEMOIZATION: 0.00058\n    Subset with sum 6076 was found in the set.\n    TIME TAKEN USING TABULATION: 0.00605\n    ```\n\n    #### 注意\n\n    所用时间的实际值会因您的系统而异。请注意数值的差异。\n\n7.  正如我们所看到的，列表解所花费的时间比记忆解和回溯解都要长。但是，使用`SubsetSum_Tabulation()`返回的 DP 表，我们可以使用下面的代码来查找每个可能的子集和:\n\n    ```cpp\n    int total = 0;\n    for(auto num : set) \n    {\n        total += num;\n    }\n    vector<vector<bool>> DP = SubsetSum_Tabulation(set);\n    vector<int> subsetSums;\n    for(int sum = 1; sum <= total; sum++)\n    {\n        if(DP[set.size()][sum])\n        {\n            subsetSums.push_back(sum);\n        }\n    }\n    cout << \"The set contains the following \" << subsetSums.size() << \" subset sums: \";\n    for(auto sum : subsetSums) \n    {\n        cout << sum << \" \";\n    }\n    cout << endl; \n    ```\n\n8.  这个的输出应该是这样开始和结束的:\n\n    ```cpp\n    The set contains the following 6760 subset sums: 3 7 9 10 12 13 16 19 20 22 …… 6790 6791 6793 6797 6800\n    ```\n\n因此，我们优化了解，也获得了所有状态的和值。\n\n在这一章中，我们探索了多种解决子集和问题的方法，这反过来证明了动态编程方法的明显优势；然而，尽管 DP 解决方案相对于替代方案具有比较优势，但我们也展示了天真且相对低效的方法如何帮助我们更好地理解问题，这极大地简化了使用 DP 设计解决方案的过程。\n\n动态编程解决方案所需的一些逻辑最初可能看起来相当复杂，难以掌握。强烈建议您在继续下一步之前充分理解我们在本节中讨论的每种解决方案方法，因为这是一个可以通过使用不同的输入参数和比较结果来加速的过程。此外，根据给定的输入绘制不同解决方案的图表可能特别有帮助。\n\n### 活动 18:旅行行程\n\n你正在为一家旅行社设计一个网络应用，希望帮助客户规划他们的假期行程。该软件的一个主要方面涉及路线规划，它允许用户指定他们想要访问的多个地点，然后查看他们在前往最终目的地的途中必须经过的城市列表。\n\n你的代理机构与每个主要城市的特定运输公司都有合同，每个运输公司都对他们能走多远设定了限制。一架飞机或火车可以穿越多个城市，甚至整个国家，而公共汽车或出租车服务可能只愿意在最初位置之外的一两个城市旅行。当您的软件生成可能的中间站列表时，它还会显示该位置的运输公司愿意行驶的最大城市数量，以便客户可以相应地规划他们的路线。\n\n您最近意识到，您的应用需要某种方法来允许客户过滤提供给他们的选项数量，因为许多受欢迎的旅游地点被密集的城镇群隔开。为此，您需要确定从给定的起始位置到达最终目的地的可能方法的总数，以便减少当信息过多时显示的信息量。\n\n您的应用已经能够计算出发点和目的地之间的理想路线上的位置列表。由此，您得到了以下数据:\n\n*   `N`:表示源和目的地之间城市数量的整数\n*   `distance`:整数数组，代表运输公司在每个地点愿意穿越的最大城市数量\n\n您的任务是实现一个算法，该算法将计算通过一系列中间位置到达目的地的可能方式的总数。\n\n**输入**\n\n第一行包含单个整数，`N`，起点和终点之间的城市数。\n\n第二行包含`N`个用空格分隔的整数，其中每个整数 di 代表从索引`i`处的城市开始可以行驶的最大距离。\n\n**输出**\n\n您的程序应该输出一个整数以及从索引`0`开始到索引`N`结束遍历城市的总次数。因为数值随着`N`的增加而变得很大，所以将每个结果输出为`modulo 1000000007`。\n\n**例**\n\n假设您得到了以下输入:\n\n```cpp\n6\n1 2 3 2 2 1\n```\n\n这意味着在源位置和目标位置之间总共有六个城市。从索引`i`处的给定城市，您可以选择前往`i + 1`至`i + distance[i]`(含)范围内的任何其他城市。如果我们把城市序列想象成一个图，前面例子的邻接关系如下:\n\n```cpp\n[0]: { 1 }\n[1]: { 2, 3 }\n[2]: { 3, 4, 5 }\n[3]: { 4, 5 }\n[4]: { 5, 6 }\n[5]: { 6 }  \n```\n\n观察下图以获得进一步的说明:\n\n![Figure 8.8: Example of city adjacencies](img/C14498_08_08.jpg)\n\n###### 图 8.8:城市周边的例子\n\n在上例中，可以通过以下方式到达目的地(以`E`代表终点):\n\n```cpp\n0 > 1 > 2 > 3 > 4 > 5 > E\n0 > 1 > 2 > 3 > 4 > E\n0 > 1 > 2 > 3 > 5 > E\n0 > 1 > 2 > 4 > 5 > E\n0 > 1 > 3 > 4 > 5 > E\n0 > 1 > 2 > 4 > E\n0 > 1 > 2 > 5 > E\n0 > 1 > 3 > 4 > E\n0 > 1 > 3 > 5 > E\n```\n\n这给了我们一个`9`的答案。\n\n一般来说，遍历总是从索引`0`开始，到索引`N`结束。保证一个城市的指数`i`与`distance[i]`之和永远不会大于`N`，每个城市至少有一个对应的距离值`1`。\n\n**测试用例**\n\n以下测试用例应该有助于您更好地理解这个问题:\n\n![Figure 8.9: Activity 18 simple test cases ](img/C14498_08_09.jpg)\n\n###### 图 8.9:活动 18 简单测试用例\n\n下面是一些更复杂的测试案例:\n\n![Figure 8.10: Activity 18 complex test cases ](img/C14498_08_10.jpg)\n\n###### 图 8.10:活动 18 复杂的测试用例\n\n**额外积分**\n\n假设您已经找到了一种方法，在合理的时间限制内通过了前面的测试用例，那么您可以用最后一个测试用例真正测试您的算法的效率，其中`N`等于`10000000`。因为值的数量会占用太多的打印空间，所以可以使用以下代码以编程方式生成数组值:\n\n```cpp\nvector<int> Generate(int n)\n{\n    vector<int> A(n);\n\n    ULL val = 1;\n\n    for(int i = 0; i < n; i++)\n    {\n        val = (val * 1103515245 + 12345) / 65536;\n        val %= 32768;\n\n        A[i] = ((val % 10000) % (n – i)) + 1;\n    }\n    return A;\n}\n```\n\n你的程序应该打印`318948158`作为这个测试用例的结果。一个最佳算法应该能够在一秒钟内找到结果。\n\n**活动指南**\n\n*   最佳方法将在`O(n)`时间内运行，并且需要精确的`n`迭代。\n*   如果您完全不确定如何制定差压解决方案，请使用本章中描述的增量方法，即先使用蛮力，然后逐步优化解决方案。\n*   For insights into how the problem's states are formed, consider the recurrence relation exhibited by the Fibonacci sequence.\n\n    #### 注意\n\n    这个活动的解决方案可以在第 556 页找到。\n\n## 字符串和序列的动态编程\n\n到目前为止，我们对动态规划的探索主要集中在组合问题和用定义的公式计算整数序列的项上。现在，我们将考虑 DP 的另一个最常见的用途，即处理数据序列中的模式。程序员将 DP 用于此目的的最典型场景通常涉及搜索、比较和构造字符串。\n\n作为软件开发人员，我们经常与几个人合作，他们都有能力对同一个项目做出贡献和修改。由于程序员可能会无意中在代码中引入一个错误，或者团队可能会针对给定的特性尝试一种不同的方法，然后决定返回到他们原来的方法，因此拥有某种版本控制系统变得极其重要。如果最近工作的某个功能神秘地出现了故障，那么有能力看到对代码所做的更改是至关重要的，尤其是在它们与早期版本的差异方面。因此，所有的版本控制系统都有一个“差异”特性，可以分析同一代码的两个版本之间的相似性，然后以某种方式向用户显示出来。\n\n例如，假设您已经将以下代码添加到存储库中:\n\n```cpp\nbool doSomething = true;\nvoid DoStuff()\n{\n    DoSomething();\n    DoSomethingElse();\n    DoAnotherThing();\n}\n```\n\n第二天，你做了一些改变:\n\n```cpp\nbool doSomething = false;\nvoid DoStuff()\n{\n    if(doSomething == true)\n    { \n        DoSomething();\n    }\n    else \n    {\n        DoSomethingElse();\n    }\n}\n```\n\n然后，diff 实用程序将显示类似如下的内容:\n\n![Figure 8.11: Diff utility output](img/C14498_08_11.jpg)\n\n###### 图 8.11:差异实用程序输出\n\n为了实现这一点，实用程序需要计算两个代码文件的相似性，方法是考虑两个版本共有的文本序列在字符串中不一定是连续的。此外，部分原始文本可能已被删除或出现在新版本的其他位置。这表明需要**近似**(或“**模糊**”**字符串匹配**，这是一种经常使用动态编程的技术。\n\n### 最长公共子序列问题\n\n**最长公共子序列问题**(通常缩写为 **LCS** )是动态规划最著名的经典例子之一。它回答了以下问题:给定两个数据序列，它们共同的最长子序列是什么？\n\n举个例子，考虑两个字符串，`A`和`B`:\n\n![Figure 8.12: Two given strings for finding the longest common subsequence](img/C14498_08_12.jpg)\n\n###### 图 8.12:找到最长公共子序列的两个给定字符串\n\n最长的公共子序列是“`LONGEST`”:\n\n![Figure 8.13: Longest common subsequence in the given strings](img/C14498_08_13.jpg)\n\n###### 图 8.13:给定字符串中最长的公共子序列\n\n根据我们从针对子集和问题实现的一系列方法中获得的见解，让我们更明智地处理这个问题。我们将从基本案例开始，提前制定一些关于问题结构的想法。\n\n由于在没有首先考虑琐碎输入的情况下，很难理解大输入的 DP 问题的本质，让我们创建一些使用小输入字符串的不同场景的例子，并尝试找到最长公共子序列的长度(LCS):\n\n```cpp\nCase 1): A or B is empty\nA   = \"\"\nB   = \"\"\nLCS = 0\nA   = \"A\"\nB   = \"\"\nLCS = 0\nA   = \"\"\nB   = \"PNEUMONOULTRAMICROSCOPICSILICOVOLCANOCONIOSIS\"\nLCS = 0\n```\n\n在一个或两个字符串都为空的情况下，很明显最长的公共子序列的长度总是等于零:\n\n```cpp\nCase 2) Both A and B contain a single character\nA   = \"A\"\nB   = \"A\"\nLCS = 1\nA   = \"A\"\nB   = \"B\"\nLCS = 0\nCase 3) A has one character, B has two characters\nA   = \"A\"\nB   = \"AB\"\nLCS = 1\nA   = \"A\"\nB   = \"BB\"\nLCS = 0\n```\n\n这两种情况有一个简单的二进制定义——它们要么有共同的特征，要么没有:\n\n```cpp\nCase 4) Both A and B contain two characters\nA:  = \"AA\"\nB:  = \"AA\"\nLCS = 2\nA   = \"BA\"\nB   = \"AB\"\nLCS = 1\nA   = \"AA\"\nB   = \"BB\"\nLCS = 0\n```\n\n长度为 2 的字符串会让事情变得更有趣，但逻辑仍然很琐碎。给定两个长度为 2 的字符串，它们要么相同，要么有一个共同的字符，要么没有共同的字符:\n\n```cpp\nCase 5) A and B both contain 3 characters\nA   = \"ABA\"\nB   = \"AAB\"\nLCS = 2    \nA   = \"ABC\"\nB   = \"BZC\"\nLCS = 2\n```\n\n现在，问题的复杂性开始显现。这个案例表明，比较逐渐变得不那么简单:\n\n```cpp\nCase 6: A and B both contain 4 characters\nA   = AAAB\nB   = AAAA\n{ \"AAA_\", \"AAA_\" }\n{ \"AAA_\", \"AA_A\" }\n{ \"AAA_\", \"A_AA\" }\n{ \"AAA_\", \"_AAA\" }\nLCS = 3\nA   = AZYB\nB   = YZBA    \n{ \"_Z_B\", \"_ZB_\" }\n{ \"__YB\", \"Y_B_\" }\nLCS = 2\n```\n\n到目前为止，应该相当明显的是，LCS 问题确实包含重叠的子问题。类似于前面的问题，我们可以观察到给定字符串有 2n 个可能的子集，其中`n`等于字符串的长度，除了现在我们有两个序列要处理。更糟糕的是，我们不能简单地独立考虑每个序列的子集，还必须对它们进行比较:\n\n![Figure 8.14: All possible character subsequences of two strings, ABCX and ACY](img/C14498_08_14.jpg)\n\n###### 图 8.14:两个字符串的所有可能的字符子序列，ABCX 和 ACY\n\n我们不仅仅是在寻找连续的字符组，这一事实有几个含义:首先，相同的字符序列可以在整个字符串中出现多次，并且可以以任何可能的排列方式在任意字符串中间隔开，假设字符的顺序相同。其次，从任何给定的索引开始，可以有许多公共子序列。\n\n在实现我们的暴力方法之前，让我们也定义什么构成了这个问题的状态。假设我们正在维护两个指针`i`和`j`，它们分别表示`A`和`B`中的字符索引，以及我们已经找到的常见字符子序列的记录:\n\n```cpp\nif i exceeds length of A, or j exceeds length of B:\n— Terminate recursion and return length of subsequence\n```\n\n如果我们已经到达任何一个字符串的末尾，那么就没有什么可以比较的了，因为子序列的索引是有序的:\n\n```cpp\nif A[i] = B[j]:\n— Increase length of subsequence by 1\n— Increment both i and j by 1 \n```\n\n如果字符相等，不将其包含在我们找到的子序列中没有好处。我们增加两个指针，因为任何给定的字符在每个子序列中只能被认为是一次:\n\n```cpp\nOtherwise:\n    Option 1) Explore further possibilities with i + 1, and j\n    Option 2) Explore further possibilities with i, and j + 1\n    LCS from this state is equal to maximum value of Option 1 and Option 2\n```\n\n如果我们没有找到匹配，我们可以选择探索下一个 A 的字符子集，或者下一个 B 的字符子集。我们不包括从这个状态同时递增两个索引的情况，因为这是多余的。下一次函数调用将探讨这种情况。概述此循环的结构如下所示:\n\n![Figure 8.15: Subproblem tree for the longest subsequence problem](img/C14498_08_15.jpg)\n\n###### 图 8.15:最长子序列问题的子问题树\n\n在上图中，重叠的子问题已经用颜色编码。这个问题的最优子结构还不太清楚，但我们仍然可以做一些基本的概括:\n\n*   我们只需要比较等长的子集。\n*   从一个给定的状态，下一个状态的可能性可以通过增加`i`、`j`或两者来探索。\n*   我们的搜索总是在到达任一字符串的末尾时结束。\n\n希望我们的初步强力实现能够提供额外的见解。让我们在下面的练习中直接进入正题。\n\n### 练习 40:使用蛮力方法找到最长的公共子序列\n\n在本练习中，我们将使用蛮力方法来解决这个问题，就像我们在*练习 36* 、*中使用蛮力方法*解决子集和问题一样。让我们开始吧:\n\n1.  首先包含以下标题，并定义我们在上一章中使用的`DEBUG`和`PRINT`宏:\n\n    ```cpp\n    #include <iostream>\n    #include <time.h>\n    #include <iomanip>\n    #include <algorithm>\n    #include <utility>\n    #include <vector>\n    #include <strings.h>\n    #define DEBUG 1\n    #if DEBUG\n    #define PRINT(x) cerr << x\n    #else \n    #define PRINT(x)\n    #endif\n    using namespace std;\n    ```\n\n2.  定义一个名为`LCS_BruteForce()`的函数，该函数接受以下参数:两个字符串`A`和`B`，两个整数`i`和`j`，以及一个整数对向量`subsequence`，并返回一个整数。在这个函数上面，我们还会声明一个全局范围的整数对的二维向量，即`found` :\n\n    ```cpp\n    vector<vector<pair<int, int>>> found;\n    int LCS_BruteForce(string A, string B, int i, int j, vector<pair<int, int>> subsequence)\n    {\n        ……\n    }\n    ```\n\n3.  `A` and `B` are, of course, the strings we are comparing, `i` and `j` represent our current positions in `A` and `B`, respectively, and `subsequence` is the collection of index pairs that form each common subsequence, which will be collected in `found` for output.\n\n    由于我们已经有了伪代码，我们可以相对容易地实现我们的函数，只需将伪代码的每一行作为注释插入到我们的函数中，并将其翻译成下面的 C++ 代码:\n\n    ```cpp\n    // If i exceeds length of A, or j exceeds length of B:\n    if(i >= A.size() || j >= B.size())\n    {\n        found.push_back(subsequence);\n        //Terminate recursion and return length of subsequence\n        return subsequence.size();\n    }\n    // if A[i] = B[j]:\n    if(A[i] == B[j])\n    {\n        // Increase length of subsequence by 1\n        subsequence.push_back({ i, j });\n        // Increment both i and j by 1 \n        return LCS_BruteForce(A, B, i + 1, j + 1, subsequence);\n    }    \n    /*\n        Option 1) Explore further possibilities with i + 1, and j        \n        Option 2) Explore further possibilities with i, and j + 1\n        LCS from this state is equal to maximum value of Option 1 and Option 2\n    */\n    return max(LCS_BruteForce(A, B, i + 1, j, subsequence),\n             LCS_BruteForce(A, B, i, j + 1, subsequence));\n    ```\n\n4.  在`main()`中，我们将以两个字符串的形式接收输入，然后在上面调用我们的函数:\n\n    ```cpp\n    int main() \n    {\n        string A, B;\n        cin >> A >> B;\n        int LCS = LCS_BruteForce(A, B, 0, 0, {}); \n        cout << \"Length of the longest common subsequence of \" << A << \" and \" << B << \" is: \" << LCS << endl;\n        …    \n    }\n    ```\n\n5.  就像我们在上一章所做的那样，如果`DEBUG`没有设置为`0`，我们也将把找到的子序列输出到`stderr`。然而，由于这个问题更复杂，我们将把这个输出放在一个单独的函数中，`PrintSubsequences()` :\n\n    ```cpp\n    void PrintSubsequences(string A, string B)\n    {\n        // Lambda function for custom sorting logic\n        sort(found.begin(), found.end(), [](auto a, auto b)\n        {\n            // First sort subsequences by length\n            if(a.size() != b.size())\n            {\n                return a.size() < b.size();\n            }\n            // Sort subsequences of same size by lexicographical order of index\n            return a < b;\n        });\n        // Remove duplicates \n        found.erase(unique(found.begin(), found.end()), found.end());\n        int previousSize = 0;\n        for(auto subsequence : found)\n        {\n            if(subsequence.size() != previousSize)\n            {\n                previousSize = subsequence.size();\n                PRINT(\"SIZE = \" << previousSize << endl);\n            }\n            // Fill with underscores as placeholder characters\n            string a_seq(A.size(), '_');\n            string b_seq(B.size(), '_');\n            for(auto pair : subsequence)\n            {\n                // Fill in the blanks with the characters of each string that are part of the subsequence\n                a_seq[pair.first] = A[pair.first];\n                b_seq[pair.second] = B[pair.second];\n            }\n            PRINT(\"\\t\" << a_seq << \" | \" << b_seq << endl);\n        }\n    }\n    ```\n\n6.  然后我们可以在`main()`中调用这个函数，指定它应该被忽略，除非`DEBUG`被设置:\n\n    ```cpp\n    int main()\n    {\n        ……\n    #if DEBUG\n        PrintSubsequences();\n    #endif\n        return 0;\n    }\n    ```\n\n7.  将`DEBUG`设置为`1`，并使用`ABCX`和`ACYXB`作为输入，将产生以下输出:\n\n    ```cpp\n    Length of the longest common subsequence of ABCX and ACYXB is: 3\n    SIZE = 1\n        A___ A____\n    SIZE = 2\n        AB__ A___B\n        A_C_ AC___\n        A__X A__X_\n    SIZE = 3\n        A_CX AC_X_\n    ```\n\n这个输出向我们展示了子序列对的所有可能组合。让我们在下一节中分析这个输出，并努力优化我们的解决方案。\n\n### 优化的第一步——找到最佳子结构\n\n让我们再次回顾一下我们之前的方法的逻辑，看看如何对其进行优化。使用上一个练习的输入字符串`ABCX`和`ACYXB`，如果我们当前的状态有`i = 0`和`j = 0`，我们可以清楚地看到我们下一个状态的唯一可能性如下:\n\n```cpp\nLCS(A, B, 0, 0) = 1 + LCS(A, B, 1, 1)\n```\n\n大家可能还记得，我们最初的见解之一是，如果一个或两个弦都是空的，LCS 等于`0`。我们还可以推广`A`给定前缀和`B`给定前缀的 LCS 等于 A 的前缀用`B`减少一个字符，【T4 的】前缀用`A`减少一个字符的最大 LCS:\n\n```cpp\nA = \"ABC\"\nB = \"AXB\"\nLCS of \"ABC\", \"AXB\" \n= max(LCS of \"AB\" and \"AXB\", LCS of \"ABC\" and \"AX\") \n= LCS of \"AB\" and \"AXB\"\n= \"AB\"\n```\n\n使用基于前缀 LCS 的两个字符串的 LCS 的概念，我们可以重新定义我们的逻辑如下:\n\n```cpp\nIf prefix for either string is empty:\n   LCS = 0\nOtherwise:\n   If character in last position of A's prefix is equal to character in last position of B's prefix:\n         LCS is equal to 1 + LCS of prefix of A with last character removed and prefix of B with last character removed\n   Otherwise:\n          LCS is equal to maximum of:\n            1) LCS of A's current prefix and B's prefix with last character removed \n            2) LCS of B's current prefix and A's prefix with last character removed \n```\n\n使用记忆，我们可以将每一步的结果存储在二维表格中，第一维等于`A`的大小，第二维等于`B`的大小。假设我们还没有到达基本情况，我们可以检查我们是否有一个缓存的结果存储在`memo[i - 1][j - 1]`中。如果我们做了，我们返回结果；如果没有，我们以与之前相同的方式递归地探索可能性，并相应地存储结果。我们将在下面的活动中实现这一点。\n\n### 活动 19:利用记忆寻找最长的公共子序列\n\n在解决子集和问题时，我们实现了各种方法，即蛮力、回溯、记忆和制表。在本练习中，您的任务是使用记忆独立实现最长公共子序列问题的解决方案。\n\n**输入**\n\n两根弦，分别为 *A* 和 *B* 。\n\n**输出**\n\n*A* 和 *B* 的最长公共子序列的长度。\n\n**测试用例**\n\n以下测试用例应该有助于您更好地理解这个问题:\n\n![Figure 8.16: Activity 19 test cases ](img/C14498_08_16.jpg)\n\n###### 图 8.16:活动 19 测试用例\n\n**活动指南:**\n\n*   可以用两个维度来表示状态，第一个维度以 *A* 的长度为界，第二个维度以 *B* 的长度为界。\n*   要将强力算法转换成记忆算法，几乎不需要做什么改动。\n*   Make sure your approach has a way to differentiate between subproblems that have already been cached versus those that have not.\n\n    #### 注意\n\n    这个活动的解决方案可以在第 563 页找到。\n\n### 从自上而下到自下而上–将记忆方法转换为列表方法\n\n如果我们打印出一对字符串`ABCABDBEFBA`和`ABCBEFBEAB`的备忘表的值，它将是这样的(注意`-1`的值是未知的):\n\n![Figure 8.17: Memo table for ABCABDBEFBA and ABCBEFBE](img/C14498_08_17.jpg)\n\n###### 图 8.17:abcbbefba 和 ABCBEFBE 的备忘录表\n\n查找任意字符相等的行/列组合(比如第 7 行和第 7 列)，我们注意到一个模式:`memo[i][j]`处的值等于`memo[i - 1][j - 1] + 1`。\n\n现在我们来看另一种情况(即人物不对等)；我们看到的模式是`memo[i][j]`等于`memo[i - 1][j]`和`memo[i][j - 1]`的最大值。\n\n假设我们已经找到了问题的最优子结构，那么仅仅通过获取记忆解产生的表格并设计一个自下而上的构建方案，使用制表来形成一个解，通常是一项相当简单的任务。我们需要用稍微不同的方式来表述我们的一些逻辑，但是总体思路基本上是相同的。首先要处理的差异是备忘录表的值被初始化为`UNKNOWN` ( `-1`)。请记住，列表式解决方案会用适当的结果填充整个*表，因此当算法完成时，没有什么是*未知的。**\n\n我们取第二行第三列的未知值；这应该等于什么？假设我们考虑的那个点的前缀是`AB_________`和`ABC_______`，那么应该相当清楚，这个点的 LCS 等于`2`。现在我们来考虑一下第 10 行第 9 列的未知值:我们此时考虑的前缀是`ABCABDBEFB_`和`ABCBEFBEA_`，此时找到的 LCS 是`ABC_B__EFB_` — > `ABCBEFB___`，7 个字符长。我们可以从逻辑上推导出，给定状态下的 LCS 值要么等于先前发现的 LCS，要么大于先前发现的 LCS(如果字符相等)。当然，最低可能的 LCS 值应该等于 0。因此，我们迭代填充 DP 表的逻辑如下所示:\n\n```cpp\nIf i = 0 or j = 0 (empty prefix):\n  LCS(i, j) = 0\nOtherwise:\n  If the last characters of both prefixes are equal:\n    LCS(i, j) = LCS(i - 1, j - 1) + 1\n  Otherwise:\n    LCS(i, j) = Maximum of:\n        LCS(i - 1, j)  LCS for A's current prefix and B's prefix with the last character removed \n        LCS(i, j - 1)  LCS for B's current prefix and A's prefix with the last character removed\n```\n\n我们的逻辑本质上与记忆化解决方案相同，只是我们没有递归地找到未探索状态的值来填充表中当前状态的值，而是先填充这些状态的值，然后根据需要简单地重用它们。我们将在下面的活动中把这个逻辑放入代码中。\n\n### 活动 20:使用列表寻找最长的公共子序列\n\n在本练习中，您的任务是使用制表来实现最长公共子序列问题的自下而上的解决方案。\n\n**输入**\n\n两根弦，分别为 *A* 和 *B* 。\n\n**输出**\n\n*A* 和 *B* 的最长公共子序列的长度。\n\n**额外积分**\n\n除了 LCS 的长度，还输出它包含的实际字符。\n\n**测试用例**\n\n以下测试用例应该有助于您更好地理解这个问题:\n\n![Figure 8.18: Activity 20 test cases ](img/C14498_08_18.jpg)\n\n###### 图 8.18:活动 20 测试用例\n\n**活动指南**\n\n*   像子集和问题一样，列表解需要迭代两个嵌套的`for`循环。\n*   对于给定的状态`LCS(I, j)`，有三种可能需要处理——要么字符串的前缀为空， *A* 和 *B* 的前缀的最后字符相等，要么 *A* 和 *B* 的前缀的最后字符不相等。\n*   Finding the characters of the LCS can be done by backtracking through the DP table.\n\n    #### 注意\n\n    这个活动的解决方案可以在第 568 页找到。\n\n## 活动 21:旋律排列\n\n#### 注意\n\n这项活动是围绕传统的西方 8 音等律音阶展开的，尽管学生们不需要了解任何音乐理论就可以进行这项活动。这里提供了所有关于音乐方面的必要信息。\n\n音乐集合论是根据音符间的关系对音乐和声和旋律进行分类的一种形式。在音乐术语中，音程可以定义为用乐谱书写时两个音符之间相对位置的距离:\n\n![Figure 8.19: Musical notations](img/C14498_08_19.jpg)\n\n###### 图 8.19:音乐符号\n\n下图展示了以乐谱形式表示的不同音符之间的距离:\n\n![Figure 8.20: Musical intervals](img/C14498_08_20.jpg)\n\n###### 图 8.20:音乐音程\n\n你是一个音乐理论家，他很好奇一个特定音符组合在不同作曲家的旋律中出现了多少次。给定一个完整旋律的音符和一组音符，计算音符组的任何[排列](http://www.apple.com)出现在旋律中的次数。对于任何有效的排列，音符可以重复任何次数，并且可以以任何顺序出现:\n\n```cpp\n               0    1    2    3    4    5   6\nMelody:     { \"A\", \"B\", \"C\", \"C\", \"E\", \"C, \"A\" }\nNote set:     { \"A\", \"C\", \"E\" }\nSubsets:\n    { 0, 2, 4 }    —>    { \"A\", \"C\", \"E\" }\n    { 0, 3, 4 }    —>    { \"A\", \"C\", \"E\" }\n    { 0, 4, 5 }    —>    { \"A\", \"E\", \"C\" }\n    { 2, 4, 6 }    —>    { \"C\", \"E\", \"A\" }\n    { 3, 4, 6 }    —>    { \"C\", \"E\", \"A\" }\n    { 4, 5, 6 }    —>    { \"E\", \"C\", \"A\" }\n\n    { 0, 2, 3, 4 }    —>    { \"A\", \"C\", \"C\", \"E\" }\n    { 0, 2, 4, 5 }    —>    { \"A\", \"C\", \"E\", \"C\" }\n    { 0, 2, 4, 6 }    —>    { \"A\", \"C\", \"E\", \"A\" }\n    { 0, 3, 4, 5 }    —>    { \"A\", \"C\", \"E\", \"C\" }\n    { 0, 3, 4, 6 }    —>    { \"A\", \"C\", \"E\", \"A\" }\n    { 0, 4, 5, 6 }    —>    { \"A\", \"E\", \"C\", \"A\" }  \n    { 2, 3, 4, 6 }    —>    { \"C\", \"C\", \"E\", \"A\" }\n    { 2, 4, 5, 6 }    —>    { \"C\", \"E\", \"C\", \"A\" }\n    { 3, 4, 5, 6 }    —>    { \"C\", \"E\", \"C\", \"A\" }\n    { 0, 2, 3, 4, 5 }       —>    { \"A\", \"C\", \"C\", \"E\", \"C\" }\n    { 0, 2, 3, 4, 6 }       —>    { \"A\", \"C\", \"C\", \"E\", \"A\" }\n    { 0, 2, 4, 5, 6 }       —>    { \"A\", \"C\", \"E\", \"C\", \"A\" }\n    { 0, 3, 4, 5, 6 }       —>    { \"A\", \"C\", \"E\", \"C\", \"A\" }\n    { 2, 3, 4, 5, 6 }       —>    { \"C\", \"C\", \"E\", \"C\", \"A\" }\n\n    { 0, 2, 3, 4, 5, 6 }    —>    { \"A\", \"C\", \"C\", \"E\", \"C\", \"A\" }\nTotal Permutations = 21\n```\n\n以下注释被描述为*在声学上等价的*，并且应该被认为是相同的:\n\n```cpp\nC  — B# (B# is pronounced as \"B sharp\")\nC# — Db (Db is pronounced as \"D flat\")\nD# — Eb\nE  — Fb\nE# — F\nF# — Gb\nG# — Ab\nA# — Bb\nB  — Cb\n```\n\n下图说明了钢琴上的这种等效性:\n\n![Figure 8.21: Enharmonically equivalent notes represented on a section of a piano](img/C14498_08_21.jpg)\n\n###### 图 8.21:在钢琴的一部分上表现出的声学上等价的音符\n\n因此，下列音符组合将被视为等同:\n\n```cpp\n{ A#, B#, C# }   = { Bb, C, Db },\n{ Fb, Db, Eb }   = { E, C#, D# },\n{ C, B#, E#, F } = { C, C, F, F }\nAnd so on…\n```\n\n以下是一些示例输入和相应的输出:\n\n输入:\n\n```cpp\nMelody:    { \"A\", \"B\", \"C\", \"C\", \"E\", \"C, \"A\" }\nNote Set:    { \"A\", \"C\", \"E\" }\n```\n\n输出:`21`\n\n输入:\n\n```cpp\nMelody:    { \"A\", \"B\", \"D\", \"C#\", \"E\", \"A\", \"F#\", \"B\", \"C\", \"C#\", \"D\", \"E\" }\nNote Set:    { \"B\", \"D\", \"F#\", \"E\" }\n```\n\n输出:`27`\n\n输入:\n\n```cpp\nMelody:    { \"Bb\", \"Db\", \"Ab\", \"G\", \"Fb\", \"Eb\", \"G\", \"G\", \"Ab\", \"A\", \"Bb\", \"Cb\", \"Gb\", \"G\", \"E\", \"A\", \"G#\" }\nNote Set:    { \"Ab\", \"E\", \"G\" }\n```\n\n输出:`315`\n\n输入:\n\n```cpp\nMelody:    { \"C\", \"C#\", \"D\", \"Bb\", \"E#\", \"F#\", \"D\", \"C#\", \"A#\", \"B#\", \"C#\", \"Eb\", \"Gb\", \"A\", \"A#\", \"Db\", \"B\", \"D#\" }\nNote Set:    { \"Bb\", \"C#\", \"D#\", \"B#\" }\n```\n\n输出:`945`\n\n输入:\n\n```cpp\nMelody:    { \"A#\", \"B\", \"D#\", \"F#\", \"Bb\", \"A\", \"C\", \"C#\", \"Db\", \"Fb\", \"G#\", \"D\", \"Gb\", \"B\", \"Ab\", \"G\", \"C\", \"Ab\", \"F\", \"F#\", \"E#\", \"G\", \"Db\" }\nNote Set:    { \"A\", \"Db\", \"Gb\", \"A#\", \"B\", \"F#\", \"E#\" }\n```\n\n输出:`1323`\n\n这项活动的指导方针如下:\n\n*   除了描述中解释的以外，你实际上不需要知道任何关于音乐理论的知识来解决这个问题。\n*   有没有更好的方法来表现音符？它们能被转换成一种更适合于表格 DP 解决方案的格式吗？\n*   What is the total count of subsets for *n* elements? Could this bit of information be useful in solving this problem?\n\n    #### 注意\n\n    这个活动的解决方案可以在第 574 页找到。\n\n## 总结\n\n在这一章中，我们分析并实现了两个典型的动态规划例子，并学习了几种处理不同动态规划问题的方法。我们还学习了如何识别可以用动态规划解决的问题的特征，如何从概念上考虑动态规划算法，以及如何使用状态、基本情况和递归关系的概念将复杂的问题分解成更简单的组件。\n\n我们仅仅触及了动态编程技术的表面。事实上，我们深入探讨的两个问题实际上非常相似，无论是在概念上还是在解决方案的实现方式上。然而，这些相似之处中的许多有助于展示几乎每个 DP 问题中遇到的几个共性，因此，它们是对公认相当复杂且难以掌握的主题的极好介绍。\n\n使用动态编程是一项技能，仅仅通过阅读或观察是不可能提高的。真正用这种技术变得更好的唯一方法是用它解决尽可能多的问题，最好没有指导。起初，在找到最佳解决方案之前，某些困难的动态规划问题可能需要多次尝试，但是您通过这一通常是艰巨的过程获得的经验可以说比您通过简单地研究任何数量的动态规划问题的解决方案可能获得的经验要多得多。\n\n本章中演示的解决 DP 问题的渐进方法可以在未来很好地为您服务，但绝不是达到最终解决方案的唯一方法。在解决了许多 DP 问题之后，您无疑会开始注意到某些模式，这些模式可以从一开始就设计出列表式解决方案。但是，在遇到一系列不同的 DP 问题之前，这些模式不太可能被发现。请记住，有了 DP，就像任何具有挑战性的技能一样，持续的练习会让它变得更容易，而且，用不了多久，原本看起来极其令人生畏的事情最终会变得完全可控，甚至相当有趣！\n\n在最后一章中，我们将学习如何将动态编程应用于更高级的情况，并更深入地了解乍看之下彼此差异很大的 DP 问题通常只是同一组概念的变体。最后，我们将通过重新讨论图的主题来演示 DP 范式如何有效地应用于最短路径问题，从而结束这本书。"
  },
  {
    "path": "docs/cpp-dsal-design-principle/09.md",
    "content": "# 九、动态规划二\n\n## 学习目标\n\n本章结束时，您将能够:\n\n*   描述如何在多项式时间和非确定性多项式时间内解决问题，以及这对我们开发高效算法能力的影响\n*   实现 0-1 和无界背包问题的解决方案\n*   将状态空间约简的概念应用于动态规划问题\n*   使用动态编程范式优化的方法确定加权图中的每条最短路径\n\n在本章中，我们将基于对动态编程方法的理解，研究如何使用它来优化我们在上一章中讨论的问题。\n\n## 简介\n\n从上一章开始，您应该对动态编程有了基本的了解，以及一套有效的策略来为一个不熟悉的问题找到动态编程(DP)解决方案。在本章中，我们将通过探索问题之间的关系来进一步发展这种理解，特别是关于如何修改一个问题的基本动态规划逻辑来找到另一个问题的方法。我们还将讨论状态空间缩减的概念，它允许我们利用问题的某些方面，通过减少寻找结果所需的维度和/或操作的数量来进一步优化有效的动态规划解决方案。我们将通过重新讨论图的主题来演示 DP 方法如何应用于最短路径问题，从而结束本章。\n\n## 磷与磷的概述\n\n在*第 8 章*、*动态规划 I、*中，我们展示了动态规划相对于其他方法所能提供的显著效率提升，但可能还不清楚这种差异有多大。重要的是要理解某些问题的复杂性将随着输入边界的增加而扩展的程度，因为这样我们就可以理解 DP 不仅是优选的，而且是必要的情况。\n\n考虑以下问题:\n\n*“给定布尔公式的项和运算符，确定它的计算结果是否为真。”*\n\n看看下面的例子:\n\n```cpp\n(0 OR 1)  —> TRUE\n(1 AND 0) —> FALSE\n(1 NOT 1) —> FALSE\n(1 NOT 0) AND (0 NOT 1) —> TRUE\n```\n\n这个问题在概念上很容易解决。获得正确结果所需的只是对给定公式的线性评估。然而，想象一下，问题是这样表述的:\n\n*“给定布尔公式的变量和运算符，确定是否存在对每个变量的真/假赋值，以便公式计算为真。”*\n\n看看下面的例子:\n\n```cpp\n(a1 OR a2) —> TRUE \n        (0 ∨ 0) = FALSE\n        (0 ∨ 1) = TRUE\n        (1 ∨ 0) = TRUE\n        (1 ∨ 1) = TRUE\n(a1 AND a2) —> TRUE\n        (0 ∧ 0) = FALSE\n        (0 ∧ 1) = FALSE\n        (1 ∧ 0) = FALSE\n        (1 ∧ 1) = TRUE\n(a1 NOT a1) —> FALSE \n        (0 ¬ 0) = FALSE\n        (1 ¬ 1) = FALSE\n(a1 NOT a2) AND (a1 AND a2) —> FALSE \n        (0 ¬ 0) ∧ (0 ∧ 0) = FALSE\n        (0 ¬ 1) ∧ (0 ∧ 1) = FALSE\n        (1 ¬ 0) ∧ (1 ∧ 0) = FALSE\n        (1 ¬ 1) ∧ (1 ∧ 1) = FALSE\n```\n\n#### 注意:\n\n如果你不熟悉逻辑符号，`¬`表示`NOT`，因此有`(1 ¬ 1) = FALSE`和`(1 ¬ 0) = TRUE`。另外，`∧`表示`AND`，而`∨`表示`OR`。\n\n基本的基本概念保持不变，但这两个问题之间的差异是巨大的。在最初的问题中，找到结果的复杂性只取决于一个因素——公式的长度——但是这样说，似乎没有明显的方法来解决它，不需要搜索变量赋值的每个可能的二进制子集，直到找到解决方案。\n\n现在，让我们考虑另一个问题:\n\n*“给定一个图，其中每个顶点被分配了三种可能的颜色之一，确定是否没有两个相邻的顶点是相同的颜色。”*\n\n像我们的第一个例子一样，这很容易实现——遍历图的每个顶点，将其颜色与其每个邻居进行比较，并且只有在找到一对匹配的相邻颜色时才返回 false。但是现在，假设问题如下:\n\n*“给定一个图，其中每个顶点被分配了三种可能的颜色之一，确定是否有可能给它的顶点着色，使得没有两个邻居共享相同的颜色。”*\n\n同样，这是一个非常不同的场景。\n\n这些问题的第一个版本一般归类为 **P** ，简单的说就是有办法在**多项式时间**内解决。当我们描述一个问题的时间复杂度为 *O(n)* 、 *O(n* *2* *)* 、 *O(log n)* 等等时，我们是在描述 *P* 类内的一个问题。然而，重述的形式——至少就目前任何人所能证明的来说——没有现有的方法来找到一个在最坏情况下复杂性不是指数级的解决方案。因此，我们将其复杂度分为 **NP** ，或者**非确定多项式时间**。\n\n这几类问题之间的关系是一个相当有争议的话题。特别令人感兴趣的是*验证*解所需的计算复杂性是“容易的”，而*产生*解的复杂性是“困难的”。这证明了编程中最广泛讨论的未解决问题之一:解的验证是在类 *P* 中这一事实是否意味着也有一种在多项式时间内产生解的方法？换句话说， *P = NP* 吗？虽然这个问题的普遍假设答案是否定的(T8)P≠NP，但这还有待证明，这样做(不管答案实际上是什么)将是算法和计算研究中真正革命性的进步。\n\n可以说，NP 中最有趣的一组问题被称为 **NP-complete** ，因为它们有一个显著的特点:如果发现一个解决方案可以有效地解决其中的任何一个问题(即在多项式时间内)，这个解决方案实际上可以被修改以有效地解决 *NP* 中的所有其他问题。换句话说，如果找到了第一个例子的多项式解(称为**布尔可满足性问题**，或 **SAT** ，相同逻辑的一些变体也可以用于解决第二个例子(称为**图着色问题**，反之亦然。\n\n请记住，并不是每一个指数级的复杂问题都适合这个分类。考虑在国际象棋比赛中决定下一个最佳棋步的问题。您可以如下描述递归逻辑:\n\n```cpp\n    For each piece, a, belonging to the current player:\n        Consider every possible move, m_a, that a can make:\n\n            For each piece, b, belonging to the opponent:\n                Consider each possible move m_b that b can make\n                in response to m_a.\n                    for each piece, a, belonging to the \n                    current player…\n                    (etc.)\n        Count number of ways player_1 can win after this move\nNext best move is the one where the probability that player_1 wins is maximized.\n```\n\n寻找解决方案的复杂性无疑是指数级的。但是这个问题并不符合*NP*-完备性的标准，因为验证某个招式是否最佳的基本动作，需要同样程度的复杂度。\n\n将此示例与解决数独难题进行比较:\n\n![Figure 9.1: A solved Sudoku puzzle](img/C14498_09_01.jpg)\n\n###### 图 9.1:一个已解决的数独难题\n\n验证需要扫描矩阵的每一行和每一列，并确定九个轮廓为 3×3 的正方形中的每一个都包含从 1 到 9 的每个数字，并且没有一行或一列包含相同的数字超过一次。一个简单的实现可以使用九个集合的三个集合，每个集合包含`{ 1, 2, 3, 4, 5, 6, 7, 8, 9 }`，第一个集合代表每行中的数字，第二个集合代表每列中的数字，第三个集合代表每个 3×3 正方形中的数字。当扫描每个单元格时，我们会检查它包含的数字是否在对应于该单元格的每个集合中；如果是，它将从集合中移除。否则，结果是*假*。一旦考虑了每个单元，如果每个单元都是空的，结果等于*真*。由于这种方法只需要我们迭代一次矩阵，我们可以得出结论，它可以在多项式时间内求解。然而，假设提供的谜题是不完整的，任务是确定是否存在解，我们将不得不递归地考虑每个单元格的每个数字组合，直到找到有效的解，导致最坏情况的复杂性为*O(9**【n**)*， *n* 等于原始网格中的空方块数；因此，我们可以得出结论，解决数独难题是在 *NP* 中。\n\n## 子集和问题的再思考\n\n在前一章中，我们讨论了子集和问题，我们看到它在最坏的情况下具有指数复杂性。让我们考虑一下这个问题的两种表达方式——寻找解决方案和验证解决方案有效性的相对难度。\n\n让我们考虑验证解决方案有效性的问题:\n\n```cpp\nSet    —> { 2 6 4 15 3 9 }\nTarget —> 24\nSubset —> { 2 6 4 }\nSum = 2 + 6 + 4 = 12 \nFALSE\nSubset —> { 2 6 15 3 }\nSum = 2 + 6 + 15 + 3 = 24\nTRUE\nSubset —> { 15 9 }\nSum = 15 + 9 = 24\nTRUE\nSubset —> { 6 4 3 9 }\nSum = 6 + 4 + 3 + 9 = 22\nFALSE\n```\n\n毫无疑问，关于每个子集的长度，验证的复杂性是线性的——将所有数字相加，并将总和与目标进行比较——这将它直接放在 P 类中。我们发现了一些看似有效的方法来处理寻找解决方案的复杂性，我们可能假设其多项式时间复杂性为 *O(N × M)* ，其中 *N* 是集合的大小， *M* 是目标和。这似乎会取消这个问题是*NP*-完成。然而，事实并非如此，因为 *M* 不是输入的大小，而是它的大小。请记住，计算机用二进制表示整数，需要更多位数来表示的整数也需要更长的处理时间。因此，每次 M 的最大值增加一倍，就需要两倍的计算时间。\n\n因此，不幸的是，我们的动态规划解决方案不具备多项式复杂性。因此，我们将解决这个问题的方法定义为在`pseudo-polynomial time`中运行，我们可以得出结论，子集和问题实际上是*NP*-完全的。\n\n## 背包问题\n\n现在，让我们重新考虑一下我们在*第 5 章*、*贪婪算法*中看到的背包问题，我们可以将其描述为子集和问题的“老大哥”它要求如下:\n\n*“给定一个容量有限的背包和一组不同价值的加权物品，背包中可以包含哪组物品，在不超过容量的情况下产生最大的组合价值？”*\n\n这个问题也是 *NP* 完备性的一个典型例子，因此，它与这个类中的其他问题有许多密切的联系。\n\n考虑以下示例:\n\n```cpp\nCapacity —> 10 \nNumber of items —> 5\nWeights —> { 2, 3, 1, 4, 6 } \nValues —>  { 4, 2, 7, 3, 9 }\n```\n\n有了这些数据，我们可以生成以下子集:\n\n![Figure 9.2: All possible subsets for the given 0-1 knapsack problem](img/C14498_09_02.jpg)\n\n###### 图 9.2:给定 0-1 背包问题的所有可能子集\n\n这显然是熟悉的领域。这需要对子集和算法稍加修改吗？\n\n### 0-1 背包——子集和算法的扩展\n\n大家可能还记得我们在*第 6 章*、*图算法一*中的讨论，前面的例子是 0-1 背包问题。在这里，我们注意到当前算法和我们用来解决子集和问题的状态逻辑之间的另一个明显的相似之处。\n\n在子集和问题中，我们得出结论，对于每个元素`x`，在`set`中的索引`i`处，我们可以执行以下操作:\n\n1.  将`x`的值加到先前找到的子集和上。\n2.  保持子集和不变。\n\n这意味着在索引`i + 1`处的新总和`y`的 DP 表条目可以标记为`TRUE`，如果它如下:\n\n1.  表的前一行中的现有总和`x`，即`DP(i, x)`\n2.  `x`与`set[i]`当前元素的总和，即`DP(i, x + set[i])`\n\n换句话说，一个和是否可以由一个跨越集合中第一个`i`元素的子集形成取决于它是否已经被更早地找到，或者它是否可以通过将当前元素的值加到另一个先前找到的和中来找到。\n\n在当前问题中，我们可以观察到，对于每一个项目，`x`，在`set`中带有权重`w`的索引`i`处，我们可以执行以下任一操作:\n\n1.  将`x`的值加到先前找到的项目值的子集和上，只要相应项目与`w`的权重之和小于或等于最大容量。\n2.  保持子集和不变。\n\n反过来，这意味着在具有组合权重`W`的项目集合的索引`i + 1`处可以找到的最大值总和`y`可以是以下之一:\n\n1.  在先前的`i`项目中发现的现有最大值总和`x`，其组合重量为`w`\n2.  `x`与索引`i`处物品价值的总和，假设物品重量加到`w`时不超过容量\n\n换句话说，可以由跨越第一`i`项并具有组合权重`w`的项的子集形成的最大值总和等于对应于先前`i – 1`项的权重`w`的最大值总和，或者等于通过将当前项的值加到先前找到的子集的总值而产生的总和。\n\n在伪代码中，我们将子集和问题的表填充方案表达如下:\n\n```cpp\nfor sum (1 <= sum <= max_sum) found at index i of the set: \n   if sum < set[i-1]: \n    DP(i, sum) = DP(i-1, sum)\n   if sum >= set[i-1]:\n    DP(i, sum) = DP(i-1, sum) OR DP(i-1, sum - set[i-1])\n```\n\n0-1 背包问题的等价逻辑如下:\n\n```cpp\nfor total_weight (1 <= total_weight <= max_capacity) found at index i of the set:\n  if total_weight < weight[i]:\n     maximum_value(i, total_weight) = maximum_value(i-1, total_weight)\n  if total_weight >= weight[i]:\n     maximum_value(i, total_weight) = maximum of:\n        1) maximum_value(i-1, total_weight)\n        2) maximum_value(i-1, total_weight – weight[i]) + value[i]\n```\n\n在这里，我们可以看到一般的算法概念实际上是相同的:我们正在遍历由集合的大小和集合元素的最大和所界定的二维搜索空间，并确定是否可以找到新的子集和。不同之处在于，我们不仅仅记录某个子集和是否存在，而是收集与每个项目子集相关联的最大对应值和，并根据它们的总组合权重来组织它们。我们将在下面的练习中研究它的实现。\n\n### 练习 41: 0-1 背包问题\n\n我们现在将使用自下而上的列表方法实现前面的逻辑。让我们开始吧:\n\n1.  我们将从包含以下标题开始:\n\n    ```cpp\n    #include <iostream>\n    #include <vector>\n    #include <algorithm>\n    using namespace std;\n    ```\n\n2.  我们的第一步是处理输入。我们需要声明两个整数，`items`和`capacity`，分别代表可供选择的物品总数和背包的重量限制。我们还需要两个数组，`value`和`weight`，我们将在其中存储每个项目对应的数据:\n\n    ```cpp\n    int main()\n    {\n        int items, capacity;\n        cin >> items >> capacity;\n        vector<int> values(items), weight(items);\n        for(auto &v : values) cin >> v;\n        for(auto &w : weight) cin >> w;\n        ……\n    }\n    ```\n\n3.  现在，我们将定义函数`Knapsack_01()`，它有与输入相对应的参数，并返回一个整数:\n\n    ```cpp\n    int Knapsack_01(int items, int capacity, vector<int> value, vector<int> weight)\n    {\n        ……\n    }\n    ```\n\n4.  我们的 DP 表将是二维的，并且与我们在子集和问题中使用的表非常接近。在子集和表中，第一维的大小被初始化为比集合长度大一，而第二维的大小被初始化为比集合中所有元素的最大和大一。在这里，我们的第一维的大小将等效初始化为`items + 1`；同样，第二维的大小将被初始化为`capacity + 1` :\n\n    ```cpp\n    vector<vector<int>> DP(items + 1, vector<int>(capacity + 1, 0));\n    ```\n\n5.  我们需要从`1`开始迭代两个维度的长度。在外循环每次迭代的开始，我们将定义两个变量`currentWeight`和`currentValue`，分别对应于`weight[i-1]`和`values[i-1]`中的元素:\n\n    ```cpp\n    for(int i = 1; i <= items; i++)\n    {\n        int currentWeight = weight[i-1];\n        int currentValue = values[i-1];\n        for(int totalWeight = 1; totalWeight <= capacity; totalWeight++)\n        {\n            ……\n        }\n    }\n    ```\n\n6.  现在，我们将实现我们的制表方案:\n\n    ```cpp\n    if(totalWeight < currentWeight)\n    {\n        DP[i][totalWeight] = DP[i-1][totalWeight];\n    }\n    else \n    {\n        DP[i][totalWeight] = max(DP[i-1][totalWeight], DP[i-1][totalWeight - currentWeight] + currentValue);\n    }\n    ```\n\n7.  在函数的最后，我们返回表的最后一个元素:\n\n    ```cpp\n    return DP[items][capacity];\n    ```\n\n8.  现在，我们向`main()`添加一个调用，并打印输出:\n\n    ```cpp\n    int result = Knapsack_01(items, capacity, values, weight);\n    cout << \"The highest-valued subset of items that can fit in the knapsack is: \" << result << endl;\n    return 0;\n    ```\n\n9.  Let's try running our program using the following input:\n\n    ```cpp\n    8 66\n    20 4 89 12 5 50 8 13\n    5 23 9 72 16 14 32 4\n    ```\n\n    输出应如下所示:\n\n    ```cpp\n    The highest-valued subset of items that can fit in the knapsack is: 180\n    ```\n\n正如我们所看到的，背包问题的相对有效的动态规划解决方案只不过是对我们用来解决子集和问题的相同算法的轻微修改。\n\n### 无界背包\n\n我们探索的关于背包问题的实现是最传统的版本，但是正如我们在本章前面提到的，这个问题实际上有许多种类，可以应用于不同的场景。我们现在将考虑集合中每个项目的数量不受限制的情况。\n\n让我们考虑一个例子，我们通过蛮力找到解决方案:\n\n```cpp\nCapacity = 25\nValues —> { 5, 13, 4, 3, 8  }\nWeight —> { 9, 12, 3, 7, 19 }\n{ 0 } —> Weight = 9, Value = 5\n{ 1 } —> Weight = 12, Value = 13\n{ 2 } —> Weight = 3, Value = 4\n{ 3 } —> Weight = 7, Value = 3\n{ 4 } —> Weight = 32, Value = 8\n{ 0, 0 } —> Weight = 18, Value = 10\n{ 0, 1 } —> Weight = 21, Value = 18\n{ 0, 2 } —> Weight = 12, Value = 9\n{ 0, 3 } —> Weight = 16, Value = 8\n{ 0, 4 } —> Weight = 28, Value = 13\n{ 1, 1 } —> Weight = 24, Value = 26\n{ 1, 2 } —> Weight = 15, Value = 17\n{ 1, 3 } —> Weight = 19, Value = 16\n{ 1, 4 } —> Weight = 31, Value = 21\n{ 2, 2 } —> Weight = 6, Value = 8\n{ 2, 3 } —> Weight = 10, Value = 7\n{ 2, 4 } —> Weight = 22, Value = 12\n{ 3, 3 } —> Weight = 14, Value = 6\n{ 3, 4 } —> Weight = 26, Value = 11\n{ 4, 4 } —> Weight = 38, Value = 16\n{ 0, 0, 0 } —> Weight = 27, Value = 15\n{ 0, 0, 1 } —> Weight = 30, Value = 26\n{ 0, 0, 2 } —> Weight = 21, Value = 14\n{ 0, 0, 3 } —> Weight = 25, Value = 13\n{ 0, 0, 4 } —> Weight = 37, Value = 18\n{ 0, 1, 1 } —> Weight = 33, Value = 31\n……\n```\n\n从暴力的角度来看，这个问题似乎要复杂得多。让我们从 0-1 背包实现中重述我们的伪代码逻辑来处理这个额外的规定。\n\n在具有组合权重`total_weight`的项目集合的索引`i`处可以找到的最大值总和`y`可以是以下任一项:\n\n1.  在先前的`i - 1`项中发现的现有最大值总和`x`，其组合权重等于`total_weight`\n2.  Assuming `total_weight` can be formed by adding `current_weight` to some other subset's total weight found within the previous `i – 1` items:\n\n    a)当前项目的值与跨越先前`i - 1`项目且组合权重为`total_weight – current_weight`的子集的最大值之和的总和\n\n    b)当前项目的值与最近迭代中发现的一些先前的`y`的总和，其组合权重为`total_weight – current_weight`\n\n根据 DP 表，我们可以表示新的逻辑如下:\n\n```cpp\nfor total_weight (1 <= total_weight <= max_capacity) found at index i of the set:\n    if total_weight < set[i-1]:\n      maximum_value(i, total_weight) = maximum_value(i-1, total_weight)\n\n    if total_weight >= set[i-1]:\n      maximum_value(i, total_weight) = maximum of:\n        1) maximum_value(i-1, total_weight)\n        2) maximum_value(i-1, total_weight - current_weight) + current_value\n        3) maximum_value(i, total_weight - current_weight) + current_value\n```\n\n我们可以这样实现:\n\n```cpp\nauto max = [](int a, int b, int c) { return std::max(a, std::max(b, c)); };\nfor(int i = 1; i <= items; i++)\n{\n    int current_weight = weight[i—1];\n    int value = values[i-1];\n    for(int total_weight = 0; total_weight <= capacity; w++)\n    {\n        if(total_weight < current_weight)\n        {\n            DP[i][total_weight] = DP[i-1][total_weight];\n        }\n        else \n        {\n            DP[i][total_weight] = max\n            (\n                DP[i-1][total_weight], \n                DP[i-1][total_weight – current_weight] + value, \n                DP[i][total_weight – current_weight] + value\n            );\n        }\n    }\n}\n```\n\n从逻辑上讲，这种方法是可行的，但事实证明，这实际上并不是最有效的实现。让我们在下一节中了解它的局限性以及如何克服它们。\n\n### 状态空间约简\n\n有效使用动态规划的一个相当棘手的方面是**状态空间缩减**的概念，这是重新制定工作动态规划算法的行为，以使用表示一个状态所需的最小空间量。这通常归结为利用问题本质固有的某种模式或对称性。\n\n为了演示这个概念，让我们考虑在**帕斯卡三角形**的 *n* *第*行和 *m* *第*列中寻找值的问题，可以表示如下:\n\n![](img/C14498_09_03.jpg)\n\n###### 图 9.3:帕斯卡三角形\n\n帕斯卡三角形是根据以下逻辑建立的:\n\n```cpp\nFor m <= n:\n        Base case:\n            m = 1, m = n —> triangle(n, m) = 1\n        Recurrence: \n            triangle(n, m) = triangle(n-1, m-1) + triangle(n-1, m)\n```\n\n换句话说，每一行的第一个值是`1`，后面的每一列值等于前一行当前列和前一列的总和。从下图中可以看到，在第二行的第二列中，我们通过添加上一行的第二列(`1`)和第一列(`1`)中的元素得到了`2`:\n\n![Figure 9.4: Getting the next values in Pascal’s triangle](img/C14498_09_04.jpg)\n\n###### 图 9.4:获取帕斯卡三角形中的下一个值\n\n使用列表解决在第 *n* *第*行和第 *m* *第*列中查找值的问题可以如下进行:\n\n```cpp\nvector<vector<int>> DP(N + 1, vector<int>(N + 1, 0));\nDP[1][1] = 1;\nfor(int row = 2; row <= N; row++)\n{\n    for(int col = 1; col <= row; col++)\n    {\n        DP[row][col] = DP[row-1][col-1] + DP[row-1][col];\n    }\n}\n```\n\n在前面的代码中构建的 DP 表对于`N = 7`来说应该是这样的:\n\n![Figure 9.5: Pascal’s triangle represented as an N × N DP table](img/C14498_09_05.jpg)\n\n###### 图 9.5:帕斯卡三角形表示为一个 N × N DP 表\n\n正如我们所看到的，这种算法在内存使用和冗余计算方面都非常浪费。显而易见的问题是该表有 *N + 1* 列，尽管事实上只有一行包含这么多值。我们可以根据需要初始化每一行，根据所需的元素数量调整大小，从而轻松降低空间复杂度，这将表格所需的空间从 *N* *2* 减少到 *N × (N + 1) / 2* 。让我们修改我们的实现如下:\n\n```cpp\nvector<vector<int>> DP(N + 1);\nDP[1] = { 0, 1 };\nfor(int row = 2; row <= N; row++)\n{\n    DP[row].resize(row + 1);\n    for(int col = 1; col <= row; col++)\n    {            \n        int a = DP[row-1][col-1];\n        int b = DP[row-1][min(col, DP[row-1].size()-1)];\n        DP[row][col] = a + b;\n    }\n}\n```\n\n我们可以进一步观察到，每行的前半部分和后半部分之间存在对称关系，这意味着我们实际上只需要计算前(n/2)列的值。因此，我们有以下几点:\n\n```cpp\nDP(7, 7) ≡ DP(7, 1)\nDP(7, 6) ≡ DP(7, 2)\nDP(7, 5) ≡ DP(7, 3)\n```\n\n我们可以这样概括地说:\n\n```cpp\nDP(N, M) ≡ DP(N, N - M + 1)\n```\n\n考虑到这一点，我们可以修改我们的实现如下:\n\n```cpp\nvector<vector<int>> DP(N + 1);\nDP[0] = { 0, 1 };\nfor(int row = 1; row <= N; row++)\n{\n    int width = (row / 2) + (row % 2);\n    DP[row].resize(width + 2);\n    for(int col = 1; col <= width; col++)\n    {\n        DP[row][col] = DP[row-1][col-1] + DP[row-1][col];\n    }\n    if(row % 2 == 0) \n    {\n        DP[row][width+1] = DP[row][width];\n    }\n}\n……\nfor(int i = 0; i < queries; i++)\n{\n    int N, M;\n    cin >> N >> M;\n    if(M * 2 > N)\n    {\n        M = N - M + 1;\n    } \n    cout << DP[N][M] << endl;\n}\n```\n\n最后，假设我们能够提前接收输入查询并预计算结果，我们可以放弃完全存储整个表，因为只需要前一行就可以为当前行生成结果。因此，我们可以进一步修改我们的实现如下:\n\n```cpp\nmap<pair<int, int>, int> results;\nvector<pair<int, int>> queries;\nint q;\ncin >> q;\nint maxRow = 0;\nfor(int i = 0; i < q; i++)\n{\n    int N, M;\n    cin >> N >> M;\n    queries.push_back({N, M});\n\n    if(M * 2 > N) M = N - M + 1;\n    results[{N, M}] = -1; \n    maxRow = max(maxRow, N);\n}\nvector<int> prev = { 0, 1 };\nfor(int row = 1; row <= maxRow; row++)\n{\n    int width = (row / 2) + (row % 2);\n    vector<int> curr(width + 2);\n    for(int col = 1; col <= width; col++)\n    {\n        curr[col] = prev[col-1] + prev[col];\n        if(results.find({row, col}) != results.end())\n        {\n            queries[{row, col}] = curr[col];\n        }\n    }\n    if(row % 2 == 0)\n    {\n        curr[width + 1] = curr[width];\n    }\n    prev = move(curr);\n}\nfor(auto query : queries)\n{\n    int N = query.first, M = query.second;\n    if(M * 2 > N) M = N - M + 1;\n\n    cout << results[{N, M}] << endl;\n}\n```\n\n现在，让我们回到无界背包问题:\n\n```cpp\nCapacity     —>   12\nValues       —> { 5, 1, 6, 3, 4 }\nWeight       —> { 3, 2, 4, 5, 2 }\n```\n\n我们在上一节中提出的解决方案构建的 DP 表如下所示:\n\n![Figure 9.6: Two-dimensional DP table constructed by the proposed algorithm](img/C14498_09_06.jpg)\n\n###### 图 9.6:由提出的算法构建的二维 DP 表\n\n我们用来生成上表的逻辑是基于我们用来解决 0-1 形式的背包问题的方法，因此，我们假设给定的`weight`和`i`类型的项目，即`DP(i, weight)`，的最大值和可以如下:\n\n1.  相同重量和`i - 1`类型项目的最大值和，不包括当前项目，即`DP(i - 1, weight)`\n2.  当前项目的`value`与`i - 1`类项目的最大和之和，即`DP(i - 1, weight - w) + value`\n3.  当前项目的`value`与`i`类型项目的最大和的和，如果该项目被包含不止一次，即`DP(i, weight - w) + value`\n\n前两个条件对应 0-1 背包问题的逻辑。然而，在无界背包的上下文中考虑它们，并根据我们的算法生成的表检查它们，我们实际上可以得出结论，前两个条件本质上是不相关的。\n\n在最初的问题中，我们关心的是`i - 1`物品的值，因为我们需要决定是包括还是排除物品`i`，但是在这个问题中，我们没有理由排除任何物品，只要它们的重量不超过背包的容量。换句话说，支配每个状态转换的条件只受`weight`的限制，因此可以在一个维度上表示！\n\n这导致了一个必须做出的重要区分:模拟*状态所需的尺寸不一定与*描述*状态所需的尺寸相同。到目前为止，我们所研究的每一个 DP 问题，当被缓存时，都会产生一个本质上等同于状态本身的形式。然而，在无界背包问题中，我们可以如下描述每个状态:*\n\n*“对于重量为 w、价值为 v 的每一项，容量为 C 的背包的最大值等于 v 加上容量为 C–w 的背包的最大值”*\n\n考虑以下输入数据:\n\n```cpp\nCapacity —> 12\nValues   —> { 5, 1, 6, 3, 4 }\nWeight   —> { 3, 2, 4, 5, 2 }\n```\n\n在下表中，每一行代表从`0`到最大容量的重量`w`，每一列代表一个项目的指数`i`。每个单元格中的数字表示在考虑了索引`i`处的项目后，每个重量的最大值和:\n\n![Figure 9.7: Subproblem results for each weight-index pair ](img/C14498_09_07.jpg)\n\n###### 图 9.7:每个权重指数对的子问题结果\n\n如上表所示，允许重复意味着只要包含在最大容量内，就不需要排除任何项目。因此，在集合的索引 *0* 或索引 *1，000* 处是否可以找到权重和是无关紧要的，因为我们永远不会保持先前找到的子集和不变，除非对它的添加超过背包的定义界限。这意味着维护项目索引的记录没有任何好处，因为它允许我们将子问题缓存在一个维度中，即遇到的任意数量项目的组合权重。我们将在下面的练习中研究它的实现。\n\n### 练习 42:无界背包\n\n在本练习中，我们将通过在一维中表示我们的动态规划表，将状态空间约简的概念应用于无界背包问题。让我们开始吧:\n\n1.  让我们使用与上一个练习中相同的标题和输入:\n\n    ```cpp\n    #include <iostream>\n    #include <vector>\n    #include <algorithm>\n    using namespace std;\n    ……\n    int main()\n    {\n        int items, capacity;\n        cin >> items >> capacity;\n        vector<int> values(items), weight(items);\n        for(auto &v : values) cin >> v;\n        for(auto &w : weight) cin >> w;\n        ……\n    }\n    ```\n\n2.  现在，我们将实现一个名为`UnboundedKnapsack()`的返回整数的函数。其参数将与输入相同:\n\n    ```cpp\n    int UnboundedKnapsack(int items, int capacity, vector<int> values, vector<int> weight)\n    {\n        ……\n    }\n    ```\n\n3.  我们的 DP 表将表示为大小等于`capacity + 1`的整数向量，每个索引初始化为`0` :\n\n    ```cpp\n    vector<int> DP(capacity + 1, 0);\n    ```\n\n4.  像 0-1 背包问题一样，我们的状态逻辑将包含在两个嵌套循环中；然而，在这个问题的变体中，我们将反转循环的嵌套，使得外部循环从`0`迭代到`capacity`(包括 T1)，并且内部循环迭代通过项目索引:\n\n    ```cpp\n    for(int w = 0; w <= capacity; w++)\n    {\n        for(int i = 0; i < items; i++)\n        {\n            ……\n        }\n    } \n    ```\n\n5.  现在，我们必须决定如何缓存我们的状态。我们唯一担心的是所选物品的重量不会超过容量。由于我们的表格仅仅足够大，可以表示从`0`到`capacity`的重量值，所以我们只需要确保`w`和`weight[i]`之间的差值是非负数。因此，所有的赋值逻辑都可以包含在一个单独的`if`语句中:\n\n    ```cpp\n    for(int w = 0; w <= capacity; w++)\n    {\n        for(int i = 0; i < items; i++)\n        {\n            if(weight[i] <= w)\n            {\n                DP[w] = max(DP[w], DP[w - weight[i]] + values[i]);\n            }\n        }\n    }\n    return DP[capacity];\n    ```\n\n6.  现在回到`main()`，给`UnboundedKnapsack()`添加一个调用，输出结果:\n\n    ```cpp\n    int main()\n    {\n            ……\n        int result = UnboundedKnapsack(items, capacity, values, weight);\n        cout << \"Maximum value of items that can be contained in the knapsack: \" << result << endl;\n        return 0;\n    }\n    ```\n\n7.  Try running your program with the following input:\n\n    ```cpp\n    30 335\n    91 81 86 64 24 61 13 57 60 25 94 54 39 62 5 34 95 12 53 33 53 3 42 75 56 1 84 38 46 62 \n    40 13 4 17 16 35 5 33 35 16 25 29 6 28 12 37 26 27 32 27 7 24 5 28 39 15 38 37 15 40 \n    ```\n\n    您的输出应该如下所示:\n\n    ```cpp\n    Maximum value of items that can be contained in the knapsack: 7138\n    ```\n\n正如前面的实现所展示的，在 DP 算法中考虑成本较低的缓存解决方案的方法通常是值得的。看似需要复杂状态表示的问题，经过仔细研究后，往往可以大大简化。\n\n### 活动 22:利润最大化\n\n你在一家大型连锁百货公司工作。像任何零售企业一样，你的公司从批发经销商那里大量购买商品，然后以更高的价格出售，以获得利润。您的商店正在销售的某些类型的产品可以从多个不同的分销商处购买，但是产品的质量和价格可能会有很大差异，这自然会影响其相应的零售价值。一旦将汇率和公共需求等因素考虑在内，来自某些分销商的产品通常可以以比最终售价低得多的单价购买。你的任务是设计一个系统，计算出你可以用分配的预算获得的最大利润。\n\n我们已经向您提供了类似产品的目录。每个列出的产品都有以下信息:\n\n*   产品的批发价\n*   加价后销售同一产品所能获得的利润\n*   经销商每单位销售的产品数量\n\n考虑到分销商只会以指定的确切数量销售产品，您的任务是确定通过购买所列产品的某个子集可以赚到的最大金额。为了确保商店提供多种选择，列出的每件商品只能购买一次。\n\n由于您只有有限的仓库空间，并且不想积压特定类型的物品，因此您还可以限制可以购买的单个单位的最大数量。因此，您的计划还应确保购买的产品总数不超过此限制。\n\n**例**\n\n假设目录中列出了五个项目，包含以下信息:\n\n![](img/C14498_09_08.jpg)\n\n###### 图 9.8:利润优化的样本值\n\n你有 100 美元的预算和 20 个单位的仓库容量。以下几组购买将是有效的:\n\n```cpp\n{ A B }    Cost: 30     | Quantity: 15    | Value: 70\n{ A D }    Cost: 70     | Quantity: 13    | Value: 110\n{ A E }    Cost: 60     | Quantity: 14    | Value: 130\n{ B C }    Cost: 25     | Quantity: 17    | Value: 40\n{ C D }    Cost: 65     | Quantity: 15    | Value: 80\n{ C E }    Cost: 55     | Quantity: 16    | Value: 100\n{ D E }    Cost: 90     | Quantity: 7     | Value: 140\n{ A B D }  Cost: 80     | Quantity: 18    | Value: 130\n{ A B E }  Cost: 70     | Quantity: 19    | Value: 150\n{ B C D }  Cost: 75     | Quantity: 20    | Value: 100\n{ B D E }  Cost: 100    | Quantity: 12    | Value: 160\n```\n\n因此，程序应该输出`160`。\n\n**输入**\n\n第一行包含三个整数，`N`为分销商数量，`budget`为可花费的最大金额，`capacity`为可购买的最大单位数量。\n\n接下来的`N`行应该包含三个用空格分隔的整数:\n\n*   `quantity`:经销商提供的单位数量\n*   `cost`:物品的价格\n*   `value`:销售产品后可以获得的利润金额\n\n**输出**\n\n单个整数，表示通过从目录中选择某些项目子集可以获得的最大利润。\n\n**测试用例**\n\n下面的测试用例集应该可以帮助您更好地理解这个问题:\n\n![Figure 9.9: Activity 22 test case 1 ](img/C14498_09_09.jpg)\n\n###### 图 9.9:活动 22 测试用例 1\n\n![Figure 9.10: Activity 22 test case 2 ](img/C14498_09_10.jpg)\n\n###### 图 9.10:活动 22 测试用例 2\n\n![Figure 9.11: Activity 22 test case 3 ](img/C14498_09_11.jpg)\n\n###### 图 9.11:活动 22 测试用例 3\n\n![Figure 9.12: Activity 22 test case 4 ](img/C14498_09_12.jpg)\n\n###### 图 9.12:活动 22 测试用例 4\n\n**活动指南**\n\n*   所需的实现与 0-1 背包问题非常相似。\n*   Since there are two constraints (capacity and budget), the DP table will require three dimensions.\n\n    #### 注意\n\n    这个活动的解决方案可以在第 581 页找到。\n\n## 图和动态规划\n\n在本节中，我们将高级图算法和动态规划作为明显不同的主题进行了讨论，但通常情况下，它们可以同时使用，具体取决于我们试图解决的问题类型和图的性质。通常与图相关的几个问题被确定为*NP*-完全(仅举两个例子，图着色和顶点覆盖问题)，并且在适当的情况下可以用动态规划来解决。然而，这些主题中的大多数都不在本书的范围之内(实际上值得让整本书专门用于它们的分析)。\n\n然而，图论中的一个问题特别适合 DP 方法，幸运的是，这是我们已经非常熟悉的一个问题:最短路径问题。事实上，在*第 7 章*、*图算法二*中，我们实际上讨论了一个通常被归类在 DP 保护伞下的算法，尽管事实上我们从未将其确定为这样。\n\n### 重新考虑贝尔曼-福特算法\n\n在我们对贝尔曼-福特算法的探索中，我们是根据我们之前对迪克斯特拉算法的讨论来看待它的，它肯定与迪克斯特拉算法有一些相似之处。但是现在我们已经牢固地掌握了动态编程范式背后的概念，让我们根据我们的新理解重新考虑 Bellman-Ford。\n\n简而言之，贝尔曼-福特公司采用的方法可以描述如下:\n\n给定一个名为`start`的源节点，图的顶点数`V`和边数`E`，执行以下操作:\n\n1.  将每个节点从`0`到`V – 1`(含)的距离标记为`UNKNOWN`，除了`start`，即`0`。\n2.  从`1`迭代到`V – 1`(含)。\n3.  每次迭代时，考虑`E`中的每条边，检查源节点各自的距离值是否为`UNKNOWN`。如果不是，则将相邻节点当前存储的距离与源节点的距离之和以及它们之间的边缘权重进行比较。\n4.  如果源节点的距离和边缘权重的总和小于目标节点的距离，请将目标节点的距离更新为较小的值。\n5.  在`V – 1`次迭代后，要么找到了最短路径，要么图的权重循环为负，这可以通过额外的边迭代来确定。\n\n该算法的成功显然依赖于问题表现出最优子结构的事实。我们可以如下说明这个概念背后的递归逻辑:\n\n![Figure 9.13: Visualizing the Bellman-Ford algorithm](img/C14498_09_13.jpg)\n\n###### 图 9.13:可视化贝尔曼-福特算法\n\n将它表示为伪代码看起来如下所示:\n\n```cpp\nSource —> A\nDestination —> E\nThe shortest path from A to E is equal to:\n    …the edge weight from A to B (4), plus…\n        …the shortest path from B to E, which is:\n            …the edge weight from B to C (3), plus:\n                …the edge weight from C to E (2).\n            …or the edge weight from B to E (9).\n    …or the edge weight from A to D (3), plus:\n        …the shortest path from D to E, which is:\n            …the edge weight from D to B (8), plus:\n                …the shortest path from B to E (9), which is:\n                    …the edge weight from B to C (3), plus:\n                        …the edge weight from C to E (2).\n                    …or the edge weight from B to E (9).\n            …the edge weight from D to C (3), plus:\n                …the edge weight from C to E (2).\n            …or the edge weight from D to E (7).\n```\n\n显然，最短路径问题也具有重叠子问题的性质。贝尔曼-福特有效地避免了由于两个关键的观察结果而导致的重新计算:\n\n*   图中任意两个节点之间的非循环遍历可以进行的最大移动次数为`| V – 1 |`(即图中每个节点减去起始节点)。\n*   N 次迭代后源节点和每个可达节点之间的最短路径相当于`| N – 1 |`次迭代后每个可达节点的最短路径，加上每个相邻节点的边权重。\n\n下面的一组数字将帮助您更好地可视化贝尔曼-福特算法中的步骤:\n\n![Figure 9.14: Bellman-Ford Step 1](img/C14498_09_14.jpg)\n\n###### 图 9.14:行李员-福特第一步\n\n![Figure 9.15: Bellman-Ford Step 2](img/C14498_09_15.jpg)\n\n###### 图 9.15:行李员-福特步骤 2\n\n![Figure 9.16: Bellman-Ford Step 3](img/C14498_09_16.jpg)\n\n###### 图 9.16:行李员-福特步骤 3\n\n据说 Bellman-Ford 解决的具体问题被称为**单源最短路径问题**，因为它是用来寻找单个节点的最短路径的。在*第 7 章*、*图算法二*中，我们讨论了约翰逊算法，该算法解决了所谓的**全对最短路径问题**，因为它找到了图中每对顶点之间的最短路径。\n\n约翰逊的算法结合了贝尔曼-福特算法中的动态规划方法和迪克斯特拉算法中的贪婪方法。在本节中，我们将探索全对最短路径问题的完整 DP 实现。然而，让我们通过实现自上而下的解决方案来更深入地考虑问题的本质。\n\n### 将最短路径问题作为动态规划问题来处理\n\n更好地理解贝尔曼-福特背后的逻辑的一个方法是将其转化为自上而下的解决方案。为此，让我们从考虑基本案例开始。\n\n贝尔曼-福特通过图的边缘执行`V – 1`迭代，通常是通过`for`循环。由于我们之前的实现已经从`1`迭代到`V – 1`了，让我们从`V – 1`开始自顶向下的解决方案，递减到`0`。根据我们的递归结构，假设每个状态可以描述如下:\n\n```cpp\nShortestPath(node, depth)\nnode —> the node being considered\ndepth —> the current iteration in the traversal\n```\n\n因此，我们的第一个基本情况可以定义如下:\n\n```cpp\nif depth = 0:\n        ShortestPath(node, depth) —> UNKNOWN\n```\n\n换句话说，如果`depth`已经递减到`0`，我们可以断定不存在路径，并终止我们的搜索。\n\n当然，我们需要处理的第二个基本情况是找到从源到目标的路径。在这种情况下，搜索的深度无关紧要；从目标到自身的最短距离永远是`0`:\n\n```cpp\nif node = target: \n\n        ShortestPath(node, depth) —> 0\n```\n\n现在，让我们定义我们的中间状态。让我们回顾一下贝尔曼-福特公司使用的迭代方法是什么样的:\n\n```cpp\nfor i = 1 to V - 1:\n        for each edge in graph:\n            edge —> u, v, weight \n            if distance(u) is not UNKNOWN and distance(u) + weight < distance(v):\n                distance(v) = distance(u) + weight\n```\n\n就递归遍历而言，这可以重申如下:\n\n```cpp\nfor each edge adjacent to node:\n\n        edge —> neighbor, weight\n    if ShortestPath(neighbor, depth - 1) + weight < ShortestPath(node, depth):\n            ShortestPath(node, depth) = ShortestPath(neighbor, depth - 1) + weight\n```\n\n由于每个状态都可以根据这两个维度进行唯一描述，并且循环的可能存在意味着我们可能会不止一次地遇到相同的状态，因此我们可以得出结论，根据节点深度对进行缓存对于记忆目的既有效又有用:\n\n```cpp\nDepth = 7:\n    SP(0, 7): 0\n    SP(1, 7): 6\n    SP(2, 7): UNKNOWN\n    SP(3, 7): 12\n    SP(4, 7): UNKNOWN\n    SP(5, 7): UNKNOWN\n    SP(6, 7): 13\n    SP(7, 7): UNKNOWN\nDepth = 6:\n    SP(0, 6): 0\n    SP(1, 6): 6\n    SP(2, 6): 14\n    SP(3, 6): 12\n    SP(4, 6): UNKNOWN\n    SP(5, 6): UNKNOWN\n    SP(6, 6): 12\n    SP(7, 6): 15\nDepth = 5:\n    SP(0, 5): 0\n    SP(1, 5): 6\n    SP(2, 5): 14\n```\n\n下图说明了这些状态:\n\n![Figure 9.17: All the states for the shortest-path problem ](img/C14498_09_17.jpg)\n\n###### 图 9.17:最短路径问题的所有状态\n\n我们将在下面的练习中研究这种方法的实现。\n\n### 练习 43:单源最短路径(记忆化)\n\n在本练习中，我们将采用自顶向下的动态规划方法来寻找单源最短路径问题的解决方案。让我们开始吧:\n\n1.  让我们首先包含以下头和`std`名称空间，并定义一个`UNKNOWN`常量:\n\n    ```cpp\n    #include <iostream>\n    #include <vector>\n    #include <utility>\n    #include <map>\n    using namespace std;\n    const int UNKNOWN = 1e9;\n    ```\n\n2.  让我们也声明`V`和`E`(分别是顶点数和边数)，以及两个二维整数向量，`adj`(我们图的邻接表)和`weight`(边权值矩阵)。最后，我们将定义一个名为`memo`的记忆表。这一次，我们将使用`std::map`来简化检查缓存中是否存在某个键与它的值是否未知之间的区别:\n\n    ```cpp\n    int V, E;\n    vector<vector<int>> adj;\n    vector<vector<int>> weight;\n    map<pair<int, int>, int> memo;\n    ```\n\n3.  在`main()`函数中，我们应该处理输入，以便接收我们希望应用算法的图。输入的第一行包含`V`和`E`，后面的`E`行包含三个整数:`u`、`v`和`w`(分别是每个边的来源、目的地和权重):\n\n    ```cpp\n    int main()\n    {\n            int V, E;\n            cin >> V >> E;\n            weight.resize(V, vector<int>(V, UNKNOWN));\n            adj.resize(V);\n            for(int i = 0; i < E; i++)\n            {\n                int u, v, w;\n                cin >> u >> v >> w;\n                adj[u].push_back(v);\n                weight[u][v] = w;\n            }\n            …\n    }\n    ```\n\n4.  我们现在将定义一个名为`SingleSourceShortestPaths()`的函数，该函数将接受一个参数— `source`，它是源顶点的索引，并将返回一个整数向量:\n\n    ```cpp\n    vector<int> SingleSourceShortestPaths(int source)\n    {\n            ……\n    }\n    ```\n\n5.  Now we will need to make some preliminary modifications to our graph. As opposed to traversing from the source node to all the other nodes in the graph, we will instead begin each traversal from the other nodes and calculate the shortest path from the source in reverse. Since our graph is directed, we will have to use its transpose to accomplish this:\n\n    ```cpp\n    // Clear table\n    vector<vector<int>> adj_t(V);\n    vector<vector<int>> weight_t(V, vector<int>(V, UNKNOWN));\n    for(int i = 0; i < V; i++)\n    {\n            // Create transpose of graph\n            for(auto j : adj[i])\n            {\n                adj_t[j].push_back(i);\n                weight_t[j][i] = weight[i][j];\n            }\n            // Base case — shortest distance from source to itself is zero at any depth\n            memo[{source, i}] = 0;\n            if(i != source) \n            {\n                // If any node other than the source has been reached \n                // after V - 1 iterations, no path exists.\n                memo[{i, 0}] = UNKNOWN;\n            }\n    }\n    ```\n\n    这里，我们定义了两个新的二维整数向量`adj_t`和`weight_t`，它们将对应于转置图的邻接表和权重矩阵。然后，我们使用嵌套循环来创建修改后的图，并初始化`memo`表中的值。\n\n6.  我们现在应该用四个参数定义`ShortestPath_Memoization()`函数:两个整数，`depth`和`node`，以及`adj`和`weight`(在这种情况下，这将是转置图的引用):\n\n    ```cpp\n        int ShortestPath_Memoization(int depth, int node, vector<vector<int>> &adj, vector<vector<int>> &weight)\n    {\n            ……\n        }\n    ```\n\n7.  我们的算法本质上将是标准的深度优先搜索，除了我们将在每个函数调用结束时缓存每个`{ node, depth }`对的结果。在函数的顶部，我们将检查缓存的结果，如果关键字存在于映射中，则返回该结果:\n\n    ```cpp\n    // Check if key exists in map\n    if(memo.find({node, depth}) != memo.end())\n    {\n        return memo[{node, depth}];\n    }\n    memo[{node, depth}] = UNKNOWN;\n    // Iterate through adjacent edges\n    for(auto next : adj[node])\n    {\n        int w = weight[node][next];\n        int dist = ShortestPath_Memoization(depth - 1, next, adj, weight) + w;\n        memo[{node, depth}] = min(memo[{node, depth}], dist);\n    }\n    return memo[{node, depth}];\n    ```\n\n8.  回到`SingleSourceShortestPaths()`函数，我们将定义一个名为`V`的整数向量`distance`，并通过对`ShortestPath_Memoization()` :\n\n    ```cpp\n    vector<int> distance;\n\n    for(int i = 0; i < V; i++)\n    {\n        distance[i] = ShortestPath_Memoization(V - 1, i, adj_t, weight_t);\n    }\n    return distance;\n    ```\n\n    的连续调用来填充它\n9.  回到`main()`，我们将定义一个名为`paths`的二维整数矩阵，它将存储从`0`到`V` :\n\n    ```cpp\n    vector<vector<int>> paths(V);\n    for(int i = 0; i < V; i++)\n    {\n        paths[i] = SingleSourceShortestPaths(i);\n    }\n    ```\n\n    的每个节点索引从`SingleSourceShortestPaths()`返回的距离\n10.  我们现在可以使用`paths`表打印图中每对节点的距离值:\n\n    ```cpp\n    cout << \"The shortest distances between each pair of vertices are:\" << endl;\n    for(int i = 0; i < V; i++)\n    {\n            for(int j = 0; j < V; j++)\n            {\n              cout << \"\\t\" << j << \": \";\n              (paths[i][j] == UNKNOWN) ? cout << \"- \";\n                                       : cout << paths[i][j] << \" \";\n            }\n            cout << endl;\n    }\n    ```\n\n11.  Now, run your code with the following input:\n\n    ```cpp\n    8 20\n    0 1 387\n    0 3 38\n    0 5 471\n    1 0 183\n    1 4 796\n    2 5 715\n    3 0 902\n    3 1 712\n    3 2 154\n    3 6 425\n    4 3 834\n    4 6 214\n    5 0 537\n    5 3 926\n    5 4 125\n    5 6 297\n    6 1 863\n    6 7 248\n    7 0 73\n    7 3 874\n    ```\n\n    输出应如下所示:\n\n    ```cpp\n    The shortest distances between each pair of vertices are:\n    0: 0 387 192 38 596 471 463 711 \n    1: 183 0 375 221 779 654 646 894 \n    2: 1252 1639 0 1290 840 715 1012 1260 \n    3: 746 712 154 0 994 869 425 673 \n    4: 535 922 727 573 0 1006 214 462 \n    5: 537 924 729 575 125 0 297 545 \n    6: 321 708 513 359 917 792 0 248 \n    7: 73 460 265 111 669 544 536 0  \n    ```\n\n不出所料，这并不是处理这个特定问题的首选方式，但是与前面的练习一样，我们可以了解到很多关于如何通过实现像这样的递归解决方案来形成最佳子结构的知识。有了这些见解，我们现在可以完全理解如何使用制表同时找到每对节点之间的最短距离。\n\n### 全对最短路径\n\n我们上一个练习中的程序确实打印了每个顶点对的最短路径，但是它的效率大致相当于对 Bellman-Ford 执行`V`调用，增加了与递归算法相关的内存相关的缺点。\n\n谢天谢地，这个问题有一个非常有用的自下而上的算法，它被配备来处理其他人在 *O(V* *3* *)* 时间和 *O(V* *2* *)* 空间中可以处理的一切。这也非常直观，尤其是在实现了本书中的其他最短路径算法之后。\n\n### 弗洛伊德-沃肖尔算法\n\n到目前为止，我们应该对贝尔曼-福特算法如何利用最短路径问题中表现出的最优子结构有了相当清楚的了解。关键的一点是，两个图顶点之间的任何最短路径都将是从源点到连接路径端点和目标顶点的边的其他最短路径的组合。\n\n**弗洛伊德-沃肖尔算法**使用了同样的概念，通过进行更广泛的概括产生了巨大的效果:\n\n*“如果节点 A 和节点 B 的最短距离为 AB，节点 B 和节点 C 的最短距离为 BC，那么节点 A 和节点 C 的最短距离为 AB + BC。”*\n\n这个逻辑本身当然不是开创性的；但是，结合 Bellman-Ford 所展示的洞察力——图的边上的 *V* 迭代足以确定从一个源节点到图中每隔一个节点的最短路径——我们可以利用这一思想，以`Node A`为源，依次生成节点对之间的最短路径，然后利用这些结果生成`Node B`、`C`、`D`等的潜在最短路径。\n\n弗洛伊德-沃肖尔通过在顶点上执行 *V* *3* 迭代来实现这一点。第一维表示每对可能的顶点 *A* 和 *C* 之间的潜在中点 *B* 。然后，算法检查从 *A* 到 *C* 的当前已知距离值是否大于从 *A* 到 *B* 和 *B* 到 *C* 的最短已知距离之和。如果是，则确定该和至少更接近于 *A* 和 *C* 的最佳最短距离值，并将其缓存在表中。Floyd-Warshall 使用图中的每个节点作为中点进行这些比较，不断提高其结果的准确性。在针对每个可能的中点测试了每个可能的起点和终点对之后，表中的结果包含每对顶点的正确最短距离值。\n\n就像任何与图相关的算法一样，弗洛伊德-沃肖尔并不保证在每种给定的情况下都是最佳选择，应该始终考虑弗洛伊德-沃肖尔和其他替代方案之间的相对复杂性。一个很好的经验法则是对密集图(即包含大量边的图)使用 Floyd-Warshall。例如，假设你有一个有 100 个顶点和 500 条边的图。在每个起始顶点上连续运行贝尔曼-福特算法(最坏情况下的复杂度为 *O(V×E)* )可能会导致 500×100×100(或 5，000，000)次运算的总复杂度，而弗洛伊德-沃肖尔需要 100×100×100(或 1，000，000)次运算。迪克斯特拉的算法通常比贝尔曼-福特更有效，也可能是一个可行的替代方案。尽管如此，Floyd-Warshall 的一个明显优势是，算法的整体复杂性始终精确地为 *O(V* *3* *)* ，而不考虑输入图的其他属性。因此，除了顶点的数量之外，我们不需要知道关于我们正在使用的图的任何细节，就能够准确地确定弗洛伊德-沃肖尔将有多高效(或低效)。\n\n最后要考虑的一点是，像贝尔曼-福特(与迪克斯特拉算法不同)一样，弗洛伊德-沃肖尔能够处理具有负边权重的图，但也会受到没有显式处理的负边权重循环的阻碍。\n\n我们将在下面的练习中实现弗洛伊德-沃肖尔算法。\n\n### 练习 44:实现弗洛伊德-沃肖尔算法\n\n在本练习中，我们将使用弗洛伊德-沃肖尔算法找到每对顶点之间的最短距离。让我们开始吧:\n\n1.  我们将首先包含以下标题并定义一个`UNKNOWN`常数:\n\n    ```cpp\n    #include <iostream>\n    #include <vector>\n    using namespace std;\n    const int UNKNOWN = 1e9;\n    ```\n\n2.  让我们从处理输入开始，就像我们在前面的练习中所做的那样。然而，这次我们不需要图的邻接表表示:\n\n    ```cpp\n    int main()\n    {\n            int V, E;\n            cin >> V >> E;\n            vector<vector<int>> weight(V, vector<int>(V, UNKNOWN));\n            for(int i = 0; i < E; i++)\n            {\n                int u, v, w;\n                cin >> u >> v >> w;\n                weight[u][v] = w;\n            }\n            ……\n            return 0;\n    }\n    ```\n\n3.  我们的`FloydWarshall()`函数将接受两个参数— `V`和`weight`，并将返回最短路径距离的二维整数向量:\n\n    ```cpp\n    vector<vector<int>> FloydWarshall(int V, vector<vector<int>> weight)\n    {\n            ……\n    }\n    ```\n\n4.  让我们定义一个名为`distance`的二维 DP 表，并将每个值初始化为`UNKNOWN`。然后，我们需要为每对节点分配最初已知的最短距离“估计值”(即`weight`矩阵中的值)，以及基本情况值(即从每个节点到自身的最短距离，`0` ):\n\n    ```cpp\n        vector<vector<int>> distance(V, vector<int>(V, UNKNOWN));\n    for(int i = 0; i < V; i++)\n    {\n        for(int j = 0; j < V; j++)\n        {\n            distance[i][j] = weight[i][j];\n        }\n        distance[i][i] = 0;\n    }\n    ```\n\n5.  我们现在将执行从`0`到`V – 1`(含)的三个嵌套`for`循环，外部循环表示当前中间顶点`mid`，中间循环表示源顶点`start`，最内部循环表示目标顶点`end`。然后，我们将比较每个顶点组合之间的距离值，并在找到较短路径时，从头到尾重新分配距离值:\n\n    ```cpp\n    for(int mid = 0; mid < V; mid++)\n    {\n        for(int start = 0; start < V; start++)\n        {\n            for(int end = 0; end < V; end++)\n            {\n                if(distance[start][mid] + distance[mid][end] < distance[start][end])\n                {\n                    distance[start][end] = distance[start][mid] + distance[mid][end];\n                }\n            }\n        }\n    }\n    ```\n\n6.  Similar to Bellman-Ford, we will need to check for negative cycles if our input is expected to contain negative edge weights. Thankfully, this can be accomplished with great ease using the distance table.\n\n    考虑这样一个事实:图循环是一条长度大于零的路径，并且是起点和终点相同的地方。在表示每对节点之间距离的表格中，节点与其自身之间的最短路径将包含在`distance[node][node]`中。在只包含正边权重的图中，`distance[node][node]`中包含的值显然只能等于`0`；但是，如果图表包含负权重循环，`distance[node][node]`将为负。因此，我们可以这样测试负周期:\n\n    ```cpp\n    for(int i = 0; i < V; i++)\n    {\n            // If distance from a node to itself is negative, there must be a negative cycle\n            if(distance[i][i] < 0)\n            {\n                return {};\n            }\n    } \n    return distance;\n    ```\n\n7.  现在我们已经完成了算法的编写，我们可以在`main()`中调用`FloydWarshall()`并输出结果:\n\n    ```cpp\n    int main()\n    {\n        ……\n        vector<vector<int>> distance = FloydWarshall(V, weight);\n        // Graphs with negative cycles will return an empty vector\n        if(distance.empty())\n        {\n            cout << \"NEGATIVE CYCLE FOUND\" << endl;\n            return 0;\n        }\n        for(int i = 0; i < V; i++)\n        {\n            cout << i << endl;\n            for(int j = 0; j < V; j++)\n            {\n                cout << \"\\t\" << j << \": \";\n\n                (distance[i][j] == UNKNOWN) \n                    ? cout << \"_\" << endl \n                    : cout << distance[i][j] << endl;\n            }\n        }\n        return 0;\n    }\n    ```\n\n8.  让我们在以下输入集上运行我们的程序:\n\n    ```cpp\n    Input:\n    7 9\n    0 1 3\n    1 2 5\n    1 3 10\n    1 5 -4\n    2 4 2\n    3 2 -7\n    4 1 -3\n    5 6 -8\n    6 0 12\n    Output:\n    0:\n            0: 0\n            1: 3\n            2: 6\n            3: 13\n            4: 8\n            5: -1\n            6: -9\n    1:\n            0: 0\n            1: 0\n            2: 3\n            3: 10\n            4: 5\n            5: -4\n            6: -12\n    2:\n            0: -1\n            1: -1\n            2: 0\n            3: 9\n            4: 2\n            5: -5\n            6: -13\n    3:\n            0: -8\n            1: -8\n            2: -7\n            3: 0\n            4: -5\n            5: -12\n            6: -20\n    4:\n            0: -3\n            1: -3\n            2: 0\n            3: 7\n            4: 0\n            5: -7\n            6: -15\n    5:\n            0: 4\n            1: 7\n            2: 10\n            3: 17\n            4: 12\n            5: 0\n            6: -8\n    6:\n            0: 12\n            1: 15\n            2: 18\n            3: 25\n            4: 20\n            5: 11\n            6: 0\n    ```\n\n9.  现在，让我们尝试另一组输入:\n\n    ```cpp\n    Input:\n    6 8\n    0 1 3\n    1 3 -8\n    2 1 3\n    2 4 2\n    2 5 5\n    3 2 3\n    4 5 -1\n    5 1 8\n    Output:\n    NEGATIVE CYCLE FOUND\n    ```\n\n如你所见，弗洛伊德-沃肖尔是一个非常有用的算法，它不仅有效，而且非常容易实现。在效率方面，我们应该选择弗洛伊德-沃肖尔算法还是约翰逊算法，完全取决于图的结构。但严格从实施的难易程度来看，弗洛伊德-沃霍尔显然是赢家。\n\n### 活动 23:住宅道路\n\n你是一个房地产开发项目的负责人，该项目正在计划建设一些高端住宅社区。你已经得到了各种各样的信息，关于将要建造开发项目的各种房产，目前的任务是尽可能便宜地设计一个道路系统。\n\n许多社区将建在充满湖泊、森林和山脉的地区。在这些地区，地形往往相当崎岖，这可能会使建筑更加复杂。有人警告你，建筑成本会随着地形的崎岖程度而增加。对于你的初稿，你被告知考虑成本的线性增长，相对于每一个坐标的强度值，在那里可能会建一条路。\n\n您已获得以下信息:\n\n*   属性的映射\n*   可以构建属性的坐标\n*   每个坐标处地形的崎岖程度\n\n在决定如何修建道路时，您还会得到以下指导方针:\n\n*   地图上可能修建道路的点将标有“`.`”字符。\n*   道路只能建在两栋房屋之间，这两栋房屋之间有直接的垂直、水平或对角线路径。\n*   社区中的所有房屋都应该可以从其他房屋到达。\n*   道路可能不会跨越水体、山脉、森林等等。\n*   在两栋房子之间修建一条道路的成本等于两栋房子之间道路的耐用度总和。\n*   只有在以尽可能低的成本到达物业指定入口点的道路上，才应在两栋房屋之间修建道路。\n*   入口点总是输入中索引最高的房子。\n\n房屋和道路的位置确定后，您应该根据以下图例制作原始地图的新版本:\n\n*   房子应该用大写字母标注，与输入时给出的顺序相对应(即 0 = `A`、1 = `B`、2 = `C`等)。\n*   道路应根据其朝向用字符`|`、`-`、`\\`和`/`表示。如果两条不同方向的道路相交，应使用`+`字符表示。\n*   地图上的所有其他内容都应该显示为输入中最初给出的内容。\n\n**输入格式**\n\n该程序应采用以下格式的输入:\n\n*   第一行包含两个空格分隔的整数`H`和`W`，代表地图的高度和宽度。\n*   第二个包含单个整数，`N`，即待建房屋数。\n*   接下来的`H`行各包含一串长度`W`，代表网格上的一行。建筑道路的有效位置将标有“`.`”字符。\n*   接下来的`N`行包含两个整数，`x`和`y`，这是房屋的坐标。最终索引(即`N - 1`)始终代表社区的入口点。\n\n**输出格式**\n\n程序应输出输入中给出的相同地图，并添加以下内容:\n\n*   每个房子的位置都要用大写字母标注对应其从零开始的索引，原点在左上角，相对于`N`(即 0 = `A`、1 = `B`、2 = `C`等等)。\n*   The roads connecting each pair of houses should be indicated as follows:\n\n    `-`如果道路的方向是水平的\n\n    `|`如果道路的方向是垂直的\n\n    `/`或`\\`如果道路的方向是对角线的\n\n    `+`如果任意数量的不同方向的道路在同一点相交\n\n**提示/指引**\n\n*   为了产生最终结果，需要许多不同的步骤。建议您在实施之前概述必要的步骤。\n*   为程序的每个独立部分设计一些调试和产生测试输出的方案可能会很有帮助。过程早期的错误很可能导致后续步骤失败。\n*   如果您在理解需要做什么方面有困难，请研究更简单的输入和输出示例。\n*   从实现你知道你需要的算法开始，特别是我们在前一章中讨论过的算法。完成这项任务的每一部分可能有多种方法——发挥创造力！\n\n**测试用例**\n\n这些测试用例应该帮助你理解你需要如何进行。让我们从一个简单的例子开始:\n\n![Figure 9.18: Activity 23, test cases 1 (left) and 2 (right) ](img/C14498_09_18.jpg)\n\n###### 图 9.18:活动 23，测试用例 1(左)和 2(右)\n\n让我们考虑上图右侧的示例输出。在那个例子中，从`E(0,4)`到`C(5,4)`的路径不能被建造为不可逾越的障碍，`#`存在。让我们考虑一些更复杂的示例:\n\n![Figure 9.19: Activity 23, test cases 3 (left) and 4 (right) ](img/C14498_09_19.jpg)\n\n###### 图 9.19:活动 23，测试用例 3(左)和 4(右)\n\n请注意，不同的符号用于表示不同类型的障碍物。尽管任何障碍的影响都是一样的，但我们不能在那里修路。最后，让我们在下面的示例中增加复杂性:\n\n![Figure 9.20: Activity 23, test case 5 ](img/C14498_09_20.jpg)\n\n###### 图 9.20:活动 23，测试用例 5\n\n#### 注意\n\n这个活动的解决方案可以在第 585 页找到。\n\n## 总结\n\n既然你已经完成了这一章，你应该对动态编程的价值有相当高的认识。如果你最初发现这个话题有些令人焦虑，你有希望意识到它并不像最初出现时那么复杂。正如我们在本章中所做的那样，通过动态编程的视角来看待熟悉的问题，当然可以帮助我们理解达成可行的动态规划解决方案所需的核心思想。为此，我们鼓励您调查背包问题的其他变体，并尝试使用提供的策略来实现它们。\n\n就这样，你在 C++ 的算法和数据结构的广阔世界中的旅行已经到达了它的结论。到了这本书的结尾，你应该对如何以及何时使用我们贸易中一些最有用的工具有了一个明显加深的理解。希望您已经对本书中介绍的结构和技术的实际应用有了更好的理解，并且扩展了对 C++ 语言及其大量特性的了解。\n\n应该注意的是，在实践中使用这些技术的适当场合不一定是显而易见的，这就是为什么将你所学的应用于一系列不同的环境是非常有益的。我们努力提供各种有趣的活动来练习本书中的概念，但强烈建议您也尝试在其他情况下使用这些技能。有太多的在线资源为所有级别的开发人员提供了独特且引人入胜的编程挑战，如果您希望训练自己认识到某些技术如何在各种情况下使用，这些资源将是无价的。\n\n当然，我们在这本书里讨论的每一个主题都值得比任何一本书更深入的研究，我们希望我们提供的信息已经使这些主题变得足够容易理解，从而鼓励你更深入地探索它们。不管你是学生，正在寻找发展工作，还是已经在专业领域工作，你很可能会遇到至少一个(很可能很多)本书所涉及的主题的使用；如果运气好的话，到时候你会知道该怎么做！"
  },
  {
    "path": "docs/cpp-dsal-design-principle/10.md",
    "content": "# 十、附录\n\n## 关于\n\n包括这一部分是为了帮助学生完成书中的活动。它包括学生为实现活动目标而要执行的详细步骤。\n\n## 第 1 章:列表、栈和队列\n\n### 活动 1:实现歌曲播放列表\n\n在本练习中，我们将实现一个双重链表的调整版本，它可以用来存储歌曲播放列表并支持必要的功能。按照以下步骤完成活动:\n\n1.  让我们首先包含头部，并用所需的数据成员编写节点结构:\n\n    ```cpp\n    #include <iostream>\n    template <typename T>\n    struct cir_list_node\n    {\n        T* data;\n        cir_list_node *next, *prev;\n\n    ~cir_list_node()\n        {\n            delete data;\n        }\n    };\n    template <typename T>\n    struct cir_list\n    {\n        public:\n            using node = cir_list_node<T>;\n            using node_ptr = node*;\n        private:\n            node_ptr head;\n            size_t n;\n    ```\n\n2.  Now, let's write a basic constructor and size function:\n\n    ```cpp\n    public:\n    cir_list(): n(0)\n    {\n        head = new node{NULL, NULL, NULL};  // Dummy node – having NULL data\n        head->next = head;\n        head->prev = head;\n    }\n    size_t size() const\n    {\n        return n;\n    }\n    ```\n\n    稍后我们将讨论在使用迭代器进行迭代的情况下，为什么在第一个和最后一个节点之间需要一个伪节点。\n\n3.  现在，让我们编写`insert`和`erase`函数。两者都需要插入或删除一个值:\n\n    ```cpp\n    void insert(const T& value)\n    {\n        node_ptr newNode = new node{new T(value), NULL, NULL};\n        n++ ;\n    auto dummy = head->prev;\n    dummy->next = newNode;\n    newNode->prev = dummy;\n        if(head == dummy)\n        {\n            dummy->prev = newNode;\n            newNode->next = dummy;\n            head = newNode;\n            return;\n        }\n        newNode->next = head;\n        head->prev = newNode;\n        head = newNode;\n    }\n    void erase(const T& value)\n    {\n        auto cur = head, dummy = head->prev;\n        while(cur != dummy)\n        {\n            if(*(cur->data) == value)\n            {\n                cur->prev->next = cur->next;\n                cur->next->prev = cur->prev;\n                if(cur == head)\n                    head = head->next;\n                delete cur;\n                n--;\n                return;\n            }\n            cur = cur->next;\n        }\n    }\n    ```\n\n4.  现在，让我们为所需的迭代器编写一个基本结构，并添加成员来访问实际数据:\n\n    ```cpp\n    struct cir_list_it\n    {\n    private:\n        node_ptr ptr;\n    public:\n        cir_list_it(node_ptr p) : ptr(p)\n        {}\n\n        T& operator*()\n        {\n            return *(ptr->data);\n        }\n        node_ptr get()\n        {\n            return ptr;\n        }\n    ```\n\n5.  现在，让我们实现迭代器的核心功能——增量前和增量后:\n\n    ```cpp\n    cir_list_it& operator++()\n    {\n        ptr = ptr->next;\n        return *this;\n    }\n    cir_list_it operator++(int)\n    {\n        cir_list_it it = *this;\n        ++(*this);\n        return it;    \n    }\n    ```\n\n6.  让我们添加与减量相关的操作，使其双向化:\n\n    ```cpp\n    cir_list_it& operator--()\n    {\n        ptr = ptr->prev;\n        return *this;\n    }\n    cir_list_it operator--(int)\n    {\n        cir_list_it it = *this;\n        --(*this);\n        return it;\n    }\n    ```\n\n7.  让我们为迭代器实现等式相关的运算符，这对于基于范围的循环来说是必不可少的:\n\n    ```cpp\n    friend bool operator==(const cir_list_it& it1, const cir_list_it& it2)\n    {\n        return it1.ptr == it2.ptr;\n    }\n    friend bool operator!=(const cir_list_it& it1, const cir_list_it& it2)\n    {\n        return it1.ptr != it2.ptr;\n    }\n    };\n    ```\n\n8.  现在，让我们也用它们的`const`版本来编写`begin`和`end`功能:\n\n    ```cpp\n    cir_list_it begin()\n    {\n        return cir_list_it{head};\n    }\n    cir_list_it begin() const\n    {\n        return cir_list_it{head};\n    }\n    cir_list_it end()\n    {\n        return cir_list_it{head->prev};\n    }\n    cir_list_it end() const\n    {\n        return cir_list_it{head->prev};\n    }\n    ```\n\n9.  让我们编写一个复制构造函数、初始化列表构造函数和析构函数:\n\n    ```cpp\n    cir_list(const cir_list<T>& other): cir_list()\n    {\n    // Although, the following will insert the elements in a reverse order, it won't matter in a logical sense since this is a circular list.\n        for(const auto& i: other)\n            insert(i);\n    }\n    cir_list(const std::initializer_list<T>& il): head(NULL), n(0)\n    {\n\n    // Although, the following will insert the elements in a reverse order, it won't matter in a logical sense since this is a circular list.\n        for(const auto& i: il)\n            insert(i);\n    }\n    ~cir_list()\n    {\n        while(size())\n        {\n            erase(head->data);\n        }\n    }\n    };\n    ```\n\n10.  现在，让我们为实际应用的音乐播放器播放列表添加一个类。为了便于理解，我们不再存储歌曲，而是直接存储指示歌曲 ID 的整数:\n\n    ```cpp\n    struct playlist\n    {\n        cir_list<int> list;\n    ```\n\n11.  现在让我们实现添加和删除歌曲的功能:\n\n    ```cpp\n    void insert(int song)\n    {\n        list.insert(song);\n    }\n    void erase(int song)\n    {\n        list.erase(song);\n    }\n    ```\n\n12.  现在，让我们实现打印所有歌曲的功能:\n\n    ```cpp\n    void loopOnce()\n    {\n        for(auto& song: list)\n            std::cout << song << \" \";\n        std::cout << std::endl;\n    }\n    };\n    ```\n\n13.  让我们写一个`main`函数来使用我们音乐播放器的播放列表:\n\n    ```cpp\n    int main()\n    {\n        playlist pl;\n        pl.insert(1);\n        pl.insert(2);\n        std::cout << \"Playlist: \";\n        pl.loopOnce();\n        playlist pl2 = pl;\n        pl2.erase(2);\n        pl2.insert(3);\n        std::cout << \"Second playlist: \";\n        pl2.loopOnce();\n    }\n    ```\n\n14.  执行此操作后，您应该会得到如下输出:\n\n    ```cpp\n    Playlist: 2 1 \n    Second playlist: 3 1\n    ```\n\n### 活动 2:模拟纸牌游戏\n\n在本练习中，我们将模拟一个纸牌游戏，并实现一个有效的数据结构来存储每个玩家的纸牌信息。按照以下步骤完成活动:\n\n1.  首先，让我们包括必要的标题:\n\n    ```cpp\n    #include <iostream>\n    #include <vector>\n    #include <array>\n    #include <sstream>\n    #include <algorithm>\n    #include <random>\n    #include <chrono>\n    ```\n\n2.  现在，让我们创建一个存储卡片的类和一个正确打印卡片的实用方法:\n\n    ```cpp\n    struct card\n    {\n        int number;\n        enum suit\n        {\n            HEART,\n            SPADE,\n            CLUB,\n            DIAMOND\n        } suit;\n        std::string to_string() const\n        {\n            std::ostringstream os;\n            if(number > 0 && number <= 10)\n                os << number;\n            else\n    {\n    switch(number)\n    {\n    case 1:\n        os << \"Ace\";\n        break;\n        case 11:\n            os << \"Jack\";\n            break;\n        case 12:\n            os << \"Queen\";\n            break;\n        case 13:\n            os << \"King\";\n            break;\n        default:\n            return \"Invalid card\";\n    }\n            }\n            os << \" of \";\n            switch(suit)\n            {\n                case HEART:\n                    os << \"hearts\";\n                    break;\n                case SPADE:\n                    os << \"spades\";\n                    break;\n                case CLUB:\n                    os << \"clubs\";\n                    break;\n                case DIAMOND:\n                    os << \"diamonds\";\n                    break;            \n            }\n            return os.str();\n        }\n    };\n    ```\n\n3.  现在，我们可以创建一副牌并洗牌，将牌随机分发给四个玩家中的每一个。我们将在一个`game`类中编写这个逻辑，稍后在`main`函数中调用这些函数:\n\n    ```cpp\n    struct game\n    {\n        std::array<card, 52> deck;\n        std::vector<card> player1, player2, player3, player4;\n        void buildDeck()\n        {\n            for(int i = 0; i < 13; i++)\n                deck[i] = card{i + 1, card::HEART};\n            for(int i = 0; i < 13; i++)\n                deck[i + 13] = card{i + 1, card::SPADE};\n            for(int i = 0; i < 13; i++)\n                deck[i + 26] = card{i + 1, card::CLUB};\n            for(int i = 0; i < 13; i++)\n                deck[i + 39] = card{i + 1, card::DIAMOND};\n        }\n        void dealCards()\n        {\n            unsigned seed = std::chrono::system_clock::now().time_since_epoch().count();\n            std::shuffle(deck.begin(), deck.end(), std::default_random_engine(seed));\n            player1 = {deck.begin(), deck.begin() + 13};\n    player2 = {deck.begin() + 13, deck.begin() + 26};\n    player3 = {deck.begin() + 26, deck.begin() + 39};\n    player4 = {deck.begin() + 39, deck.end()};\n        }\n    ```\n\n4.  让我们写下玩一轮的核心逻辑。为了避免代码重复，我们将编写一个实用函数，该函数将比较两个玩家的手牌，并在需要时移除两张牌:\n\n    ```cpp\n    bool compareAndRemove(std::vector<card>& p1, std::vector<card>& p2)\n    {\n        if(p1.back().number == p2.back().number)\n        {\n            p1.pop_back();\n            p2.pop_back();\n            return true;\n        }\n        return false;\n    }\n    void playOneRound()\n    {\n            if(compareAndRemove(player1, player2))\n            {\n                compareAndRemove(player3, player4);\n                return;\n            }\n            else if(compareAndRemove(player1, player3))\n            {\n                compareAndRemove(player2, player4);\n                return;\n            }\n            else if(compareAndRemove(player1, player4))\n            {\n                compareAndRemove(player2, player3);\n                return;\n            }\n            else if(compareAndRemove(player2, player3))\n            {\n                return;\n            }\n            else if(compareAndRemove(player2, player4))\n            {\n                return;\n            }\n            else if(compareAndRemove(player3, player4))\n            {\n    return;\n            }\n            unsigned seed = std::chrono::system_clock::now().time_since_epoch().count();\n            std::shuffle(player1.begin(), player1.end(), std::default_random_engine(seed));\n            std::shuffle(player2.begin(), player2.end(), std::default_random_engine(seed));\n            std::shuffle(player3.begin(), player3.end(), std::default_random_engine(seed));\n            std::shuffle(player4.begin(), player4.end(), std::default_random_engine(seed));\n    }\n    ```\n\n5.  现在，让我们写下主要逻辑，找出谁是赢家。我们将在一个循环中调用前面的函数，直到其中一个玩家可以扔掉他们所有的牌。为了使代码更易读，我们将编写另一个实用函数来检查游戏是否已经完成:\n\n    ```cpp\n    bool isGameComplete() const\n    {\n        return player1.empty() || player2.empty() || player3.empty() || player4.empty();\n    }\n    void playGame()\n    {\n            while(not isGameComplete())\n            {\n                playOneRound();    \n            }\n    }\n    ```\n\n6.  为了找出谁是赢家，让我们在启动`main`函数之前写一个实用函数:\n\n    ```cpp\n    int getWinner() const\n    {\n        if(player1.empty())\n            return 1;\n        if(player2.empty())\n            return 2;\n        if(player3.empty())\n            return 3;\n        if(player4.empty())\n            return 4;\n    }\n    };\n    ```\n\n7.  最后，我们编写`main`函数来执行游戏:\n\n    ```cpp\n    int main()\n    {\n        game newGame;\n        newGame.buildDeck();\n        newGame.dealCards();\n        newGame.playGame();\n        auto winner = newGame.getWinner();\n        std::cout << \"Player \" << winner << \" won the game.\" << std::endl;\n    }\n    ```\n\n8.  One of the possible outputs could be as follows:\n\n    ```cpp\n    Player 4 won the game.\n    ```\n\n    #### 注意\n\n    获胜者可以是从 1 到 4 的任何玩家。由于游戏是基于执行期间时间播种的随机性，任何玩家都可以获胜。多次运行代码可能每次都会产生不同的输出。\n\n### 活动 3:模拟办公室中共享打印机的队列\n\n在本练习中，我们将实现一个队列，用于处理对办公室共享打印机的打印请求。按照以下步骤完成活动:\n\n1.  让我们包括所需的标题:\n\n    ```cpp\n    #include <iostream>\n    #include <queue>\n    ```\n\n2.  让我们实现一个`Job`类:\n\n    ```cpp\n    class Job\n    {\n        int id;\n        std::string user;\n        int time;\n        static int count;\n    public:\n        Job(const std::string& u, int t) : user(u), time(t), id(++ count)\n        {}\n        friend std::ostream& operator<<(std::ostream& os, const Job& j)\n         {\n        os << \"id: \" << id << \", user: \" << user << \", time: \" << time << \" seconds\" << std::endl;    return os;\n         }\n    };\n    int Job::count = 0;\n    ```\n\n3.  现在，让我们实现`Printer`类。我们将使用`std::queue`为`jobs`制定先到先得的政策。我们将根据类可以在内存中存储的最大作业数来保持类模板化:\n\n    ```cpp\n    template <size_t N>\n    class Printer\n    {\n        std::queue<Job> jobs;\n    public:\n        bool addNewJob(const Job& job)\n        {\n            if(jobs.size() == N)\n                return false;\n            std::cout << \"Added job in the queue: \" << job;\n            jobs.push(job);\n            return true;\n        }\n    ```\n\n4.  现在，让我们实现另一个主要功能——打印作业:\n\n    ```cpp\n        void startPrinting()\n        {\n            while(not jobs.empty())\n            {\n                std::cout << \"Processing job: \" << jobs.front();\n                jobs.pop();\n            }\n        }\n    };\n    ```\n\n5.  现在，让我们使用这些类来模拟场景:\n\n    ```cpp\n    int main()\n    {\n        Printer<5> printer;\n        Job j1(\"John\", 10);\n        Job j2(\"Jerry\", 4);\n        Job j3(\"Jimmy\", 5);\n        Job j4(\"George\", 7);\n        Job j5(\"Bill\", 8);\n        Job j6(\"Kenny\", 10);\n        printer.addNewJob(j1);\n        printer.addNewJob(j2);\n        printer.addNewJob(j3);\n        printer.addNewJob(j4);\n        printer.addNewJob(j5);\n        if(not printer.addNewJob(j6))  // Can't add as queue is full.\n        {\n            std::cout << \"Couldn't add 6th job\" << std::endl;\n        }\n        printer.startPrinting();\n\n        printer.addNewJob(j6);  // Can add now, as queue got emptied\n        printer.startPrinting();\n    }\n    ```\n\n6.  下面是前面代码的输出:\n\n    ```cpp\n    Added job in the queue: id: 1, user: John, time: 10 seconds\n    Added job in the queue: id: 2, user: Jerry, time: 4 seconds\n    Added job in the queue: id: 3, user: Jimmy, time: 5 seconds\n    Added job in the queue: id: 4, user: George, time: 7 seconds\n    Added job in the queue: id: 5, user: Bill, time: 8 seconds\n    Couldn't add 6th job\n    Processing job: id: 1, user: John, time: 10 seconds\n    Processing job: id: 2, user: Jerry, time: 4 seconds\n    Processing job: id: 3, user: Jimmy, time: 5 seconds\n    Processing job: id: 4, user: George, time: 7 seconds\n    Processing job: id: 5, user: Bill, time: 8 seconds\n    Added job in the queue: id: 6, user: Kenny, time: 10 seconds\n    Processing job: id: 6, user: Kenny, time: 10 seconds\n    ```\n\n## 第 2 章:树、堆和图\n\n### 活动 4:为文件系统创建数据结构\n\n在本练习中，我们将使用文件系统的 N 元树创建一个数据结构。按照以下步骤完成活动:\n\n1.  首先，让我们包含所需的标题:\n\n    ```cpp\n    #include <iostream>\n    #include <vector>\n    #include <algorithm>\n    ```\n\n2.  现在，让我们写一个节点来存储一个目录/文件的数据:\n\n    ```cpp\n    struct n_ary_node\n    {\n        std::string name;\n        bool is_dir;\n        std::vector<n_ary_node*> children;\n    };\n    ```\n\n3.  现在，让我们将这个节点包装在一个树形结构中，以获得一个好的接口，并添加一个静态成员，这样我们就可以存储当前目录:\n\n    ```cpp\n    struct file_system\n    {\n        using node = n_ary_node;\n        using node_ptr = node*;\n    private:\n        node_ptr root;\n        node_ptr cwd;\n    ```\n\n4.  现在，让我们添加一个构造函数，这样我们就可以创建一个带有根目录的树:\n\n    ```cpp\n    public:\n        file_system()\n        {\n            root = new node{\"/\", true, {}};\n            cwd = root;  // We'll keep the current directory as root in the beginning\n        }\n    ```\n\n5.  现在，让我们添加一个函数来查找目录/文件:\n\n    ```cpp\n    node_ptr find(const std::string& path)\n    {\n        if(path[0] == '/')  // Absolute path\n        {\n            return find_impl(root, path.substr(1));\n        }\n        else\n        {\n            return find_impl(cwd, path);\n        }\n    }\n    private:\n    node_ptr find_impl(node_ptr directory, const std::string& path)\n    {\n        if(path.empty())\n            return directory;\n        auto sep = path.find('/');\n        std::string current_path = sep == std::string::npos ? path : path.substr(0, sep);\n        std::string rest_path = sep == std::string::npos ? \"\" : path.substr(sep + 1);\n        auto found = std::find_if(directory->children.begin(), directory->children.end(), [&](const node_ptr child)\n    {\n        return child->name == current_path;\n    });\n            if(found != directory->children.end())\n            {\n                return find_impl(*found, rest_path);\n            }\n        return NULL;\n    }\n    ```\n\n6.  现在，让我们添加一个函数来添加一个目录:\n\n    ```cpp\n    public:\n    bool add(const std::string& path, bool is_dir)\n    {\n        if(path[0] == '/')\n        {\n            return add_impl(root, path.substr(1), is_dir);\n        }\n        else\n        {\n            return add_impl(cwd, path, is_dir);\n        }\n    }\n    private:\n    bool add_impl(node_ptr directory, const std::string& path, bool is_dir)\n    {\n        if(not directory->is_dir)\n        {\n            std::cout << directory->name << \" is a file.\" << std::endl;\n            return false;\n        }\n\n    auto sep = path.find('/');\n    // This is the last part of the path for adding directory. It's a base condition of the recursion\n        if(sep == std::string::npos)\n        {\n            auto found = std::find_if(directory->children.begin(), directory->children.end(), [&](const node_ptr child)\n    {\n        return child->name == path;\n    });\n    if(found != directory->children.end())\n    {\n        std::cout << \"There's already a file/directory named \" << path << \" inside \" << directory->name << \".\" << std::endl;\n        return false;\n    }\n    directory->children.push_back(new node{path, is_dir, {}});\n    return true;\n        }\n\n        // If the next segment of the path is still a directory\n        std::string next_dir = path.substr(0, sep);\n        auto found = std::find_if(directory->children.begin(), directory->children.end(), [&](const node_ptr child)\n    {\n        return child->name == next_dir && child->is_dir;\n    });\n            if(found != directory->children.end())\n            {\n                return add_impl(*found, path.substr(sep + 1), is_dir);\n            }\n\n    std::cout << \"There's no directory named \" << next_dir << \" inside \" << directory->name << \".\" << std::endl;\n        return false;\n    }\n    ```\n\n7.  现在，让我们添加一个函数来更改当前目录。这将非常简单，因为我们已经有一个函数来寻找路径:\n\n    ```cpp\n    public:\n    bool change_dir(const std::string& path)\n    {\n        auto found = find(path);\n        if(found && found->is_dir)\n        {\n            cwd = found;\n            std::cout << \"Current working directory changed to \" << cwd->name << \".\" << std::endl;\n            return true;\n        }\n        std::cout << \"Path not found.\" << std::endl;\n        return false;\n    }\n    ```\n\n8.  现在，让我们添加一个函数来打印目录或文件。对于一个文件，我们只打印文件名。对于一个目录，我们将打印它所有孩子的名字，就像 Linux 中的`ls`命令:\n\n    ```cpp\n    public:\n    void show_path(const std::string& path)\n    {\n        auto found = find(path);\n        if(not found)\n        {\n            std::cout << \"No such path: \" << path << \".\" << std::endl;\n            return;\n        }\n        if(found->is_dir)\n        {\n            for(auto child: found->children)\n            {\n    std::cout << (child->is_dir ? \"d \" : \"- \") << child->name << std::endl;}\n        }\n        else\n        {\n            std::cout << \"- \" << found->name << std::endl;\n        }\n    }\n    };\n    ```\n\n9.  Let's write a main function so that we can use the aforementioned functions:\n\n    ```cpp\n    int main()\n    {\n        file_system fs;\n        fs.add(\"usr\", true);  // Add directory usr in \"/\"\n        fs.add(\"etc\", true);  // Add directory etc in \"/\"\n        fs.add(\"var\", true);  // Add directory var in \"/\"\n        fs.add(\"tmp_file\", false);  // Add file tmp_file in \"/\"\n        std::cout << \"Files/Directories under \\\"/\\\"\" << std::endl;\n        fs.show_path(\"/\");  // List files/directories in \"/\"\n        std::cout << std::endl;\n        fs.change_dir(\"usr\");\n        fs.add(\"Packt\", true);\n        fs.add(\"Packt/Downloads\", true);\n        fs.add(\"Packt/Downloads/newFile.cpp\", false);\n        std::cout << \"Let's see the contents of dir usr: \" << std::endl;\n        fs.show_path(\"usr\");  // This will not print the path successfully, since we're already inside the dir usr. And there's no directory named usr inside it.\n        std::cout << \"Let's see the contents of \\\"/usr\\\"\" << std::endl;\n        fs.show_path(\"/usr\");\n        std::cout << \"Let's see the contents of \\\"/usr/Packt/Downloads\\\"\" << std::endl;\n        fs.show_path(\"/usr/Packt/Downloads\");\n\n    }\n    ```\n\n    前面代码的输出如下:\n\n    ```cpp\n    Files/Directories under \"/\"\n    d usr\n    d etc\n    d var\n    - tmp_file\n    Current working directory changed to usr.\n    Let's try to print the contents of usr: \n    No such path: usr.\n    Let's see the contents of \"/usr\"\n    d Packt\n    Contents of \"/usr/Packt/Downloads\"\n    - newFile.cpp\n    ```\n\n### 活动 5:使用堆的 K 路合并\n\n在本练习中，我们将把多个排序数组合并成一个排序数组。这些步骤将帮助您完成活动:\n\n1.  从所需的标题开始:\n\n    ```cpp\n    #include <iostream>\n    #include <algorithm>\n    #include <vector>\n    ```\n\n2.  Now, implement the main algorithm for merging. It will take a vector of a vector of `int` as input and will contain the vector of all the sorted vectors. Then, it will return the merged vector of `int`. First, let's build the heap node:\n\n    ```cpp\n    struct node\n    {\n        int data;\n        int listPosition;\n        int dataPosition;\n    };\n    std::vector<int> merge(const std::vector<std::vector<int>>& input)\n    {\n        auto comparator = [] (const node& left, const node& right)\n            {\n                if(left.data == right.data)\n                    return left.listPosition > right.listPosition;\n                return left.data > right.data;\n            };\n    ```\n\n    正如我们所看到的，堆节点将包含三样东西——数据、列表在输入中的位置以及该列表中数据项的位置。\n\n3.  让我们建立堆。想法是用所有列表中最小的元素创建一个最小堆。所以，当我们从堆中弹出时，我们肯定会得到最小的元素。删除该元素后，我们需要插入同一列表中的下一个元素，如果它可用的话:\n\n    ```cpp\n    std::vector<node> heap;\n    for(int i = 0; i < input.size(); i++)\n    {\n        heap.push_back({input[i][0], i, 0});\n        std::push_heap(heap.begin(), heap.end(), comparator);\n    }\n    ```\n\n4.  现在，我们将建立合成向量。我们将简单地从堆中移除元素，直到它为空，并用它所属的同一个列表中的下一个元素替换它，如果可用的话:\n\n    ```cpp\n    std::vector<int> result;\n    while(!heap.empty())\n    {\n        std::pop_heap(heap.begin(), heap.end(), comparator);\n        auto min = heap.back();\n        heap.pop_back();\n        result.push_back(min.data);\n        int nextIndex = min.dataPosition + 1;\n        if(nextIndex < input[min.listPosition].size())\n        {\n            heap.push_back({input[min.listPosition][nextIndex], min.listPosition, nextIndex});\n            std::push_heap(heap.begin(), heap.end(), comparator);\n        }\n    }\n    return result;\n    }\n    ```\n\n5.  Let's write a `main` function so that we can use the preceding function:\n\n    ```cpp\n    int main()\n    {\n        std::vector<int> v1 = {1, 3, 8, 15, 105};\n        std::vector<int> v2 = {2, 3, 10, 11, 16, 20, 25};\n        std::vector<int> v3 = {-2, 100, 1000};\n        std::vector<int> v4 = {-1, 0, 14, 18};\n        auto result = merge({v1, v2, v3, v4});\n        for(auto i: result)\n        std::cout << i << ' ';\n        return 0;\n    }\n    ```\n\n    您应该会看到以下输出:\n\n    ```cpp\n    -2 -1 0 1 2 3 3 8 10 11 14 15 16 18 20 25 100 105 1000 \n    ```\n\n## 第 3 章:哈希表和布隆过滤器\n\n### 活动 6:将长网址映射到短网址\n\n在本练习中，我们将创建一个程序，将较短的网址映射到相应的较长的网址。按照以下步骤完成活动:\n\n1.  让我们包括所需的标题:\n\n    ```cpp\n    #include <iostream>\n    #include <unordered_map>\n    ```\n\n2.  Let's write a struct called `URLService` that will provide the interface for the required services:\n\n    ```cpp\n    struct URLService\n    {\n        using ActualURL = std::string;\n        using TinyURL = std::string;\n    private:\n        std::unordered_map<TinyURL, ActualURL> data;\n    ```\n\n    如我们所见，我们已经创建了一个从小网址到原始网址的地图。这是因为我们使用小网址进行查找。我们想把它转换成原来的网址。正如我们前面看到的，地图可以基于一个键进行快速查找。因此，我们保留了较小的网址作为地图的关键字，原始网址作为地图的值。我们创建了别名，以避免混淆我们谈论的是哪个字符串。\n\n3.  我们来增加一个`lookup`功能:\n\n    ```cpp\n    public:\n        std::pair<bool, ActualURL> lookup(const TinyURL& url) const\n        {\n            auto it = data.find(url);\n            if(it == data.end())  // If small URL is not registered.\n            {\n                return std::make_pair(false, std::string());\n            }\n            else\n            {\n                return std::make_pair(true, it->second);\n            }\n        }\n    ```\n\n4.  Now, let's write a function to register the smaller URL for the given actual URL:\n\n    ```cpp\n    bool registerURL(const ActualURL& actualURL, const TinyURL& tinyURL)\n    {\n        auto found = lookup(tinyURL).first;\n        if(found)\n        {\n            return false;\n        }\n        data[tinyURL] = actualURL;\n        return true;\n    }\n    ```\n\n    如果数据中已经存在条目，则`registerURL`函数返回。如果是这样，它将不会接触入口。否则，它将注册该条目并返回`true`以表明这一点。\n\n5.  Now, let's write a function to delete the entry:\n\n    ```cpp\n    bool deregisterURL(const TinyURL& tinyURL)\n    {\n        auto found = lookup(tinyURL).first;\n        if(found)\n        {\n            data.erase(tinyURL);\n            return true;\n        }\n        return false;\n    }\n    ```\n\n    可以看到，我们使用的是`lookup`函数，而不是再次重写查找逻辑。这个函数现在可读性更强了。\n\n6.  现在，让我们编写一个函数来打印日志记录的所有映射:\n\n    ```cpp\n    void printURLs() const\n    {\n        for(const auto& entry: data)\n        {\n            std::cout << entry.first << \" -> \" << entry.second << std::endl;\n        }\n        std::cout << std::endl;\n    }\n    };\n    ```\n\n7.  现在，编写`main`函数，这样我们就可以使用这个服务:\n\n    ```cpp\n    int main()\n    {\n        URLService service;\n        if(service.registerURL(\"https://www.packtpub.com/eu/big-data-and-business-intelligence/machine-learning-r-third-edition\", \"https://ml-r-v3\"))\n        {\n            std::cout << \"Registered https://ml-r-v3\" << std::endl;\n        }\n        else\n        {\n            std::cout << \"Couldn't register https://ml-r-v3\" << std::endl;\n        }\n        if(service.registerURL(\"https://www.packtpub.com/eu/virtualization-and-cloud/hands-aws-penetration-testing-kali-linux\", \"https://aws-test-kali\"))\n        {\n            std::cout << \"Registered https://aws-test-kali\" << std::endl;\n        }\n        else\n        {\n            std::cout << \"Couldn't register https://aws-test-kali\" << std::endl;\n        }\n        if(service.registerURL(\"https://www.packtpub.com/eu/application-development/hands-qt-python-developers\", \"https://qt-python\"))\n        {\n            std::cout << \"Registered https://qt-python\" << std::endl;\n        }\n        else\n        {\n            std::cout << \"Couldn't register https://qt-python\" << std::endl;\n        }\n\n        auto findMLBook = service.lookup(\"https://ml-r-v3\");\n        if(findMLBook.first)\n        {\n            std::cout << \"Actual URL: \" << findMLBook.second << std::endl;\n        }\n        else\n        {\n            std::cout << \"Couldn't find URL for book for ML.\" << std::endl;\n        }\n        auto findReactBook = service.lookup(\"https://react-cookbook\");\n        if(findReactBook.first)\n        {\n            std::cout << \"Actual URL: \" << findReactBook.second << std::endl;\n        }\n        else\n        {\n            std::cout << \"Couldn't find URL for book for React.\" << std::endl;\n        }\n        if(service.deregisterURL(\"https://qt-python\"))\n        {\n            std::cout << \"Deregistered qt python link\" << std::endl;\n        }\n        else\n        {\n            std::cout << \"Couldn't deregister qt python link\" << std::endl;\n        }\n        auto findQtBook = service.lookup(\"https://qt-python\");\n        if(findQtBook.first)\n        {\n            std::cout << \"Actual URL: \" << findQtBook.second << std::endl;\n        }\n        else\n        {\n            std::cout << \"Couldn't find Qt Python book\" << std::endl;\n        }\n        std::cout << \"List of registered URLs: \" << std::endl;\n        service.printURLs();\n    }\n    ```\n\n8.  让我们看看前面代码的输出:\n\n    ```cpp\n    Registered https://ml-r-v3\n    Registered https://aws-test-kali\n    Registered https://qt-python\n    Actual URL: https://www.packtpub.com/eu/big-data-and-business-intelligence/machine-learning-r-third-edition\n    Couldn't find URL for book for React.\n    Deregistered qt python link\n    Couldn't find Qt Python book\n    List of registered URLs: \n    https://ml-r-v3 -> https://www.packtpub.com/eu/big-data-and-business-intelligence/machine-learning-r-third-edition\n    https://aws-test-kali -> https://www.packtpub.com/eu/virtualization-and-cloud/hands-aws-penetration-testing-kali-linux\n    ```\n\n正如我们所看到的，我们在最后得到了两个有效的网址，而不是我们成功注销的那个。\n\n### 活动 7:电子邮件地址验证器\n\n在本练习中，我们将创建一个验证器来检查用户请求的电子邮件地址是否已经被占用。使用以下步骤完成活动:\n\n1.  让我们包括所需的标题:\n\n    ```cpp\n    #include <iostream>\n    #include <vector>\n    #include <openssl/md5.h>\n    ```\n\n2.  让我们为布隆过滤器添加一个类:\n\n    ```cpp\n    class BloomFilter\n    {\n        int nHashes;\n        std::vector<bool> bits;\n        static constexpr int hashSize = 128/8;\n        unsigned char hashValue[hashSize];\n    ```\n\n3.  Let's add a constructor for this:\n\n    ```cpp\n    BloomFilter(int size, int hashes) : bits(size), nHashes(hashes)\n    {\n        if(nHashes > hashSize)\n        {\n            throw (\"Number of hash functions too high\");\n        }\n        if(size > 255)\n        {\n            throw (\"Size of bloom filter can't be >255\");\n        }\n    }\n    ```\n\n    因为我们将使用哈希值缓冲区中的每个字节作为不同的哈希值，并且哈希值缓冲区的大小是 16 个字节(128 位)，所以我们不能有比这更多的哈希函数。由于每个哈希值只有 1 个字节，其可能的值为`0`到`255`。所以，布隆过滤器的尺寸不能超过`255`。因此，我们在构造函数本身抛出了一个错误。\n\n4.  现在，让我们写一个散列函数。它只是使用 MD5 函数来计算散列:\n\n    ```cpp\n    void hash(const std::string& key)\n    {\n        MD5(reinterpret_cast<const unsigned char*>(key.data()), key.length(), hashValue);\n    }\n    ```\n\n5.  Let's add the function so that we can insert an email:\n\n    ```cpp\n    void add(const std::string& key)\n    {\n        hash(key);\n        for(auto it = &hashValue[0]; it < &hashValue[nHashes]; it++)\n        {\n            bits[*it] = true;\n        }\n        std::cout << key << \" added in bloom filter.\" << std::endl;\n    }\n    ```\n\n    正如我们所看到的，我们从哈希值缓冲区中的字节`0`迭代到`nHashes`，并将每个位设置为`1`。\n\n6.  同样，让我们添加一个查找电子邮件地址的功能:\n\n    ```cpp\n    bool mayContain(const std::string &key)\n        {\n            hash(key);\n            for (auto it = &hashValue[0]; it < &hashValue[nHashes]; it++)\n            {\n                if (!bits[*it])\n                {\n                    std::cout << key << \" email can by used.\" << std::endl;\n                    return false;\n                }\n            }\n            std::cout << key << \" email is used by someone else.\" << std::endl;\n            return true;\n        }\n    };\n    ```\n\n7.  Let's add the `main` function:\n\n    ```cpp\n    int main()\n    {\n        BloomFilter bloom(10, 15);\n        bloom.add(\"abc@packt.com\");\n        bloom.add(\"xyz@packt.com\");\n        bloom.mayContain(\"abc\");\n        bloom.mayContain(\"xyz@packt.com\");\n        bloom.mayContain(\"xyz\");\n        bloom.add(\"abcd@packt.com\");\n        bloom.add(\"ab@packt.com\");\n        bloom.mayContain(\"abc\");\n        bloom.mayContain(\"ab@packt.com\");\n    }\n    ```\n\n    以下是上述代码的可能输出之一:\n\n    ```cpp\n    abc@packt.com added in bloom filter.\n    xyz@packt.com added in bloom filter.\n    abc email can by used.\n    xyz@packt.com email is used by someone else.\n    xyz email can by used.\n    abcd@packt.com added in bloom filter.\n    ab@packt.com added in bloom filter.\n    abcd email can by used.\n    ab@packt.com email is used by someone else.\n    ```\n\n这是可能的输出之一，因为 MD5 是一种随机化算法。如果我们深思熟虑地选择函数的数量和布隆过滤器的大小，我们应该使用 MD5 算法获得非常好的准确性。\n\n## 第四章:分而治之\n\n### 活动 8:接种疫苗\n\n在本活动中，我们将存储和查找学生的疫苗接种状态，以确定他们是否需要接种疫苗。这些步骤应该有助于您完成活动:\n\n1.  首先包含以下标题:\n\n    ```cpp\n    #include <iostream>\n    #include <vector>\n    #include <chrono>\n    #include <random>\n    #include <algorithm>\n    #include <numeric>\n    ```\n\n2.  定义`Student`类如下:\n\n    ```cpp\n    class Student\n    {\n    private:\n        std::pair<int, int> name;\n        bool vaccinated;\n    public:\n        // Constructor\n        Student(std::pair<int, int> n, bool v) :\n            name(n), vaccinated(v)\n        {}\n        // Getters\n        auto get_name() { return name; }\n        auto is_vaccinated() { return vaccinated; }\n        // Two people are same if they have the same name\n        bool operator ==(const Student& p) const\n        {\n            return this->name == p.name;\n        }\n        // The ordering of a set of people is defined by their name\n        bool operator< (const Student& p) const\n        {\n            return this->name < p.name;\n        }\n        bool operator> (const Student& p) const\n        {\n            return this->name > p.name;\n        }\n    };\n    ```\n\n3.  下面的函数让我们从随机数据中生成一个学生:\n\n    ```cpp\n    auto generate_random_Student(int max)\n    {\n        std::random_device rd;\n        std::mt19937 rand(rd());\n        // the IDs of Student should be in range [1, max]\n        std::uniform_int_distribution<std::mt19937::result_type> uniform_dist(1, max);\n        // Generate random credentials\n        auto random_name = std::make_pair(uniform_dist(rand), uniform_dist(rand));\n        bool is_vaccinated = uniform_dist(rand) % 2 ? true : false;\n        return Student(random_name, is_vaccinated);\n    }\n    ```\n\n4.  以下代码用于运行和测试我们的实现的输出:\n\n    ```cpp\n     void search_test(int size, Student p)\n    {\n        std::vector<Student> people;\n        // Create a list of random people\n        for (auto i = 0; i < size; i++)\n            people.push_back(generate_random_Student(size));\n        std::sort(people.begin(), people.end());\n        // To measure the time taken, start the clock\n        std::chrono::steady_clock::time_point begin = std::chrono::steady_clock::now();\n        bool search_result = needs_vaccination(p, people);\n        // Stop the clock\n        std::chrono::steady_clock::time_point end = std::chrono::steady_clock::now();\n        std::cout << \"Time taken to search = \" <<\n            std::chrono::duration_cast<std::chrono::microseconds>\n            (end - begin).count() << \" microseconds\" << std::endl;\n        if (search_result)\n            std::cout << \"Student (\" << p.get_name().first \n    << \" \" << p.get_name().second << \") \"\n                << \"needs vaccination.\" << std::endl;\n        else\n            std::cout << \"Student (\" << p.get_name().first \n    << \" \" << p.get_name().second << \") \"\n                << \"does not need vaccination.\" << std::endl;\n    }\n    ```\n\n5.  以下函数实现了我们判断是否需要接种疫苗的逻辑:\n\n    ```cpp\n    bool needs_vaccination(Student P, std::vector<Student>& people)\n    {\n        auto first = people.begin();\n        auto last = people.end();\n        while (true)\n        {\n            auto range_length = std::distance(first, last);\n            auto mid_element_index = std::floor(range_length / 2);\n            auto mid_element = *(first + mid_element_index);\n            // Return true if the Student is found in the sequence and \n    // he/she's not vaccinated \n            if (mid_element == P && mid_element.is_vaccinated() == false)\n                return true;\n            else if (mid_element == P && mid_element.is_vaccinated() == true)\n                return false;\n            else if (mid_element > P)\n                std::advance(last, -mid_element_index);\n            if (mid_element < P)\n                std::advance(first, mid_element_index);\n            // Student not found in the sequence and therefore should be vaccinated\n            if (range_length == 1)\n                return true;\n        }\n    }\n    ```\n\n6.  Finally, the driver code is implemented as follows:\n\n    ```cpp\n    int main()\n    {\n        // Generate a Student to search\n        auto p = generate_random_Student(1000);\n        search_test(1000, p);\n        search_test(10000, p);\n        search_test(100000, p);\n        return 0;\n    }\n    ```\n\n    #### 注意\n\n    由于我们在*步骤 3* 中对值进行随机化，因此您的输出可能会与本练习中显示的预期输出不同。\n\n### 活动 9:部分排序\n\n部分快速排序只是对原始快速排序算法的轻微修改，该算法在*练习 20* 、*快速排序*中进行了演示。和那个练习相比，只有*第四步*不同。以下是参考实现:\n\n1.  添加以下头文件:\n\n    ```cpp\n    #include <iostream>\n    #include <vector>\n    #include <chrono>\n    #include <random>\n    #include <algorithm>\n    ```\n\n2.  接下来，我们将实现分区操作，如下所示:\n\n    ```cpp\n     template <typename T>\n    auto partition(typename std::vector<T>::iterator begin,\n        typename std::vector<T>::iterator end)\n    {\n        auto pivot_val = *begin;\n        auto left_iter = begin + 1;\n        auto right_iter = end;\n        while (true)\n        {\n            // Starting from the first element of vector, \n            // find an element that is greater than pivot.\n            while (*left_iter <= pivot_val && std::distance(left_iter, right_iter) > 0)\n                left_iter++ ;\n            // Starting from the end of vector moving to the beginning, \n            // find an element that is lesser than the pivot.\n            while (*right_iter > pivot_val && std::distance(left_iter, right_iter) > 0)\n                right_iter--;\n            // If left and right iterators meet, there are no elements left to swap. \n            // Else, swap the elements pointed to by the left and right iterators\n            if (left_iter == right_iter)\n                break;\n            else\n                std::iter_swap(left_iter, right_iter);\n        }\n        if (pivot_val > *right_iter)\n            std::iter_swap(begin, right_iter);\n        return right_iter;\n    }\n    ```\n\n3.  由于期望的输出也需要快速排序算法的实现，我们将如下实现一个:\n\n    ```cpp\n     template <typename T>\n    void quick_sort(typename std::vector<T>::iterator begin,\n        typename std::vector<T>::iterator last)\n    {\n        // If there are more than 1 elements in the vector\n        if (std::distance(begin, last) >= 1)\n        {\n            // Apply the partition operation\n            auto partition_iter = partition<T>(begin, last);\n            // Recursively sort the vectors created by the partition operation\n            quick_sort<T>(begin, partition_iter-1);\n            quick_sort<T>(partition_iter, last);\n        }\n    }\n    ```\n\n4.  实现部分快速排序功能如下:\n\n    ```cpp\n     template <typename T>\n    void partial_quick_sort(typename std::vector<T>::iterator begin,\n        typename std::vector<T>::iterator last,\n        size_t k)\n    {\n        // If there are more than 1 elements in the vector\n        if (std::distance(begin, last) >= 1)\n        {\n            // Apply the partition operation\n            auto partition_iter = partition<T>(begin, last);\n            // Recursively sort the vectors created by the partition operation\n            partial_quick_sort<T>(begin, partition_iter-1, k);\n\n            // Sort the right subvector only if the final position of pivot < k \n            if(std::distance(begin, partition_iter) < k)\n                partial_quick_sort<T>(partition_iter, last, k);\n        }\n    }\n    ```\n\n5.  以下辅助函数可用于打印向量的内容并生成随机向量:\n\n    ```cpp\n     template <typename T>\n    void print_vector(std::vector<T> arr)\n    {\n        for (auto i : arr)\n            std::cout << i << \" \";\n        std::cout << std::endl;\n    }\n    // Generates random vector of a given size with integers [1, size]\n    template <typename T>\n    auto generate_random_vector(T size)\n    {\n        std::vector<T> V;\n        V.reserve(size);\n        std::random_device rd;\n        std::mt19937 rand(rd());\n        // the IDs of Student should be in range [1, max]\n        std::uniform_int_distribution<std::mt19937::result_type> uniform_dist(1, size);\n        for (T i = 0; i < size; i++)\n            V.push_back(uniform_dist(rand));\n        return std::move(V);\n    }\n    ```\n\n6.  以下函数实现了我们的排序函数的测试逻辑:\n\n    ```cpp\n    // Sort the first K elements of a random vector of a given 'size'\n    template <typename T>\n    void test_partial_quicksort(size_t size, size_t k)\n    {\n            // Create two copies of a random vector to use for the two algorithms\n            auto random_vec = generate_random_vector<T>(size);\n            auto random_vec_copy(random_vec);\n            std::cout << \"Original vector: \"<<std::endl;\n            print_vector<T>(random_vec); \n\n            // Measure the time taken by partial quick sort\n            std::chrono::steady_clock::time_point \n    begin_qsort = std::chrono::steady_clock::now();\n            partial_quick_sort<T>(random_vec.begin(), random_vec.end()-1, k);\n            std::chrono::steady_clock::time_point \n    end_qsort = std::chrono::steady_clock::now();\n\n            std::cout << std::endl << \"Time taken by partial quick sort = \" \n                << 'std::chrono::duration_cast<std::chrono::microseconds>\n                (end_qsort - begin_qsort).count() \n                << \" microseconds\" << std::endl;\n\n            std::cout << \"Partially sorted vector (only first \"<< k <<\" elements):\";\n            print_vector<T>(random_vec);\n            // Measure the time taken by partial quick sort\n            begin_qsort = std::chrono::steady_clock::now();\n            quick_sort<T>(random_vec_copy.begin(), random_vec_copy.end()-1);\n            end_qsort = std::chrono::steady_clock::now();\n            std::cout << std::endl <<\"Time taken by full quick sort = \" \n                << std::chrono::duration_cast<std::chrono::microseconds>\n                (end_qsort - begin_qsort).count() \n                << \" microseconds\" << std::endl;\n\n            std::cout << \"Fully sorted vector: \";\n            print_vector<T>(random_vec_copy);\n    }\n    ```\n\n7.  最后添加驱动代码，如下:\n\n    ```cpp\n     int main()\n    {\n        test_partial_quicksort<unsigned>(100, 10);\n        return 0;\n    }\n    ```\n\n### 活动 10:在 MapReduce 中实现字数统计\n\n在本练习中，我们将实现 MapReduce 模型来解决字数统计问题。以下是本活动的解决方案:\n\n1.  Implement the map task as follows:\n\n    ```cpp\n    struct map_task : public mapreduce::map_task<\n        std::string,                             // MapKey (filename)\n        std::pair<char const*, std::uintmax_t>>  // MapValue (memory mapped file contents)\n    {\n        template<typename Runtime>\n        void operator()(Runtime& runtime, key_type const& key, value_type& value) const\n        {\n            bool in_word = false;\n            char const* ptr = value.first;\n            char const* end = ptr + value.second;\n            char const* word = ptr;\n            // Iterate over the contents of the file, extract words and emit a <word,1> pair.\n            for (; ptr != end; ++ ptr)\n            {\n                // Convert the character to upper case.\n                char const ch = std::toupper(*ptr, std::locale::classic());\n                if (in_word)\n                {\n                    if ((ch < 'A' || ch > 'Z') && ch != '\\'')\n                    {\n    runtime.emit_intermediate(std::pair<char const*,\n                  std::uintmax_t> (word, ptr - word), 1);\n                        in_word = false;\n                    }\n                }\n                else if (ch >= 'A' && ch <= 'Z')\n                {\n                    word = ptr;\n                    in_word = true;\n                }\n            }\n            // Handle the last word.\n            if (in_word)\n            {\n                assert(ptr > word);\n                runtime.emit_intermediate(std::pair<char const*,\n                              std::uintmax_t>(word, ptr - word), 1);\n            }\n        }\n    };\n    ```\n\n    前面的映射函数单独应用于输入目录中的每个文件。输入文件的内容被接受为`value`中的`*`字符。然后，内部循环遍历文件的内容，提取不同的单词并发出 *<键，值>* 对，其中*键*是一个单词，*值*设置为 *1* 。\n\n2.  Implement the reduce task as follows:\n\n    ```cpp\n    template<typename KeyType>\n    struct reduce_task : public mapreduce::reduce_task<KeyType, unsigned>\n    {\n        using typename mapreduce::reduce_task<KeyType, unsigned>::key_type;\n        template<typename Runtime, typename It>\n        void operator()(Runtime& runtime, key_type const& key, It it, It const ite) const\n        {\n            runtime.emit(key, std::accumulate(it, ite, 0));    \n    }\n    }; \n    ```\n\n    然后，缩小操作可应用于地图功能发出的所有< key, value >对。由于在上一步中该值被设置为`1`，我们现在可以使用`std::accumulate()`来获得一个键在减少操作的输入对中出现的总次数。\n\n## 第五章:贪婪算法\n\n### 活动 11:区间调度问题\n\n在本活动中，我们将找到任务的最佳计划，以最大限度地增加可以完成的任务数量。按照以下步骤完成活动:\n\n1.  添加需要的头文件，定义`Task`结构如下:\n\n    ```cpp\n    #include <list>\n    #include <algorithm>\n    #include <iostream>\n    #include <random>\n    // Every task is represented as a pair <start_time, end_time>\n    struct Task\n    {\n        unsigned ID;\n        unsigned start_time;\n        unsigned end_time;\n    };\n    ```\n\n2.  以下功能可用于生成带有随机数据的 *N* 任务列表:\n\n    ```cpp\n    auto initialize_tasks(size_t num_tasks)\n    {\n        std::random_device rd;\n        std::mt19937 rand(rd());\n        std::uniform_int_distribution<std::mt19937::result_type> \n    uniform_dist(1, num_tasks);\n        // Create and initialize a set of tasks\n        std::list<Task> tasks;\n        for (unsigned i = 1; i <= num_tasks; i++)\n        {\n            auto start_time = uniform_dist(rand);\n            auto duration = uniform_dist(rand);\n            tasks.push_back({i, start_time, start_time + duration });\n        }\n        return tasks;\n    }\n    ```\n\n3.  执行调度算法如下:\n\n    ```cpp\n    auto schedule(std::list<Task> tasks)\n    {\n        // Sort the list of tasks by their end times\n        tasks.sort([](const auto& lhs, const auto& rhs)\n            { return lhs.end_time < rhs.end_time; });\n        // Remove the tasks that interfere with one another\n        for (auto curr_task = tasks.begin(); curr_task != tasks.end(); curr_task++)\n        {\n            // Point to the next task\n            auto next_task = std::next(curr_task, 1);\n            // While subsequent tasks interfere with the current task in iter\n            while (next_task != tasks.end() &&\n                next_task->start_time < curr_task->end_time)\n            {\n                next_task = tasks.erase(next_task);\n            }\n        }\n        return tasks;\n    }\n    ```\n\n4.  以下实用函数用于打印任务列表，测试我们的实现，并包含程序的驱动程序代码:\n\n    ```cpp\n    void print(std::list<Task>& tasks)\n    {\n        std::cout << \"Task ID \\t Starting Time \\t End time\" << std::endl;\n        for (auto t : tasks)\n            std::cout << t.ID << \"\\t\\t\" << t.start_time << \"\\t\\t\" << t.end_time << std::endl;\n    }\n    void test_interval_scheduling(unsigned num_tasks)\n    {\n        auto tasks = initialize_tasks(num_tasks);\n        std::cout << \"Original list of tasks: \" << std::endl;\n        print(tasks);\n        std::cout << \"Scheduled tasks: \" << std::endl;\n        auto scheduled_tasks = schedule(tasks);\n        print(scheduled_tasks);\n    }\n    int main()\n    {\n        test_interval_scheduling(20);\n        return 0;\n    }\n    ```\n\n### 活动 12:威尔士-鲍威尔算法\n\n在本练习中，我们将在图上实现威尔士-鲍威尔算法。这里给出了一个参考实现:\n\n1.  添加需要的头文件，声明后面要实现的图:\n\n    ```cpp\n    #include <unordered_map>\n    #include <set>\n    #include <map>\n    #include <string>\n    #include <vector>\n    #include <algorithm>\n    #include <iostream>\n    template <typename T> class Graph;\n    ```\n\n2.  实现结构，表示像这样的边:\n\n    ```cpp\n    template<typename T>\n    struct Edge\n    {\n        size_t src;\n        size_t dest;\n        T weight;\n        // To compare edges, only compare their weights,\n        // and not the source/destination vertices\n        inline bool operator< (const Edge<T>& e) const\n        {\n            return this->weight < e.weight;\n        }\n        inline bool operator> (const Edge<T>& e) const\n        {\n            return this->weight > e.weight;\n        }\n    };\n    ```\n\n3.  以下函数允许我们通过重载图数据类型的`<<`运算符来序列化和打印图:\n\n    ```cpp\n    template <typename T>\n    std::ostream& operator<<(std::ostream& os, const Graph<T>& G)\n    {\n        for (auto i = 1; i < G.vertices(); i++)\n        {\n            os << i << \":\\t\";\n            auto edges = G.outgoing_edges(i);\n            for (auto& e : edges)\n                os << \"{\" << e.dest << \": \" << e.weight << \"}, \";\n            os << std::endl;\n        }\n        return os;\n    }\n    ```\n\n4.  用边列表表示实现图，如下图所示:\n\n    ```cpp\n    template<typename T>\n    class Graph\n    {\n    public:\n        // Initialize the graph with N vertices\n        Graph(size_t N) : V(N)\n        {}\n        // Return number of vertices in the graph\n        auto vertices() const\n        {\n            return V;\n        }\n        // Return all edges in the graph\n        auto& edges() const\n        {\n            return edge_list;\n        }\n        void add_edge(Edge<T>&& e)\n        {\n            // Check if the source and destination vertices are within range\n            if (e.src >= 1 && e.src <= V &&\n                e.dest >= 1 && e.dest <= V)\n                edge_list.emplace_back(e);\n            else\n                std::cerr << \"Vertex out of bounds\" << std::endl;\n        }\n        // Returns all outgoing edges from vertex v\n        auto outgoing_edges(size_t v) const\n        {\n            std::vector<Edge<T>> edges_from_v;\n            for (auto& e : edge_list)\n            {\n                if (e.src == v)\n                    edges_from_v.emplace_back(e);\n            }\n            return edges_from_v;\n        }\n        // Overloads the << operator so a graph be written directly to a stream\n        // Can be used as std::cout << obj << std::endl;\n        template <typename T>\n        friend std::ostream& operator<< <>(std::ostream& os, const Graph<T>& G);\n    private:\n        size_t V;        // Stores number of vertices in graph\n        std::vector<Edge<T>> edge_list;\n    };\n    ```\n\n5.  初始化我们将在威尔士-鲍威尔算法的实现中使用的颜色集。让这个颜色数为`6`，如下图`unordered_map` :\n\n    ```cpp\n    // Initialize the colors that will be used to color the vertices\n    std::unordered_map<size_t, std::string> color_map = {\n        {1, \"Red\"},\n        {2, \"Blue\"},\n        {3, \"Green\"},\n        {4, \"Yellow\"},\n        {5, \"Black\"},\n        {6, \"White\"}\n    };\n    ```\n\n6.  像这样实现威尔士-鲍威尔图着色算法:\n\n    ```cpp\n    template<typename T>\n    auto welsh_powell_coloring(const Graph<T>& G)\n    {\n        auto size = G.vertices();\n        std::vector<std::pair<size_t, size_t>> degrees;\n        // Collect the degrees of vertices as <vertex_ID, degree> pairs\n        for (auto i = 1; i < size; i++)\n            degrees.push_back(std::make_pair(i, G.outgoing_edges(i).size()));\n        // Sort the vertices in decreasing order of degree\n        std::sort(degrees.begin(),\n            degrees.end(),\n            [](const auto& a, const auto& b)\n            { return a.second > b.second; });\n        std::cout << \"The vertices will be colored in the following order: \" << std::endl;\n        std::cout << \"Vertex ID \\t Degree\" << std::endl;\n        for (auto const i : degrees)\n            std::cout << i.first << \"\\t\\t\" << i.second << std::endl;\n        std::vector<size_t> assigned_colors(size);\n        auto color_to_be_assigned = 1;\n        while (true)\n        {\n            for (auto const i : degrees)\n            {\n                if (assigned_colors[i.first] != 0)\n                    continue;\n                auto outgoing_edges = G.outgoing_edges(i.first);\n                std::set<size_t> neighbour_colors;\n                // We assume that the graph is bidirectional\n                for (auto e : outgoing_edges)\n                {\n                    auto dest_color = assigned_colors[e.dest];\n                    neighbour_colors.insert(dest_color);\n                }\n    if (neighbour_colors.find(color_to_be_assigned) == neighbour_colors.end())\n                    assigned_colors[i.first] = color_to_be_assigned;\n            }\n            color_to_be_assigned++ ;\n            // If there are no uncolored vertices left, exit\n            if (std::find(assigned_colors.begin() + 1, assigned_colors.end(), 0) ==\n                assigned_colors.end())\n                break;\n        }\n        return assigned_colors;\n    }\n    ```\n\n7.  以下函数输出颜色向量:\n\n    ```cpp\n    void print_colors(std::vector<size_t>& colors)\n    {\n        for (auto i = 1; i < colors.size(); i++)\n        {\n            std::cout << i << \": \" << color_map[colors[i]] << std::endl;\n        }\n    }\n    ```\n\n8.  最后，下面的驱动程序代码创建所需的图，运行顶点着色算法，并输出结果:\n\n    ```cpp\n    int main()\n    {\n        using T = unsigned;\n        Graph<T> G(9);\n        std::map<unsigned, std::vector<std::pair<size_t, T>>> edges;\n        edges[1] = { {2, 2}, {5, 3} };\n        edges[2] = { {1, 2}, {5, 5}, {4, 1} };\n        edges[3] = { {4, 2}, {7, 3} };\n        edges[4] = { {2, 1}, {3, 2}, {5, 2}, {6, 4}, {8, 5} };\n        edges[5] = { {1, 3}, {2, 5}, {4, 2}, {8, 3} };\n        edges[6] = { {4, 4}, {7, 4}, {8, 1} };\n        edges[7] = { {3, 3}, {6, 4} };\n        edges[8] = { {4, 5}, {5, 3}, {6, 1} };\n        for (auto& i : edges)\n            for (auto& j : i.second)\n                G.add_edge(Edge<T>{ i.first, j.first, j.second });\n        std::cout << \"Original Graph\" << std::endl;\n        std::cout << G;\n        auto colors = welsh_powell_coloring<T>(G);\n        std::cout << \"Vertex Colors: \" << std::endl;\n        print_colors(colors);\n        return 0;\n    }\n    ```\n\n## 第六章:图算法一\n\n### 活动 13:使用 DFS 找出一个图是否是二分图\n\n在本练习中，我们将使用深度优先搜索遍历来检查一个图是否是二分图。按照以下步骤完成活动:\n\n1.  添加需要的头文件，声明要使用的图:\n\n    ```cpp\n    #include <string>\n    #include <vector>\n    #include <iostream>\n    #include <set>\n    #include <map>\n    #include <stack>\n    template<typename T> class Graph;\n    ```\n\n2.  编写以下结构来定义我们的图中的一条边:\n\n    ```cpp\n    template<typename T>\n    struct Edge\n    {\n        size_t src;\n        size_t dest;\n        T weight;\n        // To compare edges, only compare their weights,\n        // and not the source/destination vertices\n        inline bool operator< (const Edge<T>& e) const\n        {\n            return this->weight < e.weight;\n        }\n        inline bool operator> (const Edge<T>& e) const\n        {\n            return this->weight > e.weight;\n        }\n    };\n    ```\n\n3.  使用以下功能重载图的`<<`运算符，以便将其写入标准输出:\n\n    ```cpp\n    template <typename T>\n    std::ostream& operator<<(std::ostream& os, const Graph<T>& G)\n    {\n        for (auto i = 1; i < G.vertices(); i++)\n        {\n            os << i << \":\\t\";\n            auto edges = G.outgoing_edges(i);\n            for (auto& e : edges)\n                os << \"{\" << e.dest << \": \" << e.weight << \"}, \";\n            os << std::endl;\n        }\n        return os;\n    }\n    ```\n\n4.  实现边列表图如下:\n\n    ```cpp\n    template<typename T>\n    class Graph\n    {\n    public:\n        // Initialize the graph with N vertices\n        Graph(size_t N) : V(N)\n        {}\n        // Return number of vertices in the graph\n        auto vertices() const\n        {\n            return V;\n        }\n        // Return all edges in the graph\n        auto& edges() const\n        {\n            return edge_list;\n        }\n        void add_edge(Edge<T>&& e)\n        {\n            // Check if the source and destination vertices are within range\n            if (e.src >= 1 && e.src <= V &&\n                e.dest >= 1 && e.dest <= V)\n                edge_list.emplace_back(e);\n            else\n                std::cerr << \"Vertex out of bounds\" << std::endl;\n        }\n        // Returns all outgoing edges from vertex v\n        auto outgoing_edges(size_t v) const\n        {\n            std::vector<Edge<T>> edges_from_v;\n            for (auto& e : edge_list)\n            {\n                if (e.src == v)\n                    edges_from_v.emplace_back(e);\n            }\n            return edges_from_v;\n        }\n        // Overloads the << operator so a graph be written directly to a stream\n        // Can be used as std::cout << obj << std::endl;\n        template <typename T>\n        friend std::ostream& operator<< <>(std::ostream& os, const Graph<T>& G);\n    private:\n        size_t V;        // Stores number of vertices in graph\n        std::vector<Edge<T>> edge_list;\n    };\n    ```\n\n5.  创建*图 6.17* 所示的图，如下图:\n\n    ```cpp\n    template <typename T>\n    auto create_bipartite_reference_graph()\n    {\n        Graph<T> G(10);\n        std::map<unsigned, std::vector<std::pair<size_t, T>>> edges;\n        edges[1] = { {2, 0} };\n        edges[2] = { {1, 0}, {3, 0} , {8, 0} };\n        edges[3] = { {2, 0}, {4, 0} };\n        edges[4] = { {3, 0}, {6, 0} };\n        edges[5] = { {7, 0}, {9, 0} };\n        edges[6] = { {1, 0}, {4, 0} };\n        edges[7] = { {5, 0} };\n        edges[8] = { {2,0}, {9, 0} };\n        edges[9] = { {5, 0} };\n        for (auto& i : edges)\n            for (auto& j : i.second)\n                G.add_edge(Edge<T>{ i.first, j.first, j.second });\n        return G;\n    }\n    ```\n\n6.  现在，我们需要一个函数，以便实现我们的算法并检查该图是否是二分图。这样写函数:\n\n    ```cpp\n    template <typename T>\n    auto bipartite_check(const Graph<T>& G)\n    {\n        std::stack<size_t> stack;\n        std::set<size_t> visited;\n        stack.push(1); // Assume that BFS always starts from vertex ID 1\n        enum class colors {NONE, RED, BLUE};\n        colors current_color{colors::BLUE}; // This variable tracks the color to be assigned to the next vertex that is visited.\n        std::vector<colors> vertex_colors(G.vertices(), colors::NONE);\n        while (!stack.empty())\n        {\n            auto current_vertex = stack.top();\n            stack.pop();\n            // If the current vertex hasn't been visited in the past\n            if (visited.find(current_vertex) == visited.end())\n            {\n                visited.insert(current_vertex);\n                vertex_colors[current_vertex] = current_color;\n                if (current_color == colors::RED)\n                {\n    std::cout << \"Coloring vertex \" << current_vertex << \" RED\" << std::endl;\n                    current_color = colors::BLUE;\n                }\n                else\n                {\n                    std::cout << \"Coloring vertex \" \n    << current_vertex << \" BLUE\" << std::endl;\n                    current_color = colors::RED;\n                }\n                // Add unvisited adjacent vertices to the stack.\n                for (auto e : G.outgoing_edges(current_vertex))\n                    if (visited.find(e.dest) == visited.end())\n                        stack.push(e.dest);\n            }\n            // If the found vertex is already colored and \n            // has a color same as its parent's color, the graph is not bipartite\n            else if (visited.find(current_vertex) != visited.end() && \n                ((vertex_colors[current_vertex] == colors::BLUE && \n                    current_color == colors::RED) ||\n                (vertex_colors[current_vertex] == colors::RED && \n                    current_color == colors::BLUE)))\n                return false;\n        }\n        // If all vertices have been colored, the graph is bipartite\n        return true;\n    }\n    ```\n\n7.  使用以下函数来实现测试和驱动程序代码，该代码测试我们的二分检查算法的实现:\n\n    ```cpp\n    template <typename T>\n    void test_bipartite()\n    {\n        // Create an instance of and print the graph\n        auto BG = create_bipartite_reference_graph<T>();\n        std::cout << BG << std::endl;\n        if (bipartite_check<T>(BG))\n            std::cout << \"The graph is bipartite\" << std::endl;\n        else\n            std::cout << \"The graph is not bipartite\" << std::endl;\n    }\n    int main()\n    {\n        using T = unsigned;\n        test_bipartite<T>();\n        return 0;\n    }\n    ```\n\n8.  Run the program. You should see the following output:\n\n    ![Figure 6.34: Output of Activity 13 ](img/C14498_06_34.jpg)\n\n###### 图 6.34:活动 13 的输出\n\n### 活动 14:纽约最短路径\n\n在本练习中，我们将使用纽约市不同位置的图，并找到两个给定顶点之间的最短距离。按照以下步骤完成活动:\n\n1.  添加需要的头文件并声明图，如下图:\n\n    ```cpp\n    #include <string>\n    #include <vector>\n    #include <iostream>\n    #include <set>\n    #include <map>\n    #include <limits>\n    #include <queue>\n    #include <fstream>\n    #include <sstream>\n    template<typename T> class Graph;\n    ```\n\n2.  实现将在图中使用的加权边:\n\n    ```cpp\n    template<typename T>\n    struct Edge\n    {\n        size_t src;\n        size_t dest;\n        T weight;\n        // To compare edges, only compare their weights,\n        // and not the source/destination vertices\n        inline bool operator< (const Edge<T>& e) const\n        {\n            return this->weight < e.weight;\n        }\n        inline bool operator> (const Edge<T>& e) const\n        {\n            return this->weight > e.weight;\n        }\n    };\n    ```\n\n3.  为`Graph`类重载`<<`操作符，以便它可以输出到 C++ 流:\n\n    ```cpp\n    template <typename T>\n    std::ostream& operator<<(std::ostream& os, const Graph<T>& G)\n    {\n        for (auto i = 1; i < G.vertices(); i++)\n        {\n            os << i << \":\\t\";\n            auto edges = G.outgoing_edges(i);\n            for (auto& e : edges)\n                os << \"{\" << e.dest << \": \" << e.weight << \"}, \";\n            os << std::endl;\n        }\n        return os;\n    }\n    ```\n\n4.  实现一个边列表图，如下图所示:\n\n    ```cpp\n    template<typename T>\n    class Graph\n    {\n    public:\n        // Initialize the graph with N vertices\n        Graph(size_t N) : V(N)\n        {}\n        // Return number of vertices in the graph\n        auto vertices() const\n        {\n            return V;\n        }\n        // Return all edges in the graph\n        auto& edges() const\n        {\n            return edge_list;\n        }\n        void add_edge(Edge<T>&& e)\n        {\n            // Check if the source and destination vertices are within range\n            if (e.src >= 1 && e.src <= V &&\n                e.dest >= 1 && e.dest <= V)\n                edge_list.emplace_back(e);\n            else\n                std::cerr << \"Vertex out of bounds\" << std::endl;\n        }\n        // Returns all outgoing edges from vertex v\n        auto outgoing_edges(size_t v) const\n        {\n            std::vector<Edge<T>> edges_from_v;\n            for (auto& e : edge_list)\n            {\n                if (e.src == v)\n                    edges_from_v.emplace_back(e);\n            }\n            return edges_from_v;\n        }\n        // Overloads the << operator so a graph be written directly to a stream\n        // Can be used as std::cout << obj << std::endl;\n        template <typename T>\n        friend std::ostream& operator<< <>(std::ostream& os, const Graph<T>& G);\n    private:\n        size_t V;        // Stores number of vertices in graph\n        std::vector<Edge<T>> edge_list;\n    };\n    ```\n\n5.  编写以下函数，以便解析图文件并准备图:\n\n    ```cpp\n    template <typename T>\n    auto read_graph_from_file()\n    {\n        std::ifstream infile(\"USA-road-d.NY.gr\");\n        size_t num_vertices, num_edges;\n        std::string line;\n\n        // Read the problem description line that starts with 'p' and looks like:\n        // p <num_vertices> <num_edges>\n        while (std::getline(infile, line))\n        {\n            if (line[0] == 'p')\n            {\n                std::istringstream iss(line);\n                char p;\n                std::string sp;\n                iss >> p >>sp >> num_vertices >> num_edges; \n                std::cout << \"Num vertices: \" << num_vertices \n    << \" Num edges: \" << num_edges <<std::endl;\n                break;\n            }\n        }\n        Graph<T> G(num_vertices + 1);\n        // Read the edges and edge weights, which look like:\n        // a <source_vertex> <destination_vertex> <weight>\n        while (std::getline(infile, line))\n        {\n            if (line[0] == 'a')\n            {\n                std::istringstream iss(line);\n                char p;\n                size_t source_vertex, dest_vertex;\n                T weight;\n                iss >> p >> source_vertex >> dest_vertex >> weight;\n                G.add_edge(Edge<T>{source_vertex, dest_vertex, weight});\n            }\n        }\n        infile.close();\n        return G;\n    }\n    ```\n\n6.  现在，我们需要一个实现`Label`结构的结构，当 Dijkstra 算法运行时，该结构将被分配给每个顶点。执行如下:\n\n    ```cpp\n    template<typename T>\n    struct Label\n    {\n        size_t vertex_ID;\n        T distance_from_source;\n        Label(size_t _id, T _distance) :\n            vertex_ID(_id),\n            distance_from_source(_distance)\n        {}\n        // To compare labels, only compare their distances from source\n        inline bool operator< (const Label<T>& l) const\n        {\n            return this->distance_from_source < l.distance_from_source;\n        }\n        inline bool operator> (const Label<T>& l) const\n        {\n            return this->distance_from_source > l.distance_from_source;\n        }\n        inline bool operator() (const Label<T>& l) const\n        {\n            return this > l;\n        }\n    };\n    ```\n\n7.  Dijkstra 的算法可以实现如下:\n\n    ```cpp\n    template <typename T>\n    auto dijkstra_shortest_path(const Graph<T>& G, size_t src, size_t dest)\n    {\n        std::priority_queue<Label<T>, std::vector<Label<T>>, std::greater<Label<T>>> heap;\n        std::set<int> visited;\n        std::vector<size_t> parent(G.vertices());\n        std::vector<T> distance(G.vertices(), std::numeric_limits<T>::max());\n        std::vector<size_t> shortest_path;\n        heap.emplace(src, 0);\n        parent[src] = src;\n        // Search for the destination vertex in the graph\n        while (!heap.empty()) {\n            auto current_vertex = heap.top();\n            heap.pop();\n            // If the search has reached the destination vertex\n            if (current_vertex.vertex_ID == dest) {\n                std::cout << \"Destination \" << \n    current_vertex.vertex_ID << \" reached.\" << std::endl;\n                break;\n            }\n            if (visited.find(current_vertex.vertex_ID) == visited.end()) {\n                std::cout << \"Settling vertex \" << \n    current_vertex.vertex_ID << std::endl;\n                // For each outgoing edge from the current vertex, \n                // create a label for the destination vertex and add it to the heap\n                for (auto e : G.outgoing_edges(current_vertex.vertex_ID)) {\n                    auto neighbor_vertex_ID = e.dest;\n                    auto new_distance_to_dest=current_vertex.distance_from_source \n    + e.weight;\n                    // Check if the new path to the destination vertex \n    // has a lower cost than any previous paths found to it, if // yes, then this path should be preferred \n                    if (new_distance_to_dest < distance[neighbor_vertex_ID]) {\n                        heap.emplace(neighbor_vertex_ID, new_distance_to_dest);\n                        parent[e.dest] = current_vertex.vertex_ID;\n                        distance[e.dest] = new_distance_to_dest;\n                    }\n                }\n                visited.insert(current_vertex.vertex_ID);\n            }\n        }\n        // Construct the path from source to the destination by backtracking \n        // using the parent indexes\n        auto current_vertex = dest;\n        while (current_vertex != src) {\n            shortest_path.push_back(current_vertex);\n            current_vertex = parent[current_vertex];\n        }\n        shortest_path.push_back(src);\n        std::reverse(shortest_path.begin(), shortest_path.end());\n        return shortest_path;\n    }\n    ```\n\n8.  最后实现测试和驱动代码，如下图:\n\n    ```cpp\n    template<typename T>\n    void test_dijkstra()\n    {\n        auto G = read_graph_from_file<T>();\n        //std::cout << G << std::endl;\n        auto shortest_path = dijkstra_shortest_path<T>(G, 913, 542);\n        std::cout << \"The shortest path between 913 and 542 is:\" << std::endl;\n        for (auto v : shortest_path)\n            std::cout << v << \" \";\n        std::cout << std::endl;\n    }\n    int main()\n    {\n        using T = unsigned;\n        test_dijkstra<T>();\n        return 0;\n    }\n    ```\n\n9.  运行程序。您的输出应该如下所示:\n\n![Figure 6.35: Output of Activity 14 ](img/C14498_06_35.jpg)\n\n###### 图 6.35:活动 14 的输出\n\n## 第七章:图算法二\n\n### 活动 15:贪婪机器人\n\n我们可以使用*练习 33* 、*实施贝尔曼-福特算法(第二部分)*中的精确算法来解决此活动。这里的潜在陷阱与正确解释所需任务和在您实际试图解决的问题的上下文中表示图有关。让我们开始吧:\n\n1.  第一步与练习相同。我们将包含相同的头，并定义一个`Edge`结构和一个`UNKNOWN`常数:\n\n    ```cpp\n    #include <iostream>\n    #include <vector>\n    #include <climits>\n    using namespace std;\n    struct Edge\n    {\n            int start;\n            int end;   \n            int weight;\n            Edge(int s, int e, int w) : start(s), end(e), weight(w) {}\n    };\n    const int UNKNOWN = INT_MAX;\n    vector<Edge*> edges;\n    ```\n\n2.  在`main()`中，我们将声明一个整数，`N`，它决定了网格的高度/宽度。然后，我们将在`for`循环中从 0 迭代到 N * N - 1，并读取输入中给出的邻接数据:\n\n    ```cpp\n    int main()\n    {\n        int N;\n        cin >> N;\n        for(int i = 0; i < N * N - 1; i++)\n        {\n            string directions;\n            int power;\n\n            cin >> directions >> power;\n\n            ……\n        }\n        return 0;\n    }\n    ```\n\n3.  现在，我们必须面对第一个潜在的问题——准确地表示相邻关系。通常情况下，我们倾向于考虑二维网格，虽然这样解决问题肯定是可能的，但它不是解决这个特定问题的最佳方法。要在一个维度上重新解释网格和邻接，我们必须简单地观察一维索引`i`和相应的二维网格坐标之间的以下关系:\n\n    ```cpp\n    CURRENT CELL: (x, y) —> i\n    NORTH: (x, y - 1) —> i - N\n    SOUTH: (x, y + 1) —> i + N\n    EAST: (x + 1, y) —> i + 1\n    WEST: (x - 1, y) —> i - 1 \n    ```\n\n4.  我们可以通过遍历`directions`的字符并在`switch`语句中包含逻辑来处理这些关系:\n\n    ```cpp\n    for(int i = 0; i < N * N - 1; i++)\n    {\n        string directions;\n        int power;\n        cin >> directions >> power;\n        int next;\n        for(auto d : directions)\n        {\n            switch(d)\n            {\n                case 'N': next = i - N; break;\n                case 'S': next = i + N; break;\n                case 'E': next = i + 1; break;\n                case 'W': next = i - 1; break;\n            }\n            ……\n        }\n    }\n    ```\n\n5.  这导致了该活动的第二个有问题的方面；也就是`power`值的解释。这些，当然，将是定义相邻单元之间的边缘权重的值，但是在这个问题的上下文中，输入可能相当误导。根据问题的描述，我们希望找到与基线相比能量最大的到达终点的路径。粗心地阅读问题陈述可能会导致我们得出`power`值与边缘权重完全对应的结论，但这实际上会产生与我们想要实现的相反的结果。“能量最大化”可以被视为等同于“能量损失最小化”，由于负值实际上代表每个细胞的能量消耗，正值代表获得的能量，我们必须反转每个`power`值的符号:\n\n    ```cpp\n    for(auto d : directions)\n    {\n        switch(d)\n        {\n            ……\n        }\n        // Add edge with power variable's sign reversed \n        edges.push_back(new Edge(i, next, -power));\n    }\n    ```\n\n6.  现在，我们可以实施`BellmanFord()`。这次，我们的函数将把`N`和`edges`作为参数，返回一个等于最大相对能量的整数。为了简化我们的代码，我们将通过`N`作为网格中的单元格总数(即`N * N` ):\n\n    ```cpp\n    int BellmanFord(int N, vector<Edge*> edges)\n    {\n        vector<int> distance(N, UNKNOWN);\n\n        // Starting node is always index 0\n        distance[0] = 0;\n        for(int i = 0; i < N - 1; i++)\n        {\n            for(auto edge : edges)\n            {\n                if(distance[edge->start] == UNKNOWN)\n                {\n                    continue;\n                }\n                if(distance[edge->start] + edge->weight < distance[edge->end])\n                {\n                    distance[edge->end] = distance[edge->start] + edge->weight;\n                }\n            }\n        }\n        ……\n    }\n    ```\n\n7.  根据标准实施，我们还将执行负周期检查，以处理与机器人贪婪的能量消耗相关的情况。在发现负周期的情况下，我们将返回`UNKNOWN` :\n\n    ```cpp\n    // Check for negative cycles\n    for(auto edge : edges)\n    {\n        if(distance[edge->start] == UNKNOWN)\n        {\n            continue;\n        }\n        if(distance[edge->start] + edge->weight < distance[edge->end])\n        {\n            return UNKNOWN;\n        }\n    }\n    return distance[N];\n    ```\n\n8.  现在，我们可以在`main()`中执行对`BellmanFord()`的调用，并相应地处理输出:\n\n    ```cpp\n    int result = BellmanFord(N * N, edges);\n    (result == UNKNOWN) ? cout << \"ABORT TRAVERSAL\" << endl \n                   : cout << -1 * result << endl;\n    return 0;\n    ```\n\n### 活动 16:随机化图统计\n\n在本活动中，我们将为面试测试生成随机图表，如活动简介中所述。按照以下步骤完成活动:\n\n1.  首先包含以下标题，并定义`UNKNOWN`常数和`Edge`结构:\n\n    ```cpp\n    #include <iostream>\n    #include <vector>\n    #include <iomanip>\n    #include <algorithm>\n    #include <queue>\n    #include <utility>\n    using namespace std;\n    const int UNKNOWN = 1e9;\n    struct Edge \n    {\n        int u;\n        int v;\n        int w;\n        Edge(int u, int v, int w) \n            : u(u), v(v), w(w) {}\n    };\n    ```\n\n2.  我们的首要任务是处理每个图的生成。对于这个活动，我们将把图数据封装在一个结构中:\n\n    ```cpp\n    struct Graph\n    {\n        int V, E;\n        int maxWeight = -1e9;\n        vector<Edge> edges;\n        vector<vector<int>> adj;\n        vector<vector<int>> weight;\n        Graph(int v, int e) : V(v), E(e) \n        {\n            ...\n        }\n    };\n    ```\n\n3.  为了确保生成的边和结果图是有效的，我们将创建一个邻接矩阵，并在每次尝试创建另一条边时检查它。如果同一两个节点之间的边已经存在，我们将开始另一次迭代。为了确保每个节点都至少有一条入边或出边，我们还会将矩阵中的对角线单元格设置为 true，用于作为边的一部分的每个节点。如果在创建`E`边后，任何对角线单元格为假，图将无效。我们可以通过将`V`设置为`-1` :\n\n    ```cpp\n    Graph(int v, int e) : V(v), E(e)\n    {\n        vector<vector<bool>> used(V, vector<bool>(V, false));\n        adj.resize(V);\n        weight.resize(V, vector<int>(V, UNKNOWN));\n        while(e)\n        {\n            // Generate edge values\n            int u = rand() % V;\n            int v = rand() % V;\n            int w = rand() % 100;\n            if(rand() % 3 == 0)\n            {\n                w = -w;\n            }\n            // Check if the edge is valid\n            if(u == v || used[u][v])\n            {\n                continue;\n            }\n            // Add to edges and mark as used\n            edges.push_back(Edge(u, v, w));\n            adj[u].push_back(v);\n            weight[u][v] = w;\n            maxWeight = max(maxWeight, w);\n            used[u][u] = used[v][v] = used[u][v] = used[v][u] = true;\n            e--;\n        }\n        for(int i = 0; i < V; i++)\n        {\n            // Set V to -1 to indicate the graph is invalid\n            if(!used[i][i])\n            {\n                V = -1;\n                break;\n            }\n        }\n    }\n    ```\n\n    来指示图无效\n4.  让我们定义一个名为`RESULT`的枚举，并为我们需要考虑的每种类型的图定义相应的值:\n\n    ```cpp\n    enum RESULT\n    {\n        VALID,\n        INVALID,\n        INTERESTING\n    };\n    ```\n\n5.  在`main()`中，我们将接收输入，并为每种类型的图声明计数器。然后，我们将循环给定的迭代次数，创建一个新的图，并调用一个`TestGraph()`函数，该函数将一个`Graph`对象作为输入并返回`RESULT`。根据返回的值，我们将相应地递增每个计数器:\n\n    ```cpp\n    int main()\n    {\n        unsigned int seed;\n        int iterations, V, E;\n\n        cin >> seed;\n        cin >> iterations;\n        cin >> V >> E;\n        int invalid = 0;\n        int valid = 0;\n        int interesting = 0;\n        srand(seed);\n        while(iterations--)\n        {\n            Graph G(V, E);\n\n            switch(TestGraph(G))\n            {\n                case INVALID: invalid++ ; break;\n                case VALID: valid++ ; break;\n                case INTERESTING: \n                {\n                    valid++ ;\n                    interesting++ ;\n                    break;\n                }\n            }\n        }\n\n        return 0;\n    }\n    ```\n\n6.  `TestGraph()`将首先检查每个图的`V`值是否等于`-1`，如果等于则返回`INVALID`。否则，它将执行约翰逊算法来检索最短距离。第一步是使用贝尔曼-福特算法检索重新加权数组:\n\n    ```cpp\n    RESULT TestGraph(Graph G)\n    {\n        if(G.V == -1)\n        {\n            return INVALID;\n        }\n\n        vector<int> distance = BellmanFord(G);\n        ……\n    }\n    ```\n\n7.  此解决方案中使用的 Bellman-Ford 的实现与练习中的实现完全一致，只是它接收了一个单一的`Graph`结构作为参数:\n\n    ```cpp\n    vector<int> BellmanFord(Graph G)\n    {\n        vector<int> distance(G.V + 1, UNKNOWN);\n        int s = G.V;\n        for(int i = 0; i < G.V; i++)\n        {\n            G.edges.push_back(Edge(s, i, 0));\n        }\n\n        distance[s] = 0;\n        for(int i = 0; i < G.V; i++)\n        {\n            for(auto edge : G.edges)\n            {\n                if(distance[edge.u] == UNKNOWN)\n                {\n                    continue;\n                }\n                if(distance[edge.u] + edge.w < distance[edge.v])\n                {\n                    distance[edge.v] = distance[edge.u] + edge.w;\n                }\n            }\n        }\n        for(auto edge : G.edges)\n        {\n            if(distance[edge.u] == UNKNOWN)\n            {\n                continue;\n            }\n            if(distance[edge.u] + edge.w < distance[edge.v])\n            {\n                return {};\n            }\n        }\n    return distance;\n    }\n    ```\n\n8.  正如我们在练习中所做的，我们将检查`BellmanFord()`返回的向量是否为空。如果是，我们返回`VALID`(图有效但没意思)。否则，我们将通过重新加权边并对每个顶点调用迪克斯特拉算法来完成剩余的约翰逊算法:\n\n    ```cpp\n    RESULT TestGraph(Graph G)\n    {\n        if(G.V == -1)\n        {\n            return INVALID;\n        }\n\n        vector<int> distance = BellmanFord(G);\n        if(distance.empty())\n        {\n            return VALID;\n        }\n        for(auto edge : G.edges)\n        {\n            G.weight[edge.u][edge.v] += (distance[edge.u] – distance[edge.v]);\n        }\n        double result = 0;\n        for(int i = 0; i < G.V; i++)\n        {\n            vector<int> shortest = Dijkstra(i, G);\n        }\n    }\n    ```\n\n9.  对于这个解决方案，让我们使用一种更有效形式的 Dijkstra 算法，它使用最小优先级队列来确定遍历顺序。为此，添加到队列中的每个值都必须由两个值组成:节点的索引及其距离值。我们将使用`std::pair<int, int>`来实现这一点，它在这里被重新定义为`State`。将元素推送到队列时，第一个值必须对应于距离，因为这将是优先级队列内部排序逻辑考虑的第一个值。所有这些都可以由`std::priority_queue`处理，但是我们需要提供三个分别对应于数据类型、容器和比较谓词的模板参数:\n\n    ```cpp\n    vector<int> Dijkstra(int source, Graph G)\n    {\n        typedef pair<int, int> State;\n        priority_queue<State, vector<State>, greater<State>> Q;\n        vector<bool> visited(G.V, false);\n        vector<int> distance(G.V, UNKNOWN);\n        Q.push({0, source});\n        distance[source] = 0;\n        while(!Q.empty())\n        {\n            State top = Q.top();\n            Q.pop();\n            int node = top.second;\n            int dist = top.first;\n            visited[node] = true;\n            for(auto next : G.adj[node])\n            {\n                if(visited[next])\n                {\n                    continue;\n                }\n                if(dist != UNKNOWN && distance[next] > dist + G.weight[node][next])\n                {\n                    distance[next] = distance[node] + G.weight[node][next];\n\n                    Q.push({distance[next], next});\n                }\n\n            }\n        }\n        return distance;\n    }\n    ```\n\n10.  现在，我们将计算每组路径的`TestGraph()`中的平均值。我们通过迭代`Dijkstra()`返回的数组并保持索引不等于起始节点索引的距离总和来实现这一点。对应的数值不等于`UNKNOWN`。每次找到有效距离时，计数器也会递增，这样我们就可以通过将总和除以计数来获得最终平均值。然后将这些平均值中的每一个加到总结果中，再除以图中顶点的总数。请记住，我们必须再次重新加权距离以获得正确的值:\n\n    ```cpp\n    double result = 0;\n    for(int i = 0; i < G.V; i++)\n    {\n        vector<int> shortest = Dijkstra(i, G);\n        double average = 0;\n        int count = 0;\n        for(int j = 0; j < G.V; j++)\n        {\n            if(i == j || shortest[j] == UNKNOWN)\n            {\n                continue;\n            }\n            shortest[j] += (distance[j] – distance[i]);\n            average += shortest[j];\n            count++ ;\n        }\n        average = average / count;\n        result += average;\n    }\n    result = result / G.V;\n    ```\n\n11.  最后一步是计算结果和图中最大权重的比值。如果数值小于`0.5`，我们返回`INTERESTING`；否则，我们退回`VALID` :\n\n    ```cpp\n    double ratio = result / G.maxWeight;\n    return (ratio < 0.5) ? INTERESTING : VALID;\n    ```\n\n12.  我们现在可以返回`main()`并打印输出。第一行将等于`invalid`的值。第二行等于`interesting / valid`，乘以`100`，以百分比显示。根据您的操作方式，您可能必须将变量转换为浮点型，以防止该值被舍入为整数。打印输出时，可以使用`cout << fixed << setprecision(2)` :\n\n    ```cpp\n    double percentInteresting = (double)interesting / valid * 100;\n    cout << \"INVALID GRAPHS: \" << invalid << endl;\n    cout << \"PERCENT INTERESTING: \" << fixed << setprecision(2) << percentInteresting << endl;\n    return 0;\n    ```\n\n    轻松确保四舍五入到两位小数\n\n### 活动 17:迷宫-传送游戏\n\n整个活动非常符合我们在本章中讨论的算法的标准实现，但有一些细微的修改。\n\n问题描述中使用的术语，即*迷宫*、*房间*、*传送点*和*点*当然也可以被称为*图*、*顶点*、*边*和*边权重*。玩家能够无限降低分数的情况可以重新定义为*负体重周期*。按照以下步骤完成活动:\n\n1.  让我们首先包含必要的标题，并为活动设置变量和输入:\n\n    ```cpp\n    #include <iostream>\n    #include <vector>\n    #include <stack>\n    #include <climits>\n    struct Edge\n    {\n        int start;\n        int end;\n        int weight;\n        Edge(int s, int e, int w) : start(s), end(e), weight(w) {}\n    }\n    const int UNKNOWN = INT_MAX;\n    vector<Edge*> edges; // Collection of edge pointers\n    ```\n\n2.  我们将以与我们最初的 Bellman-Ford 实现相同的形式接收输入，但是我们也将为我们的图构建邻接表(这里表示为整数向量的向量，`adj` ):\n\n    ```cpp\n    int main()\n    {\n        int V, E;\n        cin >> V >> E;\n        vector<Edge*> edges;\n        vector<vector<int>> adj(V + 1);\n        for(int i = 0; i < E; i++)\n        {\n            int u, v, w;\n            cin >> u >> v >> w;\n            edges.push_back(new Edge(u, v, w));\n            adj[u].push_back(v);\n        }\n        vector<int> results;\n    ```\n\n3.  问题的第一部分可以通过使用贝尔曼-福特来解决，其方式与*练习 32* 、*实现贝尔曼-福特算法(第一部分)*中概述的方式相同。但是，我们不会打印距离数组中的所有值，而是将它的返回类型设置为`int`，并包含几行额外的代码，以便它只返回距源顶点的最短距离(或者如果检测到负周期，则返回`UNKNOWN`:\n\n    ```cpp\n    int BellmanFord(int V, int start, vector<Edge*> edges)\n    {\n        // Standard Bellman-Ford implementation\n        vector<int> distance(V, UNKNOWN);\n\n        distance[start] = 0;\n        for(int i = 0; i < V - 1; i++)\n        {\n            for(auto edge : edges)\n            {\n                if(distance[edge->start] == UNKNOWN)\n                {\n                    continue;\n                }\n                if(distance[edge->start] + edge->weight < distance[edge->end])\n                {\n                    distance[edge->end] = distance[edge->start] + edge->weight;\n                }\n            }\n        }\n        // Return UNKNOWN if a negative cycle is found\n        if(HasNegativeCycle(distance, edges))\n        {\n            return UNKNOWN;\n        }\n        int result = UNKNOWN;\n        for(int i = 0; i < V; i++)\n        {\n            if(i == start) continue;\n            result = min(result, distance[i]);\n        }\n        return result;\n    }\n    ```\n\n4.  我们现在可以在`main()`中调用这个函数，并为输出填充一个结果向量。如果`BellmanFord()`恰好返回`UNKNOWN`，我们输出`INVALID MAZE`并终止程序(按照第一个条件)。如果某个起始节点没有输出边，我们可以完全跳过对`BellmanFord`的调用，只需将`UNKNOWN`追加到向量中。如果我们通过每个顶点，我们可以输出结果中的值(或者如果值为`UNKNOWN`，则输出`DEAD END`):\n\n    ```cpp\n    vector<int> results;\n    for(int i = 0; i < V; i++)\n    {\n        if(adj[i].empty())\n        {\n            results.push_back(UNKNOWN);\n            continue;\n        }\n        int shortest = BellmanFord(V, i, edges);\n        if(shortest == UNKNOWN)\n        {\n            cout << \"INVALID MAZE\" << endl;\n            return 0;\n        }\n        results.push_back(shortest);\n    }\n    for(int i = 0; i < V; i++)\n    {\n        cout << i << \": \";\n        (results[i] == INVALID) ? cout << \"DEAD END\" << endl : cout << results[i] << endl;\n    }\n    ```\n\n5.  Now, we've come to the final condition – finding rooms in which players can get \"stuck.\" Considering this case in terms of graph connectivity, we can redefine it as follows: find the strongly connected components that have no outgoing edges to other components. There are many simple ways to do this once all the strongly connected components have been acquired, but let's try to maximize our program's efficiency and add the necessary logic directly into our existing Kosaraju implementation.\n\n    为此，我们将声明两个新的向量:一个类型为`bool`，名为`isStuck`，另一个类型为`int`，名为`inComponent`。`inComponent`将存储每个节点所属组件的索引，而`isStuck`将告诉我们索引为`i`的组件是否与图的其余部分断开。\n\n    为了简单起见，让我们全局声明新变量:\n\n    ```cpp\n    vector<bool> isStuck;\n    vector<int> inComponent;\n    int componentIndex;\n    ```\n\n    在这里，我们可以真正开始理解图结构的封装和面向对象实现的好处。不得不在我们的函数之间传递如此大量的数据，这不仅很难在心理上进行跟踪，而且会使我们未来可能想要进行的任何类型的修改变得非常复杂(更不用说像`GetComponent(node, adj, visited, component, isStuck, inComponent, componentIndex)`这样的函数调用令人头痛的外观了)。出于示例和可读性的考虑，我们选择全局声明这些数据，但是强烈建议不要在实际的全尺寸应用中使用这种方法。\n\n6.  在我们的`Kosaraju`函数中，我们初始化新数据如下:\n\n    ```cpp\n    isStuck.resize(V, true);\n    inComponent.resize(V, UNKNOWN);\n    componentIndex = 0;\n    ```\n\n7.  现在，我们将开始我们的`while`循环，通过跟踪在栈上执行的每个 DFS 遍历来增加`componentIndex`:\n\n    ```cpp\n    while(!stack.empty())\n    {\n        int node = stack.top();\n        stack.pop();\n        if(!visited[node])\n        {\n            vector<int> component;\n            GetComponent(node, transpose, visited, component);\n            components.push_back(component);\n            componentIndex++ ;\n        }\n    }\n    ```\n\n8.  Now, we can write the logic in `GetComponent()`, which will handle this case. We will begin by setting the value of each node's index in `inComponent` to `componentIndex`. Now, as we iterate through each node's neighbors, we will include another condition that occurs when the nodes have already been visited:\n\n    ```cpp\n    component.push_back(node);\n    visited[node] = true;\n    inComponent[node] = componentIndex;\n    for(auto next : adj[node])\n    {\n        if(!visited[next])\n        {\n            GetComponent(next, visited, adj, component);\n        }\n        else if(inComponent[node] != inComponent[next])\n        {\n            isStuck[inComponent[next]] = false;\n        }\n    }\n    ```\n\n    本质上，我们正在检查每个先前访问过的邻居的组件是否与当前节点的组件匹配。如果它们各自的组件标识不同，我们可以得出结论，邻居的组件有一条路径延伸到图的其他部分。\n\n    您可能想知道，为什么在有向图中，当前节点的边的存在表明相邻节点有一条超出其自身组件的输出路径。这个逻辑看起来“落后”的原因是因为它确实如此。请记住，我们正在遍历原始图的变换，因此邻接之间的方向都是相反的！\n\n9.  完成 DFS 遍历后，我们现在可以返回`components`向量并打印结果:\n\n    ```cpp\n    auto components = Kosaraju(V, adj);\n    for(int i = 0; i < components.size(); i++)\n    {\n        if(isStuck[i])\n        {\n            for(auto node : components[i])\n            {\n                cout << node << \" \";\n            }\n            cout << endl;\n        }\n    }\n    return 0;\n    ```\n\n## 第八章:动态规划一\n\n### 活动 18:旅行行程\n\n让我们首先考虑这个问题的基本情况和递归关系。与我们在本章中讨论的其他一些例子不同，这个特殊的问题只有一个基本情况——到达目的地的点。中间状态也很简单:给定一个位于索引`i`的位置，该位置的距离限制为`x`，我们可以到达索引`i + 1`和`i + x`之间的任何位置(包括这两个位置)。例如，让我们考虑以下两个城市:\n\n*   城市 1: `distance[1] = 2`\n*   城市 2: `distance[2] = 1`\n\n假设我们想计算到达指数`3`城市的途径数。因为我们既可以从*城市 1* 又可以从*城市 2* 到达*城市 3* ，到达*城市 3* 的途径数相当于到达城市 1 的途径数和到达*城市 2* 的途径数之和。这种递归与斐波那契数列非常相似，只是形成当前状态子结构的先前状态的数量根据`distance`的值是可变的。\n\n假设我们有以下四个城市:\n\n```cpp\n[1]: distance = 5\n[2]: distance = 3\n[3]: distance = 1\n[4]: distance = 2\n```\n\n由此，我们要计算到城市 5 的旅行方式的数量。为此，我们可以将子结构表述如下:\n\n```cpp\nCities reachable from index [1] -> { 2 3 4 5 6 }\nCities reachable from index [2] -> { 3 4 5 }\nCities reachable from index [3] -> { 4 }\nCities reachable from index [4] -> { 5 6 }\n```\n\n我们现在可以颠倒这个逻辑，从中找到我们可以通过到达给定位置的城市*:*\n\n```cpp\nCities that connect to index [1] -> START\nCities that connect to index [2] -> { 1 }\nCities that connect to index [3] -> { 1 2 }\nCities that connect to index [4] -> { 1 2 3 }\nCities that connect to index [5] -> { 1 2 }\n```\n\n更进一步，我们现在可以设计出状态逻辑的轮廓:\n\n```cpp\nWays to reach City 1 = 1 (START)\nWays to reach City 2 = 1 \n    1 \" 2\nWays to reach City 3 = 2\n    1 \" 2 \" 3\n    1 \" 3\nWays to reach City 4 = 4\n    1 \" 2 \" 3 \" 4\n    1 \" 2 \" 4\n    1 \" 3 \" 4\n    1 \" 4\nWays to reach City 5 = 6\n    1 \" 2 \" 3 \" 4 \" 5\n    1 \" 2 \" 4 \" 5\n    1 \" 2 \" 5\n    1 \" 3 \" 4 \" 5\n    1 \" 4 \" 5\n    1 \" 5\n```\n\n因此，我们可以如下定义重现:\n\n*   Base case:\n\n    *F(1) = 1* (我们已经到达目的地)\n\n*   重复:\n\n![Figure 8.22: Formula for defining recurrence](img/C14498_08_22.jpg)\n\n###### 图 8.22:定义循环的公式\n\n换句话说，到达给定位置的方式数等于到达与其相连的每个位置的方式数之和。使用这个逻辑，解决这个问题的递归函数可能如下所示:\n\n```cpp\nF(n) -> number of ways to reach n'th location\nF(i) = \n    if i = N: \n         return 1 \n        Otherwise:\n            result = 0\n            for j = 1 to distance[i]:\n                result = result + F(i + j)\n            return result\n```\n\n现在我们已经有了问题状态的功能定义，让我们开始用代码实现它。\n\n1.  对于这个问题，我们将包括以下头和`std`名称空间:\n\n    ```cpp\n    #include <iostream>\n    #include <vector>\n    #include <algorithm>\n    using namespace std;\n    ```\n\n2.  因为这个问题的输出需要计算超过 32 位的数字，所以我们将使用`long long int`作为结果。为了避免重复写这个，我们将使用`typedef`语句将其缩写为:\n\n    ```cpp\n    typedef long long LL;\n    ```\n\n3.  Finally, we will define the modulus value for outputting the results:\n\n    ```cpp\n    const LL MOD = 1000000007;\n    ```\n\n    处理这个问题中的输入和输出可以非常简单地实现:\n\n    ```cpp\n    int main()\n    {\n        int n;\n        cin >> n;\n\n    vector<int> distance(n);\n        for(int i = 0; i < n; i++)\n        {\n            cin >> distance[i];\n        }\n        LL result = TravelItinerary(n, distance);\n        cout << result << endl;\n        return 0;\n    }\n    ```\n\n4.  我们现在将定义一个名为`TravelItinerary()`的函数，该函数以`n`和`distance`为参数，并返回一个长整数:\n\n    ```cpp\n    LL TravelItinerary(int n, vector<int> distance)\n    {\n        ...\n    }\n    ```\n\n5.  现在，我们必须将前面介绍的递归算法转换为自下而上的方法。在伪代码中，这可能如下所示:\n\n    ```cpp\n    DP -> Array of size N + 1\n    DP[0] = 1 (There is one way to reach the starting location)\n    for i = 0 to N-1:\n        for j = 1 to distance[i]: \n\n            DP[i + j] += DP[i]\n    return DP[N]\n    ```\n\n6.  要在 C++ 中对此进行编码，我们将首先声明一个大小为`n + 1`的一维 DP 表，并将其所有元素初始化为`0`。然后，我们将它的第一个元素设置为`1`来表示基本情况:\n\n    ```cpp\n    vector<LL> DP(n + 1, 0);\n    DP[0] = 1;\n    ```\n\n7.  To implement the recurrence we described previously, we will first reverse the distance array so that we are essentially beginning our calculations from the destination index. There are several reasons for this, but the primary reason is so that our algorithm processes the current state by combining the results of earlier states, as opposed to calculating future states from the results of the current state. Though the logic described in the pseudocode will produce the correct result, it is generally preferable to formulate bottom-up logic in terms of how the solutions of the previous states form the result of the immediate state:\n\n    ```cpp\n    reverse(distance.begin(), distance.end());\n    DP[0] = 1;\n    for(int i = 1; i <= n; i++)\n    {\n        int dist = distance[i-1];\n        for(int j = 1; j <= dist; j++)\n        {\n            DP[i] = (DP[i] + DP[i – j]) % MOD;\n        }\n    }\n    return DP[n];\n    ```\n\n    这当然是一个可行的解决问题的办法，在绝大多数情况下是完全令人满意的。然而，由于动态编程首先是一种优化技术，我们仍然应该问自己是否存在更好的方法。\n\n    **处理额外信用测试案例**\n\n    随着`n`和最大`distance`值的增加，甚至前面的算法最终也会被证明效率相当低。如果`n = 10000000`和距离值可以在 1 到 10000 之间变化，那么在最坏的情况下，内部`for`循环将不得不执行近 10000000000 次迭代。谢天谢地，有一种非常简单的技术可以让我们完全去除内部循环，这意味着我们必须对任何输入进行精确的`n`迭代。\n\n    为了处理这种减少，我们将创建一个`prefix sum array`，它将允许我们计算以前由内部循环在恒定时间内处理的范围和。如果您不熟悉这种技术，基本概念如下:\n\n    *   创建一个名为`sums`的数组，其长度等于要求和的值的总数加 1，所有元素初始化为`0`。\n    *   对于从`0`到`n`的每个指标`i`，使用`sum[i + 1] = sum[i] + distance[i]`。\n    *   计算总和后，任何范围内所有元素的总和`[L, R]`将等于`sum[R+1] – sum[L]`。\n\n看看下面的例子:\n\n```cpp\n        0 1  2  3  4\nA    =   { 3 1 10  2  5 } \n           0 1 2  3  4  5\nsums  =  { 0 3 4 14 16 21 }\nrange(1, 3) = A[1] + A[2] + A[3]\n         = 1 + 10 + 2\n         = 13\nsums[4]  – sums[1] = 13\nrange(3, 4) = A[3] + A[4]\n        = 2 + 5\n        = 7\nsums[5] – sums[3] = 7\n```\n\n1.  我们可以在函数中实现这种方法，如下所示:\n\n    ```cpp\n    LL TravelItinerary(int n, vector<int> distance)\n    {\n        vector<LL> DP(n + 1, 0);\n        vector<LL> sums(n + 2, 0);\n        DP[0] = sums[1] = 1;\n        reverse(distance.begin(), distance.end());\n        for(int i = 1; i <= n; i++)\n        {\n            int dist = distance[i-1];\n            LL sum = sums[i] – sums[i – dist];\n            DP[i] = (DP[i] + sum) % MOD;\n            sums[i + 1] = (sums[i] + DP[i]) % MOD;\n        }\n        return DP[n];\n    }\n    ```\n\n2.  现在，您可能还会遇到一个问题，那就是前面的函数返回的结果将是负的。这是因为模运算导致`sums`中索引较高的值小于索引较低的值，这导致减法时的结果为负。这种问题在需要对非常大的数字进行频繁模运算的问题中非常常见，但是可以通过稍微修改 return 语句来轻松解决:\n\n    ```cpp\n    return (DP[n] < 0) ? DP[n] + MOD : DP[n];\n    ```\n\n通过这些细微的修改，我们现在有了一个优雅而高效的解决方案，可以在几分之一秒内处理大量输入数组！\n\n### 活动 19:利用记忆寻找最长的公共子序列\n\n1.  就像我们处理子集和问题一样，我们将在同一个代码文件中包含每种新方法，这样我们就可以比较它们的相对性能。为此，让我们以与之前相同的方式定义我们的`GetTime()`函数:\n\n    ```cpp\n    vector<string> types =\n    {\n        \"BRUTE FORCE\",\n        \"MEMOIZATION\",\n        \"TABULATION\"\n    };\n    const int UNKNOWN = INT_MAX;\n    void GetTime(clock_t &timer, string type)\n    {\n        timer = clock() - timer;\n        cout << \"TIME TAKEN USING \" << type << \": \" << fixed << setprecision(5) << (float)timer / CLOCKS_PER_SEC << \" SECONDS\" << endl;\n        timer = clock();\n    }\n    ```\n\n2.  现在，让我们定义我们的新函数`LCS_Memoization()`，它将采用与`LCS_BruteForce()`相同的参数，除了`subsequence`将改为对二维整数向量的引用`memo` :\n\n    ```cpp\n    int LCS_Memoization(string A, string B, int i, int j, vector<vector<int>> &memo)\n    {\n        ……\n    }\n    ```\n\n3.  这个函数的代码也与`LCS_BruteForce()`非常相似，除了我们将递归遍历两个字符串的前缀(从完整的字符串开始)并将结果存储在我们的`memo`表中的每一步:\n\n    ```cpp\n    // Base case — LCS is always zero for empty strings\n    if(i == 0 || j == 0)\n    {\n        return 0;\n    }\n    // Have we found a result for the prefixes of the two strings?\n    if(memo[i - 1][j - 1] != UNKNOWN)\n    {\n        // If so, return it\n        return memo[i - 1][j - 1];\n    }\n    // Are the last characters of A's prefix and B's prefix equal?\n    if(A[i-1] == B[j-1])\n    {\n        // LCS for this state is equal to 1 plus the LCS of the prefixes of A and B, both reduced by one character\n        memo[i-1][j-1] = 1 + LCS_Memoization(A, B, i-1, j-1, memo);\n        // Return the cached result\n        return memo[i-1][j-1];\n    }\n    // If the last characters are not equal, LCS for this state is equal to the maximum LCS of A's prefix reduced by one character and B's prefix, and B's prefix reduced by one character and A's prefix\n    memo[i-1][j-1] = max(LCS_Memoization(A, B, i-1, j, memo), \n                     LCS_Memoization(A, B, i, j-1, memo));\n    return memo[i-1][j-1];\n    ```\n\n4.  现在，让我们重新定义我们的`main()`函数来执行这两种方法，并显示每种方法花费的时间:\n\n    ```cpp\n    int main()\n    {\n        string A, B;\n        cin >> A >> B;\n        int tests = 2;\n        clock_t timer = clock();\n        for(int i = 0; i < tests; i++)\n        {\n            int LCS;\n            switch(i)\n            {\n                case 0:\n                {\n                    LCS = LCS_BruteForce(A, B, 0, 0, {});\n                #if DEBUG\n                    PrintSubsequences(A, B);\n                #endif\n                    break;\n                }\n                case 1:\n                {\n                    vector<vector<int>> memo(A.size(), vector<int>(B.size(), UNKNOWN));\n                    LCS = LCS_Memoization(A, B, A.size(), B.size(), memo);\n                    break;\n                }\n            }\n            cout << \"Length of the longest common subsequence of \" << A << \" and \" << B << \" is: \" << LCS << ends;\n            GetTime(timer, types[i]);\n            cout << endl;\n        }\n        return 0;\n    }\n    ```\n\n5.  现在，让我们尝试在两个新字符串`ABCABDBEFBA`和`ABCBEFBEAB`上执行我们的两个算法。您的程序输出应该类似于以下内容:\n\n    ```cpp\n    SIZE = 3\n        ABC________ ABC_______\n    SIZE = 4\n        ABC_B______ ABCB______\n        ABC_B______ ABC___B___\n        ABC_B______ ABC______B\n        ABC___B____ ABC______B\n        ABC____E___ ABC____E__\n        ABC______B_ ABC___B___\n        ABC______B_ ABC______B\n        ABC_______A ABC_____A_\n    SIZE = 5\n        ABCAB______ ABC_____AB\n        ABC_B_B____ ABCB_____B\n        ABC_B__E___ ABCB___E__\n        ABC_B____B_ ABCB__B___\n        ABC_B____B_ ABCB_____B\n        ABC_B_____A ABCB____A_\n        ABC_B_B____ ABC___B__B\n        ABC_B__E___ ABC___BE__\n        ABC_B____B_ ABC___B__B\n        ABC_B_____A ABC___B_A_\n        ABC___BE___ ABC___BE__\n        ABC____E_B_ ABC____E_B\n        ABC____E__A ABC____EA_\n        ABC_____FB_ ABC__FB___\n        ABC______BA ABC___B_A_\n    SIZE = 6\n        ABC_B_BE___ ABCB__BE__\n        ABC_B__E_B_ ABCB___E_B\n        ABC_B__E__A ABCB___EA_\n        ABC_B___FB_ ABCB_FB___\n        ABC_B____BA ABCB__B_A_\n        ABC_B__E_B_ ABC___BE_B\n        ABC_B__E__A ABC___BEA_\n        ABC___BE_B_ ABC___BE_B\n        ABC___BE__A ABC___BEA_\n        ABC____EFB_ ABC_EFB___\n        ABC_____FBA ABC__FB_A_\n    SIZE = 7\n        ABC_B_BE_B_ ABCB__BE_B\n        ABC_B_BE__A ABCB__BEA_\n        ABC_B__EFB_ ABCBEFB___\n        ABC_B___FBA ABCB_FB_A_\n        ABC____EFBA ABC_EFB_A_\n    SIZE = 8\n        ABC_B__EFBA ABCBEFB_A_\n    Length of the longest common subsequence of ABCABDBEFBA and ABCBEFBEAB is: 8\n    TIME TAKEN USING BRUTE FORCE: 0.00242 SECONDS\n    Length of the longest common subsequence of ABCABDBEFBA and ABCBEFBEAB is: 8\n    TIME TAKEN USING MEMOIZATION: 0.00003 SECONDS\n    ```\n\n6.  当然，暴力方法所花费的时间会受到打印出子序列的额外步骤的影响。将`DEBUG`常量设置为`0`后，再次运行我们的代码，现在输出如下:\n\n    ```cpp\n    Length of the longest common subsequence of ABCABDBEFBA and ABCBEFBEAB is: 8\n    TIME TAKEN USING BRUTE FORCE: 0.00055 SECONDS\n    Length of the longest common subsequence of ABCABDBEFBA and ABCBEFBEAB is: 8\n    TIME TAKEN USING MEMOIZATION: 0.00002 SECONDS\n    ```\n\n7.  Now, let's try pushing the limits of our algorithm using two much larger strings, `ABZCYDABAZADAEA` and `YABAZADBBEAAECYACAZ`. You should get an output something like this:\n\n    ```cpp\n    Length of the longest common subsequence of ABZCYDABAZADAEA and YABAZADBBEAAECYACAZ is: 10\n    TIME TAKEN USING BRUTE FORCE: 8.47842 SECONDS\n    Length of the longest common subsequence of ABZCYDABAZADAEA and YABAZADBBEAAECYACAZ is: 10\n    TIME TAKEN USING MEMOIZATION: 0.00008 SECONDS\n    ```\n\n    #### 注意\n\n    所用时间的实际值会因您的系统而异。请注意数值的差异。\n\n我们可以清楚地看到，记忆化带来的性能提升非常显著！\n\n### 活动 20:使用列表寻找最长的公共子序列\n\n正如我们之前所做的，我们将向包含我们的强力和记忆化解决方案的同一个代码文件中添加一个新函数`LCS_Tabulation()`。\n\n1.  我们的`LCS_Tabulation()`函数接收两个参数——字符串`A`和`B`，并返回一个字符串:\n\n    ```cpp\n    string LCS_Tabulation(string A, string B)\n    {\n        ……\n    } \n    ```\n\n2.  我们的第一步是定义我们的 DP 表，我们将它表示为一个二维整数向量，第一维的大小等于比字符串`A`大一个，第二维的大小等于比字符串`B`大一个:\n\n    ```cpp\n    vector<vector<int>> DP(A.size() + 1, vector<int>(B.size() + 1));\n    ```\n\n3.  像子集和问题一样，我们算法的所有逻辑可以包含在两个嵌套循环中，第一个循环从`0`迭代到`A`的大小，第二个循环从`0`迭代到`B`的大小:\n\n    ```cpp\n    for(int i = 0; i <= A.size(); i++)\n    {\n        for(int j = 0; j <= B.size(); j++)\n        {\n            ……\n        }\n    }\n    ```\n\n4.  与子集和问题不同，我们的基本情况不会在循环执行之前处理，而是在每个循环开始时处理。这是因为只要`A`或`B`的前缀为空(即`i = 0`或`j = 0`，我们的基本情况就会发生。这在我们的代码中表示如下:\n\n    ```cpp\n    if(i == 0 || j == 0)\n    {\n        DP[i][j] = 0;\n    }\n    ```\n\n5.  现在，我们必须处理 *A* 前缀和 *B* 前缀末尾的字符相等的情况。请记住，该州的 LCS 值始终等于`1`，加上该州的 LCS 值，其中两个前缀都比当前小一个字符。这可以表示如下:\n\n    ```cpp\n    else if(A[i-1] == B[j-1])\n    {\n        DP[i][j] = DP[i-1][j-1] + 1;\n    }\n    ```\n\n6.  对于最后一种情况，结尾字符是*而不是*相等。对于这种状态，我们知道 LCS 等于 *A* 的前一个前缀和 *B* 的当前前缀的 LCS，以及 B 的前一个前缀和 A 的当前前缀的 LCS 的最大值。就我们表的结构而言，这相当于说 LCS 等于表的同一列和前一行中包含的值的最大值，以及同一行和前一列中包含的值:\n\n    ```cpp\n    else\n    {\n        DP[i][j] = max(DP[i-1][j], DP[i][j-1]);\n    }\n    ```\n\n7.  When we are done, the length of the longest common subsequence will be contained in `DP[A.size()][B.size()]` – the value of the LCS when the prefixes of both `A` and `B` are equal to the entire strings. Therefore, our complete DP logic is written as follows:\n\n    ```cpp\n    string LCS_Tabulation(string A, string B)\n    {\n        vector<vector<int>> DP(A.size() + 1, vector<int>(B.size() + 1));\n        for(int i = 0; i <= A.size(); i++)\n        {\n            for(int j = 0; j <= B.size(); j++)\n            {\n                if(i == 0 || j == 0)\n                {\n                    DP[i][j] = 0;\n                }\n                else if(A[i-1] == B[j-1])\n                {\n                    DP[i][j] = DP[i-1][j-1] + 1;\n                }\n                else\n                {\n                    DP[i][j] = max(DP[i-1][j], DP[i][j-1]);\n                }\n            }\n        }\n        int length = DP[A.size()][B.size()];\n        ……\n    }\n    ```\n\n    在这一点上，我们已经讨论了找到最长公共子序列长度的几种方法，但是如果我们也想输出它的实际字符呢？当然，我们的强力解决方案可以做到这一点，但效率非常低；但是，使用前面 DP 表中包含的结果，我们可以非常容易地使用回溯来重建 LCS。让我们在表格中突出显示实现这一目标需要遵循的路径:\n\n    ![Figure 8.23: Activity 20 DP table ](img/C14498_08_23.jpg)\n\n    ###### 图 8.23:活动 20 DP 表\n\n    通过收集与值增加的路径中的每一列相关联的字符，我们得到 LCS `ABCBEFBA`。\n\n8.  Let's define a function called `ReconstructLCS()` that takes `A`, `B`, `i`, `j`, and `DP` as arguments. Our backtracking logic can be defined as follows:\n\n    ```cpp\n    if i = 0 or j = 0:\n        Return an empty string\n    If the characters at the end of A's prefix and B's prefix are equal:\n        Return the LCS of the next smaller prefix of both A and B, plus the equal character\n    Otherwise:\n        If the value of DP(i - 1, j) is greater than the value of DP(i, j - 1):\n          – Return the LCS of A's next smaller prefix with B's current prefix\n          – Otherwise:\n              Return the LCS of B's next smaller prefix with A's current prefix\n    ```\n\n    在 C++ 中，这可以编码如下:\n\n    ```cpp\n    string ReconstructLCS(vector<vector<int>> &DP, string &A, string &B, int i, int j)\n    {\n        if(i == 0 || j == 0)\n        {\n            return \"\";\n        }\n        if(A[i-1] == B[j-1])\n        {\n            return ReconstructLCS(DP, A, B, i-1, j-1) + A[i-1];\n        }\n        else if(DP[i-1][j] > DP[i][j-1])\n        {\n            return ReconstructLCS(DP, A, B, i-1, j);\n        }\n        else\n        {\n            return ReconstructLCS(DP, A, B, i, j-1);\n        }\n    }\n    ```\n\n9.  现在，我们可以在`LCS_Tabulation()`的最后一行返回`ReconstructLCS()`的结果:\n\n    ```cpp\n    string LCS_Tabulation(string A, string B)\n    {\n        ……\n        string lcs = ReconstructLCS(DP, A, B, A.size(), B.size());\n        return lcs; \n    }\n    ```\n\n10.  我们在`main()`中的代码现在应该被修改以适应`LCS_Tabulation()`的增加:\n\n    ```cpp\n    int main()\n    {\n        string A, B;\n        cin >> A >> B;\n        int tests = 3;\n        clock_t timer = clock();\n        for(int i = 0; i < tests; i++)\n        {\n            int LCS;\n            switch(i)\n            {\n                ……\n                case 2:\n                {\n                    string lcs = LCS_Tabulation(A, B);\n                    LCS = lcs.size();\n                    cout << \"The longest common subsequence of \" << A << \" and \" << B << \" is: \" << lcs << endl;\n                    break; \n                }\n            }\n            cout << \"Length of the longest common subsequence of \" << A << \" and \" << B << \" is: \" << LCS << endl;\n            GetTime(timer, types[i]);\n        }\n        return 0;\n    }\n    ```\n\n11.  Using the strings `ABCABDBEFBA` and `ABCBEFBEAB`, your program's output should be similar to this:\n\n    ```cpp\n    Length of the longest common subsequence of ABCABDBEFBA and ABCBEFBEAB is: 8\n    TIME TAKEN USING BRUTE FORCE: 0.00060 SECONDS\n    Length of the longest common subsequence of ABCABDBEFBA and ABCBEFBEAB is: 8\n    TIME TAKEN USING MEMOIZATION: 0.00005 SECONDS\n    The longest common subsequence of ABCABDBEFBA and ABCBEFBEAB is: ABCBEFBA\n    Length of the longest common subsequence of ABCABDBEFBA and ABCBEFBEAB is: 8\n    TIME TAKEN USING TABULATION: 0.00009 SECONDS\n    ```\n\n    #### 注意\n\n    所用时间的实际值会因您的系统而异。请注意数值的差异。\n\n现在，我们看了另一个详细的例子，说明如何使用不同的技术将相同的逻辑应用于相同的问题，以及这对算法执行时间的相应影响。\n\n### 活动 21:旋律排列\n\n首先要问自己的问题是:在这个问题中，什么构成了一个单一的状态？\n\n基本案例->空集合:\n\n1.  考虑旋律中的每个音符。\n2.  对于之前遇到的每个笔记子集，要么追加当前笔记，要么什么都不做。\n3.  如果子集与目标匹配，将其添加到解决方案中。\n\n考虑到我们的选择是要么在前一个子集上附加一个注释，要么保持原样，我们可以将逻辑重述如下:\n\n对于旋律中的给定音符，包含该音符的大小为| n |的子集的计数等于不包含该音符的大小为| n - 1 |的所有子集的总计数。\n\n所以，每个状态都可以用两个维度来表达:\n\n*   **维度 1** :到目前为止考虑的旋律长度。\n*   **次元 2** :取一个之前找到的子集，将位于旋律索引`[length - 1]`的音符附加到它上面，或者什么都不做，形成的结果子集。\n\n在伪代码中，逻辑可以表达如下:\n\n```cpp\nfor i = 1 to length of melody (inclusive):\n    for each subset previously found:\n    DP(i, subset) = DP(i, subset) + DP(i - 1, subset)\n    DP(i, subset ∪ melody[i - 1]) = DP(i, subset ∪ melody[i - 1]) + DP(i - 1, subset)\n```\n\n所以，现在的首要问题是，我们如何代表这些状态？\n\n请记住，对于一个 *n* 元素集合，总共有 *2* *n* 个子集组成，例如，一组 4 个元素可以分成总共 *2* *4* (或 16)个子集:\n\n```cpp\nS = { A, B, C, D }\n{ }            —>        { _ _ _ _ }\n{ A }          —>        { # _ _ _ }\n{ B }          —>        { _ # _ _ }\n{ C }          —>        { _ _ #_  }\n{ D }          —>        { _ _ _ # }\n{ A, B }       —>        { # # _ _ }\n{ A, C }       —>        { # _ #_  }\n{ A, D }       —>        { # _ _ # }\n{ B, C }       —>        { _ # #_  }\n{ B, D }       —>        { _ # _ # }\n{ C, D }       —>        { _ _ # # }\n{ A, B, C }    —>        { # # # _ }\n{ A, B, D }    —>        { # # _ # }\n{ A, C, D }    —>        { # _ # # }\n{ B, C, D }    —>        { _ # # # }\n{ A, B, C, D } —>        { # # # # }\n```\n\n如果我们以二进制形式从 *0* 迭代到 *(2* *4* *- 1)* ，我们得到以下数字:\n\n```cpp\n0     —>    0000    —>    { _ _ _ _ }\n1     —>    0001    —>    { # _ _ _ }\n2     —>    0010    —>    { _ # _ _ }\n3     —>    0011    —>    { # # _ _ }\n4     —>    0100    —>    { _ _ # _ }\n5     —>    0101    —>    { # _ # _ }\n6     —>    0110    —>    { _ # # _ }\n7     —>    0111    —>    { # # # _ }\n8     —>    1000    —>    { _ _ _ # }\n9     —>    1001    —>    { # _ _ # }\n10    —>    1010    —>    { _ # _ # }\n11    —>    1011    —>    { # # _ # }\n12    —>    1100    —>    { _ _ # # }\n13    —>    1101    —>    { # _ # # }\n14    —>    1110    —>    { _ # # # }\n15    —>    1111    —>    { # # # # }\n```\n\n如我们所见，从 *0* 到 *2* *n* 的每个二进制数的数字正好对应于 n 个元素的一个可能子集的索引。由于音阶中有 12 个音符，这意味着总共有 *2* *12* (或 4，096)个可能的音符子集。通过将音阶中的每个音符映射到 2 的幂，我们可以使用按位算术来表示每个状态中遇到的子集。\n\n以下是解决此活动的步骤:\n\n1.  继续讨论代码，我们应该从包含以下标题开始:\n\n    ```cpp\n    #include <iostream>\n    #include <vector>\n    #include <string>\n    #include <map>\n    using namespace std;\n    ```\n\n2.  让我们从处理`main()`功能中的输入开始:\n\n    ```cpp\n    int main()\n    {\n        int melodyLength;\n        int setLength;\n        cin >> melodyLength;\n        vector<string> melody(melodyLength);\n        for(int i = 0; i < melodyLength; i++)\n        {\n            cin >> melody[i];\n        }\n        cin >> setLength;\n        vector<string> set(setLength);\n        for(int i = 0; i < setLength; i++)\n        {\n            cin >> set[i];\n        }\n        ……\n    }\n    ```\n\n3.  现在，让我们编写一个名为`ConvertNotes()`的函数，该函数接收一个音符串向量作为输入，并返回它们对应整数值的向量。音阶中 12 个总音符中的每一个都需要被映射到一个特定的位(从`A`开始)，其中在声学上等同的音符被分配到相同的值。我们将使用`std::map`来处理转换:\n\n    ```cpp\n    vector<int> ConvertNotes(vector<string> notes)\n    {\n        map<string, int> M = \n        {\n            { \"A\",  0 }, \n            { \"A#\", 1 },\n            { \"Bb\", 1 },\n            { \"B\",  2 },\n            { \"Cb\", 2 },\n            { \"B#\", 3 },\n            { \"C\",  3 },\n            { \"C#\", 4 },\n            { \"Db\", 4 },\n            { \"D\",  5 },\n            { \"D#\", 6 },\n            { \"Eb\", 6 },\n            { \"E\",  7 },\n            { \"Fb\", 7 },\n            { \"E#\", 8 },\n            { \"F\",  8 },\n            { \"F#\", 9 },\n            { \"Gb\", 9 },\n            { \"G\",  10 },\n            { \"G#\", 11 },\n            { \"Ab\", 11 }\n        };\n        vector<int> converted;\n        for(auto note : notes)\n        {\n            // Map to powers of 2\n            converted.push_back(1 << M[note]); \n        }\n        return converted;\n    }\n    ```\n\n4.  现在，我们将定义一个名为`CountMelodicPermutations()`的函数，该函数以两个整数向量`melody`和`set`作为参数，并返回一个整数:\n\n    ```cpp\n    int CountMelodicPermutations(vector<int> melody, vector<int> set)\n    {\n        ……\n    }\n    ```\n\n5.  我们的第一步是定义我们的目标子集。我们将使用按位或运算符\n\n    ```cpp\n    unsigned int target = 0;\n    for(auto note : set)\n    {\n        target |= note;\n    }\n    ```\n\n    来实现这一点\n6.  例如，如果我们的目标集是`{ C, F#, A }`，映射将如下所示:\n\n    ```cpp\n    C  = 3\n    F# = 9\n    A  = 0\n    converted = { 23, 29, 20 } = { 8, 512, 1 }\n    target = (8 | 512 | 1) = 521\n        0000001000\n      + 0000000001\n      + 1000000000\n      = 1000001001\n    ```\n\n7.  我们现在将定义一个二维 DP 表，第一维初始化为`melodyLength + 1`，第二维初始化为大于最大子集值的一个值(即`111111111111 = 2` `12` `- 1`，因此第二维将包含 *2* *12* ，或 4，096 个元素):\n\n    ```cpp\n    vector<vector<int>> DP(melody.size() + 1, vector<int>(4096, 0));\n    ```\n\n8.  Our DP formula can be defined as follows:\n\n    ```cpp\n    Base case:\n        DP(0, 0) —> 1 \n    Recurrence:\n        DP(i, subset) —> DP(i, subset) + DP(i - 1, subset)\n        DP(i, subset ∪ note[i-1]) —> DP(i, subset ∪ note[i]) + DP(i - 1, subset)\n    ```\n\n    这里`i`的范围从`1`到旋律的长度。我们可以这样用 C++ 编写前面的逻辑:\n\n    ```cpp\n    // Base case —> empty set\n    DP[0][0] = 1;\n    for(int i = 1; i <= melody.size(); i++)\n    {\n        for(unsigned int subset = 0; subset < 4096; subset++)\n        {\n            // Keep results for previous values of i\n            DP[i][subset] += DP[i-1][subset];\n            // Add results for union of subset with melody[i-1]\n            DP[i][subset | melody[i-1]] += DP[i-1][subset];\n        }\n    }\n    // Solution\n    return DP[melody.size()][target];\n    ```\n\n9.  现在，我们可以通过调用`CountMelodicPermutations`并输出结果:\n\n    ```cpp\n    int count = CountMelodicPermutations(ConvertNotes(melody), ConvertNotes(set));\n    cout << count << endl;\n    ```\n\n    来完成我们的`main()`功能\n\n## 第九章:动态规划二\n\n### 活动 22:利润最大化\n\n在这项活动中，我们将优化我们的库存以实现利润最大化。按照以下步骤完成活动:\n\n1.  让我们从包含以下标题开始:\n\n    ```cpp\n    #include <iostream>\n    #include <vector>\n    using namespace std;\n    ```\n\n2.  首先，我们将定义一个结构`Product`，它封装了与每个项目相关的数据:\n\n    ```cpp\n    struct Product \n    {\n        int quantity;\n        int price;\n        int value;\n        Product(int q, int p, int v) \n            : quantity(q), price(p), value(v) {}\n    };\n    ```\n\n3.  接下来，我们将处理`main()`函数中的输入，并填充一个`Product`类型的数组:\n\n    ```cpp\n    int main()\n    {\n        int N, budget, capacity;\n        cin >> N >> budget >> capacity;\n        vector<Product> products;\n        for(int i = 0; i < N; i++)\n        {\n            int quantity, cost, value;\n            cin >> quantity >> cost >> value;\n            products.push_back(Product(quantity, cost, value));\n        }\n    ...\n    return 0;\n    }\n    ```\n\n4.  As with any DP algorithm, we must now define the states and base cases. We know that the subset of items that form the final result must match the following criteria:\n\n    –子集中所有产品的`cost`之和不得超过`budget`。\n\n    –子集中所有产品的`quantity`之和不得超过`capacity`。\n\n    –子集内所有产品的`value`之和必须最大化。\n\n    给定这些标准，我们可以看到每个状态可以由以下参数定义:\n\n    –正在考虑的当前项目\n\n    –以前购买的单位数量\n\n    –购买项目的总成本\n\n    –以零售价值销售产品后获得的总利润\n\n    我们还可以得出结论，当出现以下情况时，搜索将终止:\n\n    –所有项目都已考虑\n\n    –总成本超出预算\n\n    –单元总数超过容量\n\n    和传统的 0-1 背包问题一样，我们会从`0`到`N-1`线性考虑每一项。对于索引`i`中的每个项目，我们的状态可以通过两种方式之一进行转换:要么包含当前项目，要么离开它。用伪代码编写递归逻辑可能如下所示:\n\n    ```cpp\n    F(i, count, cost, total): \n    I        –> The index of the current item \n    Cost     –> The total money spent \n    count    –> The number of units purchased\n    total    –> The total profit value of the chosen items\n    Base cases: \n        if i = N: return total\n        if cost > budget: return 0\n        if count > capacity: return 0\n    Recurrence:\n    F(i, count, cost, total) = maximum of:\n    F(i + 1, count + quantity[i], cost + price[i], \n          total + value[i]) – Include the item\n            AND\n        F(i + 1, count, cost, total) – Leave as-is\n    ```\n\n    如上图所示，递归关系是根据`i`、`count`、`cost`和`total`的值定义的。将这个逻辑从上到下转换为自下而上可以这样完成:\n\n    ```cpp\n    Base case:\n        DP(0, 0, 0) = 0 [Nothing has been chosen yet]\n    For i = 1 to N:\n        Product -> quantity, price, value\n        For cost = 0 to budget:\n            For count = 0 to capacity:\n                If price is greater than cost OR \n               quantity is greater than count:\n                    DP(i, cost, count) = DP(i-1, cost, count)\n                Otherwise:\n                    DP(i, cost, count) = maximum of:\n                        DP(i-1, cost, count)\n                            AND\n                        DP(i-1, cost – price, count – quantity) + value\n    ```\n\n    换句话说，每个状态都是根据当前索引、总成本和总计数来描述的。对于每对有效的`cost`和`count`值，索引`i`处项目的当前结果将等于在索引`i – 1`处`cost`和`count`的相同值中找到的最大子集和(即`DP[i – 1][cost][count]`)或者当前项目的`value`与索引`i – 1`处的最大和(即`cost`和`count`之和等于包括该项目之前的值(即`DP[i - 1][cost – price][count – quantity] + value`)。\n\n5.  我们可以将前面的逻辑编码如下:\n\n    ```cpp\n    vector<vector<vector<int>>> DP(N + 1, vector<vector<int>>(budget + 1, vector<int>(capacity + 1, 0)));\n    for(int i = 1; i <= N; i++)\n    {\n        Product product = products[i-1];\n\n    for(int cost = 0; cost <= budget; cost++)\n    {\n            for(int count = 0; count <= capacity; count++)\n            {\n                if(cost < product.price || count < product.quantity)\n                {\n                    DP[i][cost][count] = DP[i-1][cost][count];\n                }\n                else\n                {\n                    DP[i][cost][count] = max\n                    (\n                        DP[i-1][cost][count],\n                        DP[i-1][cost – product.price][count – product.quantity] + product.value\n                    );\n                }\n            }\n    }\n    cout << DP[N][budget][capacity] << endl;\n    }  \n    ```\n\n如您所见，该实现相当于 0-1 背包解决方案，增加了一个维度。\n\n### 活动 23:住宅道路\n\n如果你没有一些深谋远虑，这个活动有很多潜在的陷阱。它最困难的方面是它需要许多不同的步骤，任何一点的粗心错误都可能导致整个程序失败。因此，建议逐步接近实现。所需的主要步骤如下:\n\n1.  处理输入\n2.  构建图表(查找邻接关系和权重值)\n3.  寻找图节点之间的最短距离\n4.  用最短路径重建边\n5.  重新绘制输入网格\n\n因为这比本章中的其他活动要长得多，所以让我们分别讨论这些步骤。\n\n**第 0 步:初步设置**\n\n在我们编写任何与输入相关的代码之前，我们应该提前决定如何表示我们的数据。我们将收到如下输入:\n\n*   两个整数，`H`和`W`，代表网格的高度和宽度。\n*   一个整数，`N`，表示该房产包含的房屋数量。\n*   `H`宽度为`W`的字符串，表示属性的映射。我们可以将这些数据存储为字符串的`H`元素向量。\n*   `H`行代表地形崎岖程度的`W`整数。我们可以将这些值存储在一个整数矩阵中。\n*   `N`包含两个整数的线，`x`和`y`，代表每个房子的坐标。为此，我们可以创建一个名为`Point`的简单结构，它包含两个整数，`x`和`y`。\n\n现在，让我们看看实现:\n\n1.  Include the required headers and define some global constants and variables that we will need later in this problem. We will declare most of our data globally for the sake of convenience, but it is worth reiterating the point that this is generally considered bad practice within the context of a full-scale application:\n\n    ```cpp\n    #include <iostream>\n    #include <vector>\n    using namespace std;\n    const int UNKNOWN = 1e9;\n    const char EMPTY_SPACE = '.';\n    const string roads = \"-|/\\\\\";\n    struct Point\n    {\n        int x;\n        int y;\n        Point(){}\n        Point(int x, int y) : x(x), y(y) {}\n    };\n    int N;\n    int H, W;\n    vector<string> grid;\n    vector<vector<int>> terrain;\n    vector<vector<int>> cost;\n    vector<Point> houses;\n    ```\n\n    **第一步:处理输入**\n\n2.  Since there is a fair amount of input required for this problem, let's contain it all in its own function, `Input()`, which will return void:\n\n    ```cpp\n    void Input()\n    {\n        cin >> H >> W;\n        cin >> N;\n        grid.resize(H);\n        houses.resize(N);\n        terrain.resize(H, vector<int>(W, UNKNOWN));    cost.resize(H, vector<int>(W, UNKNOWN));\n        // Map of property\n        for(auto &row : grid) cin >> row;\n        // Terrain ruggedness\n        for(int I = 0; i < H; i++)\n        {\n            for(int j = 0; j < W; j++)\n            {\n                cin >> terrain[i][j];\n            }\n        }\n        // House coordinates\n        for(int i = 0; i < N; i++)\n        {\n            cin >> houses[i].x >> house[i].y;\n            // Set house labels in grid\n            grid[houses[i].y][houses[i].x] = char(i + 'A');\n        }\n    }\n    ```\n\n    **第二步:构建图表**\n\n    问题描述如下:\n\n    *   当且仅当两个房子之间有一条直接的水平、垂直或对角线路径时，才能在两个房子之间修建道路。\n    *   道路可能不会跨越水体、山脉、森林等等。\n    *   在两栋房子之间修建一条道路的成本等于两栋房子之间道路的耐用度总和。\n\n为了测试第一个条件，我们只需要比较两点的坐标，并确定以下三个条件中的任何一个是否成立:\n\n*   `A.x = B.x`(它们之间有一条水平线)\n*   `A.y = B.y`(它们之间有一条垂直线)\n*   `| A.x – B.x | = | A.y – B.y |`(它们之间有一条对角线)\n\n现在，让我们回到我们的代码。\n\n1.  为此，让我们编写一个函数`DirectLine()`，该函数以两点`a`和`b`作为参数，并返回一个布尔值:\n\n    ```cpp\n    bool DirectLine(Point a, Point b)\n    {\n        return a.x == b.x || a.y == b.y || abs(a.x – b.x) == abs(a.y – b.y);\n    }\n    ```\n\n2.  为了处理第二种和第三种情况，我们可以简单地执行从网格中的点`a`到点`b`的线性遍历。当我们考虑网格中的每个点时，我们可以累加地形矩阵中包含的值的总和。当我们这样做的时候，我们可以同时检查`grid[a.y][a.x]`中的角色，一旦遇到不等于`EMPTY_SPACE`的角色(即，‘`.`’)就终止它。如果在遍历点`a`的末端等于点`b`，我们将把我们获得的和存储在`cost`矩阵中；否则，我们已经确定`a`和`b`之间没有邻接，这种情况下我们返回`UNKNOWN`。我们可以使用`GetCost()`函数来实现，该函数使用两个整数`start`和`end`作为参数。分别代表`a`和`b`的索引，返回一个整数:\n\n    ```cpp\n    int GetCost(int start, int end)\n    {\n        Point a = houses[start];\n        Point b = houses[end];\n        // The values by which the coordinates change on each iteration\n        int x_dir = 0;\n        int y_dir = 0;\n        if(a.x != b.x)\n        {\n            x_dir = (a.x < b.x) ? 1 : -1;\n        }\n        if(a.y != b.y)\n        {\n            y_dir = (a.y < b.y) ? 1 : -1;\n        }\n        int cost = 0;\n\n        do\n        {\n            a.x += x_dir;\n            a.y += y_dir;\n            cost += terrain[a.y][a.x];\n        }\n        while(grid[a.y][a.x] == '.');\n        return (a != b) ? UNKNOWN : res;\n    }\n    ```\n\n3.  最后一行要求我们在`Point`结构中定义`operator !=`:\n\n    ```cpp\n    struct Point\n    {\n        ......\n        bool operator !=(const Point &other) const { return x != other.x || y != other.y; }\n    }\n    ```\n\n4.  Now, let's create the following `GetAdjacencies()` function:\n\n    ```cpp\n    void GetAdjacencies()\n    {\n        for(int i = 0; i < N; i++)\n        {\n            for(int j = 0; j < N; j++)\n            {\n                if(DirectLine(houses[i], houses[j])\n                {\n                    cost[i][j] = cost[j][i] = GetCost(i, j);\n                }\n            }\n        }\n    }\n    ```\n\n    **第三步:寻找节点间的最短距离**\n\n    问题是，两栋房子应该通过一条道路连接起来，这条道路可以最大限度地降低到达出口点的成本。对于这个实现，我们将使用弗洛伊德-沃肖尔算法。让我们回到我们的代码:\n\n5.  让我们定义一个函数`GetShortestPaths()`，它将处理弗洛伊德-沃肖尔的实现以及路径的重建。为了处理后一种情况，我们将维护一个名为`next`的 *N x N* 整数矩阵，该矩阵将存储从节点`a`和`b`到最短路径上的下一点的索引。最初，它的值将被设置为图中现有的边:\n\n    ```cpp\n    void GetShortestPaths()\n    {\n        vector<vector<int>> dist(N, vector<int>(N, UNKNOWN));\n        vector<vector<int>> next(N, vector<int>(N, UNKNOWN));\n    for(int i = 0; i < N; i++)\n    {\n        for(int j = 0; j < N; j++)\n        {\n            dist[i][j] = cost[i][j]\n            if(dist[i][j] != UNKNOWN)\n            {\n                next[i][j] = j;\n            }\n        }\n        dist[i][j] = 0;\n        next[i][i] = i;\n    }\n    ...\n    }\n    ```\n\n6.  We will then perform the standard implementation of Floyd-Warshall, with one additional line in the innermost loop setting `next[start][end]` to `next[start][mid]` every time we find a shorter distance between `start` and `end`:\n\n    ```cpp\n    for(int mid = 0; mid < N; mid++)\n    {\n        for(int start = 0; start < N; start++)\n        {\n            for(int end = 0; end < N; end++)\n            {\n                if(dist[start][end] > dist[start][mid] + dist[mid][end])\n                {\n                    dist[start][end] = dist[start][mid] + dist[mid][end];\n                    next[start][end] = next[start][mid];\n                }\n            }\n        }\n    }\n    ```\n\n    **第四步:重构路径**\n\n    利用我们在`next`矩阵中获得的数据，我们可以以类似于 LCS 或 0-1 背包问题的重建方法的方式容易地重建每条路径上的点。为此，我们将定义另一个函数`GetPath()`，它有三个参数，两个整数`start`和`end`，以及对`next`矩阵的引用，并返回一个包含路径节点索引的整数向量:\n\n    ```cpp\n    vector<int> GetPath(int start, int end, vector<vector<int>> &next)\n    {\n        vector<int> path = { start };\n        do\n        {\n            start = next[start][end];\n            path.push_back(start);\n        }\n        while(next[start][end] != end);\n        return path;\n    }\n    ```\n\n7.  Returning to `GetShortestPaths()`, we will now add a loop underneath our implementation of Floyd-Warshall that calls `GetPath()` and then draws lines in the grid corresponding to each pair of points in the path:\n\n    ```cpp\n    for(int i = 0; i < N; i++)\n    {\n        auto path = GetPath(i, N – 1, next);\n\n        int curr = i;\n        for(auto neighbor : path)\n        {\n            DrawPath(curr, neighbor);\n            curr = neighbor;\n        }\n    }\n    ```\n\n    **第五步:重绘网格**\n\n8.  现在，我们必须在网格中绘制道路。我们将在另一个函数`DrawPath()`中执行此操作，该函数具有`start`和`end`参数:\n\n    ```cpp\n    void DrawPath(int start, int end)\n    {\n        Point a = houses[start];\n        Point b = houses[end];\n        int x_dir = 0;\n        int y_dir = 0;\n        if(a.x != b.x)\n        {\n            x_dir = (a.x < b.x) 1 : -1;\n        }\n        if(a.y != b.y)\n        {\n            y_dir = (a.y < b.y) 1 : -1;\n        }\n\n        ……\n    }\n    ```\n\n9.  我们需要根据每条道路的方向选择正确的字符。为此，我们将定义一个函数`GetDirection()`，该函数返回一个整数，该整数对应于我们在开始定义的`roads`字符串(“`-|/\\`”:\n\n    ```cpp\n    int GetDirection(int x_dir, int y_dir)\n    {\n        if(y_dir == 0) return 0;\n        if(x_dir == 0) return 1;\n        if(x_dir == -1)\n        {\n            return (y_dir == 1) ? 2 : 3;\n        }\n        return (y_dir == 1) ? 3 : 2;\n    }\n    void DrawPath(int start, int end)\n    {\n        ……\n        int direction = GetDirection(x_dir, y_dir);\n        char mark = roads[direction];\n            ……\n    }\n    ```\n\n    中的一个索引\n10.  我们现在可以执行从`a`到`b`的线性遍历，如果网格中的每个单元格的值为`EMPTY_SPACE`，则将它设置为`mark`。否则，我们必须检查单元格中的字符是否是不同方向的道路字符，在这种情况下，我们将其设置为`+` :\n\n    ```cpp\n    do\n    {\n        a.x += x_dir;\n        a.y += y_dir;\n\n        if(grid[a.y][a.x] == EMPTY_SPACE)\n        {\n            grid[a.y][a.x] = mark;\n        }\n        else if(!isalpha(grid[a.y][a.x]))\n        {\n                // If two roads of differing orientations intersect, replace symbol with '+'\n                grid[a.y][a.x] = (mark != grid[a.y][a.x]) ? '+' : mark;\n        }\n    }\n    while(a != b);\n    ```\n\n11.  剩下的就是在`main()`中调用我们的函数并打印输出:\n\n    ```cpp\n    int main()\n    {\n            Input();\n            BuildGraph();\n            GetShortestPaths();\n\n            for(auto it : grid)\n            {\n                cout << it << endl;\n            }\n            return 0;\n    }\n    ```"
  },
  {
    "path": "docs/cpp-dsal-design-principle/README.md",
    "content": "# C++ 数据结构和算法设计原则\n\n> 原书：[C++ Data Structures and Algorithm Design Principles](https://libgen.rs/book/index.php?md5=89B76B51877D088E41B92EEF0985A12B)\n> \n> 协议：[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)\n> \n> 阶段：机翻（1）\n> \n> 自豪地采用[谷歌翻译](https://translate.google.cn/)\n> \n> 让开源界提前感受入关。\n\n* [在线阅读](https://ccpp.apachecn.org)\n* [在线阅读（Gitee）](https://apachecn.gitee.io/apachecn-c-cpp-zh/)\n* [ApacheCN 学习资源](http://docs.apachecn.org/)\n\n## 贡献指南\n\n本项目需要校对，欢迎大家提交 Pull Request。\n\n> 请您勇敢地去翻译和改进翻译。虽然我们追求卓越，但我们并不要求您做到十全十美，因此请不要担心因为翻译上犯错——在大部分情况下，我们的服务器已经记录所有的翻译，因此您不必担心会因为您的失误遭到无法挽回的破坏。（改编自维基百科）\n\n## 联系方式\n\n### 负责人\n\n* [飞龙](https://github.com/wizardforcel): 562826179\n\n### 其他\n\n*   在我们的 [apachecn/apachecn-c-cpp-zh](https://github.com/apachecn/apachecn-c-cpp-zh) github 上提 issue.\n*   发邮件到 Email: `apachecn@163.com`.\n*   在我们的 [组织学习交流群](http://www.apachecn.org/organization/348.html) 中联系群主/管理员即可.\n\n## 赞助我们\n\n![](http://data.apachecn.org/img/about/donate.jpg)\n"
  },
  {
    "path": "docs/cpp-dsal-design-principle/SUMMARY.md",
    "content": "+   [C++ 数据结构和算法设计原则](README.md)\n+   [零、前言](00.md)\n+   [一、列表、栈和队列](01.md)\n+   [二、树、堆和图](02.md)\n+   [三、哈希表和布隆过滤器](03.md)\n+   [四、分治法](04.md)\n+   [五、贪婪算法](05.md)\n+   [六、图算法 1](06.md)\n+   [七、图算法 2](07.md)\n+   [八、动态规划一](08.md)\n+   [九、动态规划二](09.md)\n+   [十、附录](10.md)\n"
  },
  {
    "path": "docs/cpp-gui-prog-qt5/00.md",
    "content": "# 零、前言\n\nQt 的最新版本 Qt 5 使您能够为多个目标开发具有复杂用户界面的应用。它为您提供了更快、更智能的方法来为多个平台创建现代用户界面和应用。这本书将教你如何设计和构建功能性、吸引力和用户友好的图形用户界面。\n\n到本书结束时，您将成功了解高端图形用户界面应用，并将能够构建许多更强大的跨平台应用。\n\n# 这本书是给谁的\n\n这本书将吸引那些想要构建基于图形用户界面的应用的开发人员和程序员。C++ 的基础知识是必要的，Qt 的基础知识会有帮助。\n\n# 充分利用这本书\n\n为了成功执行本书中的所有代码和说明，您需要以下内容:\n\n*   基本的个人电脑/笔记本电脑\n*   有效的互联网连接\n*   Qt 5.10\n*   马里亚数据库 10.2(或 MySQL 连接器)\n*   Filezilla 服务器 0.9\n\n我们将在每章讲述安装过程和细节。\n\n# 下载示例代码文件\n\n你可以从你在[www.packtpub.com](http://www.packtpub.com)的账户下载这本书的示例代码文件。如果您在其他地方购买了这本书，您可以访问[www.packtpub.com/support](http://www.packtpub.com/support)并注册将文件直接通过电子邮件发送给您。\n\n您可以按照以下步骤下载代码文件:\n\n1.  登录或注册[www.packtpub.com](http://www.packtpub.com/support)。\n2.  选择“支持”选项卡。\n3.  点击代码下载和勘误表。\n4.  在搜索框中输入图书的名称，并按照屏幕指示进行操作。\n\n下载文件后，请确保使用最新版本的解压缩文件夹:\n\n*   视窗系统的 WinRAR/7-Zip\n*   zipeg/izp/un ARX for MAC\n*   适用于 Linux 的 7-Zip/PeaZip\n\n这本书的代码包也托管在 github 上，网址为[https://GitHub . com/PacktPublishing/hand-On-GUI-Programming-with-CPP-and-Qt5](https://github.com/PacktPublishing/Hands-On-GUI-Programming-with-CPP-and-Qt5)如果代码有更新，它将在现有的 GitHub 存储库中更新。\n\n我们还有来自丰富的图书和视频目录的其他代码包，可在**[【https://github.com/PacktPublishing/】](https://github.com/PacktPublishing/)**获得。看看他们！\n\n# 下载彩色图像\n\n我们还提供了一个 PDF 文件，其中包含本书中使用的截图/图表的彩色图像。你可以在这里下载:[https://www . packtpub . com/sites/default/files/downloads/HandsOnGUIProgrammingwithCPPandQt5 _ color images . pdf](https://www.packtpub.com/sites/default/files/downloads/HandsOnGUIProgrammingwithCPPandQt5_ColorImages.pdf)。\n\n# 使用的约定\n\n本书通篇使用了许多文本约定。\n\n`CodeInText`:表示文本中的码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟网址、用户输入和推特句柄。这里有一个例子:“我们在`MainWindow`构造函数中调用`test()`函数。”\n\n代码块设置如下:\n\n```cpp\nvoid MainWindow::test() \n{ \n   int amount = 100; \n   amount -= 10; \n   qDebug() << \"You have obtained\" << amount << \"apples!\"; \n} \n```\n\n当我们希望将您的注意力吸引到代码块的特定部分时，相关的行或项目以粗体显示:\n\n```cpp\nMainWindow::MainWindow(QWidget *parent) : \n   QMainWindow(parent), \n   ui(new Ui::MainWindow) \n{ \n   ui->setupUi(this); \n   test(); \n} \n```\n\n任何命令行输入或输出都编写如下:\n\n```cpp\n********* Start testing of MainWindow ********* \nConfig: Using QtTest library 5.9.1, Qt 5.9.1 (i386-little_endian-ilp32 shared (dynamic) debug build; by GCC 5.3.0) \nPASS   : MainWindow::initTestCase() \nPASS   : MainWindow::_q_showIfNotHidden() \nPASS   : MainWindow::testString() \nPASS   : MainWindow::testGui() \nPASS   : MainWindow::cleanupTestCase() \nTotals: 5 passed, 0 failed, 0 skipped, 0 blacklisted, 880ms \n********* Finished testing of MainWindow ********* \n```\n\n**粗体**:表示一个新的术语、一个重要的单词或者你在屏幕上看到的单词。例如，菜单或对话框中的单词像这样出现在文本中。这里有一个例子:“第三个选项是切换书签，它允许您设置一个书签供自己参考。”\n\nWarnings or important notes appear like this. Tips and tricks appear like this.\n\n# 取得联系\n\n我们随时欢迎读者的反馈。\n\n**综合反馈**:发邮件`feedback@packtpub.com`并在邮件主题中提及书名。如果您对本书的任何方面有疑问，请发电子邮件至`questions@packtpub.com`。\n\n**勘误表**:虽然我们已经尽了最大的努力来保证内容的准确性，但是错误还是会发生。如果你在这本书里发现了一个错误，如果你能向我们报告，我们将不胜感激。请访问[www.packtpub.com/submit-errata](http://www.packtpub.com/submit-errata)，选择您的图书，点击勘误表提交链接，并输入详细信息。\n\n**盗版**:如果您在互联网上遇到任何形式的我们作品的非法拷贝，如果您能提供我们的位置地址或网站名称，我们将不胜感激。请通过`copyright@packtpub.com`联系我们，并提供材料链接。\n\n**如果你有兴趣成为一名作者**:如果有一个你有专长的话题，你有兴趣写或者投稿一本书，请访问[authors.packtpub.com](http://authors.packtpub.com/)。\n\n# 在 Qt 中发现工具\n\nQt 附带了一套工具，让程序员的生活变得更轻松。其中一个工具是 Qt Creator(见下面的截图)，它是一个 **IDE** ( **集成开发环境**)，由一个代码编辑器和一个 **GUI** ( **图形用户界面**)设计器组成，与其他 Qt 工具如编译器、调试器等协同工作。其中最有吸引力的工具当然是图形用户界面设计器，它带有两种不同类型的编辑器:一种用于基于小部件的应用，称为 Qt 设计器，另一种用于 Qt 快速应用，称为 Qt 快速设计器。当你打开一个相关的文件格式时，这两个工具都可以在 Qt Creator 中直接访问。Qt Creator 还包括一个名为 Qt Assistant 的内置文档查看器。这真的很方便，因为您可以通过将鼠标光标悬停在源代码中的类名上，然后按下 *F1* 键来查找某个 Qt 类或函数的解释。然后将打开 Qt 助手，并向您显示与 Qt 类或函数相关的文档:\n\n![](img/0983f1e7-423c-40b7-9965-101a2c0a1be1.png)\n\n# 复习\n\n请留下评论。一旦你阅读并使用了这本书，为什么不在你购买它的网站上留下评论呢？然后，潜在的读者可以看到并使用您不带偏见的意见来做出购买决定，我们在 Packt 可以了解您对我们产品的看法，我们的作者可以看到您对他们的书的反馈。谢谢大家！\n\n更多关于 Packt 的信息，请访问[packtpub.com](https://www.packtpub.com/)。"
  },
  {
    "path": "docs/cpp-gui-prog-qt5/01.md",
    "content": "# 一、Qt 简介\n\nQt(发音为*可爱*)自第一次发布以来，已经被软件工程师和开发人员使用了二十多年来创建跨平台应用。在经历了几次所有权变更和大量的重大代码修改之后，Qt 变得更加功能丰富，并且支持比过去更多的平台。Qt 不仅擅长桌面应用开发，而且在移动和嵌入式系统开发方面也非常出色。\n\n在本章中，我们将涵盖以下主题:\n\n*   什么是 Qt？\n*   为什么要用 Qt？\n*   在 Qt 中使用工具\n*   下载和安装 Qt\n*   建立工作环境\n*   运行我们的第一个`Hello World` Qt 程序\n\n在本章中，我们将了解更多关于 Qt 的历史。然后，我们将使用 Qt 的最新版本，也就是 Qt 版本 5，来构建我们的第一个示例程序。为了方便读者，我们将在整本书中简称它为 Qt。\n\n# 什么是 Qt？\n\n目前，Qt 的最新版本(正如本书所写)是 5.10 版本。这个版本包含了许多新特性和数千个错误修复，这使得 Qt 成为软件开发人员和系统工程师非常强大和稳定的开发工具包。Qt 有一个巨大的 SDK(软件开发工具包)包，其中包含了广泛的工具和库，用于帮助开发人员完成工作，而不必太担心与特定平台相关的技术问题。Qt 在幕后为您处理所有混乱的集成和兼容性问题，因此您不必处理它们。这不仅会提高效率，还会降低开发成本，尤其是当您试图开发跨平台应用来迎合更广泛的用户时。\n\nQt 有两种类型的许可证:\n\n*   第一种类型是开源许可证，这是免费的，但前提是您的项目/产品符合其条款和条件。例如，如果您对 Qt 的源代码做了任何更改，您有义务将这些更改提交给 Qt 开发人员。如果做不到这一点，可能会导致严重的法律问题，因此，您可能希望选择第二个选项。\n*   第二种类型的许可证是商业许可证，它赋予您对专有 Qt 源代码修改的全部权利，并保持您的应用私有。但是，当然，这些特权伴随着一系列费用。\n\n如果你刚刚开始学习 Qt，不要被这些术语所拖累，因为你肯定不会修改 Qt 库的源代码或者从源代码中重新编译它，至少现在不会。\n\nFor more information regarding Qt's licensing, please visit [https://www.qt.io/licensing-comparison.](https://www.qt.io/licensing-comparison)\n\n# 为什么要用 Qt？\n\n不难看出，为什么 Qt 有机会战胜市场上所有其他现有的 SDKs 首先，跨平台兼容性。如果不为每个平台编写不同的代码集，你很难找到支持如此多平台的其他开发工具包。通过消除这些额外的步骤，程序员可以专注于开发他们的应用，而不需要担心每个特定于平台的特性的实现。此外，您的代码看起来很干净，没有所有的`#ifdef`宏，并且必须为不同的平台加载不同的依赖项。\n\nQt 一般使用 C++，这是一种编译语言，可以生成小而高效的代码。它也有很好的文档记录，并且遵循一套非常一致的命名约定，这减少了开发人员的学习曲线。\n\n请务必注意，Qt 确实包含少量仅在特定平台上工作的功能。然而，这些都是最小的，通常用于特殊的用例，比如只在移动平台上工作的 Qt SensorsQt Web Engine，只在桌面上工作；Qt NFC，仅适用于 Android 和 Linux 等等。这些是一些非常具体的功能，只存在于支持它们的特定平台上。除此之外，所有平台通常都支持通用特性。\n\n# Qt 设计器\n\nQt Designer 通常被开发人员用来为桌面应用设计 GUI，而 Qt Quick Designer 通常用于移动和嵌入式平台。也就是说，这两种格式在桌面和移动格式上都运行良好，唯一的区别是外观和感觉，以及使用的语言类型。\n\nQt Designer 保存的 GUI 文件带有`.ui`扩展名，以 XML 格式保存。该文件存储图形用户界面设计者放置的每个小部件的属性，如位置、大小、边距、工具提示、布局方向等。它还将信号和时隙事件名称保存在自身中，以便于在后期与代码连接。这种格式不支持编码，只适用于 Qt C++ 项目，即基于小部件的应用项目。\n\n# Qt 快速设计器\n\n另一方面，Qt 快速设计器以`.ui.qml`和`.qml`格式保存图形用户界面文件。Qt Quick 在技术概念和开发方法上是一个非常不同类型的 GUI 系统，我们将在[第 14 章](13.html)、 *Qt Quick 和 QML* 中介绍。Qt Quick Designer 用一种类似于 JavaScript 的声明性语言保存其数据，称为 **QML** 。QML 不仅允许设计师以类似 CSS(层叠样式表)的方式定制他们的图形用户界面，还允许程序员在 QML 文件中编写功能性 JavaScript。如前所述，`.ui.qml`是仅用于视觉装饰的文件格式，而`.qml`包含应用逻辑。\n\n如果你正在用 Qt Quick 做一个简单的程序，你根本不需要接触任何 C++ 编码。这对网络开发人员来说尤其受欢迎，因为他们可以立即使用 Qt Quick 并开发自己的应用，而无需陡峭的学习曲线；他们对一切都很熟悉。对于更复杂的软件，你甚至可以从 QML 链接 C++ 函数，反之亦然。同样，如果您有兴趣了解更多关于 Qt Quick 和 QML 的信息，请前往[第 14 章](13.html)、 *QtQuick 和 QML* 。\n\n因为 Qt Creator 也是用 Qt 库自己写的，所以也是完全跨平台的。因此，您可以在不同的开发环境中使用相同的工具集，并为您的团队开发统一的工作流，从而提高效率和成本效益。\n\n除此之外，Qt 还附带了许多不同的模块和插件，涵盖了项目所需的广泛功能。您通常不需要寻找其他外部库或依赖项，并自己尝试实现它们。Qt 的抽象层使后端实现对用户不可见，并导致统一的编码风格和语法。如果你自己试着把一堆外部依赖放在一起，你会发现每个库都有自己独特的编码风格。当在同一个项目中混合所有不同的编码风格时，这是相当混乱的，除非您自己制作抽象层，这是一项非常耗时的任务。由于 Qt 已经包含了创建功能丰富的应用所需的大部分(如果不是全部的话)模块，因此您不需要实现自己的模块。\n\nFor more information regarding the modules that come with Qt, please visit: [http://doc.qt.io/qt-5/qtmodules.html](http://doc.qt.io/qt-5/qtmodules.html). [](http://doc.qt.io/qt-5/qtmodules.html) \n\n也就是说，还有很多第三库扩展了 Qt 本身不支持的特性，比如专注于游戏开发的库或者任何其他为特定用户群体设计的特性。\n\n# 下载和安装 Qt\n\n在不浪费任何时间的情况下，让我们开始安装吧！要获得开源 Qt 的免费安装程序，首先去他们的网站 [https://www.qt.io](https://www.qt.io/) 。在那里，寻找说下载 Qt 的按钮(如果他们更新了，网站可能会看起来不同)。请注意，您可能正在下载商用 Qt 的免费试用版，30 天后将无法使用。请确保您下载的是 Qt 的开源版本。此外，您可能想为您的平台选择合适的安装程序，因为 Qt 有许多不同的安装程序，适用于不同的操作系统 Windows、macOS 和 Linux。\n\n你可能想知道为什么安装程序这么小——只有 19 MB 左右。这是因为统一在线安装程序实际上不包含任何 Qt 包，而是一个下载器客户端，可以帮助您下载所有相关文件，并在下载完成后将其安装到您的计算机上。下载在线安装程序后，双击它，您将看到如下界面(以下示例在 Windows 系统上运行):\n\n![](img/2887b5c2-45ea-457d-94bf-1dcd96367148.png)\n\n点击下一步按钮，将出现**数字版权管理** ( **数字版权管理**)页面，要求您使用 Qt 账户登录。如果您没有帐户，也可以在同一页面上创建您的帐户:\n\n![](img/8da1ff97-e940-4f55-9e78-b9c1c17dfcce.png)\n\n登录后，您将看到一条消息，称您的 Qt 帐户中没有该主机平台的有效商业许可证。别担心，只需点击“下一步”按钮继续。\n\n接下来，您将被要求指定安装路径。默认路径通常很好，但是您可以随意将其更改为任何其他路径。此外，您可以选中“将此常见文件类型与 Qt 创建者相关联”选项，也可以手动取消选中该选项。\n\n之后，您将看到一系列复选框，您可以选择需要安装到计算机上的 Qt 版本。通常，对于新用户，默认选项就足够了。如果你不需要其中的一些选项，比如在安卓系统上支持 Qt，你可以在这里取消选择，以减少下载量。如果需要，您可以随时使用维护工具返回并添加或删除 Qt 组件:\n\n![](img/02db9bda-c373-4b0e-957c-9dc316133d55.png)\n\n接下来，您将看到许可协议。选中第一个选项，表示我已阅读并同意许可协议中包含的条款，然后单击“下一步”按钮。请务必阅读许可协议中规定的条款和条件！\n\n最后，安装程序将要求您输入一个名称，为 Qt 创建一个开始菜单快捷方式。完成后，只需单击“下一步”，然后单击“安装”。下载过程需要几分钟到几个小时，这取决于你的网速。下载完所有文件后，安装程序会自动将文件安装到您在前面某个步骤中刚刚设置的安装路径。\n\n# 设置工作环境\n\n既然你已经安装了最新版本的 Qt，让我们启动 Qt Creator，开始创建我们的第一个项目！您应该能够在桌面或开始菜单中找到 Qt Creator 的快捷图标。\n\n让我们看看设置环境的步骤:\n\n1.  当您第一次启动 Qt Creator 时，应该会看到以下界面:\n\n![](img/78301c1b-8a25-4575-a4ea-b46880946909.png)\n\n2.  在开始创建第一个项目之前，您可能需要调整几个设置。转到顶部菜单，选择工具|选项。屏幕上会弹出一个类似这样的窗口:\n\n![](img/6a5dc3a3-954d-439e-8d96-c396f90317b6.png)\n\n3.  在窗口的左边有很多不同的类别。每个类别代表一组选项，您可以设置这些选项来定制 Qt Creator 的外观和操作方式。你可能根本不想接触这些设置，但是先了解一下就好了。您可能想更改的第一个设置之一是“语言”选项，它在“环境”类别中可用。Qt Creator 为我们提供了一个在不同语言之间切换的选项。虽然它不支持所有语言，但大多数流行的语言都是可用的，如英语、法语、德语、日语、汉语、俄语等。选择所需语言后，单击应用并重新启动 Qt 创建器。您必须重新启动 Qt 创建器才能看到更改。\n\n4.  您可能需要的下一个设置是缩进设置。默认情况下，Qt 使用空格缩进，每当您按下键盘上的 *Tab* 键时，脚本中会添加四个空格。有些人，像我一样，更喜欢制表符缩进。您可以在 C++ 类别中更改缩进设置。\n\nDo note that if you are contributing to Qt project's source code, it's required that you use space indentation instead of tabs, which is the coding standard and style of the Qt project.\n\n5.  在 C++ 类别下，您可以在右上方的某个位置找到位于“编辑”按钮旁边的“复制”按钮。点击它，会弹出一个新窗口。\n6.  插入您自己的代码样式名称，因为您不能编辑默认的内置代码样式。创建自己的设置后，单击编辑按钮。现在，您可以在常规选项卡下看到实际的选项卡和缩进设置:\n\n![](img/86b23382-3d24-4f3a-8b10-e8642f5a3a21.png)\n\n7.  请注意，即使有一个标签和缩进设置位于文本编辑器类别，我相信这是一个旧的设置，不再有任何影响的 Qt Creator。用户界面上还写了一个注释，说代码缩进是在 C++ 和 Qt Quick 设置中配置的。一个可能的原因是，由于 Qt Creator 现在支持 C++ 项目和 QML 项目，Qt 开发人员可能觉得有必要将设置分成两部分，因此旧的设置不再有效。我非常确定文本编辑器的这一部分在不久的将来会被弃用。\n\n8.  接下来，在“构建和运行”类别下，您将看到一个标记为“套件”的选项卡。\n\n9.  这是您可以为每个平台设置编译设置的地方。从下一张截图中可以看到，我的 Qt 不支持 MSVC(微软 Visual Studio 编译器)下的桌面构建，因为我从来没有在我的电脑上安装过 Visual Studio。相反，我的 Qt 只支持 MinGW 编译器下的桌面构建。从这个窗口，您可以检查并查看您的 Qt 是否支持您的项目所需的平台和编译器，并在必要时对其进行更改。但现在，我们就让它保持原样。要了解更多关于什么是*套件*以及如何配置构建设置的信息，请前往[第 15 章](14.html)、*跨平台开发*:\n\n![](img/96a3210e-7871-4d1e-bbe1-f31ed021db05.png)\n\n10.  最后，我们可以在版本控制类别中将我们的项目链接到我们的版本控制服务器。\n11.  版本控制允许您或您的团队将代码更改提交给一个集中的系统，这样每个团队成员都可以获得相同的代码，而无需手动传递文件。当你在一个大团队中工作时，手动跟踪代码变化是非常困难的，合并不同程序员完成的代码更是如此。版本控制系统旨在解决这些问题。Qt 支持不同类型的版本控制系统，比如 Git、SVN、Mercurial、Perforce 等等。虽然这是一个非常有用的功能，尤其是如果您在团队中工作，但我们现在不需要对其进行配置:\n\n![](img/8faf6c93-fcab-4c20-b1e6-b9c70cba4b3e.png)\n\n# 运行我们的第一个 Hello World Qt 程序\n\nHello World 程序是一个非常简单的程序，只不过显示一个输出，上面写着`Hello, World!`(或者任何其他东西，不一定是这个)来显示 SDK 工作正常。我们不需要写很长的代码来产生一个`Hello World`程序，我们可以只用最少和最基本的代码来完成。事实上，我们不需要在 Qt 中编写任何代码，因为它会在您第一次创建项目时生成代码！\n\n让我们按照以下步骤开始我们的项目:\n\n1.  要在 Qt 中创建新项目，请单击 Qt 创建器欢迎屏幕上的新建项目按钮。或者，您也可以转到顶部菜单并选择文件|新建文件或项目。\n\n2.  之后，您将看到一个窗口，允许您为项目或文件选择模板。在这个演示中，我们将选择 Qt 小部件应用:\n\n![](img/61467706-8fad-4cfb-be1b-a27dce3aae57.png)\n\n3.  之后，设置您的项目名称和项目目录。您还可以选中“用作默认项目位置”复选框，以便下次在 Qt 中创建新项目时可以自动获得相同的路径。\n\n4.  接下来，Qt Creator 将要求您为您的项目选择一个或多个工具包。对于这个演示，我们将选择带有 MinGW 编译器的桌面 Qt。不要担心，因为您可以在开发过程中添加或删除项目中的工具包:\n\n![](img/f117e1ba-50ee-481b-ab1b-52bdb4a9f7d1.png)\n\n5.  之后，你会看到一个页面，上面写着班级信息。这基本上是您为基本窗口设置类名的地方，但是我们不会更改任何内容，所以只需单击“下一步”按钮继续:\n\n![](img/4955e578-162a-477d-9739-1e799f4c42bd.png)\n\n6.  最后，它会要求您将项目链接到版本控制服务器。如果您以前没有向 Qt 添加任何设置，您可以单击配置按钮，这将带您进入我在本章前面部分向您展示的设置对话框。\n7.  然而，对于这个演示，我们将保持设置为<none>并按下完成按钮。然后，Qt Creator 将继续为您的项目生成必要的文件。一两秒钟后，Qt Creator 将自动切换到编辑模式，您应该能够在项目面板下看到它为您创建的文件。您可以通过双击 Qt Creator 中的任何文件来打开它们，它们将显示在位于右侧的编辑器中:</none>\n\n![](img/cb27c62e-5ab5-4690-b775-70a353b84b85.png)\n\n8.  在我们开始编译项目之前，让我们在您的项目面板中打开`Forms`目录下的`mainwindow.ui`文件。不要太担心用户界面，因为我们将在下一章讨论它。我们需要做的是单击“显示小部件”类别下的“标签”图标，并将其拖动到右侧窗口的中心，如下图所示:\n\n![](img/60c3a7ec-dbe6-4832-a1b5-0b36788c4e76.png)\n\n9.  之后，双击`Text Label`小部件，将文本更改为`Hello World!`。完成后，点击键盘上的*进入*按钮:\n\n![](img/6d7f068a-eca4-4c99-9253-8f31e8ccfed0.png)\n\n10.  最后一步是按左下角的运行按钮，如下所示:\n\n![](img/a071be71-b7fd-4cf3-92b0-ee7e0fd16d28.png)\n\n11.  我们通常会先构建程序，然后运行程序，但是 Qt Creator 足够聪明，能够发现它需要构建它。但是，单独构建和运行应用仍然是一个好习惯。经过几秒钟的编译，...瞧啊。你已经用 Qt 创建了你的第一个`Hello World`程序！：\n\n![](img/6823e337-e451-4170-898a-cd7ef9504c04.png)\n\n# 摘要\n\nQt Creator 等工具的存在使得设计应用的用户界面对开发人员来说变得简单而有趣。我们不再需要仅仅为了创建一个按钮而编写一堆代码，或者仅仅为了调整文本标签的位置而更改一堆代码，因为在我们设计图形用户界面时，Qt Designer 会为我们生成这些代码。Qt 已经将**所见即所得** ( **所见即所得**)的理念应用到工作流程中，它为我们提供了完成工作所需的所有便利和效率。\n\n在下一章，我们将学习 Qt Creator 的来龙去脉，并开始用 Qt 设计我们的第一个 GUI！"
  },
  {
    "path": "docs/cpp-gui-prog-qt5/02.md",
    "content": "# 二、Qt 小部件和样式表\n\n使用 Qt 进行软件开发的一个优势是，使用 Qt 提供的工具设计程序的**图形用户界面** ( **GUI** )非常容易。在本书中，我们将尝试创建一个单一的项目，该项目涉及 Qt 的许多不同组件和模块。我们将在每一章中浏览项目的每一部分，这样您最终将能够掌握整个 Qt 框架，同时完成演示项目，这是一个非常有价值的项目，可以添加到您的投资组合中。你可以在[找到所有的源代码。](https://github.com/PacktPublishing/Hands-On-GUI-Programming-with-C-QT5)\n\n在本章中，我们将涵盖以下主题:\n\n*   Qt 设计器简介\n*   基本 Qt 小部件\n*   qt 样式表\n\n在本章中，我们将深入研究 Qt 在轻松设计外观流畅的图形用户界面方面能为我们提供什么。在本章的开始，您将会被介绍 Qt 提供的小部件类型及其功能。之后，我们将通过一系列步骤，使用 Qt 设计我们的第一个表单应用。\n\n# Qt 设计器简介\n\nQt 中有两种类型的图形用户界面应用，即 Qt 快速应用和 Qt 小部件应用。在本书中，我们将主要介绍后者，因为这是为桌面应用设计图形用户界面的标准方式，而 Qt Quick 更广泛地用于移动和嵌入式系统:\n\n1.  我们需要做的第一件事是打开 Qt Creator 并创建一个新项目。您可以通过转到文件|新建文件或项目，或单击欢迎屏幕上的新建项目按钮来完成此操作:\n\n![](img/9d6b9538-0ada-4bb2-89f9-17caebfb9a74.png)\n\n2.  之后，会弹出一个新窗口，要求您选择要创建的项目类型。在应用类别下选择 Qt 小部件应用，然后单击选择...，然后，为您的项目创建一个名称(我选择了`Chapter2`作为我的项目名称)，并通过单击浏览来选择项目目录...按钮:\n\n![](img/4e7a8d84-01bf-4955-845d-8aa6561f1888.png)\n\n3.  接下来，您将被要求为您的项目选择一个工具包。如果您在 Windows 系统上运行此程序，并且安装了 Microsoft Visual Studio，则可以使用 MSVC 编译器选择相关的工具包；否则，选择运行 MinGW 编译器的版本。Qt 通常预装 MinGW 编译器，所以你不需要单独下载。如果你在 Linux 系统上运行这个，那么你会看到 GCC 工具包，或者如果你在 macOS 上运行这个，会看到 Clang 工具包。要了解更多关于*套件和构建设置*的信息，请查看[第 15 章](14.html)、*跨平台开发*:\n\n![](img/27f4cfa6-475e-4f42-b565-9b1032f053dc.png)\n\n4.  之后，新项目向导将要求您命名主窗口类。我们将使用默认设置，然后单击“下一步”按钮继续:\n\n![](img/807a7eff-6385-4e4b-bb7f-152a098a229e.png)\n\n5.  最后，您将被要求将您的版本控制工具链接到您的项目。通过将版本控制工具链接到您的项目，您将能够在远程服务器上保留代码的每个修订版，并跟踪对项目所做的所有更改。如果你在一个团队中工作，这尤其有用。但是，在本教程中，我们将不使用任何版本控制，因此我们只需单击“完成”按钮继续:\n\n![](img/de7bb5f9-296f-4f14-9e74-3585be29dc2d.png)\n\n6.  完成后，Qt Creator 将打开您的新项目，您将能够看到左上角显示的项目目录，如下所示:\n\n![](img/36036ee1-58f6-4813-92e1-f96125abb552.png)\n\n7.  现在，在项目目录面板上双击打开`mainwindow.ui`。然后，Qt Creator 将切换到另一种模式，称为 Qt Designer，它本质上是一种用于为程序设计基于小部件的图形用户界面的工具。一旦 Qt 设计器被激活，您将在左侧面板上看到一个可用的小部件列表，并在右侧看到一个供您设计图形用户界面的地方。在开始学习如何设计自己的 UI 之前，让我们花点时间熟悉一下 Qt Designer 的界面:\n\n![](img/0e7c7455-3546-4bdd-ad91-6e405747afe6.png)\n\n以下数字代表上一张截图中显示的用户界面:\n\n1.  **菜单栏**:菜单栏是你找到 Qt Creator 所有基本功能的地方，比如新建项目、保存文件、更改编译器设置等等。\n2.  **小部件框**:小部件框有点像工具箱，里面显示着 Qt Designer 提供的所有不同的小部件，随时可以使用。您可以将小部件框中的任何小部件直接拖放到表单编辑器的画布上，它们将出现在您的程序中。\n\n3.  **模式选择器**:模式选择器是可以通过点击“编辑”或“设计”按钮，在源代码编辑或 UI 设计之间快速轻松切换的地方。您还可以通过单击位于模式选择器面板上的调试器和探查器工具各自的按钮来轻松导航到它们。\n4.  **构建快捷方式**:这里显示了三个不同的快捷按钮——构建、运行和调试。您可以通过按这里的按钮而不是菜单栏上的按钮来轻松地构建和测试运行您的应用。\n5.  **表单编辑器**:这里是你应用你的创意，设计应用 UI 的地方。您可以将任何小部件从小部件框拖放到表单编辑器的画布上，以便它出现在您的程序中。\n6.  **表单工具栏**:表单工具栏是可以快速选择不同表单进行编辑的地方。您可以通过单击小部件框上方的下拉框并选择要用 Qt 设计器打开的用户界面文件来更改为不同的表单。还有一些按钮允许您在表单编辑器和用户界面布局的不同模式之间切换。\n7.  **对象检查器**:这是当前`.ui`文件中的所有小部件以分层方式列出的地方。小部件按照其与其他小部件的父子关系排列在树列表中。通过在表单编辑器中移动小部件，可以轻松地重新排列它的层次结构。\n8.  **属性编辑器**:当您从对象检查器窗口(或表单编辑器窗口)中选择一个小部件时，该特定小部件的属性将显示在属性编辑器中。您可以在这里更改任何属性，结果将立即显示在表单编辑器上。\n9.  **动作编辑器和信号及槽编辑器**:动作编辑器和信号及槽编辑器都位于此窗口。您可以使用动作编辑器创建链接到菜单栏和工具栏按钮的动作。信号和插槽编辑器是您\n10.  **输出窗格**:输出窗格是您在测试应用时查找问题或调试信息的地方。它由几个显示不同信息的窗口组成，如问题、搜索结果、应用输出等。\n\n简而言之，Qt 提供了一个名为 Qt Creator 的一体化编辑器。Qt Creator 与 Qt 附带的几个不同工具协同工作，例如脚本编辑器、编译器、调试器、分析器和 UI 编辑器。在前面的截图中可以看到，用户界面编辑器叫做 Qt 设计器。Qt Designer 是设计人员设计程序用户界面的完美工具，无需编写任何代码。这是因为 Qt Designer 采用了**所见即所得** ( **所见即所得**)的方法，提供了最终结果的精确可视化表示，这意味着无论你用 Qt Designer 设计什么，当程序编译和运行时，结果都是完全相同的。请注意，Qt 附带的每个工具实际上都可以单独运行，但是如果您是初学者或者只是在做一个简单的项目，建议只使用 Qt Creator，它将所有这些工具连接在一个界面中。\n\n# 基本 Qt 小部件\n\n现在，我们将看看 Qt 设计器中可用的默认小部件集。您实际上可以自己创建自定义小部件，但这是一个高级主题，不在本书的讨论范围内。让我们看一下小部件框中列出的前两个类别——布局和间隔:\n\n![](img/b0c9740b-923c-42a5-8f99-7becc72dd3f7.png)\n\n布局和间隔实际上不是您可以直接观察到的，但是它们会影响小部件的位置和方向:\n\n1.  垂直布局:垂直布局小部件在垂直列中从上到下布局小部件。\n2.  水平布局:水平布局小部件将小部件从左到右(对于从右到左的语言，从右到左)排列成水平行。\n\n3.  网格布局:网格布局小部件在二维网格中布局小部件。每个小部件可以占用多个单元格。\n4.  表单布局:表单布局小部件以两列字段样式布局小部件。顾名思义，这种类型的布局最适合输入小部件的形式。\n\nQt 提供的布局对于创建高质量的应用非常重要，并且非常强大。Qt 程序通常不会使用固定的位置来布局元素，因为布局允许对话框和窗口以合理的方式动态调整大小，同时在本地化为不同语言时处理不同长度的文本。如果你不在你的 Qt 程序中使用布局，它的 UI 在不同的计算机或设备上可能看起来非常不同，这在大多数情况下会造成不愉快的用户体验。\n\n接下来，让我们看一下间隔小部件。间隔器是一个不可见的小部件，它沿着特定的方向推动小部件，直到到达布局容器的极限。布局中必须使用间隔物，否则它们不会产生任何效果。\n\n间隔器有两种类型，即水平间隔器和垂直间隔器:\n\n1.  水平间隔器:水平间隔器小部件是占据布局内空间并沿着水平空间推动布局内其他小部件的小部件。\n2.  垂直间隔器:垂直间隔器类似于水平间隔器，只是它沿着垂直空间推动小部件。\n\n很难想象布局和间隔器在没有实际操作的情况下如何工作。别担心，因为我们马上就要试用了。Qt Designer 最强大的功能之一是，您可以试验和测试您的布局，而不必在每次更改后都更改和编译您的代码。\n\n除了布局和间隔，还有几个类别，即按钮、项目视图、容器、输入小部件和显示小部件。我不会去解释他们每一个人，因为他们的名字不言自明。您也可以将小部件拖放到表单编辑器上，查看它的功能。让我们开始吧:\n\n1.  单击按钮小部件并将其从小部件框拖动到表单编辑器，如下图所示:\n\n![](img/b08ee4fd-99be-4cc4-b9ea-05ff5ebcc182.png)\n\n2.  然后，选择新添加的按钮小部件，您将看到与此特定小部件相关的所有信息现在都出现在属性编辑器面板上:\n\n![](img/0e37a02f-3c9f-4105-8f8e-c9c5adc6a2c9.png)\n\n3.  您可以在 C++ 代码中以编程方式更改小部件的属性，如外观、焦点策略、工具提示等。有些属性也可以在表单编辑器中直接编辑。让我们双击按钮并更改按钮的文本，然后通过拖动按钮的边缘来调整按钮的大小:\n\n![](img/66b379d7-9073-4de9-9ac0-4c67809f03e4.png)\n\n4.  完成后，让我们将水平布局拖放到表单编辑器中。然后，将按钮拖到新添加的布局中。现在，您将看到按钮自动适应布局:\n\n![](img/46d70ffd-ef5a-40d5-886e-3d909a82504b.png)\n\n5.  默认情况下，主窗口不带有任何布局效果，因此小部件将停留在原来的位置，即使窗口正在调整大小，这看起来也不太好。要向主窗口添加布局效果，请在表单编辑器中的窗口上单击鼠标右键，选择“布局”，最后选择“垂直布局”。您现在将看到我们之前添加的水平布局小部件现在正在自动扩展以适合整个窗口。这是 Qt 中布局的正确行为:\n\n![](img/7df2925b-fe74-48f6-9624-923afa4de14b.png)\n\n6.  接下来，我们可以玩一下间隔器，看看它有什么效果。我们将把一个垂直间隔拖放到包含按钮的布局的顶部，然后在按钮的布局内，在按钮的两侧放置两个水平间隔:\n\n![](img/b786e4c5-9668-4c30-ac42-3ad74bade19c.png)\n\n间隔器将推动位于两端的所有小部件，并占据空间本身。在本例中，提交按钮将始终停留在窗口的底部，并保持其中间位置，而不管窗口的大小如何。这使得图形用户界面看起来很好，即使在不同的屏幕尺寸上。\n\n自从我们在窗户上加了垫片后，我们的按钮就被压缩到最小尺寸。让我们通过将其`minimumSize`属性设置为 120 x 40 来放大按钮，您会看到按钮现在看起来大了很多:\n\n![](img/10f7a183-c62d-4c25-bf11-16e2e9784767.png)\n\n7.  之后，让我们在按钮布局的上方添加一个表单布局，并在它的下方添加一个垂直间隔。现在，您将看到表单布局非常薄，因为它被我们之前放置在主窗口上的垂直间隔器挤压，当您想要将小部件拖放到表单布局中时，这可能会很麻烦。要解决这个问题，暂时将`layoutTopMargin`属性设置为`20`或更高:\n\n![](img/190414c4-4b1b-4b30-b24c-940dc11f5e7e.png)\n\n8.  然后，将两个标签拖放到表单布局的左侧，将两行编辑拖放到表单布局的右侧。双击两个标签，分别将其显示文本更改为`Username:`和`Password:`。完成后，将表单布局的`layoutTopMargin`属性设置回`0`:\n\n![](img/80a44e78-2571-458f-b9db-adbae4d956fa.png)\n\n目前，图形用户界面看起来很棒，但是表单布局现在占据了中间的整个空间，当主窗口最大化时，这并不令人愉快。为了保持表单紧凑，我们将执行以下步骤，这些步骤有点棘手:\n\n9.  首先，在表单上方拖放一个水平布局，将其`layoutTopMargin`和`layoutBottomMargin`设置为`20`，这样我们稍后放置在其中的小部件就不会离提交按钮太近。接下来，拖放整个表单布局，我们之前将它放入水平布局。然后，在表单的两侧放置水平间隔以保持居中。下面的截图说明了这些步骤:\n\n![](img/e87bdb1a-9eab-4ffc-bad2-59b8bf8a76af.png)\n\n10.  之后，我们可以进一步调整图形用户界面，使其看起来整洁，然后再进入下一部分，在那里我们将定制小部件的样式。让我们从将两个线编辑小部件的`minimumSize`属性设置为 150 x 25 开始。然后，将表单布局的`layoutLeftMargin`、`layoutRightMargin`、`layoutTopMargin`和`layoutBottomMargin`属性设置为`25`。我们之所以要这样做，是因为我们将在下一节中向表单布局添加一个大纲。\n\n11.  由于按钮现在距离表单布局太远，让我们设置水平布局的`layoutBottomMargin`属性，它将表单布局设置为`0`。这将使按钮稍微移动到表单布局的上方并靠近表单布局。之后，我们将调整按钮的大小，使其与表单布局对齐。让我们将按钮的`minimumSize`属性设置为 260 x 35，我们就完成了！：\n\n![](img/cc448268-3595-446c-a1cb-1fb416824a36.png)\n\n您也可以通过转到工具|表单编辑器|预览来预览图形用户界面，而无需构建程序。在没有陡峭学习曲线的情况下，为 Qt 程序设计圆滑的图形用户界面时，Qt 设计器是一个非常方便的工具。在下一节中，我们将学习如何使用 Qt 样式表定制小部件的外观。\n\n# qt 样式表\n\nQt 的 Widgets 应用使用了一个名为 Qt 样式表的样式系统，它类似于 web 技术的样式系统— **CSS** ( **层叠样式表**)。您所需要做的就是编写小部件的样式描述，Qt 将相应地呈现它。Qt 样式表的语法与 CSS 非常相似。\n\nQt 样式表的灵感来自于 CSS，因此它们彼此非常相似:\n\n*   Qt 样式表:\n\n```cpp\nQLineEdit { color: blue; background-color: black; } \n```\n\n*   CSS:\n\n```cpp\nh1 { color: blue; background-color: black; } \n```\n\n在前面的例子中，Qt 样式表和 CSS 都包含一个声明块和一个选择器。每个声明由属性和值组成，用冒号分隔。\n\n您可以使用两种方法来更改小部件的样式表——直接使用 C++ 代码或使用属性编辑器。如果使用 C++ 代码，可以调用`QObject::setStyleSheet()`函数，如下所示:\n\n```cpp\nmyButton->setStyleSheet(\"background-color: green\"); \n```\n\n前面的代码将我们的按钮小部件的背景颜色更改为绿色。您也可以通过将相同的声明写入 Qt 设计器中小部件的`styleSheet`属性来获得相同的结果:\n\n```cpp\nQPushButton#myButton { background-color: green } \n```\n\n要了解更多关于 Qt 样式表的语法和属性，请参考以下链接:[http://doc.qt.io/qt-5/stylesheet-reference.html](http://doc.qt.io/qt-5/stylesheet-reference.html)\n\n让我们继续我们的项目，并应用一个自定义的 Qt 样式表到我们的图形用户界面！\n\n1.  首先，右键单击提交按钮并选择更改样式表...将弹出一个窗口，供您编辑小部件的样式表:\n\n![](img/26ee01cd-38ad-4ec7-a8d5-907c466e4c39.png)\n\n2.  然后，将以下内容添加到样式表编辑器窗口:\n\n```cpp\nborder: 1px solid rgb(24, 103, 155); \nborder-radius: 5px; \nbackground-color: rgb(124, 203, 255); \ncolor: white;\n```\n\n3.  完成后，单击“确定”按钮，您应该能够看到“提交”按钮将其外观更改为:\n\n![](img/e14883aa-10c4-4615-93c4-30d02a0923c4.png)\n\n我们之前使用的样式表几乎是不言自明的。它启用按钮的边界线，并使用 RGB 值将边框颜色设置为深蓝色。然后，它还对按钮应用圆角效果，并将其背景颜色更改为浅蓝色。最后，提交文本也被更改为白色。\n\n4.  接下来，我们希望将自定义样式表应用于表单布局。但是，您会注意到没有变更样式表...选项。这是因为布局不带有该属性。为了将样式应用于表单布局，我们必须首先将其转换为 QWidget 或 QFrame 对象。为此，右键单击表单布局并选择变形到| QFrame:\n\n![](img/3634c2e1-b183-4dd3-ab0e-845d8041d60c.png)\n\n5.  完成后，您会注意到它现在带有`styleSheet`属性，因此我们现在可以自定义它的外观。让我们右键单击它并选择“更改样式表”...打开样式表编辑器窗口。然后，插入以下脚本:\n\n```cpp\n#formFrame { \nborder: 1px solid rgb(24, 103, 155); \nborder-radius: 5px; \nbackground-color: white; } \n```\n\n单词`formFrame`指的是小部件的`objectName`属性，它必须与小部件的确切名称相匹配，否则样式将不会应用于它。我们为这个例子定义小部件名称的原因(我们在上一个例子中没有这样做)是因为如果我们不指定小部件名称，样式也将应用于它的所有子代。你可以试着从前面的脚本中删除`#formFrame {}`，看看会发生什么——现在，即使是标签和线编辑也有边界线，这不是我们想要做的。图形用户界面现在如下所示:\n\n![](img/120838d2-fa4d-46c0-8ef9-fd47c956e19b.png)\n\n6.  最后，我们希望有一个好看的背景，我们可以通过附加一个背景图像来做到这一点。为此，我们首先需要将图像导入 Qt 的资源系统。转到文件|新文件或项目...然后，在文件和类类别下选择 Qt。之后，选择 Qt 资源文件并点击选择...按钮。Qt 资源系统是一种独立于平台的机制，用于在应用的可执行文件中存储二进制文件。您基本上可以使用 Qt 资源文件将所有这些重要的文件，如图标图像或语言文件，直接存储到您的可执行文件中。这些重要文件将在编译过程中直接嵌入到您的程序中。\n\n7.  然后，在按下“下一步”按钮之前键入文件名并设置其位置，然后单击“完成”按钮。现在，您将看到一个新的资源文件正在创建，我将其命名为`resource.qrc`:\n\n![](img/50822283-7c7e-4793-8d0b-b8aecd132fbd.png)\n\n8.  用 Qt 创建器打开`resource.qrc`，选择添加|添加前缀。之后，键入您喜欢的前缀，例如，`/images`。完成后，再次选择添加，这次选择添加文件。添加名为`login_bg.png`的示例项目提供的图像文件。然后，保存`resource.qrc`并右键单击图像，选择将资源路径复制到剪贴板。然后关闭`resource.qrc`，再次打开`mainwindow.ui`:\n\n![](img/6dd73f49-ae5b-4203-b622-73a4a5516060.png)\n\n9.  我们需要做的下一件事是从对象检查器中右键单击`centralWidget`对象，然后选择更改样式表...，然后插入以下脚本:\n\n```cpp\n#centralWidget { \nborder-image: urlimg/login_bg.png); }\n```\n\n10.  `url()`内的文字可以通过按 *Ctrl* + *V* (或粘贴)来插入，因为在上一步中我们选择了将资源路径复制到剪贴板时，它被复制到了剪贴板。最终结果如下:\n\n![](img/7234cdb9-3537-4851-94f0-2f498ad43a0e.png)\n\n请确保您也构建并运行了应用，然后检查最终结果是否与预期的一样。为了让它看起来真正专业，还有很多东西需要调整，但是到目前为止，它看起来非常棒！\n\n# 摘要\n\nQt Designer 确实彻底改变了我们设计程序图形用户界面的方式。它不仅包括所有常见的小部件，而且还有一些便利的东西，如布局和间隔，这使得我们的程序在不同类型的显示器和屏幕尺寸上运行得非常好。此外，请注意，我们已经成功地创建了一个具有漂亮用户界面的工作应用，而无需编写一行 C++ 代码！\n\n我们在这一章学到的仅仅是 Qt 的皮毛，因为我们还有很多特性需要学习！加入我们的下一章，了解我们如何让我们的程序真正发挥作用！"
  },
  {
    "path": "docs/cpp-gui-prog-qt5/03.md",
    "content": "# 三、数据库连接\n\n在前一章中，我们学习了如何从头开始创建登录页面。但是，它还没有运行，因为登录页面没有连接到数据库。在本章中，您将学习如何将 Qt 应用连接到验证登录凭据的 MySQL(或 MariaDB)数据库。\n\n在本章中，我们将涵盖以下主题:\n\n*   MySQL 数据库系统介绍\n*   建立 MySQL 数据库\n*   sqlcommand\n*   Qt 中的数据库连接\n*   功能登录页面\n\n我们将逐步浏览本章，以发现 Qt 附带的强大功能，并允许您的应用直接连接到数据库，而无需任何额外的第三方依赖。数据库查询本身是一个巨大的话题，但是我们将能够通过示例和实用方法从头学习最基本的命令。\n\nQt 支持多种不同类型的数据库系统:\n\n*   MySQL(或 MariaDB)\n*   SQLite(版本 2 和 3)\n*   IBM DB2\n*   神谕\n*   开放式数据库连接性\n*   一种数据库系统\n*   Sybase 自适应服务器\n\n最受欢迎的两个是 MySQL 和 SQLite。SQLite 数据库通常脱机使用，它不需要任何设置，因为它使用磁盘文件格式来存储数据。因此，在本章中，我们将学习如何建立 MySQL 数据库系统，同时学习如何将我们的 Qt 应用连接到 MySQL 数据库。用于连接到 MySQL 数据库的 C++ 代码可以被重用来连接到其他数据库系统，而无需进行许多修改。\n\n# MySQL 数据库系统介绍\n\n**MySQL** 是基于关系模型的开源数据库管理系统，是现代数据库系统为各种目的存储信息最常用的方法。\n\n与其他一些遗留模型(如对象数据库系统或分层数据库系统)不同，关系模型已经被证明更加用户友好，并且比其他模型表现更好。这就是为什么我们今天看到的大多数现代数据库系统大多使用这种方法的原因。\n\nMySQL 最初是由一家名为 **MySQL AB** 的瑞典公司开发的，它的名字是该公司联合创始人女儿的名字 *My* 和 *SQL* 的组合，后者是**结构化查询语言**的缩写。\n\n与 Qt 类似，MySQL 在其整个历史中也曾被多个不同的人拥有。最引人注目的收购发生在 2008 年，太阳微系统公司以 10 亿美元收购了 MySQL AB。一年后的 2009 年，**甲骨文公司**收购了太阳微系统公司，因此直到今天，MySQL 仍归甲骨文所有。即使 MySQL 几次易手，它仍然是一个开源软件，允许用户根据自己的目的更改代码。\n\n由于它的开源性质，还有其他数据库系统是从 MySQL 项目派生/分叉而来的，比如**马里亚数据库**、 **Percona 服务器**等等。然而，这些替代方案与 MySQL 并不完全兼容，因为它们已经对其进行了修改以满足自己的需求，因此这些系统中的一些命令可能会有所不同。\n\n根据 **Stack Overflow** 在 2017 年进行的一项调查，MySQL 是 web 开发人员中使用最广泛的数据库系统，我们可以在下面的截图中看到:\n\n![](img/ea013fb4-46cf-44fc-ac35-1968cc90e84f.png)\n\n调查结果表明，您在本章中学到的知识不仅可以应用于 Qt 项目，还可以应用于网络、移动应用和其他类型的应用。\n\n此外，MySQL 及其变体正被大公司和项目组使用，如脸书、YouTube、推特、美国宇航局、Wordpress、Drupal、Airbnb、Spotify 等。这意味着在开发过程中遇到任何技术问题时，您都可以很容易地得到答案。\n\nFor more information regarding MySQL, please visit:\n[https://www.mysql.com](https://www.mysql.com)\n\n# 建立 MySQL 数据库\n\n有许多不同的方法来设置您的 MySQL 数据库。这真的取决于你运行的平台类型，无论是 Windows、Linux、Mac 还是其他任何类型的操作系统；它还将取决于您的数据库的用途——无论是用于开发和测试，还是用于大规模生产服务器。\n\n对于大规模的服务(比如社交媒体)，最好的方法是从源头编译 MySQL，因为像这样的项目需要大量的优化、配置，有时还需要定制，以便处理大量的用户和流量。\n\n但是，如果您打算正常使用，您可以下载预编译的二进制文件，因为默认配置已经足够了。你可以从他们的官方网站安装一个独立的 MySQL 安装程序，或者下载除了 MySQL 之外的其他几个软件附带的安装包。\n\n在本章中，我们将使用名为 **XAMPP** 的软件包，这是一个由名为**阿帕奇之友**的小组开发的网络服务器堆栈包。该软件包附带有 **Apache** 、 **MariaDB** 、 **PHP** 以及其他可在安装过程中添加的可选服务。以前，MySQL 是包的一部分，但从 5.5.30 和 5.6.14 版本开始，它已经被 **MariaDB** 取代。MariaDB 的工作原理几乎与 MySQL 相同，除了那些涉及高级功能的命令，我们不会在本书中使用这些命令。\n\n我们使用 XAMPP 的原因是，它有一个控制面板，可以轻松启动和停止服务，而无需使用命令提示符，并提供对配置文件的轻松访问，而无需您自己深入安装目录。对于需要频繁测试的应用开发来说，这非常快速有效。但是，不建议您在生产服务器上使用 XAMPP，因为默认情况下某些安全功能已被禁用。\n\n或者，您也可以通过其他类似的软件包安装 MySQL，如 **AppServ** 、 **AMPPS** 、 **LAMP** (仅限 Linux)、 **WAMP** (仅限 Windows)、 **Zend** **服务器**等。\n\n现在，让我们学习如何安装 XAMPP:\n\n1.  首先，前往他们位于[https://www.apachefriends.org](https://www.apachefriends.org)的网站，点击位于您屏幕底部的下载按钮之一，该按钮显示您当前操作系统的图标:\n\n![](img/d5053fc7-76bd-47ad-bf75-a82f1f8758db.png)\n\n2.  单击下载按钮后，下载过程将在几秒钟内自动开始，一旦完成，它将继续安装程序。确保在安装过程开始之前包含 Apache 和 MySQL/MariaDB。\n\n3.  安装 XAMPP 后，从开始菜单或桌面快捷方式启动控制面板。之后，你可能会注意到什么都没发生。这是因为默认情况下，XAMPP 控制面板隐藏在任务栏中。您可以通过右键单击控制面板窗口并在弹出菜单中选择“显示/隐藏”选项来显示该窗口。下面的截图向您展示了这在 Windows 机器上的样子。对于 Linux，菜单看起来可能略有不同，但总体来说非常相似。对于 macOS，您必须从发射台或坞站启动 XAMPP:\n\n![](img/2a3b8dac-a0be-4737-9347-82c56c7345a1.png)\n\n4.  单击“显示/隐藏”选项后，您将最终看到屏幕上显示的控制面板窗口。如果您再次点按“显示/隐藏”选项，窗口将被隐藏:\n\n![](img/4984acd2-2086-41b0-bbcd-962dc8e2a416.png)\n\n5.  乍看之下，他们的控制面板几乎不言自明。在左侧，您可以看到在 XAMPP 可用的服务名称，在右侧，您将看到指示开始、配置、日志等的按钮。出于某种原因，XAMPP 将 MySQL 显示为模块名，但实际上它运行的是马里亚数据库。不用担心；两者的工作原理基本相同，因为 MariaDB 是 MySQL 的一个分支。\n6.  在本章中，我们只需要 Apache 和 MySQL (MariaDB)，所以让我们单击这些服务的开始按钮。一两秒钟后，您会看到开始按钮现在被标记为停止，这意味着服务已经启动！：\n\n![](img/ac835466-9711-40f6-b924-8cecf413aeff.png)\n\n7.  为了验证这一点，我们打开浏览器，输入`localhost`作为网址。如果看到类似下图的内容，说明 Apache web 服务器已经成功启动！：\n\n![](img/3b8247ee-f9db-4a17-a1a8-ccc18d7147e4.png)\n\n8.  Apache 在这里非常重要，因为我们将使用它来使用名为 **phpMyAdmin** 的基于网络的管理工具配置数据库。phpMyAdmin 是用 PHP 脚本语言编写的 MySQL 的管理工具，因此得名。尽管它最初是为 MySQL 设计的，但它对 MariaDB 也很有效。\n9.  要访问 phpMyAdmin 控制面板，请在浏览器上键入`localhost/phpmyadmin`。之后，您应该会看到类似这样的内容:\n\n![](img/5b480b92-1af6-46ae-8bf8-e0a965c6b8a0.png)\n\n10.  在页面的左侧，您将看到导航面板，它允许您访问 MariaDB 数据库中可用的不同数据库。页面右侧是各种工具，可让您查看表格、编辑表格、运行 SQL 命令、将数据导出到电子表格、设置权限等。\n11.  默认情况下，您只能在右侧的设置面板上修改数据库的常规设置。您必须从左侧的导航面板中选择一个数据库，然后才能修改特定数据库的设置。\n12.  数据库就像一个文件柜，您可以在里面存储日志。每个日志都称为一个表，每个表都包含数据，这些数据就像电子表格一样排序。当您想要从 MariaDB 获取数据时，您必须在从中获取数据之前指定您想要访问的文件柜(数据库)和日志簿(表)。希望这将使您更好地理解 MariaDB 和其他类似数据库系统背后的概念。\n\n13.  现在，让我们从创建第一个数据库开始！为此，您可以单击导航面板上数据库名称上方的“新建”按钮，或者单击菜单顶部的“数据库”按钮。这两个按钮都会将您带到“数据库”页面，您应该可以在菜单按钮下方看到:\n\n![](img/14b5f1e8-da81-4fc8-a2ab-32ea8391e601.png)\n\n14.  之后，让我们创建第一个数据库！键入所需的数据库名称，然后单击创建按钮。创建数据库后，您将被重定向到“结构”页面，该页面将列出该数据库中包含的所有表。默认情况下，您新创建的数据库不包含任何表，因此您将看到一行文本，显示在数据库中找不到表:\n\n![](img/e4aa4deb-6437-4a98-98f9-f8ea2582bf7c.png)\n\n15.  猜猜我们下一步要做什么？正确，我们将创建我们的第一个表！首先，让我们插入您想要创建的表的名称。因为我们将在本章后面做一个登录页面，让我们命名我们的表`user`。我们将保持默认的列数不变，然后单击开始。\n16.  之后，您将被重定向到另一个页面，其中包含许多供您填写的输入字段列。每一列代表一个数据结构，该数据结构将在创建后添加到您的表中。\n17.  您需要添加到表结构中的第一件事是一个标识，它将在每次插入新数据时自动增加。然后，添加一个时间戳列来指示数据插入的日期和时间，这有利于调试。最后但同样重要的是，我们将为登录验证添加用户名列和密码列。如果您不确定如何操作，请参考下图。确保您遵循图像中圈出的设置:\n\n![](img/75358cae-706a-4724-bf52-f7e4b844bfff.png)\n\n18.  结构的类型非常重要，必须根据其预期目的进行设置。例如，id 列必须设置为 INT(整数)，因为它必须是一个完整的数字，而用户名和密码必须设置为 VARCHAR 或其他类似的数据类型(CHAR、TEXT 等)，以便正确保存数据。\n\n19.  另一方面，时间戳必须设置为时间戳类型，并且必须将默认值设置为 CURRENT _ 时间戳，这将通知 MariaDB 在数据插入时自动生成当前时间戳。\n20.  请注意，“标识”列的索引设置必须设置为“主要”，并确保勾选了“自动递增”复选框。当您选中“增加索引”复选框时，将出现一个“增加索引”窗口。您可以保持默认设置不变，然后单击“开始”按钮完成步骤并开始创建表格:\n\n![](img/edd187b5-7f50-47b4-9f89-d5caee38de4c.png)\n\n21.  创建新表后，您应该能够看到类似于下图的内容。您仍然可以通过单击“更改”按钮随时编辑结构设置；您也可以通过单击位于列右侧的“删除”按钮来删除任何列。请注意，删除列也会删除属于该列的所有现有数据，并且此操作无法撤消:\n\n![](img/6342b9cf-6219-4036-92a8-e4ed95cf1a96.png)\n\n22.  即使我们通常会通过我们的程序或网页向数据库添加数据，我们也可以直接在 phpMyAdmin 上添加数据来进行测试。要使用 phpMyAdmin 添加数据，首先，您必须创建一个数据库和表，这是我们在前面的步骤中所做的。然后，单击位于菜单顶部的“插入”按钮:\n\n![](img/bf34412c-d063-4f91-8f52-053dd8ee6768.png)\n\n23.  之后，您会看到一个表单出现了，它类似于我们之前创建的数据结构:\n\n![](img/480a40bc-f234-468d-9184-1e3e5b45e03c.png)\n\n24.  您可以简单地忽略标识和时间戳值，因为它们将在您保存数据时自动生成。在这种情况下，只需要填写用户名和密码。为了测试，我们把`test`作为用户名，`123456`作为密码。然后，单击开始按钮保存数据。\n\nPlease note that you should not save your password in a human-readable format on your actual production server. You must encrypt the password with a **cryptographic hash** function such as SHA-512, RIPEEMD-512, BLAKE2b, and so on before passing it to the database. This will ensure that the password is not readable by hackers in case your database is being compromised. We will cover this topic at the end of this chapter.\n\n现在我们已经完成了数据库的设置并插入了第一个测试数据，让我们继续学习一些 SQL 命令！\n\n# sqlcommand\n\n大多数流行的关系数据库管理系统，如 **MySQL** 、 **MariaDB** 、 **Oracle SQL** 、**微软 SQL** 等，都使用一种称为 **SQL** ( **结构化查询语言**)的声明性语言与数据库进行交互。SQL 最初是由 IBM 工程师在 20 世纪 70 年代开发的，但后来，它被**甲骨文公司**和那个时代的其他新兴技术公司进一步增强。\n\n如今，SQL 已经成为美国国家标准协会(T2)和国际标准化组织(T4)的标准。SQL 语言已经被许多不同的数据库系统所采用，并且已经成为现代最流行的数据库语言之一。\n\n在本节中，我们将学习一些基本的 SQL 命令，您可以使用这些命令与您的 MariaDB 数据库进行交互，特别是获取、保存、修改和删除数据库中的数据。这些基本命令可以在其他类型的基于 SQL 的数据库系统中使用，也可以在 ANSI 和 ISO 标准下使用。只是，一些更高级/定制的功能在不同的系统中可能会有所不同，因此在使用这些高级功能之前，请务必阅读系统手册。\n\n好了，我们开始吧！\n\n# 挑选\n\n大多数的 SQL 语句都是一个字的简短和不言自明的。例如，该语句用于从特定表中选择一列或多列，并从所述列中获取数据。让我们来看看一些使用`SELECT`语句的示例命令。\n\n以下命令从`user`表中检索所有列的所有数据:\n\n```cpp\nSELECT * FROM user;\n```\n\n以下命令仅从用户表中检索`username`列:\n\n```cpp\nSELECT username FROM user;\n```\n\n以下命令从`user`表中检索`username`和`password`列，条件是`id`等于`1`:\n\n```cpp\nSELECT username, password FROM user WHERE id = 1;\n```\n\n您可以使用 phpMyAdmin 自己尝试这些命令。为此，请在 phpMyAdmin 中单击位于菜单顶部的“SQL”按钮。之后，您可以在下面的文本字段中键入命令，然后单击“执行”来执行查询:\n\n![](img/7a345815-c3c1-45bb-93aa-affbbcaee5fa.png)\n\nTo learn more about the `SELECT` statement, please refer to the following link: \n[https://dev.mysql.com/doc/refman/5.7/en/select.html](https://dev.mysql.com/doc/refman/5.7/en/select.html)\n\n# 插入\n\n接下来，`INSERT`语句用于将新数据保存到数据库表中。例如:\n\n```cpp\nINSERT INTO user (username, password) VALUES (\"test2\", \"123456\");\n```\n\n前面的 SQL 命令将`username`和`password`数据插入到`user`表中。还有一些其他的语句可以和`INSERT`一起使用，比如`LOW_PRIORITY`、`DELAYED`、`HIGH_PRIORITY`等等。\n\nPlease refer to the following link to learn more about these options:\n[https://dev.mysql.com/doc/refman/5.7/en/insert.html](https://dev.mysql.com/doc/refman/5.7/en/insert.html)\n\n# 更新\n\n`UPDATE`语句修改数据库中的现有数据。您必须为`UPDATE`命令指定一个条件，否则它将修改表中的每一条数据，这不是我们想要的行为。试试下面的命令，会改变第一个用户的`username`和`password`:\n\n```cpp\nUPDATE user SET username = \"test1\", password = \"1234321\" WHERE id = 1;\n```\n\n但是，如果 ID 为`1`的用户不存在，该命令将失败。如果您提供的`username`和`password`数据与存储在数据库中的数据完全匹配，该命令还将返回状态`0 rows affected`(无变化)。有关`UPDATE`声明的更多信息，请参考以下链接:\n\n[https://dev . MySQL . com/doc/ref man/5.7/en/update . html](https://dev.mysql.com/doc/refman/5.7/en/update.html)\n\n# 删除\n\n`DELETE`语句从数据库的特定表中删除数据。例如，以下命令从携带标识`1`的`user`表中删除一个数据:\n\n```cpp\nDELETE FROM user WHERE id = 1;\n```\n\n即使可以使用此语句删除不需要的数据，也不建议从数据库中删除任何数据，因为该操作无法撤消。最好在表中添加另一个名为 status 的列，并使用它来指示是否应该显示数据。例如，如果您的用户删除了前端应用上的数据，请将该数据的状态设置为(比如说)`1`而不是`0`。然后，当你想在前端显示数据时，只显示带有`0`状态的数据:\n\n![](img/7858c675-a8f2-41dd-8112-e762d1c1b0a8.png)\n\n这样，任何意外删除的数据都可以轻松恢复。如果您只打算使用 true 或 false，也可以为此使用 BOOLEAN 类型。我通常会使用 TINYINT，以防将来需要第三或第四个身份。有关`DELETE`声明的更多信息，您可以参考以下链接:\n\n[https://dev . MySQL . com/doc/ref man/5.7/en/delete . html](https://dev.mysql.com/doc/refman/5.7/en/delete.html)\n\n# 加入\n\n使用关系数据库管理系统的优点是，数据可以很容易地从不同的表中连接在一起，并且可以以单个批量返回给用户。这大大提高了开发人员的工作效率，因为在设计复杂的数据库结构时，它允许流动性和灵活性。\n\n在 MariaDB/MySQL 中有许多类型的连接语句——内部连接、完全外部连接、左连接和右连接。所有这些不同的 JOIN 语句在执行时都有不同的行为，如下图所示:\n\n![](img/12b3d2f1-150c-48c3-b1d5-07f2d459d007.png)\n\n大多数情况下，我们将使用 INNER JOIN 语句，因为它只返回两个表中具有匹配值的数据，因此只返回少量所需的数据。JOIN 命令比其他命令复杂得多，因为您首先需要将表设计为可连接的。在我们开始测试 JOIN 命令之前，让我们创建另一个表来实现这一点。我们将这个新的餐桌部门称为:\n\n![](img/02429334-d381-4597-82cc-d533541239a6.png)\n\n之后，添加两个部门，如是:\n\n![](img/e22b95e9-823e-4da3-8983-435a77825095.png)\n\n然后，转到用户表，在结构页面上，一直滚动到底部，查找显示的表单，然后单击“转到”按钮:\n\n![](img/ca934b9d-4c81-40e2-8830-f1ee803de1a0.png)\n\n增加一个名为 deptID(代表部门 ID)的新列，并将其数据类型设置为`int`(整数):\n\n![](img/50b28f30-fb37-402b-9748-76115be1cd2c.png)\n\n然后，设置几个测试用户，将他们的每个 deptID 设置为`1`或`2`:\n\n![](img/fc9743b9-80f9-45b3-b8f5-3c2dc3db7af2.png)\n\n请注意，我还在这里添加了状态列，用于检查用户是否已被删除。完成后，让我们尝试运行一个示例命令！：\n\n```cpp\nSELECT my_user.username, department.name FROM (SELECT * FROM user WHERE deptID = 1) AS my_user INNER JOIN department ON department.id = my_user.deptID AND my_user.status = 0 \n```\n\n乍一看，这看起来相当复杂，但如果你把它分成几个部分，就真的不复杂了。我们将首先从`()`括号内的命令开始，在该命令中，我们要求 MariaDB/MySQL 选择`user`表中带有`deptID =  1`的所有列:\n\n```cpp\nSELECT * FROM user WHERE deptID = 1 \n```\n\n之后，将其包含在一个`()`括号内，并将整个命令命名为`my_user.`，之后，您可以使用`INNER JOIN`语句开始将您的用户表(现在称为`my_user`)与部门表连接起来。这里我们还为其增加了一些查找数据的条件，比如部门表的 ID 必须与`my_user`的`deptID`匹配，`my_user`的状态值必须为`0`，表示数据仍然有效，没有被标记为删除:\n\n```cpp\n(SELECT * FROM user WHERE deptID = 1) AS my_user INNER JOIN department ON department.id = my_user.deptID AND my_user.status = 0 \n```\n\n最后，在前面添加以下代码来完成 SQL 命令:\n\n```cpp\nSELECT my_user.username, department.name FROM  \n```\n\n让我们尝试前面的命令，看看结果是否如您所料。\n\n只要表通过匹配的列相互链接，就可以使用此方法连接无限个表。\n\nTo find out more about the **JOIN** statement, please visit the following link:\n[https://dev.mysql.com/doc/refman/5.7/en/join.html](https://dev.mysql.com/doc/refman/5.7/en/join.html)\n\n还有许多其他 SQL 语句我们没有在本章中介绍，但是我们已经介绍过的语句几乎是您开始使用的所有语句。\n\n在我们进入下一部分之前，最后一件事——我们必须为应用创建一个用户帐户，以便访问我们的 MariaDB/MySQL 数据库。首先，转到您的 phpMyAdmin 主页，然后单击顶部菜单上的用户帐户:\n\n![](img/e7591330-3f91-4f6c-9fe3-7cee39a65a11.png)\n\n然后，转到底部，查找名为添加用户帐户的链接:\n\n![](img/bf188b4e-ac18-4800-b4a4-f484bef7e05c.png)\n\n进入“添加用户帐户”页面后，在“登录信息”表单中键入用户名和密码信息。确保主机名设置为本地:\n\n![](img/ab7d52be-0aba-4723-b40c-a90718e74ce5.png)\n\n然后，向下滚动并设置用户的全局权限。启用“数据”部分中的选项就足够了，但不要启用其他选项，因为一旦您的服务器遭到破坏，它可能会赋予黑客更改数据库结构的特权:\n\n![](img/d3803205-cff4-4c72-ae76-9da02eba99a0.png)\n\n创建用户帐户后，请按照以下步骤允许新创建的用户访问名为 test 的数据库(或您选择的任何其他表名):\n\n![](img/89c0aee6-4bd7-430d-b521-4abf695e9ba9.png)\n\n单击“执行”按钮后，您现在已经授予用户帐户访问数据库的权限！在下一节中，我们将学习如何将我们的 Qt 应用连接到数据库。\n\n# Qt 中的数据库连接\n\n现在我们已经学习了如何建立一个功能强大的 MySQL/MariaDB 数据库系统，让我们更进一步，在 Qt 中发现数据库连接模块！\n\n在我们继续处理上一章的登录页面之前，让我们先从一个新的 Qt 项目开始，这样更容易演示仅与数据库连接相关的功能，并且我们不会被其他东西分散注意力。这一次，我们将使用名为 Qt 控制台应用的终端风格的应用，因为我们在这个演示中并不真正需要任何图形用户界面:\n\n![](img/b62b379e-c4f9-4152-9670-799387f56f43.png)\n\n创建新项目后，您应该只看到项目中的两个文件，即[project_name]。pro 和 main.cpp:\n\n![](img/f74bcfc6-d1f9-463d-8fc1-25c4a112d9a6.png)\n\n首先你需要做的是打开你的项目文件(`.pro`)，在我这里是 DatabaseConnection.pro，在第一行后面加上`sql`关键字，就像这样:\n\n```cpp\nQT += core sql \n```\n\n就这么简单，我们已经成功地将`sql`模块导入了我们的 Qt 项目！然后，打开`main.cpp`，你应该会看到一个非常简单的脚本，只包含八行代码。这基本上是创建一个空控制台应用所需的全部内容:\n\n```cpp\n#include <QCoreApplication> \nint main(int argc, char *argv[]) \n{ \n   QCoreApplication a(argc, argv); \n   return a.exec(); \n} \n```\n\n为了连接到我们的数据库，我们必须首先将相关的头导入`main.cpp`，如下所示:\n\n```cpp\n#include <QCoreApplication> \n#include <QtSql> \n#include <QSqlDatabase> \n#include <QSqlQuery> \n#include <QDebug> \nint main(int argc, char *argv[]) \n{ \n   QCoreApplication a(argc, argv); \n   return a.exec(); \n} \n```\n\n没有这些头文件，我们将无法使用 Qt 的`sql`模块提供的功能，我们之前已经导入了这些功能。此外，我们还添加了`QDebug`标题，这样我们就可以轻松地在控制台显示器上打印出任何文本(类似于 C++ 标准库提供的`std::cout`功能)。\n\n接下来，我们将向`main.cpp`文件添加一些代码。在`return a.exec()`前增加以下高亮代码:\n\n```cpp\nint main(int argc, char *argv[]) \n{ \n   QCoreApplication a(argc, argv); \n   QSqlDatabase db = QSqlDatabase::addDatabase(\"QMYSQL\"); \n   db.setHostName(\"127.0.0.1\"); \n   db.setPort(3306); \n   db.setDatabaseName(\"test\"); \n   db.setUserName(\"testuser\"); \n   db.setPassword(\"testpass\"); \n   if (db.open()) \n   { \n         qDebug() << \"Connected!\"; \n   } \n   else \n   { \n         qDebug() << \"Failed to connect.\"; \n         return 0; \n   } \n   return a.exec(); \n} \n```\n\n请注意，数据库名称、用户名和密码可能与您在数据库中设置的不同，因此在编译项目之前，请确保它们是正确的。\n\n完成后，让我们单击“运行”按钮，看看会发生什么！：\n\n![](img/61992a63-5c9a-4319-95a0-50e25a5ee0ad.png)\n\n如果您看到以下错误，请不要担心:\n\n![](img/1b176a50-7762-48d9-8b8f-a1db1165c9b8.png)\n\n这仅仅是因为您必须将 MariaDB 连接器(或者 MySQL 连接器，如果您正在运行 MySQL)安装到您的计算机上，并将 DLL 文件复制到您的 Qt 安装路径上。请确保 DLL 文件与服务器的数据库库匹配。你可以打开你的 phpMyAdmin 主页，看看它当前使用的是哪个库。\n\n出于某种原因，即使我用马里亚数据库运行 XAMPP，这里的库名显示的是 libmysql 而不是 libmariadb，所以我不得不安装 MySQL Connector 来代替:\n\n![](img/3b99df22-27e4-4703-92fb-9982f7e896eb.png)\n\nIf you're using MariaDB, please download the MariaDB Connector at the following link:\n[https://downloads.mariadb.org/connector-c](https://downloads.mariadb.org/connector-c) If you're using MySQL instead (or are having the same issue as I did), please visit the other link and download MySQL Connector:\n[https://dev.mysql.com/downloads/connector/cpp/](https://dev.mysql.com/downloads/connector/cpp/)\n\n下载马里亚数据库连接器后，将其安装在您的计算机上:\n\n![](img/3766d4f4-d470-4bc6-8d6f-239d81751bcf.png)\n\n上面的截图显示了 Windows 机器的安装过程。如果您正在运行 Linux，您必须为您的 Linux 发行版下载正确的包。如果您正在运行 Debian、Ubuntu 或其变体之一，请下载 Debian 和 Ubuntu 包。如果您正在运行红帽、Fedora、CentOS 或其变体之一，请下载红帽、Fedora 和 CentOS 软件包。这些软件包的安装是自动化的，所以您可以开始了。但是，如果您两个都没有运行，那么您必须下载下载页面上列出的符合您的系统要求的 gzipped tar 文件。\n\nFor more information about installing MariaDB binary tarballs on Linux, please refer to the following link:\n[https://mariadb.com/kb/en/library/installing-mariadb-binary-tarballs/](https://mariadb.com/kb/en/library/installing-mariadb-binary-tarballs/)\n\n至于 macOS，则需要使用名为 **Homebrew** 的包管理器来安装 MariaDB 服务器。\n\nFor more information, check out the following link:\n[https://mariadb.com/kb/en/library/installing-mariadb-on-macos-using-homebrew/](https://mariadb.com/kb/en/library/installing-mariadb-on-macos-using-homebrew/)\n\n安装完成后，转到它的安装目录，查找 DLL 文件(马里亚数据库为`libmariadb.dll`，MySQL 为`libmysql.dll`)。对于 Linux 和 macOS，是`libmariadb.so`或者`libmysql.so`而不是 DLL。\n\n然后，将文件复制到应用的构建目录(与应用的可执行文件相同的文件夹)。之后，再次尝试运行您的应用:\n\n![](img/b0d5c6d5-4a32-43f6-9050-27f519df0800.png)\n\n如果您仍然收到`Failed to connect`但没有`QMYSQL driver not loaded`消息，请检查您的 XAMPP 控制面板，并确保您的数据库服务正在运行；还要确保代码中输入的数据库名称、用户名和密码都是正确的信息。\n\n接下来，我们可以开始玩 SQL 命令了！在`return a.exec()`前增加以下代码:\n\n```cpp\nQString command = \"SELECT name FROM department\"; \nQSqlQuery query(db); \nif (query.exec(command)) \n{ \n   while(query.next()) \n   { \n         QString name = query.value(\"name\").toString(); \n         qDebug() << name; \n   } \n} \n```\n\n前面的代码将命令文本发送到数据库，并同步等待服务器返回结果。之后，使用`while`循环遍历每一个结果，并将其转换为字符串格式。然后，在控制台窗口上显示结果。如果一切顺利，你应该看到这样的情况:\n\n![](img/1de75368-744f-4875-8829-37ac18d64e11.png)\n\n让我们尝试一些更复杂的东西:\n\n```cpp\nQString command = \"SELECT my_user.username, department.name AS deptname FROM (SELECT * FROM user WHERE status = 0) AS my_user INNER JOIN department ON department.id = my_user.deptID\"; \nQSqlQuery query(db); \nif (query.exec(command)) \n{ \n   while(query.next()) \n   { \n         QString username = query.value(\"username\").toString(); \n         QString department = query.value(\"deptname\").toString(); \n         qDebug() << username << department; \n   } \n} \n```\n\n这次我们用 **INNER JOIN** 组合两个表选择`username`和`department`名称。为了避免混淆名为`name`的变量，使用`AS`语句将其重命名为`deptname`。之后，在控制台窗口显示`username`和`department`名称:\n\n![](img/8b7da1e9-561b-44b2-b239-72c4041f0ccd.png)\n\n我们结束了...暂时的。让我们进入下一部分，在这里我们将学习如何使我们的登录页面发挥作用！\n\n# 创建我们的功能登录页面\n\n既然我们已经学会了如何将我们的 Qt 应用连接到 MariaDB/MySQL 数据库系统，那么现在是时候继续在登录页面上工作了！在前一章中，我们学习了如何设置登录页面的图形用户界面。然而，它根本没有任何作为登录页面的功能，因为它没有连接到数据库和验证登录凭证。因此，我们将学习如何通过增强 Qt 的`sql`模块来实现这一点。\n\n简单回顾一下，登录屏幕是这样的:\n\n![](img/cb136c81-3dfb-4e7b-83f5-61bb09344f75.png)\n\n我们现在需要做的第一件事是命名这个登录页面中重要的小部件，它们是用户名输入、密码输入和提交按钮。您可以通过选择小部件并在属性编辑器中查找属性来设置这些属性:\n\n![](img/b729735b-5cfb-437d-9b48-f45a85d46314.png)\n\n然后，将密码输入的回声模式设置为密码。此设置将通过用点替换来隐藏密码:\n\n![](img/5a5a484f-a746-4be7-8841-08707e28016e.png)\n\n之后，右键单击提交按钮并选择转到插槽...将弹出一个窗口，询问您使用哪个信号。选择已单击()并单击确定:\n\n![](img/aaf55004-4f8c-47b6-ba33-6aa31b5480b3.png)\n\n一个名为`on_loginButton_clicked()`的新函数将被自动添加到`MainWindow`类中。当用户按下提交按钮时，Qt 会触发该功能，因此您只需在此处编写代码，将`username`和`password`提交到数据库进行登录验证。信号和槽机制是 Qt 提供的一个特殊功能，用于对象之间的通信。当一个小部件发出信号时，另一个小部件将得到通知，并继续运行一个特定的功能，该功能旨在对特定的信号做出反应。\n\n让我们看看代码。\n\n首先，在您的项目中添加`sql`关键字(。pro)文件:\n\n`QT += core gui`\n**sql**\n\n然后，继续并添加相关标题到`mainwindow.cpp`:\n\n```cpp\n#ifndef MAINWINDOW_H \n#define MAINWINDOW_H \n\n#include <QMainWindow> \n\n#include <QtSql> \n#include <QSqlDatabase> \n#include <QSqlQuery> \n#include <QDebug> \n#include <QMessageBox> \n```\n\n然后，返回`mainwindow.cpp`并在`on_loginButton_clicked()`功能中添加以下代码:\n\n```cpp\nvoid MainWindow::on_loginButton_clicked() \n{ \n   QString username = ui->userInput->text(); \n   QString password = ui->passwordInput->text(); \n   qDebug() << username << password; \n} \n```\n\n现在，单击“运行”按钮，等待应用启动。然后，任意输入`username`和`password`，然后点击提交按钮。您现在应该看到您的`username`和`password`显示在 Qt Creator 的应用输出窗口上。\n\n接下来，我们将把之前编写的 SQL 集成代码复制到`mainwindow.cpp`中:\n\n```cpp\nMainWindow::MainWindow(QWidget *parent) : \n   QMainWindow(parent), \n   ui(new Ui::MainWindow) \n{ \n   ui->setupUi(this); \n\n   db = QSqlDatabase::addDatabase(\"QMYSQL\"); \n   db.setHostName(\"127.0.0.1\"); \n   db.setPort(3306); \n   db.setDatabaseName(\"test\"); \n   db.setUserName(\"testuser\"); \n   db.setPassword(\"testpass\"); \n\n   if (db.open()) \n   { \n         qDebug() << \"Connected!\"; \n   } \n   else \n   { \n         qDebug() << \"Failed to connect.\"; \n   } \n}\n```\n\n请注意，我在数据库名称、用户名和密码中使用了一些随机文本。请确保您在此处输入了正确的详细信息，并且这些信息与您在数据库系统中设置的信息相匹配。\n\n对于前面的代码，我们做了一个小的改动，就是我们只需要在`mainwindow.cpp`中调用`db = QSqlDatabase::addDatabase(\"QMYSQL\")`而不需要类名称作为声明`QSqlDatabase db`现在已经被重新定位到`mainwindow.h`:\n\n```cpp\nprivate: \n   Ui::MainWindow *ui; \n QSqlDatabase db; \n```\n\n最后，我们添加将`username`和`password`信息与 SQL 命令相结合的代码，并将整个内容发送到数据库中执行。如果有与登录信息匹配的结果，则表示登录成功，否则表示登录失败:\n\n```cpp\nvoid MainWindow::on_loginButton_clicked() \n{ \n   QString username = ui->userInput->text(); \n   QString password = ui->passwordInput->text(); \n\n   qDebug() << username << password; \n\n   QString command = \"SELECT * FROM user WHERE username = '\" + username \n   + \"' AND password = '\" + password + \"' AND status = 0\"; \n   QSqlQuery query(db); \n   if (query.exec(command)) \n   { \n         if (query.size() > 0) \n         { \n               QMessageBox::information(this, \"Login success.\", \"You \n               have successfully logged in!\"); \n         } \n         else \n         { \n               QMessageBox::information(this, \"Login failed.\", \"Login \n               failed. Please try again...\"); \n         } \n   } \n} \n```\n\n再次单击“运行”按钮，查看单击“提交”按钮时会发生什么:\n\n![](img/a486dbd1-070a-42c9-9c64-9b6f274b0987.png)\n\n臀部臀部万岁！登录页面现在功能齐全！\n\n# 摘要\n\n在本章中，我们学习了如何建立一个数据库系统，并使我们的 Qt 应用连接到它。在下一章中，我们将学习如何使用强大的 Qt 框架绘制图表。"
  },
  {
    "path": "docs/cpp-gui-prog-qt5/04.md",
    "content": "# 四、图表\n\n在前一章中，我们学习了如何使用 Qt 的`sql`模块从数据库中检索数据。有许多方法可以将这些数据呈现给用户，例如以表格或图表的形式显示。在本章中，我们将学习如何做后者——使用 Qt 的图表模块用不同类型的图形和图表呈现数据。\n\n在本章中，我们将涵盖以下主题:\n\n*   Qt 中图表的类型\n*   图表和图形实现\n*   创建仪表板页面\n\n从 Qt 5.7 开始，之前只对商业用户开放的几个模块对所有开源包用户都是免费的，包括 Qt Charts 模块。因此，对于大多数没有商业许可的 Qt 用户来说，它被认为是一个非常新的模块。\n\n请注意，与 LGPLv3 许可下提供的大多数 Qt 模块不同，Qt Chart 模块是在 GPLv3 许可下提供的。与 LGPLv3 不同，GPLv3 许可证要求您发布应用的源代码，而您的应用也必须在 GPLv3 获得许可。这意味着您不允许将 Qt 图表与您的应用静态链接。它还防止该模块用于专有软件。\n\nTo learn more about the GNU licenses, please head over to the following link: [https://www.gnu.org/licenses/gpl-faq.html.](https://www.gnu.org/licenses/gpl-faq.html)\n\n我们开始吧！\n\n# Qt 中图表的类型\n\nQt 支持最常用的图表，甚至允许开发人员定制它们的外观和感觉，以便它们可以用于许多不同的目的。Qt 图表模块提供以下图表类型:\n\n*   折线图和样条线图\n*   条形图\n*   饼图\n*   极坐标图\n*   面积图和散点图\n*   盒须图\n*   烛台图表\n\n# 折线图和样条线图\n\n第一种类型的图表是**线和样条图表**。这些图表通常显示为一系列由线条连接的点/标记。在折线图中，点用直线连接，以显示变量在一段时间内的变化。另一方面，样条曲线图表与折线图非常相似，除了点由样条曲线/曲线而不是直线连接:\n\n![](img/629caf81-65dc-4ade-bca7-c83446b9563a.png)\n\n# 条形图\n\n**条形图**是除折线图和饼图之外最常用的图表之一。条形图与折线图非常相似，只是它没有沿着轴连接数据。相反，条形图使用单个矩形显示其数据，其高度由数据值决定。这意味着该值越高，矩形将变得越高:\n\n![](img/da0e850e-1370-4f92-9b2f-59d6ff87010f.png)\n\n# 饼图\n\nA **饼图**，顾名思义，就是一种看起来像饼图的图表类型。饼图以饼图切片的形式显示其数据。每块饼图的大小将由其值相对于其余数据的总百分比决定。因此，饼图通常用于显示分数、比率、百分比或一组数据的一部分:\n\n![](img/bef47cfa-ec84-4d74-9a31-7fccac977da3.jpg)\n\n有时，饼图也可以显示为圆环形状(也称为圆环图):\n\n![](img/21decb70-9994-4aa5-9201-0d617a5577f0.png)\n\n# 极坐标图\n\n**极坐标图**以圆形图的形式呈现数据，其中数据的放置基于角度和与图形中心的距离，这意味着数据值越高，点离图表中心越远。您可以在极坐标图中显示多种类型的图形，如直线、样条线、面积和散点图，以可视化数据:\n\n![](img/12341292-4158-439e-a319-746511e60aab.png)\n\n如果你是一个游戏玩家，你应该已经注意到这种类型的图形在一些视频游戏中用来显示游戏角色的属性:\n\n![](img/388b962c-afac-421e-afe0-076d47706e35.png)\n\n# 面积图和散点图\n\n**面积图**将其数据显示为一个面积或形状来表示体积。它通常用于比较两个或多个数据集之间的差异。\n\n![](img/fb091f5c-a7aa-4329-9faf-40f1f7e1ead0.png)\n\n**散点图**则用于显示一组数据点，并用于显示两个或多个数据集之间的非线性关系。\n\n![](img/e2ce41e1-47a7-4fdf-832a-df1bc9b62b47.png)\n\n# 盒须图\n\n**箱形和须状图**将数据表示为四分位数，用须状图进行扩展，以显示数值的可变性。盒子上可能有垂直延伸的线，叫做*须*。这些线表示上四分位数和下四分位数之外的可变性，这些线或胡须之外的任何点都被认为是异常值。盒须图最常用于统计分析，如股票市场分析:\n\n![](img/bd88e23e-128d-4f49-828e-6e548e6f83ce.png)\n\n# 烛台图表\n\n**烛台图**在视觉上与盒须图非常相似，只是用来表示开盘价和收盘价之间的差异，同时通过不同的颜色显示数值的方向(无论是增加还是减少)。如果特定数据段的值保持不变，矩形将根本不会显示:\n\n![](img/1b4c98ab-bbee-4f7f-8168-c054e28be15d.png)\n\nFor more information regarding the different types of charts supported by Qt, please head over to the following link: [https://doc.qt.io/qt-5/qtcharts-overview.html.](https://doc.qt.io/qt-5/qtcharts-overview.html)\n\nQt 支持项目所需的大多数图表类型。在 Qt 中实现这些图表也非常容易。让我们看看我们怎么做！\n\n# 实现图表\n\nQt 通过将复杂的绘制算法放在不同的抽象层后面，并为我们提供一组类和函数，使绘制不同类型的图表变得容易，这些类和函数可以用来轻松创建这些图表，而无需知道绘制算法在幕后是如何工作的。这些类和函数都包含在 Qt 附带的图表模块中。\n\n让我们创建一个新的 Qt Widgets 应用项目，并尝试在 Qt 中创建我们的第一个图表。\n\n创建新项目后，打开项目文件(`.pro`)并将`charts`模块添加到您的项目中，如下所示:\n\n```cpp\nQT += core gui charts \n```\n\n然后，打开`mainwindow.h`并添加以下内容，以包括使用`charts`模块所需的头文件:\n\n```cpp\n#include <QtCharts> \n#include <QChartView> \n#include <QBarSet> \n#include <QBarSeries> \n```\n\n`QtCharts`和`QtChartView`头对于 Qt 的`charts`模块都是必不可少的。任何类型的图表都必须包含这两者才能工作。另外两个标题，即`QBarSet`和`QBarSeries`，在这里使用，因为我们要创建一个条形图。根据要创建的图表类型，项目中包含的标题会有所不同。\n\n接下来，打开`mainwindow.ui`并将垂直布局或水平布局拖动到中心小部件。然后，选择中心小部件并单击水平布局或垂直布局。布局方向不是特别重要，因为我们将只在这里创建一个图表:\n\n![](img/4e4f032b-86fb-4548-a497-f60076f9a6d3.png)\n\n然后，右键单击刚刚拖动到中心小部件的布局小部件，并选择变形到| QFrame。这将把布局小部件更改为 QFrame 小部件，同时仍然保持其布局属性。如果您从 Widget Box 创建一个 QFrame，它将没有我们需要的布局属性。这一步很重要，以便我们以后可以将其设置为图表的父级:\n\n![](img/9ef83d89-2839-43c4-9537-cb34557dddec.png)\n\n现在打开`mainwindow.cpp`并添加以下代码:\n\n```cpp\nMainWindow::MainWindow(QWidget *parent) : \n   QMainWindow(parent), \n   ui(new Ui::MainWindow) \n{ \n   ui->setupUi(this); \n\n   QBarSet *set0 = new QBarSet(\"Jane\"); \n   QBarSet *set1 = new QBarSet(\"John\"); \n   QBarSet *set2 = new QBarSet(\"Axel\"); \n   QBarSet *set3 = new QBarSet(\"Mary\"); \n   QBarSet *set4 = new QBarSet(\"Samantha\"); \n\n   *set0 << 10 << 20 << 30 << 40 << 50 << 60; \n   *set1 << 50 << 70 << 40 << 45 << 80 << 70; \n   *set2 << 30 << 50 << 80 << 13 << 80 << 50; \n   *set3 << 50 << 60 << 70 << 30 << 40 << 25; \n   *set4 << 90 << 70 << 50 << 30 << 16 << 42; \n\n   QBarSeries *series = new QBarSeries(); \n   series->append(set0); \n   series->append(set1); \n   series->append(set2); \n   series->append(set3); \n   series->append(set4); \n} \n```\n\n上面的代码初始化了将在条形图中显示的所有类别。然后，我们还向每个类别添加了六个不同的数据项，这些数据项稍后将以条形/矩形的形式表示。\n\n`QBarSet`类代表条形图中的一组条。它将几个小节组合成一个小节集，然后可以对其进行标记。另一方面，QBarSeries 表示按类别分组的一系列条形。换句话说，具有相同颜色的条属于相同的系列。\n\n接下来，初始化`QChart`对象，并向其添加序列。我们还设置了图表的标题并启用了动画:\n\n```cpp\nQChart *chart = new QChart(); \nchart->addSeries(series); \nchart->setTitle(\"Student Performance\"); \nchart->setAnimationOptions(QChart::SeriesAnimations); \n```\n\n之后，我们创建一个条形图类别轴，并将其应用于条形图的 *x* 轴。我们使用了一个`QStringList`变量，它类似于一个数组，但是明确用于存储字符串。然后`QBarCategoryAxis`将获取字符串列表，并将其填充到 *x* 轴上:\n\n```cpp\nQStringList categories; \ncategories << \"Jan\" << \"Feb\" << \"Mar\" << \"Apr\" << \"May\" << \"Jun\"; \nQBarCategoryAxis *axis = new QBarCategoryAxis(); \naxis->append(categories); \nchart->createDefaultAxes(); \nchart->setAxisX(axis, series); \n```\n\n然后，我们为 Qt 创建一个图表视图来呈现条形图，并将其设置为主窗口中框架小部件的子窗口；否则，它不会呈现在主窗口上:\n\n```cpp\nQChartView *chartView = new QChartView(chart); \nchartView->setParent(ui->verticalFrame); \n```\n\n单击 Qt 创建器中的运行按钮，您应该会看到如下内容:\n\n![](img/ca8c434b-348e-442c-83fc-d763be3e71c3.png)\n\n接下来，让我们做一个饼图；真的很简单。首先，我们不包括`QBarSet`和`QBarSeries`，而是包括`QPieSeries`和`QPieSlice`:\n\n```cpp\n#include <QPieSeries> \n#include <QPieSlice> \n```\n\n然后，创建一个`QPieSeries`对象，设置每个数据的名称和值。之后，将其中一个切片设置为不同的视觉样式，并使其从其余切片中弹出。然后，创建一个`QChart`对象，并将其与我们已经创建的`QPieSeries`对象链接起来:\n\n```cpp\nQPieSeries *series = new QPieSeries(); \nseries->append(\"Jane\", 10); \nseries->append(\"Joe\", 20); \nseries->append(\"Andy\", 30); \nseries->append(\"Barbara\", 40); \nseries->append(\"Jason\", 50); \n\nQPieSlice *slice = series->slices().at(1); \nslice->setExploded(); // Explode this chart \nslice->setLabelVisible(); // Make label visible \nslice->setPen(QPen(Qt::darkGreen, 2)); // Set line color \nslice->setBrush(Qt::green); // Set slice color \n\nQChart *chart = new QChart(); \nchart->addSeries(series); \nchart->setTitle(\"Students Performance\"); \n```\n\n最后，同样重要的是，创建`QChartView`对象，并将其与我们刚刚创建的`QChart`对象链接。然后，将其设置为框架小部件的子部件，我们就可以开始了！\n\n```cpp\nQChartView *chartView = new QChartView(chart);\nchartView->setParent(ui->verticalFrame);\n```\n\n现在按下“运行”按钮，您应该可以看到如下内容:\n\n![](img/a5355056-5e99-4777-804c-117005d6848d.png)\n\nFor more examples of how to create different charts in Qt, please check out their sample code at the following link: [https://doc.qt.io/qt-5/qtcharts-examples.html](https://doc.qt.io/qt-5/qtcharts-examples.html).\n\n既然我们已经看到用 Qt 创建图形和图表很容易，那么让我们扩展我们在前面章节中开始的项目，并为它创建一个仪表板！\n\n# 创建仪表板页面\n\n在前一章中，我们创建了一个功能性登录页面，允许用户使用他们的用户名和密码登录。我们接下来需要做的是创建仪表板页面，用户成功登录后会自动进入该页面。\n\n仪表板页面通常为用户提供有关其公司、业务、项目、资产和/或其他统计数据状态的快速概览。下图显示了仪表板页面的示例:\n\n![](img/00d8ae97-eb16-42e8-87b2-e6fc98288a8a.jpg)\n\n如您所见，仪表板页面上使用了大量图表和图形，因为这是显示大量数据而不会让用户感到不知所措的最佳方式。此外，图表可以让用户轻松了解整体情况，而无需过多挖掘细节。\n\n让我们打开之前的项目，打开`mainwindow.ui`文件。用户界面应该如下所示:\n\n![](img/d94821f7-5f65-4794-824b-f819318c9b22.png)\n\n如您所见，我们现在已经有了登录页面，但是我们还需要为仪表板添加另一个页面。为了让多个页面在同一个程序中共存，并且能够随时在不同的页面之间切换，Qt 为我们提供了一种叫做**qstackedwiget**的东西。\n\n堆叠的小部件就像一本书，你可以添加越来越多的页面，但它一次只显示一页。每个页面都是一个完全不同的图形用户界面，所以它不会干扰堆叠小部件中的其他页面。\n\n由于上一个登录页面并没有考虑堆叠的小部件，我们必须对它进行一些调整。首先，将堆叠小部件从小部件框拖放到应用的中央小部件，然后，我们需要将中央小部件下的所有内容移动到堆叠小部件的第一页，我们将其重命名为 loginPage:\n\n![](img/4e44e2d8-9594-4cea-88c6-970b2e7fb0b7.png)\n\n接下来，将中心小部件的所有布局设置设置为`0`，使其完全不包含边距，如下所示:\n\n![](img/4870550e-3423-44cc-9e59-782f31959dd6.png)\n\n之后，我们必须删除中心小部件的样式表属性中的代码，并将其粘贴到登录页面的样式表属性中。换句话说，背景图像、按钮样式和其他视觉设置现在仅应用于登录页面。\n\n完成后，在堆叠小部件的页面之间切换时，您应该会得到两个完全不同的图形用户界面(仪表板页面现在是空的):\n\n![](img/8c047da2-209c-4102-94c5-ac0e8ec76c60.png)\n\n接下来，将网格布局拖放到仪表板页面，并将“垂直布局”应用到仪表板页面:\n\n![](img/ed2ed650-4139-46e9-99f8-f2375414f6f1.png)\n\n之后，将六个垂直布局拖放到网格布局中，如下所示:\n\n![](img/cc848c1e-1aa3-4a26-a6e1-966834507a66.png)\n\n然后，选择我们刚刚添加到网格布局中的每个垂直布局，并将其转换为 QFrame:\n\n![](img/d5761610-be0c-4854-a859-603a27326ffc.png)\n\n就像我们在图表实现示例中所做的那样，我们必须将布局变成一个`QFrame`(或`QWidget`)以便我们可以将图表作为子对象附加在上面。如果您直接从小部件框中拖动`QFrame`并且不使用变形，`QFrame`对象没有布局属性，因此图表可能不会调整自身大小以适合`QFrame`的几何图形。另外，将那些`QFrame`对象命名为`chart1`到`chart6`，因为我们将在下面的步骤中需要它们。完成后，让我们继续编写代码。\n\n首先，打开您的项目(`.pro`)文件并添加`charts`模块，就像我们在本章前面的示例中所做的那样。然后，打开`mainwindow.h`并包括所有需要的标题。这一次，我们还包括用于创建折线图的`QLineSeries`标题:\n\n```cpp\n#include <QtCharts> \n#include <QChartView> \n\n#include <QBarSet> \n#include <QBarSeries> \n\n#include <QPieSeries> \n#include <QPieSlice> \n\n#include <QLineSeries> \n```\n\n之后，声明图表的指针，如下所示:\n\n```cpp\nQChartView *chartViewBar; \nQChartView *chartViewPie; \nQChartView *chartViewLine; \n```\n\n然后，我们将添加创建条形图的代码。这与我们之前在图表实现示例中创建的条形图相同，只是它现在附加到了名为`chart1`的`QFrame`对象，并被设置为在渲染时启用*抗锯齿*。抗锯齿功能可消除所有图表的锯齿边缘，从而使渲染看起来更平滑:\n\n```cpp\nMainWindow::MainWindow(QWidget *parent) : \n   QMainWindow(parent), \n   ui(new Ui::MainWindow) \n{ \n   ui->setupUi(this); \n\n   ////////BAR CHART///////////// \n   QBarSet *set0 = new QBarSet(\"Jane\"); \n   QBarSet *set1 = new QBarSet(\"John\"); \n   QBarSet *set2 = new QBarSet(\"Axel\"); \n   QBarSet *set3 = new QBarSet(\"Mary\"); \n   QBarSet *set4 = new QBarSet(\"Samantha\"); \n\n   *set0 << 10 << 20 << 30 << 40 << 50 << 60; \n   *set1 << 50 << 70 << 40 << 45 << 80 << 70; \n   *set2 << 30 << 50 << 80 << 13 << 80 << 50; \n   *set3 << 50 << 60 << 70 << 30 << 40 << 25; \n   *set4 << 90 << 70 << 50 << 30 << 16 << 42; \n\n   QBarSeries *seriesBar = new QBarSeries(); \n   seriesBar->append(set0); \n   seriesBar->append(set1); \n   seriesBar->append(set2); \n   seriesBar->append(set3); \n   seriesBar->append(set4); \n\n   QChart *chartBar = new QChart(); \n   chartBar->addSeries(seriesBar); \n   chartBar->setTitle(\"Students Performance\"); \n   chartBar->setAnimationOptions(QChart::SeriesAnimations); \n\n   QStringList categories; \n   categories << \"Jan\" << \"Feb\" << \"Mar\" << \"Apr\" << \"May\" << \"Jun\"; \n   QBarCategoryAxis *axis = new QBarCategoryAxis(); \n   axis->append(categories); \n   chartBar->createDefaultAxes(); \n   chartBar->setAxisX(axis, seriesBar); \n\n   chartViewBar = new QChartView(chartBar); \n   chartViewBar->setRenderHint(QPainter::Antialiasing); \n   chartViewBar->setParent(ui->chart1); \n} \n```\n\n接下来，我们还添加了饼图的代码。同样，这也是上一个示例中的饼图:\n\n```cpp\nQPieSeries *seriesPie = new QPieSeries(); \nseriesPie->append(\"Jane\", 10); \nseriesPie->append(\"Joe\", 20); \nseriesPie->append(\"Andy\", 30); \nseriesPie->append(\"Barbara\", 40); \nseriesPie->append(\"Jason\", 50); \n\nQPieSlice *slice = seriesPie->slices().at(1); \nslice->setExploded(); \nslice->setLabelVisible(); \nslice->setPen(QPen(Qt::darkGreen, 2)); \nslice->setBrush(Qt::green); \n\nQChart *chartPie = new QChart(); \nchartPie->addSeries(seriesPie); \nchartPie->setTitle(\"Students Performance\"); \n\nchartViewPie = new QChartView(chartPie); \nchartViewPie->setRenderHint(QPainter::Antialiasing); \nchartViewPie->setParent(ui->chart2); \n```\n\n最后，我们还向仪表板添加了一个折线图，这是一个新的东西。代码非常简单，非常类似于饼图:\n\n```cpp\nQLineSeries *seriesLine = new QLineSeries(); \nseriesLine->append(0, 6); \nseriesLine->append(2, 4); \nseriesLine->append(3, 8); \nseriesLine->append(7, 4); \nseriesLine->append(10, 5); \nseriesLine->append(11, 10); \nseriesLine->append(13, 3); \nseriesLine->append(17, 6); \nseriesLine->append(18, 3); \nseriesLine->append(20, 2); \n\nQChart *chartLine = new QChart(); \nchartLine->addSeries(seriesLine); \nchartLine->createDefaultAxes(); \nchartLine->setTitle(\"Students Performance\"); \n\nchartViewLine = new QChartView(chartLine); \nchartViewLine->setRenderHint(QPainter::Antialiasing); \nchartViewLine->setParent(ui->chart3); \n```\n\n完成后，我们必须在主窗口类中添加一个调整大小事件槽，并在主窗口调整大小时使图表跟随其各自父窗口的大小。这可以通过首先进入`mainwindow.h`并在事件处理程序声明中添加:\n\n```cpp\nprotected: \n   void resizeEvent(QResizeEvent* event); \n```\n\n然后，打开`mainwindow.cpp`并添加以下代码:\n\n```cpp\nvoid MainWindow::resizeEvent(QResizeEvent* event) \n{ \n   QMainWindow::resizeEvent(event); \n\n   chartViewBar->resize(chartViewBar->parentWidget()->size()); \n   chartViewPie->resize(chartViewPie->parentWidget()->size()); \n   chartViewLine->resize(chartViewLine->parentWidget()->size()); \n} \n```\n\n请注意，必须首先调用`QMainWindow::resizeEvent(event)`，以便在调用其下的自定义方法之前触发默认行为。`resizeEvent()`是 Qt 提供的众多事件处理程序之一，用于响应其事件，如鼠标事件、窗口事件、绘画事件等。与信号和插槽机制不同，您需要替换事件处理程序的虚拟函数，使其在调用事件时执行您希望它执行的操作。\n\n如果我们现在构建并运行该项目，我们应该会得到如下结果:\n\n![](img/2a440a24-0d6a-4d47-b54b-a50aee5eaffc.png)\n\n看起来很整洁，不是吗！然而，为了简单起见，也为了不混淆读者，这些图表都是硬编码的，没有使用数据库中的任何数据。如果您打算使用数据库中的数据，请不要在程序启动期间进行任何 SQL 查询，因为如果您加载的数据非常大，或者您的服务器非常慢，这将使您的程序冻结。\n\n最好的方法是仅当您从登录页面切换到仪表板页面时(或者切换到任何其他页面时)才加载数据，这样加载时间对用户来说就不那么明显了。为此，右键单击堆叠小部件并选择转到插槽。然后，选择当前更改(整数)并单击确定。\n\n![](img/531baa65-d083-4a70-a57c-aeff6def670a.png)\n\n之后，Qt 会自动创建一个新的槽函数。当堆叠小部件在页面之间切换时，将自动调用该函数。您可以通过检查`arg1`变量来检查当前切换到哪个页面。如果目标页面是堆叠小部件中的第一个页面，则`arg1`值将为`0`，如果目标页面是第二个页面，则为`1`，依此类推。\n\n只有当堆叠小部件显示仪表板页面时，您才能提交 SQL 查询，仪表板页面是第二个页面(`arg1`等于`1`):\n\n```cpp\nvoid MainWindow::on_stackedWidget_currentChanged(int arg1) \n{ \n   if (arg1 == 1) \n   { \n      // Do it here \n   } \n} \n```\n\n唷！这一章要消化的东西太多了！希望这一章能帮助你理解如何为你的项目创建一个漂亮且信息丰富的页面。\n\n# 摘要\n\nQt 中的图表模块是特征和视觉美感的结合。它不仅易于实现，不需要编写很长的代码来显示图表，而且还可以根据您的视觉需求进行定制。我们真的需要感谢 Qt 开发者开放了这个模块，允许非商业用户免费使用！\n\n在这一章中，我们学习了如何创建一个非常漂亮的仪表板，并使用 Qt 图表模块在上面显示不同类型的图表。在下一章中，我们将学习如何使用视图小部件、对话框和文件选择对话框。"
  },
  {
    "path": "docs/cpp-gui-prog-qt5/05.md",
    "content": "# 五、项目视图和对话框\n\n在前一章中，我们学习了如何使用不同类型的图表显示数据。图表是在屏幕上向用户呈现信息的许多方式之一。对于您的应用来说，向用户呈现重要信息非常重要，这样他们就可以确切地知道应用发生了什么——数据是否已成功保存，或者应用是否正在等待用户的输入，或者用户应该知道的警告/错误消息，等等——确保应用的用户友好性和可用性非常重要。\n\n在本章中，我们将涵盖以下主题:\n\n*   使用项目视图小部件\n*   使用对话框\n*   使用文件选择对话框\n*   图像缩放和裁剪\n\nQt 为我们提供了许多类型的小部件和对话框，我们可以轻松地使用它们向用户显示重要信息。让我们看看这些小部件是什么！\n\n# 使用项目视图小部件\n\n除了使用不同类型的图表显示数据，我们还可以使用不同类型的项目视图显示这些数据。项目视图小部件通过可视化呈现数据来呈现数据，通常沿垂直轴呈现。\n\n二维项目视图，通常称为**表格视图**，显示垂直和水平方向的数据。这使得它能够在一个紧凑的空间内显示大量数据，并使用户能够非常快速和容易地搜索一个项目。\n\n在项目视图中显示数据有两种方式。最常见的方法是使用**模型-视图架构**，它使用三个不同的组件，模型、视图和委托，从数据源中检索数据并将其显示在项目视图中。这些组件都利用 Qt 提供的**信号槽架构**相互通信:\n\n*   来自模型的信号通知视图数据源所持数据的变化\n*   来自视图的信号提供了关于用户与正在显示的项目的交互的信息\n*   来自委托的信号在编辑过程中用于告诉模型和视图编辑器的状态\n\n另一种方法是手动方式，程序员必须告诉 Qt 哪些数据进入了哪一列和哪一行。这种方法比模型视图简单得多，但与它的性能相比要慢得多。但是，对于少量数据，性能问题可以忽略不计，这是一个很好的方法。\n\n如果打开 Qt Designer，您将看到项目视图小部件的两个不同类别，即项目视图(基于模型)和项目小部件(基于项目):\n\n![](img/6f63f909-cc29-4299-baf3-b34e7655cf7d.png)\n\n尽管它们看起来可能相同，但实际上这两个类别中的小部件工作方式非常不同。在本章中，我们将学习如何使用后一个类别，因为它更简单易懂，并且能够作为前一个类别的先决知识。\n\n在项目小部件(基于项目)类别下，有三种不同的小部件，称为列表小部件、树小部件和表小部件。每个小部件都以不同的方式显示数据。选择一个适合你需求的:\n\n![](img/a50b4415-472d-4c74-b1e1-f735f0a5bd21.png)\n\n从上图可以看出，**列表小部件**以一维列表的形式显示其项目，而**表格小部件**以二维表格的形式显示其项目。尽管**树小部件**的工作方式与**列表小部件**几乎相似，但它的项目以分层结构显示，其中每个项目下可以递归地有多个子项目。一个很好的例子是我们操作系统中的文件系统，它使用树小部件显示目录结构。\n\n为了说明不同之处，让我们创建一个新的 Qt Widgets 应用项目，并亲自尝试一下。\n\n# 创建我们的 Qt 小部件应用\n\n创建项目后，打开`mainwindow.ui`并将三个不同的项目部件拖到主窗口。之后，选择主窗口并单击顶部的垂直布局按钮:\n\n![](img/e0e15392-5def-4f64-accd-075c8e6d2778.png)\n\n然后，双击列表小部件，会弹出一个新窗口。在这里，您可以通过单击+图标向列表小部件添加一些虚拟项目，或者通过从列表中选择一个项目并单击-图标来删除它们。单击“确定”按钮将最终结果应用到小部件:\n\n![](img/4a126e2c-2059-4faf-a325-fafa3b81ce9d.png)\n\n您可以对树小部件执行同样的操作。它几乎与列表小部件相同，只是您可以递归地向项目添加子项。您还可以向树小部件添加列，并命名这些列:\n\n![](img/486ef112-0666-4f94-a753-eac1472c352e.png)\n\n最后，双击表格小部件打开编辑表格小部件窗口。与其他两个项目视图不同，表格小部件是一个二维项目视图，这意味着您可以像电子表格一样向其中添加列和行。通过在“列”或“行”选项卡中进行设置，可以用所需的名称标记每一列和每一行:\n\n![](img/c3e07ba3-fb5c-4017-90db-caf9edae65e1.png)\n\n使用 Qt 设计器很容易理解一个小部件是如何工作的。只需将小部件拖放到窗口中，并摆弄它的设置，然后构建并运行项目，亲自查看结果。\n\n在本例中，我们已经演示了三个项目视图小部件之间的区别，但没有编写一行代码:\n\n![](img/cb94f990-0b6d-435e-8bea-c09205c56bf3.png)\n\n# 让我们的列表小部件发挥作用\n\n然而，为了让小部件在应用中充分发挥作用，仍然需要编写代码。让我们学习如何使用 C++ 代码向我们的项目视图小部件添加项目！\n\n首先，打开`mainwindow.cpp`，在`ui->setupui(this)`之后，将下面的代码写入类构造函数:\n\n```cpp\nui->listWidget->addItem(\"My Test Item\"); \n```\n\n就这么简单，您已经成功地向列表小部件添加了一个项目！\n\n![](img/2f54393e-6a48-42c9-90c3-0d36ae463ad2.png)\n\n还有另一种方法可以将项目添加到列表小部件。但在此之前，我们必须在`mainwindow.h`中添加以下标题:\n\n```cpp\n#ifndef MAINWINDOW_H \n#define MAINWINDOW_H \n\n#include <QMainWindow> \n#include <QDebug> \n#include <QListWidgetItem> \n```\n\n`QDebug`头用于我们打印调试消息，`QListWidgetItem`头用于我们声明 List Widget `Item`对象。接下来，打开`mainwindow.cpp`并添加以下代码:\n\n```cpp\nQListWidgetItem* listItem = new QListWidgetItem; \nlistItem->setText(\"My Second Item\"); \nlistItem->setData(100, 1000); \nui->listWidget->addItem(listItem); \n```\n\n前面的代码与前面的单行代码相同。只不过，这一次，我在项目中添加了额外的数据。`setData()`函数接受两个输入变量——第一个变量是项目的数据角色，它指示 Qt 应该如何处理它。如果放入与`Qt::ItemDataRole`枚举器匹配的值，数据会影响显示、装饰、工具提示等，这可能会改变其外观。\n\n在我的情况下，我只是简单地设置一个与`Qt::ItemDataRole`中的任何一个枚举器都不匹配的数字，这样我就可以将其存储为隐藏数据供以后使用。要检索数据，您只需拨打`data()`并插入与您刚刚设置的号码相匹配的号码:\n\n```cpp\nqDebug() << listItem->data(100); \n```\n\n构建和运行项目；您应该能够看到新项目现在被添加到列表小部件:\n\n![](img/bf13b2e0-4637-4f30-a7c4-2c79b541baad.png)\n\nFor more information about `Qt::ItemDataRole` enumerators, please check out the following link: [http://doc.qt.io/qt-5/qt.html#ItemDataRole-enum](http://doc.qt.io/qt-5/qt.html#ItemDataRole-enum)\n\n如前所述，隐藏的数据可以附加到列表项上以备后用。例如，您可以使用列表小部件来显示用户准备购买的产品列表。这些项目中的每一个都可以附加其产品标识，以便当用户选择项目并将其放在购物车上时，您的系统可以通过识别存储为数据角色的产品标识来自动识别哪个产品已添加到购物车中。\n\n在前面的示例中，我在列表项中存储了自定义数据`1000`，并将其数据角色设置为`100`，这与任何`Qt::ItemDataRole`枚举器都不匹配。这样，数据就不会显示给用户，因此只能通过 C++ 代码来检索。\n\n# 向树小部件添加功能\n\n接下来，让我们继续看树小部件。它实际上与列表小部件没有什么不同。让我们看看下面的代码:\n\n```cpp\nQTreeWidgetItem* treeItem = new QTreeWidgetItem; \ntreeItem->setText(0, \"My Test Item\"); \nui->treeWidget->addTopLevelItem(treeItem); \n```\n\n和列表小部件差不多，只是我们要在`setText()`函数中设置列 ID。这是因为树小部件介于列表小部件和表小部件之间，它可以有多个列，但不能有任何行。\n\n树小部件和其他视图小部件之间最明显的区别是，它的所有项目都可以递归地包含子项目。让我们看看下面的代码，看看如何在树小部件中将子项目添加到现有项目中:\n\n```cpp\nQTreeWidgetItem* treeItem2 = new QTreeWidgetItem; \ntreeItem2->setText(0, \"My Test Subitem\"); \ntreeItem->addChild(treeItem2); \n```\n\n真的那么简单！最终结果如下:\n\n![](img/3580d596-4c97-4c34-9699-f54ddf816393.png)\n\n# 最后，我们的表格小部件\n\n接下来，让我们对表小部件做同样的事情。从技术上讲，这些项目已经存在，并且在创建列和行时保留在表小部件中。我们需要做的是创建一个新的项，并用位于特定列和行的(当前为空的)项替换它，这就是为什么函数名被称为`setItem()`，而不是 List Widget 使用的`addItem()`。\n\n让我们看看代码:\n\n```cpp\nQTableWidgetItem* tableItem = new QTableWidgetItem; \ntableItem->setText(\"Testing1\"); \nui->tableWidget->setItem(0, 0, tableItem); \n\nQTableWidgetItem* tableItem2 = new QTableWidgetItem; \ntableItem2->setText(\"Testing2\"); \nui->tableWidget->setItem(1, 2, tableItem2); \n```\n\n从代码中可以看出，我在两个不同的位置添加了两段数据，这转化为以下结果:\n\n![](img/05cecb5e-908c-4668-8afa-dde23dae413d.png)\n\n就这样！在 Qt 中使用项目视图显示数据非常简单。如果您正在寻找更多与项目视图相关的示例，请访问以下链接:[http://doc.qt.io/qt-5/examples-itemviews.html](http://doc.qt.io/qt-5/examples-itemviews.html)\n\n# 使用对话框\n\n创建用户友好型应用的一个非常重要的方面是，当某个事件(有意或无意)发生时，能够显示关于应用状态的重要信息。为了显示这样的信息，我们需要一个外部窗口，一旦用户确认了信息，他/她就可以关闭该窗口。\n\nQt 自带这个功能，并且都驻留在`QMessageBox`类中。Qt 中可以使用几种类型的消息框；最基本的一个只使用一行代码，如下所示:\n\n```cpp\nQMessageBox::information(this, \"Alert\", \"Just to let you know, something happened!\"); \n```\n\n这个函数需要提供三个参数。第一个是消息框的父窗口，我们将它设置为主窗口。第二个参数是窗口标题，第三个参数是我们想要传递给用户的消息。前面的代码将产生以下结果:\n\n![](img/2b92e3f2-4363-46db-aa12-afc721e665e8.png)\n\n此处显示的外观运行在 Windows 系统上。在不同的操作系统(Linux、macOS 等)上，外观可能会有所不同。如您所见，该对话框甚至带有位于文本前面的图标。有几种类型的图标可以使用，如信息、警告和关键。下面的代码向您展示了用图标调用所有不同消息框的代码:\n\n```cpp\nQMessageBox::question(this, \"Alert\", \"Just to let you know, something happened!\"); \nQMessageBox::warning(this, \"Alert\", \"Just to let you know, something happened!\"); \nQMessageBox::information(this, \"Alert\", \"Just to let you know, something happened!\"); \nQMessageBox::critical(this, \"Alert\", \"Just to let you know, something happened!\"); \n```\n\n上述代码会产生以下结果:\n\n![](img/84024277-2f97-4651-b89e-b3a4e9528f8a.png)\n\n如果不需要任何图标，只需要调用`QMessageBox::about()`函数即可。您也可以通过从 Qt 提供的标准按钮列表中选择来设置您想要的按钮，例如:\n\n```cpp\nQMessageBox::question(this, \"Serious Question\", \"Am I an awesome guy?\", QMessageBox::Ignore, QMessageBox::Yes); \n```\n\n前面的代码将产生以下结果:\n\n![](img/e95537ee-6b14-4da6-9e38-af1eaaf6fda3.png)\n\n因为这些是 Qt 提供的内置功能，可以轻松创建消息框，所以它没有给开发人员完全定制消息框的自由。但是，Qt 确实允许您使用另一种方法手动创建消息框，这种方法比内置方法更具可定制性。它需要多几行代码，但编写起来仍然非常简单:\n\n```cpp\nQMessageBox msgBox; \nmsgBox.setWindowTitle(\"Alert\"); \nmsgBox.setText(\"Just to let you know, something happened!\"); \nmsgBox.exec(); \n```\n\n前面的代码将产生以下结果:\n\n![](img/f79a9076-7112-4fa4-bed4-5bc3b9bc5628.png)\n\n*看起来都一样*，你是在告诉我。添加我们自己的图标和自定义按钮怎么样？没问题:\n\n```cpp\nQMessageBox msgBox; \nmsgBox.setWindowTitle(\"Serious Question\"); \nmsgBox.setText(\"Am I an awesome guy?\"); \nmsgBox.addButton(\"Seriously Yes!\", QMessageBox::YesRole); \nmsgBox.addButton(\"Well no thanks\", QMessageBox::NoRole); \nmsgBox.setIcon(QMessageBox::Question); \nmsgBox.exec(); \n```\n\n上述代码产生以下结果:\n\n![](img/04e2e8f6-5139-4bfb-aced-564cedaf5d2d.png)\n\n在前面的代码示例中，我已经加载了 Qt 附带的问题图标，但是如果您打算这样做，也可以从资源文件中加载您自己的图标:\n\n```cpp\nQMessageBox msgBox; \nmsgBox.setWindowTitle(\"Serious Question\"); \nmsgBox.setText(\"Am I an awesome guy?\"); \nmsgBox.addButton(\"Seriously Yes!\", QMessageBox::YesRole); \nmsgBox.addButton(\"Well no thanks\", QMessageBox::NoRole); \nQPixmap myIcon(img/icon.png\"); \nmsgBox.setIconPixmap(myIcon); \nmsgBox.exec(); \n```\n\n现在构建并运行项目，您应该能够看到这个神奇的消息框:\n\n![](img/ce170a25-75c4-448e-ab26-b82691eda029.png)\n\n了解了如何创建自己的消息框后，让我们继续学习消息框附带的事件系统。\n\n当用户看到一个带有多个不同选项的消息框时，当他/她按下不同的按钮时，他/她会期望应用做出不同的反应。\n\n例如，当弹出一个消息框询问用户是否希望退出程序时，按钮“是”将使程序终止，而“否”按钮将什么也不做。\n\nQt 的`QMessageBox`类为我们提供了一个检查按钮事件的简单解决方案。当消息框被创建时，Qt 将等待用户选择；然后，它将返回被触发的按钮。通过检查正在点击哪个按钮，开发人员可以继续触发相关事件。让我们看一下示例代码:\n\n```cpp\nif (QMessageBox::question(this, \"Question\", \"Some random question. Yes or no?\") == QMessageBox::Yes) \n{ \n   QMessageBox::warning(this, \"Yes\", \"You have pressed Yes!\"); \n} \nelse \n{ \n   QMessageBox::warning(this, \"No\", \"You have pressed No!\"); \n} \n```\n\n前面的代码将产生以下结果:\n\n![](img/4ec73206-502c-4051-aa2a-ba175f839f16.png)\n\n如果您更喜欢手动方式创建消息框，检查按钮事件的代码会稍长一些:\n\n```cpp\nQMessageBox msgBox; \nmsgBox.setWindowTitle(\"Serious Question\"); \nmsgBox.setText(\"Am I an awesome guy?\"); \nQPushButton* yesButton = msgBox.addButton(\"Seriously Yes!\", QMessageBox::YesRole); \nQPushButton* noButton = msgBox.addButton(\"Well no thanks\", QMessageBox::NoRole); \nmsgBox.setIcon(QMessageBox::Question); \nmsgBox.exec(); \n\nif (msgBox.clickedButton() == (QAbstractButton*) yesButton) \n{ \n   QMessageBox::warning(this, \"Yes\", \"Oh thanks! :)\"); \n} \nelse if (msgBox.clickedButton() == (QAbstractButton*) noButton) \n{ \n   QMessageBox::warning(this, \"No\", \"Oh why... :(\"); \n} \n```\n\n即使代码稍微长一点，基本概念也差不多——被点击的按钮总是能够被开发人员检索到以触发适当的动作。然而，这一次，Qt 没有检查枚举器，而是直接检查按钮指针，因为前面的代码没有使用来自`QMessageBox`类的内置标准按钮。\n\n构建项目，您应该能够获得以下结果:\n\n![](img/9c49eca5-6076-4609-9e1c-e3f2e7a1a762.png)\n\nFor more information regarding the dialog boxes, please visit the API documents located at the following link: [http://doc.qt.io/qt-5/qdialog.html](http://doc.qt.io/qt-5/qdialog.html)\n\n# 创建文件选择对话框\n\n既然我们已经讨论了关于消息框的主题，那么我们也来了解一下另一种类型的对话框——文件选择对话框。文件选择对话框也非常有用，尤其是如果您的应用经常处理文件。要求用户键入他们想要打开的文件的绝对路径是极其不愉快的，因此在这种情况下，“文件选择对话框”非常方便。\n\nQt 为我们提供了一个内置的文件选择对话框，它看起来和我们在操作系统中看到的完全一样，因此，用户不会感到陌生。文件选择对话框本质上只做一件事——它允许用户选择他们想要的文件或文件夹，并返回所选文件或文件夹的路径；仅此而已。实际上，它并不负责打开文件并读取其内容。\n\n让我们看看如何触发文件选择对话框。首先，打开`mainwindow.h`并添加以下头文件:\n\n```cpp\n#ifndef MAINWINDOW_H \n#define MAINWINDOW_H \n\n#include <QMainWindow> \n#include <QFileDialog> \n#include <QDebug> \n```\n\n接下来，打开`mainwindow.cpp`并插入以下代码:\n\n```cpp\nQString fileName = QFileDialog::getOpenFileName(this); \nqDebug() << fileName; \n```\n\n就这么简单！现在构建并运行项目，您应该会得到:\n\n![](img/cb0d3f1e-1e61-4d02-b280-9a4935ead609.png)\n\n如果用户选择了一个文件并按下打开，则`fileName`变量将被所选文件的绝对路径填充。如果用户点击取消按钮，`fileName`变量将为空字符串。\n\n文件选择对话框还包含几个可以在初始化步骤中设置的选项。例如:\n\n```cpp\nQString fileName = QFileDialog::getOpenFileName(this, \"Your title\", QDir::currentPath(), \"All files (*.*) ;; Document files (*.doc *.rtf);; PNG files (*.png)\"); \nqDebug() << fileName; \n```\n\n在前面的代码中，我们设置了三项内容，如下所示:\n\n*   文件选择对话框的窗口标题\n*   用户在创建对话框时看到的默认路径\n*   文件类型过滤器\n\n当您只允许用户选择特定类型的文件(例如，只有 JPEG 图像文件)并隐藏其余文件时，文件类型过滤器非常方便。除了`getOpenFileName()`之外，还可以使用`getSaveFileName()`，这将允许用户指定一个尚不存在的文件名。\n\nFor more information regarding the File Selection Dialog, please visit the API documents located at the following link: [http://doc.qt.io/qt-5/qfiledialog.html](http://doc.qt.io/qt-5/qfiledialog.html)\n\n# 图像缩放和裁剪\n\n自从我们在上一节学习了文件选择对话框，我想这次我们应该学点有趣的东西！\n\n首先，让我们创建一个新的 Qt 小部件应用。然后，打开`mainwindow.ui`，创建如下用户界面:\n\n![](img/867c3332-9f87-40c0-933b-05190c15dd8e.png)\n\n让我们把这个用户界面分成三个部分:\n\n*   顶部—图像预览:\n    *   首先，向窗口添加水平布局。\n    *   然后，在我们刚刚添加的水平布局中添加一个标签小部件，然后将文本属性设置为`empty`。将标签的最小大小和最大大小属性都设置为 150x150。最后，将 QFrame 类别下的 frameShape 属性设置为 Box。\n    *   将两个水平间隔物添加到标签的侧面，使其居中。\n*   中间—用于调整的滑块:\n    *   在我们之前在步骤 1 中添加的水平布局下方，向窗口添加一个表单布局。\n    *   在表单布局中添加三个标签，并将它们的文本属性分别设置为`Scale:`、`Horizontal:`和`Vertical:`。\n    *   向表单布局添加三个水平滑块。将最小属性设置为`1`，最大属性设置为`100`。然后，将 pageStep 属性设置为`1`。\n    *   将比例滑块的值属性设置为`100`。\n*   底部—浏览按钮和保存按钮:\n    *   将水平布局添加到窗口中，在我们之前在步骤 2 中添加的表单布局下方。\n    *   向水平布局添加两个按钮，并分别将其文本属性设置为`Browse`和`Save`。\n\n现在我们已经创建了用户界面，让我们深入研究编码吧！首先，打开`mainwindow.h`并添加以下标题:\n\n```cpp\n#ifndef MAINWINDOW_H \n#define MAINWINDOW_H \n\n#include <QMainWindow> \n#include <QMessageBox> \n#include <QFileDialog> \n#include <QPainter> \n```\n\n之后，在`mainwindow.h`中加入以下变量:\n\n```cpp\nprivate: \n   Ui::MainWindow *ui; \n   bool canDraw; \n   QPixmap* pix; \n   QSize imageSize; \n   QSize drawSize; \n   QPoint drawPos; \n```\n\n然后，返回`mainwindow.ui`并右键单击浏览按钮，然后选择转到插槽。然后，会弹出一个窗口，要求您选择一个信号。选择列表顶部的`clicked()`信号，然后按确定按钮:\n\n![](img/e63f85a0-57c5-4097-a3aa-ddba2adc9e17.png)\n\n一个新的`slot`功能将自动添加到你的源文件中。现在，添加以下代码，在单击浏览按钮时打开文件选择对话框。该对话框仅列出 JPEG 图像，并隐藏其他文件:\n\n```cpp\nvoid MainWindow::on_browseButton_clicked() \n{ \n   QString fileName = QFileDialog::getOpenFileName(this, tr(\"Open   \n   Image\"), QDir::currentPath(), tr(\"Image Files (*.jpg *.jpeg)\")); \n\n   if (!fileName.isEmpty()) \n   { \n         QPixmap* newPix = new QPixmap(fileName); \n\n         if (!newPix->isNull()) \n         { \n               if (newPix->width() < 150 || newPix->height() < 150) \n               { \n                     QMessageBox::warning(this, tr(\"Invalid Size\"), \n                     tr(\"Image size too small. Please use an image  \n                     larger than 150x150.\")); \n                     return; \n               } \n\n               pix = newPix; \n               imageSize = pix->size(); \n               drawSize = pix->size(); \n\n               canDraw = true; \n\n         } \n         else \n         { \n               canDraw = false; \n\n               QMessageBox::warning(this, tr(\"Invalid Image\"), \n               tr(\"Invalid or corrupted file. Please try again with  \n               another image file.\")); \n         } \n   } \n} \n```\n\n如您所见，代码检查用户是否选择了任何图像。如果它再次检查，看看图像分辨率是否至少为 150 x 150。如果没有发现问题，我们会将图像的像素映射保存到一个名为`pix`的指针，然后将图像大小保存到`imageSize`变量，将初始绘图大小保存到`drawSize`变量。最后，我们将`canDraw`变量设置为`true`。\n\n之后，再次打开`mainwindow.h`并声明这两个功能:\n\n```cpp\npublic: \n   explicit MainWindow(QWidget *parent = 0); \n   ~MainWindow(); \n   virtual void paintEvent(QPaintEvent *event); \n   void paintImage(QString fileName, int x, int y); \n```\n\n第一个函数`paintEvent()`是一个虚拟函数，每当 Qt 需要刷新用户界面时，比如当主窗口正在调整大小时，它就会自动被调用。我们将覆盖这个函数，并将新加载的图像绘制到图像预览小部件上。在这种情况下，我们将在`paintEvent()`虚拟函数中调用`paintImage()`函数:\n\n```cpp\nvoid MainWindow::paintEvent(QPaintEvent *event) \n{ \n   if (canDraw) \n   { \n         paintImage(\"\", ui->productImage->pos().x(), ui->productImage-\n         >pos().y()); \n   } \n} \n```\n\n之后，我们将在`mainwindow.cpp`中编写`paintImage()`函数:\n\n```cpp\nvoid MainWindow::paintImage(QString fileName, int x, int y) \n{ \n   QPainter painter; \n   QImage saveImage(150, 150, QImage::Format_RGB16); \n\n   if (!fileName.isEmpty()) \n   { \n         painter.begin(&saveImage); \n   } \n   else \n   { \n         painter.begin(this); \n   } \n\n   if (!pix->isNull()) \n   { \n         painter.setClipRect(x, y, 150, 150); \n         painter.fillRect(QRect(x, y, 150, 150), Qt::SolidPattern); \n         painter.drawPixmap(x - drawPos.x(), y - drawPos.y(), \n         drawSize.width(), drawSize.height(), *pix); \n   } \n\n   painter.end(); \n\n   if (fileName != \"\") \n   { \n         saveImage.save(fileName); \n         QMessageBox::information(this, \"Success\", \"Image has been \n         successfully saved!\"); \n   } \n} \n```\n\n这个函数做两件事——如果我们不设置`fileName`变量，它将继续在图像预览小部件的顶部绘制图像，否则，它将根据图像预览小部件的尺寸裁剪图像，并将其保存到跟随`fileName`变量的磁盘中。\n\n当点击保存按钮时，我们将再次调用这个函数。这一次，我们将`fileName`变量设置为所需的目录路径和文件名，以便`QPainter`类可以正确保存图像:\n\n```cpp\nvoid MainWindow::on_saveButton_clicked() \n{ \n   if (canDraw) \n   { \n         if (!pix->isNull()) \n         { \n               // Save new pic from painter \n               paintImage(QCoreApplication::applicationDirPath() + \n               \"/image.jpg\", 0, 0); \n         } \n   } \n} \n```\n\n最后，右键单击三个滑块，并选择转到插槽。然后，选择`valueChanged(int)`并点击确定。\n\n![](img/b605edff-3b8d-4ce0-9661-b08ae23bbb5d.png)\n\n之后，我们将为上一步产生的`slot`函数编写代码:\n\n```cpp\nvoid MainWindow::on_scaleSlider_valueChanged(int value) \n{ \n   drawSize = imageSize * value / 100; \n   update(); \n} \n\nvoid MainWindow::on_leftSlider_valueChanged(int value) \n{ \n   drawPos.setX(value * drawSize.width() / 100 * 0.5); \n   update(); \n} \n\nvoid MainWindow::on_topSlider_valueChanged(int value) \n{ \n   drawPos.setY(value * drawSize.height() / 100 * 0.5); \n   update(); \n} \n```\n\n缩放滑块主要用于用户在图像预览小部件中将图像调整到所需的比例。左侧滑块供用户水平移动图像，而顶部滑块供用户垂直移动图像。通过组合这三个不同的滑块，用户可以在将图像上传到服务器之前根据自己的喜好调整和裁剪图像，或者将其用于其他目的。\n\n如果您现在构建并运行项目，您应该能够获得以下结果:\n\n![](img/2b7d67e4-2cc1-434a-87b7-8e09130c019a.png)\n\n您可以单击“浏览”按钮选择要加载的 JPG 图像文件。之后，图像应该出现在预览区域。然后，您可以移动滑块来调整裁剪大小。对结果满意后，单击保存按钮将图像保存在当前目录中。\n\n如果你想详细了解这本书，一定要查看它附带的示例代码。你可以在下面的 GitHub 页面找到源代码:[https://GitHub . com/PacktPublishing/hand-GUI-用-C-QT5 编程](https://github.com/PacktPublishing/Hands-On-GUI-Programming-with-C-QT5)\n\n# 摘要\n\n**输入输出(I/O)** 是现代计算机软件的精髓。Qt 允许我们以许多不同的方式显示我们的数据，这些方式既直观又吸引最终用户。除此之外，Qt 附带的事件系统使我们作为程序员的生活变得更加容易，因为它倾向于通过强大的信号和槽机制自动捕获用户输入，并作为响应触发自定义行为。没有 Qt，我们将很难想出如何重新发明众所周知的轮子，最终可能会创造出一个不太用户友好的产品。\n\n在这一章中，我们学习了如何利用 Qt 提供的出色功能——视图小部件、对话框和文件选择对话框，用于向用户显示重要信息。此外，我们还经历了一个有趣的小项目，该项目教我们如何使用 Qt 小部件进行用户输入来缩放和裁剪图像。在下一章中，我们将学习一些更高级(也更有趣)的东西，那就是使用 Qt 创建我们自己的网络浏览器！"
  },
  {
    "path": "docs/cpp-gui-prog-qt5/06.md",
    "content": "# 六、整合网络内容\n\n在前一章中，我们学习了如何在 Qt 中使用项目视图和对话框。在本章中，我们将学习如何将 web 内容集成到我们的 Qt 应用中。\n\n从 90 年代末和 21 世纪初的网络时代开始，我们的世界越来越多地通过互联网连接起来。自然，运行在我们计算机上的应用也在朝着这个方向发展。如今，我们的大多数(如果不是全部的话)软件都以某种方式连接到互联网，通常是为了检索有用的信息并将其显示给用户。最简单的方法是在应用的用户界面中嵌入一个网络浏览器显示(也称为网络视图)。这样，用户不仅可以查看信息，而且可以用一种审美的方式来查看。\n\n通过使用 web 视图，开发人员可以利用其渲染能力，并使用 **HTML** ( **超文本标记语言**)和 **CSS** ( **级联样式表**)的强大组合来装饰其内容。在本章中，我们将探索 Qt 的网络引擎模块，并创建我们自己的网络浏览器。\n\n在本章中，我们将涵盖以下主题:\n\n*   创建自己的网络浏览器\n*   会话、cookies 和缓存\n*   集成 JavaScript 和 C++\n\n不用多说，让我们看看如何在 Qt 中创建我们自己的网络浏览器！\n\n# 创建自己的网络浏览器\n\n曾几何时，Qt 使用一个名为 **WebKit** 的不同模块在其用户界面上呈现网页内容。但是，从 5.5 版本开始，WebKit 模块已经完全被弃用，取而代之的是一个名为 **WebEngine** 的新模块。\n\n新的 WebEngine 模块基于谷歌打造的 **Chromium** 框架，只会在 Windows 平台的 **Visual C++** 编译器上工作。因此，如果您正在运行 Windows，请确保您已经在计算机上安装了 **Microsoft Visual Studio** 以及所有与计算机上安装的 Visual Studio 版本相匹配的 Qt 的 **MSVC** 组件。除此之外，这一章还需要 Qt 网络引擎组件。如果您在 Qt 的安装过程中跳过了这些组件，您所需要做的就是再次运行相同的安装程序并将其安装在那里:\n\n![](img/02f1ed42-5efc-43a4-b0d7-40c1610c382e.png)\n\n# 添加 web 视图小部件\n\n一旦你准备好了，让我们开始吧！首先，打开 Qt Creator 并创建一个新的 Qt Widgets 应用项目。然后，打开项目(`.pro`)文件，并添加以下文本以启用模块:\n\n```cpp\nQT += core gui webengine webenginewidgets \n```\n\n如果您没有安装 MSVC 组件(在 Windows 上)或 Qt 网络引擎组件，如果您试图构建项目，此时会出现错误消息。如果是这样，请再次运行 Qt 安装程序。\n\n接下来，打开`mainwindow.h`并添加以下头文件:\n\n```cpp\n#ifndef MAINWINDOW_H \n#define MAINWINDOW_H \n\n#include <QMainWindow> \n#include <QWebEngineView> \n```\n\n之后，打开`mainwindow.h`并添加以下代码:\n\n```cpp\nprivate: \n   Ui::MainWindow *ui; \n QWebEngineView* webview; \n```\n\n然后，添加以下代码:\n\n```cpp\nMainWindow::MainWindow(QWidget *parent) : \n   QMainWindow(parent), \n   ui(new Ui::MainWindow) \n{ \n   ui->setupUi(this); \n\n   webview = new QWebEngineView(ui->centralWidget); \n   webview->load(QUrl(\"http://www.kloena.com\")); \n} \n```\n\n现在构建并运行程序，您应该会看到以下结果:\n\n![](img/d9118fa0-3227-49b7-a2bb-714e581d8784.png)\n\n其实就这么简单。您现在已经成功地在应用中放置了 web 视图！\n\n我们之所以编写 C++ 代码来创建 web 视图，是因为 Qt Creator 使用的默认 Qt Designer 在小部件框中没有 web 视图。前面的代码只是创建了`QWebEngineView`对象，设置了它的父对象(在本例中是中心小部件)，并在显示 web 视图小部件之前设置了网页的 URL。如果您想使用 Qt 设计器在您的用户界面上放置一个网络引擎视图，您必须运行位于 Qt 安装目录中的独立 Qt 设计器。比如你运行的是 Windows，它位于`C:QtQt5.10.25.10.2msvc2017_64bin`。请注意，它位于支持 web 引擎的编译器名称所在的目录中:\n\n![](img/0ff18712-2f53-4ce9-a66c-939f467147e6.png)\n\n# 为网络浏览器创建用户界面\n\n接下来，我们将把它变成一个合适的网络浏览器。首先，我们需要添加一些布局小部件，以便我们可以在之后将其他小部件放置到位。将垂直布局(1)拖到中心视图上，并从对象列表中选择中心视图。然后，单击位于顶部的垂直布局按钮(2):\n\n![](img/bf92deaf-aeb4-44b2-9ae7-84a690721694.png)\n\n然后，选择新添加的垂直布局，右键单击并选择变形为| QFrame。我们这样做的原因是，我们希望将 web 视图小部件放在这个 QFrame 对象下，而不是中心小部件下。我们必须将布局小部件转换成一个 QFrame(或任何继承的 QWidget)对象，这样它就可以*将*网页视图作为它的子视图。最后，将 QFrame 对象重命名为`webviewFrame`:\n\n![](img/4c9ede35-a9f9-4f09-9158-12a6773bb646.png)\n\n完成后，让我们在 QFrame 对象上方拖放一个水平布局小部件。现在我们可以看到水平布局小部件和 QFrame 对象的大小是相同的，我们不希望这样。接下来，选择 QFrame 对象，并将其垂直策略设置为扩展:\n\n![](img/41a44341-e29f-4869-be90-c8c26479e052.png)\n\n然后，您会看到顶部布局小部件现在非常薄。让我们暂时将其高度设置为`20`，如是:\n\n![](img/804fd92d-c9dd-4e0c-8085-4f938384d1a4.png)\n\n之后，拖放三个按钮到水平布局，我们现在可以将其上边距设置回`0`:\n\n![](img/073c8c36-2d6a-4ab1-9c34-a62fdefd8695.png)\n\n将按钮标签分别设置为`Back`、`Forward`和`Refresh`。您也可以使用图标而不是文本来显示在这些按钮上。如果您希望这样做，只需将文本属性设置为空，并从图标属性中选择一个图标。为了简单起见，我们将在本教程的按钮上显示文本。\n\n接下来，在三个按钮的右侧放置一个行编辑小部件，然后添加另一个带有`Go`标签的按钮:\n\n![](img/7c621b7f-d77a-4bab-8c9f-a4f45f2ca11c.png)\n\n之后，右键单击每个按钮并选择转到插槽。将弹出一个窗口，选择点击()并按确定。\n\n![](img/ba8acc39-41ae-4c97-9b68-464ec3008baa.png)\n\n这些按钮的信号功能如下所示:\n\n```cpp\nvoid MainWindow::on_backButton_clicked() \n{ \n   webview->back(); \n} \n\nvoid MainWindow::on_forwardButton_clicked() \n{ \n   webview->forward(); \n} \n\nvoid MainWindow::on_refreshButton_clicked() \n{ \n   webview->reload(); \n} \n\nvoid MainWindow::on_goButton_clicked() \n{ \n   loadPage(); \n} \n```\n\n基本上`QWebEngineView`类已经为我们提供了`back()`、`forward()`、`reload()`等功能，所以我们只需要在按下相应按钮时调用这些功能即可。然而，`loadPage()`函数是我们将要编写的自定义函数:\n\n```cpp\nvoid MainWindow::loadPage() \n{ \n   QString url = ui->addressInput->text(); \n   if (!url.startsWith(\"http://\") && !url.startsWith(\"https://\")) \n   { \n         url = \"http://\" + url; \n   } \n   ui->addressInput->setText(url); \n   webview->load(QUrl(url)); \n} \n```\n\n还记得在`mainwindow.h`中添加`loadPage()`的声明。\n\n我认为我们应该做更多的事情，而不仅仅是调用`load()`函数。通常情况下，用户在输入网页的网址时不会包括`http://`(或`https://)`方案，但当我们将网址传递给网页视图时，这是必需的。为了解决这个问题，我们自动检查方案的存在。如果没有找到，我们会手动将`http://`方案添加到网址中。另外，别忘了在开始时调用它来替换`load()`功能:\n\n```cpp\nMainWindow::MainWindow(QWidget *parent) : \n   QMainWindow(parent), \n   ui(new Ui::MainWindow) \n{ \n   ui->setupUi(this); \n\n webview = new QWebEngineView(ui->webviewFrame); \n   loadPage(); \n} \n```\n\n接下来，右键单击文本输入并选择转到插槽。然后，选择返回按下()并单击确定按钮:\n\n![](img/a722c4f2-e653-4d97-bb37-5547c61835d4.png)\n\n当用户输入完网页网址后，按下键盘上的*返回*键，将调用该槽功能。从逻辑上讲，用户会期望页面开始加载，而不必在每次输入完网址后都按“开始”按钮。代码真的很简单，我们只需要调用上一步刚创建的`loadPage()`函数:\n\n```cpp\nvoid MainWindow::on_addressInput_returnPressed() \n{ \n   loadPage(); \n} \n```\n\n现在我们已经完成了大量的代码，让我们构建并运行我们的项目，看看结果如何:\n\n![](img/e0729566-832f-4121-9b6c-d68ddf187c50.png)\n\n显示的结果看起来并没有那么好。出于某种原因，新的 web 视图似乎无法在不断扩大的规模政策下正常扩展，至少在撰写本书时使用的 5.10 版本上是如此。它可能会在未来的版本中被修复，但是让我们找到一种方法来解决这个问题。我所做的是在主窗口中覆盖一个名为`paintEvent()`的继承函数。在`mainwindow.h`中，简单添加函数声明，如下所示:\n\n```cpp\npublic: \n   explicit MainWindow(QWidget *parent = 0); \n   ~MainWindow(); \n void paintEvent(QPaintEvent *event); \n```\n\n然后，在`mainwindow.cpp`中这样写下它的定义:\n\n```cpp\nvoid MainWindow::paintEvent(QPaintEvent *event) \n{ \n   QMainWindow::paintEvent(event); \n   webview->resize(ui->webviewFrame->size()); \n} \n```\n\n每当主窗口需要重新渲染其小部件时(例如当窗口正在调整大小时)，Qt 将自动调用该`paintEvent()`函数。由于该函数将在应用初始化时以及窗口调整大小时调用，我们将使用该函数手动调整 web 视图的大小以适应其父小部件。\n\n再次构建并运行该程序，无论您如何调整主窗口的大小，您都应该能够很好地适应 web 视图。此外，我还删除了菜单栏、工具栏和状态栏，以使整个界面看起来更加整洁，因为我们没有在这个应用中使用这些:\n\n![](img/244f4c48-0ee6-4dab-9873-101bfac0d247.png)\n\n接下来，我们需要一个进度条向用户显示页面加载的当前进度。为此，首先我们需要在 web 视图下面放置一个进度条小部件:\n\n![](img/d092b79b-87d6-4b15-b1a9-f91c1cfb5e94.png)\n\n然后，将这两个插槽功能添加到`mainwindow.h`:\n\n```cpp\nprivate slots: \n   void on_backButton_clicked(); \n   void on_forwardButton_clicked(); \n   void on_refreshButton_clicked(); \n   void on_goButton_clicked(); \n   void on_addressInput_returnPressed(); \n   void webviewLoading(int progress); \n   void webviewLoaded(); \n```\n\n他们在`mainwindow.cpp`中的功能定义如下:\n\n```cpp\nvoid MainWindow::webviewLoading(int progress) \n{ \n   ui->progressBar->setValue(progress); \n} \n\nvoid MainWindow::webviewLoaded() \n{ \n   ui->addressInput->setText(webview->url().toString()); \n} \n```\n\n第一个函数`webviewLoading()`只是从 web 视图中获取进度级别(以百分比值的形式)，并直接将其提供给进度条小部件。\n\n第二个功能`webviewLoaded()`将把地址输入上的 URL 文本替换为网页视图加载的网页的实际 URL。如果没有此功能，在您按下后退按钮或前进按钮后，地址输入将不会显示正确的网址。完成后，让我们再次编译并运行该项目。结果看起来很神奇:\n\n![](img/39428977-f800-4408-bbd9-451b45561382.png)\n\n你会问我，如果我不用 Qt 做网页浏览器，这有什么实际用途？将网页视图嵌入到应用中还有许多其他用途，例如，通过装饰精美的 HTML 页面向用户显示产品的最新消息和更新，这是游戏市场中大多数在线游戏使用的常见方法。例如，流客户端还使用网络视图向玩家显示最新的游戏和折扣。\n\n这些通常被称为混合应用，它将 web 内容与原生 x 相结合，因此您可以利用来自 web 的动态内容以及原生运行的代码，这些代码具有高性能和一致外观的优势。\n\n除此之外，您还可以使用它以 HTML 格式显示可打印的报告。您可以轻松地将报告发送到打印机，或者通过调用`webview->page()->print()`或`webview->page()->printToPdf()`将其保存为 PDF 文件。\n\nTo learn more about printing from the web view, check out the following link: [http://doc.Qt.io/Qt-5/qwebenginepage.html#print.](http://doc.Qt.io/Qt-5/qwebenginepage.html#print)\n\n您可能还想使用 HTML 创建程序的整个用户界面，并将所有的 HTML、CSS 和图像文件嵌入到 Qt 的资源包中，并从 web 视图本地运行它。可能性是无穷无尽的，唯一的限制是你的想象力！\n\nTo learn more about Qt WebEngine, check out the documentation here: [https://doc.Qt.io/Qt-5/qtwebengine-overview.html.](https://doc.Qt.io/Qt-5/qtwebengine-overview.html)\n\n# 管理浏览器历史记录\n\nQt 的 web 引擎将用户访问过的所有链接存储到一个数组结构中，供以后使用。web 视图小部件通过调用`back()`和`forward()`在历史之间来回移动。\n\n如果您需要手动访问该浏览历史，请在`mainwindow.h`中添加以下标题:\n\n```cpp\n#include <QWebEnginePage> \n```\n\n之后，使用以下代码以`QWebEngineHistory`对象的形式获取浏览历史:\n\n```cpp\nQWebEngineHistory* history = QWebEnginePage::history(); \n```\n\n您可以从`history->items()`获取整个访问链接列表，或者使用`back()`或`forward()`等功能在历史记录之间导航。要清除浏览历史，请调用`history->clear()`。或者，您也可以这样做:\n\n```cpp\nQWebEngineProfile::defaultProfile()->clearAllVisitedLinks();\n```\n\nTo learn more about the `QWebEngineHistory` class, visit the following link: [http://doc.Qt.io/Qt-5/qwebenginehistory.html.](http://doc.Qt.io/Qt-5/qwebenginehistory.html)\n\n# 会话、cookies 和缓存\n\n与任何其他网络浏览器一样，`WebEngine`模块也支持用于存储会话和缓存的临时数据和持久数据的机制。会话和缓存非常重要，因为它们允许网站记住您的上次访问，并将您与数据(如购物车)相关联。会话、cookie 和缓存的定义如下所示:\n\n*   **Session** :通常情况下，Session 是服务器端的文件，包含带有唯一标识符的用户信息，从客户端发送出去，映射到特定的用户。然而，在 Qt 中，会话只是意味着没有任何截止日期的 cookie，因此当程序关闭时，它将消失。\n*   **Cookie**:Cookie 是客户端文件，包含用户信息或任何其他您想要保存的信息。与会话不同，cookies 有一个截止日期，这意味着它们将保持有效，并且可以在到达截止日期之前检索，即使程序已经关闭并再次打开。\n*   **缓存**:缓存是一种通过在第一次加载时将页面及其资源保存到本地磁盘来加快页面加载的方法。如果用户在下次访问时再次加载相同的页面，网络浏览器将重用缓存的资源，而不是等待下载完成，这可以显著加快页面加载时间。\n\n# 管理会话和 cookies\n\n默认情况下，`WebEngine`不保存任何 cookie，将所有用户信息视为临时会话，这意味着当您关闭程序时，您在网页上的登录会话将自动无效。\n\n要在 Qt 的`WebEngine `模块上启用 cookies，首先在`mainwindow.h`中添加以下标题:\n\n```cpp\n#include <QWebEngineProfile> \n```\n\n然后，只需调用以下函数来强制持久 cookies:\n\n```cpp\nQWebEngineProfile::defaultProfile()->setPersistentCookiesPolicy(QWebEngineProfile::ForcePersistentCookies);\n```\n\n调用上述函数后，您的登录会话将在关闭程序后继续存在。要将其恢复为非持久性 cookies，我们只需调用:\n\n```cpp\nQWebEngineProfile::defaultProfile()->setPersistentCookiesPolicy(QWebEngineProfile::NoPersistentCookies); \n```\n\n除此之外，您还可以更改 Qt 程序存储 cookies 的目录。为此，请将以下代码添加到源文件中:\n\n```cpp\nQWebEngineProfile::defaultProfile()->setPersistentStoragePath(\"your folder\");  \n```\n\n如果出于某种原因，您想要手动删除所有 cookies，请使用以下代码:\n\n```cpp\nQWebEngineProfile::defaultProfile()->cookieStore()->deleteAllCookies(); \n```\n\n# 管理缓存\n\n接下来，让我们谈谈缓存。有两种类型的缓存可以在网络引擎模块中使用，即内存缓存和磁盘缓存。内存缓存使用计算机的内存来存储缓存，一旦关闭程序，缓存就会消失。另一方面，磁盘缓存保存硬盘中的所有文件，因此即使在您关闭计算机后，它们仍然会保留。\n\n默认情况下，网络引擎模块会将所有缓存保存到磁盘，如果需要将它们更改为内存缓存，请调用以下函数:\n\n```cpp\nQWebEngineProfile::defaultProfile()->setHttpCacheType(QWebEngineProfile::MemoryHttpCache); \n```\n\n或者，您也可以通过调用:\n\n```cpp\nQWebEngineProfile::defaultProfile()->setHttpCacheType(QWebEngineProfile::NoCache); \n```\n\n至于改变你的程序保存缓存文件的文件夹，调用`setCachePath()`函数:\n\n```cpp\nQWebEngineProfile::defaultProfile()->setCachePath(\"your folder\"); \n```\n\n最后，要删除所有缓存文件，调用`clearHttpCache()`:\n\n```cpp\nQWebEngineProfile::defaultProfile()->clearHttpCache(); \n```\n\n您可以使用许多其他功能来更改与 cookies 和缓存相关的设置。\n\nYou can read more about it at the following link: [https://doc.Qt.io/Qt-5/qwebengineprofile.html](https://doc.Qt.io/Qt-5/qwebengineprofile.html)\n\n# 集成 JavaScript 和 C++\n\n使用 Qt 的 web 引擎模块的一个强大功能是，它可以从 C++ 调用 JavaScript 函数，也可以从 JavaScript 调用 C++ 函数。这使得它不仅仅是一个网页浏览器。您可以使用它来访问 web 浏览器标准不支持的功能，例如文件管理和硬件集成。W3C 标准不可能做到这一点；因此，不可能用原生 JavaScript 来实现。但是，您可以使用 C++ 和 Qt 实现这些功能，然后只需从您的 JavaScript 中调用 C++ 函数。让我们看看如何通过 Qt 实现这一点。\n\n# 从 C++ 调用 JavaScript 函数\n\n之后，在我们刚刚创建的 HTML 文件中添加以下代码:\n\n```cpp\n<!DOCTYPE html><html> \n   <head> \n      <title>Page Title</title> \n   </head> \n   <body> \n      <p>Hello World!</p> \n   </body> \n</html> \n```\n\n这些是基本的超文本标记语言标签，除了一行写着`Hello World!`的单词之外，什么也不会给你。您可以尝试使用网络浏览器加载它:\n\n![](img/84001c1c-aabc-4ff1-80bf-b05771ab51cf.png)\n\n之后，让我们回到我们的 Qt 项目，转到文件|新文件或项目，并创建一个 Qt 资源文件:\n\n![](img/1d9d3c88-e775-4c5e-bd46-ab54e7a7ab81.png)\n\n然后，打开我们刚刚创建的 Qt 资源文件，添加一个`/html`前缀，然后将 HTML 文件添加到资源文件中，如下所示:\n\n![](img/8c17bb18-d44b-4989-a04d-0e8b7ea3b91e.png)\n\n当资源文件仍然打开时，右键单击 text.html，然后选择“将资源路径复制到剪贴板”。紧接着，将您的网页视图的网址更改为:\n\n```cpp\nwebview->load(QUrl(\"qrc:///html/test.html\")); \n```\n\n你可以使用刚从资源文件中复制的链接，但是一定要在链接的前面加上 URL 方案`qrc://`。立即构建并运行您的项目，您应该能够立即看到结果:\n\n![](img/3908ba70-8603-4631-bfef-3994e2929583.png)\n\n接下来，我们需要在 JavaScript 中设置一个函数，C++ 将在稍后调用该函数。我们将创建一个简单的函数，弹出一个简单的消息框，并在调用时将`Hello World!`文本更改为其他内容:\n\n```cpp\n<!DOCTYPE html> \n<html> \n   <head> \n         <title>Page Title</title> \n         <script> \n               function hello() \n               { \n                  document.getElementById(\"myText\").innerHTML =       \n                  \"Something happened!\"; \n                  alert(\"Good day sir, how are you?\"); \n               } \n         </script> \n   </head> \n   <body> \n         <p id=\"myText\">Hello World!</p> \n   </body> \n</html> \n```\n\n请注意，我已经在`Hello World!`文本中添加了一个 ID，以便我们能够找到它并更改其文本。一旦你完成了，让我们再次去我们的 Qt 项目。\n\n让我们继续向我们的程序 UI 添加一个按钮，当按钮被按下时，我们希望我们的 Qt 程序调用我们刚刚在 JavaScript 中创建的`hello()`函数。在 Qt 中做到这一点其实非常容易；您只需从`QWebEnginePage`类调用`runJavaScript()`函数，如下所示:\n\n```cpp\nvoid MainWindow::on_pushButton_clicked() \n{ \n   webview->page()->runJavaScript(\"hello();\"); \n} \n```\n\n从下面的截图中可以看出，结果非常惊人:\n\n![](img/e860b594-575b-49c9-81dc-922f1dbb9067.png)\n\n你可以做很多事情，不仅仅是改变文本或调用消息框。例如，您可以在 HTML 画布中开始或停止动画，显示或隐藏 HTML 元素，触发 Ajax 事件以从 PHP 脚本中检索信息，等等...无尽的可能！\n\n# 从 JavaScript 调用 C++ 函数\n\n接下来，让我们看看如何从 JavaScript 调用 C++ 函数。为了演示，我将在 web 视图上方放置一个文本标签，我们将使用 JavaScript 函数更改它的文本:\n\n![](img/a962a250-b7c4-4945-93ad-ddc7ae12a78b.png)\n\n通常，JavaScript 只能在 HTML 环境中工作，因此只能改变 HTML 元素，而不能改变 web 视图之外的东西。然而，Qt 允许我们通过使用网络频道模块来做到这一点。因此，让我们打开我们的项目(`.pro`)文件，并将 web 频道模块添加到项目中:\n\n```cpp\nQT += core gui webengine webenginewidgets webchannel \n```\n\n之后，打开`mainwindow.h`并在`QWebChannel`表头添加:\n\n```cpp\n#include <QMainWindow> \n#include <QWebEngineView> \n#include <QWebChannel> \n```\n\n同时，我们还声明了一个名为`doSomething()`的函数，前面有一个`Q_INVOKABLE`宏:\n\n```cpp\nQ_INVOKABLE void doSomething(); \n```\n\n`Q_INVOKABLE`宏告诉 Qt 向 JavaScript 引擎公开该函数，这样就可以从 JavaScript 调用该函数(还有 QML，因为 QML 也是基于 JavaScript 的)。\n\n然后在`mainwindow.cpp`中，我们必须首先创建一个`QWebChannel`对象，并将我们的主窗口注册为一个 JavaScript 对象。您可以将任何 Qt 对象注册为 JavaScript 对象，只要它是从`QObject`类派生的。\n\n因为我们要从 JavaScript 调用`doSomething()`函数，所以我们必须向 JavaScript 引擎注册主窗口。之后，我们还需要将刚刚创建的`QWebChannel`对象设置为我们的网页视图的网页频道。代码如下所示:\n\n```cpp\nQWebChannel* channel = new QWebChannel(this); \nchannel->registerObject(\"mainwindow\", this); \nwebview->page()->setWebChannel(channel); \n```\n\n完成后，让我们定义`doSomething()`函数。我们只需要做一些简单的事情——更改我们的 Qt 图形用户界面上的文本标签，仅此而已:\n\n```cpp\nvoid MainWindow::doSomething() \n{ \n   ui->label->setText(\"This text has been changed by javascript!\"); \n} \n```\n\n我们已经完成了 C++ 代码，让我们打开我们的 HTML 文件。我们需要做几件事来使这个工作。首先，我们需要包含默认情况下嵌入到您的 Qt 程序中的`qwebchannel.js`脚本，这样您就不必在 Qt 目录中搜索该文件。在`head`标签之间添加以下代码:\n\n```cpp\n<script type=\"text/javascript\" src=\"qrc:///qtwebchannel/qwebchannel.js\"></script> \n```\n\n然后，当 web 视图成功加载文档时，我们在 JavaScript 中创建一个`QWebChannel`对象，并将`mainwindow`变量从 Qt(我们之前在 C++ 中注册的)链接到实际的主窗口对象。该步骤必须在网页加载完成后进行(通过`window.onload`回调)；否则，创建 web 频道时可能会出现问题:\n\n```cpp\nvar mainwindow; \nwindow.onload = function() \n{ \n   new QWebChannel(Qt.webChannelTransport,function(channel) \n   { \n         mainwindow = channel.objects.mainwindow; \n   }); \n} \n```\n\n之后，我们创建一个调用`doSomething()`函数的 JavaScript 函数:\n\n```cpp\nfunction myFunction() \n{ \n   mainwindow.doSomething(); \n} \n```\n\n最后，在 HTML 正文中添加一个按钮，确保按下按钮时`myFunction()`被调用:\n\n```cpp\n<body> \n   <p id=\"myText\">Hello World!</p> \n   <button onclick=\"myFunction()\">Do Something</button> \n</body> \n```\n\n立即构建并运行该程序，您应该能够获得以下结果:\n\n![](img/eebf2409-5486-476d-b772-06ec44cbed98.png)\n\n除了改变 Qt 小部件的属性之外，使用这个方法你可以做很多有用的事情。例如，将文件保存到本地硬盘，从条形码扫描仪获取扫描数据，等等。本地技术和网络技术之间不再有任何障碍。但是，一定要特别注意这种技术可能带来的安全隐患。俗话说:\n\n\"With great power comes great responsibility.\"\n\n# 摘要\n\n在这一章中，我们学习了如何创建自己的 web 浏览器，并使其与本机代码交互。Qt 为我们提供了网络渠道技术，这使得 Qt 成为一个非常强大的软件开发平台。\n\n它既利用了 Qt 的强大功能，又利用了网络技术的美丽，这意味着在开发方面你可以有更多的选择，而不仅仅局限于 Qt 的方法。我真的很兴奋，迫不及待地想看看你用这个能取得什么成就！\n\n在下一章加入我们，学习如何使用 Qt 创建一个类似于谷歌地图的地图查看器！"
  },
  {
    "path": "docs/cpp-gui-prog-qt5/07.md",
    "content": "# 七、地图查看器\n\n用户位置和地图显示是这几天变得越来越普遍的两个特征，并且已经被用于各种类型的应用中。它们通常用于后端分析和前端显示。\n\n地图查看器可用于导航、附近的兴趣点查找、基于位置的服务(如叫出租车)等。您可以使用 Qt 来实现大部分功能，但是如果您想要更复杂的东西，您将需要一个高级的数据库系统。\n\n在前一章中，我们学习了如何在应用中嵌入网络浏览器。在这一章中，我们将尝试一些更有趣的东西，包括以下主题:\n\n*   创建地图显示\n*   标记和形状显示\n*   获取用户的位置\n*   地理路由请求\n\n让我们继续创建我们自己的地图查看器！\n\n# 图象显示\n\nQt 位置模块为开发人员提供地理编码和导航信息。它还允许用户从服务器或用户设备上搜索需要检索数据的位置。\n\n目前，Qt 的地图视图不支持 C++，只支持 QML。这意味着我们只能使用 QML 脚本来改变与视觉相关的任何东西——显示地图、添加标记等等；另一方面，在通过 QML 向用户显示信息之前，我们可以使用模块提供的 C++ 类从数据库或服务提供商那里获取信息。\n\n简单说明一下， **QML** ( **Qt 建模语言**)是 Qt Quick 应用的用户界面标记语言。由于 QML 是由 JavaScript 框架驱动的，它的编码语法几乎与 JavaScript 相似。如果您需要深入学习 QML 和 Qt Quick，请转至[第 14 章](13.html)、 *Qt Quick 和 QML，*，因为这是专门介绍它的一整章。\n\n有很多教程教你如何使用 Qt Quick 和 QML 语言创建一个成熟的地图查看器，但是没有太多的教程教你如何将 C++ 和 QML 结合起来。我们开始吧！\n\n# 设置 Qt 定位模块\n\n1.  首先，创建一个新的 Qt Widgets 应用项目。\n2.  之后，打开您的项目文件(`.pro`)并将以下模块添加到您的 Qt 项目中:\n\n```cpp\nQT += core gui location qml quickwidgets \n```\n\n除了`location`模块，我们还增加了`qml`和`quickwidgets`模块，这是下一节地图显示小部件所需要的。这就是我们在项目中启用`Qt Location`模块所需做的全部工作。接下来，我们将继续向我们的项目添加地图显示小部件。\n\n# 创建地图显示\n\n一旦你准备好了，让我们打开`mainwindow.ui`并删除菜单栏、工具栏和状态栏，因为我们在这个项目中不需要这些:\n\n![](img/c0b7b1d8-e5fe-4bcc-a390-a313956cddb6.png)\n\n之后，将一个 QQuickWidget 从 Widget 框拖到 UI 画布上。然后，单击位于画布顶部的水平布局按钮，向其添加布局属性:\n\n![](img/28d2e932-e3fa-4392-a2b3-0426a394ff24.png)\n\n然后，将中央小部件的所有边距属性设置为 0:\n\n![](img/3a817668-af69-4e72-9b67-377db36a1240.png)\n\n接下来，我们需要通过转到文件|新文件或项目来创建一个名为`mapview.qml`的新文件....之后，选择 Qt 类别并遵循 QML 文件(Qt 快速 2):\n\n![](img/6a51fac1-dd56-4e67-bf24-8ac694b71266.png)\n\n一旦创建了 QML 文件，将其打开，并添加以下代码以将`location` 和`positioning` 模块包含到该文件中，以便我们以后可以使用其功能:\n\n```cpp\nimport QtQuick 2.0 \nimport QtLocation 5.3 \nimport QtPositioning 5.0 \n```\n\n之后，我们创建一个`Plugin`对象并将其命名为 **osm** ( **开放街道地图**)，然后我们创建一个地图对象并将插件应用到其`plugin`属性。我们还将起始坐标设置为(`40.7264175,-73.99735`)，也就是纽约州的某个地方。除此之外，默认的`zoom level`设置为`14`，这足以让城市有一个好的视角:\n\n```cpp\nItem \n{ \n    Plugin \n    { \n        id: mapPlugin \n        name: \"osm\" \n    } \n\n    Map \n    { \n        id: map \n        anchors.fill: parent \n        plugin: mapPlugin \n        center: QtPositioning.coordinate(40.7264175,-73.99735) \n        zoomLevel: 14 \n    } \n} \n```\n\n在我们能够在应用上显示地图之前，我们必须首先创建一个资源文件，并将 QML 文件添加到其中。这可以通过转到文件|创建新文件或项目来完成....然后，选择 Qt 类别并选择 Qt 资源文件。\n\n创建资源文件后，添加一个名为`qml`的前缀，并将 QML 文件添加到前缀中，如下所示:\n\n![](img/ccdd7e2f-cb58-4ebd-b4f3-dc5320be49f4.png)\n\n我们现在可以打开`mainwindow.ui`并将 QQuickWidget 的`source`属性设置为`qrc:/qml/mapview.qml`。您也可以单击 source 属性后面的按钮，直接从资源中选择 QML 文件。\n\n一旦你完成了，让我们编译和运行我们的项目，看看我们有什么！您也可以尝试使用鼠标在地图上平移和缩放:\n\n![](img/7151aaa3-59be-4e8a-8dee-883c039e2c05.png)\n\n即使我们可以通过使用 web view 小部件获得同样的结果，它也会让我们编写大量的 JavaScript 代码来显示这样的地图。通过使用 Qt Quick，我们只需要编写几行简单的 QML 代码，仅此而已。\n\n# 标记和形状显示\n\n在前面的部分中，我们成功地创建了一个地图显示，但这只是这个项目的开始。我们需要能够以分层在地图顶部的标记或形状的形式显示自定义数据，以便用户能够理解数据。\n\n# 在地图上显示位置标记\n\n如果我告诉你我最喜欢的餐厅位于(`40.7802655, -74.108644`)，你就说不通了。然而，如果这些坐标以位置标记的形式显示在地图视图上，你马上就会知道它在哪里。让我们看看如何在地图视图中添加位置标记！\n\n首先，我们需要一个标记图像，它应该看起来像这样，或者更好，设计您自己的标记:\n\n![](img/04d42016-db00-4884-a2c8-0c8edfb9052d.png)\n\n之后，我们需要将这个图像注册到我们项目的资源文件中。用 Qt Creator 打开`resource.qrc`，创建一个名为`images`的新前缀。然后，将标记图像添加到新创建的前缀中。务必确保图像具有透明背景，以便在地图上看起来很好:\n\n![](img/935d7d97-990d-4453-a247-1a9a05867f34.png)\n\n接下来，打开`mapview.qml`并用以下内容替换代码:\n\n```cpp\nItem \n{ \n    id: window \n\n    Plugin \n    { \n        id: mapPlugin \n        name: \"osm\" \n    } \n\n    Image \n    { \n        id: icon \n        source: \"qrc:img/map-marker-icon.png\" \n        sourceSize.width: 50 \n        sourceSize.height: 50 \n    } \n\n    MapQuickItem \n    { \n        id: marker \n        anchorPoint.x: marker.width / 4 \n        anchorPoint.y: marker.height \n        coordinate: QtPositioning.coordinate(40.7274175,-73.99835) \n\n        sourceItem: icon \n    } \n\n    Map \n    { \n        id: map \n        anchors.fill: parent \n        plugin: mapPlugin \n        center: QtPositioning.coordinate(40.7264175,-73.99735) \n        zoomLevel: 14 \n\n        Component.onCompleted: \n        { \n            map.addMapItem(marker) \n        } \n    } \n} \n```\n\n在前面的代码中，我们首先添加了一个将用作标记图像的图像对象。由于原始图像确实很大，我们必须通过将`sourceSize`属性设置为`50x50`来调整其大小。我们还必须将标记图像的`anchor point`设置为图像的`center-bottom`，因为这是标记尖端所在的位置。\n\n之后，我们创建一个`MapQuickItem`对象，它将作为标记本身。将标记图像设置为`MapQuickItem`对象的`sourceItem`，然后通过调用`map.addMapItem()`将标记添加到地图中。该功能必须在地图创建完成并准备显示后调用，这意味着我们只能在`Component.onCompleted`事件触发后调用。\n\n现在我们已经完成了代码，让我们编译并查看结果:\n\n![](img/6977a437-56a1-4f90-bcc9-55b5005d9fda.png)\n\n即使现在看起来一切都很好，我们也不想在 QML 硬编码。想象一下向地图添加数百个标记，使用一组不同的代码手动添加每个标记是不可能的。\n\n为了创建一个允许我们动态创建位置标记的函数，我们需要首先将标记 QML 代码从`mapview.qml`分离到一个新的 QML 文件中。让我们创建一个名为`marker.qml`的新 QML 文件，并将其添加到资源文件中:\n\n![](img/2dd8239b-895d-482c-985e-cde898426da4.png)\n\n接下来，从`mapview.qml`中移除`MapQuickItem`和`Image`物体，并将其移动到`marker.qml`:\n\n```cpp\nimport QtQuick 2.0 \nimport QtLocation 5.3 \n\nMapQuickItem \n{ \n    id: marker \n    anchorPoint.x: marker.width / 4 \n    anchorPoint.y: marker.height \n    sourceItem: Image \n    { \n        id: icon \n        source: \"qrc:img/map-marker-icon.png\" \n        sourceSize.width: 50 \n        sourceSize.height: 50 \n    } \n} \n```\n\n从前面的代码中可以看到，我已经合并了`Image`对象和`MapQuickItem`对象。坐标属性也被移除了，因为我们将只在地图上放置标记时设置它。\n\n现在，再次打开`mapview.qml`，将该功能添加到`Item`对象中:\n\n```cpp\nItem \n{ \n    id: window \n\n    Plugin \n    { \n        id: mapPlugin \n        name: \"osm\" \n    } \n\n    function addMarker(latitude, longitude) \n    { \n        var component = Qt.createComponent(\"qrc:///qml/marker.qml\") \n        var item = component.createObject(window, { coordinate: \n        QtPositioning.coordinate(latitude, longitude) }) \n        map.addMapItem(item) \n    } \n```\n\n从前面的代码中，我们首先通过加载`marker.qml`文件创建了一个组件。然后，我们通过调用`createObject()`从组件中创建一个对象/项目。在`createObject()`功能中，我们将窗口对象作为其父对象，并将其位置设置为`addMarker()`功能提供的坐标。最后，我们将该项目添加到地图中进行渲染。\n\n每当我们想要创建一个新的位置标记时，我们只需要调用这个`addMarker()`函数。为了演示这一点，让我们通过调用`addMarker()`三次来创建三个不同的标记:\n\n```cpp\nMap \n{ \n    id: map \n    anchors.fill: parent \n    plugin: mapPlugin \n    center: QtPositioning.coordinate(40.7264175,-73.99735) \n    zoomLevel: 14 \n\n    Component.onCompleted: \n    { \n        addMarker(40.7274175,-73.99835) \n        addMarker(40.7276432,-73.98602) \n        addMarker(40.7272175,-73.98935) \n    } \n} \n```\n\n再次构建并运行项目，您应该能够看到如下内容:\n\n![](img/581d34bc-8dfb-46c4-80e3-fbea4706da85.png)\n\n我们可以更进一步，给每个标记添加一个文本标签。为此，首先打开`marker.qml`，然后添加另一个名为`QtQuick.Controls`的模块:\n\n```cpp\nimport QtQuick 2.0 \nimport QtQuick.Controls 2.0 \nimport QtLocation 5.3 \n```\n\n之后，给名为`labelText`的`MapQuickItem`对象添加一个自定义属性:\n\n```cpp\nMapQuickItem \n{ \n    id: marker \n    anchorPoint.x: marker.width / 4 \n    anchorPoint.y: marker.height \n    property string labelText \n```\n\n完成后，将其`sourceItem`属性更改为:\n\n```cpp\nsourceItem: Item \n{ \n        Image \n        { \n            id: icon \n            source: \"qrc:img/map-marker-icon.png\" \n            sourceSize.width: 50 \n            sourceSize.height: 50 \n        } \n\n        Rectangle \n        { \n            id: tag \n            anchors.centerIn: label \n            width: label.width + 4 \n            height: label.height + 2 \n            color: \"black\" \n        } \n\n        Label \n        { \n            id: label \n            anchors.centerIn: parent \n            anchors.horizontalCenterOffset: 20 \n            anchors.verticalCenterOffset: -12 \n            font.pixelSize: 16 \n            text: labelText \n            color: \"white\" \n        } \n} \n```\n\n从前面的代码中，我们创建了一个`Item`对象来将多个对象组合在一起。然后，我们创建了一个`Rectangle`对象作为标签背景和一个`Label`对象作为文本。`Label`对象的`text`属性将链接到`MapQuickItem`对象的`labelText`属性。我们可以向`addMarker()`功能添加另一个输入来设置`labelText`属性，如下所示:\n\n```cpp\nfunction addMarker(name, latitude, longitude) \n{ \n        var component = Qt.createComponent(\"qrc:///qml/marker.qml\") \n        var item = component.createObject(window, { coordinate: QtPositioning.coordinate(latitude, longitude), labelText: name }) \n        map.addMapItem(item) \n} \n```\n\n因此，当我们创建标记时，我们可以这样调用`addMarker()`函数:\n\n```cpp\nComponent.onCompleted: \n{ \n   addMarker(\"Restaurant\", 40.7274175,-73.99835) \n   addMarker(\"My Home\", 40.7276432,-73.98602) \n   addMarker(\"School\", 40.7272175,-73.98935) \n} \n```\n\n再次构建并运行项目，您应该会看到:\n\n![](img/8b2f536b-7199-4fd7-bd36-b15745c7e285.png)\n\n很棒，不是吗？然而，我们还没有完成。因为我们很可能使用 C++ 通过 Qt 的 SQL 模块从数据库中获取数据，所以我们需要找到一种从 C++ 中调用 QML 函数的方法。\n\n为此，让我们注释掉`mapview.qml`中的三个`addMarker()`函数，打开`mainwindow.h`和以下标题:\n\n```cpp\n#include <QQuickItem> \n#include <QQuickView> \n```\n\n之后打开`mainwindow.cpp`调用`QMetaObject::invokeMethod()`功能，如下图:\n\n```cpp\nMainWindow::MainWindow(QWidget *parent) : \n   QMainWindow(parent), \n   ui(new Ui::MainWindow) \n{ \n   ui->setupUi(this); \n\n QObject* target = qobject_cast<QObject*>(ui->quickWidget->rootObject()); \n   QString functionName = \"addMarker\"; \n\n   QMetaObject::invokeMethod(target, functionName, Qt::AutoConnection, Q_ARG(QVariant, \"Testing\"), Q_ARG(QVariant, 40.7274175), Q_ARG(QVariant, -73.99835)); \n} \n```\n\n前面的代码看起来可能很复杂，但是如果我们剖析它并分析它的每个参数，它实际上是非常简单的。前面函数的第一个参数是我们要从中调用函数的对象，在本例中，它是地图视图小部件的根对象(在`mapview.qml`中的`Item`对象)。接下来，我们要告诉我们要调用哪个函数名，它就是`addMarker()`函数。之后，第三个参数是信号和插槽系统用来调用此方法的连接类型。对于这个，我们就让它成为默认设置，也就是`Qt::AutoConnection`。其余的是`addMarker()`函数需要的参数。我们使用`Q_ARG`宏来指示数据的类型和值。\n\n最后，再次构建并运行应用。您将看到一个带有标签的标记已添加到地图中，但这一次，它是从我们的 C++ 代码中调用的，而不是 QML:\n\n![](img/3372e8f3-0227-467a-ba57-3a7033d395e1.png)\n\n# 在地图上显示形状\n\n除了在地图上添加标记，我们还可以在地图上绘制不同类型的形状来指示感兴趣的区域或充当地理围栏，每当目标进入或离开形状覆盖的区域时，地理围栏都会发出警告。地理围栏是一种多边形形状，它在地图上为基于位置的服务定义了感兴趣的区域或虚拟地理边界。通常，地理围栏用于在设备进入和/或退出地理围栏时触发警报。使用地理围栏的一个很好的例子是，当您需要购物提醒时，您可以在超市周围画一个地理围栏，并在地理围栏上附上购物清单。当你(和你的手机)进入地理围栏区域时，你会在手机上收到通知，提醒你该买什么。那不是很好吗？\n\nFor more information about geofences, please visit: `https://en.wikipedia.org/wiki/Geo-fence`\n\n在本章中，我们不会创建功能性地理围栏，因为这是一个相当高级的主题，它通常作为服务器端服务运行，用于检查和触发警报。我们将只使用 Qt 来绘制形状并在屏幕上直观地显示它。\n\n为了在地图视图小部件上绘制形状，我们将为每种类型的形状再创建几个 QML 文件，并将它们添加到程序的资源中:\n\n![](img/7ea97aa7-6e6d-4218-8515-adc2ad7578b5.png)\n\n对于每个新创建的 QML 文件，我们将做一些类似于位置标记的事情。对于`circle.qml`，看起来是这样的:\n\n```cpp\nimport QtQuick 2.0 \nimport QtLocation 5.3 \n\nMapCircle \n{ \n    property int borderWidth \n    border.width: borderWidth \n} \n```\n\n我们只在这个文件中声明`borderWidth`，因为我们可以在后面调用`createCircle()`函数时直接设置其他属性。`rectangle.qml`也是如此:\n\n```cpp\nimport QtQuick 2.0 \nimport QtLocation 5.3 \n\nMapRectangle \n{ \n    property int borderWidth \n    border.width: borderWidth \n} \n```\n\n对`polygon.qml`重复类似的步骤:\n\n```cpp\nimport QtQuick 2.0 \nimport QtLocation 5.3 \n\nMapPolygon \n{ \n    property int borderWidth \n    border.width: borderWidth \n} \n```\n\n如果需要，您可以设置其他属性，但为了演示起见，我们只更改了一些属性，如颜色、形状和边框宽度。完成后，让我们打开`mapview.qml`并定义一些用于添加形状的函数:\n\n```cpp\nItem \n{ \n    id: window \n\n    Plugin \n    { \n        id: mapPlugin \n        name: \"osm\" \n    } \n\n    function addCircle(latitude, longitude, radius, color, borderWidth) \n    { \n       var component = Qt.createComponent(\"qrc:///qml/circle.qml\") \n       var item = component.createObject(window, { center: \n       QtPositioning.coordinate(latitude, longitude), radius: radius, \n       color: color, borderWidth: borderWidth }) \n       map.addMapItem(item) \n    } \n\n    function addRectangle(startLat, startLong, endLat, endLong, color, \n    borderWidth) \n    { \n        var component = Qt.createComponent(\"qrc:///qml/rectangle.qml\") \n        var item = component.createObject(window, { topLeft: \n       QtPositioning.coordinate(startLat, startLong), bottomRight: \n       QtPositioning.coordinate(endLat, endLong), color: color, \n       borderWidth: borderWidth }) \n        map.addMapItem(item) \n    } \n\n    function addPolygon(path, color, borderWidth) \n    { \n        var component = Qt.createComponent(\"qrc:///qml/polygon.qml\") \n        var item = component.createObject(window, { path: path, color: \n        color, borderWidth: borderWidth }) \n        map.addMapItem(item) \n    } \n```\n\n这些函数与`addMarker()`函数非常相似，只是它接受不同的参数，这些参数随后被传递给`createObject()`函数。之后，让我们尝试使用前面的函数创建形状:\n\n```cpp\naddCircle(40.7274175,-73.99835, 250, \"green\", 3); \naddRectangle(40.7274175,-73.99835, 40.7376432, -73.98602, \"red\", 2) \nvar path = [{ latitude: 40.7324281, longitude: -73.97602 }, \n            { latitude: 40.7396432, longitude: -73.98666 }, \n            { latitude: 40.7273266, longitude: -73.99835 }, \n            { latitude: 40.7264281, longitude: -73.98602 }]; \naddPolygon(path, \"blue\", 3); \n```\n\n以下是使用我们刚刚定义的函数创建的形状。我分别调用了每个函数来演示其结果，因此有三个不同的窗口:\n\n![](img/c6a0e1b2-e88a-4a65-b6a0-c5c4ea11274d.png)\n\n# 获取用户的位置\n\nQt 为我们提供了一套检索用户位置信息的功能，但只有当用户的设备支持地理定位时，它才会起作用。这应该适用于所有现代智能手机，也可能适用于一些现代电脑。\n\n要使用`Qt Location`模块获取用户位置，首先我们打开`mainwindow.h`并添加以下头文件:\n\n```cpp\n#include <QDebug> \n#include <QGeoPositionInfo> \n#include <QGeoPositionInfoSource> \n```\n\n之后，在同一文件中声明以下`slot`函数:\n\n```cpp\nprivate slots: \n   void positionUpdated(const QGeoPositionInfo &info); \n```\n\n紧接着，打开`mainwindow.cpp`并将下面的代码添加到您希望它开始获取用户位置的地方。为了演示，我将在`MainWindow`构造函数中调用它:\n\n```cpp\nQGeoPositionInfoSource *source = QGeoPositionInfoSource::createDefaultSource(this); \nif (source) \n{ \n   connect(source, &QGeoPositionInfoSource::positionUpdated, \n         this, &MainWindow::positionUpdated); \n   source->startUpdates(); \n} \n```\n\n然后，实现我们前面声明的`positionUpdated()`函数，如下所示:\n\n```cpp\nvoid MainWindow::positionUpdated(const QGeoPositionInfo &info) \n{ \n   qDebug() << \"Position updated:\" << info; \n} \n```\n\n如果您现在构建并运行应用，您可能会也可能不会获得任何位置信息，这取决于您用来运行测试的设备。如果您收到如下调试消息:\n\n```cpp\nserialnmea: No serial ports found\nFailed to create Geoclue client interface. Geoclue error: org.freedesktop.DBus.Error.Disconnected\n```\n\n![](img/d9095bd8-9aa1-4369-998f-0cc65b69698d.png)\n\n那么你可能需要为测试找到一些其他的设备。否则，您可能会得到类似如下的结果:\n\n```cpp\nPosition updated: QGeoPositionInfo(QDateTime(2018-02-22 19:13:05.000 EST Qt::TimeSpec(LocalTime)), QGeoCoordinate(45.3333, -75.9))\n```\n\n我会在这里给你留一个任务，你可以通过使用我们到目前为止创建的函数来尝试和完成。既然您现在可以获得您所在位置的坐标，那么尝试通过在地图显示中添加一个标记来显示您当前所在的位置，从而进一步增强您的应用。一起工作应该很有趣！\n\n# 地理路由请求\n\n还有一个重要的功能叫做**地理路由请求**，它是一组帮助你规划从 A 点到 b 点的路线(通常是最短路线)的功能，这个功能需要服务提供商；在这种情况下，我们将使用**开放街道地图** ( **OSM** )因为它是完全免费的。\n\n请注意，OSM 是一个在线合作项目，这意味着如果您所在地区没有人向 OSM 服务器提供路线数据，那么您将无法获得准确的结果。或者，您也可以使用付费服务，如 Mapbox 或 ESRI。\n\n让我们看看如何在 Qt 中实现地理路由请求！首先，在我们的`mainwindow.h`文件中包含以下标题:\n\n```cpp\n#include <QGeoServiceProvider>\n#include <QGeoRoutingManager>\n#include <QGeoRouteRequest>\n#include <QGeoRouteReply>\n```\n\n之后，给`MainWindow`类增加两个槽函数，即`routeCalculated()`和`routeError()`:\n\n```cpp\nprivate slots:\n    void positionUpdated(const QGeoPositionInfo &info);\n    void routeCalculated(QGeoRouteReply *reply);\n    void routeError(QGeoRouteReply *reply, QGeoRouteReply::Error error, const QString &errorString);\n```\n\n完成后，打开`mainwindow.cpp`并在`MainWindow`构造器方法中创建一个服务提供者对象。我们将使用 OSM 服务，因此我们将在启动`QGeoServiceProvider`课程时使用首字母缩略词`\"osm\"`:\n\n```cpp\nQGeoServiceProvider* serviceProvider = new QGeoServiceProvider(\"osm\");\n```\n\n紧接着，我们将从刚刚创建的服务提供商对象中获取路由管理器的指针:\n\n```cpp\nQGeoRoutingManager* routingManager = serviceProvider->routingManager();\n```\n\n然后，将来自路由管理器的`finished()`信号和`error()`信号与我们刚刚定义的`slot`功能连接起来:\n\n```cpp\nconnect(routingManager, &QGeoRoutingManager::finished, this, &MainWindow::routeCalculated);\nconnect(routingManager, &QGeoRoutingManager::error, this, &MainWindow::routeError);\n```\n\n当服务提供商对成功的请求做出答复时，或者当请求失败并返回错误消息时，将触发这些槽功能。`routeCalculated()`槽功能看起来像这样:\n\n```cpp\nvoid MainWindow::routeCalculated(QGeoRouteReply *reply)\n{\n    qDebug() << \"Route Calculated\";\n    if (reply->routes().size() != 0)\n    {\n        // There could be more than 1 path\n        // But we only get the first route\n        QGeoRoute route = reply->routes().at(0);\n        qDebug() << route.path();\n    }\n    reply->deleteLater();\n}\n```\n\n如您所见，`QGeoRouteReply`指针包含服务提供商在请求成功时发送的路由信息。有时它会附带多条路线，因此在示例中，我们只需获取第一条路线，并通过 Qt 的应用输出窗口显示它。或者，您可以使用这些坐标沿路线绘制路径或制作标记动画。\n\n至于`routeError()`槽函数，我们就只输出服务商发送的错误字符串:\n\n```cpp\nvoid MainWindow::routeError(QGeoRouteReply *reply, QGeoRouteReply::Error error, const QString &errorString)\n{\n    qDebug() << \"Route Error\" << errorString;\n    reply->deleteLater();\n}\n```\n\n完成后，让我们用`MainWindow`构造器方法启动一个地理路由请求，并将其发送给服务提供商:\n\n```cpp\nQGeoRouteRequest request(QGeoCoordinate(40.675895,-73.9562151), QGeoCoordinate(40.6833154,-73.987715));\nroutingManager->calculateRoute(request);\n```\n\n立即构建并运行项目，您应该会看到如下结果:\n\n![](img/849cb2c6-e347-4f25-b95e-743695a488fc.png)\n\n这里有另一个对你来说具有挑战性的任务——尝试将所有这些坐标放入一个数组中，并创建一个`addLine()`函数，该函数接受数组并绘制一系列代表地理路由服务描述的路线的直线。\n\n自全球定位导航系统发明以来，地理路由一直是最重要的特征之一。希望在完成教程后，您能够创建一些有用的东西！\n\n# 摘要\n\n在本章中，我们学习了如何创建自己的类似于谷歌地图的地图视图。我们已经学习了如何创建地图显示，在地图上放置标记和形状，最后找到用户的位置。请注意，您也可以使用网络视图并调用谷歌的 JavaScript 映射应用编程接口来创建类似的地图显示。然而，使用 QML 要简单得多，轻便得多(我们不必仅仅为了使用地图而加载整个网络引擎模块)，在移动和触摸屏上工作得非常好，并且它也可以轻松地移植到其他地图服务中。希望你能利用这些知识，创造一些真正令人敬畏和有用的东西。\n\n在下一章中，我们将研究如何使用图形项目显示信息。让我们继续前进！"
  },
  {
    "path": "docs/cpp-gui-prog-qt5/08.md",
    "content": "# 八、图形视图\n\n在前一章中，我们通过在地图上显示坐标数据，了解了视觉呈现对用户的重要性。在本章中，我们将进一步探讨使用 Qt 的 Graphics View 框架来表示图形数据的可能性。\n\n在本章中，我们将涵盖以下主题:\n\n*   图形视图框架\n*   可移动图形项目\n*   创建组织结构图\n\n在本章的最后，您将能够使用 C++ 和 Qt 的 API 创建组织结构图显示。我们开始吧！\n\n# 图形视图框架\n\n图形视图框架是 Qt 中小部件模块的一部分，因此默认情况下已经支持它，除非您正在运行 Qt 控制台应用，该应用不需要小部件模块。\n\nQt 中的 Graphics View 视图的工作原理非常像白板，您可以使用 C/C++ 代码在上面绘制任何东西，例如绘制形状、线条、文本甚至图像。对于初学者来说，这一章可能有点难理解，但这绝对是一个有趣的项目。我们开始吧！\n\n# 设置新项目\n\n首先，创建一个新的 Qt Widgets 应用项目。之后，打开`mainwindow.ui`，将图形视图小部件拖放到主窗口，如下所示:\n\n![](img/01a52e3d-f0ef-4e70-b7dd-c390e5edc2b1.png)\n\n然后，通过单击画布顶部的“垂直布局”按钮，为图形视图创建布局。之后，打开`mainwindow.h`并添加以下标题和变量:\n\n```cpp\n#include <QGraphicsScene> \n#include <QGraphicsRectItem> \n#include <QGraphicsEllipseItem> \n#include <QGraphicsTextItem> \n#include <QBrush> \n#include <QPen> \n\nprivate:\n  Ui::MainWindow *ui;\n  QGraphicsScene* scene;\n```\n\n之后，打开`mainwindow.cpp`。打开后，添加以下代码:\n\n```cpp\nMainWindow::MainWindow(QWidget *parent) : \n   QMainWindow(parent), \n   ui(new Ui::MainWindow) \n{ \n   ui->setupUi(this); \n\n   scene = new QGraphicsScene(this); \n   ui->graphicsView->setScene(scene); \n\n   QBrush greenBrush(Qt::green); \n   QBrush blueBrush(Qt::blue); \n   QPen pen(Qt::black); \n   pen.setWidth(2); \n\n   QGraphicsRectItem* rectangle = scene->addRect(80, 0, 80, 80, pen, greenBrush); \n   QGraphicsEllipseItem* ellipse = scene->addEllipse(0, -80, 200, 60, pen, blueBrush); \n   QGraphicsTextItem* text = scene->addText(\"Hello World!\", QFont(\"Times\", 25)); \n} \n```\n\n现在构建并运行程序，您应该会看到如下内容:\n\n![](img/8b6246e1-dedd-4df3-b865-75e323337c8a.png)\n\n代码有点长，所以让我来解释一下它是做什么的，以及它是如何把图形绘制到屏幕上的。\n\n正如我之前所说的，图形视图小部件就像一个画布或白板，允许你在上面画任何你想要的东西。然而，我们还需要一种叫做图形场景的东西，它本质上是一个场景图，在图形视图上显示图形组件之前，它以父子层次结构存储所有图形组件。场景图层次是上一张截图中出现的图像，其中每个对象都有一个或多个链接在一起的父对象或子对象:\n\n![](img/25c1c1d4-4bba-4b7d-9fe3-ec7e109bfcbb.png)\n\n在前面的代码中，我们首先创建了一个`QGraphicsScene`对象，并将其设置为图形视图小部件的图形场景:\n\n```cpp\nscene = new QGraphicsScene(this); \nui->graphicsView->setScene(scene); \n```\n\n但是，在本例中，我们不必将图形项目链接在一起，因此我们将独立创建它们，如下所示:\n\n```cpp\nQBrush greenBrush(Qt::green); \n...\nQGraphicsTextItem* text = scene->addText(\"Hello World!\", QFont(\"Times\", 25)); \n```\n\n`QPen`和`QBrush`类用于定义这些图形项的渲染样式。`QBrush`通常用于定义物品的背景颜色和图案，而`QPen`通常会影响物品的轮廓。\n\nQt 为最常见的形状提供了许多类型的图形项目，包括:\n\n*   `QGraphicsEllipseItem`–椭圆项目\n*   `QGraphicsLineItem`–行项目\n*   `QGraphicsPathItem`–任意路径项\n*   `QGraphicsPixmapItem` – 像素地图项目\n*   `QGraphicsPolygonItem`多边形项目\n*   `QGraphicsRectItem`–矩形项目\n*   `QGraphicsSimpleTextItem`–简单文本标签项目\n*   `QGraphicsTextItem`–高级格式化文本项目\n\nFor more information, please visit this link: [http://doc.qt.io/archives/qt-5.8/qgraphicsitem.html#details.](http://doc.qt.io/archives/qt-5.8/qgraphicsitem.html#details)\n\n# 可移动图形项目\n\n在前面的示例中，我们已经成功地在图形视图小部件上绘制了一些简单的形状和文本。然而，这些图形项目不是交互式的，因此不适合我们的目的。我们想要的是一个交互式组织结构图，用户可以用鼠标移动项目。让这些物品在 Qt 下移动其实真的很容易；让我们看看如何通过继续我们以前的项目来做到这一点。\n\n首先，确保您没有更改我们的图形视图小部件的默认交互属性，该属性设置为启用(复选框已选中):\n\n![](img/01e7d066-00f9-4c4e-ae4b-9352ecf34437.png)\n\n然后，在前面`Hello World`示例中我们刚刚创建的每个图形项下面添加以下代码:\n\n```cpp\nQGraphicsRectItem* rectangle = scene->addRect(80, 0, 80, 80, pen, greenBrush); \nrectangle->setFlag(QGraphicsItem::ItemIsMovable); \nrectangle->setFlag(QGraphicsItem::ItemIsSelectable); \n\nQGraphicsEllipseItem* ellipse = scene->addEllipse(0, -80, 200, 60, pen, blueBrush); \nellipse->setFlag(QGraphicsItem::ItemIsMovable); \nellipse->setFlag(QGraphicsItem::ItemIsSelectable); \n\nQGraphicsTextItem* text = scene->addText(\"Hello World!\", QFont(\"Times\", 25)); \ntext->setFlag(QGraphicsItem::ItemIsMovable); \ntext->setFlag(QGraphicsItem::ItemIsSelectable); \n```\n\n再次构建并运行程序，这一次您应该能够在图形视图中选择并移动项目。一定要注意`ItemIsMovable`和`ItemIsSelectable`都给你一个不同的行为——前者标志将使项目可通过鼠标移动，后者使项目可选择，这通常在选择时使用虚线轮廓给它一个视觉指示。每个标志都独立工作，不会影响其他标志。\n\n我们可以利用 Qt 中的信号和时隙机制来测试`ItemIsSelectable`标志的效果。让我们回到我们的代码，并添加以下行:\n\n```cpp\nui->setupUi(this); \nscene = new QGraphicsScene(this); \nui->graphicsView->setScene(scene); \nconnect(scene, &QGraphicsScene::selectionChanged, this, &MainWindow::selectionChanged); \n```\n\n只要您在图形视图小部件上选择了一个项目，就会触发`selectionChanged()`信号，然后调用我们的`MainWindow`类下的`selectionChanged()`槽函数(我们需要编写该函数)。让我们打开`mainwindow.h`并添加另一个显示调试消息的标题:\n\n```cpp\n#include <QDebug> \n```\n\n然后，我们声明插槽函数，如下所示:\n\n```cpp\nprivate: \n   Ui::MainWindow *ui; \n\npublic slots: \n void selectionChanged(); \n```\n\n之后打开`mainwindow.cpp`并定义槽功能，如下图:\n\n```cpp\nvoid MainWindow::selectionChanged() \n{ \n   qDebug() << \"Item selected\"; \n} \n```\n\n现在尝试再次运行该程序；您应该会看到一行调试消息，显示“项目选择”,每当单击一个图形项目时，就会出现该消息。真的很简单，不是吗？\n\n至于`ItemIsMovable`标志，我们无法使用信号和时隙方法进行测试。这是因为从`QGraphicsItem`类继承的所有类都不是从`QObject`类继承的，因此信号和槽机制在这些类上不起作用。这是 Qt 开发人员有意为之的，目的是使其轻量级，从而提高性能，尤其是在屏幕上呈现成千上万个项目时。\n\n即使信号和插槽不是这个选项，我们仍然可以使用事件系统，这需要覆盖`itemChange()`虚拟功能，我将在下一节中演示。\n\n# 创建组织结构图\n\n让我们继续学习如何使用图形视图创建组织结构图。组织结构图是显示组织结构及其员工职位关系层次的图表。使用图形表示很容易理解公司的结构；因此，最好使用图形视图，而不是表格。\n\n这一次，我们需要为图形项目创建我们自己的类，这样我们就可以利用 Qt 的事件系统，并对它的分组和显示有更多的控制。\n\n首先，通过转到文件|新文件或项目来创建一个 C/C++ 类:\n\n![](img/a86a053b-8bab-4827-b081-a2858e1b1d66.png)\n\n接下来，在单击下一步和完成按钮之前，将我们的班级命名为`profileBox`:\n\n![](img/e257c658-4e37-45d9-a89a-e0ef788161b7.png)\n\n之后，打开`mainwindow.h`并添加这些标题:\n\n```cpp\n#include <QWidget> \n#include <QDebug> \n#include <QBrush> \n#include <QPen> \n#include <QFont> \n#include <QGraphicsScene> \n#include <QGraphicsItemGroup> \n#include <QGraphicsItem> \n#include <QGraphicsRectItem> \n#include <QGraphicsTextItem> \n#include <QGraphicsPixmapItem> \n```\n\n然后，打开`profilebox.h`，让我们的`profileBox`类改为继承`QGraphicsItemGroup`:\n\n```cpp\nclass profileBox : public QGraphicsItemGroup \n{ \npublic: \n   explicit profileBox(QGraphicsItem* parent = nullptr); \n```\n\n之后打开`profilebox.cpp`，在类的构造器处，设置`QBrush`、`QPen`和`QFont`，一会用于渲染:\n\n```cpp\nprofileBox::profileBox(QGraphicsItem *parent) : QGraphicsItemGroup(parent) \n{ \n   QBrush brush(Qt::white); \n   QPen pen(Qt::black); \n   QFont font; \n   font.setFamily(\"Arial\"); \n   font.setPointSize(12); \n} \n```\n\n之后，同样在构造器中，创建一个`QGraphicsRectItem`、`QGraphicsTextItem`和一个`QGraphicsPixmapItem`:\n\n```cpp\nQGraphicsRectItem* rectangle = new QGraphicsRectItem(); \nrectangle->setRect(0, 0, 90, 100); \nrectangle->setBrush(brush); \nrectangle->setPen(pen); \n\nnameTag = new QGraphicsTextItem(); \nnameTag->setPlainText(\"\"); \nnameTag->setFont(font); \n\nQGraphicsPixmapItem* picture = new QGraphicsPixmapItem(); \nQPixmap pixmap(img/person-icon-blue.png\"); \npicture->setPixmap(pixmap); \npicture->setPos(15, 30); \n```\n\n然后，将这些项目添加到组中，该组是当前类，因为该类继承自`QGraphicsItemGroup`类:\n\n```cpp\nthis->addToGroup(rectangle); \nthis->addToGroup(nameTag); \nthis->addToGroup(picture); \n```\n\n最后，为当前类设置三个标志，分别是`ItemIsMovable`、`ItemIsSelectable`和`ItemSendsScenePositionChanges`:\n\n```cpp\nthis->setFlag(QGraphicsItem::ItemIsMovable); \nthis->setFlag(QGraphicsItem::ItemIsSelectable); \nthis->setFlag(QGraphicsItem::ItemSendsScenePositionChanges); \n```\n\n这些标志非常重要，因为出于性能原因，默认情况下它们都是禁用的。上一节我们已经介绍了`ItemIsMovable`和`ItemIsSelectable`，而`ItemSendsPositionChanges`则是新内容。当用户移动图形项目时，此标志会使图形项目通知图形场景，因此得名。\n\n接下来，创建另一个名为`init()`的函数来设置员工档案。为了简单起见，我们只设置员工姓名，但是，如果您愿意，您可以做更多的事情，例如根据级别设置不同的背景颜色，或者更改他们的个人资料图片:\n\n```cpp\nvoid profileBox::init(QString name, MainWindow *window, QGraphicsScene* scene) \n{ \n   nameTag->setPlainText(name); \n   mainWindow = window; \n   scene->addItem(this); \n} \n```\n\n请注意，我们还在这里设置了主窗口和图形场景指针，以便以后使用。我们必须将`QGraphicsItem`添加到场景中，然后它才会在屏幕上呈现。在这种情况下，我们将所有图形项目分组到一个`QGraphicsItemGroup`中，因此我们只需要将该组添加到场景中，而不是单个项目。\n\n请注意，您必须在`#include \"mainwindow.h\"`之后为`profilebox.h`中的`MainWindow`类进行前向声明，以避免出现递归头包含的错误。同时，我们还在`profilebox.h`中放置了`MainWindow`和`QGraphicsTextItem`指针，以便以后调用:\n\n```cpp\n#include \"mainwindow.h\" \n\nclass MainWindow; \n\nclass profileBox : public QGraphicsItemGroup \n{ \npublic: \n   explicit profileBox(QGraphicsItem* parent = nullptr); \n   void init(QString name, MainWindow* window, QGraphicsScene* scene); \n\nprivate: \n   MainWindow* mainWindow; \n   QGraphicsTextItem* nameTag; \n\n```\n\n您还会注意到，我在`QGraphicsPixmapItem`中使用了一个图标作为装饰图标:\n\n![](img/9787b7b9-c914-42cd-a823-622a852bea88.png)\n\n该图标是存储在资源文件中的一个巴布亚新几内亚图像。您可以从我们 GitHub 页面上的示例项目文件中获取此图像:[http://GitHub . com/PacktPublishing/hand-GUI-Programming-with-C-QT5](http://github.com/PacktPublishing/Hands-On-GUI-Programming-with-C-QT5)\n\n让我们为您的项目创建一个资源文件。转到文件|新文件或项目，并选择 Qt 类别下的 Qt 资源文件选项:\n\n![](img/b48d892f-781d-4781-a7d1-2548d6d5dca4.png)\n\n创建空资源文件后，通过转到添加|添加前缀来添加新前缀。我们将这个前缀称为`images`:\n\n![](img/8a5addd2-28d8-4bb3-b595-9acd7b2a0531.png)\n\n然后，选择新创建的`images`前缀，点击添加|添加文件。将图标图像添加到资源文件并保存。您现在已经成功地将图像添加到项目中。\n\n![](img/1803de41-0a6d-4761-bf75-a9da8ff984bf.png)\n\nIf your prefix name or filename is different than the prefix name or filename in this book, you may right-click on your image in the resource file and select Copy Resource Path to Clipboard and replace the one in the code with your path.\n\n![](img/27bfcbda-33d3-4330-8ed7-33cd8082e990.png)\n\n之后，打开`mainwindow.h`并添加:\n\n```cpp\n#include \"profilebox.h\"\n```\n\n然后，打开`mainwindow.cpp`并添加以下代码，手动创建一个剖面框:\n\n```cpp\nMainWindow::MainWindow(QWidget *parent) : \n   QMainWindow(parent), \n   ui(new Ui::MainWindow) \n{ \n   ui->setupUi(this); \n\n   scene = new QGraphicsScene(this); \n   ui->graphicsView->setScene(scene); \n\n   connect(scene, &QGraphicsScene::selectionChanged, this, &MainWindow::selectionChanged); \n\n   profileBox* box = new profileBox(); \n   box->init(\"John Doe\", this, scene); \n} \n```\n\n现在构建并运行项目，您应该会看到如下内容:\n\n![](img/af8e4e65-97ea-4571-a6f0-b00a43980191.png)\n\n看起来整洁；但是我们还远没有完成。还有一些事情要做——我们必须允许用户通过用户界面添加或删除配置文件框，而不是使用代码。同时，我们还需要添加连接不同配置文件框的线条，以展示不同员工之间的关系以及他们在公司中的位置。\n\n让我们从简单的部分开始。再次打开`mainwindow.ui`，在图形视图小部件底部添加一个按钮，并命名为`addButton`:\n\n![](img/03f5b860-526e-425f-b3d7-d24c86c84ebd.png)\n\n然后，右键单击按钮并选择转到插槽...之后，选择单击的选项并单击确定。将自动为您创建一个新的插槽功能，称为`on_addButton_clicked()`。添加以下代码，以允许用户在单击“添加”按钮时创建配置文件框:\n\n```cpp\nvoid MainWindow::on_addButton_clicked() \n{ \n   bool ok; \n   QString name = QInputDialog::getText(this, tr(\"Employee Name\"), \n   tr(\"Please insert employee's full name here:\"), QLineEdit::Normal,  \n   \"John Doe\", &ok); \n   if (ok && !name.isEmpty()) \n   { \n         profileBox* box = new profileBox(); \n         box->init(name, this, scene); \n   } \n} \n```\n\n用户现在可以通过单击添加按钮轻松创建任意数量的配置文件框，而不是使用代码创建每个配置文件框。还会出现一个消息框，让用户在创建配置文件框之前输入员工姓名:\n\n![](img/7f6b3cf5-f348-49a5-a43e-71172aab6166.png)\n\n接下来，我们将创建另一个名为`profileLine`的类。这次我们就让这个类继承`QGraphicsLineItem`。`profileline.h`基本上是这样的:\n\n```cpp\n#include <QWidget> \n#include <QGraphicsItem> \n#include <QPen> \n\nclass profileLine : public QGraphicsLineItem \n{ \npublic: \n   profileLine(QGraphicsItem* parent = nullptr); \n   void initLine(QGraphicsItem* start, QGraphicsItem* end); \n   void updateLine(); \n\n   QGraphicsItem* startBox; \n   QGraphicsItem* endBox; \n\nprivate: \n}; \n```\n\n类似于`profileBox`类，我们还为`profileLine`类创建了一个`init`函数，叫做`initLine()`函数。该函数接收两个`QGraphicsItem`对象作为渲染线的起点和终点。除此之外，我们还创建了一个`updateLine()`函数，用于在轮廓框移动时重新绘制线条。\n\n接下来，打开`profileline.cpp`并向构造函数添加以下代码:\n\n```cpp\nprofileLine::profileLine(QGraphicsItem *parent) : QGraphicsLineItem(parent) \n{ \n   QPen pen(Qt::black); \n   pen.setWidth(2); \n   this->setPen(pen); \n\n   this->setZValue(-999); \n} \n```\n\n我们用`QPen`设置线条的颜色为黑色，宽度为`2`。之后，我们还将该行的`Zvalue`设置为`-999`，这样它将始终保留在轮廓框的后面。\n\n之后，将以下代码添加到我们的`initLine()`函数中，使其看起来像这样:\n\n```cpp\nvoid profileLine::initLine(QGraphicsItem* start, QGraphicsItem* end) \n{ \n   startBox = start; \n   endBox = end; \n\n   updateLine(); \n} \n```\n\n它所做的基本上是为它设置盒子来定位它的起点和终点。之后，调用`updateLine()`函数渲染线条。\n\n最后，`updateLine()`功能如下:\n\n```cpp\nvoid profileLine::updateLine() \n{ \n   if (startBox != NULL && endBox != NULL) \n   { \n         this->setLine(startBox->pos().x() + startBox->boundingRect().width() / 2, startBox->pos().y() + startBox->boundingRect().height() / 2, endBox->pos().x() + endBox->boundingRect().width() / 2, endBox->pos().y() + endBox->boundingRect().height() / 2); \n   } \n} \n```\n\n前面的代码看起来有点复杂，但如果我这样说，它真的很简单:\n\n```cpp\nthis->setLine(x1, y1, x2, y2); \n```\n\n数值`x1`和`y1`基本上是第一个轮廓盒的中心位置，而`x2`和`y2`是第二个轮廓盒的中心位置。由于我们通过调用`pos()`得到的位置值是从左上角开始的，所以我们必须得到轮廓框的边界尺寸，并将其除以 2 以得到它的中心位置。然后，将该值添加到左上角位置，使其偏移到中心。\n\n完成后，让我们再次打开`mainwindow.cpp`并将以下代码添加到`on_addButton_clicked()`功能中:\n\n```cpp\nvoid MainWindow::on_addButton_clicked() \n{ \n   bool ok; \n   QString name = QInputDialog::getText(this, tr(\"Employee Name\"), tr(\"Please insert employee's full name here:\"), QLineEdit::Normal, \"John Doe\", &ok); \n   if (ok && !name.isEmpty()) \n   { \n         profileBox* box = new profileBox(); \n         box->init(name, this, scene); \n\n         if (scene->selectedItems().size() > 0) \n         { \n               profileLine* line = new profileLine(); \n               line->initLine(box, scene->selectedItems().at(0)); \n               scene->addItem(line); \n\n               lines.push_back(line); \n         } \n   } \n} \n```\n\n在前面的代码中，我们检查用户是否选择了任何配置文件框。如果没有，我们不需要创建任何线。否则，创建一个新的`profileLine`对象，并将新创建的轮廓框和当前选择的轮廓框设置为`startBox`和`endBox`属性。\n\n之后，将该行添加到我们的图形场景中，这样它就会出现在屏幕上。最后，将这个`profileLine`对象存储到一个`QList`数组中，以便我们以后使用。`mainwindow.h`中的数组声明如下:\n\n```cpp\nprivate: \n   Ui::MainWindow *ui; \n   QGraphicsScene* scene; \n   QList<profileLine*> lines; \n```\n\n立即构建并运行项目。通过点击`Add`按钮，输入名称，并在第一个框保持选中的情况下选择确定，您应该能够看到创建第二个配置文件框时出现的线条。但是，每当您将配置文件框从其原始位置移开时，您可能会注意到一个问题——这些行根本不会自动更新！：\n\n![](img/115bd268-e78e-49b4-95d5-f58d962cd051.png)\n\n这就是我们将这些行放入`QList`数组的主要原因，这样每当用户移动概要文件框时，我们就可以更新这些行。\n\n为此，首先，我们需要覆盖名为`itemChanged()`的`profileBox`类中的虚拟函数。让我们打开`profilebox.h`并添加以下代码行:\n\n```cpp\nclass profileBox : public QGraphicsItemGroup \n{ \npublic: \n   explicit profileBox(QGraphicsItem* parent = nullptr); \n   void init(QString name, MainWindow* window, QGraphicsScene* scene); \n   QVariant itemChange(GraphicsItemChange change, const QVariant \n   &value) override; \n```\n\n然后，打开`profilebox.cpp`并添加`itemChanged()`的代码:\n\n```cpp\nQVariant profileBox::itemChange(GraphicsItemChange change, const QVariant &value) \n{ \n   if (change == QGraphicsItem::ItemPositionChange) \n   { \n         qDebug() << \"Item moved\"; \n\n         mainWindow->updateLines(); \n   } \n\n   return QGraphicsItem::itemChange(change, value); \n} \n```\n\n`itemChanged()`函数是`QGraphicsItem`类中的一个虚拟函数，当图形项发生变化时，无论是位置变化、可见性变化、父项变化、选择变化等等，Qt 的事件系统都会自动调用这个函数。\n\n因此，我们所需要做的就是覆盖该函数，并向该函数添加我们自己的自定义行为。在前面的示例代码中，我们所做的只是在主窗口类中调用`updateLines()`函数。\n\n接下来，打开`mainwindow.cpp`并定义`updateLines()`功能。顾名思义，这个函数要做的是遍历存储在线数组中的所有剖面线对象，并更新其中的每一个，如下所示:\n\n```cpp\nvoid MainWindow::updateLines() \n{ \n   if (lines.size() > 0) \n   { \n         for (int i = 0; i < lines.size(); i++) \n         { \n               lines.at(i)->updateLine(); \n         } \n   } \n} \n```\n\n完成后，再次构建并运行项目。这一次，您应该能够创建一个组织结构图，如下所示:\n\n![](img/72762a59-c68a-4e39-8bb7-097924dd8425.png)\n\n这只是一个更简单的版本，向您展示了如何利用 Qt 强大的图形视图系统来显示一组数据的图形表示，普通人很容易理解这些数据。\n\n在完成之前的最后一件事——我们还没有讨论如何删除配置文件框。其实挺简单的，我们打开`mainwindow.h`添加`keyReleaseEvent()`功能，看起来是这样的:\n\n```cpp\npublic: \n   explicit MainWindow(QWidget *parent = 0); \n   ~MainWindow(); \n\n   void updateLines(); \n   void keyReleaseEvent(QKeyEvent* event); \n```\n\n当键盘按钮被按下和释放时，Qt 的事件系统也会自动调用这个虚拟函数。功能的内容在`mainwindow.cpp`中是这样的:\n\n```cpp\nvoid MainWindow::keyReleaseEvent(QKeyEvent* event) \n{ \n   qDebug() << \"Key pressed: \" + event->text(); \n\n   if (event->key() == Qt::Key_Delete) \n   { \n         if (scene->selectedItems().size() > 0) \n         { \n               QGraphicsItem* item = scene->selectedItems().at(0); \n               scene->removeItem(item); \n\n               for (int i = lines.size() - 1; i >= 0; i--) \n               { \n                     profileLine* line = lines.at(i); \n\n                     if (line->startBox == item || line->endBox == \n                     item) \n                     { \n                           lines.removeAt(i); \n                           scene->removeItem(line); \n                           delete line; \n                     } \n               } \n               delete item; \n         } \n   } \n} \n```\n\n在这个功能中，我们首先检测用户正在按下的键盘按钮。如果按钮是`Qt::Key_Delete (delete button)`，那么我们将通过检查`scene->selectedItems().size()`是否为空来检查用户是否选择了任何配置文件框。如果用户确实选择了配置文件框，则从图形场景中移除该项目。之后，循环通过线阵列，并检查是否有任何轮廓线连接到已删除的轮廓框。从场景中移除任何连接到轮廓框的线，我们就完成了:\n\n![](img/6fb5678c-13c1-4f9e-a849-3abd267b209c.png)\n\n此截图显示了从组织结构图中删除`Jane Smith`档案框的结果。请注意，连接轮廓框的线已被正确移除。这一章就到这里；我希望你发现这很有趣，也许会继续创造比这更好的东西！\n\n# 摘要\n\n在本章中，我们学习了如何使用 Qt 创建一个应用，允许用户轻松创建和编辑组织结构图。我们学习了`QGraphicsScene`、`QGrapicsItem`、`QGraphicsTextItem`、`QGraphicsPixmapItem`等帮助我们在短时间内创建交互式组织结构图的课程。在接下来的一章中，我们将学习如何使用网络摄像头拍摄图像！"
  },
  {
    "path": "docs/cpp-gui-prog-qt5/09.md",
    "content": "# 九、照相机模块\n\n在以越来越大的难度完成了这么多章节之后，让我们为这一章尝试一些更简单、更有趣的东西吧！我们将学习如何通过 Qt 的多媒体模块访问我们的相机并使用它拍照。\n\n在本章中，我们将涵盖以下主题:\n\n*   Qt 多媒体模块\n*   连接到摄像机\n*   将摄像机图像捕获到文件中\n*   将摄像机视频录制到文件中\n\n您可以使用它来创建视频会议应用、安全摄像头系统等。我们开始吧！\n\n# Qt 多媒体模块\n\nQt 中的多媒体模块是处理平台多媒体功能的模块，例如媒体回放以及相机和无线电设备的使用。本模块涵盖了广泛的主题，但我们将只关注本章的相机。\n\n# 设置新项目\n\n首先，创建一个新的 Qt Widgets 应用项目。\n\n然后，我们首先需要做的是打开项目文件(`.pro`)并添加两个关键词— `multimedia`和`multimediawidgets`:\n\n```cpp\nQT += core gui multimedia multimediawidgets \n```\n\n通过检测项目文件中的这些关键词，Qt 将在编译时将多媒体模块和所有与多媒体相关的小部件包含到您的项目中。多媒体模块包括以下四个主要组件:\n\n*   声音的\n*   录像\n*   照相机\n*   收音机\n\n每个组件都包括一系列提供各自功能的类。通过使用这个模块，您不再需要自己实现低级的、特定于平台的代码。让 Qt 为你做这项工作。真的那么简单。\n\n添加完多媒体模块后，我们打开`mainwindow.ui`将水平布局拖放到主窗口，如下图:\n\n![](img/59be5c52-c020-4ae8-8db0-3485497ad386.png)\n\n然后，在我们上一步刚刚添加的水平布局中添加一个标签、组合框(命名为`deviceSelection`)和一个按钮。之后，在组合框和按钮之间添加一个水平间隔，将它们相互推开。完成后，选择中心小部件，然后单击位于工作区上方的垂直布局按钮。\n\n然后，在前一个水平布局的底部添加另一个水平布局，并右键单击它，然后选择变形为| QFrame。之后，将其大小策略(水平策略和垂直策略)设置设置为扩展。参考以下截图:\n\n![](img/afc05a4b-5788-4b1d-ac84-33e5cd81fd92.png)\n\n你的程序的用户界面现在应该是这样的:\n\n![](img/e31cada6-b831-4f34-bad4-3c3096644d80.png)\n\n我们将布局转换为框架的原因是，我们可以将大小策略(水平策略和垂直策略)设置为扩展。然而，如果我们只是从窗口小部件框中添加一个框架窗口小部件(本质上是一个 QFrame)，我们不会在其上获得稍后附加取景器所需的布局组件。\n\n接下来，再次右键单击 QFrame 并选择“更改样式表”。将弹出一个窗口来设置该小部件的样式表。添加以下样式表代码使背景变黑:\n\n![](img/64d7d9eb-31d7-463e-b806-0c13f4de32b4.png)\n\n此步骤是可选的；我们将其背景设为黑色，只是为了指示取景器的位置。完成后，让我们在 QFrame 上方放置另一个水平布局，如下所示:\n\n![](img/bb45ce27-fc97-4962-a84f-f3e7f3cac303.png)\n\n之后，向水平布局添加两个按钮，并添加一个水平间隔以保持它们向右对齐:\n\n![](img/cd4977c9-a179-4245-a431-8f3ef6622ba3.png)\n\n就是这样；我们已经完成了多媒体模块的项目设置，并为接下来的部分很好地布局了用户界面。\n\n# 连接到摄像机\n\n最激动人心的部分来了。我们将学习如何使用 Qt 的多媒体模块访问我们的相机！首先，打开`mainwindow.h`并添加以下标题:\n\n```cpp\n#include <QMainWindow> \n#include <QDebug> \n#include <QCameraInfo> \n#include <QCamera> \n#include <QCameraViewfinder> \n#include <QCameraImageCapture> \n#include <QMediaRecorder> \n#include <QUrl> \n```\n\n接下来，添加以下变量，如下所示:\n\n```cpp\nprivate: \n   Ui::MainWindow *ui; \n   QCamera* camera; \n   QCameraViewfinder* viewfinder; \n   bool connected; \n```\n\n然后，打开`mainwindow.cpp`并在类构造函数中添加以下代码来初始化`QCamera`对象。然后，我们使用`QCameraInfo`类检索连接的摄像机列表，并在组合框小部件中填写该信息:\n\n```cpp\nMainWindow::MainWindow(QWidget *parent) : \n   QMainWindow(parent), \n   ui(new Ui::MainWindow) \n{ \n   ui->setupUi(this); \n\n   connected = false; \n   camera = new QCamera(); \n\n   qDebug() << \"Number of cameras found:\" << QCameraInfo::availableCameras().count(); \n\n   QList<QCameraInfo> cameras = QCameraInfo::availableCameras(); \n   foreach (const QCameraInfo &cameraInfo, cameras) \n   { \n         qDebug() << \"Camera info:\" << cameraInfo.deviceName() << \n         cameraInfo.description() << cameraInfo.position(); \n\n         ui->deviceSelection->addItem(cameraInfo.description()); \n   } \n} \n```\n\n让我们现在构建并运行该项目。之后，检查计算机上检测到的任何摄像机的调试输出。已经检测到的摄像机也应该显示在下拉框中。如果您运行的笔记本电脑带有支持的摄像头，您应该会看到它的列表。如果您运行的系统没有内置摄像头，那么调试输出可能不会显示任何内容，下拉框也将保持为空。如果是这种情况，请尝试插入一个便宜的 USB 摄像头，然后再次运行该程序:\n\n![](img/653beee6-57b1-4705-8987-5387abe142b4.png)\n\n之后，打开`mainwindow.ui`，右键点击连接按钮，选择转到插槽....选择`clicked()`选项，点击确定。Qt Creator 会自动为你创建一个`slot`功能；将以下代码添加到函数中，如下所示:\n\n```cpp\nvoid MainWindow::on_connectButton_clicked() \n{ \n   if (!connected) \n   { \n         connectCamera(); \n   } \n   else \n   { \n         camera->stop(); \n         viewfinder->deleteLater(); \n         ui->connectButton->setText(\"Connect\"); \n         connected = false; \n   } \n} \n```\n\n当点击连接按钮时，我们首先通过检查`connect`变量来检查`camera`是否已经连接。如果还没有连接，我们运行`connectCamera()`函数，我们将在下一步中定义该函数。如果摄像机已经连接，我们停止`camera`，删除`viewfinder`，并将连接按钮的文本设置为`Connect`。最后，将`connected`变量设置为`false`。请注意，我们这里使用的是`deleteLater()`而不是`delete()`，这是删除内存指针的推荐方式。`deleteLater()`是对一个生活在没有运行事件循环的线程中的对象调用的，该对象将在线程结束时被销毁。\n\n接下来，我们将在我们的`MainWindow`类中添加一个名为`connectCamera()`的新函数。该函数看起来像下面的代码块:\n\n```cpp\nvoid MainWindow::connectCamera() \n{ \n   QList<QCameraInfo> cameras = QCameraInfo::availableCameras(); \n   foreach (const QCameraInfo &cameraInfo, cameras) \n   { \n         qDebug() << cameraInfo.description() << ui->deviceSelection-\n         >currentText(); \n\n         if (cameraInfo.description() == ui->deviceSelection- \n         >currentText()) \n         { \n               camera = new QCamera(cameraInfo); \n               viewfinder = new QCameraViewfinder(this); \n               camera->setViewfinder(viewfinder); \n               ui->webcamLayout->addWidget(viewfinder); \n\n               connected = true; \n               ui->connectButton->setText(\"Disconnect\"); \n\n               camera->start(); \n\n               return; \n         } \n   } \n} \n```\n\n在`connectCamera()`功能中，我们重复我们在构建中所做的，并获得当前连接的摄像机列表。然后，我们在列表中循环，并将摄像机的名称(存储在`description`变量中)与组合框小部件上当前选择的设备名称进行比较。\n\n如果有一个匹配的名称，这意味着用户打算连接到那个特定的摄像机，因此我们将通过初始化一个`QCamera`对象和一个新的`QCameraViewFinder`对象来连接到那个摄像机。然后我们将`viewfinder`链接到`camera`并将`viewfinder`添加到黑色背景的布局中。然后，我们将`connected`变量设置为`true`，并将连接按钮的文本设置为`Disconnect`。最后，调用`start()`功能开始运行摄像头。\n\n立即构建并运行项目。选择要连接的摄像机，然后单击“连接”按钮。您应该能够连接到相机，并在程序中看到自己:\n\n![](img/c6d70bb6-30f2-426f-815c-a92e80f674e0.png)\n\n如果您的相机无法连接，请执行以下步骤来显示操作系统返回的任何错误。首先，打开`mainwindow.h`并加入以下`slot`功能:\n\n```cpp\nprivate slots: \n   void cameraError(QCamera::Error error); \n```\n\n之后，打开`mainwindow.cpp`并在`connectCamera()`功能中添加以下代码，将`error()`信号连接到`cameraError()`、`slot`功能:\n\n```cpp\nvoid MainWindow::connectCamera() \n{ \n   QList<QCameraInfo> cameras = QCameraInfo::availableCameras(); \n   foreach (const QCameraInfo &cameraInfo, cameras) \n   { \n         qDebug() << cameraInfo.description() << ui->deviceSelection-\n         >currentText(); \n\n         if (cameraInfo.description() == ui->deviceSelection-\n         >currentText()) \n         { \n               camera = new QCamera(cameraInfo); \n               viewfinder = new QCameraViewfinder(this); \n               camera->setViewfinder(viewfinder); \n               ui->webcamLayout->addWidget(viewfinder); \n\n               connect(camera, SIGNAL(error(QCamera::Error)), this, \n               SLOT(cameraError(QCamera::Error))); \n\n               connected = true; \n               ui->connectButton->setText(\"Disconnect\"); \n\n               camera->start(); \n\n               return; \n         } \n   } \n} \n```\n\n`cameraError()`槽功能如下:\n\n```cpp\nvoid MainWindow::cameraError(QCamera::Error error) \n{ \n   qDebug() << \"Camera error:\" << error; \n\n   connected = false; \n   camera->stop(); \n   ui->connectButton->setText(\"Connect\"); \n} \n```\n\n在前面的代码中，我们显示了错误消息，并确保摄像机已经完全停止，以防万一。通过查看错误消息，您应该能够更容易地调试问题。\n\n# 将摄像机图像捕获到文件中\n\n在前一节中，我们已经学习了如何使用 Qt 的多媒体模块连接到我们的相机。现在，我们将尝试从相机中捕获静止图像，并将其保存到 JPEG 文件中。用 Qt 其实非常非常简单。\n\n首先，打开`mainwindow.h`并添加以下变量:\n\n```cpp\nprivate: \n   Ui::MainWindow *ui; \n   QCamera* camera; \n   QCameraViewfinder* viewfinder; QCameraImageCapture* imageCapture; bool connected; \n```\n\n然后，右键单击`mainwindow.ui`中的捕获按钮，并选择转到插槽....然后，选择`clicked()`并按确定。现在，将在`mainwindow.cpp`中为您创建一个新的`slot`功能。添加以下代码以从相机捕获图像:\n\n```cpp\nvoid MainWindow::on_captureButton_clicked() \n{ \n   if (connected) \n   { \n         imageCapture = new QCameraImageCapture(camera); \n         camera->setCaptureMode(QCamera::CaptureStillImage); \n         camera->searchAndLock(); \n         imageCapture->capture(qApp->applicationDirPath()); \n         camera->unlock(); \n   } \n} \n```\n\n我们在前面的代码中所做的基本上是创建一个新的`QCameraImageCapture`对象，并将其媒体对象设置为活动摄像机。然后，将其捕捉模式设置为静止图像。在我们要求`QCameraImageCapture`对象捕捉图像之前，我们必须锁定相机，以便在捕捉图像的过程中设置保持不变。成功拍摄图像后，您可以通过调用`camera->unlock()`来解锁。\n\n我们使用`qApp->applicationDirPath()`获取应用目录，这样图像将与可执行文件一起保存。您可以将其更改为您想要的任何目录。您也可以将所需的文件名放在目录路径后面；否则，它将使用以`IMG_00000001.jpg`、`IMG_00000002.jpg`等开头的默认文件名格式依次保存图像。\n\n# 将摄像机视频录制到文件中\n\n在我们学习了如何从相机中捕捉静止图像之后，让我们继续学习如何录制视频。首先，打开`mainwindow.h`并添加以下变量:\n\n```cpp\nprivate: \n   Ui::MainWindow *ui; \n   QCamera* camera; \n   QCameraViewfinder* viewfinder; \n   QCameraImageCapture* imageCapture; \n   QMediaRecorder* recorder; \n\n   bool connected; \n   bool recording; \n```\n\n接下来，再次打开`mainwindow.ui`，右键点击记录按钮。选择转到插槽...从菜单中选择`clicked()`选项，然后点击确定按钮。将为您创建一个`slot`功能；然后将以下代码添加到`slot`功能中:\n\n```cpp\nvoid MainWindow::on_recordButton_clicked() \n{ \n   if (connected) \n   { \n         if (!recording) \n         { \n               recorder = new QMediaRecorder(camera); \n               camera->setCaptureMode(QCamera::CaptureVideo); \n               recorder->setOutputLocation(QUrl(qApp-\n               >applicationDirPath())); \n               recorder->record(); \n               recording = true; \n         } \n         else \n         { \n               recorder->stop(); \n               recording = false; \n         } \n   } \n} \n```\n\n这次我们用一个`QMediaRecorder`来代替录制视频。在调用`recorder->record()`之前，我们还必须将相机的拍摄模式设置为`QCamera::CaptureVideo`。\n\n要检查媒体记录器在记录阶段产生的错误信息，您可以将媒体记录器的`error()`信号连接到如下的`slot`功能:\n\n```cpp\nvoid MainWindow::on_recordButton_clicked() \n{ \n   if (connected) \n   { \n         if (!recording) \n         { \n               recorder = new QMediaRecorder(camera); \n               connect(recorder, SIGNAL(error(QMediaRecorder::Error)), \n               this, SLOT(recordError(QMediaRecorder::Error))); \n               camera->setCaptureMode(QCamera::CaptureVideo); \n               recorder->setOutputLocation(QUrl(qApp-\n               >applicationDirPath())); \n               recorder->record(); \n               recording = true; \n         } \n         else \n         { \n               recorder->stop(); \n               recording = false; \n         } \n   } \n} \n```\n\n然后，只需在`slot`功能中显示错误信息:\n\n```cpp\nvoid MainWindow::recordError(QMediaRecorder::Error error) \n{ \n   qDebug() << errorString(); \n} \n```\n\n请注意，在撰写本章时，`QMediaRecorder`类仅支持在 macOS、Linux、移动平台和 Windows XP 上进行视频录制。它目前在 Windows 8 和 Windows 10 上不起作用，但它将在即将到来的版本之一中移植。主要原因是 Qt 正在使用微软的`DirectShow` API 在 Windows 平台上录制视频，但此后就被 Windows 操作系统弃用了。希望在您阅读这本书的时候，这个特性已经在适用于 Windows 8 和 10 的 Qt 中完全实现了。\n\n如果还没有，你可以使用使用`OpenCV` API 录制视频的第三方插件，比如 **Qt 媒体编码库** ( **QtMEL** ) API，作为临时解决方案。请注意，QtMEL 中使用的代码与我们在本章中展示的代码完全不同。\n\nFor more information about QtMEL, please check out the following link:\n[http://kibsoft.ru](http://kibsoft.ru).\n\n# 摘要\n\n在本章中，我们学习了如何使用 Qt 连接到我们的相机。我们还学习了如何从相机中捕捉图像或录制视频。在下一章中，我们将学习网络模块，并尝试使用 Qt 制作即时消息！"
  },
  {
    "path": "docs/cpp-gui-prog-qt5/10.md",
    "content": "# 十、即时消息\n\n公司软件的一个重要特征是与员工沟通的能力。因此，内部即时消息系统是软件的关键部分。通过在 Qt 中加入网络模块，我们可以很容易地用它创建一个聊天系统。\n\n在本章中，我们将涵盖以下主题:\n\n*   Qt 网络模块\n*   创建即时消息服务器\n*   创建即时消息客户端\n\n使用 Qt 创建一个即时通讯系统比你想象的要容易得多。我们开始吧！\n\n# Qt 网络模块\n\n在下一节中，我们将了解 Qt 网络模块，以及它如何帮助我们通过 TCP 或 UDP 连接协议实现服务器-客户端通信。\n\n# 连接协议\n\nQt 中的网络模块是既提供低级网络功能(如 TCP 和 UDP 套接字)又提供高级网络类(用于 web 集成和网络通信)的模块。\n\n在本章中，我们将使用 **TCP** ( **传输控制协议**)互联网协议来代替 **UDP** ( **用户数据报协议**)协议。主要区别在于，TCP 是一种面向连接的协议，要求所有客户端在能够相互通信之前建立与服务器的连接。\n\n另一方面，UDP 是一种不需要连接的无连接协议。客户端将只发送它需要发送到目的地的任何数据，而不检查数据是否已经被另一端接收到。两种协议各有利弊，但 TCP 更适合我们的示例项目。我们想确保收件人收到每条聊天消息，不是吗？\n\n两种协议的区别如下:\n\n*   TCP:\n    *   面向连接的协议\n    *   适用于要求高可靠性的应用，并且对数据传输时间要求不高\n    *   TCP 的速度比 UDP 慢\n    *   在发送下一个数据之前，要求接收客户端确认收到\n    *   绝对保证传输的数据保持完整，并按照发送的顺序到达\n*   UDP:\n    *   无连接协议\n    *   适合需要快速、高效传输的应用，如游戏和 VOIP\n    *   因为不尝试错误恢复，所以 UDP 比 TCP 轻量级且更快\n    *   也适用于回答来自大量客户端的小查询的服务器\n    *   不能保证发送的数据到达目的地，因为没有跟踪连接，也不需要接收客户端的任何确认\n\n由于我们不采用对等连接方式，我们的聊天系统将需要两个不同的软件——服务器程序和客户端程序。服务器程序将充当中间人(就像邮递员一样)，接收来自所有用户的所有消息，并将它们相应地发送给目标接收者。服务器程序将在服务器机房的一台计算机上与普通用户隔离。\n\n另一方面，客户端程序是所有用户都使用的即时通讯软件。这个程序是安装在用户计算机上的程序。用户可以使用这个客户端程序发送他们的消息，也可以看到其他人发送的消息。我们的消息传递系统的整体架构如下所示:\n\n![](img/26bb7700-45cf-4482-9232-4eb2ce750839.png)\n\n让我们开始设置我们的项目并启用 Qt 的网络模块！对于这个项目，我们将先从服务器程序开始，然后再处理客户端程序。\n\n# 设置新项目\n\n首先，创建一个新的 Qt 控制台应用项目。然后，打开项目文件(`.pro`)并添加以下模块:\n\n```cpp\nQT += core network \nQt -= gui \n```\n\n你应该注意到这个项目没有任何`gui`模块(我们确保它被明确删除)，因为我们不需要任何服务器程序的用户界面。这也是我们选择 Qt 控制台应用而不是通常的 Qt 小部件应用的原因。\n\n实际上，就是这样——您已经成功地将网络模块添加到您的项目中。在下一节中，我们将学习如何为我们的聊天系统创建服务器程序。\n\n# 创建即时消息服务器\n\n在下一节中，我们将学习如何创建一个即时消息服务器来接收用户发送的消息，并将它们重新分发到各自的收件人。\n\n# 创建传输控制协议服务器\n\n在本节中，我们将学习如何创建一个不断监听特定端口的传入消息的 TCP 服务器。为了简单起见，我们将只创建一个全局聊天室，其中每个用户都可以看到聊天室中每个用户发送的消息，而不是带有朋友列表的一对一消息传递系统。一旦你理解了聊天系统是如何工作的，你就可以很容易地把这个系统即兴发挥给后者。\n\n首先，转到文件|新建文件或项目，并在 C++ 类别下选择 C++ 类。然后，将类命名为`server`，选择 QObject 作为基类。在继续创建自定义类之前，请确保选中了包含对象选项。你应该也注意到了`mainwindow.ui`、`mainwindow.h`和`mainwindow.cpp`的缺席。这是因为控制台应用项目中没有用户界面。\n\n一旦创建了服务器类，让我们打开`server.h`并添加以下头、变量和函数:\n\n```cpp\n#ifndef SERVER_H \n#define SERVER_H \n\n#include <QObject> \n#include <QTcpServer> \n#include <QTcpSocket> \n#include <QDebug> \n#include <QVector> \n\nprivate: \n   QTcpServer* chatServer; \n   QVector<QTcpSocket*>* allClients; \n\npublic:\n   explicit server(QObject *parent = nullptr);\n void startServer();\n   void sendMessageToClients(QString message); public slots: void newClientConnection();\n  void socketDisconnected();\n  void socketReadyRead();\n  void socketStateChanged(QAbstractSocket::SocketState state);\n```\n\n接下来，创建一个名为`startServer()`的函数，并将以下代码添加到`server.cpp`中的函数定义中:\n\n```cpp\nvoid server::startServer() \n{ \n   allClients = new QVector<QTcpSocket*>; \n\n   chatServer = new QTcpServer(); \n   chatServer->setMaxPendingConnections(10); \n   connect(chatServer, SIGNAL(newConnection()), this, \n   SLOT(newClientConnection())); \n\n   if (chatServer->listen(QHostAddress::Any, 8001)) \n   { \n         qDebug() << \"Server has started. Listening to port 8001.\"; \n   } \n   else \n   { \n         qDebug() << \"Server failed to start. Error: \" + chatServer-\n         >errorString(); \n   } \n} \n```\n\n我们创建了一个名为`chatServer`的`QTcpServer`对象，并让它不断地监听端口`8001`。您可以选择任何未使用的端口号，范围从`1024`到`49151`。这个范围之外的其他号码通常是为普通系统保留的，比如 HTTP 或 FTP 服务，所以我们最好不要使用它们来避免冲突。我们还创建了一个名为`allClients`的`QVector`数组来存储所有连接的客户端，以便我们以后可以使用它来将传入的消息重定向到所有用户。\n\n我们还使用`setMaxPendingConnections()`功能将最大挂起连接数限制为 10 个客户端。您可以使用此方法将活动客户端的数量保持在特定数量，以便服务器的带宽始终在其限制范围内。这样可以保证良好的服务质量，保持积极的用户体验。\n\n# 倾听客户\n\n每当客户端连接到服务器时，`chatServer`将触发`newConnection()`信号，因此我们将该信号连接到名为`newClientConnection()`的自定义插槽功能。插槽功能如下所示:\n\n```cpp\nvoid server::newClientConnection() \n{ \n   QTcpSocket* client = chatServer->nextPendingConnection(); \n   QString ipAddress = client->peerAddress().toString(); \n   int port = client->peerPort(); \n\n   connect(client, &QTcpSocket::disconnected, this, &server::socketDisconnected); \n   connect(client, &QTcpSocket::readyRead, this, &server::socketReadyRead); \n   connect(client, &QTcpSocket::stateChanged, this, &server::socketStateChanged); \n\n   allClients->push_back(client); \n\n   qDebug() << \"Socket connected from \" + ipAddress + \":\" + QString::number(port); \n} \n```\n\n每一个连接到服务器的新客户端都是一个`QTcpSocket`对象，可以通过调用`nextPendingConnection()`从`QTcpServer`对象中获取。您可以通过分别拨打`peerAddress()`和`peerPort()`获取客户端的信息，如其 IP 地址和端口号。然后，我们将每个新客户端存储到`allClients`阵列中以备将来使用。我们还将客户端的`disconnected()`、`readyRead()`和`stateChanged()`信号连接到其各自的插槽功能。\n\n当客户端与服务器断开连接时，会触发`disconnected()`信号，随后会调用`socketDisconnected()`、`slot`功能。我们在这个函数中所做的只是在服务器控制台上显示消息，仅此而已。您可以在这里做任何您喜欢的事情，比如将用户的离线状态保存到数据库等等。为了简单起见，我们将在控制台窗口上打印出消息:\n\n```cpp\nvoid server::socketDisconnected() \n{ \n   QTcpSocket* client = qobject_cast<QTcpSocket*>(QObject::sender()); \n   QString socketIpAddress = client->peerAddress().toString(); \n   int port = client->peerPort(); \n\n   qDebug() << \"Socket disconnected from \" + socketIpAddress + \":\" + \n   QString::number(port); \n} \n```\n\n接下来，每当客户端向服务器发送消息时，都会触发`readyRead()`信号。我们已经将信号连接到一个名为`socketReadyRead()`的槽函数，它看起来像这样:\n\n```cpp\nvoid server::socketReadyRead() \n{ \n   QTcpSocket* client = qobject_cast<QTcpSocket*>(QObject::sender()); \n   QString socketIpAddress = client->peerAddress().toString(); \n   int port = client->peerPort(); \n\n   QString data = QString(client->readAll()); \n\n   qDebug() << \"Message: \" + data + \" (\" + socketIpAddress + \":\" + \n   QString::number(port) + \")\"; \n\n   sendMessageToClients(data); \n} \n```\n\n在前面的代码中，我们只是将消息重定向到一个名为`sendMessageToClients()`的自定义函数，该函数负责将消息传递给所有连接的客户端。我们一会儿就来看看这个函数是如何工作的。我们使用`QObject::sender()`获取发出`readyRead`信号的对象的指针，并将其转换为`QTcpSocket`类，这样我们就可以访问其`readAll()`功能。\n\n之后，我们还将另一个名为`stateChanged()`的信号连接到`socketStateChanged()`槽功能。慢速功能如下所示:\n\n```cpp\nvoid server::socketStateChanged(QAbstractSocket::SocketState state) \n{ \n   QTcpSocket* client = qobject_cast<QTcpSocket*>(QObject::sender()); \n   QString socketIpAddress = client->peerAddress().toString(); \n   int port = client->peerPort(); \n\n   QString desc; \n\n   if (state == QAbstractSocket::UnconnectedState) \n         desc = \"The socket is not connected.\"; \n   else if (state == QAbstractSocket::HostLookupState) \n         desc = \"The socket is performing a host name lookup.\"; \n   else if (state == QAbstractSocket::ConnectingState) \n         desc = \"The socket has started establishing a connection.\"; \n   else if (state == QAbstractSocket::ConnectedState) \n         desc = \"A connection is established.\"; \n   else if (state == QAbstractSocket::BoundState) \n         desc = \"The socket is bound to an address and port.\"; \n   else if (state == QAbstractSocket::ClosingState) \n         desc = \"The socket is about to close (data may still be \n         waiting to be written).\"; \n   else if (state == QAbstractSocket::ListeningState) \n         desc = \"For internal use only.\"; \n\n   qDebug() << \"Socket state changed (\" + socketIpAddress + \":\" + \n   QString::number(port) + \"): \" + desc; \n} \n```\n\n每当客户端的网络状态发生变化时，例如连接、断开、侦听等，就会触发此功能。我们将简单地根据它的新状态打印出相关的消息，这样我们就可以更容易地调试我们的程序。\n\n现在，让我们看看`sendMessageToClients()`函数是什么样子的:\n\n```cpp\nvoid server::sendMessageToClients(QString message) \n{ \n   if (allClients->size() > 0) \n   { \n         for (int i = 0; i < allClients->size(); i++) \n         { \n               if (allClients->at(i)->isOpen() && allClients->at(i)-\n               >isWritable()) \n               { \n                     allClients->at(i)->write(message.toUtf8()); \n               } \n         } \n   } \n} \n```\n\n在前面的代码中，我们简单地循环通过`allClients`数组，并将消息数据传递给所有连接的客户端。\n\n最后，打开`main.cpp`并添加以下代码来启动我们的服务器:\n\n```cpp\n#include <QCoreApplication> \n#include \"server.h\" \n\nint main(int argc, char *argv[]) \n{ \n   QCoreApplication a(argc, argv); \n\n   server* myServer = new server(); \n   myServer->startServer(); \n\n   return a.exec(); \n} \n```\n\n现在构建并运行程序，您应该会看到如下内容:\n\n![](img/07666326-5c7d-4633-8a02-641e3ae73af5.png)\n\n除了显示服务器正在监听端口`8001`之外，看起来没有任何事情发生。别担心，因为我们还没有创建客户端程序。我们继续！\n\n# 创建即时消息客户端\n\n在下一节中，我们将继续创建我们的即时消息客户端，用户将使用它来发送和接收消息。\n\n# 设计用户界面\n\n在本节中，我们将学习如何为即时消息客户端设计用户界面并为其创建功能:\n\n1.  首先，通过转到文件|新文件或项目来创建另一个 Qt 项目。然后在应用类别下选择 Qt 小部件应用。\n2.  项目创建完成后，打开`mainwindow.ui`并将线条编辑和文本浏览器拖到窗口画布上。然后，选择中心小部件，并单击位于上面小部件栏上的垂直布局按钮，将垂直布局效果应用于小部件:\n\n![](img/e12c2a26-b9f7-4a29-be49-3e3e35eaa0c8.png)\n\n3.  之后，在底部放置一个水平布局，并将线编辑放入布局中。然后，从小部件框中拉出一个按钮，将其命名为`sendButton`；我们还将其标签设置为`Send`，如下图:\n\n![](img/b6569033-78ae-4f24-8c92-52d0d24ce323.png)\n\n4.  完成后，拖放另一个水平布局，并将其放在文本浏览器的顶部。之后，在水平布局中放置标签、线编辑和按钮，如下所示:\n\n![](img/65759c61-0e68-4ef7-803d-91247e04d7ae.png)\n\n我们调用行编辑小部件`nameInput`，并为其设置一个默认文本为`John Doe`，这样用户就有了一个默认名称。然后，我们调用按钮`connectButton`并将其标签更改为`Connect`。\n\n我们已经完成了一个非常简单的即时消息程序的用户界面设计，它将完成以下任务:\n\n1.  连接到服务器\n2.  让用户设置他们的名字\n3.  可以看到所有用户发送的消息\n4.  用户可以输入并发送他们的消息，让所有人都能看到\n\n现在编译并运行项目，您应该会看到您的程序如下所示:\n\n![](img/b412d5d1-4f08-44fb-bcd5-3da628cdfb2a.png)\n\n请注意，我还将窗口标题更改为`Chat Client`，使其看起来稍微专业一些。您可以通过在层次窗口中选择`MainWindow`对象并更改其`windowTitle`属性来实现。\n\n在下一节中，我们将开始编程部分的工作，并实现上面列表中提到的特性。\n\n# 实现聊天功能\n\n在我们开始编写任何代码之前，我们必须首先通过打开我们的项目文件(`.pro`)来启用网络模块，并在那里添加`network`关键字:\n\n```cpp\nQT += core gui network \n```\n\n接下来，打开`mainwindow.h`并添加以下标题和变量:\n\n```cpp\n#ifndef MAINWINDOW_H \n#define MAINWINDOW_H \n\n#include <QMainWindow> \n#include <QDebug> \n#include <QTcpSocket> \n\nprivate: \n   Ui::MainWindow *ui; \n   bool connectedToHost; \n   QTcpSocket* socket; \n```\n\n我们在`mainwindow.cpp`中默认将`connectedToHost`变量设置为`false`:\n\n```cpp\nMainWindow::MainWindow(QWidget *parent) : \n   QMainWindow(parent), \n   ui(new Ui::MainWindow) \n{ \n   ui->setupUi(this); \n   connectedToHost = false; \n} \n```\n\n完成后，我们需要实现的第一个特性是服务器连接。打开`mainwindow.ui`，右键单击连接按钮，然后选择转到插槽...，并选择`clicked()`。之后，会自动为您创建一个槽函数。在`SLOT`功能中添加以下代码:\n\n```cpp\nvoid MainWindow::on_connectButton_clicked() \n{ \n   if (!connectedToHost) \n   { \n         socket = new QTcpSocket(); \n\n         connect(socket, SIGNAL(connected()), this, \n         SLOT(socketConnected())); \n         connect(socket, SIGNAL(disconnected()), this, \n         SLOT(socketDisconnected())); \n         connect(socket, SIGNAL(readyRead()), this, \n         SLOT(socketReadyRead())); \n\n         socket->connectToHost(\"127.0.0.1\", 8001); \n   } \n   else \n   { \n         QString name = ui->nameInput->text(); \n         socket->write(\"<font color=\"Orange\">\" + name.toUtf8() + \" has \n         left the chat room.</font>\"); \n\n         socket->disconnectFromHost(); \n   } \n} \n```\n\n我们在前面的代码中所做的基本上是检查`connectedToHost`变量。如果变量是`false`(意味着客户端没有连接到服务器)，创建一个名为`socket`的`QTcpSocket`对象，并使其连接到端口`8801`上的`127.0.0.1`主机。IP 地址`127.0.0.1`代表本地主机。由于这仅用于测试目的，我们将客户端连接到位于同一台计算机上的测试服务器。如果您在另一台计算机上运行服务器，您可以根据需要将 IP 地址更改为局域网或广域网地址。\n\n当`connected()`、`disconnected()`和`readReady()`信号被触发时，我们还将`socket`对象连接到其各自的插槽功能。这与我们之前所做的服务器代码完全相同。如果客户端已经连接到服务器，并且单击了连接(现在标记为`Disconnect`)按钮，则向服务器发送断开消息并终止连接。\n\n接下来，我们将看看槽函数，我们在上一步中将其连接到了`socket`对象。第一个是`socketConnected()`函数，当客户端成功连接到服务器时将调用该函数:\n\n```cpp\nvoid MainWindow::socketConnected() \n{ \n   qDebug() << \"Connected to server.\"; \n\n   printMessage(\"<font color=\"Green\">Connected to server.</font>\"); \n\n   QString name = ui->nameInput->text(); \n   socket->write(\"<font color=\"Purple\">\" + name.toUtf8() + \" has joined \n   the chat room.</font>\"); \n\n   ui->connectButton->setText(\"Disconnect\"); \n   connectedToHost = true; \n} \n```\n\n首先，客户端将在应用输出和文本浏览器小部件上显示`Connected to server.`消息。我们将在一分钟后看到`printMessage()`功能是什么样子的。然后，我们从输入字段中获取用户名，并将其合并到文本消息中，然后将其发送到服务器，以便通知所有用户。最后，将连接按钮的标签设置为`Disconnect`，将`connectedToHost`变量设置为`true`。\n\n在这之后，让我们看看`socketDisconnected()`，顾名思义，每当客户端与服务器断开连接时都会调用它:\n\n```cpp\nvoid MainWindow::socketDisconnected() \n{ \n   qDebug() << \"Disconnected from server.\"; \n\n   printMessage(\"<font color=\"Red\">Disconnected from server.</font>\"); \n\n   ui->connectButton->setText(\"Connect\"); \n   connectedToHost = false; \n} \n```\n\n前面的代码非常简单。它所做的只是在应用输出和文本浏览器小部件上显示断开的消息，然后将断开按钮的标签设置为`Connect`，将`connectedToHost`变量设置为`false`。请注意，由于只有在客户端与服务器断开连接后才会调用该函数，因此我们无法再向服务器发送任何消息来通知它断开连接。您应该在服务器端检查连接是否断开，并相应地通知所有用户。\n\n然后是`socketReadyRead()`功能，每当服务器向客户端发送数据时都会触发。这个函数甚至比之前的函数更简单，因为它所做的只是将输入数据传递给`printMessage()`函数，而没有其他功能:\n\n```cpp\nvoid MainWindow::socketReadyRead() \n{ \n   ui->chatDisplay->append(socket->readAll()); \n} \n```\n\n最后，我们来看看`printMessage()`函数是什么样子的。其实也一样简单。它所做的只是将消息附加到文本浏览器中，然后就完成了:\n\n```cpp\nvoid MainWindow::printMessage(QString message) \n{ \n   ui->chatDisplay->append(message); \n} \n```\n\n最后，让我们看看如何实现向服务器发送消息的功能。打开`mainwindow.ui`，右键点击发送按钮，选择转到插槽...，并选择`clicked()`选项。为您创建插槽函数后，向函数中添加以下代码:\n\n```cpp\nvoid MainWindow::on_sendButton_clicked() \n{ \n   QString name = ui->nameInput->text(); \n   QString message = ui->messageInput->text(); \n   socket->write(\"<font color=\"Blue\">\" + name.toUtf8() + \"</font>: \" + \n   message.toUtf8()); \n\n   ui->messageInput->clear(); \n} \n```\n\n首先，我们取用户的名字，并将其与消息结合起来。然后，在通过调用`write()`将整个东西发送到服务器之前，我们将名称设置为蓝色。之后，清除消息输入字段，我们就完成了。由于文本浏览器默认接受富文本，我们可以通过将文本放在`<font>`标签中来使用富文本来给文本着色。\n\n立即编译并运行项目；你们应该能够在不同的客户上互相聊天！在连接客户端之前，不要忘记打开服务器。如果一切顺利，你应该看到这样的情况:\n\n![](img/3e596014-2a86-4f29-b996-da0d30ff5cd9.png)\n\n同时，您还应该看到服务器端的所有活动:\n\n![](img/0fc60d96-fc57-4fe5-b4cd-a459653d6dcf.png)\n\n就这样！我们已经成功地使用 Qt 创建了一个简单的聊天系统。欢迎你即兴发挥，创造一个完善的信息系统！\n\n# 摘要\n\n在本章中，我们学习了如何使用 Qt 的网络模块创建即时消息系统。在下一章中，我们将深入探讨使用 Qt 进行图形渲染的奇妙之处。"
  },
  {
    "path": "docs/cpp-gui-prog-qt5/11.md",
    "content": "# 十一、实现图形编辑器\n\nQt 使用`QPainter`类为我们提供了低级别的图形渲染。Qt 能够渲染位图和矢量图像。在本章中，我们将学习如何使用 Qt 绘制形状，最后，创建一个我们自己的绘制程序。\n\n在本章中，我们将涵盖以下主题:\n\n*   绘制矢量形状\n*   将矢量图像保存到 SVG 文件中\n*   创建绘画程序\n\n你准备好了吗？我们开始吧！\n\n# 绘制矢量形状\n\n在下一节中，我们将学习如何使用 QPainter 类在我们的 Qt 应用上渲染矢量图形。\n\n# 矢量与位图\n\n计算机图形学中有两种格式——位图和矢量。位图图像(也称为光栅图像)是存储为一系列称为**像素**的微小点的图像。每个像素将被分配一种颜色，并以存储的方式显示在屏幕上——像素和屏幕上显示的内容之间是一一对应的。\n\n另一方面，矢量图像不是基于位图模式，而是使用数学公式来表示可以组合以创建几何形状的直线和曲线。\n\n这里列出了这两种格式的主要特征:\n\n*   位图:\n    *   通常是更大的文件大小\n    *   不能放大到更高的分辨率，因为图像质量会受到影响\n    *   用于显示具有多种颜色的复杂图像，如照片\n*   矢量:\n    *   文件大小非常小\n    *   图形可以在不影响图像质量的情况下调整大小\n    *   每个形状只能应用有限数量的颜色(单色、渐变或图案)\n    *   复杂的形状需要产生高处理能力\n\n下图比较了位图和矢量图形:\n\n![](img/94527953-3456-480e-92b6-2303f304d7c4.png)\n\n在这一节中，我们将重点学习如何使用 Qt 绘制矢量图形，但我们也将在本章稍后介绍位图图形。\n\n# 使用 QPainter 绘制矢量形状\n\n首先，通过转到文件|新文件或项目来创建另一个 Qt 项目。然后在应用类别下选择 Qt 小部件应用。项目创建完成后，打开`mainwindow.h`并在`QPainter`表头添加:\n\n```cpp\n#include <QMainWindow> \n#include <QPainter> \n```\n\n之后，我们还声明了一个名为`paintEvent()`的虚拟函数，这是 Qt 中的一个标准事件处理程序，每当有需要绘制的内容时都会被调用，无论是图形用户界面更新、窗口大小调整还是手动调用`update()`函数:\n\n```cpp\npublic:\n    explicit MainWindow(QWidget *parent = 0);\n    ~MainWindow();\n    virtual void paintEvent(QPaintEvent *event); \n```\n\n然后，打开`mainwindow.cpp`并添加`paintEvent()`功能:\n\n```cpp\nvoid MainWindow::paintEvent(QPaintEvent *event) \n{ \n   QPainter painter; \n   painter.begin(this); \n\n   // Draw Line \n   painter.drawLine(QPoint(50, 60), QPoint(100, 100)); \n\n   // Draw Rectangle \n   painter.setBrush(Qt::BDiagPattern); \n   painter.drawRect(QRect(40, 120, 80, 30)); \n\n   // Draw Ellipse \n   QPen ellipsePen; \n   ellipsePen.setColor(Qt::red); \n   ellipsePen.setStyle(Qt::DashDotLine); \n   painter.setPen(ellipsePen); \n   painter.drawEllipse(QPoint(80, 200), 50, 20); \n\n   // Draw Rectangle \n   QPainterPath rectPath; \n   rectPath.addRect(QRect(150, 20, 100, 50)); \n   painter.setPen(QPen(Qt::red, 1, Qt::DashDotLine, Qt::FlatCap, \n   Qt::MiterJoin)); \n   painter.setBrush(Qt::yellow); \n   painter.drawPath(rectPath); \n\n   // Draw Ellipse \n   QPainterPath ellipsePath; \n   ellipsePath.addEllipse(QPoint(200, 120), 50, 20); \n   painter.setPen(QPen(QColor(79, 106, 25), 5, Qt::SolidLine, \n   Qt::FlatCap, Qt::MiterJoin)); \n   painter.setBrush(QColor(122, 163, 39)); \n   painter.drawPath(ellipsePath); \n\n   painter.end(); \n} \n```\n\n如果现在构建程序，您应该会看到以下内容:\n\n![](img/24aed423-bb6e-4adc-b33f-3804ca1972c2.png)\n\n前面的代码真的很长。我们把它分解一下，这样你更容易理解。每当`paintEvent()`被调用时(通常需要绘制窗口时会调用一次)，我们就调用`QPainter::begin()`告诉 Qt 我们要绘制一些东西，完成后我们就调用`QPainter::end()`。因此，绘制图形的代码将包含在`QPainter::begin()`和`QPainter::end()`中。\n\n让我们看看以下步骤:\n\n1.  我们画的第一个东西是一条直线，很简单——只需调用`QPainter::drawLine()`并将起点和终点值插入到函数中。请注意，Qt 使用的坐标系是像素格式的。它的原点从应用窗口的左上角开始，根据 *x* 和 *y* 的值，向右和向下增加。 *x* 值的增量将位置向右移动，而 *y* 值的增量将位置向下移动。\n2.  接下来，在形状中绘制一个带有阴影图案的矩形。这次我们先调用`QPainter::setBrush()`设置模式，再调用`drawRect()`。\n3.  之后，我们绘制了一个椭圆形状，该形状内有点划线轮廓和阴影图案。既然上一步已经设置好了模式，就不用再做了。相反，我们使用 QPen 类在调用`drawEllipse()`之前设置轮廓样式。请记住，在 Qt 的术语中，画笔用于定义形状的内部颜色或图案，而钢笔用于定义轮廓。\n4.  接下来的两个形状与前面的基本相似；我们只是改变了不同的颜色和图案，这样你就可以看到它们和前面例子的区别。\n\n# 绘图文本\n\n此外，还可以使用`QPainter`类绘制文本。你只需要在调用`QPainter::drawText()`之前调用`QPainter::setFont()`设置字体属性，比如:\n\n```cpp\nQPainter painter; \npainter.begin(this); \n\n// Draw Text \npainter.setFont(QFont(\"Times\", 14, QFont::Bold)); \npainter.drawText(QPoint(20, 30), \"Testing\"); \n\n// Draw Line \npainter.drawLine(QPoint(50, 60), QPoint(100, 100)) \n```\n\n`setFont()`功能是可选的，因为如果您不指定它，您将获得默认字体。完成后，构建并运行程序。你应该看看 Hello World 这个词！显示在窗口中:\n\n![](img/69667eda-dc36-4753-be48-1c8ac3a0143f.png)\n\n正如您在这里看到的，矢量形状基本上是由 Qt 实时生成的，无论您如何重新缩放窗口并更改其纵横比，它看起来都非常好。如果您转而渲染位图图像，当它随窗口一起重新缩放或纵横比改变时，它的视觉质量可能会降低。\n\n# 将矢量图像保存到 SVG 文件中\n\n除了绘制矢量图形，Qt 还允许我们将这些图形保存到矢量图像文件中，称为 **SVG** ( **可缩放矢量图形**)文件格式。SVG 格式是一种开放格式，被许多软件使用，包括显示矢量图形的网络浏览器。事实上，Qt 也可以读取 SVG 文件并在屏幕上呈现它们，但我们现在将跳过这一点。让我们来看看如何将矢量图形保存到一个 SVG 文件中！\n\n这个例子延续了我们在上一节中留下的内容。因此，我们不必创建一个新的 Qt 项目，只需坚持之前的项目即可。\n\n首先，让我们在主窗口中添加一个菜单栏，如果它还没有的话。然后，打开`mainwindow.ui`，在表单编辑器中，右键单击层次窗口上的主窗口对象，选择创建菜单栏:\n\n![](img/6041e5ce-79df-4fd0-8b7f-0308f37da1b9.png)\n\n完成后，将“文件”添加到菜单栏中，然后在它下面添加“另存为 SVG”:\n\n![](img/22dbdd71-1359-46bb-8a7e-6537dc52034e.png)\n\n然后，转到底部的操作编辑器，右键单击我们刚刚添加的菜单选项，并选择转到插槽...：\n\n![](img/e6fe895f-60a5-4fd8-9328-d937ea068f9a.png)\n\n会弹出一个窗口，让你选择一个信号。选择已触发()，然后单击确定。在`mainwindow.cpp`会为你创建一个新的槽功能。在我们打开`mainwindow.cpp`之前，让我们打开我们的`project file` ( `.pro`)并添加以下`svg`模块:\n\n```cpp\nQT += core gui svg \n```\n\n`svg`关键字告诉 Qt 向您的项目中添加相关的类，可以帮助您处理 SVG 文件格式。然后，我们还需要向我们的`mainwindow.h`添加两个标题:\n\n```cpp\n#include <QtSvg/QSvgGenerator> \n#include <QFileDialog> \n```\n\n之后，打开`mainwindow.cpp`，将下面的代码添加到我们上一步刚刚添加的槽函数中:\n\n```cpp\nvoid MainWindow::on_actionSave_as_SVG_triggered() \n{ \n    QString filePath = QFileDialog::getSaveFileName(this, \"Save SVG\", \"\", \"SVG files (*.svg)\"); \n\n    if (filePath == \"\") \n        return; \n\n    QSvgGenerator generator; \n    generator.setFileName(filePath); \n    generator.setSize(QSize(this->width(), this->height())); \n    generator.setViewBox(QRect(0, 0, this->width(), this->height())); \n    generator.setTitle(\"SVG Example\"); \n    generator.setDescription(\"This SVG file is generated by Qt.\"); \n\n    paintAll(&generator); \n} \n```\n\n在前面的代码中，我们使用`QFileDialog`让用户选择他们想要保存 SVG 文件的位置。然后，我们使用`QSvgGenerator`类将图形导出到一个 SVG 文件中。最后，我们调用了`paintAll()`函数，这是我们将在下一步中定义的自定义函数。\n\n实际上，我们需要修改现有的`paintAll()`方法，并将我们的渲染代码放入其中。然后，将`QSvgGenerator`对象作为绘制设备传入功能输入:\n\n```cpp\nvoid MainWindow::paintAll(QSvgGenerator *generator) \n{ \n    QPainter painter; \n\n    if (generator) \n        painter.begin(generator); \n    else \n        painter.begin(this); \n\n   // Draw Text \n    painter.setFont(QFont(\"Times\", 14, QFont::Bold)); \n   painter.drawText(QPoint(20, 30), \"Hello World!\"); \n```\n\n因此，我们的`paintEvent()`现在在`mainwindow.cpp`中简单地看起来是这样的:\n\n```cpp\nvoid MainWindow::paintEvent(QPaintEvent *event) \n{ \n   paintAll(); \n} \n```\n\n这里的过程可能看起来有点混乱，但是它所做的基本上是在创建窗口时调用`paintAll()`函数一次绘制所有图形，然后当您想要将图形保存到一个 SVG 文件时，您再次调用`paintAll()`。\n\n唯一不同的是绘制设备——一个是主窗口本身，我们将其用作绘图画布，对于后一个，我们将传递`QSvgGenerator`对象作为绘制设备，它将把图形保存到一个 SVG 文件中。\n\n现在构建并运行程序，点击文件|保存 SVG 文件，你应该可以将图形保存到一个 SVG 文件中。尝试用网络浏览器打开文件，看看它是什么样子:\n\n![](img/982756a6-c52f-45d4-ab97-f9429e05366c.png)\n\n似乎我的网络浏览器(火狐)不支持阴影模式，但其他事情证明是好的。由于矢量图形由程序生成，形状不存储在 SVG 文件中(只存储数学公式及其变量)，因此您可能需要确保用户平台支持您使用的功能。\n\n在下一节中，我们将学习如何创建我们自己的绘画程序并使用它绘制位图图像！\n\n# 创建绘画程序\n\n在下一节中，我们将进入像素领域，学习如何使用 Qt 创建一个绘画程序。用户将能够通过使用不同大小和颜色的画笔来绘制像素图像来表达他们的创造力！\n\n# 设置用户界面\n\n同样，对于这个例子，我们将创建一个新的 Qt 小部件应用。之后，打开`mainwindow.ui`，在主窗口增加一个菜单栏。然后，向菜单栏添加以下选项:\n\n![](img/6c4c1e46-259b-4888-a009-0a1ddbbac18c.png)\n\n菜单栏上有三个菜单项——文件、画笔大小和画笔颜色。“文件”菜单下有将画布保存为位图文件以及清除整个画布的功能。画笔大小类别包含不同的画笔大小选项；最后但同样重要的是，“画笔颜色”类别包含几个设置画笔颜色的选项。\n\n你可以在图形用户界面设计中使用更多类似于*绘画的*或*类似于 Photoshop 的*，但为了简单起见，我们现在将使用这个。\n\n完成所有这些后，打开`mainwindow.h`并在顶部添加以下标题:\n\n```cpp\n#include <QMainWindow> \n#include <QPainter> \n#include <QMouseEvent> \n#include <QFileDialog> \n```\n\n之后，我们还声明了几个虚函数，比如:\n\n```cpp\npublic:\n    explicit MainWindow(QWidget *parent = 0);\n    ~MainWindow();\n    virtual void mousePressEvent(QMouseEvent *event); \n    virtual void mouseMoveEvent(QMouseEvent *event); \n    virtual void mouseReleaseEvent(QMouseEvent *event); \n    virtual void paintEvent(QPaintEvent *event); \n    virtual void resizeEvent(QResizeEvent *event); \n```\n\n除了我们在前面例子中使用的`paintEvent()`函数，我们还可以添加一些用于处理鼠标事件和窗口大小调整事件的函数。然后，我们还将以下变量添加到我们的`MainWindow`类中:\n\n```cpp\nprivate: \n    Ui::MainWindow *ui; \n QImage image; \n    bool drawing; \n    QPoint lastPoint; \n    int brushSize; \n    QColor brushColor; \n```\n\n之后，我们打开`mainwindow.cpp`，从类构造器开始:\n\n```cpp\nMainWindow::MainWindow(QWidget *parent) : \n    QMainWindow(parent), \n    ui(new Ui::MainWindow) \n{ \n    ui->setupUi(this); \n\n image = QImage(this->size(), QImage::Format_RGB32); \n    image.fill(Qt::white); \n\n    drawing = false; \n    brushColor = Qt::black; \n    brushSize = 2; \n} \n```\n\n我们需要首先创建一个`QImage`对象，它充当画布，并设置其大小以匹配我们的窗口大小。然后，我们将默认笔刷颜色设置为黑色，默认大小设置为`2`。之后，我们将看看每个事件处理程序及其工作原理。\n\n首先我们来看一下`paintEvent()`函数，这个函数我们也用在了矢量图形中，比如。这一次，它所做的只是调用`QPainter::drawImage()`并在我们的主窗口上渲染`QImage`对象(我们的图像缓冲区):\n\n```cpp\nvoid MainWindow::paintEvent(QPaintEvent *event)\n{\n    QPainter canvasPainter(this);\n    canvasPainter.drawImage(this->rect(), image, image.rect());\n}\n```\n\n接下来，我们将看看`resizeEvent()`功能，每当用户调整主窗口大小时，该功能就会被触发。为了避免图像拉伸，我们必须调整图像缓冲区的大小以匹配新的窗口大小。这可以通过创建一个新的`QImage`对象并将其大小设置为与调整大小后的主窗口相同，然后复制先前 QImage 的像素信息并将其放置在新图像缓冲区的完全相同的位置来实现。\n\n这意味着，如果窗口大小小于绘图，您的图像将被裁剪，但至少在调整窗口大小时，画布不会被拉伸和扭曲图像。让我们看看代码:\n\n```cpp\nvoid MainWindow::resizeEvent(QResizeEvent *event) \n{ \n    QImage newImage(event->size(), QImage::Format_RGB32); \n    newImage.fill(qRgb(255, 255, 255)); \n\n    QPainter painter(&newImage); \n    painter.drawImage(QPoint(0, 0), image); \n    image = newImage; \n} \n```\n\n接下来，我们将看看鼠标事件处理程序，我们使用它在画布上应用颜色。首先是`mousePressEvent()`功能，当我们开始按鼠标键(这里是鼠标左键)的时候就会被触发。此时我们仍然没有绘制任何东西，但是将绘制布尔设置为`true`，并将光标位置保存到`lastPoint`变量:\n\n```cpp\nvoid MainWindow::mousePressEvent(QMouseEvent *event) \n{ \n    if (event->button() == Qt::LeftButton) \n    { \n        drawing = true; \n        lastPoint = event->pos(); \n    } \n} \n```\n\n然后，这里是`mouseMoveEvent()`功能，鼠标光标移动时会调用该功能:\n\n```cpp\nvoid MainWindow::mouseMoveEvent(QMouseEvent *event) \n{ \n    if ((event->buttons() & Qt::LeftButton) && drawing) \n    { \n        QPainter painter(&image); \n        painter.setPen(QPen(brushColor, brushSize, Qt::SolidLine, \n        Qt::RoundCap, Qt::RoundJoin)); \n        painter.drawLine(lastPoint, event->pos()); \n\n        lastPoint = event->pos(); \n        this->update(); \n    } \n} \n```\n\n在前面的代码中，我们检查在按住鼠标左键的同时是否确实在移动鼠标。如果是的话，那么我们从上一个光标位置到当前光标位置画一条线。然后，我们将当前光标位置保存到`lastPoint`变量，调用`update()`通知 Qt 触发`paintEvent()`功能。\n\n最后，当我们松开鼠标左键时，会调用`mouseReleaseEvent()`。我们只需将绘图变量设置为`false`，我们就完成了:\n\n```cpp\nvoid MainWindow::mouseReleaseEvent(QMouseEvent *event) \n{ \n    if (event->button() == Qt::LeftButton) \n    { \n        drawing = false; \n    } \n} \n```\n\n如果我们现在构建程序并运行它，我们应该能够开始在我们的小画图程序上绘制一些东西:\n\n![](img/89598a8c-02fa-4d93-9868-37aa3a30d6f8.png)\n\n即使我们现在可以画一些东西，但它的画笔大小和颜色一直都是一样的。那有点无聊！让我们右键单击主菜单中画笔大小类别的每个选项，然后选择转到插槽...，然后选择触发()选项，然后按确定。然后 Qt 会相应地为我们创建槽函数，我们需要在这些函数中做的基本上是更改 brushSize 变量，如下所示:\n\n```cpp\nvoid MainWindow::on_action2px_triggered() \n{ \n    brushSize = 2; \n} \n\nvoid MainWindow::on_action5px_triggered() \n{ \n    brushSize = 5; \n} \n\nvoid MainWindow::on_action10px_triggered() \n{ \n    brushSize = 10; \n} \n```\n\n“画笔颜色”类别下的所有选项也是如此。这次，我们相应地设置`brushColor`变量:\n\n```cpp\nvoid MainWindow::on_actionBlack_triggered() \n{ \n    brushColor = Qt::black; \n} \n\nvoid MainWindow::on_actionWhite_triggered() \n{ \n    brushColor = Qt::white; \n} \n\nvoid MainWindow::on_actionRed_triggered() \n{ \n    brushColor = Qt::red; \n} \n\nvoid MainWindow::on_actionGreen_triggered() \n{ \n    brushColor = Qt::green; \n} \n\nvoid MainWindow::on_actionBlue_triggered() \n{ \n    brushColor = Qt::blue; \n} \n```\n\n如果您再次构建并运行该程序，您将能够使用画笔的各种设置来绘制图像:\n\n![](img/a9b9ff12-8980-45c0-8cf2-ed3f9eaab8fe.png)\n\n除此之外，我们还可以添加一个现有的位图图像到我们的画布上，这样我们就可以在上面画画。假设我有一个 PNG 图像形式的企鹅图像(称为`tux.png`)，然后我们可以将以下代码添加到类构造函数中:\n\n```cpp\nMainWindow::MainWindow(QWidget *parent) : \n    QMainWindow(parent), \n    ui(new Ui::MainWindow) \n{ \n    ui->setupUi(this); \n\n    image = QImage(this->size(), QImage::Format_RGB32); \n    image.fill(Qt::white); \n\n    QImage tux; \n    tux.load(qApp->applicationDirPath() + \"/tux.png\"); \n    QPainter painter(&image); \n    painter.drawImage(QPoint(100, 100), tux); \n\n    drawing = false; \n    brushColor = Qt::black; \n    brushSize = 2; \n} \n```\n\n前面的代码基本上打开了图像文件，并将其移动到 100 x 100 的位置，然后将图像绘制到我们的图像缓冲区。现在，每当我们启动程序时，我们都能在画布上看到一只企鹅:\n\n![](img/1eb1fc12-4d31-4fd6-be34-8c5c174ff48d.png)\n\n接下来，我们将查看文件下的清除选项。当用户点击菜单栏上的这个选项时，我们使用以下代码清除整个画布(包括企鹅)并重新开始:\n\n```cpp\nvoid MainWindow::on_actionClear_triggered() \n{ \n    image.fill(Qt::white); \n    this->update(); \n} \n```\n\n最后，当用户单击文件下的保存选项时，我们打开一个文件对话框，让用户将他们的图稿保存到位图文件中。在下面的代码中，我们过滤掉了图像格式，只允许用户保存 PNG 和 JPEG 格式:\n\n```cpp\nvoid MainWindow::on_actionSave_triggered() \n{ \n    QString filePath = QFileDialog::getSaveFileName(this, \"Save Image\", \"\", \"PNG (*.png);;JPEG (*.jpg *.jpeg);;All files (*.*)\"); \n\n    if (filePath == \"\") \n        return; \n\n    image.save(filePath); \n} \n```\n\n就这样，我们成功地用 Qt 从头开始创建了一个简单的绘画程序！您甚至可以将本章学到的知识与上一章结合起来，创建一个在线协作白板！唯一的限制是你的创造力。最后，我要感谢所有的读者使用我们新创建的绘画程序创作了以下杰作:\n\n![](img/2d43a36a-906f-4b53-8e78-f4a72b9416c6.jpg)\n\n# 摘要\n\n在这一章中，我们学习了如何绘制矢量和位图图形，随后我们使用 Qt 创建了自己的绘画程序。在下一章中，我们将探讨创建一个将数据传输和存储到云的程序的各个方面。"
  },
  {
    "path": "docs/cpp-gui-prog-qt5/12.md",
    "content": "# 十二、云存储\n\n在前一章中，我们学习了如何使用 Qt 在屏幕上绘制图像。然而，在这一章中，我们将学习一些完全不同的东西，那就是设置我们自己的文件服务器并将其链接到我们的 Qt 应用。\n\n在本章中，我们将涵盖以下主题:\n\n*   设置文件传输协议服务器\n*   在列表视图中显示文件列表\n*   上传文件到文件传输协议服务器\n*   从文件传输协议服务器下载文件\n\n我们开始吧！\n\n# 设置文件传输协议服务器\n\n在下一节中，我们将学习如何设置一个文件传输协议服务器，它存储用户上传的所有文件，并允许他们随时下载。本节与 Qt 无关，因此如果您已经有一个正在运行的 FTP 服务器，请跳过这一部分，进入本章的下一节。\n\n# 介绍文件传输协议\n\n**文件传输协议**是**文件传输协议**的缩写。FTP 用于在网络上将文件从一台计算机传输到另一台计算机，通常是通过互联网。FTP 只是众多不同形式的云存储技术中的一种，但也是一种可以在自己的电脑上轻松设置的简单技术。\n\n有许多不同的 FTP 服务器是由不同的人群为特定的操作系统开发的。在本章的这一节中，我们将学习如何设置运行在 Windows 操作系统上的 FileZilla 服务器。如果您正在运行其他操作系统，如 GNU、Linux 或 macOS，还有许多其他 FTP 服务器程序可以使用，如 VSFTP 和 Pure-FTPd。\n\n在 Debian、Ubuntu 或其他类似的 Linux 变体上，在终端上运行`sudo apt-get install vsftpd`将安装和配置一个 FTP 服务器。在苹果电脑上，从苹果菜单中打开“系统偏好设置”，然后选择“共享”。然后，单击服务选项卡并选择文件传输协议访问。最后，单击开始按钮开始运行 FTP 服务器。\n\n如果您已经有一个运行的 FTP 服务器，请跳到下一节，在这一节中，我们将开始学习 C++ 编程。\n\n# 正在下载文件\n\nFileZilla 真的很容易设置和配置。它提供了一个功能齐全且易于使用的用户界面，并且不需要任何操作经验。我们需要做的第一件事是下载 FileZilla。我们将按如下方式进行:\n\n1.  打开你的浏览器，跳到[https://filezilla-project.org](https://filezilla-project.org)。您将在主页上看到两个下载按钮。\n2.  点击下载文件服务器，它会将我们带到下载页面:\n\n![](img/4bfbf211-e454-4edf-bc21-d4658021c8eb.png)\n\n3.  进入下载页面后，点击下载文件服务器按钮，开始下载软件。我们不会使用文件客户端，所以您不必下载它。一切准备就绪后，让我们开始安装软件。\n4.  像大多数 Windows 软件一样，安装过程非常简单。将所有内容保留为默认设置，并一直单击“下一步”，直到安装过程开始。安装最多需要几分钟时间才能完成。\n5.  一旦完成，点击关闭按钮，我们就完成了！：\n\n![](img/1f2e50ce-e947-4859-9cb8-7b4325f307d9.png)\n\n# 设置文件索引\n\n一旦安装了 FileZilla，控制面板很可能会自动打开。\n\n1.  由于这是您第一次启动 FileZilla，它会要求您设置服务器。保持服务器的 IP 地址为`127.0.0.1`(即**本地主机**)，管理端口为`14147`。\n2.  输入管理服务器所需的密码，并选中“始终连接到此服务器”选项。按下连接，FTP 服务器现在将启动！这显示在下面的截图中:\n\n![](img/6c47f55c-7b14-4f7e-bb5f-43c5af4b817c.png)\n\n3.  FTP 服务器开始运行后，我们需要创建一个用户帐户。单击左侧第四个图标，打开“用户”对话框:\n\n![](img/90d51fc3-b15a-43fd-afc2-b99511c8b1f6.png)\n\n4.  然后，在常规页面下，单击位于窗口右侧的添加按钮。通过设置用户名创建帐户，然后按“确定”。\n\n5.  我们现在不必将用户设置为任何组，因为用户组仅在您有许多具有相同权限设置的用户时有用，因为一次更改所有用户设置或将用户移动到不同的组更容易。创建用户后，选中密码选项并键入所需的密码。将密码放在您的 FTP 帐户上始终是一个很好的做法:\n\n![](img/93277ddc-baa0-4121-a281-25dcef5ebd9e.png)\n\n6.  之后，我们将进入共享文件夹页面，为新创建的用户添加一个共享目录。\n7.  确保选中“删除”和“追加”选项，以便可以替换同名文件。我们将使用它来更新我们的文件列表:\n\n![](img/51573cbf-7f90-4144-8677-cba2ec6bad13.png)\n\n8.  如果单击左侧第三个图标，将出现文件服务器选项对话框。您基本上可以在这里配置一切来满足您的需求。例如，如果您不想使用默认端口号`21`，您可以在“常规设置”页面下的选项窗口中简单地更改它:\n\n![](img/697d6dcd-114e-4477-bc2e-7437b0905d2a.png)\n\n9.  您也可以在“速度限制”页面下为所有用户或特定用户设置速度限制。当许多用户同时下载大量文件时，这可以防止服务器性能降低:\n\n![](img/1868a1d7-c12b-482b-9536-b844f2e7d50c.png)\n\n接下来，让我们继续创建我们的 Qt 项目！\n\n# 在列表视图中显示文件列表\n\n在前面的部分中，我们成功地设置了一个 FTP 服务器并保持其运行。在下一节中，我们将学习如何创建一个显示文件列表、将文件上传到文件传输协议服务器并最终从中下载文件的文件传输协议客户端程序。\n\n# 设置项目\n\n像往常一样，让我们使用 **Qt Creator** 创建一个新项目。以下步骤将有所帮助:\n\n1.  我们可以通过转到文件|新文件或项目并选择 Qt 小部件应用来创建一个新项目。\n2.  创建项目后，打开项目(`.pro`)文件并添加`network`关键字，以便 Qt 知道您需要项目中的网络模块:\n\n```cpp\nQT += core gui network\n```\n\n# 设置用户界面\n\n之后，打开`mainwindow.ui`并执行以下步骤来设计上传文件的用户界面上部:\n\n1.  在每一个小部件的顶部放置一个标签，上面写着上传文件。\n2.  在标签下方分别放置一个水平布局和两个按钮，分别写着“打开”和“上传”。\n3.  在水平布局下放置进度条。\n4.  在底部放一条水平线，后跟一个垂直间隔:\n\n![](img/8a026044-0639-4540-af80-4de768b78ffa.jpg)\n\n接下来，我们将构建用于下载文件的用户界面的底部:\n\n![](img/3130468f-e520-4536-8be3-2b8c472857ee.jpg)\n\n这次我们的用户界面和上半部分非常相似，只是我们在第二个进度条之前增加了一个列表视图，用于显示文件列表。对于这个示例程序，我们将所有内容都放在同一个页面上，这样解释起来就更简单、更容易理解。\n\n# 显示文件列表\n\n接下来，我们将学习如何在 FTP 服务器上保存和显示文件列表。事实上，默认情况下，文件传输协议服务器确实提供了文件列表，Qt 能够在旧版本中使用`qtftp`模块显示它。但是，从第 5 版开始，Qt 完全放弃了`qtftp`模块，这个功能也不复存在。\n\nIf you're still interested in the old `qtftp` module, you can still obtain its source code on GitHub by visiting the following link: [https://github.com/qt/qtftp](https://github.com/qt/qtftp)\n\n在 Qt 中，我们使用`QNetworkAccessManager`类与我们的文件传输协议服务器通信，因此专门为文件传输协议设计的功能不再工作。但是，不要担心，我们会研究其他替代方法来达到同样的结果。\n\n在我看来，最好的方法是使用在线数据库来存储文件列表及其信息(文件大小、格式、状态等)。如果您有兴趣学习如何将您的 Qt 应用连接到数据库，请参考[第 3 章](02.html)、*数据库连接*。然而，为了简单起见，我们将使用另一种工作正常但不太安全的方法——将文件名直接保存在文本文件中，并将其存储在 FTP 服务器上。\n\nIf you're doing a serious project for your client or company, please do not use this method. Check out [Chapter 3](02.html), *Database Connection*, and learn to use an actual database instead.\n\n好吧，假设除了使用文本文件没有其他方法；我们要怎么做？很简单:创建一个名为`files.txt`的文本文件，并将其放入我们在本章开头刚刚创建的 FTP 目录中。\n\n# 编写代码\n\n接下来，打开`mainwindow.h`并添加以下标题:\n\n```cpp\n#include <QMainWindow> \n#include <QDebug> \n#include <QNetworkAccessManager> \n#include <QNetworkRequest> \n#include <QNetworkReply> \n#include <QFile> \n#include <QFileInfo> \n#include <QFileDialog> \n#include <QListWidgetItem> \n#include <QMessageBox> \n```\n\n之后，添加以下变量和函数:\n\n```cpp\nprivate: \n   Ui::MainWindow *ui; \n QNetworkAccessManager* manager; \n\n   QString ftpAddress; \n   int ftpPort; \n   QString username; \n   QString password; \n\n   QNetworkReply* downloadFileListReply; \n   QNetworkReply* uploadFileListReply; \n\n   QNetworkReply* uploadFileReply; \n   QNetworkReply* downloadFileReply; \n\n   QStringList fileList; \n   QString uploadFileName; \n   QString downloadFileName; \n\npublic:\n   void getFileList();\n```\n\n完成上一步后，打开`mainwindow.cpp`并向类构造函数添加以下代码:\n\n```cpp\nMainWindow::MainWindow(QWidget *parent) : \n   QMainWindow(parent), \n   ui(new Ui::MainWindow) \n{ \n   ui->setupUi(this); \n\n manager = new QNetworkAccessManager(this); \n\n   ftpAddress = \"ftp://127.0.0.1/\"; \n   ftpPort = 21; \n   username = \"tester\"; // Put your FTP user name here\n   password = \"123456\"; // Put your FTP user password here \n   getFileList(); \n} \n```\n\n我们所做的基本上是初始化`QNetworkAccessManager`对象，并设置存储我们的 FTP 服务器信息的变量，因为我们将在后面的步骤中多次重复使用它。之后，我们将调用`getFileList()`功能，开始从我们的 FTP 服务器下载`files.txt`。`getFileList()`功能如下:\n\n```cpp\nvoid MainWindow::getFileList() \n{ \n   QUrl ftpPath; \n   ftpPath.setUrl(ftpAddress + \"files.txt\"); \n   ftpPath.setUserName(username); \n   ftpPath.setPassword(password); \n   ftpPath.setPort(ftpPort); \n\n   QNetworkRequest request; \n   request.setUrl(ftpPath); \n\n   downloadFileListReply = manager->get(request); \n   connect(downloadFileListReply, &QNetworkReply::finished, this, \n   &MainWindow::downloadFileListFinished); \n} \n```\n\n我们使用一个`QUrl`对象来存储关于我们的服务器和我们试图下载的文件的位置的信息，然后在通过调用`QNetworkAccessManager::get()`将其发送出去之前，将其馈送给一个`QNetworkRequest`对象。因为我们不知道什么时候所有的文件都会被完全下载，所以我们使用了 Qt 的`SIGNAL`和`SLOT`机制。\n\n我们连接了来自我们的`downloadFileListReply`指针的`finished()`信号(指向`mainwindow.h`中的一个`QNetworkReply`对象)，并将其链接到`slot`函数`downloadFileListFinished()`，我们定义如下:\n\n```cpp\nvoid MainWindow::downloadFileListFinished() \n{ \n   if(downloadFileListReply->error() != QNetworkReply::NoError) \n   { \n         QMessageBox::warning(this, \"Failed\", \"Failed to load file \n         list: \" + downloadFileListReply->errorString()); \n   } \n   else \n   { \n         QByteArray responseData; \n         if (downloadFileListReply->isReadable()) \n         { \n               responseData = downloadFileListReply->readAll(); \n         } \n\n         // Display file list \n         ui->fileList->clear(); \n         fileList = QString(responseData).split(\",\"); \n\n         if (fileList.size() > 0) \n         { \n               for (int i = 0; i < fileList.size(); i++) \n               { \n                     if (fileList.at(i) != \"\") \n                     { \n                           ui->fileList->addItem(fileList.at(i)); \n                     } \n               } \n         } \n   } \n} \n```\n\n代码有点长，所以我将函数分解为以下步骤:\n\n1.  如果下载过程中出现任何问题，显示一个消息框，告诉我们问题的性质。\n2.  如果一切顺利，下载完成，我们将通过调用`downloadFileListReply` | `readAll()`来读取数据。\n3.  然后，清除列表小部件并开始解析文本文件的内容。我们这里使用的格式非常简单；我们只使用了一个逗号符号来分隔每个文件名:`filename1,filename2,filename,...`重要的是我们在实际项目中不要这样做。\n4.  一旦我们调用`split(\",\")`将字符串分割成一个字符串列表，做一个`for`循环并在列表小部件上显示每个文件名。\n\n为了测试前面的代码是否有效，创建一个名为`files.txt`的文本文件，并将以下文本添加到文件中:\n\n```cpp\nfilename1,filename2,filename3 \n```\n\n然后，将文本文件放入您的 FTP 目录并运行您的项目。您应该能够在应用上看到它是这样显示的:\n\n![](img/515e1c2d-015e-4fd2-88e6-9588304c21a9.png)\n\n一旦它开始工作，我们就可以清除文本文件的内容，进入下一部分。\n\n# 上传文件到文件传输协议服务器\n\n由于我们的 FTP 目录中还没有任何文件(除了文件列表)，让我们编写代码来允许我们上传第一个文件。\n\n1.  首先，打开`mainwindow.ui`，右键点击打开按钮。然后，选择转到插槽并选择单击的()选项:\n\n![](img/9101a98a-64cd-4902-b2ea-d463509b03d9.png)\n\n2.  将自动为您创建一个`slot`功能。然后，将以下代码添加到函数中，以打开文件选择器窗口，供用户选择他们想要上传的文件:\n\n```cpp\nvoid MainWindow::on_openButton_clicked() \n{ \n   QString fileName = QFileDialog::getOpenFileName(this, \"Select \n   File\", qApp->applicationDirPath()); \n   ui->uploadFileInput->setText(fileName); \n}\n```\n\n3.  之后，重复此步骤，并对上传按钮执行相同的操作。这一次，其`slot`函数的代码如下所示:\n\n```cpp\nvoid MainWindow::on_uploadButton_clicked() \n{ \n   QFile* file = new QFile(ui->uploadFileInput->text()); \n   QFileInfo fileInfo(*file); \n   uploadFileName = fileInfo.fileName(); \n\n   QUrl ftpPath; \n   ftpPath.setUrl(ftpAddress + uploadFileName); \n   ftpPath.setUserName(username); \n   ftpPath.setPassword(password); \n   ftpPath.setPort(ftpPort); \n\n   if (file->open(QIODevice::ReadOnly)) \n   { \n         ui->uploadProgress->setEnabled(true); \n         ui->uploadProgress->setValue(0); \n\n         QNetworkRequest request; \n         request.setUrl(ftpPath); \n\n         uploadFileReply = manager->put(request, file); \n         connect(uploadFileReply, \n         SIGNAL(uploadProgress(qint64,qint64)), this, \n         SLOT(uploadFileProgress(qint64,qint64))); \n         connect(uploadFileReply, SIGNAL(finished()), this,  \n         SLOT(uploadFileFinished())); \n   } \n   else \n   { \n         QMessageBox::warning(this, \"Invalid File\", \"Failed to open \n         file for upload.\"); \n   } \n} \n\n```\n\n代码看起来有点长，让我们分解一下:\n\n1.  我们使用`QFile`类打开我们想要上传的文件(文件路径取自`ui->uploadFileInput->text()`)。如果文件不存在，显示一个消息框通知用户。\n2.  然后，我们将我们的 FTP 服务器和上传目的地的信息填充到一个`QUrl`对象中，然后将其馈送到一个`QNetworkRequest`对象中。\n3.  之后，我们开始读取我们文件的内容，并将其提供给`QNetworkAccessManager::put()`功能。\n\n4.  由于我们不知道文件什么时候会完全上传，所以我们使用了 Qt 提供的`SIGNAL`和`SLOT`机制。我们将`uploadProgress()`和`finished()`信号分别与我们的两个自定义`slot`功能`uploadFileProgress()`和`uploadFileFinised()`联系起来。\n\n`slot`功能`uploadFileProgress()`会告诉我们当前上传的进度，因此我们可以用它来设置进度条:\n\n```cpp\nvoid MainWindow::uploadFileProgress(qint64 bytesSent, qint64 bytesTotal) \n{ \n   qint64 percentage = 100 * bytesSent / bytesTotal; \n   ui->uploadProgress->setValue((int) percentage); \n} \n```\n\n同时，文件上传完成后将触发`uploadFileFinished()`功能:\n\n```cpp\nvoid MainWindow::uploadFileFinished() \n{ \n   if(uploadFileReply->error() != QNetworkReply::NoError) \n   { \n         QMessageBox::warning(this, \"Failed\", \"Failed to upload file: \" \n         + uploadFileReply->errorString()); \n   } \n   else \n   { \n         QMessageBox::information(this, \"Success\", \"File successfully \n         uploaded.\"); \n   } \n} \n\n```\n\n我们还没有完成前面的功能。由于新文件已经添加到 FTP 服务器，我们必须更新现有的文件列表，并替换存储在 FTP 目录中的`files.txt`文件。由于代码稍长，我们将把代码分成几个部分，所有这些都发生在显示“文件成功上传”消息框之前。\n\n1.  首先，让我们检查新上传的文件是否已经存在于我们的文件列表中(替换 FTP 服务器上的旧文件)。如果是的话，那么我们可以跳过整个事情；否则，将文件名附加到我们的`fileList`字符串列表中，如以下代码所示:\n\n```cpp\n// Add new file to file list array if not exist yet \nbool exists = false; \nif (fileList.size() > 0) \n{ \n   for (int i = 0; i < fileList.size(); i++) \n   { \n         if (fileList.at(i) == uploadFileName) \n         { \n               exists = true; \n         } \n   } \n} \n\nif (!exists) \n{ \n   fileList.append(uploadFileName); \n} \n```\n\n2.  之后，在我们应用的目录中创建一个临时文本文件(`files.txt`)，并将新文件列表保存在文本文件中:\n\n```cpp\n// Create new files.txt \nQString fileName = \"files.txt\"; \nQFile* file = new QFile(qApp->applicationDirPath() + \"/\" + fileName); \nfile->open(QIODevice::ReadWrite); \nif (fileList.size() > 0) \n{ \n   for (int j = 0; j < fileList.size(); j++) \n   { \n         if (fileList.at(j) != \"\") \n         { \n               file->write(QString(fileList.at(j) + \",\").toUtf8()); \n         } \n   } \n} \nfile->close(); \n```\n\n3.  最后，我们使用`QFile`类打开刚刚创建的文本文件，再次上传到 FTP 服务器，替换旧的文件列表:\n\n```cpp\n// Re-open the file \nQFile* newFile = new QFile(qApp->applicationDirPath() + \"/\" + fileName); \nif (newFile->open(QIODevice::ReadOnly)) \n{ \n   // Update file list to server \n   QUrl ftpPath; \n   ftpPath.setUrl(ftpAddress + fileName); \n   ftpPath.setUserName(username); \n   ftpPath.setPassword(password); \n   ftpPath.setPort(ftpPort); \n\n   QNetworkRequest request; \n   request.setUrl(ftpPath); \n   uploadFileListReply = manager->put(request, newFile); \n   connect(uploadFileListReply, SIGNAL(finished()), this, SLOT(uploadFileListFinished())); \n   file->close(); \n} \n```\n\n4.  同样，我们使用`SIGNAL`和`SLOT`机制，以便在文件列表上传后通知我们。`slot`功能`uploadFileListFinished()`如下所示:\n\n```cpp\nvoid MainWindow::uploadFileListFinished() \n{ \n   if(uploadFileListReply->error() != QNetworkReply::NoError) \n   { \n         QMessageBox::warning(this, \"Failed\", \"Failed to update file list: \" + uploadFileListReply->errorString()); \n   } \n   else \n   { \n         getFileList(); \n   } \n} \n\n```\n\n5.  我们基本上只是在把文件列表更新到 FTP 服务器后再调用`getFileList()`。如果您现在构建并运行项目，您应该能够将您的第一个文件上传到您的本地 FTP 服务器，万岁！\n\n![](img/9f62c8f3-6cf9-42aa-8a69-8d79ea69d13b.png)\n\n# 从文件传输协议服务器下载文件\n\n现在，我们已经成功地将第一个文件上传到了 FTP 服务器，让我们创建一个将文件下载回计算机的功能！\n\n1.  首先，再次打开`mainwindow.ui`，右键点击设置文件夹按钮。选择转到插槽...并点击()信号创建`slot`功能。`slot`功能很简单；它只会打开一个文件选择对话框，但这次它只会让用户选择一个文件夹，因为我们为它提供了一个`QFileDialog::ShowDirsOnly`标志:\n\n```cpp\nvoid MainWindow::on_setFolderButton_clicked() \n{ \n   QString folder = QFileDialog::getExistingDirectory(this, tr(\"Open Directory\"), qApp->applicationDirPath(), QFileDialog::ShowDirsOnly); \n   ui->downloadPath->setText(folder); \n} \n```\n\n2.  然后，右键单击列表小部件并选择转到插槽...这一次，我们将选择`itemDoubleClicked(QListWidgetItem*)`选项:\n\n![](img/ccd5fed6-24d3-4345-a7b5-06be7331f314.png)\n\n3.  当用户双击列表小部件中的项目时，将触发以下功能，启动下载。文件名可以通过调用`item->text()`从`QListWidgetItem`对象中获取:\n\n```cpp\nvoid MainWindow::on_fileList_itemDoubleClicked(QListWidgetItem *item) \n{ \n   downloadFileName = item->text(); \n\n   // Check folder \n   QString folder = ui->downloadPath->text(); \n   if (folder != \"\" && QDir(folder).exists()) \n   { \n         QUrl ftpPath; \n         ftpPath.setUrl(ftpAddress + downloadFileName); \n         ftpPath.setUserName(username); \n         ftpPath.setPassword(password); \n         ftpPath.setPort(ftpPort); \n\n         QNetworkRequest request; \n         request.setUrl(ftpPath); \n\n         downloadFileReply = manager->get(request); \n         connect(downloadFileReply, \n         SIGNAL(downloadProgress(qint64,qint64)), this, \n         SLOT(downloadFileProgress(qint64,qint64))); \n         connect(downloadFileReply, SIGNAL(finished()), this, \n         SLOT(downloadFileFinished())); \n   } \n   else \n   { \n         QMessageBox::warning(this, \"Invalid Path\", \"Please set the \n         download path before download.\"); \n   } \n} \n```\n\n4.  就像我们在`upload`功能中所做的一样，我们也使用了这里的`SIGNAL`和`SLOT`机制来获得下载过程的进程以及完成的信号。在下载过程中将调用`slot`函数`downloadFileProgress()`，我们用它来设置第二个进度条的值:\n\n```cpp\nvoid MainWindow::downloadFileProgress(qint64 byteReceived,qint64 bytesTotal) \n{ \n   qint64 percentage = 100 * byteReceived / bytesTotal; \n   ui->downloadProgress->setValue((int) percentage); \n} \n```\n\n5.  然后，当文件下载完成后，将调用`slot`功能`downloadFileFinished()`。之后，我们要做的是读取文件的所有数据，并将其保存到我们想要的目录中:\n\n```cpp\nvoid MainWindow::downloadFileFinished() \n{ \n   if(downloadFileReply->error() != QNetworkReply::NoError) \n   { \n         QMessageBox::warning(this, \"Failed\", \"Failed to download \n         file: \" + downloadFileReply->errorString()); \n   } \n   else \n   { \n         QByteArray responseData; \n         if (downloadFileReply->isReadable()) \n         { \n               responseData = downloadFileReply->readAll(); \n         } \n\n         if (!responseData.isEmpty()) \n         { \n               // Download finished \n               QString folder = ui->downloadPath->text(); \n               QFile file(folder + \"/\" + downloadFileName); \n               file.open(QIODevice::WriteOnly); \n               file.write((responseData)); \n               file.close(); \n\n               QMessageBox::information(this, \"Success\", \"File \n               successfully downloaded.\"); \n         } \n   } \n}\n```\n\n6.  立即构建程序，您应该能够下载文件列表中列出的任何文件！：\n\n![](img/9b76d2da-fc18-4ff7-9e3f-1a559ee1d2cf.png)\n\n# 摘要\n\n在本章中，我们学习了如何使用 Qt 的网络模块创建我们自己的云存储客户端。在下一章中，我们将学习更多关于多媒体模块的知识，并使用 Qt 从头开始创建我们自己的多媒体播放器。"
  },
  {
    "path": "docs/cpp-gui-prog-qt5/13.md",
    "content": "# 十三、多媒体查看器\n\n在前一章中，我们学习了如何通过云存储上传和下载文件。现在，在本章中，我们将学习如何使用 Qt 的多媒体模块打开这些文件，特别是媒体文件，如图像、音乐和视频。\n\n在本章中，我们将涵盖以下主题:\n\n*   重温多媒体模块\n*   图像查看器\n*   音乐播放器\n*   视频播放器\n\n我们开始吧！\n\n# 重温多媒体模块\n\n在本章中，我们将再次使用多媒体模块，我们之前在[第 9 章](08.html)、*摄像机模块*中已经介绍过。然而，这次我们将使用模块的一些其他部分，所以我认为解剖模块并看看里面有什么是个好主意。\n\n# 剖析模块\n\n多媒体模块是一个非常大的模块，由许多不同的部分组成，提供非常不同的特性和功能。主要类别如下:\n\n*   声音的\n*   录像\n*   照相机\n*   收音机\n\n请注意，处理图像格式的类，如`QImage`、`QPixmap`等，不是多媒体模块的一部分，而是图形用户界面模块的一部分。这是因为它们是 GUI 中不可分割的重要部分。尽管如此，我们仍将在本章中介绍`QImage`课。\n\n每个类别下都有子类别，如下所示:\n\n*   音频:\n    *   音频输出\n    *   录音机\n*   视频:\n    *   录像机\n    *   影像播放机\n    *   视频播放列表\n*   摄像机:\n    *   照相机取景器\n    *   相机图像捕捉\n    *   照相机录像机\n*   收音机:\n    *   无线电调谐器(适用于支持模拟无线电的设备)\n\n每个类都被设计来实现不同的目的。例如`QSoundEffect`用于播放低延时音频文件(如 WAV 文件)。`QAudioOutput`另一方面，将原始音频数据输出到特定的音频设备，这使您可以对音频输出进行低级控制。最后，`QMediaPlayer`是一个高级音频(和视频)播放器，支持许多不同的高延迟音频格式。在为您的项目选择合适的课程之前，您必须了解所有课程之间的差异。\n\nQt 中的多媒体模块是一个巨大的野兽，经常会让新手感到困惑，但如果你知道该选择哪个，它可能会很有优势。多媒体模块的另一个问题是，它可能在您的目标平台上工作，也可能不工作。这是因为所有这些类下面都是特定平台的本机实现。如果某个特定平台不支持某个功能，或者还没有实现该功能，那么您将无法使用这些功能。\n\nFor more information regarding the different classes provided by Qt's multimedia module, please visit the following link:\n[https://doc.qt.io/qt-5.10/qtmultimedia-index.html](https://doc.qt.io/qt-5.10/qtmultimedia-index.html)\n\n# 图像查看器\n\n数字图像已经成为我们日常生活的一个重要方面。无论是自拍、舞会之夜的照片，还是搞笑的迷因，我们都会花很多时间看数字图像。在下一节中，我们将学习如何使用 Qt 和 C++ 创建我们自己的图像查看器。\n\n# 为图像查看器设计用户界面\n\n让我们开始创建我们的第一个多媒体程序。在本节中，我们将创建一个图像查看器，顾名思义，它会打开一个图像文件并将其显示在窗口上:\n\n1.  让我们打开 Qt Creator 并创建一个新的 Qt Widgets 应用项目。\n2.  之后，打开`mainwindow.ui`并在中心小部件中添加一个`Label`(命名为`imageDisplay`，它将作为渲染我们图像的画布。然后，通过选择布局并垂直按下位于画布顶部的布局，将布局添加到中心视图获取:\n\n![](img/5e2e8370-e62f-4fe3-a04e-3b95358c4be8.png)\n\n3.  You can remove the tool bar and status bar to give space to the `Label`. Also, set the layout margins of the central widget to `0`:\n\n    ![](img/f69e4fd6-cecc-4275-b956-e6399396412e.png)\n\n4.  之后，双击菜单栏并添加一个文件操作，然后在它下面打开文件:\n\n![](img/8cb08727-5a95-4356-8c66-787f8a8a9aeb.png)\n\n5.  然后，在操作编辑器下，右键单击打开文件操作，并选择转到插槽...：\n\n![](img/b5e3338b-a8c9-4402-af8c-a5f030de6057.png)\n\n6.  将弹出一个窗口，要求您选择一个信号，因此选择“触发的”()，然后单击“确定”:\n\n![](img/c9f8f7e1-2970-42cf-adf4-726ef91fae7b.png)\n\n将自动为您创建一个`slot`功能，但我们将在下一节保留该功能。我们已经完成了用户界面，它真的很简单。接下来，让我们继续前进，开始编写我们的代码！\n\n# 为图像查看器编写 C++ 代码\n\n让我们从以下步骤开始:\n\n1.  首先，打开`mainwindow.h`并添加以下标题:\n\n```cpp\n#include <QMainWindow> \n#include <QFileDialog> \n#include <QPixmap> \n#include <QPainter>\n```\n\n2.  然后，添加以下名为`imageBuffer`的变量，该变量将作为重新缩放前指向实际图像数据的指针。然后，添加以下功能:\n\n```cpp\nprivate: \n   Ui::MainWindow *ui; \n QPixmap* imageBuffer; \n\npublic:\n   void resizeImage();\n void paintEvent(QPaintEvent *event);\n\npublic slots:\n   void on_actionOpen_triggered();\n```\n\n3.  接下来，打开`mainwindow.cpp`并初始化类构造函数中的`imageBuffer`变量:\n\n```cpp\nMainWindow::MainWindow(QWidget *parent) : \n   QMainWindow(parent), \n   ui(new Ui::MainWindow) \n{ \n   ui->setupUi(this); \n   imageBuffer = nullptr; \n} \n```\n\n4.  之后，将以下代码添加到上一节为我们创建的`slot`函数 Qt 中:\n\n```cpp\nvoid MainWindow::on_actionOpen_triggered() \n{ \n   QString fileName = QFileDialog::getOpenFileName(this, \"Open Image File\", qApp->applicationDirPath(), \"JPG (*.jpg *.jpeg);;PNG (*.png)\"); \n\n   if (!fileName.isEmpty()) \n   { \n         imageBuffer = new QPixmap(fileName); \n         resizeImage(); \n   } \n}\n```\n\n5.  前面的代码基本上打开了文件选择对话框，用选择的图像文件创建了一个`QPixmap`对象。所有这些完成后，它将调用`resizeImage()`函数，如下代码所示:\n\n```cpp\nvoid MainWindow::resizeImage() \n{ \n   if (imageBuffer != nullptr) \n   { \n         QSize size = ui->imageDisplay->size(); \n         QPixmap pixmap = imageBuffer->scaled(size, \n            Qt::KeepAspectRatio); \n\n         // Adjust the position of the image to the center \n         QRect rect = ui->imageDisplay->rect(); \n         rect.setX((this->size().width() - pixmap.width()) / 2); \n         rect.setY((this->size().height() - pixmap.height()) / 2); \n\n         QPainter painter; \n         painter.begin(this); \n         painter.drawPixmap(rect, pixmap, ui->imageDisplay->rect()); \n         painter.end(); \n   } \n} \n```\n\n`resizeImage()`函数所做的只是从`imageBuffer`变量中复制图像数据，并在窗口画布上显示图像之前调整图像大小以适合窗口大小。您可能正在打开比屏幕分辨率大得多的图像，我们不希望打开如此大的图像文件时图像被裁剪。\n\n我们使用`imageBuffer`变量的原因是为了保留原始数据的副本，并且不会因为多次调整大小而影响图像质量。\n\n最后，我们也在`paintEvent()`函数中调用这个`resizeImage()`函数。每当主窗口调整大小或从最小化状态恢复时，`paintEvent()`将自动被调用，并且`resizeImage()`功能也将被调用，如下所示:\n\n```cpp\nvoid MainWindow::paintEvent(QPaintEvent *event) \n{ \n   resizeImage(); \n} \n```\n\n就这样。如果您现在构建并运行项目，您应该会得到一个非常简洁的图像查看器，如下所示:\n\n![](img/fca8f4b0-48cb-4037-ba35-3518c6beac66.png)\n\n# 音乐播放器\n\n在下一节中，我们将学习如何使用 Qt 和 C++ 构建我们自己的定制音乐播放器。\n\n# 为音乐播放器设计用户界面\n\n让我们继续下一个项目。在这个项目中，我们将使用 Qt 构建一个音频播放器。请执行以下步骤:\n\n1.  与上一个项目一样，我们将创建一个`Qt Widgets Application`项目。\n2.  打开`project file (.pro)`，加入`multimedia`模块:\n\n```cpp\nQT += core gui multimedia \n```\n\n3.  We added the `multimedia` text so that Qt includes classes related to the multimedia module in our project. Next, open up `mainwindow.ui`, and refer to the following screenshot to construct the user interface:\n\n    ![](img/e87dedd9-939f-4e7d-a19f-3bb2bad50497.png)\n\n我们基本上在顶部添加了一个标签，后面是一个水平滑块和另一个标签来显示音频的当前时间。之后，我们在底部添加了三个按钮，分别是播放按钮、暂停按钮和停止按钮。位于这些按钮右侧的是另一个控制音量的水平布局。\n\n如你所见，所有的按钮现在都没有图标，很难理解哪个按钮是为了什么目的。\n\n1.  To add icons to the buttons, let's go to File | New File or Project and select Qt Resource File under the Qt category. Then, create a prefix called `icons`, and add the icon images to the prefix:\n\n    ![](img/d2370cbd-83c0-45ae-99b9-47fd81a252d7.png)\n\n2.  After that, add those icons to the Push Button by setting its icon property and selecting Choose Resource.... Then, set the `pixmap` property of the label, located beside the volume slider, as the volume icon:\n\n    ![](img/ab91665f-5ce4-4f6b-b1f4-9b2772ab7fa2.png)\n\n3.  After you have added the icons to the Push Button and Label, the user interface should look a lot better:\n\n    ![](img/cd321651-a9b0-45bb-8e91-72c15d5d11b3.png)\n\n我们已经完成了用户界面；让我们进入编程部分！\n\n# 为音乐播放器编写 C++ 代码\n\n要为音乐播放器编写 C++ 代码，请执行以下步骤:\n\n1.  首先，打开`mainwindow.h`并添加以下标题:\n\n```cpp\n#include <QMainWindow> \n#include <QDebug> \n#include <QFileDialog> \n#include <QMediaPlayer> \n#include <QMediaMetaData> \n#include <QTime> \n```\n\n2.  之后，添加`player`变量，这是一个`QMediaPlayer`指针。然后，声明我们稍后要定义的函数:\n\n```cpp\nprivate: \n   Ui::MainWindow *ui; \n   QMediaPlayer* player; \n\npublic:\n void stateChanged(QMediaPlayer::State state);\n void positionChanged(qint64 position);\n```\n\n3.  接下来，打开`mainwindow.cpp`并初始化玩家变量:\n\n```cpp\nMainWindow::MainWindow(QWidget *parent) : \n   QMainWindow(parent), \n   ui(new Ui::MainWindow) \n{ \n   ui->setupUi(this); \n\n   player = new QMediaPlayer(this); \n   player->setVolume(ui->volume->value()); \n   connect(player, &QMediaPlayer::stateChanged, this, &MainWindow::stateChanged); \n   connect(player, &QMediaPlayer::positionChanged, this, &MainWindow::positionChanged); \n} \n```\n\n`QMediaPlayer`类是我们的应用用来播放它加载的任何音频文件的主类。因此，我们需要知道音频播放的状态及其当前位置。我们可以通过将其`stateChanged()`和`positionChanged()`信号连接到我们的自定义`slot`功能来获取这些信息。\n\n4.  `stateChanged()`信号允许我们获取音频播放的当前状态信息。然后，我们相应地启用和禁用按钮:\n\n```cpp\nvoid MainWindow::stateChanged(QMediaPlayer::State state) \n{ \n   if (state == QMediaPlayer::PlayingState) \n   { \n         ui->playButton->setEnabled(false); \n         ui->pauseButton->setEnabled(true); \n         ui->stopButton->setEnabled(true); \n   } \n   else if (state == QMediaPlayer::PausedState) \n   { \n         ui->playButton->setEnabled(true); \n         ui->pauseButton->setEnabled(false); \n         ui->stopButton->setEnabled(true); \n   } \n   else if (state == QMediaPlayer::StoppedState) \n   { \n         ui->playButton->setEnabled(true); \n         ui->pauseButton->setEnabled(false); \n         ui->stopButton->setEnabled(false); \n   } \n} \n\n```\n\n5.  至于`positionChanged()`和`slot`功能，我们用它们来设置时间线滑块，以及计时器显示:\n\n```cpp\n void MainWindow::positionChanged(qint64 position) \n{ \n   if (ui->progressbar->maximum() != player->duration()) \n         ui->progressbar->setMaximum(player->duration()); \n\n   ui->progressbar->setValue(position); \n\n   int seconds = (position/1000) % 60; \n   int minutes = (position/60000) % 60; \n   int hours = (position/3600000) % 24; \n   QTime time(hours, minutes,seconds); \n   ui->durationDisplay->setText(time.toString()); \n} \n\n```\n\n6.  完成后，打开`mainwindow.ui`并右键单击每个按钮，然后选择转到插槽...接着选择`clicked()`信号。这将为每个按钮生成一个`slot`功能。这些`slot`功能的代码非常简单:\n\n```cpp\nvoid MainWindow::on_playButton_clicked() \n{  \n   player->play(); \n} \n\nvoid MainWindow::on_pauseButton_clicked() \n{ \n   player->pause(); \n} \n\nvoid MainWindow::on_stopButton_clicked() \n{ \n   player->stop(); \n} \n```\n\n7.  After that, right-click on both of the Horizontal Sliders, and select Go to slot... followed by choosing the `sliderMoved()` signal, and click OK:\n\n    ![](img/e66e337a-4d88-42fb-a93e-7499babbe61d.png)\n\n8.  只要用户拖动滑块改变位置，就会调用`sliderMoved()`信号。我们需要将此位置发送给媒体播放器，并告诉它调整音频音量或更改当前音频位置。请注意不要将音量滑块的默认位置设置为零。考虑以下代码:\n\n```cpp\nvoid MainWindow::on_volume_sliderMoved(int position) \n{ \n   player->setVolume(position); \n} \n\nvoid MainWindow::on_progressbar_sliderMoved(int position) \n{ \n   player->setPosition(position); \n} \n```\n\n9.  然后，我们需要将“文件”和“打开文件”操作添加到菜单栏中，就像我们在前面的示例项目中所做的那样。\n\n10.  然后，右键单击操作编辑器中的打开文件操作，并选择转到插槽...之后选择`triggered()`，让 Qt 为你生成一个`slot`函数。将以下代码添加到音频文件选择的`slot`功能中:\n\n```cpp\n void MainWindow::on_actionOpen_File_triggered() \n{ \n   QString fileName = QFileDialog::getOpenFileName(this,\n      \"Select Audio File\", qApp->applicationDirPath(), \n       \"MP3 (*.mp3);;WAV (*.wav)\"); \n   QFileInfo fileInfo(fileName); \n\n   player->setMedia(QUrl::fromLocalFile(fileName)); \n\n   if (player->isMetaDataAvailable()) \n   { \n         QString albumTitle = player-\n         >metaData(QMediaMetaData::AlbumTitle).toString(); \n         ui->songNameDisplay->setText(\"Playing \" + albumTitle); \n   } \n   else \n   { \n         ui->songNameDisplay->setText(\"Playing \" + \n           fileInfo.fileName()); \n   } \n\n   ui->playButton->setEnabled(true); \n   ui->playButton->click(); \n} \n\n```\n\n前面只是打开了一个文件选择对话框，只接受 MP3 和 WAV 文件。如果您愿意，您也可以添加其他格式，但是支持的格式因平台而异；因此，您应该测试它，以确保您想要使用的格式得到支持。\n\n之后，它会将选定的音频文件发送到媒体播放器进行预加载。然后，我们尝试从元数据中获取音乐的标题，并将其显示在`Labelwidget`上。但是，此功能(获取元数据)可能在您的平台上受支持，也可能不受支持，所以为了以防它不会出现，我们将其替换为音频文件名。最后，我们启用播放按钮并自动开始播放音乐。\n\n就这样。如果您现在构建并运行该项目，您应该能够获得一个简单但功能齐全的音乐播放器！\n\n![](img/7da6ec50-1bc9-4ef7-8120-2c9b755c11fd.png)\n\n# 视频播放器\n\n在前一节中，我们已经学习了如何创建音频播放器。在这一章中，我们将进一步即兴创作我们的程序，并使用 Qt 和 C++ 创建一个视频播放器。\n\n# 为视频播放器设计用户界面\n\n下一个例子是视频播放器。由于`QMediaPlayer`也支持视频输出，所以我们可以使用和前面音频播放器例子相同的用户界面和 C++ 代码，只需对其做一些小的改动。\n\n1.  首先打开`project file (.pro)`，再加入另一个关键词，叫做`multimediawidgets`:\n\n```cpp\nQT += core gui multimedia multimediawidgets \n```\n\n2.  然后，打开`mainwindow.ui`并在时间线滑块上方添加一个水平布局(命名为`movieLayout`)。之后，右键单击布局并选择变形为| QFrame。然后，我们将其大小策略属性设置为扩展，扩展:\n\n![](img/c0c92ef1-df28-4145-86a6-361aae7a70db.png)\n\n3.  之后，我们通过设置其`styleSheet`属性将 QFrame 的背景设置为黑色，如下所示:\n\n```cpp\nbackground-color: rgb(0, 0, 0); \n```\n\n4.  用户界面应该如下所示，我们就完成了:\n\n![](img/eebc0672-a33c-47c9-98f3-237b4dc4e74c.png)\n\n# 为视频播放器编写 C++ 代码\n\n要为视频播放器编写 C++ 代码，我们执行以下步骤:\n\n1.  对于`mainwindow.h`，并没有太多的改动。我们所需要做的就是在标题中加入`QVideoWidget`:\n\n```cpp\n#include <QMainWindow> \n#include <QDebug> \n#include <QFileDialog> \n#include <QMediaPlayer> \n#include <QMediaMetaData> \n#include <QTime> \n#include <QVideoWidget> \n```\n\n2.  然后，打开`mainwindow.cpp`。我们必须定义一个`QVideoWidget`对象并将其设置为视频输出目标，然后将其添加到我们在上一步中刚刚添加的`QFrame`对象的布局中:\n\n```cpp\nMainWindow::MainWindow(QWidget *parent) : \n   QMainWindow(parent), \n   ui(new Ui::MainWindow) \n{ \n   ui->setupUi(this); \n\n   player = new QMediaPlayer(this); \n\n   QVideoWidget* videoWidget = new QVideoWidget(this); \n   player->setVideoOutput(videoWidget); \n   ui->movieLayout->addWidget(videoWidget); \n\n   player->setVolume(ui->volume->value()); \n   connect(player, &QMediaPlayer::stateChanged, this, &MainWindow::stateChanged); \n   connect(player, &QMediaPlayer::positionChanged, this, &MainWindow::positionChanged); \n} \n```\n\n3.  在`slot`函数中，当打开文件动作被触发时，我们简单地改变文件选择对话框，只接受`MP4`和`MOV`格式。如果您愿意，也可以添加其他视频格式:\n\n```cpp\nQString fileName = QFileDialog::getOpenFileName(this, \"Select Movie File\", qApp->applicationDirPath(), \"MP4 (*.mp4);;MOV (*.mov)\"); \n```\n\n就这样。代码的其余部分与音频播放器示例完全相同。这个例子的主要区别是我们定义了视频输出小部件，Qt 将为我们处理剩下的部分。\n\n如果我们现在构建并运行这个项目，我们应该会得到一个非常流畅的视频播放器，就像你在这里看到的:\n\n![](img/45c1d750-a1a7-4261-8ce5-c7821acb069e.png)\n\nOn a windows system, there was a case where the video player would throw an error. This problem is similar to the one reported here: [https://stackoverflow.com/questions/32436138/video-play-returns-directshowplayerservicedoseturlsource-unresolved-error-cod](https://stackoverflow.com/questions/32436138/video-play-returns-directshowplayerservicedoseturlsource-unresolved-error-cod)\nTo resolve this error, simply download and install the K-Lite_Codec_Pack which you can find here: [https://www.codecguide.com/download_k-lite_codec_pack_basic.htm](https://www.codecguide.com/download_k-lite_codec_pack_basic.htm). After this, the video should play like a charm!\n\n# 摘要\n\n在本章中，我们学习了如何使用 Qt 创建自己的多媒体播放器。接下来是与我们通常的话题完全不同的事情。在下一章中，我们将学习如何使用 QtQuick 和 QML 来创建触摸屏友好、移动友好和面向图形的应用。"
  },
  {
    "path": "docs/cpp-gui-prog-qt5/14.md",
    "content": "# 十四、Qt Quick 和 QML\n\n在这一章中，我们将学习一些与本书其余章节非常不同的东西。Qt 由两种不同的开发应用的方法组成。第一种方法是 Qt Widgets 和 C++，我们在前面的章节中已经介绍过了。第二种方法是使用 Qt Quick 控件和 QML 脚本语言，这将在本章中介绍。\n\n在本章中，我们将涵盖以下主题:\n\n*   Qt Quick 和 QML 简介\n*   快速部件和控制\n*   Qt 快速设计器\n*   Qt 快速布局\n*   基本 QML 脚本\n\n你准备好了吗？我们开始吧！\n\n# Qt Quick 和 QML 简介\n\n在下一节中，我们将学习什么是 Qt Quick 和 QML，以及我们如何利用它们来开发 Qt 应用，而不需要编写 C++ 代码。\n\n# 介绍 Qt Quick\n\n**Qt Quick** 是 Qt 中的一个模块，它为开发面向触摸和面向视觉的应用提供了一整套用户界面引擎和语言基础设施。选择 Qt Quick 的开发人员将使用 Qt Quick 对象和控件，而不是使用通常的 Qt Widgets 进行用户界面设计。\n\n此外，开发人员将使用 QML 语言编写他们的代码，该语言具有类似于 JavaScript 的语法，而不是用 C++ 代码编写。但是，您可以使用 Qt 提供的 C++ API 来扩展 QML 应用，方法是交叉调用每种语言的函数(在 QML 调用 C++ 函数，反之亦然)。\n\n开发人员可以通过在创建项目时选择正确的选项来选择他们更喜欢的开发应用的方法。开发人员可以选择 Qt 快速应用，而不是选择通常的 Qt 小部件应用选项，该选项告诉 Qt 创建者为您的项目创建不同的启动文件和设置，以支持 Qt 快速模块:\n\n![](img/43e54681-0742-4983-9a4e-70c933538d25.png)\n\n当您创建一个 Qt 快速应用项目时，Qt 创建者会要求您为您的项目选择最低要求的 Qt 版本:\n\n![](img/3077a542-a06f-4c6a-96d6-2b84826ebc78.png)\n\n一旦您选择了一个 Qt 版本，Qt 快速设计器将决定启用哪些功能以及哪些小部件将出现在 QML 类型窗口中。我们将在后面的章节中更多地讨论这些。\n\n# 介绍 QML\n\n**QML** ( **Qt 建模语言**)是一种用户界面标记语言，用于设计触摸友好的用户界面，类似于 CSS 在 HTML 上的工作方式。与 C++ 或 JavaScript 都是命令式语言不同，QML 是一种声明式语言。在声明式编程中，您只表达脚本中的逻辑，而不描述其控制流。它只是告诉计算机做什么，而不是怎么做。然而，命令式编程需要语句来指定动作。\n\n当您打开新创建的 Qt Quick 项目时，您将在项目中看到`main.qml`和`MainForm.ui.qml`，而不是通常的`mainwindow.h`和`mainwindow.cpp`文件。您可以在以下截图中的项目目录中看到这一点:\n\n![](img/fc36ad7d-787e-4a09-bde7-f95ab7c362df.png)\n\n这是因为整个项目将主要在 QML 运行，而不是 C++。您将看到的唯一 C++ 文件是`main.cpp`，所做的只是在应用启动期间加载`main.qml`文件。在`main.cpp`中执行此操作的代码如下所示:\n\n```cpp\nint main(int argc, char *argv[]) \n{ \n   QGuiApplication app(argc, argv); \n\n   QQmlApplicationEngine engine; \n   engine.load(QUrl(QStringLiteral(\"qrc:/main.qml\"))); \n   if (engine.rootObjects().isEmpty()) \n         return -1; \n\n   return app.exec(); \n} \n```\n\n您应该已经意识到 QML 文件有两种类型，一种扩展名为`.qml`，另一种扩展名为`.ui.qml`。即使它们都运行在相同的语法上等等，它们在您的项目中服务于非常不同的目的。\n\n首先是`.ui.qml`文件(多了一个。`ui`开头)作为基于 Qt Quick 的用户界面设计的声明性文件。您可以使用 Qt 快速设计器可视化编辑器编辑`.ui.qml`文件，并轻松设计应用的图形用户界面。您也可以将自己的代码添加到文件中，但是它们可以包含的代码有一些限制，尤其是那些与逻辑代码相关的代码。当您运行您的 Qt Quick 应用时，Qt Quick 引擎将读取存储在`.ui.qml`文件中的所有信息，并相应地构建用户界面，这与 Qt Widgets 应用中使用的`.ui`文件非常相似。\n\n然后，我们有了另一个只有`.qml`扩展名的文件。该文件仅用于构建 Qt Quick 应用中的逻辑和功能，非常类似于 Qt Widget 应用中使用的`.h`和`.cpp`文件。这两种不同的格式将应用的可视化定义与其逻辑块分开。这允许开发人员将相同的逻辑代码应用于不同的用户界面模板。您不能使用 Qt 快速设计器打开`.qml`文件，因为它不用于图形用户界面声明。`.qml`文件由开发人员手工编写，他们使用的 QML 语言特性没有限制。\n\n让我们先打开`MainForm.ui.qml`，看看这两个 QML 文件的不同之处。默认情况下，Qt Creator 会打开用户界面设计器(Qt 快速设计器)；但是，让我们转到代码编辑模式，按下左侧面板上的编辑按钮:\n\n![](img/d2c87dda-ef9d-45f7-89c1-0bc053484c75.png)\n\n然后，您将能够看到 QML 脚本，它形成了您刚刚在设计模式中看到的用户界面。让我们分析一下这段代码，看看与 C++ 相比，QML 是如何工作的。在`MainForm.ui.qml`中首先看到的是这一行代码:\n\n```cpp\nimport QtQuick 2.6 \n```\n\n这很简单；我们需要导入带有适当版本号的`Qt Quick`模块。不同的 Qt Quick 版本可能有不同的功能，并支持不同的小部件控件。有时，甚至语法也可能略有不同。请确保您为您的项目选择了正确的版本，并且它支持您需要的功能。如果不知道用哪个版本，一定要考虑最新版本。\n\n接下来，我们将看到不同的图形用户界面对象(我们称之为 QML 类型)在两个大括号之间声明。我们看到的第一个是`Rectangle`型:\n\n```cpp\n    Rectangle { \n       property alias mouseArea: mouseArea \n       property alias textEdit: textEdit \n\n       width: 360 \n       height: 360 \n       ... \n```\n\n在这种情况下，`Rectangle`类型是窗口背景，很像 Qt widget Application 项目中使用的中心 Widget。让我们看看`Rectangle`下面的其他 QML 类型:\n\n```cpp\n    MouseArea { \n        id: mouseArea \n        anchors.fill: parent \n    } \n\n    TextEdit { \n        id: textEdit \n        text: qsTr(\"Enter some text...\") \n        verticalAlignment: Text.AlignVCenter \n        anchors.top: parent.top \n        anchors.horizontalCenter: parent.horizontalCenter \n        anchors.topMargin: 20 \n        Rectangle { \n            anchors.fill: parent \n            anchors.margins: -10 \n            color: \"transparent\" \n            border.width: 1 \n        } \n    } \n```\n\n`MousArea`类型，顾名思义，是检测鼠标点击和触摸事件的无敌形状。通过在上面放一个`MouseArea`，你基本上可以把任何东西变成一个按钮。在此之后，我们还有一个`TextEdit`类型，它的行为完全类似于 Qt 小部件应用中的`Line Edit`小部件。\n\n您可能已经注意到`Rectangle`声明中有两个属性带有`alias`关键字。这两个属性公开了`MouseArea`和`TextEdit`类型，并允许其他 QML 脚本与之交互，接下来我们将学习如何操作。\n\n现在，打开`main.qml`看看它的代码:\n\n```cpp\nimport QtQuick 2.6 \nimport QtQuick.Window 2.2 \n\nWindow { \n    visible: true \n    width: 640 \n    height: 480 \n    title: qsTr(\"Hello World\") \n\n    MainForm { \n        anchors.fill: parent \n        mouseArea.onClicked: { \n            console.log(qsTr('Clicked on background. Text: \"' + \n            textEdit.text + '\"')) \n        } \n    } \n} \n```\n\n在上面的代码中，有一个`Window`类型，只有通过导入`QtQuick.Window`模块才能使用。设置`Window`类型的属性后，声明`MainForm`类型。这种`MainForm`类型实际上是我们之前在`MainForm.ui.qml`中看到的整个用户界面。由于`MouseArea`和`TextEdit`类型已经在`MainForm.ui.qml`中暴露，我们现在可以在`main.qml`中访问和使用它们。\n\nQML 还使用了 Qt 提供的信号和时隙机制，但是编写形式略有不同，因为我们不再编写 C++ 代码了。例如，我们可以看到上面的代码中使用了`onClicked`，这是一个内置信号，相当于 Qt Widgets 应用中的`clicked()`。由于`.qml`文件是我们定义应用逻辑的地方，我们可以定义当`onClicked`被调用时会发生什么。另一方面，我们不能在`.ui.qml`做同样的事情，因为它只允许与视觉相关的代码。如果您试图在`.ui.qml`文件中编写逻辑相关的代码，您将收到 Qt Creator 的警告。\n\n就像 Qt Widgets 应用一样，您也可以像以前一样构建和运行项目。默认示例应用如下所示:\n\n![](img/cfbfa37c-2ba3-4e65-b456-4735c5c90efa.png)\n\n您可能会意识到构建过程非常快。这是因为默认情况下，QML 代码不会编译成二进制。QML 是一种解释语言，就像 JavaScript 一样，因此不需要编译就可以执行。在构建过程中，所有的 QML 文件都会被打包到应用的资源系统中。然后，一旦应用启动，QML 文件将被 Qt Quick 引擎加载和解释。\n\n但是，您仍然可以选择将您的 QML 脚本编译成二进制，使用 Qt 中包含的`Qt Quick Compiler`程序，使代码执行比平时稍快。除非您试图在资源非常有限的嵌入式系统上运行应用，否则这是一个不需要的可选步骤。\n\n现在我们已经理解了什么是 **Qt Quick** 和 **QML** 语言，让我们来看看 Qt 提供的所有不同的 QML 类型。\n\n# 快速部件和控制\n\n在 Qt Quick 的领域中，小部件和控件被称为`QML types`。默认情况下， **Qt 快速设计器**为我们提供了一组基本的 QML 类型。您还可以导入不同模块附带的其他 QML 类型。此外，如果现有的 QML 类型都不符合您的需求，您甚至可以创建自己的自定义类型。\n\n让我们看看默认情况下 Qt 快速设计器附带了哪些 QML 类型。首先，下面是基本类别下的 QML 类型:\n\n![](img/87221c3c-f5cb-4409-a1aa-2e1b86f76030.png)\n\n让我们看看不同的选项:\n\n*   **边框图像**:边框图像是一种 QML 类型，旨在创建可缩放的矩形形状，可以保持其角形状和边框。\n*   **Flickable** : Flickable 是一个 QML 类型，包含它的所有子类型，并在其剪辑区域内显示它们。Flickable 也被扩展并被`ListView`和`GridView`类型用于滚动长内容。它也可以通过触摸屏轻击手势来移动。\n*   **焦点范围**:焦点范围是一个低级的 QML 类型，用来方便其他 QML 类型的构建，可以在被按下或者释放的时候获取键盘焦点。我们通常不会直接使用这个 QML 类型，而是使用直接从它继承的其他类型，比如`GroupBox`、`ScrollView`、`StatusBar`等等。\n*   **形象**:这种`Image`类型不言自明。它在本地或从网络加载图像。\n*   **项**:`Item`类型是 Qt Quick 中所有视觉项最基本的 QML 类型。Qt Quick 中的所有可视化项目都继承自这个`Item`类型。\n*   **鼠标** **区域**:我们已经看到了默认 Qt 快速应用项目中`MouseArea`类型的用法示例。它检测预定义区域内的鼠标点击和触摸事件，并在检测到时调用点击信号。\n*   **矩形** : A `Rectangle` QML 类型与`Item`类型非常相似，只是它有一个可以填充纯色或渐变的背景。或者，您也可以用自己的颜色和厚度为其添加边框。\n*   **文字**:`Text`QML 类型也挺不言自明的。它只是在窗口上显示一行文本。您可以使用它来显示具有特定字体系列和字体大小的纯文本和富文本。\n*   **文本编辑**:文本编辑 QML 类型相当于 Qt Widgets 应用中的`Text Edit`小部件。它允许用户在聚焦时键入文本。它可以显示纯文本和格式化文本，这与`Text Input`类型有很大不同。\n*   **文本输入**:文本输入 QML 类型相当于 Qt Widgets 应用中的 Line Edit widget，它只能显示一行可编辑的纯文本，与`Text Edit`类型不同。您还可以通过验证器或输入掩码对其应用输入约束。通过将`echoMode`设置为`Password`或`PasswordEchoOnEdit`，也可用于密码输入字段。\n\n我们在这里讨论的 QML 类型是默认情况下 Qt 快速设计器附带的最基本的类型。这些也是用于构建其他一些更复杂的 QML 类型的基本构件。Qt Quick 附带了许多额外的模块，我们可以将其导入到我们的项目中，例如，如果我们在`MainForm.ui.qml`文件中添加以下行:\n\n```cpp\nimport QtQuick.Controls 2.2\n```\n\n当您切换到设计模式时，您的 Qt 快速设计器上会出现一堆附加的 QML 类型:\n\n![](img/64d47d41-00c4-45c9-9bbe-80aff6dc8bd3.png)\n\nWe won't go through all these QML types one by one, as there are too many of them. If you are interested in learning more about these QML types, please visit the following link:[ https://doc.qt.io/qt-5.10/qtquick-controls-qmlmodule.html](https://doc.qt.io/qt-5.10/qtquick-controls-qmlmodule.html)\n\n# Qt 快速设计器\n\n接下来，我们将查看 Qt 快速应用项目的 Qt 快速设计器布局。当你打开一个`.ui.qml`文件，Qt 快速设计器，Qt Creator 工具集中包含的设计器工具，会自动为你启动。\n\n那些从本书第一章开始就关注所有示例项目的人可能会意识到 Qt 快速设计器看起来与我们一直使用的有点不同。这是因为 Qt Quick 项目与 Qt Widgets 项目有很大的不同，所以设计工具自然也应该看起来不同，以满足其需求。\n\n让我们看看 Qt 快速设计器在 Qt 快速项目中的外观:\n\n![](img/a20022be-e9e5-4630-baa7-4c0b82b689c5.png)\n\n1.  库:“库”窗口显示当前项目可用的所有 QML 类型。您可以点按它并将其拖到画布窗口，以将其添加到您的用户界面。您也可以创建自己的自定义 QML 类型并在此显示。\n2.  资源:“资源”窗口显示列表中的所有资源，这些资源可以在用户界面设计中使用。\n3.  导入:通过“导入”窗口，您可以将不同的 Qt 快速模块导入到当前项目中。\n4.  导航器:“导航器”窗口以树形结构显示当前 QML 文件中的项目。它类似于 Qt Widgets 应用项目中的对象操作窗口。\n5.  连接:“连接”窗口由几个不同的选项卡组成:连接、绑定、属性和后端。这些选项卡允许您向 QML 文件添加连接(信号和插槽)、绑定和属性，而无需切换到编辑模式。\n6.  状态窗格:状态窗格显示 QML 项目中通常描述用户界面配置的不同状态，例如用户界面控件、其属性和行为以及可用的操作。\n7.  画布:画布是设计应用用户界面的工作区域。\n8.  属性窗格:类似于我们在 Qt Widgets 应用项目中使用的属性编辑器，QML 设计器中的属性窗格显示所选项目的属性。更改此处的值后，您可以立即在用户界面中看到结果。\n\n# Qt 快速布局\n\n就像 Qt Widget 应用一样，Qt Quick 应用中也存在布局系统。唯一的区别是它被称为快速定位:\n\n![](img/ee431b85-661f-47d1-9709-8f4d7a64297b.png)\n\n最明显的相似之处是列和行定位器。这两个与 Qt Widgets 应用中的垂直布局和水平布局完全相同。除此之外，网格定位器也与网格布局相同。\n\nQt Quick 中唯一额外的东西是流量定位器。流动定位器中包含的项目像页面上的文字一样排列，项目沿着一个轴排成一行，项目沿着另一个轴并排放置。\n\n![](img/931898a3-240c-472c-91f7-58409ec5cbc9.png)\n\n# 基本 QML 脚本\n\n在下一节中，我们将学习如何使用 Qt Quick Designer 和 QML 创建我们的第一个 Qt Quick 应用！\n\n# 设置项目\n\n不用多说，让我们把我们的手放在 QML 身上，自己创建一个 Qt Quick 应用！对于这个示例项目，我们将使用 Qt 快速设计器和 QML 脚本创建一个虚拟登录屏幕。首先，让我们打开 Qt Creator，通过转到文件|新文件或项目来创建一个新项目...\n\n之后，选择 Qt 快速应用并按选择....之后，一直按“下一步”，直到创建项目。我们将使用这个示例项目的所有默认设置，包括最低要求的 Qt 版本:\n\n![](img/f61ed1c4-6c26-438d-a9d0-adfe3d663049.png)\n\n创建项目后，我们需要向项目中添加一些图像文件，以便以后使用:\n\n![](img/984d0ff3-798c-46fe-9d4c-5b9745e3590c.png)\n\nYou can get the source files (including these images) at our GitHub page: [http://github.com/PacktPublishing/Hands-On-GUI-Programming-with-C-QT5](http://github.com/PacktPublishing/Hands-On-GUI-Programming-with-C-QT5)\n\n我们可以通过右键单击项目窗格中的`qml.qrc`文件并选择在编辑器中打开来将这些图像添加到我们的项目中。添加一个名为`images`的新前缀，并将所有图像文件添加到该前缀中:\n\n![](img/44cb6357-0d0f-4a8c-83c4-c8182c2cafbb.png)\n\n之后，打开`MainForm.ui.qml`，删除 QML 文件中的所有内容。我们从头开始，在画布上添加一个项目类型，将其大小设置为 400 x 400，并将其称为`loginForm`。之后，在下面加一个`Image`类型，称之为`background`。然后我们将背景图像应用到`Image`类型，画布现在看起来像这样:\n\n![](img/885c2157-4e28-477d-8ccd-ebc2c3e669ec.png)\n\n然后，在`Image`类型(背景)下添加一个`Rectangle`类型，并在属性窗格中打开布局选项卡。启用垂直和水平定位选项。之后，将`width`设置为`402`，将`height`设置为`210`，将`vertical anchor margin`设置为`50`:\n\n![](img/4b63ff3b-64b7-4cf3-919d-de2c5407db44.png)\n\n接下来，我们将矩形的颜色设置为`#fcf9f4`，边框颜色设置为`#efedeb`，然后将边框值设置为`1`。到目前为止，用户界面如下所示:\n\n![](img/2046715a-edfc-44b7-bd96-490ea0da78b6.png)\n\n接下来，在矩形下添加图像 QML 类型，并将其锚点设置为顶部锚点和水平锚点。然后，我们将其顶部锚点边距设置为`-110`，并将徽标图像应用到其`image source`属性。您可以通过点按画布顶部的小按钮来打开和关闭 QML 类型的边框和条纹，以便更容易查看结果，尤其是当画布上堆满了东西时:\n\n![](img/fd8bc259-88d1-458d-bcae-9d0aa18e09ab.png)\n\n然后，我们在`loginRect`矩形下的画布上添加三个`Rectangle`类型，分别叫做`emailRect`、`passwordRect`和`loginButton`。矩形的锚点设置如下所示:\n\n![](img/b5ff6a01-25f6-4081-92bb-6e0607e3bf95.png)\n\n然后，我们将`emailRect`和`passwordRect`的`border`值设置为`1`，将`color`设置为`#ffffff`，将`bordercolor`设置为`#efedeb`。至于`loginButton`，我们将`border`设置为`0`，将`radius`设置为`2`，将`color`设置为`#27ae61`。登录屏幕现在如下所示:\n\n![](img/d5578076-0799-402b-a013-554a56330e25.png)\n\n目前看来不错。接下来，我们将在`emailRect`和`passwordRect`上添加一个`TextInput`、`Image`、`MouseArea`和一个`Text` QML 类型。由于这里有许多 QML 类型，我将列出需要设置的属性:\n\n*   TextInput 组件:\n    *   选择颜色设置为`#4f0080`\n    *   启用左锚、右锚和垂直锚\n    *   左定位边距`20`、右定位边距`40`和垂直边距 3\n    *   将回声模式设置为密码，仅用于密码输入\n*   图像:\n    *   启用右锚和垂直锚\n    *   右锚定边距设置为`10`\n    *   将图片来源分别设置为电子邮件图标或密码图标\n    *   将图像填充模式设置为保留预期\n\n*   鼠标区域:\n    *   启用填充父项目\n*   文本:\n    *   将文本属性分别设置为`E-Mail`和`Password`\n    *   文本颜色设置为`#cbbdbd`\n    *   文本对齐方式设置为左对齐和上对齐\n    *   启用左锚、右锚和垂直锚\n    *   左锚定边距`20`、右锚定边距`40`和垂直边距-1\n\n完成后，也在`loginButton`上添加一个`MouseArea`和`Text`。启用`MouseArea`的`fill parent item`，启用【QML】类型的`vertical`和`horizontal anchors`。然后，将其`text`属性设置为`LOGIN`。\n\n你不必 100%遵循我的所有步骤，它们只是一个指导方针，让你达到与上面截图相似的结果。但是，你最好应用自己的设计，创造一些独特的东西！\n\n唷！经过上述漫长的过程，我们的登录屏幕现在应该如下所示:\n\n![](img/b17e64f5-faef-47a9-abd5-88669f766d47.png)\n\n在进入`main.qml`之前，我们需要做的最后一件事是在我们的登录屏幕中展示一些 QML 类型，这样我们就可以将其链接到我们的`main.qml`文件进行逻辑编程。事实上，我们可以直接在设计工具上做到这一点。您只需要点击位于对象名称旁边的小矩形图标，并确保图标上的三条线穿透矩形框，如下所示:\n\n![](img/882dcceb-99d9-4358-9e59-a444ab53d9d3.png)\n\n我们需要公开/导出的 QML 类型有`emailInput`(文本输入)、`emailTouch`(鼠标区域)、`emailDisplay`(文本)、`passwordInput`(文本输入)、`passwordTouch`(鼠标区域)、`passwordDisplay`(文本)和`loginMouseArea`(鼠标区域)。一旦你完成了所有这些，让我们打开`main.qml`。\n\n起初，我们的`main.qml`应该是这样的，它只会打开一个空窗口:\n\n```cpp\nimport QtQuick 2.6 \nimport QtQuick.Window 2.2 \n\nWindow { \n    id: window \n    visible: true \n    width: 800 \n    height: 600 \n    title: qsTr(\"My App\") \n} \n```\n\n之后，加入`MainForm`对象，将其锚点设置为`anchors.fill: parent`。然后，当点击`loginButton`时(或触摸，如果在触摸设备上运行)，在控制台窗口上打印出一行文本`Login pressed`:\n\n```cpp\nWindow { \n    id: window \n    visible: true \n    width: 800 \n    height: 600 \n    title: qsTr(\"My App\") \n\n    MainForm \n    { \n        anchors.fill: parent \n\n        loginMouseArea.onClicked: \n        { \n            console.log(\"Login pressed\"); \n        } \n    } \n} \n```\n\n之后，我们将对点击/触摸电子邮件输入上的`MouseArea`时的行为进行编程。由于我们是手动创建自己的文本字段，而不是使用`QtQuick.Controls`模块提供的`TextField` QML 类型，我们必须手动隐藏和显示`E-Mail`和`Password`文本显示，以及当用户点击/触摸`MouseArea`时改变输入焦点。\n\n之所以选择不用`TextField`类型，是因为我几乎无法定制`TextField's`的视觉呈现，那为什么不自己创作呢？手动聚焦电子邮件输入的代码如下所示:\n\n```cpp\nemailTouch.onClicked: \n{ \n    emailDisplay.visible = false;      // Hide emailDisplay \n    emailInput.forceActiveFocus();     // Focus emailInput \n    Qt.inputMethod.show();       // Activate virtual keyboard \n} \n\nemailInput.onFocusChanged: \n{ \n    if (emailInput.focus == false && emailInput.text == \"\") \n    { \n        emailDisplay.visible = true;   // Show emailDisplay if \n        emailInput is empty when loses focus \n    } \n} \n```\n\n之后，对密码字段执行相同的操作:\n\n```cpp\npasswordTouch.onClicked: \n{ \n    passwordDisplay.visible = false;   // Hide passwordDisplay \n    passwordInput.forceActiveFocus();  // Focus passwordInput \n    Qt.inputMethod.show();       // Activate virtual keyboard \n} \n\npasswordInput.onFocusChanged: \n{ \n    if (passwordInput.focus == false && passwordInput.text == \"\") \n    { \n        passwordDisplay.visible = true;      // Show passwordDisplay if  \n        passwordInput is empty when loses focus \n    } \n} \n```\n\n就是这样；我们完了！现在，您可以编译并运行该程序。你应该得到这样的东西:\n\n![](img/e4f5430c-1afb-4481-842b-fa0dcb61ffea.png)\n\n如果您没有看到图像，并且收到错误消息说 Qt 无法打开图像，请回到您的`MainForm.ui.qml`并在源属性的前面添加前缀`img/`。这是因为 Qt 快速设计器加载的图像没有前缀，而您的最终程序需要前缀。添加前缀后，您可能会意识到您不再看到图像显示在 Qt 快速设计器上，但它将在您的最终程序中正常工作。\n\n我不确定这是一个 bug，还是他们故意的。希望 Qt 的开发人员能把它修好，我们就不用再做那个额外的步骤了。就是这样；希望您已经理解了 Qt Widgets 应用和 Qt Quick 应用之间的异同。现在，您可以从这两个选项中选择最适合您项目需求的选项！\n\n# 摘要\n\n在本章中，我们学习了什么是 Qt Quick，以及如何使用 QML 语言创建程序。在下一章中，我们将学习如何将我们的 Qt 项目导出到不同的平台，而不会有太多的麻烦。走吧！"
  },
  {
    "path": "docs/cpp-gui-prog-qt5/15.md",
    "content": "# 十五、跨平台开发\n\n自从第一次发布以来，Qt 就以其跨平台能力而闻名。这也是创始人决定创建这个框架时的主要目标之一，早在它被**诺基亚**以及后来的**Qt 公司**接管之前。\n\n在本章中，我们将涵盖以下主题:\n\n*   编译程序\n*   生成设置\n*   部署到电脑平台\n*   部署到移动平台\n\n我们开始吧。\n\n# 理解编译器\n\n在本章中，我们将了解从 Qt 项目生成可执行文件的过程。这个过程就是我们所说的**编译**或者**构建**。用于此目的的工具称为**编译器**。在下一节中，我们将学习什么是编译器，以及如何使用它来为我们的 Qt 项目生成可执行文件。\n\n# 什么是编译器？\n\n当我们使用 Qt 或任何其他软件开发工具包开发一个应用时，我们经常不得不将我们的项目编译成可执行文件，但是当我们编译我们的项目时，实际上发生了什么呢？\n\n一个**编译器**是一个把用高级编程语言或计算机指令编写的计算机代码转换成可以被计算机读取和执行的机器代码或低级形式的软件。根据您运行的操作系统和计算机处理器，这种低级机器代码有很大的不同，但是您不必担心它，因为编译器会为您转换它。\n\n这意味着你所需要担心的是用人类可读的编程语言编写逻辑代码，并让编译器为你完成这项工作。理论上，通过使用不同的编译器，您应该能够将代码编译成可在不同操作系统和硬件上运行的可执行程序。我在这里理论上使用*这个词，是因为实际上这比仅仅使用不同的编译器要困难得多，你可能还需要实现支持目标平台的库。然而，Qt 已经为你处理了所有这些，所以你不必做额外的工作。*\n\n *在当前版本中，Qt 支持以下编译器:\n\n*   **GNU 编译器集合(GCC)** : GCC 是 Linux 和 macOS 的编译器\n*   **MinGW(Windows 的极简 GNU)**:MinGW 是 GCC 和 GNU Binutils(二进制实用程序)的原生软件端口，用于在 Windows 上开发应用\n*   **微软 Visual C++ (MSVC)** : Qt 支持 MSVC 2013、2015、2017 三个版本构建 Windows 应用\n*   **XCode** : XCode 是为 macOS 和 iOS 开发应用的开发人员使用的主要编译器\n*   **Linux ICC(英特尔 C++ 编译器)** : Linux ICC 是英特尔针对 Linux 应用开发的一套 C 和 C++ 编译器的编译器\n*   **Clang** : Clang 是 Windows、Linux 和 macOS 的 LLVM 编译器的 C、C++、Objective C 和 Objective C++ 前端\n*   **Nim** : Nim 是 Windows、Linux 和 macOS 的 Nim 编译器\n*   **QCC** : QCC 是为 QNX 操作系统编译 C++ 应用的接口\n\n# 用 Make 构建自动化\n\n在软件开发中， **Make** 是一个构建自动化工具，通过读取名为 **Makefiles** 的配置文件，指定如何派生目标平台，从而自动从源代码构建可执行程序和库。简而言之，Make 程序生成构建配置文件，并使用它们来告诉编译器在生成最终的可执行程序之前要做什么。\n\nQt 支持两种类型的 Make 程序:\n\n*   **qmake** :是 Qt 团队开发的原生 make 程序。它在 Qt Creator 上工作得最好，我强烈建议将其用于所有 Qt 项目。\n*   **CMake** :另一方面，虽然这是一个非常强大的构建系统，但是它并没有完成 qmake 专门为一个 Qt 项目所做的所有事情，比如:\n    *   运行**元对象编译器** ( **主运行中心**)\n    *   告诉编译器在哪里寻找 Qt 头\n    *   告诉链接器在哪里寻找 Qt 库\n\n为了成功编译一个 Qt 项目，你必须在 CMake 上手动执行前面的步骤。只有在下列情况下，才应该使用 CMake:\n\n*   您正在处理一个非 Qt 项目，但是希望使用 Qt 创建器来编写代码\n\n*   您正在处理一个需要复杂配置的大型项目，而 qmake 根本无法处理\n\n*   你真的很喜欢用 CMake，而且你很清楚自己在做什么\n\nQt 在为您的项目选择合适的工具时非常灵活。它不仅仅坚持自己的构建系统和编译器。它给了开发者选择最适合他们项目的自由。\n\n# 生成设置\n\n在编译或构建项目之前，编译器需要知道几个细节才能继续。这些细节被称为**构建设置**，这是编译过程中非常重要的一个方面。在下一节中，我们将了解什么是构建设置，以及如何以准确的方式配置它们。\n\n# Qt 项目(。专业)文件\n\n我相信你已经知道 **Qt 项目文件**，因为我们在整本书中已经无数次提到它。一个`.pro`文件实际上是 *qmake* 用来构建你的应用、库或插件的项目文件。它包含所有的信息，比如到头文件和源文件的链接、项目所需的库、不同平台/环境的定制过程等等。一个简单的项目文件可能如下所示:\n\n```cpp\nQT += core gui widgets \n\nTARGET = MyApp \nTEMPLATE = app \n\nSOURCES +=  \n        main.cpp  \n        mainwindow.cpp \n\nHEADERS +=  \n        mainwindow.h \n\nFORMS +=  \n        mainwindow.ui \n\nRESOURCES +=  \n    resource.qrc \n```\n\n它只是告诉 qmake 哪些 Qt 模块应该包含在项目中，可执行程序的名称是什么，应用的类型是什么，最后是指向需要包含在项目中的头文件、源文件、表单声明文件和资源文件的链接。所有这些信息对于 qmake 生成配置文件和成功构建应用都是至关重要的。对于更复杂的项目，您可能希望针对不同的操作系统对项目进行不同的配置。这也可以在 Qt 项目文件中轻松完成。\n\nTo learn more about how you can configure your project differently for different operating systems, please refer to the following link: [http://doc.qt.io/qt-5/qmake-language.html#scopes-and-conditions.](http://doc.qt.io/qt-5/qmake-language.html#scopes-and-conditions)\n\n# 评论\n\n您可以在项目文件中添加自己的注释，提醒自己添加特定配置行的目的，这样您就不会忘记为什么在一段时间没有接触后添加了一行。注释以散列符号(`#`)开始，之后您可以编写任何内容，因为构建系统将简单地忽略整行文本。例如:\n\n```cpp\n# The following define makes your compiler emit warnings if you use \n# any feature of Qt which has been marked as deprecated (the exact warnings \n# depend on your compiler). Please consult the documentation of the \n# deprecated API in order to know how to port your code away from it. \nDEFINES += QT_DEPRECATED_WARNINGS \n```\n\n您还可以添加虚线或使用空格来使您的评论与众不同:\n\n```cpp\n#------------------------------------------------- \n# \n# Project created by QtCreator 2018-02-18T01:59:44 \n# \n#------------------------------------------------- \n```\n\n# 模块、配置和定义\n\n您可以向项目中添加不同的 Qt 模块、配置选项和定义。让我们看看如何实现这些。要添加其他模块，只需在`QT +=`后面添加`module`关键字，如下所示:\n\n```cpp\nQT += core gui sql printsupport charts multimedia \n```\n\n或者您也可以在前面添加一个条件，以确定何时将特定模块添加到项目中:\n\n```cpp\ngreaterThan(QT_MAJOR_VERSION, 4): QT += widgets \n```\n\n您也可以将配置设置添加到项目中。例如，我们想特别要求编译器在编译我们的项目时遵循 2011 版的 C++ 规范(称为 C++ 11)，并使其成为多线程应用:\n\n```cpp\nCONFIG += qt c++ 11 thread\n```\n\nYou must use `+=`, not `=`, or qmake will not be able to use Qt's configuration to determine the settings needed for your project. Alternatively, you can also use `-=` to remove a module, configuration, and definition from your project.\n\n至于给我们的编译器添加定义(或变量)，我们使用`DEFINES`关键字，如下所示:\n\n```cpp\nDEFINES += QT_DEPRECATED_WARNINGS \n```\n\n在编译您的项目之前，Qmake 将此变量的值作为编译器 C 预处理器宏(`-D`选项)添加。早期的定义告诉 Qt 编译器，如果您使用了被标记为不推荐使用的 Qt 的任何功能，就会发出警告。\n\n# 平台特定设置\n\n您可以为不同的平台设置不同的配置或设置，因为不是每个设置都能适合所有的用例。例如，如果我们想要为不同的操作系统包含不同的头路径，我们可以执行以下操作:\n\n```cpp\nwin32:INCLUDEPATH += \"C:/mylibs/extra headers\" \nunix:INCLUDEPATH += \"/home/user/extra headers\" \n```\n\n或者，您也可以将设置放在大括号中，其行为类似于编程语言中的`if`语句:\n\n```cpp\nwin32 { \n    SOURCES += extra_code.cpp \n} \n```\n\nYou can check out all the settings you can use in your project file by visiting the following link: [http://doc.qt.io/qt-5/qmake-variable-reference.html.](http://doc.qt.io/qt-5/qmake-variable-reference.html)\n\n# 部署到电脑平台\n\n让我们继续学习如何在 Windows、Linux 和 macOS 等平台上部署我们的应用。\n\n# Windows 操作系统\n\n在本节中，我们将学习如何将我们的应用部署到不同的操作系统。尽管 Qt 开箱即用地支持所有主要平台，但为了使您的应用易于部署到所有平台，您可能需要设置一些配置。\n\n我们要介绍的第一个操作系统是最常见的一个，微软视窗系统。\n\nStarting from Qt 5.6, **Windows XP** is no longer supported by Qt.\n\n可能有某些插件在您尝试部署的 Windows 版本上无法正常工作，所以在决定进行项目之前，请务必查看文档。然而，可以肯定地说，Qt 的大部分功能都将开箱即用。\n\n默认情况下，当你把 MinGW 32 位编译器安装到你的电脑上时，它会和 Qt 一起出现。不幸的是，它默认不支持 64 位，除非你从源代码编译 Qt。如果你需要构建 64 位应用，你可以考虑在安装**微软 Visual Studio** 的同时安装 Qt 的 MSVC 版本。微软 Visual Studio 可从以下链接免费获取:[https://www.visualstudio.com/vs](https://www.visualstudio.com/vs)。\n\n您可以在 Qt Creator 中设置编译器设置，方法是转到工具|选项，然后转到构建和运行类别，并选择工具包选项卡:\n\n![](img/4061d513-9767-4724-9382-a1c09d727cc1.png)\n\n如您所见，有多个在不同编译器上运行的工具包，您可以在其中进行配置。默认情况下，Qt 已经配备了五个套件——一个用于安卓，一个用于 MinGW，三个用于 MSVC(2013、2015 和 2017 版本)。Qt 将自动检测这些编译器的存在，并相应地为您配置这些设置。\n\n如果您没有安装 **Visual Studio** 或 **Android SDK** ，套件选项前面会出现一个带感叹号的红色图标。安装所需的编译器后，尝试重新启动 Qt Creator。它现在将检测新安装的编译器。你应该没有问题为 Windows 平台编译，因为 Qt 将为你处理其余的。我们将在另一节中更多地讨论安卓平台。\n\n编译完应用后，打开安装 Qt 的文件夹。将相关的动态链接库文件复制到您的应用文件夹中，并将其打包，然后分发给您的用户。没有这些 DLL 文件，您的用户可能无法运行 Qt 应用。\n\nFor more information, please visit the following link: [http://doc.qt.io/qt-5/windows-deployment.html.](http://doc.qt.io/qt-5/windows-deployment.html)\n\n至于为应用设置自定义图标，您必须将以下代码添加到您的项目(`.pro`)文件中:\n\n```cpp\nwin32:RC_ICONS = myappico.ico \n```\n\n前面的代码只在 Windows 平台上有效，这就是为什么我们要在它前面加上`win32`关键字。\n\n# Linux 操作系统\n\n**Linux** (或 GNU/Linux)一般被认为是主导云/服务器市场的主要操作系统。由于 Linux 不像 Windows 或 macOS 那样是一个单一的操作系统(Linux 是由不同的供应商以不同的 Linux 发行版的形式提供的，它们并不完全兼容)，所以开发人员很难构建他们的应用，并期望它们在不同的 Linux 发行版(**发行版**)上完美运行。但是，如果您在 Qt 上开发您的 Linux 应用，只要 Qt 库存在于目标系统上，它就很有可能在大多数发行版上工作，如果不是在所有主要发行版上的话。\n\nLinux 上的默认工具包选择比 Windows 简单得多。由于 64 位应用已经成为大多数 Linux 发行版的主流和标准已经有一段时间了，所以我们在安装 Qt 时只需要包含 **GCC** 64 位编译器。安卓也有一个选项，但我们稍后会详细讨论:\n\n![](img/39cbb752-788b-4f19-be8e-80a6fe79aecb.png)\n\n如果您是第一次在 Qt Creator 上编译您的 Linux 应用，我非常肯定您会得到以下错误:\n\n![](img/8cb011d6-5884-41c7-9359-949a3da431c1.png)\n\n这是因为您没有安装构建 Linux 应用所需的相关工具，如 Make、GCC 和其他程序。\n\n不同的 Linux 发行版安装程序的方法略有不同，但我不会在这里一一解释。在我的例子中，我使用的是一个 Ubuntu 发行版，所以我首先打开终端，输入以下命令来安装包含 Make 和 GCC 的`build-essential`包:\n\n```cpp\nsudo apt-get install build-essential \n```\n\n前面的命令只适用于继承自 **Debian** 和 **Ubuntu** 的发行版，可能不适用于其他发行版，如 **Fedora** 、 **Gentoo** 、 **Slackware** 等。您应该搜索 Linux 发行版用来安装这些包的适当命令，如下图所示:\n\n![](img/ca215015-a2a1-4372-bc16-b295382402fb.png)\n\n安装适当的软件包后，重新启动 Qt Creator 并转到工具|选项。然后，转到构建和运行类别，打开工具包选项卡。现在，您应该能够为桌面工具包选择 C 和 C++ 选项的编译器:\n\n![](img/d8efa23d-958b-49c4-a0af-9d9ab6ef91b5.png)\n\n但是，在尝试再次编译时，您可能会得到另一个错误，显示无法找到-lGL:\n\n![](img/3fa99b4a-7ff3-4fbb-a194-4fadbb2b8cc1.png)\n\n这是因为 Qt 正在尝试寻找`OpenGL`库，在你的系统上找不到。这可以通过使用以下命令安装`Mesa development`库包来轻松解决:\n\n```cpp\nsudo apt-get install libgl1-mesa-dev \n```\n\n同样，前面的命令只适用于 Debian 和 Ubuntu 变体。如果您没有运行 Debian 或 Ubuntu 分叉之一，请为您的 Linux 发行版寻找合适的命令:\n\n![](img/469afd43-8058-490f-87dd-f89d97955bd7.png)\n\n一旦安装了这个包，您应该能够毫无问题地编译和运行您的 Qt 应用:\n\n![](img/f90b95a8-a587-4cfd-8c60-c291a40177ae.png)\n\n至于使用其他不太流行的编译器之一，如 **Linux ICC** 、 **Nim** 或 **QCC** ，您必须通过单击位于 Kits 界面右侧的添加按钮手动设置它，然后键入所有适当的设置以使其工作。大多数人不使用这些编译器，所以我们暂时跳过它们。\n\n说到分发 Linux 应用，它比 Windows 或 macOS 复杂得多。这是因为 Linux 不是一个单一的操作系统，而是一堆不同的发行版，它们有自己的依赖关系和配置，这使得分发程序非常困难。\n\n最安全的方法是静态编译你的程序，这有它自己的优缺点。你的程序将会变得非常庞大，这使得更新软件对那些网速慢的用户来说是一个很大的负担。除此之外，如果你没有做开源项目，没有 Qt 商业许可，Qt 许可也禁止你静态构建。要了解更多关于 Qt 许可选项的信息，请访问以下链接:[https://www1.qt.io/licensing-compariso.n.](https://www1.qt.io/licensing-comparison)\n\n另一种方法是要求用户在运行应用之前安装正确版本的 Qt，但这将在用户方面产生大量问题，因为不是每个用户都非常精通技术，并且有耐心经历所有这些麻烦来避免依赖地狱。\n\n因此，最好的方法是将 Qt 库与您的应用一起分发，就像我们在 Windows 平台上所做的那样。该库可能在某些 Linux 发行版上不起作用(很少出现这种情况，但有一点可能性)，但这可以通过为不同的发行版创建不同的安装程序来轻松克服，现在每个人都很高兴。\n\n但是，出于安全原因，默认情况下，Linux 应用通常不会在其本地目录中查找其依赖项。您必须在 qmake 项目(`.pro`)文件的可执行文件`rpath`设置中使用`$ORIGIN`关键字:\n\n```cpp\nunix:!mac{ \nQMAKE_LFLAGS += -Wl,--rpath=$$ORIGIN \nQMAKE_RPATH= \n} \n```\n\n设置`QMAKE_RPATH`将清除 Qt 库的默认`rpath`设置。这允许将 Qt 库与应用捆绑在一起。如果你想让`rpath`包含 Qt 库的路径，不要设置`QMAKE_RPATH`。\n\n之后，只需将 Qt 安装文件夹中的所有库文件复制到应用的文件夹中，并从文件名中删除其次要版本号。例如，将`libQtCore.so.5.8.1`重命名为`libQtCore.so.5`，现在它应该能够被您的 Linux 应用检测到。\n\n至于应用图标，默认情况下，您不能将任何图标应用于 Linux 应用，因为它不受支持。即使一些桌面环境如 KDE 和 GNOME 确实支持应用图标，但图标必须手动安装和配置，这对用户来说不是很方便。它甚至可能无法在某些用户的电脑上工作，因为每个发行版的工作方式与其他发行版略有不同。为应用设置图标的最佳方式是在安装过程中创建桌面快捷方式(symlink)，并将图标应用于快捷方式。\n\n# 苹果电脑\n\n在我看来， **macOS** 是软件界最单一最集中的操作系统。它不仅设计为仅在麦金塔电脑上运行，还要求您仅从苹果应用商店下载或购买软件。\n\n毫无疑问，这让一些关心选择自由的人感到不安，但另一方面，这也意味着开发人员在应用构建和分发方面需要处理的问题更少了。\n\n除此之外，macOS 应用的行为与 ZIP 存档非常相似，在 ZIP 存档中，每个应用都有自己的目录，其中携带了适当的库。因此，用户没有必要事先在他们的操作系统上安装 Qt 库，一切都开箱即用。\n\n至于套件选择，macOS 的 Qt 支持安卓、64 位、iOS 和 iOS 模拟器的套件:\n\n![](img/735061cd-1346-48d1-bc11-915a94b1f452.png)\n\n从 Qt 5.10 及更高版本开始，Qt 不再支持 32 位版本的 macOS。另外，Qt 不支持 PowerPC 上的 OS X；而且由于 Qt 内部使用 Cocoa，为 Carbon 构建也是不可能的，请注意这一点。\n\n在编译您的 macOS 应用之前，请先从应用商店安装 Xcode，然后再继续。Xcode 是一个面向 macOS 的集成开发环境，包含一套由苹果开发的软件开发工具，用于为 macOS 和 iOS 开发软件。一旦你安装了 Xcode，Qt Creator 就会检测到它的存在，并自动为你设置编译器设置，这很棒:\n\n![](img/5998a25c-8025-4cab-a703-0ebc982da29c.png)\n\n一旦您编译了项目，生成的可执行程序就是一个单一的应用包，可以很容易地分发给您的用户。因为所有的库文件都打包在应用包中，所以它应该在用户的电脑上开箱即用。\n\n为 Mac 设置应用图标是一项相当简单的任务。只需将以下代码行添加到您的项目(`.pro`)文件中，我们就可以开始了:\n\n```cpp\nICON = myapp.icns \n```\n\n请注意，图标格式是`.icns`，而不是我们通常用于 Windows 的`.ico`。\n\n# 部署到移动平台\n\n除了 Windows、Linux 和 macOS 等平台，移动平台确实同样重要。有许多开发人员希望将其应用部署到移动平台上。让我们看看是怎么做到的。我们将覆盖两大平台，它们是，iOS 和安卓。\n\n# ios\n\n在 iOS 上部署 Qt 应用真的很简单很容易。就像我们之前对 macOS 所做的一样，您需要首先在您的开发 PC 上安装 Xcode:\n\n![](img/3be1b23b-4409-4291-8b3a-50b98101ecde.png)\n\n然后，重启 Qt Creator。它现在应该检测到 Xcode 的存在，然后会自动为您设置编译器设置:\n\n![](img/3a8e54b9-b260-4f38-8b25-33541d29e9a0.png)\n\n之后，只要插上你的 iPhone，点击运行按钮！\n\n在 Qt 上构建 iOS 应用真的很容易。然而，分发它们不是。这是因为 iOS 是一个非常封闭的生态系统，就像一个有围墙的花园。您不仅需要在苹果注册为应用开发人员，还需要对您的 iOS 应用进行代码签名，然后才能将其分发给您的用户。如果你想为 iOS 构建应用，你就无法避免这些步骤。\n\n您可以通过访问以下链接了解更多信息:[https://developer.apple.com/app-store/submissions.](https://developer.apple.com/app-store/submissions)\n\n# 机器人\n\n尽管安卓是一个基于 Linux 的操作系统，但与你在电脑上运行的 Linux 平台相比，它是非常不同的。要在 Qt 上构建安卓应用，您必须首先将**安卓软件开发工具包**、**安卓 NDK** 和 **Apache ANT** 安装到您的开发电脑上，无论您运行的是 Windows、Linux 还是 macOS:\n\n![](img/c84e6e83-7ac5-46fc-b538-4f7013df7fa0.png)\n\n在 Qt 上构建安卓应用时，这三个包是必不可少的。一旦它们全部安装完毕，重启 Qt Creator，瞧，它现在应该已经检测到它们的存在，并且构建设置现在已经被自动设置:\n\n![](img/670eda7a-b1cb-49ef-80f4-e32d4bf20ced.png)\n\n最后，你可以用 Qt Creator 打开`AndroidManifect.xml`文件来配置你的安卓应用:\n\n![](img/d5039d76-b634-4e6b-9116-3b4ef38c82c0.png)\n\n您可以在这里设置所有内容，例如包名、版本代码、SDK 版本、应用图标、权限等等。\n\n与 iOS 相比，安卓是一个开放的系统，因此在将应用分发给用户之前，您不需要做任何事情。然而，如果你想在谷歌 Play 商店发布你的应用，你可以选择注册成为谷歌游戏开发者。\n\n# 摘要\n\n在这一章中，我们学习了如何为不同的平台编译和分发我们的 Qt 应用，例如 Windows、Linux、macOS、安卓和 iOS。在下一章中，我们将学习可以节省开发时间的不同调试方法。让我们来看看！*"
  },
  {
    "path": "docs/cpp-gui-prog-qt5/16.md",
    "content": "# 十六、测试和调试\n\n我们在阅读编程相关的教程或文章时，经常会看到 *debug* 这个词。但是你知道调试是什么意思吗？编程术语中的*错误*指的是计算机程序中的错误或缺陷，导致软件无法正常运行，这通常会导致不正确的输出甚至崩溃。\n\n在本章中，我们将涵盖以下主题，并学习如何调试我们的 Qt 项目:\n\n*   调试技术\n*   Qt 支持的调试器\n*   单元测试\n\n我们开始吧。\n\n# 调试技术\n\n技术问题在开发过程中一直存在。为了解决这些问题，我们需要找出所有这些问题，并在向用户发布我们的应用之前解决它们，以免影响公司/团队的声誉。用于查找技术问题的方法称为调试。在这一节中，我们将看看专业人员用来确保他们的程序可靠和高质量的常见调试技术。\n\n# 识别问题\n\n无论编程语言或平台如何，在调试程序时，最重要的是知道代码的哪一部分导致了问题。有几种方法可以识别有问题的代码:\n\n*   询问用户 bug 发生的时间点；例如，按下了哪个按钮，导致崩溃的步骤是什么，等等。\n*   注释掉部分代码，然后再次构建并运行程序，检查问题是否仍然存在。如果仍然如此，继续注释掉更多的代码，直到找到有问题的代码行。\n*   通过设置数据断点，使用内置调试器检查目标函数中的变量更改。您可以很容易地发现您的某个变量是否已更改为意外的值，或者某个对象指针是否已成为未定义的指针。\n*   确保安装程序中为用户提供的所有库的版本号与项目中使用的版本号相匹配。\n\n# 使用 QDebug 打印变量\n\n您也可以使用`QDebug`类将变量值打印到应用输出窗口。`QDebug`和标准库中的`std::cout`非常相似，但是使用`QDebug`的好处是因为它是 Qt 的一部分，所以它支持 Qt 类的开箱即用，并且它能够在不需要任何转换的情况下输出它的值。\n\n要启用`QDebug`，我们必须首先包含它的表头:\n\n```cpp\n#include <QDebug> \n```\n\n之后，我们可以调用`qDebug()`将变量打印到应用输出窗口:\n\n```cpp\nint amount = 100; \nqDebug() << \"You have obtained\" << amount << \"apples!\"; \n```\n\n结果将如下所示:\n\n![](img/085d30a2-eaa0-43d5-8887-4df07ebf0ed9.png)\n\n通过使用`QDebug`，我们将能够检查我们的功能是否正常运行。检查完问题后，您可以注释掉包含`qDebug()`的特定代码行。\n\n# 设置断点\n\n设置断点是调试程序的另一个好方法。当您在 Qt Creator 中右键单击脚本的行号时，您将获得一个包含三个选项的弹出菜单，如下图所示:\n\n![](img/6ee7547c-f999-4056-b55d-41a1ea786a99.png)\n\n第一个选项叫做在第行设置断点...，它允许您在脚本的特定行设置断点。创建断点后，行号旁边会出现一个红点图标:\n\n![](img/59956e75-08f7-4699-8bd7-2d45b6d49ee0.png)\n\n第二个选项叫做在第行设置消息跟踪点...，当程序到达该特定代码行时，它会打印一条消息。创建断点后，行号旁边会出现一个眼睛图标:\n\n![](img/488f3bb1-e9de-42ac-8600-e4b592256270.png)\n\n第三个选项是切换书签，它允许您为自己的参考设置书签。让我们创建一个名为`test()`的函数来尝试断点:\n\n```cpp\nvoid MainWindow::test() \n{ \n   int amount = 100; \n   amount -= 10; \n   qDebug() << \"You have obtained\" << amount << \"apples!\"; \n} \n```\n\n之后，我们在`MainWindow`构造函数中调用`test()`函数:\n\n```cpp\nMainWindow::MainWindow(QWidget *parent) : \n   QMainWindow(parent), \n   ui(new Ui::MainWindow) \n{ \n   ui->setupUi(this); \n   test(); \n} \n```\n\n然后，按下位于 Qt 创建器窗口左下方的开始调试按钮:\n\n![](img/dc999d58-3ca2-4a24-91b9-0474314d3908.png)\n\n您可能会收到如下错误消息:\n\n![](img/d8c0529c-a614-491e-ac8f-0089a0b6a7c5.png)\n\n在这种情况下，请确保项目工具包链接了调试器。如果这个错误仍然发生，请关闭您的 Qt 创建器，转到您的项目文件夹并删除`.pro.user`文件。之后，用 Qt Creator 打开你的项目。Qt Creator 将再次重新配置您的项目，现在调试模式应该可以工作了。\n\n让我们在代码中添加两个断点并运行它。一旦我们的程序启动，我们将看到一个黄色箭头出现在第一个红点的顶部:\n\n![](img/d5b0fc54-92ce-48ef-a6e6-7ce0af651b97.png)\n\n这意味着调试器已在第一个断点处停止。位于 Qt Creator 右侧的“局部变量和表达式”窗口现在将在此显示变量及其值和类型:\n\n![](img/401c5d9f-2626-4871-ac40-f7bba1272cab.png)\n\n在上图中，您可以看到该值仍然为 100，因为此时减操作尚未运行。我们需要做的下一件事是点击位于您的 Qt Creator 底部的堆栈窗口顶部的“单步执行”按钮:\n\n![](img/fb1565d7-beb1-4642-ae50-737e7795057c.png)\n\n之后，调试器将移动到下一个断点，在这里我们可以看到该值如预期的那样减少到 90:\n\n![](img/fca1e49a-7ed7-4b7b-b17e-56aed8a01921.png)\n\n您可以使用此方法轻松检查您的应用。要删除断点，您只需再次单击红点图标。\n\n请注意，您必须在调试模式下运行此程序。这是因为在调试模式下编译时，附加的调试符号将嵌入到您的应用或库中，使您的调试器能够访问二进制源代码中的信息，例如标识符、变量和例程的名称。这也是为什么如果在调试模式下编译，应用或库的文件大小会大得多的原因。\n\n# Qt 支持的调试器\n\nQt 支持不同类型的调试器。根据您为项目运行的平台和编译器，所使用的调试器也会有所不同。以下是 Qt 通常支持的调试器列表:\n\n*   **windows(minw):**gdb(GNU 调试器)\n*   **Windows(MSVC):**CDB(Windows 调试工具)\n*   **macOS** : LLDB (LLVM 调试器)，FSF·GDB(实验)\n*   **Linux** : GDB，LLDB(实验)\n*   **Unix** (FreeBSD、OpenBSD 等)。):GDB\n*   **Android** : GDB\n*   **iOS** : LLDB\n\n# 电脑调试\n\n有了 **GDB (GNU 调试器)，**如果你在 Windows 上使用 MinGW 编译器，就不需要任何手动设置，因为它通常伴随着你的 Qt 安装。如果您正在运行其他操作系统，如 Linux，您可能需要在将其与 Qt Creator 链接之前手动安装它。Qt Creator 检测到 GDB 的存在，并自动将其与您的项目链接。如果没有，您可以很容易地找到位于您的 Qt 目录中的 GDB 可执行文件，并自己链接它。\n\n**CDB(Windows 调试工具)**另一方面，需要在你的 Windows 机器上手动安装。请注意，Qt 不支持 Visual Studio 的内置调试器。因此，您需要通过在安装 windows SDK 时选择一个名为 Windows 调试工具的可选组件来单独安装 CDB 调试器。Qt Creator 通常也会识别 CDB 的存在，并将其放在调试器选项页面下的调试器列表中。您可以转到工具|选项|生成和运行|调试器来查找设置，如下图所示:\n\n![](img/9b474cf0-8099-4386-860c-4ef15f5e5e40.png)\n\n# 安卓设备调试\n\n安卓设备的调试比个人电脑稍微复杂一些。您必须安装安卓开发所需的所有软件包，如 JDK(版本 6 或更高版本)、安卓软件开发工具包和安卓 NDK。然后还需要 Windows 平台上的 Android Debug Bridge (ADB) 驱动才能启用 USB 调试，因为 Windows 上默认的 USB 驱动不允许调试。\n\n# macOS 和 iOS 的调试\n\n至于 macOS 和 iOS，使用的调试器是 **LLDB (LLVM Debugger)** ，默认自带 Xcode。Qt Creator 还会识别它的存在，并自动将其与您的项目链接。\n\n每个调试器都有一点不同，在 Qt Creator 上的行为可能会有所不同。如果您熟悉这些工具并知道自己在做什么，还可以在它们各自的 IDE (Visual Studio、XCode 等)上运行非 GDB 调试器。\n\n如果需要向项目中添加其他调试器，可以转到工具|选项|生成和运行|工具包，然后单击克隆复制现有工具包。然后，在调试器选项卡下，单击添加按钮添加新的调试器选项:\n\n![](img/471d3646-ff16-4764-a526-92ea5bf8f6b4.png)\n\n在“名称”字段中，键入调试器的描述性名称，以便您可以轻松记住它的用途。然后，在“路径”字段中指定调试器二进制文件的路径，以便 Qt Creator 知道在您启动调试过程时要运行哪个可执行文件。除此之外，Qt 创建者使用类型和版本字段来标识调试器版本的类型。此外，Qt Creator 还展示了将在 ABIs 领域的嵌入式设备上使用的 ABI 版本。\n\nTo learn more about the in-depth information on how to set up different debuggers in Qt, please visit the following link:\n[http://doc.qt.io/qtcreator/creator-debugger-engines.html.](http://doc.qt.io/qtcreator/creator-debugger-engines.html)\n\n# 单元测试\n\n单元测试是对应用中的单个模块、类或方法进行测试的自动化过程。单元测试在开发周期的早期发现问题。这既包括程序员实现中的错误，也包括单元规范中的缺陷或缺失部分。\n\n# Qt 中的单元测试\n\nQt 自带内置的单元测试模块，我们可以通过在我们的项目文件(`.pro`)中添加`testlib`关键字来使用:\n\n```cpp\nQT += core gui testlib \n```\n\n之后，在我们的源代码中添加以下标题:\n\n```cpp\n#include <QtTest/QtTest> \n```\n\n然后，我们可以开始测试我们的代码。我们必须将我们的测试函数声明为私有槽。除此之外，类还必须从`QOBject`类继承。例如，我创建了两个名为`testString()`和`testGui()`的文本函数，如下所示:\n\n```cpp\nprivate slots: \n   void testString(); \n   void testGui(); \n```\n\n函数定义如下所示:\n\n```cpp\nvoid MainWindow::testString() \n{ \n   QString text = \"Testing\"; \n   QVERIFY(text.toUpper() == \"TESTING\"); \n} \n\nvoid MainWindow::testGui() \n{ \n   QTest::keyClicks(ui->lineEdit, \"testing gui\"); \n   QCOMPARE(ui->lineEdit->text(), QString(\"testing gui\")); \n} \n```\n\n我们使用了`QTest`类提供的一些宏，如`QVERIFY`、`QCOMPARE`等，来评估作为参数传递的表达式。如果表达式的计算结果为`true`，测试函数将继续执行。否则，描述失败的消息被附加到测试日志中，测试函数停止执行。\n\n我们在应用中也使用了`QTest::keyClicks()`来模拟鼠标点击。在前面的示例中，我们模拟了在主窗口小部件上单击线编辑小部件。然后，我们向行编辑输入一行文本，并使用`QCOMPARE`宏测试文本是否已经正确插入行编辑小部件。如果发生任何错误，Qt 将在应用输出窗口中向我们显示问题。\n\n然后，注释掉我们的`main()`函数，改用`QTEST_MAIN()`函数开始测试我们的`MainWindow`类:\n\n```cpp\n/*int main(int argc, char *argv[]) \n{ \n   QApplication a(argc, argv); \n   MainWindow w; \n   w.show(); \n\n   return a.exec(); \n}*/ \nQTEST_MAIN(MainWindow) \n```\n\n如果我们现在构建并运行我们的项目，我们应该会得到如下类似的结果:\n\n```cpp\n********* Start testing of MainWindow ********* \nConfig: Using QtTest library 5.9.1, Qt 5.9.1 (i386-little_endian-ilp32 shared (dynamic) debug build; by GCC 5.3.0) \nPASS   : MainWindow::initTestCase() \nPASS   : MainWindow::_q_showIfNotHidden() \nPASS   : MainWindow::testString() \nPASS   : MainWindow::testGui() \nPASS   : MainWindow::cleanupTestCase() \nTotals: 5 passed, 0 failed, 0 skipped, 0 blacklisted, 880ms \n********* Finished testing of MainWindow ********* \n```\n\n还有很多宏可以用来测试应用。\n\nFor more information, please visit the following link:\n[http://doc.qt.io/qt-5/qtest.html#macros](http://doc.qt.io/qt-5/qtest.html#macros)\n\n# 摘要\n\n在本章中，我们学习了如何通过使用多种调试技术来识别我们的 Qt 项目中的技术问题。除此之外，我们还了解了 Qt 在不同操作系统上支持的不同调试器。最后，我们还学习了如何通过单元测试来自动化一些调试步骤。\n\n就这样！我们已经到了这本书的结尾。希望这本书对学习如何使用 Qt 从头开始构建自己的应用很有用。你可以在 GitHub 上寻找所有的源代码。祝你一切顺利！"
  },
  {
    "path": "docs/cpp-gui-prog-qt5/README.md",
    "content": "# C++ Qt5 GUI 编程\n\n> 原书：[C++ GUI Programming with QT5](https://libgen.rs/book/index.php?md5=63069FF6B9B588D5C75E8D5B8DBFB5ED)\n> \n> 协议：[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)\n> \n> 阶段：机翻（1）\n> \n> 自豪地采用[谷歌翻译](https://translate.google.cn/)\n> \n> 让开源界提前感受入关。\n\n* [在线阅读](https://ccpp.apachecn.org)\n* [在线阅读（Gitee）](https://apachecn.gitee.io/apachecn-c-cpp-zh/)\n* [ApacheCN 学习资源](http://docs.apachecn.org/)\n\n## 贡献指南\n\n本项目需要校对，欢迎大家提交 Pull Request。\n\n> 请您勇敢地去翻译和改进翻译。虽然我们追求卓越，但我们并不要求您做到十全十美，因此请不要担心因为翻译上犯错——在大部分情况下，我们的服务器已经记录所有的翻译，因此您不必担心会因为您的失误遭到无法挽回的破坏。（改编自维基百科）\n\n## 联系方式\n\n### 负责人\n\n* [飞龙](https://github.com/wizardforcel): 562826179\n\n### 其他\n\n*   在我们的 [apachecn/apachecn-c-cpp-zh](https://github.com/apachecn/apachecn-c-cpp-zh) github 上提 issue.\n*   发邮件到 Email: `apachecn@163.com`.\n*   在我们的 [组织学习交流群](http://www.apachecn.org/organization/348.html) 中联系群主/管理员即可.\n\n## 赞助我们\n\n![](http://data.apachecn.org/img/about/donate.jpg)\n"
  },
  {
    "path": "docs/cpp-gui-prog-qt5/SUMMARY.md",
    "content": "+   [C++ Qt5 GUI 编程](README.md)\n+   [零、前言](00.md)\n+   [一、Qt 简介](01.md)\n+   [二、Qt 小部件和样式表](02.md)\n+   [三、数据库连接](03.md)\n+   [四、图表](04.md)\n+   [五、项目视图和对话框](05.md)\n+   [六、整合网络内容](06.md)\n+   [七、地图查看器](07.md)\n+   [八、图形视图](08.md)\n+   [九、照相机模块](09.md)\n+   [十、即时消息](10.md)\n+   [十一、实现图形编辑器](11.md)\n+   [十二、云存储](12.md)\n+   [十三、多媒体查看器](13.md)\n+   [十四、Qt Quick 和 QML](14.md)\n+   [十五、跨平台开发](15.md)\n+   [十六、测试和调试](16.md)\n"
  },
  {
    "path": "docs/cpp-hiperf/00.md",
    "content": "# 零、前言\n\n今天的 C++ 为程序员提供了编写富于表现力和健壮的代码的能力，同时仍然可以针对几乎任何硬件平台，同时满足性能关键的要求。这使得 C++ 成为一种独特的语言。在过去的几年里，C++ 已经变成了一种现代语言，使用起来更有趣，并且有更好的默认值。\n\n这本书旨在为您编写高效的应用打下坚实的基础，并深入了解在现代 C++ 中实现库的策略。我试图用一种实用的方法来解释今天 C++ 是如何工作的，其中来自 C++ 17 和 C++ 20 的特性是语言的自然组成部分，而不是从历史上来看 C++。\n\n第二版是为了涵盖 C++ 20 中增加的新特性而编写的。我已经包含了我认为与本书其余内容和重点非常契合的功能。自然，讨论新特性的章节更多的是作为介绍，包含较少的最佳实践和经验证的解决方案。\n\n在出版这本书的时候，编译器对一些 C++ 20 特性的支持仍然是实验性的。如果你在接近出版日期的时候阅读这本书，你可能需要等待一些特性被你的编译器完全支持。\n\n许多章节跨越了广泛的困难。它们以绝对的基础知识开始，以高级主题(如自定义内存分配器)结束。如果某个部分与您无关，请随意跳过或稍后再来。除了前三章，大部分章节都可以独立阅读。\n\n我们的主要技术评论家，帖木儿·杜姆勒，对这个新版本产生了很大的影响。他的热情和出色的反馈导致了第一版的一些章节被重写，以更彻底和更深入地解释主题。帖木儿在将新的 C++ 20 特性融入到它们自然适合的章节中时，也是一个重要的贡献者。阿瑟·奥德怀尔、马里乌斯·班希拉和刘易斯·贝克也对这本书的某些部分进行了评论。在这个项目上有如此优秀的评审是一种真正的快乐。我希望你和我一样喜欢阅读这个新版本。\n\n# 这本书是给谁的\n\n这本书期望你对 C++ 和计算机体系结构有基本的了解，并对发展你的技能有真正的兴趣。希望当你读完这本书的时候，你已经获得了一些关于如何改进 C++ 应用的见解，包括性能和语法。除此之外，我也希望你能有几个“啊哈”的时刻。\n\n# 这本书涵盖了什么\n\n*第一章*、*C++ 简介*介绍了 c++ 的一些重要性质，如零成本抽象、值语义、常量正确性、显式所有权和错误处理。它还讨论了 C++ 的缺点。\n\n*第 2 章*、*基本 C++ 技术*，概述了使用 auto、lambda 函数、移动语义和错误处理的自动类型推断。\n\n*第三章*、*分析和测量性能*，将教你如何使用大 O 符号分析算法复杂度。本章还讨论了如何分析代码以发现热点，以及如何使用谷歌基准测试设置性能测试。\n\n*第 4 章*、*数据结构*，带您了解结构化数据的重要性，以便快速访问。引入了标准库中的容器，如`std::vector`、`std::list`、`std::unordered_map`和`std::priority_queue`。最后，本章演示如何使用并行数组。\n\n*第五章*、*算法*，介绍了标准库中最重要的算法。您还将学习如何使用迭代器和范围，以及如何实现自己的通用算法。\n\n*第 6 章*、*范围和视图*，将教你如何使用 C++ 20 中引入的范围库编写算法。您将了解为什么范围库中的视图是有用的，以及延迟求值的一些好处。\n\n*第 7 章**内存管理*，重点介绍安全高效的内存管理。这包括内存所有权、RAII、智能指针、堆栈内存、动态内存和自定义内存分配器。\n\n*第 8 章*、*编译时编程*，解释了使用`constexpr`、`consteval`和类型特征的元编程技术。您还将学习如何使用 C++ 20 概念和新概念库。最后，它提供了元编程用例的实际例子，例如反射。\n\n*第 9 章*、*基本实用程序*将指导您浏览实用程序库，以及如何使用编译时编程技术从诸如`std::optional`、`std::any`和`std::variant`等类型中受益。\n\n*第 10 章*、*代理对象和延迟求值*探讨了如何使用代理对象来执行幕后优化，同时保持干净的语法。此外，还演示了运算符重载的一些创造性用法。\n\n*第 11 章*、*并发*，涵盖了并发编程的基础，包括并行执行、共享内存、数据竞争和死锁。它还包括对 C++ 线程支持库、原子库和 C++ 内存模型的介绍。\n\n*第 12 章*、*协程和延迟生成器*，包含协程抽象的一般介绍。您将了解如何使用堆栈和堆在中央处理器上执行普通函数和协程。介绍了 C++ 20 无堆栈协程，您将发现如何使用生成器解决问题。\n\n*第 13 章*、*用 coroutines 进行异步编程*，介绍了 C++ 20 中使用无堆栈 Coroutines 进行并发编程，并涉及到使用 Boost.Asio 进行异步网络编程的主题\n\n*第 14 章*、*并行算法*首先展示了编写并行算法的复杂性以及如何衡量它们的性能。然后演示如何使用执行策略在并行环境中利用标准库算法。\n\n# 充分利用这本书\n\n要充分利用这本书，你需要有 C++ 的基础知识。您最好已经面临过与性能相关的问题，并且正在寻找新的工具和实践，以便为下一次使用性能和 C++ 做好准备。\n\n这本书里有很多代码示例。有些来自现实世界，但大多数都是人为的或大大简化的例子来证明一个概念，而不是为您提供生产就绪的代码。\n\n我已经把所有的代码示例放在按章节划分的源文件中，这样就很容易找到您想要尝试的示例。如果你打开源代码文件，你会注意到我已经用谷歌测试框架编写的测试用例替换了例子中的大部分`main()`函数。我希望这能帮助你，而不是迷惑你。它允许我为每个示例编写有用的描述，并且它还使得一次运行一章中的所有示例变得更加容易。\n\n为了编译和运行示例，您需要以下内容:\n\n*   电脑\n*   操作系统(示例已在 Windows、Linux 和 macOS 上得到验证)\n*   一个编译器(我用了 Clang、GCC 和微软 Visual C++)\n*   CMake\n\n示例代码提供的 CMake 脚本将下载并安装更多的依赖项，如 Boost、Google Benchmark 和 Google Test。\n\n在写这本书的过程中，我发现使用**编译器探索者**很有帮助，它可以在[https://godbolt.org/](https://godbolt.org/)找到。编译器资源管理器是一个在线编译器服务，允许您尝试各种编译器和版本。如果你还没试过，那就试试吧！\n\n## 下载示例代码文件\n\n本书的代码包托管在 GitHub[https://GitHub . com/PacktPublishing/Cpp-高性能-第二版](https://github.com/PacktPublishing/Cpp-High-Performance-Second-Edition)。如果代码有更新，它将在现有的 GitHub 存储库中更新。\n\n在[https://github.com/PacktPublishing/](https://github.com/PacktPublishing/)上，还有来自 Packt 丰富的图书和视频目录的其他代码包。看看他们！\n\n## 下载彩色图像\n\nPackt 还提供了一个 PDF 文件，其中包含了本书中使用的截图/图表的彩色图像。可以在这里下载:[https://static . packt-cdn . com/downloads/9781839216541 _ color images . pdf](https://static.packt-cdn.com/downloads/9781839216541_ColorImages.pdf)。\n\n## 使用的约定\n\n本书通篇使用了许多文本约定。\n\n`CodeInText`:表示文本中的码字、文件夹名称、文件名、文件扩展名、虚拟网址和用户输入。下面举个例子:“关键字`constexpr`是在 C++ 11 中引入的。”\n\n代码块设置如下:\n\n```cpp\n#include <iostream>\nint main() {\n  std::cout << \"High Performance C++ \\n\"; \n} \n```\n\n当我希望将您的注意力吸引到代码块的特定部分时，相关的行或项目以粗体显示:\n\n```cpp\n#include <iostream>\nint main() {\nstd`::`cout `<<` \"High Performance C++ \\n\"`;`\n} \n```\n\n任何命令行输入或输出都编写如下:\n\n```cpp\n$ clang++ -std=c++ 20 high_performance.cpp\n$ ./a.out\n$ High Performance C++ \n```\n\n**粗体**:表示一个新的术语，一个重要的单词，或者你在屏幕上看到的单词。例如:“填写表格，点击**保存**按钮。”\n\n警告或重要注意事项是这样出现的。\n\n# 取得联系\n\n欢迎读者反馈。\n\n**一般反馈**:如果你对这本书的任何方面有疑问，请在你的信息主题中提到书名，并在`customercare@packtpub.com`发送电子邮件至 Packt。\n\n**勘误表**:虽然我们已经尽了最大的努力来保证内容的准确性，但是错误还是会发生。如果你在这本书里发现了错误，如果你能向我们报告，我们将不胜感激。请访问[www.packtpub.com/support/errata](http://www.packtpub.com/support/errata)，选择您的图书，点击**勘误表**链接，输入详细信息。\n\n**盗版**:如果您在互联网上遇到任何形式的我们作品的非法拷贝，如果您能提供我们的位置地址或网站名称，我们将不胜感激。请通过`copyright@packt.com`联系我们，并提供材料链接。\n\n**如果你有兴趣成为一名作者**:如果有一个你有专长的话题，你有兴趣写或者投稿一本书，请访问[authors.packtpub.com](http://authors.packtpub.com)。\n\n## 复习\n\n请留下评论。一旦你阅读并使用了这本书，为什么不在你购买它的网站上留下评论呢？潜在的读者可以看到并使用你的不带偏见的意见来做出购买决定，我们在 Packt 可以了解你对我们产品的看法，我们的作者可以看到你对他们的书的反馈。谢谢大家！\n\n更多关于 Packt 的信息，请访问[packt.com](http://packt.com)。"
  },
  {
    "path": "docs/cpp-hiperf/01.md",
    "content": "# 一、C++ 简介\n\n这本书旨在为您提供编写高效应用的坚实基础，以及在现代 C++ 中实现库的策略。我试图用一种实用的方法来解释今天 C++ 是如何工作的，从 C++ 11 到 C++ 20 的现代特性是语言的自然组成部分，而不是从历史上来看 C++。\n\n在本章中，我们将:\n\n*   介绍 C++ 的一些特性，这些特性对于编写健壮、高性能的应用非常重要\n*   讨论 C++ 相对于竞争语言的优缺点\n*   浏览本书中使用的库和编译器\n\n# 为什么是 C++？\n\n让我们从探索今天使用 C++ 的一些原因开始。简而言之，C++ 是一种高度可移植的语言，它提供零成本的抽象。此外，C++ 为程序员提供了编写和管理大型、富于表现力和健壮的代码库的能力。在这一节中，我们将看看我们所说的*零成本抽象*是什么意思，将 C++ 抽象与其他语言中的抽象进行比较，并讨论可移植性和健壮性，以及为什么这些特性很重要。\n\n让我们从零成本抽象开始。\n\n## 零成本抽象\n\n活跃的代码库在增长。开发人员在代码库上工作的越多，代码库就越大。为了管理越来越复杂的代码库，我们需要诸如变量、函数和类这样的语言特性，以便能够用自定义名称和接口创建我们自己的抽象，从而抑制实现的细节。\n\nC++ 允许我们定义自己的抽象，但它也带有内置的抽象。例如，C++ 函数的概念本身就是控制程序流的抽象。基于范围的`for`循环是内置抽象的另一个例子，它可以更直接地迭代一系列值。作为程序员，我们在开发程序的同时不断增加新的抽象。同样，C++ 的新版本为语言和标准库引入了新的抽象。但是不断增加抽象和新的间接层次是有代价的——效率。这就是零成本抽象发挥作用的地方。C++ 提供的许多抽象在空间和时间方面的运行时成本非常低。\n\n有了 C++，当需要的时候，你可以自由谈论内存地址和其他与计算机相关的低级术语。然而，在大型软件项目中，最好用处理应用正在做的任何事情的术语来表达代码，并让库处理与计算机相关的术语。图形应用的源代码可能涉及铅笔、颜色和滤镜，而游戏可能涉及吉祥物、城堡和蘑菇。与计算机相关的低级术语，如内存地址，可能会隐藏在性能至关重要的 C++ 库代码中。\n\n### 编程语言和机器代码抽象\n\n为了让程序员不再需要处理与计算机相关的术语，现代编程语言使用抽象，例如，字符串列表可以被处理并被认为是字符串列表，而不是地址列表，如果我们犯了最轻微的错别字，我们可能会很容易忘记。抽象不仅能让程序员摆脱 bug，还能通过使用应用领域的概念让代码更具表现力。换句话说，代码用更接近口语的术语来表达，而不是用抽象的编程关键字来表达。\n\nC++ 和 C 现在是两种完全不同的语言。尽管如此，C++ 与 C 高度兼容，并且从 C 继承了它的许多语法和习惯用法。为了给你一些 C++ 抽象的例子，我将展示如何在 C 和 C++ 中解决问题。\n\n看看下面的 C/C++ 代码片段，对应的问题是:“这份书单里有多少本《哈姆雷特》？”\n\n我们将从 C 版开始:\n\n```cpp\n// C version\nstruct string_elem_t { const char* str_; string_elem_t* next_; };\nint num_hamlet(string_elem_t* books) {\n  const char* hamlet = \"Hamlet\";\n  int n = 0;\n  string_elem_t* b; \n  for (b = books; b != 0; b = b->next_)\n    if (strcmp(b->str_, hamlet) == 0)\n      ++ n;\n  return n;\n} \n```\n\n使用 C++ 的等效版本如下所示:\n\n```cpp\n// C++ version\nint num_hamlet(const std::forward_list<std::string>& books) {\n  return std::count(books.begin(), books.end(), \"Hamlet\");\n} \n```\n\n尽管 C++ 版本更像是一种机器人语言，而不是人类语言，但由于更高层次的抽象，许多编程术语已经不复存在。以下是前两个代码片段之间的一些显著差异:\n\n*   指向原始内存地址的指针根本不可见\n*   `std::forward_list<std::string>`容器使用`string_elem_t`代替手工制作的链表\n*   `std::count()`功能取代了`for`循环和`if`语句\n*   `std::string`类提供了比`char*`和`strcmp()`更高级别的抽象\n\n基本上，`num_hamlet()`的两个版本翻译成大致相同的机器码，但是 C++ 的语言特性使得库隐藏指针等计算机相关术语成为可能。许多现代 C++ 语言特性可以看作是基本 C 功能之上的抽象。\n\n### 其他语言的抽象\n\n大多数编程语言都是基于抽象的，抽象被转换成机器代码，由中央处理器执行。C++ 已经发展成为一种高度表达的语言，就像今天许多其他流行的编程语言一样。C++ 与大多数其他语言的区别在于，虽然其他语言已经以运行时性能为代价实现了这些抽象，但 C++ 一直努力在运行时以零成本实现其抽象。这并不意味着用 C++ 编写的应用在默认情况下比用 C#编写的应用更快。相反，这意味着通过使用 C++，如果需要，您可以对发出的机器代码指令和内存占用进行细粒度控制。\n\n平心而论，现在很少需要最佳性能，在许多情况下，像其他语言一样，为了更低的编译时间、垃圾收集或安全性而牺牲性能更合理。\n\n### 零开销原则\n\n“零成本抽象”是一个常用的术语，但它有一个问题——大多数抽象通常都有成本。如果在运行程序时没有，它几乎总是会花费一些成本，比如很长的编译时间，难以解释的编译错误消息等等。通常更有趣的是零开销原则。C++ 的发明者比雅尼·斯特劳斯特鲁普这样定义零开销原则:\n\n*   你不用的东西，你不用付钱\n*   你使用的东西，你不能更好地手工编码\n\n这是 C++ 的一个核心原则，也是语言进化的一个非常重要的方面。为什么，你可能会问？建立在这个原则上的抽象将被性能敏感的程序员广泛接受和使用并且在性能非常关键的环境中。找到许多人都同意并广泛使用的抽象，使我们的代码库更容易阅读和维护。\n\n相反，C++ 语言中不完全遵循零开销原则的特性往往会被程序员、项目和公司抛弃。这一类别中最值得注意的两个特征是**异常**(不幸的是)和**运行时类型信息** ( **RTTI** )。这两项功能即使在不使用时也会对性能产生影响。我强烈建议使用异常，除非你有很好的理由不这样做。与使用其他机制处理错误相比，性能开销在大多数情况下可以忽略不计。\n\n## 轻便\n\n长期以来，C++ 一直是一种流行而全面的语言。它与 C 高度兼容，很少在语言中被弃用，无论好坏。C++ 的历史和设计使它成为一种高度可移植的语言，现代 C++ 的进化确保了它将在未来很长一段时间内保持这种状态。C++ 是一种活的语言，编译器供应商目前在快速实现新的语言特性方面做得非常出色。\n\n## 稳健性\n\n除了性能、表现力和可移植性之外，C++ 还提供了一组语言特性，使程序员能够编写健壮的代码。\n\n根据作者的经验，健壮性不是指编程语言本身的强度——用任何语言编写健壮的代码都是可能的。相反，严格的资源所有权、`const`正确性、值语义、类型安全性和对象的确定性销毁是 C++ 提供的一些特性，这些特性使得编写健壮的代码变得更加容易。也就是说，编写易于使用且不易误用的函数、类和库的能力。\n\n## 今天的 C++\n\n总而言之，今天的 C++ 为程序员提供了编写富于表现力和健壮性的代码库的能力，同时仍然可以选择针对几乎任何硬件平台或实时需求。在当今最常用的语言中，只有 C++ 拥有所有这些属性。\n\n我现在已经提供了一个简短的纲要，说明为什么 C++ 仍然是一种相关的和广泛使用的编程语言。在下一节中，我们将看看 C++ 与其他现代编程语言相比如何。\n\n# C++ 与其他语言相比\n\n自从 C++ 首次发布以来，已经出现了大量的应用类型、平台和编程语言。尽管如此，C++ 仍然是一种广泛使用的语言，其编译器可用于大多数平台。迄今为止，最大的例外是网络平台，JavaScript 及其相关技术是其基础。然而，web 平台正在发展成为能够执行以前只有在桌面应用中才能执行的功能，在这种情况下，C++ 已经找到了使用 Emscripten、asm.js 和 WebAssembly 等技术进入 web 应用的方法。\n\n在本节中，我们将从性能的角度来看竞争语言。接下来，我们将看看与其他语言相比，C++ 如何处理对象所有权和垃圾收集，以及我们如何在 C++ 中避免空对象。最后，我们将介绍 C++ 的一些缺点，用户在考虑该语言是否适合他们的需求时应该记住这些缺点。\n\n## 竞争语言和性能\n\n为了理解与其他编程语言相比，C++ 是如何实现其性能的让我们讨论一下 C++ 与大多数其他现代编程语言之间的一些基本差异。\n\n为简单起见，本节将重点比较 C++ 和 Java，尽管大多数部分的比较也适用于基于垃圾收集器的其他编程语言，如 C#和 JavaScript。\n\n首先，Java 编译成字节码，然后在应用执行时编译成机器码，而大多数 C++ 实现直接将源代码编译成机器码。虽然字节码和即时编译器理论上可能能够获得与预编译机器代码相同(或者理论上甚至更好)的性能，但截至目前，它们通常不能。不过，公平地说，他们在大多数情况下表现得足够好。\n\n其次，Java 处理动态内存的方式与 C++ 完全不同。在 Java 中，内存由垃圾收集器自动释放，而 C++ 程序手动或通过引用计数机制处理内存释放。垃圾收集器确实可以防止内存泄漏，但代价是性能和可预测性。\n\n第三，Java 将其所有对象放在单独的堆分配中，而 C++ 允许程序员将对象放在堆栈和堆上。在 C++ 中，也可以在一次堆分配中创建多个对象。这可能是一个巨大的性能提升，原因有二:可以在不总是分配动态内存的情况下创建对象，并且多个相关对象可以在内存中彼此相邻放置。\n\n在下面的例子中，看看内存是如何分配的。C++ 函数对对象和整数都使用堆栈；Java 将对象放在堆上:\n\n<colgroup><col> <col></colgroup> \n| C++ | Java 语言(一种计算机语言，尤用于创建网站) |\n| \n\n```cpp\nclass Car {\npublic:\n  Car(int doors)\n      : doors_(doors) {}\nprivate:\n  int doors_{}; \n};\nauto some_func() {\n  auto num_doors = 2;\n  auto car1 = Car{num_doors};\n  auto car2 = Car{num_doors};\n  // ...\n} \n```\n\n | \n\n```cpp\nclass Car {\n  public Car(int doors) { \n    doors_ = doors;\n  }\n  private int doors_;\n  static void some_func() {\n    int numDoors = 2;\n    Car car1 = new Car(numDoors);\n    Car car2 = new Car(numDoors);\n    // ...\n  }\n} \n```\n\n |\n| C++ 将所有东西都放在堆栈上:\n\n<figure class=\"mediaobject\">![](img/B15619_01_01.png)</figure>\n\n | Java 将`Car`对象放在堆上:\n\n<figure class=\"mediaobject\">![](img/B15619_01_02.png)</figure>\n\n |\n\n现在看下一个例子，看看当分别使用 C++ 和 Java 时，`Car`对象的数组是如何放置在内存中的:\n\n<colgroup><col> <col></colgroup> \n| C++ | Java 语言(一种计算机语言，尤用于创建网站) |\n| \n\n```cpp\nauto n = 4;\nauto cars = std::vector<Car>{};\ncars.reserve(n);\nfor (auto i=0; i<n;++ i) {\n   cars.push_back(Car{2});\n} \n```\n\n | \n\n```cpp\nint n = 4;\nArrayList<Car> cars = \n  new ArrayList<Car>();\nfor (int i=0; i<n; i++) {\n  cars.addElement(new Car(2));\n} \n```\n\n |\n| 下图显示了 C++ 中`Car`对象在内存中的布局方式:\n\n<figure class=\"mediaobject\">![](img/B15619_01_03.png)</figure>\n\n | 下图显示了 Java 中`Car`对象在内存中的布局方式:\n\n<figure class=\"mediaobject\">![](img/B15619_01_04.png)</figure>\n\n |\n\nC++ 向量包含放置在一个连续内存块中的实际`Car`对象，而 Java 中的等价对象是引用到`Car`对象的*的连续内存块。在 Java 中，对象是单独分配的，这意味着它们可以位于堆的任何地方。*\n\n这影响了的性能，因为在这个例子中，Java 实际上必须在 Java 堆空间中执行五次分配。它也意味着每当应用迭代列表时，C++ 都有性能胜利，因为访问附近的内存位置比访问内存中的几个随机点更快。\n\n## 与性能无关的 C++ 语言特性\n\n人们很容易相信，只有在性能是主要考虑因素的情况下，才应该使用 C++ 语言。不就是 C++ 只是由于手工内存处理增加了代码库的复杂度，可能导致内存泄漏和难以追踪的 bug 吗？\n\n这在几个 C++ 版本之前可能是正确的，但是现代 C++ 程序员依赖于提供的容器和智能指针类型，它们是标准库的一部分。过去 10 年中增加的大量 C++ 特性使得这种语言更加强大，使用起来也更加简单。\n\n我想在这里强调 C++ 的一些古老但强大的特性，这些特性与健壮性而不是性能有关，这些特性很容易被忽略:值语义、`const`正确性、所有权、确定性破坏和引用。\n\n### 价值语义学\n\nC++ 支持值语义和引用语义。值语义允许我们通过值传递对象，而不仅仅是传递对对象的引用。在 C++ 中，值语义是默认的，这意味着当传递类或结构的实例时，它的行为与传递`int`、`float`或任何其他基本类型相同。为了使用引用语义，我们需要显式地使用引用或指针。\n\nC++ 类型的系统给了我们显式声明对象所有权的能力。比较 C++ 和 Java 中简单类的以下实现。我们将从 C++ 版本开始:\n\n```cpp\n// C++\nclass Bagel {\npublic:\n  Bagel(std::set<std::string> ts) : toppings_(std::move(ts)) {}\nprivate:\n  std::set<std::string> toppings_;\n}; \n```\n\nJava 中相应的实现可能如下所示:\n\n```cpp\n// Java\nclass Bagel {\n  public Bagel(ArrayList<String> ts) { toppings_ = ts; }\n  private ArrayList<String> toppings_;\n} \n```\n\n在 C++ 版本中，程序员声明`toppings`被`Bagel`类完全封装。如果程序员打算在几个百吉饼之间共享头名列表，它将被声明为某种指针:`std::shared_ptr`如果所有权在几个百吉饼之间共享，或者`std::weak_ptr`如果其他人拥有头名列表并且应该在程序执行时修改它。\n\n在 Java 中，对象以共享的所有权相互引用。因此，无法区分头名列表是否打算在几个百吉饼之间共享，或者是否在其他地方处理，或者是否像大多数情况下一样完全归`Bagel`类所有。\n\n比较以下功能；由于在 Java(和大多数其他语言)中，默认情况下每个对象都是共享的，因此程序员必须对细微的错误采取预防措施，例如:\n\n<colgroup><col> <col></colgroup> \n| C++ | Java 语言(一种计算机语言，尤用于创建网站) |\n| \n\n```cpp\n// Note how the bagels do\n// not share toppings:\nauto t = std::set<std::string>{};\nt.insert(\"salt\");\nauto a = Bagel{t};\n// 'a' is not affected\n// when adding pepper\nt.insert(\"pepper\");\n// 'a' will have salt\n// 'b' will have salt & pepper \nauto b = Bagel{t};\n// No bagel is affected\nt.insert(\"oregano\"); \n```\n\n | \n\n```cpp\n// Note how both the bagels\n// share toppings:\nTreeSet<String> t = \n  new TreeSet<String>();\nt.add(\"salt\");\nBagel a = new Bagel(t);\n// Now 'a' will subtly \n// also have pepper\nt.add(\"pepper\");\n// 'a' and 'b' share the\n// toppings in 't'\nBagel b = new Bagel(t);\n// Both bagels are affected\ntoppings.add(\"oregano\"); \n```\n\n |\n\n### 常量正确性\n\nC++ 的另一个强大的特性是能够编写完全`const`正确的代码，这是 Java 和许多其他语言所缺乏的。Const 正确性是指一个类的每个成员函数签名明确告诉调用者对象是否会被修改；如果调用者试图修改声明为`const`的对象，它将不会编译。在 Java 中，可以使用`final`关键字声明常量，但是这缺乏将成员函数声明为`const`的能力。\n\n下面是一个我们如何使用`const`成员函数来防止无意中修改对象的例子。在下面的`Person`类中，成员函数`age()`被声明为`const`，因此不允许变异`Person`对象，而`set_age()`变异该对象，因此*不能被声明为*:\n\n```cpp\nclass Person {\npublic:\n  auto age() const { return age_; }\n  auto set_age(int age) { age_ = age; }\nprivate:\n  int age_{};\n}; \n```\n\n还可以区分返回成员的可变引用和不可变引用。在下面的`Team`类中，成员函数`leader()` `const`返回一个不可变的`Person`，而`leader()`返回一个可能变异的`Person`对象:\n\n```cpp\nclass Team {\npublic:\n  auto& leader() const { return leader_; }\n  auto& leader() { return leader_; }\nprivate:\n  Person leader_{};\n}; \n```\n\n现在让我们看看当我们试图变异不可变对象时，编译器如何帮助我们发现错误。在下面的例子中，函数参数`teams`被声明为`const`，明确表示该函数不允许修改它们:\n\n```cpp\nvoid nonmutating_func(const std::vector<Team>& teams) {\n  auto tot_age = 0;\n\n  // Compiles, both leader() and age() are declared const\n  for (const auto& team : teams) \n    tot_age += team.leader().age();\n  // Will not compile, set_age() requires a mutable object\n  for (auto& team : teams) \n    team.leader().set_age(20);\n} \n```\n\n如果我们想写一个*可以*变异`teams`对象的函数，我们只需去掉`const`。这向调用者发出信号，这个函数可能会改变`teams`:\n\n```cpp\nvoid mutating_func(std::vector<Team>& teams) {\n  auto tot_age = 0;\n\n  // Compiles, const functions can be called on mutable objects\n  for (const auto& team : teams) \n    tot_age += team.leader().age();\n  // Compiles, teams is a mutable variable\n  for (auto& team : teams) \n    team.leader().set_age(20);\n} \n```\n\n### 对象所有权\n\n除了在非常罕见的情况下，C++ 程序员应该将内存处理留给容器和智能指针，永远不要依赖手动内存处理。\n\n说得明白一点，Java 中的垃圾收集模型几乎可以在 C++ 中通过对每个对象使用`std::shared_ptr`来模拟。请注意，垃圾收集语言不使用与`std::shared_ptr`相同的分配跟踪算法。`std::shared_ptr`是一个基于引用计数算法的智能指针，如果对象有循环依赖，它会泄漏内存。垃圾收集语言有更复杂的方法来处理和释放循环依赖对象。\n\n然而，强制严格的所有权并不依赖于垃圾收集器，而是微妙地避免了默认情况下共享对象可能导致的细微错误，就像 Java 的情况一样。\n\n如果程序员最小化 C++ 中的共享所有权，那么生成的代码就更容易使用，也更难滥用，因为它可以迫使类的用户按照预期的方式使用它。\n\n### C++ 中的确定性破坏\n\n在 C++ 中，对象的销毁是确定性的。这意味着我们(能够)确切地知道一个物体何时被摧毁。对于像 Java 这样的垃圾收集语言来说，情况并非如此，在 Java 中，垃圾收集器决定未被引用的对象何时被最终确定。\n\n在 C++ 中，我们可以可靠地反转对象生命周期中所做的事情。起初，这似乎是一件小事。但事实证明，它对我们如何在 C++ 中提供异常安全保证和处理资源(如内存、文件句柄、互斥锁等)有很大影响。\n\n确定性破坏也是使 C++ 可预测的特性之一。程序员非常重视的东西，也是对性能至关重要的应用的要求。\n\n我们将在本书后面花更多的时间讨论对象所有权、生存期和资源管理。所以如果这在目前没有太大意义，不要太担心。\n\n### 使用 C++ 引用避免空对象\n\n除了严格的所有权外，C++ 还有引用的概念，与 Java 中的引用不同。在内部，引用是不允许为空或重新打印的指针；因此，将它传递给函数时不涉及复制。\n\n因此，C++ 中的函数签名可以明确限制程序员将空对象作为参数传递。在 Java 中，程序员必须使用文档或注释来指示非空参数。\n\n看看这两个计算球体体积的 Java 函数。第一个抛出运行时异常如果一个空对象被传递给它，而第二个默默忽略空对象。\n\n如果传递一个空对象，Java 中的第一个实现会抛出一个运行时异常:\n\n```cpp\n// Java\nfloat getVolume1(Sphere s) {\n  float cube = Math.pow(s.radius(), 3);\n  return (Math.PI * 4 / 3) * cube; \n} \n```\n\nJava 中的第二个实现默默处理空对象:\n\n```cpp\n// Java\nfloat getVolume2(Sphere s) { \n  float rad = s == null ? 0.0f : s.radius();\n  float cube = Math.pow(rad, 3);\n  return (Math.PI * 4 / 3) * cube;\n} \n```\n\n在用 Java 实现的两个函数中，函数的调用方必须检查函数的实现，以确定是否允许空对象。\n\n在 C++ 中，第一个函数签名通过使用不能为空的引用，明确地只接受初始化的对象。使用指针作为参数的第二个版本明确显示空对象被处理。\n\n作为引用传递的 C++ 参数指示不允许空值:\n\n```cpp\nauto get_volume1(const Sphere& s) {   \n  auto cube = std::pow(s.radius(), 3.f);\n  auto pi = 3.14f;\n  return (pi * 4.f / 3.f) * cube;\n} \n```\n\n作为指针传递的 C++ 参数指示正在处理空值:\n\n```cpp\nauto get_volume2(const Sphere* s) {\n  auto rad = s ? s->radius() : 0.f;\n  auto cube = std::pow(rad, 3);\n  auto pi = 3.14f;\n  return (pi * 4.f / 3.f) * cube;\n} \n```\n\n能够在 C++ 中使用引用或值作为参数会立即通知 C++ 程序员该函数打算如何使用。相反，在 Java 中，用户必须检查函数的实现，因为对象总是作为指针传递的，并且它们有可能为空。\n\n## C++ 的缺点\n\n如果不提及 C++ 的一些缺点，将它与其他编程语言进行比较是不公平的。如前所述，C++ 有更多的概念需要学习，因此更难正确使用并发挥其全部潜力。然而，如果一个程序员能掌握 C++，更高的复杂性就变成了优势，代码库变得更健壮，性能也更好。\n\n尽管如此，C++ 也有一些缺点，这些只是缺点。这些缺点中最严重的是编译时间长和导入库的复杂性。直到 C++ 20，C++ 一直依赖于一个过时的导入系统，在这个系统中，导入的头被简单地粘贴到任何包含它们的内容中。C++ 20 中正在引入的 C++ 模块将解决基于包含头文件的系统的一些问题，也将对大型项目的编译时间产生积极影响。\n\nC++ 的另一个明显缺点是缺少提供的库。虽然其他语言通常带有大多数应用所需的所有库，如图形、用户界面、网络、线程、资源处理等，但 C++ 或多或少只提供最基本的算法、线程，以及从 C++ 17 开始的文件系统处理。除此之外，程序员不得不依赖外部库。\n\n综上所述，虽然 C++ 比大多数其他语言有更陡峭的学习曲线，但如果使用正确，C++ 的健壮性与许多其他语言相比是一个优势。因此，尽管编译时间很长，并且缺少提供的库，我相信 C++ 是非常适合大型项目的语言，即使对于性能不是最高优先级的项目也是如此。\n\n# 本书使用的库和编译器\n\n如前所述，就库而言，C++ 只提供了最基本的必需品。因此，在本书中，我们将不得不依赖外部图书馆。C++ 世界中最常用的库可能是 Boost 库([http://www.boost.org](http://www.boost.org))。\n\n本书的某些部分使用了标准 C++ 库不够用的 Boost 库。我们将只使用 Boost 库的仅头部分，这意味着自己使用它们不需要任何特定的构建设置；相反，您只需要包含指定的头文件。\n\n此外，我们将使用 Google Benchmark(一个微基准测试支持库)来评估小代码片段的性能。谷歌基准测试将在*第三章*、*性能分析和测量*中介绍。\n\n在[https://github . com/PacktPublishing/Cpp-High-Performance-Second-Edition](https://github.com/PacktPublishing/Cpp-High-Performance-Second-Edition)上提供的存储库以及该书的附带源代码使用了谷歌测试框架，使您更容易构建、运行和测试代码。\n\n还应该提到的是，这本书使用了很多来自 C++ 20 的新特性。在撰写本文时，我们使用的编译器(Clang、GCC 和 Microsoft Visual C++)还没有完全实现其中的一些功能。所呈现的一些特性完全缺失或仅得到实验支持。在[https://en.cppreference.com/w/cpp/compiler_support](https://en.cppreference.com/w/cpp/compiler_support)可以找到主要 C++ 编译器当前状态的优秀最新摘要。\n\n# 摘要\n\n在这一章中，我强调了 C++ 的一些特性和缺点，以及它是如何发展到今天的状态的。此外，我们还从性能和健壮性的角度讨论了 C++ 与其他语言相比的优缺点。\n\n在下一章中，我们将探索一些对语言发展产生重大影响的现代和基本的 C++ 特性。"
  },
  {
    "path": "docs/cpp-hiperf/02.md",
    "content": "# 二、基本的 C++ 技术\n\n在这一章中，我们将深入研究一些基本的 C++ 技术，例如移动语义、错误处理和 lambda 表达式，这些将在本书中用到。这些概念中的一些仍然让有经验的 C++ 程序员感到困惑，因此我们将研究他们的用例以及他们是如何工作的。\n\n本章将涵盖以下主题:\n\n*   自动类型推演以及声明函数和变量时如何使用`auto`关键字。\n*   移动语义和五的*规则和零*的*规则。*\n*   错误处理和合同。虽然这些主题没有提出任何可以被认为是现代 C++ 的东西，但是异常和契约都是当今 C++ 中备受争议的领域。\n*   使用 lambda 表达式创建函数对象，这是 C++ 11 最重要的特性之一。\n\n我们先来看看自动类型演绎。\n\n# 带有 auto 关键字的自动类型扣除\n\n自从 C++ 11 中`auto`关键字的引入之后，C++ 社区中就出现了很多关于如何使用`auto`不同口味的`const``auto&``auto&``auto&&`和`decltype(auto)`的困惑。\n\n## 在函数签名中使用自动\n\n虽然有些 C++ 程序员不鼓励使用，但根据我的经验，在函数签名中使用`auto`可以增加浏览和查看头文件时的可读性。\n\n与带有显式类型的传统语法相比，`auto`语法是这样的:\n\n<colgroup><col> <col></colgroup> \n| 显式类型的传统语法: | 自动的新语法: |\n| \n\n```cpp\nstruct Foo {\n  int val() const {    return m_;   }  const int& cref() const {    return m_;   }  int& mref() {    return m_;   }  int m_{};}; \n```\n\n | \n\n```cpp\nstruct Foo {\n  auto val() const {    return m_;   }  auto& cref() const {    return m_;   }  auto& mref() {    return m_;   }  int m_{};}; \n```\n\n |\n\n`auto`语法可以在有或没有尾随返回类型的情况下使用。在某些上下文中，尾随返回是必要的。例如，如果我们正在编写一个虚拟函数，或者函数声明放在头文件中，函数定义放在`.cpp`文件中。\n\n注意`auto`语法也可以用于自由函数:\n\n<colgroup><col> <col></colgroup> \n| 返回类型 | 句法变体(a、b 和 c 对应于相同的结果): |\n| 价值 | \n\n```cpp\nauto val() const                // a) auto, deduced type\nauto val() const -> int         // b) auto, trailing type\nint val() const                 // c) explicit type \n```\n\n |\n| 常量引用 | \n\n```cpp\nauto& cref() const              // a) auto, deduced type\nauto cref() const -> const int& // b) auto, trailing type\nconst int& cref() const         // c) explicit type \n```\n\n |\n| 可变引用 | \n\n```cpp\nauto& mref()                    // a) auto, deduced type\nauto mref() -> int&             // b) auto, trailing type\nint& mref()                     // c) explicit type \n```\n\n |\n\n### 使用 decltype(自动)转发返回类型\n\n有一个有点罕见的版本自动类型推演叫做`decltype(auto)`。它最常见的用途是从函数中转发精确的类型。假设我们正在为上一个表中声明的`val()`和`mref()`编写包装函数，如下所示:\n\n```cpp\nint val_wrapper() { return val(); }    // Returns int\nint& mref_wrapper() { return mref(); } // Returns int& \n```\n\n现在，如果我们想对包装函数使用返回类型推断，那么`auto`关键字会在两种情况下将返回类型推断为`int`:\n\n```cpp\nauto val_wrapper() { return val(); }   // Returns int\nauto mref_wrapper() { return mref(); } // Also returns int \n```\n\n如果我们想让我们的`mref_wrapper()`返回一个`int&`，我们需要写`auto&`。在这个例子中，这很好，因为我们知道`mref()`的返回类型。然而，情况并非总是如此。因此，如果我们希望编译器选择完全相同的类型，而不明确地说`int&`或`auto&`代表`mref_wrapper()`，我们可以使用`decltype(auto)`:\n\n```cpp\ndecltype(auto) val_wrapper() { return val(); }   // Returns int\ndecltype(auto) mref_wrapper() { return mref(); } // Returns int& \n```\n\n这样，当我们不知道`val()`或`mref()`返回什么函数时，我们可以避免在写入`auto`或`auto&`之间进行显式选择。这种情况通常发生在泛型代码中，被包装的函数类型是一个模板参数。\n\n## 对变量使用自动\n\nC++ 11 中`auto`关键字的引入在 C++ 程序员中引发了相当大的争论。许多人认为它降低了可读性，甚至认为它使 C++ 类似于动态类型语言。我倾向于不参与这些辩论，但我个人的观点是，你应该(几乎)总是使用`auto`，因为根据我的经验，它让代码更安全，也不那么杂乱无章。\n\n过度使用`auto`会使代码更难理解。在阅读代码时，我们通常想知道某些对象支持哪些操作。一个好的 IDE 可以为我们提供这些信息，但是在源代码中并没有明确地提供。C++ 20 概念通过关注对象的行为来解决这个问题。有关 C++ 概念的更多信息，请参见*第 8 章*、*编译时编程*。\n\n我更喜欢用`auto`来表示局部变量，使用从左到右的初始化风格。这意味着保持变量在左边，后面跟一个等号，然后类型在右边，如下所示:\n\n```cpp\nauto i = 0;\nauto x = Foo{};\nauto y = create_object();\nauto z = std::mutex{};     // OK since C++ 17 \n```\n\nC++ 17 引入*保证复制省略*，语句`auto x = Foo{}`与`Foo x{}`相同；也就是说，在这种情况下，语言保证没有需要移动或复制的临时对象。这意味着我们现在可以使用从左到右的初始化风格，而不用担心性能，我们也可以将其用于不可移动/不可复制的类型，如`std::atomic`或`std::mutex`。\n\n对变量使用`auto`的一个很大的好处是，由于`auto x;`不编译，所以永远不会让变量保持未初始化状态。未初始化的变量是未定义行为的一个特别常见的来源，您可以按照这里建议的样式完全消除它。\n\n使用`auto`将帮助您为变量使用正确的类型。但是，您仍然需要做的是，通过指定您是需要引用还是副本，以及您是想要修改变量还是只是从中读取，来表达您打算如何使用变量。\n\n### 常量引用\n\n由`const auto&`表示的`const`引用具有绑定任何东西的能力。原始对象永远不能通过这样的引用进行变异。我认为`const`参考应该是复制潜在昂贵对象的默认选择。\n\n如果`const`引用绑定到临时对象，则临时对象的生存期将延长到引用的生存期。这在下面的示例中得到了演示:\n\n```cpp\nvoid some_func(const std::string& a, const std::string& b) {\n  const auto& str = a + b;  // a + b returns a temporary\n  // ...\n} // str goes out of scope, temporary will be destroyed \n```\n\n也有可能通过使用`auto&`以`const`引用结束。这可以从以下示例中看出:\n\n```cpp\n auto foo = Foo{};\n auto& cref = foo.cref(); // cref is a const reference\n auto& mref = foo.mref(); // mref is a mutable reference \n```\n\n即使这是完全有效的，最好总是明确表示我们正在使用`const auto&`处理`const`引用，更重要的是，我们应该使用`auto&`到*仅*表示可变引用。\n\n### 可变引用\n\n与`const`引用相反，可变的引用不能绑定到临时引用。如上所述，我们使用`auto&`来表示可变引用。仅当您打算更改可变引用所引用的对象时，才使用可变引用。\n\n### 转发参考\n\n`auto&&`称为转发参考(也称为*通用参考*)。它可以绑定到任何东西，这使得它在某些情况下很有用。转发引用将像`const`引用一样，延长临时引用的生命周期。但是与`const`引用相反，`auto&&`允许我们变异它引用的对象，包括临时对象。\n\n对于只转发给其他代码的变量，使用`auto&&`。在那些转发的情况下，你很少关心变量是`const`还是可变的；你只是想把它传递给一些实际上要使用这个变量的代码。\n\n需要注意的是，`auto&&`和`T&&`只有在函数模板中使用时才是转发引用，其中`T`是该函数模板的模板参数。使用带有显式类型的`&&`语法，例如`std::string&&`，表示**右值**引用，不具有转发引用的属性(右值和移动语义将在本章后面讨论)。\n\n### 易于使用的实践\n\n虽然这是我的个人观点，但是我推荐基本型(`int`、`float`等)和像`std::pair`、`std::complex`这样的小非基本型使用`const auto`。对于可能复制成本较高的较大型号，请使用`const auto&`。这应该涵盖了 C++ 代码库中的大多数变量声明。\n\n`auto&`和`auto`应该只在需要可变引用或显式副本的行为时使用；这告诉代码的读者，这些变量很重要，因为它们要么复制一个对象，要么变异一个被引用的对象。最后，仅使用`auto&&`转发代码。\n\n遵循这些规则使您的代码库更容易阅读、调试和推理。\n\n虽然我建议对大多数变量声明使用`const auto`和`const auto&`，但我倾向于在本书的某些地方使用简单的`auto`，这可能看起来很奇怪。使用普通`auto`的原因是一本书的格式提供的空间有限。\n\n在继续之前，我们将花一点时间讨论`const`以及使用指针时如何传播`const`。\n\n## 指针的常量传播\n\n通过使用关键字`const`，我们可以通知编译器哪些对象是不可变的。然后，编译器可以检查我们是否试图变异不打算改变的对象。换句话说，编译器检查我们代码的`const`-正确性。用 C++ 编写`const`-正确代码时的一个常见错误是，`const`初始化的对象仍然可以操作成员指针指向的值。下面的例子说明了这个问题:\n\n```cpp\nclass Foo {\npublic:\n  Foo(int* ptr) : ptr_{ptr} {} \n  auto set_ptr_val(int v) const { \n    *ptr_ = v; // Compiles despite function being declared const!\n  }\nprivate:\n  int* ptr_{};\n};\nint main() {\n  auto i = 0;\n  const auto foo = Foo{&i};\n  foo.set_ptr_val(42);\n} \n```\n\n虽然函数`set_ptr_val()`正在变异`int`的值，但是声明它`const`是有效的，因为指针`ptr_`本身没有变异，只是指针指向的`int`对象。\n\n为了以可读的方式防止这种情况发生，标准库扩展中添加了一个名为`std::experimental::propagate_const`的包装器(在编写本文时，包含在最新版本的 Clang 和 GCC 中)。使用`propagate_const`，函数`set_ptr_val()`不会编译。注意`propagate_const`只适用于指针，类似指针的类如`std::shared_ptr`、`std::unique_ptr`，不适用`std::function`。\n\n下面的例子演示了当试图在`const`函数中变异一个对象时如何使用`propagate_const`产生编译错误:\n\n```cpp\n#include <experimental/propagate_const>\nclass Foo { \npublic: \n  Foo(int* ptr) : ptr_{ptr} {}\n  auto set_ptr(int* p) const { \n    ptr_ = p;  // Will not compile, as expected\n  }\n  auto set_val(int v) const { \n    val_ = v;  // Will not compile, as expected\n  }\n  auto set_ptr_val(int v) const { \n    *ptr_ = v; // Will not compile, const is propagated\n  }\nprivate:\n  std::experimental::propagate_const<int*> ptr_ = nullptr; \n  int val_{}; \n}; \n```\n\n大型代码库中正确使用`const`的重要性怎么强调都不为过，`propagate_const`的引入使得`const`-正确性更加有效。\n\n接下来，我们将看看 move 语义和一些处理类内资源的重要规则。\n\n# 移动语义解释\n\n移动语义是 C++ 11 中引入的一个概念，根据我的经验，这个概念很难掌握，即使是有经验的程序员也很难掌握。因此，我将尝试向您深入解释它是如何工作的，编译器何时使用它，以及最重要的是，为什么需要它。\n\n本质上，C++ 之所以甚至有移动语义的概念，而大多数其他语言没有，是因为它是一种基于值的语言，正如*第 1 章*、*c++ 简介*中所讨论的。如果 C++ 没有内置移动语义，基于值的语义的优势将在许多情况下丧失，程序员将不得不进行以下权衡之一:\n\n*   以高性能成本执行冗余深度克隆操作\n*   像 Java 那样对对象使用指针，失去了值语义的健壮性\n*   以牺牲可读性为代价执行容易出错的交换操作\n\n我们不想要这些，所以让我们看看移动语义如何帮助我们。\n\n## 复制-构建、交换和移动\n\n在我们进入移动的细节之前，我将首先解释和说明复制构造一个对象、交换两个对象和移动构造一个对象之间的区别。\n\n### 复制-构建对象\n\n当复制处理资源的对象时，需要分配一个新的资源，并且需要复制来自源对象的资源，以便两个对象完全分离。假设我们有一个类`Widget`，它引用了某种需要在构建时分配的资源。以下代码默认构造一个`Widget`对象，然后复制构造一个新实例:\n\n```cpp\nauto a = Widget{}; \nauto b = a;        // Copy-construction \n```\n\n下图说明了所进行的资源分配:\n\n<figure class=\"mediaobject\">![](img/B15619_02_01.png)</figure>\n\n图 2.1:复制带有资源的对象\n\n分配和复制是缓慢的过程，在许多情况下，源对象不再需要。有了移动语义，编译器就可以检测到类似这样的情况，即旧对象没有绑定到变量，而是执行移动操作。\n\n#### 交换两个对象\n\n在 C++ 11 中添加移动语义之前，交换两个对象的内容是一种常见的数据传输方式，无需分配和复制。如下所示，对象只是相互交换内容:\n\n```cpp\nauto a = Widget{};\nauto b = Widget{};\nstd::swap(a, b); \n```\n\n下图说明了该过程:\n\n<figure class=\"mediaobject\">![](img/B15619_02_02.png)</figure>\n\n图 2.2:在两个对象之间交换资源\n\n`std::swap()`函数是一个简单但有用的工具，在本章后面的复制和交换习惯用法中使用。\n\n#### 移动-构建对象\n\n移动对象时，目标对象直接从源对象窃取资源，源对象被重置。\n\n如您所见，这与交换非常相似，除了*从*移动的对象不必从*移动到*的对象接收资源:\n\n```cpp\nauto a = Widget{}; \nauto b = std::move(a); // Tell the compiler to move the resource into b \n```\n\n下图说明了该过程:\n\n<figure class=\"mediaobject\">![](img/B15619_02_03.png)</figure>\n\n图 2.3:将资源从一个对象移动到另一个对象\n\n虽然源对象被重置，但它仍然处于有效状态。编译器不会自动为我们重置源对象。相反，我们需要在移动构造函数中实现重置，以确保对象处于可以销毁或分配的有效状态。我们将在本章后面讨论有效状态。\n\n只有当对象类型拥有某种资源(最常见的情况是堆分配内存)时，移动对象才有意义。如果所有数据都包含在对象中，移动对象最有效的方法就是复制它。\n\n现在您已经基本掌握了移动语义，让我们来详细了解一下。\n\n## 资源获取与五大法则\n\n为了完全理解移动语义，我们需要回到 C++ 中类和资源获取的基础。C++ 中的一个基本概念就是一个类应该完全处理自己的资源。这意味着当一个类被复制、移动、拷贝分配、移动分配或析构时，该类应该确保其资源得到相应的处理。实现这五个功能的必要性通常被称为为**五大法则**。\n\n让我们看看如何在处理分配资源的类中实现五的规则。在下面的代码片段中定义的`Buffer`类中，分配的资源是由原始指针`ptr_`指向的`float`的数组:\n\n```cpp\nclass Buffer { \npublic: \n  // Constructor \n  Buffer(const std::initializer_list<float>& values)       : size_{values.size()} { \n    ptr_ = new float[values.size()]; \n    std::copy(values.begin(), values.end(), ptr_); \n  }\n  auto begin() const { return ptr_; } \n  auto end() const { return ptr_ + size_; } \n  /* The 5 special functions are defined below */\nprivate: \n  size_t size_{0}; \n  float* ptr_{nullptr};\n}; \n```\n\n在这种情况下，处理的资源是在`Buffer`类的构造函数中分配的一块内存。内存可能是类要处理的最常见的资源，但是资源可以更多:互斥体、显卡上纹理的句柄、线程句柄等等。\n\n五项规则中提到的五项职能被省略了，接下来将继续。我们将从复制构造函数、复制赋值和析构函数开始，它们都需要参与资源处理:\n\n```cpp\n// 1\\. Copy constructor \nBuffer::Buffer(const Buffer& other) : size_{other.size_} { \n  ptr_ = new float[size_]; \n  std::copy(other.ptr_, other.ptr_ + size_, ptr_); \n} \n// 2\\. Copy assignment \nauto& Buffer::operator=(const Buffer& other) {\n  delete [] ptr_;\n  ptr_ = new float[other.size_];\n  size_ = other.size_;\n  std::copy(other.ptr_, other.ptr_ + size_, ptr_);\n  return *this;\n} \n// 3\\. Destructor \nBuffer::~Buffer() { \n  delete [] ptr_; // OK, it is valid to delete a nullptr\n  ptr_ = nullptr;  \n} \n```\n\n在 C++ 11 中引入移动语义之前，这三个函数通常被称为三个的**规则。在以下情况下调用复制构造函数、复制赋值和析构函数:**\n\n```cpp\nauto func() { \n  // Construct \n  auto b0 = Buffer({0.0f, 0.5f, 1.0f, 1.5f}); \n  // 1\\. Copy-construct \n  auto b1 = b0; \n  // 2\\. Copy-assignment as b0 is already initialized \n  b0 = b1; \n} // 3\\. End of scope, the destructors are automatically invoked \n```\n\n虽然正确实现这三个功能是一个类处理其内部资源所需要的，但是会出现两个问题:\n\n*   **不可复制的资源**:在`Buffer`类的例子中，我们的资源是可以复制的，但是在其他类型的资源中，复制没有意义。例如，类中包含的资源可能是`std::thread`、网络连接或其他无法复制的东西。在这些情况下，我们不能绕过物体。\n*   **不必要的复制**:如果我们从一个函数返回我们的`Buffer`类，整个数组都需要复制。(不过，在某些情况下，编译器会优化掉副本，但我们暂时忽略这一点。)\n\n这些问题的解决方案是移动语义。除了复制构造函数和复制赋值，我们还可以在类中添加一个移动构造函数和一个移动赋值操作符。移动版本接受一个`Buffer&&`对象，而不是一个`const`引用(`const Buffer&`)作为参数。\n\n`&&`修饰符表示参数是我们打算从中移动而不是复制的对象。用 C++ 术语来说，这被称为右值，我们将在后面详细讨论。\n\n`copy()`函数复制一个对象，而移动等效函数用于将资源从一个对象移动到另一个对象，从资源中释放被移动的对象。\n\n这就是我们如何用移动构造函数和移动赋值扩展我们的`Buffer`类。如您所见，这些函数不会抛出任何异常，因此可以标记为`noexcept`。这是因为，与复制构造函数/复制赋值相反，它们不分配内存或做一些可能引发异常的事情:\n\n```cpp\n// 4\\. Move constructor\nBuffer::Buffer(Buffer&& other) noexcept     : size_{other.size_}, ptr_{other.ptr_} {\n  other.ptr_ = nullptr;\n  other.size_ = 0;\n}\n// 5\\. Move assignment\nauto& Buffer::operator=(Buffer&& other) noexcept {\n  ptr_ = other.ptr_;\n  size_ = other.size_;\n  other.ptr_ = nullptr;\n  other.size_ = 0;\n  return *this;\n} \n```\n\n现在，当编译器检测到我们执行了看似复制的操作，比如从一个函数中返回一个`Buffer`，但是复制自的值不再使用时，它将使用不抛出的移动构造函数/移动赋值，而不是复制。\n\n这是相当甜蜜的；界面仍然像复制时一样清晰，但是在幕后，编译器执行了一个简单的移动。因此，程序员不需要使用任何深奥的指针或输出参数来避免复制；当类实现了移动语义时，编译器会自动处理。\n\n不要忘记将你的移动构造函数和移动赋值操作符标记为`noexcept`(当然，除非它们可能抛出异常)。不标记它们`noexcept`会阻止标准库容器和算法使用它们，而是在特定条件下使用常规拷贝/分配。\n\n为了能够知道何时允许编译器移动对象而不是复制，理解右值是必要的。\n\n## 命名变量和值\n\n那么，什么时候才允许编译器移动对象而不是复制呢？简而言之，当对象可以归类为右值时，编译器会移动该对象。术语**右值**听起来可能很复杂，但本质上它只是一个没有绑定到命名变量的对象，原因如下:\n\n*   它直接来自一个函数\n*   我们使用`std::move()`使一个变量成为一个右值\n\n以下示例演示了这两种情况:\n\n```cpp\n// The object returned by make_buffer is not tied to a variable\nx = make_buffer();  // move-assigned\n// The variable \"x\" is passed into std::move()\ny = std::move(x);   // move-assigned \n```\n\n我也将在本书中交替使用术语**左值**和**命名变量**。左值对应于我们可以在代码中通过名称引用的对象。\n\n现在，我们将通过在类中使用类型为`std::string`的成员变量来使这更高级一点。以下`Button`课将作为示例:\n\n```cpp\nclass Button { \npublic: \n  Button() {} \n  auto set_title(const std::string& s) { \n    title_ = s; \n  } \n  auto set_title(std::string&& s) { \n    title_ = std::move(s); \n  } \n  std::string title_; \n}; \n```\n\n我们还需要一个返回标题和`Button`变量的自由函数:\n\n```cpp\nauto get_ok() {\n  return std::string(\"OK\");\n}\nauto button = Button{}; \n```\n\n考虑到这些先决条件，让我们详细看看几个复制和移动的例子:\n\n*   **案例 1** : `Button::title_`是副本分配的，因为`string`对象绑定到变量`str` :\n\n    ```cpp\n    auto str = std::string{\"OK\"};\n    button.set_title(str);              // copy-assigned \n    ```\n\n*   **情况 2** : `Button::title_`被移动分配，因为`str`经过`std::move()` :\n\n    ```cpp\n    auto str = std::string{\"OK\"};\n    button.set_title(std::move(str));   // move-assigned \n    ```\n\n*   **情况 3** : `Button::title_`是移动指定的，因为新的`std::string`对象直接从功能中出来:\n\n    ```cpp\n    button.set_title(get_ok());        // move-assigned \n    ```\n\n*   **情况 4** : `Button::title_`被拷贝分配，因为`string`对象被绑定到`s` (这与*情况 1* 相同):\n\n    ```cpp\n    auto str = get_ok();\n    button.set_title(str);             // copy-assigned \n    ```\n\n*   **案例 5** : `Button::title_`被拷贝分配，因为`str`被声明为`const`，因此不允许变异:\n\n    ```cpp\n    const auto str = get_ok();\n    button.set_title(std::move(str));  // copy-assigned \n    ```\n\n正如你所看到的，确定一个物体是被移动还是被复制是很简单的。如果有变量名，则复制；否则，它会被移动。如果使用`std::move()`移动一个命名对象，该对象不能被声明为`const`。\n\n## 默认移动语义和零规则\n\n本节讨论自动生成的复制分配运算符。重要的是要知道生成的函数没有强异常保证。因此，如果在复制分配期间引发异常，对象可能会处于只被部分复制的状态。\n\n与复制构造函数和复制赋值一样，移动构造函数和移动赋值可以由编译器生成。虽然有些编译器允许自己在特定条件下自动生成这些函数(后面会详细介绍)，但我们可以简单地使用`default`关键字强制编译器生成它们。\n\n对于不手动处理任何资源的`Button`类，我们可以简单地这样扩展它:\n\n```cpp\nclass Button {\npublic: \n  Button() {} // Same as before\n\n  // Copy-constructor/copy-assignment \n  Button(const Button&) = default; \n  auto operator=(const Button&) -> Button& = default;\n  // Move-constructor/move-assignment \n  Button(Button&&) noexcept = default; \n  auto operator=(Button&&) noexcept -> Button& = default; \n  // Destructor\n  ~Button() = default; \n  // ...\n}; \n```\n\n更简单地说，如果我们不声明*任何*自定义复制构造函数/复制赋值或析构函数，那么移动构造函数/移动赋值是隐式声明的，这意味着第一个`Button`类实际上处理一切:\n\n```cpp\nclass Button {\npublic: \n  Button() {} // Same as before\n\n  // Nothing here, the compiler generates everything automatically! \n  // ...\n}; \n```\n\n很容易忘记，只添加五个函数中的一个会阻止编译器生成其他函数。以下版本的`Button`类有一个自定义析构函数。因此，不会生成移动运算符，该类将始终被复制:\n\n```cpp\nclass Button {\npublic: \n  Button() {} \n  ~Button() \n    std::cout << \"destructed\\n\"\n  }\n  // ...\n}; \n```\n\n让我们看看在实现应用类时，如何将这种洞察力用于生成的函数。\n\n### 真实代码库中的零规则\n\n在实践中，必须自己编写复制/移动构造函数、复制/移动赋值和构造函数的情况应该很少。编写您的类，使它们不需要显式编写任何这些特殊的成员函数(或`default` -声明)，通常被称为零规则。这意味着，如果应用代码库中的一个类需要显式编写这些函数中的任何一个，那么这段代码最好放在代码库的库部分。\n\n在本书的后面，我们将讨论`std::optional`，这是一个在应用零规则时处理可选成员的便利实用类。\n\n#### 关于空析构函数的一点注记\n\n编写空析构函数可以阻止编译器实现某些优化。正如您在下面的片段中所看到的，用空析构函数复制一个普通类的数组会产生与用手工`for`循环复制相同的(非优化的)汇编代码。第一个版本使用空析构函数`std::copy()`:\n\n```cpp\nstruct Point {\n int x_, y_;\n ~Point() {}     // Empty destructor, don't use!\n};\nauto copy(Point* src, Point* dst) {\n  std::copy(src, src+64, dst);\n} \n```\n\n第二个版本使用了一个没有析构函数的`Point`类，但是有一个手工的`for`循环:\n\n```cpp\nstruct Point {\n  int x_, y_;\n};\nauto copy(Point* src, Point* dst) {\n  const auto end = src + 64;\n  for (; src != end; ++ src, ++ dst) {\n    *dst = *src;\n  }\n} \n```\n\n两个版本都生成了以下 x86 汇编程序，对应于一个简单的循环:\n\n```cpp\n xor eax, eax\n.L2:\n mov rdx, QWORD PTR [rdi+rax]\n mov QWORD PTR [rsi+rax], rdx\n add rax, 8\n cmp rax, 512\n jne .L2\n rep ret \n```\n\n然而，如果我们移除析构函数或声明析构函数`default`，编译器优化`std::copy()`以利用`memmove()`而不是循环:\n\n```cpp\nstruct Point { \n  int x_, y_; \n  ~Point() = default; // OK: Use default or no constructor at all\n};\nauto copy(Point* src, Point* dst) {\n  std::copy(src, src+64, dst);\n} \n```\n\n前面的代码通过`memmove()`优化生成了下面的 x86 汇编程序:\n\n```cpp\n mov rax, rdi\n mov edx, 512\n mov rdi, rsi\n mov rsi, rax\n jmp memmove \n```\n\n汇编程序是在*编译器浏览器*中使用 GCC 7.1 生成的，可在[https://godbolt.org/](https://godbolt.org/)上获得。\n\n总而言之，使用`default`析构函数或无析构函数来支持空析构函数，以从应用中挤出更多的性能。\n\n### 一个常见的陷阱——转移非资源\n\n使用默认创建的移动赋值时，有一个常见的陷阱:将基本类型与更高级的复合类型混合在一起的类。与复合类型相反，基本类型(如`int`、`float`、`bool`)在移动时只是被复制，因为它们不处理任何资源。\n\n当一个简单的类型与一个拥有资源的类型混合时，移动分配就变成了移动和复制的混合。\n\n下面是一个会失败的类的例子:\n\n```cpp\nclass Menu {\npublic:\n  Menu(const std::initializer_list<std::string>& items)       : items_{items} {}\n  auto select(int i) {\n    index_ = i;\n  }\n  auto selected_item() const {\n     return index_ != -1 ? items_[index_] : \"\";\n  }\n  // ...\nprivate:\n  int index_{-1}; // Currently selected item\n  std::vector<std::string> items_; \n}; \n```\n\n如果像这样使用`Menu`类，它将具有未定义的行为:\n\n```cpp\nauto a = Menu{\"New\", \"Open\", \"Close\", \"Save\"};\na.select(2);\nauto b = std::move(a);\nauto selected = a.selected_item(); // crash \n```\n\n未定义的行为随着`items_`向量的移动而发生，因此是空的。另一方面，`index_`是复制的，因此在移动对象`a`中仍然具有值`2`。当调用`selected_item()`时，函数将尝试在索引`2`处访问`items_`，程序将崩溃。\n\n在这些情况下，移动构造函数/赋值可以通过简单地交换成员来实现，如下所示:\n\n```cpp\nMenu(Menu&& other) noexcept { \n  std::swap(items_, other.items_); \n  std::swap(index_, other.index_); \n} \nauto& operator=(Menu&& other) noexcept { \n  std::swap(items_, other.items_); \n  std::swap(index_, other.index_); \n  return *this; \n} \n```\n\n这样，`Menu`级可以安全移动，同时还保留了不丢球的保证。在*第 8 章**编译时编程*中，您将学习如何利用 C++ 中的反射技术来自动创建交换元素的移动构造函数/赋值函数。\n\n## 将&&修饰符应用于类成员函数\n\n除了将应用于对象之外，您还可以将`&&`修改器添加到类的成员函数，就像您可以将`const`修改器应用到成员函数一样。与`const`修改器一样，只有当对象是右值时，才会通过重载解析来考虑具有`&&`修改器的成员函数:\n\n```cpp\nstruct Foo { \n  auto func() && {} \n}; \nauto a = Foo{}; \na.func();            // Doesn't compile, 'a' is not an rvalue \nstd::move(a).func(); // Compiles \nFoo{}.func();        // Compiles \n```\n\n似乎很奇怪会有人想要这种行为，但是有用例。我们将在*第 10 章* *代理对象和懒评*中调查其中一个。\n\n## 无论如何，当副本被删除时不要移动\n\n从函数中返回值时，使用`std::move()`可能很有诱惑力，如下所示:\n\n```cpp\nauto func() {\n  auto x = X{};\n  // ...\n  return std::move(x);  // Don't, RVO is prevented\n} \n```\n\n然而，除非`x`是只动类型，否则你不应该这样做。`std::move()`的这种用法阻止了编译器使用**返回值优化** ( **RVO** )从而完全省略了`x`的复制，这比移动它更有效率。所以，按值返回新创建的对象时，不要使用`std::move()`；相反，只需返回对象:\n\n```cpp\nauto func() {\n  auto x = X{};\n  // ...\n  return x;  // OK\n} \n```\n\n这个名为的对象被省略的特殊例子通常被称为 **NRVO** ，或者名为-RVO 的**。RVO 和 NRVO 目前由所有主要的 C++ 编译器实现。如果你想阅读更多关于 RVO 的内容，复制省略，可以在[https://en.cppreference.com/w/cpp/language/copy_elision](https://en.cppreference.com/w/cpp/language/copy_elision)找到详细的总结。**\n\n## 适用时按值传递\n\n考虑一个函数将一个`std::string`转换成小写。为了在适用的情况下使用移动构造函数，否则使用复制构造函数，似乎需要两个函数:\n\n```cpp\n// Argument s is a const reference\nauto str_to_lower(const std::string& s) -> std::string {\n  auto clone = s;\n  for (auto& c: clone) c = std::tolower(c);\n  return clone;\n}\n// Argument s is an rvalue reference\nauto str_to_lower(std ::string&& s) -> std::string {\n  for (auto& c: s) c = std::tolower(c);\n  return s;\n} \n```\n\n然而，通过取值`std::string`，我们可以编写一个函数来涵盖这两种情况:\n\n```cpp\nauto str_to_lower(std::string s) -> std::string {\n  for (auto& c: s) c = std::tolower(c);\n  return s;\n} \n```\n\n让我们看看为什么`str_to_lower()`的这个实现在可能的情况下避免了不必要的复制。当传递一个正则变量时，如下所示，`str`的内容在函数调用之前被复制构造到`s`中，然后在函数返回时被移动分配回`str`:\n\n```cpp\nauto str = std::string{\"ABC\"};\nstr = str_to_lower(str); \n```\n\n当传递一个右值时，如下所示，`str`的内容在函数调用之前被移动构造到`s`中，然后在函数返回时被移动分配回`str`。因此，不会通过函数调用进行复制:\n\n```cpp\nauto str = std::string{\"ABC\"};\nstr = str_to_lower(std::move(str)); \n```\n\n乍一看，这项技术似乎适用于所有参数。然而，这种模式并不总是最佳的，正如您接下来将看到的。\n\n### 传递值不适用的情况\n\n有时这种先接受价值再转移的模式实际上是一种模仿。例如，考虑下面的类，其中函数`set_data()`将保存传递给它的参数的副本:\n\n```cpp\nclass Widget {\n  std::vector<int> data_{};\n  // ...\npublic:\n  void set_data(std::vector<int> x) { \n    data_ = std::move(x);               \n  }\n}; \n```\n\n假设我们调用`set_data()`并传递一个左值，如下所示:\n\n```cpp\nauto v = std::vector<int>{1, 2, 3, 4};\nwidget.set_data(v);                  // Pass an lvalue \n```\n\n由于我们正在传递一个命名对象`v`，代码将复制-构造一个新的`std::vector`对象`x`，然后将该对象移动-分配到`data_`成员中。除非我们将一个空向量对象传递给`set_data()`，否则`std::vector`复制构造函数将为其内部缓冲区执行堆分配。\n\n现在将此与针对左值优化的以下版本的`set_data()`进行比较:\n\n```cpp\nvoid set_data(const std::vector<int>& x) { \n    data_ = x;  // Reuse internal buffer in data_ if possible\n} \n```\n\n这里，只有当当前向量`data_`的容量小于源对象`x`的大小时，赋值运算符内部才会有堆分配。换句话说，`data_`的内部预分配缓冲区在许多情况下可以在赋值操作符中重用，并使我们免于额外的堆分配。\n\n如果我们发现有必要为左值和右值优化`set_data()`，在这种情况下，最好提供两个重载:\n\n```cpp\nvoid set_data(const std::vector<int>& x) {\n  data_ = x;\n}\nvoid set_data(std::vector<int>&& x) noexcept { \n  data_ = std::move(x);\n} \n```\n\n第一个版本最适合左值，第二个版本最适合右值。\n\n最后，我们现在来看一个场景，在这个场景中，我们可以安全地传递值，而不用担心刚刚演示的模拟。\n\n### 移动构造函数参数\n\n在构造函数中初始化类成员时，我们可以安全地使用按值传递然后移动的模式。在构建新对象的过程中，不可能有预先分配的缓冲区可以用来避免堆分配。下面是一个带有一个`std::vector`成员和一个构造函数的类的例子来演示这个模式:\n\n```cpp\nclass Widget {\n  std::vector<int> data_;\npublic:\n  Widget(std::vector<int> x)       // By value\n      : data_{std::move(x)} {}     // Move-construct\n  // ...\n}; \n```\n\n我们现在将把焦点转移到一个不能被认为是*现代 C++* 但即使在今天也经常被讨论的话题上。\n\n# 设计带有错误处理的接口\n\n错误处理是重要的，也是函数和类的接口中经常被忽略的部分。在 C++ 中，错误处理是一个备受争议的话题，但是讨论往往集中在异常和其他错误机制上。虽然这是一个有趣的领域，但是在关注错误处理的实际实现之前，还有其他方面的错误处理更需要理解。显然，异常和错误代码已经在许多成功的软件项目中使用过，偶然发现将两者结合在一起的项目并不少见。\n\n无论使用何种编程语言，错误处理的一个基本方面是区分编程错误(也称为 bug)和**运行时错误**。运行时错误可进一步分为**可恢复运行时错误**和**不可恢复运行时错误**。不可恢复的运行时错误的一个例子是*堆栈溢出*(参见*第 7 章*、*内存管理*)。当不可恢复的错误发生时，程序通常会立即终止，因此发出这类错误的信号是没有意义的。然而，一些错误可能在一种类型的应用中被认为是可恢复的，但在其他应用中是不可恢复的。\n\n在讨论可恢复和不可恢复的错误时，经常出现的一个边缘情况是 C++ 标准库在内存不足时的一些不幸行为。当您的程序内存不足时，这通常是不可恢复的，然而当这种情况发生时，标准库(尝试)会抛出一个`std::bad_alloc`异常。我们在这里不会花时间讨论不可恢复的错误，但是如果你想更深入地研究这个话题，强烈推荐赫伯·萨特([https://sched.co/SiVW](https://sched.co/SiVW))的演讲*去碎片化 C++ :让异常和 RTTI 变得更加经济和可用。*\n\n当设计和实现一个应用编程接口时，您应该始终思考您正在处理什么类型的错误，因为不同类别的错误应该以完全不同的方式处理。确定错误是编程错误还是运行时错误可以通过使用名为**合同设计**的方法来完成；这是一个值得单独成书的话题。然而，我将在这里介绍基本原理，这些对于我们的目的来说已经足够了。\n\n有人提议在 C++ 中增加对契约的语言支持，但目前契约还没有达到标准。然而，许多 c++ API 和指南都假设您了解契约的基础知识，因为契约使用的术语使讨论和记录类和函数的接口变得更加容易。\n\n## 契约\n\n一个**契约**是在某个函数的调用者和函数本身(被调用者)之间的一组规则。C++ 允许我们使用 C++ 类型系统显式指定一些规则。例如，考虑以下函数签名:\n\n```cpp\nint func(float x, float y) \n```\n\n它指定`func()`正在返回一个整数(除非它抛出一个异常)，并且调用方必须传递两个浮点值。但是，它没有说明允许什么样的浮点值。例如，我们可以传递值 0.0 或负值吗？此外，`x`和`y`之间可能存在一些不容易用 C++ 类型系统表达的必需关系。当我们在 C++ 中谈论契约时，我们通常指的是调用者和被调用者之间存在的规则，这些规则不能用类型系统轻松表达。\n\n在不太正式的情况下，这里将介绍几个与契约设计相关的概念，以便为您提供一些术语，您可以用来推理接口和错误处理:\n\n*   **先决条件**规定了函数调用方的*职责。传递给函数的参数可能有限制。或者，如果它是成员函数，对象在调用函数之前可能必须处于特定状态。例如，在`std::vector`上调用`pop_back()`的前提条件是向量不为空。`pop_back()`的*调用者*有责任确保向量不为空。*\n*   **一个后置条件**规定了功能返回后的*职责。如果是成员函数，函数在什么状态下离开对象？例如`std::list::sort()`的后置条件是列表中的元素按升序排序。*\n*   **不变量**是一个应该永远成立的条件。不变量可以在许多环境中使用。一个*循环不变量*是在每个循环迭代开始时必须为真的条件。此外，*类不变量*定义了对象的有效状态。例如`std::vector`的一个不变量就是`size() <= capacity()`。明确陈述一些代码周围的不变量可以让我们更好地理解代码。不变量也是一个工具，可以用来证明某些算法做了它应该做的事情。\n\n类不变量非常重要；因此，我们将花更多的时间来讨论它们是什么，以及它们如何影响类的设计。\n\n### 类不变量\n\n如上所述，**类不变量**定义了对象的有效状态。它指定类内部数据成员之间的关系。在成员函数执行期间，对象可能暂时处于无效状态。重要的是，每当函数将控制传递给其他可以观察对象状态的代码时，不变量都会得到维护。当函数:\n\n*   返回\n*   引发异常\n*   调用回调函数\n*   调用其他一些可能观察当前调用对象状态的函数；一个常见的场景是将对`this`的引用传递给其他函数\n\n重要的是要认识到类不变量是类的每个成员函数的前置条件和后置条件的隐含部分。如果成员函数使对象处于无效状态，则后条件尚未满足。类似地，成员函数可以始终假设对象在调用该函数时处于有效状态。这个规则的例外是类的构造函数和析构函数。如果我们想插入代码来检查类不变量是否成立，我们可以从以下几点着手:\n\n```cpp\nstruct Widget {\n  Widget() {\n    // Initialize object…\n    // Check class invariant\n  }\n  ~Widget() {\n    // Check class invariant\n    // Destroy object…\n   }\n   auto some_func() {\n     // Check precondition (including class invariant)\n     // Do the actual work…\n     // Check postcondition (including class invariant)\n   }\n}; \n```\n\n这里省略了复制/移动构造函数和复制/移动赋值操作符，但是它们分别遵循与构造函数和`some_func()`相同的模式。\n\n当一个对象被移出时，该对象可能处于某个空状态或重置状态。这也是对象的有效状态，因此是类不变量的一部分。但是，当对象处于这种状态时，通常只能调用少数成员函数。例如，您不能在已经移动的`std::vector`上调用`push_back()`、`empty()`或`size()`，但您可以调用`clear()`，这将使向量处于可以再次使用的状态。\n\n但是，您应该知道，这个额外的重置状态会使类不变量变得越来越弱，越来越没用。为了完全避免这种状态，您应该以这样一种方式实现您的类，以便从对象被重置为对象在默认构造后的状态。我的建议是始终这样做，除非在极少数情况下，将移动状态重置为默认状态会带来不可接受的性能损失。通过这种方式，您可以更好地推理移动自状态，并且使用该类更安全，因为在该对象上调用成员函数是可以的。\n\n如果你能确保一个对象总是处于有效状态(类不变量成立)，你很可能有一个很难误用的类，如果你在实现中有 bug，它们通常很容易被发现。你最不想看到的就是在你的代码库中找到一个类，然后想知道这个类的某些行为是一个 bug 还是一个特性。违反合同总是一个严重的错误。\n\n为了能够写出有意义的类不变量，要求我们写出内聚性高、可能状态少的类。如果您曾经为自己编写的类编写过单元测试，那么您可能已经注意到，在编写单元测试时，很明显 API 可以从初始版本进行改进。单元测试迫使您使用和思考类的接口，而不是实现细节。同样，类不变量让你思考对象可能处于的所有有效状态。如果你发现很难定义一个类不变量，那通常是因为你的类有太多的责任和处理太多的状态。因此，定义类不变量通常意味着最终得到设计良好的类。\n\n### 维护合同\n\n契约是你设计并实现的应用编程接口的一部分。但是，如何使用您的应用编程接口维护合同并将其传达给客户呢？C++ 还没有内置的合同支持，但是正在努力将其添加到 C++ 的未来版本中。不过，有一些选择:\n\n*   使用诸如 Boost.Contract 之类的库\n*   记录合同。这样做的缺点是运行程序时不检查合同。此外，当代码改变时，文档往往会过时。\n*   使用`static_assert()`和`<cassert>`中定义的`assert()`宏。断言是可移植的，标准的 C++。\n*   构建一个自定义库，其中包含类似于断言的自定义宏，但是可以更好地控制失败契约的行为。\n\n在本书中，我们将使用断言，这是检查违反合同的最原始的方法之一。尽管如此，断言可能非常有效，并对代码质量产生巨大影响。\n\n#### 启用和禁用资产\n\n从技术上讲，我们有两种标准的方式来断言 C++ 中的事情:使用`static_assert()`或者来自`<cassert>`头的`assert()`宏。`static_assert()`是在代码编译期间验证的，因此需要一个可以在编译时而不是运行时检查的表达式。失败的`static_assert()`导致编译错误。\n\n对于只能在运行时评估的断言，需要使用`assert()`宏来代替。`assert()`宏是一个运行时检查，通常在调试和测试期间处于活动状态，当程序在发布模式下构建时，它将被完全禁用。`assert()`宏观通常是这样定义的:\n\n```cpp\n#ifdef NDEBUG\n#define assert(condition) ((void)0)\n#else\n#define assert(condition) /* implementation defined */\n#endif \n```\n\n这意味着您可以通过定义`NDEBUG`完全删除所有断言和用于检查条件的代码。\n\n现在，用一些术语从你的腰带下的契约设计开始，让我们关注契约违反(错误)以及如何在你的代码中处理它们。\n\n## 错误处理\n\n在设计具有适当错误处理的 API 时，首先要做的是区分编程错误和运行时错误。因此，在我们深入研究错误处理策略之前，我们将使用契约式设计来定义我们正在处理的错误类型。\n\n### 编程错误还是运行时错误？\n\n如果我们发现违反了合同，我们也发现了程序中的错误。例如，如果我们可以检测到有人在空向量上调用`pop_back()`，我们就知道我们的源代码中至少有一个 bug 需要修复。只要不满足前提条件，我们就知道我们正在处理一个*编程错误*。\n\n另一方面，如果我们有一个函数从磁盘加载一些记录，并且由于磁盘上的读取错误而无法返回记录，那么我们检测到了一个*运行时错误*:\n\n```cpp\nauto load_record(std::uint32_t id) {\n  assert(id != 0);           // Precondition\n  auto record = read(id);    // Read from disk, may throw\n  assert(record.is_valid()); // Postcondition\n  return record;\n} \n```\n\n前提条件满足，但是后条件因为我们程序外的原因无法满足。源代码中没有错误，但是由于一些与磁盘相关的错误，该函数无法返回在磁盘上找到的记录。由于无法满足后置条件，必须向调用者报告运行时错误，除非调用者可以通过重试等方式从错误中恢复过来。\n\n### 编程错误(错误)\n\n总的来说，编写代码来表示和处理代码中的 bug 是没有意义的。相反，使用断言(或者前面提到的其他替代方法)让开发人员意识到代码中的问题。对于可恢复的运行时错误，您应该只使用异常或错误代码。\n\n#### 通过假设缩小问题空间\n\n断言指定了作为某些代码的作者，您所做的假设。只有当代码中的所有断言都成立时，才能保证代码按预期工作。这使得编码变得更加容易，因为您可以有效地限制需要处理的案例数量。当使用、读取和修改您编写的代码时，断言对您的团队也是一个巨大的帮助。所有的假设都以断言的形式清晰地记录下来。\n\n#### 发现资产中的 bug\n\n失败的断言总是一个严重的错误。当你发现一个断言在测试中失败时，基本上有三个选项:\n\n*   断言是正确的，但是代码是错误的(要么是因为函数实现中的错误，要么是因为调用站点上的错误)。以我的经验，这是最常见的情况。获得正确的断言通常比获得正确的代码更容易。修复代码并再次测试。\n*   代码是正确的，但是断言是错误的。有时会发生这种情况，如果您正在查看旧代码，通常会非常不舒服。更改或删除失败的断言可能会很耗时，因为您需要 100%确定代码确实有效，并理解为什么旧的断言突然开始失败。通常，这是因为原始作者没有想到的新用例。\n*   断言和代码都是错误的。这通常需要重新设计类或函数。可能需求变了，程序员做的假设不再成立。但不要绝望；相反，您应该很高兴这些假设是使用断言显式编写的；现在你知道为什么代码不再工作了。\n\n运行时断言需要测试，否则断言不会被执行。新编写的带有许多断言的代码在测试时通常会中断。这并不意味着你是一个糟糕的程序员；这意味着您添加了有意义的断言来捕获一些错误，否则这些错误可能会进入生产。同样，导致程序测试版本终止的错误也可能被修复。\n\n#### 性能影响\n\n在你的代码中有许多运行时断言很可能会降低你的测试构建的性能。然而，断言从来不意味着在优化程序的最终版本中使用。如果您的断言使您的测试构建太慢而无法使用，那么在剖析器中找到减慢代码的断言集通常很容易跟踪(有关剖析的更多信息，请参见*第 3 章*、*分析和测量性能*)。\n\n通过让您的程序的发布版本完全忽略各种编程错误，您的程序将不会花时间检查由错误引起的错误状态。相反，您的代码将运行得更快，并且只花时间解决它应该解决的实际问题。它将只检查需要恢复的运行时错误。\n\n总而言之，在测试程序时应该检测到编程错误。不需要使用异常或其他错误处理机制来处理编程错误。相反，编程错误最好记录一些有意义的东西，并终止程序，以通知程序员需要修复错误。遵循这一准则可以大大减少代码中需要处理异常的地方。我们将在优化的构建中有更好的性能，并且希望更少的错误，因为它们已经被失败的断言检测到了。但是，也有可能在运行时出现错误的情况，这些错误需要我们实现的代码来处理和恢复。\n\n### 可恢复的运行时错误\n\n如果一个函数不能维护它在契约中的部分(即后置条件)，那么运行时错误已经发生，需要在代码中的某个地方发出信号来处理它并恢复有效状态。处理可恢复错误的目的是将错误从错误发生的地方传递到可以恢复有效状态的地方。有很多方法可以实现这一点。这枚硬币有两面:\n\n*   对于信号部分，我们可以在 C++ 异常、错误代码、返回一个`std::optional`或`std::pair`，或使用`boost::outcome`或`std::experimental::expected`之间进行选择。\n*   保持程序的有效状态而不泄漏任何资源。确定性析构函数和自动存储持续时间是在 C++ 中实现这一点的工具。\n\n实用程序类`std::optional`和`std::pair`将包含在*第 9 章*、*基本实用程序*中。我们现在将关注 C++ 异常，以及如何在从错误中恢复时避免资源泄漏。\n\n#### 例外\n\n例外是 C++ 提供的标准错误处理机制。这种语言被设计成在有例外的情况下使用。这方面的一个例子是构造函数失败；从构造函数发出错误信号的唯一方法是使用异常。\n\n根据我的经验，异常有许多不同的用法。其中一个原因是，不同的应用在处理运行时错误时会有截然不同的需求。对于一些应用，如起搏器或电厂控制系统，如果崩溃可能会产生严重影响，我们可能必须处理每一种可能的异常情况，如内存不足，并保持应用处于运行状态。有些应用甚至完全不使用堆内存，要么是因为平台根本没有可用的堆，要么是因为堆引入了不可控制的不确定性，因为分配新内存的机制超出了应用的控制范围。\n\n我假设您已经知道抛出和捕获异常的语法，这里就不赘述了。保证不抛出异常的函数可以标记为`noexcept`。重要的是要理解编译器不*而*验证这一点；相反，应该由代码的作者来判断他们的函数是否会抛出异常。\n\n标有`noexcept`的函数使得编译器在某些情况下可以生成更快的代码。如果标记有`noexcept`的函数抛出异常，程序将调用`std::terminate()`而不是展开堆栈。下面的代码演示了如何将函数标记为不抛出:\n\n```cpp\nauto add(int a, int b) noexcept {\n  return a + b;\n} \n```\n\n您可能会注意到，本书中的许多代码示例没有使用`noexcept`(或`const`)，即使它在生产代码中是合适的。这只是因为一本书的格式；我通常会在所有地方添加`noexcept`和`const`，这将使代码难以阅读。\n\n#### 保持有效状态\n\n异常处理需要我们程序员思考异常安全保障；也就是说，异常发生前后的程序状态是怎样的？强异常安全可以看作一个事务。函数要么提交所有状态更改，要么在出现异常时执行完全回滚。\n\n为了更具体一点，让我们来看看下面这个简单的函数:\n\n```cpp\nvoid func(std::string& str) {\n  str += f1();  // Could throw\n  str += f2();  // Could throw\n} \n```\n\n该函数将`f1()`和`f2()`的结果追加到字符串`str`中。现在考虑一下如果调用函数`f2()`时抛出异常会发生什么；只有来自`f1()`的结果会被附加到`str`上。相反，如果出现异常，我们希望`str`保持不变。这可以通过使用一个叫做**复制并交换**的习惯用法来解决。这意味着在我们让应用的状态被非抛出`swap()`函数修改之前，我们执行可能在临时副本上抛出异常的操作:\n\n```cpp\nvoid func(std::string& str) {\n  auto tmp = std::string{str};  // Copy\n  tmp += f1();                  // Mutate copy, may throw\n  tmp += f2();                  // Mutate copy, may throw\n  std::swap(tmp, str);          // Swap, never throws\n} \n```\n\n在成员函数中可以使用相同的模式来保持对象的有效状态。假设我们有一个包含两个数据成员的类，一个类不变量表示数据成员不能相等比较，如下所示:\n\n```cpp\nclass Number { /* ... */ };\nclass Widget {\npublic:\n  Widget(const Number& x, const Number& y) : x_{x}, y_{y} {\n    assert(is_valid());           // Check class invariant\n  }\nprivate:\n  Number x_{};\n  Number y_{};\n  bool is_valid() const {         // Class invariant\n   return x_ != y_;               // x_ and y_ must not be equal\n  }\n}; \n```\n\n接下来，假设我们添加了一个更新两个数据成员的成员函数，如下所示:\n\n```cpp\nvoid Widget::update(const Number& x, const Number& y) {\n  assert(x != y && is_valid());   // Precondition\n  x_ = x;\n  y_ = y;          \n  assert(is_valid());             // Postcondition\n} \n```\n\n前提条件是`x`和`y`不能相等比较。如果`x_`和`y_`的赋值可以抛出，`x_`可能会更新，但`y_`不会。这可能会导致类不变量被破坏；即处于无效状态的对象。如果发生错误，我们希望函数保留赋值操作之前对象的有效状态。同样，一个可能的解决方案是使用复制和交换习惯用法:\n\n```cpp\nvoid Widget::update(const Number& x, const Number& y) {\n    assert(x != y && is_valid());     // Precondition\n    auto x_tmp = x;  \n    auto y_tmp = y;  \n    std::swap(x_tmp, x_); \n    std::swap(y_tmp, y_); \n    assert(is_valid());               // Postcondition\n  } \n```\n\n首先，在不修改对象状态的情况下创建本地副本。然后，如果没有抛出异常，可以使用非抛出`swap()`来改变对象的状态。复制和交换习惯用法也可以在实现赋值操作符时使用，以实现强大的异常安全保证。\n\n错误处理的另一个重要方面是避免在错误发生时泄漏资源。\n\n#### 资源获取\n\nC++ 对象的破坏是可预测的，这意味着我们可以完全控制何时以及以何种顺序释放我们已经获得的资源。下面的例子进一步说明了这一点，互斥变量`m`在退出函数时总是解锁的，因为当我们退出作用域时，作用域锁会释放它，而不管我们如何以及在哪里退出:\n\n```cpp\nauto func(std::mutex& m, bool x, bool y) {\n  auto guard = std::scoped_lock{m}; // Lock mutex \n  if (x) { \n    // The guard automatically releases the mutex at early exit\n    return; \n  }\n  if (y) {\n    // The guard automatically releases if an exception is thrown\n    throw std::exception{};\n  }\n  // The guard automatically releases the mutex at function exit\n} \n```\n\n所有权、对象的生存期和资源获取是 C++ 中的基本概念，我们将在*第 7 章*、*内存管理*中进行介绍。\n\n#### 表演\n\n不幸的是，在性能方面，例外的名声很差。有些担忧是合理的，而有些则是基于编译器没有有效实现异常时的历史观察。然而，今天人们放弃例外有两个主要原因:\n\n*   即使没有抛出异常，二进制程序的大小也会增加。尽管这通常不是一个问题，但它并不遵循零开销原则，因为我们在为我们不使用的东西付费。\n*   抛出和捕捉异常相对昂贵。抛出和捕获异常的运行时成本是不确定的。这使得异常不适用于有严格实时要求的环境。在这种情况下，其他替代方法可能更好，例如返回带有返回值和错误代码的`std::pair`。\n\n另一方面，当没有抛出异常时，异常表现得非常出色；也就是说，当程序遵循成功路径时。其他错误报告机制，如错误代码，要求检查`if-else`语句中的返回代码，即使程序运行时没有任何错误。\n\n异常应该很少发生，通常当异常发生时，异常处理增加的额外性能损失在这些情况下通常不是问题。通常有可能在一些性能关键的代码运行之前或之后执行可能引发的计算。这样，我们就可以避免在程序中我们负担不起异常的地方抛出和捕获异常。\n\n为了公平地比较异常和其他错误报告机制，指定要比较的内容非常重要。有时将异常与完全没有错误处理进行比较，这是不公平的；当然，异常需要与提供相同功能的机制进行比较。在衡量异常可能产生的影响之前，不要因为性能原因而放弃它们。您可以在下一章阅读更多关于分析和测量性能的内容。\n\n现在我们将远离错误处理，探索如何使用 lambda 表达式创建函数对象。\n\n# 函数对象和 lambda 表达式\n\n在 C++ 11 中引入的 Lambda 表达式是现代 C++ 中最有用的特性之一，此后的每个 C++ 版本都进一步增强了它。它们的多功能性不仅来自于容易地将函数传递给算法，还来自于它们在许多需要传递代码的情况下的使用，尤其是当你可以在`std::function`中存储一个 lambda 时。\n\n虽然 lambdas 使这些编程技术的使用变得非常简单，但是在这个部分中提到的所有东西都可以在没有它们的情况下执行。lambda——或者更正式地说，lambda 表达式——是构造函数对象的一种便捷方式。但是我们可以用重载的`operator()`来实现类，然后实例化这些类来创建函数对象，而不是使用 lambda 表达式。\n\n稍后我们将探讨 lambda 与这些类的相似之处，但首先我将在一个简单的用例中介绍 lambda 表达式。\n\n## C++ lambda 的基本语法\n\n简而言之，lambdas 使程序员能够将函数传递给其他函数，就像传递变量一样容易。\n\n让我们将传递 lambda 给算法与传递变量进行比较:\n\n```cpp\n// Prerequisite \nauto v = std::vector{1, 3, 2, 5, 4}; \n\n// Look for number three \nauto three = 3; \nauto num_threes = std::count(v.begin(), v.end(), three); \n// num_threes is 1 \n\n// Look for numbers which is larger than three \nauto is_above_3 = [](int v) { return v > 3; }; \nauto num_above_3 = std::count_if(v.begin(), v.end(), is_above_3);\n// num_above_3 is 2 \n```\n\n在第一种情况下，我们传递一个变量给`std::count()`，在后一种情况下，我们传递一个函数对象给`std::count_if()`。这是 lambdas 的典型用例；我们将一个函数传递给另一个函数(在本例中为`std::count_if()`)进行多次求值。\n\n此外，lambda 不需要绑定到变量；就像我们可以将变量放入表达式一样，我们也可以用 lambda:\n\n```cpp\nauto num_3 = std::count(v.begin(), v.end(), 3); \nauto num_above_3 = std::count_if(v.begin(), v.end(), [](int i) { \n  return i > 3; \n}); \n```\n\n到目前为止你所看到的 lambdas 被称为**无状态 lambdas**；它们不复制或引用 lambda 外部的任何变量，因此不需要任何内部状态。让我们通过使用捕获块引入**有状态 lambdas** 来使这个更高级一点。\n\n## 俘获条款\n\n在前面的例子中，我们对 lambda 中的值`3`进行了硬编码，以便我们总是计算大于 3 的数字。如果我们想在 lambda 内部使用外部变量呢？我们所做的是通过将外部变量放入**捕获子句**来捕获它们；也就是λ的`[]`部分:\n\n```cpp\nauto count_value_above(const std::vector<int>& v, int x) { \n  auto is_above = [x](int i) { return i > x; }; \n  return std::count_if(v.begin(), v.end(), is_above); \n} \n```\n\n在这个例子中，我们通过将变量`x`复制到 lambda 中来捕获它。如果我们要声明`x`作为参考，我们在开头放一个`&`，像这样:\n\n```cpp\nauto is_above = [&x](int i) { return i > x; }; \n```\n\n该变量现在只是对外部`x`变量的引用，就像 C++ 中的常规引用变量一样。当然，我们需要非常谨慎地对待通过引用传递到 lambda 中的对象的生命周期，因为 lambda 可能在被引用对象已经不存在的上下文中执行。因此，通过价值获取更安全。\n\n### 按引用捕获与按值捕获\n\n使用 capture 子句进行引用和复制变量就像常规变量一样工作。看看这两个例子，看看你是否能发现其中的区别:\n\n<colgroup><col> <col></colgroup> \n| 按价值获取 | 引用捕获 |\n| \n\n```cpp\nauto func() {\n  auto vals = {1,2,3,4,5,6};\n  auto x = 3;\n  auto is_above = [x](int v) {\n    return v > x;\n  };\n  x = 4;\n  auto count_b = std::count_if(\n    vals.begin(),\n    vals.end(),\n    is_above\n   );  // count_b equals 3 } \n```\n\n | \n\n```cpp\nauto func() {\n  auto vals = {1,2,3,4,5,6};\n  auto x = 3;\n  auto is_above = [&x](int v) {\n    return v > x;\n  };\n  x = 4;\n  auto count_b = std::count_if(\n    vals.begin(),\n    vals.end(),\n    is_above\n   );  // count_b equals 2 } \n```\n\n |\n\n在第一个例子中，`x`被*复制到了*λ中，因此当`x`被 突变时不受影响；因此`std::count_if()`计算的数值大于 3。\n\n在第二个例子中，`x`被参考捕获到*，因此`std::count_if()`计算出的值的数大于 4。*\n\n### λ和类之间的相似性\n\n我前面提到 lambda 表达式生成函数对象。函数对象是定义了调用运算符`operator()()`的类的实例。\n\n为了理解 lambda 表达式由什么组成，您可以将其视为一个有限制的常规类:\n\n*   该类只包含一个成员函数\n*   capture 子句是类成员变量及其构造函数的组合\n\n下表显示了 lambda 表达式和相应的类。左栏使用*按值捕捉*，右栏使用 *c* 按引用捕捉:\n\n<colgroup><col> <col></colgroup> \n| 按值捕获的λ… | 通过引用捕获的λ... |\n| \n\n```cpp\nauto x = 3;auto is_above = [x](int y) { return y > x;};auto test = is_above(5); \n```\n\n | \n\n```cpp\nauto x = 3;auto is_above = [&x](int y) { return y > x;};auto test = is_above(5); \n```\n\n |\n| ...对应于此类: | ...对应于此类: |\n| \n\n```cpp\nauto x = 3;class IsAbove {\npublic: IsAbove(int x) : x{x} {} auto operator()(int y) const {   return y > x; }private: int x{}; // Value };auto is_above = IsAbove{x};\nauto test = is_above(5); \n```\n\n | \n\n```cpp\nauto x = 3;class IsAbove {\npublic: IsAbove(int& x) : x{x} {} auto operator()(int y) const {   return y > x; }private: int& x; // Reference };\nauto is_above = IsAbove{x};\nauto test = is_above(5); \n```\n\n |\n\n得益于 lambda 表达式，我们不必手动将这些函数对象类型实现为类。\n\n### 在捕获中初始化变量\n\n如前面的示例所示，capture 子句初始化相应类中的成员变量。这意味着我们也可以初始化 lambda 中的成员变量。这些变量只能从 lambda 内部看到。下面是一个初始化名为`numbers`的捕获变量的 lambda 示例:\n\n```cpp\nauto some_func = [numbers = std::list<int>{4,2}]() {\n  for (auto i : numbers)\n    std::cout << i;\n};\nsome_func();  // Output: 42 \n```\n\n对应的类看起来像这样:\n\n```cpp\nclass SomeFunc {\npublic:\n SomeFunc() : numbers{4, 2} {}\n void operator()() const {\n  for (auto i : numbers)\n    std::cout << i;\n }\nprivate:\n std::list<int> numbers;\n};\nauto some_func = SomeFunc{};\nsome_func(); // Output: 42 \n```\n\n初始化捕获内部的变量时，可以想象在变量名前面有一个隐藏的`auto`关键字。在这种情况下，你可以认为`numbers`被定义为像`auto numbers = std::list<int>{4, 2}`一样。如果你想初始化一个引用，你可以在名字前面用一个&符号，对应`auto&`。这里有一个例子:\n\n```cpp\nauto x = 1;\nauto some_func = [&y = x]() {\n  // y is a reference to x\n}; \n```\n\n同样，当引用(而不是复制)lambda 之外的对象时，您必须非常谨慎地对待生存期。\n\n也可以在 lambda 内移动对象，这在使用仅移动类型(如`std::unique_ptr`)时是必要的。这是如何做到的:\n\n```cpp\nauto x = std::make_unique<int>(); \nauto some_func = [x = std::move(x)]() {\n  // Use x here..\n}; \n```\n\n这也证明了变量可以使用相同的名称(`x`)。这是没有必要的。相反，我们可以在 lambda 中使用一些其他名称，例如`[y = std::move(x)]`。\n\n### 突变 lambda 成员变量\n\n由于 lambda 的工作原理就像一个带有成员变量的类，它也可以变异它们。但是一个 lambda 的函数调用运算符默认为`const`，所以我们明确需要指定 lambda 可以通过使用`mutable`关键字对其成员进行变异。在下面的例子中，lambda 每次被调用时都会变异`counter`变量:\n\n```cpp\nauto counter_func = [counter = 1]() mutable {\n  std::cout << counter++ ;\n};\ncounter_func(); // Output: 1\ncounter_func(); // Output: 2\ncounter_func(); // Output: 3 \n```\n\n如果一个 lambda 只通过引用捕获变量，我们不需要在声明中添加`mutable`修饰符，因为 lambda 本身不会变异。可变和不可变 lambdas 之间的区别在下面的代码片段中演示:\n\n<colgroup><col> <col></colgroup> \n| 按价值获取 | 引用捕获 |\n| \n\n```cpp\nauto some_func() {\n  auto v = 7;\n  auto lambda = [v]() mutable {\n    std::cout << v << \" \";\n    ++ v;\n  };\n  assert(v == 7);\n  lambda();  lambda();\n  assert(v == 7);\n  std::cout << v;\n} \n```\n\n | \n\n```cpp\nauto some_func() {\n  auto v = 7;\n  auto lambda = [&v]() {\n    std::cout << v << \" \";\n    ++ v;\n  };\n  assert(v == 7);\n  lambda();\n  lambda();\n  assert(v == 9);\n  std::cout << v;\n} \n```\n\n |\n| 输出:`7 8 7` | 输出:`7 8 9` |\n\n在右边的例子中`v`被引用捕获，lambda 将变异变量`v`，它属于`some_func()`的范围。左栏的突变 lambda 只会突变`v`的一个副本，属于 lambda 本身。这就是为什么我们最终会在两个版本中得到不同的输出。\n\n#### 从编译器的角度改变成员变量\n\n为了理解在前面的例子中发生了什么，看看编译器是如何看到前面的 lambda 对象的:\n\n<colgroup><col> <col></colgroup> \n| 按价值获取 | 引用捕获 |\n| \n\n```cpp\nclass Lambda {\n public:\n Lambda(int m) : v{m} {}\n auto operator()() {\n   std::cout<< v << \" \";\n   ++ v;\n }\nprivate:\n  int v{};\n}; \n```\n\n | \n\n```cpp\nclass Lambda {\n public:\n Lambda(int& m) : v{m} {}\n auto operator()() const {\n   std::cout<< v << \" \";\n   ++ v;\n }\nprivate:\n int& v;\n}; \n```\n\n |\n\n如您所见，第一种情况对应于具有常规成员的类，而通过引用捕获的情况只是对应于成员变量是引用的类。\n\n您可能已经注意到，我们在按引用捕获类的`operator()`成员函数上添加了修饰符`const`，并且我们也没有在相应的 lambda 上指定`mutable`。这个类仍然被认为是`const`的原因是我们没有在实际的类/lambda 里面变异任何东西；实际突变适用于参考值，因此该函数仍被视为`const`。\n\n### 捕获全部\n\n除了逐个捕获变量外，范围内的所有变量都可以通过简单写`[=]`或`[&]`来捕获。\n\n使用`[=]`意味着每个变量都将被值捕获，而`[&]`通过引用捕获所有变量。\n\n如果我们在成员函数中使用 lambdas，也可以通过使用`[this]`引用或通过编写`[*this]`复制来捕获整个对象:\n\n```cpp\nclass Foo { \npublic: \n auto member_function() { \n   auto a = 0; \n   auto b = 1.0f;\n   // Capture all variables by copy \n   auto lambda_0 = [=]() { std::cout << a << b; }; \n   // Capture all variables by reference \n   auto lambda_1 = [&]() { std::cout << a << b; }; \n   // Capture object by reference \n   auto lambda_2 = [this]() { std::cout << m_; }; \n   // Capture object by copy \n   auto lambda_3 = [*this]() { std::cout << m_; }; \n }\nprivate: \n int m_{}; \n}; \n```\n\n注意，使用`[=]`并不意味着范围内的所有变量都被复制到 lambda 中；仅复制 lambda 内部实际使用的变量。\n\n通过值捕获所有变量时，可以通过引用指定要捕获的变量(反之亦然)。下表显示了捕获块中不同组合的结果:\n\n<colgroup><col> <col></colgroup> \n| 捕获块 | 结果捕获类型 |\n| \n\n```cpp\nint a, b, c;auto func = [=] { /*...*/ }; \n```\n\n | 通过数值捕捉`a`、`b`、`c`。 |\n| \n\n```cpp\nint a, b, c;auto func = [&] { /*...*/ }; \n```\n\n | 参照捕捉`a`、`b`、`c`。 |\n| \n\n```cpp\nint a, b, c;auto func = [=, &c] { /*...*/ }; \n```\n\n | 通过数值捕捉`a`、`b`。参照捕捉`c`。 |\n| \n\n```cpp\nint a, b, c;auto func = [&, c] { /*...*/ }; \n```\n\n | 参照捕捉`a`、`b`。通过数值捕捉`c`。 |\n\n虽然用`[&]`或`[=]`捕获所有变量很方便，但我建议逐个捕获变量，因为它通过明确 lambda 范围内使用了哪些变量来提高代码的可读性。\n\n## 将 C 函数指针分配给 lambdas\n\n没有捕获的 Lambdas】可以隐式转换为函数指针。假设您使用的是一个 C 库，或者旧的 C++ 库，使用回调函数作为参数，如下所示:\n\n```cpp\nextern void download_webpage(const char* url,\n                              void (*callback)(int, const char*)); \n```\n\n回调用返回代码和一些下载的内容调用。调用`download_webpage()`时可以传递一个λ作为参数。由于回调是一个常规函数指针，lambda 不能有任何捕获，您必须在 lambda 前面使用一个加号(`+`):\n\n```cpp\nauto lambda = +[](int result, const char* str) {\n  // Process result and str\n};\ndownload_webpage(\"http://www.packt.com\", lambda); \n```\n\n这样，λ就被转换成了一个规则的函数指针。请注意，为了使用这个功能，lambda 根本不能有任何捕获。\n\n## Lambda 类型\n\n从 C++ 20 开始，没有捕获的 lambdas 是默认可构造和可赋值的。通过使用`decltype`，现在很容易构造具有相同类型的不同 lambda 对象:\n\n```cpp\nauto x = [] {};   // A lambda without captures\nauto y = x;       // Assignable\ndecltype(y) z;    // Default-constructible\nstatic_assert(std::is_same_v<decltype(x), decltype(y)>); // passes\nstatic_assert(std::is_same_v<decltype(x), decltype(z)>); // passes \n```\n\n然而，这仅适用于没有捕获的 lambdas。Lambdas *跟*抓拍都有自己独特的类型。即使两个带有捕获的 lambda 函数是彼此的简单克隆，它们仍然有自己独特的类型。因此，不可能将一个带有捕获的λ分配给另一个λ。\n\n## Lambdas 和 std::函数\n\n如前一节所述，带有捕获的 lambda(有状态 lambda)不能分配给彼此，因为它们有唯一的类型，即使它们看起来完全相同。为了能够用捕获来存储和传递 lambda，我们可以使用`std::function`来保存由 lambda 表达式构造的函数对象。\n\n`std::function`的签名定义如下:\n\n```cpp\nstd::function< return_type ( parameter0, parameter1...) > \n```\n\n因此，一个不返回任何东西并且没有参数的`std::function`是这样定义的:\n\n```cpp\nauto func = std::function<void(void)>{}; \n```\n\n以一个`int`和一个`std::string`作为参数返回一个`bool`的`std::function`定义如下:\n\n```cpp\nauto func = std::function<bool(int, std::string)>{}; \n```\n\n共享相同签名(相同参数和相同返回类型)的 Lambda 函数可以由相同类型的`std::function`对象持有。A `std::function`也可以在运行时重新分配。\n\n这里重要的是，lambda 捕获的内容不会影响其签名，因此无论有无捕获，lambda 都可以分配给同一个`std::function`变量。下面的代码显示了如何将不同的 lambdas 分配给同一个名为`func`的`std::function`对象:\n\n```cpp\n// Create an unassigned std::function object \nauto func = std::function<void(int)>{}; \n// Assign a lambda without capture to the std::function object \nfunc = [](int v) { std::cout << v; }; \nfunc(12); // Prints 12 \n// Assign a lambda with capture to the same std::function object \nauto forty_two = 42; \nfunc = [forty_two](int v) { std::cout << (v + forty_two); }; \nfunc(12); // Prints 54 \n```\n\n接下来，让我们将`std::function`用于类似于真实世界示例的东西中。\n\n### 用标准::函数实现一个简单的按钮类\n\n假设我们设置来实现一个`Button`类。然后我们可以使用`std::function`来存储点击按钮对应的动作，这样当我们调用`on_click()`成员函数时，相应的代码就会被执行。\n\n我们可以这样声明`Button`类:\n\n```cpp\nclass Button {\npublic: \n  Button(std::function<void(void)> click) : handler_{click} {} \n  auto on_click() const { handler_(); } \nprivate: \n  std::function<void(void)> handler_{};\n}; \n```\n\n然后，我们可以使用它来创建大量具有不同动作的按钮。按钮可以方便地存放在容器中，因为它们都具有相同的类型:\n\n```cpp\nauto create_buttons () { \n  auto beep = Button([counter = 0]() mutable {  \n    std::cout << \"Beep:\" << counter << \"! \"; \n    ++ counter; \n  }); \n  auto bop = Button([] { std::cout << \"Bop. \"; }); \n  auto silent = Button([] {});\n  return std::vector<Button>{beep, bop, silent}; \n} \n```\n\n迭代列表并在每个按钮上调用`on_click()`将执行相应的功能:\n\n```cpp\nconst auto& buttons = create_buttons();\nfor (const auto& b: buttons) {\n  b.on_click();\n}\nbuttons.front().on_click(); // counter has been incremented\n// Output: \"Beep:0! Bop. Beep:1!\" \n```\n\n前面带有按钮和点击处理程序的示例演示了将`std::function`与 lambdas 结合使用的一些好处；即使每个有状态 lambda 都有自己唯一的类型，单个`std::function`类型也可以包装共享相同签名(返回类型和参数)的 lambda。\n\n顺便说一下，你可能已经注意到`on_click()`成员函数被声明为`const`。然而，它通过增加一个点击处理程序中的`counter`变量来改变成员变量`handler_`。这似乎违反了常量正确性规则，因为`Button`的常量成员函数被允许在它的一个类成员上调用变异函数。允许的原因与允许成员指针在常量上下文中改变其指向值的原因相同。在本章的前面，我们讨论了如何传播指针数据成员的常量。\n\n### 标准::功能的性能考虑\n\n与直接由λ表达式构造的函数对象相比，α`std::function`有一些性能损失。本节将讨论使用`std::function`时需要考虑的一些与性能相关的问题。\n\n#### 阻止内联优化\n\n谈到 lambdas，编译器有能力内联函数调用；也就是说，函数调用的开销被消除了。`std::function`的灵活设计使得编译器几乎不可能内联一个包装在`std::function`中的函数。如果频繁调用包装在`std::function`中的小函数，防止内联优化会对性能产生负面影响。\n\n#### 为捕获的变量动态分配内存\n\n如果一个`std::function`被分配给一个带有捕获变量/引用的λ，`std::function`在大多数情况下将使用堆分配的内存来存储捕获的变量。如果捕获变量的大小低于某个阈值，`std::function`的一些实现不分配额外的内存。\n\n这意味着，由于额外的动态内存分配，不仅存在性能损失，而且速度更慢，因为堆分配的内存会增加缓存未命中的数量(请阅读*第 4 章*、*数据结构*中关于缓存未命中的更多内容)。\n\n#### 额外的运行时计算\n\n调用一个`std::function`一般来说比执行一个 lambda 要慢一点，因为会涉及到更多的代码。对于小型且经常被调用的`std::function`来说，这种开销可能会变得很大。假设我们有一个非常小的λ，定义如下:\n\n```cpp\nauto lambda = [](int v) { return v * 3; }; \n```\n\n接下来的基准测试展示了为显式 lambda 类型的`std::vector`和相应的`std::function`的`std::vector`执行 1000 万个函数调用之间的区别。我们将从使用显式 lambda 的版本开始:\n\n```cpp\nauto use_lambda() { \n  using T = decltype(lambda);\n  auto fs = std::vector<T>(10'000'000, lambda);\n  auto res = 1;\n  // Start clock\n  for (const auto& f: fs)\n    res = f(res);\n  // Stop clock here\n  return res;\n} \n```\n\n我们只测量在函数内部执行循环所需的时间。下一个版本将我们的λ包装在一个`std::function`中，看起来像这样:\n\n```cpp\nauto use_std_function() { \n  using T = std::function<int(int)>;\n  auto fs = std::vector<T>(10'000'000, T{lambda});\n  auto res = 1;\n  // Start clock\n  for (const auto& f: fs)\n    res = f(res);\n  // Stop clock here\n  return res;\n} \n```\n\n我正在我 2018 年的 MacBook Pro 上编译这段代码，使用的是启用了优化的 Clang(`-O3`)。第一个版本`use_lambda()`以大约 2 毫秒的速度执行循环，而第二个版本`use_std_function()`几乎需要 36 毫秒来执行循环。\n\n## 通用 lambdas\n\n通用的 lambda 是一个 lambda 接受`auto`参数，可以用任何类型调用它。它就像一个普通的 lambda 一样工作，但是`operator()`已经被定义为一个成员函数模板。\n\n只有参数是模板变量，而不是捕获的值。换句话说，无论`v0`和`v1`的类型如何，以下示例中的捕获值`v`都将是类型`int`:\n\n```cpp\nauto v = 3; // int\nauto lambda = [v](auto v0, auto v1) {\n  return v + v0*v1;\n}; \n```\n\n如果我们将上面的 lambda 翻译成一个类，它将对应于如下内容:\n\n```cpp\nclass Lambda {\npublic:\n  Lambda(int v) : v_{v} {}\n  template <typename T0, typename T1>\n  auto operator()(T0 v0, T1 v1) const { \n    return v_ + v0*v1; \n  }\nprivate:\n  int v_{};\n};\nauto v = 3;\nauto lambda = Lambda{v}; \n```\n\n就像模板化版本一样，编译器在调用 lambda 之前不会生成实际的函数。所以，如果我们像这样调用前面的 lambda:\n\n```cpp\nauto res_int = lambda(1, 2);\nauto res_float = lambda(1.0f, 2.0f); \n```\n\n编译器将生成类似于以下 lambdas 的东西:\n\n```cpp\nauto lambda_int = [v](int v0, const int v1) { return v + v0*v1; };\nauto lambda_float = [v](float v0, float v1) { return v + v0*v1; };\nauto res_int = lambda_int(1, 2);\nauto res_float = lambda_float(1.0f, 2.0f); \n```\n\n正如你可能已经发现的，这些版本就像普通的 lambdas 一样被进一步处理。\n\nC++ 20 的一个新特性是，我们可以使用`typename`而不仅仅是`auto`来表示泛型 lambda 的参数类型。以下通用 lambdas 是相同的:\n\n```cpp\n// Using auto\nauto x = [](auto v) { return v + 1; };\n// Using typename\nauto y = []<typename Val>(Val v) { return v + 1; }; \n```\n\n这使得命名类型或引用 lambda 主体内部的类型成为可能。\n\n# 摘要\n\n在本章中，您已经学习了如何使用将在本书中使用的现代 C++ 特性。自动类型推导、移动语义和 lambda 表达式是当今每个 C++ 程序员都需要适应的基本技术。\n\n我们还花了一些时间研究错误处理和如何考虑 bug，以及有效状态和如何从运行时错误中恢复。错误处理是编程中非常重要的一部分，很容易被忽略。考虑调用者和被调用者之间的契约是一种方法，可以使您的代码正确，并避免在程序的发布版本中进行不必要的防御检查。\n\n在下一章中，我们将研究分析和测量 C++ 性能的策略。"
  },
  {
    "path": "docs/cpp-hiperf/03.md",
    "content": "# 三、分析和测量性能\n\n由于这是一本关于编写高效运行的 C++ 代码的书，我们需要涵盖一些关于如何衡量软件性能和评估算法效率的基础知识。本章中的大多数主题都不是针对 C++ 的，只要遇到性能问题，就可以使用。\n\n您将学习如何使用大 O 符号来估计算法效率。这是从 C++ 标准库中选择算法和数据结构时必不可少的知识。如果你是大 O 符号的新手，这部分可能需要一些时间来消化。但是不要放弃！为了理解这本书的其余部分，更重要的是，为了成为一名有性能意识的程序员，这是一个非常重要的主题。如果你想对这些概念有一个更正式或更实用的介绍，有大量的书籍和在线资源专门讨论这个话题。另一方面，如果您已经掌握了大 O 符号，并且知道什么是摊销时间复杂度，那么您可以浏览下一节，转到本章的后面部分。\n\n本章包括以下几节:\n\n*   使用大 O 符号估计算法效率\n*   优化代码时的建议工作流程，这样您就不会在没有充分理由的情况下花时间微调代码\n*   中央处理器剖析器——它们是什么，为什么要使用它们\n*   微基准标记\n\n让我们首先看一下如何使用大 O 符号来估计算法效率。\n\n# 渐近复杂性和大 O 符号\n\n解决一个问题的方法通常不止一种，如果效率是一个考虑因素，你应该首先通过选择正确的算法和数据结构来关注高级优化。评估和比较算法的一个有用方法是分析它们的渐近计算复杂度——也就是说，分析当输入的大小增加时，运行时间或内存消耗是如何增加的。此外，C++ 标准库规定了所有容器和算法的渐近复杂性，这意味着如果您正在使用这个库，对这个主题的基本理解是必须的。如果您已经对算法复杂性和大 O 符号有了很好的理解，您可以放心地跳过这一部分。\n\n让我们从一个例子开始。假设我们想写一个算法，如果它在数组中找到一个特定的键就返回`true`，否则返回`false`。为了找出我们的算法在通过不同大小的数组时的行为，我们想要分析作为其输入大小的函数的该算法的运行时间:\n\n```cpp\nbool linear_search(const std::vector<int>& vals, int key) noexcept { \n  for (const auto& v : vals) { \n    if (v == key) { \n      return true; \n    } \n  } \n  return false; \n} \n```\n\n算法很简单。它遍历数组中的元素，并将每个元素与键进行比较。如果我们幸运的话，我们在数组的开头找到了键，它会立即返回，但是我们可能会在整个数组中循环，根本找不到键。这将是算法的最坏情况，一般来说，这是我们要分析的情况。\n\n但是当我们增加输入大小时，运行时间会发生什么呢？假设我们将数组的大小加倍。好吧，在最坏的情况下，我们需要比较数组中的所有元素，这将使运行时间翻倍。输入大小和运行时间之间似乎是线性关系。我们称之为线性增长率:\n\n<figure class=\"mediaobject\">![](img/B15619_03_01.png)</figure>\n\n图 3.1:线性增长率\n\n现在考虑以下算法:\n\n```cpp\nstruct Point { \n  int x_{}; \n  int y_{}; \n}; \n\nbool linear_search(const std::vector<Point>& a, const Point& key) { \n  for (size_t i = 0; i < a.size(); ++ i) { \n    if (a[i].x_ == key.x_ && a[i].y_ == key.y_) { \n      return true; \n    } \n  } \n  return false; \n} \n```\n\n我们比较的是点而不是整数，我们使用一个带有下标操作符的索引来访问每个元素。这些变化对运行时间有何影响？与第一种算法相比，绝对运行时间可能更长，因为我们做了更多的工作——例如，点的比较涉及两个整数，而不是数组中每个元素一个整数。然而，在这个阶段，我们对算法显示的增长率感兴趣，如果我们将运行时间与输入大小进行比较，我们仍然会得到一条直线，如上图所示。\n\n作为搜索整数的最后一个例子，让我们看看如果我们假设数组中的元素是排序的，是否能找到更好的算法。不管元素的顺序如何，我们的第一个算法都可以工作，但是如果我们知道它们是排序的，我们可以使用二分搜索法。它通过查看中间的元素来决定是继续在数组的前半部分搜索还是后半部分搜索。为简单起见，索引`high`、`low`和`mid`属于`int`类型，需要一个`static_cast`。更好的选择是使用迭代器，这将在后面的章节中介绍。以下是算法:\n\n```cpp\nbool binary_search(const std::vector<int>& a, int key) {\n  auto low = 0; \n  auto high = static_cast<int>(a.size()) - 1;\n  while (low <= high) {\n    const auto mid = std::midpoint(low, high); // C++ 20\n    if (a[mid] < key) {\n      low = mid + 1;\n    } else if (a[mid] > key) {\n      high = mid - 1;\n    } else {\n      return true;\n    }\n  }\n  return false;\n} \n```\n\n可以看到，这个算法比简单的线性扫描更难得到的正确。它通过*猜测*在数组的中间来寻找指定的键。如果不是，它会将键与中间的元素进行比较，以决定应该在数组的哪一半继续查找键。因此，在每次迭代中，它会将数组切成两半。\n\n假设我们用包含 64 个元素的数组来调用`binary_search()`。在第一次迭代中，我们拒绝 32 个元素，在下一次迭代中，我们拒绝 16 个元素，在下一次迭代中，我们拒绝 8 个元素，以此类推，直到没有更多的元素可以比较，或者直到我们找到关键字。对于 64 的输入大小，最多有 7 次循环迭代。如果我们*把输入尺寸*增加一倍到 128 呢？由于我们在每次迭代中将规模减半，这意味着我们只需要*再进行一次循环迭代*。显然，增长率不再是线性的——它实际上是对数的。如果我们测量`binary_search()`的运行时间，我们会看到增长率看起来如下:\n\n<figure class=\"mediaobject\">![](img/B15619_03_02.png)</figure>\n\n图 3.2:对数增长率\n\n在我的机器上，三个算法的快速计时以不同的输入大小重复调用 10000 次( *n* )产生了下表所示的结果:\n\n<colgroup><col> <col> <col> <col></colgroup> \n| 算法 | n = 10 | n = 1，000 | n = 10 万 |\n| 通过`int`进行线性搜索 | 0.04 毫秒 | 4.7 毫秒 | 458 毫秒 |\n| 通过`Point`进行线性搜索 | 0.07 毫秒 | 6.7 毫秒 | 725 毫秒 |\n| 二分搜索法与`int` | 0.03 毫秒 | 0.08 毫秒 | 0.16 毫秒 |\n\n表 3.1:不同版本搜索算法的比较\n\n比较算法 1 和 2，我们可以看到比较点而不是整数需要更多的时间，但是即使输入大小增加，它们仍然在同一个数量级。然而，如果我们在输入大小增加时比较所有三种算法，真正重要的是算法表现出的增长率。通过利用数组被排序的事实，我们可以用很少的循环迭代来实现搜索功能。对于大型阵列，与线性扫描阵列相比，二分搜索法实际上是自由的。\n\n在确定已经为问题选择了正确的算法和数据结构之前，花时间调优代码通常不是一个好主意。\n\n如果我们能以一种有助于我们决定使用哪种算法的方式来表达算法的增长率，那不是很好吗？这里是大 O 符号派上用场的地方。\n\n下面是一个非正式的定义:\n\n如果 *f(n)* 是一个用输入大小 *n* 指定算法运行时间的函数，我们说 *f(n)* 是*O(g(n)】*如果有一个常数 *k* 使得![](img/B15619_03_001.png)。\n\n这意味着我们可以说`linear_search()`的时间复杂度是 *O(n)* ，对于两个版本(用整数运算的和用点运算的)，而`binary_search()`的时间复杂度是 *O(log n)* 或者 *log n* 的大 O。\n\n在实践中，当我们想要找到一个函数的大 O 时，我们可以通过消除除增长率最大的项之外的所有项，然后移除任何常数因子来实现。例如，如果我们有一个时间复杂度由*f(n)= 4n*<sup class=\"italic\">2</sup>*+30n+100*描述的算法，我们挑选出增长率最高的术语，4 *n* <sup class=\"italic\">2</sup> 。接下来，我们去掉常数因子 4，以 *n* <sup class=\"italic\">2</sup> 结束，这意味着我们可以说我们的算法运行在*O(n*<sup class=\"italic\">2</sup>*)*中。找到算法的时间复杂度可能很难，但是在编写代码时，你越开始考虑它，它就会变得越容易。在大多数情况下，跟踪循环和递归函数就足够了。\n\n让我们试着找出以下排序算法的时间复杂度:\n\n```cpp\nvoid insertion_sort(std::vector<int>& a) { \n  for (size_t i = 1; i < a.size(); ++ i) { \n    auto j = i; \n    while (j > 0 && a[j-1] > a[j]) {  \n      std::swap(a[j], a[j-1]); \n      --j;  \n    } \n  } \n} \n```\n\n输入大小是数组的大小。运行时间可以通过查看迭代所有元素的循环来近似估计。首先，有一个迭代 *n - 1* 元素的外部循环。内循环不同:我们第一次到达`while`-循环，`j`为 1，循环只运行一次迭代。在下一次迭代中，`j`从 2 开始，减少到 0。对于外部`for`循环中的每次迭代，内部循环需要做的工作越来越多。最后，`j`从 *n - 1* 开始，这意味着在最坏的情况下，我们已经执行了`swap()` *1 + 2 + 3 +...+ (n - 1)* 次。我们可以用 *n* 来表示，注意这是一个算术级数。该系列的总和为:\n\n<figure class=\"mediaobject\">![](img/B15619_03_002.png)</figure>\n\n因此，如果我们设置 *k = (n - 1)* ，则排序算法的时间复杂度为:\n\n<figure class=\"mediaobject\">![](img/B15619_03_003.png)</figure>\n\n现在，我们可以通过首先消除除增长率最大的项之外的所有项来找到该函数的大 O，剩下的是 *(1/2)n* <sup class=\"italic\">2</sup> 。之后我们去掉常数 *1/2* ，得出排序算法的运行时间为 *O(n* <sup class=\"italic\">2</sup> *)* 。\n\n## 增长率\n\n如前所述，寻找一个复杂度函数的大 O 的第一步是去掉除了增长率最高的项之外的所有项。为了能够做到这一点，我们必须知道一些常见函数的增长率。在下图中，我绘制了一些最常见的函数:\n\n<figure class=\"mediaobject\">![](img/B15619_03_03.png)</figure>\n\n图 3.3:增长率函数的比较\n\n增长率与机器或编码方式等无关。当两种算法之间的增长率不同时，当输入大小变得足够大时，增长率最慢的算法总是会赢。让我们看看，如果我们假设执行 1 个单位的工作需要 1 毫秒，那么对于不同的增长率，运行时间会发生什么。下表列出了增长函数及其常用名称和不同的输入大小， *n* :\n\n<colgroup><col> <col> <col> <col> <col></colgroup> \n| 大 O | 名字 | n = 10 | n = 50 | n = 1000 |\n| *O(1)* | 常数 | 0.001 秒 | 0.001 秒 | 0.001 秒 |\n| *O(对数 n)* | 对数的 | 0.003 秒 | 0.006 秒 | 0.01 秒 |\n| *O(n)* | 线性的 | 0.01 秒 | 0.05 秒 | 1 秒 |\n| *O(n 对数 n)* | 线性或 *n 对数* | 0.03 秒 | 0.3 秒 | 10 秒 |\n| *O(n* <sup class=\"italic\">2</sup> *)* | 二次的 | 0.1 秒 | 2.5 秒 | 16.7 分钟 |\n| *O(2* <sup class=\"italic\">n</sup> *)* | 指数的 | 1 秒 | 35700 年 | 3.4 * 10 <sup class=\"Superscript--PACKT-\">290</sup> 年 |\n\n表 3.2:不同增长率和不同输入大小值的绝对运行时间\n\n请注意，右下角单元格中的数字是一个 291 位数！把这个和宇宙的年龄对比一下，13.7 * 10 <sup class=\"Superscript--PACKT-\">9</sup> 年，这只是一个 11 位数。\n\n接下来，我将介绍 C++ 标准库中经常使用的摊销时间复杂度。\n\n## 摊销时间复杂性\n\n通常，算法在不同的输入下表现不同。回到我们的线性搜索数组中元素的算法，我们正在分析一个键根本不在数组中的情况。对于该算法来说，这是最坏的情况——也就是说，它使用了该算法所需的大部分资源。最佳情况是指算法将需要的*最少的*资源量，而平均情况是指算法在不同输入下平均使用的资源量。\n\n标准库通常指对容器进行操作的函数的*摊销运行时间*。如果一个算法在恒定的摊销时间内运行，这意味着它几乎在所有情况下都会在 *O(1)* 中运行，只有极少数情况下它的性能会更差。乍一看，摊销运行时间可能与平均时间混淆，但正如您将看到的，它们是不一样的。\n\n为了理解摊销时间的复杂性，我们将花一些时间思考`std::vector::push_back()`。让我们假设向量内部有一个固定大小的数组来存储它的所有元素。如果在调用`push_back()`时固定大小数组中有更多元素的空间，操作将在恒定时间内运行，*O(1)*—也就是说，只要内部数组还有一个空间，它就不依赖于向量中已经有多少元素:\n\n```cpp\nif (internal_array.size() > size) { \n  internal_array[size] = new_element; \n  ++ size; \n} \n```\n\n但是当内部数组已满时会发生什么？处理增长向量的一种方法是创建一个新的更大的空内部数组，然后将所有元素从旧数组移动到新数组。这显然不再是恒定时间，因为我们需要数组中每个元素移动一次，也就是说， *O(n)* 。如果我们认为这是最坏的情况，那就意味着`push_back()`是 *O(n)* 。但是如果我们多次调用`push_back()`的话，我们知道昂贵的`push_back()`不可能经常发生，所以会比较悲观，也不是很有用，如果我们知道`push_back()`连续多次调用的话，说`push_back()`就是 *O(n)* 。\n\n摊销运行时间用于分析一个*操作序列*，而不是单个操作序列。我们仍然在分析最坏的情况，但是对于一系列的操作。摊销运行时间可以通过首先分析整个序列的运行时间，然后除以序列的长度来计算。假设我们正在执行一系列的 *m* 操作，总运行时间 *T(m)* :\n\n<figure class=\"mediaobject\">![](img/B15619_03_004.png)</figure>\n\n其中 *t* <sub class=\"italic\">0</sub> *= 1* ， *t* <sub class=\"italic\">1</sub> *= n* ，*t*<sub class=\"italic\">2</sub>*= 1*，*t*<sub class=\"italic\">3</sub>*= n*等等。换句话说，一半的操作以恒定时间运行，另一半以线性时间运行。所有 *m* 操作的总时间 *T* 可表示如下:\n\n<figure class=\"mediaobject\">![](img/B15619_03_005.png)</figure>\n\n每项操作的摊余复杂度为总时间除以操作次数，变为 *O(n)* :\n\n<figure class=\"mediaobject\">![](img/B15619_03_006.png)</figure>\n\n然而，如果我们能够保证昂贵操作的数量与恒定时间操作的数量相比相差几个数量级，我们将实现更低的摊余运行成本。例如，如果我们能保证一个昂贵的操作在一个序列中只发生一次*T(n)+T(1)+T(1+)...*，则摊销运行时间为 *O(1)* 。因此，根据昂贵操作的频率，摊余运行时间会发生变化。\n\n现在，回到`std::vector`。C++ 标准规定`push_back()`需要在摊销常数时间内运行， *O(1)* 。图书馆供应商是如何做到这一点的？如果容量在每次向量变满时增加固定数量的元素，我们将有一个类似于前面的例子，其中我们有一个运行时间 *O(n)* 。即使我们使用一个大的常数，容量变化仍然会以固定的时间间隔发生。关键的见解是，向量需要指数增长，以使昂贵的操作很少发生。在内部，向量使用增长因子，这样新阵列的容量就是当前大小乘以增长因子。\n\n一个大的增长因素可能会浪费更多的内存，但会降低昂贵操作的发生频率。为了简化数学运算，让我们使用一个通用策略，即每次向量需要增长时将容量增加一倍。我们现在可以估计昂贵的电话发生的频率。对于大小为 *n* 的向量，我们需要将内部数组 *log* <sub class=\"italic\">2</sub> *(n)* 次增长，因为我们一直在将大小翻倍。每次我们扩展数组时，我们都需要移动当前数组中的所有元素。第 *i* <sup class=\"italic\">第</sup>次我们生长阵列时会有 *2* <sup class=\"italic\">i</sup> 元素移动。因此，如果我们执行 *m* 个`push_back()`操作，增长操作的总运行时间将为:\n\n<figure class=\"mediaobject\">![](img/B15619_03_007.png)</figure>\n\n这是一个几何级数，也可以表示为:\n\n<figure class=\"mediaobject\">![](img/B15619_03_008.png)</figure>\n\n将这个除以序列的长度 *m* ，我们得到摊销运行时间 *O(1)* 。\n\n正如我已经说过的，摊销时间复杂度在标准库中被大量使用，所以理解分析很好。思考如何在摊销常数时间中实现`push_back()`帮助我记住了摊销常数时间的简化版本:它几乎在所有情况下都会在 *O(1)* 中运行，除了极少数情况下它会表现得更差。\n\n这就是我们将要讨论的关于渐近复杂性的全部内容。现在我们将继续讨论如何通过优化代码来解决性能问题并有效工作。\n\n# 测量什么以及如何测量？\n\n优化几乎总是会增加代码的复杂性。高级别的优化，比如选择算法和数据结构，可以让代码的意图更加清晰，但在很大程度上，优化会让代码更难阅读和维护。因此，我们希望绝对确保我们添加的优化对我们试图实现的性能有实际影响。我们真的需要让代码更快吗？用什么方式？代码真的占用了太多内存吗？为了理解哪些优化是可能的，我们需要很好地理解需求，例如延迟、吞吐量和内存使用。\n\n优化代码很有趣，但也很容易迷失，没有任何可衡量的收益。我们将从优化代码时要遵循的建议工作流开始本节:\n\n1.  **定义一个目标**:如果你有一个明确的量化目标，那么更容易知道如何优化以及何时停止优化。对于一些应用来说，从一开始就很清楚需求是什么，但是在许多情况下，它往往更加模糊。尽管代码运行太慢可能是显而易见的，但知道什么足够好是很重要的。每个领域都有自己的限制，所以一定要了解与你的应用相关的限制。下面是一些让它更具体的例子:\n    *   用户交互应用的响应时间为 100 毫秒；参考[https://www . nngroup . com/articles/response-times-3-重要-限制](https://www.nngroup.com/articles/response-times-3-important-limits)。\n    *   每秒 60 帧(FPS)的显卡为您提供每帧 16 毫秒的时间。\n    *   以 44.1 千赫采样速率使用 128 个采样缓冲器的实时音频意味着略低于 3 毫秒。\n2.  **测量**:一旦我们知道要测量什么以及极限是什么，我们就开始测量应用现在的性能。从*第一步*开始，如果我们对平均时间、峰值、负荷等感兴趣，应该是显而易见的。在这一步中，我们只关心衡量我们设定的目标。根据不同的应用，测量可以是从使用秒表到使用高度复杂的性能分析工具。\n3.  **找到瓶颈**:接下来，我们需要找到应用的瓶颈——那些太慢而使应用无用的部分。此时不要相信自己的直觉！也许你通过在*步骤 2* 中的不同点测量代码获得了一些见解——这很好，但是你通常需要进一步剖析你的代码，以便找到最重要的热点。\n4.  **进行有根据的猜测**:想出一个如何提高成绩的假设。可以使用查找表吗？我们能否缓存数据以获得整体吞吐量？我们能改变代码以便编译器能够向量化它吗？我们可以通过重用内存来减少关键部分的分配数量吗？如果你知道想法只是有根据的猜测，那么想出想法通常并不难。错了也没关系——以后你会发现它们是否有影响。\n5.  **优化**:我们来实现我们在*第四步*中勾画的假设。不要花太多时间在这一步上，在你知道它真的有效果之前，让它变得完美。准备好拒绝这种优化。它可能没有预期的效果。\n6.  **评估**:再次测量。进行与*步骤 2* 完全相同的测试，并比较结果。我们得到了什么？如果我们一无所获，拒绝代码并返回*步骤 4* 。如果优化实际上产生了积极的效果，你需要问自己是否足够好，可以花更多的时间在上面。优化有多复杂？值得付出努力吗？这是总体性能提升还是高度特定于某个案例/平台？它可维护吗？我们能封装它吗，还是它遍布整个代码库？如果不能激励优化，回到*步骤 4* ，否则继续最后一步。\n7.  **重构**:如果你按照*第五步*中的说明，没有花太多时间在第一时间编写完美代码，那么是时候重构优化，让它更干净了。优化几乎总是需要一些注释来解释我们为什么以不同寻常的方式做事。\n\n遵循这个过程将确保你保持在正确的轨道上，不会以没有动力的复杂优化结束。花时间定义具体目标和衡量的重要性怎么估计也不过分。为了在这方面取得成功，您需要了解哪些性能属性与您的应用相关。\n\n## 性能属性\n\n在开始测量之前，您必须知道哪些性能属性对您正在编写的应用很重要。在本节中，我将解释一些在衡量性能时经常使用的术语。根据您正在编写的应用，有些属性比其他属性更相关。例如，如果您正在编写在线图像转换器服务，吞吐量可能是比延迟更重要的属性，而在编写具有实时需求的交互式应用时，延迟是关键。以下是一些有价值的术语和概念，值得在绩效评估过程中熟悉:\n\n*   **延迟/响应时间**:根据领域的不同，延迟和响应时间可能有非常精确和不同的含义。然而，在本书中，我指的是操作的请求和响应之间的时间——例如，图像转换服务处理一个图像所需的时间。\n*   **吞吐量**:这是指单位时间内处理的事务(操作、请求等)数量——例如，图像转换服务每秒可以处理的图像数量。\n*   **I/O 绑定或 CPU 绑定**:一个任务通常大部分时间都是在 CPU 上计算事情或者等待 I/O(硬盘、网络等)。如果一个任务在中央处理器更快的情况下运行得更快，它就被称为是受中央处理器限制的。如果它通过加快输入/输出速度来运行得更快，那么它就被称为是输入/输出绑定的。有时你也会听说内存受限的任务，这意味着主内存的数量或速度是当前的瓶颈。\n*   **功耗**:对于在带电池的移动设备上执行的代码来说，这是一个非常重要的考虑因素。为了降低功耗，应用需要更高效地使用硬件，就像我们在优化 CPU 使用率、网络效率等一样。除此之外，应该避免高频轮询，因为它会阻止 CPU 进入睡眠状态。\n*   **数据聚合**:性能测量时采集大量样本时，通常需要对数据进行聚合。有时候*平均值*是一个很好的指标，可以反映程序的表现，但是更多时候*中值*可以告诉你更多关于实际表现的信息，因为它对异常值的反应更强。如果您对异常值感兴趣，您可以随时测量*最小值*和*最大值*值(例如，第 10 个百分位数)。\n\n这个列表并不详尽，但这是一个好的开始。这里需要记住的重要一点是，在衡量绩效时，我们可以使用既定的术语和概念。花一些时间来定义我们优化代码的真正含义有助于我们更快地达到目标。\n\n## 加速执行时间\n\n当我们比较一个程序或功能的两个版本之间的相对性能时，习惯上谈论**加速**。在这里，我将给你一个比较执行时间(或延迟)时加速的定义。假设我们已经测量了一些代码的两个版本的执行时间:旧的较慢版本和新的较快版本。然后可以相应地计算执行时间的加速:\n\n<figure class=\"mediaobject\">![](img/B15619_03_009.png)</figure>\n\n其中 *T* <sub class=\"italic\">旧的</sub>是代码初始版本的执行时间，*T*T6【新的】T7 是优化版本的执行时间。这个加速的定义意味着 1 的加速意味着根本没有加速。\n\n让我们通过一个例子来确保您知道如何测量相对执行时间。假设我们有一个在 10 毫秒内执行的函数( *T* <sub class=\"italic\">旧的</sub> = 10 毫秒)，经过一些优化后，我们设法让它在 4 毫秒内运行( *T* <sub class=\"italic\">新的</sub> = 4 毫秒)。然后，我们可以计算加速比如下:\n\n<figure class=\"mediaobject\">![](img/B15619_03_010.png)</figure>\n\n换句话说，我们新的优化版本提供了 2.5 倍的加速。如果我们想用百分比来表示这个改进，我们可以用下面的公式将加速转换成百分比改进:\n\n<figure class=\"mediaobject\">![](img/B15619_03_011.png)</figure>\n\n然后，我们可以说新版本的代码比旧版本的代码运行速度快 60%，这相当于 2.5 倍的加速。在本书中，当比较执行时间时，我将始终使用加速，而不是百分比改进。\n\n最后，我们通常对执行时间感兴趣，但时间并不总是最好的衡量标准。通过检查硬件上的其他值，硬件可能会为我们优化代码提供一些其他有用的指导。\n\n## 性能计数器\n\n除了显而易见的属性，如执行时间和内存使用情况，它有时对测量其他东西也是有益的。要么是因为它们更可靠，要么是因为它们可以让我们更好地了解是什么导致我们的代码运行缓慢。\n\n许多中央处理器都配备了硬件性能计数器，可以为我们提供诸如指令数量、中央处理器周期、分支预测错误和高速缓存未命中等指标。我还没有在本书中介绍这些硬件方面，我们也不会深入探讨性能计数器。不过，很高兴知道它们的存在，并且有现成的工具和库(可通过 API 访问)供所有主要操作系统在运行程序时收集**性能监控计数器** ( **PMC** )。\n\n对性能计数器的支持因 CPU 和操作系统而异。英特尔提供了一个名为 VTune 的强大工具，可用于监控性能计数器。FreeBSD 提供`pmcstat`。macOS 配备了 DTrace 和 Xcode 仪器。Microsoft Visual Studio 提供了在 Windows 上收集 CPU 计数器的支持。\n\n另一个流行的工具是`perf`，它在 GNU/Linux 系统上可用。运行命令:\n\n```cpp\nperf stat ./your-program \n```\n\n会揭示很多有趣的事件，比如上下文切换的次数、页面错误、预测错误的分支等等。以下是运行小程序时输出的示例:\n\n```cpp\nPerformance counter stats for './my-prog':\n     1 129,86 msec task-clock               # 1,000 CPUs utilized          \n            8      context-switches         # 0,007 K/sec                  \n            0      cpu-migrations           # 0,000 K/sec                  \n       97 810      page-faults              # 0,087 M/sec                  \n3 968 043 041      cycles                   # 3,512 GHz                    \n1 250 538 491      stalled-cycles-frontend  # 31,52% frontend cycles idle\n  497 225 466      stalled-cycles-backend   # 12,53% backend cycles idle    \n6 237 037 204      instructions             # 1,57  insn per cycle         \n                                            # 0,20  stalled cycles per insn\n1 853 556 742      branches                 # 1640,516 M/sec                  \n    3 486 026      branch-misses            # 0,19% of all branches        \n  1,130355771 sec  time elapsed\n  1,026068000 sec  user\n  0,104210000 sec  sys \n```\n\n我们现在将继续强调测试和评估性能时的一些最佳实践。\n\n## 性能测试—最佳实践\n\n由于某种原因，回归测试覆盖功能需求比性能需求或测试中覆盖的其他非功能需求更常见。性能测试通常更零星地进行，而且在开发过程中往往太晚了。我的建议是，通过向您的夜间构建添加性能测试，尽早进行度量并尽快检测回归。\n\n如果要处理大量输入，请明智地选择算法和数据结构，但不要在没有充分理由的情况下微调代码。尽早用真实的测试数据测试应用也很重要。在项目早期询问有关数据大小的问题。应用应该处理多少个表行，并且仍然能够平滑滚动？不要仅仅用 100 个元素来尝试它，并希望你的代码能够扩展——测试它！\n\n绘制数据图是了解您收集的数据的一种非常有效的方式。今天有这么多好用的标图工具，真的没有不标图的借口。RStudio 和 Octave 都提供了强大的绘图功能。其他例子包括 gnuplot 和 Matplotlib (Python)，它们可以在各种平台上使用，并且在收集数据后只需要最少的脚本就可以生成有用的图。一个情节不一定要看起来漂亮才有用。一旦你绘制了你的数据，你将会看到异常值和模式，这些通常很难在满是数字的表格中找到。\n\n我们的*要测量什么以及如何测量*到此结束？部分。接下来，我们将继续探索寻找代码中浪费太多资源的关键部分的方法。\n\n# 了解您的代码和热点\n\n自 100 多年前意大利经济学家维尔弗雷多·帕累托首次观察到帕累托原则(即 80/20 规则)以来，该原则已被应用于各个领域。他能够证明 20%的意大利人口拥有 80%的土地。在计算机科学中，它被广泛使用，甚至可能被过度使用。在软件优化中，它建议 20%的代码负责程序使用的 80%的资源。\n\n当然，这只是一个经验法则，不应该太随便。然而，对于尚未优化的代码，通常会发现一些相对较小的热点，它们占用了总资源的绝大部分。作为一名程序员，这实际上是一个好消息，因为这意味着我们可以编写大部分代码，而无需出于性能原因对其进行调整，而是专注于保持代码干净。也意味着在做优化的时候，我们需要知道*在*哪里做；否则，我们很有可能优化不会对整体性能产生影响的代码。在这一节中，我们将研究寻找可能值得优化的 20%代码的方法和工具。\n\n使用探查器通常是识别程序中热点的最有效方法。分析器分析程序的执行，并输出统计概要，即程序中函数或指令被调用的频率。\n\n此外，分析程序通常还会输出一个调用图，显示函数调用之间的关系，即分析过程中调用的每个函数的调用方和被调用方。下图中可以看到`sort()`函数是从`main()`(调用者)调用的，`sort()`调用了函数`swap()`(被调用者):\n\n<figure class=\"mediaobject\">![](img/New_B15619_03_04.png)</figure>\n\n图 3.4:调用图的例子。函数 sort()被调用一次，swap()被调用 50 次。\n\nprofiler 主要有两大类:采样 profiler 和仪器 profiler。这些方法也可以混合起来，形成采样和仪器的混合。 Unix 性能分析工具`gprof`就是一个例子。接下来的章节将重点介绍仪器分析器和采样分析器。\n\n## 仪表分析器\n\n所谓插装，我指的是将代码插入到要分析的程序中，以便收集关于每个函数执行频率的信息。通常，插入的检测代码记录每个入口点和出口点。您可以通过自己手动插入代码来编写自己的原始检测探查器，也可以使用自动插入必要代码的工具作为构建过程中的一个步骤。\n\n对于您的目的来说，一个简单的实现可能已经足够好了，但是要注意添加的代码对性能的影响，这会使概要文件产生误导。像这样幼稚的实现的另一个问题是，它可能会阻止编译器优化，或者冒被优化的风险。\n\n仅举一个工具分析器的例子，这里是我在以前的项目中使用的计时器类的简化版本:\n\n```cpp\nclass ScopedTimer { \npublic: \n  using ClockType = std::chrono::steady_clock;\n  ScopedTimer(const char* func) \n      : function_name_{func}, start_{ClockType::now()} {}\n  ScopedTimer(const ScopedTimer&) = delete; \n  ScopedTimer(ScopedTimer&&) = delete; \n  auto operator=(const ScopedTimer&) -> ScopedTimer& = delete; \n  auto operator=(ScopedTimer&&) -> ScopedTimer& = delete;\n  ~ScopedTimer() {\n    using namespace std::chrono;\n    auto stop = ClockType::now(); \n    auto duration = (stop - start_); \n    auto ms = duration_cast<milliseconds>(duration).count(); \n    std::cout << ms << \" ms \" << function_name_ << '\\n'; \n  } \n\nprivate: \n  const char* function_name_{}; \n  const ClockType::time_point start_{}; \n}; \n```\n\n`ScopedTimer`类将测量从它被创建到它超出范围，也就是被破坏的时间。我们正在使用类`std::chrono::steady_clock`，从 C++ 11 开始可用，它是为测量时间间隔而设计的。`steady_clock`是单调的，这意味着它在两次连续调用`clock_type::now()`之间永远不会减少。例如，系统时钟就不是这样，它可以随时调整。\n\n我们现在可以使用我们的计时器类，通过在每个函数的开头创建一个`ScopedTimer`实例来测量程序中的每个函数:\n\n```cpp\nauto some_function() {\n  ScopedTimer timer{\"some_function\"};\n  // ...\n} \n```\n\n尽管我们一般不建议使用预处理器宏，但这可能是使用预处理器宏的一种情况:\n\n```cpp\n#if USE_TIMER \n#define MEASURE_FUNCTION() ScopedTimer timer{__func__} \n#else \n#define MEASURE_FUNCTION() \n#endif \n```\n\n我们使用自 C++ 11 以来唯一预定义的函数-局部`__func__`变量来获取函数的名称。C++ 20 还引入了得心应手的`std::source_location`类，为我们提供了`function_name()`、`file_name()`、`line()`和`column()`等功能。如果您的编译器还不支持`std::source_location`，那么还有其他非标准的预定义宏被广泛支持，对调试非常有用，例如`__FUNCTION__`、`__FILE__`和`__LINE__`。\n\n现在，我们的`ScopedTimer`类可以这样使用:\n\n```cpp\nauto some_function() { \n  MEASURE_FUNCTION(); \n  // ...\n} \n```\n\n假设我们在编译我们的计时器时定义了`USE_TIMER`，那么每次`some_function()`返回时，它都会产生以下输出:\n\n```cpp\n2.3 ms some_function \n```\n\n我已经演示了如何通过插入打印代码中两点之间经过时间的代码来手动检测代码。虽然这对于某些场景来说是一个方便的工具，但是请注意像这样一个简单的工具可能会产生误导性的结果。在下一节中，我将介绍一种不需要对执行代码进行任何修改的分析方法。\n\n## 取样剖面仪\n\n采样分析器通过以均匀的时间间隔查看运行中的程序的状态来创建一个概要文件——通常是每 10 毫秒一次。采样分析器通常对程序的实际性能影响最小，并且也可以在所有优化都打开的情况下在发布模式下构建程序。采样剖析器的一个缺点是它们的不准确性和统计方法，只要你意识到这一点，这通常不是问题。\n\n下图显示了一个运行程序的采样会话，它有五个功能:`main()`、`f1()`、`f2()`、`f3()`和`f4()`。**t**<sub class=\"bold\">1</sub>-**t**<sub class=\"bold\">10</sub>标签指示每个样品的采集时间。方框指示每个执行功能的入口和出口点:\n\n<figure class=\"mediaobject\">![](img/B15619_03_05.png)</figure>\n\n图 3.5:采样分析器会话的示例\n\n概况如下表所示:\n\n<colgroup><col> <col> <col></colgroup> \n| 功能 | 总数 | 自己 |\n| `main()` | 100% | 10% |\n| `f1()` | 80% | 10% |\n| `f2()` | 70% | thirty percent |\n| `f3()` | 50% | 50% |\n\n表 3.3:对于每个函数，概要文件显示了它出现在调用堆栈中的总百分比(总计)和它出现在堆栈顶部的调用堆栈的百分比(自)。\n\n上表中的**总计**列显示了包含某个函数的调用堆栈的百分比。在我们的示例中，主函数出现在 10 个调用栈中的所有 10 个中(100%)，而`f2()`函数仅在 7 个调用栈中被检测到，这对应于所有调用栈的 70%。\n\n**Self** 列显示了每个函数在调用堆栈顶部出现的次数。在第五个样本 **t** <sub class=\"bold\">5</sub> 的调用堆栈顶部检测到一次`main()`函数，而在样本 **t** <sub class=\"bold\">6</sub> 、 **t** <sub class=\"bold\">8</sub> 和 **t** <sub class=\"bold\">9</sub> 的调用堆栈顶部检测到一次`main()`函数，对应于 3/10 = 30%。\n\n`f3()`函数具有最高的**自我**值(5/10)，并且每当检测到它时，它都在调用堆栈的顶部。\n\n从概念上讲，采样分析器以均匀的时间间隔存储调用堆栈的样本。它检测当前在中央处理器上运行的内容。纯采样分析器通常只检测当前正在运行的线程中执行的函数，因为睡眠线程不会在中央处理器上被调度。这意味着，如果一个函数正在等待一个导致线程休眠的锁，那么这个时间将不会出现在时间配置文件中。这很重要，因为您的瓶颈可能是由线程同步引起的，而线程同步对于采样探查器来说可能是不可见的。\n\n`f4()`功能怎么了？根据图表，它是由样本 2 和样本 3 之间的`f2()` 函数调用的，但它从未出现在我们的统计数据中，因为它从未在任何调用堆栈中注册过。这是采样剖面仪的一个重要特性。如果每个采样之间的时间太长或总采样时间太短，那么短且不经常调用的函数将不会出现在配置文件中。这通常不是问题，因为这些函数很少是您需要调整的函数。你可能会注意到`f3()`功能在 **t** <sub class=\"bold\">5</sub> 和 **t** <sub class=\"bold\">6</sub> 之间也被错过了，但是由于`f3()`被调用的非常频繁，不管怎么说对侧面的影响还是很大的。\n\n确保你理解你的时间分析器实际记录了什么。意识到它的局限性和优势，以便尽可能有效地使用它。\n\n# 微基准标记\n\n概要分析可以帮助我们找到代码中的瓶颈。如果这些瓶颈是由低效的数据结构(参见*第 4 章*、*数据结构*)、错误的算法选择(参见*第 5 章*、*算法*)或不必要的争用(参见*第 11 章*、*并发*造成的，那么这些更大的问题应该首先解决。但是有时候我们会发现一个小函数或者一小段代码需要优化，在这种情况下，我们可以使用一种叫做**微基准标记**的方法。通过这个过程，我们创建了一个微基准——一个独立于程序的其他部分运行一小段代码的程序。微基准标记的过程包括以下步骤:\n\n1.  找到需要调整的热点，最好使用探查器。\n2.  将其与代码的其余部分分开，并创建一个独立的微基准。\n3.  优化微基准。在优化过程中，使用基准框架来测试和评估代码。\n4.  将新优化的代码集成到程序中，并再次*测量*以查看当代码在更大的上下文中以更相关的输入运行时，优化是否相关。\n\n过程的四个步骤如下图所示:\n\n<figure class=\"mediaobject\">![](img/B15619_03_06.png)</figure>\n\n图 3.6:微基准标记过程\n\n微基准测试很有趣。但是，在深入尝试加快某个特定功能的过程之前，我们应该首先确保:\n\n*   运行程序时在函数内部花费的时间会显著影响我们想要加速的程序的整体性能。剖析和阿姆达尔定律将帮助我们理解这一点。阿姆达尔定律将在下面解释。\n*   我们不能轻易减少函数被调用的次数。消除对昂贵函数的调用通常是优化程序整体性能的最有效方法。\n\n使用微基准测试优化代码通常应该被视为最后的手段。预期的整体性能提升通常很小。然而，有时我们无法避免这样一个事实，即我们需要通过调整一小段代码的实现来使其运行得更快，在这种情况下，微基准测试可能非常有效。\n\n接下来，您将了解微基准测试的加速如何影响程序的整体加速。\n\n## 阿姆达尔定律\n\n使用微基准时，一定要记住隔离代码的优化对整个程序的影响有多大(或多小)。我们的经验是，在改进微基准时，有时很容易有点过于兴奋，只是意识到整体效果几乎可以忽略不计。这种无处可去的风险部分是通过使用合理的分析技术来解决的，但也要记住优化的整体影响。\n\n假设我们正在微基准中优化程序的一个孤立部分。然后可以使用阿姆达尔定律计算整个程序的整体加速上限。为了计算整体加速比，我们需要知道两个值:\n\n*   首先，我们需要知道孤立部分占总执行时间的比例是多少。我们用字母 *p* 表示*比例执行*时间的值。\n*   其次，我们需要知道我们正在优化的部分的加速——也就是微基准。我们用字母 *s* 表示*本地加速*的这个值。\n\n使用 *p* 和 *s* ，我们现在可以使用阿姆达尔定律来计算整体加速比:\n\n<figure class=\"mediaobject\">![](img/B15619_03_012.png)</figure>\n\n希望这看起来不要太复杂，因为投入使用后非常直观。为了获得对阿姆达尔定律的直觉，您可以看到当使用 *p* 和 *s* 的各种极值时，整体加速变得如何:\n\n*   设置*p = 0**s = 5x*意味着我们优化的零件对整体执行时间没有影响。因此，无论*的*值多少，整体加速永远是 1x。\n*   设置 *p = 1* ， *s = 5x* 意味着我们优化了一个占程序整个执行时间的部分，在这种情况下，整体加速将始终等于我们在优化部分实现的加速——在这种情况下为 5x。\n*   设置 *p = 0.5* 和*s =∩*意味着我们完全去掉了程序中占一半执行时间的部分。整体加速将是 2 倍。\n\n结果总结如下表:\n\n<colgroup><col> <col> <col></colgroup> \n| p | s | 整体加速 |\n| Zero | 5x | 1x |\n| one | 5x | 5x |\n| Zero point five | ∞ | 2x |\n\n表 3.4:p 和 s 的极值以及实现的整体加速\n\n一个完整的例子将演示我们如何在实践中使用阿姆达尔定律。假设你正在优化一个功能，使优化后的版本比原版本快 2 倍，即 *2x (s = 2)* 的加速比。此外，让我们假设这个函数只负责一个程序总执行时间的 1%(*p = 0.01*)，那么整个程序的总加速可以计算如下:\n\n<figure class=\"mediaobject\">![](img/B15619_03_013.png)</figure>\n\n因此，即使我们设法将孤立代码的速度提高了 2 倍，整体加速也只有 1.005 倍——并不是说这种加速必然可以忽略不计，而是我们需要不断回头，根据更大的图景来审视我们的收益。\n\n## 微基准测试的陷阱\n\n一般来说，在度量软件性能，尤其是微基准测试时，有很多隐藏的困难。在这里，我将列出使用微基准时需要注意的事项:\n\n*   结果有时过于一般化，被视为普遍真理。\n*   编译器优化独立代码的方式可能与优化完整程序的方式不同。例如，一个函数可能在微基准中内联，但在完整程序中编译时不会内联。或者，编译器可能能够预计算微基准的部分。\n*   基准测试中未使用的返回值可能会使编译器移除我们试图测量的函数。\n*   微基准中提供的静态测试数据可能会在优化代码时给编译器带来不切实际的优势。例如，如果我们硬编码一个循环将被执行多少次，并且编译器知道这个硬编码的值恰好是 8 的倍数，它可以不同地向量化循环，跳过可能与 SIMD 寄存器大小不一致的部分的序言和结尾。然后在真正的代码中，这个硬编码的编译时常数被一个运行时值替换，优化就不会发生。\n*   运行基准测试时，不切实际的测试数据会对分支预测产生影响。\n*   由于频率缩放、缓存污染和其他进程的调度等因素，多个测量之间的结果可能会有所不同。\n*   代码性能的限制因素可能是由于缓存未命中，而不是执行指令实际花费的时间。因此，在许多场景中，微基准测试的一个重要规则是，在进行测量之前，您必须对缓存进行彻底的研究，否则您就没有真正测量任何东西。\n\n我希望我有一个简单的公式来避免上面列出的所有陷阱，但不幸的是，我没有。然而，在下一节中，我们将看一个具体的例子，看看如何通过使用微基准标记支持库来解决这些缺陷。\n\n## 微基准示例\n\n我们将通过从这一章回到线性搜索和二分搜索法的初始例子来结束这一章，并演示如何使用基准框架对它们进行基准测试。\n\n本章开始时，我们比较了两种在`std::vector`中搜索整数的方法。如果我们知道向量已经排序，我们可以使用二分搜索法，它优于简单的线性搜索算法。我在这里不再重复函数的定义，但是声明是这样的:\n\n```cpp\nbool linear_search(const std::vector<int>& v, int key);\nbool binary_search(const std::vector<int>& v, int key); \n```\n\n一旦输入足够大，这些函数的执行时间差异就非常明显，但对于我们的目的来说，这将是一个足够好的例子。我们将从测量`linear_search()`开始。然后，当我们有了一个工作基准，我们将添加`binary_search()`并比较两个版本。\n\n为了制作一个测试程序，我们首先需要一种方法来生成一个排序的整数向量。如下所示，一个简单的实现就足以满足我们的需求:\n\n```cpp\nauto gen_vec(int n) {\n  std::vector<int> v;\n  for (int i = 0; i < n; ++ i) { \n    v.push_back(i); \n  }\n  return v;\n} \n```\n\n返回的向量将包含 0 到 *n - 1* 之间的所有整数。一旦我们做好了这些，我们就可以创建这样一个简单的测试程序:\n\n```cpp\nint main() { // Don't do performance tests like this!\n  ScopedTimer timer(\"linear_search\");\n  int n = 1024;\n  auto v = gen_vec(n);\n  linear_search(v, n);\n} \n```\n\n我们正在搜索值`n`，我们知道它不在向量中，所以算法将使用这个测试数据展示它的最坏情况性能。这就是这次测试的精彩之处。除此之外，它还受到许多缺陷的困扰，这些缺陷将使该基准变得无用:\n\n*   使用优化编译这段代码很可能会完全删除代码，因为编译器可以看到函数的结果没有被使用。\n*   我们不想测量创建和填充`std::vector`所需的时间。\n*   只运行一次`linear_search()`函数，我们不会得到统计上稳定的结果。\n*   测试不同的输入大小很麻烦。\n\n让我们看看如何通过使用微基准标记支持库来解决这些问题。对标的工具/库有很多种，但我们会用 **Google Benchmark** 、[https://github.com/google/benchmark](https://github.com/google/benchmark)，因为它的使用非常广泛，作为加分项，也可以在[http://quick-bench.com](http://quick-bench.com)页面轻松在线测试，不需要任何安装。\n\n以下是使用谷歌基准测试时`linear_search()`的简单微基准测试结果:\n\n```cpp\n#include <benchmark/benchmark.h> // Non-standard header\n#include <vector>\nbool linear_search(const std::vector<int>& v, int key) { /* ... */ }\nauto gen_vec(int n) { /* ... */ }\nstatic void bm_linear_search(benchmark::State& state) {\n  auto n = 1024;\n  auto v = gen_vec(n);\n  for (auto _ : state) {\n    benchmark::DoNotOptimize(linear_search(v, n));\n  }\n}\nBENCHMARK(bm_linear_search); // Register benchmarking function\nBENCHMARK_MAIN(); \n```\n\n就这样！我们唯一没有解决的问题是输入大小被硬编码为 1024。我们会尽快解决的。编译和运行这个程序将生成如下内容:\n\n```cpp\n-------------------------------------------------------------------\nBenchmark                Time   CPU           Iterations\n-------------------------------------------------------------------\nbm_linear_search         361 ns 361 ns        1945664 \n```\n\n最右边一列中报告的迭代次数报告了在获得统计稳定结果之前需要执行的循环次数。传递给我们的基准函数的`state`对象决定何时停止。每次迭代的平均时间分为两栏:**时间**是挂钟时间，**中央处理器**是主线程在中央处理器上花费的时间。在这种情况下，它们是相同的，但是如果`linear_search()`被阻塞等待输入/输出(例如)，中央处理器时间将低于挂钟时间。\n\n另一个需要注意的重要事项是，生成向量的代码不包含在报告的时间中。唯一被测量的代码是这个循环中的代码:\n\n```cpp\nfor (auto _ : state) {   // Only this loop is measured\n  benchmark::DoNotOptimize(binary_search(v, n));\n} \n```\n\n从我们的搜索函数返回的布尔值被包装在`benchmark::DoNotOptimize()`中。这是用于确保返回值不会被优化掉的机制，这可能会使对`linear_search()`的整个调用消失。\n\n现在，让我们通过改变输入大小来让这个基准测试更有趣一点。我们可以通过使用`state`对象将参数传递给我们的基准函数来实现这一点。下面是如何做到的:\n\n```cpp\nstatic void bm_linear_search(benchmark::State& state) {\n  auto n = state.range(0);\n  auto v = gen_vec(n);\n  for (auto _ : state) {\n    benchmark::DoNotOptimize(linear_search(v, n));\n  }\n}\nBENCHMARK(bm_linear_search)->RangeMultiplier(2)->Range(64, 256); \n```\n\n这将从开始，输入大小为 64，然后加倍，直到达到 256。在我的机器上，测试生成了以下输出:\n\n```cpp\n-------------------------------------------------------------------\nBenchmark                Time    CPU          Iterations\n-------------------------------------------------------------------\nbm_linear_search/64      17.9 ns 17.9 ns      38143169\nbm_linear_search/128     44.3 ns 44.2 ns      15521161\nbm_linear_search/256     74.8 ns 74.7 ns      8836955 \n```\n\n作为最后一个例子，我们将使用可变的输入大小对`linear_search()`和`binary_search()`函数进行基准测试，并尝试让框架估计我们函数的时间复杂度。这可以通过使用`SetComplexityN()`功能向`state`对象提供输入尺寸来实现。完整的微基准示例如下所示:\n\n```cpp\n#include <benchmark/benchmark.h>\n#include <vector>\nbool linear_search(const std::vector<int>& v, int key) { /* ... */ }\nbool binary_search(const std::vector<int>& v, int key) { /* ... */ }\nauto gen_vec(int n) { /* ... */ }\nstatic void bm_linear_search(benchmark::State& state) {\n  auto n = state.range(0); \n  auto v = gen_vec(n);\n  for (auto _ : state) { \n    benchmark::DoNotOptimize(linear_search(v, n)); \n  }\n  state.SetComplexityN(n);\n}\nstatic void bm_binary_search(benchmark::State& state) {\n  auto n = state.range(0); \n  auto v = gen_vec(n);\n  for (auto _ : state) { \n    benchmark::DoNotOptimize(binary_search(v, n)); \n  }\n  state.SetComplexityN(n);\n}\nBENCHMARK(bm_linear_search)->RangeMultiplier(2)->\n  Range(64, 4096)->Complexity();\nBENCHMARK(bm_binary_search)->RangeMultiplier(2)->\n  Range(64, 4096)->Complexity();\nBENCHMARK_MAIN(); \n```\n\n运行基准时，我们会将以下结果打印到控制台:\n\n```cpp\n-------------------------------------------------------------------\nBenchmark                Time     CPU         Iterations\n-------------------------------------------------------------------\nbm_linear_search/64      18.0 ns  18.0 ns     38984922\nbm_linear_search/128     45.8 ns  45.8 ns     15383123\n...\nbm_linear_search/8192    1988 ns  1982 ns     331870\nbm_linear_search_BigO    0.24 N   0.24 N\nbm_linear_search_RMS        4 %   4 %\nbm_binary_search/64      4.16 ns  4.15 ns     169294398\nbm_binary_search/128     4.52 ns  4.52 ns     152284319\n...\nbm_binary_search/4096    8.27 ns  8.26 ns     80634189\nbm_binary_search/8192    8.90 ns  8.90 ns     77544824\nbm_binary_search_BigO    0.67 lgN 0.67 lgN\nbm_binary_search_RMS        3 %   3 % \n```\n\n输出与我们在本章中的初始结果一致，我们得出结论，算法分别表现出线性运行时间和对数运行时间。如果我们在表格中绘制这些值，我们可以清楚地看到函数的线性和对数增长率。\n\n下图是使用 Python 和 Matplotlib 生成的:\n\n<figure class=\"mediaobject\">![](img/B15619_03_07.png)</figure>\n\n图 3.7:绘制不同输入大小的执行时间揭示了搜索函数的增长率\n\n您现在有很多工具和见解来发现和提高代码的性能。我再怎么强调衡量和设定目标对工作绩效的重要性也不为过。引用安德烈·亚历山德雷斯库的话来结束这一节:\n\n> “测量让你比不需要测量的专家更有优势。”\n\n> -安德烈·亚历山德雷斯库，2015 年，编写快速代码一，代码::潜水会议，2015 年，https://codedive.pl/2015/writing-fast-code-part-1.\n\n# 摘要\n\n在本章中，您学习了如何使用大 O 符号来比较算法的效率。您现在知道 C++ 标准库为算法和数据结构提供了复杂性保证。所有的标准库算法都指定了它们的最坏情况或平均情况的性能保证，而容器和迭代器则指定了分摊的或精确的复杂性。\n\n您还发现了如何通过测量延迟和吞吐量来量化软件性能。\n\n最后，您学习了如何通过使用 CPU 剖析器来检测代码中的热点，以及如何执行微基准测试来改进程序中的孤立部分。\n\n在下一章中，您将了解如何有效地使用 C++ 标准库提供的数据结构。"
  },
  {
    "path": "docs/cpp-hiperf/04.md",
    "content": "# 四、数据结构\n\n在最后一章中，我们讨论了如何分析时间和内存复杂性以及如何衡量性能。在本章中，我们将讨论如何从标准库中选择和使用数据结构。为了理解为什么某些数据结构在今天的计算机上工作得非常好，我们首先需要介绍一些关于计算机内存的基础知识。在本章中，您将了解:\n\n*   计算机内存的特性\n*   标准库容器:序列容器和关联容器\n*   标准库容器适配器\n*   并行阵列\n\n在我们开始浏览标准库提供的容器和一些其他有用的数据结构之前，我们将简要讨论计算机内存的一些属性。\n\n# 计算机内存的特性\n\nC++ 将内存视为一系列单元。每个单元的大小是 1 字节，每个单元都有一个地址。通过地址访问内存中的一个字节是一个恒定时间的操作，*0(1)*，换句话说，它与内存单元的总数无关。在 32 位机器上，理论上可以寻址 2 <sup class=\"Superscript--PACKT-\">32</sup> 字节，也就是 4 GB 左右，这限制了一个进程一次可以使用的内存量。在 64 位机器上，理论上可以寻址 2 <sup class=\"Superscript--PACKT-\">64</sup> 字节，这个字节太大了，几乎没有地址用完的风险。\n\n下图显示了在内存中布局的存储单元序列。每个单元包含 8 位。十六进制数是存储单元的地址:\n\n<figure class=\"mediaobject\">![](img/B15619_04_01.png)</figure>\n\n图 4.1:存储单元序列\n\n由于通过地址访问一个字节是一个 *O(1)* 操作，从程序员的角度来看，很容易相信每个存储单元的访问速度都一样快。这种内存方法在很多情况下都很简单，也很有用，但是在选择有效使用的数据结构时，您需要考虑现代计算机中存在的内存层次结构。内存层次结构的重要性增加了，因为与当今处理器的速度相比，从主内存中读取和写入所需的时间变得更加昂贵。下图显示了具有一个中央处理器和四个内核的机器的体系结构:\n\n<figure class=\"mediaobject\">![](img/B15619_04_02.png)</figure>\n\n图 4.2:具有四个内核的处理器示例；标记为 L1i、L1d、L2 和 L3 的框是内存缓存\n\n我目前正在 2018 年的 MacBook Pro 上写这一章，它配备了英特尔四核 i7 CPU。在这款处理器上，每个内核都有自己的 L1 和 L2 缓存，而三级缓存由所有四个内核共享。从终端运行以下命令:\n\n```cpp\nsysctl -a hw \n```\n\n给了我以下信息:\n\n```cpp\nhw.memsize: 17179869184\nhw.cachelinesize: 64\nhw.l1icachesize: 32768\nhw.l1dcachesize: 32768\nhw.l2cachesize: 262144\nhw.l3cachesize: 8388608 \n```\n\n上报的`hw.memsize`是主存总量，本例为 16 GB。\n\n向报告的`hw.cachelinesize`是 64 字节，是高速缓存行的大小，也称为块。访问内存中的一个字节时，机器不仅仅是获取被请求的字节；相反，机器总是获取一个高速缓存行，在这种情况下，是 64 字节。中央处理器和主存储器之间的各种高速缓存跟踪 64 字节的块，而不是单个字节。\n\n`hw.l1icachesize`是 L1 指令缓存的大小。这是一个 32 KB 的高速缓存，专用于存储中央处理器最近使用过的指令。`hw.l1dcachesize`也是 32 KB，专用于数据，而不是指令。\n\n最后，我们可以读取 L2 缓存和三级缓存的大小，分别为 256 KB 和 8 MB。一个重要的观察是，与可用的主内存量相比，缓存很小。\n\n在不提供从缓存层次结构中的每一层访问数据所需的实际周期数的任何详细事实的情况下，一个非常粗略的准则是两个相邻层(例如，L1 和 L2)之间的延迟存在数量级的差异。下表显示了彼得·诺维格在一篇名为《十年内自学编程，2001 年》([http://norvig.com/21-days.html](http://norvig.com/21-days.html))的文章中提供的延迟数字摘录。这个完整的表通常被称为*延迟数，每个程序员都应该知道*，并归功于杰夫·迪恩:\n\n<colgroup><col> <col></colgroup> \n| L1 缓存引用 | 0.5 ns |\n| L2 缓存引用 | 7 ns |\n| 主存储器参考 | 100 ns |\n\n以充分利用高速缓存的方式构建数据会对性能产生巨大影响。访问最近使用过的数据，因此可能已经驻留在缓存中，这将使您的程序更快。这就是所谓的**时间地点**。\n\n此外，访问位于您正在使用的其他数据附近的数据将增加您需要的数据已经在先前从主存储器中提取的高速缓存行中的可能性。这就是所谓的**空间位置**。\n\n不断清除内部循环中的缓存行可能会导致非常糟糕的性能。这就是有时被称为**缓存抖动**。让我们看一个例子:\n\n```cpp\nconstexpr auto kL1CacheCapacity = 32768; // The L1 Data cache size \nconstexpr auto kSize = kL1CacheCapacity / sizeof(int); \nusing MatrixType = std::array<std::array<int, kSize>, kSize>; \nauto cache_thrashing(MatrixType& matrix) { \n  auto counter = 0;\n  for (auto i = 0; i < kSize; ++ i) {\n    for (auto j = 0; j < kSize; ++ j) {\n      matrix[i][j] = counter++ ;\n    }\n  }\n} \n```\n\n这个版本在我的电脑上运行大约需要 40 毫秒。但是，只需将内部循环中的线路更改为以下内容，完成该功能所需的时间就会从 40 毫秒增加到 800 毫秒以上:\n\n```cpp\nmatrix[j][i] = counter++ ; \n```\n\n在第一个例子中，当使用`matrix[i][j]`时，大部分时间我们将访问已经在 L1 缓存中的内存，而在使用`matrix[j][i]`的修改版本中，每次访问都将生成一个 L1 缓存未命中。几张图片可能有助于你理解正在发生的事情。这里显示的是一个微小的 3 x 3 矩阵，而不是绘制完整的 32768 x 32768 矩阵。\n\n<figure class=\"mediaobject\">![](img/B15619_04_03.png)</figure>\n\n图 4.3:3x 3 矩阵\n\n即使这可能是我们对矩阵如何驻留在记忆中的想象，也没有二维记忆这种东西。相反，当这个矩阵被布置在一维存储空间中时，它看起来像这样:\n\n<figure class=\"mediaobject\">![](img/B15619_04_04.png)</figure>\n\n图 4.4:一维记忆空间中的二维矩阵\n\n也就是说，它是一个连续的元素数组，一行一行地排列。在我们算法的快速版本中，数字按照它们在内存中连续排列的相同顺序依次访问，如下所示:\n\n<figure class=\"mediaobject\">![](img/B15619_04_05.png)</figure>\n\n图 4.5:快速顺序跨步-1 访问\n\n而在算法的慢版本中，以完全不同的模式访问元素。使用慢速版本访问前四个元素现在看起来是这样的:\n\n<figure class=\"mediaobject\">![](img/B15619_04_06.png)</figure>\n\n图 4.6:使用较大步幅的慢速访问\n\n由于空间局部性差，以这种方式访问数据要慢得多。现代处理器通常还配备有**预取器**，它可以自动识别内存访问模式，并尝试将数据从内存预取到可能在不久的将来被访问的缓存中。对于较小的步幅，预取员往往表现最好。你可以在兰道尔·e·布莱恩特和大卫·r·O·哈拉伦的优秀著作《计算机系统，程序员的视角》中读到更多关于这方面的内容。\n\n总结这一节，即使内存访问是恒定时间操作，缓存也会对访问内存的实际时间产生显著影响。在使用或实现新的数据结构时，这一点要时刻牢记在心。\n\n接下来，我将介绍一组来自 C++ 标准库的数据结构，称为容器。\n\n# 标准库容器\n\nC++ 标准库提供了一组非常有用的容器类型。容器是包含元素集合的数据结构。容器管理它保存的元素的内存。这意味着我们不必显式地创建和删除放在容器中的对象。我们可以将在堆栈上创建的对象传递给一个容器，该容器会将它们复制并存储在自由存储中。\n\n迭代器用于访问容器中的元素，因此是理解标准库中算法和数据结构的基本概念。迭代器的概念包含在*第 5 章*、*算法*中。对于本章来说，知道迭代器可以被认为是指向元素的指针就足够了，迭代器根据它们所属的容器定义了不同的运算符。例如，类似数组的数据结构为其元素提供随机访问迭代器。这些迭代器支持使用`+`和`-`的算术表达式，而例如链表的迭代器只支持`++ `和`--`运算符。\n\n容器分为三类:序列容器、关联容器和容器适配器。本节将简要介绍这三类容器中的每一类，并阐述当性能成为问题时需要考虑的最重要的事情。\n\n## 序列容器\n\n序列容器将元素按照我们在向容器添加元素时指定的顺序保存。标准库中的序列容器有`std::array`、`std::vector`、`std::deque`、`std::list`和`std::forward_list`。我也将在这一节讨论`std::basic_string`，虽然它不是一个正式的通用序列容器，因为它只处理字符类型的元素。\n\n在选择序列容器之前，我们应该知道以下问题的答案:\n\n1.  元素的数量是多少(数量级)？\n2.  有哪些使用模式？您多久添加一次数据？读取/遍历数据？删除数据？重新排列数据？\n3.  在序列中，您最常添加数据的位置是哪里？在结尾，在开头，还是在序列的中间？\n4.  需要对元素进行排序吗？或者你在乎订单吗？\n\n根据这些问题的答案，我们可以确定哪些序列容器或多或少适合我们的需求。但是，要做到这一点，我们需要对每种序列容器的接口和性能特征有一个基本的了解。\n\n接下来的章节将简要介绍不同的序列容器，首先介绍一个最广泛使用的容器。\n\n### 向量和数组\n\n`std::vector`大概是最常用的容器类型，而且理由很充分。向量是一个数组在需要时动态增长。添加到向量中的元素保证在内存中连续布局，这意味着您可以通过数组的索引在恒定时间内访问数组中的任何元素。这也意味着，由于前面提到的空间局部性，它在按照元素的布局顺序遍历元素时提供了出色的性能。\n\n一个向量有一个**大小**和一个**容量**。大小是容器中当前容纳的元素数量，容量是向量在需要分配更多空间之前可以容纳的元素数量:\n\n<figure class=\"mediaobject\">![](img/B15619_04_07.png)</figure>\n\n图 4.7:标准向量的大小和容量\n\n使用`push_back()`功能将元素添加到向量的末尾是快速的，只要大小小于容量即可。当添加一个元素并且没有更多空间时，向量将分配一个新的内部缓冲区，然后将所有元素移动到新的空间。正如我们在*第三章*、*分析和测量性能*中所讨论的那样，容量将以很少发生调整缓冲区大小的方式增长，从而使`push_back()`成为摊销的恒定时间操作。\n\n类型为`std::vector<Person>`的矢量模板实例将按值存储`Person`对象。当向量需要重新排列`Person`对象时(例如，作为插入的结果)，值将被复制构造或移动。如果对象具有`nothrow`移动构造函数，它们将被移动。否则，这些对象将被复制构造，以保证强异常安全:\n\n```cpp\nPerson(Person&& other) {         // Will be copied \n   // ...\n} \nPerson(Person&& other) noexcept { // Will be moved \n   // ...\n} \n```\n\n在内部，`std::vector`使用`std::move_if_noexcept`来确定对象是应该复制还是移动。`<type_traits>`头可以帮助您在编译时验证您的类在移动时保证不会抛出异常:\n\n```cpp\nstatic_assert(std::is_nothrow_move_constructible<Person>::value); \n```\n\n如果您要将新创建的对象添加到矢量中，您可以利用`emplace_back()`功能，该功能将为您就地创建对象，而不是创建一个对象，然后使用`push_back()`功能将其复制/移动到矢量中:\n\n```cpp\npersons.emplace_back(\"John\", 65); \n```\n\n向量的容量可以通过以下方式改变:\n\n*   通过在`capacity == size`时向向量添加一个元素\n*   通过呼叫`reserve()`\n*   通过呼叫`shrink_to_fit()`\n\n除此之外，向量不会改变容量，因此不会分配或解除分配动态内存。例如，成员函数`clear()`清空一个向量，但不改变其容量。这些内存保证使得向量即使在实时环境中也是可用的。\n\n从 C++ 20 开始，还有两个自由函数可以从`std::vector`中删除元素。在 C++ 20 之前，我们必须使用*擦除-移除成语*，我们将在*第 5 章*、*算法*中讨论。然而，现在推荐的从`std::vector`中删除元素的方法是使用`std::erase()`和`std::erase_if()`。下面是如何使用这些功能的一个简短示例:\n\n```cpp\nauto v = std::vector{-1, 5, 2, -3, 4, -5, 5};\nstd::erase(v, 5);                               // v: [-1,2,-3,4,-5]\nstd::erase_if(v, [](auto x) { return x < 0; }); // v: [2, 4] \n```\n\n作为动态大小向量的替代，标准库还提供了一个名为`std::array`的固定大小版本，该版本通过使用堆栈而不是自由存储来管理其元素。数组的大小是在编译时指定的模板参数，这意味着大小和类型元素成为具体类型的一部分:\n\n```cpp\nauto a = std::array<int, 16>{};\nauto b = std::array<int, 1024>{}; \n```\n\n在本例中，`a`和`b`不是同一个类型，这意味着当使用类型作为函数参数时，您必须指定大小:\n\n```cpp\nauto f(const std::array<int, 1024>& input) { \n  // ... \n} \n\nf(a);  // Does not compile, f requires an int array of size 1024 \n```\n\n这一开始看起来有点乏味，但这实际上是内置数组类型(C 数组)的最大优势，它在传递给函数时会丢失大小信息，因为它会自动将指针转换为数组的第一个元素:\n\n```cpp\n// input looks like an array, but is in fact a pointer \nauto f(const int input[]) {  \n  // ... \n} \n\nint a[16]; \nint b[1024]; \nf(a); // Compiles, but unsafe \n```\n\n失去大小信息的阵列通常被称为**阵列衰减**。稍后，您将在一章中看到如何通过使用`std::span`将连续数据传递给函数来避免数组衰减。\n\n### 双端队列\n\n有时候，你会发现自己处于的情况，你需要频繁地在一个序列的开头和结尾添加元素。如果你使用的是`std::vector`并且需要加速前面的插页，你可以改为使用`std::deque`，这是**双头队列**的缩写。`std::deque`通常被实现为固定大小数组的集合，这使得在恒定时间内通过元素的索引来访问元素成为可能。但是，如下图所示，并非所有元素都连续存储在内存中，这就是`std::vector`和`std::array`的情况。\n\n<figure class=\"mediaobject\">![](img/B15619_04_08.png)</figure>\n\n图 4.8:标准的可能布局\n\n### 列表和转发列表\n\n`std::list`是一个**双向链表**，这意味着每个元素都有一个到下一个元素的链接和一个到前一个元素的链接。这使得向后和向前遍历列表成为可能。还有一个**单链表**名为`std::forward_list`。你不会总是选择双向链表而不是`std::forward_list`的原因是双向链表中的反向指针占用了过多的内存。所以，如果不需要向后遍历列表，使用`std::forward_list`。转发列表的另一个有趣的特性是，它针对非常短的列表进行了优化。当列表为空时，它只占用一个单词，这使得它成为稀疏数据的可行数据结构。\n\n请注意，即使元素是按顺序排列的，它们也是*而不是*像向量和数组一样在内存中连续排列，这意味着与向量相比，迭代链表很可能会产生更多的缓存未命中。\n\n简单回顾一下，`std::list`是一个双向链表，带有指向下一个和上一个元素的指针:\n\n<figure class=\"mediaobject\">![](img/B15619_04_09.png)</figure>\n\n图 4.9: std::list 是一个双向链表\n\n`std::forward_list`是一个单链表，带有指向下一个元素的指针:\n\n<figure class=\"mediaobject\">![](img/B15619_04_10.png)</figure>\n\n图 4.10: std::forward_list 是一个单链表\n\n`std::forward_list`的内存效率更高，因为它只有一个指向下一个元素的指针。\n\n列表也是唯一支持**拼接**的容器，这是一种在列表之间传输元素的方式，无需复制或移动元素。这意味着，例如，可以在恒定时间内将两个列表连接成一个，*0(1)*。其他容器至少需要线性时间进行这种操作。\n\n### 基本字符串\n\n我们将在本节中介绍的最后一个模板类是`std::basic_string`。`std::string`是`std::basic_string<char>`的`typedef`。从历史上看，`std::basic_string`并不能保证在记忆中连续布局。C++ 17 改变了这一点，它使得将字符串传递给需要字符数组的 API 成为可能。例如，下面的代码将整个文件读入一个字符串:\n\n```cpp\nauto in = std::ifstream{\"file.txt\", std::ios::binary | std::ios::ate}; \nif (in.is_open()) { \n  auto size = in.tellg(); \n  auto content = std::string(size, '\\0'); \n  in.seekg(0); \n  in.read(&content[0], size); \n  // \"content\" now contains the entire file \n} \n```\n\n通过使用`std::ios::ate`打开文件，位置指示器被设置到流的末尾，这样我们就可以使用`tellg()`来检索文件的大小。之后，我们将输入位置设置到流的开头并开始读取。\n\n`std::basic_string`的大多数实现都利用了一种叫做**小对象优化**的东西，这意味着如果字符串的大小很小，它们不会分配任何动态内存。我们将在本书后面讨论小对象优化。现在，让我们继续讨论关联容器。\n\n## 关联容器\n\n关联容器基于元素本身放置它们的元素。例如，不可能像我们使用`std::vector::push_back()`或`std::list::push_front()`那样，在关联容器的后面或前面添加元素。相反，添加元素的方式使得无需扫描整个容器就可以找到元素。因此，关联容器对我们想要存储在容器中的对象有一些要求。我们将在稍后查看这些要求。\n\n关联容器有两个主要类别:\n\n*   **有序关联容器**:这些容器基于树；容器使用一棵树来存储它们的元素。它们要求元素由小于运算符(`<`)排序。在基于树的容器中，添加、删除和查找元素的功能都是 O(log n)。容器命名为`std::set`、`std::map`、`std::multiset`和`std::multimap`。\n*   **无序关联容器**:这些容器基于哈希表；容器使用散列表来存储它们的元素。它们要求将元素与等式运算符(`==`)进行比较，并且有一种基于元素计算哈希值的方法。稍后会有更多。添加、删除和查找元素的功能都是基于哈希表的容器中的 *O(1)* 。容器命名为`std::unordered_set`、`std::unordered_map`、`std::unordered_multiset`和`std::unordered_multimap`。\n\n从 C++ 20 开始，所有的关联容器都配备了一个名为`contains()`的函数，当你想知道一个容器是否包含一些特定的元素时，应该使用这个函数。在 C++ 的早期版本中，需要使用`count()`或`find()`来找出容器是否包含元素。\n\n始终使用专门的功能，如`contains()`、`empty()`，而不是使用`count() > 0`或`size() == 0`。专门的功能保证是最有效的。\n\n### 有序集和映射\n\n有序关联容器保证插入、删除和搜索可以在对数时间内完成，*0(对数 n)* 。如何实现这一点取决于标准库的实现。然而，我们所知道的实现确实使用了某种自平衡二叉查找树。树保持近似平衡的事实对于控制树的高度是必要的，因此也是访问元素时最坏情况下的运行时间。树不需要预先分配内存，因此，通常情况下，每次插入元素时，树都会在空闲存储上分配内存，并且每当元素被擦除时，树也会释放内存。看下图，平衡树的高度为 *O(log n)* :\n\n<figure class=\"mediaobject\">![](img/B15619_04_11.png)</figure>\n\n图 4.11:如果平衡，树的高度为 0(对数 n)\n\n### 无序集和映射\n\n集合和映射的无序版本为基于树的版本提供了基于散列的替代版本。这种数据结构通常被称为哈希表。理论上，散列表提供摊销的常数时间插入、添加和删除操作，这可以与在 *O(log n)* 中运行的基于树的版本相比较。然而，在实践中，这种差异可能并不那么明显，尤其是在容器中没有存储大量元素的情况下。\n\n让我们看看哈希表如何提供 *O(1)* 操作。散列表将其元素保存在某种桶数组中。向哈希表中添加元素时，将使用哈希函数为元素计算一个整数。整数通常被称为元素的**散列**。哈希值然后被限制到数组的大小(例如，通过使用模运算)，以便新的限制值可以用作数组中的索引。一旦计算出索引，哈希表就可以将元素存储在该索引处的数组中。元素的查找以类似的方式工作，首先计算我们正在寻找的元素的哈希值，然后访问数组。\n\n除了计算哈希值之外，这种技术似乎很简单。不过，这只是故事的一半。如果两个不同的元素生成相同的索引，要么是因为它们生成了相同的哈希值，要么是因为两个不同的哈希值被限制在相同的索引中，该怎么办？当两个不相等的元素在同一个索引处结束时，我们称之为**哈希冲突**。这不仅仅是边缘情况:这种情况会经常发生，即使我们使用的是一个很好的哈希函数，尤其是当数组与我们添加的元素数量相比很小时。有多种方法来处理哈希冲突。在这里，我们将集中在标准库中正在使用的一个，它被称为**单独链接**。\n\n单独链接解决了两个不相等的元素在同一个索引处结束的问题。数组不是直接将元素存储在数组中，而是一系列**桶**。每个存储桶可以包含多个元素，也就是说，所有元素都被散列到同一个索引。所以，每个桶也是某种容器。用于存储桶的确切数据结构没有定义，不同的实现可能会有所不同。然而，我们可以把它看作一个链表，并假设在一个特定的桶中找到一个元素是缓慢的，因为它需要线性扫描桶中的元素。\n\n下图显示了一个包含八个桶的哈希表。元素落在三个不同的桶里。带索引的桶 **2** 包含四个元素，带索引的桶 **4** 包含两个元素，带索引的桶 **5** 只包含一个元素。其他桶是空的:\n\n<figure class=\"mediaobject\">![](img/B15619_04_12.png)</figure>\n\n图 4.12:每个桶包含 0 个或更多元素\n\n#### 散列和等于\n\n哈希值可以是相对于容器的大小在恒定时间内计算的，它决定了元素将被放置在哪个桶中。因为可能有多个对象会生成相同的哈希值，因此最终会在同一个桶中，所以每个键还需要提供一个 equals 函数，用于将我们正在寻找的键与桶中的所有键进行比较。\n\n如果两个密钥相等，则需要它们生成相同的哈希值。然而，两个对象返回相同的哈希值而彼此不相等是完全合法的。\n\n一个好的散列函数计算起来很快，并且还会在存储桶之间平均分配密钥，以便最小化每个存储桶中的元素数量。\n\n下面是一个非常糟糕但有效的散列函数的例子:\n\n```cpp\nauto my_hash = [](const Person& person) {\n  return 47; // Bad, don't do this!\n}; \n```\n\n它是有效的，因为它将为两个相等的对象返回相同的哈希值。散列函数也非常快。但是，由于所有元素都将产生相同的哈希值，因此所有键都将在同一个桶中结束，这意味着找到一个元素将是 *O(n)* ，而不是我们要寻找的 *O(1)* 。\n\n另一方面，好的散列函数确保元素在桶中均匀分布，以最小化散列冲突。C++ 标准实际上对此有一个注释，声明散列函数为两个不同的对象产生相同的散列值应该是非常罕见的。幸运的是，标准库已经为我们提供了良好的基本类型散列函数。在许多情况下，我们可以在为用户定义的类型编写自己的散列函数时重用这些函数。\n\n假设我们想在`unorordered_set`中使用`Person`类作为键。`Person`类有两个数据成员:`age`，是`int`和`name`，是`std::string`。我们从写相等谓词开始:\n\n```cpp\nauto person_eq = [](const Person& lhs, const Person& rhs) {\n  return lhs.name() == rhs.name() && lhs.age() == rhs.age();\n}; \n```\n\n两个`Person`对象要相等，需要同名同岁。我们现在可以通过组合 equals 谓词中包含的所有数据成员的散列值来定义散列谓词。不幸的是，在 C++ 标准中还没有函数来组合哈希值，但是在 Boost 中有一个很好的函数，我们将在这里使用它:\n\n```cpp\n#include <boost/functional/hash.hpp>\nauto person_hash = [](const Person& person) { \n  auto seed = size_t{0};\n  boost::hash_combine(seed, person.name()); \n  boost::hash_combine(seed, person.age()); \n  return seed;\n}; \n```\n\n如果因为某种原因，你不能使用 Boost，`boost::hash_combine()`真的只是一个单行，可以从[https://www . Boost . org/doc/libs/1 _ 55 _ 0/doc/html/hash/reference . html # Boost . hash _ combine](https://www.boost.org/doc/libs/1_55_0/doc/html/hash/reference.html#boost.hash_combine)找到的文档中复制。\n\n定义了等式和散列函数后，我们最终可以创建我们的`unordered_set`:\n\n```cpp\nusing Set = std::unordered_set<Person, decltype(person_hash),                                decltype(person_eq)>; \nauto persons = Set{100, person_hash, person_eq}; \n```\n\n一个很好的经验法则是，在生成哈希值时，始终使用 equal 函数中使用的所有数据成员。这样，我们遵守了 equals 和 hash 之间的约定，同时，这使我们能够提供有效的 hash 值。例如，在计算哈希值时只使用名称是正确但低效的，因为这将意味着所有具有相同名称的`Person`对象将结束在同一个桶中。然而，更糟糕的是在散列函数中包含 equals 函数中没有使用的数据成员。这很可能会导致一场灾难，在这场灾难中，你无法在你的`unordered_set`中找到事实上可以同等比较的物体。\n\n#### 哈希策略\n\n除了创建散列值在桶之间平均分配密钥之外，我们还可以通过拥有多个桶来减少冲突的数量。每个铲斗的平均元件数量称为**负载系数**。在前面的例子中，我们创建了一个有 100 个桶的`unordered_set`。如果我们向集合中添加 50 个`Person`对象，`load_factor()`将返回 0.5。`max_load_factor`是负载系数的上限，当达到该值时，该组将需要增加铲斗的数量，因此，还需要重新存储该组中当前的所有元素。也可以通过`rehash()`和`reserve()`成员功能手动触发重挂。\n\n让我们继续看第三类:容器适配器。\n\n## 集装箱适配器\n\n标准库中有三个容器适配器:`std::stack`、`std::` `queue`和`std::` `priority_queue`。容器适配器与序列容器和关联容器有很大不同，因为它们代表了**抽象数据类型**，可以由底层序列容器实现。例如，支持栈顶推弹出的**后进先出** ( **LIFO** )数据结构的栈，可以通过使用`vector`、`list`、`deque`或任何其他支持`back()`、`push_back()`和`pop_back()`的自定义序列容器来实现。`queue`也是如此，它是**先进先出** ( **先进先出**)数据结构，`priortiy_queue`。\n\n在本节中，我们将重点讨论`std::priority_queue`，这是一个非常有用的数据结构，很容易忘记。\n\n### 优先队列\n\n一个**优先级队列**提供一个具有最高优先级的元素的恒定时间查找。优先级是使用元素的小于运算符定义的。插入和删除都在对数时间内运行。优先级队列是一种部分排序的数据结构，当使用优先级队列而不是完全排序的数据结构(例如，树或排序向量)时，这可能并不明显。但是，在某些情况下，优先级队列可以为您提供所需的功能，并且比完全排序的容器成本更低。\n\n标准库已经提供了部分排序算法，所以我们不需要自己编写。但是让我们看看如何使用优先级队列实现部分排序算法。假设我们正在编写一个程序来搜索给定查询的文档。匹配的文档(搜索命中)应该按排名排序，我们只对排名最高的前 10 个搜索命中感兴趣。\n\n文档由以下类表示:\n\n```cpp\nclass Document { \npublic:  \n  Document(std::string title) : title_{std::move(title)} {}\nprivate:  \n  std::string title_; \n  // ... \n}; \n```\n\n搜索时，算法选择与查询匹配的文档，并计算搜索命中的排名。每个匹配的文档由一个`Hit`表示:\n\n```cpp\nstruct Hit { \n  float rank_{}; \n  std::shared_ptr<Document> document_; \n}; \n```\n\n最后，我们需要对点击进行排序，并返回顶部的 *m* 文档。点击排序有哪些选项？如果命中包含在提供随机访问迭代器的容器中，我们可以使用`std::sort()`并且只返回 *m* 第一个元素。或者，如果点击总数比我们要返回的 *m* 文档大得多，我们可以使用`std::partial_sort()`，这将比`std::sort()`更有效。\n\n但是如果我们没有随机访问迭代器呢？也许匹配算法只提供命中的前向迭代器。在这种情况下，我们可以使用一个优先级队列，并仍然提出一个有效的解决方案。我们的排序界面如下所示:\n\n```cpp\ntemplate<typename It>\nauto sort_hits(It begin, It end, size_t m) -> std::vector<Hit> { \n```\n\n我们可以用任何定义了增量运算符的迭代器来调用这个函数。接下来，我们创建一个由一个`std::vector`支持的`std::priority_queue`，使用一个自定义的比较函数将*最低的*排名点击保持在队列的顶部:\n\n```cpp\n auto cmp = [](const Hit& a, const Hit& b) { \n    return a.rank_ > b.rank_; // Note, we are using greater than \n  };\n  auto queue = std::priority_queue<Hit, std::vector<Hit>,                                    decltype(cmp)>{cmp}; \n```\n\n我们最多只会在优先级队列中插入 *m* 个元素。优先级队列将包含迄今为止排名最高的命中。在当前优先级队列中的元素中，等级最低的命中将是最高的元素:\n\n```cpp\n for (auto it = begin; it != end; ++ it) { \n    if (queue.size() < m) { \n      queue.push(*it); \n    } \n    else if (it->rank_ > queue.top().rank_) { \n      queue.pop(); \n      queue.push(*it); \n    } \n  } \n```\n\n现在，我们已经收集了优先级队列中排名最高的命中，所以剩下唯一要做的就是将它们以相反的顺序放入一个向量中，并返回*m*-排序的命中:\n\n```cpp\n auto result = std::vector<Hit>{}; \n  while (!queue.empty()) { \n    result.push_back(queue.top()); \n    queue.pop(); \n  } \n  std::reverse(result.begin(), result.end()); \n  return result; \n} // end of sort_hits() \n```\n\n这个算法的复杂度是多少？如果我们用 n 表示命中次数，用 *m* 表示返回的命中次数，我们可以看到内存消耗是 *O(m)* ，而时间复杂度是 *O(n * log m)* ，因为我们是迭代 *n* 个元素。此外，在每次迭代中，我们可能不得不执行推送和/或弹出，这两种操作都在 *O(log m)* 时间内运行。\n\n我们现在将离开标准库容器，专注于几个与标准容器密切相关的新的有用的类模板。\n\n# 使用视图\n\n在本节中，我们将讨论 C++ 标准库中一些相对较新的类模板:`std::string_view`来自 C++ 17 和`std::span`，它们是在 C++ 20 中引入的。\n\n这些类模板不是容器，而是一系列连续元素的轻量级视图(或切片)。视图是指通过值复制的小对象。它们不分配内存，也不提供任何关于它们所指向的内存寿命的保证。换句话说，它们是非自有引用类型，与本章前面描述的容器有很大不同。同时，它们与`std::string`、`std::array`、`std::vector`密切相关，我们很快就会看到。我先描述一下`std::string_view`。\n\n### 使用字符串视图避免复制\n\n一个`std::string_view`包含一个指针，指向一个不可变的字符串缓冲区的开始和一个大小。由于字符串是连续的字符序列，指针和大小完全定义了有效的子字符串范围。典型地，一个`std::string_view`指向一个`std::string`拥有的一些记忆。但是它也可以指向具有静态存储持续时间的字符串，或者类似于内存映射文件的东西。下图显示了一个`std::string_view`指向一个`std::string`拥有的内存:\n\n<figure class=\"mediaobject\">![](img/B15619_04_13.png)</figure>\n\n图 4.13:一个 std::string_view 对象，指向 std::string 实例所拥有的内存\n\n由`std::string_view`定义的字符序列不需要以空字符结束，但是具有包含空字符的字符序列是完全有效的。另一方面，`std::string`需要能够从`c_str()`返回一个空终止的字符串，这意味着它总是在序列的末尾存储一个额外的空字符。\n\n`string_view`不需要空终止符的事实意味着它可以比 C 风格的字符串或`std::string`更有效地处理子字符串，因为它不必仅仅为了添加空终止符而创建新的字符串。使用`std::string_view`的`substr()`的复杂性是恒定的，这应该与以线性时间运行的`std::string`的`substr()`版本相比较。\n\n当将字符串传递给函数时，也有性能上的优势。考虑以下代码:\n\n```cpp\nauto some_func(const std::string& s) {\n  // process s ...\n}\nsome_func(\"A string literal\"); // Creates a std::string \n```\n\n将字符串文字传递给`some_func()`时，编译器需要构造一个新的`std::string`对象来匹配参数的类型。但是，如果我们让`some_func()`接受一个`std::string_view`，就不再需要建造一个`std::string`:\n\n```cpp\nauto some_func(std::string_view s) { // Pass by value\n  // process s ... \n}\nsome_func(\"A string literal\"); \n```\n\n一个`std::string_view`实例可以从一个`std::string`和一个字符串有效地构造出来，因此是一个非常适合函数参数的类型。\n\n### 使用标准::span 消除阵列衰减\n\n在本章前面讨论`std::vector`和`std::array`时，我提到当内置数组传递给函数时，数组衰减(失去数组的大小信息)会发生:\n\n```cpp\n// buffer looks like an array, but is in fact a pointer \nauto f1(float buffer[]) {\n  const auto n = std::size(buffer);   // Does not compile!\n  for (auto i = 0u; i < n; ++ i) {     // Size is lost!\n    // ...\n  }\n} \n```\n\n我们可以通过添加一个大小参数来解决这个问题:\n\n```cpp\nauto f2(float buffer[], size_t n) {\n  for (auto i = 0u; i < n; ++ i) {\n    // ...\n  }\n} \n```\n\n虽然这在技术上可行，但是将正确的数据传递给这个函数既容易出错又繁琐，如果`f2()`将缓冲区传递给其他函数，就需要记住传递大小正确的变量`n`。`f2()`的呼叫站点可能是这样的:\n\n```cpp\nfloat a[256]; \nf2(a, 256);     \nf2(a, sizeof(a)/sizeof(a[0])); // A common tedious pattern\nf2(a, std::size(a)); \n```\n\n数组衰减是许多与绑定相关的 bug 的来源，在使用内置数组的情况下(由于这样或那样的原因)，`std::span`提供了一种更安全的方式将数组传递给函数。由于 span 将指向内存的指针和大小一起保存在一个对象中，因此在将元素序列传递给函数时，我们可以将它用作单一类型:\n\n```cpp\nauto f3(std::span<float> buffer) {  // Pass by value\n  for (auto&& b : buffer) {         // Range-based for-loop\n    // ...\n  }\n}\nfloat a[256]; \nf3(a);          // OK! Array is passed as a span with size\nauto v = std::vector{1.f, 2.f, 3.f, 4.f};\nf3(v);          // OK! \n```\n\nspan 在内置数组上使用也更方便，因为它更像一个支持迭代器的常规容器。\n\n`std::string_view`和`std::span`在数据成员(指针和大小)和成员函数方面有很多相似之处。但是也有一些显著的区别:由`std::span`指向的记忆是可变的，而`std::string_view`总是指向恒定的记忆。`std::string_view`还包含特定于字符串的功能，如`hash()`和`substr()`，它们自然不是`std::span`的一部分。最后，`std::span`中没有`compare()`功能，所以无法直接在`std::span`对象上使用比较运算符。\n\n现在是时候在使用标准库中的数据结构时，强调一些与性能相关的要点了。\n\n# 一些性能考虑\n\n我们现在已经涵盖了三个主要的容器类别:序列容器、关联容器和容器适配器。本节将为您提供一些在使用容器时需要考虑的一般性能建议。\n\n## 复杂性保证和开销之间的平衡\n\n在容器之间进行选择时，了解数据结构的时间和内存复杂性非常重要。但是同样重要的是要记住，每个容器都受到开销成本的困扰，这对较小数据集的性能影响更大。复杂性保证只有在足够大的数据集上才会变得有趣。然而，这取决于您决定在您的用例中什么是足够大的含义。同样，在这里，您需要在执行程序时对其进行测量，以获得洞察力。\n\n此外，计算机配备内存缓存的事实使得对缓存友好的数据结构的使用更有可能表现得更好。这通常有利于`std::vector`，它具有较低的内存开销，并且将其元素连续存储在内存中，使得访问和遍历更快。\n\n下图显示了两种算法的实际运行时间。一个以线性时间运行， *O(n)* ，另一个以对数时间运行， *O(log n)* ，但开销较大。当输入大小低于标记的阈值时，对数算法比线性时间算法慢:\n\n<figure class=\"mediaobject\">![](img/B15619_04_14.png)</figure>\n\n图 4.14:对于小尺寸的 n，线性算法 O(n)比在 O(log n)中运行的算法更快\n\n我们要记住的下一点是更具体的，并强调使用最合适的 API 函数的重要性。\n\n## 了解并使用适当的应用编程接口函数\n\n在 C++ 中，做某事的方式通常不止一种。语言和库在继续发展，但是很少有特性被弃用。当新的函数被添加到标准库中时，我们应该学习何时使用它们，并思考我们可能已经使用了什么模式来补偿以前丢失的函数。\n\n在这里，我们将重点关注两个小但重要的函数，它们可以在标准库中找到:`contains()`和`empty()`。检查关联容器中是否存在元素时，使用`contains()`。如果你想知道一个容器是否有任何元素或者是空的，使用`empty()`。除了更清楚地表达意图，它还有性能优势。检查链表的大小是一个 *O(n)* 操作，而调用链表上的`empty()`是在恒定时间内运行的， *O(1)* 。\n\n在 C++ 20 和`contains()`函数引入之前，每次我们想检查关联容器中是否存在某个值时，都要绕道而行。您很可能偶然发现了使用各种方法来寻找元素存在的代码。假设我们有一个用`std::multiset`实现的单词包:\n\n```cpp\nauto bag = std::multiset<std::string>{}; // Our bag-of-words\n// Fill bag with words ... \n```\n\n如果我们想知道某个特定的单词是否在我们的单词包中，有许多方法可以前进。一种替代方法是使用`count()`，如下所示:\n\n```cpp\nauto word = std::string{\"bayes\"}; // Our word we want to find\nif (bag.count(word) > 0) {\n   // ...\n} \n```\n\n这似乎是合理的，但它可能会有轻微的开销，因为它计算了所有与我们的话相匹配的*元素。另一种选择是使用`find()`，但是它有相同的开销，因为它返回所有匹配的单词，而不仅仅是第一个出现的单词:*\n\n```cpp\nif (bag.find(word) != bag.end()) {\n  // ...\n} \n```\n\n在 C++ 20 之前，推荐的方法是使用`lower_bound()`，因为它只返回第一个匹配的元素，如下所示:\n\n```cpp\nif (bag.lower_bound(word) != bag.end()) { \n  // ...\n} \n```\n\n现在，有了 C++ 20 和`contains()`的引入，我们可以更清楚地表达我们的意图，并且当我们只想检查一个元素的存在时，也可以确信库将为我们提供最有效的实现:\n\n```cpp\nif (bag.contains(word)) { // Efficient and with clear intent \n  // ...\n} \n```\n\n一般规则是，如果有一个特定的成员函数或一个为特定容器设计的自由函数，那么如果它符合您的需求，就使用它。它将是高效的，并且它将更清楚地表达意图。不要因为没有学会完整的 API，或者因为你有用某种方式做事的旧习惯，就走前面展示的弯路。\n\n还应该说，零开销原则特别适用于像这样的函数，所以不要花时间通过手工制作自己的函数来试图智胜库实现者。\n\n我们现在将继续看一个更长的例子，说明我们如何以不同的方式对数据重新排序，以优化特定用例的运行时性能。\n\n# 并行阵列\n\n我们将通过讨论迭代元素和探索迭代类似数组的数据结构时提高性能的方法来结束这一章。我已经提到了访问数据时性能的两个重要因素:空间局部性和时间局部性。当迭代内存中连续存储的元素时，由于空间局部性，如果我们设法保持对象较小，我们将增加我们需要的数据已经被缓存的概率。显然，这将对性能产生很大影响。\n\n回想一下本章开头显示的缓存抖动示例，其中我们遍历了一个矩阵。它表明，我们有时需要考虑我们访问数据的方式，即使我们有一个相当紧凑的数据表示。\n\n接下来，我们将比较迭代不同大小的对象需要多长时间。我们将从定义两个结构`SmallObject`和`BigObject`开始:\n\n```cpp\nstruct SmallObject { \n  std::array<char, 4> data_{}; \n  int score_{std::rand()}; \n};\n\nstruct BigObject { \n std::array<char, 256> data_{}; \n int score_{std::rand()}; \n}; \n```\n\n`SmallObject`和`BigObject`是相同的，除了初始数据数组的大小。这两个结构都包含一个名为`score_`的`int`，我们初始化为一个随机值只是为了测试。我们可以让编译器通过使用`sizeof`运算符告诉我们对象的大小:\n\n```cpp\nstd::cout << sizeof(SmallObject); // Possible output is 8 \nstd::cout << sizeof(BigObject);   // Possible output is 260 \n```\n\n为了评估性能，我们需要大量的对象。创建一百万个各种类型的对象:\n\n```cpp\nauto small_objects = std::vector<SmallObject>(1'000'000); \nauto big_objects = std::vector<BigObject>(1'000'000); \n```\n\n现在开始迭代。假设我们想要对所有对象的分数求和。我们倾向于使用`std::accumulate()`，我们将在本书的后面介绍，但是，现在，一个简单的`for`循环就可以了。我们把这个函数写成一个模板，这样我们就不必为每种类型的对象手工编写一个版本。该函数对对象进行迭代，并对所有分数求和:\n\n```cpp\ntemplate <class T> \nauto sum_scores(const std::vector<T>& objects) {  \n  ScopedTimer t{\"sum_scores\"};    // See chapter 3 \n\n  auto sum = 0; \n  for (const auto& obj : objects) { \n    sum += obj.score_; \n  } \n  return sum; \n} \n```\n\n现在，我们准备好看一下将小对象与大对象的分数相加需要多长时间:\n\n```cpp\nauto sum = 0; \nsum += sum_scores(small_objects); \nsum += sum_scores(big_objects); \n```\n\n为了获得可靠的结果，我们需要重复测试几次。在我的电脑上，计算小物体的总和大约需要 1 毫秒，计算大物体的总和需要 10 毫秒。这个例子类似于本章开头的高速缓存抖动的例子，造成巨大差异的一个原因还是因为计算机使用高速缓存层次结构从主内存中获取数据的方式。\n\n当使用比前面的例子更真实的场景时，我们如何利用迭代较小对象的集合比迭代较大对象更快的事实？\n\n显然，我们可以尽最大努力保持班级规模小，但这往往说起来容易做起来难。此外，如果我们正在使用一个已经增长了一段时间的旧代码库，我们很有可能会遇到一些非常大的类，它们有太多的数据成员和太多的责任。\n\n我们现在来看一个在线游戏系统中代表用户的类，看看我们如何将它分成更小的部分。该类具有以下数据成员:\n\n```cpp\nstruct User { \n  std::string name_; \n  std::string username_; \n  std::string password_; \n  std::string security_question_; \n  std::string security_answer_; \n  short level_{}; \n  bool is_playing_{}; \n}; \n```\n\n用户有一个经常使用的名称和一些很少使用的身份验证信息。该类还跟踪玩家当前在哪个级别玩。最后，`User`结构还通过存储`is_playing_`布尔值知道用户当前是否在玩。\n\n`sizeof`运算符报告在为 64 位架构编译时，`User`类是 128 字节。下图显示了数据成员的大致布局:\n\n<figure class=\"mediaobject\">![](img/B15619_04_15.png)</figure>\n\n图 4.15:用户类的内存布局\n\n所有用户都被保存在一个`std::vector`中，有两个全局函数被调用的非常频繁，需要快速运行:`num_users_at_level()`和`num_playing_users()`。这两个函数迭代所有用户，因此我们需要快速迭代用户向量。\n\n第一个函数返回达到特定级别的用户数量:\n\n```cpp\nauto num_users_at_level(const std::vector<User>& users, short level) { \n  ScopedTimer t{\"num_users_at_level (using 128 bytes User)\"}; \n\n  auto num_users = 0; \n  for (const auto& user : users)\n    if (user.level_ == level)\n      ++ num_users; \n  return num_users; \n} \n```\n\n第二个函数计算当前有多少用户在玩:\n\n```cpp\nauto num_playing_users(const std::vector<User>& users) { \n  ScopedTimer t{\"num_playing_users (using 128 bytes User)\"}; \n\n  return std::count_if(users.begin(), users.end(), \n    [](const auto& user) { \n      return user.is_playing_; \n    }); \n} \n```\n\n这里，我们使用算法`std::count_if()`代替手写循环，就像我们在`num_users_at_level()`中所做的那样。`std::count_if()`将调用我们在用户向量中为每个用户提供的谓词，并返回谓词返回`true`的次数。这基本上也是我们在第一个函数中所做的，所以我们也可以在第一个例子中使用`std::count_if()`。这两个函数都以线性时间运行。\n\n用一百万个用户的向量调用这两个函数会产生以下输出:\n\n```cpp\n11 ms num_users_at_level (using 128 bytes User)\n10 ms num_playing_users (using 128 bytes User) \n```\n\n我们假设通过使`User`类变小，迭代向量会更快。如前所述，密码和安全数据字段很少使用，可以在单独的结构中分组。这将为我们提供以下课程:\n\n```cpp\nstruct AuthInfo { \n  std::string username_; \n  std::string password_; \n  std::string security_question_; \n  std::string security_answer_; \n}; \n\nstruct User { \n  std::string name_; \n  std::unique_ptr<AuthInfo> auth_info_; \n  short level_{}; \n  bool is_playing_{}; \n}; \n```\n\n此更改将`User`类的大小从 128 字节减少到 40 字节。我们使用指针来引用新的`AuthInfo`对象，而不是在`User`类中存储四个字符串。下图显示了我们如何将`User`类分成两个更小的类:\n\n<figure class=\"mediaobject\">![](img/B15619_04_16.png)</figure>\n\n图 4.16:当身份验证信息保存在单独的类中时的内存布局\n\n从设计的角度来看，这种改变也是有意义的。将认证数据保存在单独的类中增加了`User`类的内聚性。`User`类包含一个指向认证信息的指针。当然，用户数据占用的内存总量并没有减少，但是现在重要的是收缩`User`类，以便加快迭代所有用户的函数。\n\n从优化的角度来看，我们必须再次测量这一点，以验证我们关于较小数据的假设是有效的。事实证明，这两个函数的运行速度是较小的`User`类的两倍多。运行修改版本时的输出是:\n\n```cpp\n4 ms num_users_at_level with User\n3 ms num_playing_users with User \n```\n\n接下来，我们将尝试一种更激进的方法，通过使用**并行数组**来减少我们需要迭代的数据量。首先，一个警告:这是一个优化，在许多情况下，有太多的缺点，是一个可行的替代方案。不要把这当成一般的技巧，不假思索地应用。在看了几个例子后，我们将回到并行阵列的优缺点。\n\n通过使用并行数组，我们简单地将大的结构分成更小的类型，类似于我们对`User`类的认证信息所做的。但是，我们不是使用指针来关联对象，而是将较小的结构存储在大小相等的独立数组中。共享同一索引的不同数组中较小的对象构成完整的原始对象。\n\n一个例子将阐明这个技巧。我们使用的`User`类由 40 个字节组成。它现在只包含一个用户名字符串、一个指向认证信息的指针、一个用于当前级别的整数和`is_playing_`布尔值。通过使用户对象变小，我们看到在迭代对象时性能得到了提高。用户对象数组的内存布局如下图所示。我们将暂时忽略内存对齐和填充，但将在*第 7 章*、*内存管理*中回到这些主题:\n\n<figure class=\"mediaobject\">![](img/B15619_04_17.png)</figure>\n\n图 4.17:连续存储在向量中的用户对象\n\n我们可以将所有的`short`级别和`is_playing_`标志存储在单独的向量中，而不是一个向量包含用户对象。用户数组中索引 0 处的用户的当前级别也存储在级别数组中的索引 0 处。这样，我们就可以避免使用指向级别的指针，而是只使用索引来连接数据字段。我们可以对布尔`is_playing_`域做同样的事情，最后得到三个平行的数组，而不是只有一个。三个向量的内存布局如下所示:\n\n<figure class=\"mediaobject\">![](img/B15619_04_18.png)</figure>\n\n图 4.18:使用三个并行阵列时的内存布局\n\n我们使用三个平行阵列在一个特定的领域快速迭代。`num_users_at_level()`功能现在可以只使用级别数组计算特定级别的用户数量。该实现现在只是`std::count()`的包装器:\n\n```cpp\nauto num_users_at_level(const std::vector<int>& users, short level) { \n  ScopedTimer t{\"num_users_at_level using int vector\"}; \n  return std::count(users.begin(), users.end(), level); \n} \n```\n\n同样的，`num_playing_users()`函数只需要遍历布尔向量就可以确定播放用户的数量。同样，我们使用`std::count()`:\n\n```cpp\nauto num_playing_users(const std::vector<bool>& users) { \n  ScopedTimer t{\"num_playing_users using vector<bool>\"}; \n  return std::count(users.begin(), users.end(), true); \n} \n```\n\n有了并行数组，我们根本不用使用用户数组。提取的数组所占用的内存量远远小于用户数组，因此让我们检查一下在一百万个用户上再次运行这些函数时，性能是否有所提高:\n\n```cpp\nauto users = std::vector<User>(1'000'000); \nauto levels = std::vector<short>(1'000'000); \nauto playing_users = std::vector<bool>(1'000'000); \n\n// Initialize data \n// ... \n\nauto num_at_level_5 = num_users_at_level(levels, 5);\nauto num_playing = num_playing_users(playing_users); \n```\n\n使用整数数组时，计算某个级别的用户数量只需 0.7 ms 左右。概括地说，最初使用 128 字节的`User`类的版本花费了大约 11 毫秒。较小的`User`类在 4 毫秒内执行，现在，通过仅使用`levels`数组，我们下降到 0.7 毫秒。这是一个相当戏剧性的变化。\n\n对于第二个功能`num_playing_users()`，变化更大——只需要 0.03 ms 左右就能统计出当前有多少用户在玩。之所以能这么快，是因为有一种叫做**位数组**的数据结构。原来`std::vector<bool>`根本不是 C++ `bool`对象的标准向量。相反，在内部，它是一个位数组。像`count()`和`find()`这样的操作可以在一个位数组中非常有效地优化，因为它一次可以处理 64 位(在 64 位机器上)，或者通过使用 SIMD 寄存器可能会更多。`std::vector<bool>`的未来尚不清楚，它可能很快会被弃用，取而代之的是固定大小的`std::bitset`和新的动态大小的位集。Boost 中已经有一个名为`boost::dynamic_bitset`的版本。\n\n这一切都太棒了，但我警告过你一些缺点。首先，从字段实际所属的类中提取字段会对代码的结构产生很大的影响。在某些情况下，将大型类分成更小的部分是非常合理的，但在其他情况下，它完全破坏了封装，并暴露了可能隐藏在具有更高抽象的接口后面的数据。\n\n确保数组同步也很麻烦，因此我们总是需要确保构成一个对象的字段存储在所有数组的相同索引处。像这样的隐性关系很难维持，而且容易出错。\n\n最后一个缺点实际上与性能有关。在前面的示例中，您看到对于一次迭代一个字段的算法，性能有很大的提高。然而，如果我们有一个算法需要访问已经提取到不同数组中的多个字段，它将比迭代一个具有更大对象的数组慢得多。\n\n因此，与处理性能时的情况一样，没有什么是不需要成本的，公开数据和将一个简单的阵列拆分为多个阵列的成本可能会也可能不会太高。这完全取决于您所面临的场景，以及您在测量后遇到的性能提升。在真正面临性能问题之前，不要考虑并行阵列。总是首先选择合理的设计原则，并倾向于明确的方式来表达对象之间的关系，而不是含蓄的方式。\n\n# 摘要\n\n本章介绍了标准库中的容器类型。您了解到，我们构建数据的方式对我们在对象集合上执行某些操作的效率有很大影响。当在不同的数据结构中进行选择时，标准库容器的渐近复杂性规格是需要考虑的关键因素。\n\n此外，您还了解了现代处理器中的缓存层次结构如何影响我们组织数据以有效访问内存的方式。高效利用缓存级别的重要性怎么强调都不为过。这也是为什么将元素连续保存在内存中的容器使用最多的原因之一，比如`std::vector`和`std::string`。\n\n在下一章中，我们将研究如何使用迭代器和算法来有效地操作容器。*"
  },
  {
    "path": "docs/cpp-hiperf/05.md",
    "content": "# 五、算法\n\nC++ 程序员广泛使用标准库中的容器。例如，很少找到没有引用`std::vector`或`std::string`的 C++ 代码库。然而，根据我的经验，标准库算法的使用频率要低得多，尽管它们提供了与容器相同的好处:\n\n*   在解决复杂问题时，它们可以用作构建模块\n*   它们被很好地记录下来(包括参考资料、书籍和视频)\n*   许多 C++ 程序员已经熟悉它们了\n*   它们的空间和运行时间成本是已知的(复杂性保证)\n*   他们的实现是精心制作和高效的\n\n如果这还不够，C++ 的特性，如 lambdas、执行策略、概念和范围，都使标准算法更加强大，同时也更易于使用。\n\n在本章中，我们将了解如何使用**算法库**在 C++ 中编写高效的算法。您将了解到在应用中使用标准库算法作为构建模块的好处，包括性能和可读性。\n\n在本章中，您将了解:\n\n*   C++ 标准库中的算法\n*   迭代器和范围——容器和算法之间的粘合剂\n*   如何实现可以在标准容器上运行的通用算法\n*   使用 C++ 标准算法的最佳实践\n\n让我们先来看看标准库算法，以及它们是如何变成今天这样的。\n\n# 介绍标准库算法\n\n将标准的库算法集成到你的 C++ 词汇表中是很重要的。在这篇介绍中，我将介绍一组通过使用标准库算法可以有效解决的常见问题。\n\nC++ 20 通过引入**范围库**和 *C++ 概念*的语言特性，对算法库进行了巨大的改变。因此，在我们开始之前，我们需要一个 C++ 标准库历史的简要背景。\n\n## 标准库算法的演变\n\n你可能听过关于 STL 算法或者 STL 容器。希望你已经听说了 C++ 20 引入的新的**范围库**。C++ 20 中的标准库增加了很多内容。在继续之前，我需要弄清楚一些术语。我们将从 STL 开始。\n\n**STL** ，或者**标准模板库**，最初是 20 世纪 90 年代添加到 C++ 标准库中的一个库的名称。它包含算法、容器、迭代器和函数对象。这个名字很有粘性，我们已经习惯于听到和谈论 STL 算法和容器。然而 C++ 标准并没有提到 STL 取而代之的是*标准库*及其单个组件，如**迭代器库**和**算法库**。我将尽量避免在本书中使用 STL 这个名字，而是在需要时谈论标准库或单个库。\n\n现在进入范围库，我称之为**约束算法**。Ranges 库是 C++ 20 中添加到标准库中的一个库，它引入了一个名为`<ranges>`的全新头文件，我们将在下一章中详细介绍。但是 Ranges 库的加入也对`<algorithm>`头产生了很大的影响，因为它引入了所有以前存在的算法的重载版本。我将这些算法称为*约束算法*，因为它们是使用 C++ 概念进行约束的。因此，`<algorithm>`头现在包括了旧的基于迭代器的算法和新的算法，这些算法受 C++ 概念的约束，可以对范围进行操作。这意味着我们将在本章中讨论的算法有两种风格，如下例所示:\n\n```cpp\n#include <algorithm>\n#include <vector>\nauto values = std::vector{9, 2, 5, 3, 4};\n// Sort using the std algorithms\nstd::sort(values.begin(), values.end());\n// Sort using the constrained algorithms under std::ranges\nstd::ranges::sort(values); \nstd::ranges::sort(values.begin(), values.end()); \n```\n\n请注意，`sort()`的两个版本都位于`<algorithm>`头中，但是它们通过不同的名称空间和签名来区分。本章将使用这两种风格，但总的来说，我建议尽可能使用新的约束算法。阅读本章后，好处将会变得显而易见。\n\n现在，您已经准备好开始学习如何使用现成的算法来解决常见问题。\n\n## 解决日常问题\n\n我将在这里列出一些常见场景和有用的算法，只是为了让您体验一下标准库中可用的算法。库中有许多算法，我将在本节中只介绍几个。关于标准库算法的快速而完整的概述，我推荐乔纳森·博卡拉在[https://sched.co/FnJh](https://sched.co/FnJh)的演讲 *CppCon 2018，105 不到一小时的 STL 算法*。\n\n### 迭代一个序列\n\n有一个可以打印序列元素的短助手功能是很有用的。下面的通用函数适用于任何保存元素的容器，这些元素可以使用`operator<<()`打印到输出流中:\n\n```cpp\nvoid print(auto&& r) {\n  std::ranges::for_each(r, [](auto&& i) { std::cout << i << ' '; });\n} \n```\n\n`print()`功能使用的是`for_each()`，是从`<algorithm>`表头导入的算法。`for_each()`为范围内的每个元素调用我们提供的函数一次。我们提供的函数的返回值被忽略，并且对我们传递给`for_each()`的序列没有影响。我们可以使用`for_each()`来处理副作用，例如打印到`stdout`(我们在本例中就是这样做的)。\n\n一个类似的，非常通用的算法是`transform()`。它还为序列中的每个元素调用一个函数，但是它没有忽略返回值，而是将函数的返回值存储在输出序列中，如下所示:\n\n```cpp\nauto in = std::vector{1, 2, 3, 4};\nauto out = std::vector<int>(in.size());\nauto lambda = [](auto&& i) { return i * i; };\nstd::ranges::transform(in, out.begin(), lambda);\nprint(out); \n// Prints: \"1 4 9 16\" \n```\n\n这段代码还演示了我们如何使用前面定义的`print()`函数。`transform()`算法将为输入范围内的每个元素调用一次我们的λ。为了指定输出的存储位置，我们为`transform()`提供了一个输出迭代器`out.begin()`。我们将在本章后面更多地讨论迭代器。\n\n有了我们的`print()`函数和一些最通用算法的演示，我们将继续研究一些生成元素的算法。\n\n### 生成元素\n\n有时我们需要给一个元素序列分配一些初始值或者重置整个序列。以下示例用值-1 填充向量:\n\n```cpp\nauto v = std::vector<int>(4);\nstd::ranges::fill(v, -1);\nprint(v); \n// Prints \"-1 -1 -1 -1 \" \n```\n\n下一个算法`generate()`为每个元素调用一个函数，并将返回值存储在当前元素中:\n\n```cpp\nauto v = std::vector<int>(4);\nstd::ranges::generate(v, std::rand);\nprint(v);\n// Possible output: \"1804289383 846930886 1681692777 1714636915 \" \n```\n\n在前面的例子中，对每个元素调用一次`std::rand()`函数。\n\n我要提到的最后一个生成算法是来自`<numeric>`头的`std::iota()`。它以递增的顺序生成值。必须将起始值指定为第二个参数。下面是一个生成 0 到 5 之间的值的简短示例:\n\n```cpp\n auto v = std::vector<int>(6);\n  std::iota(v.begin(), v.end(), 0);\n  print(v); // Prints: \"0 1 2 3 4 5 \" \n```\n\n这个序列已经被排序了，但是它更常见的情况是，你有一个需要排序的无序元素集合，我们接下来会看到它。\n\n### 排序元素\n\n对元素进行排序是非常常见的操作。有一些排序算法的选择是很好了解的，但是在这个介绍中，我将只展示最常规的版本，简单命名为`sort()`:\n\n```cpp\nauto v = std::vector{4, 3, 2, 3, 6};\nstd::ranges::sort(v);\nprint(v);       // Prints: \"2 3 3 4 6 \" \n```\n\n如上所述，这不是唯一的排序方式，有时我们可以使用部分排序算法来获得性能。我们将在本章后面讨论更多的异常排序。\n\n### 查找元素\n\n另一个非常常见的任务是找出某个特定值是否在集合中。也许我们想知道一个集合中有多少特定值的实例。如果我们知道集合已经排序，这些搜索值的算法可以更有效地实现。您在*第 3 章*、*分析和测量性能*中看到了这一点，我们将线性搜索与二分搜索法进行了比较。\n\n这里我们从`find()`算法开始，它不需要排序的集合:\n\n```cpp\nauto col = std::list{2, 4, 3, 2, 3, 1};\nauto it = std::ranges::find(col, 2);\nif (it != col.end()) {\n  std::cout << *it << '\\n';\n} \n```\n\n如果找不到我们要找的元素，`find()`返回集合的`end()`迭代器。在最坏的情况下，`find()`需要检查序列中的所有元素，因此在 *O(n)* 时间运行。\n\n### 使用二分搜索法查找\n\n如果我们知道集合已经排序，我们可以使用二分搜索法算法之一:`binary_search()`、`equal_range()`、`upper_bound()`或`lower_bound()`。如果我们将这些函数与提供对其元素的随机访问的容器一起使用，它们都保证在 *O(log n)* 时间内运行。当我们在本章后面讨论迭代器和范围时，您将更好地理解算法如何提供复杂性保证，即使它们在不同的容器上运行(有趣的是，接下来有一个部分被命名为*迭代器和范围*)。\n\n在以下示例中，我们将使用带有以下元素的排序后的`std::vector`:\n\n<figure class=\"mediaobject\">![](img/B15619_05_01.png)</figure>\n\n图 5.1:一个包含七个元素的分类标准::向量\n\n`binary_search()`函数返回`true`或`false`，具体取决于是否能找到我们搜索的值:\n\n```cpp\nauto v = std::vector{2, 2, 3, 3, 3, 4, 5};    // Sorted!\nbool found = std::ranges::binary_search(v, 3);\nstd::cout << std::boolalpha << found << '\\n'; //   Output: true \n```\n\n在调用`binary_search()`之前，你要绝对确定集合已经排序。我们可以很容易地在代码中使用`is_sorted()`断言这一点，如下所示:\n\n```cpp\nassert(std::ranges::is_sorted(v)); \n```\n\n该检查将在 *O(n)* 中运行，但仅在断言被激活时调用，因此不会影响最终程序的性能。\n\n我们正在处理的排序集合包含多个 3。如果我们想知道集合中前 3 位或后 3 位的位置会怎样？在这种情况下，我们可以使用`lower_bound()`来查找前 3 个，或者使用`upper_bound()`来查找后 3 个之后的元素:\n\n```cpp\nauto v = std::vector{2, 2, 3, 3, 3, 4, 5};\nauto it = std::ranges::lower_bound(v, 3);\nif (it != v.end()) {\n  auto index = std::distance(v.begin(), it);\n  std::cout << index << '\\n'; // Output: 2\n} \n```\n\n该代码将输出`2`，因为这是前 3 的索引。要从迭代器中获取元素的索引，我们使用`<iterator>`头中的`std::distance()`。\n\n以同样的方式，我们可以使用`upper_bound()`获得元素*的迭代器，经过*最后 3:\n\n```cpp\nconst auto v = std::vector{2, 2, 3, 3, 3, 4, 5};\nauto it = std::ranges::upper_bound(v, 3);\nif (it != v.end()) {\n  auto index = std::distance(v.begin(), it);\n  std::cout << index << '\\n'; // Output: 5\n} \n```\n\n如果同时需要上限和下限，可以改为使用`equal_range()`，它返回包含 3s 的集合的子范围:\n\n```cpp\nconst auto v = std::vector{2, 2, 3, 3, 3, 4, 5};\nauto subrange = std::ranges::equal_range(v, 3);\nif (subrange.begin() != subrange.end()) {\n  auto pos1 = std::distance(v.begin(), subrange.begin());\n  auto pos2 = std::distance(v.begin(), subrange.end());\n  std::cout << pos1 << \" \" << pos2 << '\\n';\n} // Output: \"2 5\" \n```\n\n现在让我们探索一些其他有用的算法来检查一个集合。\n\n### 测试某些条件\n\n有三种非常方便的算法叫做`all_of()`、`any_of()`和`none_of()`。它们都接受一个范围、一元谓词(接受一个参数并返回`true`或`false`的函数)和一个可选的投影函数。\n\n假设我们有一个数字列表和一个决定数字是否为负数的小λ:\n\n```cpp\nconst auto v = std::vector{3, 2, 2, 1, 0, 2, 1};\nconst auto is_negative = [](int i) { return i < 0; }; \n```\n\n我们可以使用`none_of()`来检查这些数字是否都不是负数:\n\n```cpp\nif (std::ranges::none_of(v, is_negative)) {\n  std::cout << \"Contains only natural numbers\\n\";\n} \n```\n\n进一步，我们可以使用`all_of()`来询问列表中的所有元素是否都是负数:\n\n```cpp\nif (std::ranges::all_of(v, is_negative)) {\n  std::cout << \"Contains only negative numbers\\n\";\n} \n```\n\n最后，使用`any_of()`，我们可以看到列表中是否至少包含一个负数:\n\n```cpp\nif (std::ranges::any_of(v, is_negative)) {\n  std::cout << \"Contains at least one negative number\\n\";\n} \n```\n\n很容易忘记这些位于标准库中的小而方便的构建块。但是一旦你养成了使用它们的习惯，你就再也不会回头，开始手写这些了。\n\n### 计数元素\n\n计算等于某个值的元素数量最明显的方法是调用`count()`:\n\n```cpp\nconst auto numbers = std::list{3, 3, 2, 1, 3, 1, 3};\nint n = std::ranges::count(numbers, 3);\nstd::cout << n;                    // Prints: 4 \n```\n\n`count()`算法以线性时间运行。然而，如果我们知道序列是排序的，并且我们使用的是向量或其他随机存取数据结构，我们可以使用`equal_range()`，它将在*0(log n)*时间运行。以下是一个例子:\n\n```cpp\nconst auto v = std::vector{0, 2, 2, 3, 3, 4, 5};\nassert(std::ranges::is_sorted(v)); // O(n), but not called in release\nauto r = std::ranges::equal_range(v, 3);\nint n = std::ranges::size(r);\nstd::cout << n;                    // Prints: 2 \n```\n\n`equal_range()`函数找到包含我们想要计数的值的所有元素的子范围。一旦找到子范围，我们可以使用`<ranges>`头中的`size()`来检索子范围的长度。\n\n### 最小、最大和夹紧\n\n我想提一套小但极其有用的算法，这对一个经验丰富的 C++ 程序员来说是必不可少的知识。函数`std::min()`、`std::max()`和`std::clamp()`有时会被遗忘，取而代之的是我们经常发现自己像这样写代码:\n\n```cpp\nconst auto y_max = 100;\nauto y = some_func();\nif (y > y_max) {\n  y = y_max;\n} \n```\n\n代码保证`y`的值在一定的限度内。这段代码可以工作，但是我们可以通过使用`std::min()`来避免可变变量和`if`语句，如下所示:\n\n```cpp\nconst auto y = std::min(some_func(), y_max); \n```\n\n通过使用`std::min()`，混淆我们代码的可变变量和`if`语句都被消除了。我们可以将`std::max()`用于类似的场景。如果我们想将一个值限制在最小值和最大值之内，我们可以这样做:\n\n```cpp\nconst auto y = std::max(std::min(some_func(), y_max), y_min); \n```\n\n但是，从 C++ 17 开始，我们现在有了`std::clamp()`在一个函数中为我们完成这个任务。因此，我们可以如下使用`clamp()`:\n\n```cpp\nconst auto y = std::clamp(some_func(), y_min, y_max); \n```\n\n有时我们需要在一个未排序的元素集合中找到极值。为此，我们可以使用`minmax()`，它(不出所料)返回序列的最小值和最大值。结合结构化绑定，我们可以按如下方式打印极值:\n\n```cpp\nconst auto v = std::vector{4, 2, 1, 7, 3, 1, 5};\nconst auto [min, max] = std::ranges::minmax(v);\nstd::cout << min << \" \" << max;      // Prints: \"1 7\" \n```\n\n我们也可以使用`min_element()`或`max_element()`找到最小或最大元素的位置。它不是返回值，而是返回一个指向我们正在寻找的元素的迭代器。在下面的例子中，我们找到了最小元素:\n\n```cpp\nconst auto v = std::vector{4, 2, 7, 1, 1, 3};\nconst auto it = std::ranges::min_element(v);\nstd::cout << std::distance(v.begin(), it); // Output: 3 \n```\n\n这段代码打印出`3`，这是找到的第一个最小值的索引。\n\n这是对标准库中一些最常见算法的简单介绍。算法的运行时成本是在 C++ 标准中规定的，所有的库实现都需要遵守这些标准，尽管不同平台之间的具体实现可能会有所不同。为了理解如何为处理多种不同类型容器的通用算法保留复杂性保证，我们需要仔细研究迭代器和范围。\n\n# 迭代器和范围\n\n正如在前面的例子中看到的，标准库算法对迭代器和范围而不是容器类型进行操作。本节将重点介绍迭代器和 C++ 20 中引入的新的范围概念。一旦掌握了迭代器和范围，正确使用容器和算法就变得容易了。\n\n## 引入迭代器\n\n迭代器构成了标准库算法和范围的基础。迭代器是数据结构和算法之间的粘合剂。正如您已经看到的，C++ 容器以非常不同的方式存储它们的元素。迭代器提供了一种通用的方法来浏览序列中的元素。通过让算法对迭代器而不是容器类型进行操作，算法变得更加通用和灵活，因为它们不依赖于容器的类型和容器在内存中排列元素的方式。\n\n迭代器的核心是一个对象，它代表序列中的一个位置。它有两个主要职责:\n\n*   在序列中导航\n*   在当前位置读写值\n\n迭代器抽象根本不是 C++ 独有的概念，而是存在于大多数编程语言中。迭代器概念的 C++ 实现与其他编程语言的区别在于，C++ 模仿原始内存指针的语法。\n\n基本上，迭代器可以被认为是具有与原始指针相同属性的对象；它可以步进到下一个元素并取消引用(如果指向有效的地址)。算法只使用指针允许的一些操作，尽管迭代器在内部可能是一个穿越树状结构的重对象。\n\n直接在`std`命名空间下找到的大多数算法只对迭代器进行操作，而不是对容器(即`std::vector`、`std::map`等)进行操作。许多算法返回迭代器而不是值。\n\n为了能够在序列中导航而不越界，我们需要一种通用的方法来判断迭代器何时到达序列的末尾。这就是我们的哨兵价值观。\n\n## 哨兵值和结束迭代器\n\n一个**标记值**(或者简单地说是一个标记)是一个特殊的值，表示一个序列的结束。哨兵值使得迭代一个值序列成为可能，而无需事先知道序列的大小。哨兵值的一个示例用法是以空终止的 C 风格字符串(在本例中，哨兵是`'\\0'`字符)。指向字符串开头的指针和结尾的标记足以定义一个字符序列，而不是跟踪空终止字符串的长度。\n\n受约束的算法使用迭代器来定义序列中的第一个元素，并使用标记来指示序列的结束。哨兵的唯一要求是它可以与迭代器进行比较，这实际上意味着`operator==()`和`operator!=()`应该被定义为接受哨兵和迭代器的组合:\n\n```cpp\nbool operator=!(sentinel s, iterator i) {\n  // ...\n} \n```\n\n既然你知道了哨点是什么，我们如何创建一个哨点来指示一个序列的结束？这里的诀窍是使用一个叫做**的过去式** **迭代器**作为哨兵。它只是一个迭代器，指向我们定义的序列中最后一个元素(或过去)之后的元素*。看看下面的代码片段和图表:*\n\n<colgroup><col> <col></colgroup> \n| \n\n```cpp\nauto vec = std::vector {\n  'a','b','c','d'\n};\nauto first = vec.begin();\nauto last = vec.end(); \n```\n\n | \n\n<figure class=\"mediaobject\">![](img/B15619_05_02.png)</figure>\n\n |\n\n如上图所示，`last`迭代器现在指向`'d'`之后的一个想象元素。这使得通过使用循环来迭代序列中的所有元素成为可能:\n\n```cpp\nfor (; first != last; ++ first) {\n  char value = *first; // Dereference iterator\n  // ... \n```\n\n我们可以使用过去结束标记与我们的迭代器`it`进行比较，但是我们不能取消标记，因为它没有指向范围的元素。过去式迭代器的概念由来已久，甚至适用于内置的 C 数组:\n\n```cpp\nchar arr[] = {'a', 'b', 'c', 'd'};\nchar* end = arr + sizeof(arr);\nfor (char* it = arr; it != end; ++ it) { // Stop at end\n   std::cout << *it << ' ';} \n// Output: a b c d \n```\n\n再次注意`end`实际上指出了边界，所以我们不允许取消引用它，但是我们被允许读取指针值并将其与我们的`it`变量进行比较。\n\n## 范围\n\n范围是对我们在引用元素序列时使用的迭代器-哨兵对的替换。`<range>`标题包含多个定义不同种类范围需求的概念，例如`input_range`、`random_access_range`等。这些都是对最基本的概念`range`的提炼，定义如下:\n\n```cpp\ntemplate<class T>\nconcept range = requires(T& t) {\n  ranges::begin(t);\n  ranges::end(t);\n}; \n```\n\n这意味着任何公开`begin()`和`end()`函数的类型都被认为是一个范围(假设这些函数返回迭代器)。\n\n对于 C++ 标准容器，`begin()`和`end()`函数将返回相同类型的迭代器，而对于 C++ 20 范围，这通常是不正确的。具有相同迭代器和哨兵类型的范围实现了`std::ranges::common_range`的概念。新的 C++ 20 视图(将在下一章中介绍)返回可以是不同类型的迭代器-哨兵对。但是，可以使用`std::views::common`将它们转换为与迭代器和哨兵具有相同类型的视图。\n\n在`std::ranges`命名空间中找到的约束算法可以对范围而不是迭代器对进行操作。由于所有标准容器(`vector`、`map`、`list`等)都满足范围概念，我们可以将范围直接传递给约束算法，如下所示:\n\n```cpp\nauto vec = std::vector{1, 1, 0, 1, 1, 0, 0, 1};\nstd::cout << std::ranges::count(vec, 0); // Prints 3 \n```\n\n范围是可迭代的东西(可以循环的东西)的抽象，在某种程度上，它们隐藏了 C++ 迭代器的直接使用。然而，迭代器仍然是 C++ 标准库的主要部分，并且在 Ranges 库中也广泛使用。\n\n接下来你需要了解的是存在的不同类型的迭代器。\n\n## 迭代器类别\n\n既然您已经更好地理解了范围是如何定义的，以及我们如何知道何时到达序列的末尾，现在是时候更仔细地研究迭代器可以支持的操作，以便导航、读取和写入值了。\n\n序列中的迭代器导航可以通过以下操作完成:\n\n*   向前一步:`std::next(it)`或`++ it`\n*   后退:`std::prev(it)`或`--it`\n*   跳到任意位置:`std::advance(it, n)`或`it += n`\n\n在迭代器所代表的位置读写一个值是通过*取消对迭代器的引用*来完成的。以下是它的外观:\n\n*   读作:`auto value = *it`\n*   写:`*it = value`\n\n这些是容器公开的迭代器最常见的操作。但是除此之外，迭代器可能对数据源进行操作，其中写或读意味着前进一步。这种数据源的例子可以是用户输入、网络连接或文件。这些数据源需要以下操作:\n\n*   只读*和*向前一步:`auto value = *it; ++ it;`\n*   只写*和*向前一步:`*it = value; ++ it;`\n\n这些操作只能用两个后续表达式来表示。第一个表达式的后置条件是第二个表达式必须有效。这也意味着我们只能对一个位置读取或写入一次值。如果我们想读取或写入一个新值，我们必须首先将迭代器推进到下一个位置。\n\n并非所有迭代器都支持前面列表中的所有操作。例如，一些迭代器只能*读取*值和*向前一步*，而其他迭代器既可以*读取*、*写入*，又可以*跳转*到任意位置。\n\n现在，如果我们考虑一些基本的算法，很明显，不同的算法对迭代器的要求是不同的:\n\n*   如果一个算法计算一个值的出现次数，它需要*读取*和*向前一步*操作\n*   如果一个算法用一个值填充一个容器，它需要*写*和*向前一步*操作\n*   排序集合上的二分搜索法算法需要*读取*和*跳转*操作\n\n根据迭代器支持的操作，一些算法可以更有效地实现。就像容器一样，标准库中的所有算法都有复杂性保证(使用大 O 符号)。一个算法要满足一定的复杂度保证，就要对它所操作的迭代器提出*要求*。这些需求被分成六个基本迭代器类别，如下图所示:\n\n<figure class=\"mediaobject\">![](img/B15619_05_03.png)</figure>\n\n图 5.2:六个迭代器类别及其相互关系\n\n箭头表示迭代器类别也具有它所指向的类别的所有功能。例如，如果一个算法需要一个前向迭代器，我们也可以给它传递一个双向迭代器，因为双向迭代器具有前向迭代器的所有功能。\n\n这六项要求由以下概念正式规定:\n\n*   `std::input_iterator`:支持*只读，向前一步*(一次)。`std::count()`等单程算法可以使用输入迭代器。`std::istream_iterator`是一个输入迭代器的例子。\n*   `std::output_iterator`:支持*只写，向前一步*(一次)。注意，输出迭代器只能写，不能读。`std::ostream_iterator`是输出迭代器的一个例子。\n*   `std::forward_iterator`:支持*读**写**向前一步*。当前位置的值可以多次读取或写入。像`std::forward_list`这样的单链表公开了前向迭代器。\n*   `std::bidirectional_iterator`:支持*读、写、*前进、*后退*。双向链接`std::list`公开双向迭代器。\n*   `std::random_access_iterator`:支持*读*、*写*、*向前一步*、*向后一步*、*在恒定时间内跳转*到任意位置。`std::deque`内部的元素可以用随机访问迭代器访问。\n*   `std::contiguous_iterator`:与随机访问迭代器相同，但也保证了底层数据是一个连续的内存块，比如`std::string`、`std::vector`、`std::array`、`std::span`、`std::valarray`(很少使用)。\n\n迭代器类别对于理解算法的时间复杂度要求非常重要。对底层数据结构有很好的理解使得知道迭代器通常属于哪个容器变得相当容易。\n\n我们现在准备深入挖掘大多数标准库算法使用的常见模式。\n\n# 标准算法的特征\n\n为了更好地理解标准算法，最好了解一下`<algorithm>`标题中所有算法使用的特性和常见模式。如前所述，`std`和`std::ranges`名称空间下的算法有很多共同点。我们将从适用于`std`算法和`std::range`约束算法的一般原则开始。然后，在下一节，我们将继续讨论`std::ranges`下的约束算法特有的特性。\n\n## 算法不会改变容器的大小\n\n从`<algorithm>`开始的函数只能修改指定范围内的元素；从不在基础容器中添加或删除元素。因此，这些函数永远不会改变它们所操作的容器的大小。\n\n例如，`std::remove()`或`std::unique()`实际上并没有从容器中移除元素(不管它们的名称如何)。相反，它将应该保留的元素移动到容器的前面，然后返回一个标记，该标记定义了元素有效范围的新结束:\n\n<colgroup><col> <col></colgroup> \n| 代码示例 | 结果向量 |\n| \n\n```cpp\n// Example with std::remove()\nauto v = std::vector{1,1,2,2,3,3};\nauto new_end = std::remove(\n  v.begin(), v.end(), 2);\nv.erase(new_end, v.end()); \n```\n\n | \n\n<figure class=\"mediaobject\">![](img/B15619_05_04.png)</figure>\n\n |\n| \n\n```cpp\n// Example with std::unique()\nauto v = std::vector{1,1,2,2,3,3};\nauto new_end = std::unique(\n  v.begin(), v.end());\nv.erase(new_end, v.end()); \n```\n\n | \n\n<figure class=\"mediaobject\">![](img/B15619_05_05.png)</figure>\n\n |\n\nC++ 20 在`<vector>`头中添加了新版本的`std::erase()`和`std::erase_if()`函数，这将立即从向量中删除值，而无需首先调用`remove()`然后调用`erase()`。\n\n事实上，标准库算法从不改变容器的大小，这意味着当调用产生输出的算法时，我们需要自己分配数据。\n\n## 有输出的算法需要分配的数据\n\n将数据写入输出迭代器的算法，如`std::copy()`或`std::transform()`，需要为输出保留已分配的数据。由于算法只使用迭代器作为参数，它们不能自己分配数据。为了扩大算法操作的容器，它们依赖迭代器能够扩大它所迭代的容器。\n\n如果将一个空容器的迭代器传递给算法进行输出，程序很可能会崩溃。下面的例子`squared`为空，说明了这个问题:\n\n```cpp\nconst auto square_func = [](int x) { return x * x; };\nconst auto v = std::vector{1, 2, 3, 4};\nauto squared = std::vector<int>{};\nstd::ranges::transform(v, squared.begin(), square_func); \n```\n\n相反，您必须执行以下任一操作:\n\n*   为生成的容器预先分配所需的大小，或者\n*   使用插入迭代器，在迭代时将元素插入容器\n\n下面的代码片段显示了如何使用预分配的空间:\n\n```cpp\nconst auto square_func = [](int x) { return x * x; };\nconst auto v = std::vector{1, 2, 3, 4};\nauto squared = std::vector<int>{};\nsquared.resize(v.size());\nstd::ranges::transform(v, squared.begin(), square_func); \n```\n\n下面的代码片段显示了如何使用`std::back_inserter()`和`std::inserter()`将值插入到未预分配的容器中:\n\n```cpp\nconst auto square_func = [](int x) { return x * x; };\nconst auto v = std::vector{1, 2, 3, 4};\n// Insert into back of vector using std::back_inserter\nauto squared_vec = std::vector<int>{};\nauto dst_vec = std::back_inserter(squared_vec);\nstd::ranges::transform(v, dst_vec, square_func);\n// Insert into a std::set using std::inserter\nauto squared_set = std::set<int>{};\nauto dst_set = std::inserter(squared_set, squared_set.end());\nstd::ranges::transform(v, dst_set, square_func); \n```\n\n如果您在`std::vector`上操作，并且知道结果容器的预期大小，您可以在执行算法之前使用`reserve()`成员函数，以避免不必要的分配。否则，向量可能会在算法期间多次重新分配新的内存块。\n\n## 默认情况下，算法使用运算符==()和运算符\n\n相比之下，算法依赖于基本的`==`和`<`运算符，就像整数一样。为了能够使用自己的类和算法，`operator==()` 和`operator<()`必须由类提供或者作为算法的参数。\n\n通过使用三向比较运算符`operator<=>()`，我们可以拥有编译器生成的必要运算符。下面的例子展示了一个简单的`Flower`类，其中`operator==()`被`std::find()`利用，`operator<()`被`std::max_element()`利用:\n\n```cpp\nstruct Flower {\n    auto operator<=>(const Flower& f) const = default; \n    bool operator==(const Flower&) const = default;\n    int height_{};\n};\nauto garden = std::vector<Flower>{{67}, {28}, {14}};\n// std::max_element() uses operator<()\nauto tallest = std::max_element(garden.begin(), garden.end());\n// std::find() uses operator==()\nauto perfect = *std::find(garden.begin(), garden.end(), Flower{28}); \n```\n\n除了使用当前类型的默认比较函数之外，也可以使用自定义比较函数，我们接下来将对此进行探讨。\n\n### 自定义比较器功能\n\n有时我们需要在不使用默认比较运算符的情况下比较对象，例如，按长度排序或查找字符串时。在这些情况下，可以提供自定义函数作为附加参数。而原算法使用的是一个值(例如`std::find()`)，带有特定运算符的版本名称与末尾附加的`_if`(`std::find_if()`、`std::count_if()`等)相同:\n\n```cpp\nauto names = std::vector<std::string> {\n  \"Ralph\", \"Lisa\", \"Homer\", \"Maggie\", \"Apu\", \"Bart\"\n};\nstd::sort(names.begin(), names.end(), \n          [](const std::string& a,const std::string& b) {\n            return a.size() < b.size(); });\n// names is now \"Apu\", \"Lisa\", \"Bart\", \"Ralph\", \"Homer\", \"Maggie\"\n// Find names with length 3\nauto x = std::find_if(names.begin(), names.end(), \n  [](const auto& v) { return v.size() == 3; });\n// x points to \"Apu\" \n```\n\n## 约束算法使用投影\n\n`std::ranges`下受约束的算法为我们提供了一个称为**投影**的便利特性，它减少了编写自定义比较函数的需要。上一节中的前一个示例可以使用标准谓词`std::less`结合自定义投影进行重写:\n\n```cpp\nauto names = std::vector<std::string>{\n  \"Ralph\", \"Lisa\", \"Homer\", \"Maggie\", \"Apu\", \"Bart\"\n};\nstd::ranges::sort(names, std::less<>{}, &std::string::size);\n// names is now \"Apu\", \"Lisa\", \"Bart\", \"Ralph\", \"Homer\", \"Maggie\"\n\n// Find names with length 3\nauto x = std::ranges::find(names, 3, &std::string::size);\n// x points to \"Apu\" \n```\n\n还可以将 lambda 作为投影参数传递，当您想要在投影中组合多个属性时，这将非常方便:\n\n```cpp\nstruct Player {\n  std::string name_{};\n  int level_{};\n  float health_{};\n  // ...\n};\nauto players = std::vector<Player>{\n  {\"Aki\", 1, 9.f}, \n  {\"Nao\", 2, 7.f}, \n  {\"Rei\", 2, 3.f}};\nauto level_and_health = [](const Player& p) {\n  return std::tie(p.level_, p.health_);\n}; \n// Order players by level, then health\nstd::ranges::sort(players, std::greater<>{}, level_and_health); \n```\n\n将投影对象传递给标准算法的可能性是一个非常受欢迎的特性，并且确实简化了自定义比较的使用。\n\n## 算法要求移动操作符不抛出\n\n当移动元素时，所有算法都使用`std::swap()`和`std::move()`，但前提是移动构造函数和移动赋值被标记为`noexcept`。因此，在使用算法时，为重物实现这些是很重要的。如果它们不可用且无异常，则这些元素将被复制。\n\n请注意，如果您在类中实现了移动构造函数和移动赋值运算符，`std::swap()`将使用它们，因此不需要指定的`std::swap()`重载。\n\n## 算法有复杂性保证\n\n标准库中每个算法的复杂度用大 O 符号指定。算法是在考虑性能的情况下创建的。因此，它们不分配内存，也没有比 *O(n log n)* 更高的时间复杂度。不符合这些标准的算法不包括在内，即使它们是相当常见的操作。\n\n注意`stable_sort()`、`inplace_merge()`和`stable_partition()`的例外情况。许多实现倾向于在这些操作期间临时分配内存。\n\n例如，让我们考虑一个测试非排序范围是否包含重复的算法。一种选择是通过遍历该范围并搜索该范围的其余部分来实现它。这将产生具有*0(n*<sup class=\"italic\">2</sup>*)*复杂度的算法:\n\n```cpp\ntemplate <typename Iterator>\nauto contains_duplicates(Iterator first, Iterator last) {\n  for (auto it = first; it != last; ++ it)\n    if (std::find(std::next(it), last, *it) != last)\n      return true;\n  return false;\n} \n```\n\n另一种选择是复制整个范围，对其进行排序，并寻找相邻的相等元素。这将导致 *O(n log n)* 的时间复杂度，`std::sort()`的复杂度。然而，由于它需要复制完整的范围，它仍然不符合构建块算法的条件。分配意味着我们不能相信它不会抛出:\n\n```cpp\ntemplate <typename Iterator>\nauto contains_duplicates(Iterator first, Iterator last) {\n  // As (*first) returns a reference, we have to get \n  // the base type using std::decay_t\n  using ValueType = std::decay_t<decltype(*first)>;\n  auto c = std::vector<ValueType>(first, last);\n  std::sort(c.begin(), c.end());\n  return std::adjacent_find(c.begin(),c.end()) != c.end();\n} \n```\n\n复杂性保证从一开始就是 C++ 标准库的一部分，也是其巨大成功背后的主要原因之一。C++ 标准库中的算法是在考虑性能的情况下设计和实现的。\n\n## 算法的性能与 C 库函数相当\n\n标准 C 库附带了许多低级算法，包括`memcpy()`、`memmove()`、`memcmp()`和`memset()`。根据我的经验，有时人们使用这些函数，而不是标准算法库中的等价函数。原因是人们倾向于相信 C 库函数更快，因此接受类型安全的权衡。\n\n对于现代标准库的实现来说，情况并非如此；等效的算法`std::copy()`、`std::equal()`和`std::fill()`在看似合理的地方求助于这些低级的 C 函数；因此，它们提供了性能和类型安全。\n\n当然，也有例外，C++ 编译器无法检测到使用低级 C 函数是安全的。例如，如果一个类型不容易复制，`std::copy()`就不能使用`memcpy()`。但这是有充分理由的；希望一个不容易复制的类的作者有充分的理由用这种方式设计这个类，我们(或者编译器)不应该通过不调用适当的构造函数来忽略这一点。\n\n有时，来自 C++ 算法库的函数甚至比它们的 C 库同类函数更好。最突出的例子是 C 库中的`std::sort()`对`qsort()`。`std::sort()`和`qsort()`的一个很大的区别就是`qsort()`是*功能*`std::sort()`是*功能模板*。当`qsort()`调用作为函数指针提供的比较函数时，一般比调用一个普通的比较函数要慢很多，这个比较函数在使用`std::sort()`时可能会被编译器内联。\n\n在使用标准算法和实现定制算法时，我们将在本章的剩余部分介绍一些最佳实践。\n\n# 编写和使用通用算法\n\n算法库包含通用算法。为了让这里的事情尽可能具体，我将展示一个如何实现通用算法的例子。这将为您提供一些如何使用标准算法的见解，同时证明实现通用算法并没有那么难。我将有意避免在这里解释示例代码的所有细节，因为我们将在本书的后面花费大量时间在泛型编程上。\n\n在下面的例子中，我们将把一个简单的非通用算法转换成一个成熟的通用算法。\n\n## 非通用算法\n\n一个通用的算法是算法，它可以用于各种范围的元素，而不仅仅是一个特定的类型，比如`std::vector`。以下算法是仅适用于`std::vector<int>`的非通用算法的示例:\n\n```cpp\nauto contains(const std::vector<int>& arr, int v) {\n  for (int i = 0; i < arr.size(); ++ i) {\t\n    if (arr[i] == v) { return true; }\n  }\n  return false;\n} \n```\n\n为了找到我们要找的元素，我们依赖于`std::vector`的接口，它为我们提供了`size()`函数和下标运算符(`operator[]()`)。然而，并不是所有的容器都为我们提供了这些功能，无论如何，我不建议您像这样编写原始循环。相反，我们需要创建一个对迭代器进行操作的函数模板。\n\n## 遗传算法\n\n通过用两个迭代器替换`std::vector`，用一个模板参数替换`int`，我们可以将算法转换为通用版本。以下版本的`contains()`可用于任何容器:\n\n```cpp\ntemplate <typename Iterator, typename T>\nauto contains(Iterator begin, Iterator end, const T& v) {\n  for (auto it = begin; it != end; ++ it) {\n    if (*it == v) { return true; }\n  }\n  return false;\n} \n```\n\n例如，要将其与`std::vector`一起使用，您必须通过`begin()`和`end()`迭代器:\n\n```cpp\nauto v = std::vector{3, 4, 2, 4};\nif (contains(v.begin(), v.end(), 3)) {\n // Found the value...\n} \n```\n\n我们可以通过提供一个接受范围的版本来改进这个算法，而不是两个独立的迭代器参数:\n\n```cpp\nauto contains(const auto& r, const auto& x) {\n  auto it = std::begin(r);\n  auto sentinel = std::end(r);\n  return contains(it, sentinel, x);\n} \n```\n\n这个算法不会强制客户端提供`begin()`和`end()`迭代器，因为我们已经将它们移到了函数内部。我们使用的是 C++ 20 中的**缩写函数模板**语法，以避免明确说明这是一个函数模板。作为最后一步，我们可以向参数类型添加约束:\n\n```cpp\nauto contains(const std::ranges::range auto& r, const auto& x) {\n  auto it = std::begin(r);\n  auto sentinel = std::end(r);\n  return contains(it, sentinel, x);\n} \n```\n\n如您所见，创建一个健壮的通用算法确实不需要那么多代码。我们传递给算法的数据结构的唯一要求是它可以公开`begin()`和`end()`迭代器。您将在*第 8 章*、*编译时编程*中了解更多约束和概念。\n\n## 通用算法可以使用的数据结构\n\n这让我们认识到我们创建的新的定制数据结构可以被标准的通用算法使用，只要它们公开`begin()`和`end()`迭代器或一个范围。举个简单的例子，我们可以实现一个二维的`Grid`结构，其中行作为一对迭代器公开，如下所示:\n\n```cpp\nstruct Grid {\n  Grid(std::size_t w, std::size_t h) : w_{w}, h_{h} {    data_.resize(w * h); \n  }\n  auto get_row(std::size_t y); // Returns iterators or a range\n\n  std::vector<int> data_{};\n  std::size_t w_{};\n  std::size_t h_{};\n}; \n```\n\n下图说明了带有迭代器对的`Grid`结构的布局:\n\n<figure class=\"mediaobject\">![](img/B15619_05_06.png)</figure>\n\n图 5.3:基于一维向量的二维网格\n\n`get_row()`的一个可能的实现将返回一个保存迭代器的`std::pair`，该迭代器代表行的开头和结尾:\n\n```cpp\nauto Grid::get_row(std::size_t y) {\n  auto left = data_.begin() + w_ * y;\n  auto right = left + w_;\n  return std::make_pair(left, right);\n} \n```\n\n代表一行的迭代器对可以被标准库算法使用。在下面的例子中，我们使用`std::generate()`和`std::count()`:\n\n```cpp\nauto grid = Grid{10, 10};\nauto y = 3;\nauto row = grid.get_row(y);\nstd::generate(row.first, row.second, std::rand);\nauto num_fives = std::count(row.first, row.second, 5); \n```\n\n虽然这是可行的，但是使用`std::pair`有点笨拙，还需要客户端知道如何处理迭代器对。没有任何内容明确表示`first`和`second`成员实际上表示半开范围。如果它可以公开一个强类型的范围，那不是很好吗？幸运的是，我们将在下一章探索的 Ranges 库为我们提供了一个名为`std::ranges::subrange`的视图类型。现在，`get_row()`功能可以这样实现:\n\n```cpp\nauto Grid::get_row(std::size_t y) {\n  auto first = data_.begin() + w_ * y;\n  auto sentinel = first + w_;\n  return std::ranges::subrange{first, sentinel};\n} \n```\n\n我们甚至可以更懒一些，使用为这个场景量身定制的便捷视图`std::views::counted()`\n\n```cpp\nauto Grid::get_row(std::size_t y) {\n  auto first = data_.begin() + w_ * y;\n  return std::views::counted(first, w_);\n} \n```\n\n从`Grid`类返回的行现在可以与任何接受范围而不是迭代器对的约束算法一起使用:\n\n```cpp\nauto row = grid.get_row(y);\nstd::ranges::generate(row, std::rand);\nauto num_fives = std::ranges::count(row, 5); \n```\n\n这就完成了我们编写和使用支持迭代器对和范围的通用算法的示例。希望这给了你一些关于如何以通用的方式编写数据结构和算法的见解，以避免如果我们必须为所有类型的数据结构编写专门的算法时会出现的组合爆炸。\n\n# 最佳实践\n\n让我们考虑一下在使用我们一直在讨论的算法时会对您有所帮助的实践。我将首先强调实际利用标准算法的重要性。\n\n## 使用约束算法\n\nC++ 20 引入的`std::ranges`下的约束算法比`std`下的基于迭代器的算法提供了一些好处。受约束的算法执行以下操作:\n\n*   支持投影，这简化了元素的自定义比较。\n*   支持范围而不是迭代器对。不需要将`begin()`和`end()`迭代器作为单独的参数传递。\n*   易于正确使用，并且由于受到 C++ 概念的约束，在编译期间会提供描述性错误消息。\n\n我建议开始使用约束算法，而不是基于迭代器的算法。\n\n你可能已经注意到这本书在很多地方使用了基于迭代器的算法。其原因是，在撰写本书时，并非所有的标准库实现都支持约束算法。\n\n## 仅针对需要检索的数据进行排序\n\n算法库包含三种基本的排序算法:`sort()`、`partial_sort()`和`nth_element()`。此外，它还包含一些变体，包括`stable_sort()`，但我们将重点关注这三个，因为根据我的经验，很容易忘记，在许多情况下，可以通过使用`nth_element()`或`partial_sort()`来避免完整的排序。\n\n当`sort()`对整个范围进行排序时， `partial_sort()`和`nth_element()`可以被认为是检查该排序范围的部分的算法。在许多情况下，您只对排序范围的特定部分感兴趣，例如:\n\n*   如果要计算某个范围的中值，则需要排序范围中间的值。\n*   如果要创建一个人体扫描仪，该扫描仪可以使用 80%的平均身高，则需要排序范围内的两个值:距离最高的人 10%的值和距离最短的人 10%的值。\n\n下图说明了与完全排序的范围相比，`std::nth_element`和`std::partial_sort`如何处理一个范围:\n\n<colgroup><col> <col></colgroup> \n| \n\n```cpp\nauto v = std::vector{6, 3, 2, 7,\n                     4, 1, 5};\nauto it = v.begin() + v.size()/2; \n```\n\n | \n\n<figure class=\"mediaobject\">![](img/B15619_05_07.png)</figure>\n\n |\n| \n\n```cpp\nstd::ranges::sort(v); \n```\n\n | \n\n<figure class=\"mediaobject\">*![](img/B15619_05_08.png)* </figure>\n\n |\n| \n\n```cpp\nstd::nth_element(v.begin(), it,\n                 v.end()); \n```\n\n | \n\n<figure class=\"mediaobject\">*![](img/B15619_05_09.png)* </figure>\n\n |\n| \n\n```cpp\nstd::partial_sort(v.begin(), it,\n                  v.end()); \n```\n\n | \n\n<figure class=\"mediaobject\">*![](img/B15619_05_10.png)* </figure>\n\n |\n\n图 5.1:使用不同算法的排序和非排序元素\n\n下表显示了它们的算法复杂度；注意 *m* 表示正在完全排序的子范围:\n\n<colgroup><col> <col></colgroup> \n| 算法 | 复杂性 |\n| `std::sort()` | *O(n 对数 n)* |\n| `std::partial_sort()` | *0(n log m)* |\n| `std::nth_element()` | *O(n)* |\n\n表 5.2:算法复杂性\n\n### 用例\n\n现在你已经对`std:nth_element()`和`std::partial_sort()`有了深入的了解，让我们看看如何将它们结合起来检查一个范围的部分，就像整个范围被排序一样:\n\n<colgroup><col> <col></colgroup> \n| \n\n```cpp\nauto v = std::vector{6, 3, 2, 7,\n                     4, 1, 5};\nauto it = v.begin() + v.size()/2; \n```\n\n | \n\n<figure class=\"mediaobject\">![](img/B15619_05_07.png)</figure>\n\n |\n| \n\n```cpp\nauto left = it - 1;\nauto right = it + 2;\nstd::nth_element(v.begin(),\n                 left, v.end());\nstd::partial_sort(left, right,\n                  v.end()); \n```\n\n | \n\n<figure class=\"mediaobject\">*![](img/B15619_05_12.png)* </figure>\n\n |\n| \n\n```cpp\nstd::nth_element(v.begin(), it,\n                 v.end());\nstd::sort(it, v.end()); \n```\n\n | \n\n<figure class=\"mediaobject\">*![](img/B15619_05_13.png)* </figure>\n\n |\n| \n\n```cpp\nauto left = it - 1;\nauto right = it + 2;\nstd::nth_element(v.begin(),\n                 right, v.end());\nstd::partial_sort(v.begin(),\n                  left, right);\nstd::sort(right, v.end()); \n```\n\n | \n\n<figure class=\"mediaobject\">*![](img/B15619_05_14.png)* </figure>\n\n |\n\n图 5.3:组合算法和相应的部分排序结果\n\n如您所见，通过使用`std::sort()`、`std::nth_element()`和`std::partial_sort()`的组合，有很多方法可以避免在不绝对需要时对整个范围进行排序。这是获得性能的有效方法。\n\n### 性能赋值\n\n让我们看看`std::nth_element()`和`std::partial_sort()`如何与`std::sort()`相抗衡。我们已经用一个包含 1000 万个随机`int`元素的`std::vector`对此进行了测量:\n\n<colgroup><col> <col> <col></colgroup> \n| 操作 | 代码，其中`r`是操作的范围 | 时间(加速) |\n| 分类 | \n\n```cpp\nstd::sort(r.begin(), r.end()); \n```\n\n | 760 毫秒(1.0 倍) |\n| 找到中位数 | \n\n```cpp\nauto it = r.begin() + r.size() / 2;\nstd::nth_element(r.begin(), it, r.end()); \n```\n\n | 83 毫秒(9.2 倍) |\n| 排序范围的前十分之一 | \n\n```cpp\nauto it = r.begin() + r.size() / 10;\nstd::partial_sort(r.begin(), it, r.end()); \n```\n\n | 378 毫秒(2.0 倍) |\n\n表 5.3:部分排序算法的基准测试结果\n\n## 对原始 for 循环使用标准算法\n\n很容易忘记复杂的算法可以通过组合标准库中的算法来实现。也许是因为一个旧习惯，即试图用手解决问题，并立即开始手工`for`-循环，并使用命令式方法解决问题。如果这听起来对你来说很熟悉，我的建议是充分了解标准算法，以便你开始考虑将它们作为首选。\n\n我提倡在原始`for`循环上使用标准库算法，原因有很多:\n\n*   标准算法提供性能。即使标准库中的一些算法看起来微不足道，但它们通常是以乍一看并不明显的方式进行优化设计的。\n*   标准算法提供安全性。甚至更简单的算法也可能有容易被忽视的角落案例。\n*   标准算法是经得起未来考验的；如果你想利用 SIMD 扩展、并行性，甚至是后期的 GPU，一个给定的算法可以被一个更合适的算法取代(见*第 14 章*、*并行算法*)。\n*   标准算法被完整地记录下来。\n\n此外，通过使用算法而不是`for`-循环，每个操作的意图都由算法的名称明确指示。代码的读者不需要检查原始`for`循环中的细节来确定如果您使用标准算法作为构建块，您的代码会做什么。\n\n一旦你养成了用算法思考的习惯，你就会意识到很多`for`循环往往是一些简单算法的变体，比如`std::transform()`、`std::any_of()`、`std::copy_if()`和`std::find()`。\n\n使用算法也将使代码更加清晰。您通常可以在没有嵌套代码块的情况下实现函数，同时避免可变变量。这将在下面的示例中演示。\n\n### 示例 1:可读性问题和可变变量\n\n我们的第一个例子来自真实世界的代码库，尽管变量名已经被伪装了。因为它只是一个切口，所以您不必理解代码的逻辑。这里的例子只是为了向您展示，与嵌套的`for`循环相比，使用算法时复杂性是如何降低的。\n\n最初的版本是这样的:\n\n```cpp\n// Original version using a for-loop\nauto conflicting = false;\nfor (const auto& info : infos) {\n  if (info.params() == output.params()) {\n    if (varies(info.flags())) {\n      conflicting = true;\n      break;\n    }\n  }\n  else {\n    conflicting = true;\n    break;\n  }\n} \n```\n\n在`for` -loop 版本中，很难理解`conflicting`设置为`true`的时间或原因，而在下面的算法版本中，你可以本能地看到如果`info`满足一个谓词，就会发生这种情况。此外，标准算法版本不使用可变变量，可以使用短λ和`any_of()`的组合来编写。以下是它的外观:\n\n```cpp\n// Version using standard algorithms\nconst auto in_conflict = [&](const auto& info) {\n  return info.params() != output.params() || varies(info.flags());\n};\nconst auto conflicting = std::ranges::any_of(infos, in_conflict); \n```\n\n尽管这可能夸大了这一点，但想象一下，如果我们跟踪一个 bug 或将其并行化，使用λ和`any_of()`的标准算法版本将更容易理解和推理。\n\n### 示例 2:不幸的异常和性能问题\n\n为了进一步说明使用算法而不是“T0”循环的重要性，我想展示一些不太明显的问题，当你使用手工“T1”循环而不是标准算法时，你可能会遇到这些问题。\n\n假设我们需要一个函数，将第一个 *n 个*元素从容器的前面移到后面，如下所示:\n\n<figure class=\"mediaobject\">![](img/B15619_05_15.png)</figure>\n\n图 5.4:将前三个元素移到范围的后面\n\n#### 方法 1:使用传统的 for 循环\n\n一种天真的方法是将第一个 *n* 元素复制到后面，同时迭代它们，然后擦除第一个 *n* 元素:\n\n<figure class=\"mediaobject\">![](img/B15619_05_16.png)</figure>\n\n图 5.5:为了将元素移到范围的后面而进行的分配和解除分配\n\n下面是相应的实现:\n\n```cpp\ntemplate <typename Container>\nauto move_n_elements_to_back(Container& c, std::size_t n) {\n  // Copy the first n elements to the end of the container\n  for (auto it = c.begin(); it != std::next(c.begin(), n); ++ it) {\n    c.emplace_back(std::move(*it));\n  }\n  // Erase the copied elements from front of container\n  c.erase(c.begin(), std::next(c.begin(), n));\n} \n```\n\n乍一看，它看起来似乎是合理的，但是检查它揭示了一个严重的问题——如果容器在迭代期间由于`emplace_back()`而重新分配，迭代器`it`将不再有效。当算法试图访问无效的迭代器时，算法将进入未定义的行为，在最好的情况下，会崩溃。\n\n#### 方法 2:环路安全(以性能为代价的安全)\n\n由于未定义的行为是一个明显的问题，我们将不得不重写算法。我们仍然使用手工制作的`for`循环，但是我们将使用索引而不是迭代器:\n\n```cpp\ntemplate <typename Container>\nauto move_n_elements_to_back(Container& c, std::size_t n) {\n  for (size_t i = 0; i < n; ++ i) {\n    auto value = *std::next(c.begin(), i);\n    c.emplace_back(std::move(value));\n  }\n  c.erase(c.begin(), std::next(c.begin(), n));\n} \n```\n\n解决方案奏效了；它不再崩溃了。但是现在，它有一个微妙的性能问题。`std::list`上的算法明显慢于`std::vector`上的算法。原因是和`std::list::iterator`一起使用的`std::next(it, n)`是 *O(n)* ，a `std::vector::iterator`上的 *O(1)* 。由于`std::next(it, n)`在`for`循环的每一步都被调用，该算法在`std::list`等容器上的时间复杂度为*O(n*<sup class=\"italic\">2</sup>*)*。除了这个性能限制之外，前面的代码还有以下限制:\n\n*   由于`emplace_back()`，它不适用于静态大小的容器，如`std::array`\n*   它可能会抛出一个异常，因为`emplace_back()`可能会分配内存并失败(不过这可能很少见)\n\n#### 方法 3:找到并使用合适的标准库算法\n\n当我们到达这个阶段时，我们应该浏览标准库，看看它是否包含合适的算法来用作构建块。方便的是，`<algorithm>`头提供了一个名为`std::rotate()`的算法，它在避免前面提到的所有缺点的同时，准确地完成了我们正在寻找的东西。这是我们使用`std::rotate()`算法的最终版本:\n\n```cpp\ntemplate <typename Container>\nauto move_n_elements_to_back(Container& c, std::size_t n) {\n  auto new_begin = std::next(c.begin(), n);\n  std::rotate(c.begin(), new_begin, c.end());\n} \n```\n\n我们来看看使用`std::rotate()`的优势:\n\n*   算法不会抛出异常，因为它不会分配内存(尽管包含的对象可能会抛出异常)\n*   它适用于尺寸无法改变的容器，如`std::array`\n*   性能是 *O(n)* 不管它在哪个容器上运行\n*   考虑到特定的硬件，该实现很可能得到优化\n\n也许你会觉得`for`循环和标准算法之间的这个比较不公平，因为这个问题还有其他既优雅又高效的解决方案。然而，在现实世界中，当标准库中有算法在等待解决你的问题时，看到像你刚刚看到的实现是很常见的。\n\n### 示例 3:利用标准库优化\n\n最后一个例子强调了这样一个事实，即使看起来非常简单的算法也可能包含您不会考虑的优化。我们来看看`std::find()`吧，比如。一眼看去，明显的实现再优化不过了。以下是`std::find()`算法的可能实现:\n\n```cpp\ntemplate <typename It, typename Value>\nauto find_slow(It first, It last, const Value& value) {\n  for (auto it = first; it != last; ++ it)\n    if (*it == value)\n      return it;\n  return last;\n} \n```\n\n然而，纵观 GNU libstdc++ 实现，当与`random_access_iterator`(换句话说，`std::vector`、`std::string`、`std::deque`和`std::array`一起使用时，libc++ 实现者已经一次将主循环展开成四个循环的块，导致比较(`it != last`)被执行四分之一次。\n\n以下是取自 libstdc++ 库的`std::find()`优化版本:\n\n```cpp\ntemplate <typename It, typename Value>\nauto find_fast(It first, It last, const Value& value) {\n  // Main loop unrolled into chunks of four\n  auto num_trips = (last - first) / 4;\n  for (auto trip_count = num_trips; trip_count > 0; --trip_count) {\n    if (*first == value) {return first;} ++ first;\n    if (*first == value) {return first;} ++ first;\n    if (*first == value) {return first;} ++ first;\n    if (*first == value) {return first;} ++ first;\n  }\n  // Handle the remaining elements\n  switch (last - first) {\n    case 3: if (*first == value) {return first;} ++ first;\n    case 2: if (*first == value) {return first;} ++ first;\n    case 1: if (*first == value) {return first;} ++ first;\n    case 0:\n    default: return last;\n  }\n} \n```\n\n请注意，实际上是`std::find_if()`而不是`std::find()`利用了这种循环展开优化。但是`std::find()`是用`std::find_if()`实现的。\n\n除了`std::find()`之外，libstdc++ 中的大量算法都是使用`std::find_if()`来实现的，例如`any_of()`、`all_of()`、`none_of()`、`find_if_not()`、`search()`、`is_partitioned()`、`remove_if()`和`is_permutation()`，这意味着所有这些都比手工`for`循环稍快一些。\n\n我所说的轻微，实际上是指轻微；加速约为 1.07 倍，如下表所示:\n\n<colgroup><col> <col> <col></colgroup> \n| 在 10，000，000 个元素的`std::vector`中找到一个整数 |\n| 算法 | 时间 | 加速 |\n| `find_slow()` | 3.06 毫秒 | 1.00 倍 |\n| `find_fast()` | 3.26 毫秒 | 1.07x |\n\n表 5.5: find_fast()使用 libstdc++ 中的优化。基准测试显示，find_fast()比 find_slow()稍快。\n\n然而，即使好处几乎可以忽略不计，使用标准算法，你可以免费得到它。\n\n#### “与零相比”优化\n\n除了循环展开，一个非常微妙的优化是`trip_count`向后迭代，以便与零而不是值进行比较。在一些 CPU 上，与零比较比任何其他值稍快，因为它使用另一个汇编指令(在 x86 平台上，它使用`test`而不是`cmp`)。\n\n下表显示了使用 gcc 9.2 时组件输出的差异:\n\n<colgroup><col> <col> <col></colgroup> \n| 行动 | C++ | 组装 x86 |\n| 与零比较 | \n\n```cpp\nauto cmp_zero(size_t val) {\n  return val > 0;\n} \n```\n\n | \n\n```cpp\ntest edi, edi\nsetne al\nret \n```\n\n |\n| 与其他值进行比较 | \n\n```cpp\nauto cmp_val(size_t val) {\n  return val > 42;\n} \n```\n\n | \n\n```cpp\ncmp edi, 42\nsetba al\nret \n```\n\n |\n\n<figure class=\"mediaobject\">Table 5.6: The difference in assembly output</figure>\n\n即使在标准库实现中鼓励这种优化，也不要为了从这种优化中获益而重新排列手工循环，除非这是一个(非常)热点。这样做会严重降低代码的可读性；让算法来处理这类优化。\n\n这是我关于使用算法而不是`for`循环的建议的结尾。如果你还没有使用标准算法，我希望我已经给了你一些论据来说服你试一试。现在我们将继续讨论我关于有效使用算法的最后一个建议。\n\n## 避免容器副本\n\n我们将通过突出一个在试图组合来自算法库的多个算法时的常见问题来结束这一章:很难避免底层容器的不必要的副本。\n\n举个例子可以澄清我在这里的意思。假设我们有某种类型的`Student`类来代表特定年份和特定考试分数的学生，如下所示:\n\n```cpp\nstruct Student {\n  int year_{};\n  int score_{};\n  std::string name_{};\n  // ...\n}; \n```\n\n如果我们想在一大群学生中找到第二年分数最高的学生，我们可能会在`score_`上使用`max_element()`，但是由于我们只想考虑第二年的学生，这就变得棘手了。本质上，我们想从`copy_if()`和`max_element()`的组合中合成一个新算法，但是使用算法库合成算法是不可能的。相反，我们必须将第二年的所有学生复制到一个新容器中，然后迭代新容器以找到最高分数:\n\n```cpp\nauto get_max_score(const std::vector<Student>& students, int year) {\n  auto by_year = [=](const auto& s) { return s.year_ == year; }; \n  // The student list needs to be copied in\n  // order to filter on the year\n  auto v = std::vector<Student>{};\n  std::ranges::copy_if(students, std::back_inserter(v), by_year);\n  auto it = std::ranges::max_element(v, std::less{}, &Student::score_);\n  return it != v.end() ? it->score_ : 0; \n} \n```\n\n这是其中一个的地方，很容易从零开始编写自定义算法，而不利用标准算法。但是，正如您将在下一章中看到的，没有必要为这样的任务放弃标准库。编写算法的能力是使用 Ranges 库的主要动机之一，我们将在下一篇文章中介绍。\n\n# 摘要\n\n在本章中，您学习了如何使用算法库中的基本概念，将它们用作构建块而不是手写`for`-循环的优势，以及为什么使用标准算法库有利于在后期优化您的代码。我们还讨论了标准算法的保证和权衡，这意味着从现在开始，您可以放心地使用它们。\n\n通过使用算法而不是手动`for`循环的优势，您的代码库已经为并行化技术做好了充分的准备，这些技术将在本书的后续章节中讨论。标准算法缺少的一个关键特性是组合算法的可能性，当我们试图避免不必要的容器副本时，这一点得到了强调。在下一章中，您将学习如何使用 C++ Ranges 库中的视图来克服标准算法的这一限制。"
  },
  {
    "path": "docs/cpp-hiperf/06.md",
    "content": "# 六、范围和视图\n\n本章将从上一章关于算法及其局限性的地方继续。范围库中的视图是算法库的强大补充，它允许我们在一系列元素上将多个转换组合成一个延迟的评估视图。阅读本章后，您将了解什么是范围视图，以及如何将它们与标准库中的容器、迭代器和算法结合使用。\n\n具体来说，我们将涵盖以下主要主题:\n\n*   算法的可组合性\n*   范围适配器\n*   将视图具体化为容器\n*   生成、转换和采样范围内的元素\n\n在我们进入 Ranges 库之前，让我们讨论一下为什么它被添加到 C++ 20 中，以及为什么我们想要使用它。\n\n# Ranges 库的动机\n\n随着将 Ranges 库引入 C++ 20，我们在实现算法时如何从标准库中获益得到了一些重大改进。以下列表显示了新功能:\n\n*   定义迭代器和范围需求的概念现在可以被编译器更好地检查，并在开发过程中提供更多帮助\n*   `<algorithm>`头中所有函数的新重载都被刚才提到的概念所约束，并接受范围作为参数，而不是迭代器对\n*   迭代器头中的受约束迭代器\n*   范围视图，使合成算法成为可能\n\n本章将集中讨论最后一项:视图的概念，它允许我们编写算法来避免将数据不必要地复制到拥有的容器中。为了充分理解这一点的重要性，让我们从演示算法库中缺乏可组合性开始。\n\n## 算法库的局限性\n\n标准库算法缺少一个基本方面:可组合性。让我们通过查看第 5 章**算法*中的最后一个例子来研究这是什么意思，我们在这里对此进行了简要讨论。如果你还记得的话，我们有一节课要用一个特定的考试分数来代表一个特定年份的`Student`:*\n\n```cpp\nstruct Student {\n  int year_{};\n  int score_{};\n  std::string name_{};\n  // ...\n}; \n```\n\n如果我们想从一大群第二年的学生中找到最高分，我们可能会在`score_`上使用`max_element()`，但由于我们只想考虑特定年份的学生，这变得很棘手。通过使用既接受范围又接受投影的新算法(参考*第 5 章*、*算法*，我们可能会得出如下结论:\n\n```cpp\nauto get_max_score(const std::vector<Student>& students, int year) {\n  auto by_year = [=](const auto& s) { return s.year_ == year; }; \n  // The student list needs to be copied in\n  // order to filter on the year\n  auto v = std::vector<Student>{};\n  std::ranges::copy_if(students, std::back_inserter(v), by_year);\n  auto it = std::ranges::max_element(v, std::less{}, &Student::score_);\n  return it != v.end() ? it->score_ : 0; \n} \n```\n\n下面是如何使用它的一个例子:\n\n```cpp\nauto students = std::vector<Student>{\n  {3, 120, \"Niki\"},\n  {2, 140, \"Karo\"},\n  {3, 190, \"Sirius\"},\n  {2, 110, \"Rani\"},\n   // ...\n};\nauto score = get_max_score(students, 2);\nstd::cout << score << '\\n'; \n// Prints 140 \n```\n\n`get_max_score()`的这种实现很容易理解，但是在使用`copy_if()`和`std::back_inserter()`时会产生不必要的`Student`对象的副本。\n\n你现在可能在想`get_max_score()`可以写成一个简单的`for-`循环，这可以减轻我们由于`copy_if()`带来的额外分配:\n\n```cpp\nauto get_max_score(const std::vector<Student>& students, int year) {\n  auto max_score = 0;\n  for (const auto& student : students) {\n    if (student.year_ == year) {\n      max_score = std::max(max_score, student.score_);\n    }\n  }\n  return max_score;\n} \n```\n\n虽然这在这个小例子中很容易实现，但我们希望能够通过组成小的算法构建块来实现这个算法，而不是使用单个`for`循环从头开始实现它。\n\n我们想要的是像使用算法一样可读的语法，但是能够避免为算法中的每一步构建新的容器。这就是范围库中的视图发挥作用的地方。虽然范围库包含的不仅仅是视图，但与算法库的主要区别是能够将本质上是不同类型的迭代器组成一个延迟的求值范围。\n\n如果前面的示例是使用范围库中的视图编写的，那么它会是这样的:\n\n```cpp\nauto max_value(auto&& range) {\n  const auto it = std::ranges::max_element(range);\n  return it != range.end() ? *it : 0;\n}\nauto get_max_score(const std::vector<Student>& students, int year) {\n  const auto by_year = [=](auto&& s) { return s.year_ == year; };\n  return max_value(students \n    | std::views::filter(by_year)\n    | std::views::transform(&Student::score_));\n} \n```\n\n现在我们回到使用算法，因此可以避免可变变量、`for`-循环和`if`-语句。在我们最初的例子中，额外的向量在特定的一年里吸引了学生，现在已经被消除了。相反，我们构建了一个范围视图，它代表了所有被`by_year`谓词过滤的学生，然后转换以仅显示分数。该视图然后被传递给一个小的实用函数`max_value()`，该函数使用`max_element()`算法来比较所选学生的分数，以便找到最大值。\n\n这种通过将算法链接在一起并同时避免不必要的复制来组合算法的方式促使我们开始使用范围库中的视图。\n\n# 理解范围库中的视图\n\n范围库中的视图是一个范围内的延迟评估迭代。从技术上来说，它们只是带有内置逻辑的迭代器，但是从语法上来说，它们为许多常见的操作提供了非常令人愉快的语法。\n\n以下是如何使用视图对向量中的每个数字进行平方(通过迭代)的示例:\n\n```cpp\nauto numbers = std::vector{1, 2, 3, 4};\nauto square = [](auto v) {  return v * v; };\nauto squared_view = std::views::transform(numbers, square);\nfor (auto s : squared_view) {  // The square lambda is invoked here\n  std::cout << s << \" \";\n}\n// Output: 1 4 9 16 \n```\n\n变量`squared_view`不是数值平方的`numbers`向量的副本；它是数字的代理对象，只有一个细微的区别——每次你访问一个元素时，都会调用`std::transform()`函数。这就是为什么我们说一个视图是延迟求值的。\n\n从外部来看，您仍然可以像任何常规容器一样迭代`squared_view`，因此，您可以执行常规算法，如`find()`或`count()`，但是，在内部，您没有创建另一个容器。\n\n如果要存储范围，可以使用`std::ranges::copy()`将视图物化到一个容器中。(这将在本章后面演示。)一旦视图被复制回一个容器，原始的和转换后的容器之间就不再有任何依赖关系。\n\n有了范围，也可以创建一个过滤视图，其中只有一部分范围是可见的。在这种情况下，迭代视图时，只有满足条件的元素才可见:\n\n```cpp\nauto v = std::vector{4, 5, 6, 7, 6, 5, 4};\nauto odd_view = \n  std::views::filter(v, [](auto i){ return (i % 2) == 1; });\nfor (auto odd_number : odd_view) {\n  std::cout << odd_number << \" \";\n}\n// Output: 5 7 5 \n```\n\nRanges 库多功能性的另一个例子是它提供了创建一个视图的可能性，该视图可以遍历几个容器，就像它们是一个列表一样:\n\n```cpp\nauto list_of_lists = std::vector<std::vector<int>> {\n  {1, 2},\n  {3, 4, 5},\n  {5},\n  {4, 3, 2, 1}\n};\nauto flattened_view = std::views::join(list_of_lists);\nfor (auto v : flattened_view) \n  std::cout << v << \" \";\n// Output: 1 2 3 4 5 5 4 3 2 1\n\nauto max_value = *std::ranges::max_element(flattened_view);\n// max_value is 5 \n```\n\n现在，我们已经简要地看了一些使用视图的例子，让我们检查所有视图的共同需求和属性\n\n## 视图是可组合的\n\n观点的全部力量来自于结合它们的能力。因为它们不复制实际数据，所以您可以在数据集上表达多个操作，而在内部，只能迭代一次。为了理解视图是如何组成的，让我们看看我们最初的例子，但是不使用管道操作符来组成视图；相反，让我们直接构造实际的视图类。以下是它的外观:\n\n```cpp\nauto get_max_score(const std::vector<Student>& s, int year) {\n  auto by_year = [=](const auto& s) { return s.year_ == year; };\n\n  auto v1 = std::ranges::ref_view{s}; // Wrap container in a view\n  auto v2 = std::ranges::filter_view{v1, by_year};\n  auto v3 = std::ranges::transform_view{v2, &Student::score_};\n  auto it = std::ranges::max_element(v3);\n  return it != v3.end() ? *it : 0;\n} \n```\n\n我们首先创建一个`std::ranges::ref_view`，它是一个容器周围的薄包装。在我们的例子中，它把向量`s`变成了一个廉价复制的视图。我们需要这个，因为我们的下一个视图`std::ranges::filter_view`，需要一个视图作为它的第一个参数。如您所见，我们通过引用链中的前一个视图来构建下一个视图。\n\n当然，这个可组合视图链可以任意变长。算法`max_element()`不需要了解完整的链；它只需要迭代范围`v3`，因为它是一个普通的容器。\n\n下图是 `max_element()`算法、视图和输入容器之间关系的简化视图:\n\n<figure class=\"mediaobject\">![](img/B15619_06_01.png)</figure>\n\n图 6.1:顶层算法 std::ranges::max_element()，从视图中提取值，这些视图从底层容器(std::vector)中轻松处理元素\n\n现在，撰写视图的这种风格有点啰嗦，如果我们试图删除中间变量`v1`和`v2`，我们最终会得到这样的结果:\n\n```cpp\nusing namespace std::ranges; // _view classes live in std::ranges\nauto scores = \n  transform_view{filter_view{ref_view{s}, by_year},\n    &Student::score_}; \n```\n\n现在，这可能看起来在语法上并不优雅。通过去掉中间变量，我们得到了即使训练有素的人也很难读懂的东西。我们还被迫从内到外阅读代码来理解依赖关系。幸运的是，范围库为我们提供了范围适配器，这是组合视图的首选方式。\n\n## 范围视图配有范围适配器\n\n正如您之前看到的，Ranges 库还允许我们使用范围适配器和管道操作符来构建视图，以获得更优雅的语法(您将在*第 10 章*、*代理对象和延迟求值*中了解更多关于在自己的代码中使用管道操作符的信息)。前面的代码示例可以通过使用 range adaptor 对象来重写，我们将得到如下内容:\n\n```cpp\nusing namespace std::views; // range adaptors live in std::views\nauto scores = s | filter(by_year) | transform(&Student::score_); \n```\n\n从左到右而不是从里到外阅读语句的能力使代码更容易阅读。如果您使用过 Unix shell，您可能对链接命令的这种符号很熟悉。\n\n范围库中的每个视图都有一个相应的范围适配器对象，可以与管道操作器一起使用。在使用范围适配器时，我们也可以跳过额外的`std::ranges::ref_view`，因为范围适配器直接与`viewable_ranges`一起工作，即可以安全转换成`view`的范围。\n\n您可以将范围适配器视为一个全局无状态对象，它实现了两个功能:`operator()()`和`operator|()`。这两个函数都构造和返回视图对象。管道操作符就是前面例子中使用的。但是也可以使用 call 运算符，使用带有括号的嵌套语法来形成视图，如下所示:\n\n```cpp\nusing namespace std::views;\nauto scores = transform(filter(s, by_year), &Student::score_); \n```\n\n同样，当使用范围适配器时，不需要将输入容器包装在`ref_view`中。\n\n总的来说，范围库中的每个视图都包括:\n\n*   对视图对象进行操作的类模板(实际视图类型)，例如`std::ranges::transform_view`。这些视图类型可以在名称空间`std::ranges`下找到。\n*   从范围创建视图类实例的范围适配器对象，例如`std::views::transform`。所有范围适配器都实现了`operator()()`和`operator|()`，这使得使用管道操作符或通过嵌套进行转换成为可能。范围适配器对象位于名称空间`std::views`下。\n\n## 视图是具有复杂性保证的非自有范围\n\n在前一章中，引入了范围的概念。任何提供函数`begin()`和`end()`的类型，其中`begin()`返回一个迭代器，`end()`返回一个哨兵，都有资格作为一个范围。我们得出结论，所有标准容器都是范围。容器拥有自己的元素，因此我们可以称之为拥有范围。\n\n视图也是一个范围，即提供`begin()`和`end()`功能。但是，与容器不同的是，视图不拥有视图所覆盖范围内的元素。\n\n视图的构造需要是一个恒定时间的操作， *O(1)* 。它不能执行任何依赖于底层容器大小的工作。这同样适用于分配、复制、移动和析构视图。这使得在使用视图组合多种算法时，很容易对性能进行推理。这也使得视图不可能拥有元素，因为这将需要线性时间复杂性的建设和破坏。\n\n## 视图不会改变底层容器\n\n乍一看，视图可能看起来像是输入容器的变异版本。然而，容器根本没有变化:所有的处理都是在迭代器中执行的。视图只是一个代理对象，当迭代时，*看起来像一个变异的容器。*\n\n这也使得视图可以公开不同于输入元素类型的元素类型。下面的代码片段演示了视图如何将元素类型从`int`转换为`std::string`:\n\n```cpp\nauto ints = std::list{2, 3, 4, 2, 1};\nauto strings = ints \n  | std::views::transform([](auto i) { return std::to_string(i); }); \n```\n\n也许我们有一个在容器上运行的函数，我们希望使用范围算法对其进行转换，然后我们希望将其返回并存储回容器中。例如，在上面的例子中，我们可能希望将字符串实际存储在一个单独的容器中。在下一节中，您将学习如何做到这一点。\n\n## 视图可以具体化为容器\n\n有时候，我们想把视图存放在一个容器里，也就是**物化**视图。所有视图都可以物化到容器中，但这并不像您希望的那样容易。为 C++ 20 提出了一个名为`std::ranges::to<T>()`的函数模板，它可以将视图转换成任意的容器类型`T`，但并没有成功。希望我们能在未来的 C++ 版本中得到类似的东西。在此之前，我们需要自己做更多的工作来实现观点。\n\n在上例中，我们将`ints`转换为`std::strings`，如下所示:\n\n```cpp\nauto ints = std::list{2, 3, 4, 2, 1};\nauto r = ints \n  | std::views::transform([](auto i) { return std::to_string(i); }); \n```\n\n现在，如果我们想将范围`r`具体化为一个向量，我们可以这样使用`std::ranges::copy()`:\n\n```cpp\nauto vec = std::vector<std::string>{};\nstd::ranges::copy(r, std::back_inserter(vec)); \n```\n\n物化视图是一个常见的操作，所以如果我们有一个通用的实用程序来处理这种情况会很方便。说我们想把一些任意的视图物化成一个`std::vector`；我们可以使用一些通用编程来实现以下方便的实用功能:\n\n```cpp\nauto to_vector(auto&& r) {\n  std::vector<std::ranges::range_value_t<decltype(r)>> v;\n  if constexpr(std::ranges::sized_range<decltype(r)>) {\n    v.reserve(std::ranges::size(r));\n  }\n  std::ranges::copy(r, std::back_inserter(v));\n  return v;\n} \n```\n\n这个片段摘自帖木儿·杜姆勒的博文[https://Timur . audio/如何从 c20 范围](https://timur.audio/how-to-make-a-container-from-a-c20-range)制作容器，非常值得一读。\n\n在本书中，我们还没有过多地讨论泛型编程，但是接下来的几章将解释`auto`参数类型和`if constexpr`的使用。\n\n我们正在使用`reserve()`来优化该功能的性能。它将为该范围内的所有元素预分配足够的空间，以避免进一步分配。但是，如果我们知道范围的大小，我们只能调用`reserve()`，因此我们必须在编译时使用`if constexpr`语句来检查范围是否是`size_range`。\n\n有了这个实用工具，我们可以将某种类型的容器转换成保存另一种任意类型元素的向量。让我们看看如何使用`to_vector()`将整数列表转换为`std::strings`的向量。这里有一个例子:\n\n```cpp\nauto ints = std::list{2, 3, 4, 2, 1};\nauto r = ints \n  | std::views::transform([](auto i) { return std::to_string(i); });\nauto strings = to_vector(r); \n// strings is now a std::vector<std::string> \n```\n\n请记住，一旦视图被复制回容器，原始容器和转换后的容器之间就不再有任何依赖关系。这也意味着物化是一个急切的操作，而所有的视图操作都是延迟的。\n\n## 视图被偷懒评估\n\n由视图执行的所有工作都懒洋洋地发生。这与`<algorithm>`头中的函数相反，这些函数在被调用时会立即对所有元素执行工作。\n\n你已经看到`std::views::filter`视图可以代替`std::copy_if()`算法，`std::views::transform`视图可以代替`std::transform()`算法。当我们使用视图作为构建块并将它们链接在一起时，通过避免急切算法所需的容器元素的不必要的副本，我们从延迟求值中受益。\n\n但是`std::sort()`呢？有相应的排序视图吗？答案是否定的，因为它需要视图首先急切地收集所有元素，以便找到要返回的第一个元素。相反，我们必须通过在我们的视图上明确地调用 sort 来实现这一点。在大多数情况下，我们还需要在排序之前物化视图。我们可以用一个例子来澄清这一点。假设我们有一个被谓词过滤的数字向量，如下所示:\n\n```cpp\nauto vec = std::vector{4, 2, 7, 1, 2, 6, 1, 5};\nauto is_odd = [](auto i) { return i % 2 == 1; };\nauto odd_numbers = vec | std::views::filter(is_odd); \n```\n\n如果我们试图使用`std::ranges::sort()`或`std::sort()`对视图`odd_numbers`进行排序，我们会得到一个编译错误:\n\n```cpp\nstd::ranges::sort(odd_numbers); // Doesn't compile \n```\n\n编译器抱怨`odd_numbers`范围提供的迭代器类型。排序算法需要随机访问迭代器，但这不是我们视图提供的迭代器类型，即使底层输入容器是`std::vector`。我们需要做的是在排序之前物化视图:\n\n```cpp\nauto v = to_vector(odd_numbers);\nstd::ranges::sort(v);\n// v is now 1, 1, 5, 7 \n```\n\n但是为什么有这个必要呢？答案是这是延迟评价的结果。当评估需要通过一次读取一个元素来偷懒时，过滤器视图(和许多其他视图)不能保留底层范围的迭代器类型(在本例中为`std::vector`)。\n\n那么，有没有可以排序的视图呢？是的，一个例子是`std::views::take`，它返回一个范围内的第一个 *n* 元素。以下示例编译并运行良好，无需在排序前具体化视图:\n\n```cpp\nauto vec = std::vector{4, 2, 7, 1, 2, 6, 1, 5};\nauto first_half = vec | std::views::take(vec.size() / 2);\nstd::ranges::sort(first_half);\n// vec is now 1, 2, 4, 7, 2, 6, 1, 5 \n```\n\n迭代器的质量得到了保留，因此可以对`first_half`视图进行排序。最终结果是基础向量`vec`中的前半部分元素已经排序。\n\n您现在已经很好地理解了范围库中的视图是什么以及它们是如何工作的。在下一节中，我们将探讨如何使用标准库中包含的视图。\n\n# 标准库中的视图\n\n到目前为止，在本章中，我们已经讨论了范围库中的视图。正如前面所描述的，这些视图类型需要在恒定时间内构建，并且还具有恒定时间复制、移动和赋值操作符。然而，在 C++ 中，在将 Ranges 库添加到 C++ 20 之前，我们已经讨论过视图类。这些视图类是非拥有类型，就像`std::ranges::view`一样，但是没有复杂度保证。\n\n在本节中，我们将从探索范围库中与`std::ranges::view`概念相关联的视图开始，然后进入`std::string_view`和`std::span`，它们与`std::ranges::view`无关。\n\n## 范围视图\n\nRanges 库中已经有多个视图了，我想在未来的 C++ 版本中我们会看到更多的视图。本节将快速概述一些可用的视图，并根据它们的功能将它们分为不同的类别。\n\n### 生成视图\n\n生成视图产生值。它们可以生成有限或无限范围的值。这一类别中最明显的例子是`std::views::iota`，它产生的值在半开范围内。以下代码片段打印了值`-2`、`-1`、`0`和`1`:\n\n```cpp\nfor (auto i : std::views::iota(-2, 2)) {\n  std::cout << i << ' ';\n}\n// Prints -2 -1 0 1 \n```\n\n通过省略第二个参数，`std::views::iota`将根据请求产生无限多个值。\n\n### 转换视图\n\n变换视图是变换范围元素或范围本身的结构的视图。一些例子包括:\n\n*   `std::views::transform`:转换每个元素的值和/或类型\n*   `std::views::reverse`:返回输入范围的反转版本\n*   `std::views::split`:将一个元素拆开，将每个元素拆分为一个子范围。结果范围是一系列范围\n*   `std::views::join`:分裂的反义词；展平所有子范围\n\n以下示例使用`split`和`join`从逗号分隔值的字符串中提取所有数字:\n\n```cpp\nauto csv = std::string{\"10,11,12\"};\nauto digits = csv \n  | std::views::split(',')      // [ [1, 0], [1, 1], [1, 2] ]\n  | std::views::join;           // [ 1, 0, 1, 1, 1, 2 ]\nfor (auto i : digits) {   std::cout << i; }\n// Prints 101112 \n```\n\n### 抽样视图\n\n采样视图是选择一个范围内元素子集的视图，例如:\n\n*   `std::views::filter`:只返回满足所提供谓词的元素\n*   `std::views::take`:返回一个范围的第一个元素 *n*\n*   `std::views::drop`:删除第一个 *n* 元素后，返回一个范围内的所有剩余元素\n\n你在本章中已经看到了大量使用`std::views::filter`的例子；这是一个非常有用的观点。`std::views::take`和`std::views::drop`都有`_while`版本，接受谓词而不是号。下面是一个使用`take`和`drop_while`的例子:\n\n```cpp\n auto vec = std::vector{1, 2, 3, 4, 5, 4, 3, 2, 1};\n auto v = vec\n   | std::views::drop_while([](auto i) { return i < 5; })\n   | std::views::take(3);\n for (auto i : v) { std::cout << i << \" \"; }\n // Prints 5 4 3 \n```\n\n本示例使用`drop_while`来丢弃前面小于 5 的值。剩余的元素传递给`take`，T1 返回前三个元素。现在进入最后一类范围视图。\n\n### 实用程序视图\n\n您已经在本章中看到了一些实用程序视图的作用。当你有一些你想转换或当作一种观点的东西时，它们就派上用场了。这类视图中的一些示例有`ref_view`、`all_view`、`subrange`、`counted`和`istream_view`。\n\n下面的示例向您展示了如何读取带有浮点数的文本文件，然后打印它们。\n\n假设我们有一个名为`numbers.txt`的文本文件，其中充满了重要的浮点数，如下所示:\n\n```cpp\n1.4142 1.618 2.71828 3.14159 6.283 ... \n```\n\n然后我们可以通过使用`std::ranges::istream_view`来创建`floats`的视图:\n\n```cpp\nauto ifs = std::ifstream(\"numbers.txt\");\nfor (auto f : std::ranges::istream_view<float>(ifs)) {\n  std::cout << f << '\\n';\n}\nifs.close(); \n```\n\n通过创建一个`std::ranges::istream_view`并将其传递给一个`istream`对象，我们有了一种简洁的方式来处理来自文件或任何其他输入流的数据。\n\nRanges 库中的视图经过精心选择和设计。在即将发布的标准版本中，很可能会有更多这样的版本。意识到不同类别的视图有助于我们将它们区分开来，并在我们需要时使它们易于找到。\n\n## 重温标准::字符串 _ 视图和标准::span\n\n值得注意的是标准库为我们提供了范围库之外的其他视图。在*第 4 章*、*数据结构*中介绍的`std::string_view`和`std::span`都是非自有范围，非常适合与范围视图结合使用。\n\n不能保证这些视图可以在恒定的时间内构建，范围库中的视图就是这种情况。例如，从空终止的 C 风格字符串构造`std::string_view`可以调用`strlen()`，这是一个 *O(n)* 操作。\n\n假设，出于某种原因，我们有一个函数可以重置某个范围内的第一个`n`值:\n\n```cpp\nauto reset(std::span<int> values, int n) {\n  for (auto& i : std::ranges::take_view{values, n}) {\n    i = int{};\n  }\n} \n```\n\n在这种情况下，不需要使用带有`values`的范围适配器，因为`values`已经是视图。通过使用`std::span`，我们可以传递两个内置数组或一个容器，如`std::vector`:\n\n```cpp\nint a[]{33, 44, 55, 66, 77};\nreset(a, 3); \n// a is now [0, 0, 0, 66, 77]\nauto v = std::vector{33, 44, 55, 66, 77};\nreset(v, 2); \n// v is now [0, 0, 55, 66, 77] \n```\n\n类似地，我们可以将`std::string_view`与 Ranges 库一起使用。下面的函数将`std::string_view`的内容拆分为`std::string`元素的`std::vector`:\n\n```cpp\nauto split(std::string_view s, char delim) {\n  const auto to_string = [](auto&& r) -> std::string {\n    const auto cv = std::ranges::common_view{r};\n    return {cv.begin(), cv.end()};\n  };\n  return to_vector(std::ranges::split_view{s, delim} \n    | std::views::transform(to_string));\n} \n```\n\nλ`to_string`将一系列`char`转化为`std::string`。`std::string`构造函数需要相同的迭代器和哨兵类型，因此，范围被包装在一个`std::ranges::common_view`中。实用程序`to_vector()`将视图具体化，并返回一个`std::vector<std::string>`。 `to_vector()`在本章前面已经定义过了。\n\n我们的`split()`功能现在可以同时用于`const char*`弦和`std::string`物体，如下所示:\n\n```cpp\n const char* c_str = \"ABC,DEF,GHI\";  // C style string\n  const auto v1 = split(c_str, ',');  // std::vector<std::string>\n  const auto s = std::string{\"ABC,DEF,GHI\"};\n  const auto v2 = split(s, ',');      // std::vector<std::string>\n  assert(v1 == v2);                   // true \n```\n\n现在，我们将在这一章结束时，稍微讨论一下在 C++ 的未来版本中，我们期望在 Ranges 库中看到什么。\n\n# 范围库的未来\n\n在 C++ 20 中被接受的 Ranges 库是基于 Eric Niebler 创作的一个库，可在[https://github.com/ericniebler/range-v3](https://github.com/ericniebler/range-v3)获得。目前，这个库的组件中只有一小部分进入了标准，但很可能很快会添加更多的东西。\n\n除了许多尚未被接受的有用视图，如`group_by`、`zip`、`slice`、`unique`之外，还有**动作**的概念，可以像视图一样管道化。然而，行动不是像视图一样被延迟地评估，而是执行范围的急切突变。排序是一个典型动作的例子。\n\n如果您等不及将这些功能添加到标准库中，我建议您看一看 range-v3 库。\n\n# 摘要\n\n本章介绍了使用范围视图构建算法背后的一些动机。通过使用视图，我们可以使用管道操作符以简洁的语法高效地编写算法。您还了解了类是视图意味着什么，以及如何使用范围适配器将范围转换成视图。\n\n视图没有自己的元素。构建一个范围视图需要一个恒定的时间操作，并且所有视图都是延迟计算的。您已经看到了如何将容器转换成视图，以及如何将视图物化回拥有的容器的例子。\n\n最后，我们简要概述了标准库附带的视图，以及 C++ 中范围的可能未来。\n\n本章是关于容器、迭代器、算法和范围系列的最后一章。我们现在将继续讨论 C++ 中的内存管理。*"
  },
  {
    "path": "docs/cpp-hiperf/07.md",
    "content": "# 七、内存管理\n\n读完前面几章，我们处理内存的方式会对性能产生巨大影响，这应该不再令人惊讶。中央处理器花费大量时间在中央处理器寄存器和主存储器之间洗牌(向主存储器加载数据和从主存储器存储数据)。如*第四章*、*数据结构*所示，CPU 使用内存缓存来加速对内存的访问，程序需要缓存友好才能快速运行。\n\n本章将揭示计算机如何使用内存的更多方面，以便您知道在调整内存使用时必须考虑哪些因素。此外，本章还包括:\n\n*   自动内存分配和动态内存管理。\n*   C++ 对象的生命周期以及如何管理对象所有权。\n*   高效的内存管理。有时，硬内存限制迫使我们保持数据表示紧凑，有时，我们有足够的可用内存，但需要程序通过提高内存管理效率来加快速度。\n*   如何最小化动态内存分配？分配和释放动态内存相对昂贵，有时，我们需要避免不必要的分配，以使程序运行得更快。\n\n在深入研究 C++ 内存管理之前，我们将从解释一些您需要理解的概念开始这一章。本介绍将解释虚拟内存和虚拟地址空间、堆栈内存与堆内存、分页和交换空间。\n\n# 计算机存储器\n\n计算机的物理内存由系统上运行的所有进程共享。如果一个进程占用大量内存，其他进程很可能会受到影响。但是从程序员的角度来看，我们通常不必担心其他进程正在使用的内存。内存的这种隔离是因为当今大多数操作系统都是**虚拟内存**操作系统，这给人一种错觉，以为一个进程拥有自己所有的内存。每个进程都有自己的**虚拟地址空间**。\n\n## 虚拟地址空间\n\n程序员看到的虚拟地址空间中的地址被操作系统和作为处理器一部分的**内存管理单元** ( **MMU** )映射到物理地址。这种映射或转换在我们每次访问内存地址时都会发生。\n\n这种额外的间接层使得操作系统可以将物理内存用于当前正在使用的进程部分，并将剩余的虚拟内存备份到磁盘上。从这个意义上说，我们可以将物理主内存视为虚拟内存空间的缓存，虚拟内存空间位于辅助存储上。二级存储器中用于备份内存页面的区域通常是称为**交换空间**、**交换文件**或简称**页面文件**，具体取决于操作系统。\n\n虚拟内存使进程可以拥有比物理地址空间更大的虚拟地址空间，因为不使用的虚拟内存不必占用物理内存。\n\n## 内存页面\n\n如今实现虚拟内存最常见的方法是将地址空间划分为个固定大小的块，称为**内存页**。当进程访问虚拟地址的内存时，操作系统会检查内存页面是否有物理内存(页面框架)支持。如果内存页面没有映射到主内存中，就会发生硬件异常，页面会从磁盘加载到内存中。这种类型的硬件异常被称为**页面故障**。这不是一个错误，而是将数据从磁盘加载到内存所必需的中断。但是，正如您可能已经猜到的，与读取已经驻留在内存中的数据相比，这非常慢。\n\n当主内存中没有可用的页面框架时，必须逐出一个页面框架。如果要收回的页面是脏的，也就是说，自从上次从磁盘加载以来，它已经被修改过，则需要先将其写入磁盘，然后才能替换它。这个机制叫做**寻呼**。如果该内存页没有被修改，则该内存页被简单地逐出。\n\n并非所有支持虚拟内存的操作系统都支持分页。例如，iOS 确实有虚拟内存，但脏页永远不会存储在磁盘上；只有干净的页面才能从内存中被逐出。如果主内存已满，iOS 将开始终止进程，直到再次有足够的可用内存。安卓也采用了类似的策略。不将内存页面写回移动设备的闪存的一个原因是它会耗尽电池，并且还会缩短闪存本身的寿命。\n\n下图显示了两个正在运行的进程。它们都有自己的虚拟内存空间。有些页面被映射到物理内存，而有些则没有。如果进程 1 需要使用从地址 0x1000 开始的内存页面中的内存，则会发生页面错误。然后，内存页面将被映射到一个空闲的内存帧。另外，请注意虚拟内存地址与物理地址不同。从虚拟地址 0x0000 开始的进程 1 的第一个内存页被映射到从物理地址 0x4000 开始的内存帧:\n\n<figure class=\"mediaobject\">![](img/B15619_07_01.png)</figure>\n\n图 7.1:虚拟内存页面，映射到物理内存中的内存帧。未使用的虚拟内存页面不必占用物理内存。\n\n## 痛打\n\n**当系统物理内存不足，因此不断分页时，可能会发生系统颠簸**。每当一个进程被安排在中央处理器上的时间，它就试图访问已经被调出的内存。加载新的内存页面意味着其他页面必须首先存储在磁盘上。在磁盘和内存之间来回移动数据通常非常慢；在某些情况下，这或多或少会使计算机停顿，因为系统将所有时间都花在分页上。查看系统的页面故障频率是确定程序是否已经开始颠簸的好方法。\n\n在优化性能时，了解硬件和操作系统如何处理内存的基本知识非常重要。接下来，我们将看到在 C++ 程序执行期间内存是如何处理的。\n\n# 进程内存\n\n堆栈和堆是 C++ 程序中最重要的两个内存段。还有也是静态存储和线程本地存储，不过这个我们后面会多讲。实际上，形式上正确的说，C++ 不谈栈和堆；相反，它讨论了免费存储、存储类和对象的存储持续时间。然而，由于堆栈和堆的概念在 C++ 社区中被广泛使用，并且我们知道的所有 C++ 实现都使用堆栈来实现函数调用和管理局部变量的自动存储，所以理解什么是堆栈和堆是很重要的。\n\n在本书中，我还将使用术语*堆栈*和*堆*而不是对象的存储持续时间。我将交替使用*堆*和*自由商店*这两个术语，不会对它们做任何区分。\n\n堆栈和堆都驻留在进程的虚拟内存空间中。栈是所有局部变量驻留的地方；这也包括函数的参数。每次调用函数时，堆栈都会增长，当函数返回时，堆栈会收缩。每个线程都有自己的堆栈，因此堆栈内存可以被认为是线程安全的。另一方面，堆是一个全局内存区域，由正在运行的进程中的所有线程共享。当我们用`new`(或 C 库函数`malloc()`和`calloc()`)分配内存时，堆增长，当我们用`delete`(或`free()`)释放内存时，堆收缩。通常，堆从低地址开始向上增长，而堆栈从高地址开始向下增长。*图 7.2* 显示了堆栈和堆如何在虚拟地址空间中以相反的方向增长:\n\n<figure class=\"mediaobject\">![](img/B15619_07_02.png)</figure>\n\n图 7.2:一个进程的地址空间。堆栈和堆向相反的方向增长。\n\n接下来的部分将提供更多关于堆栈和堆的细节，并解释我们在编写的 C++ 程序中何时使用这些内存区域。\n\n## 栈存储器\n\n与堆相比，堆栈在许多方面不同。这里是堆栈的一些独特属性:\n\n*   堆栈是一个连续的内存块。\n*   它有一个固定的最大尺寸。如果程序超过最大堆栈大小，程序将崩溃。这种情况称为堆栈溢出。\n*   堆栈内存永远不会变得碎片化。\n*   从堆栈中分配内存(几乎)总是很快。页面错误是可能的，但很少。\n*   程序中的每个线程都有自己的堆栈。\n\n本节接下来的代码示例将研究其中的一些属性。让我们从分配和解除分配开始，了解堆栈在程序中是如何使用的。\n\n通过检查堆栈分配数据的地址，我们可以很容易地发现堆栈向哪个方向发展。下面的示例代码演示了在进入和离开函数时堆栈是如何增长和收缩的:\n\n```cpp\nvoid func1() {\n  auto i = 0;\n  std::cout << \"func1(): \" << std::addressof(i) << '\\n';\n}\nvoid func2() {\n  auto i = 0;\n  std::cout << \"func2(): \" << std::addressof(i) << '\\n';\n  func1();\n}\n\nint main() { \n  auto i = 0; \n  std::cout << \"main():  \" << std::addressof(i) << '\\n'; \n  func2();\n  func1(); \n} \n```\n\n运行程序时可能的输出如下所示:\n\n```cpp\nmain():  0x7ea075ac \nfunc2(): 0x7ea07594 \nfunc1(): 0x7ea0757c \nfunc1(): 0x7ea07594 \n```\n\n通过打印堆栈分配整数的地址，我们可以确定堆栈在我的平台上增长了多少以及向哪个方向增长。每当我们输入`func1()`或`func2()`时，堆栈就会增加 24 个字节。将在堆栈上分配的整数`i`为 4 字节长。剩下的 20 个字节包含函数结束时需要的数据，比如返回地址，也许还有一些对齐的填充。\n\n下图说明了在程序执行期间堆栈如何增长和收缩。第一个方框说明了程序刚进入`main()`功能时内存的样子。第二个框显示了当我们执行`func1()`时堆栈是如何增加的，以此类推:\n\n<figure class=\"mediaobject\">![](img/B15619_07_03.png)</figure>\n\n图 7.3:当输入函数时，栈增长并收缩\n\n为堆栈分配的总内存是在线程启动时创建的固定大小的连续内存块。那么，堆栈有多大，当我们到达堆栈的极限时会发生什么？\n\n如前所述，每次程序进入一个函数时，栈都会增长，当函数返回时，栈会收缩。每当我们在同一个函数中创建一个新的堆栈变量时，堆栈也会增长，当这样的变量超出范围时，堆栈就会收缩。堆栈溢出最常见的原因是深度递归调用和/或在堆栈上使用大型自动变量。堆栈的最大大小因平台而异，也可以针对单个进程和线程进行配置。\n\n让我们看看我们是否可以写一个程序来看看在我的系统上默认情况下堆栈有多大。我们将从编写一个函数`func()`开始，它将无限递归。在每个函数的开始，我们会分配一个 1 千字节的变量，每次进入`func()`时，这个变量都会被放入栈中。每次执行`func()`时，我们都会打印当前堆栈的大小:\n\n```cpp\nvoid func(std::byte* stack_bottom_addr) { \n  std::byte data[1024];     \n  std::cout << stack_bottom_addr - data << '\\n'; \n  func(stack_bottom_addr); \n} \n\nint main() { \n  std::byte b; \n  func(&b); \n} \n```\n\n栈的大小只是一个估计。我们通过从`func()`中定义的第一个局部变量中减去`main()`中第一个局部变量的地址来计算。\n\n当我用 Clang 编译代码时，我得到一个警告`func()`永远不会返回。通常情况下，这是一个我们不应该忽略的警告，但这一次，这正是我们想要的结果，所以我们忽略了这个警告，无论如何都要运行程序。当堆栈达到极限时，程序会在短暂的后崩溃。在程序崩溃之前，它设法用当前的堆栈大小打印出数千行。输出的最后几行如下所示:\n\n```cpp\n... \n8378667 \n8379755 \n8380843 \n```\n\n因为我们要减去`std::byte`指针，所以大小是以字节为单位的，所以看起来我的系统上堆栈的最大大小大约是 8 MB。在类似 Unix 的系统上，可以通过使用带有选项`-s`的`ulimit`命令来设置和获取进程的堆栈大小:\n\n```cpp\n$ ulimit -s\n$ 8192 \n```\n\n`ulimit`(用户限制的缩写)以千字节为单位返回最大堆栈大小的当前设置。`ulimit`的输出证实了我们实验的结果:如果我不明确配置，我的 Mac 上的堆栈大约是 8 MB。\n\n在 Windows 上，默认堆栈大小通常设置为 1 MB。如果堆栈大小配置不正确，在 macOS 上运行良好的程序可能会因 Windows 上的堆栈溢出而崩溃。\n\n通过这个例子，我们还可以得出结论，我们不想耗尽堆栈内存，因为当这种情况发生时，程序将崩溃。在本章的后面，我们将看到如何实现一个基本的内存分配器来处理固定大小的分配。然后我们将理解堆栈只是另一种类型的内存分配器，可以非常有效地实现，因为使用模式总是顺序的。我们总是在栈顶(连续内存的末端)请求和释放内存。这确保了堆栈内存永远不会变得碎片化，并且我们可以只通过移动堆栈指针来分配和释放内存。\n\n## 堆内存\n\n堆(或自由存储，这是 C++ 中更正确的术语)是具有动态存储的数据所在的地方。如前所述，堆由多个线程共享，这意味着对堆的内存管理需要考虑并发性。这使得堆中的内存分配比堆栈分配更复杂，堆栈分配是每个线程的本地分配。\n\n堆栈内存的分配和解除分配模式是顺序的，也就是说，内存总是按照与分配顺序相反的顺序解除分配。另一方面，对于动态内存，分配和解除分配可以任意发生。对象的动态寿命和内存分配的可变大小增加了**内存碎片**的风险。\n\n理解内存碎片问题的一个简单方法是看一个内存碎片是如何发生的例子。假设我们有一个 16 KB 的小型连续内存块，从中分配内存。我们正在分配两种类型的对象:类型 **A** ，1kb；类型 **B** ，2 KB。我们首先分配一个类型为**的对象，然后分配一个类型为**的对象。这样重复，直到内存看起来像下图:****\n\n<figure class=\"mediaobject\">![](img/B15619_07_04.png)</figure>\n\n图 7.4:分配 A 和 B 类型对象后的内存\n\n接下来，不再需要所有类型为 **A** 的对象，因此可以解除分配。现在的记忆是这样的:\n\n<figure class=\"mediaobject\">![](img/B15619_07_05.png)</figure>\n\n图 7.5:类型 A 的对象被释放后的内存\n\n现在有 10 KB 的内存在使用，6 KB 可用。现在，假设我们要分配一个类型为 **B** 的新对象，即 2 KB。虽然有 6 KB 的空闲内存，但是没有地方我们可以找到 2 KB 的内存块，因为内存已经变得碎片化了。\n\n既然你已经很好地理解了计算机内存是如何构造的，以及在运行过程中是如何使用的，现在是时候探索 C++ 对象是如何生活在内存中的了。\n\n# 内存中的对象\n\n我们在 C++ 程序中使用的所有对象都驻留在内存中。在这里，我们将探索对象是如何从内存中创建和删除的，并描述对象是如何在内存中布局的。\n\n## 创建和删除对象\n\n在这一节中，我们将挖掘使用`new`和`delete`的细节。考虑以下使用`new`在免费商店创建一个对象，然后使用`delete`删除它的方式:\n\n```cpp\nauto* user = new User{\"John\"};  // allocate and construct \nuser->print_name();             // use object \ndelete user;                    // destruct and deallocate \n```\n\n我不建议你以这种方式明确调用`new`和`delete`，但我们暂时忽略这一点。让我们言归正传；正如评论所暗示的那样，`new`实际上做了两件事，即:\n\n*   分配内存来保存`User`类型的新对象\n*   通过调用`User`类的构造函数，在分配的内存空间中构造一个新的`User`对象\n\n`delete`也是如此，它:\n\n*   通过调用其析构函数来析构`User`对象\n*   释放放置`User`对象的内存\n\n在 C++ 中，实际上可以将这两个动作(内存分配和对象构造)分开。这很少使用，但是在编写库组件时有一些重要且合法的用例。\n\n### 新位置\n\nC++ 允许我们将内存分配与对象构造分开。例如，我们可以用`malloc()`分配一个字节数组，并在内存区域中构造一个新的`User`对象。看看下面的代码片段:\n\n```cpp\nauto* memory = std::malloc(sizeof(User));\nauto* user = ::new (memory) User(\"john\"); \n```\n\n使用`::new (memory)`的可能不熟悉的语法被称为**放置新**。是`new`的非分配形式，只构造一个对象。`new`前面的双冒号(`::`)确保了解析是从全局命名空间进行的，以避免拾取过载版本的`operator new`。\n\n在前面的示例中，放置新构造了`User`对象，并将其放置在指定的内存位置。因为我们是用`std::malloc()`为单个对象分配内存，所以它保证是正确对齐的(除非类`User`被声明为过度对齐)。稍后，我们将探讨在使用放置新时必须考虑对齐的情况。\n\n没有放置删除，所以为了销毁对象并释放内存，我们需要显式调用析构函数，然后释放内存:\n\n```cpp\nuser->~User();\nstd::free(memory); \n```\n\n这是唯一一次您应该显式调用析构函数。永远不要这样调用析构函数，除非你已经创建了一个新的对象。\n\nC++ 17 在`<memory>`中引入了一组实用函数，用于在不分配或解除分配内存的情况下构造和销毁对象。因此，现在可以使用名称以`std::uninitialized_`开头的`<memory>`中的一些函数来构造、复制对象并将对象移动到未初始化的内存区域，而不是调用 placement new。我们现在可以不用显式调用析构函数，而是使用`std::destroy_at()`在特定的内存地址处销毁一个对象，而无需释放内存。\n\n使用这些新函数可以重写前面的例子。以下是它的外观:\n\n```cpp\nauto* memory = std::malloc(sizeof(User));\nauto* user_ptr = reinterpret_cast<User*>(memory);\nstd::uninitialized_fill_n(user_ptr, 1, User{\"john\"});\nstd::destroy_at(user_ptr);\nstd::free(memory); \n```\n\nC++ 20 还引入了`std::construct_at()`，可以用以下内容替换`std::uninitialized_fill_n()`调用:\n\n```cpp\nstd::construct_at(user_ptr, User{\"john\"});        // C++ 20 \n```\n\n请记住我们展示这些裸低级内存设施是为了更好地理解 C++ 中的内存管理。使用`reinterpret_cast`和这里演示的内存实用程序应该在 C++ 代码库中保持绝对最小。\n\n接下来，您将看到当我们使用`new`和`delete`表达式时，运算符被称为什么。\n\n### 新建和删除运算符\n\n函数`operator new`是在调用新表达式时负责分配内存。`new`运算符可以是一个全局定义的函数，也可以是一个类的静态成员函数。有可能使全球运营商`new`和`delete`超负荷。在本章的后面，我们将看到这在分析内存使用情况时非常有用。\n\n下面是如何做到的:\n\n```cpp\nauto operator new(size_t size) -> void* { \n  void* p = std::malloc(size); \n  std::cout << \"allocated \" << size << \" byte(s)\\n\"; \n  return p; \n} \n\nauto operator delete(void* p) noexcept -> void { \n  std::cout << \"deleted memory\\n\"; \n  return std::free(p); \n} \n```\n\n我们可以验证在创建和删除`char`对象时，我们的重载操作符实际上正在被使用:\n\n```cpp\nauto* p = new char{'a'}; // Outputs \"allocated 1 byte(s)\"\ndelete p;                // Outputs \"deleted memory\" \n```\n\n当使用`new[]`和`delete[]`表达式创建和删除对象数组时，会使用另一对运算符，即`operator new[]`和`operator delete[]`。我们可以用同样的方式让这些操作符过载:\n\n```cpp\nauto operator new[](size_t size) -> void* {\n  void* p = std::malloc(size); \n  std::cout << \"allocated \" << size << \" byte(s) with new[]\\n\"; \n  return p; \n} \n\nauto operator delete[](void* p) noexcept -> void { \n  std::cout << \"deleted memory with delete[]\\n\"; \n  return std::free(p); \n} \n```\n\n切记如果超负荷`operator new`，也要超负荷`operator delete`。分配和释放内存的函数成对出现。分配内存的分配器应该释放内存。例如，使用`std::malloc()`分配的内存应该总是使用`std::free()`释放，而使用`operator new[]`分配的内存应该使用`operator delete[]`释放。\n\n也可以覆盖特定类别的`operator new`或`operator delete`。这可能比重载全局操作符更有用，因为我们更可能需要一个特定类的自定义动态内存分配器。\n\n这里，我们为`Document`类重载`operator new`和`operator delete`:\n\n```cpp\nclass Document { \n// ...\npublic:  \n  auto operator new(size_t size) -> void* {\n    return ::operator new(size);\n  } \n  auto operator delete(void* p) -> void {\n    ::operator delete(p); \n  } \n}; \n```\n\n当我们创建新的动态分配的`Document`对象时，将使用类特定版本的`new`:\n\n```cpp\nauto* p = new Document{}; // Uses class-specific operator new\ndelete p; \n```\n\n如果我们想使用全局`new`和`delete`，使用全局范围(`::`)仍然是可能的:\n\n```cpp\nauto* p = ::new Document{}; // Uses global operator new\n::delete p; \n```\n\n我们将在本章后面讨论内存分配器，然后我们将看到重载的`new`和`delete`操作符在使用。\n\n总结一下到目前为止我们所看到的，一个`new`的表达涉及到两件事:分配和建设。`operator new`分配内存，您可以全局或按类重载内存，以自定义动态内存管理。放置新可用于在已分配的内存区域中构建对象。\n\n为了有效利用记忆，我们需要理解的另一个重要但相当低级的话题是记忆的**对齐**。\n\n## 内存对齐\n\n中央处理器一次一个字地将存储器读入寄存器。64 位架构的字长为 64 位，32 位架构的字长为 32 位，依此类推。为了使中央处理器在处理不同数据类型时高效工作，它对不同类型的对象所在的地址有限制。C++ 中的每种类型都有一个对齐要求，它定义了某种类型的对象在内存中应该位于的地址。\n\n如果类型的对齐方式为 1，则意味着该类型的对象可以位于任何字节地址。如果类型的对齐方式为 2，则意味着连续允许地址之间的字节数为 2。或者引用 C++ 标准:\n\n> \"对齐是实现定义的整数值，表示给定对象可以分配的连续地址之间的字节数。\"\n\n我们可以使用`alignof`找出一个类型的对齐方式:\n\n```cpp\n// Possible output is 4  \nstd::cout << alignof(int) << '\\n'; \n```\n\n当我运行这个代码时，它输出`4`，这意味着类型`int`的对齐要求在我的平台上是 4 字节。\n\n下图显示了来自 64 位字系统的两个内存示例。上面一行包含三个 4 字节整数，它们位于 4 字节对齐的地址上。CPU 可以高效地将这些整数加载到寄存器中，在访问其中一个`int`成员时，永远不需要读取多个字。将此与第二行进行比较，第二行包含两个位于未对齐地址的`int`成员。第二个`int`甚至跨越了两个字的界限。在最好的情况下，这只是效率低下，但在某些平台上，程序会崩溃:\n\n<figure class=\"mediaobject\">![](img/B15619_07_06.png)</figure>\n\n图 7.6:在对齐和未对齐的内存地址中包含整数的两个内存示例\n\n假设我们有一个对齐要求为 2 的类型。C++ 标准没有说明有效地址是 1、3、5 还是 7...或者 0，2，4，6....我们知道的所有平台都从 0 开始计数地址，因此，实际上我们可以通过使用模运算符(`%`)检查对象的地址是否是对齐的倍数来检查对象是否正确对齐。\n\n然而，如果我们想编写完全可移植的 C++ 代码，我们需要使用`std::align()`而不是模来检查对象的对齐。`std::align()`是来自`<memory>`的一个函数，它将根据我们作为参数传递的对齐方式来调整指针。如果我们传递给它的内存地址已经对齐，指针就不会被调整。因此，我们可以使用`std::align()`实现一个名为`is_aligned()`的小实用函数，如下所示:\n\n```cpp\nbool is_aligned(void* ptr, std::size_t alignment) {\n  assert(ptr != nullptr);\n  assert(std::has_single_bit(alignment)); // Power of 2\n  auto s = std::numeric_limits<std::size_t>::max();\n  auto aligned_ptr = ptr;\n  std::align(alignment, 1, aligned_ptr, s);\n  return ptr == aligned_ptr;\n} \n```\n\n首先，我们确保`ptr`参数不为空，并且`alignment`是 2 的幂，这在 C++ 标准中是一个要求。我们正在使用`<bit>`头中的 C++ 20 `std::has_single_bit()`来检查这一点。接下来，我们打电话给`std::align()`。`std::align()`的典型用例是当我们有一个一定大小的内存缓冲区，我们想要在其中存储一个具有一定对齐要求的对象。在这种情况下，我们没有缓冲区，我们不关心对象的大小，所以我们说对象的大小为 1，缓冲区是 a `std::size_t`的最大值。然后，我们可以比较原始的`ptr`和调整后的`aligned_ptr`，看看原始指针是否已经对齐。在接下来的例子中，我们将会用到这个工具。\n\n当使用`new`或`std::malloc()`分配内存时，我们获得的内存应该与我们指定的类型正确对齐。下面的代码显示在我的平台上分配给`int`的内存至少是 4 字节对齐的:\n\n```cpp\nauto* p = new int{};\nassert(is_aligned(p, 4ul)); // True \n```\n\n事实上，`new`和`malloc()`保证总是返回对任何标量类型都适当对齐的内存(如果它能够返回内存的话)。`<cstddef>`头为我们提供了一个名为`std::max_align_t`的类型，它的对齐要求至少和所有标量类型一样严格。稍后，我们将看到这种类型在编写自定义内存分配器时非常有用。所以，即使我们在免费商店只为`char`请求内存，它也会为`std::max_align_t`进行适当的对齐。\n\n下面的代码显示了从`new`返回的内存对于`std::max_align_t`以及任何标量类型都是正确对齐的:\n\n```cpp\nauto* p = new char{}; \nauto max_alignment = alignof(std::max_align_t);\nassert(is_aligned(p, max_alignment)); // True \n```\n\n让我们用`new`连续分配`char`两次:\n\n```cpp\nauto* p1 = new char{'a'};\nauto* p2 = new char{'b'}; \n```\n\n然后，记忆可能看起来像这样:\n\n<figure class=\"mediaobject\">![](img/B15619_07_07.png)</figure>\n\n图 7.7:两次单独分配一个字符后的内存布局\n\n`p1`和`p2`之间的间距取决于`std::max_align_t`的对中要求。在我的系统中，它是`16`字节，因此，每个`char`实例之间有 15 个字节，即使`char`的对齐只有 1。\n\n当使用`alignas`说明符声明变量时，可以指定比默认对齐更严格的自定义对齐要求。假设我们的高速缓存行大小为 64 字节，并且出于某种原因，我们希望确保两个变量放在不同的高速缓存行上。我们可以做到以下几点:\n\n```cpp\nalignas(64) int x{};\nalignas(64) int y{};\n// x and y will be placed on different cache lines \n```\n\n定义类型时，也可以指定自定义对齐方式。下面是一个在使用时正好占用一个缓存行的结构:\n\n```cpp\nstruct alignas(64) CacheLine {\n    std::byte data[64];\n}; \n```\n\n现在，如果我们创建一个类型为`CacheLine`的堆栈变量，它将按照 64 字节的自定义对齐方式进行对齐:\n\n```cpp\nint main() {\n  auto x = CacheLine{};\n  auto y = CacheLine{};\n  assert(is_aligned(&x, 64));\n  assert(is_aligned(&y, 64));\n  // ...\n} \n```\n\n在堆上分配对象时，也满足了更严格的对齐要求。为了支持具有非默认对齐要求的类型的动态分配，C++ 17 引入了`operator new()`和`operator delete()`的新重载，它们接受类型为`std::align_val_t`的对齐参数。还有一个在`<cstdlib>`中定义的`aligned_alloc()`函数，可以用来手动分配对齐的堆内存。\n\n下面是一个例子，在这个例子中，我们分配了一个堆内存块，它应该正好占用一个内存页面。在这种情况下，当使用`new`和`delete`时，将调用`operator new()`和`operator delete()`的对齐感知版本:\n\n```cpp\nconstexpr auto ps = std::size_t{4096};      // Page size\nstruct alignas(ps) Page {\n    std::byte data_[ps];\n};\nauto* page = new Page{};                    // Memory page\nassert(is_aligned(page, ps));               // True\n// Use page ...\ndelete page; \n```\n\n内存页面不是 C++ 抽象机器的一部分，因此没有可移植的方法来以编程方式获得当前运行的系统的页面大小。但是，您可以在 Unix 系统上使用`boost::mapped_region::get_page_size()`或特定于平台的系统调用，如`getpagesize()`。\n\n最后需要注意的是，支持的对齐集是由您正在使用的标准库的实现定义的，而不是 C++ 标准。\n\n## 填料\n\n编译器有时需要向我们的用户定义类型添加额外的字节，**填充**。当我们在类或结构中定义数据成员时，编译器被迫按照我们定义它们的顺序来放置成员。\n\n但是，编译器还必须确保类内部的数据成员具有正确的对齐方式；因此，如果需要，它需要在数据成员之间添加填充。例如，假设我们有一个定义如下的类:\n\n```cpp\nclass Document { \n  bool is_cached_{}; \n  double rank_{}; \n  int id_{}; \n};\nstd::cout << sizeof(Document) << '\\n'; // Possible output is 24 \n```\n\n可能的输出为 24 的原因是编译器在`bool`和`int`之后插入填充，以满足单个数据成员和整个类的对齐要求。编译器将`Document`类转换成如下形式:\n\n```cpp\nclass Document {\n  bool is_cached_{};\n  std::byte padding1[7]; // Invisible padding inserted by compiler\n  double rank_{};\n  int id_{};\n  std::byte padding2[4]; // Invisible padding inserted by compiler\n}; \n```\n\n`bool`和`double`之间的第一个填充是 7 字节，因为`double`类型的`rank_`数据成员具有 8 字节的对齐。`int`后添加的第二个填充是 4 字节。这是为了满足`Document`级本身的校准要求。具有最大对齐要求的成员也决定了整个数据结构的对齐要求。在我们的例子中，这意味着`Document`类的总大小必须是 8 的倍数，因为它包含一个 8 字节对齐的`double`值。\n\n我们现在意识到，我们可以通过从具有最大对齐要求的类型开始，以最小化编译器插入的填充的方式重新排列`Document`类中数据成员的顺序。让我们创建一个新版本的`Document`类:\n\n```cpp\n// Version 2 of Document class\nclass Document {\n  double rank_{}; // Rearranged data members\n  int id_{};\n  bool is_cached_{};\n}; \n```\n\n随着成员的重新排列，编译器现在只需要在`is_cached_`数据成员后填充，以调整`Document`的对齐。这是填充后类的外观:\n\n```cpp\n// Version 2 of Document class after padding\nclass Document { \n  double rank_{}; \n  int id_{}; \n  bool is_cached_{}; \n  std::byte padding[3]; // Invisible padding inserted by compiler \n}; \n```\n\n新`Document`类的大小现在只有 16 字节，而第一个版本是 24 字节。这里的洞见应该是物体的大小可以通过改变成员的声明顺序来改变。我们还可以在更新版本的`Document`上再次使用`sizeof`运算符来验证这一点:\n\n```cpp\nstd::cout << sizeof(Document) << '\\n'; // Possible output is 16 \n```\n\n下图显示了`Document`类版本 1 和版本 2 的内存布局:\n\n<figure class=\"mediaobject\">![](img/B15619_07_08.png)</figure>\n\n图 7.8:文档类的两个版本的内存布局。对象的大小可以通过改变其成员的声明顺序来改变。\n\n一般来说，您可以将最大的数据成员放在开头，将最小的成员放在结尾。通过这种方式，您可以最大限度地减少由填充引起的内存开销。稍后，我们将看到，在知道我们正在创建的对象的对齐方式之前，我们需要考虑将对象放入我们分配的内存区域时的对齐方式。\n\n从性能的角度来看，也有可能需要将对象与缓存行对齐，以最小化对象跨越的缓存行数量。虽然我们讨论的是缓存友好性的主题，但还应该提到的是，将经常一起使用的多个数据成员放在一起是有益的。\n\n保持数据结构紧凑对性能很重要。许多应用受内存访问时间的限制。内存管理的另一个重要方面是永远不要为不再需要的对象泄漏或浪费内存。通过明确资源的所有权，我们可以有效地避免各种资源泄漏。这是下一节的主题。\n\n# 内存所有权\n\n资源所有权是编程时要考虑的一个基本方面。资源的所有者负责在不再需要资源时释放资源。资源通常是内存块，但也可以是数据库连接、文件句柄等。无论您使用哪种编程语言，所有权都很重要。然而，这在 C 和 C++ 等语言中更为明显，因为默认情况下动态内存不会被垃圾收集。每当我们在 C++ 中分配动态内存时，我们都必须考虑该内存的所有权。幸运的是，现在该语言非常支持通过使用智能指针来表达各种类型的所有权，我们将在本节稍后介绍这一点。\n\n标准库中的智能指针帮助我们指定动态变量的所有权。其他类型的变量已经定义了所有权。例如，局部变量属于当前范围。当范围结束时，在范围内创建的对象将被自动销毁:\n\n```cpp\n{\n  auto user = User{};\n} // user automatically destroys when it goes out of scope \n```\n\n静态和全局变量归程序所有，当程序终止时将被销毁:\n\n```cpp\nstatic auto user = User{}; \n```\n\n数据成员由它们所属的类的实例拥有:\n\n```cpp\nclass Game {\n  User user; // A Game object owns the User object\n  // ...\n}; \n```\n\n只有动态变量没有默认的所有者，程序员需要确保所有动态分配的变量都有一个所有者来控制变量的生存期:\n\n```cpp\nauto* user = new User{}; // Who owns user now? \n```\n\n有了现代 C++，我们可以在不显式调用`new`和`delete`的情况下编写大部分代码，这是一件很棒的事情。手动跟踪对`new`和`delete`的调用很容易成为内存泄漏、双重删除和其他严重错误的问题。原始指针不表示任何所有权，如果我们是只使用原始指针来引用动态内存，这使得所有权很难跟踪。\n\n我建议您明确所有权，但要尽量减少手动内存管理。通过遵循一些处理内存所有权的相当简单的规则，您将增加代码干净和正确的可能性，而不会泄漏资源。接下来的部分将指导您完成一些最佳实践。\n\n## 隐式处理资源\n\n首先，使对象隐式处理动态内存的分配/解除分配:\n\n```cpp\nauto func() {\n  auto v = std::vector<int>{1, 2, 3, 4, 5};\n} \n```\n\n在前面的例子中，我们同时使用了堆栈和动态内存，但是我们不必显式调用`new`和`delete`。我们创建的`std::vector`对象是一个自动对象，它将存在于堆栈中。因为它属于作用域，所以当函数返回时，它将被自动销毁。`std::vector`对象本身使用动态内存来存储整数元素。`v`超出范围时，其析构函数可以安全释放动态内存。这种让析构函数释放动态内存的模式很容易避免内存泄漏。\n\n当我们讨论释放资源的话题时，我认为提到 RAII 是有意义的。 **RAII** 是一种众所周知的 C++ 技术，简称为**资源获取是初始化**，其中资源的生存期由对象的生存期控制。模式很简单，但是对于处理资源(包括内存)非常有用。但是让我们说，作为一个改变，我们需要的资源是某种发送请求的连接。每当我们使用完连接时，我们(所有者)必须记得关闭它。以下是我们手动打开和关闭连接以发送请求时的外观示例:\n\n```cpp\nauto send_request(const std::string& request) { \n  auto connection = open_connection(\"http://www.example.com/\"); \n  send_request(connection, request); \n  close(connection); \n} \n```\n\n如您所见，我们必须记住在使用后关闭连接，否则连接将保持打开(泄漏)。在这个例子中，似乎很难忘记，但是一旦代码在插入适当的错误处理和多个退出路径后变得更加复杂，就很难保证连接总是关闭的。RAII 解决这个问题的方法是依靠自动变量的生命周期是以一种可预测的方式为我们处理的。我们需要的是一个与我们从`open_connection()`调用中获得的连接具有相同寿命的对象。我们可以为此创建一个类，称为`RAIIConnection`:\n\n```cpp\nclass RAIIConnection { \npublic: \n  explicit RAIIConnection(const std::string& url) \n      : connection_{open_connection(url)} {} \n  ~RAIIConnection() { \n    try { \n      close(connection_);       \n    } \n    catch (const std::exception&) { \n      // Handle error, but never throw from a destructor \n    } \n  }\n  auto& get() { return connection_; } \n\nprivate:  \n  Connection connection_; \n}; \n```\n\n`Connection`对象现在封装在一个控制连接(资源)生存期的类中。我们现在可以让`RAIIConnection`为我们处理这个问题，而不是手动关闭连接:\n\n```cpp\nauto send_request(const std::string& request) { \n  auto connection = RAIIConnection(\"http://www.example.com/\"); \n  send_request(connection.get(), request); \n  // No need to close the connection, it is automatically handled \n  // by the RAIIConnection destructor \n} \n```\n\nRAII 让我们的代码更安全。即使`send_request()`在这里抛出异常，连接对象仍然会被析构并关闭连接。我们可以将 RAII 用于许多类型的资源，而不仅仅是内存、文件句柄和连接。另一个例子是 C++ 标准库中的`std::scoped_lock`。它试图在创建时获取锁(互斥体)，然后在销毁时释放锁。你可以在*第十一章*、*并发*中阅读更多关于`std::scoped_lock`的内容。\n\n现在，我们将探索更多在 C++ 中明确内存所有权的方法。\n\n## 容器\n\n您可以使用标准容器来处理对象集合。您使用的容器将拥有动态内存，它需要存储您添加到其中的对象。这是在代码中最小化手动`new`和`delete`表达式的非常有效的方法。\n\n也可以使用`std::optional`来处理可能存在或不存在的对象的生存期。`std::optional`可视为最大尺寸为 1 的容器。\n\n我们在这里不再讨论容器，因为它们已经包含在*第 4 章*、*数据结构*中。\n\n## 智能指针\n\n标准库中的智能指针包装了一个原始的指针，并明确了它所指向的对象的所有权。正确使用时，谁负责删除动态对象是毫无疑问的。三种智能指针类型为:`std::unique_ptr`、`std::shared_ptr`和`std::weak_ptr`。顾名思义，它们代表一个对象的三种所有权:\n\n*   独特的所有权表达了我，也只有我，拥有对象。当我用完后，我会删除它。\n*   共享所有权表示我和其他人一起拥有对象。当没有人需要这个对象时，它将被删除。\n*   弱所有权表示，如果对象存在，我会使用它，但不要只为了我而让它活着。\n\n我们将在下面的小节中分别讨论这些类型。\n\n### 唯一指针\n\n最安全、最不复杂的所有权是独一无二的所有权，应该是在思考智能指针时首先映入脑海的东西。唯一指针代表唯一的所有权；也就是说，拥有的资源恰好是一个实体。独特的所有权可以转让给别人，但不能复制，因为那样会破坏其独特性。下面是如何使用`std::unique_ptr`:\n\n```cpp\nauto owner = std::make_unique<User>(\"John\");\nauto new_owner = std::move(owner); // Transfer ownership \n```\n\n唯一指针也非常有效，因为与普通的原始指针相比，它们只增加了很少的性能开销。`std::unique_ptr`有一个非平凡的析构函数，这意味着(不像原始指针)它在传递给函数时不能在 CPU 寄存器中传递，这导致了轻微的开销。这使得它们比原始指针慢。\n\n### 共享指针\n\n共享所有权意味着一个对象可以有多个所有者。当最后一个所有者不再存在时，该对象将被删除。这是一种非常有用的指针类型，但也比唯一指针更复杂。\n\n`std::shared_ptr`对象使用引用计数来跟踪一个对象拥有的所有者数量。当计数器达到 0 时，对象将被删除。计数器需要存储在某个地方，因此与唯一指针相比，它确实有一些内存开销。此外，`std::shared_ptr`是内部线程安全的，因此计数器需要自动更新以防止争用情况。\n\n创建共享指针拥有的对象的推荐方式是使用`std::make_shared<T>()`。这比用`new`手动创建对象，然后将其传递给`std::shared_ptr`构造函数更安全(从异常安全的角度来看)也更有效。通过再次重载`operator new()`和`operator delete()`来跟踪分配，我们可以进行一个实验来找出为什么使用`std::make_shared<T>()`更有效:\n\n```cpp\nauto operator new(size_t size) -> void* { \n  void* p = std::malloc(size); \n  std::cout << \"allocated \" << size << \" byte(s)\" << '\\n'; \n  return p; \n} \nauto operator delete(void* p) noexcept -> void { \n  std::cout << \"deleted memory\\n\"; \n  return std::free(p); \n} \n```\n\n现在我们先来试试推荐的方式，使用`std::make_shared()`:\n\n```cpp\nint main() { \n  auto i = std::make_shared<double>(42.0); \n  return 0; \n} \n```\n\n运行程序时的输出如下:\n\n```cpp\nallocated 32 bytes \ndeleted memory \n```\n\n现在，让我们使用`new`显式分配`int`值，然后将其传递给`std::shared_ptr`构造函数:\n\n```cpp\nint main() { \n  auto i = std::shared_ptr<double>{new double{42.0}}; \n  return 0; \n} \n```\n\n该程序将生成以下输出:\n\n```cpp\nallocated 4 bytes \nallocated 32 bytes \ndeleted memory \ndeleted memory \n```\n\n我们可以得出结论，第二个版本需要两个分配，一个用于`double`，一个用于`std::shared_ptr`，而第一个版本只需要一个分配。这也意味着通过使用`std::make_shared()`，由于空间局部性，我们的代码将更加缓存友好。\n\n### 弱指针\n\n弱所有权不能保持任何物体的生命；它只允许我们在别人拥有的情况下使用一个对象。为什么会想要弱所有权这样模糊的所有权？使用弱指针的一个常见原因是打破参考周期。当两个或多个对象使用共享指针相互引用时，就会发生引用循环。即使所有的外部`std::shared_ptr`构造器都消失了，对象也是通过引用自己而保持活着。\n\n为什么不直接使用原始指针呢？弱指针不正是原始指针吗？一点也不。使用弱指针是安全的，因为我们不能引用该对象，除非它实际存在，而悬空的原始指针则不是这样。一个例子将澄清这一点:\n\n```cpp\nauto i = std::make_shared<int>(10); \nauto weak_i = std::weak_ptr<int>{i};\n\n// Maybe i.reset() happens here so that the int is deleted... \nif (auto shared_i = weak_i.lock()) { \n  // We managed to convert our weak pointer to a shared pointer \n  std::cout << *shared_i << '\\n'; \n} \nelse { \n  std::cout << \"weak_i has expired, shared_ptr was nullptr\\n\"; \n} \n```\n\n每当我们试图使用弱指针时，我们需要首先使用成员函数`lock()`将其转换为共享指针。如果对象没有过期，共享指针将是指向该对象的有效指针；否则，我们会得到一个空的`std::shared_ptr`回来。这样，我们在使用`std::weak_ptr`代替原始指针时就可以避免悬空指针。\n\n这将结束我们关于内存中对象的部分。C++ 为处理内存提供了极好的支持，包括低级概念，如对齐和填充，以及高级概念，如对象所有权。\n\n在使用 C++ 时，对所有权、RAII 和引用计数有一个良好的理解非常重要。对 C++ 不熟悉并且之前没有接触过这些概念的程序员可能需要一些时间来完全掌握这一点。同时，这些概念并不是 C++ 独有的。在大多数语言中，它们更加分散，但在其他语言中，它们更加突出(Rust 就是后者的一个例子)。所以，一旦掌握了，它也会提高你其他语言的编程技能。思考对象所有权将对您编写的程序的设计和架构产生积极影响。\n\n现在，我们将转向优化技术，减少动态内存分配的使用，尽可能使用堆栈。\n\n# 小对象优化\n\n像`std::vector`这样的容器有一个很棒的地方，就是它们会在需要的时候自动分配动态内存。然而，有时对只包含少量元素的容器对象使用动态内存会影响性能。将元素保留在容器本身中，并且只使用堆栈内存，而不是在堆上分配小的内存区域，会更有效。`std::string`的大多数现代实现将利用这样一个事实，即正常程序中的许多字符串都很短，并且短字符串在不使用堆内存的情况下处理起来更有效。\n\n一种替代方法是在字符串类本身中保留一个单独的小缓冲区，当字符串内容很短时可以使用。这将增加字符串类的大小，即使不使用短缓冲区。\n\n因此，更节省内存的解决方案是使用 union，当字符串处于短模式时，它可以保存一个短缓冲区，否则，保存处理动态分配的缓冲区所需的数据成员。优化容器以处理小数据的技术通常被称为字符串的小字符串优化，或者其他类型的小对象优化和小缓冲区优化。我们热爱的事物有很多名字。\n\n一个简短的代码示例将演示来自 LLVM 的 libc++ 的`std::string`如何在我的 64 位系统上运行:\n\n```cpp\nauto allocated = size_t{0}; \n// Overload operator new and delete to track allocations \nvoid* operator new(size_t size) {  \n  void* p = std::malloc(size); \n  allocated += size; \n  return p; \n} \n\nvoid operator delete(void* p) noexcept { \n  return std::free(p); \n} \n\nint main() { \n  allocated = 0; \n  auto s = std::string{\"\"}; // Elaborate with different string sizes \n\n  std::cout << \"stack space = \" << sizeof(s) \n    << \", heap space = \" << allocated \n    << \", capacity = \" << s.capacity() << '\\n'; \n} \n```\n\n为了跟踪动态内存分配，代码从重载全局`operator new`和`operator delete`开始。我们现在可以开始测试不同尺寸的绳子`s`来看看`std::string`的表现。在我的系统上以发布模式构建和运行前面的示例时，它会生成以下输出:\n\n```cpp\nstack space = 24, heap space = 0, capacity = 22 \n```\n\n这个输出告诉我们`std::string`在堆栈上占据了 24 个字节，并且在不使用任何堆内存的情况下，它有 22 个字符的容量。让我们通过用 22 个字符的字符串替换空字符串来验证这是否是真的:\n\n```cpp\nauto s = std::string{\"1234567890123456789012\"}; \n```\n\n程序仍然产生相同的输出，并验证没有分配动态内存。但是当我们将字符串增加到 23 个字符时会发生什么呢？\n\n```cpp\nauto s = std::string{\"12345678901234567890123\"}; \n```\n\n现在运行程序会产生以下输出:\n\n```cpp\nstack space = 24, heap space = 32, capacity = 31 \n```\n\n`std::string`类现在被迫使用堆来存储字符串。它分配 32 个字节，并报告容量为 31。这是因为 libc++ 总是在内部存储以 null 结尾的字符串，因此在 null 字符的末尾需要一个额外的字节。仍然值得注意的是，字符串类只能是 24 个字节，并且可以在不分配任何内存的情况下保存长度为 22 个字符的字符串。它是如何做到这一点的？如前所述，通过使用两种不同布局的并集来节省内存是很常见的:一种用于短模式，另一种用于长模式。真正的 libc++ 实现中有很多聪明之处，可以最大限度地利用可用的 24 个字节。为了演示这个概念，这里的代码被简化了。长模式的布局如下所示:\n\n```cpp\nstruct Long { \n  size_t capacity_{}; \n  size_t size_{}; \n  char* data_{}; \n}; \n```\n\n长布局中的每个成员都是 8 字节，因此总大小是 24 字节。`char`指针`data_`是指向动态分配的内存的指针，该内存将保存长字符串。短模式的布局如下所示:\n\n```cpp\nstruct Short { \n  unsigned char size_{};\n  char data_[23]{}; \n}; \n```\n\n在短模式下，不需要为容量使用变量，因为它是编译时常数。在这种布局中，也可以对`size_`数据成员使用较小的类型，因为我们知道，如果字符串是短字符串，它的长度只能在 0 到 22 之间。\n\n两种布局都使用联合进行组合:\n\n```cpp\nunion u_ { \n  Short short_layout_; \n  Long long_layout_; \n}; \n```\n\n然而，这里缺少了一点:字符串类如何知道它当前存储的是短字符串还是长字符串？需要一个标志来表明这一点，但是它存储在哪里呢？原来 libc++ 在长模式下使用`capacity_`数据成员上的最低有效位，在短模式下使用`size_`数据成员上的最低有效位。对于长模式，该位是多余的，因为字符串总是分配 2 的倍数的内存大小。在短模式下，可以仅使用 7 位来存储大小，以便一位可以用于标志。当编写这段代码来处理大端字节顺序时，它变得更加复杂，因为位需要放在内存中的相同位置，而不管我们使用的是联合的短结构还是长结构。您可以在 https://github.com/llvm/llvm-project/tree/master/libcxx[的 libc++ 实现](https://github.com/llvm/llvm-project/tree/master/libcxx)中查找细节。\n\n*图 7.9* 总结了小字符串优化的高效实现所使用的并集的简化(但仍然相当复杂)内存布局:\n\n<figure class=\"mediaobject\">![](img/B15619_07_09.png)</figure>\n\n图 7.9:分别用于处理短字符串和长字符串的两种不同布局的结合\n\n像这样聪明的技巧是你在尝试推出自己的类之前，应该努力使用标准库提供的高效且测试良好的类的原因。然而，了解这些优化以及它们如何工作是重要和有用的，即使你从来不需要自己写一个。\n\n# 自定义内存管理\n\n这一章我们已经走了很长的路。我们已经介绍了虚拟内存、堆栈和堆、`new`和`delete`表达式、内存所有权以及对齐和填充的基础知识。但是在结束本章之前，我们将展示如何在 C++ 中自定义内存管理。我们将看到在编写自定义内存分配器时，本章前面介绍的部分将如何派上用场。\n\n但是首先，什么是自定义内存管理器，为什么我们需要一个？\n\n使用`new`或`malloc()`分配内存时，我们使用 C++ 中内置的内存管理系统。`operator new`的大多数实现使用`malloc()`，这是一个通用的内存分配器。设计和构建一个通用的内存管理器是一个复杂的任务，已经有很多人花了很多时间研究这个课题。尽管如此，还是有几个原因让您想要编写一个定制的内存管理器。以下是一些例子:\n\n*   **调试诊断**:这一章我们已经做了几次了，重载`operator new`和`operator delete`，只是为了打印一些调试信息。\n*   **沙箱**:自定义内存管理器可以为不允许分配无限制内存的代码提供一个沙箱。沙盒还可以跟踪内存分配，并在沙盒代码完成执行时释放内存。\n*   **性能**:如果我们需要动态内存，又无法避免分配，那么我们可能不得不编写一个定制的内存管理器，能够更好地满足我们的特定需求。稍后，我们将介绍一些我们可以用来超越`malloc()`的情况。\n\n话虽如此，许多经验丰富的 C++ 程序员从未遇到过需要他们定制系统附带的标准内存管理器的问题。这很好地表明了如今通用内存管理器实际上有多好，尽管它们在不了解我们的具体用例的情况下必须满足所有需求。我们对应用中的内存使用模式了解得越多，我们就越有可能写出比`malloc()`更有效的东西。例如，还记得堆栈吗？与堆相比，从堆栈中分配和释放内存非常快，因为它不需要处理多个线程，并且释放总是以相反的顺序进行。\n\n构建自定义内存管理器通常从分析确切的内存使用模式开始，然后实现一个竞技场。\n\n## 建造竞技场\n\n使用内存分配器时最常用的两个术语是**竞技场**和**内存池**。我们不会在这本书里区分这些术语。我所说的竞技场，是指一个连续内存的块，包括分配部分内存并在以后回收的策略。\n\n竞技场在技术上也可以被称为内存资源“T2”或分配器“T4”，但这些术语将被用来指代标准库中的抽象。我们稍后开发的自定义分配器将使用我们在这里创建的竞技场来实现。\n\n在设计竞技场时，有一些通用的策略可以用来使分配和解除分配的性能比`malloc()`和`free()`更好:\n\n*   **单线程**:如果我们知道一个竞技场只能从一个线程使用，就没有需要用同步原语保护数据，比如锁或者原子。使用竞技场的客户端不存在被其他线程阻塞的风险，这在实时环境中很重要。\n*   **固定大小分配**:如果竞技场只分发固定大小的内存块，使用空闲列表在没有内存碎片的情况下高效回收内存相对容易。\n*   **有限生命期**:如果你知道从竞技场分配的对象只需要在有限且定义明确的生命期内生存，竞技场可以推迟回收并一次性释放所有内存。例如，在服务器应用中处理请求时创建的对象。当请求完成时，在请求期间分发的所有内存都可以一步回收。当然，竞技场需要足够大，以处理请求期间的所有分配，而不需要不断回收内存；否则，这个策略就行不通了。\n\n我不会深入讨论这些策略的细节，但是当在程序中寻找改进内存管理的方法时，意识到这些可能性是很好的。正如优化软件的常见情况一样，关键是要了解程序运行的环境，并分析特定的内存使用模式。与通用内存管理器相比，我们这样做是为了找到改进自定义内存管理器的方法。\n\n接下来，我们将看到一个简单的竞技场类模板，它可以用于需要动态存储持续时间的小对象或少数对象，但是它需要的内存通常很小，可以放在堆栈上。该代码基于霍华德·欣南特的`short_alloc`，发布于[https://howardhinnant.github.io/stack_alloc.html](https://howardhinnant.github.io/stack_alloc.html)。如果您想更深入地了解自定义内存管理，这是一个很好的起点。我认为这是一个很好的演示例子，因为它可以处理多种尺寸的对象，这需要正确处理对齐。\n\n但是，请记住，这是演示概念的简化版本，而不是为您提供生产就绪代码:\n\n```cpp\ntemplate <size_t N> \nclass Arena { \n  static constexpr size_t alignment = alignof(std::max_align_t); \npublic: \n  Arena() noexcept : ptr_(buffer_) {} \n  Arena(const Arena&) = delete; \n  Arena& operator=(const Arena&) = delete; \n\n  auto reset() noexcept { ptr_ = buffer_; } \n  static constexpr auto size() noexcept { return N; } \n  auto used() const noexcept {\n    return static_cast<size_t>(ptr_ - buffer_); \n  } \n  auto allocate(size_t n) -> std::byte*; \n  auto deallocate(std::byte* p, size_t n) noexcept -> void; \n\nprivate: \n  static auto align_up(size_t n) noexcept -> size_t { \n    return (n + (alignment-1)) & ~(alignment-1); \n  } \n  auto pointer_in_buffer(const std::byte* p) const noexcept -> bool {\n    return std::uintptr_t(p) >= std::uintptr_t(buffer_) &&\n           std::uintptr_t(p) < std::uintptr_t(buffer_) + N;\n  } \n  alignas(alignment) std::byte buffer_[N]; \n  std::byte* ptr_{}; \n}; \n```\n\n竞技场包含一个`std::byte`缓冲区，其大小在编译时决定。这使得有可能在堆栈上创建一个 arena 对象，或者创建一个具有静态或线程本地存储持续时间的变量。对齐可能在堆栈上分配；因此，除非我们将`alignas`说明符应用于数组，否则不能保证它将与`char`以外的类型对齐。如果您不习惯按位运算，助手`align_up()`功能可能看起来很复杂。然而，它基本上只是达到了我们使用的对齐要求。这个版本将分发的内存将与使用`malloc()`时的相同，因为它适用于任何类型。如果我们将竞技场用于较小对齐要求的小型类型，这有点浪费，但这里我们将忽略这一点。\n\n当回收内存时，我们需要知道被要求回收的指针是否真正属于我们的竞技场。`pointer_in_buffer()`函数通过比较指针地址和竞技场的地址范围来检查这一点。顺便提一下，将原始指针与不相交的对象进行关系比较是未定义的行为；这可能会被优化编译器使用，并产生令人惊讶的效果。为了避免这种情况，我们在比较地址之前将指针指向`std::uintptr_t`。如果你对这背后的细节感到好奇，你可以在的陈雷蒙德的文章*如何检查指针是否在内存范围*中找到一个完整的解释 p=97095 。\n\n接下来，我们需要实现分配和解除分配:\n\n```cpp\ntemplate<size_t N> \nauto Arena<N>::allocate(size_t n) -> std::byte* { \n  const auto aligned_n = align_up(n); \n  const auto available_bytes =  \n    static_cast<decltype(aligned_n)>(buffer_ + N - ptr_); \n  if (available_bytes >= aligned_n) { \n    auto* r = ptr_; \n    ptr_ += aligned_n; \n    return r; \n  } \n  return static_cast<std::byte*>(::operator new(n)); \n} \n```\n\n`allocate()`函数返回一个指针，指向具有指定大小的正确对齐的内存`n`。如果缓冲区中没有所请求大小的可用空间，它将返回到使用`operator new`来代替。\n\n下面的`deallocate()`函数首先检查指向要解除分配的内存的指针是来自缓冲区，还是已经用`operator new`分配了。如果不是来自缓冲区，我们只需用`operator delete`删除即可。否则，我们检查要解除分配的内存是否是我们从缓冲区分配的最后一个内存，然后通过移动当前的`ptr_`来回收它，就像堆栈一样。我们只是忽略了回收内存的其他尝试:\n\n```cpp\ntemplate<size_t N> \nauto Arena<N>::deallocate(std::byte* p, size_t n) noexcept -> void { \n  if (pointer_in_buffer(p)) { \n    n = align_up(n); \n    if (p + n == ptr_) { \n      ptr_ = p; \n    } \n  } \n  else { \n    ::operator delete(p);\n  }\n} \n```\n\n差不多就是这样；我们的竞技场现在可以使用了。让我们在分配`User`对象时使用它:\n\n```cpp\nauto user_arena = Arena<1024>{}; \n\nclass User { \npublic: \n  auto operator new(size_t size) -> void* { \n    return user_arena.allocate(size); \n  } \n  auto operator delete(void* p) -> void { \n    user_arena.deallocate(static_cast<std::byte*>(p), sizeof(User)); \n  } \n  auto operator new[](size_t size) -> void* { \n    return user_arena.allocate(size); \n  } \n  auto operator delete[](void* p, size_t size) -> void { \n    user_arena.deallocate(static_cast<std::byte*>(p), size); \n  } \nprivate:\n  int id_{};\n}; \n\nint main() { \n  // No dynamic memory is allocated when we create the users \n  auto user1 = new User{}; \n  delete user1; \n\n  auto users = new User[10]; \n  delete [] users; \n\n  auto user2 = std::make_unique<User>(); \n  return 0; \n} \n```\n\n本例中创建的`User`对象将全部驻留在`user_area`对象的缓冲区中。也就是说，当我们在这里调用`new`或`make_unique()`时，不会分配动态内存。但是还有其他方法可以在 C++ 中创建`User`对象，这个例子没有展示。我们将在下一节讨论它们。\n\n## 自定义内存分配器\n\n当尝试使用特定类型的定制内存管理器时，效果非常好！但是有一个问题。事实证明，类特定的`operator new`并没有在我们可能预期的所有场合被调用。考虑以下代码:\n\n```cpp\nauto user = std::make_shared<User>(); \n```\n\n当我们想要拥有 10 个用户的`std::vector`时会发生什么？\n\n```cpp\nauto users = std::vector<User>{};\nusers.reserve(10); \n```\n\n在这两种情况下，都没有使用我们的自定义内存管理器。为什么呢？从共享指针开始，我们必须回到前面的例子，我们看到`std::make_shared()`实际上为引用计数数据和它应该指向的对象都分配了内存。`std::make_shared()`不可能用`new User()`这样的表达式只分配一次就创建用户对象和计数器。相反，它分配内存并使用 placement new 构造用户对象。\n\n`std::vector`对象类似。当我们调用`reserve()`时，它不会在一个数组中默认构造 10 个对象。这将需要一个默认的构造函数，用于向量使用的所有类。相反，它分配的内存可以在添加 10 个用户对象时用来存放它们。同样，新的布局是实现这一点的工具。\n\n幸运的是，我们可以为`std::vector`和`std::shared_ptr`提供自定义内存分配器，以便让它们使用我们的自定义内存管理器。对于标准库中的其余容器也是如此。如果我们不提供自定义分配器，容器将使用默认的`std::allocator<T>`类。因此，为了使用我们的竞技场，我们需要编写一个容器可以使用的分配器。\n\n在 C++ 社区中，自定义分配器一直是一个激烈争论的话题。已经实现了许多自定义容器来控制如何管理内存，而不是使用带有自定义分配器的标准容器，这可能是有充分理由的。\n\n然而，对编写自定义分配器的支持和要求在 C++ 11 中得到了改进，现在变得更好了。这里，我们将只关注 C++ 11 及更高版本的分配器。\n\nC++ 11 中的最小分配器现在如下所示:\n\n```cpp\ntemplate<typename T> \nstruct Alloc {  \n  using value_type = T; \n  Alloc(); \n  template<typename U> Alloc(const Alloc<U>&); \n  T* allocate(size_t n); \n  auto deallocate(T*, size_t) const noexcept -> void; \n}; \ntemplate<typename T> \nauto operator==(const Alloc<T>&, const Alloc<T>&) -> bool;   \ntemplate<typename T> \nauto operator!=(const Alloc<T>&, const Alloc<T>&) -> bool; \n```\n\n由于 C++ 11 的改进，它真的不再是那么多代码了。使用分配器的容器实际上使用`std::allocator_traits`，如果分配器省略它们，它会提供合理的默认值。我建议你看一下`std::allocator_traits`，看看可以配置哪些特性，默认是什么。\n\n通过使用`malloc()`和`free()`，我们可以非常容易地实现一个最小的定制分配器。在这里，我们将展示最早由 Stephan T. Lavavej 在博客中发布的古老而著名的`Mallocator`，演示如何使用`malloc()`和`free()`编写一个最小定制分配器。从那以后，它被更新为 C++ 11，使其更加苗条。以下是它的外观:\n\n```cpp\ntemplate <class T>  \nstruct Mallocator { \n\n  using value_type = T; \n  Mallocator() = default;\n\n  template <class U>  \n  Mallocator(const Mallocator<U>&) noexcept {} \n\n  template <class U>  \n  auto operator==(const Mallocator<U>&) const noexcept {  \n    return true;  \n  } \n\n  template <class U>  \n  auto operator!=(const Mallocator<U>&) const noexcept {  \n    return false;  \n  } \n\n  auto allocate(size_t n) const -> T* { \n    if (n == 0) {  \n      return nullptr;  \n    } \n    if (n > std::numeric_limits<size_t>::max() / sizeof(T)) { \n      throw std::bad_array_new_length{}; \n    } \n    void* const pv = malloc(n * sizeof(T)); \n    if (pv == nullptr) {  \n      throw std::bad_alloc{};  \n    } \n    return static_cast<T*>(pv); \n  } \n  auto deallocate(T* p, size_t) const noexcept -> void { \n    free(p); \n  } \n}; \n```\n\n`Mallocator`是一个**无状态分配器**，这意味着分配器实例本身没有任何可变状态；相反，它使用全局函数进行分配和解除分配，即`malloc()`和`free()`。无状态分配器应该总是与相同类型的分配器进行比较。它表示用`Mallocator`分配的内存也应该用`Mallocator`解除分配，与`Mallocator`实例无关。无状态分配器是编写起来最简单的分配器，但是它也是有限的，因为它依赖于全局状态。\n\n要使用我们的竞技场作为堆栈分配对象，我们需要一个**状态分配器**，它可以引用竞技场实例。在这里，我们实现的竞技场类真正开始有意义了。比方说，我们想在一个函数中使用一个标准容器来做一些处理。我们知道，大多数情况下，我们处理的数据量非常小，可以放入堆栈中。但是一旦我们使用了标准库中的容器，它们将从堆中分配内存，在这种情况下，这将损害我们的性能。\n\n除了使用堆栈来管理数据和避免不必要的堆分配之外，还有什么替代方法？另一种方法是构建一个定制容器，使用我们在`std::string`中看到的小对象优化的变体。\n\n也可以使用 Boost 中的一个容器，比如`boost::container::small_vector`，它是基于 LLVM 的小向量。如果您还没有，我们建议您查看一下:[http://www . boost . org/doc/libs/1 _ 74 _ 0/doc/html/container/non _ standard _ containers . html](http://www.boost.org/doc/libs/1_74_0/doc/html/container/non_standard_containers.html)。\n\n然而，另一种选择是使用自定义分配器，我们接下来将探讨这一点。由于我们已经准备好了一个竞技场模板类，我们可以简单地在堆栈上创建一个竞技场的实例，并让一个自定义分配器将其用于分配。然后我们需要做的是实现一个有状态分配器，它可以保存对堆栈分配的 arena 对象的引用。\n\n同样，我们将实现的这个定制分配器是霍华德·欣南特的`short_alloc`的简化版本:\n\n```cpp\ntemplate <class T, size_t N> \nstruct ShortAlloc { \n\n  using value_type = T; \n  using arena_type = Arena<N>; \n\n  ShortAlloc(const ShortAlloc&) = default; \n  ShortAlloc& operator=(const ShortAlloc&) = default; \n\n  ShortAlloc(arena_type& arena) noexcept : arena_{&arena} { }\n\n  template <class U>\n  ShortAlloc(const ShortAlloc<U, N>& other) noexcept\n      : arena_{other.arena_} {}\n\n  template <class U> struct rebind {\n    using other = ShortAlloc<U, N>;\n  };\n  auto allocate(size_t n) -> T* {\n    return reinterpret_cast<T*>(arena_->allocate(n*sizeof(T)));\n  }\n  auto deallocate(T* p, size_t n) noexcept -> void {\n    arena_->deallocate(reinterpret_cast<std::byte*>(p), n*sizeof(T));\n  }\n  template <class U, size_t M>\n  auto operator==(const ShortAlloc<U, M>& other) const noexcept {\n    return N == M && arena_ == other.arena_;\n  }\n  template <class U, size_t M>\n  auto operator!=(const ShortAlloc<U, M>& other) const noexcept {\n    return !(*this == other);\n  }\n  template <class U, size_t M> friend struct ShortAlloc;\nprivate:\n  arena_type* arena_;\n}; \n```\n\n分配器持有对竞技场的引用。这是分配器仅有的状态。功能`allocate()`和`deallocate()`只是将他们的请求转发到竞技场。比较操作符确保`ShortAlloc`类型的两个实例使用相同的竞技场。\n\n现在，我们实现的分配器和竞技场可以与标准容器一起使用，以避免动态的内存分配。当我们使用小数据时，我们可以使用堆栈来处理所有的分配。我们来看一个使用`std::set`的例子:\n\n```cpp\nint main() { \n\n  using SmallSet =  \n    std::set<int, std::less<int>, ShortAlloc<int, 512>>; \n\n  auto stack_arena = SmallSet::allocator_type::arena_type{}; \n  auto unique_numbers = SmallSet{stack_arena}; \n\n  // Read numbers from stdin \n  auto n = int{}; \n  while (std::cin >> n)\n    unique_numbers.insert(n); \n\n  // Print unique numbers  \n  for (const auto& number : unique_numbers)\n    std::cout << number << '\\n'; \n} \n```\n\n该程序从标准输入中读取整数，直到到达文件结尾(在类似 Unix 的系统上为 Ctrl + D，在 Windows 上为 Ctrl + Z)。然后，它以升序打印唯一的数字。根据从`stdin`中读取的数量，程序将使用我们的`ShortAlloc`分配器使用堆栈内存或动态内存。\n\n## 使用多态内存分配器\n\n如果您已经阅读了这一章，您现在知道如何实现一个定制的分配器，它可以与任意容器一起使用，包括那些来自标准库的容器。假设我们想对我们在代码库中找到的一些代码使用新的分配器，这些代码正在处理类型为`std::vector<int>`的缓冲区，如下所示:\n\n```cpp\nvoid process(std::vector<int>& buffer) {\n  // ...\n}\nauto some_func() {\n  auto vec = std::vector<int>(64);\n  process(vec); \n  // ...\n} \n```\n\n我们渴望尝试我们的新分配器，它正在利用堆栈内存，并尝试像这样注入内存:\n\n```cpp\nusing MyAlloc = ShortAlloc<int, 512>;  // Our custom allocator\nauto some_func() {\n  auto arena = MyAlloc::arena_type();\n  auto vec = std::vector<int, MyAlloc>(64, arena);\n  process(vec);\n  // ...\n} \n```\n\n编译时，我们痛苦地意识到`process()`是一个期望`std::vector<int>`的函数，而我们的`vec`变量现在是另一种类型。GCC 给了我们以下错误:\n\n```cpp\nerror: invalid initialization of reference of type 'const std::vector<int>&' from expression of type 'std::vector<int, ShortAlloc<int, 512> > \n```\n\n类型不匹配的原因是，我们想要使用的自定义分配器`MyAlloc`被作为模板参数传递给`std::vector`，因此成为我们实例化的类型的一部分。因此，`std::vector<int>`和`std::vector<int, MyAlloc>`不能互换。\n\n对于您正在处理的用例来说，这可能是一个问题，也可能不是，您可以通过让`process()`函数接受`std::span`或者让它成为一个处理范围的通用函数来解决这个问题，而不需要`std::vector`。无论如何，重要的是要认识到，当使用标准库中的分配器感知模板类时，分配器实际上变成了类型的一部分。\n\n那么`std::vector<int>`使用的是什么分配器呢？答案是`std::vector<int>`使用默认模板参数`std::allocator`。所以，写`std::vector<int>`相当于`std::vector<int, std::allocator<int>>`。模板类`std::allocator`是一个空类，当它完成来自容器的分配和解除分配请求时，使用全局`new`和全局`delete`。这也意味着使用空分配器的容器的大小小于使用我们定制分配器的容器的大小:\n\n```cpp\nstd::cout << sizeof(std::vector<int>) << '\\n';\n// Possible output: 24\nstd::cout << sizeof(std::vector<int, MyAlloc>) << '\\n';\n// Possible output: 32 \n```\n\n从 libc++ 中检查`std::vector`的实现，我们可以看到它使用的是一个名为**压缩对**的俏皮类型，而这个压缩对又基于*空基类优化*来摆脱通常由空类成员占用的不必要的存储。这里就不赘述了，不过如果有兴趣的话，可以看看`compressed_pair`的 boost 版本，在[https://www . boost . org/doc/libs/1 _ 74 _ 0/libs/utility/doc/html/compressed _ pair . html](https://www.boost.org/doc/libs/1_74_0/libs/utility/doc/html/compressed_pair.html)上有记载。\n\n在 C++ 17 中，通过引入一个额外的间接层，解决了使用不同分配器时以不同类型结束的问题；名称空间`std::pmr`下的所有标准容器都使用同一个分配器，即`std::pmr::polymorphic_allocator`，它将所有分配/解除分配请求分派给一个**内存资源**类。因此，我们可以使用名为`std::pmr::polymorphic_allocator`的通用多态内存分配器，而不是编写新的自定义内存分配器，而是编写新的自定义内存资源，这些资源将在构建过程中交给多态分配器。内存资源类似于我们的`Arena`类，`polymorphic_allocator`是额外的间接层，包含指向资源的指针。\n\n下图显示了当向量委托给它的分配器实例，分配器又委托给它所指向的内存资源时的控制流:\n\n<figure class=\"mediaobject\">![](img/B15619_07_10.png)</figure>\n\n图 7.10:使用多态分配器分配内存\n\n要开始使用多态分配器，我们需要将命名空间从`std`更改为`std::pmr`:\n\n```cpp\nauto v1 = std::vector<int>{};             // Uses std::allocator\nauto v2 = std::pmr::vector<int>{/*...*/}; // Uses polymorphic_allocator \n```\n\n编写一个定制的内存资源是相对简单的，尤其是有了内存分配器和竞技场的知识。但是我们甚至不需要编写自定义内存资源来实现我们想要的。C++ 已经为我们提供了一些有用的实现，我们应该在编写自己的实现之前考虑一下。所有内存资源都来自基类`std::pmr::memory_resource`。以下内存资源位于`<memory_resource>`头中:\n\n*   `std::pmr::monotonic_buffer_resource`:这个和我们`Arena`班挺像的。当我们创建许多生命周期很短的对象时，这个类更好。只有当`monotonic_buffer_resource`实例被析构时，内存才会被释放，这使得分配非常快。\n*   `std::pmr::unsynchronized_pool_resource`:这使用包含固定大小内存块的内存池(也称为“板”)，避免了每个池内的碎片。每个池为一定大小的对象分配内存。如果您正在创建许多不同大小的对象，这个类可能会很有用。此内存资源不是线程安全的，除非您提供外部同步，否则无法从多个线程使用。\n*   `std::pmr::synchronized_pool_resource`:这是`unsynchronized_pool_resource`的线程安全版本。\n\n内存资源可以被链接。创建内存资源实例时，我们可以为其提供一个**上游内存资源**。如果当前资源无法处理该请求(类似于我们在`ShortAlloc`中使用`malloc()`处理的情况，一旦我们的小缓冲区已满)，或者当资源本身需要分配内存时(例如`monotonic_buffer_resource`需要分配其下一个缓冲区时)，将使用该选项。`<memory_resource>`头为我们提供了自由函数，这些函数返回指向全局资源对象的指针，这些指针在指定上游资源时非常有用:\n\n*   `std::pmr::new_delete_resource()`:使用全局`operator new`和`operator delete`。\n*   `std::pmr::null_memory_resource()`:每当被要求分配内存时总是抛出`std::bad_alloc`的资源。\n*   `std::pmr::get_default_resource()`:返回一个全局默认的内存资源，可以在运行时由`set_default_resource()`设置。初始默认资源是`new_delete_resource()`。\n\n让我们看看如何从上一节重写我们的例子，但是这次使用了一个`std::pmr::set`:\n\n```cpp\nint main() {\n  auto buffer = std::array<std::byte, 512>{};\n  auto resource = std::pmr::monotonic_buffer_resource{\n    buffer.data(), buffer.size(), std::pmr::new_delete_resource()};\n  auto unique_numbers = std::pmr::set<int>{&resource};\n  auto n = int{};\n  while (std::cin >> n) {\n    unique_numbers.insert(n);\n  }\n  for (const auto& number : unique_numbers) {\n    std::cout << number << '\\n';\n  }\n} \n```\n\n我们将一个堆栈分配的缓冲区传递给内存资源，然后将从`new_delete_resource()`返回的对象作为上游资源提供给它，以便在缓冲区变满时使用。如果我们省略了上游资源，它将使用默认内存资源，在这种情况下，这将是相同的，因为我们的代码不改变默认内存资源。\n\n## 实现自定义内存资源\n\n实现自定义内存资源相当简单。我们需要从`std::pmr::` `memory_resource`公开继承，然后实现三个将由基类(`std::pmr::memory_resource`)调用的纯虚函数。让我们实现一个简单的内存资源，它打印分配和解除分配，然后将请求转发给默认的内存资源:\n\n```cpp\nclass PrintingResource : public std::pmr::memory_resource {\npublic:\n  PrintingResource() : res_{std::pmr::get_default_resource()} {}\nprivate:\n  void* do_allocate(std::size_t bytes, std::size_t alignment)override {\n    std::cout << \"allocate: \" << bytes << '\\n';\n    return res_->allocate(bytes, alignment);\n  }\n  void do_deallocate(void* p, std::size_t bytes,\n                     std::size_t alignment) override {\n    std::cout << \"deallocate: \" << bytes << '\\n';\n    return res_->deallocate(p, bytes, alignment);\n  }\n  bool do_is_equal(const std::pmr::memory_resource& other) \n    const noexcept override {\n    return (this == &other);\n  }\n  std::pmr::memory_resource* res_;  // Default resource\n}; \n```\n\n请注意，我们将默认资源保存在构造函数中，而不是直接从`do_allocate()`和`do_deallocate()`调用`get_default_resource()`。原因是有人可能会在分配和解除分配之间的时间内通过调用`set_default_resource()`来更改默认资源。\n\n我们可以使用自定义内存资源来跟踪`std::pmr`容器的分配。下面是一个使用`std::pmr::vector`的例子:\n\n```cpp\nauto res = PrintingResource{};\nauto vec = std::pmr::vector<int>{&res};\nvec.emplace_back(1);\nvec.emplace_back(2); \n```\n\n运行程序时可能的输出是:\n\n```cpp\nallocate: 4\nallocate: 8\ndeallocate: 4\ndeallocate: 8 \n```\n\n使用多态分配器时需要非常小心的一点是，我们正在传递指向内存资源的原始非拥有指针。这不是多态分配器所特有的；实际上，我们的`Arena`类和`ShortAlloc`类也有同样的问题，但是当使用来自`std::pmr`的容器时，这可能更容易忘记，因为这些容器使用相同的分配器类型。考虑以下示例:\n\n```cpp\nauto create_vec() -> std::pmr::vector<int> {\n  auto resource = PrintingResource{};\n  auto vec = std::pmr::vector<int>{&resource}; // Raw pointer\n  return vec;                                  // Ops! resource\n}                                              // destroyed here \nauto vec = create_vec();\nvec.emplace_back(1);                           // Undefined behavior \n```\n\n由于资源在`create_vec()`结束范围时被破坏，我们新创建的`std::pmr::vector`是无用的，使用时很可能会崩溃。\n\n自定义内存管理部分到此结束。这是一个复杂的主题，如果您想使用自定义内存分配器来获得性能，我建议您在使用和/或实现自定义分配器之前，仔细测量和分析应用中的内存访问模式。通常，应用中只有一小部分类或对象需要使用自定义分配器进行调整。同时，减少应用中动态内存分配的数量，或者在内存的某些区域将对象分组在一起，都会对性能产生巨大的影响。\n\n# 摘要\n\n本章涵盖了很多内容，从虚拟内存的基础知识开始，最后实现一个自定义分配器，标准库中的容器可以使用这个分配器。很好地理解你的程序如何使用内存是很重要的。动态内存的过度使用可能是一个性能瓶颈，您可能需要对其进行优化。\n\n在您开始实现自己的容器或自定义内存分配器之前，请记住，您之前的许多人可能都遇到过与您可能面临的问题非常相似的内存问题。所以，很有可能适合你的工具已经在图书馆里了。构建快速、安全和健壮的定制内存管理器是一项挑战。\n\n在下一章中，您将学习如何受益于 C++ 概念的新引入特性，以及我们如何使用模板元编程让编译器为我们生成代码。"
  },
  {
    "path": "docs/cpp-hiperf/08.md",
    "content": "# 八、编译时编程\n\nC++ 能够在编译时计算表达式，这意味着程序执行时已经计算出了值。尽管自 C++ 98 以来元编程已经成为可能，但由于其复杂的基于模板的语法，它最初非常复杂。随着`constexpr`、`if constexpr`以及最近的 C++ *概念*的引入，元编程变得更加类似于编写常规代码。\n\n本章将向您简要介绍 C++ 中的编译时表达式计算，以及它们如何用于优化。\n\n我们将涵盖以下主题:\n\n*   使用 C++ 模板的元编程以及如何在 C++ 20 中编写缩写的函数模板\n*   在编译时使用类型特征检查和操作类型\n*   由编译器计算的常量表达式\n*   C++ 20 概念以及如何使用它们来为我们的模板参数添加约束\n*   元编程的一些真实例子\n\n我们将首先介绍模板元编程。\n\n# 模板元编程简介\n\n当编写常规的 C++ 代码时，它最终被转换成机器代码。**元编程**另一方面，允许我们编写将自身转换为常规 C++ 代码的代码。更广义地说，元编程是一种我们编写转换或生成其他代码的代码的技术。通过使用元编程，我们可以避免复制基于我们使用的数据类型仅略有不同的代码，或者我们可以通过预先计算最终程序执行前已知的值来最小化运行时成本。没有什么能阻止我们使用其他语言生成 C++ 代码。例如，我们可以通过广泛使用预处理器宏或者编写 Python 脚本来为我们生成或修改 C++ 文件来进行元编程:\n\n<figure class=\"mediaobject\">![](img/B15619_08_01.png)</figure>\n\n图 8.1:元程序生成常规的 C++ 代码，这些代码稍后将被编译成机器代码\n\n尽管我们可以使用任何语言来生成常规代码，但是使用 C++，我们有特权使用**模板**和**常量表达式**在语言本身内编写元程序。C++ 编译器可以执行我们的元程序，并生成常规的 C++ 代码，编译器将进一步将其转换为机器代码。\n\n使用模板和常量表达式直接在 C++ 中进行元编程比使用其他技术有很多优势:\n\n*   我们不必解析 C++ 代码(编译器会为我们解析)。\n*   使用 C++ 模板元编程时，对于分析和操作 C++ 类型有很好的支持。\n*   元程序的代码和常规的非泛型代码混合在 C++ 源代码中。有时，这可能会使您很难理解在运行时和编译时分别执行了哪些部分。然而，总的来说，这是使 C++ 元编程有效使用的一个非常重要的方面。\n\n在其最简单和最常见的形式中，C++ 中的模板元编程用于生成接受不同类型的函数、值和类。当编译器使用模板生成类或函数时，模板被称为**实例化**。常量表达式由编译器进行**评估**以生成常量值:\n\n<figure class=\"mediaobject\">![](img/B15619_08_02.png)</figure>\n\n图 8.2:c++ 中的编译时编程。将生成常规 C++ 代码的元程序是用 C++ 本身编写的。\n\n这是一个有些简化的视图；没有什么说 C++ 编译器需要以这种方式执行转换。然而，考虑在这两个不同的阶段执行 C++ 元编程是很有用的:\n\n*   初始阶段，模板和常量表达式生成函数、类和常量值的常规 C++ 代码。这个阶段通常被称为**恒定评估**。\n*   第二阶段，编译器最终将常规 C++ 代码编译成机器代码。\n\n本章后面，我将把元编程生成的 C++ 代码称为*常规 C++ 代码*。\n\n当使用元编程时，重要的是要记住它的主要用例是创建优秀的库，从而对用户代码隐藏复杂的构造/优化。请注意，无论元程序的代码内部有多复杂，重要的是将其隐藏在一个好的界面后面，以便用户代码库易于阅读和使用。\n\n让我们继续，创建我们的第一个生成函数和类的模板。\n\n## 创建模板\n\n让我们来看看一个简单的`pow()`函数和一个`Rectangle`类。通过使用**类型模板参数**，`pow()`函数和`Rectangle`类可以用于任何整数或浮点类型。没有模板，我们将不得不为每个基本类型创建一个单独的函数/类。\n\n编写元编程代码可能非常复杂；更简单的方法是想象预期的常规 C++ 代码是什么样子的。\n\n下面是一个简单函数模板的例子:\n\n```cpp\n// pow_n accepts any number type \ntemplate <typename T> \nauto pow_n(const T& v, int n) { \n  auto product = T{1}; \n  for (int i = 0; i < n; ++ i) { \n    product *= v; \n  }\n  return product; \n} \n```\n\n使用此函数将生成一个返回类型取决于模板参数类型的函数:\n\n```cpp\nauto x = pow_n<float>(2.0f, 3); // x is a float \nauto y = pow_n<int>(3, 3);      // y is an int \n```\n\n显式模板参数类型(在这种情况下为`float`和`int`)可以(最好)省略，编译器可以自己解决这个问题。这个机制叫做**模板参数演绎**，因为编译器*演绎*模板参数。以下示例将导致与前面所示相同的模板实例化:\n\n```cpp\nauto x = pow_n(2.0f, 3);  // x is a float \nauto y = pow_n(3, 3);     // y is an int \n```\n\n相应地，简单的类模板可以定义如下:\n\n```cpp\n// Rectangle can be of any type \ntemplate <typename T> \nclass Rectangle { \npublic: \n  Rectangle(T x, T y, T w, T h) : x_{x}, y_{y}, w_{w}, h_{h} {} \n  auto area() const { return w_ * h_; } \n  auto width() const { return w_; } \n  auto height() const { return h_; } \nprivate:\n  T x_{}, y_{}, w_{}, h_{}; \n}; \n```\n\n当使用类模板时，我们可以显式指定模板应该为其生成代码的类型，如下所示:\n\n```cpp\nauto r1 = Rectangle<float>{2.0f, 2.0f, 4.0f, 4.0f}; \n```\n\n但是也有可能受益于**类模板参数推演** ( **CTAD** )，让编译器为我们推演参数类型。以下代码将实例化一个`Rectangle<int>`:\n\n```cpp\nauto r2 = Rectangle{-2, -2, 4, 4};   // Rectangle<int> \n```\n\n然后，函数模板可以接受一个`Rectangle`对象，其中使用任意类型`T`定义矩形尺寸，如下所示:\n\n```cpp\ntemplate <typename T> \nauto is_square(const Rectangle<T>& r) { \n  return r.width() == r.height(); \n} \n```\n\n类型模板参数是最常见的模板参数。接下来，您将看到如何使用数字参数而不是类型参数。\n\n## 使用整数作为模板参数\n\n除了一般类型之外，模板也可以是其他类型，例如整型和浮点型。在下面的例子中，我们将在模板中使用一个`int`，这意味着编译器将为作为模板参数传递的每个唯一整数生成一个新函数:\n\n```cpp\ntemplate <int N, typename T> \nauto const_pow_n(const T& v) { \n  auto product = T{1}; \n  for (int i = 0; i < N; ++ i) { \n    product *= v; \n  }\n  return product; \n} \n```\n\n下面的代码将强制编译器实例化两个不同的函数:一个将值平方，一个将值立方:\n\n```cpp\nauto x2 = const_pow_n<2>(4.0f);   // Square\nauto x3 = const_pow_n<3>(4.0f);   // Cube \n```\n\n注意模板参数`N`和功能参数`v`的区别。对于`N`的每个值，编译器都会生成一个新函数。但是，`v`作为常规参数传递，因此不会产生新的函数。\n\n## 提供模板的专门化\n\n默认情况下，每当我们使用带有新参数的模板时，编译器都会生成常规的 C++ 代码。但是也可以为模板参数的某些值提供自定义实现。比方说，我们想要提供我们的`const_pow_n()`函数的常规 C++ 代码，当它与整数一起使用时，`N`的值是`2`。我们可以为这种情况编写一个**模板专门化**，如下所示:\n\n```cpp\ntemplate<>\nauto const_pow_n<2, int>(const int& v) {\n  return v * v;\n} \n```\n\n对于函数模板，我们需要在编写特殊化时固定*所有*模板参数。例如，不可能只指定`N`的值，而让类型参数`T`不指定。但是，对于类模板，可以只指定模板参数的一个子集。这叫**偏模板特殊化**。编译器会先选择最具体的模板。\n\n我们不能将部分模板专门化应用于函数的原因是函数可以重载(而类不能)。如果我们被允许混合重载和部分专门化，这将是非常难以理解的。\n\n## 编译器如何处理模板函数\n\n当编译器处理一个模板函数时，它用扩展的模板参数构造一个正则函数。以下代码将使编译器生成常规函数，因为它使用模板:\n\n```cpp\nauto a = pow_n(42, 3);          // 1\\. Generate new function\nauto b = pow_n(42.f, 2);        // 2\\. Generate new function\nauto c = pow_n(17.f, 5);        // 3.\nauto d = const_pow_n<2>(42.f);  // 4\\. Generate new function\nauto e = const_pow_n<2>(99.f);  // 5.\nauto f = const_pow_n<3>(42.f);  // 6\\. Generate new function \n```\n\n因此，当编译时，与常规函数不同，编译器将为每一组唯一的*模板参数*生成新的函数。这意味着它相当于手动创建四个不同的函数，如下所示:\n\n```cpp\nauto pow_n__float(float v, int n) {/*...*/}   // Used by: 1\nauto pow_n__int(int v, int n) {/*...*/}       // Used by: 2 and 3\nauto const_pow_n__2_float (float v) {/*...*/} // Used by: 4 and 5\nauto const_pow_n__3_float(float v) {/*...*/}  // Used by: 6 \n```\n\n这对于理解元编程是如何工作的很重要。模板代码生成非模板化的 C++ 代码，然后作为常规代码执行。如果生成的 C++ 代码不编译，将在编译时捕获错误。\n\n## 缩写函数模板\n\nC++ 20 引入了一种新的缩写语法来编写函数模板，它采用了与泛型 lambdas 相同的风格。通过对函数参数类型使用`auto`，我们实际上是在创建一个函数模板，而不是一个常规函数。回想一下我们最初的`pow_n()`模板，它是这样声明的:\n\n```cpp\ntemplate <typename T>\nauto pow_n(const T& v, int n) { \n  // ... \n```\n\n使用缩写的函数模板语法，我们可以改为使用`auto`来声明它:\n\n```cpp\nauto pow_n(const auto& v, int n) { // Declares a function template\n  // ... \n```\n\n这两个版本的区别在于缩写版本没有变量`v`类型的显式占位符。由于我们在实现中使用了占位符`T`，因此该代码将不幸无法编译:\n\n```cpp\nauto pow_n(const auto& v, int n) {\n  auto product = T{1}; // Error: What is T?\n  for (int i = 0; i < n; ++ i) { \n    product *= v; \n  } \n  return product;\n} \n```\n\n要解决这个问题，我们可以使用`decltype`说明符。\n\n## 接收带有 decltype 的变量的类型\n\n`decltype`说明符用于检索变量的类型，并在显式类型名称不可用时使用。\n\n有时，我们需要一个类型的显式占位符，但是没有可用的占位符，只有变量名可用。这发生在我们之前实现`pow_n()`函数时，使用缩写的函数模板语法。\n\n让我们看一个通过修复我们的`pow_n()`实现来使用`decltype`的例子:\n\n```cpp\nauto pow_n(const auto& v, int n) {\n  auto product = decltype(v){1};   // Instead of T{1}\n  for (int i = 0; i < n; ++ i) { product *= v; } \n  return product;\n} \n```\n\n虽然这段代码编译并运行，但是我们有点幸运，因为`v`的类型实际上是一个`const`引用，而不是我们想要的变量`product`的类型。我们可以通过使用从左到右的声明样式来解决这个问题。但是，试图重写产品被定义为看起来相同的东西的界限揭示了一个问题:\n\n```cpp\nauto pow_n(const auto& v, int n) {\n  decltype(v) product{1};\n  for (int i = 0; i < n; ++ i) { product *= v; } // Error!\n  return product;\n} \n```\n\n现在，我们得到了一个编译错误，因为`product`是一个`const`引用，可能没有被赋值。\n\n我们真正想要的是在定义变量`product`时，从`v`的类型中去掉`const`引用。为此，我们可以使用名为`std::remove_cvref`的便捷模板。我们对`product`的定义应该是这样的:\n\n```cpp\ntypename std::remove_cvref<decltype(v)>::type product{1}; \n```\n\n唷！在这种特殊情况下，坚持我们最初的`template <typename T>`语法可能会更容易。但是现在，您已经学会了如何将`std::remove_cvref`与`decltype`一起使用，这是编写通用 C++ 代码时的常见模式。\n\n在 C++ 20 之前，在泛型 lambdas 的体中常见到`decltype`。然而，现在可以通过向通用 lambdas 添加显式模板参数来避免相当不方便的`decltype`:\n\n```cpp\nauto pow_n = []<class T>(const T& v, int n) { \n  auto product = T{1};\n  for (int i = 0; i < n; ++ i) { product *= v; }\n  return product;\n}; \n```\n\n在λ的定义中，我们编写`<class T>`是为了获得可以在函数体内部使用的参数类型的标识符。\n\n可能需要一些时间来习惯使用`decltype`和实用程序来操作类型。可能`std::remove_cvref`刚开始看起来有点神秘。这是一个来自`<type_traits>`标题的模板，我们将在下一节进一步研究。\n\n# 类型特征\n\n在进行模板元编程时，您可能经常会发现自己处于这样的情况:您需要在编译时处理的类型的信息。在编写常规(非泛型)C++ 代码时，我们使用我们完全了解的具体类型，但在编写模板时情况并非如此；在编译器实例化模板之前，具体的类型是不确定的。类型特征让我们提取模板正在处理的类型的信息，以便生成高效且正确的 C++ 代码。\n\n为了提取关于模板类型的信息，标准库提供了一个类型特征库，可在`<type_traits>`头中获得。所有类型特征都是在编译时评估的。\n\n## 类型特征类别\n\n类型性状有两类:\n\n*   以布尔值或整数值形式返回类型信息的类型特征。\n*   返回新类型的类型特征。这些类型特征也被称为元功能。\n\n第一类根据输入返回`true`或`false`，以`_v`(值的缩写)结束。\n\n`_v`后缀是在 C++ 17 中添加的。如果你的库实现没有为类型特征提供`_v`后缀，那么你可以使用旧版本`std::is_floating_point<float>::value`。换句话说，去掉`_v`分机，在最后加上`::value`。\n\n下面是一些使用基本类型的类型特征进行编译时类型检查的例子:\n\n```cpp\nauto same_type = std::is_same_v<uint8_t, unsigned char>; \nauto is_float_or_double = std::is_floating_point_v<decltype(3.f)>; \n```\n\n类型特征也可以用于用户定义的类型:\n\n```cpp\nclass Planet {};\nclass Mars : public Planet {};\nclass Sun {};\nstatic_assert(std::is_base_of_v<Planet, Mars>);\nstatic_assert(!std::is_base_of_v<Planet, Sun>); \n```\n\n第二类类型特征返回一个新类型，以`_t`(类型的简称)结束。这些类型特征转换(或元函数)在处理指针和引用时非常有用:\n\n```cpp\n// Examples of type traits which transforms types\nusing value_type = std::remove_pointer_t<int*>;  // -> int\nusing ptr_type = std::add_pointer_t<float>;      // -> float* \n```\n\n我们前面使用的类型特征`std::remove_cvref`也是这个范畴的一部分。它从类型中移除引用部分(如果有)以及`const`和`volatile`限定符。`std::remove_cvref`在 C++ 20 中引入。在此之前，通常使用`std::decay`来完成这项任务。\n\n## 使用类型特征\n\n正如已经提到的一样，所有类型特征都是在编译时评估的。例如，这个函数，如果值大于或等于零，则返回`1`，否则返回`-1`，对于无符号整数，可以立即返回`1`，如下所示:\n\n```cpp\ntemplate<typename T>\nauto sign_func(T v) -> int {\n  if (std::is_unsigned_v<T>) { \n    return 1; \n  } \n  return v < 0 ? -1 : 1; \n} \n```\n\n由于类型特征是在编译时评估的，当分别用无符号整数和有符号整数调用时，编译器将生成下表中所示的代码:\n\n<colgroup><col> <col></colgroup> \n| 与无符号整数一起使用... | ...生成的函数: |\n| \n\n```cpp\nauto unsigned_v = uint32_t{42};\nauto sign = sign_func(unsigned_v); \n```\n\n | \n\n```cpp\nint sign_func(uint32_t v) {\n  if (true) { \n    return 1; \n  } \n  return v < 0 ? -1 : 1; \n} \n```\n\n |\n| 与有符号整数一起使用... | ...生成的函数: |\n| \n\n```cpp\nauto signed_v = int32_t{-42}; \nauto sign = sign_func(signed_v); \n```\n\n | \n\n```cpp\nint sign_func(int32_t v) {\n  if (false) { \n    return 1; \n  } \n  return v < 0 ? -1 : 1; \n} \n```\n\n |\n\n表 8.1:根据我们传递给 sign_func()的类型(在左列)，编译器会生成不同的函数(在右列)。\n\n接下来，我们来谈谈常量表达式。\n\n# 用常量表达式编程\n\n前缀为`constexpr`关键字的表达式告诉编译器，表达式应该在编译时计算:\n\n```cpp\nconstexpr auto v = 43 + 12; // Constant expression \n```\n\n`constexpr`关键字也可以和函数一起使用。在这种情况下，它告诉编译器，如果允许编译时评估的所有条件都满足，那么某个函数将在编译时进行评估。否则，它将在运行时执行，就像常规函数一样。\n\nA `constexpr`功能有几个限制；不允许执行以下操作:\n\n*   处理局部静态变量\n*   处理`thread_local`变量\n*   调用任何本身不是`constexpr`函数的函数\n\n使用`constexpr`关键字，编写编译时评估的函数就像编写常规函数一样简单，因为它的参数是常规参数，而不是模板参数。\n\n考虑以下`constexpr`功能:\n\n```cpp\nconstexpr auto sum(int x, int y, int z) { return x + y + z; } \n```\n\n让我们这样调用函数:\n\n```cpp\nconstexpr auto value = sum(3, 4, 5); \n```\n\n由于`sum()`的结果在常量表达式中使用，并且其所有参数都可以在编译时确定，编译器将生成以下常规 C++ 代码:\n\n```cpp\nconst auto value = 12; \n```\n\n然后像往常一样，将其编译成机器代码。换句话说，编译器计算一个`constexpr`函数，并生成计算结果的常规 C++ 代码。\n\n如果我们改为调用`sum()`并将结果存储在标有`constexpr`的变量*而不是*中，编译器*可能会在编译时*(最有可能)评估`sum()`:\n\n```cpp\nauto value = sum(3, 4, 5); // value is not constexpr \n```\n\n总之，如果一个`constexpr`函数是从常量表达式中调用的，并且它的所有参数都是常量表达式，那么它保证会在编译时被求值。\n\n## 运行时上下文中的 Constexpr 函数\n\n在前面的例子中，的求和值(`3`、`4`、`5`)在编译时是编译器已知的，但是`constexpr`函数如何处理那些直到运行时才知道值的变量呢？如前所述，`constexpr`是编译器的一个指示器，在特定条件下，可以在编译时对函数进行求值。如果变量的值在运行时被调用之前是未知的，它们将像常规函数一样被计算。\n\n在下面的例子中，`x`、`y`和`z`的值是在运行时由用户提供的，因此编译器不可能在编译时计算总和:\n\n```cpp\nint x, y, z; \nstd::cin >> x >> y >> z;      // Get user input\nauto value = sum(x, y, z); \n```\n\n如果我们根本不打算在运行时使用`sum()`，我们可以通过使其成为即时函数来禁止这种使用。\n\n## 使用 consteval 声明即时函数\n\n可以在运行时或编译时调用`constexpr`函数。如果我们想限制某个函数的使用，使其只在编译时被调用，我们可以通过使用关键字`consteval`而不是`constexpr`来实现。让我们假设我们想要在运行时禁止`sum()`的所有使用。使用 C++ 20，我们可以用下面的代码做到这一点:\n\n```cpp\nconsteval auto sum(int x, int y, int z) { return x + y + z; } \n```\n\n使用`consteval`声明的函数被称为**立即函数**，并且只能产生常数。如果要调用`sum()`，需要从常量表达式内部调用，否则编译会失败:\n\n```cpp\nconstexpr auto s = sum(1, 2, 3); // OK\nauto x = 10;\nauto s = sum(x, 2, 3);           // Error, expression is not const \n```\n\n如果我们试图将`sum()`用于编译时未知的参数，编译器也会抱怨:\n\n```cpp\nint x, y, z; \nstd::cin >> x >> y >> z; \nconstexpr auto s = sum(x, y, z); // Error \n```\n\n接下来我们讨论`if` `constexpr`语句。\n\n## if constexpr 语句\n\n`if constexpr`语句允许模板函数在编译时评估同一函数中的不同范围(也称为编译时多态性)。看看下面的例子，一个名为`speak()`的函数模板试图根据类型来区分成员函数:\n\n```cpp\nstruct Bear { auto roar() const { std::cout << \"roar\\n\"; } }; \nstruct Duck { auto quack() const { std::cout << \"quack\\n\"; } }; \ntemplate <typename Animal> \nauto speak(const Animal& a) { \n  if (std::is_same_v<Animal, Bear>) { a.roar(); } \n  else if (std::is_same_v<Animal, Duck>) { a.quack(); } \n} \n```\n\n假设我们编译了以下几行:\n\n```cpp\nauto bear = Bear{};\nspeak(bear); \n```\n\n编译器随后将生成一个`speak()`函数，类似如下:\n\n```cpp\nauto speak(const Bear& a) {\n  if (true) { a.roar(); }\n  else if (false) { a.quack(); } // This line will not compile\n} \n```\n\n如您所见，编译器将保留对成员函数`quack()`的调用，这将导致编译失败，因为`Bear`不包含`quack()`成员函数。由于`else if (false)`声明，即使`quack()`成员函数永远不会被执行，也会发生这种情况。\n\n为了使`speak()`函数编译，无论类型如何，我们都需要通知编译器，如果`if`语句是`false`，我们希望完全忽略范围。顺势而为，这正是`if constexpr`所做的。\n\n以下是我们如何编写能够同时处理`Bear`和`Duck`的`speak()`函数，即使它们不共享一个公共接口:\n\n```cpp\ntemplate <typename Animal> \nauto speak(const Animal& a) { \n  if constexpr (std::is_same_v<Animal, Bear>) { a.roar(); } \n  else if constexpr (std::is_same_v<Animal, Duck>) { a.quack(); } \n} \n```\n\n当`Animal == Bear`调用`speak()`时，如下所示:\n\n```cpp\nauto bear = Bear{};\nspeak(bear); \n```\n\n编译器生成以下函数:\n\n```cpp\nauto speak(const Bear& animal) { animal.roar(); } \n```\n\n当`Animal == Duck`、调用`speak()`时，如下所示:\n\n```cpp\nauto duck = Duck{};\nspeak(duck); \n```\n\n编译器生成以下函数:\n\n```cpp\nauto speak(const Duck& animal) { animal.quack(); } \n```\n\n如果用任何其他原语类型调用`speak()`，如`Animal == int`，如下所示:\n\n```cpp\nspeak(42); \n```\n\n编译器生成一个空函数:\n\n```cpp\nauto speak(const int& animal) {} \n```\n\n与常规的`if`语句不同，编译器现在能够生成多个不同的函数:一个使用`Bear`，另一个使用`Duck`，如果类型既不是`Bear`也不是`Duck`，则生成最后一个。如果我们想让第三种情况成为编译错误，我们可以通过添加一个带有`static_assert`的`else`情况来实现:\n\n```cpp\ntemplate <typename Animal> \nauto speak(const Animal& a) { \n  if constexpr (std::is_same_v<Animal, Bear>) { a.roar(); } \n  else if constexpr (std::is_same_v<Animal, Duck>) { a.quack(); }\n  else { static_assert(false); } // Trig compilation error\n} \n```\n\n稍后我们将更多地讨论`static_assert`的用处。\n\n如前所述，这里使用`constexpr`的方式可以称为编译时多态性。那么，它与运行时多态性有什么关系呢？\n\n### 与运行时多态性的比较\n\n作为的旁注，如果我们是用传统的运行时多态性实现前面的例子，使用继承和虚函数来实现相同的功能，那么实现将如下所示:\n\n```cpp\nstruct AnimalBase {\n  virtual ~AnimalBase() {}\n  virtual auto speak() const -> void {}\n};\nstruct Bear : public AnimalBase {\n  auto roar() const { std::cout << \"roar\\n\"; } \n  auto speak() const -> void override { roar(); }\n};\nstruct Duck : public AnimalBase {\n  auto quack() const { std::cout << \"quack\\n\"; }\n  auto speak() const -> void override { quack(); }\n}; \nauto speak(const AnimalBase& a) { \n  a.speak();\n} \n```\n\n必须使用指针或引用来访问对象，并且在*运行时*推断类型，这导致与编译时版本相比的性能损失，在编译时版本中，当应用执行时，一切都是可用的。下图显示了 C++ 中两种类型多态性的区别:\n\n<figure class=\"mediaobject\">![](img/B15619_08_03.png)</figure>\n\n图 8.3:虚函数支持运行时多态性，而函数/运算符重载和 if constexpr 支持编译时多态性。\n\n现在，我们将继续看看如何将`if constexpr`用于更有用的事情。\n\n### 使用 if constexpr 的通用模数函数示例\n\n这个例子将向您展示如何使用`if constexpr`来区分运算符和全局函数。在 C++ 中，`%`运算符用于获取整数的模，而`std::fmod()`用于浮点类型。假设我们想要概括我们的代码库并创建一个名为`generic_mod()`的通用模数函数。\n\n如果我们用常规的`if`语句来实现`generic_mod()`，如下所示:\n\n```cpp\ntemplate <typename T> \nauto generic_mod(const T& v, const T& n) -> T {\n  assert(n != 0);\n  if (std::is_floating_point_v<T>) { return std::fmod(v, n); }\n  else { return v % n; }\n} \n```\n\n如果使用`T == float`调用，它将失败，因为编译器将生成以下函数，这将无法编译:\n\n```cpp\nauto generic_mod(const float& v, const float& n) -> float {\n  assert(n != 0);\n  if (true) { return std::fmod(v, n); }\n  else { return v % n; } // Will not compile\n} \n```\n\n即使应用无法到达它，编译器也会生成不符合`float`的行`return v % n;`。编译器不在乎应用无法访问它——因为它无法为它生成程序集，所以它将无法编译。\n\n如同在前面的例子中，我们将将`if`语句更改为`if constexpr`语句:\n\n```cpp\ntemplate <typename T> \nauto generic_mod(const T& v, const T& n) -> T { \n  assert(n != 0);\n  if constexpr (std::is_floating_point_v<T>) {\n    return std::fmod(v, n);\n  } else {                 // If T is a floating point,\n    return v % n;          // this code is eradicated\n  }\n} \n```\n\n现在，当用浮点类型调用该函数时，它将生成以下函数，其中`v % n`操作被根除:\n\n```cpp\nauto generic_mod(const float& v, const float& n) -> float { \n  assert(n != 0);\n  return std::fmod(v, n); \n} \n```\n\n运行时`assert()`告诉我们，如果第二个参数为 0，我们就不能调用这个函数。\n\n## 编译时检查编程错误\n\nAssert 语句是一个简单但非常强大的工具，用于验证代码库中调用者和被调用者之间的不变量和契约，(参见*第 2 章*、*基本 C++ 技术*。)使用`assert()`可以在执行程序时检查编程错误。但是我们要始终努力尽早发现错误，如果有一个常量表达式，在使用`static_assert()`编译程序的时候就能捕捉到编程错误。\n\n### 使用断言在运行时触发错误\n\n查看`pow_n()`的模板化版本。假设我们想防止用负指数(即`n`值)来调用它。为了防止在运行时版本中出现这种情况，其中`n`是常规参数，我们可以添加一个运行时断言:\n\n```cpp\ntemplate <typename T> \nauto pow_n(const T& v, int n) { \n  assert(n >= 0); // Only works for positive numbers \n  auto product = T{1}; \n  for (int i = 0; i < n; ++ i) {\n    product *= v; \n  }\n  return product; \n} \n```\n\n如果调用函数时`n`为负值，程序会中断，并通知我们应该从哪里开始寻找 bug。这很好，但是如果我们能够在编译时而不是运行时跟踪这个错误，那就更好了。\n\n### 使用 static_assert 在编译时触发错误\n\n如果我们对模板版本做同样的操作，我们可以利用`static_assert()`。与常规断言不同的是，`static_assert()`声明将拒绝编译，如果条件不满足的话。所以，中断构建比在运行时中断程序要好。在下面的例子中，如果模板参数`N`是负数，`static_assert()`将阻止函数编译:\n\n```cpp\ntemplate <int N, typename T>\nauto const_pow_n(const T& v) {\n  static_assert(N >= 0, \"N must be positive\"); \n  auto product = T{1}; \n  for (int i = 0; i < N; ++ i) { \n    product *= v; \n  } \n  return product; \n}\nauto x = const_pow_n<5>(2);  // Compiles, N is positive\nauto y = const_pow_n<-1>(2); // Does not compile, N is negative \n```\n\n换句话说，有了正则变量，编译器只知道类型，不知道包含什么。使用编译时值，编译器知道类型和值。这允许编译器计算其他编译时值。\n\n我们可以(应该)使用`unsigned int`来代替使用`int`并断言它是非负的。在本例中，我们仅使用签名的`int`来演示`assert()`和`static_assert()`的使用。\n\n使用编译时断言是在编译时检查约束的一种方式。这是一个简单但非常有用的工具。在过去的几年里，对编译时编程的支持在 C++ 中取得了一些非常令人兴奋的进展。现在，我们将继续讨论 C++ 20 最大的特性之一，它将约束检查提升到了一个新的水平。\n\n# 制约因素和概念\n\n到目前为止，我们已经介绍了许多编写 C++ 元程序的重要技术。您已经看到了模板如何在类型特征库的出色支持下为我们生成具体的类和函数。此外，您已经看到了`constexpr`、`consteval`和`if constexpr`的使用如何帮助我们将计算从运行时转移到编译时。这样，我们可以在编译时检测编程错误，并以更低的运行时成本编写程序。这很好，但是在用 C++ 编写和使用泛型代码时，仍然有很大的改进空间。我们尚未解决的一些问题包括:\n\n1.  接口太通用了。当使用任意类型的模板时，很难知道该类型的需求是什么。如果我们只检查模板接口，这使得模板很难使用。相反，我们必须依赖文档或者深入挖掘模板的实现。\n2.  编译器很晚才捕捉到类型错误。编译器最终会在编译常规 C++ 代码时检查类型，但是错误消息通常很难解释。相反，我们希望在实例化阶段捕获类型错误。\n3.  不受约束的模板参数使元编程变得困难。到目前为止，我们在本章中编写的代码使用了不受约束的模板参数，除了一些静态断言。对于小示例来说，这是可以管理的，但是如果我们能够访问更有意义的类型，就更容易编写元程序并对其进行推理，就像类型系统帮助我们编写正确的非泛型 C++ 代码一样。\n4.  条件代码生成(编译时多态性)可以使用`if constexpr`来执行，但是它很快变得难以大规模读写。\n\n正如您将在本节中看到的，C++ 概念通过引入两个新的关键词:`concept`和`requires`，以优雅而有效的方式解决了这些问题。在探索约束和概念之前，我们将花一些时间考虑没有概念的模板元编程的缺点。然后，我们将使用约束和概念来加强我们的代码。\n\n## 点 2D 模板的无约束版本\n\n假设我们正在编写一个处理二维坐标系的程序。我们有一个类模板，用`x`和`y`坐标表示一个点，如下所示:\n\n```cpp\ntemplate <typename T>\nclass Point2D {\npublic:\n  Point2D(T x, T y) : x_{x}, y_{y} {}\n  auto x() { return x_; }\n  auto y() { return y_; }\n  // ...\nprivate:\n  T x_{};\n  T y_{};\n}; \n```\n\n假设我们需要找到两点之间的欧几里得距离 **p1** 和 **p2** ，如下图所示:\n\n<figure class=\"mediaobject\">![](img/B15619_08_04.png)</figure>\n\n图 8.4:寻找 p1 和 p2 之间的欧几里得距离\n\n为了计算距离，我们实现了一个取两点的自由函数，并使用了毕达哥拉斯定理(实际的数学在这里不太重要):\n\n```cpp\nauto dist(auto p1, auto p2) {\n  auto a = p1.x() - p2.x();\n  auto b = p1.y() - p2.y();\n  return std::sqrt(a*a + b*b);\n} \n```\n\n一个小的测试程序验证了我们可以用整数实例化`Point2D`模板并计算两点之间的距离:\n\n```cpp\nint main() {\n  auto p1 = Point2D{2, 2};\n  auto p2 = Point2D{6, 5};\n  auto d = dist(p1, p2);\n  std::cout << d;\n} \n```\n\n这段代码编译运行良好，输出`5`到控制台。\n\n### 通用接口和错误消息\n\n在继续之前，让我们绕一小段路，反思一下功能模板`dist()`。假设我们无法轻松访问`dist()`的实现，只能读取界面:\n\n```cpp\nauto dist(auto p1, auto p2) // Interface part \n```\n\n退货类型和`p1`、`p2`的类型能说什么？实际上没什么——因为`p1`和`p2`完全是*不受约束的*，所以`dist()`的界面并没有给我们透露什么。然而，这并不意味着我们可以将任何东西传递给`dist()`，因为最终，生成的常规 C++ 代码必须编译。\n\n例如，如果我们尝试用两个整数而不是像这样的`Point2D`对象来实例化我们的`dist()`模板:\n\n```cpp\n auto d = dist(3, 4); \n```\n\n编译器会很乐意生成一个常规的 C++ 函数，类似于这样:\n\n```cpp\nauto dist(int p1, int p2) {\n  auto a = p1.x() – p2.x();  // Will generate an error:\n  auto b = p1.y() – p2.y();  // int does not have x() and y()\n  return std::sqrt(a*a + b*b);\n} \n```\n\n稍后编译器检查常规 C++ 代码时，将会发现错误。当试图用两个整数实例化`dist()`时，Clang 生成以下错误消息:\n\n```cpp\nerror: member reference base type 'int' is not a structure or union\nauto a = p1.x() – p2.y(); \n```\n\n这个错误消息指的是`dist()`的*实现*，这个功能`dist()`的调用者不需要知道。这是一个微不足道的例子，但是试图解释由于从复杂的模板库中向模板提供错误类型而导致的错误消息可能是一个真正的挑战。\n\n更糟糕的是，如果我们真的运气不好，我们会通过提供完全没有意义的类型来完成整个编译。在这种情况下，我们用`const char*`实例化一个`Point2D`:\n\n```cpp\nint main() {\n  auto from = Point2D{\"2.0\", \"2.0\"}; // Ouch!\n  auto to = Point2D{\"6.0\", \"5.0\"};   // Point2D<const char*>\n  auto d = dist(from, to);\n  std::cout << d;\n} \n```\n\n它编译并运行，但是输出可能不是我们所期望的。我们希望在过程的早期捕捉到这些类型的错误，这可以通过使用如下图所示的约束和概念来实现:\n\n<figure class=\"mediaobject\">![](img/B15619_08_05.png)</figure>\n\n图 8.5:类型错误可以在实例化阶段使用约束和概念来捕获\n\n稍后，您将看到如何使这段代码更具表现力，以便更容易正确使用，更难误用。我们将通过在代码中添加概念和约束来实现这一点。但是首先，我将快速概述如何定义和使用概念。\n\n## 约束和概念的句法概述\n\n本节是对约束和概念的简短介绍。我们不会在本书中完全涵盖它们，但我会为您提供足够的材料来提高生产力。\n\n### 定义新概念\n\n借助你已经熟悉的类型特征，定义新概念很简单。以下示例使用关键字`concept`定义概念`FloatingPoint`:\n\n```cpp\ntemplate <typename T>\nconcept FloatingPoint = std::is_floating_point_v<T>; \n```\n\n赋值表达式的右边是我们可以指定类型`T`的约束的地方。也可以使用`||`(逻辑或)和`&&`(逻辑与)组合多个约束。以下示例使用`||`将浮点和积分组合成一个`Number`概念:\n\n```cpp\ntemplate <typename T>\nconcept Number = FloatingPoint<T> || std::is_integral_v<T>; \n```\n\n您会注意到，也可以使用右侧已经定义的概念来构建概念。标准库包含一个`<concepts>`头，它定义了很多有用的概念，比如`std::floating_point`(我们应该使用它，而不是定义我们自己的)。\n\n此外，我们可以使用`requires`关键字来添加一组应该添加到我们的概念定义中的语句。例如，这是范围库中概念`std::range`的定义:\n\n```cpp\ntemplate<typename T>\nconcept range = requires(T& t) {\n  ranges::begin(t);\n  ranges::end(t);\n}; \n```\n\n简而言之，这个概念表明范围是我们可以传递给`std::ranges::begin()`和`std::ranges::end()`的东西。\n\n可以写比这更复杂的`requires`子句，稍后你会看到更多。\n\n### 用概念约束类型\n\n我们可以使用`requires`关键字为模板参数类型添加约束。以下模板只能使用`std::integral`概念用整型参数实例化:\n\n```cpp\ntemplate <typename T>\nrequires std::integral<T>\nauto mod(T v, T n) { \n  return v % n;\n} \n```\n\n我们可以在定义类模板时使用相同的技术:\n\n```cpp\ntemplate <typename T>\nrequires std::integral<T>\nstruct Foo {\n  T value;\n}; \n```\n\n另一种语法允许我们用更简洁的方式来写，直接用以下概念替换`typename`:\n\n```cpp\ntemplate <std::integral T>\nauto mod(T v, T n) { \n  return v % n;\n} \n```\n\n此表单也可以与类模板一起使用:\n\n```cpp\ntemplate <std::integral T>\nstruct Foo {\n  T value;\n}; \n```\n\n如果我们想在定义函数模板时使用缩写的函数模板形式，可以在`auto`关键字前添加概念:\n\n```cpp\nauto mod(std::integral auto v, std::integral auto n) {\n  return v % n;\n} \n```\n\n返回类型也可以通过使用概念来约束:\n\n```cpp\nstd::integral auto mod(std::integral auto v, std::integral auto n) {\n  return v % n;\n} \n```\n\n如您所见，有许多方法可以指定相同的内容。结合概念的缩写形式确实使读写约束函数模板变得容易。C++ 概念的另一个强大特性是能够以一种清晰和表达的方式重载函数。\n\n### 函数重载\n\n回想一下我们之前使用`if constexpr`实现的`generic_mod()`功能。它看起来像这样:\n\n```cpp\ntemplate <typename T> \nauto generic_mod(T v, T n) -> T { \n  if constexpr (std::is_floating_point_v<T>) {\n    return std::fmod(v, n);\n  } else {\n    return v % n;\n  } \n} \n```\n\n通过使用概念，我们可以重载一个函数模板，就像我们写一个普通的 C++ 函数一样:\n\n```cpp\ntemplate <std::integral T>\nauto generic_mod(T v, T n) -> T {             // Integral version\n  return v % n;\n}\ntemplate <std::floating_point T>\nauto generic_mod(T v, T n) -> T {             // Floating point version\n  return std::fmod(v, n);\n} \n```\n\n有了您对约束和概念的新知识，是时候使用`Point2D`模板回到我们的例子，看看如何改进它了。\n\n## 点 2D 模板的受约束版本\n\n现在知道了如何定义和使用概念，让我们通过编写更好版本的模板`Point2D`和`dist()`来使用它们。请记住，我们的目标是获得一个更具表现力的界面，并让不相关的参数类型导致的错误出现在模板实例化中。\n\n我们将从创建算术类型的概念开始:\n\n```cpp\ntemplate <typename T>\nconcept Arithmetic = std::is_arithmetic_v<T>; \n```\n\n接下来，我们将创建一个名为`Point`的概念，定义一个点应该具有成员函数`x()`和`y()`返回相同的类型，并且该类型应该支持算术运算:\n\n```cpp\ntemplate <typename T>\nconcept Point = requires(T p) {\n  requires std::is_same_v<decltype(p.x()), decltype(p.y())>;\n  requires Arithmetic<decltype(p.x())>;\n}; \n```\n\n这个概念现在可以通过显式约束使`dist()`的界面变得更好:\n\n```cpp\nauto dist(Point auto p1, Point auto p2) {\n  // Same as before ... \n```\n\n这是开始看起来非常有希望，所以让我们也给我们的返回类型添加一个约束。虽然`Point2D`可能是用整型实例化的，但是我们知道距离可以是一个浮点数。标准库中的概念`std::floating_point`非常适合于此。以下是`dist()`的最终版本:\n\n```cpp\nstd::floating_point auto dist(Point auto p1, Point auto p2) { \n  auto a = p1.x() - p2.x();\n  auto b = p1.y() - p2.y();\n  return std::sqrt(a*a + b*b);\n} \n```\n\n我们的接口现在更具描述性，当我们试图用错误的参数类型实例化它时，我们将在实例化阶段而不是最终编译阶段得到错误。\n\n我们现在应该对我们的`Point2D`模板做同样的事情，以避免有人不小心用它不打算处理的类型实例化它。例如，我们希望防止有人用`const char*`实例化`Point2D`类，如下所示:\n\n```cpp\nauto p1 = Point2D{\"2.0\", \"2.0\"}; // How can we prevent this? \n```\n\n我们已经创建了`Arithmetic`概念，我们可以在这里使用它在`Point2D`的模板参数中放置约束。我们是这样做的:\n\n```cpp\ntemplate <Arithmetic T> // T is now constrained!\nclass Point2D {\npublic:\n  Point2D(T x, T y) : x_{x}, y_{y} {}\n  auto x() { return x_; }\n  auto y() { return y_; }\n  // ...\nprivate:\n  T x_{};\n  T y_{};\n}; \n```\n\n我们唯一需要改变的是指定类型`T`应该支持由概念`Arithmetic`指定的操作。当编译器试图实例化一个`Point2D<const char*>`类时，试图使用`const char*`实例化一个模板将会产生一个直接的错误消息。\n\n## 向代码中添加约束\n\n概念的有用性远远超出了模板元编程。这是 C++ 20 的一个基本特性，它改变了我们使用概念而不是具体类型或用`auto`声明的完全不受约束的变量来编写和推理代码的方式。\n\n一个概念非常类似于一个类型(如`int`、`float`或`Plot2D<int>`)。类型和概念都指定了一组支持的对象操作。通过检查类型或概念，我们可以确定成员函数如何构造、移动、比较和访问某些对象，等等。然而，一个很大的区别是，一个概念并没有说明一个对象是如何存储在内存中的，而一个类型除了提供它所支持的一组操作之外，还提供了这些信息。例如，我们可以在类型上使用`sizeof`运算符，但不能在概念上使用。\n\n有了概念和`auto`，我们可以声明变量，而不需要拼写出确切的类型，但仍然可以用我们的代码非常清楚地表达意图。看看下面的代码片段:\n\n```cpp\nconst auto& v = get_by_id(42); // What can I do with v? \n```\n\n大多数时候，当我们偶然发现这样的代码时，我们感兴趣的是知道我们可以在`v`上执行什么操作，而不是知道确切的类型。在`auto`前面加上一个概念就不同了:\n\n```cpp\nconst Person auto& v = get_by_id(42);\nv.get_name(); \n```\n\n几乎在所有我们可以使用关键字`auto`的上下文中都可以使用概念:局部变量、返回值、函数参数等等。在我们的代码中使用概念使更容易阅读。在撰写本书时(2020 年年中)，目前还没有对已建立的 C++ IDEs 中的概念的额外支持。然而，代码完成只是时间问题，基于概念的其他有用的编辑器功能也将可用，并使 C++ 编码更加有趣和安全。\n\n## 标准库中的概念\n\nC++ 20 还包含了一个带有预定义概念的新`<concepts>`头。你已经看到了的一些动作。许多概念都是基于类型特征库中的特征。然而，有几个基本概念以前没有用特征来表达。其中最重要的是`std::equality_comparable`、`std::totally_ordered`等比较概念，以及`std::movable`、`std::copyable`、`std::regular`、`std::semiregular`等客体概念。我们不会在标准库中的概念上花费更多的时间，但是在开始定义自己的概念之前，请记住记住它们。在正确的一般性水平上定义概念并不容易，在现有概念的基础上定义新概念通常是明智的。\n\n让我们通过看一些 C++ 中元编程的真实例子来结束这一章。\n\n# 元编程的真实例子\n\n高级元编程可以看起来非常学术，所以为了展示它的有用性，让我们看一些例子，这些例子不仅展示了元编程的语法，还展示了它如何在实践中使用。\n\n## 示例 1:创建通用安全转换函数\n\n当在 C++ 中的数据类型之间进行转换时，有许多不同的方法会出错:\n\n*   如果转换为位长度较低的整数类型，可能会丢失一个值。\n*   如果将负值转换为无符号整数，可能会丢失一个值。\n*   如果从指针转换到除`uintptr_t`以外的任何整数，正确的地址可能会变得不正确。这是因为 C++ 只保证`uintptr_t`是唯一可以保留地址的整数类型。\n*   如果从`double`到`float`选角，结果可能是`int`如果`double`数值太大`float`无法扣压。\n*   如果用`static_cast()`在指针之间进行转换，如果类型没有共享一个公共基类，我们可能会得到未定义的行为。\n\n为了使我们的代码更加健壮，我们可以创建一个通用的检查转换函数，该函数在调试模式下验证我们的转换，并在发布模式下尽可能快地执行我们的转换。\n\n根据要转换的类型，会执行不同的检查。如果我们试图在未验证的类型之间转换，它将不会编译。\n\n这些是`safe_cast()`要处理的情况:\n\n*   **同类型**:很明显，如果我们铸造的是同类型，我们只需要返回输入值。\n*   **指向指针**的指针:如果在指针之间进行转换，`safe_cast()`在调试模式下执行动态转换，以验证其是否可转换。\n*   **双精度到浮点** : `safe_cast()`在从`double`到`float`进行铸造时接受精度损失，但有一个例外——如果从`double`到`float`进行铸造，双精度可能太大，浮动无法处理结果。\n*   **算术到算术**:如果在算术类型之间转换，值会转换回其原始类型，以验证精度没有丢失。\n*   **指向非指针的指针**:如果从指针转换为非指针类型，`safe_cast()`验证目标类型是`uintptr_t`或`intptr_t`，这是唯一保证保存地址的整数类型。\n\n在任何其他情况下，`safe_cast()`函数都无法编译。\n\n让我们看看如何实现这一点。我们从在`constexpr`布尔中获取关于我们的强制转换操作的信息开始。它们之所以是`constexpr`布尔人而不是`const`布尔人，是因为我们稍后将在`if constexpr`表达式中使用它们，这需要`constexpr`条件:\n\n```cpp\ntemplate <typename T> constexpr auto make_false() { return false; }\ntemplate <typename Dst, typename Src> \nauto safe_cast(const Src& v) -> Dst{ \n  using namespace std;\n  constexpr auto is_same_type = is_same_v<Src, Dst>;\n  constexpr auto is_pointer_to_pointer =  \n    is_pointer_v<Src> && is_pointer_v<Dst>; \n  constexpr auto is_float_to_float =  \n    is_floating_point_v<Src> && is_floating_point_v<Dst>; \n  constexpr auto is_number_to_number =  \n    is_arithmetic_v<Src> && is_arithmetic_v<Dst>; \n  constexpr auto is_intptr_to_ptr = \n    (is_same_v<uintptr_t,Src> || is_same_v<intptr_t,Src>)\n    && is_pointer_v<Dst>;\n  constexpr auto is_ptr_to_intptr =\n    is_pointer_v<Src> &&\n    (is_same_v<uintptr_t,Dst> || is_same_v<intptr_t,Dst>); \n```\n\n因此，既然已经拥有了关于作为`constexpr`布尔人的强制转换的所有必要信息，我们在编译时断言我们可以执行强制转换。如前所述，如果条件不满足，则`static_assert()`将无法编译(不同于常规断言，后者在运行时验证条件)。\n\n注意`if` / `else`链条末端`static_assert()`和`make_false<T>`的用法。我们不能仅仅输入`static_assert(false)`，因为那样会阻止`safe_cast()`编译；相反，我们利用模板功能`make_false<T>()`将生成延迟到需要的时候。\n\n当执行实际的`static_cast()`时，我们转换回原始类型，并使用常规运行时`assert()`验证结果是否等于未测试的参数。这样，我们可以确保`static_cast()`没有丢失任何数据:\n\n```cpp\n if constexpr(is_same_type) { \n    return v; \n  }\n  else if constexpr(is_intptr_to_ptr || is_ptr_to_intptr){\n    return reinterpret_cast<Dst>(v); \n  } \n  else if constexpr(is_pointer_to_pointer) { \n    assert(dynamic_cast<Dst>(v) != nullptr); \n    return static_cast<Dst>(v); \n  } \n  else if constexpr (is_float_to_float) { \n    auto casted = static_cast<Dst>(v); \n    auto casted_back = static_cast<Src>(v); \n    assert(!isnan(casted_back) && !isinf(casted_back)); \n    return casted; \n  }  \n  else if constexpr (is_number_to_number) { \n    auto casted = static_cast<Dst>(v); \n    auto casted_back = static_cast<Src>(casted); \n    assert(casted == casted_back); \n    return casted; \n  } \n  else {\n    static_assert(make_false<Src>(),\"CastError\");\n    return Dst{}; // This can never happen, \n    // the static_assert should have failed \n  }\n} \n```\n\n注意我们如何使用`if constexpr`来有条件地编译函数。如果我们使用常规的`if`语句，函数将无法编译:\n\n```cpp\nauto x = safe_cast<int>(42.0f); \n```\n\n这是因为编译器会尝试编译下面一行`dynamic_cast`只接受指针:\n\n```cpp\n// type To is an integer\nassert(dynamic_cast<int>(v) != nullptr); // Does not compile \n```\n\n但是，由于有了`if constexpr`和`safe_cast<int>(42.0f)`构造，以下函数可以正确编译:\n\n```cpp\nauto safe_cast(const float& v) -> int {\n  constexpr auto is_same_type = false;\n  constexpr auto is_pointer_to_pointer = false;\n  constexpr auto is_float_to_float = false;\n  constexpr auto is_number_to_number = true;\n  constexpr auto is_intptr_to_ptr = false;\n  constexpr auto is_ptr_to_intptr = false\n  if constexpr(is_same_type) { /* Eradicated */ }\n  else if constexpr(is_intptr_to_ptr||is_ptr_to_intptr){/* Eradicated */}\n  else if constexpr(is_pointer_to_pointer) {/* Eradicated */}\n  else if constexpr(is_float_to_float) {/* Eradicated */}\n  else if constexpr(is_number_to_number) {\n    auto casted = static_cast<int>(v);\n    auto casted_back = static_cast<float>(casted);\n    assert(casted == casted_back);\n    return casted;\n  }\n  else { /* Eradicated */ }\n} \n```\n\n正如你所看到的，除了`is_number_to_number`子句之外，`if constexpr`语句之间的所有东西都被完全消除了，允许函数编译。\n\n## 示例 2:编译时的哈希字符串\n\n假设我们有一个资源系统，由识别位图的无序字符串映射组成。如果已经加载了位图，系统返回加载的位图；否则，它会加载位图并返回它:\n\n```cpp\n// External function which loads a bitmap from the filesystem\nauto load_bitmap_from_filesystem(const char* path) -> Bitmap {/* ... */}\n// Bitmap cache \nauto get_bitmap_resource(const std::string& path) -> const Bitmap& { \n  // Static storage of all loaded bitmaps\n  static auto loaded = std::unordered_map<std::string, Bitmap>{};\n  // If the bitmap is already in loaded_bitmaps, return it\n  if (loaded.count(path) > 0) {\n    return loaded.at(path);\n  } \n  // The bitmap isn't already loaded, load and return it \n  auto bitmap = load_bitmap_from_filesystem(path.c_str());\n  loaded.emplace(path, std::move(bitmap)); \n  return loaded.at(path); \n} \n```\n\n然后，在需要位图资源的地方使用位图缓存:\n\n*   如果尚未加载，`get_bitmap_resource()`功能将加载并返回\n*   如果已经加载到其他地方，`get_bitmap_resource()`将简单地返回加载的函数\n\n因此，不管先执行哪一个绘制函数，第二个函数都不必从磁盘加载位图:\n\n```cpp\nauto draw_something() {\n  const auto& bm = get_bitmap_resource(\"my_bitmap.png\");\n  draw_bitmap(bm);\n}\nauto draw_something_again() {\n  const auto& bm = get_bitmap_resource(\"my_bitmap.png\");\n  draw_bitmap(bm);\n} \n```\n\n由于我们使用的是无序映射，所以每当我们检查位图资源时，都需要计算哈希值。现在，您将看到我们如何通过将计算转移到编译时来优化运行时代码。\n\n### 编译时散列和计算的优点\n\n我们将尝试解决的问题是，每次执行第`get_bitmap_resource(\"my_bitmap.png\")`行时，应用都会在运行时计算字符串`\"my_bitmap.png\"`的散列和。我们想要做的是在编译时执行这个计算，这样当应用执行时，散列和已经被计算出来了。换句话说，正如您已经学会了在编译时使用元编程来生成函数和类一样，我们现在将让它在编译时生成散列和。\n\n你可能已经得出结论，这是一个所谓的*微优化*:计算一个小字符串的散列和完全不会影响应用的性能，因为这是一个非常小的操作。这大概是完全正确的；这只是如何将计算从运行时转移到编译时的一个示例，可能还有其他情况会对性能产生重大影响。\n\n顺便说一下，当为弱硬件编写软件时，字符串散列是一种纯粹的奢侈，但是在编译时散列字符串在任何平台上都给了我们这种奢侈，因为一切都是在编译时计算的。\n\n### 实现并验证编译时哈希函数\n\n为了使编译器能够在编译时计算散列和，我们重写了`hash_function()`，使其将原始的空终止`char`字符串作为像`std::string`这样的高级类的参数，该类在编译时无法计算。现在，我们可以将`hash_function()`标记为`constexpr`:\n\n```cpp\nconstexpr auto hash_function(const char* str) -> size_t {\n  auto sum = size_t{0};\n  for (auto ptr = str; *ptr != '\\0'; ++ ptr)\n    sum += *ptr;\n  return sum;\n} \n```\n\n现在，让我们用编译时已知的原始文本字符串来调用它:\n\n```cpp\nauto hash = hash_function(\"abc\"); \n```\n\n编译器将生成以下代码，即`a`、`b`和`c` ( `97`、`98`、`99`)对应的 ASCII 值之和:\n\n```cpp\nauto hash = size_t{294}; \n```\n\n仅仅累加单个值是一个非常糟糕的散列函数；不要在实际应用中这样做。之所以在这里只是因为容易把握。更好的散列函数是将所有单个字符与`boost::hash_combine()`结合，如*第 4 章*、*数据结构*所述。\n\n`hash_function()`只有编译器在编译时知道字符串，才会在编译时求值；如果没有，编译器会在运行时执行`constexpr`，就像其他任何表达式一样。\n\n现在我们已经有了散列函数，是时候创建一个使用它的字符串类了。\n\n### 构造一个预散列的字符串类\n\n我们现在已经准备好为预散列字符串实现一个类，该类将使用我们创建的散列函数。该类包括以下内容:\n\n*   将原始字符串作为参数并在构造时计算哈希的构造函数。\n*   比较运算符。\n*   一个`get_hash()`成员函数，返回哈希值。\n*   `std::hash()`的重载，它只是返回哈希值。此重载由`std::unordered_map`、`std::unordered_set`或标准库中使用哈希值的任何其他类使用。简单来说，这使得容器知道`PrehashedString`有一个散列函数。\n\n下面是一个`PrehashedString`类的基本实现:\n\n```cpp\nclass PrehashedString {\npublic:\n  template <size_t N>\n  constexpr PrehashedString(const char(&str)[N])\n      : hash_{hash_function(&str[0])}, size_{N - 1},\n      // The subtraction is to avoid null at end\n        strptr_{&str[0]} {}\n  auto operator==(const PrehashedString& s) const {\n    return\n      size_ == s.size_ &&\n      std::equal(c_str(), c_str() + size_, s.c_str());\n  }\n  auto operator!=(const PrehashedString& s) const {\n    return !(*this == s); }\n  constexpr auto size()const{ return size_; }\n  constexpr auto get_hash()const{ return hash_; }\n  constexpr auto c_str()const->const char*{ return strptr_; }\nprivate:\n  size_t hash_{};\n  size_t size_{};\n  const char* strptr_{nullptr};\n};\nnamespace std {\ntemplate <>\nstruct hash<PrehashedString> {\n  constexpr auto operator()(const PrehashedString& s) const {\n    return s.get_hash();\n  }\n};\n} // namespace std \n```\n\n注意构造函数中的模板技巧。这迫使`PrehashedString`只接受编译时字符串。原因是`PrehashedString`类不拥有`const char* ptr`，因此我们只能在编译时创建的字符串中使用它:\n\n```cpp\n// This compiles\nauto prehashed_string = PrehashedString{\"my_string\"};\n// This does not compile\n// The prehashed_string object would be broken if the str is modified\nauto str = std::string{\"my_string\"};\nauto prehashed_string = PrehashedString{str.c_str()};\n// This does not compile.\n// The prehashed_string object would be broken if the strptr is deleted\nauto* strptr = new char[5];\nauto prehashed_string = PrehashedString{strptr}; \n```\n\n那么，现在我们已经准备好了一切，让我们看看编译器是如何处理`PrehashedString`的。\n\n### 计算预灰化字符串\n\n这里有一个简单的测试函数，返回字符串`\"abc\"` 的哈希值(为了简单起见):\n\n```cpp\nauto test_prehashed_string() {\n  const auto& hash_fn = std::hash<PrehashedString>{};\n  const auto& str = PrehashedString(\"abc\");\n  return hash_fn(str);\n} \n```\n\n由于我们的哈希函数只是对值求和，并且`\"abc\"`中的字母的 ASCII 值为 *a* = 97、 *b* = 98、 *c* = 99，汇编程序(由 Clang 生成)应该在某处输出和 97 + 98 + 99 = 294。检查汇编器，我们可以看到`test_prehashed_string()`函数恰好编译成一个`return`语句，返回`294`:\n\n```cpp\nmov eax, 294\nret \n```\n\n这意味着整个`test_prehashed_string()`函数已经在编译时执行；当应用执行时，散列和已经被计算出来了！\n\n### 用预灰化字符串计算 get_bitmap_resource()\n\n让我们回到我们最初的`get_bitmap_resource()`功能，`std::string`，它最初是用换来的一个`PrehashedString`:\n\n```cpp\n// Bitmap cache\nauto get_bitmap_resource(const PrehashedString& path) -> const Bitmap& \n{\n  // Static storage of all loaded bitmaps\n  static auto loaded_bitmaps =\n    std::unordered_map<PrehashedString, Bitmap>{};\n  // If the bitmap is already in loaded_bitmaps, return it\n  if (loaded_bitmaps.count(path) > 0) {\n    return loaded_bitmaps.at(path);\n  }\n  // The bitmap isn't already loaded, load and return it\n  auto bitmap = load_bitmap_from_filesystem(path.c_str());\n  loaded_bitmaps.emplace(path, std::move(bitmap));\n  return loaded_bitmaps.at(path);\n} \n```\n\n我们还需要一个函数来测试:\n\n```cpp\nauto test_get_bitmap_resource() { return get_bitmap_resource(\"abc\"); } \n```\n\n我们想知道的是这个函数是否预先计算了散列和。由于`get_bitmap_resource()`做了相当多的工作(构造一个静态的`std::unordered_map`、检查地图等等)，最终的装配大约是 500 行。然而，如果我们的神奇散列和在汇编程序中被发现，这意味着我们成功了。\n\n当检查由 Clang 生成的汇编程序时，我们会发现一行对应于我们的散列和，`294`:\n\n```cpp\n.quad   294                     # 0x126 \n```\n\n为了确认这一点，我们将字符串从`\"abc\"`更改为`\"aaa\"`，这应该会将汇编器中的这一行更改为 97 * 3 = 291，但其他一切都应该完全相同。\n\n我们这样做是为了确保这不仅仅是突然出现的其他神奇数字，与散列和完全无关。\n\n检查生成的汇编程序，我们会发现期望的结果:\n\n```cpp\n.quad   291                     # 0x123 \n```\n\n除了这一行之外，所有内容都是相同的，因此我们可以放心地假设散列是在编译时计算的。\n\n我们看到的例子表明，我们可以将编译时编程用于非常不同的事情。添加可以在编译时验证的安全检查允许我们在不运行程序和用覆盖测试搜索错误的情况下发现错误。并且将昂贵的运行时操作转移到编译时间会使我们的最终程序更快。\n\n# 摘要\n\n在本章中，您已经学习了如何使用元编程在编译时而不是运行时生成函数和值。您还发现了如何通过使用模板、`constexpr`、`static_assert()`和`if constexpr`、类型特征和概念，以现代 C++ 方式实现这一点。此外，通过常量字符串散列，您看到了如何在实际环境中使用编译时计算。\n\n在下一章中，您将学习如何进一步扩展您的 C++ 工具箱，以便您可以通过构造隐藏的代理对象来创建库。"
  },
  {
    "path": "docs/cpp-hiperf/09.md",
    "content": "# 九、基本工具\n\n本章将介绍 C++ 实用程序库中的一些基本类。为了有效地处理包含不同类型元素的集合，将使用前一章中介绍的一些元编程技术。\n\nC++ 容器是同质的，这意味着它们只能存储一种类型的元素。一个`std::vector<int>`存储一组整数，所有存储在一个`std::list<Boat>`中的对象都属于`Boat`类型。但是有时候，我们需要跟踪不同类型元素的集合。我将这些集合称为**异质集合**。在异构集合中，元素可能有不同的类型。下图显示了`int`的同质集合和包含不同类型元素的异质集合的示例:\n\n<figure class=\"mediaobject\">![](img/B15619_09_01.png)</figure>\n\n图 9.1:同质和异质集合\n\n本章将介绍 C++ 实用程序库中的一组有用的模板，这些模板可用于存储各种类型的多个值。本章分为四节:\n\n*   用`std::optional`表示可选值\n*   使用`std::pair`、`std::tuple`和`std::tie()`的固定大小集合\n*   使用带有类型`std::any`和`std::variant`元素的标准容器动态调整集合大小\n*   一些真实的例子证明了`std::tuple`和`std::tie()`的有用性，以及我们在*第 8 章*、*编译时编程*中介绍的元编程概念\n\n让我们从探索`std::optional`及其一些重要的用例开始。\n\n# 用 std::optional 表示可选值\n\n虽然 C++ 17 中的一个很小的特性，`std::optional`是标准库的一个很好的补充。它简化了一个在`std::optional`之前无法清晰直接表达的常见案例。简而言之，它是任何类型的小型包装器，其中包装的类型可以是初始化的，也可以是未初始化的。\n\n用 C++ 的行话来说，`std::optional`是一个*堆栈分配的容器，最大大小为一个*。\n\n## 可选返回值\n\n在引入`std::optional`之前，没有明确的方法定义可能不返回定义值的函数，比如两条线段的交点。有了`std::optional`的引入，这样的可选返回值就可以清晰的表达出来。下面是一个函数的实现，该函数返回两行之间的可选交集:\n\n```cpp\n// Prerequisite\nstruct Point { /* ... */ }; \nstruct Line { /* ... */ };  \nauto lines_are_parallel(Line a, Line b) -> bool { /* ... */ }\nauto compute_intersection(Line a, Line b) -> Point { /* ... */ }\nauto get_intersection(const Line& a, const Line& b) \n  -> std::optional<Point> \n{\n  if (lines_are_parallel(a, b))\n    return std::optional{compute_intersection(a, b)};\n  else\n    return {};\n} \n```\n\n`std::optional`的语法类似指针的语法；该值由`operator*()`或`operator->()`访问。试图使用`operator*()`或`operator->()`访问空可选值是未定义的行为。也可以使用`value()`成员函数访问该值，如果可选值不包含任何值，该函数将引发`std::bad_optional_access`异常。下面是一个返回的`std::optional`的简单例子:\n\n```cpp\nauto set_magic_point(Point p) { /* ... */ }\nauto intersection = get_intersection(line0, line1);\nif (intersection.has_value()) {\n  set_magic_point(*intersection);\n} \n```\n\n`std::optional`持有的对象始终是堆栈分配的，将一个类型包装成`std::optional`的内存开销是一个 bool 的大小(通常是一个字节)，加上可能的填充。\n\n## 可选成员变量\n\n假设我们有一个代表人头的类。头部可以有某种帽子，也可以没有帽子。通过使用`std::optional`来表示 hat 成员变量，实现尽可能具有表现力:\n\n```cpp\nstruct Hat { /* ... */ };\nclass Head {\npublic:\n  Head() { assert(!hat_); }      // hat_ is empty by default\n  auto set_hat(const Hat& h) { \n    hat_ = h; \n  }\n  auto has_hat() const { \n    return hat_.has_value(); \n  }\n  auto& get_hat() const { \n    assert(hat_.has_value()); \n    return *hat_; \n  }\n  auto remove_hat() { \n    hat_ = {};        // Hat is cleared by assigning to {}\n  } \nprivate:\n  std::optional<Hat> hat_;\n}; \n```\n\n如果没有`std::optional`，表示可选成员变量将依赖于例如指针或额外的`bool`成员变量。两者都有缺点，例如在堆上分配，或者在没有警告的情况下意外访问被认为是空的可选对象。\n\n## 避免枚举中的空状态\n\n在旧的 C++ 代码库中可以看到的一种模式是`enum` s 中的*空状态*或*空状态*。\n\n```cpp\nenum class Color { red, blue, none };  // Don't do this! \n```\n\n在前面的`enum`中，`none`是所谓的空状态。在`Color` `enum`中添加`none`值的原因是为了能够表示可选颜色，例如:\n\n```cpp\nauto get_color() -> Color; // Returns an optional color \n```\n\n但是这种设计没有办法表示非可选的颜色，这就使得*所有的*代码都需要处理多余的空状态`none`。\n\n更好的选择是避免额外的空状态，而是用类型`std::optional<Color>`表示可选颜色:\n\n```cpp\nenum class Color { red, blue };\nauto get_color() -> std::optional<Color>; \n```\n\n这清楚地表明，我们可能不会得到一个颜色回来。但是我们也知道，一旦我们有了一个`Color`对象，它就不可能是空的:\n\n```cpp\nauto set_color(Color c) { /* c is a valid color, now use it ... */ } \n```\n\n实现`set_color()`时，我们知道客户端已经传递了一个有效的颜色。\n\n## 排序和比较标准::可选\n\n使用下表所示的规则`std::optional`同样具有可比性和可排序性:\n\n<colgroup><col> <col></colgroup> \n| 两个*空*可选值被认为相等。 | 一个空的可选项被认为比一个非空的少*。* |\n| \n\n```cpp\nauto a = std::optional<int>{};\nauto b = std::optional<int>{};\nauto c = std::optional<int>{4};\nassert(a == b);\nassert(b != c); \n```\n\n | \n\n```cpp\nauto a = std::optional<int>{};\nauto b = std::optional<int>{4};\nauto c = std::optional<int>{5};\nassert(a < b);\nassert(b < c); \n```\n\n |\n\n因此，如果对`std::optional<T>`的容器进行排序，空的可选值将在容器的开头结束，而非空的可选值将照常排序，如下所示:\n\n```cpp\nauto c = std::vector<std::optional<int>>{{3}, {}, {1}, {}, {2}};\nstd::sort(c.begin(), c.end());\n// c is {}, {}, {1}, {2}, {3} \n```\n\n如果您习惯于使用指针表示可选值，使用 out 参数设计 API，或者在枚举中添加特殊的空状态，那么是时候将`std::optional`添加到您的工具箱中了，因为它为这些反模式提供了一个高效且安全的替代方案。\n\n让我们继续探索可以容纳不同类型元素的固定大小的集合。\n\n# 固定大小的异构集合\n\nC++ 实用程序库包括两个类模板，可用于存储不同类型的多个值:`std::pair`和`std::tuple`。它们都是固定大小的集合。就像`std::array`一样，不可能在运行时动态添加更多的值。\n\n`std::pair`和`std::tuple`的最大区别在于`std::pair`只能保存两个值，而`std::tuple`可以在编译时以任意大小实例化。在进入`std::tuple`之前，我们先简单介绍一下`std::pair`。\n\n## 使用标准::对\n\n类模板`std::pair`存在于`<utility>`头中，自从引入标准模板库以来，在 C++ 中已经可用。它用在标准库中，算法需要返回两个值，例如`std::minmax()`，它可以返回初始值设定项列表的最小值和最大值:\n\n```cpp\nstd::pair<int, int> v = std::minmax({4, 3, 2, 4, 5, 1});\nstd::cout << v.first << \" \" << v.second;     // Outputs: \"1 5\" \n```\n\n前面的示例显示了可以通过成员`first`和`second`访问`std::pair`的元素。\n\n在这里，`std::pair`保存相同类型的值，所以，也可以在这里返回一个数组。但是让`std::pair`更有趣的是它可以保存不同类型的*值。这就是为什么我们认为这是一个异类集合，尽管事实上它只能保存两个值。*\n\n标准库中`std::pair`保存不同值的一个例子是关联容器`std::map`。`std::map`的值类型是由键和与键相关联的元素组成的一对:\n\n```cpp\nauto scores = std::map<std::string, int>{};\nscores.insert(std::pair{\"Neo\", 12}); // Correct but ineffecient\nscores.emplace(\"Tri\", 45);           // Use emplace() instead\nscores.emplace(\"Ari\", 33);\nfor (auto&& it : scores) { // \"it\" is a std::pair\n  auto key = it.first;\n  auto val = it.second;\n  std::cout << key << \": \" << val << '\\n';\n} \n```\n\n显式命名`std::pair`类型的要求已经降低，在现代 C++ 中，使用初始化列表和结构化绑定来隐藏我们正在处理`std::pair`的值的事实是很常见的。下面的例子表达了同样的事情，但没有明确提到潜在的`std::pair`:\n\n```cpp\nauto scores = std::map<std::string, int> {\n  {\"Neo\", 12},                            // Initializer lists\n  {\"Tri\", 45},\n  {\"Ari\", 33}\n};\nfor (auto&& [key, val] : scores) {       // Structured bindings\n  std::cout << key << \": \" << val << '\\n';\n} \n```\n\n我们将在本章后面的内容中更多地讨论结构化绑定。\n\n顾名思义，`std::pair`只能保存两个值。C++ 11 引入了一个名为`std::tuple`的新实用类，它是`std::pair`的推广，可以容纳任意数量的元素。\n\n## 标准::元组\n\n`std::tuple`可以用作固定大小的异构集合，可以声明为任何大小。例如与`std::vector`相比，它的大小在运行时不能改变；您不能添加或删除元素。\n\n元组可以通过如下方式明确指定其成员类型来构建:\n\n```cpp\nauto t = std::tuple<int, std::string, bool>{}; \n```\n\n或者，我们可以使用类模板参数推导来初始化它，如下所示:\n\n```cpp\nauto t = std::tuple{0, std::string{}, false}; \n```\n\n这将使编译器生成一个类，大致可以这样看:\n\n```cpp\nstruct Tuple {\n  int data0_{};\n  std::string data1_{};\n  bool data2_{};\n}; \n```\n\n与 C++ 标准库中的许多其他类一样，`std::tuple`也有一个对应的`std::make_tuple()`函数，它从参数中自动推导出类型:\n\n```cpp\nauto t = std::make_tuple(42, std::string{\"hi\"}, true); \n```\n\n但是正如前面所说的，从 C++ 17 开始，这些`std::make_`函数中的很多都是多余的，因为 C++ 17 类可以从构造函数中推导出这些类型。\n\n### 访问元组的成员\n\n使用自由功能模板`std::get<Index>()`可以访问`std::tuple`的各个元素。你可能想知道为什么不能像普通的`at(size_t index)`成员功能的容器一样访问成员。原因是像`at()`这样的成员函数只允许返回一种类型，而元组由不同索引处的不同类型组成。相反，函数模板`std::get()`与索引一起用作模板参数:\n\n```cpp\nauto a = std::get<0>(t);     // int\nauto b = std::get<1>(t);     // std::string\nauto c = std::get<2>(t);     // bool \n```\n\n我们可以想象`std::get()`函数是这样实现的:\n\n```cpp\ntemplate <size_t Index, typename Tuple>\nauto& get(const Tuple& t) {\n  if constexpr(Index == 0) {\n    return t.data0_;\n  } else if constexpr(Index == 1) {\n    return t.data1_;\n  } else if constexpr(Index == 2) {\n    return t.data2_;\n  }\n} \n```\n\n这意味着当我们如下创建和访问元组时:\n\n```cpp\nauto t = std::tuple(42, true);\nauto v = std::get<0>(t); \n```\n\n编译器大致生成以下代码:\n\n```cpp\n// The Tuple class is generated first:\nclass Tuple { \n  int data0_{};\n  bool data1_{};\npublic:\n  Tuple(int v0, bool v1) : data0_{v0}, data1_{v1} {} \n};\n// get<0>(Tuple) is then generated to something like this:\nauto& get(const Tuple& tpl) { return data0_; }\n\n// The generated function is then utilized:\nauto t = Tuple(42, true); \nauto v = get(t); \n```\n\n请注意，这个例子只能被认为是一种简单的方式来想象编译器在构建`std::tuple`时会生成什么；`std::tuple`的内部非常复杂。然而，重要的是要理解`std::tuple`类基本上是一个简单的结构，其成员可以被编译时索引访问。\n\n`std::get()`函数模板也可以使用 typename 作为参数。它是这样使用的:\n\n```cpp\nauto number = std::get<int>(tuple);\nauto str = std::get<std::string>(tuple); \n```\n\n只有当指定的类型在元组中包含一次时，这才是可能的。\n\n### 迭代标准::元组成员\n\n从程序员的角度来看，`std::tuple`似乎可以像任何其他容器一样，用基于范围的常规`for`循环进行迭代，如下所示:\n\n```cpp\nauto t = std::tuple(1, true, std::string{\"Jedi\"});\nfor (const auto& v : t) {\n  std::cout << v << \" \";\n} \n```\n\n这不可能的原因是`const auto& v`的类型只被求值一次，并且由于`std::tuple`包含不同类型的元素，所以这段代码根本不会编译。\n\n常规算法也是如此，因为迭代器不会改变指向的类型；因此，`std::tuple`不提供`begin()`或`end()`成员函数，也不提供下标运算符`[]`来访问值。因此，我们需要想出一些其他的方法来展开元组。\n\n### 展开元组\n\n由于元组不能像往常一样迭代，我们需要做的是使用元编程来展开循环。从前面的例子中，我们希望编译器生成如下内容:\n\n```cpp\nauto t = std::tuple(1, true, std::string{\"Jedi\"});\nstd::cout << std::get<0>(t) << \" \";\nstd::cout << std::get<1>(t) << \" \";\nstd::cout << std::get<2>(t) << \" \";\n// Prints \"1 true Jedi\" \n```\n\n如您所见，我们迭代元组的每个索引，这意味着我们需要元组中包含的类型/值的数量。然后，由于元组包含不同的类型，我们需要编写一个元函数，为元组中的每种类型生成一个新函数。\n\n如果我们从一个为特定索引生成调用的函数开始，它将如下所示:\n\n```cpp\ntemplate <size_t Index, typename Tuple, typename Func> \nvoid tuple_at(const Tuple& t, Func f) {\n  const auto& v = std::get<Index>(t);\n  std::invoke(f, v);\n} \n```\n\n然后我们可以把它和一个通用的 lambda 结合起来，就像你在*第二章* *【基本 C++ 技巧】* *:* 中所学的那样\n\n```cpp\nauto t = std::tuple{1, true, std::string{\"Jedi\"}};\nauto f = [](const auto& v) { std::cout << v << \" \"; };\ntuple_at<0>(t, f);\ntuple_at<1>(t, f);\ntuple_at<2>(t, f);\n// Prints \"1 true Jedi\" \n```\n\n有了函数`tuple_at()`之后，我们就可以进行实际的迭代了。我们首先需要的是元组中作为编译时常数的值的数量。幸运的是，这个值可以通过类型特征`std::tuple_size_v<Tuple>`获得。使用`if constexpr`，我们可以通过创建一个类似的函数来展开迭代，该函数根据索引采取不同的动作:\n\n*   如果索引等于元组大小，它将生成一个空函数\n*   否则，它将在传递的索引处执行 lambda，并生成一个新函数，索引中添加 1\n\n这是代码的外观:\n\n```cpp\ntemplate <typename Tuple, typename Func, size_t Index = 0> void tuple_for_each(const Tuple& t, const Func& f) {\n  constexpr auto n = std::tuple_size_v<Tuple>;\n  if constexpr(Index < n) {\n    tuple_at<Index>(t, f);\n    tuple_for_each<Tuple, Func, Index+1>(t, f);\n  }\n} \n```\n\n正如您所看到的，默认索引被设置为零，这样我们在迭代时就不必指定它了。这个`tuple_for_each()`函数可以这样调用，λ直接到位:\n\n```cpp\nauto t = std::tuple{1, true, std::string{\"Jedi\"}};\ntuple_for_each(t, [](const auto& v) { std::cout << v << \" \"; });\n// Prints \"1 true Jedi\" \n```\n\n相当不错；在语法上，它看起来与`std::for_each()`算法非常相似。\n\n#### 为元组实现其他算法\n\n将扩展到`tuple_for_each()`，迭代元组的不同算法可以以类似的方式实现。以下是元组的`std::any_of()`是如何实现的示例:\n\n```cpp\ntemplate <typename Tuple, typename Func, size_t Index = 0> \nauto tuple_any_of(const Tuple& t, const Func& f) -> bool { \n  constexpr auto n = std::tuple_size_v<Tuple>; \n  if constexpr(Index < n) { \n    bool success = std::invoke(f, std::get<Index>(t)); \n    if (success) {\n      return true;\n    }\n    return tuple_any_of<Tuple, Func, Index+1>(t, f); \n  } else { \n    return false; \n  } \n} \n```\n\n它可以这样使用:\n\n```cpp\nauto t = std::tuple{42, 43.0f, 44.0}; \nauto has_44 = tuple_any_of(t, [](auto v) { return v == 44; }); \n```\n\n函数模板`tuple_any_of()`遍历元组中的每种类型，并为当前索引处的元素生成一个 lambda 函数，然后与`44`进行比较。在这种情况下，`has_44`会评估到`true`，作为最后一个元素，一个`double`值，就是`44`。如果我们添加一个与`44`不可比的类型的元素，比如`std::string`，我们会得到一个编译错误。\n\n### 访问元组元素\n\n在到 C++ 17 之前，有两种访问`std::tuple`元素的标准方式:\n\n*   对于访问单个元素，使用功能`std::get<N>(tuple)`。\n*   为了访问多个元素，使用了功能`std::tie()`。\n\n虽然它们都有效，但执行如此简单任务的语法非常冗长，如下例所示:\n\n```cpp\n// Prerequisite \nusing namespace std::string_literals;  // \"...\"s\nauto make_saturn() { return std::tuple{\"Saturn\"s, 82, true}; }\nint main() {\n  // Using std::get<N>()\n  {\n    auto t = make_saturn();\n    auto name = std::get<0>(t);\n    auto n_moons = std::get<1>(t);\n    auto rings = std::get<2>(t);\n    std::cout << name << ' ' << n_moons << ' ' << rings << '\\n';\n    // Output: Saturn 82 true   }\n    // Using std::tie()\n  {\n    auto name = std::string{};\n    auto n_moons = int{};\n    auto rings = bool{};\n    std::tie(name, n_moons, rings) = make_saturn();\n    std::cout << name << ' ' << n_moons << ' ' << rings << '\\n';\n  }\n} \n```\n\n为了能够优雅地执行这个常见任务，在 C++ 17 中引入了结构化绑定。\n\n#### 结构化绑定\n\n使用结构化绑定，可以使用`auto`和括号声明列表一次初始化多个变量。与一般的`auto`关键字一样，您可以通过使用相应的修饰符来控制变量应该是可变引用、前向引用、常量引用还是值。在以下示例中，正在构建`const`参考文献的结构化绑定:\n\n```cpp\nconst auto& [name, n_moons, rings] = make_saturn();\nstd::cout << name << ' ' << n_moons << ' ' << rings << '\\n'; \n```\n\n结构化绑定也可用于提取`for`循环中元组的单个成员，如下所示:\n\n```cpp\nauto planets = { \n  std::tuple{\"Mars\"s, 2, false}, \n  std::tuple{\"Neptune\"s, 14, true} \n};\nfor (auto&& [name, n_moons, rings] : planets) { \n   std::cout << name << ' ' << n_moons << ' ' << rings << '\\n'; \n} \n// Output:\n// Mars 2 false \n// Neptune 14 true \n```\n\n这里有一个快速提示。如果希望返回多个带有命名变量而不是元组索引的参数，可以返回在函数内部定义的结构，并使用自动返回类型推断:\n\n```cpp\nauto make_earth() {\n  struct Planet { std::string name; int n_moons; bool rings; };\n  return Planet{\"Earth\", 1, false}; \n}\n// ...\nauto p = make_earth(); \nstd::cout << p.name << ' ' << p.n_moons << ' ' << p.rings << '\\n'; \n```\n\n结构化绑定也与结构一起工作，因此，我们可以如下直接捕获单个数据成员，即使它是一个结构:\n\n```cpp\nauto [name, num_moons, has_rings] = make_earth(); \n```\n\n在这种情况下，我们可以为标识符选择任意的名称，因为相关的是`Planet`的数据成员的顺序，就像返回元组一样。\n\n现在，我们将看看在处理任意数量的函数参数时`std::tuple`和`std::tie()`的另一个用例。\n\n### 可变模板参数包\n\n**变量** **模板参数包** 使程序员能够创建可以接受任意数量参数的模板函数。\n\n#### 具有可变数量参数的函数示例\n\n如果我们要创建一个函数，用任意数量的不带变量模板参数包的参数组成一个字符串，我们需要使用 C 风格的变量参数(就像`printf()`一样)，或者为每个数量的参数创建一个单独的函数:\n\n```cpp\nauto make_string(const auto& v0) { \n  auto ss = std::ostringstream{}; \n  ss << v0; \n  return ss.str(); \n} \nauto make_string(const auto& v0, const auto& v1) { \n   return make_string(v0) + \" \" + make_string(v1); \n}\nauto make_string(const auto& v0, const auto& v1, const auto& v2) { \n  return make_string(v0, v1) + \" \" + make_string(v2); \n} \n// ... and so on for as many parameters we might need \n```\n\n这是我们功能的预期用途:\n\n```cpp\nauto str0 = make_string(42);\nauto str1 = make_string(42, \"hi\");\nauto str2 = make_string(42, \"hi\", true); \n```\n\n如果我们需要大量的参数，这将变得乏味，但是使用参数包，我们可以将其实现为接受任意数量参数的函数。\n\n#### 如何构造变量参数包\n\n参数包是通过在类型名前面加三个点，在变量参数扩展包后加三个点来标识的，中间加一个逗号:\n\n```cpp\ntemplate<typename ...Ts> \nauto f(Ts... values) {\n  g(values...);\n} \n```\n\n下面是语法解释:\n\n*   `Ts`是类型列表\n*   `<typename ...Ts>`表示函数处理列表\n*   `values...`扩展包，以便在每个值之间添加一个逗号\n\n要将其转化为代码，请考虑这个`expand_pack()`函数模板:\n\n```cpp\ntemplate <typename ...Ts>\nauto expand_pack(const Ts& ...values) {\n   auto tuple = std::tie(values...);\n} \n```\n\n让我们这样调用前面的函数:\n\n```cpp\nexpand_pack(42, std::string{\"hi\"}); \n```\n\n在这种情况下，编译器将生成类似如下的函数:\n\n```cpp\nauto expand_pack(const int& v0, const std::string& v1) {\n  auto tuple = std::tie(v0, v1);\n} \n```\n\n这是单个参数包部件扩展到的内容:\n\n<colgroup><col> <col></colgroup> \n| 表情: | 扩展到: |\n| `template <typename... Ts>` | `template <typename T0, typename T1>` |\n| `expand_pack(const Ts& ...values)` | `expand_pack(const T0& v0, const T1& v1)` |\n| `std::tie(values...)` | `std::tie(v0, v1)` |\n\n表 9.1:扩展表达式\n\n现在，让我们看看如何使用可变参数包创建`make_string()`函数。\n\n进一步说初始的`make_string()`函数，为了从每个参数中创建一个字符串，我们需要迭代这个包。没有办法直接迭代一个参数包，但是一个简单的解决方法是用它做一个元组，然后用`tuple_for_each()`函数模板迭代它，如下所示:\n\n```cpp\ntemplate <typename ...Ts> \nauto make_string(const Ts& ...values) { \n  auto ss = std::ostringstream{}; \n  // Create a tuple of the variadic parameter pack \n  auto tuple = std::tie(values...); \n  // Iterate the tuple \n  tuple_for_each(tuple, [&ss](const auto& v) { ss << v; }); \n  return ss.str();\n}\n// ...\nauto str = make_string(\"C++\", 20);  // OK: str is \"C++\" \n```\n\n参数包用`std::tie()`转换成`std::tuple`，然后用`tuple_for_each()`迭代。综上所述，我们需要使用`std::tuple`来处理参数的原因是因为我们希望支持任意数量的各种类型的参数。如果我们只需要支持一种特定类型的参数，我们可以使用带有基于范围的`for`循环的`std::array`，如下所示:\n\n```cpp\ntemplate <typename ...Ts>\nauto make_string(const Ts& ...values) {\n  auto ss = std::ostringstream{};\n  auto a = std::array{values...};     // Only supports one type\n  for (auto&& v : a) { ss << v; }\n  return ss.str();\n}\n// ...\nauto a = make_string(\"A\", \"B\", \"C\");  // OK: Only one type\nauto b = make_string(100, 200, 300);  // OK: Only one type\nauto c = make_string(\"C++\", 20);      // Error: Mixed types \n```\n\n如您所见，`std::tuple`是一个具有固定大小和固定元素位置的异构集合——或多或少像一个常规结构，但没有命名成员变量。\n\n我们如何在此基础上创建一个动态大小的集合(例如`std::vector`和`std::list`)，但是能够存储混合类型的元素？我们将在下一节中研究解决这个问题的方法。\n\n# 动态调整大小的异构集合\n\n我们在本章的第一节中提到，C++ 提供的动态大小的容器是同质的，这意味着我们只能存储一种类型的元素。但是有时候，我们需要跟踪一个包含不同类型元素的动态集合。为了做到这一点，我们将使用包含类型`std::any`或`std::variant`元素的容器。\n\n最简单的解决方法就是用`std::any`作为基型。`std::any`对象可以在其中存储任何类型的值:\n\n```cpp\nauto container = std::vector<std::any>{42, \"hi\", true}; \n```\n\n不过，它也有一些缺点。首先，每次访问其中的值时，都必须在运行时测试该类型。换句话说，我们在编译时完全丢失了存储值的类型信息。相反，我们必须依赖运行时类型检查来获取信息。其次，它在堆上而不是堆栈上分配对象，这可能会对性能产生重大影响。\n\n如果我们想要迭代我们的容器，我们需要明确地对每个`std::any`对象说这个:*如果你是一个 int，那么做这个，如果你是一个 char 指针，那么做那个*。这是不可取的，因为它需要重复的源代码，并且它也比使用其他替代方法效率低，我们将在本章后面介绍。\n\n以下示例编译；该类型被明确测试和铸造:\n\n```cpp\nfor (const auto& a : container) {\n  if (a.type() == typeid(int)) {\n    const auto& value = std::any_cast<int>(a);\n    std::cout << value;\n  }\n  else if (a.type() == typeid(const char*)) {\n    const auto& value = std::any_cast<const char*>(a);\n    std::cout << value;\n  }\n  else if (a.type() == typeid(bool)) {\n    const auto& value = std::any_cast<bool>(a);\n    std::cout << value;\n  }\n} \n```\n\n因为`std::any`对象不知道如何访问它的存储值，所以我们不能用常规的流操作符打印它。因此，下面的代码不会编译；编译器不知道`std::any`中存储了什么:\n\n```cpp\nfor (const auto& a : container) { \n  std::cout << a;                // Does not compile\n} \n```\n\n我们通常不需要`std::any`提供的类型的完全灵活性，并且在许多情况下，我们最好使用`std::variant`，我们接下来将介绍它。\n\n## 标准::变体\n\n如果我们不需要在容器中存储*任何*类型的能力，而是希望专注于容器初始化时声明的一组固定类型，则`std::variant`是更好的选择。\n\n`std::variant`与`std::any`相比，有两个主要优势:\n\n*   它不会将其包含的类型存储在堆中(与`std::any`不同)\n*   它可以用泛型 lambda 来调用，这意味着您不必明确知道它当前包含的类型(本章后面的部分将详细介绍这一点)\n\n`std::variant`的工作方式有点类似于元组，只是它一次只存储一个对象。包含的类型和值是我们最后赋予它的类型和值。下图说明了使用相同类型实例化的`std::tuple`和`std::variant`之间的区别:\n\n<figure class=\"mediaobject\">![](img/B15619_09_02.png)</figure>\n\n图 9.2:类型元组与类型变量\n\n这里有一个使用`std::variant`的例子:\n\n```cpp\nusing VariantType = std::variant<int, std::string, bool>; \nVariantType v{}; \nstd::holds_alternative<int>(v);  // true, int is first alternative\nv = 7; \nstd::holds_alternative<int>(v);  // true\nv = std::string{\"Anne\"};\nstd::holds_alternative<int>(v);  // false, int was overwritten \nv = false; \nstd::holds_alternative<bool>(v); // true, v is now bool \n```\n\n我们使用`std::holds_alternative<T>()`来检查变体当前是否持有给定的类型。您可以看到，当我们为变量分配新值时，类型会发生变化。\n\n除了存储实际值外，`std::variant`还通过使用通常大小为`std::size_t`的指数来跟踪当前持有的替代品。这意味着`std::variant`的总规模通常是最大备选方案的规模加上指数的规模。我们可以通过对我们的类型使用`sizeof`运算符来验证这一点:\n\n```cpp\nstd::cout << \"VariantType: \"<< sizeof(VariantType) << '\\n';\nstd::cout << \"std::string: \"<< sizeof(std::string) << '\\n';\nstd::cout << \"std::size_t: \"<< sizeof(std::size_t) << '\\n'; \n```\n\n使用带有 libc++ 的 Clang 10.0 编译和运行这段代码会生成以下输出:\n\n```cpp\nVariantType: 32\nstd::string: 24\nstd::size_t: 8 \n```\n\n可以看到，`VariantType`的大小是`std::string`和`std::size_t`的总和。\n\n### std::变体的异常安全性\n\n当一个新值被分配给一个`std::variant`对象时，它被放置在与当前变量保持值相同的位置。如果由于某种原因，新值的构造或赋值失败并引发异常，旧值可能无法恢复。相反，变体可能变得毫无价值。您可以使用成员函数`valueless_by_exception()`检查变量对象是否无值。当试图使用`emplace()`成员函数构造一个对象时，可以证明这一点:\n\n```cpp\nstruct Widget {\n  explicit Widget(int) {    // Throwing constructor\n    throw std::exception{};\n  }\n};\nauto var = std::variant<double, Widget>{1.0};\ntry {\n  var.emplace<1>(42); // Try to construct a Widget instance\n} catch (...) {\n  std::cout << \"exception caught\\n\";\n  if (var.valueless_by_exception()) {  // var may or may not \n    std::cout << \"valueless\\n\";        // be valueless\n  } else {\n    std::cout << std::get<0>(var) << '\\n';\n  }\n} \n```\n\n抛出并捕获异常后，初始`double`值 1.0 可能会也可能不会消失。该操作不能保证回滚，这通常可以从标准库容器中得到。换句话说，`std::variant`没有提供强有力的异常安全保证，其原因是性能开销，因为它需要`std::variant`使用堆分配。`std::variant`的这种行为是一个有用的特性，而不是缺点，因为这意味着您可以在具有实时要求的代码中安全地使用`std::variant`。\n\n如果你想要一个堆分配版本，但是有很强的异常安全保证和“永不清空”保证，`boost::variant`提供了这个功能。如果你对实现这种类型的挑战感兴趣，[https://www . boost . org/doc/libs/1 _ 74 _ 0/doc/html/variant/design . html](https://www.boost.org/doc/libs/1_74_0/doc/html/variant/design.html)提供了一个有趣的阅读。\n\n### 访问变体\n\n当访问`std::variant`中的变量时，我们使用全局函数`std::visit()`。正如您可能已经猜到的，在处理异构类型时，我们必须使用我们的主要伙伴:泛型 lambda:\n\n```cpp\nauto var = std::variant<int, bool, float>{};\nstd::visit([](auto&& val) { std::cout << val; }, var); \n```\n\n当在示例中用泛型 lambda 和变量`var`调用`std::visit()`时，编译器将在概念上将 lambda 转换为一个常规类，该类对变量中的每种类型都有`operator()`重载。这看起来类似于这样:\n\n```cpp\nstruct GeneratedFunctorImpl {\n  auto operator()(int&& v)   { std::cout << v; }\n  auto operator()(bool&& v)  { std::cout << v; }\n  auto operator()(float&& v) { std::cout << v; }\n}; \n```\n\n然后使用`std::holds_alternative<T>()`将`std::visit()`功能扩展为`if...else`链，或者使用`std::variant`索引的跳转表，以生成对`std::get<T>()`的正确调用。\n\n在前面的例子中，我们将通用 lambda 中的值直接传递给`std::cout`，而不考虑当前持有的备选项。但是，如果我们想做不同的事情，这取决于我们要去的是哪种类型呢？可以用于这种情况的模式是定义一个变量类模板，该模板将从一组 lambdas 继承。然后，我们需要为我们访问的每种类型定义这个。听起来很复杂，不是吗？这一开始看起来有点神奇，也考验了我们的元编程技能，但是一旦我们有了变量类模板，它就很容易使用了。\n\n我们将从变量类模板开始。以下是它的外观:\n\n```cpp\ntemplate<class... Lambdas>\nstruct Overloaded : Lambdas... {\n  using Lambdas::operator()...;\n}; \n```\n\n如果您使用的是 C++ 17 编译器，您还需要添加一个显式推导指南，但是从 C++ 20 开始就不需要了:\n\n```cpp\ntemplate<class... Lambdas> \nOverloaded(Lambdas...) -> Overloaded<Lambdas...>; \n```\n\n就这样。模板类`Overloaded`将继承我们用来实例化模板的所有 lambda，函数调用运算符`operator()()`将被每个 lambda 重载一次。现在可以创建一个无状态对象，它只包含调用运算符的多个重载:\n\n```cpp\nauto overloaded_lambdas = Overloaded{\n  [](int v)   { std::cout << \"Int: \" << v; },\n  [](bool v)  { std::cout << \"Bool: \" << v; },\n  [](float v) { std::cout << \"Float: \" << v; }\n}; \n```\n\n我们可以使用不同的参数对其进行测试，并验证是否调用了正确的重载:\n\n```cpp\noverloaded_lambdas(30031);    // Prints \"Int: 30031\"\noverloaded_lambdas(2.71828f); // Prints \"Float: 2.71828\" \n```\n\n现在，我们可以用`std::visit()`来使用它，而不需要将`Overloaded`对象存储在左值中。以下是最终的样子:\n\n```cpp\nauto var = std::variant<int, bool, float>{42};\nstd::visit(Overloaded{\n  [](int v)   { std::cout << \"Int: \" << v; },\n  [](bool v)  { std::cout << \"Bool: \" << v; },\n  [](float v) { std::cout << \"Float: \" << v; }\n}, var);\n// Outputs: \"Int: 42\" \n```\n\n因此，一旦我们有了`Overloaded`模板，我们就可以使用这种方便的方式为不同类型的参数指定一组 lambdas。在下一节中，我们将开始将`std::variant`与标准容器一起使用。\n\n## 使用变体的异构集合\n\n现在我们有了一个可以存储任何类型的提供列表的变体，我们可以在此基础上扩展到一个异构集合。我们通过简单地创建我们变体的`std::vector`来做到这一点:\n\n```cpp\nusing VariantType = std::variant<int, std::string, bool>;\nauto container = std::vector<VariantType>{}; \n```\n\n我们现在可以将不同类型的元素推送到向量中:\n\n```cpp\ncontainer.push_back(false);\ncontainer.push_back(\"I am a string\"s);\ncontainer.push_back(\"I am also a string\"s);\ncontainer.push_back(13); \n```\n\n向量现在在内存中看起来像这样，向量中的每个元素都包含变量的大小，在这种情况下是`sizeof(std::size_t) + sizeof(std::string)`:\n\n<figure class=\"mediaobject\">![](img/B15619_09_03.png)</figure>\n\n图 9.3:变体载体\n\n当然，我们也可以`pop_back()`或者以容器允许的任何其他方式修改容器:\n\n```cpp\ncontainer.pop_back();\nstd::reverse(container.begin(), container.end());\n// etc... \n```\n\n## 访问变量容器中的值\n\n现在已经有了大小动态的异构集合的样板，让我们看看如何像普通的`std::vector`一样使用它:\n\n1.  **构建变体**的异构容器:这里，我们构建一个不同类型的`std::vector`。注意初始化列表包含不同的类型:\n\n    ```cpp\n    using VariantType = std::variant<int, std::string, bool>;\n    auto v = std::vector<VariantType>{ 42, \"needle\"s, true }; \n    ```\n\n2.  **通过使用常规 for 循环**进行迭代来打印内容:要使用常规`for`循环来迭代容器，我们使用`std::visit()`和泛型 lambda。全局函数`std::visit()`负责类型转换。该示例将每个值打印到`std::cout`，与类型无关:\n\n    ```cpp\n    for (const auto& item : v) { \n      std::visit([](const auto& x) { std::cout << x << '\\n';}, item);\n    } \n    ```\n\n3.  **检查容器中有哪些类型**:这里，我们按类型检查容器的每个元素。这是通过使用全局函数`std::holds_alternative<type>`来实现的，如果变量当前持有要求的类型，则返回`true`。以下示例计算容器中当前包含的布尔值数量:\n\n    ```cpp\n    auto num_bools = std::count_if(v.begin(), v.end(),\n                                   [](auto&& item) {\n      return std::holds_alternative<bool>(item);\n    }); \n    ```\n\n4.  **通过包含的类型和值找到内容**:在这个例子中，我们通过组合`std::holds_alternative()`和`std::get()`来检查容器的类型和值。本示例检查容器是否包含具有值`\"needle\"` :\n\n    ```cpp\n    auto contains = std::any_of(v.begin(), v.end(),\n                                [](auto&& item) {\n      return std::holds_alternative<std::string>(item) &&\n        std::get<std::string>(item) == \"needle\";\n    }); \n    ```\n\n    的`std::string`\n\n### 全局函数 std::get()\n\n全局功能模板`std::get()`可用于`std::tuple`、`std::pair`、`std::variant`和`std::array`。实例化`std::get()`有两种方法，一种是用索引，一种是用类型:\n\n*   `std::get<Index>()`:当`std::get()`与一个索引一起使用时，如在`std::get<1>(v)`中，它在`std::tuple`、`std::pair`或`std::array`中返回相应索引处的值。\n*   `std::get<Type>()`:当`std::get()`与类型一起使用时，如在`std::get<int>(v)`中，返回`std::tuple`、`std::pair` 或`std::variant`中的相应值。在`std::variant`的情况下，如果变体当前不包含该类型，则会引发`std::bad_variant_access` 异常。请注意，如果`v`是一个`std::tuple`并且`Type`被包含了不止一次，您必须使用索引来访问该类型。\n\n讨论了实用程序库中的基本模板后，让我们来看看本章中介绍的一些实际应用。\n\n# 一些真实的例子\n\n我们将通过检查两个例子来结束本章的，其中`std::tuple`、`std::tie()`和一些模板元编程可以帮助我们在实践中编写干净高效的代码。\n\n## 示例 1:投影和比较运算符\n\n随着 C++ 20 的出现，实现类的比较运算符的需求急剧减少，但是当我们想要为特定场景以某种定制顺序对对象进行排序时，仍然有需要提供定制比较函数的情况。考虑以下类别:\n\n```cpp\nstruct Player {\n  std::string name_{};\n  int level_{};\n  int score_{};\n  // etc...\n};\nauto players = std::vector<Player>{};\n// Add players here... \n```\n\n假设我们想要根据属性对玩家进行排序:主要排序顺序`level_`和次要排序顺序`score_`。在实现比较和排序时看到这样的代码并不少见:\n\n```cpp\nauto cmp = [](const Player& lhs, const Player& rhs) {\n  if (lhs.level_ == rhs.level_) {\n    return lhs.score_ < rhs.score_;\n  }\n  else {\n    return lhs.level_ < rhs.level_;\n  }\n};\nstd::sort(players.begin(), players.end(), cmp); \n```\n\n当属性数量增加时，使用嵌套的`if-else`块以这种方式编写比较运算符很快变得容易出错。我们真正想表达的是，我们正在比较一个*投影`Player`属性的*(在这个例子中，是一个严格的子集)。`std::tuple`可以帮助我们以更简洁的方式重写这段代码，而不需要`if-else`语句。\n\n让我们使用`std::tie()`，它创建了一个`std::tuple`来保存我们传递给它的左值的引用。下面的代码创建了两个投影，`p1`和`p2`，并使用`<`运算符对它们进行比较:\n\n```cpp\nauto cmp = [](const Player& lhs, const Player& rhs) {\n  auto p1 = std::tie(lhs.level_, lhs.score_); // Projection\n  auto p2 = std::tie(lhs.level_, lhs.score_); // Projection\n  return p1 < p2;\n};\nstd::sort(players.begin(), players.end(), cmp); \n```\n\n与使用`if-else`语句的初始版本相比，这非常干净且易于阅读。但这真的有效率吗？似乎我们只需要创建临时对象来比较两个玩家。当在微基准中运行这个并检查生成的代码时，使用`std::tie()`真的没有任何开销；事实上，在这个例子中，使用`std::tie()`的版本比使用`if-else`语句的版本稍快。\n\n使用范围算法，我们可以通过向`std::ranges::sort()`提供投影作为参数来进行排序，这使得代码更加清晰:\n\n```cpp\nstd::ranges::sort(players, std::less{}, [](const Player& p) {\n  return std::tie(p.level_, p.score_); \n}); \n```\n\n这是一个例子，说明了如何在不需要带有命名成员的完整结构的情况下使用`std::tuple`，而不牺牲代码的清晰度。\n\n## 示例 2:反射\n\n术语**反射**是指在对一个类的内容一无所知的情况下检查该类的能力。与许多其他编程语言相比，C++ 没有内置反射，这意味着我们必须自己编写反射功能。反射计划包含在 C++ 标准的未来版本中；希望我们能在 C++ 23 中看到这个特性。\n\n在这个例子中，我们将限制反射，使类能够迭代它们的成员，就像我们可以迭代元组的成员一样。通过使用反射，我们可以为序列化或日志记录创建通用函数，这些函数可以自动处理任何类。这减少了大量的样板代码，这是传统上 C++ 类所需要的。\n\n### 让一个类反映它的成员\n\n由于我们需要自己实现所有的反射功能，我们将从通过一个名为`reflect()`的函数公开成员变量开始。我们将继续使用上一节中介绍的`Player`类。下面是我们添加`reflect()`成员函数和构造函数时的样子:\n\n```cpp\nclass Player {\npublic:\n  Player(std::string name, int level, int score)\n      : name_{std::move(name)}, level_{level}, score_{score} {}\n\n  auto reflect() const {\n    return std::tie(name_, level_, score_);\n  } \nprivate:\n  std::string name_;\n  int level_{};\n  int score_{};\n}; \n```\n\n`reflect()`成员函数通过调用`std::tie()`返回成员变量的引用元组。我们现在可以开始使用`reflect()`函数，但是首先，要注意使用手工反射的替代方法。\n\n### 简化反射的 C++ 库\n\n在 C++ 库世界中，已经有不少简化反射创建的尝试。一个例子是路易斯·迪翁的元编程库 *Boost Hana* ，它通过一个简单的宏赋予类反射能力。最近， *Boost* 还增加了安东尼·波罗钦的*精准平映*，自动*反映类的公共内容，只要所有成员都是简单类型即可。*\n\n *然而，为了清楚起见，在这个例子中，我们将只使用我们自己的`reflect()`成员函数。\n\n### 使用反射\n\n既然`Player`类有能力反映它的成员变量，我们可以自动化批量功能的创建，否则需要我们重新键入每个成员变量。您可能已经知道，C++ 可以自动生成构造函数、析构函数和比较运算符，但是其他运算符必须由程序员实现。一个这样的功能是`operator<<()`，它将其内容输出到一个流中，以便将它们存储在一个文件中，或者更常见的是，将它们记录在应用日志中。\n\n通过重载`operator<<()`并使用我们在本章前面实现的`tuple_for_each()`函数模板，我们可以简化类的`std::ostream`输出的创建，如下所示:\n\n```cpp\nauto& operator<<(std::ostream& ostr, const Player& p) { \n  tuple_for_each(p.reflect(), [&ostr](const auto& m) { \n    ostr << m << \" \"; \n  }); \n  return ostr; \n} \n```\n\n现在，该类可以用于任何`std::ostream`类型，如下所示:\n\n```cpp\nauto v = Player{\"Kai\", 4, 2568}; \nstd::cout << v;                  // Prints: \"Kai 4 2568 \" \n```\n\n通过一个元组来反映我们的类成员，我们只需要在类中添加/移除成员时更新我们的反映函数，而不是更新每个函数和迭代所有成员变量。\n\n### 有条件地重载全局函数\n\n现在我们有了一个使用反射而不是手动键入每个变量来编写大容量函数的机制，我们仍然需要为每种类型键入简化的大容量函数。如果我们希望为每一种可以反映的类型生成这些函数，会怎么样？\n\n通过使用约束，我们可以有条件地为所有具有`reflect()`成员函数的类启用`operator<<()`。\n\n首先，我们需要创建一个引用`reflect()`成员函数的新概念:\n\n```cpp\ntemplate <typename T> \nconcept Reflectable = requires (T& t) {\n  t.reflect();\n}; \n```\n\n当然，这个概念只是检查一个类是否有名为`reflect()`的成员函数；它并不总是返回元组。一般来说，我们应该对像这样只使用单个成员函数的弱概念持怀疑态度，但是它符合示例的目的。无论如何，我们现在可以在全局命名空间中重载`operator<<()`，让所有可反射的类能够被比较并打印到一个`std::ostream`:\n\n```cpp\nauto& operator<<(std::ostream& os, const Reflectable auto& v) {\n  tuple_for_each(v.reflect(), [&os](const auto& m) {\n    os << m << \" \";\n  });\n  return os;\n} \n```\n\n前面的函数模板将仅针对包含`reflect()`成员函数的类型进行实例化，因此不会与任何其他重载冲突。\n\n### 测试反射能力\n\n现在，我们已经准备好了一切:\n\n*   我们将要测试的`Player`类有一个`reflect()`成员函数，该函数返回对其成员的一组引用\n*   全局`std::ostream& operator<<()`对于可反射类型是重载的\n\n下面是一个验证该功能的简单测试:\n\n```cpp\nint main() {\n  auto kai = Player{\"Kai\", 4, 2568}; \n  auto ari = Player{\"Ari\", 2, 1068}; \n\n  std::cout << kai; // Prints \"Kai 4 2568\" \n  std::cout << ari; // Prints \"Ari 2 1068\" \n} \n```\n\n这些例子已经证明了一些小但重要的实用程序的有用性，比如`std::tie()`和`std::tuple`结合一点元编程。\n\n# 摘要\n\n在本章中，您已经学习了如何使用`std::optional`来表示代码中的可选值。您还看到了如何将`std::pair`、`std::tuple`、`std::any`和`std::variant`与标准容器和元编程结合在一起，以存储和迭代不同类型的元素。您还了解到`std::tie()`是一个概念简单但功能强大的工具，可用于投影和反射。\n\n在下一章中，您将了解如何通过学习如何构造隐藏的代理对象来进一步扩展您的 C++ 工具箱以创建库。*"
  },
  {
    "path": "docs/cpp-hiperf/10.md",
    "content": "# 十、代理对象和延迟求值\n\n在本章中，您将学习如何使用代理对象和延迟求值，以便将某些代码的执行推迟到需要的时候。使用代理对象可以在幕后进行优化，从而保持暴露的接口不变。\n\n本章包括:\n\n*   延迟而急切的评价\n*   使用代理对象来避免多余的计算\n*   使用代理对象时重载运算符\n\n# 引入延迟计算和代理对象\n\n首先也是最重要的一点，本章中使用的技术用于向库的用户隐藏库中的优化。这很有用，因为将每一个优化技术作为一个单独的功能公开需要库用户的大量关注和教育。它还用大量特定的函数膨胀了代码库，使其难以阅读和理解。通过使用代理对象，我们可以实现引擎盖下的优化；生成的代码既优化又可读。\n\n## 延迟与急切的评价\n\n**懒人** **求值**是一种将手术推迟到真正需要其结果时使用的手法。相反的，马上进行的操作，叫做**急切求值**。在某些情况下，急切的求值是不可取的，因为我们最终可能会构建一个从未使用过的值。\n\n为了演示急切求值和延迟求值之间的区别，让我们假设我们正在编写某种具有多个级别的游戏。每当一个关卡完成后，我们需要显示当前的分数。在这里，我们将重点关注游戏的几个组成部分:\n\n*   一个`ScoreView`类，负责在获得奖励的情况下用可选的奖励图像显示用户的分数\n*   一个表示加载到内存中的图像的`Image`类\n*   从磁盘加载图像的`load()`功能\n\n在这个例子中，类和函数的实现并不重要，但是声明如下:\n\n```cpp\nclass Image { /* ... */ };                   // Buffer with JPG data\nauto load(std::string_view path) -> Image;   // Load image at path\nclass ScoreView {\npublic:\n  // Eager, requires loaded bonus image\n  void display(const Image& bonus);\n  // Lazy, only load bonus image if necessary\n  void display(std::function<Image()> bonus);\n  // ...\n}; \n```\n\n提供了两个版本的`display()`:第一个版本需要一个完全加载的奖励图像，而第二个版本接受一个只有在需要奖励图像时才会调用的功能。使用第一个*急切的*版本会是这样的:\n\n```cpp\n// Always load bonus image eagerly\nconst auto eager = loadimg/stars.jpg\");\nscore.display(eager); \n```\n\n使用第二个*延迟*版本会是这样的:\n\n```cpp\n// Load default image lazily if needed\nauto lazy = [] { return loadimg/stars.jpg\"); }; \nscore.display(lazy); \n```\n\n即使从未显示过，急切版本也总是会将默认图像加载到内存中。但是奖励图像的懒加载会保证只有`ScoreView`真的需要显示奖励图像时才会加载图像。\n\n这是一个非常简单的例子，但是它的思想是你的代码以几乎相同的方式被表达，就像它被急切地声明一样。一种隐藏代码延迟计算的技术是使用代理对象。\n\n## 代理对象\n\n代理对象是内部库对象，不打算对库的用户可见。他们的任务是将操作推迟到需要的时候，并收集一个表达式的数据，直到它可以被求值和优化。然而，代理对象在黑暗中起作用；库的用户应该能够处理表达式，就像代理对象不在那里一样。换句话说，使用代理对象，您可以在库中封装优化，同时保持接口不变。现在，您将学习如何使用代理对象来求值更高级的表达式。\n\n# 避免使用代理对象构造对象\n\n急切的求值可能会产生不必要的效果，即对象被不必要地构造。通常这不是一个问题，但是如果对象构建成本很高(例如，因为堆分配)，可能有合理的理由优化掉无用的短期对象的不必要构建。\n\n## 使用代理比较串联字符串\n\n我们现在将通过一个使用代理对象的最小例子来告诉你它们是什么以及可以用来做什么。它不是为您提供优化字符串比较的通用生产就绪解决方案。\n\n说了这么多，看看这个连接两个字符串并比较结果的代码片段:\n\n```cpp\nauto a = std::string{\"Cole\"}; \nauto b = std::string{\"Porter\"}; \nauto c = std::string{\"ColePorter\"}; \nauto is_equal = (a + b) == c;        // true \n```\n\n下面是前面代码片段的可视化表示:\n\n<figure class=\"mediaobject\">![](img/B15619_10_01.png)</figure>\n\n图 10.1:将两个字符串连接成一个新字符串\n\n这里的问题是(`a + b`)构造一个新的临时字符串，以便与`c`进行比较。不用构造一个新字符串，我们可以直接比较连接，如下所示:\n\n```cpp\nauto is_concat_equal(const std::string& a, const std::string& b,\n                     const std::string& c) { \n  return  \n    a.size() + b.size() == c.size() && \n    std::equal(a.begin(), a.end(), c.begin()) &&  \n    std::equal(b.begin(), b.end(), c.begin() + a.size()); \n} \n```\n\n我们可以这样使用它:\n\n```cpp\nauto is_equal = is_concat_equal(a, b, c); \n```\n\n就性能而言，我们已经取得了胜利，但是从语法上来说，像这样充满特殊情况便利功能的代码库很难维护。因此，让我们看看如何在保持原始语法不变的情况下实现这种优化。\n\n## 实现代理\n\n首先，我们将创建一个代理类来表示两个字符串的连接:\n\n```cpp\nstruct ConcatProxy { \n  const std::string& a; \n  const std::string& b; \n}; \n```\n\n然后，我们将构建自己的`String`类，它包含一个`std::string`和一个重载的`operator+()`函数。请注意，这是如何制作和使用代理对象的示例；创建自己的`String`类不是我推荐的:\n\n```cpp\nclass String { \npublic: \n  String() = default; \n  String(std::string str) : str_{std::move(str)} {} \n  std::string str_{};\n}; \n\nauto operator+(const String& a, const String& b) {\n   return ConcatProxy{a.str_, b.str_};\n} \n```\n\n下面是前面代码片段的可视化表示:\n\n<figure class=\"mediaobject\">![](img/B15619_10_02.png)</figure>\n\n图 10.2:代表两个字符串连接的代理对象\n\n最后，我们将创建一个全局`operator==()`函数，该函数将依次使用优化的`is_concat_equal()`函数，如下所示:\n\n```cpp\nauto operator==(ConcatProxy&& concat, const String& str) {\n  return is_concat_equal(concat.a, concat.b, str.str_); \n} \n```\n\n既然我们已经做好了一切，我们就能两全其美:\n\n```cpp\nauto a = String{\"Cole\"}; \nauto b = String{\"Porter\"}; \nauto c = String{\"ColePorter\"}; \nauto is_equal = (a + b) == c;     // true \n```\n\n换句话说，我们获得了`is_concat_equal()`的性能，同时保留了使用`operator==()`的表达语法。\n\n## 右值修饰符\n\n在前面的代码中，全局`operator==()`函数只接受`ConcatProxy`值:\n\n```cpp\nauto operator==(ConcatProxy&& concat, const String& str) { // ... \n```\n\n如果我们接受左值，我们可能会意外地错误使用代理，如下所示:\n\n```cpp\nauto concat = String{\"Cole\"} + String{\"Porter\"};\nauto is_cole_porter = concat == String{\"ColePorter\"}; \n```\n\n这里的问题是，在执行比较时，持有`\"Cole\"`和`\"Porter\"`的临时`String`对象都已经被破坏，导致失败。(记住`ConcatProxy`类只保存对字符串的引用。)但是由于我们强制使`concat`对象成为一个右值，前面的代码将不会编译，从而避免了可能的运行时崩溃。当然，您可以使用`std::move(concat) == String(\"ColePorter\")`将其转换为右值来强制编译它，但这并不是一个现实的情况。\n\n## 分配串联代理\n\n现在，你可能会想，如果我们真的想将连接的字符串存储为新字符串，而不只是比较它，会怎么样？我们所做的只是重载一个`operator String()`函数，如下所示:\n\n```cpp\nstruct ConcatProxy {\n  const std::string& a;\n  const std::string& b;\n  operator String() const && { return String{a + b}; }\n}; \n```\n\n两个字符串的串联现在可以隐式地将其自身转换为一个字符串:\n\n```cpp\nString c = String{\"Marc\"} + String{\"Chagall\"}; \n```\n\n但是有一个小问题:我们不能用`auto`关键字初始化新的`String`对象，因为这会导致`ConcatProxy`:\n\n```cpp\nauto c = String{\"Marc\"} + String{\"Chagall\"};\n// c is a ConcatProxy due to the auto keyword here \n```\n\n可惜我们没有办法绕开这个；结果必须显式转换为`String`。\n\n是时候看看我们的优化版本与正常情况相比有多快了。\n\n## 性能赋值\n\n为了求值的性能优势，我们将使用以下基准，它连接并比较大小为`50`的`10'000`字符串:\n\n```cpp\ntemplate <typename T>\nauto create_strings(int n, size_t length) -> std::vector<T> {\n  // Create n random strings of the specified length\n  // ...\n}\ntemplate <typename T> \nvoid bm_string_compare(benchmark::State& state) {\n  const auto n = 10'000, length = 50;\n  const auto a = create_strings<T>(n, length);\n  const auto b = create_strings<T>(n, length);\n  const auto c = create_strings<T>(n, length * 2);\n  for (auto _ : state) {\n    for (auto i = 0; i < n; ++ i) {\n      auto is_equal = a[i] + b[i] == c[i];\n      benchmark::DoNotOptimize(is_equal);\n    }\n  }\n}\nBENCHMARK_TEMPLATE(bm_string_compare, std::string);\nBENCHMARK_TEMPLATE(bm_string_compare, String);\nBENCHMARK_MAIN(); \n```\n\n在英特尔酷睿 i7 CPU 上执行时，我使用 gcc 实现了 40 倍的加速。使用`std::string`的版本直接在 1.6 ms 内完成，而使用`String`的代理版本仅在 0.04 ms 内完成，当使用长度为 10 的短字符串运行相同的测试时，加速约为 20 倍。大变化的一个原因是小字符串将通过利用*第 7 章*、*内存管理*中讨论的小字符串优化来避免堆分配。基准测试向我们展示了当我们去掉临时字符串和可能的堆分配时，代理对象的加速是相当可观的。\n\n`ConcatProxy`类帮助我们在比较字符串时隐藏一个优化。希望这个简单的例子能启发您开始思考如何在实现性能优化的同时保持 API 设计的整洁。\n\n接下来，您将看到可以隐藏在代理类后面的另一个有用的优化。\n\n# 推迟 sqrt 计算\n\n本节将向您展示如何使用代理对象，以便在比较二维向量的长度时推迟甚至避免使用计算量大的`std::sqrt()`函数。\n\n## 一个简单的二维向量类\n\n让我们从一个简单的二维向量类开始。它有 *x* 和 *y* 坐标和一个名为`length()`的成员函数，该函数计算从原点到位置 *(x，y)* 的距离。我们将这个班级称为`Vec2D`。定义如下:\n\n```cpp\nclass Vec2D {\npublic:\n  Vec2D(float x, float y) : x_{x}, y_{y} {}\n  auto length() const {\n    auto squared = x_*x_ + y_*y_;\n    return std::sqrt(squared);\n  }\nprivate:\n  float x_{};\n  float y_{};\n}; \n```\n\n下面是客户端如何使用`Vec2D`的一个例子:\n\n```cpp\nauto a = Vec2D{3, 4}; \nauto b = Vec2D{4, 4};\nauto shortest = a.length() < b.length() ? a : b;\nauto length = shortest.length();\nstd::cout << length; // Prints 5 \n```\n\n示例创建两个向量，并比较它们的长度。然后将最短向量的长度打印成标准长度。*图 10.3* 显示了矢量和计算出的原点长度:\n\n<figure class=\"mediaobject\">![](img/B15619_10_03.png)</figure>\n\n图 10.3:两个不同长度的 2D 矢量。向量 a 的长度是 5。\n\n## 基础数学\n\n看看计算的数学，你可能会注意到一些有趣的东西。长度公式如下:\n\n<figure class=\"mediaobject\">![](img/B15619_10_001.png)</figure>\n\n然而，如果我们只需要比较两个向量之间的距离，那么平方长度就是我们所需要的，如下式所示:\n\n<figure class=\"mediaobject\">![](img/B15619_10_002.png)</figure>\n\n可以使用函数`std::sqrt()`计算平方根。但是，如前所述，如果我们只想比较两个向量之间的长度，则不需要平方根运算，因此可以省略它。好的一点是`std::sqrt()`是一个相对较慢的操作，这意味着如果我们根据向量的长度来比较许多向量，我们可以获得一些性能。问题是，我们如何在保持干净语法的同时做到这一点？让我们看看当比较长度时，如何使用代理对象让一个简单的库在引擎盖下执行这种优化。\n\n为了清楚起见，我们从最初的`Vec2D`类开始，但是我们将`length()`函数分成两部分–`length_squared()`和`length()`，如下所示:\n\n```cpp\nclass Vec2D {\npublic:\n  Vec2D(float x, float y) : x_{x}, y_{y} {}  \n  auto length_squared() const {\n    return x_*x_ + y_*y_;  \n  }\n  auto length() const {\n    return std::sqrt(length_squared());\n  }\nprivate:\n  float x_{};\n  float y_{};\n}; \n```\n\n现在我们的`Vec2D` 类的客户如果想在只比较不同向量的长度时获得一些性能，可以使用`length_squared()`。\n\n假设我们想要实现一个方便的实用函数，返回一系列`Vec2D`对象的最小长度。我们现在有两个选项:在进行比较时，使用`length()`功能或`length_squared()`功能。它们相应的实现如下例所示:\n\n```cpp\n// Simple version using length()\nauto min_length(const auto& r) -> float {\n  assert(!r.empty());\n  auto cmp = [](auto&& a, auto&& b) {\n    return a.length () < b.length();\n  };\n  auto it = std::ranges::min_element(r, cmp);\n  return it->length();\n} \n```\n\n使用`length_squared()`进行比较的第二个优化版本如下:\n\n```cpp\n// Fast version using length_squared()\nauto min_length(const auto& r) -> float {\n  assert(!r.empty());\n  auto cmp = [](auto&& a, auto&& b) {\n    return a.length_squared() < b.length_squared(); // Faster\n  };\n  auto it = std::ranges::min_element(r, cmp);\n  return it->length(); // But remember to use length() here!\n} \n```\n\n在`cmp`里面使用`length()`的第一个版本的优点是可读性更强，更容易答对，而第二个版本的优点是速度更快。提醒大家，第二个版本的加速是因为我们可以避免调用`cmp` lambda 里面的`std::sqrt()`。\n\n最佳解决方案是第一个版本的语法使用`length()`，第二个版本的性能使用`length_squared()`。\n\n根据这个类将要使用的上下文，可能有充分的理由公开一个函数，比如`length_squared()`。但是让我们假设我们团队中的其他开发人员不理解拥有`length_squared()`函数的原因，并且发现这个类令人困惑。因此，我们决定想出一些更好的方法来避免函数的两个版本暴露向量的长度属性。正如您可能已经猜到的那样，现在是代理类隐藏这种复杂性的时候了。\n\n为了实现这一点，我们返回一个对用户隐藏的中间对象，而不是从`length()`成员函数返回一个`float`值。根据用户如何使用隐藏的代理对象，它应该避免`std::sqrt()`操作，直到真正需要它。在接下来的部分中，我们将实现一个名为`LengthProxy`的类，这将是我们将从`Vec2D::length()`返回的代理对象的类型。\n\n## 实现长度代理对象\n\n是时候实现包含表示平方长度的数据成员的`LengthProxy`类了。实际的平方长度从不公开，以防止类的用户将平方长度与常规长度混合。相反，`LengthProxy`有一个隐藏的`friend`函数，将它的平方长度与常规长度进行比较，如下所示:\n\n```cpp\nclass LengthProxy { \npublic: \n  LengthProxy(float x, float y) : squared_{x * x + y * y} {} \n  bool operator==(const LengthProxy& other) const = default; \n  auto operator<=>(const LengthProxy& other) const = default; \n  friend auto operator<=>(const LengthProxy& proxy, float len) { \n    return proxy.squared_ <=> len*len;   // C++ 20\n  } \n  operator float() const {      // Allow implicit cast to float\n    return std::sqrt(squared_); \n  }  \nprivate: \n  float squared_{}; \n}; \n```\n\n我们已经定义`operator float()`允许从`LengthProxy`到`float`的隐式转换。`LengthProxy`物体之间也可以相互比较。通过使用新的 C++ 20 比较，我们 简单地`default`相等运算符和三向比较运算符，让编译器为我们生成所有必要的比较运算符。\n\n接下来，我们重写`Vec2D`类以返回类`LengthProxy`的对象，而不是实际的`float`长度:\n\n```cpp\nclass Vec2D { \npublic: \n  Vec2D(float x, float y) : x_{x}, y_{y} {} \n  auto length() const { \n    return LengthProxy{x_, y_};    // Return proxy object\n  } \n  float x_{}; \n  float y_{}; \n}; \n```\n\n有了这些添加，是时候使用我们新的代理类了。\n\n## 比较长度和长度代理\n\n在这个例子中，我们将 比较两个向量`a`和`b`，并确定`a`是否比`b`短。请注意，代码在语法上看起来与我们没有使用代理类时完全一样:\n\n```cpp\nauto a = Vec2D{23, 42}; \nauto b = Vec2D{33, 40}; \nbool a_is_shortest = a.length() < b.length(); \n```\n\n在引擎盖下，最终陈述扩展为类似于以下内容:\n\n```cpp\n// These LengthProxy objects are never visible from the outside\nLengthProxy a_length = a.length(); \nLengthProxy b_length = b.length(); \n// Member operator< on LengthProxy is invoked, \n// which compares member squared_ \nauto a_is_shortest = a_length < b_length; \n```\n\n很好！`std::sqrt()`操作省略，而`Vec2D`类的界面仍然完好无损。由于省略了`std::sqrt()`操作，我们之前实现的`min_length()`的简单版本现在可以更有效地执行比较。下面是简单的实现，现在也变得高效了:\n\n```cpp\n// Simple and efficient \nauto min_length(const auto& r) -> float { \n  assert(!r.empty()); \n  auto cmp = [](auto&& a, auto&& b) { \n    return a.length () < b.length(); \n  }; \n  auto it = std::ranges::min_element(r, cmp); \n  return it->length(); \n} \n```\n\n`Vec2D`对象之间的优化长度比较现在发生在引擎盖下。实现`min_length()`功能的程序员不需要知道这个优化就能从中受益。如果我们需要实际长度，让我们看看它看起来像什么。\n\n## 使用长度代理计算长度\n\n请求 实际长度时，调用代码有一点变化。要触发对`float`的隐式转换，我们必须在声明下面的`len`变量时提交一个`float`；也就是说，我们不能像平时一样只使用`auto`:\n\n```cpp\nauto a = Vec2D{23, 42};\nfloat len = a.length(); // Note, we cannot use auto here \n```\n\n如果我们只写`auto`，那么`len`对象将是`LengthProxy`类型，而不是`float`类型。我们不希望代码库的用户显式处理`LengthProxy`对象；代理对象应该在黑暗中操作，应该只利用它们的结果(在这种情况下，比较结果或实际距离值为`float`)。即使我们不能完全隐藏代理对象，让我们看看如何收紧它们以防止误用。\n\n### 防止长度代理的滥用\n\n您可能已经注意到 可能会出现使用`LengthProxy`类可能导致性能下降的情况。在下面的例子中，根据程序员对长度值的请求，多次调用`std::sqrt()`函数:\n\n```cpp\nauto a = Vec2D{23, 42};\nauto len = a.length();\nfloat f0 = len;       // Assignment invoked std::sqrt()\nfloat f1 = len;       // std::sqrt() of len is invoked again \n```\n\n虽然这是一个人为的例子，但是现实世界中可能会发生这种情况，我们希望强制`Vec2d`的用户对每个`LengthProxy`对象只调用一次`operator float()`。为了防止误用，我们使`operator float()`成员函数仅在右值上可调用；也就是说，`LengthProxy`对象只有在不绑定变量的情况下才能转换为浮点。\n\n我们通过在`operator float()`成员函数中使用`&&`作为修饰符来强制这种行为。`&&`修改器就像`const`修改器一样工作，但是当`const`修改器强制成员函数不修改对象时，`&&`修改器强制函数对临时对象进行操作。\n\n修改如下:\n\n```cpp\noperator float() const && { return std::sqrt(squared_); } \n```\n\n如果我们在与变量相关联的`LengthProxy`对象上调用`operator float()`，比如下面例子中的`dist`对象，编译器将拒绝编译:\n\n```cpp\nauto a = Vec2D{23, 42};\nauto len = a.length(); // len is of type LenghtProxy\nfloat f = len;         // Doesn't compile: len is not an rvalue \n```\n\n但是，我们仍然可以直接在从`length()`返回的右值上调用`operator float()`，如下所示:\n\n```cpp\nauto a = Vec2D{23, 42}; \nfloat f = a.length();    // OK: call operator float() on rvalue \n```\n\n一个临时的`LengthProxy`实例仍然会在后台创建，但是由于它没有绑定到一个变量，我们 可以隐式地将其转换为`float`。这将防止误用，例如在`LengthProxy`对象上多次调用`operator float()`。\n\n## 性能赋值\n\n为了方便起见，让我们看看我们实际获得了多少性能。我们将对以下版本的`min_element()`进行基准测试:\n\n```cpp\nauto min_length(const auto& r) -> float {\n  assert(!r.empty());\n  auto it = std::ranges::min_element(r, [](auto&& a, auto&& b) {\n    return a.length () < b.length(); });\n  return it->length();\n} \n```\n\n为了将代理对象优化与某些东西进行比较，我们将定义一个替代版本`Vec2DSlow`，它总是使用`std::sqrt()`计算实际长度:\n\n```cpp\nstruct Vec2DSlow {\n  float length() const {                  // Always compute\n    auto squared = x_ * x_ + y_ * y_;     // actual length\n    return std::sqrt(squared);            // using sqrt()\n  }\n  float x_, y_;\n}; \n```\n\n使用带有函数模板的 Google Benchmark，我们可以看到在找到 1000 个向量的最小长度时，我们获得了多少性能:\n\n```cpp\ntemplate <typename T> \nvoid bm_min_length(benchmark::State& state) {\n  auto v = std::vector<T>{};\n  std::generate_n(std::back_inserter(v), 1000, [] {\n    auto x = static_cast<float>(std::rand());\n    auto y = static_cast<float>(std::rand());\n    return T{x, y};\n  });\n  for (auto _ : state) {\n    auto res = min_length(v);\n    benchmark::DoNotOptimize(res);\n  }\n}\nBENCHMARK_TEMPLATE(bm_min_length, Vec2DSlow);\nBENCHMARK_TEMPLATE(bm_min_length, Vec2D);\nBENCHMARK_MAIN(); \n```\n\n在英特尔 i7 CPU 上运行该基准测试产生了以下结果:\n\n*   使用未优化的`Vec2DSlow`和`std::sqrt()`需要 7900 纳秒\n*   使用`Vec2D`和`LengthProxy`需要 1800 纳秒\n\n这一性能优势相当于超过 4 倍的加速。\n\n这是我们如何避免在某些情况下不必要的计算的一个例子。但是我们没有让`Vec2D`的界面变得更加复杂，而是设法将优化封装在代理对象中，这样所有客户端都可以从优化中受益，而不会牺牲清晰度。\n\n在 C++ 中优化表达式的相关技术是**表达式模板**。这使用模板元编程在编译时生成表达式树。该技术可用于避免临时变量，并支持延迟求值。表达式模板是 Boost **基础线性代数库** ( **uBLAS** )和 **Eigen** 、[http://eigen.tuxfamily.org](http://eigen.tuxfamily.org)中使线性代数算法和矩阵运算快速的技术之一。您可以在比雅尼·斯特劳斯特鲁普的《C++ 编程语言》*第四版*中*阅读更多关于设计矩阵类时如何使用表达式模板和融合运算的内容。*\n\n在本章的最后，我们将研究当代理对象与重载操作符结合时，从代理对象中获益的其他方法。\n\n# 创造性运算符重载和代理对象\n\n正如你可能已经知道的，C++ 有能力重载几个运算符，包括标准数学运算符，如加号和减号。可以利用重载的数学运算符来创建自定义的数学类，这些类表现为数字内置类型，以使代码更易读。另一个例子是流操作符，它在标准库中被重载，以便将对象转换为流，如下所示:\n\n```cpp\nstd::cout << \"iostream \" << \"uses \" << \"overloaded \" << \"operators.\"; \n```\n\n然而，一些库在其他上下文中使用重载。如前所述，Ranges 库使用重载来组成如下视图:\n\n```cpp\nconst auto r = {-5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5};\nauto odd_positive_numbers = r \n  | std::views::filter([](auto v) { return v > 0; }) \n  | std::views::filter([](auto v) { return (v % 2) == 1; }); \n```\n\n接下来，我们将探讨如何在代理类中使用管道操作符。\n\n## 作为扩展方法的管道操作符\n\n相比其他语言，比如 C#、Swift、JavaScript，C++ 不支持扩展方法；也就是说，不能用新的成员函数在本地扩展类。\n\n例如，不能用`contains(T val)`函数扩展`std::vector`来像这样使用:\n\n```cpp\nauto numbers = std::vector{1, 2, 3, 4};\nauto has_two = numbers.contains(2); \n```\n\n但是，您可以重载管道操作符来实现这种几乎等效的语法:\n\n```cpp\nauto has_two = numbers | contains(2); \n```\n\n通过使用代理类，可以很容易地完成这个任务。\n\n### 管道操作员\n\n我们这里的目标是实现一个简单的管道操作符，这样我们就可以编写以下内容:\n\n```cpp\nauto numbers = std::vector{1, 3, 5, 7, 9}; \nauto seven = 7; \nbool has_seven = numbers | contains(seven); \n```\n\n与可移植语法一起使用的`contains()`函数有两个参数:`numbers`和`seven`。因为左边的参数`numbers`可以是任何东西，所以我们需要重载来包含右边唯一的东西。因此，我们创建了一个名为`ContainsProxy`的`struct`模板，它保存了右边的参数；这样，超载的管道操作员可以识别超载:\n\n```cpp\ntemplate <typename T>\nstruct ContainsProxy { const T& value_; };\ntemplate <typename Range, typename T>\nauto operator|(const Range& r, const ContainsProxy<T>& proxy) {\n  const auto& v = proxy.value_;\n  return std::find(r.begin(), r.end(), v) != r.end();\n} \n```\n\n现在我们可以这样使用`ContainsProxy`:\n\n```cpp\nauto numbers = std::vector{1, 3, 5, 7, 9}; \nauto seven = 7; \nauto proxy = ContainsProxy<decltype(seven)>{seven};  \nbool has_seven = numbers | proxy; \n```\n\n管道操作符可以工作，尽管语法仍然很难看，因为我们需要指定类型。为了使语法更简洁，我们可以简单地创建一个便利函数，该函数接受值并创建一个包含以下类型的代理:\n\n```cpp\ntemplate <typename T>\nauto contains(const T& v) { return ContainsProxy<T>{v}; } \n```\n\n这就是我们所需要的。我们现在可以将它用于任何类型或容器:\n\n```cpp\nauto penguins = std::vector<std::string>{\"Ping\",\"Roy\",\"Silo\"};\nbool has_silo = penguins | contains(\"Silo\"); \n```\n\n本节中包含的示例显示了实现管道操作器的基本方法。保罗·富尔茨的“范围”库和“适合”库等库可在[https://github.com/pfultz2/Fit](https://github.com/pfultz2/Fit)获得，它们实现了采用常规函数的适配器，并赋予其使用管道语法调用的能力。\n\n# 摘要\n\n在这一章中，你学会了延迟评价和渴望评价的区别。您还学习了如何使用隐藏的代理对象在幕后实现延迟求值，这意味着您现在了解了如何实现延迟求值优化，同时保留类的易于使用的接口。将复杂的优化隐藏在库类中，而不是将其公开在应用代码中，这使得应用代码更易读，更不容易出错。\n\n在下一章中，我们将转移注意力，转而使用 C++ 进行并发和并行编程。"
  },
  {
    "path": "docs/cpp-hiperf/11.md",
    "content": "# 十一、并发\n\n在上一章介绍了延迟求值和代理对象之后，我们现在将探讨如何使用共享内存的线程在 C++ 中编写并发程序。我们将通过编写没有数据竞争和死锁的程序来寻找使并发程序正确的方法。本章还将包含如何让并发程序以低延迟和高吞吐量运行的建议。\n\n在我们继续之前，您应该知道这一章并不是对并发编程的完整介绍，也不会涵盖 C++ 中并发的所有细节。相反，本章介绍了用 C++ 编写并发程序的核心构建块，并混合了一些与性能相关的指导原则。如果您以前没有编写过并发程序，那么浏览一些介绍性材料来涵盖并发编程的理论方面是明智的。像死锁、临界区、条件变量和互斥这样的概念将被简要讨论，但是这将更多地作为一个复习而不是对概念的彻底介绍。\n\n本章包括以下内容:\n\n*   并发编程的基础，包括并行执行、共享内存、数据竞争和死锁\n*   介绍 C++ 线程支持库、原子库和 C++ 内存模型\n*   无锁编程的一个简单例子\n*   绩效指南\n\n# 理解并发的基础\n\n一个并发程序可以同时执行多个任务。一般来说，并发编程比顺序编程要困难得多，但是程序从并发中受益有几个原因:\n\n*   **效率**:现在的智能手机和台式电脑都有多个 CPU 内核，可以并行执行多个任务。如果你设法把一个大任务拆分成可以并行运行的子任务，理论上可以用 CPU 核数来划分大任务的运行时间。对于在只有一个内核的机器上运行的程序，如果任务是输入/输出绑定的，性能仍然会有所提高。当一个子任务等待输入输出时，其他子任务仍然可以在中央处理器上执行有用的工作。\n*   **响应性和低延迟上下文**:对于具有图形用户界面的应用，重要的是永远不要阻塞 UI，使应用变得无响应。为了防止无响应，通常让长时间运行的任务(如从磁盘加载文件或从网络获取一些数据)在单独的后台线程中执行，这样负责用户界面的线程就永远不会被长时间运行的任务阻塞。另一个低延迟的例子是实时音频。负责产生音频数据缓冲区的功能在单独的高优先级线程中执行，而程序的其余部分可以在低优先级线程中运行，以处理用户界面等。\n*   **模拟**:并发可以让模拟现实世界中并发的系统变得更加容易。毕竟，我们身边的大多数事情都是并发发生的，有时用顺序编程模型来建模并发流是非常困难的。在本书中，我们不会关注模拟，而是关注与性能相关的并发方面。\n\n并发为我们解决了许多问题，但引入了新的问题，我们将在下一节讨论。\n\n# 是什么让并发编程变得困难？\n\n并发编程很难的原因有很多，如果你以前写过并发程序，你很可能已经遇到了这里列出的那些:\n\n*   以安全的方式在多个线程之间共享状态是困难的。每当我们拥有可以同时读写的数据时，我们都需要某种方式来保护这些数据免受数据竞争的影响。稍后你会看到很多这样的例子。\n*   并发程序通常更复杂，因为有多个并行执行流。\n*   并发使调试变得复杂。由于数据竞争而出现的错误可能很难调试，因为它们依赖于线程的调度方式。这类错误很难重现，在最坏的情况下，当使用调试器运行程序时，它们甚至可能不复存在。有时，对控制台的无害调试跟踪可以改变多线程程序的行为方式，并使错误暂时消失。你被警告了！\n\n在我们开始看使用 C++ 的并发编程之前，先介绍几个与并发和并行编程相关的通用概念。\n\n# 并发性和并行性\n\n**并发**和**并行**是两个有时可以互换使用的术语。然而，它们并不相同，了解它们之间的差异很重要。如果一个程序有多个单独的控制流在重叠时间段内运行，则称该程序并发运行。在 C++ 中，每个单独的控制流都由一个线程来表示。不过，线程可能会也可能不会同时执行。如果他们这样做，他们被称为并行执行。对于并行运行的并发程序，需要在支持指令并行执行的机器上执行；也就是拥有多个 CPU 内核的机器。\n\n乍一看，似乎很明显，出于效率的原因，如果可能的话，我们总是希望并发程序并行运行。然而，这并不一定总是正确的。本章中介绍的许多同步原语(如互斥锁)只需要支持线程的并行执行。不是并行运行的并发任务不需要相同的锁定机制，并且更容易推理。\n\n## 时间分片\n\n你可能会问，“在只有一个 CPU 内核的机器上并发线程是如何执行的？”答案是**时间切片**。它与操作系统用来支持进程并发执行的机制相同。为了理解时间切片，让我们假设有两个应该同时执行的独立指令序列，如下图所示:\n\n<figure class=\"mediaobject\">![](img/B15619_11_01.png)</figure>\n\n图 11.1:两个独立的指令序列\n\n编号框代表说明。每个指令序列在单独的线程中执行，标记为 **T1** 和 **T2** 。操作系统将调度每个线程在中央处理器上有一些有限的时间，然后执行上下文切换。上下文切换将存储正在运行的线程的当前状态，并加载应该执行的线程的状态。这样做的频率足够高，以至于看起来好像线程同时在运行。然而，上下文切换非常耗时，而且很可能会在每次新线程在 CPU 内核上执行时产生大量缓存未命中。因此，我们不希望上下文切换发生得太频繁。\n\n下图显示了在单个 CPU 上调度的两个线程的可能执行顺序:\n\n<figure class=\"mediaobject\">![](img/B15619_11_02.png)</figure>\n\n图 11.2:两个线程的可能执行。圆点表示上下文切换\n\n线程 T1 的第一条指令开始，然后是一个上下文切换，让线程 T2 执行前两条指令。作为程序员，无论操作系统调度程序如何调度任务，我们都必须确保程序能够按预期运行。如果一个序列由于某种原因无效，有办法通过使用锁来控制指令的执行顺序，这将在后面介绍。\n\n如果一台机器有多个中央处理器内核，就有可能并行执行这两个线程。然而，不能保证(甚至不太可能)在程序的整个生命周期中，这两个线程将分别在一个内核上执行。整个系统在中央处理器上共享时间，因此调度程序也会让其他进程执行。这也是线程没有被调度在专用内核上的原因之一。\n\n*图 11.3* 显示了相同的两个线程的执行，但是现在它们运行在一个有两个 CPU 内核的机器上。正如所见，第一个线程的第二条和第三条指令(白色方框)与另一个线程执行的时间完全相同——两个线程并行执行:\n\n<figure class=\"mediaobject\">![](img/B15619_11_03.png)</figure>\n\n图 11.3:在多核机器上执行的两个线程。这使得并行执行两个线程成为可能。\n\n接下来我们讨论共享内存。\n\n## 共用存储器\n\n在同一进程中创建的线程共享相同的虚拟内存。这意味着线程可以访问进程中可寻址的任何数据。操作系统保护使用虚拟内存的进程之间的内存，但并不能防止我们意外访问进程内部的内存，而这些内存并不打算在不同的线程之间共享。虚拟内存只是保护我们不去访问分配给我们自己的不同进程的内存。\n\n在多个线程之间共享内存是处理线程间通信的一种非常有效的方式。然而，在线程之间以安全的方式共享内存是用 C++ 编写并发程序的主要挑战之一。我们应该始终努力最小化线程间共享资源的数量。\n\n幸运的是，默认情况下并非所有内存都是共享的。每个线程都有自己的堆栈，用于存储处理函数调用所需的局部变量和其他数据。除非一个线程将对局部变量的引用或指针传递给其他线程，否则没有其他线程能够从该线程访问堆栈。这是尽可能多地使用堆栈的又一个原因(如果您在阅读*第 7 章*、*内存管理*后，还没有确信堆栈是存放数据的好地方)。\n\n还有**线程本地存储**，有时缩写为 **TLS** ，可用于存储线程上下文中的全局变量，但线程之间不共享这些变量。线程局部变量可以被认为是一个全局变量，其中每个线程都有自己的副本。\n\n其他一切默认共享；即堆上分配的动态内存、全局变量和静态局部变量。每当您共享了由某个线程变异的数据时，您需要确保没有其他线程同时访问该数据，否则您将面临数据竞争。\n\n还记得*第七章**内存管理*中*进程内存*部分的图吗，图中图解了一个进程的虚拟地址空间？在这里，它再次被修改，以显示当一个进程包含多个线程时它是什么样子。如下图所示，每个线程都有自己的堆栈内存，但所有线程只有一个堆:\n\n<figure class=\"mediaobject\">![](img/B15619_11_04.png)</figure>\n\n图 11.4:一个进程的虚拟地址空间的可能布局\n\n在这个例子中，进程包含三个线程。默认情况下，堆内存由所有线程共享。\n\n## 数据竞赛\n\n当两个线程同时访问同一个内存，并且至少有一个线程正在改变数据时，就会发生**数据竞争**。如果你的程序有数据竞赛，这意味着你的程序有未定义的行为。编译器和优化器将*假设*你的代码中没有数据竞争，并在这个假设下优化它。这可能会导致崩溃或其他完全令人惊讶的行为。换句话说，你在任何情况下都不能允许程序中的数据竞争。编译器通常不会警告您数据竞争，因为它们在编译时很难被检测到。\n\n调试数据竞赛可能是一个真正的挑战，有时需要工具，如**线程消毒工具**(来自 Clang)或**并发可视化工具**(Visual Studio 扩展)。这些工具通常会检测代码，以便运行时库可以在运行您正在调试的程序时检测、警告或可视化潜在的数据竞争。\n\n### 示例:数据竞赛\n\n*图 11.5* 显示了两个线程将要更新一个名为`counter`的整数。假设这些线程都是用指令`++ counter`递增一个全局计数器变量。事实证明，增加一个`int`可能涉及多个中央处理器指令。这可以在不同的 CPU 上以不同的方式完成，但让我们假设`++ counter`会生成以下编造的机器指令:\n\n*   **R** :从内存中读取计数器\n*   **+1** :递增计数器\n*   **W** :将新的计数器值写入内存\n\n现在，如果我们有两个线程将更新最初为 42 的`counter`值，我们将期望在两个线程都运行之后它变成 44。但是，如下图所示，不能保证指令会按顺序执行以保证`counter`变量的正确增量。\n\n<figure class=\"mediaobject\">![](img/B15619_11_05.png)</figure>\n\n图 11.5:两个线程都在增加同一个共享变量\n\n如果没有数据竞争，计数器将达到值 44，但实际上，它只达到 43。\n\n在本例中，两个线程都读取值 42，并将该值增加到 43。然后，他们都写了新的值，43，这意味着我们永远不会达到 44 的正确答案。如果第一个线程能够在下一个线程开始读取之前写入值 43，我们最终将得到 44。还要注意，即使只有一个中央处理器内核，这也是可能的。调度程序可以用类似的方式调度这两个线程，这样两个读指令都可以在任何写操作之前执行。\n\n同样，这是一个可能的场景，但重要的是行为是未定义的。当你的程序进行数据竞赛时，任何事情都可能发生。其中一个例子是**撕**，这是**撕读**和**撕写**的通称。当一个线程将部分值写入内存，而另一个线程同时读取值时，就会发生这种情况，因此最后会得到一个损坏的值。\n\n### 避免数据竞争\n\n如何避免数据比赛？有两个主要选项:\n\n*   使用原子数据类型代替`int`。这将告诉编译器以原子方式执行读取、递增和写入。我们将在本章后面花更多的时间讨论原子数据类型。\n*   使用互斥锁(互斥)，保证多个线程不会同时执行一个关键部分。**临界区**是代码中不能同时执行的地方，因为它更新或读取共享内存，这可能会产生数据竞争。\n\n还值得强调的是，不可变的数据结构——从不改变的数据结构——可以被多个线程访问，而没有任何数据竞争的风险。最大限度地减少可变对象的使用有很多好处，但是在编写并发程序时，这变得更加重要。一种常见的模式是总是创建新的不可变对象，而不是改变现有的对象。当新对象完全构造并表示新状态时，它可以与旧对象交换。这样，我们就可以最小化代码的关键部分。只有交换是一个关键部分，因此需要受到原子操作或互斥的保护。\n\n## 互斥（体）…\n\n一个**互斥体**，简称**互斥锁**，是一个避免数据竞争的同步原语。需要进入临界区的线程首先需要锁定互斥体(锁定有时也称为获取互斥锁)。这意味着在持有锁的第一个线程解锁互斥体之前，没有其他线程可以锁定同一个互斥体。这样，互斥保证了一次只有一个线程在临界区内。\n\n在*图 11.6* 中，可以看到*一节演示的竞态条件是如何通过使用互斥来避免数据竞态示例*的。标有 **L** 的指令为锁定指令，标有 **U** 的指令为解锁指令。在核心 0 上执行的第一个线程首先到达临界区，并在读取计数器值之前锁定互斥体。然后，它将计数器加 1，并将其写回内存。之后，它会释放锁。\n\n在核心 1 上执行的第二个线程在第一个线程获得互斥锁后到达临界区。由于互斥体已经被锁定，线程被阻塞，直到第一个线程不受干扰地更新了计数器并释放了互斥体:\n\n<figure class=\"mediaobject\">![](img/B15619_11_06.png)</figure>\n\n图 11.6:互斥锁保护了关键部分，避免了计数器变量上的数据竞争\n\n最终的结果是，这两个线程可以以安全和正确的方式更新可变共享变量。但是也意味着两个线程不能再并行运行了。如果线程所做的大部分工作都不能在不序列化的情况下完成，那么从性能的角度来看，使用线程是没有意义的。\n\n第二线程被第一线程阻塞的状态称为**争用**。这是我们努力最小化的事情，因为它损害了并发程序的可伸缩性。如果争用程度很高，增加更多的 CPU 内核不会提高性能。\n\n## 僵局\n\n当使用互斥锁保护共享资源时，存在陷入名为**死锁**状态的风险。当两个线程在等待对方释放锁时，可能会发生死锁。这两个线程都无法继续运行，并且都陷入了死锁状态。发生死锁需要满足的一个条件是，一个已经持有锁的线程试图获取额外的锁。当系统变得越来越大时，跟踪系统中运行的所有线程可能使用的所有锁变得越来越困难。这是总是试图最小化共享资源使用的一个原因，这证明了独占锁定的必要性。\n\n*图 11.7* 显示两个线程处于等待状态，试图获取另一个线程持有的锁:\n\n<figure class=\"mediaobject\">![](img/B15619_11_07.png)</figure>\n\n图 11.7:死锁状态的一个例子\n\n接下来让我们讨论同步和异步任务。\n\n## 同步和异步任务\n\n本章我将参考**同步任务**和**异步任务**。同步任务就像普通的 C++ 函数。当同步任务完成它应该做的任何事情时，它会将控制权返回给任务的调用方。任务的调用方正在等待或被阻止，直到同步任务完成。\n\n另一方面，异步任务会立即将控件返回给调用方，并同时执行它的工作。\n\n*图 11.8* 中的顺序分别显示了调用同步和异步任务的区别:\n\n<figure class=\"mediaobject\">![](img/B15619_11_08.png)</figure>\n\n图 11.8:同步调用和异步调用。异步任务立即返回，但在调用方重新获得控制后继续工作。\n\n如果你之前没有见过异步任务，一开始可能会觉得奇怪，因为 C++ 中的普通函数遇到返回语句或者到达函数体末尾的时候总是会停止执行。异步应用编程接口越来越普遍，你可能以前遇到过，例如，在使用异步 JavaScript 时。\n\n有时，我们使用术语**阻塞**来表示阻塞调用者的操作；也就是说，让调用者一直等到操作完成。\n\n对并发性有了一个大概的介绍之后，是时候探索 C++ 对线程编程的支持了。\n\n# C++ 中的并发编程\n\nC++ 中的并发支持使得程序可以并发执行多个任务。如前所述，编写一个正确的并发 C++ 程序通常比编写一个在一个线程中顺序执行所有任务的程序要困难得多。本节还将演示一些常见的陷阱，让您意识到编写并发程序所涉及的所有困难。\n\n并发支持最初是在 C++ 11 中引入的，后来扩展到 C++ 14、C++ 17 和 C++ 20。在并发成为语言的一部分之前，它是通过操作系统、**POSIX Threads**(**pthreads**)或其他一些库的本机并发支持来实现的。\n\n有了 C++ 语言中直接的并发支持，我们就可以编写跨平台的并发程序了，太棒了！然而，有时在处理平台上的并发性时，您必须获得特定于平台的功能。例如，C++ 标准库中不支持设置线程优先级、配置 CPU 亲缘关系(CPU 固定)或设置新线程的堆栈大小。\n\n还应该说，随着 C++ 20 的发布，线程支持库得到了相当大的扩展，在语言的未来版本中很可能会增加更多的特性。由于硬件的开发方式，对良好的并发支持的需求越来越大，在高并发程序的效率、可伸缩性和正确性方面还有很多有待发现的地方。\n\n## 线程支持库\n\n我们现在将浏览 C++ 线程支持库，并介绍其最重要的组件。\n\n### 线\n\n一个正在运行的程序至少包含一个线程。当你的主函数被调用时，它在一个线程上执行，这个线程通常被称为**主线程**。每个线程都有一个标识符，这在调试并发程序时非常有用。以下程序打印主线程的线程标识符:\n\n```cpp\nint main() { \n  std::cout << \"Thread ID: \" <<  std::this_thread::get_id() << '\\n'; \n} \n```\n\n运行前面的程序可能会产生如下结果:\n\n```cpp\n Thread ID: 0x1001553c0 \n```\n\n可以让线程休眠。生产代码中很少使用 Sleep，但是在调试过程中非常有用。例如，如果您有一个只在极少数情况下发生的数据竞争，那么在代码中添加睡眠可能会使它出现得更频繁。这就是如何让当前运行的线程休眠一秒钟:\n\n```cpp\nstd::this_thread::sleep_for(std::chrono::seconds{1}); \n```\n\n在代码中插入随机休眠后，您的程序永远不应该暴露任何数据竞争。添加睡眠后，您的程序可能无法令人满意地工作；缓冲区可能会变满，UI 可能会滞后，等等，但是它应该总是以可预测和定义的方式运行。我们无法控制线程的调度，随机休眠模拟了不太可能但可能的调度场景。\n\n现在，让我们使用`<thread>`头中的`std::thread`类创建一个额外的线程。它代表一个执行线程，通常是一个操作系统线程的包装器。`print()`函数将从我们显式创建的线程中调用:\n\n```cpp\nvoid print() { \n  std::this_thread::sleep_for(std::chrono::seconds{1}); \n  std::cout << \"Thread ID: \"<<  std::this_thread::get_id() << '\\n'; \n} \n\nint main() { \n  auto t1 = std::thread{print}; \n  t1.join(); \n  std::cout << \"Thread ID: \"<<  std::this_thread::get_id() << '\\n'; \n} \n```\n\n当创建线程时，我们传入一个可调用对象(一个函数、lambda 或一个函数对象)，每当线程在 CPU 上获得预定时间时，它就会开始执行该对象。我添加了一个睡眠调用，让我们明白为什么需要在线程上调用`join()`。当一个`std::thread`对象被析构时，它一定是*加入了*或者*分离了*，否则会导致程序调用`std::terminate()`，如果我们没有安装自定义的`std::terminate_handler`，默认情况下会调用`std::abort()`。\n\n在前面的例子中，`join()`函数是阻塞的——它一直等到线程运行完毕。因此，在前面的例子中，`main()`函数将不会返回，直到线程`t1`完成运行。考虑以下行:\n\n```cpp\nt1.join(); \n```\n\n假设我们通过用下面一行替换前面一行来分离线程`t1`:\n\n```cpp\nt1.detach(); \n```\n\n在这种情况下，我们的主函数会在线程`t1`醒来打印消息之前结束，结果程序(很可能)只会输出主线程的线程 ID。请记住，我们无法控制线程的调度，在`print()`函数有时间休眠、唤醒并打印其线程标识后，主线程有可能但极不可能输出其消息*。*\n\n在这个例子中用`detach()`代替`join()`也引入了另一个问题。我们在没有任何同步的情况下使用两个线程的`std::cout`，并且由于`main()`不再等待线程`t1`完成，理论上它们都可以并行使用`std::cout`。幸运的是，`std::cout`是线程安全的，可以从多个线程使用，不会引入数据竞争，因此没有未定义的行为。但是，线程生成的输出仍然有可能是交错的，导致如下情况:\n\n```cpp\nThread ID: Thread ID: 0x1003a93400x700004fd4000 \n```\n\n如果我们想避免交错输出，我们需要将字符的输出视为一个关键部分，并同步访问`std::cout`。稍后我们将更多地讨论关键路段和比赛条件，但首先，让我们介绍一下`std::thread`的一些细节。\n\n### 线程状态\n\n在我们进一步讨论之前，您应该很好地理解`std::thread`对象真正代表什么，以及它可以处于什么状态。我们还没有讨论在执行 C++ 程序的系统中通常有什么样的线程。\n\n在下图中，您可以看到一个假设的运行系统的快照。\n\n<figure class=\"mediaobject\">![](img/B15619_11_09.png)</figure>\n\n图 11.9:假设运行系统的快照\n\n从底部开始，图中显示了 CPU 及其**硬件线程**。那些是中央处理器上的执行单元。在这个例子中，中央处理器提供了四个硬件线程。通常表示它有四个内核，但也可以是其他配置；例如，一些内核可以执行两个硬件线程。这就是通常所说的**超线程**。运行时可以用以下内容打印硬件线程的总数:\n\n```cpp\n std::cout << std::thread::hardware_concurrency() << '\\n';\n  // Possible output: 4 \n```\n\n如果在运行平台上无法确定硬件线程的数量，前面的代码也可能输出`0`。\n\n硬件线程上面的层包含**操作系统线程**。这些是实际的软件线程。操作系统调度器确定硬件线程何时执行操作系统线程以及执行多长时间。在*图 11.9* 中，目前六个软件线程中有三个正在执行。\n\n图中最顶层包含`std::thread`对象。`std::thread`对象只不过是一个普通的 C++ 对象，可能与底层操作系统线程相关联，也可能不相关联。`std::thread`的两个实例不能与同一个底层线程相关联。在图中可以看到程序目前有三个`std::thread`的实例；两个与线程相关联，一个与线程无关。可以使用`std::thread::joinable`属性来找出`std::thread`对象处于什么状态。如果螺纹已经:\n\n*   默认构造；也就是说，如果它没有什么可执行的\n*   已从(其关联的运行线程已转移到另一个`std::thread`对象)\n*   通过调用`detach()`分离\n*   已经加入了对`join()`的呼叫\n\n否则`std::thread`对象是处于可合并状态的。请记住，当`std::thread`对象被析构时，它必须不再处于可连接状态，否则程序将终止。\n\n### 可接合螺纹\n\nC++ 20 引入了一个新的线程类`std::jthread`。它与`std::thread`非常相似，但有几个重要的补充:\n\n*   `std::jthread`支持使用停止令牌停止线程。这是我们在 C++ 20 之前使用`std::thread`时不得不手动实现的东西。\n*   `std::jthread`的析构函数将发送停止请求，并在销毁时加入线程，而不是在应用处于不可连接状态时终止应用。\n\n接下来我将说明后一点。首先，我们将使用`print()`函数，其定义如下:\n\n```cpp\nvoid print() {\n  std::this_thread::sleep_for(std::chrono::seconds{1});\n  std::cout << \"Thread ID: \"<<  std::this_thread::get_id() << '\\n';\n} \n```\n\n它休眠一秒钟，然后打印当前线程标识符:\n\n```cpp\nint main() {\n  std::cout << \"main begin\\n\"; \n  auto joinable_thread = std::jthread{print};  \n  std::cout << \"main end\\n\";\n} // OK: jthread will join automatically \n```\n\n在我的机器上运行代码时产生了以下输出:\n\n```cpp\nmain begin\nmain end\nThread ID: 0x1004553c0 \n```\n\n现在让我们改变我们的`print()`函数，使它在一个循环中连续输出消息。然后，我们需要某种方式来告知`print()`功能何时停止。`std::jthread`(相对于`std::thread`)通过使用停止令牌对此提供了内置支持。当`std::jthread`调用`print()`函数时，如果`print()`函数接受这样的参数，它可以传递一个`std::stop_token`的实例。下面是我们如何使用停止标记实现这个新的`print()`函数的一个例子:\n\n```cpp\nvoid print(std::stop_token stoken) {\n  while (!stoken.stop_requested()) { \n    std::cout << std::this_thread::get_id() << '\\n';\n    std::this_thread::sleep_for(std::chrono::seconds{1});\n  }\n  std::cout << \"Stop requested\\n\";\n} \n```\n\n`while`-循环在每次迭代时通过调用`stop_requested()`检查该函数是否被请求停止。从我们的`main()`功能，现在可以通过调用我们的`std::jthread`实例上的`request_stop()`来请求停止:\n\n```cpp\nint main() {\n  auto joinable_thread = std::jthread(print);\n  std::cout << \"main: goes to sleep\\n\";\n  std::this_thread::sleep_for(std::chrono::seconds{3});\n  std::cout << \"main: request jthread to stop\\n\";\n  joinable_thread.request_stop();\n} \n```\n\n当我运行这个程序时，它会生成以下输出:\n\n```cpp\nmain: goes to sleep\nThread ID: 0x70000f7e1000\nThread ID: 0x70000f7e1000\nThread ID: 0x70000f7e1000\nmain: request jthread to stop\nStop requested \n```\n\n在这个例子中，我们可以省略对`request_stop()`的显式调用，因为`jthread`将在销毁时自动调用`request_stop()`。\n\n新的`jthread`类是对 C++ 线程库的欢迎添加，它应该是在 C++ 中获取线程类的首选。\n\n### 保护关键部分\n\n正如我已经提到的，我们的代码不能包含任何数据竞争。不幸的是，用数据种族编写代码非常容易。当使用线程以这种方式编写并发程序时，找到关键部分并用锁保护它们是我们经常需要考虑的事情。\n\nC++ 为我们提供了一个`std::mutex`类，可以用来保护关键部分，避免数据竞争。我将通过一个经典的例子演示如何使用互斥体，这个例子使用了一个由多个线程更新的共享可变计数器变量。\n\n首先，我们定义一个全局可变变量和递增计数器的函数:\n\n```cpp\nauto counter = 0; // Warning! Global mutable variable\nvoid increment_counter(int n) {\n  for (int i = 0; i < n; ++ i)\n    ++ counter;\n} \n```\n\n接下来的`main()`函数创建了两个线程，它们都将执行`increment_counter()`函数。还要注意在这个例子中，我们如何将参数传递给线程调用的函数。我们可以向线程构造函数传递任意数量的参数，以便匹配要调用的函数签名中的参数。最后，我们断言，如果程序没有竞争条件，计数器具有我们期望的值:\n\n```cpp\nint main() {\n  constexpr auto n = int{100'000'000};\n  {\n    auto t1 = std::jthread{increment_counter, n};\n    auto t2 = std::jthread{increment_counter, n};\n  }\n  std::cout << counter << '\\n';\n  // If we don't have a data race, this assert should hold:\n  assert(counter == (n * 2));\n} \n```\n\n这个计划很可能会失败。`assert()`功能不成立，因为程序当前包含比赛条件。当我重复运行程序时，最终得到的是不同的计数器值。我没有达到`200000000`的值，反而最终只得到`137182234`。该示例与本章前面说明的数据竞争示例非常相似。\n\n带有表达式`++ counter`的行是一个关键部分——它使用一个共享的可变变量，并由多个线程执行。为了保护关键部分，我们现在将使用包含在`<mutex>`标题中的`std::mutex`。稍后，您将看到我们如何通过使用 atomics 来避免本例中的数据竞争，但是，目前，我们将使用锁。\n\n首先，我们在`counter`旁边添加全局`std::mutex`对象:\n\n```cpp\nauto counter = 0; // Counter will be protected by counter_mutex\nauto counter_mutex = std::mutex{}; \n```\n\n但是`std::mutex`对象本身不就是一个可变的共享变量吗，如果被多个线程使用，会产生数据竞争？是的，它是一个可变的共享变量，但是不，它不会产生数据竞争。C++ 线程库中的同步原语，比如`std::mutex`，就是为这个特殊目的而设计的。在这方面，它们非常特殊，使用硬件指令，或者我们平台上需要的任何东西，来保证它们自己不会产生数据竞赛。\n\n现在我们需要在关键部分使用互斥体来读取和更新计数器变量。我们可以在`counter_mutex`上使用`lock()`和`unlock()`成员函数，但是更安全的方法是总是使用 RAII 来处理互斥。把互斥体想象成一种资源，当我们使用完它时，它总是需要被解锁。线程库为我们提供了一些有用的 RAII 类模板来处理锁定。在这里，我们将使用`std::scoped_lock<Mutex>`模板来确保我们安全地释放互斥体。下面是更新后的`increment_counter()`函数，现在用互斥锁保护:\n\n```cpp\nvoid increment_counter(int n) {\n  for (int i = 0; i < n; ++ i) {\n    auto lock = std::scoped_lock{counter_mutex};\n    ++ counter;\n  }\n} \n```\n\n该程序现已从数据竞赛中解放出来，并按预期运行。如果我们再运行一次，`assert()`函数中的条件现在将成立。\n\n### 避免僵局\n\n只要一个线程从不一次获取多个锁，就没有死锁的风险。然而，有时需要在已经持有先前获得的锁的同时获得另一个锁。在这些情况下，可以通过同时抓住两个锁来避免死锁的风险。C++ 通过使用`std::lock()`函数有一种方法可以做到这一点，该函数接受任意数量的锁和块，直到获取所有锁。\n\n下面是一个账户间转账的例子。在交易过程中，两个账户都需要保护，因此我们需要同时获取两个锁。以下是它的工作原理:\n\n```cpp\nstruct Account { \n  Account() {} \n  int balance_{0}; \n  std::mutex m_{}; \n}; \n\nvoid transfer_money(Account& from, Account& to, int amount) { \n   auto lock1 = std::unique_lock<std::mutex>{from.m_, std::defer_lock}; \n   auto lock2 = std::unique_lock<std::mutex>{to.m_, std::defer_lock}; \n\n   // Lock both unique_locks at the same time \n   std::lock(lock1, lock2); \n\n   from.balance_ -= amount; \n   to.balance_ += amount; \n} \n```\n\n我们再次使用一个 RAII 类模板来确保每当这个函数返回时，我们都会释放锁。在这种情况下，我们使用`std::unique_lock`，这为我们提供了延迟互斥锁的可能性。然后，我们通过使用`std::lock()`函数显式地同时锁定两个互斥体。\n\n### 条件变量\n\n一个**条件变量**使得线程可以等待，直到满足某个特定的条件。线程也可以使用条件变量向其他线程发出条件已经改变的信号。\n\n并发程序中的一种常见模式是让一个或多个线程等待数据以某种方式被使用。这些线程通常被称为**消费者**。另一组线程则负责生成准备使用的数据。这些产生数据的线程被称为**生产者**，如果只是一个线程，则称为**生产者**。\n\n生产者和消费者模式可以使用条件变量来实现。为此，我们可以使用`std::condition_variable`和`std::unique_lock`的组合。让我们看一个生产者和消费者的例子，让他们不那么抽象:\n\n```cpp\nauto cv = std::condition_variable{}; \nauto q = std::queue<int>{}; \nauto mtx = std::mutex{};     // Protects the shared queue \nconstexpr int sentinel = -1; // Value to signal that we are done \n\nvoid print_ints() { \n  auto i = 0; \n  while (i != sentinel) { \n    { \n      auto lock = std::unique_lock<std::mutex>{mtx}; \n      while (q.empty()) {\n        cv.wait(lock); // The lock is released while waiting \n      }\n      i = q.front(); \n      q.pop(); \n    } \n    if (i != sentinel) { \n      std::cout << \"Got: \" << i << '\\n'; \n    } \n  } \n} \n\nauto generate_ints() { \n  for (auto i : {1, 2, 3, sentinel}) { \n    std::this_thread::sleep_for(std::chrono::seconds(1)); \n    { \n      auto lock = std::scoped_lock{mtx}; \n      q.push(i); \n    } \n    cv.notify_one(); \n  } \n} \n\nint main() { \n   auto producer = std::jthread{generate_ints}; \n   auto consumer = std::jthread{print_ints}; \n} \n```\n\n我们正在创建两条线程:一条`consumer`线程和一条`producer`线程。`producer`线程生成一个整数序列，每秒钟将它们推到全局`std::queue<int>`一次。每当一个元素被添加到队列中，生产者使用`notify_one()`发出条件已经改变的信号。\n\n程序检查队列中是否有可供消费线程消费的数据。还要注意，在通知条件变量时，不需要持有锁。\n\n使用者线程负责将数据(即整数)打印到控制台。它使用条件变量来等待空队列的改变。当消费者调用`cv.wait(lock)`时，线程进入睡眠状态，离开 CPU 让其他线程执行。重要的是要理解为什么我们在调用`wait()`时需要传递变量`lock`。除了让线程休眠之外，`wait()`还会在休眠时解锁互斥体，然后在互斥体返回之前获取互斥体。如果`wait()`没有释放互斥体，生产者将无法向队列中添加元素。\n\n为什么消费者在等待带有`while`循环的条件变量，而不是`if`语句？这是一种常见的模式，有时我们需要这样做，因为可能有其他消费者也在我们之前被唤醒并清空了队列。然而，在我们的程序中，我们只有一个消费者线程，所以这不可能发生。然而，消费者有可能从等待中醒来，即使生产者线程没有信号。这种现象被称为**虚假唤醒**，这种情况发生的原因超出了本书的范围。\n\n作为使用`while`循环的替代，我们可以使用接受谓词的`wait()`的重载版本。这个版本的`wait()`检查谓词是否满足，并将为我们做循环。在我们的示例中，它看起来像这样:\n\n```cpp\n// ...\nauto lock = std::unique_lock<std::mutex>{mtx}; \ncv.wait(lock, [] { return !q.empty(); });\n// ... \n```\n\n你可以在安东尼·威廉姆斯的 *C++ 并发运行**第二版*中找到更多关于虚假唤醒的信息。你现在至少知道如何处理可能发生虚假唤醒的情况:总是在 while 循环中检查条件，或者使用接受谓词*的重载版本`wait()`。*\n\n条件变量和互斥体是自在 C++ 中引入线程以来，在 C++ 中已经可用的同步原语。C++ 20 附带了用于同步线程的额外有用的类模板，即`std::counting_semaphore`、`std::barrier`和`std::latch`。稍后我们将介绍这些新的原语。首先，我们将花一些时间在返回值和错误处理上。\n\n### 返回数据和处理错误\n\n本章到目前为止给出的例子已经使用共享变量在线程之间传递状态。我们使用互斥锁来确保避免数据竞争。像我们一直在做的那样，在互斥体中使用共享数据，当程序的大小增加时，可能很难做到正确。在维护使用扩展到代码库的显式锁定的代码方面，也有很多工作要做。跟踪共享内存和显式锁定会让我们远离真正想要完成的事情，并在编写程序时花费时间。\n\n此外，我们还没有处理任何错误处理。如果一个线程需要向其他线程报告错误怎么办？我们如何使用异常来做到这一点，就像我们习惯于在函数需要报告运行时错误时所做的那样？\n\n在标准库`<future>`头中，我们可以找到一些类模板，可以帮助我们编写没有全局变量和锁的并发代码，此外，还可以在线程之间传递异常来处理错误。我现在将展示**期货**和**承诺**，它们代表了价值的两个方面。未来是价值的接受方，承诺是价值的回报方。\n\n下面是一个使用`std::promise`将结果返回给调用者的例子:\n\n```cpp\nauto divide(int a, int b, std::promise<int>& p) { \n  if (b == 0) { \n    auto e = std::runtime_error{\"Divide by zero exception\"}; \n    p.set_exception(std::make_exception_ptr(e)); \n  } \n  else { \n    const auto result = a / b; \n    p.set_value(result); \n  } \n} \n\nint main() { \n   auto p = std::promise<int>{}; \n   std::thread(divide, 45, 5, std::ref(p)).detach(); \n\n   auto f = p.get_future(); \n   try { \n     const auto& result = f.get(); // Blocks until ready \n     std::cout << \"Result: \" << result << '\\n'; \n   } \n   catch (const std::exception& e) { \n     std::cout << \"Caught exception: \" << e.what() << '\\n'; \n   } \n} \n```\n\n调用者(即`main()`函数)创建`std::promise`对象，并将其传递给`divide()`函数。我们需要使用来自`<functional>`的`std::ref`，以便可以通过`std::thread`到`compute()`正确转发参考。\n\n当`divide()`函数计算出结果后，它通过调用`set_value()`函数通过传递返回值。如果`divide()`函数出现错误，它会调用承诺上的`set_exception()`函数。\n\n未来代表计算的价值，它可能已经计算，也可能还没有计算。因为未来是一个普通的对象，例如，我们可以把它传递给其他需要计算值的对象。最后，当某个客户端需要该值时，它会调用`get()`来获取实际值。如果在该时间点没有计算，对`get()`的调用将被阻止，直到完成。\n\n还要注意我们是如何通过正确的错误处理来回传递数据的，没有使用任何共享的全局数据，也没有显式锁定。承诺为我们解决了这个问题，我们可以专注于实现程序的基本逻辑。\n\n### 任务\n\n有了未来和承诺，我们设法摆脱了显式锁和共享的全局数据。我们的代码将受益于尽可能使用更高级别的抽象，尤其是当代码库增长时。在这里，我们将进一步探索自动为我们设置未来和承诺的类。您还将看到我们如何摆脱线程的手动管理，并将其留给库。\n\n在许多情况下，我们不需要管理线程；相反，我们真正需要的是能够异步执行一个**任务**，并让该任务自己与程序的其余部分同时执行，然后最终将结果或错误传达给程序中需要它的部分。这项任务应该单独执行，以最大限度地减少争用和数据竞争的风险。\n\n我们将从重写前面两个数相除的例子开始。这一次，我们将使用`<future>`中的`std::packaged_task`，这使得设定承诺的所有工作对我们来说都是正确的:\n\n```cpp\nint divide(int a, int b) { // No need to pass a promise ref here! \n  if (b == 0) { \n    throw std::runtime_error{\"Divide by zero exception\"}; \n  } \n  return a / b; \n} \n\nint main() { \n  auto task = std::packaged_task<decltype(divide)>{divide}; \n  auto f = task.get_future(); \n  std::thread{std::move(task), 45, 5}.detach(); \n\n  // The code below is unchanged from the previous example \n  try { \n    const auto& result = f.get(); // Blocks until ready \n    std::cout << \"Result: \" << result << '\\n'; \n  } \n  catch (const std::exception& e) { \n    std::cout << \"Caught exception: \" << e.what() << '\\n'; \n  } \n  return 0; \n} \n```\n\n`std::packaged_task`本身是一个可调用的对象，可以移动到我们正在创建的`std::thread`对象。如你所见，`std::packaged_task`现在为我们做了大部分工作:我们不必自己创造承诺。但是，更重要的是，我们可以像普通函数一样编写我们的`divide()`函数，而不需要通过 promise 显式返回值或异常；`std::packaged_task`会为我们做到这一点。\n\n作为本节的最后一步，我们还希望摆脱手动线程管理。创建线程不是免费的，稍后您将看到程序中的线程数量会影响性能。似乎我们是否应该为我们的`divide()`函数创建一个新线程的问题不一定取决于`divide()`的调用者。库再次通过提供另一个有用的名为`std::async()`的函数模板来帮助我们。在我们的`divide()`示例中，我们唯一需要做的是用对`std::async()`的简单调用替换创建`std::packaged_task`和`std::thread`对象的代码:\n\n```cpp\n auto f = std::async(divide, 45, 5); \n```\n\n我们现在已经从基于线程的编程模型切换到了基于任务的模型。完整的基于任务的示例现在如下所示:\n\n```cpp\nint divide(int a, int b) { \n  if (b == 0) { \n    throw std::runtime_error{\"Divide by zero exception\"}; \n  } \n  return a / b; \n} \n\nint main() { \n  auto future = std::async(divide, 45, 5); \n  try { \n    const auto& result = future.get(); \n    std::cout << \"Result: \" << result << '\\n'; \n  } \n  catch (const std::exception& e) { \n    std::cout << \"Caught exception: \" << e.what() << '\\n'; \n  } \n} \n```\n\n这里只剩下最少的代码来处理并发性。异步调用函数的推荐方式是使用`std::async()`。关于为什么以及什么时候首选`std::async()`的更深入的讨论，我强烈推荐斯科特·迈耶斯在*有效现代 C++* 中的*并发*章节。\n\n## C++ 20 中的附加同步原语\n\nC++ 20 附带了一些额外的同步原语，即`std::latch`、`std::barrier`和`std::counting_semaphore`(以及模板专门化`std::binary_semaphore`)。本部分将概述这些新类型以及它们可能有用的一些典型场景。我们将从`std::latch`开始。\n\n### 使用闩锁\n\n锁存器是一种同步原语，可用于同步多个线程。它创建了一个所有线程必须到达的同步点。你可以把锁存器想象成递减计数器。通常，所有线程都会递减计数器一次，然后等待锁存达到零再继续。\n\n锁存器通过传递内部计数器的初始值来构造:\n\n```cpp\nauto lat = std::latch{8}; // Construct a latch initialized with 8 \n```\n\n然后线程可以使用`count_down()`递减计数器:\n\n```cpp\nlat.count_down(); // Decrement but don't wait \n```\n\n线程可以等待锁存达到零:\n\n```cpp\nlat.wait(); // Block until zero \n```\n\n也可以检查(无阻塞)计数器是否达到零:\n\n```cpp\nif (lat.try_wait()) { \n  // All threads have arrived ...\n} \n```\n\n通常在递减计数器后等待锁存器达到零，如下所示:\n\n```cpp\nlat.count_down();\nlat.wait(); \n```\n\n事实上，这个用例足够常见，值得一个定制的成员函数；`arrive_and_wait()`递减锁存器，然后等待锁存器达到零:\n\n```cpp\nlat.arrive_and_wait(); // Decrement and block while not zero \n```\n\n在处理并发性时，加入一组分叉任务是一个常见的场景。如果任务只需要在最后加入，我们可以使用未来对象的数组(等待)或者只等待所有线程完成。但是在其他情况下，我们希望一组异步任务到达一个公共的同步点，然后让任务继续运行。这些情况通常发生在多个工作线程开始实际工作之前需要某种初始化的时候。\n\n#### 示例:使用 std::latch 初始化线程\n\n下面的例子演示了当多个工作线程在开始工作之前需要运行一些初始化代码时如何使用`std::latch`。\n\n创建线程时，会为堆栈分配一个连续的内存块。通常，当该内存首次在虚拟地址空间中分配时，它尚未驻留在物理内存中。相反，当使用堆栈时，将产生*页错误*，以便将虚拟内存映射到物理内存。操作系统为我们处理映射，这是一种在需要时延迟映射内存的有效方法。通常，这正是我们想要的:我们尽可能晚且仅在需要时支付内存映射的成本。但是，在低延迟很重要的情况下，例如在实时代码中，可能需要完全避免页面错误。堆栈内存不太可能被操作系统调出，因此运行一些会产生页面错误的代码，从而将虚拟堆栈内存映射到物理内存，通常就足够了。这个过程叫做**预置**。\n\n没有可移植的方法来设置或获取 C++ 线程的堆栈大小，所以这里我们将假设堆栈至少为 500 KB。下面的代码试图预设堆栈的前 500 KB:\n\n```cpp\nvoid prefault_stack() {\n  // We don't know the size of the stack\n  constexpr auto stack_size = 500u * 1024u; \n  // Make volatile to avoid optimization\n  volatile unsigned char mem[stack_size]; \n  std::fill(std::begin(mem), std::end(mem), 0);\n} \n```\n\n这里的想法是在堆栈上分配一个会占用大量堆栈内存的数组。然后，为了生成页面错误，我们使用`std::fill()`写入数组中的每个元素。volatile 关键字在前面没有提到，它在 C++ 中是一个有些混乱的关键字。与并发无关；这里添加它只是为了防止编译器优化掉这些代码。通过声明`mem`数组`volatile`，编译器不允许忽略对数组的写入。\n\n现在，让我们关注实际的`std::latch`。假设我们想要创建大量的工作线程只有在所有的线程栈都被预置后才开始工作。我们可以使用`std::latch`来实现这种同步，如下所示:\n\n```cpp\nauto do_work() { /* ... */ }\nint main() {\n  constexpr auto n_threads = 2;\n  auto initialized = std::latch{n_threads};\n  auto threads = std::vector<std::thread>{};\n  for (auto i = 0; i < n_threads; ++ i) {\n    threads.emplace_back([&] {\n      prefault_stack();\n      initialized.arrive_and_wait(); \n      do_work();\n    });\n  }\n  initialized.wait();\n  std::cout << \"Initialized, starting to work\\n\";\n  for (auto&& t : threads) {\n    t.join();\n  }\n} \n```\n\n所有线程到达后，主线程可以开始向工作线程提交工作。在这个例子中，所有线程都在等待其他线程到达，方法是在锁存器上调用`arrive_and_wait()`。一旦闩锁达到零，就不能再使用了。没有复位闩锁的功能。如果我们有一个场景需要多个同步点，我们可以改为使用`std::barrier`。\n\n### 使用屏障\n\n屏障类似于闩锁，但有两个主要的增加:屏障可以*重用*，并且每当所有线程都到达屏障时，它可以运行*完成功能*。\n\n通过传递内部计数器的初始值和完成函数来构建屏障:\n\n```cpp\nauto bar = std::barrier{8, [] {\n  // Completion function\n  std::cout \"All threads arrived at barrier\\n\";\n}}; \n```\n\n线程可以像我们使用闩锁一样到达并等待:\n\n```cpp\nbar.arrive_and_wait(); // Decrement but don't wait \n```\n\n每当所有线程都到达时(也就是说，当屏障的内部计数器达到零时)，就会发生两件事:\n\n*   屏障调用提供给构造函数的完成函数。\n*   完成函数返回后，内部计数器复位到初始值。\n\n障碍在基于**分叉连接模型**的并行编程算法中很有用。通常，迭代算法包含一个可以并行运行的部分和另一个需要顺序运行的部分。多个任务被分叉并并行运行。然后，当所有任务都完成并连接后，执行一些单线程代码来确定算法应该继续还是完成。\n\n<figure class=\"mediaobject\">![](img/B15619_11_10.png)</figure>\n\n图 11.10:分叉连接模型的一个例子\n\n遵循分叉连接模型的并发算法将受益于使用障碍，并且可以优雅而高效的方式避免其他显式锁定机制。让我们看看如何使用屏障但是用两个主要的来解决一个简单的问题。\n\n#### 示例:使用 std::barrier 的分叉连接\n\n我们的下一个例子是一个玩具问题，它将演示叉连接模型。我们将创建一个小的程序，它将模拟一组掷骰子，并计算在获得所有 6 分之前需要掷骰子的次数。掷骰子是我们可以同时做的事情。在单线程中执行的连接步骤检查结果，并确定是再次掷骰子还是结束。\n\n首先，我们需要实现六面掷骰子的代码。为了生成 1 到 6 之间的数字，我们可以使用在`<random>`标题中找到的类的组合，如下所示:\n\n```cpp\nauto engine = \n  std::default_random_engine{std::random_device{}()};\nauto dist = std::uniform_int_distribution<>{1, 6};\nauto result = dist(engine); \n```\n\n这里`std::random_device`负责为引擎生成一个种子，该种子将产生伪随机数。要以相等的概率选择 1 到 6 之间的整数，我们使用`std::uniform_int_distribution`。变量`result`是掷骰子的结果。\n\n现在，我们希望将这段代码封装到一个函数中，该函数将生成一个随机整数。生成种子和创建引擎通常很慢，我们希望避免每次调用都这样做。一种常见的方法是声明具有`static`持续时间的随机引擎，以便它在程序的整个生命周期内都存在。然而，`<random>`中的类不是线程安全的，所以我们需要以某种方式保护`static`引擎。我将借此机会演示如何使用线程本地存储，而不是用互斥锁来同步访问，互斥锁会使随机数生成器按顺序运行。\n\n下面是如何将引擎声明为`static thread_local`对象:\n\n```cpp\nauto random_int(int min, int max) {\n  // One engine instance per thread\n  static thread_local auto engine = \n    std::default_random_engine{std::random_device{}()};\n  auto dist = std::uniform_int_distribution<>{min, max};\n  return dist(engine);\n} \n```\n\n每个线程将创建一个存储持续时间为`thread_local`的静态变量；因此，在不使用任何同步原语的情况下，从多个线程并发调用`random_int()`是安全的。有了这个小助手函数，我们可以继续使用`std::barrier`来实现我们程序的其余部分:\n\n```cpp\nint main() {\n  constexpr auto n = 5; // Number of dice\n  auto done = false;\n  auto dice = std::array<int, n>{};\n  auto threads = std::vector<std::thread>{};\n  auto n_turns = 0;\n  auto check_result = [&] { // Completion function\n    ++ n_turns;\n    auto is_six = [](auto i) { return i == 6; };\n    done = std::all_of(dice.begin(), dice.end(), is_six); \n  };\n  auto bar = std::barrier{n, check_result}; \n  for (int i = 0; i < n; ++ i) {\n    threads.emplace_back([&, i] {\n      while (!done) {\n        dice [i] = random_int(1, 6); // Roll dice        \n        bar.arrive_and_wait();       // Join\n      }});\n  }\n  for (auto&& t : threads) { \n    t.join();\n  }\n  std::cout << n_turns << '\\n';\n} \n```\n\nlambda `check_result()`是每次所有线程到达屏障时都会调用的完成函数。完成功能检查每个骰子的值，并确定是否应该玩新一轮或我们是否完成。\n\n传递给`std::thread`对象的 lambda 通过值捕获索引`i`，这样所有线程都有一个唯一的索引。其他变量`done`、`dice`和`bar`通过引用获取。\n\n还要注意我们如何变异和读取不同线程通过引用捕获的变量，而不会引入任何数据竞争，这要归功于屏障执行的协调。\n\n### 使用信号量的信令和资源计数\n\n字**旗语**的意思是可以用来发信号的东西，比如旗帜或者灯。在接下来的例子中，你将看到我们如何使用信号量为发信号通知其他线程可以等待的不同状态。\n\n信号量也可以用来控制对资源的访问，类似于`std::mutex`如何限制对关键部分的访问:\n\n```cpp\nclass Server {\npublic:\n  void handle(const Request& req) {\n    sem_.acquire();\n    // Restricted section begins here.\n    // Handle at most 4 requests concurrently.\n    do_handle(req);\n    sem_.release();\n  }\nprivate:\n  void do_handle(const Request& req) { /* ... */ }\n  std::counting_semaphore<4> sem_{4};\n}; \n```\n\n在这种情况下，信号量用`4`的值初始化，这意味着最多可以同时处理四个并发请求。多个线程可以访问同一个部分，而不是互斥地访问代码中的某个部分，但是对该部分中当前线程的数量有限制。\n\n如果信号量大于零，成员函数`acquire()`递减信号量。否则`acquire()`阻塞，直到信号量允许其递减并进入受限部分。`release()`递增计数器而不阻塞。如果信号量在增加`release()`之前为零，等待的线程将被发出信号。\n\n除了`acquire()`功能，也可以尝试使用`try_acquire()`功能在不阻塞的情况下递减计数器*。如果成功递减计数器，则返回`true`，否则返回`false`。功能`try_acquire_for()`和`try_acquire_until()`可以类似的方式使用。但是当计数器已经为零时，他们不会立即返回`false`，而是在返回给调用者之前，在指定的时间内自动尝试递减计数器。*\n\n这三个函数遵循与标准库中其他类型相同的模式，例如，`std::timed_mutex`及其`try_lock()`、`try_lock_for()`和`try_lock_until()`成员函数。\n\n`std::counting_semaphore`是一个模板，其中一个模板参数接受信号量的最大值。将信号量增加(释放)到其最大值以上被认为是编程错误。\n\n最大大小为 1 的`std::counting_semaphore` 被称为**二进制信号量**。`<semaphore>` 报头包括二进制信号量的别名声明:\n\n```cpp\nstd::binary_semaphore = std::counting_semaphore<1>; \n```\n\n保证二进制信号量比具有更高最大值的计数信号量更有效地实现。\n\n信号量的另一个重要属性是释放信号量的线程可能不是获取它的线程。这与`std::mutex`形成对比，后者要求获取互斥体的线程也是必须释放互斥体的线程。然而，对于信号量，一种类型的任务做等待(获取)和另一种类型的任务做信号(释放)是很常见的。这将在我们的下一个示例中演示。\n\n#### 示例:使用信号量的有界缓冲区\n\n下面的示例演示了一个有界缓冲区。这是一个固定大小的缓冲区，可以让多个线程从中读写。同样，这个例子演示了您已经看到的使用条件变量的生产者-消费者模式。生产者线程是写入缓冲区的线程，而读者线程是从缓冲区读取(和弹出元素)的线程。\n\n下图显示了缓冲区(固定大小的数组)和跟踪读写位置的两个变量:\n\n<figure class=\"mediaobject\">![](img/B15619_11_11.png)</figure>\n\n图 11.11:有界缓冲区有一个固定的大小\n\n我们将一步一步来，从关注有界缓冲区内部逻辑的版本开始。使用信号量的信令将在下一个版本中添加。这里，最初的尝试演示了如何使用读取和写入位置:\n\n```cpp\ntemplate <class T, int N> \nclass BoundedBuffer {\n  std::array<T, N> buf_;\n  std::size_t read_pos_{};\n  std::size_t write_pos_{};\n  std::mutex m_;\n  void do_push(auto&& item) {\n    /* Missing: Should block if buffer is full */\n    auto lock = std::unique_lock{m_};\n    buf_[write_pos_] = std::forward<decltype(item)>(item);\n    write_pos_ = (write_pos_ + 1) % N;\n  }\npublic:\n  void push(const T& item) { do_push(item); }\n  void push(T&& item) { do_push(std::move(item)); }\n  auto pop() {\n    /* Missing: Should block if buffer is empty */\n    auto item = std::optional<T>{};\n    {\n      auto lock = std::unique_lock{m_};\n      item = std::move(buf_[read_pos_]);\n      read_pos_ = (read_pos_ + 1) % N;\n    }\n    return std::move(*item);\n  }\n}; \n```\n\n第一次尝试包含固定大小的缓冲区、读写位置和一个互斥体，用于保护数据成员免受数据竞争的影响。这个实现应该能够让任意数量的线程同时调用`push()`和`pop()`。\n\n`push()`功能在`const T&`和`T&&`上过载。这是标准库容器使用的优化技术。`T&&`版本避免了调用者传递右值时复制参数。\n\n为了避免重复推送操作的逻辑，一个辅助函数`do_push()`包含了实际的逻辑。通过使用转发引用(`auto&& item`)和`std::forward`，`item`参数将被移动分配或复制分配，这取决于客户端是使用右值还是左值调用`push()`。\n\n然而，这个版本的有界缓冲区并不完整，因为它不能保护我们不使`write_pos`点位于(或超出)`read_pos`。同样地，`read_pos`绝不能指向`write_pos`(或更远)。我们想要的是一个缓冲区，生产者线程在缓冲区满的时候阻塞，消费者线程在缓冲区空的时候阻塞。\n\n这是一个使用计数信号量的完美应用。信号量*阻塞*当信号量已经为零时试图减少信号量的线程。每当值为零的信号量递增时，信号量*就向被阻塞的线程发出信号*。\n\n对于有界缓冲区，我们需要两个信号量:\n\n*   第一个信号量`n_empty_slots`记录缓冲区中空槽的数量。它将以缓冲区大小的值开始。\n*   第二个信号量`n_full_slots`记录缓冲区中满槽的数量。\n\n确保你理解为什么需要两个计数信号量(而不是一个)。原因是有两种截然不同的*状态*需要发出信号:当缓冲器*满时*和当缓冲器*空时*。\n\n在使用两个计数信号量添加信号处理之后，有界缓冲区现在看起来像这样(在这个版本中添加的行用“new”标记):\n\n```cpp\ntemplate <class T, int N> \nclass BoundedBuffer {\n  std::array<T, N> buf_;\n  std::size_t read_pos_{};\n  std::size_t write_pos_{};\n  std::mutex m_;\n  std::counting_semaphore<N> n_empty_slots_{N}; // New\n  std::counting_semaphore<N> n_full_slots_{0};  // New\n  void do_push(auto&& item) {\n    // Take one of the empty slots (might block)\n    n_empty_slots_.acquire();                   // New\n    try {\n      auto lock = std::unique_lock{m_};\n      buf_[write_pos_] = std::forward<decltype(item)>(item);\n      write_pos_ = (write_pos_ + 1) % N;\n    } catch (...) {\n      n_empty_slots_.release();                 // New\n      throw;\n    }\n    // Increment and signal that there is one more full slot\n    n_full_slots_.release();                    // New\n  }\npublic:\n  void push(const T& item) { do_push(item); }\n  void push(T&& item) { do_push(std::move(item)); }\n  auto pop() {\n    // Take one of the full slots (might block)\n    n_full_slots_.acquire();                // New\n    auto item = std::optional<T>{};\n    try {\n      auto lock = std::unique_lock{m_};\n      item = std::move(buf_[read_pos_]);\n      read_pos_ = (read_pos_ + 1) % N;\n    } catch (...) {\n      n_full_slots_.release();             // New\n      throw;\n    }\n    // Increment and signal that there is one more empty slot\n    n_empty_slots_.release();              // New\n    return std::move(*item);\n  }\n}; \n```\n\n这个版本支持多个生产者和消费者。两个信号量的使用保证了两个信号量都不会达到大于缓冲区中元素最大数量的值。例如，生产者线程不可能在没有首先检查至少有一个空槽的情况下添加一个值并增加`n_full_slots`信号量。\n\n还要注意的是`acquire()`和`release()`是从不同的线程调用的。例如，消费者线程在等待(`acquire()`)在`n_full_slots`信号量上，生产者线程在同一信号量上发信号(`release()`)。\n\n添加到 C++ 20 中的新同步原语是众所周知的结构，在线程库中很常见。与`std::mutex`和`std::condition_variable`相比，它们为同步访问共享资源提供了方便且通常更有效的选择。\n\n## C++ 中的原子支持\n\n标准库包含对**原子变量**的支持，有时称为**原子**。原子变量是一个变量，可以安全地使用并从多个线程中变异，而不会引入数据竞争。\n\n你还记得我们之前看到的数据竞争的例子吗，两个线程更新了一个全局计数器？我们通过添加互斥锁和计数器解决了这个问题。我们可以使用`std::atomic<int>`来代替使用显式锁:\n\n```cpp\nstd::atomic<int> counter; \n\nauto increment_counter(int n) { \n  for (int i = 0; i < n; ++ i) \n    ++ counter; // Safe, counter is now an atomic<int> \n} \n```\n\n`++ counter`是表示`counter.fetch_add(1)`的方便方式。可以在原子上调用的所有成员函数都可以安全地从多个线程并发调用。\n\n原子类型来自`<atomic>`头。在`std::atomic_int`表单上命名的所有标量数据类型都有类型定义。这和说`std::atomic<int>`一模一样。可以在`std::atomic`模板中包装一个自定义类型，只要该自定义类型是可复制的即可。基本上，这意味着一个类的对象完全由其数据成员的位来描述。这样，通过仅复制原始字节，可以用例如`std::memcpy()`复制对象。因此，如果一个类包含虚函数、指向动态内存的指针等等，就不可能仅仅复制对象的原始位并期望它工作，因此它是不可复制的。这可以在编译时检查，因此如果您试图创建一个不容易复制的类型的原子，您将会得到编译错误:\n\n```cpp\nstruct Point { \n  int x_{}; \n  int y_{}; \n}; \n\nauto p = std::atomic<Point>{};       // OK: Point is trivially copyable \nauto s = std::atomic<std::string>{}; // Error: Not trivially copyable \n```\n\n也可以创建原子指针。这使得指针本身是原子的，而不是它所指向的对象。稍后我们将更多地讨论原子指针和引用。\n\n### 无锁属性\n\n使用原子而不是用互斥来保护对变量的访问的一个原因是为了避免使用`std::mutex`带来的性能开销。此外，互斥体可以在不确定的持续时间内阻塞线程并引入优先级反转(参见*线程优先级*一节)这一事实排除了在低延迟上下文中的互斥体。换句话说，您的代码中可能有些部分有延迟要求，完全禁止使用互斥锁。在这种情况下，知道原子变量是否使用互斥是很重要的。\n\n原子变量可以使用也可以不使用锁来保护数据；这取决于变量的类型和平台。如果原子不用锁，就说是**无锁**。如果变量是无锁的，您可以在运行时查询它:\n\n```cpp\nauto variable = std::atomic<int>{1};\nassert(variable.is_lock_free());          // Runtime assert \n```\n\n这很好，因为现在我们至少在运行程序时断言使用`variable`对象是无锁的。通常，所有相同类型的原子对象要么是无锁的，要么是不锁的，但是在一些奇异的平台上，两个原子对象可能会产生不同的答案。\n\n一般来说，更有趣的是知道原子类型(`std::atomic<T>`)在某个平台上是否保证是无锁的，最好我们希望在编译时而不是运行时知道这一点。从 C++ 17 开始，也可以通过使用`is_always_lock_free()`来验证原子专门化在编译时是无锁的，如下所示:\n\n```cpp\nstatic_assert(std::atomic<int>::is_always_lock_free); \n```\n\n如果`atomic<int>`在我们瞄准的平台上不是无锁的，这个代码会产生编译错误。现在，如果我们编译一个假设`std::atomic<int>`不使用锁的程序，它将无法编译，这正是我们想要的。\n\n在现代平台上，任何符合原生字长的`std::atomic<T>`通常都是*无锁的*。而在现代 x64 芯片上，你甚至可以得到两倍于此的金额。例如，在现代英特尔 CPU 上编译的 libc++ 上，`std::atomic<std::complex<double>>`总是无锁的。\n\n### 原子旗帜\n\n保证始终无锁定的原子类型是`std::atomic_flag`(与目标平台无关)。因此，`std::atomic_flag`没有为我们提供`is_always_lock_free()` / `is_lock_free()`功能，因为它们总是返回`true`。\n\n原子标志可以用来保护关键部分，作为使用`std::mutex`的替代。因为锁在概念上很容易理解，所以我将在这里用它作为一个例子。但是，应该注意的是，我在本书中演示的锁的实现不是生产就绪代码，而是概念实现。下面的示例演示了如何从概念上实现一个简单的自旋锁:\n\n```cpp\nclass SimpleMutex {       \n  std::atomic_flag is_locked_{};           // Cleared by default\npublic:\n  auto lock() noexcept {\n    while (is_locked_.test_and_set()) {\n      while (is_locked_.test());           // Spin here\n    }\n  } \n  auto unlock() noexcept {\n    is_locked_.clear();\n  }\n}; \n```\n\n`lock()`功能调用`test_and_set()`设置标志，同时获取标志的前一个值。如果`test_and_set()`返回`false`，则意味着呼叫者设法获得锁(在先前被清除时设置标志)。否则，内部`while`循环将在旋转循环中使用`test()`不断轮询标志的状态。我们在额外的内部循环中使用`test()`的原因是性能:`test()`不会使缓存行无效，而`test_and_set()`会。该锁定协议称为**测试和测试设置**。\n\n这种自旋锁可以工作，但对资源不是很友好；当线程正在执行时，它不断地使用 CPU 一遍又一遍地检查相同的条件。我们可以在每次迭代中添加一个指数回退的短暂睡眠，但是很难针对不同的平台和场景进行微调。\n\n幸运的是，C++ 20 给`std::atomic`增加了一个等待和通知 API，这使得线程可以等待(以资源友好的方式)一个原子变量来改变它的值。\n\n### 原子等待和通知\n\n由于 C++ 20，`std::atomic`和`std::atomic_flag`提供了等待和通知的功能。函数`wait()`阻塞当前线程，直到原子变量的值改变，其他线程通知等待的线程。线程可以通过调用`notify_one()`或`notify_all()`来通知发生了变化。\n\n有了这个新的功能，我们可以避免连续轮询原子的状态，而是以一种更加资源友好的方式等待，直到值发生变化；这类似于 a `std::condition_variable`如何让我们等待并通知状态变化。\n\n通过使用等待和通知，上一节实现的`SimpleMutex`可以这样改写:\n\n```cpp\nclass SimpleMutex {       \n  std::atomic_flag is_locked_{}; \npublic:\n  auto lock() noexcept {\n    while (is_locked_.test_and_set())\n      is_locked_.wait(true);    // Don't spin, wait\n  } \n  auto unlock() noexcept {\n    is_locked_.clear();\n    is_locked_.notify_one();   // Notify blocked thread\n  }\n}; \n```\n\n我们将旧值(`true`)传递给`wait()`。等到`wait()`返回时，原子变量肯定已经改变，不再是`true`。但是，不能保证我们将捕捉到*所有*变量的变化。该变量可能已从状态 A 更改为状态 B，然后返回到状态 A，而没有通知等待的线程。这是无锁编程中的一种现象，称为 **ABA 问题**。\n\n这个例子演示了使用`std::atomic_flag`的等待和通知功能。在`std::atomic`类模板上也有相同的等待和通知 API。\n\n请注意，本章中介绍的自旋锁不是生产就绪代码。实现高效的锁通常需要正确使用内存排序(稍后讨论)和不可移植的代码，这超出了本书的范围。详细讨论可在[https://Timur . audio/使用-锁定-实时-音频-安全处理](https://timur.audio/using-locks-in-real-time-audio-processing-safely)中找到。\n\n现在，我们将继续讨论原子指针和原子引用。\n\n### 在多线程环境中使用 shared_ptr\n\n那`std::shared_ptr`呢？能否在多线程环境下使用，当多个线程正在访问一个被多个共享指针引用的对象时，引用计数是如何处理的？\n\n为了理解共享指针和线程安全，我们需要回忆一下`std::shared_ptr`通常是如何实现的(另请参见*第 7 章*、*内存管理*)。考虑以下代码:\n\n```cpp\n// Thread 1 \nauto p1 = std::make_shared<int>(42); \n```\n\n代码在堆上创建一个`int`和一个指向`int`对象的引用计数智能指针。使用`std::make_shared()`创建共享指针时，会在`int`旁边创建一个`control block`。控制块包含一个参考计数变量，每当创建指向`int`的新指针时，该变量递增，每当指向`int`的指针被破坏时，该变量递减。总而言之，当执行前面的代码行时，会创建三个独立的实体:\n\n*   实际的`std::shared_ptr`对象`p1`(栈上的局部变量)\n*   控制块(堆对象)\n*   一个`int`(堆对象)\n\n下图显示了三个对象:\n\n<figure class=\"mediaobject\">![](img/B15619_11_12.png)</figure>\n\n图 11.12:一个 shared_ptr 实例 p1，指向整数对象和一个包含引用计数的控制块。在这种情况下，只有一个使用 int 的共享指针，因此 ref 计数为 1。\n\n现在，考虑一下如果下面的代码由第二个线程执行会发生什么:\n\n```cpp\n// Thread 2 \nauto p2 = p1; \n```\n\n我们正在创建一个指向`int`(和控制块)的新指针。当创建`p2`指针时，我们读取`p1`，但是在更新引用计数器时，我们也需要变异控制块。控制块位于堆中，由两个线程共享，因此需要同步以避免数据竞争。由于控制块是隐藏在`std::shared_ptr`接口后面的实现细节，我们没有办法知道如何保护它，事实证明它已经被实现处理了。\n\n通常，它会使用可变的原子计数器。换句话说，引用计数器更新是线程安全的，这样我们就可以使用来自不同线程的多个共享指针，而不用担心同步引用计数器。这是一个很好的实践，也是设计类时需要考虑的事情。如果从客户端的角度来看，您正在对看似语义只读(`const`)的方法中的变量进行变异，那么您应该使变异变量线程安全。另一方面，客户端可以检测到的所有变异函数都应该留给类的客户端来同步。\n\n下图显示了可以访问同一对象的两个`std::shared_ptrs`、`p1`和`p2`。`int`是共享对象，控制块是`std::shared_ptr`实例之间的内部共享对象。默认情况下，控制块是线程安全的:\n\n<figure class=\"mediaobject\">![](img/B15619_11_13.png)</figure>\n\n图 11.13:两个共享 ptr 访问同一个对象\n\n总结一下:\n\n*   共享对象，本例中的`int`不是线程安全的，如果从多个线程访问，需要显式锁定。\n*   控制块已经是线程安全的，所以引用计数机制在多线程环境中工作。\n\n让我们继续保护`shared_ptr`实例。\n\n#### 保护 shared_ptr 实例\n\n现在只剩下一部分了:上例中实际的`std::shared_ptr`物体、`p1`和`p2`呢？为了理解这一点，让我们来看一个只使用一个名为`p`的全局`std::shared_ptr`对象的例子:\n\n```cpp\n// Global, how to protect? \nauto p = std::shared_ptr<int>{}; \n```\n\n如何在不引入数据竞争的情况下，从多线程中变异`p`？一种选择是每当我们使用`p`时，用显式互斥来保护`p`。或者，我们可以为`std::shared_ptr`使用模板专门化`std::atomic`(在 C++ 20 中引入)。换句话说，可以像这样将`p`声明为原子共享指针:\n\n```cpp\n// Global, protect using atomic\nauto p = std::atomic<std::shared_ptr<int>>{}; \n```\n\n此模板专门化可能是也可能不是无锁的。你可以用`is_lock_free()`成员函数来验证。另一件需要注意的事情是，专门化`std::atomic<std::shared_ptr<T>>`是规则`std::atomic`的一个例外，该规则只能专门化那些很容易复制的类型。无论如何，我们很高兴终于在标准库中拥有了这种有用的类型。\n\n下面的示例演示如何从多个线程自动加载和存储共享指针对象:\n\n```cpp\n// Thread T1 calls this function\nauto f1() { \n  auto new_p = std::make_shared<int>(std::rand());  // ... \n  p.store(new_p);\n} \n\n// Thread T2 calls this function\nauto f2() { \n  auto local_p = p.load(); \n  // Use local_p... \n} \n```\n\n在前面的例子中，我们假设有两个线程`T1`和`T2`，分别调用函数`f1()`和`f2()`。通过调用`std::make_shared<int>()`，从线程`T1`创建新的堆分配的`int`对象。\n\n这个例子中有一个微妙的细节需要考虑:堆分配的`int`在哪个线程中被删除了？当`local_p`超出`f2()`功能范围时，可能是对`int`的最后一次引用(引用计数为零)。在这种情况下，从线程`T2`中删除堆分配的`int`。否则，调用`std::atomic_store()`时会从线程`T1`中删除。所以，答案是`int`的删除可以从两个线程中进行。\n\n### 原子引用\n\n到目前为止，你已经看到`std::atomc_flag`和`std::atomic<>`有许多有用的专门化。`std::atomic`可以用`std::atomic<T*>`这样的指针来专门化，但是你还没有看到如何用引用类型来使用原子。写`std::atomic<T&>`是不可能的；相反，标准库为我们提供了一个名为`std::atomic_ref`的模板。\n\n模板`std::atomic_ref`是在 C++ 20 中引入的。它的接口与`std::atomic`相同，之所以有一个单独的名称是为了避免影响使用`std::atomic<T>`的现有通用代码的风险。\n\n原子引用允许我们对引用的非原子对象执行原子操作。当我们引用客户端提供的对象或一些不提供内部同步对象的第三方代码时，这可能很方便。我们将看一个例子来演示原子引用的有用性。\n\n#### 示例:使用原子引用\n\n假设我们正在编写一个将硬币翻转指定次数的函数:\n\n```cpp\nvoid flip_coin(std::size_t n, Stats& outcomes); \n```\n\n结果累积在类型为`Stats`的`outcomes`对象中，如下所示:\n\n```cpp\nstruct Stats {\n  int heads_{};\n  int tails_{};\n};\nstd::ostream& operator<<(std::ostream& os, const Stats &s) {\n  os << \"heads: \" << s.heads_ << \", tails: \" << s.tails_;\n  return os;\n} \n```\n\n客户端可以使用同一个`Stats`实例多次调用`flip_coins()`，翻转的结果被添加到`Stats`:\n\n```cpp\nauto outcomes = Stats{};\nflip_coin(30, outcomes); \nflip_coin(10, outcomes); \n```\n\n假设我们想并行化`flip_coin()`的实现，让多个线程变异`Stats`对象。在中，我们可以假设如下:\n\n*   `Stats`结构无法更改(可能来自第三方库)。\n*   我们希望客户端不知道我们的效用函数`flip_coin()`是并发的；也就是说，`flip_coin()`函数的并发应该对调用者完全*透明。*\n\n在本例中，我们将重用之前定义的函数来生成随机数:\n\n```cpp\nint random_int(int min, int max); // See implementation above \n```\n\n现在我们准备定义我们的`flip_coin()`函数，它将使用两个线程来翻转硬币`n`的次数:\n\n```cpp\nvoid flip_coin(std::size_t n, Stats &outcomes) {\n  auto flip = [&outcomes](auto n) {\n    auto heads = std::atomic_ref<int>{outcomes.heads_};\n    auto tails = std::atomic_ref<int>{outcomes.tails_};\n    for (auto i = 0u; i < n; ++ i) {\n      random_int(0, 1) == 0 ? ++ heads : ++ tails;\n    }\n  };\n  auto t1 = std::jthread{flip, n / 2};       // First half\n  auto t2 = std::jthread{flip, n - (n / 2)}; // The rest\n} \n```\n\n无论何时抛出硬币，这两个线程都会更新非原子结果对象。我们将创建两个`std::atomic_ref<int>`变量来自动更新结果对象的成员，而不是使用`std::mutex`。重要的是要记住，为了保护正面和反面计数器免受数据竞争的影响，所有对计数器的并发访问都需要使用`std::atomic_ref`来保护。\n\n下面的小程序演示了`flip_coin()`函数可以在不知道`flip_coin()`的并发实现的情况下调用:\n\n```cpp\nint main() {\n  auto stats = Stats{};\n  flip_coin(5000, stats);       // Flip 5000 times\n  std::cout << stats << '\\n';\n  assert((stats.tails_ + stats.heads_) == 5000);\n} \n```\n\n在我的机器上运行该程序会产生以下输出:\n\n```cpp\nheads: 2592, tails: 2408 \n```\n\n这个例子结束了我们关于 C++ 中各种原子类模板的部分。自 C++ 11 以来，原子一直是标准库的一部分，并且还在继续发展。C++ 20 引入了:\n\n*   专业化`std::atomic<std::shared_ptr<T>>`\n*   原子引用；也就是`std::atomic_ref<T>`模板\n*   等待和通知 API，它是使用条件变量的轻量级替代\n\n我们现在将继续讨论 C++ 内存模型，以及它与原子和并发编程的关系。\n\n## C++ 内存模型\n\n为什么我们要在关于并发的一章中讨论 C++ 的内存模型？内存模型与并发性密切相关，因为它定义了对内存的读写应该如何在线程间可见。这是一个相当复杂的主题，涉及到编译器优化和多核计算机体系结构。不过，好消息是，如果您的程序没有数据竞争，并且您使用了 atomics 库默认提供的内存顺序，那么您的并发程序将根据易于理解的直观内存模型运行。尽管如此，重要的是至少要了解什么是内存模型以及默认内存顺序保证了什么。\n\n赫伯·萨特在他的演讲*原子武器:C++ 内存模型和现代硬件 1 & 2* 中彻底解释了本部分所涵盖的概念。这些讲座可在[https://herbsutter . com/2013/02/11/原子武器-c-memory-model-and-model-hardware/](https://herbsutter.com/2013/02/11/atomic-weapons-the-c-memory-model-and-modern-hardware/)免费获得，如果您需要更多这方面的深度，强烈推荐您参加。\n\n### 指令重新排序\n\n为了理解内存模型的重要性，您首先需要一些关于我们编写的程序是如何实际执行的背景知识。\n\n当我们编写并运行一个程序时，可以合理地假设源代码中的指令将按照它们在源代码中出现的相同顺序执行。这不是真的。我们编写的代码将在最终执行之前分多个阶段进行优化。编译器和硬件都会对指令重新排序，目的是更有效地执行程序。这不是新技术:编译器已经这样做了很长时间，这也是优化构建比非优化构建运行得更快的原因之一。只要在运行程序时无法观察到指令的重新排序，编译器(和硬件)就可以自由地对指令进行重新排序。程序运行*就好像*一切都按照程序顺序发生。\n\n让我们看一个示例代码片段:\n\n```cpp\nint a = 10;      // 1 \nstd::cout << a;  // 2 \nint b = a;       // 3 \nstd::cout << b;  // 4 \n// Observed output: 1010 \n```\n\n这里，很明显，第二行和第三行可以互换，而不会引入任何可观察到的效果:\n\n```cpp\nint a = 10;      // 1 \nint b = a;       // 3 This line moved up  \nstd::cout << a;  // 2 This line moved down \nstd::cout << b;  // 4 \n// Observed output: 1010 \n```\n\n下面是另一个例子，它与*第 4 章*、*数据结构*中的例子相似，但不完全相同，其中编译器可以在迭代二维矩阵时优化缓存不友好的版本:\n\n```cpp\nconstexpr auto ksize = size_t{100}; \nusing MatrixType = std::array<std::array<int, ksize>, ksize>; \n\nauto cache_thrashing(MatrixType& matrix, int v) { // 1 \n  for (size_t i = 0; i < ksize; ++ i)              // 2 \n    for (size_t j = 0; j < ksize; ++ j)            // 3 \n      matrix[j][i] = v;                           // 4 \n} \n```\n\n您在*第 4 章*、*数据结构*中看到，类似这样的代码会产生大量的缓存未命中，从而影响性能。编译器可以通过重新排序`for`语句来优化这一点，如下所示:\n\n```cpp\nauto cache_thrashing(MatrixType& matrix, int v) { // 1 \n  for (size_t j = 0; j < ksize; ++ j)              // 3 Line moved up \n    for (size_t i = 0; i < ksize; ++ i)            // 2 Line moved down \n      matrix[j][i] = v;                           // 4  \n} \n```\n\n在执行程序时没有办法观察两个版本的区别，但是后者会跑得更快。\n\n编译器和硬件执行的优化(包括指令流水线、分支预测和缓存层次结构)是非常复杂且不断发展的技术。幸运的是，原始程序的所有这些转换都可以看作是源代码中读写的重新排序。这也意味着执行转换的是编译器还是硬件的某个部分并不重要。C++ 程序员需要知道的重要一点是，指令可以被重新排序，但没有任何可观察到的效果。\n\n如果您一直在尝试调试程序的优化版本，您可能已经注意到，由于重新排序，很难完成它。因此，通过使用调试器，重新排序在某种意义上是可观察的，但是当以正常方式运行程序时，它们是不可观察的。\n\n### 原子和内存顺序\n\n当用 C++ 编写单线程程序时，没有发生数据竞争的风险。我们可以愉快地编写程序，而不用担心指令的重新排序。然而，当谈到多线程程序中的共享变量时，情况就完全不同了。编译器(和硬件)只根据*一个*线程的真实和可观察到的情况进行所有优化。编译器无法知道其他线程能够通过共享变量观察到什么，所以作为程序员，我们的工作就是通知编译器允许什么样的重新排序。事实上，当我们使用原子变量或互斥来保护我们免受数据竞争时，这正是我们正在做的。\n\n当用互斥体保护临界区时，保证只有当前拥有锁的线程才能执行临界区。但是，互斥体也在关键部分周围创建内存栅栏，以通知系统在关键部分边界不允许某些重新排序。获取锁时增加`acquire`栅栏，解除锁时增加`release`栅栏。\n\n我将用一个例子来证明这一点。假设我们有四个指令: **i1** 、 **i2** 、 **i3** 和 **i4** 。每个指令之间没有依赖性，因此系统可以任意地对指令重新排序，而没有任何可观察到的影响。指令 i2 和 i3 使用共享数据，因此是需要互斥保护的关键部分。添加互斥锁的`acquire`和`release`后，现在有一些重新排序不再有效。显然，我们不能将属于关键部分的指令移到关键部分之外，否则它们将不再受到互斥体的保护。单向栅栏确保没有指令可以从关键部分移出。i1 指令可以通过获取栅栏在临界区内移动，但不能超出释放栅栏。i4 指令也可以通过释放栅栏在临界区内移动，但不能超出获取栅栏。\n\n下图显示了单向栅栏如何限制指令的重新排序。任何读或写指令都不能越过获取栅栏，任何东西都不能越过释放栅栏:\n\n<figure class=\"mediaobject\">![](img/B15619_11_14.png)</figure>\n\n图 11.14:单向栅栏限制了指令的重新排序\n\n当获取互斥体时，我们正在创建一个获取内存栅栏。它告诉系统，任何内存访问(读或写)都不能移动到获取栏所在的行之上。系统有可能将 i4 指令移至释放栏之上，超过 i3 和 i2 指令，但由于存在获取栏，因此不会超出此范围。\n\n现在，让我们来看看原子变量而不是互斥体。当我们在程序中使用共享原子变量时，它会给我们两件事:\n\n*   **防止撕裂写入**:原子变量总是自动更新的，因此读取器无法读取部分写入的值。\n*   **通过添加足够的内存栅栏来同步内存**:这防止了某些指令重新排序，以保证原子操作指定的特定内存顺序。\n\n如果我们的程序没有数据竞争，并且我们在使用 atomics 时使用默认内存顺序，那么 C++ 内存模型保证了**顺序一致性**。那么，什么是顺序一致性呢？顺序一致性保证执行的结果与按照原程序指定的顺序执行的操作相同。线程之间的指令交错是任意的；也就是说，我们无法控制线程的调度。起初这听起来可能很复杂，但这可能是您已经想到的如何执行并发程序的方式。\n\n顺序一致性的缺点是会损害性能。因此，可以用原子来代替松散的内存模型。这意味着您只能获得防止写入被破坏的保护，而不能获得由顺序一致性提供的内存顺序保证。\n\n我强烈建议您不要使用除默认顺序一致性内存顺序之外的任何东西，除非您对较弱的内存模型可能带来的影响有非常透彻的了解。\n\n我们在这里不再进一步讨论放松记忆顺序，因为它超出了本书的范围。但是作为附带说明，您可能有兴趣知道`std::shared_ptr`中的参考计数器在递增计数器时(但在递减计数器时)使用了一个宽松的模型。这就是为什么`std::shared_ptr`成员函数`use_count()`在多线程环境中使用时只报告实际引用的大概数量。\n\n内存模型和原子高度相关的一个领域是无锁编程。下一节将让你了解什么是无锁编程以及它的一些应用。\n\n# 无锁编程\n\n无锁编程很难。在本书中，我们不会花很多时间讨论无锁编程，但是相反，我将为您提供一个如何实现非常简单的无锁数据结构的示例。在网上和书中(比如前面提到的安东尼·威廉姆斯的书)，有大量的资源致力于无锁编程，这些资源将解释在编写自己的无锁数据结构之前需要理解的概念。一些你可能听说过的概念，比如**比较互换** ( **CAS** )和 ABA 问题，本书就不做进一步讨论了。\n\n## 示例:无锁队列\n\n在这里，您将在看到一个无锁队列的例子，这是一个相对简单但有用的无锁数据结构。无锁队列可以用于与不能使用锁同步访问共享数据的线程进行单向通信。\n\n它的实现很简单，因为需求有限:它只支持*一个读取器*线程和*一个写入器*线程。队列的容量也是固定的，在运行时不能改变。\n\n无锁队列是组件的一个例子，可以在异常通常被放弃的环境中使用。因此，接下来的队列设计没有例外，这使得该应用编程接口不同于本书中的其他示例。\n\n类模板`LockFreeQueue<T>`有如下公共界面:\n\n*   `push()`:向队列中添加一个元素，成功后返回`true`。该函数只能由(唯一的)*编写线程*调用。为了避免客户端提供右值时不必要的复制，`push()`在`const T&`和`T&&.`上重载。这种技术也在本章前面介绍的`BoundedBuffer`类中使用。\n*   `pop()`:返回一个带有队列前元素的`std::optional<T>`，除非队列为空。该函数只能由(唯一的)*读者线程*调用。\n*   `size()`:返回队列的当前大小。这个函数可以由*两个线程*同时调用。\n\n以下是队列的完整实现:\n\n```cpp\ntemplate <class T, size_t N>\nclass LockFreeQueue {\n  std::array<T, N> buffer_{};   // Used by both threads\n  std::atomic<size_t> size_{0}; // Used by both threads\n  size_t read_pos_{0};          // Used by reader thread\n  size_t write_pos_{0};         // Used by writer thread\n  static_assert(std::atomic<size_t>::is_always_lock_free);\n  bool do_push(auto&& t) {      // Helper function\n    if (size_.load() == N) { \n      return false; \n    }\n    buffer_[write_pos_] = std::forward<decltype(t)>(t);\n    write_pos_ = (write_pos_ + 1) % N;\n    size_.fetch_add(1);\n    return true;\n  }\npublic:\n  // Writer thread\n  bool push(T&& t) { return do_push(std::move(t)); }\n  bool push(const T& t) { return do_push(t); }\n  // Reader thread\n  auto pop() -> std::optional<T> {\n    auto val = std::optional<T>{};    \n    if (size_.load() > 0) {\n      val = std::move(buffer_[read_pos_]);\n      read_pos_ = (read_pos_ + 1) % N;\n      size_.fetch_sub(1);\n    }\n    return val;\n  }\n  // Both threads can call size()\n  auto size() const noexcept { return size_.load(); }\n}; \n```\n\n唯一需要原子访问的数据成员是`size_`变量。`read_pos_` 成员仅由读者线程使用，`write_pos_`仅由作者线程使用。那么那`std::array`型的缓冲器呢？它是可变的，可以被两个线程访问？这不需要同步吗？由于算法确保两个线程永远不会同时访问数组中的同一个元素，所以 C++ 保证数组中的单个元素可以在没有数据竞争的情况下被访问。元素有多小并不重要；即使是`char`阵也持有这种保证。\n\n像这样的非阻塞队列什么时候有用？一个例子是在音频编程中，当有一个 UI 运行在主线程上，需要从实时音频线程发送或接收数据，在任何情况下都不能阻塞。实时线程不能使用互斥锁、分配/释放内存，或者做任何其他可能导致线程等待优先级较低的线程的事情。像这样的场景需要无锁数据结构。\n\n读取器和写入器在`LockFreeQueue`中都是无锁的，因此我们可以有两个队列实例在主线程和音频线程之间双向通信，如下图所示:\n\n<figure class=\"mediaobject\">![](img/B15619_11_15.png)</figure>\n\n图 11.15:使用两个无锁队列在主线程和实时音频线程之间传递状态\n\n正如已经提到的，这本书只是触及了无锁编程的表面。现在是时候用一些关于编写并发程序时性能的指南来结束这一章了。\n\n# 绩效指南\n\n在试图提高性能之前，让一个并发程序正确运行的重要性怎么强调都不为过。此外，在应用这些与绩效相关的指导方针之前，您首先需要建立一种可靠的方法来衡量您正在努力改进的方面。\n\n## 避免争论\n\n每当多个线程使用共享数据时，就会发生争用。争用会损害性能有时争用导致的开销会使并行算法比单线程算法工作得更慢。\n\n使用导致等待和上下文切换的锁是一个明显的性能损失，但是同样不明显的是，锁和原子都禁用编译器生成的代码中的优化，并且它们在运行时当 CPU 执行代码时这样做。为了保证顺序一致性，这是必要的。但是请记住，解决此类问题的方法是永远不要忽略同步，从而引入数据竞争。数据竞赛意味着未定义的行为，拥有一个快速但不正确的程序不会让任何人高兴。\n\n相反，我们需要尽量减少花在关键部分的时间。我们可以通过减少进入关键部分的次数，并通过最小化关键部分本身来做到这一点，这样一旦我们进入其中，我们就可以尽快离开它。\n\n## 避免阻塞操作\n\n要编写一个总是流畅运行的现代响应 UI 应用，绝对有必要永远不要阻塞主线程超过几毫秒。一个流畅运行的应用每秒更新其界面 60 次。这意味着，如果你正在做的事情阻塞用户界面线程超过 16 毫秒，FPS 将下降。\n\n考虑到这一点，您可以在应用中设计您的内部 API。每当您编写一个执行输入/输出或其他可能需要几毫秒以上时间的函数时，都需要将其实现为异步函数。这种模式在 iOS 和 Windows 中变得非常普遍，例如，所有网络 API 都变成了异步的。\n\n## 线程/中央处理器内核数量\n\n一台机器的中央处理器内核越多，运行的线程就越活跃。如果你成功地将一个顺序的受 CPU 限制的任务分割成一个并行版本，你可以通过让多个内核并行处理该任务来获得性能。\n\n从单线程算法到可以由两个线程运行的算法，在最好的情况下，性能可以翻倍。但是，在添加越来越多的线程之后，当没有更多的性能提升时，您最终会达到一个极限。添加超过该限制的线程实际上会降低性能，因为添加的线程越多，上下文切换造成的开销就越大。\n\nI/O 密集型任务，例如，将花费大量时间等待网络数据的网络爬虫，在达到 CPU 超额订阅的极限之前需要大量线程。等待输入/输出的线程很可能会从中央处理器中切换出来，为准备执行的其他线程腾出空间。对于受 CPU 限制的任务，使用比机器上的内核更多的线程通常没有意义。\n\n控制一个大程序中的线程总数可能很难。控制线程数量的一个好方法是使用可以调整大小以匹配当前硬件的线程池。\n\n在*第 14 章*、*并行算法*中，你会看到如何并行算法以及如何根据 CPU 内核数量调整并发量的例子。\n\n## 线程优先级\n\n线程的优先级会影响线程的调度方式。优先级高的线程比优先级低的线程更容易被调度。线程优先级对于降低任务延迟非常重要。\n\n操作系统提供的线程通常有优先级。目前没有办法用当前的 C++ 线程 API 设置线程的优先级。但是，通过使用`std::thread::native_handle`，您可以获得底层操作系统线程的句柄，并使用本机 API 来设置优先级。\n\n与线程优先级相关的一种现象会损害性能，应该避免，这种现象被称为**优先级反转**。当具有高优先级的线程等待获取当前由低优先级线程持有的锁时，就会发生。这种依赖关系伤害了高优先级线程，高优先级线程被阻塞，直到下一次低优先级线程被调度以释放锁。\n\n对于实时应用，这是一个大问题。实际上，这意味着您不能使用锁来保护任何需要被实时线程访问的共享资源。例如，产生实时音频的线程以最高可能的优先级运行，为了避免优先级反转，音频线程不可能调用任何可能阻塞并导致上下文切换的函数(包括`std::malloc()`)。\n\n## 线程关联性\n\n线程关联性使得调度程序可以提示哪些线程可以从共享相同的中央处理器缓存中受益。换句话说，这是对调度器的请求，如果可能的话，一些线程应该在特定的内核上执行，以最小化缓存未命中。\n\n为什么要在一个特定的内核上执行一个线程？答案还是缓存。运行在同一内存上的线程可以从运行在同一个内核上获益，因此可以利用热缓存。对于调度程序来说，这只是将线程分配给内核时要考虑的众多参数之一，因此这几乎不能保证，但同样，操作系统之间的行为也有很大不同。线程优先级，甚至所有内核的利用率(避免过热)，都是现代调度器需要考虑的需求之一。\n\n用当前的 c++ API 以可移植的方式设置线程关联性是不可能的，但是大多数平台都支持在线程上设置关联性掩码的某种方式。为了访问特定于平台的功能，您需要获得本机线程的句柄。下面的示例演示了如何在 Linux 上设置线程关联掩码:\n\n```cpp\n#include <pthreads> // Non-portable header \nauto set_affinity(const std::thread& t, int cpu) {\n  cpu_set_t cpuset;\n  CPU_ZERO(&cpuset);\n  CPU_SET(cpu, &cpuset);\n  pthread_t native_thread = t.native_handle(); \n  pthread_set_affinity(native_thread, sizeof(cpu_set_t), &cpuset); \n} \n```\n\n注意，这不是可移植的 C++，但是如果您正在进行性能关键的并发编程，很可能需要对线程进行一些不可移植的配置。\n\n## 虚假分享\n\n**虚假共享**，或者破坏性干扰，会显著降低性能。当两个线程使用一些数据(在逻辑上不在线程间共享)但碰巧位于同一高速缓存行时，就会出现这种情况。想象一下，如果这两个线程在不同的内核上执行，并不断更新驻留在共享缓存行上的变量，会发生什么。尽管线程之间没有真正的数据共享，但线程会使彼此的缓存行无效。\n\n当使用全局数据或线程间共享的动态分配数据时，很可能会出现错误共享。可能发生错误共享的一个例子是，当分配一个在线程间共享的数组，但每个线程只使用该数组的一个元素时。\n\n这个问题的解决方案是填充阵列中的每个元素，使得两个相邻的元素不能驻留在同一高速缓存行上。从 C++ 17 开始，就有了一种可移植的方法，将`<new>`中定义的`std::hardware_destructive_interference_size`常数与和`alignas`说明符结合使用。下面的示例演示如何创建防止错误共享的元素:\n\n```cpp\nstruct alignas(std::hardware_destructive_interference_size) Element {\n   int counter_{};\n}; \n\nauto elements = std::vector<Element>(num_threads); \n```\n\n向量中的元素现在保证驻留在单独的高速缓存行上。\n\n# 摘要\n\n在本章中，您已经看到了如何创建可以并发执行多个线程的程序。我们还介绍了如何通过锁保护关键部分或使用原子来避免数据竞争。您了解到 C++ 20 附带了一些有用的同步原语:锁存、屏障和信号量。然后我们研究了执行顺序和 C++ 内存模型，这在编写无锁程序时变得非常重要。您还发现不可变数据结构是线程安全的。本章最后给出了一些提高并发应用性能的指南。\n\n接下来的两章专门介绍一个全新的 C++ 20 特性，名为 coroutines，它允许我们以顺序的方式编写异步代码。"
  },
  {
    "path": "docs/cpp-hiperf/12.md",
    "content": "# 十二、协程和延迟生成器\n\n计算已经成为一个等待的世界，我们需要我们的编程语言的支持，以便能够表达*等待*。总的想法是暂停(暂时暂停)当前流程，并将执行移交给其他流程，只要它到达我们知道可能需要等待的点。这个*我们需要等待的东西*可能是网络请求、用户的点击、数据库操作，甚至是我们花费太长时间来阻止的内存访问。相反，我们在代码中说，我们将等待，继续一些其他流程，然后在准备好的时候再回来。花冠允许我们这样做。\n\n在这一章中，我们将主要关注添加到 C++ 20 中的协程。你将了解它们是什么，如何使用它们，以及它们的性能特征。但是我们也将花一些时间从更广泛的意义上来看 coroutines，因为这个概念在许多其他语言中都很明显。\n\nC++ 协程几乎没有标准库的支持。为 coroutines 添加标准库支持是 C++ 23 版本的一个高优先级特性。为了在日常代码中有效地使用协程，我们需要实现一些通用的抽象。这本书将向您展示如何实现这些抽象，以便学习 C++ 协程，而不是为您提供生产就绪的代码。\n\n了解存在的各种类型的协程、协程可以用来做什么以及是什么促使 C++ 增加新的语言特性来支持协程也很重要。\n\n这一章涵盖了很多内容。下一章也是关于协程的，但是重点是异步应用。总之，本章将指导您完成:\n\n*   关于协程的一般理论，包括堆栈式协程和无堆栈式协程的区别，以及它们是如何被编译器转换并在计算机上执行的。\n*   C++ 中的无堆栈协程介绍。将讨论和演示在 C++ 20 中使用`co_await`、`co_yield`和`co_return`对协程的新语言支持。\n*   使用 C++ 20 协程作为生成器所需的抽象。\n*   一些真实的例子显示了使用 coroutine 的可读性和简单性方面的好处，以及我们如何通过使用 coroutine 编写延迟评估的可组合组件。\n\n如果您已经使用过其他语言的 coroutines，那么在阅读本章的其余部分之前，您需要做好两件事的准备:\n\n*   有些内容对你来说可能是基本的。虽然关于 C++ 协程如何工作的细节远非微不足道，但是使用示例可能会让您觉得微不足道。\n*   我们将在本章中使用的一些术语(协程、生成器、任务等)可能与您当前对这些术语的看法不一致。\n\n另一方面，如果你对 coroutines 完全陌生，这一章的部分可能看起来很像魔法，需要一些时间来掌握。因此，我将首先向您展示一些使用 coroutines 时 C++ 代码的外观示例。\n\n# 几个激励人心的例子\n\nCoroutines 是类似于 lambda 表达式的特性之一，它提供了一种完全改变我们编写和思考 C++ 代码方式的方法。这个概念非常笼统，可以有许多不同的应用方式。为了让您体验 C++ 在使用 coroutines 时的样子，我们将在这里简单地看两个例子。\n\nyield-expression 可用于实现生成器，即延迟生成值序列的对象。在这个例子中，我们将使用关键字`co_yield`和`co_return`来控制流量:\n\n```cpp\nauto iota(int start) -> Generator<int> {\n  for (int i = start; i < std::numeric_limits<int>::max(); ++ i) {\n    co_yield i;\n  }\n}\nauto take_until(Generator<int>& gen, int value) -> Generator<int> {\n  for (auto v : gen) {\n    if (v == value) {\n      co_return;\n    }\n    co_yield v;\n  }\n}\nint main() {\n  auto i = iota(2);\n  auto t = take_until(i, 5);\n  for (auto v : t) {          // Pull values\n    std::cout << v << \", \";\n  }\n  return 0;\n}\n// Prints: 2, 3, 4 \n```\n\n在前面的例子中，`iota()`和`take_until()`是并列关系。`iota()`生成一个整数序列，`take_until()`产生值，直到找到指定的值。`Generator`模板是一个自定义类型，我将在本章稍后向您展示如何设计和实现。\n\n构建生成器是协程的一个常见用例，另一个是实现异步任务。下一个例子将演示我们如何使用操作符`co_await`来等待一些东西，而不阻塞当前正在执行的线程:\n\n```cpp\nauto tcp_echo_server() -> Task<> {\n  char data[1024];\n  for (;;) {\n    size_t n = co_await async_read(socket, buffer(data));\n    co_await async_write(socket, buffer(data, n));\n  }\n} \n```\n\n`co_await`不是阻塞，而是暂停执行，直到恢复执行，异步读写功能完成。这里给出的例子是不完整的，因为我们不知道什么是`Task`、`socket`、`buffer`，异步输入输出函数是什么。但是我们将在下一章中讨论异步任务。\n\n如果现在还不清楚这些例子是如何工作的，不要担心——我们将在本章的后面花很多时间深入研究细节。这里的例子给你一个提示，如果你以前从未遇到过，我们可以做什么。\n\n在深入研究 C++ 20 协程之前，我们需要讨论一些术语和共同的基础知识，以便更好地理解在 2020 年为 C++ 增加一个相当复杂的语言特性的设计和动机。\n\n# 协同抽象\n\n我们现在将后退一步，一般性地讨论协程，而不仅仅关注 C++ 20 中添加的协程。这将使你更好地理解为什么花冠是有用的，以及有哪些类型的花冠和它们有什么不同。如果您已经熟悉 stackful 和 stackless coroutines 以及它们是如何执行的，那么您可以跳过这一节，直接跳到下一节，C++ 中的*coroutine*。\n\n协同抽象已经存在了 60 多年，许多语言已经在它们的语法或标准库中采用了某种协同。这意味着在不同的语言和环境中，协同词可以表示稍微不同的东西。由于这是一本关于 C++ 的书，我将使用 C++ 标准中使用的术语。\n\n协程非常类似于子程序。在 C++ 中，我们没有任何明确称为子程序的东西；相反，我们编写函数(例如自由函数或成员函数)来创建子程序。我将交替使用术语**普通功能**和 T4 子程序。\n\n## 子程序和协程\n\n为了理解协程和子程序(普通函数)之间的区别，我们将在这里重点介绍子程序和协程的最基本属性，即如何启动、停止、暂停和恢复它们。当我们程序的其他部分调用子程序时，它就会启动。当子程序返回给调用者时，子程序停止:\n\n```cpp\nauto subroutine() {\n  // Sequence of statements ...\n\n  return;     // Stop and return control to caller\n}\nsubroutine(); // Call subroutine to start it\n// subroutine has finished \n```\n\n子程序的调用链是严格嵌套的。在下图中，子程序`f()`不能返回到`main()`，直到子程序`g()`返回:\n\n<figure class=\"mediaobject\">![](img/B15619_12_01.png)</figure>\n\n图 12.1:子程序调用和返回的链\n\n花冠也可以像子程序一样启动和停止，但也可以**暂停**(暂停)和**恢复**。如果你以前没有使用过 coroutines，这在一开始可能看起来很奇怪。暂停和恢复**的点称为暂停/恢复点**。一些暂停点是隐式的，而另一些以某种方式在代码中被显式标记。以下伪代码显示了使用`await`和`yield`标记的三个显式暂停/恢复点:\n\n```cpp\n// Pseudo code\nauto coroutine() {\n  value = 10;  \n  await something;        // Suspend/Resume point\n  // ...\n  yield value++ ;          // Suspend/Resume point\n  yield value++ ;          // Suspend/Resume point\n  // ...\n  return;\n}\nauto res = coroutine();    // Call\nres.resume();              // Resume \n```\n\n在 C++ 中，使用关键字`co_await`和`co_yield`标记显式暂停点。下图显示了如何从一个子例程调用(调用)一个协程，然后再从代码的不同部分恢复:\n\n<figure class=\"mediaobject\">![](img/B15619_12_02.png)</figure>\n\n图 12.2:对协程的调用可以暂停和恢复。coroutine 调用在被挂起时保持其内部状态。\n\n协程中局部变量的状态在协程暂停时被保留。这些州属于某个共同诉讼请求。也就是说，它们不像静态局部变量，后者在函数的所有调用中是全局共享的。\n\n总而言之，协程是也可以暂停和恢复的子程序。另一种看待它的方式是说子程序是不能暂停或恢复的协程的特化。\n\n从现在开始，我在区分*呼叫*和*恢复*、*暂停*和*返回*时会非常严格。它们的意思完全不同。调用协程会创建一个可以暂停和恢复的协程的新实例。从协同工作中返回会破坏协同工作实例，并且无法再继续。\n\n为了真正理解 coroutines 如何帮助我们编写高效的程序，您需要了解一些关于 C++ 中的函数通常如何转换为机器代码然后执行的低级细节。\n\n## 在中央处理器上执行子程序和协程\n\n在本书中，我们已经讨论了内存层次结构、缓存、虚拟内存、线程调度以及其他硬件和操作系统概念。但是我们还没有真正讨论过如何使用中央处理器寄存器和堆栈在中央处理器上执行指令。当比较子程序和不同风格的协程时，这些概念很重要。\n\n### 中央处理器寄存器、指令和堆栈\n\n本节将提供一个非常简化的 CPU 模型，用于理解上下文切换、函数调用，以及一些关于调用堆栈的更多细节。当我在这个上下文中说 CPU 时，我指的是一些类似于配备有多个通用寄存器的 x86 系列 CPU 的 CPU。\n\n一个程序包含一系列由中央处理器执行的指令。指令序列存储在计算机内存的某个地方。中央处理器在名为**程序计数器**的寄存器中记录当前执行指令的地址。这样，中央处理器就知道下一步要执行什么指令。\n\n中央处理器包含固定数量的寄存器。寄存器类似于具有预定义名称的变量，可以存储值或内存地址。寄存器是计算机上最快的数据存储设备，它离中央处理器最近。当中央处理器处理数据时，它使用寄存器。一些寄存器对中央处理器有特殊的意义，而其他寄存器可以被当前执行的程序更自由地使用。\n\n对中央处理器有特殊意义的两个非常重要的寄存器是:\n\n*   **程序计数器**(**PC**):存储当前执行指令的内存地址的寄存器。每当执行指令时，该值自动递增。有时也称为*指令指针*。\n*   **栈指针** ( **SP** ):存储当前使用的调用栈顶部的地址。分配和解除分配堆栈内存是改变存储在这个寄存器中的值的问题。\n\n<figure class=\"mediaobject\">![](img/B15619_12_03.png)</figure>\n\n图 12.3:带寄存器的中央处理器\n\n假设寄存器名为 **R0** 、 **R1** 、 **R2** 和 **R3** ，如上图所示。典型的算术指令可能是这样的:\n\n```cpp\nadd 73, R1   // Add 73 to the value stored in R1 \n```\n\n数据也可以在寄存器和存储器之间复制:\n\n```cpp\nmov SP, R2   // Copy the stack pointer address to R2\nmov R2, [R1] // Copy value of R2 to memory address stored in R1 \n```\n\n一组指令隐式引用调用堆栈。中央处理器通过堆栈指针知道调用堆栈的顶部在哪里。在堆栈上分配内存只是更新堆栈指针的问题。该值的增加或减少取决于堆栈是向更高还是更低的地址增长。\n\n以下指令使用堆栈:\n\n```cpp\npush R1     // Push value of R1 to the top of the stack \n```\n\n推送指令将寄存器中的值复制到内存中由堆栈指针*指向的位置，并且*递增(或递减)堆栈指针。\n\n我们还可以使用`pop`指令从堆栈中弹出值，该指令也读取并更新堆栈指针:\n\n```cpp\npop R2      // Pop value from the stack into R2 \n```\n\n每当执行一条指令时，中央处理器自动递增程序计数器。但是程序计数器也可以通过指令明确更新，例如`jump`指令:\n\n```cpp\njump R3     // Set the program counter to the address in R3 \n```\n\nCPU 可以在两种模式下运行:用户模式或内核模式。在用户模式和内核模式下运行时，中央处理器寄存器的使用方式不同。当中央处理器在用户模式下执行时，它以不能访问硬件的受限权限运行。操作系统提供在内核模式下运行的系统调用。一个像`std::puts()`这样的 C++ 库函数，将值打印到`stdout`中，因此必须进行系统调用来完成它的任务，迫使 CPU 在用户模式和内核模式之间切换。\n\n在用户和内核模式之间转换是昂贵的。为了理解为什么，让我们再次思考我们的示意性 CPU。中央处理器通过使用其寄存器有效地运行，因此避免了不必要地将值溢出到堆栈上。但是 CPU 是所有用户进程和操作系统之间的共享资源，每当我们需要在任务之间切换时(例如，进入内核模式时)，处理器的状态，包括它的所有寄存器，都需要保存在内存中，以便以后可以恢复。\n\n### 打电话回来\n\n现在您已经对 CPU 如何使用寄存器和堆栈有了基本的理解，我们可以讨论子程序调用了。调用子程序并从中返回时，会涉及到很多机制，我们可能会认为这是理所当然的。当我们的编译器将 C++ 函数转换为高度优化的机器代码时，他们做得非常好。\n\n下面的列表显示了调用、执行和从子程序返回时需要考虑的方面:\n\n*   调用和返回(在代码中的点之间跳转)。\n*   传递参数—参数可以通过寄存器或堆栈传递，或者两者都传递。\n*   在堆栈上为局部变量分配存储空间。\n*   返回值—从子程序返回的值需要存储在调用者可以找到的地方。通常，这是一个专用的中央处理器寄存器。\n*   在不干扰其他功能的情况下使用寄存器——子程序使用的寄存器需要恢复到调用子程序之前的状态。\n\n关于如何执行函数调用的确切细节由称为**调用约定**的东西来指定。它们为呼叫者/被呼叫者提供了一个协议，以就谁负责哪些部分达成一致。调用约定在 CPU 架构和编译器之间有所不同，并且是构成**应用二进制接口** ( **ABI** )的主要部分之一。\n\n当一个函数被调用时，该函数的**调用框架**(或激活框架)被创建。呼叫框包含:\n\n*   传递给函数的*参数*。\n*   函数的*局部变量*。\n*   我们打算使用的寄存器的*快照，因此需要在返回之前恢复。*\n*   一个*返回地址*，链接回内存中调用函数的地方。\n*   一个可选的*帧指针*，它指向呼叫者呼叫帧的顶部。当检查堆栈时，帧指针对调试器很有用。我们不会在本书中进一步讨论框架指针。\n\n由于子程序的严格嵌套性质，我们可以将子程序的调用帧保存在堆栈上，以非常高效地支持嵌套调用。存储在堆栈上的调用帧通常称为**堆栈帧**。\n\n下图显示了调用堆栈上的多个调用帧，并突出显示了单个调用帧的内容:\n\n<figure class=\"mediaobject\">![](img/B15619_12_04.png)</figure>\n\n图 12.4:具有多个调用框架的调用堆栈。右侧的呼叫帧是单个呼叫帧的放大版本。\n\n当一个子例程返回到它的调用者时，它使用返回地址知道跳转到哪里，恢复它已经变异的寄存器，并从堆栈中弹出(解除分配)整个调用帧。这样，堆栈和寄存器都恢复到调用子程序之前的状态。然而，有两个例外。首先，程序计数器(PC)在调用后已经移动到指令。其次，一个向调用者返回一个值的子例程通常将该值存储在一个专用寄存器中，调用者知道在哪里可以找到它。\n\n在理解如何通过暂时使用堆栈来执行子程序，然后在将控制返回给调用方之前恢复中央处理器寄存器之后，我们现在可以开始研究如何暂停和恢复协程。\n\n### 暂停和恢复\n\n考虑下面的伪代码，它定义了一个具有多个暂停/恢复点的协程:\n\n```cpp\n// Pseudo code\nauto coroutine() { \n  auto x = 0;\n  yield x++ ;       // Suspend\n  g();             // Call some other function\n  yield x++ ;       // Suspend\n  return;          // Return \n}\nauto co = coroutine(); // Call subroutine to start it\n// ...                 // Coroutine is suspended\nauto a = resume(co);   // Resume coroutine to get\nauto b = resume(co);   // next value \n```\n\n当`coroutine()`挂起时，我们不能再像子程序返回调用方时那样删除调用帧。为什么呢？因为我们需要保持变量`x`的当前值，并且还要记住*在协程中的*位置，我们应该在下次协程恢复时继续执行。这些信息被放入一个叫做**的坐标框架**中。协同帧包含恢复暂停协同所需的所有信息。不过，这也提出了几个新问题:\n\n*   花冠架存放在哪里？\n*   花冠框架有多大？\n*   当一个协程调用一个子程序时，它需要一个堆栈来管理嵌套的调用框架。如果我们试图从嵌套调用框架中恢复，会发生什么？那么当协同恢复时，我们需要恢复整个堆栈。\n*   从协程调用和返回的运行时开销是多少？\n*   暂停和恢复协同的运行时开销是多少？\n\n对这些问题的简短回答是，这取决于我们正在讨论的是哪种类型的验尸官:无堆叠或堆叠型验尸官。\n\nStackful 协程有一个单独的侧栈(类似于线程)，包含协程框架和嵌套的调用框架。这使得从嵌套调用帧挂起成为可能:\n\n<figure class=\"mediaobject\">![](img/B15619_12_05.png)</figure>\n\n图 12.5:对 stackful coroutine 的每一次调用都会创建一个带有唯一堆栈指针的独立侧堆栈\n\n#### 暂停和恢复无堆栈的协同工作\n\n无堆栈协程需要将协程框架存储在其他地方(通常在堆上)，然后使用当前执行线程的堆栈来存储嵌套的调用框架。\n\n但这并不是全部事实。调用方负责创建调用框架，保存返回地址(程序计数器的当前值)和堆栈上的参数。调用方不知道它正在调用一个将挂起和恢复的协程。因此，协程本身需要创建协程框架，并在调用时将参数和寄存器从调用框架复制到协程框架:\n\n<figure class=\"mediaobject\">![](img/B15619_12_06.png)</figure>\n\n图 12.6:一个无堆栈的协程有一个单独的协程框架(通常在堆上)，它包含恢复协程所需的状态\n\n当协程最初挂起时，协程的堆栈帧从堆栈中弹出，但协程帧继续存在。指向协同框架的内存地址(句柄/指针)被返回给调用者:\n\n<figure class=\"mediaobject\">![](img/B15619_12_07.png)</figure>\n\n图 12.7:悬挂的花冠。协同框架包含恢复协同所需的所有信息。\n\n为了恢复协同工作，调用方使用它之前接收到的句柄，并调用一个恢复函数，并将协同工作句柄作为参数传递。恢复功能使用存储在协同框架中的暂停/恢复点来继续执行协同。对 resume 函数的调用也是一个普通的函数调用，它将生成一个堆栈帧，如下图所示:\n\n<figure class=\"mediaobject\">![](img/B15619_12_08.png)</figure>\n\n图 12.8:恢复协同为恢复调用创建一个新的调用框架。resume 函数使用协同状态的句柄从正确的挂起点恢复。\n\n最后，当一个协程返回时，它通常被挂起并最终被解除分配。堆栈的状态如下图所示:\n\n<figure class=\"mediaobject\">![](img/B15619_12_09.png)</figure>\n\n图 12.9:协同框架在返回时被解除分配\n\n每个协同调用没有单独的侧栈的一个重要后果是，当一个无栈协同被挂起时，它不能在栈上留下任何嵌套的调用帧。请记住，当控制转移回调用方时，调用方的调用框架必须在堆栈的顶部。\n\n最后，还应该提到的是，在某些情况下，协同帧所需的内存可以在呼叫者的呼叫帧内分配*。我们将在查看 C++ 20 协程时更详细地讨论这一点。*\n\n## 无堆叠与堆叠式花冠\n\n如前一节所述，无栈协程使用当前运行线程的栈来处理嵌套函数调用。这样做的结果是，无堆栈协程永远不会从嵌套调用框架中挂起。\n\nStackful coroutines 有时被称为**fiber**，在编程语言 Go 中，它们被称为**goroutine**。Stackful coroutines 提醒我们线程，其中每个线程管理自己的堆栈。不过，堆栈式协同线程(或光纤)和操作系统线程之间有两大区别:\n\n*   操作系统线程由内核调度，在两个线程之间切换是内核模式操作。\n*   大多数操作系统会先发制人地切换操作系统线程**(线程被调度程序中断)，而两个光纤之间的切换会协同发生**。正在运行的光纤会一直运行，直到它将控制权移交给某个管理器，然后该管理器可以调度另一个光纤。****\n\n ****还有一类线程叫做**用户级线程**或者**绿色线程**。这些是轻量级线程，不涉及内核模式切换(因为它们在用户模式下运行，因此内核不知道)。纤维是用户级线程的一个例子。但是用户级线程也有可能被用户库或虚拟机抢先调度。Java 线程是抢占式用户级线程的一个例子。\n\n无堆栈协程还允许我们编写和组合多个并发运行的任务，但不需要每个流都有一个独立的侧堆栈。无堆栈协程和状态机紧密相关。有可能将状态机转换成协程，反之亦然。为什么知道这个有用？首先，它让你更好地理解了什么是无堆栈协同。其次，如果您已经擅长识别可以使用状态机解决的问题，那么您可以更容易地看到协程作为一个合适的解决方案可能适合哪里。状态机是非常一般的抽象，可以应用于各种各样的问题。然而，状态机通常应用的一些领域是解析、手势识别和输入/输出复用，仅举几例。这些都是无堆叠协同办公在表现力和性能方面真正大放异彩的领域。\n\n### 绩效成本\n\nCoroutines 是一个抽象，它允许我们以清晰简洁的方式编写延迟评估代码和异步程序。但是，创建和销毁协同工作以及暂停和恢复协同工作会产生性能成本。在比较无堆栈和堆栈式协同的性能成本时，需要解决两个主要方面:*内存占用*和*上下文切换*。\n\n### 内存占用\n\n堆栈式协程需要一个单独的调用堆栈来处理嵌套调用框架中的挂起。因此，当调用协程时，我们需要为这个新的侧栈动态分配一大块内存。这立即引发了一个问题:我们需要分配多大的堆栈？除非我们有一些关于协程及其嵌套调用框架可以消耗多少堆栈的策略，否则我们可能需要一个与普通线程调用堆栈大小大致相同的堆栈。\n\n一些实现已经试验了分段堆栈，这将允许堆栈在必要时增长。另一种选择是从一个小的连续堆栈开始，然后在需要时将堆栈复制到一个更大的新分配的内存区域(类似于`std::vector`的增长方式)。Go(goro tines)中的 coroutine 实现已经从使用分段堆栈切换到动态增长的连续堆栈。\n\n无堆栈协程不需要为单独的侧堆栈分配内存。相反，为了支持挂起和恢复，它们需要一个单独的分配来存储每个协同帧。这种分配发生在调用 coroutine 时(但不是在挂起/恢复时)。当协程返回时，调用框架被解除分配。\n\n总之，stackful coroutines 需要为 coroutine 框架和侧堆栈分配大量的初始内存，或者需要支持不断增长的堆栈。无堆栈协程只需要为协程框架分配内存。调用 coroutine 的内存占用可以总结如下:\n\n*   无堆叠:冠状框架\n*   堆栈:协同框架+调用堆栈\n\n性能成本的下一个方面涉及暂停和恢复协同工作。\n\n### 上下文开关程序\n\n上下文切换可以发生在不同的级别。一般来说，当我们需要中央处理器在两个或多个正在进行的任务之间切换时，就会发生上下文切换。即将暂停的任务需要保存 CPU 的整个状态，以便可以在稍后阶段恢复。\n\n在不同进程和操作系统线程之间切换是相当昂贵的操作，涉及系统调用，需要 CPU 进入内核模式。内存缓存失效，对于进程切换，需要替换包含虚拟内存和物理内存之间映射的表。\n\n暂停和恢复协同也是一种上下文切换，因为我们在多个并发流之间切换。在协程之间切换比在进程和操作系统线程之间切换要快得多，部分原因是它不涉及任何需要中央处理器在内核模式下运行的系统调用。\n\n但是在堆叠式花冠之间切换和在无堆叠式花冠之间切换还是有区别的。栈式和无栈式协程的上下文切换的相对运行时性能取决于调用模式。但是，一般来说，stackful coroutine 的上下文切换操作更昂贵，因为与无 stack ful coroutine 相比，它在挂起和恢复期间有更多的信息要保存和恢复。恢复无堆栈的协同工作类似于正常的函数调用。\n\n无栈与栈式的争论已经在 C++ 社区中持续了好几年，我将尽我所能远离这场争论，总结它们都有有效的用例——一些用例会支持栈式协同，而其他用例会支持无栈式协同。\n\n为了让您更好地理解协程是如何执行的，这一部分稍微绕了一下。让我们简单回顾一下你所学的内容。\n\n## 到目前为止你学到了什么\n\n协程是可以暂停和恢复的功能。普通函数没有这种能力，这使得移除返回的函数的调用框架成为可能。但是，挂起的协程需要保持调用帧活动，以便能够在它恢复后恢复协程的状态。协程比子程序更强大，在生成的机器代码中涉及更多的簿记。然而，由于协程和普通函数之间的密切关系，今天的编译器非常擅长优化无堆栈协程。\n\nStackful 协程可以被视为非抢占式用户级线程，而 stack ful 协程提供了一种以直接命令方式编写状态机的方法，使用关键字`await`和`yield`来指定挂起点。\n\n在介绍了一般的协同抽象之后，现在是时候了解无堆栈协同是如何在 C++ 中实现的了。\n\n# C++ 中的协程\n\n添加到 C++ 20 的协程是无栈协程。通过使用第三方库，也可以选择在 C++ 中使用 stackful 协程。最著名的跨平台库是 Boost.Fiber. C++ 20 无堆栈协程引入了新的语言构造，而 Boost。Fiber 是一个可以与 C++ 11 及更高版本一起使用的库。我们不会在本书中进一步讨论堆栈式协程，而是将重点放在 C++ 20 中已经标准化的无堆栈式协程上。\n\nC++ 20 中的无堆栈协程的设计目标如下:\n\n*   在某种意义上是可扩展的，因为它们增加的内存开销非常小。这使得有可能比可能的线程数量或堆栈数量多得多的活的协程。\n*   高效的上下文切换，这意味着暂停和恢复协程应该和普通的函数调用一样便宜。\n*   高度灵活。C++ 协程有超过 15 个定制点，这给了应用开发人员和库编写人员很大的自由来配置和塑造他们喜欢的协程。关于协程应该如何工作的决定可以由美国开发人员决定，而不是硬编码在语言规范中。一个例子是协程在被调用后是应该直接挂起还是继续执行到第一个显式挂起点。这样的问题通常用其他语言进行硬编码，但是在 C++ 中，我们可以使用定制点定制这种行为。\n*   不需要 C++ 异常来处理错误。这意味着您可以在异常关闭的环境中使用协程。请记住，coroutines 是与普通功能相当的低级功能，在嵌入式环境和有实时要求的系统中非常有用。\n\n考虑到这些目标，一开始理解 C++ 协程有点复杂可能并不奇怪。\n\n## 标准 C++ 包含哪些内容(哪些不包含)？\n\n一些 C++ 特性是纯库特性(如范围库)，而其他特性是纯语言特性(如借助`auto`关键字的类型推断)。然而，一些特性需要添加到核心语言和标准库中。C++ 协程就是这些特性之一；它们为语言引入了新的关键词，但也为标准库添加了新的类型。\n\n在语言方面，总结一下，我们有以下与协同工作相关的关键词:\n\n*   `co_await`:暂停当前协同的操作符\n*   `co_yield`:向调用者返回一个值并暂停协同\n*   `co_return`:完成一个协同指令的执行，并且可以选择返回值\n\n在库端，有一个新的`<coroutine>`头，包括以下内容:\n\n*   `std::coroutine_handle`:引用协同状态的模板类，支持协同的暂停和恢复\n*   `std::suspend_never`:一种从不挂起的微不足道的可唤醒类型\n*   `std::suspend_always`:总是挂起的微不足道的可唤醒类型\n*   `std::coroutine_traits`:用于定义协同诉讼的承诺类型\n\nC++ 20 附带的库类型是绝对最少的。例如，用于协程和调用者之间通信的基础设施不是 C++ 标准的一部分。为了在我们的应用代码中有效地使用协程，我们需要的一些类型和函数已经在新的 C++ 提案中提出，例如模板类`task`和`generator`以及函数`sync_wait()`和`when_all()`。C++ 协程的库部分很可能在 C++ 23 中得到补充。\n\n在本书中，我将提供一些简化的类型来填补这个空白，而不是使用第三方库。通过实现这些类型，您将深入了解 C++ 协程是如何工作的。然而，如果不引入生命周期问题，很难设计出可与 coroutines 一起使用的健壮库组件。因此，如果您计划在您当前的项目中使用 coroutines，那么使用第三方库可能是从头开始实现它们的更好选择。在撰写本文时， **CppCoro** 库是这些通用原语的事实标准。该图书馆由刘易斯·贝克创建，可在[https://github.com/lewissbaker/cppcoro](https://github.com/lewissbaker/cppcoro)获得。\n\n## 是什么让 C++ 函数成为协程？\n\n如果一个 C++ 函数包含任何一个关键字`co_await`、`co_yield`或`co_return`，那么它就是一个协同词。此外，编译器对 coroutine 的返回类型有特殊要求。但是，尽管如此，我们需要检查定义(身体)，而不仅仅是声明，以知道我们面对的是一个协同作用还是一个普通的功能。这意味着协程的调用方不需要知道它调用的是协程还是普通函数。\n\n与普通函数相比，协程也有以下限制:\n\n*   协程不能使用像`f(const char*...)`这样的变量参数\n*   验尸官不能返回`auto`或概念类型:`auto f()`\n*   验尸官不能宣布`constexpr`\n*   构造函数和析构函数不能是协同的\n*   `main()`函数不能是协同函数\n\n一旦编译器确定一个函数是一个协同函数，它就把协同函数和许多类型联系起来，使协同机器工作。下图突出显示了当一个*呼叫者*使用一个*协程*时所涉及的不同组件:\n\n<figure class=\"mediaobject\">![](img/B15619_12_10.png)</figure>\n\n图 12.10:协程和它的调用者之间的关系\n\n调用者和协程是我们通常在应用代码中实现的实际功能。\n\n**返回对象**是协程返回的类型，通常是为某些特定用例设计的通用类模板，例如*生成器*或*异步任务*。*调用者*与返回对象进行交互，以恢复协程并获取协程发出的值。return 对象通常将其所有调用委托给 coroutine 句柄。\n\n**科罗廷手柄**是**科罗廷状态**的非拥有手柄。通过协同手柄，我们可以恢复和破坏协同状态。\n\n*协同状态*就是我之前所说的协同框架。这是一个不透明的物体，这意味着我们不知道它的大小，除了通过手柄，我们无法以任何其他方式接近它。协同状态存储所有必要的信息，以便从上次暂停的地方恢复协同。协同状态也包含**承诺**。\n\n承诺对象是验尸官本身通过关键词`co_await`、`co_yield`、`co_return`间接沟通的对象。如果值或错误是从协同提交的，它们将首先到达承诺对象。promise 对象就像是 coroutine 和调用者之间的通道，但是两者都不能直接访问 promise。\n\n诚然，乍一看，这可能看起来相当密集。一个完整但最小的例子会帮助你更好地理解不同的部分。\n\n## 一个最小但完整的例子\n\n让我们从一个最小的例子开始，以达到理解协同工作的目的。首先，我们实现一个小的*协程*，在它返回之前被暂停和恢复:\n\n```cpp\nauto coroutine() -> Resumable {    // Initial suspend\n  std::cout << \"3 \";\n  co_await std::suspend_always{};  // Suspend (explicit)\n  std::cout << \"5 \";\n}                                  // Final suspend then return \n```\n\n其次，我们创建协程的*调用者*。注意这个程序的输出和控制流程。这是:\n\n```cpp\nint main() {            \n  std::cout << \"1 \";\n  auto resumable = coroutine(); // Create coroutine state\n  std::cout << \"2 \";\n  resumable.resume();           // Resume\n  std::cout << \"4 \";\n  resumable.resume();           // Resume\n  std::cout << \"6 \";\n}                               // Destroy coroutine state\n// Outputs: 1 2 3 4 5 6 \n```\n\n第三，需要定义协程的返回对象`Resumable`:\n\n```cpp\nclass Resumable {                // The return object\n  struct Promise { /*...*/ };    // Nested class, see below\n  std::coroutine_handle<Promise> h_;\n  explicit Resumable(std::coroutine_handle<Promise> h) : h_{h} {}\npublic:\n  using promise_type = Promise;\n  Resumable(Resumable&& r) : h_{std::exchange(r.h_, {})} {}\n  ~Resumable() { if (h_) { h_.destroy(); } }\n  bool resume() {\n    if (!h_.done()) { h_.resume(); }\n    return !h_.done();\n  }\n}; \n```\n\n最后，承诺类型被实现为`Resumable`内的嵌套类，如下所示:\n\n```cpp\nstruct Promise {\n  Resumable get_return_object() {\n    using Handle = std::coroutine_handle<Promise>;\n    return Resumable{Handle::from_promise(*this)};\n  }\n  auto initial_suspend() { return std::suspend_always{}; }\n  auto final_suspend() noexcept { return std::suspend_always{}; }\n  void return_void() {}\n  void unhandled_exception() { std::terminate(); }\n}; \n```\n\n这个例子很少，但是走过了很多值得关注和需要理解的事情:\n\n*   函数`coroutine()`是一个协同函数，因为它包含使用`co_await`的显式暂停/恢复点\n*   协程不产生任何值，但是仍然需要返回一个带有特定约束的类型(T0)，这样调用者就可以继续协程\n*   我们使用的是名为`std::suspend_always`的*唤醒型*\n*   `resumable`对象的`resume()`功能从暂停的点恢复协同\n*   `Resumable`是法定状态的拥有者。当`Resumable`对象被破坏时，它会使用`coroutine_handle`破坏协程\n\n调用方、协程、协程句柄、承诺和可恢复之间的关系如下图所示:\n\n<figure class=\"mediaobject\">![](img/B15619_12_11.png)</figure>\n\n图 12.11:可恢复示例中涉及的函数/协程和对象之间的关系\n\n现在是时候仔细看看每个部分了。我们将从`Resumable`类型开始。\n\n### 协同返回对象\n\n我们的程序返回一个类型为`Resumable`的对象。这个`Resumable`类很简单。这是协程返回的对象，调用者可以使用它来恢复和销毁协程。为了方便起见，这里再次给出完整的定义:\n\n```cpp\nclass Resumable {               // The return object\n  struct Promise { /*...*/ };   // Nested class\n  std::coroutine_handle<Promise> h_;\n  explicit Resumable(std::coroutine_handle<Promise> h) : h_{h} {}\npublic:\n  using promise_type = Promise;\n  Resumable(Resumable&& r) : h_{std::exchange(r.h_, {})} {}\n  ~Resumable() { if (h_) { h_.destroy(); } }\n  bool resume() {\n    if (!h_.done()) { h_.resume(); }\n    return !h_.done();\n  }\n}; \n```\n\n`Resumable`是只动型，是花冠手柄的拥有者(因此控制着花冠的寿命)。移动构造器通过使用`std::exchange()`确保在源对象中清除协同句柄。当一个`Resumable`对象被破坏时，如果它仍然拥有它，它就破坏了协程。\n\n`resume()`成员函数将继续调用委托给协程句柄，如果协程还活着的话。\n\n为什么我们需要`Resumable`里面的成员类型别名`promise_type = Promise`？每一个承诺都有一个相关的承诺对象。当编译器看到一个协程时(通过检查函数的主体)，它需要找出相关的承诺类型。为此，编译器使用`std::coroutine_traits<T>`模板，其中`T`是协程的返回类型。您可以提供`std::coroutine_traits<T>`的模板专门化，或者利用`std::coroutine_traits`的默认实现将在 coroutine 的返回类型`T`中寻找名为`promise_type`的`public`成员类型或别名的事实。在我们的案例中，`Resumable::promise_type`是`Promise`的别名。\n\n### 承诺类型\n\n承诺类型控制协程的行为。同样，为了方便起见，这里复制了完整的定义:\n\n```cpp\nstruct Promise {\n  auto get_return_object() { return Resumable{*this}; }\n  auto initial_suspend() { return std::suspend_always{}; }\n  auto final_suspend() noexcept { return std::suspend_always{}; }\n  void return_void() {}\n  void unhandled_exception() { std::terminate(); }\n}; \n```\n\n我们不应该直接调用这些函数；相反，当编译器将协程转换为机器代码时，它会插入对 promise 对象的调用。如果我们不提供这些成员函数，编译器就不知道如何为我们生成代码。你可以把承诺看作一个协同控制器对象，它负责:\n\n*   产生从调用 coroutine 返回的值。这由功能`get_return_object()`处理。\n*   通过实现函数`initial_suspend()`和`final_supsend()`，定义协程创建时和销毁前的行为。在我们的`Promise`类型中，我们说应该通过返回`std::suspend_always`在这些点暂停验尸官(见下一节)。\n*   定制当协程最终返回时的行为。如果一个协程使用一个`co_return`和一个计算为类型`T`的值的表达式，承诺必须定义一个名为`return_value(T)`的成员函数。我们的 coroutine 没有返回值，但是 C++ 标准要求我们提供名为`return_void()`的定制点，这里我们留空。\n*   处理不在协同体内处理的异常。在函数`unhandled_exception()`中，我们简单地称之为`std::terminate()`，但是我们将在后面的例子中更优雅地处理它。\n\n代码的最后一些部分需要更多的关注，即`co_await`表达式和可调用的类型。\n\n### 可识别的类型\n\n我们使用`co_await`在代码中添加了一个显式挂起点，并向其传递了一个可调用类型的实例`std::suspend_always`。`std::suspend_always`的实现看起来是这样的:\n\n```cpp\nstruct std::suspend_always {\n  constexpr bool await_ready() const noexcept { return false; }\n  constexpr void await_suspend(coroutine_handle<>) const noexcept {}\n  constexpr void await_resume() const noexcept {}\n}; \n```\n\n`std::suspend_always`之所以被称为微不足道的唤醒类型，是因为它总是说自己从未准备好，从而导致协同暂停。还有另一种琐碎的唤醒类型总是报告它已经准备好了，称为`std::suspend_never`:\n\n```cpp\nstruct std::suspend_never {\n  constexpr bool await_ready() const noexcept { return true; }\n  constexpr void await_suspend(coroutine_handle<>) const noexcept {}\n  constexpr void await_resume() const noexcept {}\n}; \n```\n\n我们可以创建自己的可调用类型，这将在下一章中介绍，但是现在我们可以使用这两个微不足道的标准类型。\n\n这就完成了这个例子。但是当我们有了`Promise`和`Resumable`类型的时候，我们可以做更多的实验。让我们看看我们能用一个开始的花冠做什么。\n\n### 传递我们的花冠\n\n一旦创建了`Resumable`对象，我们就可以将其传递给其他函数，并从那里恢复它。我们甚至可以将协程传递给另一个线程。以下示例显示了这种灵活性的一部分:\n\n```cpp\nauto coroutine() -> Resumable {\n  std::cout << \"c1 \";\n  co_await std::suspend_always{};\n  std::cout << \"c2 \";\n}                                \nauto coro_factory() {             // Create and return a coroutine\n  auto res = coroutine();\n  return res;\n}\nint main() {\n  auto r = coro_factory();\n  r.resume();                     // Resume from main\n  auto t = std::jthread{[r = std::move(r)]() mutable {\n    using namespace std::chrono_literals;\n    std::this_thread::sleep_for(2s);\n    r.resume();                   // Resume from thread\n  }};\n} \n```\n\n前面的例子表明，一旦我们调用了我们的协程，并且有了一个句柄，我们就可以像其他可移动类型一样移动它。这种将它传递给其他线程的能力实际上在我们需要避免特定线程上协同状态的可能堆分配的情况下非常有用。\n\n## 分配协同状态\n\n协同状态，或协同框架，是协同在暂停时存储其状态的地方。协同状态的生存期从调用协同时开始，并在协同执行一个`co_return`语句时被销毁(或者控制从协同体的末尾流出)，除非它在更早的时候通过协同句柄被销毁。\n\n协同状态通常在堆上分配。编译器会插入单独的堆分配。然而，在某些情况下，这种单独的堆分配可以通过将协同状态内联到调用者的框架(可以是普通的堆栈框架或另一个协同框架)中来省略。不幸的是，永远无法保证堆分配的省略。\n\n为了让编译器能够省略堆分配，coroutine 状态的完整生存期必须严格嵌套在调用方的生存期内。此外，编译器需要计算出协同状态的总大小，并且通常需要有被调用协同的主体的可见性，以便它的一部分可以被内联。像虚函数调用和对其他翻译单元或共享库中的函数的调用这样的情况通常会使这成为不可能。如果编译器缺少所需的信息，它将插入一个堆分配。\n\n协同状态的堆分配使用`operator` `new`执行。可以在 promise 类型上提供自定义的类级别`operator new`，然后将使用它来代替全局`operator new`。因此，可以检查堆分配是否被取消。如果不是，我们可以找出协同状态需要多少记忆。下面是一个使用我们之前定义的`Promise`类型的例子:\n\n```cpp\nstruct Promise {\n  /* Same as before ... */\n  static void* operator new(std::size_t sz) {\n    std::cout << \"custom new for size \" << sz << '\\n';\n    return ::operator new(sz);\n  }\n  static void operator delete(void* ptr) {\n    std::cout << \"custom delete called\\n\";\n    ::operator delete(ptr);\n  }\n} \n```\n\n另一个使用特定的承诺类型来验证堆分配对于所有的协程是完全省略的技巧是声明`operator new`和`operator delete`，但是省略它们的定义。如果编译器随后插入对这些运算符的调用，程序将由于未解析的符号而无法链接。\n\n## 避免悬空引用\n\n事实上，一个协程可以在我们的代码中传递，这意味着我们需要非常小心传递给协程的参数的生命周期，以避免悬空引用。协同框架包含通常存在于堆栈中的对象的副本，例如传递给协同框架的局部变量和参数。如果一个验尸官通过引用接受一个论点，引用的是*引用*，而不是对象。这意味着，当遵循函数参数的通常准则时，我们很容易以悬空引用结束；也就是参照`const`传递复制成本较高的对象。\n\n### 将参数传递给 coroutines\n\n下面的标题引用了一个`const std::string`:\n\n```cpp\nauto coroutine(const std::string& str) -> Resumable { \n  std::cout << str;\n  co_return;\n} \n```\n\n假设我们有一个工厂函数创建并返回 coroutine，如下所示:\n\n```cpp\nauto coro_factory() {\n  auto str = std::string{\"ABC\"};\n  auto res = coroutine(str);\n  return res;\n} \n```\n\n最后，一个`main()`函数使用了协程:\n\n```cpp\nint main() {\n  auto coro = coro_factory();\n  coro.resume();\n} \n```\n\n当程序试图访问包含字符串`\"ABC\"`的`std::string`对象时，该代码表现出未定义的行为。希望这不会让你感到意外。这个问题类似于让 lambda 通过引用捕获变量，然后将 lambda 传递给其他代码，而不保持被引用对象的活动状态。通过引用传递 lambda 捕获变量时，也可以获得类似的例子:\n\n```cpp\nauto lambda_factory() {\n  auto str = std::string{\"ABC\"};\n  auto lambda = [&str]() {         // Capture str by reference\n    std::cout << str;     \n  };\n  return lambda;                   // Ops! str in lambda becomes\n}                                  // a dangling reference\nint main() {\n  auto f = lambda_factory();\n  f();                             // Undefined behavior\n} \n```\n\n如你所见，同样的问题也可能发生在 lambdas 身上。在*第 2 章*、*基本 C++ 技术*中，我警告过你要用 lambdas 来捕获引用，通常最好用按值捕获来避免这种情况。\n\n避免使用 coroutines 悬空引用的解决方案类似:在使用 coroutines 时，避免通过引用传递参数。相反，使用按值传递，整个参数对象将安全地放置在协同框架中:\n\n```cpp\nauto coroutine(std::string str) -> Resumable {  // OK, by value!\n  std::cout << str;\n  co_return;\n}\nauto coro_factory() {\n  auto str = std::string{\"ABC\"};\n  auto res = coroutine(str);\n  return res;\n}\nint main() {\n  auto coro = coro_factory();\n  coro.resume();                                 // OK!\n} \n```\n\n使用协程时，参数是寿命问题的一个重要且常见的来源，但它们不是唯一的来源。现在我们将探讨与协程和悬空引用相关的一些其他陷阱。\n\n### 协同的成员函数\n\n成员函数也可以是协同函数。例如，没有什么可以阻止我们在成员函数中使用`co_await`，如下例所示:\n\n```cpp\nstruct Widget {\nauto coroutine() -> Resumable {       // A member function \n    std::cout << i_++ << \" \";         // Access data member\n    co_await std::suspend_always{};\n    std::cout << i_++ << \" \";\n  }\n  int i_{};\n};\nint main() {\n  auto w = Widget{99};\n  auto coro = w.coroutine();\n  coro.resume();\n  coro.resume();\n}\n// Prints: 99 100 \n```\n\n重要的是要理解`coroutine()`(在这种情况下为`main()`)的调用者有责任确保`Widget`对象`w`在验尸官的整个生命周期内保持存活。协程从它所属的对象访问数据成员，但是`Widget`对象本身是*而不是*由协程保持活动。如果我们将协程传递给程序的其他部分，这很容易成为一个问题。\n\n假设我们正在使用前面演示的一些协同工厂函数，但是返回一个成员函数协同:\n\n```cpp\nauto widget_coro_factory() {      // Create and return a coroutine\n  auto w = Widget{};\n  auto coro = w.coroutine();\n  return coro; \n}                                 // Object w destructs here\nint main() {\n  auto r = widget_coro_factory();\n  r.resume();                     // Undefined behavior \n  r.resume();                  \n} \n```\n\n这段代码展示了未定义的行为，因为我们现在有了一个从 coroutine 到`widget_coro_factory()`函数中创建和析构的`Widget`对象的悬空引用。换句话说，我们最终得到两个具有不同生存期的对象，而其中一个对象引用另一个对象，但没有任何明确的所有权。\n\n### 那些是花冠\n\n不仅仅是成员功能可以成为协程。还可以通过在 lambda 的主体中插入`co_await`、`co_return`和/或`co_yield`来使用 lambda 表达式创建协程。\n\nCoroutine lambdas 可能会有一点额外的棘手处理。理解 coroutine lambdas 最常见的寿命问题的一种方法是考虑函数对象。回想一下*第二章*、*基本 C++ 技术*，一个 lambda 表达式被编译器转换成一个函数对象。此对象的类型是实现了调用运算符的类。现在，假设我们用`co_return`体内的一个λ；这意味着呼叫操作员`operator()()`成为协管员。\n\n考虑以下使用 lambda 的代码:\n\n```cpp\nauto lambda = [](int i) -> Resumable {\n  std::cout << i;\n  co_return;              // Make it a coroutine\n};\nauto coro = lambda(42);   // Call, creates the coroutine frame\ncoro.resume();            // Outputs: 42 \n```\n\nlambda 对应的类型如下所示:\n\n```cpp\nstruct LambdaType {\n  auto operator()(int i) -> Resumable {  // Member function\n    std::cout << i;                      // Body\n    co_return;\n  }\n};\nauto lambda = LambdaType{};\nauto coro = lambda(42);\ncoro.resume(); \n```\n\n这里需要注意的重要一点是，实际的协程是一个*成员函数*，即呼叫操作符`operator()()`。上一节已经演示了拥有协同成员函数的缺陷:我们需要在协同的生命周期内保持对象的活力。在前面的例子中，这意味着只要协同框架是活动的，我们就需要保持名为`lambda`的函数对象是活动的。\n\nlambdas 的一些用法使得在 coroutine 框架被破坏之前，很容易意外地破坏函数对象。例如，通过使用立即调用的λ，我们很容易陷入麻烦:\n\n```cpp\nauto coro = [i = 0]() mutable -> Resumable { \n  std::cout << i++ ; \n  co_await std::suspend_always{};\n  std::cout << i++ ;\n}();               // Invoke lambda immediately\ncoro.resume();     // Undefined behavior! Function object\ncoro.resume();     // already destructed \n```\n\n这个代码看起来是无辜的；lambda 没有通过引用捕获任何东西。但是，由 lambda 表达式创建的函数对象是一个临时对象，一旦被调用并且 coroutine 捕获到对它的引用，它就会被析构。当协同恢复时，程序可能会崩溃或产生垃圾。\n\n同样，更好地理解这一点的一种方法是将λ转换为定义了`operator()`的普通类:\n\n```cpp\nstruct LambdaType {\n  int i{0};\n  auto operator()() -> Resumable {\n    std::cout << i++ ; \n    co_await std::suspend_always{};\n    std::cout << i++ ;\n  }\n};\nauto coro = LambdaType{}(); // Invoke operator() on temporary object\ncoro.resume();              // Ops! Undefined behavior \n```\n\n现在你可以看到这个非常类似于我们有一个成员函数是一个协程的情况。协同框架不会使函数对象保持活动状态。\n\n### 防止悬空引用的准则\n\n除非你有很好的理由通过引用接受论点，否则如果你正在写一个协程，选择通过值接受论点。协同框随后会保留你传递给它的对象的完整副本，对象保证和协同框一样长的寿命。\n\n如果您使用的 lambdas 或成员函数是 coroutine，请特别注意 coroutine 所属对象的生存期。请记住，存储在协同框架中的对象(或功能对象)是*而不是*。验尸官的召唤者有责任让它活着。\n\n## 处理错误\n\n有不同的方法将错误从协程转移回调用它或恢复它的代码部分。我们并不被迫对信号错误使用异常。相反，我们可以根据需要定制错误处理。\n\n当客户端从协程中获得一个值时(当协程产生或返回时)，协程可以通过抛出异常或返回错误代码，使用协程将错误传递回客户端。\n\n如果我们正在使用异常，并且一个异常被传播出了协程的主体，那么承诺对象的函数`unhandled_exception()`被调用。这个调用发生在编译器插入的 catch 块中，因此可以使用`std::current_exception()`来获取抛出的异常。来自`std::current_exception()`的结果可以作为`std::exception_ptr`存储在验尸官中，并在以后再次抛出。当使用异步协程时，你将在下一章中看到这样的例子。\n\n## 定制点\n\n你已经看到很多定制点了，我觉得一个有效的问题是:为什么这么多定制点？\n\n*   **通用性**:定制点使得各种方式使用卡罗拉成为可能。很少有关于如何使用 C++ 协程的假设。库作者可以自定义`co_await`、`co_yield`和`co_return`的行为。\n*   **效率**:一些定制点可以根据用例实现可能的优化。一个例子是`await_ready()`，如果已经计算了一个值，它可以返回`true`以避免不必要的暂停。\n\n还应该说，我们接触到这些定制点是因为 C++ 标准没有提供任何类型(除了`std::coroutine_handle`)来与协程通信。一旦它们到位，我们就可以重用这些类型，而不用太担心那些定制点。然而，了解定制点对于充分理解如何有效地使用 C++ 协程是有价值的。\n\n# 发电机\n\n生成器是一种向调用者返回值的协程。例如，在本章开头，我演示了生成器`iota()`如何产生不断增加的整数值。通过实现可以充当迭代器的通用生成器类型，我们可以简化实现与基于范围的`for`循环、标准库算法和范围兼容的迭代器的工作。一旦我们有了生成器模板类，我们就可以重用它了。\n\n到目前为止，在本书中，您已经在访问容器元素的上下文中以及在使用标准库算法时看到了迭代器。然而，迭代器不必绑定到容器。可以编写产生值的迭代器。\n\n## 实现生成器\n\n我们即将实现的生成器是基于 CppCoro 库中的生成器的。生成器模板旨在用作产生一系列值的 coroutines 的返回类型。应该可以将这种类型的对象与基于范围的`for`循环以及接受迭代器和范围的标准算法一起使用。为了实现这一点，我们将实现三个组件:\n\n*   `Generator`，是返回对象\n*   `Promise`，作为协同控制器\n*   `Iterator`，客户端与`Promise`的接口\n\n这三种类型紧密耦合，它们与协同状态之间的关系如下图所示:\n\n<figure class=\"mediaobject\">![](img/B15619_12_12.png)</figure>\n\n图 12.12:迭代器、生成器、承诺和协同状态之间的关系\n\n返回对象，在本例中为`Generator`类，与`Promise`类型紧密耦合；`Promise`类型负责创建`Generator`对象，`Generator`类型负责向编译器公开正确的 `promise_type`。以下是`Generator`的实现:\n\n```cpp\ntemplate <typename T>\nclass Generator {\n  struct Promise { /* ... */ };   // See below\n  struct Sentinel {};  \n  struct Iterator { /* ... */ };  // See below\n\n  std::coroutine_handle<Promise> h_;\n  explicit Generator(std::coroutine_handle<Promise> h) : h_{h} {}\npublic: \n  using promise_type = Promise;\n  Generator(Generator&& g) : h_(std::exchange(g.h_, {})) {}\n  ~Generator() { if (h_) { h_.destroy();  } }\n  auto begin() {\n    h_.resume();\n    return Iterator{h_};\n  }\n  auto end() { return Sentinel{}; }\n}; \n```\n\n`Promise`和`Iterator`的实施很快就会到来。`Generator`和我们之前定义的`Resumable`类没什么不同。`Generator`是验尸官的归还对象，也是`std::coroutine_handle`的拥有者。发电机是可移动的。当移动时，协同手柄被转移到新构造的`Generator`物体上。当拥有协同句柄的生成器被销毁时，它通过调用协同句柄上的`destroy`来销毁协同状态。\n\n`begin()`和`end()`功能使得在基于范围的`for`循环和接受范围的算法中使用该生成器成为可能。`Sentinel`类型是空的——它是一个伪类型——并且`Sentinel`实例在那里能够传递一些东西给`Iterator`类的比较运算符。`Iterator`的实现是这样的:\n\n```cpp\nstruct Iterator {\n  using iterator_category = std::input_iterator_tag;\n  using value_type = T;\n  using difference_type = ptrdiff_t;\n  using pointer = T*;\n  using reference = T&;\n\n  std::coroutine_handle<Promise> h_;  // Data member\n\n  Iterator& operator++() {\n    h_.resume();\n    return *this;\n  }\n  void operator++(int) { (void)operator++(); }\n  T operator*() const { return h_.promise().value_; }\n  T* operator->() const { return std::addressof(operator*()); }\n  bool operator==(Sentinel) const { return h_.done(); }\n}; \n```\n\n迭代器需要将协同句柄存储在数据成员中，这样它就可以将调用委托给协同句柄和 promise 对象:\n\n*   当迭代器被取消引用时，它返回承诺持有的当前值\n*   当迭代器递增时，它恢复协同工作\n*   当迭代器与 sentinel 值进行比较时，迭代器忽略 sentinel 并将调用委托给 coroutine 句柄，coroutine 句柄知道是否有更多的元素要生成\n\n现在只剩下`Promise`型留给我们去实施。`Promise`的完整定义如下:\n\n```cpp\nstruct Promise {\n  T value_;\n  auto get_return_object() -> Generator {\n    using Handle = std::coroutine_handle<Promise>;\n    return Generator{Handle::from_promise(*this)};\n  }\n  auto initial_suspend() { return std::suspend_always{}; }\n  auto final_suspend() noexcept { return std::suspend_always{}; }\n  void return_void() {}\n  void unhandled_exception() { throw; }\n  auto yield_value(T&& value) {\n    value_ = std::move(value);\n    return std::suspend_always{};\n  }\n  auto yield_value(const T& value) {\n    value_ = value;\n    return std::suspend_always{};\n  }\n}; \n```\n\n我们生成器的承诺对象负责:\n\n*   创建`Generator`对象\n*   定义到达初始和最终暂停点时的行为\n*   跟踪从验尸官那里得到的最后一个价值\n*   处理由协同体引发的异常\n\n就这样！我们现在已经准备好了所有的东西。返回某些`Generator<T>`类型的协程现在可以使用`co_yield`缓慢地产生值。协程的调用者与`Generator`和`Iterator`对象交互来检索值。下面说明对象之间的交互:\n\n<figure class=\"mediaobject\">![](img/B15619_12_13.png)</figure>\n\n图 12.13:调用者与生成器和迭代器对象通信，从协程中检索值\n\n现在，让我们看看如何使用新的`Generator`模板，以及它如何简化各种迭代器的实现。\n\n## 使用生成器类\n\n这个例子是由戈尔·尼沙诺夫在 CppCon 2016([https://sched.co/7nKt](https://sched.co/7nKt))上的演讲 *C++ Coroutines:在封面下*启发而来的。它清楚地展示了我们如何从刚刚实现的生成器类型中获益。小型可组合生成器现在可以这样实现:\n\n```cpp\ntemplate <typename T>\nauto seq() -> Generator<T> {\n  for (T i = {};; ++ i) {\n    co_yield i;\n  }\n}\ntemplate <typename T>\nauto take_until(Generator<T>& gen, T value) -> Generator<T> {\n  for (auto&& v : gen) {\n    if (v == value) {\n      co_return;\n    }\n    co_yield v;\n  }\n}\ntemplate <typename T>\nauto add(Generator<T>& gen, T adder) -> Generator<T> {\n  for (auto&& v : gen) {\n    co_yield v + adder;\n  }\n} \n```\n\n一个小的使用例子表明，我们可以将我们的生成器传递给基于范围的`for`循环:\n\n```cpp\nint main() {\n  auto s = seq<int>();\n  auto t = take_until<int>(s, 10);\n  auto a = add<int>(t, 3);\n  int sum = 0;\n  for (auto&& v : a) {\n      sum += v;\n  }\n  return sum; // returns 75\n} \n```\n\n发电机被懒洋洋地评估。在程序到达`for`循环之前，不产生任何值，该循环将值从生成器链中拉出。\n\n这个程序的另一个有趣的方面是，当我在优化打开的情况下使用 Clang 10 编译它时，*整个*程序的汇编代码如下所示:\n\n```cpp\nmain:  # @main\nmov  eax, 75\nret \n```\n\n太神奇了！程序只是定义了一个返回值`75`的主函数。换句话说，编译器优化器已经能够在编译时完全评估生成器链，并得出单个值`75`。\n\n我们的`Generator`类也可以和距离算法一起使用。在以下示例中，我们使用算法`includes()`来查看序列`{5,6,7}`是否是生成器生成的数字的子范围:\n\n```cpp\nint main() { \n  auto s = seq<int>();                           // Same as before\n  auto t = take_until<int>(s, 10);\n  auto a = add<int>(t, 3);\n  const auto v = std::vector{5, 6, 7};\n  auto is_subrange = std::ranges::includes(a, v); // True\n} \n```\n\n随着`Generator`模板的实现，我们可以将其重用到各种生成器函数中。我们已经实现了一个通用且非常有用的库组件，应用代码在构建延迟生成器时可以在很多地方从中受益。\n\n### 解决发电机问题\n\n我现在将提出一个小问题，我们将尝试使用不同的技术来解决它，以便理解哪些编程习惯用法我们可以用生成器来替代。我们将要编写一个小工具，用于生成起始值和终止值之间的线性间隔序列。\n\n如果您一直在使用 MATLAB/Octave 或 Python NumPy，您可能会认识到这种使用名为`linspace()`的函数生成均匀(线性)间隔数字的方式。这是一个方便的工具，可以在任意范围的各种上下文中使用。\n\n我们将称我们的发电机为`lin_space()`。下面是在`2.0`和`3.0`之间生成五个等距值的用法示例:\n\n```cpp\nfor (auto v: lin_space(2.0f, 3.0f, 5)) {\n  std::cout << v << \", \";\n}\n// Prints: 2.0, 2.25, 2.5, 2.75, 3.0, \n```\n\n生成浮点值时，我们必须谨慎一点，因为我们不能简单地计算每个步长(在前面的示例中为 0.25)并将其累加，因为步长可能无法使用浮点数据类型来精确表示。可能的舍入误差将在每次迭代中累加，最终我们可能得到完全无意义的值。相反，我们需要做的是使用线性插值计算特定增量下开始值和停止值之间的一个数字。\n\nC++ 20 给`<cmath>`增加了一个方便实用的叫做`std::lerp()`的工具，它可以计算两个值之间具有指定数量的线性插值。在我们的情况下，金额将是一个介于 0.0 和 1.0 之间的值；0 值返回`start`值，1.0 值返回`stop`值。以下是使用`std::lerp()`的几个例子:\n\n```cpp\nauto start = -1.0;\nauto stop = 1.0;\nstd::lerp(start, stop, 0.0);    // -1.0\nstd::lerp(start, stop, 0.5);    //  0.0\nstd::lerp(start, stop, 1.0);    //  1.0 \n```\n\n我们将要编写的`lin_space()`函数都将使用以下小实用函数模板:\n\n```cpp\ntemplate <typename T>\nauto lin_value(T start, T stop, size_t index, size_t n) {  \n  assert(n > 1 && index < n);\n  const auto amount = static_cast<T>(index) / (n - 1);\n  const auto v = std::lerp(start, stop, amount);   // C++ 20\n  return v;\n} \n```\n\n该函数返回线性序列中范围为[ `start`，`stop` ]的值。`index`参数是我们将要生成的`n`总数的序列中的当前数字。\n\n有了`lin_value()`助手，我们现在可以轻松实现`lin_space()`生成器了。在看到使用协程的解决方案之前，我们将研究其他常见的技术。接下来的章节将探讨实施`lin_space()`时的以下不同方法:\n\n*   急切地生成并返回所有值\n*   使用回调(延迟)\n*   使用自定义迭代器(延迟)\n*   使用范围库(延迟)\n*   与我们的`Generator`类一起使用协程(延迟)\n\n对于每一个例子，都会有每种方法的优缺点的简短反映。\n\n#### 急切的线性范围\n\n我们将首先实现一个简单的急切版本，它计算该范围内的所有值，并返回一个包含所有值的向量:\n\n```cpp\ntemplate <typename T>\nauto lin_space(T start, T stop, size_t n) {\n  auto v = std::vector<T>{};\n  for (auto i = 0u; i < n; ++ i)\n    v.push_back(lin_value(start, stop, i, n));\n  return v;\n} \n```\n\n由于此版本返回一个标准容器，因此可以将返回值与基于范围的`for`循环和其他标准算法一起使用:\n\n```cpp\nfor (auto v : lin_space(2.0, 3.0, 5)) {\n  std::cout << v << \", \";\n}\n// Prints: 2, 2.25, 2.5, 2.75, 3, \n```\n\n这个版本很简单，也很容易阅读。缺点是我们需要分配一个向量并用*填充所有的*值，尽管调用者不一定对所有的值都感兴趣。这个版本也缺乏可组合性，因为没有办法在不首先生成所有值的情况下过滤掉中间的元素。\n\n现在让我们尝试实现一个延迟版本的`lin_space()`生成器。\n\n#### 使用回调的延迟版本\n\n在*第 10 章* *代理对象和*中，我们得出结论:延迟评估可以通过使用回调函数来完成。我们将实现的延迟版本将基于向`lin_space()`传递回调，并在发出值时调用回调函数:\n\n```cpp\ntemplate <typename T, typename F>\nrequires std::invocable<F&, const T&>               // C++ 20 \nvoid lin_space(T start, T stop, std::size_t n, F&& f) {\n  for (auto i = 0u; i < n; ++ i) {\n    const auto y = lin_value(start, stop, i, n);\n    f(y);\n  }\n} \n```\n\n如果我们想打印生成器产生的值，我们可以这样调用这个函数:\n\n```cpp\nauto print = [](auto v) { std::cout << v << \", \"; };\nlin_space(-1.f, 1.f, 5, print);\n// Prints: -1, -0.5, 0, 0.5, 1, \n```\n\n迭代现在发生在`lin_space()`函数中。没有办法取消生成器，但是通过一些改变，我们可以让回调函数返回一个`bool`来指示它是否想要生成更多的元素。\n\n这种方法有效，但不太优雅。当试图组合生成器时，这种设计的问题变得更加明显。如果我们想添加一个过滤器来选择一些特殊的值，我们最终会得到嵌套的回调函数。\n\n我们现在将继续讨论如何实现基于迭代器的解决方案。\n\n### 迭代器实现\n\n另一种选择是通过公开`begin()`和`end()`迭代器来实现符合范围概念的类型。这里定义的类模板`LinSpace`，可以迭代数值的线性范围:\n\n```cpp\ntemplate <typename T>\nstruct LinSpace {\n  LinSpace(T start, T stop, std::size_t n)\n      : begin_{start, stop, 0, n}, end_{n} {}\n  struct Iterator {\n    using difference_type = void;\n    using value_type = T;\n    using reference = T;\n    using pointer = T*;\n    using iterator_category = std::forward_iterator_tag;\n    void operator++() { ++ i_; }\n    T operator*() { return lin_value(start_, stop_, i_, n_);}\n    bool operator==(std::size_t i) const { return i_ == i; } \n    T start_{};\n    T stop_{};\n    std::size_t i_{};\n    std::size_t n_{};\n  };\n  auto begin() { return begin_; }\n  auto end() { return end_; }\n private:\n  Iterator begin_{};\n  std::size_t end_{};\n};\ntemplate <typename T>\nauto lin_space(T start, T stop, std::size_t n) {\n  return LinSpace{start, stop, n};\n} \n```\n\n这个实现非常高效。然而，它受到大量样板代码的困扰，我们试图封装的小算法现在被分散到不同的部分:`LinSpace`构造函数实现了设置开始和停止值的初始工作，而计算值所需的工作最终在`Iterator`类的成员函数中完成。这使得算法的实现与我们已经看到的其他版本相比更难理解。\n\n### 使用范围库的解决方案\n\n另一种选择是使用范围库(C++ 20)的构建块来构建我们的算法，如下所示:\n\n```cpp\ntemplate <typename T>\nauto lin_space(T start, T stop, std::size_t n) {\n  return std::views::iota(std::size_t{0}, n) |\n    std::views::transform([=](auto i) {\n      return lin_value(start, stop, i, n);\n    });\n} \n```\n\n这里我们将整个算法封装在一个小函数中。我们正在使用`std::views::iota`为我们生成索引。将索引转换为线性值是一个简单的转换，可以链接到`iota`视图之后。\n\n这个版本是高效且可组合的。从`lin_space()`返回的对象是一个类型为`std::ranges::view`的随机访问范围，可以使用基于范围的`for`循环进行迭代，或者传递给其他算法。\n\n最后，是时候使用我们的`Generator`类来实现我们的算法了。\n\n#### 使用协程的解决方案\n\n看了不少于四个版本的这个非常相同的问题，我们现在已经达到了最后的解决方案。这里我将展示一个版本，它使用了前面实现的通用`Generator`类模板:\n\n```cpp\ntemplate <typename T> \nauto lin_space(T start, T stop, std::size_t n) -> Generator<T> {\n   for (auto i = 0u; i < n; ++ i) {\n     co_yield lin_value(start, stop, i, n);\n   }\n } \n```\n\n它简洁明了，易于理解。通过使用`co_yield`，我们可以用看起来类似于简单急切版本的方式编写代码，但是不需要收集容器中的所有值。正如您将在本章末尾看到的那样，可以基于协程来链接多个生成器。\n\n这个版本也兼容基于范围的`for`-循环和标准算法。但是，这个版本公开了一个输入范围，所以不可能跳过任意数量的元素，这在使用范围库的版本中是可能的。\n\n### 结论\n\n显然，做这件事的方法不止一种。但是我为什么要展示所有这些方法呢？\n\n首先，如果你是一个新手，你将有希望开始看到使用协同的模式。\n\n其次，`Generator`模板和`co_yield`的使用让我们能够以非常清晰简洁的方式实现延迟生成器。当我们将该解决方案与其他版本进行比较时，这一点变得显而易见。\n\n最后，对于这个示例问题，有些方法可能看起来很做作，但是经常在其他环境中使用。默认情况下，C++ 是一种渴望的语言，许多人(包括我自己)已经习惯于创建类似于渴望版本的代码。使用回调的版本可能看起来很奇怪，但它是异步代码中常用的模式，在异步代码中，coroutines 可以包装或替换那些基于回调的 API。\n\n我们实现的生成器类型部分基于 CppCoro 库中的同步生成器模板。CppCoro 还提供了一个`async_generator`模板，使得在发电机协同内使用`co_await`操作符成为可能。我在本章中提供了`Generator`模板，目的是演示如何实现生成器，以及我们如何与协程交互。但是如果您计划在代码中开始使用生成器，请考虑使用第三方库。\n\n## 使用生成器的真实例子\n\n当例子稍微高级一点的时候，使用协程来简化迭代器真的很棒。将`co_yield`与`Generator`类一起使用允许我们高效地实现和组合小算法，而不需要样板代码来将它们粘合在一起。下一个例子将试图证明这一点。\n\n### 问题是\n\n我们将在这里通过一个例子来说明我们如何使用我们的`Generator`类来实现一个压缩算法，该算法可以在搜索引擎中用来压缩通常存储在磁盘上的搜索索引。曼宁等人的《信息检索导论》一书中对这个例子进行了详尽的描述，该书可在[https://nlp.stanford.edu/IR-book/](https://nlp.stanford.edu/IR-book/)免费获得。以下是问题的简要背景和简短描述。\n\n搜索引擎使用一种叫做“T2”的倒排索引的数据结构。它就像一本书末尾的索引。使用索引，我们可以找到包含我们正在搜索的术语的所有页面。\n\n现在假设我们有一个充满食谱的数据库，并且我们为这个数据库建立了一个倒排索引。该索引的部分内容可能如下所示:\n\n<figure class=\"mediaobject\">![](img/B15619_12_14.png)</figure>\n\n图 12.14:一个包含三个术语的倒排索引及其对应的文档引用列表\n\n每个术语都与文档标识符的排序列表相关联。(例如**苹果**一词包含在 id 为 **4** 、 **9** 、 **67** 、 **89** 的食谱中。)如果我们想找到同时包含**豆类** *和* **辣椒**的食谱，我们可以运行类似合并的算法来找到**豆类**和**辣椒**列表的交集:\n\n<figure class=\"mediaobject\">![](img/B15619_12_15.png)</figure>\n\n图 12.15 术语“豆子”和“辣椒”的文档列表的交集\n\n现在假设我们有一个大数据库，我们选择用一个 32 位整数来表示文档标识符。对于许多文档中出现的术语，文档标识符列表可能会变得非常长，因此我们需要压缩这些列表。一种可能的方法是使用增量编码结合可变字节编码方案。\n\n### 增量编码\n\n由于列表是排序的，我们可以存储两个相邻元素之间的**间隙**，而不是保存文档标识符。这种技术被称为**δ编码**或**间隙编码**。下图显示了使用文档标识和间隙的示例:\n\n<figure class=\"mediaobject\">![](img/B15619_12_16.png)</figure>\n\n图 12.16:间隙编码将两个相邻元素之间的间隙存储在列表中\n\nGap 编码非常适合这类数据；经常使用的术语因此会有许多小的空白。真正长的名单只会包含非常小的差距。在对列表进行间隙编码后，我们可以使用可变字节编码方案，通过使用更少的字节来缩小间隙，从而实际压缩列表。\n\n但是首先，让我们开始实现间隙编码功能。我们将从编写两个小的协程开始，这两个程序将进行间隙编码/解码。编码器将排序的整数序列转换为间隙序列:\n\n```cpp\ntemplate <typename Range>\nauto gap_encode(Range& ids) -> Generator<int> {\n  auto last_id = 0;\n  for (auto id : ids) {\n    const auto gap = id - last_id;\n    last_id = id;\n    co_yield gap;\n  }\n} \n```\n\n通过使用`co_yield`，不需要急切地传递一个完整的数字列表和分配一个大的输出差距列表。相反，花冠懒洋洋地一次处理一个数字。注意功能`gap_encode()`如何包含关于如何将文档标识转换为间隙的所有知识。将它实现为传统的迭代器是可能的，但是这将使逻辑分散在迭代器的构造函数和运算符中。\n\n我们可以构建一个小程序来测试我们的 gap 编码器:\n\n```cpp\nint main() {\n  auto ids = std::vector{10, 11, 12, 14};\n  auto gaps = gap_encode();\n  for (auto&& gap : gaps) {\n    std::cout << gap << \", \";\n  }\n} // Prints: 10, 1, 1, 2, \n```\n\n解码器做相反的事情；它将一系列间隙作为输入，并将其转换为有序数字列表:\n\n```cpp\ntemplate <typename Range>\nauto gap_decode(Range& gaps) -> Generator<int> {\n  auto last_id = 0;\n  for (auto gap : gaps) {\n    const auto id = gap + last_id;\n    co_yield id;\n    last_id = id;\n  }\n} \n```\n\n通过使用间隙编码，我们将平均存储更小的数字。但是由于我们仍然使用`int`值来存储小间隙，如果我们将这些间隙保存到磁盘上，我们并没有真正获得什么。不幸的是，我们不能只使用较小的固定大小的数据类型，因为我们仍然有可能遇到真正大的差距，这将需要一个完整的 32 位`int`。我们想要的是一种使用更少的位来存储小间隙的方法，如下图所示:\n\n<figure class=\"mediaobject\">![](img/B15619_12_17.png)</figure>\n\n图 12.17:小数字应该使用更少的字节\n\n为了使这个列表在物理上更小，我们可以使用**可变字节编码**，这样小的间隙比大的间隙用更少的字节编码，如上图所示。\n\n#### 可变字节编码\n\n可变字节编码是一种非常常见的压缩技术。UTF-8 和 MIDI 消息是使用这种技术的一些众所周知的编码。为了在编码时使用可变的字节数，我们将每个字节的 7 位用于实际有效载荷。每个字节的第一位代表一个**延续位**。如果有更多字节要读取，则设置为`0`，或者为编码数的最后一个字节设置为`1`。下图举例说明了编码方案:\n\n<figure class=\"mediaobject\">![](img/B15619_12_18.png)</figure>\n\n图 12.18:使用可变字节编码，只需要一个字节来存储十进制值 3，两个字节来编码十进制值 1025\n\n现在我们已经准备好实现可变字节编码和解码方案。这比增量编码稍微复杂一点。编码器应该将一个数字转换成一个或多个字节的序列:\n\n```cpp\nauto vb_encode_num(int n) -> Generator<std::uint8_t> {\n  for (auto cont = std::uint8_t{0}; cont == 0;) {\n    auto b = static_cast<std::uint8_t>(n % 128);\n    n = n / 128;\n    cont = (n == 0) ? 128 : 0;\n    co_yield (b + cont);\n  }\n} \n```\n\n代码中名为`cont`的延续位是 0 或 128，对应于位序列 10000000。这个例子中的细节理解起来并不重要，但是为了使编码更容易，字节以相反的顺序生成，以便最低有效字节优先。这不是问题，因为我们可以在解码过程中轻松处理。\n\n有了数字编码器，就可以很容易地对数字序列进行编码，并将其转换为字节序列:\n\n```cpp\ntemplate <typename Range>\nauto vb_encode(Range& r) -> Generator<std::uint8_t> {\n  for (auto n : r) {\n    auto bytes = vb_encode_num(n);\n    for (auto b : bytes) {\n      co_yield b;\n    }\n  }\n} \n```\n\n解码器可能是最复杂的部分。但同样，它被完全封装成一个具有干净接口的单一功能:\n\n```cpp\ntemplate <typename Range>\nauto vb_decode(Range& bytes) -> Generator<int> {\n  auto n = 0;\n  auto weight = 1;\n  for (auto b : bytes) {\n    if (b < 128) {  // Check continuation bit\n      n += b * weight;\n      weight *= 128;\n    } \n    else {\n      // Process last byte and yield\n      n += (b - 128) * weight;\n      co_yield n;\n      n = 0;       // Reset\n      weight = 1;  // Reset\n    }\n  }\n} \n```\n\n如您所见，这段代码中几乎不需要样板代码。每个协程封装了所有的状态，并清楚地描述了如何一次处理一个工件。\n\n我们需要的最后一块是将 gap 编码器与可变字节编码器结合起来，以便压缩我们的文档标识符排序列表:\n\n```cpp\ntemplate <typename Range>\nauto compress(Range& ids) -> Generator<int> {\n  auto gaps = gap_encode(ids);\n  auto bytes = vb_encode(gaps);\n  for (auto b : bytes) {\n    co_yield b;\n  }\n} \n```\n\n解压是一个简单的`vb_decode()`后跟`gap_decode()`的链接:\n\n```cpp\ntemplate <typename Range>\nauto decompress(Range& bytes) -> Generator<int> {\n  auto gaps = vb_decode(bytes);\n  auto ids = gap_decode(gaps);\n  for (auto id : ids) {\n    co_yield id;\n  }\n} \n```\n\n由于`Generator`类公开了迭代器，我们可以更进一步，使用 iostreams 轻松地将值流式传输到磁盘或从磁盘传输。(虽然，更现实的方法是使用内存映射的输入/输出来获得更好的性能。)这里有两个小函数，用于向磁盘写入压缩数据和从磁盘读取压缩数据:\n\n```cpp\ntemplate <typename Range>\nvoid write(const std::string& path, Range& bytes) {\n  auto out = std::ofstream{path, std::ios::out | std::ofstream::binary};\n  std::ranges::copy(bytes.begin(), bytes.end(),    \n                    std::ostreambuf_iterator<char>(out));\n}\nauto read(std::string path) -> Generator<std::uint8_t> {\n  auto in = std::ifstream {path, std::ios::in | std::ofstream::binary};\n  auto it = std::istreambuf_iterator<char>{in};\n  const auto end = std::istreambuf_iterator<char>{};\n  for (; it != end; ++ it) {\n    co_yield *it;\n  }\n} \n```\n\n一个小测试程序将总结这个例子:\n\n```cpp\nint main() {\n  {\n    auto documents = std::vector{367, 438, 439, 440};\n    auto bytes = compress(documents);\n    write(\"values.bin\", bytes);\n  }\n  {\n    auto bytes = read(\"values.bin\");\n    auto documents = decompress(bytes);\n    for (auto doc : documents) {\n      std::cout << doc << \", \";\n    }\n  }\n}\n// Prints: 367, 438, 439, 440, \n```\n\n这个例子旨在说明我们可以将延迟的程序分成小的封装的协程。C++ 协程的低开销使它们适合构建高效的生成器。我们最初实现的`Generator`是一个完全可重用的类，它帮助我们在这样的例子中最大限度地减少样板代码的数量。\n\n关于发电机的部分到此结束。我们现在将继续讨论使用 coroutines 时的一些一般性能考虑。\n\n# 表演\n\n每次创建一个协程时(当它第一次被调用时)，都会分配一个协程帧来保存协程状态。在某些情况下，可以在堆或堆栈上分配帧。但是，不能保证完全避免堆分配。如果您处于禁止堆分配的情况下(例如，在实时上下文中)，可以在不同的线程中创建并立即挂起协程，然后将其传递给程序中需要实际使用协程的部分。挂起和恢复保证不分配任何内存，并且成本与普通函数调用相当。\n\n在撰写本书时，编译器已经对 coroutines 提供了实验支持。小实验显示了与性能相关的有希望的结果，表明 coroutines 对优化器是友好的。然而，我不会在这本书里给你提供任何关于协同的基准。相反，我已经向您展示了如何评估无堆栈协同工作，以及如何以最小的开销实现协同工作。\n\n生成器示例表明，协程可能对编译器非常友好。我们在那个例子中编写的生成器链是在运行时完全评估的。实际上，这是 C++ 协程的一个非常好的特性。它们允许我们编写编译器和人类都容易理解的代码。C++ 协程通常会产生易于优化的干净代码。\n\n在同一个线程上执行的协程可以共享状态，而无需使用任何锁定原语，因此可以避免同步多个线程带来的性能开销。这将在下一章中演示。\n\n# 摘要\n\n在本章中，您已经看到了如何使用关键字`co_yield` 和`co_return`使用 C++ 协程来构建生成器。为了更好地理解 C++ 无堆栈协程与堆栈式协程的区别，我们比较了两者，并查看了 C++ 协程提供的定制点。这让您深刻理解了 C++ 协程有多灵活，以及如何使用它们来实现效率。无堆栈协程与状态机密切相关。通过将传统实现的状态机重写为使用协程的代码，我们探索了这种关系，您看到了编译器如何将我们的协程转换和优化为机器语言。\n\n在下一章中，我们将通过关注异步编程来继续讨论协程，并将加深您对`co_await`关键字的理解。****"
  },
  {
    "path": "docs/cpp-hiperf/13.md",
    "content": "# 十三、使用协程的异步编程\n\n在前一章中实现的生成器类帮助我们使用 coroutines 来构建延迟评估的序列。C++ 协程也可以用于异步编程，方法是让协程表示异步计算或**异步任务**。虽然异步编程是在 C++ 中拥有协程的最重要的驱动因素，但是在标准库中不支持基于协程的异步任务。如果你想使用协程进行异步编程，我建议你找到并使用一个补充 C++ 20 协程的库。我已经推荐了 CppCoro([https://github.com/lewissbaker/cppcoro](https://github.com/lewissbaker/cppcoro)，在撰写本文时，这似乎是最有希望的选择。还可以使用异步协程和完善的库 Boost。Asio，正如你将在本章后面看到的。\n\n本章将展示使用协程进行异步编程是可能的，并且有一些库可以补充 C++ 20 协程。更具体地说，我们将重点关注:\n\n*   `co_await`关键字和可选类型\n*   基本任务类型的实现——可以从执行一些异步工作的协程中返回的类型\n*   助推。Asio 用 coroutines 举例说明异步编程\n\n在继续之前，还应该指出的是，本章中没有与性能相关的主题，提供的指南和最佳实践也很少。相反，这一章更多的是介绍 C++ 中异步协程的新特性。我们将从探索可感知的类型和`co_await`语句开始介绍。\n\n# 重新审视可识别的类型\n\n在上一章中，我们已经讨论了一点唤醒类型。但是现在我们需要更具体地了解一下`co_await`是做什么的，以及什么是可感知的类型。关键字`co_await`是一元运算符，意思是它接受单个参数。我们传递给`co_await`的论点需要满足一些我们将在本节探讨的要求。\n\n当我们在代码中说`co_await`时，我们表示我们是*在等待*一些可能已经准备好，也可能还没有准备好的东西。如果还没有准备好，`co_await`暂停当前正在执行的协程，并将控制权返回给调用者。当异步任务完成时，它应该将控制转移回最初等待任务完成的协程。从这里开始，我通常将等待功能称为**延续**。\n\n现在考虑以下表达式:\n\n```cpp\nco_await X{}; \n```\n\n要编译这段代码，`X`需要是一个可调用的类型。到目前为止，我们只使用了微不足道的唤醒类型:`std::suspend_always`和`std::suspend_never`。任何直接实现下面列出的三个成员函数的类型，或者定义`operator co_wait()`来产生具有这些成员函数的对象的类型，都是一个可选择的类型:\n\n*   `await_ready()`返回`bool`，表示结果是否准备好(`true`)或者是否需要暂停当前的协同并等待结果准备好。\n*   `await_suspend(coroutine_handle)`–如果`await_ready()`返回`false`，这个函数将被调用，并带有执行`co_await`的程序的句柄。这个函数让我们有机会开始异步工作，并订阅一个通知，该通知将在任务完成时触发，然后恢复协同工作。\n*   `await_resume()`是负责将结果(或错误)解包回协程的函数。如果在`await_suspend()`启动的工作过程中出现错误，该功能可能会重新抛出捕获的错误或返回错误代码。整个`co_await`表达式的结果就是无论`await_resume()`返回什么。\n\n为了演示`operator co_await()`的使用，这里有一个片段，其灵感来自 C++ 20 标准中定义`operator co_await`时间间隔的部分:\n\n```cpp\nusing namespace std::chrono;\ntemplate <class Rep, class Period> \nauto operator co_await(duration<Rep, Period> d) { \n  struct Awaitable {     \n    system_clock::duration d_;\n    Awaitable(system_clock::duration d) : d_(d) {} \n    bool await_ready() const { return d_.count() <= 0; }\n    void await_suspend(std::coroutine_handle<> h) { /* ... */ } \n    void await_resume() {}\n  }; \n  return Awaitable{d};\n} \n```\n\n有了这个过载，我们现在可以将一个时间间隔传递给`co_await`操作员，如下所示:\n\n```cpp\nstd::cout << \"just about to go to sleep...\\n\";\nco_await 10ms;                   // Calls operator co_await()\nstd::cout << \"resumed\\n\"; \n```\n\n这个例子并不完整，但是给了你一个如何使用一元运算符`co_await`的提示。大家可能已经注意到了，三个`await_*()`函数不是我们直接调用的；相反，它们由编译器插入的代码调用。另一个例子将阐明编译器进行的转换。假设编译器在我们的代码中偶然发现了以下语句:\n\n```cpp\nauto result = co_await expr; \n```\n\n然后，编译器会(非常)粗略地将代码转换为如下内容:\n\n```cpp\n// Pseudo code\nauto&& a = expr;         // Evaluate expr, a is the awaitable\nif (!a.await_ready()) {  // Not ready, wait for result\n  a.await_suspend(h);    // Handle to current coroutine\n                         // Suspend/resume happens here\n}\nauto result = a.await_resume(); \n```\n\n首先调用`await_ready()`功能，检查是否需要暂停。如果是这样的话，`await_suspend()`会被调用一个将被暂停的验尸官的句柄(带有`co_await`语句的验尸官)。最后，请求唤醒的结果并将其分配给`result`变量。\n\n## 隐式暂停点\n\n正如在很多例子中看到的，一个协程通过使用`co_await`和`co_yield`来定义*显式的*暂停点。每个花冠也有两个*隐含*暂停点:\n\n*   **初始暂停点**，出现在执行协同体之前协同体的初始调用\n*   **最终暂停点**，出现在验尸官尸体被执行之后，在验尸官被销毁之前\n\n承诺类型通过实现`initial_suspend()`和`final_suspend()`来定义这两点的行为。这两个函数都返回可调用的对象。通常，我们从`initial_suspend()`函数中传递`std::suspend_always`，这样花冠就懒洋洋地而不是急切地开始了。\n\n最终暂停点对异步任务起着重要作用，因为它使我们可以调整`co_await`的行为。正常情况下，已经`co_await:`执行的协程应该在最终暂停点恢复等待的协程。\n\n接下来，让我们更好地理解这三个可调用的函数是如何使用的，以及它们是如何与`co_await`操作符协作的。\n\n# 实现基本的任务类型\n\n我们将要实现的任务类型是一种可以从代表异步任务的协程中返回的类型。任务是呼叫者可以使用`co_await`等待的东西。目标是能够编写如下所示的异步应用代码:\n\n```cpp\nauto image = co_await load(\"image.jpg\");\nauto thumbnail = co_await resize(image, 100, 100);\nco_await save(thumbnail, \"thumbnail.jpg\"); \n```\n\n标准库已经提供了一种类型，允许函数返回一个对象，调用方可以使用该对象等待计算结果，即`std::future`。我们有可能将`std::future`包装成符合可感知界面的东西。然而，`std::future`不支持延续，这意味着每当我们试图从`std::future`获取值时，我们会阻塞当前线程。换句话说，当使用`std::future`时，没有办法在不阻塞的情况下合成异步操作。\n\n另一种选择是使用`std::experimental::future`或 Boost 库中支持延续的未来类型。但是这些未来的类型分配堆内存，并包含同步原语，这些原语在为我们的任务设置的用例中是不需要的。相反，我们将创建一个开销最小的新类型，其职责是:\n\n*   将返回值和异常转发给调用方\n*   让来电者继续等待结果\n\n提出了一个协同任务类型(见[处的 p 1056 r0 http://www 7 . open-STD . org/JTC 1/SC22/WG21/docs/papers/2018/p 1056 r0 . html](http://www7.open-std.org/JTC1/SC22/WG21/docs/papers/2018/p1056r0.html))，这个提议给了我们一个很好的提示我们需要什么组件。接下来的实现是基于戈尔·尼沙诺夫的工作和刘易斯·贝克共享的源代码，该源代码可以在 CppCoro 库中找到。\n\n下面是用于表示异步任务的类模板的实现:\n\n```cpp\ntemplate <typename T>\nclass [[nodiscard]] Task {\n  struct Promise { /* ... */ };          // See below\n  std::coroutine_handle<Promise> h_;\n  explicit Task(Promise & p) noexcept\n      : h_{std::coroutine_handle<Promise>::from_promise(p)} {}\n public:\n  using promise_type = Promise;\n  Task(Task&& t) noexcept : h_{std::exchange(t.h_, {})} {}\n  ~Task() { if (h_) h_.destroy(); }\n  // Awaitable interface\n  bool await_ready() { return false; }\n  auto await_suspend(std::coroutine_handle<> c) {\n    h_.promise().continuation_ = c;\n    return h_;\n  }\n  auto await_resume() -> T {\n    auto& result = h_.promise().result_;\n    if (result.index() == 1) {\n      return std::get<1>(std::move(result));\n    } else {\n      std::rethrow_exception(std::get<2>(std::move(result)));\n    }\n  }\n}; \n```\n\n每个部分的解释将在随后的部分中进行，但是首先我们需要使用`std::variant`保存值或错误的 promise 类型的实现。承诺还使用`continuation_`数据成员引用等待任务完成的协程:\n\n```cpp\nstruct Promise {\n  std::variant<std::monostate, T, std::exception_ptr> result_;\n  std::coroutine_handle<> continuation_;  // A waiting coroutine\n  auto get_return_object() noexcept { return Task{*this}; }\n  void return_value(T value) { \n    result_.template emplace<1>(std::move(value)); \n  }\n  void unhandled_exception() noexcept {\n    result_.template emplace<2>(std::current_exception());\n  }\n  auto initial_suspend() { return std::suspend_always{}; }\n  auto final_suspend() noexcept {\n    struct Awaitable {\n      bool await_ready() noexcept { return false; }\n      auto await_suspend(std::coroutine_handle<Promise> h) noexcept {\n        return h.promise().continuation_;\n      }\n      void await_resume() noexcept {}\n    };\n    return Awaitable{};\n  }\n}; \n```\n\n区分我们正在使用的两个协同手柄很重要:标识*当前协同*的手柄和标识*延续*的手柄。\n\n请注意，由于`std::variant`的限制，以及`return_value()`和`return_void()`不能同时在同一个承诺类型上的限制，该实现不支持`Task<void>`。不支持`Task<void>`是不幸的，因为不是所有的异步任务都必须返回值。我们将通过为`Task<void>`提供模板专门化来克服这个限制。\n\n由于我们在前一章中实现了一些协同返回类型(`Resumable`和`Generator`)，您已经熟悉了可以从协同返回的类型的要求。在这里，我们将关注对您来说是新的东西，例如异常处理和恢复当前等待我们的呼叫者的能力。让我们开始看看`Task`和`Promise`如何处理返回值和异常。\n\n## 处理返回值和异常\n\n异步任务可以通过返回(一个值或`void`)或抛出异常来完成。值和错误需要交给调用者，调用者一直在等待任务完成。像往常一样，这是承诺对象的责任。\n\n`Promise`类使用一个`std::variant`来存储三种可能结果的结果:\n\n*   一点价值都没有(第`std::monostate`)。在我们的变体中，我们使用这个来使它成为默认可构造的，但是不要求其他两种类型是默认可构造的。\n*   类型为`T`的返回值，其中`T`是`Task`的模板参数。\n*   一个`std::exception_ptr`，它是一个对之前抛出的异常的句柄。\n\n使用功能`Promise::unhandled_exception()`中的`std::current_exception()`功能捕获异常。通过存储一个`std::exception_ptr`，我们可以稍后在另一个上下文中重新抛出这个异常。这也是在线程之间传递异常时使用的机制。\n\n使用`co_return value;`的协程必须有实现`return_value()`的承诺类型。但是，使用`co_return;`或在不返回值的情况下从主体中运行的协程必须具有实现`return_void()`的承诺类型。实现同时包含`return_void()`和`return_value()`的承诺类型会产生编译错误。\n\n## 继续等待验尸官\n\n当异步任务完成后，它应该将控制权转移回协程，等待任务完成。为了能够恢复这个延续，`Task`对象需要`coroutine_handle`到延续的关联。这个句柄被传递给了`Task`对象的`await_suspend()`函数，方便的是，我们确保将这个句柄保存到承诺对象中:\n\n```cpp\nclass Task {\n  // ...\n  auto await_suspend(std::coroutine_handle<> c) {\n    h_.promise().continuation_ = c;      // Save handle\n    return h_;\n  }\n  // ... \n```\n\n`final_suspend()`功能负责在该协同的最终暂停点暂停，并将执行转移到等待协同。这是为方便大家转载的`Promise`的相关部分:\n\n```cpp\nauto Promise::final_suspend() noexcept {\n  struct Awaitable {\n    bool await_ready() noexcept { return false; } // Suspend\n    auto await_suspend(std::coroutine_handle<Promise> h) noexcept{\n      return h.promise().continuation_;  // Transfer control to\n    }                                    // the waiting coroutine\n    void await_resume() noexcept {}\n  };\n  return Awaitable{};\n} \n```\n\n首先，从`await_ready()`返回`false`将会在最后的暂停点使花冠暂停。我们这样做的原因是为了让承诺仍然存在，并且可以让延续者有机会从这个承诺中得出结果。\n\n接下来我们来看看`await_suspend()`功能。这是我们想要恢复延续的地方。我们可以直接在`continuation_`手柄上调用`resume()`并等待它完成，如下所示:\n\n```cpp\n// ...\nauto await_suspend(std::coroutine_handle<Promise> h) noexcept {\n  h.promise().resume();         // Not recommended\n}\n// ... \n```\n\n然而，这将冒在堆栈上创建一长串嵌套调用帧的风险，最终可能导致堆栈溢出。让我们用一个简短的例子来看看这是如何发生的，这个例子使用了两个协程:`a()`和`b()`:\n\n```cpp\nauto a() -> Task<int> {  co_return 42; } \nauto b() -> Task<int> {         // The continuation\n  auto sum = 0;\n  for (auto i = 0; i < 1'000'000; ++ i) {\n    sum += co_await a();\n  }\n  co_return sum;\n} \n```\n\n如果与协同`a()`关联的`Promise`对象直接将手柄上的`resume()`调用到协同`b()`，则在`a()`的调用帧顶部的堆栈上将创建一个新的调用帧来恢复`b()`。这个过程将在循环中一遍又一遍地重复，为每次迭代在堆栈上创建新的嵌套调用框架。当两个函数相互调用时，这种调用序列是一种递归形式，有时称为相互递归:\n\n<figure class=\"mediaobject\">![](img/B15619_13_01.png)</figure>\n\n图 13.1: Coroutine b()调用 coroutine a()，后者恢复 b()，后者调用 a()，后者恢复 b()，以此类推\n\n即使只为`b()`创建了一个协同帧，对`resume()` 的每次调用都会恢复协同`b()`在堆栈上创建一个新帧。避免这个问题的解决方案是称为**对称转移**。任务对象不是直接从即将完成的协程中恢复延续，而是从`await_suspend()`返回识别延续的`coroutine_handle`:\n\n```cpp\n// ...\nauto await_suspend(std::coroutine_handle<Promise> h) noexcept {\n  return h.promise().continuation_;     // Symmetric transfer\n}\n// ... \n```\n\n然后编译器保证会发生一个名为*尾调用优化*的优化。在我们的例子中，这意味着编译器将能够直接将控制转移到延续，而无需创建新的嵌套调用框架。\n\n我们不会花更多的时间在对称转移和尾调用的细节上，但是这些主题的优秀和更深入的解释可以在刘易斯·贝克的文章 *C++ Coroutines:了解对称转移*中找到，可在[https://lewissbaker . github . io/2020/05/11/Understanding _ Symmetric _ Transfer](https://lewissbaker.github.io/2020/05/11/understanding_symmetric_transfer)上找到。\n\n如前所述，我们的`Task`模板有不处理`void`类型模板参数的限制。现在是时候解决这个问题了。\n\n## 支持无效任务\n\n为了克服前面提到的无法处理不产生任何价值的任务的限制，我们需要一个模板专门化`Task<void>`。为了完整起见，此处对其进行了详细说明，但除了前面定义的通用`Task`模板之外，它并没有增加许多新的见解:\n\n```cpp\ntemplate <>\nclass [[nodiscard]] Task<void> {\n\n  struct Promise {\n    std::exception_ptr e_;   // No std::variant, only exception\n    std::coroutine_handle<> continuation_; \n    auto get_return_object() noexcept { return Task{*this}; }\n    void return_void() {}   // Instead of return_value() \n    void unhandled_exception() noexcept { \n      e_ = std::current_exception(); \n    }\n    auto initial_suspend() { return std::suspend_always{}; }\n    auto final_suspend() noexcept {\n      struct Awaitable {\n        bool await_ready() noexcept { return false; }\n        auto await_suspend(std::coroutine_handle<Promise> h) noexcept {\n          return h.promise().continuation_;\n        }\n        void await_resume() noexcept {}\n      };\n      return Awaitable{};\n    }\n  };\n  std::coroutine_handle<Promise> h_;\n  explicit Task(Promise& p) noexcept \n      : h_{std::coroutine_handle<Promise>::from_promise(p)} {}\npublic:\n  using promise_type = Promise;\n\n  Task(Task&& t) noexcept : h_{std::exchange(t.h_, {})} {}\n  ~Task() { if (h_) h_.destroy(); }\n  // Awaitable interface\n  bool await_ready() { return false; }\n  auto await_suspend(std::coroutine_handle<> c) {\n    h_.promise().continuation_ = c;\n    return h_;\n  }\n  void await_resume() {\n    if (h_.promise().e_)\n      std::rethrow_exception(h_.promise().e_);\n  }\n}; \n```\n\n该模板专门化中的 promise 类型只保留对潜在未处理异常的引用。而不是定义`return_value()`，承诺包含成员函数`return_void()`。\n\n我们现在可以表示返回值或`void`的任务。但是在我们真正构建一个独立的程序来测试我们的`Task`类型之前，还有一些工作要做。\n\n## 同步等待任务完成\n\n`Task`类型的一个重要方面是，任何调用返回`Task`的协程的东西都必须在其上`co_await`，因此也是协程。这就产生了一系列的关联(延续)。例如，假设我们有一个像这样的推论:\n\n```cpp\nTask<void> async_func() {      // A coroutine\n  co_await some_func();\n} \n```\n\n那么，就不可能用下面的方式来使用它:\n\n```cpp\nvoid f() {                          \n  co_await async_func(); // Error: A coroutine can't return void\n} \n```\n\n一旦我们调用了一个返回`Task`的异步函数，我们需要对其进行`co_await`，否则什么都不会发生。这也是我们将`Task`声明为`nodiscard`的原因；这样如果忽略返回值就会产生编译警告；如下所示:\n\n```cpp\nvoid g() {        \n  async_func();          // Warning: Does nothing\n} \n```\n\n协程的强制链接有一个有趣的效果，我们最终得到了程序的的`main()`函数，C++ 标准说这是不允许的协程。这需要以某种方式解决，建议的解决方案是提供至少一个同步等待异步链完成的函数。比如 CppCoro 库就包含了函数`sync_wait()`，它具有这种打破协程链的效果，使得一个普通的函数使用协程成为可能。\n\n不幸的是，实现`sync_wait()`相当复杂，但为了至少能够编译和测试我们的`Task`类型，我将在这里提供一个基于标准 C++ 提案 P1171R0、[https://wg21.link/P1171R0](https://wg21.link/P1171R0)的简化版本。我们的目标是能够编写这样的测试程序:\n\n```cpp\nauto some_async_func() -> Task<int> { /* ... */ }\nint main() { \n  auto result = sync_wait(some_async_func());\n  return result;\n} \n```\n\n以测试和运行异步任务为目的，让我们继续`sync_wait()`的实现。\n\n### 正在实现同步等待()\n\n`sync_wait()`内部使用专门为我们设计的自定义任务类，称为`SyncWaitTask`。它的定义一会儿就要揭晓了，不过先来看看功能模板`sync_wait()`的定义:\n\n```cpp\ntemplate<typename T>\nusing Result = decltype(std::declval<T&>().await_resume());\ntemplate <typename T>\nResult<T> sync_wait(T&& task) {\n  if constexpr (std::is_void_v<Result<T>>) {\n    struct Empty {};\n    auto coro = [&]() -> detail::SyncWaitTask<Empty> {\n      co_await std::forward<T>(task);\n      co_yield Empty{};\n      assert(false);\n    };\n    coro().get();\n  } else {\n    auto coro = [&]() -> detail::SyncWaitTask<Result<T>> {\n      co_yield co_await std::forward<T>(task);\n      // This coroutine will be destroyed before it\n      // has a chance to return.\n      assert(false);\n    };\n    return coro().get();\n  }\n} \n```\n\n首先，为了指定任务返回的类型，我们使用了`decltype`和`declval`的组合。相当繁琐的`using-e`表达式给出了`T::await_resume()`返回的类型，其中`T`是传递给`sync_wait()`的任务类型。\n\n在`sync_wait()`内部，我们区分返回值的任务和返回`void`的任务。我们在这里进行了区分，以避免需要实现`SyncWaitTask`的模板专门化来处理`void`和非空类型。通过引入一个空的`struct`，这两种情况都得到类似的处理，这个空的【】可以作为模板参数提供给`SyncWaitTask`来处理`void`任务。\n\n在返回一个实际的值的情况下，一个λ表达式被用来定义一个对结果进行`co_await`运算，然后最终得出它的值。需要注意的是，协程可能会从另一个线程上的`co_await`恢复，这需要我们在`SyncWaitTask`的实现中使用同步原语。\n\n调用协同λ上的`get()`继续协同，直到它产生一个值。`SyncWaitTask`的实现保证了在`co_yield`声明之后，coroutine lambda 再也没有机会恢复。\n\n我们在前一章中大量使用了`co_yield`，但没有提到它与`co_await`的关系；即以下`co_yield`的表述:\n\n```cpp\n co_yield some_value; \n```\n\n被编译器转换成:\n\n```cpp\nco_await promise.yield_value(some_value); \n```\n\n其中`promise`是与当前执行的协程相关联的承诺对象。了解这一点有助于理解`sync_wait()`和`SyncWaitTask`类之间的控制流。\n\n### 实现同步等待任务\n\n现在我们准备检查`SyncWaitTask`，这是一个只打算作为`sync_wait()`助手的类型。为此，我们将其添加到名为`detail`的名称空间下，以明确该类是一个实现细节:\n\n```cpp\nnamespace detail { // Implementation detail\ntemplate <typename T>\nclass SyncWaitTask {  // A helper class only used by sync_wait()\n  struct Promise { /* ... */ }; // See below\n  std::coroutine_handle<Promise> h_;\n  explicit SyncWaitTask(Promise& p) noexcept\n      : h_{std::coroutine_handle<Promise>::from_promise(p)} {}\n public:\n  using promise_type = Promise;\n\n  SyncWaitTask(SyncWaitTask&& t) noexcept \n      : h_{std::exchange(t.h_, {})} {}\n  ~SyncWaitTask() { if (h_) h_.destroy();}\n  // Called from sync_wait(). Will block and retrieve the\n  // value or error from the task passed to sync_wait()\n  T&& get() {\n    auto& p = h_.promise();\n    h_.resume();\n    p.semaphore_.acquire();               // Block until signal\n    if (p.error_)\n      std::rethrow_exception(p.error_);\n    return static_cast<T&&>(*p.value_);\n  }\n  // No awaitable interface, this class will not be co_await:ed\n};\n} // namespace detail \n```\n\n最有趣的部分需要注意的是函数`get()`及其对由 promise 对象拥有的信号量`acquire()`的阻塞调用。这就是这个任务类型同步等待结果为我们准备好的原因。拥有二进制信号量的承诺类型如下所示:\n\n```cpp\nstruct Promise {\n  T* value_{nullptr};\n  std::exception_ptr error_;\n  std::binary_semaphore semaphore_;\n  SyncWaitTask get_return_object() noexcept { \n    return SyncWaitTask{*this}; \n  }\n  void unhandled_exception() noexcept { \n    error_ = std::current_exception(); \n  }\n  auto yield_value(T&& x) noexcept {     // Result has arrived\n    value_ = std::addressof(x);\n    return final_suspend();\n  }\n  auto initial_suspend() noexcept { \n    return std::suspend_always{}; \n  }\n  auto final_suspend() noexcept { \n  struct Awaitable {\n      bool await_ready() noexcept { return false; }\n      void await_suspend(std::coroutine_handle<Promise> h) noexcept {\n        h.promise().semaphore_.release();          // Signal! \n      }\n      void await_resume() noexcept {}\n    };\n    return Awaitable{};\n  }\n  void return_void() noexcept { assert(false); }\n}; \n```\n\n这里有很多我们已经讨论过的样板代码。但是要特别注意`yield_value()`和`final_suspend()`，这是这节课有趣的部分。回想一下`sync_wait()`中的 coroutine lambda 产生了如下的返回值:\n\n```cpp\n// ...\nauto coro = [&]() -> detail::SyncWaitTask<Result<T>> {\n  co_yield co_await std::forward<T>(task);  \n  // ... \n```\n\n所以，一旦价值产生，我们就在承诺对象的`yield_value()`结束。而事实上`yield_value()`可以返回一个可选择的类型，这给了我们定制`co_yield`关键词行为的机会。在这种情况下，`yield_value()`返回一个唤醒信号，该信号将通过二进制信号量表明来自原始`Task`对象的值已经产生。\n\n信号量在`await_suspend()`内部发出信号。我们不能在此之前发出信号，因为等待信号的代码的另一端最终会破坏协程。只有当验尸官处于暂停状态时，销毁验尸官才会发生。\n\n对`semaphore_`的阻塞调用。`acquire()`将从`SyncWaitTask::get()`内部返回信号，最后计算出的值将被传递给调用`sync_wait()`的客户端。\n\n## 使用 sync_wait()测试异步任务\n\n最后，一个使用`Task`和`sync_wait()`的小型异步测试程序可以这样构建:\n\n```cpp\nauto height() -> Task<int> { co_return 20; }     // Dummy coroutines\nauto width() -> Task<int> { co_return 30; }\nauto area() -> Task<int> { \n  co_return co_await height() * co_await width(); \n}\n\nint main() {\n  auto a = area();\n  int value = sync_wait(a);\n  std::cout << value;          // Outputs: 600\n} \n```\n\n我们已经实现了使用 C++ 协程的异步任务的绝对最低基础设施。然而，为了有效地将协程用于异步编程，还需要更多的基础设施。这与生成器(在前一章中介绍)有很大的不同，生成器只需要相当少量的基础工作，我们就可以真正从中受益。为了更接近真实世界，我们将在接下来的部分中探索一些使用 Boost.Asio 的例子。我们要做的第一件事是尝试将基于回调的 API 包装在与 C++ coroutines 兼容的 API 中。\n\n# 包装基于回调的应用编程接口\n\n基于回调的异步 API 有很多。通常，异步函数采用调用者提供的回调函数。异步函数立即返回，然后当异步函数有一个计算值或等待某件事完成时，最终调用回调(完成处理程序)。\n\n为了向您展示基于异步回调的应用编程接口是什么样子，我们将浏览一个名为 **Boost 的异步输入/输出的 Boost 库。Asio** 。关于 Boost 有很多需要学习的地方。这里不涉及的 Asio 我将只描述 Boost 代码的绝对最小值，而是关注与 C++ 协程直接相关的部分。\n\n为了使代码适合本书的页面，这些示例假设每当我们使用 Boost 中的代码时，都已经定义了以下名称空间别名。Asio:\n\n```cpp\nnamespace asio = boost::asio; \n```\n\n下面是使用 Boost 的完整示例。Asio 用于延迟函数调用，但不阻塞当前线程。这个异步示例在一个线程中运行:\n\n```cpp\n#include <boost/asio.hpp>\n#include <chrono>\n#include <iostream>\nusing namespace std::chrono;\nnamespace asio = boost::asio;\nint main() {\n  auto ctx = asio::io_context{};\n  auto timer = asio::system_timer{ctx};\n  timer.expires_from_now(1000ms);\n  timer.async_wait([](auto error) {       // Callback\n    // Ignore errors..                          \n    std::cout << \"Hello from delayed callback\\n\"; \n  });\n  std::cout << \"Hello from main\\n\";\n  ctx.run();\n} \n```\n\n编译和运行该程序将生成以下输出:\n\n```cpp\nHello from main\nHello from delayed callback \n```\n\n使用 Boost 时。Asio，我们总是需要创建一个`io_context`对象来运行一个事件处理循环。对`async_wait()`的调用是异步的；它立即返回到`main()`并在定时器到期时调用回调(lambda)。\n\n计时器示例不使用协程，而是使用回调应用编程接口来提供异步性。助推。Asio 也与 C++ 20 coroutines 兼容，我将在后面演示。但是在我们探索可唤醒类型的道路上，我们将绕道而行，而是假设我们需要在 Boost.Asio 的基于回调的 API 之上提供一个返回可唤醒类型的基于 coroutine 的 API。这样，我们可以使用`co_await`表达式来调用和等待(但不阻塞当前线程)异步任务的完成。我们不希望使用回调，而是希望能够这样写:\n\n```cpp\nstd::cout << \"Hello! \";\nco_await async_sleep(ctx, 100ms);\nstd::cout << \"Delayed output\\n\"; \n```\n\n让我们看看如何实现`async_sleep()`功能，以便与`co_await`一起使用。我们将遵循的模式是让`async_sleep()`返回一个可实现三个必需功能的可调用对象:`await_ready()`、`await_suspend()`和`await_resume()`。接下来将对代码进行解释:\n\n```cpp\ntemplate <typename R, typename P>\nauto async_sleep(asio::io_context& ctx,\n                 std::chrono::duration<R, P> d) {\n  struct Awaitable {\n    asio::system_timer t_;\n    std::chrono::duration<R, P> d_;\n    boost::system::error_code ec_{};\n    bool await_ready() { return d_.count() <= 0; }\n    void await_suspend(std::coroutine_handle<> h) {\n      t_.expires_from_now(d_);\n      t_.async_wait([this, h](auto ec) mutable {\n        this->ec_ = ec;\n        h.resume();\n      });\n    } \n    void await_resume() {\n      if (ec_) throw boost::system::system_error(ec_);\n    }\n  };\n  return Awaitable{asio::system_timer{ctx}, d};\n} \n```\n\n我们再次创建了一个自定义的唤醒类型，它可以完成所有必要的工作:\n\n*   `await_ready()`将返回`false`，除非计时器已经达到零。\n*   `await_suspend()`启动异步操作，并传递一个回调，当定时器到期或产生错误时将调用该回调。回调保存错误代码(如果有)并恢复挂起的协同工作。\n*   `await_resume()`没有要解包的结果，因为我们正在包装的异步函数`boost::asio::timer::async_wait()`除了一个可选的错误代码之外，不返回任何值。\n\n在我们可以在独立程序中实际测试`async_sleep()`之前，我们需要一些方法来启动`io_context`运行循环并打破协同链，就像我们之前测试`Task`类型时所做的那样。在这里，我们将通过实现两个函数`run_task()`和`run_task_impl()`以及一个名为`Detached`的简单的 coroutine 返回类型来实现这一点，该返回类型忽略错误处理并且可以被调用者丢弃:\n\n```cpp\n// This code is here just to get our example up and running\nstruct Detached { \n  struct promise_type {\n    auto get_return_object() { return Detached{}; }\n    auto initial_suspend() { return std::suspend_never{}; }\n    auto final_suspend() noexcept { return std::suspend_never{};}\n    void unhandled_exception() { std::terminate(); } // Ignore\n    void return_void() {}\n  };\n};\nDetached run_task_impl(asio::io_context& ctx, Task<void>&& t) {\n  auto wg = asio::executor_work_guard{ctx.get_executor()};\n  co_await t;\n}\nvoid run_task(asio::io_context& ctx, Task<void>&& t) {\n  run_task_impl(ctx, std::move(t));\n  ctx.run();\n} \n```\n\n`Detached`类型立即启动协程，并运行与调用方分离的协程。`executor_work_guard`阻止`run()`呼叫返回，直到验尸官`run_task_impl()`完成。\n\n通常应避免启动和分离操作。它类似于分离的线程或没有任何引用的分配内存。然而，这个例子的目的是演示我们可以使用什么样的可用类型，以及我们如何编写异步程序并单线程运行它们。\n\n一切就绪；名为`async_sleep()`的包装器返回一个`Task`和一个函数`run_task()`，可以用来执行一个任务。是时候写一个小程序来测试我们实现的新代码了:\n\n```cpp\nauto test_sleep(asio::io_context& ctx) -> Task<void> {\n  std::cout << \"Hello!  \";\n  co_await async_sleep(ctx, 100ms);\n  std::cout << \"Delayed output\\n\";\n}\nint main() {\n  auto ctx = asio::io_context{};\n  auto task = test_sleep(ctx);\n  run_task(ctx, std::move(task));  \n}; \n```\n\n执行该程序将生成以下输出:\n\n```cpp\nHello! Delayed output \n```\n\n您已经看到了基于回调的应用编程接口如何被包装在`co_await`可以使用的函数中，从而允许我们在异步编程中使用协程而不是回调。该程序还提供了一个典型的例子，说明如何使用唤醒类型中的功能。然而，如前所述，事实证明，从 1.70 开始的最新版本的 Boost 已经提供了一个与 C++ 20 coroutines 兼容的接口。在下一节中，我们将在构建一个小型的 TCP 服务器时使用这个新的协同应用编程接口。\n\n# 使用 Boost 的并发服务器。Asio\n\n本节将演示如何编写具有多个执行线程但只使用一个操作系统线程的并发程序。我们即将实现一个可以处理多个客户端的初级并发单线程 TCP 服务器。C++ 标准库中没有联网功能，但幸运的是有 Boost。Asio 为我们提供了一个平台无关的接口来处理套接字通信。\n\n而不是包装基于回调的 Boost。Asio API，我将演示如何使用`boost::asio::awaitable`类，目的是展示一个使用 coroutines 的异步应用编程的更现实的例子。类模板`boost::asio::awaitable`对应我们之前创建的`Task`模板；它被用作代表异步计算的协程的返回类型。\n\n## 实现服务器\n\n服务器很简单；一旦客户端连接，它就开始更新一个数字计数器，并在每次更新时写回该值。这次我们将按照代码从上到下，从`main()`功能开始:\n\n```cpp\n#include <boost/asio.hpp>\n#include <boost/asio/awaitable.hpp>\n#include <boost/asio/use_awaitable.hpp>\nusing namespace std::chrono;\nnamespace asio = boost::asio;\nusing boost::asio::ip::tcp;\nint main() {\n  auto server = [] {\n    auto endpoint = tcp::endpoint{tcp::v4(), 37259};\n    auto awaitable = listen(endpoint);\n    return awaitable;\n  };\n  auto ctx = asio::io_context{};\n  asio::co_spawn(ctx, server, asio::detached);\n  ctx.run(); // Run event loop from main thread\n} \n```\n\n强制`io_context`运行事件处理循环。如果我们希望我们的服务器执行多个操作系统线程，也可以从多个线程调用`run()`。在我们的例子中，我们只使用一个线程，但是有多个并发流。功能`boost::asio::co_spawn()`启动一个分离的并发流程。服务器是使用 lambda 实现的；它定义了一个 TCP 端点(端口为 37259)，并开始侦听端点上的传入客户端连接。\n\n验尸官`listen()`相当简单，看起来像这样:\n\n```cpp\nauto listen(tcp::endpoint endpoint) -> asio::awaitable<void> {\n  auto ex = co_await asio::this_coro::executor;\n  auto a = tcp::acceptor{ex, endpoint};\n  while (true) {\n    auto socket = co_await a.async_accept(asio::use_awaitable);\n    auto session = [s = std::move(socket)]() mutable {\n      auto awaitable = serve_client(std::move(s));\n      return awaitable;\n    };\n    asio::co_spawn(ex, std::move(session), asio::detached);\n  }\n} \n```\n\n执行器是负责实际执行我们的异步函数的对象。例如，执行器可以代表线程池或单个系统线程。我们很可能会在即将到来的 C++ 版本中看到某种形式的执行器，以使我们程序员能够更好地控制代码在何时何地执行(包括 GPU)。\n\n接下来，程序运行一个无限循环，等待 TCP 客户端连接。第一个`co_await`表达式在新客户端成功连接到我们的服务器时返回一个套接字。然后，套接字对象被移动到协程`serve_client()`，该程序将为新连接的客户端提供服务，直到客户端断开连接。\n\n服务器的主要应用逻辑发生在处理每个客户端的协程中。以下是它的外观:\n\n```cpp\nauto serve_client(tcp::socket socket) -> asio::awaitable<void> {\n  std::cout << \"New client connected\\n\";\n  auto ex = co_await asio::this_coro::executor;\n  auto timer = asio::system_timer{ex};\n  auto counter = 0;\n  while (true) {\n    try {\n      auto s = std::to_string(counter) + \"\\n\";\n      auto buf = asio::buffer(s.data(), s.size());\n      auto n = co_await async_write(socket, buf, asio::use_awaitable);\n      std::cout << \"Wrote \" << n << \" byte(s)\\n\";\n      ++ counter;\n      timer.expires_from_now(100ms);\n      co_await timer.async_wait(asio::use_awaitable);\n    } catch (...) {\n      // Error or client disconnected\n      break;\n    }\n  }\n} \n```\n\n在整个客户端会话期间，每个协同调用服务于一个唯一的客户端；它会一直运行，直到客户端与服务器断开连接。协程定期(每 100 毫秒)更新一个计数器，并使用`async_write()`将该值异步写回客户端。注意我们如何以线性方式编写函数`serve_client()`，尽管它调用了两个异步操作:`async_write()`和`async_wait()`。\n\n## 运行并连接到服务器\n\n一旦我们启动了这个服务器，我们就可以在端口 37259 上连接客户端。为了尝试这一点，我正在使用一个名为`nc` (netcat)的工具，它可以用于通过 TCP 和 UDP 进行通信。下面是一个短会话示例，其中客户端连接到运行在 localhost 上的服务器:\n\n```cpp\n[client] $ nc localhost 37259              \n0\n1\n2\n3 \n```\n\n我们可以启动多个客户端，它们都将由一个专用的`serve_client()`协同调用提供服务，并拥有自己的递增计数器变量副本，如下图所示:\n\n<figure class=\"mediaobject\">![](img/B15619_13_02.png)</figure>\n\n图 13.2:一台运行着的服务器，有两个相连的客户端\n\n创建同时服务多个会话的应用的另一种方法是为每个连接的新客户端创建一个线程。然而，线程的内存开销会将会话数量的限制设置得比使用 coroutines 的模型低得多。\n\n这个例子中的协程都是在同一个线程上执行的，这样就不需要锁定共享资源了。假设我们有一个全局计数器，每个会话都会更新。如果我们使用多线程，对全局计数器的访问将需要某种同步(使用互斥或原子数据类型)。对于在同一个线程上执行的协程，这是不必要的。换句话说，在同一线程上执行的协程可以共享状态，而无需使用任何锁定原语。\n\n## 我们在服务器方面取得的成就(以及我们没有取得的成就)\n\n使用 Boost 的示例应用。Asio 演示了 coroutines 可以用于异步编程。我们可以使用`co_await`语句以线性方式编写代码，而不是用嵌套回调实现延续。然而，这个例子是最小的，并且避免了异步编程的一些真正重要的方面，例如:\n\n*   异步读写操作。服务器只向其客户端写入数据，而忽略了同步读写操作的挑战。\n*   取消异步任务和正常关机。服务器在无限循环中运行，完全忽略了干净关闭的挑战。\n*   使用多个`co_await`语句时的错误处理和异常安全。\n\n这些主题非常重要，但超出了本书的范围。我已经提到最好避免分离操作。使用`boost::asio::co_spawn()`创建分离的任务，如示例所示，应该非常小心。一个相当新的避免分离工作的编程范例叫做**结构化并发**。它的目标是通过将并发性封装到通用的可重用算法中，如`when_all()`和`stop_when()`，来解决异常安全和多个异步任务的取消。关键思想是绝不允许某个子任务超过其父任务的生命周期。这使得通过引用异步子操作安全、更好地传递局部变量成为可能。并发任务的严格嵌套生存期也使代码更容易推理。\n\n另一个重要的方面是异步任务应该总是延迟的(立即挂起)，以便在抛出任何异常之前可以附加延续。如果您希望能够以安全的方式取消任务，这也是一项要求。\n\n在未来的几年里，很可能会有很多与这个重要主题相关的讲座、图书馆和文章。CppCon 2019 年的两次会谈探讨了这一主题:\n\n*   *c++ 中异步的统一抽象*，埃里克·内布勒和 D. S .霍尔曼，[https://sched.co/SfrC](https://sched.co/SfrC)\n*   *结构化并发:用协程和算法编写更安全的并发代码*，刘易斯·贝克，[https://sched.co/SfsU](https://sched.co/SfsU)\n\n# 摘要\n\n在本章中，您已经看到了如何使用 C++ 协程来编写异步任务。为了能够以`Task`类型和`sync_wait()`函数的形式实现基础设施，您需要完全理解可调用类型的概念，以及如何使用它们来定制 C++ 中协程的行为。\n\n通过使用 Boost。Asio，我们可以构建一个真正最小但功能齐全的并发服务器应用，在一个线程上执行，同时处理多个客户端会话。\n\n最后，我简要介绍了一种称为结构化并发的方法，并给出了一些关于这个主题的更多信息。\n\n在下一章中，我们将继续探索并行算法，这是一种利用多核加速并发程序的方法。"
  },
  {
    "path": "docs/cpp-hiperf/14.md",
    "content": "# 十四、并行算法\n\n前面几章集中讨论了如何通过使用线程和协程在我们的程序中引入并发和异步。本章着重于独立任务的并行执行，它与并发性相关但又不同。\n\n在前面的章节中，我强调我更喜欢标准的库算法，而不是手工的`for`-循环。在本章中，您将看到在 C++ 17 引入的执行策略中使用标准库算法的一些巨大优势。\n\n这一章不会深入探讨并行算法或并行编程的理论，因为这些主题太复杂了，无法在一章中涵盖。此外，有许多关于这个主题的书。相反，本章将采用一种更实用的方法，并演示如何扩展当前的 C++ 代码库来利用并行性，同时保持代码库的可读性。换句话说，我们不希望并行性妨碍可读性；相反，我们希望将并行性抽象出来，这样代码的并行化就只是改变算法参数的问题。\n\n在本章中，您将学习:\n\n*   用于实现并行算法的各种技术\n*   如何评估并行算法的性能\n*   如何调整代码库以使用标准库算法的并行扩展\n\n并行编程是一个复杂的话题，所以在开始之前，首先需要了解引入并行的动机。\n\n# 平行的重要性\n\n从程序员的角度来看，如果现在的计算机硬件是 100 GHz 的单核 CPU，而不是 3 GHz 的多核 CPU，那就非常方便了；我们不需要关心并行性。不幸的是，让单核处理器越来越快已经达到了物理极限。因此，随着计算机硬件向多核处理器和可编程图形处理器方向发展，程序员必须使用高效的并行模式来充分利用硬件。\n\n并行算法允许我们通过在多核 CPU 或 GPU 上同时执行多个独立任务或子任务来优化程序。\n\n# 并行算法\n\n如*第 11 章*、*并发*所述，术语*并发*和*并行*可能会有点难以区分。提醒一下，如果一个程序有多个独立的控制流在重叠的时间段内运行，那么它就被称为并发运行。另一方面，并行程序同时(完全同时)执行多个任务或子任务，这需要具有多个内核的硬件。我们使用并行算法来优化延迟或吞吐量。如果我们没有能够同时执行多个任务以获得更好性能的硬件，那么并行化算法是没有意义的。下面将介绍几个简单的公式，帮助您了解在评估并行算法时需要考虑哪些因素。\n\n## 评估并行算法\n\n在本章中，**加速**被定义为算法的顺序和并行版本之间的比，如下所示:\n\n<figure class=\"mediaobject\">![](img/B15619_14_001.png)</figure>\n\n*T* <sub class=\"italic\">1</sub> 是使用在一个核上执行的顺序算法解决问题所花费的时间，*T*T6【n】T7 是使用 *n* 个核解决相同问题所花费的时间。*时间*指挂钟时间(不是 CPU 时间)。\n\n与顺序算法相比，并行算法通常更复杂，需要更多的计算资源(例如，中央处理器时间)。并行版本的好处来自于将算法分散到几个处理单元的能力。\n\n考虑到这一点，同样值得注意的是，并非所有算法在并行运行时都能获得相同的性能提升。并行算法的**效率**可以通过以下公式计算:\n\n<figure class=\"mediaobject\">![](img/B15619_14_002.png)</figure>\n\n在这个公式中， *n* 是执行算法的核心数。由于*T*<sub class=\"italic\">1</sub>/*T*<sub class=\"italic\">n</sub>表示加速，所以效率也可以表示为*加速* / *n* 。\n\n如果效率为 *1.0* ，算法并行化完美。例如，这意味着我们在具有八个内核的计算机上执行并行算法时，可以实现 8 倍的加速。然而，在实践中，有许多参数限制了并行执行，例如创建线程、内存带宽和上下文切换，如*第 11 章*、*并发中所述。*因此，通常情况下，效率远低于 1.0。\n\n并行算法的效率取决于如何独立处理每个工作块。例如，`std::transform()`对于并行化来说是微不足道的，因为每个元素彼此完全独立地被处理。这将在本章后面进行演示。\n\n效率还取决于问题大小和内核数量。例如，由于并行算法增加的复杂性导致的开销，并行算法在小数据集上的性能可能非常差。同样，在大量内核上执行程序可能会遇到计算机中的其他瓶颈，例如内存带宽。我们说，当我们改变内核数量和/或输入大小时，如果效率保持不变，则并行算法可以扩展。\n\n记住并非程序的所有部分都可以并行化也很重要。这一事实限制了程序的理论最大加速，即使我们有无限数量的内核。我们可以使用*第三章*、*分析和测量性能*中介绍的**阿姆达尔定律**来计算最大可能加速比。\n\n## 重新审视阿姆达尔定律\n\n在这里，我们将把阿姆达尔定律应用于并行程序。它是这样工作的:一个程序的总运行时间可以分成两个不同的部分或*分数*:\n\n*   *F* <sub class=\"italic\">seq</sub> 是程序中只能依次*执行的部分*\n*   *F* <sub class=\"italic\">par</sub> 是可以在*并行*中执行的程序的分数\n\n由于这两个分数一起组成了整个程序，这意味着*F*<sub xmlns:epub=\"http://www.idpf.org/2007/ops\" class=\"italic\">seq</sub>= 1-*F*T6】par。现在，阿姆达尔定律告诉我们，在 *n 个*核上执行的程序的**最大加速比**为:\n\n<figure class=\"mediaobject\">![](img/B15619_14_003.png)</figure>\n\n为了直观显示这个定律的效果，下图显示了一个程序的执行时间，顺序部分在底部，并行部分在顶部。增加内核数量只会影响并行分数，这将对最大加速进行限制:\n\n<figure class=\"mediaobject\">![](img/B15619_14_01.png)</figure>\n\n图 14.1:阿姆达尔定律定义了最大加速比；在这种情况下，它是 2x\n\n在上图中，在单个 CPU 上运行时，顺序部分占执行时间的 50%。因此，在执行这样的程序时，我们通过增加更多内核可以实现的最大加速是 2 倍。\n\n为了让您对如何实现并行算法有所了解，我们现在将通过几个示例。我们将从`std::transform()`开始，因为它相对容易分裂成多个独立的部分。\n\n## 实现并行标准::转换()\n\n虽然从算法上来说`std::transform()`很容易实现，但实际上，即使是实现一个的初级并行版本也比乍看起来要复杂。\n\n算法`std::transform()`为序列中的每个元素调用一个函数，并将结果存储在另一个序列中。`std::transform()`的顺序版本的一个可能的实现可能看起来像这样:\n\n```cpp\ntemplate<class SrcIt, class DstIt, class Func>\nauto transform(SrcIt first, SrcIt last, DstIt dst, Func func) {\n  while (first != last) {\n      *dst++ = func(*first++);\n  }\n} \n```\n\n标准库版本也返回`dst`迭代器，但是在我们的例子中我们将忽略它。为了理解`std::transform()`并行版本的挑战，让我们从一个天真的方法开始。\n\n### 幼稚的实现\n\n`std::transform()`的一个天真的并行实现可能看起来像这样:\n\n*   将元素分成与计算机内核数量相对应的块\n*   在单独的任务中处理每个块\n*   等待所有任务完成\n\n使用`std::thread::hardware_concurrency()`确定支持的硬件线程数量，一个可能的实现如下所示:\n\n```cpp\ntemplate <typename SrcIt, typename DstIt, typename Func>\nauto par_transform_naive(SrcIt first, SrcIt last, DstIt dst, Func f) {\n  auto n = static_cast<size_t>(std::distance(first, last));\n  auto n_cores = size_t{std::thread::hardware_concurrency()};\n  auto n_tasks = std::max(n_cores, size_t{1});\n  auto chunk_sz = (n + n_tasks - 1) / n_tasks;\n  auto futures = std::vector<std::future<void>>{};\n  // Process each chunk on a separate\n  for (auto i = 0ul; i < n_tasks; ++ i) {\n    auto start = chunk_sz * i;\n    if (start < n) {\n      auto stop = std::min(chunk_sz * (i + 1), n);\n      auto fut = std::async(std::launch::async,\n         [first, dst, start, stop, f]() {\n          std::transform(first + start, first + stop, dst + start, f);\n      });\n      futures.emplace_back(std::move(fut));\n    }\n  }\n  // Wait for each task to finish\n  for (auto&& fut : futures) {\n    fut.wait();\n  }\n} \n```\n\n请注意，`hardware_concurrency()`可能会返回`0`，如果由于某种原因，它是不确定的，因此被箝位为至少一个。\n\n`std::transform()`和我们的并行版本之间的一个细微差别是，它们对迭代器提出了不同的要求。`std::transform()`可以对绑定到`std::cin`的`std::istream_iterator<>`等输入输出迭代器进行操作。这对于`par_transform_naive()`是不可能的，因为迭代器是从多个任务中复制和使用的。正如您将看到的，本章中没有可以对输入和输出迭代器进行操作的并行算法。相反，并行算法至少需要允许多遍遍历的前向迭代器。\n\n#### 性能赋值\n\n继续的天真实现，让我们用一个简单的性能评估来衡量它的性能，与在单个 CPU 内核上执行的`std::transform()`的顺序版本相比。\n\n在这个测试中，我们将测量时间(墙上的时钟)和改变数据输入大小时花费在 CPU 上的总时间。\n\n我们将使用*第 3 章*、*性能分析和测量*中介绍的谷歌基准来设置该基准。为了避免代码重复，我们将实现一个为我们的基准测试设置测试夹具的函数。夹具需要一个带有一些示例值的源范围、一个结果的目标范围和一个转换函数:\n\n```cpp\nauto setup_fixture(int n) {\n  auto src = std::vector<float>(n);\n  std::iota(src.begin(), src.end(), 1.0f); // Values from 1.0 to n\n  auto dst = std::vector<float>(src.size());\n  auto transform_function = [](float v) { \n    auto sum = v;\n    for (auto i = 0; i < 500; ++ i) {\n      sum += (i * i * i * sum);\n    }\n    return sum;\n  };\n  return std::tuple{src, dst, transform_function};\n} \n```\n\n现在我们已经建立了我们的夹具，是时候实施实际的基准了。会有两个版本:一个是顺序版本`std::transform()`，一个是我们的并行版本`par_transform_naive()`:\n\n```cpp\nvoid bm_sequential(benchmark::State& state) {\n  auto [src, dst, f] = setup_fixture(state.range(0));\n  for (auto _ : state) {\n    std::transform(src.begin(), src.end(), dst.begin(), f);\n  }\n}\nvoid bm_parallel(benchmark::State& state) {\n  auto [src, dst, f] = setup_fixture(state.range(0));\n  for (auto _ : state) {\n    par_transform_naive(src.begin(), src.end(), dst.begin(), f);\n  }\n} \n```\n\n仅测量`for`循环内的代码。通过使用`state.range(0)`作为输入大小，我们可以通过向每个基准添加一系列值来生成不同的值。事实上，我们需要为每个基准测试指定几个参数，因此我们创建了一个助手函数来应用我们需要的所有设置:\n\n```cpp\nvoid CustomArguments(benchmark::internal::Benchmark* b) {\n  b->Arg(50)->Arg(10'000)->Arg(1'000'000)  // Input size\n      ->MeasureProcessCPUTime()            // Measure all threads\n      ->UseRealTime()                      // Clock on the wall \n      ->Unit(benchmark::kMillisecond);     // Use ms\n} \n```\n\n关于自定义参数需要注意的几点:\n\n*   我们将值 50、10，000 和 1，000，000 作为参数传递给基准。在`setup_fixture()`函数中创建向量时，它们被用作输入大小。使用测试功能中的`state.range(0)`可以访问这些值。\n*   默认情况下，Google Benchmark 只测量主线程上的 CPU 时间。但是由于我们对所有线程的总 CPU 时间感兴趣，所以我们使用`MeasureProcessCPUTime()`。\n*   谷歌基准测试决定每个测试需要重复多少次，直到达到统计稳定的结果。我们希望库为此使用挂钟时间，而不是 CPU 时间，因此我们应用设置`UseRealTime()`。\n\n差不多了。最后，注册基准并致电 main:\n\n```cpp\nBENCHMARK(bm_sequential)->Apply(CustomArguments);\nBENCHMARK(bm_parallel)->Apply(CustomArguments);\nBENCHMARK_MAIN(); \n```\n\n在打开优化(使用带有-O3 的 gcc)的情况下编译了这段代码之后，我在一台使用八个内核的笔记本电脑上执行了这个基准测试。下表显示了使用 50 个元素时的结果:\n\n<colgroup><col> <col> <col> <col></colgroup> \n| 算法 | 中央处理器 | 时间 | 加速 |\n| `std::transform()` | 0.02 毫秒 | 0.02 毫秒 | 0.25 倍 |\n| `par_transform_naive()` | 0.17 毫秒 | 0.08 毫秒 |\n\n*CPU* 是花在 CPU 上的总时间。*时间*是挂钟时间，也是我们最感兴趣的。*加速*是比较顺序版本和并行版本经过的时间时的相对加速(本例中为 0.02/0.08)。\n\n显然，对于这个只有 50 个元素的小数据集，顺序版本优于并行算法。有了 10，000 个元素，我们才真正开始看到并行化的好处:\n\n<colgroup><col> <col> <col> <col></colgroup> \n| 算法 | 中央处理器 | 时间 | 加速 |\n| `std::transform()` | 0.89 毫秒 | 0.89 毫秒 | 4.5 倍 |\n| `par_transform_naive()` | 1.95 毫秒 | 0.20 毫秒 |\n\n最后，使用 1，000，000 个元素会给我们带来更高的效率，如下表所示:\n\n<colgroup><col> <col> <col> <col></colgroup> \n| 算法 | 中央处理器 | 时间 | 加速 |\n| `std::transform()` | 9071 毫秒 | 9092 毫秒 | 7.3 倍 |\n| `par_transform_naive()` | 9782 毫秒 | 1245 毫秒 |\n\n这次最后一次运行的并行算法效率真的很高。它在八个内核上执行，因此效率为 7.3x/8 = 0.925。这里给出的结果(绝对执行时间和相对加速)不应该太依赖。其中，结果取决于计算机体系结构、操作系统调度程序以及执行测试时机器上当前正在运行的其他工作。然而，基准测试结果证实了前面讨论的几个要点:\n\n*   对于小数据集，顺序版本`std::transform()`比并行版本快得多，因为创建线程等会产生开销。\n*   与`std::transform()`相比，并行版本总是使用更多的计算资源(CPU 时间)。\n*   对于大型数据集，当测量挂钟时间时，并行版本优于顺序版本。在八核机器上运行时，加速比超过 7 倍。\n\n我们的算法效率高(至少在大数据集上)的一个原因是计算成本分布均匀，每个子任务都是高度独立的。然而，情况并非总是如此。\n\n#### 幼稚实现的缺点\n\n如果每个工作块都有相同的计算成本，并且算法在没有其他应用使用硬件的环境中执行，那么简单的实现可能会做得很好。然而，情况很少如此；相反，我们想要一个既高效又可扩展的好的通用并行实现。\n\n下面的插图显示了我们想要避免的问题。如果每个区块的计算成本不相等，则实现仅限于耗时最多的区块:\n\n<figure class=\"mediaobject\">![](img/B15619_14_02.png)</figure>\n\n图 14.2:计算时间与块大小不成比例的可能情况\n\n如果应用和/或操作系统有其他进程要处理，操作将不会并行处理所有区块:\n\n<figure class=\"mediaobject\">![](img/B15619_14_03.png)</figure>\n\n图 14.3:计算时间与块大小成比例的可能情况\n\n在*图 14.3* 中可以看到，将操作拆分成更小的块，使得并行化调整到当前条件，避免了单个任务拖住整个操作。\n\n还要注意，对于小数据集，天真的实现是不成功的。有许多方法可以调整幼稚的实现，以获得更好的性能。例如，我们可以通过将内核数量乘以某个大于 1 的因子来创建更多任务和更小的任务。或者，为了避免小数据集上的大量开销，我们可以让块大小决定要创建的任务数量等。\n\n现在，您已经了解了如何实现和评估一个简单的并行算法。我们不会对幼稚的实现进行任何微调；相反，我将展示一种在实现并行算法时使用的不同的有用技术。\n\n### 分步解决\n\n将问题划分为更小子问题的算法技术称为**分治**。我们将在这里使用分治法实现另一个版本的并行变换算法。其工作原理如下:如果输入范围小于指定的阈值，则处理该范围；否则，范围将被分成两部分:\n\n*   第一部分在新分支的任务上处理\n*   另一部分在调用线程中递归处理\n\n下图显示了分治算法如何使用以下数据和参数递归转换一个范围:\n\n*   范围大小:16\n*   源范围包含从 1.0 到 16.0 的浮动\n*   块大小:4\n*   变换函数:`[](auto x) { return x*x; }`\n\n<figure class=\"mediaobject\">![](img/B15619_14_04.png)</figure>\n\n图 14.4:一个范围被递归划分用于并行处理。源数组包含从 1.0 到 8.0 的浮点值。目标数组包含转换后的值。\n\n在*图 14.4* 中，你可以看到主任务生成了两个异步任务(**任务 1** 和**任务 2** )最后对范围内的最后一个组块进行变换。**任务 1** 生成**任务 3** ，然后变换剩余的包含值 5.0、6.0、7.0 和 8.0 的元素。让我们开始实施。\n\n#### 履行\n\n就实现而言，这是相当小的一部分代码。传入的范围被递归地分成两个块；第一个块作为新任务调用，第二个块在同一任务上递归处理:\n\n```cpp\ntemplate <typename SrcIt, typename DstIt, typename Func>\nauto par_transform(SrcIt first, SrcIt last, DstIt dst,\n                   Func func, size_t chunk_sz) {\n  const auto n = static_cast<size_t>(std::distance(first, last));\n  if (n <= chunk_sz) {\n    std::transform(first, last, dst, func);\n    return;\n  }\n  const auto src_middle = std::next(first, n / 2);\n  // Branch of first part to another task\n  auto future = std::async(std::launch::async, [=, &func] {\n    par_transform(first, src_middle, dst, func, chunk_sz);\n  });\n  // Recursively handle the second part\n  const auto dst_middle = std::next(dst, n / 2);\n  par_transform(src_middle, last, dst_middle, func, chunk_sz);\n  future.wait(); \n} \n```\n\n像这样将递归和多线程结合在一起可能需要一段时间才能理清思路。在下面的例子中，您将会看到在实现更复杂的算法时也可以使用这种模式。但首先，让我们看看它的表现。\n\n#### 性能赋值\n\n为了评估我们的新版本，我们将通过更新转换功能来修改基准夹具，根据输入值，该版本需要更多的时间。输入值的范围将通过使用`std::iota()`填充该范围来增加。这样做意味着算法需要处理不同大小的作业。以下是新的`setup_fixture()`功能:\n\n```cpp\nauto setup_fixture(int n) {\n  auto src = std::vector<float>(n);\n  std::iota(src.begin(), src.end(), 1.0f);  // From 1.0 to n\n  auto dst = std::vector<float>(src.size());\n  auto transform_function = [](float v) { \n    auto sum = v;\n    auto n = v / 20'000;                  // The larger v is, \n    for (auto i = 0; i < n; ++ i) {        // the more to compute\n      sum += (i * i * i * sum);\n    }\n    return sum;\n  };\n  return std::tuple{src, dst, transform_function};\n} \n```\n\n现在，我们可以尝试通过使用块大小的递增参数来找到分治算法要使用的最佳块大小。看看我们的分而治之算法与这个新夹具上的天真版本相比表现如何也将是有趣的，这个新夹具需要处理不同大小的作业。下面是完整的代码:\n\n```cpp\n// Divide and conquer version\nvoid bm_parallel(benchmark::State& state) {\n  auto [src, dst, f] = setup_fixture(10'000'000);\n  auto n = state.range(0);        // Chunk size is parameterized\n  for (auto _ : state) {\n    par_transform(src.begin(), src.end(), dst.begin(), f, n);\n  }\n}\n// Naive version\nvoid bm_parallel_naive(benchmark::State& state) {\n  auto [src, dst, f] = setup_fixture(10'000'000);\n  for (auto _ : state) {\n    par_transform_naive(src.begin(), src.end(), dst.begin(), f);\n  }\n}\nvoid CustomArguments(benchmark::internal::Benchmark* b) {\n  b->MeasureProcessCPUTime()\n    ->UseRealTime()\n    ->Unit(benchmark::kMillisecond);\n}\nBENCHMARK(bm_parallel)->Apply(CustomArguments)\n  ->RangeMultiplier(10)           // Chunk size goes from \n  ->Range(1000, 10'000'000);      // 1k to 10M\nBENCHMARK(bm_parallel_naive)->Apply(CustomArguments);\nBENCHMARK_MAIN(); \n```\n\n下图显示了我在使用八核英特尔酷睿 i7 CPU 的 macOS 上运行测试时所取得的结果:\n\n<figure class=\"mediaobject\">![](img/B15619_14_05.png)</figure>\n\n图 14.5:我们的朴素算法和使用不同块大小的分治算法之间的比较\n\n当使用大约 10，000 个元素的块时，达到了最佳效率，这创建了 1，000 个任务。对于较大的块，性能在处理最终块所需的时间上受到瓶颈，而与计算相比，过小的块导致创建和调用任务的开销过大。\n\n这个例子的一个要点是，调度 1000 个小任务而不是几个大任务的性能损失在这里不是问题。使用线程池限制线程数量是可能的，但是`std::async()`在这种情况下似乎工作得相当好。通用实现会选择使用相当多的任务，而不是试图匹配内核的确切数量。\n\n在实现并行算法时，找到块大小和任务数量的最佳值是一个真正的问题。如您所见，这取决于许多变量，也取决于是否针对延迟或吞吐量进行了优化。获得洞察力的最好方法是在你的算法应该运行的环境中进行测量。\n\n现在，您已经学习了如何使用分治法实现并行转换算法，让我们看看如何将相同的技术应用于其他问题。\n\n## 实现并行标准::计数 _if()\n\n分治法的一个好处是它可以应用于许多问题。我们可以很容易地使用相同的技术来实现`std::count_if()`的并行版本，不同的是我们需要累加返回值，如下所示:\n\n```cpp\ntemplate <typename It, typename Pred> \nauto par_count_if(It first, It last, Pred pred, size_t chunk_sz) { \n  auto n = static_cast<size_t>(std::distance(first, last)); \n  if (n <= chunk_sz) \n    return std::count_if(first, last, pred);\n  auto middle = std::next(first, n/2); \n  auto fut = std::async(std::launch::async, [=, &pred] { \n    return par_count_if(first, middle, pred, chunk_sz); \n  }); \n  auto num = par_count_if(middle, last, pred, chunk_sz); \n  return num + fut.get(); \n} \n```\n\n如您所见，这里唯一的区别是我们需要在函数的末尾对结果求和。如果你想让块的大小取决于核心的数量，你可以很容易地将`par_count_if()`包装在一个外部函数中:\n\n```cpp\ntemplate <typename It, typename Pred> \nauto par_count_if(It first, It last, Pred pred) { \n  auto n = static_cast<size_t>(std::distance(first, last));\n  auto n_cores = size_t{std::thread::hardware_concurrency()};\n  auto chunk_sz = std::max(n / n_cores * 32, size_t{1000});\n\n  return par_count_if(first, last, pred, chunk_sz);\n} \n```\n\n这里的神奇数字 32 是一个有些任意的因素，如果给我们一个大的输入范围，它会给我们更多的块和更小的块。像往常一样，我们需要测量性能，以得出一个好的常数。现在让我们继续，尝试解决一个更复杂的并行算法。\n\n## 实现并行标准::copy_if()\n\n我们已经看了`std::transform()`和`std::count_if()`，它们很容易顺序和并行实现。如果我们采用另一种很容易按顺序实现的算法`std::copy_if()`，并行执行就会困难得多。\n\n依次实现`std::copy_if()`就这么简单:\n\n```cpp\ntemplate <typename SrcIt, typename DstIt, typename Pred> \nauto copy_if(SrcIt first, SrcIt last, DstIt dst, Pred pred) { \n  for (auto it = first; it != last; ++ it) { \n    if (pred(*it)) { \n      *dst = *it; \n      ++ dst;\n    }\n  }\n  return dst;\n} \n```\n\n为了演示如何使用它，请考虑以下示例，其中我们有一个包含整数序列的范围，并且我们只想将奇数复制到另一个范围中:\n\n```cpp\nconst auto src = {1, 2, 3, 4}; \nauto dst = std::vector<int>(src.size(), -1); \nauto new_end = std::copy_if(src.begin(), src.end(), dst.begin(), \n                            [](int v) { return (v % 2) == 1; }); \n// dst is {1, 3, -1, -1}\ndst.erase(new_end, dst.end()); // dst is now {1, 3} \n```\n\n现在，如果我们想制作`copy_if()`的并行版本，我们会立即遇到问题，因为我们不能同时写入目标迭代器。这是一次行为未定义的失败尝试，因为两个任务将写入目标范围内的相同位置:\n\n```cpp\n// Warning: Undefined behavior\ntemplate <typename SrcIt, typename DstIt, typename Func> \nauto par_copy_if(SrcIt first, SrcIt last, DstIt dst, Func func) { \n  auto n = std::distance(first, last);\n  auto middle = std::next(first, n / 2); \n  auto fut0 = std::async([=]() { \n    return std::copy_if(first, middle, dst, func); }); \n  auto fut1 = std::async([=]() { \n    return std::copy_if(middle, last, dst, func); });\n  auto dst0 = fut0.get();\n  auto dst1 = fut1.get();\n  return *std::max(dst0, dst1); // Just to return something...\n} \n```\n\n我们现在有两种简单的方法:要么同步我们写入的索引(通过使用原子/无锁变量)，要么将算法分成两部分。接下来我们将探讨这两种方法。\n\n### 方法 1:使用同步写入位置\n\n我们可能考虑的第一种方法是通过使用原子`size_t`和`fetch_add()`成员函数来同步写位置，正如您在*第 11 章*、*并发*中所了解的。每当一个线程试图写一个新元素时，它获取当前索引并自动添加一个；因此，每个值都被写入一个唯一的索引。\n\n在我们的代码中，我们将算法分成两个函数:内部函数和外部函数。原子写索引将在外部函数中定义，而算法的主要部分将在内部函数中实现。\n\n#### 内部功能\n\n内部的函数需要一个原子`size_t`来同步写位置。由于算法是递归的，不能存储原子`size_t`本身；它需要一个外部函数来调用算法:\n\n```cpp\ntemplate <typename SrcIt, typename DstIt, typename Pred>\nvoid inner_par_copy_if_sync(SrcIt first, SrcIt last, DstIt dst,\n                            std::atomic_size_t& dst_idx,\n                            Pred pred, size_t chunk_sz) {\n  const auto n = static_cast<size_t>(std::distance(first, last));\n  if (n <= chunk_sz) {\n    std::for_each(first, last, [&](const auto& v) {\n      if (pred(v)) {\n        auto write_idx = dst_idx.fetch_add(1);\n        *std::next(dst, write_idx) = v;\n      }\n    });\n    return;\n  }\n  auto middle = std::next(first, n / 2);\n  auto future = std::async([first, middle, dst, chunk_sz, &pred, &dst_idx] {\n    inner_par_copy_if_sync(first, middle, dst, dst_idx, pred, chunk_sz);\n  });\n  inner_par_copy_if_sync(middle, last, dst, dst_idx, pred, chunk_sz);\n  future.wait();\n} \n```\n\n这仍然是一个各个击破的算法，希望你会开始看到我们正在使用的模式。写索引`dst_idx`的原子更新确保了多个线程永远不会写入目标序列中的同一个索引。\n\n#### 外部函数\n\n从客户端代码调用的外部函数只是原子`size_t`的占位符，它被初始化为零。然后，该函数初始化内部函数，从而进一步并行化代码:\n\n```cpp\ntemplate <typename SrcIt, typename DstIt, typename Pred>\nauto par_copy_if_sync(SrcIt first,SrcIt last,DstIt dst,\n                      Pred p, size_t chunk_sz) {\n  auto dst_write_idx = std::atomic_size_t{0};\n  inner_par_copy_if_sync(first, last, dst, dst_write_idx, p, chunk_sz);\n  return std::next(dst, dst_write_idx);\n} \n```\n\n一旦内部函数返回，我们可以使用`dst_write_idx`来计算目标范围的结束迭代器。现在让我们看看解决同样问题的另一种方法。\n\n### 方法 2:将算法分成两部分\n\n第二种方法是将算法分成两部分。首先，在并行块中执行条件复制，然后将得到的稀疏范围压缩为连续范围。\n\n#### 第一部分–将元素并行复制到目标范围\n\n第一部分以块的形式复制元素，从而产生稀疏的目标数组，如图 14.6*所示。每个块都有条件地并行复制，结果范围迭代器存储在`std::future`对象中，供以后检索:*\n\n<figure class=\"mediaobject\">![](img/B15619_14_06.png)</figure>\n\n图 14.6:条件复制第一步后的稀疏目标范围\n\n下面的代码实现了算法的前半部分:\n\n```cpp\ntemplate <typename SrcIt, typename DstIt, typename Pred>\nauto par_copy_if_split(SrcIt first, SrcIt last, DstIt dst, \n                       Pred pred, size_t chunk_sz) -> DstIt {\n  auto n = static_cast<size_t>(std::distance(first, last));\n  auto futures = std::vector<std::future<std::pair<DstIt, DstIt>>>{};\n  futures.reserve(n / chunk_sz);\n  for (auto i = size_t{0}; i < n; i += chunk_sz) {\n    const auto stop_idx = std::min(i + chunk_sz, n);\n    auto future = std::async([=, &pred] {\n      auto dst_first = dst + i;\n      auto dst_last = std::copy_if(first+i, first+stop_idx,                                   dst_first, pred);\n      return std::make_pair(dst_first, dst_last);\n    });\n    futures.emplace_back(std::move(future));\n  }\n  // To be continued ... \n```\n\n我们现在已经将元素(应该复制的)复制到稀疏目标范围中。是时候通过将元素向左移动来填补空白了。\n\n#### 第二部分–将稀疏范围按顺序移动到连续范围\n\n创建稀疏范围时，使用每个`std::future`的结果值进行合并。当零件重叠时，合并按顺序执行:\n\n```cpp\n // ...continued from above... \n  // Part #2: Perform merge of resulting sparse range sequentially \n  auto new_end = futures.front().get().second; \n  for (auto it = std::next(futures.begin()); it != futures.end(); ++ it)  { \n    auto chunk_rng = it->get(); \n    new_end = std::move(chunk_rng.first, chunk_rng.second, new_end);\n  } \n  return new_end; \n} // end of par_copy_if_split \n```\n\n算法的第二部分将所有子范围移动到范围的开头，如下图所示:\n\n<figure class=\"mediaobject\">![](img/B15619_14_07.png)</figure>\n\n图 14.7:将稀疏范围合并成连续范围\n\n随着两个算法解决同一个问题，是时候看看它们如何达标了。\n\n### 性能赋值\n\n使用这个并行版本的`copy_if()`带来的性能提升在很大程度上取决于谓词的开销。因此，我们在基准测试中使用了两个不同的谓词，计算成本不同。这里是*廉*谓词:\n\n```cpp\nauto is_odd = [](unsigned v) { \n  return (v % 2) == 1; \n}; \n```\n\n越昂贵的*谓词检查其参数是否是质数:*\n\n```cpp\nauto is_prime = [](unsigned v) {\n  if (v < 2) return false;\n  if (v == 2) return true;\n  if (v % 2 == 0) return false;\n  for (auto i = 3u; (i * i) <= v; i+=2) {\n    if ((v % i) == 0) {\n      return false; \n     }\n  }\n  return true;\n}; \n```\n\n注意，这并不是实现`is_prime()`的特别优化的方式，这里仅用于基准测试的目的。\n\n基准测试代码在这里没有详细说明，但是包含在附带的源代码中。比较了三种算法:`std::copy_if()`、`par_copy_if_split()`和`par_copy_if_sync()`。下图显示了使用英特尔酷睿 i7 处理器测量的结果。在这个基准测试中，并行算法使用的块大小为 100，000。\n\n<figure class=\"mediaobject\">![](img/B15619_14_08.png)</figure>\n\n图 14.8:条件复制策略与计算时间的关系\n\n衡量性能时最明显的观察是同步版本`par_copy_if_sync()`在使用廉价的`is_odd()`谓词时有多慢。灾难性的性能实际上不是由于原子写索引；相反，这是因为硬件的缓存机制由于几个线程写入同一缓存行而被破坏(正如您在*第 7 章*、*内存管理*中了解到的)。\n\n所以，有了这些知识，我们现在明白为什么`par_copy_if_split()`表现更好了。在便宜的谓词上，`is_odd()`、`par_copy_if_split()`比`std::copy_if()`快 2 倍左右，但是有了昂贵的`is_prime()`，效率几乎提高到 5 倍。效率的提高是因为在并行执行的算法的第一部分花费了大部分计算。\n\n您现在应该已经掌握了一些可以用于并行化算法的技术。当使用标准库中的并行算法时，这些新的见解将帮助您理解需求和期望。\n\n# 并行标准库算法\n\n从 C++ 17 开始，标准库已经用大多数(但不是全部)算法的并行版本进行了扩展。更改您的算法以允许并行执行只是添加一个参数，告诉算法使用哪个并行执行策略。\n\n正如本书前面所强调的，如果您的代码库基于标准的库算法，或者至少如果您有使用算法编写 C++ 的习惯，那么通过在合适的地方添加执行策略，您将几乎免费获得即时的性能提升:\n\n```cpp\nauto v = std::vector<std::string>{ \n  \"woody\", \"steely\", \"loopy\", \"upside_down\" \n};\n// Parallel sort\nstd::sort(std::execution::par, v.begin(), v.end()); \n```\n\n一旦您指定了执行策略，您就进入了并行算法的领域，与最初的顺序版本相比，并行算法有一些显著的不同。首先，最小迭代器类别要求从输入迭代器变为前向迭代器。其次，您的代码抛出的异常(从复制构造函数或传递给算法的函数对象)永远不会到达您这里。而是要求算法调用`std::terminate()`。第三，由于并行实现的复杂性增加，算法的复杂性保证(时间和内存)可能会放松。\n\n当使用标准库算法的并行版本时，您需要指定一个执行策略，说明如何允许算法并行执行。然而，实现可以决定顺序执行算法。如果您比较不同标准库实现中并行算法的效率和可伸缩性，您可以预期看到很大的差异。\n\n## 执行策略\n\n**执行策略**通知算法执行是否可以并行化以及如何并行化。标准库的并行扩展中包含四个默认的执行策略。编译器和第三方库可以针对某些硬件和条件扩展这些策略。例如，已经可以使用供应商特定的策略，从标准库算法中使用现代显卡的并行能力。\n\n执行策略在标题`<execution>`中定义，并位于名称空间`std::execution`中。目前有四种不同的标记类型，每个执行策略一种。您不能实例化这些类型；相反，每种类型都有一个预定义的对象。例如，并行执行策略有一个名为`std::execution::parallel_policy`的类型，该类型的预定义实例名为`std::execution::par`。每个策略有一个*类型*(而不是一个有多个预定义实例的类型)的原因是，您提供的策略可以在编译时由库来区分。\n\n### 顺序策略\n\n顺序执行策略`std::execution::seq`使算法以无并行性的顺序执行，类似于没有额外执行策略参数的算法的运行方式。然而，每当您指定一个执行策略时，这意味着您正在使用一个具有宽松复杂性保证和更严格迭代器要求的算法版本；它还假设你提供的代码没有抛出异常，否则算法会调用`std::terminate()`。\n\n### 平行政策\n\n并行执行策略`std::execution::par`可以认为是并行算法的标准执行策略。您提供给算法的代码需要是线程安全的。理解这个要求的一种方法是考虑你将要使用的算法的顺序版本中的循环体。例如，想一想`copy_if()`的顺序版本，我们在本章前面是这样拼写的:\n\n```cpp\ntemplate <typename SrcIt, typename DstIt, typename Pred> \nauto copy_if(SrcIt first, SrcIt last, DstIt dst, Pred pred) { \n  for (auto it = first; it != last; ++ it) \n  {                            // Start of loop body\n    if (pred(*it)) {           // Call predicate\n      *dst = *it;              // Copy construct \n      ++ dst;\n    }\n  }                            // End of loop body \n  return dst;\n} \n```\n\n在该算法中，循环体内部的代码将调用您提供的谓词，并对范围内的元素调用复制赋值运算符。如果您将`std::execution::par`传递给`copy_if()`，您有责任保证这些部分是线程安全的，并且可以安全地并行执行。\n\n让我们看一个提供不安全代码的例子，然后看看我们能做些什么。假设我们有一个字符串向量:\n\n```cpp\nauto v = std::vector<std::string>{\"Ada\", \"APL\" /* ... */ }; \n```\n\n如果我们想使用并行算法计算向量中所有字符串的总大小，一个不充分的方法是使用`std::for_each()`，如下所示:\n\n```cpp\nauto tot_size = size_t{0};\nstd::for_each(std::execution::par, v.begin(), v.end(),\n              [&](const auto& s) { \n  tot_size += s.size(); // Undefined behavior, data race!\n}); \n```\n\n由于函数对象的主体不是线程安全的(因为它从多个线程更新共享变量)，这段代码表现出未定义的行为。当然，我们可以用`std::mutex`来保护`tot_size`变量，但是这将会破坏并行执行这段代码的全部目的，因为互斥体一次只允许一个线程进入主体。使用`std::atomic`数据类型将是另一种选择，但这也会降低效率。\n\n这里的解决办法是*根本不用*来解决这个问题。相反，我们可以使用`std::transform_reduce()`或`std::reduce()`，它们是为这种工作量身定做的。以下是使用`std::reduce()`的方法:\n\n```cpp\nauto tot_size = std::reduce(std::execution::par, v.begin(), v.end(),                             size_t{0}, [](auto i, const auto& s) { \n  return i + s.size();   // OK! Thread safe\n}); \n```\n\n通过去掉 lambda 内部的可变引用，lambda 的主体现在是线程安全的。对`std::string`对象的`const`引用没问题，因为它从不变异任何字符串对象，因此不会引入任何数据竞争。\n\n通常，您传递给算法的代码是线程安全的，除非您的函数对象通过引用捕获对象或者具有其他副作用，例如写入文件。\n\n### 未排序策略\n\nC++ 20 中添加了未排序策略。它告诉算法，允许使用例如 SIMD 指令对循环进行矢量化。实际上，这意味着您不能在传递给算法的代码中使用任何同步原语，因为这可能会导致死锁。\n\n为了理解死锁是如何发生的，我们将回到前面不充分的例子，计算向量中所有字符串的总大小。假设我们不使用`std::reduce()`，而是通过添加互斥来保护`tot_size`变量，如下所示:\n\n```cpp\nauto v = std::vector<std::string>{\"Ada\", \"APL\" /* ... */ };\nauto tot_size = size_t{0};\nauto mut = std::mutex{};\nstd::for_each(std::execution::par, v.begin(), v.end(),\n              [&](const auto& s) { \n    auto lock = std::scoped_lock{mut}; // Lock\n    tot_size += s.size(); \n  }                                    // Unlock\n); \n```\n\n现在使用`std::execution::par`执行这段代码是安全的，但是效率非常低。如果我们将执行策略更改为`std::execution::unseq`，结果不仅是一个低效的程序，而且是一个有死锁风险的程序！\n\n未排序的执行策略告诉算法，它可能会以优化编译器通常不允许的方式对代码的指令重新排序。\n\n为了使算法受益于矢量化，它需要从输入范围读取多个值，然后一次将 SIMD 指令应用于多个值。让我们分析一下`for_each()`循环中的两次迭代会是什么样子，有没有重新排序。这里有两个没有任何重新排序的循环迭代:\n\n```cpp\n{ // Iteration 1\n  const auto& s = *it++ ;\n  mut.lock();\n  tot_size += s.size();\n  mut.unlock();\n}\n{ // Iteration 2\n  const auto& s = *it++ ;\n  mut.lock();\n  tot_size += s.size();\n  mut.unlock();\n} \n```\n\n算法允许以如下方式合并这两次迭代:\n\n```cpp\n{ // Iteration 1 & 2 merged\n  const auto& s1 = *it++ ;\n  const auto& s2 = *it++ ;\n  mut.lock();\n  mut.lock();                // Deadlock!\n  tot_size += s1.size();     // Replace these operations\n  tot_size += s2.size();     // with vectorized instructions\n  mut.unlock();\n  mut.unlock();\n} \n```\n\n试图在同一个线程上执行这段代码将会死锁，因为我们试图连续锁定同一个互斥体两次。换句话说，当使用`std::execution::unseq`策略时，您必须确保您提供给算法的代码不会获取任何锁。\n\n请注意，优化编译器可以随时对您的代码进行矢量化。然而，在这些情况下，就像编译器和硬件被允许执行的任何其他优化一样，由编译器来保证矢量化不会改变程序的含义。这里的不同之处在于，当向算法明确提供`std::execute::unseq`策略时，*保证您提供的代码是安全的，可以进行矢量化。*\n\n### 并行未排序策略\n\n并行未排序策略`std::execution::par_unseq`像并行策略一样并行执行算法，此外它还可以向量化循环。\n\n除了四个标准执行策略之外，标准库供应商还可以为您提供带有自定义行为的附加策略，并对输入设置其他约束。例如，英特尔并行 STL 库定义了四种只接受随机访问迭代器的自定义执行策略。\n\n## 异常处理\n\n如果您向算法提供四种标准执行策略之一，您的代码不得抛出异常，否则算法将调用`std::terminate()`。这与通常的单线程算法有很大区别，后者总是将异常传播回调用者:\n\n```cpp\nauto v = {1, 2, 3, 4};\nauto f = [](auto) { throw std::exception{}; };\ntry {\n  std::for_each(v.begin(), v.end(), f);\n} catch (...) {\n  std::cout << \"Exception caught\\n\";\n} \n```\n\n使用执行策略运行相同的代码会导致调用`std::terminate()`:\n\n```cpp\ntry {\n  std::for_each(std::execution::seq, v.begin(), v.end(), f);\n} catch (...) {\n  // The thrown std:::exception never reaches us.\n  // Instead, std::terminate() has been called \n} \n```\n\n你可能认为这意味着并行算法被声明为`noexcept`，但事实并非如此。很多并行算法需要分配内存，因此允许标准并行算法本身抛出`std::bad_alloc`。\n\n还应该说，其他库提供的执行策略可能会以不同的方式处理异常。\n\n现在，我们将继续讨论在 C++ 17 中首次引入并行算法时添加和修改的一些算法。\n\n## 对并行算法的添加和更改\n\n标准库中的大多数算法都是现成的并行版本。但是，也有一些值得注意的例外，包括`std::accumulate()`和`std::for_each()`，因为它们的原始规格要求按顺序执行。\n\n### 标准::累积()和标准::减少()\n\n`std::accumulate()`算法不能并行化，因为它必须按照元素的顺序执行，这是不可能并行化的。取而代之的是，添加了一个名为`std::reduce()`的新算法，它的工作原理与`std::accumulate()`类似，只是它的执行是无序的。\n\n对于交换运算，它们的结果是一样的，因为累加的顺序无关紧要。换句话说，给定一个整数范围:\n\n```cpp\nconst auto r = {1, 2, 3, 4}; \n```\n\n通过加法或乘法累加它们:\n\n```cpp\nauto sum = \n  std::accumulate(r.begin(), r.end(), 0, std::plus<int>{});\n\nauto product = \n  std::accumulate(r.begin(), r.end(), 1, std::multiplies<int>{}); \n```\n\n将产生与调用`std::reduce()`而不是`std::accumulate()`相同的结果，因为整数的加法和乘法都是可交换的。例如:\n\n<figure class=\"mediaobject\">![](img/B15619_14_004.png)</figure>\n\n但是，如果运算不可交换，结果就是*不确定*，因为它取决于参数的顺序。例如，如果我们要累积如下字符串列表:\n\n```cpp\nauto v = std::vector<std::string>{\"A\", \"B\", \"C\"};\nauto acc = std::accumulate(v.begin(), v.end(), std::string{});\nstd::cout << acc << '\\n'; // Prints \"ABC\" \n```\n\n该代码将始终产生字符串`\"ABC\"`。但是，通过使用`std::reduce()`，得到的字符串中的字符可以是任何顺序，因为字符串连接是不可交换的。换句话说，字符串`\"A\" + \"B\"`并不等于和`\"B\" + \"A\"`。因此，以下使用`std::reduce()`的代码可能会产生不同的结果:\n\n```cpp\nauto red = std::reduce(v.begin(), v.end(), std::string{}); \nstd::cout << red << '\\n'; \n// Possible output: \"CBA\" or \"ACB\" etc \n```\n\n与性能相关的一个有趣的点是浮点数学是不可交换的。通过对浮点值使用`std::reduce()`，结果可能会有所不同，但这也意味着`std::reduce()`可能比`std::accumulate()`快得多。这是因为`std::reduce()`被允许对操作进行重新排序，并以一种`std::accumulate()`在使用严格浮点数学时不被允许的方式利用 SIMD 指令。\n\n#### std::transform_reduce()\n\n作为标准库算法的补充，`std::transform_reduce()`也被添加到`<numeric>`头。它确实做到了它所说的:它将一系列元素转换为`std::transform()`，然后应用一个函数对象。这使它们无序累积，如`std::reduce()`:\n\n```cpp\nauto v = std::vector<std::string>{\"Ada\",\"Bash\",\"C++\"}; \nauto num_chars = std::transform_reduce( \n  v.begin(), v.end(), size_t{0}, \n  [](size_t a, size_t b) { return a + b; },     // Reduce\n  [](const std::string& s) { return s.size(); } // Transform \n); \n// num_chars is 10 \n```\n\n引入并行算法时，`std::reduce()`和`std::transform_reduce()`都被添加到 C++ 17 中。另一个必要的改变是调整`std::for_each()`的返回类型。\n\n### std::for_each()\n\n很少使用的属性`std::for_each()`是返回传递给它的函数对象。这使得使用`std::for_each()`在有状态函数对象中积累值成为可能。以下示例演示了一个可能的用例:\n\n```cpp\nstruct Func {\n  void operator()(const std::string& s) {\n    res_ += s;\n  };\n  std::string res_{};    // State\n};\nauto v = std::vector<std::string>{\"A\", \"B\", \"C\"};\nauto s = std::for_each(v.begin(), v.end(), Func{}).res_;\n// s is \"ABC\" \n```\n\n这种用法类似于我们使用`std::accumulate()`可以实现的，因此在尝试并行化它时也表现出相同的问题:无序执行函数对象会产生不确定的结果，因为调用顺序是未定义的。因此，`std::for_each()`的并行版本只是返回`void`。\n\n## 并行化基于索引的 for 循环\n\n尽管我建议使用算法，但有时特定任务需要原始的、基于索引的`for`循环。通过将算法`std::for_each()`包含在库中，标准库算法提供了基于范围的`for`循环的等价物。\n\n然而，没有等同于基于索引的`for`循环的算法。换句话说，我们不能简单地通过向代码添加并行策略来轻松地并行化代码:\n\n```cpp\nauto v = std::vector<std::string>{\"A\", \"B\", \"C\"};\nfor (auto i = 0u; i < v.size(); ++ i) { \n  v[i] += std::to_string(i+1); \n} \n// v is now { \"A1\", \"B2\", \"C3\" } \n```\n\n但是让我们看看如何通过结合算法来构建一个。正如你已经得出的结论，实现并行算法是复杂的。但是在这种情况下，我们将使用`std::for_each()`作为构建块来构建`parallel_for()`算法，从而将复杂的并行性留给`std::for_each()`。\n\n### 将 std::for_each()与 STD::view::iota()组合在一起\n\n基于标准库算法的基于索引的`for`循环可以通过将范围库中的`std::for_each()`与`std::views::iota()`相结合来创建(参见*第 6 章*、*范围和视图*)。它看起来是这样的:\n\n```cpp\nauto v = std::vector<std::string>{\"A\", \"B\", \"C\"};\nauto r = std::views::iota(size_t{0}, v.size()); \nstd::for_each(r.begin(), r.end(), [&v](size_t i) { \n  v[i] += std::to_string(i + 1); \n}); \n// v is now { \"A1\", \"B2\", \"C3\" } \n```\n\n然后可以通过使用并行执行策略进一步并行化:\n\n```cpp\nstd::for_each(std::execution::par, r.begin(), r.end(), [&v](size_t i) { \n  v[i] += std::to_string(i + 1); \n}); \n```\n\n如前所述，当传递对 lambda 的引用时，我们必须非常小心，lambda 将像这样从多个线程调用。通过仅通过唯一索引`i`访问向量元素，我们避免了在对向量中的字符串进行变异时引入数据竞争。\n\n### 通过包装简化结构\n\n为了使以简洁的语法迭代索引，前面的代码被包装到一个名为`parallel_for()`的实用函数中，如下所示:\n\n```cpp\ntemplate <typename Policy, typename Index, typename F>\nauto parallel_for(Policy&& p, Index first, Index last, F f) {\n  auto r = std::views::iota(first, last);\n  std::for_each(p, r.begin(), r.end(), std::move(f));\n} \n```\n\n`parallel_for()`功能模板可以这样直接使用:\n\n```cpp\nauto v = std::vector<std::string>{\"A\", \"B\", \"C\"};\nparallel_for(std::execution::par, size_t{0}, v.size(),\n              [&](size_t i) { v[i] += std::to_string(i + 1); }); \n```\n\n由于`parallel_for()`建立在`std::for_each()`之上，它接受`std::for_each()`接受的任何政策。\n\n我们将在这一章结束时简单介绍一下 GPU，以及它们现在和将来如何用于并行编程。\n\n# 在图形处理器上执行算法\n\n**图形** **处理单元**(**GPU**)最初设计用于处理计算机图形渲染的点和像素。简而言之，图形处理器所做的是检索像素数据或顶点数据的缓冲区，对每个缓冲区单独执行简单的操作，并将结果存储在新的缓冲区中(最终显示)。\n\n以下是一些可以在早期阶段在 GPU 上执行的简单、独立操作的示例:\n\n*   将点从世界坐标转换为屏幕坐标\n*   在特定点执行光照计算(通过光照计算，我指的是计算图像中特定像素的颜色)\n\n由于这些操作可以并行执行，图形处理器被设计成并行执行小操作。后来，这些图形操作变得可编程，尽管程序是根据计算机图形编写的(也就是说，内存读取是根据从纹理读取颜色来完成的，并且结果总是作为颜色写入纹理)。这些程序被称为**着色器**。\n\n随着时间的推移，引入了更多着色器类型的程序，着色器获得了越来越多的低级选项，例如从缓冲区读取和写入原始值，而不是从纹理读取和写入颜色值。\n\n从技术上讲，一个中央处理器通常由几个通用的缓存内核组成，而一个图形处理器由大量高度专业化的内核组成。这意味着可伸缩性好的并行算法非常适合在 GPU 上执行。\n\nGPU 有自己的内存，在算法可以在 GPU 上执行之前，CPU 需要在 GPU 内存中分配内存，并将数据从主内存复制到 GPU 内存。接下来发生的事情是，中央处理器在图形处理器上启动一个例程(也称为内核)。最后，中央处理器将数据从图形处理器内存复制回主内存，使其可用于在中央处理器上执行的“正常”代码。在中央处理器和图形处理器之间来回复制数据所产生的开销是图形处理器更适合批处理任务的原因之一，在批处理任务中，吞吐量比延迟更重要。\n\n现在有几个库和抽象层可以从 C++ 访问 GPU 编程，但是标准的 C++ 在这方面几乎什么都不提供。但是并行执行策略`std::execution::par`和`std::execution::par_unseq`允许编译器将标准算法的执行从 CPU 转移到 GPU。这方面的一个例子是 NVC++，英伟达高性能计算编译器。它可以被配置为编译标准的 C++ 算法，以便在 NVIDIA GPUs 上执行。\n\n如果你想了解更多关于 C++ 和 GPU 编程的现状，我强烈推荐王敏德([https://accu.org/video/spring-2019-day-3/wong/](https://accu.org/video/spring-2019-day-3/wong/))在 ACCU 2019 大会上的演讲*用现代 C++* 进行 GPU 编程。\n\n## 摘要\n\n在本章中，您已经了解了手工制作并行执行的算法的复杂性。您现在还知道如何分析、测量和调整并行算法的效率。您在学习并行算法时获得的见解将加深您对 C++ 标准库中并行算法的需求和行为的理解。C++ 自带四种标准执行策略，编译器厂商可以对其进行扩展。这为将图形处理器用于标准算法打开了大门。下一个 C++ 标准 C++ 23 很可能会增加对 GPU 并行编程的支持。\n\n你现在已经读到这本书的结尾了。恭喜你！性能是代码质量的一个重要方面。但是，性能往往以牺牲其他质量方面为代价，比如可读性、可维护性和正确性。掌握编写高效干净代码的艺术需要实践训练。我希望你已经从这本书中学到了一些东西，你可以在创建令人惊叹的软件时将其融入到你的日常生活中。\n\n解决绩效问题通常归结为愿意进一步调查事情。通常情况下，它需要充分了解硬件和底层操作系统，以便能够从测量数据中得出结论。当我觉得有必要的时候，这本书已经触及了这些方面的表面。在第二版中写了关于 C++ 20 的特性之后，我现在期待着开始在我作为软件开发人员的职业中使用这些特性。正如我之前提到的，本书中的许多代码目前只得到编译器的部分支持。我将继续更新 GitHub 存储库，并添加关于编译器支持的信息。祝你好运！\n\n**分享你的经历**\n\n谢谢你抽出时间来读这本书。如果你喜欢这本书，帮助别人找到它。在[https://www.amazon.com/dp/1839216549](https://www.amazon.com/dp/1839216549)留下评论。*"
  },
  {
    "path": "docs/cpp-hiperf/README.md",
    "content": "# C++ 高性能编程\n\n> 原书：[C++ High Performance](https://libgen.rs/book/index.php?md5=753C0F2773B6B78B5104ECB1B57442D4)\n> \n> 协议：[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)\n> \n> 阶段：机翻（1）\n> \n> 自豪地采用[谷歌翻译](https://translate.google.cn/)\n> \n> 让开源界提前感受入关。\n\n* [在线阅读](https://ccpp.apachecn.org)\n* [在线阅读（Gitee）](https://apachecn.gitee.io/apachecn-c-cpp-zh/)\n* [ApacheCN 学习资源](http://docs.apachecn.org/)\n\n## 贡献指南\n\n本项目需要校对，欢迎大家提交 Pull Request。\n\n> 请您勇敢地去翻译和改进翻译。虽然我们追求卓越，但我们并不要求您做到十全十美，因此请不要担心因为翻译上犯错——在大部分情况下，我们的服务器已经记录所有的翻译，因此您不必担心会因为您的失误遭到无法挽回的破坏。（改编自维基百科）\n\n## 联系方式\n\n### 负责人\n\n* [飞龙](https://github.com/wizardforcel): 562826179\n\n### 其他\n\n*   在我们的 [apachecn/apachecn-c-cpp-zh](https://github.com/apachecn/apachecn-c-cpp-zh) github 上提 issue.\n*   发邮件到 Email: `apachecn@163.com`.\n*   在我们的 [组织学习交流群](http://www.apachecn.org/organization/348.html) 中联系群主/管理员即可.\n\n## 赞助我们\n\n![](http://data.apachecn.org/img/about/donate.jpg)\n"
  },
  {
    "path": "docs/cpp-hiperf/SUMMARY.md",
    "content": "+   [C++ 高性能编程](README.md)\n+   [零、前言](00.md)\n+   [一、C++ 简介](01.md)\n+   [二、基本的 C++ 技术](02.md)\n+   [三、分析和测量性能](03.md)\n+   [四、数据结构](04.md)\n+   [五、算法](05.md)\n+   [六、范围和视图](06.md)\n+   [七、内存管理](07.md)\n+   [八、编译时编程](08.md)\n+   [九、基本工具](09.md)\n+   [十、代理对象和延迟求值](10.md)\n+   [十一、并发](11.md)\n+   [十二、协程和延迟生成器](12.md)\n+   [十三、使用协程的异步编程](13.md)\n+   [十四、并行算法](14.md)\n"
  },
  {
    "path": "docs/cpp-react-prog/00.md",
    "content": "# 零、前言\n\n这本书将帮助你学习如何用 C++ 实现反应式编程范式，并构建异步和并发应用。这本书包括真实世界的问题，你将使用反应式编程模型来解决。它强调了事件处理在编程世界中的发展方式。您将学习 C++ 中的语言级并发和函数式反应式编程。函数式编程和面向对象编程中的构造将使您能够编写高效的程序。之后，您将在 C++ 中学习微服务，并为`RxCpp`创建自定义操作符。\n\n# 这本书是给谁的\n\n如果你是一个对使用反应式编程来构建异步和并发应用感兴趣的 C++ 开发人员，你会发现这本书非常有用。这本书没有假设任何以前的反应式编程知识。\n\n# 这本书涵盖了什么\n\n[第 1 章](01.html#J2B80-51c8384cc2cb48e691b461190723b468)、*反应式编程模型–概述和历史*，介绍了 Rx 编程模型的一些关键数据结构。它还包括图形用户界面事件处理、反应式编程概述，以及在 MFC 上实现不同界面的图形用户界面版本。\n\n[第 2 章](02.html#12AK80-51c8384cc2cb48e691b461190723b468)、*现代 C++ 及其关键习惯用法之旅*，涵盖 C++ 特性、类型推断、变量模板、右值引用和移动语义、lambda 函数、初等函数编程、可移植运算符以及迭代器和观察器的实现。\n\n[第 3 章](03.html#1O8H60-51c8384cc2cb48e691b461190723b468)、*C++*中的语言级并发和并行，讨论了 c++ 标准中可用的线程库。您将学习如何启动和管理线程，并讨论线程库的不同方面。本章为现代 C++ 中引入的并发支持奠定了良好的基础。\n\n[第 4 章](04.html#27GQ60-51c8384cc2cb48e691b461190723b468)、*c++ 中的异步和无锁编程*，讨论了标准库为编写基于任务的并行性提供的设施。它还讨论了现代 C++ 语言提供的新的多线程感知内存模型。\n\n[第 5 章](05.html#2RHM00-51c8384cc2cb48e691b461190723b468)、*可观测物介绍*，讲述了 GoF 观察者模式，并说明了其不足。您将在表达式树建模的上下文中了解 GoF 复合/访问者模式。\n\n[第六章](06.html#352RK0-51c8384cc2cb48e691b461190723b468)*使用 C++* 的事件流编程介绍，重点介绍事件流编程的话题。我们还将查看 Streamulus 库，它提供了一种 DSEL 方法来处理事件流，后面还有几个程序。\n\n[第 7 章](07.html#3M85O0-51c8384cc2cb48e691b461190723b468)、*数据流计算和 RxCpp 库*的介绍，从数据流计算范式的概念概述开始，快速进入编写一些基本的`RxCpp`程序。您将了解`RxCpp`库支持的一组操作符。\n\n[第 8 章](08.html#49AH00-51c8384cc2cb48e691b461190723b468)*RxCPP–关键元素*，让您了解 Rx 编程模型的各个部分是如何结合在一起的。本章从 Observables 开始，接下来将介绍订阅机制和调度器实现。\n\n[第 9 章](09.html#4U9TC0-51c8384cc2cb48e691b461190723b468)、*使用 Qt/C++* 进行反应式图形用户界面编程，涉及使用 Qt 进行反应式图形用户界面编程的主题。您将了解 Qt 框架中的概念，例如 Qt 对象层次结构、元对象系统以及信号和槽。然后，您将编写一个应用来处理鼠标事件并过滤它们。在此之后，你还将学习如何在`RxCpp`中创建自定义反应操作符的高级主题，如果现有的一组操作符不足以满足目的的话。本主题还通过组合现有运算符来帮助您创建复合运算符。本书中没有这个主题，但可以在[https://www . packtpub . com/sites/default/files/downloads/Creating _ Custom _ Operators _ in _ rxcpp . pdf](https://www.packtpub.com/sites/default/files/downloads/Creating_Custom_Operators_in_RxCpp.pdf)下载。\n\n[第十章](10.html#5GDO20-51c8384cc2cb48e691b461190723b468)、*c++ Rx 编程的设计模式和习惯用法*，深究设计模式和习惯用法的奇妙世界。从 GOF 设计模式开始，我们将转向反应式编程模式。\n\n[第 11 章](11.html#5TOVU0-51c8384cc2cb48e691b461190723b468)、*使用 C++* 的反应式微服务，讲述了如何使用 Rx 编程模型使用 C++ 编写反应式微服务。它向您介绍了微软 C++ REST SDK 及其编程模型。\n\n[第 12 章](12.html#6FSQK0-51c8384cc2cb48e691b461190723b468)、*高级流和处理错误*讨论了`RxCpp`中的错误处理，以及处理`RxCpp`库中流的一些高级构造和操作符。我们将讨论当错误出现时如何继续流，如何等待流的生产者纠正错误并继续序列，以及如何执行适用于成功和错误路径的常见操作。\n\n# 充分利用这本书\n\n为了跟上这本书的主题，你需要有 C++ 编程的知识。\n\n# 下载示例代码文件\n\n你可以从你在[www.packtpub.com](http://www.packtpub.com)的账户下载这本书的示例代码文件。如果您在其他地方购买了这本书，您可以访问[www.packtpub.com/support](http://www.packtpub.com/support)并注册将文件直接通过电子邮件发送给您。\n\n您可以按照以下步骤下载代码文件:\n\n1.  登录或注册[www.packtpub.com](http://www.packtpub.com/support)。\n2.  选择“支持”选项卡。\n3.  点击代码下载和勘误表。\n4.  在搜索框中输入图书的名称，并按照屏幕指示进行操作。\n\n下载文件后，请确保使用最新版本的解压缩文件夹:\n\n*   视窗系统的 WinRAR/7-Zip\n*   zipeg/izp/un ARX for MAC\n*   适用于 Linux 的 7-Zip/PeaZip\n\n这本书的代码包也托管在 https://github.com/PacktPublishing/CPP-Reactive-Programming 的 GitHub 上。如果代码有更新，它将在现有的 GitHub 存储库中更新。\n\n我们还有来自丰富的图书和视频目录的其他代码包，可在**[【https://github.com/PacktPublishing/】](https://github.com/PacktPublishing/)**获得。看看他们！\n\n# 下载彩色图像\n\n我们还提供了一个 PDF 文件，其中包含本书中使用的截图/图表的彩色图像。你可以在这里下载:[https://www . packtpub . com/sites/default/files/downloads/CPPReactiveProgramming _ color images . pdf](https://www.packtpub.com/sites/default/files/downloads/CPPReactiveProgramming_ColorImages.pdf)。\n\n# 使用的约定\n\n本书通篇使用了许多文本约定。\n\n`CodeInText`:表示文本中的码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟网址、用户输入和推特句柄。这里有一个例子:“前面的代码片段用窗口的必要模板初始化了一个名为`WNDCLASS`(或者现代系统中的`WNDCLASSEX`)的结构。”\n\n代码块设置如下:\n\n```cpp\n/* close connection to server */\nXCloseDisplay(display);\n\nreturn 0;\n}\n```\n\n当我们希望将您的注意力吸引到代码块的特定部分时，相关的行或项目以粗体显示:\n\n```cpp\n/* close connection to server */\nXCloseDisplay(display);\n\nreturn 0;\n}\n```\n\n任何命令行输入或输出都编写如下:\n\n```cpp\n$ mkdir css\n$ cd css\n```\n\n**粗体**:表示一个新的术语、一个重要的单词或者你在屏幕上看到的单词。例如，菜单或对话框中的单词像这样出现在文本中。这里有一个例子:“用开窗的说法，它被称为**消息**循环。”\n\nWarnings or important notes appear like this. Tips and tricks appear like this.\n\n# 取得联系\n\n我们随时欢迎读者的反馈。\n\n**综合反馈**:发邮件`feedback@packtpub.com`并在邮件主题中提及书名。如果您对本书的任何方面有疑问，请发电子邮件至`questions@packtpub.com`。\n\n**勘误表**:虽然我们已经尽了最大的努力来保证内容的准确性，但是错误还是会发生。如果你在这本书里发现了一个错误，如果你能向我们报告，我们将不胜感激。请访问[www.packtpub.com/submit-errata](http://www.packtpub.com/submit-errata)，选择您的图书，点击勘误表提交链接，并输入详细信息。\n\n**盗版**:如果您在互联网上遇到任何形式的我们作品的非法拷贝，如果您能提供我们的位置地址或网站名称，我们将不胜感激。请通过`copyright@packtpub.com`联系我们，并提供材料链接。\n\n**如果你有兴趣成为一名作者**:如果有一个你有专长的话题，你有兴趣写或者投稿一本书，请访问[authors.packtpub.com](http://authors.packtpub.com/)。\n\n# 复习\n\n请留下评论。一旦你阅读并使用了这本书，为什么不在你购买它的网站上留下评论呢？然后，潜在的读者可以看到并使用您不带偏见的意见来做出购买决定，我们在 Packt 可以了解您对我们产品的看法，我们的作者可以看到您对他们的书的反馈。谢谢大家！\n\n更多关于 Packt 的信息，请访问[packtpub.com](https://www.packtpub.com/)。"
  },
  {
    "path": "docs/cpp-react-prog/01.md",
    "content": "# 一、反应式编程模型——概述和历史\n\nX Windows 系统、微软 Windows 和 IBM OS/2 演示管理器使 GUI 编程在 PC 平台上流行起来。这是对之前存在的字符模式用户界面和批处理风格编程模型的重大转变。对事件的响应成为全球软件开发人员的主要关注点，平台供应商求助于创建基于 C 的低级 API，这些 API 依赖于函数指针和回调来使程序员能够处理事件。编程模型大多基于协作多线程模型，随着更好的微处理器的出现，大多数平台开始支持先发制人的多线程。处理事件(和其他异步任务)变得更加复杂，以传统方式响应事件变得不太可扩展。尽管出现了优秀的基于 C++ 的图形用户界面工具包，但事件处理主要是使用消息标识、基于函数指针的调度和其他低级技术来完成的。一家著名的编译器供应商甚至尝试在 C++ 语言中添加语言扩展，以实现更好的 Windows 编程。处理事件、异步和相关的问题需要重新看待问题。幸运的是，现代 C++ 标准支持函数式编程、语言级并发(带有内存模型)和更好的内存管理技术，使程序员能够处理异步数据流(通过将事件视为流)。这是使用称为反应式编程的编程模型来实现的。为了客观地看待问题，本章将概述以下主题:\n\n*   事件驱动编程模型及其在各种平台上的实现。\n*   什么是反应式编程？\n*   反应式编程的不同模型。\n*   一些简单的程序，使概念理解更好。\n*   我们书的哲学。\n\n# 事件驱动编程模型\n\n事件驱动编程是一种编程模型，其中流程控制由事件决定。事件的例子有鼠标点击、按键、手势、传感器数据、来自其他程序的消息等等。一个事件驱动的应用有一种机制，可以在接近实时的基础上检测事件，并通过调用适当的事件处理过程来响应或反应它们。由于早期的大部分事件处理程序都是用 C/C++ 编写的，因此它们采用了回调(使用函数指针)等低级技术来编写这些事件处理程序。后来的系统，如 Visual Basic、Delphi 和其他快速应用开发工具，确实增加了对事件驱动编程的本地支持。为了更清楚地说明问题，我们将参观各个平台的事件处理机制。这将帮助读者理解反应式编程模型正在解决的问题(从图形用户界面编程上下文)。\n\nReactive programming treats data as streams and events in windowing systems can be treated as streams to be processed in a uniform manner. The Reactive programming model provides support for gathering events from different sources as streams, filtering streams, the transformation of streams, performing actions on streams, and so on. The programming model handles asynchrony, scheduling details as part of the framework. This chapter is mostly based on the key data structures of the Reactive programming model and how we can implement basic Reactive programs. In an industrial-strength reactive program, the code written will be asynchronous and the examples from this chapter are synchronous. We give the necessary background information and language constructs in the following chapters before out of order execution and schedules are discussed. These implementations are here for elucidation and can be treated as learning examples.\n\n# 事件驱动编程\n\nX Windows 编程模型是一个跨平台的 API，大部分在 POSIX 系统上支持，甚至已经移植到微软 Windows。事实上，X 是一个网络窗口协议，它需要一个窗口管理器来管理窗口堆栈。屏幕内容由 X 服务器管理，客户端库将提取内容并在本地机器上显示。在桌面环境中，服务器在同一台机器上本地运行。以下程序将帮助读者理解 XLib 编程模型的要点以及事件在平台中是如何处理的:\n\n```cpp\n#include <X11/Xlib.h>\n#include <stdio.h>\n#include <stdlib.h>\n#include <string.h>\n\nint main(void)\n{\n    Display *display;\n    Window window;\n    XEvent event;\n    char *msg = \"Hello, World!\";\n    int s;\n```\n\n前面的代码片段包含了正确的头文件，程序员应该包含这些头文件来获取 XLib C 库提供的函数原型。程序员从头开始编写 XLib 程序时，应该了解一些数据结构。如今，人们使用 Qt、WxWidgets、Gtk+、Fox 工具包等库来编写商业质量的 X 程序。\n\n```cpp\n    /* open connection with the server */\n    display = XOpenDisplay(NULL);\n    if (display == NULL){\n        fprintf(stderr, \"Cannot open display\\n\");\n        exit(1);\n    }\n    s = DefaultScreen(display);\n    /* create window */\n    window = XCreateSimpleWindow(display,\n             RootWindow(display, s), 10, 10, 200, 200, 1,\n             BlackPixel(display, s), WhitePixel(display, s));\n\n    /* select kind of events we are interested in */\n    XSelectInput(display, window, ExposureMask | KeyPressMask);\n\n    /* map (show) the window */\n    XMapWindow(display, window);\n```\n\n前面的代码片段初始化了服务器，并按照特定的规范创建了一个窗口。传统上，大多数视窗操作系统程序运行在一个管理级联窗口的窗口管理器下。在显示窗口之前，我们通过调用`XSelectInput` API 调用来选择我们感兴趣的消息:\n\n```cpp\n    /* event loop */\n    for (;;)\n    {\n        XNextEvent(display, &event);\n\n        /* draw or redraw the window */\n        if (event.type == Expose)\n        {\n            XFillRectangle(display, window,\n                DefaultGC(display, s), 20, 20, 10, 10);\n            XDrawString(display, window,\n                DefaultGC(display, s), 50, 50, msg, strlen(msg));\n        }\n        /* exit on key press */\n        if (event.type == KeyPress)\n        break;\n    }\n```\n\n然后，程序进入无限循环，同时轮询任何事件，适当的 Xlib 应用编程接口将用于在窗口上绘制一个字符串。用窗口术语来说，它被称为**消息**循环。事件的检索将通过`XNextEvent`应用编程接口调用完成:\n\n```cpp\n    /* close connection to server */\n    XCloseDisplay(display);\n\n    return 0;\n    }\n```\n\n一旦我们脱离了无限消息循环，到服务器的连接将被关闭。\n\n# 微软视窗上的事件驱动编程\n\n微软公司创造了一个图形用户界面编程模型，可以被认为是世界上最成功的窗口系统。第三版的视窗软件获得了巨大的成功(1990 年)，微软随后推出了视窗 NT 和视窗 95/98/ME 系列。让我们来看看微软视窗的事件驱动编程模型(请参考微软文档，详细了解这种编程模型是如何工作的)。以下程序将帮助我们理解使用 C/C++ 编写 Windows 编程的要点:\n\n```cpp\n#include <windows.h>\n//----- Prtotype for the Event Handler Function\nLRESULT CALLBACK WndProc(HWND hWnd, UINT message,\n                         WPARAM wParam, LPARAM lParam);\n//--------------- Entry point for a Idiomatic Windows API function\nint WINAPI WinMain(HINSTANCE hInstance,\n              HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)\n{\n\nMSG msg = {0};\nWNDCLASS wc = {0};\nwc.lpfnWndProc = WndProc;\nwc.hInstance = hInstance;\nwc.hbrBackground = (HBRUSH)(COLOR_BACKGROUND);\nwc.lpszClassName = \"minwindowsapp\";\nif( !RegisterClass(&wc) )\n  return 1;\n```\n\n前面的代码片段用窗口的必要模板初始化了一个名为`WNDCLASS`(现代系统为`WNDCLASSEX`)的结构。结构中最重要的字段是`lpfnWndProc`，它是响应此窗口实例中事件的函数的地址:\n\n```cpp\nif( !CreateWindow(wc.lpszClassName,\n                  \"Minimal Windows Application\",\n                  WS_OVERLAPPEDWINDOW|WS_VISIBLE,\n                  0,0,640,480,0,0,hInstance,NULL))\n    return 2;\n```\n\n我们将调用`CreateWindow`(或现代系统上的`CreateWindowEx`)应用编程接口调用，根据`WNDCLASS.lpszClassname`参数中提供的类名创建一个窗口:\n\n```cpp\n    while( GetMessage( &msg, NULL, 0, 0 ) > 0 )\n        DispatchMessage( &msg );\n    return 0;\n}\n```\n\n前面的代码片段进入了一个无限循环，消息将从消息队列中检索，直到我们得到一条`WM_QUIT`消息。`WM_QUIT`信息将我们带出无限循环。在调用`DispatchMessage`应用编程接口调用之前，消息有时会被翻译。`DispatchMessage`调用窗口回调程序(`lpfnWndProc`):\n\n```cpp\nLRESULT CALLBACK WndProc(HWND hWnd, UINT message,\n                         WPARAM wParam, LPARAM lParam) {\nswitch(message){\n  case WM_CLOSE:\n    PostQuitMessage(0);break;\n  default:\n    return DefWindowProc(hWnd, message, wParam, lParam);\n}\nreturn 0;\n}\n```\n\n前面的代码片段是一个极简`callback`函数。您可以参考微软文档来了解 Windows API 编程以及在这些程序中如何处理事件\n\n# Qt 下的事件驱动编程\n\nQt 框架是一个工业级、跨平台和多平台的图形用户界面工具包，运行在视窗、GNU Linux、macOS X 和其他苹果系统上。该工具包已被编译成嵌入式系统和移动设备。C++ 编程模型利用了一种叫做**元对象编译器** ( **MOC** )的东西，它将仔细阅读指令的源代码(一堆嵌入在源代码中的宏和语言扩展)，并生成适当的附加源代码来生成事件处理程序。因此，在 C++ 编译器获得源代码之前，必须运行 MOC 过程，通过移除那些特定于 Qt 系统的额外语言构造来生成合法的 ANSI C++ 代码。请参考 Qt 文档了解更多信息。以下简单的 Qt 程序将演示 Qt 编程及其事件处理系统的关键方面:\n\n```cpp\n#include <qapplication.h>\n#include <qdialog.h>\n#include <qmessagebox.h>\n#include <qobject.h>\n#include <qpushbutton.h>\n\nclass MyApp : public QDialog {\n  Q_OBJECT\npublic:\n    MyApp(QObject* /*parent*/ = 0):\n    button(this)\n    {\n      button.setText(\"Hello world!\"); button.resize(100, 30);\n\n      // When the button is clicked, run button_clicked\n      connect(&button,\n              &QPushButton::clicked, this, &MyApp::button_clicked);\n    }\n```\n\n宏`Q_OBJECT`是对主运行中心生成`Event Dispatch`表的指令。当我们将事件源连接到事件接收器时，会给`Event Dispatch`表一个条目。生成的代码将与 C++ 代码一起编译，以生成可执行文件:\n\n```cpp\npublic slots:\n    void button_clicked() {\n      QMessageBox box;\n      box.setWindowTitle(\"Howdy\");\n      box.setText(\"You clicked the button\");\n      box.show();\n      box.exec();\n    }\n\nprotected:\n  QPushButton button;\n};\n```\n\n语言扩展*公共槽*将被 MOC 剥离(做好源代码生成工作后)为与 ANSI C/C++ 编译器兼容的形式:\n\n```cpp\nint main(int argc, char** argv) {\n  QApplication app(argc, argv);\n  MyApp myapp;\n  myapp.show();\n  return app.exec();\n}\n```\n\n前面的代码片段初始化了 Qt 应用对象并显示了主窗口。实际上，Qt 是 C++ 语言最突出的应用开发框架，并且它与 Python 编程语言有很好的绑定。\n\n# MFC 下的事件驱动编程\n\n微软基金会类库仍然是编写基于微软视窗的桌面程序的流行库。如果我们将 **ActiveX 模板库** ( **ATL** )与它混合在一起，它确实对 web 编程有一些支持。作为一个 C++ 库，MFC 使用一种称为消息映射的机制来处理事件。作为宏给出的示例事件处理表是每个 MFC 程序的一部分:\n\n```cpp\nBEGIN_MESSAGE_MAP(CClockFrame,CFrameWnd)\n    ON_WM_CREATE()\n    ON_WM_PAINT()\n    ON_WM_TIMER()\nEND_MESSAGE_MAP()\n```\n\n前面的消息映射将响应`OnCreate`、`OnPaint`和`Ontimer`标准窗口应用编程接口消息。在这些消息图的深处是数组，我们将使用`message id`作为调度事件的索引。仔细观察，它与标准的视窗应用编程接口消息模型没有太大区别。\n\nThe code listing is not given here because we have globally a GUI implementation of one of the key interfaces for the Reactive Programming model using MFC. The implementation is based on the MFC library and the reader can go through the annotated listing to gain an understanding of non-trivial event processing in MFC.\n\n# 其他事件驱动的编程模型\n\nCOM+和 CORBA 等分布式对象处理框架确实有自己的事件处理框架。COM+事件模型基于连接点的概念(由`IConnectionPointContainer` / `IConnectionPoint`接口建模)，CORBA 确实有自己的事件服务模型。CORBA 标准提供了基于拉和基于推的事件通知。COM+和 CORBA 超出了本书的范围，读者应该查阅各自的文档。\n\n# 经典事件处理模型的局限性\n\n浏览各种平台支持的事件处理的全部目的是将事情放在正确的角度。这些平台中的事件响应逻辑大多与编写代码的平台相耦合。随着多核编程的出现，编写低级多线程代码变得困难，并且 C++ 编程语言提供了基于声明性任务的编程模型。但是事件源大多在 C++ 标准之外！C++ 语言没有标准的图形用户界面编程库、访问外部设备的接口标准等等。出路是什么？幸运的是，来自外部来源的事件和数据可以聚合成流(或序列)，并且通过使用函数式编程结构(如 Lambda 函数)可以非常高效地处理。额外的好处是，如果我们求助于关于变量和流的可变性的某种限制，并发性和并行性被构建到流处理模型中。\n\n# 反应式编程模型\n\n简单来说，反应式编程就是用异步数据流编程。通过对流应用各种操作，我们可以实现不同的计算目标。反应式程序的主要任务是将数据转换成流，而不管数据的来源是什么。在编写现代图形用户界面应用时，我们处理鼠标移动和点击事件。目前，大多数系统都获得回调，并在事件发生时处理这些事件。大多数情况下，处理程序在调用与事件调用相关联的操作方法之前会执行一系列过滤操作。在这个特定的上下文中，反应式编程帮助我们将鼠标移动和点击事件聚合到一个集合中，并在通知处理程序逻辑之前对它们设置一个过滤器。这样，应用/处理程序逻辑不会被不必要地执行。\n\n流处理模型是众所周知的，它很容易被应用开发人员编码。几乎任何东西都可以转换成一条流。这些候选包括消息、日志、属性、推特订阅源、博客文章、RSS 订阅源等等。函数式编程技术非常擅长处理流。像现代 C++ 这样的语言，对对象/函数编程有很好的支持，是编写反应式程序的自然选择。反应式编程背后的基本思想是，随着时间的推移，某些数据类型代表一个值。在这个编程范例中，这些数据类型(或者说数据序列)被表示为可观察的序列。涉及这些变化的(时间相关的)值的计算本身也将具有随时间变化的值，并且需要异步接收通知(当相关数据变化时)。\n\n# 功能反应编程\n\n几乎所有现代编程语言都支持函数式编程结构。函数式编程结构，如转换、应用、过滤、折叠等，非常适合处理流。使用函数式编程结构对异步数据流进行编程通常称为函数式反应式编程(出于所有实际目的)。这里给出的定义是可操作的。请参考作为哈斯克尔社区一部分的 Conal Elliott 和 Paul Hudak 所做的工作，了解严格的定义。如今，将反应式编程与 FP 相结合在开发人员中越来越受欢迎。Rx.Net、RxJava、RxJs、RxCpp 等库的出现就是一个证明。\n\nEven though reactive programming is the core subject of this book, in this chapter we will be sticking to an OOP approach. This is necessitated because of the fact that we need to introduce some standard interfaces (emulated in C++ using virtual functions) necessary for doing Reactive programming. Later on, after learning about FP constructs supported by C++ , readers can do some mental model mapping from OOP to FP constructs. We will also keep away from concurrency stuff to focus on software interfaces in this chapter. [Chapters 2](02.html#12AK80-51c8384cc2cb48e691b461190723b468), *A Tour of the Modern C++ and Its Key Idioms*, [Chapter 3](03.html#1O8H60-51c8384cc2cb48e691b461190723b468), *Language-Level Concurrency and Parallelism in C++*, and [Chapter 4](04.html#27GQ60-51c8384cc2cb48e691b461190723b468), *Asynchronous and Lock-Free Programming in C++*, will give the necessary background to understand reactive programming using FP constructs.\n\n# 反应式程序的关键接口\n\n为了帮助你理解反应性程序内部真正发生的事情，我们将编写一些玩具程序来将事情放在适当的上下文中。从软件设计的角度来看，如果您将并发性/并行性放在一边，专注于软件接口，那么反应式程序应该具有:\n\n*   实现`IObservable<T>`的事件源\n*   实现`IObserver<T>`的事件接收器\n*   向事件源添加订阅者的机制\n*   当数据出现在源位置时，将通知订阅者\n\nIn this particular chapter, we have written code using classic C++ constructs. This is because we have not yet introduced Modern C++ constructs. We have also used raw pointers, something which we can mostly avoid while writing Modern C++ code. The code in this chapter is written to conform to the ReactiveX documentation in general. In C++, we do not use inheritance-based techniques like we do in Java or C#.\n\n首先，让我们定义观察者、可观察和一个`CustomException`类:\n\n```cpp\n#pragma once \n//Common2.h \n\nstruct CustomException /*:*public std::exception */ {\n```\n\n```cpp\n   const char * what() const throw () { \n         return \"C++ Exception\"; \n   } \n}; \n```\n\n`CustomException`类只是一个占位符，让界面变得完整。既然我们已经决定在本章中只使用经典的 C++ 语言，我们就没有偏离`std::exception`类:\n\n```cpp\ntemplate<class T> class IEnumerator {\npublic:\n      virtual bool HasMore() = 0;\n      virtual T next() = 0;\n      //--------- Omitted Virtual destructor for brevity\n};\ntemplate <class T> class IEnumerable{\npublic:\n      virtual IEnumerator<T> *GetEnumerator() = 0;\n      //---------- Omitted Virtual destructor for brevity\n};\n```\n\n`Enumerable`接口由数据源使用，我们可以从中枚举数据，`IEnuerator<T>`将由客户端用于迭代。\n\nThe purpose of defining interfaces for Iterator (`IEnuerable<T>`/`IEnumerator<T>`) is to make the reader understand that they are very closely related to the `Observer<T>`/`Observable<T>` pattern. We will define `Observer<T>`/`Observable<T>` as follows:\n\n```cpp\ntemplate<class T> class IObserver\n{\npublic:\n      virtual void OnCompleted() = 0;\n      virtual void OnError(CustomException *exception) = 0;\n      virtual void OnNext(T value) = 0;\n};\ntemplate<typename T>\nclass IObservable\n{\npublic:\n      virtual bool Subscribe(IObserver<T>& observer) = 0;\n};\n```\n\n`IObserver<T>`是数据接收器将用来接收来自数据源的通知的接口。数据源将实现`IObservable<T>`接口。\n\nWe have defined the `IObserver<T>` interface and it has got three methods. They are `OnNext` (when the item is notified to the Observer), `OnCompleted` (when there is no more data), and `OnError` (when an exception is encountered). `Observable<T>` is implemented by the event source and event sinks can insert objects that implement `IObserver<T>` to receive notifications.\n\n# 基于拉与推的反应式编程\n\n反应性程序可分为**推式**和**拉式**。基于拉的系统等待将数据流推送到请求者(在我们的例子中是订阅者)的请求。这是主动轮询数据源以获取更多信息的典型情况。这采用了迭代器模式，`IEnumerable <T>` / `IEnumerator <T>`接口是专门为这种本质上同步的场景而设计的(应用可以在拉数据时阻塞)。另一方面，基于推送的系统聚合事件并通过信号网络推送来实现计算。在这种情况下，与基于拉的系统不同，数据和相关更新从源(在这种情况下是可观察的序列)传递给订户。这种异步特性是通过不阻塞订阅者，而是让它对更改做出反应来实现的。正如您所看到的，在丰富的用户界面环境中，使用这种推送模式更为有益，在这种环境中，您不希望在等待某些事件时阻塞主用户界面线程。这变得非常理想，从而使反应性程序具有响应性。\n\n# IEnumerable/IObservable 对偶\n\n如果你仔细看看，这两种模式之间只有细微的区别。`IEnumerable<T>`可以认为是推式`IObservable<T>`的拉式等价物。事实上，它们是对偶。当两个实体交换信息时，一个实体的拉动对应于另一个实体推动信息。下图说明了这种二元性:\n\n![](img/00005.jpeg)\n\n让我们通过查看这个样本代码来理解这种二元性，它是一个数字序列生成器:\n\nWe have striven to use classic C++ constructs to write programs for this particular chapter as there are chapters on Modern C++ language features, language level concurrency, lock-free programming, and related topics for implementing Reactive constructs in Modern C++.\n\n```cpp\n#include <iostream>\n#include <vector>\n#include <iterator>\n#include <memory>\n#include \"../Common2.h\"\nusing namespace std;\n\nclass ConcreteEnumberable : public IEnumerable<int>\n{\n      int *numberlist,_count;\npublic:\n      ConcreteEnumberable(int numbers[], int count):\n            numberlist(numbers),_count(count){}\n      ~ConcreteEnumberable() {}\n\n      class Enumerator : public IEnumerator<int>\n      {\n      int *inumbers, icount, index;\n      public:\n      Enumerator(int *numbers,\n            int count):inumbers(numbers),icount(count),index(0) {}\n      bool HasMore() { return index < icount; }\n      //---------- ideally speaking, the next function should throw\n      //---------- an exception...instead it just returns -1 when the \n      //---------- bound has reached\n      int next() { return (index < icount) ?\n                   inumbers[index++ ] : -1; }\n      ~Enumerator() {}\n      };\n      IEnumerator<int> *GetEnumerator()\n            { return new Enumerator(numberlist, _count); }\n};\n```\n\n前面的类将整数数组作为参数，我们可以在实现`IEnumerable<T>`接口时枚举这些元素。`Enumeration`逻辑由嵌套类实现，嵌套类实现`IEnumerator<T>`接口:\n\n```cpp\nint main()\n{\n      int x[] = { 1,2,3,4,5 };\n      //-------- Has used Raw pointers on purpose here as we have\n      //------- not introduced unique_ptr,shared_ptr,weak_ptr yet\n      //-------- using auto_ptr will be confusting...otherwise\n      //-------- need to use boost library here... ( an overkill)\n      ConcreteEnumberable *t = new ConcreteEnumberable(x, 5);\n      IEnumerator<int> * numbers = t->GetEnumerator();\n      while (numbers->HasMore())\n            cout << numbers->next() << endl;\n      delete numbers;delete t;\n      return 0;\n}\n```\n\n主程序实例化`ConcreteEnuerable`类的一个实现，并遍历每个元素。\n\n我们将编写一个偶数序列生成器来演示这些数据类型如何一起工作，将基于拉的程序转换为基于推的程序。健壮性方面的优先级较低，以保持列表简洁:\n\n```cpp\n#include \"stdafx.h\"\n#include <iostream>\n#include <vector>\n#include <iterator>\n#include <memory>\n#include \"../Common2.h\"\nusing namespace std;\n\nclass EvenNumberObservable : IObservable<int>{\n      int *_numbers,_count;\npublic:\n      EvenNumberObservable(int numbers[],\n            int count):_numbers(numbers),_count(count){}\n      bool Subscribe(IObserver<int>& observer){\n            for (int i = 0; i < _count; ++ i)\n                  if (_numbers[i] % 2 == 0)\n                        observer.OnNext(_numbers[i]);\n            observer.OnCompleted();\n            return true;\n      }\n};\n```\n\n前面的程序取一个整数数组，过滤掉奇数，如果遇到偶数就通知`Observer<T>`。在这种特殊情况下，数据源将数据推送到`observer`。`Observer<T>`执行情况如下:\n\n```cpp\nclass SimpleObserver : public IObserver<int>{\npublic:\n      void OnNext(int value) { cout << value << endl; }\n      void OnCompleted() { cout << _T(\"hello completed\") << endl; }\n      void OnError( CustomException * ex) {}\n};\n```\n\n`SimpleObserver`类实现了`IObserver<T>`接口，它能够接收通知并对通知做出反应:\n\n```cpp\nint main()\n{\n      int x[] = { 1,2,3,4,5 };\n      EvenNumberObservable *t = new EvenNumberObservable(x, 5);\n      IObserver<int>> *xy = new SimpleObserver();\n      t->Subscribe(*xy);\n      delete xy; delete t;\n      return 0;\n}\n```\n\n从前面的例子中，你可以看到一个人如何自然地从一个可观察的自然数序列中订阅偶数。当检测到偶数时，系统会自动将数值`push` ( `publish`)转换为`observer` ( `subscriber`)。代码给出了关键接口的显式实现，这样人们就可以理解或推测幕后真正发生了什么。\n\n# 将事件转换为可观察的\n\n我们现在已经理解了如何将基于`IEnumerable<T>`的拉程序转换为基于`IObservable<T>` / `IObserver<T>`的推程序。在现实生活中，事件源并不像我们在前面给出的数字流例子中发现的那么简单。让我们看看如何用一个小的 MFC 程序将`MouseMove`事件转换成流:\n\nWe have chosen MFC for this particular implementation because we have a chapter dedicated to Qt-based reactive programming. In that chapter, we will be implementing Reactive programs in idiomatic asynchronous push-based streams. In this MFC program, we simply do a filtering operation to see whether the mouse is moving in a bounding rectangle and, if so, notify the `observer`. We are using synchronous dispatch here. This example is synchronous too:\n\n```cpp\n#include \"stdafx.h\"\n#include <afxwin.h>\n#include <afxext.h>\n#include <math.h>\n#include <vector>\n#include \"../Common2.h\"\n\nusing namespace std;\nclass CMouseFrame :public CFrameWnd,IObservable<CPoint>\n{\nprivate:\n      RECT _rect;\n      POINT _curr_pos;\n      vector<IObserver<CPoint> *> _event_src;\npublic:\n      CMouseFrame(){\n            HBRUSH brush =\n                  (HBRUSH)::CreateSolidBrush(RGB(175, 238, 238));\n            CString mywindow = AfxRegisterWndClass(\n                  CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS,\n                  0, brush, 0);\n            Create(mywindow, _T(\"MFC Clock By Praseed Pai\"));\n      }\n```\n\n代码的前一部分定义了一个`Frame`类，该类从`MFC`库中派生出`CFrameWnd`类，并且还实现了`IObservable<T>`接口，以迫使程序员实现`Subscribe`方法。`IObserver<T>`的一个向量将存储`observers`或`Subscribers`的列表。对于这个例子，我们只有一个`observer`。代码中`observer`的数量没有限制:\n\n```cpp\n      virtual bool Subscribe(IObserver<CPoint>& observer) {\n            _event_src.push_back(&observer);\n            return true;\n      }\n```\n\n`Subscribe`方法只是把对`observer`的引用存储到一个向量上，返回`true`:当鼠标移动时，我们从`MFC`库中得到通知，如果是矩形区域，会通知`observer`(通知代码如下):\n\n```cpp\n      bool FireEvent(const CPoint& pt) {\n            vector<IObserver<CPoint> *>::iterator it =\n                  _event_src.begin();\n            while (it != _event_src.end()){\n                  IObserver<CPoint> *observer = *it;\n                  observer->OnNext(pt);\n                  //---------- In a Real world Rx programs there is a \n                  //--------- sequence stipulated to call methods...\n                  //--------- OnCompleted will be called only when \n                  //--------- all the data is processed...this code\n                  //--------- is written to demonstrate the call schema\n                  observer->OnCompleted();\n                  it++ ;\n            }\n            return true;\n      }\n```\n\n`FireEvent`方法遍历`observer`并调用`observer`的`OnNext`方法。它还调用观察者的每个实例的`OnCompleted`方法:Rx 调度机制在调用`observer`方法时遵循某些规则。如果调用`OnComplete`方法，同一`observer`上不再调用`OnNext`。同样，如果调用`OnError`，则不会向`observer`发送更多消息。如果我们需要遵循这里的 Rx 模型所规定的约定，那么列表就会变得复杂。这里给出代码的目的是以示意的方式展示 Rx 编程模型是如何工作的。\n\n```cpp\n      int OnCreate(LPCREATESTRUCT l){\n            return CFrameWnd::OnCreate(l);\n      }\n      void SetCurrentPoint(CPoint pt) {\n            this->_curr_pos = pt;\n            Invalidate(0);\n      }\n```\n\n`SetCurrentPoint`方法由`observer`调用，以设置必须绘制文本的当前点。调用`Invalidate`方法来触发`WM_PAINT`消息，并且`MFC`子系统将它路由到`OnPaint`(因为它在`Message`地图中是有线的):\n\n```cpp\n      void OnPaint()\n      {\n            CPaintDC d(this);\n            CBrush b(RGB(100, 149, 237));\n            int x1 = -200, y1 = -220, x2 = 210, y2 = 200;\n            Transform(&x1, &y1); Transform(&x2, &y2);\n            CRect rect(x1, y1, x2, y2);\n            d.FillRect(&rect, &b);\n            CPen p2(PS_SOLID, 2, RGB(153, 0, 0));\n            d.SelectObject(&p2);\n\n            char *str = \"Hello Reactive C++\";\n            CFont f;\n            f.CreatePointFont(240, _T(\"Times New Roman\"));\n            d.SelectObject(&f);\n            d.SetTextColor(RGB(204, 0, 0));\n            d.SetBkMode(TRANSPARENT);\n            CRgn crgn;\n            crgn.CreateRectRgn(rect.left,rect.top,\n            rect.right ,rect.bottom);\n            d.SelectClipRgn(&crgn);\n            d.TextOut(_curr_pos.x, _curr_pos.y,\n            CString(str), strlen(str));\n      }\n```\n\n当进行`Invalidate`调用时，`MFC`框架调用`OnPaint`方法。该方法在屏幕上绘制`literal`字符串`Hello Reactive C++ `:\n\n```cpp\n      void Transform(int *px, int *py) {\n            ::GetClientRect(m_hWnd, &_rect);\n            int width = (_rect.right - _rect.left) / 2,\n            height = (_rect.bottom - _rect.top) / 2;\n           *px = *px + width; *py = height - *py;\n      }\n```\n\n`Transform`方法计算`Frame`的工作区的边界，并将`Cartesian`坐标转换为设计坐标。这种计算可以通过世界坐标转换更好地完成:\n\n```cpp\n      void OnMouseMove(UINT nFlags, CPoint point)\n      {\n            int x1 = -200,y1= -220, x2 = 210,y2 = 200;\n            Transform(&x1, &y1);Transform(&x2, &y2);\n            CRect rect(x1, y1, x2, y2);\n            POINT pts;\n            pts.x = point.x; pts.y = point.y;\n            rect.NormalizeRect();\n            //--- In a real program, the points will be aggregated\n            //---- into a list (stream)\n            if (rect.PtInRect(point)) {\n                  //--- Ideally speaking this notification has to go\n                  //--- through a non blocking call\n                  FireEvent(point);\n            }\n      }\n```\n\n`OnMouseMove`方法检查鼠标位置是否在屏幕中心的矩形内，并向`observer`发出通知:\n\n```cpp\n      DECLARE_MESSAGE_MAP();\n};\n\nBEGIN_MESSAGE_MAP(CMouseFrame, CFrameWnd)\n      ON_WM_CREATE()\n      ON_WM_PAINT()\n      ON_WM_MOUSEMOVE()\nEND_MESSAGE_MAP()\nclass WindowHandler : public IObserver<CPoint>\n{\nprivate:\n      CMouseFrame *window;\npublic:\n      WindowHandler(CMouseFrame *win) : window(win) { }\n      virtual ~WindowHandler() { window = 0; }\n      virtual void OnCompleted() {}\n      virtual void OnError(CustomException *exception) {}\n      virtual void OnNext(CPoint value) {\n            if (window) window->SetCurrentPoint(value);\n      }\n};\n```\n\n前一类`WindowHandler`实现`IObserver<T>`接口，处理`CMouseFrame`通知的事件，实现`IObservable<CPoint>`接口。在这个预设的例子中，我们通过调用`SetCurrentPoint`方法在鼠标位置绘制字符串来设置当前点:\n\n```cpp\nclass CMouseApp :public CWinApp\n{\n      WindowHandler *reactive_handler;\npublic:\n      int InitInstance(){\n            CMouseFrame *p = new CMouseFrame();\n            p->ShowWindow(1);\n            reactive_handler = new WindowHandler(p);\n            //--- Wire the observer to the Event Source\n            //--- which implements IObservable<T>\n            p->Subscribe(*reactive_handler);\n            m_pMainWnd = p;\n            return 1;\n      }\n      virtual ~CMouseApp() {\n            if (reactive_handler) {\n                  delete reactive_handler;\n                  reactive_handler = 0;\n           }\n      }\n};\n\nCMouseApp a;\n```\n\n# 我们书的哲学\n\n本章的目的是向读者介绍反应式编程模式的关键接口——它们是`IObservable<T>`和`IObserver<T>.`,它们实际上是`IEnumerable<T>`和`IEnumerator<T>`接口的对偶。我们学习了如何在经典的 C++ 中建模这些接口(嗯，大部分是)，并拥有所有这些接口的玩具实现。最后，我们实现了一个捕获鼠标移动并通知观察者列表的图形用户界面程序。这些玩具实现是为了让我们熟悉 Reactive 编程模型的思想和理想。我们的实现可以被认为是基于面向对象的反应式编程的实现。\n\n为了精通 C++ 反应式编程，程序员必须熟悉以下主题:\n\n*   现代 C++ 提供的高级语言结构\n*   现代 C++ 提供的函数式编程结构\n*   异步编程(RxCpp 为您处理！)模型\n*   事件流处理\n*   对 RxCpp 等工业实力图书馆的了解\n*   RxCpp 在图形用户界面和网络编程中的应用\n*   高级反应式编程结构\n*   处理错误和异常\n\n这一章主要是关于关键的习惯用法，以及为什么我们需要一个健壮的模型来处理异步数据。接下来的三章将涵盖现代 C++ 的语言特性，用 C++ 标准构造处理并发/并行，以及无锁编程(通过内存模型保证成为可能)。上述主题将为用户掌握功能反应式编程打下坚实的基础。\n\n在[第 5 章](05.html#2RHM00-51c8384cc2cb48e691b461190723b468)、*可观测性介绍*中，我们将再次回到可观测性的话题，并以功能性的方式实现接口，以重申一些概念。在[第 6 章](06.html#352RK0-51c8384cc2cb48e691b461190723b468)、*使用 C++* 的事件流编程介绍中，我们将借助两个工业级的库走向高级事件流处理主题，这两个库使用**领域特定嵌入式语言** ( **DSEL** )方法进行事件流处理。\n\n到目前为止，已经为用户接触工业级 RxCpp 库及其细微差别以编写专业质量的现代 C++ 程序做好了准备。在[第 7 章](07.html#3M85O0-51c8384cc2cb48e691b461190723b468)、*数据流计算和 RxCpp 库介绍*和[第 8 章](08.html#49AH00-51c8384cc2cb48e691b461190723b468)、*RxCpp–关键元素*中，我们将介绍这个精彩的库。以下章节将涵盖使用 Qt 库和 RxCpp 中的高级运算符的反应式图形用户界面编程。\n\n最后三章涵盖了反应式设计模式、C++ 中的微服务以及处理错误/异常的高级主题。到这本书的结尾，以经典 C++ 开始的读者将会涵盖很多领域，不仅是在编写 Reactive 程序方面，而且在 C++ 语言本身方面。由于主题的性质，我们将涵盖 C++ 17 的大部分特性(在编写时)。\n\n# 摘要\n\n在本章中，我们了解了 Rx 编程模型的一些关键数据结构。我们实现了它们的玩具版本，以使我们熟悉支撑它们的概念上的细微差别。我们从窗口应用编程接口、XLib 应用编程接口、MFC 和 Qt 如何处理图形用户界面事件开始。我们也简要地讨论了事件是如何在 COM+/CORBA 中处理的。然后，快速概述了反应式编程。在介绍了一些接口之后，我们从头开始实现了它们。最后，为了完整起见，在 MFC 之上实现了这些接口的 GUI 版本。我们还讨论了这本书的关键哲学方面。\n\n在下一章中，我们将通过强调移动语义、Lambdas、类型推断、基于范围的循环、可管道操作符、智能指针等，对现代 c++(c++ version 11/14/17)的关键特性进行一次旋风式的考察。这对于编写反应式编程的基本代码来说是必不可少的。"
  },
  {
    "path": "docs/cpp-react-prog/02.md",
    "content": "# 二、现代 C++ 及其关键习语概述\n\n经典的 C++ 编程语言在 1998 年实现了标准化，随后在 2003 年进行了小规模的修订(大部分是修正)。为了支持高级抽象，开发人员依赖于 Boost([http://www.boost.org](http://www.boost.org))库和其他公共领域库。由于下一波标准化浪潮，该语言(从 C++ 11 开始)得到了增强，现在开发人员可以对最广泛使用的抽象进行编码(由其他语言支持)，而无需依赖外部库。甚至线程和文件系统接口，这些都在库的保护之下，现在也是标准语言的一部分。现代 C++(代表 C++ 版本 11/14/17)包含对该语言及其库的卓越补充，这使得 C++ 成为编写工业力量生产软件的事实上的选择。本章涵盖的特性是程序员使用反应式编程结构(尤其是 RxCpp)必须了解的最基本的特性。本章的主要目的是介绍该语言中最重要的补充内容，这些内容使得实现反应式编程结构变得更加容易，而无需求助于深奥的语言技术。诸如 Lambda 函数、自动类型推断、右值引用、移动语义和语言级并发等构造是本书作者认为每个 C++ 程序员都应该知道的一些构造。在本章中，我们将涵盖以下主题:\n\n*   C++ 编程语言设计的关键问题\n*   为了编写更好的代码，对 C++ 进行了一些增强\n*   通过右值引用和移动语义实现更好的内存管理\n*   使用一组增强的智能指针实现更好的对象生存期管理\n*   使用 Lambda 函数和表达式的行为参数化\n*   函数包装器(`std::function`类型)\n*   其他功能\n*   编写迭代器和观察器(将所有东西放在一起)\n\n# C++ 编程语言的关键问题\n\n就开发人员而言，C++ 编程语言设计人员牢记的三个关键问题是(现在仍然是):\n\n*   零成本抽象-对更高级别的抽象没有性能损失\n*   表现性-用户定义的类型 ( **UDT** )或类应该像内置类型一样表现性\n*   可替代性-UDT 可以在任何期望内置类型的地方被替代(如在通用数据结构和算法中)\n\n我们将简要讨论这些。\n\n# 零成本抽象\n\nC++ 编程语言一直帮助开发人员编写利用微处理器(生成的代码在其上运行)的代码，并在重要时提高抽象级别。在提升抽象的同时，语言的设计者总是试图最小化(几乎消除)他们的性能开销。这被称为零成本抽象或零间接成本抽象。唯一值得注意的损失是分派虚函数时的间接调用(通过函数指针)成本。尽管语言增加了大量的特性，设计者从一开始就保持了语言隐含的“零成本抽象”保证。\n\n# 表达性\n\nC++ 帮助开发人员编写用户定义的类型或类，它们可以像编程语言的内置类型一样具有表现力。这使得人们能够编写任意精度的算术类(在某些语言中被称为`BigInteger` / `BigFloat`，它包含了双精度或浮点的所有特性。为了便于解释，我们定义了一个包装 IEEE 双精度浮点数的`SmartFloat`类，双数据类型可用的大多数运算符都是重载的。下面的代码片段显示，可以编写模仿内置类型(如 int、float 或 double)语义的类型:\n\n```cpp\n//---- SmartFloat.cpp\n#include <iostream>\n#include <vector>\n#include <algorithm>\nusing namespace std;\nclass SmartFloat {\n     double _value; // underlying store\n   public:\n      SmartFloat(double value) : _value(value) {}\n      SmartFloat() : _value(0) {}\n      SmartFloat( const SmartFloat& other ) { _value = other._value; }\n      SmartFloat& operator = ( const SmartFloat& other ) {\n          if ( this != &other ) { _value = other._value;}\n          return *this;\n      }\n      SmartFloat& operator = (double value )\n       { _value = value; return *this;}\n      ~SmartFloat(){ }\n```\n\n`SmartFloat`类包装了一个双精度值，并定义了一些构造函数和赋值运算符来正确初始化实例。在下面的代码片段中，我们将定义一些有助于增加值的运算符。定义了运算符的前缀和后缀变体:\n\n```cpp\n      SmartFloat& operator ++ () { _value++ ; return *this; }\n      SmartFloat operator ++ (int) { // postfix operator\n             SmartFloat nu(*this); ++ _value; return nu;\n      }\n      SmartFloat& operator -- () { _value--; return *this; }\n      SmartFloat operator -- (int) {\n           SmartFloat nu(*this); --_value; return nu;\n      }\n```\n\n前面的代码片段实现了增量运算符(前缀和后缀)，仅用于演示目的。在现实世界的类中，我们将检查浮点上溢和下溢，以使代码更加健壮。包装一个类型的全部目的是为了编写健壮的代码！\n\n```cpp\n     SmartFloat& operator += ( double x ) { _value += x; return *this;}\n     SmartFloat& operator -= ( double x ) { _value -= x;return *this; }\n     SmartFloat& operator *= ( double x ) { _value *= x; return *this;}\n     SmartFloat& operator /= ( double x ) { _value /= x; return *this;}\n```\n\n前面的代码片段实现了 C++ 风格的赋值操作符，同样，为了简短起见，我们没有检查是否有任何浮点上溢或下溢。我们在这里也不处理异常，以保持列表的简短。\n\n```cpp\n      bool operator > ( const SmartFloat& other )\n        { return _value > other._value; }\n      bool operator < ( const SmartFloat& other )\n       {return _value < other._value;}\n      bool operator == ( const SmartFloat& other )\n        { return _value == other._value;}\n      bool operator != ( const SmartFloat& other )\n        { return _value != other._value;}\n      bool operator >= ( const SmartFloat& other )\n        { return _value >= other._value;}\n      bool operator <= ( const SmartFloat& other )\n        { return _value <= other._value;}\n```\n\n前面的代码实现了关系运算符，与双精度浮点相关联的大多数语义都已实现，如下所示:\n\n```cpp\n      operator int () { return _value; }\n      operator double () { return _value;}\n};\n```\n\n为了完整起见，我们实现了对`int`和`double`的转换运算符。我们将编写两个函数来聚合存储在数组中的值。第一个函数需要一个`double`数组作为参数，第二个函数需要一个`SmartFloat`数组作为参数。两个例程中的代码是相同的，只有类型发生了变化。两者将产生相同的结果:\n\n```cpp\ndouble Accumulate( double a[] , int count ){\n    double value = 0;\n    for( int i=0; i<count; ++ i) { value += a[i]; }\n    return value;\n}\ndouble Accumulate( SmartFloat a[] , int count ){\n    SmartFloat value = 0;\n    for( int i=0; i<count; ++ i) { value += a[i]; }\n    return value;\n}\nint main() {\n    // using C++ 1z's initializer list\n    double x[] = { 10.0,20.0,30,40 };\n    SmartFloat y[] = { 10,20.0,30,40 };\n    double res = Accumulate(x,4); // will call the double version\n    cout << res << endl;\n    res = Accumulate(y,4); // will call the SmartFloat version\n    cout << res << endl;\n}\n```\n\nC++ 语言帮助我们编写扩展基本类型语义的表达类型。语言的表现力也有助于使用语言支持的多种技术编写好的值类型和引用类型。由于支持操作符重载、转换操作符、布局新以及其他相关技术，与同时代的其他语言相比，该语言将类设计提升到了一个更高的水平。但是，伴随着权力而来的是责任，语言有时会给你足够的绳索来搬起石头砸自己的脚。\n\n# 可置换性\n\n在前面的示例中，我们看到了如何使用用户定义的类型来表达在内置类型上完成的所有操作。C++ 的另一个目标是以通用的方式编写代码，在这种方式中，我们可以替换一个用户定义的类，该类模仿内置类型之一的语义，如`float`、`double`、`int`等:\n\n```cpp\n//------------- from SmartValue.cpp\ntemplate <class T>\nT Accumulate( T a[] , int count ) {\n    T value = 0;\n    for( int i=0; i<count; ++ i) { value += a[i]; }\n    return value;\n}\nint main(){\n    //----- Templated version of SmartFloat\n    SmartValue<double> y[] = { 10,20.0,30,40 };\n    double res = Accumulate(y,4);\n    cout << res << endl;\n}\n```\n\nThe C++ programming language supports different programming paradigms and the three principles outlined previously are just some of them. The language gives support for constructs that can help create robust types (domain-specific) for writing better code. These three principles gave us a powerful and fast programming language for sure. Modern C++ did add a lot of new abstractions to make the life of a programmer easier. But the three design principles outlined previously have not been sacrificed in any way to achieve those objectives. This was partly possible because of the meta programming support the language had due to the inadvertent Turing completeness of the template mechanism. Read about **template meta programming** (**TMP**) and Turing Completeness with the help of your favorite search engine.\n\n# 对 C++ 的增强，用于编写更好的代码\n\n在过去的十年里，编程语言世界发生了很大的变化，这些变化应该反映在 C++ 编程语言的新形象中。现代 C++ 的大部分创新涉及处理高级抽象和引入函数式编程结构来支持语言级并发。大多数现代语言都有一个垃圾收集器，运行时管理这些复杂性。C++ 编程语言没有自动垃圾收集作为语言标准的一部分。C++ 编程语言以其对零成本抽象(您不为您不使用的东西付费)和最大运行时性能的隐式保证，不得不求助于许多编译时技巧和元编程技术来实现 C#、Java 或 Scala 等语言支持的抽象级别。其中一些将在下面的章节中概述，您可以自己深入研究这些主题。网站[http://en.cppreference.com](http://en.cppreference.com)是一个提高你的 C++ 编程语言知识的好网站。\n\n# 类型演绎和推理\n\n现代 C++ 语言编译器在从程序员指定的表达式和语句中推导类型方面做得很好。大多数现代编程语言都支持类型推断，现代 C++ 也是如此。这是从函数式编程语言如 Haskell 和 ML 中借用的一个习惯用法。C#和 Scala 编程语言已经提供了类型推断。我们将编写一个小程序来启动类型推断:\n\n```cpp\n//----- AutoFirst.cpp\n#include <iostream>\n#include <vector>\nusing namespace std;\nint main(){\n    vector<string> vt = {\"first\", \"second\", \"third\", \"fourth\"};\n    //--- Explicitly specify the Type ( makes it verbose)\n    for (vector<string>::iterator it = vt.begin();\n        it != vt.end(); ++ it)\n    cout << *it << \" \";\n    //--- Let the compiler infer the type for us\n    for (auto it2 = vt.begin(); it2 != vt.end(); ++ it2)\n        cout << *it2 << \" \";\n    return 0;\n}\n```\n\n`auto`关键字指定变量的类型将由编译器根据表达式中指定的函数的初始化和返回值来推导。在这个特殊的例子中，我们没有得到多少。随着我们的声明变得越来越复杂，最好让编译器进行类型推断。我们的代码清单将使用 auto 来简化整本书的代码。现在，让我们写一个简单的程序，让这个想法更加清晰:\n\n```cpp\n//----- AutoSecond.cpp\n#include <iostream>\n#include <vector>\n#include <initializer_list>\nusing namespace std;\nint main() {\n    vector<double> vtdbl = {0, 3.14, 2.718, 10.00};\n    auto vt_dbl2 = vtdbl; // type will be deduced\n    auto size = vt_dbl2.size(); // size_t\n    auto &rvec = vtdbl; // specify a auto reference\n    cout << size << endl;\n    // Iterate - Compiler infers the type\n    for ( auto it = vtdbl.begin(); it != vtdbl.end(); ++ it)\n        cout << *it << \" \";\n    // 'it2' evaluates to iterator to vector of double\n    for (auto it2 = vt_dbl2.begin(); it2 != vt_dbl2.end(); ++ it2)\n        cout << *it2 << \" \";\n    // This will change the first element of vtdbl vector\n    rvec[0] = 100;\n    // Now Iterate to reflect the type\n    for ( auto it3 = vtdbl.begin(); it3 != vtdbl.end(); ++ it3)\n        cout << *it3 << \" \";\n    return 0;\n}\n```\n\n前面的代码演示了在编写现代 C++ 代码时类型推理的使用。C++ 编程语言还有一个新的关键字，可以帮助查询作为参数给出的表达式类型。关键词的一般形式是`decltype(<expr>)`。以下程序有助于演示这个特定关键字的用法:\n\n```cpp\n//---- Decltype.cpp\n#include <iostream>\nusing namespace std;\nint foo() { return 10; }\nchar bar() { return 'g'; }\nauto fancy() -> decltype(1.0f) { return 1;} //return type is float\nint main() {\n    // Data type of x is same as return type of foo()\n    // and type of y is same as return type of bar()\n    decltype(foo()) x;\n    decltype(bar()) y;\n    //--- in g++, Should print i => int\n    cout << typeid(x).name() << endl;\n    //--- in g++, Should print c => char \n    cout << typeid(y).name() << endl;\n    struct A { double x; };\n    const A* a = new A();\n    decltype(a->x) z; // type is double\n    decltype((a->x)) t= z; // type is const double&\n    //--- in g++, Should print  d => double\n    cout << typeid(z).name() << endl;\n    cout << typeid(t).name() << endl;\n    //--- in g++, Should print  f => float\n    cout << typeid(decltype(fancy())).name() << endl;\n    return 0;\n}\n```\n\n`decltype`是一个编译时构造，它有助于指定变量的类型(编译器将努力找出它)，也有助于我们强制变量的类型(参见前面的`fancy()`函数)。\n\n# 变量的统一初始化\n\n经典的 C++ 有某种特殊的变量初始化语法。现代 C++ 支持统一初始化(我们已经在类型推断部分看到了例子)。该语言为开发人员提供帮助类，以支持自定义类型的统一初始化:\n\n```cpp\n//----------------Initialization.cpp\n#include <iostream>\n#include <vector>\n#include <initializer_list>\nusing namespace std;\ntemplate <class T>\nstruct Vector_Wrapper {\n    std::vector<T> vctr;\n    Vector_Wrapper(std::initializer_list<T> l) : vctr(l) {}\n    void Append(std::initializer_list<T> l)\n    { vctr.insert(vctr.end(), l.begin(), l.end());}\n};\nint main() {\n    Vector_Wrapper<int> vcw = {1, 2, 3, 4, 5}; // list-initialization\n    vcw.Append({6, 7, 8}); // list-initialization in function call\n```\n\n```cpp\n    for (auto n : vcw.vctr) { std::cout << n << ' '; }\n    std::cout << '\\n';\n}\n```\n\n前面的清单显示了如何为程序员创建的自定义类启用初始化列表。\n\n# 可变模板\n\n在 C++ 11 和更高版本中，作为标准语言的一部分，支持变量模板。变量模板是一个模板类或模板函数，它在模板参数中取一个变量数。在经典的 C++ 中，模板实例化是用固定数量的参数进行的。类级和函数级都支持变量模板。在本节中，我们将讨论变量函数，因为它们广泛用于编写函数式程序、编译时编程(元编程)和可管道函数:\n\n```cpp\n//Variadic.cpp\n#include <iostream>\n#include <iterator>\n#include <vector>\n#include <algorithm>\nusing namespace std;\n//--- add given below is a base case for ending compile time\n//--- recursion\nint add() { return 0; } // end condition\n//---- Declare a Variadic function Template\n//---- ... is called parameter pack. The compiler\n//--- synthesize a function based on the number of arguments\n//------ given by the programmer.\n//----- decltype(auto) => Compiler will do Type Inference\ntemplate<class T0, class ... Ts>\ndecltype(auto) add(T0 first, Ts ... rest) {\n    return first + add(rest ...);\n}\nint main() { int n = add(0,2,3,4); cout << n << endl; }\n```\n\n在前面的代码中，编译器根据传递的参数数量合成一个函数。编译器理解`add`是一个变量函数，在编译时通过递归解包参数来生成代码。当编译器处理完所有参数后，编译时递归将停止。基本用例版本是对编译器停止递归的提示。下一个程序展示了如何使用可变模板和完美转发来编写一个接受任意数量参数的函数:\n\n```cpp\n//Variadic2.cpp\n#include <iostream>\n#include <iterator>\n#include <vector>\n#include <algorithm>\nusing namespace std;\n//--------- Print values to the console for basic types\n//-------- These are base case versions\nvoid EmitConsole(int value) { cout << \"Integer: \" << value << endl; }\nvoid EmitConsole(double value) { cout << \"Double: \" << value << endl; }\nvoid EmitConsole(const string& value){cout << \"String: \"<<value<< endl; }\n```\n\n`EmitConsole`的三个变体将参数打印到控制台。我们有打印`int`、`double`和`string`的功能。使用这些函数作为基本案例，我们将编写一个使用通用引用和完美转发的函数来编写采用任意值的函数:\n\n```cpp\ntemplate<typename T>\nvoid EmitValues(T&& arg) { EmitConsole(std::forward<T>(arg)); }\n\ntemplate<typename T1, typename... Tn>\nvoid EmitValues(T1&& arg1, Tn&&... args){\n    EmitConsole(std::forward<T1>(arg1));\n    EmitValues(std::forward<Tn>(args)...);\n}\n\nint main() { EmitValues(0,2.0,\"Hello World\",4); }\n```\n\n# 右值引用\n\n如果你已经用 C++ 编程很长时间了，你可能会熟悉这样一个事实:C++ 引用可以帮助你别名化一个变量，你可以对引用赋值来反映别名化变量的变化。C++ 支持的引用类型被称为左值引用(因为它们是对赋值左侧变量的引用)。以下代码片段显示了左值引用的使用:\n\n```cpp\n//---- Lvalue.cpp\n#include <iostream>\nusing namespace std;\nint main() {\n  int i=0;\n  cout << i << endl; //prints 0\n  int& ri = i;\n  ri = 20;\n  cout << i << endl; // prints 20\n}\n```\n\n`int&`是左值引用的一个实例。在现代 C++ 中，有右值引用的概念。右值被定义为任何不是左值的东西，这种东西可以出现在赋值的右边。在经典的 C++ 中，没有右值引用的概念。现代 C++ 引入了它:\n\n```cpp\n///---- Rvaluref.cpp\n#include <iostream>using namespace std;\nint main() {\n    int&& j = 42;int x = 3,y=5; int&& z = x + y; cout << z << endl;\n    z = 10; cout << z << endl;j=20;cout << j << endl;\n}\n```\n\n右值引用由两个`&&`表示。以下程序将清楚地演示调用函数时右值引用的使用:\n\n```cpp\n//------- RvaluerefCall.cpp\n#include <iostream>\nusing namespace std;\nvoid TestFunction( int & a ) {cout << a << endl;}\nvoid TestFunction( int && a ){\n    cout << \"rvalue references\" << endl;\n    cout << a << endl;\n}\nint main() {\nint&& j = 42;\nint x = 3,y=5;\nint&& z = x + y;\n    TestFunction(x + y ); // Should call rvalue reference function\n    TestFunction(j); // Calls Lvalue Refreence function\n}\n```\n\n右值引用的真正威力在内存管理中显而易见。C++ 编程语言有复制构造函数和赋值操作符的概念。它们主要复制源对象内容。在右值引用的帮助下，可以通过交换指针来避免昂贵的复制，因为右值引用是临时的或中间表达式。下一节将对此进行演示。\n\n# 移动语义\n\nC++ 编程语言为我们设计的每个类隐式保证了一个复制构造函数、赋值操作符和一个析构函数(有时是虚拟的)。这意味着在克隆对象或分配给现有对象时进行资源管理。有时复制一个对象非常昂贵，所有权的移动(通过指针)有助于编写快速代码。现代 C++ 拥有提供移动构造函数和移动赋值操作符的功能，以帮助开发人员在创建新对象或向新对象赋值的过程中避免复制大对象。右值引用可以作为编译器的提示，当涉及临时对象时，构造函数的移动版本或赋值的移动版本更适合上下文:\n\n```cpp\n//----- FloatBuffer.cpp\n#include <iostream>\n#include <vector>\nusing namespace std;\nclass FloatBuffer {\n    double *bfr; int count;\npublic:\n    FloatBuffer():bfr(nullptr),count(0){}\n    FloatBuffer(int pcount):bfr(new double[pcount]),count(pcount){}\n        // Copy constructor.\n    FloatBuffer(const FloatBuffer& other) : count(other.count)\n        , bfr(new double[other.count])\n    { std::copy(other.bfr, other.bfr + count, bfr); }\n    // Copy assignment operator - source code is obvious\n    FloatBuffer& operator=(const FloatBuffer& other) {\n        if (this != &other) {\n          if ( bfr != nullptr) \n            delete[] bfr; // free memory of the current object\n            count = other.count;\n            bfr = new double[count]; //re-allocate\n            std::copy(other.bfr, other.bfr + count, bfr);\n        }\n        return *this;\n    }\n    // Move constructor to enable move semantics\n    // The Modern STL containers supports move sementcis\n    FloatBuffer(FloatBuffer&& other) : bfr(nullptr) , count(0) {\n    cout << \"in move constructor\" << endl;\n    // since it is a move constructor, we are not copying elements from\n    // the source object. We just assign the pointers to steal memory\n    bfr = other.bfr;\n    count = other.count;\n    // Now that we have grabbed our memory, we just assign null to\n    // source pointer\n    other.bfr = nullptr;\n    other.count = 0;\n    }\n// Move assignment operator.\nFloatBuffer& operator=(FloatBuffer&& other) {\n    if (this != &other)\n    {\n        // Free the existing resource.\n        delete[] bfr;\n       // Copy the data pointer and its length from the\n       // source object.\n       bfr = other.bfr;\n       count = other.count;\n       // We have stolen the memory, now set the pinter to null\n       other.bfr = nullptr;\n       other.count = 0;\n    }\n    return *this;\n}\n\n};\nint main() {\n    // Create a vector object and add a few elements to it.\n    // Since STL supports move semantics move methods will be called.\n    // in this particular case (Modern Compilers are smart)\n    vector<FloatBuffer> v;\n    v.push_back(FloatBuffer(25));\n    v.push_back(FloatBuffer(75));\n}\n```\n\n`std::move`函数可用于指示(在传递参数时)候选对象是可移动的，编译器将调用适当的方法(移动赋值或移动构造函数)来优化与内存管理相关的成本。基本上，`std::move`是一个参考值的`static_cast`。\n\n# 智能指针\n\n对于 C++ 编程语言来说，管理对象生存期一直是一个有问题的领域。如果开发人员不小心，程序可能会泄漏内存并降低性能。智能指针是原始指针周围的包装类，其中诸如取消引用(*)和引用(->)等操作符被重载。智能指针可以进行对象生存期管理，充当有限形式的垃圾收集，释放内存等等。现代 C++ 语言有:\n\n*   `unique_ptr<T>`\n*   `shared_ptr<T>`\n*   `weak_ptr<T>`\n\n一个`unique_ptr<T>`是一个原始指针的包装器，它拥有包装器的独占所有权。以下代码片段将演示`<unique_ptr>`的使用:\n\n```cpp\n//---- Unique_Ptr.cpp\n#include <iostream>\n#include <deque>#include <memory>\nusing namespace std;\nint main( int argc , char **argv ) {\n    // Define a Smart Pointer for STL deque container...\n    unique_ptr< deque<int> > dq(new deque<int>() );\n    //------ populate values , leverages -> operator\n    dq->push_front(10); dq->push_front(20);\n    dq->push_back(23); dq->push_front(16);\n    dq->push_back(41);\n    auto dqiter = dq->begin();\n    while ( dqiter != dq->end())\n    { cout << *dqiter << \"\\n\"; dqiter++ ; }\n    //------ SmartPointer will free reference\n    //------ and it's dtor will be called here\n    return 0;\n}\n```\n\n`std::shared_ptr`是一个智能指针，它使用引用计数来跟踪对对象特定实例的引用。当指向底层对象的最后一个剩余`shared_ptr`被破坏或重置时，底层对象被破坏:\n\n```cpp\n//----- Shared_Ptr.cpp\n#include <iostream>\n#include <memory>\n#include <stdio.h>\nusing namespace std;\n////////////////////////////////////////\n// Even If you pass shared_ptr<T> instance\n// by value, the update is visible to callee\n// as shared_ptr<T>'s copy constructor reference\n// counts to the orgininal instance\n//\n\nvoid foo_byvalue(std::shared_ptr<int> i) { (*i)++ ;}\n\n///////////////////////////////////////\n// passed by reference,we have not\n// created a copy.\n//\nvoid foo_byreference(std::shared_ptr<int>& i) { (*i)++ ; }\nint main(int argc, char **argv )\n{\n    auto sp = std::make_shared<int>(10);\n    foo_byvalue(sp);\n    foo_byreference(sp);\n    //--------- The output should be 12\n    std::cout << *sp << std::endl;\n}\n```\n\n`std:weak_ptr`是原始指针的容器。它是作为`shared_ptr`的副本创建的。`weak_ptr`副本的存在或销毁对`shared_ptr`或其其他副本没有影响。在一个`shared_ptr`的所有副本被销毁后，所有`weak_ptr`副本都变成空的。下面的程序演示了一种机制，帮助我们使用`weak_ptr`检测失效的指针:\n\n```cpp\n//------- Weak_Ptr.cpp\n#include <iostream>\n#include <deque>\n#include <memory>\n\nusing namespace std;\nint main( int argc , char **argv )\n{\n    std::shared_ptr<int> ptr_1(new int(500));\n    std::weak_ptr<int> wptr_1 = ptr_1;\n    {\n        std::shared_ptr<int> ptr_2 = wptr_1.lock();\n        if(ptr_2)\n        {\n            cout << *ptr_2 << endl; // this will be exeucted\n        }\n    //---- ptr_2 will go out of the scope\n    }\n\n    ptr_1.reset(); //Memory is deleted.\n\n    std::shared_ptr<int> ptr_3= wptr_1.lock();\n    //-------- Always else part will be executed\n    //-------- as ptr_3 is nullptr now \n    if(ptr_3)\n        cout << *ptr_3 << endl;\n    else\n        cout << \"Defunct Pointer\" << endl;\n    return 0;\n}\n```\n\n经典 C++ 有一个智能指针类型叫做`auto_ptr`，它已经从语言标准中删除了。需要使用`unique_ptr`来代替。\n\n# λ函数\n\nC++ 语言的主要新增内容之一是 Lambda 函数和 Lambda 表达式。它们是匿名函数，程序员可以在调用点定义它们来执行一些逻辑。这简化了逻辑，代码的可读性也显著提高。\n\n与其定义什么是 Lambda 函数，不如让我们写一段代码，帮助我们计算`vector<int>`中正数的个数。在这种情况下，我们需要过滤掉负值并计算其余的。我们将使用一个 STL `count_if`来编写代码:\n\n```cpp\n//LambdaFirst.cpp\n#include <iostream>\n#include <iterator>\n#include <vector>\n#include <algorithm>\nusing namespace std;\nint main() {\n    auto num_vect =\n        vector<int>{ 10, 23, -33, 15, -7, 60, 80};\n    //---- Define a Lambda Function to Filter out negatives\n    auto filter = [](int const value) {return value > 0; };\n    auto cnt= count_if(\n        begin(num_vect), end(num_vect),filter);\n    cout << cnt << endl;\n}\n```\n\n在前面的代码片段中，变量过滤器被分配了一个匿名函数，我们在`count_if STL`函数中使用过滤器。现在，让我们编写一个简单的 Lambda 函数，我们将在函数调用站点指定它。我们将使用 STL 累加来聚合向量中的值:\n\n```cpp\n//-------------- LambdaSecond.cpp\n#include <iostream>\n#include <iterator>\n#include <vector>\n#include <algorithm>\n#include <numeric>\nusing namespace std;\nint main() {\n    auto num_vect =\n        vector<int>{ 10, 23, -33, 15, -7, 60, 80};\n    //-- Define a BinaryOperation Lambda at the call site\n    auto accum = std::accumulate(\n        std::begin(num_vect), std::end(num_vect), 0,\n        [](auto const s, auto const n) {return s + n;});\n    cout << accum << endl;\n}\n```\n\n# 函子和 Lambdas\n\n在经典的 C++ 中，当使用 STL 时，我们通过重载函数运算符来编写转换过滤器和在 STL 容器上执行约简，从而广泛使用函数对象或函子:\n\n```cpp\n//----- LambdaThird.cpp\n#include <iostream>\n#include <numeric>\nusing namespace std;\n//////////////////////////\n// Functors to add and multiply two numbers\ntemplate <typename T>\nstruct addition{\n    T operator () (const T& init, const T& a ) { return init + a; }\n};\ntemplate <typename T>\nstruct multiply {\n    T operator () (const T& init, const T& a ) { return init * a; }\n};\nint main()\n{\n    double v1[3] = {1.0, 2.0, 4.0}, sum;\n    sum = accumulate(v1, v1 + 3, 0.0, addition<double>());\n    cout << \"sum = \" << sum << endl;\n    sum = accumulate(v1,v1+3,0.0, [] (const double& a ,const double& b   ) {\n        return a +b;\n    });\n    cout << \"sum = \" << sum << endl;\n    double mul_pi = accumulate(v1, v1 + 3, 1.0, multiply<double>());\n    cout << \"mul_pi = \" << mul_pi << endl;\n    mul_pi= accumulate(v1,v1+3,1, [] (const double& a , const double& b ){\n        return a *b;\n    });\n    cout << \"mul_pi = \" << mul_pi << endl;\n}\n```\n\n下面的程序通过编写一个玩具分类程序清楚地演示了 Lambda 的用法。我们将展示如何使用函数对象和 Lambdas 来编写等效的代码。代码是以通用的方式编写的，但它假设数字是预期的(`double`、`float`、`integer`，或用户定义的等价物):\n\n```cpp\n/////////////////\n//-------- LambdaFourth.cpp\n#include <iostream>\n#include <vector>\n#include <algorithm>\nusing namespace std;\n//--- Generic functions for Comparison and Swap\ntemplate <typename T>\nbool Cmp( T& a , T&b ) {return ( a > b ) ? true: false;}\ntemplate <typename T>\nvoid Swap( T& a , T&b ) { T c = a;a = b;b = c;}\n```\n\n`Cmp`和`Swap`是通用函数，分别用于在执行排序操作时比较相邻元素和交换元素:\n\n```cpp\ntemplate <typename T>\nvoid BubbleSortFunctor( T *arr , int length ) {\n    for( int i=0; i< length-1; ++ i )\n        for(int j=i+1; j< length; ++ j )\n            if ( Cmp( arr[i] , arr[j] ) )\n                Swap(arr[i],arr[j] );\n}\n```\n\n有了 Cmp 和 Swap，编写一个冒泡排序是一件简单的事情。我们需要一个嵌套循环，在这里我们将比较两个元素，如果 Cmp 返回 true，我们将调用 Swap 来交换值:\n\n```cpp\ntemplate <typename T>\nvoid BubbleSortLambda( T *arr , int length ) {\n    auto CmpLambda = [] (const auto& a , const auto& b )\n    { return ( a > b ) ? true: false; };\n    auto SwapLambda = [] ( auto& a , auto& b )\n    { auto c = a;a = b;b = c;};\n    for( int i=0; i< length-1; ++ i )\n        for(int j=i+1; j< length; ++ j )\n            if ( CmpLambda( arr[i] , arr[j] ) )\n                SwapLambda (arr[i],arr[j] );\n}\n```\n\n在前面的例程中，我们将比较和交换函数定义为 Lambdas。Lambda 函数是一种内联指定一段代码或表达式的机制，通常称为匿名函数。该定义可以用 C++ 语言指定的语法给出，并且可以分配给变量、作为参数传递或从函数返回。在前面的函数中，变量`CmpLambda`和`SwapLambda`是 Lambda 语法中指定的匿名函数的例子。Lambda 函数的主体与前面的函数版本没有太大区别。要了解关于 Lambda 函数和表达式的更多信息，您可以参考位于[http://en.cppreference.com/w/cpp/language/lambda](http://en.cppreference.com/w/cpp/language/lambda)的页面。\n\n```cpp\ntemplate <typename T>\nvoid Print( const T& container){\n    for(auto i = container.begin() ; i != container.end(); ++ i )\n        cout << *i << \"\\n\" ;\n}\n```\n\n`Print`例程只是循环遍历容器中的元素，并将内容打印到控制台:\n\n```cpp\nint main( int argc , char **argv ){\n    double ar[4] = {20,10,15,-41};\n    BubbleSortFunctor(ar,4);\n    vector<double> a(ar,ar+4);\n    Print(a);\n    cout << \"=========================================\" << endl;\n    ar[0] = 20;ar[1] = 10;ar[2] = 15;ar[3] = -41;\n    BubbleSortLambda(ar,4);\n    vector<double> a1(ar,ar+4);\n    Print(a1);\n    cout << \"=========================================\" << endl;\n}\n```\n\n# 合成、电流和部分功能应用\n\nLambdas 的一个优点是，您可以将两个函数组合在一起，创建一个函数组合，就像您在数学中所做的那样(使用最喜欢的搜索引擎阅读数学和函数编程上下文中的函数组合)。下面的程序演示了这个想法。这是一个玩具实现，编写通用实现超出了本章的范围:\n\n```cpp\n//------------ Compose.cpp\n//----- g++ -std=c++ 1z Compose.cpp\n#include <iostream>\nusing namespace std;\n//---------- base case compile time recursion\n//---------- stops here\ntemplate <typename F, typename G>\nauto Compose(F&& f, G&& g)\n{ return [=](auto x) { return f(g(x)); };}\n//----- Performs compile time recursion based\n//----- on number of parameters\ntemplate <typename F, typename... R>\nauto Compose(F&& f, R&&... r){\n    return [=](auto x) { return f(Compose(r...)(x)); };\n}\n```\n\n`Compose`是一个变量模板函数，编译器通过递归扩展`Compose`参数生成代码，直到处理完所有参数。在前面的代码中，我们使用了`[=]`来指示编译器，我们应该通过值来捕获 Lambda 主体中引用的所有变量。您可以在函数式编程的上下文中学习更多关于闭包和变量捕获的知识。C++ 语言通过值(以及使用`[&]`)或通过明确指定要捕获的变量(如`[&var]`)为`Capture`变量提供了灵活性。\n\n函数式编程范式基于美国数学家阿隆佐·邱奇发明的一种叫做 Lambda 演算的数学形式主义。Lambda 演算只支持一元函数，currying 是一种技术，它将一个多参数函数分解成一系列一次接受一个参数的函数求值。\n\n使用 Lambdas 并以特定的方式编写函数，我们可以在 C++ 中模拟 currying:\n\n```cpp\nauto CurriedAdd3(int x) {\n    return [x](int y) { //capture x\n        return [x, y](int z){ return x + y + z; };\n    };\n};\n```\n\n部分函数应用涉及将具有多个参数的函数转换为固定数量的参数。如果固定的参数数量小于函数的 arity(参数计数)，将返回一个新的函数，该函数需要其余的参数。当接收到所有参数时，将调用该函数。我们可以将部分应用视为某种形式的记忆，在那里缓存参数，直到我们接收到所有参数并调用它们。\n\n在下面的代码片段中，我们使用了像模板参数包和变量模板这样的构造。模板参数包是接受零个或多个模板参数(非类型、类型或模板)的模板参数。函数参数包是接受零个或多个函数参数的函数参数。至少包含一个参数包的模板称为变量模板。了解`sizeof...`构造需要一个关于参数包和变量模板的好主意。\n\n```cpp\ntemplate <typename... Ts>\nauto PartialFunctionAdd3(Ts... xs) {\n    //---- http://en.cppreference.com/w/cpp/language/parameter_pack\n    //---- http://en.cppreference.com/w/cpp/language/sizeof...\n    static_assert(sizeof...(xs) <= 3);\n    if constexpr (sizeof...(xs) == 3){\n        // Base case: evaluate and return the sum.\n        return (0 + ... + xs);\n    }\n    else{\n        // Recursive case: bind `xs...` and return another\n        return [xs...](auto... ys){\n            return PartialFunctionAdd3(xs..., ys...);\n        };\n    }\n}\nint main() {\n    // ------------- Compose two functions together\n    //----https://en.wikipedia.org/wiki/Function_composition\n    auto val = Compose(\n        [](int const a) {return std::to_string(a); },\n        [](int const a) {return a * a; })(4); // val = \"16\"\n    cout << val << std::endl; //should print 16\n    // ----------------- Invoke the Curried function\n    auto p = CurriedAdd3(4)(5)(6);\n    cout << p << endl;\n    //-------------- Compose a set of function together\n    auto func = Compose(\n        [](int const n) {return std::to_string(n); },\n        [](int const n) {return n * n; },\n        [](int const n) {return n + n; },\n        [](int const n) {return std::abs(n); });\n    cout << func(5) << endl;\n    //----------- Invoke Partial Functions giving different arguments\n    PartialFunctionAdd3(1, 2, 3);\n    PartialFunctionAdd3(1, 2)(3);\n    PartialFunctionAdd3(1)(2)(3);\n}\n```\n\n# 函数包装\n\n函数包装器是可以将任何函数、函数对象或 Lambdas 包装成可复制对象的类。包装器的类型取决于类的函数原型。`<functional>`头中的`std::function(<prototype>)`代表一个函数包装:\n\n```cpp\n//---------------- FuncWrapper.cpp Requires C++ 17 (-std=c++ 1z )\n#include <functional>\n#include <iostream>\nusing namespace std;\n//-------------- Simple Function call\nvoid PrintNumber(int val){ cout << val << endl; }\n// ------------------ A class which overloads function operator\nstruct PrintNumber {\n    void operator()(int i) const { std::cout << i << '\\n';}\n};\n//------------ To demonstrate the usage of method call\nstruct FooClass {\n    int number;\n    FooClass(int pnum) : number(pnum){}\n    void PrintNumber(int val) const { std::cout << number + val<< endl; }\n};\nint main() {\n    // ----------------- Ordinary Function Wrapped\n    std::function<void(int)> \n    displaynum = PrintNumber;\n    displaynum(0xF000);\n    std::invoke(displaynum,0xFF00); //call through std::invoke\n    //-------------- Lambda Functions Wrapped\n    std::function<void()> lambdaprint = []() { PrintNumber(786); };\n        lambdaprint();\n        std::invoke(lambdaprint);\n        // Wrapping member functions of a class\n        std::function<void(const FooClass&, int)>\n        class display = &FooClass::PrintNumber;\n        // creating an instance\n        const FooClass fooinstance(100);\n        class display (fooinstance,100);\n}\n```\n\n在接下来的部分中，我们将在代码中广泛使用`std::function`，因为它有助于将函数调用作为数据进行拖动。\n\n# 与管道操作员一起组合功能\n\nUnix 操作系统的命令行外壳允许一个函数的标准输出通过管道传输到另一个函数，以形成一个过滤器链。后来，这个特性成为大多数操作系统提供的命令行外壳的一部分。在编写函数式代码时，当我们通过函数组合来组合方法时，代码会因为深度嵌套而变得难以阅读。现在，使用现代 C++ 我们可以重载 pipe ( `|`)操作符，允许将几个函数链接在一起，就像我们在 Unix shell 或 Windows PowerShell 控制台中执行命令一样。这就是为什么有人把 LISP 语言重新命名为“许多恼人的和愚蠢的括号”。RxCpp 库广泛使用`|`运算符来组合函数。下面的代码帮助我们理解如何创建可管道化的函数。我们将看看如何在原则上实现这一点。这里给出的代码仅用于说明目的:\n\n```cpp\n//---- PipeFunc2.cpp\n//-------- g++ -std=c++ 1z PipeFunc2.cpp\n#include <iostream>\nusing namespace std;\n\nstruct AddOne {\n    template<class T>\n    auto operator()(T x) const { return x + 1; }\n};\n```\n\n```cpp\nstruct SumFunction {\n    template<class T>\n    auto operator()(T x,T y) const { return x + y;} // Binary Operator\n};\n```\n\n前面的代码创建了一组可调用类，它将被用作组成函数链的一部分。现在，我们需要创建一个机制来将任意函数转换为闭包:\n\n```cpp\n//-------------- Create a Pipable Closure Function (Unary)\n//-------------- Uses Variadic Templates Paramter pack\ntemplate<class F>\nstruct PipableClosure : F{\n    template<class... Xs>\n    PipableClosure(Xs&&... xs) : // Xs is a universal reference\n    F(std::forward<Xs>(xs)...) // perfect forwarding\n    {}\n};\n//---------- A helper function which converts a Function to a Closure\ntemplate<class F>\nauto MakePipeClosure(F f)\n{ return PipableClosure<F>(std::move(f)); }\n// ------------ Declare a Closure for Binary\n//------------- Functions\n//\ntemplate<class F>\nstruct PipableClosureBinary {\n    template<class... Ts>\n    auto operator()(Ts... xs) const {\n        return MakePipeClosure([=](auto x) -> decltype(auto)\n        { return F()(x, xs...);}); }\n};\n//------- Declare a pipe operator\n//------- uses perfect forwarding to invoke the function\ntemplate<class T, class F> //---- Declare a pipe operator\ndecltype(auto) operator|(T&& x, const PipableClosure<F>& pfn)\n{ return pfn(std::forward<T>(x)); }\n\nint main() {\n    //-------- Declare a Unary Function Closure\n    const PipableClosure<AddOne> fnclosure = {};\n    int value = 1 | fnclosure| fnclosure;\n    std::cout << value << std::endl;\n```\n\n```cpp\n    //--------- Decalre a Binary function closure\n    const PipableClosureBinary<SumFunction> sumfunction = {};\n    int value1 = 1 | sumfunction(2) | sumfunction(5) | fnclosure;\n    std::cout << value1 << std::endl;\n}\n```\n\n现在，我们可以创建一个以一元函数为参数的`PipableClosure`实例，并将对闭包的一系列调用链接(或组合)在一起。前面的代码片段应该在控制台上打印三个。我们还创建了一个`PipableBinaryClosure`实例，将一元函数和二元函数串在一起。\n\n# 其他功能\n\n到目前为止，我们已经介绍了从 C++ 11 标准开始的语言最重要的语义变化。本章的目的是强调在编写惯用的现代 C++ 程序时可能有用的关键变化。C++ 17 标准在语言中加入了更多的东西。我们将突出这种语言的几个特点来结束这次讨论。\n\n# 折叠表达式\n\nC++ 17 标准增加了对 fold 表达式的支持，以简化变量函数的生成。编译器进行模式匹配，并通过推断程序员的意图来生成代码。下面的代码片段演示了这个想法:\n\n```cpp\n//---------------- Folds.cpp\n//--------------- Requires C++ 17 (-std=c++ 1z )\n//--------------- http://en.cppreference.com/w/cpp/language/fold\n#include <functional>\n#include <iostream>\n\nusing namespace std;\ntemplate <typename... Ts>\nauto AddFoldLeftUn(Ts... args) { return (... + args); }\ntemplate <typename... Ts>\nauto AddFoldLeftBin(int n,Ts... args){ return (n + ... + args);}\ntemplate <typename... Ts>\nauto AddFoldRightUn(Ts... args) { return (args + ...); }\ntemplate <typename... Ts>\nauto AddFoldRightBin(int n,Ts... args) { return (args + ... + n); }\ntemplate <typename T,typename... Ts>\nauto AddFoldRightBinPoly(T n,Ts... args) { return (args + ... + n); }\ntemplate <typename T,typename... Ts>\nauto AddFoldLeftBinPoly(T n,Ts... args) { return (n + ... + args); }\n\nint main() {\n    auto a = AddFoldLeftUn(1,2,3,4);\n    cout << a << endl;\n    cout << AddFoldRightBin(a,4,5,6) << endl;\n    //---------- Folds from Right\n    //---------- should produce \"Hello  World C++\"\n    auto b = AddFoldRightBinPoly(\"C++ \"s,\"Hello \"s,\"World \"s );\n    cout << b << endl;\n    //---------- Folds (Reduce) from Left\n    //---------- should produce \"Hello World C++\"\n    auto c = AddFoldLeftBinPoly(\"Hello \"s,\"World \"s,\"C++ \"s );\n    cout << c << endl;\n}\n```\n\n控制台上的预期输出如下\n\n```cpp\n10\n 25\n Hello World C++\n Hello World C++\n```\n\n# 变体类型\n\n变体的古怪定义是“类型安全联合”。我们可以在定义变体时给出一个类型列表作为模板参数。在任何给定时间，对象将只保存模板参数列表中的一种类型的数据。如果我们试图访问不保存当前值的索引，将会抛出`std::bad_variant_access`。以下代码不处理此异常:\n\n```cpp\n//------------ Variant.cpp\n//------------- g++ -std=c++ 1z Variant.cpp\n#include <variant>\n#include <string>\n#include <cassert>\n#include <iostream>\nusing namespace std;\n\nint main(){\n    std::variant<int, float,string> v, w;\n    v = 12.0f; // v contains now contains float\n    cout << std::get<1>(v) << endl;\n    w = 20; // assign to int\n    cout << std::get<0>(w) << endl;\n    w = \"hello\"s; //assign to string\n    cout << std::get<2>(w) << endl;\n}\n```\n\n# 其他重要话题\n\n现代 C++ 支持语言级并发、内存保证和异步执行等特性，这些将在接下来的两章中介绍。该语言支持可选数据类型和`std::any`类型。最重要的特性之一是大多数 STL 算法的并行版本。\n\n# 基于范围的循环和观察\n\n在本节中，我们将在自己编写的自定义类型上实现基于范围的 for 循环，以帮助您理解如何将本章前面提到的所有内容组合起来编写支持现代习惯用法的程序。我们将实现一个类，该类在一个界限内返回一系列数字，并将实现对基于范围的 for 循环的值迭代的基础结构支持。首先，我们通过利用基于范围的 for 循环来编写“Iterable/Iterator”(又名“Enumerable/Enumerable”)版本。经过一些调整后，实现将被转换为可观察/观察者(反应式编程的关键接口)模式:这里的可观察/观察者模式的实现只是为了说明的目的，不应该被认为是这些模式的工业实力实现。\n\n下面的`iterable`类是嵌套类:\n\n```cpp\n// Iterobservable.cpp\n// we can use Range Based For loop as given below (see the main below)\n// for (auto l : EnumerableRange<5, 25>()) { std::cout << l << ' '; }\n// std::cout << endl;\n#include <iostream>\n#include <vector>\n#include <iterator>\n#include <algorithm>\n#include <functional>\nusing namespace std;\n\ntemplate<long START, long END>\nclass EnumerableRange {\npublic:\n\n    class iterable : public std::iterator<\n        std::input_iterator_tag, // category\n        long, // value_type\n        long, // difference_type\n        const long*, // pointer type\n        long> // reference type\n        {\n            long current_num = START;\n            public:\n                reference operator*() const { return current_num; }\n                explicit iterable(long val = 0) : current_num(val) {}\n                iterable& operator++() {\n                    current_num = ( END >= START) ? current_num + 1 :\n                        current_num - 1;\n                return *this;\n            }\n            iterable operator++(int) {\n                iterable retval = *this; ++(*this); return retval;\n            }\n            bool operator==(iterable other) const\n                { return current_num == other.current_num; }\n            bool operator!=(iterable other) const\n                { return !(*this == other); }\n    };\n```\n\n前面的代码实现了一个从`std::iterator`派生的内部类，以满足通过基于范围的循环可枚举的类型的要求。我们现在将编写两个公共方法(`begin()`和`end()`，因此类的消费者可以使用基于范围的循环:\n\n```cpp\niterable begin() { return iterable(START); }\n    iterable end() { return iterable(END >= START ? END + 1 :\n        END - 1); }\n};\n```\n\n现在，我们可以编写代码来使用前面的类，如下所示:\n\n```cpp\nfor (long l : EnumerableRange<5, 25>())\n    { std::cout << l << ' '; }\n```\n\n上一章我们定义了`IEnumerable<T>`界面。这个想法是坚持使用反应式扩展的文档。iterable 类与前一章中的`IEnumerable<T>`实现非常相似。如前一章所述，如果我们稍微调整一下代码，前面的类可以基于 push。让我们编写一个包含三种方法的`OBSERVER`类。我们将使用标准库中可用的函数包装来定义方法:\n\n```cpp\nstruct OBSERVER {\n    std::function<void(const long&)> ondata;\n    std::function<void()> oncompleted;\n    std::function<void(const std::exception &)> onexception;\n};\n```\n\n这里给出的`ObservableRange`类包含一个`vector<T>`，用于存储订户列表。当生成新号码时，该事件将通知所有订户。如果我们从异步方法中调度通知调用，那么消费者就与范围流的生产者分离了。我们还没有为以下类实现`IObserver/IObserver<T>`接口，但是我们可以通过订阅方法订阅通知:\n\n```cpp\ntemplate<long START, long END>\nclass ObservableRange {\n    private:\n        //---------- Container to store observers\n        std::vector<\n            std::pair<const OBSERVER&,int>> _observers;\n        int _id = 0;\n```\n\n我们将把用户列表存储在`std::vector`中作为`std::pair`。`std::pair`中的第一个值是对`OBSERVER`的引用，`std::pair`中的第二个值是唯一标识订户的整数。消费者应该使用 subscribe 方法返回的 ID 取消订阅:\n\n```cpp\n//---- The following implementation of iterable does\n//---- not allow to take address of the pointed value [ &(*it)\n//---- Eg- &(*iterable.begin()) will be ill-formed\n//---- Code is just for demonstrate Obervable/Observer\nclass iterable : public std::iterator<\n    std::input_iterator_tag, // category\n    long, // value_type\n    long, // difference_type\n    const long*, // pointer type\n    long> // reference type\n    {\n        long current_num = START;\n    public:\n        reference operator*() const { return current_num; }\n        explicit iterable(long val = 0) : current_num(val) {}\n        iterable& operator++() {\n            current_num = ( END >= START) ? current_num + 1 :\n                current_num - 1;\n            return *this;\n        }\n        iterable operator++(int) {\n            iterable retval = *this; ++(*this); return retval;\n        }\n        bool operator==(iterable other) const\n            { return current_num == other.current_num; }\n        bool operator!=(iterable other) const\n            { return !(*this == other); }\n        };\n    iterable begin() { return iterable(START); }\n    iterable end() { return iterable(END >= START ? END + 1 : END - 1); }\n// generate values between the range\n// This is a private method and will be invoked from the generate\n// ideally speaking, we should invoke this method with std::asnyc\nvoid generate_async()\n{\n    auto& subscribers = _observers;\n    for( auto l : *this )\n        for (const auto& obs : subscribers) {\n            const OBSERVER& ob = obs.first;\n            ob.ondata(l);\n    }\n}\n\n//----- The public interface of the call include generate which triggers\n//----- the generation of the sequence, subscribe/unsubscribe pair\npublic:\n    //-------- the public interface to trigger generation\n    //-------- of thevalues. The generate_async can be executed\n    //--------- via std::async to return to the caller\n    void generate() { generate_async(); }\n    //---------- subscribe method. The clients which\n    //----------- expects notification can register here\n    int subscribe(const OBSERVER& call) {\n        // https://en.cppreference.com/w/cpp/container/vector/emplace_back\n        _observers.emplace_back(call, ++ _id);\n        return _id;\n    }\n    //------------ has just stubbed unsubscribe to keep\n    //------------- the listing small\n    void unsubscribe(const int subscription) {}\n\n};\n\nint main() {\n    //------ Call the Range based enumerable\n    for (long l : EnumerableRange<5, 25>())\n        { std::cout << l << ' '; }\n    std::cout << endl;\n    // instantiate an instance of ObservableRange\n    auto j = ObservableRange<10,20>();\n    OBSERVER test_handler;\n    test_handler.ondata = [=](const long & r)\n    {cout << r << endl; };\n    //---- subscribe to the notifiactions\n    int cnt = j.subscribe(test_handler);\n    j.generate(); //trigget events to generate notifications\n    return 0;\n}\n```\n\n# 摘要\n\n在本章中，我们学习了 C++ 程序员在编写 Reactive 程序时应该熟悉的编程语言特性，或者任何种类的程序。我们讨论了类型推断、变量模板、右值引用和移动语义、Lambda 函数、初等函数编程、可管道操作符以及迭代器和观察器的实现。在下一章中，我们将学习 C++ 编程语言提供的并发编程支持。"
  },
  {
    "path": "docs/cpp-react-prog/03.md",
    "content": "# 三、C++ 中的语言级并发和并行\n\n自从 C++ 11 语言标准问世以来，C++ 就对并发编程有了极好的支持。在此之前，线程化是一件由特定平台库处理的事情。微软公司有自己的线程库，其他平台(GNU Linux/macOS X)支持 POSIX 线程模型。作为语言的一部分，线程机制帮助 C++ 程序员编写了可在多个平台上运行的可移植代码。\n\n最初的 C++ 标准发布于 1998 年，语言设计委员会坚信线程、文件系统、GUI 库等最好留给平台特定的库。赫伯·萨特在《多布斯博士杂志》上发表了一篇有影响力的文章，题为《免费午餐结束了》，他在文章中倡导利用当时处理器中可用的多核编程技术。在编写并行代码时，函数式编程模型非常适合这项任务。线程、Lambda 函数和表达式、移动语义和内存保证等特性帮助人们编写并发或并行代码，没有太多麻烦。本章旨在使开发人员能够利用线程库及其最佳实践。\n\n在本章中，我们将涵盖以下主题:\n\n*   什么是并发？\n*   一个使用多线程的特色 Hello World 程序\n*   如何管理线程的生存期和资源\n*   线程间共享数据\n*   如何编写线程安全的数据结构\n\n# 什么是并发？\n\n在基本层面上，并发性代表不止一个活动同时发生。我们可以将并发性与现实生活中的许多情况联系起来，比如一边看电影一边吃爆米花，或者同时用两只手执行不同的功能，等等。那么，什么是计算机中的并发？\n\n几十年前，计算机系统能够进行任务切换，多任务操作系统已经存在了很长时间。为什么计算领域突然对并发产生了新的兴趣？微处理器制造商通过向处理器中塞入越来越多的硅来提高计算能力。在这个过程的某个阶段，当他们到达基本的物理极限时，他们无法将更多的东西塞进同一个区域。那些时代的中央处理器一次只有一条执行路径，它们通过切换任务(指令流)运行多条指令路径。在中央处理器级别，只有一个指令流被执行，当事情发生得非常快时(与人类的感知相比)，用户感觉动作同时发生。\n\n大约在 2005 年，英特尔宣布了他们的新多核处理器(支持硬件级别的多种执行路径)，这是一个游戏规则的改变者。多核处理器不是一个处理器通过在它们之间切换来完成每一项任务，而是作为一种解决方案来并行执行它们。但这给程序员带来了另一个挑战；编写代码来利用硬件级的并发性。此外，与任务切换造成的假象相比，实际硬件并发行为的问题也出现了。在多核处理器问世之前，芯片制造商一直在竞相提高计算能力，预计在 21 世纪第一个十年结束之前，计算能力可能会达到 10 千兆赫。正如赫伯·萨特在*免费午餐结束*([http://www.gotw.ca/publications/concurrency-ddj.htm](http://www.gotw.ca/publications/concurrency-ddj.htm))中所说，“*如果软件要利用这种增加的计算能力，它必须被设计成同时运行多个任务*”。赫伯警告程序员，那些忽视并发性的人在编写程序时也必须考虑到这一点。\n\n现代 C++ 标准库提供了一套支持并发和并行的机制。首先，`std::thread`与同步对象(如`std::mutex`、`std::lock_guards`、**、**、`std::unique_lock`、`std::condition_variables`等)一起授权程序员使用标准 C++ 编写并发多线程代码。其次，使用基于任务的并行性(如。NET 和 Java)，C++ 引入了类`std::future`和`std::promise`，两者成对工作，分离函数调用，等待结果。\n\n最后，为了避免管理线程的额外开销，C++ 引入了一个名为`std::async`的类，这将在下一章详细介绍，其中讨论的重点将是编写无锁并发程序(嗯，至少尽可能最小化锁)。\n\nConcurrency is when two or more threads or execution paths can start, run, and complete in overlapping time periods (in some kind of interleaved execution). Parallelism means two tasks can run at the same time (like you see on a multicore CPU). Concurrency is about response time and parallelism is mostly about exploiting available resources.\n\n# 你好并发世界(使用标准::线程)\n\n现在，让我们开始使用`std::thread`库的第一个程序。您应该使用 C++ 11 或更高版本来编译我们将在本章中讨论的程序。在进入多线程 Hello World 之前，让我们举一个简单、经典的 Hello World 示例作为参考:\n\n```cpp\n//---- Thanks to Dennis Ritchie and Brian Kernighan, this is a norm for all languages\n#include <iostream> \nint main() \n{ \n   std::cout << \"Hello World\\n\"; \n} \n```\n\n这个程序只是将 Hello World 写入标准输出流(主要是控制台)。现在，让我们看另一个例子，它做同样的事情，但是使用一个后台线程(通常称为工作线程):\n\n```cpp\n#include <iostream> \n#include <thread> \n#include <string> \n//---- The following function will be invoked by the thread library \nvoid thread_proc(std::string msg) \n{ \n   std::cout << \"ThreadProc msg:\" << msg; \n}  \nint main() \n{ \n   // creates a new thread and execute thread_proc on it. \n   std::thread t(thread_proc, \"Hello World\\n\");  \n   // Waiting for the thread_proc to complete its execution \n   // before exiting from the program \n   t.join(); \n} \n```\n\n与传统代码的第一个区别是包含了`<thread>`标准头文件。所有多线程支持函数和类都在这个新的头中声明。但是为了实现同步和共享数据保护，支持类在其他头中可用。如果您熟悉 Windows 或 POSIX 系统中的平台级线程，所有线程都需要一个初始函数。标准库也遵循同样的概念。在这个例子中，`thread_proc`函数是在主函数中声明的线程的初始函数。初始函数(通过函数指针)在`std::thread`对象`t`的构造函数中指定，构造开始线程的执行。\n\n最显著的区别是，现在应用将新线程(后台线程)的消息写入标准输出流，这导致在这个应用中有两个线程或一个执行路径。一旦新线程被启动，主线程就继续执行。如果主线程没有等待新启动的线程完成，则`main()`函数将结束，因此这将是应用的结束——甚至在新线程有机会完成其执行之前。这就是在主线程结束前调用`join()`的原因，为了等待这里开始的新线程`t`。\n\n# 管理线程\n\n在运行时，执行从用户入口点`main()`开始(在启动代码执行之后)，并且它将在已经创建的默认线程中执行。因此，每个程序都至少有一个执行线程。在程序执行过程中，可以通过标准库或特定于平台的库创建任意数量的线程。如果中央处理器内核可以执行，这些线程可以并行运行。如果线程的数量多于 CPU 内核的数量，即使存在并行性，我们也不能同时运行所有的线程。所以，线程切换也发生在这里。一个程序可以从主线程启动任意数量的线程，并且这些线程在初始线程上并发运行。我们可以看到，一个程序线程的初始函数是`main()`，当主线程从执行中返回时，程序结束。这将终止所有并行线程。因此，主线程需要等到所有子线程完成执行。那么，让我们看看线程的启动和连接是如何发生的。\n\n# 线程启动\n\n在前面的例子中，我们看到初始化函数作为参数传递给`std::thread`构造函数，线程被启动。这个函数在自己的线程上运行。线程启动发生在线程对象的构造过程中，但是初始化函数也可以有其他选择。函数对象是线程类中另一个可能的参数。C++ 标准库确保`std::thread`可以与任何可调用类型一起工作。\n\n现代 C++ 标准支持通过以下方式初始化线程:\n\n*   函数指针(如前一节)\n*   实现调用运算符的对象\n*   希腊字母的第 11 个\n\n任何可调用实体都是初始化线程的候选对象。这使得`std::thread`能够接受带有重载函数调用运算符的类对象:\n\n```cpp\nclass parallel_job \n{ \npublic: \nvoid operator() () \n{ \n    some_implementation(); \n} \n};  \nparallel_job job; \nstd::thread t(job); \n```\n\n这里，新创建的线程将对象复制到其存储中，因此必须确保复制行为。在这里，我们也可以使用`std::move`来避免与复制相关的问题:\n\n```cpp\nstd::thread t(std::move(job)); \n```\n\n如果传递临时(右值)而不是函数对象，语法如下:\n\n```cpp\nstd::thread t(parallel_job()); \n```\n\n该代码可以被编译器解释为接受函数指针并返回`std::thread`对象的函数声明。但是，我们可以通过使用新的统一初始化语法来避免这种情况，如下所示:\n\n```cpp\nstd::thread t{ parallel_job() };\n```\n\n如下面的代码片段所示，一组额外的括号也可以避免将`std::thread`对象声明解释为函数声明:\n\n```cpp\nstd::thread t((parallel_job()));\n```\n\n启动线程的另一个有趣的方法是将 C++ Lambdas 作为参数提供给`std::thread`构造函数。Lambdas 可以捕获局部变量，从而避免不必要的使用任何参数。Lambdas 在编写匿名函数时非常有用，但这并不意味着它们应该在任何地方都使用。\n\nLambda 函数可以与线程声明一起使用，如下所示:\n\n```cpp\nstd::thread t([]{ \n    some_implementation(); \n}); \n```\n\n# 线程连接\n\n在 Hello World 示例中，您可能已经注意到在离开函数之前在`main()`末尾使用了`t.join()`。对相关线程实例上的`join()`的调用确保了启动的函数将等待直到后台线程完成它的执行。在没有连接的情况下，线程将在线程开始之前被终止，直到当前上下文完成(它们的子线程也将被终止)。\n\n`join()`是直接函数，要么等待线程完成，要么不完成。为了获得对线程的更多控制，我们有其他机制，如互斥、条件变量和未来，它们将在本章和下一章的后面部分讨论。对`join()`的调用清理了与线程相关联的存储，因此它确保对象不再与启动的线程相关联。这表明`join()`函数每个线程只能调用一次；对`joinable()`的呼叫在对`join()`的呼叫之后将总是返回假。前面有功能对象的例子可以修改如下理解`join()`:\n\n```cpp\nclass parallel_job \n{ \n   int& _iterations; \n\npublic: \n    parallel_job(int& input): _iterations(input) \n    {} \n\n    void operator() () \n    { \n        for (int i = 0; i < _iterations; ++ i) \n        { \n            some_implementation(i); \n        } \n    } \n}; \nvoid func() \n{ \n    int local_Val = 10000; \n    parallel_job job(local_Val); \n    std::thread t(job); \n\n    if(t.joinable()) \n        t.join(); \n} \n```\n\n在这种情况下，在`func()`函数结束时，验证线程对象以确认线程是否仍在执行。我们调用`joinable()`来查看它的返回值，然后再进行加入调用。\n\n为了防止等待`func()`，标准引入了一种机制来继续执行，即使父函数完成了它的执行。这可以使用另一个标准功能`detach()`来实现:\n\n```cpp\nif(t.joinable()) \n         t.detach(); \n```\n\n在分离线程之前，我们需要考虑几件事情；当`func()`退出时，`t`线程可能仍在运行。根据前面例子中给出的实现，线程正在使用在`func()`中创建的局部变量的引用，这不是一个好主意，因为旧的堆栈变量可以在大多数架构上随时被覆盖。在代码中使用`detach()`时，必须始终解决这些情况。处理这种情况最常见的方法是使一个线程成为独立的，并将数据复制到线程中，而不是共享它。\n\n# 将参数传递给线程\n\n因此，我们已经知道如何启动并等待一个线程。现在，让我们看看如何将参数传递给线程初始化函数。让我们看一个例子来找出一个数的阶乘:\n\n```cpp\nclass Factorial \n{ \nprivate: \n    long long myFact; \n\npublic: \n    Factorial() : myFact(1) \n    { \n    } \n\n    void operator() (int number) \n    { \n        myFact = 1; \n        for (int i = 1; i <= number; ++ i) \n        { \n            myFact *= i; \n        } \n        std::cout << \"Factorial of \" << number << \" is \" << myFact; \n    } \n}; \n\nint main() \n{ \n    Factorial fact; \n\n    std::thread t1(fact, 10); \n\n    t1.join(); \n} \n\n```\n\n从这个例子中，很明显，将参数传递到线程函数或线程可调用对象中可以通过将附加参数传递到`std::thread()`声明中来实现。有一点我们必须牢记在心；*传递的参数被复制到线程的内部存储中，以便进一步执行*。对于线程的执行来说，拥有自己的参数副本是很重要的，因为我们已经看到了与超出范围的局部变量相关的问题。为了进一步讨论将参数传递到线程中，让我们从本章回到我们的第一个 Hello World 示例:\n\n```cpp\nvoid thread_proc(std::string msg); \n\nstd::thread t(thread_proc, \"Hello World\\n\"); \n```\n\n在这种情况下，`thread_proc()`函数将`std::string`作为参数，但是我们将一个`const char*`作为参数传递给线程函数。只有在线程的情况下，参数才会被传递、转换和复制到线程的内部存储中。在这里，`const char*`将转换为`std::string`。在牢记这一点的同时，必须选择提供给线程的参数类型。让我们看看如果将指针作为参数提供给线程会发生什么:\n\n```cpp\nvoid thread_proc(std::string msg); \nvoid func() \n{ \n   char buf[512]; \n   const char* hello = \"Hello World\\n\"; \n   std::strcpy(buf, hello); \n\n   std::thread t(thread_proc, buf); \n   t.detach(); \n} \n```\n\n在前面的代码中，提供给线程的参数是指向局部变量`buf`的指针。在线程上发生`buf`到`std::string`的转换之前，`func()`功能很可能会退出。这可能会导致未定义的行为。这个问题可以通过在声明本身中将`buf`变量转换为`std::string`来解决，如下所示:\n\n```cpp\nstd::thread t(thread_proc, std::string(buf)); \n```\n\n现在，让我们看看您希望引用在线程中更新的情况。在典型的场景中，线程复制提供给线程的值以确保安全执行，但是标准库也提供了一种通过引用线程来传递参数的方法。在许多实际的系统中，您可能已经看到共享数据结构正在线程内部更新。下面的示例显示了如何在线程中实现引用传递:\n\n```cpp\nvoid update_data(shared_data& data);\n\nvoid another_func() \n{ \n   shared_data data; \n   std::thread t(update_data, std::ref(data)); \n   t.join(); \n   do_something_else(data); \n} \n```\n\n在前面的代码中，用`std::ref`包装传递给`std::thread`构造函数的参数确保了线程内部提供的变量引用了实际参数。您可能已经注意到线程初始化函数的函数原型正在接受对`shared_data`对象的引用，但是为什么您仍然需要`std::ref()`包装来调用线程呢？考虑下面的线程调用代码:\n\n```cpp\nstd::thread t(update_data, data);\n```\n\n在这种情况下，`update_data()`函数期望将`shared_data`参数视为对实际参数的引用。但是当用作线程初始化函数时，参数只是在内部复制。当调用`update_data()`时，它将传递一个对参数内部副本的引用，而不是对实际参数的引用。\n\n# 使用兰姆达斯\n\n现在，让我们看看 Lambda 表达式对于多线程的用处。在下面的代码中，我们将创建五个线程，并将它们放入一个向量容器中。每个线程将使用一个 Lambda 函数作为初始化函数。以下代码中初始化的线程正在按值捕获循环索引:\n\n```cpp\nint main() \n{ \n    std::vector<std::thread> threads; \n\n    for (int i = 0; i < 5; ++ i) \n    { \n        threads.push_back(std::thread( [i]() { \n            std::cout << \"Thread #\" << i << std::endl; \n        })); \n    } \n\n    std::cout << \"nMain function\"; \n\n    std::for_each(threads.begin(), threads.end(), [](std::thread &t) { \n        t.join(); \n    }); \n} \n```\n\n向量容器线程存储在循环内部创建的五个线程。一旦执行结束，它们就在`main()`函数的末尾连接起来。前面代码的输出可能如下所示:\n\n```cpp\nThread # Thread # Thread # Thread # Thread #\nMain function\n0\n4\n1\n3\n2\n```\n\n每次运行的程序输出可能不同。这个程序是展示与并发编程相关的非确定性的一个很好的例子。在下一节中，我们将讨论`std::thread`对象的移动属性。\n\n# 所有权管理\n\n从本章到目前为止讨论的例子中，您可能已经注意到启动线程的函数必须等待线程使用`join()`函数完成其执行，否则它将调用`detach()`，代价是程序失去对线程的控制。在现代 C++ 中，很多标准类型是可移动的，但不能复制；`std::thread`就是其中之一。这意味着在移动语义的帮助下，线程执行的所有权可以在`std::thread`实例之间移动。\n\n在许多情况下，我们希望将所有权转移到另一个线程，例如，如果我们希望线程在后台运行，而不在创建线程的函数上等待它。这可以通过将线程所有权传递给调用函数来实现，而不是等待它在创建的函数中完成。在另一个实例中，将所有权传递给某个其他函数，该函数将等待线程完成其执行。这两种情况都可以通过将所有权从一个线程实例传递给另一个线程实例来实现。\n\n为了进一步解释，让我们定义两个函数用作线程函数:\n\n```cpp\nvoid function1() \n{ \n    std::cout << \"function1()n\"; \n} \n\nvoid function2() \n{ \n    std::cout << \"function2()n\"; \n} \n```\n\n让我们看看从先前声明的函数中产生线程的主要函数:\n\n```cpp\nint main() \n{ \n    std::thread t1(function1); \n\n    // Ownership of t1 is transferred to t2 \n    std::thread t2 = std::move(t1);\n```\n\n在前面的代码中，一个新的线程在`main()`的第一行以`t1`开始。然后使用`std::move()`函数将所有权转移到`t2`，该函数调用与`t2`关联的`std::thread`的移动构造函数。现在，t1 实例没有相关的执行线程。初始化功能`function1()`现在与`t2`相关联:\n\n```cpp\n    t1 = std::thread(function2); \n```\n\n然后，使用右值启动一个新线程，该右值调用`std::thread`的移动分配运算符，该运算符与`t1`相关联。因为我们使用的是右值，所以不需要明确调用`std::move()`:\n\n```cpp\n    // thread instance Created without any associated thread execution \n    std::thread t3; \n\n    // Ownership of t2 is transferred to t3 \n    t3 = std::move(t2); \n```\n\n`t3`在没有任何执行线程的情况下被实例化，这意味着它正在调用默认构造函数。然后，通过明确调用`std::move()`函数，移动分配操作符将当前与`t2`关联的所有权转移到`t3`:\n\n```cpp\n    // No need to join t1, no longer has any associated thread of execution \n    if (t1.joinable())  t1.join(); \n    if (t3.joinable())  t3.join(); \n\n    return 0; \n} \n```\n\n最后，在程序退出之前，具有相关执行线程的`std::thread`实例被连接。这里，`t1`和`t3`是具有相关执行线程的实例。\n\n现在，让我们假设以下代码出现在前面示例中的线程`join()`之前:\n\n```cpp\nt1 = std::move(t3); \n```\n\n这里，实例`t1`已经与正在运行的函数(`function2`)相关联。当`std::move()`试图将`function1`的所有权转移回`t1`时，会调用`std::terminate()`终止程序。这保证了`std::thread`析构器的一致性。\n\n`std::thread`中的移动支持有助于将线程的所有权转移出函数。以下示例演示了这样一个场景:\n\n```cpp\nvoid func() \n{ \n    std::cout << \"func()n\"; \n} \n\nstd::thread thread_creator() \n{ \n    return std::thread(func); \n} \n\nvoid thread_wait_func() \n{ \n    std::thread t = thread_creator(); \n\n    t.join(); \n} \n```\n\n这里，`thread_creator()`函数返回与`func()`函数相关的`std::thread`。`thread_wait_func()`函数调用`thread_creator()`，然后返回线程对象，这是一个赋给`std::thread`对象的右值。这将线程的所有权转移到`std::thread`对象`t`中，对象`t`正在等待转移函数中线程执行的完成。\n\n# 线程间共享数据\n\n我们已经看到了如何启动一个线程以及管理它们的不同方法。现在，让我们讨论如何在线程之间共享数据。并发的一个关键特性是它能够在运行的线程之间共享数据。首先，让我们看看与线程访问公共(共享)数据相关的问题是什么。\n\n如果线程之间共享的数据是不可变的(只读)，就不会有问题，因为一个线程读取的数据不受其他线程是否读取相同数据的影响。线程开始修改共享数据的那一刻就是问题开始出现的时候。\n\n例如，如果线程正在访问一个公共数据结构，那么如果更新正在发生，那么与该数据结构相关联的不变量就会被破坏。在这种情况下，元素的数量存储在数据结构中，这通常需要修改多个值。考虑自平衡树或双向链表的删除操作。如果您没有做任何特殊的事情来确保，否则，如果一个线程正在读取数据结构，而另一个线程正在移除节点，读取线程很可能会看到部分移除节点的数据结构，因此不变量被破坏。这可能会永久破坏数据结构，并可能导致程序崩溃。\n\nAn invariant is a set of assertions that must always be true during the execution of a program or lifetime of an object. Placing proper assertion within the code to see whether invariants have been violated will result in robust code. This is a great way to document software as well as a good mechanism to prevent regression bugs. More can be read about this in the following Wikipedia article: [https://en.wikipedia.org/wiki/Invariant_(computer_science)](https://en.wikipedia.org/wiki/Invariant_(computer_science)).\n\n这通常会导致一种叫做*竞争条件*的情况，这是并发程序中最常见的 bug 原因。在多线程中，竞争条件意味着线程竞争执行各自的操作。这里，结果取决于在两个或更多线程中执行操作的相对顺序。通常，术语“竞争条件”意味着有问题的竞争条件；正常的比赛状态不会导致任何错误。有问题的竞争条件通常发生在操作完成需要修改两位或更多位数据的情况下，例如删除树数据结构或双链表中的一个节点。因为修改必须访问单独的数据，所以当另一个线程试图访问数据结构时，必须在单独的指令中修改这些数据。当前面的修改完成一半时，就会出现这种情况。\n\n竞争条件通常很难找到，也很难复制，因为它们发生在很短的执行窗口内。对于使用并发的软件，实现的主要复杂性来自于避免有问题的竞争条件。\n\n有许多方法来处理有问题的比赛条件。最常见和最简单的选择是使用*同步原语*，这是基于锁的保护机制。这通过使用一些锁定机制来包装数据结构，以防止在数据结构执行期间访问其他线程。我们将在本章中详细讨论可用的同步原语及其用途。\n\n另一个选择是改变数据结构及其不变量的设计，这样修改就保证了代码的顺序一致性，甚至跨多个线程。这是一种很难写程序的方式，通常被称为*无锁编程*。无锁编程和 C++ 内存模型将在[第 4 章](04.html#27GQ60-51c8384cc2cb48e691b461190723b468)、*c++ 异步和无锁编程*中介绍。\n\n然后，还有其他机制，例如将数据结构的更新作为事务处理，因为数据库的更新是在事务中完成的。目前，这个话题不在本书的讨论范围内，因此不在讨论范围内。\n\n现在，让我们考虑一下 C++ 标准中保护共享数据的最基本机制，即*互斥*。\n\n# 互斥体\n\n互斥是并发控制中用来防止竞争条件的一种机制。互斥锁的功能是防止一个执行线程进入其*临界区*，同时另一个并发线程进入其自己的临界区。它是一个可锁定的对象，用于在代码的关键部分需要独占访问时发出信号，从而限制执行中具有相同保护的其他并发线程以及内存访问。C++ 11 标准在标准库中引入了`std::mutex`类，以实现跨并发线程的数据保护。\n\n`std::mutex`类由`lock()`和`unlock()`函数组成，用于在代码中创建关键部分。在使用成员函数创建关键部分时，需要记住的一点是，永远不要跳过与锁定函数相关联的解锁函数来标记代码中的关键部分。\n\n现在，让我们讨论一下用于讨论带有线程的 Lambdas 的相同代码。在那里，我们观察到程序的输出由于具有公共资源`std::cout`和`std::ostream`操作符的竞争条件而被打乱。该代码现在正在使用`std::mutex`打印线程索引进行重写:\n\n```cpp\n#include <iostream> \n#include <thread> \n#include <mutex> \n#include <vector>  \nstd::mutex m; \nint main() \n{ \n    std::vector<std::thread> threads; \n\n    for (int i = 1; i < 10; ++ i) \n    { \n        threads.push_back(std::thread( [i]() { \n            m.lock(); \n            std::cout << \"Thread #\" << i << std::endl; \n            m.unlock();\n        })); \n    }      \n    std::for_each(threads.begin(), threads.end(), [](std::thread &t) { \n        t.join(); \n    }); \n} \n```\n\n前面代码的输出可能如下所示:\n\n```cpp\nThread #1 \nThread #2 \nThread #3 \nThread #4 \nThread #5 \nThread #6 \nThread #7 \nThread #8 \nThread #9 \n```\n\n在前面的代码中，互斥用于保护共享资源，即`std::cout`和级联的`std::ostream`运算符。与前面的例子不同，在代码中添加互斥体避免了混乱的输出，但是它会以随机的顺序出现。在`std::mutex`类中使用`lock()`和`unlock()`功能保证了输出不会乱码。但是，不建议直接调用成员函数，因为您需要在函数的每个代码路径上调用 unlock，包括异常情况。相反，C++ 标准引入了一个新的模板类`std::lock_guard`，它实现了互斥体的**资源获取是初始化**(**RAI**)习惯用法。它在构造函数中锁定提供的互斥体，并在析构函数中解锁它。这个模板类的实现可以在`<mutex>`标准头库中找到。前面的例子可以用`std::lock_guard`改写如下:\n\n```cpp\nstd::mutex m; \nint main() \n{ \n    std::vector<std::thread> threads;  \n    for (int i = 1; i < 10; ++ i) \n    { \n        threads.push_back(std::thread( [i]() { \n            std::lock_guard<std::mutex> local_lock(m); \n            std::cout << \"Thread #\" << i << std::endl; \n        })); \n    }      \n    std::for_each(threads.begin(), threads.end(), [](std::thread &t) { \n        t.join(); \n    }); \n}\n```\n\n在前面的代码中，保护关键部分的互斥体在全局范围内，并且每次线程执行时`std::lock_guard`对象都是 Lambda 的本地对象。这样，一旦对象被构造，互斥体就获得了锁。当 Lambda 执行结束时，它通过调用析构函数来解锁互斥体。\n\nRAII is a C++ idiom where the lifetime of entities such as database/file handles, socket handles, mutexes, dynamically allocated memory on the heap, and so on are bounded to the life cycle of the object holding it. You can read more about RAII at the following Wikipedia page: [https://en.wikipedia.org/wiki/Resource_acquisition_is_initialization](https://en.wikipedia.org/wiki/Resource_acquisition_is_initialization).\n\n# 避免死锁\n\n在处理互斥体时，可能出现的最大问题是死锁。要理解什么是死锁，就想象一个 iPod。iPod 要达到目的，既需要 iPod，也需要耳机。如果两个兄弟姐妹共用一个 iPod，就会出现两个人都想同时听音乐的情况。想象一下，一个人拿着 iPod，另一个人拿着耳机，两个人都不愿意分享他们拥有的东西。现在他们被卡住了，除非他们中的一个人试着变好，让另一个人听音乐。\n\n在这里，兄弟姐妹们在为一个 iPod 和一个耳机争吵，但回到我们的情况，线程们在为互斥锁争吵。这里，每个线程都有一个互斥体，并在等待另一个。这里不能进行互斥，因为每个线程都在等待另一个线程释放它的互斥。这个场景叫做**死锁**。\n\n避免死锁有时非常简单，因为不同的互斥体服务于不同的目的，但是有些情况下处理这种情况并不那么明显。我能给你的避免死锁的最好建议是总是以相同的顺序锁定多个互斥锁。这样，你就永远不会陷入僵局。\n\n考虑一个有两个线程的程序的例子；每个线程都打算单独打印奇数和偶数。由于两个线程的意图不同，程序使用两个互斥体来控制每个线程。两个线程共享的资源是`std::cout`。让我们来看看下面这个有死锁情况的程序:\n\n```cpp\n// Global mutexes \nstd::mutex evenMutex; \nstd::mutex oddMutex;  \n// Function to print even numbers \nvoid printEven(int max) \n{ \n    for (int i = 0; i <= max; i +=2) \n    { \n        oddMutex.lock(); \n        std::cout << i << \",\"; \n        evenMutex.lock(); \n        oddMutex.unlock(); \n        evenMutex.unlock(); \n    } \n} \n```\n\n`printEven()`功能定义为将小于`max`值的所有正偶数打印到标准控制台中。同样，让我们定义一个`printOdd()`函数来打印所有小于`max`的正奇数，如下所示:\n\n```cpp\n// Function to print odd numbers \nvoid printOdd(int max) \n{ \n    for (int i = 1; i <= max; i +=2) \n    { \n        evenMutex.lock(); \n        std::cout << i << \",\"; \n        oddMutex.lock(); \n        evenMutex.unlock(); \n        oddMutex.unlock(); \n\n    } \n} \n```\n\n现在，让我们编写`main`函数，使用之前定义的函数作为每个操作的线程函数，生成两个独立的线程来打印奇数和偶数:\n\n```cpp\nint main() \n{ \n    auto max = 100; \n\n    std::thread t1(printEven, max); \n    std::thread t2(printOdd, max); \n\n    if (t1.joinable()) \n        t1.join(); \n    if (t2.joinable()) \n        t2.join(); \n} \n```\n\n在本例中，`std::cout`由两个互斥体`printEven`和`printOdd`保护，它们以不同的顺序执行锁定。有了这段代码，我们总是以死锁告终，因为每个线程显然都在等待被另一个线程锁定的互斥体。运行这段代码会导致挂起。如前所述，死锁可以通过以相同的顺序锁定它们来避免，如下所示:\n\n```cpp\nvoid printEven(int max) \n{ \n    for (int i = 0; i <= max; i +=2) \n    { \n        evenMutex.lock(); \n        std::cout << i << \",\"; \n        oddMutex.lock(); \n        evenMutex.unlock(); \n        oddMutex.unlock(); \n    } \n}  \nvoid printOdd(int max) \n{ \n    for (int i = 1; i <= max; i +=2) \n    { \n        evenMutex.lock(); \n        std::cout << i << \",\"; \n        oddMutex.lock(); \n        evenMutex.unlock(); \n        oddMutex.unlock(); \n\n    } \n} \n```\n\n但是这个代码显然不干净。您已经知道，将互斥体与 RAII 习惯用法一起使用会使代码更加干净和安全，但是为了确保锁定的顺序，C++ 标准库引入了一个新的函数，`std::lock`—一个可以一次锁定两个或多个互斥体而没有死锁风险的函数。下面的例子显示了如何在我们之前的奇偶校验程序中使用它:\n\n```cpp\nvoid printEven(int max) \n{ \n    for (int i = 0; i <= max; i +=2) \n    { \n        std::lock(evenMutex, oddMutex); \n        std::lock_guard<std::mutex> lk_even(evenMutex, std::adopt_lock); \n        std::lock_guard<std::mutex> lk_odd(oddMutex, std::adopt_lock); \n        std::cout << i << \",\"; \n    } \n}  \nvoid printOdd(int max) \n{ \n    for (int i = 1; i <= max; i +=2) \n    { \n        std::lock(evenMutex, oddMutex); \n        std::lock_guard<std::mutex> lk_even(evenMutex, std::adopt_lock); \n        std::lock_guard<std::mutex> lk_odd(oddMutex, std::adopt_lock); \n\n        std::cout << i << \",\"; \n\n    } \n} \n```\n\n在这种情况下，线程执行一进入循环，对`std::lock`的调用就锁定了两个互斥体。为每个互斥体构造两个`std::lock_guard`实例。除了互斥实例之外，还向`std::lock_guard`提供了`std::adopt_lock`参数，以指示互斥体已经被锁定，它们应该只采用互斥体上现有锁的所有权，而不是试图在构造函数中锁定互斥体。这保证了安全解锁，即使在特殊情况下。\n\n但是`std::lock`可以帮助你避免在程序要求同时锁定两个或多个互斥体的情况下出现死锁；如果它们是单独获得的，也没有帮助。死锁是多线程程序中最难解决的问题之一。它最终依赖于程序员的纪律，不会陷入任何死锁情况。\n\n# 用 std::unique_lock 锁定\n\n与`std::lock_guard`相比，`std::unique_lock`在操作上更加灵活一点。`std::unique_lock`实例并不总是拥有与之相关联的互斥体。首先，您可以将`std::adopt_lock`作为第二个参数传递给构造函数，以管理类似于`std::lock_guard`的互斥锁。其次，通过将`std::defer_lock`作为第二个参数传递给构造函数，互斥体可以在构造过程中保持解锁状态。因此，在代码的后面，可以通过在同一个`std::unique_lock`对象上调用`lock()`来获取锁。但是`std::unique_lock`的灵活性是有代价的；就存储这些额外信息而言，它比`lock_guard`稍慢，需要更新。因此，建议使用`lock_guard`，除非`std::unique_lock`提供的灵活性确实需要。\n\n`std::unique_lock`的另一个有趣的特点是它可以转让所有权。由于`std::unique_lock`必须拥有其关联的互斥体，这导致了互斥体的所有权转移。与`std::thread`类似，`std::unique_lock`级也是只动型。C++ 标准库中所有的移动语义语言细微差别和右值引用处理也适用于`std::unique_lock`。\n\n类似于`std::mutex`的`lock()`和`unlock()`等成员函数的可用性，与`std::lock_guard`相比，增加了其在代码中使用的灵活性。在`std::unique_lock`实例被破坏之前释放锁的能力，这意味着如果很明显不再需要锁，你可以在代码的任何地方随意释放它。不必要地按住锁会大大降低应用的性能，因为等待锁的线程被阻止执行超过必要的时间。因此，`std::unique_lock`是 C++ 标准库引入的一个非常方便的特性，它支持 RAII 习惯用法，并且可以有效地最小化适用代码的关键部分的大小:\n\n```cpp\nvoid retrieve_and_process_data(data_params param) \n{ \n   std::unique_lock<std::mutex> local_lock(global_mutex, std::defer_lock); \n   prepare_data(param); \n\n   local_lock.lock(); \n   data_class data = get_data_to_process(); \n   local_lock.unlock(); \n\n   result_class result = process_data(data); \n\n   local_lock.lock(); \n   strore_result(result); \n} \n```\n\n在前面的代码中，您可以看到通过利用`std::unique_lock`的灵活性实现的细粒度锁定。当函数开始执行时，一个`std::unique_lock`对象被构造为处于解锁状态的`global_mutex`。数据立即用参数准备，参数不需要独占访问；它是自由执行的。在检索准备好的数据之前，`local_lock`正在使用`std::unique_lock`中的锁定成员功能标记关键部分的开始。数据检索一结束，锁就被释放，标志着关键部分的结束。紧接着，对`process_data()`函数的调用(同样不需要独占访问)被自由执行。最后，在执行`store_result()`函数之前，互斥锁被锁定以保护写操作，写操作更新处理结果。退出函数时，当`std::unique_lock`的本地实例被破坏时，锁被释放。\n\n# 条件变量\n\n我们已经知道互斥体可以用来共享公共资源和同步线程之间的操作。但是如果不小心的话，使用互斥锁的同步有点复杂并且容易出现死锁。在本节中，我们将讨论如何等待带有条件变量的事件，以及如何以更简单的方式使用它们进行同步。\n\n当使用互斥锁进行同步时，如果等待的线程已经获得了对互斥锁的锁定，它就不能被任何其他线程锁定。此外，等待一个线程通过定期检查由互斥体保护的状态标志来完成其执行是对 CPU 资源的浪费。这是因为这些资源可以被系统中的其他线程有效利用，而不必等待更长的时间。\n\n为了解决这些问题，C++ 标准库提供了条件变量的两种实现:`std::condition_variable`和`std::condition_variable_any`。两者都在`<condition_variable>`库头中声明，并且两个实现都需要使用互斥来同步线程。`std::condition_variable`的实施仅限于与`std::mutex`合作。另一方面，`std::condition_variable_any`可以处理任何满足类互斥标准(类互斥语义)的东西，因此`suffix _any`。由于其通用行为，`std::condition_variable_any`最终会消耗更多内存并降低性能。除非有真正的、量身定制的需求，否则不建议这样做。\n\n下面的程序是我们在讨论互斥锁时讨论的奇偶线程的实现，现在正在使用条件变量重新实现:\n\n```cpp\nstd::mutex numMutex; \nstd::condition_variable syncCond; \nauto bEvenReady = false; \nauto bOddReady  = false; \nvoid printEven(int max) \n{ \n    for (int i = 0; i <= max; i +=2) \n    { \n        std::unique_lock<std::mutex> lk(numMutex); \n        syncCond.wait(lk, []{return bEvenReady;}); \n\n        std::cout << i << \",\"; \n\n        bEvenReady = false; \n        bOddReady  = true; \n        syncCond.notify_one(); \n    } \n}\n```\n\n程序从声明一个互斥体、一个条件变量和两个全局布尔标志开始，这样我们就可以在两个线程之间同步它们。`printEven`函数在工作线程中执行，只打印从 0 开始的偶数。在这里，当它进入循环时，互斥用`std::unique_lock`而不是`std::lock_guard`来保护；我们一会儿就会看到原因。然后线程调用`std::condition_variable`中的`wait()`函数，传递锁对象和表示等待条件的 Lambda 谓词函数。这可以用任何返回 bool 的可调用对象来替换。在这个函数中，谓词函数返回`bEvenReady`标志，这样函数在变为真时继续执行。如果谓词返回 false，`wait()`函数将解锁互斥体，并等待另一个线程通知它，因此`std::unique_lock`对象在这里很方便，提供了锁定和解锁的灵活性。\n\n一旦`std::cout`打印出循环索引，`bEvenReady`标志被提升为假，`bOddReady`被提升为真。然后，对与`syncCond`关联的`notify_one()`函数的调用向等待的奇数线程发出信号，将奇数写入标准输出流:\n\n```cpp\nvoid printOdd(int max) \n{ \n    for (int i = 1; i <= max; i +=2) \n    { \n        std::unique_lock<std::mutex> lk(numMutex); \n        syncCond.wait(lk, []{return bOddReady;}); \n\n        std::cout << i << \",\"; \n\n        bEvenReady = true; \n        bOddReady  = false; \n        syncCond.notify_one(); \n    } \n} \n```\n\n`printOdd`函数在另一个工作线程中执行，只打印从`1`开始的奇数。像`printEven`函数一样，循环迭代并打印由全局声明的条件变量和互斥体保护的索引。与`printEven`函数不同，条件变量的`wait()`函数中使用的谓词返回`bOddReady`，并且`bEvenReady`标志被提升为`true`，而`bOddReady`标志被提升为`false`。随后，调用与`syncCond`关联的`notify_one()`函数向等待的偶数线程发出信号，将偶数写入标准输出流。偶数和奇数的交错打印一直持续到最大值:\n\n```cpp\nint main() \n{ \n    auto max = 10; \n    bEvenReady = true; \n\n    std::thread t1(printEven, max); \n    std::thread t2(printOdd, max); \n\n    if (t1.joinable()) \n        t1.join(); \n    if (t2.joinable()) \n        t2.join(); \n\n} \n```\n\n主功能启动两个后台线程，`t1`，关联`printEven`功能，`t2`，关联`printOdd`功能。当线程启动前通过将`bEvenReady`标志升至 true 来确认奇偶校验时，输出开始。\n\n# 线程安全的堆栈数据结构\n\n到目前为止，我们已经讨论了如何启动和管理线程，以及如何同步并发线程之间的操作。但是，当涉及到实际系统时，数据以数据结构的形式表示，必须根据情况适当选择数据结构，以保证程序的性能。在本节中，我们将讨论如何使用条件变量和互斥体设计并发堆栈。下面的程序是`std::stack`的包装器，它在库标题`<stack>`下声明，堆栈包装器将有不同的重载用于 pop 和 push 功能(这样做是为了保持列表较小，这也演示了我们如何调整顺序数据结构以在并发上下文中工作):\n\n```cpp\ntemplate <typename T> \nclass Stack \n{ \nprivate: \n    std::stack<T> myData; \n    mutable std::mutex myMutex; \n    std::condition_variable myCond; \n\npublic: \n    Stack() = default; \n    ~Stack() = default; \n    Stack& operator=(const Stack&) = delete; \n\n    Stack(const Stack& that) \n    { \n        std::lock_guard<std::mutex> lock(that.myMutex); \n        myData = that.myData; \n    }\n```\n\n`Stack`类包含模板类`std::stack`的对象，以及用于`std::mutex`和`std::condition_variable`的成员变量。该类的构造函数和析构函数被标记为默认，让编译器为它们生成一个默认实现，复制赋值运算符被标记为删除，以防止在编译时调用该类的赋值运算符。复制构造函数被定义，它通过调用它自己的复制赋值操作符来复制`std::stack`成员对象`myData`，该操作符由右侧对象的互斥体保护:\n\n```cpp\n      void push(T new_value) \n      { \n          std::lock_guard<std::mutex> local_lock(myMutex); \n          myData.push(new_value); \n          myCond.notify_one(); \n      } \n```\n\n会员功能`push()`正在包装`std::stack container`的`push`功能。如您所见，互斥成员变量`myMutex`被`std::lock_guard`对象锁定，以保护下一行的`push`操作。随后，使用成员`std::condition_variable`对象调用`notify_one()`函数，以引发一个事件，通过这个相同的条件变量通知等待的线程。您将在下面的代码清单中看到`pop`操作有两个重载，它们会等待这个条件变量发出信号:\n\n```cpp\n    bool try_pop(T& return_value) \n    { \n        std::lock_guard<std::mutex> local_lock(myMutex); \n        if (myData.empty()) return false; \n        return_value = myData.top(); \n        myData.pop(); \n        return true; \n    }\n```\n\n`try_pop()`函数以模板参数为参考。由于实现从不等待堆栈填充至少一个元素，因此使用`std::lock_guard`对象来保护线程。如果堆栈为空，函数返回`false`，否则返回`true`。这里，通过调用`std::stack`的`top()`函数，输出被分配为输入引用参数，该函数返回堆栈中最上面的元素，然后调用`pop()`函数从堆栈中清除最上面的元素。`pop`函数的所有重载都会调用`top()`函数，然后调用`std::stack`的`pop()`函数:\n\n```cpp\n    std::shared_ptr<T> try_pop() \n    { \n        std::lock_guard<std::mutex> local_lock(myMutex); \n        if (myData.empty()) return std::shared_ptr<T>(); \n\n        std::shared_ptr<T> return_value(std::make_shared<T>(myData.top())); \n        myData.pop(); \n\n        return return_value;\n    } \n```\n\n这是`try_pop()`函数的另一个重载，它返回模板类型的`std::shared_ptr`(智能指针)的一个实例。正如你已经看到的，`try_pop`函数重载，从不等待栈填充至少一个元素；因此，本实现使用`std::lock_guard`。如果内部堆栈为空，该函数返回一个`std::shared_ptr`的实例，并且不保存堆栈中的任何元素。否则，返回保存堆栈顶部元素的`std::shared_ptr`实例:\n\n```cpp\n    void wait_n_pop(T& return_value) \n    { \n        std::unique_lock<std::mutex> local_lock(myMutex); \n        myCond.wait(local_lock, [this]{ return !myData.empty(); }); \n        return_value = myData.top(); \n        myData.pop(); \n    }      \n    std::shared_ptr<T> wait_n_pop() \n    { \n        std::unique_lock<std::mutex> local_lock(myMutex); \n        myCond.wait(local_lock, [this]{ return !myData.empty(); }); \n        std::shared_ptr<T> return_value(std::make_shared<T>(myData.top())); \n        return return_value; \n    }   \n}; \n```\n\n到目前为止，`pop`函数的重载并没有等待堆栈填充至少一个空元素。为了实现这一点，增加了`pop`函数的两个重载，它使用了与`std::condition_variable`关联的等待函数。第一个实现返回模板值作为输出参数，第二个实现返回`std::shared_ptr`实例。两个功能都使用`std::unique_lock`来控制互斥量，以提供`std::condition_variable`的`wait()`功能。在`wait`功能中，`predicate`功能是检查堆栈是否为空。如果堆栈为空，则`wait()`功能解锁互斥体，并继续等待，直到收到来自`push()`功能的通知。一调用 push，谓词就返回 true，`wait_n_pop`继续执行。函数重载接受模板引用，并将顶部元素赋给输入参数，后者实现返回一个`std::shared_ptr`实例，保存顶部元素。\n\n# 摘要\n\n在本章中，我们讨论了 C++ 标准库中可用的线程库。我们看到了如何启动和管理线程，并讨论了线程库的不同方面，例如如何将参数传递到线程中、线程对象的所有权管理、线程之间的数据共享等等。C++ 标准线程库可以作为线程执行大多数可调用对象！我们已经看到了与线程相关联的所有可用可调用对象的重要性，例如`std::function`、Lambdas 和函子。我们讨论了 C++ 标准库中可用的同步原语，从简单的`std::mutex`开始，使用 RAII 习惯用法来保护互斥体免受未处理的退出情况的影响，以避免显式解锁，并使用类，如`std::lock_guard`和`std::unique_lock`。我们还讨论了线程同步上下文中的条件变量(`std::condition_variable`)。本章为现代 C++ 中引入的并发支持奠定了良好的基础，从而将本书的旅程推向函数式习惯用法。\n\n在下一章中，我们将介绍 C++ 中更多的并发库特性，例如基于任务的并行和无锁编程。"
  },
  {
    "path": "docs/cpp-react-prog/04.md",
    "content": "# 四、C++ 中的异步和无锁编程\n\n在前一章中，我们研究了现代 C++ 引入的线程库以及创建、管理和同步线程的各种方法。用线程编写代码的方式相当低级，容易出现与并发代码相关的潜在错误(死锁、实时锁等)。尽管现代 C++ 语言没有被许多程序员注意到，但它提供了一个标准的内存模型，有助于更好地编写并发代码。作为一种从基础开始的并发编程语言，一种语言必须向开发人员提供关于内存访问和运行时执行顺序的某些保证。如果我们使用诸如互斥体、条件变量和未来这样的结构来表示事件，就不需要知道内存模型。但是对内存模型及其保证的了解将帮助我们使用无锁编程技术编写更快的并发代码。可以使用称为原子操作的东西来模拟锁，我们将深入研究这种技术。\n\n正如我们在[第 2 章](02.html#12AK80-51c8384cc2cb48e691b461190723b468)、*现代 C++ 及其关键习惯用法之旅*中所讨论的，零成本抽象仍然是 C++ 编程语言最基本的原则之一。C++ 一直是系统程序员的语言，标准委员会设法在该语言支持的高级抽象机制和访问低级资源编写系统程序的能力之间取得了良好的平衡。C++ 公开了原子类型和一组关联的操作，以对程序的执行进行细粒度控制。标准委员会已经公布了内存模型的详细语义，该语言有一套库来帮助程序员利用它们。\n\n在前一章中，我们学习了如何使用条件变量同步不同线程中的操作。本章讨论标准库提供的使用*期货*执行基于任务的并行的工具。在本章中，我们将介绍:\n\n*   C++ 中基于任务的并行性\n*   C++ 内存模型\n*   原子类型和原子操作\n*   同步操作和内存排序\n*   如何编写无锁数据结构\n\n# C++ 中基于任务的并行性\n\n*任务*是一种可能与其他计算同时执行的计算。线程是任务的系统级表示。在前一章中，我们学习了如何通过构造一个`std::thread`对象来同时执行一个任务和其他任务，该对象以任务作为构造函数的参数。任务可以是任何可调用的对象，如函数、Lambda 或函子。但是这种同时使用`std::thread`执行函数的方法被称为*基于线程的方法*。并发执行的首选是*基于任务的方法*，这将在本章中讨论。与基于线程的方法相比，基于任务的方法的优势是在任务的(较高)概念级别上操作，而不是直接在线程和锁的较低级别上操作。基于任务的并行是通过以下标准库功能实现的:\n\n*   未来和承诺从与单独线程相关联的任务中返回值\n*   `packaged_task`帮助启动任务并提供返回结果的机制\n*   `async()`用于启动类似于函数调用的任务\n\n# 未来和承诺\n\nC++ 任务通常表现得像某种数据通道。发送端，通常称为承诺，将数据发送到接收端，通常称为**未来**。关于未来和承诺的重要概念是，它们能够在两个任务之间传递价值，而无需明确使用锁。值的传递由系统(运行时)本身处理。**未来**和**承诺**背后的基本概念很简单；当一个任务想要将一个值传递给另一个任务时，它会将该值放入**承诺**中。\n\n标准库确保与这个承诺相关的未来得到这个值。另一个任务可以从这个**未来的**读取这个值(下图必须从右向左读取):\n\n![](img/00006.jpeg)\n\n如果一个调用线程需要等待一个特定的*一次性事件*，未来就派上用场了。表示此事件的未来使其自身对调用线程可用，并且一旦未来就绪(当值被设置为相应的承诺时)，调用线程就可以访问该值。在执行过程中，未来可能会有与之相关的数据，也可能没有。一旦事件发生，数据将在未来可用，并且无法重置。\n\n与基于任务的并行性相关联的模板类在库标题`<future>`中声明。标准库中有两种期货可供选择:唯一期货(`std::future<>`)和共享期货(`std::shared_future<>`)。您可以将这些分别与智能指针`std::unique_ptr<>`和`std::shared_ptr<>` *、*关联起来。`std::future`实例是指关联事件的唯一实例。相反，`std::shared_future`的多个实例可能指向同一个事件。在`shared_future`的情况下，与公共事件相关联的所有实例将同时就绪，并且它们可以访问与该事件相关联的数据。模板参数为关联数据，没有关联数据的应使用`std::future<void>`和`std::shared_future<void>`模板规范。即使线程之间的数据通信由 futures 内部管理，但 future 对象本身并不提供同步访问。如果多个线程需要访问单个`std::future`对象，则必须使用互斥或其他同步机制来保护它们。\n\n类`std::future`和`std::promise`成对工作，分离任务调用并等待结果。对于一个`std::future<T>`对象`f`，我们可以使用`std::future`类函数`get()`访问与之关联的值`T`。同样的对于一个`std::promise<T>`，有两个 put 操作功能可用(`set_value()`和`set_exception()`)来匹配未来的`get()`。对于承诺对象，可以使用`set_value()`给它赋值，也可以使用`set_exception()`给它传递一个异常。例如，下面的伪代码可以帮助您了解如何在承诺中设置值(在`func1`中)以及如何在调用`future<T>:: get()`的函数中消耗东西(在`func2`中):\n\n```cpp\n// promise associated with the task launched \nvoid func1(std::promise<T>& pr) \n{ \n    try \n    { \n        T val; \n        process_data(val); \n        pr.set_value(val); // Can be retrieved by future<T>::get() \n    } \n    catch(...) \n    { \n        // Can be retrieved by future<T>::get() \n        // At the future level, when we call get(), the  \n        // get will propagate the exception  \n        pr.set_exception(std::current_exception()); \n    } \n} \n```\n\n在前一种情况下，`T`类型的*值*在处理并获得结果后被设置为承诺 *pr* 。如果在执行过程中发生任何异常，该异常也被设置为 promise。现在，让我们看看如何访问您设置的值:\n\n```cpp\n// future corresponding to task already launched \nvoid func2(std::future<T>& ft) \n{ \n    try \n    { \n        // An exception will be thrown here, if the corresponding  \n        // promise had set an exception ..otherwise, retrieve the  \n        // value sets by the promise.  \n        T result = ft.get() \n    } \n    catch(...)\n```\n\n```cpp\n    { \n        // Handle exception  \n    } \n} \n```\n\n这里，使用作为参数传递的将来值来访问相应承诺中设置的值。与`std::future()`关联的`get()`函数检索任务执行期间存储的值。对`get()`的调用必须准备好捕捉通过未来传输的异常并处理它。在解释完`std::packaged_task`之后，我们将展示一个完整的例子，期货和承诺在行动中协同工作。\n\n# 标准::打包任务\n\n现在，让我们讨论如何在需要结果的代码中获得与未来相关的返回值。`std::packaged_task`是一个模板类，可以在标准库中使用，在期货和承诺的帮助下实现基于任务的并行。通过在线程中设置未来和承诺，它简化了任务的设置，而没有任何共享结果的显式锁。一个`packaged_task`实例为`std::thread`提供了一个包装器，将返回值或异常捕获到一个承诺中。`std::packaged_task`中的会员功能`get_future()`会给你对应承诺关联的未来实例。让我们看一个例子，它使用一个打包的任务来寻找一个向量中所有元素的总和(promise 的工作深入到`packaged_task`的实现中):\n\n```cpp\n// Function to calculate the sum of elements in an integer vector \nint calc_sum(std::vector<int> v) \n{ \n    int sum = std::accumulate(v.begin(), v.end(), 0); \n    return sum; \n} \n\nint main() \n{ \n    // Creating a packaged_task encapsulates a function \n    std::packaged_task<int(std::vector<int>)> task(calc_sum); \n\n    // Fetch associated future from packaged_task \n    std::future<int> result = task.get_future(); \n\n    std::vector<int> nums{1,2,3,4,5,6,7,8,9,10}; \n\n    // Pass packaged_task to thread to run asynchronously \n    std::thread t(std::move(task), std::move(nums)); \n\n    t.join();\n```\n\n```cpp\n    // Fetch the result of packaged_task, the value returned by calc_sum() \n    int sum = result.get(); \n\n    std::cout << \"Sum = \" << sum << std::endl; \n    return 0; \n}\n```\n\n`packaged_task`对象以任务的类型作为其模板参数，以函数指针(`calc_sum`)作为构造函数参数。未来实例通过调用任务对象的`get_future()`函数获得。由于无法复制`packaged_task`实例，因此使用了显式的`std::move()`。这是因为它是一个资源句柄，负责它的任务可能拥有的任何资源。然后，对`get()`函数的调用从任务中提取结果并打印出来。\n\n现在，让我们看看`packaged_task`如何与 Lambdas 一起使用:\n\n```cpp\n    std::packaged_task<int(std::vector<int>)> task([](std::vector<int> \n    v) { \n        return std::accumulate(v.begin(), v.end(), 0); \n    }); \n```\n\n这里，不是函数指针，而是将一个 Lambda 传递到`packaged_task`的构造函数中。正如您在前面几章中已经看到的，对于一小段并发运行的代码，Lambdas 会派上用场。期货背后的主要概念是能够获得结果，而不必担心管理沟通的机制。此外，这两个操作在两个不同的线程中运行，因此是并行的。\n\n# 标准::异步\n\n现代 C++ 提供了一种执行任务的机制，比如一个可能并行执行也可能不并行执行的函数。这里指的是`std::async`*，内部管理线程细节。`std::async`以一个可调用对象为参数，返回一个`std::future`，用于存储已启动任务的结果或异常。让我们重写前面的示例，使用`std::async`计算向量中所有元素的总和:*\n\n```cpp\n// Function to calculate the sum of elements in a vector \nint calc_sum(std::vector<int> v) \n{ \n   int sum = std::accumulate(v.begin(), v.end(), 0); \n   return sum; \n} \n\nint main() \n{ \n   std::vector<int> nums{1,2,3,4,5,6,7,8,9,10}; \n\n   // task launch using std::async \n   std::future<int> result(std::async(std::launch::async, calc_sum,    std::move(nums))); \n\n   // Fetch the result of async, the value returned by calc_sum() \n   int sum = result.get(); \n\n   std::cout << \"Sum = \" << sum << std::endl; \n   return 0; \n} \n```\n\n首先，当使用`std::async`进行基于任务的并行时，任务的启动和从任务中获取结果遵循简单的语法，并且与任务执行很好地分离。在前面的代码中，`std::async`接受三个参数:\n\n*   `async`标志决定了`async`任务和`std::launch::async`的启动策略，意味着`async`在新的执行线程上执行任务。`std::launch::deferred`标志不会产生新的线程，但是会执行*懒惰评估*。如果两个标志都设置为`std::launch::async`和`std::launch::deferred`，则由实现决定是执行异步执行还是延迟评估。如果明确没有将任何启动策略传递到`std::async`中，那么选择执行方式还是要看实现。\n*   `std::async`的第二个参数是一个可调用对象，它可以是函数指针、函数对象或 Lambda。在本例中，`calc_sum`函数是在单独线程中执行的任务。\n*   第三个参数是任务的输入参数。通常，这是一个变量参数，它可以传递任务可调用对象所需的参数数量。\n\n现在，让我们看看`async`和 Lambda 是如何在同一个例子中走到一起的:\n\n```cpp\n// Fetch associated future from async\nstd::future<int> result( async([](std::vector<int> v) {\nreturn std::accumulate(v.begin(), v.end(), 0); \n}, std::move(nums))); \n```\n\n在这个例子中，可调用对象参数内部有一个 Lambda 函数，它返回`std::accumulate()`的结果。像往常一样，简单的操作和 Lambda 美化了代码的整体外观，提高了可读性。\n\n使用`async`，不用考虑线程和锁。但是，只要考虑异步执行计算的任务，您不知道将使用多少线程，因为这取决于内部实现，根据调用时可用的系统资源来决定。在决定使用多少线程之前，它会检查可用的空闲内核(处理器)。这指出了`async`的明显局限性，因为它需要用于共享需要锁的资源的任务。\n\n# C++ 内存模型\n\n经典的 C++ 本质上是一种单线程语言。即使人们用 C++ 编写多线程程序，他们也在使用各自的平台线程工具来编写它们。现代 C++ 可以被认为是一种并发编程语言。语言标准在标准库的帮助下提供了标准的线程和任务机制(正如我们已经看到的)。因为它是标准库的一部分，所以语言规范已经定义了事情应该如何以精确的方式在平台上运行。对于线程、任务等，拥有一致的平台无关行为是一个巨大的挑战，标准委员会处理得非常好。该委员会设计并指定了一个标准的内存模型，用于在程序运行时实现一致的行为。记忆模型由两个方面组成:\n\n*   **结构方面**，涉及数据在内存中的布局方式\n*   **并发**方面，处理内存的并发访问\n\n对于一个 C++ 程序，所有的数据都是由*对象*组成的。该语言将一个对象定义为一个*存储区域*，该区域用其类型和寿命来定义。对象可以是基本类型(如 int 或 double)的实例，也可以是用户定义类型的实例。有些对象可能有子对象，但有些没有。关键是每个变量都是一个对象，包括其他对象的成员对象，每个对象都至少占用一些内存位置。现在，让我们看看这与并发性有什么关系。\n\n# 内存访问和并发性\n\n对于多线程应用，所有东西都挂在这些内存位置上。如果多个线程访问不同的内存位置，一切正常。但是如果两个线程访问同一个内存位置，那么你必须非常小心。正如您在[第 3 章](03.html#1O8H60-51c8384cc2cb48e691b461190723b468)、*c++ 中的语言级并发和并行*中所看到的，多个线程试图从同一个内存位置读取不会带来任何麻烦，但是只要有任何线程试图修改公共内存位置中的数据，就会出现*竞争条件*的机会。\n\n有问题的竞争条件只能通过在多个线程中的访问之间强制排序来避免。正如在[第 3 章](https://cdp.packtpub.com/c___reactive_programming/wp-admin/post.php?post=48&action=edit#post_40)、*c++ 语言级并发和并行*中所讨论的，使用互斥锁的基于锁的内存访问是一个流行的选择。另一种方法是通过强制执行两个线程中的访问顺序来利用*原子操作*的同步属性。在本章的后面部分，您将看到使用原子操作来强制排序。\n\nAtomic operation appears to the rest of the system and occurs at once without being interrupted (no task switch happens during atomic operation) in concurrent programming. Atomicity is a guarantee of isolation from interrupts, signals, concurrent processes, and threads. More can be read on this topic at the Wikipedia article at [https://en.wikipedia.org/wiki/Linearizability](https://en.wikipedia.org/wiki/Linearizability).\n\n如果不同线程对单个内存位置的多次访问之间没有强制排序，则一次或两次访问都不是原子的。如果涉及写操作，则可能导致数据竞争，并可能导致未定义的行为。数据竞赛是一个严重的错误，必须不惜一切代价避免它。未定义的行为可以通过原子操作来避免，但这并不能防止竞争情况。原子操作确保当操作进行时线程切换永远不会发生。这是防止交叉存取内存的保证。原子操作保证了交叉存取存储器的排除(串行排序)，但不能防止竞争条件(因为有可能覆盖更新)。\n\n# 修改合同\n\n当程序或进程正在执行时，系统中的所有线程应该就修改顺序(对于内存)达成一致。每个程序都是在一个环境中执行的，这个环境涉及到指令流、内存、寄存器、堆、栈、缓存、虚拟内存等等。这个修改命令是程序员和系统之间的契约，由内存模型定义。该系统包括将程序变形为可执行代码的编译器(和链接器)、执行流中指定的指令集的处理器、高速缓存和程序的相关状态。合同要求要求程序员遵守某些规则，这使得系统能够生成完全优化的程序。程序员在编写代码访问内存时必须遵守的这一组规则(或试探法)是在标准库中引入的原子类型和原子操作的帮助下实现的。\n\n这些操作不仅是原子的，而且它们对程序的执行产生同步和顺序约束。与在[第 3 章](https://cdp.packtpub.com/c___reactive_programming/wp-admin/post.php?post=48&action=edit#post_40)、*中讨论的更高级别的基于锁的同步原语(互斥体和条件变量)相比，C++ 中的语言级并发和并行*可以根据您的需求定制同步和顺序约束。C++ 内存模型的重要之处在于:尽管该语言采用了许多现代编程习惯和语言特性，但 C++ 作为系统程序员的语言，对您的内存资源给予了更多的低级控制，以根据您的需要优化代码。\n\n# C++ 中的原子操作和类型\n\n通常，非原子操作可能被其他线程视为完成了一半。如[第 3 章](https://cdp.packtpub.com/c___reactive_programming/wp-admin/post.php?post=48&action=edit#post_40)、*c++ 语言级并发和并行*中所讨论的，在这种情况下，与共享数据结构相关的不变性将被打破。当对共享数据结构的修改需要修改多个值时，就会发生这种情况。最好的例子是二叉树的一个部分移除的节点。如果另一个线程试图同时从这个数据结构中读取，不变量将被破坏，并可能导致未定义的行为。\n\n使用*原子操作*，您不能从系统中的任何线程观察到一个半完成的操作，因为原子操作是不可分割的。如果与对象相关联的任何操作(如读取)都是原子的，那么对对象的所有修改也是原子的。C++ 提供了原子类型，因此您可以根据需要使用原子性。\n\n# 原子类型\n\n标准库定义的所有原子类型都可以在`<atomic>`头库找到。系统保证这些类型的原子性以及与这些类型相关的所有操作。有些操作可能不是原子的，但在这种情况下，系统会产生原子性的错觉。标准的原子类型使用成员函数`is_lock_free()`，允许用户确定对给定类型的操作是直接使用原子指令完成的(`is_lock_free()`返回`true`)还是由编译器和库使用内部锁完成的(`is_lock_free()`返回`false`)。\n\n`std::atomic_flag`在所有原子类型中是不同的。根据标准，这种类型的操作需要是原子的。因此，这不提供`is_lock_free()`成员功能。这是一个非常简单的类型，允许的操作最少，例如`test_and_set()`(可以查询或设置)或`clear()`(清除值)。\n\n根据`std::atomic<>`类模板的规范，其余原子类型遵循类似的签名。与`std::atomic_flag`相比，这些类型功能更全面，但并非所有操作都是原子的。*运营*的原子性也高度依赖于平台。在流行的平台上，内置类型的原子变体确实是无锁的，但这并不能保证在所有地方都是如此。\n\n不使用`std::atomic<>`模板类，可以使用实现提供的直接类型，如下表所示:\n\n| **原子型** | **对应专精** |\n| `atomic_bool` | `std::atomic<bool>` |\n| `atomic_char` | `std::atomic<char>` |\n| `atomic_schar` | `std::atomic<signed char>` |\n| `atomic_uchar` | `std::atomic<unsigned char>` |\n| `atomic_int` | `std::atomic<int>` |\n| `atomic_uint` | `std::atomic<unsigned>` |\n| `atomic_short` | `std::atomic<short>` |\n| `atomic_ushort` | `std::atomic<unsigned short>` |\n| `atomic_long` | `std::atomic<long>` |\n| `atomic_ulong` | `std::atomic<unsigned long>` |\n| `atomic_llong` | `std::atomic<long long>` |\n| `atomic_ullong` | `std::atomic<unsigned long long>` |\n| `atomic_char16_t` | `std::atomic<char16_t>` |\n| `atomic_char32_t` | `std::atomic<char32_t>` |\n| `atomic_wchar_t` | `std::atomic<wchar_t>` |\n\n除了所有这些基本的原子类型，C++ 标准库还为原子类型提供了一组`typedefs`，与标准库中可用的`typedefs`相比，如`std::size_t`。有一个简单的模式来识别相应的原子版本`typedefs`:对于任何标准`typedef T`，使用`atomic_ prefix` : `atomic_T`。下表列出了标准原子`typedefs`及其相应的内置`typedefs`:\n\n| **原子**T0】 | **标准库** `typedef` |\n| `atomic_size_t` | `size_t` |\n| `atomic_intptr_t` | `intptr_t` |\n| `atomic_uintptr_t` | `uintptr_t` |\n| `atomic_ptrdiff_t` | `ptrdiff_t` |\n| `atomic_intmax_t` | `intmax_t` |\n| `atomic_uintmax_t` | `uintmax_t` |\n| `atomic_int_least8_t` | `int_least8_t` |\n| `atomic_uint_least8_t` | `uint_least8_t` |\n| `atomic_int_least16_t` | `int_least16_t` |\n| `atomic_uint_least16_t` | `uint_least16_t` |\n| `atomic_int_least32_t` | `int_least32_t` |\n| `atomic_uint_least32_t` | `uint_least32_t` |\n| `atomic_int_least64_t` | `int_least64_t` |\n| `atomic_uint_least64_t` | `uint_least64_t` |\n| `atomic_int_fast8_t` | `int_fast8_t` |\n| `atomic_uint_fast8_t` | `uint_fast8_t` |\n| `atomic_int_fast16_t` | `int_fast16_t` |\n| `atomic_uint_fast16_t` | `uint_fast16_t` |\n| `atomic_int_fast32_t` | `int_fast32_t` |\n| `atomic_uint_fast32_t` | `uint_fast32_t` |\n| `atomic_int_fast64_t` | `int_fast64_t` |\n| `atomic_uint_fast64_t` | `uint_fast64_t` |\n\n`std::atomic<>`类模板不仅仅是一组专门化；它们有一个要扩展的主模板和一个用户定义类型的原子变体。作为通用模板类，支持的操作仅限于`load()`、`store()`、`exchange()`、`compare_exchange_weak()`和`compare_exchange_strong()`。原子类型上的每个操作都有一个可选的参数来指定所需的内存排序语义。内存排序的概念将在本章的后面部分详细介绍。现在，请记住，所有原子操作都可以分为三类:\n\n*   **店铺运营:**这些运营可以有`memory_order_relaxed`、`memory_order_release`或者`memory_order_seq_cst`下单\n*   **装载操作:**这些可以有`memory_order_relaxed`、`memory_order_consume`、`memory_order_acquire`或`memory_order_seq_cst`命令\n*   **读-修改-写操作:**这些操作可以有`memory_order_relaxed`、`memory_order_consume`、`memory_order_acquire`、`memory_order_release`、`memory_order_acq_rel`或`memory_order_seq_cst`顺序\n\n所有原子操作的默认内存排序是`memory_order_seq_cst`。\n\n与传统的标准 C++ 类型相比，标准原子类型不是*可复制的*或*可分配的*。这意味着它们没有复制构造函数或复制赋值运算符。除了直接成员函数之外，它们还支持从和到相应内置类型的隐式转换。对原子类型的所有操作都被定义为原子的，赋值和复制构造涉及两个对象。涉及两个不同对象的操作不能是原子的。在这两种操作中，值必须从一个对象读取，然后写入另一个对象。因此，这些操作不能被认为是原子的。\n\n现在，让我们看看您实际上可以对每个标准原子类型执行的操作，从`std::atomic_flag`开始。\n\n# 标准::原子标志\n\n`std::atomic_flag`代表一个布尔标志，是标准库中所有原子类型中最简单的。这是每个平台上所有操作都要求*无锁*的唯一类型。这种类型是非常基本的，因此它仅用作构建模块。\n\n必须始终用`ATOMIC_FLAG_INIT`初始化一个`std::atomic_flag`对象，以将状态设置为*清除*:\n\n```cpp\nstd::atomic_flag flg = ATOMIC_FLAG_INIT;\n```\n\n这是唯一需要这种初始化的原子类型，不管其声明的范围如何。一旦初始化，这种类型只允许三种操作:销毁它、清除它或设置对前一个值的查询。这些分别对应析构函数、`clear()`成员函数和`test_and_set()`成员函数。`clear()`是一个*商店*操作，而`test_and_set()`是一个读-修改-写操作，如前一节所述:\n\n```cpp\nflg.clear()\nbool val = flg.test_and_set(std::memory_order_relaxed);\n```\n\n在前面的代码片段中，`clear()`函数调用请求用默认的内存顺序清除标志，即`std:: memory_order_seq_cst`，而对`test_and set()`的调用使用了宽松的语义(在*宽松的顺序*中有更多关于这一点的内容)，明确用于设置标志和检索旧值。\n\n`std::atomic_flag`的原始实现使它成为自旋锁互斥的理想选择。让我们看一个自旋锁的例子:\n\n```cpp\nclass spin_lock\n{\n    std::atomic_flag flg;\n    public:\n    spin_lock() : flg(ATOMIC_FLAG_INIT){}\n    void lock() {\n        // simulates a lock here... and spin\n        while (flg.test_and_set(std::memory_order_acquire));\n        //----- Do some action here\n        //----- Often , the code to be guarded will be sequenced as\n        // sp.lock() ...... Action_to_Guard() .....sp.unlock()\n    }\n    void unlock() {\n        //------ End of Section to be guarded\n        flg.clear(std::memory_order_release); // release lock\n    }\n};\n```\n\n在前面的代码片段中，实例变量`flg`(属于`std::atomic_flag`类型)最初被清除。在锁定方法中，它试图通过测试`flg`来设置标志，以查看该值是否被清除。\n\n如果该值被清除，该值将被设置，我们将退出循环。只有通过`unlock()`方法清除标志时，标志中的值才会复位。换句话说，该实现通过在`lock()`中的繁忙等待来实现互斥。\n\n由于其局限性，`std::atomic_ flag`不能作为布尔原子类型，不支持任何*非修改查询*操作。所以，让我们看看`std::atomic<bool>`来补偿原子布尔标志的需求。\n\n# 标准::原子\n\n`std::atomic<bool>`与`std::atomic_flag`相比是全功能原子布尔类型。但是对于这种类型，复制构造和赋值都是不可能的。`std::atomic<bool>`对象的值最初可以是`true`或`false`。这种类型的对象可以从非原子`bool`构造或赋值:\n\n```cpp\nstd::atomic<bool> flg(true);\nflg = false;\n```\n\n关于原子类型的赋值运算符，有一点需要注意，那就是运算符返回非原子类型的值，而不是返回引用的常规方案。如果返回的是一个引用而不是一个值，那么就会产生这样一种情况:赋值的结果得到了另一个线程修改的结果，也就是说，如果它依赖于赋值运算符的结果。在将赋值运算符的结果作为非原子值返回时，可以避免这种额外的加载，并且您可以推断获得的值是实际存储的值。\n\n现在，让我们继续进行`std::atomic<bool>`支持的操作。首先，在`std::atomic<bool>`中可用的`store()`成员功能用于写操作(或者`true`或者`false`，它取代了`std::atomic_flag`的相应限制性`clear()`功能。此外，`store()`功能是原子存储操作。类似地，`test_and_set()`函数已被更通用的`exchange()`成员函数有效替代，该函数允许您用选定的新值替换存储的值并检索原始值。这是原子*读-修改-写*操作。然后，`std::atomic<bool>`通过对`load()`的显式调用支持一个简单的值的非修改查询，这是一个原子加载操作:\n\n```cpp\nstd::atomic<bool> flg;\nflg.store(true);\nbool val = flg.load(std::memory_order_acquire);\nval = flg.exchange(false, std::memory_order_acq_rel);\n```\n\n除了`exchange()`之外，`std::atomic<bool>`引入了执行读-修改-写操作的操作，该操作执行流行的原子**比较-交换** ( **CAS** )指令。如果当前值等于预期值，此操作将存储一个新值。这称为比较/交换操作。标准库原子类型中有两种操作实现:`compare_exchange_weak()`和`compare_exchange_strong()`。此操作将原子变量的值与提供的期望值进行比较，如果它们相等，则存储提供的值。如果这些值不相等，预期值将更新为原子变量的实际值。比较/交换功能的返回类型为 *bool* ，如果执行了存储，则为`true`；否则，就是`false`。\n\n对于`compare_exchange_weak()`，即使期望值和原值相等，店铺也可能不成功。在这种情况下，价值交换不会发生，功能将返回`false`。这通常发生在缺少单个比较和交换指令的平台上，这意味着处理器不能保证操作会自动执行。在这样的机器中，执行操作的线程可能在执行与操作相关联的指令序列的中途被切换出来，并且另一个线程将被操作系统调度在它的位置上，在给定的条件下，运行的线程多于可用处理器的数量。这种情况被称为**乱真故障**。\n\n由于`compare_exchange_weak()`会导致虚假故障，因此应在循环中使用:\n\n```cpp\nbool expected = false;\natomic<bool> flg;\n...\nwhile(!flg.compare_exchange_weak(expected, true));\n```\n\n在前面的代码中，只要预期是`false`，循环就继续迭代，这表示`compare_exchange_weak()`调用发生了虚假故障。相反，如果实际值不等于期望值，`compare_exchange_strong()`保证返回`false`。这可以避免像前面的情况那样需要循环，在前面的情况中，您想要知道与正在运行的线程相关的变量的状态。\n\n比较/交换函数可以采用两个内存排序参数，以便在成功和失败的情况下允许内存排序语义不同。这些内存排序语义只对存储操作有效，不能用于故障情况，因为存储操作不会发生:\n\n```cpp\nbool expected;\nstd::atomic<bool> flg;\n```\n\n```cpp\nb.compare_exchange_weak(expected, true, std::memory_order_acq_rel, std::memory_order_acquire);\nb.compare_exchange_weak(expected, true, std::memory_order_release);\n```\n\n如果您不指定任何内存排序语义，成功和失败的情况都将采用默认的`memory_order_seq_cst`。如果您没有为失败指定任何顺序，那么它被假定为与成功相同，除了顺序的发布部分被省略。`memory_order_acq_rel`变成`memory_order_acquire``memory_order_release`变成`memory_order_relaxed`。\n\n内存排序的规格和结果将在本章的*内存排序*部分详细讨论。现在，让我们来看看原子积分类型作为一个组的使用。\n\n# 标准原子积分类型\n\n与`std::atomic<bool>`类似，标准的原子整型既不能复制构造，也不能复制赋值。然而，它们可以从相应的非原子标准变体中构造和分配。除了强制的`is_lock_free()`成员函数外，标准的原子积分类型，如`std::atomic<int>`或`std::atomic<unsigned long long>`，也有`load()`、`store()`、`exchange()`、`compare_exchange_weak()`和`compare_exchange_strong()`成员函数，语义与`std::atomic<bool>`相似。\n\n原子类型的整型变量确实支持数学运算，如`fetch_add()`、`fetch_sub()`、`fetch_and()`、`fetch_or()`和`fetch_xor()`、复合赋值运算符(`+=`、`-=`、`&=`、`|=`和`^=`)以及带有`++ `和`--`的前后递增和递减运算符。\n\n命名函数，如`fetch_add()`和`fetch_sub()`，自动执行它们的操作并返回旧值，但是复合赋值运算符返回新值。前后递增/递减按照通常的 C/C++ 约定工作:后递增/递减执行操作，但返回旧值，前递增/递减运算符执行操作并返回新值。以下简单的示例可以轻松演示这些操作的规范:\n\n```cpp\nint main() \n{ \nstd::atomic<int> value; \n\nstd::cout << \"Result returned from Operation: \" << value.fetch_add(5) << 'n'; \nstd::cout << \"Result after Operation: \" << value << 'n'; \n\nstd::cout << \"Result returned from Operation: \" << value.fetch_sub(3) << 'n'; \nstd::cout << \"Result after Operation: \" << value << 'n'; \n\nstd::cout << \"Result returned from Operation: \" << value++ << 'n'; \nstd::cout << \"Result after Operation: \" << value << 'n'; \n\nstd::cout << \"Result returned from Operation: \" << ++ value << 'n'; \nstd::cout << \"Result after Operation: \" << value << 'n'; \n\nvalue += 1; \nstd::cout << \"Result after Operation: \" << value << 'n'; \n\nvalue -= 1; \nstd::cout << \"Result after Operation: \" << value << 'n'; \n} \n```\n\n这段代码的输出应该如下所示:\n\n```cpp\nResult returned from Operation: 0 \nResult after Operation: 5 \nResult returned from Operation: 5 \nResult after Operation: 2 \nResult returned from Operation: 2 \nResult after Operation: 3 \nResult returned from Operation: 4 \nResult after Operation: 4 \nResult after Operation: 5 \nResult after Operation: 4 \n```\n\n除了`std::atomic_flag`和`std::atomic<bool>`之外，第一个表中列出的所有其他原子类型都是原子积分类型。现在，让我们来看看原子指针特殊化，`std::atomic<T*>`。\n\n# 标准::原子<t>–指针算法</t>\n\n除了通常的一组操作如`load()`、`store()`、`exchange()`、`compare_exchange_weak()`和`compare_exchange_strong()`之外，原子指针类型还加载了指针算术操作。成员函数`fetch_add()`和`fetch_sub()`为类型提供操作支持，对存储的地址进行原子加法和减法，操作符`+=`和`-=`，以及前后递增/递减都使用`++ `和`--`操作符。\n\n运算符的工作方式与标准非原子指针算法相同。如果`obj`是`std::atomic<some_class*>`，则一个对象指向一组`some_class`对象的第一个条目。`obj+=2`将其更改为指向数组中的第三个元素，并返回指向数组中第三个元素的`some_class*`的原始指针。如*标准原子整数类型*部分所述，命名函数如`fetch_add()`和`fetch_sub`对原子类型执行操作，但将指针返回到数组中的第一个元素。\n\n原子操作的函数形式还允许在函数调用的附加参数中指定内存排序语义:\n\n```cpp\nobj.fetch_add(3, std::memory_order_release);\n```\n\n由于`fetch_add()`和`fetch_sub`都是读-修改-写操作，它们可以使用标准原子库中的任何内存排序语义。但是，对于运算符形式，无法指定内存排序，因此这些运算符将始终具有`memory_order_seq_cst`语义。\n\n# std::原子<>初级类模板\n\n标准库中的主类模板允许用户创建**用户定义类型** ( **UDT** )的原子变体。要将用户定义的类型用作原子类型，您必须在实现类之前遵循一些标准。对于一个用户定义的类 UDT，如果这个类型有一个简单的复制赋值操作符，`std::atomic<UDT>`是可能的。这意味着用户定义的类不应包含任何虚拟函数或虚拟基类，并且必须使用编译器生成的默认复制分配运算符。此外，用户定义类的每个基类和非静态数据成员都必须有一个简单的复制赋值运算符。这允许编译器执行`memcpy()` 或赋值操作的等效操作，因为没有用户编写的代码要执行。\n\n除了赋值操作符的要求，用户定义的类型必须是*位相等可比的*。这意味着您必须能够使用`memcmp()`比较相等的实例。这种保证是确保比较/交换操作正常进行所必需的。\n\n对于具有用户定义类型`T`，即`std::atomic<T>`的标准原子类型的实例，界面仅限于`std::atomic<bool>` : `load()`、`store()`、`exchange()`、`compare_exchange_weak()`、`compare_exchange_strong()`可用的操作，以及从类型`T`实例的赋值和转换。\n\n# 内存排序\n\n我们已经了解了标准库中可用的原子类型和原子操作符。在对原子类型执行操作时，我们需要为某些操作指定内存排序。现在，我们将讨论不同内存排序语义的意义和用例。原子操作背后的关键思想是跨多个线程提供数据访问的同步，这是通过强制执行顺序来实现的。例如，如果对数据的写入发生在对数据的读取之前，那么一切都会好起来。否则，你就麻烦了！标准库有六个内存排序选项，可应用于原子类型的操作:`memory_order_relaxed`、`memory_order_consume`、`memory_order_acquire`、`memory_order_release`、`memory_order_acq_rel`和`memory_order_seq_cst`。对于原子类型上的所有原子操作，默认情况下，`memory_order_seq_cst`是内存顺序，除非您指定其他内容。\n\n这六个选项可以分为三类:\n\n*   **顺序一致排序** : `memory_order_seq_cst`\n*   **获取-发布订单** : `memory_order_consume`、`memory_order_release`、`memory_order_acquire`和`memory_order_acq_rel`\n*   **轻松点餐** : `memory_order_relaxed`\n\n对于不同的内存排序模型，执行成本因不同的 CPU 而异。与阻止顺序一致的排序相比，不同的内存排序模型的可用性允许专家利用更细粒度的排序关系的更高性能，但是要根据需要选择合适的内存模型，应该了解这些选项如何影响程序的行为。让我们先看看顺序一致的模型。\n\n# 顺序一致性\n\n顺序一致性的概念是由莱斯利·兰波特在 1979 年定义的。顺序一致性为程序的执行提供了两个保证。首先也是最重要的，内存排序程序的指令是按源代码顺序执行的，否则编译器会保证源代码顺序的假象。然后，所有线程中的所有原子操作都有一个全局顺序。\n\n对于程序员来说，顺序一致性的全局排序行为(所有线程中的所有操作都发生在全局时钟中)是一个有趣的高地，但也是一个缺点。\n\n顺序一致性的有趣之处在于，代码按照我们对多个并发线程的直觉工作，但是系统需要做大量的后台工作。下面的程序是一个简单的例子，让我们了解顺序一致性:\n\n```cpp\nstd::string result; \nstd::atomic<bool> ready(false); \n\nvoid thread1() \n{ \n    while(!ready.load(std::memory_order_seq_cst)); \n    result += \"consistency\"; \n} \n\nvoid thread2() \n{ \n    result = \"sequential \"; \n    ready=true; \n} \n\nint main() \n{ \n    std::thread t1(thread1); \n    std::thread t2(thread2); \n    t1.join(); \n    t2.join(); \n\n    std::cout << \"Result : \" << result << 'n'; \n} \n\n```\n\n前面的程序在顺序一致性的帮助下同步线程`thread1`和`thread2`。由于顺序一致性，执行完全是*确定性的*，所以这个程序的输出总是如下:\n\n```cpp\nResult : sequential consistency \n```\n\n这里，`thread1`在 while 循环中等待，直到原子变量`ready`是`true`。一旦`thread2`中的*就绪*变为`true`，则`thread1`继续执行，因此结果总是以相同的顺序用字符串更新。顺序一致性的使用允许两个线程以相同的顺序查看其他线程中的操作，因此两个线程遵循相同的全局时钟。loop 语句还有助于保持两个线程同步的时间时钟。\n\n*获取-发布语义*的细节将在下一节中介绍。\n\n# 获取-发布顺序\n\n现在，让我们深入研究 C++ 标准库提供的内存排序语义。这是程序员对多线程代码排序的直觉开始消退的地方，因为在原子操作的获取-释放语义中，线程之间没有全局同步。这些语义只允许同一原子变量上的原子操作之间的同步。详细来说，在一个线程中执行的对原子变量的加载操作可以与在另一个线程中对同一原子变量进行的存储操作同步。程序员必须提取这个特征，在原子变量之间建立一个*先于*的关系，以便在线程之间同步。这使得使用 acquire-release 模型有点困难，但同时也更令人兴奋。acquire-release 语义缩短了实现无锁编程的旅程，因为您不需要担心线程的同步，但是不同线程中相同原子变量的同步是我们需要思考的问题。\n\n正如我们之前解释的，获取-发布语义的关键思想是发布操作与对同一原子变量的获取操作之间的同步，以及除此之外建立一个*排序常数*。现在，顾名思义，获取操作包括获取锁，锁包括用于读取原子变量的操作，例如`load()`和`test_and_set()`函数。因此，锁的释放是一个释放操作，包括原子操作，如`store()`和`clear()`。\n\n换句话说，锁定*互斥体*是一个获取操作，而解锁是一个释放操作。因此，在*临界区*中，变量的操作不能在两个方向上进行。但是，变量可以在临界区内移动，因为变量从不受保护的区域移动到受保护的区域。如下图所示:\n\n![](img/00007.jpeg)\n\n临界区包含单向屏障:获取屏障和释放屏障。同样的推理可以应用于启动一个线程和在一个线程上发出一个连接调用，以及与标准库可用的所有其他同步原语相关的操作。\n\n由于同步发生在原子变量级别，而不是线程级别，让我们重新回顾一下使用`std::atomic_flag`实现的自旋锁:\n\n```cpp\nclass spin_lock \n{ \n    std::atomic_flag flg; \n\npublic: \n    spin_lock() : flg(ATOMIC_FLAG_INIT) \n    {} \n\n    void lock() \n    { \n        // acquire lock and spin \n        while (flg.test_and_set(std::memory_order_acquire)); \n    } \n\n    void unlock() \n    { \n        // release lock \n        flg.clear(std::memory_order_release); \n    } \n}; \n```\n\n在本代码中，`lock()`功能是一个`acquire`操作。现在使用显式的获取内存排序标志，而不是使用上一个示例中使用的默认顺序一致内存排序。此外，`unlock()`函数也是一个使用默认内存顺序的发布操作，现在已经被显式的发布语义所取代。因此，具有两个线程顺序一致性的重量级同步被轻量级和高性能的获取-释放语义所取代。\n\n随着使用`spin_lock`的线程数量增加到两个以上，使用`std::memory_order_acquire`的一般获取语义将是不够的，因为锁定方法变成了获取-释放操作。因此，记忆模式必须改为`std::memory_order_acq_rel`。\n\n到目前为止，我们已经看到顺序一致的排序确保了线程之间的同步，而获取-释放排序则建立了多个线程上对同一原子变量的读写操作之间的排序。现在，让我们看看宽松内存排序的规范。\n\n# 宽松排序\n\n使用标签`std::memory_order_relaxed`以宽松的内存排序对原子类型执行的操作不是同步操作。与标准库中可用的其他排序选项相比，它们不会在并发内存访问之间强加顺序。宽松的内存排序语义只保证同一线程内同一原子类型的操作不能被重新排序，这种保证称为**修改顺序一致性**。事实上，宽松的排序只能保证原子性和修改顺序的一致性。因此，其他线程可以以不同的顺序看到这些操作。\n\n在不需要同步或排序的地方，可以有效地使用宽松的内存排序，原子性可以成为性能提升的额外优势。一个典型的例子是递增计数器，例如 **std::shared_ptr** 的引用计数器，它们只需要原子性。但是减少引用计数需要与这个模板类的析构函数进行获取-释放同步。\n\n让我们看一个简单的例子来计算以宽松排序产生的线程数:\n\n```cpp\nstd::atomic<int> count = {0}; \n\nvoid func() \n{ \n    count.fetch_add(1, std::memory_order_relaxed); \n} \n\nint main() \n{ \n    std::vector<std::thread> v; \n    for (int n = 0; n < 10; ++ n) \n    { \n        v.emplace_back(func); \n    } \n    for (auto& t : v) \n    { \n        t.join(); \n    } \n\n    std::cout << \"Number of spawned threads : \" << count << 'n'; \n} \n```\n\n在这段代码中，十个线程由`main()`函数和一个线程函数`func()`产生，其中在每个线程上，原子整数值使用原子操作`fetch_add()`增加 1。与`std::atomic<int>`提供的复合赋值运算符以及后递增和前递增运算符相比，`fetch_add()`函数可以接受内存排序参数，它就是`std::memory_order_relaxed`。\n\n程序打印程序中产生的线程数，如下所示:\n\n```cpp\nNumber of spawned threads : 10 \n```\n\n对于任何其他相关的内存排序标签，程序的输出保持不变，但是宽松的内存排序确保了原子性，从而确保了性能。\n\n到目前为止，我们已经讨论了不同内存模型的级别，以及它们对原子和非原子操作的影响。现在，让我们深入研究一个使用原子操作的无锁数据结构的实现。\n\n# 无锁数据结构队列\n\n正如我们已经知道的，实际系统中的数据通常以数据结构的形式表示，当涉及到数据结构上的并发操作时，性能是一个大问题。在[第 3 章](https://cdp.packtpub.com/c___reactive_programming/wp-admin/post.php?post=48&action=edit#post_40)、*c++*中的语言级并发和并行，我们学习了如何编写线程安全的堆栈。然而，我们使用锁和条件变量来实现它。为了解释如何编写无锁数据结构，让我们使用生产者/消费者范例编写一个非常基本的队列系统，而不使用锁或条件变量。这肯定会提高代码的性能。我们将从头开始推广它，而不是在标准数据类型上使用包装器。在这种情况下，我们假设只有一个生产者和一个消费者:\n\n```cpp\ntemplate<typename T> \nclass Lock_free_Queue \n{ \nprivate: \n    struct Node \n    { \n        std::shared_ptr<T> my_data; \n        Node* my_next_node; \n        Node() : my_next_node(nullptr) \n        {} \n    }; \n\n    std::atomic<Node*> my_head_node; \n    std::atomic<Node*> my_tail_node; \n\n    Node* pop_head_node() \n    { \n        Node* const old_head_node = my_head_node.load(); \n        if(old_head_node == my_tail_node.load()) \n        { \n            return nullptr; \n        } \n        my_head_node.store(old_head_node->my_next_node); \n        return old_head_node; \n    } \n```\n\n`Lock_free_stack`类包含一个表示队列节点(名为`Node`)的结构，其中数据成员表示节点(`my_data`)的数据，指针指向下一个节点。然后，该类包含两个指向用户定义结构`Node`的原子指针实例，该结构已经在该类中定义。一个实例存储指向队列头节点的指针，而另一个实例指向尾节点。最后，使用`private pop_head_node()`函数通过调用原子*存储*操作来检索队列的头节点，但前提是队列至少包含一个元素。这里，原子操作遵循默认的顺序一致的内存排序语义:\n\n```cpp\npublic: \nLock_free_Queue() : my_head_node(new Node), my_tail_node(my_head_node.load()) \n    {} \n    Lock_free_Queue(const Lock_free_Queue& other) = delete; \n    Lock_free_Queue& operator= (const Lock_free_Queue& other) = delete; \n\n    ~Lock_free_Queue() \n    { \n        while(Node* const old_head_node = my_head_node.load()) \n        { \n            my_head_node.store(old_head_node->my_next_node); \n            delete old_head_node; \n        } \n    }\n```\n\n构造队列对象时，头节点被实例化，尾节点指向该内存。复制构造函数和复制赋值运算符被标记为已删除，以防止它们被使用。在析构函数中，队列中的所有元素都被迭代删除:\n\n```cpp\n    std::shared_ptr<T> dequeue() \n    { \n        Node* old_head_node = pop_head_node(); \n        if(!old_head_node) \n        { \n            return std::shared_ptr<T>(); \n        } \n        std::shared_ptr<T> const result(old_head_node->my_data); \n        delete old_head_node; \n        return result; \n    } \n\n    void enqueue(T new_value) \n    { \n        std::shared_ptr<T> new_data(std::make_shared<T>(new_value)); \n        Node* p = new Node; \n        Node* const old_tail_node = my_tail_node.load(); \n        old_tail_node->my_data.swap(new_data); \n        old_tail_node->my_next_node = p; \n        my_tail_node.store(p); \n    } \n}; \n```\n\n前面的代码片段实现了标准的队列操作，即入队和出队。在这里，我们已经使用交换和存储原子操作确保了在入队和出队之间的关系之前有*发生。*\n\n# 摘要\n\n在本章中，我们讨论了标准库提供的编写基于任务的并行性的工具。我们看到了如何使用`std::packaged_task`和`std::async`的期货和承诺。我们讨论了现代 C++ 语言提供的新的多线程感知内存模型。之后，我们介绍了原子类型以及与之相关的操作。我们学到的最重要的东西是语言的各种记忆排序语义。简而言之，这一章和前一章将使我们能够对反应式编程模型的并发方面进行推理。\n\n在下一章中，我们将把注意力从语言和并发转移到反应式编程模型的标准接口上。我们将报道天文台！*"
  },
  {
    "path": "docs/cpp-react-prog/05.md",
    "content": "# 五、可观察对象介绍\n\n在最后三章中，我们学习了现代 C++ 的语言特性:多线程、无锁编程模型等等。这里涉及的主题可以被认为是开始学习反应式编程模型的一种先决条件。反应式编程模型保证了函数式编程、并发编程、调度器、对象/函数式编程、设计模式和事件流处理等技能。在前一章中，我们已经讨论或触及了函数式编程、对象/函数式编程以及一些与调度相关的主题。这一次，我们将覆盖设计模式的奇妙世界，来理解反应式编程的关键，特别是 Observables。在下一章中，我们将在进入 RxCpp 库之前处理事件流编程的主题。随着**四人组** ( **GoF** )出版了一本名为*设计模式:可重用面向对象软件的元素*的书，设计模式运动达到了临界质量。他将一组 23 种模式归类为创造型、结构型和行为型。GoF 目录在行为模式类别中定义了观察者模式。我们在这里要传递的一个关键信息是，可以通过对古老的 GoF 模式的了解来理解反应式编程模型。在本章中，我们将介绍:\n\n*   GoF 观察者模式\n*   GoF 观测器模式的局限性\n*   从整体上看待设计模式和可观察对象\n*   使用复合设计模式建模真实世界的层次结构\n*   使用访问者的复合材料行为处理\n*   展平复合并在迭代器模式中导航\n*   通过反转凝视从迭代器到可观察/观察者的转换！\n\n# GoF 观察者模式\n\nGoF 观察者模式在 GoF 书中也被称为*发布-订阅模式*。想法很简单。`EventSource`(发出事件的类)将与事件接收器(监听事件通知的类)具有一对多的关系。每个`EventSource`都会有一个事件接收器订阅的机制，以便获得不同类型的通知。一个`EventSource`可能会发出多个事件。当一个`EventSource`的状态发生变化或者在它的领域中发生重大事情时，它可以向成千上万的订阅者(事件接收器或监听器)发送通知。`EventSource`将浏览订户名单，并逐一通知他们。GoF 书是在世界大部分时间都在进行顺序编程的时候写的。并发性等主题大多与平台特定的库或`POSIX`线程库相关。我们将编写一个简单的 C++ 程序来演示观察者模式的整个思想。目的是快速理解观察者模式，而像健壮性这样的想法已经被赋予了次要的优先权。列表是独立的，易于理解:\n\n```cpp\n//-------------------- Observer.cpp \n#include <iostream> \n#include  <vector> \n#include <memory> \nusing namespace std; \n//---- Forward declaration of event sink \ntemplate<class T> \nclass EventSourceValueObserver; \n//----------A toy implementation of EventSource\ntemplate<class T> \nclass EventSourceValueSubject{ \n   vector<EventSourceValueObserver<T> *> sinks;  \n   T State; // T is expected to be a value type \n  public: \n   EventSourceValueSubject() { State = 0; } \n   ~EventSourceValueSubject() { \n       sinks.clear(); \n   } \n   bool Subscribe( EventSourceValueObserver<T> *sink ) { sinks.push_back(sink);} \n   void NotifyAll() { for (auto sink : sinks) { sink->Update(State); }} \n   T GetState() { return State; } \n   void SetState(T pstate) { State = pstate; NotifyAll(); } \n};\n```\n\n前面的代码片段实现了一个微不足道的`EventSource`，它可能会存储一个整数值作为状态。在现代 C++ 中，我们可以使用类型特征来检测消费者是否已经用整型实例化了这个类。由于我们的重点是阐明，我们没有添加与类型约束相关的断言。在下一个 C++ 标准中，有一个概念叫做**概念**(在其他语言中被称为约束)，它将有助于直接强制执行(没有类型特征)。在现实场景中，`EventSource`可能会存储大量变量或值流。其中的任何更改都将广播给所有订户。在`SetState`方法中，当`EventSource`类的消费者(事件接收器本身就是该类中的消费者)发生状态突变时，`NotifyAll()`方法将被触发。`NotifyAll()`方法通过接收器列表工作，并调用`Update()`方法。然后，事件接收器可以执行特定于其上下文的任务。我们没有实施退订等方法来关注核心问题:\n\n```cpp\n//--------------------- An event sink class for the preceding EventSources \ntemplate <class T> \nclass EventSourceValueObserver{ \n    T OldState; \n  public: \n    EventSourceValueObserver() { OldState = 0; } \n    virtual ~EventSorceValueObserver() {} \n    virtual void Update( T State ) { \n       cout << \"Old State \" << OldState << endl; \n       OldState = State; \n       cout << \"Current State \" << State << endl;  \n    } \n}; \n```\n\n`EventSourceValueObserver`类已经实现了`Update`方法来完成与其上下文相关的任务。在这里，它只是将旧状态和当前状态的值打印到控制台上。在现实生活中，接收器可能会修改 UX 元素，或者通过通知将状态传播给其他对象。我们再写一个事件接收器，它将继承自`EventSourceValueObserver`:\n\n```cpp\n//------------ A simple specialized Observe \nclass AnotherObserver : public EventSourceValueObserver<double> { \n  public: \n    AnotherObserver():EventSourceValueObserver() {} \n    virtual ~AnotherObserver() {} \n    virtual void Update( double State )  \n    { cout << \" Specialized Observer\" << State <<  endl; } \n};\n```\n\n为了演示的目的，我们实现了观察者的一个专门版本。这样做是为了表明我们可以拥有两个类实例的订阅者(可以从`EventSourceObserver<T>`继承)。此外，当我们收到`EventSource`的通知时，我们不会做太多事情:\n\n```cpp\nint main() { \n   unique_ptr<EventSourceValueSubject<double>> \n                 evsrc(new EventSourceValueSubject<double>()); \n    //---- Create Two instance of Observer and Subscribe \n   unique_ptr<AnotherObserver> evobs( new AnotherObserver());\n   unique_ptr<EventSourceValueObserver<double>> \n               evobs2( new EventSourceValueObserver<double>());\n   evsrc->Subscribe( evobs.get() );\n   evsrc->Subscribe( evobs2.get());\n   //------ Change the State of the EventSource \n   //------ This should trigger call to Update of the Sink \n   evsrc->SetState(100); \n} \n```\n\n前面的代码片段实例化了一个`EventSource`对象，并添加了两个订阅者。当我们更改`EventSource`的状态时，订阅者会收到通知。这是观察者模式的关键。在一个普通的面向对象程序中，对象的消耗是通过以下方式完成的:\n\n1.  实例化对象\n2.  调用一个方法来计算一些值或改变状态\n3.  根据返回值或状态变化做一些有用的事情\n\n在这里，就观察者而言，我们做了以下工作:\n\n1.  实例化对象(`EventSource`)\n2.  通过实现观察者订阅通知(用于事件监听)\n3.  当`EventSource`发生变化时，会通知你\n4.  用通过通知收到的值做一些事情\n\n这里概述的`Method`功能有助于分离关注点，并且已经实现了模块化。这是实现事件驱动代码的好机制。您要求得到通知，而不是轮询事件。如今大多数图形用户界面工具包都使用类似的范例。\n\n# GoF 观察者模式的局限性\n\nGoF 模式书是在世界真正进行顺序编程的时候写的。从当前的编程模型世界观来看，观察者模式实现的架构有很多异常。以下是其中的一些:\n\n*   主体和观察者之间的紧密耦合。\n*   `EventSource`的寿命由观察者控制。\n*   观察者(水槽)可以挡住`EventSource`。\n*   该实现不是线程安全的。\n*   事件过滤在接收器级别完成。理想情况下，应该在数据所在的位置过滤数据(在主题级别，通知之前)。\n*   大多数时候，观察者不会做太多事情，CPU 周期会被浪费。\n*   `EventSource`应该理想地向环境发布该值。环境应该通知所有订户。这种间接级别可以促进诸如事件聚合、事件转换、事件过滤和规范化事件数据等技术。\n\n随着不可变变量、函数组合、函数风格转换、无锁并发编程等函数式编程技术的出现，我们可以规避经典 Observer 模式的限制。该行业概述的解决方案是可观察的概念。\n\n在经典的观察者模式中，勤奋的读者可能已经看到了异步编程模型被合并的潜力。`EventSource`可以异步调用订阅者方法，而不是顺序循环订阅者。通过使用火灾和遗忘机制，我们可以将`EventSource`与其水槽分离。调用可以从后台线程、异步任务、打包任务或合适的上下文机制中完成。通知方法的异步调用还有一个额外的优点，即如果任何客户端阻塞(通过进入无限循环或崩溃)，其他客户端仍然可以获得通知。异步方法适用于以下模式:\n\n1.  定义处理数据、异常和数据结尾的方法(在事件接收器端)\n2.  观察者(事件接收器)接口应该有`OnData`、`OnError`和`OnCompleted`方法\n3.  每个事件接收器都应该实现观察者界面\n4.  每个`EventSource`(可观察)都应该有订阅和取消订阅的方法\n\n5.  事件接收器应该通过订阅方法订阅可观察的实例\n6.  当一个事件发生时，观察者会被可观察到的事物通知\n\n其中一些已经在[第 1 章](01.html#J2B80-51c8384cc2cb48e691b461190723b468)、*反应式编程模型-概述和历史*中提到。我们当时没有讨论异步部分。在本章中，我们将重温这些想法。根据作者基于技术演示和与开发人员的交互所获得的经验，直接进入编程的可观察/观察者模型无助于理解。大多数开发人员对可观察/观察者感到困惑，因为他们不知道这个模式解决了什么特殊问题。这里给出的经典 GoF 观察者实现是为可观察流的讨论设置上下文。\n\n# 从整体上看 GoF 模式\n\n设计模式运动开始的时候，世界正在努力适应面向对象软件设计方法的复杂性。GoF 书籍和相关的模式目录为开发人员提供了一套设计大规模系统的技术。诸如并发性和并行性之类的主题并不在设计目录的人的脑海中。(至少，他们的工作没有体现这一点！)\n\n我们已经看到，通过经典的观察者模式进行事件处理有一些局限性，这在某些情况下可能是一个问题。出路是什么？我们需要退一步重新看待事件处理的问题。对于反应式编程模型(用可观察流编程！)正在努力解决。我们的旅程将帮助我们很好地从 GOF 模式过渡到使用函数式编程结构的反应式编程世界。\n\n这一部分的以下内容有点抽象，在这里提供了一个概念背景，这本书的作者从这个背景开始探讨本章所涵盖的主题。我们解释可观察对象的方法从 GoF 复合/访问者模式开始，并迭代到可观察对象的主题。这种方法的想法来自一本关于 Advaita Vedanta 的书，这是一种起源于印度的神秘哲学传统。这个话题已经用西方哲学术语解释过了。如果一件事看起来有点抽象，请随意掩饰。\n\n纳塔拉贾·古鲁(1895-1973)是一位印度哲学家，他是阿维达·韦丹塔哲学的支持者，这是一种基于支配我们所有人的至高力量的非二元论的印度哲学流派。根据这一哲学流派，无论我们在周围看到什么，无论是人类、动物还是植物，都是绝对(梵语中称为婆罗门)的表现形式，它唯一的肯定是 SAT-CHIT-ANAND(吠檀多哲学用矛盾的否定和证明来描绘婆罗门)。这可以翻译成英语，作为存在、本质和极乐(极乐的隐含意义在这里是“好”)。在新德里 DK Print World 出版的名为*统一哲学*的书中，他给出了 SAT-CHIT-ANAND 到本体论、认识论和价值论(哲学的三个主要分支)的映射。本体论、认识论和价值论分别是存在论、认识论和价值论。下表给出了 SAT-CHIT-ANAND 到其他实体的可能映射，它们的含义大致相同:\n\n| **SAT** | **CHIT** | 菠萝 |\n| 存在 | 本质 | 布利斯 |\n| 本体论 | 认识论 | 价值论 |\n| 我是谁？ | 我能知道什么？ | 我该怎么办？ |\n| 结构 | 行为 | 功能 |\n\n在吠檀多哲学中，整个世界被视为存在、本质和极乐。从表中，我们将把软件设计世界中的问题映射到结构、行为和功能的问题上。世界上的每个系统都可以从结构、行为和功能的角度来看待。面向对象程序的规范结构是层次结构。我们将把我们感兴趣的世界建模为层次结构，并以规范的方式处理它们。GOF 模式目录有用于建模层次结构的复合模式(结构模式)和处理它们的访问者模式(行为模式)。\n\n# 面向对象编程模型和层次结构\n\nThis section is bit conceptual in nature and those of you who have not dabbled with GoF design patterns will find it a bit difficult. The best strategy could be to skip this section and focus on the running example. Once you have understood the running example, this particular section can be revisited.\n\n面向对象编程非常擅长建模层次结构。事实上，层次结构可以被认为是面向对象数据处理的规范数据模型。在 GoF 模式世界中，我们使用复合模式来建模层次结构。复合模式被归类为结构模式。每当使用复合模式时，访问者模式也将是系统的一部分。Visitor 模式有利于处理复合，从而为结构增加行为。访问者/组合模式在现实生活中是一对。当然，复合的一个实例可以由不同的访问者处理。在编译器项目中，**抽象语法树** ( **AST** )将被建模为一个组合，并且将有用于类型检查、代码优化、代码生成和静态分析等的 Visitor 实现。\n\nVisitor 模式的一个问题是，它必须对复合的结构有所了解才能进行处理。此外，在需要处理复合层次结构中可用数据的过滤子集的上下文中，这将导致代码膨胀。对于每个过滤标准，我们可能需要不同的访问者。GoF 模式目录有另一种属于行为范畴的模式，叫做迭代器，这是每个 C++ 程序员都熟悉的。迭代器模式擅长以结构不可知的方式处理数据。任何类型的层次结构都必须被线性化或展平，以形成一个适合迭代器处理的形状。一个例子可以是树，它可以用 BFS 迭代器或 DFS 迭代器来处理。对于应用程序员来说，突然之间，树变成了线性结构。我们需要展平层次结构，使其处于一种结构服从迭代器的状态。该过程将由实现该应用编程接口的人来实现。迭代器模式有一些限制(它是基于拉的)，我们将使用一种称为 Observerable/Observer 的模式来反转凝视，并使系统基于推，Observer 模式的增强版本。这一部分有点抽象，但是在看完整个章节后，你可以回来理解正在发生的事情。简而言之，我们可以把整件事总结如下:\n\n*   我们可以使用复合模式来建模层次结构\n*   我们可以使用访问者模式来处理复合\n*   我们可以展平或线性化复合，通过迭代器进行导航\n*   迭代器遵循拉方法，对于基于推的方案，我们需要反向凝视\n*   现在，我们已经设法达到了实现事物的可观察/观察者方式\n*   可观测值和迭代器是二元对立的(一个人的推动就是另一个人的拉动！)\n\n我们将实施上述所有要点，为可观察对象打下坚实的基础。\n\n# 用于表达式处理的复合/访问者模式\n\n为了演示从 GoF 模式目录到 Observables 的旅程，我们将模拟一个四功能计算器作为运行示例。因为表达式树或 AST 本质上是分层的，所以它们将是一个很好的例子来建模为复合模式。我们有意省略了编写解析器，以保持代码清单的小:\n\n```cpp\n#include <iostream> \n#include <memory> \n#include <list> \n#include <stack> \n#include <functional> \n#include <thread> \n#include <future> \n#include <random> \n#include \"FuncCompose.h\" // available int the code base \nusing namespace std; \n//---------------------List of operators supported by the evaluator \nenum class OPERATOR{ ILLEGAL,PLUS,MINUS,MUL,DIV,UNARY_PLUS,UNARY_MINUS };  \n```\n\n我们定义了一个枚举类型来表示四个二元运算符(`+`、`-`、`*`、`/`)和两个一元运算符(`+`、`-`)。除了标准的 C++ 头之外，我们还包括了一个自定义头(`FuncCompose.h`)，它可以在与本书相关的 GitHub repo 上找到。它包含用于编写函数的代码和用于函数编写的管道操作符(`|`)。我们可以使用 Unix 管道样式组合将一组转换联系在一起:\n\n```cpp\n//------------ forward declarations for the Composites  \nclass Number;  //----- Stores IEEE double precision floating point number  \nclass BinaryExpr; //--- Node for Binary Expression \nclass UnaryExpr;  //--- Node for Unary Expression \nclass IExprVisitor; //---- Interface for the Visitor  \n//---- Every node in the expression tree will inherit from the Expr class \nclass Expr { \n  public: \n   //---- The standard Visitor double dispatch method \n   //---- Normally return value of accept method are void.... and Concrete\n   //---- classes store the result which can be retrieved later\n   virtual double accept(IExprVisitor& expr_vis) = 0; \n   virtual ~Expr() {} \n}; \n//----- The Visitor interface contains methods for each of the concrete node  \n//----- Normal practice is to use \nstruct IExprVisitor{ \n   virtual  double Visit(Number& num) = 0; \n   virtual  double Visit(BinaryExpr& bin) = 0; \n   virtual  double Visit(UnaryExpr& un)=0 ; \n}; \n```\n\n表达式类将作为表达式树中所有节点的基类。因为我们的目的是演示复合/访问者 GoF 模式，所以我们只支持常量、二进制表达式和一元表达式。Expr 类中的 accept 方法接受一个 Visitor 引用作为参数，并且该方法的主体对于所有节点都是相同的。该方法会将调用重定向到 Visitor 实现上的适当处理程序。要更深入地了解本节所涵盖的整个主题，请阅读关于*双派单*和*访客模式*的内容，方法是使用您最喜欢的搜索引擎搜索网页。\n\n访问者界面(`IExprVisitor`)包含处理层次结构支持的所有节点类型的方法。在我们的例子中，有处理常数、二进制运算符和一元运算符的方法。让我们看看节点类型的代码。我们从数字课开始:\n\n```cpp\n//---------A class to represent IEEE 754 interface \nclass Number : public Expr { \n   double NUM; \n  public: \n   double getNUM() { return NUM;}    \n   void setNUM(double num)   { NUM = num; } \n   Number(double n) { this->NUM = n; } \n   ~Number() {} \n   double accept(IExprVisitor& expr_vis){ return expr_vis.Visit(*this);} \n}; \n```\n\nNumber 类包装了一个 IEEE 双精度浮点数。代码显而易见，我们只需要关心`accept`方法的内容。该方法接收类型为访问者(`IExprVisitor&`)的参数。该例程只是将调用反射回 Visitor 实现上的适当节点。在这种情况下，它会在`IExpressionVisitor`上调用`Visit(Number&)`:\n\n```cpp\n//-------------- Modeling Binary Expresison  \nclass BinaryExpr : public Expr { \n   Expr* left; Expr* right; OPERATOR OP; \n  public: \n   BinaryExpr(Expr* l,Expr* r , OPERATOR op ) { left = l; right = r; OP = op;} \n   OPERATOR getOP() { return OP; } \n   Expr& getLeft() { return *left; } \n   Expr& getRight() { return *right; } \n   ~BinaryExpr() { delete left; delete right;left =0; right=0; } \n   double accept(IExprVisitor& expr_vis) { return expr_vis.Visit(*this);} \n};  \n```\n\n`BinaryExpr`类用左右操作数模拟二进制运算。操作数可以是层次结构中的任何类。候选班级有`Number`、`BinaryExpr`、`UnaryExpr`。这可以达到任意深度。在我们的例子中，终端节点是数字。前面的代码支持四个二进制运算符:\n\n```cpp\n//-----------------Modeling Unary Expression \nclass UnaryExpr : public Expr { \n   Expr * right; OPERATOR op; \n  public: \n   UnaryExpr( Expr *operand , OPERATOR op ) { right = operand;this-> op = op;} \n   Expr& getRight( ) { return *right; } \n   OPERATOR getOP() { return op; } \n   virtual ~UnaryExpr() { delete right; right = 0; } \n   double accept(IExprVisitor& expr_vis){ return expr_vis.Visit(*this);} \n};  \n```\n\n`UnaryExpr`方法用一个运算符和一个右侧表达式对一元表达式进行建模。对于这个实现，我们支持一元正和一元负。右侧的表情可以依次是`UnaryExpr`、`BinaryExpr`或`Number`。现在我们已经有了所有支持的节点类型的实现，让我们把重点放在访问者接口的实现上。我们将编写一个树行者和评估器来计算表达式的值:\n\n```cpp\n//--------An Evaluator for Expression Composite using Visitor Pattern  \nclass TreeEvaluatorVisitor : public IExprVisitor{ \n  public: \n   double Visit(Number& num){ return num.getNUM();} \n   double Visit(BinaryExpr& bin) { \n     OPERATOR temp = bin.getOP(); double lval = bin.getLeft().accept(*this); \n     double rval = bin.getRight().accept(*this); \n     return (temp == OPERATOR::PLUS) ? lval + rval: (temp == OPERATOR::MUL) ?  \n         lval*rval : (temp == OPERATOR::DIV)? lval/rval : lval-rval;   \n   } \n   double Visit(UnaryExpr& un) { \n     OPERATOR temp = un.getOP(); double rval = un.getRight().accept(*this); \n     return (temp == OPERATOR::UNARY_PLUS)  ? +rval : -rval; \n   } \n};\n```\n\n这将对 AST 进行深度优先遍历，并递归评估节点。让我们编写一个表达式处理器(一个`IExprVisitor`的实现)，它将以**反向波兰符号** ( **RPN** )的形式将表达式树打印到控制台:\n\n```cpp\n//------------A Visitor to Print Expression in RPN\nclass ReversePolishEvaluator : public IExprVisitor {\n    public:\n    double Visit(Number& num){cout << num.getNUM() << \" \" << endl; return 42;}\n    double Visit(BinaryExpr& bin){\n        bin.getLeft().accept(*this); bin.getRight().accept(*this);\n        OPERATOR temp = bin.getOP();\n        cout << ( (temp==OPERATOR::PLUS) ? \" + \" :(temp==OPERATOR::MUL) ?\n        \" * \" : (temp == OPERATOR::DIV) ? \" / \": \" - \" ) ; return 42;\n    }\n    double Visit(UnaryExpr& un){\n        OPERATOR temp = un.getOP();un.getRight().accept(*this);\n        cout << (temp == OPERATOR::UNARY_PLUS) ?\" (+) \" : \" (-) \"; return 42;\n    }\n};\n```\n\nRPN 符号也称为后缀概念，其中运算符位于操作数之后。它们适合使用评估堆栈进行处理。它们构成了基于堆栈的虚拟机体系结构的基础，Java 虚拟机和。NET CLR。现在，让我们编写一个主函数来将所有内容组合在一起:\n\n```cpp\nint main( int argc, char **argv ){ \n     unique_ptr<Expr>   \n            a(new BinaryExpr( new Number(10) , new Number(20) , OPERATOR::PLUS)); \n     unique_ptr<IExprVisitor> eval( new TreeEvaluatorVisitor()); \n     double result = a->accept(*eval); \n     cout << \"Output is => \" << result << endl; \n     unique_ptr<IExprVisitor>  exp(new ReversePolishEvaluator()); \n     a->accept(*exp); \n}\n```\n\n这段代码片段创建了一个复合的实例(一个`BinaryExpr`的实例)，并且还实例化了一个`TreeEvaluatorVisitor`和`ReversePolshEvaluator`的实例。然后调用 Expr 的`accept`方法开始处理。我们将在控制台上看到该表达式的值和该表达式的一个 RPN 等价物。在这一节中，我们学习了如何创建一个组合并使用一个访问者界面来处理该组合。复合/访问者的其他潜在例子是存储目录内容及其遍历、XML 处理、文档处理等等。流行的观点认为，如果你知道复合/访问者二人组，你已经很好地理解了 GoF 模式目录。\n\n我们已经看到复合模式和访问者模式作为一对来处理系统的结构和行为方面，并提供一些功能。访客必须以一种预先假定对复合材料结构的认知的方式书写。从抽象的角度来看，这可能是一个潜在的问题。层次结构的实现者可以提供一种机制，将层次结构展平成一个列表(这在大多数情况下是可能的)。这将使应用编程接口实现者能够提供一个基于迭代器的应用编程接口。基于迭代器的应用编程接口也很适合函数式处理。让我们看看它是如何工作的。\n\n# 展平复合材料进行迭代处理\n\n我们已经了解到，访问者模式必须知道组合的结构，才能有人编写访问者界面的实例。这可能会产生一个异常，称为*抽象泄漏*。GoF 模式目录有一个模式，可以帮助我们以结构不可知的方式导航树的内容。是的，你可能猜对了:迭代器模式是候选模式！为了让迭代器完成它的工作，复合必须被展平成一个列表序列或流。让我们编写一些代码来展平上一节中建模的表达式树。在编写展平复合的逻辑之前，让我们创建一个数据结构，将 AST 的内容存储为一个列表。列表中的每个节点都必须存储一个运算符或值，这取决于我们是否需要存储运算符或操作数。为此，我们描述了一个名为`EXPR_ITEM`的数据结构:\n\n```cpp\n//////////////////////////// \n// A enum to store discriminator -> Operator or a Value? \nenum class ExprKind{  ILLEGAL_EXP,  OPERATOR , VALUE }; \n// A Data structure to store the Expression node. \n// A node will either be a Operator or Value \nstruct EXPR_ITEM { \n    ExprKind knd; double Value; OPERATOR op; \n    EXPR_ITEM():op(OPERATOR::ILLEGAL),Value(0),knd(ExprKind::ILLEGAL_EXP){} \n    bool SetOperator( OPERATOR op ) \n    {  this->op = op;this->knd = ExprKind::OPERATOR; return true; } \n    bool SetValue(double value)  \n    {  this->knd = ExprKind::VALUE;this->Value = value;return true;} \n    string toString() {DumpContents();return \"\";} \n   private: \n      void DumpContents() { //---- Code omitted for brevity } \n}; \n```\n\n`list<EXPR_ITEM>`数据结构将以线性结构存储复合的内容。让我们编写一个类来展平复合:\n\n```cpp\n//---- A Flattener for Expressions \nclass FlattenVisitor : public IExprVisitor { \n        list<EXPR_ITEM>  ils; \n        EXPR_ITEM MakeListItem(double num) \n        { EXPR_ITEM temp; temp.SetValue(num); return temp; } \n        EXPR_ITEM MakeListItem(OPERATOR op) \n        { EXPR_ITEM temp;temp.SetOperator(op); return temp;} \n        public: \n        list<EXPR_ITEM> FlattenedExpr(){ return ils;} \n        FlattenVisitor(){} \n        double Visit(Number& num){ \n           ils.push_back(MakeListItem(num.getNUM()));return 42; \n        } \n        double Visit(BinaryExpr& bin) { \n            bin.getLeft().accept(*this);bin.getRight().accept(*this); \n            ils.push_back(MakeListItem(bin.getOP()));return 42; \n        } \n         double Visit(UnaryExpr& un){ \n            un.getRight().accept(*this); \n            ils.push_back(MakeListItem(un.getOP())); return 42; \n        } \n};  \n```\n\n`FlattenerVistor`类将复合`Expr`节点展平为一个`EXPR_ITEM`列表。一旦组合被线性化，就可以使用迭代器模式来处理项目。让我们编写一个小的全局函数，将`Expr`树转换为`list<EXPR_ITEM>`:\n\n```cpp\nlist<EXPR_ITEM> ExprList(Expr* r) { \n   unique_ptr<FlattenVisitor> fl(new FlattenVisitor()); \n    r->accept(*fl); \n    list<EXPR_ITEM> ret = fl->FlattenedExpr();return ret; \n }\n```\n\n全局子程序`ExprList`将展平一列`EXPR_ITEM`的任意表达式树。一旦我们展平了复合，我们就可以使用迭代器来处理内容。将结构线性化为列表后，我们可以使用堆栈数据结构来评估表达式数据，以生成输出:\n\n```cpp\n//-------- A minimal stack to evaluate RPN expression \nclass DoubleStack : public stack<double> { \n   public: \n    DoubleStack() { } \n    void Push( double a ) { this->push(a);} \n    double Pop() { double a = this->top(); this->pop(); return a; } \n};  \n```\n\n`DoubleStack`是 STL 堆栈容器的包装器。这可以被认为是某种帮助例程，以保持列表的简洁。让我们为扁平表达式编写一个赋值器。如果遇到值，我们将遍历列表`<EXPR_ITEM>`并将值推送到堆栈。如果遇到运算符，我们将从堆栈中弹出值并应用该操作。结果再次被推入堆栈。迭代结束时，堆栈中的现有元素将是与表达式关联的值:\n\n```cpp\n//------Iterator through eachn element of Expression list \ndouble Evaluate( list<EXPR_ITEM> ls) { \n   DoubleStack stk; double n; \n   for( EXPR_ITEM s : ls ) { \n     if (s.knd == ExprKind::VALUE) { stk.Push(s.Value); } \n     else if ( s.op == OPERATOR::PLUS) { stk.Push(stk.Pop() + stk.Pop());} \n     else if (s.op == OPERATOR::MINUS ) { stk.Push(stk.Pop() - stk.Pop());} \n     else if ( s.op ==  OPERATOR::DIV) { n = stk.Pop(); stk.Push(stk.Pop() / n);} \n     else if (s.op == OPERATOR::MUL) { stk.Push(stk.Pop() * stk.Pop()); } \n     else if ( s.op == OPERATOR::UNARY_MINUS) { stk.Push(-stk.Pop()); } \n    } \n   return stk.Pop(); \n} \n//-----  Global Function Evaluate an Expression Tree \ndouble Evaluate( Expr* r ) { return Evaluate(ExprList(r)); } \n```\n\n让我们编写一个主程序，它将调用这个函数来计算表达式。评估器中的代码列表很容易理解，因为我们正在减少一个列表。在基于树的解释器中，事情并不明显:\n\n```cpp\nint main( int argc, char **argv ){      \n     unique_ptr<Expr>\n```\n\n```cpp\n         a(new BinaryExpr( new Number(10) , new Number(20) , OPERATOR::PLUS)); \n     double result = Evaluate( &(*a)); \n     cout << result << endl; \n} \n```\n\n# 列表上的映射和过滤操作\n\nMap 是一个函数运算符，函数将应用于列表。Filter 将对一个列表应用谓词，并返回另一个列表。它们是任何功能处理管道的基石。它们也被称为高阶函数。我们可以使用`std::list`的`std::transform`和`std::vector`编写一个通用的地图函数:\n\n```cpp\ntemplate <typename R, typename F> \nR Map(R r , F&& fn) { \n      std::transform(std::begin(r), std::end(r), std::begin(r), \n         std::forward<F>(fn)); \n      return r; \n} \n```\n\n让我们也写一个函数来过滤一个`std::list`(我们假设只传递一个列表)。同样可以在`std::vector`上工作。我们可以使用管道操作符组成一个更高阶的函数。复合函数也可以作为谓词传递:\n\n```cpp\ntemplate <typename R, typename F> \nR Filter( R r , F&& fn ) { \n   R ret(r.size()); \n   auto first = std::begin(r), last = std::end(r) , result = std::begin(ret);  \n   bool inserted = false; \n   while (first!=last) { \n    if (fn(*first)) { *result = *first; inserted = true; ++ result; }  \n    ++ first; \n   } \n   if ( !inserted ) { ret.clear(); ret.resize(0); } \n   return ret; \n}\n```\n\n在 Filter 的这个实现中，由于`std::copy_if`的限制，我们被迫滚动自己的迭代逻辑。一般建议使用函数的 STL 实现来编写包装器。对于这个特定的场景，我们需要检测一个列表是否为空:\n\n```cpp\n//------------------ Global Function to Iterate through the list  \nvoid Iterate( list<EXPR_ITEM>& s ){ \n    for (auto n : s ) { std::cout << n.toString()  << 'n';} \n} \n```\n\n让我们写一个主函数来把所有的东西放在一起。该代码将演示如何在应用代码中使用`Map`和`Filter`。功能组合逻辑和管道操作器可在`FuncCompose.h`获得:\n\n```cpp\nint main( int argc, char **argv ){ \n     unique_ptr<Expr>   \n        a(new BinaryExpr( new Number(10.0) , new Number(20.0) , OPERATOR::PLUS)); \n      //------ExprList(Expr *) will flatten the list and Filter will by applied \n      auto cd = Filter( ExprList(&(*a)) , \n            [](auto as) {  return as.knd !=   ExprKind::OPERATOR;} ); \n      //-----  Square the Value and Multiply by 3... used | as composition Operator \n      //---------- See FuncCompose.h for details \n      auto cdr = Map( cd, [] (auto s ) {  s.Value *=3; return s; } |  \n                  [] (auto s ) { s.Value *= s.Value; return s; } ); \n      Iterate(cdr);  \n} \n```\n\n`Filter`例程创建一个新的`list<Expr>`，它只包含表达式中使用的值或操作数。`Map`例程对值列表应用一个复合函数来返回一个新列表。\n\n# 逆转可观察的凝视！\n\n我们已经了解到，我们可以将一个组合转换成一个列表，并通过迭代器遍历它们。迭代器模式从数据源提取数据，并在消费者级别操作结果。我们面临的最重要的问题是我们正在耦合我们的`EventSource`和事件接收器。GoF 观察者模式在这里也没有帮助。\n\n让我们编写一个可以充当事件中枢的类，接收器将订阅该类。通过拥有一个事件中枢，我们现在将拥有一个对象，它将充当`EventSource`和事件接收器之间的中介。这种间接性的一个优势显而易见，因为我们的类可以在事件到达消费者之前进行聚合、转换和过滤。消费者甚至可以在事件中心级别设置转换和过滤标准:\n\n```cpp\n//----------------- OBSERVER interface \nstruct  OBSERVER { \n    int id; \n    std::function<void(const double)> ondata; \n    std::function<void()> oncompleted; \n    std::function<void(const std::exception &)> onexception; \n}; \n//--------------- Interface to be implemented by EventSource \nstruct OBSERVABLE { \n   virtual bool Subscribe( OBSERVER * obs ) = 0; \n    // did not implement unsuscribe  \n}; \n```\n\n我们已经在[第 1 章](https://cdp.packtpub.com/c___reactive_programming/wp-admin/post.php?post=53&action=edit#post_26)、*反应式编程模型–概述和历史*和[第 2 章](02.html#12AK80-51c8384cc2cb48e691b461190723b468)、*现代 C++ 及其关键习惯用法之旅*中介绍了`OBSERVABLE`和`OBSERVER`。`EventSource`实现`OBSERVABLE`，事件接收器实现`OBSERVER`接口。从`OBSERVER`派生的类将实现以下方法:\n\n*   `ondata`(用于接收数据)\n*   `onexception`(异常处理)\n*   `oncompleted`(数据结束)\n\n`EventSource`类将从`OBSERVABLE`派生，并且必须实现:\n\n*   订阅(订阅通知)\n*   取消订阅(在我们的案例中未实现)\n\n```cpp\n//------------------A toy implementation of EventSource \ntemplate<class T,class F,class M, class Marg, class Farg > \nclass EventSourceValueSubject : public OBSERVABLE { \n   vector<OBSERVER> sinks;  \n   T *State;  \n   std::function<bool(Farg)> filter_func; \n   std::function<Marg(Marg)> map_func;\n```\n\n`map_func`和`filter_func`是可以帮助我们在以异步方式将值分派给订阅者之前对其进行转换和过滤的函数。当我们实例化`EventSource`类时，我们给出这些值作为参数。目前，我们编写代码时假设只有`Expr`对象将存储在`EventSource`中。我们可以有一个表达式的列表或向量，并将值流式传输给订阅者。为此，可以将标量值推送给侦听器:\n\n```cpp\n  public: \n   EventSourceValueSubject(Expr *n,F&& filter, M&& mapper) { \n       State = n; map_func = mapper; filter_func = filter; NotifyAll();  \n   } \n   ~EventSourceValueSubject() {  sinks.clear(); } \n   //------ used Raw Pointer ...In real life, a shared_ptr<T>\n   //------ is more apt here\n   virtual  bool Subscribe( OBSERVER  *sink ) { sinks.push_back(*sink); return true;} \n```\n\n我们做了一些假设`Expr`对象将由调用者拥有。我们还省略了取消订阅方法的实现。构造函数接受一个`Expr`对象、`Filter`谓词(可以是使用|运算符的复合函数)和一个`Mapping`函数(可以是使用`|`运算符的复合函数):\n\n```cpp\n   void NotifyAll() { \n      double ret = Evaluate(State); \n      list<double> ls; ls.push_back(ret); \n      auto result = Map( ls, map_func);; // Apply Mapping Logic \n      auto resulttr = Filter( result,filter_func); //Apply Filter \n      if (resulttr.size() == 0 ) { return; } \n```\n\n评估表达式后，标量值将被放入 STL 列表。然后，将在列表中应用映射函数来转换该值。将来，我们将处理一个值列表。一旦我们映射或转换了值，我们将对列表应用过滤器。如果列表中没有值，则该方法返回而不通知订阅者:\n\n```cpp\n      double dispatch_number = resulttr.front(); \n      for (auto sink : sinks) {  \n           std::packaged_task<int()> task([&]()  \n           { sink.ondata(dispatch_number); return 1;  }); \n           std::future<int> result = task.get_future();task(); \n           double dresult = result.get(); \n         } \n     }\n```\n\n在这段代码中，我们将调用`packaged_task`将数据分派给事件接收器。工业级的库使用一段名为 Scheduler 的代码来完成这部分任务。既然是用火忘了，水槽就挡不住`EventSource`。这是 Observables 最重要的用例之一:\n\n```cpp\n      T* GetState() { return State; } \n      void SetState(T *pstate) { State = pstate; NotifyAll(); } \n}; \n```\n\n现在，让我们编写一个基于现代 C++ 随机数生成器的方法来发出具有均匀概率分布的随机表达式。这种分布的选择相当随意。我们也可以尝试其他发行版，以获得不同的结果:\n\n```cpp\nExpr *getRandomExpr(int start, int end) { \n    std::random_device rd; \n    std::default_random_engine reng(rd()); \n    std::uniform_int_distribution<int> uniform_dist(start, end); \n    double mean = uniform_dist(reng); \n    return  new  \n          BinaryExpr( new Number(mean*1.0) , new Number(mean*2.0) , OPERATOR::PLUS); \n} \n```\n\n现在，让我们编写一个主函数来将所有内容组合在一起。我们将用一个`Expr`、一个`Filter`和一个`Mapper`来实例化`EventSourceValueSubject`类:\n\n```cpp\nint main( int argc, char **argv ){ \n     unique_ptr<Expr>   \n         a(new BinaryExpr( new Number(10) , new Number(20) , OPERATOR::PLUS)); \n     EventSourceValueSubject<Expr,std::function<bool(double)>, \n                    std::function<double(double)>,double,double>  \n                    temp(&(*a),[] (auto s ) {   return s > 40.0;  }, \n                    []  (auto s ) { return s+ s ; }  | \n                    []  (auto s ) { return s*2;} ); \n```\n\n在实例化对象时，我们使用了管道操作符来合成两个 Lambdas。这是为了演示我们可以组成一个任意的函数列表来形成一个复合函数。当我们编写 RxCpp 程序时，我们会大量利用这种技术:\n\n```cpp\n     OBSERVER obs_one ;     OBSERVER obs_two ; \n     obs_one.ondata = [](const double  r) {  cout << \"*Final Value \" <<  r << endl;}; \n     obs_two.ondata = [] ( const double r ){ cout << \"**Final Value \" << r << endl;}; \n```\n\n在这段代码中，我们已经实例化了两个`OBSERVER`对象，并使用 Lambda 函数将它们分配给 ondata 成员。我们没有实现其他方法。这仅用于演示目的:\n\n```cpp\n     temp.Subscribe(&obs_one); temp.Subscribe(&obs_two);   \n```\n\n我们使用`OBSERVER`实例订阅了事件通知。我们只实现了 ondata 方法。实现`onexception`和`oncompleted`是琐碎的任务:\n\n```cpp\n     Expr *expr = 0; \n     for( int i= 0; i < 10; ++ i ) { \n           cout << \"--------------------------\" <<  i << \" \"<< endl; \n           expr = getRandomExpr(i*2, i*3 ); temp.SetState(expr); \n           std::this_thread::sleep_for(2s); delete expr; \n     } \n} \n```\n\n我们通过将表达式设置为`EventSource`对象来评估一系列随机表达式。经过变换和过滤后，如果还有剩余值，该值会通知到`OBSERVER`并打印到控制台。有了这个，我们已经设法用`packaged_taks`写了一个无阻塞的`EventSource`。我们在本章中演示了以下内容:\n\n*   使用组合为表达式树建模\n*   通过访问者界面处理组合\n*   将表达式树展平成一个列表，并通过迭代器进行处理(拉)\n*   将视线从`EventSource`反向至事件接收器(推动)\n\n# 摘要\n\n在这一章中，我们已经讲了很多内容，慢慢走向反应式编程模式。我们了解了 GoF 观察者模式，并了解了它的缺点。然后，我们进入哲学，从结构、行为和功能的角度来理解看待世界的方法。我们在建模表达式树的上下文中学习了 GoF 复合/访问者模式。我们学习了如何将层次结构展平成一个列表，并通过迭代器导航它们。最后，我们稍微改变了一下事物的模式，以达到可观察对象。通常，Observables 与 Streams 一起工作，但是在我们的例子中，它是一个标量值。在下一章中，我们将学习事件流处理，以完成学习反应式编程的先决条件。"
  },
  {
    "path": "docs/cpp-react-prog/06.md",
    "content": "# 六、C++ 事件流编程简介\n\n这一章将是使用 C++ 编程反应系统所需的一系列先决条件章节中的最后一章。我们之所以需要经历相当多的概念，是因为反应式编程模型在实现其健壮的编程模型时统一了许多计算概念。要开始被动思考，程序员必须熟悉面向对象编程、函数式编程、语言级并发、无锁编程、异步编程模型、设计模式、调度算法、数据流编程模型、声明式编程，甚至一点图论！我们在开始这本书时，先看了各种图形用户界面系统的事件驱动编程模型，以及围绕它们构建代码的方法。我们讲述了现代 C++ 的核心精髓[第 2 章](02.html#12AK80-51c8384cc2cb48e691b461190723b468)，*现代 C++ 及其关键习惯用法之旅*。在[第 3 章](03.html#1O8H60-51c8384cc2cb48e691b461190723b468)、*C++*中的语言级并发和并行，以及[第 4 章](04.html#27GQ60-51c8384cc2cb48e691b461190723b468)、*c++ 中的异步和无锁编程*中，我们分别介绍了 c++ 语言和无锁编程支持的语言级并发。在[第 5 章](05.html#2RHM00-51c8384cc2cb48e691b461190723b468)、*可观察对象介绍*中，我们重点讨论了如何在 GOF 模式的背景下，通过处理反应式编程模型来透视它。剩下的就是事件流编程。现在我们将关注事件流的处理或事件流编程。在本章中，我们将了解以下内容:\n\n*   什么是 Stream 编程模型？\n*   流编程模型的优势\n*   使用带有公共域库的 C++ 进行流编程\n*   使用 Streamulus 的流编程\n*   事件流编程\n\n# 什么是 Stream 编程模型？\n\n在我们进入 Stream 编程模型的主题之前，我们将后退一步，看看与 POSIX shell 编程模型的相似之处。在典型的命令行 shell 程序中，每个命令都是一个程序，每个程序都是一个命令。在实现计算目标或任务后，我们可以将一个程序的输出通过管道传输到另一个程序。实际上，我们可以链接一系列命令来完成更大的计算任务。我们可以将它视为一个数据流，通过一系列过滤器或转换来获取输出。我们也可以把前面的过程称为*命令合成*。在现实生活中，使用*命令组合*，大量的程序被少量的外壳代码所取代。通过将函数的输入视为流、序列或列表，同样的过程可以在 C++ 程序中实现。数据可以作为一个标准的数据容器从一个函数或函数对象(又名函子)传递到另一个函数或函数对象。\n\nDr. Donald Knuth, the legendary computer scientist and Stanford University Professor was asked to write a program that:\n\n*   读取文本文件并确定 *n* 个常用词\n*   打印出单词及其频率的排序列表\n\nKnuth's solution was a ten-page Pascal program! Doug McIlroy realized the same with just the following shell script:\n\n`tr -cs A-Za-z ' n ' | tr A-Z a-z | sor t | uniq -c | sor t -rn | sed ${1}q`So much for the power of command composition.\n\n# 流编程模型的优势\n\n传统的面向对象程序对层次结构建模良好，与处理线性集合相比，处理层次结构是一个困难的过程。在 Stream 编程模型的情况下，我们可以将输入视为放入容器中的实体流，将输出视为实体包，而无需修改输入数据流。使用 C++ 泛型编程技术，我们可以编写与容器无关的代码来处理流。这种模式的一些优点是:\n\n*   流编程简化了程序逻辑\n*   流可以支持惰性评估和功能风格转换\n*   流更适合并发编程模型(源流是不可变的)\n*   我们可以组合函数来创建更高阶的函数来处理它们\n*   流促进了声明式编程模型\n*   他们可以聚合、过滤和转换来自不同来源的数据\n*   它们分离数据源和处理它们的实体\n*   它们提高了代码的可读性(开发人员可以更快地理解代码)\n*   他们可以利用数据并行性和任务并行性\n*   我们可以利用数百个定义良好的流操作符(算法)来处理数据\n\n# 使用流库的应用流编程\n\n在本节中，我们将介绍使用由乔纳·谢尼曼编写的公共领域库 *`Streams`* 库进行流编程的主题。图书馆位于[https://github.com/jscheiny/Streams](https://github.com/jscheiny/Streams)，可从[http://jscheiny.github.io/Streams/api.html#](http://jscheiny.github.io/Streams/api.html)获得 API 文档。介绍如下(摘自图书馆 GitHub 页面):\n\n`Streams` is a C++ library that provides lazy evaluation and functional-style transformations on data, to ease the use of C++ standard library containers and algorithms. `Streams` supports many common functional operations such as map, filter, and reduce, as well as various other useful operations such as various set operations (union, intersection, difference), partial sum, and adjacent difference, as well as many others.\n\n我们可以看到一个熟悉**标准模板库** ( **STL** )的程序员显然会对这个库放心。STL 容器被视为流数据源，STL 算法可以被视为流数据源上的转换。该库使用现代 C++ 支持的函数式编程习惯用法，并且还支持惰性评估。懒评估的概念在这里非常有意义，因为它是函数式编程模型和 Rx 编程模型的基石。\n\n# 懒惰评价\n\n在编程语言中，有两种重要的方法来计算函数的参数，如下所示:\n\n*   **应用顺序评估** ( **AO** )\n*   **正常顺序评估** ( **否**)\n\n在 AO 的情况下，参数在传递给被调用方之前，在调用上下文中进行评估。大多数传统编程语言都遵循这种方法。在否的情况下，变量的评估被推迟，直到计算结果在被调用者的上下文中得到保证。一些函数式编程语言，如 Haskell、F#、ML，都遵循 NO 模型。在函数式编程语言中，大多数函数的计算都是透明的(函数的调用不会产生副作用)；我们只需要对表达式求值一次(对于作为参数的特定值),结果可以共享，当带有相同参数的相同函数的求值再次出现执行时。这叫**懒评**。因此，懒惰评估可以被认为是一个与先前计算结果共享的 NO。默认情况下，C++ 编程语言不支持函数参数的惰性计算，但是可以使用不同的技术进行模拟，例如变量模板和表达式模板。\n\n# 一个简单的流程序\n\n为了开始使用`Streams`库，让我们编写一个小程序来生成一个数字流，并计算前十个数字的平方:\n\n```cpp\n//--------- Streams_First.cpp \n#include \"Stream.h\" \nusing namespace std; \nusing namespace Stream; \nusing namespace Stream::op; \nint main(){ \n  //-------- counter(n) - Generate a series of value \n  //-------- Map (Apply a Lambda) \n  //-------- limit(n) -- Take first ten items \n  //-------- Sum -- aggregate \n  int total = MakeStream::counter(1) \n    | map_([] (int x) { return x * x; } // Apply square on each elements \n    | limit(10) //take first ten elements\n```\n\n```cpp\n   | sum();  // sum the Stream contents Streams::op::sum \n   //----------- print the result \n   cout << total << endl; \n} \n```\n\n前面的代码片段生成了一个值列表(使用`MakeStream::counter(1)`)，生成的值将使用 map 函数进行转换(在本例中，计算正方形)。当在流中组装十个元素(`limit(10)`)时，我们称之为流上的运算符和。\n\n# 使用流范例聚合值\n\n现在我们已经理解了 Stream 库所设想的 Stream 编程的基础，让我们编写一段代码来计算存储在 std::vector 容器中的数字的平均值:\n\n```cpp\n//--------------- Streams_Second.cpp \n// g++ -I./Streams-master/sources Streams_Second.cpp \n// \n#include \"Stream.h\" \n#include <ioStream> \n#include <vector> \n#include <algorithm> \n#include <functional> \nusing namespace std; \nusing namespace Stream; \nusing namespace Stream::op; \nint main() { \n  std::vector<double> a = { 10,20,30,40,50 }; \n  //------------ Make a Stream and reduce  \n  auto val =  MakeStream::from(a)  | reduce(std::plus<void>()); \n  //------ Compute the arithematic average \n  cout << val/a.size() << endl; \n} \n```\n\n前面的代码片段从`std::vector`创建了一个流，并使用`std::plus`函子应用了一个简化过程。这相当于聚合了流中的值。最后，我们用`std::vector`中的元素数除以聚合值。\n\n# 短期交易日志和流范例\n\n`Streams`库可以与 STL 容器无缝配合。以下代码片段将映射流上的函数，并将结果数据转换为向量容器:\n\n```cpp\n//--------------- Streams_Third.cpp \n// g++ -I./Streams-master/sources Streams_Third.cpp \n// \n#include \"Stream.h\" \n#include <ioStream> \n#include <vector> \n#include <algorithm> \n#include <functional> \n#include <cmath> \nusing namespace std; \nusing namespace Stream; \nusing namespace Stream::op; \ndouble square( double a ) { return a*a; } \nint main() { \n  std::vector<double> values = { 1,2,3,4,5 }; \n  std::vector<double> outputs = MakeStream::from(values) \n               | map_([] (double a ) { return a*a;})  \n               | to_vector(); \n  for(auto pn : outputs ) \n  { cout << pn << endl; } \n} \n```\n\n前面的代码片段将`std::vector<double>`转换为流，应用平方函数，并将素材转换回`std:::vector<double>`。之后，向量被迭代以打印内容。`Streams`库文档非常详细，包含许多代码示例，您可以使用它们来为生产质量的应用编写代码。参考美国石油学会文件，可在[http://jscheiny.github.io/Streams/api.html](http://jscheiny.github.io/Streams/api.html)获得。\n\n# 关于流库的一句话\n\n`Streams`库是一个设计良好的软件，具有直观的编程模型。任何使用过函数式编程和 Streams 编程的程序员都会在几个小时内对它感到很舒服。熟悉 STL 的人也会发现这个库非常直观。从编程模型的角度来看，应用编程接口可以分为:\n\n*   核心方法(流初始化)\n*   生成器(流创建者)\n*   有状态中间操作符(功能性不可变转换)\n*   无状态中间运算符\n*   终端操作员\n\n前面提到的图书馆文献揭示了这个奇妙图书馆的各个方面。\n\n# 事件流编程\n\n我们对流编程模型的工作有了一些了解。当我们将事件作为流处理时，它可以归类为事件流编程。在编程社区中，事件驱动架构被认为是制作现代程序的更好模型。依赖事件流编程的软件的一个很好的例子是版本控制系统。在版本控制系统中，一切都被视为一个事件。典型的例子包括签出代码、提交、回滚和分支。\n\n# 事件流编程的优势\n\n与传统的事件编程模型相比，将事件聚合为流并在下游系统中处理它们具有许多优势。一些主要优势是:\n\n*   事件源和事件接收器不耦合\n*   事件接收器可以处理事件，而不用担心事件源\n*   我们可以应用流处理操作符来处理和过滤流\n*   转换和过滤可以在聚合级别完成\n*   事件可以通过流处理网络传播\n*   事件处理可以很容易地并行化(声明式并行)\n\n# 流库及其编程模型\n\n来自 Irit Katiel 的 Streamulus 库是一个通过编程模型使事件流的编程变得更容易的库，它实现了**特定领域的嵌入式语言** ( **DSEL** )。为了理解编程模型，让我们检查一个将数据流式传输到聚合接收数据的类中的程序:\n\n```cpp\n#include \"Streamulus.h\" \n#include <ioStream> \nusing namespace std; \nusing namespace Streamulus; \nstruct print {     \n    static double temp; \n    print() { } \n    template<typename T> \n    T operator()(const T& value) const {  \n        print::temp += value; \n        std::cout << print::temp << std::endl;  return value; \n     } \n}; \ndouble print::temp = 0; \n```\n\n前面的函子只是累加传递给静态变量的值。对于`Streamify`模板(`Streamify<print>(s)`)对函数的每次调用，到目前为止累积的值将被打印到控制台。通过下面的列表可以了解更多信息:\n\n```cpp\nvoid hello_Stream() { \n    using namespace Streamulus; \n    // Define an input Stream of strings, whose name is \"Input Stream\" \n    InputStream<double> s = \n             NewInputStream<double>(\"Input Stream\", true /* verbose */); \n    // Construct a Streamulus instance \n    Streamulus Streamulus_engine;   \n\n```\n\n我们使用`NewInputStream<T>`模板方法创建一个流。该函数需要一个参数来确定日志是否应该打印到控制台。通过给出第二个参数`false`，我们可以关闭详细模式。我们需要创建一个 Streamulus 引擎的实例来编排数据流。流引擎对流表达式进行拓扑排序，以确定变更传播顺序:\n\n```cpp\n    // For each element of the Stream:  \n    //     aggregate the received value into a running sum\n```\n\n```cpp\n    //     print it  \n    Streamulus_engine.Subscribe(Streamify<print>( s));    \n```\n\n我们使用`Streamify<f>` strop (Stream 运算符)来序列化对我们刚刚创建的打印函子的调用。我们可以创建自己的流操作符，通常 Streamify 就足够了。Streamfiy 创建一个事件函子和一个 strop:\n\n```cpp\n    // Insert data to the input Stream \n    InputStreamPut<double>(s, 10); \n    InputStreamPut<double>(s, 20); \n    InputStreamPut<double>(s, 30);     \n} \nint main() {  hello_Stream();  return 0; } \n```\n\n前面的代码片段将一些值发送到流中。我们将能够看到累计金额在控制台上打印三次。在主函数中，我们调用`hello_Stream`函数来触发所有的动作。\n\n既然我们已经了解了 Streamulus 系统如何用一个简单的程序工作，那么让我们编写一个程序，更好地阐明这个库的语义。下面的程序通过一系列单参数函子对数据进行流式处理，以演示库的功能。我们还在清单中大量使用流表达式:\n\n```cpp\n/////////////////////////// \n//  g++ -I\"./Streamulus-master/src\"  -I<PathToBoost>s Streamulus_second.cpp \n#include \"Streamulus.h\" \n#include <ioStream> \nusing namespace std; \nusing namespace Streamulus; \n//-------  Functors for doubling/negating and halfving values \nstruct twice {     \n    template<typename T> \n    T operator()(const T& value) const {return value*2;} \n}; \nstruct neg {     \n    template<typename T> \n    T operator()(const T& value) const{ return -value; } \n}; \nstruct half{     \n    template<typename T> \n    T operator()(const T& value) const { return 0.5*value;} \n};\n```\n\n前面这组函子本质上是算术性质的。`twice`函子将参数加倍，`neg`函子翻转参数的符号，`half`函子将值缩放 0.5 以将参数的值减半:\n\n```cpp\nstruct print{     \n    template<typename T> \n    T operator()(const T& value) const{  \n        std::cout << value << std::endl; \n        return value; \n    } \n}; \nstruct as_string  { \n    template<typename T> \n    std::string operator()(const T& value) const {  \n        std::stringStream ss; \n        ss << value; \n        return ss.str(); \n    } \n};\n```\n\n前面两个函数对象如何工作是显而易见的——第一个(打印)只是将值输出到控制台。`as_string`使用`std::stringStream`类将参数转换为字符串:\n\n```cpp\nvoid DataFlowGraph(){ \n    // Define an input Stream of strings, whose name is \"Input Stream\" \n    InputStream<double> s = \n          NewInputStream<double>(\"Input Stream\", false /* verbose */); \n    // Construct a Streamulus instance \n    Streamulus Streamulus_engine;             \n    // Define a Data Flow Graph for Stream based computation  \n    Subscription<double>::type val2 =  Streamulus_engine.Subscribe(Streamify<neg> \n                         (Streamify<neg>(Streamify<half>(2*s)))); \n    Subscription<double>::type val3 = Streamulus_engine.Subscribe( \n                                      Streamify<twice>(val2*0.5)); \n    Streamulus_engine.Subscribe(Streamify<print>(Streamify<as_string>(val3*2))); \n    //------------------ Ingest data into the Stream \n    for (int i=0; i<5; i++) \n        InputStreamPut(s, (double)i); \n}\n```\n\n`DataFlowGraph()`创建了`InputStream<T>`来处理双值流。实例化`Streamulus`对象(引擎)后，我们通过`Streamify<f>`流操作符粘附了一系列函子。该操作可以被认为是一种具有单个自变量函数的函数组合。建立机制后，我们使用`InputStreamPut`函数将数据注入到流中:\n\n```cpp\nint main(){ \n    DataFlowGraph(); //Trigger all action \n    return 0; \n} \n```\n\n# Streamulus 库——对其内部的一瞥\n\n`Streamulus`库基本上创建了一个变更传播图来简化流处理。我们可以将图的节点视为计算，将边视为将数据从一个节点带到另一个节点的缓冲区。几乎所有的数据流系统都遵循相同的语义。`Streamulus`库帮助我们构建因变量的图，这有助于我们将更改传播到子节点。变量更新的顺序将通过对图进行拓扑排序来定义。\n\nA graph is a data structure where a set of dependent entities is represented as nodes (or vertices) and their relationship (as edges) between them. In computer science, especially when it comes to scheduling and analyzing dependencies, a particular version of graph, called directed acyclic graphs, is preferred for its unique qualities. A DAG is a directed graph without cycles. We can perform an operation called a topological sort to determine the linear order in which the entities are dependent. The topological sorting can only be performed on a DAG and they are not unique. In the following graph, we can find multiple topological orders:\n\n![](img/00008.jpeg)\n\n# Streamulus 库——表达式处理研究\n\n我们将看看`Streamulus`如何使用简单的流表达式处理表达式:\n\n```cpp\nInputStream<int>::type x = NewInputStream<int>(\"X\"); \nEngine.Subscribe( -(x+1)); \n```\n\n`- (x+1)`流表达式将产生以下图形。术语 strop 代表流操作符，每个节点被组织为一个 strop:\n\n![](img/00009.jpeg)\n\n一旦正确标记了节点，将对图进行拓扑排序以确定执行顺序。下图显示了拓扑排序(可以有多个拓扑顺序):\n\n![](img/00010.jpeg)\n\nStreamulus 引擎遍历该图，找出流操作符必须应用于通过网络传播的数据的顺序。**至**标签代表**拓扑顺序**。拓扑排序后，将产生一个按拓扑顺序排列的线性流算子列表。执行引擎将按照拓扑顺序执行代码。\n\nThe Streamulus engine performs its magic using the boost proto library. The latter manages expression trees for the Streamulus library. To really go through the source code of the library, you need to be comfortable with template meta programming, especially expression templates. Meta programming is a technique where we write code to generate or transform source code. It turned out that the C++ template mechanism was Turing complete by Erwin Unruh in the year 1994.\n\n# 电子表格库——一个变更传播引擎\n\n电子表格经常被吹捧为反应系统的典型例子。在电子表格中，页面被组织为单元格矩阵。当单元格发生变化时，将重新计算所有从属单元格以反映变化。每个细胞都会这样。实际上，如果您有一个像 Streamulus 这样的库，那么为电子表格建模是很容易的。幸运的是，库的设计者自己编写了另一个依赖 Streamulus 进行变更传播的库。\n\nSpreadsheet is a C++ library that enables spreadsheet-programming, that is, setting up variables (cells) where each cell is assigned an expression that can contain the values of other cells. Changes are propagated to all dependent cells, as in a spreadsheet. Spreadsheet was developed to demonstrate the use of Streamulus. Spreadsheet is a header-only library. It uses boost and Streamulus. So put these three libraries in your include path. The details of the library can be found at [https://github.com/iritkatriel/spreadsheet](https://github.com/iritkatriel/spreadsheet).\n\n我们将浏览一个利用`Spreadsheet`库的示例程序，该库包含在项目的 GitHub 存储库中(`main.cpp`):\n\n```cpp\n#include \"spreadsheet.hpp\" \n#include <ioStream> \nint main (int argc, const char * argv[]) {  \n    using namespace spreadsheet; \n    Spreadsheet sheet; \n    Cell<double> a = sheet.NewCell<double>(); \n    Cell<double> b = sheet.NewCell<double>(); \n    Cell<double> c = sheet.NewCell<double>(); \n    Cell<double> d = sheet.NewCell<double>(); \n    Cell<double> e = sheet.NewCell<double>(); \n    Cell<double> f = sheet.NewCell<double>();\n```\n\n前面的代码片段创建了一组单元格，用作 IEEE 双精度浮点数的容器。初始化单元格后，我们将开始用以下一组表达式来改变单元格的值:\n\n```cpp\n    c.Set(SQRT(a()*a() + b()*b())); \n    a.Set(3.0); \n    b.Set(4.0); \n    d.Set(c()+b()); \n    e.Set(d()+c()); \n```\n\n现在，我们将使用前面的表达式对值进行变异。在通过`Set`方法的每次赋值之后，将通过单元触发一次计算传递。`Streamulus`库管理底层流程:\n\n```cpp\n    std::cout << \" a=\" << a.Value()  \n              << \" b=\" << b.Value()  \n              << \" c=\" << c.Value()  \n              << \" d=\" << d.Value()  \n              << \" e=\" << e.Value()  \n              << std::endl;\n```\n\n前面的代码片段将单元格的值打印到控制台。我们将再次更改单元格的表达式，以触发计算流程图:\n\n```cpp\n    c.Set(2*(a()+b())); \n    c.Set(4*(a()+b())); \n    c.Set(5*(a()+b())); \n    c.Set(6*(a()+b())); \n    c.Set(7*(a()+b())); \n    c.Set(8*(a()+b())); \n    c.Set(a()); \n    std::cout << \" a=\" << a.Value()  \n              << \" b=\" << b.Value()  \n              << \" c=\" << c.Value()  \n              << \" d=\" << d.Value()  \n              << \" e=\" << e.Value()  \n              << std::endl;     \n    std::cout << \"Goodbye!n\"; \n    return 0; \n} \n```\n\n可以细读图书馆的源代码来了解图书馆的内部运作。电子表格是一个很好的例子，说明如何利用 Streamulus 库来编写健壮的软件。\n\n# 另一个流处理库\n\nRaftLib 是一个值得任何对并行编程或基于流的编程感兴趣的人(开发人员)查看的库。图书馆在 https://github.com/RaftLib/RaftLib。以下描述可从前面的网站获得\n\nRaftLib is a C++ Library for enabling Stream/data-flow parallel computation. Using simple right-shift operators (just like the C++ Streams that you would use for string manipulation), you can link parallel compute kernels together. With RaftLib, we do away with explicit use of pthreads, std::thread, OpenMP, or any other parallel threading library. These are often mis-used, creating non-deterministic behavior. RaftLib's model allows lock-free FIFO-like access to the communications channels connecting each compute kernel. The full system has many auto-parallelization, optimization, and convenience features that enable relatively simple authoring of performant applications.\n\n由于篇幅限制，我们不会在本书中详细介绍`RaftLib`。图书馆作者(乔纳森·比尔德)的精彩演讲可在[https://www.youtube.com/watch?v=IiQ787fJgmU](https://www.youtube.com/watch?v=IiQ787fJgmU)获得。让我们看一下展示这个库的工作原理的代码片段:\n\n```cpp\n#include <raft> \n#include <raftio> \n#include <cstdlib> \n#include <string> \n\nclass hi : public raft::kernel \n{ \npublic: \n    hi() : raft::kernel(){ output.addPort< std::string >( \"0\" ); } \n    virtual raft::kstatus run(){ \n        output[ \"0\" ].push( std::string( \"Hello Worldn\" ) ); \n        return( raft::stop );  \n    } \n}; \n\nint main( int argc, char **argv ) { \n    /** instantiate print kernel **/ \n    raft::print< std::string > p; \n    /** instantiate hello world kernel **/ \n    hi hello; \n    /** make a map object **/ \n    raft::map m; \n    /** add kernels to map, both hello and p are executed concurrently **/ \n    m += hello >> p; \n    /** execute the map **/ \n    m.exe(); \n    return( EXIT_SUCCESS ); \n} \n```\n\n作为一名程序员，你应该为定制计算定义一个内核，并使用`>>`操作符来流式传输数据。在前面的代码中，`hi`类就是这样一个内核。参考`Raftlib`文档(可在前面的 RaftLib 网址上获得)和源代码示例，了解更多关于这个精彩的库的信息。\n\n# 这些东西和 Rx 编程有什么关系？\n\n基本上，反应式编程模型将事件视为通过变更传播图传播的数据流。为此，我们需要将事件元素聚合到基于容器的数据结构中，并在此基础上创建一个流。有时，如果有大量数据，我们甚至会将统计技术应用于样本事件。在被通知给等待通知的观察者之前，生成的流可以在源级别使用函数转换进行过滤和转换。事件源应该对事件流调度采取一种先发制人的方法，以避免事件源接收器和事件接收器之间的耦合。何时调度事件数据将由调度软件决定，该软件以异步方式运行功能转换管道。因此，反应式编程的关键要素是:\n\n*   可观察的(其他人感兴趣的数据流)\n*   观察者(对观察对象感兴趣并订阅通知的实体)\n*   调度程序(它决定流何时应该在网络中传播)\n*   函数运算符(事件过滤和转换)\n\n简而言之，`Scheduler`(Rx 引擎的一部分)在通知订阅者之前，异步取一个`Observable`进行过滤和转换，如图所示:\n\n![](img/00011.jpeg)\n\n# 摘要\n\n在本章中，我们讨论了事件流编程的主题。与传统的事件处理模型相比，将事件视为流有许多优势。我们从`Streams`库开始，了解了它的编程模型。我们还编写了一些程序来熟悉这个库及其语义。`Streams`库有很好的文档，您应该参考它的文档来了解更多信息。在 Streams 库之后，我们看了 Streamulus 库，它为事件流的操作提供了一种 DSEL 方法。我们写了几个程序，还研究了 T2 图书馆的一些示例程序。我们还提到了`Raftlib`库，一个用于流处理的替代库。随着事件流编程模型的覆盖，我们现在已经完成了理解反应式编程的先决条件，特别是 RxCpp 库。在下一章中，我们将开始使用 RxCpp 库进入反应式系统设计的编程模型。"
  },
  {
    "path": "docs/cpp-react-prog/07.md",
    "content": "# 七、数据流计算和 RxCpp 库简介\n\n从这一章开始，我们将进入反应式编程模型的核心。您可以将前面的章节视为理解反应式编程模型的一种先决条件，更具体地说，是功能反应式编程。如果我们回顾一下，我们涵盖了必要的先决条件，包括以下内容:\n\n*   各种图形用户界面平台上的事件编程模型\n*   现代 C++ 语言的旋风之旅(包括函数式编程)\n*   更好的并发系统的语言级并发\n*   无锁编程模型(朝着声明性编程迈出的一步)\n*   先进的设计模式和可观察的概念\n*   事件流编程\n\n在**功能反应编程** ( **玻璃钢**)的情况下，所有这些主题以系统的方式结合在一起。\n\n简单地说，反应式编程就是用异步数据流编程。通过对 Streams 应用各种操作，我们可以实现不同的计算目标。反应式程序的主要任务是将数据转换成流，而不考虑数据的来源。事件流通常被称为可观察的，事件流订阅者被称为**观察者**。在可观测值和观测值之间，有流操作符(过滤器/变换)。\n\n由于隐式假设在数据通过运算符传递时数据源不会发生变化，因此我们可以在 Observables 和 Observers 之间有多个运算符路径。不变性为无序执行提供了选项，调度可以委托给一个称为调度器的特殊软件。因此，观察器、观察器、流操作器和调度器构成了玻璃钢模型的主干。\n\n在本章中，我们将涵盖以下主题:\n\n*   浅谈数据流计算范式\n*   RxCpp 库及其编程模型介绍\n*   一些基本的 RxCpp 程序来让我们的脚湿\n*   接收流操作符\n*   大理石图表\n*   行程安排\n*   `flat` / `concat`地图古怪\n*   更多操作员\n\n# 数据流计算范式\n\n传统上，程序员根据控制流来编码他们的程序。这意味着我们将程序编码为一系列小语句(序列、分支、迭代)或函数(包括递归)，以及它们的相关状态。我们使用构造，例如选择(`if` / `else`)、迭代(`while` / `for`)和递归函数来编码我们的计算。为这些类型的程序处理并发性和状态管理确实是有问题的，它们会导致微妙的错误。我们需要围绕共享的可变状态放置锁和其他同步原语。在编译器层面，语言编译器将解析源代码，创建**抽象语法树** ( **AST** )，进行类型分析、代码生成和代码生成。事实上，AST 是一个信息流图，您可以在其中执行数据流分析(用于数据/寄存器级优化)和控制流分析，以利用处理器级的代码流水线优化。即使程序员根据控制流来编码程序，编译器(至少是它的一部分)也试图根据数据流来看程序。这里的底线是，每个程序中都有一个隐藏的隐式数据流图。\n\n数据流计算将计算组织为一个显式图，其中节点是计算，边是数据在节点之间流动的路径。如果我们对节点上的计算设置某些限制(例如通过处理输入数据的副本来保存数据状态，避免原地算法)，我们就可以利用并行的机会。调度程序将通过对图形数据结构进行拓扑排序来寻找并行的机会。我们将使用流(`Path`)和流上的操作(`Node`)来构建图。这可以通过声明的方式完成，因为操作符可以被编码为 Lambdas，它进行一些本地计算。有一套基本的标准(函数/流)操作符，如`map`、`reduce`、`filter`和`take`，由函数编程社区识别。数据流计算框架中有一项规定，将数据转换为流。机器学习的 TensorFlow 库就是使用这种范式的一个库。RxCpp 库也可以被认为是一个数据流计算库，即使图形创建不是完全显式的，就像 TensorFlow 的情况一样。因为函数式编程构造支持惰性评估，所以当我们用异步数据流和操作构造流管道时，我们正在创建一个计算流图。\n\n# RxCpp 库简介\n\n在本书的其余部分，我们将使用 RxCpp 库来编写我们的反应程序。RxCpp 库是一个只有头文件的 C++ 库，可以从 GitHub repo 下载:[http://reactive-extensions.github.io/RxCpp/](http://reactive-extensions.github.io/RxCpp/)。RxCpp 依赖于现代 C++ 构造，如语言级并发、Lambda 函数/表达式、函数组合/转换和运算符重载，来实现反应式编程构造。RxCpp 库是按照`Rx.net`和`Rxjava`这样的库来构建的。\n\n像任何其他反应式编程框架一样，在编写第一行代码之前，每个人都应该了解一些关键的构造。它们是:\n\n*   可观察的(可观察的流)\n*   观察员(订阅《观察家报》)\n*   运算符(例如，筛选器、转换和缩减)\n*   调度程序\n\nRxCpp 是一个只有头部的库，大部分计算都是基于可观测的概念。该库提供了许多原语来从各种数据源创建可观察的流。数据源可以是范围、STL 容器等等。我们可以将操作者放在 Observables 和他们的消费者(被称为观察者)之间。由于函数式编程结构支持函数的组合，我们可以将一系列操作符作为单个实体放在订阅流的 Observables 和 Observers 之间。与库相关联的调度器将确保当数据在可观察流中可用时，它将通过操作者传递，并且在一系列过滤和转换之后，如果存在数据，将向订阅者发出通知。当来自订阅者的 Lambda 方法被调用时，观察者需要操心一些事情。观察员可以专注于他们主要负责的任务。\n\n# RxCpp 库及其编程模型\n\n在这一部分，我们将编写一些程序，帮助读者理解 RxCpp 库的编程模型。这些程序的目的是阐明接收概念，它们本质上大多是琐碎的。代码对于程序员来说足够了，只要稍加调整就可以将它们合并到生产实现中。数据生产者及其可观测性将基于范围、STL 容器等。\n\n# 简单的可观察/观察者互动\n\n让我们编写一个简单的程序，帮助我们理解 RxCpp 库的编程模型。在这个特定的程序中，我们将有一个可观察的流和一个订阅该流的观察者。我们将使用 range 对象生成一系列从 1 到 12 的数字。在创建了值的范围和其上的可观测值之后，我们将为可观测值附加一个订户。当我们执行该程序时，它会将一系列数字打印到控制台，并进行额外的测试:\n\n```cpp\n////////// \n// First.cpp \n// g++ -I<PathToRxCpplibfoldersrc> First.cpp \n#include \"rxcpp/rx.hpp\" \n#include <ioStream> \nint main() { \n //------------- Create an Observable.. a Stream of numbers \n //------------- Range will produce a sequence from 1 to 12 \n auto observable = rxcpp::observable<>::range(1, 12);\n```\n\n```cpp\n //------------ Subscribe (only OnNext and OnCompleted Lambda given \n observable.Subscribe(  \n    [](int v){printf(\"OnNext: %dn\", v);}, \n    [](){printf(\"OnCompleted\\n\");}); \n} \n```\n\n前面的程序会把数字打印到控制台上，`OnCompleted`会发射到控制台上。这个程序演示了我们如何创建一个可观察的流，并使用`Subscribe`方法将一个观察者连接到该流。\n\n# 可观察的过滤器和转换\n\n除了使用 subscribe 方法将观察者连接到可观测流的常见机制之外，以下程序将帮助我们理解过滤器和`map`操作符是如何工作的。filter 方法计算流中每个项目上的谓词，如果该计算恰好产生一个肯定断言，则该项目将出现在输出流中。`map`运算符对输入流的每个元素应用一个表达式，并帮助生成一个输出队列:\n\n```cpp\n/////////////////////////////////////// \n// Second.cpp \n#include \"rxcpp/rx.hpp\" \n#include <ioStream> \nint main() { \n  auto values = rxcpp::observable<>::range(1, 12). \n      filter([](int v){ return v % 2 ==0 ;}). \n      map([](int x) {return x*x;});  \n  values.subscribe( \n           [](int v){printf(\"OnNext: %dn\", v);}, \n           [](){printf(\"OnCompleted\\n\");}); \n} \n```\n\n前面的程序生成一个数字流，并通过一个过滤函数传递该流。`filter`功能尝试检测数字是否为偶数。输出流将被传递到`map`函数，该函数将对流的内容进行平方。最终，流的内容将被打印到控制台。\n\n# 从 C++ 容器流式传输值\n\n即使接收意味着处理随时间变化的数据，我们也可以将 STL 容器转换为反应流。我们需要使用 Iterate 运算符进行转换。这有时很方便，并且有助于集成来自使用 STL 的代码库的代码:\n\n```cpp\n// STLContainerStream.cpp\n#include \"rxcpp/rx.hpp\"\n#include <ioStream>\n#include <array>\nint main() {\n    std::array< int, 3 > a={{1, 2, 3}};\n    auto values = rxcpp::observable<>::iterate(a);\n    values.subscribe([](int v){printf(\"OnNext: %dn\", v);},\n    [](){printf(\"OnCompleted\\n\");});\n}\n```\n\n# 从头开始创建观察点\n\n到目前为止，我们已经编写了从范围对象或 STL 容器创建可观察流的程序。让我们看看如何从头开始创建一个可观察的流。嗯，差不多:\n\n```cpp\n// ObserverFromScratch.cpp \n#include \"rxcpp/rx.hpp\" \n#include \"rxcpp/rx-test.hpp\" \nint main() { \n      auto ints = rxcpp::observable<>::create<int>(  [](rxcpp::subscriber<int> s){ \n            s.on_next(1); \n            s.on_next(4); \n            s.on_next(9); \n           s.on_completed(); \n    }); \n    ints.subscribe( [](int v){printf(\"OnNext: %dn\", v);}, \n                             [](){printf(\"OnCompletedn\");}); \n} \n```\n\n前面的程序调用`next`方法来发出一系列完美平方的数字。这些数字将被打印到控制台上。\n\n# 连接可观察的流\n\n我们可以将两个流连接起来形成一个新的流，这在某些情况下会很方便。让我们通过编写一个简单的程序来看看这是如何工作的:\n\n```cpp\n//------------- Concactatenate.cpp \n#include \"rxcpp/rx.hpp\" \n#include \"rxcpp/rx-test.hpp\" \nint main() { \n auto values = rxcpp::observable<>::range(1);  \n auto s1 = values.take(3).map([](int prime) { return 2*prime;);}); \n auto s2 = values.take(3).map([](int prime) { return prime*prime);}); \n s1.concat(s2).subscribe(rxcpp::util::apply_to( \n            []( int p) { printf(\" %dn\", p);})); \n} \n```\n\n串联操作符通过保持顺序一个接一个地附加流。\n\n# 取消订阅可观察的流\n\n下面的程序展示了如何订阅可观察的流并停止订阅。该程序仅显示了可用的选项，应参考文档以:\n\n```cpp\n//---------------- Unsubscribe.cpp \n#include \"rxcpp/rx.hpp\" \n#include \"rxcpp/rx-test.hpp\" \n#include <ioStream> \nint main() { \n    auto subs = rxcpp::composite_subscription(); \n    auto values = rxcpp::observable<>::range(1, 10); \n    values.subscribe( \n        subs,[&subs](int v){ \n            printf(\"OnNext: %dn\", v); \n            if (v == 6) \n                subs.unsubscribe(); //-- Stop recieving events \n        }, \n        [](){printf(\"OnCompletedn\");}); \n}\n```\n\n# 视觉表现用大理石图介绍\n\n很难可视化反应流，因为数据异步流动。Rx 系统的设计者已经创建了一组可视化提示，称为**大理石图**:\n\n```cpp\n//------------------ Map.cpp \n#include \"rxcpp/rx.hpp\" \n#include \"rxcpp/rx-test.hpp\" \n#include <ioStream> \n#include <array> \nint main() { \n    auto ints = rxcpp::observable<>::range(1,10). \n                 map( [] ( int n  ) {return n*n; }); \n    ints.subscribe( \n            [](int v){printf(\"OnNext: %dn\", v);}, \n            [](){printf(\"OnCompletedn\");}); \n} \n```\n\n我们先来看一个描绘`map`运算符的大理石图，而不是描述大理石图:\n\n![](img/00012.gif)\n\n大理石图的顶部显示了两条时间线，这些时间线将通过将第二条时间线的内容附加到第一条时间线来组合在一起，以形成复合时间线。\n\n# 流操作符\n\n面向流的处理的主要优势之一是我们可以在其上应用函数式编程原语。用 RxCpp 的话来说，处理是使用运算符完成的。它们只不过是流上的过滤器、转换、聚合和缩减。在前面的例子中，我们已经看到了`map`、`filter`和`take`操作符是如何工作的。\n\n# 平均算子\n\n`average`运算符计算可观测流的算术平均值。支持的其他统计运算符包括:\n\n*   福建话\n*   最大\n*   数数\n*   总和\n\n下面的程序只是演示了`average`操作符。对于前面列表中的其他运算符，模式是相同的:\n\n```cpp\n//----------- Average.cpp \n#include \"rxcpp/rx.hpp\" \n#include \"rxcpp/rx-test.hpp\" \n#include <ioStream> \nint main() { \n    auto values = rxcpp::observable<>::range(1, 20).average(); \n    values.subscribe( \n            [](double v){printf(\"average: %lfn\", v);}, \n            [](){printf(\"OnCompletedn\");}); \n} \n```\n\n# 扫描操作员\n\n`scan`运算符依次对流的每个元素应用一个函数，并将该值累积为一个种子值。当数值累加时，下列程序产生一系列数字的平均值:\n\n```cpp\n//----------- Scan.cpp \n#include \"rxcpp/rx.hpp\" \n#include \"rxcpp/rx-test.hpp\" \n#include <ioStream> \nint main() { \n    int count = 0; \n    auto values = rxcpp::observable<>::range(1, 20). \n        scan( 0,[&count](int seed, int v){ \n                count++ ; \n                return seed + v; \n            }); \n    values.subscribe( \n        [&](int v){printf(\"Average through Scan: %fn\", (double)v/count);}, \n        [](){printf(\"OnCompletedn\");}); \n} \n```\n\n运行平均值将打印在控制台上。`OnNext`在`OnCompleted`被调用之前会被调用十九次。\n\n# 通过管道操作器组成操作器\n\nRxCpp 库允许您链接或组合运算符以启用运算符组合。该库允许您使用`pipe` ( `|`)运算符来组成运算符，程序员可以将一个运算符的输出管道传输到另一个运算符，就好像它们在 UNIX shell 的命令行中一样。这使我们能够理解一段代码的作用。以下程序使用`|`操作符绘制范围。RxCpp 示例包含许多使用管道函数的示例:\n\n```cpp\n//------------------ Map_With_Pipe.cpp \n#include \"rxcpp/rx.hpp\" \n#include \"rxcpp/rx-test.hpp\" \nnamespace Rx { \nusing namespace rxcpp; \nusing namespace rxcpp::sources; \nusing namespace rxcpp::operators; \nusing namespace rxcpp::util; \n} \nusing namespace Rx; \n#include <ioStream> \nint main() { \n    //---------- chain map to the range using the pipe operator \n    //----------- avoids the use of . notation. \n    auto ints = rxcpp::observable<>::range(1,10) |  \n                 map( [] ( int n  ) {return n*n; }); \n    ints.subscribe( \n            [](int v){printf(\"OnNext: %dn\", v);}, \n            [](){printf(\"OnCompletedn\");}); \n}\n```\n\n# 使用调度程序\n\n在前一节中，我们已经了解了可观测值、操作符和观测值。我们已经知道，在可观测值和观测值之间，我们可以应用标准的接收操作符来过滤和转换流。在函数式编程的情况下，我们编写不可变的函数(没有副作用的函数)，不可变的一个后果是可能会无序执行。如果我们能保证运算符的输入永远不会被修改，那么我们求值的顺序就无关紧要了。由于一个接收程序将操作多个观察器和订阅器，我们可以将选择执行顺序的任务委托给调度器模块。默认情况下，RxCpp 将在我们称为`subscriber`方法的线程中调度执行。可以使用`observe_on`和`subscriber_on`操作符指定不同的螺纹。此外，一些可观察的操作符将调度器作为参数，其中执行可以发生在由调度器管理的线程中。\n\nRxCpp 库支持以下两种调度程序类型:\n\n*   `ImmediateScheduler`\n*   `EventLoopScheduler`\n\n默认情况下，RxCpp 库是单线程的。但是您可以使用某些操作符将其配置为在多个线程中运行:\n\n```cpp\n//----------ObserveOn.cpp \n#include \"rxcpp/rx.hpp\" \n#include \"rxcpp/rx-test.hpp\" \n#include <ioStream> \n#include <thread> \nint main(){ \n //---------------- Generate a range of values \n //---------------- Apply Square function \n auto values = rxcpp::observable<>::range(1,4). \n               map([](int v){ return v*v;}); \n //------------- Emit the current thread details \n std::cout  << \"Main Thread id => \"  \n            << std::this_thread::get_id()  \n            << std::endl; \n //---------- observe_on another thread.... \n //---------- make it blocking to  \n values.observe_on(rxcpp::synchronize_new_thread()).as_blocking(). \n subscribe( [](int v){  \n                   std::cout << \"Observable Thread id => \"  \n                             << std::this_thread::get_id()  \n                             << \"  \" << v << std::endl ;}, \n                  [](){ std::cout << \"OnCompleted\" << std::endl; }); \n //------------------ Print the main thread details \n std::cout << \"Main Thread id => \"  \n           << std::this_thread::get_id()  \n           << std::endl;   \n} \n```\n\n前面的程序将产生以下输出。我们将使用 STD C++ 线程 ID 来帮助我们区分新线程中调度的项目(其中一个不同于主线程):\n\n```cpp\nMain Thread id => 1 \nObservable Thread id => 2  1 \nObservable Thread id => 2  4 \nObservable Thread id => 2  9 \nObservable Thread id => 2  16 \nOnCompleted \nMain Thread id => 1 \n```\n\n以下程序将演示`subscribe_on`方法的用法。`observe_on`和`subscribe_on`方法在行为方面有细微的区别。我们将在下一章探讨这个问题。下面列表的目的是显示可用于声明性调度的选项:\n\n```cpp\n//---------- SubscribeOn.cpp \n#include \"rxcpp/rx.hpp\" \n#include \"rxcpp/rx-test.hpp\" \n#include <ioStream> \n#include <thread> \n#include <mutex> \n//------ A global mutex for output synch. \nstd::mutex console_mutex; \n//------ Print the Current Thread details \nvoid CTDetails() { \n   console_mutex.lock(); \n   std::cout << \"Current Thread id => \"  \n           << std::this_thread::get_id()  << std::endl;  \n   console_mutex.unlock();  \n} \n//---------- a function to Yield control to other threads \nvoid Yield( bool y ) { \n   if (y) { std::this_thread::yield(); } \n\n} \nint main(){ \n    auto threads = rxcpp::observe_on_event_loop(); \n    auto values = rxcpp::observable<>::range(1); \n    //------------- Schedule it in another thread \n    auto s1 = values.subscribe_on(threads). \n        map([](int prime) {  \n             CTDetails(); Yield(true); return std::make_tuple(\"1:\", prime);}); \n    //-------- Schedule it in Yet another theread \n    auto s2 = values. subscribe_on(threads).  \n        map([](int prime) { \n           CTDetails(); Yield(true) ; return std::make_tuple(\"2:\", prime);}); \n\n    s1.merge(s2). take(6).as_blocking().subscribe(rxcpp::util::apply_to( \n            [](const char* s, int p) { \n                CTDetails(); \n                console_mutex.lock(); \n                printf(\"%s %dn\", s, p); \n                console_mutex.unlock(); \n            })); \n} \n```\n\n# 两个操作符的故事——平面图对串联图\n\n开发人员的困惑来源通常集中在`flat`地图和`concat`地图操作符上。它们之间的差异非常微妙，我们将在本节中介绍它们。让我们来看看`flat`地图操作符及其工作原理:\n\n```cpp\n//----------- Flatmap.cpp \n#include \"rxcpp/rx.hpp\" \n#include \"rxcpp/rx-test.hpp\" \n#include <ioStream> \nnamespace rxu=rxcpp::util; \n#include <array> \nint main() { \n     std::array< std::string,4 > a={{\"Praseed\", \"Peter\", \"Sanjay\",\"Raju\"}}; \n     //---------- Apply Flatmap on the array of names \n     //---------- Flatmap returns an Observable<T> ( map returns T ) \n     //---------- The First lamda creates a new Observable<T> \n     //---------- The Second Lambda manipulates primary Observable and  \n     //---------- Flatmapped Observable \n     auto values = rxcpp::observable<>::iterate(a).flat_map( \n              [] (std::string v ) { \n                   std::array<std::string,3> salutation= \n                       { { \"Mr.\" ,  \"Monsieur\" , \"Sri\" }}; \n                   return rxcpp::observable<>::iterate(salutation); \n              }, \n              [] ( std::string f , std::string s ) {return s + \" \" +f;}); \n     //-------- As usual subscribe  \n     //-------- Here the value will be interleaved as flat_map merges the  \n     //-------- Two Streams \n     values.subscribe(  \n              [] (std::string f) { std::cout << f <<  std::endl; } ,  \n              [] () {std::cout << \"Hello World..\" << std::endl;} ); \n      } \n```\n\n前一个程序以交错方式产生输出。程序的输出如下所示。这种行为的原因与映射操作后流的后处理有关:\n\n```cpp\nMr. Praseed \nMonsieur Praseed \nMr. Peter \nSri Praseed \nMonsieur Peter \nMr. Sanjay \nSri Peter \nMonsieur Sanjay \nMr. Raju \nSri Sanjay \nMonsieur Raju \nSri Raju \nHello World.. \n```\n\n下面的大理石图显示了操作的模式。`flat`图将λ应用于可观测流，并产生一个新的可观测流。产生的流被合并在一起以提供输出。在图中，红球被转换成一对相似颜色的钻石，而绿球和蓝球的输出产生交错的钻石作为新创建的可观察的输出:\n\n![](img/00013.jpeg)\n\n让我们通过一个列表来看看`concat_map`运算符。程序列表是相同的。唯一的变化是`flatMap`更名为`concatMap`。即使列表中没有差异，但输出行为有明显的差异。也许`concatMap`产生的输出适合程序员的心智模式:\n\n```cpp\n//----------- ConcatMap.cpp \n#include \"rxcpp/rx.hpp\" \n#include \"rxcpp/rx-test.hpp\" \n#include <ioStream> \nnamespace rxu=rxcpp::util; \n\n#include <array> \nint main() { \n\n     std::array< std::string,4 > a={{\"Praseed\", \"Peter\", \"Sanjay\",\"Raju\"}}; \n     //---------- Apply Concat map on the array of names \n     //---------- Concat Map returns an Observable<T> ( oncat returns T ) \n     //---------- The First lamda creates a new Observable<T> \n     //---------- The Second Lambda manipulates primary Observable and  \n     //---------- Concatenated Observable \n     auto values = rxcpp::observable<>::iterate(a).flat_map( \n              [] (std::string v ) { \n                   std::array<std::string,3> salutation= \n                       { { \"Mr.\" ,  \"Monsieur\" , \"Sri\" }}; \n                   return rxcpp::observable<>::iterate(salutation); \n              }, \n              [] ( std::string f , std::string s ) {return s + \" \" +f;}); \n\n     //-------- As usual subscribe  \n     //-------- Here the value will be interleaved as concat_map concats the  \n     //-------- Two Streams \n     values.subscribe(  \n              [] (std::string f) { std::cout << f <<  std::endl; } ,  \n              [] () {std::cout << \"Hello World..\" << std::endl;} ); \n } \n```\n\n以下是输出的外观:\n\n```cpp\nMr. Praseed \nMonsieur Praseed \nSri Praseed \nMr. Peter \nMonsieur Peter \nSri Peter \nMr. Sanjay \nMonsieur Sanjay \nSri Sanjay \nMr. Raju \nMonsieur Raju \nSri Raju \nHello World.. \n```\n\n下图为运行中的`concatMap`。与平面图大理石图不同，输出是同步的(红色、绿色和蓝色球按照处理输入的顺序产生相同颜色的输出):\n\n![](img/00014.jpeg)\n\n在`flatMap`的情况下，我们以交错的方式得到输出。但是在`concatMap`的情况下，我们按照预期输出的顺序得到了值。这里真正的区别是什么？为了明确区别，让我们看一下两个操作符:`concat`和`merge`。让我们来看看流的串联是如何工作的。它基本上一个接一个地附加流的内容，保持顺序:\n\n```cpp\n//---------------- Concat.cpp \n#include \"rxcpp/rx.hpp\" \n#include \"rxcpp/rx-test.hpp\" \n#include <ioStream> \n#include <array> \nint main() { \n    auto o1 = rxcpp::observable<>::range(1, 3); \n    auto o3 = rxcpp::observable<>::from(4, 6); \n    auto values = o1.concat(o2); \n    values.subscribe( \n            [](int v){printf(\"OnNext: %dn\", v);},[](){printf(\"OnCompletedn\");}); \n} \n```\n\n下面的大理石图清楚地显示了当一个`concat`操作符应用于两个流时会发生什么。我们通过将第二个附加到第一个的内容来创建一个新的流。这保持了顺序:\n\n![](img/00015.jpeg)\n\n现在，让我们看看当两个流合并时会发生什么。下面的代码显示了如何合并两个流:\n\n```cpp\n//------------ Merge.cpp \n#include \"rxcpp/rx.hpp\" \n#include \"rxcpp/rx-test.hpp\" \n#include <ioStream> \n#include <array> \nint main() { \n    auto o1 = rxcpp::observable<>::range(1, 3); \n    auto o2 = rxcpp::observable<>::range(4, 6); \n    auto values = o1.merge(o2); \n    values.subscribe( \n            [](int v){printf(\"OnNext: %dn\", v);}, \n             [](){printf(\"OnCompletedn\");}); \n} \n```\n\n下面的大理石图清楚地显示了当我们合并两个可观察的流时会发生什么。输出队列的内容将是两个流的交错组合:\n\n![](img/00016.jpeg)\n\n`flat`地图和`concact`地图或多或少都做同样的操作。区别在于价值观结合在一起的方式。`flat`地图使用`merge`运算符，`concat`地图使用`concact`运算符。在`merge`的情况下，顺序无关紧要。`concat`运算符一个接一个地追加可观察值。这就是为什么你按照我们期望的顺序得到这些值。\n\n# 更多重要的操作符\n\n我们现在理解了反应式编程模型的关键，因为我们涵盖了基本主题，如可观察对象、观察器、操作器和调度器。为了更好地编写逻辑，我们应该了解更多的运算符。在本节中，我们将介绍`tap`和`buffer`操作员。我们将探索`tap`运算符，它有助于查看流的内容:\n\n```cpp\n//----------- TapExample.cpp \n#include \"rxcpp/rx.hpp\" \n#include \"rxcpp/rx-test.hpp\" \n#include <ioStream> \nint main() { \n    //---- Create a mapped Observable \n     auto ints = rxcpp::observable<>::range(1,3). \n                 map( [] ( int n  ) {return n*n; }); \n     //---- Apply the tap operator...The Operator  \n     //---- will act as a filter/debug operator \n     auto values = ints.tap( \n          [](int v)  {printf(\"Tap -       OnNext: %dn\", v);}, \n          [](){printf(\"Tap -       OnCompletedn\"); \n     }); \n     //------- Do some action \n     values.subscribe( \n          [](int v){printf(\"Subscribe - OnNext: %dn\", v);}, \n          [](){printf(\"Subscribe - OnCompletedn\");}); \n } \n```\n\n现在，我们来看看`defer`运算符。`defer`运算符将一个可观察工厂作为参数，为每个订阅它的客户端创建一个可观察工厂。在下面的程序中，当有人试图连接到指定的可观测值时，我们调用`observable_factory`λ:\n\n```cpp\n//----------- DeferExample.cpp \n#include \"rxcpp/rx.hpp\" \n#include \"rxcpp/rx-test.hpp\" \n#include <ioStream> \nint main() { \n    auto observable_factory = [](){ \n         return rxcpp::observable<>::range(1,3). \n                 map( [] ( int n  ) {return n*n; }); \n    }; \n    auto ints = rxcpp::observable<>::defer(observable_factory); \n    ints.subscribe([](int v){printf(\"OnNext: %dn\", v);}, \n            [](){printf(\"OnCompletedn\");}); \n    ints.subscribe( \n            [](int v){printf(\"2nd OnNext: %dn\", v);}, \n            [](){printf(\"2nd OnCompletedn\");}); \n} \n```\n\n`buffer`运算符发出一个可观测值，该值包含一个可观测值的非重叠内容，每个可观测值最多包含计数参数指定的项目数。这将帮助我们以适合内容的方式处理项目:\n\n```cpp\n//----------- BufferExample.cpp \n#include \"rxcpp/rx.hpp\" \n#include \"rxcpp/rx-test.hpp\" \n#include <ioStream> \nint main() { \n   auto values = rxcpp::observable<>::range(1, 10).buffer(2); \n   values.subscribe( [](std::vector<int> v){ \n                printf(\"OnNext:{\"); \n                std::for_each(v.begin(), v.end(), [](int a){ \n                    printf(\" %d\", a); \n                }); \n                printf(\"}n\"); \n            }, \n            [](){printf(\"OnCompletedn\");}); \n} \n```\n\n`timer`运算符发出一个以间隔周期为参数的可观测值。可以选择将`Scheduler`对象指定为参数。库中有这个函数的各种版本；我们在下面的代码中显示了一个:\n\n```cpp\n//----------- TimerExample.cpp \n#include \"rxcpp/rx.hpp\" \n#include \"rxcpp/rx-test.hpp\" \n#include <ioStream> \nint main() { \n     auto Scheduler = rxcpp::observe_on_new_thread(); \n     auto period = std::chrono::milliseconds(1); \n     auto values = rxcpp::observable<>::timer(period, Scheduler). \n            finally([](){ \n            printf(\"The final actionn\"); \n        });     \n      values.as_blocking().subscribe( \n         [](int v){printf(\"OnNext: %dn\", v);}, \n         [](){printf(\"OnCompletedn\");}); \n} \n```\n\n# 对我们尚未涉及的事物的一瞥\n\nRx 编程模型可视为以下因素的融合:\n\n*   数据流计算\n*   声明式并发\n*   函数式编程\n*   流处理(事件)\n*   设计模式和习惯用语\n\n为了全面了解整个学科，您需要广泛地使用编程模型。最初，事情不会有太大意义。在某个时刻，你会到达一个*点击点*，在那里一切都将变得有意义。到目前为止，我们已经讨论了以下主题:\n\n*   可观察物和观察者\n*   基本和中间操作符\n*   基本和中间调度\n\n这只是一个开始，我们需要覆盖更多的主题来熟悉编程模型。它们是:\n\n*   冷热观察([第 9 章](09.html#4U9TC0-51c8384cc2cb48e691b461190723b468)、*使用 Qt/C++* 的反应式图形用户界面编程)\n*   Rx 组件的详细探索([第 9 章](https://cdp.packtpub.com/c___reactive_programming/wp-admin/post.php?post=79&action=edit#post_86)，*使用 Qt/C++* 的反应式图形用户界面编程)\n*   高级调度([第 9 章](https://cdp.packtpub.com/c___reactive_programming/wp-admin/post.php?post=79&action=edit#post_86)，*使用 Qt/C++ 的反应式图形用户界面编程)*\n*   使用 Qt/C++ 编程图形用户界面系统([第 9 章](https://cdp.packtpub.com/c___reactive_programming/wp-admin/post.php?post=79&action=edit#post_86)、*反应式图形用户界面编程)*\n*   高级操作符([第 9 章](https://cdp.packtpub.com/c___reactive_programming/wp-admin/post.php?post=79&action=edit#post_86)、*使用 Qt/C++* 的反应式图形用户界面编程)\n*   反应式设计模式([第 10 章](10.html#5GDO20-51c8384cc2cb48e691b461190723b468)、*c++ Rx 编程的设计模式和习惯用法*\n*   鲁棒性编程([第 12 章](12.html#6FSQK0-51c8384cc2cb48e691b461190723b468)、*高级流和处理错误*)\n\n# 摘要\n\n在这一章中，我们讨论了理解 Rx 编程模型的一些基础知识，特别是 RxCpp。我们从数据流计算范式的概念概述开始，然后很快开始编写一些基本的 RxCpp 程序。在介绍了 Rx 大理石图之后，我们了解了 RxCpp 库支持的一组运算符。我们还介绍了调度器的重要主题，最后讨论了`flat`地图和`concat`地图操作符的区别。在下一章中，我们将讨论`hot`和`cold`可观测值、高级调度以及本章中未涉及的主题。"
  },
  {
    "path": "docs/cpp-react-prog/08.md",
    "content": "# 八、关键要素\n\n在前一章中，我们介绍了 RxCpp 库及其编程模型。我们编写了一些程序来了解图书馆的运作。我们还介绍了 RxCpp 库的基本元素。在这一章中，我们将深入讨论 RxCpp 库的关键元素以及反应式编程模型，包括以下内容:\n\n*   看得见的\n*   观察者及其变体(订阅者)\n*   学科\n*   调度程序\n*   经营者\n\n实际上，反应式编程的关键方面如下:\n\n*   观察点是观察者可以订阅通知的流\n*   主体是可观察物和观察者的组合\n*   `schedulers`执行与操作员相关联的操作，并帮助数据从观察点流向观察者\n*   运算符是接受一个可观测值并发出另一个可观测值的函数\n\n# 看得见的\n\n在前一章中，我们从头开始创建了 Observables，并为这些 Observables 编写了订阅者。在我们所有的例子中，Observables 创建了一个`Producer`类的实例。`Producer`类产生一个事件流。换句话说，可观察对象是连接用户和生产者的功能。在我们继续之前，让我们剖析一个可观察的对象和与之相关的核心活动:\n\n*   可观测值是以观察者为参数并返回一个函数的函数\n*   可观察对象将观察者连接到生产者(生产者对于观察者是不透明的)\n*   生产者是可观察的价值来源\n*   观察者是具有`on_next`、`on_error`和`on_completed`方法的物体\n\n# 制片人是什么？\n\n生产者是可观察的价值来源。生产者可以是窗口、定时器、网络套接字、DOM 树、集合/容器上的迭代器等等。它们可以是任何可以传递给观察者的数据源。下一个(值) (在`RxCpp`、`observer.on_next(value)`中。)\n\n# 冷热观察\n\n在前一章的大多数例子中，我们看到生产者是在可观察函数中创建的。生产者可以在可观察函数外部创建，对生产者的引用可以放在可观察函数内部。引用其内部生产者实例的可观察对象称为热可观察对象。我们在里面创造了一个生产者的任何可观察的东西被称为冷的可观察的。为了说明问题，让我们编写一个程序来演示一个冷的可观察的:\n\n```cpp\n//---------- ColdObservable.cpp \n#include <rxcpp/rx.hpp> \n#include <memory> \nint main(int argc, char *argv[])  \n{\n```\n\n```cpp\n //----------- Get a Coordination \n auto eventloop = rxcpp::observe_on_event_loop(); \n //----- Create a Cold Observable \n auto values = rxcpp::observable<>::interval( \n               std::chrono::seconds(2)).take(2);   \n```\n\n间隔创建了一个冷的可观察值，因为事件流的生产者是由`interval`函数实例化的。当订阅或观察者附加到可观察对象时，冷可观察对象将发出数据。即使订阅有延迟，结果也是一致的。这意味着我们将获得所有可观测到的数据:\n\n```cpp\n //----- Subscribe Twice \n values.subscribe_on(eventloop). \n    subscribe([](int v){printf(\"[1] onNext: %dn\", v);}, \n        [](){printf(\"[1] onCompletedn\");}); \n values.subscribe_on(eventloop). \n    subscribe([](int v){printf(\"[2] onNext: %dn\", v);}, \n        [](){printf(\"[2] onCompletedn\");}); \n  //---- make a blocking subscription to see the results \n values.as_blocking().subscribe(); \n //----------- Wait for Two Seconds \n rxcpp::observable<>::timer(std::chrono::milliseconds(2000)). \n       subscribe([&](long){ }); \n} \n```\n\n程序发出的输出如下。对于每次运行，控制台中内容的顺序可能会改变，因为我们在同一线程中调度观察者方法的执行。不会因为订阅延迟而导致任何数据丢失:\n\n```cpp\n[1] onNext: 1 \n[2] onNext: 1 \n[2] onNext: 2 \n[1] onNext: 2 \n[2] onCompleted \n[1] onCompleted \n```\n\n# 可观察到的热\n\n我们可以通过调用可观测的`publish`方法，将冷的可观测转换为热的可观测。将冷的可观测值转换为热的可观测值的后果是数据会被以后的订阅遗漏。无论是否有订阅，热点可观察都会发出数据。以下程序演示了这一点:\n\n```cpp\n//---------- HotObservable.cpp\n\n#include <rxcpp/rx.hpp> \n#include <memory> \nint main(int argc, char *argv[]) { \n auto eventloop = rxcpp::observe_on_event_loop(); \n //----- Create a Cold Observable \n //----- Convert Cold Observable to Hot Observable  \n //----- using .Publish(); \n auto values = rxcpp::observable<>::interval( \n               std::chrono::seconds(2)).take(2).publish();   \n //----- Subscribe Twice \n values. \n    subscribe_on(eventloop). \n    subscribe( \n        [](int v){printf(\"[1] onNext: %dn\", v);}, \n        [](){printf(\"[1] onCompletedn\");}); \n  values. \n    subscribe_on(eventloop). \n    subscribe( \n        [](int v){printf(\"[2] onNext: %dn\", v);}, \n        [](){printf(\"[2] onCompletedn\");}); \n //------ Connect to Start Emitting Values \n values.connect(); \n //---- make a blocking subscription to see the results \n values.as_blocking().subscribe(); \n //----------- Wait for Two Seconds \n rxcpp::observable<>::timer( \n       std::chrono::milliseconds(2000)). \n       subscribe([&](long){ }); \n} \n```\n\n在下一个例子中，我们将看看`RxCpp`支持的`publish_synchronized`机制。从编程接口的角度来看，这只是一个小小的改变。看看这个程序:\n\n```cpp\n//---------- HotObservable2.cpp \n#include <rxcpp/rx.hpp> \n#include <memory> \n\nint main(int argc, char *argv[]) { \n\n auto eventloop = rxcpp::observe_on_event_loop(); \n //----- Create a Cold Observable \n //----- Convert Cold Observable to Hot Observable  \n //----- using .publish_synchronized(); \n auto values = rxcpp::observable<>::interval( \n               std::chrono::seconds(2)). \n               take(5).publish_synchronized(eventloop);   \n //----- Subscribe Twice \n values. \n    subscribe( \n        [](int v){printf(\"[1] onNext: %dn\", v);}, \n        [](){printf(\"[1] onCompletedn\");}); \n\n values. \n    subscribe( \n        [](int v){printf(\"[2] onNext: %dn\", v);}, \n        [](){printf(\"[2] onCompletedn\");}); \n\n //------ Start Emitting Values \n values.connect(); \n //---- make a blocking subscription to see the results \n values.as_blocking().subscribe(); \n\n //----------- Wait for Two Seconds \n rxcpp::observable<>::timer( \n       std::chrono::milliseconds(2000)). \n       subscribe([&](long){ }); \n} \n```\n\n程序的输出如下。我们可以看到输出是很好地同步的，也就是说，输出是以正确的顺序打印的:\n\n```cpp\n[1] onNext: 1 \n[2] onNext: 1 \n[1] onNext: 2 \n[2] onNext: 2 \n[1] onNext: 3 \n[2] onNext: 3 \n[1] onNext: 4 \n[2] onNext: 4 \n[1] onNext: 5 \n[2] onNext: 5 \n[1] onCompleted \n[2] onCompleted\n```\n\n# 热点观察和重放机制\n\n无论是否有用户，一个热的可观察对象都会发出数据。这有时会成为一个问题。反应式编程中有一种缓存数据的机制，这样以后的用户就可以通过一个可观察的。我们可以使用`.replay()`方法来创建这样一个可观察的。让我们编写一个程序来演示重放机制，这在编写热 Observables 时非常有用:\n\n```cpp\n//---------- ReplayAll.cpp \n#include <rxcpp/rx.hpp> \n#include <memory> \nint main(int argc, char *argv[]) { \n\n  auto values = rxcpp::observable<>::interval( \n                std::chrono::milliseconds(50),  \n                rxcpp::observe_on_new_thread()). \n                take(5).replay(); \n    // Subscribe from the beginning \n    values.subscribe( \n        [](long v){printf(\"[1] OnNext: %ldn\", v);}, \n        [](){printf(\"[1] OnCompletedn\");}); \n    // Start emitting \n    values.connect(); \n    // Wait before subscribing \n    rxcpp::observable<>::timer( \n         std::chrono::milliseconds(125)).subscribe([&](long){ \n        values.as_blocking().subscribe( \n            [](long v){printf(\"[2] OnNext: %ldn\", v);}, \n            [](){printf(\"[2] OnCompletedn\");}); \n    }); \n //----------- Wait for Two Seconds \n rxcpp::observable<>::timer( \n       std::chrono::milliseconds(2000)). \n       subscribe([&](long){ }); \n\n} \n```\n\n在编写反应式程序时，您真的需要理解热观察值和冷观察值之间的语义差异。我们只谈到了其中的一些方面。请参考 RxCpp 文档和 ReactiveX 文档了解更多信息。网上有无数关于这个话题的文章。\n\n# 观察者及其变体(订阅者)\n\n观察者订阅一个可观察对象，等待事件被通知。上一章已经谈到了观察员。因此，我们将重点关注订阅者，它是观察者和订阅者的组合。订户可以取消订阅。用普通的观察者，你只能订阅。以下程序将很好地解释这些概念:\n\n```cpp\n//---- Subscriber.cpp \n#include \"rxcpp/rx.hpp\" \nint main() { \n     //----- create a subscription object \n     auto subscription = rxcpp::composite_subscription(); \n     //----- Create a Subscription  \n     auto subscriber = rxcpp::make_subscriber<int>( \n        subscription, \n        [&](int v){ \n            printf(\"OnNext: --%dn\", v); \n            if (v == 3) \n                subscription.unsubscribe(); // Demonstrates Un Subscribes \n        }, \n        [](){ printf(\"OnCompletedn\");}); \n\n    rxcpp::observable<>::create<int>( \n        [](rxcpp::subscriber<int> s){ \n            for (int i = 0; i < 5; ++ i) { \n                if (!s.is_subscribed())  \n                    break; \n                s.on_next(i); \n           } \n            s.on_completed();   \n    }).subscribe(subscriber); \n    return 0; \n} \n```\n\n对于编写具有并发性和动态性的非平凡程序，订阅和取消订阅的能力非常方便。通过查阅 RxCpp 文档，深入了解该主题。\n\n# 学科\n\n主体是同时既是观察者又是可观察的实体。它有助于将通知从一个可观察对象传递给一组观察对象。我们可以实现复杂的技术，比如数据的缓存和缓冲。我们也可以用一个主语把热的可观察转换成冷的可观察。`RxCpp.`中实现了四种主体变体，如下所示:\n\n*   `SimpleSubject`\n*   `BehaviorSubject`\n*   `ReplaySubject`\n*   `SynchronizeSubject`\n\n让我们编写一个简单的程序，它将作为观察者订阅数据，并作为一对订阅者的可观察对象:\n\n```cpp\n//------- SimpleSubject.cpp \n#include <rxcpp/rx.hpp> \n#include <memory> \nint main(int argc, char *argv[]) { \n    //----- Create an instance of Subject \n    rxcpp::subjects::subject<int> subject; \n    //----- Retreive the Observable  \n    //----- attached to the Subject \n    auto observable = subject.get_observable(); \n    //------ Subscribe Twice \n    observable.subscribe( [] ( int v ) { printf(\"1------%dn\",v ); }); \n    observable.subscribe( [] ( int v ) { printf(\"2------%dn\",v );}); \n    //--------- Get the Subscriber Interface \n    //--------- Attached to the Subject \n    auto subscriber = subject.get_subscriber(); \n    //----------------- Emit Series of Values \n    subscriber.on_next(1); \n    subscriber.on_next(4); \n    subscriber.on_next(9); \n    subscriber.on_next(16); \n    //----------- Wait for Two Seconds \n    rxcpp::observable<>::timer(std::chrono::milliseconds(2000)). \n       subscribe([&](long){ }); \n}\n```\n\n`BehaviorSubject`是 subject 的一个变体，作为实现的一部分存储最后发出的(当前)值。任何新用户将立即获得*当前值*。否则，它的行为就像一个正常的主体。`BehaviorSubject`也称为属性或单元格。在我们用一系列数据更新特定单元或内存的情况下，例如在事务中，它非常有用。让我们编写一个程序来演示`BehaviorSubject`的工作原理:\n\n```cpp\n//-------- BehaviorSubject.cpp \n#include <rxcpp/rx.hpp> \n#include <memory> \n\nint main(int argc, char *argv[]) { \n\n    rxcpp::subjects::behavior<int> behsubject(0); \n\n    auto observable = behsubject.get_observable(); \n    observable.subscribe( [] ( int v ) { \n        printf(\"1------%dn\",v ); \n     }); \n\n      observable.subscribe( [] ( int v ) { \n        printf(\"2------%dn\",v ); \n     }); \n\n    auto subscriber = behsubject.get_subscriber(); \n    subscriber.on_next(1); \n    subscriber.on_next(2); \n\n    int n = behsubject.get_value(); \n\n    printf (\"Last Value ....%dn\",n); \n\n} \n```\n\n`ReplaySubject`是存储已经发出的数据的主体的变体。我们可以指定参数来指示主题必须保留多少值。这在处理热门的 Observables 时非常方便。各种重放重载的原型如下:\n\n```cpp\nreplay (Coordination cn,[optional] composite_subscription cs) \nreplay (std::size_t count, Coordination cn, [optional]composite_subscription cs) \nreplay (duration period, Coordination cn, [optional] composite_subscription cs) \nreplay (std::size_t count, duration period, Coordination cn,[optional] composite_subscription cs).\n```\n\n我们写个程序看看`ReplaySubject`的语义:\n\n```cpp\n//------------- ReplaySubject.cpp \n#include <rxcpp/rx.hpp> \n#include <memory> \nint main(int argc, char *argv[]) { \n    //----------- instantiate a ReplaySubject \n    rxcpp::subjects::replay<int,rxcpp::observe_on_one_worker>       \n           replay_subject(10,rxcpp::observe_on_new_thread()); \n    //---------- get the observable interface \n    auto observable = replay_subject.get_observable(); \n    //---------- Subscribe! \n    observable.subscribe( [] ( int v ) {printf(\"1------%dn\",v );}); \n    //--------- get the subscriber interface \n    auto subscriber = replay_subject.get_subscriber(); \n    //---------- Emit data  \n    subscriber.on_next(1); \n    subscriber.on_next(2); \n    //-------- Add a new subscriber \n    //-------- A normal subject will drop data \n    //-------- Replay subject will not \n    observable.subscribe( [] ( int v ) {  printf(\"2------%dn\",v );}); \n     //----------- Wait for Two Seconds \n    rxcpp::observable<>::timer( \n       std::chrono::milliseconds(2000)). \n       subscribe([&](long){ }); \n} \n```\n\n在这一节中，我们已经讨论了一个主题的三种变体。主要的用例是通过使用可观察的接口来利用来自不同来源的事件和数据，并允许一组订阅者使用利用的数据。`SimpleSubject`既可以作为可观测者，也可以作为观察者来处理数据流。`BehaviorSubject`用于监控一段时间内属性或变量的变化。`ReplaySubject`将帮助您避免因订阅延迟而导致的数据丢失。`SynchronizeSubject`是一个在其实现中内置同步逻辑的主题。\n\n# 调度程序\n\nRxCpp 库提供了一个声明式线程机制，这要归功于与之打包的健壮的调度子系统。从可观察的角度来看，数据可以沿着变更传播图通过不同的路径流动。通过向流处理管道给出提示，我们可以在不同的线程、同一个线程或一个后台线程中调度执行。这有助于更好地抓住程序员的意图。\n\nRxCpp 中的声明性调度模型是可能的，因为在操作符的实现中流是不变性的。流操作符将一个可观测值作为参数，并返回一个新的可观测值作为结果。输入参数未被修改。这有助于无序执行。RxCpp 的调度子系统包含以下结构:\n\n*   调度程序\n*   工人\n*   协调\n*   协调者\n*   可调度的\n*   时间表\n\nRxCpp 的版本 2 借鉴了`RxJava`系统的调度架构。它依赖于`RxJava`使用的调度器和工作器习惯用法。以下是关于调度程序的一些重要事实:\n\n*   调度程序有一个时间表。\n*   调度程序可以在时间线中创建许多工作人员。\n*   工作人员在时间线中拥有一个可调度的队列。\n*   `schedulable`拥有一个功能(称为`Action`)，并且有寿命。\n*   一个`Coordination`作为协调器的工厂，并有一个调度器。\n*   每个协调员都有一名工人，是以下工作的工厂:\n    *   协调`schedulable`\n    *   协调的观察点和用户\n\n我们一直在我们的程序中使用 Rx `schedulers`，没有考虑它们是如何在引擎盖下工作的。让我们写一个玩具程序，这将有助于我们理解调度是如何工作的:\n\n```cpp\n//------------- SchedulerOne.cpp \n#include \"rxcpp/rx.hpp\" \nint main(){ \n    //---------- Get a Coordination  \n    auto Coordination function= rxcpp::serialize_new_thread(); \n    //------- Create a Worker instance  through a factory method  \n    auto worker = coordination.create_coordinator().get_worker(); \n    //--------- Create a action object \n    auto sub_action = rxcpp::schedulers::make_action( \n         [] (const rxcpp::schedulers::schedulable&) {   \n          printf(\"Action Executed in Thread # : %dn\",  \n          std::this_thread::get_id());   \n          } );  \n    //------------- Create a schedulable and schedule the action \n    auto scheduled = rxcpp::schedulers::make_schedulable(worker,sub_action); \n    scheduled.schedule(); \n    return 0; \n} \n```\n\n在`RxCpp`中，所有以多个流为输入，或处理与时间有关系的任务的操作员，都以一个`Coordination`函数为参数。使用特定调度程序的一些`Coordination`功能如下:\n\n*   `identity_immediate()`\n*   `identity_current_thread()`\n*   `identity_same_worker(worker w)`\n*   `serialize_event_loop()`\n*   `serialize_new_thread()`\n*   `serialize_same_worker(worker w)`\n*   `observe_on_event_loop()`\n*   `observe_on_new_thread()`\n\n在前面的程序中，我们手动安排了一个动作(事实上，它只是一个 Lambda)。让我们继续讨论调度程序的声明性方面。我们将编写一个程序，使用`Coordination`函数安排任务:\n\n```cpp\n//----------- SchedulerTwo.cpp \n#include \"rxcpp/rx.hpp\" \nint main(){ \n    //-------- Create a Coordination function \n    auto Coordination function= rxcpp::identity_current_thread(); \n    //-------- Instantiate a coordinator and create a worker     \n    auto worker = coordination.create_coordinator().get_worker(); \n    //--------- start and the period \n    auto start = coordination.now() + std::chrono::milliseconds(1); \n    auto period = std::chrono::milliseconds(1);      \n    //----------- Create an Observable (Replay ) \n    auto values = rxcpp::observable<>::interval(start,period). \n    take(5).replay(2, coordination); \n    //--------------- Subscribe first time using a Worker \n    worker.schedule([&](const rxcpp::schedulers::schedulable&){ \n       values.subscribe( [](long v){ printf(\"#1 -- %d : %ldn\",  \n                   std::this_thread::get_id(),v);  }, \n                        [](){ printf(\"#1 --- OnCompletedn\");}); \n    }); \n    worker.schedule([&](const rxcpp::schedulers::schedulable&){ \n      values.subscribe( [](long v){printf(\"#2 -- %d : %ldn\",  \n                   std::this_thread::get_id(),v); }, \n                     [](){printf(\"#2 --- OnCompletedn\");});  \n    }); \n    //----- Start the emission of values  \n   worker.schedule([&](const rxcpp::schedulers::schedulable&) \n   { values.connect();}); \n   //------- Add blocking subscription to see results \n   values.as_blocking().subscribe(); return 0; \n} \n```\n\n我们使用重放机制创建了一个热观察器来处理一些观察器的延迟订阅。我们还创建了一个 Worker 来调度订阅，并将观察者与可观察对象连接起来。上一个程序演示了调度程序如何在`RxCpp`中工作。\n\n# 观察与订阅\n\n`ObserveOn`和`SubscribeOn`操作符的行为方式不同，这一直是反应式编程新手的困惑之源。`ObserveOn`操作符改变其下方的操作符和观察者的线程。在`SubscribeOn`的情况下，它也影响上面和下面的操作者和方法。下面的程序演示了由`SubscribeOn`和`ObserveOn`操作员行为方式引起的行为的细微变化。让我们编写一个使用`ObserveOn`运算符的程序:\n\n```cpp\n//-------- ObservableOnScheduler.cpp \n#include \"rxcpp/rx.hpp\" \nint main(){ \n    //------- Print the main thread id \n    printf(\"Main Thread Id is %dn\",  \n             std::this_thread::get_id()); \n    //-------- We are using observe_on here \n    //-------- The Map will use the main thread \n    //-------- Subscribed Lambda will use a new thread \n    rxcpp::observable<>::range(0,15). \n        map([](int i){ \n            printf(\"Map %d : %dn\", std::this_thread::get_id(),i);  \n            return i; }). \n        take(5).observe_on(rxcpp::synchronize_new_thread()). \n        subscribe([&](int i){ \n           printf(\"Subs %d : %dn\", std::this_thread::get_id(),i);  \n        }); \n    //----------- Wait for Two Seconds \n    rxcpp::observable<>::timer( \n       std::chrono::milliseconds(2000)). \n       subscribe([&](long){ }); \n\n    return 0; \n} \n```\n\n前面程序的输出如下:\n\n```cpp\nMain Thread Id is 1 \nMap 1 : 0 \nMap 1 : 1 \nSubs 2 : 0 \nMap 1 : 2 \nSubs 2 : 1 \nMap 1 : 3 \nSubs 2 : 2 \nMap 1 : 4 \nSubs 2 : 3 \nSubs 2 : 4 \n```\n\n前面程序的输出清楚地显示 map 在主线程中工作，而`subscribe`方法在辅助线程中被调度。这清楚地表明`ObserveOn`只对其下的运营商和用户起作用。让我们编写一个或多或少相同的程序，使用`SubscribeOn`运算符而不是`ObserveOn`运算符。看看这个:\n\n```cpp\n//-------- SubscribeOnScheduler.cpp \n#include \"rxcpp/rx.hpp\" \nint main(){ \n    //------- Print the main thread id \n    printf(\"Main Thread Id is %dn\",  \n             std::this_thread::get_id()); \n    //-------- We are using subscribe_on here \n    //-------- The Map and subscribed Lambda will  \n    //--------- use the secondary thread \n    rxcpp::observable<>::range(0,15). \n        map([](int i){ \n            printf(\"Map %d : %dn\", std::this_thread::get_id(),i);  \n            return i; \n        }). \n        take(5).subscribe_on(rxcpp::synchronize_new_thread()). \n        subscribe([&](int i){ \n           printf(\"Subs %d : %dn\", std::this_thread::get_id(),i);  \n        }); \n    //----------- Wait for Two Seconds \n    rxcpp::observable<>::timer( \n       std::chrono::milliseconds(2000)). \n       subscribe([&](long){ }); \n\n    return 0; \n} \n```\n\n前面程序的输出如下:\n\n```cpp\nMain Thread Id is 1 \nMap 2 : 0 \nSubs 2 : 0 \nMap 2 : 1 \nSubs 2 : 1 \nMap 2 : 2 \nSubs 2 : 2 \nMap 2 : 3 \nSubs 2 : 3 \nMap 2 : 4 \nSubs 2 : 4 \n```\n\n前面程序的输出显示 map 和 subscription 方法都在辅助线程中工作。这清楚地表明`SubscribeOn`改变了前后项的线程行为。\n\n# 运行循环调度程序\n\nRxCpp 库没有内置的主线程调度器。你能做的最接近的就是利用`run_loop`类来模拟主线程中的调度。在下面的程序中，可观察对象在后台线程中执行，订阅方法在主线程中运行。我们使用`subscribe_on`和`observe_on`来实现这一目标:\n\n```cpp\n//------------- RunLoop.cpp \n#include \"rxcpp/rx.hpp\" \nint main(){ \n    //------------ Print the Main Thread Id \n    printf(\"Main Thread Id is %dn\",  \n                std::this_thread::get_id()); \n    //------- Instantiate a run_loop object \n    //------- which will loop in the main thread \n    rxcpp::schedulers::run_loop rlp; \n    //------ Create a Coordination functionfor run loop \n    auto main_thread = rxcpp::observe_on_run_loop(rlp); \n    auto worker_thread = rxcpp::synchronize_new_thread(); \n    rxcpp::composite_subscription scr; \n    rxcpp::observable<>::range(0,15). \n        map([](int i){ \n            //----- This will get executed in worker \n            printf(\"Map %d : %dn\", std::this_thread::get_id(),i);  \n            return i; \n        }).take(5).subscribe_on(worker_thread). \n        observe_on(main_thread). \n        subscribe(scr, [&](int i){ \n            //--- This will get executed in main thread \n            printf(\"Sub %d : %dn\", std::this_thread::get_id(),i); }); \n    //------------ Execute the Run Loop \n    while (scr.is_subscribed() || !rlp.empty()) { \n        while (!rlp.empty() && rlp.peek().when < rlp.now()) \n        { rlp.dispatch();} \n    }  \n    return 0; \n} \n```\n\n前面程序的输出如下:\n\n```cpp\nMain Thread Id is 1 \nMap 2 : 0 \nMap 2 : 1 \nSub 1 : 0 \nSub 1 : 1 \nMap 2 : 2 \nMap 2 : 3 \nSub 1 : 2 \nMap 2 : 4 \nSub 1 : 3 \nSub 1 : 4 \n```\n\n我们可以看到，映射是在工作线程中调度的，订阅方法是在主线程中执行的。这是因为`Subscribe_on`和`Observe_on`方法的合理放置，我们在前面的章节中已经介绍过了。\n\n# 经营者\n\n算子是作用于可观测值以产生新的可观测值的函数。在这个过程中，最初的可观察对象不是突变的，而是一个纯粹的函数。在我们编写的示例程序中，我们已经介绍了许多运算符。在[第 9 章](https://cdp.packtpub.com/c___reactive_programming/wp-admin/post.php?post=79&action=edit#post_86)、*使用 Qt/C++* 的反应式 GUI 编程中，我们将学习如何创建自定义操作符来处理 Observables。一个操作符不变异一个可观察的事实是声明式调度在 Rx 编程模型中工作的一个原因。Rx 运营商可分为以下几类:\n\n*   创建运算符\n*   转换运算符\n*   过滤运算符\n*   组合运算符\n*   错误处理运算符\n*   公用事业运营商\n*   布尔运算符\n*   数学运算符\n\n有一些操作符不属于这些类别。我们将在一个表格中概述上述类别中的一些关键运算符，以供快速参考。\n\n# 创造算子\n\n这些操作符将帮助人们从输入数据中创建各种各样的可观测值。我们已经在示例代码中演示了 create、from、interval 和 range 的用法。参考这些示例和 RxCpp 文档，了解更多相关信息。包含如下一些运算符的表:\n\n| 观察到 | **描述** |\n| `create` | 通过以编程方式调用观察者方法来创建可观察对象 |\n| `defer` | 为每个观察者/订阅者创建一个新的可观测值 |\n| `empty` | 创建一个不发射任何东西(只发射完成的)的可观测值 |\n| `from` | 基于参数创建可观察值(多态) |\n| `interval` | 创建一个在时间间隔内发出一系列值的可观测值 |\n| `just` | 创建发出单个值的可观测值 |\n| `range` | 创建发出一系列值的可观察值 |\n| `never` | 创造一个永远不会发射任何东西的可观察物体 |\n| `repeat` | 创建重复值流的可观察值 |\n| `timer` | 创建一个在指定为参数的延迟后发出值的可观测值 |\n| `throw` | 创建发出错误的可观测值 |\n\n# 转换运算符\n\n这些操作符帮助用户创建一个新的可观测值，而无需修改源可观测值。它们通过应用一个 Lambda 对源可观察中的单个项目进行操作。包含一些最有用的转换运算符的表如下:\n\n| 观察到 | **描述** |\n| `buffer` | 收集过去的值并在发出信号时发出的可观察值 |\n| `flat_map` | 发出将函数应用于由源可观测值和集合可观测值发出的一对值的结果的可观测值 |\n| `group_by` | 有助于对可观察值进行分组的可观察值 |\n| `map` | 可观察的，从源可观察的发出项目，由指定的函数转换 |\n| `scan` | 发出对累加器函数的每次调用结果的可观察值 |\n| `window` | 发出连接的非重叠窗口的可观察对象，每个窗口最多包含来自源可观察对象的计数项 |\n\n# 过滤运算符\n\n过滤流的能力是流处理中的常见活动。Rx 编程模型定义了很多这样的运算符，这并不罕见。过滤运算符主要是谓词函数或 Lambdas。下表包含筛选运算符列表:\n\n| 观察到 | **描述** |\n| `debounce` | 如果特定的时间跨度已经过去，而没有从源发出另一个项目，则发出一个项目的可观察 |\n| `distinct` | 从源发出那些项目的可观察的 |\n| `element_at` | 发出位于指定索引位置的项的可观察对象 |\n| `filter` | 仅发出源发出的项目的可观察值过滤器评估为真的可观察值 |\n| `first` | 仅发出源发出的第一个项目的可观察 |\n| `ignore_eleements` | 从源发出终止通知的可观察 |\n| `last` | 仅发出源发出的最后一个项目的可观察 |\n| `sample` | 可观测的，发出由源发出的最近的项目可观测的在周期时间间隔内 |\n| `skip` | 与源可观测值相同的可观测值，除了它不发出源可观测值发出的第一个 t 项 |\n| `skip_last` | 与源可观测值相同的可观测值，除了它不发出源可观测值发出的最后 t 个项目 |\n| `take` | 仅发出源可观察对象发出的前 t 个项目的可观察对象，或者如果该可观察对象发出的项目少于 t 个，则发出源可观察对象的所有项目 |\n| `take_last` | 仅发出源发出的最后 t 个项目的可观测值 |\n\n# 组合运算符\n\nRx 编程模型的主要目标之一是将事件源与事件接收器分离。显然，需要能够组合来自各种来源的流的运营商。RxCpp 库实现了一组这样的操作符。下表概述了一组常用的组合运算符:\n\n| 观察到 | **描述** |\n| `combine_latest` | 当一个项目由两个可观察对象中的任何一个发出时，通过指定的函数组合每个可观察对象发出的最新项目，并基于该函数的结果发出项目 |\n| `merge` | 这通过合并它们的排放将多个可观测值合并成一个 |\n| `start_with` | 这将在开始从源可观察对象发出项目之前发出指定的项目序列 |\n| `switch_on_next` | 这将发出可观测值的可观测值转换为发出最近发出的可观测值的单个可观测值 |\n| `zip` | 这通过一个指定的函数将多个可观测值的发射组合在一起，并基于该函数的结果为每个组合发射单个项目 |\n\n# 错误处理运算符\n\n这些操作符有助于从可观察到的错误通知中恢复。看看这张桌子:\n\n| 观察到 | **描述** |\n| `Catch` | 不支持`RxCpp` |\n| `retry` | 一个反映源可观测值的可观测值，如果调用`on_error`达到指定的重试次数，则重新订阅该可观测值 |\n\n# 可观察效用算子\n\n下面是一个有用的操作工具箱，用于操作可观察对象:\n\n| 观察到 | **描述** |\n| `finally` | 发出与源可观察对象相同项目的可观察对象，然后调用给定的操作 |\n| `observe_on` | 指定观察者将在其上观察该可观察对象的调度程序 |\n| `subscribe` | 对可观察到的排放和通知进行操作 |\n| `subscribe_on` | 指定订阅时可观察对象应该使用的调度程序 |\n| `scope` | 创建与可观察资源具有相同生命周期的可支配资源 |\n\n# 条件运算符和布尔运算符\n\n以下是评估一个或多个可观测值或由可观测值发出的项目的运算符:\n\n| 观察到 | **描述** |\n| `all` | 如果源可观测值发出的每个项目都满足指定条件，则发出 true 的可观测值；否则，它会发出 false |\n| `amb` | 发出与源 Observables 中最先发出项目或发送终止通知的源 Observables 相同序列的 Observables |\n| `contains` | 如果源可观测值发出指定的项目，则发出 true 的可观测值；否则它会发出假的 |\n| `default_if_empty` | 如果源可观测值发出指定的项目，则发出 true 的可观测值；否则它会发出假的 |\n| `sequence_equal` | 仅当两个序列在以相同顺序发出相同的项目序列后正常终止时才发出 true 的可观察值；否则，它会发出假的 |\n| `skip_until` | 丢弃可观察对象发出的项目，直到第二个可观察对象发出项目 |\n| `skip_while` | 丢弃由可观察对象发出的项目，直到指定的条件变为假 |\n| `take_until` | 在第二个可观察对象发出项目或终止后，丢弃可观察对象发出的项目 |\n| `take_while` | 在指定条件变为假后，丢弃由可观察对象发出的项目 |\n\n# 数学和聚合运算符\n\n这些运算符对可观察到的发出的整个项目序列进行操作:\n\n| 观察到 | **描述** |\n| `average` | 计算可观测值发出的数字的平均值，并发出该平均值 |\n| `concat` | 从两个或两个以上的观察点发射辐射，而不交错它们 |\n| `count` | 计算源可观测值发出的项目数，并只发出该值 |\n| `max` | 确定并发出可观测值发出的最大值项 |\n| `Min` | 确定并发出可观测值发出的最小值项目 |\n| `reduce` | 对可观察对象发出的每个项目应用一个函数，依次发出最终值 |\n| `sum` | 计算可观测值发出的数字之和，并发出这个和 |\n\n# 可连接可观测算子\n\n可连接的可观察操作符是特殊的可观察操作符，具有更精确控制的订阅动态。下表列出了其中的一些:\n\n| 观察到 | **描述** |\n| `connect` | 指示可连接的可观察对象开始向其订户发射项目 |\n| `publish` | 将普通可观测值转换为可连接可观测值 |\n| `ref_count` | 让一个可连接的可观察对象像普通的可观察对象一样工作 |\n| `replay` | 确保所有观察者看到相同的发射项目序列，即使他们是在可观察对象开始发射项目后订阅的 |\n\n# 摘要\n\n在本章中，我们了解了 Rx 编程模型的各个部分是如何结合在一起的。我们从可观察对象开始，很快就进入了冷热可观察对象的话题。然后，我们介绍了订阅机制及其使用。然后，我们进入主题的重要主题，了解主题调度器实现的许多变体。最后，我们对 RxCpp 系统中可用的各种运营商进行了分类。在下一章中，我们将学习如何使用 Qt 框架以被动的方式使用这些知识编写图形用户界面程序。"
  },
  {
    "path": "docs/cpp-react-prog/09.md",
    "content": "# 九、Qt/C++ 反应式图形用户界面编程\n\nQt(发音为*可爱*)生态系统是一个全面的基于 C++ 的框架，用于编写跨平台和多平台的 GUI 应用。如果您使用库的可移植核心编写程序，您可以利用框架支持的*一次编写和随处编译*范例。在某些情况下，人们使用特定于平台的功能，例如支持编写基于 Windows 的应用的 ActiveX 编程模型。\n\n我们遇到过 Qt 比 MFC 更适合在 Windows 中编写应用的情况。一个看似合理的原因可能是易于编程，因为 Qt 使用 C++ 语言特性的一个非常小的子集作为它的库。当然，框架的最初目标是跨平台开发。Qt 跨平台的单一源代码可移植性、特性的丰富性、源代码的可用性以及更新的文档，使它成为一个非常程序员友好的框架。自 1995 年第一次发行以来，这帮助它繁荣了二十多年。\n\nQt 提供了一个完整的界面环境，支持开发多平台 GUI 应用、Webkit APIs、媒体流、文件系统浏览器、OpenGL APIs 等等。要涵盖这个奇妙图书馆的全部特征，需要一本自己的书。本章的目的是介绍如何利用 Qt 和 RxCpp 库编写反应式图形用户界面应用。我们已经在[第 7 章](07.html#3M85O0-51c8384cc2cb48e691b461190723b468)、*数据流计算和 RxCpp 库介绍*和[第 8 章](08.html#49AH00-51c8384cc2cb48e691b461190723b468)、*RxCPP–关键元素*中介绍了反应式编程模型的核心。现在，是时候将前几章所学付诸实践了！Qt 框架本身有一个健壮的事件处理系统，在他或她将 RxCpp 结构合并到组合中之前，需要学习这些库特性。\n\n在本章中，我们将探讨:\n\n*   Qt 图形用户界面编程快速入门\n*   你好世界–Qt 计划\n*   Qt 事件模型，包括信号/时隙/主运行中心——一个例子\n*   将 RxCpp 库与 Qt 事件模型集成\n*   在 Rxcpp 中创建自定义运算符\n\n# Qt 图形用户界面编程快速入门\n\nQt 是一个跨平台应用开发框架，用于编写软件，该软件可以作为本机应用在许多平台上运行，而无需更改太多代码，具有本机平台功能和速度。除了图形用户界面应用，我们还可以使用该框架编写控制台或命令行应用，但是主要的用例是图形用户界面。\n\n虽然使用 Qt 的应用通常是用 C++ 编写的，但是 QML 对其他语言的绑定也存在。Qt 使用全面而强大的 API 和工具简化了 C++ 开发的许多方面。Qt 支持很多编译器工具链，比如 GCC C++ 编译器和 Visual C++ 编译器。Qt 还提供了 Qt Quick(包括 QML，一种基于 ECMAScript 的声明性脚本语言)来编写逻辑。这有助于移动平台的快速应用开发，尽管可以使用本机代码编写逻辑以获得最佳性能。ECMAScript/C++ 组合提供了最好的声明式开发和本机代码速度。\n\nQt 目前正由 Qt 公司开发和维护，该框架可通过开源和专有许可证获得。第一次推出时，Qt 通过模仿不同平台的外观和感觉来使用自己的绘画引擎和控件(得益于定制的绘画引擎，人们可以在 GNU Linux 下创建 Windows 外观和感觉)。这有助于开发人员轻松地跨平台移植，因为对目标平台的依赖性最小。由于模拟不完善，Qt 开始为平台使用本机风格的 API，并有自己的本机小部件集。这通过模拟 Qt 自己的油漆引擎解决了这个问题，但代价是平台之间没有更一致的外观和感觉。Qt 库与 Python 编程语言有很好的绑定，命名为 PyQt。\n\n程序员在利用库之前，必须了解一些基本的东西。在接下来的部分中，我们将快速介绍 Qt 对象模型、信号和槽、事件系统和元对象系统的各个方面。\n\n# Qt 对象模型\n\n在图形用户界面框架中，运行时效率和高级别灵活性都是关键因素。标准的 C++ 对象模型提供了非常高效的运行时支持，但是它的静态特性在某些有问题的领域中是不灵活的。Qt 框架结合了 C++ 的速度和 Qt 对象模型的灵活性。\n\nQt 对象模型支持以下特性:\n\n*   **信号和插槽**，用于无缝对象通信\n*   可查询和可设计的**对象属性**\n*   强大的事件和事件过滤器\n*   强大的内部驱动计时器，能够在事件驱动的图形用户界面中流畅、无阻塞地完成许多任务\n*   **带上下文字符串翻译的国际化**\n*   被引用对象被销毁时自动设置为 0 的保护指针( **QPointers** )\n*   跨库边界工作的**动态转换**\n\n这些特性中的许多是作为标准的 C++ 类实现的，基于从`QObject`的继承。其他的，像信号和插槽以及对象属性系统，需要由 Qt 自己的**元对象编译器** ( **MOC** )提供的元对象系统。元对象系统是 C++ 语言的扩展，使其更适合于图形用户界面编程。主运行中心充当预编译器，它根据源代码中嵌入的提示生成代码，并为 ANSI C++ 编译器移除这些提示以执行其正常编译任务。\n\n让我们看看 Qt 对象模型中的一些类:\n\n| **类名** | **描述** |\n| `QObject` | 所有 Qt 对象的基类([http://doc.qt.io/archives/qt-4.8/qobject.html](http://doc.qt.io/archives/qt-4.8/qobject.html)) |\n| `QPointer` | 为`QObject`([http://doc.qt.io/archives/qt-4.8/qpointer.html](http://doc.qt.io/archives/qt-4.8/qpointer.html))提供保护指针的模板类 |\n| `QSignalMapper` | 将来自可识别发送方的信号打包([http://doc.qt.io/archives/qt-4.8/qsignalmapper.html](http://doc.qt.io/archives/qt-4.8/qsignalmapper.html)) |\n| `QVariant` | 就像最常见的 Qt 数据类型([http://doc.qt.io/archives/qt-4.8/qvariant.html](http://doc.qt.io/archives/qt-4.8/qvariant.html))的联合 |\n| `QMetaClassInfo` | 关于一个类的附加信息([http://doc.qt.io/archives/qt-4.8/qmetaclassinfo.html](http://doc.qt.io/archives/qt-4.8/qmetaclassinfo.html)) |\n| `QMetaEnum` | 关于枚举器的元数据([http://doc.qt.io/archives/qt-4.8/qmetaenum.html](http://doc.qt.io/archives/qt-4.8/qmetaenum.html)) |\n| `QMetaMethod` | 关于成员函数的元数据([http://doc.qt.io/archives/qt-4.8/qmetamethod.html](http://doc.qt.io/archives/qt-4.8/qmetamethod.html)) |\n| `QMetaObject` | 包含关于 Qt 对象的元信息([http://doc.qt.io/archives/qt-4.8/qmetaobject.html](http://doc.qt.io/archives/qt-4.8/qmetaobject.html)) |\n| `QMetaProperty` | 关于某个属性的元数据([http://doc.qt.io/archives/qt-4.8/qmetaproperty.html](http://doc.qt.io/archives/qt-4.8/qmetaproperty.html)) |\n| `QMetaType` | 管理元对象系统中的命名类型([http://doc.qt.io/archives/qt-4.8/qmetatype.html](http://doc.qt.io/archives/qt-4.8/qmetatype.html)) |\n| `QObjectCleanupHandler` | 观看多个`QObject`([http://doc.qt.io/archives/qt-4.8/qobjectcleanuphandler.html](http://doc.qt.io/archives/qt-4.8/qobjectcleanuphandler.html))的寿命 |\n\nQt 对象通常被视为身份，而不是值。身份是克隆的，不是复制或分配的；克隆身份比复制或赋值更复杂。因此，`QObject`和`QObject`的所有子类(直接或间接)都禁用了它们的复制构造函数和赋值运算符。\n\n# 信号和插槽\n\n信号和槽是 Qt 中用来实现对象间通信的机制。作为一个图形用户界面框架，信号和槽机制是 Qt 的核心特征。通过这种机制，小部件会得到 Qt 中其他小部件变化的通知。一般来说，任何类型的对象都使用这种机制相互通信。例如，当用户点击关闭按钮时，我们可能希望调用窗口的`close()`函数。\n\n信号和槽是 C/C++ 中回调技术的替代。特定事件发生时会发出信号。Qt 框架中的所有小部件都有预定义的信号，但是我们总是可以子类化一个小部件来添加我们自己的信号。插槽是响应信号而调用的函数。类似于预定义的信号，Qt 小部件有许多预定义的槽，但是我们可以添加自定义槽来处理我们感兴趣的信号。\n\nQt 官方文档([http://doc.qt.io/archives/qt-4.8/signalsandslots.html](http://doc.qt.io/archives/qt-4.8/signalsandslots.html))中的下图展示了对象间通信是如何通过信号和插槽进行的:\n\n![](img/00017.jpeg)\n\n信号和插槽是松散耦合的通信机制；发出信号的类不关心接收信号的插槽。信号是火灾和遗忘系统的完美例子。信号和插槽系统确保如果一个信号连接到一个插槽，该插槽将在正确的时间用信号参数调用。信号和槽都可以接受任意类型的任意数量的参数，并且它们是完全类型安全的。信号和接收时隙的签名必须匹配；因此，编译器可以帮助我们检测类型不匹配，这是一个好处。\n\n从`QObject`或其任何子类(如`QWidget`)继承的所有对象都可以包含信号和槽。一个物体改变状态时会发出信号，这可能会引起其他物体的兴趣。对象不知道(或不关心)接收端是否有任何对象。一个信号可以连接到所需数量的插槽。同样，我们可以将任意多的信号连接到一个插槽。甚至可以将一个信号连接到另一个信号；因此，信号链接是可能的。\n\n因此，信号和系统共同构成了一个极其灵活和可插拔的组件编程机制。\n\n# 事件系统\n\n在 Qt 中，事件表示应用或应用需要了解的用户活动中发生的事情。在 Qt 中，事件是从抽象的`QEvent`类派生的对象。事件可以由`QObject`子类的任何实例接收和处理，但是它们与小部件特别相关。\n\n每当一个事件发生时，一个适当的`QEvent`子类实例被构造出来，并通过调用其`event()`函数将其所有权赋予`QObject`的一个特定实例(或任何相关的子类)。此函数不处理事件本身；根据传递的事件类型，它调用该特定类型事件的事件处理程序，并根据事件是被接受还是被忽略来发送响应。\n\n有些事件，比如`QCloseEvent`、`QMoveEvent`，来自应用本身；有些，如`QMouseEvent`和`QKeyEvent`，来自窗户系统；还有一些，比如`QTimerEvent`，来自其他渠道。大多数事件都有从`QEvent`派生的特定子类，有时还有特定于事件的函数来满足扩展事件的特定行为。举例来说，`QMouseEvent`类添加了`x()`和`y()`功能，使小部件能够发现鼠标光标的位置。\n\n每个事件都有一个关联的类型，在`QEvent::Type`下定义，这是一个运行时类型信息的方便来源，用于快速识别事件是从哪个子类构造的。\n\n# 事件处理程序\n\n通常，事件是通过调用关联的虚函数来呈现的。虚拟功能负责按预期做出响应。如果自定义虚拟函数实现没有执行所有必需的操作，我们可能需要调用基类的实现。\n\n例如，以下示例处理自定义标签小部件上的鼠标左键单击，同时将所有其他按钮单击传递给基础`QLabel`类:\n\n```cpp\nvoid my_QLabel::mouseMoveEvent(QMouseEvent *evt)\n{\n    if (event->button() == Qt::LeftButton) {\n        // handle left mouse button here\n        qDebug() <<\" X: \" << evt->x() << \"t Y: \" << evt->y() << \"n\";\n    }\n    else {\n        // pass on other buttons to base class\n        QLabel::mouseMoveEvent(event);\n    }\n}\n```\n\n如果我们想替换基类功能，我们必须实现虚函数重写中的所有内容。如果需求是简单地扩展基类功能，我们可以实现我们想要的，并为我们不想处理的任何其他情况调用基类函数。\n\n# 发送事件\n\n许多使用 Qt 框架的应用想要发送自己的事件，就像框架提供的事件一样。通过使用事件对象并用`QCoreApplication::sendEvent()`和`QCoreApplication::postEvent()`发送，可以构建合适的自定义事件。\n\n`sendEvent()`执行同步；因此，它会立即处理该事件。对于很多事件类，有一个叫做`isAccepted()`的函数，它告诉我们事件是被最后一个被调用的处理程序接受还是拒绝。\n\n`postEvent()`执行异步；因此，它将事件发布到队列中，以备以后调度。下一次 Qt 的主事件循环运行时，它会调度所有发布的事件，并进行一些优化。例如，如果有几个调整大小事件，它们将被压缩为一个，作为所有调整大小事件的联合，这避免了用户界面中的闪烁。\n\n# 元对象系统\n\nQt 元对象系统实现了对象间通信的信号和槽机制、动态属性系统和运行时类型信息。\n\nQt 元对象系统基于三个关键方面:\n\n*   `QObject`类:为 Qt 对象提供元对象系统优势的基类\n*   `Q_OBJECT`宏:在类声明的私有部分提供的宏，用于启用元对象特性，如动态属性、信号和槽\n*   主运行中心:它为每个`QObject`子类提供必要的代码来实现元对象特性\n\n主运行中心在 Qt 源文件的实际编译之前执行。当主运行中心找到包含`Q_OBJECT`宏的类声明时，它为这些类中的每一个生成另一个带有元对象代码的 C++ 源文件。这个生成的源文件或者使用`#include`包含在类的源文件中，或者更常见的是，编译并与类的实现链接。\n\n# 你好世界–Qt 计划\n\n现在，让我们开始使用 Qt/C++ 开发图形用户界面应用。在进入以下部分之前，请从 Qt 的官方网站([https://www.qt.io/download](https://www.qt.io/download))下载 Qt SDK 和 Qt Creator。我们将在本章中讨论的代码完全与 LGPL 兼容，并将通过编写纯 C++ 代码进行手工编码。Qt 框架被设计得令人愉快和直观，这样您就可以在不使用 Qt Creator IDE 的情况下手工编写整个应用。\n\nQt Creator is a cross-platform C++, JavaScript, and QML integrated development environment, a part of the SDK for the Qt GUI application development framework. It includes a visual debugger and an integrated GUI layout and forms designer. The editor's features include syntax highlighting and autocompletion. Qt Creator uses the C++ compiler from the GNU Compiler Collection on Linux and FreeBSD. On Windows, it can use MinGW or MSVC, with the default install, and can also use Microsoft Console Debugger, when compiled from source code. Clang is also supported. – *Wikipedia* ([https://en.wikipedia.org/wiki/Qt_Creator](https://en.wikipedia.org/wiki/Qt_Creator))\n\n让我们从一个简单的*你好世界*程序开始，使用一个标签小部件。在本例中，我们将创建并显示一个标签小部件，文本为`Hello World, QT!`:\n\n```cpp\n#include <QApplication> \n#include <QLabel> \n\nint main (int argc, char* argv[]) \n{ \n    QApplication app(argc, argv); \n    QLabel label(\"Hello World, QT!\"); \n    Label.show(); \n    return app.execute(); \n}\n```\n\n在这段代码中，我们包含了两个库:`<QApplication>`和`<QLabel>`。`QApplication`对象是在`QApplication`库中定义的，它管理应用中的资源，运行任何基于 Qt 图形用户界面的应用都需要它。这个对象接受程序的命令行参数，当调用`app.execute()`时，Qt 事件循环启动。\n\nAn **event loop** is a program structure that permits events to be prioritized, queued, and dispatched to objects. In an event-based application, certain functions are implemented as passive interfaces that get called in response to certain events. The event loop generally continues running until a terminating event occurs (the user clicks on the QUIT button, for example).\n\n`QLabel`是所有 Qt 小部件中最简单的小部件，在`<QLabel>`中定义。在这段代码中，标签用文本`Hello World, QT`实例化。当`label.show()`被调用时，一个带有实例化文本的标签将出现在屏幕上自己的窗口框架中。\n\n现在，为了构建和运行应用，我们首先需要的是一个项目文件。要创建项目文件并编译应用，我们需要遵循以下步骤:\n\n1.  创建一个目录并将源代码保存在一个 CPP 文件中，驻留在这个目录中。\n2.  打开一个外壳，使用`qmake -v` 命令验证安装的`qmake`版本。如果找不到`qmake`，需要将安装路径添加到环境变量中。\n3.  现在，将目录更改为 shell 中的 Qt 文件路径，并执行`qmake -project`命令。这将为应用创建一个项目文件。\n4.  打开项目文件，在`INCLUDEPATH`后的`.pro`文件中添加以下一行:\n\n```cpp\n... \nINCLUDEPATH += . \nQT += widgets \n... \n```\n\n5.  然后，在没有参数的情况下运行`qmake`来创建包含构建应用的规则的`make`文件。\n6.  运行`make` ( `nmake`或`gmake`，视平台而定)，根据`Makefile`中指定的规则构建应用。\n7.  如果你运行应用，一个小窗口，上面有一个标签，写着你好，QT！会出现。\n\nThe steps to building any Qt GUI applications are the same, except for the changes that may be required in project files. For all of the future examples that we will discuss in this chapter, *build and run* means to follow these steps.\n\n在我们继续下一个例子之前，让我们找点乐子。用以下代码替换`QLabel`实例化:\n\n```cpp\nQLabel label(\"<h2><i>Hello World</i>, <font color=green>QT!</font></h2>\"); \n```\n\n现在，重建并运行应用。如这段代码所示，通过使用一些简单的 HTML 样式的格式，很容易定制 Qt 的用户界面。\n\n在下一节中，我们将学习如何处理 Qt 事件以及如何使用信号和插槽进行对象通信。\n\n# 带有信号/时隙/主运行中心的 Qt 事件模型——一个例子\n\n在本节中，我们将创建一个应用来处理`QLabel`中的鼠标事件。我们将在自定义`QLabel`中覆盖鼠标事件，并在放置自定义标签的对话框中处理它们。该应用的方法如下:\n\n1.  创建一个自定义的`my_QLabel`类，继承自框架`QLabel`类，并覆盖鼠标事件，如鼠标移动、鼠标按下和鼠标离开。\n2.  在`my_QLabel`中定义与这些事件对应的信号，并从相应的事件处理程序中发出。\n3.  创建一个继承自`QDialog`类的对话框类，并手工编码所有小部件的位置和布局，包括为处理鼠标事件而创建的自定义小部件。\n4.  在对话框类中，定义槽来处理从`my_QLabel`对象发出的信号，并在对话框中显示适当的结果。\n5.  在`QApplication`对象下实例化该对话框，并执行。\n6.  创建项目文件来构建一个小部件应用，并使其启动和运行。\n\n# 创建自定义小部件\n\n让我们编写头文件`my_qlabel.h`来声明类`my_QLabel`:\n\n```cpp\n#include <QLabel> \n#include <QMouseEvent> \n\nclass my_QLabel : public QLabel \n{ \n    Q_OBJECT \npublic: \n    explicit my_QLabel(QWidget *parent = nullptr); \n\n    void mouseMoveEvent(QMouseEvent *evt); \n    void mousePressEvent(QMouseEvent* evt); \n    void leaveEvent(QEvent* evt); \n\n    int x, y; \n\nsignals: \n    void Mouse_Pressed(); \n    void Mouse_Position(); \n    void Mouse_Left(); \n}; \n```\n\n`QLabel`和`QMouseEvent`在包含的库、`<QLabel>`和`<QMouseEvent>`下定义。该类派生自`QLabel`以继承其默认行为，`QObject`则恰当地处理信号机制。\n\n在头文件的私有部分，我们添加了一个`Q_OBJECT`宏，通知 MOC 必须为这个类生成元对象代码。信号和槽机制、运行时类型信息和动态属性系统都需要元对象代码。\n\n在类头中，与构造函数声明一起，鼠标事件(如鼠标移动事件、鼠标按下事件和鼠标离开事件)被重写。此外，公共整数变量保存鼠标指针的当前 *X* 和 *Y* 坐标。最后，从每个鼠标事件发出的信号在信号部分下声明。\n\n现在，让我们在一个 CPP 文件`my_qlabel.cpp`中定义这些项目:\n\n```cpp\n#include \"my_qlabel.h\" \n\nmy_QLabel::my_QLabel(QWidget *parent) : QLabel(parent), x(0), y(0)  {} \n\nvoid my_QLabel::mouseMoveEvent(QMouseEvent *evt) \n{ \n    this->x = evt->x(); \n    this->y = evt->y(); \n    emit Mouse_Position(); \n} \n```\n\n在构造函数中，父类被传递给`QLabel`基类，继承被覆盖类中未处理的情况，坐标变量被初始化为零。在`mouse-move`事件处理程序中，保存鼠标坐标的成员变量得到更新，并发出信号`Mouse_Position()`。使用`my_QLabel`的对话框可以将该信号连接到父对话框类中相应的`mouse-move`槽，并更新图形用户界面:\n\n```cpp\nvoid my_QLabel::mousePressEvent(QMouseEvent *evt) \n{ \n    emit Mouse_Pressed(); \n} \n\nvoid my_QLabel::leaveEvent(QEvent *evt) \n{ \n   emit Mouse_Left(); \n} \n```\n\n从`mouse-press`事件处理程序发出信号`Mouse_Pressed()`，从`mouse-leave`事件发出信号`Mouse_Left()`。这些信号连接到父小部件(`Dialog`类)的相应插槽，并更新图形用户界面。因此，我们编写了一个自定义标签类来处理鼠标事件。\n\n# 创建应用对话框\n\n由于标签类已经实现，我们需要实现对话框类来放置所有的小部件，并处理从`my_QLabel`对象发出的所有信号。让我们从`dialog.h`头文件开始:\n\n```cpp\n#include <QDialog> \n\nclass my_QLabel; \nclass QLabel; \n\nclass Dialog : public QDialog \n{ \n    Q_OBJECT \npublic: \n    explicit Dialog(QWidget *parent = 0); \n    ~Dialog(); \n\nprivate slots: \n    void Mouse_CurrentPosition(); \n    void Mouse_Pressed(); \n    void Mouse_Left(); \n\nprivate: \n    void initializeWidgets(); \n    my_QLabel *label_MouseArea; \n    QLabel *label_Mouse_CurPos; \n    QLabel *label_MouseEvents; \n}; \n```\n\n这里，我们正在创建一个继承自`QDialog`的`Dialog`类，在`<QDialog>`库下定义。类`QLabel`和`my_QLabel`在这个类头中被正向声明，因为实际的库将包含在类定义文件中。正如我们已经讨论过的，必须包含`Q_OBJECT`宏来生成用于启用信号和槽机制的元对象代码、运行时类型信息和动态属性系统。\n\n除了构造函数和析构函数声明之外，还声明了私有槽来连接从`my_QLabel`对象发出的信号。插槽是正常功能，可以正常调用；它们唯一的特点是信号可以连接到它们。`Mouse_CurrentPosition()`槽将连接到从`my_QLabel`物体的`mouseMoveEvent()`发出的信号。同样的，`Mouse_Pressed()`会连接到`mousePressEvent()`，而`MouseLeft()`会连接到`my_QLabel`对象的`leaveEvent()`。\n\n最后，完成所有小部件指针和一个名为`initializeWidgets()`的私有函数的声明，以实例化和布局对话框中的小部件。\n\n`Dialog`类的实现属于`dialog.cpp`:\n\n```cpp\n#include \"dialog.h\" \n#include \"my_qlabel.h\" \n#include <QVBoxLayout> \n#include <QGroupBox> \n\nDialog::Dialog(QWidget *parent) : QDialog(parent) \n{ \n    this->setWindowTitle(\"My Mouse-Event Handling App\"); \n    initializeWidgets(); \n\n    connect(label_MouseArea, SIGNAL(Mouse_Position()), this, SLOT(Mouse_CurrentPosition())); \n    connect(label_MouseArea, SIGNAL(Mouse_Pressed()), this, SLOT(Mouse_Pressed())); \n    connect(label_MouseArea, SIGNAL(Mouse_Left()), this, SLOT(Mouse_Left())); \n} \n```\n\n在构造器中，应用对话框的标题设置为`My Mouse-Event Handling App`。然后，`initializeWidgets()`函数被调用——稍后将解释该函数。创建并设置调用`initializeWidgets()`的布局后，从`my_QLabel`对象发出的信号连接到在`Dialog`类中声明的相应插槽:\n\n```cpp\nvoid Dialog::Mouse_CurrentPosition() \n{ \n    label_Mouse_CurPos->setText(QString(\"X = %1, Y = %2\") \n                                    .arg(label_MouseArea->x) \n                                    .arg(label_MouseArea->y)); \n    label_MouseEvents->setText(\"Mouse Moving!\"); \n} \n```\n\n`Mouse_CurrentPosition()`功能是从`my_QLabel`对象的鼠标移动事件发出的信号的插槽。在该功能中，标签小部件`label_Mouse_CurPos`用当前鼠标坐标进行更新，`label_MouseEvents`将其文本更新为`Mouse Moving!`:\n\n```cpp\nvoid Dialog::Mouse_Pressed() \n{ \n    label_MouseEvents->setText(\"Mouse Pressed!\"); \n} \n```\n\n`Mouse_Pressed()`功能是鼠标按下事件发出的信号的插槽，每次用户点击鼠标区域(对象`my_QLabel`内部)时都会调用该功能。该功能将`label_MouseEvents`标签中的文本更新为`\"Mouse Pressed!\"`:\n\n```cpp\nvoid Dialog::Mouse_Left() \n{ \n    label_MouseEvents->setText(\"Mouse Left!\"); \n} \n```\n\n最后，每当鼠标离开鼠标区域时，`my_QLabel`对象的鼠标离开事件会发出一个连接到`Mouse_Left()`插槽功能的信号。然后，它将`label_MouseEvents`标签中的文本更新为`\"Mouse Left!\"`。\n\n使用`initializeWidgets()`功能实例化并设置对话框中的布局，如下所示:\n\n```cpp\nvoid Dialog::initializeWidgets() \n{ \n    label_MouseArea = new my_QLabel(this); \n    label_MouseArea->setText(\"Mouse Area\"); \n    label_MouseArea->setMouseTracking(true); \n    label_MouseArea->setAlignment(Qt::AlignCenter|Qt::AlignHCenter); \n    label_MouseArea->setFrameStyle(2); \n```\n\n在这段代码中，`label_MouseArea`对象用自定义标签类`my_QLabel` *进行实例化。*然后，修改标签属性(如标签文本修改为`\"Mouse Area\"`)，在`label_MouseArea`对象内部启用鼠标跟踪，对齐设置为居中，框架样式设置为粗线。\n\n```cpp\nlabel_Mouse_CurPos = new QLabel(this);\nlabel_Mouse_CurPos->setText(\"X = 0, Y = 0\");\nlabel_Mouse_CurPos->setAlignment(Qt::AlignCenter|Qt::AlignHCenter);\nlabel_Mouse_CurPos->setFrameStyle(2);\nlabel_MouseEvents = new QLabel(this);\nlabel_MouseEvents->setText(\"Mouse current events!\");\nlabel_MouseEvents->setAlignment(Qt::AlignCenter|Qt::AlignHCenter);\nlabel_MouseEvents->setFrameStyle(2);\n```\n\n标签对象`label_Mouse_CurPos`和`label_MouseEvents`正在更新其属性，例如文本对齐和框架样式，类似于`label_MouseArea`对象。但是`label_Mouse_CurPos`中的文本最初设置为`\"X = 0, Y = 0\"`，而`label_MouseEvents`标签设置为`\"Mouse current events!\"`:\n\n```cpp\n    QGroupBox *groupBox = new QGroupBox(tr(\"Mouse Events\"), this); \n    QVBoxLayout *vbox = new QVBoxLayout; \n    vbox->addWidget(label_Mouse_CurPos); \n    vbox->addWidget(label_MouseEvents); \n    vbox->addStretch(0); \n    groupBox->setLayout(vbox); \n\n    label_MouseArea->move(40, 40); \n    label_MouseArea->resize(280,260); \n    groupBox->move(330,40); \n    groupBox->resize(200,150); \n}\n```\n\n最后，创建一个垂直的方框布局(`QVBoxLayout`)，并在其中添加`label_Mouse_CurPos`和`label_MouseEvents`标签小部件。另外，用标签`Mouse Events`创建一个分组框，分组框的布局被做成垂直的框布局，用小部件创建。最后，鼠标区域标签和鼠标事件组框的位置和大小被设置为预定义的值。因此，小部件的创建和布局设置就完成了。\n\n# 执行应用\n\n我们现在可以编写`main.cpp`来创建`Dialog`类并显示它:\n\n```cpp\n#include \"dialog.h\" \n#include <QApplication> \n\nint main(int argc, char *argv[]) \n{ \n    QApplication app(argc, argv); \n    Dialog dialog; \n    dialog.resize(545, 337); \n    dialog.show(); \n    return app.exec(); \n} \n```\n\n这段代码与我们讨论的 Hello World Qt 应用完全一样。我们正在实例化我们创建的`Dialog`类，而不是`QLabel`，通过使用`resize()`函数将对话框窗口调整到预定义的值。现在，应用已经准备好构建和运行了。但是，在构建应用之前，让我们手工编码项目文件:\n\n```cpp\nQT += widgets \n\nSOURCES +=  \n        main.cpp  \n        dialog.cpp  \n    my_qlabel.cpp \n\nHEADERS +=  \n        dialog.h  \n    my_qlabel.h \n```\n\n现在，构建应用并运行它。将弹出如下对话框(Windows 平台):\n\n![](img/00018.jpeg)\n\n当我们将鼠标指针悬停在左侧标签(鼠标区域)上时，鼠标的坐标将在右侧的第一个标签中更新，右侧的第二个标签将显示文本，鼠标移动！按下鼠标区域中的任何鼠标按钮，第二个标签中的文本将变为“鼠标按下”！当鼠标指针离开鼠标区域时，文字会更新为鼠标左键！\n\n在本节中，我们学习了如何创建对话框窗口、对话框下的小部件、小部件中的布局等。我们还学习了如何启用自定义小部件(标签小部件)，以及如何处理系统事件。然后，我们学习了使用用户定义的信号和插槽创建和连接对象。最后，我们使用了所有这些小部件，包括一个自定义小部件，并创建了一个应用来处理窗口中的 Qt 鼠标事件。\n\n现在，让我们实现一个类似的应用来处理`QLabel`中的鼠标事件，并在另一个标签中显示鼠标坐标。这里，事件处理通过使用事件订阅和事件过滤来执行，具有`RxCpp`可观察值和 Qt 事件过滤器。\n\n# 将 RxCpp 库与 Qt 事件模型集成\n\n在前面的章节中，我们已经从鸟瞰图中看到了 Qt 框架。我们学习了如何处理 Qt 事件，尤其是鼠标事件和信号/槽机制。在前两章中，我们也了解了`RxCpp`库及其编程模型。在这个过程中，我们遇到了许多重要的反应操作符，这些操作符在利用反应方法编写程序时很重要。\n\n在本节中，我们将编写一个应用来处理标签小部件中的鼠标事件，这与前面的示例类似。在这个例子中，我们将使用`RxCpp`订阅者订阅 Qt 鼠标事件，并从结果鼠标事件流中过滤不同的鼠标事件，而不是处理鼠标事件来发出信号(就像我们在上一个例子中所做的那样)。事件(未被过滤掉)将与订阅者相关。\n\n# Qt 事件过滤器–反应式方法\n\n如前所述，Qt 框架有一个健壮的事件机制。我们需要在 Qt 和 RxCpp 方案之间架起一座桥梁。为了开始使用这个应用，我们将编写一个头文件`rx_eventfilter.h`，包装所需的 RxCpp 头和 Qt 事件过滤器:\n\n```cpp\n#include <rxcpp/rx.hpp> \n#include <QEvent> \nnamespace rxevt { \n    // Event filter object class \n    class EventEater: public QObject  { \n    Public: \n        EventEater(QObject* parent, QEvent::Type type, rxcpp::subscriber<QEvent*> s): \n        QObject(parent), eventType(type), eventSubscriber(s) {} \n       ~EventEater(){ eventSubscriber.on_completed();}\n```\n\n包含`<rxcpp/rx.hpp>`库是为了得到我们在这个类中使用的`RxxCppsubscriber`和`observable`的定义，以及`QEvent`定义的`<QEvent>`库。整个头文件在命名空间`rxevt`下定义。现在，`EventEater`类是植入到`filter-in`的 Qt 事件过滤器类，这是成员`eventType`唯一初始化的 Qt 事件。为此，类有两个成员变量。第一个是`eventSubscriber`，是`QEvent`型的`rxcpp::subscriber`，下一个是`eventType`，用来握持`QEvent::Type`。\n\n在构造函数中，父类`QObject`(需要过滤事件的小部件)被传递给基类`QObject`。成员变量`eventType`和`eventSubscriber`用需要过滤的`QEvent::Type`和对应事件类型的`rxcpp::subscriber`初始化:\n\n```cpp\n        bool eventFilter(QObject* obj, QEvent* event) { \n            if(event->type() == eventType) \n            { eventSubscriber.on_next(event);} \n            return QObject::eventFilter(obj, event); \n        } \n```\n\n只有当事件类型与初始化类型相同时，我们才会覆盖`eventFilter()`函数来调用`on_next()`。`EventEater`是一个事件过滤器对象，接收发送到该对象的所有事件。筛选器可以停止该事件，也可以将其转发给此对象。`EventEater`对象通过其`eventFilter()`功能接收事件。如果事件应该被过滤(换句话说，停止)，则`eventFilter()`功能([http://doc.qt.io/qt-5/qobject.html#eventFilter](http://doc.qt.io/qt-5/qobject.html#eventFilter))必须返回真；否则，必须返回`false`:\n\n```cpp\n    private: \n        QEvent::Type eventType; \n        rxcpp::subscriber<QEvent*> eventSubscriber; \n    }; \n```\n\n因此，让我们在同一个头文件下编写一个实用函数，使用`EventEater`对象从事件流中创建并返回一个`rxcpp::observable`:\n\n```cpp\n    // Utility function to retrieve the rxcpp::observable of filtered events \n    rxcpp::observable<QEvent*> from(QObject* qobject, QEvent::Type type) \n    { \n        if(!qobject) return rxcpp::sources::never<QEvent*>(); \n         return rxcpp::observable<>::create<QEvent*>( \n            [qobject, type](rxcpp::subscriber<QEvent*> s) { \n                qobject->installEventFilter(new EventEater(qobject, type, s)); \n            } \n        ); \n    } \n} // rxevt \n```\n\n在这个函数中，我们从事件流中返回`QEvent`的可观察值，我们将使用`EventEater`对象对其进行过滤。一个`QObject`实例可以被设置为在另一个`QObject`实例看到它们之前监控它们的事件。这是 Qt 事件模型的一个非常强大的特性。`installEventFilter()`函数的调用使其成为可能，`EventEater`类具备执行过滤的条件。\n\n# 创建窗口-设置布局和路线\n\n现在，让我们编写应用代码来创建小部件窗口，它包含两个标签小部件。一个标签将用作鼠标区域，类似于前面的示例，后者将用于显示过滤后的鼠标事件和鼠标坐标。\n\n让我们将`main.cpp`中的代码分为两部分来看。首先，我们将讨论创建和设置小部件布局的代码:\n\n```cpp\n#include \"rx_eventfilter.h\" \nint main(int argc, char *argv[]) \n{ \n    QApplication app(argc, argv); \n    // Create the application window \n    auto widget = std::unique_ptr<QWidget>(new QWidget()); \n    widget->resize(280,200); \n        // Create and set properties of mouse area label \n    auto label_mouseArea   = new QLabel(\"Mouse Area\"); \n    label_mouseArea->setMouseTracking(true); \n    label_mouseArea->setAlignment(Qt::AlignCenter|Qt::AlignHCenter); \n    label_mouseArea->setFrameStyle(2); \n    // Create and set properties of message display label \n    auto label_coordinates = new QLabel(\"X = 0, Y = 0\"); \n    label_coordinates->setAlignment(Qt::AlignCenter|Qt::AlignHCenter); \n    label_coordinates->setFrameStyle(2);\n```\n\n我们已经包含了`rx_eventfilter.h`头文件，以使用使用`RxCpp`库实现的事件过滤机制。在这个应用中，不是在对话框中创建这些小部件，而是创建一个`QWidget`对象，并将两个`QLabel`小部件添加到一个`QVBoxLayout`布局中；这被设置为应用小部件的布局。应用窗口的大小是一个预定义值`200pixels`宽和`280pixels`高。与前面的应用类似，第一个标签启用了鼠标跟踪:\n\n```cpp\n    // Adjusting the size policy of widgets to allow stretching \n    // inside the vertical layout \n    label_mouseArea->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); \n    label_coordinates->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); \n    auto layout = new QVBoxLayout; \n    layout->addWidget(label_mouseArea); \n    layout->addWidget(label_coordinates); \n    layout->setStretch(0, 4); \n    layout->setStretch(1, 1); \n    widget->setLayout(layout); \n```\n\n两个小部件的大小策略都设置为`QSizePolicy::Expanding`，以允许在垂直布局框内拉伸小部件。这允许我们使鼠标区域标签大于状态显示标签。`setStretch()`功能将位置索引处的拉伸因子设置为拉伸。\n\n# 特定于事件类型的可观察值\n\n订阅鼠标事件`rxcpp::observable`的代码如下:\n\n*   鼠标移动\n*   鼠标按键\n*   鼠标按键双击\n\n程序如下:\n\n```cpp\n    // Display the mouse move message and the mouse coordinates \n    rxevt::from(label_mouseArea, QEvent::MouseMove) \n            .subscribe([&label_coordinates](const QEvent* e){ \n        auto me = static_cast<const QMouseEvent*>(e); \n        label_coordinates->setText(QString(\"Mouse Moving : X = %1, Y = %2\") \n                                   .arg(me->x()) \n                                   .arg(me->y())); \n    });\n```\n\n`rxevt::from()`函数根据我们作为参数传递的`QEvent::Type`从`label_mouseArea`返回事件的`rxcpp::observable`。在这段代码中，我们订阅了`label_mouseArea`中的一组事件，它们属于`QEvent::MouseMove`类型。这里，我们用鼠标指针当前的 *X* 和 *Y* 位置更新`label_coordinates`文本:\n\n```cpp\n    // Display the mouse signle click message and the mouse coordinates \n    rxevt::from(label_mouseArea, QEvent::MouseButtonPress) \n            .subscribe([&label_coordinates](const QEvent* e){ \n        auto me = static_cast<const QMouseEvent*>(e); \n        label_coordinates->setText(QString(\"Mouse Single click at X = %1, Y = %2\") \n                                   .arg(me->x()) \n                                   .arg(me->y())); \n    }); \n```\n\n类似于鼠标移动过滤，可观察到的`QEvent`由`rxevt::from()`函数返回，仅包括类型为`QEvent::MouseButtonPress`的事件。然后，文本在`label_coordinates`中更新，鼠标点击的位置:\n\n```cpp\n    // Display the mouse double click message and the mouse coordinates \n    rxevt::from(label_mouseArea, QEvent::MouseButtonDblClick) \n            .subscribe([&label_coordinates](const QEvent* e){ \n        auto me = static_cast<const QMouseEvent*>(e); \n        label_coordinates->setText(QString(\"Mouse Double click at X = %1, Y = %2\") \n                                   .arg(me->x()) \n                                   .arg(me->y())); \n    }); \n    widget->show(); \n    return app.exec(); \n} // End of main \n```\n\n最后事件类型`QEvent::MouseButtonDblClick`的处理也类似于鼠标的单次点击，`label_coordinates`中的文本也随着双击位置而更新。然后调用应用窗口小部件的`show()`函数，调用`exec()`函数启动事件循环。\n\n项目文件`Mouse_EventFilter.pro`如下:\n\n```cpp\nQT += core widgets \nCONFIG += c++ 14 \n\nTARGET = Mouse_EventFilter \nINCLUDEPATH += include \n\nSOURCES +=  \n    main.cpp \nHEADERS +=  \n    rx_eventfilter.h  \n```\n\n由于 RxCpp 库是一个只包含标题的库，因此在项目目录内创建了一个名为`include`的文件夹，RxCpp 库文件夹被复制到那里。更新`INCLUDEPATH`将帮助应用获取指定目录中的任何包含文件。现在，让我们构建并运行该应用。\n\n# RxQt 简介\n\n`RxQt`库是在`RxCpp`库的基础上编写的公共领域库，可以轻松地用 Qt 事件和信号进行反应式编程。为了理解这个库，让我们跳到一个例子中，这样我们就可以跟踪鼠标事件，并使用库提供的可观察值过滤它们。该库可从[https://github.com/tetsurom/rxqt](https://github.com/tetsurom/rxqt)的 GitHub 资源库下载:\n\n```cpp\n#include <QApplication> \n#include <QLabel> \n#include <QMouseEvent> \n#include \"rxqt.hpp\" \n\nint main(int argc, char *argv[]) \n{ \n    QApplication app(argc, argv); \n\n    auto widget = new QWidget(); \n    widget->resize(350,300); \n    widget->setCursor(Qt::OpenHandCursor); \n\n    auto xDock = new QLabel((QWidget*)widget); \n    xDock->setStyleSheet(\"QLabel { background-color : red}\"); \n    xDock->resize(9,9); \n    xDock->setGeometry(0, 0, 9, 9); \n\n    auto yDock = new QLabel((QWidget*)widget); \n    yDock->setStyleSheet(\"QLabel { background-color : blue}\"); \n    yDock->resize(9,9); \n    yDock->setGeometry(0, 0, 9, 9); \n```\n\n前面的代码创建了`QWidget`，作为另外两个`QLabels`的父级。创建了两个标签小部件，沿着窗口的顶部和左侧边界在父小部件内部移动。沿 *X* 轴的可停靠标签为红色，沿 *Y* 轴的标签为蓝色:\n\n```cpp\n    rxqt::from_event(widget, QEvent::MouseButtonPress) \n            .filter([](const QEvent* e) { \n        auto me = static_cast<const QMouseEvent*>(e); \n        return (Qt::LeftButton == me->buttons()); \n    }) \n            .subscribe([&](const QEvent* e) { \n        auto me = static_cast<const QMouseEvent*>(e); \n        widget->setCursor(Qt::ClosedHandCursor); \n        xDock->move(me->x(), 0); \n        yDock->move(0, me->y()); \n    }); \n```\n\n在前面的代码中，`rxqt::from_event()`函数从 widget 类中过滤掉除了`QEvent::MouseButtonPress`事件之外的所有事件，并返回一个`rxcpp::observable<QEvent*>`实例。如果按钮是鼠标左键，这里的`rxcpp::observable`已经用那些鼠标事件过滤了。然后，在`subscribe()`方法的 Lambda 函数中，我们将光标变为`Qt::ClosedHandCursor`。我们还将`xDock`的位置设置为鼠标*x*-位置值，以及窗口的上边缘，将`yDock`的位置设置为鼠标*y*-位置，以及窗口的左边缘:\n\n```cpp\n    rxqt::from_event(widget, QEvent::MouseMove) \n            .filter([](const QEvent* e) { \n        auto me = static_cast<const QMouseEvent*>(e); \n        return (Qt::LeftButton == me->buttons()); \n    }) \n            .subscribe([&](const QEvent* e) { \n        auto me = static_cast<const QMouseEvent*>(e); \n        xDock->move(me->x(), 0); \n        yDock->move(0, me->y()); \n    });\n```\n\n在这段代码中，我们使用`RxQt`库过滤小部件窗口中的所有鼠标移动事件。这里可以观察到的是一系列鼠标事件，包括鼠标移动和鼠标左键按下事件。在 subscribe 方法中，代码沿着窗口的上边缘和左边缘更新`xDock`和`yDock`的位置:\n\n```cpp\n    rxqt::from_event(widget, QEvent::MouseButtonRelease) \n            .subscribe([&widget](const QEvent* e) { \n        widget->setCursor(Qt::OpenHandCursor); \n    }); \n\n    widget->show(); \n    return app.exec(); \n} \n```\n\n最后过滤掉过滤后的鼠标按键释放事件，将鼠标光标设置回`Qt::OpenHandCursor`。为了给这个应用增加一些乐趣，让我们再创建一个小部件，类似于`xDock`和`yDock`；这将是一个重力物体。按下时，重力对象将跟随鼠标光标:\n\n```cpp\n#ifndef GRAVITY_QLABEL_H \n#define GRAVITY_QLABEL_H \n\n#include <QLabel> \n\nclass Gravity_QLabel : public QLabel \n{ \n   public: \n    explicit Gravity_QLabel(QWidget *parent = nullptr): \n         QLabel(parent), prev_x(0), prev_y(0){} \n\n    int prev_x, prev_y; \n}; \n\n#endif // GRAVITY_QLABEL_H \n```\n\n现在，我们必须在应用窗口下创建一个重力小部件的实例(来自新创建的`Gravity_QLabel`类):\n\n```cpp\n    auto gravityDock = new Gravity_QLabel((QWidget*)widget); \n    gravityDock->setStyleSheet(\"QLabel { background-color : green}\"); \n    gravityDock->resize(9,9); \n    gravityDock->setGeometry(0, 0, 9, 9);\n```\n\n类似于`xDock`和`yDock`的创建和大小设置，新的`gravityDock`对象已经创建。此外，每当抛出`press`事件时，必须在鼠标坐标值中设置该对象的位置。因此，在`QEvent::MouseButtonPress`的 subscribe 方法的 Lambda 函数内部，我们需要添加以下代码行:\n\n```cpp\n    gravityDock->move(me->x(),me->y()); \n```\n\n最后`gravityDock`的位置需要更新，按照鼠标移动。为此，在`QEvent::MouseMove`的`subscribe`方法的 Lambda 函数内部，我们需要添加以下代码:\n\n```cpp\n    gravityDock->prev_x = gravityDock->prev_x * .96 + me->x() * .04; \n    gravityDock->prev_y = gravityDock->prev_y * .96 + me->y() * .04; \n    gravityDock->move(gravityDock->prev_x, gravityDock->prev_y); \n```\n\n这里`gravityDock`的位置被更新为一个新的值，该值是先前值的 96%和新位置的 4%之和。因此，我们使用`RxQt`和 RxCpp 库过滤 Qt 事件，以创建一个 *X* - *Y* 鼠标位置指示器和一个重力对象。现在，让我们构建并运行该应用。\n\n# 摘要\n\n在本章中，我们讨论了使用 Qt 进行反应式图形用户界面编程的主题。我们首先快速概述了使用 Qt 开发图形用户界面应用。我们学习了 Qt 框架中的概念，例如 Qt 对象层次结构、元对象系统以及信号和槽。我们使用一个简单的标签小部件编写了一个基本的*你好世界*应用。然后，我们使用自定义标签小部件编写了一个鼠标事件处理应用。在那个应用中，我们了解了更多关于 Qt 事件系统如何工作，以及如何使用信号和槽机制进行对象通信。最后，我们编写了一个应用来处理鼠标事件，并通过使用`RxCpp`订阅模型和 Qt 事件过滤器来过滤它们。我们介绍了如何在图形用户界面框架(如 Qt)中使用 RxCpp 来遵循反应式编程模型。我们还介绍了`RxQt`库，这是一个集成了 RxCpp 和 Qt 库的公共领域。\n\n在进入下一章之前，您需要了解如何为 RxCpp 可观测值编写*自定义运算符。这一主题将在在线部分讨论。您可以参考以下链接:[https://www . packtpub . com/sites/default/files/downloads/Creating _ Custom _ Operators _ in _ rxcpp . pdf](https://www.packtpub.com/sites/default/files/downloads/Creating_Custom_Operators_in_RxCpp.pdf)。*\n\n阅读完前面提到的主题后，我们可以进入下一章，在这一章中，我们将了解 C++ 反应式编程的设计模式和习惯用法。"
  },
  {
    "path": "docs/cpp-react-prog/10.md",
    "content": "# 十、C++ 反应式编程的设计模式和习惯用法\n\n我们已经讨论了在 C++ 中使用反应式编程模型的很多内容。到目前为止，我们已经了解了 RxCpp 库及其编程模型、RxCpp 库的关键元素以及反应式 GUI 编程。\n\n在本章中，我们将涵盖以下主题:\n\n*   图案和图案运动介绍\n*   设计模式和反应式编程\n*   一些反应式编程模式和习惯用法\n\n# 面向对象和设计模式运动\n\n**面向对象编程** ( **OOP** )已经达到了临界质量，这要归功于 90 年代初优秀 C++ 编译器的大量涌现。20 世纪 90 年代初的程序员经常努力理解面向对象程序设计，以及如何在大型项目中有效地使用它。没有互联网这样的病毒媒介，这是一场相当艰难的斗争。早期采用者发表技术报告，在期刊/期刊上写作，并举办研讨会来普及 OOP 技术。像《多布博士杂志》和《C++ 报告》这样的杂志过去都有面向对象的专栏。\n\n需要将专家的智慧传递给不断增长的编程社区，但这种知识传播并没有发生。德国传奇数学家卡尔·弗里德里希·高斯说过:“一个人总是向大师们学习。尽管高斯心中有数学，但他的说法对任何非平凡的人类努力都是正确的。然而，以前，面向对象技术的大师很少，学徒模式没有很好地扩展。\n\nJames Coplien published an influential book entitled *Advanced C++ Styles and Idioms*, which dealt with the low-level patterns (idioms) associated with usage of the C++ programming language. Even though it is not widely cited, authors consider it a notable book for cataloging the best practices and techniques for OOP.\n\n埃里希·伽马从一位名叫克里斯托弗·亚历山大的建筑建筑师那里获得灵感，开始着手设计一个图案目录，作为他博士论文的一部分。克里斯托弗·亚历山大的《城镇和建筑的模式》是埃里希·伽马灵感的来源。在此之后，拥有类似想法的人，即拉尔夫·约翰逊、约翰·弗利西德斯和理查德·赫尔姆，与埃里希·伽马联手创建了一个包含 23 种设计模式的目录，现在被亲切地称为**四人帮** ( **GOF** )设计模式。爱迪生·韦斯利在 1994 年出版了《设计模式:可重用面向对象软件的元素》一书。这很快成为程序员的一个很好的参考，并推动了面向模式的软件开发。GOF 目录主要集中在软件设计上。\n\n1996 年，西门子的一群工程师出版了《面向模式的软件架构》一书，主要关注构建系统的架构方面。约翰·威利父子出版的五本书记录了整个 POSA 模式目录。加入该小组的还有道格拉斯·施密特，他是**自适应通信环境** ( **ACE** )网络编程库和 TAO(ACE ORB)的创建者。他后来成为**对象管理组** ( **OMG** )的主席，该组开发、采用和维护标准，如 CORBA 和 UML。\n\n在前两项倡议之后，活动如潮水般涌来；其他值得注意的模式目录如下:\n\n*   *企业应用架构的模式*，马丁·福勒等著。\n*   *企业整合模式*，作者:格雷格·霍普和波比·沃尔夫。\n*   *核心 J2EE 模式*，Deepak Alur 等人。\n*   *领域驱动设计*，埃里克·埃文斯。\n*   *企业模式和 MDA* ，作者吉姆·阿洛和伊拉·纽斯塔德。\n\n尽管这些书本身意义重大，但它们倾向于当时新兴的企业软件开发领域。对于 C++ 开发人员来说，GOF 目录和 POSA 目录是最重要的。\n\n# 关键模式目录\n\n模式是软件设计中常见问题的命名解决方案。模式通常被编目在某种模式库中。其中一些是作为书籍出版的。最受欢迎和广泛使用的模式目录是 GOF。\n\n# GOF 模式\n\n以目录创造者命名的“四人帮”(g of)发起了模式运动。创作者主要集中于设计和构建面向对象的软件。克里斯托弗·亚历山大的思想被借用到软件工程学科，并被应用到应用架构、并发性、安全性等方面。“四人帮”将目录分为结构模式、创造模式和行为模式。原著用 C++ 和 Smalltalk 来解释这些概念。这些模式已经在当今大多数编程语言中得到移植和利用。看看这张桌子:\n\n| **Sl。编号** | **图案类型** | **图案** |\n| one | 创造模式 | 抽象工厂，构建器，工厂方法，原型，单例 |\n| Two | 结构模式 | 适配器、桥、复合材料、装饰器、门面、飞轮、代理 |\n| three | 行为模式 | 责任链、命令、解释器、迭代器、中介器、纪念品、观察者、状态、策略、模板方法、访问者 |\n\n我们相信，对于任何程序员来说，很好地理解 g of 模式都是必要的。这些模式无处不在，与应用领域无关。GOF 模式帮助我们以语言不可知的方式交流和推理系统。它们在。NET 和 Java 世界。Qt 框架广泛利用了 GOF 存储库中的模式，给出了一个直观的编程模型。\n\n# 编目姿势\n\n*软件架构的模式*(五卷)是一个有影响力的书系列，它涵盖了开发任务关键系统的大多数适用模式。该目录适用于编写大型软件关键任务子系统的人员，尤其是数据库引擎、分布式系统、中间件系统等。目录的另一个优点是它非常适合 C++ 程序员。\n\n下表列出了涵盖五个已发布卷的目录:\n\n| Sl。号码 | 模式类型 | 模式 |\n| one | 建筑学的 | 层、管道和过滤器、黑板、代理、MVC、表示-抽象-控制、微内核、反射 |\n| Two | 设计 | 整体-部分、主从、代理、命令处理器、视图处理器、转发器-接收器、客户端-调度器-服务器、发布者-订阅者 |\n| three | 服务访问和配置模式 | 包装器外观、组件配置器、拦截器、扩展接口 |\n| four | 事件处理模式 | 反应器、主动方、异步完成令牌、接受方连接器 |\n| five | 同步模式 | 范围锁定，策略锁定，线程安全接口，双重检查锁定优化 |\n| six | 并发模式 | 活动对象、监控对象、半同步/半异步、领导者/追随者、线程特定存储 |\n| seven | 资源获取模式 | 查找，惰性获取，急切获取，部分获取 |\n| eight | 资源生命周期 | 缓存、池、协调器、资源生命周期管理器 |\n| nine | 资源释放模式 | 租赁，驱逐者 |\n| Ten | 分布式计算的模式语言 | 分布式编程环境中不同目录模式的合并 |\n| Eleven | 论模式和模式语言 | 最后一卷给出了一些关于模式、模式语言和用法的元信息 |\n\n需要研究 POSA 目录，以深入了解部署在世界各地的大规模系统的架构基础。我们认为，尽管这个目录很重要，但它没有得到应有的重视。\n\n# 设计模式 redux\n\nGOF 模式和反应式编程有更深层次的联系，这从表面上看是显而易见的。GOF 模式主要与编写基于面向对象的程序有关。反应式编程是关于函数式编程、流编程和并发性的。我们已经了解到反应式编程涵盖了经典 GOF 观测器模式中的一些缺陷(在[第 5 章](05.html#2RHM00-51c8384cc2cb48e691b461190723b468)、*观测器介绍*的第一节中，我们涵盖了这一缺陷)。\n\nOOP 程序基本上是关于建模层次的，从模式世界来看，复合模式是建模部分/整体层次的方式。无论哪里有组合，访问者实现的集合都会随之而来。换句话说，复合访问者二人组是编写面向对象系统的规范模式。\n\n访问者实现应该对复合结构有所了解。随着访问者数量的激增，使用访问者模式的行为处理变得困难。此外，向处理中添加转换和过滤器会使问题进一步复杂化。\n\n输入迭代器模式，这有利于序列、流或项目列表的导航。使用对象/函数编程结构，我们可以非常容易地过滤和转换序列。微软的语言集成查询和 Java 中的 Lambda/Streams 处理(8 及以上)就是迭代器模式的很好例子。\n\n现在，我们将如何将分层数据转换为线性结构？大多数层次结构可以展平成一个流，以便进一步处理。最近，人们开始做以下事情:\n\n*   使用复合模式建模它们的层次结构。\n*   使用访问者将层次结构展平成一个序列。\n*   使用迭代器模式导航这些序列。\n*   在对序列执行操作之前，对它们应用一系列转换和过滤器。\n\n前面的方法称为编程的`pull`方法。消费者或客户端从事件或数据源中提取数据进行处理。该方案存在以下问题:\n\n*   数据被不必要地拉入客户端。\n*   转换和过滤器应用于事件接收器端。\n*   事件接收器可以阻止服务器。\n*   这种风格不适合异步处理，因为异步处理中的数据会随着时间的推移而变化。\n\n这个问题的一个好的解决方案是反向凝视，其中数据作为流从服务器异步推送，事件接收器将对流做出反应。这种系统的另一个优点是在事件源端放置转换和过滤器。这导致只有绝对重要的数据需要在接收器端处理的情况。\n\n方案如下:\n\n*   数据以流的形式处理，这被称为可观测值。\n*   我们可以对它们应用一系列运算符，或者更高阶的运算符。\n*   一个运算符总是接受一个可观测值，然后返回另一个可观测值。\n*   我们可以订阅一个可观察的通知。\n*   观察者有标准的机制来处理它们。\n\n在这一节中，我们学习了 OOP 模式和反应式编程是如何密切相关的。两种范例的明智混合产生了高质量、可维护的代码。我们还讨论了如何转换面向对象设计模式(复合/访问者)来利用迭代器模式。我们讨论了如何通过轻微的推动来改进迭代方案(事件源端的一个火了就忘了的习惯用法)。在下一节中，我们将通过编写代码来演示整个技术。\n\n# 从设计模式到反应式编程\n\n尽管设计模式运动与面向对象程序设计相一致，而反应式编程与面向对象程序设计相一致，但它们之间有着密切的相似之处。在前一章中，我们学习了以下内容:\n\n*   面向对象模型有利于对系统的结构方面进行建模。\n*   FP 模型很适合对系统的行为方面进行建模。\n\n为了说明 OOP 和反应式编程之间的联系，我们将编写一个程序，该程序将遍历目录来枚举给定文件夹中的文件和子文件夹。\n\n我们将创建一个包含以下内容的复合结构:\n\n*   一个`FileNode`(继承自`EntryNode`)建模文件信息\n*   一个`DirectoryNode`(继承自`EntryNode`)建模文件夹信息\n\n在定义了前面的组合之后，我们将为以下内容定义访问者:\n\n*   打印文件名和文件夹名\n*   将复合层次结构转换为文件名列表\n\n不用多说，让我们进入正题。看看这段代码:\n\n```cpp\n//---------- DirReact.cpp \n#include <rxcpp/rx.hpp> \n#include <memory> \n#include <map> \n#include <algorithm> \n#include <string> \n#include <vector> \n#include <windows.h> // This is omitted in POSIX version \n#include <functional> \n#include <thread> \n#include <future> \nusing namespace std; \n//////////////////////////////////// \n//-------------- Forward Declarations \n//-------------- Model Folder/File \nclass FileNode; \nclass DirectoryNode; \n//////////////////////////////// \n//------------- The Visitor Interface \nclass IFileFolderVisitor; \n```\n\n前面的正向声明是为了防止编译器在编译程序时发出错误和警告。`FileNode`存储文件名及其大小作为实例变量。`DirectoryNode`存储文件夹名称和`FileNode`列表，以指示目录中的文件和文件夹。`FileNode` / `DirectoryNode`层级由`IFileFolderVisitor`界面处理。我们可以看到如下声明:\n\n```cpp\n///////////////////////////////// \n//------ a Type to store FileInformation \nstruct FileInformation{ \n   string name; \n   long size; \n   FileInformation( string pname,long psize ) \n   { name = pname;size = psize; } \n}; \n////////////////////////////// \n//-------------- Base class for File/Folder data structure \nclass EntryNode{ \n    protected: \n      string  name; \n      int isdir; \n      long size; \n    public: \n      virtual bool Isdir() = 0; \n      virtual long getSize() = 0; \n      virtual void Accept(IFileFolderVisitor& ivis)=0; \n      virtual ~EntryNode() {} \n};\n```\n\n当我们创建复合时，我们需要创建一个节点类，作为层次结构中所有成员的基类。在我们的例子中，`EntryNode`类就是这样做的。我们将文件或文件夹的名称、大小等存储在基类中。除了应该由派生类实现的三个虚函数之外，我们还有一个虚拟析构函数。虚拟析构函数的存在确保了治疗中的析构函数被正确调用，如下所示:\n\n```cpp\n//-------------The Visitor Interface \nclass IFileFolderVisitor{ \n   public: \n    virtual void Visit(FileNode& fn )=0; \n    virtual void Visit(DirectoryNode& dn )=0; \n}; \n```\n\n每当我们使用复合模式样式实现定义层次结构时，我们就定义一个访问者接口来处理层次结构中的节点。对于层次结构中的每个节点，在访问者界面中都会有一个`visit`方法。组合的类层次结构中的每个节点都有一个`accept`方法，访问者接口将调用分派给相应节点的`accept`方法。`accept`方法将呼叫调度回访问者中正确的`visit`方法。这个过程叫做**双派单**:\n\n```cpp\n// The Node which represents Files \nclass FileNode : public EntryNode { \n   public:  \n   FileNode(string pname, long psize) {  isdir = 0; name = pname; size = psize;} \n   ~FileNode() {cout << \"....Destructor FileNode ....\" << name << endl; } \n   virtual bool  Isdir() { return isdir == 1; } \n   string getname() { return name; }\n```\n\n```cpp\n   virtual long getSize() {return size; } \n   virtual void Accept( IFileFolderVisitor& ivis ){ivis.Visit(*this);} \n}; \n```\n\n`FileNode`类只是存储文件的名称和大小，存储在节点中。该类还实现了基类(`EntryNode`)中声明的所有虚拟方法。`accept`方法将调用重定向到正确的访问者级别方法，如下所示:\n\n```cpp\n// Node which represents Directory \nclass DirectoryNode : public EntryNode { \n  list<unique_ptr<EntryNode>> files;   \npublic: \n  DirectoryNode(string pname)  \n  { files.clear(); isdir = 1; name = pname;} \n  ~DirectoryNode() {files.clear();} \n  list<unique_ptr<EntryNode>>& GetAllFiles() {return files;} \n  bool AddFile( string pname , long size) { \n       files.push_back(unique_ptr<EntryNode> (new FileNode(pname,size))); \n       return true; \n  } \n  bool AddDirectory( DirectoryNode *dn ) { \n        files.push_back(unique_ptr<EntryNode>(dn)); \n        return true; \n  } \n  bool Isdir() { return isdir == 1; } \n  string  getname() { return name; } \n  void   setname(string pname) { name = pname; } \n  long getSize() {return size; } \n  void Accept( IFileFolderVisitor& ivis ){ivis.Visit(*this); } \n}; \n```\n\n`DirectoryNode`类用文件和子文件夹的列表来建模一个文件夹。我们使用智能指针来存储条目。像往常一样，我们也实现了所有与`EntryNode`类相关的虚拟功能。方法`AddFile`和`AddDirectory`意在填充列表。当使用操作系统特定的函数遍历目录时，我们用前面两种方法填充目录:\n\n```cpp\n//------Directory Helper Has to be written for Each OS \nclass DirHelper { \n public: \n    static  DirectoryNode  *SearchDirectory(const std::string& refcstrRootDirectory){ \n           //--------------- Do some OS specific stuff to retrieve \n           //--------------- File/Folder hierarchy from the root folder \n           return DirNode; \n}}; \n```\n\n`DirHelper`逻辑在 Windows 和 GNU Linux/macOS X 之间有所不同，我们从书中省略了源代码。相关网站包含上述程序的完整源代码。基本上，程序递归遍历目录来填充数据结构，如下所示:\n\n```cpp\n///////////////////////////////////// \n//----- A Visitor Interface that prints \n//----- The contents of a Folder \nclass PrintFolderVisitor : public IFileFolderVisitor \n{ \n  public: \n    void Visit(FileNode& fn ) {cout << fn.getname() << endl; } \n    void Visit(DirectoryNode& dn ) { \n      cout << \"In a directory \" << dn.getname() << endl; \n      list<unique_ptr<EntryNode>>& ls = dn.GetAllFiles(); \n      for ( auto& itr : ls ) { itr.get()->Accept(*this);} \n    } \n}; \n```\n\n`PrintFolderVisitor`类是一个访问者实现，它将文件和文件夹信息吐到控制台。该类演示了如何为组合实现基本访问者。在我们的例子中，组合只有两个节点，编写访问者实现非常容易。在某些情况下，层次结构中的节点类型数量众多，编写一个访问者实现并不容易。为访问者编写过滤器和转换可能会很困难，而且逻辑是临时的。让我们编写一个程序来打印文件夹的内容。这是:\n\n```cpp\nvoid TestVisitor( string directory ){ \n  // Search files including subdirectories \n  DirectoryNode *dirs = DirHelper::SearchDirectory(directory); \n  if ( dirs == 0 ) {return;} \n  PrintFolderVisitor *fs = new PrintFolderVisitor (); \n  dirs->Accept(*fs); delete fs; delete dirs; \n} \n```\n\n前面的函数递归遍历一个目录并创建一个组合(`DirectoryNode *`)。我们使用`PrintFolderVisitor`打印文件夹的内容，如下图所示:\n\n```cpp\nint main(int argc, char *argv[]) {  TestVisitor(\"D:\\Java\"); }\n```\n\n# 展平层次结构以浏览它们\n\n访问者实现必须对组合的结构有所了解。在复合实现的某些情况下，将有许多访问者需要实现。此外，在访问者界面的情况下，在节点上应用转换和过滤器有点困难。GOF 模式目录有一个迭代器模式，可以用来导航一系列项目。问题是:我们如何使用迭代器模式来线性化处理层次结构？大多数层次结构都可以通过编写访问者实现来展平为列表、序列或流。让我们为上述任务编写一个扁平化的访问者。\n\n看看下面的代码:\n\n```cpp\n// Flatten the File/Folders into a linear list \nclass FlattenVisitor : public IFileFolderVisitor{ \n    list <FileInformation> files; \n    string CurrDir; \n public: \n    FlattenVisitor() { CurrDir = \"\";} \n    ~FlattenVisitor() { files.clear();} \n    list<FileInformation> GetAllFiles() { return files; } \n    void Visit(FileNode& fn ) { \n       files.push_back( FileInformation{ CurrDir +\"\\\" + fn.getname(),fn.getSize())); \n    } \n    void Visit(DirectoryNode& dn ) { \n        CurrDir = dn.getname(); \n        files.push_back( FileInformation( CurrDir, 0 )); \n        list<unique_ptr<EntryNode>>& ls = dn.GetAllFiles(); \n        for ( auto& itr : ls ) { itr.get()->Accept(*this);} \n    } \n}; \n```\n\n`FlattenVisitor`类收集 STL 列表中的文件和文件夹。对于每个目录，我们遍历文件列表并发出`accept`方法。使用熟悉的双分派，让我们编写一个函数，返回一个`FileInformation`列表供我们迭代。下面是代码:\n\n```cpp\nlist<FileInformation> GetAllFiles(string dirname ){ \n   list<FileInformation> ret_val; \n   // Search files including subdirectories \n   DirectoryNode *dirs =DirHelper::SearchDirectory(dirname); \n   if ( dirs == 0 ) {return ret_val;} \n   FlattenVisitor *fs = new FlattenVisitor(); \n   dirs->Accept(*fs); \n   ret_val = fs->GetAllFiles(); \n   delete fs; delete dirs; \n   return ret_val; \n} \nint main(int argc, char *argv[]) { \n  list<FileInformation> rs = GetAllFiles(\"D:\\JAVA\"); \n  for( auto& as : rs ) \n    cout << as.name << endl; \n} \n```\n\n`FlattenVisitor`类遍历`DirectoryNode`层次结构，并将完全扩展的路径名收集到 STL 列表结构中。一旦我们将层次线性化为一个列表，我们就可以迭代它。\n\n我们已经学习了如何将层次结构建模为复合结构，并最终将其展平为适合用迭代器模式导航的形式。在下一节中，我们将学习迭代器如何被转换成可观察的。我们将使用 RxCpp 通过使用火来实现可观察对象，并忘记模型，将值从源推送到接收器。\n\n# 从迭代器到观察器\n\n迭代器模式是从 STL 容器、生成器和流中提取数据的标准机制。它们非常适合空间中聚合的数据。本质上，这意味着我们提前知道应该检索多少数据，或者数据已经被捕获。在有些情况下，数据异步到达，消费者不知道有多少数据，也不知道数据何时到达。在这种情况下，迭代器需要等待，或者我们需要借助超时策略来处理场景。在这种情况下，基于推送的方法似乎是更好的选择。利用 Rx 的主题结构，我们可以使用一种先发制人的策略。让我们编写一个发出目录内容的类，如下所示:\n\n```cpp\n////////////////////////////// \n// A Toy implementation of Active  \n// Object Pattern... \ntemplate <class T> \nstruct ActiveObject { \n    rxcpp::subjects::subject<T> subj; \n    // fire-and-forget \n    void FireNForget(T & item){subj.get_subscriber().on_next(item);} \n    rxcpp::observable<T> GetObservable()  \n    { return subj.get_observable(); } \n    ActiveObject(){}  \n    ~ActiveObject() {} \n}; \n/////////////////////// \n// The class uses a FireNForget mechanism to  \n// push data to the Data/Event sink \n// \nclass DirectoryEmitter { \n      string rootdir; \n      //-------------- Active Object ( a Pattern in it's own right ) \n      ActiveObject<FileInformation> act; // more on this below  \n  public: \n      DirectoryEmitter(string s )   { \n         rootdir = s; \n         //----- Subscribe  \n         act.GetObservable().subscribe([] ( FileInformation item ) { \n            cout << item.name << \":\" << item.size << endl; \n         }); \n      } \n      bool Trigger() { \n           std::packaged_task<int()> task([&]() {  EmitDirEntry(); return 1; }); \n           std::future<int> result = task.get_future(); \n           task(); \n           //------------ Uncomment the below line  \n           //------------ to return immediately \n           double dresult = result.get(); \n           return true; \n      } \n      //----- Iterate over the list of files  \n      //----- uses ActiveObject Pattern to do FirenForget \n      bool EmitDirEntry() { \n           list<FileInformation> rs = GetAllFiles(rootdir); \n           for( auto& a : rs ) { act.FireNForget(a); } \n           return false; \n      } \n}; \nint main(int argc, char *argv[]) { \n  DirectoryEmitter emitter(\"D:\\JAVA\"); \n  emitter.Trigger(); return 0; \n} \n```\n\n`DirectoryEmitter`类使用现代 C++ 的`packaged_task`构造以一种火了就忘的方式进行异步调用。在前面的列表中，我们正在等待结果(使用`std::future<T>`)。我们可以取消订单，立即返回。\n\n# 细胞模式\n\n我们已经了解到，反应式编程就是处理随时间变化的值。他们以可观察的概念为中心。有两种变体，如下所示:\n\n*   单元格:单元格是一个实体(变量或内存位置)，其中的值会随着时间的推移定期更新。在某些情况下，它们也被称为属性或行为。\n*   流:流代表事件流。它们是经常与动作相关联的数据。当人们想到可观测值时，他们得到了可观测值的流变体。\n\n我们将实现一个玩具版的细胞编程模式。我们将只专注于实现基本功能。代码需要整理以供生产使用。\n\n如果我们正在实现一个单元控制器类，当每个单元发生变化时，每个单元都会通知，那么下面的实现可以被优化。然后，单元控制器类可以通过计算表达式来更新依赖关系。这个实现展示了单元模式如何成为独立计算的可行机制:\n\n```cpp\n//------------------ CellPattern.cpp \n#include <rxcpp/rx.hpp> \n#include <memory> \n#include <map> \n#include <algorithm> \nusing namespace std; \nclass Cell \n{ \n  private: \n    std::string name; \n    std::map<std::string,Cell *> parents; \n    rxcpp::subjects::behavior<double> *behsubject;   \n  public: \n    string get_name() { return name;} \n    void SetValue(double v )  \n    { behsubject->get_subscriber().on_next(v);} \n    double GetValue()  \n    { return behsubject->get_value(); } \n    rxcpp::observable<double> GetObservable()  \n    { return behsubject->get_observable(); } \n    Cell(std::string pname) { \n       name = pname; \n       behsubject = new rxcpp::subjects::behavior<double>(0); \n    } \n    ~Cell() {delete behsubject; parents.clear();} \n    bool GetCellNames( string& a , string& b ) \n    { \n         if ( parents.size() !=2 ) { return false; } \n         int i = 0; \n         for(auto p  : parents ) { \n            ( i == 0 )? a = p.first : b = p.first; \n            i++ ;      \n         } \n         return true; \n    } \n    ///////////////////////////// \n    // We will just add two parent cells... \n    // in real life, we need to implement an  \n    // expression evaluator \n    bool Recalculate() { \n        string as , bs ; \n        if (!GetCellNames(as,bs) ) { return false; } \n        auto a = parents[as]; \n        auto b = parents[bs]; \n        SetValue( a->GetValue() + b->GetValue() ); \n        return true; \n    } \n    bool Attach( Cell& s ) { \n       if ( parents.size() >= 2 ) { return false; } \n       parents.insert(pair<std::string,Cell *>(s.get_name(),&s)); \n       s.GetObservable().subscribe( [=] (double a ) { Recalculate() ;}); \n       return true; \n    } \n    bool Detach( Cell& s ) { //--- Not Implemented  \n    } \n}; \n```\n\n单元格类假设每个单元格都有两个父依赖项，只要父值发生变化，单元格的值就会被重新计算。我们实现了一个加法运算符(以保持列表的小)。`recalculate`方法实现逻辑，如下图所示:\n\n```cpp\nint main(int argc, char *argv[]) {     \n    Cell a(\"a\");  \n    Cell b(\"b\"); \n    Cell c(\"c\"); \n    Cell d(\"d\"); \n    Cell e(\"e\"); \n    //-------- attach a to c \n    //-------- attach b to c \n    //-------- c is a + b  \n    c.Attach(a); \n    c.Attach(b); \n    //---------- attach c to e \n    //---------- attach d to e \n    //---------- e is c + d or e is a + b + d; \n    e.Attach(c); \n    e.Attach(d); \n    a.SetValue(100);  // should print 100 \n    cout << \"Value is \" << c.GetValue() << endl; \n    b.SetValue(200);  // should print 300 \n    cout << \"Value is \" << c.GetValue() << endl; \n    b.SetValue(300);  // should print 400 \n    cout << \"Value is \" << c.GetValue() << endl; \n    d.SetValue(-400); // should be Zero \n    cout << \"Value is \" << e.GetValue() << endl; \n} \n```\n\n主程序演示了我们如何使用单元模式将更改向下传播到依赖项中。通过更改值，我们强制重新计算从属单元格中的值。\n\n# 活动对象模式\n\n活动对象是一个将方法调用和方法执行解耦的类，非常适合激发和忘记异步调用。附加到类的调度程序处理执行请求。该模式由以下六个要素组成:\n\n*   一个代理，它为具有公共可访问方法的客户端提供了一个接口\n*   定义活动对象上的方法请求的接口\n*   来自客户端的挂起请求列表\n*   调度程序，决定下一步执行什么请求\n*   活动对象方法的实现\n*   客户端接收结果的回调或变量\n\n我们将剖析活动对象模式的一个实现。这个程序是为了说明而写的；对于生产使用，我们需要使用更复杂一点的。尝试产品质量实现会使代码变得相当长。让我们看看代码:\n\n```cpp\n#include <rxcpp/rx.hpp> \n#include <memory> \n#include <map> \n#include <algorithm> \n#include <string> \n#include <vector> \n#include <windows.h> \n#include <functional> \n#include <thread> \n#include <future> \nusing namespace std; \n//------- Active Object Pattern Implementation \ntemplate <class T> \nclass ActiveObject { \n    //----------- Dispatcher Object \n    rxcpp::subjects::subject<T> subj; \n    protected: \n    ActiveObject(){ \n       subj.get_observable().subscribe([=] (T s ) \n       { Execute(s); }); \n    }  \n    virtual void Execute(T s) {} \n    public: \n    // fire-and-forget \n    void FireNForget(T item){ subj.get_subscriber().on_next(item);} \n    rxcpp::observable<T> GetObservable() { return subj.get_observable(); } \n    virtual ~ActiveObject() {} \n}; \n```\n\n前面的实现声明了`subject<T>`类的一个实例，作为一个通知机制。`FireNForget`方法通过调用`get_subscriber`方法将值放入主题中。方法立即返回，订阅方法将检索该值并调用`Execute`方法。这个类应该被一个具体的实现覆盖。让我们看看代码:\n\n```cpp\nclass ConcreteObject : public ActiveObject<double> { \n    public: \n     ConcreteObject() {} \n     virtual void Execute(double a ) { cout << \"Hello World.....\" << a << endl;} \n}; \nint main(int argc, char *argv[]) { \n  ConcreteObject temp; \n  for( int i=0; i<=10; ++ i ) \n      temp.FireNForget(i*i); \n  return 0; \n}\n```\n\n前面的代码片段调用了带有双精度值的`FireNForget`方法。在控制台上，我们可以看到正在打印的值。被覆盖的`Execute`方法被自动调用。\n\n# 资源贷款模式\n\n顾名思义，贷款模式将资源贷款给你的职能部门。它执行以下步骤:\n\n1.  它创建了一个您可以使用的资源\n2.  它将资源借给将要使用它的功能\n3.  这个函数由调用者传递\n4.  资源被破坏了\n\n下面的代码实现了资源管理的资源借出模式。该模式有助于在编写代码时避免资源泄漏:\n\n```cpp\n//----------- ResourceLoan.cpp \n#include <rxcpp/rx.hpp> \nusing namespace std; \n////////////////////////// \n// implementation of Resource Loan  Pattern. The Implementation opens a file \n// and does not pass the file handle to user  defined Lambda. The Ownership remains with \n// the class  \nclass ResourceLoan { \n   FILE *file; \n   string filename; \n  public: \n     ResourceLoan(string pfile) { \n        filename = pfile; \n        file = fopen(filename.c_str(),\"rb\"); \n     }   \n     //////////////////////////// \n     // Read upto 1024 bytes to a buffer  \n     // return the buffer contents and number of bytes \n     int ReadBuffer( std::function<int(char pbuffer[],int val )> func ) \n     { \n          if (file == nullptr ) { return -1; } \n          char buffer[1024]; \n          int result = fread (buffer,1,1024,file); \n          return func(buffer,result); \n     }  \n     //---------- close the resource \n     ~ResourceLoan() { fclose(file);} \n}; \n//////////////////////////////// \n// A Sample Program to invoke the preceding \n// class \n// \nint main(int argc, char *argv[]) { \n  ResourceLoan res(\"a.bin\"); \n  int nread ; \n  //------------- The conents of the buffer \n  //------------- and size of buffer is stored in val \n  auto rlambda =  [] (char buffer[] , int val ) { \n       cout <<  \"Size \" << val << endl; \n       return val; \n  }; \n  //------- The File Handle is not available to the  \n  //------- User defined Lambda \n  while ((nread = res.ReadBuffer(rlambda)) > 0) {} \n  //---- When the ResourceLoan object goes out of scope \n  //---- File Handle is closed \n  return 0; \n} \n```\n\n资源贷款模式适合避免资源泄漏。资源的持有者从不把资源的句柄或指针交给消费者。主程序演示了我们如何使用实现。\n\n# 事件总线模式\n\n事件总线充当事件源和事件接收器之间的中介。事件源或生产者将事件发送到总线，订阅事件的类(消费者)将得到通知。模式可以是中介模式的一个实例。在事件总线实现中，我们有以下内容:\n\n*   **产生者**:产生事件的类\n*   **消费者**:消费事件的类\n*   **控制器**:作为生产者和消费者的类\n\n在接下来的实现中，我们省略了控制器的实现。下面的代码实现了一个事件总线:\n\n```cpp\n//----------- EventBus.cpp \n#include <rxcpp/rx.hpp> \n#include <memory> \n#include <map> \n#include <algorithm> \nusing namespace std; \n//---------- Event Information \nstruct EVENT_INFO{ \n   int id; \n   int err_code; \n   string description; \n   EVENT_INFO() { id = err_code = 0 ; description =\"default\";} \n   EVENT_INFO(int pid,int perr_code,string pdescription ) \n   { id = pid; err_code = perr_code; description = pdescription; } \n   void Print() { \n      cout << \"id & Error Code\" << id << \":\" << err_code << \":\"; \n      cout << description << endl; \n   } \n}; \n```\n\n`EVENT_INFO`结构建模一个事件，它有以下内容:\n\n*   `Id`:事件标识\n*   `err_code`:错误代码\n*   `description`:事件描述\n\n代码的其余部分相当明显；这是:\n\n```cpp\n//----------- The following method \n//----------- will be invoked by  \n//----------- Consumers \ntemplate <class T> \nvoid DoSomeThingWithEvent( T ev ) \n{ev.Print();} \n\n//---------- Forward Declarations  \ntemplate <class T> \nclass EventBus; \n//------------- Event Producer \n//------------- Just Inserts event to a Bus \ntemplate <class T> \nclass Producer { \n  string name; \n public: \n   Producer(string pname ) { name = pname;} \n   bool Fire(T ev,EventBus<T> *bev ) { \n         bev->FireEvent(ev); \n         return false; \n   } \n}; \n```\n\n生产者类的实现相当简单。骨架实现相当琐碎。`Fire`方法以兼容的`EventBus<T>`为参数，调用`EventBus<T>`类的`FireEvent`方法。生产实现需要一些花哨的东西。让我们看看代码:\n\n```cpp\n//------------ Event Consumer \n//------------ Subscribes to a Subject \n//------------ to Retrieve Events \ntemplate <class T> \nclass Consumer { \n  string name; \n  //--------- The subscription member helps us to \n  //--------- Unsubscribe to an Observable  \n  rxcpp::composite_subscription subscription; \npublic: \n  Consumer(string pname) { name = pname;} \n  //--------- Connect a Consumer to a Event Bus \n  bool Connect( EventBus<T> *bus ) { \n      //------ If already subscribed, Unsubscribe! \n      if ( subscription.is_subscribed() ) \n             subscription.unsubscribe(); \n      //------- Create a new Subscription \n      //------- We will call DoSomeThingWithEvent method \n      //------- from Lambda function \n      subscription = rxcpp::composite_subscription(); \n      auto subscriber = rxcpp::make_subscriber<T>( \n        subscription,[=](T value){ \n            DoSomeThingWithEvent<T>(value); \n        },[](){ printf(\"OnCompletedn\");}); \n      //----------- Subscribe! \n      bus->GetObservable().subscribe(subscriber); \n      return true; \n  } \n  //-------- DTOR ....Unsubscribe \n  ~Consumer() { Disconnect(); } \n  bool Disconnect() {\n```\n\n```cpp\n   if (subscription.is_subscribed() ) \n        subscription.unsubscribe(); \n  } \n}; \n```\n\n`Consumer<T>`的功能性相当明显。`Connect`方法的工作是在`EventBus<T>`课上订阅主题的可观察的一面。每次新的连接请求到来时，现有订阅都会被取消订阅，如下所示:\n\n```cpp\n//--- The implementation of the EventBus class \n//--- We have not taken care of Concurrency issues \n//--- as our purpose is to demonstrate the pattern \ntemplate <class T> \nclass EventBus \n{ \n  private: \n    std::string name; \n    //----- Reference to the Subject... \n    //----- Consumers get notification by  \n    //----- Subscribing to the Observable side of the subject \n    rxcpp::subjects::behavior<T> *replaysubject;  \n  public: \n    EventBus<T>() {replaysubject = new rxcpp::subjects::behavior<T>(T());} \n    ~EventBus() {delete replaysubject;} \n    //------ Add a Consumer to the Bus... \n    bool AddConsumer( Consumer<T>& b ) {b.Connect(this);} \n    //------ Fire the Event... \n    bool FireEvent ( T& event ) { \n       replaysubject->get_subscriber().on_next(event); \n       return true; \n    } \n    string get_name() { return name;} \n    rxcpp::observable<T> GetObservable()  \n    { return replaysubject->get_observable(); } \n}; \n```\n\n`EventBus<T>`充当生产者和消费者之间的管道。我们在引擎盖下使用一个`replaysubject`，来通知消费者，如下图所示:\n\n```cpp\n///////////////////// \n//The EntryPoint \n// \n// \nint main(int argc, char *argv[]) { \n    //---- Create an instance of the EventBus \n    EventBus<EVENT_INFO> program_bus; \n    //---- Create a Producer and Two Consumers \n    //---- Add Consumers to the EventBus \n    Producer<EVENT_INFO> producer_one(\"first\"); \n    Consumer<EVENT_INFO> consumer_one(\"one\"); \n    Consumer<EVENT_INFO> consumer_two(\"two\"); \n    program_bus.AddConsumer(consumer_one); \n    program_bus.AddConsumer(consumer_two); \n    //---- Fire an Event... \n    EVENT_INFO ev; \n    ev.id = 100; \n    ev.err_code = 0; \n    ev.description = \"Hello World..\"; \n    producer_one.Fire(ev,&program_bus); \n    //---- fire another by creating a second  \n    //---- Producer \n    ev.id = 100; \n    ev.err_code = 10; \n    ev.description = \"Error Happened..\"; \n    Producer<EVENT_INFO> producer_two(\"second\"); \n    producer_two.Fire(ev,&program_bus); \n} \n```\n\n在主功能中，我们执行以下任务:\n\n1.  创建`EventBus<T>`的实例\n2.  创建生产者的实例\n3.  创建消费者实例\n4.  将事件发送到总线\n\n我们只介绍了适合编写反应式程序的设计模式的一个子集。首先，我们的重点是将 GOF 设计模式与反应式编程世界联系起来。事实上，作者认为反应式编程模型是经典 GOF 设计模式的增强实现。由于现代编程语言增加了函数式编程结构，增强是可能的。事实上，对象/函数编程是编写现代 C++ 代码的好方法。这一章主要是基于这个想法。\n\n# 摘要\n\n在这一章中，我们深入研究了设计模式和习惯用法的奇妙世界。从 GOF 设计模式开始，我们转向反应式编程模式。我们讨论了诸如单元、活动对象、资源借出和事件总线等模式。从 GOF 模式过渡到反应式编程有助于你从更广泛的意义上看待反应式编程。\n\n在下一章中，我们将学习使用 C++ 进行微服务开发。"
  },
  {
    "path": "docs/cpp-react-prog/11.md",
    "content": "# 十一、使用 C++ 的反应式微服务\n\n到目前为止，我们已经讨论了使用 C++ 进行反应式编程的基本方面。涵盖的一些关键主题包括:\n\n*   反应式规划模型及其认知前提\n*   RxCpp 库及其编程模型\n*   使用 Qt/RxCpp 进行反应式图形用户界面编程\n*   设计模式和反应式编程模型\n\n如果你仔细看看，本书迄今为止的所有例子都与流程内部发生的事情有关。或者，我们主要关注共享内存并行和并发技术。Rx.net RxJava 主要关注共享内存并发和并行编程。像 Akka 这样的系统将反应式编程模型应用于分布式世界。在 Akka 中，我们可以编写跨越整个过程的反应逻辑。反应式编程模型也有利于公开和消费基于 REST 的 web 服务。RxJs 库主要用于从浏览器页面消费基于 REST 的服务。RxCpp 库可用于编写网络客户端，以聚合来自各种服务端点的内容。我们可以从控制台和图形用户界面应用中利用 RxCpp。另一个用例是聚合来自多个细粒度服务的数据，并将其交付给 web 客户端。\n\n在本章中，我们将使用 C++ 编写一个基本的 web 应用，它将利用 C++ REST SDK 编写服务器部分，并使用其客户端库来使用这些服务。在这个过程中，我们将解释什么是微服务以及如何消费它们。我们还将解释如何通过在`libcurl`库的顶部编写一个包装器来使用 RxCpp 访问 REST 端点和 HTML 页面。我们计划利用 Kirk Shoop 的 RxCurl 库(作为他的 Twitter 分析应用的一部分编写)来演示这项技术。\n\n# C++ 语言和网络编程\n\n如今，大多数以网络为中心的应用都是使用 Python、Java、C#、PHP 和其他高级语言开发的。但是，对于这些应用，人们放置反向代理，如 NGINX、Apache Web 服务器或 IIS 重定向器，来管理高级语言编写的应用的流量。所有这些反向代理都是用 C++ 编写的。同样，大多数网络浏览器和 HTTP 客户端库，如`libwww`、`libcurl`和`WinInet`，都是用 C++ 编写的。\n\nJava、(静态类型的)C#和其他动态语言(如 Python、Ruby 和 PHP)变得流行的一个原因是，这些语言支持反射能力(在静态语言的情况下，如 C#/Java)和鸭子类型(由动态语言支持)。这些特性帮助 web 应用服务器动态加载处理程序。通过搜索*反射 API**鸭子打字*等关键词来了解它们。\n\n# REST 编程模型\n\n代表代表状态转移的 REST 是罗伊·菲尔丁博士论文中率先提出的一种建筑风格。如今，它是公开和使用 web 服务的最流行的技术之一。REST 遵循以资源为中心的方法，并很好地映射到 CRUD 模式，这在精通编写企业业务应用的程序员中很受欢迎。我们在编写 REST 服务时使用 **JavaScript 对象符号**(也称为 **JSON** )作为负载，而不是 XML 格式(SOAP 服务流行的格式)。REST 编程模型依赖 HTTP 动词来指示在接收 REST API 调用时要执行的操作类型。支持的最流行的方法有:\n\n*   `POST`:创建新资源\n*   `GET`:检索资源\n*   `PUT`:更新现有资源(如果是新资源，行为类似`POST`)\n*   `DELETE`:删除资源\n\n# C++ REST 软件开发工具包\n\nC++ REST SDK 是一个微软项目，使用现代异步 C++ API 设计，以本机代码进行基于云的客户端-服务器通信。该项目旨在帮助 C++ 开发人员连接到服务并与之交互。SDK 具有以下功能，可帮助您编写健壮的服务:\n\n*   HTTP 客户端/服务器\n*   JSON\n*   异步流\n*   网络套接字的客户端\n*   oAuth\n\nC++ REST 软件开发工具包依赖于并行模式库的任务应用编程接口。PPL 任务是一个基于现代 C++ 特性的强大的异步操作组合模型。C++ REST SDK 支持 Windows 桌面、Windows 商店(UWP)、Linux、macOS、Unix、iOS 和 Android。\n\n# 使用 C++ REST 软件开发工具包的 HTTP 客户端编程\n\nC++ REST SDK 编程模型本质上是异步的，我们也可以以同步的方式调用 API 调用。下面的程序将演示我们如何异步调用 HTTP 客户端 API 调用。该程序演示了 C++ REST SDK 支持的 HTTP 协议客户端的工作方式。我们使用一种叫做**任务延续**(一种链接代码块的技术)的技术从网页中检索数据，并将其存储在本地磁盘文件中。C++ REST SDK 遵循异步 I/O 模型，我们将操作链接在一起。最后，我们使用`Wait`方法调用合成:\n\n```cpp\n#include <cpprest/http_client.h> \n#include <cpprest/filestream.h> \n#include <string> \n#include <vector> \n#include <algorithm> \n#include <sstream> \n#include <iostream> \n#include <fstream> \n#include <random> \n#include \"cpprest/json.h\" \n#include \"cpprest/http_listener.h\" \n#include \"cpprest/uri.h\" \n#include \"cpprest/asyncrt_utils.h\" \n//////////////////////////////////////////////// \n// A Simple HTTP Client to Demonstrate  \n// REST SDK Client programming model \n// The Toy sample shows how one can read  \n// contents of a web page \n// \nusing namespace utility;  // Common utilities like string conversions \nusing namespace web;      // Common features like URIs. \nusing namespace web::http;// Common HTTP functionality \nusing namespace web::http::client;// HTTP client features \nusing namespace concurrency::streams;// Asynchronous streams \n\nint main(int argc, char* argv[]) \n{ \n       auto fileStream = std::make_shared<ostream>(); \n   // Open stream to output file. \n   pplx::task<void> requestTask =  \n              fstream::open_ostream(U(\"google_home.html\")). \n         then([=](ostream outFile) \n   { \n         *fileStream = outFile; \n         // Create http_client to send the request. \n         http_client client(U(\"http://www.google.com\")); \n         // Build request URI and start the request. \n          uri_builder builder(U(\"/\")); \n         return client.request(methods::GET, builder.to_string()); \n\n   }).then([=](http_response response) \n   { \n         printf(\"Received response status code:%un\",  \n                                    response.status_code()); \n             return response.body(). \n                           read_to_end(fileStream->streambuf()); \n   }).then([=](size_t){ \n         return fileStream->close(); \n   }); \n\n       // We have not started execution, just composed \n       // set of tasks in a Continuation Style \n   // Wait for all the outstanding I/O to complete  \n       // and handle any exceptions, If any  \n   try \n   { \n              //-- All Taskss will get triggered here \n         requestTask.wait(); \n   } \n   catch (const std::exception &e) \n   { \n         printf(\"Error exception:%sn\", e.what()); \n   } \n        //---------------- pause for a key  \n   getchar(); \n\n   return 0; \n} \n```\n\n该程序演示了任务连续式编程的工作方式。大部分代码都是关于组合操作的，实际执行是从调用`wait()`方法开始的。我们也可以以同步的方式调用操作。请参考 C++ REST SDK 文档了解更多信息。\n\n# 使用 C++ REST 软件开发工具包进行 HTTP 服务器编程\n\n我们已经了解了 C++ REST SDK 支持的 HTTP 客户端编程模型。我们使用基于异步任务延续的应用编程接口来检索网页的内容，并将其保存到磁盘文件中。现在，是时候开始专注于 REST SDK HTTP 服务器编程了。C++ REST SDK 有一个处理 HTTP 请求的侦听器接口，我们可以为每种类型的 HTTP 动词放置处理程序，例如`GET`、`PUT`和`POST`:\n\n```cpp\n///////////////////////////////// \n//  A Simple Web Application with C++ REST SDK \n//  We can use Postman Or Curl to test the Server \nusing namespace std; \nusing namespace web; \nusing namespace utility; \nusing namespace http; \nusing namespace web::http::experimental::listener; \n///////////////////////////// \n// SimpleServer is a Wrapper over  \n// http_listener class available with C++ REST SDK \nclass SimpleServer \n{ \npublic: \n\n   SimpleServer(utility::string_t url); \n   ~SimpleServer() {} \n   pplx::task<void> Open() { return m_listener.open(); } \n   pplx::task<void> Close() { return m_listener.close(); } \n\nprivate: \n   //--- Handlers for HTTP verbs \n       void HandleGet(http_request message); \n   void HandlePut(http_request message); \n   void HandlePost(http_request message); \n   void HandleDelete(http_request message); \n   //--------------- The  HTTP listener class \n   http_listener m_listener; \n}; \n```\n\n`SimpleServer` C++ 类基本上是 C++ REST SDK 支持的`http_listener`类之上的包装器。该类侦听传入的 HTTP 请求，并且可以为每种请求类型设置请求处理程序(`GET`、`POST`、`PUT`等)。当请求到达时，`http_listener`会将请求信息发送给相关的处理程序:\n\n```cpp\n////////////////////////////////// \n// The Constructor Binds HTTP verbs to instance methods \n// Based on the naming convention, we can infer what is happening \nSimpleServer::SimpleServer(utility::string_t url) : m_listener(url) \n{ \n   m_listener.support(methods::GET, std::bind(&SimpleServer::HandleGet, \n               this, std::placeholders::_1)); \n   m_listener.support(methods::PUT, std::bind(&SimpleServer::HandlePut, \n               this, std::placeholders::_1)); \n   m_listener.support(methods::POST, std::bind(&SimpleServer::HandlePost,  \n               this, std::placeholders::_1)); \n   m_listener.support(methods::DEL, std::bind(&SimpleServer::HandleDelete,  \n                this, std::placeholders::_1)); \n\n} \n```\n\n前面的代码片段将请求处理程序绑定到`http_request`对象。我们只关注`GET`、`PUT`、`POST`和`DELETE`动词。这些动词是 REST 实现支持的最流行的命令:\n\n```cpp\n///////////////////////////////////// \n// For this implementation, what we do is  \n// spit the HTTP request details on the Server Console \n// and return 200 OK and a String which indicates  Success of Operations  \nvoid SimpleServer::HandleGet(http_request message){ \n   ucout << message.to_string() << endl; \n   message.reply(status_codes::OK,L\"GET Operation Succeeded\"); \n} \nvoid SimpleServer::HandlePost(http_request message){ \n   ucout << message.to_string() << endl; \n   message.reply(status_codes::OK, L\"POST Operation Succeeded\"); \n}; \n\nvoid SimpleServer::HandleDelete(http_request message){ \n   ucout << message.to_string() << endl; \n   message.reply(status_codes::OK, L\"DELETE Operation Succeeded\"); \n} \nvoid SimpleServer::HandlePut(http_request message){ \n   ucout << message.to_string() << endl; \n   message.reply(status_codes::OK, L\"PUT Operation Succeeded\"); \n}; \n```\n\n前面的代码块遵循一种任何开发人员都可以轻松破译的模式。处理程序所做的只是将请求参数打印到服务器的控制台上，并向客户端返回一个字符串来指示操作成功。我们将展示如何通过 POSTMAN 和 CURL 实用程序访问这些服务:\n\n```cpp\n//////////////////////////////// \n// A Smart Pointer for Server Instance... \n// \nstd::unique_ptr<SimpleServer> g_http; \n////////////////////////////////////////////////// \n// STart the Server with the Given URL \n// \nvoid StartServer(const string_t& address) \n{ \n   // Build our listener's URI from the address given \n   // We just append DBDEMO/ to the base URL \n   uri_builder uri(address); \n   uri.append_path(U(\"DBDEMO/\")); \n   auto addr = uri.to_uri().to_string(); \n   ///////////////////////////////// \n   // Create an Instance of the Server and Invoke Wait to  \n   // start the Server... \n   g_http = std::unique_ptr<SimpleServer>(new SimpleServer(addr)); \n   g_http->Open().wait(); \n   //---- Indicate the start and spit URI to the Console \n   ucout << utility::string_t(U(\"Listening for requests at: \")) <<  \n                addr << std::endl; \n\n   return; \n} \n\n//////////////////////////////////////// \n// Simply Closes the Connection... Close returns  \n// pplx::task<void> ...we need to Call wait to invoke the  \n// operation... \nvoid ShutDown(){ \n   g_http->Close().wait(); \n   return; \n} \n/////////////////////////////// \n// EntryPoint function \nint wmain(int argc, wchar_t *argv[]) \n{ \n   utility::string_t port = U(\"34567\"); \n   if (argc == 2){ port = argv[1];} \n   //--- Create the Server URI base address \n   utility::string_t address = U(\"http://localhost:\"); \n   address.append(port); \n   StartServer(address); \n   std::cout << \"Press ENTER to exit.\" << std::endl; \n   //--- Wait Indefenintely, Untill some one has  \n   // pressed a key....and Shut the Server down \n   std::string line; \n   std::getline(std::cin, line); \n   ShutDown(); \n   return 0; \n} \n```\n\n主函数通过`StartServer`函数实例化`SimpleListener`的 *n* 实例。然后，`main`功能在调用`ShutDown`功能之前等待按键。一旦我们启动了应用，我们就可以使用`CURL`工具或者邮差来测试程序是如何工作的。\n\n# 使用 CURL 和 POSTMAN 测试 HTTP 服务器\n\n`CURL`是一个命令行工具，可以跨 Windows、GNU Linux、macOS 和其他符合 POSIX 的系统移植。该工具有助于使用各种基于 TCP/IP 的应用协议传输数据。支持的一些常见协议包括 HTTP、HTTPS、FTP、FTPS、SCP、SFTP、TFTP、DICT、TELNET 和 LDAP。\n\n我们将使用`CURL`工具来测试我们编写的 HTTP 服务器。可以通过提供必要的命令行参数来调用命令行实用程序，以便将 HTTP 请求与相关联的谓词放在一起。我们向我们编写的服务器提供调用`GET`和`PUT`请求的命令行参数:\n\n```cpp\n    curl -X PUT http://localhost:34567/DBDEMO/  -H \"Content-Type: application/json\" -d '{\"SimpleContent\":\"Value\"}'\n    curl -X GET -H \"Content-Type: application/json\"  http://localhost:34567/DBDEMO/\n\n```\n\n根据您的平台，将前面的命令嵌入到批处理文件或 shell 脚本中。控制台上的输出应该如下所示:\n\n```cpp\nPUT Operation Succeeded\nGET Operation Succeeded\n```\n\n同样，通过查阅`CURL`文档，我们也可以测试其他 HTTP 动词。\n\nPOSTMAN 是一个强大的 HTTP 客户端，用于测试基于 HTTP 的服务。它最初是一个名叫阿比纳夫·阿斯特哈纳的印度开发商的附属项目；这是一个在网上疯传的 Chrome 插件。今天，它是一个独立的平台，并且存在一个围绕应用组建的公司，Asthana 是该公司的首席执行官。您可以下载 POSTMAN 工具来测试这些服务。\n\n# libcurl 和 HTTP 客户端编程\n\n我们已经遇到了 CURL 实用程序。CURL 实用程序是`libcurl`库顶部的包装器。我们将在本章中使用该库来访问 REST 服务。为了让您熟悉编程模型，我们将使用库编写一个基本的 HTTP 客户端:\n\n```cpp\n/////////////////////////////////// \n// A Simple Program to demonstrate  \n// the usage of libcurl library \n// \n#include <stdio.h> \n#include <curl/curl.h> \n/////////////////////// \n// Entrypoint for the program \n//  \nint main(void) \n{ \n  CURL *curl; \n  CURLcode res; \n  /////////////////////////// \n  // Initialize the library \n  // \n  curl = curl_easy_init(); \n  if(curl) { \n    //----------- Set the URL  \n    curl_easy_setopt(curl, CURLOPT_URL,  \n                     \"http://example.com\"); \n    ////////////////////////////////////////// \n    // To support URL re-direction, we need to configure \n    // the lib curl library with CURLOPT_FOLLOWLOCATION \n    //  \n    curl_easy_setopt(curl,  \n               CURLOPT_FOLLOWLOCATION, 1L); \n\n    /////////////////////////////////////////////////// \n    // Now that, we have setup the options necessary, \n    // invoke the operation to pull data  \n    // \n    res = curl_easy_perform(curl); \n\n    if(res != CURLE_OK) { \n      //----- if error, print the error on console \n      cout << \"curl_easy_perform() failed: \" \n              << curl_easy_strerror(res) << endl; \n    } \n    curl_easy_cleanup(curl); \n  } \n  return 0; \n} \n```\n\n之前的代码通过 ping[http://example.com](http://example.com)的网址来检索其内容，并将它们打印到控制台。编程模型非常简单，库的文档也非常好。它是访问 TCP/IP 应用服务的最流行的库之一。\n\n# 柯克·肖普的 CURL 包装库\n\nRxCpp 库的主要实现者是 Kirk Shoop，他目前与微软有关联。他写了一个推特分析示例应用([https://github.com/kirkshoop/twitter](https://github.com/kirkshoop/twitter))来展示反应式编程的各个方面。作为计划的一部分，他做的事情之一是在`libcurl`上编写一个反应式包装器来实现 HTTP `GET`和`POST`方法。这本书的作者扩展了代码来支持`PUT`和`DELETE`方法。\n\n看看这本书源代码捆绑的`RxCurl`库:\n\n```cpp\n////////////////////////////////////////// \n// A Simple program to pull HTTP conent  \n// using a Rx wrapper on top of the Libcurl \n// \n// \n#include <iostream> \n#include <stdio.h> \n#include <stdlib.h> \n#include <map> \n#include <chrono> \nusing namespace std; \nusing namespace std::chrono; \n//////////////////////// \n// include Curl Library and  \n// Rxcpp library  \n// \n#include <curl/curl.h> \n#include <rxcpp/rx.hpp> \nusing namespace rxcpp; \nusing namespace rxcpp::rxo; \nusing namespace rxcpp::rxs; \n////////////////////////// \n// include the modified rxcurl library from  \n// Kirk Shoop's Twitter Analysis app \n// \n#include \"rxcurl.h\" \nusing namespace rxcurl; \nint main() { \n     ///////////////////////////////////// \n     // \n     // Create a factory object to create  \n     // HTTP request.  The http_request structure \n     // is defined in rxcurl.h \n     string url = \"http://example.com\"; \n     auto factory = create_rxcurl(); \n     auto request  = factory.create(http_request{url, \"GET\",{}, {}}) | \n            rxo::map([](http_response r){ \n                return r.body.complete; \n            });\n```\n\n我们使用`factory`类创建了一个`observable`来创建 HTTP `request`对象。`map`功能只是检索响应对象的主体。整个代码中最重要的结构是`http_request`结构，其定义如下:\n\n```cpp\nstruct http_request{ \n                      string url; \n                      string method; \n                      std::map<string, string> headers; \n                      string body; \n}; \n     //////////////////////////////////////// \n     // make a blocking call to the url.. \n     observable<string>   response_message; \n     request.as_blocking().subscribe([&] (observable<string> s) { \n               response_message = s.sum(); \n     } ,[] () {}); \n```\n\n使用以`observable<string>`为`map`函数返回`observable<string>`的 Lambda 函数，可以为`on_next`订阅`request`可观测值。在`on_next`函数的主体中，我们使用`observable<string>::sum()`减速器聚合内容以生成字符串:\n\n```cpp\n     /////////////////////////////// \n     // retrieve the html content form the site  \n     string html; \n     response_message.as_blocking().subscribe( [&html] ( string temp ) {          \n                   html = temp; \n     }, [&html] () { } ); \n     //------------ Print to the Console... \n     cout << html << endl; \n} \n```\n\n`response_message`可观测值由一个λ订阅，该λ将字符串作为一个参数。在`on_next`函数的主体中，我们只需将包含 HTML 的字符串分配给`html`变量。最后，我们将该值打印到控制台。请看`rxcurl.h`头文件，看看库是怎么工作的。\n\n# JSON 和 HTTP 协议\n\n用于调用 web 服务的负载格式曾经被 XML 格式垄断。基于 SOAP 的服务大多支持 XML 格式。随着基于 REST 的服务的出现，开发人员使用 **JavaScript 对象符号** ( **JSON** )作为有效载荷格式。下表显示了 XML 和相应的 JSON 对象之间的比较:\n\n| **XML** | JSON |\n| \n\n```cpp\n<person>\n  <firstName>John</firstName>\n  <lastName>Smith</lastName>\n  <age>25</age>\n  <address>\n    <streetAddress>21 2nd \nStreet</streetAddress>\n    <city>New York</city>\n    <state>NY</state>\n    <postalCode>10021</postalCode>\n  </address>\n  <phoneNumber>\n    <type>home</type>\n    <number>212 555-1234</number>\n  </phoneNumber>\n  <phoneNumber>\n    <type>fax</type>\n    <number>646 555-4567</number>\n  </phoneNumber>\n  <gender>\n    <type>male</type>\n  </gender>\n</person>\n\n```\n\n | \n\n```cpp\n{\n  \"firstName\": \"John\",\n  \"lastName\": \"Smith\",\n  \"age\": 25,\n  \"address\": {\n    \"streetAddress\": \"21 2nd Street\",\n    \"city\": \"New York\",\n    \"state\": \"NY\",\n    \"postalCode\": \"10021\"\n  },\n  \"phoneNumber\": [\n    {\n      \"type\": \"home\",\n      \"number\": \"212 555-1234\"\n    },\n    {\n      \"type\": \"fax\",\n      \"number\": \"646 555-4567\"\n    }\n  ],\n  \"gender\": {\n    \"type\": \"male\"\n  }\n}\n```\n\n |\n\nJSON 格式包含以下数据类型:\n\n*   线\n*   数字\n*   对象(JSON 对象)\n*   排列\n*   布尔代数学体系的\n\n下面的 JSON 对象，我们已经覆盖了前面的大部分数据类型。映射如下:\n\n*   `name`:值为字符串类型(`\"john\"`)\n*   `age`:数值为数字(`35`)\n*   `spouse`:这是一个 JSON 对象\n*   `siblings`:这是一个数组\n*   `employed`:这是布尔(`true`)\n\n代码如下:\n\n```cpp\n{ \n { \"name\":\"John\" }, \n { \"age\":35 }, \n { \n   \"spouse\":{ \"name\":\"Joanna\",  \n              \"age\":30,  \n              \"city\":\"New York\" } \n }, \n { \n    \"siblings\":[\"Bob\", \"Bill\", \"Peter\" ] \n }, \n { \"employed\":true } \n} \n```\n\n现在我们对 JSON 及其核心方面有了更好的理解，我们将编写一个简单的程序来演示 JSON API 的用法，它是 REST SDK 的一部分:\n\n```cpp\n/////////////////////////////////// \n// A Console Application to demonstrate JSON API \n// available as part of the C++ SDK \nusing namespace std; \nusing namespace web; \nusing namespace utility; \nusing namespace http; \nusing namespace web::http::experimental::listener; \n/////////////////////////////////////// \n// Define a Simple struct to demonstrate the  \n// Working of JSON API \nstruct EMPLOYEE_INFO{ \n   utility::string_t name; \n   int age; \n   double salary; \n   ///////////////////////////////// \n   // Convert a JSON Object to a C++ Struct \n   // \n   static EMPLOYEE_INFO JSonToObject(const web::json::object & object){ \n         EMPLOYEE_INFO result; \n         result.name = object.at(U(\"name\")).as_string(); \n         result.age = object.at(U(\"age\")).as_integer(); \n         result.salary = object.at(U(\"salary\")).as_double(); \n         return result; \n   } \n```\n\n`JSonToObject`静态方法将 JSON 对象转换为`EMPLOYEE_INFO`结构。`json::at`根据我们用来索引的字符串返回对`json::value`的引用。结果`json::value`引用用于调用类型特定的转换方法，如`as_string`、`as_integer`和`as_double`:\n\n```cpp\n   /////////////////////////////////////////// \n   // Convert a C++ struct to a Json Value \n   // \n   web::json::value ObjectToJson() const{ \n         web::json::value result = web::json::value::object(); \n         result[U(\"name\")] = web::json::value::string(name); \n         result[U(\"age\")] = web::json::value::number(age); \n         result[U(\"salary\")] = web::json::value::number(salary); \n         return result; \n   } \n}; \n```\n\n`ObjectToJson`是`EMPLOYEE_STRUCT`的一个实例方法，有助于从实例数据中产生 JSON 输出。这里，我们使用转换方法将实例数据传输到`json::value`。接下来，我们将关注如何从头开始创建`json::object`:\n\n```cpp\n///////////////////////////////////////// \n// Create a Json Object group and Embed and  \n// Array in it... \nvoid MakeAndShowJSONObject(){ \n   // Create a JSON object (the group) \n   json::value group; \n   group[L\"Title\"] = json::value::string(U(\"Native Developers\")); \n   group[L\"Subtitle\"] =  \n              json::value::string(U(\"C++ devekioers on Windws/GNU LINUX\")); \n   group[L\"Description\"] =  \n               json::value::string(U(\"A Short Description here \")); \n   // Create a JSON object (the item) \n   json::value item; \n   item[L\"Name\"] = json::value::string(U(\"Praseed Pai\")); \n   item[L\"Skill\"] = json::value::string(U(\"C++ / java \")); \n   // Create a JSON object (the item) \n   json::value item2; \n   item2[L\"Name\"] = json::value::string(U(\"Peter Abraham\")); \n   item2[L\"Skill\"] = json::value::string(U(\"C++ / C# \")); \n   // Create the items array \n   json::value items; \n   items[0] = item; \n   items[1] = item2; \n   // Assign the items array as the value for the Resources key \n   group[L\"Resources\"] = items; \n   // Write the current JSON value to wide char string stream \n   utility::stringstream_t stream; \n   group.serialize(stream); \n   // Display the string stream \n   std::wcout << stream.str(); \n} \n\nint wmain(int argc, wchar_t *argv[]) \n{ \n   EMPLOYEE_INFO dm; \n   dm.name = L\"Sabhir Bhatia\"; \n   dm.age = 50; \n   dm.salary = 10000; \n   wcout << dm.ObjectToJson().serialize() << endl; \n```\n\n我们创建一个`EMPLOYEE_INFO`结构，并将一些值赋给字段。然后我们调用`EMPLOYEE_INFO::ObjectToJSon()`来创建一个`json::value`对象。我们调用`serialize()`方法来生成 JSON 文本输出:\n\n```cpp\n      utility::string_t port =  \n           U(\"{\"Name\": \"Alex Stepanov\",\"Age\": 55,\"salary\":20000}\");; \n      web::json::value json_par; \n      json::value obj = json::value::parse(port); \n      wcout << obj.serialize() << endl; \n```\n\n前面的代码片段演示了如何解析文本字符串来生成`json::value`对象。我们调用`serialize`方法将 JSON 字符串打印到控制台:\n\n```cpp\n   MakeAndShowJSONObject(); \n   getchar(); \n   return 0; \n} \n```\n\n# 基于 C++ REST 软件开发工具包的 REST 服务器\n\n在本节中，我们利用了 Marius Bancila 关于 C++ REST SDK 的优秀文章中的代码。事实上，键/值数据库代码是从他的实现中借来的。作者感谢他的优秀文章，可在[https://mariusbancila . ro/blog/2017/11/19/reviewed-full-future-client-server-example-with-c-rest-SDK-2-10/](https://mariusbancila.ro/blog/2017/11/19/revisited-full-fledged-client-server-example-with-c-rest-sdk-2-10/)查阅。\n\n让我们编写一个微服务，将我们到目前为止所学的一切都放在微软 C++ REST SDK 的上下文中。我们将通过利用柯克·肖普编写的 RxCurl 库来使用 REST 服务，作为他的推特分析应用的一部分。我们增加了对 DELETE 和 PUT 动词的支持。这里实现的 REST 服务支持以下动词:\n\n*   `GET`:列出存储中的所有键/值对。响应将采用`{ key:value,key:value}`格式。\n*   `POST`:检索与一组键对应的值。请求应采用`[key1,...,keyn]`格式。回复将采用`{key:value,key:value....}`格式。\n*   `PUT`:将键/值对的集合插入存储器。请求应采用`{key:value,key:value}`格式。\n*   `DELETE`:从存储器中删除一组键及其对应的值。请求应采用`[key,key]`格式。\n\n让我们看看代码:\n\n```cpp\n// MicroServiceController.cpp : Defines the entry point for the console application. \n#include <cpprest/http_client.h> \n#include <cpprest/filestream.h> \n#include <string> \n#include <vector> \n#include <algorithm> \n#include <sstream> \n#include <iostream> \n#include <fstream> \n#include <random> \n#include <set> \n\n#include \"cpprest/json.h\" \n#include \"cpprest/http_listener.h\" \n#include \"cpprest/uri.h\" \n#include \"cpprest/asyncrt_utils.h\" \n\n#ifdef _WIN32 \n#ifndef NOMINMAX \n#define NOMINMAX \n#endif \n#include <Windows.h> \n#else \n# include <sys/time.h> \n#endif \n\nusing namespace std; \nusing namespace web; \nusing namespace utility; \nusing namespace http; \nusing namespace web::http::experimental::listener; \n\n////////////////////////////// \n// \n// The following code dumps a json to the Console... \nvoid  DisplayJSON(json::value const & jvalue){ \n   wcout << jvalue.serialize() << endl; \n} \n\n/////////////////////////////////////////////// \n// A Workhorse routine to perform an action on the request data type \n// takes a lambda as parameter along with request type \n// The Lambda should contain the action logic...whether it is PUT,POST or DELETE \n// \nvoid RequeatWorker( http_request& request, \nfunction<void(json::value const &, json::value &)> handler) \n{ \n   auto result = json::value::object(); \n   request.extract_json().then([&result, &handler](pplx::task<json::value> task) {      \n         try{ \n            auto const & jvalue = task.get(); \n            if (!jvalue.is_null()) \n                  handler(jvalue, result); // invoke the lambda \n         } \n         catch (http_exception const & e) { \n               //----------- do exception processsing  \n               wcout << L\"Exception ->\" << e.what() << endl; \n         } \n   }).wait(); \n    request.reply(status_codes::OK, result); \n} \n```\n\n`RequestWorker`是一个全局函数，它将`http_request`作为一个参数，还有一个带有特定签名的λ。λ有两个参数:\n\n*   `json::value`类型的传入 JSON 对象(常量)\n*   一个输出 JSON 对象，包含 Lambda 调用的结果\n\nJSON 有效载荷被提取并传递到`then`继续。一旦检索到数据，就调用处理程序 Lambda。由于结果是通过引用传递的，我们可以使用结果 JSON 来生成 HTTP 响应。现在，我们将创建一个简单的键/值数据存储来模拟工业级键/值数据库:\n\n```cpp\n///////////////////////////////////////// \n// A Mock data base Engine which Simulates a key/value DB \n// In Real life, one should use an Industrial strength DB \n// \nclass HttpKeyValueDBEngine { \n   ////////////////////////////////// \n   //----------- Map , which we save,retrieve,  update and  \n   //----------- delete data  \n   map<utility::string_t, utility::string_t> storage; \npublic: \n   HttpKeyValueDBEngine() { \n         storage[L\"Praseed\"]= L\"45\"; \n         storage[L\"Peter\"] = L\"28\"; \n         storage[L\"Andrei\"] = L\"50\"; \n   } \n```\n\n为了便于实现，键/值对存储在 STL 映射中。在构造函数中，我们用一些记录初始化映射。我们可以使用`PUT`和`POST`添加附加记录，使用`DELETE`删除记录:\n\n```cpp\n   //////////////////////////////////////////////////////// \n   // GET - ?Just Iterates through the Map and Stores \n   // the data in a JSon Object. IT is emitted to the  \n   // Response Stream \n   void GET_HANDLER(http_request& request) { \n         auto resp_obj = json::value::object(); \n         for (auto const & p : storage) \n\n               resp_obj[p.first] = json::value::string(p.second); \n        request.reply(status_codes::OK, resp_obj); \n   } \n```\n\n当 HTTP 侦听器遇到作为请求一部分的 HTTP `GET`动词时，`GET_HANLDER`方法将被调用。创建`json::value::object`后，我们将存储地图的内容填充到其中。产生的 JSON 对象被返回到 HTTP 客户端:\n\n```cpp\n   ////////////////////////////////////////////////// \n   // POST - Retrieves a Set of Values from the DB \n   // The PAyload should be in [\"Key1\" , \"Key2\"...,\"Keyn\"] \n   // format \n   void POST_HANDLER(http_request& request) {       \n   RequeatWorker(request, \n         [&](json::value const & jvalue, json::value & result){ \n         //---------- Write to the Console for Diagnostics \n         DisplayJSON(jvalue); \n             for (auto const & e : jvalue.as_array()){ \n               if (e.is_string()){ \n                     auto key = e.as_string(); \n                     auto pos = storage.find(key); \nif (pos == storage.end()){ \n                        //--- Indicate to the Client that Key is not found \n                         result[key] = json::value::string(L\"notfound\"); \n                     } \n                     else { \n                     //------------- store the key value pair in the result \n                     //------------- json. The result will be send back to  \n                     //------------- the client \n                     result[pos->first] = json::value::string(pos->second); \n                     } \n               } \n         } \n         }); \n\n   } \n```\n\n`POST_HANDLER`期望在主体中有一个 JSON 值的数组，并循环遍历每个元素，检索与提供的键对应的数据。结果对象存储返回值。如果键/值数据库中不存在某些键，将返回一个字符串来指示找不到该值:\n\n```cpp\n   //////////////////////////////////////////////////////// \n   // PUT - Updates Data, If new KEy is found  \n   //       Otherwise, Inserts it \n   // REST Payload should be in  \n   //      { Key1..Value1,...,Keyn,Valuen}  format \n   // \n   // \n   void PUT_HANDLER(http_request& request) { \n         RequeatWorker( \n               request, \n               [&](json::value const & jvalue, json::value & result){ \n               DisplayJSON(jvalue); \n               for (auto const & e : jvalue.as_object()){ \n                     if (e.second.is_string()){ \n                           auto key = e.first; \n                           auto value = e.second.as_string(); \n                           if (storage.find(key) == storage.end()){ \n                                 //--- Indicate to the client that we have \n                                 //--- created a new record \n                                 result[key] = json::value::string(L\"<put>\"); \n                           } \n                           else { \n                                 //--- Indicate to the client that we have \n                                 //--- updated a new record \nresult[key] = json::value::string(L\"<updated>\"); \n                           } \n                           storage[key] = value; \n                     } \n               } \n         });    \n   } \n```\n\n`PUT_HANDLER`需要 JSON 格式的键/值对列表。重复键的集合来查找存储。如果存储中已经存在该键，则更新该值，否则将该键/值插入存储中。返回一个 JSON 对象(结果)来指示对每个键执行的操作(无论是插入还是更新):\n\n```cpp\n   /////////////////////////////////////////////////// \n   // DEL - Deletes a Set of Records \n   // REST PayLoad should be in \n   //      [ Key1,....,Keyn] format \n   // \n   void DEL_HANDLER(http_request& request) \n   { \nRequeatWorker( \n               request,[&](json::value const & jvalue, json::value & result) \n         { \n               //--------------- We aggregate all keys into this set \n               //--------------- and delete in one go \n               set<utility::string_t> keys; \n               for (auto const & e : jvalue.as_array()){ \n                     if (e.is_string()){ \n                           auto key = e.as_string(); \n                           auto pos = storage.find(key); \n                           if (pos == storage.end()){ \nresult[key] = json::value::string(L\"<failed>\"); \n                           } \n                           else { \nresult[key] = json::value::string(L\"<deleted>\"); \n                                 //---------- Insert in to the delete list \n                                 keys.insert(key); \n                           } \n                     } \n               } \n               //---------------Erase all \n               for (auto const & key : keys) \n                     storage.erase(key); \n         }); \n   } \n}; \n```\n\n`DEL_HANDLER`需要一个键数组作为输入，它在数组中循环检索数据。如果密钥已经存在于存储器中，则密钥被添加到删除列表(密钥-一个 STL 集合)。JSON 对象(结果)填充了对键采取的操作类型。结果对象将返回给客户端:\n\n```cpp\n/////////////////////////////////////////////// \n// \n// Instantiates the Global instance of key/value DB \nHttpKeyValueDBEngine g_dbengine; \n```\n\n现在我们已经有了一个功能性的模拟键/值数据库`engine`，我们将使用`GET`、`POST`、`PUT`和`DELETE`命令将数据库的功能作为 REST 服务端点对外使用。HTTP 处理程序将把调用委托给`HttpValueDBEngine`实例。该代码与我们为`SimpleServer`类编写的代码非常相似:\n\n```cpp\nclass RestDbServiceServer{ \npublic: \n   RestDbServiceServer(utility::string_t url); \n   pplx::task<void> Open() { return m_listener.open(); } \n   pplx::task<void> Close() { return m_listener.close(); } \nprivate: \n   void HandleGet(http_request message); \n   void HandlePut(http_request message); \n   void HandlePost(http_request message); \n   void HandleDelete(http_request message); \n   http_listener m_listener; \n}; \nRestDbServiceServer::RestDbServiceServer(utility::string_t url) : m_listener(url) \n{ \n    m_listener.support(methods::GET,  \n       std::bind(&RestDbServiceServer::HandleGet, this, std::placeholders::_1)); \n    m_listener.support(methods::PUT,  \n       std::bind(&RestDbServiceServer::HandlePut, this, std::placeholders::_1)); \n    m_listener.support(methods::POST,  \n       std::bind(&RestDbServiceServer::HandlePost, this, std::placeholders::_1)); \n    m_listener.support(methods::DEL,  \n        std::bind(&RestDbServiceServer::HandleDelete, this, std::placeholders::_1)); \n} \n```\n\n前面的代码将 HTTP 谓词绑定到相应的处理程序。处理程序的主体在性质上是相似的，因为处理程序只是将调用委托给键/值引擎:\n\n```cpp\nvoid RestDbServiceServer::HandleGet(http_request message) \n{g_dbengine.GET_HANDLER(message);}; \nvoid RestDbServiceServer::HandlePost(http_request message) \n{g_dbengine.POST_HANDLER(message);}; \nvoid RestDbServiceServer::HandleDelete(http_request message) \n{g_dbengine.DEL_HANDLER(message);} \nvoid RestDbServiceServer::HandlePut(http_request message) \n{g_dbengine.PUT_HANDLER(message);}; \n//---------------- Create an instance of the Server  \nstd::unique_ptr<RestDbServiceServer> g_http; \nvoid StartServer(const string_t& address) \n{ \n       uri_builder uri(address); \n   uri.append_path(U(\"DBDEMO/\")); \n   auto addr = uri.to_uri().to_string(); \n   g_http = std::unique_ptr<RestDbServiceServer>(new RestDbServiceServer(addr)); \n   g_http->Open().wait(); \n   ucout << utility::string_t(U(\"Listening for requests at: \")) << \n               addr << std::endl; \n   return; \n} \nvoid ShutDown(){ \n      g_http->Close().wait(); \n      return; \n} \n/////////////////////////////// \n// The EntryPoint function \nint wmain(int argc, wchar_t *argv[]){ \n   utility::string_t port = U(\"34567\"); \n   if (argc == 2){port = argv[1];} \n   utility::string_t address = U(\"http://localhost:\"); \n   address.append(port); \n   StartServer(address); \n   std::cout << \"Press ENTER to exit.\" << std::endl; \n   std::string line; \n   std::getline(std::cin, line); \n   ShutDown(); \n   return 0; \n} \n```\n\nHTTP 控制器的代码与我们在本章前面写的`SimpleServer`没有什么不同。为了完整起见，我们在此提供了列表。至此，我们已经学会了如何向外界公开一个 REST 服务端点。\n\n我们已经讨论了如何公开 REST 端点，以及如何为各种 HTTP 动词编写处理程序。在微服务架构风格中，我们将独立部署许多 REST 端点。将粗粒度服务分解成微服务的过程是一门高度依赖于上下文的艺术。微服务暴露于外部世界，有时是通过聚合服务。聚合服务是编写用于访问 REST 微服务的反应式客户端逻辑的候选。由于网络调用是异步的，反应式编程模型在这里是自然的。\n\n# 使用 RxCurl 库调用 REST 服务\n\n柯克·肖普写的`RcCurl`库最初只支持`GET`和`POST`动词。推特分析应用只能保证这一点。这本书的作者增加了对`PUT`和`DELETE`动词的支持。下面的代码片段帮助我们支持`PUT`动词。您可以参考`rxcurl.h`的来源来查看支持附加动词的必要更改:\n\n```cpp\n#include <iostream> \n#include <stdio.h> \n#include <iostream> \n#include <stdio.h> \n#include <stdlib.h> \n#include <map> \n#include <chrono> \nusing namespace std; \nusing namespace std::chrono; \n//////////////////////// \n// include Curl Library and  \n// Rxcpp library  \n// \n#include <curl/curl.h> \n#include <rxcpp/rx.hpp> \nusing namespace rxcpp; \nusing namespace rxcpp::rxo; \nusing namespace rxcpp::rxs; \n////////////////////////// \n// include the modified rxcurl library from  \n// Kirk Shoop's Twitter Analysis app \n// \n#include \"rxcurl.h\" \nusing namespace rxcurl; \nrxcurl::rxcurl factory; \n```\n\n使用`factory`对象，我们可以通过调用`create`方法发出请求。`creates`法期望:\n\n*   网址端点\n*   HTTP 方法\n*   HTTP 头\n*   请求的正文:\n\n```cpp\nstring HttpCall( string url ,  \n               string method, \n               std::map<string,string> headers, \n               string  body  ) \n{         \n\n     auto request  = factory.create(http_request{url,method,headers,body}) | \n            rxo::map([](http_response r){ \n                return r.body.complete; \n            });      \n```\n\n前面的代码通过组合创建的 HTTP 请求和从`http_response`映射到 HTTP 主体的函数来创建一个`request`对象。有一个返回大块数据的选项。我们预计这里只有少量数据:\n\n```cpp\n     //////////////////////////////////////// \n     // make a blocking call to the url.. \n     observable<string>   response_message; \n     request.as_blocking().subscribe([&] (observable<string> s) { \n               response_message = s.sum(); \n     } ,[] () {printf(\"\");});\n```\n\n前面的代码对我们之前创建的`observable`进行了阻塞调用。`subscribe`方法的`on_next`函数的主体将内容连接起来，形成另一个可观察的对象。在现实生活中，我们也可以异步方式进行这个调用。这需要更多的编程工作。此外，代码清单不符合可用的页面预算:\n\n```cpp\n\n     /////////////////////////////// \n     // \n     // retrieve the html content form the site  \n     string html; \n     response_message.as_blocking().subscribe( [&html] ( string temp ) {          \n                   html = temp; \n     }, [] () { printf(\"\"); } ); \n     return html; \n} \n///////////////////////// \n// The EntryPoint... \n// \nint main() { \n\n     /////////////////////////////////// \n     // set the url and create the rxcurl object \n     string url = \"http://localhost:34567/DBDEMO/\"; \n     factory = create_rxcurl(); \n     ///////////////////////////////// \n     // default header values \n     std::map<string,string> headers; \n     headers[\"Content-Type\"] = \"application/json\"; \n     headers[\"Cache-Control\"] = \"no-cache\"; \n\n     //------- invoke GET to retrieve the contents \n     string html = HttpCall( url,\"GET\",headers, \"\" ); \n     cout << html << endl; \n\n     //------- Retrieve values for the following  \n     string body = string(\"[\"Praseed\"]rn\"); \n     html = HttpCall( url,\"POST\", headers,body); \n     cout << html << endl; \n     //--------- Add new Values using PUT \n     body = string(\"rn{\"Praveen\": \"29\",\"Rajesh\" :\"41\"}rn\"); \n     html = HttpCall( url,\"PUT\", headers,body); \n     cout << html << endl; \n     //-------- See whether values has been added \n     html = HttpCall( url,\"GET\",headers, \"\" ); \n     cout << \"-------------------------current database state\" << endl; \n     cout << html << endl; \n     //--------------- DELETE a particular record \n     body = string(\"[\"Praseed\"]rn\"); \n     html = HttpCall( url,\"DELETE\", headers,body); \n     cout << \"Delleted...\" << html << endl; \n     html = HttpCall( url,\"GET\",headers, \"\" ); \n     cout << \"-------------------------current database state\" << endl; \n     cout << html << endl; \n} \n```\n\n`main`方法演示了我们如何调用自己创建的`HttpCall`方法。提供的代码展示了如何利用 RxCurl 库。我们可以使用库异步发出多个请求，并等待它们完成。\n\n# 关于反应式微服务架构的一句话\n\n我们已经学习了如何使用 C++ REST SDK 编写微服务控制器。也许我们可以说，我们刚刚实现的服务器可以是一个微服务实例。在现实生活中的微服务场景中，将有多个服务托管在不同的盒子(Docker 容器或虚拟机)中，微服务控制器将访问这些独立部署的服务来迎合客户端。微服务控制器将聚合来自不同服务的输出，作为响应发送给客户端。下图显示了微服务应用的基本架构:\n\n![](img/00019.jpeg)\n\n在上图中，REST (HTTP)客户端对微服务控制器进行 HTTP 调用，该调用包装了`http_listener`对象。控制器调用三个微服务来检索数据，结果数据将被组装或合并，以向 REST 客户端提供响应。端点可以使用 Docker 等技术部署在一个容器或不同的容器中。\n\n根据马丁·福勒的说法:\n\n”*在过去的几年里，“微服务架构”这个术语如雨后春笋般出现，用来描述将软件应用设计为可独立部署的服务套件的特定方式。虽然这种架构风格没有精确的定义，但围绕业务能力、自动化部署、端点智能以及语言和数据的分散控制等方面，组织有一些共同的特征*。”\n\n微服务架构的主题本身就是一个主题，这个主题值得一本属于自己的书。我们在这里讨论的是如何利用 C++ 编程语言以这种风格编写 web 应用。这里给出的描述是为了给读者指出正确的信息。反应式编程模型适用于聚合来自不同服务端点的信息，并将其统一呈现给客户端。服务的聚合是读者应该研究的关键问题。\n\n当我们谈论微服务架构时，我们需要了解以下主题:\n\n*   细粒度服务\n*   多语种持久性\n*   独立部署\n*   服务编排和服务编排\n*   反应性 web 服务调用\n\n我们将在下面的章节中详细讨论它们。\n\n# 细粒度服务\n\n传统的基于 SOA 和 REST 的服务大多是粗粒度的服务，编写时的心态是网络往返是核心问题。为了减少网络往返，开发人员经常创建本质上是复合的有效载荷格式。因此，一个端点或一个 URI 被用来处理多个关注点，并且违反了关注点分离的原则。微服务体系结构期望服务执行单一的职责，并且有效载荷格式是为此而定制的。这样，服务就变得精细化了。\n\n# 多语种持久性\n\n多语种持久化是一个术语，用来表示在持久化数据时使用多种存储技术。该术语来自术语**多语种编程**，其中编程语言的选择由上下文决定。在多语种编程的情况下，我们混合使用不同的编程语言。作者遇到过使用 Java 作为应用服务器代码的系统，使用 Scala 作为流处理的系统，使用 C++ 作为与存储相关的关注点的系统，使用 C#编写 web 层的系统，当然还有用于客户端编程的 TypeScript/JavaScript。在多语种持久性的情况下，我们可以选择使用关系数据库管理系统、键/值存储、文档数据库、图形数据库、柱状数据库，甚至时间序列数据库。\n\n电子商务门户是一个典型的例子，在这个系统中，多语种持久性非常方便。这样的平台将处理多种类型的数据(例如，购物车、库存和已完成订单)。我们可以使用 RDBMS(记录事务)、键/值 DBs(缓存和查找)、用于存储日志的文档数据库等等，而不是试图将所有这些数据存储在一个数据库中。*为你的关注选择合适的坚持模式*是这里的主要座右铭。\n\n# 独立部署\n\n微服务架构和传统 SOA 最大的区别在于部署领域。随着容器技术的发展，我们可以很好地独立部署服务。DevOps 运动在推广服务和应用的独立部署模式方面帮助很大。我们现在可以自动执行为虚拟机和相关容器配置中央处理器、内存、存储、附加磁盘、虚拟网络、防火墙、负载平衡以及云服务(如 AWS 或谷歌云)部署策略自动扩展的过程。策略帮助您使用脚本以自动方式部署微服务。\n\n当使用微服务架构风格开发应用时，容器技术的概念会一次又一次地出现。一个相关的运动，叫做 DevOps，被带入了讨论的领域。在独立部署的背景下涵盖 DevOps 和容器化(以及集群管理)超出了本书的范围。您可以搜索 Docker、Kubernetes 和“基础架构即代码”来获得对这些技术的更多了解。\n\n# 服务编排和编排\n\n让我们从服务编排开始。您通过固定的逻辑将几个服务组合在一起。这个逻辑在一个地方描述。但是为了保证安全，我们可能会部署相同服务的多个实例。聚合器服务将独立调用这些服务，并为下游系统聚合数据。另一方面，在服务编排中，决策逻辑是分布式的，没有集中点。没有集中的逻辑。在数据到达下游系统之前，对服务的调用将触发服务之间的多次调用。服务编排需要比实现编排更多的努力。您可以通过搜索 web 来阅读更多关于服务编排和编排的信息。\n\n# 反应式网络服务呼叫\n\nweb 请求的处理被很好地映射到反应式编程模型。对于具有响应用户界面的应用，我们通常只调用服务器一次。聚合器服务将异步产生一系列请求。所产生的响应被聚合，以给出对 UI 层的响应。修改后的`RxCurl`可以作为调用多个服务的机制。\n\n# 摘要\n\n在本章中，我们介绍了如何使用 C++ 使用 Rx 编程模型编写反应式微服务。作为过程的一部分，我们向您介绍了微软 C++ REST SDK 及其编程模型。在编写客户端代码时，C++ REST SDK 遵循基于称为任务延续风格的技术的异步编程模型。为了编写 REST 客户端，我们利用了 Kirk Shoop 的`RxCurl`库，并做了一些修改来支持`PUT`和`DELETE`动词。最后，我们编写了一个 REST 服务器，并以被动的方式使用它。\n\n在下一章中，我们将学习如何使用 RxCpp 库中可用的构造来处理错误和异常。"
  },
  {
    "path": "docs/cpp-react-prog/12.md",
    "content": "# 十二、高级流和错误处理\n\n在这本书里，我们在解释现代 C++ 技术和 RxCpp 库方面覆盖了相当多的内容。我们从使用 C++ 进行反应式编程的一组先决条件开始。前六章主要是关于先决条件和适应功能反应式编程，特别是 RxCpp 库中的特性。我们在松散的意义上使用了函数式反应式编程这个术语——我们正在利用函数式编程技术来编写反应式程序。一些纯粹主义者在这一点上与我们不同。他们不认为 Rx 系列库是功能反应式编程的完整实现。程序员必须经历的最大转变是心态的改变，以采用声明式编程范式。\n\n传统上，我们设计复杂的数据结构，并在这些数据结构上编写算法，来编写我们的程序。这适用于操作空间中存在的数据的程序。当时间进入画面时，异步是自然的结果。在反应式编程中，我们将复杂的数据结构简化为数据流，并将操作符放在数据流中，然后根据通知得到执行某些操作的通知。我们已经看到了在使用 C++ 编程语言的图形用户界面程序、网络程序和控制台应用中，这是如何简化编程的。\n\n在我们的例子中，我们省略了反应式程序中的异常处理(和错误处理)逻辑。这是有目的的，以便集中关注核心反应要素及其相互作用。现在我们已经涵盖了所有的要点，接下来，我们将重点关注反应式程序中的异常处理。在进入错误和异常处理之前，我们将讨论反应系统的特性。\n\n在本章中，我们将涵盖以下主题:\n\n*   简单回顾一下反应系统的特征\n*   `RxCpp`—错误处理操作员\n*   调度和错误处理\n*   基于事件的流处理—一些示例\n\n# 简单回顾一下反应系统的特征\n\n我们现在生活在一个需要增强可扩展性和快速响应的世界。反应式编程的概念是为了满足高可用性、可伸缩性和快速响应的需求而出现的。根据无功宣言([https://www.reactivemanifesto.org/](https://www.reactivemanifesto.org/))，无功系统为:\n\n*   **响应性**:(系统)在时间范围内完成指定任务的能力。响应性也意味着问题被快速发现，并得到有效处理。关键是系统的一致行为。一致性帮助用户建立对系统的信心。\n*   **弹性**:在行为变化的背景下，系统防御失败的能力就是弹性。它与响应性相关，因为一致性也保证了错误处理。弹性是通过隔离和遏制容易出错的组件并保护系统免受故障影响来实现的。\n*   **弹性**:弹性是系统通过自动重新分配所需资源来适应工作负载变化的能力。反过来，在每个时间实例中，使用的资源尽可能与需求匹配。反应系统通过提供相关的实时性能测量来实现弹性。\n*   **消息驱动**:反应式系统通过异步消息传递机制的通信能力，实现系统的隔离和松耦合。通过使用消息队列，不同模块和命令的相互依赖的处理在反应系统中成为可能。通过消息驱动架构的非阻塞通信允许接收者仅在活动时使用资源:\n\n![](img/00020.jpeg)\n\n通过将这些原则应用于其结构的所有层面，反应系统变得可组合。\n\n本章的重点将是反应系统的弹性，通过解释高级流和错误处理。\n\n# RxCpp 错误和异常处理运算符\n\n在现实场景中，没有一个系统是完美的。正如我们在上一节中讨论的，弹性是反应系统的品质之一。一个系统如何处理错误和异常决定了这个系统的未来。早期检测和对错误的无缝处理使系统具有一致性和响应性。与命令式编程方法相比，当系统检测到错误或抛出异常时，反应式编程模型帮助用户单独处理错误。\n\n在本节中，我们将了解如何使用 RxCpp 库处理异常和错误。有多种 RxCpp 操作符可用于对 Observables 的`on_error`通知做出反应。例如，我们可以:\n\n*   通过优雅地退出序列来处理错误\n*   忽略错误并切换到备份可观察到的继续序列\n*   忽略错误并发出默认值\n*   忽略错误，并立即尝试重新启动失败的可观察\n*   忽略该错误，并在一段时间后尝试重新启动失败的可观测值\n\n异常处理是可能的，因为`observer<>`包含三种方法:\n\n*   `on_next`\n*   `on_completed`\n*   `on_error`\n\n`on_error`方法意味着当异常发生时，或者当它们被`observable<>`或组合链中的任何操作符抛出时，处理异常。迄今为止的例子忽略了系统的错误处理方面。观测器方法的原型如下:\n\n*   `void observer::on_next(T);`\n*   `void observer::on_error(std::exception_ptr);`\n*   `void observer::on_completed();`\n\n# 对错误执行操作\n\n当错误发生时，我们需要以优雅的方式处理它。到目前为止，在本书讨论的 RxCpp 程序中，编写的程序只处理`subscribe`方法中的`on_next`和`on_completed`场景。`subscribe`函数还有一个方法，它也可以为`on_error`场景接受一个λ函数。让我们看一个简单的例子来理解如何使用`subscribe`函数中的错误处理程序:\n\n```cpp\n//------ OnError1 \n#include \"rxcpp/rx.hpp\" \n\nint main() \n{ \n    //------ Creating Observable with an error appended \n    //------ A canned example to demonstrate error \n    auto values = rxcpp::observable<>::range(1, 3). \n                  concat(rxcpp::observable<>:: \n                  error<int>(std::runtime_error(\"Error from producer!\"))); \n\n    values. \n        subscribe( \n         //--------------- on_next \n            [](int v) { printf(\"OnNext: %dn\", v); }, \n            //---------------- on_error \n            [](std::exception_ptr ep) { \n                 printf(\"OnError: %sn\", rxcpp::util::what(ep).c_str()); \n            }, \n            //---------------- on_completed \n            []() { printf(\"OnCompletedn\"); }); \n} \n```\n\n对于第二个 Lambda，传递到`subscribe`函数的函数调用出现错误时所需的操作。代码的输出如下所示:\n\n```cpp\nOnNext: 1 \nOnNext: 2 \nOnNext: 3 \nOnError: Error from producer!\n```\n\n在前面的代码中，错误被附加到可观察的流中，以在订户端启动关于异常/错误处理的讨论。让我们看看异常是如何通过可观察流传播到订户级别的:\n\n```cpp\n//------- OnError2.cpp \n#include \"rxcpp/rx.hpp\" \n\nint main() { \n    //------- Create a subject instance  \n    //------  and retrieve subscriber abd Observable handle  \n    rxcpp::rxsub::subject<int> sub; \n    auto subscriber = sub.get_subscriber(); \n    auto observable = sub.get_observable(); \n\n    //--------------------------- Subscribe! \n    observable.subscribe( \n        [](int v) { printf(\"OnNext: %dn\", v); }, \n        [](std::exception_ptr ep) { \n            printf(\"OnError: %sn\", rxcpp::util::what(ep).c_str()); \n        }, \n        []() { printf(\"OnCompletedn\"); } \n    );\n```\n\n前面的代码创建了一个`subject<T>`类的实例，我们在[第 8 章](08.html#49AH00-51c8384cc2cb48e691b461190723b468)、RxCpp -关键元素中讨论过。我们订阅了`subject<T>`的可观察部分。我们还检索订阅者句柄，将值或异常发送到流中:\n\n```cpp\n    for (int i = 1; i <= 10; ++ i) { \n        if (i > 5) { \n            try { \n                std::string().at(1); \n            } \n            catch (std::out_of_range& ex) { \n                //------------ Emit exception. \n                subscriber.on_error(std::make_exception_ptr(ex)); \n            } \n        } \n        subscriber.on_next(i * 10); \n    } \n    subscriber.on_completed(); \n} \n```\n\n`on_next()`函数向订阅者发出新的值，该函数将被多次调用。一旦在流上调用了`on_completed()`或`on_error()`，就不会调用`on_next()`函数。`on_completed()`功能通知用户可观察已经完成发送**基于推送的通知**。如果可观测量已经调用了`on_error()`函数，它将不会调用该函数。最后，`on_error()`功能通知用户可观察到出现了错误情况，如果可观察到调用该功能，则此后不会调用`on_next()`或`on_completed()`。\n\n# 出现错误时恢复\n\n出现错误会中断标准反应流的顺序流。RxCpp 库还提供了在出现错误时调用操作的机制。但是，有时用户希望使用默认选项恢复序列；这就是`on_error_resume_next()`的作用:\n\n```cpp\n//------- OnError3.cpp \n#include \"rxcpp/rx.hpp\" \n\nint main() \n{ \n    //------- Create an Observable with appended error \n    auto values = rxcpp::observable<>::range(1, 3). \n        concat(rxcpp::observable<>:: \n        error<int>(std::runtime_error(\"Error from producer!    \"))). \n        //------- Resuming with another Stream \n        on_error_resume_next([](std::exception_ptr ep) { \n            printf(\"Resuming after: %sn\", rxcpp::util::what(ep).c_str()); \n            return rxcpp::observable<>::range(4,6); \n        }); \n\n    values. \n        subscribe( \n            [](int v) {printf(\"OnNext: %dn\", v); }, \n            [](std::exception_ptr ep) { \n                printf(\"OnError: %sn\", rxcpp::util::what(ep).c_str()); }, \n            []() {printf(\"OnCompletedn\"); }); \n} \n```\n\n如果流中有错误，可观察运算符`on_error_resume_next()`将被执行。在这段代码中，从作为参数给定的 Lambda 返回一个新流，用这个新流恢复序列。这样，通过继续有意义的序列，可以防止错误传播。上一个程序的输出如下所示:\n\n```cpp\nOnNext: 1 \nOnNext: 2 \nOnNext: 3 \nResuming after: Error from producer! \nOnNext: 4 \nOnNext: 5 \nOnNext: 6 \nOnCompleted \n```\n\n除了用另一个序列恢复之外，该序列还可以用默认的单个项目恢复。在上例中，将运算符`on_error_resume_next()`的调用替换为以下行:\n\n```cpp\n        //------- Resuming with a default single value \n        on_error_resume_next([](std::exception_ptr ep) { \n            printf(\"Resuming after: %sn\", rxcpp::util::what(ep).c_str()); \n            return rxcpp::observable<>::just(-1); \n        });\n```\n\n替换代码后，输出将如下所示:\n\n```cpp\nOnNext: 1 \nOnNext: 2 \nOnNext: 3 \nResuming after: Error from source \nOnNext: -1 \nOnCompleted \n```\n\n让我们看看描绘`on_error_resume_next()`运算符的大理石图:\n\n![](img/00021.jpeg)\n\n简而言之，`on_error_resume_next()`函数在遇到特定可观测值的错误时返回一个可观测值实例。该流切换到新的可观测值并继续执行。\n\n`on_error_resume_next()`操作符在很多地方派上了用场，用户需要继续传播错误。例如，在流的创建和订阅之间，流可能会经历不同的转换和缩减。此外，如[第 9 章](09.html#4U9TC0-51c8384cc2cb48e691b461190723b468)、*使用 Qt/C++* 的反应式图形用户界面编程中所述，用户定义的操作符可以通过组合现有的 RxCpp 操作符来构建。在这种情况下，打算在聚合和转换的每个阶段使用`on_error_resume_next()`运算符来转换异常/错误，直到订阅阶段。类似于默认值或从该操作符发出的序列，错误本身可以被重新传输，以恢复错误的流程，直到`subscribe()`操作符的错误处理程序:\n\n```cpp\nauto processed_strm = Source_observable. \nmap([](const string& s) { \nreturn do_string_operation(s); \n      }). \n// Translating exception from the source \non_error_resume_next([](std::exception_ptr){ \nreturn rxcpp::sources::error<string>(runtime_error(rxcpp::util::what(ep).c_str())); \n      });\n```\n\n前面的代码片段解释了如何使用`on_error_resume_next()`运算符来翻译错误。\n\n# 出现错误时重试\n\n在许多情况下，正常的序列可能会被生产者端的临时故障打破。在这种情况下，值得选择等待，直到异常在生产者端被修复，以继续正常的执行流程。RxCpp 为用户提供了一个非常相似的选项，在出现错误时重试。重试选项最适合您预期序列会遇到可预测问题的情况。\n\n重试操作符通过重新订阅源可观测值来响应源可观测值的`on_error`通知，而不是将该调用传递给它的观测值。这给了源另一个机会来完成它的序列而不出错。重试总是将`on_next`通知传递给它的观察者，即使是来自以错误结束的序列；这会导致重复排放。下面的大理石图将进一步解释这一点:\n\n![](img/00022.jpeg)\n\n下面是一个使用`retry()`运算符的示例:\n\n```cpp\n//------- Retry1.cpp \n#include \"rxcpp/rx.hpp\" \n\nint main() \n{ \n    auto values = rxcpp::observable<>::range(1, 3). \n        concat(rxcpp::observable<>:: \n        error<int>(std::runtime_error(\"Error from producer!\"))). \n        retry(). \n        take(5); \n\n    //----- Subscription \n    values. \n        subscribe( \n            [](int v) {printf(\"OnNext: %dn\", v); }, \n            []() {printf(\"OnCompletedn\"); }); \n} \n```\n\n在本例中，由于使用`concat()`运算符将错误附加到流中，因此我们使用`take()`运算符来避免无限等待。由于在错误情况下对重试操作符的无限等待，订阅服务器可以省略订阅中使用的错误处理程序。\n\n该代码的输出将是:\n\n```cpp\nOnNext: 1 \nOnNext: 2 \nOnNext: 3 \nOnNext: 1 \nOnNext: 2 \nOnCompleted \n```\n\n大多数情况下，对于错误情况，最好使用固定的重试次数。这可以通过`retry()`的另一个重载来实现，该重载接受重试次数:\n\n```cpp\n//------- Retry2.cpp \n#include \"rxcpp/rx.hpp\" \n\nint main() \n{ \n    auto source = rxcpp::observable<>::range(1, 3). \n        concat(rxcpp::observable<>:: \n        error<int>(std::runtime_error(\"Error from producer!\"))). \n        retry(2); \n\n    source. \n        subscribe( \n            [](int v) {printf(\"OnNext: %dn\", v); }, \n            [](std::exception_ptr ep) { \n                printf(\"OnError: %sn\", rxcpp::util::what(ep).c_str()); }, \n            []() {printf(\"OnCompletedn\"); }); \n}\n```\n\n代码的输出如下所示:\n\n```cpp\nOnNext: 1 \nOnNext: 2 \nOnNext: 3 \nOnNext: 1 \nOnNext: 2 \nOnNext: 3 \nOnError: Error from producer! \n```\n\n# 使用 finally()运算符进行清理\n\n到目前为止，在本章中，我们已经看到 RxCpp 中的源序列在抛出异常后可以优雅地终止。当我们使用外部资源时，或者当需要释放程序其他部分分配的一些资源时，`finally()`运算符非常有用。正如我们所知，已经有数百万行代码是为用 C++ 构建各种系统而编写的，当使用遗留的外部依赖关系时，我们很可能需要处理资源管理。这是`RxCpp`中`finally()`派上用场的地方:\n\n```cpp\n//------- Finally.cpp \n#include \"rxcpp/rx.hpp\" \n\nint main() \n{ \n    auto values = rxcpp::observable<>::range(1, 3). \n        concat(rxcpp::observable<>:: \n        error<int>(std::runtime_error(\"Error from producer!\"))). \n        //----- Final action \n        finally([]() { printf(\"The final actionn\"); \n    }); \n\n    values. \n        subscribe( \n            [](int v) {printf(\"OnNext: %dn\", v); }, \n            [](std::exception_ptr ep) { \n                  printf(\"OnError: %sn\", rxcpp::util::what(ep).c_str()); }, \n            []() {printf(\"OnCompletedn\"); }); \n}\n```\n\n`finally()`操作符在新创建的可观察对象的末尾添加一个新动作。上一个程序的输出如下所示:\n\n```cpp\nOnNext: 1 \nOnNext: 2 \nOnNext: 3 \nOnError: Error from producer! \nThe final action \n```\n\n可以看到，在前面的输出中，如果源产生错误，最终的操作仍然被调用。如果我们移除链接到源可观测值的错误，程序的输出将如下所示:\n\n```cpp\nOnNext: 1 \nOnNext: 2 \nOnNext: 3 \nOnCompleted \nThe final action \n```\n\n# 调度程序和错误处理\n\n我们已经在[第 8 章](08.html#49AH00-51c8384cc2cb48e691b461190723b468)、*RxCPP–关键要素*中介绍了调度的主题。RxCpp 中的调度程序将值排队，并使用提供的协调来传递排队的值。协调可以是当前执行线程、RxCpp 运行循环、`RxCpp`事件循环或新线程。调度器操作的执行可以通过使用 RxCpp 操作符来实现，例如`observe_on()`或`subscribe_on()`。这些操作符接受选择的协调作为参数。默认情况下，RxCpp 库是单线程的，因此它执行调度器操作。用户必须明确选择执行的线程:\n\n```cpp\n//----------OnError_ObserveOn1.cpp  \n#include \"rxcpp/rx.hpp\" \n#include <iostream> \n#include <thread> \n\nint main() { \n    //---------------- Generate a range of values \n    //---------------- Apply Square function \n    auto values = rxcpp::observable<>::range(1, 4). \n        transform([](int v) { return v * v; }). \n        concat(rxcpp::observable<>:: \n        error<int>(std::runtime_error(\"Error from producer!\"))); \n\n    //------------- Emit the current thread details \n    std::cout << \"Main Thread id => \" \n        << std::this_thread::get_id() \n        << std::endl; \n```\n\n我们已经使用 range 运算符创建了一个可观察的流，并连接了一个错误，以演示基本的错误处理如何与`RxCpp`中的调度器一起工作:\n\n```cpp\n    //---------- observe_on another thread.... \n    //---------- make it blocking too \n    values.observe_on(rxcpp::synchronize_new_thread()).as_blocking(). \n        subscribe([](int v) { \n             std::cout << \"Observable Thread id => \" \n            << std::this_thread::get_id() \n            << \" \" << v << std::endl; }, \n            [](std::exception_ptr ep) { \n            printf(\"OnError: %sn\", rxcpp::util::what(ep).c_str()); }, \n            []() { std::cout << \"OnCompleted\" << std::endl; }); \n\n    //------------------ Print the main thread details \n    std::cout << \"Main Thread id => \" \n        << std::this_thread::get_id() \n        << std::endl; \n} \n```\n\n使用`observe_on()`操作符，可观察的流被订阅到一个新的线程中作为它的协调。类似于我们在本章中讨论的前面的例子，错误处理程序提供了`subscribe()`功能。代码的输出可能如下所示:\n\n```cpp\nMain Thread id => 5776 \nObservable Thread id => 12184 1 \nObservable Thread id => 12184 4 \nObservable Thread id => 12184 9 \nObservable Thread id => 12184 16 \nOnError: Error from producer! \nMain Thread id => 5776 \n```\n\n现在，让我们看另一个例子，两个用户来自同一个来源。订阅者应该以两种不同的方式得到通知:\n\n```cpp\n//------- OnError_ObserveOn2.cpp \n#include \"rxcpp/rx.hpp\" \n#include <mutex> \n\nstd::mutex printMutex; \n\nint main() { \n\n    rxcpp::rxsub::subject<int> sub; \n    auto subscriber = sub.get_subscriber(); \n    auto observable1 = sub.get_observable(); \n    auto observable2 = sub.get_observable(); \n```\n\n创建`subject`实例，将数据添加到源流中；从主题实例中，创建了一个订阅者和两个可观察对象，在两个不同的线程中进行调度:\n\n```cpp\n    auto onNext = [](int v) { \n        std::lock_guard<std::mutex> lock(printMutex); \n        std::cout << \"Observable Thread id => \" \n            << std::this_thread::get_id() \n            << \"t OnNext: \" << v << std::endl; \n    }; \n\n    auto onError = [](std::exception_ptr ep) { \n        std::lock_guard<std::mutex> lock(printMutex); \n        std::cout << \"Observable Thread id => \" \n            << std::this_thread::get_id() \n            << \"t OnError: \" \n            << rxcpp::util::what(ep).c_str() << std::endl; \n    }; \n```\n\n两个 Lambda 函数被声明为与`subscribe`方法一起使用，互斥同步应用于`std::ostream`操作符的使用，以获得有组织的输出。如果在写入流期间发生线程切换，在`std::ostream`周围放置互斥体将避免交错输出:\n\n```cpp\n    //------------- Schedule it in another thread \n    observable1\\. \n        observe_on(rxcpp::synchronize_new_thread()). \n        subscribe(onNext, onError, \n            []() {printf(\"OnCompletedn\"); }); \n\n    //------------- Schedule it in yet another thread \n    observable2\\. \n        observe_on(rxcpp::synchronize_event_loop()). \n        subscribe(onNext, onError, \n            []() {printf(\"OnCompletedn\"); });\n```\n\n从源流中检索两个可观察值，并安排它们从单独的线程中进行观察。对于`observable1`函数对象，通过在`observe_on()`操作符中将`rxcpp::synchronize_new_thread()`作为参数传递，单独的 C++ 线程被指定为协调器。对于第二个可观测值`observable2`，协调器是一个事件循环，通过将`rxcpp::observe_on_event_loop()`传递到`observe_on()`:\n\n```cpp\n    //------------- Adding new values into the source Stream \n    //------------- Adding error into Stream when exception occurs \n    for (int i = 1; i <= 10; ++ i) { \n        if (i > 5) { \n            try { \n                std::string().at(1); \n            } \n            catch (...) { \n                std::exception_ptr eptr = std::current_exception(); \n                subscriber.on_error(eptr); \n            } \n        } \n        subscriber.on_next(i * 10); \n    } \n    subscriber.on_completed(); \n\n    //----------- Wait for Two Seconds \n    rxcpp::observable<>::timer(std::chrono::milliseconds(2000)). \n        subscribe([&](long) {}); \n}     \n```\n\n最后，通过使用主题实例将值添加到可观察流中，并将异常显式传递到流中，以便一起理解调度器和错误处理程序的行为。该代码的输出如下:\n\n```cpp\nObservable Thread id => 2644    OnNext: 10 \nObservable Thread id => 2304    OnNext: 10 \nObservable Thread id => 2644    OnNext: 20 \nObservable Thread id => 2304    OnNext: 20 \nObservable Thread id => 2644    OnNext: 30 \nObservable Thread id => 2304    OnNext: 30 \nObservable Thread id => 2644    OnNext: 40 \nObservable Thread id => 2304    OnNext: 40 \nObservable Thread id => 2304    OnNext: 50 \nObservable Thread id => 2304    OnError: invalid string position \nObservable Thread id => 2644    OnNext: 50 \nObservable Thread id => 2644    OnError: invalid string position\n```\n\n这个例子演示了如何通过订阅一个公共源的两个独立的 Observables 来传播数据。源中产生的误差由相应的`subscribe`功能的两个可观测值接收和处理。现在，让我们看一个示例，演示如何使用`subscribe_on()`运算符在调度中进行错误处理:\n\n```cpp\n//---------- SubscribeOn.cpp \n#include \"rxcpp/rx.hpp\" \n#include <thread> \n#include <mutex> \n\n//------ A global mutex for output sync. \nstd::mutex printMutex; \n\nint main() { \n    //-------- Creating Observable Streams \n    auto values1 = rxcpp::observable<>::range(1, 4). \n        transform([](int v) { return v * v; }); \n\n    auto values2 = rxcpp::observable<>::range(5, 9). \n                   transform([](int v) { return v * v; }). \n                   concat(rxcpp::observable<>: \n:error<int>(std::runtime_error(\"Error from source\"))); \n```\n\n使用`rxcpp::observable<>::range()`运算符创建两个整数上的随机可观察流，一个流与一个错误连接，以解释调度序列中的错误处理:\n\n```cpp\n    //-------- Schedule it in another thread \n    auto s1 = values1.subscribe_on(rxcpp::observe_on_event_loop()); \n\n    //-------- Schedule it in Yet another thread \n    auto s2 = values2.subscribe_on(rxcpp::synchronize_new_thread()); \n```\n\n使用`subscribe_on()`操作符，可观察的流在不同的线程中排队。第一个流以事件循环作为其协调线程进行调度，第二个流在另一个 C++ 线程上进行调度:\n\n```cpp\n    auto onNext = [](int v) { \n        std::lock_guard<std::mutex> lock(printMutex); \n        std::cout << \"Observable Thread id => \" \n                  << std::this_thread::get_id() \n                  << \"tOnNext: \" << v << std::endl; \n    }; \n\n    auto onError = [](std::exception_ptr ep) { \n        std::lock_guard<std::mutex> lock(printMutex); \n        std::cout << \"Observable Thread id => \" \n                  << std::this_thread::get_id() \n                  << \"tOnError: \" \n                  << rxcpp::util::what(ep).c_str() << std::endl; \n    }; \n```\n\n前面的 Lambda 函数被定义为参数，代替`subscribe`方法的`on_next`和`on_error`函数。这些 Lambda 函数受互斥保护，以同步对`std::ostream`运算符的调用:\n\n```cpp\n    //-------- Subscribing the merged sequence \n    s1.merge(s2).as_blocking().subscribe( \n        onNext, onError, \n        []() { std::cout << \"OnCompleted\" << std::endl; }); \n\n    //-------- Print the main thread details \n    std::cout << \"Main Thread id => \" \n        << std::this_thread::get_id() \n        << std::endl; \n} \n```\n\n代码的输出如下所示:\n\n```cpp\nObservable Thread id => 12380   OnNext: 1 \nObservable Thread id => 9076    OnNext: 25 \nObservable Thread id => 12380   OnNext: 4 \nObservable Thread id => 9076    OnNext: 36 \nObservable Thread id => 12380   OnNext: 9 \nObservable Thread id => 12380   OnNext: 16 \nObservable Thread id => 9076    OnNext: 49 \nObservable Thread id => 9076    OnNext: 64 \nObservable Thread id => 9076    OnNext: 81 \nObservable Thread id => 9076    OnError: Error from producer! \nMain Thread id => 10692\n```\n\n# 基于事件的流处理–一些示例\n\n在我们结束本章之前，让我们讨论几个例子，使用 RxCpp 库使用基于事件的系统。在本节中，我们将讨论两个例子，以了解 RxCpp 库在满足现实场景方面的有效性。我们将讨论一个使用 RxCpp 库演示流中数据聚合和应用事件处理的例子。\n\n# 基于流数据的聚合\n\n在本节中，流项目是用户定义的类型，用于表示员工，代码旨在根据员工的角色和工资对输入流进行分组:\n\n```cpp\n#include \"rxcpp/rx.hpp\" \n\nnamespace Rx { \n    using namespace rxcpp; \n    using namespace rxcpp::sources; \n    using namespace rxcpp::subjects; \n    using namespace rxcpp::util; \n} \n\nusing namespace std; \n\nstruct Employee { \n    string name; \n    string role; \n    int salary; \n}; \n```\n\n代码中所需的库和名称空间都包括在内，并且声明了表示`Employee`的数据结构。`Employee`类型结构简单，有`name`、`role`、`salary`等数据项。我们将薪资字段视为一个整数:\n\n```cpp\nint main() \n{ \n    Rx::subject<Employee> employees; \n\n    // Group Salaries by Role \n    auto role_sal = employees.\n```\n\n```cpp\n        get_observable(). \n        group_by( \n            [](Employee& e) { return e.role; }, \n            [](Employee& e) { return e.salary; }); \n```\n\n在`main()`功能中，使用`Employee`类型创建一个主体，以创建一个**热可观察**。基于角色和薪资的分组是在主体的可观察项上执行的。RxCpp 运算符`group_by()`返回一个发出`grouped_observables`的可观测值，每个可观测值对应于来自源可观测值的唯一键/值对:\n\n```cpp\n    // Combine min max and average reductions based on salary. \n    auto result = role_sal. \n        map([](Rx::grouped_observable<string, int> group) { \n            return group. \n                count(). \n                combine_latest([=](int count, int min, int max, double average) { \n                return make_tuple(group.get_key(), count, min, max, average); \n        }, \n        group.min(), \n        group.max(), \n        group.map([](int salary) -> double { return salary; }).average()); \n    }). \n    merge(); \n```\n\n这里，结果“可观察值”结合了基于角色的“可观察值”，而基于薪资的减少是通过附加每个角色的最低薪资、最高薪资和平均薪资来执行的。当所有参数都有值时，将调用`combine_latest()`内部的 Lambda。在这种情况下，当一个特定的组完成时，对应于该组的流内部的所有值都被简化为单元组。因此，每个角色只调用一次 Lambda，每次迭代都有最终值。这里，应用于`group`的映射返回类型为`observable<tuple<string, int, int, int, double>>`的可观测值，`merge()`运算符返回类型为`tuple<string, int, int, int, double>`的可观测值。应用合并是为了防止数据丢失，因为分组的可观察数据是热的，如果不立即订阅，数据将会丢失:\n\n```cpp\n    // Display the aggregated result \n    result. \n        subscribe(Rx::apply_to( \n        [](string role, int count, int min, int max, double avg) { \n          std::cout << role.c_str() << \":tCount = \" << count <<  \n           \", Salary Range = [\" << min  \n            << \"-\" << max << \"], Average Salary = \" << avg << endl; \n        })); \n\n    // Supplying input data \n    Rx::observable<>::from( \n        Employee{ \"Jon\", \"Engineer\", 60000 }, \n        Employee{ \"Tyrion\", \"Manager\", 120000 }, \n        Employee{ \"Arya\", \"Engineer\", 92000 }, \n        Employee{ \"Sansa\", \"Manager\", 150000 }, \n        Employee{ \"Cersei\", \"Accountant\", 76000 }, \n        Employee{ \"Jaime\", \"Engineer\", 52000 }). \n        subscribe(employees.get_subscriber()); \n\n    return 0; \n} \n```\n\n然后订阅结果可观测量，以便显示输入数据的聚合结果。数据项从`employees`主题提供给订户，该主题是用`Employees`类型创建的。在前面的代码中，源可以是任何东西，例如通过网络或从另一个线程检索的数据。由于这里创建的可观测值是一个热门的可观测值，因此聚合是基于提供的最新数据执行的。\n\n该代码的输出如下:\n\n```cpp\nAccountant:    Count = 1, Salary Range = [76000-76000], Average Salary = 76000 \nEngineer:      Count = 3, Salary Range = [52000-92000], Average Salary = 68000 \nManager:       Count = 2, Salary Range = [120000-150000], Average Salary = 135000 \n```\n\n# 应用事件处理示例\n\n下面的示例是一个命令行程序，用事件来表示用户界面应用的基本操作。我们将在这个程序中使用 RxCpp 来处理这些事件的流程。该应用是一个命令行程序，可以很容易地映射到图形用户界面程序。为了简洁起见，在代码清单中这样做了:\n\n```cpp\n//--------- UI_EventsApp.cpp \n#include <rxcpp/rx.hpp> \n#include <cassert> \n#include <cctype> \n#include <clocale> \n\nnamespace Rx { \n    using namespace rxcpp; \n    using namespace rxcpp::sources; \n    using namespace rxcpp::operators; \n    using namespace rxcpp::util; \n    using namespace rxcpp::subjects; \n} \n\nusing namespace Rx; \nusing namespace std::chrono; \n\n// Application events \nenum class AppEvent { \n    Active, \n    Inactive, \n    Data, \n    Close, \n    Finish, \n    Other \n}; \n```\n\n我们将在程序中使用的库和名称空间包含(声明)在这里。此外，还声明了一个枚举`AppEvent`，以表示可以从通用系统发出的一些基本事件状态:\n\n```cpp\nint main() \n{ \n    //------------------- \n    // A or a - Active \n    // I or i - Inactive \n    // D or d - Data \n    // C or c - Close \n    // F or f - Finish \n    // default - Other \n    auto events = Rx::observable<>::create<AppEvent>( \n        [](Rx::subscriber<AppEvent> dest) { \n        std::cout << \"Enter Application Events:n\"; \n        for (;;) { \n            int key = std::cin.get(); \n            AppEvent current_event = AppEvent::Other; \n\n            switch (std::tolower(key)) { \n            case 'a': current_event = AppEvent::Active; break; \n            case 'i': current_event = AppEvent::Inactive; break; \n            case 'd': current_event = AppEvent::Data; break; \n            case 'c': current_event = AppEvent::Close; break; \n            case 'f': current_event = AppEvent::Finish; break; \n            default:  current_event = AppEvent::Other; \n            } \n\n            if (current_event == AppEvent::Finish) { \n                dest.on_completed(); \n                break; \n            } \n            else { \n                dest.on_next(current_event); \n            } \n        } \n    }). \n    on_error_resume_next([](std::exception_ptr ep) { \n        return rxcpp::observable<>::just(AppEvent::Finish); \n    }). \n    publish(); \n```\n\n在前面的代码中，我们通过将一些键盘条目映射到定义的事件类型，创建了一个`AppEvent`类型的可观察流。`create`函数的λ内部的无限循环表示图形用户界面应用中的`event_loop/message_loop`。为了使冷的可观测值声明为热的，并获得独立于后续订阅的到源的连接，使用`publish()`操作符。它还有助于将流中的最新值发送给新订户:\n\n```cpp\n    // Event fires when application is active \n    auto appActive = events. \n        filter([](AppEvent const& event) { \n        return event == AppEvent::Active; \n    }); \n\n    // Event fires when application is inactive \n    auto appInactive = events. \n        filter([](AppEvent const& event) { \n        return event == AppEvent::Inactive; \n    }); \n\n    // Event fires when data Stream starts \n    auto appData = events. \n        filter([](AppEvent const& event) { \n        return event == AppEvent::Data; \n    }); \n\n    // Event fires when application is closed \n    auto appClose = events. \n        filter([](AppEvent const& event) { \n        return event == AppEvent::Close; \n    });\n```\n\n定义了一些过滤的可观察值，以处理反应系统的用例。每当流中出现`AppEvent::Active`事件时，`appActive`可观察值就会过滤掉。同样地，`appInactive`代表`AppEvent::Inactive`，`appData`代表`AppEvent::Data`，而`appClose`代表`AppEvent::Close`事件:\n\n```cpp\n    auto dataFromApp = appActive. \n        map([=](AppEvent const& event) { \n        std::cout << \"**Application Active**n\" << std::flush; \n        return appData. // Return all the data events \n            take_until(appInactive). // Stop when the application goes inactive \n            finally([]() { \n            std::cout << \"**Application Inactive**n\"; \n        }); \n    }). \n        switch_on_next(). // only listen to most recent data \n        take_until(appClose). // stop everything when Finish/Close event recieved \n        finally([]() { \n        std::cout << \"**Application Close/Finish**n\"; \n    }); \n\n    dataFromApp. \n        subscribe([](AppEvent const& event) { \n        std::cout << \"**Application Data**n\" << std::flush; \n    }); \n\n    events.connect(); \n\n    return 0; \n} \n```\n\n只有在接收到`AppEvent::Active`事件时，程序才会开始接受来自可观察事件的数据流。然后，应用将接受数据，直到收到`AppEvent::Inactive`。只有在发出下一个`AppEvent::Active`时，事件流才会恢复。当发出`AppEvent::Close`或`AppEvent::Finish`时，应用将优雅地退出，类似于图形用户界面应用中的**关闭**或**应用**事件/消息。\n\n# 摘要\n\n在本章中，我们讨论了`RxCpp`中的错误处理，以及一些在 RxCpp 库中处理流的高级构造和操作符。当我们讨论错误处理机制时，我们参观了反应系统的基本原理，并且更加强调了反应系统的关键支柱之一，弹性。我们讨论了诸如错误处理程序(`on_error`)等需要与订阅一起使用的特性。此外，我们还讨论了 RxCpp 操作符，如`on_error_resume_next()`、`retry()`和`finally()`，以讨论当错误出现时如何继续流，如何等待流的生产者纠正错误并继续序列，以及如何执行适用于成功和错误路径的常见操作。最后，我们讨论了两个示例程序，以进一步了解流处理。这些程序演示了如何使用 RxCpp 库来处理 UX 事件流(使用控制台程序模拟)和聚合数据流。\n\n在下一章中，我们将研究如何为 RxCpp 可观测值编写自定义运算符。"
  },
  {
    "path": "docs/cpp-react-prog/README.md",
    "content": "# C++ 反应式编程\n\n> 原书：[C++ Reactive Programming](https://libgen.rs/book/index.php?md5=EAAD897414447E821196F7913CC7AEBA)\n> \n> 协议：[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)\n> \n> 阶段：机翻（1）\n> \n> 自豪地采用[谷歌翻译](https://translate.google.cn/)\n> \n> 让开源界提前感受入关。\n\n* [在线阅读](https://ccpp.apachecn.org)\n* [在线阅读（Gitee）](https://apachecn.gitee.io/apachecn-c-cpp-zh/)\n* [ApacheCN 学习资源](http://docs.apachecn.org/)\n\n## 贡献指南\n\n本项目需要校对，欢迎大家提交 Pull Request。\n\n> 请您勇敢地去翻译和改进翻译。虽然我们追求卓越，但我们并不要求您做到十全十美，因此请不要担心因为翻译上犯错——在大部分情况下，我们的服务器已经记录所有的翻译，因此您不必担心会因为您的失误遭到无法挽回的破坏。（改编自维基百科）\n\n## 联系方式\n\n### 负责人\n\n* [飞龙](https://github.com/wizardforcel): 562826179\n\n### 其他\n\n*   在我们的 [apachecn/apachecn-c-cpp-zh](https://github.com/apachecn/apachecn-c-cpp-zh) github 上提 issue.\n*   发邮件到 Email: `apachecn@163.com`.\n*   在我们的 [组织学习交流群](http://www.apachecn.org/organization/348.html) 中联系群主/管理员即可.\n\n## 赞助我们\n\n![](http://data.apachecn.org/img/about/donate.jpg)\n"
  },
  {
    "path": "docs/cpp-react-prog/SUMMARY.md",
    "content": "+   [C++ 反应式编程](README.md)\n+   [零、前言](00.md)\n+   [一、反应式编程模型——概述和历史](01.md)\n+   [二、现代 C++ 及其关键习语概述](02.md)\n+   [三、C++ 中的语言级并发和并行](03.md)\n+   [四、C++ 中的异步和无锁编程](04.md)\n+   [五、可观察对象介绍](05.md)\n+   [六、C++ 事件流编程简介](06.md)\n+   [七、数据流计算和 RxCpp 库简介](07.md)\n+   [八、关键要素](08.md)\n+   [九、Qt/C++ 反应式图形用户界面编程](09.md)\n+   [十、C++ 反应式编程的设计模式和习惯用法](10.md)\n+   [十一、使用 C++ 的反应式微服务](11.md)\n+   [十二、高级流和错误处理](12.md)\n"
  },
  {
    "path": "docs/cpp-sys-prog-cb/00.md",
    "content": "# 零、前言\n\n这本书旨在为系统编程的基本方面提供现成的解决方案(给开发人员)，尽可能使用最新的 C++ 标准。系统编程涉及构建与操作系统紧密交互的计算机程序，并允许计算机硬件与程序员和用户交互。由于其高效的特性，即低级计算、数据抽象和面向对象的特性，C++ 是系统编程的首选语言。您将学习如何创建健壮的并发系统，还将了解具有共享内存和管道的进程间通信机制。展望未来，您将深入研究 C++ 内置库和框架，以便根据您的需求设计健壮的系统。\n\n# 这本书是给谁的\n\n这本书是为想获得系统编程实用知识的 C++ 开发人员准备的。虽然没有 Linux 系统编程的经验，但是中级的 C++ 知识是必要的。\n\n# 这本书涵盖了什么\n\n[第 1 章](01.html)、*系统编程入门*、*、*向您介绍了一些基础知识，如学习外壳、用户和组、进程标识和线程标识，以便能够熟练使用 Linux 系统，等等，这些是您在本书剩余部分必须了解的。例如，您将了解 Linux 是如何设计的，外壳、用户和组、进程标识和线程标识。此外，您将学习如何开发一个简单的`Hello World`程序，编写它的 makefile，执行它，并调试它。这些知识虽然是基本的，但对于后面章节中出现的更高级的主题来说是至关重要的。\n\n[第二章](02.html)、*重温 C++* 、*T5】刷新你对 C++ 17 的理解，这将贯穿全书。它将展示为什么 C++ 代表了一个编写高质量代码的绝佳机会，这种代码比以往任何时候都更加简洁和可移植。本章包含了 C++ 11/17/20 引入的所有新特性，您会发现这些特性在本书中很有用。*\n\n[第三章](03.html)、*处理进程和线程，*向你介绍进程和线程，它们是任何阐述的基础。一个程序很少仅仅由一个过程组成。本章揭示了在 C++ 中处理线程和进程的技术。本章将展示与 POSIX 相比，处理线程(和任务)是多么容易和方便。虽然 C++ 没有创建过程的正式方式，但是很少有线程不能完成工作的情况*。*\n\n[第四章](04.html)、*深度潜入内存管理，*给大家介绍内存，这是处理系统开发的核心概念之一。分配、释放和学习如何管理内存以及 C++ 可以提供什么来简化和管理内存至关重要。此外，本章还介绍了如何检查和分配对齐的内存以及如何处理内存映射的输入/输出\n\n[第 5 章](05.html)、*使用互斥体、信号量和条件变量*、*T5】向我们展示了 POSIX 机制解决方案和 C++ 提供的同步线程和进程的解决方案。*\n\n[第 6 章](06.html)、*管道、先进先出(FIFO)、消息队列和共享内存，*着重于使进程相互通信。有不同的解决方案——管道、先进先出、消息队列和共享内存。对于每个进程间通信机制，提供一个配方。\n\n[第 7 章](07.html)、*网络编程*，演示了通信是如何从连接到结束的。不同机器上的进程之间的通信是当今互联网的基础，而 TCP/IP 是事实上的标准。将详细描述 **TCP** (传输控制协议的简称)和 **UDP** (用户数据报协议**的简称**，因为前者代表面向连接，后者代表面向无连接。这在当今非常重要，尤其是在线视频流服务。\n\n[第 8 章](08.html)、*处理控制台 I/O 和文件*、*T5】向您展示了处理文件、控制台 I/O 和字符串流的有用方法。*\n\n[第 9 章](09.html)、*处理时间接口、*通过 C++ 和 POSIX 提供的特性，让您深刻理解如何处理和测量时间。本章将提供每种方法的现成配方。\n\n[第 10 章](10.html)、*管理信号，*向我们介绍了软件中断信号。它们提供了一种管理异步事件的方法。例如，用户从终端键入中断键，或者另一个进程发送必须管理的信号。每个信号都有一个以`SIG`开头的名字(例如`SIGABRT`)。本章将向读者展示如何编写代码来正确管理软件中断，Linux 为每个信号定义的默认操作是什么，以及如何覆盖它们。\n\n[第 11 章](11.html)、*调度、*向您展示了如何使用 POSIX(c++ 标准没有提供这个)来设置调度器参数、调度器策略和调度器优先级。迄今为止，系统编程是关于与底层操作系统的交互。调度器是每个操作系统的主要组件之一，它影响进程在 CPU 上的分配方式。有些情况下，开发人员需要对此进行控制，或者至少试图影响调度程序。\n\n# 充分利用这本书\n\n以下是这本书的要求清单:\n\n*   C++ 中级知识。\n*   任何额外的要求都在每章的*技术要求*部分提及。\n*   免责声明:C++ 20 标准已由 WG21 在 2 月底布拉格的一次会议上批准(即技术上最终确定)。这意味着，本书使用的 GCC 编译器版本 8.3.0 不包括(或支持非常非常有限的)新的和酷的 C++ 20 特性。因此，Docker 映像不包括 C++ 20 配方代码。\n    GCC 保持分支机构最新功能的开发(你必须为此使用适当的标志，例如`-std=c++ 2a`)；因此，鼓励你自己去试验它们。所以，克隆和探索 GCC 合同和模块分支，享受乐趣吧。\n*   一些菜谱(尤其是在[第 11 章](11.html)、*调度*中)需要以管理员权限运行的 Docker 映像才能正确执行。根据您的 Docker 配置，您可能需要使用`sudo`运行 Docker。为了避免这种情况，您可以创建一个 Linux 组(例如，`docker`)并向其中添加用户。\n\n# 下载示例代码文件\n\n你可以从你在[www.packt.com](http://www.packt.com)的账户下载这本书的示例代码文件。如果您在其他地方购买了这本书，您可以访问[www.packtpub.com/support](https://www.packtpub.com/support)并注册将文件直接通过电子邮件发送给您。\n\n您可以按照以下步骤下载代码文件:\n\n1.  登录或注册[www.packt.com](http://www.packt.com)。\n2.  选择“支持”选项卡。\n3.  点击代码下载。\n4.  在搜索框中输入图书的名称，并按照屏幕指示进行操作。\n\n下载文件后，请确保使用最新版本的解压缩文件夹:\n\n*   视窗系统的 WinRAR/7-Zip\n*   zipeg/izp/un ARX for MAC\n*   适用于 Linux 的 7-Zip/PeaZip\n\n这本书的代码包也托管在 GitHub 上[https://GitHub . com/PacktPublishing/C-System-Programming-cook book](https://github.com/PacktPublishing/C-System-Programming-Cookbook)。如果代码有更新，它将在现有的 GitHub 存储库中更新。\n\n我们还有来自丰富的图书和视频目录的其他代码包，可在**[【https://github.com/PacktPublishing/】](https://github.com/PacktPublishing/)**获得。看看他们！\n\n# 下载彩色图像\n\n我们还提供了一个 PDF 文件，其中包含本书中使用的截图/图表的彩色图像。可以在这里下载:[https://static . packt-cdn . com/downloads/9781838646554 _ color images . pdf](https://static.packt-cdn.com/downloads/9781838646554_ColorImages.pdf)。\n\n# 行动中的代码\n\n请访问以下链接查看 CiA 视频:[http://bit.ly/2uXftdA](http://bit.ly/2uXftdA)\n\n# 使用的约定\n\n本书通篇使用了许多文本约定。\n\n`CodeInText`:表示文本中的码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟网址、用户输入和推特句柄。这里有一个例子:“第二步，我们开始开发`main`方法。”\n\n代码块设置如下:\n\n```cpp\n std::cout << \"Start ... \" << std::endl;\n    {\n        User* developer = new User();\n        developer->cheers();\n        delete developer;\n    }\n```\n\n当我们希望将您的注意力吸引到代码块的特定部分时，相关的行或项目以粗体显示:\n\n```cpp\nauto* mapPtr = static_cast<T*> (mmap(0, sizeof(T) * n, \n                                PROT_READ | PROT_WRITE, \n```\n\n任何命令行输入或输出都编写如下:\n\n```cpp\n $ grep \"text\" filename\n $ ls -l | grep filename \n```\n\n**粗体**:表示一个新的术语，一个重要的单词，或者你在屏幕上看到的单词。例如，菜单或对话框中的单词像这样出现在文本中。下面是一个示例:“从管理面板中选择系统信息。”\n\nWarnings or important notes appear like this. Tips and tricks appear like this.\n\n# 部分\n\n在这本书里，你会发现几个经常出现的标题(*准备*，*怎么做...*、*它是如何工作的...*、*还有更多...*和*参见*。\n\n要给出如何完成配方的明确说明，请使用以下部分。\n\n# 准备好\n\n本节告诉您配方中的预期内容，并描述如何设置配方所需的任何软件或任何初步设置。\n\n# 怎么做…\n\n本节包含遵循配方所需的步骤。\n\n# 它是如何工作的…\n\n这一部分通常包括对前一部分发生的事情的详细解释。\n\n# 还有更多…\n\n本节包含关于配方的附加信息，以便您更好地了解配方。\n\n# 请参见\n\n本节提供了该配方的其他有用信息的有用链接。\n\n# 取得联系\n\n我们随时欢迎读者的反馈。\n\n**一般反馈**:如果你对这本书的任何方面有疑问，在你的信息主题中提到书名，发邮件给我们`customercare@packtpub.com`。\n\n**勘误表**:虽然我们已经尽了最大的努力来保证内容的准确性，但是错误还是会发生。如果你在这本书里发现了一个错误，如果你能向我们报告，我们将不胜感激。请访问[www.packtpub.com/support/errata](https://www.packtpub.com/support/errata)，选择您的图书，点击勘误表提交链接，并输入详细信息。\n\n**盗版**:如果您在互联网上遇到任何形式的我们作品的非法拷贝，如果您能提供我们的位置地址或网站名称，我们将不胜感激。请通过`copyright@packt.com`联系我们，并提供材料链接。\n\n**如果你有兴趣成为一名作者**:如果有一个你有专长的话题，你有兴趣写或者投稿一本书，请访问[authors.packtpub.com](http://authors.packtpub.com/)。\n\n# 复习\n\n请留下评论。一旦你阅读并使用了这本书，为什么不在你购买它的网站上留下评论呢？然后，潜在的读者可以看到并使用您不带偏见的意见来做出购买决定，我们在 Packt 可以了解您对我们产品的看法，我们的作者可以看到您对他们的书的反馈。谢谢大家！\n\n更多关于 Packt 的信息，请访问[packt.com](http://www.packt.com/)。"
  },
  {
    "path": "docs/cpp-sys-prog-cb/01.md",
    "content": "# 一、系统编程入门\n\n在这一章，你将被介绍整本书的框架基础。您将学习(或更新您的知识)Linux 是如何设计的，您还将学习外壳、用户和组、进程标识和线程标识，以便能够熟练使用 Linux 系统，并为下一章做好准备。此外，你还将学习如何开发一个简单的`hello world`程序，并了解它的 makefile，以及如何执行和调试它。本章的另一个重要方面是从 shell 和源代码的角度了解 Linux 如何处理错误。这些基础知识对于理解后面章节中的其他高级主题非常重要。如果不需要复习，您可以安全地跳过这一章和下一章。\n\n本章将涵盖以下食谱:\n\n*   学习 Linux 基础知识—体系结构\n*   学习 Linux 基础知识–外壳\n*   学习 Linux 基础知识-用户\n*   使用 makefile 编译和链接程序\n*   使用 **GNU 项目调试器** ( **GDB** )调试程序\n*   学习 Linux 基础——进程和线程\n*   处理 Linux bash 错误\n*   处理 Linux 代码错误\n\n# 技术要求\n\n为了让您立即尝试这些程序，我们设置了一个 Docker 映像，其中包含了我们在整本书中需要的所有工具和库。这是基于 Ubuntu 19.04 的。\n\n要进行设置，请执行以下步骤:\n\n1.  从[www.docker.com](https://www.docker.com/)下载并安装 Docker 引擎。\n2.  从 Docker Hub 中拉出图像:`docker pull kasperondocker/system_programming_cookbook:latest`。\n3.  图像现在应该可以使用了。输入以下命令查看图像:`docker images`。\n4.  你现在至少应该有这个形象:`kasperondocker/system_programming_cookbook`。\n5.  借助以下命令，使用交互式外壳运行 Docker 映像:`docker run -it --cap-add sys_ptrace kasperondocker/system_programming_cookbook:latest /bin/bash`。\n6.  运行容器上的外壳现已可用。运行`root@39a5a8934370/# cd /BOOK/`按章节开发所有程序。\n\n需要`--cap-add sys_ptrace`参数来允许 Docker 容器中的 GDB 设置断点，默认情况下，Docker 不允许设置断点。\n\n# 学习 Linux 基础-架构\n\n**Linux** 是 Unix 操作系统的克隆，由 Linus Torvalds 在 90 年代初开发。这是一个多用户、多任务操作系统，运行在各种平台上。出于性能原因，Linux 内核采用了单片架构。这意味着它在一个二进制文件中是独立的，并且它的所有**服务**都在内核空间中运行。这是一开始最有争议的话题之一。安迪·塔南鲍姆(阿姆斯特丹自由大学教授)反对它的单一系统，他说:*这是回到 1970 年代的一大步。*他还反驳了它的可移植性，称: *LINUX 与 80 x 86 的联系相当紧密。不是办法*。在 *minix* 用户群中，还是有涉及 Torvalds、Tanenbaum 等人的全聊天的线程。\n\n下图显示了主要的 Linux 构建块:\n\n![](img/9a7b4405-e9e4-431b-b068-0883d1189150.png)\n\n让我们描述一下我们在图表中看到的层:\n\n*   在顶层，有用户应用、进程、编译器和工具。该层(运行在用户空间)通过系统调用与 Linux 内核(运行在内核空间)通信。\n*   **系统库**:这些是一组函数，应用可以通过它们与内核进行交互。\n*   **内核**:这个组件包含了 Linux 系统的核心。除此之外，它还有调度程序、网络、内存管理和文件系统。\n*   **内核模块**:这些模块包含一些内核代码，它们仍然在内核空间中运行，但是是完全动态的(也就是说，它们可以随运行的系统一起加载和卸载)。它们通常包含设备驱动程序、特定于实现协议的特定硬件模块的内核代码等。内核模块的一个巨大优势是用户可以在不重建内核的情况下加载它们。\n\n**GNU** 是递归首字母缩略词，代表 **GNU 不是 Unix** 。GNU 是一个自由软件的操作系统。注意这里的术语*操作系统*。事实上，单独使用 GNU 意味着代表操作系统所需的全套工具、软件和内核部分。GNU 操作系统内核被称为 **Hurd** 。由于赫德还没有做好生产准备，GNU 通常使用 Linux 内核，这种组合被称为 **GNU/Linux 操作系统**。\n\n那么，GNU/Linux 操作系统上有哪些 GNU 组件呢？软件包*例如 **GNU 编译器集合**(**GCC**)**GNU C 库**、GDB、GNU Bash 外壳和 **GNU 网络对象模型环境** ( **GNOME** )桌面环境，仅举几例。理查德·斯托尔曼和**自由软件基金会**(**FSF**)——史泰曼是其创始人——创作了**自由软件定义**来帮助尊重用户的自由。*自由软件*被认为是授予用户以下四种自由(所谓**基本自由**:[https://isocpp.org/std/the-standard](https://isocpp.org/std/the-standard))的任何软件包:\n\n1.  为任何目的运行程序的自由(自由 *0* )。\n2.  研究程序如何工作和改变它的自由，所以它可以按照你的意愿进行计算(自由 *1* )。获取源代码是实现这一点的先决条件。\n3.  重新分发副本以便帮助他人的自由(自由 *2* )。\n4.  向他人分发您的修改版本的自由(自由 *3* )。通过这样做，你可以给整个社区一个从你的改变中获益的机会。获取源代码是实现这一点的先决条件。\n\n这些原则的具体实例在 FSF 创作的 GNU/GPU 许可中。所有的 GNU 软件包都是在 GNU/GPU 许可下发布的。\n\n# 怎么做...\n\nLinux 在所有发行版中都有一个非常标准的文件夹结构，所以知道这一点可以让你很容易地找到程序并将它们安装在正确的位置。让我们看一下它，如下所示:\n\n1.  在 Docker 图像上打开一个终端。\n2.  键入命令`ls -l /`。\n\n# 它是如何工作的...\n\n该命令的输出将包含以下文件夹:\n\n![](img/4a845486-dd7f-40b4-a271-fc851692fe1e.png)\n\n正如您所看到的，这个文件夹结构非常有条理，并且在所有发行版中都是一致的。在幕后，Linux 文件系统是非常模块化和灵活的。用户应用可以与 GNU C 库(提供打开、读取、写入和关闭等接口)或直接与 Linux 系统调用交互。在这种情况下，系统调用接口与**虚拟的** **文件系统** *(* 通常被称为***)*。VFS 是具体文件系统实现之上的抽象(例如，ext3、**日志文件系统** ( **JFS** )等等)。我们可以想象，这种架构提供了高度的灵活性。**\n\n **# 学习 Linux 基础-外壳\n\n外壳是一个命令解释器，它接收输入中的命令，将它们重定向到 GNU/Linux，并返回输出。它是用户和 GNU/Linux 之间最常见的接口。有不同的 shell 程序可用。最常用的是 Bash shell(GNU 项目的一部分)、tcsh shell、ksh shell 和 zsh shell(这基本上是一个扩展的 Bash shell)。\n\n你为什么需要一个贝壳？如果用户需要通过**命令行**与操作系统交互，则需要外壳。在这个食谱中，我们将展示一些最常见的 shell 命令。很多时候，术语*外壳*和*终端*可以互换使用，尽管严格来说，它们不是完全一样的东西。\n\n# 怎么做...\n\n在本节中，我们将学习在 shell 上运行的基本命令，例如，查找文件、`grep`将文本导入文件、复制和删除:\n\n1.  打开一个 shell:根据 GNU/Linux 发行版的不同，打开一个新的 shell 命令有不同的快捷键。在 Ubuntu 上，按*Ctrl*+*Alt*+*T*，或者按 *Alt* + *F2、*然后输入`gnome-terminal`。\n2.  关闭外壳:要关闭终端，只需输入`exit`并按*进入*。\n3.  `find`命令:用于在目录层次结构中搜索文件。最简单的形式是这样的:\n\n```cpp\nfind . -name file\n```\n\n它也支持通配符:\n\n```cpp\n$ find /usr/local \"python*\"\n```\n\n4.  `grep`命令通过匹配模式打印线条:\n\n```cpp\n $ grep \"text\" filename\n```\n\n`grep`还支持递归搜索:\n\n```cpp\n $ grep \"text\" -R /usr/share\n```\n\n5.  管道命令:在外壳上运行的命令可以连接起来，使一个命令的输出成为另一个命令的输入。连接是通过`|`(管道)操作符完成的:\n\n```cpp\n$ ls -l | grep filename\n```\n\n6.  编辑文件:在 Linux 上编辑文件最常用的两个工具是`vi`和`emacs`(如果你对编辑文件不感兴趣，`cat filename`会将文件打印成标准输出)。前者由 Unix 操作系统继承，后者是 GNU 项目的一部分。本书将广泛使用`vi`:\n\n```cpp\n $ vi filename\n```\n\n接下来，我们将研究与文件操作相关的 shell 命令。\n\n7.  这是删除文件的命令:\n\n```cpp\n$ rm filename\n```\n\n8.  这是删除目录的命令:\n\n```cpp\n$ rm -r directoryName\n```\n\n9.  这是克隆文件的命令:\n\n```cpp\n$ cp file1 file2\n```\n\n10.  这是克隆文件夹的命令:\n\n```cpp\n$ cp -r folder1 folder2  \n```\n\n11.  这是使用相对和绝对路径克隆文件夹的命令:\n\n```cpp\n$ cp -r /usr/local/folder1 relative/folder2\n```\n\n下一节将描述这些命令。\n\n# 它是如何工作的...\n\n让我们来看看*中讨论的命令是如何做到的...*部分，详细说明:\n\n1.  第一个命令从当前文件夹中搜索(`.`)，可以包含绝对路径(例如`/usr/local`)或相对路径(例如`tmp/binaries`)。比如这里，`-name`就是要搜索的文件。\n2.  第二个命令从`/usr/local`文件夹中搜索任何以`python`开头的文件或文件夹。`find`命令提供了巨大的灵活性和多种选择。更多信息，请通过`man find`命令参考`man page`。\n3.  `grep`命令在`filename`文件中搜索并打印任何包含单词`text`的行。\n4.  `grep`递归搜索命令从`/usr/share`文件夹中递归搜索并打印任何文件中包含单词`text`的任何一行。\n5.  Pipe 命令(`|`):第一个命令的输出如下截图所示。所有文件和目录的列表作为输入传递给第二个命令(`grep`，它将用于`grep`文件名:\n\n![](img/11e480e2-e934-4db1-8d69-fe05a480546d.png)\n\n现在，让我们来看看执行诸如编辑文件、添加/删除文件和目录等操作的命令。\n\n**编辑文件**:\n\n*   `vi`命令将在编辑模式下打开文件名，假设当前用户对其具有写权限(我们将在后面更详细地讨论权限)。\n    以下是`vi`中最常用命令的简短总结:\n    *   *移动* *+ :* (即*移动*键+冒号)切换编辑模式。\n    *   *Shift + :i* 插入。\n    *   *Shift +:追加一个*。\n    *   *Shift + :q！*退出当前会话，不保存。\n    *   *Shift + :wq* 保存并退出当前会话。\n    *   *Shift +:设置 nu* 显示文件上的行号。\n    *   *班次+ :23* ( *进入*)到 23 号线。\n    *   按下( *Esc* ) 键切换到命令模式。\n    *   *。*重复最后一个命令。\n    *   *cw* 改变单词，或将光标指向单词的开头。\n    *   *dd 至*移除当前线路。\n    *   *yy* 复制当前行。如果在 *yy* 命令之前选择了一个数字 *N* ，则 *N* 行将被复制。\n    *   *p* 用 *yy* 命令粘贴复制的行。\n    *   *u* 撤销。\n\n**添加和删除文件和目录**:\n\n1.  第一个命令删除名为`filename`的文件。\n2.  第二个命令递归删除`directoryName`及其内容。\n3.  第三个命令创建`file2`，这是`file1`的精确副本。\n4.  第四个命令创建`folder2`作为`folder1`的克隆:\n\n![](img/1d86e0fb-6dda-477a-b460-51fc4ae8f88a.png)\n\n在这个配方中显示的命令的执行中有一个共同的模式。它们如下所示:\n\n1.  用户输入命令，点击*进入*。\n2.  该命令由 Linux 解释。\n3.  Linux 与其不同的部分(内存管理、网络、文件系统等)交互来执行命令。这发生在内核空间**。**\n4.  结果将返回给用户。\n\n# 还有更多...\n\n这个食谱显示了一些最常见的命令。掌握所有选项，即使只是最常见的 shell 命令，也是很棘手的，这就是为什么`man pages`被创建的原因。它们为 Linux 用户提供了坚实而清晰的参考。\n\n# 请参见\n\n[第八章](08.html)、*处理控制台 I/O 和文件*，将深入到控制台 I/O 和文件管理。\n\n# 学习 Linux 基础知识-用户\n\nLinux 是一个多用户和多任务操作系统，所以基本的用户管理技能是必须的。这个方法将向您展示文件和目录的权限是如何构造的，如何添加和删除用户，如何更改用户的密码，以及如何将用户分配到组。\n\n# 怎么做...\n\n以下一系列步骤显示了用于基本用户管理活动的有用命令:\n\n1.  **创建用户**:使用 Linux 为每个人配置一个用户不仅是最佳实践，也是推荐的做法。创建用户非常简单:\n\n```cpp\nroot@90f5b4545a54:~# adduser spacex --ingroup developers\nAdding user `spacex' ...\nAdding new user `spacex' (1001) with group `developers' ...\nCreating home directory `/home/spacex' ...\nCopying files from `/etc/skel' ...\nNew password:\nRetype new password:\npasswd: password updated successfully\nChanging the user information for spacex\nEnter the new value, or press ENTER for the default\nFull Name []: Onorato\nRoom Number []:\nWork Phone []:\nHome Phone []:\nOther []:\nIs the information correct? [Y/n] Y\n```\n\n`spacex`用户已创建并分配到现有的`developers`组。要切换到新创建的用户，请使用新用户的凭据登录:\n\n```cpp\nroot@90f5b4545a54:~# login spacex\nPassword:\nWelcome to Ubuntu 19.04 (GNU/Linux 4.9.125-linuxkit x86_64)\n* Documentation: https://help.ubuntu.com\n* Management: https://landscape.canonical.com\n* Support: https://ubuntu.com/advantage\nThis system has been minimized by removing packages and content that are\nnot required on a system that users do not log into.\nTo restore this content, you can run the 'unminimize' command.\nThe programs included with the Ubuntu system are free software;\nthe exact distribution terms for each program are described in the\nindividual files in /usr/share/doc/*/copyright.\nUbuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by\napplicable law.\nspacex@90f5b4545a54:~$\n```\n\n2.  **更新用户密码**:定期更换密码。下面是执行此操作的命令:\n\n```cpp\nspacex@90f5b4545a54:~$ passwd\nChanging password for spacex.\n Current password:\n New password:\n Retype new password:\n passwd: password updated successfully\n spacex@90f5b4545a54:~$\n```\n\n3.  **将用户分配到一个组**:如图所示，用户在创建时可以被分配到一个组。或者，通过运行以下命令，可以随时将用户分配到组中:\n\n```cpp\nroot@90f5b4545a54:~# usermod -a -G testers spacex\n here spacex is added to the testers group\n```\n\n4.  **删除用户**:同样，删除用户也很简单:\n\n```cpp\nroot@90f5b4545a54:~# userdel -r spacex\nuserdel: spacex mail spool (/var/mail/spacex) not found\nroot@90f5b4545a54:~#\n```\n\n`-r`选项表示删除`spacex`主目录和`mail spool`。\n\n5.  现在，让我们看看最后一个命令，它显示了当前用户(`spacex`)所属的组列表:\n\n```cpp\nspacex@90f5b4545a54:~$ groups\n developers testers\n spacex@90f5b4545a54:~$\n```\n\n如您所见，`spacex`用户属于`developers`和`testers`组。\n\n# 它是如何工作的...\n\n在*步骤 1* 中，我们使用`adduser`命令添加`spacex`用户，并根据上下文将该用户添加到`developers`组。\n\nS *tep 2* 显示如何更改当前用户的密码。要更改密码，必须提供以前的密码。定期更改密码是一种很好的做法。\n\n如果我们想将一个用户分配到一个组，可以通过`usermod`命令来完成。在*步骤 3* 中，我们已经将`spacex`用户添加到了`testers`组。`-a`和`-G`参数只是表示新的组(`-G`)将被附加到用户的当前组(`-a`)中。也就是说，`spacex`用户将被分配到`testers`组，该组将根据上下文创建。在同一步骤中，`groups`命令显示当前用户属于哪个组。如果你只想创建一个组，那么`groupadd group-name`就是你需要的命令。\n\n*步骤 4* 显示如何通过`userdel`命令移除用户，传递`-r`参数。此参数确保我们要删除的用户的所有文件都将被删除。\n\n# 还有更多...\n\n在 Linux 文件系统上，每个文件和目录都有一组定义谁能做什么的信息。机制简单，功能强大。文件(或目录)上允许的操作分别是读、写和执行(`r`、`w`和`x`)。这些操作可以由文件或目录的所有者、一组用户或所有用户来完成。Linux 用所有者:`rwx`表示此信息；集团:`rwx`；所有用户:`rwx`；或者，更简单地说:`rwx-rwx-rwx`(共 9 个)。实际上，Linux 在这些标志之上还有一个标志，代表文件的类型。它可以是文件夹(`d`)、到另一个文件的符号链接(`l`)、常规文件(`-`)、命名管道(`p`)、套接字(`s`)、字符设备文件(`c`)和块设备(`b`)。文件的典型权限如下所示:\n\n```cpp\nroot@90f5b4545a54:/# ls -l\n -rwxr-xr-x 1 root root 13 May 8 20:11 conf.json\n```\n\n让我们详细看看这个:\n\n*   从左侧阅读，第一个字符`-`，告知我们`conf.json`是一个常规文件。\n*   接下来的三个角色是关于当前用户`rwx`。用户对文件拥有完全的**读取** ( **r** )、**写入** ( **w** )和**执行** ( **x** )权限。\n*   接下来的三个字符是关于用户所属的组，`r-x`。属于该组的所有用户都可以读取和执行该文件，但不能对其进行修改(未选择`w`，标记为`-`)。\n*   最后三个字符是关于所有其他用户的，`r-x`。所有其他用户只能读取和执行文件(`r`和`x`被标记，而`w`没有)。\n\n所有者(或根用户)可以更改文件的权限。实现这一点最简单的方法是通过`chmod`命令:\n\n```cpp\n $ chmod g+w conf.json \n```\n\n这里，我们要求 Linux 内核将写权限(`w`)添加到组用户类型(`g`)中。用户类型如下:`u`(针对用户)、`o`(针对其他人)、`a`(针对所有人)和`g`(针对群组)，权限标志可以是`x`、`w`、`r`，如前所述。`chmod`也可以接受一个整数:\n\n```cpp\n $ chmod 751 conf.json \n```\n\n每个组类型的权限标志都有二进制到十进制的转换，例如:\n`wxr`:111 = 7\n`w-r`:101 = 5\n`--r`:001 = 1\n\n一开始可能有点神秘，但对于日常使用来说非常实用和方便。\n\n# 请参见\n\n`man pages`是一个无限的信息资源，应该是你首先看到的。像`man groups`、`man userdel`或`man adduser`这样的命令会有所帮助。\n\n# 使用 makefile 编译和链接程序\n\nmakefile 是描述`make`实用程序用来构建(编译和链接)目标(可执行文件、共享对象等)的程序源之间关系的文件。Makefiles 非常重要，因为它们有助于保持源代码的组织性和易于维护。一个程序要成为可执行的，必须被编译并与其他库链接。GCC 是最广泛使用的编译器集合。C 和 C++ 世界中使用的两个编译器是 GCC 和 g++(分别用于 C 和 C++ 程序)。这本书将使用 g++。\n\n# 怎么做...\n\n本节将展示如何编写 makefile 来编译和运行一个简单的 C++ 程序。我们将开发一个简单的程序，并创建它的 makefile 来学习它的规则:\n\n1.  让我们从打开`hello.cpp`文件开始开发程序:\n\n```cpp\n$vi hello.cpp\n```\n\n2.  输入以下代码(参考*学习 Linux 基础知识- shell* 配方查看`vi`命令):\n\n```cpp\n#include <iostream>\nint main()\n{\n    std::cout << \"Hello World!\" << std::endl;\n    return 0;\n}\n```\n\n3.  保存并退出:在`vi`中，从命令模式中，键入`:wq`，这意味着写入并退出。`:x`的命令也有同样的效果。\n4.  从 shell 中，创建一个名为`Makefile`的新文件:\n\n```cpp\n$ vi Makefile\n```\n\n5.  输入以下代码:\n\n```cpp\nCC = g++\nall: hello\nhello: hello.o\n      ${CC} -o hello hello.o\nhello.o: hello.cpp\n      ${CC} -c hello.cpp\nclean:\n      rm hello.o hello\n```\n\n虽然这是一个典型的`Hello World!` 程序，但是展示一个 makefile 的结构是很有用的。\n\n# 它是如何工作的...\n\n简单地说，makefile 由一组规则组成。规则由目标、先决条件列表和命令组成。\n\n第一步，我们打开文件(`hello.cpp`)并输入*第二步*中列出的程序。同样，我们在`hello.cpp`程序的同一个文件夹中打开了另一个文件`Makefile`，并输入了特定的 makefile 命令。现在让我们深入到 makefile 内部。典型的 makefile 包含以下内容:\n\n1.  第一个规则由一个名为`all`的目标和一个名为`hello`的先决条件组成。此规则没有命令。\n2.  第二条规则由一个名为`hello`的目标组成。它在`hello.o`上有一个先决条件和一个链接的命令:`g++ `。\n3.  第三个规则有一个名为`hello.o`的目标，是`hello.cpp`的先决条件，还有一个编译命令:`g++ -c hello.cpp`。\n4.  最后一个规则有一个`clean`目标，带有删除所有`hello`和`hello.o`可执行文件的命令。这将强制重新编译文件。\n5.  对于任何规则，如果任何源文件发生变化，则执行定义的命令。\n\n我们现在可以使用我们创建的 makefile 来编译程序:\n\n```cpp\n$ make\n```\n\n我们还能够执行该程序，其输出如下:\n\n![](img/2ffd955d-5371-4d04-a52b-13cf17e6eeaf.png)\n\n从源文件生成二进制可执行文件的过程包括编译和链接阶段，此处压缩在单个命令中；大多数情况下都是这样。一般来说，大型系统代码库依赖于更复杂的机制，但是步骤仍然是相同的:源文件编辑、编译和链接。\n\n# 还有更多...\n\n这个简单的例子向我们展示了 makefile 及其`make`命令的基本概念。事情远不止如此。这里有几个例子:\n\n1.  宏的使用:makefile 允许使用宏，可以看作**变量**。这些可用于将 makefile 组织得更加模块化，例如:\n    *   程序中使用的所有动态库的宏:`LIBS = -lxyz -labc`。\n    *   编译器本身的宏(如果您想更改为另一个编译器):`COMPILER = GCC`。\n    *   在所有生成文件中引用这些宏:`$(CC)`。这给了我们在一个地方做出改变的自由。\n2.  只需在 shell 中键入`make`，makefile 中定义的第一个规则就会运行。在我们的案例中，第一条规则是`all`。如果我们通过将 **`clean`** 作为第一个规则来改变 makefile，那么在没有参数的情况下运行`make`将会执行`clean`规则。一般来说，您总是会传递一些参数——例如，`make clean`。\n\n# 用 GDB 调试程序\n\n调试是从软件系统中识别和消除错误的过程。GNU/Linux 操作系统有一个名为 GDB 的**标准** *事实上的*工具(也就是说，不是任何标准的一部分，但几乎被 Linux 世界中的任何人使用)。安装在本书 Docker 上的 GDB 版本是 8.2.91 版本。当然，也有一些图形工具可以在幕后使用 GDB，但是 Linux 上的 GDB 以其可靠性、简单性和速度走在了前面。在这个配方中，我们将调试我们在前面配方中编写的软件。\n\n# 怎么做...\n\n为了使用一些 GDB 命令，我们需要修改之前的程序并在其中添加一些变量:\n\n1.  打开一个外壳，通过输入以下代码修改`hello.cpp`文件:\n\n```cpp\n #include <iostream>\n int main()\n {\n    int x = 10;\n    x += 2;\n    std::cout << \"Hello World! x = \" << x << std::endl;\n    return 0;\n }\n```\n\n这是一个非常简单的程序:取一个变量，加上`2`，打印结果。\n\n2.  让我们通过键入以下命令来确保程序已被编译:\n\n```cpp\nroot@bffd758254f8:~/Chapter1# make\n g++ -c hello.cpp\n g++ -o hello hello.o\n```\n\n3.  现在我们有了可执行文件，我们将调试它。在命令行中，键入`gdb hello`:\n\n```cpp\nroot@bffd758254f8:~/Chapter1# gdb hello\n GNU gdb (Ubuntu 8.2.91.20190405-0ubuntu3) 8.2.91.20190405-git\n Copyright (C) 2019 Free Software Foundation, Inc.\n License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>\n This is free software: you are free to change and redistribute it.\n There is NO WARRANTY, to the extent permitted by law.\n Type \"show copying\" and \"show warranty\" for details.\n This GDB was configured as \"x86_64-linux-gnu\".\n Type \"show configuration\" for configuration details.\n For bug reporting instructions, please see:\n <http://www.gnu.org/software/gdb/bugs/>.\n Find the GDB manual and other documentation resources online at:\n <http://www.gnu.org/software/gdb/documentation/>.\nFor help, type \"help\".\n Type \"apropos word\" to search for commands related to \"word\"...\n Reading symbols from hello...\n (No debugging symbols found in hello)\n (gdb)\n```\n\n4.  可以看到，最后一行写着(`No debugging symbols found in hello`)。GDB 不需要调试符号来调试程序，所以我们必须与编译器沟通，在编译过程中包含调试符号。我们必须退出当前会话；为此，输入`q` ( *进入*)。然后，编辑 makefile，并将`-g`选项添加到`g++ `编译器部分(`hello.o`目标):\n\n```cpp\nCC = g++\nall: hello\nhello: hello.o\n    ${CC} -o hello hello.o\nhello.o: hello.cpp\n    $(CC) -c -g hello.cpp\nclean:\n    rm hello.o hello\n```\n\n5.  让我们再次运行它，但是，首先，我们必须用`make`命令重建应用:\n\n```cpp\nroot@bcec6ff72b3c:/BOOK/chapter1# gdb hello\nGNU gdb (Ubuntu 8.2.91.20190405-0ubuntu3) 8.2.91.20190405-git\nCopyright (C) 2019 Free Software Foundation, Inc.\nLicense GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>\nThis is free software: you are free to change and redistribute it.\nThere is NO WARRANTY, to the extent permitted by law.\nType \"show copying\" and \"show warranty\" for details.\nThis GDB was configured as \"x86_64-linux-gnu\".\nType \"show configuration\" for configuration details.\nFor bug reporting instructions, please see:\n<http://www.gnu.org/software/gdb/bugs/>.\nFind the GDB manual and other documentation resources online at:\n <http://www.gnu.org/software/gdb/documentation/>.\nFor help, type \"help\".\nType \"apropos word\" to search for commands related to \"word\"...\nReading symbols from hello...\n(No debugging symbols found in hello)\n(gdb)\n```\n\n我们准备调试它。调试会话通常包括设置断点、观察变量的内容、设置观察点等。下一节将展示最常见的调试命令。\n\n# 它是如何工作的...\n\n在前一节中，我们已经看到了创建程序和 makefile 所需的步骤。在这一节中，我们将学习如何调试我们开发的`Hello World!`程序。\n\n让我们从可视化我们要调试的代码开始。我们通过运行`l`命令(列表的缩写)来实现:\n\n```cpp\n(gdb) l\n 1 #include <iostream>\n 2 int main()\n 3 {\n 4    int x = 10;\n 5    x += 2;\n 6    std::cout << \"Hello World! x = \" << x << std::endl;\n 7    return 0;\n 8 }\n```\n\n我们必须设置一个断点。要设置断点，我们运行`b 5`命令。这会在当前模块中的代码行号`5`处设置一个断点:\n\n```cpp\n(gdb) b 5\n Breakpoint 1 at 0x1169: file hello.cpp, line 5.\n (gdb)\n```\n\n现在是运行程序的时候了。要运行程序，我们键入`r`命令。这运行了我们从 GDB 开始的`hello`程序:\n\n```cpp\n(gdb) r\n Starting program: /root/Chapter1/hello\n```\n\n一旦启动，GDB 将自动在流程遇到的任何断点处停止。在这种情况下，过程运行，然后在`hello.cpp`文件的第`5`行停止:\n\n```cpp\nBreakpoint 1, main () at hello.cpp:5\n 5 x += 2;\n```\n\n为了逐步进行，我们在 GDB 上运行`n`命令(即，单步执行)。这将执行当前可视化的代码行。类似的命令是`s`(步入)。如果当前命令是一个函数，它将进入函数:\n\n```cpp\n(gdb) n\n6 std::cout << \"Hello World! x = \" << x << std::endl;\nthe 'n' command (short for next) execute one line. Now we may want to check the content of the variable x after the increment:\n```\n\n如果我们需要知道一个变量的内容，我们运行`p`命令(print 的缩写)，它打印一个变量的内容。在这种情况下，如预期的那样，`x = 12`被打印:\n\n```cpp\n(gdb) p x\n$1 = 12\n```\n\n现在，让我们运行程序直到结束(或者直到下一个断点，如果设置的话)。这是通过`c`命令完成的(continue 的缩写):\n\n```cpp\n(gdb) c \n Continuing.\n Hello World! x = 12\n [Inferior 1 (process 101) exited normally]\n (gdb)\n```\n\nGDB 通过让程序员一行行地单步执行程序，真正起到了解释器的作用。这有助于开发人员解决问题、在运行时查看变量的内容、更改变量的状态等等。\n\n# 还有更多...\n\nGDB 有很多非常有用的命令。在接下来的章节中，GDB 将会被更多的探索。这里还有四个命令要显示:\n\n1.  `s`:台阶的简称。如果在一个方法上被调用，它就会进入这个方法。\n2.  `bt`:回溯的简称。打印调用堆栈。\n3.  `q`:戒的简称。用来离开 GDB。\n4.  `d`:删除的简称。它删除了一个断点。例如，`d 1`删除第一个断点集。\n\nThe main page of the GNU GDB Project can be found here: [https://www.gnu.org/software/gdb](https://www.gnu.org/software/gdb). More detailed information can be found on the `man dbg` `man pages` and online. You can also refer to *Using GDB: A Guide to the GNU Source-Level Debugger,* by Richard M. Stallman and Roland H. Pesch*.*\n\n# 学习 Linux 基础——进程和线程\n\n进程和线程是任何操作系统的执行单元。在本食谱中，您将学习如何在命令行上处理 GNU/Linux 上的进程和线程。进程是一个程序的运行实例，具有一组定义明确的资源，如文件、处理器状态和分配给它的执行线程。\n\nLinux 中的进程由`sched.h`头文件中定义的`task_struct`结构定义。另一方面，线程由`thread_info.h`头文件中的`thread_info`结构定义。线程是执行主进程的一个可能流程。一个进程至少有一个线程(主线程)。一个进程的所有线程在一个系统上并发运行。\n\n在 Linux 上需要记住的一个方面是，它没有区分进程和线程。线程就像一个进程，与其他进程共享一些资源。因此，在 Linux 中，线程通常被称为**轻量级进程** ( **LWP** )。\n\n# 怎么做...\n\n在本节中，我们将逐步学习在 GNU/Linux 发行版上控制进程和线程的所有最常见的命令:\n\n1.  `ps`命令显示当前系统中的进程、属性和其他参数:\n\n```cpp\nroot@5fd725701f0f:/# ps u\nUSER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND\nroot 1 0.0 0.1 4184 3396 pts/0 Ss 17:20 0:00 bash\nroot 18 0.0 0.1 5832 2856 pts/0 R+ 17:22 0:00 ps u\n```\n\n2.  获取进程(及其线程)信息的另一种方法是查看`/process/PID`文件夹。该文件夹包含所有进程信息、进程的线程(以子文件夹的形式，带有**进程标识符** ( **进程标识符**))、内存等等:\n\n```cpp\nroot@e9ebbdbe3899:/# ps aux\nUSER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND\nroot 1 0.0 0.1 4184 3344 pts/0 Ss 16:24 0:00 bash\nroot 149 0.0 0.1 4184 3348 pts/1 Ss 17:40 0:00 bash\nroot 172 85.0 0.0 5832 1708 pts/0 R+ 18:02 0:04 ./hello\nroot 173 0.0 0.1 5832 2804 pts/1 R+ 18:02 0:00 ps aux\nroot@e9ebbdbe3899:/# ll /proc/172/\ntotal 0\ndr-xr-xr-x 9 root root 0 May 12 18:02 ./\ndr-xr-xr-x 200 root root 0 May 12 16:24 ../\ndr-xr-xr-x 2 root root 0 May 12 18:02 attr/\n-rw-r--r-- 1 root root 0 May 12 18:02 autogroup\n-r-------- 1 root root 0 May 12 18:02 auxv\n-r--r--r-- 1 root root 0 May 12 18:02 cgroup\n--w------- 1 root root 0 May 12 18:02 clear_refs\n-r--r--r-- 1 root root 0 May 12 18:02 cmdline\n-rw-r--r-- 1 root root 0 May 12 18:02 comm\n-rw-r--r-- 1 root root 0 May 12 18:02 coredump_filter\n-r--r--r-- 1 root root 0 May 12 18:02 cpuset\nlrwxrwxrwx 1 root root 0 May 12 18:02 cwd -> /root/Chapter1/\n-r-------- 1 root root 0 May 12 18:02 environ\nlrwxrwxrwx 1 root root 0 May 12 18:02 exe -> /root/Chapter1/hello*\ndr-x------ 2 root root 0 May 12 18:02 fd/\ndr-x------ 2 root root 0 May 12 18:02 fdinfo/\n-rw-r--r-- 1 root root 0 May 12 18:02 gid_map\n-r-------- 1 root root 0 May 12 18:02 io\n-r--r--r-- 1 root root 0 May 12 18:02 limits\n... \n```\n\n3.  一个过程也可能被扼杀。从技术上讲，终止一个进程意味着停止它的执行:\n\n```cpp\nroot@5fd725701f0f:/# kill -9 PID\n```\n\n该命令将`kill`信号(`9`)发送给用 PID 识别的过程。其他信号可以发送到进程，例如`HUP`(挂断)和`INT`(中断)。\n\n# 它是如何工作的...\n\n在 *s* *tep 1* 中，对于每个流程，我们可以看到以下内容:\n\n*   流程所属的用户\n*   PID\n*   特定时刻 CPU 和内存的百分比\n*   进程开始的时间及其运行时间\n*   用于运行进程的命令\n\n通过`ps aux`命令，我们可以抓取`hello`流程的 PID，也就是`172`。我们现在可以进入`/proc/172`文件夹。\n\n进程和线程是操作系统的构建模块。在这个食谱中，我们已经看到了如何在命令行上与内核交互，通过一个命令(例如，`ps`)获取进程的信息，以及通过查看进程运行时 Linux 更新的特定文件夹。同样，每次我们调用一个命令(在这种情况下，为了获得进程的信息)，该命令必须进入内核空间以获得关于它的有效和更新的信息。\n\n# 还有更多...\n\n`ps`命令的参数比本食谱中的基本参数多得多。完整的列表可以在它的 Linux 手册页`man ps`上找到。\n\n作为`ps`的替代方案，一个更高级和交互式的命令是`top`命令，`man top`。\n\n# 处理 Linux bash 错误\n\n我们已经看到，与 Linux 内核交互的一种方式是通过外壳，通过调用命令。我们可以想象，一个命令可能会失败，一种传达失败的方法是返回一个非负整数值。0，在大多数情况下，意味着成功。这个食谱将告诉你如何处理外壳上的错误。\n\n# 怎么做...\n\n本节将向您展示如何直接从 shell 和通过脚本获取错误，这是脚本开发的一个基本方面:\n\n1.  首先，运行以下命令:\n\n```cpp\nroot@e9ebbdbe3899:/# cp file file2\n cp: cannot stat 'file': No such file or directory\n root@e9ebbdbe3899:/# echo $?\n 1\n```\n\n2.  创建一个名为`first_script.sh`的新文件，并输入以下代码:\n\n```cpp\n#!/bin/bash\ncat does_not_exists.txt\nif [ $? -eq 0 ]\nthen\n    echo \"All good, does_not_exist.txt exists!\"\n    exit 0\nelse\n    echo \"does_not_exist.txt really DOES NOT exists!!\" >&2\n    exit 11\nfi\n```\n\n3.  保存文件，退出(`:wq`或`:x`)。\n4.  给予当前用户对`first_script.sh`文件的执行权限(`x`标志):\n\n```cpp\nroot@e9ebbdbe3899:~# chmod u+x first_script.sh\n```\n\n下一节将详细介绍这些步骤。\n\n# 它是如何工作的...\n\n在*步骤 1* 中，`cp`命令失败，因为`file`和`file2`不存在。通过查询`echo $?`，得到错误码；在这种情况下，就是`1`。这在编写 bash 脚本时特别有用，因为我们可能需要检查特定的条件。\n\n在*步骤 2* 中，脚本只是列出了`does_not_exist.txt`文件并读取返回的错误代码。如果一切顺利，它会打印一条确认消息并返回`0`。否则，返回错误代码`11`。\n\n通过运行脚本，我们得到如下输出:\n\n![](img/5d809462-bb33-4827-9f73-a2cbe6881bbc.png)\n\n在这里，我们注意到一些事情:\n\n*   我们记录了我们的错误字符串。\n*   错误代码是我们在脚本中设置的。\n\n在幕后，每次调用一个命令，它就进入内核空间。该命令被执行，并且返回状态以整数的形式被发送回用户。考虑这个返回状态真的很重要，因为我们可能有一个明显成功(没有输出)但最终失败(返回不同于`0`的代码)的命令。\n\n# 还有更多...\n\n命令返回状态的一个重要方面是，它可以用来(有条件地)运行下一个命令。为此使用了两个重要的运算符:`&&`(与)和`||`(或)。\n\n在这里的两个命令中，第二个命令在第一个命令成功的情况下运行(也只有在第一个命令成功的情况下运行`&&`运算符)。如果将`file.txt`复制到项目文件夹中，则会删除它:\n\n```cpp\ncp file.txt ~/projects && rm -f file.txt\n```\n\n让我们看看第二个例子:\n\n```cpp\ncp file.txt ~/projects || echo 'copy failed!'\n```\n\n在前面的例子中，只有当第一个命令失败时，第二个命令才会运行(操作符`||`)。如果复印失败，则打印`copy failed!`。\n\n在这个食谱中，我们刚刚展示了命令可以组合在一个 shell 脚本上来创建一个更复杂的命令，并且通过控制错误代码，我们可以控制执行的流程。手册页是一个很好的资源，因为它们包含所有的命令和错误代码(例如，`man cp`和`man cat`)。\n\n# 处理 Linux 代码错误\n\n这个方法代表了错误处理主题的第二面:源代码级别的错误处理。Linux *通过命令以及编程接口公开了*的内核特性。在这个食谱中，我们将看到如何处理错误代码和`errno`通过一个 C 程序，打开一个文件。\n\n# 怎么做...\n\n在本节中，我们将看到如何从 C 程序中的系统调用中获取错误。为此，我们将创建一个程序来打开一个不存在的文件，并显示 Linux 返回的错误的详细信息:\n\n1.  创建新文件:`open_file.c`。\n2.  在新创建的文件中编辑以下代码:\n\n```cpp\n#include <fcntl.h>\n#include <stdio.h>\n#include <stdlib.h>\n#include <errno.h>\n#include <string.h>\n\nint main(int argc, char *argv[])\n{\n    int fileDesc = open(\"myFile.txt\", O_RDONLY);\n    if (fileDesc == -1)\n    {\n        fprintf(stderr, \"Cannot open myFile.txt .. error: %d\\n\", \n           fileDesc);\n        fprintf(stderr, \"errno code = %d\\n\", errno);\n        fprintf(stderr, \"errno meaningn = %s\\n\", strerror(errno));\n        exit(1);\n    }\n}\n```\n\n3.  保存文件并退出(`:x`)。\n4.  编译代码:`gcc open_file.c`。\n5.  前面的编译(不带参数)将产生一个名为`a.out`的二进制文件(这是 Linux 和 Unix 操作系统上的默认名称)。\n\n# 它是如何工作的...\n\n列出的程序试图以读取模式打开文件。通过`fprintf`命令，误差被打印在标准误差上。通过运行它，输出如下:\n\n![](img/ec4464be-3ce9-4b95-b6fe-e888f82b52dd.png)\n\n有几个注意事项需要强调。程序是严格按照开放系统调用的手册页(`man 2 open`)开发的:\n\n```cpp\nRETURN VALUES\n     If successful, open() returns a non-negative integer, termed a \nfile descriptor. It \n      returns -1 on failure, and sets errno to indicate the error\n```\n\n开发人员(本例中为我们)检查文件描述符为`-1`(由`fprintf`确认)也打印`errno`(代码为`2`)。`errno 2`是什么意思？`strerror`对于这个范围非常有用，可以从`errno`(很神秘)翻译成程序员(或用户)能理解的东西。\n\n# 还有更多...\n\n在[第 2 章](02.html)*重温 C++* ，我们将看到 C++ 如何通过提供更高级的机制，以及易于编写和更简洁的代码来帮助程序员。即使我们试图直接最小化与内核 API 的交互，支持使用 C++ 11-14-17 更高级的机制，也会有需要检查错误状态的情况。在这种情况下，请注意错误管理。**"
  },
  {
    "path": "docs/cpp-sys-prog-cb/02.md",
    "content": "# 二、重温 C++\n\n这一章是对 C++ 11-20 的复习，这将贯穿全书。我们将解释为什么 C++ 代表了一个伟大的机会，在编写比以往任何时候都简洁和更具可移植性的高质量代码时，不应该错过这个机会。\n\n本章不包含所有由 C++ (11 到 20)引入的新特性——只是我们将在本书剩余部分使用的特性。具体来说，您将获得一个复习(如果您已经知道)或学习(如果您是新的)编写现代代码所需的最基本的新 C++ 技能。仅举几个例子，您将实际操作 lambda 表达式、原子和移动语义。\n\n本章将涵盖以下食谱:\n\n*   理解 C++ 基元类型\n*   λ表达式\n*   自动类型扣除和`decltype`\n*   学习原子是如何工作的\n*   了解`nullptr`如何工作\n*   智能指针–`unique_ptr`和`shared_ptr`\n*   学习语义是如何工作的\n*   理解并发性\n*   理解文件系统\n*   C++ 核心指南\n*   将 GSL 添加到您的制作文件中\n*   理解概念\n*   使用跨度\n*   了解范围是如何工作的\n*   学习模块如何工作\n\n# 技术要求\n\n为了让您立即试用本章中的程序，我们设置了一个 Docker 映像，其中包含了我们在本书中需要的所有工具和库。是基于 Ubuntu 19.04 的。\n\n要进行设置，请执行以下步骤:\n\n1.  从[www.docker.com](http://www.docker.com)下载并安装 Docker 引擎。\n2.  从 Docker Hub 中拉出图像:`docker pull kasperondocker/system_programming_cookbook:latest`。\n3.  图像现在应该可以使用了。输入以下命令查看图像:`docker images`。\n4.  现在，你应该有如下图像:`kasperondocker/system_programming_cookbook`。\n5.  借助以下命令，使用交互式外壳运行 Docker 映像:`docker run -it --cap-add sys_ptrace kasperondocker/system_programming_cookbook:latest /bin/bash`。\n6.  运行容器上的外壳现已可用。使用`root@39a5a8934370/# cd /BOOK/`获取本书各章节开发的所有程序。\n\n需要`--cap-add sys_ptrace`参数来允许 GDB 在 Docker 容器中设置断点，默认情况下，Docker 不允许这样做。\n\n**Disclaimer**: The C++ 20 standard has been approved (that is, technically finalized) by WG21 in a meeting in Prague at the end of February. This means that the GCC compiler version that this book uses, 8.3.0, does not include (or has very, very limited support for) the new and cool C++ 20 features. For this reason, the Docker image does not include the C++ 20 recipe code. GCC keeps the development of the newest features in branches (you have to use appropriate flags for that, for example, `-std=c++ 2a`); therefore, you are encouraged to experiment with them by yourself. So, clone and explore the GCC contracts and module branches and have fun.\n\n# 理解 C++ 基元类型\n\n这个配方将显示由 C++ 标准定义的所有原始数据类型，以及它们的大小。\n\n# 怎么做...\n\n在这一节中，我们将进一步了解 C++ 标准定义了哪些原语，以及哪些其他信息是重要的。我们还将了解到，虽然标准没有为每一个定义大小，但它定义了另一个重要参数:\n\n1.  首先，打开一个新的终端并输入以下程序:\n\n```cpp\n#include <iostream>\n#include <limits>\n\nint main ()\n {\n    // integral types section\n    std::cout << \"char \" << int(std::numeric_limits<char>::min())\n              << \"-\" << int(std::numeric_limits<char>::max())\n              << \" size (Byte) =\" << sizeof (char) << std::endl;\n    std::cout << \"wchar_t \" << std::numeric_limits<wchar_t>::min()\n              << \"-\" <<  std::numeric_limits<wchar_t>::max()\n              << \" size (Byte) =\"\n              << sizeof (wchar_t) << std::endl;\n    std::cout << \"int \" << std::numeric_limits<int>::min() << \"-\"\n              << std::numeric_limits<int>::max() << \" size\n                  (Byte) =\"\n              << sizeof (int) << std::endl;\n    std::cout << \"bool \" << std::numeric_limits<bool>::min() << \"-\"\n              << std::numeric_limits<bool>::max() << \"\n                  size (Byte) =\"\n              << sizeof (bool) << std::endl;\n\n    // floating point types\n    std::cout << \"float \" << std::numeric_limits<float>::min() <<    \n                  \"-\"\n              << std::numeric_limits<float>::max() << \" size\n                  (Byte) =\"\n              << sizeof (float) << std::endl;\n    std::cout << \"double \" << std::numeric_limits<double>::min()\n                  << \"-\"\n              << std::numeric_limits<double>::max() << \" size\n                  (Byte) =\"\n              << sizeof (double) << std::endl;\n    return 0;\n }\n```\n\n2.  接下来，构建(编译并链接)`g++ primitives.cpp`。\n3.  这将产生一个名为`a.out`的可执行文件。\n\n# 它是如何工作的...\n\n前面程序的输出如下所示:\n\n![](img/17a5c520-563d-45b6-b17d-5e3c197d535a.png)\n\n这表示类型可以表示的最小值和最大值，以及当前平台的字节大小。\n\nC++ 标准**没有**定义每种类型的大小，但是定义了最小**宽度**T4:\n\n*   `char`:最小宽度= 8\n*   `short int`:最小宽度= 16\n*   `int`:最小宽度= 16\n*   `long int`:最小宽度= 32\n*   `long int int`:最小宽度= 64\n\n这一点有着巨大的含义，因为不同的平台可能有不同的大小，程序员应该处理这个问题。为了帮助我们获得一些关于数据类型的指导，有一个数据模型的概念。一个**数据模型**是由每个实现(编译器和操作系统遵循的架构的 psABI)做出的一组选择(每个类型一个特定的大小)来定义所有的原始数据类型。下表显示了现有的各种类型和数据模型的子集:\n\n| **数据类型** | **LP32** | **ILP32** | **LLP64** | **LP64** |\n| `char` | eight | eight | eight | eight |\n| `short int` | Sixteen | Sixteen | Sixteen | Sixteen |\n| `int` | Sixteen | Thirty-two | Thirty-two | Thirty-two |\n| `long` | Thirty-two | Thirty-two | Thirty-two | Sixty-four |\n| `pointer` | Thirty-two | Thirty-two | Sixty-four | Sixty-four |\n\nLinux 内核对 64 位架构(x86_64)使用 LP64 数据模型。\n\n我们简短地谈到了 psABI 主题(平台特定应用二进制接口(**ABI**)的缩写)。每个架构(例如 x86_64)都有一个操作系统遵循的 psABI 规范。 **GNU 编译器集合** ( **GCC** )必须知道这些细节，因为它必须知道它编译的原语类型的大小。`i386.h` GCC 头文件包含该架构的原始数据类型的大小:\n\n```cpp\nroot@453eb8a8d60a:~# uname -a\n Linux 453eb8a8d60a 4.9.125-linuxkit #1 SMP Fri Sep 7 08:20:28 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux\n```\n\n程序输出显示，当前的 OS(实际上是我们正在运行的 Ubuntu 映像)使用了预期的 LP64 数据模型，机器的架构是 x86_64。\n\n# 还有更多...\n\n正如我们所看到的，C++ 标准定义了以下基本数据类型:\n\n*   整数:`int`\n*   角色:`char`\n*   布尔值： `bool`\n*   浮点:`float`\n*   双浮点:`double`\n*   作废:`void`\n*   宽字符:`wchar_t`\n*   空指针:`nullptr_­t`\n\n数据类型可以有其他信息，以便定义它们的类型:\n\n*   修饰语:`signed`、`unsigned`、`long`和`short`\n*   限定词:`const`和`restrict`\n*   存储类型:`auto`、`static`、`extern`、`mutable`\n\n显然，不是所有这些附加属性都可以应用于所有类型；例如，`unsigned`不能应用于`float`和`double`类型(它们各自的 IEEE 标准不允许这样做)。\n\n# 请参见\n\n特别是对于 Linux，Linux 内核文档通常是开始深入研究这个问题的好地方:[https://www.kernel.org/doc/html/latest](https://www.kernel.org/doc/html/latest/)。GCC 源代码显示了每个支持的体系结构的原始数据类型的大小。更多详情请参考以下链接:[https://github.com/gcc-mirror/gcc](https://github.com/gcc-mirror/gcc)。\n\n# λ表达式\n\n一个 **lambda 表达式**(或者 **lambda** **函数**)是一种定义匿名的、小的、一次性使用的函数的便捷方式，以便在需要的地方使用。Lambda 对于**标准模板库** ( **STL** )特别有用，我们会看到。\n\n# 怎么做...\n\n在本节中，我们将编写一些代码来熟悉 lambda 表达式。虽然机制很重要，但是要注意 lambda 的代码可读性，尤其是结合 STL。请遵循以下步骤:\n\n1.  在这个程序中，lambda 函数获取一个整数并将其打印到标准输出。让我们打开一个名为`lambda_01.cpp`的文件，并在其中编写以下代码:\n\n```cpp\n#include <iostream>\n#include <vector>\n#include <algorithm>\nint main ()\n{\n    std::vector<int> v {1, 2, 3, 4, 5, 6};\n    for_each (begin(v), end(v), [](int x) {std::cout << x\n        << std::endl;});\n    return 0;\n}\n```\n\n2.  在第二个程序中，lambda 函数通过引用捕获前缀，并将其添加到标准输出中的整数前面。让我们把下面的代码写在一个名为`lambda_02.cpp`的文件中:\n\n```cpp\n#include <iostream>\n#include <vector>\n#include <algorithm>\nint main ()\n{\n    std::vector<int> v {1, 2, 3, 4, 5, 6};\n    std::string prefix (\"0\");\n    for_each (begin(v), end(v), [&prefix](int x) {std::cout\n        << prefix << x << std::endl;});\n    return 0;\n}\n```\n\n3.  最后用`g++ lambda_02.cpp`编译。\n\n# 它是如何工作的...\n\n在第一个例子中，lambda 函数只是获取一个整数作为输入并打印出来。请注意，代码简洁易读。Lambda 可以通过引用`&`或通过值`=`捕获范围内的变量。\n\n第二个程序的输出如下:\n\n![](img/271646b2-f3b5-450a-ad5c-ed95229b6c34.png)\n\n在第二个例子中，lambda **通过引用捕获变量前缀** ，使其对 lambda 可见。这里，我们通过引用捕获了`prefix`变量，但是我们可能捕获了以下任何一个:\n\n*   所有变量通过引用`[&]`\n*   所有变量按值`[=]`\n*   指定*捕捉什么变量*和*如何捕捉它们*T0\n\n在某些情况下，我们必须明确要返回的类型，如本例所示:\n\n```cpp\n[](int x) -> std::vector<int>{\n             if (x%2)\n                 return {1, 2};\n             else\n                 return {3, 4};\n });\n```\n\n名为**尾随返回类型**的`-> std::vector<int>`运算符告诉编译器，这个λ将返回一个整数向量。\n\n# 还有更多...\n\nλ可以分解为六个部分:\n\n1.  捕获条款:`[]`\n2.  参数表:`()`\n3.  可变规格:`mutable`\n4.  异常说明:`noexcept`\n5.  尾随返回类型:`-> type`\n6.  正文:`{}`\n\n这里 *1* 、 *2* 、 *6* 为必选。\n\n虽然可选，但是可变规范和异常规范值得一看，因为它们在某些情况下可能很方便。可变规范允许通过 lambda 的主体修改副值参数。参数列表中的一个变量通常由*按值常量*捕获，因此可变规范只是消除了这个限制。第二种情况是异常规范，我们可以用它来指定 lambda 可能引发的异常。\n\n# 请参见\n\n斯科特·梅耶斯的《有效的现代 C++ 》( T1)和比雅尼·斯特劳斯特鲁普的《T2 的 c++ 编程语言》( T3)这两本书非常详细地涵盖了这些主题。\n\n# 自动类型演绎和类型演绎\n\nC++ 提供了两种从表达式中扣除类型的机制:`auto`和`decltype()`。`auto`用于从初始值设定项中推导出一个类型，而`decltype()`用于推导出更复杂情况下的类型。这个食谱将展示如何使用两者的例子。\n\n# 怎么做...\n\n避免显式指定将要使用的变量类型可能会很方便(实际上也是如此)，尤其是当变量特别长并且在本地使用时:\n\n1.  让我们从一个典型的例子开始:\n\n```cpp\nstd::map<int, std::string> payslips;\n// ... \nfor (std::map<int, \n     std::string>::const_iterator iter = payslips.begin(); \n     iter !=payslips.end(); ++ iter) \n{\n // ... \n}\n```\n\n2.  现在，我们用`auto`改写一下:\n\n```cpp\nstd::map<int, std::string> payslips;\n// ... \nfor (auto iter = payslips.begin(); iter !=payslips.end(); ++ iter) \n{\n    // ... \n}\n```\n\n3.  让我们看另一个例子:\n\n```cpp\nauto speed = 123;         // speed is an int\nauto height = calculate ();    // height will be of the\n                         // type returned by calculate()\n```\n\n`decltype()`是 C++ 提供的另一种机制，当表达式比`auto`情况更复杂时，可以推导出表达式的类型。\n\n4.  让我们用一个例子来看看这个:\n\n```cpp\ndecltype(a) y = x + 1;  // deducing the type of a\ndecltype(str->x) y;     // deducing the type of str->x, where str is \n                        // a struct and x \n                        // an int element of that struct\n```\n\n这两个例子可以用`auto`代替`decltype()`吗？我们将在下一部分看一看。\n\n# 它是如何工作的...\n\n带有`auto`的第一个例子表明，在编译时，类型是从右边的参数推导出来的。`auto`用于简单情况。\n\n`decltype()`推导表达式的类型。在本例中，它定义了`y`变量，因此它与`a`是同一类型。可以想象，这在`auto`上是不可能的。为什么呢？这很简单:`decltype()`告诉编译器*定义一个特定类型的变量*；在第一个例子中，`y`是一个与`a`类型相同的变量。用`auto`*自动推导类型。*\n\n *我们应该在任何时候使用`auto`和`decltype()`，而不必显式指定变量的类型；例如，当我们需要一个`double`型(而不是一个`float`)时。值得一提的是，`auto`和`decltype()`都是演绎编译器已经知道的表达式类型，所以**不是运行时机制**。\n\n# 还有更多...\n\n有一个具体的案例必须提及。当`auto`使用`{}`(统一初始值设定项)进行类型推演时，会引起一些头疼(或者至少是我们意想不到的行为)。让我们看一个例子:\n\n```cpp\nauto fuelLevel {0, 1, 2, 3, 4, 5};\n```\n\n在这种情况下，推导出的类型是`initializer_list<T>`，而不是我们所期望的整数数组。\n\n# 请参见\n\n斯科特·梅耶斯的《有效的现代 C++ 》( T1)和比雅尼·斯特劳斯特鲁普的《T2 的 c++ 编程语言》( T3)这两本书非常详细地涵盖了这些主题。\n\n# 学习原子是如何工作的\n\n传统上，C 和 C++ 在系统编程的可移植代码方面有着悠久的传统。C++ 11 标准中引入的`atomic`特性强化了这一点，它在本地添加了一个保证，即一个操作被其他线程视为原子操作。原子就是一个模板，比如`template <class T> struct atomic;`或者`template <class T> struct atomic<T*>;`。C++ 20 在`T`和`T*`中增加了`shared_ptr`和`weak_ptr`。对`atomic`变量执行的任何操作现在都受到保护，不受其他线程的影响。\n\n# 怎么做...\n\n`std::atomic`是现代 C++ 处理并发的一个重要方面。让我们写一些代码来掌握这个概念:\n\n1.  第一段代码展示了`atomic`操作的基础。让我们现在写这个:\n\n```cpp\nstd::atomic<int> speed (0);         // Other threads have access to the speed variable\nauto currentSpeed = speed.load();   // default memory order: memory_order_seq_cst\n```\n\n2.  在第二个程序中，我们可以看到如果实现是无锁的或者已经使用锁实现了，那么`is_lock_free()`方法返回`true`。让我们编写这段代码:\n\n```cpp\n#include <iostream>\n#include <utility>\n#include <atomic>\nstruct MyArray { int z[50]; };\nstruct MyStr { int a, b; };\nint main()\n{\n     std::atomic<MyArray> myArray;\n     std::atomic<MyStr> myStr;\n     std::cout << std::boolalpha\n               << \"std::atomic<myArray> is lock free? \"\n               << std::atomic_is_lock_free(&myArray) << std::endl\n               << \"std::atomic<myStr> is lock free? \"\n               << std::atomic_is_lock_free(&myStr) << std::endl;\n}               \n```\n\n3.  让我们编译程序。这样做的时候，你可能需要用`g++ atomic.cpp -latomic`将`atomic`库添加到 g++(由于一个 GCC 错误)。\n\n# 它是如何工作的...\n\n`std::atomic<int> speed (0);`将`speed`变量定义为原子整数。虽然变量将是原子的，但是这个初始化**不是原子的**！相反，下面的代码:`speed +=10;`自动增加`10`的速度。这意味着不会有比赛条件。根据定义，当访问变量的线程中，至少有 1 个是写线程时，就会发生争用情况。\n\n`std::cout << \"current speed is: \" << speed;`指令自动读取速度的当前值。注意从速度读取值是原子的，但接下来发生的不是原子的(即通过`cout`打印)。规则是读和写是原子的，但是周围的操作不是，正如我们所看到的。\n\n第二个程序的输出如下:\n\n![](img/878ed611-133b-41a3-8388-b49f0f8a688e.png)\n\n原子的基本操作是加载、存储、交换和 **cas** (简称**比较和交换**)，它们在所有类型的原子上都可用。根据类型(例如，`fetch_add`)的不同，也有其他可用的选项。\n\n不过，有一个问题仍然悬而未决。为什么`myArray`用锁而`myStr`是无锁的？原因很简单:C++ 为所有的基元类型提供了无锁实现，`MyStr`里面的变量都是基元类型。用户将设置`myStr.a`和`myStr.b`。`MyArray`另一方面，不是基本类型，所以底层实现会使用锁。\n\n标准保证是，对于每个原子操作，每个线程都将取得进展。要记住的一个重要方面是编译器经常进行代码优化。atomics 的使用对编译器如何对代码进行重新排序施加了限制。限制的一个例子是，在写入`atomic`变量之前的任何代码都不能在原子写入之后移动*。*\n\n# 还有更多...\n\n在这个食谱中，我们使用了名为`memory_order_seq_cst`的默认记忆模型。其他一些可用的内存型号包括:\n\n*   `memory_order_relaxed`:只保证当前操作原子性。也就是说，无法保证不同线程中的内存访问是如何相对于原子操作进行排序的。\n*   `memory_order_consume`:一旦释放线程中所有对释放操作有依赖关系的内存访问都发生了，操作就被命令发生。\n*   `memory_order_acquire`:一旦释放线程中对内存的所有访问都发生，操作就被命令发生。\n*   `memory_order_release`:操作被命令发生在消耗或获取操作之前。\n*   `memory_order_seq_cst`:操作顺序一致有序。\n\n# 请参见\n\n斯科特·梅耶斯的《有效的现代 C++ 》( T1)和比雅尼·斯特劳斯特鲁普的《T2 的 c++ 编程语言》( T3)这两本书非常详细地涵盖了这些主题。此外，赫伯·萨特在 YouTube 上免费提供的*原子武器*演讲([https://www.youtube.com/watch?v=A8eCGOqgvH4](https://www.youtube.com/watch?v=A8eCGOqgvH4))是一个很好的介绍。\n\n# 了解 nullptr 的工作原理\n\n在 C++ 11 之前，`NULL`标识符是用于指针的。在这个食谱中，我们将看到为什么这是一个问题，以及 C++ 11 如何解决它。\n\n# 怎么做...\n\n为了理解`nullptr`为什么重要，我们来看看`NULL`的问题:\n\n1.  让我们编写以下代码:\n\n```cpp\nbool speedUp (int speed);\nbool speedUp (char* speed);\nint main()  \n{\n    bool ok = speedUp (NULL);\n}\n```\n\n2.  现在，让我们使用`nullptr`重写前面的代码:\n\n```cpp\nbool speedUp (int speed);\nbool speedUp (char* speed);\nint main()  \n{\n    bool ok = speedUp (nullptr);\n}\n```\n\n# 它是如何工作的...\n\n第一个程序可能没有编译，或者(如果它编译了)调用了错误的方法。我们希望它改叫`bool speedUp (char* speed);`。`NULL`的问题恰恰是这样的:`NULL`被定义为`0`，这是一个整数类型，由**预处理器**使用(它将`NULL`的所有事件替换为`0`)。这是一个巨大的区别，因为`nullptr`现在属于 C++ 原语类型，由**编译器**管理。\n\n对于第二个程序，调用`speedUp`(重载)方法，其中`char*`指针指向`nullptr`。这里没有歧义，我们称之为`char*`类型的版本。\n\n# 还有更多...\n\n`nullptr`代表*一个不指向任何物体的指针*:\n\n```cpp\nint* p = nullptr;\n```\n\n因此，不存在歧义，这意味着可读性提高了。另一个提高可读性的例子如下:\n\n```cpp\nif (x == nullptr) \n{\n    // ...\\\n}\n```\n\n这使得代码更易读，并且清楚地表明我们正在比较指针。\n\n# 请参见\n\n斯科特·梅耶斯的《有效的现代 C++ 》( T1)和比雅尼·斯特劳斯特鲁普的《T2 的 c++ 编程语言》( T3)这两本书非常详细地涵盖了这些主题。\n\n# 智能指针–唯一指针和共享指针\n\n本食谱将展示`unique_ptr`和`shared_ptr`的基本用法。这些智能指针是不想手动处理内存释放的程序员的主要助手。一旦你学会了如何正确地使用它们，这将节省头痛和调试会话的夜晚。\n\n# 怎么做...\n\n在本节中，我们将了解两个智能指针`std::unique_ptr`和`std::shared_ptr`的基本用法:\n\n1.  让我们通过开发以下类来开发一个`unique_ptr`示例:\n\n```cpp\n#include <iostream>\n#include <memory>\nclass CruiseControl\n{\npublic:\n    CruiseControl()\n    {\n        std::cout << \"CruiseControl object created\" << std::endl;\n    };\n    ~CruiseControl()\n    {\n        std::cout << \"CruiseControl object destroyed\" << std::endl;\n    }\n    void increaseSpeedTo(int speed)\n    {\n        std::cout << \"Speed at \" << speed << std::endl;\n    };\n};\n```\n\n2.  现在，让我们通过调用前面的类来开发一个`main`类:\n\n```cpp\nint main ()\n{\n    std::cout << \"unique_ptr test started\" << std::endl;\n    std::unique_ptr<CruiseControl> cruiseControl =\n    std::make_unique<CruiseControl>();\n    cruiseControl->increaseSpeedTo(12);\n    std::cout << \"unique_ptr test finished\" << std::endl;\n}\n```\n\n3.  我们来编译`g++ unique_ptr_01.cpp`。\n4.  `unique_ptr`的另一个例子显示了它在数组中的行为。让我们重用同一个类(`CruiseControl`):\n\n```cpp\nint main ()\n{\n    std::cout << \"unique_ptr test started\" << std::endl;\n    std::unique_ptr<CruiseControl[]> cruiseControl = \n        std::make_unique<CruiseControl[]>(3);\n    cruiseControl[1].increaseSpeedTo(12); \n    std::cout << \"unique_ptr test finished\" << std::endl;\n}\n```\n\n5.  让我们用一个小程序来看看`std::shared_ptr`的动作:\n\n```cpp\n#include <iostream>\n #include <memory>\nclass CruiseControl\n{\npublic:\n    CruiseControl()\n    {\n        std::cout << \"CruiseControl object created\" << std::endl;\n    };\n    ~CruiseControl()\n    {\n        std::cout << \"CruiseControl object destroyed\" << std::endl;\n    }\n    void increaseSpeedTo(int speed)\n    {\n        std::cout << \"Speed at \" << speed << std::endl;\n    };\n};\n```\n\n`main`看起来是这样的:\n\n```cpp\nint main ()\n{\n    std::cout << \"shared_ptr test started\" << std::endl;\n    std::shared_ptr<CruiseControl> cruiseControlMaster(nullptr);\n    {\n        std::shared_ptr<CruiseControl> cruiseControlSlave = \n           std::make_shared<CruiseControl>();\n        cruiseControlMaster = cruiseControlSlave;\n    }\n    std::cout << \"shared_ptr test finished\" << std::endl;\n}\n```\n\n它是如何工作的...一节将详细介绍这三个程序。\n\n# 它是如何工作的...\n\n通过运行第一个`unique_ptr`程序，即`./a.out`，我们得到如下输出:\n\n![](img/a50a8dd3-47ed-411d-bb58-ac9a532dff0c.png)\n\n`unique_ptr`是一个体现独特所有权理念的**智能指针**。唯一所有权，简单来说就是有且只有一个变量可以*拥有*一个指针。这个概念的第一个结果是，两个唯一的指针变量上不允许有复制操作符。只允许`move`，所有权从一个变量转移到另一个变量。运行的可执行文件显示该对象在当前范围的末尾被解除分配(在本例中，是`main`函数):`CruiseControl object destroyed`。开发人员不需要在需要的时候费力地记住调用`delete`，但是仍然保持对内存的控制，这是 C++ 相对于基于垃圾收集器的语言的主要优势之一。\n\n在第二个`unique_ptr`示例中，对于数组，有三个`CruiseControl`类型的对象已经被分配，然后被释放。为此，输出如下:\n\n![](img/54984bcf-dcb3-49ff-aeaa-a0c0aac1599c.png)\n\n第三个例子展示了`shared_ptr`的用法。程序的输出如下:\n\n![](img/9aecd0d1-6647-41a7-9f60-fcc91164b7aa.png)\n\n`shared_ptr`智能指针表示一个对象被多个变量指向(即被所有者指向)的概念。在这种情况下，我们谈论的是共享所有权。很明显，规则不同于`unique_ptr`的情况。一个对象**不能被释放**直到至少有一个变量在使用它。在这个例子中，我们定义了一个指向`nullptr`的`cruiseControlMaster`变量。然后，我们定义了一个块，在这个块中，我们定义了另一个变量:`cruiseControlSlave`。到目前为止，一切顺利！然后，仍然在区块内部，我们将`cruiseControlSlave`指针分配给`cruiseControlMaster`。此时，分配的对象有两个指针:`cruiseControlMaster`和`cruiseControlSlave`。当这个块被关闭时，`cruiseControlSlave`析构函数被调用，但是对象没有被释放，因为它仍然被另一个使用:`cruiseControlMaster`！当程序结束时，我们看到`shared_ptr test finished`日志，紧接着`cruiseControlMaster`之后，因为它是唯一一个指向`CruiseControl`对象释放的，对象然后构造函数被调用，如`CruiseControl object destroyed`日志中所报告的。\n\n很明显，`shared_ptr`数据类型有一个**引用计数**的概念来记录指针的数量。这些引用在构造函数期间增加(不总是；`move`构造函数不是)和复制赋值操作符，在析构函数中减少。\n\n参考计数变量可以安全增减吗？指向同一个对象的指针可能在不同的线程中，所以操作这个变量可能是个问题。这不是问题，因为引用计数变量是原子管理的(也就是说，它是原子变量)。\n\n关于尺寸的最后一点。`unique_ptr`和原始指针一样大，而`shared_ptr`通常是`unique_ptr`的两倍，因为引用计数变量。\n\n# 还有更多...\n\n我强烈建议始终使用`std::make_unique`和`std::make_shared`。它们的使用消除了代码重复并提高了异常安全性。想知道更多细节吗？`shared_ptr.h`([https://github . com/GCC-mirror/GCC/blob/master/libstdc % 2B % 2B-v3/include/bits/shared _ ptr . h](https://github.com/gcc-mirror/gcc/blob/master/libstdc%2B%2B-v3/include/bits/shared_ptr.h))和`shared_ptr_base.h`([https://github . com/GCC-mirror/GCC/blob/master/libstdc % 2B % 2B-v3/include/bits/shared _ ptr _ base . h](https://github.com/gcc-mirror/gcc/blob/master/libstdc%2B%2B-v3/include/bits/shared_ptr_base.h))包含 GCC `shared_ptr`实现，这样我们就可以看到如何操纵引用计数了\n\n# 请参见\n\n斯科特·梅耶斯的《有效的现代 C++ 》( T1)和比雅尼·斯特劳斯特鲁普的《T2 的 c++ 编程语言》( T3)这两本书非常详细地涵盖了这些主题。\n\n# 学习移动语义是如何工作的\n\n我们知道复制品很贵，尤其是重物。C++ 11 中引入的移动语义帮助我们避免昂贵的副本。`std::move`和`std::forward`背后的基本概念是**参考值**。这个食谱将告诉你如何使用`std::move`。\n\n# 怎么做...\n\n让我们开发三个程序来了解`std::move`及其普遍参考:\n\n1.  让我们从开发一个简单的程序开始:\n\n```cpp\n#include <iostream>\n#include <vector>\nint main () \n{\n    std::vector<int> a = {1, 2, 3, 4, 5};\n    auto b = std::move(a);\n    std::cout << \"a: \" << a.size() << std::endl;\n    std::cout << \"b: \" << b.size() << std::endl;\n}\n```\n\n2.  让我们开发第二个例子:\n\n```cpp\n#include <iostream>\n#include <vector>\nvoid print (std::string &&s)\n{\n    std::cout << \"print (std::string &&s)\" << std::endl;\n    std::string str (std::move(s));\n    std::cout << \"universal reference ==> str = \" << str\n              << std::endl;\n    std::cout << \"universal reference ==> s = \" << s << std::endl;\n}\nvoid print (std::string &s)\n{\n    std::cout << \"print (std::string &s)\" << std::endl;\n}\nint main()\n{\n    std::string str (\"this is a string\");\n    print (str);\n    std::cout << \"==> str = \" << str << std::endl;\n    return 0;\n}\n```\n\n3.  让我们看一个具有普遍意义的例子:\n\n```cpp\n#include <iostream>\nvoid print (std::string &&s)\n{\n    std::cout << \"print (std::string &&s)\" << std::endl;\n    std::string str (std::move(s));\n    std::cout << \"universal reference ==> str = \" << str\n              << std::endl;\n    std::cout << \"universal reference ==> s = \" << s << std::endl;\n}\nvoid print (std::string &s)\n{\n    std::cout << \"print (std::string &s)\" << std::endl;\n}\nint main()\n{\n    print (\"this is a string\");\n    return 0;\n}\n```\n\n下一节将详细描述这三个程序。\n\n# 它是如何工作的...\n\n第一个程序的输出如下(`g++ move_01.cpp`和`./a.out`):\n\n![](img/863d862f-50f8-46c8-894c-f4b94345d9ae.png)\n\n在这个程序中，`auto b = std::move(a);`做了几件事:\n\n1.  它将向量`a`转换为**右值参考**。\n2.  因为它是一个右值引用，所以调用向量移动构造函数，将`a`向量的内容移动到`b`向量。\n3.  `a`已经没有原始数据了，`b`有了。\n\n第二个程序的输出如下(`g++ moveSemantics2.cpp`和`./a.out`):\n\n![](img/0289ab6d-50b2-4b65-9cac-6cf1cddacdbe.png)\n\n在第二个例子中，我们传递给`print`方法的`str`字符串是一个**左值引用**(也就是说，我们可以取那个变量的地址)，所以它是通过引用传递的。\n\n第三个程序的输出如下(`g++ moveSemantics3.cpp`和`./a.out`):\n\n![](img/bd79797d-ed44-45d1-9215-35b82981f9b3.png)\n\n在第三个例子中，被调用的方法是以**通用参考**作为参数的方法:`print (std::string &&s)`。这是因为我们不能取`this is a string`的地址，这意味着它是一个右值引用。\n\n现在应该很清楚了`std::move`实际上并没有移动任何东西——它是一个函数模板，**对右值执行无条件强制转换**，就像我们在第一个例子中看到的那样。这允许我们将数据移动(而不是复制)到目标，并使源无效。`std::move`的好处是巨大的，尤其是每次我们看到一个方法(`T&&`)的右值引用参数时，它很可能是该语言以前版本(C++ 98 及以前)的副本。\n\n*可能:这取决于编译器优化。\n\n# 还有更多...\n\n`std::forward`有些相似(但目的不同)。它是对右值引用的条件转换。请通过阅读下一节中引用的书籍来了解更多关于`std::forward`、右值和左值的信息。\n\n# 请参见\n\n斯科特·梅耶斯的《有效的现代 C++ 》( T1)和比雅尼·斯特劳斯特鲁普的《T2 的 c++ 编程语言》( T3)这两本书非常详细地涵盖了这些主题。\n\n# 理解并发性\n\n过去，C++ 开发人员通常使用线程库或本机线程机制(例如`pthread`，一个 Windows 线程)来编写程序。自 C++ 11 以来，这种情况发生了巨大的变化，并发性是另一个重要的特性，它朝着自洽语言的方向发展。我们将在本食谱中看到的两个新功能是`std::thread`和`std::async`。\n\n# 怎么做...\n\n在本节中，我们将学习如何将`std::thread`用于基本场景(创建和加入)，以及如何向其传递和接收参数:\n\n1.  `std::thread`:使用基本线程方法`create`和`join`，编写以下代码:\n\n```cpp\n#include <iostream>\n#include <thread>\nvoid threadFunction1 ();\nint main()\n{\n    std::thread t1 {threadFunction1};\n    t1.join();\n    return 0;\n}\nvoid threadFunction1 ()\n{\n    std::cout << \"starting thread 1 ... \" << std::endl;\n    std::cout << \"end thread 1 ... \" << std::endl;\n}\n```\n\n2.  用`g++ concurrency_01.cpp -lpthread`编译。\n\n第二个例子类似于前面的例子，但是在这种情况下，我们传递并获取参数:\n\n1.  `std::thread`:创建并连接一个线程，传递一个参数，得到一个结果。编写以下代码:\n\n```cpp\n#include <iostream>\n#include <thread>\n#include <vector>\n#include <algorithm>\nvoid threadFunction (std::vector<int> &speeds, int& res);\nint main()\n{\n    std::vector<int> speeds = {1, 2, 3, 4, 5};\n    int result = 0;\n    std::thread t1 (threadFunction, std::ref(speeds), \n                    std::ref(result));\n    t1.join();\n    std::cout << \"Result = \" << result << std::endl;\n    return 0;\n}\nvoid threadFunction (std::vector<int> &speeds, int& res)\n{\n    std::cout << \"starting thread 1 ... \" << std::endl;\n    for_each(begin(speeds), end(speeds), [](int speed) \n    {\n        std::cout << \"speed is \" << speed << std::endl;\n    });\n    res = 10;\n    std::cout << \"end thread 1 ... \" << std::endl;\n}\n```\n\n2.  使用`g++ concurrency_02.cpp -lpthread`编译。\n\n第三个例子使用**异步**创建一个任务，执行它，得到结果，如下所示:\n\n1.  `std::async`:这里我们可以看到为什么 async 被称为**基于任务的线程**。编写以下代码:\n\n```cpp\nroot@b6e74d5cf049:/Chapter2# cat concurrency_03.cpp\n#include <iostream>\n#include <future>\nint asyncFunction ();\nint main()\n{\n    std::future<int> fut = std::async(asyncFunction);\n    std::cout << \"max = \" << fut.get() << std::endl;\n    return 0;\n}\nint asyncFunction()\n{\n    std::cout << \"starting asyncFunction ... \" << std::endl;\n    int max = 0;\n    for (int i = 0; i < 100000; ++ i)\n    {\n        max += i;\n    }\n    std::cout << \" Finished asyncFunction ...\" << std::endl;\n    return max;\n}\n```\n\n2.  现在，我们需要编译程序。这里有一个陷阱。由于我们使用了线程机制，编译器依赖于本机实现，在我们的例子中是`pthread`。为了编译和链接没有错误(我们会得到一个未定义的引用)，我们需要包括`-lpthread`:\n\n```cpp\ng++ concurrency_03.cpp -lpthread\n```\n\n在第四个例子中，`std::async`与`std::promise`和`std::future`结合使用，是一种让两个任务相互通信的好且简单的方法。让我们来看看:\n\n1.  `std::async`:这是另一个展示基本通信机制的`std::async`例子。让我们对它进行编码:\n\n```cpp\n#include <iostream>\n#include <future>\nvoid asyncProducer(std::promise<int> &prom);\nvoid asyncConsumer(std::future<int> &fut);\nint main()\n{\n    std::promise<int> prom;\n    std::future<int> fut = prom.get_future();\n    std::async(asyncProducer, std::ref(prom));\n    std::async(asyncConsumer, std::ref(fut));\n    std::cout << \"Async Producer-Consumer ended!\" << std::endl;\n    return 0;\n}\nvoid asyncConsumer(std::future<int> &fut)\n{\n    std::cout << \"Got \" << fut.get() << \" from the producer ... \"\n        << std::endl;\n}\nvoid asyncProducer(std::promise<int> &prom)\n{\n    std::cout << \" sending 5 to the consumer ... \" << std::endl;\n    prom.set_value (5);\n}\n```\n\n2.  最后编译:`g++ concurrency_04.cpp -lpthread`\n\n# 它是如何工作的...\n\n让我们分析一下前面的四个程序:\n\n1.  `std::thread`:下面的程序展示了创建和连接的基本线程用法:\n\n![](img/d24a2f92-5ce9-46f7-ab4f-7c3b1cba03ab.png)\n\n第一次测试没有什么复杂的。`std::thread`通过统一初始化用函数初始化并加入(等待线程完成)。线程将接受一个函数对象:\n\n```cpp\nstruct threadFunction \n{\n    int speed;\n    void operator ()();\n}\nstd::thread t(threadFunction);\n```\n\n2.  `std::thread`:创建并连接一个线程，传递一个参数，得到一个结果:\n\n![](img/ae0e61f3-9191-417d-b82f-9b9789c85852.png)\n\n第二个测试展示了如何使用`std::vector<int>& speeds`将参数传递给线程，并获得返回参数`int& ret`。这个测试展示了如何将参数传递给一个线程，*并不是*多线程代码(也就是说，如果*至少有一个*线程会在其他线程上写，那么将相同的参数传递给其他线程会导致一个争用的情况)！\n\n3.  `std::async`:这里我们可以看到 async 为什么叫**基于任务的** **穿线**:\n\n![](img/2c7dfa24-2b7e-420a-a868-5dca78c347a6.png)\n\n请注意，当我们调用`std::async(asyncFunction);`时，我们可以在编译时使用`auto fut = std::async(asyncFunction);`从`std::async`推导返回的类型。\n\n4.  `std::async`:这是另一个`std::async`的例子，展示了一个基本的通信机制:\n\n![](img/3659b664-f69d-4cda-a2f7-1a24654284c2.png)\n\n消费者`void asyncConsumer(std::future<int> &fut)`调用未来的`get()`方法，通过承诺上的`set_value()`方法得到生产者设定的价值。`fut.get()`等待需要计算的值(也就是说，这是一个阻塞调用)。\n\n# 还有更多...\n\nC++ 并发库不仅仅包括这个配方中显示的特性，尽管这些是基础特性。请前往比雅尼·斯特劳斯特鲁普的《C++ 编程语言》*第五章*第三段，探索可用的全套并发工具。\n\n# 请参见\n\n斯科特·梅耶斯的《有效的现代 C++ 》( T1)和比雅尼·斯特劳斯特鲁普的《T2 的 c++ 编程语言》( T3)这两本书非常详细地涵盖了这些主题。\n\n# 理解文件系统\n\nC++ 17 在新特性方面标志着又一个巨大的里程碑。`filesystem`库提供了一种更简单的与文件系统交互的方式。灵感来自`Boost.Filesystem`(2003 年上市)。这个食谱将展示它的基本特征。\n\n# 怎么做...\n\n在本节中，我们将通过使用`directory_iterator`和`create_directories`展示两个`filesystem`库的例子。虽然这个名称空间下肯定还有更多，但这两个片段的目标是突出它们的简单性:\n\n1.  `std::filesystem::directory_iterator`:我们来写下面的代码:\n\n```cpp\n#include <iostream>\n#include <filesystem>\nint main()\n{\n    for(auto& p: std::filesystem::directory_iterator(\"/\"))\n    std::cout << p << std::endl;\n}\n```\n\n2.  现在用`g++ filesystem_01.cpp -std=c++ 17 -lstdc++ fs`编译，其中 **`-std=c++ 17`** 告诉编译器使用 C++ 17 标准，`-lstdc++ fs`告诉编译器使用`filesystem`库。\n\n第二个例子是关于创建一个目录和一个文件:\n\n1.  `std::filesystem::create_directories`:写下以下代码:\n\n```cpp\n#include <iostream>\n#include <filesystem>\n#include <fstream>\nint main()\n{\n    std::filesystem::create_directories(\"test/src/config\");\n    std::ofstream(\"test/src/file.txt\") << \"This is an example!\"\n                                       << std::endl;\n}\n```\n\n2.  编译同上例:`g++ filesystem_02.cpp -std=c++ 17 -lstdc++ fs`。\n\n只用两行代码，我们就创建了一个文件夹结构，一个文件，并且还在上面写了字！就这么简单(而且便携)。\n\n# 它是如何工作的...\n\n`filesystem`库位于`std::filesystem`命名空间下的`<filesystem>`头。这两个测试虽然非常简单，但却是展示`filesystem`库有多强大所必需的。第一个程序的输出如下:\n\n![](img/e3d7f330-c990-493c-aac9-28ea974e1a71.png)\n\n`std::filesystem`方法的完整列表可以在这里找到:[https://en.cppreference.com/w/cpp/header/filesystem](https://en.cppreference.com/w/cpp/header/filesystem)。\n\n`std::filesystem::create_directories`在当前文件夹中创建一个目录(递归地，如果`test/src`不存在的话)，在这种情况下。当然，也管理绝对路径，当前行将完全有效，即`std::filesystem::create_directories(\"/usr/local/test/config\");`。\n\n源代码的第二行使用`ofstream`创建一个名为`test/src/file.txt` 的输出文件流，并将`<<`追加到字符串中:`This is an example!` *。*\n\n# 还有更多...\n\n`filesystem`图书馆深受`Boost.Filesystem`的启发，从 2003 年开始提供。如果你想做一点实验和调试，只需给编译器添加`-g`选项(将调试符号添加到二进制文件中):`g++ **-g** fs.cpp -std=c++ 17 -lstdc++ fs`。\n\n# 请参见\n\n斯科特·梅耶斯的《有效的现代 C++ 》( T1)和比雅尼·斯特劳斯特鲁普的《T2 的 c++ 编程语言》( T3)这两本书非常详细地涵盖了这些主题。\n\n# C++ 核心指南\n\n《C++ 核心指南》是由比雅尼·斯特劳斯特鲁普领导的一项合作努力，很像 C++ 语言本身。它们是许多组织多年讨论和设计的结果。他们的设计鼓励普遍适用性和广泛采用，但他们可以自由复制和修改，以满足您组织的需求*。*更准确地说，这些指南指的是 C++ 14 标准。\n\n# 准备好\n\n转到 GitHub，转到 C++ 核心指南文档([http://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines](http://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines)，以及 GitHub 项目页面:[https://github.com/isocpp/CppCoreGuidelines](https://github.com/isocpp/CppCoreGuidelines)。\n\n# 怎么做...\n\n《C++ 核心指南》分为易于浏览的部分。这些部分包括类和类层次结构、资源管理、性能和错误处理。《C++ 核心指南》是由比雅尼·斯特劳斯特鲁普和赫伯·萨特领导的一项合作努力，但总共有 200 多名投稿人参与(要了解更多信息，请访问[https://github . com/isocpp/cppcoreeguidelines/graph/投稿人](https://github.com/isocpp/CppCoreGuidelines/graphs/contributors))。他们提出的质量、建议和最佳实践令人难以置信。\n\n# 它是如何工作的...\n\n使用 C++ Core Guidelines 最常见的方法是在 GitHub 页面上保持浏览器选项卡打开，并为您的日常任务不断查阅它。\n\n# 还有更多...\n\n如果你想对已经提供的问题有所贡献，GitHub 页面包含了很多项目，可以随时挑选。更多信息请访问[https://github.com/isocpp/CppCoreGuidelines/issues](https://github.com/isocpp/CppCoreGuidelines/issues)。\n\n# 请参见\n\n本章的*在你的 makefile* 配方中添加 GSL 会有帮助。\n\n# 将 GSL 添加到您的制作文件中\n\n*“GSL”是本指南中指定的一小组类型和别名。在撰写本文时，它们在此的规范过于稀疏；我们计划添加一个 WG21 风格的接口规范，以确保不同的实现达成一致，并作为对可能的标准化的贡献提出建议，一如既往地服从委员会决定接受/改进/更改/拒绝的任何内容。”*–c++ 核心指南常见问题解答 50。\n\n# 准备好\n\n转到 GitHub，转到 C++ 核心指南文档:[http://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines](http://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines)。\n\n# 怎么做...\n\n在本节中，我们将通过修改 makefile 将**指南支持库** ( `gsl`)集成到一个程序中:\n\n1.  下载并复制一个`gsl`实现(例如[https://github.com/microsoft/GSL](https://github.com/microsoft/GSL))。\n2.  将`gsl`文件夹复制到您的项目中。\n3.  将 include 添加到 makefile: `-I$HOME/dev/GSL/include`。\n4.  在你的源文件中，包含`#include <gsl/gsl>`。\n\n`gsl`目前提供以下内容:\n\n*   `GSL.view`\n*   `GSL.owner`\n*   `GSL.assert: Assertions`\n*   `GSL.util: Utilities`\n*   `GSL.concept: Concepts`\n\n# 它是如何工作的...\n\n您可能已经注意到，要使`gsl`工作，您只需要在 makefile 中指定头文件文件夹路径，即`-I$HOME/dev/GSL/include`。另一个需要注意的细节是 makefile 中没有指定库。\n\n这是因为整个实现是在`gsl`文件夹下的头文件内联*提供的。*\n\n *# 还有更多...\n\n微软 GSL([http://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines](http://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines))只是微软维护的一个实现。你可以在这里找到另一个实现:[https://github.com/martinmoene/gsl-lite](https://github.com/martinmoene/gsl-lite)。这两个实现都已在麻省理工学院许可类型下发布。\n\n# 请参见\n\n*本章的 C++ 核心指南*食谱。\n\n# 理解概念\n\n一个**概念**是一个与模板结合使用的编译时谓词。C++ 20 标准通过为开发人员提供更多的编译时机会来传达其意图，无疑促进了泛型编程。我们可以将需求(或约束)等概念可视化，模板的用户必须遵守这些概念。为什么我们需要概念？你必须自己定义概念吗？这个食谱将回答这些和更多的问题。\n\n# 怎么做...\n\n在本节中，我们将使用`concepts`开发一个具体的模板示例:\n\n1.  我们想从 C++ 标准库中创建自己版本的`std::sort`模板函数。让我们从在`.cpp`文件中编写以下代码开始:\n\n```cpp\n#include <algorithm>\n#include <concepts>\n\nnamespace sp\n{\n    template<typename T>\n        requires Sortable<T>\n    void sort(T& container)\n    {\n        std::sort (begin(container), end(container));\n    };\n}\n```\n\n2.  现在，让我们使用我们的新模板类，其约束是我们传递的类型`std::vector`必须是可排序的；否则，编译器会通知我们:\n\n```cpp\nint main()\n{\n    std::vector<int> myVec {2,1,4,3};\n    sp::sort(vec);\n\n    return 0;\n}\n```\n\n我们将在下一节中查看细节。\n\n# 它是如何工作的...\n\n我坚信`concepts`是缺失的特征。在他们之前，模板没有一套定义良好的需求，在编译错误的情况下，也没有对它的简单而简短的描述。这是推动`concepts`功能设计的两大支柱。\n\n*步骤 1* 包括`std::sort`方法和`concepts`标题的算法`include`。为了不混淆编译器和我们自己，我们将新模板封装在一个名称空间`sp`中。如您所见，与我们过去使用的经典模板相比，差异非常小，差异在于`requires`关键字。\n\n`requires`告知编译器(以及模板用户)该模板仅在`T Sortable`类型(`Sortable<T>`)下有效。好的；什么是`Sortable`？这是一个只有在评估为真时才满足的谓词。还有其他方法可以指定约束，如下所示:\n\n*   跟在后面`requires`:\n\n```cpp\ntemplate<typename T>\nvoid sort(T& container) requires Sortable<T>;\n```\n\n*   作为`template`参数:\n\n```cpp\ntemplate<Sortable T>\nvoid sort(T& container)\n```\n\n我个人比较喜欢*里的风格怎么做...*节因为它更地道，更重要的是，允许我们把所有的`requires`保持在一起，就像这样:\n\n```cpp\ntemplate<typename T>\n requires Sortable<T> && Integral<T>\nvoid sort(T& container)\n{\n    std::sort (begin(container), end(container));\n}; \n```\n\n在这个例子中，我们想要传达我们的`sp::sort`方法对于类型`T`是有效的，也就是`Sortable`和`Integral`，不管什么原因。\n\n*第二步*简单地使用我们新定制的排序版本。为此，我们实例化了一个向量(也就是`Sortable`！)并输入到`sp::sort`方法中。\n\n# 还有更多...\n\n可能有些情况下你需要创造自己的概念。标准库包含了很多，所以你很可能需要一个。正如我们在上一节中所学的，当且仅当一个概念被评估为真时，它才是谓词。将一个概念定义为两个现有概念的组合可能是这样的:\n\n```cpp\ntemplate <typename T>\nconcept bool SignedSwappable() \n{\n    return SignedIntegral<T>() && Swappable<T>();\n}\n\n```\n\n在这里，我们可以使用`sort`方法:\n\n```cpp\ntemplate<typename T>\n requires SignedSwappable<T>\nvoid sort(T& container)\n{\n    std::sort (begin(container), end(container));\n}; \n```\n\n为什么这很酷？有几个原因:\n\n*   它让我们立即知道模板期望什么，而不会迷失在实现细节中(也就是说，需求或约束是显式的)。\n*   在编译时，编译器将评估是否满足约束。\n\n# 请参见\n\n*   *c++ 之旅，第二版，* B. Stroustrup: *第 7.2 章*和*第*章 *12.7* 获取标准库中定义的概念的完整列表。\n*   [https://gcc.gnu.org/projects/cxx-status.html](https://gcc.gnu.org/projects/cxx-status.html)获取用 GCC 版本和状态映射的 C++ 20 特性列表。\n\n# 使用跨度\n\n我们可能会遇到这样的情况:我们需要编写一个方法，但是我们希望能够灵活地接受一个普通数组或 STL 容器作为输入。`std::span`解决了这个问题。它为用户提供了一个连续元素序列的视图。这个食谱会教你如何使用它。\n\n# 怎么做...\n\n在这个食谱中，我们将编写一个带有一个参数(`std::span`)的方法，可以在不同的上下文中使用。然后，我们将强调它提供的灵活性:\n\n1.  让我们从添加我们需要的内容开始。然后，我们需要通过传递`std::span`类型的`container`变量来定义`print`方法:\n\n```cpp\n#include <iostream>\n#include <vector>\n#include <array>\n#include <span>\n\nvoid print(std::span<int> container)\n{\n    for(const auto &c : container) \n        std::cout << c << \"-\";\n}\n```\n\n2.  在`main`中，我们想要通过调用`print`方法来打印我们的数组:\n\n```cpp\nint main()\n{\n    int elems[]{4, 2, 43, 12};\n    print(elems);\n\n    std::vector vElems{4, 2, 43, 12};\n    print(vElems);\n}\n```\n\n让我们看看这是如何工作的。\n\n# 它是如何工作的...\n\n`std::span`描述一个对象，它引用一个连续的元素序列。C++ 标准将数组定义为具有连续的内存部分。这无疑简化了`std::span`的实现，因为典型的实现包括一个指向序列的第一个元素和大小的指针。\n\n*第 1 步*定义了传递`std::span`的`print`方法，我们可以解读为一个整数序列。任何具有连续内存的数组类型在方法中都将被视为一个序列。\n\n*步骤 2* 使用两个不同数组的`print`方法，一个是 C 风格的，另一个是 STL 库的`std::vector`部分。由于两个数组都被定义在内存的连续部分，因此`std::span`能够无缝地管理它们。\n\n# 还有更多...\n\n我们的方法考虑`int`类型的`std::span`。您可能需要使该方法通用。在这种情况下，您需要编写如下内容:\n\n```cpp\ntemplate <typename T>\nvoid print(std::span<T> container)\n{\n    for(const auto &c : container) \n        std::cout << c << \"-\";\n}\n```\n\n正如我们在*理解概念*食谱中所学，在这个模板中指定一些需求是明智的。因此，我们可能会写信给以下人员:\n\n```cpp\ntemplate <typename T>\n    requires Integral<T>\nvoid print(std::span<T> container)\n{\n    for(const auto &c : container) \n        std::cout << c << \"-\";\n}\n```\n\n`requires Integral<T>`将明确模板的`Integral`类型的需求。\n\n# 请参见\n\n*   *理解概念*食谱复习如何用模板编写概念并将其应用于`std::span`。\n*   [https://gcc.gnu.org/projects/cxx-status.html](https://gcc.gnu.org/projects/cxx-status.html)获取用 GCC 版本及其状态映射的 C++ 20 特性列表。\n\n# 了解范围是如何工作的\n\nC++ 20 标准增加了范围，它是容器的抽象，允许程序在容器的元素上统一操作。此外，Ranges 代表了一种非常现代和简洁的编写富于表现力的代码的方式。我们将了解到，使用管道和适配器时，这种表现力会更强。\n\n# 怎么做...\n\n在这一节中，我们将编写一个程序来帮助我们学习 Ranges 与管道和适配器的主要用例。给定一系列温度，我们希望过滤掉负温度，并将正温度(暖温度)转换为华氏温度:\n\n1.  在新的源文件上，键入以下代码。如您所见，两个 lambda 函数和一个`for`范围循环起作用:\n\n```cpp\n#include <vector>\n#include <iostream>\n#include <ranges>\n\nint main()\n{\n    auto temperatures{28, 25, -8, -3, 15, 21, -1};\n    auto minus = [](int i){ return i <= 0; };\n    auto toFahrenheit = [](int i) { return (i*(9/5)) + 32; };\n    for (int t : temperatures | std::views::filter(minus) \n                              | std::views::transform(toFahrenheit)) \n        std::cout << t << ' ';  // 82.4 77 59 69.8\n}\n```\n\n我们将在下一节分析 Ranges 背后的原因。我们还将了解到 Ranges 是`concepts`的第一批用户。\n\n# 它是如何工作的...\n\n`std::ranges`代表一种非常现代的方式，以可读的格式描述容器上的一系列动作。这是语言提高可读性的情况之一。\n\n*第 1 步*定义`temperatures`向量，其中包含一些数据。然后，我们定义了一个 lambda 函数，如果输入`i`大于或等于零，则该函数返回 true。我们定义的第二个λ将`i`转换为华氏温度。然后，我们循环温度(`viewable_range`)并通过管道连接到`filter`(在范围内称为`adaptor`)，这消除了基于`minus`λ函数的负温度。输出通过管道传输到另一个适配器，该适配器转换容器的每一个单独的项目，以便进行最后的循环并打印到标准输出。\n\nC++ 20 在我们用来迭代容器元素的层次上提供了另一个层次，一个更现代、更习惯的层次。通过将`viewable_range`与适配器相结合，代码更加简洁、紧凑和可读。\n\nC++ 20 标准库提供了更多遵循相同逻辑的适配器，包括`std::views::all`、`std::views::take`和`std::views::split`。\n\n# 还有更多...\n\n所有适配器都是模板，使用概念来定义特定适配器所需的需求。这方面的一个例子如下:\n\n```cpp\ntemplate<ranges::input_range V,                  std::indirect_unary_predicate<ranges::iterator_t<V>> Pred >\n    requires ranges::view<V> && std::is_object_v<Pred>\nclass filter_view : public ranges::view_interface<filter_view<V, Pred>>\n```\n\n这个模板就是我们在这个食谱中使用的`std::views::filter`。这个模板有两种类型:第一种是`V`，输入范围(即容器)，而第二种是`Pred`(在我们的例子中是 lambda 函数)。我们为此模板指定了两个约束:\n\n*   `V`必须是视图\n*   谓词必须是对象类型:函数、lambda 等等\n\n# 请参见\n\n*   *理解概念*食谱复习概念。\n*   前往[https://github.com/ericniebler/range-v3](https://github.com/ericniebler/range-v3)查看 C++ 20 库提案作者(Eric Niebler)的`range`实现。\n*   *学习[第 1 章](01.html)、*系统编程入门*中的 Linux 基础–shell*配方，注意到 C++ 20 Ranges 管道与我们在 shell 中看到的管道概念非常相似。\n*   欲了解更多关于`std::is_object`的信息，请访问以下链接:[https://en.cppreference.com/w/cpp/types/is_object](https://en.cppreference.com/w/cpp/types/is_object)。\n\n# 学习模块如何工作\n\n在 C++ 20 之前，只有一种分部分构造程序的方法:通过`#include`指令(由预编译器解析)。最新标准增加了另一种更现代的方法来达到同样的效果，称为**模块**。本食谱将向您展示如何使用模块编写代码以及`#include`和模块之间的区别。\n\n# 怎么做...\n\n在本节中，我们将编写一个由两个模块组成的程序。这个程序是我们在*学习 Range 如何工作*食谱中开发的程序的改进。我们将把温度代码封装在一个模块中，并在客户端模块中使用它。让我们开始吧:\n\n1.  让我们创建一个名为`temperature.cpp`的新`.cpp`源文件，并输入以下代码:\n\n```cpp\nexport module temperature_engine;\nimport std.core\n#include <ranges>\n\nexport \nstd::vector<int> toFahrenheitFromCelsius(std::vector<int>& celsius)\n{\n    std::vector<int> fahrenheit;\n    auto toFahrenheit = [](int i) { return (i*(9/5)) + 32; };\n    for (int t : celsius | std::views::transform(toFahrenheit)) \n        fahrenheit.push_back(t);\n\n    return fahrenheit;\n}\n```\n\n2.  现在，我们必须使用它。创建一个新文件(例如，`temperature_client.cpp`)并包含以下代码:\n\n```cpp\nimport temperature_engine;\nimport std.core;  // instead of iostream, containers \n                  // (vector, etc) and algorithm\nint main()\n{ \n    auto celsius = {28, 25, -8, -3, 15, 21, -1};\n    auto fahrenheit = toFahrenheitFromCelsius(celsius);\n    std::for_each(begin(fahrenheit), end(fahrenheit),\n        [&fahrenheit](int i)\n    {\n        std::cout << i << \";\";\n    });\n}\n```\n\n下一节将解释模块是如何工作的，它们与名称空间的关系，以及它们相对于`#include`预编译器指令的优势。\n\n# 它是如何工作的...\n\n模块是对`#include`指令的 C++ 20 解决方案。可能在这里是强制性的，因为数百万行遗留代码不能在一夜之间转换为使用模块。\n\n*步骤 1* 的主要目标是定义我们的`temperature_engine`模块。第一行`export module temperature_engine;`，定义了我们要导出的模块。接下来，我们有`import std.core`。这是 C++ 20 带来的最大区别之一:不再需要使用`#include`。具体来说，`import std.core`相当于`#include <iostream>`。我们还`#include`了范围。在这种情况下，我们以旧的方式*向您展示了混合新旧解决方案的代码是可能的。这很重要，因为它将允许我们如何更好地管理到模块的过渡。每次我们想从我们的模块中导出一些东西，我们只需要在它前面加上`export`关键字，就像我们用`toFahrenheitFromCelsius`方法做的那样。方法的实现不受影响，所以它的逻辑不会改变。*\n\n*步骤 2* 包含使用`temperature_engine`的模块客户端的代码。和上一步一样，我们只需要使用`import temperature_engine`并使用导出的对象。我们也用`import std.core`代替`#include <iostream>`。现在，我们可以像平常一样使用导出的方法，调用`toFahrenheitFromCelsius`并传递预期的输入参数。`toFahrenheitFromCelsius`方法返回一个整数向量，表示转换后的温度，单位为华氏度，这意味着我们只需要使用`for_each`模板方法，通过使用 **`import std.core`** 打印值，而我们通常会使用`#include <algorithm>`。\n\n此时的主要问题是:为什么要用模块而不是`#include`？`Module`不仅仅代表了句法上的差异，它更深层次:\n\n*   一个模块只编译一次，而`#includes`不编译。要使`#include`只编译一次，我们需要使用`#ifdef` `#define`和`#endif`预编译器。\n*   模块可以以任何顺序导入，而不影响其含义。这对于`#include`来说是不一样的。\n*   如果没有从模块中导出符号，客户端代码将无法使用它，如果用户使用了，编译器将发出错误通知。\n*   与包含不同，模块是不可传递的。将模块`A`导入模块`B`，当模块`C`使用模块`B`时，并不意味着自动获得模块`A`的访问权限。\n\n这对可维护性、代码结构和编译时间有很大影响。\n\n# 还有更多...\n\n一个反复出现的问题是，模块与名称空间不冲突(或重叠)吗？这是一个很好的观点，答案是否定的。名称空间和模块解决了两个不同的问题。命名空间是表达将一些声明组合在一起的意图的另一种机制。将组声明放在一起的其他机制是函数和类。如果两个班级发生冲突怎么办？我们可以将其中一个封装到一个名称空间中。你可以在*理解概念*食谱中看到一个这样的例子，在那里我们创建了我们自己的版本，叫做`sp::sort`。另一方面，模块是功能的逻辑集合。这两个概念是**正交的**，这意味着我可以让我的名字空间分布在更多的模块上。一个具体的例子是`std::vector`和`std::list`容器，它们在两个不同的模块中，但在同一个`namespace` : `std`上。\n\n另一件值得强调的事情是，模块允许我们将模块的一部分设置为`private`，以使其他**翻译单元** ( **状态**)无法访问它。如果要将符号导出为不完整的类型，这很有用，例如:\n\n```cpp\nexport module temperature_engine;\nimport std.core\n#include <ranges>\n\nexport struct ConversionFactors;  //exported as incomplete type\n\nexport \nvoid myMethod(ConversionFactors& factors)\n{\n    // ...\n}\n\nmodule: private;\nstruct ConversionFactors\n{\n    int toFahrenheit;\n    int toCelsius;\n};\n```\n\n# 请参见\n\n*   前往[https://gcc.gnu.org/projects/cxx-status.html](https://gcc.gnu.org/projects/cxx-status.html)查看模块(以及其他 C++ 20 特性)支持时间线。\n*   Lambda 的复习食谱。**"
  },
  {
    "path": "docs/cpp-sys-prog-cb/03.md",
    "content": "# 三、处理进程和线程\n\n进程和线程是任何计算的基础。一个程序很少仅仅由一个线程或进程组成。在本章中，您将学习处理线程和进程的基本方法。您还将了解到与**便携式操作系统界面** ( **POSIX** )相比，处理线程是多么简单方便。作为系统开发人员核心技能的一部分，学习这些技能非常重要。C++ 在其标准库中没有*进程*的概念，所以将使用 Linux 原生实现。\n\n本章将涵盖以下食谱:\n\n*   开始新的进程\n*   扼杀一个进程\n*   创建新线程\n*   创建守护进程\n\n# 技术要求\n\n为了让您立即尝试这些程序，我们设置了一个 Docker 映像，其中包含了我们在整本书中需要的所有工具和库。这是基于 Ubuntu 19.04 的。\n\n要进行设置，请执行以下步骤:\n\n1.  从[www.docker.com](https://www.docker.com/)下载安装 Docker 引擎。\n2.  通过运行以下命令从 Docker Hub 中拉出图像:`docker pull kasperondocker/system_programming_cookbook:latest`。\n3.  图像现在应该可以使用了。输入以下命令查看图像:`docker images`。\n\n4.  你现在至少应该有这个形象:`kasperondocker/system_programming_cookbook`。\n5.  借助以下命令，使用交互式外壳运行 Docker 映像:`docker run -it --cap-add sys_ptrace kasperondocker/system_programming_cookbook:latest /bin/bash`。\n6.  运行容器上的外壳现已可用。键入`root@39a5a8934370/# cd /BOOK/`以获取所有开发的程序，按章节。\n\n需要`--cap-add sys_ptrace`参数来允许 Docker 容器中的 **GNU 项目调试器** ( **GDB** )设置断点，默认情况下，Docker 不允许设置断点。\n\n**Disclaimer**: The C++ 20 standard has been approved (that is, technically finalized) by WG21 in a meeting in Prague at the end of February. This means that the GCC compiler version that this book uses, 8.3.0, does not include (or has very, very limited support for) the new and cool C++ 20 features. For this reason, the Docker image does not include the C++ 20 recipe code. GCC keeps the development of the newest features in branches (you have to use appropriate flags for that, for example, `-std=c++ 2a`); therefore, you are encouraged to experiment with them by yourself. So, clone and explore the GCC contracts and module branches and have fun.\n\n# 开始新的进程\n\n这个食谱将展示如何以编程方式开始一个新的过程。C++ 标准不包括对进程的任何支持，因此将使用 Linux 本机实现。能够管理程序中的流程是一项重要的技能，本食谱将教你流程的基本概念、**流程标识符** ( **PID** )、父 PID 以及所需的系统调用。\n\n# 怎么做...\n\n这个食谱将展示如何启动一个子进程，以及如何通过使用 Linux 系统调用让父进程等待子进程完成。应该展示两种不同的技巧:第一种，父母只是叉孩子；第二种，子进程使用`execl`系统调用运行应用。\n\n系统调用的另一种选择是使用外部库(或框架)，如 **Boost** 库。\n\n1.  首先，在名为`process_01.cpp`的新文件中键入程序:\n\n```cpp\n#include <stddef.h>\n#include <stdlib.h>\n#include <unistd.h>\n#include <sys/types.h>\n#include <sys/wait.h>\n#include <iostream>\n\nint main(void)\n{\n    pid_t child;\n    int status;\n    std::cout << \"I am the parent, my PID is \" << getpid()\n        << std::endl;\n    std::cout << \"My parent's PID is \" << getppid() << std::endl;\n    std::cout << \"I am going to create a new process...\"\n        << std::endl;\n    child = fork();\n    if (child == -1)\n    {\n```\n\n2.  我们必须考虑一个孩子可能没有分叉的情况，所以我们需要写下这一部分:\n\n```cpp\n        // fork() returns -1 on failure\n        std::cout << \"fork() failed.\" << std::endl;\n        return (-1);\n    }\n    else if (child == 0)\n    {\n```\n\n3.  这个分支是一个很好的例子，父母可以正确地分叉他们的孩子。这里的子节点只是将其 PID 打印到标准输出:\n\n```cpp\n      std::cout << \"I am the child, my PID is \" << std::endl;\n      std::cout << \"My parent's PID is \" << getppid() << std::endl;\n    }\n    else\n    {\n```\n\n4.  现在，我们必须让父进程等待子进程完成:\n\n```cpp\n        wait(&status); // wait for the child process to finish...\n        std::cout << \"I am the parent, my PID is still \"\n            << getpid() << std::endl;\n    }\n    return (0);\n}\n```\n\n现在，让我们开发上一个程序的`fork-exec`版本。\n\n1.  首先，在名为`process_02.cpp`的新文件中键入程序:\n\n```cpp\n#include <stddef.h>\n#include <stdlib.h>\n#include <stdio.h>\n#include <unistd.h>\n#include <sys/types.h>\n#include <sys/wait.h>\n#include <iostream>\n\nint main(void)\n{\n    pxid_t child;\n    int status;\n    std::cout << \"I am the parent, my PID is \" \n              << getpid() << std::endl;\n    std::cout << \"My parent's PID is \" \n              << getppid() << std::endl;\n    std::cout << \"I am going to create a new process...\" \n              << std::endl;\n    child = fork();\n    if (child == -1)\n    {\n        // fork() returns -1 on failure\n        std::cout << \"fork() failed.\" << std::endl;\n        return 1;\n    }\n    else if (child == 0)\n    {\n```\n\n2.  下面的代码块显示了运行`execl`*`ls -l`的子部分:*\n\n```cpp\n        if (execl(\"/usr/bin/ls\", \"ls\", \"-l\", NULL) < 0) \n        {\n            std::cout << \"execl failed!\" << std::endl;\n            return 2;\n        }\n        std::cout << \"I am the child, my PID is \" \n                  << getpid() << std::endl;\n        std::cout << \"My parent's PID is \" \n                  << getppid() << std::endl;\n    }\n    else\n    {\n        wait(&status); // wait for the child process to finish...\n    }\n    return (0);\n}\n```\n\n下一节将描述两种不同方法的细节(`fork`对`fork-exec` ) *。*\n\n# 它是如何工作的...\n\n让我们分析前面两个例子:\n\n1.  `fork`系统调用:通过编译`g++ process_01.cpp`和运行`./a.out`，输出如下:\n\n![](img/ee5c3fb9-61b9-4ed8-a7c1-ba0ba262c4d8.png)\n\n程序通过调用`fork`，创建调用过程的副本。这意味着这两个进程具有相同的代码，尽管它们是两个完全不同的进程，但代码库将是相同的。用户必须在`else if (child == 0)`部分钩住子代码。最终，父母将不得不等待孩子通过`wait(&status);`呼叫完成任务。另一种选择是`waitpid (123, &status, WNOHANG);`调用，它等待一个特定的 PID(或者等待所有子进程，如果第一个参数是`-1`)。`WNOHANG`使`waitpid`立即返回，即使孩子的状态不是立即可用。\n\n如果父进程没有等待子进程完成，会发生什么？也就是说，发生的事情是没有`wait(&status);`调用？从技术上来说，父母会完成，而仍在奔跑的孩子会变成**僵尸**。这在 2.6 版本之前的 Linux 内核中是一个巨大的问题，因为僵尸进程一直留在系统中，直到它们等待*。孩子的过程现在被`init`过程所采用(其 PID 为`1`，定期等待可能死亡的孩子。*\n\n *2.  `fork-exec`系统调用:\n\n![](img/ba51686a-cefb-4c91-9f71-5b4a93a1fc55.png)\n\n最常见的创建流程的方式是`fork` / `exec`组合。正如我们所看到的，`fork`用自己的 PID 创建了一个全新的进程，但是现在，`else if (child == 0)`部分执行一个外部进程，它有一个不同的代码库。这个例子只是调用`ls -l`命令来列出文件和目录，但是开发人员可以将任何可执行文件放在这里。\n\n# 还有更多...\n\n为什么应该使用进程而不是线程是需要考虑的一个重要方面。答案视情况而定，但一般来说，应考虑以下几个方面:\n\n*   线程在启动它的进程的相同内存空间中运行。这方面有利也有弊。主要的含义是，如果一个线程崩溃，整个应用就会崩溃。\n*   线程间的通信比进程间的通信快得多。\n*   可以用较低的权限(通过`setrlimit`)产生一个进程，以限制不可信代码可用的资源。\n*   在进程中设计的程序比在线程中设计的程序更加分离。\n\n这个食谱中的`fork` / `execl` / `wait`召唤有很多变体。`man pages`为整个通话系列提供全面的文档。以下截图指`man execl`:\n\n![](img/67bf3f79-e515-41b1-9a1a-779d804c909d.png)\n\n# 请参见\n\n参见[第 1 章](01.html)、*系统编程入门*，了解`man pages`和 Linux 的一般知识。\n\n# 扼杀一个进程\n\n在前面的食谱中，我们已经看到了两种方法来开始一个新的过程，在这个过程中，父母总是等待他们的孩子完成任务。情况并非总是如此。有时候，父母应该能够杀死孩子的过程。在这个食谱中，我们将看到一个如何做到这一点的例子。\n\n# 准备好\n\n作为先决条件，必须通过*开始一个新的过程*配方。\n\n# 怎么做...\n\n在本节中，我们创建了一个程序，其中父进程分叉其子进程，子进程将执行无限循环，父进程终止它:\n\n1.  让我们开发将被父母杀死的子程序:\n\n```cpp\n#include <stddef.h>\n#include <stdlib.h>\n#include <stdio.h>\n#include <unistd.h>\n#include <sys/types.h>\n#include <sys/wait.h>\n#include <iostream>\n\nint main(void)\n{\n    std::cout << \"Running child ...\" << std::endl;\n    while (true)\n        ;\n}\n```\n\n2.  接下来，我们要开发父程序(`/BOOK/Chapter03`文件夹中的`process_03.cpp`):\n\n```cpp\n#include <stddef.h>\n#include <stdlib.h>\n#include <stdio.h>\n#include <unistd.h>\n#include <sys/types.h>\n#include <sys/wait.h>\n#include <iostream>\nint main(void)\n{\n    pid_t child;\n    int status;\n    std::cout << \"I am the parent, my PID is \" << getpid() \n              << std::endl;\n    child = fork();\n    std::cout << \"Forked a child process with PID = \" \n              << child << std::endl;\n    if (child == -1)\n    {\n        std::cout << \"fork() failed.\" << std::endl;\n        return 1;\n    }\n    else if (child == 0)\n    {\n```\n\n3.  接下来，在父程序的子部分，我们启动上一步开发的子程序:\n\n```cpp\n        std::cout << \"About to run the child process with PID = \" \n                  << child << std::endl;\n        if (execl(\"./child.out\", \"child.out\", NULL) < 0)\n        {\n            std::cout << \"error in executing child proceess \" \n                      << std::endl;\n            return 2;\n        }\n    }\n    else\n    {\n```\n\n4.  在父程序的父部分(`else`部分)，我们必须杀死子进程并检查它是否被正确杀死:\n\n```cpp\n        std::cout << \"killing the child process with PID = \" \n                  << child << std::endl;\n        int status = kill (child, 9);\n        if (status == 0)\n            std::cout << \"child process killed ....\" << std::endl;\n        else\n            std::cout << \"there was a problem killing\n                the process with PID = \" \n                      << child << std::endl;\n    }\n    return (0);\n}\n```\n\n我们已经看到了父程序和子程序，父程序杀死子程序。在下一节中，我们将学习这些程序的机制。\n\n# 它是如何工作的...\n\n在这之前，我们必须编译子程序和父程序——`g++ process_03.cpp`和`g++ -o child.out process_04.cpp`。\n\n在编译`process_04.cpp`时，我们需要根据父进程的需要指定`-o child.out`(进程名为`a.out` *)* 。通过运行它，产生的输出如下:\n\n![](img/93a1d294-77bb-4e04-b466-55e24149172d.png)\n\n执行显示`PID = 218`的子进程被父进程正确杀死。\n\n该配方中的代码只是*开始新流程*配方的变体。不同的是，现在，作为其细化的一部分，父进程杀死了子进程`int status = kill (child, 9);`。`kill`系统调用接受要终止的进程的 PID 作为第一个参数，接受要发送给子进程的信号作为第二个参数。接受的信号如下:\n\n*   `1` = `HUP`(悬置)\n*   `2` = `INT`(中断)\n*   `3` = `QUIT`(退出)\n*   `6` = `ABRT`(中止)\n*   `9` = `KILL`(不可捕捉，不可忽略的杀戮)\n*   `14` = `ALRM`(闹钟)\n*   `15` = `TERM`(软件终止信号)\n\n`man 2 kill`, the `kill` system call, sends a signal to a process. On success, return `0`; otherwise, return `-1`. You need to include `#include <sys/types.h>` and `#include <signal.h>` to use it.\n\n# 还有更多...\n\n在[第二章](02.html)、*重新审视 C++* 中的*理解并发*食谱中，如果可能的话，我们基于`std::thread`和`std::async`提供了两种备选解决方案(并根据本书的性质提倡它们)。下一个食谱还提供了一个使用`std::thread`的具体例子。\n\n# 创建新线程\n\n过程不是构建软件系统的唯一方式；一个轻量级的替代方法是使用线程。这个方法展示了如何使用 C++ 标准库创建和管理线程。我们已经看到，使用 C++ 标准库的主要优势是它的可移植性，以及它不依赖于外部库(例如，Boost)的事实。\n\n# 怎么做...\n\n我们将要编写的代码将是对一个大整数向量求和的并发版本。向量被分成两部分；每个线程计算其部分的总和，主线程显示结果。\n\n1.  我们定义一个 10 万整数的向量，用`main`方法生成随机数:\n\n```cpp\n#include <iostream>\n#include <thread>\n#include <vector>\n#include <algorithm>\n\nvoid threadFunction (std::vector<int> &speeds, int start, int\n    end, int& res);\n\nint main()\n{    \n    std::vector<int> speeds (100000);\n    std::generate(begin(speeds), end(speeds), [] () \n        { return rand() % 10 ; });\n\n```\n\n2.  接下来，启动第一个线程，传递前 50，000 个整数:\n\n```cpp\n    int th1Result = 0;\n    std::thread t1 (threadFunction, std::ref(speeds), 0, 49999, \n        std::ref(th1Result));\n\n```\n\n3.  然后，启动第二个线程，传递第二个 50，000 个整数:\n\n```cpp\n    int th2Result = 0;    \n    std::thread t2 (threadFunction, std::ref(speeds), 50000, 99999, \n        std::ref(th2Result));\n\n```\n\n4.  等待两个线程的结果:\n\n```cpp\n    t1.join();\n    t2.join();\n    std::cout << \"Result = \" << th1Result + th2Result\n        << std::endl;\n    return 0;\n}\n\nvoid threadFunction (std::vector<int> &speeds, int start, int \n    end, int& res)\n{\n    std::cout << \"starting thread ... \" << std::endl;\n    for (int i = start; i <= end; ++ i)\n    res += speeds[i];\n    std::cout << \"end thread ... \" << std::endl;\n}\n```\n\n下一节将解释这一动态。\n\n# 它是如何工作的...\n\n用`g++ thread_01.cpp -lpthread`编译程序并执行，输出如下:\n\n![](img/49847d80-39be-498e-864d-f9653ab3426d.png)\n\n在*第 1 步*中，我们定义了`threadFunction`方法，这是一个基本的线程单元，负责将`start`到`end`的元素汇总到`speeds`中，并将结果保存在`res`输出变量中。\n\n在 s *步骤 2* 和*步骤 3* 中，我们启动了两个线程来对`t1`线程的前 5 万项和`t2`线程的后 5 万项进行计算。这两个线程同时运行，所以我们需要等待它们完成才能这样做。在*步骤 4* 中，我们等待`th1`和`th2`结果完成，将两个结果— `th1Results`和`th2Results`相加，并打印在标准输出(`stdout`)中。\n\n# 还有更多...\n\n*启动新流程*配方展示了如何创建流程，以及在什么情况下流程适合解决方案。值得强调的一个重要方面是，一个线程运行在创建它的进程的**相同的地址空间**中。尽管线程仍然是在一个更独立的(可运行的)模块中构建系统软件的一种好方法，但是如果一个线程崩溃(由于分段错误，或者如果 **`terminate`** 以某种方式被调用，等等)，整个应用就会崩溃。\n\n从积极的一面来看，线程间的轻松通信，正如我们在前面的代码中看到的，非常简单和高效。此外，线程彼此共享**静态**和**堆**内存，以及创建它们的过程。\n\n这个配方中的代码虽然简单，但是已经展示了一个任务(一个大数组的总和)是如何被并发执行的。顺便提一下，如果算法不是为并发运行而设计的，也就是说，如果线程之间存在依赖关系，那么多线程应用就毫无价值。\n\n在这种情况下需要注意的是，如果两个线程同时在两个处理器上运行，我们会使用**并行**这个词。在这种情况下，我们没有这个保证。\n\n我们已经使用了 C++ 标准库中的`std::thread`，但是同样的例子也可以使用`std::async`来编写。[第二章](02.html)、*重温 C++* ，展示了两者的一个例子。邀请您使用第二种方法重写该配方的代码。\n\n# 请参见\n\n在[第二章](02.html)、*重访 C++* 的*理解并发*配方中，有一个包含`std::thread`和`std::async`的配方介绍并发主题。还邀请您阅读斯科特·迈耶斯的*有效现代 C++* 和比雅尼·斯特劳斯特鲁普的*c++ 编程语言*中专门讨论线程的部分。\n\n# 创建守护进程\n\n系统编程实际上是密切处理操作系统资源、创建进程、线程、释放资源等等。有些情况下，我们需要一个进程来无限期地运行*；*即一个进程首先提供一些服务或者管理一个资源，然后一直保持运行。在后台无限期运行*的进程称为**守护进程**。这个方法将展示如何以编程方式产生守护进程。*\n\n# 怎么做...\n\n如上所述，守护进程是一个无限期运行的进程。一个进程，为了被归类为守护进程*，*必须有一些明确定义的属性，这些属性将在这个配方中用程序显示出来。\n\n1.  通过调用`umask`系统调用，键入以下代码重置子进程的初始访问权限:\n\n```cpp\n#include <unistd.h>\n#include <sys/stat.h>\n#include <iostream>\n\nint main(void)\n{\n    pid_t child;\n    int status;\n    std::cout << \"I am the parent, my PID is \" << getpid()\n        << std::endl;\n    std::cout << \"I am going to create a new daemon process...\"\n        << std::endl;\n\n    // 1\\. clear file creation mask\n    umask(0);\n\n```\n\n2.  为孩子键入要分叉的代码:\n\n```cpp\n    child = fork();\n    if (child == -1)\n    {\n        std::cout << \"fork() failed.\" << std::endl;\n        return (-1);\n    }\n    else if (child == 0) // child (daemon) process\n    {\n\n```\n\n3.  在子进程上键入`setsid`命令:\n\n```cpp\n        setsid();\n\n```\n\n4.  将工作目录更改为子进程(现在是守护进程):\n\n```cpp\n        if (chdir(\"/\") < 0)\n            std::cout << \"Couldn't change directly\" << std::endl;\n\n```\n\n5.  运行守护程序特定的任务——在这种情况下，只需休眠`10`秒:\n\n```cpp\n        // Attach here the daemon specific long running\n        // tasks ... sleep for now.\n        sleep (10);\n    }\n\n```\n\n6.  父进程在`fork`后退出:\n\n```cpp\n    return (0);\n}\n```\n\n下一节将更详细地解释这六点。\n\n# 它是如何工作的...\n\n用`g++ daemon_01.cpp`(在 Docker 映像的(`/BOOK/Chapter03`)文件夹中)编译代码并运行。输出如下:\n\n![](img/d3d2b3b2-859c-41b4-a28e-75a75e9411ee.png)\n\n当我们在 shell 上运行一个进程时，终端会等待子进程完成，然后再准备另一个命令。我们可以用`&`符号(例如`ls -l &`)运行命令，外壳会提示终端输入另一个命令。请注意，子进程仍将与父进程处于同一会话中。对于要成为守护进程的进程，应该应用以下规则(数字 *2* 和 *3* 是强制性的；其他是可选的):\n\n1.  **用参数`0`** ( `umask(0)`)调用`umask`:父进程创建子进程时，继承文件模式创建掩码(即子进程将继承父进程的初始访问权限)。我们要确保重置它们。\n2.  **让父进程在分叉**后退出:在前面的代码中，父进程创建了子进程后，返回。\n3.  **叫** `setsid`。这有三个作用:\n    *   子进程成为新创建会话的领导者。\n    *   它成为新流程组的领导者。\n    *   它与其控制终端分离。\n4.  **更改工作目录**:父进程可能运行在一个可能不会存在很久的临时(或挂载)文件夹中。设置当前文件夹以满足守护进程的长期期望是一个很好的做法。\n5.  **记录**:由于守护服务不再与任何终端设备相关联，将标准输入、输出和错误重定向到`/dev/null`是一个很好的做法。\n\n# 还有更多...\n\n到目前为止，进程的唯一标识符是 PID。也属于有**流程** **组 ID** ( **PGID** )的组。流程组是一个或多个流程的集合。同一组中的所有进程都可以从同一终端接收信号。每个组都有一个领导者，PGID 的值与领导者的 PID 相同。\n\n会话是一个或多个组的集合。该配方显示，通过调用`setsid`方法可以创建一个新的会话。\n\n一个会话可以有一个(单个)控制终端。`ps -efj`命令显示所有运行着`PID`、`PPID`和`PGID`的进程，以及每个进程的控制终端(`TTY`)信息:\n\n![](img/68f40447-735e-48ba-a16b-4a44ac8be662.png)\n\n输出显示`./a.out`守护进程有`PID = 19`，它是组(`PGID = 19`)的领导者，它没有连接到任何控制终端(`TTY= ?`)。\n\n# 请参见\n\n斯蒂文斯的《UNIX 环境下的高级编程》 【T4 环境下的高级编程】第 13 章致力于守护进程。*"
  },
  {
    "path": "docs/cpp-sys-prog-cb/04.md",
    "content": "# 四、深入探讨内存管理\n\n内存是处理系统开发的核心概念之一。分配、释放和学习如何管理内存，以及知道 C++ 可以提供什么来简化和管理内存，都是至关重要的。本章将通过学习如何使用 C++ 智能指针、对齐内存、内存映射输入/输出和分配器来帮助您掌握内存是如何工作的。\n\n本章将涵盖以下主题:\n\n*   学习自动记忆还是动态记忆\n*   学习何时使用`unique_ptr`，以及尺寸的含义\n*   学习何时使用`shared_ptr`，以及尺寸的含义\n*   分配对齐的内存\n*   检查分配的内存是否对齐\n*   处理内存映射的输入/输出\n*   亲自与分配器打交道\n\n# 技术要求\n\n为了让您立即尝试这些程序，我们设置了一个 Docker 映像，其中包含了我们在整本书中需要的所有工具和库。这是基于 Ubuntu 19.04 的。\n\n要进行设置，请执行以下步骤:\n\n1.  从[下载并安装 Docker 引擎](https://www.docker.com/)[r.com](https://www.docker.com/)。\n2.  通过运行以下命令从 Docker Hub 中拉出图像:`docker pull kasperondocker/system_programming_cookbook:latest`。\n\n3.  图像现在应该可以使用了。输入以下命令查看图像:`docker images`。\n4.  你现在至少应该有这个形象:`kasperondocker/system_programming_cookbook`。\n5.  借助以下命令，使用交互式外壳运行 Docker 映像:`docker run -it --cap-add sys_ptrace kasperondocker/system_programming_cookbook:latest /bin/bash`。\n6.  运行容器上的外壳现已可用。按章节输入`root@39a5a8934370/# cd /BOOK/`获取所有开发的程序。\n\n需要`--cap-add sys_ptrace`参数来允许 Docker 容器中的 **GNU 项目调试器** ( **GDB** )设置断点，默认情况下，Docker 不允许设置断点。\n\n**Disclaimer**: The C++ 20 standard has been approved (that is, technically finalized) by WG21 in a meeting in Prague at the end of February. This means that the GCC compiler version that this book uses, 8.3.0, does not include (or has very, very limited support for) the new and cool C++ 20 features. For this reason, the Docker image does not include the C++ 20 recipe code. GCC keeps the development of the newest features in branches (you have to use appropriate flags for that, for example, `-std=c++ 2a`); therefore, you are encouraged to experiment with them by yourself. So, clone and explore the GCC contracts and module branches and have fun.\n\n# 学习自动记忆还是动态记忆\n\n本食谱将重点介绍 C++ 提供的两种主要内存分配策略:**自动**和**动态**内存分配。当变量的作用域持续到定义它的块的持续时间时，它是自动的，并且它的分配和解除分配是自动的(也就是说，不由开发人员决定)。变量在堆栈上分配。\n\n如果变量分配在内存的动态部分(自由存储，通常称为*堆*)，则该变量是动态的，分配和解除分配由开发人员决定。动态内存分配提供的更大灵活性是有代价的，因为开发人员需要做更多的工作来避免内存泄漏、悬空指针等等。\n\n# 怎么做...\n\n本节将展示自动和动态变量分配的两个示例。\n\n1.  让我们创建一个我们需要的实用程序类:\n\n```cpp\nclass User\n{\npublic:\n    User(){\n        std::cout << \"User constructor\" << std::endl;\n    };\n    ~User(){\n        std::cout << \"User Destructor\" << std::endl;\n    };\n\n    void cheers() \n    {\n        std::cout << \" hello!\" << std::endl;};\n    };\n};\n```\n\n2.  现在，让我们创建`main`模块来显示自动内存使用情况:\n\n```cpp\n#include <iostream>\n\nint main()\n{\n    std::cout << \"Start ... \" << std::endl;\n    {\n        User developer;\n        developer.cheers();\n    }\n    std::cout << \"End ... \" << std::endl;\n}\n```\n\n3.  现在，我们将为动态内存使用编写`main`模块:\n\n```cpp\n#include <iostream>\n\nint main()\n{\n    std::cout << \"Start ... \" << std::endl;\n    {\n        User* developer = new User();\n        developer->cheers();\n        delete developer;\n    }\n    std::cout << \"End ... \" << std::endl;\n}\n```\n\n这两个节目，虽然结局相同，却展现了两种不同的处理记忆的方式。\n\n# 它是如何工作的...\n\n第一步，我们定义了一个`User`类，用来展示自动和动态内存分配的区别。它的构造函数和析构函数将用于显示类何时被分配和释放。\n\n在*第 2 步*中，我们可以看到变量只是定义为`User developer;`。C++ 运行时将负责分配堆栈内存并释放它，而不需要开发人员做额外的工作。这种类型的内存管理更快更容易，但有两个主要成本:\n\n*   内存量是有限的。\n*   该变量仅在内部`{ }`块中有效且可见，在该块中它被分配。\n\n在*步骤 3* 中，相同的对象被分配到动态内存(即**堆**)上。主要区别在于开发人员现在负责分配和解除分配所需的内存量。如果不释放内存(使用`free`)，就会出现泄漏。动态管理内存的优点如下:\n\n*   灵活性:指针引用分配的内存(变量`developer`)可以在整个程序中使用。\n*   可用内存的数量远远超过自动内存管理。\n\n# 还有更多...\n\n使用较新的 C++ 标准(从版本 11 开始)，可以安全地避免`new`和`delete`使用智能指针(`shared_ptr`和`unique_ptr`)。当不再使用内存时，这两个工具将负责释放内存。[第二章](02.html)、*重温 C++* ，提供了智能指针的复习。\n\n# 请参见\n\n接下来的两个食谱将显示何时使用`unique_ptr`和`shared_ptr`。\n\n# 学习何时使用 unique_ptr，以及对大小的影响\n\n在前面的食谱中，我们已经学习了 C++ 中管理内存的两种基本方式:自动和动态。我们还了解到，与自动内存(即从堆栈中获得的内存)相比，开发人员可以获得更多的动态内存，并且提供了极大的灵活性。另一方面，处理动态记忆可能是一种不愉快的经历:\n\n*   指针不指示它是指向数组还是指向单个对象。\n*   释放分配的内存时，不知道是必须使用`delete`还是`delete[]`，要看变量是怎么定义的。\n*   没有明确的方法来判断指针是否悬空。\n\n这些只是您在处理动态内存时可能会遇到的几个问题，然后是`new`和`delete`。`unique_ptr`是一个智能指针，这意味着它知道什么时候应该释放内存，减轻了开发人员的负担。在这个食谱中，你将学习如何正确使用`unique_ptr`和`make_unique`。\n\n# 怎么做...\n\n在这一部分，我们将开发一个程序来学习为什么`unique_ptr`是处理动态记忆的一种便捷方式；第二个方面是了解`unique_ptr`是否与原始指针大小相同:\n\n1.  我们将重用在之前的配方中开发的`User`类。\n2.  让我们编写`main`程序，用`make_unique`分配一个`User`对象，使用`unique_ptr`:\n\n```cpp\n#include <iostream>\n\nint main()\n{\n    std::cout << \"Start ... \" << std::endl;\n    {\n        auto developer = std::make_unique<User>();\n        developer->cheers();\n    }\n    std::cout << \"End ... \" << std::endl;\n}\n```\n\n3.  让我们看看记忆的含义:\n\n```cpp\nauto developer = std::make_unique<User>();\ndeveloper->cheers();\n\nUser* developer2 = new User();\nstd::cout << \"developer size = \" << sizeof (developer) << std::endl;\nstd::cout << \"developer2 size = \" << sizeof (developer2) << std::endl;\ndelete developer2;\n```\n\n你认为`developer`和`developer2`的大小会有什么不同？\n\n# 它是如何工作的...\n\n在*步骤 2* 中，我们使用`unique_ptr`来定义使用`std::make_unique`分配的变量。一旦分配了变量，就没有内存泄漏的风险，因为析构函数会自动为我们释放内存。输出如下:\n\n![](img/a9d63859-8852-406f-8c0e-3474395f5d97.png)\n\n在*步骤 3* 中，我们想要检查`unique_ptr`与原始指针相比是否增加了任何内存。好消息是`unique_ptr`的大小和原始指针版本一样。该步骤的输出如下:\n\n![](img/541b1ef5-331f-4f88-b69d-1e6006d78c37.png)\n\n`developer`和`developer2`变量大小相同，开发者可以同样对待。\n\n一个经验法则是将`unique_ptr`用于管理资源的变量，这些变量只拥有**的专属所有权**，这代表了大多数开发人员的用例。\n\n# 还有更多...\n\n默认情况下，`unique_ptr`为对象调用默认的`delete`析构函数，但是可以指定自定义的`delete`析构函数。如果指针变量不代表独占所有权，而是共享所有权，将其转换为`shared_ptr`很容易。\n\n需要强调的一个重要方面是`make_unique`不是 C++ 11 标准库的一部分，而是 C++ 14 库的一部分。如果您使用的是 C++ 11 标准库，那么它的实现非常简单。\n\n# 请参见\n\n[第 2 章](02.html)、*重温 C++* 有一个关于智能指针的专用配方，其中一个配方是关于共享和唯一指针的。建议阅读斯科特·迈耶斯的《T4 有效的现代 c++》。\n\n# 了解何时使用 shared_ptr，以及对大小的影响\n\n在前面的食谱中，我们已经学习了如何使用`unique_ptr`以非常方便的方式管理动态内存(在堆上分配)。我们还了解到`unique_ptr`必须被使用，以防内存的独占所有权，或者内存管理的资源。但是如果我们有一个由更多实体共同拥有的资源呢？如果我们必须管理的内存必须在所有所有者都完成他们的工作后释放呢？嗯，这正是`shared_ptr`的用例。就像`unique_ptr`一样，对于`shared_ptr`我们不用`new`分配内存，但是有一个模板函数(C++ 标准库的一部分)`make_shared`。\n\n# 怎么做...\n\n在这一部分，我们将开发一个程序来展示如何使用`shared_ptr`。您将了解到，只有当没有所有者再使用内存时，才会释放内存:\n\n1.  我们将重用第一个配方中开发的`User`类。现在让我们编写`main`模块:\n\n```cpp\nint main()\n{\n    std::cout << \"Start ... \" << std::endl;\n    auto shared1 = std::make_shared<User>();\n    {\n        auto shared2 = shared1;\n        shared2->cheers(); std::cout << \" from shared2\"\n            << std::endl;\n        shared1->cheers(); std::cout << \" from shared1\"\n            << std::endl;\n    }\n    std::cout << \"End ... \" << std::endl;\n}\n```\n\n2.  现在，让我们通过编写这个程序来看看`shared_ptr`使用的内存:\n\n```cpp\nint main()\n{\n    std::cout << \"Start ... \" << std::endl;\n    auto shared1 = std::make_shared<User>();\n   {\n        auto shared2 = shared1;\n        User* newAllocation = new User();\n        auto uniqueAllocation = std::make_unique<User>();\n\n        std::cout << \"shared2 size = \" << sizeof (shared2)\n            << std::endl;\n        std::cout << \"newAllocation size = \" <<\n            sizeof (newAllocation) << std::endl;\n        std::cout << \"uniqueAllocation size = \" <<\n            sizeof (uniqueAllocation) << std::endl;\n\n        delete newAllocation;\n    }\n    std::cout << \"End ... \" << std::endl;\n}\n```\n\n此时，我们应该知道`unique_ptr`相对于原始指针的大小(正如我们在*学习何时使用 unique_ptr 以及*配方大小的含义中所学的)。`shared_ptr`变量的大小是多少？还是老样子？在下一节中，我们将了解这一重要方面。\n\n# 它是如何工作的...\n\n在前面的第一个程序中，我们展示了如何使用`shared_ptr`。首先，我们分配了一个内存块，其中包含一个类型为`User`带有`auto shared1 = std::make_shared<User>();`的对象。到目前为止，`User`资源归`shared1`变量所有。接下来，进入块，我们将`shared1`变量分配给`shared2`到`auto shared2 = shared1;`。这意味着包含`User`对象的内存现在被`shared1`和`shared2`指向。使用构造函数副本`auto shared2 (shared1);`也可以达到同样的目的。由于`User`现在由两个变量指向，所以只有当所有变量都超出范围时，才会释放已用内存。实际上，输出证明内存是在主块的末尾被解除分配的(调用了`User`的析构函数)，而不是像`unique_ptr`那样在内部块的末尾:\n\n![](img/a75c73de-d7b4-4ff1-9412-af044422965d.png)\n\n`shared_ptr`对记忆的影响和`unique_ptr`不一样。原因是`shared_ptr`实现需要一个原始指针来跟踪内存(类似于`unique_ptr`，另一个原始指针用于资源的引用计数。\n\n这个引用计数变量必须是原子变量，因为它可以由不同的线程递增和递减:\n\n![](img/2bbb9914-5677-489e-adc5-d9acef0e9550.png)\n\n在运行第二个程序时，`shared_ptr`变量的内存大小通常是原始指针大小的两倍，正如我们在前面的输出中看到的。\n\n# 还有更多...\n\n另一个不可忽视的有趣点是，由于`shared_ptr`包含一个原子变量，它通常比普通变量慢。\n\n# 请参见\n\n[第 2 章](02.html)*重温 C++* ，有一个关于智能指针的专用配方，其中一个配方是关于共享和唯一指针的。建议阅读斯科特·迈耶斯的《T4 有效的现代 c++》。\n\n# 分配对齐的内存\n\n编写系统程序可能需要使用在内存中对齐的数据，以便有效地访问硬件(在某些情况下，甚至完全访问硬件)。例如，在 32 位架构的机器上，我们分配的内存与 4 字节边界对齐。在这个食谱中，你将学习如何使用 C++ 11 `std::aligned_storage`来分配对齐的内存。当然，还有其他更传统的机制来分配对齐的内存，但本书的目标是尽可能多地使用 C++ 标准库工具。\n\n# 怎么做...\n\n在本节中，我们将编写一个程序，该程序将使用分配给`std::aligned_storage`的内存，并将显示`std::alignment_of`的使用:\n\n1.  让我们从编写一个程序来检查当前机器上整数和双精度的默认对齐边界开始:\n\n```cpp\n#include <type_traits>\n#include <iostream>\nint main()\n{\n    std::cout << \"int alignment = \" << std::alignment_of<int>\n        ::value << std::endl;\n    std::cout << \"double alignment = \" << \n        std::alignment_of<double>::value << std::endl;\n    return (0);\n}\n```\n\n2.  现在，让我们编写一个程序来分配特定大小的内存。为此，我们使用`std::aligned_storage`:\n\n```cpp\n#include <type_traits>\n#include <iostream>\ntypedef std::aligned_storage<sizeof(int), 8>::type intAligned;\nint main()\n{\n    intAligned i, j;\n    new (&i) int();\n    new (&j) int();\n\n    int* iu = &reinterpret_cast<int&>(i);\n    *iu = 12;\n    int* ju = &reinterpret_cast<int&>(j);\n    *ju = 13;\n\n    std::cout << \"alignment = \" << std::alignment\n        _of<intAligned>::value << std::endl;\n    std::cout << \"value = \" << *iu << std::endl;\n    std::cout << \"value2 = \" << reinterpret_cast<int&>(i)\n        << std::endl;\n    return (0);\n}\n```\n\n分配对齐的内存可能很棘手，C++ 标准库(从版本 11 开始)提供了这两个特性(`std::alignment_of`、`std::aligned_storage`)来简化它。下一节将描述其背后的机制。\n\n# 它是如何工作的...\n\n第一个程序相当简单，通过`std::alignment_of`显示了两种图元类型在内存中的自然对齐。通过编译(`g++ alignedStorage.cpp`)和运行程序，我们有以下输出:\n\n![](img/cb15931f-34a9-47f0-8177-6b312346afba.png)\n\n这意味着每个整数将在边界的`4`字节对齐，浮点类型将与`8`字节对齐。\n\n在第二个程序中，我们需要一个与`8`字节对齐的整数。通过编译它并运行可执行文件，输出如下所示:\n\n![](img/9f9a06a2-f91c-41b7-a846-570c3b5837e4.png)\n\n你可能已经注意到我用`-g`选项进行了编译(添加调试符号)。我们这样做是为了用 GDB 的内存转储表明整数的内存在`8`字节处正确对齐:\n\n![](img/71381006-7525-4b1d-9919-2163adf644e0.png)\n\n从调试会话中，我们可以看到通过`x/20bd iu` ( `x` = *内存转储*)命令，我们在`iu`变量的地址后转储了`20`字节的内存。我们可以在这里看到一些有趣的东西:`iu`和`ju`变量都在`8`字节对齐。每个内存行显示`8`字节(测试一下:`0x7ffc57654470`*–*`0x7ffc57654468`=`8`)。\n\n# 还有更多...\n\n玩内存总是有风险的，这些新的 C++ 特性(以及其他在`std`命名空间中可用的特性)帮助我们**安全地玩**。建议还是一样:过早优化一定要谨慎使用；仅在必要时优化(即使用对齐内存)。最后一个建议:不鼓励使用`reinterpret_cast`，因为它在低水平上操纵记忆。你需要知道你在使用它的时候在做什么。\n\n# 请参见\n\n比雅尼·斯特劳斯特鲁普最新版*C+**+编程语言，第四版*有一段关于*内存对齐* ( *6.2.9* )和*对齐 _ 存储* ( *35.4.1* )。\n\n# 检查分配的内存是否对齐\n\n在前面的食谱中，您已经学习了如何使用 C++ 11 来分配对齐的内存。现在的问题是:我们如何知道记忆是正确对齐的？这个食谱会教你这个。\n\n# 怎么做...\n\n我们将使用前面的程序，通过稍微修改一下，我们将看到如何检查指针是否对齐:\n\n1.  让我们修改之前的程序，如下所示:\n\n```cpp\n#include <type_traits>\n#include <iostream>\n\nusing intAligned8 = std::aligned_storage<sizeof(int), 8>::type;\nusing intAligned4 = std::aligned_storage<sizeof(int), 4>::type;\n\nint main()\n{\n    intAligned8 i; new(&i) int();\n    intAligned4 j; new (&j) int();\n\n    int* iu = &reinterpret_cast<int&>(i);\n    *iu = 12;\n    int* ju = &reinterpret_cast<int&>(j);\n    *ju = 13;\n\n    if (reinterpret_cast<unsigned long>(iu) % 8 == 0)\n        std::cout << \"memory pointed by the <iu> variable \n        aligned to 8 byte\" << std::endl;\n    else\n        std::cout << \"memory pointed by the <iu> variable NOT \n        aligned to 8 bytes\" << std::endl;\n    if (reinterpret_cast<unsigned long>(ju) % 8 == 0)\n        std::cout << \"memory pointed by the <ju> variable aligned to \n        8 bytes\" << std::endl;\n    else\n        std::cout << \"memory pointed by the <ju> variable NOT \n        aligned to 8 bytes\" << std::endl;\n\n    return (0);\n}\n```\n\n我们特意创建了两个类型定义，一个用于对齐`8`字节(`intAligned8`)，一个用于对齐`4`字节(`intAligned4`)。\n\n# 它是如何工作的...\n\n在程序中，我们定义了两个变量`i`和`j`，分别为`intAligned8`和`intAligned4`类型。借助这两个变量(与`8`和`4`字节对齐)，我们可以通过检查`8`的除法结果是否为`0` : `((unsigned long)iu % 8 == 0)`来查看它们是否正确对齐。这确保了`iu`指针与`8`字节对齐。对于`ju`变量也是如此。通过运行前面的程序，我们将得到以下结果:\n\n![](img/d2ddef14-28cf-4cc5-9991-e6f703144054.png)\n\n不出所料:`iu`与`8`字节对齐，而`ju`则不对齐。\n\n# 还有更多...\n\n正如你可能已经注意到的，我们使用`reinterpret_cast`来允许模数(`%`)操作符，而不是 C 型转换`((unsigned long)iu % 8 == 0)`。如果您在 C++ 中开发，出于两个基本原因，我们鼓励您使用命名转换(`static_cast`、`reinterpret_cast`、`const_cast`、`dynamic_cast`):\n\n*   为了让程序员表达演员的意图\n*   为了保证演员的安全\n\n# 请参见\n\n关于这个主题的更多信息可以在 W. Richard Stevens 和 Stephen A. Rago 的《UNIX 中的高级编程》 *环境*中找到。\n\n当一部分内存对齐时，编译器可以进行很好的优化。编译器不可能知道这一点，因此无法进行任何优化。最后一个 C++ 20 标准增加了`std::assume_aligned`特性。这通知编译器指针的值是与特定字节数对齐的内存地址。可能发生的情况是，当我们分配一些对齐的内存时，指向该内存的指针被传递给其他函数。\n\n`std::assume_aligned`功能通知编译器假定指针指向的内存已经对齐，因此进行优化是安全的:\n\n```cpp\nvoid myFunc (int* p)\n{\n    int* pAligned = std::assume_aligned<64>(p);\n    // using pAligned from now on.\n}\n\n```\n\n`std::assume_aligned<64>(p);`功能通知编译器`p`已经至少与`64`字节对齐。如果内存没有对齐，您将获得未定义的行为。\n\n# 处理内存映射的输入/输出\n\n有时候，我们需要以一种不太常规或者说不太常见的方式来操作记忆。正如我们所看到的，内存是用`new`分配的，用`delete`释放(或者更好的是用`make_unique`和`make_shared`)。可能有些情况下，我们需要跳过某一层——也就是说，使用 Linux 系统调用；为了表现；或者是因为我们无法用 C++ 标准库映射的自定义行为。这就是`mmap` Linux 系统调用(`man 2 mmap`)的情况。`mmap`是一个符合 POSIX 的系统调用，允许程序员将一个文件映射到内存的一部分。除此之外，`mmap`还允许分配内存，这个食谱会教你怎么做。\n\n# 怎么做...\n\n本节将展示两个`mmap`用例:第一，如何将一个文件映射到内存的一部分；第二，如何使用`mmap`分配内存。让我们首先编写一个将文件映射到内存的程序。\n\n1.  在一个 shell 中，让我们创建一个名为`mmap_write.cpp`的新源文件。我们需要打开一个文件来映射:\n\n```cpp\n int fd = open(FILEPATH, O_RDWR | O_CREAT | O_TRUNC, (mode_t)0600);\n if (fd == -1)\n {\n    std::cout << \"Error opening file \" << FILEPATH << std::endl;\n    return 1;\n }\n```\n\n2.  其次，我们必须在文件中创建一个空间，稍后我们将使用它(`mmap`不这样做):\n\n```cpp\nint result = lseek(fd, FILESIZE-1, SEEK_SET);\nif (result == -1)\n{\n    close(fd);\n    std::cout << \"Error calling lseek \" << std::endl;\n    return 2;\n}\n\nresult = write(fd, \"\", 1);\nif (result != 1)\n{\n    close(fd);\n    std::cout << \"Error writing into the file \" << std::endl;\n    return 3;\n}\n```\n\n3.  然后，我们可以将文件(由`fd`文件描述符表示)映射到`map`变量:\n\n```cpp\n int* map = (int*) mmap(0, FILESIZE, PROT_READ | PROT_WRITE, \n     MAP_SHARED, fd, 0);\n if (map == MAP_FAILED)\n {\n     close(fd);\n     std::cout << \"Error mapping the file \" << std::endl;\n     return 4;\n }\n```\n\n4.  最后，我们需要在其中写入一些价值:\n\n```cpp\nfor (int i = 1; i <=NUM_OF_ITEMS_IN_FILE; ++ i)\n    map[i] = 2 * i;\n```\n\n5.  我们不要忘记关闭使用的资源:\n\n```cpp\nif (munmap(map, FILESIZE) == -1)\n    std::cout << \"Error un-mapping\" << std::endl;\n\nclose(fd);\n```\n\n6.  到目前为止看到的步骤都与用`mmap`写文件有关。为了完整起见，在这一步中，我们开发了一个程序来读取一个名为`mmap_read.cpp`的文件，它与我们所看到的非常相似。在这里，我们将只看到重要的部分(Docker 图像包含读者和作者的完整版本):\n\n```cpp\nint* map = (int*) mmap(0, FILESIZE, PROT_READ, MAP_SHARED, fd, 0);\nif (map == MAP_FAILED)\n{\n    close(fd);\n    std::cout << \"Error mapping the file \" << std::endl;\n    return 4;\n}\n\nfor (int i = 1; i <= NUM_OF_ITEMS_IN_FILE; ++ i)\n    std::cout << \"i = \" << map[i] << std::endl;\n```\n\n现在我们来学习如何使用`mmap`来分配内存。\n\n1.  现在我们用`mmap`来分配内存:\n\n```cpp\n#include <sys/mman.h>\n#include <iostream>\n#include <cstring>\n\nconstexpr auto SIZE = 1024;\n\nint main(int argc, char *argv[])\n{\n    auto* mapPtr = (char*) mmap(0, SIZE, \n                                PROT_READ | PROT_WRITE, \n                                MAP_PRIVATE | MAP_ANONYMOUS, \n                                -1, 0);\n if (mapPtr == MAP_FAILED)\n {\n     std::cout << \"Error mapping memory \" << std::endl;\n     return 1;\n }\n std::cout << \"memory allocated available from: \" << mapPtr\n   << std::endl;\n\n strcpy (mapPtr, \"this is a string!\");\n std::cout << \"mapPtr val = \" << mapPtr << std::endl;\n\n if (munmap(mapPtr, SIZE) == -1)\n     std::cout << \"Error un-mapping\" << std::endl;\n\n return 0;\n}\n```\n\n虽然简单，但这两个程序向您展示了如何使用`mmap`分配内存和管理文件。在下一节中，我们将看到它是如何工作的。\n\n# 它是如何工作的...\n\n在第一个程序中，我们学习了`mmap`最常见的用法:将文件映射到内存的一部分。由于在 Linux 中几乎任何资源都可以映射到一个文件，这意味着我们几乎可以用`mmap`将任何东西映射到内存。它确实接受文件描述符。通过首先编译并运行`mmap_write.cpp`程序，我们能够在内存中写入一个包含整数列表的文件。生成的文件将被称为`mmapped.txt`。有趣的是运行`mmap_read.cpp`读者程序。让我们编译并运行它:\n\n![](img/04bbe152-7fff-49a6-a27f-fc23edc804d7.png)\n\n我们可以看到，它正确地打印出了文件中的所有整数。\n\n严格来说，`mmap`不在堆内存中分配内存，也不在栈上分配内存。它是一个独立的内存区域，仍然在进程的虚拟空间中。`munmap`反其道而行之:它释放映射的内存，并将数据刷新到文件中(这种行为可以通过`msync`系统调用来控制)。\n\n第二个程序展示了`mmap`的第二个用例:以另一种方式为`new`和`malloc`分配内存。我们可以在对`mmap`的调用中看到一些差异:\n\n*   `MAP_PRIVATE`:修改是私密的。对内存所做的任何修改都不会反映回文件或其他映射。文件被映射为写入时复制。\n*   `MAP_ANONYMOUS`:表示将分配一部分大小为`SIZE`的内存，不与任何特定文件关联。\n*   我们传递的第五个参数`-1`是为了分配内存(即没有文件描述符)。\n\n我们分配了 1 KB 的内存，并使用了一个字符串。输出如下:\n\n![](img/b4a4ef22-fc72-4027-89d9-435480e7b79c.png)\n\n同样，当我们用`free`或`delete`释放内存时，我们需要用`munmap`释放映射内存。\n\n# 还有更多...\n\n`mmap`有几个优点值得一提:\n\n1.  如果将`mmap`与`MAP_SHARED`或`MAP_SHARED_VALIDATE`标志一起使用，则读写内存映射文件可以避免从实际文件中复制`read()`和`write()`所需的内容。事实上，当我们将一大块数据写入文件时，缓冲区会从用户空间移动到内核空间，读取一大块数据时也是如此。\n2.  读写内存映射文件是一种简单的内存访问。内存映射文件只能在内存中读写；在`munmap`调用时，内存被刷新回文件中。这种行为可以通过`msync`系统调用的`MS_SYNC`、`MS_ASYNC`和`MS_INVALIDATE`标志参数来控制。\n\n3.  非常方便的是，当多个进程映射内存中的同一个文件时，数据在所有进程之间共享(`MAP_SHARED`)。\n\n# 请参见\n\n查看`man 2 mmap`了解更多信息。更多信息可以在罗伯特·拉芙的 *Linux 系统编程，第二版*中找到。\n\n# 亲自与分配器打交道\n\nC++ **标准模板库** ( **STL** )容器是一种简单有效的资源管理方式。容器的一个巨大好处是它们可以管理(几乎)任何类型的数据。然而，在处理系统编程时，我们可能需要为容器提供一种管理内存的替代方法。分配器正是这样:它们为容器提供了一个定制的实现。\n\n# 怎么做...\n\n在本食谱中，您将学习实现自己的自定义分配器(在本例中基于`mmap`)以提供给标准库容器(`std::vector`):\n\n1.  让我们首先创建一个空的分配器模板:\n\n```cpp\ntemplate<typename T>\nclass mmap_allocator\n{\npublic:\n    using value_type = T;\n\n    template<typename U> struct rebind {\n        using alloc = mmap_allocator<U>;\n    };\n\n    mmap_allocator(){};\n    template <typename U>\n    mmap_allocator(const mmap_allocator<U> &alloc) noexcept {};\n\n    T* allocate(std::size_t n){};\n\n    void deallocate(T* p, std::size_t n) {}\n};\n```\n\n2.  可以看到，有复制构造函数、`allocate`和`deallocate`方法可以实现。让我们逐个实现它们(在这种情况下，不需要实现默认构造函数):\n\n```cpp\n    mmap_allocator(const mmap_allocator<U> &alloc) noexcept {\n      (void) alloc;};\n```\n\n3.  接下来，执行`allocate`方法:\n\n```cpp\n    std::cout << \"allocating ... n = \" << n << std::endl;\n    auto* mapPtr = static_cast<T*> (mmap(0, sizeof(T) * n, \n                                    PROT_READ | PROT_WRITE, \n                                    MAP_PRIVATE | MAP_ANONYMOUS, \n                                    -1, 0));\n    if (mapPtr != MAP_FAILED)\n        return static_cast<T*>(mapPtr);\n    throw std::bad_alloc();\n```\n\n4.  最后，执行`deallocate`方法:\n\n```cpp\n    std::cout << \"deallocating ... n = \" << n << std::endl;\n    (void) n;\n    munmap(p, sizeof(T) * n);\n```\n\n5.  `main`方法是这样的:\n\n```cpp\nint main ()\n{\n    std::vector<int, mmap_allocator<int>> mmap_vector = {1, 2,\n        3, 4, 5};\n\n    for (auto i : mmap_vector)\n        std::cout << i << std::endl;\n\n    return 0;\n}\n```\n\n`std::vector`的使用，可以看到，从用户的角度来看是无缝的。唯一的区别是指定我们想要使用哪个分配器。该容器将分配和释放内存，仅使用`mmap`和`munmap`，而不是基于`new`和`delete`的默认实现。\n\n# 它是如何工作的...\n\n这个程序的中心部分是两个方法:`allocate`，返回一个表示分配内存的指针，和`deallocate`，取一个指向要释放内存的指针。\n\n在第一步中，我们已经画出了将要用来分配和释放内存的接口。这是一个模板类，因为我们希望它对任何类型都有效。如前所述，我们必须实现的两种方法是`allocate`和`deallocate`。\n\n在第二步中，我们开发了复制构造函数，当我们想要构造一个对象时将调用它，传入相同类型的对象的输入。我们只是返回一个`typedef`，它将传达新对象使用哪个分配器。\n\n第三步，我们实现了构造器，基本上是用`mmap`分配`T`类型的对象`n`的空间。我们已经在之前的食谱中看到了`mmap`的用法，因此邀请您再次阅读该食谱。\n\n在第四步中，我们实现了`deallocate`方法，在这种情况下是调用`munmap`方法，该方法删除指定地址范围的映射。\n\n最后，`main`方法展示了如何将我们的自定义分配器用于`std::vector`(它可以是任何容器——例如，列表)。在变量`mmap_vector`的定义中，我们传递了两个参数:第一个参数`int`，通知编译器它将是一个整数向量，第二个参数`mmap_allocator<int>`，指示使用我们的自定义分配器`mmap_allocator`，而不是默认分配器。\n\n# 还有更多...\n\n在系统编程中，有一个**内存池**的概念，该内存池是系统预先保留的，并且必须在资源的整个生命周期中使用。本食谱中看到的`map_allocator`类可以很容易地修改，在构造函数中预先分配一部分内存，并在不影响系统内存的情况下从池中获取和释放。\n\n# 请参见\n\n斯科特·梅耶斯的《有效的现代 C++ 》( T2)和比雅尼·斯特劳斯特鲁普的《c++ 程序设计》( T4)的《T5 语言》( T6)这两本书详细介绍了这些主题。有关`mmap`的更多详细信息，请参考*处理内存映射输入/输出*配方。"
  },
  {
    "path": "docs/cpp-sys-prog-cb/05.md",
    "content": "# 五、使用互斥、信号量和条件变量\n\n本章将重点介绍可用于同步共享资源访问的最常见机制。我们将研究的同步机制可以防止一个关键部分(负责资源的程序段)被两个或多个进程或线程并发执行。在本章中，您将学习如何同时使用 POSIX 和 C++ 标准库同步构建块，例如互斥体、`std::condition_variable`、`std::promise`和`std::future`。\n\n本章将涵盖以下食谱:\n\n*   使用 POSIX 互斥体\n*   使用 POSIX 信号量\n*   POSIX 信号量高级用法\n*   同步构造块\n*   通过简单事件学习线程间通信\n*   使用条件变量学习线程间通信\n\n# 技术要求\n\n为了让您可以立即试用本章中的所有程序，我们设置了一个 Docker 映像，其中包含了我们在本书中需要的所有工具和库。它基于 Ubuntu 19.04。\n\n要进行设置，请执行以下步骤:\n\n1.  从[www.docker.com](http://www.docker.com)下载并安装 Docker 引擎。\n2.  从 Docker Hub 中拉出图像:`docker pull kasperondocker/system_programming_cookbook:latest`。\n3.  图像现在应该可以使用了。键入`docker images`命令查看图像。\n4.  你应该有如下图像:`kasperondocker/system_programming_cookbook`。\n5.  使用`docker run -it --cap-add sys_ptrace kasperondocker/system_programming_cookbook:latest /bin/bash`命令运行带有交互式外壳的 Docker 图像。\n6.  运行容器上的外壳现已可用。使用`root@39a5a8934370/# cd /BOOK/`获取本书将要开发的所有程序。\n\n允许 GDB 设置断点需要`--cap-add sys_ptrace`参数。Docker 默认不允许这样做。\n\n# 使用 POSIX 互斥体\n\n这个方法将教你如何使用 POSIX 互斥锁来同步从多个线程对资源的访问。我们将通过开发一个包含方法(关键部分)的程序来实现这一点，该方法将执行不能并发运行的任务。我们将使用`pthread_mutex_lock`、`pthread_mutex_unlock`和`pthread_mutex_init` POSIX 方法来同步线程对它的访问。\n\n# 怎么做...\n\n在本食谱中，我们将创建一个多线程程序，只需将一个整数增加到`200000`。为此，我们将开发负责递增计数器的关键部分，它必须受到保护。然后，我们将开发主部分，它将创建两个线程并管理它们之间的协调。让我们继续:\n\n1.  打开一个名为`posixMutex.cpp`的新文件，开发其结构和临界截面方法:\n\n```cpp\n#include <pthread.h>\n#include <iostream>\n\nstruct ThreadInfo\n{\n    pthread_mutex_t lock;\n    int counter;\n};\n\nvoid* increment(void *arg)\n{\n    ThreadInfo* info = static_cast<ThreadInfo*>(arg);\n    pthread_mutex_lock(&info->lock);\n\n    std::cout << \"Thread Started ... \" << std::endl;\n    for (int i = 0; i < 100000; ++ i)\n        info->counter++ ;\n    std::cout << \"Thread Finished ... \" << std::endl;\n\n    pthread_mutex_unlock(&info->lock);\n    return nullptr;\n}\n```\n\n2.  现在，在`main`部分，为线程间同步所需的锁添加`init`方法:\n\n```cpp\nint main()\n{\n    ThreadInfo thInfo;\n    thInfo.counter = 0;\n    if (pthread_mutex_init(&thInfo.lock, nullptr) != 0)\n    {\n        std::cout << \"pthread_mutex_init failed!\" << std::endl;\n        return 1;\n    }\n```\n\n3.  现在我们有了执行`increment`(也就是要保护的关键部分)的方法和管理线程间同步的锁，让我们创建线程:\n\n```cpp\n    pthread_t t1;\n    if (pthread_create(&t1, nullptr, &increment, &thInfo) != 0)\n    {\n        std::cout << \"pthread_create for t1 failed! \" << std::endl;\n        return 2;\n    }\n\n    pthread_t t2;\n    if (pthread_create(&t2, nullptr, &increment, &thInfo) != 0)\n    {\n        std::cout << \"pthread_create for t2 failed! \" << std::endl;\n        return 3;\n    }\n```\n\n4.  现在，我们必须等待线程完成任务:\n\n```cpp\n    pthread_join(t1, nullptr);\n    pthread_join(t2, nullptr);\n    std::cout << \"Threads elaboration finished. Counter = \" \n              << thInfo.counter << std::endl;\n    pthread_mutex_destroy(&thInfo.lock);\n    return 0;\n```\n\n这个程序(可以在`/BOOK/Chapter05/`文件夹下的 Docker 图像中找到)向我们展示了如何使用 POSIX 互斥接口来同步线程之间共享资源的使用——在本例中是一个计数器。我们将在下一节详细解释这个过程。\n\n# 它是如何工作的...\n\n在第一步中，我们创建了将参数传递给线程所需的`struct`:`struct ThreadInfo`。在这个`struct`中，我们放置了保护资源`counter`和计数器本身所需的锁。然后，我们开发了`increment`功能。`increment`逻辑上需要锁定`pthread_mutex_lock(&info->lock);`资源，增加计数器(或临界段需要的任何其他动作)，解锁`pthread_mutex_unlock(&info->lock);`资源让其他线程也这样做。\n\n第二步，我们开始开发`main`方法。我们做的第一件事是用`pthread_mutex_init`初始化锁互斥。这里，我们需要传递一个指向本地分配资源的指针。\n\n在第三步中，我们创建了两个线程，`th1`和`th2`。这些人负责兼管`increment`法。这两个线程是通过传递在*步骤 2* 中分配的`thInfo`地址，用`pthread_create` POSIX API 创建的。如果线程创建成功，它会立即开始细化。\n\n在第四步，也是最后一步，我们等待`th1`和`th2`将计数器的值打印到标准输出，我们期望是`200000`。通过编译`g++ posixMutex.cpp -lpthread`和运行`./a.out`程序，我们得到如下输出:\n\n![](img/5918fefd-5ad2-4e38-80ca-a057d0b440b7.png)\n\n正如我们所看到的，这两个线程从不重叠执行。因此，关键部分中的计数器资源得到了适当的管理，并且输出是我们所期望的。\n\n# 还有更多...\n\n在这个食谱中，为了完整起见，我们使用了`pthread_create`。使用 C++ 标准库中的`std::thread`和`std::async`可以达到完全相同的目标。\n\nThe `pthread_mutex_lock()` function locks the mutex. If the mutex is already locked, the calling thread will be blocked until the mutex becomes available. The `pthread_mutex_unlock` function unlocks the mutex if the current thread holds the lock on a mutex; otherwise, it results in undefined behavior.\n\n# 请参见\n\n欢迎您修改本程序，并结合 C++ 标准库中的`std::thread`或`std::async`使用`pthread_mutex_lock`和`pthread_mutex_unlock`。参见[第二章](02.html)、*重访 C++* ，刷新自己对这个话题的认识。\n\n# 使用 POSIX 信号量\n\nPOSIX 互斥体显然不是您可以用来同步对共享资源的访问的唯一机制。这个食谱将告诉你如何使用另一个 POSIX 工具来达到同样的结果。信号量不同于互斥体，这个食谱将教你它们的基本用法，而下一个将向你展示更高级的用法。信号量是线程和/或进程之间的通知机制。根据经验，尝试使用互斥作为同步机制，信号量作为通知机制。在这个配方中，我们将开发一个类似于我们在*中使用 POSIX 互斥体*配方构建的程序，但是这一次，我们将使用信号量保护关键部分。\n\n# 怎么做...\n\n在这个食谱中，我们将创建一个多线程程序来增加一个整数，直到它达到`200000`。同样，负责增量的代码部分必须受到保护，我们将使用 POSIX 信号量。`main`方法将创建两个线程，并确保资源被正确销毁。让我们开始吧:\n\n1.  让我们打开一个名为`posixSemaphore.cpp`的新文件，开发结构和临界区方法:\n\n```cpp\n#include <pthread.h>\n#include <semaphore.h>\n#include <iostream>\n\nstruct ThreadInfo\n{\n    sem_t sem;\n    int counter;\n};\n\nvoid* increment(void *arg)\n{\n    ThreadInfo* info = static_cast<ThreadInfo*>(arg);\n    sem_wait(&info->sem);\n\n    std::cout << \"Thread Started ... \" << std::endl;\n    for (int i = 0; i < 100000; ++ i)\n        info->counter++ ;\n    std::cout << \"Thread Finished ... \" << std::endl;\n\n    sem_post(&info->sem);\n    return nullptr;\n}\n```\n\n2.  现在，在`main`部分，为线程间同步所需的锁添加`init`方法:\n\n```cpp\nint main()\n{\n    ThreadInfo thInfo;\n    thInfo.counter = 0;\n    if (sem_init(&thInfo.sem, 0, 1) != 0)\n    {\n        std::cout << \"sem_init failed!\" << std::endl;\n        return 1;\n    }\n```\n\n3.  现在`init`部分已经完成，让我们编写启动两个线程的代码:\n\n```cpp\npthread_t t1;\nif (pthread_create(&t1, nullptr, &increment, &thInfo) != 0)\n{\n    std::cout << \"pthread_create for t1 failed! \" << std::endl;\n    return 2;\n}\n\npthread_t t2;\nif (pthread_create(&t2, nullptr, &increment, &thInfo) != 0)\n{\n    std::cout << \"pthread_create for t2 failed! \" << std::endl;\n    return 3;\n}\n```\n\n4.  最后，这里是结束部分:\n\n```cpp\n    pthread_join(t1, nullptr);\n    pthread_join(t2, nullptr);\n\n    std::cout << \"posixSemaphore:: Threads elaboration\n        finished. Counter = \" \n              << thInfo.counter << std::endl;\n    sem_destroy(&thInfo.sem);\n    return 0;\n}\n```\n\n我们用于 POSIX 互斥体的相同程序现在用 POSIX 信号量运行。如您所见，程序的设计没有改变——真正改变的是我们用来保护关键部分的 API。\n\n# 它是如何工作的...\n\n第一部分包含用于与`increment`方法通信的结构和方法本身的定义。与程序的互斥版本相比，主要区别在于我们现在包含了`#include <semaphore.h>`头，这样我们就可以使用 POSIX 信号量 APIs。然后，在结构中，我们使用`sem_t`类型，这是保护关键部分的实际信号量。`increment`方法有两个壁垒保护实际逻辑:`sem_wait(&info->sem);`和`sem_post(&info->sem);`。这两种方法分别自动递减和递增`sem`计数器。`sem_wait(&info->sem);`通过将计数器递减`1`来获取锁。如果计数器的值大于 0，则获取锁，线程可以进入临界区。`sem_post(&info->sem);`退出临界区时，只需将计数器加 1。\n\n第二步，我们通过调用`sem_init` API 初始化信号量。这里，我们传递了三个参数:\n\n*   要初始化的信号量。\n*   `pshared`论点。这表明信号量是在进程的线程之间共享还是在进程之间共享。`0`表示第一种选择。\n*   最后一个参数表示信号量的初始值。通过将`1`传递给`sem_init`，我们要求信号量保护一个资源。信号量通过`sem_wait`和`sem_post`，将在内部自动增加和减少计数器，让每个线程一次一个地进入临界区。\n\n在第三步中，我们创建了两个使用`increment`方法的线程。\n\n在最后一步中，我们等待两个线程用`pthread_join`完成细化，并且，与本节最相关的是，我们通过传递到目前为止使用的信号量结构，用`sem_destroy`破坏了信号量结构。\n\n让我们编译并执行程序:`g++ posixSemaphore.cpp -lpthread`。即使在这种情况下，我们也需要通过在使用`pthreads`时将`-lpthread`选项传递给 g++ 来将程序与`libpthread.a`链接起来。这样做的输出如下:\n\n![](img/6b0aabdc-b066-4e25-b9fc-5300752d41a4.png)\n\n不出所料，输出显示计数器在`200000`。这也表明两条线程并不重叠。\n\n# 还有更多...\n\n我们通过将值`1`传递给`sem_init`方法，将`sem_t`用作二进制信号量。信号量可以用作*计数信号量*，这意味着将大于 1 的值传递给`init`方法。在这种情况下，意味着临界区将被 *N* 线程并发访问。\n\nFor more information on the GNU/Linux man pages, type `man sem_init` in a shell.\n\n# 请参见\n\n你可以在下一个食谱中找到更多关于*计数信号量*的信息，我们将在这里了解互斥体和信号量的区别。\n\n欢迎您修改本程序，并结合 C++ 标准库中的`std::thread`或`std::async`使用`pthread_mutex_lock`和`pthread_mutex_unlock`。\n\n# POSIX 信号量高级用法\n\n*使用 POSIX 信号量*配方向我们展示了如何使用 POSIX 信号量来保护一个关键区域。在本食谱中，您将学习如何将其用作计数信号量和通知机制。我们将通过开发一个经典的发布-订阅程序来做到这一点，其中有一个发布者线程和一个消费者线程。这里的挑战是，我们希望将队列中项目的最大数量限制为一个定义的值。\n\n# 怎么做...\n\n在这个食谱中，我们将编写一个程序来表示计数信号量的典型用例——一个生产者-消费者问题，在这个问题中，我们希望将队列中的项目数量限制在一定的数量。让我们开始吧:\n\n1.  让我们打开一个名为`producerConsumer.cpp`的新文件，并在两个线程中编码我们需要的结构:\n\n```cpp\n#include <pthread.h>\n#include <semaphore.h>\n#include <iostream>\n#include <vector>\n\nconstexpr auto MAX_ITEM_IN_QUEUE = 5;\n\nstruct QueueInfo\n{\n    sem_t mutex;\n    sem_t full;\n    sem_t empty;\n    std::vector<int> queue;\n};\n```\n\n2.  现在，让我们为`producer`编写代码:\n\n```cpp\nvoid* producer(void *arg)\n{\n    QueueInfo* info = (QueueInfo*)arg;\n    std::cout << \"Thread Producer Started ... \" << std::endl;\n    for (int i = 0; i < 1000; i++)\n    {\n        sem_wait(&info->full);\n\n        sem_wait(&info->mutex);\n        info->queue.push_back(i);\n        std::cout << \"Thread Producer Started ... size = \" \n                  << info->queue.size() << std::endl;\n        sem_post(&info->mutex);\n\n        sem_post(&info->empty);\n    }\n    std::cout << \"Thread Producer Finished ... \" << std::endl;\n    return nullptr;\n}\n```\n\n3.  我们对`consumer`也这样做:\n\n```cpp\nvoid* consumer(void *arg)\n{\n    QueueInfo* info = (QueueInfo*)arg;\n    std::cout << \"Thread Consumer Started ... \" << std::endl;\n    for (int i = 0; i < 1000; i++)\n    {\n        sem_wait(&info->empty);\n\n        sem_wait(&info->mutex);\n        if (!info->queue.empty())\n        {\n            int b = info->queue.back();\n            info->queue.pop_back();\n        }\n        sem_post(&info->mutex);\n\n        sem_post(&info->full);\n    }\n    std::cout << \"Thread Consumer Finished ... \" << std::endl;\n    return nullptr;\n}\n```\n\n4.  现在，我们需要对`main`方法进行编码，以便初始化资源(例如信号量):\n\n```cpp\nint main()\n{\n    QueueInfo thInfo;\n    if (sem_init(&thInfo.mutex, 0, 1) != 0 ||\n        sem_init(&thInfo.full, 0, MAX_ITEM_IN_QUEUE) != 0 ||\n        sem_init(&thInfo.empty, 0, 0) != 0)\n    {\n        std::cout << \"sem_init failed!\" << std::endl;\n        return 1;\n    }\n\n    pthread_t producerPthread;\n    if (pthread_create(&producerPthread, nullptr, &producer, \n        &thInfo) != 0)\n    {\n        std::cout << \"pthread_create for producer failed! \"\n            << std::endl;\n        return 2;\n    }\n    pthread_t consumerPthread;\n    if (pthread_create(&consumerPthread, nullptr, &consumer, \n        &thInfo) != 0)\n    {\n        std::cout << \"pthread_create for consumer failed! \"\n           << std::endl;\n        return 3;\n    }\n```\n\n5.  最后，我们需要对释放资源的部分进行编码:\n\n```cpp\n    pthread_join(producerPthread, nullptr);\n    pthread_join(consumerPthread, nullptr);\n\n    sem_destroy(&thInfo.mutex);\n    sem_destroy(&thInfo.full);\n    sem_destroy(&thInfo.empty);\n    return 0;\n}\n```\n\n这个程序是基于信号量的消费者-生产者问题的典型实现，展示了如何将资源的使用限制在 *N* (在我们的例子中，`MAX_ITEM_IN_QUEUE`)。这个概念可以应用于其他问题，包括如何限制数据库的连接数等等。如果我们启动两个生产者线程，而不是一个生产者，会发生什么？\n\n# 它是如何工作的...\n\n在程序的第一步，我们定义了让两个线程通信所需的`struct`。它包含以下内容:\n\n*   一`full`信号量(计数信号量):该信号量设置为`MAX_ITEM_IN_QUEUE`。这限制了队列中项目的数量。\n*   一个`empty`信号量(计数信号量):这个信号量在队列为空时通知进程。\n*   一个`mutex`信号量(二进制信号量):这是一个用信号量实现的互斥体，需要它来提供对队列访问的互斥。\n*   队列:用`std::vector`实现。\n\n第二步，我们实现了`producer`方法。该方法的核心部分是`for`循环实现。生产者的目标是同时将不超过`MAX_ITEM_IN_QUEUE`项的项推入队列，这样生产者试图通过减少`full`信号量(我们在`sem_init`中将其初始化为`MAX_ITEM_IN_QUEUE`，然后将项推入队列并增加空信号量(这给予消费者继续并从队列中读取的权限)来进入关键区域。为什么我们需要通知消费者可以阅读某个项目？换句话说，为什么我们需要在制作方调用`sem_post(&info->empty);`？如果我们不这样做，消费者线程将连续读取项目，并将继续增加`full`信号量到大于`MAX_ITEM_IN_QUEUE`的值，结果是队列中有超过`MAX_ITEM_IN_QUEUE`个项目。\n\n第三步，我们实现了`consumer`方法。这是`producer`的镜面。消费者所做的是等待通知用`sem_wait(&info->empty);`从队列中读取一个项目，从队列中读取，然后递增`full`信号量。最后一步可以这样理解:我刚刚消费了队列中的一个项目。\n\n第四步是启动两个线程并初始化三个信号量。\n\n第五步是收尾部分。\n\n如果我们启动更多的生产者，代码仍然可以工作，因为`full`和`empty`信号量将确保我们前面描述的行为，队列上的`mutex`确保一次只有一个项目在上面写入/读取。\n\nPOSIX 互斥体和信号量都可以在线程和进程之间使用。为了让一个信号量在进程之间工作，我们只需要在`sem_init`的第二个参数中传递一个不同于 0 的值。对于互斥体，我们需要在调用`pthread_mutexattr_setpshared`时传递`PTHREAD_PROCESS_SHARED`标志。通过构建和运行程序，我们将得到如下输出:\n\n![](img/df663e70-e7d1-4142-85b6-54910957bcbb.png)\n\n让我们在下一节看到更多关于这个食谱的内容。\n\n# 还有更多...\n\n值得强调的是，一个信号量可以被初始化(第三个参数`sem_init`方法)为三个可能的值:\n\n*   致`1`:在这种情况下，我们使用信号量作为互斥体。\n*   致`N`:在这种情况下，我们使用信号量作为*计数信号量*。\n*   到`0`:我们像使用通知机制一样使用信号量(参见前面的`empty`信号量示例)。\n\n一般来说，信号量必须被视为线程或进程之间的通知机制。\n\n什么时候应该使用 POSIX 信号量和 POSIX 互斥体？尝试使用互斥作为同步机制，信号量作为通知机制。此外，考虑到 POSIX 互斥体通常比 Linux 内核中的 POSIX 信号量更快。\n\n最后一件事:记住 POSIX 互斥体和信号量都会让任务进入睡眠状态，而 spinlocks 不会。事实上，当互斥体或信号量被锁定时，Linux 调度程序会将任务放入等待队列。\n\n# 请参见\n\n请查看以下列表了解更多信息:\n\n*   本章中的*使用 POSIX 互斥体*方法学习如何编程 POSIX 互斥体\n*   本章中的*使用 POSIX 信号量*方法来学习如何编程 POSIX 互斥体\n*   *Linux 内核开发*，罗伯特·拉芙\n\n# 同步构造块\n\n根据这个食谱和接下来的两个，我们将回到 C++ 世界。在这个食谱中，我们将学习 C++ 同步构建块。具体来说，我们将结合**资源获取是初始化**(**RAI**)来研究使用`std::lock_guard`和`std::unique_lock`，这是一种面向对象的编程习惯用法，可以使代码更加健壮和可读。`std::lock_guard`和`std::unique_lock`用 RAII 概念将互斥体的 C++ 概念包装在两个类周围。`std::lock_guard`是最简单最小的守卫，而`std::unique_lock`则在它的基础上增加了一些功能。\n\n# 怎么做...\n\n在这个食谱中，我们将开发两个程序来学习如何使用`std::unique_lock`和`std::lock_guard`。让我们开始吧:\n\n1.  从一个 shell 中，创建一个名为`lock_guard.cpp`的新文件。然后，编写`ThreadInfo`结构和`increment`(线程)方法的代码:\n\n```cpp\n#include <iostream>\n#include <mutex>\n#include <thread>\n\nstruct ThreadInfo\n{\n    std::mutex mutex;\n    int counter;\n};\n\nvoid increment(ThreadInfo &info)\n{\n    std::lock_guard<std::mutex> lock(info.mutex);\n    std::cout << \"Thread Started ... \" << std::endl;\n\n    for (int i = 0; i < 100000; ++ i)\n        info.counter++ ;\n\n    std::cout << \"Thread Finished ... \" << std::endl;\n}\n```\n\n2.  现在，为`main`方法编写代码，如下所示:\n\n```cpp\nint main()\n{\n    ThreadInfo thInfo;\n\n    std::thread t1 (increment, std::ref(thInfo));\n    std::thread t2 (increment, std::ref(thInfo));\n\n    t1.join();\n    t2.join();\n\n    std::cout << \"Threads elaboration finished. Counter = \" \n              << thInfo.counter << std::endl;\n    return 0;\n}\n```\n\n3.  让我们为`std::unique_lock`编写相同的程序。从 shell 中，创建一个名为`unique_lock.cpp`的新文件，并为`ThreadInfo`结构和`increment`(线程)方法编写代码:\n\n```cpp\n#include <iostream>\n#include <mutex>\n#include <thread>\nstruct ThreadInfo\n{\n    std::mutex mutex;\n    int counter;\n};\n\nvoid increment(ThreadInfo &info)\n{\n    std::unique_lock<std::mutex> lock(info.mutex);\n    std::cout << \"Thread Started ... \" << std::endl;\n    // This is a test so in a real scenario this is not be needed.\n    // it is to show that the developer here has the possibility to \n    // unlock the mutex manually.\n    // if (info.counter < 0)\n    // {\n    //    lock.unlock();\n    //    return;\n    // }\n    for (int i = 0; i < 100000; ++ i)\n        info.counter++ ;\n    std::cout << \"unique_lock:: Thread Finished ... \" << std::endl;\n}\n```\n\n4.  关于`main`方法，这里与我们在*中看到的使用 POSIX 互斥体*配方没有区别:\n\n```cpp\nint main()\n{\n    ThreadInfo thInfo;\n\n    std::thread t1 (increment, std::ref(thInfo));\n    std::thread t2 (increment, std::ref(thInfo));\n\n    t1.join();\n    t2.join();\n\n    std::cout << \"Unique_lock:: Threads elaboration finished. \n        Counter = \" \n              << thInfo.counter << std::endl;\n    return 0;\n}\n```\n\n这两个程序是我们在*使用 POSIX 互斥体*配方中编写的程序的 C++ 版本。注意代码的简洁。\n\n# 它是如何工作的...\n\n`lock_guard.cpp`程序的*步骤 1* 定义了所需的`ThreadInfo`结构和`increment`方法。首先我们可以看到的是`std::mutex`作为临界区保护机制的使用。`increment`方法现在被简化了，对开发人员来说麻烦更少了。注意，我们有`std::lock_guard<std::mutex> lock(info.mutex);`变量定义。正如我们在方法中看到的，末尾没有`unlock()`调用——这是为什么呢？让我们看看`std::lock_guard`是如何工作的:它的构造函数锁定互斥体。由于`std::lock_guard`是一个类，当对象超出范围时(在这种情况下，在方法的末尾)，析构函数被调用。在`std::lock_guard`析构函数中调用`std::mutex`对象的解锁。这意味着无论`increment`方法发生什么，构造函数都会被调用，因此不存在死锁的风险，开发人员也不必处理`unlock()`。我们在这里描述的是 RAII C++ 技术，它将`info.mutex`对象的生命周期与`lock`变量的生命周期绑定在一起。\n\n*步骤 2* 包含用于管理两个线程的主代码。在这种情况下，C++ 有一个更干净、更简单的接口。用`std::thread t1 (increment, std::ref(thInfo));`创建一个线程。这里，`std::thread`接受两个参数:第一个是线程将调用的方法，而第二个是传递给增量方法的`ThreadInfo`。\n\n`unique_lock.cpp`程序是我们到目前为止描述的`lock_guard`的版本。主要区别在于`std::unique_lock`给了开发者更多的自由。在这种情况下，我们修改了`increment`方法来模拟`if (info.counter < 0)`情况下的互斥解锁需求。使用`std::unique_lock`，我们能够`unlock()`互斥并从方法返回。我们不能在`std::lock_guard`班做同样的事情。当然`lock_guard`无论如何都会在范围的末尾解锁，但是我们这里要强调的是，有了`std::unique_lock`，开发者可以随时手动解锁互斥体。\n\n通过编译`lock_guard.cpp` : `g++ lock_guard.cpp -lpthread`并运行生成的可执行文件，我们得到如下输出:\n\n![](img/bd008ce4-2587-419a-b206-6f98ee10173c.png)\n\n`unique_lock.cpp` : `g++ unique_lock.cpp -lpthread`也是如此，输出如下:\n\n![](img/113fd27c-e7a5-4481-9a8f-49d066070a57.png)\n\n不出所料，两个输出完全相同，优点是使用`lock_guard`的代码从开发人员的角度看起来更干净，肯定更安全。\n\n# 还有更多...\n\n正如我们在这个食谱中看到的，`std::lock_guard`和`std::unique_lock`是我们和`std::mutex` `object.lock_guard`一起使用的模板类。`unique_lock`可以用其他互斥对象来定义，比如 **`std::timed_mutex`** ，这允许我们在特定的时间内获得一个锁:\n\n```cpp\n#include <chrono>\nusing std::chrono::milliseconds;\n\nstd::timed_mutex timedMutex;\nstd::unique_lock<std::timed_mutex> lock {timedMutex, std::defer_lock};\nlock.try_lock_for(milliseconds{5});\n```\n\n`lock`对象将在`5`毫秒内尝试获取锁。我们在添加`std::defer_lock`时要小心，它不会在构造时自动锁定互斥体。这只有在`try_lock_for`成功时才会发生。\n\n# 请参见\n\n以下是您可以参考的参考列表:\n\n*   *Linux 内核开发*，罗伯特·拉芙\n*   本章中的*使用 POSIX 互斥*配方\n*   本章中的*使用 POSIX 信号量*配方\n*   [第二章](02.html)、*重温 C++* ，重温 C++\n\n# 通过简单事件学习线程间通信\n\n到目前为止，我们知道如何使用 POSIX 和 C++ 标准库机制来同步关键部分。有些用例我们不需要显式使用锁；相反，我们可以使用更简单的通信机制。`std::promise`和`std::future`可以用来允许两个线程通信，而没有同步的麻烦。\n\n# 怎么做...\n\n在这个食谱中，我们将编写一个程序，将问题分成两部分:线程 1 将运行高度密集的计算，并将结果发送给线程 2，线程 2 是结果的消费者。我们将通过使用`std::promise`和`std::future`来做到这一点。让我们开始吧:\n\n1.  打开一个名为`promiseFuture.cpp`的新文件，输入以下代码:\n\n```cpp\n#include <iostream>\n#include <future>\n\nstruct Item\n{\n    int age;\n    std::string nameCode;\n    std::string surnameCode;\n};\n\nvoid asyncProducer(std::promise<Item> &prom);\nvoid asyncConsumer(std::future<Item> &fut);\n```\n\n2.  写`main`的方法:\n\n```cpp\nint main()\n{\n    std::promise<Item> prom;\n    std::future<Item> fut = prom.get_future();\n\n    std::async(asyncProducer, std::ref(prom));\n    std::async(asyncConsumer, std::ref(fut));\n\n    return 0;\n}\n```\n\n3.  消费者负责通过`std::future`获取结果并使用:\n\n```cpp\nvoid asyncConsumer(std::future<Item> &fut)\n{\n    std::cout << \"Consumer ... got the result \" << std::endl;\n    Item item = fut.get();\n    std::cout << \"Age = \" << item.age << \" Name = \"\n        << item.nameCode\n              << \" Surname = \" << item.surnameCode << std::endl;\n}\n```\n\n4.  生产者执行一个细化来获取项目并将其发送给等待的消费者:\n\n```cpp\nvoid asyncProducer(std::promise<Item> &prom)\n{\n    std::cout << \"Producer ... computing \" << std::endl;\n\n    Item item;\n    item.age = 35;\n    item.nameCode = \"Jack\";\n    item.surnameCode = \"Sparrow\";\n\n    prom.set_value(item);\n}\n```\n\n这个程序展示了`std::promise`和`std::future`的典型用例，其中一次通信不需要互斥或信号量。\n\n# 它是如何工作的...\n\n在*步骤 1* 中，我们定义了在生产者和消费者之间使用的`struct Item`，并声明了两种方法的原型。\n\n在*步骤 2* 中，我们通过传递定义的承诺和未来，使用`std::async`定义了两个任务。\n\n在*第三步*中，`asyncConsumer`方法用`fut.get()`方法等待细化的结果，这是一个阻塞调用。\n\n在*第 4 步*中，我们实现了`asyncProducer`方法。这个方法很简单——它只是返回一个固定的答案。在真实的场景中，生产者执行高度密集的细化。\n\n这个简单的程序向我们展示了如何简单地将问题从信息的生产者(promise)和消费者(consumer)中分离出来，而不用考虑线程之间的同步。这种使用`std::promise`和`std::future`的解决方案只适用于一次通信类型(也就是说，我们不能在发送和获取项目的两个线程中有循环)。\n\n# 还有更多...\n\n`std::promise`和`std::future`只是 C++ 标准库提供的并发工具。除了`std::future`之外，C++ 标准库还提供了`std::shared_future`。在这个食谱中，我们有一个信息生产者和一个信息消费者，但是如果我们有更多的消费者呢？`std::shared_future`允许多个线程等待相同的信息(来自`std::promise`)。\n\n# 请参见\n\n斯科特·梅耶斯的《有效的现代 C++ 》( T1)和比雅尼·斯特劳斯特鲁普的《T2 的 c++ 编程语言》( T3)这两本书非常详细地涵盖了这些主题。\n\nYou're also invited to read more about concurrency through the C++ Core Guideline in the *CP: Concurrency and parallelism* ([https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#cp-concurrency-and-parallelism](https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#cp-concurrency-and-parallelism)) section.\n\n# 使用条件变量学习线程间通信\n\n在本食谱中，您将了解标准库中另一个允许多线程通信的 C++ 工具。我们将使用`std::condition_variable`和`std::mutex`来开发一个生产者-消费者程序。\n\n# 怎么做...\n\n该配方中的程序将使用`std::mutex`来保护队列免受并发访问，并使用`std::condition_variable`来通知消费者某个项目已被推入队列。让我们开始吧:\n\n1.  打开一个名为`conditionVariable.cpp`的新文件，输入以下代码:\n\n```cpp\n#include <iostream>\n#include <queue>\n#include <condition_variable>\n#include <thread>\n\nstruct Item\n{\n    int age;\n    std::string name;\n    std::string surname;\n};\n\nstd::queue<Item> queue;\nstd::condition_variable cond;\nstd::mutex mut;\n\nvoid producer();\nvoid consumer();\n```\n\n2.  现在，让我们编写`main`方法，它为消费者和生产者创建线程:\n\n```cpp\nint main()\n{\n    std::thread t1 (producer);\n    std::thread t2 (consumer);\n\n    t1.join();\n    t2.join();\n    return 0;\n}\n```\n\n3.  我们来定义一下`consumer`方法:\n\n```cpp\nvoid consumer()\n{\n    std::cout << \"Consumer ... \" << std::endl;\n    while(true)\n    {\n        std::unique_lock<std::mutex> lck{mut};\n        std::cout << \"Consumer ... loop ... START\" << std::endl;\n        cond.wait(lck);\n        // cond.wait(lck, []{ return !queue.empty();});\n        auto item = queue.front();\n        queue.pop();\n        std::cout << \"Age = \" << item.age << \" Name = \" \n                  << item.name << \" Surname = \" << item.surname\n                    << std::endl;\n        std::cout << \"Queue Size = \" << queue.size() << std::endl;\n        std::cout << \"Consumer ... loop ... END\" << std::endl;\n        lck.unlock();\n    }\n}\n```\n\n4.  最后，我们来定义一下`producer`方法:\n\n```cpp\nvoid producer()\n{\n    while(true)\n    {\n        Item item;\n        item.age = 35;\n        item.name = \"Jack\";\n        item.surname = \"Sparrow\";\n        std::lock_guard<std::mutex> lock {mut};\n        std::cout << \"Producer ... loop ... START\" << std::endl;\n        queue.push(item);\n        cond.notify_one();\n        std::cout << \"Producer ... loop ... END\" << std::endl;\n    }\n}\n```\n\n虽然我们开发的程序解决了我们在前面的配方中看到的典型的生产者-消费者问题，但是代码更加地道，易于阅读，并且不容易出错。\n\n# 它是如何工作的...\n\n第一步，我们定义了`struct Item`我们需要从生产者传递到消费者。这一步有趣的点是`std::queue`变量的定义；它使用一个互斥体来同步对队列的访问和`std::condition_variable`来将事件从生产者传递给消费者。\n\n在第二步中，我们定义了生产者线程和消费者线程，并调用了`join()`方法。\n\n在第三步中，消费者方法本质上做了四件事:获取锁以从队列中读取项目，等待带有条件变量`cond`的生产者的通知，从队列中弹出一个项目，然后释放锁。有趣的是，条件变量使用`std::unique_lock`而不是`std::lock_guard`，原因很简单:只要调用条件变量上的`wait()`方法，锁就会(在内部)释放，这样生产者就不会被阻塞。当生产者调用`notify_one`方法时，消费者上的`cond`变量被唤醒并再次锁定互斥体。这允许它安全地从队列中弹出一个项目，并在最后用`lck.unlock()`再次释放锁。紧接在`cond.wait()`(注释掉的代码)之后，还有一种调用`wait()`的替代方法，即传递第二个参数，即谓词，如果第二个参数返回 false，谓词将进一步等待。在我们的例子中，如果队列不是空的，消费者就不会等待。\n\n最后一步非常简单:我们创建一个项目，用互斥体上的`lock_guard`锁定它，并将其推送到队列中。注意，通过使用`std::lock_guard`，我们不需要调用解锁；`lock`变量的析构函数会处理这个问题。在结束当前循环之前，我们需要做的最后一件事是用`notify_one`方法通知消费者。\n\n`g++ conditionVariable.cpp -lpthread`程序的编译和执行将产生以下输出:\n\n![](img/726a088f-c4c0-4c67-8d11-ddca1550ff4b.png)\n\n请注意，由于`condition_variable`是异步的，生产者比消费者快得多，因此需要支付延迟。你可能已经注意到了，生产者和消费者无限运行，所以你必须手动停止这个过程( *Ctrl* + *C* )。\n\n# 还有更多...\n\n在这个食谱中，我们在生产者的`condition_variable`上使用了`notify_one`方法。另一种方法是使用`notify_all`，它会通知所有等待的线程。\n\n需要强调的另一个重要方面是，当生产者想要通知其中一个等待线程计算中发生的事件，以便消费者可以采取行动时，最好使用条件变量。例如，假设生产者通知消费者已经推送了一个特殊项目，或者生产者通知队列管理器队列已满，因此必须产生另一个消费者。\n\n# 请参见\n\n*   [第二章](02.html)、*中的*创建新线程*食谱重温 C++* ，了解更多或刷新自己在 C++ 中的线程。\n*   *比雅尼·斯特劳斯特鲁普的 C++ 编程语言*非常详细地涵盖了这些主题。"
  },
  {
    "path": "docs/cpp-sys-prog-cb/06.md",
    "content": "# 六、管道、先进先出、消息队列和共享内存\n\n进程间的通信是软件系统的重要组成部分，选择合适的通信技术并不是一件简单的事情。开发人员在做出选择时应该记住的一个重要区别是进程是否将在同一台机器上运行。本章重点介绍第一类，您将学习如何基于管道、**先进先出** ( **先进先出**)、消息队列和共享内存开发**进程间通信** ( **IPC** )解决方案。它将首先概述第一个食谱中的四种 IPC 类型、它们的特点以及这些类型之间的差异。然后，每种类型的食谱将提供将它们应用于日常工作所需的实践信息。本章不包含任何特定于 C++ 的解决方案，以便让您熟悉 Linux 本机机制。\n\n本章将涵盖以下主题:\n\n*   学习不同类型的仪表板组合仪表\n*   学习如何使用最古老的 IPC 形式——管道\n*   学习如何使用先进先出\n*   学习如何使用消息队列\n*   学习如何使用共享内存\n\n# 技术要求\n\n为了让您立即尝试这些程序，我们设置了一个 Docker 映像，其中包含了我们在整本书中需要的所有工具和库。这是基于 Ubuntu 19.04 的。\n\n要进行设置，请执行以下步骤:\n\n1.  从[www.docker.com](http://www.docker.com)下载安装 Docker 引擎。\n2.  通过运行以下命令从 Docker Hub 中拉出图像:`docker pull kasperondocker/system_programming_cookbook:latest`。\n3.  图像现在应该可以使用了。输入以下命令查看图像:`docker images`。\n4.  你现在至少应该有这个形象:`kasperondocker/system_programming_cookbook`。\n5.  借助以下命令，使用交互式外壳运行 Docker 映像:`docker run -it --cap-add sys_ptrace kasperondocker/system_programming_cookbook:latest /bin/bash`。\n6.  运行容器上的外壳现已可用。键入`root@39a5a8934370/# cd /BOOK/`以获取所有开发的程序，按章节。\n\n需要`--cap-add sys_ptrace`参数来允许 Docker 容器中的 **GNU 项目调试器** ( **GDB** )设置断点，默认情况下，Docker 不允许设置断点。\n\n**Disclaimer**: The C++ 20 standard has been approved (that is, technically finalized) by WG21 in a meeting in Prague at the end of February. This means that the GCC compiler version that this book uses, 8.3.0, does not include (or has very, very limited support for) the new and cool C++ 20 features. For this reason, the Docker image does not include the C++ 20 recipe code. GCC keeps the development of the newest features in branches (you have to use appropriate flags for that, for example, `-std=c++ 2a`); therefore, you are encouraged to experiment with them by yourself. So, clone and explore the GCC contracts and module branches and have fun.\n\n# 学习不同类型的仪表板组合仪表\n\n该配方的目标是为在同一台机器上运行的进程通常使用的不同 IPC 解决方案提供指导。它将从开发人员的角度(您的角度)概述主要特征。)，解释它们之间的不同之处。\n\n# 怎么做...\n\n下表显示了 Linux 机器上始终可用的四种 IPC 类型，其中各列代表我们认为开发人员在做出设计选择时应该考虑的独特因素:\n\n|  | ****流程【r】******需要得意洋洋吗？** | **需要同步吗？** | **通讯类型** | **范围** | **仁涉？** |\n| **管道** | 是 | 一般不会 | 半双工 | 同一台机器 | 是 |\n| **先进先出** | 不 | 一般不会 | 半双工 | 通常是同一台机器 | 是 |\n| **讯息伫列** | 不 | 一般不会 | 半双工 | 同一台机器 | 是 |\n| **共享内存** | 不 | 是 | 半双工 | 同一台机器 | 是 |\n\n该表的列有以下描述:\n\n*   **需要流程的关系？**:表示是否需要流程之间的关系(例如父子关系)来实现特定的 IPC。\n*   **需要同步吗？**:这表示你是否需要考虑进程之间任何形式的同步(例如互斥、信号量等等；参见[第 5 章](05.html)、*是否使用互斥体、信号量和条件变量*。\n*   **通信类型**:两个或两个以上实体之间的通信可以是半双工(最接近的类比是对讲机，在对讲机中，在任何给定时间只有一个人可以说话)或全双工(例如电话，由此两个人可以同时说话)。这将对设计的解决方案产生深远的影响。\n*   **范围**:这表示解决方案是否可以应用到更广的范围，就不同机器上的进程之间的 IPC 而言。\n*   **仁涉？**:这警告你关于内核参与通信过程。*它是如何工作的...*部分将解释为什么这很重要。\n\n在下一节中，我们将逐行分析表中突出显示的单个特征。\n\n# 它是如何工作的...\n\n列表中的第一个 IPC 机制是一个**管道**。管道需要两个进程(例如，父子进程)之间的关系才能工作。这种关系是必要的，以便通过两个过程(与先进先出相反)使管道**可见**。它就像一个变量，必须通过一个方法可见才能使用。在管道配方中，我们将看到这在技术上是如何工作的。\n\n通信类型为半双工:数据从进程 *A* 流向进程 *B、*，因此不需要同步。为了在两个进程之间实现全双工通信类型，必须使用两个管道。出于两个进程必须有关系才能使用管道的相同原因，管道不能用作两个不同机器上的进程之间的通信机制。Linux 内核参与了通信，因为数据被复制到内核，然后被进一步复制到接收进程。\n\n表格中的第二个 IPC 机制是**先进先出**(或**命名管道**)。它是一个命名管道，因为它需要创建一个路径名，实际上，它是一种特殊的文件。这使得先进先出可以被任何进程使用，即使它们之间没有关系。他们所需要的只是所有进程将使用的先进先出路径(同样，一个文件名)。这种情况下也不需要同步。但是，我们必须小心，因为有些情况下需要同步，正如`man page`所规定的。\n\nPOSIX.1 says that writes ([http://man7.org/linux/man-pages/man2/write.2.html](http://man7.org/linux/man-pages/man2/write.2.html)) of less than `pipe_BUF` bytes must be atomic (that is, the output data is written to the pipe as a contiguous sequence). Writes of more than `pipe_BUF` bytes may be nonatomic (that is, the kernel may interleave the data with data written by other processes). POSIX.1 requires `pipe_BUF` to be at least 512 bytes. (On Linux, `pipe_BUF` is 4,096 bytes.) The precise semantics depends on whether the file descriptor is nonblocking (`O_NONBLOCK`); whether there are multiple writers to the pipe; and on *n*, the number of bytes to be written.\n\n一般规则是，如果您对进程之间应该进行多少数据交换有任何疑问，请始终提供同步机制(例如，互斥、信号量和许多其他机制)。先进先出(同样，管道)提供半双工通信机制，除非为每个进程提供两个先进先出(每个进程一个读取器和一个写入器)；在这种情况下，它将成为全双工通信。先进先出通常用于同一台机器上进程之间的 IPC，但是，由于它是基于文件的，如果文件被其他机器看到，先进先出可能用于不同机器上进程之间的 IPC。即使在这种情况下，内核也参与到 IPC 中，数据从内核空间复制到进程的用户空间。\n\n一个**消息队列**是一个存储在内核中的消息链表。该定义已经包含一条信息；这是内核提供的一种通信机制，同样，这意味着数据在内核之间来回复制。消息队列不需要进程之间的任何关系；他们必须共享一个密钥才能访问同一个队列。如果消息小于或等于`pipe_BUF`，Linux 内核保证队列上操作的原子性。在这种情况下，需要同步机制。消息队列不能在计算机范围之外使用。\n\n表中最后一个 IPC 机制是**共享内存**。这是最快的 IPC 形式。这是有代价的，因为使用共享内存的进程应该使用某种形式的同步(例如互斥或信号量)，正如`man page`所建议的那样(`man shm_overview`)。\n\nAny time there is a critical section to protect, processes must synchronize the access using a mechanism we've seen in [Chapter 5](05.html), *Using Mutexes, Semaphores, and Condition Variables.*\n\n进程必须在同一台机器上运行，才能使用相同的共享内存，并且它用一个键来标识，消息队列也是如此。由于共享内存驻留在内核空间中，数据从内核空间复制到读取和删除它的进程。\n\n# 还有更多...\n\n这四种形式的 IPC 最初是在 Unix System V 上开发的，然后在 Linux 支持的更现代的 POSIX 标准中重新实现。有些情况下，进程不在同一台机器上，在这些情况下，我们需要使用其他机制，比如套接字，我们将在下一章中看到。当然，套接字在通信过程中具有更广泛的适用性，而与它在网络中的位置无关。\n\n可以说，这种普遍性是有代价的:它们比这个食谱中描述的机制要慢。因此，作为开发人员，这是在做出设计选择时必须考虑的一个因素。\n\n# 请参见\n\n*   [第 5 章](05.html) *，使用互斥体、信号量和条件变量*:关于可以使用的同步机制。\n*   [第 7 章](07.html) *，网络编程*:用套接字的概念(面向连接和无连接)来补充这一章。\n\n# 学习如何使用最古老的 IPC 形式——管道\n\n在上一个食谱中，你学会了如何根据一些关键因素来选择 IPC。现在是动手操作这四种通信类型的时候了，这个食谱的重点是管道。在本食谱中，您将学习如何使用管道使两个进程通过使用两个管道进行全双工通信。我们通常不会使用任何形式的同步，因为这不是必需的。在*它是如何工作的...*部分，我们来看看为什么不需要，什么时候不需要。\n\n# 怎么做...\n\n在本节中，我们将开发一个程序，该程序将创建两个进程，其独特的目标是相互发送消息。如我们所见，使用管道，数据向一个方向流动。为了进行双向通信，并模拟一般情况，我们将使用两个管道:\n\n1.  我们实例化了要发送的两条消息，以及它们的大小，稍后我们将需要它们:\n\n```cpp\n#include <stdio.h>\n#include <unistd.h>\n#include <string.h>\n#include <sys/types.h>\n#include <sys/wait.h>\n\nchar* msg1 = \"Message sent from Child to Parent\";\nchar* msg2 = \"Message sent from Parent to Child\";\n#define MSGSIZE 34\n#define IN      0\n#define OUT 1\n```\n\n2.  接下来，我们进入初始化部分。我们需要为收到的消息实例化空间，包括`childToParent`和`parentToChild`管道，以及我们用来跟踪孩子的**进程标识符** ( **PID** ):\n\n```cpp\nint main()\n{\n    char inbufToParent[MSGSIZE];\n    char inbufToChild[MSGSIZE];\n    int childToParent[2], parentToChild[2], pid, nbytes;\n\n    inbufToParent[0] = 0;\n    inbufToChild[0] = 0;\n    if (pipe(childToParent) < 0)\n        return 1;\n\n    if (pipe(parentToChild) < 0)\n        return 1;\n```\n\n3.  现在，让我们看看儿童部分。这个部分有两部分:第一部分，孩子向家长发送`msg1`消息；第二种，孩子从父母那里收到`msg2`信息:\n\n```cpp\nif ((pid = fork()) > 0)\n{\n        printf(\"Created child with PID = %d\\n\", pid);\n        close(childToParent[IN]);\n        write(childToParent[OUT], msg1, strlen(msg1));\n        close(childToParent[OUT]);\n\n        close (parentToChild[OUT]);\n\n        read(parentToChild[IN], inbufToChild, strlen(msg2));\n        printf(\"%s\\n\", inbufToChild);\n        close (parentToChild[IN]);\n        wait(NULL);\n}\n```\n\n4.  最后，让我们看看父代码。它有两个部分:一部分接收来自孩子的信息，第二部分回复信息:\n\n```cpp\nelse\n{\n        close (childToParent[OUT]);\n        read(childToParent[IN], inbufToParent, strlen(msg1));\n        printf(\"%s\\n\", inbufToParent);\n        close (childToParent[IN]);\n\n        close (parentToChild[IN]);\n        write(parentToChild[OUT], msg2, strlen(msg2));\n        close (parentToChild[OUT]);\n}\nreturn 0;\n```\n\n我们已经通过编程实现了我们在[第 1 章](01.html)、*系统编程入门*中所学的 shell(参见*学习 Linux 基础知识–shell*食谱)。下一节将详细介绍这些步骤。\n\n# 它是如何工作的...\n\n在第一步中，我们只是定义了两个进程要使用的`msg1`和`msg2`，并定义了读取它们所需的消息长度`MSGSIZE`。\n\n第二步本质上将两个管道`childToParent`和`parentToChild`定义为两个整数的数组。它们被`pipe`系统调用用来创建两个通信缓冲区，进程可以通过`childToParent[0]`和`childToParent[1]`文件描述符访问这两个缓冲区。该消息被写入`childToParent[1]`并用先进先出策略从`childToParent[0]`读取。为了避免缓冲器未初始化的情况，该步骤将`inbuf1`和`inbuf2`的指针设置为`0`。\n\n第三步处理孩子的代码。它写入`childToParent[1]`，然后从`parentToChild[0]`读取。子进程对`childToParentp[1]`的写入可以由父进程在`childToParent[0]`上读取。`read`和`write`系统调用使进程进入内核模式，并将输入数据暂时保存在内核空间，直到第二个进程读取它。要遵循的一条规则是，管道的未使用端必须关闭。在我们的情况下，我们写信给`childToParent[1]`；所以，我们关闭管道的`read`端，`childToParent[0]`，一旦读取，我们关闭`write`端，因为这是不使用的。\n\n第四步，非常类似于第三步，有子代码的对称代码。它在`childToParent[0]`管道上读取并在`parentToChild[1]`上写入，遵循关闭未使用的管道末端的相同规则。\n\n从分析的代码来看，管道不能被非祖先进程使用的原因现在应该很清楚了:`childToParent`和`parentToChild`文件描述符在运行时必须对父进程和子进程可见。\n\n如果我们在 Docker 容器的`/BOOK/Chapter06/`文件夹中用`gcc pipe.c`编译代码并运行它，输出如下:\n\n![](img/91aaa497-4f77-4015-8784-f09f8d31dffd.png)\n\n这表明父节点和子节点正确地发送和接收了两条消息。\n\n# 还有更多...\n\n对于绝大多数用例，管道旨在用于少量数据，但是可能有需要大量数据的场景。我们在本章中遵循的标准 POSIX 规定，小于`pipe_BUF`字节的`write`必须是原子的。它进一步规定`pipe_BUF`必须至少是 512 字节(在 Linux 上，是 4kb)；否则，您必须通过使用信号量和互斥体等机制来处理用户级的同步。\n\n# 请参见\n\n*   [第 1 章](01.html)、*系统编程入门*，从 shell 角度展示了管道概念。\n*   [第 5 章](05.html)、*使用互斥体、信号量和条件变量*有必要的工具来添加同步，以防发送和接收的数据大于`pipe_BUF`。\n\n# 学习如何使用先进先出\n\n我们在前面的配方中看到的管道是暂时的，也就是说，当没有过程打开它们时，它们就不复存在了。 **FIFOs** (也叫**命名管道**)不同；它们是作为文件系统上的特殊文件存在的特殊管道。原则上，任何进程，假设它有正确的权限，都可以访问先进先出。最后一个是先进先出的特点。使用文件允许我们编写一个更通用的通信机制来将进程置于通信中，即使没有祖先关系；或者，换句话说，我们可以使用 FIFO 来获取任意两个文件进行通信。在这个食谱中，你将学习如何编程先进先出。\n\n# 怎么做...\n\n在本节中，我们将开发一个基于 FIFOs 的非常原始的聊天程序，从而产生两个不同的程序，在运行时允许两个用户聊天:\n\n1.  让我们创建一个名为`fifo_chat_user1.c`的文件，并添加我们稍后需要的包含，并且`MAX_LENGTH`定义确定两个用户可以交换的消息的最大长度:\n\n```cpp\n#include <stdio.h>\n#include <string.h>\n#include <fcntl.h>\n#include <sys/stat.h>\n#include <unistd.h>\n\n#define MAX_LENGTH 128\n```\n\n2.  接下来，从`main`开始。这里需要定义`fd`文件描述符来打开文件；我们打算存储文件的路径；我们将用来存储`msgReceived`和`msgToSend`消息的两个字符串；最后，`mkfifo`系统调用以定义的路径创建先进先出:\n\n```cpp\nint main()\n{\n    char* fifoChat = \"/tmp/chat\";\n    mkfifo(fifoChat, 0600);\n\n    char msgReceived[MAX_LENGTH], msgToSend[MAX_LENGTH];\n```\n\n3.  我们现在需要一个无限循环来不断地`write`和`read`。我们通过创建两个部分来做到这一点:在`write`部分，我们以写模式打开`fifoChat`文件，用`fgets`从用户那里获得消息，并将`msgToSend`写入文件，用`fd`文件描述符表示。在阅读器部分，我们以读取模式打开文件，用`read`方法读取文件内容，打印输出，关闭`fd`:\n\n```cpp\n    while (1)\n    {\n        int fdUser1 = open(fifoChat, O_WRONLY);\n        printf(\"User1: \");\n        fgets(msgToSend, MAX_LENGTH, stdin);\n        write(fdUser1, msgToSend, strlen(msgToSend)+1);\n        close(fdUser1);\n\n        int fdUser2 = open(fifoChat, O_RDONLY);\n        read(fdUser2, msgReceived, sizeof(msgReceived));\n        printf(\"User2: %s\\n\", msgReceived);\n        close(fdUser2);\n    }\n    return 0;\n}\n```\n\n4.  第二个程序非常相似。唯一不同的是`while`循环，反之亦然。在这里，我们有`read`部分，然后是`write`部分。您可以将`fifo_chat_user1.c`文件复制到`fifo_chat_user2.c`中进行修改，如下所示:\n\n```cpp\nwhile (1)\n{\n        int fdUser2 = open(myfifo, O_RDONLY);\n        read(fdUser2, msgReceived, sizeof(msgReceived));\n        printf(\"User1: %s\\n\", msgReceived);\n        close(fdUser2);\n\n        int fdUser1 = open(myfifo, O_WRONLY);\n        printf(\"User2: \");\n        fgets(msgToSend, MAX_LENGTH, stdin);\n        write(fdUser1, msgToSend, strlen(msgToSend)+1);\n        close(fdUser1);\n}\n```\n\n虽然这不是你能找到的最具互动性的聊天方式，但尝试先进先出绝对有用。在下一节中，我们将分析本节中看到的步骤。\n\n# 它是如何工作的...\n\n让我们首先编译并运行这两个程序。在这种情况下，我们希望给可执行文件一个不同的名称，以便区分它们:\n\n```cpp\ngcc fifo_chat_user1.c -o chatUser1\n\ngcc fifo_chat_user2.c -o chatUser2\n```\n\n这会创建两个可执行文件:`chatUser1`和`chatUser2`。让我们在两个独立的终端上运行它们，然后聊天:\n\n![](img/7359f0c3-5c8e-4a74-95fa-74763e739dbf.png)\n\n在*步骤 1* 中，我们本质上定义了`MAX_LENGTH`到`128`字节，并添加了我们需要的定义。\n\n在*步骤 2* 中，我们在`fifoChat`指定的路径上创建了`mkfifo` FIFO，指向`/tmp/chat`文件，权限为`6`(用户读写)、`0`(用户所属组不读、不写、不执行)和`0`(用户不读、不写、不执行)。一旦调用`mkfifo`就可以检查这些设置:\n\n```cpp\nroot@d73a2ef8d899:/BOOK/chapter6# ls -latr /tmp/chat\nprw------- 1 root root 0 Oct 1 23:40 /tmp/chat\n```\n\n在*步骤 3* 中，我们用`open`方法打开了 FIFO。值得一提的是`open`是打开普通文件的同一个方法，在返回的描述符上，我们可以调用`read`和`write`，就像我们在普通文件上做的一样。在这一步中，我们做了一个无限循环，让用户想聊多久就聊多久。如您所见，`read`和`write`部分在*步骤 4* 中互换，如果第一个用户正在写，则允许第二个用户读取，反之亦然。\n\n先进先出由内核通过先进先出策略进行内部管理。每次我们`write`或`read`数据从/到 FIFO，数据就从/传到内核。你应该记住这一点。当`chat2`程序调用`read`方法时，消息从`chat1`可执行文件传递到内核空间，然后再返回到用户空间。\n\n# 还有更多...\n\n到目前为止应该很清楚，先进先出是一种特殊的管道。这意味着我们对管道的限制同样适用于先进先出。例如，除非发送的数据量超过`pipe_BUF`限制，标准 POSIX 定义为 512 字节，Linux 设置为 4 KB，否则不需要同步。\n\n另一个需要强调的方面是，命名管道(FIFO)可以用于 *N* 到 *M* 的通信类型(即多个读取器和多个写入器)。如果满足前面的条件，内核保证操作(`read`和`write`调用)的原子性。\n\n# 请参见\n\n*   [第三章](03.html)，*处理流程和线程*\n*   [第 5 章](05.html)、*使用互斥体、信号量和条件变量*\n\n# 学习如何使用消息队列\n\n另一个由符合 POSIX 的操作系统(以及 Linux 内核)直接支持的机制是消息队列。消息队列本质上是存储在内核中的消息的链表，其中每个队列都由一个标识来标识。在这个食谱中，我们将使用一个消息队列重写聊天程序，强调主要的优点和缺点。\n\n# 怎么做...\n\n在这一部分，我们将从*学习如何使用先进先出*食谱改写聊天程序。这将允许您亲自查看先进先出和消息队列之间的异同:\n\n1.  创建一个名为`mq_chat_user_1.c`的新文件，并添加以下包含和定义:\n\n```cpp\n#include <stdio.h>\n#include <string.h>\n#include <mqueue.h>\n\n#define MAX_MESSAGES 10\n#define MAX_MSG_SIZE 256\n```\n\n2.  在`main`方法中，现在让我们定义两个消息队列描述符(`user1Desc`和`user2Desc`)来存储后面`mq_open`方法的结果。我们必须定义并初始化`mq_attr`结构来存储我们将要创建的消息队列的配置:\n\n```cpp\nint main()\n{\n    mqd_t user1Desc, user2Desc;\n    char message[MAX_MSG_SIZE];\n    char message2[MAX_MSG_SIZE];\n\n    struct mq_attr attr;\n    attr.mq_flags = 0;\n    attr.mq_maxmsg = MAX_MESSAGES;\n    attr.mq_msgsize = MAX_MSG_SIZE;\n    attr.mq_curmsgs = 0;\n```\n\n3.  我们可以打开两个`/user1`和`/user2`消息队列:\n\n```cpp\n    if ((user1Desc = mq_open (\"/user1\", O_WRONLY | O_CREAT,\n         \"0660\", &attr)) == -1)\n    {\n        perror (\"User1: mq_open error\");\n        return (1);\n     }\n     if ((user2Desc = mq_open (\"/user2\", O_RDONLY | O_CREAT,\n         \"0660\", &attr)) == -1)\n     {\n         perror (\"User2: mq_open error\");\n         return (1);\n     }\n```\n\n4.  程序的中心部分是发送和接收来自两个用户的消息的循环。为此，我们必须:\n    1.  使用`user1Desc`消息队列描述符，通过`mq_send`方法向用户 2 发送消息。\n    2.  使用`user2Desc`消息队列描述符接收用户 2 通过`mq_receive`发送给我们的最终消息:\n\n```cpp\n    while (1)\n    {\n        printf(\"USER 1: \");\n        fgets(message, MAX_MSG_SIZE, stdin);\n        if (mq_send (user1Desc, message, strlen (message)\n            + 1, 0) == -1)\n        {\n            perror (\"Not able to send message to User 2\");\n            continue;\n        }\n        if (mq_receive (user2Desc, message2, MAX_MSG_SIZE,\n             NULL) == -1)\n        {\n            perror (\"tried to receive a message from User 2\n                but I've failed!\");\n            continue;\n        }\n        printf(\"USER 2: %s\\n\", message2);\n    }\n    return 0;\n}\n```\n\n5.  我们需要另一个程序来回复用户 1。这个程序很相似；唯一不同的是，它在`user2Desc`上发送消息(这次在写入模式下打开)并从`user1Desc`读取(在读取模式下打开)。\n\n让我们现在运行程序。我们需要在 shell 中键入以下两个命令来编译`mq_chat_user_1.c`和`mq_chat_user_2.c`程序:\n\n```cpp\ngcc mq_chat_user_1.c -o user1 -g -lrt\ngcc mq_chat_user_2.c -o user2 -g -lrt\n```\n\n我们正在编译和链接程序，并生成`user1`和`user2`可执行文件。我们添加了`-lrt`(这是 POSIX.1b 实时扩展库)，因为我们需要包含 POSIX 消息队列实现。请记住，对于`-l`，您要求编译器为链接器阶段考虑一个特定的库。在下一节中，我们将看到输出，并分析之前看到的所有步骤。\n\n# 它是如何工作的...\n\n通过运行`./user1`和`./user2`可执行文件，我们将得到以下输出:\n\n![](img/5838d556-e1d8-4538-a817-e7b1fcbe6004.png)\n\n让我们看看以下步骤:\n\n1.  **第一步**:我们需要`#include <stdio.h>`进行用户输入输出，`#include <string.h>`通过`strlen`获取字符串长度，`#include <mqueue.h>`可以访问消息队列接口。在这一步中，我们定义了队列中消息的最大数量(`10`)和队列中消息的最大大小(`256`字节)。\n2.  **第二步**:在程序的`main`方法中，我们定义了两个消息队列描述符(`user1Desc`和`user2Desc`)来保持对消息队列的引用；两个消息数组(`message`和`message2`)存储两个用户之间发送和接收的消息；最后，我们定义并初始化了`struct mq_attr`结构，用于初始化我们将在下一步中使用的消息队列。\n3.  **第三步**:这一步，我们打开了两个消息队列。这是`/user1`和`/user2`，它们位于`/dev/mqueue`:\n\n```cpp\nroot@1f5b72ed6e7f:/BOOK/chapter6# ll /dev/mqueue/user*\n------x--- 1 root root 80 Oct 7 13:11 /dev/mqueue/user1*\n------x--- 1 root root 80 Oct 7 13:11 /dev/mqueue/user2*\n```\n\n`mq_chat_user_1.c`以只写模式打开`/user1`消息队列，如果不存在，则创建该队列。它还以只读模式打开`/user2`，如果不存在，则创建它。应该清楚的是，如果当前进程没有消息队列(我们用`660`打开)的访问权限，`mq_open`将失败。\n\n4.  **第四步**:这一步包含了我们程序的主要逻辑。它有一个无限循环，从用户 1 向用户 2 发送消息，从用户 2 向用户 1 接收消息。用来发送消息的方法是`mq_send`。它需要消息队列描述符、要发送的消息、其长度(`+1`，因为我们需要包含终止符)和消息优先级(在本例中我们没有使用)。`mq_send`(更多信息参见`man mq_send`)如果队列中没有空间，则阻塞直到有足够的空间可用。\n\n发送后，我们调用`mq_receive`方法(更多信息参见`man mq_receive`)从用户 2 处获取最终消息。它需要消息队列描述符，一个包含消息的数组，我们可以接收的最大大小，以及优先级。请记住，如果队列中没有消息，则`mq_receive`会阻止。\n\nFor more info, see the `man mq_receive` page.\n\n由于发送和接收是核心概念，让我们用一个模式来更深入地分析它们:\n\n![](img/6f49dbd8-e83c-495b-b7b7-fb2546928205.png)\n\n**(1)** 在这种情况下，用户 1 进程调用`mq_send`。Linux 内核复制了一份从用户空间发送到内核空间的消息。 **(3)** 的情况也是如此。\n\n**(2)** 当用户 2 进程在同一个消息队列(`user1Desc`)上调用`mq_receive`时，Linux 内核将消息从内核空间复制到用户空间，复制`message2`缓冲区中的数据。 **(4)** 的情况也是如此。\n\n# 还有更多...\n\n有些情况下，您可能需要根据优先级从队列中获取消息，但我们在本例中没有使用。你能修改这个食谱的程序以包括优先权吗？你有什么要修改的？\n\n你可能已经注意到我们在这个食谱中使用了`perror`方法。`perror`方法在标准输出中打印最后一个错误(`errno`)，该错误以描述性格式出现。对开发人员的好处是，您不必显式获取`errno`值并将其转换为字符串；这是自动为你完成的。\n\n我们为管道和先进先出描述的原子性概念同样适用于消息队列。如果消息小于`pipe_BUF`，则保证消息的传递是原子的。否则，开发人员必须提供同步机制。\n\n# 请参见\n\n[第 3 章](03.html)、*处理进程和线程*(关于线程)和[第 5 章](05.html)、*使用互斥体、信号量和条件变量*(关于同步)中的食谱。像往常一样，`man pages`提供了一个很好的信息来源，建议的起点是`man  mq_overview`。\n\n# 学习如何使用共享内存\n\n正如我们所知，在我们目前看到的所有 IPC 机制中，内核在进程之间的通信中扮演着积极的角色。信息确实从 Linux 内核流向进程，反之亦然。在这个食谱中，我们将学习最快的进程间通信形式，它不需要内核作为进程间的中介。像往常一样，尽管 System V APIs 广泛可用，但我们将使用最新、更简单、设计更好的 POSIX APIs。我们将使用共享内存重写我们的聊天应用，更详细地挖掘它。\n\n# 怎么做...\n\n在这一节中，我们将着重于使用 POSIX 共享内存 API 开发一个简单的聊天应用。由于内核不(直接)参与通信过程，我们需要提供一种同步机制来保护关键部分(共享内存)免受两个进程的读写影响:\n\n1.  让我们从添加我们需要的包含和定义开始。我们将有两个共享内存空间(`STORAGE_ID1`和`STORAGE_ID2`)来实现进程之间的双向通信:\n\n```cpp\n#include <stdio.h>\n#include <sys/mman.h>\n#include <fcntl.h>\n#include <unistd.h>\n#include <string.h>\n\n#define STORAGE_ID1 \"/SHM_USER1\"\n#define STORAGE_ID2 \"/SHM_USER2\"\n#define STORAGE_SIZE 32\n```\n\n2.  在`main`方法中，我们需要两个数组来存储发送和接收的消息。此外，我们需要打开两个具有以下标志的共享内存空间:读写模式，如果不存在则创建，以及分别指示文件所有者的读写权限的标志`(S_IRUSR`和`S_IWUSR`:\n\n```cpp\nint main(int argc, char *argv[])\n{\n    char message1[STORAGE_SIZE];\n    char message2[STORAGE_SIZE];\n\n    int fd1 = shm_open(STORAGE_ID1, O_RDWR | O_CREAT, S_IRUSR | \n        S_IWUSR);\n    int fd2 = shm_open(STORAGE_ID2, O_RDWR | O_CREAT, S_IRUSR | \n        S_IWUSR);\n    if ((fd1 == -1) || (fd2 == -1))\n    {\n        perror(\"open\");\n        return 10;\n    }\n```\n\n3.  由于共享内存基于`mmap`(我们本质上是将一个文件映射到内存的一部分)，我们需要将文件描述符 1 ( `fd1`)指向的文件扩展到我们需要的大小`STORAGE_SIZE`。然后，我们需要将两个文件描述符映射到共享模式(`MAP_SHARED`)下的一部分内存，当然，还要检查错误:\n\n```cpp\n    // extend shared memory object as by default it's initialized \n    //  with size 0\n    int res1 = ftruncate(fd1, STORAGE_SIZE);\n    if (res1 == -1)\n    {\n        perror(\"ftruncate\");\n        return 20;\n    }\n\n    // map shared memory to process address space\n    void *addr1 = mmap(NULL, STORAGE_SIZE, PROT_WRITE, MAP_SHARED, \n        fd1, 0);\n    void *addr2 = mmap(NULL, STORAGE_SIZE, PROT_WRITE, MAP_SHARED, \n        fd2, 0);\n    if ((addr1 == MAP_FAILED) || (addr2 == MAP_FAILED))\n    {\n        perror(\"mmap\");\n        return 30;\n    }\n```\n\n4.  在`main`循环中，与前面两个食谱一样，我们在两个共享内存实例中使用`read`和`write`:\n\n```cpp\n    while (1)\n    {\n        printf(\"USER 1: \");\n        fgets(message1, STORAGE_SIZE, stdin);\n        int len = strlen(message1) + 1;\n        memcpy(addr1, message1, len);\n\n        printf(\"USER 2 (enter to get the message):\"); getchar();\n        memcpy(message2, addr2, STORAGE_SIZE);\n        printf(\"%s\\n\", message2);\n    }\n\n    return 0;\n}\n```\n\n5.  第二个程序反映了这个。你可以在`/BOOK/Chapter06`文件夹中找到他们两个:`shm_chat_user1.c`(我们描述的那个)和`shm_chat_user2.c`。\n\n让我们通过在外壳上键入以下两个命令来编译和链接两个`shm_chat_user1.c`和`shm_chat_user2.c`程序:\n\n```cpp\ngcc shm_chat_user1.c -o user1 -g -lrt\ngcc shm_chat_user2.c -o user2 -g -lrt\n```\n\n输出将是两个二进制文件:`user1`和`user2`。在这种情况下，我们也添加了`-lrt`，因为我们需要包含 POSIX 共享内存实现(没有它，链接阶段将抛出`undefined reference to 'shm_open'`错误)。在下一节中，我们将分析本节中看到的所有步骤。\n\n# 它是如何工作的...\n\n运行`./user1`和`./user2`程序会产生以下交互:\n\n![](img/4d2095df-4516-4651-bdfa-36f932343e57.png)\n\n让我们执行以下步骤:\n\n*   **第一步**:第一步只是包括我们需要的几个表头:`stdio.h`为标准输入/输出(例如:`perror`、`printf`等)；`mman.h`为共享内存 APIs`mmap`和`fcntl.h`为`shm_open`旗帜(例如，`O_CREAT`、`O_RDWR`等)；`unistd.h`为`ftruncate`法；`string.h`为`strlen`和`memcpy`方法。\n\n我们定义了`STORAGE_ID1`和`STORAGE_ID2`来标识两个共享内存对象，它们将在`/dev/shm`文件夹中可用:\n\n```cpp\nroot@1f5b72ed6e7f:/BOOK/chapter6# ll /dev/shm/SHM_USER*\n-rw------- 1 root root 32 Oct 7 23:26 /dev/shm/SHM_USER1\n-rw------- 1 root root 0 Oct 7 23:26 /dev/shm/SHM_USER2\n```\n\n*   **第二步**:在这一步中，我们在堆栈上为两个消息(`message1`和`message2`)分配了空间，我们将使用这两个消息在进程之间发送和接收消息。然后，我们创建并打开两个新的共享内存对象，并检查是否有任何错误。\n*   **步骤 3** :一旦两个共享内存对象可用，我们就需要扩展这两个文件(通过两个文件描述符`fd1`和`fd2`，每个程序一个文件描述符)，并且——非常重要——将`fd1`和`fd2`映射到当前进程的虚拟地址空间。\n*   **第四步**:这一步是程序的中心部分。这里有几件有趣的事情需要注意。首先，我们可以看到，用户空间和内核空间之间的数据移动不像 FIFOs、管道和消息队列那样。我们只是在本地缓冲区(在堆栈上分配)和我们映射的内存之间进行内存复制，反之亦然。第二个因素是，由于我们只是处理内存复制，性能将优于其他 IPC 机制。\n\n这一步的机制很简单:我们要求用户键入一条消息并将其存储在`message1`缓冲区中，然后用`addr1`将该缓冲区复制到内存映射地址。read 部分(我们从第二个用户那里读取消息)也很简单:我们将消息从内存复制到本地缓冲区`message2`。\n\n# 还有更多...\n\n如您所见，在这个配方中，两个过程之间没有同步。那是为了让你只关注一个方面:共享记忆的交流。再次邀请读者改进这段代码，通过使用线程使其更具交互性，并通过使用同步机制使其更安全。\n\n从内核 2.6.19 开始，Linux 支持使用**访问控制列表** ( **访问控制列表**)来控制虚拟文件系统中对象的权限。更多信息见`man acl`。\n\n# 请参见\n\n关于线程和同步的方法:\n\n*   [第三章](03.html)，*处理流程和线程*\n*   [第 5 章](05.html)、*使用互斥体、信号量和条件变量*"
  },
  {
    "path": "docs/cpp-sys-prog-cb/07.md",
    "content": "# 七、网络编程\n\n在[第 6 章](06.html)、*管道、先进先出(FIFO)、消息队列和共享内存*中，我们学习了不同的 IPC 技术，以允许在同一台机器上运行的进程相互通信。在本章中(补充了[第 6 章](06.html)、*管道、先进先出(FIFO)、消息队列和共享内存*中的内容)，您将了解在两台不同的计算机上运行的两个进程如何实现相同的结果。这里介绍的主题是当今互联网如何工作的基础。你将通过实践学习面向连接和面向无连接通信的区别，定义端点的特征，最后两个食谱将教你如何使用 TCP/IP 和 UDP/IP。\n\n本章将涵盖以下主题:\n\n*   学习面向连接的通信基础知识\n*   学习面向无连接通信的基础知识\n*   了解什么是通信端点\n*   学习使用 TCP/IP 与另一台机器上的进程通信\n*   学习使用 UDP/IP 与另一台机器上的进程通信\n*   处理字符顺序\n\n# 技术要求\n\n为了让您立即开始使用这些程序，我们设置了一个 Docker 映像，其中包含了我们在整本书中需要的所有工具和库。它基于 Ubuntu 19.04。\n\n要进行设置，请执行以下步骤:\n\n1.  从[www.docker.com](https://www.docker.com/)下载安装 Docker 引擎。\n2.  使用`docker pull kasperondocker/system_programming_cookbook:latest`从 Docker Hub 中拉出图像。\n3.  图像现在应该可以使用了。输入`docker images`查看图像。\n4.  你现在至少应该有`kasperondocker/system_programming_cookbook`了。\n5.  使用`docker run -it --cap-add sys_ptrace kasperondocker/system_programming_cookbook:latest /bin/bash`运行带有交互式外壳的 Docker 图像。\n6.  运行容器上的外壳现已可用。使用`root@39a5a8934370/# cd /BOOK/`获取所有程序，按章节列出。\n\n需要`--cap-add sys_ptrace`参数来允许 Docker 容器中的 **GNU 项目调试器** ( **GDB** )设置断点，默认情况下 Docker 是不允许的。要在同一容器上启动第二个外壳，运行`docker exec -it container-name bash`命令。您可以从`docker ps`命令中获取容器名称。\n\n**Disclaimer**: The C++ 20 standard has been approved (that is, technically finalized) by WG21 in a meeting in Prague at the end of February. This means that the GCC compiler version that this book uses, 8.3.0, does not include (or has very, very limited support for) the new and cool C++ 20 features. For this reason, the Docker image does not include the C++ 20 recipe code. GCC keeps the development of the newest features in branches (you have to use appropriate flags for that, for example, `-std=c++ 2a`); therefore, you are encouraged to experiment with them by yourself. So, clone and explore the GCC contracts and module branches and have fun.\n\n# 学习面向连接的通信基础知识\n\n如果你坐在办公桌前浏览互联网，很可能你使用的是一种面向连接的交流方式。当您通过 HTTP 或 HTTPS 请求页面时，在实际通信发生之前，您的机器和您试图联系的服务器之间就已经建立了连接。互联网通信事实上的*标准是**传输控制协议** ( **TCP** )。在本章中，您将了解它是什么以及它为什么重要，您还将了解(在命令行上)什么是连接。*\n\n# 怎么做...\n\n在本节中，我们将探索命令行的使用，以了解当我们与远程机器建立连接时会发生什么。具体来说，我们将学习 TCP/IP 连接的内部方面。让我们完成以下步骤:\n\n1.  在 Docker 映像运行时，打开一个外壳，键入以下命令，然后按*进入*:\n\n```cpp\ntcpdump -x tcp port 80\n```\n\n2.  打开另一个外壳，输入以下命令，按*进入*:\n\n```cpp\ntelnet amazon.com 80\n```\n\n3.  在第一个 shell 中，您将看到类似如下的输出:\n\n![](img/5a48ff38-c4f1-4ee1-934f-f71da9db0be1.png)\n\n所有这些看起来很神秘，但实际上很简单。下一部分将向您详细解释它是如何工作的。\n\n# 它是如何工作的...\n\n面向连接的通信基于两个实体之间建立连接的假设。在这一节中，我们将探究到底什么是连接。\n\n第一步使用`tcpdump` ( `man tcpdump`)，这是一个命令行工具，可以转储网络上的所有流量。在我们的例子中，它将来自端口`80`的所有 TCP 流量写入标准输出，以十六进制表示显示数据。一旦按下*进入*，则`tcpdump`将切换到收听模式。\n\n第二步使用`telnet`与在`amazon.com`的端口`80`上运行的远程服务建立连接。一旦按下*进入*，片刻后，连接将建立。\n\n在第三步中，我们看到本地机器通过`telnet`(或`man telnet`，给它取全名)服务和位于`amazon.com`的远程机器(翻译成 IP)之间的连接输出。首先要记住的是，TCP 中的连接是一个三步过程，称为**三次握手**。客户端发送 *SYN* ，服务器回复 *SYN+ACK* ，客户端回复 *ACK* 。下图显示了 TCP 报头规范:\n\n![](img/a90e0c44-8eec-4e64-b1de-f2cb80cfd1ff.png)\n\n客户端和服务器在*SYN*|*SYN+ACK*|*ACK*阶段交换什么数据才能成功建立连接？让我们一步步来。\n\n1.  客户端发送 *SYN* 到服务器(`amazon.com`):\n\n![](img/40eb9534-c86f-4741-9bba-1c40ba1910ca.png)\n\n先从`0xe8f4`和`0x050`说起(以太网头在此之前，不在本章范围内)。从前面的 TCP 报头中我们可以看到，前两个字节代表源端口(`0xe8f4` = `59636`)，后两个字节代表目的端口(`0x0050` = `80`)。在接下来的四个字节中，客户端设置一个称为序列号的随机数:`0x9bd0 | 0xb114`。在这种情况下，不会设置确认号。为了将此数据包标记为 *SYN* ，客户端必须将 *SYN* 位设置为`1`，实际上接下来两个字节的值是`0xa002`，二进制为`1010 0000 0000 0010`。我们可以看到第二位到最后一位被设置为 1(将此与 TCP 报头进行比较，如前面的截图所示)。\n\n2.  服务器发送 *SYN+ACK* 给客户端:\n\n![](img/e30cbd29-ea67-47fc-92dd-8dad6943277d.png)\n\n从客户端接收到 *SYN* 的服务器必须用 *SYN+ACK* 进行响应。去掉前 16 个字节，即以太网报头，我们可以看到以下内容:2 个字节表示源端口(`0x0050` = `80`)，第二个 2 个字节表示目的端口(`0xe8f4` = `59636`)。然后我们开始看到一些有趣的事情:服务器在序列号中放一个随机数，在这个例子中是`0x1afe = | 0x5e1e`，在确认号中是从客户端接收的序列号+ 1 = `0x9bd0 | 0xb11**5**`。我们了解到，服务器必须将标志设置为 *SYN+ACK* ，并且根据 TCP 报头，通过将两个字节设置为`0x7012` = `0111 0000 000**1** 00**1**0`来正确实现规范。突出显示的部分分别是*确认*和*同步*。然后，TCP 数据包被发送回客户端。\n\n3.  客户端向服务器发送*确认*(`amazon.com`):\n\n![](img/626f2c3e-2a7b-4b54-9cb3-a082f5324929.png)\n\n三次握手算法的最后一步是接收客户端发送给服务器的确认包。消息由两个字节组成，分别代表源端口(`0xe8f4` = `59636`)和目的端口(`0x050`=`80`)；这次的序列号包含服务器最初从客户端接收的值`0x9bd0 | 0xb115`；并且确认号包含从服务器+ 1 接收的随机值:`0x1afe = | 0x5e1**f**`。最后通过设置`0x5010` = `0101 0000 000**1** 0000`值发送*确认*(该值高亮显示的部分为*确认*；将其与之前的 TCP 报头图片进行比较)。\n\n# 还有更多...\n\n到目前为止，您所学习的协议在 RFC 793([https://tools.ietf.org/html/rfc793](https://tools.ietf.org/html/rfc793))中有所描述。如果互联网起作用，那是因为所有的网络供应商、设备驱动程序实现和许多程序都完美地实现了这个 RFC(和其他相关标准)。TCP RFC 定义的内容比我们在本食谱中了解到的要多得多，本食谱严格侧重于连通性。它定义了流量控制(通过窗口的概念)和可靠性(通过序列号和其中的*确认*的概念)。\n\n# 请参见\n\n*   *学习使用 TCP/IP 与另一台机器上的进程进行通信*食谱以编程方式展示了两台机器上的两个进程如何进行通信。正如我们将看到的，连接部分隐藏在系统调用中。\n*   [第 3 章](03.html)、*处理进程和线程*，了解进程和线程的更新。\n\n# 学习面向无连接通信的基础知识\n\n在*学习面向连接的通信基础知识*食谱中，我们了解到具有流量控制的面向连接的通信是可靠的。要使两个过程进行交流，我们必须先建立联系。这显然是以性能为代价的，我们不能总是为此付出代价——例如，当您观看在线电影时，可用带宽可能不足以支持 TCP 附带的所有功能。\n\n在这种情况下，底层通信机制很可能是无连接的。用于无连接通信的*事实上的*标准协议是**用户数据协议** ( **UDP** )，它与 TCP 处于同一逻辑级别。在这个食谱中，我们将学习 UDP 在命令行上的样子。\n\n# 怎么做...\n\n在本节中，我们将使用`tcpdump`和`netcast` ( `nc`)来分析 UDP 上的无连接链路:\n\n1.  在 Docker 映像运行时，打开一个外壳，键入以下命令，然后按*进入*:\n\n```cpp\ntcpdump -i lo udp port 45998 -X\n```\n\n2.  让我们打开另一个外壳，输入以下命令，按*进入*:\n\n```cpp\necho -n \"welcome\" | nc -w 1 -u localhost 45998\n```\n\n3.  在第一个 shell 中，您将看到类似如下的输出:\n\n![](img/e671d1fb-07cb-4c1a-a09e-5187ebd9b0e9.png)\n\n这看起来也很神秘，但实际上很简单。下一节将详细解释这些步骤。\n\n# 它是如何工作的...\n\n在 UDP 连接中，没有连接的概念。在这种情况下，一个数据包被发送到接收器。没有流量控制，链路不可靠。从下图中可以看出，UDP 报头确实非常简单:\n\n![](img/a29b794e-a166-43b2-96de-6adc69398346.png)\n\n*步骤 1* 通过打印`hex`和`ASCII`中每个数据包的数据，使用`loopback`接口(`-i lo`)上的`UDP`协议，使用`tcpdump`监听端口`45998`。\n\n*步骤 2* 使用`netcast`命令`nc` ( `man nc`)向本地主机发送包含字符串`welcome`的 UDP 数据包(`-u`)。\n\n*步骤 3* 显示了 UDP 协议的详细信息。我们可以看到，源端口(发送方随机选择)为`0xdb255` = `56101`，目的端口正确设置为`0xb3ae` = `459998`。接下来，我们将长度设置为`0x000f` = `15`，校验和设置为`0xfe22` = `65058`。长度为`15`字节，因为`7`字节是接收数据的长度，`8`字节是 UDP 报头的长度(源端口+目的端口+长度+校验和)。\n\n没有重传，没有控制流，没有连接。无连接链路实际上只是发送方发送给接收方的消息，而接收方知道它可能没有收到。\n\n# 还有更多...\n\n我们已经讨论了连接，并在 UDP 报头中看到了源端口和目的端口的概念。发送方和接收方的地址存储在其他地方，在 **IP** (简称**互联网** **协议**层，逻辑上就在 UDP 层下面。IP 层包含发送方和接收方地址(IP 地址)的信息，用于将 UDP 数据包从客户端路由到服务器，反之亦然。\n\n在 RFC 768 中，在[https://www.ietf.org/rfc/rfc768.txt](https://www.ietf.org/rfc/rfc768.txt)详细定义了 UDP。\n\n# 请参见\n\n*   [第 1 章](01.html)、*系统编程入门*，查看命令管道\n*   学习面向无连接通信的基础知识与 TCP 协议进行比较的方法\n\n# 了解什么是通信端点\n\n当两个实体相互通信时，它们基本上交换信息。为了实现这一点，每个实体都必须清楚将信息发送到哪里。从程序员的角度来看，参与通信的每个实体都必须有一个明确的端点。这个食谱将教你什么是端点，并将在命令行上显示如何识别它们。\n\n# 怎么做...\n\n在本节中，我们将使用`netstat`命令行实用程序来检查和了解端点是什么:\n\n1.  在 Docker 映像运行时，打开一个外壳，键入以下命令，然后按*进入*:\n\n```cpp\nb07d3ef41346:/# telnet amazon.com 443\n```\n\n2.  打开第二个外壳并键入以下命令:\n\n```cpp\nb07d3ef41346:/# netstat -ntp\n```\n\n下一节将解释这两个步骤。\n\n# 它是如何工作的...\n\n在*步骤 1* 中，我们使用`telnet`实用程序连接到本地机器，端口`443`上有`amazon.com`远程主机(HTTP)。该命令的输出如下:\n\n![](img/62b15a2f-680e-4b7e-af13-a937e1bc9e0a.png)\n\n它在等待命令，我们不会发送命令，因为我们真正关心的是连接。\n\n在*步骤 2* 中，我们想知道我们在本地机器(`localhost`)和远程主机(`amazon.com`端口`443`)之间建立的连接的细节。为此，我们在*步骤 2* 中执行了命令。输出如下:\n\n![](img/027525f4-3f59-4b27-b3c6-6ef58b76f388.png)\n\n我们可以从这个命令行的输出中检索到什么信息？我们可以检索到一些非常有用的信息。让我们从前面的截图中了解一下，从左到右阅读代码:\n\n*   `tcp`代表连接类型。这是一种面向连接的连接，这意味着本地和远程主机经历了我们在*学习面向连接的通信基础知识*食谱中看到的三次握手。\n*   `Recv-Q`是包含本地主机上当前进程要处理的数据的队列。\n*   `Send-Q`是一个队列，包含本地主机上当前进程要发送给远程进程的数据。\n*   `Local Address`是 IP 地址和端口号的组合，它真正代表了我们通信的第一个端点，本地端点。从编程的角度来看，这样的端点通常被称为`Socket`，它是一个整数，本质上代表`IP`和`PORT`。在这种情况下，端点是`172.17.0.2:40850`。\n*   `Foreign Address`和`Local Address`一样，是`IP`和`PORT`的组合，代表远程端点，本例中为`176.32.98.166:443`。注意`443`是一个众所周知的端口，代表`https`服务。\n*   `State`表示两个端点之间的连接状态，在本例中为`ESTABLISHED`。\n*   `PID/Program Name`，或者在我们的例子中，`65` / `telnet`，代表使用两个端点与远程主机通信的本地进程。\n\n当程序员谈论`socket`时，他们谈论的是通信的每个端点的`IP`和`PORT`。正如我们所看到的，Linux 使得分析通信的端点和它们所连接的进程变得容易。\n\n需要强调的一个重要方面是`PORT`代表一种服务。在我们的示例中，本地进程 telnet 使用端口`80`处的 IP `176.32.98.166`与远程主机连接，我们知道一个 HTTP 守护程序正在该端口运行。但是我们如何知道特定服务的端口号呢？由 **IANA** (简称**互联网号码分配机构**)维护的知名端口列表([https://www . iana . org/assignments/service-name-port-Numbers/service-name-port-Numbers . XHTML](https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml))为服务分配`PORTS`。例如，HTTPS 服务预计在`PORT 443`运行，`sftp`(安全文件传输协议**的简称**)在`PORT 22`运行，等等。\n\n# 还有更多...\n\n`port`信息是一个 16 位无符号整数值(即`unsigned int`)，由 IANA([https://www.iana.org/](https://www.iana.org/))维护，分为以下范围:\n\n*   0-1023:知名端口。众所周知的端口，例如 HTTP、SFTP 和 HTTPS。\n*   1024-49151:注册端口。组织可以要求注册的端口。\n*   49152-65535:动态、私有或短暂端口。免费使用。\n\n# 请参见\n\n*   学习面向无连接通信的基础知识学习没有连接的通信是如何工作的\n*   *学习面向连接的通信基础知识*学习连接通信如何工作的方法\n*   *学习使用 TCP/IP 与另一台机器上的进程通信*学习如何开发面向连接的程序\n*   *学习使用 UDP/IP 与另一台机器上的进程通信*学习如何开发面向无连接的程序\n\n# 学习使用 TCP/IP 与另一台机器上的进程通信\n\n这个食谱将向您展示如何使用面向连接的机制来连接两个程序。这个食谱将使用 TCP/IP，这是互联网上事实上的*标准。到目前为止，我们已经了解到 TCP/IP 是一种可靠的通信形式，它的连接分三个阶段进行。现在是时候写一个程序来学习如何让两个程序相互通信了。虽然使用的语言将是 C++，但通信部分将使用 Linux 系统调用编写，因为它不受 C++ 标准库的支持。*\n\n *# 怎么做...\n\n我们将开发两个程序，一个客户端和一个服务器。服务器将在准备接受传入连接的特定端口上启动并`listen`。客户端将启动并连接到由 IP 和端口号标识的服务器:\n\n1.  在 Docker 映像运行的情况下，打开一个 shell 并创建一个新文件`clientTCP.cpp`。让我们添加一些稍后需要的标题和常量:\n\n```cpp\n#include <stdio.h>\n#include <stdlib.h>\n#include <unistd.h>\n#include <string.h>\n#include <sys/types.h>\n#include <sys/socket.h>\n#include <netinet/in.h>\n#include <netdb.h>\n#include <iostream>\n\nconstexpr unsigned int SERVER_PORT = 50544;\nconstexpr unsigned int MAX_BUFFER = 128;\n```\n\n2.  现在开始写`main`法吧。我们从初始化`socket`开始，获取与服务器相关的信息:\n\n```cpp\nint main(int argc, char *argv[])\n{\n    int sockfd = socket(AF_INET, SOCK_STREAM, 0);\n    if (sockfd < 0) \n    {\n        std::cerr << \"socket error\" << std::endl;\n        return 1;\n    }\n    struct hostent* server = gethostbyname(argv[1]);\n    if (server == nullptr) \n    {\n        std::cerr << \"gethostbyname, no such host\" << std::endl;\n        return 2;\n    }\n```\n\n3.  接下来，我们要`connect`到服务器，但是我们需要正确的信息，即`serv_addr`:\n\n```cpp\n    struct sockaddr_in serv_addr;\n    bzero((char *) &serv_addr, sizeof(serv_addr));\n    serv_addr.sin_family = AF_INET;\n    bcopy((char *)server->h_addr, \n          (char *)&serv_addr.sin_addr.s_addr, \n          server->h_length);\n    serv_addr.sin_port = htons(SERVER_PORT);\n    if (connect(sockfd, (struct sockaddr *) &serv_addr, sizeof\n        (serv_addr)) < 0)\n    {\n        std::cerr << \"connect error\" << std::endl;\n        return 3;\n    }\n```\n\n4.  服务器会用连接`ack`回复，所以我们称之为`read`方法:\n\n```cpp\n    std::string readBuffer (MAX_BUFFER, 0);\n    if (read(sockfd, &readBuffer[0], MAX_BUFFER-1) < 0)\n    {\n        std::cerr << \"read from socket failed\" << std::endl;\n        return 5;\n    }\n    std::cout << readBuffer << std::endl;\n```\n\n5.  我们现在可以通过调用`write`系统调用将数据发送到服务器:\n\n```cpp\n    std::string writeBuffer (MAX_BUFFER, 0);\n    std::cout << \"What message for the server? : \";\n    getline(std::cin, writeBuffer);\n    if (write(sockfd, writeBuffer.c_str(), strlen(write\n        Buffer.c_str())) < 0) \n    {\n        std::cerr << \"write to socket\" << std::endl;\n        return 4;\n    }\n```\n\n6.  最后，让我们看一下清洁部分，我们必须关闭插座:\n\n```cpp\n    close(sockfd);\n    return 0;\n}\n```\n\n7.  现在让我们开发服务器程序。在第二个 shell 中，我们创建了`serverTCP.cpp`文件:\n\n```cpp\n#include <stdio.h>\n#include <stdlib.h>\n#include <string.h>\n#include <unistd.h>\n#include <sys/types.h>\n#include <sys/socket.h>\n#include <netinet/in.h>\n#include <iostream>\n#include <arpa/inet.h>\n\nconstexpr unsigned int SERVER_PORT = 50544;\nconstexpr unsigned int MAX_BUFFER = 128;\nconstexpr unsigned int MSG_REPLY_LENGTH = 18;\n```\n\n8.  在第二个 shell 中，首先，我们需要一个`socket`描述符来标识我们的连接:\n\n```cpp\nint main(int argc, char *argv[])\n{\n     int sockfd =  socket(AF_INET, SOCK_STREAM, 0);\n     if (sockfd < 0)\n     {\n          std::cerr << \"open socket error\" << std::endl;\n          return 1;\n     }\n\n     int optval = 1;\n     setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, (const\n       void *)&optval , sizeof(int));\n\n```\n\n9.  我们必须将`socket`绑定到本地机器上的端口和`serv_addr`:\n\n```cpp\n     struct sockaddr_in serv_addr, cli_addr;\n     bzero((char *) &serv_addr, sizeof(serv_addr));\n     serv_addr.sin_family = AF_INET;\n     serv_addr.sin_addr.s_addr = INADDR_ANY;\n     serv_addr.sin_port = htons(SERVER_PORT);\n     if (bind(sockfd, (struct sockaddr *) &serv_addr, sizeof\n        (serv_addr)) < 0)\n     {\n          std::cerr << \"bind error\" << std::endl;\n          return 2;\n     }\n```\n\n10.  接下来，我们必须等待并接受任何传入的连接:\n\n```cpp\n     listen(sockfd, 5);\n     socklen_t clilen = sizeof(cli_addr);\n     int newsockfd = accept(sockfd, (struct sockaddr *) &cli_addr, \n         &clilen);\n     if (newsockfd < 0)\n     {\n          std::cerr << \"accept error\" << std::endl;\n          return 3;\n     }\n```\n\n11.  一旦我们获得连接，我们记录谁连接到标准输出(使用他们的 IP 和端口)，并发送确认*确认*:\n\n```cpp\n     std::cout << \"server: got connection from = \"\n               << inet_ntoa(cli_addr.sin_addr)\n               << \" and port = \" << ntohs(cli_addr.sin_port)\n                  << std::endl;\n     write(incomingSock, \"You are connected!\", MSG_REPLY_LENGTH);\n```\n\n12.  我们建立了联系(三次握手，记得吗？)，所以现在我们可以读取来自客户端的任何数据:\n\n```cpp\n     std::string buffer (MAX_BUFFER, 0);\n     if (read(incomingSock, &buffer[0], MAX_BUFFER-1) < 0)\n     {\n          std::cerr << \"read from socket error\" << std::endl;\n          return 4;\n     }\n     std::cout << \"Got the message:\" << buffer << std::endl;\n```\n\n13.  最后，我们关闭两个插座:\n\n```cpp\n     close(incomingSock);\n     close(sockfd);\n     return 0;\n}\n```\n\n我们已经写了相当多的代码，所以是时候解释所有这些是如何工作的了。\n\n# 它是如何工作的...\n\n客户机和服务器都有一个非常通用的算法，为了让您理解和概括这个概念，我们必须对其进行描述。客户端的算法如下:\n\n```cpp\nsocket() -> connect() -> send() -> receive()\n```\n\n这里，`connect()`和`receive()`是阻塞调用(即调用程序将等待它们的完成)。`connect`这个短语特别启动了我们在*学习面向连接的交流基础*食谱中详细描述的三次握手。\n\n服务器的算法如下:\n\n```cpp\nsocket() -> bind() -> listen() -> accept() -> receive() -> send()\n```\n\n在这里，`accept`和`receive`正在阻止通话。现在让我们详细分析客户机和服务器的代码。\n\n客户端代码分析如下:\n\n1.  第一步只包含正确使用我们在前面的客户端算法部分列出的四个 API 所需的必要包含。请注意，纯 C++ 风格的常量不是使用`#define`宏定义的，而是使用`constexpr`定义的。区别在于后者由编译器管理，而前者由预处理器管理。根据经验，您应该始终尝试依赖编译器。\n2.  `socket()`系统调用创建了一个套接字描述符，我们将其命名为`sockfd`，它将用于向/从服务器发送和接收信息。这两个参数表明该套接字将是 TCP ( `SOCK_STREAM` )/IP ( `PF_INET`)套接字类型。一旦我们有了有效的套接字描述符，在调用`connect`方法之前，我们需要知道服务器的详细信息；为此，我们使用`gethostbyname()`方法，给定一个类似`localhost`的字符串，它将返回一个指向`struct hostent *`的指针，其中包含关于主机的信息。\n3.  我们现在准备调用`connect()`方法，它将处理三次握手过程。通过查看它的原型(`man connect`)，我们可以看到它和套接字一样需要一个`const struct sockaddr *address`结构，所以我们需要将各自的信息复制到其中，并传递给`connect()`；这就是为什么我们使用`utility`方法`bcopy()` ( `bzero()`只是一个在使用前重置`sockaddr`结构的辅助方法)。\n4.  我们现在准备发送和接收数据。一旦连接建立，服务器将发送确认消息(`You are connected!`)。你有没有注意到我们正在使用`read()`方法通过套接字从服务器接收信息？这就是在 Linux 环境中编程的美丽和简单。一个方法可以支持多个接口——事实上，我们可以用同一个方法来读取文件，用套接字接收数据，以及做其他许多事情。\n5.  我们可以向服务器发送消息。你可能已经猜到了，使用的方法是`write()`。我们将`socket`传递给它，它标识连接、我们希望服务器接收的消息以及消息的长度，以便 Linux 知道何时停止从缓冲区读取。\n6.  像往常一样，我们需要关闭、清理和释放所有使用的资源。在这种情况下，我们只需要使用`close()`方法关闭套接字，传递套接字描述符。\n\n服务器代码分析如下:\n\n1.  我们使用与客户端类似的代码，但是包括一些头和三个定义的常数，我们将在后面使用和解释。\n2.  我们必须通过调用`socket()` API 来定义套接字描述符。请注意，客户端和服务器之间没有区别。我们只需要一个能够管理 TCP/IP 类型连接的套接字。\n3.  我们必须将上一步创建的套接字描述符绑定到网络接口，并将其移植到本地机器上。我们使用`bind()`方法来实现，该方法将一个地址(`const struct sockaddr *address`作为第二个参数传递)分配给作为第一个参数传递的套接字描述符。调用`setsockopt()`方法只是为了避免绑定错误`Address already in use`。\n4.  我们通过调用`listen()`应用编程接口开始监听任何传入的连接。`listen()`系统调用非常简单:它获取我们正在监听的`socket`描述符和待处理连接队列中要保留的最大连接数，在我们的例子中，我们将其设置为`5`。然后我们在套接字描述符上调用`accept()`。`accept`方法是一个阻塞调用:这意味着它将阻塞，直到有新的传入连接可用，然后它将返回一个表示套接字描述符的整数。`cli_addr`结构填充了连接的信息，我们用它来记录谁连接了(`IP`和`port`)。\n5.  这一步只是第 10 步的逻辑延续。一旦服务器接受了一个连接，我们就登录到连接的标准输出(根据他们的`IP`和`port`)。我们通过查询由`accept`方法填写在`cli_addr`结构中的信息来做到这一点。\n6.  在这一步中，我们通过`read()`系统调用从连接的客户端接收信息。我们传入输入、传入连接的套接字描述符、`buffer`数据将被保存的位置，以及我们想要读取的数据的最大长度(`MAX_BUFFER-1`)。\n7.  然后，我们清理并释放所有最终使用和/或分配的资源。在这种情况下，我们必须关闭用于服务器的两个套接字描述符(`sockfd`和用于传入连接的`incomingSock`)。\n\n通过构建和运行服务器和客户端(按此顺序)，我们得到以下输出:\n\n*   服务器构建和输出如下:\n\n![](img/6cb2d008-c48a-4572-95b5-c20f08518f1a.png)\n\n*   客户端构建和输出如下:\n\n![](img/56ff6da3-b779-438d-95c5-6821223a16ac.png)\n\n这证明了我们在这个食谱中学到了什么。\n\n# 还有更多...\n\n我们如何改进服务器应用来管理多个并发的传入连接？我们实现的服务器算法是顺序的；在`listen()`之后，我们只是等待`accept()`直到结束，在那里我们关闭连接。作为练习，您应该完成以下步骤:\n\n1.  在`accept()`上运行一个无限循环，这样一个服务器就可以随时为客户服务。\n2.  为每个接受的连接分出一个新线程。可以使用`std::thread`或`std::async`来实现。\n\n另一个重要的实践是关注客户端和服务器之间交换的数据。通常，他们同意使用他们都知道的协议。它可能是一个 web 服务器，在这种情况下，它将涉及客户端和服务器之间的 HTML、文件、资源等的交换。如果它是一个监控系统，它可能是一个由特定标准定义的协议。\n\n# 请参见\n\n*   [第 3 章](03.html)、*处理进程和线程*，刷新你对进程和线程如何工作以改进这里描述的服务器解决方案的记忆\n*   学习面向连接的通信基础知识学习 TCP 连接如何工作的方法\n*   学习什么是通信端点的诀窍是学习什么是端点以及它与套接字的关系\n\n# 学习使用 UDP/IP 与另一台机器上的进程通信\n\n当一个进程与另一个进程通信时，可靠性并不总是决定通信机制的主要标准。有时，我们需要的是快速通信，而没有负担或连接、流量控制以及 TCP 协议为使其可靠而实现的所有其他控制。视频流、**互联网协议语音** ( **VoIP** )呼叫以及许多其他情况都是如此。在本食谱中，我们将学习如何编写 UDP 代码，使两个(或多个)进程相互通信。\n\n# 怎么做...\n\n我们将开发两个程序，一个客户端和一个服务器。服务器将启动，将套接字绑定到本地地址，然后只从客户端接收数据:\n\n1.  在 Docker 映像运行的情况下，打开一个 shell，创建一个新文件`serverUDP.cpp`，并添加一些我们稍后需要的标题和常量:\n\n```cpp\n#include <stdio.h>\n#include <stdlib.h>\n#include <string.h>\n#include <unistd.h>\n#include <sys/types.h>\n#include <sys/socket.h>\n#include <netinet/in.h>\n#include <iostream>\n#include <arpa/inet.h>\n\n```\n\n```cpp\nconstexpr unsigned int SERVER_PORT = 50544;\nconstexpr unsigned int MAX_BUFFER = 128;\n```\n\n2.  在`main`函数中，我们必须实例化`DATAGRAM `类型的套接字，并设置每次重新运行服务器时重用该地址的选项:\n\n```cpp\nint main(int argc, char *argv[])\n{\n     int sockfd =  socket(AF_INET, SOCK_DGRAM, 0);\n     if (sockfd < 0) \n     {\n          std::cerr << \"open socket error\" << std::endl;\n          return 1;\n     }\n     int optval = 1;\n     setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, (const void \n         *)&optval , sizeof(int));\n```\n\n3.  我们必须用本地地址绑定我们创建的套接字:\n\n```cpp\n     struct sockaddr_in serv_addr, cli_addr;\n     bzero((char *) &serv_addr, sizeof(serv_addr));\n     serv_addr.sin_family = AF_INET;  \n     serv_addr.sin_addr.s_addr = INADDR_ANY;  \n     serv_addr.sin_port = htons(SERVER_PORT);\n     if (bind(sockfd, (struct sockaddr *) &serv_addr, sizeof\n        (serv_addr)) < 0)\n     {\n          std::cerr << \"bind error\" << std::endl;\n          return 2;\n     }\n```\n\n4.  我们现在准备接收来自客户端的数据包，这次使用`recvfrom`应用编程接口:\n\n```cpp\n     std::string buffer (MAX_BUFFER, 0);\n     unsigned int len;\n     if (recvfrom(sockfd, &buffer[0], \n                  MAX_BUFFER, 0, \n                  (struct sockaddr*)& cli_addr, &len) < 0)\n     {\n          std::cerr << \"recvfrom failed\" << std::endl;\n          return 3;\n     }\n     std::cout << \"Got the message:\" << buffer << std::endl;\n```\n\n5.  我们希望通过`sendto`应用编程接口向客户端发送*确认*消息:\n\n```cpp\n     std::string outBuffer (\"Message received!\");\n     if (sendto(sockfd, outBuffer.c_str(), \n                outBuffer.length(), 0, \n                (struct sockaddr*)& cli_addr, len) < 0)\n     {\n          std::cerr << \"sendto failed\" << std::endl;\n          return 4;\n     }\n```\n\n6.  最后，我们可以关闭套接字:\n\n```cpp\n     close(sockfd);\n     return 0; \n}\n```\n\n7.  现在让我们创建客户端程序。在另一个 shell 中，创建文件`clientUDP.cpp`:\n\n```cpp\n#include <stdio.h>\n#include <stdlib.h>\n#include <unistd.h>\n#include <string.h>\n#include <sys/types.h>\n#include <sys/socket.h>\n#include <netinet/in.h>\n#include <netdb.h>\n#include <iostream>\n\nconstexpr unsigned int SERVER_PORT = 50544;\nconstexpr unsigned int MAX_BUFFER = 128;\n```\n\n8.  我们必须实例化`datagram`类型的套接字:\n\n```cpp\nint main(int argc, char *argv[])\n{\n    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);\n    if (sockfd < 0) \n    {\n        std::cerr << \"socket error\" << std::endl;\n        return 1;\n    }\n```\n\n9.  我们需要获取主机信息，以便能够识别我们想要将数据包发送到的服务器，我们通过调用`gethostbyname` API 来实现这一点:\n\n```cpp\n    struct hostent* server = gethostbyname(argv[1]);\n    if (server == NULL) \n    {\n        std::cerr << \"gethostbyname, no such host\" << std::endl;\n        return 2;\n    }\n\n```\n\n10.  让我们将主机信息复制到`sockaddr_in`结构中，以识别服务器:\n\n```cpp\n    struct sockaddr_in serv_addr, cli_addr;\n    bzero((char *) &serv_addr, sizeof(serv_addr));\n    serv_addr.sin_family = AF_INET;\n    bcopy((char *)server->h_addr, \n          (char *)&serv_addr.sin_addr.s_addr, \n          server->h_length);\n    serv_addr.sin_port = htons(SERVER_PORT);\n```\n\n11.  我们最终可以使用套接字描述符、来自用户的消息和服务器地址向服务器发送消息:\n\n```cpp\n    std::string outBuffer (MAX_BUFFER, 0);\n    std::cout << \"What message for the server? : \";\n    getline(std::cin, outBuffer);\n    unsigned int len = sizeof(serv_addr);\n    if (sendto(sockfd, outBuffer.c_str(), MAX_BUFFER, 0, \n               (struct sockaddr *) &serv_addr, len) < 0)\n    {\n        std::cerr << \"sendto failed\" << std::endl;\n        return 3;\n    }\n```\n\n12.  我们知道服务器会回复一个*确认*，所以让我们用`recvfrom`方法来接收它:\n\n```cpp\n    std::string inBuffer (MAX_BUFFER, 0);\n    unsigned int len_cli_add;\n    if (recvfrom(sockfd, &inBuffer[0], MAX_BUFFER, 0, \n                 (struct sockaddr *) &cli_addr, &len_cli_add) < 0)\n    {\n        std::cerr << \"recvfrom failed\" << std::endl;\n        return 4;\n    }\n    std::cout << inBuffer << std::endl;\n```\n\n13.  最后，像往常一样，我们负责关闭和释放所有使用的结构:\n\n```cpp\n    close(sockfd);\n    return 0;\n}\n```\n\n让我们深入代码，看看所有这些是如何工作的。\n\n# 它是如何工作的...\n\n在*学习使用 TCP/IP 与另一台机器上的进程通信*食谱中，我们学习了客户端和服务器的 TCP 算法。UDP 算法更简单，如您所见，缺少连接部分:\n\n**UDP 客户端的算法:**\n\n```cpp\nsocket() ->  sendto() -> recvfrom()\n```\n\n**UDP 服务器的算法:**\n\n```cpp\nsocket() -> bind() ->  recvfrom() -> sendto()\n```\n\n请注意它们现在有多简单——例如，在这种情况下，服务器不为和传入连接提供`listen`。\n\n服务器端代码分析如下:\n\n1.  我们刚刚定义了一些头和两个常量，它们表示服务器将公开服务的端口(`SERVER_PORT`)和数据的最大大小(`MAX_BUFFER`)。\n2.  在这一步中，我们定义了套接字(`sockfd`)，就像我们在 TCP 代码中所做的那样，但是这次我们使用了`SOCK_DGRAM` (UDP)类型。为了避免`Address already in use`的绑定问题，我们设置了允许套接字重用地址的选项。\n\n3.  接下来是`bind`呼叫。它接受`int socket`、`const struct sockaddr *address`和`socklen_t address_len`的参数，这些参数基本上是套接字、绑定套接字的地址以及地址结构的长度。在`address`变量中，我们指定我们正在监听所有可用的本地网络接口(`INADDR_ANY`)，并且我们将使用互联网协议版本 4 ( `AF_INET`)。\n4.  我们现在可以使用`recvfrom`方法开始接收数据。该方法将套接字描述符(`sockfd`)、用于存储数据的缓冲区(`buffer`)、我们可以存储的最大数据大小、用于设置接收消息的特定属性的标志(在本例中为`0`)、数据报发送方的地址(`cli_addr`)以及地址长度(`len`)作为输入。最后两个参数被填充返回，这样我们就知道谁发送了数据报。\n5.  我们现在可以向客户端发送*确认*。我们使用`sendto`方法。由于 UDP 是无连接协议，我们没有连接客户端，所以我们需要以某种方式传递这些信息。我们通过将由`recvfrom`方法填写的`cli_addr`连同长度(`len`)传递给`sendto`方法来实现。除此之外，我们还需要传递套接字描述符(`sockfd`)、要发送的缓冲区(`outBuffer`)、缓冲区的长度(`outBuffer.length()`)和标志(在本例中为`0`)。\n6.  然后，我们只需要在程序结束时进行清理。我们必须用`close()`方法关闭套接字描述符。\n\n客户端代码分析如下:\n\n1.  在这一步中，我们找到了与在`serverUDP.cpp`源文件中`SERVER_PORT`和`MAX_BUFFER`相同的标题。\n2.  我们必须通过调用`socket`方法来定义数据报类型的套接字，再次作为输入传递`AF_INET`和`SOCK_DGRAM`。\n3.  因为我们需要知道向谁发送数据报，所以客户端应用在命令行上将我们传递给`gethostbyname`的服务器地址(例如`localhost`)作为输入，而`gethostbyname`返回主机地址(`server`)。\n4.  我们使用`server`变量来填充`serv_addr`结构，该结构用于标识我们要将数据报发送到的服务器的地址(`serv_addr.sin_addr.s_addr`)、端口(`serv_addr.sin_port`)和协议家族(`AF_INET`)。\n\n5.  然后我们可以使用`sendto`方法，通过传递`sockfd`、`outBuffer`、`MAX_BUFFER`的参数、设置为`0`的标志、服务器的地址`serv_addr`及其长度(`len`)向服务器发送用户消息。同样，客户端在这个阶段不知道谁是消息的接收者，因为它没有连接到任何人，这就是为什么`serv_addr`结构必须正确填写，以便它包含有效的地址。\n6.  我们知道服务器会发回一个应用 *ACK* ，所以我们必须接收它。我们调用`recvfrom`方法，该方法将套接字描述符(`sockfd`)作为输入，将返回的数据存储在(`buffer`)中的缓冲区，我们可以获得的最大数据大小，以及设置为`0`的标志。`recvfrom`返回消息发送者的地址及其长度，我们分别存储在`cli_addr`和`len`中。\n\n我们先运行服务器，然后运行客户端。\n\n按照以下步骤运行服务器:\n\n![](img/bdbbe7da-c8df-4197-912f-246ee3751e02.png)\n\n按照以下步骤运行客户端:\n\n![](img/9a159ad9-61df-452c-91f6-b98de7bbfb2a.png)\n\n这显示了 UDP 是如何工作的。\n\n# 还有更多...\n\n另一种使用 UDP 协议的方式，作为一种无连接通信，是以多播或广播格式发送数据报。多播是一种用于向多个主机发送相同数据报的通信技术。代码不会改变；我们只需要设置多播组的 IP，这样它就知道将消息发送到哪里。这是一种便捷高效的*一对多*通信方式，节省了大量带宽。另一种选择是以广播模式发送数据报。我们要用`172.30.255.255`形式的子网掩码设置接收方的 IP。该消息将发送给同一子网中的所有主机。\n\n请通过以下步骤改进服务器代码:\n\n1.  在`recvfrom()`上建立一个无限循环，这样你就可以随时准备好服务器来服务客户。\n2.  为每个接受的连接启动一个新线程。可以使用`std::thread`或`std::async`来实现。\n\n# 请参见\n\n*   [第 3 章](03.html)、*处理进程和线程*，刷新进程和线程如何工作来改进这里描述的服务器解决方案\n*   学习面向无连接通信的基础知识学习 UDP 连接如何工作的方法\n*   *学习什么是通信端点*的诀窍是学习什么是端点以及它与套接字的关系\n\n# 处理字符顺序\n\n在系统级编写代码可能意味着要处理不同的处理器架构。这样做的时候，在 C++ 20 之前，有一件事情是程序员必须自己处理的，那就是 **endianness** 。Endianness 指的是数字的二进制表示中的字节顺序。幸运的是，最后一个 C++ 标准帮助我们在编译时输入端序信息。这个食谱将教你如何知道字节序，并编写可以在小字节序和大字节序架构上运行的代码。\n\n# 怎么做...\n\n我们将开发一个在编译时查询机器的程序，这样我们就可以有意识地决定如何处理以不同格式表示的数字:\n\n1.  我们需要包含`<bit>`头文件；然后我们可以使用`std::endian`枚举:\n\n```cpp\n#include <iostream>\n#include <bit>\n\nint main()\n{ \n    if (std::endian::native == std::endian::big)\n        // prepare the program to read/write \n        // in big endian ordering.\n        std::cout << \"big\" << std::endl;\n    else if (std::endian::native == std::endian::little)\n        // prepare the program to read/write \n        // in little endian ordering.\n        std::cout << \"little\" << std::endl; \n\n return 0;\n}\n```\n\n让我们在下一节中仔细看看这有什么影响。\n\n# 它是如何工作的...\n\n大端和小端是数据表示的两种主要类型。小端排序格式意味着最低有效字节(也称为 **LSB** )位于最高地址，而在大端机器中，最高有效字节(也称为 **MSB** )位于最低地址。十六进制值`0x1234`的表示示例如下:\n\n|  | **地址** | **地址+1(字节)** |\n| **大端** | `12` | `34` |\n| **小端** | `34` | `12` |\n\n步骤 1 中代码片段的主要目标是回答这个问题:我如何知道我正在处理的是什么机器架构？新的 C++ 20 枚举`std::endian`帮助我们完美地解决了这个问题。怎么做？嗯，首先从 T2 的端序意识来说。将`std::endian`作为 C++ 标准库的一部分有助于程序员随时查询底层机器的 endian 架构。第二:对于共享资源，两个程序必须就一种格式达成一致(就像 TCP 协议一样，即以*网络顺序*发送信息)，以便阅读器(或接收器，如果通过网络交换数据)可以进行适当的转换。\n\n另一个问题是:我该怎么办？你应该做两件事:一是与应用观点有关，二是与网络有关。在这两种情况下，如果您的应用与另一台具有不同 endian 格式的机器交换数据(交换的文件或共享的文件系统等)，或者通过互联网将数据发送到具有不同体系结构的机器，那么您必须确保您的数据被理解。为此，可以使用`hton`、`ntoh`宏和好友；这确保了号码从主机转换到网络(对于`hton`)和从网络转换到主机(对于`ntoh`)。我们不得不提到，大多数互联网协议使用大端格式，这就是为什么，如果你从大端机器调用`hton`，该函数将不执行任何转换的原因。\n\n英特尔 x86 系列和 AMD64 系列处理器均采用小端格式，而 IBM z/Architecture、飞思卡尔和所有摩托罗拉 68000 传统处理器均采用大端格式。有些处理器(如 PowerPC)可以切换字符顺序。\n\n# 还有更多...\n\n理论上，除了小端和大端之外，数据表示格式确实存在。一个例子是霍尼韦尔 316 小型计算机使用的中端格式。\n\n# 请参见\n\n*   *学习使用 TCP/IP 与另一台机器上的进程通信*配方\n*   *学习使用 UDP/IP 与另一台机器上的进程通信*配方*"
  },
  {
    "path": "docs/cpp-sys-prog-cb/08.md",
    "content": "# 八、处理控制台输入/输出和文件\n\n本章介绍了使用 C++ 标准库基于控制台、流和文件输入/输出的方法。我们已经在其他章节中向我们编写的程序中读取了参数，但是还有其他几种方法可以做到这一点。我们将深入探讨这些主题，并通过具体的、专门的实践方法学习每一个主题的替代方法、技巧和最佳实践。\n\n再说一次，我们的主要重点是尽可能多地使用 C++(及其标准库)来编写系统编程软件，因此代码将具有非常有限的 C 和 POSIX 解决方案。\n\n本章将涵盖以下主题:\n\n*   实现控制台的输入/输出\n*   操纵输入输出字符串\n*   使用文件\n\n# 技术要求\n\n为了让您从一开始就尝试这些程序，我们设置了一个 Docker 映像，其中包含了我们在整本书中需要的所有工具和库。它基于 Ubuntu 19.04。\n\n要进行设置，请执行以下步骤:\n\n1.  从[www.docker.com](https://www.docker.com/)下载并安装 Docker 引擎。\n2.  从 Docker\n    中心拉出图像:`docker pull kasperondocker/system_programming_cookbook:latest`\n\n3.  图像现在应该可以使用了。输入以下命令查看图像:`docker images`\n4.  你现在应该有这个图像了:`kasperondocker/system_programming_cookbook`\n5.  借助以下命令，使用交互式外壳运行 Docker 映像:`docker run -it **-**-cap-add sys_ptrace kasperondocker/system_programming_cookbook:latest /bin/bash`\n6.  运行容器上的外壳现已可用。使用`root@39a5a8934370/# cd /BOOK/`获取我们在整本书中开发的所有程序，按章节组织。\n\n需要`--cap-add sys_ptrace`参数来允许 Docker 容器中的 GDB 设置断点，这是 Docker 默认不允许的。\n\n# 实现控制台的输入/输出\n\n这个食谱主要关注控制台输入/输出。我们编写的大多数程序都需要与用户进行某种交互:我们需要获取输入，进行一些处理，并返回输出。例如，考虑一下您可以在将要构建的应用中收集的用户输入。在本食谱中，我们将编写代码，展示从控制台获取输入并返回输出的不同方法。\n\n# 怎么做...\n\n让我们写一些代码:\n\n1.  随着 Docker 映像的运行，让我们创建一个名为`console_01.cpp`的新文件，并在其中键入以下代码:\n\n```cpp\n#include <iostream>\n#include <string>\nint main ()\n{\n    std::string name;\n    std::cout << \"name: \";\n    std::cin >> name;\n\n    std::string surname;\n    std::cout << \"surname: \";\n    std::cin >> surname;\n\n    int age;\n    std::cout << \"age: \";\n    std::cin >> age;\n\n    std::cout << \"Hello \" << name << \", \" \n              << surname << \": \" << age << std::endl;\n    return 0;\n}\n```\n\n2.  现在创建另一个名为`console_02.cpp`的文件，并输入该代码以查看这种方法的局限性:\n\n```cpp\n#include <iostream>\n#include <string>\nint main ()\n{\n    std::string fullNameWithCin;\n    std::cout << \"full Name got with cin: \";\n    std::cin >> fullNameWithCin;\n\n    std::cout << \"hello \" << fullNameWithCin << std::endl;\n    return 0;\n}\n```\n\n3.  最后，让我们创建一个新文件并命名为`console_03.cpp`；让我们看看`std::getline`和`std::cin`如何克服之前的这个限制:\n\n```cpp\n#include <iostream>\n#include <string>\n\nint main ()\n{\n    std::string fullName;\n    std::cout << \"full Name: \";\n    std::getline (std::cin, fullName);\n    std::cout << \"Hello \" << fullName << std::endl;\n    return 0;\n}\n```\n\n虽然这些都是非常简单的例子，但是它们展示了与控制台标准输入和输出交互的 C++ 方式。\n\n# 它是如何工作的...\n\n第一步，`console_01.cpp`程序只是使用`std::cin`和`std::cout`获取用户的`name`和`surname`信息，保存在`std::string`变量中。当需要与标准输入和输出进行简单的交互时，首先要使用这些东西。通过构建和运行`console_01.cpp`文件，我们将获得以下输出:\n\n![](img/2c32601b-b89d-43d4-857c-f37964704b56.png)\n\n食谱的第二步显示`std::cin`和`std::cout`的限制。用户将命令行中的`name`和`surname`赋予编程的运行过程，但奇怪的是，只是名字存储在`fullNameWithCin`变量中，完全跳过了姓氏。怎么会这样原因很简单:`std:cin`始终将空格、制表符或换行符视为从标准输入中捕获的值的分隔符。那么，我们如何从标准输入中获得完整的行呢？通过编译运行`console_02.cpp`，我们得到如下结果:\n\n![](img/ebac7a09-1cfb-49bb-aa61-fe0dcce7482a.png)\n\n第三步显示了结合使用`getline`功能和`std::cin`从标准输入中获取整行。`std::getline`从`std::cin`获取该行，并将其存储在`fullName`变量中。一般来说，`std::getline`接受任何`std::istream`作为输入，可以指定分隔符。标准库中可用的原型如下:\n\n```cpp\nistream& getline (istream& is, string& str, char delim);\nistream& getline (istream&& is, string& str, char delim);\nistream& getline (istream& is, string& str);\nistream& getline (istream&& is, string& str);\n```\n\n这些使得`getline`成为一个非常灵活的方法。通过构建和运行`console_03.cpp`，我们得到如下输出:\n\n![](img/313c17da-22e0-4075-9502-54c86b4e5119.png)\n\n让我们看一下下面的示例，其中我们将一个流传递给方法、存储提取的信息的变量和分隔符:\n\n```cpp\n#include <iostream>\n#include <string>\n#include <sstream>\n\nint main ()\n{\n    std::istringstream ss(\"ono, vaticone, 43\");\n\n    std::string token;\n    while(std::getline(ss, token, ','))\n    {\n        std::cout << token << '\\n';\n    }\n\n    return 0;\n}\n```\n\n上述方法的输出如下:\n\n![](img/6272f92b-7756-45a2-b4c5-a7fb7102e7b7.png)\n\n这可以为构建您自己的标记器方法奠定基础。\n\n# 还有更多...\n\n`std::cin`和`std::cout`允许链请求，这使得代码更加易读和简洁:\n\n```cpp\nstd::cin >> name >> surname;\nstd::cout << name << \", \" << surname << std::endl;\n```\n\n`std::cin`期望用户先传自己的名字，再传自己的姓氏。它们必须用空格、制表符或换行符隔开。\n\n# 请参见\n\n*   *学习如何操作输入/输出字符串*食谱涵盖了如何操作字符串作为控制台输入/输出的补充。\n\n# 学习如何操作输入输出字符串\n\n字符串操作几乎是任何软件的一个非常重要的方面。能够简单有效地操作字符串是软件开发的一个关键方面。如何读取或解析应用的配置文件？这个食谱将教你 C++ 提供了什么工具来让`std::stringstream`课成为一个愉快的任务。\n\n# 怎么做...\n\n在本节中，我们将通过使用`std::stringstream`来解析流来开发一个程序，流实际上可以来自任何来源:文件、字符串、输入参数等。\n\n1.  让我们开发一个打印文件所有条目的程序。在一个新的 CPP 文件中输入以下代码，`console_05.cpp`:\n\n```cpp\n#include <iostream>\n#include <string>\n#include <fstream>\n\nint main ()\n{\n    std::ifstream inFile (\"file_console_05.txt\", std::ifstream::in);\n    std::string line;\n    while( std::getline(inFile, line) )\n        std::cout << line << std::endl;\n\n    return 0;\n}\n```\n\n2.  `std::stringstream`在我们必须将字符串解析成变量时非常方便。让我们通过在一个新文件`console_06.cpp`中编写以下代码来看看这一点:\n\n```cpp\n#include <iostream>\n#include <string>\n#include <fstream>\n#include <sstream>\n\nint main ()\n{\n    std::ifstream inFile (\"file_console_05.txt\",\n        std::ifstream::in);\n    std::string line;\n    while( std::getline(inFile, line) )\n    {\n        std::stringstream sline(line);\n        std::string name, surname; \n        int age{};\n        sline >> name >> surname >> age;\n        std::cout << name << \"-\" << surname << \"-\"<< age << \n            std::endl;\n    }\n    return 0;\n}\n```\n\n3.  此外，为了补充第二步，解析和创建字符串流也很容易。让我们在`console_07.cpp`中进行:\n\n```cpp\n#include <iostream>\n#include <string>\n#include <fstream>\n#include <sstream>\n\nint main ()\n{\n    std::stringstream sline;\n    for (int i = 0; i < 10; ++ i)\n        sline << \"name = name_\" << i << \", age = \" << i*7 << \n            std::endl;\n\n    std::cout << sline.str();\n    return 0;\n}\n```\n\n前面三个程序展示了用 C++ 解析字符串是多么简单。下一节将逐步解释它们。\n\n# 它是如何工作的...\n\n*步骤 1* 显示`std::getline`接受任何流作为输入，而不仅仅是标准输入(即`std::cin`)。在这种情况下，它从文件中获取流。我们包括`std::cout`的`iostream`、`string`可以使用字符串、`fstream`可以读取文件。\n\n然后，我们使用`std::fstream`(文件流)打开`file_console_05.txt`文件。在它的构造函数中，我们传递文件名和标志(在这种情况下，只是带有`std::ifstream::in`的输入文件的信息)。我们将文件流传递给`std::getline`，T3 将负责从流中复制每一行，并将其存储在刚刚打印的`std::string`变量`line`中。该程序的输出如下:\n\n![](img/867418ac-6e1b-4f44-ba9c-7ce3e3c45e78.png)\n\n*第 2 步*显示了相同的程序读取`file_console_05.txt`文件，但是，这次我们要解析文件的每一行。我们通过将`line`字符串变量传递给`sline` `std::stringstream`变量来实现。`std::stringstream`提供方便易用的解析功能。\n\n通过只写行`sline >> name >> surname >> age`，`std::stringstream`类的`operator>>`将把`name`，`surname`和`age`保存到各自的变量中，注意类型转换(也就是说，对于`age`变量，从`string`到`int`)，假设这些变量在文件中以该顺序出现。`operator>>`将解析该字符串，并通过跳过前导的**空格** *，*将为每个标记调用适当的方法(例如，`basic_istream& operator>>( short& value );`或`basic_istream& operator>>( long long& value );`等)。该程序的输出如下:\n\n![](img/137a9b31-5a6d-45c2-9966-1de8b3c8cc6b.png)\n\n*第 3 步*显示了将流解析成变量的简单性同样适用于构建流。相同的`std::stringstream`变量`sline`与`<<`运算符一起使用，表示数据流现在流向`string stream`变量，该变量在下面的截图中以两行打印到标准输出。正如预期的那样，该程序的输出如下:\n\n![](img/0bfd621e-e616-449c-b2a4-5c4a1be90335.png)\n\n`std::stringstream`使得解析字符串和流变得非常容易，无论它们来自哪里。\n\n# 还有更多...\n\n如果您正在寻找低延迟，使用`std::stringstream`进行流操作可能不是您的首选。我们始终建议您衡量绩效，并根据数据做出决定。如果是这样，您可以尝试不同的解决方案:\n\n*   如果可以的话，只需关注要优化的代码的低延迟部分。\n*   使用标准的 C 或 C++ 方法来解析数据，例如典型的`atoi()`方法来编写您的层。\n*   使用任何开源的低延迟框架。\n\n# 请参见\n\n*   *实现控制台输入输出*的方法包括如何处理控制台输入输出。\n\n# 使用文件\n\n这个食谱将教会你处理文件所需的基本知识。C++ 标准库在历史上提供了一个非常好的接口，但是 C++ 17 增加了一个名为`std::filesystem`的命名空间，这进一步丰富了这个功能。不过，我们不会利用 C++ 17 `std::filesystem`命名空间，因为它已经在[第 2 章](02.html)、*中介绍过了。考虑一个创建配置文件的具体用例，或者需要复制该配置文件的地方。这个食谱将教你 C++ 如何让这个任务变得容易。*\n\n# 怎么做...\n\n在本节中，我们将编写三个程序来学习如何使用`std::fstream`、`std::ofstream`和`std::ifstream`处理文件:\n\n1.  让我们使用`std::ofstream`开发一个打开并写入新文件`file_01.cpp`的程序:\n\n```cpp\n#include <iostream>\n#include <fstream>\n\nint main ()\n{\n    std::ofstream fout;\n    fout.open(\"file_01.txt\");\n\n    for (int i = 0; i < 10; ++ i)\n        fout << \"User \" << i << \" => name_\" << i << \" surname_\" \n            << i << std::endl;\n\n    fout.close();\n}\n```\n\n2.  在一个新的源文件`file_02.cpp`中，让我们从一个文件中读取并打印到标准输出:\n\n```cpp\n#include <iostream>\n#include <fstream>\n\nint main ()\n{\n    std::ifstream fiut;\n    fiut.open(\"file_01.txt\");\n\n    std::string line;\n    while (std::getline(fiut, line))\n        std::cout << line << std::endl;\n\n    fiut.close();\n}\n```\n\n3.  现在，我们希望将打开文件的灵活性与读写结合起来。我们将使用`std::fstream`将`file_01.txt`的内容复制到`file_03.txt`中，然后打印其内容。在另一个源文件`file_03.cpp`中，键入以下代码:\n\n```cpp\n#include <iostream>\n#include <fstream>\n\nint main ()\n{\n    std::fstream fstr;\n    fstr.open(\"file_03.txt\", std::ios::trunc | std::ios::out | std::ios::in);\n\n    std::ifstream fiut;\n    fiut.open(\"file_01.txt\");\n    std::string line;\n    while (std::getline(fiut, line))\n        fstr << line << std::endl;\n    fiut.close();\n\n    fstr.seekg(0, std::ios::beg);\n    while (std::getline(fstr, line))\n        std::cout << line << std::endl; \n    fstr.close();\n}\n\n```\n\n让我们看看这个食谱是如何工作的。\n\n# 它是如何工作的...\n\n在深入研究前面三个程序之前，我们必须阐明标准库是如何针对文件流构建的。让我们看看下表:\n\n|  |  | `<fstream>` |\n| `<ios>` | <ostream> | ofstream |\n| `<ios>` | <istream> | ifstream |\n\n让我们把它分解如下:\n\n*   `<ostream>`:负责输出流的 streams 类。\n*   `<istream>`:负责输入流的 streams 类。\n*   `ofstream`:流类，用于写入文件。出现在`fstream`头文件中。\n*   `ifstream`:流类，用于读取文件。出现在`fstream`头文件中。\n\n`std::ofstream`和`std::ifstream`分别继承自`std::ostream`和`std::istream`的泛型流类。可以想象，`std::cin`和`std::cout`也是从`std::istream`和`std::ostream`继承而来的(上表未显示)。\n\n*第一步*:我们首先要做的就是包含`<iostream>`和`<fstream>`，以便使用`std::cout`和`std::ofstream`读取`file_01.txt`文件。然后我们调用`open`方法，在这种情况下，它以写入模式打开文件，因为我们使用的是`std::ofstream`类。我们现在准备用`<<`操作符将字符串写入`fout`文件流。最后，我们必须关闭流，这将最终关闭文件。通过编译和运行程序，我们将获得以下输出:\n\n![](img/54d34028-d689-4189-be42-0a164bbe3750.png)\n\n*第二步*:这种情况下我们反其道而行之:从`file_01.txt`文件中读取，打印到标准输出。在这种情况下，唯一的区别是我们使用了`std::ifstream`类，它代表一个读取文件流。通过调用`open()`方法，文件以读取模式(`std::ios::in`)打开。通过使用`std::getline`方法，我们可以打印到标准输出文件的所有行。输出如下所示:\n\n![](img/d24e9dd6-2fda-49c5-905b-b1e690ba9987.png)\n\n最后的第三步展示了`std::fstream`类的用法，通过允许我们以读写模式打开文件(`std::ios::out` | `std::ios::in`)给了我们更多的自由。如果文件存在，我们也要截断它(`std::ios::trunc`)。有更多的选择可以传递给`std::fstream`建造者。\n\n# 还有更多...\n\nC++ 17 通过在标准库中添加`std::filesystem`进行了巨大的改进。它并不是全新的——它受到了 Boost 库的极大启发。曝光的主要公众成员如下:\n\n| **方法名称** | **描述** |\n| `path` | 表示路径 |\n| `filesystem_error` | 文件系统错误异常 |\n| `directory_iterator` | 目录内容的迭代器(递归版本也可用) |\n| `space_info` | 关于文件系统上可用空间的信息 |\n| `perms` | 标识文件系统权限系统 |\n\n在`std::filesystem`命名空间中，也有给出文件信息的辅助函数，如`is_directory()`、`is_fifo()`、`is_regular_file()`、`is_socket()`等。\n\n# 请参见\n\n*   [第二章](02.html)*中的*理解文件系统*配方，重温 C++* ，给出了这个主题的复习。"
  },
  {
    "path": "docs/cpp-sys-prog-cb/09.md",
    "content": "# 九、处理时间接口\n\n时间在操作系统和应用中有多种形式。通常，应用需要处理以下**类**时间:\n\n*   **时钟**:实际的时间和日期，就像你在手表上看到的一样\n*   **时间点**:对应用的使用情况(例如，处理器或一般资源)进行分析、监控和故障排除所花费的处理时间\n*   **持续时间**:单调时间，即某一事件经过的时间\n\n在本章中，我们将从 C++ 和 POSIX 的角度处理所有这些方面，以便您的工具箱中有更多可用的工具。本章中的食谱将教你如何通过使用时间点来衡量一个事件，为什么你应该使用一个稳定的时钟，以及时间何时超过以及如何缓解它。您将学习如何用 POSIX 和 C++ 实现这些概念。\n\n本章将涵盖以下食谱:\n\n*   了解 C++ 时间接口\n*   使用 C++ 20 日历和时区\n*   了解 Linux 计时\n*   处理睡眠时间和超时\n\n# 技术要求\n\n为了立即试用本章中的程序，我们设置了一个 Docker 映像，其中包含了我们在本书中需要的所有工具和库。它基于 Ubuntu 19.04。\n\n要进行设置，请执行以下步骤:\n\n1.  从[www.docker.com](https://www.docker.com/)下载安装 Docker 引擎。\n2.  从 Docker Hub 中拉出图像:`docker pull kasperondocker/system_programming_cookbook:latest`。\n3.  图像现在应该可以使用了。输入以下命令查看图像:`docker images`。\n4.  你应该有如下图像:`kasperondocker/system_programming_cookbook`。\n5.  借助`docker run -it --cap-add sys_ptrace kasperondocker/system_programming_cookbook:latest /bin/bash`命令，用交互式外壳运行 Docker 图像。\n6.  运行容器上的外壳现已可用。前往`root@39a5a8934370/# cd /BOOK/`获取本书将要开发的所有程序。\n\n需要`--cap-add sys_ptrace`参数来允许 **GDB** (简称 **GNU 项目调试器**)设置断点，Docker 默认不允许。\n\n**Disclaimer**: The C++ 20 standard has been approved (that is, technically finalized) by WG21 in a meeting in Prague at the end of February. This means that the GCC compiler version that this book uses, 8.3.0, does not include (or has very, very limited support for) the new and cool C++ 20 features. For this reason, the Docker image does not include the C++ 20 recipe code. GCC keeps the development of the newest features in branches (you have to use appropriate flags for that, for example, `-std=c++ 2a`); therefore, you are encouraged to experiment with them by yourself. So, clone and explore the GCC contracts and module branches and have fun.\n\n# 了解 C++ 时间接口\n\nC++ 11 标准确实标志着关于时间的重要一步。在此之前(C++ 标准 98 及之前)，系统和应用开发人员不得不依赖于特定于实现的 API(即 POSIX)或外部库(例如`boost`)来操纵**时间**，这意味着可移植性较低的代码。这个食谱将教你如何使用标准时间操作库编写 C++ 代码。\n\n# 怎么做...\n\n让我们编写一个程序来学习 C++ 标准中支持的**时钟**、**时间点**和**持续时间**的概念:\n\n1.  创建一个新文件，并将其称为`chrono_01.cpp`。我们首先需要几个包括:\n\n```cpp\n#include <iostream>\n#include <vector>\n#include <chrono>\n```\n\n2.  在`main`部分，我们需要测量一些东西，所以让我们用一些整数填充一个`std::vector`:\n\n```cpp\nint main ()\n{\n    std::cout << \"Starting ... \" << std::endl;\n    std::vector <int> elements;\n    auto start = std::chrono::system_clock::now();\n\n    for (auto i = 0; i < 100'000'000; ++ i)\n        elements.push_back(i);\n\n    auto end = std::chrono::system_clock::now();\n```\n\n3.  现在我们有了两个时间点`start`和`end`，让我们计算一下差值(即持续时间)并打印出来，看看花了多长时间:\n\n```cpp\n    // default seconds\n    std::chrono::duration<double, std::milli> diff = end - start;\n    std::cout << \"Time Spent for populating a vector with     \n        100M of integer ...\" \n              << diff.count() << \"msec\" << std::endl;\n```\n\n4.  现在，我们要以另一种格式打印`start`变量；例如，在带有`ctime`的日历本地时间格式中:\n\n```cpp\n    auto tpStart = std::chrono::system_clock::to_time_t(start);\n    std::cout << \"Start: \" << std::ctime(&tpStart) << std::endl;\n\n    auto tpEnd = std::chrono::system_clock::to_time_t(end);\n    std::cout << \"End: \" << std::ctime(&tpEnd) << std::endl;\n    std::cout << \"Ended ... \" << std::endl;\n}\n```\n\n这个程序使用了一些`std::chrono`特性，如`system_clock`、`time_point`和标准库中可用的持续时间，从 C++ 标准的第 11 版开始就这样做了。\n\n# 它是如何工作的...\n\n*步骤 1* 负责包含我们稍后需要的标题:`<iostream>`用于标准输出，`<vector>`和`<chrono>`用于时间。\n\n*步骤 2* 定义了一个称为元素的 **int 向量。因此，我们可以在`chrono`命名空间中的`system_clock`类上调用`now()`方法来获取当前时间。虽然我们使用了`auto`，但是这个方法返回了一个表示时间点的`time_point`对象。然后，我们循环了 1 亿次来填充`elements`数组，以强调我们使用了新的 C++ 14 特性来表示 *100，000，000* ，这提高了代码的可读性。最后，我们通过调用`now()`方法并将`time_point`对象存储在`end`变量中来获取另一个时间点。**\n\n在*第 3 步*中，我们看了执行循环需要多长时间。为了计算这个，我们实例化了一个`duration`对象，这是一个需要两个参数的模板类:\n\n*   **表示**:表示刻度数的类型。\n*   **时期**:这个可以是(除了别的以外)`std::nano`、`std:micro`、`std::milli`等等。\n\n期间的默认值为`std::seconds`。然后，我们只在标准输出上写`diff.cout()`，它代表`start`和`end`之间的毫秒数。计算这种差异的另一种方法是使用`duration_cast`；例如`std::chrono::duration_cast<std::chrono::milliseconds> (end-start).count()`。\n\n在*步骤 4 中，*我们在日历`localtime`表示中打印`start`和`end` `time_point`变量(注意容器时间可能与主机容器不同步)。为此，我们需要使用`system_clock`类的`to_time_t()`静态变量将它们转换成`time_t`，然后将它们传递给`std::ctime`方法。\n\n现在，让我们构建并运行这个:\n\n![](img/b733171a-695f-4db6-b3ae-eab79b55b5d8.png)\n\n在下一节中，我们将学习更多关于这个食谱的知识。\n\n# 还有更多...\n\n我们开发的程序使用`system_clock`类。`chrono`命名空间中有三个时钟类:\n\n*   `system_clock`:这代表所谓的**挂钟时间** *。*它可以随时调整，例如通过闰秒引入额外的不精确性或用户刚刚设置它。在大多数实现中，它的纪元(即它的起点)使用 UNIX 时间，这意味着从 1970 年 1 月 1 日 <sup>st</sup> 开始计数。\n*   `steady_clock`:这代表了所谓的**单调时钟**。永远不会调整。它保持稳定。在大多数实现中，它的起点是机器启动的时间。为了计算某个事件的过去时间，您应该考虑使用这种类型的时钟。\n*   `high_resolution_clock`:这是可用的滴答时间最短的时钟。它可能只是`system_clock`或`steady_clock`的别名，或者是一个完全不同的实现。它是实现定义的。\n\n要记住的第二个方面是 C++ 20 标准包括`time_of_day`、日历和时区。\n\n# 请参见\n\n*   *学习 Linux 定时*食谱进行简单对比\n*   *c++ 之旅，第二版，*比雅尼·斯特劳斯特鲁普\n\n# 使用 C++ 20 日历和时区\n\nC++ 20 标准用日历特性丰富了`std::chrono`命名空间。它们包括了你所期望的所有典型特性，以及一种更习惯和直观的玩法。这个食谱将教你一些最重要的特性，以及与`std::chrono`名称空间的日历部分交互有多简单。\n\n# 怎么做...\n\n让我们看一些代码:\n\n1.  创建一个新文件，确保包含`<chrono>`和`<iostream>`。我们有个约会，我们想知道一周中的哪一天`bday`会到来:\n\n```cpp\n#include <chrono>\n#include <iostream>\n\nusing namespace std;\nusing namespace std::chrono;\n\nint main ()\n{\n    auto bday = January/30/2021;\n    cout << weekday(bday) << endl;\n\n    auto anotherDay = December/25/2020;\n    if (bday == anotherDay)\n        cout << \"the two date represent the same day\" << endl;\n    else\n        cout << \"the two dates represent two different days\"    \n            << endl;\n}\n```\n\n2.  有一整套课程可以让你玩日历。让我们来看看其中的一些:\n\n```cpp\n#include <chrono>\n#include <iostream>\n\nusing namespace std;\nusing namespace std::chrono;\n\nint main ()\n{\n    auto today = year_month_day{ floor<days>(system_clock::now()) };\n    auto ymdl = year_month_day_last(today.year(), month*day* last{ month{ 2 } });\n    auto last_day_feb = year_month_day{ ymdl };\n    std::cout << \"last day of Feb is: \" << last_day_feb\n        << std::endl;\n\n    return 0;\n}\n```\n\n3.  让我们玩一下时区，并打印不同时区的时间列表:\n\n```cpp\n#include <chrono>\n#include <iostream>\n\nusing namespace std;\nusing namespace std::chrono;\n\nint main()\n{\n    auto zone_names = {\n       \"Asia/Tokyo\",\n       \"Europe/Berlin\",\n       \"Europe/London\",\n       \"America/New_York\",\n    };\n\n    auto localtime = zoned_time<milliseconds>(date::current_zone(),\n                                              system_clock::now());\n    for(auto const& name : zone_names)\n        cout << name\n             << zoned_time<milliseconds>(name, localtime)\n             << std::endl;\n\n    return 0;\n}\n```\n\n4.  经常使用的一个特性是用来找出两个时区之间的差异:\n\n```cpp\n#include <chrono>\n#include <iostream>\n\nusing namespace std;\nusing namespace std::chrono;\n\nint main()\n{\n    auto current = system_clock::now();\n    auto lon = zoned_time{\"Europe/London\", current_time};\n    auto newYork = zoned_time{\"America/New_York\", current_time};\n    cout <<\"Time Difference between London and New York:\" \n         << (lon.get_local_time() - newYork.get_local_time())\n             << endl;\n\n    return 0;\n}\n```\n\n让我们深入`std::chrono`日历部分，了解更多关于这个食谱的信息。\n\n# 它是如何工作的...\n\n在新的 C++ 20 标准中有很多日历和时区助手函数。这个食谱只是触及了表面，但仍然让我们理解了处理时间是多么容易。所有`std::chrono`日历和时区功能的参考可以在[https://en.cppreference.com/w/cpp/chrono](https://en.cppreference.com/w/cpp/chrono)找到。\n\n*第一步*使用`weekday`方法获取一周中的某一天(使用公历)。在调用`weekday`方法之前，我们需要得到一个具体的日期，用 C++ 20，我们只需要设置`auto bday = January/30/2021`，代表一个日期。现在，我们可以将其传递给`weekday`方法，以获得一周中的特定一天，在我们的情况下是周六。一个有用的特性是我们可以比较日期，就像我们可以比较`bday`和`anotherDay`变量一样。`weekday`和所有其他`std::chrono`日历方法一样，处理闰秒。\n\n*步骤 2* 显示了`year_month_day`和`year_month_day_last`方法的使用。这个库包含一整套类似于这两个的类，例如`month_day`和`month_day_lat`等等。他们显然有不同的范围，但他们的原则保持不变。在这一步中，我们有兴趣知道 2 月的最后一天。我们用`year_month_day{ floor<days>(system_clock::now()) }`在`today`变量中设置当前日期，然后将`today`传递给`year_month_day_last`方法，该方法将返回类似于`2020/02/last`的内容，并将其存储在`ymdl`变量中。我们可以再用`year_month_day`的方法得到二月的最后一天。我们可以跳过几个步骤，直接调用`year_month_day_last`方法。出于教育目的，我们执行了这一步骤。\n\n*第三步*进入时区范围。此步骤中的代码片段通过迭代`zone_names`数组打印时区列表。这里，我们首先通过遍历由字符串标识的每个时区来获得`localtime`。然后，我们使用`zoned_time`方法将`localtime`转换为由`name`变量标识的时区。\n\n在*第 4 步*中，我们讲述了一个有趣且反复出现的问题:寻找两个时区之间的时差。原则不变；我们仍然使用`zoned_time`方法获取两个时区的当地时间，在本例中是`\"America/New_York\"`和`\"Europe/London\"`。然后，我们减去两个本地时间，得到差值。\n\n# 还有更多...\n\n`std::chrono`日历提供了各种各样的方法，欢迎您探索。完整列表可在[https://en.cppreference.com/w/cpp/chrono](https://en.cppreference.com/w/cpp/chrono)获得。\n\n# 请参见\n\n*   *c++ 之旅，第二版*，作者:比雅尼·斯特劳斯特鲁普，*第 13.7 章，时间*\n\n# 学习 Linux 定时\n\n在 C++ 11 之前，标准库不包含任何直接的时间管理支持，所以系统开发人员不得不使用*外部*源。外部，我们指的是外部库(例如，Boost([https://www.boost.org/](https://www.boost.org/))或特定于操作系统的应用编程接口。我们认为系统开发人员有必要从 Linux 的角度理解时间的概念。本食谱将帮助您使用 POSIX 标准掌握**时钟**、**时间点**、**持续时间**等概念。\n\n# 怎么做...\n\n在这个食谱中，我们将编写一个程序，这样我们就可以从 Linux 的角度了解**时钟**、**时间点**和**持续时间**的概念。让我们开始吧:\n\n1.  在 shell 中，创建一个名为`linux_time_01.cpp`的新文件，并添加以下包含和函数原型:\n\n```cpp\n#include <iostream>\n#include <time.h>\n#include <vector>\n\nvoid timespec_diff(struct timespec* start, struct timespec* stop, struct timespec* result);\n```\n\n2.  现在，我们想看看`clock_gettime`通话中`CLOCK_REALTIME`和`CLOCK_MONOTONIC`的区别。我们需要定义两个`struct timespec`变量:\n\n```cpp\nint main ()\n{\n    std::cout << \"Starting ...\" << std::endl;\n    struct timespec tsRealTime, tsMonotonicStart;\n    clock_gettime(CLOCK_REALTIME, &tsRealTime);\n    clock_gettime(CLOCK_MONOTONIC, &tsMonotonicStart);\n```\n\n3.  接下来，我们需要打印`tsRealTime`和`tsMonoliticStart`变量的内容来看看区别:\n\n```cpp\n    std::cout << \"Real Time clock (i.e.: wall clock):\"\n        << std::endl;\n    std::cout << \" sec :\" << tsRealTime.tv_sec << std::endl;\n    std::cout << \" nanosec :\" << tsRealTime.tv_nsec << std::endl;\n\n    std::cout << \"Monotonic clock:\" << std::endl;\n    std::cout << \" sec :\" << tsMonotonicStart.tv_sec << std::endl;\n    std::cout << \" nanosec :\" << tsMonotonicStart.tv_nsec+\n        << std::endl;\n```\n\n4.  我们需要一个任务来监控，所以我们将使用一个`for`循环来填充一个`std::vector`。在这之后，我们立即得到一个时间点在`tsMonotonicEnd`变量:\n\n```cpp\n    std::vector <int> elements;\n    for (int i = 0; i < 100'000'000; ++ i)\n        elements.push_back(i);\n\n    struct timespec tsMonotonicEnd;\n    clock_gettime(CLOCK_MONOTONIC, &tsMonotonicEnd);\n```\n\n5.  现在，我们要打印任务的工期。为此，我们调用`timespec_diff`(辅助方法)来计算`tsMonotonicEnd`和`tsMonotonicStart`之间的差异:\n\n```cpp\n    struct timespec duration;\n    timespec_diff (&tsMonotonicStart, &tsMonotonicEnd, &duration);\n\n    std::cout << \"Time elapsed to populate a vector with\n        100M elements:\" << std::endl;\n    std::cout << \" sec :\" << duration.tv_sec << std::endl;\n    std::cout << \" nanosec :\" << duration.tv_nsec << std::endl;\n    std::cout << \"Finished ...\" << std::endl;\n}\n```\n\n6.  最后，我们需要实现一个辅助方法来计算`start`和`stop`变量表示的时间之间的时间差(即持续时间):\n\n```cpp\n// helper method\nvoid timespec_diff(struct timespec* start, struct timespec* stop, struct timespec* result)\n{\n    if ((stop->tv_nsec - start->tv_nsec) < 0) \n    {\n        result->tv_sec = stop->tv_sec - start->tv_sec - 1;\n        result->tv_nsec = stop->tv_nsec - start->tv_nsec\n          + 100'000'0000;\n    } \n    else \n    {\n        result->tv_sec = stop->tv_sec - start->tv_sec;\n        result->tv_nsec = stop->tv_nsec - start->tv_nsec;\n    }\n    return;\n}\n```\n\n前面的程序展示了如何收集时间点来计算事件的持续时间。现在，让我们深入了解这个项目的细节。\n\n# 它是如何工作的...\n\n首先，让我们编译并执行程序:\n\n![](img/f4a8718b-5a8b-46ac-a1d0-cd3365395fdf.png)\n\n我们可以立即注意到实时时钟(秒)比单调时钟(秒)大得多。通过做一些数学计算，你会注意到第一个大约是 49 年，第二个大约是 12 个小时。为什么会这样？第二个观察是，我们的代码花费了`1 second`和`644348500`纳秒来填充一个 1 亿个项目的向量。让我们收集一些见解来解释这一点。\n\n*第 1 步*只是增加了一些 includes 和我们写的计算时间差的原型。\n\n*步骤 2* 定义了两个变量`struct timespec tsRealTime`和`struct timespec tsMonotonicStart`，用于存储两个时间点。然后，我们通过传递`CLOCK_REALTIME`和`tsRealTime`变量两次调用`clock_gettime()`方法。我们通过用`tsMonotonicStart`变量传递`CLOCK_MONOTONIC`第二次这样做。`CLOCK_REALTIME`和`CLOCK_MONOTONIC`都是`clockid_t`型。当`clock_gettime()`被`CLOCK_REALTIME`调用时，我们得到的时间将是`wall-clock`时间(或实时)。\n\n这个时间点和`std::chrono::SYSTEM_CLOCK`有着相同的问题，我们在*学习关于 C++ 时间界面*的食谱中看到过。它可以调整(例如，如果系统时钟与 NTP 同步)，因此这不适合计算事件的经过时间(或持续时间)。当使用`CLOCK_MONOTONIC`参数调用`clock_gettime()`时，时间不会调整，大多数实现会从系统启动时就开始计时(即从机器启动时开始计时)。这非常适合事件持续时间的计算。\n\n*第三步*只是打印时间点的结果，即`tsRealTime`和`tsMonotonicStart`。我们可以看到第一个包含 1970 年 1 月 1 日 <sup>st</sup> 以来的秒数(大约 49 年)，而后者包含我的机器启动后的秒数(大约 12 小时)。\n\n*第四步*只需在一个`std::vector`中增加 1 亿个项目，然后在`tsMonotonicEnd`中获得另一个时间点，该时间点将用于计算该事件的持续时间。\n\n*步骤 5* 计算`tsMonotonicStart`和`tsMonotonicEnd`之间的差值，并通过调用`timespec_diff()`辅助方法将结果存储在`duration`变量中。\n\n*第六步*执行`timespec_diff()`方法，逻辑计算(`tsMonotonicEnd - tsMonotonicStart`)。\n\n# 还有更多...\n\n对于`clock_gettime()`方法，我们使用 POSIX 作为对应物集合方法:`clock_settime()`。`gettimeofday()` : `settimeofday()`同样有效。\n\n值得强调的是`gettimeofday()`是`time()`的扩展，返回一个`struct timeval`(即秒和微秒)。这种方法的问题是它可以调整。这是什么意思？假设你用`usegettimeofday()`得到事件发生前的一个时间点进行测量，然后得到事件发生后的另一个时间点进行测量。在这里，你会计算两个时间点之间的差异，认为一切都很好。这里可能会出现什么问题？想象一下，在您获取的两个时间点之间，**网络时间协议** ( **NTP** )服务器要求本地机器调整本地时钟，使其与时间服务器同步。计算的持续时间不准确，因为事件发生后的时间点会受到 NTP 同步的影响。NTP 只是这方面的一个例子。本地时钟也可以用其他方式调整。\n\n# 请参见\n\n*   *学习 C++ 时间界面*配方，用于与 C++ 进行比较\n*   *Linux 系统编程，第二版*，*作者:*罗伯特·拉芙\n\n# 处理睡眠时间和超时\n\n在系统编程环境中，时间不仅仅包括测量事件持续时间或读取时钟的行为。也有可能让一个进程休眠一段时间。这个食谱将教你如何使用基于秒 *-* 的应用编程接口、基于微秒的应用编程接口和具有纳秒分辨率的`clock_nanosleep()`方法来让一个进程休眠。此外，我们将了解什么是时间超支，以及如何最大限度地减少它们。\n\n# 怎么做...\n\n在这一节中，我们将编写一个程序来学习如何使用不同的 POSIX APIs 让程序进入睡眠状态。我们还将研究 C++ 替代方案:\n\n1.  打开一个 shell，创建一个名为`sleep.cpp`的新文件。我们需要添加一些稍后需要的标题:\n\n```cpp\n#include <iostream>\n#include <chrono>\n#include <thread>    // sleep_for\n#include <unistd.h>  // for sleep\n#include <time.h>    // for nanosleep and clock_nanosleep\n```\n\n2.  我们将使用`sleep()`方法和`std::chrono::steady_clock`类作为时间点来计算结束时的持续时间，从而使程序休眠`1`秒钟:\n\n```cpp\nint main ()\n{\n    std::cout << \"Starting ... \" << std::endl;\n\n    auto start = std::chrono::steady_clock::now();\n    sleep (1);\n    auto end = std::chrono::steady_clock::now();\n    std::cout << \"sleep() call cause me to sleep for: \" \n              << std::chrono::duration_cast<std::chrono::\n                  milliseconds> (end-start).count() \n              << \" millisec\" <<     std::endl;\n```\n\n3.  让我们看看`nanosleep()`是如何工作的。我们仍然使用`std::chrono::steady_clock`来计算持续时间，但是我们需要一个`struct timespec`。我们将使进程休眠大约`100`毫秒:\n\n```cpp\n    struct timespec reqSleep = {.tv_sec = 0, .tv_nsec = 99999999};\n    start = std::chrono::steady_clock::now();\n    int ret = nanosleep (&reqSleep, NULL);\n    if (ret)\n         std::cerr << \"nanosleep issue\" << std::endl;\n    end = std::chrono::steady_clock::now();\n    std::cout << \"nanosleep() call cause me to sleep for: \" \n              << std::chrono::duration_cast<std::\n                  chrono::milliseconds> (end-start).count() \n              << \" millisec\" << std::endl;\n```\n\n4.  让进程进入睡眠状态的一种更高级的方法是使用`clock_nanosleep()`，它允许我们指定一些有趣的参数(更多细节见下一节):\n\n```cpp\n    struct timespec reqClockSleep = {.tv_sec = 1, \n        .tv_nsec = 99999999};\n    start = std::chrono::steady_clock::now();\n    ret = clock_nanosleep (CLOCK_MONOTONIC, 0,\n        &reqClockSleep, NULL);\n    if (ret)\n        std::cerr << \"clock_nanosleep issue\" << std::endl;\n    end = std::chrono::steady_clock::now();\n    std::cout << \"clock_nanosleep() call cause me to sleep for: \" \n              << std::chrono::duration_cast<std::chrono::\n                  milliseconds> (end-start).count() \n              << \" millisec\" << std::endl;\n```\n\n5.  现在，让我们看看如何使用 C++ 标准库(通过`std::this_thread::sleep_for`模板方法)让当前线程进入睡眠状态:\n\n```cpp\n    start = std::chrono::steady_clock::now();\n    std::this_thread::sleep_for(std::chrono::milliseconds(1500));\n    end = std::chrono::steady_clock::now();\n    std::cout << \"std::this_thread::sleep_for() call\n      cause me to sleep for: \" \n              << std::chrono::duration_cast<std::chrono::\n                  milliseconds> (end-start).count() \n              << \" millisec\" << std::endl;\n    std::cout << \"End ... \" << std::endl;\n}\n```\n\n现在，让我们更详细地看一下这些步骤。\n\n# 它是如何工作的...\n\n该程序将以四种不同的方式进入休眠状态。让我们看看运行时:\n\n![](img/a3c3be5d-eca0-4e1b-97a5-1d4492bd48a0.png)\n\n*步骤 1* 只包含我们需要的标题:`<iostream>`为标准输出和标准误差(`cout`和`cerr`)，`<chrono>`为将用于测量实际睡眠的时间点，`<thread>`为`sleep_for`方法，`<unistd>`为`sleep()`，而`<time.h>`为`nanosleep()`和`clock_nanosleep()`。\n\n*第二步*使用`sleep()`方法使流程休眠`1`秒。我们使用`steady_clock::now()`来获得时间点，`duration_cast`来投射差异并获得实际持续时间。准确地说，`sleep()`如果进程至少已经成功休眠了指定的时间，则返回`0`，但是它可以返回一个介于 0 和指定秒之间的值，该值将代表**而不是**休眠的时间。\n\n*第三步*展示如何使用`nanosleep()`让一个进程进入睡眠状态。我们决定使用这种方法，因为`usleep()`在 Linux 上已经被否决了。`nanosleep()`比`sleep()`有优势，因为它有纳秒级的分辨率，`POSIX.1b`是标准化的。`nanosleep()`成功返回`0`，出错返回`-1`。它通过将`errno`全局变量设置为发生的特定错误来实现这一点。`struct timespec`变量包含`tv_sec`和`tv_nsec`(秒和纳秒)。\n\n*第四步*使用了更复杂的`clock_nanosleep()`。这个方法包含两个我们还没有看到的参数。第一个参数是`clock_id`并接受`CLOCK_REALTIME`和`CLOCK_MONOTONIC`，这是我们在前面的食谱中看到的。根据经验，如果你睡到绝对时间(挂钟时间)，你想使用第一个，如果你睡到相对时间值，你想使用第二个。根据我们在前面的食谱中看到的内容，这是有意义的。\n\n第二个参数是标志；可以是`TIME_ABSTIME`也可以是`0`。如果第一个被通过，`reqClockSleep`变量将被视为绝对变量，但是如果`0`被通过，那么它将被视为相对变量。为了进一步澄清绝对时间的概念，它可能来自于之前对`clock_gettime()`的调用，该调用将绝对时间点存储在一个变量中，比如`ts`。加上`2`秒，我们就可以把`&ts`(也就是变量`ts`的地址)传给`clock_nanosleep()`，等到那个特定的绝对时间。\n\n*第五步*让进程的当前线程休眠 1.5 秒(本例中当前线程为主线程，所以整个进程都会休眠)(1500 毫秒= 1.5 秒)。`std::this_thread::sleep_for`简单有效。它是一个模板方法，接受一个参数作为输入；也就是`duration`，需要表示类型和周期(`_Rep`、`_Period`)，就像我们在*学习 C++ 时间界面*菜谱中看到的一样。在这种情况下，我们只传递了以毫秒为单位的周期，并使表示处于默认状态。\n\n这里有一个问题我们要注意:时间****超限**。我们在本食谱中使用的所有界面都保证了该过程将休眠*至少与请求的时间一样长*。否则他们会返回一个错误。出于不同的原因，他们的睡眠时间可能比我们要求的时间稍长。一个原因可能是调度程序选择了不同的任务来运行。当计时器的粒度大于请求的时间时，就会出现此问题。例如，想想计时器显示的时间(`10msec`)以及睡眠时间为`5 msec`。我们可能会遇到这样的情况，流程必须比预期多等待`5`毫秒，这是 100%长。通过使用支持高精度时间源的方法，如`clock_nanosleep()`、`nanosleep()`和`std::this_thread::sleep_for()`，可以缓解时间超限。**\n\n **# 还有更多...\n\n我们没有明确提到`nanosleep()`和`clock_nanosleep()`的线程含义。这两种方法都会导致当前线程休眠。在 Linux 上休眠意味着线程(或者进程，如果是单线程应用)将进入**不可运行**状态，这样 CPU 就可以继续执行其他任务(请记住 Linux 并不区分线程和进程)。\n\n# 请参见\n\n*   *学习 C++ 时间界面*食谱复习`std::chrono::duration<>`模板类\n*   *学习 Linux 定时*食谱，回顾**实时**和**单调**的概念**"
  },
  {
    "path": "docs/cpp-sys-prog-cb/10.md",
    "content": "# 十、管理信号\n\n信号是软件中断。它们提供了一种管理异步事件的方法，例如，终端用户键入中断键或另一个进程发送必须管理的信号。每个信号都有一个以`SIG`开头的名字(例如`SIGABRT`)。本章将教你如何编写代码来正确管理软件中断，Linux 为每个信号定义的默认动作是什么，以及如何覆盖它们。\n\n本章将涵盖以下食谱:\n\n*   了解所有信号及其默认动作\n*   学习如何忽略信号\n*   学习如何捕捉信号\n*   学习如何向另一个进程发送信号\n\n# 技术要求\n\n为了让您立即尝试本章中的程序，我们建立了一个 Docker 映像，它包含了我们在整本书中需要的所有工具和库，它基于 Ubuntu 19.04。\n\n要进行设置，请执行以下步骤:\n\n1.  从[www.docker.com](http://www.docker.com)下载并安装 Docker 引擎。\n2.  从 Docker Hub 中拉出图像:`docker pull kasperondocker/system_programming_cookbook:latest`。\n3.  图像现在应该可以使用了。输入以下命令查看图像:`docker images`。\n4.  你现在至少应该有这个形象:`kasperondocker/system_programming_cookbook`。\n\n5.  借助以下命令，使用交互式外壳运行 Docker 映像:`docker run -it --cap-add sys_ptrace kasperondocker/system_programming_cookbook:latest /bin/bash`。\n6.  运行容器上的外壳现已可用。使用`root@39a5a8934370/# cd /BOOK/`按章节获取所有开发的程序。\n\n`--cap-add sys_ptrace`参数是允许 Docker 容器中的 GDB 设置断点所必需的，默认情况下，Docker 不允许设置断点。\n\n# 了解所有信号及其默认动作\n\n这个食谱将向您展示 Linux 支持的所有信号和相关的默认操作。我们还将了解为什么信号是一个重要的概念，以及 Linux 对软件中断做了什么。\n\n# 怎么做...\n\n在本节中，我们将列出我们的 Linux 发行版支持的所有信号，以便能够描述*中最常见的信号是如何工作的...*段。\n\n在 shell 中，键入以下命令:\n\n```cpp\nroot@fefe04587d4e:/# kill -l\n```\n\n如果您在基于 Ubuntu 版发行版的图书 Docker 映像上运行此命令，您将获得以下输出:\n\n```cpp\n 1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP\n 6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1\n11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM\n16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP\n21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ\n26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR\n31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3\n38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8\n43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13\n48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12\n53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7\n58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2\n63) SIGRTMAX-1 64) SIGRTMAX\n\n```\n\n在下一节中，我们将了解一个进程可以接收的最常见信号的默认动作是什么，每个信号的描述，以及 Linux 如何管理这些软件中断。\n\n# 它是如何工作的...\n\n在*第 1 步中，*我们执行`kill -l`命令来获取当前 Linux 发行版支持的所有信号。下表提供了带有默认操作和描述的最常见信号列表:\n\n| **信号** | **描述** | **默认动作** |\n| `SIGHUP` | 控制进程的终端已关闭(例如，用户已注销？) | 结束的 |\n| `SIGABRT` | `abort()`发出的信号 | 终止(如果可能，使用核心转储) |\n| `SIGSEGV` | 无效的内存引用 | 终止(如果可能，使用核心转储) |\n| `SIGSYS` | 错误的系统调用或进程试图执行无效的系统调用。 | 终止(如果可能，使用核心转储) |\n| `SIGINT` | 键盘产生的中断(例如 *Ctrl* + *C* ) | 结束的 |\n| `SIGQUIT` | 键盘生成的退出(例如: *Ctrl* + */* ) | 终止(如果可能，使用核心转储) |\n| `SIGPIPE` | 一个进程试图写入管道，但没有读取器 | 结束的 |\n| `SIGILL` | 一个进程试图执行非法指令 | 终止(如果可能，使用核心转储) |\n| `SIGALRM` | `alarm()`发出的信号 | 结束的 |\n| `SIGSTOP` | 停止进程 | 停止进程 |\n| `SIGIO` | 异步输入/输出事件 | 结束的 |\n| `SIGTRAP` | 断点被捕获 | 结束的 |\n| `SIGTERM` | 终止信号(可捕捉) | 结束的 |\n| `SIGKILL` | 流程终止(不可捕捉) | 结束的 |\n\n对于发送给进程的每个信号，Linux 应用其默认动作。当然，系统开发人员可以通过在过程中实现期望的动作来覆盖这个动作，正如我们将在*学习如何捕捉信号*配方中看到的。\n\n信号在`<signal.h>`头文件中定义，只是正整数，有意义的名称总是以`SIG` 字为前缀。当一个信号(也就是软件中断)被发出时，Linux 会做什么？简单地说，它总是应用相同的顺序生命周期，如下所示:\n\n1.  信号是由另一个进程的用户发出的，或者是由 Linux 本身发出的。\n2.  信号被存储起来，直到 Linux 能够传送它。\n3.  交付后，Linux 会执行以下特定操作之一:\n    1.  忽略信号:我们已经看到有不可忽略的信号(例如`SIGKILL`)。\n    2.  执行默认操作:您可以参考上表的第 3 列。\n    3.  用注册的函数(系统开发人员实现的)处理信号。\n\n# 还有更多...\n\n在`<signal.h>`头文件中描述和定义的所有信号都符合 POSIX。这意味着每个标识符、它们的名称和默认动作都是由 POSIX.1-2003 标准定义的，Linux 遵循该标准。这保证了应用中`signals`实现或支持的可移植性。\n\n# 请参见\n\n*   学习如何捕捉信号食谱\n*   学习如何忽略信号食谱\n*   学习如何向另一个过程发送信号配方\n*   [第 3 章](03.html)、*处理进程和线程*刷新进程和线程。\n\n# 学习如何忽略信号\n\n可能有些情况下我们只需要忽略一个特定的信号。不过，放心，不可忽视的信号很少，比如`SIGKILL`(不可查)。这个食谱将教你如何忽略一个可捕捉的信号。\n\n# 怎么做...\n\n要忽略可捕捉的信号，请执行以下步骤:\n\n1.  在 shell 中，打开一个名为`signal_ignore.cpp`的新源文件，并通过添加以下代码开始:\n\n```cpp\n#include<stdio.h>\n#include<signal.h>\n#include <iostream>\n\nint main()\n{\n    std::cout << \"Starting ...\" << std::endl;\n    signal(SIGTERM, SIG_IGN);\n    while (true) ;\n    std::cout << \"Ending ...\" << std::endl;\n    return 0;\n}\n```\n\n2.  在第二个程序(`signal_uncatchable.cpp`)中，我们希望看到一个*不可切换的*信号不能被*忽略*。为此，我们将使用我们在*学习所有信号及其默认动作*配方中看到的`SIGKILL`信号，该信号不可捕捉(即程序不能忽略):\n\n```cpp\n#include<stdio.h>\n#include<signal.h>\n#include <iostream>\n\nint main()\n{\n    std::cout << \"Starting ...\" << std::endl;\n    signal(SIGKILL, SIG_IGN);\n    while (true) ;\n    std::cout << \"Ending ...\" << std::endl;\n    return 0;\n}\n```\n\n下一部分将解释前面两个程序的细节。\n\n# 它是如何工作的...\n\n*步骤 1* 包含忽略`SIGTERM`信号的程序。我们通过调用`signal();`系统调用来做到这一点，方法是将特定信号作为第一个参数(`SIGTERM`)传递，将要遵循的动作作为第二个参数传递，在这种情况下，第二个参数是`SIG_IGN`，忽略它。\n\n*第二步*与*第一步*代码相同。我们只是使用了传递`SIGKILL`参数和`SIG_IGN`的`signal();`方法。换句话说，我们要求 Linux 忽略这个进程的`SIGKILL`信号(`signal_uncatchable.cpp`一旦构建并执行就会成为一个进程)。正如我们在*学习所有信号及其默认动作*配方中了解到的那样，`SIGKILL`是一个不可调谐的信号。\n\n现在让我们构建并运行这两个程序。我们期待看到的是第一个节目中忽略的`SIGTERM`信号和第二个节目中分别无法忽略的`SIGKILL`信号。第一个程序的输出如下:\n\n![](img/9d9e43c4-0a85-492b-ad1d-5bcd9eddc98d.png)\n\n在这里，我们使用`ps aux`检索流程的`PID`，并通过运行命令发送`SIGTERM`信号:`kill -15 115`(其中`15`代表`SIGKILL`)。如您所见，通过完全忽略终止该进程的信号，该进程一直在运行。\n\n第二个程序`signal_uncatchable.cpp`显示，即使我们指定捕捉`SIGKILL`信号，Linux 还是忽略了这一点，杀死了我们的进程。我们可以在下面的截图中看到这一点:\n\n![](img/2ff0fb88-d0b7-463d-a3b6-c1a69327f748.png)\n\n# 还有更多...\n\n要获得 Linux 机器上支持的所有信号的列表，`kill -l`命令非常有帮助，`man signal`包含成功将信号集成到程序中所需的所有细节。\n\n# 请参见\n\n*   学习[第 1 章](01.html)*中的*Linux 基础–外壳*食谱，了解如何在外壳上运行程序*\n*   *学习如何捕捉信号*食谱\n*   *学习如何向另一个过程发送信号*食谱\n*   *学习所有信号及其默认动作*配方\n*   [章节 3](03.html) *，处理进程和线程，*刷新进程和线程\n\n# 学习如何捕捉信号\n\n这个食谱将教你如何在程序中捕捉(或捕捉)信号。可能需要对特定信号执行一些操作。例如，当应用收到终止信号(`SIGTERM`)时，我们需要在退出前清理一些已使用的资源。\n\n# 怎么做...\n\n让我们编写一个应用，捕捉`SIGTERM`信号，打印一个字符串，并终止应用:\n\n1.  在一个 shell 中，创建一个名为`signal_trap.cpp`的新文件。除了其他报头之外，我们还需要包括`<signal.h>`，以便能够处理信号。我们还必须添加管理我们想要捕获的信号所需的原型。在`main`方法中，我们通过传递想要捕获的`SIGTERM`和用于管理它的方法来调用`signal()`系统调用:\n\n```cpp\n#include<stdio.h>\n#include<signal.h>\n#include <iostream>\n\nvoid handleSigTerm (int sig);\n\nint main()\n{\n    std::cout << \"Starting ...\" << std::endl;\n    signal(SIGTERM, handleSigTerm);\n    while (true);\n    std::cout << \"Ending ...\" << std::endl;\n    return 0;\n}\n```\n\n2.  我们需要定义`handleSigTerm()`方法(可以任意命名):\n\n```cpp\nvoid handleSigTerm (int sig)\n{\n    std::cout << \"Just got \" << sig << \" signal\" << std::endl;\n    std::cout << \"cleaning up some used resources ...\"\n        << std::endl;\n    abort();\n}\n```\n\n下一节将详细描述该程序。\n\n# 它是如何工作的...\n\n*步骤 1* 本质上定义了`main`方法。首先，我们需要`<signal.h>`表头。在`main`方法的定义中，中心部分是`signal()`系统调用，我们在这里传递我们想要诱捕的`SIGTERM`信号和我们想要被 Linux 调用的方法。这是一个值得强调的重要方面。`signal()`系统调用接受(作为第二个参数)一个指向系统开发人员必须定义的函数的指针，正如我们所做的那样。在内核中，当一个软件中断被引发时，Linux 将其发送到特定的进程，并且该方法将被调用(以回调的形式)。`signal()`方法的原型是这样的:\n\n```cpp\nvoid(*signal(int, void (*)(int)))(int);\n```\n\n*步骤 2* 定义了管理我们想要捕捉的`SIGTERM`信号的方法。这种简单的方法展示了一些有趣的东西。首先，这个方法是从`signal()`系统调用中调用的回调。其次，我们必须将其原型定义为`void (*)(int) `，即返回 void 并接受输入中的一个整数(它代表应用实际接收到的信号)。任何与这个原型不同的东西都会导致编译错误。\n\n现在，让我们构建并执行上一节中开发的程序:\n\n![](img/6f6b91f1-1a20-4bfb-b77b-33756494b1f5.png)\n\n我们构建并链接了`signal_trap.cpp`程序，并生成了`a.out`可执行文件。我们经营它；与流程相关的 PID 是`46`。在右边的外壳上，我们发送`SIGTERM`信号(标识符= `15`)到带有 PID `46`的进程。正如你在标准输出(左边的外壳)上看到的，这个过程捕捉到了信号，并调用了我们定义的方法`handleSigTerm()`。该方法在标准输出中打印一些日志，并调用`abort()`系统调用，向运行进程发送`SIGABORT`信号。在*学习所有信号及其默认动作*配方中可以看到，`SIGABORT`的默认动作是终止进程(并生成核心转储)。当然，根据您的需求，您可以使用它并以另一种更合适的方式终止该过程(例如，`exit()`)。\n\n# 还有更多...\n\n那么，当一个进程分叉(或执行)另一个进程时，信号会发生什么呢？下表将帮助您理解如何处理具有流程-子流程关系的信号:\n\n| **信号行为** | **工艺叉** | **流程执行** |\n| 默认 | 遗传的 | 遗传的 |\n| 忽略 | 遗传的 | 遗传的 |\n| 有把手的 | 遗传的 | 不是遗传的 |\n\n在这个阶段，当一个进程分叉另一个进程时，子进程基本上继承了父进程的所有行为，你不应该感到惊讶。当一个进程执行另一个任务(带有`exec`)时，它继承**默认行为**和**忽略行为**，但它不继承已实现的已处理方法。\n\n# 请参见\n\n*   学习如何忽略信号食谱\n*   *学习所有信号及其默认动作*配方\n*   学习如何向另一个过程发送信号配方\n*   [第 3 章](03.html)、*处理进程和线程，*刷新进程和线程\n\n# 学习如何向另一个进程发送信号\n\n可能会有这样的场景:一个进程需要向其他进程发送信号。这个食谱将教你如何通过动手实践来实现。\n\n# 怎么做...\n\n我们将编写一个程序，向正在运行的进程发送`SIGTERM`信号。我们将看到进程如预期的那样终止。在一个 shell 中，打开一个名为`signal_send.cpp`的新源文件。我们将使用系统调用`kill()`，它向`pid`指定的进程发送信号`sig`。程序接受一个输入参数，即终止程序的`pid`:\n\n```cpp\n#include<stdio.h>\n#include<signal.h>\n#include <iostream>\n\nint main(int argc, char* argv[])\n{\n    std::cout << \"Starting ...\" << std::endl;\n    if (argc <= 1)\n    {\n       std::cout << \"Process pid missing ...\" << std::endl;\n       return 1;\n    }\n    int pid = std::atoi(argv[1]);\n    kill (pid, SIGTERM);\n\n    std::cout << \"Ending ...\" << std::endl;\n    return 0;\n}\n```\n\n我们将使用在*中开发的`signal_trap.cpp`程序学习如何捕获信号*配方作为终止过程。下一节将深入讨论这里的代码细节。\n\n# 它是如何工作的...\n\n为了看到正确的行为，我们需要运行一个我们打算终止的进程。我们将运行`signal_trap.cpp`程序。让我们如下构建并运行`signal_send.cpp`程序:\n\n![](img/90e6729d-3779-4be7-a592-193d458d00a3.png)\n\n在这里，我们做了一些事情，如下所示:\n\n1.  我们已经构建了`signal_trap.cpp`程序并生成了`a.out`可执行文件。\n2.  运行`./a.out`。\n3.  在左边的外壳上，我们取了`a.out`流程的`pid`，也就是`133`。\n4.  我们已经将`signal_send.cpp`程序构建为`terminate`可执行文件。\n5.  我们用流程的变量`a.out`运行`./terminate`，我们想要终止:`./terminate 133`。\n6.  在右边的外壳上，我们可以看到`a.out`进程正确终止。\n\n*第一步*有几件事我们必须解释。首先，我们从命令行参数解析`pid`变量，转换成整数，然后保存到`pid`变量中。其次，我们通过将`pid`变量和`SIGTERM`信号传递给运行过程来调用`kill()`系统调用。\n\n`man 2 kill`: `int kill(pid_t pid, int sig);` \nThe `kill()` function sends the signal specified by `sig` to `pid`.\nFor System V compatibility, if the PID is negative (but not `-1`), the signal is sent to all of the processes whose process group IDs are equal to the absolute value of the process number. However, if the `pid` is 0, `sig` is sent to every process in the **invoking process's** process group.\n\n# 还有更多...\n\n为了向另一个(或多个)进程发送信号，发送进程必须具有适当的权限。简单来说，一个进程可以向另一个进程发送信号，如果当前用户拥有它的话。\n\n在某些情况下，进程必须向自己发送信号。在这种情况下，系统调用`raise()`执行以下任务:\n\n```cpp\nint raise (int signo);\n```\n\n注意最后一点，也是非常重要的一点:管理发出的信号的处理程序代码必须是可重入的。背后的基本原理是，进程可能处于任何处理的中间，因此处理程序在修改任何静态或全局数据时必须非常小心。如果操作的数据是在堆栈上分配的或者是在输入中传递的，那么函数就是**可重入的**。\n\n# 请参见\n\n*   学习如何捕捉信号食谱\n*   学习如何忽略信号食谱\n*   *学习所有信号及其默认动作*配方"
  },
  {
    "path": "docs/cpp-sys-prog-cb/11.md",
    "content": "# 十一、调度编排\n\n系统编程是关于与底层操作系统的交互。调度器是每个操作系统的核心组件之一，影响进程在处理器上的分配方式。最终，这是最终用户所关心的:流程平稳运行，并且优先于其他流程。本章将教你通过改变进程的策略、其`nice`值、实时优先级、处理器关联性以及实时进程如何产生**处理器来与调度程序交互所需的实用技能。**\n\n本章将涵盖以下食谱:\n\n*   学习设置和获取调度程序策略\n*   学习获取时间片值\n*   学习如何设定一个好的值\n*   学习如何生产处理器\n*   了解处理器关联性\n\n# 技术要求\n\n为了尝试本章中的程序，我们设置了一个 Docker 映像，其中包含了我们在本书中需要的所有工具和库。它基于 Ubuntu 19.04。\n\n要设置它，请按照下列步骤操作:\n\n1.  从[www.docker.com](https://www.docker.com/)下载安装 Docker 引擎。\n2.  从 Docker Hub 中拉出图像:`docker pull kasperondocker/system_programming_cookbook:latest`。\n3.  图像现在应该可以使用了。输入以下命令查看图像:`docker images`。\n\n4.  你应该有如下图像:`kasperondocker/system_programming_cookbook`。\n5.  借助`docker run -it --cpu-rt-runtime=95000 --ulimit rtprio=99 --cap add=sys_nice kasperondocker/system_programming_cookbook:latest /bin/bash`命令，用交互式外壳运行 Docker 图像。\n6.  运行容器上的外壳现已可用。使用`root@39a5a8934370/# cd /BOOK/`获取为本书开发的所有程序。\n\n需要`--cpu-rt-runtime=95000`、`--ulimit rtprio=99`和`--cap add=sys_nice`参数来允许用 Docker 编写的软件设置调度器参数。如果主机配置正确，软件不会有任何问题。\n\n**Disclaimer**: The C++ 20 standard has been approved (that is, technically finalized) by WG21 in a meeting in Prague at the end of February. This means that the GCC compiler version that this book uses, 8.3.0, does not include (or has very, very limited support for) the new and cool C++ 20 features. For this reason, the Docker image does not include the C++ 20 recipe code. GCC keeps the development of the newest features in branches (you have to use appropriate flags for that, for example, `-std=c++ 2a`); therefore, you are encouraged to experiment with them by yourself. So, clone and explore the GCC contracts and module branches and have fun.\n\n# 学习设置和获取调度程序策略\n\n在系统编程环境中，有些情况下某些进程必须以不同于其他进程的方式进行处理。我们所说的不同是指一个进程获得处理器时间或不同优先级的不同方式。系统程序员必须意识到这一点，并学习如何与调度程序的应用编程接口进行交互。本食谱将向您展示如何更改一个流程的**策略**，以满足不同的调度要求。\n\n# 怎么做...\n\n该配方将向您展示如何获取和设置流程的*策略*以及可以分配给它的限制。让我们开始吧:\n\n1.  在一个 shell 中，让我们打开一个名为`schedParameters.cpp`的新源文件。我们需要检查当前(默认)的流程策略是什么。为此，我们将使用`sched_getscheduler()`系统调用:\n\n```cpp\n#include <sched.h>\n#include <iostream>\n#include <string.h>\n#include <sys/types.h>\n#include <unistd.h>\n\nint main ()\n{\n    int policy = sched_getscheduler(getpid());\n    switch(policy) \n    {\n        case SCHED_OTHER: std::cout << \"process' policy = \n            SCHED_OTHER\" \n                                    << std::endl ; break;\n        case SCHED_RR: std::cout << \"process' policy = SCHED_RR\" \n                                 << std::endl; break;\n        case SCHED_FIFO: std::cout << \"process' policy = SCHED_FIFO\" \n                                   << std::endl; break;\n        default: std::cout << \"Unknown policy\" << std::endl;\n    }\n```\n\n2.  现在，我们要为`SCHED_FIFO`策略分配一个实时(`rt`)优先级。为了使代码可移植，我们从`sched_get_priority_min`和`sched_get_priority_max`API 获取最小值和最大值:\n\n```cpp\n    int fifoMin = sched_get_priority_min(SCHED_FIFO);\n    int fifoMax = sched_get_priority_max(SCHED_FIFO);\n    std::cout << \"MIN Priority for SCHED_FIFO = \" << fifoMin\n        << std::endl;\n    std::cout << \"MAX Priority for SCHED_FIFO = \" << fifoMax\n        << std::endl;\n\n    struct sched_param sched;\n    sched.sched_priority = (fifoMax - fifoMin) / 2;\n    if (sched_setscheduler(getpid(), SCHED_FIFO, &sched) < 0)\n        std::cout << \"sched_setscheduler failed = \" \n                  << strerror(errno) << std::endl;\n    else\n        std::cout << \"sched_setscheduler has set priority to = \"\n                  << sched.sched_priority << std::endl;\n```\n\n3.  我们应该能够检查分配了`sched_getscheduler()`功能的新`SCHED_FIFO`策略:\n\n```cpp\n    policy = sched_getscheduler(getpid());\n    std::cout << \"current process' policy = \" << policy << std\n        ::endl ;\n    return 0;\n} \n```\n\n下一节将详细描述前面的代码。\n\n# 它是如何工作的...\n\nPOSIX 标准定义了以下策略:\n\n*   `SCHED_OTHER`:正常的调度策略(即不针对实时进程)\n*   `SCHED_FIFO`:先进先出\n*   `SCHED_RR`:循环赛\n\n这里`SCHED_OTHER`是默认的，`SCHED_FIFO``SCHED_RR`是实时的。实际上，Linux 将`SCHED_NORMAL`、`SCHED_BATCH`、`SCHED_IDLE`定义为其他实时策略。这些在`sched.h`头文件中定义。\n\n*步骤 1* 调用`sched_getscheduler()`检查流程的当前策略。不出所料，默认为`SCHED_OTHER`。我们将输入传递给`getpid()`函数(`<unistd.h>`，该函数返回当前进程的 PID。`sched_getscheduler()`也接受`0`，这种情况下代表当前流程。\n\n*第二步*的目标是设置实时策略，用`sched_setscheduler()`功能优先当前进程。我们希望这个进程比机器上运行的正常进程具有更高的优先级。例如，考虑一个(软)实时应用，其中计算不能被中断，或者如果接收到软件中断，其处理不能被推迟。这些 Linux 盒子通常很少运行专用的进程。为此，要设置的策略是`SCHED_FIFO`，我们设置的优先级是当前系统上可以设置的最小值和最大值之间的中间值。总是建议用`sched_get_priority_max()`和`sched_get_priority_min()`函数检查这些值，以便编写可移植代码。需要强调的一点是`sched_setscheduler()`功能在内部设置`struct task_struct`的`rt_priority`字段。\n\n*步骤 3* 通过调用`sched_getscheduler()`功能检查`SCHED_FIFO`是否已正确设置，类似于*步骤 1* 中发生的情况。\n\n# 还有更多...\n\n`SCHED_FIFO`和`SCHED_RR`是 POSIX 定义并在 Linux 上实现的两个策略，在更适合实时软件的处理器上分配任务。让我们看看它们是如何工作的:\n\n*   `SCHED_FIFO`:当一个任务被这个策略返回时，它继续运行，直到它阻塞(例如，I/O 请求)，它让出处理器，或者一个更高优先级的任务抢占它。\n*   `SCHED_RR`:这与`SCHED_FIFO`的逻辑完全相同，但有一点不同:使用此策略调度的任务分配了一个时间片，以便任务继续运行，直到时间片到期或更高的任务抢占它或让出处理器。\n\n请注意，当`SCHED_OTHER`(或`SCHED_NORMAL`)实现抢先形式的多任务处理时，`SCHED_FIFO`和`SCHED_RR`是合作的(它们没有被抢先)。\n\nLinux 主调度器功能循环所有策略，对于每个策略，它要求下一个任务运行。它通过`pick_next_task()`功能来实现这一点，该功能由每个策略来实现。主调度器在`kernel/sched.c`中定义，它定义了`sched_class`结构。这表示必须定义和实施每个策略，以便所有不同的策略都能正常工作。让我们从图形层面来看一下:\n\n*   `kernel/sched.c`:定义`struct sched_class`并循环执行以下策略:\n    *   `kernel/rt.c`(针对`SCHED_FIFO`、`SCHED_RR`)设置`const struct sched_class rt_sched_class`具有特定的实时策略功能。\n    *   `kernel/fair.c`(对于`SCHED_NORMAL`或`SCHED_OTHER`)设置`const struct sched_class fair_sched_class`特定于公平调度程序的功能。\n\n观察 Linux 调度程序设计的一种方式是这样的:`kernel/sched.c`定义接口和接口下的特定策略。界面由`struct sched_class`结构表示。以下是`SCHED_OTHER`/`SCHED_NORMAL`(CFS 公平调度策略)的接口实现:\n\n```cpp\nstatic const struct sched_class fair_sched_class = {\n .next = &idle_sched_class,\n .enqueue_task = enqueue_task_fair,\n .dequeue_task = dequeue_task_fair,\n .yield_task = yield_task_fair,\n .check_preempt_curr = check_preempt_wakeup,\n .pick_next_task = pick_next_task_fair,\n .put_prev_task = put_prev_task_fair,\n\n#ifdef CONFIG_SMP\n .select_task_rq = select_task_rq_fair,\n .load_balance = load_balance_fair,\n .move_one_task = move_one_task_fair,\n .rq_online = rq_online_fair,\n .rq_offline = rq_offline_fair,\n .task_waking = task_waking_fair,\n#endif\n .set_curr_task = set_curr_task_fair,\n .task_tick = task_tick_fair,\n .task_fork = task_fork_fair,\n .prio_changed = prio_changed_fair,\n .switched_to = switched_to_fair,\n .get_rr_interval = get_rr_interval_fair,\n\n#ifdef CONFIG_FAIR_GROUP_SCHED\n .task_move_group = task_move_group_fair,\n#endif\n};\n```\n\n`SCHED_FIFO`、`SCHED_RR`策略的实时优先级范围为`[1, 99]`，而`SCHED_OTHER`优先级(称为`nice`)为`[-20, 10]`。\n\n# 请参见\n\n*   *学习如何设置一个好的值*配方，看看实时优先级与好的优先级是如何相关的\n*   *学习如何产生处理器*配方学习如何产生运行的实时任务\n*   *Linux 内核开发*，*第三版*，罗伯特·拉芙\n\n# 学习获取时间片值\n\nLinux 调度程序为任务分配处理器时间提供了不同的策略。*学习设置和获取调度程序策略*食谱显示了哪些策略可用以及如何更改它们。`SCHED_RR`策略，即循环策略，是用于实时任务的策略(使用`SCHED_FIFO`)。`SCHED_RR`策略为每个进程分配一个时间片。这个食谱将告诉你如何配置时间片。\n\n# 怎么做...\n\n在本食谱中，我们将编写一个小程序，通过使用`sched_rr_get_interval()`函数来获取循环时间片:\n\n1.  在一个新的 shell 中，打开一个名为`schedGetInterval.cpp`的新文件。我们必须包含调度程序功能的`<sched.h>`、`<iostream.h>`以登录到标准输出，以及`<string.h>`以使用`strerror`功能并将`errno`整数转换为可读字符串:\n\n```cpp\n#include <sched.h>\n#include <iostream>\n#include <string.h>\n\nint main ()\n{\n    std::cout << \"Starting ...\" << std::endl;\n```\n\n2.  为了获得循环间隔，我们必须为我们的进程设置调度器策略:\n\n```cpp\n    struct sched_param sched;\n    sched.sched_priority = 8;\n    if (sched_setscheduler(0, SCHED_RR, &sched) == -1)\n        std::cout << \"sched_setscheduler failed = \"\n            << strerror(errno) \n                  << std::endl;\n    else\n        std::cout << \"sched_setscheduler, priority set to = \" \n                  << sched.sched_priority << std::endl;\n```\n\n3.  现在，我们可以用`sched_rr_get_interval()`函数得到区间:\n\n```cpp\n    struct timespec tp;\n    int retCode = sched_rr_get_interval(0, &tp);\n    if (retCode == -1)\n    {\n        std::cout << \"sched_rr_get_interval failed = \" \n                  << strerror(errno) << std::endl;\n        return 1;\n    }    \n\n    std::cout << \"timespec sec = \" << tp.tv_sec \n              << \" nanosec = \" << tp.tv_nsec << std::endl;\n    std::cout << \"End ...\" << std::endl;\n    return 0;\n}\n```\n\n让我们看看这在引擎盖下是如何工作的。\n\n# 它是如何工作的...\n\n当一个任务获得具有`SCHED_RR`策略的处理器时，它拥有优先于`SCHED_OTHER`和`SCHED_NORMAL`任务的优先权，并被分配一个定义的时间片，该时间片继续运行直到时间片到期。较高优先级的任务会一直运行，直到它们明确地让出处理器或块。对于系统程序员来说，一个重要的因素是知道`SCHED_RR`策略的时间片。这很重要。如果时间片太大，其他进程可能会等待很长时间才能获得 CPU 时间，而如果时间片太小，系统可能会花费大量时间进行上下文切换。\n\n*步骤 1* 显示了程序其余部分所需的内容。`<iostream>`为标准输出，`<sched.h>`用于访问调度器功能，`<string.h>`用于`strerror()`功能。\n\n*第 2 步*非常重要，因为它为当前流程设定了`SCHED_RR`政策。大家可能已经注意到了，我们通过`0`作为第一个参数。这很好，因为`sched_setscheduler()`功能的手册页上说，*如果 pid 等于零，调用线程的策略将被设置*。\n\n*第三步*调用`sched_rr_get_interval()`功能。它接受两个参数:PID 和`struct timespec`。第一个是输入参数，第二个是输出参数，包含`{sec, nanoseconds}`形式的时间片。对于第一个参数，我们可以通过`getpid()`函数，返回当前流程的 PID。然后，我们简单地将标准输出记录到返回的时间片上。\n\n# 还有更多...\n\n`SCHED_RR`时间片从何而来？正如我们已经知道的，Linux 调度程序有不同的策略。它们都是在不同的模块中实现的:`SCHED_NORMAL`或`SCHED_OTHER`的`kernel/sched_fair.c`、`SCHED_RR`和`SCHED_FIFO`的`kernel/rt.c`。通过查看`kernel/rt.c`，我们可以看到`sched_rr_get_interval()`函数返回`sched_rr_timeslice()`变量，该变量在模块顶部定义。我们还可以看到，如果`sched_rr_timeslice()`是为`SCHED_FIFO`策略调用的，它会返回`0`。\n\n# 请参见\n\n*   学习如何产生处理器配方，作为停止运行任务而不是等待时间片的替代方案\n*   学习设置和获取调度程序策略配方\n*   *Linux 内核开发，第三版*，罗伯特·拉芙\n\n# 学习如何设定一个好的值\n\n`SCHED_OTHER` / `SCHED_NORMAL`策略实现了所谓的完全公平调度器(`CFS`)。这个食谱将向您展示如何为正常流程设置好值，以增加它们的优先级。我们将看到这个好的值被用来衡量一个进程的时间片。优先级不能和实时优先级混淆，实时优先级是针对`SCHED_FIFO`和`SCHED_RR`政策的。\n\n# 怎么做...\n\n在这个食谱中，我们将实现一个程序，增加一个过程的美好价值:\n\n1.  在一个 shell 中，打开一个名为`schedNice.cpp`的新源文件。我们需要添加一些包含，并通过传递我们想要为当前进程设置的值来调用`nice()`系统调用:\n\n```cpp\n#include <string.h>\n#include <iostream>\n#include <unistd.h>\n\nint main ()\n{\n    std::cout << \"Starting ...\" << std::endl;\n\n    if (nice(5) == -1)\n        std::cout << \"nice failed = \" << strerror(errno)\n            << std::endl;\n    else\n        std::cout << \"nice value successfully set = \" << std::endl;\n\n    while (1) ;\n\n    std::cout << \"End ...\" << std::endl;\n    return 0;\n}\n```\n\n在下一节中，我们将看到这个程序是如何工作的，以及如何使用`nice`值来影响任务进入处理器的时间。\n\n# 它是如何工作的...\n\n*第 1 步*基本调用`nice()`系统调用，将任务的静态优先级递增给定量。为了清楚起见，假设一个进程以`0`的优先级开始(这是`SCHED_OTHER`和`SCHED_NORMAL`策略的默认值)，连续两次调用`nice(5)`会将其静态优先级设置为`10`。\n\n让我们构建并运行`schedNice.cpp`程序:\n\n![](img/72ada2c0-3f65-42c4-9324-8ae514069606.png)\n\n在这里，我们可以看到，在左边，我们有我们的进程正在运行，在右边，我们已经运行了`ps -el`命令来获得正在运行的进程的良好值。我们可以看到`./a.out`流程现在的`nice`值为`5`。要赋予任务更高的优先级(然后是更低的`nice`值)，流程需要以 root 身份运行。\n\n# 还有更多...\n\n`struct task_struct`结构有三个值来表示任务优先级:`rt_prio`、`static_prio`和`prio`。我们在*学习设置和获取调度器策略*配方中讨论了`rt_prio`，并定义该字段代表实时任务的优先级。`static_prio`是`struct task_struct`字段，用于存储`nice`值，而`prio`包含实际任务优先级。`static_prio`越低，任务的`prio`值越高。\n\n可能有些情况下，我们需要在运行时设置进程的`nice`值。这种情况下我们应该使用的命令是`renice value -p pid`；例如，`renice 10 -p 186`。\n\n# 请参见\n\n*   学习如何产生处理器配方，作为停止运行任务而不是等待时间片的替代方案\n*   学习设置和获取调度程序策略配方\n\n# 学习如何生产处理器\n\n当使用实时调度策略之一(即`SCHED_RR`或`SCHED_FIFO`)调度任务时，您可能需要从处理器中让出任务(让出任务意味着让出 CPU，使其可用于其他任务)。正如我们在*学习设置和获取调度器策略*配方中所描述的，当一个任务用`SCHED_FIFO`策略调度时，它直到某个事件发生才离开处理器；也就是说，没有时间片的概念。该配方将向您展示如何产生具有`sched_yield()`功能的流程。\n\n# 怎么做...\n\n在这个配方中，我们将开发一个程序来产生当前的过程:\n\n1.  在 shell 中，打开一个名为`schedYield.cpp`的新源文件，并输入以下代码:\n\n```cpp\n#include <string.h>\n#include <iostream>\n#include <sched.h>\n\nint main ()\n{\n    std::cout << \"Starting ...\" << std::endl;\n\n    // set policy to SCHED_RR.\n    struct sched_param sched;\n    sched.sched_priority = 8;\n    if (sched_setscheduler(0, SCHED_RR, &sched) == -1)\n        std::cout << \"sched_setscheduler failed = \" \n                  << strerror(errno) \n                  << std::endl;\n\n   for( ;; )\n   {\n      int counter = 0;\n      for(int i = 0 ; i < 10000 ; ++ i)\n         counter += i;\n\n      if (sched_yield() == -1)\n      {\n         std::cout << \"sched_yield failed = \" \n                   << strerror(errno) << std::endl;\n         return 1;\n      }\n   }\n\n   // we should never get here ...\n   std::cout << \"End ...\" << std::endl;\n   return 0;\n}\n```\n\n在下一节，我们将描述我们的程序和`sched_yield()`是如何工作的。\n\n# 它是如何工作的...\n\n当对通过`SCHED_FIFO`或`SCHED_RR`调度的任务调用`sched_yield()`时，它被移动到具有相同优先级的队列的末尾，并且运行另一个任务。收益会导致上下文切换，因此应该在严格需要时谨慎使用。\n\n*第 1 步*定义程序，向我们展示如何使用`sched_yield()`。我们模拟了一种受中央处理器限制的进程，在这种进程中，我们定期检查以产生处理器。在此之前，我们必须将此流程的策略类型设置为`SCHED_RR`，优先级设置为`8`。如您所见，没有关于要产出的流程(PID)的信息，因此它假设当前任务将产出。\n\n# 还有更多...\n\n`sched_yield()`是用户空间应用可以使用的系统调用。Linux 通常调用`yield()`系统调用，这样做的好处是让进程保持`RUNNABLE`状态。\n\n# 请参见\n\n*   *学习设置和获取调度器策略*配方，以查看如何更改策略的类型\n*   *Linux 内核开发，* *第三版*，作者:罗伯特·拉芙\n\n# 了解处理器关联性\n\n在多处理器环境中，调度器必须处理多个处理器或内核上的任务分配。从 Linux 的角度来看，进程和线程是一回事；两者都由`struct task_struct`内核结构表示。可能需要强制两个或多个任务(即线程或进程)在同一处理器上运行，以通过避免缓存失效来利用例如缓存。这个食谱将教你如何在任务中设定*硬亲和力*。\n\n# 怎么做...\n\n在这个食谱中，我们将开发一个小软件，我们将强制它在一个中央处理器上运行:\n\n1.  在一个 shell 中，打开一个名为`schedAffinity.cpp`的新源文件。我们想要的是检查新创建的进程的相似性掩码。然后，我们需要准备`cpu_set_t`掩码，将中央处理器上的亲和力设置为`3`:\n\n```cpp\n#include <iostream>\n#include <sched.h>\n#include <unistd.h>\n\nvoid current_affinity();\nint main ()\n{\n    std::cout << \"Before sched_setaffinity => \";\n    current_affinity();\n\n    cpu_set_t cpuset;\n    CPU_ZERO(&cpuset);\nint cpu_id = 3;\n    CPU_SET(cpu_id, &cpuset);\n```\n\n2.  现在，我们准备调用`sched_setaffinity()`方法，并在 CPU 号`3`上强制当前任务的硬关联。为了检查关联性是否设置正确，我们还将打印掩码:\n\n```cpp\n    int set_result = sched_setaffinity(getpid(), \n                                       sizeof(cpu_set_t), \n                                       &cpuset);\n    if (set_result != 0) \n    {\n        std::cerr << \"Error on sched_setaffinity\" << std::endl;\n    }\n\n    std::cout << \"After sched_setaffinity => \";\n    current_affinity();\n    return 0;\n}\n```\n\n3.  现在，我们必须开发`current_affinity()`方法，它将只打印处理器的掩码:\n\n```cpp\n// Helper function\nvoid current_affinity()\n{\n    cpu_set_t mask;\n    if (sched_getaffinity(0, sizeof(cpu_set_t), &mask) == -1) \n    {\n        std::cerr << \"error on sched_getaffinity\";\n        return;\n    }\n    else\n    {\n        long nproc = sysconf(_SC_NPROCESSORS_ONLN);\n        for (int i = 0; i < nproc; i++) \n        {\n            std::cout << CPU_ISSET(i, &mask);\n        }\n        std::cout << std::endl;\n    }\n}\n```\n\n如果我们在一个不存在的 CPU(例如`cpu_id = 12`)上设置亲缘关系会发生什么？亲和掩码信息存储在内核的什么地方？我们将在下一节回答这些和其他问题。\n\n# 它是如何工作的...\n\n*第一步*做两件事。首先，它打印默认的相似性掩码。我们可以看到，该进程计划在所有处理器上运行。其次，它通过用`CPU_ZERO`宏初始化来准备代表一组 CPU 的`cpu_set_t`，并用`CPU_SET`宏在 CPU `3`上设置亲和力。请注意，`cpu_set_t`对象必须直接操作，但只能通过提供的宏操作。手册页上记录了宏的完整列表:`man cpu_set`。\n\n*步骤 2* 调用`sched_setaffinity()`系统调用，用`getpid()`函数返回的 PID 在进程上设置亲和度(在`mask`变量中指定，即`cpu_set_t`)。我们本可以通过`0`而不是`getpid()`，也就是目前的进程。`setaffinity`功能后，我们打印了 CPU 的掩码，验证新值是否正确。\n\n*步骤 3* 包含了我们用来将标准输出打印到 CPU 掩码上的辅助函数的定义。请注意，我们通过`sysconf()`系统调用并通过传递`_SC_NPROCESSORS_ONLN`获得可用处理器的数量。该功能检查`/sys/`文件夹中的系统信息。然后，我们遍历每个处理器并调用`CPU_ISSET`宏，同时通过`i-th`。`CPU_ISSET`宏将为`i-th`中央处理器设置相应的位。\n\n如果你试图修改`int cpu_id = 3`并通过一个不同的处理器，也就是一个不存在的处理器(例如`15`)，那么`sched_setaffinity()`函数显然会失败，返回`EINVAL`，亲和掩码保持不变。\n\n现在让我们来看看这个程序:\n\n![](img/7d5e58de-a8e5-4864-95ee-0d348ada14af.png)\n\n我们可以看到，每个处理器的 CPU 掩码都设置为 1。这意味着在这个阶段，可以在每个中央处理器上调度进程。现在，我们设置掩码，要求调度程序只在 CPU `3`上运行进程(**硬关联**)。当我们叫`sched_getaffinity()`的时候，面具反映了这一点。\n\n# 还有更多...\n\n当我们调用`sched_setaffinity()`系统调用时，我们要求调度程序在特定的处理器上运行一个任务。我们称之为硬亲和力。还有一种柔软的亲和力。这是由调度程序自动管理的。Linux 总是试图优化资源，避免缓存失效，以加快整个系统的性能。\n\n当我们通过宏设置亲和掩码时，我们基本上是在`task_struct`结构中设置`cpus_allowed`。这很有意义，因为我们在一个或多个处理器上设置了进程或线程的亲缘关系。\n\n如果要设置一个任务与多个 CPU 的亲缘关系，必须为要设置的 CPU 调用`CPU_SET`宏。\n\n# 请参见\n\n*   学习如何产生处理器配方\n*   学习获取时间片值配方\n*   学习设置和获取调度程序策略配方"
  },
  {
    "path": "docs/cpp-sys-prog-cb/README.md",
    "content": "# C++ 系统编程秘籍\n\n> 原书：[C++ System Programming Cookbook](https://libgen.rs/book/index.php?md5=8831DE64312A5D338410EC40C70FD171)\n> \n> 协议：[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)\n> \n> 阶段：机翻（1）\n> \n> 自豪地采用[谷歌翻译](https://translate.google.cn/)\n> \n> 让开源界提前感受入关。\n\n* [在线阅读](https://ccpp.apachecn.org)\n* [在线阅读（Gitee）](https://apachecn.gitee.io/apachecn-c-cpp-zh/)\n* [ApacheCN 学习资源](http://docs.apachecn.org/)\n\n## 贡献指南\n\n本项目需要校对，欢迎大家提交 Pull Request。\n\n> 请您勇敢地去翻译和改进翻译。虽然我们追求卓越，但我们并不要求您做到十全十美，因此请不要担心因为翻译上犯错——在大部分情况下，我们的服务器已经记录所有的翻译，因此您不必担心会因为您的失误遭到无法挽回的破坏。（改编自维基百科）\n\n## 联系方式\n\n### 负责人\n\n* [飞龙](https://github.com/wizardforcel): 562826179\n\n### 其他\n\n*   在我们的 [apachecn/apachecn-c-cpp-zh](https://github.com/apachecn/apachecn-c-cpp-zh) github 上提 issue.\n*   发邮件到 Email: `apachecn@163.com`.\n*   在我们的 [组织学习交流群](http://www.apachecn.org/organization/348.html) 中联系群主/管理员即可.\n\n## 赞助我们\n\n![](http://data.apachecn.org/img/about/donate.jpg)\n"
  },
  {
    "path": "docs/cpp-sys-prog-cb/SUMMARY.md",
    "content": "+   [C++ 系统编程秘籍](README.md)\n+   [零、前言](00.md)\n+   [一、系统编程入门](01.md)\n+   [二、重温 C++](02.md)\n+   [三、处理进程和线程](03.md)\n+   [四、深入探讨内存管理](04.md)\n+   [五、使用互斥、信号量和条件变量](05.md)\n+   [六、管道、先进先出、消息队列和共享内存](06.md)\n+   [七、网络编程](07.md)\n+   [八、处理控制台输入/输出和文件](08.md)\n+   [九、处理时间接口](09.md)\n+   [十、管理信号](10.md)\n+   [十一、调度编排](11.md)\n"
  },
  {
    "path": "docs/cpp-workshop/00.md",
    "content": "# 零、前言\n\n## 大约\n\n本节简要介绍本课程和软件要求，以便完成所有包含的活动和练习。\n\n# 关于书\n\n你已经知道你想学习 C++ 开发，学习 C++ 更聪明的方法是边做边学。*C++ 工作坊*专注于建立你的实践技能，这样你就可以用有效的现代 c++ 开发高性能的软件应用，甚至是你自己的独立游戏。你将从导致真实结果的真实例子中学习。\n\n在整个*C++ 研讨会*中，您将采取一种引人入胜的逐步方法来理解 c++ 代码。你不必耐着性子听完任何不必要的理论。如果时间紧迫，你可以每天跳一个练习，或者花一整个周末学习高级面向对象的原理。这是你的选择。按照你的方式学习，你会以一种令人满意的方式建立和加强关键技能。\n\n*c++ Workshop*的每一个物理打印副本都解锁了对交互版的访问。有了详细介绍所有练习和活动的视频，您将始终有一个有指导的解决方案。您还可以对照评估来评估自己，跟踪进度，并接收内容更新。完成后，您甚至可以获得一个可以在线共享和验证的安全凭据。这是一种额外的学习体验，包含在您的印刷本中。要兑换，请遵循 C++ 书籍开头的说明。\n\n快节奏直接，*C++ 工作坊*是 c++ 初学者的理想伴侣。您将像软件开发人员一样构建和迭代代码，一路学习。这个过程意味着你会发现你的新技能会坚持下去，并作为最佳实践嵌入其中。未来几年的坚实基础。\n\n## 关于章节\n\n*第 1 章**您的第一个 C++ 应用*将为您提供开始构建基本 C++ 应用所需的基本工具和技术。\n\n*第 2 章*、*控制流*，介绍了用于控制整个应用的执行流的各种工具和技术。\n\n*第 3 章*、*内置数据类型*，介绍了 C++ 提供的内置数据类型，包括它们的基本属性以及在向量和数组中的使用。然后在创建一个真实的注册应用时使用这些。\n\n*第 4 章*、*运算符、*介绍了 C++ 提供的各种运算符，描述了它们的作用以及它们如何允许我们操纵数据。\n\n*第 5 章*、*指针和引用、*介绍了 C++ 提供的各种运算符，描述了它们的作用以及它们如何允许我们操作数据。\n\n*第 6 章*、*动态变量*引入了动态变量——也就是说，可以在需要时创建的变量，可以保存任意大量的数据，这些数据只受可用内存的限制。\n\n*第 7 章*、*动态变量的所有权和生存期、*使得 C++ 程序中指针的使用更安全、更容易理解。\n\n*第 8 章*、*类和结构、*借助实例和练习介绍了结构和类的基础知识。\n\n*第 9 章*、*面向对象原则、*介绍了设计类的最佳实践，并将向您概述抽象和封装，在哪里使用它们，以及它们如何使您的定制 C++ 类型受益。\n\n*第 10 章*、*高级面向对象原则、*介绍了一些高级面向对象原则，包括继承和多态，这些原则将允许我们构建更加动态和强大的 C++ 应用。\n\n*第 11 章*、*模板、*涵盖了模板的概述，并给出了一些如何使用模板以及在哪里使用模板的例子，并教你如何实现模板类型和函数。\n\n*第 12 章*、*容器和迭代器*，提供了使用 C++ 标准库提供的容器和迭代器的概述。\n\n*第 13 章*、*异常处理*介绍了异常处理，这是 C++ 用于报告和恢复程序中意外事件的机制。\n\n## 惯例\n\n文本中的码字如下所示:`#include <typeinfo>`行通过`name()`函数给出了对传入类型名称的访问\n\n代码块设置如下:\n\n```cpp\n#include <iostream>\n#include <string.h>\nusing namespace std;\ntemplate<typename T>\nbool compare(T t1, T t2)\n{\n    return t1 == t2;\n}\n```\n\n新的术语和重要的词语是这样显示的:“在前面的章节中，介绍了**面向对象编程** ( **OOP** )以及示例和用例。”\n\n长代码片段被截断，GitHub 上代码文件的对应名称被放在截断代码的顶部。到整个代码的永久链接被放在代码片段的下面。它应该如下所示:\n\n```cpp\nExample09_1.cpp\n23     string getName() \n24     {\n25         return m_trackName;\n26     }\n27 \n28     void setName(string newTrackName) \n29     {\n30         // if S-Club is not found set the track name - otherwise do nothing \n31         if (newTrackName.find(\"S-Club\") == string::npos) \n32         {\n33             m_trackName = newTrackName;\n34          }\n35     }\n36 \n37     void setLength(float newTrackLength) \n38     {\n39         if (newTrackLength < MAX_TRACK_LENGTH && newTrackLength > 0) \n40         // no prog metal for us! \n41         {\n42             m_lengthInSeconds = newTrackLength;\n43         }\n44     }\nThe complete code for this example can be found at: https://packt.live/2DLDVQf\n```\n\n## 开始之前\n\n我们可以使用许多工具来编译我们的 C++ 程序，这里涉及的工具太多了，所以这里有一些建议和入门指南:\n\n## 在线编译器\n\n**cpp.sh** 是一个在线 C++ 编译器，也是作者在本书中广泛使用的一个。访问 [cpp.sh](http://cpp.sh) 并确保选项设置如下所示:\n\n![Figure 0.1: Screenshot of the cpp.sh online compiler ](img/C14195_Preface_01.jpg)\n\n图 0.1:CPP . sh 在线编译器截图\n\n这就是我们开始使用这个编译器所需要做的。只需写出代码并点击运行按钮。任何错误都将出现在编译选项卡中，交互式标准输入和输出将位于执行选项卡上。这里是在线 C++ 编译器的部分列表，您可以在练习时使用。如果你正在使用的一个变得迟钝，或者你根本找不到它，尝试另一个:\n\n**Tutorialspoint C++ 编译器**:这个网站允许你编译一个包含在单个文件中的 C++ 程序。它打印来自操作系统的错误消息。你可以在[https://www.tutorialspoint.com/compile_cpp_online.php](https://www.tutorialspoint.com/compile_cpp_online.php)找到它。\n\n**godbolt 编译器浏览器**:这个网站可以让你在很多不同的编译器上编译单个文件，并显示输出汇编语言；对于某些口味来说，它的 UI 有点微妙。它打印来自操作系统的错误消息。你可以在[https://godbolt.org/](https://godbolt.org/)找到它。\n\n**coliru** :这个网站允许你编译单个文件。它打印来自操作系统的错误消息。你可以在[http://coliru.stacked-crooked.com/](http://coliru.stacked-crooked.com/)找到它。\n\n**repl.it** :这个网站可以让你编译多个文件。你可以在[https://repl.it/languages/cpp](https://repl.it/languages/cpp)找到它。\n\n**Rextester** :这个网站让你用微软 Visual C++ 编译一个文件。你可以在[https://rextester.com/](https://rextester.com/)找到它。\n\n## 安装代码包\n\n在[https://github.com/PacktWorkshops/The-CPP-Workshop](https://github.com/PacktWorkshops/The-CPP-Workshop)从 GitHub 下载代码文件，并将其放入名为`C:\\Code`的新文件夹中。有关完整的代码包，请参考这些代码文件。\n\n如果您在安装或代码启动和运行方面遇到任何问题，请联系 workshops@packt.com 的团队。"
  },
  {
    "path": "docs/cpp-workshop/01.md",
    "content": "# 一、您的第一个 C++ 应用\n\n概观\n\n本章为您提供了开始构建基本 C++ 应用所需的基本工具和技术。我们将从将 C++ 应用分解为其核心组件开始，通过它们的角色来识别每个组件。然后，我们将看看定义 C++ 的核心语言，包括处理器前指令——允许我们在编译代码之前执行操作的语句。最后，在最后的练习中，我们将了解如何从我们的应用(输入/输出)中获取信息，然后将这些信息放在一起，您将在在线编译器中编写和运行自己的 C++ 应用。\n\n# 简介\n\n随着世界变得越来越智能，我们的设备也变得越来越智能。从手表到我们的冰箱，现在所有的东西都有能力运行代码，其中很大一部分是 C++。1972 年至 1973 年间，丹尼斯·里奇在贝尔实验室工作时创作了 C 语言程序。尽管由于低级内存访问等特性，C 语言的效率很高，但它是一种过程语言，因此不提供面向对象的特性。对此，Bjarne Stroustup 也在贝尔实验室工作时，于 1979 年开始研究“带类的 C”。1983 年，这种语言被改名为 C++，两年后的 1985 年，它迎来了第一次商业发布。此后，它经历了多次标准化，最后一次是在 2017 年 12 月，并继续由国际标准化组织管理。\n\n从操作系统到尖端的 3D 游戏引擎，C++ 被广泛应用于各种领域，是无数系统和行业的支柱，尤其是因为它的高性能、灵活性和可移植性。C++ 让您更接近硬件，因此它通常是性能关键型应用的首选工具。\n\n本课程的目标是揭开 C++ 编程语言的神秘面纱，并通过非常实用的方法让您尽快编写出高质量的代码。虽然理论当然是必需的，并且将在必要的地方被涵盖，但是我们将主要关注实际应用——通过处理真实世界的练习和活动来学习。\n\n为了开始我们的旅程，我们看了语言的简史。虽然仅仅这一点并不能让你成为一个更好的程序员，但是了解我们正在做什么以及为什么做总是好的。通过学习这门语言的起源及其在工业中的应用，我们将为自己的未来之旅建立一个明智的起点。\n\n然后，我们将直接开始剖析一个基本的 C++ 应用。通过将一个应用分解成它的组成部分，我们可以了解它包含的主要部分。然后，我们将通过在本章余下的介绍中更详细地查看每个部分来扩展这一基本理解。\n\n当我们结束这一章时，我们不仅会了解语言的起源；我们还将熟悉应用的不同核心部分。我们将能够带着一种意义感和理解感来看一个示例 C++ 应用。然后，我们将利用这一基本理解进入下一章，在这一章中，我们将更深入地研究语言的特定特性和功能。\n\n## c++ 的优势\n\n在我们深入研究 C++ 程序的结构之前，让我们先来看看这种语言的几个主要优点:\n\n*   **性能**:通过让程序员靠近硬件，C++ 让我们可以写出非常高效的程序。除了低级内存访问，代码和机器将做的事情之间的抽象比大多数其他语言都要小，这意味着您可以更好地操作系统。\n*   **可移植性** : C++ 可以交叉编译到各种各样的平台，从手表到电视都可以运行。如果您正在为多个平台编写应用或库，那么 C++ 将大放异彩。\n*   **通用** : C++ 是一种通用编程语言，从电子游戏到企业，无所不在。从直接内存管理到类和其他面向对象编程(OOP)原则，有了丰富的功能集，您可以让 C++ 为您服务。\n*   **大型库**:由于这种语言被用在了如此多的应用中，所以有大量的库可供选择。有数百个开源存储库，信息的财富(以及随之而来的支持系统)是巨大的。\n\n然而 C++ 是一把双刃剑，正如那句名言所说:*“权力大，责任大”*。C++ 给了你足够的空间去做伟大的事情，但如果使用不当也会给自己带来麻烦。Bjarne 自己也曾这样评价这种语言，*“C 让你很容易搬起石头砸自己的脚；C++ 让它变得更难，但当你这么做的时候，你的整个腿都会被炸掉。”*这并不是说无论如何都要避免 C++ 的使用，只是说要有意识、有考虑地使用它——这是下一章要传授的。\n\n# 剖析一个 C++ 应用\n\n有了对语言历史的简单了解，我们将开始我们的旅程，深入研究一个基本的 C++ 程序，看看我们正在使用什么。没有比你好世界更合适的开始了！。这个著名的程序把`Hello World!`的字样打印到控制台上，已经成为你们之前几十个程序员的起点。虽然是基本的，但它包含了 C++ 应用的所有关键组件，因此将为我们提供一个很好的示例来进行重构和学习。\n\n让我们先来看一下整个计划:\n\n```cpp\n// Hello world example.\n#include <iostream>\nint main()\n{\n    std::cout << \"Hello World!\";\n    return 0;\n}\n```\n\n这个小程序仅由七行代码组成，包含了我们了解 C++ 程序基本结构所需的一切。在接下来的章节中，我们将更详细地介绍这个程序的各个方面，所以当我们分解这个程序时，如果不是所有的事情都有完美的意义，请不要担心。这里的目的只是让我们熟悉一些核心概念，然后在我们前进的过程中更详细地介绍它们。\n\n从顶部开始，我们有一个**预处理器指令**:\n\n```cpp\n#include <iostream>\n```\n\n预处理器指令是允许我们在程序构建之前执行某些操作的语句。`include`指令是一个非常常见的指令，你会在大多数 C++ 文件中看到，它的意思是“复制到这里。”因此，在这种情况下，我们将把 **iostream 头文件**的内容复制到我们的应用中，这样做的时候，允许我们自己使用它提供的输入/输出功能。\n\n接下来，我们有了我们的切入点`main()`:\n\n```cpp\nint main()\n```\n\n`main()`函数是你的 C++ 应用开始的地方。所有应用都将定义这个函数，它标志着我们的应用的开始——将运行的第一个代码。这通常是您最外层的循环，因为一旦这个函数中的代码完成，您的应用就会关闭。\n\n接下来，我们有一个 IO 语句，它将向控制台输出一些文本:\n\n```cpp\n    std::cout << \"Hello World!\";\n```\n\n因为我们已经在应用的开始包含了`iostream`头，我们可以访问各种输入和输出功能。在这种情况下，`std::cout`。`cout`允许我们向控制台发送文本，所以当我们运行我们的应用时，我们看到文本`\"Hello World!\"`被打印。在接下来的章节中，我们将更详细地介绍数据类型。\n\n最后，我们有一个`return`声明:\n\n```cpp\n    return 0;\n```\n\n这表明我们已经完成了当前函数。您返回的值将取决于函数，但在这种情况下，我们返回`0`来表示应用运行没有错误。由于这是我们应用中唯一的功能，所以我们一返回它就会结束。\n\n这是我们的第一个 C++ 应用；没什么大不了的。从这里开始，天空是极限，我们可以构建像我们喜欢的那样大而复杂的应用，但是这里涵盖的基础将始终保持不变。\n\n看到这个应用被打印出来是一回事，但是让我们在第一个练习中运行它。\n\n## 练习 1:编译我们的第一个应用\n\n在本练习中，我们将编译并运行我们的第一个 C++ 应用。在本书的整个过程中，我们将使用在线编译器(这样做的原因将在本练习后解释)，但是现在，让我们启动并运行该编译器。执行以下步骤完成练习:\n\n注意\n\n这个练习的代码文件可以在这里找到:[https://packt.live/2QEHoaI](https://packt.live/2QEHoaI)。\n\n1.  Head to [cpp.sh](http://cpp.sh) and take a look around. This is the compiler that we'll be using. Once you go to the address, you should observe the following window:\n\n    ![Figure 1.1: C++ shell, the online compiler we'll be using ](img/C14195_01_01.jpg)\n\n    图 1.1: C++ 外壳，我们将使用的在线编译器\n\n    **选项**:这可以让我们更改各种编译设置。我们不会碰这个的。\n\n    **编译**:这显示了我们程序的状态。如果有任何编译问题，它们会显示在这里，这样我们就可以解决它们。\n\n    **执行**:这个窗口是我们的控制台，允许我们和应用进行交互。我们将在这里输入我们的值，并查看应用的输出。\n\n    对于我们的第一个程序，我们将运行我们在前面部分解构的“`Hello World!`”应用。\n\n2.  在代码窗口中输入以下代码，替换已经存在的所有内容，然后点击`Run` :\n\n    ```cpp\n    //Hello world example.\n    #include <iostream>\n    int main()\n    {\n        std::cout <<\"Hello World!\";\n        return 0;\n    }\n    ```\n\n可以看到，控制台现在包含了文字 **Hello World！**，意思是我们的程序运行没有问题:\n\n![Figure 1.2: Output of our \"Hello World\" program ](img/C14195_01_02.jpg)\n\n图 1.2:我们的“你好世界”程序的输出\n\n尝试将文本更改为独特的内容，然后再次运行该程序。\n\n注意\n\n这里是在线 C++ 编译器的部分列表，您可以在练习时使用。如果你正在使用的那个变得迟钝，或者你根本找不到它，试试另一个。在线编译器很有用，因为除了编程语言之外，它们将你必须学习的东西减少到几乎没有。\n\nTutorialspoint C++ 编译器:这个网站允许你编译一个包含在单个文件中的 C++ 程序。它打印来自操作系统的错误消息。你可以在[https://www.tutorialspoint.com/compile_cpp_online.php](https://www.tutorialspoint.com/compile_cpp_online.php)找到它。\n\ncpp.sh:这个网站允许你挑选一个 C++ 语言版本和警告级别，并编译一个单独的文件。但是，它不会打印来自操作系统的错误消息。你可以在[http://cpp.sh/](http://cpp.sh/)找到它。\n\ngodbolt 编译器浏览器:这个网站允许你在许多不同的编译器上编译一个文件，并显示输出汇编语言；对于某些口味来说，它的 UI 有点微妙。它打印来自操作系统的错误消息。你可以在[https://godbolt.org/](https://godbolt.org/)找到它。\n\n这个网站允许你编译一个文件。它打印来自操作系统的错误消息。你可以在[http://coliru.stacked-crooked.com/](http://coliru.stacked-crooked.com/)找到它。\n\n回复:这个网站允许你编译多个文件。你可以在[https://repl.it/languages/cpp](https://repl.it/languages/cpp)找到它。\n\n这个网站允许你使用微软的 Visual C++ 编译一个文件。你可以在[https://rextester.com/](https://rextester.com/)找到它。\n\n## C++ 构建管道 ine\n\n在我们继续之前，让我们花点时间讨论一下构建管道。这是将我们编写的代码转换成机器能够运行的可执行文件的过程。当我们编写 C++ 代码时，我们正在编写一组高度抽象的指令。我们的机器并不像我们一样阅读 C++ 本身，同样，当我们编写 C++ 文件时，它们也无法运行。它们首先必须被编译成可执行文件。这个过程由许多不连续的步骤组成，并且将我们的代码转换成一种更加机器友好的格式:\n\n*   **Preprocessor**: As the name implies, it runs through our code before it's compiled, resolving any preprocessor directives that we may have used. These include things such as `include` statements, which we saw previously, and others such as macros and defines that we'll look at later in this chapter.\n\n    我们的文件现在仍然是人类可读的。把预处理器想象成一个有用的编辑器，它将运行你的代码，完成你标记的所有小工作，为下一步——编译器——准备我们的代码。\n\n*   **编译**:编译器把我们人类可读的文件转换成计算机可以使用的格式——也就是二进制。这些都存储在以`.o`或`.obj`结尾的目标文件中，具体取决于平台。考虑一下我们之前剖析的小的`Hello World !`应用。所有这些代码都存在于一个文件 main.cpp 中。如果我们把它传递给编译器，我们会得到 main.o 一个目标文件，包含机器可以运行的源代码的二进制版本。这还没有完全准备好运行，您不能直接执行一个对象文件。在执行应用之前，我们需要查看管道的最后一步——链接器。\n*   **链接器**:链接器是产生我们的可执行文件的最后一步。一旦编译器将我们的源代码转换成二进制对象，链接器就会通过并将它们链接在一起，组成最终的可执行文件。\n\n上述步骤已在以下流程图中可视化:\n\n![Figure 1.3: The various step of compilation and linking ](img/C14195_01_03.jpg)\n\n图 1.3:编译和链接的各个步骤\n\n这三个步骤是每个 C++ 应用都要经历的，无论是单文件程序，比如“Hello World！”我们已经讨论过的程序，或者你可能在现实应用中看到的几千个文件的应用；这些基本步骤保持不变。\n\n不同的操作系统有不同的工具集来执行这些操作，覆盖它们不仅会分散编写 C++ 本身的注意力，还可能根据设置产生不同的体验，尤其是因为它们总是在变化。这就是为什么在这本书里我们将使用在线编译器。我们不仅可以直接开始编写代码，而且可以确定每个人都会有相同的结果。\n\n这些过程的概述有望提供一个坚实的基础概述，这样当您将来确实希望编译您的应用时，这个过程将是熟悉的，您将了解幕后发生了什么。\n\n# C++ 关键词\n\n**关键词**是 C++ 保留的词。因此，我们不能在我们的应用中使用它们，除了它们的预期目的。例如，一个常见的关键字是`if`，因此您将无法定义该名称的变量或函数。正是这些关键词构成了 C++ 语言，通过它们的使用，我们指导我们的程序应该做什么。\n\n语言中定义了许多关键词，在这个早期阶段覆盖所有关键词是没有必要的。相反，让我们看看在接下来的章节中我们会遇到的关键词。\n\n这些词有的定义基本类型，(`bool`、`char`、`int`等)，有的是定义程序流的语句(`if`、`else`、`switch`等)，有的定义对象和范围(`class`、`struct`、`namespace`等)。\n\n我们将在整本书中使用这些词，但现在我们只需要知道这些词是由 C++ 保留的。你可以分辨出来，因为大多数现代文本编辑器会突出这些单词，从而使它们脱颖而出。让我们看看如何在代码编辑器中区分关键词。遵守以下程序:\n\n```cpp\n// Keywords example.\n#include <iostream>\n#include <string>\nint main() \n{\n    // Data type keywords.\n    int myInt = 1;\n    double myDouble = 1.5;\n    char myChar = 'c';\n    bool myBool = true;\n    // Program flow keywords.\n    if (myBool) \n    {\n        std::cout << \"true\";\n    } \n    else \n    {\n        std::cout << \"false\";\n    }\n    struct myStruct \n    {\n        int myInt = 1;\n    };\n}\n```\n\n在编译器窗口中，前面的代码将如下所示:\n\n![Figure 1.4: Keywords and their highlighting ](img/C14195_01_04.jpg)\n\n图 1.4:关键词及其突出显示\n\n我们可以看到，这个程序中的关键词在编辑器中被赋予了特殊的呈现方式，通常是不同的颜色，来表示它们的状态。这在 IDEs 之间会有所不同。\n\n注意\n\nIDE 代表集成开发环境，是我们用来开发应用的软件。示例 ide 包括 Visual Studio 和 CLion。\n\n## 关键词示例\n\n没有必要逐个浏览每个关键词。我们将在后面介绍它们，但是我们可以快速了解一些常见的关键词组及其功能。\n\n类型关键字表示 C++ 提供的基本变量类型。这些包括`int`、`bool`、`char`、`double`和`float`:\n\n```cpp\n    int myInt = 1;\n    char myChar = 'a';\n    bool myBool = true;\n    double myDouble = 1.5;\n    float myFloat = 1.5f;\n```\n\n程序流关键字允许我们构建应用的逻辑。其中包括`if`、`else`、`then`和`switch`，如下图所示:\n\n```cpp\n    if (expression)\n    {\n        // do this\n    }\n    else\n    {\n        // do this instead.\n    }\n```\n\n访问修饰符决定了哪些其他类和组件可以看到和不能看到我们的 C++ 变量和函数。当构建类时(我们将很快看到)，我们有三种选择:`public`、`protected`和`private`。正确使用这些修饰符在构建健壮的系统中起着很大的作用，确保我们的数据和功能不会被滥用或危险地误用。这里有一个例子:\n\n```cpp\nclass MyClass()\n{\npublic:\n    int var1; // Accessible to the class, everything that can see MyClass.\nprotected:\n    int var2; // Accessible to the class, and any child classes.\nprivate:\n    int var3; // Accessible to the class only.\n}\n```\n\n修饰符类型改变了我们变量的属性。这些包括`const`、`static`、`signed`和`unsigned`。通过将这些放在变量和函数前面，我们可以改变它们在应用中的行为方式，如下例所示:\n\n```cpp\n    unsigned int var1 = 1;      // Unsigned means it can only be positive.\n    signed int var2 = -1;      // Signed can be both positive or negative.\n    const std::string var3 = \"Hello World\"; // Const means the value                                             // cannot be modified\n    static char var4 = 'c';     // Static means the value is shared                                 // between all instances of a given class.\n```\n\n# 预处理器指令\n\n这个术语我们已经见过几次了，让我们来看看它的意思。预处理器指令是在编译代码之前运行的语句。这对于一系列不同的事情非常有用，从头文件到选择性代码编译。\n\n## 包括\n\n最常见的指令之一`#include`，我们已经看过了；这里的意思是“T2”。“当预处理运行时，它会将包含文件的内容复制并粘贴到它的位置。这意味着，包含`include`指令的类现在也可以访问该标题中定义的任何函数、变量、类等。\n\n这个指令有两种变体:\n\n```cpp\n// Include example.\n// Version 1 - Generally for system files.\n#include <headerfile>\n// Version 2 - Generally for programmer files.\n#include \"headerfile\"\n```\n\n在`Version 1`中，您指导预处理器使用预定义的搜索路径来查找文件。这通常用于系统头，例如，这些路径可能由您的 IDE 设置；它们是实现定义的。\n\n在`Version 2`中，你指导预处理器在文件所在的本地开始搜索。这通常用于包含您自己的项目标题。如果搜索失败，它将使用与`Version 1`相同的路径。\n\n## 宏\n\n`#define/#undef`指令允许我们在程序中定义宏。宏的工作原理类似于`#include`语句，因为它替换了内容。您可以定义一个名称，在它后面加上一些数据或功能，然后每当您想要使用该代码时，您可以用它定义的名称来引用它。当预编译器运行时，它将简单地用这个定义的内容替换宏名的实例。\n\n宏的定义如下:\n\n```cpp\n#define name content\n```\n\n有了这个，前面代码中`name`的任何实例都将被`content`直接替换。让我们举一个简单的定义单词的例子:\n\n```cpp\n// Macro example 1 - Defining a value.\n#include <iostream>\n#include <string>\n#define HELLO_WORLD \"Hello World!\"\nint main()\n{\n    std::cout << HELLO_WORLD;\n}\n```\n\n有了我们的宏，我们的输出行直接相当于以下内容:\n\n```cpp\n    std::cout << \"Hello World!\";\n```\n\n如果我们在在线编译器中运行这段代码，我们可以看到这一点。如您所见，我们可以在任何想要使用该字符串的地方获得输出`Hello World!`，我们可以使用宏来代替。\n\n![Figure 1.5: Hello World output using macro ](img/C14195_01_05.jpg)\n\n图 1.5:使用宏的 Hello World 输出\n\n除了定义单个值，我们还可以定义功能，如下面的代码片段所示:\n\n```cpp\n// Macro example 2 - Defining functionality\n#include <iostream>\n#define MULTIPLY(a,b) (a * b)\nint main()\n{\n    std::cout << MULTIPLY(3, 4);\n}\n```\n\n![Figure 1.6: Using a macro to define multiply functionality ](img/C14195_01_06.jpg)\n\n图 1.6:使用宏定义乘法功能\n\n注意\n\n通过宏定义功能的一个显著好处是速度，因为它减少了函数调用的开销。然而，有一种更好的方法来实现这一点，那就是使用内联函数。\n\n一旦定义，就可以使用`#undef`指令来定义宏。这将删除分配给宏的值/功能。如果在任何地方调用此宏，将会出现错误，因为它不再包含有效值。\n\n我们可以通过第一个例子看到这一点。假设我们使用宏对`std::cout`进行了两次调用，但是在这两次调用之间，我们取消了宏的定义:\n\n```cpp\n// Macro example 3 – Undefined macro.\n#include <iostream>\n#include <string>\n#define HELLO_WORLD \"Hello World!\"\nint main()\n{\n    std::cout << HELLO_WORLD;\n    #undef HELLO_WORLD\n    std::cout << HELLO_WORLD;\n}\n```\n\n当我们这次运行代码时，您会期望什么行为？\n\n![Figure 1.7: Compilation error as 'HELLO_WORLD' is undefined ](img/C14195_01_07.jpg)\n\n图 1.7:编译错误，因为“HELLO_WORLD”未定义\n\n正如我们所看到的，第一次通话仍然正常。当编译器命中该行时，`HELLO_WORLD`仍然被定义。然而，当我们第二次调用时，`HELLO_WORLD`还没有定义，所以编译器抛出了一个错误。可以使用这种宏的一个例子是调试行为。您可以定义一个宏`DEBUG`，等于`1`，并在需要的地方使用它在应用中生成调试代码，在不需要的地方使用它`#undef`。\n\n当我们开始使用宏时，定义它们是至关重要的，所以让我们看看如何确保这一点。\n\n## 条件编译\n\n我们刚刚看到，如果我们试图使用一个没有定义的宏，编译器会抛出一个错误。值得庆幸的是，我们有`#ifdef` / `#endif`指令来帮助我们防止这种情况，让我们检查当前是否定义了给定值。\n\n如果我们举最后一个例子，我们得到了一个编译器错误，但是通过使用这些新的语句来防止这个错误，我们可以满足编译器，如下面的代码所示:\n\n```cpp\n// Macro example 4 – Ifdef macro.\n#include <iostream>\n#include <string>\n#define HELLO_WORLD \"Hello World!\"\nint main()\n{\n    #ifdef HELLO_WORLD\n        std::cout << HELLO_WORLD;\n    #endif\n    #undef HELLO_WORLD\n    #ifdef HELLO_WORLD\n        std::cout << HELLO_WORLD;\n    #endif\n}\n```\n\n如果我们修改我们的程序并运行前面的代码，我们可以看到编译器现在满足了，并将正确运行程序，完全跳过第二个输出:\n\n![Figure 1.8: Safeguarded against the use of an undefined macro ](img/C14195_01_08.jpg)\n\n图 1.8:防止使用未定义的宏\n\n这里发生的是`#ifdef` / `else`指令中的代码没有被编译到我们的最终程序中，如果当时没有定义指定的宏的话。我们还有可用的`#ifndef` 指令，用于检查该值是否未定义。这与`#ifdef`的用法相同，但明显返回相反的值；`true`当值未定义时，`false`如果是。\n\n可以想象，我们可以将这些用于很多事情，并且还有其他指令允许我们用任何`constant`表达式来做这件事，而不仅仅是检查某个东西是否被定义。这些是`#if`、`#else`和`#elif`。\n\n注意\n\n常量表达式只是一个表达式，它的值可以在编译时(在程序运行之前)确定。\n\n下面的程序展示了如何使用这些预处理器指令来操作编译到我们程序中的代码的例子:\n\n```cpp\n// Conditional compilation example.\n#include <iostream>\n#define LEVEL 3\nint main()\n{\n    #if LEVEL == 0\n        #define SCORE 0\n    #else\n    #if LEVEL == 1\n        #define SCORE 15\n    #endif\n    #endif\n    #if LEVEL == 2\n        #define SCORE 30\n    #elif LEVEL == 3\n        #define SCORE 45\n    #endif\n    #ifdef SCORE\n        std::cout << SCORE;\n    #endif\n}\n```\n\n这里，我们使用`LEVEL`宏的值来确定我们给`SCORE`宏什么值。让我们将这段代码复制到编译器中，看看它是如何工作的。改变`LEVEL`的值，看看这对输出有什么影响。\n\n注意\n\n如果我们使用`#if`和`#else`，每一个都需要自己匹配的对`#endif`的调用。`#elif`不是这样。\n\n![Figure 1.9: We can use macros to determine what code gets compiled ](img/C14195_01_09.jpg)\n\n图 1.9:我们可以使用宏来确定编译了什么代码\n\n正如我们所看到的，通过改变`LEVEL`的值，我们可以改变哪些代码最终被编译到我们的应用中。这在实践中的一个常见用途是编译特定于平台的东西。\n\n假设您有一个函数需要在 OSX 和 Windows 之间做一些稍微不同的事情。解决这个问题的一种方法是将每个函数定义包装在一个平台定义中，以便为每个平台编译正确的函数。以下是该功能的一个示例:\n\n![Figure 1.10: Using defines to run certain code based on OS ](img/C14195_01_10.jpg)\n\n图 1.10:使用定义运行基于操作系统的特定代码\n\n注意\n\n使用`#ifdef`时没有`#elif`的等价物。相反，我们只需链接`#ifdef` / `#endif` 语句。\n\n现在我们已经对预处理器指令有了一个基本的了解，我们将通过编写一个通过它们定义值的程序来应用我们所学的一些概念。\n\n## 练习 2:使用预处理器指令定义值\n\n在本练习中，我们将构建一个小应用，给考试分数一个字母等级。我们将在宏中定义分数阈值，并使用它们来分配分数:\n\n注意\n\n这个练习的完整代码可以在这里找到:[https://packt.live/2rZFyqB](https://packt.live/2rZFyqB)。\n\n1.  We'll start by including our `iostream` and string headers, and defining our grade macros:\n\n    ```cpp\n    // Preprocessor directives activity.\n    #include <iostream>\n    #include <string>\n    #define GRADE_C_THRESHOLD 25\n    #define GRADE_B_THRESHOLD 50\n    #define GRADE_A_THRESHOLD 75\n    ```\n\n    定义你认为合适的阈值。\n\n2.  Allow the user of the program to input their test score by typing in the following code:\n\n    ```cpp\n    int main()\n    {\n        int value = 0;\n        std::cout << \"Please enter test score (0 - 100): \";\n        std:: cin >> value;\n    ```\n\n    不要担心我们还没有涵盖我们将要使用的 IO 语句。我们接下来会报道他们。\n\n3.  Output the grade the user got based on their test score.\n\n    这是我们使用前面定义的值的地方。通过在宏中定义这些，我们可以在以后很容易地更新它们。这很好，因为它允许我们在一个位置修改阈值。因此，使用这些宏的所有地方都将被更新。使用以下代码来完成此操作:\n\n    ```cpp\n        if (value < GRADE_C_THRESHOLD) \n        {\n            std::cout << \"Fail\";\n        } \n        else if (value < GRADE_B_THRESHOLD) \n        {\n            std::cout << \"Pass: Grade C\";\n        } \n        else if (value < GRADE_A_THRESHOLD) \n        {\n            std::cout << \"Pass: Grade B\";\n        } \n        else \n        {\n            std::cout << \"Pass: Grade A\";\n        }\n    }\n    ```\n\n4.  完整的代码如下:\n\n    ```cpp\n    // Preprocessor directives activity.\n    #include <iostream>\n    #include <string>\n    #define GRADE_C_THRESHOLD 25\n    #define GRADE_B_THRESHOLD 50\n    #define GRADE_A_THRESHOLD 75\n    int main() \n    {\n        int value = 0;\n        std::cout << \"Please enter test score (0 - 100): \";\n        std::cin >> value;\n        if (value < GRADE_C_THRESHOLD) \n        {\n            std::cout << \"Fail\";\n        } \n        else if (value < GRADE_B_THRESHOLD) \n        {\n            std::cout << \"Pass: Grade C\";\n        } \n        else if (value < GRADE_A_THRESHOLD) \n        {\n            std::cout << \"Pass: Grade B\";\n        } \n        else \n        {\n            std::cout << \"Pass: Grade A\";\n        }\n    } \n    ```\n\n5.  Now let's run our program. If a user inputs a score between 1-100, we can provide them with a letter grade. For an input of **50**, you will obtain the following output:\n\n    ![Figure 1.11: Assigning a letter grade to a user's test score ](img/C14195_01_11.jpg)\n\n图 1.11:给用户的测试分数分配一个字母等级\n\n# 基本输入/输出语句\n\nI/O 代表**输入/输出**，是我们从程序中获取信息的方式。这可以采取多种形式，从通过键盘输入文本，到用鼠标点击按钮，再到加载文件，等等。在本章中，总的来说，我们将继续讨论文本输入/输出。为此，我们将使用`iostream`标题。\n\n在本节中，我们将直接从输入中读取，很少或没有数据验证。然而，在工作应用中，输入将被严格验证，以确保其格式正确。我们缺乏这一点严格来说只是为了举例。\n\n`iostream`头包含我们通过键盘与应用接口所需的一切，允许我们将数据输入和输出应用。这是通过`std::cin`和`std::cout`对象完成的。\n\n注意\n\n这里的`std::`前缀表示命名空间。这将在本书后面更深入地讨论，但目前我们只能知道他们习惯于分组代码。\n\n有几种方法可以从键盘上读取数据。首先，我们可以使用`std::cin`和提取操作符:\n\n```cpp\n    std::cin >> myVar\n```\n\n这将把您的输入放入`myVar`变量，并且对字符串和整数类型都有效。\n\n观察以下包含`std::cin`对象的代码:\n\n```cpp\n// Input example.\n#include <iostream>\n#include <string>\nint main()\n{\n    std::string name;\n    int age;\n    std::cout << \"Please enter your name: \";\n    std::cin >> name;\n    std::cout << \"Please enter you age: \";\n    std::cin >> age;\n    std::cout << name << std::endl;\n    std::cout << age;\n}\n```\n\n如果我们在编译器中运行这段代码，我们可以看到我们可以输入我们的详细信息，并将它们打印回我们:\n\n![Figure 1.12: Basic IO ](img/C14195_01_12.jpg)\n\n图 1.12:基本输入输出系统\n\n如果您试图输入一个带有空格的名称，您将遇到一个问题，即只捕获了第一个名称。这让我们对`std::cin`是如何工作的有了更多的了解；即当遇到终止字符(空格、制表符或新行)时，它将停止捕获输入。我们现在可以明白为什么只有我们的名字被正确地捕获了。\n\n知道提取`>>`操作符可以被链接也是有用的。这意味着以下两个代码示例是等效的:\n\n**例 1** :\n\n```cpp\n    std::cin >> myVar1;\n    std::cin >> myVar2;\n```\n\n**例 2** :\n\n```cpp\n    std::cin >> myVar1 >> myVar2;\n```\n\n为了避免遇到终止字符(如空格)时字符串被切断，我们可以使用`getline`函数将用户输入的全部内容拉进一个变量中。让我们使用这个函数更新我们的代码来获取用户名:\n\n```cpp\n    std::cout << \"Please enter your name: \";\n    getline(std::cin, name); \n```\n\n如果我们再次运行代码，我们现在可以看到我们可以在名字中使用空格，并且`getline()`将捕获整个输入。使用`getline()`更好，因为这意味着我们不必担心直接使用`cin`提取会带来的线路问题。\n\n![Figure 1.13: Using getline() to capture entire input ](img/C14195_01_13.jpg)\n\n图 1.13:使用 getline()捕获整个输入\n\n当我们使用`getline()`时，我们将用户的输入读入一个字符串，但这并不意味着我们不能用它来读取整数值。要将一个字符串值转换成它的整数等价物，我们有`std::stoi`函数。例如，字符串“`1`”将作为`int 1`返回。将其与`getline()`结合是解析整数输入的好方法:\n\n```cpp\n    std::string inputString = \"\";\n    int inputInt = 0;\n    getline(std::cin, inputString);\n    inputInt = std::stoi(inputString);\n```\n\n无论我们使用哪种方法，我们都需要确保正确处理字符串和数值。例如，也许我们有一些期望用户输入数字的代码:\n\n```cpp\n    int number;\n    std::cout << \"Please enter a number between 1-10: \";\n    std::cin >> number;\n```\n\n如果用户在这里输入一个字符串，也许他们输入`five`而不是输入数字，程序不会崩溃，但是我们的`number`变量不会被赋值。这是我们从用户那里获得输入时需要注意的事情。在我们尝试在程序中使用它之前，我们需要确保它的格式正确。\n\n输出文本就像调用`std::cout`一样简单，使用插入操作符`<<`来传递我们的数据。这将同时接受字符串和数值，因此以下两个代码片段都将按预期工作:\n\n```cpp\n    std::cout << \"Hello World\";\n    std::cout << 1;\n```\n\n与提取操作一样，插入操作符可以被链接以构建更复杂的输出:\n\n```cpp\n    std::cout << \"Your age is \" << age;\n```\n\n最后，当输出文本时，有时我们想要开始一个新行或者插入一个空白行。对此，我们有两个选择，`\\n`和`std::endl`。这两个都将结束当前行并移动到下一行。鉴于此，以下代码片段给出了相同的输出:\n\n```cpp\n    std::cout << \"Hello\\nWorld\\n!\";\n    std::cout << \"Hello\" << std::endl << \"World\" << std::endl << \"!\";endl\n```\n\n如前所述，还有其他类型的输入和输出与应用相关联；然而，大多数情况下，IO 将通过某种形式的 UI 得到促进。就我们的目的而言，这两个基本对象`std::cin` / `std::cout`就足够了。\n\n我们将在下一个练习中应用我们对`getline()`方法和`std::cin`、`std:cout`和`std::endl` 对象的知识。\n\n## 练习 3:阅读用户详细信息\n\n在本练习中，我们将编写一个应用，允许您输入全名和年龄。然后我们将把这些信息打印出来，格式化成完整的句子。执行以下步骤完成练习:\n\n注意\n\n这个练习的完整代码可以在这里找到:[https://packt.live/37qJdhF](https://packt.live/37qJdhF)。\n\n1.  Define the `firstName`, `lastName`, and `age` variables, which will hold our user's inputs, as shown in the following snippet:\n\n    ```cpp\n    // IO Exercise.\n    #include <iostream>\n    #include <string>\n    int main()\n    {\n        std::string firstName;\n        std::string lastName;\n        int age;\n    ```\n\n    注意\n\n    我们将在后面的章节中介绍数据类型，所以如果目前还不清楚这些变量类型的确切性质，请不要担心。\n\n2.  输入以下代码，要求用户输入自己的名字:\n\n    ```cpp\n        std::cout << \"Please enter your first name(s): \";\n        getline(std::cin, firstName);\n    ```\n\n3.  We'll do the same for surnames, again using `getline()` using the following snippet:\n\n    ```cpp\n        std::cout << \"Please enter your surname: \";\n        getline(std::cin, lastName);\n    ```\n\n    对于我们的最终输入，我们将允许用户输入他们的年龄。对此，我们可以直接使用`cin`，因为这是我们的最后一次输入，所以我们不需要担心终止行字符，我们期待一个单一的数值。\n\n4.  Type the following code to have the user input their age:\n\n    ```cpp\n        std::cout << \"Please enter your age: \";\n        std::cin >> age;\n    ```\n\n    注意\n\n    同样，这只是因为我们正在编写简单的示例程序，所以我们信任用户输入正确的数据，而不做任何验证。在生产环境中，所有用户输入数据在使用前都将经过严格验证。\n\n5.  最后，我们将把这些信息呈现给用户，利用链式插入来格式化完整的字符串和句子，代码如下:\n\n    ```cpp\n        std::cout << std::endl;\n        std::cout << \"Welcome \" << firstName << \" \" << lastName               << std::endl;\n        std::cout << \"You are \" << age << \" years old.\" << std::endl;\n    ```\n\n6.  完整的代码如下:\n\n    ```cpp\n    // IO Exercise.\n    #include <iostream>\n    #include <string>\n    int main() \n    {\n        std::string firstName;\n        std::string lastName;\n        int age;\n        std::cout << \"Please enter your first name(s): \";\n        getline(std::cin, firstName);\n        std::cout << \"Please enter your surname: \";\n        getline(std::cin, lastName);\n        std::cout << \"Please enter your age: \";\n        std::cin >> age;\n        std::cout << std::endl;\n        std::cout << \"Welcome \" << firstName << \" \" << lastName               << std::endl;\n        std::cout << \"You are \" << age << \" years old.\" << std::endl;\n    }\n    ```\n\n7.  Run our application now and test it with some data.\n\n    对于我们的测试数据(易小轩·多伊，年龄:30 岁)，我们获得以下输出:\n\n    ![Figure 1.14: A small application that allows users to input various details ](img/C14195_01_14.jpg)\n\n图 1.14:一个允许用户输入各种细节的小应用\n\n因此，随着本练习的完成，我们已经通过基本的输入输出系统整合了一个小程序，允许用户输入一些个人信息。我们现在将进入下一个主题——函数。\n\n# 功能\n\n**c++ 中的函数**将我们的代码封装成功能的逻辑单元。然后我们可以调用这些函数，而不是在整个项目中使用重复的代码。例如，考虑一个小应用，它询问用户的姓名，问候他们，然后将该姓名存储在列表中，如下面的代码片段所示:\n\n```cpp\n    // Get name.\n    std::cout << \"Please enter your name: \" << \"\\n\";\n    getline(std::cin, name);\n    std::cout << \"Welcome \" << name << \".\\n\";\n    names.push_back(name);\n```\n\n这是我们在应用的生命周期中可能要多次调用的代码，所以它是一个很好的函数候选。这样做的好处是，它减少了应用中的重复代码，为我们提供了一个可以维护代码和修复任何错误的地方。如果它在整个代码库中被复制，任何时候你想要升级它或者修复某个东西，你必须找到所有的实例并对每个实例执行。\n\n一个函数被分成两部分:一个**声明**和一个**定义**。在函数声明中，您声明了关于该函数如何工作的最基本信息，即函数将返回的值类型、函数名称和任何参数。函数行为的实际逻辑由定义决定。让我们分解一个函数声明。\n\n函数声明如下:\n\n```cpp\nreturn_type function_name(parameters);\n```\n\n*   **return_type** :这是您将从函数中返回的值的类型。如果不想返回任何内容，也可以返回`void`，一个 C++ 关键字。例如，如果您有一个将两个数字相加的函数，返回类型可能是`integer`。\n*   **函数名**:这是函数的名字，也是你在代码中引用它的方式。\n*   **参数**:这些是传递给函数的一组可选值。同样，以两个数字相加为例，您将有两个`integer`参数:您的第一个和第二个数字。\n\n这个声明通常和其他函数声明一起存在于头文件(`.h`)中，然后它们被定义在`.cpp`文件中。*这就是为什么我们经常看到#include 指令*。我们在头文件中声明对象的功能，然后实际定义它们在`.cpp`文件中的工作方式。我们通常将它们分成单独的文件，因为这样可以隐藏实现细节。通常情况下，头文件是公开的，所以我们可以看到对象的功能并使用它，但是该功能的确切实现是保密的。\n\n注意\n\n我们暂时不担心这个。因为我们在一个文件中工作，所以我们将同时定义和声明函数，而不是单独定义和声明。\n\n让我们回到前面的例子，我们可以获取允许用户输入姓名的代码片段，并在如下代码片段所示的函数中定义姓名:\n\n```cpp\nvoid GetNextName()\n{\n    std::string name;\n    std::cout << \"Please enter your name: \" << \"\\n\";\n    getline(std::cin, name);\n    std::cout << \"Welcome \" << name << \".\\n\";\n    names.push_back(name);\n}\n```\n\n现在，每次我们需要这个功能时，我们可以只调用这个函数。该函数提供了自己的变量`name`，供我们使用，但注意`names`变量是从主程序中使用的。这是可能的，因为它在函数的范围内。范围将在后面的章节中详细介绍，但是目前我们只能观察到`name`变量是在函数内部定义的，而`names`是在函数外部定义的。\n\n很容易想象，现在我们没有重复的代码，只有对同一个函数的多次调用，这要整洁得多。这使得我们的代码更易读、更易维护、更容易调试。重构代码的这个过程叫做重构。我们应该始终致力于编写易于维护、调试和扩展的代码，良好的结构在其中起着重要作用。\n\n## 通过值传递，通过引用传递\n\n函数参数是我们传递给函数的值。如果我们认为我们的函数是一个离散的功能，那么我们的参数允许我们给它运行所需的东西。将参数传递给函数有两种方式，一种是通过值传递，另一种是通过引用传递，了解两者的区别很重要。\n\n当我们通过值将一个参数传递给一个函数时，这意味着我们正在制作一个副本，并将使用它。可视化的最简单方法是编写一个小的测试应用。请遵守以下代码:\n\n```cpp\n// Pass by value-by-reference example.\n#include <iostream>\n#include <string>\nvoid Modify(int a)\n{\n    a = a - 1;\n}\nint main()\n{\n    int a = 10;\n    Modify(a);\n    std::cout << a;\n}\n```\n\n在这个简单的程序中，我们将一个数字定义为 10，将其传递给一个将从中减去 1 的函数，然后打印该值。因为我们从 10 开始，减去 1，所以可以合理地预计输出为 9。然而，当我们运行前面的代码片段时，我们获得了以下输出:\n\n![Figure 1.15: Passing by value means the change doesn't stick ](img/C14195_01_15.jpg)\n\n图 1.15:通过价值传递意味着变化不会持续\n\n### 为什么我们输出 10？\n\n因为当我们将`a`变量传递到函数中时，它是通过值传递的。该函数创建了`a`的本地副本，在本例中为 10，然后它对该值所做的任何事情都与我们传入的原始`a`值完全分离。\n\n通过引用传递与此相反，表示“实际处理这个变量；不要复制。”同样，这是最容易看到的行动。让我们对我们的代码进行以下修改:\n\n```cpp\nvoid Modify(int& a)\n```\n\n一个非常微妙的变化，但是我们在这里所做的是在函数中的`int`类型之后添加`&`。这个符号的意思是“的地址”我们在书中后面的章节会更详细地介绍记忆，所以我们在这里保持轻松，但实际上它的意思是，“不要复制；实际使用这个值。”\n\n让我们在做了这些更改后重新运行代码。\n\n![Figure 1.16: Since we're now passing by reference, the change does stick ](img/C14195_01_16.jpg)\n\n图 1.16:因为我们现在是通过引用传递的，所以变化确实存在\n\n通过价值或参考传递是一个需要理解的重要概念。如果您正在处理大对象，传递值可能会很昂贵，因为必须构建/解构临时对象。这是另一个主题，将在后面的章节中介绍。目前，去掉值可以通过值或引用传递的事实(正如我们在这里看到的)就足够了。我们稍后将在此基础上进行构建。\n\n## 功能过载\n\n编写函数来封装我们的行为是朝着创建通用和可维护的代码迈出的一大步。然而，我们可以做得更多；我们可以让他们超负荷工作。在这种情况下，重载意味着提供多个版本的函数。假设我们定义一个简单的函数来乘以两个数字:\n\n```cpp\nint Multiply(int a, int b)\n{\n    return a * b;\n}\n```\n\n这个函数的参数是类型`int`，那么如果我们想要乘以`float`类型或者`double`会发生什么呢？在这种情况下，它们会被转换成整数，我们会失去精度，这不是我们通常想要的。为了解决这个问题，我们可以提供函数的另一个声明，具有相同的名称，可以使用这些类型。我们的函数声明如下所示:\n\n```cpp\nint Multiply(int a, int b);\nfloat Multiply(float a, float b);\ndouble Multiply(double a, double b);\n```\n\n最棒的是我们不需要担心调用这个函数的正确版本。如果我们提供了正确的类型，编译器会自动为我们调用合适的函数。我们可以通过一个简单的测试看到这一点。我们可以为其中的每一个创建函数定义，并为每一个添加唯一的输出，这样我们就可以知道哪个被击中了。\n\n下面是一个如何做到这一点的例子:\n\n```cpp\n// Function overloading example.\n#include <iostream>\n#include <string>\nint Multiply(int a, int b)\n{\n    std::cout << \"Called the int overload.\" << std::endl;\n    return a * b;\n}\nfloat Multiply(float a, float b)\n{\n    std::cout << \"Called the float overload.\" << std::endl;\n    return a * b;\n}\ndouble Multiply(double a, double b)\n{\n    std::cout << \"Called the double overload.\" << std::endl;\n    return a * b;\n}\nint main()\n{\n    Multiply(3, 4);\n    Multiply(4.f, 6.f);\n    Multiply(5.0, 3.0);\n    return 0;\n}\n```\n\n在前面的代码中，我们有我们的重载函数和对它的三个调用，每个调用都有不同的类型。运行此应用时，将获得以下输出:\n\n![Figure 1.17: The compiler knows which version of the function to call ](img/C14195_01_17.jpg)\n\n图 1.17:编译器知道要调用哪个版本的函数\n\n正如我们所看到的，编译器知道调用哪个版本的函数，因为我们在每种情况下都匹配指定的参数类型。一个`multiply`函数有点多余，当然这是一个简单的用例，但是很好地展示了我们如何使我们的函数更加有用和灵活。\n\n实现这种灵活性的另一种方法是通过模板。不是为每种类型重载一个函数，而是用一个模板创建一个单一的、高度通用的函数版本，可以接受任何类型。模板将在后面的章节中介绍。\n\n## 默认参数\n\n另一个让我们的函数更灵活的方法是使用默认参数。这允许我们将一些参数设置为可选的，我们通过在声明中给它们一个默认值来实现，如下所示:\n\n```cpp\nreturn_type function_name(type parameter1, type parameter2 = default value);\n```\n\n现在可以通过两种方式调用该函数:\n\n```cpp\nfunction_name(value1, value2);\n```\n\n在这种情况下，两个参数值都正常传递到函数中:\n\n```cpp\nfunction_name(value1);\n```\n\n在这种情况下，由于省略了第二个参数，因此将使用默认值。能够提供默认参数使我们的函数能够更加灵活，但这是有限度的。一个函数的重点是巧妙地封装某个行为，所以我们不想让它变得如此灵活，以至于它开始负责多个行为。在这种情况下，最好创建一个新的离散函数。\n\n让我们用另一个练习快速看一下这个例子。\n\n## 练习 4:功能\n\n在本练习中，我们将定义并使用一个函数，该函数将输出两个数字中较大的一个。这个函数需要一个返回类型和两个参数。执行以下步骤完成练习:\n\n注意\n\n这个练习的完整代码可以在这里找到:[https://packt.live/346VDJv](https://packt.live/346VDJv)。\n\n1.  Declare the function, assigning its return type, name, and parameters:\n\n    ```cpp\n    #include<iostream>\n    int Max(int a, int b)\n    ```\n\n    正如我们之前看到的，如果我们纯粹在头文件中声明这个函数，我们会在它的末尾添加一个分号，并在其他地方定义它。然而，既然不是这样，我们就直接打开花括号并定义我们的功能。\n\n2.  定义函数的行为。我们想要返回具有最高值的数字，所以这个的逻辑很简单，如下例所示:\n\n    ```cpp\n    int Max(int a, int b)\n    {\n        if (a > b)\n        {\n            return a;\n        }\n        else\n        {\n            return b;\n        }\n    }\n    ```\n\n3.  现在我们需要做的就是从用户那里得到两个数字。本章前面我们已经介绍了 IO，所以我们应该对此感到满意:\n\n    ```cpp\n    int main()\n    {\n        int value1 = 0;\n        int value2 = 0;\n        std::cout << \"Please input number 1: \";\n        std::cin >> value1;\n        std::cout << \"Please input number 2: \";\n        std::cin >> value2;\n    ```\n\n4.  最后，我们需要向用户输出答案。我们之前也讨论过这个问题，但是这次，我们将调用我们的新函数，传递用户的号码:\n\n    ```cpp\n        std::cout << \"The highest number is \" << Max(value1, value2);\n    }\n    ```\n\n    ，而不是在我们的`cout`语句中使用变量\n5.  完整的代码如下:\n\n    ```cpp\n    // IO Exercise.\n    #include <iostream>\n    #include <string>\n    int Max(int a, int b) \n    {\n        if (a > b) \n        {\n            return a;\n        } \n        else \n        {\n            return b;\n        }\n    }\n    int main() \n    {\n        int value1 = 0;\n        int value2 = 0;\n        std::cout << \"Please input number 1: \";\n        std::cin >> value1;\n        std::cout << \"Please input number 2: \";\n        std::cin >> value2;\n        std::cout << \"The highest number is \" << Max(value1, value2);\n    }\n    ```\n\n6.  Run this in the compiler and test it with some numbers.\n\n    对于我们的测试用例(1 和 10)，我们获得了以下输出:\n\n    ![Figure 1.18: We can treat our function as its return type, in this case int, and output that value ](img/C14195_01_18.jpg)\n\n图 1.18:我们可以将函数视为其返回类型，在本例中为 int，并输出该值\n\n通过将我们的代码拉成这样的函数，我们能够从很少的代码中获得广泛的功能。不仅如此，通过将该功能本地化为单个功能，我们给自己一个单点故障，这更容易调试。理论上，我们还获得了一段可重用的代码，可以部署在任何地方。好的程序架构是一门艺术，一种随着时间和经验发展的技能。\n\n注意\n\n我说“理论上”是因为虽然在这种非常简单的情况下，代码可以很容易地移动和重用，但在更大的系统中，情况往往不是这样。即使是简单的功能最终也是如此根深蒂固地存在于系统中(并被依赖关系所束缚)，以至于不容易在其他地方重新使用它。\n\n分解了 C++ 应用的核心元素后，让我们看看如何从头开始编写我们自己的小应用，将我们在第一章中学到的一切付诸实践。\n\n## 活动 1:编写自己的 C++ 应用\n\n这项活动的目的是编写一个系统，询问用户的名字和年龄。用户将根据年龄分组，我们将使用宏来定义这些年龄段。我们将使用函数封装任何重复的功能，将用户的信息打印回给他们，以及他们分配的组(其名称也由您决定)。我们期望的结果将是一个小程序，能够将用户分类成组，如下面的截图所示:\n\n![Figure 1.19: Our program asked for the user's name and age, and assigned them to the appropriate group ](img/C14195_01_19.jpg)\n\n图 1.19:我们的程序询问用户的姓名和年龄，并将他们分配到适当的组\n\n在开始之前，请确保之前的所有练习都已完成，因为本练习将测试我们在本介绍性章节中介绍的许多主题。以下是完成活动的步骤:\n\n注意\n\n这个活动的代码可以在这里找到:[https://packt.live/2QD64k4](https://packt.live/2QD64k4)。\n\n1.  使用`#defines`定义你的年龄段阈值。\n2.  Define a name for each group using `#defines`.\n\n    提示:复习*练习 2* 、*用预处理器指令*定义值来完成这一步。\n\n3.  输出询问用户姓名的文本，并在变量中捕获响应。\n4.  输出询问用户年龄的文本，并在变量中捕获响应。\n5.  编写一个函数，接受年龄作为参数，并返回适当的组名。\n6.  Output the user's name and the group that they have been assigned to.\n\n    提示:复习*练习* *2* 和 *3* 完成第 4、5、6 步。\n\n这个小程序涉及到我们在这一章介绍的所有内容。我们使用预处理器语句来定义一些应用数据，使用 IO 语句来获取应用中的数据，并将代码整齐地封装在函数中。在继续之前，请随意花一些时间使用这个应用，并根据自己的需要进行扩展。\n\n注意\n\n这个活动的解决方案可以在第 514 页找到。\n\n# 总结\n\n在第一章中，我们了解了一些 C++ 的历史，涵盖了它在多个行业中的各种应用，并解构了一个示例程序。这使我们能够识别组成 C++ 应用的核心组件和概念。\n\n首先，我们讨论了这种语言的历史，看看它旨在解决的问题。有了这个上下文，我们解构了一个示例应用，确定了 C++ 应用的关键特性。\n\n现在确定了这些关键概念，我们开始更详细地研究每个概念。我们学习了一些常见的 C++ 关键字以及它们的作用。我们研究了预处理器指令，以及如何在编译代码之前使用它们来执行操作。然后，我们查看基本的 IO 语句，使用`std::cin`和`std::cout`从我们的应用中获取信息。最后，我们研究了函数，我们可以将行为封装成良好的可重用代码块的方法。\n\n为了将所有这些付诸实践，我们以一个编程任务结束，在这个任务中，我们从一个集合概要构建了一个应用。通过开发一个应用，允许用户输入他们的详细信息，然后将他们分组，我们将所学的技能付诸实践。\n\n有了对 C++ 应用解剖的基本理解，我们现在可以开始深入研究 C++ 的语言特性和工具了。获得对应用的初步理解是必要的，这样我们就能理解我们的应用是如何构建和运行的。接下来，我们将关注控制流——我们控制哪些代码执行以及何时执行的方法，从而允许我们构建更大、更复杂的应用。"
  },
  {
    "path": "docs/cpp-workshop/02.md",
    "content": "# 二、控制流\n\n概观\n\n本章介绍了用于控制整个应用执行流程的各种工具和技术。这包括但不限于:if 语句、switch 语句和各种循环。我们还将研究如何使用这些技术控制应用的生命周期，以及如何有效地使用它们。本章将以创建一个数字猜测来结束，这个数字猜测将实现各种循环和条件语句。\n\n# 简介\n\n在第一章中，我们介绍了 C++ 的绝对要点，并查看了 C++ 应用的关键组件。我们研究了应用是如何运行的，它们是如何构建的，以及我们如何通过一些基本的输入/输出从它们中获取信息。到目前为止，我们构建的应用主要是按顺序运行的；也就是说，我们编写的代码已经一行一行地按顺序执行了。虽然这对于演示目的来说很好，但这通常不是真实世界应用的工作方式。\n\n为了正确地表示逻辑系统，我们需要在我们做什么和什么时候灵活。例如，我们可能只想在给定语句为真的情况下执行某个操作，或者再次返回到较早的代码段。以这种方式操纵执行被称为控制流(或程序流)，也是本章的主题。\n\n首先，我们来看看最基础的逻辑语句之一`if`语句。然后我们将扩展到查看`switch`语句，这是一个很好的替代长串`if` / `else` 语句的方法。接下来，我们将看看循环。具体来说，我们将看到如何使用它们来重复代码执行，以及如何使用`break`和`continue`语句使它们更加高效和精确。\n\n这一章将以一个有趣的活动结束，在这个活动中，我们将从头开始创建一个猜数字游戏。这不仅需要我们在*第 1 章*、*你的第一个 C++ 应用*中学习的技能，还需要我们即将介绍的程序流技能。当这一章结束时，你不仅会对核心逻辑语句和循环有一个坚实的理解，而且你还会在实际练习中实现它们。\n\n# 如果/否则\n\n最基本也是最重要的控制流语句之一是 if。这个简单的关键字是所有逻辑的核心，只有当指定的条件为真时，才允许我们执行给定的操作。通过创造性地将这些`if`语句链接在一起，我们可以为任何逻辑系统建模。\n\n`if`语句的语法如下:\n\n```cpp\n    if (condition) { // do stuff. }\n```\n\n如果我们用作条件的语句解析为`true`，那么大括号内的代码将被执行。如果语句为`false`，则跳过。我们的条件可以是任何真实或虚假的东西。这可以是一些简单的事情，比如检查布尔值，也可以是一些更复杂的事情，比如另一个操作或函数的结果。\n\n我们还有`else`语句。这允许在且仅在前面的`if`语句的条件评估为`false`时执行代码。但是，如果条件评估为真，并且`if`语句因此被执行，则`else`语句中的代码将不会被执行。这里有一个例子:\n\n```cpp\n    if (MyBool1)\n    {\n        // Do something.\n    }\n    else\n    {\n        // Do something else.\n    }\n```\n\n在这个例子中，如果`MyBool1`是`true`，那么我们将执行`// Do something`代码，而不是`// Do something else`。然而，如果`MyBool1`评估为`false`，我们将执行`// Do something else`代码而不是`// Do something`。\n\n`else`语句也可以和`if`语句一起使用。有了一个`else` / `if`模块，如果第一个`if`检查失败，那么第二个将被评估。这里有一个例子:\n\n```cpp\n    if (MyBool1)\n    {\n        // Do something.\n    }\n    else if (MyBool2)\n    {\n        // Do something else.\n    }\n```\n\n在本例中，将首先检查`MyBool1`。如果返回`true,`，则`// Do Something`代码将被执行，但`// Do something else`不会。然而，如果`MyBool1`是`false`，那么`MyBool2`将被检查，同样的规则将适用:如果`MyBool2`为真，那么`// Do something else`将被执行。所以，如果`MyBool1`和`MyBool2`都是假的，那么这两个代码都不会被执行。\n\n也可以将`if`语句放在彼此内部。这种做法被称为嵌套。这里有一个例子:\n\n```cpp\n    if (MyBool1)\n    {\n        if (MyBool2)\n        {\n            // Do something\n        }\n    }\n```\n\n在本例中，如果`MyBool1`返回`true`，则第二条`if`语句将被求值。如果`MyBool2`也是`true`，则执行`// Do Something`；否则，什么都不会被执行。C++ 允许我们嵌套很多层。该标准建议使用 256(虽然没有强制执行)，但是通常来说，层次越深，代码就越混乱。尽可能减少筑巢是一个好的做法。\n\n现在，让我们编写一些代码，看看这些`if` / `else`语句是如何工作的。\n\n## 练习 5:实现 if/else 语句\n\n在本练习中，我们将编写一个简单的应用，根据输入值输出特定的字符串。用户将输入一个数字，应用将使用`if` / `else`语句来确定它是高于还是低于 10。\n\n按照以下步骤完成练习:\n\n注意\n\n完整的代码可以在这里找到:[https://packt.live/2qnQHRV](https://packt.live/2qnQHRV)。\n\n1.  进入`main()`功能，然后定义一个名为`number`的变量:\n\n    ```cpp\n    // if/else example 1.\n    #include <iostream>\n    #include <string>\n    int main()\n    {\n        std::string input;\n        int number;\n    ```\n\n2.  Write code that prints the `Please enter a number:` string, gets the user input, and then assigns it to the `number` variable:\n\n    ```cpp\n        std::cout << \"Please enter a number: \";\n        getline (std::cin, input);\n        number = std::stoi(input);\n    ```\n\n    注意\n\n    我们在这里使用了`std::stoi`函数，这是我们在*第一章*、*你的第一个 C++ 应用*中第一次看到的。这个函数将一个字符串值转换成它的整数等价物。例如，字符串`1`将作为`int 1`返回。正如我们之前所做的，将其与`getline`相结合是解析整数输入的好方法。\n\n3.  使用`if` / `else`语句根据用户输入评估条件，然后打印`The number you've entered was less than 10!`或`The number you've entered was greater than 10!` :\n\n    ```cpp\n        if (number < 10)\n        {\n            std::cout << \"The number you entered was less than 10!\\n\";\n        }\n        else if (number > 10) \n        {\n            std::cout << \"The number you entered was greater than 10!\\n\";\n        }\n        return 0;\n    }\n    ```\n\n4.  完整的代码如下:\n\n    ```cpp\n    // if/else example 1.\n    #include <iostream>\n    #include <string>\n    int main() \n    {\n        std::string input;\n        int number;\n        std::cout << \"Please enter a number: \";\n        getline(std::cin, input);\n        number = std::stoi(input);\n        if (number < 10) \n        {\n            std::cout << \"The number you entered was less than 10!\\n\";\n        } \n        else if (number > 10) \n        {\n            std::cout << \"The number you entered was greater than 10!\\n\";\n        }\n        return 0;\n    }\n    ```\n\n5.  Run the complete code in your editor. You will see that it evaluates the statements and outputs the correct string, as shown in the following screenshot:\n\n    ![Figure 2.1: The if/else statement allows us to execute certain code based on conditions ](img/C14195_02_01.jpg)\n\n图 2.1:if/else 语句允许我们根据条件执行某些代码\n\n在前面的练习中，我们使用了两个`if`语句，这两个语句都评估一个条件，但是如果两个条件都不为真，那么我们想要一个默认操作呢？我们可以通过单独使用`else`语句来实现这一点:\n\n```cpp\n    if (condition1)\n    {\n        // Do stuff.\n    }\n    else if (condition2)\n    {\n        // Do different stuff.\n    }\n    else\n    {\n        // Do default stuff.\n    }\n```\n\n在这种情况下，如果`condition1`和`condition2`都不能被证明为真，那么`else`块中的代码将被默认执行。这是因为没有`if`语句，所以没有什么必须是`true`才能进入。\n\n将此应用于我们的简单数字示例，我们当前检查该数字是小于还是大于 10，但如果它正好是 10，则不检查。我们可以用一个`else`语句来处理这个问题，如下所示:\n\n```cpp\n    if (number < 10)\n    {\n        std::cout << \"The number you entered was less than 10!\\n\";\n    }\n    else if (number > 10) \n    {\n        std::cout << \"The number you entered was greater than 10!\\n\";\n    }\n    else\n    {\n        std::cout << \"The number you entered was exactly 10!\\n\";\n    }\n```\n\n## 三元算子\n\n三元运算符是一个简洁的特性，它允许我们基于`if`语句的结果快速赋值。这最好用一个例子来说明。也许我们有一个浮点变量，它的值取决于一个布尔值。如果不使用三元运算符，我们可以这样写:\n\n```cpp\n    if (MyBool == true)\n    {\n        MyFloat = 10.f;\n    }\n    else\n    {\n        MyFloat = 5.f;\n    }\n```\n\n注意\n\n这里，我们使用了`==`而不仅仅是`=`。`=`运算符为变量赋值，而==运算符检查两个值是否相等，如果相等则返回真，否则返回假。这将在后面关于操作符的一章中详细介绍。\n\n使用三元运算符，我们还可以编写如下相同的代码:\n\n```cpp\n    MyFloat = MyBool ? 10.f : 5.f;\n```\n\n那就简洁多了。让我们在这里分解语法，看看发生了什么。三元语句的写法如下:\n\n```cpp\n    variable = condition ? value_if_true : value_if_false;\n```\n\n注意\n\n虽然三元语句可以像我们之前看到的`if`语句一样嵌套，但最好还是避免它。它们可能是一个真正的痛苦阅读和理解一目了然。\n\n我们从指定我们想要评估的条件开始，然后用`?`字符跟随它。这就启动了我们的三元陈述。然后，如果值是`true`或`false`，我们定义想要使用的不同值。我们总是从`true`值开始，然后是`false`值，中间用`:`字符隔开。这是一个简洁处理`if/else`场景的好方法。\n\n## 练习 6:使用 if/else 语句创建简单菜单程序\n\n在本练习中，我们将编写一个简单的程序，为一个食品商店提供菜单选项。用户将能够从一个菜单中选择多个选项，我们将根据该选择显示价格信息。\n\n以下是完成练习的步骤:\n\n注意\n\n这个练习的完整代码可以在这里找到:[https://packt.live/35wflPd](https://packt.live/35wflPd)。\n\n1.  创建模板应用，并向用户输出我们的三个菜单选项:\n\n    ```cpp\n    // if/else exercise – Menu Program\n    #include <iostream>\n    #include <string>\n    int main()\n    {\n        std::string input;\n        int number;\n        std::cout << \"Menu\\n\";\n        std::cout << \"1: Fries\\n\";\n        std::cout << \"2: Burger\\n\";\n        std::cout << \"3: Shake\\n\";\n    ```\n\n2.  接下来，我们将要求他们输入他们的选择并存储:\n\n    ```cpp\n        std::cout << \"Please enter a number 1-3 to view an item price: \";\n        getline (std::cin, input);\n        number = std::stoi(input);\n    ```\n\n3.  现在，我们可以使用我们的`if/else`语句来检查用户输入并输出正确的信息:\n\n    ```cpp\n        if (number == 1)\n        {\n            std::cout << \"Fries: $0.99\\n\";\n        }\n        else if (number == 2) \n        {\n            std::cout << \"Burger: $1.25\\n\";\n        }\n        else if (number == 3)\n        {\n            std::cout << \"Shake: $1.50\\n\";\n        }\n        else\n        {\n            std::cout << \"Invalid choice.\";\n        }\n        return 0;\n    }\n    ```\n\n4.  完整的代码如下:\n\n    ```cpp\n    // if/else exercise – Menu Program\n    #include <iostream>\n    #include <string>\n    int main() \n    {\n        std::string input;\n        int number;\n        std::cout << \"Menu\\n\";\n        std::cout << \"1: Fries\\n\";\n        std::cout << \"2: Burger\\n\";\n        std::cout << \"3: Shake\\n\";\n        std::cout << \"Please enter a number 1-3 to view an item price: \";\n        getline(std::cin, input);\n        number = std::stoi(input);\n        if (number == 1) \n        {\n            std::cout << \"Fries: $0.99\\n\";\n        } \n        else if (number == 2) \n        {\n            std::cout << \"Burger: $1.25\\n\";\n        } \n        else if (number == 3) \n        {\n            std::cout << \"Shake: $1.50\\n\";\n        } \n        else \n        {\n            std::cout << \"Invalid choice.\";\n        }\n        return 0;\n    }\n    ```\n\n5.  运行应用。当我们输入菜单选项时，我们会看到该项目的正确信息，如下图所示:\n\n![Figure 2.2: We can make menu selections and output the correct information ](img/C14195_02_02.jpg)\n\n图 2.2:我们可以选择菜单并输出正确的信息\n\n这种在给定条件为真的情况下执行操作的能力是所有编程的核心。如果你把任何一个系统分解得足够远，它就会包含“如果 x 是真的，就做 y。”有了这些，可能性是无穷无尽的。\n\n# 开关/外壳\n\n正如我们已经看到的，我们可以使用`if` / `else`根据哪些条件为真来执行某些动作。当您评估多个条件语句以确定流程时，这非常有用，例如:\n\n```cpp\n    if (checkThisCondition)\n    {\n        // Do something ...\n    }\n    else if (checkAnotherCondition)\n    {\n        // Do something else ...\n    }\n```\n\n然而，当我们评估单个变量的不同可能性时，我们有一个不同的语句:T0 语句。这允许我们以类似于`if` / `else`语句的方式进行分支，但是每个分支都基于我们正在打开的单个变量的不同可能值。\n\n一个合适的例子是我们在前面的练习中创建的菜单应用。目前，我们链接`if` / `else`语句来处理不同的可能值，但是因为我们打开了单个变量(菜单索引)，所以它更适合作为 switch 语句。\n\n`switch`语句块的基本实现如下:\n\n```cpp\n    switch (condition)\n    {\n        case value1:\n            // Do stuff.\n        break;\n        case value2:\n            // Do stuff.\n        break;\n        default:\n            // Do stuff.\n        break;\n    }\n```\n\n将此应用于前面的菜单示例，条件将是我们从用户那里读取的所选菜单索引，不同的值将是我们支持的可能性(1-3)。默认语句会捕捉到用户输入我们没有处理的选项的情况。在这些情况下，我们可以打印一条错误消息，让他们选择不同的选项。\n\nswitch 语句包含多个关键字:\n\n*   **开关**:表示我们正在评估的情况。我们将根据它的价值来改变我们的行为。\n*   **case** :每个 case 语句后面都是我们要处理的值。然后，我们可以为那个场景定义我们的行为。\n*   **break** :这个语句标志着我们给定情况下代码的结束。在下一个主题中有更多关于这些的内容。\n*   **default**: This is the default case and is what will get called should none of the other cases match.\n\n    注意\n\n    默认情况不是必需的，但建议使用。它允许我们处理所有其他的值，也许抛出一个异常。\n\n`switch`语句的一个重要限制是只能用于某些类型。这些是整数和`enum`值。这意味着，例如，我们不能在 switch 语句中使用字符串或浮点类型。\n\n注意\n\n枚举类型，或`enum`，是 C++ 中用户生成的数据类型。对此的详细讨论超出了本书的范围。但是，您可以参考以下文档了解更多详细信息:[https://packt.live/35l6QWT](https://packt.live/35l6QWT)。\n\n还值得注意的是，并不是每个案例都需要`break`语句。它们是可选的，尽管在绝大多数情况下可能是必需的。然而，如果省略了`break`语句，那么执行流程将继续到下一个`case`语句，直到遇到中断。这里要小心，因为缺少`break`语句是导致 bug 难找的常见原因；确保每个案例在需要的地方都有一个`break`声明可以为您节省大量潜在的调试时间。\n\n也许看到`switch`语句使用的最好方法是转换一些`if/else`链来切换语句。这将是以下练习的目标。\n\n## 练习 7:将 if/else 链重构为开关/外壳\n\n在本练习中，我们将重用上一练习中的代码，并将其重构为`switch`语句。这将清楚地显示我们如何使用这两种方法来表示相同的功能。然而，因为我们只检查单个变量的不同可能值，所以`switch`语句是优选的。\n\n注意\n\n确保您已经在编译器窗口中复制了上一练习(步骤 1-2)中的代码。完整的代码可以在这里找到:[https://packt.live/32ZZ5Ek](https://packt.live/32ZZ5Ek)。\n\n我们将把它分解成许多简单的步骤:\n\n1.  首先，我们在这里检查的变量是`number`，所以这将是我们打开的条件。将它添加到一个`switch`语句中，并打开我们的花括号，为开关块的其余部分做好准备:\n\n    ```cpp\n        switch (number)\n        {\n    ```\n\n2.  接下来，我们将把第一个`if`语句转换成`case`语句。如果我们看第一个，我们在检查`number`是否等于 1。将此作为我们的第一个`case`值，并将输出复制到`case`体内:\n\n    ```cpp\n        case 1:\n            std::cout << \"Fries: $0.99\\n\";\n        break;\n    ```\n\n3.  Now, repeat this for each of the `if` statements, apart from the last one. If you remember, this statement had no condition that it checked; it's simply the last option. This meant that if all other checks failed, execution would fall right through to that final default statement. This is exactly how the default case works, so we will end by moving that `else` statement into a default case. We should end up with the following `switch` statement, which will replace our `if`/`else` chain:\n\n    ```cpp\n        switch (number)\n        {\n            case 1:\n                std::cout << \"Fries: $0.99\\n\";\n            break;\n            case 2:\n                std::cout << \"Burger: $1.25\\n\";\n            break;\n            case 3:\n                std::cout << \"Shake: $1.50\\n\";\n            break;\n            default:\n                std::cout << \"Invalid choice.\";\n            break;\n        }\n    ```\n\n    该语句的功能与链接的`if` / `else`相同，因此您可以使用其中任何一个；然而，你通常会在长长的`if`链上看到 switch 语句。现在，让我们运行这段代码，检查它的行为是否符合我们的预期。\n\n4.  完整的代码如下:\n\n    ```cpp\n    // if/else to switch/case\n    #include <iostream>\n    #include <string>\n    int main() \n    {\n        std::string input;\n        int number;\n        std::cout << \"Menu\\n\";\n        std::cout << \"1: Fries\\n\";\n        std::cout << \"2: Burger\\n\";\n        std::cout << \"3: Shake\\n\";\n        std::cout << \"Please enter a number 1-3 to view an item price: \";\n        getline(std::cin, input);\n        number = std::stoi(input);\n        switch (number) \n        {\n        case 1:\n            std::cout << \"Fries: $0.99\\n\";\n        break;\n        case 2:\n            std::cout << \"Burger: $1.25\\n\";\n        break;\n        case 3:\n            std::cout << \"Shake: $1.50\\n\";\n        break;\n        default:\n            std::cout << \"Invalid choice.\";\n        break;\n        }\n    }\n    ```\n\n5.  运行完整的代码。您将获得类似于以下内容的输出:\n\n![Figure 2.3: The code works the same, but this time presented as a switch statement ](img/C14195_02_03.jpg)\n\nF 图 2.3:代码的工作原理相同，但这次呈现为一个 switch 语句\n\n程序以同样的方式运行，但可以说更整洁，更容易理解。我们可以清楚地看到每个可能的行为分支以及让它执行的案例。\n\n# 循环\n\n除了`if`语句，循环也是最基本的编程概念。如果没有循环，我们的代码将通过逐个运行逻辑语句然后结束来执行。到目前为止，我们的应用就是这样工作的；然而，在现实中，这真的不切实际。系统往往由许多活动的部分组成，代码执行将围绕代码库跳到需要的地方。\n\n我们已经看到了如何通过在代码中创建可以计算语句的分支来实现这一点，并且我们根据结果做了不同的事情。另一种方法是通过循环。循环允许我们重新运行代码段，根据我们选择哪一个，可以是固定次数，也可以是无限次数。我们将看到三个:`while`、`do while`和`for`循环。\n\n## 而\n\n`while`循环是你的武器库中最基本的循环之一，通常是应用中最外层的循环。当执行进入 while 循环时，它通常不会离开，直到条件为假。我们说一般是因为多线程应用可以打破这个规则；然而，它们超出了本入门书的范围。以下是`while`循环的基本实现:\n\n```cpp\n    while (condition)\n    {\n        // Do stuff.\n    }\n```\n\n以下流程图显示了`while`循环的结构和逻辑流程:\n\n![Figure 2.4: A while loop flowchart ](img/C14195_02_04.jpg)\n\n图 2.4:while 循环流程图\n\n在应用中常见的是一个最外面的`while`循环，它将评估一个`bool`，比如`bIsRunning`。这样，您就可以为您的应用设置一个不确定的生命周期，这通常是我们想要的。我们希望软件能运行多久就运行多久。只要我们希望循环停止运行，我们只需将 bool 更改为`false`。然而，我们在这里需要小心，因为很容易形成一个永远不会结束的`while`循环，因为条件永远不会评估`false`。在这种情况下，你的循环将无限期地陷入困境，没有出路。\n\n下面的代码片段展示了使用`while`循环作为最外层循环来控制应用生存期的方法。当`bIsRunning`是`true`时，应用将无限期运行:\n\n```cpp\nint main()\n{\n    bool bIsRunning;\n\n    // Do application setup.\n    while (bIsRunning)\n    {\n        // Run application logic.\n    }\n    // Do application cleanup.\n    return 0;\n}\n```\n\n我们已经编写了几个接受用户输入的示例应用，但通常在第一次输入后停止。让我们取一个现有的应用，对其进行修改，使其在`while`循环中运行；我们将继续使用我们重构为开关的菜单应用。我们想把所有我们想重新运行的代码放入`while`循环中。这包括输出菜单项、用户选择和输出他们的答案。\n\n## 练习练习 8:实现一个 while 循环\n\n在本练习中，我们将重用*练习 7* 、*中的代码，将 if/else Chain 重新因子化为 switch/case* ，并在菜单程序中实现一个`while`循环。\n\n注意\n\n这个练习的完整代码可以在这里找到:[https://packt.live/35lj81p](https://packt.live/35lj81p)。\n\n按照以下步骤完成练习:\n\n1.  将上一练习中的代码复制到编译器窗口中。\n2.  现在，执行一个`while`循环，并将值`true`传递给它，如下所示:\n\n    ```cpp\n    #include <iostream>\n    #include <string>\n    int main()\n    bool bIsRunning = true;\n    {\n        while (bIsRunning)\n        {\n            std::string input;\n            int number;\n            std::cout << \"Menu\\n\";\n            std::cout << \"1: Fries\\n\";\n            std::cout << \"2: Burger\\n\";\n            std::cout << \"3: Shake\\n\";\n            std::cout << \"Please enter a number 1-3 to view an                   item price: \";\n            getline (std::cin, input);\n            number = std::stoi(input);\n            switch (number)\n            {\n                case 1:\n                    std::cout << \"Fries: $0.99\\n\";\n                break;\n                case 2:\n                    std::cout << \"Burger: $1.25\\n\";\n                break;\n                case 3:\n                    std::cout << \"Shake: $1.50\\n\";\n                break;\n                default:\n                    std::cout << \"Invalid choice.\";\n                break;\n             }\n        }\n    }\n    ```\n\n3.  完整的代码如下:\n\n    ```cpp\n    #include <iostream>\n    #include <string>\n    int main() \n    bool bIsRunning = true;\n    {\n       while (bIsRunning)\n        {\n            std::string input;\n            int number;\n            std::cout << \"Menu\\n\";\n            std::cout << \"1: Fries\\n\";\n            std::cout << \"2: Burger\\n\";\n            std::cout << \"3: Shake\\n\";\n            std::cout << \"Please enter a number 1-3 to view an                   item price: \";\n            getline(std::cin, input);\n            number = std::stoi(input);\n            switch (number) \n            {\n                case 1:\n                    std::cout << \"Fries: $0.99\\n\";\n                break;\n                case 2:\n                    std::cout << \"Burger: $1.25\\n\";\n                break;\n                case 3:\n                    std::cout << \"Shake: $1.50\\n\";\n                break;\n                default:\n                    std::cout << \"Invalid choice.\";\n                break;\n            }\n        }\n    }\n    ```\n\n4.  Run the program.\n\n    目前，我们只是希望这个应用无限期运行，因此我们使用`true`作为我们的表达式。我们可以看到它循环，再次要求用户进行选择，如以下输出所示:\n\n![Figure 2.5: The application now loops and is able to process multiple user inputs ](img/C14195_02_05.jpg)\n\n图 2.5:应用现在循环，并且能够处理多个用户输入\n\n## 边做边看\n\ndo while 循环的结构与`while`循环的结构非常相似，但有一个根本区别:条件检查在身体之后。这种细微的差别意味着身体总是会被执行至少一次。`do` `while`循环的基本结构如下:\n\n```cpp\n    do\n    {\n        // code\n    }\n    while (condition);\n```\n\n以下流程图显示了一个`do` `while`循环的结构和逻辑流程:\n\n![Figure 2.6: Diagram of a do while loop ](img/C14195_02_06.jpg)\n\n图 2.6:边做边循环的示意图\n\n请看下面的例子:\n\n```cpp\n    while (false)\n    {\n        // Do stuff.\n    }\n```\n\n这个`while`语句中的代码永远不会被执行，因为我们首先计算表达式`false`，从而跳过该代码。然而，如果我们对`do` `while`循环使用相同的条件，如下面的代码片段所示，我们将看到不同的行为:\n\n```cpp\n    do\n    {\n        // Do stuff.\n    }\n    while (false);\n```\n\n在这种情况下，由于执行是从上到下运行的，所以首先执行代码，然后执行条件；即使是`false`，代码也已经运行过一次了。在我们老朋友的帮助下，我们将会看到这个 T2。\n\n## 练习 9:用假条件实现 while 和 do while 循环\n\n在本练习中，我们将编辑我们的“你好世界”程序，以包括一个`while`和一个`do` `while`循环。对于这两个循环，我们将通过`false`条件并观察输出。\n\n注意\n\n这个练习的完整代码可以在这里找到:[https://packt.live/2rc9vU2](https://packt.live/2rc9vU2)。\n\n按照以下步骤完成练习:\n\n1.  Insert the following code, which includes a `while` loop only, in the compiler window, and then execute it:\n\n    ```cpp\n    // While loop.\n    #include <iostream>\n    #include <string>\n    int main()\n    {\n        while (false)\n        {\n            std::cout << \"Hello World!\";\n        }\n        return 0;\n    }\n    ```\n\n    您将获得以下输出:\n\n    ![Figure 2.7: Output when using the while loop ](img/C14195_02_07.jpg)\n\n    图 2.7:使用 while 循环时的输出\n\n    从输出中可以看出，我们在执行窗口中看不到任何东西。因为我们首先进行了评估，所以程序从未执行代码。但是，如果我们用 do while 循环替换 while 循环，这种情况就会改变。\n\n2.  编辑代码以包含一个`do while`循环，如下面的代码片段所示:\n\n    ```cpp\n    // do ... while loop.\n    #include <iostream>\n    #include <string>\n    int main()\n    {\n        do\n        {\n            std::cout << \"Hello World!\";\n        }\n        while (false);\n        return 0;\n    }\n    ```\n\n3.  运行代码。您应该获得以下输出:\n\n![Figure 2.8: A do while loop showing that the body is executed at least once ](img/C14195_02_08.jpg)\n\n图 2.8:一个 do while 循环，显示主体至少被执行一次\n\n现在，我们可以看到我们确实将`Hello` `World`字样打印到了控制台上；所以，虽然这两个循环在本质上是相似的，但它们有很大的区别。`while`循环将首先评估条件，而`do` `while`循环将在之后评估条件。\n\n## 为\n\n`while`和`do while`循环都是不定循环，这意味着它们只有在条件评估为`false`时才会停止。通常，在构建这些循环时，我们不知道需要多少次迭代；我们只是让它运行，并在稍后的某个时刻停止它。`for`然而，当我们知道我们需要多少次迭代时，当我们需要知道我们当前正在进行什么迭代时，就会使用循环。\n\n例如，假设我们有一个联系人集合，我们想要遍历所有联系人，打印出他们的姓名和号码。因为我们知道这个集合的大小，所以我们可以编写一个 for 循环来迭代正确的次数，从而允许我们顺序访问集合中的每个元素。因为我们也知道我们当前在哪个迭代中，我们可以用它来决定我们如何输出数据。也许，对于联系人列表的前半部分，我们希望输出姓名和号码，而对于后半部分，我们只需要号码。或者我们想对列表中的第一个和最后一个联系人做一些特别的事情。一个`for`循环将允许我们做所有这些事情。\n\n注意\n\n一次迭代只是运行一次的循环。如果说一个循环迭代了五次，那只是意味着它运行了五次。\n\n`for`循环的基本结构如下:\n\n```cpp\n    for (initialization; condition; iteration expression) \n    {\n        statement(s);\n    }\n```\n\n以下流程图显示了`for`循环的结构和逻辑流程:\n\n![Figure 2.9: A for loop diagram  ](img/C14195_02_09.jpg)\n\n图 2.9:循环图\n\n在`for`循环中使用了三个子句:\n\n*   **初始化**:这是一个在循环开始时运行一次的语句。这用于声明将用作计数器的变量。\n*   **条件**:这是每次循环运行前检查的条件。如果条件为`true`，则循环运行。如果条件是`false`，那就是`for`循环的结束。这用于检查计数器变量是否低于指定值。这就是我们如何控制循环运行的次数。\n*   **迭代表达式**:这是一个在每个循环结束时运行的语句。它用于递增计数器变量。\n\n现在，让我们在下一个练习中实现一个基本的`for`循环来巩固我们的理解。\n\n## 练习 10:实现 for 循环\n\n在本练习中，我们将创建一个`for`循环，该循环将运行五次以打印出一串数字:`01234`。\n\n注意\n\n这个练习的完整代码可以在这里找到:[https://packt.live/332boQl](https://packt.live/332boQl)。\n\n执行以下步骤完成练习:\n\n1.  从主功能开始:\n\n    ```cpp\n    #include <iostream>\n    #include <string>\n    int main()\n    {\n    ```\n\n2.  创建一个`for`循环，变量`i`初始化为`0`，将`i`设置为小于`5`；递增计数器，最后打印输出。您可以使用以下代码:\n\n    ```cpp\n        for (int i = 0; i < 5; ++ i)\n        {\n            std::cout << i;\n        }\n    }\n    ```\n\n3.  完整的代码如下:\n\n    ```cpp\n    #include <iostream>\n    #include <string>\n    int main() \n    {\n        for (int i = 0; i < 5; ++ i) \n        {\n            std::cout << i;\n        }\n    }\n    ```\n\n4.  运行代码。您将获得以下输出:\n\n![Figure 2.10: Output of the for loop ](img/C14195_02_10.jpg)\n\n图 2.10:for 循环的输出\n\n我们可以看到打印出了 5 个数字，从 0 到 4，如前面的截图所示。请注意，数字是 0 到 4，因为增量在主循环体之后运行，`i`从值 0 开始。\n\n我们可以将代码分解为前一节中确定的三个语句:**初始化**、**条件**和**增量**。我们在这个循环中的**初始化**语句如下:\n\n```cpp\n    int i = 0\n```\n\n通过这个语句，我们创建了我们的计数器，并将其值设置为 0。这个计数器将用来记录我们希望循环运行多少次。我们在此循环中的**条件**陈述如下:\n\n```cpp\n    i < 5\n```\n\n这是**条件**，我们检查以确保循环可以运行，类似于`while`循环如何工作。在每次迭代开始时，检查这个**条件**。如果`I`(我们的计数器变量)小于指定值，那么循环将运行。我们在这个循环中的**增量**语句如下:\n\n```cpp\n    ++ i\n```\n\n该语句在循环的每次迭代后被调用，并增加我们的计数器，这样我们就可以跟踪循环已经运行了多少次。\n\n## 基于范围的 for 循环\n\n我们要看的最后一个循环，比前三个循环更简单，是基于范围的循环。在 C++ 11 中引入，这个循环允许我们快速迭代集合中的所有对象。我们还没有涉及到集合，所以我们将只在这里讨论基础。\n\n当使用`for`循环迭代集合时，我们使用迭代器。在我们的用例中，这是用来访问元素的`i`变量，如下面的代码片段所示:\n\n```cpp\n    int myVector[] {0, 1, 2, 3, 4};\n    for (int i = 0; i < myVector.size(); ++ i)\n    {\n        int currentValue = myVector[i];\n        std::cout << \"\\n\" << currentValue;\n    }\n```\n\n然而，对于基于范围的`for`循环，我们不会通过递增的值手动获取元素。相反，循环只是给我们集合中的每个值:\n\n```cpp\n    int myVector[] {0, 1, 2, 3, 4};\n    for (int currentValue : myVector)\n    {\n        std::cout << \"\\n\" << currentValue;\n    }\n```\n\n这两个循环将产生相同的输出，但是我们可以看到第二个循环更简洁，更不容易出错，因为我们不是手动获取元素，并且也很可能更高效。通常，如果您不需要索引值，那么这种循环将允许您拥有更干净、更可靠的代码。\n\n## 练习 11:使用循环生成随机数\n\n在本练习中，我们将构建一个应用，为用户生成一组随机数。我们的应用将由一个主外部循环和其中的另一个循环组成，以控制我们的数字的生成。\n\n对于外部循环，我们将使用`while`循环，这是应用的常见设置。我们知道这个循环将无限期运行，所以它非常适合控制应用的最外层范围。对于内部循环，我们将使用`for`循环，因为我们将知道用户想要生成多少个数字。\n\n注意\n\n这个练习的完整代码可以在这里找到:[https://packt.live/2s4it6l](https://packt.live/2s4it6l)。\n\n按照以下步骤完成练习:\n\n1.  我们将从创建我们的`main`函数和定义我们的`main`变量开始。这包括`bIsRunning`T3，它将控制我们应用的生命周期:\n\n    ```cpp\n    #include <iostream>\n    #include <string>\n    #include <cstdlib> \n    #include <ctime>\n    int main()\n    {\n        bool bIsRunning = true;\n        std::string input = \"\";\n        int count = 0;\n    ```\n\n2.  接下来，我们将输出标题内容并创建`main`循环。我们使用的是`while`循环，我们的条件是`bool`我们刚刚定义的:\n\n    ```cpp\n        std::cout << \"***Random number generator***\\n\";\n        while (bIsRunning)\n        {\n    ```\n\n3.  With our `while` loop in place, we can now add all the code that we want to run during each iteration of the `main` loop. This starts with outputting our instructions and reading the user input:\n\n    ```cpp\n            std::cout << \"Enter amount of numbers to generate,                   or 0 to exit: \";\n            // Get count from user.\n            getline(std::cin, input); \n            count = std::stoi(input);\n    ```\n\n    本章已经介绍了`break`，现在我们可以使用它来检查用户是否想要退出应用。如果用户输入了一个`0`，表明这一点，我们可以调用`break`，退出主`while`循环，结束应用。我们还将为我们的随机数生成设置种子。\n\n    注意\n\n    为了生成我们的随机数，我们使用`rand`和`srand`。`rand`给我们我们的随机数，`srand`为随机数生成设置种子。通过使用`time(0)`，以秒为单位的时间自纪元以来，我们获得了一个足够随机的种子和数字来满足我们的需求。\n\n4.  输入以下代码，插入`break`语句，允许用户退出应用。我们稍后将更详细地介绍“`break`”:\n\n    ```cpp\n            // Check if user wants to quit application.\n            if (count == 0)\n            {\n                break;\n            }\n            // Generate and output random numbers.\n            srand((unsigned)time(0));\n    ```\n\n5.  Now, we can write the `main` loop that will generate our random numbers and output them to the user. Since we got a `count` variable from our user, we can use that to ensure we iterate the correct number of times. Within the loop, we'll generate a random number and do a bit of formatting. After each number, we want to print a comma to create a well-formatted list, but not after the last one. We can use a `continue` statement for this:\n\n    注意\n\n    `continue`语句将在下一个主题中介绍。现在，请注意，它允许我们跳过当前循环的剩余部分，立即开始下一个循环。\n\n    ```cpp\n            for (int i = 0; i < count; ++ i)\n            {\n                std::cout << rand() % 10;\n                if (i == count - 1)\n                {\n                    continue; \n                }\n                std::cout << \", \";\n            }\n    ```\n\n    注意\n\n    模数%运算符返回除法后的余数。在前面的步骤中，我们使用它和`rand()`一起生成 0 到 9 之间的数字。我们将在*第 4 章*、*操作员*中详细介绍这一点和许多其他操作员。\n\n6.  最后，我们将输出几行空白行用于演示，并添加最后的大括号:\n\n    ```cpp\n            std::cout << \"\\n\\n\";\n        }\n    }\n    ```\n\n7.  完整的代码如下:\n\n    ```cpp\n    #include <iostream>\n    #include <string>\n    #include <cstdlib>\n    #include <ctime>\n    int main() \n    {\n        bool bIsRunning = true;\n        std::string input = \"\";\n        int count = 0;\n        std::cout << \"***Random number generator***\\n\";\n        while (bIsRunning) \n        {\n            std::cout << \"Enter amount of numbers to generate,                   or 0 to exit: \";\n            // Get count from user.\n            getline(std::cin, input);\n            count = std::stoi(input);\n            // Check if user wants to quit application.\n            if (count == 0) \n            {\n                break;\n            }\n            // Generate and output random numbers.\n            srand((unsigned) time(0));\n            for (int i = 0; i < count; ++ i) \n            {\n                std::cout << rand() % 10;\n                if (i == count - 1) \n                {\n                    continue;\n                }\n                std::cout << \", \";\n            }\n            std::cout << \"\\n\\n\";\n        }\n    }\n    ```\n\n8.  Run the application. When complete, the application should be able to generate the specified number of random integers, as shown here:\n\n    ![Figure 2.11: Program that will run indefinitely, outputting a series of numbers if the user doesn't quit ](img/C14195_02_11.jpg)\n\n图 2.11:将无限期运行的程序，如果用户不退出，将输出一系列数字\n\n通过使用`while`循环，我们已经能够创建一个可以无限期使用的应用。想象一下，如果每次你在电脑上做一件事，你只能在它需要重启之前做一件事。这不太实际。拥有循环代码和操作程序流的能力是必不可少的。\n\n# 中断/继续\n\n拥有循环代码段的能力非常重要，但必须谨慎使用。我们已经看到，创建永不结束的循环是可能的，另一个关注点是确保它们得到有效利用。到目前为止，我们看到的循环一直很小，我们很高兴看到它们完整地运行。但是，如果我们需要更多的控制我们的循环，也许提前结束一个循环呢？值得庆幸的是，我们有两个重要的关键词可以帮助我们做到这一点——`break`和`continue`。\n\n## 休息\n\n`break`是一个 C++ 关键字，将退出当前循环，如果有代码，执行将跳转到下一段代码。这个关键字适用于我们已经介绍过的不同类型的循环，我们可以使用一个简单的计数应用很好地演示它，如下面的代码片段所示:\n\n```cpp\n// Break example.\n#include <iostream>\n#include <string>\nint main()\n{\n    std::cout << \"Loop Starting ...\\n\";\n    int count = 0;\n    while (count < 5)\n    {\n        ++ count;\n        std::cout << \"\\n\" << count;\n    }\n    std::cout << \"\\n\\nLoop finished.\";\n}\n```\n\n在这个例子中，我们将打印出 5 个数字，0-4。如果我们按原样运行这段代码，我们可以看到循环完整地运行，并给出我们期望的结果。我们在循环的开始和结束都有语句，因此我们可以更清楚地看到流程执行:\n\n![Figure 2.12: Example counting application will print out numbers 0-4 ](img/C14195_02_12.jpg)\n\n图 2.12:示例计数应用将打印出数字 0-4\n\n现在，如果有一个条件意味着我们希望这个循环在计数等于 2 时停止执行呢？嗯，我们可以使用`if`语句将`break`语句放入支票中:\n\n```cpp\n#include <iostream>\nusing namespace std;\nint main()\n{\n    std::cout << \"Loop Starting ...\\n\";\n    int count = 1; // init\n    while (count <= 5) // condition\n    {\n        std::cout << \"\\n\" << count;\n        if (count == 2)\n        break;\n        ++ count; // increment\n    }\n    std::cout << \"\\n\\nLoop finished.\";\n\n    return 0;\n}\n```\n\n有了`break`条件，一旦计数等于`2`(意味着我们将有 2 次循环迭代)，那么断点将被命中，我们将退出循环。现在，让我们运行应用，看看我们得到了什么:\n\n![Figure 2.13: With the break statement in place, we only execute 2 loop iterations ](img/C14195_02_13.jpg)\n\n图 2.13:break 语句就位后，我们只执行 2 次循环迭代\n\n我们现在可以看到，一旦满足该条件并且`break`语句被命中，循环就停止迭代，并且代码执行在循环之后立即开始。如果我们把它写成一个`do`……`while`，结果会完全一样:\n\n```cpp\n#include <iostream>\nusing namespace std;\nint main()\n{\n    std::cout << \"Loop Starting ...\\n\";\n    int count = 1; // init\n    do\n    {\n        std::cout << \"\\n\" << count;\n        if (count == 2)\n        break;\n        ++ count; // increment\n        }\n        while (count <= 5); // condition\n\n        std::cout << \"\\n\\nLoop finished.\";\n        return 0;\n}\n```\n\n如果我们把它写成一个`for`循环，也是一样的:\n\n```cpp\n#include <iostream>\nusing namespace std;\nint main()\n{   \n    std::cout << \"Loop Starting ...\\n\";\n    // init condition increment\n    for (int count = 1; count <= 5; ++ count)\n    {\n        std::cout << \"\\n\" << count;\n        if (count == 2)\n        break;\n    }\n\n    std::cout << \"\\n\\nLoop finished.\";\n    return 0;\n}\n```\n\n这两个循环给出完全相同的行为；到达`break`语句并退出循环之前的两次迭代:\n\n![Figure 2.14: All loops give the same outcome: two iterations before exiting ](img/C14195_02_13.jpg)\n\n图 2.14:所有的循环给出相同的结果:退出前两次迭代\n\n这表明这些循环有时是可以互换的，尽管有些比其他更适合某些用例。例如，对于我们在这里使用的计数示例，一个`for`循环可能是最合适的，因为它带有一个整数值，用于递增每个循环——这是我们必须手动对`while`和`do while`循环进行的操作。然而，当不需要递增整数时，建议使用基于范围的`for`循环。\n\n## 继续\n\n我们可以使用的另一个关键词是`continue`。这个关键字允许我们跳过当前的循环迭代，但保留在循环中，与`break`形成对比。同样，计数示例将允许我们演示这一点。在我们的例子中，我们打印数字 0-4；让我们使用`continue`关键字跳过数字 3 的打印。\n\n就像我们对`break`所做的那样，我们可以写一个条件来检查计数是否等于 3，如果是，就调用`count`:\n\n```cpp\n    if (count == 3)\n    {\n        continue;\n    }\n```\n\n我们还需要在我们的功能中改变这个位置。`continue`关键字将跳过循环体的其余部分。目前，这段代码位于该主体的末尾，因此我们实际上不会跳过任何内容。为了让`continue`按预期工作，它需要出现在我们想要跳过的任何代码之前，但是在我们想要执行的任何代码之后。\n\n对于本例，我们将把`continue`关键字放在`if`语句中:\n\n```cpp\n// continue example.\n#include <iostream>\n#include <string>\nint main() \n{\n    std::cout << \"Loop Starting ...\\n\";\n    int count = 0;\n    while (count < 5) \n    {\n        ++ count;\n        if (count == 3) \n        {\n            continue;\n        }\n        std::cout << \"\\n\" << count;\n    }\n    std::cout << \"\\n\\nLoop finished.\";\n}\n```\n\n这里，我们总是要增加我们的`counter`变量，然后检查我们是否想要跳过当前迭代。如果我们跳过它，我们将回到下一个循环的开始，如果我们没有，我们将像往常一样执行循环的剩余部分。运行此代码后，您将获得以下输出:\n\n![Figure 2.15: The printing of number 3 has been skipped ](img/C14195_02_15.jpg)\n\n图 2.15:数字 3 的打印已被跳过\n\n我们已经按照自己的意愿跳过了数字 3 的打印，但是循环继续执行剩下的部分。这在搜索某样东西时非常有用。想象一下，我们有一个名字列表，我们只想用那些以字母 D 开头的名字来做事。我们可以遍历所有的名字，首先检查第一个字母是否是 D；如果没有，我们继续。这样，我们可以有效地跳过我们不感兴趣的用例。\n\n## 练习 12:使用中断和继续使循环更有效\n\n在本练习中，我们将利用`break`和`continue`来提高循环的效率。我们将创建一个在数字 1-100 上运行的循环，只打印给定值的特定倍数。\n\n注意\n\n完整的代码可以在这里找到:[https://packt.live/2KJrnN8](https://packt.live/2KJrnN8)。\n\n按照以下步骤完成练习:\n\n1.  我们首先要求用户选择要打印倍数的值，以及要打印的最大倍数:\n\n    ```cpp\n    #include <iostream>\n    #include <string>\n    int main()\n    {\n        int multiple = 0;\n        int count = 0;\n        int numbersPrinted = 0;\n        std::string input = \"\";\n        std::cout << \"Enter the value whose multiples will be printed: \";\n        getline(std::cin, input);\n        multiple = std::stoi(input);\n        std::cout << \"Enter maximum amount of numbers to print: \";\n        getline(std::cin, input);\n        count = std::stoi(input);\n    ```\n\n2.  接下来，我们将创建`for`循环来迭代数字 1-100:\n\n    ```cpp\n        for (int i = 1; i <= 100; ++ i)\n        {\n        }\n    ```\n\n3.  现在，在`for`循环中，我们可以编写确定倍数的逻辑。首先，我们有一组要打印的数字，所以我们可以检查一下，如果已经达到了这个数字\n\n    ```cpp\n            if (numbersPrinted == count)\n            {\n                break;\n            }\n    ```\n\n4.  我们只对给定倍数的数字感兴趣，所以如果不是这样，我们可以使用`continue`语句直接跳到下一个迭代:\n\n    ```cpp\n            if (i % multiple != 0)\n            {\n                continue;\n            }\n    ```\n\n5.  如果循环迭代通过了这两个语句，那么我们就找到了一个有效的数字。在这种情况下，我们将打印它，然后使用下面的代码片段增加我们的`numbersPrinted`变量:\n\n    ```cpp\n                std::cout << i << \"\\n\";\n                ++ numbersPrinted;\n            }\n    ```\n\n6.  完整的代码如下:\n\n    ```cpp\n    #include <iostream>\n    #include <string>\n    int main()\n    {\n        int multiple = 0;\n        int count = 0;\n        int numbersPrinted = 0;\n        std::string input = \"\";\n\n        std::cout << \"Enter the value whose multiples will be printed: \";\n        getline(std::cin, input);\n        multiple = std::stoi(input);\n\n        std::cout << \"Enter maximum amount of numbers to print: \";\n        getline(std::cin, input);\n        count = std::stoi(input);\n        for (int i = 1; i <= 100; ++ i)\n        {\n              if (numbersPrinted == count)\n              {\n                  break;\n              }\n              if (i % multiple != 0)\n              {\n                  continue;\n              }\n              std::cout << i << \"\\n\";\n              ++ numbersPrinted;\n        }\n    }\n    ```\n\n7.  运行应用。您将获得以下输出:\n\n![Figure 2.16: We use break and continue to control loop execution ](img/C14195_02_16.jpg)\n\n图 2.16:我们使用 break 并继续控制循环执行\n\n通过使用 **break** 和 **continue** 语句，我们能够控制循环的执行，使它们更加高效和可控。\n\n## 活动 2:使用循环和条件语句创建猜数字游戏\n\n对于本章的活动，我们将编写一个小的猜数字游戏。这将使我们能够利用我们在本章中已经介绍过的技术。因此，在尝试本练习之前，请确保您已经完成了本章前面的所有练习。\n\n该程序将允许用户选择猜测次数:最小次数和最大次数。应用将在该范围内生成一个数字，然后允许用户猜测该数字。如果他们在开始时指定的猜测次数内这样做，他们就赢得了比赛。赢得游戏后，最终输出应该类似于以下内容:\n\n![Figure 2.17: Number-guessing game output ](img/C14195_02_17.jpg)\n\n图 2.17:猜数字游戏输出\n\n注意\n\n这个活动的完整代码可以在这里找到:[https://packt.live/2pBYnPT](https://packt.live/2pBYnPT)。\n\n以下是完成活动的步骤，以及一些提示:\n\n1.  申报我们需要的所有变量。这包括`guessCount`、`minNumber`、`maxNumber`和`randomNumber`。\n2.  创建一个运行应用的主外部循环。\n3.  Present the user with some introductory text (`\"Enter the number of guesses\"`) and get from them the following: a number of guesses, a minimum number, and a maximum number.\n\n    注意\n\n    您可以将猜测次数、最小次数和最大次数的用户输入传递给变量。\n\n4.  Generate a random number within the range specified by the user.\n\n    注意\n\n    在*练习 11* 、*使用循环*生成随机数时，我们使用了`rand()`来生成 0 到 9 之间的随机数。这里可以使用类似`rand () % (maxNumber - minNumber + 1)`的函数，生成任意两个极限之间的随机数。\n\n5.  创建一个循环，重复用户指定的猜测次数。\n6.  在`count`循环中，获取用户的猜测。\n7.  Inside the `count` loop, check whether the user's guess is correct or too high/low. We can use `break` here to exit when the correct value has been guessed.\n\n    提示:参考*练习 7* 、*将 if/else 链重构为开关/外壳*，看看我们如何使用`break`提前退出循环。\n\n8.  When the number has been found, or the user has run out of guesses, present them with the option to either continue or exit the application.\n\n    注意\n\n    这个活动的解决方案可以在第 516 页找到。\n\n在这个应用中，我们使用了许多技术来控制代码流，以复制更复杂的场景。我们使用`while`循环作为主应用循环，因为我们最初不知道需要多少次迭代。然后，我们使用`for`循环运行代码设定的次数，并使用`if/else`语句检查用户的输入并采取相应的行动。\n\n# 总结\n\n在这一章中，我们已经了解了程序流，以及如何通过应用操纵执行流。这是表示逻辑系统的基础。\n\n我们从查看基本的 **if** / **else** 语句开始。这些允许我们根据条件来分支我们的代码，并且是编程中最基本的思想之一。借助这种分支能力，我们能够通过控制应用的执行流程来复制逻辑系统和行为。然后我们看了一些基本的 **if** / **else** 语句的替代语句，比如 **switch** 和**三元**语句。\n\n接下来，我们看了一些不同的循环。我们从**开始，而**和**则在**循环；只要检查的条件为真，循环就会无限期运行。然后我们看一下**的**循环，它运行了设定的迭代次数。最后，我们看一下基于范围的循环，这对于遍历集合很有用。最后，我们看看如何确保我们的循环是有效的，用 **break** 语句提前结束它们，或者用 **continue** 语句跳过迭代。\n\n我们通过构建一个简单的游戏来练习所有这些新技能，该游戏允许用户猜测一个随机选择的数字。我们允许用户输入一些值来设置游戏，然后给他们一些猜测来试图找到这个数字。我们利用了在*第 1 章*、*你的第一个 C++ 应用*中所学的一切，以及我们在本章中看到的 if/else 语句和一些循环。\n\n在下一章中，我们将仔细研究 C++ 提供的各种数据类型。我们将从查看各种内置类型(`int`、`double`、`char`等)开始，然后查看这些类型的数组和集合。然后，我们将讨论存储生存期、范围、类和结构等概念。了解了一般的 C++ 应用、控制执行流程，以及如何用各种数据类型来表示和存储数据，我们很快就能对 C++ 语言有一个功能性的理解。"
  },
  {
    "path": "docs/cpp-workshop/03.md",
    "content": "# 三、内置数据类型\n\n概观\n\n本章介绍了 C++ 提供的内置数据类型，包括它们的基本属性以及在向量和数组中的使用。我们将从识别和描述核心数据类型的选择开始，然后分别在向量和数组等容器中进行实现。然后，在我们将创建的注册应用中实现它们之前，我们将查看它们的生命周期和范围，作为本章的最后练习。\n\n# 简介\n\n在前一章中，我们研究了控制流，学习了许多通过应用操纵执行流的方法。在这一章中，我们将仔细研究如何使用不同的数据类型来表示这些信息；具体来说，就是 C++ 提供的内置数据类型。\n\n我们以前用过一些。例如，我们知道整数代表数字，字符串代表单词和字符，但是让我们更详细地讨论一下。C++ 提供的核心类型集是我们稍后将创建的任何和所有用户定义类型的构建块，因此很好地理解我们可用的类型非常重要。我们将从查看它们存储的数据、它们是如何分配的以及它们的大小开始。然后，我们可以继续研究类型修饰符——允许我们修改其属性的关键字。将提供一个图表供将来参考。\n\n接下来，我们将继续关注创建这些类型的数组。到目前为止，我们的大多数变量(如果不是全部的话)都是单一的——也就是说，一个数字或一个字符串。除了单独存储这些内容，我们还可以将这些内容的多个集合存储在一起。这些被称为数组，是一个重要的特性来理解和使用。\n\n在阵列之后，我们将关注存储寿命或范围。这是变量归属的概念，以及变量可访问的时间。这是一个基本的主题，所以强有力的理解是关键，并将引导我们进入我们的最后一个主题——类和结构。这些对象封装了我们的数据和功能，是**面向对象编程** ( **OOP** )的核心。这些将在*第九章*、*对象* - *面向* *原则*中详细介绍，因此我们在此的介绍仅构成简要介绍。\n\n为了完成这一章，我们将通过创建一个真实的注册应用来测试我们所学的内容。这将是我们迄今为止创建的最大的应用，将允许用户注册一个系统，并通过一个标识查找现有记录。这不仅将利用本章中涵盖的概念，还将利用前面的所有概念。\n\n当本章完成时，您不仅会对我们所使用的各种类型的属性有更深入的了解，还会了解它们的生命周期以及它们在我们的应用中是如何出现/消失的。\n\n# 数据类型\n\n到目前为止，我们已经在整本书中看到，我们将数据存储在变量中——用户的姓名、年龄或食品价格。鉴于这些是不同类型的数据——字母、数字等等，我们将它们存储在不同的变量类型中。我们现在要看的就是这些类型，因为为要存储的数据使用正确的变量类型很重要。\n\n## 类型修饰符\n\n然而，在我们了解基本数据类型本身之前，让我们先快速了解一下类型修饰符。最初在*第 1 章*、*你的第一个 C++ 应用*中提到，当我们查看关键字时，类型修饰符允许我们更改整数类型的属性。我们可以使用以下修改器:\n\n*   `signed`:关键字`signed`指定我们的变量可以同时保存正值和负值。这增加了最大的下限值，因为我们现在可以为负，但这样做会减少最大的上限值。这是因为变量可以容纳的值的范围不会改变；它只是移动，这意味着现在有一半的范围是负数。\n*   `unsigned`:关键字`unsigned`指定我们的变量应该只保存正值。这增加了变量的上限，但降低了下限，因为上限为 0。\n*   `long`:`long`关键字确保我们的变量至少有一个`int`那么大；通常，这将是 4 字节。在某些情况下，这将增加可以存储的值的范围。\n*   `long long`(c++ 11):c++ 11 中增加的`long long`关键字，保证了我们的变量会比`long`更大；通常，这将是 8 字节。在大多数情况下，这将增加可存储的值的范围。\n*   `short`: The `short` keyword ensures that our variable has the smallest memory footprint it can, whilst ensuring a size less than `long`; typically, this will be 4 bytes.\n\n    注意\n\n    数据类型的确切大小取决于一些因素，例如您正在使用的体系结构和设置的编译器标志，尽管典型的大小将很快在参考图表中显示。值得注意的是，C++ 标准并不保证类型的绝对大小，而是保证它们必须能够存储的最小范围。这意味着不同平台之间的修改类型也可能不同。\n\n## 内置类型\n\n现在我们已经有了关于修饰符的入门知识，我们可以看看 C++ 为我们提供的基本数据类型的核心集合。这些类型大部分时间都会服务于你的需求，你不需要做什么特别的事情来使用它们；它们是语言的一部分。这些内置类型如下:\n\n*   `bool`:`bool`类型存储一个`true`(非零)或`false` (0)值，大小为一个字节。\n*   `int`:`int`类型用于存储整数，通常大小为四个字节。\n*   `char`:`char`类型用于存储单个字符。它以整数的形式存储，并根据使用的字符集(通常是 ASCII)解析为一个字符。这种数据类型的大小为一个字节。\n*   `float`:`float`类型代表单精度浮点数，大小通常为 4 字节。\n*   `double`:`double`类型代表双精度浮点数，大小通常为 8 字节。\n*   `void`:`void`类型是表示空值的特殊类型。您不能创建`void`类型的对象。然而，它可以被指针和函数用来表示一个空值——例如，一个不指向任何东西的`void`指针，或者一个不返回任何东西的`void`函数。\n*   `wide character`:`wchar_t`类型用于存储宽字符(Unicode UTF-16)。`wchar_t`的大小是特定于编译器的，尽管 C++ 11 引入了固定大小类型`char16_t`和`char32_t`。\n\n## 参考表\n\n下面是一个由 C++ 提供的基本数据类型表，其中包含一些类型修饰符:\n\n![Figure 3.1: Table of C++ data types and their sizes ](img/C14195_03_01.jpg)\n\n图 3.1:c++ 数据类型及其大小表\n\n注意\n\n这些类型的范围由它们的大小决定，不依赖于数据类型。此外，上表中的值仅适用于 Microsoft Visual C++。gcc 和 clang 中基本类型的大小不同于 visual studio 中的大小。\n\n## 练习 13:声明数据类型\n\n对于第一章的练习，我们将声明一些不同的变量，有和没有类型修饰符，并使用`sizeof`操作符打印出它们的大小。以下是完成练习的步骤:\n\n注意\n\n如果您使用的编译器与本书中的不同，如果您的大小不同，请不要惊慌。请记住，它们可以在不同的平台和体系结构上进行不同的调整。这个练习的代码文件可以在这里找到:[https://packt.live/2rdD8Em](https://packt.live/2rdD8Em)。\n\n1.  我们将从使用上表中的三种类型来定义一些变量开始:\n\n    ```cpp\n        int myInt = 1;\n        bool myBool = false;\n        char myChar = 'a';\n    ```\n\n2.  `sizeof`运算符会以字节为单位给出变量的大小。对于之前定义的每个变量，添加一个`output`语句，该语句将打印其大小:\n\n    ```cpp\n        std::cout << \"The size of an int is \" << sizeof(myInt) << \".\\n\";\n        std::cout << \"The size of a bool is \" << sizeof(myBool) << \".\\n\";\n        std::cout << \"The size of a char is \" << sizeof(myChar) << \".\\n\";\n    ```\n\n3.  完整的代码如下:\n\n    ```cpp\n    #include<iostream>\n    using namespace std;\n    int main()\n    {\n        int myInt = 1;\n        bool myBool = false;\n        char myChar = 'a';\n        std::cout << \"The size of an int is \" << sizeof(myInt) << \".\\n\";\n        std::cout << \"The size of a bool is \" << sizeof(myBool) << \".\\n\";\n        std::cout << \"The size of a char is \" << sizeof(myChar) << \".\\n\";\n        return 0;\n    }\n    ```\n\n4.  运行这段代码。您应该会看到我们的变量的大小被打印出来:\n\n![Figure 3.2: Using sizeof to determine the size of our variables ](img/C14195_03_02.jpg)\n\n图 3.2:使用 sizeof 来确定变量的大小\n\n通过使用`sizeof`，我们可以快速看到我们变量的大小。同样，根据您使用的平台和编译器配置，您的里程可能会有所不同。继续使用前面参考图表中列出的其他一些数据类型，并查看您的大小是否与给定的大小匹配。了解关于我们的数据类型的信息是很好的，这样我们就可以确保在给定的场景中使用最合适的数据类型。\n\n# 容器\n\n现在我们已经了解了 C++ 提供的一些内置数据类型，让我们来看几个容器——允许我们将多个元素存储在一起的对象。它们有多种形状和大小，具体取决于您存储的数据以及您希望如何存储。在这一章的开头，我们将集中讨论两个基本容器——数组和向量。并非所有语言都提供这些类型；例如，Python 两者都没有，而是提供列表。然而，对于 C++，我们被宠坏了，无法选择。标准库包含了无数的集合来满足我们的需求，但是这两个是我们在本章中要关注的。\n\n## 阵列\n\n数组是对象的容器，因此我们可以存储许多数组，而不是在变量中存储单个值。这些都在内存中一个挨着一个，所以我们通过一个变量和一个索引来访问它们。当我们声明一个数组时，我们需要在编译时知道它的大小，因为它的内存是预先分配的:\n\n![Figure 3.3: An array diagram ](img/C14195_03_03.jpg)\n\n图 3.3:一个数组图\n\n例如，也许我们想存储一些客户的年龄；假设其中五个。我们可以做到以下几点:\n\n```cpp\n    int customerAge1;\n    int customerAge2;\n    int customerAge3;\n    int customerAge4;\n    int customerAge5;\n```\n\n这给了我们五个值，但它采用了五个变量声明，每次我们想访问客户的年龄时，我们都需要知道我们需要使用哪个变量。但是，使用数组，我们可以将所有这些数据存储在一个变量中。此外，如果你把你的注意力放回到*第二章，控制流*，我们看到了如何使用循环来迭代数组，这是另一个非常有用的属性。因此，让我们将这些数据存储在一个数组中。\n\n我们声明数组如下:\n\n```cpp\n    type arrayName [numberOfElements]\n```\n\n因此，在前面的例子中，我们可以这样做:\n\n```cpp\n    int customerAges[5];\n```\n\n请注意，这只是在内存中为五个`int`值创建了空间，以便并排放置。它还没有给这些整数中的任何一个赋值，这意味着此时它们将包含垃圾。如果我们在正确初始化数组之前尝试访问它的元素，我们可以看到这一点，如下面的代码片段所示:\n\n注意\n\n我们将很快介绍访问数组值，所以不要担心以下语法对您来说是否是新的。\n\n```cpp\n    int customerAges[5];\n    std::cout << customerAges[0] << std::endl;\n    std::cout << customerAges[1] << std::endl;\n    std::cout << customerAges[2] << std::endl;\n    std::cout << customerAges[3] << std::endl;\n    std::cout << customerAges[4] << std::endl;\n```\n\n如果我们运行这段代码，我们会得到垃圾数据，因为我们还没有给集合中的单个整数赋值:\n\n![Figure 3.4: Since our array is uninitialized, our values hold garbage data ](img/C14195_03_04.jpg)\n\n图 3.4:由于我们的数组未初始化，我们的值保存垃圾数据\n\n让我们看看如何补救。\n\n## 初始化\n\n为了用值初始化数组，C++ 给了我们许多选项，所有这些选项都使用了大括号`{` `}`。当我们定义数组时，我们可以通过将每个元素放在大括号中并将其分配给我们的新数组来显式地赋予它们一个值:\n\n```cpp\n    int customerAges[5] = {1, 2, 3, 4, 5};\n```\n\n这是一个完整的初始化，因为我们声明了一个包含五个元素的数组，并传递了五个值，每个值一个。如果我们重新运行前面的代码，我们将看到所有值现在都有效:\n\n![Figure 3.5: With the array properly initialized, we have valid data ](img/C14195_03_05.jpg)\n\n图 3.5:数组正确初始化后，我们有了有效的数据\n\n当我们像这样初始化一个数组，为每个元素传入一个值时，我们可以省略方括号中的大小，因为编译器能够为我们计算出来。在本例中，我们传入了五个元素，因此将创建一个这样大小的数组。这意味着以下两个数组声明是有效的，并导致相同的数组:\n\n```cpp\n    int customerAges[5] = {1, 2, 3, 4, 5};\n    int customerAges[] = {1, 2, 3, 4, 5};\n```\n\n我们还可以通过为我们的一些元素提供值来提供部分初始化，但不是全部:\n\n```cpp\n    int customerAges[5] = {1, 2, 3};\n```\n\n如果我们进行这种更改，然后重新运行前面的代码和我们的三个初始化值，接着是最后两个包含垃圾的值，那么我们将会得到这样的结果:\n\n![Figure 3.6: With partial initialization, we have a mix of our defined values and a default ](img/C14195_03_06.jpg)\n\n图 3.6:通过部分初始化，我们混合了我们定义的值和默认值\n\n我们得到了初始化值和默认值的混合。这是因为 C++ 会将空大括号视为默认值，因此缺失的元素会被视为默认值。作为这种行为的扩展，我们甚至可以用一组空的括号来初始化数组，所有元素都将被赋予这个默认值:\n\n```cpp\n    int customerAges[5] = {};\n```\n\n在这种情况下，输出如下:\n\n![Figure 3.7: All our elements have default values since we used empty brackets ](img/C14195_03_07.jpg)\n\n图 3.7:因为我们使用了空括号，所以我们所有的元素都有默认值\n\n这里需要注意的是，虽然传入的元素少于数组所能容纳的元素(在本例中是三个，而数组的大小是五)，但反过来就不行了。也就是说，传入的元素不能超过数组所能容纳的数量。请考虑以下陈述:\n\n```cpp\n    int customerAges[5] = {1, 2, 3, 4, 5, 6};\n```\n\n我们声明了一个大小为五的数组，但是试图初始化六个元素。值得庆幸的是，编译器会发出警告，我们的错误可以在造成损害之前得到纠正:\n\n![Figure 3.8: Trying to initialize too many elements throws a compiler error ](img/C14195_03_08.jpg)\n\n图 3.8:试图初始化太多元素会引发编译器错误\n\n最后，自 C++ 11 以来，我们已经能够直接用大括号初始化成员数组，这意味着不再需要`=`符号。实际上，这意味着以下两个数组声明是相同的，并且将产生相同的数组:\n\n```cpp\n    int customerAges[5] = {1, 2, 3, 4, 5};\n    int customerAges[5] {1, 2, 3, 4, 5};\n```\n\n## 访问元素\n\n由于我们现在在一个集合中用一个变量名存储多个值，我们需要一种单独访问元素的方法。为此，我们使用指数。它们放在变量名后面的方括号中，表示我们要获取集合中的哪个元素:\n\n```cpp\n    int myArray[5] {1, 2, 3, 4, 5};\n    int mySecondValue = myArray[1];\n```\n\n需要注意的是，在 C++ 和大多数其他语言中，索引从`0`开始，而不是`1`。在前面的例子中，这意味着我们将输出数字 2，而不是 1。同样重要的是不要试图访问不存在的元素。例如，在前面的数组中，我们总共有 5 个元素，这意味着索引 0-4 是有效的。如果我们试图访问索引为 5 的元素，我们的应用将会崩溃。\n\n让我们看看下面的片段:\n\n```cpp\n    int myArray[5] {1, 2, 3, 4, 5};\n    int mySecondValue = myArray[5];\n```\n\n在这段代码中，我们的数组只有五个元素，但是我们试图访问第六个元素。这将读取不属于我们阵列的内存，并且几乎总是会导致崩溃。因此，我们必须确保在访问元素时使用有效的索引。\n\n有几种方法可以做到这一点。一种更经典的方法是找到整个数组的大小，找到一个元素的大小，然后对它们进行划分，计算它包含多少个元素:\n\n```cpp\n    sizeof(myArray)/sizeof(myArray[0])\n```\n\nC++ 11 给了我们`std::array`，它的长度是可访问的。这可通过`<array>`标题访问:\n\n```cpp\n    std::array<int, 5> myArray {1, 2, 3, 4, 5};\n    std::cout << myArray.size() << std::endl;\n```\n\n最后，C++ 17 给了我们`std::size()`，一个返回两个标准容器或一个 C 风格数组的元素计数的函数:\n\n```cpp\n    std::array<int, 5> myArray {1, 2, 3, 4, 5};\n    std::cout << std::size(myArray) << std::endl;\n    int myArray[5] = {1, 2, 3, 4, 5};\n    std::cout << std::size(myArray) << std::endl;\n```\n\n注意\n\n您的编译器必须启用 C++ 17 支持才能使用。\n\n无论我们试图完成什么，我们通常都有多种选择；这一切都是为了找到最适合每个场景的。\n\n## 阵列存储器\n\n假设数组中的所有值都并排存储在内存中，我们可以通过指定一个索引轻松地获取其中的任何一个值。我们在 C++ 数组中的第一个索引总是 0，并且在内存中；这是我们数组结构的开始。下一个元素的索引为 1。所以，为了得到它，我们从 0 开始，通过元素的大小乘以我们的索引在内存中前进。在这种情况下，一个整数是 4 个字节，我们需要索引 1，所以我们将在数组的开始前寻找 4 个字节，在那里我们将找到我们的元素:\n\n![Figure 3.9: Memory access ](img/C14195_03_09.jpg)\n\n图 3.9:内存访问\n\n如果我们单独打印出元素的内存地址，我们就可以看到这一点。我们不打算在这里详细讨论——我们将在后面的章节中适当地讨论——但是 C++ 中的&运算符(`&`)获取它后面的对象的内存地址。我们可以用它来观察我们的元素在记忆中的位置。\n\n以下代码是一个示例:\n\n```cpp\n    int customerAges[] = {1, 2, 3, 4, 5};\n    std::cout << &customerAges[0] << std::endl;\n    std::cout << &customerAges[1] << std::endl;\n    std::cout << &customerAges[2] << std::endl;\n    std::cout << &customerAges[3] << std::endl;\n    std::cout << &customerAges[4] << std::endl;\n```\n\n如果我们运行前面的代码，我们将看到每个元素的地址:\n\n![Figure 3.10: Printing the address of each element shows the addresses are incremented by 4 bytes ](img/C14195_03_10.jpg)\n\n图 3.10:打印每个元素的地址显示地址增加了 4 个字节\n\n内存地址以十六进制格式存储(基数 16)，但是我们可以看到第一个地址，元素 **0** ，以 **50** 结束。如果我们接着看下一个地址，元素 1，它以 **54** 结束，因为它的值增加了 4 个字节。4 字节是整数的大小，所以这是有意义的。如果我们再看下一个，元素 3，它的内存地址在 **58** 结束。这比元素 1 多了 4 个字节，比元素 0 多了 8 个字节，展示了我们的索引如何让我们导航内存来处理数组中的单个值。\n\n## 练习 14:实现存储用户名的容器\n\n在本练习中，我们将编写一个小应用，将用户名存储在一个数组中，并允许稍后再次获取用户名:\n\n注意\n\n这个练习的完整代码可以在这里找到:[https://packt.live/35kFKix](https://packt.live/35kFKix)。\n\n1.  我们将从定义一个宏开始，该宏将决定我们的系统将保存多少名称，我们将使用它来初始化一个大小正确的数组:\n\n    ```cpp\n    // Arrays exercise.\n    #include <iostream>\n    #include <string>\n    #define NAME_COUNT 5\n    int main()\n    {\n        std::string names[NAME_COUNT];\n    ```\n\n2.  接下来，我们有一点输入输出，我们想问我们的用户正确的名字数量。我们可以像在前面的练习中一样，对此使用`for`循环。当我们使用`getline`获取输入时，我们将使用`for`循环的索引将其直接放入我们的数组中:\n\n    ```cpp\n        std::cout << \"Please input usernames.\" << std::endl;\n        for (int i = 0; i < NAME_COUNT; ++ i)\n        {\n            std::cout << \"User \" << i + 1 << \": \";\n            std::getline(std::cin, names[i]);\n        }\n    ```\n\n3.  现在我们已经将用户名存储在阵列中，我们希望允许用户选择任意数量的用户名。我们在*章节**2**控制* *流程*中看到了如何使用`while`循环来实现这一点，我们将在这里采用相同的方法。该循环将允许用户连续选择要查看的记录的索引，或者如果他们想要退出应用，可以输入`-1` 的索引。\n\n    ```cpp\n        bool bIsRunning = true;\n        while (bIsRunning)\n        { \n            int userIndex = 0;\n            std::string inputString = \"\";\n            std::cout << \"Enter user-id of user to fetch or -1 to quit: \";\n            std::getline(std::cin, inputString);\n            userIndex = std::stoi(inputString);\n            if (userIndex == -1)\n            {\n                bIsRunning = false;\n            }\n    ```\n\n4.  We're now at the final section of our application, where we want to fetch a user record based on the index. We need to be careful here to ensure that the index that the user has passed in is valid. We saw earlier in the chapter what happens if that's not the case.\n\n    首先，我们知道我们能拥有的最低指数是`0`，所以任何低于这个数值都是无效的。我们还知道数组的大小`NAME_COUNT`，由于我们从`0`开始计数，我们的最大有效索引将是`NAME_COUNT – 1`。如果用户指定的索引符合这两个条件，那么很好，我们可以使用它。如果没有，我们将打印一个错误并让他们重新挑选:\n\n    ```cpp\n            else\n            {\n                if (userIndex >= 0 && userIndex < NAME_COUNT)\n                {\n                    std::cout << \"User \" << userIndex << \" = \" \n                        << names[userIndex] <<std::endl;\n                }\n                else\n                {\n                    std::cout << \"Invalid user index\" << std::endl;\n                }\n            }\n        }\n    }\n    ```\n\n    这应该是一切。我们定义数组，收集用户记录，然后允许用户再次获取它们，确保他们提供给我们的索引是有效的。让我们运行应用并测试它:\n\n![Figure 3.11: Our small name records application, which allows users to store and fetch name records ](img/C14195_03_11.jpg)\n\n图 3.11:我们的小名记录应用，它允许用户存储和获取姓名记录\n\n在本练习中，我们使用了一个数组来动态存储名称。我们可以通过为我们的每个名称使用单独的字符串变量来实现类似的功能，但这不是动态的。我们必须单独实现额外的名称，而使用这种方法，我们只需要更改在应用顶部定义的宏。我们还仔细检查了我们在数组中使用的索引的健全性，这在这种情况下尤其重要，因为它是由用户提供的。\n\n## 多维数组\n\n我们已经看到了数组是如何用来存储对象集合的，没有什么能阻止我们存储数组的数组。这些被称为多维数组，起初可能会令人困惑，但它们非常有用。\n\n到目前为止，我们使用的阵列都是一维的(1D)；也就是说，它们的元素完全是线性的，可以用一行来表示，如下图所示:\n\n![Figure 3.12: A 1D array ](img/C14195_03_12.jpg)\n\n图 3.12:1D 阵列\n\n如果我们将数组看作一个值表(如上)，要访问一个值，我们只需要指定列号。这是我们以前使用的单一索引。然而，有可能增加我们使用的行数，当我们这样做时，我们创建了一个二维(2D)数组:\n\n![Figure 3.13: A 2D array ](img/C14195_03_13.jpg)\n\n图 3.13:2D 阵列\n\n正如我们在这里看到的，我们现在必须使用多行，而不是将数据映射到一行。这意味着我们能够存储更多的数据，但是它引入了对第二个索引的需求，因为我们现在需要指定行和列。\n\n在代码中声明 2D 数组非常类似于 1D 数组。不同的是，对于 2D 数组，我们需要提供两个大小值:一个用于行计数，另一个用于列计数。因此，上图中所示的数组定义如下:\n\n```cpp\n    int myArray[3][5];\n```\n\n与 1D 数组一样，我们也可以在声明值的同时初始化它们。由于我们现在有多行，我们在它自己的花括号嵌套集中初始化每一行。初始化我们刚刚定义的数组如下所示:\n\n```cpp\n    int myArray[3][5] { {1, 2, 3, 4, 5}, {1, 2, 3, 4, 5}, {1, 2, 3, 4, 5} };\n```\n\n理论上，数组可以由任意多个维度组成；它们不仅仅局限于两个。然而，在实践中看到二维以上的阵列并不常见，因为它们的复杂性和内存占用成为了影响因素。\n\n## 练习 15:使用多维数组存储更多数据\n\n让我们扩展之前的应用，也存储用户的姓氏。我们将使用多维数组来实现这一点:\n\n注意\n\n这个练习的完整代码可以在这里找到:[https://packt.live/33cBVuE](https://packt.live/33cBVuE)。\n\n1.  将*练习 14* 的最终输出复制到代码窗口。\n2.  我们将首先更新保存名称的数组。我们希望这是二维的，其中每个记录有两个值——一个名字和一个姓氏:\n\n    ```cpp\n         std::string names[NAME_COUNT][2] {\"\"};\n    ```\n\n3.  接下来，在我们当前从用户那里获取名字并将它们读入`names[i]`的地方，我们需要改为要求名字。我们还需要指定第二个索引，我们将在其中存储这个输入。既然是`Forename`，那就要指数`0` :\n\n    ```cpp\n        std::cout << \"User \" << i + 1 << \" Forename: \";\n        std::getline(std::cin, names[i][0]);\n    ```\n\n4.  然后我们会再做同样的事情，这次是问姓。由于我们现在存储第二个元素，我们将希望使用索引`1`而不是`0` :\n\n    ```cpp\n        std::cout << \"User \" << i + 1 << \" Surname: \";\n        std::getline(std::cin, names[i][1]);\n    ```\n\n5.  最后的`for`循环现在应该如下:\n\n    ```cpp\n        for (int i = 0; i < NAME_COUNT; ++ i)\n        {\n            std::cout << \"User \" << i + 1 << \" Forename: \";\n            std::getline(std::cin, names[i][0]);\n            std::cout << \"User \" << i + 1 << \" Surname: \";\n            std::getline(std::cin, names[i][1]);\n        }\n    ```\n\n6.  最后，我们需要更新我们的输出，以包括这两个名称。我们目前只是打印`names[userIndex]`，所以我们需要更新它以同时使用第一和第二个指数— `Forename`和`Surname`，分别为:\n\n    ```cpp\n        std::cout << \"User \" << userIndex << \" = \" << names[userIndex][0]              << \" \" << names[userIndex][1] << std::endl;\n    ```\n\n7.  完整的代码如下:\n\n    ```cpp\n    // Arrays exercise.\n    #include <iostream>\n    #include <string>\n    #define NAME_COUNT 5\n    int main() \n    {\n        std::string names[NAME_COUNT][2] {\"\"};\n        std::cout << \"Please input usernames.\" << std::endl;\n        for (int i = 0; i < NAME_COUNT; ++ i) \n        {\n            std::cout << \"User \" << i + 1 << \" Forename: \";\n            std::getline(std::cin, names[i][0]);\n            std::cout << \"User \" << i + 1 << \" Surname: \";\n            std::getline(std::cin, names[i][1]);\n        }\n        bool bIsRunning = true;\n        while (bIsRunning) \n        {\n            int userIndex = 0;\n            std::string inputString = \"\";\n            std::cout << \"Enter user-id of user to fetch or -1 to quit: \";\n            std::getline(std::cin, inputString);\n            userIndex = std::stoi(inputString);\n            if (userIndex == -1) \n            {\n                bIsRunning = false;\n            } \n            else \n            {\n                if (userIndex >= 0 && userIndex < NAME_COUNT) \n                {\n                    std::cout << \"User \" << userIndex << \" = \"                           << names[userIndex][0] << \"\" \"\"                           << names[userIndex][1] << std::endl;\n                } \n                else \n                {\n                    std::cout << \"Invalid user index\" << std::endl;\n                }\n            }\n        }\n    }\n    ```\n\n8.  运行应用。输入一些虚拟名称，并检查我们是否可以正确访问它们，显示两个名称:\n\n![Figure 3.14: We've stored more data by adding another dimension to our names array ](img/C14195_03_14.jpg)\n\n图 3.14:通过向我们的名称数组添加另一个维度，我们存储了更多的数据\n\n通过在我们的名字数组中添加第二个维度，我们能够存储更多的信息——在本例中是一个姓氏。\n\n## 向量\n\n向量类似于数组，因为它们在内存中连续存储元素集合，但向量具有动态大小。这意味着我们不需要在编译时知道它们的大小；我们可以定义一个向量，随意添加/删除元素。考虑到这一点，他们谨慎地管理自己的规模。\n\n每次向量需要增长时，它必须找到并分配正确的内存量，然后将所有元素从原始内存位置复制到新的位置——这是一项繁重的任务。因此，它们不会随着每次插入而增长，而是会分配比实际需要更多的内存。这给了它们一个缓冲区，在这里可以添加许多元素，而不需要另一个增长操作。然而，当达到极限时，它们将不得不再次增长。\n\n这种动态调整大小的能力使它们在需要存储的元素数量波动时比数组更受欢迎。以注册应用为例，其中将注册的用户数量未知。如果我们在这里使用数组，我们将不得不选择一个任意的上限，并声明一个这样大小的数组。除非应用已满，否则这将导致大量空间浪费。同样，如果我们将上限设置为 1000 个用户，而只注册 100 个，那就浪费了很多空间。我们还强制规定了可注册人数的绝对上限。如果我们在这种情况下使用向量，这些问题就会得到缓解。\n\n声明向量的操作如下:\n\n```cpp\n    std::vector<int> myVector;\n```\n\n在这一点上，向量不包含任何元素，但是在我们看如何添加它们之前，我们要看如何访问它们。这将为我们接下来要做的练习做好准备，我们将迭代向量，打印出每个元素。\n\n## 访问元素\n\n要访问向量中的元素，我们有几个选项。首先，由于向量将其元素连续存储在内存中，就像数组一样，我们可以使用`[]`运算符来访问它们:\n\n```cpp\n    int myFirstElement = myVector[0];\n    int mySecondElement = myVector[1];\n```\n\n请记住，元素从索引`0`开始，所以对于我们的第二个元素，我们想要索引`1`。我们还需要考虑与数组相同的问题，比如确保我们总是使用有效的索引。值得庆幸的是，向量为我们提供了一个`at`函数，它的行为非常类似于`[]`运算符，并增加了一个检查来确保索引有效。\n\n例如，要像刚才那样获取第一个和第二个元素，但是使用`at`函数，我们将执行以下操作:\n\n```cpp\n    int myFirstElement = myVector.at(0);\n    int mySecondElement = myVector.at(1);\n```\n\n这里的关键区别在于，如果我们将一个越界索引传递给`at`函数，而不是未定义的行为，那么该函数将抛出一个异常。异常将在*第 13 章*、*异常**c++ 中进行处理*，但是它们允许我们以安全的方式捕获和处理错误，而不会导致我们的应用崩溃。\n\n现在，我们已经了解了如何访问向量的元素，让我们编写一个小应用，它可以循环遍历并打印出所有元素。这将是非常有用的，因为我们正在考虑添加和删除元素。\n\n## 练习 16:在向量上循环\n\n在本节中，我们将以各种方式与向量进行交互，因此能够通过单个函数调用来可视化向量将非常有用。在继续之前，让我们编写一个小应用来实现这一点。以下是完成练习的步骤:\n\n注意\n\n这个练习的代码文件可以在这里找到:[https://packt.live/2QCGTxZ](https://packt.live/2QCGTxZ)。\n\n1.  首先，初始化一个`int`类型的向量:\n\n    ```cpp\n    // Vector example.\n    #include <iostream>\n    #include <string>\n    #include <vector>\n    std::vector<int> myVector;\n    ```\n\n2.  接下来，定义一个名为`PrintVector`的函数。我们将在这里编写打印矢量内容的功能:\n\n    ```cpp\n    void PrintVector()\n    {\n    }\n    ```\n\n3.  要访问向量中的元素，我们可以使用索引，就像我们以前访问数组一样。为此使用`for`循环，使用索引访问向量中的各种元素。在函数的最后，我们将打印出几行空白行作为间隔符:\n\n    ```cpp\n    void PrintVector()\n    {\n        for (int i = 0; i < myVector.size(); ++ i)\n        {\n            std::cout << myVector[i];\n        }\n        std::cout << \"\\n\\n\";\n    }\n    ```\n\n4.  最后，在`main`中添加对我们新的`PrintVector`函数的调用:\n\n    ```cpp\n    int main()\n    {\n        PrintVector();\n    }\n    ```\n\n5.  完整的代码如下:\n\n    ```cpp\n    // Vector example.\n    #include <iostream>\n    #include <string>\n    #include <vector>\n    std::vector < int > myVector;\n    void PrintVector() \n    {\n        for (int i = 0; i < myVector.size(); ++ i) \n        {\n            std::cout << myVector[i];\n        }\n        std::cout << \"\\n\\n\";\n    }\n    int main() \n    {\n        PrintVector();\n    }\n    ```\n\n6.  Run the program. We've not yet initialized `myVector` with any data, so there won't be any output, but we can confirm that it's compiling without errors:\n\n    ![Figure 3.15: The program should compile without any errors ](img/C14195_03_15.jpg)\n\n    图 3.15:程序应该编译没有任何错误\n\n7.  我们将很快使用这个应用，所以保持它在编译器中打开。现在，让我们将一些数据添加到向量中。\n\n## 初始化\n\n与数组一样，我们有许多选项可以用数据初始化向量。我们将研究的第一种方法是单独指定元素。下面的初始化将给出一个包含五个元素的向量，其值如下:`1`、`2`、`3`、`4`和`5`:\n\n```cpp\n    std::vector<int> myVector {1, 2, 3, 4, 5};\n```\n\n我们还可以指定向量的大小，以及每个元素的默认值。下面的初始化将给出一个包含三个元素的向量，所有元素的值都是`1`:\n\n```cpp\n    std::vector<int> myVector(3, 1);\n```\n\n最后，可以从现有的数组和向量中创建向量。这是通过传入它们的起始和结束内存位置来实现的。这将分别按如下方式进行:\n\n```cpp\n    std::vector<int> myVector(myArray, myArray + myArraySize);\n    std::vector<int> myVector(myVector2.begin(), myVector2.end());\n```\n\n## 修改元素\n\n与初始化一样，向量中的元素可以通过多种方式添加/移除。要在向量末尾添加一个元素，我们可以使用`push_back()`函数。同样，要从向量的末尾移除一个元素，我们可以使用`pop_back()`:\n\n```cpp\n    myVector.push_back(1);\n    myVector.pop_back();\n```\n\n在这个片段中，我们将元素`1`添加到向量的后面，然后立即移除它。\n\n我们还可以使用`insert`和`erase`函数更精确地添加和移除矢量。这两种方法都使用迭代器来确定操作应该发生在数组的什么位置。我们现在不打算详细讨论迭代器，但是它们是允许我们遍历集合的对象。\n\n要在特定位置添加和移除向量中的元素，我们将执行以下操作:\n\n```cpp\n    myVector.erase(myVector.begin() + 1);\n    myVector.insert(myVector.begin() + 2, 9);\n```\n\n在这个例子中，我们使用`begin()`方法，该方法返回指向向量中第一个元素的迭代器。然后，我们可以添加一个偏移量来获得我们想要的元素。记住索引从`0`开始，我们将删除索引`1`处的元素，然后添加一个索引为`2`的元素——向量中的第二个和第三个元素。\n\n注意\n\n迭代器是通过“指向”集合中的项目来帮助我们迭代集合的对象。它们包含在第 12 章、*容器和迭代器*中。您也可以参考以下文档了解更多详情:[https://packt.live/37rHlVA](https://packt.live/37rHlVA)。\n\n让我们使用这些函数用数据初始化一个向量，然后通过在不同的位置添加和移除元素来修改它。\n\n## 练习 17:修改向量\n\n在本练习中，我们将通过添加和移除元素来修改向量。我们将利用我们在前面的练习中创建的应用，在各个步骤之间打印出我们的矢量，这样我们就可以清楚地看到我们在做什么。以下是完成本练习的文件:\n\n注意\n\n这个练习的代码文件可以在这里找到:[https://packt.live/2QEZAB4](https://packt.live/2QEZAB4)。\n\n1.  将我们在*练习 16、* *中创建的程序复制到一个向量*上，如果编译器窗口还没有的话。\n2.  将当前向量定义替换为同时用以下元素初始化向量的定义:`1`、`2`、`3`、`4`和`5` :\n\n    ```cpp\n        std::vector<int> myVector {1, 2, 3, 4, 5};\n    ```\n\n3.  接下来，在`main`函数中，调用`PrintVector`后，使用`pop_back`从向量中移除最后一个元素。立即拨打另一个电话至`PrintVector()` :\n\n    ```cpp\n        myVector.pop_back();\n        PrintVector();\n    ```\n\n4.  使用`push_back`功能在向量后面添加一个值为`6`的新元素。同样，接下来请致电`PrintVector()` :\n\n    ```cpp\n        myVector.push_back(6);\n        PrintVector();\n    ```\n\n5.  用`erase`函数去掉向量中的第二个元素。接下来再打一个电话给`PrintVector()` :\n\n    ```cpp\n        myVector.erase(myVector.begin() + 1);\n        PrintVector();\n    ```\n\n6.  最后，使用插入操作符在第四个位置插入一个值为`8`的元素。接下来是对`PrintVector()` :\n\n    ```cpp\n        myVector.insert(myVector.begin() + 3, 8);\n        PrintVector();\n    ```\n\n    的最后一次通话\n7.  完整的代码如下:\n\n    ```cpp\n    // Vector example.\n    #include <iostream>\n    #include <string>\n    #include <vector>\n    std::vector<int> myVector {1, 2, 3, 4, 5};\n    void PrintVector()\n    {\n        for (int i = 0; i < myVector.size(); ++ i)\n        {\n            std::cout << myVector[i];\n        }\n        std::cout << \"\\n\\n\";\n    }\n    int main()\n    {\n        PrintVector();\n        myVector.pop_back();\n        PrintVector();\n        myVector.push_back(6);\n        PrintVector();\n        myVector.erase(myVector.begin() + 1);\n        PrintVector();\n        myVector.insert(myVector.begin() + 3, 8);\n        PrintVector();\n    }\n    ```\n\n8.  Run the application and observe the state of the vector after each step.\n\n    ![Figure 3.16: We've manipulated the elements in our vector by means of a number of methods ](img/C14195_03_16.jpg)\n\n图 3.16:我们已经通过许多方法操纵了向量中的元素\n\n在这个应用中，我们已经用值初始化了一个数组，然后通过许多方法修改了它们。我们有简单的`push` / `pop`功能来添加/移除数组后面的项目。我们还能够通过使用`insert` / `erase`功能更具体地说明在哪里添加/删除值。通过用`for`循环迭代向量，我们能够打印出每个阶段的元素，这样我们就可以清楚地看到我们所做的修改的效果。\n\n还有其他可用的容器，如堆栈、树和链表，每种容器都有其优缺点。你用哪一个取决于你的情况，因为通常没有单一的正确答案。数组和向量是一个很好的起点，将为我们继续学习提供必要的工具。当你继续前进的时候，一定要分支到这些不同的容器中，看看它们是如何表现的，以及在给定的情况下它们是如何最好地为你服务的。\n\n# 类/结构\n\nC++ 提供的基本类型是一个很好的起点，但是这些是应用中唯一需要的变量类型是很少见的。当我们表示真实世界的信息时，例如用户记录或对象的各种属性，我们通常需要更复杂的数据类型来存储信息。C++ 允许我们在类和结构中创建这样的类型。在后面的章节中将会更详细地介绍类，但是现在，我们将简单地介绍一些关键的概念。\n\n## 类\n\n类是变量和功能的集合，封装在一个对象中。当我们定义一个类时，我们正在为这个对象创建一个蓝图。这意味着每次我们想要创建一个这种类型的对象时，我们都使用这个蓝图来构建我们的对象。类是 C++ 的核心部分；毕竟，C++ 最初被命名为 *C，带有类*。\n\n默认情况下，C++ 类中声明的成员(变量和函数)是私有的。这意味着它们只能被类本身访问，因此不能被外部类访问。然而，这可以通过使用访问修饰符来改变；我们很快会谈到这些。类也可以相互继承，但这将在*章**8**类和结构*中介绍。\n\n用 C++ 声明类的语法如下:\n\n```cpp\nclass MyClassName:\n{\nAccess Modifier:\n    data members.\n    member functions.\n}\n```\n\n使用这个语法，让我们定义一个简单的类:\n\n```cpp\n// Class example. \n#include <iostream>\n#include <string>\nclass MyClass \n{\n    int myInt = 0;\npublic:\n    void IncrementInt() \n    {\n        myInt++ ;\n        std::cout << \"MyClass::IncrementInt: \" << myInt;\n    };\n};\nint main() \n{\n    MyClass classObject;\n    classObject.IncrementInt();\n}\n```\n\n在这段代码中，我们定义了一个名为`MyClass`的小类，它包含一个变量和一个函数。第一个是私有的，因此只能通过类本身访问，另一个是公共的，因此可以从类所在的任何地方访问。\n\n在我们的`main`函数中，我们声明了类的一个实例。这给了我们一个对象，`classObject`，它包含了我们在`MyClass`中定义的所有属性和功能。既然我们定义了一个`public`函数，`IncrementInt`，我们可以通过那个类对象来调用它:\n\n![Figure 3.17: Running our code, we can see that our member function was called ](img/C14195_03_17.jpg)\n\n图 3.17:运行我们的代码，我们可以看到我们的成员函数被调用\n\n## 结构\n\n**结构**与类非常相似。两者的区别在于，默认情况下，类成员是私有的，而在结构中，它们是公共的。因此，我们倾向于使用结构来定义主要用于存储数据的对象。如果我们有一个存储数据的对象，但是它有许多相关的功能，那么它通常被定义为一个类。\n\n良好使用结构的一个简单例子是存储坐标。包括一个`x`和一个`y`值，我们可以只定义两个独立的浮点变量。然而，这种方法要求每个坐标有两个变量。然后我们必须管理它们，保持它们在一起，等等。定义一个`struct`更容易，它将那些单独的变量封装并包含在一个逻辑单元中。\n\n声明一个`struct`和声明一个类几乎一样，但是我们用`struct`替换`class`关键字:\n\n```cpp\n// Struct example. \n#include <iostream>\n#include <string>\nstruct Coordinate \n{\n    float x = 0;\n    float y = 0;\n};\nint main() \n{\n    Coordinate myCoordinate;\n    myCoordinate.x = 1;\n    myCoordinate.y = 2;\n    std::cout << \"Coordinate: \" << myCoordinate.x << \", \"               << myCoordinate.y;\n}\n```\n\n在这里，我们已经在`struct`中定义了我们的坐标，由于成员默认是公共的，所以我们不必担心访问修饰符。我们可以简单地声明该类的一个实例，并在代码中开始使用它的成员，而不必担心这个问题。下面是通过运行前面的代码获得的输出:\n\n![Figure 3.18: We're about to access our struct members by default as they're public ](img/C14195_03_18.jpg)\n\n图 3.18:我们将默认访问我们的结构成员，因为它们是公共的\n\n在定义和实例化类和结构方面，我们将把它留在那里。当我们深入研究 OOP 时，它们将在后面的章节中详细介绍，因此简单地熟悉它们的语法就足够了。我们在本章后面还有一个简短的练习，但是让我们看一下访问修饰符。\n\n## 访问修饰符\n\n如前所述，类和结构的区别在于成员变量和函数的默认可见性。这并不是说它们不能被改变。在声明这些成员时，我们有以下三个可用的访问修饰符:\n\n*   Public—任何声明为 public 的成员都可以从该类所在的任何位置访问。\n*   私有—任何声明为私有的成员只对定义它们的类和友元函数可用。\n*   Protected—Protected members are similar to private members, with the addition that child classes can access them.\n\n    注意\n\n    子类是从基类继承的类。这将在*第 10 章*、*高级面向对象原理*中详细介绍。\n\n通过用这些关键字定义我们的成员，我们可以控制它们对我们的应用的可见性。使用这些修饰符的语法如下:\n\n```cpp\nclass MyClass\n{\npublic:\n    // Any members declared from this point forth will be public.\nprotected:\n    // Any members declared from this point forth will be protected.\nprivate:\n    // Any members declared from this point forth will be private.\n};\n```\n\n我们通过调用`public` / `protected` / `private`关键字在可访问性组中定义成员，然后随后声明的成员将具有该可见性。您可以在类定义中多次使用这些修饰符；您不局限于像这样严格的组，但是如果成员被整齐地分组，它会使您的代码更易读。\n\n## 练习 18:使用可访问性修饰符控制访问\n\n作为一个简短的练习，让我们在前面的类模板中添加一些成员，看看这会如何影响我们如何使用它们。对于每个可见性修饰符，我们将定义一个整数变量，然后尝试访问它。这将向我们展示不同的可访问性修饰符将如何影响我们在实践中的变量:\n\n注意\n\n这个练习的完整代码可以在这里找到:[https://packt.live/35nNi4h](https://packt.live/35nNi4h)。\n\n1.  分别声明`myPublicInt`、`myProtectedInt`和`myPrivateInt`为`public`、`protected`和`private`变量:\n\n    ```cpp\n    // Accessibility example.\n    #include <iostream>\n    #include <string>\n    class MyClass\n    {\n    public:\n        int myPublicInt = 0;\n    protected:\n        int myProtectedInt = 0;\n    private:\n        int myPrivateInt = 0;\n    };\n    ```\n\n2.  接下来，实例化`MyClass`类的一个实例，并尝试访问我们刚刚在`cout`语句中定义的每个成员:\n\n    ```cpp\n    int main()\n    {\n        MyClass testClass;\n        std::cout << testClass.myPublicInt << \"\\n\";\n        std::cout << testClass.myProtectedInt << \"\\n\";\n        std::cout << testClass.myPrivateInt << \"\\n\";\n    } \n    ```\n\n3.  完整的代码如下:\n\n    ```cpp\n    // Accessibility example.\n    #include <iostream>\n    #include <string>\n    class MyClass \n    {\n    public:\n        int myPublicInt = 0;\n    protected:\n        int myProtectedInt = 0;\n    private:\n        int myPrivateInt = 0;\n    };\n    int main() \n    {\n        MyClass testClass;\n        std::cout << testClass.myPublicInt << \"\\n\";\n        std::cout << testClass.myProtectedInt << \"\\n\";\n        std::cout << testClass.myPrivateInt << \"\\n\";\n    }\n    ```\n\n4.  运行代码，让我们看看编译器给了我们什么:\n\n![Figure 3.19: Only our public member variables are accessible; the others throw errors ](img/C14195_03_19.jpg)\n\n图 3.19:只有我们的公共成员变量可以访问；其他人抛出错误\n\n我们可以看到只有我们的公共成员变量是可访问的。另外两个抛出错误，声明它们是受保护的和私有的；因此，我们不能像以前那样使用它们。在构建应用时，为成员提供正确的可访问性是一个很好的实践，这样我们就可以确保我们的数据只按照我们希望的方式使用和访问。\n\n为了使用方便，有时将变量和函数公开会让人觉得很有诱惑力，尤其是当我们的应用变得更大、更复杂的时候；处理谁能从哪里看到什么可能会成为一个有点任务。但是，对我们的数据和功能的适当访问不应受到损害；这对于创建不会被滥用的安全系统至关重要。在*第 9 章，面向对象原则*中，我们将介绍`getter` / `setter`范例，通过该范例，我们定义了允许以安全和受控的方式访问私有类成员的函数。\n\n## 构造函数/析构函数\n\n当我们在 C++ 中实例化/销毁一个对象时，我们可能想要做某些事情。例如，当一个对象被实例化时，我们可能想要为该对象做一些设置；也许给一些变量默认值，或者从某个地方获取一些信息。同样，当我们想要销毁一个对象时，我们可能首先要做一些清理。也许我们已经创建了一个临时文件，我们想删除或取消分配一些内存。C++ 让我们通过给我们构造函数和析构函数来实现这一点，当一个对象被实例化或销毁时，这些函数如果被定义，就会自动运行。\n\n对象的构造函数保证在对象被实例化时运行，但在它被用于任何地方之前。这使我们有机会执行对象正确操作所需的任何设置。为了定义一个构造函数，我们创建了一个公共函数，它的名字就是类的名字——例如，为我们的`MyClass`对象定义一个构造函数:\n\n```cpp\npublic:\n    MyClass()\n```\n\n为了看到我们的构造函数在运行，我们可以添加一个 print 语句并初始化我们的`myPublicInt`变量。在我们的应用启动时添加一个 print 语句，我们可以看到执行的顺序:\n\n```cpp\n#include <iostream>\n#include <string>\nclass MyClass \n{\npublic:\n    MyClass() \n    {\n        std::cout << \"My Class Constructor Called\\n\";\n        myPublicInt = 5;\n    }\n    int myPublicInt = 0;\n};\nint main() \n{\n    std::cout << \"Application started\\n\";\n    MyClass testClass;\n    std::cout << testClass.myPublicInt << \"\\n\";\n}\n```\n\n注意\n\n我们可以重载我们的构造函数，就像我们在前一章重载我们的普通函数一样。然而，我们不会在这一章讨论这个问题。这是进一步阅读的任务。\n\n运行前面的代码片段后，您将获得以下输出:\n\n![Figure 3.20: We can see that our constructor is called at the point our object is created ](img/C14195_03_20.jpg)\n\n图 3.20:我们可以看到，我们的构造函数是在创建对象时被调用的\n\n对象的析构函数的操作方式与构造函数非常相似，只是在对象生命周期的另一端。这给了我们执行任何清理的机会，比如取消分配内存等等。析构函数的语法与构造函数的语法相同，但前面有一个波浪号字符:\n\n```cpp\n~MyClass()\n```\n\n如果我们扩展前面的代码来声明一个析构函数，并在其中为自己打印另一条语句，我们可以看到它何时被调用。这发生在主函数结束时；应用关闭并自行清理，因此，我们的析构函数被调用，我们看到我们的语句:\n\n```cpp\n~MyClass()\n{\n    std::cout << \"My Class Destructor Called\\n\";\n}\n```\n\n![Figure 3.21: Our destructor is called at the end of the application as it performs cleanup, destroying the MyClass object ](img/C14195_03_21.jpg)\n\n图 3.21:我们的析构函数在应用执行清理时被调用，销毁了 MyClass 对象\n\n正如在本节开始时所提到的，在后面的章节中进行深入的检查之前，我们在这里只做一个简短的类和结构的介绍。我希望你从中得到的启示是:\n\n*   类和结构封装变量和行为。\n*   默认情况下，类成员是私有的，而在结构中，它们是公共的。\n*   我们可以使用访问修饰符修改成员的可见性。\n*   构造函数和析构函数可用于在对象生命周期的每一端调用代码。\n\n## 练习 19:类别/结构\n\n作为本节的一个简短练习，我们将在`class`和`struct`中封装相同的数据和功能，并再次观察它如何影响它们的使用。我们要封装的数据如下:\n\n*   整数变量\n*   到 bool 变量\n*   A function that will return a string\n\n    注意\n\n    这个练习的完整代码可以在这里找到:[https://packt.live/37rPd9B](https://packt.live/37rPd9B)。\n\n以下是完成练习的步骤:\n\n1.  让我们从创建一个封装这个行为和数据的类开始:\n\n    ```cpp\n    class MyClass\n    {\n        int myInt = 0;\n        bool myBool = false;\n        std::string GetString()\n        {\n            return \"Hello World!\";\n        }\n    };\n    ```\n\n2.  接下来，对结构执行同样的操作。这里唯一的变化就是将`class`关键字替换为`struct`。\n3.  To test how accessible our variables are, instantiate an instance of our class and make calls to each member:\n\n    ```cpp\n        MyClass classObject;\n        std::cout << \"classObject::myInt: \" << classObject.myInt << \"\\n\";\n        std::cout << \"classObject::myBool: \" << classObject.myBool               << \"\\n\";\n        std::cout << \"classObject::GetString: \" << classObject.GetString()               << \"\\n\"; \n    ```\n\n    然后，我们将对结构执行同样的操作，并运行应用。我们将看到一些关于类中成员的错误，这些成员是不可访问的，但结构却不可访问:\n\n    ![Figure 3.22: Inaccessible members due to the default private access that classes have ](img/C14195_03_22.jpg)\n\n    图 3.22:由于类具有默认的私有访问权限，成员不可访问\n\n4.  To fix this, we'll use the `public` access modifier to make our members accessible. The final code is as follows:\n\n    注意\n\n    本章前面已经说过，公开一切通常是不好的做法，这是站得住脚的；这是为了演示。在后面的章节中，当我们正确地看待 OOP 时，我们将讨论 getter/setter 架构，它允许我们以更可控的方式访问变量。\n\n    ```cpp\n    // Classes/struct exercise.\n    #include <iostream>\n    #include <string>\n    class MyClass \n    {\n    public:\n        int myInt = 0;\n        bool myBool = false;\n        std::string GetString() \n        {\n            return \"Hello World!\";\n        }\n    };\n    struct MyStruct \n    {\n        int myInt = 0;\n        int myBool = 0;\n        std::string GetString() \n        {\n            return \"Hello World!\";\n        }\n    };\n    int main() \n    {\n        MyClass classObject;\n        std::cout << \"classObject::myInt: \" << classObject.myInt << \"\\n\";\n        std::cout << \"classObject::myBool: \" << classObject.myBool               << \"\\n\";\n        std::cout << \"classObject::GetString: \" << classObject.GetString()               << \"\\n\"; \n        MyStruct structObject;\n        std::cout << \"\\nstructObject::myInt: \" << structObject.myInt               << \"\\n\";\n        std::cout << \"structObject::myBool: \" << structObject.myBool               << \"\\n\";\n        std::cout << \"structbject::GetString: \"               << structObject.GetString() << \"\\n\";\n    } \n    ```\n\n最终输出如下:\n\n![Figure 3.23: With the addition of the public access modifier, our class members become accessible ](img/C14195_03_23.jpg)\n\n图 3.23:通过添加公共访问修饰符，我们的类成员变得可访问\n\n在本练习中，我们概括了如何使用类和结构来封装行为，并利用访问修饰符来确保它们的可见性。这种对两者之间根本区别的理解将为我们在前面到达面向对象的章节提供更多的背景。\n\n# 储存寿命\n\n到目前为止，在我们编写的应用和代码中，我们已经在我们的`main`函数中声明了所有的变量。因为我们所有的其他代码也存在于这个函数中，所以我们可以完全访问所有这些变量。然而，当我们开始构建更大的应用，并开始使用函数和类时，我们需要了解*范围*和*存储寿命*。\n\n对象生存期是指一个对象对我们有效和可访问的时间。到目前为止，我们的大部分变量已经在我们的`main`函数中声明，它们的生命周期已经与我们正在编写的应用的生命周期相匹配，没有什么好担心的。只要我们想使用这个变量，它就一直存在并且有效，因为我们在同一个范围内工作。范围指的是一段代码，它表示在其中声明的对象的生存期，因此我们可以看到这些术语是如何关联的。\n\n我们使用花括号来表示范围，可以是函数中的范围，也可以是单独的范围，如下面的代码片段所示:\n\n```cpp\nvoid MyFunc()\n{\n    // scope 1\n}\nint main()\n{\n    // scope 2\n    {\n        // scope 3\n    }\n}\n```\n\n在这个例子中，我们有三个不同级别的范围:一个在`MyFunc`、(`scope 1`)、一个在`main` ( `scope 2`)以及另一个在它们自己的花括号中(`scope 3`)。我们在这个例子中声明变量的范围将直接影响我们何时何地可以使用这些数据，以及它的生命周期有多长。让我们看看这是怎么回事。\n\n## 练习 20:存储寿命示例\n\n为了清楚地了解范围如何影响我们的变量，让我们来做一个快速练习。在每个不同的范围内，我们将定义一个整数变量，并在`main`函数的末尾，尝试打印出每个变量的值。然后，我们可以检查输出以查看范围之间的差异:\n\n注意\n\n这个练习的完整代码可以在这里找到:[https://packt.live/2XE9FzJ](https://packt.live/2XE9FzJ)。\n\n1.  将前面的代码片段复制到编译器中，用整数变量定义替换关于范围的各种注释:\n\n    ```cpp\n    #include <iostream>\n    void MyFunc()\n    {\n        int myInt1 = 1;\n    }\n    int main()\n    {\n        int myInt2 = 2;\n        {\n            int myInt3 = 3;\n        }\n    ```\n\n2.  接下来，编写三个输出语句，一个用于刚刚定义的每个变量，打印它们的值。然后，用最后一个“`}`”关闭`main`功能:\n\n    ```cpp\n        // print values\n        std::cout << myInt1 << std::endl;\n        std::cout << myInt2 << std::endl;\n        std::cout << myInt3 << std::endl;\n    }\n    ```\n\n3.  完整代码如下:\n\n    ```cpp\n    #include <iostream>\n    void MyFunc() \n    {\n        int myInt1 = 1;\n    }\n    int main() \n    {\n        int myInt2 = 2; \n        {\n            int myInt3 = 3;\n        }\n        // print values\n        std::cout << myInt1 << std::endl;\n        std::cout << myInt2 << std::endl;\n        std::cout << myInt3 << std::endl;\n    }\n    ```\n\n4.  运行代码。我们的编译器会向我们抛出一些错误和警告，如下所示:\n\n![Figure 3.24: Various variables we've tried to use are not within scope, so we get errors ](img/C14195_03_24.jpg)\n\n图 3.24:我们试图使用的各种变量不在范围内，所以我们会得到错误\n\n如果我们阅读编译窗口中的错误，我们可以看到我们有两个声明`myInt1`和`myInt3`没有在这个范围内声明。当执行离开给定的范围时(例如，当执行从`MyFunc`函数返回时)，其中声明的变量被销毁，内存被回收。一旦发生这种情况，变量就不再可访问。\n\n注意\n\n情况并非总是如此。我们将在以后考虑使用指针，这样就有可能不会发生这种情况，但是现在，我们可以在这样一个前提下工作，即当范围结束时，其中的所有变量都为我们整理好了。\n\n考虑到这一点，我们可以看到为什么我们不能用我们现有的方式使用这些变量。我们的第一个变量`myInt1`的作用域是`MyFunc`函数，所以除此之外，它将是不可访问的。`myInt3`也是如此，因为它也在自己的范围内被宣布。我们能够使用的一个变量`myInt2`是在与使用它的代码相同的范围内声明的，所以没关系。\n\n随着我们的前进，熟悉范围和对象生命周期非常重要。将我们所有的变量放在尽可能高的范围内，然后不必担心它们在哪里/从哪里不可访问，这很有诱惑力。不过，这是个糟糕的设计。我们的目标应该是在尽可能小的范围内声明变量，这样我们就不会有不使用的内存。\n\n## 静态\n\nStatic 是 C++ 中的一个特殊关键字，它将对象的生存期限定在应用的生存期内。这意味着静态变量在应用中只初始化一次，因此始终保持它们的值。变量和函数都可以变成静态的，所以让我们快速看一个例子:\n\n```cpp\n// Static example. \n#include <iostream>\n#include <string>\nint MyInt() \n{\n    int myInt = 0;\n    return ++ myInt;\n}\nint main() \n{\n    for (int i = 0; i < 5; ++ i) \n    {\n        std::cout << MyInt();\n    }\n}\n```\n\n在这个例子中，我们有一个函数，它将返回一个它定义的整数，然后我们打印它的值。如您所料，如果我们按原样运行这段代码，我们将得到五个相同值的输出。每次调用该函数时，变量都被重新初始化为值`0`，递增，然后返回:\n\n![Figure 3.25: Since our variable is re-initialized each time the function is called, our output is the same ](img/C14195_03_25.jpg)\n\n图 3.25:因为我们的变量在每次调用函数时都被重新初始化，所以我们的输出是相同的\n\n但是，如果我们对此应用进行更改，并将我们的`myInt`变量定义为`static`，那么我们的程序将表现得非常不同:\n\n```cpp\n    static int myInt = 0;\n```\n\n我们的变量在应用的生命周期中只初始化一次——第一次遇到它。这意味着，虽然我们将该值初始化为`0`，但这只会被观察一次，从而允许`myInt`在不同的函数调用之间保持其值。让我们再次运行该应用:\n\n![Figure 3.26: With the variable now declared static, its value persists between function calls ](img/C14195_03_26.jpg)\n\n图 3.26:现在变量被声明为静态的，它的值在函数调用之间保持不变\n\n我们现在可以看到该值正在增加，这证实了这样一个事实，即由于 static 关键字，每次调用函数时，变量都不会被重新初始化。\n\n## 活动 3:报名申请\n\n在第三个活动中，我们将编写一个用户注册应用。这将允许用户向系统注册，提供他们的姓名和年龄，我们将以自己的自定义类型存储这些信息。我们还将为用户提供通过标识进行查找的能力，检索他们的信息。\n\n完成活动后，您应该会获得类似以下内容的输出:\n\n![Figure 3.27: Our application allows the user to add records and then recall them via an ID ](img/C14195_03_27.jpg)\n\n图 3.27:我们的应用允许用户添加记录，然后通过一个 ID 调用它们\n\n本活动将对您在本章中学到的所有内容进行测试，扩展我们在查看容器时所做的练习。您还将依靠以前学习的技能，例如循环、分支和读取用户输入。我们开始吧。\n\n注意\n\n在这个应用中，我们将处理一个异常。到目前为止，这还不是我们所涉及的内容，因此，如果您觉得它与您无关，请不要担心。这个活动的完整代码可以在这里找到:[https://packt.live/2KHdXRx](https://packt.live/2KHdXRx)。\n\n以下是完成活动的步骤:\n\n1.  首先包含应用需要的各种头文件。\n2.  Next, define the class that will represent a record in the system. This is going to be a person, containing both a name and an age. Also, declare a vector of this type to store these records. A vector is used for the flexibility it gives in not having to declare an array size upfront.\n\n    注意\n\n    您可以参考*练习 19，类/结构，*了解如何定义结构的提示，以及*练习 16，在向量上循环，*了解向量初始化。\n\n3.  现在，您可以开始添加一些函数来添加和获取记录；首先，补充。记录由名字和年龄组成，所以编写一个函数，接受这两个作为参数，创建一个记录对象，并将其添加到我们的记录向量中。命名该功能`Add Record`。\n4.  添加一个函数来获取记录。该函数应该接受一个参数(用户标识)并返回该用户的记录。命名该功能`Fetch Record`。\n5.  进入`main`功能，启动应用主体。从一个外部`main`循环开始，就像您在上一章中使用的那样，并向用户输出一些选项。你会给他们三个选择:`Add Record`、`Fetch Record`和`Quit`。\n6.  将这些选项呈现给用户，然后捕获他们的输入。\n7.  现在有三种可能的分支，这取决于用户输入，我们将用`switch`语句来处理。案例 1 是添加一条记录，为此，您将从用户那里获得用户的姓名和年龄，然后调用我们的`AddRecord`功能。\n8.  The next case is the user wanting to fetch a record. For this, you need to get a user ID from the user and then make a call to `FetchRecord`, outputting its result. This is where you'll be catching an exception, something we've not covered before, so the following code is provided:\n\n    ```cpp\n    try\n    {\n        person = FetchRecord(userID);\n    }\n    catch (const std::out_of_range& oor)\n    {\n        std::cout << \"\\nError: Invalid UserID.\\n\\n\";\n        break;\n    }\n    ```\n\n    注意\n\n    前面代码片段中的函数和变量的名称可能会有所不同，这取决于您对它们的命名。\n\n    调用这段代码后，您只需要输出记录细节。同样，如果您不熟悉这个语法，也不要担心，因为它将在后面的章节中介绍。\n\n9.  下一种情况是用户想要退出应用。这个相当简单；你只需要退出我们的`main`循环。\n10.  最后，添加一个默认案例。这将处理用户输入的无效选项。这里您要做的就是输出一条错误消息，并将它们发送回应用的开始。\n11.  With all of this in place, the application should be ready to go.\n\n    注意\n\n    这个活动的解决方案可以在第 520 页找到。\n\n# 总结\n\n在本章中，我们重点介绍了 C++ 提供的各种数据类型，以及如何创建自己的更复杂的对象来表示数据和封装功能。从 C++ 提供的内置数据类型开始，我们更仔细地观察了它们，研究了它们的内存占用和不同的关键字修饰符，以便扩展和更改它们的行为和属性。\n\n然后我们继续看数组和向量。这些派生类型允许我们将不同元素的集合存储在一个变量名下，但仍然使用索引对它们进行单独寻址。我们研究了固定数组(一个需要在编译时知道其大小的集合)和更灵活的向量，它可以动态地增长/收缩以满足我们的需求。正是后一个容器，我们在最终活动中使用它来创建我们的用户记录应用。\n\n接下来，我们对类和结构进行了一次简短的参观。这些将是后面章节的重点，所以我们只讨论了基础知识，看看类和结构之间的区别，我们如何声明它们，以及构造函数和析构函数如何操作。最后，我们查看了存储生命周期和范围，以更好地了解我们的对象存在多长时间，以及何时/何地可以访问它们。\n\n在第四章*的第一部分，操作员*中，我们将会看到操作员。到目前为止，我们已经在整个工作中使用了其中的一些，但我们将花一些时间来接近它们，并更深入地了解它们。操作符是我们操作对象和数据的方式，因此对它们的操作有深刻的理解是至关重要的。"
  },
  {
    "path": "docs/cpp-workshop/04.md",
    "content": "# 四、运算符\n\n概观\n\n本章介绍了 C++ 提供的各种运算符，描述了它们的功能，以及它们如何允许我们操作数据。到本章结束时，您将能够描述和使用各种算术、关系和赋值运算符。然后我们将研究一元运算符和逻辑运算符，最后是重载运算符，用于自定义类型。这一章将以一个流行的编程练习结束，这个练习将实现这一章中涉及的操作符。\n\n# 简介\n\n在最后一章中，我们学习了 C++ 提供的各种数据类型，以及如何使用它们来存储和表示系统中的数据。在这一章中，我们将看看操作符，我们分配和操作这些数据的机制。到目前为止，我们在整个工作中一直在使用它们——至少在某种程度上，很难编写 C++ 并且不使用它们——但是我们还没有正面解决它们。这就是我们现在要做的。\n\n运算符有多种形状和大小，但总的来说，它们的作用是允许我们与数据进行交互。无论是赋值、修改还是复制，这些都是通过操作符完成的。我们将从算术和关系运算符开始。这使我们能够进行数学运算，如加、减、除数字，并相互比较数值。\n\n然后我们将继续研究赋值运算符。这些允许我们将数据分配给变量，并将变量相互分配。到目前为止，这是我们使用最多的运算符，但关于这一点以及将赋值运算符和算术运算符结合在一起的多种变体，肯定还有更多要了解的。\n\n我们将看到的最后一种运算符类型是逻辑运算符和一元运算符。逻辑运算符允许我们检查条件，从而产生一个布尔值供我们检查。一元运算符是对单个值进行操作的运算符，以某种方式对其进行更改。\n\n我们将通过查看重载和分配我们自己的运算符来结束这一章。虽然我们有各种各样的运算符可用，但有时我们可能需要重载它们，为特定类型提供我们自己的行为。C++ 允许我们这样做。它还允许我们为自己的用户定义类型定义运算符。\n\n在本章的最后，我们将在最后一个活动中测试我们对操作者的理解，在这个活动中，我们创建了 **Fizz Buzz** 应用，这是一个用于测试 C++ 熟练程度的常见活动。完成后，我们将对现有的操作员有全面的了解，使我们能够自信而胜任地与系统中的数据进行交互。\n\n# 算术运算符\n\n算术运算符是那些允许我们对数据进行数学运算的运算符。除了模数运算符之外，这些都是非常简单明了的，因为它们具有我们日常数学中使用的相同符号。例如，为了添加一个数字，您只需像在任何地方一样使用“+”号。一般来说，这些运算符将用于数字数据类型，但是，没有什么能阻止类型实现这个运算符。这将作为本章的最后一个主题。\n\n让我们快速了解一下我们的四个基本运算符:加法、减法、乘法和除法。如前所述，这四个运算符具有您日常使用的相同符号，因此应该很熟悉。以下示例实现了所有四种类型的算术运算符:\n\n```cpp\n// Arithmetic operators. \n#include <iostream>\n#include <string>\nint main() \n{\n    int addition = 3 + 4;\n    int subtraction = 5 - 2;\n    int division = 8 / 4;\n    int multiplication = 3 * 4;\n    std::cout << addition << \"\\n\";\n    std::cout << subtraction << \"\\n\";\n    std::cout << division << \"\\n\";\n    std::cout << multiplication << \"\\n\";\n}\n```\n\n如果运行前面的代码，应该会获得以下输出:\n\n![Figure 4.1: Observing our simple arithmetic operators ](img/C14195_04_01.jpg)\n\n图 4.1:观察我们简单的算术运算符\n\n在这些操作中，我们可以同时使用变量和常数(也就是简单的数字)，它们是可以互换的。这里有一个例子:\n\n```cpp\n    int myInt = 3;\n    int addition = myInt + 4;\n```\n\n在这段代码中，我们将常量`4`的值添加到变量`myInt`中。其结果是`addition`变量现在的值为 7。\n\n我们要看的最后一个算术运算符是模数运算符。该运算符返回整数除法的余数，用`%`符号表示:\n\n```cpp\n// Arithmetic operators.\n#include <iostream>\n#include <string>\nint main()\n{\n    int modulus = 11 % 2;\n    std::cout << modulus << \"\\n\";\n}\n```\n\n运行上述代码后，您将获得以下输出:\n\n![Figure 4.2: The modulo operator ](img/C14195_04_02.jpg)\n\n图 4.2:模运算符\n\n在这个例子中，我们执行`11 % 2`。这里，2 将 11 除以 5，剩下 1 的余数。这是模数运算符找到的值。这个操作符在很多情况下都很有用，比如确定一个数是偶数还是奇数，以设定的增量做某事，或者在随机数生成过程中，就像我们在*第三章【控制流】*中看到的那样。让我们看一些这样的例子:\n\n```cpp\n    // Determine if a number is even.\n    bool isEven = myInt % 2 == 0;\n    // Print multiples of 5.\n    for (int i = 0; i < 100; ++ i)\n    {\n        if (i % 5 == 0)\n        {\n            std::cout << i << \"\\n\";\n        }\n    }\n    // Generate a random number between 1 and 10.\n    srand(time(0));\n    int random = (rand() % 10) + 1;\n```\n\n注意\n\n在前面的代码中，使用了`=`和`==`运算符。稍后将更详细地介绍这些内容；但是，`=`是赋值运算符，`==`是等式运算符。前者给事物赋值，后者检查事物是否相等。稍后会有更多关于这个的内容。\n\n在这个片段中，我们首先通过检查除以 2 后是否有余数来确定一个数是否为偶数。如果没有余数，那么这个数干净利落地除以 2——因此，它是偶数。\n\n接下来，我们将数字从 0 循环到 99，只打印 5 的倍数。这使用了与第一个例子类似的方法，但是在这里，我们只除以 5。如果我们这样做，没有余数，那么它确实是 5 的倍数。\n\n在代码片段的最后两行，我们使用模数运算符生成一个范围内的随机数。`rand() % 10`运算会得到一个 0 到 9 之间的答案，然后我们加 1 将这个范围从 1 增加到 10。\n\n重要的是要注意这里的运算符优先级，以及求值的顺序。值得庆幸的是，我们在数学中所学的关于求和运算顺序的基本规则在 C++ 中得到了维护；也就是说，加法将优先于减法。C++ 包含许多运算符，因此可以在[https://packt.live/2QO1j7t](https://packt.live/2QO1j7t)找到运算符及其优先级的完整列表。意识到什么优先于什么会非常有帮助。\n\n但是，如果我们想手动指定求和中的运算顺序，我们可以使用括号。以下面两个总和为例:\n\n```cpp\n    //Example 1\n    int a = 3 * 4 - 2;    // a = 10\n    //Example 2\n    int b = 3 * (4 - 2);  // b = 6\n```\n\n在第一个总和中，我们把操作的顺序留给它们自然的样子。这意味着先做乘法，再做减法，给我们的答案是 10。在第二个例子中，我们把减法用括号括起来，所以它首先被计算出来。4 减去 2 得到 2，然后乘以 3 得到我们的解:6。圆括号的正确使用是非常重要的，它让我们能够确保我们的表达式以我们期望的方式被评估。\n\n现在让我们编写一个应用，实现这里介绍的一些运算符来确定一个数是否是质数。\n\n## 练习 21:质数检查器\n\n在本章的第一个练习中，我们将编写一个应用来确定一个数是否是质数。这将利用模数运算符；其他的操作符都很简单，所以我们不需要在这里再讨论什么。质数是一个整数，大于 1，只能被 1 和自身整除；我们可以使用模数运算符来帮助我们确定这一点。看看下面的步骤:\n\n注意\n\n这个练习的完整代码可以在这里找到:[https://packt.live/2QDdILi](https://packt.live/2QDdILi)。\n\n1.  首先，我们将要求用户输入他们想要检查的数字是否是质数:\n\n    ```cpp\n    // Prime number checker.\n    #include <iostream>\n    #include <string>\n    int main()\n    {\n        int numberToCheck = 0;\n        std::cout << \"Prime number checker\\n\";\n        std::cout << \"Enter the number you want to check: \";\n        std::cin >> numberToCheck;\n    ```\n\n2.  我们现在可以开始确定这个数是否是质数的过程了。我们对质数的部分定义是，它必须大于 1，所以我们可以直接忽略低于或等于这个值的任何值:\n\n    ```cpp\n        if (numberToCheck <= 1)\n        {\n            std::cout << numberToCheck << \" is not prime.\";\n            return 0;\n        }\n    ```\n\n3.  2 是一个有趣的质数，因为它是唯一的偶数。所有大于这个数的偶数至少可以被 1、2 和它们自己的值整除。考虑到这一点，我们现在可以添加一个快速检查来处理这种情况:\n\n    ```cpp\n        else if (numberToCheck == 2)\n        {\n            std::cout << numberToCheck << \" is prime.\";\n            return 0;   \n        }\n    ```\n\n4.  Now we can get to the main section of the prime check. We've handled the \"special\" cases, where the number entered is 0, 1, or 2, so now we need to handle values greater than 2\\. To do this, we can determine whether any numbers greater than 1 and less than the value the user inputs, will divide exactly into the number we're checking.\n\n    注意\n\n    对此有更多可能的优化，例如只检查偶数值。然而，为了不太偏离模数运算，我们将省略它们。\n\n    我们之前使用了模数运算符，并看到它如何在除法之后获取余数；如果我们将它与用户的输入和一个循环值一起使用，我们就可以确定我们的输入值是否有除 1 或自身之外的任何因素。1 的因子和数本身是不会被查的，所以我们知道如果我们找到任何其他的因子，那么数就不可能是质数。如果我们找不到，那就是:\n\n    ```cpp\n        for (int i = 2; i < numberToCheck; ++ i)\n        {\n            if (numberToCheck % i == 0)\n            {\n                std::cout << numberToCheck << \" is not prime.\";\n                return 0;\n            }\n        }\n        std::cout << numberToCheck << \" is prime.\";\n    }\n    ```\n\n5.  完整的程序如下:\n\n    ```cpp\n    // Exercise 21: Prime number checker.\n    #include <iostream>\n    #include <string>\n    int main() \n    {\n        int numberToCheck = 0;\n        std::cout << \"Prime number checker\\n\";\n        std::cout << \"Enter the number you want to check: \";\n        std::cin >> numberToCheck;\n        if (numberToCheck <= 1) \n        {\n            std::cout << numberToCheck << \" is not prime.\";\n            return 0;\n        } \n        else if (numberToCheck == 2) \n        {\n            std::cout << numberToCheck << \" is prime.\";\n            return 0;\n        }\n        for (int i = 2; i < numberToCheck; ++ i) \n        {\n            if (numberToCheck % i == 0) \n            {\n                std::cout << numberToCheck << \" is not prime.\";\n                return 0;\n            }\n        }\n        std::cout << numberToCheck << \" is prime.\";\n    }\n    ```\n\n6.  我们现在可以运行这个应用并测试它的功能。前五个质数是 2、3、5、7 和 11。我们可以检查这些以及它们周围的数字，以确定我们的应用是否正常工作:\n\n![Figure 4.3: Determining whether a number is a prime ](img/C14195_04_03.jpg)\n\n图 4.3:确定一个数是否是质数\n\n通过使用模数运算符，我们能够确定一个数是否是素数。这只是模数运算符以及一般算术运算符的众多用途之一。\n\n# 关系运算符\n\n关系运算符允许我们相互比较值。例如，我们可以检查一个值是否大于另一个值，或者两个值是否相等。这些运算符不仅处理整数值，还处理集合和对象。经常检查的两个基本关系是:**相等**和**比较**。\n\n## 平等\n\n用于确定两个值相等的关系运算符是`==`和`!=`；也就是分别相等和不相等。一个值放在运算符的两边，左边是 LHS，右边是 RHS，比较的是这两个值。返回一个布尔值，表示相等检查是否为真。\n\n这两个运算符可以如下使用:\n\n```cpp\n// Relational operators. Equality. \n#include <iostream>\n#include <string>\nint main()\n{\n    int myInt1 = 1;\n    int myInt2 = 1;\n    int myInt3 = 5;\n    if (myInt1 == myInt2)\n    {\n        std::cout << myInt1 << \" is equal to \" << myInt2 << \".\\n\";\n    }\n    if (myInt1 != myInt3) \n    {\n        std::cout << myInt1 << \" is not equal to \" << myInt3;\n    }\n}\n```\n\n在这个小程序中，我们已经声明了一些整数，并使用两个关系等式运算符确定了哪些是相等的。运行上述代码后，将获得以下输出:\n\n![Figure 4.4: We can test the equality of two values or objects by using relational operators ](img/C14195_04_04.jpg)\n\n图 4.4:我们可以通过使用关系运算符来测试两个值或对象的相等性\n\n我们的两个相等检查都返回 true，因此我们执行了两个 print 语句。请注意，仅仅因为它们都返回 true，并不意味着它们都是相等的。在第一个例子中，我们检查它们是否相等，在第二个例子中，我们检查它们是否不相等。\n\n除了使用简单的整数值，我们还可以使用它来测试浮点类型、对象和列表的相等性，假设这些运算符已经定义。正是在这个运算符定义中，概述了确定两个对象是否相等的规则；我们将在这一章的最后详细介绍这一点以及重载操作符。\n\n比较浮点类型的相等性时，重要的是要知道`==`可能会产生错误的结果。所有浮点运算都有出错的可能，因为浮点数无法用二进制精确表示；相反，它们被存储为非常接近的近似值。这可能会导致错误。为了抵消这一点，通常检查两个浮点数之间的差值是否低于某个非常小的值，例如ε。如果差值低于这个小值，我们通常会认为两者“足够接近”。当然，这取决于你的需求，但一般来说，这就足够了。我们不会更详细地讨论浮点错误，因为这是一个很大的话题；但是，在使用浮点比较时，请记住这一点。\n\n注意\n\n关于浮点比较的进一步阅读，可以参考[https://packt.live/2s4njk2](https://packt.live/2s4njk2)。\n\n## 对比\n\n关系运算符的另一个子集是比较运算符。这些允许我们比较变量的值。我们有四种选择:大于(`>`)、小于(`<`)、大于或等于(`>=`)以及小于或等于(`<=`)。它们的使用方式与等式运算符相同；也就是说，它们既有左侧值，也有右侧值，如果比较结果为`true`，它们将返回真；如果比较结果为假，它们将返回`false`。\n\n如何使用这些运算符的示例如下:\n\n```cpp\n// Relational operators. Equality. \n#include <iostream>\n#include <string>\nint main() \n{\n    int myInt1 = 1;\n    int myInt2 = 1;\n    int myInt3 = 5;\n    if (myInt1 > myInt2) \n    {\n        std::cout << myInt1 << \" is greater than\" << myInt2 << \".\\n\";\n    }\n    if (myInt1 < myInt3) \n    {\n        std::cout << myInt1 << \" is less than \" << myInt3 << \".\\n\";\n    }\n    if (myInt3 >= myInt2) \n    {\n        std::cout << myInt3 << \" is greater than or equal to \" << myInt2                   << \".\\n\";\n    }\n    if (myInt2 <= myInt1) \n    {\n        std::cout << myInt2 << \" is less than or equal to \" << myInt1;\n    }\n}\n```\n\n类似于我们检查相等性的方式，这里，我们将两个值进行比较。前两个相当简单——我们只是检查一个数字是大于另一个，还是小于另一个。最后两个语句使用“或等于”运算符。在这些情况下，如果值也相等，则大于或小于检查将返回 true。它是我们前面看到的等式(`==`)运算符和前两个比较运算符的混合。\n\n如果我们在编译器中运行这段代码，我们可以看到执行了哪些语句:\n\n![Figure 4.5: Using relational comparison operators to determine the relationship between values ](img/C14195_04_05.jpg)\n\n图 4.5:使用关系比较运算符来确定值之间的关系\n\n我们可以看到，除了一个比较外，所有比较都被评估为真，因此我们执行了三个打印语句。\n\n## 练习 22:时间计算器\n\n在本练习中，我们将编写一个小应用，根据小时来确定一天中的时间。我们将让用户以军事时间格式(例如，1800)输入时间，并将向他们呈现一个代表一天中适当时间的字符串。以下是完成练习的步骤:\n\n注意\n\n这个练习的完整代码可以在这里找到:[https://packt.live/2rg9ONu](https://packt.live/2rg9ONu)。\n\n1.  我们将从向用户输出指令开始，然后将他们的答案读入一个整数:\n\n    ```cpp\n    #include <iostream>\n    #include <string>\n    int main() \n    {\n        std::cout << \"***Time of Day Calculator***\\n\";\n        std::cout << \"Enter time in military format.               eg. (1800, 1430)\\n\\n\";\n        std::cout << \"Enter time: \";\n\n        std::string input;\n        getline(std::cin, input);\n\n        int time = std::stoi(input);\n    ```\n\n2.  现在我们可以开始评估我们的时代了。我们从确保我们的价值在有效范围内开始。如果`time`小于`0000`或大于`2400`，那么我们会打印一条消息给用户，通知他们他们的时间无效:\n\n    ```cpp\n        if (time < 0000 || time > 2400)\n        {\n            std::cout << \"Invalid time.\";\n            return 0;\n        }\n    ```\n\n3.  在我们开始定义时间范围之前，我们可以检查一天中的特定时间，从午夜开始。当时间等于`0000`时会是这种情况，如果是，我们会打印消息`\"It's currently midnight.\"` :\n\n    ```cpp\n        if (time == 0000)\n        {\n            std::cout << \"It's currently midnight.\";\n        }\n    ```\n\n4.  接下来，我们检查时间是否是中午。当时间等于`1200`时会是这种情况，如果是，我们会打印消息`\"It's currently noon.\"` :\n\n    ```cpp\n        else if (time == 1200)\n        {\n            std::cout << \"It's currently noon.\";\n        }\n    ```\n\n5.  现在我们将开始定义一些时间范围。我们将从早上开始，我们将它归类为早上 6 点到中午之间的时间。如果是这样的话，我们会打印消息`\"It's currently morning.\"` :\n\n    ```cpp\n        else if (time >= 0600 && time < 1200)\n        {\n            std::cout << \"It's currently morning.\";\n        }\n    ```\n\n6.  我们的下一个时间范围是下午。这将适用于下午 12:01 到 5 点之间的时间。在这种情况下，我们将打印消息`\"It's currently afternoon.\"` :\n\n    ```cpp\n        else if (time > 1200 && time <= 1700)\n        {\n            std::cout << \"It's currently afternoon.\";\n        }\n    ```\n\n7.  接下来是晚上，我们将这个范围定义为下午 5 点以后但晚上 8 点以前的任何时间，当这种情况出现时，我们将打印消息`\"It's currently evening.\"` :\n\n    ```cpp\n        else if (time > 1700 && time <= 2000)\n        {\n            std::cout << \"It's currently evening.\";\n        }\n    ```\n\n8.  我们的最终时间范围是晚上，我们将它定义为晚上 8 点之后但早上 6 点之前的任何时间。当这种情况发生时，我们将打印消息`\"It's currently night.\"` :\n\n    ```cpp\n        else if (time > 2000 || time < 0600)\n        {\n            std::cout << \"It's currently night.\";\n        }\n    }\n    ```\n\n9.  完整的程序如下:\n\n    ```cpp\n    // Time of Day Calculator.\n    #include <iostream>\n    #include <string>\n    int main() \n    {\n        std::cout << \"***Time of Day Calculator***\\n\";\n        std::cout << \"Enter time in military format.               eg. (1800, 1430)\\n\\n\";\n        std::cout << \"Enter time: \";\n        std::string input;\n        getline(std::cin, input);\n        int time = std::stoi(input);\n        if (time < 0000 || time > 2400) \n        {\n            std::cout << \"Invalid time.\";\n            return 0;\n        }\n        if (time == 0000) \n        {\n            std::cout << \"It's currently midnight.\";\n        }\n        else if (time == 1200) \n        {\n            std::cout << \"It's currently noon.\";\n        }\n        else if (time >= 0600 && time < 1200) \n        {\n            std::cout << \"It's currently morning.\";\n        } \n        else if (time > 1200 && time <= 1700) \n        {\n            std::cout << \"It's currently afternoon.\";\n        } \n        else if (time > 1700 && time <= 2000) \n        {\n            std::cout << \"It's currently evening.\";\n        } \n        else if (time > 2000 || time < 0600) \n        {\n            std::cout << \"It's currently night.\";\n        }\n    }\n    ```\n\n10.  如果我们现在运行这个应用，我们的用户应该能够输入时间，并向他们显示一天中的时间:\n\n![Figure 4.6: Using relational operators, we can determine what time of day it is ](img/C14195_04_06.jpg)\n\n图 4.6:使用关系运算符，我们可以确定现在是一天中的什么时间\n\n在本练习中，我们使用了一些关系运算符来确定一天中的当前时间。没有输入验证，因此用户输入必须与我们期望的匹配，否则我们将得到未定义的行为，但是我们可以研究如何使用关系运算符来比较和分类输入的时间。\n\n# 一元运算符\n\n到目前为止，我们使用的运算符都有一个值，通常称为操作数，位于它们的两边:rhs 和 lhs。然而，一元运算符是那些只取一个值并对其进行修改的运算符。我们将快速查看负(`-`)、增量(`++ `)和减量(`--`)。还有许多其他一元运算符(逻辑补码(`!`)和按位补码(`~`))，但我们将在下面的章节中介绍这些运算符。\n\n让我们从负(`-`)运算符开始；这允许我们操纵一个值的符号。它相当简单——当放在一个值前面时，它会将一个负值变成正值，将一个正值变成负值。\n\n这里有一个例子:\n\n```cpp\n// Negation example.\n#include <iostream>\n#include <string>\nint main()\n{\n    int myInt = -1;\n    std::cout << -myInt * 5 << std::endl;\n    myInt = 1;\n    std::cout << -myInt * 5 << std::endl;\n}\n```\n\n如果我们在代码编辑器中运行这个应用，我们可以看到这些运算符对我们的价值的影响:\n\n![Figure 4.7: Using the minus operator to change sign ](img/C14195_04_07.jpg)\n\n图 4.7:使用减号运算符更改符号\n\n我们可以从这个输出中看到，我们输出的符号与变量的符号相反，因为我们将它与减运算符一起使用。\n\n我们要看的其他一元运算符是递增(`++ `)和递减(`--`)。这两个运算符允许我们分别将一个值增加或减少一。我们已经在`for`循环中使用了增量(`++ `)运算符来增加循环计数器。减量(`--`)的工作方式相同，但方向相反。\n\n在下面的代码中，我们定义一个值，然后递增或递减它，并查看它的值:\n\n```cpp\n// Increment/Decrement example.\n#include <iostream>\n#include <string>\nint main()\n{\n    int myInt = 1;\n    std::cout << ++ myInt << std::endl;\n    std::cout << --myInt << std::endl;\n}\n```\n\n在这个简单的片段中，我们将一个值定义为 1，递增它，然后立即再次递减它，在每个阶段打印它的值。在代码编辑器中运行后，您将获得以下输出:\n\n![Figure 4.8: Using increment or decrement to modify a value ](img/C14195_04_08.jpg)\n\n图 4.8:使用增量或减量来修改值\n\n我们可以看到，在增加我们的值后，它增加了 1，在减少值后，它恢复正常。我们需要注意一些有趣的事情。与减运算符不同，增运算符和减运算符实际上改变了与之一起使用的变量的值。在递增之后，我们的变量没有返回到它的初始值，就像我们看到的减运算符一样；也就是说，一旦增加，增加的值就成为新值。\n\n同样重要的是要注意，一个值可以是前增量，也可以是后增量。也就是说，递增或递减运算符可以放在变量的前面或后面，这将改变值的返回方式。让我们进行一个小练习，突出这个细微的差别。\n\n## 练习 23:增量前/增量后示例\n\n我们刚刚看到，可以对一个值进行前增量或后增量，它们各自在操作方式上有细微但明显的区别。让我们通过编写一个同时做这两者的应用来看一个例子。你能猜出输出吗？看看下面的步骤:\n\n注意\n\n这个练习的完整代码可以在这里找到:[https://packt.live/2QADmQC](https://packt.live/2QADmQC)。\n\n1.  首先声明我们的函数标题和`#include`语句:\n\n    ```cpp\n    // Pre/Post Increment Example.\n    #include <iostream>\n    #include <string>\n    ```\n\n2.  接下来，我们将定义我们的`main`函数并定义一个`int`，给它一个默认值`5`。然后，我们将在打印语句中预先增加该值，然后单独打印该值:\n\n    ```cpp\n    int main()\n    {\n        int myInt = 5;\n        std::cout << ++ myInt << std::endl;\n        std::cout << myInt << std::endl;\n    ```\n\n3.  现在，我们将整数重置为 5，然后在一个 print 语句中再次递增。然而，这一次，我们将后递增该值:\n\n    ```cpp\n        myInt = 5;\n        std::cout << myInt++ << std::endl;\n        std::cout << myInt << std::endl;\n    }\n    ```\n\n4.  完整的程序如下:\n\n    ```cpp\n    // Pre/Post Increment Example.\n    #include <iostream>\n    #include <string>\n    int main() \n    {\n        int myInt = 5;\n        std::cout << ++ myInt << std::endl;\n        std::cout << myInt << std::endl;\n        myInt = 5;\n        std::cout << myInt++ << std::endl;\n        std::cout << myInt << std::endl;\n    }\n    ```\n\n5.  让我们运行这段代码，检查不同类型的增量是如何与`std::cout`语句交互的。你认为每条线的产量会是多少？运行应用前，请做好记录:\n\n![Figure 4.9: Pre-increment versus post-increment gives us different results ](img/C14195_04_09.jpg)\n\n图 4.9:前增量和后增量给了我们不同的结果\n\n在第一种情况下，我们两次都输出`6`。这意味着增量发生在打印值之前。然而，在第二种情况下，我们可以看到我们打印了数字`5`和`6`。这意味着首先打印该值，然后进行增量。记住操作的顺序很重要，因为从这个例子中很容易看出我们是如何引入一个难以追踪的微妙错误的。然而，如果你增加一个值而忽略表达式结果，比如增加一个`for`循环，那么两者都可以。\n\n# 赋值运算符\n\n赋值操作符允许我们给对象赋值。到目前为止，在我们的章节中，我们已经多次使用了这个运算符——这是编程中最基本的操作之一，但和往常一样，我们可以了解到更多关于这些运算符的信息。\n\n最基本的赋值运算符是我们取一个值并将其赋给一个对象，如下所示:\n\n```cpp\n    int myInt = 5;\n```\n\n我们对此很熟悉，但我们可能不熟悉的是将这些与算术运算符相结合的概念。让我们想象一个场景，我们需要将一个值增加 5。我们可以这样做:\n\n```cpp\n    myInt = myInt + 5;\n```\n\n我们取`myInt`的值，加上`5`，再赋回原变量。然而，我们可以通过将两个操作符结合在一起，以更精细的方式来实现这一点。赋值运算符之前可以有一个算术运算符来实现这一点，如下所示:\n\n```cpp\n    myInt += 5;\n```\n\n这是任何算术运算符的情况；它们可以在赋值运算符之前，并且它们的效果是组合的。这可以在下面的示例应用中看到:\n\n```cpp\n// Assignment Operators Example. \n#include <iostream>\n#include <string>\nint main()\n{\n    int myInt = 5;\n    myInt += 5;\n    std::cout << myInt << std::endl;\n    myInt -= 5;\n    std::cout << myInt << std::endl;\n    myInt *= 5;\n    std::cout << myInt << std::endl;\n    myInt /= 5;\n    std::cout << myInt << std::endl;\n    myInt %= 5;\n    std::cout << myInt << std::endl;\n}\n```\n\n如果我们在编辑器中运行这段代码，我们可以看到赋值语句如何改变`myInt`的值:\n\n![Figure 4.10: Combining the simple assignment operator with arithmetic operators ](img/C14195_04_10.jpg)\n\n图 4.10:简单赋值运算符和算术运算符的组合\n\n通过将简单的赋值运算符与算术运算符结合起来，我们能够在一条语句中执行数学运算和赋值。这适用于我们稍后将介绍的各种按位运算符。\n\n# 逻辑运算符\n\n逻辑运算符允许我们在一条语句中一起计算多个布尔值。我们之前已经看到，当我们评估一个条件时，比如在`if`语句中，我们会得到一个布尔值。因此，我们可以使用逻辑运算符同时组合和计算两个或多个条件。\n\n我们有三个这样的运营商可供选择:\n\n*   **和(& & )** :当两个条件都为真时，返回真，否则返回假。\n*   **或(||)** :当任一条件为真时返回真，否则返回假。\n*   **不是(！)**:条件为假则返回真，否则返回真；本质上，它返回与条件相反的结果。\n\n让我们用一个例子来看看这些操作符是如何工作的。\n\n## 练习 24:逻辑运算符示例\n\n为了演示这些逻辑运算符是如何工作的，让我们创建一个快速的示例应用。我们将从用户那里获取一些输入，可能是一些名字，并使用我们的操作符对它们进行检查:\n\n注意\n\n这个练习的完整代码可以在这里找到:[https://packt.live/2KGX0a2](https://packt.live/2KGX0a2)。\n\n1.  首先，让我们添加一个程序标题，并添加我们的`#include`语句:\n\n    ```cpp\n    // Logical Operators Exercise.\n    #include <iostream>\n    #include <string>\n    ```\n\n2.  现在我们可以定义我们的`main`函数了。首先，我们需要定义三个字符串变量，并从用户那里获取三个名称:\n\n    ```cpp\n    int main()\n    {\n        std::string name1;\n        std::string name2;\n        std::string name3;\n        std::cout << \"Please enter name 1: \";\n        std::cin >> name1;\n        std::cout << \"Please enter name 2: \";\n        std::cin >> name2;\n        std::cout << \"Please enter name 3: \";\n        std::cin >> name3;\n    ```\n\n3.  现在我们可以做第一次检查了。我们将首先检查一下我们的名字是否都一样。为此，我们将对照`name2`检查`name1`，对照`name3`检查`name2`。然后我们将使用`&&`运算符来确保这两个都是真的。如果是，我们知道所有匹配的名字，所以我们可以输出一条消息:\n\n    ```cpp\n        // Check if all or any of the names match.\n        if (name1 == name2 && name2 == name3)\n        {\n            std::cout << \"\\nAll the names are the same.\";\n        }\n    ```\n\n4.  如果失败，我们将检查是否有匹配的名字。我们将对照其他条件检查每个`name`，如果以下任一条件为真，则使用`||`运算符返回真:\n\n    ```cpp\n        else if (name1 == name2 || name2 == name3 || name1 == name3)\n        {\n            std::cout << \"\\nSome of the names matched.\";    \n        }\n    ```\n\n5.  Finally, we'll use the `!` operator check whether `name1` and `name2` match. We're also going to use a ternary statement for this. First, we'll add the code, and then look at what it's doing:\n\n    ```cpp\n        // Check if names 1 and 2 are different.\n        std::cout << \"\\nNames 1 and 2 are \"               << (!(name1 == name2) ? \"different.\" : \"the same.\")               << std::endl;\n        }\n    ```\n\n    在这个三元语句中，我们检查`name1`和`name2`是否匹配，然后用`!`运算符否定结果。这意味着如果两个名称不同，三元语句条件将为真。然后我们用它来返回正确的字符串。\n\n    请注意，我们在这里使用了括号，这可以归结为我们前面谈到的优先顺序。例如，我们希望在尝试应用`!`运算符之前对`name1`和`name2`进行评估。同样，我们希望在将整个三元语句与`<<`运算符一起使用之前对其进行求值；否则，我们会得到一个错误。这是一个很好的例子，说明我们如何使用括号来控制优先顺序。\n\n6.  完整的程序如下:\n\n    ```cpp\n    // Logical Operators Exercise.\n    #include <iostream>\n    #include <string>\n    int main() \n    {\n        std::string name1;\n        std::string name2;\n        std::string name3;\n        std::cout << \"Please enter name 1: \";\n        std::cin >> name1;\n        std::cout << \"Please enter name 2: \";\n        std::cin >> name2;\n        std::cout << \"Please enter name 3: \";\n        std::cin >> name3;\n        // Check if all or any of the names match.\n        if (name1 == name2 && name2 == name3) \n        {\n            std::cout << \"\\nAll the names are the same.\";\n        } \n        else if (name1 == name2 || name2 == name3 || name1 == name3) \n        {\n            std::cout << \"\\nSome of the names matched.\";\n        }\n        // Check if names 1 and 2 are different.\n        std::cout << \"\\nNames 1 and 2 are \"               << (!(name1 == name2) ? \"different.\" : \"the same.\")               << std::endl;\n    }\n    ```\n\n7.  运行应用，并用几个不同的名称测试它:\n\n![Figure 4.11: Using logical operators to test conditions ](img/C14195_04_11.jpg)\n\n图 4.11:使用逻辑运算符测试条件\n\n在本练习中，我们使用了许多具有不同条件的逻辑运算符。通过这样做，我们能够作为一个集体来评估多个条件，例如只有当所有的值都为真时，我们才能做一些事情。我们还能够通过翻转条件的逻辑值来操纵条件(使用`!`运算符返回相反的值)。这非常有用，就如何使用它们而言，这只是冰山一角。\n\n# 操作员超载\n\n到目前为止，我们看到的所有运算符都是由 C++ 定义的。然而，这并不是说我们不能像处理函数一样在自己的类中重载它们。运算符重载非常强大，允许我们用自己的类型为 C++ 中的大多数运算符定义自己的行为。重载运算符的语法如下:\n\n```cpp\nreturnType operator symbol (arguments)\n```\n\n让我们用一个简单的测试类来看一下这个例子:\n\n```cpp\n// Operator Overloading Example \n#include <iostream>\n#include <string>\nclass MyClass \n{\n    public:\n    void operator + (MyClass const & other) \n    {\n        std::cout << \"Overloaded Operator Called\" << std::endl;\n        return;\n    }\n};\nint main() \n{\n    MyClass A = MyClass();\n    MyClass B = MyClass();\n    A + B;\n}\n```\n\n在这个微不足道的例子中，我们创建了一个小的`MyClass`类，并重载了`+`运算符，提供了我们自己的定义。目前，我们在那里所做的只是打印一条消息，让我们知道我们的操作员代码已经运行。然而，您可以想象我们如何在这里放置我们想要的任何东西，为我们的对象定义自定义行为。让我们运行代码并确认我们使用的是重载运算符:\n\n![Figure 4.12: Overloading an operator with our own behavior ](img/C14195_04_12.jpg)\n\n图 4.12:用我们自己的行为重载操作符\n\n在运行应用时，我们确实看到了我们打印的消息，所以我们知道我们正在运行我们的重载操作行为。通过这样做，我们可以将本章中介绍的运算符用于我们自己的类型。让我们通过重载一个自定义类型的相等操作符来看看这个更真实的应用。\n\n## 练习 25:运算符重载示例\n\n让我们覆盖一个简单的`Person`类的等式运算符，该类封装了一个名称和年龄。我们可以想象对同一个人有多个引用，并且想要检查它们是否相同，例如检查同一个人是否存在于多个列表中。等式运算符会让我们检查。看看下面的步骤:\n\n注意\n\n这个练习的完整代码可以在这里找到:[https://packt.live/2QyS4b0](https://packt.live/2QyS4b0)。\n\n1.  首先，我们将添加我们的`#includes` :\n\n    ```cpp\n    // Operator Overloading Example\n    #include <iostream>\n    #include <string>\n    ```\n\n2.  接下来，我们将宣布我们的`Person`类。这将是一个简单的类，包含一个名字和一个年龄。首先定义类名，我们需要的成员变量，以及初始化它们的构造函数:\n\n    ```cpp\n    class Person\n    {\n    public:\n        Person(int age, std::string name) : age(age), name(name)\n        {\n        };\n        float age = 0;\n        std::string name = \"\";\n    ```\n\n3.  现在我们可以让`==`操作员超载了。我们将从初始声明开始。我们想重载`==`运算符，返回一个 bool 我们将接受另一个与我们要比较的对象类型相同的对象:\n\n    ```cpp\n        bool operator== (Person const& other)\n        {\n    ```\n\n4.  现在是操作者身体的时候了；如果姓名和年龄完全匹配，则可以认为两个`Person`记录相同。我们可以检查这一点，并返回结果值。这也将完成我们的类定义，所以我们将添加我们的结束括号:\n\n    ```cpp\n          return ((age == other.age) && (name == other.name));\n       }\n    };\n    ```\n\n5.  现在，为了看到我们的新操作员在行动，我们将宣布三个`Person`记录。两个相同，第三个名字不同，年龄不同:\n\n    ```cpp\n    int main()\n    {\n        Person PersonA = Person(27, \"Lucy\");\n        Person PersonB = Person(27, \"Lucy\");\n        Person PersonC = Person(27, \"Susan\");\n    ```\n\n6.  最后，我们将使用新的运算符检查哪些类型是相同的。评价`PersonA``PersonB``PersonB``PersonC`:\n\n    ```cpp\n        std::cout << (PersonA == PersonB) << std::endl;\n        std::cout << (PersonB == PersonC) << std::endl;\n    }\n    ```\n\n    的平等性\n7.  完整的程序如下:\n\n    ```cpp\n    // Operator Overloading Example\n    #include <iostream>\n    #include <string>\n    class Person \n    {\n    public:\n        Person(int age, std::string name): age(age), name(name) {};\n        float age = 0;\n        std::string name = \"\";\n        bool operator == (Person const & other) \n        {\n            return ((age == other.age) && (name == other.name));\n        }\n    };\n    int main() \n    {\n        Person PersonA = Person(27, \"Lucy\");\n        Person PersonB = Person(27, \"Lucy\");\n        Person PersonC = Person(27, \"Susan\");\n        std::cout << (PersonA == PersonB) << std::endl;\n        std::cout << (PersonB == PersonC) << std::endl;\n    }\n    ```\n\n8.  让我们运行这段代码，看看我们得到了什么:\n\n![Figure 4.13: Person A and B were a match. Person B and C were not ](img/C14195_04_13.jpg)\n\n图 4.13:人甲和人乙匹配。乙和丙不是\n\n由于 A 和 B 的姓名和年龄匹配，我们的等式运算符返回 true，因此我们打印该值。人 B 和人 C 的名字不同，所以不匹配，我们打印 0(即假)。我们可以看到，通过为我们自己的用户类型定义这些操作符，我们为它们提供了很多功能。\n\n# 按位运算符\n\n按位操作是对单个位进行操作的操作，例如向左移动一位，为此，我们有一套称为按位运算符的专用运算符。我们在这里不打算讲太多细节——关于按位运算符的完整讨论改天再说。但是，我们将快速了解一下我们可以使用的按位运算符，以及它们的一些使用示例。这会给你一些初步的了解，这样当你以后遇到他们的时候，他们就会很熟悉了。\n\n注意\n\n请记住，位(即二进制数字)是计算机中最基本的数据单位。有两个可能的值，1 或 0，所有数据都以位的形式存储。机器上最小的可寻址数据单元是一个字节，它由 8 位组成，因此按位运算允许我们单独操作位。\n\n在下面的例子中，我们将使用位组。这是一个简单的位集合，允许我们看到按位运算符的结果。每个示例将采用以下格式:\n\n```cpp\n{lhs bitset} {operator} {rhs bitset} = {resulting bitset}\n```\n\n原则上，这与正常计算没有什么不同(如 *a + b = c* )，所以不要让任何潜在的对位的不熟悉造成混淆。有了序言，我们继续吧。\n\nC++ 为我们提供了六个按位运算符，如下所示:\n\n*   **&二进制与**:该运算符仅将两个操作数中的那些位复制到新值中。考虑以下例子:00110 & 01100 = 00100。这里，两个原始值中都只有第三位，因此这是结果中唯一设置的位。\n*   **|二进制或**:该运算符将任一操作数中的位复制到新值。考虑以下示例:00110 | 01100 = 01110。这里，在我们的第一个操作数中，设置了第二位和第三位，在第二个操作数中，设置了第三位和第四位。因此，结果是设置了第二、第三和第四位。\n*   **~二进制补码**:该运算符翻转一个值中的每个位。考虑以下示例:~00110 = 11001。这里，在我们的第一个操作数中，唯一设置的位是第二个和第三个。因此，我们的结果设置了除这些以外的所有位。\n*   **< <二进制左移位运算符**:该运算符将左操作数中的位向左移位右操作数中指定的数字。考虑以下示例:00110 < < 2 = 11000。这里，我们的左操作数设置了第二位和第三位，因此将它们向左移动两个位置后，现在设置了第四位和第五位。\n*   **>> Binary Right Shift Operator**: This operator will shift the bits in the left operand to the right by the number specified in the right operand. Consider the following example: 01100 >> 2 = 00011\\. Here, our left operand has the third and fourth bits set, so after shifting them two places to the right, the first and second bits are now set.\n\n    注意\n\n    在本文中，术语“按位”和“二进制”是可以互换的。同样正确的说法是“二进制与”或“按位与”。\n\n让我们看看这些代码示例。作为标准库的一部分，提供的是 bitset 类。这使我们能够将一个整数值表示为它的一系列位，从而更容易看到按位运算的结果。下面的代码表示前面给出的示例:\n\n```cpp\n// Bitwise Operator Examples. \n#include <iostream>\n#include <string>\n#include <bitset>\nint main() \n{\n    int myInt1 = 6; // 00110 when expressed in binary \n    int myInt2 = 12; // 01100 when expressed in binary \n    // Binary AND \n    std::cout << std::bitset < 5 > (myInt1 & myInt2) << std::endl;\n    // Binary OR \n    std::cout << std::bitset < 5 > (myInt1 | myInt2) << std::endl;\n    // Binary Ones Compliment \n    std::cout << std::bitset < 5 > (~myInt1) << std::endl;\n    // Binary Left Shift Operator \n    std::cout << std::bitset < 5 > (myInt1 << 2) << std::endl;\n    // Binary Right Shift Operator \n    std::cout << std::bitset < 5 > (myInt2 >> 2) << std::endl;\n}\n```\n\n注意\n\n`std::bitset<5>`中`5`的值表示位集中的位数。关于位集的更多信息，可以参考[https://packt.live/2QGLqzp](https://packt.live/2QGLqzp)。\n\n如果我们在编辑器中运行这段代码，我们可以看到按位运算的结果与练习的结果相匹配:\n\n![Figure 4.14: We can see the results of our bitwise operations by using the bitset class ](img/C14195_04_14.jpg)\n\n图 4.14:通过使用 bitset 类，我们可以看到按位运算的结果\n\n虽然一开始操纵单个位看起来令人生畏，但在很多情况下，它非常有用。一个这样的场合是用旗帜。也许我们想跟踪多个事物，比如说，游戏引擎中的活动层。我们有多个可以同时激活的层，因此我们可以定义一个整数，给出一系列位，并使用每个位来确定哪些层是激活的:\n\n```cpp\n    int layer1 = 1;             // 00001\n    int layer2 = 2;             // 00010\n    int layer3 = 4;             // 00100\n    int layer4 = 8;             // 01000\n    //[…]\n    int activeLayers = 9;       // 01001\n```\n\n在前面的示例片段中，我们定义了四个层，每个层都有不同的位设置为值`1`。由于每一层需要不同的位，我们可以用一个 4 位组来表示它们。例如，`layer 1`设置第一位，`layer 4`设置第四位。如果我们想表示这两个层都是活动的，我们可以将它们的两位值都设置为 1，得到数字 9(二进制的 01001，或者第一个和第四个位)。这只是它们各自值的按位“与”。这称为位屏蔽，有许多潜在的应用——管理活动层，如本例所示。\n\n这就是位运算的全部内容，因为它是一个很大的主题。希望这个简短的介绍已经解释了基础知识，这样将来当你运行位运算时就不会完全陌生了。现在让我们进入最后一个活动，在这个活动中，我们创建了一个著名的编程测试:Fizz Buzz。\n\n## 活动 4:嘶嘶嗡嗡\n\n第一部分的最后一项活动将是我们创建 Fizz Buzz 应用。这是一项常见的活动，用于测试各种语言之间的编程理解，并利用到目前为止涵盖的许多主题。\n\nFizz Buzz 测试背后的想法很简单:编写一个程序，输出数字 1 到 100。对于 3 的倍数，打印单词“Fizz”而不是数字，对于 5 的倍数，打印单词“Buzz”:\n\n![Figure 4.15: The Fizz Buzz application – a common coding test exercise ](img/C14195_04_15.jpg)\n\n图 4.15:Fizz Buzz 应用——一个常见的编码测试练习\n\n注意\n\n这个活动的完整代码可以在这里找到:[https://packt.live/2KHiSC7](https://packt.live/2KHiSC7)。\n\n以下步骤将帮助您完成本活动:\n\n1.  像往常一样，我们将从包含应用所需的头开始，并开始我们的主循环。\n2.  Fizz Buzz 应用告诉我们，对于 3 的倍数，我们将打印`Fizz`，对于 5 的倍数，我们将打印`Buzz`。然而，这两种情况可以同时发生。例如，15 是两者的倍数，所以我们接下来将定义一个布尔值(`multiple`)，这将有助于我们跟踪这一点，并给它一个初始值`false`。\n3.  接下来，我们可以检查我们当前的循环值`i`是否是 3 的倍数。如果是这样，我们将打印单词`Fizz`并将我们的倍数布尔设置为`true`。\n4.  然后我们可以对`Buzz`进行同样的操作，检查`i`是否是 5 的倍数。同样，如果是这样，我们将把我们的多重布尔设置为真。\n5.  现在，我们已经检查了我们的数字是 3 的倍数还是 5 的倍数，并且有一个布尔值，如果是`true`，我们可以用它来确定我们是否打印正常的数字。如果我们的`multiple` `bool`仍然是`false`的话，那么我们知道我们需要打印正常的数字，`i`。\n6.  最后，我们将做一点格式化。如果我们不在循环的最后一次迭代中，我们将打印一个逗号后跟一个空格。这将使我们的应用在打印时更加整洁。\n7.  Let's run the application now and see it in action. We should see numbers leading up to 100\\. Multiples of 3 will be replaced with `Fizz`, multiples of 5 by `Buzz`, and multiples of both by `FizzBuzz`.\n\n    注意\n\n    这个活动的解决方案可以在第 524 页找到。\n\n这个简单的应用允许我们在一个常见的编码练习中使用一些常见的运算符，申请人可以被要求这样做。操作员允许我们与程序中的数据进行交互，因此对它们的使用有深刻的理解是关键。\n\n# 总结\n\n在这一章中，我们仔细研究了 C++ 提供的操作符，以及如何使用它们与数据进行交互。它们被分组展示——首先是算术运算符。这些允许我们对我们的值执行数学运算(例如将两个数字相加)，或者在我们刚刚完成的活动的情况下，使用模数来确定一个数字是否是另一个数字的倍数。然后，我们继续研究关系运算符。这些允许我们相互比较值，例如确定两个对象是否相等，或者一个数是否大于另一个数。\n\n然后我们继续讨论一元运算符。这些运算符对单个操作数进行操作，例如增加一个值或否定一个布尔值。这导致了对赋值和逻辑运算符的关注。我们探索了如何将简单赋值运算符和算术运算符结合起来，以更简洁地相乘我们的值，以及如何在单个条件下计算多个布尔值，例如检查两个布尔值是否为真。\n\n最后，我们快速浏览了一些高级的按位运算符，介绍了按位运算的概念。然后，我们通过查看运算符重载来结束这一章，通过这种方式，我们可以为用户定义的类型定义这些运算符的行为。\n\n我们在这一章学到的技能在这一章的最后一项活动`Fizz` `Buzz`挑战中得到了应用。我们打印了数字 1 到 100，但是当满足特定标准时，我们打印的是单词而不是数字。这是跨不同学科和语言的应用的常见编码练习，因此这是一个测试我们技能的很好的真实例子。\n\n本章总结了我们对 C++ 的初步介绍。前四章的目标是介绍一些核心主题和概念，让我们尽快开始编写代码。希望您现在对基础知识充满信心，并且能够轻松地打开编辑器并编写简单的 C++ 应用。现在我们进入下一组章节，我们将在这些基本技能的基础上，以继承、多态性和面向对象编程等主题更深入地探索 C++。"
  },
  {
    "path": "docs/cpp-workshop/05.md",
    "content": "# 五、指针和引用\n\n概观\n\n本章详细介绍了 C++ 内置的指针类型和引用类型，以便您有效地使用它们。指针和引用类型是构建数据结构的重要原材料，因此理解这些简单、原始的类型对于您作为 C++ 开发人员的成功至关重要。\n\n本章结束时，您将能够描述 C++ 使用的内存地址模型；解释指针和引用如何引用其他变量；声明、初始化和使用指针和引用；解释指针和数组是如何相似的；描述指针如何步进通过数组的元素；执行指针算术，并使用指针和引用作为函数参数。\n\n# 简介\n\n到目前为止，这本书已经研究了几种类型的变量:整数、字符、浮点数加数组和由这些简单类型组成的结构。在前几章中，已经向您介绍了指针和引用。在本章中，我们将更详细地研究这些变量。\n\n指针是*指向*另一个变量的变量。指针有类型；也就是说，指向`int` *的指针指向*或者指向一个`int`。`char`的指针指的是`char`。指向`int`的指针可以分配给另一个指向`int`的指针，但不能分配给指向`char`的指针。指向类`foo`的指针引用类`foo`的实例。指针也可以是特殊值`nullptr`，表示指针没有指向任何东西。引用是一个指针，但有一些限制，使用起来更安全，这将在本章后面详细讨论。\n\nC++ 指针可以指向任何数据结构中的任何变量，并且可以遍历数组。为了提高指针的效率，C++ 不检查指针是否指向包含与指针类型相同的变量的有效内存位置。这意味着指针会造成严重破坏，意外地覆盖不小心使用它们的程序中的数据。较新语言的发明者总是将指针命名为避免 C++ 的理由。然而，正如我们将在后面看到的，指针的风险相对容易管理。\n\n在 C++ 的早期，指针使它在遍历数组时比其他语言具有巨大的速度优势。当程序使用指针时，即使是简单的编译器也会产生优秀的代码。这种特殊的优势在现代 C++ 实现中不太重要，因为编译器已经变得更加复杂，但是指针仍然有优势。指针被深深地编织到 C++ 语言的结构和 C++ 编程的文化中。\n\n因为指针和引用可以指向其他数据结构，所以使用指针是一种快捷方式，无需重复编写代码来访问数据。这也可以让 C++ 比其他语言有速度优势。\n\n指针和引用可用于将复杂数据结构的一部分链接到另一部分。指针可以遍历数组，也可以遍历链接的数据结构。本章稍后将介绍遍历数组。下一章将讨论遍历链接数据结构。\n\n指针和引用也很有用，因为指向大数组或类实例的指针可以传递给函数，而不是将数组或实例复制到函数的形式参数中。指针在引用动态变量方面有重要作用；这将在第 6 章中描述。\n\n# 内存地址\n\n计算机的内存可以建模为一个很长的字节数组。每个字节都有一个地址，其作用与数组下标相同。每个变量都有一个地址，它可能是存储变量位的几个字节地址中的第一个。普通变量的名字被编译器翻译成地址。下图将内存区域显示为从左向右延伸的长磁带。磁带上方的十六进制数字是内存地址。为简单起见，我们只显示了每四个字节的地址:\n\n![Figure 5.1: Visualizing computer memory as a long array of bytes   ](img/C14195_05_01.jpg)\n\n图 5.1:将计算机内存可视化为一长串字节\n\n在程序声明变量之前，内存字节没有固定的意义。在图中，程序已经声明了一个名为`i`的`int`变量，并将其初始化为整数值`12345`。编译器为`int`变量保留了 4 个字节的存储空间，该变量定义了保存整数值的特定存储空间。编译器最初将`12345`放在那个存储器中，尽管程序可以在以后更改它。名称`i`现在是内存地址 **0x12A00404** 的同义词。\n\n## 指针\n\n指针是保存另一个变量地址的变量。也就是说，一个指针*指向另一个变量*。指针用类型名和星号`*`声明；因此，要声明一个名为`ptr`的指向`int`变量的指针，声明看起来像`int* ptr;`。纯粹主义者可能更喜欢把星号和变量名放在一起，就像在`int *ptr;`中一样。这里不涉及这种偏好的原因。\n\n运算符的`&`地址产生其参数的地址，将变量转换为指向该变量的指针。如果`i1`是一个`int`变量，那么`&i1`就是指向`i1`的`int`指针。`&`操作符可理解为“取…的地址”。地址运算符的作用可以通过参考下图来理解:\n\n![Figure 5.2: Pointer initialization ](img/C14195_05_02.jpg)\n\n图 5.2:指针初始化\n\n在该图中，使用声明`int *pi = &i;`，指针`pi`被初始化为指向`int`变量`i`。它指向内存地址 **0x12A00400** ，这是编译器放置`i`的地址。\n\n像 C++ 中的其他基本类型的变量一样，如果指针没有初始化，也没有为指针赋值，那么它就包含了创建时碰巧在内存中的随机位。这些随机位可能不指向任何有效变量的地址。\n\n因为指针中包含的值没有可解释的含义，所以很难判断指针是否被赋值。为了帮助解决这个问题，C++ 将常量`nullptr`定义为一个指针值，保证不指向任何有效的内存地址。`nullptr`可以分配给任何类型的指针。当整数常量`0`被赋值或与指针比较时，其含义与`nullptr`相同。在较旧的 C++ 代码中，您可能还会看到分配给指针的预处理器宏`NULL`，而不是`nullptr`。`NULL`通常定义为零。当所有指针变量被声明时，给它们赋值`nullptr`是个好主意。\n\n`*` ( `dereference`)运算符取消指针引用。也就是说，如果指针`p`引用一个`int`变量，`*p`就是它所引用的`int`变量。如果程序应用`*`运算符取消引用设置为`nullptr`的指针，程序将崩溃并显示一条简短的错误消息，因为程序试图访问一个没有映射到任何实际内存的机器地址。如果取消引用从未设置的指针，它可能会崩溃，或者继续运行，但不会产生有效的结果。\n\n考虑到指针的基本功能，第一个练习提供了一个非常简单的例子，说明如何在一个正常运行的 C++ 程序中把各个部分组合在一起。\n\n## 练习 26:指针\n\n在本练习中，您将编写一个非常简单的程序，创建一个指针，将其设置为指向一个`int`，然后通过指针更改`int`的值。程序将说明指针声明和赋值的语法。程序还将打印指针的值和`int`的地址，以证明它们是相同的，并且通过指针改变前后`int`的值，以验证它已经改变。\n\n注意\n\n这个练习的完整代码可以在这里找到:[https://packt.live/2qnUzCt](https://packt.live/2qnUzCt)。\n\n以下是完成练习的步骤:\n\n1.  首先进入`main()`功能的骨架:\n\n    ```cpp\n    #include <iostream>\n    using namespace std;\n    int main()\n    {\n        return 0;\n    }\n    ```\n\n2.  在函数`main()`中，声明一个`int`变量`i`，并将其初始化为`12345` :\n\n    ```cpp\n        int i = 12345;\n    ```\n\n3.  声明一个指向`int`变量`p`的指针，并将其初始化为指向`int` :\n\n    ```cpp\n        int *p = &i;\n    ```\n\n4.  Output the value of the pointer and the address of the `int` variable:\n\n    ```cpp\n        cout << \"p = \" << p << \", &i = \" << &i << endl;\n    ```\n\n    打印的具体十六进制地址可能因编译器而异，因运行而异，但关键是两个数字是一样的；也就是指针指向`int`。\n\n5.  输出`int`变量的值，`i` :\n\n    ```cpp\n        cout << \"i = \" << i << endl;\n    ```\n\n6.  使用`*`操作符取消指针引用，产生指向`int`。然后，将`2`添加到值中并再次保存:\n\n    ```cpp\n        *p = *p + 2;\n    ```\n\n7.  最后，打印出该值，以证明将`2`添加到取消引用的指针也将`2`添加到了`int` :\n\n    ```cpp\n        cout << \"i = \" << i << endl;\n    ```\n\n8.  完整的程序如下:\n\n    ```cpp\n    #include <iostream>\n    using namespace std;\n    int main()\n    {\n        int i = 12345;\n        int *p = &i;\n        cout << \"p = \" << p << \", &i = \" << &i << endl;\n        cout << \"i = \" << i << endl;\n        *p = *p + 2;\n        cout << \"i = \" << i << endl; \n        return 0;\n    }\n    ```\n\n9.  Compile and run the program. This is the output of one particular run of the compiled program:\n\n    ![Figure 5.3: Output produced by exercise 26 ](img/C14195_05_03.jpg)\n\n图 5.3:练习 26 产生的输出\n\n该结果中显示的十六进制地址可能与运行程序时打印的地址不同。这是意料之中的。重要的是这两个地址将是相同的。在为取消引用的指针赋值后，`int`的值也如预期的那样发生了变化。\n\n## 练习 27:取消引用空值\n\n取消引用`nullptr`会在运行时导致错误并停止程序。取消引用`nullptr`不是程序员故意做的事情。当程序中的某个执行路径在指针被使用之前没有将指针初始化为有效的机器地址时，这种情况就会发生。初始化每个指向`nullptr`的指针会产生一个特定的错误消息，而取消对未初始化指针的引用会导致更微妙的错误。以下是您可以执行的一些步骤，以查看其实际效果:\n\n注意\n\n这个练习的完整代码可以在这里找到:[https://packt.live/2pGNtZi](https://packt.live/2pGNtZi)。\n\n1.  Type in the following program:\n\n    ```cpp\n    #include <iostream>\n    using namespace std;\n    int main()\n    {\n        int *p1 = nullptr;\n        cout << \"p1 = \" << p1 << endl;\n        *p1 = 22;\n        return 0;\n    }\n    ```\n\n    您可以将其输入到一个在线 C++ 编译器中，或者使用您选择的编辑器为传统的 C++ 编译器创建一个文件。\n\n2.  现在运行程序。程序的一个特定运行的输出如下所示:\n\n![Figure 5.4: The program crashes with an error message ](img/C14195_05_04.jpg)\n\n图 5.4:程序崩溃并显示一条错误消息\n\n注意\n\n并非所有的在线 C++ 编译器都从操作系统打印消息。使用诸如 tutorialspoint([https://www.tutorialspoint.com/compile_cpp_online.php](https://www.tutorialspoint.com/compile_cpp_online.php))这样的编译器，确保看到前面的输出。\n\n不出所料，程序因操作系统的错误信息而崩溃。Windows 和 Linux 都会产生一条错误消息。如果您正在使用在线编译器，并且所使用的特定在线编译器没有显示错误消息，请尝试使用不同的在线编译器。\n\n## 指向数组的指针\n\n在 C++ 中，数组和指针几乎无法区分。指向数组开头的指针、第一个元素的地址和裸数组名都表示相同的意思。\n\n数组元素是变量。`&`运算符可用于获取要分配给指针的数组元素的地址。表达式`p = &a[2];`更新`p`指向数组`a`中的第三个条目(记住，数组从零开始)。\n\n指针的工作方式类似于 C++ 中的数组。它可以像数组一样下标。如果`p`指向`a[2]`，则表达式`p[3]`获取数组中的第六个条目(即`a[5]`处的条目)。\n\n## 练习 28:指向数组的指针\n\n这是关于指针和数组的几个练习中的第一个。在这个简单的练习中，您将设置一个指向数组元素的指针，并测试它是否指向预期值。您将下标一个指针，并看到它产生预期的数组元素。记住 C++ 中数组从零开始，这样`a[5]`就是第六个元素。\n\n注意\n\n这个练习的完整代码可以在这里找到:[https://packt.live/2OA77yz](https://packt.live/2OA77yz)。\n\n以下是完成练习的步骤:\n\n1.  Enter the skeleton `main()` function, as follows:\n\n    ```cpp\n    #include <iostream>\n    using namespace std;\n    int main()\n    {\n        return 0;\n    }\n    ```\n\n    如果你愿意，你可以编译和运行这个程序的每个部分；否则，您可以等到全部输入后再运行它。\n\n2.  跟随`main()`的左花括号，声明一个名为`a`的`7` `ints`数组，并初始化它。然后，声明一个名为`p`的指向`int`的指针，并将其设置为`nullptr`，这样我们就知道它被设置为未知地址:\n\n    ```cpp\n        int a[7]{ 1, 3, 5, 4, 2, 9, -1 };\n        int *p = nullptr;\n    ```\n\n3.  现在，使用运算符的`&`地址将`p`设置为`a[2]`的地址，以获取数组元素的地址:\n\n    ```cpp\n        p = &a[2];\n    ```\n\n4.  输出取消引用的指针`*p`和`a[2]`的值，查看指针是否实际指向`a[2]` :\n\n    ```cpp\n        cout << \"*p = \" << *p << \", a[2] = \" << a[2] << endl;\n    ```\n\n5.  接下来，输出`p[3]`和`a[5]`。这表明指针可以像数组一样下标，`p[3]`指向与`a[5]` :\n\n    ```cpp\n        cout << \"p[3] = \" << p[3] << \", a[5] = \" << a[5] << endl;\n    ```\n\n    相同的值\n6.  完整的程序如下:\n\n    ```cpp\n    #include <iostream>\n    using namespace std;\n    int main() \n    {\n        int a[7] {1, 3, 5, 4, 2, 9, -1};\n        int * p = nullptr;\n        p = & a[2];\n        cout << \"*p = \" << * p << \", a[2] = \" << a[2] << endl;\n        cout << \"p[3] = \" << p[3] << \", a[5] = \" << a[5] << endl;\n        return 0;\n    }\n    ```\n\n7.  编译并运行程序。下面是这个程序的输出:\n\n![Figure 5.5: Output of program in exercise 28](img/C14195_05_05.jpg)\n\n图 5.5:练习 28 中程序的输出\n\n正如预期的那样，打印的值是相等的。它们都是同一个数组元素，这可以通过查看数组初始值来验证。订阅指针的工作方式与订阅数组完全一样；然而，由于`a[2]`的地址被分配给指针而不是`a[0]`的地址，所以指针的下标偏离了数组的下标。\n\n## 指针算法\n\nC++ 将数组的名称转换成指向`a[0]`的指针，T0 是数组的第一个条目。其中`a`是数组的语句`p = a;`更新`p`指向`a`中的第一个条目。\n\n程序可以给指针添加一个。如果指针指向一个数组，`p+1`的结果是指向下一个数组元素的指针。指针的十六进制地址值随数组元素的字节大小而变化。\n\n该程序可以将任何整数表达式的值添加到指针上，从而产生一个前进了那么多元素的指针。如果`p`是指针，`k`是`int`，那么指针表达式`p+k`是与`p`相同类型的指针。\n\n如果一个指针指向同一个数组，程序可以从另一个指针中减去一个。结果是两个指针之间的数组元素的数量。如果两个指针没有指向同一个数组，则不能解释减去指针的结果。\n\n如果两个指针指向同一个数组，程序可以使用任何关系运算符(如`==`、`!=`、`<`、`>`、`<=`和`>=`)对它们进行比较。如果指针指向不同的数组，那么就会产生一个无意义的答案。\n\n## 练习 29:指针运算\n\n本练习演示了指针算术和指针关系运算符的工作原理，还将让您习惯于解释指针表达式。\n\n注意\n\n这个练习的完整代码可以在这里找到:[https://packt.live/2KVIvPV](https://packt.live/2KVIvPV)。\n\n以下是完成练习的步骤:\n\n1.  进入骨架`main()`功能。您可以在每一步后运行程序，或者等到全部输入后再运行:\n\n    ```cpp\n    #include <iostream>\n    using namespace std;\n    int main()\n    {\n        return 0;\n    }\n    ```\n\n2.  在`main()`的左花括号之后，声明一个五个`ints`的数组，叫做`numbers`。声明一个指向`int`的名为`pint`的指针，并将其初始化为`numbers`。声明另一个指向`int`的名为`p2`的指针，并将其初始化为指向`numbers[3]` :\n\n    ```cpp\n        int numbers[5]{ 0, 100, 200, 300, 400 };\n        int* pint = numbers;\n        int* p2 = &numbers[3];\n    ```\n\n3.  接下来，输出`pint`的值、指针表达式`pint+1`的值和`sizeof(int)`，它告诉你一个`int`在这台机器上占用多少字节的内存。虽然为指针打印的十六进制值通常是人类无法解释的，但是你会发现打印的两个十六进制数字有所不同`sizeof(int)`。将`1`添加到指针会增加指向类型的大小:\n\n    ```cpp\n        cout << \"pint = \" << pint << \", pint+1 = \" << pint+1 \n             << \", sizeof(int) = \" << sizeof(int) << endl;\n    ```\n\n4.  输出表达式`*(pint+1)`和下标指针的值`pint[1]`，以证明它们是相同的。然后输出`*(pint+4)`和`pint[4]`，也是一样的:\n\n    ```cpp\n        cout << \"*(pint+1) = \" << *(pint+1)\n             << \", pint[1] = \" << pint[1] << endl;\n        cout << \"*(pint+4) = \" << *(pint+4)\n             << \", pint[4] = \" << pint[4] << endl;\n    ```\n\n5.  输出指针表达式`p2 – pint`。差额应打印为`3` :\n\n    ```cpp\n        cout << \"p2 - pint = \" << p2 - pint << endl;\n    ```\n\n6.  使用`==`和`>`运算符输出两个指针比较。输出操纵器`boolalpha`使类型`bool`的表达式打印为`true`或`false`。否则转换为`int`，打印为 1 或 0。此外，比较运算符的运算符优先级低于输出插入运算符`<<`。比较表达式必须加圆括号以避免编译错误:\n\n    ```cpp\n        cout << \"p2 == pint = \" << boolalpha << (p2 == pint) << endl;\n        cout << \"p2 > pint = \" << boolalpha << (p2 > pint) << endl;\n    ```\n\n7.  完整的程序如下:\n\n    ```cpp\n    #include <iostream>\n    using namespace std;\n    int main() \n    {\n        int numbers[5] {0, 100, 200, 300, 400};\n        int * pint = numbers;\n        int * p2 = & numbers[3];\n        cout << \"pint = \" << pint << \", pint+1 = \" << pint + 1\n             << \", sizeof(int) = \" << sizeof(int) << endl;\n        cout << \"*(pint+1) = \" << * (pint + 1) \n             << \", pint[1] = \" << pint[1] << endl;\n        cout << \"*(pint+4) = \" << * (pint + 4) \n             << \", pint[4] = \" << pint[4] << endl;\n        cout << \"p2 - pint = \" << p2 - pint << endl;\n        cout << \"p2 == pint = \" << boolalpha << (p2 == pint) << endl;\n        cout << \"p2 > pint = \" << boolalpha << (p2 > pint) << endl;\n        return 0;\n    }\n    ```\n\n8.  编译并运行程序。程序的输出如下；请注意，在程序的另一次运行中，特定的十六进制地址可能会有所不同:\n\n![Figure 5.6: Output of program in exercise 29 ](img/C14195_05_06.jpg)\n\n图 5.6:练习 29 中程序的输出\n\n这是我们期望的输出:`a[1] == *(pint + 1)`和`a[4] == *(pint + 4)`。指针的行为就像 C++ 中的数组，指针减法的工作原理和预期的一样:`p2 – pint == 3`。最后，可以按照预期使用六个比较运算符来比较指针。\n\n## 练习 30:递增指针\n\n本练习将前面的练习组合在一起，做一些有用的工作，即在数组中步进指针并打印每个数组元素。\n\n注意\n\n这个练习的完整代码可以在这里找到:[https://packt.live/2CZzUHs](https://packt.live/2CZzUHs)。\n\n以下是完成练习的步骤:\n\n1.  再次进入骨架`main()`功能:\n\n    ```cpp\n    #include <iostream>\n    using namespace std;\n    int main()\n    {\n        return 0;\n    }\n    ```\n\n2.  在`main()`的左花括号之后，声明一个由五个`int`组成的名为`a`的数组并初始化它。声明一个名为`p`的`int`指针。代码如下:\n\n    ```cpp\n        int a[5]{ 10, 20, 30, 40, 50 };\n        int* p;\n    ```\n\n3.  现在进入`for`循环，从`a`的第一个元素开始`p`迭代`a`的每个元素，在 C++ 中是`a[0]`。递增`p`，使其依次指向每个条目。当`p`从`a`末端脱落时停止，即为`a[5]`。在循环中，输出每个条目。请注意，在输出表达式中，末尾有一个空格(`\" \"`)但没有`endl`，因此这些打印值出现在同一行。不要忘记在循环结束时输出一个`endl`。代码如下:\n\n    ```cpp\n        for (p = &a[0]; p < &a[5]; p = p + 1)\n        {\n            cout << *p << \" \";\n        }\n        cout << endl;\n    ```\n\n4.  完整的程序如下:\n\n    ```cpp\n    #include <iostream>\n    using namespace std;\n    int main()\n    {\n        int a[5]{ 10, 20, 30, 40, 50 };\n        int* p;\n        for (p = &a[0]; p < &a[5]; p = p + 1)\n        {\n            cout << *p << \" \";\n        }\n        cout << endl;\n        return 0;\n    }\n    ```\n\n5.  Compile and run the program. Here is its output:\n\n    ![Figure 5.7: Output of the program in exercise 30 ](img/C14195_05_07.jpg)\n\n    图 5.7:练习 30 中程序的输出\n\n    **细化 for 循环**\n\n    这个程序可以更好。现在，它在几个方面都不整洁。程序依赖于知道数组`a`有五个项目长。依赖数字常量是很危险的，因为如果以后在数组`a`中添加更多的元素，开发人员必须记住在常量出现的任何地方更改它们，而 C++ 在这方面没有提供任何帮助。首先要改变的是让`a`的大小由其初始值设定项来设定。声明`int a[]{ 10, 20, 30, 40, 50 };`表示让`a`的初始化器声明它的大小。\n\n    第二个要改变的是`for`循环。`a`的第一个元素可以写成`&a[0]`，但也可以只写成`a`，看起来比较简单:\n\n    ```cpp\n    for (p = a; p < &a[5]; p = p + 1)\n    ```\n\n    当`p`从数组`a`的末尾落下时，循环结束。有一种方法可以在不知道`a`大小的情况下构建这个指针表达式。表达式`sizeof(a)/sizeof(a[0])`表示以字节为单位取`a`的大小，除以`a`的一个元素的大小。结果是`a`中的元素数量。因此，终止条件是一个指针表达式，指向`a`结束后的第一个字节。看起来是这样的:\n\n    ```cpp\n    for (p = a; p < a + sizeof(a)/sizeof(a[0]); p = p + 1)\n    ```\n\n    最后要改变的是`for`循环步表达式。这个本来写的是`p = p + 1`，但是 C++ 里面有另外一个运算符也是这么做的。这叫做前缀增量运算符，`++ `。前缀递增运算符将指针值加 1，将结果保存在指针变量中，然后生成递增的指针。\n\n    此外，还有一个后缀`++ `增量运算符(`p++ `)，其工作原理略有不同。后缀递增运算符在递增指针值之前首先记录指针的值，给指针加 1 并将结果保存到指针变量中，然后在递增之前生成保存的值。\n\n    有前缀和后缀`--`递减运算符，它们的工作方式与它们的`++ `表亲相似，只是它们从指针中减去一。所以，`for`的声明最后看起来是这样的:\n\n    ```cpp\n    for (p = a; p < a + sizeof(a)/sizeof(a[0]); ++ p)\n    ```\n\n    这看起来像是你在商业 C++ 代码中会遇到的那种`for`循环。\n\n    那么，为什么 C++ 中会有一个特殊的`++ `运算符呢？嗯，这是因为一种被称为 PDP-11 的过时小型计算机可以在一条指令中进行前后递增和递减。大多数现代处理器，受 C 和 C++ 存在的影响，也有做前后递增和递减的指令。现在，你可以看到 C++ 是如何得名的。这是一个双关语，这种语言是在 c 语言中加入最少量的内容而产生的。\n\n6.  完整的更新程序如下。运行该程序，并自己验证它产生的输出是否与之前的版本相同:\n\n    ```cpp\n    #include <iostream>\n    using namespace std;\n    int main()\n    {\n        int a[]{ 10, 20, 30, 40, 50 };\n        int* p;\n        for (p = a; p < a + sizeof(a)/sizeof(a[0]); ++ p)\n        {\n            cout << *p << \" \";\n        }\n        cout << endl;\n        return 0;\n    }\n    ```\n\n在数组元素中递增指针的习惯用法在 C++ 中经常出现。这个`for`循环有多种编写方式——有些使用指针，有些没有。\n\n## 指向指针的指针\n\n一个指针可以引用另一个指针。如果`char* p;`是指向`char`的指针，那么`char** q = &p;`就是指向`char`的指针。这种奇异的类型在哪里可能有用？当然，当处理指针数组时。\n\n## 练习 31:指向指针的指针\n\n在本练习中，您将使用指向指针的指针来操作指针数组。\n\n注意\n\n这个练习的完整代码可以在这里找到:[https://packt.live/2O6jG5t](https://packt.live/2O6jG5t)。\n\n以下是完成练习的步骤:\n\n1.  输入骨架`main()`功能:\n\n    ```cpp\n    #include <iostream>\n    using namespace std;\n    int main()\n    {\n        return 0;\n    }\n    ```\n\n2.  Following the opening curly brace of `main()`, declare an array alphabet of literal character strings. `alphabet` is an array of pointers to `const char`:\n\n    ```cpp\n        char* alphabet[26] \n        {\n            \"alpha\",\n            \"bravo\",\n            \"charlie\",\n            \"delta\",\n            \"echo\",\n            \"foxtrot\"\n        }; \n    ```\n\n    数组字母表被声明为具有`26`条目，大概对应于构成北约无线电字母表的 26 个口语单词。但是，只有前六个数组条目被初始化；编译器将剩余的 20 个条目设置为`nullptr`。在指针数组中创建最后一个条目`nullptr`是提供循环终止条件的另一种方式。\n\n3.  Next, enter a `for` loop to print the entries of `alphabet` until the program comes to one that is equal to `nullptr`:\n\n    ```cpp\n        for (char **p = alphabet; *p != nullptr; ++ p)\n        {\n            cout << *p << \" \";\n        }\n        cout << endl;\n    ```\n\n    感应变量`p`是指向`char`的指针类型。现在，`p`最初被设置为`alphabet`(指向`char`的指针数组)，编译器将其转换为指向`char`的指针。`for`循环的继续条件是`*p`不等于`nullptr`。每次迭代结束时，指针`p` 递增。在`for`循环中，我们打印`*p`，这是一个指向 char 的指针，后跟一个空格。\n\n    通过打印不带尾部的条目`endl`，它们都被打印在同一行上。C++ 输出流试图打印一个指向`char`的指针，就像它是一个空终止的字符串一样。像在前面的练习中一样，在循环之后输出`endl`，这样该行实际上就进入了输出。\n\n4.  完整的程序如下:\n\n    ```cpp\n    #include <iostream>\n    using namespace std;\n    int main()\n    {\n        char* alphabet[26] \n        {\n            \"alpha\",\n            \"bravo\",\n            \"charlie\",\n            \"delta\",\n            \"echo\",\n            \"foxtrot\"\n        }; \n        for (char **p = alphabet; *p != nullptr; ++ p)\n        {\n            cout << *p << \" \";\n        }\n        cout << endl; \n        return 0;\n    }\n    ```\n\n5.  编译并运行程序。该程序的输出如下:\n\n![Figure 5.8: Printing the first six entries of the array alphabet ](img/C14195_05_08.jpg)\n\n图 5.8:打印数组字母表的前六个条目\n\n除了输出，编译器还会打印六行警告消息，每一行都表示类似*警告的内容:ISO C++ 禁止将字符串常量转换为‘char *’*或类似的内容。一些在线编译器将这些错误消息打印在与输出相同的窗口中。对于其他人，您必须单击编译按钮来查看错误消息。要使这些错误信息消失，请将`alphabet`的类型更改为`char const* alphabet[26]`，并将`p`(即`for`回路感应变量)的类型更改为`char const** p;`。编译并运行更改后的程序，注意警告信息已经消失。\n\n在 C++ 中，文字字符串是指向`const char`的指针类型。因此，文字字符串数组的类型指针指向`const char`。\n\n声明符`const char`表示程序不能改变指向的字符。在 C 语言中，文字字符串是指向`char`的指针类型。C++ 最初也是这样的，但是 C++ 进行了更新，将这些字符串改为指向`const char`。常量这个话题在 C++ 中是很重要的，但是这个话题的范围太广了，在这本书里谈不到。\n\n注意\n\n消除代码中的警告信息是专业开发人员的标志。\n\n# 参考文献\n\n引用是保存另一个变量地址的第二种变量。也就是说，参考*指向*另一个变量。与指针不同，指针可以引用有效的变量、无效的内存位置或`nullptr`，引用在声明时必须被初始化以指向变量。\n\n引用和指针的一个区别是引用不能被更新；一旦声明，它总是指向同一个变量。这意味着引用不能像指针那样递增来遍历数组。\n\n第二个区别是引用在使用中被隐式取消引用。应用于引用的算术和关系运算符会影响指向的变量。如果`ir`是`int`引用，那么语句`ir = ir – 10;`从引用的`int`中减去 10。因此，涉及引用的数学表达式具有非常自然的外观。开发人员可以使用引用来有效地指向具有数字含义的变量，如复数或矩阵，而表达式如`a = b * c;`有其预期的含义。\n\n相比之下，指针上的算术和关系运算指的是作为指针本身值的机器地址，而不是指向的变量。如果像矩阵这样的数字类型被指针指向，那么得到的数学表达式需要显式的去引用操作符，这样它们可能看起来就像`*a = *b * *c;`一样，聪明的学生可能会注意到它包含许多误解的可能性。\n\n在下一个练习中，我们将练习声明和使用引用。\n\n## 练习 32:参考文献\n\n本练习涉及一个小程序，该程序创建一些引用来说明它们的语法并演示它们的属性。\n\n注意\n\n这个练习的完整代码可以在这里找到:[https://packt.live/33aoIlH](https://packt.live/33aoIlH)。\n\n以下是要完成的步骤:\n\n1.  键入骨架`main()`功能:\n\n    ```cpp\n    #include <iostream>\n    using namespace std;\n    int main()\n    {\n        return 0;\n    }\n    ```\n\n2.  跟随`main()`的左花括号，声明一个名为`i`的`int`变量，并将其初始化为`10`。声明一个`int`引用，`ir`，并将其初始化为指向`i`。引用用类型名和`&`声明，并初始化为一个变量，例如`int& ir = i;`或`int& ir {i};` :\n\n    ```cpp\n        int i = 10;\n        int& ir = i;\n    ```\n\n3.  现在将`i + 10`分配给`i`，将`ir * 10`分配给`ir`。请注意，使用`int`的算术表达式看起来和使用`int`引用的一样:\n\n    ```cpp\n        i = i + 10;\n        ir = ir * 10;\n    ```\n\n4.  输出`i`的值，证明当程序改变`ir`时，确实改变了`i`的内容(提示:`(10 + 10) * 10 = 200` ):\n\n    ```cpp\n        cout << \"i = \" << i << endl;\n    ```\n\n5.  声明一个名为`ip`的指针，并将其初始化到`ir`的地址。运算符`&`的地址影响`ir`指向的变量，因此`ip`现在指向`i`。取消引用`ip`以更改`ip`指向`33`的变量的值:\n\n    ```cpp\n        int* ip = &ir;\n        *ip = 33;\n    ```\n\n6.  现在输出`i`、`*ip`、`ir`来证明改变`*ip`真的改变了`i`，并且`ir`也改变了:\n\n    ```cpp\n        cout << \"i = \" << i << \", *ip = \" << *ip\n             << \", ir = \" << ir << endl;\n    ```\n\n7.  完整的程序如下:\n\n    ```cpp\n    #include <iostream>\n    using namespace std;\n    int main() \n    {\n        int i = 10;\n        int & ir = i;\n        i = i + 10;\n        ir = ir * 10;\n        cout << \"i = \" << i << endl;\n        int *ip = & ir;\n        *ip = 33;\n        cout << \"i = \" << i << \", *ip = \" << * ip \n             << \", ir = \" << ir << endl;\n        return 0;\n    }\n    ```\n\n8.  编译并运行程序。程序的输出如下所示:\n\n![Figure 5.9: Output of program in exercise 32 ](img/C14195_05_09.jpg)\n\n图 5.9:练习 32 中程序的输出\n\n输出显示引用和指针都是指向另一个变量的类型。当程序修改引用或取消引用的指针时，它会修改指向的变量。\n\n## 练习 33:不良参考\n\n声明时引用总是指向变量，有效的引用总是指向变量。不幸的是，引用可能会变得无效。本练习向您介绍了 C++ 的一条黑暗的小巷，在那里引用可以是空的或无效的。\n\n注意\n\n这个练习的完整代码可以在这里找到:[https://packt.live/2KHdpes](https://packt.live/2KHdpes)。\n\n以下是完成练习的步骤:\n\n1.  输入以下非常短的程序:\n\n    ```cpp\n    int main()\n    {\n        char* p = nullptr;\n        char& r = *p;\n        r = '!';\n        return 0;\n    }\n    ```\n\n2.  Run the program. If you are using an online compiler, use one such as **coliru**, which captures error messages output from the operating system:\n\n    ![Figure 5.10: Dereferencing nullptr causes the operating system to stop the program ](img/C14195_05_10.jpg)\n\n    图 5.10:取消引用 nullptr 会导致操作系统停止程序\n\n    请注意，它因操作系统错误而崩溃。很明显发生了什么。指针指向`nullptr`。参考设置为指向`nullptr`。取消引用`nullptr`会导致操作系统停止程序。这称为空引用。C++ 在你编译的时候会对你微笑，只有在运行的时候你才会发现你的致命错误。其他编程语言可能会在取消引用`nullptr`之前检查每个引用，但这会降低执行速度，而 C++ 则关乎性能。C++ 允许你编写崩溃的代码，因为它假设你知道你在做什么。\n\n3.  Examine the following function:\n\n    ```cpp\n    int& invalid_ref() \n    {\n        int a = 10; \n        return a; \n    }\n    ```\n\n    此函数返回对函数调用堆栈上局部变量的引用。当函数返回时，变量超出范围并变得无效，从而产生无效的引用。下一个被调用的函数几乎肯定会覆盖先前被`a`占用的存储。程序不一定会崩溃，但也不会可靠地产生正确的答案。\n\n一些专家会告诉你，引用比指针更安全。忽视这个建议是明智的。诚然，有效的引用总是指向一个变量，但是 C++ 允许开发人员创建无效的引用和空引用。指针和引用之间的差异应该被认为是风格上的差异，而不是安全性上的差异。\n\n## 作为函数参数的指针和引用\n\n当表达式是函数调用的参数时，表达式的值被复制到函数调用堆栈上的函数本地存储中。当表达式是基本类型(如`int`或`float`)时，复制成本不是问题，但是当参数是具有许多成员的结构或类实例时，复制会消耗大量时间。这些对象可能包含大型数组或链接数据结构(这些将在下一节中讨论)。\n\n程序可以将实例的引用或指针传递给函数，而不是将结构或类实例直接传递给函数。这允许大数据结构被有效地传递。指针和引用同样有效，所以选择使用哪一个取决于风格。\n\n传递到函数中的指针应检查`nullptr`。使用引用作为函数参数证明了程序员认为引用必须是有效的，并且不会在函数内部被检查。\n\n因为一个指针可以是`nullptr`，所以当一个参数是可选的时候是很有用的。也就是说，计算函数时可能需要参数，也可能不需要参数。必须始终为引用参数提供值。\n\n当参数指向数组时，指针参数是合适的。\n\n因为指针或引用指向的存储来自函数外部，所以当程序想要从函数传递信息时，或者当函数的目的是修改数据结构时，指针或引用参数也很有用。\n\n## 练习 34:指针作为函数参数\n\n这个程序包含一个将`char`的数组复制到另一个数组中的函数。由于函数的参数是数组，指针比函数的形式参数的引用更合适。\n\n注意\n\n这个练习的完整代码可以在这里找到:[https://packt.live/2D3sYcs](https://packt.live/2D3sYcs)。\n\n以下是完成练习的步骤:\n\n1.  进入骨架`main()`功能:\n\n    ```cpp\n    #include <iostream>\n    using namespace std;\n    int main()\n    {\n        return 0;\n    }\n    ```\n\n2.  跟随`using namespace std`；进入`copychars()`功能的骨架。`copychars()`使用两个`char`指针，一个用于复制，另一个用于复制。还需要一个`int`T5 的人物来复制:\n\n    ```cpp\n    void copychars(char* from, char* to, int count)\n    {\n    }\n    ```\n\n3.  指针应该与`nullptr`进行比较，除非开发人员绝对确定调用者已经检查过它们。这个代码应该在`copychars()`的左括号后面。看起来是这样的:\n\n    ```cpp\n        if (from == nullptr || to == nullptr)\n            return;\n    ```\n\n4.  Now enter the main copy loop, that copies `count` characters:\n\n    ```cpp\n        while (count-- > 0)\n        {\n           *to++ = *from++ ;\n        }\n    ```\n\n    每个字符从`from`指向的位置复制到`to`指向的位置。循环的核心是语句`*to++ = *from++ ;`，它复制一个字符并增加两个指针，以便它们准备复制下一个字符。这是 C++ 中非常常见的一个成语，所以值得详细看一下。这两个`++ `操作符称为后增量操作符。他们使用将要增加的变量，然后作为副作用增加它。你可以想象这个语句扩展成复合语句{ `*to = *from;` `to = to + 1;` `from = from + 1;` }。编译器知道如何为这个习惯用法生成非常高效的代码。运算符优先级是合理的，因此您不必在任何东西周围加括号来使这个语句起作用。\n\n    现在输入`main()`功能的内容。首先，声明一个名为`string[]`的数组，并将其初始化为`\"uvwxyz\"`。当你编译这个的时候，你会注意到没有关于`string[]`不是`const char`的消息。那是因为`string[]`初始化时，文字字符串`\"uvwxyz\"`被复制到`string[]`中。注意程序没有为数组`string[]`指定大小。C++ 编译器知道它是用七个字符初始化的——七个字符，因为一个空字符`'\\0'`被附加到文本字符串的末尾以标记它的结尾:\n\n    ```cpp\n        char string[] { \"uvwxyz\" };\n    ```\n\n5.  声明一个名为`buffer[]`的`10`字符数组。这是程序将复制到的数组。现在程序可以调用`copychars()`，其中`string[]`在`from`参数位置，`buffer[]`在`to`参数位置。`count`设置为`7` :\n\n    ```cpp\n        char buffer[10];\n        copychars (string, buffer, 7);\n    ```\n\n6.  最后输出`buffer[]`证明`string[]`被移入`buffer[]` :\n\n    ```cpp\n        cout << buffer << endl;\n    ```\n\n7.  完整的程序如下:\n\n    ```cpp\n    #include <iostream>\n    using namespace std;\n    void copychars(char* from, char* to, int count)\n    {\n        if (from == nullptr || to == nullptr)\n            return;\n        while (count-- > 0)\n        {\n            *to++ = *from++ ;\n        }\n    }\n    int main()\n    {\n        char string[] { \"uvwxyz\" };\n        char buffer[10];\n        copychars (string, buffer, 7);\n        cout << buffer << endl;\n        return 0;\n    }\n    ```\n\n8.  编译并运行程序，如果你还没有。程序输出如下:\n\n![Figure 5.11: Output of the program in exercise 34 ](img/C14195_05_11.jpg)\n\n图 5.11:练习 34 中程序的输出\n\n这证明字符如预期的那样被复制到输出缓冲区。\n\n注意\n\n缓冲区复制功能几乎总是充满安全风险。复制字符直到到达`from`缓冲区的空终止的函数有复制比`to`缓冲区声明保存的更多字符的风险。这会导致意外覆盖其他变量。标准库`strcpy()`功能有这个缺陷。假设调用程序已经检查了缓冲区`to`是否有足够的空间，指定长度只能稍微降低这种风险。完全安全的函数将指定缓冲区的最大大小`to`，并使用空终止或另一个`count`来指定要复制的字符数。\n\n## 指向类或结构的指针\n\n使用`.`成员访问或点运算符选择类或结构的成员，例如`instance.membername`。当指针指向一个实例时，必须首先使用`*`操作符取消指针引用。由于运算符优先级和关联性规则，此表达式必须加上圆括号，例如`(*pinstance).membername`。C++ 的开发人员提供了一种简化的符号。\n\n`pinstance->membername`取消指针引用，然后选择命名成员。\n\n## 练习 35:指向类实例的指针\n\n在本练习中，程序将输出结构实例数组的内容。结构和类在 C++ 中是相似的。一个结构的所有成员都是公共的，因此该结构只需要较少的行。在生产代码中，更有可能使用一个类，这将在另一章中描述。\n\n注意\n\n这个练习的完整代码可以在这里找到:[https://packt.live/2Xw60Eb](https://packt.live/2Xw60Eb)。\n\n以下是完成练习的步骤:\n\n1.  进入骨架`main()`功能。它有下面熟悉的形式:\n\n    ```cpp\n    #include <iostream>\n    using namespace std;\n    int main()\n    {\n        return 0;\n    }\n    ```\n\n2.  进入`struct mydata`。它有一个名为`name_`的`char const*`字段和一个名为`hero_`的`int`字段。`mydata`是一个结构，所以字段被自动声明为公共的，因此可以从结构外部访问。请注意，成员名称有一个尾随下划线。我们一会儿再谈。结构`mydata`看起来像这样:\n\n    ```cpp\n    struct mydata\n    {\n        char const* name_;\n        bool hero_;\n    };\n    ```\n\n3.  接下来，创建一个名为`cast`的`mydata`实例数组，并初始化它，如下所示。你可能会认出数组中的条目是一些漫画超级英雄的名字。这里`hero_`成员设置为`true`如果角色是英雄，设置为`false`如果角色是反派。没有给数组一个明确的大小，所以初始化器的数量设置它的大小:\n\n    ```cpp\n    mydata heroes[]\n    {\n        { \"Spider Man\", true },\n        { \"The Joker\", false },\n        { \"Doctor Octopus\", false },\n        { \"Thor\", true },\n        { \"Batman\", true },\n        { \"Loki\", false }\n    };\n    ```\n\n4.  接下来，输入`printdata()`功能。该函数打印出一个`mydata`实例:\n\n    ```cpp\n    void printdata(mydata * p) \n    {\n        cout << \"Hello. I am \" << ( * p).name_ << \". \";\n        if (p - > hero_)\n            cout << \"I am a hero.\" << endl;\n        else\n            cout << \"I am a villain.\" << endl;\n    }\n    ```\n\n5.  在`main()`中，输出结构`mydata`实例的大小，后跟指向`mydata`的指针的大小。实例更大，因此作为参数复制到函数中比指针更昂贵。在生产代码中，`mydata`可能有数百或数千字节长，或者有一个执行昂贵操作的构造函数。因此，传递指针而不是复制实例更有效:\n\n    ```cpp\n        cout << sizeof(mydata) << \" \" << sizeof(mydata*) << endl;\n    ```\n\n6.  接下来，输入一个`for`循环，打印出`heroes[]`数组中的`mydata`实例。您以前见过这样的代码:从第一个实例开始，单步执行下一个实例，当结束时终止。是的，这个代码与使用硬连线常数来描述数组的大小有相同的问题:\n\n    ```cpp\n        for (mydata* p = heroes; p < heroes + 6; ++ p)\n        {\n            printdata(p);\n        }\n    ```\n\n7.  完整的程序如下:\n\n    ```cpp\n    #include <iostream>\n    using namespace std;\n    struct mydata \n    {\n        char const * name_;\n        bool hero_;\n    };\n    mydata heroes[]\n    {\n        {\"Spider Man\", true}, \n        {\"The Joker\", false}, \n        {\"Doctor Octopus\", false},\n        {\"Thor\", true},\n        {\"Batman\", true},\n        {\"Loki\", false}\n    };\n    void printdata(mydata * p) \n    {\n        cout << \"Hello. I am \" << ( * p).name_ << \". \";\n        if (p - > hero_)\n            cout << \"I am a hero.\" << endl;\n        else\n            cout << \"I am a villain.\" << endl;\n    }\n    int main() \n    {\n        cout << sizeof(mydata) << \" \" << sizeof(mydata * ) << endl;\n        for (mydata * p = heroes; p < heroes + 6; ++ p) \\\n        {\n            printdata(p);\n        }\n        return 0;\n    }\n    ```\n\n8.  Compile and run the program. Here is the output of the program:\n\n    ![Figure 5.12: Output of the program in exercise 35 ](img/C14195_05_12.jpg)\n\n    图 5.12:练习 35 中程序的输出\n\n    我们可以解决硬编码的尺寸问题。我们在使用`sizeof(array) / sizeof(array[0])`之前做过一次。然而，还有另一种方法——使用`std::end()`功能。`std::end()`本质上做了和`sizeof`相同的事情，但是它必须使用沉重的模板魔法将整个数组声明复制到函数中，并防止它衰减成指针。以下是`std::end()`的`for`声明:\n\n    ```cpp\n        for (mydata* p = heroes; p < std::end(heroes); ++ p)\n    ```\n\n9.  `std::end()` works for arrays and pointers, and it also works for iterators that step through standard library container classes, which you will learn more about in *Chapter 10*, *Advanced Object-Oriented Principles*. There's another function, `std::begin()`, that produces a pointer to the beginning of the array (or an iterator to the beginning of a standard library container).\n\n    完善你的`for`循环还有一个部分。`std::begin()`返回指针或迭代器。然而，`for`语句声明了一个指针。这并不完全通用，但现代 C++ 提供了一个解决方案。叫`auto`。\n\n    现在，`auto`声明一个变量，当它的类型在上下文中很明显时，比如当它是赋值语句的目标时。`auto`非常适合声明`for`循环归纳变量。在我们的程序中，我们已经包含了`namespace` `std`，所以我们不需要使用`std::`前缀。有了所有这些变化，我们的`for`声明看起来非常精简:\n\n    ```cpp\n        for (auto p = begin(heroes); p < end(heroes); ++ p)\n    ```\n\n    注意\n\n    除非由文字常量初始化，否则使用像`mydata::name_`这样的指针成员是有风险的。`name_`指向的存储必须保持有效，直到类实例超出范围，否则指针将指向无效内存，程序将运行不良。\n\n现在，我们已经进行了一些将指针解引用到类实例的练习，并且我们已经学习了如何构建漂亮且通用的`for`循环。下一个练习是关于使用引用作为函数参数。\n\n## 作为函数参数的引用\n\n引用包含指向数据的指针，就像指针一样。但是，如前所述，应用于引用的运算符也应用于指向的对象。要选择引用所指向的结构或类的成员，请使用`.`成员访问或点运算符。点运算符适用于指向变量；也就是类实例。将`.`用于引用会生成与将`->`用于指针相同的代码。\n\n引用和指针的另一个区别是，可以用变量初始化引用。对于指针，您必须显式获取变量的地址，将其转换为指针，以便将其分配给指针。同样的约定也适用于函数参数。引擎盖下发生的事情是，对实例的类型引用的形式参数被初始化为指向实际的参数实例。\n\n有一种特殊形式的`for`循环，当程序需要引用数组的每个元素时，它适用于遍历数组。它被称为基于范围的`for`循环。语法如下:`for (mydata& ref : arr)`。编译器将变量`arr`识别为数组，并生成代码来遍历数组的每个元素。每个元素依次被分配给`ref`。记得我们说过引用变量一旦设置就不能修改，但是这个引用变量每次通过循环都是新创建的。\n\n这个`for`循环的进一步细化是使用`auto`关键字，就像在`for (auto& ref : arr)`中一样。`auto`关键字要求编译器通过查看`arr`的元素类型来推断`ref`的类型。`&`运算符告诉`for`循环，每次通过循环时，它应该初始化对数组元素的引用，而不是将数组元素复制到实例变量中。\n\n## 练习 36:作为函数参数的引用\n\n这个程序与前面练习中的程序非常相似，只是它使用引用而不是指针。它打印一个类实例数组。\n\n注意\n\n这个练习的完整代码可以在这里找到:[https://packt.live/2QBoj9l](https://packt.live/2QBoj9l)。\n\n以下是完成练习的步骤:\n\n1.  输入之前看过很多次的骨骼`main()`功能:\n\n    ```cpp\n    #include <iostream>\n    using namespace std;\n    int main()\n    {\n        return 0;\n    }\n    ```\n\n2.  Next, enter the definition of struct `mydata`. This example is `const-correct` and does not generate any warning messages from the compiler, like the previous exercise did:\n\n    ```cpp\n    struct mydata\n    {\n        char const* name_;\n        bool darkside_;\n        mydata (char const* name, bool dark) \n        { \n            name_ = name; darkside_ = dark; \n        }\n    }\n    ```\n\n    请注意，构造函数的`name`参数具有类型`char const*`，并且`name_`成员也具有类型。\n\n    为什么结构`mydata`的成员变量的名字后面有下划线？\n\n    是为了让*8 号线*的施工人员工作。如果构造函数有一个名为`name`的参数，而该结构有一个名为`name`的成员，您将无法在构造函数中设置该成员，因为它的名称将被参数的名称隐藏。大多数 C++ 编码标准要求类字段具有特定格式的名称。尾部下划线是 C++ 标准文档中使用的一种这样的形式。还有很多其他的。\n\n3.  初始化三个`mydata`实例的数组转换:\n\n    ```cpp\n    mydata cast[3]\n    {\n        { \"Darth Vader\", true },\n        { \"Luke Skywalker\", false },\n        { \"Han Solo\", false }\n    };\n    ```\n\n4.  输入`printname()`功能。它引用了`mydata`的一个实例作为参数。当使用对结构或类实例的引用时，使用点`.`成员访问运算符来访问成员。点运算符适用于被引用的对象，而不是引用:\n\n    ```cpp\n    void printname(mydata& data)\n    {\n        cout << \"Hello. I am \" << data.name_ << endl;\n        if (data.darkside_)\n            cout << \"I was seduced by the dark side\" << endl;\n    }\n    ```\n\n5.  Now enter the contents of function `main()`:\n\n    ```cpp\n        for (mydata& data : cast)\n        {\n            printname(data);\n        }\n    ```\n\n    因为程序使用引用，所以可以使用`for`循环的基于范围的版本。它由一个归纳变量的声明组成，在这个例子中看起来像`mydata&` `data`，后跟一个冒号，然后是可以产生一系列数据的东西。在这种情况下，数组产生一系列数据。\n\n6.  完整的程序如下:\n\n    ```cpp\n    #include <iostream>\n    using namespace std;\n    struct mydata\n    {\n        char const* name_;\n        bool darkside_;\n        mydata (char const* name, bool dark) \n        {\n            name_ = name; darkside_ = dark; \n        }\n    };\n    mydata cast[3]\n    {\n        { \"Darth Vader\", true },\n        { \"Luke Skywalker\", false },\n        { \"Han Solo\", false }\n    };\n    void printname(mydata& data)\n    {\n        cout << \"Hello. I am \" << data.name_ << endl;\n        if (data.darkside_)\n            cout << \"I was seduced by the dark side\" << endl;\n    }\n    int main()\n    {\n        for (mydata& data : cast)\n        {\n            printname(data);\n        }\n        return 0;\n    }\n    ```\n\n7.  Compile and run the program. Here is its output:\n\n    ![Figure 5.13: The output of the program in exercise 36 ](img/C14195_05_13.jpg)\n\n    图 5.13:练习 36 中程序的输出\n\n8.  编辑程序，在`for`循环中使用`auto`，使其显示`for (auto& data : cast)`。编译并运行程序，看看它是如何工作的。\n9.  移除`for`循环中的`&`，使其显示`for (auto data : cast)`。编译并运行程序。`auto data`也有效，但效率较低，因为它将数组的元素复制到`data`中，这是类型`mydata`，而不是`mydata&`。如果这些元素中有大量数据，那就是大量复制。\n\n## 活动 5:使用指针和引用来操作字符串数组\n\n这是本章关于指针和参考文献的总结活动。在本练习中，您将被要求使用指针和引用来编写操作字符串数组的函数，并提供测试以确保代码正确工作。这个函数就像世界各地的开发人员每年编写的成千上万个类似的函数。\n\n该函数称为`printarray()`。它将两个指针作为参数放入以 null 结尾的文字字符串数组中。一个指针指向`printarray()`将要打印的数组的第一个条目，另一个指针指向最后一个要打印的条目之后的一个条目。`printarray()`还将`printarray()`设置的`int`引用为非`nullptr`字符串的计数作为参数。此外，`printarray()`向控制台输出非`nullptr`的字符串，每行一个字符串。`printarray()`如果运行成功则返回 1，如果检测到参数有问题则返回 0。该数组的最大大小为 26 个元素。\n\n注意\n\n这个活动的完整代码可以在这里找到:[https://packt.live/2XxhSWt](https://packt.live/2XxhSWt)。\n\n主程序必须用各种参数测试函数，包括无效参数。\n\n以下是完成活动的步骤:\n\n1.  Enter a skeleton `main()` function.\n\n    在`main()`上方，创建一个字符串数组。如果使用按字母顺序排列的字符串，比如`\"alpha\"`、`\"bravo\"`、`\"charlie\"`等，或者`\"alphs\"`、`\"bets\"`、`\"gamms\"`等，代码会更容易调试。\n\n2.  输入`printarray()`功能的框架。因为我们打印的是一个文字字符串数组，所以指针是`char const**`类型的。`count`的论点是`int`的参考。定义返回类型，在分配中指定为`int`。\n3.  在`printarray()`中，输入代码以检测`printarray()`的参数中的错误。\n4.  清除`count`。\n5.  输入控制打印的循环。\n6.  Inside `main()`, write some tests. The tests should check whether the returned value is correct for the arguments. You can also look at the count of arguments printed.\n\n    注意\n\n    这个活动的解决方案可以在第 526 页找到。\n\n# 总结\n\n指针和引用是指向其他变量的两种 C++ 类型。它们在重叠的情况下很有用，指针和引用之间的选择主要是风格的选择。指针和引用是“不安全”的 C++ 特性的例子，也就是说，它们必须被有知识地使用来防止导致程序崩溃的错误。到目前为止，指针和引用最重要的用途是遍历数组，并将大型数组或类实例有效地传递到函数中。\n\n下一章探讨指针的另一个非常重要的用途——即引用动态变量。动态变量没有名称，只有引用它们的指针知道。`Dynamic variables`允许 C++ 程序访问现代计算机中的大量内存，并构建复杂的容器。"
  },
  {
    "path": "docs/cpp-workshop/06.md",
    "content": "# 六、动态变量\n\n概观\n\n本章介绍了动态变量，即可以在需要时创建的变量，它可以保存任意大量的数据，只受可用内存量的限制。本章结束时，您将能够描述为什么动态变量很重要；创建动态变量和数组；描述堆栈和堆之间的区别；通过指针引用动态变量和数组；删除动态变量和数组，并使用指针创建链接数据结构。\n\n# 简介\n\n到目前为止介绍的所有基本类型的变量、数组和结构都有一个在编译时已知的固定大小。固定大小的变量有很多优点；它们可以首尾相连，以便有效地使用内存。编译后的机器代码可以非常快速地访问固定大小的变量，但是固定大小的变量有一个缺点。没有办法在固定大小的变量中保存任意大的数据结构。开发人员必须预料到程序将被要求解决的最大问题。当一个程序解决一个较小的问题时，内存就被浪费了，当一个程序试图超过它的容量时，它就会失败。\n\n例如，想象一下，一个开发人员想要将所有的单词存储在一本书里，但是只能使用固定大小的变量。他们可以声明一个二维数组`char`来保存单词，但是数组应该有多大呢？\n\n平均每本书有 75，000 到 100，000 字。开发者可以选择 100，000 字的最差大小来容纳许多书，但可能不是全部。英语单词平均长度约为 8 个字符，但最长的单词要长得多。开发人员也必须为单词选择最差的大小——例如 20 个字符。因此，数组的声明如下:\n\n```cpp\nchar book[100000][20];\n```\n\n这个数组的大小是 200 万字节，以现代的比较标准来看，这是适度的。但是不管你把数组做得多大，一本书可能都放不下，要么是因为它有很长的单词，要么是因为它有太多的单词。开发人员可能会发明比普通数组更复杂的数据结构，但他们都会遇到其中一个或两个问题。运行程序的计算机可能有千兆字节的可用内存，但程序无法利用它。\n\n幸运的是，C++ 为这个问题提供了一个名为**动态变量**的解决方案。\n\n# 动态变量\n\n全局变量在程序启动时分配的单个内存块中首尾相连。因此，声明一个全局变量没有运行时成本，但是所有的全局变量在程序的整个生命周期中都继续占用存储空间，即使它们没有被使用。\n\n由`{`和`}`界定的函数或其他块范围的局部变量在局部变量堆栈的顶部首尾相连。为局部变量分配内存的成本可以忽略不计。当执行离开该块时，该块中局部变量的存储从堆栈顶部弹出。下次执行进入块范围时，可以有效地重用该存储。\n\n动态变量由可执行语句构造，而不是像其他类型的变量一样声明。每个动态变量的存储空间是从称为堆的内存区域中单独分配的。当执行退出由`{`和`}`限定的块范围或程序结束时，动态变量不会自动销毁。相反，每个动态变量都被另一个可执行语句显式删除，其存储被单独返回到堆中。\n\n堆是未使用的内存块的集合。当程序请求新的动态变量时，C++ 运行时系统在堆中搜索适当大小的内存块。C++ 运行时系统可以从堆中返回一个可用的块，可以将一个较大的内存块分成两部分并返回其中的一部分，或者可以向操作系统请求一个新的内存块。当程序删除一个动态变量时，该动态变量的存储被返回到堆的可用内存块集合中，以便该存储可以被另一个动态变量重用。\n\n对于可以创建的动态变量的数量或大小没有固定的限制。然而，这并不意味着程序可以创建无限数量的动态变量。这只是意味着计算机、操作系统和先前请求的模式都对特定请求能否得到满足有所贡献。\n\n当无法满足创建动态变量的请求时，C++ 会引发异常。本书*第 13 章【C++ 中的异常处理】*中介绍了异常。\n\n动态变量的力量不是免费的。创建和删除动态变量有很大的运行时成本。事实上，创建和删除动态变量是 C++ 内置的最昂贵的操作。这是因为需要在可用内存块堆中扫描适当大小的块。\n\n使用`new`表达式创建动态变量。`new`-表达式将一个类型作为其操作数，并返回一个指向命名类型实例的指针。动态变量是通过这个指针知道的，而不是像全局变量和局部变量这样的名字。`new`-表达式不只是返回一些随机字节的存储；它将变量构造到返回的存储中，根据类型初始化它或调用它的构造函数。\n\n以下是使用`new`表达式创建动态变量的一些示例:\n\n```cpp\nchar *p1 = new char;\nint *p2 = new int{12345};\nsomeclass *p3 = new someclass(\"testing\", 123);\n```\n\n这里，`p1`被分配了一个指向足以保存`char`的存储器的指针。因为没有指定初始值，`char`没有被初始化为任何值，但是包含当它被分配给新的动态变量时在存储器中的随机位。`p2`被分配一个足以容纳`int`的存储指针。`int`初始化为`12345`。`p3`被分配一个指向足以保存类实例的存储的指针`someclass`。通过调用构造函数`someclass::someclass(char const*, int)`来构造实例。创建动态`char`或`int`变量不是很有用，在程序中也很少见到。然而，程序经常创建动态的`class`或`struct`实例。\n\n使用`delete`表达式删除动态变量。当动态变量被删除时，C++ 运行时系统调用其**析构函数成员函数**，如果有的话，其存储由 C++ 运行时系统返回堆。一个`delete`-表达式获取一个指向由一个`new`表达式创建的对象的指针，并返回`void`。\n\n上面创建的三个动态变量被以下三行代码删除:\n\n```cpp\ndelete p1;\ndelete p2;\ndelete p3;\n```\n\n虽然删除指针会破坏指向的对象并将它所占用的存储空间返回给 C++ 运行时系统，但它不会改变指针的值。指针仍然包含内存地址；只是现在，这个地址不是动态变量的地址。如果程序试图访问这个无效地址，程序很可能会崩溃，但可能不会马上崩溃。\n\n每个用`new`-表达式创建的动态变量必须被匹配的`delete`-表达式删除，否则程序将无法访问该变量占用的存储空间；内存将从程序中泄漏*。如果内存泄漏的程序长时间运行，可能会耗尽计算机上的所有内存，导致程序、其他程序或操作系统变得不稳定并崩溃。*\n\n *接下来的四个练习涵盖了创建和删除动态变量和数组的基础知识。\n\n## 练习 37:创建和删除基本类型的动态变量\n\n第一个练习包括一个简短的程序来创建和销毁一些动态变量。它检查指向这些变量的指针并检查变量的值，只是为了证明`new`和`delete`的行为符合预期。\n\n注意\n\n练习的完整代码可以在[https://packt.live/349pGjw](https://packt.live/349pGjw)找到。\n\n以下是执行本练习的步骤:\n\n1.  进入骨架`main()`功能，如下:\n\n    ```cpp\n    #include <iostream>\n    using namespace std;\n    int main()\n    {\n        return 0;\n    }\n    ```\n\n2.  在`main()`的左花括号后，输入以下代码创建一个动态`int`变量。声明一个指向名为`pint`的`int`的指针，并将其初始化为`nullptr`。然后，给`pint`分配一个新的`int`。`new`-表达式从堆中检索足以容纳`int`的存储，并将指向该存储的指针分配到`pint` :\n\n    ```cpp\n        int* pint = nullptr;\n        pint = new int;\n    ```\n\n    中\n3.  输出`pint`表示有内存地址，不再是`nullptr` :\n\n    ```cpp\n        cout << \"pint = \" << pint << endl;\n    ```\n\n4.  删除`pint`。这会将动态`int`变量占用的存储空间返回到堆:\n\n    ```cpp\n        delete pint;\n    ```\n\n5.  Finally, output `pint` again to demonstrate that it still holds a pointer to the invalid memory location that was formerly a dynamic `int` variable:\n\n    ```cpp\n        cout << \"pint = \" << pint << endl;\n    ```\n\n    注意\n\n    因为动态变量没有初始化，所以它的值是随机的。我们没有让程序打印它的值，因为一些操作系统将新的和删除的存储设置为零，以帮助调试。假设不必为动态变量设置初始值，依赖这种行为是一个可怕的想法。总有一天，你会用一个不同行为的编译器，你的程序会神秘地出现故障。\n\n    到目前为止，该程序如下所示:\n\n    ```cpp\n    #include <iostream>\n    using namespace std;\n    int main()\n    {\n        int* pint = nullptr;\n        pint = new int;\n        cout << \"pint = \" << pint << endl;\n        delete pint;\n        cout << \"pint = \" << pint << endl;\n        return 0;\n    }\n    ```\n\n6.  Compile and run the program and observe the result, which looks something like this:\n\n    ![Figure 6.1: Output of the program in exercise 37 ](img/C14195_06_01.jpg)\n\n    图 6.1:练习 37 中程序的输出\n\n    十六进制数字是机器地址。您的程序可能会报告不同的十六进制数，但这两个数字是相同的。\n\n    给`pint`分配新的`int`后，`pint`包含一个内存地址。这是动态`int`变量的地址。删除`pint`后，仍包含相同的内存地址，但该地址不再有效。这意味着它不再指向动态变量。在删除指针所指向的变量后使用指针是 C++ 程序中常见的错误原因。\n\n7.  按照刚才添加的代码，创建一个新的动态`int`变量，并将其分配给`pint`。`pint`可以重用，因为它没有指向任何有效的东西。请注意，`new`表达式在`int`后面有一个初始值设定项，它将动态`int`变量设置为`33333`。输出动态`int`变量的值，只是为了证明它是按预期初始化的。然后，删除动态`int`变量。代码如下:\n\n    ```cpp\n        pint = new int{33333};\n        cout << \"*pint = \" << *pint << endl;\n        delete pint;\n    ```\n\n8.  完整的程序是这样的。\n\n    ```cpp\n    #include <iostream>\n    using namespace std;\n    int main() \n    {\n        int * pint = nullptr;\n        pint = new int;\n        cout << \"pint = \" << pint << endl;\n        delete pint;\n        cout << \"pint = \" << pint << endl;\n        pint = new int {33333};\n        cout << \"*pint = \" << * pint << endl;\n        delete pint;\n        return 0;\n    }\n    ```\n\n9.  编译并运行完成的程序。该程序具有以下输出:\n\n![Figure 6.2: Output of the revised program of exercise 37 ](img/C14195_06_02.jpg)\n\n图 6.2:练习 37 的修订程序的输出\n\n`pint`指向的动态变量已按预期初始化。\n\n虽然创建基本数据类型(如`int`或`char`)的动态实例相对不常见，但创建动态`class`或`struct`实例却很常见。类实例构成了链接数据结构的基本构件，如列表、树和图形。\n\n## 练习 38:创建和删除动态类实例\n\n本练习演示创建动态类实例的基础。动态类实例，如`int`或`char`变量，是用`new`表达式创建的。唯一的区别是类实例是用构造函数参数列表初始化的。\n\n注意\n\n练习的完整代码可以在[https://packt.live/35kwCKR](https://packt.live/35kwCKR)找到。\n\n以下是执行本练习的步骤:\n\n1.  在`main()`功能的框架中键入。看起来是这样的:\n\n    ```cpp\n    #include <iostream>\n    using namespace std;\n    int main()\n    {\n        return 0;\n    }\n    ```\n\n2.  Following `using namespace std;`, type in a definition of class `noisy`. Now, `noisy` is useful for illustrating the behavior of dynamic variables. Its constructor function, which runs when an instance of `noisy` is created, prints the message `constructing noisy X`, where `X` is the value of the constructor argument. The constructor uses a constructor initializer instead of a simple assignment in the body of the constructor. A constructor initializer list can set the value of any member variable, and may use constructor arguments or expressions containing constructor arguments.\n\n    当`noisy`的实例被删除时运行的析构函数打印消息`destroying noisy X`。\n\n    由于`noisy`是类而不是`struct`，成员默认是私有的，所以需要`public:`访问控制声明。以下是`noisy`的定义:\n\n    ```cpp\n    class noisy\n    {\n        int i;\n    public:\n        noisy(int i) : i_(i)\n        { \n            cout << \"constructing noisy \" << i << endl; \n        }\n       ~noisy() \n        { \n            cout << \"destroying noisy \" << i_ << endl;\n        }\n    };\n    ```\n\n3.  在`main()`的左花括号之后，声明一个名为`N`的`noisy`实例。传递 1 给`N`的构造者。当我们运行该程序时，N 将打印一条消息来证明本地类实例在作用域退出时被自动销毁:\n\n    ```cpp\n        noisy N(1);\n    ```\n\n4.  声明一个指向名为`p`的`noisy`的指针，并将`p`初始化为`noisy`的新实例，初始化为`noisy(2)`。然后，删除`p`。这表明必须使用`delete`表达式销毁动态类实例。代码如下:\n\n    ```cpp\n        noisy* p = new noisy(2);\n        delete p;\n    ```\n\n5.  完整的程序是这样的。\n\n    ```cpp\n    #include <iostream>\n    using namespace std;\n    class noisy\n    {\n        int i_;\n    public:\n        noisy(int i) : i_(i)\n        { \n            cout << \"constructing noisy \" << i << endl; \n        }\n       ~noisy() \n        { \n            cout << \"destroying noisy \" << i_ << endl;\n        }\n    };\n    int main()\n    {\n        noisy N(1);\n        noisy* p = new noisy(2);\n        delete p;\n        return 0;\n    }\n    ```\n\n6.  编译并运行程序。输出如下:\n\n![Figure 6.3: Output of the program for exercise 38 ](img/C14195_06_03.jpg)\n\n图 6.3:练习 38 的程序输出\n\n这个节目是怎么回事？嗯，当执行进入`main()`的时候，就构造了一个`noisy`的实例。调用`noisy`构造函数，打印第一条消息`constructing noisy 1`。\n\n下一条语句创建一个动态`noisy`实例。构建`noisy`实例，打印第二条消息`constructing noisy 2`。下一条语句删除`p`，导致`noisy 2`的析构函数打印一条消息。执行离开`main()`的范围，导致局部变量`N`超出范围，触发对`noisy 1`析构函数的调用。这是您对具有函数作用域的变量的期望。\n\n## 动态数组\n\n**可以创建基本类型或类或结构实例的动态数组**。它们遵循与动态变量相同的规则。\n\n动态数组在运行时使用`new[]`表达式创建。像动态变量一样，当执行退出作用域或程序结束时，动态数组不会被破坏。它们必须通过`delete[]`表达式明确删除。像其他动态变量一样，动态数组不是通过名称知道的，而是通过指向动态数组的指针知道的。\n\n创建新的动态数组时，可以在运行时通过表达式指定动态数组的大小。大小不必像数组声明中的大小一样是常数。如果动态数组有两个或更多维度，则在运行时只能指定最左边维度的大小。\n\n## 练习 39:创建和删除基本类型的动态数组\n\n这个简短的练习创建并删除一个`char`的动态数组，并用一个空终止的文字字符串填充它。这是 C 编程中非常常见的一个习惯用法。在 C++ 中，有一个更复杂的名为`std::string`的字符串容器类，它有许多用于插入和提取子字符串的有用函数。本书第 12 章*容器和迭代器*中还有更多关于`std::string`的内容。\n\n注意\n\n练习的完整代码可以在[https://packt.live/35nB0ZO](https://packt.live/35nB0ZO)找到。\n\n以下是执行本练习的步骤:\n\n在 C++ 编译器中输入骨架`main()`函数。代码如下所示:\n\n```cpp\n#include <iostream>\nusing namespace std;\nint main()\n{\n    return 0;\n}\n```\n\n1.  这个程序将使用标准库函数来处理空终止的字符串，所以它必须包含`<cstring>`头。在`#include <iostream>`预处理器指令后添加以下一行:\n\n    ```cpp\n    #include <cstring>\n    ```\n\n2.  在函数`main()`中，声明一个名为`cp`的`char const`指针，并将其初始化为任何以空终止的字符串。接下来，声明一个名为`buffer`的`char`指针。创建一个新的动态`char`数组，足以容纳`cp`指向的空终止字符串。长度可以通过从标准库中调用函数`strlen()`来确定，该函数计算字符串中的字符数。还必须为空终止标记`\\0`预留空间，该标记不包括在`strlen()` :\n\n    ```cpp\n        char const* cp = \"arbitrary null terminated text string\";\n        char* buffer = new char[ strlen(cp)+1 ];\n    ```\n\n    返回的计数中\n3.  Copy the string pointed to by `cp` into `buffer`, using the standard library `strcpy()` function. `strcpy()` copies the characters from a null-terminated string source into the destination array until it copies the null-termination at the end of the source string:\n\n    ```cpp\n        strcpy(buffer, cp);\n    ```\n\n    一些编译器在程序使用`strcpy()`时会发出警告，因为它不做任何事情来确保目标数组中有足够的空间来保存源字符串。在这种情况下，到目前为止编写的代码计算目标数组缓冲区的大小，因此没有风险。\n\n4.  输出`buffer`的内容证明复制成功:\n\n    ```cpp\n        cout << \"buffer = \" << buffer << endl;\n    ```\n\n5.  Delete `buffer` using a `delete[]`-expression. The resulting code looks like this:\n\n    ```cpp\n        delete[] buffer;\n    ```\n\n    完整的程序如下所示:\n\n    ```cpp\n    #include <iostream>\n    #include <cstring>\n    using namespace std;\n    int main()\n    {\n        char const* cp = \"arbitrary null terminated text string\";\n        char* buffer = new char[ strlen(cp)+1 ];\n        strcpy(buffer, cp);\n        cout << \"buffer = \" << buffer << endl;\n        delete[] buffer;\n        return 0;\n    }\n    ```\n\n6.  编译并运行程序。它的输出是`buffer`的副本，如下所示:\n\n![Figure 6.4: Output of the program in exercise 39 ](img/C14195_06_04.jpg)\n\n图 6.4:练习 39 中程序的输出\n\n除了`new[]` -和`delete[]`-表达式的语法略有不同之外，创建和删除动态数组遵循与基本类型的动态变量相同的规则。\n\n## 练习 40:创建和删除类的动态数组\n\n也可以创建和删除类实例的动态数组。关于类实例的动态数组，有趣的是，数组中的每个实例都是构造的；也就是说，调用它的构造函数成员函数。销毁类实例的数组会调用每个实例的析构函数。\n\n注意\n\n练习的完整代码可以在[https://packt.live/35o4BlL](https://packt.live/35o4BlL)找到。\n\n以下是执行本练习的步骤:\n\n1.  在 C++ 编译器中输入骨架`main()`函数。代码如下:\n\n    ```cpp\n    #include <iostream>\n    using namespace std;\n    int main()\n    {\n        return 0;\n    }\n    ```\n\n2.  在`using namespace`声明后键入类`noisy`的定义。从*练习 38* 、*创建和删除动态类实例*、`noisy`中可以看到`noisy`实例的构造和销毁。这个版本的`noisy`被定义为一个结构而不是一个类，它的两个小成员函数被内联定义。这减少了这个熟悉的类占用的空间:\n\n    ```cpp\n    struct noisy\n    {\n        noisy() { cout << \"constructing noisy\" << endl; }\n       ~noisy() { cout << \"destroying noisy\" << endl; }\n    };\n    ```\n\n3.  在`main()`里面，输出一条消息`getting a noisy array`。声明一个指向名为`pnoisy`的`noisy`的指针，并为其分配一个由三个`noisy`实例组成的新动态数组:\n\n    ```cpp\n        cout << \"getting a noisy array\" << endl;\n        noisy* pnoisy = new noisy[3];\n    ```\n\n4.  输出信息`deleting noisy array`。然后，使用参数为`pnoisy`的`delete[]`表达式删除`noisy`数组。生成的代码如下所示:\n\n    ```cpp\n        cout << \"deleting noisy array\" << endl;\n        delete[] pnoisy;\n    ```\n\n5.  完整的程序是这样的。\n\n    ```cpp\n    #include <iostream>\n    using namespace std;\n    struct noisy\n    {\n        noisy() { cout << \"constructing noisy\" << endl; }\n       ~noisy() { cout << \"destroying noisy\" << endl; }\n    };\n    int main()\n    {\n        cout << \"getting a noisy array\" << endl;\n        noisy* pnoisy = new noisy[3];\n        cout << \"deleting noisy array\" << endl;\n        delete[] pnoisy;\n        return 0;\n    }\n    ```\n\n6.  编译并运行程序。结果输出如下:\n\n![Figure 6.5: Creating and deleting dynamic arrays of classes ](img/C14195_06_05.jpg)\n\n图 6.5:创建和删除类的动态数组\n\n需要注意的重要一点是，动态数组中的每个类实例都是构造的，而不是碰巧在内存中的随机位。删除阵列后，实例将被销毁。\n\n简单来说就是这样:如何创建新的动态变量和数组，以及如何删除它们。也许你听说过 C++ 指针和引用有多难。也许你现在想知道这些喧嚣是怎么回事。下一个主题讨论了程序员在使用动态变量时可能出错的一些方式。\n\n# 七种动态可变捷联惯导系统\n\n接下来的七个练习说明了滥用动态变量会破坏程序的七种方式，要么是将其发送到堆损坏的混乱中，要么是调用操作系统陷阱的突然霹雳。\n\n以下几个练习是为了打印错误信息并终止程序而设计的。产生的特定消息既取决于 C++ 运行时系统版本，也取决于运行程序的操作系统。不能保证您会看到相同的错误消息，因此每个示例都包含对所发生情况的描述。\n\n## 练习 41:在创建动态变量之前使用它\n\n第一个致命的动态变量 sin 是在创建动态变量之前使用指向动态变量的指针。显而易见，取消指向无效存储的指针将导致未定义的行为，这可能包括崩溃，或者仅仅产生错误的结果。\n\n注意\n\n练习的完整代码可以在[https://packt.live/2XAak52](https://packt.live/2XAak52)找到。\n\n以下是执行本练习的步骤:\n\n1.  进入以下简写骨架`main()`功能:\n\n    ```cpp\n    int main()\n    {\n        return 0;\n    }\n    ```\n\n2.  在`main()`内部，创建一个名为`p` :\n\n    ```cpp\n        char* p = nullptr;\n    ```\n\n    的字符指针\n3.  Set `p[10]` to `'!'`.This symbol is known to typesetters as \"bang”:\n\n    ```cpp\n        p[10] = '!';\n    ```\n\n    完整的程序如下所示:\n\n    ```cpp\n    #include<iostream>\n    using namespace std;\n    int main() \n    {\n        char * p = nullptr;\n        p[10] = '!';\n        return 0;\n    }\n    ```\n\n4.  Compile and run the program. If you are using an online compiler, use a compiler that displays the output from the operating system, such as Coliru; cpp.sh does not. The output of the program is an error message (depending on the operating system):\n\n    ![Figure 6.6: Error message resulting from running the program in exercise  41 ](img/C14195_06_06.jpg)\n\n图 6.6:在练习 41 中运行程序产生的错误消息\n\n在这个简短的例子中，错误是显而易见的；没有动态变量被分配给`p`。在更大的程序中，指针可能设置在一个地方，在另一个地方使用，在第三个地方删除。在错误发生的地方可能不太清楚是什么代码路径导致使用变量而不创建它。嵌入式操作系统可能无法捕捉到对无效地址的写入，而是可能覆盖关键的系统信息，使嵌入式程序不稳定，而不是立即崩溃。\n\n相对信息丰富的`segmentation fault`错误消息的出现是因为这个特定的指针被初始化为`nullptr`。操作系统捕获了对未映射内存的访问。如果指针有一些其他无效地址，它可能会覆盖变量或损坏堆的空闲块列表，从而在程序中远离问题来源的某个点产生不同的错误消息。这只是为什么初始化指向`nullptr`的指针是个好主意的一个原因。\n\n## 练习 42:删除动态变量后使用它\n\n第二个致命的动态变量 sin 是删除一个动态变量，然后继续使用指针，就好像它仍然引用那个动态变量一样。\n\n注意\n\n练习的完整代码可以在[https://packt.live/338B65K](https://packt.live/338B65K)找到。\n\n以下是执行本练习的步骤:\n\n1.  进入`main()`功能的骨架:\n\n    ```cpp\n    #include <iostream>\n    using namespace std;\n    int main()\n    {\n        return 0;\n    }\n    ```\n\n2.  在`main()`内部，创建一个名为`p`的`char`指针，该指针被初始化为`new[]`表达式的结果，创建一个由 10 个`char`组成的数组:\n\n    ```cpp\n        char* p = new char[10];\n    ```\n\n3.  将`p[0]`设置为`'!'` :\n\n    ```cpp\n        p[0] = '!';\n    ```\n\n4.  使用`delete[]`表达式删除`p`，因为`p`指向一个数组:\n\n    ```cpp\n        delete[] p;\n    ```\n\n5.  打印`p[0]`的值。请记住，`p`在这一点上没有指向任何有效的东西:\n\n    ```cpp\n        cout << \"p[0] = \" << p[0] << endl;\n    ```\n\n6.  完整的程序是这样的。\n\n    ```cpp\n    #include <iostream>\n    using namespace std;\n    int main() \n    {\n        char * p = new char[10];\n        p[0] = '!';\n        delete[] p;\n        cout << \"p[0] = \" << p[0] << endl;\n        return 0;\n    }\n    ```\n\n7.  Compile and run the program. Its output on one particular compiler and operating system is as follows:\n\n    ![Figure 6.7: Output of the program in exercise 42 ](img/C14195_06_07.jpg)\n\n图 6.7:练习 42 中程序的输出\n\n您可能希望`p[0]`的值为`'!'`，因为您可以看到设置为`'!'`的代码行。然而，当程序删除`p`时，它向 C++ 运行时系统发出信号，程序是使用`p`指向的存储完成的。之后，C++ 用它做了一些其他的事情。C++ 运行时系统可能会将指向空闲内存块列表中下一个项目的指针放入`p`的开头。然而，它可以做任何事情。程序打印字符`'p'`的事实纯属巧合。肯定没有打印`'!'`。\n\n一个特殊的地狱圈是为犯了这种罪的程序保留的，因为程序不会立即崩溃。该错误的症状包括变量意外改变值，或者后续的`new`表达式或`delete`表达式崩溃，该表达式可能出现在远离发生错误的程序行的一行上。\n\n## 练习 43:不删除动态变量\n\n下一个致命的动态变量 sin 是创建一个动态变量，然后忘记删除它。\n\n注意\n\n练习的完整代码可以在[https://packt.live/2CZPSRU](https://packt.live/2CZPSRU)找到。\n\n以下是执行本练习的步骤:\n\n1.  进入骨架`main()`功能:\n\n    ```cpp\n    #include <iostream>\n    using namespace std;\n    int main()\n    {\n        return 0;\n    }\n    ```\n\n2.  在`main()`中，创建一个名为`p`的字符指针，初始化为一个`new[]`表达式的结果，创建一个由 10 个字符组成的数组:\n\n    ```cpp\n        char* p = new char[10];\n    ```\n\n3.  将`p[0]`设置为`'!'` :\n\n    ```cpp\n        p[0] = '!';\n    ```\n\n4.  打印`p[0]`的值:\n\n    ```cpp\n        cout << \"p[0] = \" << p[0] << endl;\n    ```\n\n5.  完整的程序是这样的。\n\n    ```cpp\n    #include <iostream>\n    using namespace std;\n    int main() \n    {\n        char * p = new char[10];\n        p[0] = '!';\n        cout << \"p[0] = \" << p[0] << endl;\n        return 0;\n    }\n    ```\n\n6.  Compile and run the program. The output is as you expected. It looks like this:\n\n    ![Figure 6.8: Output of the program in exercise 43 ](img/C14195_06_08.jpg)\n\n图 6.8:练习 43 中程序的输出\n\n也没有错误消息。那么，为什么这是一种致命的罪恶呢？问题是，用`new`表达式或`new[]`表达式创建的每个动态变量都必须被匹配的`delete`表达式或`delete[]`表达式删除。否则，程序将无法访问变量占用的存储空间；也就是说，内存会从程序中*泄漏*。\n\n如果一个内存泄漏的程序长时间运行，那么它会把运行它的整个计算机送到内存耗尽的地狱，导致运行的程序、其他程序或者操作系统本身变得不稳定和崩溃。一些操作系统在程序终止时回收泄漏的内存。其他人没有。依赖操作系统来回收未删除的动态变量是老 Unix 程序员的一个坏习惯，建议您避免。\n\n## 练习 44:覆盖指向动态变量的指针\n\n如果覆盖了指向动态变量的有效指针，可能会破坏对该变量的最后一次引用，导致其泄漏。这是一种致命的动态变量罪。\n\n注意\n\n练习的完整代码可以在[https://packt.live/2KD5DSU](https://packt.live/2KD5DSU)找到。\n\n以下是执行本练习的步骤:\n\n1.  重新键入来自*练习 38:创建和删除动态类实例*的代码。代码如下:\n\n    ```cpp\n    #include <iostream>\n    using namespace std;\n    class noisy \n    {\n        int i_;\n    public:\n        noisy(int i): i_(i) \n        {\n            cout << \"constructing noisy \" << i << endl;\n        }\n       ~noisy() \n        {\n            cout << \"destroying noisy \" << i_ << endl;\n        }\n    };\n    int main() \n    {\n        noisy N(1);\n        noisy * p = new noisy(2);\n        p = new noisy(3);\n        delete p;\n        return 0;\n    }\n    ```\n\n2.  就在删除`p`之前，添加一条语句，将初始化为`noisy(3)`的`noisy`的另一个新实例分配给`p`。代码如下:\n\n    ```cpp\n        p = new noisy(3);\n    ```\n\n3.  完整的程序如下:\n\n    ```cpp\n    #include <iostream>\n    using namespace std;\n    class noisy\n    {\n        int i_;\n    public:\n        noisy(int i) : i_(i)\n        {\n            cout << \"constructing noisy \" << i << endl; \n        }\n       ~noisy() \n        { \n            cout << \"destroying noisy \" << i_ << endl;\n        }\n    };\n    int main()\n    {\n        noisy N(1);\n        noisy* p = new noisy(2);\n        p = new noisy(3);\n        delete p;\n        return 0;\n    }\n    ```\n\n4.  编译并运行完成的程序。该程序的输出如下:\n\n![Figure 6.9: Output of the program in exericse 44 ](img/C14195_06_09.jpg)\n\n图 6.9:练习 44 中程序的输出\n\n当执行进入`main()`时，会创建一个`noisy`的实例。调用`noisy`构造函数，打印第一条消息`constructing noisy 1`。\n\n下一条语句创建第一个动态噪声实例，导致打印第二条消息`constructing noisy 2`。下一条语句构造另一个`noisy`实例`noisy 3`，并将指向该实例的指针分配给`p`，替换指向`noisy 2`的指针。\n\n下一条语句删除`p`，导致`noisy 3`的析构函数打印一条消息。然后`main()`返回，导致局部变量`N`超出范围，触发对`noisy 1`析构函数的调用。\n\n`noisy 2`怎么了？一个动态变量必须被删除，但是程序中没有剩下指向`noisy 2`的指针，因为它被指向`noisy 3`的指针覆盖了，所以`noisy 2`不能被删除。然而，`noisy 2`并不只是不复存在。操作系统不知道`noisy 2`的存储在哪里。操作系统把`noisy 2`的存储管理交给了程序，但是程序忘记了`noisy 2`在哪里；`noisy 2`已经*泄露*出程序。\n\n之后会发生什么取决于操作系统。Linux 让你轻松，释放程序使用的所有内存。然而，在嵌入式系统和视窗系统上，泄漏的内存可能会消失，直到下次计算机重新启动。每次程序运行时，`noisy 2`的另一个实例变得不可访问。如果这种情况发生足够多次，操作系统将没有足够的内存来运行程序。它将变得不稳定，并导致操作系统内核崩溃。\n\n## 练习 45:删除动态变量两次\n\n下一个致命的动态变量 sin 是多次删除一个动态变量——也就是说，在多个`delete`表达式中使用同一个指针。\n\n当一个程序删除一个动态变量时，该动态变量的存储会回到堆的可用存储块列表中。然而，指针没有改变；它仍然指向过去动态变量的开始。如果程序再次删除同一个指针，C++ 运行时系统会尝试调用已经被破坏的前一个动态变量的析构函数，这可能会导致程序崩溃。\n\n然后，C++ 试图将已经在可用存储列表上的前一个动态变量的存储再次放到可用存储块列表上，这很可能会破坏可用存储块列表。\n\n注意\n\n本练习的完整代码可以在[https://packt.live/330kHjW](https://packt.live/330kHjW)找到。\n\n1.  进入简化骨架`main()`程序:\n\n    ```cpp\n    #include <iostream>\n    using namespace std;\n    int main()\n    {\n        return 0;\n    }\n    ```\n\n2.  在`main()`内部，声明一个新的指向 char 的指针，名为`p`。将其初始化为新的 10 `char`动态数组:\n\n    ```cpp\n        char* p = new char[10];\n    ```\n\n3.  Add two lines that each say `delete[] p;`:\n\n    ```cpp\n        delete[] p;\n        delete[] p;\n    ```\n\n    完整的程序如下:\n\n    ```cpp\n    #include <iostream>\n    using namespace std;\n    int main() \n    {\n        char * p = new char[10];\n        delete[] p;\n        delete[] p;\n        return 0;\n    }\n    ```\n\n4.  编译并运行程序。如果您使用的是在线编译器，请使用显示操作系统输出的编译器，如 coliru。输出可能包含一条错误消息，在 Linux 上看起来像这样:\n\n![Figure 6.10: Error output of the program in exercise 45 ](img/C14195_06_10.jpg)\n\n图 6.10:练习 45 中程序的错误输出\n\n这条信息还会持续很多行。并非所有的 C++ 编译器都会为此程序生成运行时错误消息；这取决于编译器和编译选项。该编译器的运行时系统检查是否删除了已删除的动态变量，并打印一条警告。不能保证 C++ 运行时系统会打印此错误消息，尤其是当您命令编译器执行优化时。相反，当程序试图执行一个`new`表达式或`delete`表达式时，操作系统可能会在未来的某个时候将程序抛入火坑。\n\n## 练习 46:用 delete 而不是 delete[]删除动态数组\n\n使用`delete`删除动态数组是下一个致命的动态变量 sin。\n\n注意\n\n本练习的完整代码可以在[https://packt.live/2OAtSlO](https://packt.live/2OAtSlO)找到。\n\n1.  进入骨架`main()`功能和结构`noisy`的定义。看起来是这样的:\n\n    ```cpp\n    #include <iostream>\n    using namespace std;\n    struct noisy\n    {\n        noisy() { cout << \"constructing noisy\" << endl; }\n       ~noisy() { cout << \"destroying noisy\" << endl; }\n    };\n    int main()\n    {\n        return 0;\n    }\n    ```\n\n2.  在`main()`内部，声明一个名为`p`的`noisy`指针，使用`new[]`表达式\n\n    ```cpp\n        noisy* p = new noisy[3];\n    ```\n\n    将其初始化为三个`noisy`实例的动态数组\n3.  用`delete`表达式代替`delete[]`表达式删除`p`:\n\n    ```cpp\n        delete p;\n    ```\n\n4.  完整的程序是这样的。\n\n    ```cpp\n    #include <iostream>\n    using namespace std;\n    struct noisy\n    {\n        noisy() { cout << \"constructing noisy\" << endl; }\n       ~noisy() { cout << \"destroying noisy\" << endl; }\n    };\n    int main()\n    {\n        noisy* p = new noisy[3];\n        delete p;\n        return 0;\n    }\n    ```\n\n5.  编译并运行程序。如果您使用的是在线编译器，请使用显示操作系统输出的编译器，如 coliru。以下是操作系统的代表性输出:\n\n![Figure 6.11: Error message when deleting a dynamic array with delete instead of delete[] ](img/C14195_06_11.jpg)\n\n图 6.11:用 delete 而不是 delete[]删除动态数组时的错误消息\n\n从输出来看，除了崩溃报告之外，这个问题还有一个很好的征兆。构建了三个`noisy`实例，但只销毁了一个`noisy`实例，而不是三个。\n\n在这个特定的编译器和操作系统上，C++ 运行时系统检测到一个问题，打印一条消息，然后终止程序。不能保证另一个 C++ 运行时系统会检测到问题。有保证`noisy`的另外两个实例没有被破坏，所以它们泄露了它们包含的任何动态变量。至少`noisy`阵列的部分存储没有返回到空闲存储列表中，所以也泄漏了。还有一种可能是，堆的空闲内存块列表已经损坏，因此，最终，程序将变得不稳定。\n\n## 练习 47:用 delete[]而不是 delete 删除动态变量\n\n这最后一个致命的动态变量 sin 与前一个相反:用`delete[]`删除一个非数组动态变量，用于删除数组。\n\n注意\n\n本练习的完整代码可以在[https://packt.live/2qwT8l3](https://packt.live/2qwT8l3)找到。\n\n1.  进入骨架`main()`功能和结构`noisy`的定义。看起来是这样的:\n\n    ```cpp\n    #include <iostream>\n    using namespace std;\n    struct noisy\n    {\n        noisy() { cout << \"constructing noisy\" << endl; }\n       ~noisy() { cout << \"destroying noisy\" << endl; }\n    };\n    int main()\n    {\n        return 0;\n    }\n    ```\n\n2.  在`main()`内部，声明一个名为`p`的`noisy`指针，使用`new`表达式将其初始化为一个新的动态`noisy`实例:\n\n    ```cpp\n    noisy* p = new noisy;\n    ```\n\n3.  用`delete[]`表达式代替`delete`表达式删除`p`:\n\n    ```cpp\n    delete[] p;\n    ```\n\n4.  完整的程序是这样的。\n\n    ```cpp\n    #include <iostream>\n    using namespace std;\n    struct noisy\n    {\n        noisy() { cout << \"constructing noisy\" << endl; }\n       ~noisy() { cout << \"destroying noisy\" << endl; }\n    };\n    int main()\n    {\n        noisy* p = new noisy;\n        delete[] p;\n        return 0;\n    }\n    ```\n\n5.  编译并运行程序。Linux 操作系统输出示例如下所示:\n\n![Figure 6.12: Error message when deleting a dynamic variable with delete instead of delete[] ](img/C14195_06_12.jpg)\n\n图 6.12:用 delete[]而不是 delete 删除动态变量时的错误消息\n\n这里发生了什么？构建了一个嘈杂的实例，但是删除了无数个实例。\n\n一个`new[]`-表达式保存它分配的数组的大小，这样它可以为数组中的每个实例调用一个析构函数。a`new`-表达式不保存该值。`delete[]`-表达式查找实例的秘密计数并读取垃圾。除非垃圾恰好等于 1，这是非常不可能的，`delete[]`会尝试删除不存在的实例。然后，程序将下降到混乱的地狱循环，称为**未定义的行为**。\n\nC++ 提供了非常强大、非常高效的工具。然而，效率是以警惕为代价的。C++ 不会检查程序员做的每一件事，以确保他们做得对。忘记做正确的事情，C++ 将为你的程序面临可怕的命运。C++ 会让开发者做大事。然而，它不会溺爱他们。如果这在之前不明显的话，研究动态变量有望让这一点变得清晰。正如第一章已经提到的，如果 C++ 有哲学的话，那就是*权力越大，责任越大*。\n\n管理动态变量已经够难的了，现代 C++ 定义了一种叫做*智能指针*的东西；也就是说，当智能指针超出范围时，自动删除其动态变量的类。智能指针包含在*第 7 章，动态变量的所有权和寿命*中。智能指针是处理动态变量的唯一好方法。在我们找到智能指针之前，你必须用传统的方式处理动态变量。\n\n# 动态容器\n\n一个**容器**是一个由同一数据类型的多个实例组成的数据结构。例如，C++ 数组是一种简单的容器。数组的类型表示它包含哪种数据。数组具有在编译时指定的固定大小。动态数组是具有固定类型和任意大小的容器，但是在创建容器时大小是固定的。\n\n## 链接列表\n\n使用动态类实例，每个实例包含一个指针，程序可以创建一个容器，该容器可以增长到一个不预先确定的大小。容器中的每个条目都是一个类(或结构)实例。该类有一个有效负载(一个名为`value_`的成员，在下面的例子中是`int`)和一个指针成员(在下面的例子中是名为`next_`，它引用容器中的下一个实例。类定义如下所示:\n\n```cpp\nstruct numeric_item\n{\n    int value_;\n    numeric_item* next_;\n};\n```\n\n这个类的动态创建的实例可以使用它们的`next_`成员链接在一起。下图中称为`head`的指针变量指向整个容器。链条末端的`next_`指针设置为`nullptr`:\n\n![Figure 6.13: Visualizing linked lists ](img/C14195_06_13.jpg)\n\n图 6.13:可视化链表\n\n这样的容器叫做**链表**。\n\n## 二分搜索法树\n\n使用动态类实例，每个包含两个指针，程序可以创建另一种容器，它可以增长到一个不预先确定的大小。容器中的每个条目都是一个类实例。该类有一个有效载荷(在下面的例子中称为`value_`的`int`成员)和两个指针成员(在这个例子中称为`left_`和`right_`)。类定义如下所示:\n\n```cpp\nstruct numeric_tree\n{\n    int value_;\n    numeric_tree* left_;\n    numeric_tree* right_;\n};\n```\n\n这个类的动态创建的实例可以使用它们的`left_`和`right_`成员链接在一起。下图中称为`root`的指针变量指向整个容器。不指向实例的`left_`和`right_`指针设置为`nullptr`。产生的数据结构类似于一棵倒着生长的树:\n\n![Figure 6.14: A binary search tree growing upside down ](img/C14195_06_14.jpg)\n\n图 6.14:一个颠倒生长的二叉查找树\n\n这样的容器叫做二叉树。\n\n## 递归数据结构\n\n链表和二叉树是递归数据结构的例子，也就是说，数据结构是根据它们自己定义的。例如，链接列表可以定义为`nullptr`或具有指向链接列表的单个链接的项目。二叉树是一种数据结构，要么是`nullptr`，要么是由一个带有两个链接的项目组成，称为**左子树**和**右子树**，它们指向二叉树。递归定义的数据结构很有趣，因为它可以由本身是递归的函数来操作，尽管这不能保证是有效的。\n\nA **二叉查找树**是一个二叉树，额外的属性是一个项目左边子树上所有项目的值都小于那个项目的值，右边子树上所有项目的值都大于那个项目的值。上图中的二叉树是二叉查找树树。二叉查找树的一个优点是，有一种算法可以按照升序或降序有效地访问二叉查找树的节点。\n\n## 访问递归数据结构中的项目\n\n链表的递归定义告诉我们如何编写一个函数来访问链表中的所有项目并打印每个项目。如果链表为空，除了`endl`没有什么可以打印的。否则，打印项目，然后递归打印`next_`指针作为链表。该函数如下所示:\n\n```cpp\nvoid print_recursive(numeric_item* p)\n{\n    if (p == nullptr)\n    {\n        cout << endl;\n    }\n    else\n    {\n        cout << p->value_ << \" \";\n        print_recursive(p->next_);\n    }\n}\n```\n\n递归打印链表的唯一问题是，如果链表中有 *n* 项，那么在递归函数开始返回之前，会有 *n* 个嵌套调用。如果`n = 10`这个没问题，但是如果 *n* 是 100 万确实是一个很大的问题。幸运的是，有一种非递归的方法可以打印链表。一个`while`循环打印当前列表项，而不是`nullptr`，然后打印循环后的`endl`。这类似于递归函数，用循环代替递归调用。迭代(即非递归)`print`函数如下所示:\n\n```cpp\nvoid print(numeric_item* p)\n{\n    while (p != nullptr)\n    {\n        cout << p->value_ << \" \";\n        p = p->next_;\n    }\n    cout << endl;\n}\n```\n\n为了递归地访问和打印二叉查找树的项目，如果树是空的，函数立即返回；否则，它递归地访问左边的子树，打印当前项目，并递归地访问右边的子树。这样一个`print`函数看起来是这样的:\n\n```cpp\nvoid print(numeric_tree* item)\n{\n    if (item == nullptr)\n    {\n        return;\n    }\n    print(item->left_);\n    cout << item->value_ << \" \";\n    print(item->right_);\n}\n```\n\n有一种迭代的方法来打印二叉树，但是它使用模拟函数调用堆栈的堆栈。与递归函数相比，它没有任何优势。如果项目在二叉查找树中插入的顺序是随机的，那么一个百万项目树将只有大约 20 级递归调用。这比一百万个嵌套调用更不可能引起问题。*图 6.2* 中树的插入顺序为`4, 2, 1, 3, 6, 5`。有些插入顺序会产生更深的树。例如，插入顺序`1, 2, 3, 4, 5, 6`将生成一棵树，它的所有左子树都是空的，并且具有最坏情况下的递归深度。\n\n## 寻找物品\n\n通过将项目与键值进行比较并返回找到的项目的指针，可以在列表或树中找到项目，如果没有找到项目，则返回`nullptr`。这个指针让开发人员可以访问找到的项的字段，但它不能访问指向该项的指针，这对于插入或移除项非常方便。一个稍微不同的函数使用并返回一个指向指针的指针来遍历指向每个项目的指针，这样如果需要，可以在找到的项目之前插入一个新项目。\n\n链表的迭代解决方案使用`while`循环。初始条件(在`while`循环之前)将`pp`设置为`head`的地址。如果`*pp`是`nullptr`(到达列表末尾)，或者如果`(*pp)->value_`等于`v`，循环终止。循环步骤表达式将`pp`设置为`(*pp)->next_`的地址:\n\n```cpp\n    numeric_item** pp = &head;\n    while((*pp) != nullptr && (*pp)->value_ != v)\n    {\n         pp = &((*pp)->next_);\n    }\n```\n\n最初，`pp`指向`head`的地址，这样`(*pp)`指向第一个列表项，或者`nullptr`。如果`(*pp)`为`nullptr`或者列表项的`value_`等于目标值`v`，则执行脱离循环。否则，指向指针`pp`的指针将步进指向列表项的`next_`指针的地址，该地址指向下一个列表项。循环结束后，要么`(*pp)`指向值为`v`的项目，要么`(*pp)`等于`nullptr`。`pp`指向在`v`之前应该插入新项目的指针；否则，如果没有找到`v`，则指向列表中的最后一个指针。\n\n使用相同指针对指针习惯用法的递归函数在二叉查找树中找到插入点。如果指针为`nullptr`，则`if`块的第一个分支结束递归。否则，`find()`使用二叉查找树属性，如果键小于当前节点的值，则向下递归左子树，否则向下递归右子树:\n\n```cpp\nnumeric_tree** find(int v, numeric_tree** pp)\n{\n    if (*pp == nullptr)\n    {\n       return pp;\n    }\n    else if (v < (*pp)->value_)\n    {\n       return find(v, &((*pp)->left_));\n    }\n    else\n    {\n        return find(v, &((*pp)->right_));\n    }\n}\n```\n\n## 添加项目\n\n要将项目添加到链接列表中，该列表在概念上分为位于插入项目之前的头部和位于插入项目之后的尾部。头部可能只有列表指针(即图中的变量`head`)那么短，也可能有整个列表那么长。插入的项目首先添加到列表尾部的前面，然后添加到列表头部的后端。\n\n列表头的后端由列表头中最后一个指针的地址表示——也就是一个指向指针的指针，在下面的代码中称为`pp`。列表头部的最后一个指针指向尾部。要插入的列表项由`newp`指向。\n\n指向尾部的指针从列表头部的最后一个指针复制到插入项的`next_`指针。这将插入的项目链接到列表的尾部。然后列表头部的最后一个指针被更新，通过指针指向指针，指向插入的项目。这会将插入的项目附加到列表的头部。如果`pp`是指向头部末端的指针的指针，`newp`是指向要插入的新项目的指针，如下所示:\n\n```cpp\n    newp->next_ = *pp;\n    *pp = newp->next_;\n```\n\n下图显示了如何向链接列表添加项目:\n\n![Figure 6.15: Adding items to a linked list ](img/C14195_06_15.jpg)\n\n图 6.15:向链表添加项目\n\n这个代码可以在任何位置添加一个项目，甚至在前面，因为`pp`可以设置为`head`的地址。上图显示了添加项目之前(虚线)和之后(实线)的列表。\n\n插入二叉查找树更容易。`find()`返回的值总是指向插入点，因此将指向新项目的指针分配给`find()`返回的取消引用指针会插入新项目。\n\n## 删除动态项目\n\n必须删除动态创建的项目，以避免内存泄漏。删除链表的一个常见习惯是从链表头删除每一项，然后删除该项。用户经常犯的一个错误就是删除动态变量`head`指向，然后说`head = head->next_;`。这个代码的问题在于删除`head`后，它不再指向任何有效的东西。根据您的编译器，这个不正确的习惯用法可能看起来有效，但可能会在将来某个时候意外失败。\n\n而是使用一个指针变量，在下面的代码中称为`p`，来临时保存`head`的值，将`head`设置为`head->next_`，然后删除`p`:\n\n```cpp\n    while (head != nullptr)\n    {\n        numeric_item* p = head;\n        head = head->next_;\n        delete p;\n    }\n```\n\n这些成语值得记忆。在用 C++ 编程时，您将多次使用它们。\n\n## 练习 48:创建类实例的链接列表\n\n在本练习中，您将创建一个用于存储一系列数字的容器。您将需要在列表中添加一个项目、在列表中查找一个项目以及打印一个列表的功能，因此本练习将比前面的一些练习稍长一些:\n\n注意\n\n本练习的完整代码可以在[https://packt.live/2QqiWJO](https://packt.live/2QqiWJO)找到。\n\n1.  开始练习，进入`main()`程序的框架:\n\n    ```cpp\n    #include <iostream>\n    using namespace std;\n    int main()\n    {\n         return 0;\n    }\n    ```\n\n2.  在`main()`上方，添加结构`numeric_item`的定义。`numeric_item`有一个名为`value_`的 int 成员和一个名为`next_` :\n\n    ```cpp\n    struct numeric_item\n    {\n        int value_;\n        numeric_item* next_;\n    };\n    ```\n\n    的指向`numeric_item`的指针\n3.  声明`numeric_item`列表指针`head`。将`head`初始化为`nullptr`，因为这是程序判断列表为空的方式:\n\n    ```cpp\n    numeric_item* head = nullptr;\n    ```\n\n4.  So far, the program looks like this; you can compile the program, but it doesn't do anything yet:\n\n    ```cpp\n    #include <iostream>\n    using namespace std;\n    struct numeric_item\n    {\n        int value_;\n        numeric_item* next_;\n    };\n    numeric_item* head = nullptr;\n    int main()\n    {\n         return 0;\n    }\n    ```\n\n    **遍历链表**\n\n    下一个函数遍历列表，打印列表中每个`numeric_item`结构的值:\n\n5.  在`head`声明之后，定义一个`void`函数`print()`。`print()`的骨架是这样的:\n\n    ```cpp\n    void print()\n    {\n    }\n    ```\n\n6.  `print()`包含一个`for`循环来打印每个列表条目。初始化子句声明了一个指向名为`p`的`numeric_item`的指针，该指针被初始化为`head`，这是指向列表第一项的指针。如果`p`为`nullptr`，则终止子句退出循环，并且步骤子句通过将`p->next`分配给`p`来将`p`前进到下一列表项。`for`语句看起来是这样的:\n\n    ```cpp\n        for (numeric_item* p = head; p != nullptr; p = p->next_)\n    ```\n\n7.  If the list is empty, `head` will be equal to `nullptr`, so `p` will initially be set equal to `nullptr` and the loop will terminate. Otherwise, the value of the list entry will be printed. When the function is done printing the item, `p` is set to `p->next_`—that is, the next list item. This repeats for each list item. The final list item's `next_` pointer is equal to `nullptr`. When `p->next_` is assigned to `p`, it becomes equal to `nullptr`, and the `for` loop terminates. The code looks like this:\n\n    ```cpp\n        for (numeric_item* p = head; p != nullptr; p = p->next_)\n        {\n             cout << p->value_ << \" \";\n        }\n    ```\n\n    这个练习展示了`for`循环不仅仅可以对整数值进行计数或者通过数组进行递增。你可以编译代码，但是运行它还不行。\n\n8.  输出的列表项带有一个分隔空格，没有`endl`，以便将线路冲洗到终端。在`for`循环结束时，输出`endl`，这样输出线变得可见:\n\n    ```cpp\n        cout << endl;\n    ```\n\n9.  The complete `print()` function looks like this:\n\n    ```cpp\n    void print()\n    {\n         for (numeric_item* p = head; p != nullptr; p = p->next_)\n         {\n            cout << p->value_ << \" \";\n         }\n         cout << endl;\n    }\n    ```\n\n    如果你愿意，你可以在这一点上编译程序，但它仍然不会做任何事情。\n\n    **将项目插入链表**\n\n    下一个函数在列表中的任何位置插入一个项目:\n\n10.  在`print()`下方增加`add()`功能。现在，`add()`取两个参数:一个`int`值，`v`，将初始化一个新的动态`numeric_item`；和一个指向名为`pp`的指针的指针，该指针将成为插入点。`add()`是一个`void`函数，因为它根本不返回任何东西。`add()`的骨架是这样的:\n\n    ```cpp\n    void add(int v, numeric_item** pp)\n    {\n    }\n    ```\n\n11.  `add()`定义一个名为`newp`的`numeric_item`指针，并将创建动态`numeric_item`实例的结果分配给`newp`。`newp`的`value_`成员设置为`v`参数:\n\n    ```cpp\n        numeric_item* newp = new numeric_item;\n        newp->value_ = v;\n    ```\n\n12.  新项目指向列表尾部，当前由`*pp`给出。现在，`*pp`更新为指向新物品，`newp` :\n\n    ```cpp\n        newp->next_ = *pp;\n        *pp = newp;\n    ```\n\n13.  完成的`add()`功能如下:\n\n    ```cpp\n    void add(int v, numeric_item** pp)\n    {\n        numeric_item* newp = new numeric_item;\n        newp->value_ = v;\n        newp->next_ = *pp;\n        *pp = newp;\n    }\n    ```\n\n14.  现在程序中有足够的机器来实际做一些事情。在`main()`中，编写一个`for`循环，向列表中添加一些`numeric_item`实例。使用`for`循环感应变量设置这些`numeric_item`实例的值。从 1 开始循环，并在每次迭代中添加 2，以创建包含值 1、3、5、7 和 9 的实例:\n\n    ```cpp\n        for (int i = 1; i < 10; i = i + 2)\n        {\n             insert_at(i, &head);\n        }\n    ```\n\n15.  After the `for` loop, call `print()` to output the list that was just created:\n\n    ```cpp\n        print();\n    ```\n\n    `main()`的内容是这样的:\n\n    ```cpp\n    int main()\n    {\n         for (int i = 1; i < 10; i = i + 2)\n         {\n            insert_at(i, &head);\n         }\n         print();\n         return 0;\n    }\n    ```\n\n16.  Compile and run the program. Its output should look like this:\n\n    ![Figure 6.16: Output of the first version of the program in exercise 48 ](img/C14195_06_16.jpg)\n\n    图 6.16:练习 48 中程序的第一个版本的输出\n\n    请注意，我们刚刚运行的程序犯了动态变量的七大致命错误之一——在完成后不删除动态变量。我们会尽快解决的。\n\n    **在列表中查找项目**\n\n17.  定义一个名为`find()`的函数。该函数在由指向指针`pp`给出的链表中找到一个项目，其值等于`int`参数`v`。它返回指向找到的项目的指针的地址，换句话说，就是指向指针的指针。需要指向指针的指针，以便将项目插入列表中找到的位置。如果没有找到任何项目，它将返回一个指向列表末尾的指针。进入`find()`的骨架，看起来是这样的:\n\n    ```cpp\n    numeric_item** find(int v, numeric_item** pp)\n    {\n    }\n    ```\n\n18.  在列表中循环指针到指针，寻找其`value_`成员等于函数参数`v`的项目。最初，`pp`被设置为`head`的地址。如果`pp`是`nullptr`(即已经到达列表的末尾)或者如果`(pp)->value_`等于`v`，则循环终止。该步骤将`pp`设置为`(*pp)->next_`的地址。完成的`find()`功能如下:\n\n    ```cpp\n    numeric_item** find(int v, numeric_item** pp)\n    {\n        while ((*pp) != nullptr && (*pp)->value_ != v)\n        {\n             pp = &((*pp)->next_);\n        }\n        return pp;\n    }\n    ```\n\n19.  若要继续执行`main()`，请使用`find()`查找一些项目，并在它们之前插入新项目。将指针声明为指向`numeric_item`的指针，并将其称为`pp`。将`pp`设置为找到第 7 项的结果。`pp`现在指向第 7 项的指针。使用`add()`功能，在第 7 项前增加第 8 项。使用`print()`输出结果。代码如下:\n\n    ```cpp\n        numeric_item** pp;\n        pp = find(7, &head);\n        add(8, pp);\n        print();\n    ```\n\n20.  Now insert another item. You can use `find()` to get a pointer to the end of the list by searching for an item that is known not to be in the list. Search for -1\\. Insert an item, 0, at the end of the list. Output the result using `print()`. The code below is another way to do that:\n\n    ```cpp\n        add(0, find(-1, &head));\n        print();\n    ```\n\n    **删除链表中的动态项**\n\n    必须销毁动态创建的列表项，以避免内存泄漏:\n\n21.  进入`while`循环。当`head`不等于`nullptr` :\n\n    ```cpp\n        while (head != nullptr)\n        {\n        }\n    ```\n\n    时，循环继续\n22.  在循环中，创建一个指针`p`，以记住列表中的第一项:\n\n    ```cpp\n            numeric_item* p = head;\n    ```\n\n23.  指定`head`指向下一个列表项:\n\n    ```cpp\n            head = head->next_;\n    ```\n\n24.  Delete `p`:\n\n    ```cpp\n            cout << \"deleting \" << p->value_ << endl;\n            delete p;\n    ```\n\n    完成的`while`循环如下所示:\n\n    ```cpp\n        while (head != nullptr)\n        { \n             numeric_item* p = head;\n             head = head->next_;\n             cout << \"deleting \" << p->value_ << endl;\n             delete p;\n        }\n    ```\n\n25.  编译并运行程序。该程序的输出如下:\n\n![Figure 6.17: Output of the program in exercise 48 ](img/C14195_06_17.jpg)\n\n图 6.17:练习 48 中程序的输出\n\n这种链接动态数据结构的优点是，限制它变大的唯一因素是有多少内存可用。回顾一下我们的图书示例，我们可以在这个列表中为图书中的每个单词创建一个条目，但是条目的值将是包含一个单词的动态数组。我们可以创造 70，000 个单词，或者 100，000 个，或者 120，000 个——也就是说，无论这本书需要什么。\n\n构建动态数据结构是指针和动态变量如此强大的原因。\n\n## 活动 6:创建类实例的二分搜索法树\n\n写一个构建二分搜索法树的程序。应该有一个功能`add()`给树增加一个项目，一个功能`delete_tree()`删除一棵树，一个功能`find()`在树中找到一个插入点，一个功能`print()`打印树。\n\n注意\n\n本次活动的完整代码可在[https://packt.live/331I6Bu](https://packt.live/331I6Bu)找到。\n\n以下是完成活动的步骤:\n\n1.  从一个骨架`main()`功能开始。\n2.  为结构`numeric_tree`添加一个定义。它需要一个 int `value_`成员，以及指向左右子树的指针，左右子树本身就是`numeric_tree`实例。\n3.  添加一个名为`root`的变量作为树根。这是指向`numeric_tree`的指针。\n4.  添加功能框架`add()`。`add()`将一个要添加的`int`值和一个指向树的指针地址的指针作为参数，也就是指向指针的指针。这类似于链表的`add()`功能。对于`add()`功能，认识到添加的项目将总是被添加到等于`nullptr`的子树中。\n5.  为`delete_tree()`功能添加骨架。`delete_tree()`以一个指向树的指针作为参数。`delete_tree()`最容易实现为递归函数。\n6.  添加`find()`功能的骨架。`find()`将一个要添加的`int`值和一个指向树指针地址的指针作为参数，也就是指向指针的指针。`find()`返回一个指向指针的指针。`find()`可以递归或迭代实现。本章前面已经定义了递归版本。\n7.  `print()`功能之前已经描述过。最好递归实现。\n8.  在`main()`中，一次可以添加一个项目，但是我们选择了自动化过程，使用`for`循环从一组`int`值中插入每个项目。`for`循环为每个值调用`add()`。\n9.  打印新构造的树(否则程序可能根本没有任何输出)。\n10.  树是一种动态数据结构。完成后，必须将其删除。\n\n以下是一些提示:\n\n1.  The functions are the same functions you implemented to build the linked list.\n\n    函数可以递归定义，使用上面二叉查找树的递归定义作为指导，或者可以迭代定义。\n\n    所有的函数都以一棵树作为参数。\n\n2.  The function to add an item can use the pointer-to-pointer idiom used in the function `add()` for linked lists.\n\n    添加项目的顺序很重要。如果按照`1, 2, 3, 4, 5, 6`的顺序添加项目，树只会使用右边的子树指针。实际上，这将是一个链表。`4, 2, 1, 3, 6, 5`的插入顺序会做出一个很好的对称树，类似于*图 6.14* ，会对你的程序进行更多的测试。\n\n    注意\n\n    这个活动的解决方案可以在第 529 页找到。\n\n列表和树是两种易于定义的数据结构，但它们只是触及了动态变量的表面。在 C++ 的头 20 年里，开发人员构建了与这些非常相似的数据结构。\n\n*第 12 章*、*容器和迭代器*探讨了 C++ 标准库的容器类。这些容器最初是在 20 世纪 90 年代末引入的，它们提供了标准化的、复杂的列表、树和其他数据结构版本，可以通过预编写的代码来满足大多数用户的需求，这些代码可以通过模板进行定制。现代 C++ 开发人员几乎总是使用标准的库容器，而不是编写自己的自定义容器类。\n\n# 总结\n\n在本章中，您了解到程序可以创建的动态变量或数组的数量没有固定的限制。唯一的限制是可用内存的数量。您了解到动态变量是由一个显式语句创建的，并由另一个显式语句销毁。有两种`new`表达式和两种`delete`表达式——普通变量和数组各一个。您了解到，创建、使用和删除动态变量时所犯的错误会导致不同的后果，具体取决于操作系统和编译器，但通常会导致程序崩溃。您了解到动态变量是由指针引用的，它们可以通过指针链接到列表、树和其他数据结构中。\n\n下一章讨论动态变量的所有权和生命周期。理解这些概念可以让开发人员更可靠地管理动态变量，这样当不再需要它们时，它们就会被删除，并且不会泄漏。*"
  },
  {
    "path": "docs/cpp-workshop/07.md",
    "content": "# 七、动态变量的所有权和寿命\n\n概观\n\n本章提供了一些工具和策略，使 C++ 程序中指针和动态变量的使用更加安全和易于理解。到本章结束时，您将能够描述 C++ 在动态变量所有权和生存期方面的弱点；解释自有和无主指针的使用；将指针嵌入到类实例中，以建立所有权并控制动态变量的生存期；使用智能指针类`unique_ptr<>`和`shared_ptr<>`来自动化动态变量的所有权和生存期。\n\n# 简介\n\n在前一章中，我们学习了如何创建和删除动态变量和动态数组。我们还学习了至少七种使用动态变量出错的方法，这些方法会导致程序崩溃。显然，这些强大的工具需要一些纪律才能正确使用。在这一章中，我们描述了管理动态变量的方法，这些方法可以减少在使用动态变量时出错的机会，首先从生命周期和所有权的概念开始。\n\n生存期和所有权是有经验的开发人员用来驯服动态变量复杂性的关键概念，这样就不会发生内存泄漏。动态变量的生存期和所有权的概念并没有完全用 C++ 语法来表达。它们是开发人员必须管理的东西。\n\n每个`new`表达式的结果必须分配给一个指针变量，否则新的动态变量将不可访问。这个指针变量可以说在创建时就拥有动态变量。每个`delete`-表达式都以一个指向动态变量的指针作为参数。当这个指针被删除时，可以说它拥有动态变量。\n\n## 动态变量的寿命\n\n大多数变量都有明确的生命周期。在他们生命的开始，变量被构造或初始化。在其生命的最后，变量被破坏。全局(`static`或`extern`)变量在`main()`开始之前以特定的顺序被构造。在`main()`退出后，它们会自动销毁，与它们的构建顺序相反。函数局部变量和块局部变量也称为自动变量，在声明它们的地方构造。当执行离开声明变量的范围(用花括号分隔)时，它们就会被销毁。当包含类成员变量的类实例被销毁时，它们也被销毁。\n\n动态变量，包括动态数组，是这些简单规则的令人沮丧的例外。动态变量从可执行语句创建之时起一直存在，直到被另一个可执行语句销毁。该程序可以显式控制由动态变量构建的数据结构的生命周期。这是一把双刃剑，因为如果程序忘记销毁动态变量，它的内存将变得不可访问。\n\n## 动态变量的所有权\n\n销毁动态变量的责任由整个程序分担。C++ 允许程序的任何一行创建或销毁指向动态变量的附加指针，并且允许程序自由定义动态变量的生存期。然而，如果开发人员忘记删除或双重删除动态变量，C++ 会受到严厉的惩罚:内存泄漏或操作系统的错误陷阱导致程序停止运行。此外，搜寻这些 bug 可能需要跟踪程序中的所有执行路径。这种管理动态变量的责任分散、失败成本巨大的情况几乎令人无法忍受。\n\n开发人员可能试图通过非正式地记录动态变量的*所有权*来驯服动态变量的原始力量。指针变量被指定为*在其生命周期内拥有*一个动态变量。该指针被称为*拥有的*指针。指定的指针变量用于删除动态变量。任何其他指向动态变量的指针都被称为*无主*指针。这不是 C++ 编译器可以帮助的。这是开发人员可能会出错的事情，因为编译器不会强制执行开发人员的所有权规则。\n\nC++ 充满了无主指针。标准库迭代器和`std::string_view`实例是无主指针。许多函数将无主指针返回到标准库数据结构中。非 C++ 编程语言的拥护者可能会引用无主指针是多么危险，作为放弃 C++ 的充分理由。这种担心在某种程度上是有道理的。但不要放弃所有希望；有很好的方法来应对所有权和寿命。\n\n管理动态变量所有权的最强大方法之一是将动态变量的所有者指针设置为类成员变量，并在类实例被销毁时删除动态成员变量。然后开发人员可以声明该类的全局、函数局部或块局部实例，该类实例中包含的动态变量将与该类实例具有相同的生存期。这种技术使得动态变量的生命周期和其他类型变量的生命周期一样容易理解。\n\n## 资源获取是初始化(RAII)\n\n拥有指向动态变量的指针并在实例被销毁时删除动态变量的类实例是更广泛的习惯用法的一个实例，其中类实例获取一些资源，拥有该资源，并在类实例被销毁时释放该资源。这个成语叫做 **RAII** ( **资源获取就是初始化**)。\n\nRAII 是 C++ 中一个强大的习惯用法，用于许多资源:动态变量、打开的文件、窗口句柄、信号量和互斥体(这是多线程同步原语，本书不讨论)。RAII 类如此有用的原因是它们管理自己拥有的资源的生命周期。开发人员不必考虑如何释放资源。都是自动的。\n\n## 练习 49:终生示范\n\n本练习中的程序创建并销毁了一些类实例，以说明全局变量、函数局部变量和块局部变量的生存期:\n\n注意\n\n这个练习的完整代码可以在这里找到:[https://packt.live/2Dd3qJD](https://packt.live/2Dd3qJD)。\n\n1.  进入程序的骨架:\n\n    ```cpp\n    #include <iostream>\n    using namespace std;\n    int main()\n    { \n        return 0;\n    }\n    ```\n\n2.  输入`class noisy`的定义。在这种情况下，`noisy`接受一个以 null 结尾的字符串构造函数参数，该参数对声明实例的范围提供注释:\n\n    ```cpp\n    class noisy\n    {\n        char const* s_;\n    public:\n        noisy(char const* s) \n        { cout << \"constructing \" << s << endl; s_ = s; }\n       ~noisy()\n        { cout << \"destroying \" << s_ << endl; }\n    };\n    ```\n\n3.  输入函数`func()`的定义，声明一个函数-局部`noisy`实例，然后返回:\n\n    ```cpp\n    void func(char const* s)\n    {\n        noisy func(s);\n    }\n    ```\n\n4.  Enter two global declarations of `noisy` instances at file scope:\n\n    ```cpp\n    noisy f(\"global 1\");\n    noisy ff(\"global 2\");\n    ```\n\n    在控制转移到`main()`之前，所有的文件范围变量都按照它们被声明的顺序一个接一个地被构造。执行从`main()`返回后，按相反顺序销毁。如果您在多个文件中声明文件范围变量，有一个规则会告诉您它们的构造顺序。如果你需要了解，你可以在标准的副本中查找。\n\n5.  In `main()`, create an instance of `noisy` called `n1`:\n\n    ```cpp\n        noisy n1(\"main() function local 1\");\n    ```\n\n    `n1`是对所有`main()`都有效的函数局部变量。这实际上与全局变量的生存期相同。不同的是`n1`的定义只在`main()`内可见，而`f`和`ff`的定义在文件中随处可见。\n\n6.  用一个说法`\"function local 2\"`来称呼`func()`。`func()`将在创建时打印一对消息，然后立即销毁一个`noisy`实例:\n\n    ```cpp\n        func(\"function local 2\");\n    ```\n\n7.  接下来，输入由大括号组成的块范围。这是合法的 C++ 语法，它允许开发人员用自己的局部声明创建一个范围:\n\n    ```cpp\n        {\n        }\n    ```\n\n8.  在空的花括号里是另一个`noisy`的声明和对`func()`的调用。因此，构造了块局部`noisy`，然后调用`func()`，这在函数范围内构造了一个`noisy`实例。然后`func()`返回，破坏功能范围`noisy`实例。当执行离开大括号块时，它会暂停足够长的时间，以便在经过时破坏`noisy`实例:\n\n    ```cpp\n            noisy n(\"block local\");\n            func(\"function local 3\");\n    ```\n\n9.  After the block, `main()` returns, which also destroys the `noisy` instance in `main()`'s scope.\n\n    没有任何与此对应的可见代码，但是两个全局`noisy`实例被销毁，顺序与构建相反。\n\n10.  完整的程序如下所示。\n\n    ```cpp\n    #include <iostream>\n    using namespace std;\n    class noisy\n    {\n        char const* s_;\n    public:\n        noisy(char const* s) \n        { cout << \"constructing \" << s << endl; s_ = s; }\n       ~noisy()\n        { cout << \"destroying \" << s_ << endl; }\n    };\n    void func(char const* s)\n    {\n        noisy func(s);\n    }\n    noisy f(\"global 1\");\n    noisy ff(\"global 2\");\n    int main()\n    {\n        noisy n1(\"main() function local 1\");\n        func(\"function local 2\");\n        {\n            noisy n(\"block local\");\n            func(\"function local 3\");\n        }\n        return 0;\n    }\n    ```\n\n11.  Compile and run the program. Its output is as follows, along with a description of what you are seeing:\n\n    ![Figure 7.1: Output of the program in exercise 49 with corresponding description ](img/C14195_07_01.jpg)\n\n图 7.1:练习 49 中程序的输出以及相应的描述\n\n所以，我们从这个练习中学到了:\n\n*   一个变量的生命周期——如果适当地包装在一个类实例中，也意味着一个动态变量——可以扩展到一个程序的所有执行，或者所有的`main()`和从`main()`内部调用的任何函数，或者所有的其他函数，或者一个函数中的一个块。\n*   我们可以使用大括号在函数的作用域内创建块作用域，不同的声明在该作用域内有效。这比动态变量更有用。\n*   由于变量的生命周期从它被声明时开始，我们可以通过在我们希望它的生命周期开始的块的中间声明变量来进一步约束变量的生命周期。\n\n任何地方你看到`class noisy`的一个实例说`constructing…`，记住这也是一个机会去创建一个动态变量，它的指针被一个类拥有，比如`noisy`，并且和那个`noisy`实例有相同的寿命。\n\n当`class noisy`的一个实例说`destroying…`时，这是一个销毁与`noisy`实例具有相同寿命的动态变量的机会。\n\n很难想象一个动态变量需要有一个生命周期，而不是使用这个例子说明的 RAII 类可能有的生命周期，但是维护完全自由的代码审查和调试成本很少值得，因为 RAII 是如此强大。\n\n## 练习 50:数据结构中的自有指针\n\n本练习中的程序展示了如何管理数据结构中动态变量的生存期和所有权。拥有动态内容的数据结构是 C++ 开发中经常出现的模式。在本练习中，`numeric_list`类拥有构成列表的所有动态创建的`numeric_item`实例。\n\n注意\n\n这个练习的完整代码可以在这里找到:[https://packt.live/2QIrBYG](https://packt.live/2QIrBYG)。\n\n一个常见的 C++ 习惯用法是，一个类定义数据结构，而另一个类定义数据结构中的项目。在非面向对象的语言中，程序员通常会为数据结构项声明一个记录结构，而一个简单的指针指向数据结构的根。\n\n在 C++ 中，您可以附加作用于整个数据结构的成员函数，例如打印或删除它。`numeric_list::head_`是指向`numeric_item`的自有指针。当`numeric_list`的一个实例被销毁时，析构函数删除列表中的每个动态变量。单个`numeric_item`实例中的`next_`指针不是自有指针；所有实例都属于`numeric_list`中的`head_`指针:\n\n1.  输入默认的`main()`函数和结构`numeric_item`的定义。你以前见过这些，在*第 6 章动态变量*的末尾:\n\n    ```cpp\n    #include <iostream>\n    using namespace std;\n    struct numeric_item\n    {\n        int value_;\n        numeric_item* next_;\n    };\n    int main()\n    {\n        return 0;\n    }\n    ```\n\n2.  Enter a definition of class `numeric_list`:\n\n    ```cpp\n    class numeric_list\n    {\n        numeric_item* head_;\n    public:\n        numeric_list() : head_(nullptr) {}\n       ~numeric_list();\n        void print();\n        void add(int v);\n        numeric_item* find(int v);\n    };\n    ```\n\n    `numeric_list`有一个私有成员变量——一个指向`numeric_item`的指针，叫做`head_`。它的公共接口有一个构造函数、一个析构函数、一个名为`print()`的`void`函数、一个名为`add()`的`void`函数和一个名为`find()`的函数，该函数接受一个`int`参数并返回一个指向`numeric_item`的无主指针，该指针可能是`nullptr`。\n\n3.  The functions in `numeric_list` are declared in the class definition for `numeric_list` but are defined outside the class definition, the way you would do if the class definition was in a header file while the member functions were defined in a `.cpp` file. When defined in this way, the compound function name with `numeric_list` and `::`, must be added to the front of the function name as it appears inside the class definition.\n\n    注意\n\n    在 Java 中，所有成员函数都必须在类定义中定义。这种语法在 C++ 中也有效，但对于大多数 C++ 开发人员来说，这不是一种熟悉的风格。C++ 开发人员习惯于将类定义作为类和其他地方出现的成员函数定义的简明概述。大多数开发人员将模板成员函数放在类定义中。一些开发人员将短成员函数放在类定义中。\n\n    析构函数由 C++ 运行时系统隐式调用，但如果您显式调用它，它的名称将是`~numeric_list()`。因为`head_`是一个拥有的指针，析构函数必须删除指针拥有的任何动态变量，在这种情况下，这是列表的每个元素。析构函数遍历列表，重复从列表中删除第一个项目，然后删除那个被删除的项目。\n\n    析构函数如下所示:\n\n    ```cpp\n    numeric_list::~numeric_list()\n    {\n        while (head_ != nullptr)\n        {\n            numeric_item* p = head_;\n            head_ = head_->next_;\n            cout << \"deleting \" << p->value_ << endl;\n            delete p;\n        }\n    }\n    ```\n\n    删除`head_`，然后说`head_ = head_->next_;`是常见的错误。这个看似简单的符号的问题在于，删除`head_`后，它不再指向任何东西。我见过运行此错误的代码。我也见过它失败。\n\n4.  `print()`功能与上一章练习中的相同。转载于此:\n\n    ```cpp\n    void numeric_list::print()\n    {\n        for (numeric_item* p = head_; p != nullptr; p = p->next_)\n        {\n            cout << p->value_ << \" \";\n        }\n        cout << endl;\n    }\n    ```\n\n5.  `add()`函数创建一个新的`numeric_item`实例，并将其添加到列表的头部:\n\n    ```cpp\n    void numeric_list::add(int v)\n    {\n        numeric_item* newp = new numeric_item;\n        newp->value_ = v;\n        newp->next_ = head_;\n        head_ = newp;\n    }\n    ```\n\n6.  The `find()` function iterates through the list looking for an item that has the same value as the `v` argument. It returns an unowned pointer to the found item, or `nullptr` if no item is found:\n\n    ```cpp\n    numeric_item* numeric_list::find(int v)\n    {\n        for (numeric_item* p = head_; p != nullptr; p = p->next_)\n        {\n            if (p->value_ == v) \n                return p;\n        }\n        return nullptr;\n    }\n    ```\n\n    为什么`find()`返回的这个指针是无主的？删除此指针会损坏列表。前一个列表项将指向不再有效的东西，因此程序的行为将是未定义的。即使程序在那之前没有崩溃`numeric_item`的析构函数最终会重复删除已经删除的项目，让程序成为一个燃烧的残骸。\n\n    一个没有写这个列表类的开发者怎么知道`find()`返回的指针是无主的？他们不知道，除非编写这个类的开发人员证明了这一点。类定义中带有函数声明的注释可能会说:`// returns unowned pointer to list_item, or nullptr`。\n\n    任何时候容器类的成员函数，比如`numeric_list`，返回一个无主指针都是一个出错的机会。开发人员必须确保容器类销毁后不使用无主指针。正如后面将要显示的，无主指针变量和列表容器同时被销毁——因为执行离开了`main()`——所以在这种情况下，使用无主指针不会引起问题。\n\n7.  下一步是开始进入`main()`的身体。首先，声明一个名为`l`的`numeric_list`实例。当`main()`返回:\n\n    ```cpp\n        numeric_list l;\n    ```\n\n    时，该函数-局部变量将被销毁\n8.  创建一个`for`循环向列表中添加五个项目，然后打印出列表:\n\n    ```cpp\n        for (int i = 1; i < 6; ++ i)\n        {\n            l.add(i);\n        }\n        l.print();\n    ```\n\n9.  Declare a `numeric_item` pointer called `p`, and assign to `p` the value returned by `l.find(4)`. The value in `p` is an unowned pointer. We already know `find()` will discover an item with this value because we added it moments ago. Output a message if the returned pointer is not `nullptr`, just to be sure:\n\n    ```cpp\n        numeric_item* p = l.find(4);\n        if (p != nullptr)\n            cout << \"found numeric_item 4\" << endl;\n    ```\n\n    就这样。当`main()`返回时，`p`仍然指向`l`中的一个项目。`p`没有被删除，但是没关系，因为`p`是一个无主的指针。\n\n    当`main()`返回时，`l`的析构函数被调用。由于`l`有一个自有指针，`l`的析构函数必须删除它所指向的任何东西，这就是整个列表。\n\n10.  Compile and run the program. Its output is reproduced here:\n\n    ![Figure 7.2: Output of the program in exercise 50 ](img/C14195_07_02.jpg)\n\n图 7.2:练习 50 中程序的输出\n\n不出所料，程序在列表中插入了五项，五项由`print()`输出。在列表中找到了`4`项，列表的析构函数删除了这五项。\n\n无主指针是不安全的指针。绝对没有什么*可以阻止*开发者在引用它的拥有的指针已经被销毁并且动态变量已经被删除之后，仍然持有一个没有拥有的指针。C++ 开发人员必须承担责任，确保这不会发生，以换取他们代码的快速执行。\n\n但是开发人员如何知道这个无主指针没有指向垃圾呢？我们可以很容易地在`l`里面看到拥有的指针的寿命。是`main()`的全部。这基本上就是整个程序。无主指针`p`的寿命呢？它从它的声明点开始，在那里它被初始化为一个值，直到`main()`结束。当它指向垃圾时，就没有机会使用`p`。如果`l`和`p`在功能或块范围内声明，分析将是相同的。\n\n注意\n\n你会发现不喜欢写评论的人会声称代码应该是自己的文档。这个危险的建议忽略了这样一个事实，即程序本身并不记录动态变量的寿命和所有权。\n\n有时，一个函数读取一个文件，接受一个输入，或者以其他方式收集数据，这些数据变成了一个动态变量。当这种情况发生时，拥有的指针可能是函数本地的。当指向动态变量的指针被返回时，所有权转移发生。C++ 代码中没有任何内容告诉您函数返回的原始指针何时是自有指针。您阅读了该函数的文档或注释，或者您意识到，由于该函数返回的是无数字节的缓冲区，而不是参数，因此它必须是一个自有指针。\n\n## 练习 51:所有权的转让\n\n本练习提供了一个程序必须转移原始指针所有权的例子。以下是完成练习的步骤:\n\n注意\n\n这个练习的完整代码可以在这里找到:[https://packt.live/2s0Kvzs](https://packt.live/2s0Kvzs)。\n\n1.  进入骨架`main()`程序:\n\n    ```cpp\n    #include <iostream>\n    using namespace std;\n    int main()\n    {\n        return 0;\n    }\n    ```\n\n2.  为`<cstring>`添加`include`指令，因为该程序使用字符串函数:\n\n    ```cpp\n    #include <cstring>\n    ```\n\n3.  定义一个名为`noisy_and_big`的`noisy`类。它不同于通常的`noisy`类，有一个 10，000 字节的`char`数组，模拟一个`struct`大到必须动态分配:\n\n    ```cpp\n    struct noisy_and_big\n    {\n        noisy_and_big() { cout << \"constructing noisy\" << endl; }\n       ~noisy_and_big() { cout << \"destroying noisy\" << endl; }\n        char big_buffer_[10000];\n    };\n    ```\n\n4.  Define a function to create `noisy_and_big` instances:\n\n    ```cpp\n    noisy_and_big* get_noisy_and_big(char const* str)\n    {\n        noisy_and_big* ownedp = new noisy_and_big;\n        strcpy(ownedp->big_buffer_, str);\n    }\n    ```\n\n    在实际代码中，缓冲区将通过读取文件或获取网络数据包来填充，但是对于这个简短的演示，只需在缓冲区中写入一个字符串。同样在真实代码中，在我们使用`strcpy()`将其复制到`big_buffer`之前，我们必须检查`str`的长度。\n\n    创建`noisy_and_big`实例时，`ownedp`是其所有者。然而，`ownedp`只活到了功能结束。`get_noisy_and_big()`必须要么删除`ownedp`(这没有意义)，要么在概念上将所有权转移给呼叫者。\n\n5.  进入`main()`的身体。第一个语句声明了一个名为`newownedp`的指针，并将调用`get_noisy_and_big()`的结果赋给它。现在，`newownedp`是主人。输出一条消息，显示缓冲区的内容已经到达:\n\n    ```cpp\n        noisy_and_big* newownedp = get_noisy_and_big(\"a big, big buffer\");\n        cout << \"noisy and big: \" << newownedp->big_buffer_ << endl;\n    ```\n\n6.  生产代码对`noisy_and_big`实例做了一些有用的事情，然后是时候删除拥有的指针:\n\n    ```cpp\n        delete newownedp;\n    ```\n\n7.  完整的程序如下所示。\n\n    ```cpp\n    #include <iostream>\n    #include <cstring>\n    using namespace std;\n    struct noisy_and_big\n    {\n        noisy_and_big() { cout << \"constructing noisy\" << endl; }\n       ~noisy_and_big() { cout << \"destroying noisy\" << endl; }\n        char big_buffer_[10000];\n    };\n    noisy_and_big* get_noisy_and_big(char const* str)\n    {\n        noisy_and_big* ownedp = new noisy_and_big;\n        strcpy(ownedp->big_buffer_, str);\n    }\n    int main()\n    {\n        noisy_and_big* newownedp = get_noisy_and_big(\"a big, big buffer\");\n        cout << \"noisy and big: \" << newownedp->big_buffer_ << endl;\n        delete newownedp;\n        return 0;\n    }\n    ```\n\n8.  编译并运行程序。其输出转载于此:\n\n![Figure 7.3: Output of the program in exercise 51 ](img/C14195_07_03.jpg)\n\n图 7.3:练习 51 中程序的输出\n\n`noisy_and_big`实例在`get_noisy_and_big()`中构建，在`main()`中销毁。所有权从一个拥有指针转移到另一个拥有指针。重要的是，C++ 中没有任何内容指示转移。这都是关于文件和惯例。\n\n跟踪动态变量所有权的规则是在 C++ 开发的前 20 年里将有经验的 C++ 开发人员与新手区分开来的事情之一。大多数人都认为，缺乏对所有权跟踪的自动化支持远非最佳。幸运的是，有更好的解决方案，我们接下来将对此进行研究。\n\n# 智能指针—自动拥有动态变量\n\n之前的练习已经演示了拥有的指针可以被包装在类实例中，这样可以在类被破坏时删除指针拥有的动态变量。这种设计可以更进一步，创建一个只包含指向动态变量的自有指针的类。这样的对象称为智能指针。\n\nC++ 标准库中智能指针的设计利用了 C++ 的大多数高级特性，包括运算符函数、模板元编程、移动语义、变量模板和完美转发。这个设计太先进了，无法在这个简短的课程中涵盖。然而，结果是一个看起来和行为都很像原始指针的东西，但是当智能指针被销毁时，它会删除自己拥有的动态变量。\n\n## unique_ptr < >\n\n`unique_ptr<>`是一个拥有动态变量的智能指针模板类。在 C++ 中，模板类是一种可以生成类族的宏。模板是 C++ 编程中的一个重要话题。它们包含在*第 11 章*、*模板*中。现在，最重要的是包含一个带有`include`指令的模板类库，然后通过在模板类声明中用尖括号命名类型来专门化特定类型的模板，如下所示:\n\n```cpp\n#include <memory>\nunique_ptr<MyClass> pMyClass;\n```\n\n该声明创建了一个指向动态 MyClass 变量的智能指针。`unique_ptr<>`生成的代码与原始指针的代码一样快。之所以叫`unique_ptr`，是因为它不分享所有权。\n\n`unique_ptr<>`相对于原始指针的诸多优势包括:\n\n*   `unique_ptr<>`拥有自己的动态变量，当`unique_ptr<>`被破坏时删除动态变量。\n*   `unique_ptr<>`从不包含随机位。要么包含`nullptr`要么包含指向动态变量的指针。\n*   `unique_ptr<>`在其动态变量被删除后不包含悬空指针。它在销毁时删除动态变量，或者`unique_ptr::reset()`删除动态变量并将`unique_ptr<>`内部的指针设置为`nullptr`。\n*   `unique_ptr<>`单据所有权。在使用`unique_ptr<>`作为自有指针的程序中，原始指针是无主指针。\n\n## Ex ercise 52:与 unique_ptr < >合作\n\n本练习构建并销毁一些指向动态变量和动态数组的`unique_ptr<>`实例，并展示如何将动态变量的所有权从一个`unique_ptr<>`转移到另一个:\n\n注意\n\n这个练习的完整代码可以在这里找到:[https://packt.live/2D3A1lk](https://packt.live/2D3A1lk)。\n\n1.  进入骨架`main()`程序:\n\n    ```cpp\n    #include <iostream>\n    using namespace std;\n    int main()\n    {\n        return 0;\n    }\n    ```\n\n2.  `<memory>`表头是定义`unique_ptr<>`模板的地方。包括`#include <iostream>`下面的`<memory>`:\n\n    ```cpp\n    #include <memory>\n    ```\n\n3.  该程序使用`<cstring>`标题中的一个字符串函数。包括`<cstring>`:T2\n4.  输入`class noisy`的定义。这个版本的`noisy`有两种不同的构造函数:默认构造函数和接受 int 参数的构造函数。在本练习中，我们将通过以两种方式构建`noisy`来展示`new`表达式的一些选项:\n\n    ```cpp\n    struct noisy\n    {\n        noisy() { cout << \"default constructing noisy\" << endl; }\n        noisy(int i) { cout << \"constructing noisy: arg \" << i << endl; }\n       ~noisy() { cout << \"destroying noisy\" << endl; }\n    };\n    ```\n\n5.  在`main()`内部，首先声明一个名为`u1`的`unique_ptr<noisy>`实例，并将其初始化为一个新的`noisy`实例。这个`new`-表达式调用`noisy`的默认构造函数。:\n\n    ```cpp\n        unique_ptr<noisy> u1(new noisy);\n    ```\n\n6.  声明一个名为`u2`的`unique_ptr<noisy>`实例。`unique_ptr<>`的默认构造函数将该指针设置为`nullptr`。然后，将`u2`设置为初始化为`100`的新`noisy`实例。使用接受`int`参数的`noisy`构造函数。成员函数`unique_ptr::reset()`删除`unique_ptr`当前引用的任何动态变量，然后将`unique_ptr`设置为指向`reset()`的参数。在这种情况下，`u2`指向`nullptr`，所以`reset()`的作用是将`u2`设置为新的`noisy`实例:\n\n    ```cpp\n        unique_ptr<noisy> u2;\n        u2.reset(new noisy(100));\n    ```\n\n7.  Declare a `unique_ptr<>` instance to a `noisy` array called `u3`, and initialize it to a new dynamic array of three `noisy` instances:\n\n    ```cpp\n        unique_ptr<noisy[]> u3(new noisy[3]);\n    ```\n\n    关于这个声明有几件事需要注意。首先，一个数组的`unique_ptr<>`与一个普通变量的`unique_ptr<>`的声明不同，因此模板将选择适合删除数组的`delete[]`表达式。另一件需要注意的事情是`unique_ptr<>`从不自己创建动态变量，而是接受在`unique_ptr<>`之外创建的动态变量的所有权。在下一个练习中，我们将看到`make_unique()`函数，它同时创建了`unique_ptr<>`实例和动态变量。\n\n8.  向名为`u4`的`noisy`数组声明一个`unique_ptr`实例。将其初始化为两个`noisy`实例的新动态数组，第一个初始化为 1，第二个默认初始化，因为初始值设定项列表中没有足够的初始值设定项:\n\n    ```cpp\n        unique_ptr<noisy[]> u4(new noisy[2]{1});\n    ```\n\n9.  声明一个名为`u5`的`unique_ptr<noisy>`实例。默认初始化为`nullptr` :\n\n    ```cpp\n        unique_ptr<noisy> u5;\n    ```\n\n10.  输出`u1`和`u5`的原始指针值，使用`get()`成员函数获得一个原始的、无主的指针:\n\n    ```cpp\n        cout << \"before transfer of ownership u1 = \" << u1.get()          << \", u5 = \" << u5.get() << endl;\n    ```\n\n11.  将`u1`中动态变量的所有权转移至`u5`。使用`release()`成员函数释放`u1`动态变量的所有权，并返回一个拥有的原始指针。这成为`reset()`的参数，它删除`u5`拥有的动态变量，然后从`u1`接受拥有的原始指针的所有权。由于`u5`是默认构造的，所以它之前的值是`nullptr` :\n\n    ```cpp\n        u5.reset(u1.release());\n    ```\n\n12.  输出所有权转移后`u1`和`u5`的原始指针:\n\n    ```cpp\n        cout << \"after transfer of ownership u1 = \" << u1.get()\n             << \", u5 = \" << u5.get() << endl;\n    ```\n\n13.  通过不同的方法将`u5`的所有权转移回`u1`。使用移动语义，使用功能`std::move()`将`u5`移动到`u1`。移动语义是一个高级的 C++ 概念，它太复杂了，本书无法涵盖，但它是一个值得您在未来关注的概念。函数返回的`unique_ptr<>`实例也通过移动语义转移所有权。在此声明的末尾，`u5`是`nullptr` :\n\n    ```cpp\n        u1 = move(u5);\n    ```\n\n14.  转移后输出`u1`和【T1:\n\n    ```cpp\n        cout << \"after second transfer u1 = \" << u1.get()\n             << \", u5 = \" << u5.get() << endl;\n    ```\n\n15.  Create a `unique_ptr<>` instance to a `char` array. This is a common idiom for creating dynamically sized buffers without having to worry about deleting them later. This buffer will be automatically deleted as `main()` returns. Put a short string in the buffer and use `get()` to output the `char` pointer as a string:\n\n    ```cpp\n        unique_ptr<char[]> buf(new char[20]);\n        strcpy(buf.get(), \"xyzzy\");\n        cout << \"buf = \" << buf.get() << endl;\n    ```\n\n    当`main()`返回时，所有`unique_ptr<>`实例被销毁，其动态内容被删除。完成的程序是这样的。\n\n    ```cpp\n    #include <iostream>\n    #include <memory>\n    #include <cstring>\n    using namespace std;\n    struct noisy\n    {\n        noisy() { cout << \"default constructing noisy\" << endl; }\n        noisy(int i) { cout << \"constructing noisy: arg \" << i << endl; }\n       ~noisy() { cout << \"destroying noisy\" << endl; }\n    };\n    int main()\n    {\n        unique_ptr<noisy> u1(new noisy);\n        unique_ptr<noisy> u2;\n        u2.reset(new noisy(100));\n        unique_ptr<noisy[]> u3(new noisy[3]);\n        unique_ptr<noisy[]> u4(new noisy[2]{1});\n        unique_ptr<noisy> u5;\n        cout << \"before transfer of ownership u1 = \" << u1.get()\n             << \", u5 = \" << u5.get() << endl;\n        u5.reset(u1.release());\n        cout << \"after transfer of ownership u1 = \" << u1.get() \n             << \", u5 = \" << u5.get() << endl;\n        u1 = move(u5);\n        cout << \"after second transfer u1 = \" << u1.get() \n             << \", u5 = \" << u5.get() << endl;\n        unique_ptr<char[]> buf(new char[20]);\n        strcpy(buf.get(), \"xyzzy\");\n        cout << \"buf = \" << buf.get() << endl;\n        return 0;\n    }\n    ```\n\n16.  Compile and run the completed program. Its output looks as shown below. Of course, different runs of the program may print out different hexadecimal pointer addresses:\n\n    ![Figure 7.4: Outyput of the program in exercise 52 ](img/C14195_07_04.jpg)\n\n图 7.4:练习 52 中程序的输出\n\n第一行输出来自`u1`。然后，以`100`为构造函数参数构造`u2`。接下来的三行是`u3`中嘈杂阵的三个成员。接下来的两行是`u4`中嘈杂的阵。从`u1`到`u5`再回到的转移出现在意料之中。输出缓冲区。然后删除`u1`中的`noisy`实例、`u2`中的`noisy`实例、`u3`中的三个`noisy`实例、`u4`中的两个`noisy`实例。`u5`为`nullptr`。\n\n值得注意的是，开发者不需要删除任何东西。智能指针跟踪事物，并自动删除它们拥有的动态变量。\n\n注意\n\n处置由`unique_ptr<>`实例拥有的动态变量的`delete`表达式隐藏在`unique_ptr<>`的定义中。一些软件团队利用这一点在大型代码库上运行自动化工具，寻找`delete`关键字的实例来标记代码评审期间的特别注意。消除所有显式的`delete`-使用智能指针的表达式可能会提高代码库的质量。\n\n`unique_ptr<>`并不能解决所有问题。例如，`unique_ptr<>`的默认版本不够智能，无法在列表头被破坏时删除链表数据结构的所有成员。`unique_ptr<>`模板有一个很少使用的可选第二个参数，称为`deleter`，这是当`unique_ptr`实例被破坏时要调用的函数。这允许`unique_ptr<>`的扩展删除整个数据结构，并允许它做其他事情，例如关闭打开的文件。\n\nC++ 中智能指针的广泛采用标志着 C++ 程序相对于旧的 C 和 C++ 代码库的可靠性有了显著提高。尽管 C++ 是一种“不安全”的语言，但是遵循现代 C++ 实践的团队几乎没有内存泄漏的问题。我们将在未来的章节中发现，智能指针在与 C++ 异常处理相结合时特别强大。\n\n## make_unique()\n\n`make_unique()`是一个模板函数，它创建一个动态变量，并将其分配给适当类型的`unique_ptr`实例，然后返回。正如`unique_ptr<>`在其定义中隐藏了`delete`表达式一样，`make_unique()`对`new`表达式也是如此。这允许团队使用禁止“裸”`new` -和`delete`-表达式的编码标准来提高代码质量。\n\n## 练习 53:使用 make_unique()\n\n本练习演示如何使用`make_unique()`隐藏新的表达式。以下是完成练习的步骤:\n\n注意\n\n这个练习的完整代码可以在这里找到:[https://packt.live/35nhLiQ](https://packt.live/35nhLiQ)。\n\n1.  进入骨架`main()`程序:\n\n    ```cpp\n    #include <iostream>\n    using namespace std;\n    int main()\n    {\n        return 0;\n    }\n    ```\n\n2.  `<memory>`表头是定义`unique_ptr<>`模板的地方。包括`#include <iostream>`下面的`<memory>`:\n\n    ```cpp\n    #include <memory>\n    ```\n\n3.  输入类`noisy`的定义:\n\n    ```cpp\n    struct noisy\n    {\n        noisy() { cout << \"constructing noisy\" << endl; }\n       ~noisy() { cout << \"destroying noisy\" << endl; }\n    };\n    ```\n\n4.  在`main()`内部，声明一个名为`u1`的`unique_ptr<noisy>`实例，并将其初始化为一个新的`noisy`实例:\n\n    ```cpp\n        unique_ptr<noisy> u1(new noisy);\n    ```\n\n5.  声明一个`unique_ptr<noisy>`叫做`u2`。将呼叫返回的值分配给`make_unique<noisy>()` :\n\n    ```cpp\n        unique_ptr<noisy> u2 = make_unique<noisy>();\n    ```\n\n6.  Declare a `unique_ptr<>` instance to a `noisy` array called `u3`, using the `auto` keyword to avoid re-entering the type of the variable, then assign the value returned by `make_unique<noisy[]>(4)`, which creates an array of four `noisy` instances:\n\n    ```cpp\n        auto u3 = make_unique<noisy[]>(4);\n    ```\n\n    将`u3`的声明与前一行声明`u2`进行比较:`u3`声明中的`auto`关键字允许开发人员省略类型名称，因为它可以从初始值中推导出来。这种现代 C++ 语法的优势在开发人员第一次声明具有长类型名或一堆模板参数的东西时变得很明显。\n\n7.  完成的程序是这样的。\n\n    ```cpp\n    #include <iostream>\n    #include <memory>\n    using namespace std;\n    struct noisy\n    {\n        noisy() { cout << \"constructing noisy\" << endl; }\n       ~noisy() { cout << \"destroying noisy\" << endl; }\n    };\n    int main()\n    {\n        unique_ptr<noisy> u1(new noisy);\n        unique_ptr<noisy> u2 = make_unique<noisy>();\n        auto u3 = make_unique<noisy[]>(4);\n        return 0;\n    }\n    ```\n\n8.  编译并运行完成的程序。它的输出如下所示:\n\n![Figure 7.5: Output of the program in exercise 53 ](img/C14195_07_05.jpg)\n\n图 7.5:练习 53 中程序的输出\n\n第一个声明创建一个`noisy`实例。第二个创造了另一个。第三个创造四个。当`main()`退出时，由于`unique_ptr<>`实例被销毁，所有六个都被删除。\n\n`make_unique()`并不完美。例如，数组版本只能默认初始化一个动态数组。`make_unique()`具有隐藏`new`关键词的宝贵属性。`make_unique()`存在的部分原因是与`make_shared()`的风格兼容，这将在后面介绍。\n\n在下一个练习中，我们将看到智能指针如何帮助简化类的实现，通常不需要编写析构函数。\n\n## 作为类成员变量的唯一 _ptr < >\n\n当类被销毁时，调用其析构函数，销毁类实例；然后调用每个成员变量的析构函数，销毁成员。基本类型的析构函数，如`int`或`char`，什么也不做。但是，当成员是类实例时，会调用该成员的析构函数。\n\n当类成员是智能指针时，当包含智能指针的类实例被销毁时，会自动调用智能指针的析构函数。开发人员无需编写任何代码来删除智能指针所拥有的动态变量。\n\n如果包含动态变量的所有类成员都是智能指针，则类析构函数可能为空。即使析构函数为空，成员析构函数也会在类实例被销毁时运行。智能指针使您的类的代码看起来简单而流畅，并使代码更容易检查。一旦你开始使用智能指针，你就再也不想回去了。\n\n## 练习 54:使用唯一 _ptr < >作为类成员变量\n\n本练习包括创建一个简单的类，其成员是智能指针，以说明当智能指针是类成员时产生的简化类语法:\n\n注意\n\n这个练习的完整代码可以在这里找到:[https://packt.live/2DaBO7W](https://packt.live/2DaBO7W)。\n\n1.  进入骨架`main()`程序:\n\n    ```cpp\n    #include <iostream>\n    using namespace std;\n    int main()\n    {\n        return 0;\n    }\n    ```\n\n2.  `unique_ptr<>`模板在`<memory>`表头定义。包括`#include <iostream>`之后的`<memory>`:\n\n    ```cpp\n    #include <memory>\n    ```\n\n3.  该程序使用`<cstring>`标题中的一个字符串函数。包括`<cstring>`如下:\n\n    ```cpp\n    #include <cstring>\n    ```\n\n4.  输入类`noisy`的常用定义:\n\n    ```cpp\n    struct noisy\n    {\n        noisy() { cout << \"constructing noisy\" << endl; }\n       ~noisy() { cout << \"destroying noisy\" << endl; }\n    };\n    ```\n\n5.  Enter a definition for class `autobuf`, which is intended to model a class containing a very large buffer, as if the data were read in from a file or a network packet:\n\n    ```cpp\n    class autobuf\n    {\n        unique_ptr<noisy> np_;\n        unique_ptr<char[]> ptr_;\n    public:\n        autobuf(char const* str);\n        char* get();\n    };\n    ```\n\n    该类有两个成员变量。一个名为 np 的成员是类`noisy`实例的`unique_ptr<>`。这种嘈杂实例的存在会打印一条信息，使得在构建或销毁`autobuf`实例时更容易看到。另一个是一个`unique_ptr<>`实例到一个`char`阵，叫做`ptr_`。现在，`autobuf`有一个默认构造函数和一个名为`get()`的访问器函数，该函数返回一个指向缓冲区的无主指针。`autobuf`的析构函数由编译器自动生成。编译器把它做对了，我们根本不用考虑它。\n\n6.  接下来，定义`autobuf`的两个成员函数。`:`后面的两行是构造函数初始化列表。这为两个成员变量提供了初始值。`np_`获得一个新的动态`noisy`实例，`ptr_`获得一个足够大的`char`缓冲区来保存构造函数的`str`参数:\n\n    ```cpp\n    autobuf::autobuf(char const* str)\n      : np_(make_unique<noisy>()),\n        ptr_(make_unique<char[]>(strlen(str) + 1))\n    {\n        strcpy(ptr_.get(), str);\n    }\n    ```\n\n7.  `get()`函数使用`unique_ptr<>`的`get()`成员函数:\n\n    ```cpp\n    char* autobuf::get() \n    {\n        return ptr_.get();\n    }\n    ```\n\n    返回一个指向缓冲区的无主指针\n8.  在`main()`中，声明一个名为`buffer`的`autobuf`实例。将其初始化为任何方便的文字字符串。使用`buffer`的`get()`成员函数在`buffer`中输出字符串，以返回指向`char`数组的指针:\n\n    ```cpp\n        autobuf buffer(\"my favorite test string\");\n        cout << \"Hello World! \" << buffer.get() << endl;\n    ```\n\n9.  完整的程序转载如下。\n\n    ```cpp\n    #include <iostream>\n    #include <memory>\n    #include <cstring>\n    using namespace std;\n    struct noisy\n    {\n        noisy() { cout << \"constructing noisy\" << endl; }\n        ~noisy() { cout << \"destroying noisy\" << endl; }\n    };\n    class autobuf\n    {\n        unique_ptr<noisy> np_;\n        unique_ptr<char[]> ptr_;\n    public:\n        autobuf(char const* str);\n        char* get();\n    };\n    autobuf::autobuf(char const* str)\n      : np_(make_unique<noisy>()),\n        ptr_(make_unique<char[]>(strlen(str) + 1))\n    {\n        strcpy(ptr_.get(), str);\n    }\n    char* autobuf::get() \n    {\n        return ptr_.get();\n    }\n    int main()\n    {\n        autobuf buffer(\"my favorite test string\");\n        cout << \"Hello World! \" << buffer.get() << endl; \n        return 0;\n    }\n    ```\n\n10.  Compile and run the program. Its output is reproduced here:\n\n    ![Figure 7.6: Output of the program in exercise 54 ](img/C14195_07_06.jpg)\n\n图 7.6:练习 54 中程序的输出\n\n当`main()`开始执行时，`autobuf`的构造函数创建一个新的`noisy`实例，打印输出的第一行。输出语句写输出的第二行，包含`buffer`的内容。然后，行刑离开`main()`，导致`buffer`被摧毁。编译器生成的`buffer`析构函数销毁`np_`(指向`noisy`的智能指针)，删除`noisy`并打印第三行输出，以及`ptr_`(指向`char`数组的智能指针)，删除`char`数组。\n\n在这个简单的例子中，开发人员不需要为`autobuf`编写析构函数似乎并不是什么大事。然而，乘以许多类——其中一些有多个成员变量——会有所不同，因为开发人员不必记得为每个动态变量向每个析构函数添加代码。\n\n## 函数参数和返回值中唯一的 _ptr < >\n\n通常将无主指针作为函数参数传递给函数，因为调用程序中的指针通常在被调用函数的持续时间内存在。A `unique_ptr<>`用作函数参数是危险的。如果实际参数是`unique_ptr<>`实例，函数的形式参数将窃取实际参数的值，使实际参数等于`nullptr`。如果函数的实际参数是一个无主指针，`unique_ptr<>`参数将获得指针的所有权，并在函数退出时将其删除。这几乎从来都不是开发者想要的。相反，将函数的形式参数设为无主指针，并使用`unique_ptr<>`的`get()`成员函数获取无主指针作为函数参数。\n\n`unique_ptr<>`可以在函数返回时使用，表示调用者必须获得返回的动态变量的所有权。\n\n## 练习 55:在函数返回值中使用唯一的 _ptr < >\n\n本练习展示了如何通过向动态变量返回`unique_ptr<>`实例来转移其所有权:\n\n注意\n\n这个练习的完整代码可以在这里找到:[https://packt.live/2OB8ZHm](https://packt.live/2OB8ZHm)。\n\n1.  进入骨架`main()`程序:\n\n    ```cpp\n    #include <iostream>\n    using namespace std;\n    int main()\n    {\n        return 0;\n    }\n    ```\n\n2.  `unique_ptr<>`在`<memory>`表头定义。包括`#include <iostream>`之后的`<memory>`:\n\n    ```cpp\n    #include <memory>\n    ```\n\n3.  定义类`noisy`。我们之前见过这个类:\n\n    ```cpp\n    struct noisy\n    {\n        noisy() { cout << \"constructing noisy\" << endl; }\n       ~noisy() { cout << \"destroying noisy\" << endl; }\n    };\n    ```\n\n4.  创建一个名为`func()`的函数，该函数模拟一个创建指向大型数据结构的自有指针的函数，可能是通过读取文件或接收网络数据包。`func()`不接受任何参数，返回一个`unique_ptr<>`实例:\n\n    ```cpp\n    unique_ptr<noisy> func()\n    {\n        return make_unique<noisy>();\n    }\n    ```\n\n5.  在`main()`中，调用`func()`，捕捉返回值。使用`auto`关键字可以避免查找`func()`返回的指针的确切类型。请注意这种现代 C++ 语法是如何简化的:\n\n    ```cpp\n    auto u1 = func();\n    ```\n\n6.  完成的程序是这样的。\n\n    ```cpp\n    #include <iostream>\n    #include <memory>\n    using namespace std;\n    struct noisy\n    {\n        noisy() { cout << \"constructing noisy\" << endl; }\n       ~noisy() { cout << \"destroying noisy\" << endl; }\n    };\n    unique_ptr<noisy> func()\n    {\n        return make_unique<noisy>();\n    }\n    int main()\n    {\n        auto u1 = func();\n        return 0;\n    }\n    ```\n\n7.  The output of this program is reproduced here. A single instance of `noisy` is created inside `func()`, transferred to `u1`, and then deleted when `main()` returns, signaling the successful transfer of ownership:\n\n    ![Figure 7.7: Output of the program in exercise 55 ](img/C14195_07_07.jpg)\n\n图 7.7:练习 55 中程序的输出\n\n在绝大多数编程情况下，一个指向动态变量的指针是合适的。对于其余的情况，C++ 为共享所有权提供了一个引用计数的`shared_ptr<>`。\n\n# 动态变量的共享所有权\n\n在 C++ 11 之前，标准库中有一个更有限的智能指针`auto_ptr<>`。`auto_ptr<>`模板类的许多限制之一是，它不能用作 C++ 标准库容器类中的元素类型，也不能将动态变量的所有权转移到函数之外。标准库包含一个被称为`shared_ptr<>`的引用计数智能指针类，该类可用于函数参数、返回值和标准库容器。几年来，一些球队专门使用`shared_ptr<>`并禁止使用原始指针。\n\n`shared_ptr<>`的问题是在运行时指令方面比较昂贵。除了`shared_ptr<>`拥有的动态变量外，它还创建了第二个动态变量来保存引用计数，如图*图 7.8* 所示，并在删除最后一个引用时删除引用计数。对内存分配器的每次调用都非常昂贵:\n\n![Figure 7.8: Simplified memory layout of shared_ptr ](img/C14195_07_08.jpg)\n\n图 7.8:共享 ptr 的简化内存布局\n\n递增和递减引用计数的代码使用昂贵的线程安全互锁递增和递减。这比简单的增量或减量慢 10 倍。作为`shared_ptr<>`实例的每个函数参数必须在调用函数时递增，并在函数返回时递减。对于频繁调用的函数，成本会变得很高。`shared_ptr<>`的实现变得更加复杂，以支持很少使用的自定义删除器和`weak_ptr<>`实例，这在本章中没有描述。\n\n## 练习 56:使用 shared_ptr < >\n\n在本练习中，我们创建了几个共享指针，并调用了一个以共享指针为参数并返回共享指针的函数。这个程序的思想是一个函数条件一个动态变量永远不是`nullptr`，所以程序不需要测试`nullptr`:\n\n注意\n\n这个练习的完整代码可以在这里找到:[https://packt.live/2ObtwDb](https://packt.live/2ObtwDb)。\n\n1.  进入骨架`main()`程序:\n\n    ```cpp\n    #include <iostream>\n    using namespace std;\n    int main()\n    {\n        return 0;\n    }\n    ```\n\n2.  `shared_ptr<>`模板在`<memory>`表头定义。包括`#include <iostream>`下面的`<memory>`:\n\n    ```cpp\n    #include <memory>\n    ```\n\n3.  定义一个函数`func()`，该函数以`char`数组的`shared_ptr<>`为参数，并返回一个`shared_ptr<>`到一个`char`数组:\n\n    ```cpp\n    shared_ptr<char[]> func(shared_ptr<char[]> str)\n    {\n    }\n    ```\n\n4.  在`func()`内，测试`str`是否等于`nullptr`。如果`str`为`nullptr` :\n\n    ```cpp\n        if (!str)\n        {\n        }\n    ```\n\n    ，表达式`!str`返回`true`\n5.  如果`str`为`nullptr`，将其值重置为新的一个字符数组。将字符值设置为空终止符`'\\0'` :\n\n    ```cpp\n            str.reset(new char[1]);\n            str[0] = '\\0';\n    ```\n\n6.  在`main()`内部，创建一个`shared_ptr<>`实例到一个名为`null`的`char`数组。`shared_ptr<>`默认构造函数将`null`初始设置为`nullptr` :\n\n    ```cpp\n        shared_ptr<char[]> null;\n    ```\n\n7.  测试`null`是否等于`nullptr`。这次我们通过获取无主指针并将其与`nullptr`进行比较来执行测试，而不是使用表达式`!null`。如果`null`等于`nullptr`，打印消息:\n\n    ```cpp\n        if (null.get() == nullptr)\n            cout << \"null is equal to nullptr\" << endl;\n    ```\n\n8.  以`null`为自变量调用`func()`。创建一个名为`result1`的`auto`变量来接收`func()`返回的值:\n\n    ```cpp\n        auto result1 = func(null);\n    ```\n\n9.  如果`result1`等于`nullptr`，则输出消息:\n\n    ```cpp\n        if (result1.get() == nullptr)\n            cout << \"result1 is equal to nullptr\" << endl;\n    ```\n\n10.  以`result1`为自变量再次调用`func()`。然后，在`result1`中捕捉返回值:\n\n    ```cpp\n        result1 = func(result1);\n    ```\n\n11.  生成的程序如下所示:\n\n    ```cpp\n    #include <iostream>\n    #include <memory>\n    using namespace std;\n    shared_ptr<char[]> func(shared_ptr<char[]> str)\n    {\n        if (!str)\n        {\n            str.reset(new char[1]);\n            str[0] = '\\0';\n        }\n        return str;\n    }\n    int main()\n    {\n        shared_ptr<char[]> null;\n        if (null.get() == nullptr)\n            cout << \"null is equal to nullptr\" << endl;\n        auto result1 = func(null);\n        if (result1.get() == nullptr)\n            cout << \"result1 is equal to nullptr\" << endl;\n        result1 = func(result1);\n        return 0;\n    }\n    ```\n\n12.  Compile and execute the program on a C++ compiler that supports C++ 17\\. Online compilers supporting C++ 17 include **Coliru** and **Tutorialspoint**. Unfortunately, cpp.sh is only a C++ 14 compiler and will not compile this code. The output looks like this:\n\n    ![Figure 7.9: Output of the program in exercise 56  ](img/C14195_07_09.jpg)\n\n图 7.9:练习 56 中程序的输出\n\n这个程序看起来很简单，但是有很多事情在进行。我们无法通过仪器`shared_ptr`看到它在运行，所以下面是程序执行的描述:\n\n*   创建`shared_ptr`实例`null`。它的指针被设置为`nullptr`。因为它不指向动态变量，所以动态变量或引用计数不需要分配。\n*   通过获取原始指针并验证其等于`nullptr`，证明`null`为空。\n*   呼叫`func()`。实际论点`null`被复制到形式论点`str`中。既然`null`等于`nullptr`，`str`也包含`nullptr`。\n*   测试`str`是否等于`nullptr`。既然是，创建一个新的动态`char`数组并将`str`重置为该值。`str`还创建一个新的动态变量来保存引用计数，并将引用计数设置为 1。\n*   将`str`拥有的动态变量设置为空终止符`'\\0'`。\n*   返回`str`。现在，`str`被复制到`result1`中，它将指向动态字符数组的指针和引用计数复制到`str`中，并将其引用计数增加到 2，因为`str`和`result1`都指向动态数组。然后调用`str`的析构函数，将`str`的引用计数减 1，函数返回。\n*   测试`result1`是否等于`nullptr`。不等于`nullptr`因为刚刚设置了一个一字动态数组，所以什么都不打印。\n*   现在再次调用`func()`。\n*   `result1`被复制构建成`str`。由于`result1`不是`nullptr`，其参考计数增加到 2。\n*   `str`测试为`nullptr`。由于`str`不是`nullptr`(是单字符数组)，测试失败。\n*   返回`str`。`str`被复制到`result1`中。请记住`str`和`result1`已经指向相同的`char`数组和相同的参考计数。因此，首先，将要分配的对象的引用计数增加到 3。然后，将要被覆盖的对象的引用计数减为 2。然后，`str`被破坏，参考计数递减到 1。由于没有引用计数变为 0，因此不会删除任何内容。\n*   现在`main()`回归。`result1`被销毁，因此它所拥有的对象的引用计数递减为 0。`char`阵被删除。参考计数被删除。\n*   `null`被摧毁。已经等于`nullptr`了，所以什么都没发生。\n\n## make_shared()\n\n`make_shared()`是一个模板函数，它创建一个动态变量，并将其分配给适当类型的`shared_ptr<>`实例。正如`unique_ptr<>`和`shared_ptr<>`在其定义中隐藏了`delete`表达式一样，`make_shared()`对`new`表达式也是如此:\n\n![Figure 7.10: Simplified memory layout of a dynamic object and reference count after make_shared() ](img/C14195_07_10.jpg)\n\n图 7.10:make _ shared()后动态对象和引用计数的简化内存布局\n\n`make_shared()`有额外的注意能力。它为单个对象中的动态变量和引用计数分配存储空间。由于创建动态对象是最昂贵的 C++ 操作，因此减少分配数量可以显著提高大量使用`shared_ptr<>`的代码的性能。\n\n`make_shared()`有局限性。从 C++ 17 开始不能创建动态数组，尽管它是为 C++ 20 提出的。无法指定删除程序。\n\n## 练习 57:使用 make_shared()\n\n本练习使用`make_shared()`构建几个动态变量:\n\n注意\n\n这个练习的完整代码可以在这里找到:[https://packt.live/2OyeFBO](https://packt.live/2OyeFBO)。\n\n1.  进入骨架`main()`程序:\n\n    ```cpp\n    #include <iostream>\n    using namespace std;\n    int main()\n    {\n        return 0;\n    }\n    ```\n\n2.  `make_shared()`模板功能在`<memory>`表头定义。包括`#include <iostream>`之后的`<memory>`:\n\n    ```cpp\n    #include <memory>\n    ```\n\n3.  输入类`noisy`的定义:\n\n    ```cpp\n    struct noisy\n    {\n        noisy() { cout << \"constructing noisy\" << endl; }\n       ~noisy() { cout << \"destroying noisy\" << endl; }\n    };\n    ```\n\n4.  在`main()`内部，声明一个名为`u1`的`shared_ptr<noisy>`实例，并将其初始化为一个新的`noisy`实例:\n\n    ```cpp\n        shared_ptr<noisy> u1(new noisy);\n    ```\n\n5.  声明一个名为`u2`的`shared_ptr<noisy>`实例。将呼叫返回的值分配给`make_shared<noisy>()` :\n\n    ```cpp\n        shared_ptr<noisy> u2 = make_shared<noisy>();\n    ```\n\n6.  声明一个名为`u3`的`shared_ptr<noisy>`实例，并为其分配`u2`。`noisy`实例的所有权由`u2`和`u3`共享:\n\n    ```cpp\n        shared_ptr<noisy> u3 = u2;\n    ```\n\n7.  释放`u2`的所有权。现在，`u3`是唯一拥有者:\n\n    ```cpp\n        u2.reset();\n    ```\n\n8.  完成的程序是这样的。\n\n    ```cpp\n    #include <iostream>\n    #include <memory>\n    using namespace std;\n    struct noisy\n    {\n        noisy() { cout << \"constructing noisy\" << endl; }\n       ~noisy() { cout << \"destroying noisy\" << endl; }\n    };\n    int main()\n    {\n        shared_ptr<noisy> u1(new noisy);\n        shared_ptr<noisy> u2 = make_shared<noisy>();\n        shared_ptr<noisy> u3 = u2;\n        u2.reset();\n        return 0;\n    }\n    ```\n\n9.  Compile and run the completed program. Its output looks like this:\n\n    ![Figure 7.11: Output of program in exercise 56 ](img/C14195_07_11.jpg)\n\n图 7.11:练习 56 中程序的输出\n\n不出所料，`noisy`的两个实例被创建，两个被销毁。\n\n## 活动 7:使用动态变量存储一本书的单词\n\n在*第 6 章*、*动态变量*的开头，我们看了一个不使用动态变量的数据结构，用来存储一本书里的所有单词。不可能存储每本书，要么是因为单个单词太长，要么是因为单词数量太多，或者两者都有。有了动态变量，这些问题就可以克服。\n\n在本练习中，您将实现一个程序来存储不受上述任何一个限制的书的单词。这个问题类似于书籍阅读器、网络浏览器和文本编辑器设计中面临的问题。\n\n程序必须能够精确地重构输入。也就是说，它需要记录每个单词周围的空格数，并将每行的单词收集在一起。该程序应具有完全按照输入打印书籍的功能。\n\n图书阅读器程序的输入可能是一个包含相关文本的文件。然而，对于这个活动，程序的输入是一个指向文字字符串的指针数组。文字字符串由由一个或多个空格或字符串终止符(`'\\0'`)分隔的非空格单词组成，每个字符串代表一行书。\n\nC++ 标准库定义的容器类可能很容易解决这个问题，但是请不要使用它们，即使您已经知道它们。`std::string`也会有帮助，但也不要用那个。这个活动的目的是测试你对指针和动态变量的知识。\n\n这个示例输入，来自哈姆雷特的简短引用，足以测试您的设计，而不会增加您的耐心:\n\n```cpp\n    \"What a piece of work is man, \"\n    \"   How noble in reason, how infinite in faculty,\"\n    \"In form and moving how express and admirable, \"\n    \"   In action how like an Angel, In apprehension how like a god.\"\n    \"The beauty of the world.  The paragon of animals.\"\n```\n\n注意\n\n这个活动的完整代码可以在这里找到:[https://packt.live/339ahi6](https://packt.live/339ahi6)。\n\n以下是一些帮助您完成活动的提示:\n\n1.  这是一个很大的任务，但是可以分解成几个部分。您需要一个数据结构来保存一个包含缓冲区的单词，该缓冲区包含单词字符串和空格数。它必须能够输出单词和空格。你可以把它们放到`cout`上，但是把它们放到绳子上可以让测试变得更容易。\n2.  然后，您需要一个可以容纳一行单词的数据结构。它还必须能够输出该行。最后，你需要一个可以容纳整本书的行的数据结构，你需要输出这本书。使你创建的类彼此相似将有助于你在这个作业中找到结构，这将使你的工作进行得更快。\n3.  每个单词都可以存储，以及该单词后面的空格数。如何表示前导空格？考虑一个零长度的单词。\n4.  应该用什么数据结构来保存一个单词？单词的文本应该是`char`的动态数组。尾随空格的数量可以是`int`。指向动态数组的智能指针将使删除变得容易。\n5.  应该使用什么数据结构来保持一条线？单词链表是一个明显的选择，所以下一个指针应该被添加到另外两个保存单词的字段中。列表的头节点应该是一个包含原始指针的类，该指针带有删除一行中所有单词的析构函数。\n6.  应该使用什么数据结构来保存整本书？链表行将是一个合适的选择。可以使用动态数组，但是如果对数组大小的最初猜测是错误的，那么就必须有代码将数组复制到更大的数组中。书的头节点应该是一个包含原始指针的类，该指针带有删除书所有行的析构函数。您将希望向 line 类添加下一个指针。\n7.  应该如何将空终止的字符串转换为单词列表？分隔字符串时只需要三个字符。每个单词后面都有空格。空终止符位于行尾。在循环中，您可以设置一个指向单词开头的指针，然后步进第二个指针寻找空格或空终止符。这些指针之间的区别在于单词的大小。别忘了给空终止符加 1。\n8.  You will need a function to produce a string representing the line from the list of words. You will need to know how many characters to allocate. You can find this out by counting the size of the word, plus the number of spaces for each word, and adding 1 for a null terminator at the end of the line. Then, you can copy words and spaces into this array. Since you need to know the length of each word, it is appropriate to store the number of characters of each word as another `int` in the word class.\n\n    注意\n\n    这个活动的解决方案可以在第 534 页找到。\n\n# 总结\n\n指针和动态变量是基本 C++ 工具包中最有价值的两个工具。理解它们并很好地使用它们是值得的。我们了解到，C++ 并不强制执行任何关于动态变量创建或删除的规则。开发人员可以记录动态变量的所有权，为这种混乱带来一些秩序。我们了解到，管理动态变量所有权的一个强大方法是通过使用智能指针，它将动态变量与一个普通变量联系起来，该变量在一个定义明确的地方被销毁。C++ 是一种强大的编程语言，因为它提供了一系列选择，从最基本的不安全编程到昂贵的自动化智能库类。\n\n在下一章中，我们将深入探讨类类型和面向对象编程。"
  },
  {
    "path": "docs/cpp-workshop/08.md",
    "content": "# 八、类和结构\n\n概观\n\n本章借助实例和练习介绍了结构和类的基础知识。到本章结束时，您将能够描述类、结构和联合类型之间的区别，以及如何使用构造函数和析构函数正确初始化和处理它们。\n\n# 简介\n\nC++ 是一门广泛的语言，你要学习的每一个特性或范例都需要深入的知识来释放它的全部潜力。C++ 有两种类型:内置类型和类类型。内置类型是构成语言核心的任何类型，例如`int`、`float`和`char`。类类型可以被认为是用户定义的类型；这些是我们通过声明类、结构、联合等创建的类型。C++ 标准库中的特性和类型(如向量和队列)都是使用 C++ 构建的类类型，这显示了该语言的真正力量及其创建感觉像内置类型一样易于使用的类型的能力。类是面向对象编程的基础，更详细地介绍它们将有助于为您提供所需的基础。拥有用可靠的接口创建健壮类型的能力对于成为一个强大的 C++ 程序员来说是至关重要的。\n\n在*第 6 章*、*动态变量*中，您学习了构造函数和析构函数以及`new`和`delete`以及`new[]`和`delete[]`的使用。在本章中，您将学习如何使用构造函数初始化类成员变量，以及如何使用析构函数在类被销毁时进行清理。此外，您将了解复制构造函数和赋值运算符，以及它们之间的相互关系。最后，您将学习如何声明和使用`union`类型——封装数据的另一种方式。\n\n# 类与结构\n\n在 C++ 中，您可以选择将对象声明为结构还是类。两者都可以利用成员函数和继承，并且混合了公共、受保护和私有字段(后面几章将详细介绍)。类和结构的主要区别在于结构的成员变量和方法是公共的，而类的成员变量和方法是私有的。在下面的示例中，声明了两种等效的数据类型，以显示当类默认为私有时，结构如何将其成员默认为公共的(不使用公共、私有或受保护的关键字):\n\n```cpp\nstruct MyStruct \n{\n    int myInt = 0; // this defaults to public \n};\nclass MyClass \n{\n    int myInt = 0; // this defaults to private \n};\nint main() \n{\n    MyStruct myStruct;\n    MyClass myClass;\n    // allowed - public \n    int i = myStruct.myInt;\n    // not allowed - private - compiler error \n    int j = myClass.myInt;\n    return 0;\n}\n```\n\n除了这个细节之外，这些对象是相同的。C++ 中结构的实例与类的实例完全相同。在编译代码中，它们是相同的；内存使用、访问时间和内存对齐完全相同，并且没有任何开销与其中一个相关联。传统上，结构被用作**普通旧数据** ( **POD** )类型，以帮助向后兼容 C 库。POD 类型是没有构造函数、析构函数或虚拟成员函数的类或结构。在这种情况下，经常使用结构来表示这种意图，即使它从根本上没有任何区别。\n\n# 工会\n\n类和结构将数据成员存储在单独的内存块中，而联合类型只分配足够的内存来存储最大的数据成员。联盟的所有成员共享相同的内存位置；因此，如果不同的数据类型在内存中布局相同，则可以使用一大块分配的内存来访问不同的数据类型。联合是一种你不常见到的数据类型，但了解一下它们是如何工作的是值得的。一个有用的优势是能够以一种格式读取数据，然后以另一种格式访问它。\n\n以下示例显示了名为`Backpack`的联合类型。它有一个由四个整数组成的数组和一个名为`data`的结构，该结构有四个`int`成员。仔细观察如何使用数组和结构设置和读取数据:\n\n```cpp\nExample08_1.cpp\n1  #include <iostream>\n2  \n3  using namespace std;\n4  \n5  union Backpack\n6  {\n7      int contents[4];\n8      struct\n9      {\n10         int food, water, key, flashlight;\n11     }\n12     data;\n13 };\n14\n15 void DisplayContents(Backpack& backpack)\n16 {\n17     cout << \"Has Food = \" << backpack.data.food << endl;\n18     cout << \"Has Water = \" << backpack.data.water << endl;\n19     cout << \"Has Key = \" << backpack.data.key << endl;\n20     cout << \"Has Flashlight = \" << backpack.data.flashlight << endl;\n21 }\n22\n23 void UpdateBackpack(Backpack& backpack, int contents[4])\n24 {\n25     for(int i = 0; i < 4; i++)\n26     {\n27         backpack.contents[i] = contents[i] > backpack.contents[i]\n28         ? contents[i] : backpack.contents[i];\n29     }\n30 }\n31\n32 void RemoveFromBackpack(Backpack& backpack, int idx)\n33 {\n34     backpack.contents[idx] = 0;\n35 }\nThe complete example can be found here: https://packt.live/362LT3j\n```\n\n正如所展示的，联合可以允许以不同的方式存储和访问数据。这并不总是最好的主意，因为我们不能保证整数结构的大小与整数数组的大小相同。如果工会突然出现在你需要做的工作中，请记住这一点。\n\n# 构造函数和析构函数\n\n构造函数是用于初始化对象的类函数。每当创建对象时，都会调用构造函数。相反，每当对象被销毁时，就调用析构函数。构造函数不同于普通的成员函数，因为它们与它们所属的类同名。它们没有返回类型，如前所述，每当创建它们所属的类的实例时，都会自动调用它们。\n\n## 施工人员\n\n本节将介绍三种不同类型的构造函数:\n\n*   默认构造函数\n*   参数化构造函数\n*   复制构造函数\n\n这些类型的构造函数将通过创建一个简单的歌曲曲目列表类来依次覆盖，该类保存关于特定曲目的各种信息。\n\n## 默认构造函数\n\n默认构造函数是不带参数的构造函数，或者是所有参数都有默认值的构造函数。让我们看看一个非常简单的类，它有几个成员变量。这个类叫做`Track`，它代表一首音乐曲目(换句话说，一首歌):\n\n```cpp\n#include <iostream>\n#include <string>\nusing namespace std;\nclass Track\n{\npublic:\n    float lengthInSeconds;\n    string trackName;\n};\n```\n\n下面是一个名为`Track`的类的声明，其中包含一些可能与它相关的数据:它的名称和长度。请注意，还没有为此类定义构造函数；至少，初始化类的实例需要一个默认构造函数。\n\n由于在前面的`Track`类声明中没有显式定义默认构造函数，编译器会隐式为我们生成一个。下面的代码将创建一个`Track`类的实例:\n\n```cpp\n#include <iostream>\n#include <string>\nusing namespace std;\nclass Track \n{\npublic:\n    float lengthInSeconds;\n    string trackName;\n};\nint main() \n{\n    Track track;\n    cout << \"Track Name = \" << track.trackName << endl;\n    cout << \"Track Length = \" << track.lengthInSeconds << endl;\n    return 0;\n}\n```\n\n运行此代码将输出一个空字符串和一个随机浮点值，如下所示:\n\n![Figure 8.1: Output of the code ](img/C14195_08_01.jpg)\n\n图 8.1:代码的输出\n\n`cpp.sh`的编译器将我们的浮点值初始化为 0，然而我们不能总是保证不同的编译器都是这样。原因是编译器生成的默认构造函数会将数据成员初始化为默认值；在字符串是类类型的情况下(后面章节将详细介绍)，它有自己的默认构造函数，初始化为`empty`，在浮点的情况下，任何随机浮点值。这种行为显然不是有意针对哪怕是一个默认的`Track`对象；毕竟，谁听说过有一首曲目是`-4.71077e-33`(它是随机的，记得吗)秒长，除了一些晦涩的艺术歌曲之外，什么都没有？\n\n现在，让我们在下面的练习中纠正这一点，并创建一个显式的默认构造函数，将成员变量初始化为“合理的”或者至少是合乎逻辑的。\n\n## 练习 58:定义默认构造函数\n\n默认构造函数与类同名，没有参数，也没有返回类型。在本练习中，我们将创建一个公共的构造函数，这样我们就可以从类外部调用它:\n\n注意\n\n这个练习的完整代码可以在这里找到:[https://packt.live/2KG6Icx](https://packt.live/2KG6Icx)。\n\n1.  首先，我们可以在`Track`类的`public`关键字下创建构造函数的存根:\n\n    ```cpp\n    Track()\n    {\n    }\n    ```\n\n2.  我们现在必须填写我们的构造函数，以便在构造类后将成员变量设置为合理的值。我们将轨道长度设置为`0`，轨道名称设置为`not set`:T2\n3.  现在，我们可以使用上一个示例中的`main`函数来测试我们的构造函数是否被调用:\n\n    ```cpp\n    int main()\n    {\n        Track track;\n\n        cout << \"Track Name = \" << track.trackName << endl;\n        cout << \"Track Length = \" << track.lengthInSeconds << endl;\n        return 0;\n    }\n    ```\n\n4.  完整的代码如下:\n\n    ```cpp\n    #include <iostream>\n    #include <string>\n    using namespace std;\n    class Track\n    { \n    public:\n        float lengthInSeconds;\n        string trackName;\n        Track ()\n        {\n            lengthInSeconds = 0.0f; \n            trackName = \"not set\"; \n        }\n    };\n    int main()\n    {\n        Track track;\n        cout << \"Track Name = \" << track.trackName << endl;\n        cout << \"Track Length = \" << track.lengthInSeconds << endl;\n        return 0;\n    }\n    ```\n\n5.  运行代码。从下面可以看出，我们的输出反映了构造函数中的值:\n\n![Figure 8.2: Output reflecting the value from the constructor ](img/C14195_08_02.jpg)\n\n图 8.2:反映构造函数值的输出\n\n这更有意义，并且可以更好地控制`Track`实例的默认初始化。默认构造函数的定义还声明，它可以使用具有默认值的参数来定义。这种类型的构造函数既可以用作默认构造函数，也可以用作参数化构造函数，因此应该与参数化构造函数一起使用。\n\n## 参数化构造函数\n\n构造函数可以像任何其他函数一样接受参数。参数化构造函数是至少接受一个参数的构造函数。这是一个极其重要的概念，在 C++ 中你会不断利用它。当前的`Track`类构造函数如下所示:\n\n```cpp\nTrack()\n{\n    lengthInSeconds = 0.0f;\n    trackName = \"not set\";\n}\n```\n\n每当创建`Track`的实例时，其成员变量将被设置为该构造函数中的值。显然，这不是特别有用；歌曲总是有不同的名字和长度。参数化构造函数允许我们通过将参数传递给构造函数来设置`Track`对象的成员变量在初始化时应该是什么。\n\n您会记得，只要所有参数都有默认值，默认构造函数也可以接受参数。这实现了一种混合方法，其中构造函数可以用作默认构造函数，或者根据情况将参数传递给它。\n\n## 练习 59:定义参数化构造函数\n\n参数化构造函数本质上与默认构造函数具有相同的语法，不同之处在于，它自然采用参数。让我们看看通过向`Track`构造函数添加参数来创建参数化构造函数:\n\n注意\n\n这个练习的完整代码可以在这里找到:[https://packt.live/35kthvj](https://packt.live/35kthvj)。\n\n1.  将现有的`Track`构造函数写入编译器:\n\n    ```cpp\n    Track()\n    {\n        lengthInSeconds = 0.0f;\n        trackName = \"not set\";\n    }\n    ```\n\n2.  首先增加一些可以设置`lengthInSeconds`、`trackName`的参数；我们需要一个`float`参数和一个`string`参数:\n\n    ```cpp\n    Track(float lengthInSeconds, string trackName)\n    {\n    ```\n\n3.  此时，我们希望更清楚哪些变量是我们的类成员，哪些是传入的参数。为此，我们将在变量名前加上`m_`(在变量前加上`m_`是将变量表示为成员变量的常见方式):\n\n    ```cpp\n        // m_ prefix added to member variables, to avoid naming conflicts     //with parameter names\n        float m_lengthInSeconds;\n        string m_trackName;\n    ```\n\n4.  最后，我们可以将这些成员变量设置为传入参数的值:\n\n    ```cpp\n    Track(float lengthInSeconds, string trackName)\n    {\n        m_lengthInSeconds = lengthInSeconds;\n        m_trackName = trackName;\n    }\n    ```\n\n5.  现在，让我们用一个新的`main`函数来测试一下。我们将使用与默认构造函数练习相同的代码，但是现在，当创建我们的`Track`实例时，我们必须使用参数化构造函数。我们不再有默认构造函数，因为我们已经定义了自己的构造函数，编译器不会为我们生成默认值:\n\n    ```cpp\n    int main()\n    {\n        Track track(200.0f, \"Still Alive\");\n        cout << \"Track Name = \" << track.m_trackName << endl;\n        cout << \"Track Length = \" << track.m_lengthInSeconds << endl;\n        return 0;\n    }\n    ```\n\n6.  完整的代码如下:\n\n    ```cpp\n    #include <iostream>\n    #include <string>\n    using namespace std;\n    class Track\n    { \n    public:\n        // m_ prefix added to member variables, to avoid naming conflicts     //with parameter names\n        float m_lengthInSeconds;\n        string m_trackName;\n        Track(float lengthInSeconds, string trackName)\n        {\n            m_lengthInSeconds = lengthInSeconds;\n            m_trackName = trackName;\n        }\n    };\n    int main()\n    {\n        Track track(200.0f, \"Still Alive\");\n        cout << \"Track Name = \" << track.m_trackName << endl;\n        cout << \"Track Length = \" << track.m_lengthInSeconds << endl;\n        return 0;\n    }\n    ```\n\n7.  运行程序。程序应该分别输出`200`和`Still Alive`的长度和名称，如下图所示:\n\n![Figure 8.3: Track name and length output ](img/C14195_08_03.jpg)\n\n图 8.3:轨道名称和长度输出\n\n参数化构造函数可以有默认值。这意味着我们可以像默认构造函数一样使用它们(没有参数)。这种默认值参数化构造函数在传递给构造函数的值大部分时间都是相同的情况下非常有用，但是我们希望在需要时可以选择更改它。您可以混合和匹配默认和非默认参数，但是任何默认参数都必须在非默认参数之后。下面是一个带有默认参数的`Track`类构造函数的例子:\n\n```cpp\n    // set default values to parameters\n    Track(float lengthInSeconds = 0.0f, string trackName = \"not set\")\n    {\n        m_lengthInSeconds = lengthInSeconds;\n        m_trackName = trackName;\n    }\n```\n\n我们现在将转向更高级的构造函数。在编写健壮的类时，它们同样重要，这些类在所有情况下的行为都和我们期望的一样。\n\n## 复制构造函数\n\n复制构造函数是创建现有类实例副本的构造函数。除了默认构造函数之外，如果没有定义，编译器还会自动为每个类类型创建一个复制构造函数。\n\n复制构造函数在许多情况下都会被调用，但是要记住的最重要的一点是，当一个变量或对象从另一个对象创建时，使用复制构造函数。复制构造函数创建现有对象的副本，因此称为复制构造函数。\n\n以我们的`Track`类为例，复制构造函数的语法如下:\n\n```cpp\n    Track(const Track& track)\n    {\n        lengthInSeconds = track.lengthInSeconds;\n        trackName = track.trackName;\n    }\n```\n\n查看这个语法，我们可以看到，复制构造函数的声明方式几乎与前面介绍的构造函数相同，但有一个重要的区别；它引用了一个`const`参数。使参数`const`确保复制构造函数不会改变传入的参数。对参数的引用用于复制构造函数的情况，这是调用复制构造函数的情况之一的结果；当对象通过值传递给函数时，将调用复制构造函数。\n\n因此，如果参数不是引用，那么将它传递到复制构造函数将需要调用复制构造函数来进行复制。这个复制构造函数将创建一个副本，该副本将继续调用复制构造函数，以此类推(一个无限循环)。\n\n## 浅拷贝或深拷贝\n\n如前所述，编译器将为我们的类型创建一个复制构造函数。这个编译器生成的复制构造函数很可能与上一节示例中显示的相同。这称为浅拷贝，它贯穿每个成员变量，并为它们分配当前被拷贝对象的相应值。这种编译器生成的复制构造函数在很多情况下可能都很好，我们不必自己定义一个。当从已存在的对象创建新对象时，将调用复制构造函数。\n\n以下示例显示了将调用复制构造函数的另一种情况(在本例中，是编译器生成的复制构造函数):\n\n```cpp\n#include <iostream>\n#include <string>\nusing namespace std;\nclass Track\n{\npublic:\n    Track(float lengthInSeconds = 0.0f, string trackName = \"not set\")\n    {\n          m_lengthInSeconds = lengthInSeconds;\n          m_trackName = trackName;\n    }\n    // m_ prefix added to member variables, to avoid naming conflicts with \n    //parameter names\n    float m_lengthInSeconds;\n    string m_trackName;\n};\nint main()\n{\n    Track track(200.0f, \"Still Alive\");\n    Track track2 = track; // copy constructor is called\n    cout << \"Track Name = \" << track.m_trackName << endl;\n    cout << \"Track Length = \" << track.m_lengthInSeconds << endl;\n    cout << \"Track Name = \" << track2.m_trackName << endl;\n    cout << \"Track Length = \" << track2.m_lengthInSeconds << endl;\n    return 0;\n}\n```\n\n前面的代码应该输出以下内容:\n\n![Figure 8.4: Output from the compiler-generated copy constructor ](img/C14195_08_04.jpg)\n\n图 8.4:编译器生成的复制构造函数的输出\n\n`track2`对象是从`track`对象创建的；编译器生成的复制构造函数创建一个浅层副本。对象的浅拷贝拷贝所有成员。当所有成员都是值时，这通常很好。那么，什么时候肤浅的复制还不够呢？当一个类有动态分配的内存时，通常需要深度复制。\n\n当对指向动态内存的指针进行浅拷贝时，只拷贝指针，而不拷贝指针指向的内存。`Track`类可以有一个可播放的片段样本，可能是几秒钟的声音。为了简洁起见，假设我们可以将这个可播放片段的数据存储在一个字符数组中，由其他一些声音软件来解析和播放。以下是具有此概念的示例`Track`类:\n\n```cpp\n#include <iostream>\n#include <string>\n#include <cstring>\nusing namespace std;\nclass Track \n{\npublic:\n    Track(float lengthInSeconds = 0.0f, string trackName = \"not set\", const \n    char * data = NULL) \n    {\n        m_lengthInSeconds = lengthInSeconds;\n        m_trackName = trackName;\n        // create the sample clip from data \n        m_dataSize = strlen(data);\n        m_data = new char[m_dataSize + 1];\n        strcpy(m_data, data);\n    }\n    // definitely need a destructor to clean up the data \n   ~Track() \n    {\n        delete[] m_data;\n    }\n    // m_ prefix added to member variables, to avoid naming conflicts with\n    //parameter names \n    float m_lengthInSeconds;\n    string m_trackName;\n    // sample clip data \n    int m_dataSize;\n    char * m_data;\n};\nint main() \n{\n    Track track(200.0f, \"Still Alive\",     \"f651270d6011098375db09912b03e5e7\");\n    Track track2 = track;\n    cout << \"Track 1\" << endl;\n    cout << \"Track Name = \" << track.m_trackName << endl;\n    cout << \"Track Length = \" << track.m_lengthInSeconds << endl;\n    cout << \"Track Data = \" << track.m_data << endl;\n    cout << endl;\n    cout << \"Track 2\" << endl;\n    cout << \"Track Name = \" << track2.m_trackName << endl;\n    cout << \"Track Length = \" << track2.m_lengthInSeconds << endl;\n    cout << \"Track Data = \" << track2.m_data << endl;\n    return 0;\n}\n```\n\n前面的代码将产生以下输出:\n\n![Figure 8.5: Shallow copy ](img/C14195_08_05.jpg)\n\n图 8.5:浅拷贝\n\n此时的类还在使用编译器生成的复制构造函数，这意味着`track2`是`track`的浅拷贝。这里的问题是，浅拷贝只是拷贝指针的地址。换句话说，`track`和`track2`的`m_data`变量都指向同一个内存地址。这可以通过向`Track`类添加额外的功能来演示，以允许通过函数更改`m_data`变量，如以下代码片段所示:\n\n```cpp\n#include <iostream>\n#include <string>\n#include <cstring>\nusing namespace std;\nclass Track \n{\npublic:\n    // added additional artist name constructor parameter \n    Track(float lengthInSeconds = 0.0f, string trackName = \"not set\",           string artistName = \"not set\", const char * data = NULL) \n    {\n        m_lengthInSeconds = lengthInSeconds;\n        m_trackName = trackName;\n        m_artistName = artistName;\n        // create the sample clip from data \n        m_dataSize = strlen(data);\n        m_data = new char[m_dataSize + 1];\n        strcpy(m_data, data);\n        }\n   ~Track() \n    {\n        delete[] m_data;\n    }\n    void SetData(float lengthInSeconds = 0.0f, string trackName = \"not                  set\", const char * newData = NULL) \n    {\n        m_lengthInSeconds = lengthInSeconds;\n        m_trackName = trackName;\n        // delete the array so it can be recreated \n        delete[] m_data;\n        // create the sample clip from data \n        m_dataSize = strlen(newData);\n        m_data = new char[m_dataSize + 1];\n        strcpy(m_data, newData);\n    }\n    // m_ prefix added to member variables, to avoid naming conflicts with\n    //parameter names \n    float m_lengthInSeconds;\n    string m_trackName;\n    // additional artist name string member variable \n    string m_artistName;\n    // sample clip data \n    int m_dataSize;\n    char * m_data;\n};\n```\n\n一个合理的步骤可能是允许创建`Track`对象，然后创建具有相同艺术家姓名的这些对象的副本，以便创建一个分类相册。新增的`SetData`功能以新的长度、曲目名称和可播放的片段数据作为参数，如果一个新的曲目只是另一个的副本，那么就不再需要在每个曲目上设置艺术家的名字。真是天才。下面的片段展示了实践中的这个想法:\n\n```cpp\nint main() \n{\n    Track track(200.0f, \"Still Alive\", \"GlaDos\",    \"f651270d6011098375db09912b03e5e7\");\n    // copy the first track with the artist name \n    Track track2 = track;\n    // set the new needed data \n    track2.SetData(300.0f, \"Want You Gone\",     -\"db6fd7d74393b375344010a0c9cc4535\");\n    cout << \"Track 1\" << endl;\n    cout << \"Artist = \" << track.m_artistName << endl;\n    cout << \"Track Name = \" << track.m_trackName << endl;\n    cout << \"Track Length = \" << track.m_lengthInSeconds << endl;\n    cout << \"Track Data = \" << track.m_data << endl;\n    cout << endl;\n    cout << \"Track 2\" << endl;\n    cout << \"Artist = \" << track2.m_artistName << endl;\n    cout << \"Track Name = \" << track2.m_trackName << endl;\n    cout << \"Track Length = \" << track2.m_lengthInSeconds << endl;\n    cout << \"Track Data = \" << track2.m_data << endl;\n    return 0;\n}\n```\n\n前面的代码片段应该会产生以下输出:\n\n![Figure 8.6: Updated track data for a given artist using the SetData function  ](img/C14195_08_06.jpg)\n\n图 8.6:使用设置数据功能更新给定艺术家的轨迹数据\n\n不幸的是，如果我们不同时添加一个显式的复制构造函数，这个天才的想法有一个致命的缺陷。请注意，虽然`track2`上的`m_data`变量确实发生了变化，但这也影响了复制的`track`对象上的`m_data`变量，因为它们指向同一个地方。当程序完成并调用两个轨道的析构函数，试图释放已经被破坏的内存时，这将导致运行时错误。这就是所谓的双自由误差。以下是一个看似无害的功能:\n\n```cpp\nvoid PrintTrackName(Track track)\n{\n    cout << \"Track Name = \" << track.m_trackName << endl;\n}\n```\n\n当对象通过值传递给函数时会发生什么？\n\n复制构造函数被调用。调用此函数时，其值被打印的`Track`对象实际上是一个局部变量，它是传入的`Track`对象的副本，一旦这个局部变量超出范围，它的析构函数将被调用。`Track`类删除其析构函数中的`m_data`数组，并且由于`Track`类没有正确执行深度复制的用户定义的复制构造函数，因此它删除传入对象使用的相同的`m_data`变量。下面是一个超出范围的变量示例:\n\n```cpp\nvoid PrintTrackName(Track track)\n{\n    cout << \"Track Name = \" << track.m_trackName << endl;\n}\nint main()\n{\n    Track track(200.0f, \"Still Alive\", \"GlaDos\",     \"f651270d6011098375db09912b03e5e7\");\n    PrintTrackName(track);\n    cout << \"Track 1\" << endl;\n    cout << \"Artist = \" << track.m_artistName << endl;\n    cout << \"Track Name = \" << track.m_trackName << endl;\n    cout << \"Track Length = \" << track.m_lengthInSeconds << endl;\n    cout << \"Track Data = \" << track.m_data << endl;\n    return 0;\n}\n```\n\n前面的代码将产生以下输出:\n\n![Figure 8.7: Output in the case when an object is passed by value  ](img/C14195_08_07.jpg)\n\n图 8.7:通过值传递对象时的输出\n\n由于函数通过值传递给打印轨道函数，然后超出范围，轨道中的数据被删除。这两个问题都可以通过添加执行深度复制的复制构造函数来解决。\n\n我们需要一种方法来正确处理我们动态分配的内存的副本，我们知道编译器生成的副本构造函数不会为我们这样做；我们需要自己写。我们可以从查看我们的`Track`类如何在其通常的构造函数中构造自己开始。以下是*浅拷贝或深拷贝*部分中的示例中概述的使用动态分配数据的`Track`构造函数:\n\n```cpp\n        // added additional artist name constructor parameter\n        Track(float lengthInSeconds = 0.0f, string trackName = \"not set\",               string artistName = \"not set\", const char* data = NULL)\n        {\n            m_lengthInSeconds = lengthInSeconds;\n            m_trackName = trackName;\n            m_artistName = artistName;\n            // create the sample clip from data\n            m_dataSize = strlen(data);\n            m_data = new char[m_dataSize + 1];\n            strcpy(m_data, data);\n        }\n```\n\n现在我们已经学习了几个复制构造函数的例子，我们将在下面的练习中通过定义一个复制构造函数来实现我们的学习。我们将使用前面的代码片段作为参考，然后在此基础上进行构建。\n\n## 练习 60:定义复制构造函数\n\n在本练习中，我们将定义一个复制构造函数。为此，我们可以使用前面代码片段中的构造函数作为引用，但使用传递到`Track`对象中的新构造函数的值:\n\n注意\n\n练习的完整代码可以在这里找到:[https://packt.live/2rX6ozE](https://packt.live/2rX6ozE)。\n\n1.  首先，我们创建复制构造函数的存根。您会记得，我们需要将对`Track`对象的`const`引用传递给我们的复制构造函数:\n\n    ```cpp\n    Track(const Track& track)\n    {\n    }\n    ```\n\n2.  现在，我们可以将成员变量分配给传入的`Track`对象的值，方式类似于常规构造函数:\n\n    ```cpp\n    Track(const Track& track)\n    {\n        // these can be shallow copied\n        m_lengthInSeconds = track.m_lengthInSeconds;\n        m_trackName = track.m_trackName;\n        m_artistName = track.m_artistName;\n        m_dataSize = track.m_dataSize;\n    }\n    ```\n\n3.  现在，我们不能只将数据数组分配给磁道数据数组，因为正如我们所讨论的，这只会复制指针地址，并导致两个数据数组指向同一个位置。因此，我们必须使用`new[]`初始化数据数组(我们已经从`m_dataSize`中的存储值知道了大小):\n\n    ```cpp\n    Track(const Track& track)\n    {\n        // these can be shallow copied\n        m_lengthInSeconds = track.m_lengthInSeconds;\n        m_trackName = track.m_trackName;\n        m_artistName = track.m_artistName;\n        m_dataSize = track.m_dataSize;\n        // allocate memory for the copied pointer\n        m_data = new char[m_dataSize + 1];\n    ```\n\n4.  Finally, we use the `strcpy` function just like the constructor, but pass in the data from the `track` object we are copying from:\n\n    ```cpp\n    Track(const Track& track)\n    {\n        // these can be shallow copied\n        m_lengthInSeconds = track.m_lengthInSeconds;\n        m_trackName = track.m_trackName;\n        m_artistName = track.m_artistName;\n        m_dataSize = track.m_dataSize;\n        // allocate memory for the copied pointer\n        m_data = new char[m_dataSize + 1];\n        // copy the value from the old object\n        strcpy(m_data, track.m_data);\n    }\n    ```\n\n    我们现在有了一个可以正确处理数据的工作副本构造函数。\n\n5.  运行程序。您应该获得以下输出:\n\n![Figure 8.8: Output when using the copy constructor  ](img/C14195_08_08.jpg)\n\n图 8.8:使用复制构造函数时的输出\n\n复制构造函数直接复制值类型的任何成员，但是，在动态创建来保存数据的`char`数组的情况下，它必须为新类创建一个新的`char`数组，然后从另一个实例复制数据。我们现在知道这是必需的，因为我们想要数据的副本，而不是指向其他实例数据的指针。\n\n## 复制分配运算符\n\n要遵循的一般规则，被称为三个的**规则(由于 C++ 11 中额外的特殊成员函数，现在被称为五个**的**规则，将在后面的章节中更详细地介绍)，是如果析构函数、复制构造函数或赋值操作符被显式定义，那么这三个可能都应该被显式定义(记住编译器将隐式定义这些没有被显式定义的)。当一个现有对象被分配给另一个现有对象时，调用赋值运算符。**\n\n当这个复制赋值操作发生时，它的行为很像复制构造函数，只是它必须处理现有变量的清理，而不是给未初始化的变量赋值。赋值运算符也必须正确处理自我赋值。\n\n就像复制构造函数一样，如果没有显式声明，编译器将生成一个复制赋值操作符，就像复制构造函数一样，这只是一个浅复制。以下示例在实践中展示了这一点:\n\n```cpp\n{\n    Track track(200.0 f, \"Still Alive\",\n    \"GlaDos\",\"f651270d6011098375db09912b03e5e7\");\n    PrintTrackName(track);\n    // construct another track with new values    \n    Track track2(300.0 f, \"Want You Gone\", \"GlaDos\", \n    \"db6fd7d74393b375344010a0c9cc4535\");\n    // here the assignment operator is called \n    track2 = track;\n    // set the new needed data \n    track2.SetData(300.0 f, \"Want You Gone\",     \"db6fd7d74393b375344010a0c9cc4535\");\n    cout << \"Track 1\" << endl;\n    cout << \"Artist = \" << track.m_artistName << endl;\n    cout << \"Track Name = \" << track.m_trackName << endl;\n    cout << \"Track Length = \" << track.m_lengthInSeconds << endl;\n    cout << \"Track Data = \" << track.m_data << endl;\n    cout << endl;\n    cout << \"Track 2\" << endl;\n    cout << \"Artist = \" << track2.m_artistName << endl;\n    cout << \"Track Name = \" << track2.m_trackName << endl;\n    cout << \"Track Length = \" << track2.m_lengthInSeconds << endl;\n    cout << \"Track Data = \" << track2.m_data << endl;\n    return 0;\n}\n```\n\n前面的代码应该会产生以下输出:\n\n![Figure 8.9: Output when overloading the assignment operator ](img/C14195_08_09.jpg)\n\n图 8.9:重载赋值运算符时的输出\n\n我们用复制构造函数讨论的同样的问题发生在编译器生成的复制赋值操作符上；我们的动态数据没有被正确复制。\n\n创建重载赋值运算符时，我们可以再次查看之前编写的代码来帮助我们。上一个练习(*步骤 2)* 中的复制构造函数是一个很好的起点:\n\n```cpp\nTrack(const Track& track)\n{\n    // these can be shallow copied\n    m_lengthInSeconds = track.m_lengthInSeconds;\n    m_trackName = track.m_trackName;\n    m_artistName = track.m_artistName;\n    m_dataSize = track.m_dataSize;\n    // allocate memory for the copied pointer\n    m_data = new char[m_dataSize + 1];\n    // copy the value from the old object\n    strcpy(m_data, track.m_data);\n}\n```\n\n在下面的练习中，我们将实现这一点来重载赋值运算符。\n\n## 练习 61:重载赋值运算符\n\n在本练习中，我们将重载赋值运算符来创建`Track`类中对象的副本。以下是完成练习的步骤:\n\n注意\n\n这个练习的完整代码可以在这里找到:[https://packt.live/2KHv3ij](https://packt.live/2KHv3ij)。\n\n1.  Create the stub of our overloaded assignment operator:\n\n    ```cpp\n    Track& operator=(const Track& track)\n    {\n    }\n    ```\n\n    就像复制构造函数一样，我们将传入对`track`的`const`引用，但是由于这不是一个构造函数，我们将需要一个返回值。该返回值将是非常量`Track`引用(这不是必需的，但这是编译器生成赋值运算符的方式)。\n\n2.  赋值运算符的一个重要检查是验证我们没有试图将对象赋值给它自己。这就是所谓的自我分配，如果是这种情况，我们不需要执行我们的复制。\n\n    ```cpp\n    Track& operator=(const Track& track)\n    {\n        // check for self assignment\n        if(this != &track)\n        {\n    ```\n\n3.  接下来，我们可以做成员变量的浅拷贝:\n\n    ```cpp\n            // these can be shallow copied\n            m_lengthInSeconds = track.m_lengthInSeconds;\n            m_trackName = track.m_trackName;\n            m_artistName = track.m_artistName;\n            m_dataSize = track.m_dataSize;\n    ```\n\n4.  现在我们到了一个步骤，它在功能上与复制构造函数不同。由于我们分配给一个现有的对象，我们需要删除动态分配的数组，这样我们就可以将新的值复制到它上面。首先，我们创建一个新的`char*`数组，并将传入的轨迹参考对象数据复制到其中:\n\n    ```cpp\n            // allocate new memory and copy the existing data from the         //passed in object\n            char* newData = new char[m_dataSize];\n            strcpy(newData, track.m_data);\n    ```\n\n5.  现在删除现有的`m_data`数组:\n\n    ```cpp\n            // since this is an already existing object we must deallocate         //existing memory\n            delete[] m_data;\n    ```\n\n6.  最后，我们可以将`newData`数组分配给现在删除的`m_data`数组。请注意，我们不能只将传入的轨迹引用`m_data`分配给现有的`m_data`数组，因为这样我们就会让它们指向同一个地方，我们知道这不是我们想要的行为。为了解决这个问题，我们创建了一个新的数组，并使`m_data`数组指向该数组:\n\n    ```cpp\n            // assign the new data \n            m_data = newData;\n    ```\n\n7.  现在，我们可以返回一个我们分配到的轨道的引用；使用`this`关键字:\n\n    ```cpp\n        }\n        return *this;\n    }\n    ```\n\n8.  Now that we have a working assignment operator, we can test it using the same example as the copy constructor. Run the code, and you will obtain the following output:\n\n    ![Figure 8.10: Output for the overloaded assignment operator  ](img/C14195_08_10.jpg)\n\n图 8.10:重载赋值运算符的输出\n\n虽然比复制构造函数稍微复杂一点，但原理基本上是一样的，而且很明显，如果需要定义显式复制构造函数，那么几乎总是需要定义显式复制赋值运算符。\n\n## 析构函数\n\n析构函数是特殊的成员函数，在对象的生命周期结束时调用。当对象超出范围或指向它们的指针被删除时，对象就会被销毁。正如构造函数负责对象的创建，析构函数负责对象的销毁。如果已经动态分配了任何内存，则对象的析构函数必须使用`delete`或`delete[]`释放该内存，具体取决于数据类型。析构函数与类同名，不带参数，没有返回值，用波浪符号`~`表示。以下示例显示了定义析构函数所需的语法:\n\n```cpp\n~Track()\n{\n    delete[] m_data;\n}\n```\n\n当处理动态分配内存的成员变量时，可以使用析构函数来确保在对象被销毁时释放内存。与前面的概念相关的动态分配内存的问题也适用于析构函数。如果一个类动态分配内存，那么应该创建一个显式析构函数来确保这个内存被正确释放。\n\n我们不需要对非动态分配的成员变量和内置类型做任何事情；他们会毁了自己。\n\n## 活动 8:创建视频剪辑类\n\n`Track`班教会了我们很多关于写作课的知识。我们现在将实现一些非常相似的东西来帮助巩固我们的理解。我们将编写一个表示视频剪辑的类。这在很大程度上与我们的`Track`类相同，需要构造函数、析构函数、复制构造函数和复制赋值运算符重载。我们希望这个活动的结果是有一个行为类似于`Track`类的`VideoClip`类。成功完成活动后，输出应该包含视频轨道长度、名称和发行年份等信息。一种可能的输出如下:\n\n![Figure 8.11: A possible output from the VideoClip class ](img/C14195_08_11.jpg)\n\n图 8.11:视频剪辑类的可能输出\n\n以下步骤将帮助您完成活动:\n\n注意\n\n活动的完整代码可以在这里找到:[https://packt.live/2KHMwXP](https://packt.live/2KHMwXP)。\n\n1.  打开`cpp.sh`开始一个空白项目。\n2.  创建`VideoClip`课程大纲。\n3.  为视频长度和视频名称创建成员变量。\n4.  编写一个默认构造函数，将视频长度和名称初始化为默认值。\n5.  编写一个参数化构造函数，将视频长度和名称设置为传递的参数。\n6.  创建一个数据字符数组和数据大小成员变量，并在两个构造函数中初始化它们。\n7.  创建正确处理数据数组复制的复制构造函数。\n8.  创建一个复制赋值运算符重载，以正确处理数据数组的复制。\n9.  编写一个析构函数，删除分配的数据数组。\n10.  更新`main`功能，创建三个不同的`videoClip`实例，并输出它们的值。\n11.  Test the copy constructor and copy assignment operators within the `main` function by initializing a video clip using an existing instance and initializing an instance of a video clip with its constructor and then later assigning it to another existing instance.\n\n    注意\n\n    这个活动的解决方案可以在第 542 页找到。\n\n# 总结\n\n本章中我们已经介绍了几个概念。我们研究了联合和结构，以及它们与类的区别(以及它们的区别)。然后，我们详细讨论了不同类型的构造函数，并讨论了复制对象时可能出现的问题以及如何解决这些问题。我们了解了三大法则及其重要性。最后，我们快速看一下析构函数。\n\n我们发现，在定义自己的类型时，C++ 有一些非常具体的东西我们必须记住，并发现我们必须非常小心地处理动态内存，并相应地设计我们的类。只要我们遵循本章中的指导方针，我们就可以看到 C++ 为我们提供了创建健壮且易于使用的类型所需的所有工具。\n\n所有这些信息为我们提供了必要的知识，使我们能够更深入地了解面向对象的概念，相信我们已经掌握了基础知识。下一章将讨论如何最好地从面向前的角度来设计我们的类，以确保它们只能按照我们想要的方式使用。"
  },
  {
    "path": "docs/cpp-workshop/09.md",
    "content": "# 九、面向对象原则\n\n概观\n\n本章介绍了设计类的最佳实践，并将向您概述抽象和封装，在哪里使用它们，以及它们如何使您的自定义 C++ 类型受益。关于类的更多细节以及它们如何适应面向对象的编程范式也将被涵盖。\n\n# 简介\n\n前一章提供了关于对象构造的详细信息，以及关于 C++ 提供的定义这些对象的不同关键字的信息。我们了解到，在创建自己的类型时，我们必须小心，并确保它们被适当地构造和销毁。本章将进一步深入面向对象编程，解释设计类时应该牢记的重要原则，以便最有效地利用**面向对象编程** ( **OOP** )范例。\n\n在本章中，我们将进一步介绍定义我们自己的类型的最佳实践。通过这些知识，我们可以编写防止意外使用的类，并且，使用公共和私有函数以及成员变量，我们可以清楚地表明我们打算如何使用一个类。\n\n封装允许我们隐藏我们不希望用户直接访问的数据，而抽象为类的用户提供了一个接口，该接口公开了类的所有重要用途，但隐藏了细节。这两个主题都将在本章中涉及，同时还有一些关于类的更详细的解释。\n\n# 类和面向对象\n\n类是一种将数据分组并提供操作该数据的功能的方法。类类型是对象的 C++ 表示。类本质上是对象的同义词。在前一章中，`Track`类是`Track`类型对象的原型。\n\n注意\n\n原型一词是一个描述性术语，以方便解释，而不是一个官方术语。\n\n注意原型这个词很重要，因为它暗示了可重用性的概念，这是面向对象设计方法的主要好处之一。一旦一个对象有了它的原型基础，那么细节就可以在不改变底层描述的情况下暴露出来。从原型(**类**)用自己的细节(**数据**)构建的对象被称为对象或类的实例。\n\n以下是前一章中使用的`Track`类:\n\n```cpp\n#include <iostream>\n#include <string>\nusing namespace std;\nclass Track\n{\npublic:\n    Track(float lengthInSeconds, string trackName)\n    {\n        m_lengthInSeconds = lengthInSeconds;\n        m_trackName = trackName;\n    }\n    float m_lengthInSeconds;\n    string m_trackName;\n};\n```\n\n如果让你从根本上描述`Track`物体，你很可能会把它描述成一个有名字和长度的东西。这描述了组成类的成员变量。当被要求描述一个特定的轨道时，比如`Track track(180.0f, \"Still Alive\")`，很可能你会把它描述为**一个名为《还活着》的轨道，它有 3 分钟长**。这显然是一个对象实例的描述。从根本上说，它仍然是一个有名称和长度的轨道，但现在描述更详细，因为细节已经设定。使用以下片段构建的另一个`Track`怎么样？\n\n```cpp\n    Track anotherTrack(260.0f, \"Want You Gone\");\n```\n\n同样，很有可能我们会把它描述为**一首 4 分 20 秒长的名为《想要你离开》的曲目**。当描述一个对象时，知道预期是什么使得描述它(即实现它)变得更加简单。同样的概念也适用于面向对象设计的类，因为我们知道如何存储对象的细节，以及我们想要访问的细节的名称。这也扩展到创建可以在多个程序中使用的类，而不仅仅是在同一个程序中，本质上是创建一个“代码库”，可以用来执行以前编写的任务，例如，数学类或文件解析类。这是**复用**的基础。\n\n在这一章的下一部分，我们将讨论一系列概念中的一个，这些概念被称为首字母缩写 **SOLID** 。这个首字母缩略词及其思想是由罗伯特·c·马丁创造的，他通常被称为鲍勃叔叔**【马丁 97】**。这个首字母缩略词是前五个面向对象设计原则的简写，如下所示:\n\n*   **S**–单一责任原则\n*   **O**–开闭原理\n*   **L**–利斯科夫替代原则\n*   **I**–界面分离原理\n*   **D**–依赖倒置原理\n*   我们不会涵盖所有这些，但是 **S** (对于单一责任原则)对本章特别重要。\n\n## 固态硫\n\n固体首字母缩写中的 S 代表**单一责任原则** ( **SRP** ) **【马丁 97】**。SRP 规定“*一个班应该只有一个也只有一个变更的理由，也就是说一个班应该只有一个工作。*”\n\n回到**可重用性的概念**可以阐明这个原则的重要性。如果为了某种目的试图重用一些代码，它不应该带来一堆需要维护的额外责任或者可能是冗余的代码。此外，这些额外的职责可能依赖于其他类。因此，他们也需要搬进新项目。显然，这种依赖类的循环是不可取的。任何具有我们需要的功能的类都应该能够独立使用(*...一个班应该只有一个工作*”)。有时候，给一个班级一点额外的责任似乎是无害的。然而，应该仔细考虑将这个责任抽象并交给另一个类，然后这个类可以被需要它提供的功能的其他类重用。\n\n## 练习 62:创建打印值的类\n\n在本练习中，我们将创建一个类，用于打印我们的类中的值。SRP 上的前一段指出，类应该只有一项工作——一项职责——因此，让我们演示一种方法，通过从类本身中移除打印到控制台的职责，并将该职责赋予另一个类，我们可以实现这一点。虽然这个练习可能很简单，但是如果我们愿意的话，可以轻松地将打印到控制台的类替换为输出到文件的类，这对我们来说是非常有用的。以下是完成练习的步骤:\n\n注意\n\n这个练习的完整代码可以在这里找到:[https://packt.live/2pD3LlP](https://packt.live/2pD3LlP)。\n\n1.  Add the `ValuePrinter` class to an empty file in `cpp.sh`. It is very simple and only consists of overloaded functions to print a message plus a `float`, an `int`, or a `string`. It will look like the following:\n\n    ```cpp\n    #include <iostream>\n    #include <string>\n    using namespace std;\n    class ValuePrinter\n    {\n    public:\n        void Print(string msg, float f)\n        {\n            cout << msg << \" : \" << f << endl;\n        }\n        void Print(string msg, int i)\n        {\n            cout << msg << \" : \" << i << endl;\n        }\n        void Print(string msg, string s)\n        {\n            cout << msg << \" : \" << s << endl;\n        }\n    };\n    ```\n\n    现在让我们创建一个可以利用这个`ValuePrinter`的类。\n\n2.  创建一个名为`Article`的类，并为其提供标题、页数、字数和作者的成员变量。我们还将编写一个构造函数来初始化我们的成员变量，并添加`ValuePrinter`作为成员变量。这个类应该如下:\n\n    ```cpp\n    class Article\n    {\n    public:\n        Article(string title, int pageCount, int wordCount, string author)\n        {\n            m_title = title;\n            m_pageCount = pageCount;\n            m_wordCount = wordCount;\n            m_author = author;\n        }\n        string m_title;\n        int m_pageCount;\n        int m_wordCount;\n        string m_author;\n        ValuePrinter valuePrinter;\n    ```\n\n3.  接下来我们想在`Article`内部创建一个函数，使用`ValuePrinter`成员对象打印我们的成员变量。我们将调用这个函数`ShowDetails`，它应该看起来像下面的例子:\n\n    ```cpp\n        void ShowDetails()\n        {\n            valuePrinter.Print(\"Article Title\", m_title);\n            valuePrinter.Print(\"Article Page Count\", m_pageCount);\n            valuePrinter.Print(\"Article Word Count\", m_wordCount);\n            valuePrinter.Print(\"Article Author\", m_author);\n        }\n    };\n    ```\n\n4.  我们现在可以在`main`函数中测试这一点，以查看使用`ValuePrinter` :\n\n    ```cpp\n    int main()\n    {\n        Article article(\"Celebrity Crushes!\", 2, 200, \"Papa Ratsea\");\n        article.ShowDetails();\n        return 0;\n    }\n    ```\n\n    打印出的值\n5.  Run the complete program. You should obtain the following output:\n\n    ![Figure 9.1: Printing values using ValuePrinter ](img/C14195_09_01.jpg)\n\n图 9.1:使用值打印机打印值\n\n作为真正巩固这个概念的附加练习，尝试实现一个类型有一个`string`成员变量和一个保存区域的`float`的`Shape`类，然后重用`ValuePrinter`编写一个`Shape` `ShowDetails`函数。\n\n我们可以看到在前面的练习中呈现的模式是多么的有用。我们已经将打印到控制台的责任从`Article`类转移到了另一个类。如果我们需要改变`ValuePrinter`的内部运作方式，那么`Article`就根本不需要改变。以这种方式使用`ValuePrinter`可以很好地进入我们接下来的两个主题:**封装**和**抽象**。\n\n## 封装\n\n作为面向对象程序设计中的一个基本概念，封装对于理解非常重要，应该应用于您设计的大多数类。封装结合了类的数据和成员函数来处理这些数据。只能通过类提供的成员对类中的数据进行操作；成员数据不应直接访问。\n\n这就是所谓的**数据隐藏**，C++ 给了我们一些关键词，当我们编写实现这一点的类时，可以利用这些关键词。这些关键词被称为**访问修饰语**。下表(*图 9.2* )显示了这些关键词及其含义:\n\n![Figure 9.2: Table describing the different keywords  ](img/C14195_09_02.jpg)\n\n图 9.2:描述不同关键字的表格\n\n使用前面的表格和封装的解释，考虑下面的`Track`类，我们一直在使用它:\n\n```cpp\n#include <iostream>\n#include <string>\nusing namespace std;\nclass Track\n{\npublic:\n    Track(float lengthInSeconds, string trackName)\n    {\n        m_lengthInSeconds = lengthInSeconds;\n        m_trackName = trackName;\n    }\n    float m_lengthInSeconds;\n    string m_trackName;\n};\n```\n\n符合这个原则吗？不，不是。保存数据的两个成员变量都在`public`关键字下，因此，如图 9.2 所示，可以从类内和类外的任何地方访问。另一段代码完全有可能抓取一个`Track`实例并弄乱它，唯一的限制是类型。下面的`main`函数使用了`Track`类，并展示了从`Track`类中获取数据并修改它是多么容易:\n\n```cpp\nint main()\n{\n    // create\n    Track t(260.0f, \"Still Alive\");\n    cout << \"My Favourite Song is: \" << t.m_trackName << endl;  \n    // mess with it\n    t.m_lengthInSeconds = 9405680394634.4895645f; \n    // Song is now pretty much 300 millennia long!\n    t.m_trackName = \"S-Club Party\"; // OH NO!!\n    cout << \"My Favourite Song is: \" << t.m_trackName;\n    return 0;\n}\n```\n\n我们对封装的定义是**成员数据不应该被直接访问**。*图 9.2* 向我们展示了要使成员变量从类外部不可访问，我们可以使用`private`关键字。下面的代码片段显示了`private`关键字用于阻止从类外部访问成员变量，然后显示了试图改变它们的`main`函数:\n\n注意\n\n建造师依然是**公众**。**私人**建造者是另一章的话题。\n\n```cpp\n#include <iostream> \n#include <string> \nusing namespace std; \nclass Track \n{\npublic: \n    Track(float lengthInSeconds, string trackName) \n    { \n        m_lengthInSeconds = lengthInSeconds; \n        m_trackName = trackName; \n    } \nprivate: \n    float m_lengthInSeconds; \n    string m_trackName; \n}; \nint main() \n{\n    // create \n    Track t(260.0f, \"Still Alive\"); \n    cout << \"My Favourite Song is: \" << t.m_trackName << endl; \n    // mess with it - Agh! thwarted, compiler error: these variables are private \n    t.m_lengthInSeconds = 9405680394634.4895645f; \n    t.m_trackName = \"S-Club Party\"; \n    cout << \"My Favourite Song is: \" << t.m_trackName; \n    return 0; \n}\n```\n\n运行此代码会导致编译器出现如下错误:\n\n```cpp\n'float Track::m_lengthInSeconds' is private within this context\n'std::string Track::m_trackName' is private within this context\n```\n\n现在成员变量是`private`，任何试图直接设置这些变量的人都将面临编译器错误。这些数据现在是隐藏的，一旦在构造函数中设置了变量，就不能直接更改或访问它们。然而，这提出了一个新问题；既然变量无法从类外部访问，就不能将它们打印到控制台或读入可能需要使用它们的地方。例如，前面代码片段中的下面一行代码将不再编译:\n\n```cpp\n    cout << \"My Favourite Song is: \" << t.m_trackName << endl;\n```\n\n成员函数也可以是`private`，因为可能有我们希望保留在类内部的函数。函数对于拆分代码或实现可在类中的其他函数中重用的功能很重要。通过制作这些功能`private`，我们保证它们只会被类本身使用，不会暴露给类的用户；他们不是`public`界面的一部分。\n\n## 练习 63:使用私有成员变量创建职位类\n\n在本练习中，我们将创建一个名为`Position`的类，该类保存 2D 笛卡尔坐标:x 和 y。x 和`y`都是将在构造函数中设置的`private`成员变量，我们将创建一个`public`成员函数，该函数采用另一组浮点数(`x`、`y`)并将它们和我们的位置之间的欧几里德距离作为`float`类型返回。以下是执行本练习的步骤:\n\n注意\n\n这个练习的完整代码可以在这里找到:[https://packt.live/2OwnHPU](https://packt.live/2OwnHPU)。\n\n1.  首先，我们可以通过将`Position`声明为类来创建我们类的存根。在`cpp.sh`中创建新项目，并输入下面的类存根和我们将要使用的`#include`语句。我们需要`cmath`的平方根函数:\n\n    ```cpp\n    #include <iostream>\n    #include <cmath>\n    class Position\n    {\n    };\n    ```\n\n2.  接下来，我们可以创建组成我们的坐标的成员变量，`x`和`y`。我们希望这些成员变量是私有的，所以我们将在它们上面使用`private`关键字。两个变量都是浮点数，我们将在它们前面加上`m_`，用于表示一个变量是成员变量:\n\n    ```cpp\n    #include <iostream>\n    #include <cmath>\n    class Position\n    {\n    private:\n        float m_x;\n        float m_y;\n    };\n    ```\n\n3.  我们需要一种方法来设置这些变量。我们将在构造函数中使用初始化列表语法来实现这一点。我们的构造函数需要是`public`，所以我们将在我们的构造函数上面使用该关键字，如下面的代码片段所示:\n\n    ```cpp\n    #include <iostream>\n    #include <cmath>\n    class Position\n    {\n    public:\n        Position(float x, float y) : m_x(x), m_y(y) {}\n    private:\n        float m_x;\n        float m_y;\n    };\n    ```\n\n4.  我们现在需要创建我们的`distance`函数。这是一个`public`成员函数，它将另一个`x`坐标和任意一个`y`坐标作为参数，并将该坐标到我们类中存储的位置的距离返回为(`m_x`，`m_y`)。在实现功能之前，我们可以先创建这个成员函数的存根:\n\n    ```cpp\n    #include <iostream>\n    #include <cmath>\n    class Position\n    {\n    public:\n        Position(float x, float y) : m_x(x), m_y(y) {}\n        float distance(float x, float y)\n        {\n            // we must return something at this point if we want it to         //compile\n            return 0;\n        }\n    private:\n        float m_x;\n        float m_y;\n    };\n    ```\n\n5.  We can now implement the `distance` function by using a derivation of the Pythagorean Theorem:\n\n    ![Figure 9.3: Pythagorean theorem ](img/C14195_09_03.jpg)\n\n    图 9.3:勾股定理\n\n6.  这将是我们两个位置之间的直线距离:\n\n    ```cpp\n        float distance(float x, float y)\n        {\n            float xDiff = x - m_x;\n            float yDiff = y - m_y;\n            return std::sqrt(((xDiff * xDiff) + (yDiff * yDiff)));\n        }\n    ```\n\n7.  我们现在都准备好测试我们的新班级了。创建一个`main`函数，创建一个设置为(`10`，`20`)的`Position`对象，并打印从该对象到(`100`，`40`)的距离。下面是这样做的代码:\n\n    ```cpp\n    int main()\n    {\n        Position pos(10.0f, 20.0f);\n        std::cout << \"The distance from pos to (100, 40) is:\"               << pos.distance(100.0f, 40.0f) << std::endl;\n        return 0;\n    }\n    ```\n\n8.  运行完整的代码。您将收到以下输出:\n\n![Figure 9.4: Distance output ](img/C14195_09_04.jpg)\n\n图 9.4:距离输出\n\n在本练习中，我们通过将位置数据`private`添加到我们的`Position`类中来封装位置数据。任何希望使用这些值的人都必须通过我们提供的`public`功能来使用。我们如何在保持一定程度的控制的同时访问`private`数据成员？本章的下一部分将介绍一种可以提供解决方案的常见模式。\n\n## 吸气剂和沉降剂\n\n类以某种方式被利用，但是**封装**的概念声明成员数据不应该被直接访问。使成员变量**私有**确保了我们例子中的情况，但是我们最终渲染了`Track`类，因为它最明显的目的:保存关于轨道的可读数据。保护数据同时仍允许合理访问的常用技术是使用**获取器和设置器**。不出所料，getter 获取数据，setter 设置数据。获取者通常以单词`get`为前缀，设置者以单词`set`为前缀。这里是`Track`类，它的成员数据有 getters:\n\n```cpp\n#include <iostream>\n#include <string>\nusing namespace std;\nclass Track \n{\npublic:\n    Track(float lengthInSeconds, string trackName) \n    {\n        m_lengthInSeconds = lengthInSeconds;\n        m_trackName = trackName;\n    }\n    float getLength() \n    {\n        return m_lengthInSeconds;\n    }\n    string getName() \n    {\n    return m_trackName;\n    }\nprivate:\n    float m_lengthInSeconds;\n    string m_trackName;\n};\nint main() \n{\n    // create \n    Track t(260.0 f, \"Still Alive\");\n    cout << \"My Favourite Song is: \" << t.getName() << endl;\n    cout << \"It is :\" << t.getLength() / 60.0 f << \" minutes long\";\n    return 0;\n}\n```\n\n在前面的代码中，`getLength`返回`m_lengthInSeconds`变量，`getName`返回`m_trackName`变量。这些函数都是`public`函数，因此可以在类外使用，允许我们打印它们的值，同时保留变量本身`private`，因此可以安全地从类外直接访问。\n\n**设置器**允许设置一些数据。值得注意的是，直接设置器本质上会破坏封装，因为它会暴露要再次更改的变量。setter 允许公开的成员变量不允许的一件事是对要设置的数据进行验证。请在以下代码的帮助下考虑这一点:\n\n```cpp\nExample09_1.cpp\n23     string getName() \n24     {\n25         return m_trackName;\n26     }\n27 \n28     void setName(string newTrackName) \n29     {\n30         // if S-Club is not found set the track name - otherwise do nothing \n31         if (newTrackName.find(\"S-Club\") == string::npos)\n32         {\n33             m_trackName = newTrackName;\n34          }\n35     }\n36 \n37     void setLength(float newTrackLength) \n38     {\n39         if (newTrackLength < MAX_TRACK_LENGTH && newTrackLength > 0) \n40         // no prog metal for us! \n41         {\n42             m_lengthInSeconds = newTrackLength;\n43         }\n44     }\nThe complete code for this example cam be found at: https://packt.live/2DLDVQf\n```\n\n在上例中，添加了`setName`和`setLength`。`setName`函数将一个字符串作为参数来设置`m_trackName`，但是首先，它会检查该参数是否等于`S-Club`，如果是这样，则不会设置变量。`setLength`函数将一个浮点数作为参数，并使用它来设置`m_trackLengthInSeconds`变量，但在设置该变量之前，它会验证该变量是否大于零且不大于`MAX_TRACK_LENGTH`。\n\n运行前面的示例代码片段将产生以下输出:\n\n![Figure 9.5: Output using the setter method ](img/C14195_09_05.jpg)\n\n图 9.5:使用 setter 方法的输出\n\n在这里讨论的例子中，试图将数据设置为无效的东西(在这个上下文中)是行不通的，因此满足了前面概述的封装标准。我们的数据现在更安全了，只能以我们已经批准的方式使用公共接口进行设置。\n\n注意\n\n这些检查也可以在构造函数中执行，以确保数据在该点有效。\n\n## 练习 64:位置类中的获取者和设定者\n\n在*练习 63* 、*创建位置类*中，使用`Private`成员变量，我们创建了一个`Position`类和一个`distance`函数。我们的`distance`函数的问题是，我们不能将另一个`Position`类的对象值作为参数传递给它，因为计算距离所需的变量对我们不可用；他们是私人的。解决这个问题的一种方法是传递一个`Position`对象作为参数，这提出了一个关于 C++ 中私有元素的重要注意点:它们实际上可以被相同类型的类访问，因为这种访问控制是基于每个类的，而不是基于每个对象的。\n\n不过，目前我们不会传入一个`Position`对象，因为为了便于讨论，我们假设我们不知道一个`Position`对象有多大，当我们只需要它的`x`和`y`值时，我们不想不必要地复制它。因此，在本练习中，我们将为我们的`Position`类实现一些 getters，并创建一个使用`distance`函数的示例程序，以确保两个位置不会相距太远。\n\n注意\n\n这个练习的完整代码可以在这里找到:[https://packt.live/2O63D7O](https://packt.live/2O63D7O)。\n\n以下是完成练习的步骤:\n\n1.  首先，我们可以从*练习 63* 、*创建位置类*中查看`Position`类，并将其复制到我们的新示例中:\n\n    ```cpp\n    #include <iostream>\n    #include <cmath>\n    class Position\n    {\n    public:\n        Position(float x, float y) : m_x(x), m_y(y) {}\n        float distance(float x, float y)\n        {\n            float xDiff = x - m_x;\n            float yDiff = y - m_y;\n            return std::sqrt(((xDiff * xDiff) + (yDiff * yDiff)));\n        }\n    private:\n        float m_x;\n        float m_y;\n    };\n    ```\n\n2.  在这个类中，我们可以为名为`getX()`和`getY()`的私有成员变量添加一些 getters。他们只会返回我们的`m_x`和`m_y`变量。在我们的`distance`功能后添加它们，但确保它们仍在`public`关键字下:\n\n    ```cpp\n        float getX() { return m_x; }\n        float getY() { return m_y; }\n    ```\n\n3.  此时，我们可以构造一个新的`Position`对象，并访问其变量以通过距离检查。让我们看看这在更新后的`main`函数中会是什么样子:\n\n    ```cpp\n    int main()\n    {\n        Position pos(10.0f, 20.0f);\n        Position pos2(100.0f, 200.0f);\n        std::cout << \"The distance between pos and pos2 is: \" \n            << pos.distance(pos2.getX(), pos2.getY());\n        return 0;\n    }\n    ```\n\n4.  在我们进入`main`函数之前，我们将为成员变量创建设置器。在我们的吸气剂下面添加沉降器，如图所示:\n\n    ```cpp\n        void setX(float x) { m_x = x; }\n        void setY(float y) { m_y = y; }\n    ```\n\n5.  现在，对于我们的`main`函数，我们将定义我们的位置可以分开的最大距离(在这种情况下，将是 500 个单位)。然后，我们将在一个循环中更新我们的位置，如果我们到达这个最大距离，就停止。为此，我们将利用我们的吸气剂和设置剂，以及`distance`功能。我们将一个位置向另一个位置的相反方向移动，首先通过减去它们的 *x* 和 *y* 值(*方向(x，y)**=**(pos2X–pos1X，pos2Y–pos1Y)*)得到两个位置之间的方向，然后归一化。我们可以通过将两个位置之间的 *x* 和 *y* 除以距离来归一化(我们在上一步中得到了这个)。你可能会认为这是向量数学，但如果不是，别担心；重要的部分是我们吸气剂和沉降剂的使用。这是我们的`distance`检查的`main`功能:\n\n    ```cpp\n    int main()\n    {\n        float maxDistance = 500.0f;\n        Position pos(10.0f, 20.0f);\n        Position pos2(100.0f, 200.0f);\n        bool validDistance = true;\n        int numberOfTimesMoved = 0;\n        while(validDistance)\n        {\n            float distance = pos.distance(pos2.getX(), pos2.getY());\n            if(distance > maxDistance)\n            {\n                validDistance = false;\n                break;\n            }\n            // get direction\n            float xDirection = pos2.getX() - pos.getX();\n            float yDirection = pos2.getY() - pos.getY();\n            // normalize\n            float normalizedX = xDirection / distance;\n            float normalizedY = yDirection / distance;\n            pos.setX(pos.getX() - normalizedX);\n            pos.setY(pos.getY() - normalizedY);\n            numberOfTimesMoved++ ;\n        }\n        std::cout << \"Too far apart.\" << \" Moved \" << numberOfTimesMoved               << \" times\" ;\n        return 0;\n    }\n    ```\n\n6.  运行此`main`功能获得输出:\n\n![Figure 9.6: Printing the count to the console ](img/C14195_09_06.jpg)\n\n图 9.6:将计数打印到控制台\n\n请注意，该程序还会输出位置在到达最大距离之前改变的次数。\n\n# 返回值或参考值\n\n决定如何返回 getter 的值很重要，并且需要一些关于可用选项的知识。在 C++ 中，我们可以通过值、指针和引用以及它们的`const`对应物来返回变量，我们将很快讨论；但是，我们不会在本章中介绍指针。如何返回变量的选择很大程度上取决于它的用例，本章的这一部分将在我们的`Track`类的上下文中讨论这一点，特别是它如何应用于我们的获取器和设置器。\n\n## 按值返回\n\n从`Track`类看下面的`getLength`方法:\n\n```cpp\n    float getLength() { return m_lengthInSeconds; }\n```\n\n这是**按值返回**。换句话说，这个方法返回`m_lengthInSeconds`值的副本。如果该值被分配给另一个变量，那么对`m_lengthInSeconds`的任何修改都不会反映在新变量中(反之亦然)，因为它是返回的值的副本。这里有一个例子:\n\n```cpp\nint main()\n{\n    // create\n    Track t(260.0f, \"Still Alive\");\n    cout << \"My Favourite Song is: \" << t.getName() << endl;\n    cout << \"It is :\" << t.getLength() / 60.0f << \" minutes long\" << endl;\n    // create a new variable and assign to it     // the value of the track length\n    float tLength = t.getLength();\n    // modify it\n    tLength = 100.0f;\n    cout << \"My Favourite Song is: \" << t.getName() << endl;\n    cout << \"It is :\" << t.getLength() / 60.0f << \" minutes long\";\n    return 0;\n}\n```\n\n前面的代码将产生以下输出:\n\n![Figure 9.7: Printing duration of the song ](img/C14195_09_05.jpg)\n\n图 9.7:歌曲的打印时长\n\n修改`tLength`并没有修改`m_lengthInSeconds`的值(反之亦然)。这是安全的，通常也是可取的行为。我们不希望类外的东西能够修改`private`成员变量。\n\n## 参照返回\n\n除了返回数据或对象的值，方法还可以返回对数据或对象的引用。返回引用不会复制数据的值；它将返回一个引用，允许数据继续被修改。通过引用返回很快，因为它不需要执行`copy`操作。通常，它用于返回大型结构或类，在这些结构或类中，复制对性能有害。以下方法是`getLength`功能，修改后返回一个引用:\n\n```cpp\nfloat& getLength() { return m_lengthInSeconds; }\n```\n\n这允许以下列方式修改数据:\n\n```cpp\nint main()\n{\n    // create\n    Track t(260.0f, \"Still Alive\");\n    cout << \"My Favourite Song is: \" << t.getName() << endl;\n    cout << \"It is :\" << t.getLength() / 60.0f << \" minutes long\" << endl;\n    // getLength now returns a reference and can be modified\n    t.getLength() = 100.0f;\n    cout << \"My Favourite Song is: \" << t.getName() << endl;\n    cout << \"It is :\" << t.getLength() / 60.0f << \" minutes long\";\n    return 0;\n}\n```\n\n前面的代码将产生以下输出:\n\n![Figure 9.8: Modifying data to print the song duration ](img/C14195_09_08.jpg)\n\n图 9.8:修改数据以打印歌曲时长\n\n从输出中可以看出，轨道长度已被修改。封装和**数据隐藏**已经被抛出窗外。我们的数据不应该被这样修改。**只能通过类提供的方法对类中的数据进行操作。成员数据不应直接访问。**前面的例子完全打破了这个概念。\n\n值得注意的是，将返回的引用分配给非引用类型变量实际上只会分配一个副本，而不是引用。以下面的片段为例:\n\n```cpp\nint main()\n{\n    // create\n    Track t(260.0f, \"Still Alive\");\n    cout << \"My Favourite Song is: \" << t.getName() << endl;\n    cout << \"It is :\" << t.getLength() / 60.0f << \" minutes long\" << endl;\n    // getLength returns a reference but this actually is a copy\n    float tLength = t.getLength();\n    tLength = 100.0f;\n    cout << \"My Favourite Song is: \" << t.getName() << endl;\n    cout << \"It is :\" << t.getLength() / 60.0f << \" minutes long\";\n    return 0;\n}\n```\n\n该代码片段产生以下输出:\n\n![Figure 9.9: Printing duration of the song ](img/C14195_09_05.jpg)\n\n图 9.9:歌曲的打印时长\n\n前面的代码片段使用了`getLength`函数，该函数返回一个引用。然而，从输出中我们可以看到，它实际上并没有给`tLength`赋值。这可能看起来很明显，因为`tLength`实际上不是一个参考类型，但它值得知道，这样你就不会在未来被发现。\n\n将引用分配给另一个引用类型时，任何修改都将反映在类成员数据中，因为新引用本质上只是同一事物的另一个名称。这里有一个例子:\n\n```cpp\nint main()\n{\n    // create\n    Track t(260.0f, \"Still Alive\");\n    cout << \"My Favourite Song is: \" << t.getName() << endl;\n    cout << \"It is :\" << t.getLength() / 60.0f << \" minutes long\" << endl;\n    // getLength now returns a reference and can be modified\n    float& tLength = t.getLength();\n    tLength = 100.0f;\n    cout << \"My Favourite Song is: \" << t.getName() << endl;\n    cout << \"It is :\" << t.getLength() / 60.0f << \" minutes long\";\n    return 0;\n}\n```\n\n前面的代码片段产生了以下输出:\n\n![Figure 9.10: Printing duration of the song ](img/C14195_09_08.jpg)\n\n图 9.10:歌曲的打印时长\n\n请注意，修改`tLength`也会修改`Track`对象的长度。\n\n通过引用返回时要记住的另一件重要事情是不要通过引用返回函数的局部变量，因为一旦该变量超出范围(局部变量在函数末尾超出范围并被破坏)，引用将是对垃圾的引用。这里有一个例子来说明这一点:\n\n```cpp\n    float& getLengthInMinutes()\n    {\n        float lengthInMinutes = m_lengthInSeconds / 60.0f;\n        return lengthInMinutes;\n    } // lengthInMinutes out of scope here\n```\n\n谢天谢地，运行前面的代码通常会导致警告或错误。这个建议也适用于**临时**变量。例如，在计算表达式时，编译器将生成一个临时变量来存储表达式结果:\n\n```cpp\n    float& getLengthInMinutes()\n    {\n        // creates a temporary\n        return m_lengthInSeconds / 60.0f;\n    }  // temporary out of scope here\n```\n\n注意\n\n这两个例子都可以通过值返回。\n\n## const\n\n如前所述，在某些情况下，类可能希望返回一个引用，例如当它返回的对象很大时，复制它会对性能产生影响。返回引用的问题是它破坏了封装。在需要返回引用但不可修改的情况下，我们可以使用 C++ `const`关键字。该关键字将数据标记为只读。解释`const`的各种用途，它是如何使用的，以及什么可以被标记为`const`需要很长时间，可能会相当混乱，但请记住，基本上这是一种明确如何使用一段数据的方式。\n\n## 返回常量引用\n\n我们知道如何以及何时通过引用返回，但是我们可以用不同的方式从函数中返回变量，那就是以`const`引用的形式。我们知道`const`将数据标记为只读，因此`const`引用是被标记为只读的引用—不可修改的引用。\n\n这是我们在参考文献示例中使用的`getLength`函数，现在用`const`标记:\n\n```cpp\nconst float& getLength() { return m_lengthInSeconds; }\n```\n\n下面的代码片段试图以与引用部分中的示例相同的方式使用这些数据:\n\n```cpp\nint main()\n{\n    // create\n    Track t(260.0f, \"Still Alive\");\n    cout << \"My Favourite Song is: \" << t.getName() << endl;\n    cout << \"It is :\" << t.getLength() / 60.0f << \" minutes long\" << endl;\n    // getLength now returns a const reference\n    float& tLength = t.getLength();\n    tLength = 100.0f;\n    cout << \"My Favourite Song is: \" << t.getName() << endl;\n    cout << \"It is :\" << t.getLength() / 60.0f << \" minutes long\";\n    return 0;\n}\n```\n\n如果运行此代码，我们将收到以下错误:\n\n```cpp\nerror: binding 'const float' to reference of type 'float&' discards qualifiers\n```\n\n这个编译器错误告诉我们从`getLength()`返回的`const`引用只能绑定到另一个`const`引用。我们可以利用这一点；因为该引用将是`const`，所以它也将是只读的，从而保护数据。\n\n下面是一个例子，说明我们如何通过将返回的`const`引用分配给另一个`const`引用来消除前面的编译器错误:\n\n```cpp\nint main()\n{\n    // create\n    Track t(260.0f, \"Still Alive\");\n    cout << \"My Favourite Song is: \" << t.getName() << endl;\n    cout << \"It is :\" << t.getLength() / 60.0f << \" minutes long\" << endl;\n    // getLength now returns a const reference\n    const float& tLength = t.getLength();\n    tLength = 100.0f;\n    cout << \"My Favourite Song is: \" << t.getName() << endl;\n    cout << \"It is :\" << t.getLength() / 60.0f << \" minutes long\";\n    return 0;\n}\n```\n\n请注意，该示例还试图为`const`引用分配一个新值，导致另一个编译器错误——这与我们的预期完全一样:\n\n```cpp\nerror: assignment of read-only reference\n```\n\n注意\n\n使用以下语法会引发相同的错误:`t.getLength() = 100.0f;`。\n\n## 常量函数\n\n成员函数也可以声明为`const`。声明为`const`的成员函数不允许修改成员数据，即使它们是类本身的一部分。这使得程序员清楚函数的意图，任何修改类的人都应该知道函数的意图是`const`，因此不应该修改成员数据，因为这会对整个应用产生影响。这里是标记为`const`的`getLength`功能。注意`const`是在声明之后表示函数本身为`const`，而不是返回的`float`值:\n\n```cpp\n    float getLength() const\n    {\n    // modify member data in const function\n    m_lengthInSeconds = 10.0f;\n    return m_lengthInSeconds;\n    }\n```\n\n运行此程序会产生以下错误:\n\n```cpp\nerror: assignment of member 'Track::m_lengthInSeconds' in read-only object\n```\n\n这个代码片段产生的编译器错误是`const` `getLength`成员函数试图修改一些成员数据的结果。\n\n注意`const`成员函数可以在非常数和`const`对象上调用，而非常数成员函数只能在非`const`对象上调用。\n\n假设我们在`Track`类中有以下非常数成员函数:\n\n```cpp\n    float getLength() { return m_lengthInSeconds; }\n```\n\n`main`函数创建一个`const` `Track`对象，并尝试调用非`const`函数:\n\n```cpp\nint main()\n{\n    // create\n    const Track t(260.0f, \"Still Alive\");\n    cout << \"It is :\" << t.getLength() / 60.0f << \" minutes long\" << endl;\n    return 0;\n}\n```\n\n这将导致以下编译器错误，因为`Track`对象`t`是`const`并试图调用非`const`成员函数:\n\n```cpp\nerror: passing 'const Track' as 'this' argument discards qualifiers\n```\n\n`const`是 C++ 中一个重要的、偶尔会让人困惑的部分。前面的例子和段落只是一个小例子。测试创建`const`对象并通过`const`引用返回，以获得语法的感觉。\n\n## 抽象\n\n**抽象**和**封装**是同一个硬币的两面。将数据封装在类中可以抽象出数据的功能，只向用户公开类设计所需的方法，并隐藏类对其成员数据执行的所有基本实现细节。**抽象只为用户提供了一个必不可少的界面，隐藏了背景细节**。\n\n接下来的示例将使用一个`Playlist`类来说明这一点，该类可以保存`Track`对象以及以下功能:\n\n*   添加轨道并按名称删除它们\n*   按字母顺序或相反的字母顺序对轨道进行排序\n*   按最短或最长轨道对轨道进行排序\n*   打印当前轨道的名称及其长度\n\n`Playlist`类将不负责创建`Track`对象。和前面的例子一样，`main`函数将创建这些`Track`对象。`Track`对象已经从前面的例子简化为**不变的**(创建后不能改变)。这是通过使用`const`实现的，如下面的片段所示，这是我们的`Track`类在本章这一部分的上下文中的声明:\n\n```cpp\n#include <iostream>\n#include <string>\n#include <vector>\n#include <algorithm>\nusing namespace std;\nclass Track \n{\npublic:\n    Track(float lengthInSeconds, string trackName) \n    {\n        m_lengthInSeconds = lengthInSeconds;\n        m_trackName = trackName;\n    }\n    float getLength() const \n    {\n        return m_lengthInSeconds;\n    }\n    string getName() const \n    {\n        return m_trackName;\n    }\nprivate:\n    float m_lengthInSeconds;\n    string m_trackName;\n};\n```\n\n`Playlist`类比较长，使用了 STL(排序和**向量**)的一些特性，这些特性在本书中还没有涉及到。理解类中的每一行代码并不重要(尽管我们敦促您看得更远一点，并尝试理解一些代码来拓宽您的知识)；更重要的是要理解对最终用户隐藏所有这些细节的概念。以下是`Playlist`类的定义:\n\n```cpp\nExample9_02.cpp\n1  class Playlist \n2  { \n3  public:   \n4      void AddTrack(const Track* track) \n5      { \n6          if(!any_of(m_tracks.begin(), m_tracks.end(), [&track](const Track* t){            return t->getName() == track->getName(); })) \n7          { \n8             m_tracks.push_back(track); \n9             return;\n10         } \n11         cout << \"Track: \" << track->getName()                 << \" Not added as already exists in playlist\"; \n12     } \n13 \n14     void RemoveTrack(const string trackName) \n15     { \n16         m_tracks.erase(remove_if(m_tracks.begin(), m_tracks.end(), \n17         [&trackName](const Track* t){ return t->getName() == trackName; })); \n18     } \n19 \n20     void PrintTracks() const \n21     {\n22         for (auto & track : m_tracks) \n23         {\n24             // round seconds \n25             int seconds = static_cast<int>(track->getLength()); \n26             std:: cout << track->getName() << \" - \" << seconds / 60 << \":\"                << seconds % 60 << endl; \n27         } \n28     } \nThe complete code can be found at: https://packt.live/3a4XPVa\n```\n\n这个班有很多，但这就是重点。很明显，所有这些功能不需要为`Playlist`类的用户所知，只需要知道作为公共接口提供的方法。下面是这个`Playlist`类的一个示例用例。从使用该类的人的角度来看，如下面的代码片段所示，`Playlist`类没有太多内容。所有的细节都被提取出来，现在以一个简单的`public`界面的形式呈现给用户:\n\n```cpp\nint main()\n{\n    Track t(100.0f, \"Donut Plains\");\n    Track t2(200.0f, \"Star World\");\n    Track t3(300.0f, \"Chocolate Island\");\n    Playlist p;\n    p.AddTrack( &t);\n    p.AddTrack( &t2);\n    p.AddTrack( &t3);\n    p.SortAlphabetically(false);\n    p.PrintTracks();\n    p.SortAlphabetically(true);\n    p.PrintTracks();\n    p.SortByLength(false);\n    p.PrintTracks();\n    p.SortByLength(true);\n    p.PrintTracks();\n    return 0;\n}\n```\n\n运行前面的代码片段后，您将获得以下输出:\n\n![Figure 9.11: Output for the Playlist class ](img/C14195_09_11.jpg)\n\n图 9.11:播放列表类的输出\n\n在外面，`Playlist`类使用起来很简单。所有的细节都在类本身内部，还有`Track`对象，从而确保`Playlist`类自己的数据不受外部干扰。这种类型的细节抽象意味着这些细节可以在不使用类的情况下进行更改，甚至不需要知道发生了更改。好的**封装**和**抽象**将代码从需要知道被使用对象的任何具体信息中分离出来，如果具体信息不重要，那么它们可以很容易地被更改。例如，`Track`对象可以以与前面示例完全不同的方式存储在`Playlist`类中，任何使用`Playlist`的东西都不需要了解它。\n\n## 活动 9:一个基本的 RPG 战斗系统\n\n既然您已经了解了封装和抽象，我们可以将它与我们关于创建类、获取器和设置器、构造器及其各种形式的知识结合起来。为了帮助巩固我们关于类的知识，我们现在将从头开始创建一个类，同时努力将我们所学到的关于最佳实践的一切都牢记在心。我们要创建一个非常简单的 **RPG 战斗系统**。一个 RPG 是一个角色扮演游戏，在这些游戏中，通常会有英雄和怪物轮流发动攻击和使用物品的战斗。这些攻击和物品的属性会以某种方式影响它们；我们将实现这个战斗系统的一个非常简单的版本的开始。完成活动后，字符名称及其项目的统计数据应显示在屏幕上。\n\n注意\n\n这个练习的完整代码可以在这里找到:[https://packt.live/3312hzy](https://packt.live/3312hzy)。\n\n以下是一些有助于您完成活动的步骤:\n\n1.  为字符、攻击和项目创建类，每个类中有一个`name`变量，可以在构造函数中设置。\n2.  给攻击一个攻击统计(T0)变量，给物品一个治疗统计(T1)变量。添加适当的获取器、设置器和构造器。\n3.  让字符在其构造函数中接受一系列攻击和项目，并存储它们，以便在需要使用时按名称查找。\n4.  创建攻击其他角色、使用物品和对攻击做出反应的函数。\n5.  创建名为`strengthMultiplier`和`defenseMultiplier`的成员变量。这些应该会影响角色的攻防统计。\n6.  创建一个函数，将字符的名称和其他统计信息打印到控制台。\n7.  用几个不同的字符测试`main`函数中的所有内容。\n\n以下是一些帮助您完成活动的提示:\n\n1.  记住三的规则。\n2.  回顾前几章，提醒自己如何在存储字符名称时创建动态`char`数组。\n3.  Feel free to use any containers that you may already be familiar with, but if intending to keep working with arrays (the solution uses arrays), then `memcpy` is the equivalent function to `strcpy` that you will need if the array is not a `char` array.\n\n    注意\n\n    这个活动的解决方案可以在第 549 页找到。然而，解决方案只是创建该系统的一种方式，不应被视为最终答案。\n\n# 总结\n\n在 C++ 中定义类型时，最佳实践的主题非常广泛，我们在本章中已经介绍了大量内容，以帮助您创建健壮且可维护的类。我们通过使用`private`关键字来封装数据，以确保我们决定如何访问该数据。我们查看了 getters 和 setters，以提供对数据的访问，并以一种可验证的方式对其进行修改。我们还研究了如何使用引用来访问我们的数据并直接修改它，以及当我们只希望在其他地方读取或使用数据而不更改对象的内部数据时，如何通过值返回数据。我们发现`const`可以用来确保任何我们不希望被改变的成员变量可以和成员函数一起被标记为这样。\n\n在下一章中，我们将看看可以使用什么来确保我们创建的任何动态对象都可以使用智能指针正确销毁。指针是 C++ 的主要部分，有自己的陷阱和最佳实践。您将了解普通指针和智能指针之间的区别，以及它们为什么重要。"
  },
  {
    "path": "docs/cpp-workshop/10.md",
    "content": "# 十、高级面向对象原则\n\n概观\n\n本章介绍了许多高级的面向对象原则，包括继承和多态，这将允许我们构建更复杂、动态和强大的 C++ 应用。您将通过从基类继承功能来创建新对象，实现虚拟函数和抽象类，使用多态性来创建通用代码，在类型之间安全转换，并使用高级 OOP 原则构建复杂的应用。\n\n# 简介\n\n贯穿*第 8 章*、*类和结构*和*第 9 章*、*面向对象原则*我们在 C++ 中介绍了面向对象原则。我们从查看类和结构开始，创建自己的用户定义对象来封装成员。然后，我们继续讨论一些基本的面向对象原则。\n\n在本章中，我们将讨论一些更高级的面向对象编程概念，如继承、虚拟成员函数、抽象类、多态性和类型间转换。有了对这些原则的理解，我们就可以真正开始利用使 C++ 成为多才多艺和强大的语言的伟大特性。\n\n我们将从继承开始，通过继承，我们可以在单个基类中定义公共功能，然后在唯一的子类中扩展它；这是 OOP 中的基本概念之一。这将引导我们关注虚拟成员函数。这些允许我们在这些公共基类中定义函数，这些函数可以在继承类中被覆盖。\n\n接下来，我们将把注意力转向多态性。通过多态性，我们能够根据调用函数的继承对象来调用同一个函数的不同实现。然后我们将使用`static_cast`和`dynamic_cast`来观察类型之间的转换，观察两者之间的差异。\n\n为了完成我们关于高级面向对象原则的工作，我们将完成一项活动，在这项活动中，我们创建一个小的百科全书应用，它将显示关于选定动物的各种信息。将创建一个基类来定义一个底层结构，然后我们将使用单个动物记录来扩展这个基类，利用多态性来获取它们的数据。当这一章完成时，你将对 OOP 的这些核心原则有一个很好的理解。\n\n# 遗传\n\n在 C++ 中声明一个类时，我们有能力从另一个类继承。事实上，我们可以同时从多个类继承——这是 C++ 的一个特性，不是所有面向对象的语言都有这个特性。当我们从另一个类继承时，我们获得了它的所有成员，这些成员具有公共或受保护的隐私修饰符。私有成员只对定义它们的类可见，而对继承类不可见。这是 OOP 中的基本概念之一，允许我们构建灵活、可维护的对象，其中公共功能只能声明一次，然后在需要的地方实现和扩展。\n\n让我们使用车辆，看一个快速的例子。我们可以定义一个基类`Vehicle`，它定义了一些常见的属性，比如最大速度或门的数量。然后，我们可以继承这个类来创建专门的车辆类，如`Car`、`Bike`或`Lorry`。我们创建共享一个公共基类的多个类，因此共享公共成员。\n\n要从类继承，我们将使用以下语法:\n\n```cpp\nclass DerivedClassName : [access modifier] BaseClassName\n```\n\n我们将我们的类定义为普通类，然后使用`:`操作符开始声明我们想要继承的类。首先，我们提供一个访问修饰符。我们将很快介绍不同修饰符对继承的影响，但结果是它们决定了继承成员的可见性。接下来，我们简单地声明我们希望继承的类的名称:\n\n```cpp\nclass MyBaseClass\n{\n};\nclass MyDerivedClass : public MyBaseClass\n{\n};\n```\n\n我们的派生类现在可以访问基类中声明的所有公共成员和受保护成员。记住，私有成员只能被声明它们的类和朋友类访问。\n\n注意\n\n一个类的私有和受保护成员可以被声明为该类的朋友的其他类访问。朋友课不在这本书的范围之内。不过，更多信息可以在这里找到:[https://packt.live/37vA8ns](https://packt.live/37vA8ns)。\n\n这种关系是从基类继承而来的派生类，可以在下面的简单图表中看到:\n\n![Figure 10.1: Single inheritance diagram ](img/C14195_10_01.jpg)\n\n图 10.1:单一继承图\n\n如果我们想禁止从继承一个类，C++ 11 为我们提供了`final`关键字:\n\n```cpp\nclass MyBaseClass final\n{\n};\nclass MyDerivedClass : public MyBaseClass\n{\n};\n```\n\n在这种情况下，代码将无法编译，给我们一个错误，说明`MyBaseClass`是`final`:\n\n![Figure 10.2: Compilation error since MyBaseClass is declared final ](img/C14195_10_02.jpg)\n\n图 10.2:编译错误，因为我的基类被声明为最终类\n\n使类成为最终类的主要原因之一是，您可以确保它们不是从继承而来的，因此是您想要的确切实现。例如，如果你正在编写一个公共图书馆，你可能有一个`Record`类。这个类的实现应该是完全一样的，这可能是非常重要的，所以您可以将这个类标记为 final，以防止任何人继承它并使用他们的用户定义版本。不管你的理由是什么，将一个类标记为 final 意味着没有类可以继承它。\n\n让我们看一个代码继承的例子。想象我们有三个物体，它们都有一个共同的成员；说出形状和它们的面积。如果我们单独定义这三个类，我们会得到这样的结果:\n\n```cpp\nclass Square\n{\npublic:\n    int area = 10;\n};\nclass Circle\n{\npublic:\n    int area = 10;\n};\nclass Triangle\n{\npublic:\n    int area = 10;\n};\n```\n\n我们可以看到这里有共同的代码。我们已经为每个形状声明了相同的成员，这是不必要的。因为它在每个类之间是通用的，所以我们可以把它移到自己的类中，让其他类继承它。这在两者之间创造了一种关系。具有公共功能的类被称为**基**类，继承该行为的类被称为**派生的**类。\n\n让我们将这些类和它们的公共成员移到一个基类中。\n\n## 练习 65:遗传\n\n在前面的代码片段中，我们看到了当我们声明三个 shape 类时，我们是如何以代码重复结束的，每个类都有一个用于 area 的成员变量。让我们利用继承来重构这段代码:\n\n注意\n\n这个练习的完整代码可以在这里找到:[https://packt.live/2XE3HyT](https://packt.live/2XE3HyT)。\n\n1.  Declare a base class, `Shape`, that will contain the shared member. We'll also add a function to return it:\n\n    ```cpp\n    #include <iostream>\n    class Shape\n    {\n    public:\n        int area = 10;\n        int GetArea() { return area; }\n    };\n    ```\n\n    注意\n\n    由于我们的`area`变量是公共的，这里的`get`函数并不是严格必要的。这里只是用一个函数来演示继承。\n\n2.  接下来，声明我们的三个单独的形状类。然而，这一次，我们不再像以前那样在每个类中声明`area`成员，而是从我们新的`Shape`类中继承它:\n\n    ```cpp\n    class Square : public Shape\n    {\n    };\n    class Circle : public Shape\n    {\n    };\n    class Triangle : public Shape\n    {\n    };\n    ```\n\n3.  在我们的`main`函数中实例化这些类中的一个:\n\n    ```cpp\n    int main()\n    {\n        Square mySquare;\n        Circle myCircle;\n        Triangle myTriangle;\n    ```\n\n4.  现在，这就是我们看到继承在发挥作用的地方。对于我们刚刚创建的`Square`类，我们将为`area`成员设置一个值，然后调用`GetArea()`方法将其打印到控制台:\n\n    ```cpp\n        mySquare.area = 5;\n        std::cout << \"Square Area: \" << mySquare.GetArea() << std::endl;\n    ```\n\n5.  对`Circle`班也这样做，但对`Triangle`班不这样做。我们只打印这个值，不为继承的成员赋予新的值:\n\n    ```cpp\n        myCircle.area = 15;\n        std::cout << \"Circle Area: \" << myCircle.GetArea() << std::endl;\n        std::cout << \"Triangle Area: \" << myTriangle.GetArea()               << std::endl;\n    }\n    ```\n\n6.  运行应用:\n\n![Figure 10.3: Accessing the area member defined in the base Shape class ](img/C14195_10_03.jpg)\n\n图 10.3:访问在基本形状类中定义的区域成员\n\n从输出和我们的程序编译无误的事实可以看出，我们的三个类`Square`、`Circle`和`Triangle`都继承了`Shape`类的两个成员。在`Square`和`Circle`中，我们给继承的成员变量一个新的值，这在我们调用`GetArea`时得到了反映。在`Triangle`中，我们没有看到的地方，我们可以看到`Shape`中定义的原始值被输出。\n\n我们可以继续在这个基类中定义任何进一步的共享属性或功能，让任何派生类自由地给它们唯一的值。这是继承背后的主要原则；我们在基类中定义共享成员，让继承类进行专门化。\n\n# 多重遗传\n\n在前面的例子中，我们创建了一个继承自单个基类的派生类，但是 C++ 的许多伟大特性之一是支持多重继承。这意味着单个派生类可以从多个基类继承变量和功能，以创建更复杂的对象。我们所知道的关于单一继承的一切都是正确的，唯一的区别是继承的成员来自多个来源。\n\n从多个类继承的语法如下:\n\n```cpp\nclass DerivedClassName : [access modifier] BaseClassName, [access modifier] AnotherBaseClassName\n```\n\n下面的继承图显示了定向类如何有两个基类，它将从这两个基类继承成员:\n\n![Figure 10.4: Multiple inheritance diagram ](img/C14195_10_04.jpg)\n\n图 10.4:多重继承图\n\nC++ 没有对可以继承的类的数量实施硬性限制；它是特定于实现的，尽管 C++ 标准确实提供了推荐的最小值:\n\n*   直接和间接基类[16，384]\n*   单个类的直接基类[1，024]\n*   一类的直接和间接虚基[1，024]\n\n让我们来看看多重继承的作用:\n\n```cpp\nclass MyClassA\n{\nprotected:\n    int myInt;\n};\nclass MyClassB\n{\nprotected:\n    std::string myString;\n};\nclass MyClassC: public MyClassA, public MyClassB\n{\n    MyClassC()\n    {\n        myInt = 1;\n        myString = 2;\n    }\n};\n```\n\n在前面的代码片段中，我们定义了两个基类，`MyClassA`和`MyClassB`。然后，我们创建派生类型`MyClassC`，并从它们两者继承。`MyClassC`现在可以访问两者的成员。这有助于我们从多个来源继承价值观和行为，但是有一些事情需要注意。\n\n第一个问题被称为钻石问题，因其继承图的形状而得名，是一个类从两个基类继承的结果，这两个基类本身共享一个公共基。这在图表中可以看得更清楚:\n\n![Figure 10.5: Diamond problem ](img/C14195_10_05.jpg)\n\n图 10.5:钻石问题\n\n在这个图中，我们可以看到`MyClassB`和`MyClassC`都是从`MyClassA`继承的。`MyClassD`然后去继承`MyClassB`和`MyClassC`。这导致`MyClassD`在`MyClassA`中拥有所有内容的两个副本，因为它被实例化了两次，一次来自`MyClassB`，一次来自`MyClassC`。在代码中，这将如下所示:\n\n```cpp\n// Diamond problem example. \n#include <iostream> \n#include <string> \nclass MyClassA\n{\nprotected:\n    int myInt;\n};\nclass MyClassB: public MyClassA\n{\n};\nclass MyClassC: public MyClassA\n{\n};\nclass MyClassD: public MyClassB, public MyClassC\n{\n    MyClassD()\n    {\n        myInt = 1;\n    }\n};\nint main()\n{\n}\n```\n\n如果我们试着运行这段代码，我们会得到一个错误，说明`myInt`不明确。那是因为`MyClassA`被实例化了两次，所以有两个版本。编译器不知道使用哪一个:\n\n![Figure 10.6: Properties accessed in scope ](img/C14195_10_06.jpg)\n\n图 10.6:在范围内访问的属性\n\n这可以通过两种方式避免。首先是限定您想要访问的变量版本:\n\n```cpp\nclass MyClassD : public MyClassB, public MyClassC\n{\n    MyClassD()\n    {\n        MyClassB::myInt = 1;\n    }\n};\n```\n\n这很好，因为我们已经通过在它前面加上`MyClassB::`来限定了我们想要使用的`myInt`版本。这确保我们可以访问它的`MyClassB`版本。\n\n第二种解决方案是通过使用虚拟继承。当我们从类继承时使用`virtual`关键字时，我们确保任何派生类都只继承基类成员变量的一个副本:\n\n```cpp\nclass MyClassB : public virtual MyClassA\n{\n};\nclass MyClassC : public virtual MyClassA\n{\n};\n```\n\n现在`MyClassB`、`MyClassC`虚拟继承自`MyClassA`，其构造函数将只从`MyClassD`直接调用一次。这避免了重复的属性，减轻了钻石问题。\n\n## 练习 66:多重遗传\n\n让我们扩展*练习 65* 、*继承*，利用多重继承。我们从`Shape`基类继承来提供区域成员，所以我们从第二个类继承来继承一些颜色成员:\n\n注意\n\n这个练习的完整代码可以在这里找到:[https://packt.live/2OBiGFB](https://packt.live/2OBiGFB)。\n\n1.  将*练习 65* 、*继承*的代码复制到编译器窗口。\n2.  添加一个定义`color`变量的新类`Color`，以及一个返回它的方法:\n\n    ```cpp\n    class Color\n    {\n    public:\n        std::string color = \"\";\n        std::string GetColor() { return color; }\n    };\n    ```\n\n3.  接下来，更新我们所有的派生类，也继承这个新类，以及原来的`Shape`类:\n\n    ```cpp\n    class Square : public Shape, public Color\n    //[...]\n    class Circle : public Shape, public Color\n    //[...]\n    class Triangle : public Shape, public Color\n    ```\n\n4.  在代码中，我们设置了`Square`区域变量，然后在`cout`语句中返回它。让我们对我们的新`color`成员\n\n    ```cpp\n        mySquare.area = 5;\n        mySquare.color = \"red\";\n        std::cout << \"Square Area: \" << mySquare.GetArea() << std::endl;\n        std::cout << \"Square Color: \" << mySquare.GetColor() << std::endl;\n    ```\n\n    做同样的事情\n5.  对另外两个派生类重复这些步骤:\n\n    ```cpp\n        myCircle.area = 10;\n        myCircle.color = \"blue\";\n        std::cout << \"Circle Area: \" << myCircle.GetArea() << std::endl;\n        std::cout << \"Circle Color: \" << myCircle.GetColor() << std::endl;\n        myTriangle.area = 15;\n        myTriangle.color = \"green\";\n        std::cout << \"Triangle Area: \" << myTriangle.GetArea()               << std::endl;\n        std::cout << \"Triangle Color: \" << myTriangle.GetColor()               << std::endl;\n    ```\n\n6.  运行应用。您将获得以下输出:\n\n![Figure 10.7: Accessing members from both base classes ](img/C14195_10_07.jpg)\n\n图 10.7:访问两个基类的成员\n\n现在我们已经继承了两个类，我们可以访问两组成员:`Shape`类的`area`和`GetArea`，以及`Color`类的`color`和`GetColor`。\n\n到目前为止，在我们的例子中，我们使用了公共可访问性，因为这意味着所有成员在任何地方都是可见的。然而，这只是为了演示，并不是我们在系统中通常想要的，因为它会导致潜在的误用。一般来说，我们的成员应该尽可能拥有最严格的可见性。在下一节中，看看可访问性是如何与继承一起工作的。\n\n## 访问修饰符和继承\n\n在利用继承时，我们需要注意两个方面的可访问性。第一个是基类成员的可访问性，第二个是从类继承时定义的访问修饰符。我们将从第一个开始，因为它已经在前面的章节中提到过了。\n\n在声明成员时，我们有三个访问修饰符可以用来确定它们的可见性:\n\n*   **公共**:随处可见\n*   **受保护的**:对定义它们的类和任何派生类可见\n*   **私有**:只对定义它们的类可见\n\n这意味着，如果我们希望一个变量可以被派生类访问，那么它必须具有公共或受保护的可见性。然而，这仅决定了该成员对派生类本身是否可见，而不是对其他成员可见。为此，我们转向从类继承时声明的访问修饰符。\n\n如果您记得，从类继承的语法如下:\n\n```cpp\nclass DerivedClassName : [access modifier] BaseClassName\n```\n\n我们在此提供的访问修饰符与单个基本成员上的修饰符一起使用，以确定它们的可见性；限制性最强的修改器获胜。下表显示了不同的继承类型如何与基类成员的访问修饰符交互:\n\n![Figure 10.8: Access modifier combinations ](img/C14195_10_08.jpg)\n\n图 10.8:访问修饰符组合\n\n如果我们看一下这张表中所有可能的组合，前面的说法“限制性最强的修饰语获胜”应该会变得更清楚。每当两个不同的修饰符组合在一起时(比如一个被私有继承的受保护的基类变量)，最具限制性的修饰符就会出现；在这种情况下，它将是私有的。\n\n## 练习 67:访问修饰符和继承\n\n为了更好地了解访问修饰符是如何影响事物的，让我们创建一个利用它们的程序。我们将创建一个包含三个成员的基类:一个公共的、一个受保护的和一个私有的。然后，我们将使用各种访问修饰符从这个类继承，以查看每个成员的可见性:\n\n注意\n\n这个练习的完整代码可以在这里找到:[https://packt.live/2D97LxH](https://packt.live/2D97LxH)。\n\n1.  我们将从声明基类开始。我们将继续形状示例(这是一个经过试验和测试的类比)并声明三个成员，给出三个可能的访问修饰符中的每一个:\n\n    ```cpp\n    #include <iostream>\n    #include <string>\n    class Shape\n    {\n    public:\n        int area = 0;\n    protected:\n        std::string color = \"\";\n    private:\n        bool hasOutline = false;\n    };\n    ```\n\n2.  接下来，我们将从这个类继承，创建派生的`Square`类。对于第一个例子，我们将使用公共继承:\n\n    ```cpp\n    class Square : private Shape\n    {\n    public:\n        Square()\n        {\n            area = 5;\n            color = \"red\";\n            hasOutline = true;\n        };\n    };\n    ```\n\n3.  为了测试成员的可见性，我们将实例化这个派生类，并尝试在`cout`语句中访问它的每个成员:\n\n    ```cpp\n    int main()\n    {\n        Square mySquare;\n        std::cout << \"Square Area: \" << mySquare.area << std::endl;\n        std::cout << \"Square Color: \" << mySquare.color << std::endl;\n        std::cout << \"Square Has Outline: \" << mySquare.hasOutline               << std::endl;\n    }\n    ```\n\n4.  Let's run this application and see what our compiler gives us:\n\n    ![Figure 10.9: Errors to the accessibility of the members ](img/C14195_10_09.jpg)\n\n    图 10.9:成员可访问性的错误\n\n    在本例中，我们创建了一个具有公共继承的派生类。首先，我们无法访问`Square`构造函数中的`hasOutline`成员。我们遇到以下错误:\n\n    ```cpp\n        error: 'bool Shape::hasOutline' is private\n    ```\n\n    该成员在基类中是私有的，因此派生类无法访问它。\n\n    接下来，如果我们查看`main`函数内的代码，我们可以看到访问`area`变量时没有错误。该成员在基类中是公共的，因此它仍然是公共的，可以自由访问。然而，访问`color`成员会给我们以下错误:\n\n    ```cpp\n        'std::string Shape::color' is protected\n    ```\n\n    即使我们使用了公共继承，基类的受保护修饰符的限制性更强，所以这是被使用的一个。这意味着我们不能公开访问这个变量。我们在试图访问`hasOutline`时也会得到一个错误:\n\n    ```cpp\n        'bool Shape::hasOutline' is private\n    ```\n\n    这也是因为基类赋予了这个变量私有访问权。它甚至对派生类都不可见，所以肯定不能被公开访问。\n\n5.  现在，将继承中使用的访问修饰符更改为`protected`。运行应用并通读编译器输出，就像我们之前做的那样。\n6.  最后，将继承中使用的访问修饰符更改为`private`并再次执行相同的操作。你应该有希望知道这会导致什么样的错误。如果需要，请参考前面给出的图表进行说明。\n\n理解不同的访问修饰符如何影响继承是很重要的，而且经常会引起混淆。首先，不管访问修饰符是什么，所有变量对于定义它们的类都是完全可见的。派生类(从基类继承的那些)可以访问公共和受保护的成员。最后，从基类继承时使用的访问修饰符决定了成员的最终可见性，从而决定了所有其他类如何访问它们。前面的图表显示了所有可能的组合，但请记住，最严格的修饰符将总是被选择。\n\n## 虚拟功能\n\n当我们从基类继承时，我们已经看到我们可以访问任何公共的和受保护的成员。对于成员变量，我们接着在派生类中赋予它们唯一的值；但是有了函数，我们只需访问并调用它们。然而，在派生类中专门化一个函数是可能的，就像给成员变量一个唯一的值一样。我们通过使用虚函数来做到这一点。\n\n在 C++ 中，虚函数是一种可以被派生类重写其功能的函数。要将一个函数标记为虚函数，我们只需在其声明的开头使用`virtual`关键字:\n\n```cpp\nvirtual return_type function_name();\n```\n\n这样就可以在派生类中重写该函数。首先，通过声明一个具有相同签名、返回类型、名称和 override 关键字的函数，然后定义它。让我们看一个例子:\n\n```cpp\nclass MyBaseClass\n{\npublic:\n    virtual void PrintMessage() \n    {\n        std::cout << \"Hello \";\n    }\n};\nclass MyDerivedClass: public MyBaseClass \n{\npublic: \n    void PrintMessage() override \n    {\n        std::cout << \"World!\";\n    }\n};\n```\n\n在这段代码中，我们定义了两个类:`MyBaseClass`和`MyDerivedClass`。在`MyBaseClass`中，我们声明了一个虚拟的`PrintMessage`函数，它将把`Hello`这个词打印到控制台上。然后我们在`MyDerivedClass`中继承这个类，并覆盖函数来打印单词`World`。如果我们实例化`MyDerivedClass`并调用它的`PrintMessage`函数，你认为我们会看到什么？\n\n![Figure 10.10: The output from our overridden virtual function ](img/C14195_10_10.jpg)\n\n图 10.10:我们被覆盖的虚拟函数的输出\n\n我们看到`World!`这个词，表示没有调用基函数，但是派生类中的覆盖函数调用了。如果我们查看源代码，您可以在派生类中的函数定义之后看到' override '关键字。这个可选的标识符不仅让程序员清楚这是一个被覆盖的虚函数，而且导致编译时检查，以确保它是基函数的有效覆盖。没有这个标识符，重写虚函数也能正常工作，但是包含它是一个很好的做法。\n\n注意\n\n与`virtual`不同，override 不是关键字。相反，它是一个具有特殊意义的标识符。它在虚函数的上下文之外没有特殊的意义。\n\n因此，当我们重写一个虚函数并调用它时，它将调用派生类中定义的版本。但是如果我们也要调用基本定义呢？谢天谢地，这是可能的。在我们被覆盖的`virtual`函数中，也可以调用基础实现。这是通过以下语法完成的:\n\n```cpp\nvoid MyFunction()\n{\n    BaseClass::MyFunction();\n}\n```\n\n在我们的重写函数中，我们可以通过基类类型调用基类函数。这将在运行覆盖函数的逻辑之前运行函数的基础版本中定义的逻辑。让我们更新我们的示例，看看这个:\n\n```cpp\nclass MyDerivedClass : public MyBaseClass\n{\npublic:\n    void PrintMessage() override\n    {\n        MyBaseClass::PrintMessage();\n        std::cout << \"World!\";\n     }\n};\n```\n\n我们已经更新了我们的覆盖函数，首先调用`MyBaseClass`实现。让我们看看如果我们现在运行应用会得到什么:\n\n![Figure 10.11: The functionality of both implementations ](img/C14195_10_11.jpg)\n\n图 10.11:两种实现的功能\n\n由于我们通过调用基本功能开始覆盖函数实现，我们首先输出`Hello`，然后返回处理派生函数中的逻辑。这会将`Hello World!,`全部打印到控制台上。这可能非常有用。您可以在基础实现中定义任何公共功能，然后在派生实现中将其专门化。\n\n再次以电子游戏为例，项目系统可以利用这一点。假设我们定义了一个名为`Item`的基类，它包含一些泛型成员，包括一个从玩家那里获取能量的`Use`函数。然后我们可以继续从这个继承来创建任意多的派生项目类型，在每个类型中实现`Use`函数。也许对于一个`Health Potion`物品，我们给玩家一些生命值；或者对于一个`Torch`物品，我们创造一个光。这两个派生类不仅可以存储在类型为`Item*`的公共容器中，还可以在它们自己的实现之前调用`Use`的基础实现。\n\n## 纯虚函数/抽象类\n\n覆盖正常的`virtual`功能是可选的；然而，如果我们想强迫我们的用户在派生类中实现一个虚函数，我们可以在基类中使它成为纯虚函数。纯虚函数在基类中没有实现，它只是被声明。纯虚函数的语法如下:\n\n```cpp\nvirtual void MyFunction() = 0;\n```\n\n当一个类包含一个或多个纯虚函数时，它就变成了一个抽象类。这是一个不能直接实例化的类。这里有一个例子:\n\n```cpp\nclass MyAbstractClass\n{\n    virtual void MyPureVirtualFunction() = 0;\n};\nclass MyDerivedClass : public MyAbstractClass\n{\n    void MyPureVirtualFunction() override\n    {\n        std::cout << \"Hello World!\";\n    }\n};\nint main()\n{\n    MyAbstractClass myAbstractClass;\n}\n```\n\n在这段代码中，我们在基类中定义了一个纯虚函数。然后我们在`MyDerivedClass`中继承这个类，并为函数提供一个定义。在我们的主函数中，我们尝试实例化抽象类的一个实例。让我们运行这个，看看编译器给了我们什么:\n\n![Figure 10.12: Trying to instantiate an abstract class ](img/C14195_10_12.jpg)\n\n图 10.12:尝试实例化一个抽象类\n\n编译器对我们试图实例化这个类不满意，因为没有函数的定义。如果我们改为实例化我们的派生类，编译器会同意，因为我们已经提供了一个定义。如果我们从派生类中省略这个定义，它也会变得抽象，因此不能直接实例化。\n\n如果我们不想在基类中提供定义，但仍然想使重写函数成为可选的，我们可以给它一个空的主体:\n\n```cpp\nvirtual void MyPureVirtualFunction() {}\n```\n\n如果我们更新我们的代码来声明`MyPureVirtualFunction`(就像我们之前做的那样)，我们的代码将会编译。因为我们给了它一个空体，类不会变得抽象；我们只是有一个什么都不做的功能。\n\n抽象类对于控制我们的用户可以实例化什么和不能实例化什么非常有用。一个很好的例子是视频游戏引擎中的对象系统。通常有一个基类叫做`Object`之类的东西。这将定义所有对象将拥有的共享特征，例如唯一的 GUID，并将作为所有其他对象的基类——一个`player`对象。由于基类纯粹是为了提供共享的功能和属性，但本身并没有什么用处，所以我们可以将它做成一个抽象类，以确保它不能被直接实例化。它只能从继承以创建派生类。\n\n## 练习 68:虚拟功能\n\n让我们把这种对虚函数的新理解运用起来；`shape`的例子将很好地工作。我们已经声明了一个基类`Shape`并从它继承来创建特殊的形状，比如圆形和正方形。我们的形状类本身不是很有用；它不包含任何特定的内容，主要目的是提供共享的功能和成员，是抽象类的完美候选。\n\n注意\n\n这个练习的完整代码可以在这里找到:[https://packt.live/2D7zNd2](https://packt.live/2D7zNd2)。\n\n我们将抽象这个类，并提供一个虚拟函数来计算形状的面积:\n\n1.  让我们从定义我们的基础`Shape`类开始。我们希望我们的共享成员在这里被宣布；存储形状面积的整数和计算它的函数。我们可以使用一些访问修饰符来确保那些不需要公开的变量不是:\n\n    ```cpp\n    #include <iostream>\n    #include <string>\n    class Shape\n    {\n    public:\n        virtual int CalculateArea() = 0;\n    protected:\n        int area = 0;\n    };\n    ```\n\n2.  现在我们将声明我们的第一个派生类`Square`。这个派生类想要用适当的计算覆盖`CalculateArea`函数，并为正方形的高度提供自己的变量。我们将公开继承`Shape`:\n\n    ```cpp\n    class Square : public Shape\n    {\n    public:\n        int height = 0;\n        int CalculateArea() override\n        {\n            area = height * height;\n            return area;\n        }\n    };\n    ```\n\n3.  接下来，创建另一个派生类`Circle`。这将类似于我们的`Square`类，但是我们将提供一个`radius`变量，而不是提供一个`height`变量。我们还将在`CalculateArea`功能中更新计算:\n\n    ```cpp\n    class Circle : public Shape\n    {\n    public:\n        int radius = 0;\n        int CalculateArea() override\n        {\n            area = 3.14 * (radius * radius);\n            return area;\n        }\n    };\n    ```\n\n4.  在我们的`main`函数中，我们现在将实例化这些派生类，设置我们为它们声明的成员变量，并调用`CalculateArea`函数。我们从`Square` :\n\n    ```cpp\n    int main()\n    {\n        Square square;\n        square.height = 10;\n        std::cout << \"Square Area: \" << square.CalculateArea()               << std::endl;\n    ```\n\n    开始\n5.  最后，我们将为我们的`Circle`类做同样的事情，完成我们的应用:\n\n    ```cpp\n        Circle circle;\n        circle.radius = 10;\n        std::cout << \"Circle Area: \" << circle.CalculateArea()               << std::endl;\n    }\n    ```\n\n6.  运行应用。您将获得以下输出:\n\n![Figure 10.13: We overrode CalculateArea, specializing that function in each derived class ](img/C14195_10_13.jpg)\n\n图 10.13:我们覆盖了 CalculateArea，在每个派生类中专门化了这个函数\n\n正如我们所看到的，我们被覆盖的`CalculateArea`函数已经被每个派生类成功调用。我们的基类`Shape`提供了泛型基本信息，我们将`CalculateArea`函数设为纯虚函数，以确保它不能被直接实例化。如果您试图在这个应用中这样做，您会得到一个编译器错误。即使是这个微不足道的例子也展示了如何使用这个强大的特性来控制哪些对象可以实例化，哪些对象不能实例化，以及如何创建共享类似接口的类的专用版本。\n\n# 多态性\n\n我们现在已经看到了如何使用继承来创建对象的通用基础版本，然后将它们专门化为派生类。这样做的许多好处包括减少代码重复、实现公共接口的能力以及多态性。\n\n多态性允许我们根据调用函数的继承对象来调用同一个函数的不同实现。我们可以这样做，因为我们可以将派生类型存储在它们的基类型的指针变量中。当我们这样做的时候，我们限制自己只能访问在基类中声明的成员，但是当它被调用的时候，我们将得到派生类的实现。\n\n让我们看看一些代码，看看这是如何操作的:\n\n```cpp\n// Polymorphism. \n#include <iostream>\n#include <string>\nclass MyClassA \n{\npublic:\n    virtual std::string GetString() = 0;\n};\nclass MyClassB: public MyClassA \n{\npublic: \n    std::string GetString() override \n    {\n        return \"Hello \";\n    }\n};\nclass MyClassC: public MyClassA \n{\npublic: \n    std::string GetString() override \n    {\n        return \" world!\";\n    }\n};\nint main()\n{\n    MyClassA * myClass = new MyClassB(); \n    std::cout << myClass->GetString(); \n    myClass = new MyClassC(); \n    std::cout << myClass->GetString();\n    delete myClass;\n    myClass = nullptr;\n}\n```\n\n我们在这里创建了两个派生对象，`MyClassB`和`MyClassC`，它们都继承自`MyClassA`。由于这些对象共享公共基类`MyClassA`，我们可以将它们存储在指向该类型的指针中(`MyClassA*`)并访问该基类中声明的任何成员。然而，当我们调用它们时，我们得到了它们的派生实现。\n\n如果我们运行代码，我们可以看到这一点:\n\n![Figure 10.14: We've called two different implementations of a function from the same object type ](img/C14195_10_14.jpg)\n\n图 10.14:我们从同一个对象类型中调用了一个函数的两种不同实现\n\n尽管对同一个变量`myClass`调用了函数，但我们得到了不同的结果，因为它存储了不同的派生类。这就是多态性在起作用。需要注意的是，多态性只适用于非值类型，即引用和指针。关于多态性，它们的作用基本相同；但是，引用在法律上不能为空。这意味着`dynamic_cast`将改为对失败的演员抛出异常，而不是返回`nullptr`。\n\n选角很重要，将在本章的下一部分介绍，但是在我们开始之前，让我们在前面的练习基础上继续。我们将以多形态存储我们的派生形状类，并根据最初存储的派生类型来看如何获得不同的实现。\n\n## 练习 69:多态性\n\n在*练习 68* 、*虚拟函数*中，我们使用了虚拟覆盖函数来提供`GetArea()`的多个实现。让我们做一些类似的事情，但是这一次我们将多形态地存储这些类型。我们将看到，即使我们有两个相同类型的变量，因为我们分配了不同的派生类，我们的函数调用的实现也会有所不同。\n\n注意\n\n这个练习的完整代码可以在这里找到:[https://packt.live/2OBx5Bz](https://packt.live/2OBx5Bz)。\n\n1.  将上一练习的代码复制到您的编译器中。\n2.  接下来，给出`Square`和`Circle`中的成员变量默认值:\n\n    ```cpp\n    class Square : public Shape\n    {\n    public:\n        int height = 10;\n        int CalculateArea() override\n        {\n            area = height * height;\n            return area;\n        }\n    };\n    class Circle : public Shape\n    {\n    public:\n        int radius = 10;\n        int CalculateArea() override\n        {\n            area = 3.14 * (radius * radius);\n            return area;\n        }\n    };\n    ```\n\n3.  Now we can implement polymorphism. We're currently instantiating an instance of each of our derived classes. The `square` variable is of type `Square`, and the `circle` variable is of type `Circle`. Let's change this so both of them are of type `Shape*`—that is, a pointer to a `Shape` object:\n\n    ```cpp\n        Shape* square = new Square();\n        Shape* circle = new Circle();\n    ```\n\n    注意\n\n    虽然我们在这里使用的是原始指针，但是您也可以使用智能指针，如前一章所述。为了简单起见，这里使用原始指针来保持对手头主题的关注。\n\n4.  因为我们现在使用的是指向基类的指针，所以我们不能再访问`height`和`radius`变量。这将在下一部分中讨论，我们将在这里讨论铸造。现在，取消那些电话。\n5.  Finally, with our square and circle variables now pointers, we need to change how we access the `CalculateArea` methods. We need to use the `->` operator instead of the `.` operator. We also need to delete the pointer:\n\n    ```cpp\n        std::cout << \"Square Area: \" << square->CalculateArea()               << std::endl;\n        std::cout << \"Circle Area: \" << circle->CalculateArea()               << std::endl;\n        delete square;\n        square = nullptr;\n        delete circle;\n        circle = nullptr;\n    ```\n\n    注意\n\n    删除这里的指针并不是绝对必要的，因为我们的应用无论如何都会终止。然而，将任何对`new`的调用与对`delete`的调用相匹配总是一种好的做法。这将节省潜在的内存泄漏。\n\n6.  运行程序。您将获得以下输出:\n\n![Figure 10.15: Using polymorphism, we've stored our derived types as pointers to their base ](img/C14195_10_15.jpg)\n\n图 10.15:使用多态性，我们将派生类型存储为指向它们的基的指针\n\n在这个应用中，我们展示了如何将继承的类型多形态地存储为指向它们的基的指针。当我们调用这个对象上的函数时，我们得到了派生类提供的实现。如果没有提供被覆盖的实现，我们将返回到调用基类实现。\n\n# 类型间铸造\n\n既然我们可以多形态地存储和交互类型，我们需要知道如何在它们之间进行转换。铸造是将一个对象从一种类型转换成另一种类型的过程。如果我们将派生类型存储在类型为基的集合中，这一点很重要。在这种情况下，我们需要从基类型转换为派生类型。这称为向下转换，需要进行类型检查。我们也可以从派生类型转换为基类，这称为上转换。这些总是被允许的。\n\n请考虑以下几点:\n\n```cpp\n// Casting. \n#include <iostream>\n#include <string>\nclass MyClassA \n{\npublic:\n    int myInt = 0;\n};\nclass MyClassB: public MyClassA \n{\npublic: \n    std::string myString = \"\";\n};\nint main() \n{\n    MyClassA * myClass = new MyClassB();\n    std::cout << myClass->myInt << std::endl;\n    std::cout << myClass->myString << std::endl;\n    delete myClass;\n    myClass = nullptr;\n}\n```\n\n在这个例子中，我们有从`MyClassA`继承的`MyClassB`。我们实例化`MyClassB`，将其存储在指向`MyClassA`的指针中，然后尝试访问两者的成员。如果我们运行这个应用，我们会得到什么？\n\n![Figure 10.16: We're unable to access the members declared in the derived class from the base ](img/C14195_10_16.jpg)\n\n图 10.16:我们无法从基类访问派生类中声明的成员\n\n我们得到一个编译错误。因为我们使用的是`MyClassA`对象，所以我们只能访问该类的成员。为了访问派生成员，我们需要转换为类型。我们将在本章中介绍三种类型的演员阵容:`static_cast`、`dynamic_cast`和 C 风格的演员阵容。\n\n## 静态铸造\n\n让我们从一个`static_cast`开始。当你确定你正在处理一个特定类型的对象时，使用`static_cast`。因此，不进行任何检查。例如，在我们的示例中，我们显然存储了一个类型为`MyClassB`的对象，因此我们可以安全地将`static_cast`转换为该类型。\n\n`static_cast`的语法如下:\n\n```cpp\n    static_cast<type_to_cast_to*>(object_to_cast_from);\n```\n\n如果我们将它应用于前面的代码示例，我们可以转换为我们的派生类型，然后访问成员就不是问题了:\n\n```cpp\nint main()\n{\n    MyClassA* myClass = new MyClassB();\n    std::cout << myClass->myInt << std::endl;\n    MyClassB* myClassB = static_cast<MyClassB*>(myClass);\n    std::cout << myClassB->myString << std::endl;\n    delete myClass;\n    myClass = nullptr;\n    delete myClassB;\n    myClassB = nullptr;\n}\n```\n\n这段代码现在可以编译了，我们可以访问对象的`myString`成员。\n\n## 动态铸造\n\n当我们不确定使用的是哪种类型的对象时，会使用第二个强制类型`dynamic_cast`。如果我们尝试动态转换，但失败了，就会返回`nullptr`。然后我们可以检查我们的对象是否有效。\n\n`dynamic_cast`的语法如下:\n\n```cpp\n    dynamic_cast<type_to_cast_to*>(object_to_cast);\n```\n\n要使`dynamic_cast`在向下转换时工作，基类必须包含至少一个虚函数。如果我们试图将`MyClassA`降级为`MyClassB`，我们会得到一个编译器错误，如下图所示:\n\n```cpp\n    MyClassB* myClassB = dynamic_cast<MyClassB*>(myClass);\n    if (myClassB != nullptr)\n    {\n    std::cout << myClassB->myString << std::endl;\n    }\n```\n\n错误如下:\n\n![Figure 10.17: We're unable to downcast from MyClassA to MyClassB since it's not a polymorphic type ](img/C14195_10_17.jpg)\n\n图 10.17:我们无法从我的类 a 向下转换到我的类 b，因为它不是多态类型\n\n然而，如果`MyClassA`包含一个`virtual`函数——因此是一个多态类型——这就可以了。使用`dynamic_cast`比`static_cast`更安全，因为如果强制转换失败，它将返回空指针。\n\n## C 型演员表\n\n最后，C 风格的强制转换，或者常规的强制转换，尝试多种不同的强制转换，选择第一种有效的。然而这还不包括`dynamic_cast`，所以和`static_cast`一样不安全。\n\nC 风格转换的语法如下:\n\n```cpp\n(type_to_cast_to *) object_to_cast\n```\n\n例如，如果我们使用 C 风格的演员表将`MyClassA`转换为`MyClassB`，我们会这样做:\n\n```cpp\n    MyClassB* myClassB = (MyClassB*)myClass;\n```\n\n既然我们知道`myClass`是`MyClassB`类型，那么这个施法是可以的，并且会产生一个可用的对象。\n\n你用哪个演员取决于你的场景。目标是在编写 C++ 时使用各种 C++ 风格转换，确定类型时选择`static_cast`，不确定类型时选择`dynamic_cast`。还有其他可用的 C++ 强制转换，例如`const_cast`和`reinterpret_cast`，但是它们不在本章的讨论范围内。\n\n注意\n\n如果你想进一步阅读这些其他的 C++ 强制转换，前往[https://packt.live/37tJksD](https://packt.live/37tJksD)。\n\n为了将我们新发现的铸造技巧付诸实践，让我们在前面的练习基础上再接再厉。\n\n## 练习 70:施法\n\n对于本章的最后一个练习，我们将再次扩展我们的形状应用。在前面的练习中，我们移动到多形态存储我们的各种形状类型。我们没有将它们存储为各自的类型，而是将它们存储为指向基类的指针，并通过多态性访问它们的`CalculateArea`函数。然而，我们必须做的一件事是给它们的半径和高度变量默认值，因为我们没有办法设置它们。让我们用选角来弥补。我们将为此使用`dynamic_cast`。\n\n注意\n\n这个练习的完整代码可以在这里找到:[https://packt.live/37svU07](https://packt.live/37svU07)。\n\n1.  将*练习 69* 、*多态性*的代码复制到编译器窗口。\n2.  我们首先将类`Shape`、`Square`和`Circle`中的`area`、`height`和`radius`变量的值分别改回`0` :\n\n    ```cpp\n    //[...] \n    public:\n        int area = 0;\n    //[...]\n    public:\n        int height = 0;\n    //[...]\n    public: \n        int radius = 0; \n    //[...]\n    ```\n\n3.  现在我们需要将我们的`Shape*`类型转换成它们的派生类型。既然我们恰当地命名了正方形和圆形，我们就可以确定它们的类型。正因为如此，我们才会使用`static_cast`。在`main()`中，我们首先将我们的平方变量转换为类型`Square*`，在它被定义之后:\n\n    ```cpp\n        Square* square2 = static_cast<Square*>(square);\n    ```\n\n4.  现在我们的对象是 Square 类型，我们可以访问高度变量并将其设置为 10，就像以前一样:\n\n    ```cpp\n        square2->height = 10;\n    ```\n\n5.  现在，对我们的`Circle`课也这样做:\n\n    ```cpp\n        Circle* circle2 = static_cast<Circle*>(circle);\n        circle2->radius = 10;\n    ```\n\n6.  运行程序:\n\n![Figure 10.18: Casting to our derived type allows us to call members specific to them ](img/C14195_10_18.jpg)\n\n图 10.18:转换到我们的派生类型允许我们调用特定于它们的成员\n\n在本练习中，我们已经看到了如何从基类型转换为派生类型来访问派生成员。反过来也是如此——从派生类到基类——尽管我们只能访问基类中声明的成员。当我们开始处理多态性和继承时，知道如何在类型之间进行转换是关键。\n\n## 活动 10:百科全书应用\n\n为了完成我们关于高级面向对象原理的工作，我们将做一个活动，在这个活动中，我们创建一个小的百科全书应用，它将显示一些动物的各种信息。将创建一个基类来定义一个底层结构，我们将用单个动物记录来扩展这个基类，利用多态性来获取它们的数据。输出如下所示:\n\n![Figure 10.19: Users can view information on various animals ](img/C14195_10_19.jpg)\n\n图 10.19:用户可以查看各种动物的信息\n\n注意\n\n这个活动的完整代码可以在这里找到:[https://packt.live/2ODU5Ad](https://packt.live/2ODU5Ad)。\n\n以下是帮助您执行活动的步骤:\n\n1.  首先包含应用所需的所有文件。\n2.  创建一个结构`AnimalInfo`，可以存储**名称**、**来源**、**预期寿命**和**重量。**\n3.  创建一个函数，以简洁的格式打印数据。命名为`PrintAnimalInfo`。\n4.  现在，为我们的动物创建基类。命名为`Animal`。它应该提供一个类型为`AnimalInfo`的成员变量，以及一个返回它的函数。请务必使用适当的访问修饰符。\n5.  接下来，创建第一个派生类`Lion`。该类将从`Animal`继承，为最终类，并在其构造函数中填写`AnimalInfo`成员。\n6.  接下来，创建第二个派生类`Tiger`。填写相同的数据。\n7.  创建最终的派生类`Bear`，同时填充`AnimalInfo`成员。\n8.  定义`main`功能。声明一个指向基本`Animal`类型的指针向量，并添加每个动物衍生类型。\n9.  输出应用标题。\n10.  为应用创建主外部循环，并向用户输出一条消息，提示他们选择索引。\n11.  向用户输出可能的选择。为此使用`for`循环，每个选项都应该包括一个索引和动物的名称。此外，还包括一个选项，用户可以通过输入`-1`退出应用。\n12.  获取用户输入并将其转换为整数。\n13.  检查用户是否进入`-1`并因此想要退出应用。如果他们这么做了，处理好这件事。\n14.  接下来，检查用户输入的索引是否无效。无效索引是小于`-1`且大于动物矢量`-1`大小的索引(因为索引从 0 开始，而不是从 1 开始)。如果有，输出一条错误消息，让他们重新选择。\n15.  如果用户输入一个有效的索引，调用前面创建的`PrintAnimalInfo`，传入你将从向量中得到的动物信息。\n16.  Outside of the `main` loop, clean up the pointers. This means deleting their memory, setting them to `0`, and then clearing the vector.\n\n    注意\n\n    这个活动的解决方案可以在第 559 页找到。\n\n# 总结\n\n在这一章中，我们已经讨论了一些关于 OOP 的进一步主题，从继承开始。我们看到了如何使用它来定义基类中的行为，然后从基类继承来创建一个派生类。我们的派生类专门处理这些更通用的基类，继承任何公共和受保护的成员，同时也定义它们自己的成员。我们可以继续创建继承链，或者一次从多个类继承来创建复杂的对象。\n\n然后我们看了虚拟成员函数。当我们在基类中声明函数时，我们可以将它们标记为虚拟的，这意味着它们的实现可以被覆盖。如果愿意，派生类可以为虚函数提供自己的实现。然而，如果一个函数被标记为纯虚函数——因此基类是抽象的——那么派生类必须提供一个定义或者也变成抽象的。\n\n这导致了多态和类型转换。对于共享类似接口的对象——在共享基类中声明的成员——我们可以将它们存储为指向其基类型的指针。当我们这样做时，我们只能访问基类中声明的成员，但是当我们调用它们时，我们将获得派生类的实现。如果我们想要访问特定的成员，我们需要转换回我们的派生类型，我们介绍了不同的方法:`static_cast`、`dynamic_cast`和 C 风格转换。\n\n我们通过为动物园创建百科全书应用来完成这一章。通过利用本章中涉及的面向对象主题，我们为动物定义了一个基类，并创建了许多派生类。然后，我们允许用户通过索引选择动物，并打印各种信息。\n\n在本书的最后一部分，我们将会看到更高级的概念，包括模板、容器和迭代器，以及异常处理。模板允许我们创建高度可重用的代码，并打开一个充满可能性的世界。我们看了几个基本的容器、数组和向量，所以我们将通过看更多的标准库容器和迭代器来扩展它们。最后，异常处理是我们将在*第 13 章*、*c++ 中的异常处理*中介绍的内容。处理异常是创建稳定软件的关键，当它发现自己处于糟糕状态时不会崩溃。"
  },
  {
    "path": "docs/cpp-workshop/11.md",
    "content": "# 十一、模板\n\n概观\n\n本章概述了模板，并给出了一些如何使用模板以及在哪里使用模板的示例。到本章结束时，您将有足够的信心在可能适用的地方实现模板类型和函数，并拥有一些基础知识。\n\n# 简介\n\n在前几章中，介绍了 **OOP** ，以及示例和用例。详细介绍了类和创建类的最佳实践。在这一章中，我们将看到 OOP 的另一个强大特性——模板。\n\n**模板**允许对不同的数据类型重用代码。使用模板的一个例子是 C++ 标准模板库(或 STL)。这个库是一组提供通用容器和算法的模板类。该库可以用于任何数据类型，这是使用模板函数和类实现的。在本章中，我们将介绍模板类和模板函数的创建，以允许创建可重用的代码。\n\n具体来说，我们将描述以下主题:模板类、模板函数和模板专门化。\n\n模板的核心是一种通用编程的形式。它允许我们重用一组功能，而这些功能不需要特定于一种类型。例如，一个类可以保存数据并为类型为`int`的变量提供一些功能。如果我们需要对`float`类型的变量执行相同的功能，那么我们需要复制该代码，用`float`替换`int`。但是，使用模板，我们可以重用这些代码，并允许编译器为每种类型生成我们需要的代码。我们将从展示如何声明模板开始，然后通过例子和练习来更详细地介绍。\n\n# 语法\n\n创建模板需要使用一个新的 C++ 关键字:`template`。这个关键字让编译器知道这个类或函数打算用作模板，模板*定义*中模板参数的实例应该用模板*实例化*提供的实际数据类型替换:\n\n```cpp\ntemplate <typename T>\ntemplate <class T>\n```\n\n在前面的例子中，`T`是模板参数。在模板类或函数中使用类型`T`的任何地方，它都将被实际类型替换。通过一些例子，这将变得更加清晰。`T`是模板参数的一个非常常见的名称，但是这个名称可以是您想要的任何名称。\n\n## 模板类\n\n这里提供了一个非常简单的模板类示例:\n\n```cpp\ntemplate<typename T>\nclass Position\n{\npublic:\n    Position(T x, T y) \n    {\n        m_x = x;\n        m_y = y;\n    }\n    T const getX() { return m_x; }\n    T const getY() { return m_y; }\nprivate:\n    T m_x;\n    T m_y;\n};\n```\n\n请注意模板语法的使用。在这种情况下，我们声明`T`出现的任何地方都可以用我们在创建这个类的实例时选择的类型来替换。这个类是 2D 位置值的简单持有者。根据所需的精度，这些位置值可以存储为`int`、`float`甚至`long`类型。通过使用模板，通过在创建类实例时传递预期的类型，该类可以用于所有这些类型。让我们用一个小练习来测试一下。\n\n## 练习 71:为位置对象创建不同的类型\n\n使用前面的模板类，编写一个`main`函数，创建几个不同的`Position`对象。每个都应该使用不同的模板参数(`T`的替换)，然后打印出成员变量的类型，该类型现在是模板参数的类型。要获取变量的类型，我们可以使用包含在`<typeinfo>`头中的`typeid`运算符。\n\n注意\n\n练习的完整代码可以在[https://packt.live/2rhi6Vm](https://packt.live/2rhi6Vm)找到。\n\n以下是完成练习的步骤:\n\n1.  声明一个将`T`替换为`int`的`Position`对象:\n\n    ```cpp\n    int main()\n    {\n        Position<int> intPosition(1, 3);\n    ```\n\n2.  声明一个将`T`替换为`float`的`Position`对象:\n\n    ```cpp\n        Position<float> floatPosition(1.5f, 3.14f);\n    ```\n\n3.  声明一个将`T`替换为`long`的`Position`对象:\n\n    ```cpp\n        Position<long> longPosition(1, 3);\n    ```\n\n4.  包括我们文件顶部的`<typeinfo>`标题:\n\n    ```cpp\n    #include <typeinfo>\n    ```\n\n5.  在`Position`的每个实例中输出我们的`m_x`变量的类型:\n\n    ```cpp\n        cout << \"type: \" << typeid(intPosition.getX()).name() <<  \" X: \"          << intPosition.getX() << \" Y: \" << intPosition.getY()          << endl;\n        cout << \"type: \" << typeid(floatPosition.getX()).name() <<  \" X: \"          << floatPosition.getX() << \" Y: \" << floatPosition.getY()          << endl;\n        cout << \"type: \" << typeid(longPosition.getX()).name() <<  \" X: \"          << longPosition.getX() << \" Y: \" << longPosition.getY()          << endl;\n    ```\n\n6.  The complete program looks like this:\n\n    ```cpp\n    #include <iostream>\n    #include <string>\n    #include <typeinfo>\n    using namespace std;\n    template<typename T>\n    class Position\n    {\n    public:\n        Position(T x, T y)\n        {\n            m_x = x;\n            m_y = y;\n        }\n        T const getX() { return m_x; }\n        T const getY() { return m_y; }\n    private:\n        T m_x;\n        T m_y;\n    };\n    int main()\n    {\n        Position<int> intPosition(1, 3);\n        Position<float> floatPosition(1.5f, 3.14f);\n        Position<long> longPosition(1, 3);\n        cout << \"type: \" << typeid(intPosition.getX()).name() << \" X: \"          << intPosition.getX() << \" Y: \" << intPosition.getY()          << endl;\n        cout << \"type: \" << typeid(floatPosition.getX()).name() << \" X: \"          << floatPosition.getX() << \" Y: \" << floatPosition.getY()          << endl;\n        cout << \"type: \" << typeid(longPosition.getX()).name() << \" X: \"          << longPosition.getX() << \" Y: \" << longPosition.getY()          << endl;\n        return 0;\n    }\n    ```\n\n    运行上述代码后，您将获得以下输出:\n\n![Figure 11.1: The three Position types ](img/C14195_11_01.jpg)\n\n图 11.1:三种位置类型\n\n在我们的练习中，创建了三个`Position`类型，每个类型都有不同的模板参数类型:`int`、`float`和`long`。`#include <typeinfo>`行允许通过`name()`函数访问传入类型的名称(注意这些名称不能保证在编译器之间是相同的)。通过传递`Position`类的`x`值来打印该函数的值表明`T`的类型确实已经被传递给模板类的类型所替换。输出显示`i`、`f`和`l`在本编译器中分别是`int`、`float`和`long`的名称。\n\n## 多个模板参数\n\n在前一节中，我们看到在示例中使用了单个模板参数。但是，也可以使用多个模板参数。在以下示例中，有一个附加的模板参数`U`，它被用作`Position`类中`z`旋转的数据类型:\n\n```cpp\n#include <iostream>\n#include <typeinfo>\nusing namespace std;\ntemplate<typename T, typename U>\nclass Position\n{\npublic:\n    Position(T x, T y, U zRot)\n    {\n        m_x = x;\n        m_y = y;\n        m_zRotation = zRot;\n    }\n    T const getX() { return m_x; }\n    T const getY() { return m_y; }\n    U const getZRotation() { return m_zRotation; } \nprivate:\n    T m_x;\n    T m_y;\n    U m_zRotation;\n};\n```\n\n就像`T`一样，当我们创建类的实例时，任何使用`U`的地方都会被另一种类型替换。前面的类和以前几乎一样，但是现在有一个`getZRotation`函数不返回`T`将要引用的类型。相反，它返回`U`将引用的类型。我们可以使用`main`函数进行测试，一旦创建了一个实例，该函数将再次打印我们类中的值类型:\n\n```cpp\nint main()\n{\n    Position<int, float> intPosition(1, 3, 80.0f);\n    Position<float, int> floatPosition(1.0f, 3.0f, 80);\n    Position<long, float> longPosition(1.0, 3.0, 80.0f);\n    cout << \"type: \" << typeid(intPosition.getX()).name() <<  \" X: \"          << intPosition.getX() << \" Y: \" << intPosition.getY() << endl;\n    cout << \"type: \" << typeid(floatPosition.getX()).name() <<  \" X: \"          << floatPosition.getX() << \" Y: \" << floatPosition.getY() << endl;\n    cout << \"type: \" << typeid(longPosition.getX()).name() <<  \" X: \"          << longPosition.getX() << \" Y: \" << longPosition.getY() << endl;\n    cout << \"type: \" << typeid(intPosition.getZRotation()).name()          <<  \" Z Rot: \" << intPosition.getZRotation() << endl;\n    cout << \"type: \" << typeid(floatPosition.getZRotation()).name()          <<  \" Z Rot: \" << floatPosition.getZRotation() << endl;\n    cout << \"type: \" << typeid(longPosition.getZRotation()).name()          << \" Z Rot: \" << longPosition.getZRotation() << endl;\n    return 0;\n}\n```\n\n上述代码产生以下输出:\n\n![Figure 11.2: Output for type U ](img/C14195_11_02.jpg)\n\n图 11.2:U 型的输出\n\n类模板非常强大，可以帮助您在整个 C++ 应用中重用代码。只要我们知道一个类在一个类型上执行的某些功能对不止一个类型有用，那么我们就有了一个模板类的候选。然而，模板类并不是我们实现这种重用的唯一方式；另一种方式是通过使用**模板功能**。\n\n## 模板函数\n\n拥有一个可以利用许多不同数据类型的类是非常有用的，但有时，它是一小段需要以“模板化”方式重用的代码。这就是**模板函数**进来的地方。模板函数允许特定的函数是通用的，而不是整个类。下面是一个模板函数的示例，它返回两个相同类型的数字之间的最大值:\n\n```cpp\ntemplate<typename T>\nT getLargest(T t1, T t2)\n{\n    if(t1 > t2)\n    {\n        return t1;\n    }\n    else\n    {\n        return t2;\n    }\n}\n```\n\n这里使用的语法与声明模板类时相同，但它被放在函数签名之上，而不是类声明之上。此外，就像模板类一样，在任何出现`T`的地方，它都将被模板参数的类型替换。\n\n在下面的练习中，我们将使用`getLargest`类来比较之前创建的`Position`类的`x`和`y`变量。\n\n## 练习 72:使用模板函数比较位置值\n\n上例中的`getLargest`函数可以用来比较`Position`类的`x`和`y`变量。与我们在创建类的实例时所做的不同，我们在使用它时不必将类型传递给函数。编译程序时，编译器会创建一个适用于每种类型的函数版本。让我们写一个有助于理解这一点的例子。\n\n注意\n\n练习的完整代码可以在[https://packt.live/2O7pICT](https://packt.live/2O7pICT)找到。\n\n以下是完成练习的步骤:\n\n1.  首先，我们从`Position`类开始:\n\n    ```cpp\n    #include <iostream>\n    #include <typeinfo>\n    using namespace std;\n    template<typename T, typename U>\n    class Position\n    {\n    public:\n        Position(T x, T y, U zRot)\n        {\n            m_x = x;\n            m_y = y;\n            m_zRotation = zRot;\n        }\n            T const getX() { return m_x; }\n            T const getY() { return m_y; }\n            U const getZRotation() { return m_zRotation; }\n        private:\n            T m_x;\n            T m_y;\n            U m_zRotation;\n    };\n    ```\n\n2.  这个类之后，我们可以添加`getLargest`功能:\n\n    ```cpp\n    template<typename T>\n    T getLargest(T t1, T t2)\n    {\n        if(t1 > t2)\n        {\n            return t1;\n        }\n        else\n        {\n            return t2;\n        }\n    }\n    ```\n\n3.  And finally, we can create some `Position` objects and pass the `x` value to `getLargest` while comparing the `m_zRotation` values of the `int` and `long` `Position` instances:\n\n    ```cpp\n    int main()\n    {\n        Position<int, float> intPosition(1, 3, 80.5f);\n        Position<float, int> floatPosition(2.5f, 3.14f, 80);\n        Position<long, float> longPosition(5, 3, 200);\n        cout << \"largest is: \" << getLargest(intPosition.getX(),          intPosition.getY()) << endl;\n        cout << \"largest is: \" << getLargest(floatPosition.getX(),          floatPosition.getY()) << endl;\n        cout << \"largest is: \" << getLargest(longPosition.getX(),          longPosition.getY()) << endl;\n        cout << \"largest ZRot is:\" << getLargest(intPosition.         getZRotation(), longPosition.getZRotation()) << endl; \n        return 0;\n    }\n    ```\n\n    当您运行完整的代码时，您应该获得以下输出:\n\n![Figure 11.3: Output containing a comparison of the x and y variables  ](img/C14195_11_03.jpg)\n\n图 11.3:包含 x 和 y 变量比较的输出\n\n我们可以看到，我们不需要指定传递给模板函数的类型；编译器为我们做了这项工作。此外，请注意，该函数的类型不是基于我们使用的实例的`T`值，而是基于我们传入的成员变量的实际类型。通过将`z`旋转值与`x`位置值进行比较，这一点就很清楚了，后者是不同类型的——编译器仍然为我们创建了正确的函数。\n\n## 模板专门化\n\n虽然模板在很大程度上是为了使类更通用而设计的，但是在某些情况下，一个特定的数据类型需要有自己的实现。模板专门化允许我们创建以我们已经理解的通用方式工作的模板，但是对于特定的数据类型可以有不同的行为。这在许多情况下都很有用，例如对于大多数数据类型来说很快的算法，但在特定情况下可能会很慢或效率低下。为了说明这一点，这里有一个简单的`compare`函数，在 C 风格字符串的情况下使用`strcmp`，在其他类型的情况下使用*等式运算符*进行比较。\n\n**模板功能:**\n\n```cpp\ntemplate<typename T>\nbool compare(T t1, T t2)\n{\n    return t1 == t2;\n}\n```\n\n这是一个例子，我们需要一个特殊的环境，因此需要一个特殊化，因为`const` `char*`是一个指针，等式运算符只比较指针地址，而不比较字符串的内容:\n\n**专用模板功能:**\n\n```cpp\ntemplate <>\nbool compare<const char*>(const char* c1, const char* c2)\n{\n    return strcmp(c1, c2) == 0;\n}\n```\n\n请注意，在使用专门的模板函数时，使用具体的数据类型来代替通用的`T`，但它不会作为模板参数传递到模板<>；相反，它在函数名之后传入。以下示例测试这些功能:\n\n```cpp\n#include <iostream>\n#include <string.h>\n\nusing namespace std;\ntemplate<typename T>\nbool compare(T t1, T t2)\n{\n    return t1 == t2;\n}\ntemplate <>\nbool compare<const char*>(const char* c1, const char* c2)\n{\n    return strcmp(c1, c2) == 0;\n}\nconst char* TRUE_STR = \"TRUE\";\nconst char* FALSE_STR = \"FALSE\";\nint main()\n{\n    cout << (compare(1, 1) ? TRUE_STR : FALSE_STR) << endl;\n    cout << (compare(\"hello\",\"hello\") ? TRUE_STR : FALSE_STR) << endl;\n    cout << (compare(1, 2) ? TRUE_STR : FALSE_STR) << endl;\n    cout << (compare(\"hello\",\"goodbye\") ? TRUE_STR : FALSE_STR) << endl;\n    return 0;\n}\n```\n\n前面的代码将产生以下输出:\n\n![Figure 11.4: Output when comparing strings using strcmp and other types using the equality operator ](img/C14195_11_04.jpg)\n\n图 11.4:使用 strcmp 比较字符串时的输出和使用等式运算符比较其他类型时的输出\n\n使用类代替函数也可以达到同样的效果，如以下示例所示:\n\n```cpp\n#include <iostream>\nusing namespace std;\ntemplate <class T>\nclass MyClass\n{\npublic:\n    MyClass() { cout << \"My class generic\" << endl; }\n};\ntemplate <>\nclass MyClass <int>\n{\npublic:\n    MyClass() { cout << \"My class int specialization\" << endl; }\n};\n\nint main()\n{\n    MyClass<float> floatClass;\n    MyClass<int> intClass;\n    return 0;\n}\n```\n\n上述代码产生以下输出:\n\n![Figure 11.5: Output when using classes instead of functions ](img/C14195_11_05.jpg)\n\n图 11.5:使用类代替函数时的输出\n\n## 其他模板注意事项\n\n创建和使用模板类时，需要考虑几点。这里简单介绍一下。\n\n### 强制接受类型\n\n当创建我们的模板类时，值得记住的是，已经假设传入的参数属于可以在我们想要的上下文中使用的类型。情况并非总是如此，需要采取措施来确保事情按照预期的方式运行——例如，如果我们有一个模板函数，它将值相加，然后我们向它传递一个字符串或自定义类型。不幸的是，我们无法在模板声明中设置我们想要接受的类型。有很多方法可以实现这一点，但是很多选项都是针对特定用途的，超出了本书的范围。如果您知道可能会使用肯定会导致重大问题的类型，那么应该记住如何处理无效类型。\n\n### 模板和默认构造函数\n\n前面例子中的另一个假设是，任何模板参数类型都有一个默认构造函数。这是因为模板类和其他类一样，仍然有责任调用其成员变量的默认构造函数，如果给定的类型没有默认构造函数，它将无法编译。以`Position`类为例(因为它没有默认构造函数)，我们可以看到如果我们将该类型作为模板参数传递给另一个模板类会发生什么:\n\n```cpp\n#include <iostream>\nusing namespace std;\ntemplate<typename T>\nclass Position\n{\npublic:\n    Position(T x, T y)\n    {\n        m_x = x;\n        m_y = y;\n    }\n    T const getX() { return m_x; }\n    T const getY() { return m_y; }\nprivate:\n    T m_x;\n    T m_y;\n};\n```\n\n该类已被再次简化为其原始形式。下面是一个模板类的示例，该模板类的模板参数类型可以是位置:\n\n```cpp\ntemplate<class T>\nclass PositionHolder\n{\npublic:\n    PositionHolder()\n    {\n    }\n    T getPosition() { return m_position; }\nprivate:\n    T m_position;\n};\nint main()\n{\n    PositionHolder<Position<float>> positionHolder;\n    return 0;\n}\n```\n\n`PositionHolder`是一个新的模板类，将用于包装`Position<T>`类型。运行此代码将产生类似下面的编译器错误:\n\n```cpp\nerror: no matching function for call to 'Position<float>::Position()\n```\n\n由此我们可以推断，已经尝试调用`Position`的默认构造函数，由于`Position`没有构造函数，这导致了编译器错误。修复此错误的一个选项是使`PositionHolder`构造函数成为模板化函数，该函数可以将正确类型的值传递给初始化列表中`Position<T>`的构造函数:\n\n```cpp\n    template<typename U>\n    PositionHolder(U x, U y) : m_position(x,y)\n    {\n    }\n```\n\n创建`PositionHolder`现在需要传入`T`参数在其构造函数中需要的变量值。本质上，我们现在让`PositionHolder`负责将适当的值传递给`T`的构造者:\n\n```cpp\nint main()\n{\n    PositionHolder<Position<float>> positionHolder(20.0f, 30.0f);\n    return 0;\n}\n```\n\n这是可行的，但是会很快变得笨拙，对`Position`构造函数的任何更新都意味着对`PositionHolder`构造函数的更新，并且`PositionHolder`可以包含的任何类型都需要这个双参数构造函数。更好的选择是在`Position`中定义一个复制构造函数并调用它。这里是`Position`类的复制构造函数:\n\n```cpp\n    Position(const T& t)\n    {\n        m_x = t.m_x;\n        m_y = t.m_y;\n    }\n```\n\n现在，`PositionHolder`可以在自己的构造函数中使用这个复制构造函数，如下所示:\n\n```cpp\n    PositionHolder(const T& t) : m_position(t)\n    {\n    }\n```\n\n现在，当我们想要添加一个`Position`对象到`PositionHolder`时，我们可以构造一个新的位置来复制，或者根据情况添加一个现有的`Position`对象:\n\n```cpp\nint main()\n{\n    PositionHolder<Position<float>> positionHolder(Position<float>(20.0f, 30.0f));\n    return 0;\n}\n```\n\n现在可以从另一个位置创建存储的位置，由模板参数类型来定义它自己的复制构造函数。请注意，在前面的情况下，不需要定义复制构造函数，因为浅复制就足够了，但是情况并不总是这样，应该记住这一点。\n\n# 创建通用队列\n\n有了这些新的模板知识，我们现在可以尝试创建一些实用的东西。在接下来的章节中，我们将介绍 **STL** 中的容器，但在此之前，了解一些容器如何在更简单的层次上工作是很有用的。然后，如果出现其中一个不太适合我们需求的情况，我们可以写一些更适合我们的东西，仍然给我们 STL 的易用界面。\n\n## 什么是队列？\n\n我们可以将队列定义为一个容器，其数据结构为先入**、**先出** ( **先进先出**)。元素从后面插入，从前面删除。队列对许多事情都很有用，例如安排可以执行然后删除的任务。想象一下排队，就像你在商店排队一样。如果你排在队伍的第一位，那么你将被首先招待。**\n\n对于我们的例子，我们将把我们的队列建立在 STL 队列的基础上，并尝试实现它已经提供的所有东西。也就是说，STL 队列提供了以下功能:\n\n*   `empty()`:返回 bool，表示队列是否为空。\n*   `size()`:返回队列的当前大小或元素数量。\n*   `swap()`:交换两个队列的内容(这里我们不实现这个，但是您可以自己尝试作为扩展任务)。\n*   `emplace()`:在队列的末尾添加一个元素(同样，我们不会实现这个，因为它不在本章的范围内)。\n*   `front()`和`back()`:分别返回队列中第一个和最后一个元素的指针。\n*   `push(element)`和`pop()`:分别将一个元素推到队列的末尾，删除第一个元素。\n\n以下是初始类定义，示例的其余部分将从该定义开始构建:\n\n```cpp\ntemplate<class T>\nclass Queue\n{\npublic:\nprivate:\n};\n```\n\n查看我们需要实现的函数，我们可以看到我们需要存储队列中的第一个和最后一个元素，以便它们可以从`front()`和`back()`返回。随着本章的深入，我们将讨论使用动态内存来存储队列元素。当使用这个动态内存时，我们将分配内存来保存我们的数据，然后返回一个指向这个内存块中第一个元素的指针。因此，`front()`将仅仅是指向我们队列数据的指针，`back()`将指向我们数据的最后一个构造元素减 1(换句话说，最后一个构造元素)之后的一个。\n\n下面是元素在队列中的布局图:\n\n![Figure 11.6: Elements being laid out in the queue ](img/C14195_11_06.jpg)\n\n图 11.6:元素在队列中的布局\n\n考虑到这一点，下面是反映这一点的更新类:\n\n```cpp\ntemplate<class T>\nclass Queue\n{\npublic:\n    T* front() { return queueData; }\n    const T* front() const { return queueData; }\n    T* back() { return queueDataEnd - 1; }\n    const T* back() const { return queueDataEnd - 1; }\nprivate:\n    T* queueData;\n    T* queueDataEnd;\n};\n```\n\n请注意，`front()`和`back()`成员函数有`const`和非`const`版本。这允许我们同时使用`const`和非`const`队列。我们现在需要定义`size()`函数，但是我们实际上不需要存储这个函数，因为我们可以从指向第一个元素的指针和经过最后一个元素的指针(`queueData`和`queueDataEnd`)来计算它。**从另一个指针中减去一个指针，得到这两个指针位置之间的元素数量；该值属于** `ptrdiff_t` **类型**。\n\n`size`函数需要返回一个我们知道可以在队列中存储任意数量元素的类型的值。在下面这个片段中，`size_t`可以存储任何类型(包括数组)的理论上可能的对象的最大大小，我们的`Queue`类带有一个实现的`size()`函数，现在变成如下:\n\n```cpp\ntemplate<class T> \nclass Queue \n{ \npublic: \n    T* front() { return queueData; }\n    const T* front() const { return queueData; }\n    T* back() { return queueDataEnd - 1; }\n    const T* back() const { return queueDataEnd - 1; }\n        size_t size() const { return queueDataEnd - queueData; }\nprivate:\n    T* queueData;\n    T* queueDataEnd;\n};\n```\n\n我们现在也可以通过检查`size()`是否返回`0`来简单地创建`empty()`函数:\n\n```cpp\n    bool empty() const { return size() == 0; }\n```\n\n## 在队列中实现构造函数和析构函数\n\n对于这个队列，我们将编写两个构造函数:一个默认构造函数创建一个空队列(第一个和最后一个元素都是 0)，一个构造函数取一个大小值并分配足够的内存来存储`T`的那么多元素。初始化过程将使用一个我们称之为`init()`的函数，该函数负责为我们的元素分配内存。下面是包含这些构造函数的更新类(稍后将介绍`init`函数):\n\n```cpp\ntemplate<class T>   \nclass Queue   \n{ \npublic:\n    Queue() { init(); } \n    explicit Queue(size_t numElements, const T& initialValue = T()) \n    {\n        init(numElements, initialValue); \n    } \n    T* front() { return queueData; }  \n    const T* front() const { return queueData; }  \n    T* back() { return queueDataEnd - 1; }  \n    const T* back() const { return queueDataEnd - 1; }  \n    size_t size() const { return queueDataEnd - queueData; }  \n    bool empty() const { return size() == 0; } \nprivate:  \n    void init() {} \n    void init(size_t numElements, const T& initialValue) {} \n    T* queueData;  \n    T* queueDataEnd; \n};\n```\n\n请注意，采用大小的`Queue`构造函数的默认参数为`initialValue`，该参数使用的是`T`的默认构造函数。当没有初始值被传递给构造函数时使用。`explicit`关键字还用于确保如果作为参数传递，编译器不能隐式构造此类型以从一种类型转换为另一种类型:\n\n```cpp\n    explicit Queue(size_t numElements, const T& initialValue = T()) \n    {\n        init(numElements, initialValue); \n    }\n```\n\n还要注意有两个`init()`函数:一个重载取两个参数，将被同样取两个参数的构造函数使用，如图:\n\n```cpp\nvoid init() {} \nvoid init(size_t numElements, const T& initialValue) {}\n```\n\n我们现在有了构造函数，但是，当然，我们需要一个析构函数和它的等价函数`init()``destroy()`。这是带有析构函数的更新类和我们的`destroy()`函数的框架:\n\n```cpp\ntemplate<class T>   \nclass Queue   \n{ \npublic:   \n    Queue() { init(); } \n    explicit Queue(size_t numElements, const T& initialValue = T()) \n    {\n         init(numElements, initialValue); \n    } \n   ~Queue() { destroy(); }\n    T* front() { return queueData; }  \n    const T* front() const { return queueData; }  \n    T* back() { return queueDataEnd - 1; }  \n    const T* back() const { return queueDataEnd - 1; }  \n    size_t size() const { return queueDataEnd - queueData; }  \n    bool empty() const { return size() == 0; } \nprivate:  \n    void init() {} \n    void init(size_t numElements, const T& initialValue) {} \n    void destroy() {}\n    T* queueData;  \n    T* queueDataEnd; \n};\n```\n\n`destroy`函数将负责释放内存并销毁我们队列中的任何元素。`init`功能将使用包含在`<memory>`标题中的两个功能:`uninitialized_fill`和`uninitialized_copy`。现在，`uninitialized_fill`将一个值复制到由范围[第一个，最后一个]定义的未初始化的内存区域，而`uninitialized_copy`将一个值范围[第一个，最后一个]复制到未初始化的内存区域。在我们更新类以拥有其中一个`init`函数之前，使用`uninitialized_fill`函数，我们需要覆盖我们将用来分配内存的内容。\n\n## 动态记忆\n\n我们希望每当一个新元素被推到队列上时，队列能够增长，就像 STL 版本一样。我们首先想到的可能是使用新的`T[]`数组初始值来分配内存。然而，这会给我们带来本章前面概述的相同问题，即新的`T[]`数组将调用`T`的默认构造函数，因此`T`将仅限于具有默认构造函数的类型。我们希望情况不是这样，因此，我们必须找到另一个为容器分配内存的选项。\n\n### 分配器\n\n使用`<memory>`标题，我们可以进入`allocator<T>`类型。这种类型允许我们分配一块内存来存储`T`的对象，并且不初始化对象。使用`allocator<T>`类型还允许我们分配比当前需要的更多的内存，这样我们就可以消除初始化内存的开销，并且只有当队列变得太大时才这样做。每当队列需要增长时，创建两倍于我们所需的存储是一个好策略。如果队列永远不会比这个大，那么分配更多内存的开销就会被移除；请注意，新的`T[]`数组不允许我们进行这种优化。\n\n对`allocator<T>`的一个警告是，我们现在需要跟踪初始化和未初始化内存之间的分区，因此我们的类变得稍微复杂一些；但是好处还是很明显的。\n\n我们可以更新我们的类，使其有一个`allocator<T>`变量作为成员，这样我们就可以利用它，还有一个新的指针，它将指向已分配内存末尾的一个指针:\n\n```cpp\n#include <iostream> \n// need the memory header \n#include <memory> \nusing namespace std; \ntemplate<class T>    \nclass Queue    \n{\npublic:    \n    Queue() { init(); }  \n    explicit Queue(size_t numElements, const T& initialValue = T())  \n    {   \n        init(numElements, initialValue);  \n    }  \n   ~Queue() { destroy(); } \n    T* front() { return queueData; }   \n    const T* front() const { return queueData; }   \n    T* back() { return queueDataEnd - 1; }   \n    const T* back() const { return queueDataEnd - 1; }   \n    size_t size() const { return queueDataEnd - queueData; }   \n    bool empty() const { return size() == 0; }  \nprivate:   \n    void init() {}  \n    void init(size_t numElements, const T& initialValue) {}  \n    void destroy() {} \n\n    // the allocator object \n    allocator<T> alloc; \n    T* queueData;   \n    T* queueDataEnd; \n    T* memLimit; // one past the end of allocated memory \n};\n```\n\n在前面的例子中，我们添加了内存头、一个`allocator<T>`成员变量和一个指向已分配内存末尾的指针。使用`allocator<T>`成员变量允许我们实现`init()`函数来分配内存，并使用`uninitialized_fill`向其复制初始值:\n\n```cpp\nvoid init()\n{\n    queueData = queueDataEnd = memLimit = 0;\n}\n\nvoid init(size_t numElements, const T& initialValue)\n{\n    queueData = alloc.allocate(numElements);\n    queueDataEnd = memLimit = queueData + numElements;\n    uninitialized_fill(queueData, queueDataEnd, initialValue);\n}\n```\n\n不带参数的`init`函数只是将我们所有的指针设置为`0`，创建了一个没有分配内存的空队列。第二个`init`函数分配足够的内存来保存我们的`T`对象的`numElements`。从`allocate`函数，它返回一个指向我们数据的第一个元素的指针。为了获取我们需要的超过最后构造的元素的末尾和内存限制的其他指针，我们简单地增加`numElements`到`queueData`指针(第一个元素)，并将其分配给`queueDataEnd`和`memLimit`指针。这两个都指向最后一个构造元素之后的一个，此时，它是分配内存之后的一个。然后，我们使用`uninitialized_fill`将初始元素复制到内存块中，分别使用`queueData`和`queueDataEnd`作为范围中的第一个和最后一个。下面的例子就是我们的`destroy`功能；它使用`destroy`分配器和解除分配函数来清理我们的类:\n\n```cpp\nvoid destroy()\n{\n    if (queueData != 0)\n    {\n        T* it = queueDataEnd;\n        while (it != queueData)\n        { \n            alloc.destroy(--it);\n        }\n        alloc.deallocate(queueData, memLimit - queueData);\n    }\n    queueData = queueDataEnd = memLimit = 0;\n}\n```\n\n这个函数通过我们的`queueData`向后循环，调用任何构造元素的析构函数，然后使用`deallocate`函数释放分配的内存。进入`deallocate`的第二个参数是我们希望释放的内存大小。我们跟踪第一个和第二个经过分配的内存，这样我们就可以得到指针差，并将其用作`deallocate`函数的第二个参数。\n\n## 调整大小和追加\n\n现在我们有了一个分配器，我们可以使用它来创建函数，在需要时调整内存块的大小，并在可用内存中构造对象。我们将这些函数称为`resize()`和`append()`。如前所述，每当调整队列大小时，我们都会将分配的内存量增加一倍。以下是全部功能:\n\n```cpp\nvoid resize()\n{\n    size_t newSize = max(2 * (queueDataEnd - queueData), ptrdiff_t(1));\n    T* newData = alloc.allocate(newSize);\n    T* newDataEnd = uninitialized_copy(queueData, queueDataEnd, newData);\n    destroy();\n    queueData = newData;\n    queueDataEnd = newDataEnd;\n    memLimit = queueData + newSize;\n    }\nvoid append(const T& newValue)\n{\n    alloc.construct(queueDataEnd++, newValue);\n}\n```\n\n`resize()`函数首先计算需要分配多少内存，由于队列可能是空的，它使用`max`函数来确保我们总是为至少一个元素分配足够的空间(2 乘以 0 仍然是 0)。然后使用分配器分配这个`newSize`内存量，`uninitialized_copy`将现有的`queueData`复制到新的内存区域。然后调用`destroy`函数删除现有数据，然后将新指针重新分配给我们的成员指针。我们的成员指针现在正确地指向新分配的内存空间及其开始和限制。`append()`使用分配器的`construct`功能在已分配内存的第一个可用空间中构造一个元素，在构造的元素之后。\n\n## 推和弹出\n\n现在我们来到`Queue`的界面端。这些是任何使用队列的人都会使用的功能，它们允许在我们的队列中添加和删除元素。已经写了很多复杂的东西，所以这些函数并不是很多。然而，有一件事要记住:我们正在创建一个先进先出容器。因此，当调用`pop`时，被推入容器的第一个元素将首先被移除。这意味着我们需要销毁队列中的第一个元素，然后将所有剩余的元素移过，并减少指向最后一个元素的指针(`queueDataEnd`)。这里是`pop()`功能，它以一种非常简单的方式实现了这个功能:\n\n```cpp\nvoid pop()\n{\n    if (queueData != 0)\n    {\n        alloc.destroy(queueData);\n        for (int i = 0; i < size(); i++)\n        {\n            queueData[i] = queueData[i + 1];\n        }\n        queueDataEnd -= 1;\n    }\n}\n```\n\n随着这个循环的进行，它将`i`处的元素分配给`i + 1`处的元素，因此元素`1`将被转换为元素`0`，而`2`将被转换为`1`，以此类推。然后，它递减指向`queueDataEnd`的指针，因为队列现在小了一个元素。\n\n推送元素以如下方式使用我们现有的`resize`和`append`功能:\n\n```cpp\nvoid push(const T& element)\n{\n    if (queueDataEnd == memLimit)\n        resize();\n        append(element);\n}\n```\n\n如果在我们分配的内存中有足够的空间，那么队列将不会被调整大小。无论哪种方式，一个元素都会被添加到队列中。在调用`resize`之后调用`append`(如果需要的话)可以确保我们有足够的空间来做追加而不需要先检查。\n\n## 定型和测试\n\n最后，我们的`Queue`实现了我们为其设定的所有功能。你会记得在关于构造函数的一章中，讨论了第三条的**规则，如果一个类需要实现一个析构函数，那么它几乎总是需要实现一个复制构造函数并重载赋值运算符。我们不会详细讨论复制构造函数和赋值操作符，因为它们已经被介绍过了，但是我们将讨论一个新的`init()`函数的创建，该函数可以在实现它们时使用:**\n\n```cpp\nvoid init(T* front, T* back)\n{\n    queueData = alloc.allocate(back - front);\n    memLimit = queueDataEnd = uninitialized_copy(front, back, queueData);\n}\n```\n\n给定指向内存块开始和结束的指针，这个重载的`init()`函数分配空间，然后将元素复制到它上面。您可以看到这在复制构造函数和重载赋值运算符中是多么有用；它大大简化了这些功能，因为我们在复制时不必重写任何`init`代码:\n\n```cpp\n    Queue(const Queue& q) { init(q.front(), q.back()); }\n    Queue& operator=(const Queue& rhs)\n    {\n        if (&rhs != this)\n        {\n             destroy();\n             init(rhs.front(), rhs.back());\n        }\n        return *this;\n    }\n```\n\n现在一切就绪，我们终于可以测试队列的功能了。以下是对`int`值队列的一个非常简单的测试:\n\n```cpp\nExample 11_01.cpp\n113 int main()\n114 {\n115     Queue<int> testQueue;\n116     testQueue.push(1);\n117     testQueue.push(2);\n118     cout << \"queue contains values: \";\n119 \n120     for (auto it = testQueue.front(); it != testQueue.back() + 1; ++ it)\n121     {\n122         cout << *it << \" \";\n123     }\n124 \n125     cout << endl;\n126     cout << \"queue contains \" << testQueue.size() << \" elements\" << endl;\n127     testQueue.pop();\n128     cout << \"queue contains values: \";\n129 \n130     for (auto it = testQueue.front(); it != testQueue.back() + 1; ++ it)\n131     {\n132         cout << *it << \" \";\n133     }\n134 \n135     cout << endl;\n136     cout << \"queue contains \" << testQueue.size() << \" elements\" << endl;\n137     \n138     testQueue.push(9);\n139     testQueue.push(50);\n140     \n141     cout << \"queue contains values: \";\n        //[…]\n163     return 0;\n164 }\nThe complete code for this example can be found at: https://packt.live/2O8A9WR\n```\n\n当您使用完整且更新的`Queue`类运行前面的代码时，您将获得以下输出:\n\n![Figure 11.7: Output when testing the queue  ](img/C14195_11_07.jpg)\n\n图 11.7:测试队列时的输出\n\n## 活动 11:创建通用堆栈\n\n队列具有**先进先出**数据结构，而堆栈具有**后进**、**先出** ( **后进先出**)数据结构。想象一堆数据结构就像一堆杂志。在此堆栈中，您从顶部取出料盒(我们不会从堆栈底部抓取料盒)，顶部也是添加到堆栈中的最后一个料盒。在本练习中，您将创建一个堆栈数据结构。本章的*创建通用队列*部分概述了实现这一点的所有要素，最重要的区别包含在`pop()`函数中。`front()`和`back()`功能也将分别被重命名为`top()`和`bottom()`，并将指向堆栈中的正确位置。\n\n注意\n\n活动的完整代码可以在[https://packt.live/2r9XgYi](https://packt.live/2r9XgYi)找到。\n\n以下是一些有助于完成活动的步骤:\n\n1.  使用通用队列示例作为基础编写通用堆栈。\n2.  改变`pop()`函数来处理后进先出数据结构。\n3.  在`main`函数中测试堆栈，输出数据测试堆栈是否正常工作。\n\n成功完成活动后，您应该获得类似以下内容的输出:\n\n![Figure 11.8: Final output for the activity ](img/C14195_11_08.jpg)\n\n图 11.8:活动的最终输出\n\n注意\n\n这个活动的解决方案可以在第 564 页找到。\n\n# 总结\n\n模板是一个复杂的主题，然而通过这一章，我们发现这种复杂性可以创造出惊人的可重用代码。这里创建的`Queue`类可以保存任何元素的队列，而无需对其内部进行任何更改，这在编写需要在许多领域重用的代码时是一个巨大的好处。虽然不如 STL 队列功能全面、健壮或性能好，但它仍然能让我们了解，如果 STL 对我们不可用，或者由于某种原因不适合我们的需求，如何在不做太多工作的情况下重新创建 STL 容器的初级版本。我们查看了模板函数和类，发现了它们在正确使用时的强大功能，以及我们可能需要注意的地方。\n\n在下一章中，我们将介绍 STL 本身，并仔细观察它提供的容器。STL 将成为你未来 C++ 编程的无价之宝。它为我们做了很多艰苦的工作，这意味着我们不必像到目前为止在章节练习和示例中使用的那样一直实现低级内存操作。我们可以利用 STL 为我们提供的通用算法和容器的强大实现。"
  },
  {
    "path": "docs/cpp-workshop/12.md",
    "content": "# 十二、容器和迭代器\n\n概观\n\n本章将概述如何使用 C++ 标准库提供的容器和迭代器。该库为我们提供了许多算法和存储数据的方法，这样我们就可以专注于编写实用的代码，同时相信一个经过测试的健壮库正在为我们的程序提供动力。\n\n# 简介\n\n在本课程的前几章中，我们给出了没有使用 C++ 标准库的例子和练习。这可能会导致很多代码经常做得很少。我们坚持使用原始数组来帮助理解语言的基础；但是，在这一章中，我们将介绍强大的功能，这些功能将允许您用惊人的少量代码编写复杂的行为和功能。一旦你将标准库引入到你的项目中，C++ 真的会成为一种工作的乐趣，而且，任何 C++ 编译器都会很方便地提供它。我们可以不使用原始数组，不编写自己的队列和堆栈，而是使用预先存在的实现，所有这些都有一个公共接口。在开始我们的旅程之前，我们将首先解释什么是容器，并讨论它们的不同类型，然后讨论迭代器，以及它们如何使用这些容器变得非常自然和高效。\n\n# 容器\n\nC++ 标准库中的容器是一组常见的数据结构。这些结构采用列表、堆栈、数组等形式。容器可以存储数据和对象，并且可以分为几种不同的类型。这些类型以及与之关联的容器类是顺序容器——字符串、向量、列表、deque 和数组。\n\n## 绳子是一个容器\n\n在 C++ 中，字符串是一种类类型，这意味着它们是一个对象，具有成员变量和作用于它们的函数；它们是标准库中的一个容器，就像其他容器一样。在字符串类接口下面是一个 C 风格的字符数组，字符串类提供了在这个序列中访问单个字节的功能。与标准字符数组相比，使用字符串的好处是，许多可以在其他容器中使用的算法，如排序和搜索，也可以应用于字符串。如果我们使用一个标准的 C 风格的字符数组来保存我们的字符串，就像我们在前面的章节中所做的那样，那么我们将无法利用这些预先编写的算法，我们将不得不编写自己的算法。\n\n标准库提供的每个容器都有一组构造函数，这些构造函数为我们初始化容器提供了灵活性。本章将在引入新容器时介绍这些内容。有些构造函数不会立即对我们有用，但它们值得我们将来了解。\n\n## 字符串构造函数\n\n字符串类有几种不同的构造函数可以使用:\n\n*   `string();`:我们可以用这个创建一个没有字符的空字符串。\n*   `string(const string& str);`:我们可以用这个从另一个字符串的副本构造一个字符串。\n*   `string(const string& str, size_t pos, size_t len = npos);`:我们可以用这个从现有字符串的子串构造一个字符串。\n*   `string(const char* s);`:我们可以用这个用`s`指向的 C 风格 char 数组的副本来构造一个字符串。\n*   `string(const char* s, size_t pos, size_t n);`:我们可以用这个来构造一个字符串，这个字符串使用的是一个由`s`指向的 C 风格的`char`数组的副本，其中有特定数量的元素要复制(`n`)。\n*   `string(size_t n, char c);`:我们可以用它来构造一个大小为`n`字符的字符串，初始化为`c`的副本。\n*   `template <class InputIterator> string (InputIterator first, InputIterator last);`:我们可以用它来构造一个范围在第一个迭代器和最后一个迭代器之间的字符串。\n\n我们将在下面的练习中实现其中的一些字符串。\n\n## 练习 73:创建字符串\n\n如*字符串构造器*部分所示，有许多方法可以构造字符串。我们可以尝试这些，并打印它们的值。首先，在 cpp.sh 上打开一个新文件，创建一个基本的`main`函数，该函数还包括`<iostream>`和`<string>`。我们还将声明我们正在使用`std`命名空间，因为字符串存在于该命名空间中，并且我们可以避免需要键入范围运算符和命名空间。\n\n注意\n\n本练习的完整代码可以在[https://packt.live/2rXH6ln](https://packt.live/2rXH6ln)找到。\n\n以下是完成练习的步骤:\n\n1.  从`main`功能开始:\n\n    ```cpp\n    #include <iostream>\n    #include <string>\n    using namespace std;\n    int main()\n    {\n        return 0;\n    }\n    ```\n\n2.  使用`string()`构造函数\n\n    ```cpp\n        string str;\n    ```\n\n    创建一个空字符串\n3.  使用带有`string(const char* s)` :\n\n    ```cpp\n        string str1(\"Hello, I'm a string!\");\n    ```\n\n    的 C 风格字符数组创建字符串\n4.  使用`string(const string& str);` :\n\n    ```cpp\n        string str2(str1);\n    ```\n\n    从另一个字符串的副本创建一个字符串\n5.  使用`string(const string& str, size_t pos, size_t len = npos)` :\n\n    ```cpp\n        string str3(str1, 0, 5);\n    ```\n\n    从现有字符串的子字符串创建一个字符串\n6.  用`string(const char* s, size_t pos, size_t n)` :\n\n    ```cpp\n        string str4(\"Hello, I'm a string!\", 0, 5);\n    ```\n\n    从 C 风格字符数组的子字符串创建一个字符串\n7.  使用字符和`string(size_t n, char c)` :\n\n    ```cpp\n        string str5(10, 'x');\n    ```\n\n    所需的长度创建一个字符串\n8.  从现有字符串的子字符串创建一个字符串，但是使用迭代器来遗漏第一个和最后一个字符`<class InputIterator> string (InputIterator first, InputIterator last)`模板:\n\n    ```cpp\n        string str6(str4.begin() + 1, str4.end() - 1);\n    ```\n\n9.  编写以下输出命令:\n\n    ```cpp\n        cout << str << endl;\n        cout << str1 << endl;\n        cout << str2 << endl;\n        cout << str3 << endl;\n        cout << str4 << endl;\n        cout << str5 << endl;\n        cout << str6 << endl;\n    ```\n\n10.  下面是完整的程序:\n\n    ```cpp\n    #include <iostream>\n    #include <string>\n    using namespace std;\n    int main()\n    {\n        string str;\n        string str1(\"Hello, I'm a string!\");\n        string str2(str1);\n        string str3(str1, 0, 5);\n        string str4(\"Hello, I'm a string!\", 0, 5);\n        string str5(10, 'x');\n        string str6(str4.begin() + 1, str4.end() - 1);\n        cout << str << endl;\n        cout << str1 << endl;\n        cout << str2 << endl;\n        cout << str3 << endl;\n        cout << str4 << endl;\n        cout << str5 << endl;\n        cout << str6 << endl;\n        return 0;\n    }\n    ```\n\n11.  Run the complete program. You should obtain the following output:\n\n    ![Figure 12.1: Output strings ](img/C14195_12_01.jpg)\n\n图 12.1:输出字符串\n\n所有这些构造函数都给了我们很大的灵活性，允许我们控制如何初始化字符串。\n\n## 分配给字符串\n\n字符串也可以使用赋值操作符以及通过构造函数来初始化。有几种不同的重载可用，如下所述:\n\n*   `string& operator= (const string& str);`:从现有字符串的副本初始化\n*   `string& operator= (const char* s);`:从 C 风格字符数组的副本初始化\n*   `string& operator= (char c);`:用`char`初始化\n\n下面的代码片段显示了一些给字符串赋值的例子。我们在这里不需要太多的细节，但是知道当我们想要将一个字符串分配给另一个字符串或者从一个现有的字符串初始化一个字符串时，那么标准库已经涵盖了我们:\n\n```cpp\n#include <iostream>\n#include <string>\nusing namespace std;\nint main()\n{\n    string str = \"Hello, I'm a string!\";\n    string str1 = str;\n    string str2;\n    str2 = 'x';\n    cout << str << endl;\n    cout << str1 << endl;\n    cout << str2 << endl;\n    return 0;\n}\n```\n\n上述代码产生以下输出:\n\n![Figure 12.2: Output when using the assignment operator ](img/C14195_12_02.jpg)\n\n图 12.2:使用赋值运算符时的输出\n\n以这个例子中`str`的方式初始化字符串是非常常见的。`Hello, I'm a string!`实际上是一个字符数组，所以我们从 C 风格的 char 数组的副本中初始化一个字符串。\n\n## 对字符串的操作\n\n字符串类提供了许多不同的操作来操作底层的字节序列。您可能会发现其中一些类似于第 11 章、*模板*中的通用队列。对字符串的操作非常有用。我们可能需要在字符串中添加一个字符来帮助我们识别它们，或者我们可能需要删除程序中不需要的无关字符，当我们读取字符串的地方以这种方式存储它们时。让我们看看一些可以使用的字符串操作:\n\n*   `push_back(char c)`:将一个字符(c)推到字符串的末尾。\n*   `pop_back()`:删除字符串的最后一个字符。\n*   `capacity()`:提供字符串的当前容量。这不一定对应于字符串的当前大小，因为就像*第 11 章*、*模板*中描述的队列一样，它可能有预分配的额外内存。\n*   `resize(size_t n) & resize(size_t n, char c)`:调整字符串的大小。如果该值小于当前字符串大小，字符序列中该大小之后的任何内容都将被移除。重载函数接受一个字符 c，并将任何新元素初始化为 c 的副本。\n*   `shrink_to_fit()`:将字符串的容量设置为当前大小。\n*   `reserve(size_t n)`:将字符串的容量更改为允许 n 个字符。\n\n下面是一个简单程序中使用的所有这些函数的示例:\n\n```cpp\n#include <iostream> \n#include <string> \nusing namespace std; \nint main() \n{\n    string str(\"Hello, I'm a string\"); \n    str.push_back('!'); \n    str.push_back('!'); \n    cout << str << endl; \n    str.pop_back(); \n    cout << str << endl; \n    // notice this will keep existing contents and append x for the rest \n    str.resize(25, 'x'); \n    // reserve space for 50 chars - capacity() \n    str.reserve(50); \n    // notice that ! is pushed after the last char not     //the end of allocated space \n    str.push_back('!'); \n    cout << str << endl; \n    cout << str.capacity() << endl; \n    cout << str.size() << endl; \n    str.shrink_to_fit(); \n    // note : shrink_to_fit is not guaranteed to be exactly size() \n    // depending on compiler implementation \n    cout << str.capacity() << endl; \n    return 0; \n}\n```\n\n前面的代码将产生以下输出:\n\n![Figure 12.3: Output when performing push back operations on strings ](img/C14195_12_03.jpg)\n\n图 12.3:对字符串执行回推操作时的输出\n\n## 迭代器\n\n就像我们将介绍的其他容器一样，字符串有迭代器，可以用来允许在循环中遍历字符串。\n\n来自字符串和大多数标准库容器的迭代器可以通过以下函数访问:\n\n*   `begin()`:字符串开头的迭代器\n*   `end()`:字符串末尾的迭代器\n*   `rbegin()`:字符串开头的反向迭代器\n*   `rend()`:字符串末尾的反向迭代器\n\n标准库中容器的迭代器类型可以使用类名加上作用域运算符和迭代器反向迭代器来获得，具体取决于需要哪种类型。对于字符串，这意味着可以使用`string::iterator`或`string::reverse_iterator`获得迭代器。这些迭代器可以存储为变量以供重用，或者在`for`循环中使用，它们存在于 for 循环的范围内。\n\n下面是一个使用这些迭代器循环并显示字符串内容的简单示例:\n\n```cpp\n#include <iostream> \n#include <string> \nusing namespace std; \nint main() \n{\n    string str(\"Hello, I'm a string\"); \n    for(string::iterator it = str.begin(); it != str.end(); it++) \n    { \n        cout << *it; \n    }\n    cout << endl; \n    for(string::reverse_iterator rit = str.rbegin(); rit != str.rend();         rit++) \n    { \n        cout << *rit; \n    } \n    cout << endl; \n    return 0;\n}\n```\n\n上述代码产生以下输出:\n\n![Figure 12.4: Output when using the string iterators and reverse iterators ](img/C14195_12_04.jpg)\n\n图 12.4:使用字符串迭代器和反向迭代器时的输出\n\n请注意，反向迭代器允许我们在容器中反向移动的同时使用递增 for 循环。这两个迭代器都被称为双向迭代器，这意味着我们可以在两个方向上进行迭代，因此如果需要，我们可以在两个方向上循环这两种类型的迭代器。\n\n## 进一步研究\n\n字符串类有许多有用的操作，与 C 风格的字符数组相比，这些操作非常强大。有一些函数可以查找字符串中的某些字符，删除或替换字符串中的字符，将一个字符串复制到字符数组中，并将一个字符串追加到另一个字符串中。此外，字符串类提供重载操作符，如`+=`追加到字符串中，以及`+`连接字符串。与其在这里用例子列出所有这些函数，不如研究这些函数并将它们应用到自己的示例程序中。\n\n## 练习 74:是回文吗？\n\n因为字符串的行为就像标准库中的其他容器一样，所以我们可以利用`<algorithm>`头中存在的常见算法。这使得能够用最少的代码进行复杂的操作。\n\n下面的练习展示了我们如何编写一个函数来检查一个字符串是否是回文，利用一些算法。\n\n注意\n\n本练习的完整代码可以在[https://packt.live/37tTYQ3](https://packt.live/37tTYQ3)找到。\n\n以下是完成练习的步骤:\n\n1.  我们的第一个任务是允许用户输入要检查的文本。我们将使用`std::getline`函数从`std::cin`中读取并将其存储在字符串中。这就是我们的主要功能:T2\n2.  Now we must create the stub of our `isPalindrome` function and place it above our `main` function. This function will take a `const` reference to a string and return a `bool`:\n\n    ```cpp\n    bool isPalindrome(const string& str)\n    {\n        return false;\n    }\n    int main()\n    {\n        string str;\n\n        getline(cin, str);\n\n        cout << \"'\" << str << \"'\" << \" is a palindrome? \"          << (isPalindrome(str) ? \"YES\" : \"NO\") << endl;\n        return 0;\n    }\n    ```\n\n    通过输入要检查的字符串来测试这一点。当然，在这一点上它会说`NO`，但我们会知道到目前为止一切都在编译中。\n\n3.  There are some characters that we would like to ignore in our palindrome check, such as spaces and punctuation. The following code implements this:\n\n    ```cpp\n    bool isPalindrome(const string& str)\n    {\n        // make a copy of the string\n        string s(str.begin(), str.end());\n        // remove any spaces or punctuation\n        s.erase(remove_if(s.begin (), s.end (), [](const char& c) { return\n        ispunct(c) || isspace(c);}), s.end());\n    ```\n\n    有很多回文包含标点和空格，如果考虑到这些，会使回文无效。我们会选择完全忽略这些，只检查字母。为此，我们将使用范围构造函数复制传入的字符串，然后使用`remove_if`算法给我们一个只包含非空格或标点符号元素的范围。\n\n    `remove_if`算法将任何不满足我们给它的谓词的元素转移到容器的末尾，并将满足谓词的元素留在容器开头的一个范围内。然后它返回一个迭代器，我们可以使用它来指向我们想要的范围内的最后一个元素。我们可以使用这个迭代器作为字符串`erase`函数的第一个参数，删除所有超出我们关心范围的元素。\n\n    现在，我们已经删除了我们不需要的元素，我们现在可以移动到较低的外壳，我们剩余的元素；至于我们的回文检查，我们希望它不区分大小写。为此，我们可以使用`transform`算法。这个算法允许我们对一个范围内的每个元素调用一个函数。我们将在范围内的每个元素上使用一个名为`tolower`的函数。\n\n4.  编写以下代码，将`transform`算法合并到`isPalindrome`函数中:\n\n    ```cpp\n        // lower case what's left\n        transform(s.begin(), s.end(), s.begin(), ::tolower);\n    ```\n\n5.  现在我们有了一系列小写字符，不包含任何标点符号或空格，我们可以创建字符串的反向版本。为此，我们将再次使用范围构造函数，但这次我们将传入反向迭代器。然后我们可以比较这两个字符串，看看它们是否匹配。如果有，那么我们有一个回文:\n\n    ```cpp\n        // create a reversed version of the string\n        string sr(s.rbegin(), s.rend());\n\n        // compare them\n        return (s == sr);\n    }\n    ```\n\n6.  现在我们可以在一个新的`main`函数中测试我们的回文检查器。我们将把我们的字符串初始化为一个经典的回文，“从不奇数或偶数”，然后把它传递到我们的函数中，并显示它是否是回文:\n\n    ```cpp\n    int main()\n    {\n        string str = \"Never odd or even\";\n\n        cout << \"'\" << str << \"'\" << \" is a palindrome? \"          << (isPalindrome(str) ? \"YES\" : \"NO\") << endl;\n\n        return 0;\n    }\n    ```\n\n7.  下面是完整的代码:\n\n    ```cpp\n    #include <iostream>\n    #include <string>\n    #include <algorithm>\n    using namespace std;\n    bool isPalindrome(const string & str) {\n        // make a copy of the string\n        string s(str.begin(), str.end());\n        // remove any spaces or punctuation\n        s.erase(remove_if(s.begin(), s.end(), [](const char & c) {\n        return ispunct(c) || isspace(c); }), s.end());\n        // lower case what's left\n        transform(s.begin(), s.end(), s.begin(), ::tolower);\n        // create a reversed version of the string\n        string sr(s.rbegin(), s.rend());\n        // compare them\n        return (s == sr);\n    }\n    int main() \n    {\n        string str = \"Never odd or even\";\n        cout << \"'\" << str << \"'\" << \" is a palindrome? \"          << (isPalindrome(str) ? \"YES\" : \"NO\") << endl;\n        return 0;\n    }\n    ```\n\n8.  运行完整的代码。您将获得以下输出:\n\n![Figure 12.5: Palindrome  ](img/C14195_12_05.jpg)\n\n图 12.5:回文\n\n字符串通常是比使用字符数组更好的选择。它们提供了许多有用的功能，并且通常更容易使用。标准库还升级了我们经常使用的另一种类型:数组。让我们继续讨论数组的替代方法——向量。\n\n# 向量-方便、可调整大小的数组\n\n向量实现了多用途的动态数组。请记住，C++ 数组必须是固定大小的；我们不能调整数组的大小，如果需要添加另一个元素，我们将不得不创建新的数组并复制内容。我们在本课程中创建的自定义类中已经做了很多这样的工作。向量允许我们创建一个容器，它可以在我们需要的时候调整大小。向量中的元素是连续存储的，即以相邻的方式布局，因此可以通过指向元素的偏移指针和迭代器来访问。我们可以通过包含`<vector>`头来利用向量。\n\n## 向量构造函数\n\n当我们想要创建一个新的向量时，向量给了我们许多不同的构造函数来使用。这里概述了这些构造函数的选择:\n\n*   `vector();`:构造一个空向量。\n*   `vector(size_t n, const T& value = T());`:使用以 t 为参数的构造函数或者`T`默认的默认构造函数，构造一个包含多个元素(n)的向量，可选地进行初始化。\n*   `template <class InputIterator> vector(InputIterator first, InputIterator last);`:从第一个迭代器和最后一个迭代器之间的范围构造一个向量。\n*   `vector(const vector& other);`:通过复制另一个向量来构造一个向量(复制构造函数)。\n*   `vector (initializer_list<value_type> il);`:使用初始化列表构造一个向量。\n\n下面的代码片段显示了这些正在使用的构造函数:\n\n```cpp\n#include <iostream>\n#include <vector>\nusing namespace std;\nint main()\n{\n    // default constructed empty vector\n    vector<int> intVector;\n    vector<int> initializerListIntVector = {1,2,3};\n    // default constructed vector of 5 floats\n    vector<float> floatVector(5);\n    // vector of 5 floats initialized to 1.0f\n    vector<float> floatVectorAllOne(5, 1.0f);\n    // vector constructed from an existing vector\n    vector<float> anotherFloatVector(floatVector);\n    // range constructed vector\n    vector<float> rangeConstructedFloatVector(anotherFloatVector.begin(),\n    anotherFloatVector.end());\n    return 0; \n}\n```\n\n每个构造函数都在这个片段中使用，供您参考。所有这些构造函数在某些情况下都是有用的，所以花点时间去学习它们，以确保你的程序使用了正确的构造函数。\n\n## 向量赋值\n\n向量也可以使用向量重载赋值运算符`vector& operator=( const vector& other )`为其赋值。这里有一个例子:\n\n```cpp\n#include <iostream>\n#include <vector>\nusing namespace std;\nint main()\n{\n    // default constructed vector of 5 floats\n    vector<float> floatVector(5);\n    vector<float> floatVectorAssigned = floatVector;\n    return 0;\n}\n```\n\n当给向量赋值时，它必须具有有效的转换。如果没有，编译器将在该行报告错误。例如`vector<int>`不能分配给`vector<float>`。但是，如果您使用的是范围构造函数，并且两种类型之间存在转换，那么可以按照下面的代码片段来构造它们:\n\n```cpp\n#include <iostream>\n#include <vector>\nusing namespace std;\nint main()\n{\n    // default constructed vector of 5 floats\n    vector<float> floatVector(5);\n\n    vector<int> intVector(floatVector.begin(), floatVector.end());\n\n    return 0;\n}\n```\n\n这里，我们已经使用一系列的`float`构造了一个`int`向量。因为这些类型可以被转换，所以构造函数可以正确地处理这个。\n\n## 练习 75:访问向量中的元素\n\n从本章的介绍中我们知道，向量是一个顺序容器，因此我们的元素可以顺序迭代，并且可以通过索引访问。向量的行为确实非常像原始数组，而且当您需要一个顺序容器时，向量几乎总是比数组更好的选择。为了引入向量，我们将创建一个小程序，该程序填充一个向量并通过索引访问一个元素，然后还迭代该向量和其中所有元素的值:\n\n注意\n\n本练习的完整代码可以在[https://packt.live/2QQtalv](https://packt.live/2QQtalv)找到。\n\n1.  从`cpp.sh`上的新文件开始，添加一个主功能。要访问向量，我们需要包含向量头和 iostream，这样我们就可以打印我们的值:\n\n    ```cpp\n    #include <iostream>\n    #include <vector>\n    using namespace std;\n    int main()\n    {\n        return 0;\n    }\n    ```\n\n2.  现在，我们可以创建我们的向量，并使用初始化列表构造函数`(initializer_list<value_type> il)`向量\n\n    ```cpp\n    int main()\n    {\n        vector<int> vec = {1,2,3,4,5,6,7,8,9,10};\n\n        return 0;\n    }\n    ```\n\n    用一些值来填充它\n3.  When we want to iterate over the elements in a container, we can use the range `for` loop. In this case, it would look like the following:\n\n    ```cpp\n        for(auto v : vec)\n        {\n            cout << v << \" \";\n        }\n    ```\n\n    `auto`关键字允许我们让编译器计算出向量中包含的内容。然后，它循环遍历所有元素，并按顺序将它们作为这里称为`v`的元素返回。\n\n4.  我们现在还可以通过索引访问向量中的元素。获取数字 4，在我们的零索引向量中，它位于索引`3` :\n\n    ```cpp\n        cout << vec[3];\n    ```\n\n5.  当我们运行程序时，我们应该看到我们所有的`vec`值都和数字 4 一起打印出来。下面是代码:\n\n    ```cpp\n    #include <iostream>\n    #include <vector>\n    using namespace std;\n    int main()\n    {\n        vector<int> vec = {1,2,3,4,5,6,7,8,9,10};\n\n        for(auto v : vec)\n        {\n            cout << v << \" \";\n        }\n\n        cout << vec[3];\n\n        return 0;\n    }\n    ```\n\n6.  运行代码。您将获得以下输出:\n\n![Figure 12.6: Accessing elements of a vector ](img/C14195_12_06.jpg)\n\n图 12.6:访问向量的元素\n\n访问向量的元素显然非常重要，然而向量允许我们执行许多其他操作。下一节将向您概述这些操作。\n\n## 向量运算\n\n对向量的大多数有用操作都涉及到在向量中添加和移除元素。这是向量的本质，也是它们优于标准数组的原因。就像字符串一样，我们将开始看到不同容器上的操作之间的一些共性。以下是一些可应用于向量的有用函数:\n\n*   `push_back()`:将一个元素推到向量的后面。\n*   `pop_back()`:移除并销毁向量中的最后一个元素。\n*   `insert(const_iterator pos, const T& val)`:在`pos`迭代器指定的位置插入`val`元素。\n*   `erase(const_iterator pos) & erase(const_iterator first, const_iterator last)`:擦除`pos`迭代器中的元素或第一个和最后一个迭代器之间的范围。\n*   `clear()`:移除并销毁矢量的所有元素。\n*   `emplace()`:类似于`insert`，但是 insert 引用了一个已经构建的元素，而侵位构建了一个新的元素，并增长了向量来适应它。\n\n与字符串类似，向量也公开函数来获取容量、调整大小和保留内存。\n\n## 搜索向量\n\n很多编程都涉及到根据一些特定的逻辑找到特定的元素或元素范围。我们可能希望找到值小于 10 的`vector<int>`的所有元素。我们可能想在向量中搜索一个符合我们标准的特定元素。包含在标准库<算法>头中的算法可以应用于我们的向量，允许我们用最少的代码轻松搜索特定的元素。最重要的向量搜索算法是`std::find`和`std::find_if`。\n\n这些算法使用迭代器来搜索向量中的一系列元素，并将迭代器返回到符合条件的第一个元素。如果没有元素符合条件，那么返回一个到向量末尾的迭代器。这可以在检查元素是否存在时进行比较。看下面的例子，它检查一个特定的`int`是否包含在一个`vector<int>`中:\n\n```cpp\n#include <iostream>\n#include <algorithm>    // std::find\n#include <vector>\nusing namespace std;\nbool contains(const int value, const vector<int>& vec)\n{\n    return (find(vec.begin(), vec.end(), value) != vec.end());\n}\n```\n\n在前面的函数中，find 算法将迭代器带到我们想要搜索的范围的开始。由于我们要搜索整个向量，所以我们传入`begin()`，这是向量的开始，然后结束为向量的结束。这构成了整个阵列。最后一个参数是值，也就是我们希望找到的`int`。然后将从该函数返回的值与`end()`进行比较。如果它相等，那么我们知道它不包含在向量中，因为`end()`指向最后一个元素的过去，而不是元素本身。下面的代码片段使用这个函数来检查整数向量中的数字 9:\n\n```cpp\nint main()\n{\n    vector<int> vec = {1,2,3,4,5,6,7,8,9,10};\n\n    const int numToCheck = 9;\n\n    cout << \"Vector contains \" << numToCheck << \" \"          << (contains(numToCheck, vec) ? \"YES\" : \"NO\");\n    cout << endl;\n    return 0;\n}\n```\n\n该检查的输出应该是:\n\n![Figure 12.7: Output for the find algorithm ](img/C14195_12_07.jpg)\n\n图 12.7:查找算法的输出\n\n此外，我们可以使用`std::distance`函数，通过获取从向量开始到找到的元素的偏移量，返回向量中元素的索引。下面的代码片段检查一个元素是否包含在一个向量中，如果包含，则返回它的索引。如果没有找到元素，则返回`-1`，我们可以对照检查:\n\n```cpp\n#include <iostream>\n#include <algorithm>    // std::find\n#include <vector>\nusing namespace std;\nlong contains(const int value, const vector<int>& vec)\n{\n    vector<int>::const_iterator it = find(vec.begin(), vec.end(), value);\n\n    if(it != vec.end()) // we found the element\n        return distance(vec.begin(), it);\n\n    return -1;\n}\n```\n\n在这个函数中，`find`算法的使用方式与之前类似，但这次找到的元素的迭代器存储在`it`中。对`it`是否等于`end`进行相同的检查(未找到)，如果找到元素，距离函数返回索引，即数组中第一个元素和找到的元素之间的元素数量。\n\n下面的代码片段利用这个函数来检查数字 9 是否包含在我们的向量中，如果包含，我们可以得到它的索引:\n\n```cpp\nint main()\n{\n    vector<int> vec = {1,2,3,4,5,6,7,8,9,10};\n    const int numToCheck = 9;\n    long index = contains(numToCheck, vec);\n    cout << \"Vector contains \" << numToCheck << \" \"          << (index != -1 ? \"YES\" : \"NO\");\n    if(index != -1)\n        cout << \" and its index is \" << index;\n\n    cout << endl;\n    return 0;\n}\n```\n\n前面的代码将产生以下输出:\n\n![Figure 12.8: Checking for elements in a vector ](img/C14195_12_08.jpg)\n\n图 12.8:检查向量中的元素\n\n我们可以更进一步，创建一个模板函数，它可以从任何类型的向量中找到该类型的元素。下面的例子展示了标准库在编写最少的通用代码时是多么强大:\n\n```cpp\ntemplate<typename T>\nlong contains(const T& value, const vector<T>& vec)\n{\n    auto it = find(vec.begin(), vec.end(), value);\n\n    if(it != vec.end()) // we found the element\n        return distance(vec.begin(), it);\n    return -1;\n}\n```\n\n这里，我们用`auto`关键字替换了`int`迭代器。这意味着我们将得到正确类型的迭代器，即使它不是显式类型的。此外，该函数现在是一个模板函数，任何类型都已被`T`取代。现在，如果我们运行与之前相同的代码片段，但是使用新的`contains()`函数，我们应该会得到相同的结果，而不需要显式使用`int`纯向量`contains()`函数。\n\n## 练习 76:使用自定义比较对向量进行排序\n\n向量排序是另一种常见的操作。有一些内置的比较可以用于简单的排序，比如在整数向量上按升序和降序排序。有时，我们可能希望基于更精细的东西对向量进行排序，例如基于自定义类型中某个特定变量的升序或降序。\n\n注意\n\n本练习的完整代码可以在[https://packt.live/2tXeDN2](https://packt.live/2tXeDN2)找到。\n\n使用轨迹对象实现这一点，并根据轨迹长度对轨迹进行排序:\n\n1.  首先，我们将编写一个简单的类轨道，它有一个名称、长度和受欢迎程度:\n\n    ```cpp\n    #include <iostream>\n    #include <algorithm>\n    using namespace std;\n    class Track\n    {\n    public:\n        Track(float length, string name, int popularity) :           m_trackLength(length), m_trackName(name),           m_popularityRating(popularity) {}\n        float getLength() const { return m_trackLength; }\n        string getName() const { return m_trackName; }\n        int getPopularity() const { return m_popularityRating; }\n    private:\n        float m_trackLength;\n        string m_trackName;\n        int m_popularityRating;\n    };\n    ```\n\n2.  由此，我们可以创建一个轨迹向量，我们将使用我们的自定义比较进行排序:\n\n    ```cpp\n    int main()\n    { \n        vector<Track> tracks;\n        tracks.push_back(Track(199.0f, \"God's Plan\", 100));\n        tracks.push_back(Track(227.0f, \"Hold On, We're Going Home\", 95));\n        tracks.push_back(Track(182.0f, \"The Motto\", 80));\n        return 0;\n    }\n    ```\n\n3.  自定义比较本质上是一个函数，它接受两个相同类型的参数并返回一个`bool`。我们如何定义这个函数取决于我们自己，但是对于我们当前的程序，我们只想按升序对轨道长度进行排序，这样我们就可以比较轨道长度值:\n\n    ```cpp\n    bool trackLengthCompare(const Track& t1, const Track& t2)\n    {\n        return (t1.getLength() < t2.getLength());\n    }\n    ```\n\n4.  现在，我们可以在排序算法中使用它来按升序对我们的曲目进行排序，然后打印出来。我们可以将自定义排序函数作为参数传递给排序函数，同时将迭代器传递给向量的开头和结尾:\n\n    ```cpp\n        sort(tracks.begin(), tracks.end(), trackLengthCompare);\n    ```\n\n5.  We can test this code in our `main` function and also print out the values once they are sorted:\n\n    ```cpp\n    int main()\n    { \n        vector<Track> tracks;\n        tracks.push_back(Track(199.0f, \"God's Plan\", 100));\n        tracks.push_back(Track(227.0f, \"Hold On, We're Going Home\", 95));\n        tracks.push_back(Track(182.0f, \"The Motto\", 80));\n        sort(tracks.begin(), tracks.end(), trackLengthCompare);\n        for (auto t : tracks)\n        {\n            cout << t.getName() << endl;\n        }\n        return 0;\n    }\n    ```\n\n    下面是完整的代码:\n\n    ```cpp\n    #include<iostream>\n    #include<string>\n    #include<algorithm>\n    using namespace std;\n    class Track\n    {\n    public:\n        Track(float length, string name, int popularity) :           m_trackLength(length), m_trackName(name),           m_popularityRating(popularity) {}\n        float getLength() const { return m_trackLength; }\n        string getName() const { return m_trackName; }\n        int getPopularity() const { return m_popularityRating; }\n    private:\n        float m_trackLength;\n        string m_trackName;\n        int m_popularityRating;};\n        bool trackLengthCompare(const Track& t1, const Track& t2)\n        {\n            return (t1.getLength() < t2.getLength());\n        }\n    int main()\n    {\n        vector<Track> tracks;\n        tracks.push_back(Track(199.0f, \"God's Plan\", 100));\n        tracks.push_back(Track(227.0f, \"Hold On, We're Going Home\", 95));\n        tracks.push_back(Track(182.0f, \"The Motto\", 80));\n        sort(tracks.begin(), tracks.end(), trackLengthCompare);\n        for (auto t : tracks)\n        {\n            cout << t.getName() << endl;\n        }\n        return 0;\n    }\n    ```\n\n    运行代码后，您将获得以下输出:\n\n    ![Figure 12.9: Output for sorting with a custom comparison ](img/C14195_12_09.jpg)\n\n    图 12.9:带有自定义比较的排序输出\n\n    既然我们已经理解了如何使用 sort 函数，那么尝试用以下额外的功能来扩展这个练习:\n\n6.  写一个人气等级对比:\n\n    ```cpp\n        bool trackPopularityCompare(const Track& t1, const Track& t2)\n        {\n            return (t1.getPopularity () < t2.getPopularity());\n        }\n    ```\n\n7.  按曲目长度降序排序:\n\n    ```cpp\n        bool trackLengthCompare(const Track& t1, const Track& t2)\n        {\n            return (t1.getLength() > t2.getLength());\n        }\n    ```\n\n8.  按名称长度排序:\n\n    ```cpp\n        bool trackNameLengthCompare(const Track& t1, const Track& t2)\n        {\n            return (t1.getName().size() < t2.getName().size());\n        }\n    ```\n\n# 地图/无序地图:我们的关联容器\n\n地图和无序地图是关联容器。地图/无序地图中的元素与一个键相关联，并通过该键进行访问。地图/无序地图中没有两个元素可以具有相同的键。在内部，使用内部比较对象按关键字对地图进行排序。相反，无序映射中的元素不是按关键字排序的，而是以允许按关键字快速检索元素的方式存储的。映射通常更适合迭代和查找键，而无序映射更能有效地通过相关键直接访问元素。如果你最终在数据中循环并以某种方式使用每个元素，那么地图是最合适的；如果您主要是通过元素的键从容器中获取元素，而不是在整个集合中循环，那么无序映射是最合适的。\n\n## 构建地图和无序地图\n\n以下是构建地图时要使用的构造函数的选择:\n\n*   `map()`:默认构造函数，空映射\n*   `map(const map& x)`:从另一个地图的副本构建一个地图\n*   `template <class InputIterator>; map (InputIterator first, InputIterator last)`:根据范围构建地图\n*   `map (initializer_list<value_type> il)`:使用初始化列表构造一个地图\n\n下面是一个示例片段，展示了如何使用上述构造函数:\n\n```cpp\n#include <iostream>\n#include <map>\nusing namespace std;\nint main()\n{\n    map<int, int> myMap;\n    map<int, int> copiedMap(myMap);\n    map<int, int> rangeMap(copiedMap.begin(), copiedMap.end());\n    map<int, int> initList = { {1,2}, {2,3}, {3,4} };\n}\n```\n\n当声明一个映射时，我们需要传入两个模板参数，如前面的片段所示。首先是我们希望地图中的键是什么类型。这些是我们将与第二个参数类型相关联的标识符；价值。键和值的结合是关联容器的“关联”部分。\n\n当然，无序映射也有一组类似的构造函数，其中一些添加了指定内部哈希表使用的最小桶数的选项，用于通过键检索元素。桶在内部将容器的序列划分成更小的子序列。如果未指定该值，则自动确定。这些是无序映射的构造函数:\n\n*   `unordered_map(size_type n)`:构建一个具有可选最小桶大小的无序地图\n*   `unordered_map(const unordered_map& x)`:从另一个无序地图的副本构建一个无序地图\n*   `template <class InputIterator>unordered_map (InputIterator first, InputIterator last)`:从一个范围构建一个无序的地图\n*   `unordered_map (initializer_list<value_type> il, size_type n)`:用可选的最小存储桶大小，从初始化列表中构造一个无序映射\n\n下面是一个示例片段，其中实现了上述每个映射:\n\n```cpp\n#include <iostream>\n#include <unordered_map>\nusing namespace std;\nint main()\n{\n    unordered_map<int, int> myUnorderedMap;\n    unordered_map<int, int> copiedUnorderedMap(myUnorderedMap);\n    unordered_map<int, int> rangeUnorderedMap(copiedUnorderedMap.begin(), \n    copiedUnorderedMap.end());\n    unordered_map<int, int> initList = { {1,2}, {2,3}, {3,4} };\n}\n```\n\n## 对地图和无序地图的操作\n\n映射和无序映射公开了与其他容器类似的操作，如字符串和向量。这些容器的独特之处在于，当向`map` / `unordered_map`中添加元素时，我们使用`std::pair`；我们在下文中称之为键值对。我们可以使用`std::make_pair`创建键值对。以下示例显示了如何将元素插入到地图和无序地图中。注意插入元素时`std::pair`的使用。这个键值对是键和值的关联:\n\n```cpp\n#include <iostream> \n#include <unordered_map> \n#include <map>\nusing namespace std;\n\nint main()\n{\n    unordered_map<int, int> myUnorderedMap;\n    map<int, int> myMap;\n    myUnorderedMap.insert(make_pair(1, 2));\n    myMap.insert(make_pair(1, 2));\n}\n```\n\n遍历映射时，迭代器将指向映射中的键值对。映射或无序映射中的每个元素都是一个键值对，该对的键类型是我们声明的键模板参数类型，值是我们的值模板参数类型(在前面的例子中，键是一个`int`，值也是一个`int`)。我们可以使用第一个访问键，使用第二个访问值。\n\n以下示例显示了地图的简单遍历和打印(同样适用于无序地图):\n\n```cpp\n#include <iostream> \n#include <map>\n#include <string>\nusing namespace std;\nint main()\n{\n    map<string, string> myStringMap = \n    {\n        {\"Hello\", \"Hola\"},\n        {\"Goodbye\", \"Adiós\"},\n        {\"Programmer\", \"Programación\"}\n    };\n    for (const auto& loc : myStringMap)\n    {\n       cout << loc.first << \" In Spanish is \" << loc.second << endl;\n    }\n}\n```\n\n当您运行前面的代码时，您将获得以下输出:\n\n![Figure 12.10: Traversal and printing of a map ](img/C14195_12_10.jpg)\n\n图 12.10:地图的遍历和打印\n\n在这个例子中，映射是使用初始化列表构造函数预先填充的。使用初始值设定项列表时，值用逗号和大括号分隔，组成键值对:\n\n```cpp\n{\"Hello\", \"Hola\"},\n```\n\n然后，该示例使用 ranged `for`循环遍历每个元素，并分别打印其第一个和第二个键和值。\n\n## 练习 77:地图测验\n\n除了像浮点数和整数这样的内置类型，我们还可以使用自定义类型作为键。关键自定义类型的一个要求是，我们需要通过重载`<`运算符来实现比较，或者创建一个自定义对象，该对象可以在声明地图时作为模板参数传入。这是必需的，以便地图知道如何对其关键字进行排序。\n\n注意\n\n本练习的完整代码可以在[https://packt.live/2FkP6js](https://packt.live/2FkP6js)找到。\n\n让我们用一张地图写一些实际的东西——在本例中，是一个有多个选项的测验，在最后显示我们的分数:\n\n1.  首先，我们将创建名为`Question`的自定义键类型。这将有问题本身的成员变量，它的问题编号(我们将在比较对象中使用它)，以及正确答案的索引:\n\n    ```cpp\n    #include <iostream>\n    #include <map>\n    #include <string>\n    #include <vector>\n    using namespace std;\n    class Question \n    {\n    public:\n        Question(int questionNumber, string question, int answerIndex):              m_questionNumber(questionNumber), m_question(question),              m_answerIndex(answerIndex) {}\n        int getQuestionNumber() const \n        {\n            return m_questionNumber;\n        }\n        string getQuestion() const \n        {\n            return m_question;\n        }\n        int getAnswerIndex() const \n        {\n            return m_answerIndex;\n        }\n    private:\n        int m_questionNumber;\n        string m_question;\n        int m_answerIndex;\n    };\n    ```\n\n2.  Next, we will create the custom comparison object that the map will use to sort the keys:\n\n    ```cpp\n    struct QuestionCompare\n    {\n        bool operator() (const Question& lhs, const Question& rhs) const\n        {\n            return lhs.getQuestionNumber() < rhs.getQuestionNumber();\n        }\n    };\n    ```\n\n    我们现在有几个步骤来创建我们的测验，所以我们必须声明我们的地图。我们地图的关键将是我们定制的`Question`类型。容器中键值对的值部分也可以是容器。这意味着我们可以拥有一对`<int, vector<int>>`或`<float, map<int, int>>`类型的键值对。通过使用字符串向量作为值类型，我们将利用一对的值部分作为我们多重选择的容器的能力。最后，我们将使用自定义比较对象作为最终模板参数:\n\n    ```cpp\n    int main()\n    {\n        map<Question, vector<string>, QuestionCompare> quiz;\n    ```\n\n3.  现在我们可以创建我们的`Question`对象和包含答案的字符串向量，然后将它们插入测验地图:\n\n    ```cpp\n        Question question1(1, \"Which two actors directed themselves     in movies and won Oscars 2 for Best Actor?\", 2);\n        vector<string> question1Answers =\n        {\n            \"Al Pacino and Timothy Hutton\",\n            \"Jack Nicholson and Kevin Spacey\",\n            \"Laurence Olivier and Roberto Benigni\",\n            \"Tom Hanks and Paul Newman\"\n        };\n        Question question2(2, \"\\\"After all, tomorrow is another day!\\\"     was the last line 12 in which Oscar-winning Best Picture?\", 0);\n        vector<string> question2Answers =\n        {\n            \"Gone With the Wind\",\n            \"Great Expectations\",\n            \"Harold and Maude\",\n            \"The Matrix\"\n        };\n        quiz.insert(make_pair(question1, question1Answers));\n        quiz.insert(make_pair(question2, question2Answers));\n    ```\n\n4.  现在我们可以创建我们的主循环，它会询问我们的问题，然后等待输入。我们将首先为我们的地图获取一个迭代器，然后继续提问，获取答案，并增加我们的迭代器，直到它匹配`quiz.end()` :\n\n    ```cpp\n        cout << \"Welcome to the movie quiz\" << endl;\n        cout << \"Type your answer between 1-4 and press enter:\"          << endl;\n        map<Question, vector<string>>::        iterator quizIterator = quiz.begin();\n        vector<bool> correctAnswers;\n        while (quizIterator != quiz.end())\n        {\n            cout << quizIterator->first.getQuestion() << endl;\n            int answerIndex = 1;\n            for(auto answer : quizIterator->second)\n            {\n                cout << answerIndex << \" : \" << answer << endl;\n                answerIndex++ ;\n            }\n            int answer;\n            cin >> answer;\n    ```\n\n5.  We will push any correct answers onto a vector of `bool` values and then use the size of that vector to output the score at the end:\n\n    ```cpp\n            int correctAnswer = quizIterator->first.getAnswerIndex();\n            bool wasCorrect = answer - 1 == correctAnswer;\n            cout << (wasCorrect ? \"CORRECT!\" : \"INCORRECT!\")              << \" Correct answer is: \"              << quizIterator->second[correctAnswer] << endl;\n            if (wasCorrect)\n                    correctAnswers.push_back(answer);\n            quizIterator++ ;\n        }\n        cout << \"Your score was \" << correctAnswers.size()          << \" out of \" << quiz.size() << endl;\n        cout << \"done\";\n    }\n    ```\n\n    完整测验的输出如下:\n\n![Figure 12.11: Final output ](img/C14195_12_11.jpg)\n\n图 12.11:最终输出\n\n如您所见，关联容器对于依赖于可以通过键查找的值的程序非常有用，或者甚至只是将一个特定的值与另一个值相关联，就像练习对问题及其各自可能的`answer`向量所做的那样。\n\n# 集合/多集合\n\n集合就像地图一样是一个关联容器，就像地图一样，每个键都是唯一的；它不能有多个相同的键。集合的区别在于它不是键值对的容器。它本质上是一个唯一键的容器，就像一个向量，向量中的每个元素都是唯一的。元素一旦被添加到集合中，就不能被修改；但是它可以从集合中移除。多集的行为就像一个集合，除了允许多个非唯一键。\n\n## 施工人员\n\n构建集合时，我们还可以传入用于对集合进行排序的比较器。比较器是一个用来决定元素在集合中如何排序的函数。以下是用于集合和多集合的构造函数的选择(为简洁起见，仅显示集合):\n\n*   `set();`:默认空集\n*   `set(const key_compare& comp);`:用选择的比较对象清空集合\n*   `set(const set& x);`:复制构造函数\n*   `template <class InputIterator> set (InputIterator first, InputIterator last, const key_compare& comp = key_compare());`:带有可选选择比较的范围构造器\n*   `set (initializer_list<value_type> il, const key_compare& comp = key_compare());`:带有可选选择比较的初始化列表\n\n## 练习 78:一组的自定义比较器\n\n默认情况下，集合将按升序对元素进行排序，因此如果我们要创建一组整数，它们将按升序排序。这对于像`int`这样的内置类型来说很好，但是如果我们有自己的自定义类型，那么我们必须定义元素是如何排序的。我们可以定义该类型的哪个属性用于排序，甚至可以将默认排序从升序更改为降序。在本练习中，我们将创建一个名为 Person 的类，其中包含姓名和年龄。然后，我们将为该集合创建一个自定义比较，以按年龄降序对人员元素进行排序:\n\n注意\n\n本练习的完整代码可以在[https://packt.live/2D6re26](https://packt.live/2D6re26)找到。\n\n1.  首先，我们来写`Person`课。很简单:\n\n    ```cpp\n    class Person\n    {\n    public:\n        Person(string name, int age)\n        {\n            m_name = name;\n            m_age = age;\n        }\n        string getName() const { return m_name; }\n        int getAge() const { return m_age; }\n    private:\n        string m_name;\n        int m_age;\n    };\n    ```\n\n2.  现在，创建一个自定义比较器。这里，我们将使用一个函子，它是一个*对象*，可以像*函数*一样使用。函子将被用来比较集合中的每个元素，当它被添加时，决定顺序。比较器获取两个元素并返回一个`bool`:T1\n3.  现在我们可以创建一些`Person`对象，将它们添加到集合中，然后打印出来。下面是完整的源代码:\n\n    ```cpp\n    #include <iostream>\n    #include <string>\n    #include <set>\n    using namespace std;\n    class Person \n    {\n    public:\n        Person(string name, int age) \n        {\n            m_name = name;\n            m_age = age;\n        }\n        string getName() const \n        {\n            return m_name;\n        }\n        int getAge() const {\n        return m_age;\n        }\n    private:\n        string m_name;\n        int m_age;\n    };\n    struct customComparator \n        {\n            bool operator()(const Person & a,\n            const Person & b) const {\n            return (a.getAge() > b.getAge());\n        }\n    };\n    int main() \n    {\n        set < Person, customComparator > personSet;\n        Person a(\"bob\", 35);\n        Person b(\"bob\", 25);\n        personSet.insert(a);\n        personSet.insert(b);\n\n        for (auto person: personSet) \n        {\n            cout << person.getAge() << endl;\n        }\n    }\n    ```\n\n4.  运行完整的代码。您应该获得以下输出:\n\n![Figure 12.12: Output for the custom comparator for a set ](img/C14195_12_12.jpg)\n\n图 12.12:一组自定义比较器的输出\n\n## 操作\n\n集合还有一些额外的有用操作，以及获取大小的等效操作，以及插入和获取其他容器公开的迭代器:\n\n*   `count(const T& val);`:返回集合内匹配`val`的元素个数。记住一个集合的元素都是唯一的，所以这个函数最多只能返回 1。这对于查找集合中是否存在某个键很有用。\n*   `lower_bound(const T& val);`:返回一个迭代器到`val`，如果存在的话，或者返回一个基于比较对象的集合中肯定不会在`val`之前的值。\n*   `upper_bound(const T& val);`:返回一个迭代器到`val`，如果它存在的话，或者返回一个基于比较对象的集合中肯定会跟随`val`的值。\n*   `equal_range(const T& val);`:返回一个包含`val`的范围的上下界的对的迭代器。同样，由于集合中的元素是唯一的，如果迭代器存在的话，它将返回给`val`。\n\n在下面的练习中，我们将使用一个集合来获取多集合中唯一元素的数量。\n\n## 练习 79:使用集合获取多集合中唯一元素的数量\n\n因为集合不允许非唯一元素，但是多集合允许，所以我们可以通过在多集合中插入每个元素来使用集合来获取多集合中唯一元素的数量。如果我们试图添加集合中已经存在的元素，那么它将不会被添加。在尝试添加所有元素后，我们可以计算集合中包含多少元素。这将是唯一元素的数量:\n\n注意\n\n本练习的完整代码可以在[https://packt.live/2OqKyMs](https://packt.live/2OqKyMs)找到。\n\n1.  在一个新的`main`函数中，我们可以声明我们的集合和多集合:\n\n    ```cpp\n    #include <iostream>\n    #include <string>\n    #include <set>\n    #include <stdlib.h>\n    using namespace std;\n    int main()\n    {\n        set<int> intSet;\n        multiset<int> intMultiset;\n    ```\n\n2.  We can then add a bunch of random numbers into our multiset by looping and inserting into the multiset:\n\n    ```cpp\n        for(unsigned int i = 0; i < 100; i++)\n        {\n            intMultiset.insert(1 + rand() % 100);\n        }\n    ```\n\n    注意\n\n    前面的代码片段是一个简单的随机数生成示例。\n\n3.  我们现在可以迭代这个多集合，并尝试将每个元素添加到集合中:\n\n    ```cpp\n        for(auto i  : intMultiset)\n        {\n            intSet.insert(i);\n        }\n    ```\n\n4.  最后，我们可以使用`size`功能打印集合中的元素数量:\n\n    ```cpp\n        cout << \"there are \" << intSet.size()          << \" unique elements in the multiset\";\n    }\n    ```\n\n5.  下面是完整的代码:\n\n    ```cpp\n    #include <iostream>\n    #include <string>\n    #include <set>\n    #include <stdlib.h>\n    using namespace std;\n    int main() \n    {\n        set < int > intSet;\n        multiset < int > intMultiset;\n        for (unsigned int i = 0; i < 100; i++) \n        {\n            intMultiset.insert(1 + rand() % 100);\n        }\n        for (auto i: intMultiset) \n        {\n            intSet.insert(i);\n        }\n        cout << \"there are \" << intSet.size()          << \" unique elements in the multiset\";\n    }\n    ```\n\n6.  以下是我们程序的可能输出:\n\n![Figure 12.13: Final output of the exercise ](img/C14195_12_13.jpg)\n\n图 12.13:练习的最终输出\n\n# 队列/堆栈\n\n队列是一种具有先进先出行为的容器。相反，堆栈是具有后进先出行为的容器。当向队列中添加元素时，它们被添加到队列的末尾，元素将从前面弹出或偷看。当向堆栈中添加元素时，我们可以认为它们被添加到顶部，然后元素也会从顶部弹出或偷看。这就是先进先出和后进先出的区别。当需要以特定的顺序检索和移除元素时，这两个容器都非常有用；章末活动将利用一个队列来处理以定义的顺序遍历元素。\n\n## 施工人员\n\n构建队列时，我们还可以决定该队列的底层容器。这同样适用于堆栈。如果我们不为自己选择容器类型，那么默认情况下，将使用`std::deque`。\n\n队列和堆栈的模板声明使用以下语法:\n\n```cpp\ntemplate <class T, class Container = deque<T> > class queue;\ntemplate <class T, class Container = deque<T> > class stack;\n```\n\n我们可以看到两种类型的容器默认为`deque`。\n\n堆栈和队列的构造函数都很少，通常，我们希望使用这些类型的默认构造函数和一个可选的自定义容器对象:\n\n*   `queue();`:构建一个空队列\n*   `stack();`:构造一个空堆栈\n\n## 操作\n\n队列和堆栈都支持以下操作:\n\n*   `empty();`:返回容器是否为空\n*   `size();`:返回容器内的元素数量\n*   `push();`:插入元素\n*   `pop();`:移除元素\n\n队列专门公开后续函数:\n\n*   `front();`:队列中的下一个元素\n*   `back();`:队列中的最后一个元素\n\n堆栈显示`top()`功能，这是`queue`的`front()`功能的后进先出等效功能。\n\n在前一章中，您已经看到了队列和堆栈的实现。我们鼓励您在这里进一步尝试自己的实现。\n\n## 活动 12:将 RPG 战斗转换为使用标准库容器\n\n现在我们已经了解了标准库提供的不同容器，我们可以通过使用容器来改进之前的一个活动， **RPG 战斗**(包含在*第 11 章*、*模板*中)，而不是原始数组，也是一个队列来创建一个怪物的战书供我们战斗。这个活动将有助于展示我们能用相对少量的代码对容器做多少事情。使用标准的库容器，我们以前使用原始数组的所有函数都将变得更短，并且我们不需要实现复制构造函数和赋值操作符；标准库容器将自己处理复制。成功完成活动后，您应该会获得以下输出:\n\n![Figure 12.14: Final output of the activity ](img/C14195_12_14.jpg)\n\n图 12.14:活动的最终输出\n\n注意\n\n本次活动的完整代码可在[https://packt.live/2pCMg55](https://packt.live/2pCMg55)找到。\n\n以下是完成活动的步骤:\n\n1.  更改`Attack`、`Item`和`Character`类，使用字符串代替`char`数组。\n2.  删除任何现在不需要的复制构造函数、析构函数和赋值实现。\n3.  让`Character`类取`Attack`和`Item`的向量，而不是原始数组。\n4.  实现`attack`和`defend`功能，用矢量代替数组，更新显示功能，利用矢量。\n5.  在主功能中，实现一个保存不同`Character`类型的队列，供玩家对战。\n6.  Fight each monster in the queue until the queue is empty and display a `win` string. Also, allow the use of items and a default attack.\n\n    注意\n\n    这个活动的解决方案可以在第 567 页找到。\n\n# 总结\n\n在 C++ 中有很多我们可以利用的容器，每一个都有自己非常具体的用途。虽然这些细节很重要，但是意识到这些容器中的接口到底有多相似也很重要。这个通用接口帮助我们编写更少的代码，并且也快速理解如何使用我们以前可能没有使用过的容器。这非常强大，是编写坚实的 C++ 代码的基础。在本书的下一章，也是最后一章，我们将研究异常的处理。"
  },
  {
    "path": "docs/cpp-workshop/13.md",
    "content": "# 十三、C++ 中的异常处理\n\n概观\n\n本章介绍异常处理，这是 C++ 用于报告和恢复程序中意外事件的机制。到本章结束时，您将能够识别适合异常处理的事件类型；知道何时抛出异常，何时返回错误代码；使用异常处理编写健壮的代码；使用带有异常处理的 RAII 在意外事件后自动回收资源；并从意外事件中恢复并继续执行。\n\n# 简介\n\n前几章介绍了 C++ 控制流语句和变量声明。我们已经尝到了面向对象编程的滋味，并从动态变量中创建了数据结构。在本章中，我们将注意力转向 C++ 如何帮助开发人员处理程序中意外出错时出现的情况。\n\n用户输入的无效数字、等待响应的意外超时以及逻辑错误都是程序中事件的例子。这些事件中的一些，比如输入错误，可能会频繁或可预测地发生，因此必须预测和处理它们，否则程序将无法使用。其他的事件，比如超时，很少发生，也从来不会发生在程序和运行它的系统工作正常的时候。还有一些事件，比如逻辑错误，根本就不应该发生，但有时它们确实会发生。\n\n用户输入错误事件是预期事件。用特定的代码处理；可能会显示一个“用户输入错误”对话框，程序将返回等待再次输入。从用户输入错误中恢复的代码很可能在词汇上接近检测到错误的代码，因为它的操作强烈依赖于发生的特定事件。普通的控制流语句适用于处理预期事件。处理完预期事件后，代码可以继续正常执行，就像事件没有发生一样。\n\n逻辑错误是一个*意外*事件。不可能编写特定的代码来处理意外事件，因为这些事件实际上是意外的——它们不应该发生。\n\n无法编写特定代码来处理意外事件的另一个原因是，每个语句中都可能发生大量意外事件。每个函数调用都可能有逻辑错误、参数错误和运行时错误。如果您必须编写特定的代码来处理每个可能发生的事件，那么程序将是 99.9%的事件处理程序，什么也做不了。\n\n无法编写处理意外事件的特定代码的第三个原因是因为这些事件阻止了程序的前进。程序无法修复逻辑错误(因为它是意外的)，因此它无法通过逻辑错误测试。这使得程序处理意外事件的方法数量有限。\n\n程序可能会暂停，它可能会重试一段代表某种计算的代码，以查看意外事件是否消失，或者它可能会放弃包含意外事件的计算，并尝试做其他事情。这些处理动作相对通用。每个动作可能适合许多不同的意外事件。\n\nC++ 异常处理是为意外事件设计的，即响应以下事件:\n\n*   不经常和不可预测地发生\n*   阻止程序向前推进\n\n当然，您可以对*预期的*事件使用异常处理，但它不是这项工作的合适工具。您也可以使用异常处理从函数返回，但是向同事解释会更慢更难。异常处理不适合这些工作，就像锤子不适合拧螺丝一样。用锤子敲螺丝是*可能的*，但是这样做是困难且低效的。\n\n# 应对突发事件\n\n意外事件可能在程序中的任何地方被检测到，但它们通常在与操作系统和外部世界交互的库函数中被检测到。对这些函数的调用通常嵌套在函数调用堆栈的许多层中。\n\n意外事件会阻止程序当前计算的前进。当遇到意外事件时，程序可以选择突然停止，但是如果它想做除了停止之外的任何事情(包括简单地保存工作和打印消息)，它必须放弃当前的计算，并返回到启动新计算的更高级代码。正是在这个更高级别的代码中，程序可以决定执行是可以继续还是必须停止。\n\n这有两种可能发生的方式。传统上，检测到意外事件的函数可以停止它正在做的事情，手动清理它正在使用的任何资源，并向它的调用方返回一个错误代码。调用者反过来清理并将错误代码返回给调用者。错误代码像桶旅一样一步一步地沿着调用链向上传递，直到它到达能够响应它的代码，如下图所示:\n\n![Figure 13.1: Visualizing the step-by-step return of error codes ](img/C14195_13_01.jpg)\n\n图 13.1:可视化错误代码的逐步返回\n\n错误代码的逐步返回充满了风险。如果函数没有捕获所有被调用函数的返回代码，它可能会尝试继续，而不是将错误代码传递给它的调用方，如下图所示:\n\n![Figure: 13.2: Intermediate-level function dropping an error code ](img/C14195_13_02.jpg)\n\n图:13.2:中级函数删除错误代码\n\n试图在意外事件后继续执行通常会导致越来越严重的意外事件级联，直到操作系统强制停止程序。如果一个函数不删除动态变量、关闭打开的文件句柄和释放其他资源，这些资源就会泄漏，导致程序或操作系统最终变得不稳定并崩溃。\n\n将执行返回到高级代码的另一种方法是使用 C++ 异常处理。异常处理有三个部分。一个`throw`语句“抛出”一个异常，表明一个意外事件的发生。C++ 运行时系统“解绕”函数调用堆栈，调用每个局部变量的析构函数，而不返回对包含局部变量的函数的控制。然后一个`try/catch`块“捕获”异常，结束展开过程并允许继续执行。\n\n![Figure 13.3: Visualizing throwing and catching of exceptions ](img/C14195_13_03.jpg)\n\n图 13.3:可视化异常的抛出和捕获\n\n抛出的异常不能像返回的错误代码一样被忽略。异常要么被`try/catch`块捕获，要么 C++ 运行时系统终止程序。\n\n当 C++ 运行时系统在处理抛出的异常时展开堆栈，它调用所有局部变量的析构函数。封装在智能指针或 C++ 类中的资源被删除，因此不会泄漏。开发人员不必编写复杂的控制流代码来处理正常执行情况和意外错误情况，就像他们在逐步返回错误代码时必须做的那样。这些特性使得异常处理成为处理意外事件的更好方法。\n\n## 抛出异常\n\n一个`throw`语句抛出一个异常，向 C++ 运行时系统发出发生了意外事件的信号。`throw`语句由`throw`关键字和任意类型的表达式组成。引发的异常与表达式的类型相同。C++ 提供了一个异常类型库，这些异常类型是从`std::exception`派生出来的类实例，但是一个程序并不局限于抛出这些或者其他任何类实例；一个程序可以抛出一个`int`或者一个`char*`或者任何其他想要的类型。以下是一些`throw`语句的例子:\n\n*   抛出`std::exception`类型的异常:\n\n    ```cpp\n    throw std::exception;\n    ```\n\n*   抛出`std::logic_error`类型的异常，这是一个从`std::exception`派生的类。该异常有一个描述特定异常的可选文本字符串:\n\n    ```cpp\n    throw std::logic_error(\"This should not be executed\");\n    ```\n\n*   抛出`std::runtime_error`类型的异常，这是一个从`std::exception`派生的类。异常有一个整数错误代码和一个可选的文本字符串，该字符串进一步描述了特定的异常。整数代码是操作系统特有的错误:\n\n    ```cpp\n    throw std::runtime_error(LastError(), \"in OpenFile()\");\n    ```\n\n*   抛出 Linux `errno`伪变量作为`int`类型的异常。整数值是特定于操作系统的错误代码:\n\n    ```cpp\n    throw errno;\n    ```\n\n*   抛出`char const*`类型的异常。字符串的内容描述了异常:\n\n    ```cpp\n    throw \"i before e except after c\";\n    ```\n\n开发人员将使用的大多数标准异常要么是`std::logic_error`及其派生词，要么是`std::runtime_error`及其派生词，尤其是`std::system_error`。其余的标准异常由 C++ 标准库函数抛出。标准的意图从来都不清楚为什么一个例外是`logic_error`，另一个是`runtime_error`，还有一个两者都不是。这根本不是 C++ 设计得更好的部分之一。\n\n在内存不足的情况下，使用标准异常是有问题的，因为大多数标准异常在构造时可能会分配动态变量。标准异常的`what`参数没有定义的含义。只是插入到`std::exception`的`what()`成员函数返回的字符串中的一点文字。\n\n## 未捕获的异常\n\n异常由`throw`语句抛出，并被`try/catch`块中的`catch`子句捕获。我们将在后面研究捕捉异常。\n\n如果抛出的异常没有被`try/catch`块捕获，C++ 运行时系统将终止程序。抛出异常比调用`exit()`或`abort()`终止程序执行要好，因为它记录了一个意外事件已经发生。抛出异常还允许程序在以后通过捕获异常并决定是终止程序还是继续来改进。\n\n## 练习 80:抛出未捕获的异常\n\n在本练习中，我们将看到当我们抛出一个未被`try/catch`块的`catch`子句捕获的异常时会发生什么:\n\n注意\n\n练习的完整代码可以在这里找到:[https://packt.live/37tOlS4](https://packt.live/37tOlS4)。\n\n1.  在`main()`功能的框架中键入。看起来是这样的:\n\n    ```cpp\n    #include <iostream>\n    using namespace std;\n    int main()\n    {\n        return 0;\n    }\n    ```\n\n2.  在`main()`内，插入`throw`语句。这是由`throw`关键字后跟任意类型的表达式组成的。异常通常是从 C++ 标准库`std::exception`类派生的类实例，但是任何类型的表达式都可以。您可以抛出一个整数，如错误号，甚至是描述异常的空终止文本字符串:\n\n    ```cpp\n       throw \"An exception of some type\";\n    ```\n\n3.  完成的程序如下所示:\n\n    ```cpp\n    #include <iostream>\n    using namespace std;\n    int main()\n    {\n        throw \"An exception of some type\";\n        return 0;\n    }\n    ```\n\n4.  Run the program. While the precise message printed varies by operating system and compiler, the output from the tutorialspoint online compiler looks like this:\n\n    ![Figure 13.4: Output of the program in exercise 80 ](img/C14195_13_04.jpg)\n\n    图 13.4:练习 80 中程序的输出\n\n    这里发生了什么？当抛出异常而未被捕获时，C++ 运行时系统调用标准库`terminate()`函数。`terminate()`不返回；而是导致程序退出，向操作系统发出异常终止的信号。在 Linux 上，这种异常终止会转储一个核心文件进行调试。\n\n5.  在`using namespace std;`之后，添加一个名为`deeply_nested()`的`int`函数。功能骨架如下图:\n\n    ```cpp\n    int deeply_nested()\n    {\n        return 0;\n    }\n    ```\n\n6.  添加代码抛出`int`值`123`。然后输出`\"in deeply_nested after throw\"`。完成的功能如下:\n\n    ```cpp\n    int deeply_nested()\n    {\n        throw 123;\n        cout << \"in deeply_nested() after throw\" << endl;\n        return 0;\n    }\n    ```\n\n7.  在`deeply_nested()`之后，添加另一个名为`intermediate()`的`int`函数。它的骨架是这样的:\n\n    ```cpp\n    int intermediate()\n    {\n        return 0;\n    }\n    ```\n\n8.  给`deeply_nested()`增加一个通话。不要忘记在名为`rc`的`int`变量中捕获`deeply_nested()`的返回值(代表返回代码)。输出消息`\"in intermediate(), after deeply_nested()\"`。然后，从`deeply_nested()`返回`rc`中的返回码。完整的功能如下:\n\n    ```cpp\n    int intermediate()\n    {\n        int rc = deeply_nested();\n        cout << \"in intermediate(), after deeply_nested()\";\n        return rc;\n    }\n    ```\n\n9.  在`main()`中，将`throw`语句替换为对`intermediate()`的调用。不要从`intermediate()` :\n\n    ```cpp\n        intermediate();\n    ```\n\n    获取退货代码\n10.  更新后的程序如下:\n\n    ```cpp\n    #include <iostream>\n    using namespace std;\n    int deeply_nested()\n    {\n        throw 123;\n        cout << \"in deeply_nested() after throw\" << endl;\n        return 0;\n    }\n    int intermediate()\n    {\n        int rc = deeply_nested();\n        cout << \"in intermediate(), after deeply_nested()\";\n        return rc;\n    }\n    int main()\n    {\n        intermediate();\n        return 0;\n    }\n    ```\n\n11.  运行程序。虽然打印的确切消息因操作系统和编译器而异，但 tutorialspoint 在线编译器的输出如下所示:\n\n![Figure 13.5: Output of the updated program in exercise 80 ](img/C14195_13_05.jpg)\n\n图 13.5:练习 80 中更新程序的输出\n\n这里发生了什么？`main()`叫`intermediate()`，也就是叫`deeply_nested()`。这是为了表示程序的正常行为，当抛出异常时，程序通常执行嵌套多层的函数。当执行`throw`语句时，`deeply_nested()`中代码的执行停止。C++ 运行时系统开始寻找一个`try/catch`块来捕获异常。`deeply_nested()`没有，`intermediate()`没有，`main()`也没有，所以 C++ 运行时系统调用`terminate()`。输出由操作系统产生，并且可能因操作系统或编译器版本而异。注意`throw`后面的输出语句都没有执行，表示`throw`语句后功能停止执行。\n\n注意\n\n这个程序需要注意的另一件事是返回代码。`deeply_nested()`返回一个代码，可能描述一个错误。`intermediate()`也是。但是`main()`没有捕获`intermediate()`的返回代码，所以如果没有抛出异常，错误信息就会丢失。如果异常没有被捕获，它们会可靠地停止程序，并且它们会可靠地将错误信息从`throw`语句传输到`catch`子句的位置。\n\n## 捕捉异常\n\n捕捉异常的代码称为`try/catch`块。它由两部分组成；`try`块由`try`关键字组成，后跟一个用花括号括起来的语句列表，是由`try/catch`块控制的语句块。在`try`区块之后是一个或多个`catch`条款。每个`catch`子句由`catch`关键字组成，后跟一个带圆括号的变量声明，声明要被`catch`子句捕获的异常类型。最后一个`catch`子句可能是`catch (...)`，它捕捉以前没有捕捉到的每个异常。\n\n这里有一个样本`try/catch`块:\n\n```cpp\ntry\n{\n    auto p = make_unique<char[]>(100);\n}\ncatch (std::exception& e)\n{\n    cout << e.what() << endl;\n}\n```\n\n`try`块包含单个语句:\n\n```cpp\n    auto p = make_unique<char[]>(100);\n```\n\n如果没有足够的内存来创建动态变量，该语句可能会引发类型为`std::bad_alloc`的异常。执行`try`块中的语句。如果没有异常发生，执行将继续执行最后一个`catch`子句之后的语句。\n\n样本`try/catch`块中有一个`catch`子句，用于处理任何类型源自`std::exception`的异常，包括`std::bad_alloc`。如果出现异常，该异常的类型将与第一个`catch`子句的类型进行比较。如果异常的类型可以构造或初始化`catch`子句的变量，实际函数参数构造形式参数的方式，则`catch`子句开始执行。示例`catch`子句中的可执行语句使用`std::exception::what()`成员函数打印出异常的描述:\n\n```cpp\n    cout << e.what() << endl;\n```\n\n执行`catch`子句中的复合语句后，该异常被视为已处理。在最后一个`catch`条款之后的位置继续执行。\n\n如果抛出异常的类型不能构造第一个`catch`子句中的变量，则比较下一个`catch`子句。`catch`条款的顺序很重要。C++ 运行时系统从上到下匹配针对`catch`子句抛出的异常。第一个`catch`条款抓住了例外，在这个条款中可以构造例外。`catch`条款的顺序应该从最具体到最一般，最一般的`catch (...)`在列表的最后。\n\n如果没有`catch`子句与抛出异常的类型匹配，则在包围`try/catch`块的范围(由花括号分隔)内继续搜索`try/catch`块。\n\n## 练习 81:试/抓积木\n\n这个练习展示了`try/catch`方块的基本形式。`try/catch`块的目的是处理一些或所有抛出的异常，以决定程序的执行是否可以继续:\n\n注意\n\n练习的完整代码可以在这里找到:[https://packt.live/2sa34l1](https://packt.live/2sa34l1)。\n\n1.  进入`main()`功能的骨架。代码如下:\n\n    ```cpp\n    #include <iostream>\n    using namespace std;\n    int main()\n    {\n        return 0;\n    }\n    ```\n\n2.  输入上一练习中的功能`deeply_nested()`。代码如下:\n\n    ```cpp\n    int deeply_nested()\n    {\n        throw 123;\n        return 0;\n    }\n    ```\n\n3.  在`main()`内部，创建一个`try/catch`块。在`try`街区内，呼叫`deeply_nested()`。添加一个`catch`块，使用 catch 子句`catch(...)`捕获所有异常。在`catch`块内，输出`\"in catch ...\"`弦。代码如下:\n\n    ```cpp\n        try\n        {\n            deeply_nested();\n        }\n        catch (...)\n        {\n            cout << \"in catch ...\" << endl;\n        }\n    ```\n\n4.  `try/catch`闭塞后，输出`\"in main(), after try/catch\"`串。代码如下:\n\n    ```cpp\n        cout << \"in main(), after try/catch\" << endl;\n    ```\n\n5.  完整的程序如下:\n\n    ```cpp\n    #include <iostream>\n    using namespace std;\n    int deeply_nested()\n    {\n        throw 123;\n        return 0;\n    }\n    int main()\n    {\n        try\n        {\n            deeply_nested();\n        }\n        catch (...)\n        {\n            cout << \"in catch ...\" << endl;\n        }\n        cout << \"in main(), after try/catch\" << endl;\n        return 0;\n    }\n    ```\n\n6.  运行程序。它的输出如下所示:\n\n![Figure 13.6: Output of the program in exercise 81 ](img/C14195_13_06.jpg)\n\n图 13.6:练习 81 中程序的输出\n\n请注意，程序没有调用`terminate()`，也没有以操作系统的异常终止消息结束。`main()`叫做`deeply_nested()`。`deeply_nested()`中抛出的异常被`catch(...)`条款捕获，该条款打印了消息`\"in catch ..`。”，所以正常的程序执行继续在 main()的`catch`子句后并打印出消息`\"in main(), after try/catch\"`。\n\n不要删除程序。下一个练习将在此基础上进行。\n\nC++ 标准库的某些 C++ 语句和某些函数会引发异常。C++ 语句和函数抛出的所有异常都是从`std::exception`派生的类的实例，可以在`<exception>`头中找到。源自`std::exception`的异常的一个有用特性是，它们提供了成员函数，您可以调用这些函数来获取关于异常的更多信息。要访问这些成员函数，一个`catch`子句必须将捕获的异常分配给一个变量。\n\n捕捉类型为`std::exception`的异常并将对该异常的引用放入名为`e`的变量中的`catch`子句是:\n\n```cpp\n    catch (std::exception& e)\n```\n\n您可能已经通过使用`catch`语句的值捕捉到了相同的异常:\n\n```cpp\n    catch (std::exception e)\n```\n\n但这需要复制例外。抛出的异常存在于为此目的保留的内存中，因此不需要动态变量来保存异常。这很重要，因为 C++ 抛出的一个异常是`bad_alloc`异常，它发生在内存无法分配的时候。复制异常可能需要创建一个动态变量，如果内存不足，会导致程序崩溃。\n\n## 练习 82:c++ 抛出的异常\n\n并非每个异常都是由开发人员自己的代码中的`throw`语句引发的。C++ 语句和标准库函数会引发一些异常。在本练习中，我们将捕捉由 C++ 标准库函数引发的异常:\n\n注意\n\n练习的完整代码可以在这里找到:[https://packt.live/2KNtmQy](https://packt.live/2KNtmQy)。\n\n1.  从上一个练习的完整程序开始。如果需要重新输入，看起来是这样的:\n\n    ```cpp\n    #include <iostream>\n    using namespace std;\n    int deeply_nested() \n    {\n        throw 123;\n        return 0;\n    }\n    int main() \n    {\n        try \n        {\n            deeply_nested();\n        }\n        catch (...) \n        {\n            cout << \"in catch ...\" << endl;\n        }\n        cout << \"in main(), after try/catch\" << endl;\n        return 0;\n    }\n    ```\n\n2.  在表头`<iostream>`的`include`下方，为`<exception>`增加一个`include`，为`<string>`增加一个`include`。这是代码:\n\n    ```cpp\n    #include <exception>\n    #include <string>\n    ```\n\n3.  In `deeply_nested()`, replace the `throw` statement with the following statement:\n\n    ```cpp\n        string(\"xyzzy\").at(100);\n    ```\n\n    这个语句的作用是创建一个标准的库字符串，将其初始化为一个五个字母的单词，然后请求字符串的第 100 个字符。显然这是不可能的，所以`at()`成员函数抛出异常。\n\n    到目前为止，程序如下所示:\n\n    ```cpp\n    #include <iostream>\n    #include <exception>\n    #include <string>\n    using namespace std;\n    int deeply_nested() \n    {\n        string(\"xyzzy\").at(100);\n        return 0;\n    }\n    int main() \n    {\n        try \n        {\n            deeply_nested();\n        } \n        catch (...) \n        {\n            cout << \"in catch ...\" << endl;\n        }\n        cout << \"in main(), after try/catch\" << endl;\n        return 0;\n    }\n    ```\n\n4.  Run the program. Its output is the same as that of the previous exercise:\n\n    ![Figure 13.7: Output for the program in exercise 82 ](img/C14195_13_07.jpg)\n\n    图 13.7:练习 82 中程序的输出\n\n5.  在`main()`中的`catch(...)`条款之前，增加一个新的`catch`条款。抓住参考变量`e`中的`exception`类型(记住，这是`std::exception`，因为`using namespace std`语句)。在`catch`子句中，输出`e.what()`返回的值，该值打印描述异常的文本字符串。新的`catch`条款如下:\n\n    ```cpp\n        catch (exception& e)\n        {\n             cout << \"caught \" << e.what() << endl;\n        }\n    ```\n\n6.  更新后的程序如下:\n\n    ```cpp\n    #include <iostream>\n    #include <exception>\n    #include <string>\n    using namespace std;\n    int deeply_nested()\n    {\n        string(\"xyzzy\").at(100);\n        return 0;\n    }\n    int main()\n    {\n        try\n        {\n            deeply_nested();\n        }\n        catch (exception& e)\n        {\n            cout << \"caught \" << e.what() << endl;\n        }\n        catch (...)\n        {\n            cout << \"in catch ...\" << endl;\n        }\n        cout << \"in main(), after try/catch\" << endl;\n        return 0;\n    }\n    ```\n\n7.  Run the program. It produces the following output:\n\n    ![Figure 13.8: Output of the revised program in exercise 82 ](img/C14195_13_08.jpg)\n\n    图 13.8:练习 82 中修订程序的输出\n\n    `main()`称为`deeply_nested()`。在`deeply_nested()`内部，语句`string(\"xyzzy\").at(100);`抛出了一个源自`std::exception`类型的异常。什么类型是例外？它是类`out_of_range`的一个实例，类`logic_error`派生自类`exception`。该例外首先与`std::exception&`匹配。对派生类的引用可以初始化对基类的引用，所以这个`catch`子句被执行，产生第一行输出。\n\n    捕捉到异常后，执行继续执行`try/catch`块后面的行，如预期的那样，该行打印第二个输出行。没有执行`catch(...)`子句，因为 C++ 已经将抛出的异常与之前的`catch`子句进行了匹配，并执行了`catch`子句的语句。\n\n8.  如果程序在`catch (exception& e)`子句之前包含了`logic_error`或`out_of_range`的`catch`子句，那么`catch`子句就会被执行。但是如果`catch (exception& e)`条款首先出现，它就会被执行。请记住，`catch`条款是按顺序检查的。执行与异常匹配的第一个`catch`子句，而不是最佳匹配的子句。\n\n## 展开堆叠\n\n展开栈是销毁栈上每个作用域的局部变量，寻找一个`try/catch`块的过程。\n\n下面是 C++ 运行时系统在处理抛出的异常时所做的事情。展开堆栈从最里面的动态嵌套范围开始。这是`throw`语句周围的范围(用花括号分隔)。之所以称之为动态嵌套作用域，是因为在程序执行过程中，当一个函数调用另一个函数时，函数堆栈上的函数作用域堆栈会动态变化。\n\n对于函数激活堆栈上的每个作用域，C++ 运行时系统执行以下步骤，只要有更多的作用域，就重复这些步骤:\n\n*   当前范围内的所有局部变量都将被销毁。C++ 精确地跟踪每个作用域中需要销毁的变量。如果正在构造的类引发异常，则只会销毁已经构造的基类和成员变量。如果一个块中只有一些变量被构造，那么只有那些变量被销毁。\n*   如果当前范围是一个函数范围，函数的激活记录将从堆栈中弹出，C++ 运行时系统将处理下一个封闭范围。\n*   如果当前范围不是`try`块，C++ 继续处理下一个封闭范围。\n*   否则，当前范围是一个`try`块。C++ 运行时系统依次将每个`catch`子句与抛出异常的类型进行比较。如果抛出异常的类型可以构造成`catch`子句中的变量，`catch`子句变量的构造方式与函数形式参数相同，则执行`catch`子句。然后，紧接着最后一个`catch`块继续执行语句，该过程完成。\n*   如果抛出的异常不能被构造到任何`catch`子句中，C++ 将处理下一个封闭范围。\n*   如果没有(更多)作用域，则不捕获异常。C++ 运行时系统调用`terminate()`，然后将控制权返回给操作系统，表示异常终止状态。\n\n## 资源获取是初始化)和异常处理\n\n毫无疑问，C++ 异常的堆栈展开行为非常强大。C++ 标准库定义了许多类，它们获取资源，拥有这些资源，并在类实例被破坏时释放这些资源。这个习惯用法，即一个类拥有一个资源并在删除时释放它，被称为 RAII(资源获取就是初始化)。我们在第 8 章中看到的智能指针是删除自己拥有的动态变量的 RAII 类。在作用域中拥有资源的任何智能指针或其他 RAII 类实例都会在作用域退出之前释放这些资源，这样资源就不会泄漏。\n\n异常处理和 RAII 的结合将开发人员从编写删除所拥有资源的两条不同路径中解放出来:当执行成功时遵循一条路径，当意外事件发生时遵循第二条路径。开发人员只需要使用智能指针和 C++ 标准库的其他 RAII 类。C++ 关于在离开作用域时销毁对象的规则和 RAII 类的行为自动管理资源的释放，无需显式编码。\n\n下一个练习使用带有噪声类实例的代码来演示堆栈展开过程。\n\n## 练习 83:展开堆叠\n\n在本练习中，我们将创建一个调用函数来产生动态嵌套变量范围的程序。程序抛出一个异常来说明堆栈展开过程是如何发生的:\n\n注意\n\n练习的完整代码可以在这里找到:[https://packt.live/2pGj9xP](https://packt.live/2pGj9xP)。\n\n1.  进入骨架`main()`功能。代码如下:\n\n    ```cpp\n    #include <iostream>\n    using namespace std;\n    int main()\n    {\n        return 0;\n    }\n    ```\n\n2.  添加标题`<exception>`和`<memory>`库的`include`指令。这个程序抛出一个源自`std::exception`的异常，需要`<memory>` :\n\n    ```cpp\n    #include <exception>\n    #include <memory>\n    ```\n\n    中定义的智能指针\n3.  输入我们老朋友类的定义`noisy`。代码如下:\n\n    ```cpp\n    class noisy\n    {\n        char const* s_;\n    public:\n        noisy(char const* s) { cout << \"constructing \" << (s_ = s) << endl; }\n       ~noisy() { cout << \"destroying \" << s_ << endl; }\n    };\n    ```\n\n4.  输入`int`功能`deeply_nested()`。它的骨架是这样的:\n\n    ```cpp\n    int deeply_nested()\n    {\n        return 0;\n    }\n    ```\n\n5.  在`deeply_nested()`中，使用`make_unique()`创建一个指向动态`noisy`变量的智能指针。代码如下:\n\n    ```cpp\n        auto n = make_unique<noisy>(\"deeply_nested\");\n    ```\n\n6.  抛出一个逻辑错误。`logic_error`接受空终止的字符串构造函数参数。这可以是你喜欢的任何东西；试试`\"totally illogical\"` :\n\n    ```cpp\n        throw logic_error(\"totally illogical\");\n    ```\n\n7.  进入`int`功能`intermediate()`。它的骨架是这样的:\n\n    ```cpp\n    int intermediate()\n    {\n        return 0;\n    }\n    ```\n\n8.  创建类`noisy`的本地实例。它的论点可以是`\"intermediate\"`。类的析构函数`noisy`打印一条消息。它是在析构函数中显式释放资源的类的替身:\n\n    ```cpp\n        noisy n(\"intermediate\");\n    ```\n\n9.  给`deeply_nested()`增加一个通话。在`int`变量`rc`中捕获`deeply_nested()`的返回值。输出消息`\"after calling deeply_nested\"`。返回`rc` :\n\n    ```cpp\n        int rc = deeply_nested();\n        cout << \"after calling deeply_nested()\" << endl;\n        return rc;\n    ```\n\n10.  在功能`main()`中，添加一个`try/catch`块。它应该有一个用于类异常的`catch`条款。`try/catch`街区的骨架是这样的:\n\n    ```cpp\n        try\n        {\n        }\n        catch (exception& e)\n        {\n        }\n    ```\n\n11.  在`try`块中，使用构造函数参数`\"try in main\"` :\n\n    ```cpp\n            auto n = make_unique<noisy>(\"try in main\");\n    ```\n\n    构造一个指向动态`noisy`实例的智能指针\n12.  调用`intermediate()`并打印退货代码:\n\n    ```cpp\n            int rc = intermediate();\n            cout << \"intermediate() returned \" << rc << endl;\n    ```\n\n13.  在`catch`子句中，输出`e.what()`，这样我们就知道捕捉到了什么异常:\n\n    ```cpp\n            cout << \"in catch: exception: \" << e.what() << endl;\n    ```\n\n14.  `try/catch`块后，输出字符串`\"ending main()\"` :\n\n    ```cpp\n            cout << \"ending main\" << endl;\n    ```\n\n15.  完成的程序如下所示:\n\n    ```cpp\n    #include <iostream>\n    #include <exception>\n    #include <memory>\n    using namespace std;\n    class noisy\n    {\n        char const* s_;\n    public:\n        noisy(char const* s) { cout << \"constructing \" << (s_ = s) << endl; }\n       ~noisy() { cout << \"destroying \" << s_ << endl; }\n    };\n    int deeply_nested()\n    {\n        auto n = make_unique<noisy>(\"deeply_nested\");\n        throw logic_error(\"totally illogical\");\n        return 0;\n    }\n    int intermediate()\n    {\n        noisy n(\"intermediate\");\n        int rc = deeply_nested();\n        cout << \"after calling deeply_nested()\" << endl;\n        return rc;\n    }\n    int main()\n    {\n        try\n        {\n            auto n = make_unique<noisy>(\"try in main\");\n            int rc = intermediate();\n            cout << \"intermediate() returned \" << rc << endl;\n        }\n        catch (exception& e)\n        {\n            cout << \"in catch: exception: \" << e.what() << endl;\n        }\n        cout << \"ending main\" << endl;\n        return 0;\n    }\n    ```\n\n16.  Compile and run the program. Its output looks like this:\n\n    ![Figure 13.9: Output of the program in exercise 83 ](img/C14195_13_09.jpg)\n\n    图 13.9:练习 83 中程序的输出\n\n    `main()`中的`try`块构造了`noisy`的动态实例(输出的第一行)。`main()`称为`intermediate()`，代表多层函数调用。`intermediate()`构造了一个`noisy`的实例(第二行)。`intermediate()`称为`deeply_nested()`，构造了一个动态的`noisy`实例(第三行)。此时，函数调用堆栈帧如下所示:\n\n    ![Figure 13.10: The function call stack ](img/C14195_13_10.jpg)\n\n    图 13.10:函数调用堆栈\n\n    `deeply_nested()`抛出了一个异常。我们知道发生这种情况是因为`deeply_nested()`中的`noisy`实例被破坏了(第四行)，但是`intermediate()`中的输出语句没有被执行。`intermediate()`中没有`try/catch`区块，所以`intermediate()`中的`noisy`实例被破坏了(第五行)。`main()`中有一个`try/catch`块。`try/catch`块中`noisy`的动态实例被破坏(第六行)。异常的类型为`std::logic_error`，由`std::exception`派生而来。有`catch`条款为例外，故执行`catch`条款(第七行)。执行继续到`try/catch`块后的输出语句(第八行)。\n\n    注意`deeply_nested()`和`intermediate()`返回值并抛出异常。因为引发了异常，所以没有执行返回该值的代码。\n\n17.  在这个非常理想的自动退卷过程中，有一个不幸的“陷阱”。用`try`块的内容替换`main()`中的`try/catch`块，使`main()`看起来像这样:\n\n    ```cpp\n    int main()\n    {\n        auto n = make_unique<noisy>(\"try in main\");\n        int rc = intermediate();\n        cout << \"intermediate() returned \" << rc << endl;\n        cout << \"ending main\" << endl;\n        return 0;\n    }\n    ```\n\n18.  Compile and run the program again. If you see the same output, breathe a sigh of relief. However, you may see output similar to the following:\n\n    ![Figure 13.11: Output of the modified program in exercise 83 ](img/C14195_13_11.jpg)\n\n图 13.11:练习 83 中修改后程序的输出\n\n没有一个`noisy`实例被销毁。发生了什么事？事实证明，C++ 标准允许实现在未捕获到异常时不展开堆栈。\n\n注意\n\n每一个使用异常处理的程序都应该在`main()`的内容周围放置至少一个最小的`try/catch`块，这样一个意外的异常会打开堆栈。\n\n在本章的总结活动中，您将探索程序如何捕捉异常，并在发生意外事件后选择继续还是终止程序执行。\n\n## 活动 13:处理异常\n\n想象你想写一个程序，一遍又一遍地做一些任意的事情，直到一些你无法控制的终止条件发生。您可以调用名为`do_something()`的`bool`函数来执行程序的操作。只要`do_something()`返回`true`你就继续，当`do_something()`最终返回`false`时结束程序。您的程序可能如下所示:\n\n```cpp\n#include <iostream>\nusing namespace std;\nint main()\n{\n    bool continue_flag;\n    do\n    {\n        continue_flag = do_something();\n    }\n    while (continue_flag == true);\n    return 0;\n}\n```\n\n好吧，那很简单。让我们提高赌注。\n\n假设你的程序所做的事情是监控一个 200 兆瓦核电反应堆的安全运行。你的`do_something()`功能现在叫做`reactor_safety_check()`。它读取传感器并设置控制器，以防止反应堆爆炸并辐射英国伦敦。这是一个非常重要的程序，无论如何都需要继续运行。该程序只有在感应到控制棒被一路推入且堆芯温度低于 100℃时才能停止运行，此时`reactor_safety_check()`返回`false`。\n\n作为首席主回路软件工程师，您已经了解到实施反应堆安全检查代码的团队选择在运行时错误上抛出`std::runtime_error`异常，例如*故障读取传感器*。你的电气工程师向你保证，这些错误只是暂时的小故障。即使程序的一次迭代报告了`runtime_error`异常，下一次通过时，错误也很可能不会出现。\n\n因为你心存疑虑，所以你在源代码中搜索`throw`语句，结果令你沮丧的是，发现有几个。你不知道这些其他例外意味着什么，但是如果不被抓住，它们带来的风险显然是严重的。你知道另一个叫做`SCRAM()`的功能，它一路推动控制棒，排出蒸汽，启动紧急给水泵，这是反应堆不受控制时你能做的一切。\n\n你妈妈和你姐姐住在伦敦，所以即使你不想承担这个责任，你也不敢辞职。由你来防止堆芯熔毁或更严重的事件，核反应堆工程师委婉地称之为“迅速临界快速拆卸”，这意味着一个小的热核爆炸。\n\n编写一个重复调用`bool`函数`reactor_safety_check()`的程序。继续`main`循环，处理`runtime_error`异常。通过调用`SCRAM()`并退出来处理其他异常。\n\n注意\n\n练习的完整代码可以在这里找到:[https://packt.live/33chUEq](https://packt.live/33chUEq)。\n\n以下是完成活动的一些步骤:\n\n1.  编写`reactor_safety_check()`的测试版本，偶尔抛出异常来测试你的代码。这里有一个写`reactor_safety_check()`的提示。如果创建一个名为`count`的静态`int`变量，并在每次调用`reactor_safety_check()`时递增`count`，则可以使用`count`来决定在`reactor_safety_check()`中做什么。例如，也许你想在每次调用`reactor_safety_check()`时抛出一个小故障异常。\n2.  你会想要捕捉所有可能的异常，而不仅仅是`std::runtime_error`，因为你不想在反应器还在运行的时候就终止循环。\n3.  You can assume that after you call `SCRAM()`, you don't have to monitor the reactor any longer because there's nothing else that can be done. This is typical of error recovery actions, which take place on a best-effort basis.\n\n    注意\n\n    这个活动的解决方案可以在第 574 页找到。\n\n# 总结\n\n向程序通知意外事件的传统方式是使用检测到事件的函数的错误返回代码。这种方式充满风险，因为开发人员并不总是记得检查返回代码。异常克服了这个风险，因为异常要么被捕获，要么终止程序。\n\nC++ 异常处理的特性旨在处理程序执行过程中的意外事件。\n\n抛出的异常展开堆栈，在展开时调用每个作用域中每个变量的析构函数。使用 RAII 习惯用法，拥有资源(如动态变量、打开的文件句柄、互斥体等)的类可以释放这些资源。因为资源是在堆栈展开时释放的，所以在捕获异常后继续程序执行是安全的。\n\n一个`try/catch`块可以捕捉异常。`catch`子句可以选择继续或停止程序执行。\n\n这是本书的结尾，*c++ Workshop*，但这只是你学习的开始。在前几章中，我们描述了 C++ 的控制流语句。如果您希望能够使用这些语句，您将需要练习编写程序，并在一个在线 C++ 编译器或您选择的 C++ IDE 上自行执行它们。这本书向你介绍了几个概念和相关练习。然而，只有通过反复练习，你才能充分欣赏和利用你所学到的技能。\n\n我们看了 C++ 中变量的基本类型，但也有变化。例如，`int`类型有`short int`、`long int`和`long long int`三个变种，加上所有这些的无符号变体。浮点类型有三种:`float`、`double`和`long double`。有数组和结构要尝试，还有带有成员函数的类。\n\n动态变量让你在内存中构建任意大的数据结构，只要你避开了动态变量的死罪。智能指针和 RAII 将在这方面帮助你。\n\n如果您以前的编程经验不包括对象，您可能需要几年时间才能适应面向对象编程。整本书都是关于这个主题的。\n\nC++ 有一个标准库，包含打包为模板函数和类的算法和数据结构。在这本入门书中，我们没有空间教授模板编程，但是一旦你对基础知识感到满意，这是一个非常值得学习的主题。\n\n您使用了 C++ 输出语句，但实际上只是触及了 C++ 输入/输出流接口的表面。它具有独特的功能和灵活性，因此值得进一步了解。\n\nC++ 异常处理是一个强大的工具，它被压缩成两个语句。这是值得掌握的，有很多我们无法告诉你。\n\n事实上，几乎每个 C++ 语句、声明、表达式和指令都有我们无法覆盖的皱纹。C++ 标准本身运行超过 1500 页。我们建议您在练习时查阅陈述，看看还有哪些额外的知识可以帮助您。这是即使是非常有经验的 C++ 开发人员都会做的事情，所以不要羞于不断提高自己的知识。\n\n要多久你才有一个熟练工的 C++ 知识？不到一周——即使有我们优秀的书。只靠自学，大多数人需要全职两年的练习才能达到自己舒服的程度。作者认为你完成这本书有一个很好的开端，但是继续练习。"
  },
  {
    "path": "docs/cpp-workshop/14.md",
    "content": "# 十四、附录\n\n## 关于\n\n包括这一部分是为了帮助学生完成书中的活动。它包括学生完成和实现本书目标的详细步骤。\n\n# 1.您的第一个 C++ 应用\n\n## 活动 1:编写自己的 C++ 应用\n\n**解决方案**:\n\n1.  使用`#defines`定义你的年龄段阈值。\n2.  Define a name for each group using `#defines`.\n\n    以下是步骤 1 和 2 所需的代码:\n\n    ```cpp\n    // Activity 1\\. \n    #include <iostream>\n    #include <string>\n    #define GROUP_1_THRESHOLD 12\n    #define GROUP_2_THRESHOLD 28\n    #define GROUP_1_NAME \"Group A\"\n    #define GROUP_2_NAME \"Group B\"\n    #define GROUP_3_NAME \"Group C\"\n    ```\n\n3.  输出一个文本字符串，询问用户的姓名，并在变量中捕获响应。\n4.  输出询问用户年龄的文本，并在变量中捕获响应。\n5.  编写一个函数，接受年龄作为参数，并返回适当的组名。\n6.  Output the user's name, and the group that they have been assigned to.\n\n    以下是执行步骤 3-6 所需的代码:\n\n    ```cpp\n    std::string GetGroup(int age);\n    int main()\n    {\n        std::string name = \"\";\n        int age = 0;\n        std::string group = \"\";\n        std::cout << \"Please enter your name: \";\n        getline(std::cin, name);\n        std::cout << \"And please enter your age: \";\n        std::cin >> age;\n        group = GetGroup(age);\n        std::cout << \"Welcome \"<< name << \". You are in \"               << group << \".\\n\";\n    }\n    std::string GetGroup(int age)\n    {\n        if (age <= GROUP_1_THRESHOLD)\n        {\n            return GROUP_1_NAME;\n        }\n        else if (age <= GROUP_2_THRESHOLD)\n        {\n            return GROUP_2_NAME;\n        }\n         else\n        {\n            return GROUP_3_NAME;\n        }\n    }\n    ```\n\n7.  Run the complete code. You will obtain the following output:\n\n    ![Figure 1.20: Our program asked for the user's name and age, and assigned them to the appropriate group ](img/C14195_01_20.jpg)\n\n图 1.20:我们的程序询问用户的姓名和年龄，并将他们分配到适当的组\n\n# 2.控制流\n\n## 活动 2:使用循环和条件语句创建猜数字游戏\n\n**解决方案**:\n\n1.  声明我们需要的所有变量。这包括`guessCount`、`minNumber`、`maxNumber`和`randomNumber` :\n\n    ```cpp\n    // Activity 2: Number guessing game.\n    #include <iostream>\n    #include <string>\n    int main()\n    {\n        // Declare variables.\n        int guessCount = 0;\n        int minNumber = 0;\n        int maxNumber = 0;\n        int randomNumber = 0;\n        std::string input = \"\";\n        bool bIsRunning = true;\n    ```\n\n2.  创建一个运行应用的主外部循环:\n\n    ```cpp\n        while (bIsRunning)\n        {\n        }\n    ```\n\n3.  Present the user with some introductory text (`\"Enter the number of guesses\"`) and get from them the following: a number of guesses, a minimum number, and a maximum number:\n\n    ```cpp\n        while (bIsRunning)\n        {\n            // Output instructions and get user inputs.\n            std::cout << \"***Number guessing game***\\n\";\n            std::cout << \"\\nEnter the number of guesses: \";\n            getline(std::cin, input);\n            guessCount = std::stoi(input);\n\n            std::cout << \"Enter the minimum number: \";\n            getline(std::cin, input);\n            minNumber = std::stoi(input);\n\n            std::cout <<\"Enter the maximum number: \";\n            getline(std::cin, input);\n            maxNumber = std::stoi(input);\n        }\n    ```\n\n    注意\n\n    我们在这里不做检查，以确保最大数量大于最小数量。这是为了代码简洁，但是在编写产品代码时，总是有必要对用户输入进行健全性检查。\n\n4.  Generate a random number within the range specified by the user:\n\n    ```cpp\n        while (bIsRunning)\n        {    \n            // Output instructions and get user inputs.\n            //[…]\n            // Generate random number within range.\n            srand((unsigned)time(0));\n            randomNumber = rand() % (maxNumber - minNumber + 1)         + minNumber;\n        }\n    ```\n\n    注意\n\n    我们在本章前面使用了同样的方法来生成随机数，因此返回到*练习 2.3* ，*将 if/else 链重构为开关/案例*，以便在必要时进行提醒。\n\n5.  创建一个循环，重复用户指定的猜测次数。\n6.  在`count`循环中，获取用户的猜测。\n7.  在`count`循环内，检查用户的猜测是否正确，或者过高/过低。我们可以在这里使用`break`在猜对数值后退出。\n8.  When the number has been found, or the user has run out of guesses, present them with the option to either continue or exit the application.\n\n    执行步骤 5-8 的代码如下:\n\n    ```cpp\n        while  (bIsRunning)\n        {\n            // Output instructions and get user inputs.\n            //[…]\n            // Generate random number within range.\n            //[…]\n            // Process user guesses.\n            for (int i = 0; i < guessCount; ++ i)\n            {\n                int guess = 0;\n                std::cout << \"\\nEnter your guess: \";\n                getline(std::cin, input);\n                guess = std::stoi(input);\n                if (guess == randomNumber)\n                {\n                    std::cout << \"Well done, you guessed the number!\\n\";\n                    break;\n                }\n                int guessesRemaining = guessCount - (i + 1);\n                std::cout << \"Your guess was too \"                       << (guess < randomNumber ? \"low. \" : \"high. \");\n                std::cout << \"You have \" << guessesRemaining                       << (guessesRemaining > 1 ? \" guesses\" : \"                       guess\") << \" remaining\";\n            }\n            std::cout << \"\\nEnter 0 to exit, or any number                   to play again: \";\n            getline(std::cin, input);\n            if (std::stoi(input) == 0)\n            {\n                bIsRunning = false;\n            }\n        }\n    ```\n\n9.  Run the complete code. You will obtain the following output:\n\n    ![Figure 2.18: Number-guessing game output ](img/C14195_02_171.jpg)\n\n图 2.18:猜数字游戏输出\n\n# 3.内置数据类型\n\n## 活动 3:报名申请\n\n**解决方案**:\n\n1.  首先包含应用需要的各种标题:\n\n    ```cpp\n    // Activity 3: SignUp Application.\n    #include <iostream>\n    #include <string>\n    #include <vector>\n    #include <stdexcept>\n    ```\n\n2.  接下来，定义将在系统中表示记录的类。这将是一个人，包含姓名和年龄。另外，声明一个这种类型的向量来存储这些记录。使用向量是因为它提供了不必预先声明数组大小的灵活性:\n\n    ```cpp\n    struct Person\n    {\n        int age = 0;\n        std::string name = \"\";\n    };\n    std::vector<Person> records;\n    ```\n\n3.  现在，您可以开始添加一些函数来添加和获取记录；首先，补充。记录由名字和年龄组成，所以编写一个函数，接受这两个作为参数，创建一个记录对象，并将其添加到我们的记录向量中。命名该功能`AddRecord` :\n\n    ```cpp\n    void AddRecord(std::string newName, int newAge)\n    {\n        Person newRecord;\n        newRecord.name = newName;\n        newRecord.age = newAge;\n        records.push_back(newRecord);\n        std::cout << \"\\nUser record added successfully.\\n\\n\";\n    };\n    ```\n\n4.  添加一个函数来获取记录。这个函数应该接受一个参数，一个用户标识，并返回该用户的记录。命名该功能`FetchRecord` :\n\n    ```cpp\n    Person FetchRecord(int userID)\n    {\n        return records.at(userID);\n    };\n    ```\n\n5.  进入`main`功能，启动应用主体。从一个外部`main`循环开始，就像你在上一章中使用的那样，并向用户输出一些选项。你会给他们三个选择:`Add Record`、`Fetch Record`、`Quit` :\n\n    ```cpp\n    int main()\n    {\n        std::cout << \"User SignUp Application\\n\" << std::endl;\n        bool bIsRunning = true;\n        while (bIsRunning)\n        {\n            std::cout << \"Please select an option:\\n\";\n            std::cout << \"1: Add Record\\n\";\n            std::cout << \"2: Fetch Record\\n\";\n            std::cout << \"3: Quit\\n\\n\";\n    ```\n\n6.  向用户展示这些选项，然后捕捉他们的输入:\n\n    ```cpp\n            std::cout << \"Enter option: \";\n            std::string inputString;\n            std::getline(std::cin, inputString);\n    ```\n\n7.  现在有三种可能的分支，这取决于用户输入，我们将用`switch`语句来处理。案例 1 是添加一条记录，为此，您将获得用户的姓名和年龄，然后调用我们的`AddRecord`功能:\n\n    ```cpp\n            // Determine user selection.\n            switch (std::stoi(inputString))\n            {\n                case 1:\n                {\n                    std::string name = \"\";\n                    int age = 0;\n                    std::cout << \"\\nAdd User. Please enter                           user name and age:\\n\";\n                    std::cout << \"Name: \";\n                    std::getline(std::cin, name);\n                    std::cout << \"Age: \";\n                    std::getline(std::cin, inputString);\n                    age = std::stoi(inputString);\n                    AddRecord(name, age);\n                }\n                break;\n    ```\n\n8.  下一种情况是用户想要获取记录。为此，你需要从用户那里得到一个`userID`，然后打电话给`FetchRecord`，输出其结果:\n\n    ```cpp\n                case 2:\n                {\n                    int userID = 0;\n                    std::cout << \"\\nPlease enter user ID:\\n\";\n                    std::cout << \"User ID: \";\n                    std::getline(std::cin, inputString);\n                    userID = std::stoi(inputString);\n                    Person person;\n                    try\n                    {\n                        person = FetchRecord(userID);\n                    }\n                    catch (const std::out_of_range& oor) \n                    {\n                        std::cout << \"\\nError: Invalid UserID.\\n\\n\";\n                    break;\n                    }\n                    std::cout << \"User Name: \" << person.name << \"\\n\";\n                    std::cout << \"User Age: \" << person.age << \"\\n\\n\";\n                }\n                break;\n    ```\n\n9.  下一种情况是用户想要退出应用。这个相当简单；你只需要退出我们的`main`循环:\n\n    ```cpp\n                case 3:\n                    bIsRunning = false;\n                break\n    ```\n\n10.  Finally, add a default case. This will handle invalid options entered by the user. All you'll do here is output an error message and send them back to the start of the application:\n\n    ```cpp\n                default:\n                    std::cout << \"\\n\\nError: Invalid option                           selection.\\n\\n\";\n                break;\n                    }\n                }\n            }\n    ```\n\n    有了所有这些，应用应该可以运行了。\n\n11.  运行完整的代码。您将获得以下输出:\n\n![Figure 3.27: Our application allows the user to add records and then recall them via an ID ](img/C14195_03_271.jpg)\n\n图 3.27:我们的应用允许用户添加记录，然后通过一个 ID 调用它们\n\n这个应用是迄今为止我们最复杂的，它汇集了我们到目前为止学到的所有东西；从函数，到控制流、类、范围、IO 等等。我们现在可以看到所有这些不同的元素如何结合在一起，让我们能够构建复杂的系统。这只是开始。我们只介绍了绝对的基础知识，我们已经可以看到如何将这些部分组合在一起来解决现实世界的问题。\n\n# 4.运算符\n\n## 活动 4:嘶嘶嗡嗡\n\n**解决方案**:\n\n1.  像往常一样，我们将从包含应用所需的头开始，并开始我们的主循环:\n\n    ```cpp\n    // Activity 4: Fizz Buzz.\n    #include <iostream>\n    int main()\n    {\n    ```\n\n2.  接下来，我们将定义我们的循环。我们要打印 100 个数字，所以需要迭代 100 次，从 1:\n\n    ```cpp\n        for (int i = 1; i <= 100; ++ i)\n        {\n    ```\n\n    开始\n3.  `Fizz Buzz`应用告诉我们，对于 3 的倍数，我们将打印`Fizz`，对于 5 的倍数，我们将打印`Buzz`。但是，一个数可以同时是 3 和 5 的倍数；例如，15 是两者的倍数，因此我们接下来将定义一个布尔值`multiple`，这将有助于我们跟踪这一点，给它一个初始值`false` :\n\n    ```cpp\n        bool multiple = false;\n    ```\n\n4.  接下来，我们可以检查我们当前的循环值`i`是否是 3 的倍数。如果是这样，我们将打印单词`Fizz`并将我们的多个布尔值设置为`true` :\n\n    ```cpp\n        if (i % 3 == 0)\n            {\n                std::cout << \"Fizz\";\n                multiple = true;\n            }\n    ```\n\n5.  然后我们可以对`Buzz`进行同样的操作，检查`i`是否是 5 的倍数。同样，我们将把`multiple`的布尔值设置为`true`，如果是这样的话:\n\n    ```cpp\n        if (i % 5 == 0)\n            {\n                std::cout << \"Buzz\";\n                multiple = true;\n            }\n    ```\n\n6.  现在我们已经检查了我们的数字是 3 的倍数还是 5 的倍数，并且有一个布尔值，如果是`true`，我们可以用它来确定我们是否打印正常的数字。如果我们的`multiple` `bool`仍然是`false`的话，那么我们知道我们需要打印正常的号码，`i` :\n\n    ```cpp\n        if (!multiple)\n            {\n                std::cout << i;\n            }\n    ```\n\n7.  最后，我们将做一点格式化。如果我们不在循环的最后一次迭代中，我们将打印一个逗号后跟一个空格。这将使我们的应用在打印时更加整洁:\n\n    ```cpp\n        if (i < 100)\n            {\n                std::cout << \", \";\n            }\n        }\n    }\n    ```\n\n8.  Let's run the application now and see it in action. We should see numbers leading up to 100\\. Multiples of 3 will be replaced by `Fizz`, multiples of 5 by `Buzz`, and multiples of both by `FizzBuzz`.\n\n    ![Figure 4.16: The Fizz Buzz application – a common coding test exercise ](img/C14195_04_16.jpg)\n\n图 4.16:Fizz Buzz 应用——一个常见的编码测试练习\n\n# 5.指针和引用\n\n## 活动 5:使用指针和引用来操作字符串数组\n\n**解决方案**:\n\n1.  进入骨架`main()`功能:\n\n    ```cpp\n    #include <iostream>\n    using namespace std;\n    int main()\n    {\n        return 0;\n    }\n    ```\n\n2.  Above `main()`, create an array of strings:\n\n    ```cpp\n    char const* array[26]\n    {    \"alpha\", \"bravo\", \"charlie\", \"delta\", \"echo\"   };\n    ```\n\n    数组长度必须为 26 个元素，否则程序可能会因某些有效参数而崩溃。\n\n3.  进入`printarray()`功能的骨架。定义参数。因为我们打印的是一个字符串数组，所以指针的类型是`char const**`。`count`的论点是`int&`。定义返回类型，在分配中指定为`int`:\n\n    ```cpp\n    int printarray(char const** begin, char const** end, int& count)\n    {\n        return 1;\n    }\n    ```\n\n4.  清除`count` :\n\n    ```cpp\n        count = 0;\n    ```\n\n5.  输入代码以检测参数中的错误:\n\n    ```cpp\n        if (begin == nullptr || end == nullptr || \n            begin > end || end-begin > 26)\n        {\n            return 0;\n        }\n    ```\n\n6.  Enter a loop to control printing:\n\n    ```cpp\n        for (count = 0; begin < end; ++ begin)\n        {\n            if (*begin != nullptr)\n            {\n                ++ count;\n                cout << *begin << endl;\n            }\n        }\n    ```\n\n    有几种方法可以做到这一点。一种自我记录的方法是使用`for`循环，因为`for`循环有初始条件、延续条件和增量。它帮助你记住像这样的任务所需的所有部分。由于`for`循环最初没有任何其他事情要做，将计数设置为零移到`for`语句的初始化槽中。\n\n7.  Inside `main()`, write some tests:\n\n    ```cpp\n        int count;\n        if (printarray(nullptr, nullptr, count) == 0 || count != 0)\n        {\n            cout << \"error in printarray() call 1\" << endl;\n        }\n        else\n        {\n            cout << \"count = \" << count << endl;\n        }\n    ```\n\n    所有其他测试看起来都差不多:\n\n    ```cpp\n        if (printarray(array, &array[4], count) == 0 || count != 4)\n        {\n            cout << \"error in printarray() call 2\" << endl;\n        }\n        else\n        {\n            cout << \"count = \" << count << endl;\n        }\n        if (printarray(&array[4], &array[3], count) == 0 || count != 0)\n        {\n            cout << \"error in printarray() call 3\" << endl;\n        }\n        else\n        {\n            cout << \"count = \" << count << endl;\n        }\n        if (printarray(&array[4], &array[10], count) == 0 || count != 1)\n        {\n            cout << \"error in printarray() call 4\" << endl;\n        }\n        else\n        {\n            cout << \"count = \" << count << endl;\n        }\n        if (printarray(&array[0], &array[100], count) == 0 || count != 0)\n        {\n            cout << \"error in printarray() call 5\" << endl;\n        }\n        else\n        {\n            cout << \"count = \" << count << endl;\n        }\n    ```\n\n8.  Run the program. The output looks like this:\n\n    ![Figure 5.13: Using Pointers and References to Manipulate an Array of Strings ](img/C14195_05_131.jpg)\n\n图 5.13:使用指针和引用来操作字符串数组\n\n# 6.动态变量\n\n## 活动 6:创建类实例的二分搜索法树\n\n**解决方案**:\n\n1.  Start with the skeleton `main()` function:\n\n    ```cpp\n    #include <iostream>\n    using namespace std;\n    int main()\n    {\n        return 0;\n    }\n    ```\n\n    添加`struct numeric_tree`的定义。它需要一个`int value_`成员，以及指向左右子树的指针，左右子树本身就是`numeric_tree`实例:\n\n    ```cpp\n    struct numeric_tree\n    {\n        int value_;\n        numeric_tree* left_;\n        numeric_tree* right_;\n    };\n    ```\n\n2.  这棵树的根叫做`root`。它指向`numeric_tree` :\n\n    ```cpp\n    numeric_tree* root = nullptr;\n    ```\n\n3.  `add()`函数将待添加的`int`值和指向`tree`的指针地址的指针作为参数，即指向指针:\n\n    ```cpp\n    void add(int v, numeric_tree** pp)\n    {\n    }\n    ```\n\n    的指针\n4.  对于`add()`函数，请理解添加的项目将始终被添加到等于`nullptr` :\n\n    ```cpp\n        *pp = new numeric_tree;\n         (*pp)->value_ = v;\n         (*pp)->left_ = (*pp)->right_ = nullptr;\n    ```\n\n    的子树中\n5.  函数`delete_tree()`最容易实现为递归函数:\n\n    ```cpp\n    void delete_tree(numeric_tree* item)\n    {\n        if (item == nullptr)\n        {\n            return;\n        }\n        else\n        {\n            delete_tree(item->left_);\n            delete_tree(item->right_);\n            cout << \"deleting \" << item->value_ << endl;\n            delete item;\n        }\n    }\n    ```\n\n6.  `find()`函数将一个要添加的`int`值和一个指向`numeric_tree`的指针地址的指针作为参数，也就是指向指针的指针。`find()`返回指针对指针。`find()`可以递归或迭代实现。递归版本如下:\n\n    ```cpp\n    numeric_tree** find(int v, numeric_tree** pp)\n    {\n    }\n    ```\n\n7.  `find()`函数使用二分搜索法树的递归描述。如果`pp`指向的变量是`nullptr`，那么`find()`已经定位了插入点，返回:\n\n    ```cpp\n        if (*pp == nullptr)\n        {\n            return pp;\n        }\n    ```\n\n8.  如果`v`参数小于当前项目的`value_`成员，则`find()`在左子树中递归。否则，它沿着右子树向下递归:\n\n    ```cpp\n        else if (v < (*pp)->value_)\n        {\n            return find(v, &((*pp)->left_));\n        }\n        else\n        {\n            return find(v, &((*pp)->right_));\n        }\n    ```\n\n9.  完成的`find()`功能如下:\n\n    ```cpp\n    numeric_tree** find(int v, numeric_tree** pp)\n    {\n        if (*pp == nullptr)\n        {\n            return pp;\n        }\n        else if (v < (*pp)->value_)\n        {\n            return find(v, &((*pp)->left_));\n        }\n        else\n        {\n            return find(v, &((*pp)->right_));\n        }\n    }\n    ```\n\n10.  The `print()` function was previously described. It is best implemented recursively; `print()` looks like this:\n\n    ```cpp\n    void print(numeric_tree* item)\n    {\n        if (item == nullptr)\n        {\n            return;\n        }\n        else\n        {\n            print(item->left_);\n            cout << item->value_ << \" \";\n            print(item->right_);\n        }\n    }\n    ```\n\n    使用二叉查找树的递归定义，如果指针是`nullptr`，则没有要打印的内容。否则，打印左边的子树(其中值较低)，然后打印当前项目的值，然后打印右边的子树(其中值较大)。\n\n11.  在`main()`中，一次可以添加一个项目，但是我选择使用`for`循环从一组`int`值中插入每个项目来自动化这个过程。`for`循环为每个值调用`add()`:\n\n    ```cpp\n        int insert_order[] { 4, 2, 1, 3, 6, 5 };\n        for (int i = 0; i < 6; ++ i)\n        {\n            int v = insert_order[i];\n            add(v, find(v, &root));\n        }\n    ```\n\n12.  It's appropriate to print the newly constructed tree. As you might expect, it looks like this:\n\n    ```cpp\n        print(root);\n        cout << endl;\n    ```\n\n    注意`print()`并不输出`endl`，所以这要在之后进行。如果你想隐藏这个细节，你可以把`print()`用一个名字包装在另一个函数中，比如`print_tree()`。\n\n13.  树是一种动态数据结构。完成后，必须将其删除。`delete_tree()`功能是这样做的:\n\n    ```cpp\n        delete_tree(root);\n    ```\n\n14.  The output of the program depends on your implementation choices. However, the output of the model program is as follows:\n\n    ![Figure 6.18: Output for creating binary search trees of class instances ](img/C14195_06_18.jpg)\n\n图 6.18:创建类实例的二分搜索法树的输出\n\n# 7.动态变量的所有权和寿命\n\n## 活动 7:使用动态变量存储一本书的单词\n\n**解决方案**:\n\n1.  从骨架`main()`程序开始。可能是这样的:\n\n    ```cpp\n    #include <iostream>\n    #include <memory>\n    using namespace std;\n    int main()\n    {\n        return 0;\n    }\n    ```\n\n2.  Define the `word` class:\n\n    ```cpp\n    class word\n    {\n        friend class line;\n        unique_ptr<char[]> ptr_;\n        int letters_;\n        int spaces_;\n        word* next_;\n    public:\n        word(char const* srcp, int l, int spaces);\n        void to_string(char* dstp);\n        int size();\n    };// end word\n    ```\n\n    有一个保存单词字母的`unique_ptr<>`到`char`数组，以及字母和空格的计数。最后，因为一行中的单词将是一个链表，所以有一个下一个指针。\n\n    构造函数复制单词串以及字母和空格的数量。`word`的析构函数是由编译器构建的。太聪明了。`to_string()`将单词复制到`char`缓冲区。在程序的其他地方，有些东西会调整`char`缓冲区的大小，但是为了测试，你可以只使用`char buf[100]`。`size()`返回单词中的字符数加上空格数。要确定一行的大小，请遍历该行中单词的链表，并将所有单词的大小相加。\n\n    定义`line`类:\n\n    ```cpp\n    class line\n    {\n        friend class page;\n        word* head_;\n        line* next_;\n    public:\n        line(char const* str);\n        ~line();\n        void append(word* w);\n        void to_string(char* dstp);\n        int size();\n    };// end line\n    ```\n\n    这包含单词列表的头节点和下一个指针，因为书是一个链接的行列表。`line`类的结构与`word`类相同。构造函数将字符串转换为单词列表。析构函数删除单词列表，因为第行有一个指向列表的指针。`to_string()`将单词列表转换为缓冲区中以空结尾的字符串。`size()`产生该行的字符数。\n\n    使课程尽可能相似有助于你记住你必须做什么。`line`有一个附加功能，`append()`，在`line`的词表末尾增加一个新词。\n\n3.  类`page`包含链表行的头节点。析构函数就像`line`的析构函数一样。现在，`append()`就跟`line`的`append()`功能一样。构造函数是空的，因为书是从外部构建的。`print()`在`cout`上出书:\n\n    ```cpp\n    class page\n    {\n        line* head_;\n    public:\n        page();\n       ~page();\n        void append(line* lp);\n        void print();\n    };// end page\n    ```\n\n4.  Let's look next at the contents of `main()`. The range-based `for` loop fetches the strings that comprise the book one at a time. As the lines are processed, they are each output, to give something to compare the reconstructed output against.\n\n    为什么要在行周围打印单引号(`'\\''`字符)？这样做是为了让您可以看到前导和尾随空格被正确打印。下一行创建一个`line`对象的`unique_ptr<>`实例。字符串指针被传递给构造函数，构造函数构建构成该行的单词。\n\n    下一行将`line`实例追加到页面。循环结束后，程序在输出中放一个空行来分隔这本书的两个副本。最后一行调用`page::print()`，打印出书的所有行:\n\n    ```cpp\n        page pg;\n        for (auto* p : book)\n        {\n            cout << '\\'' << p << '\\'' << endl;\n            auto l = make_unique<line>(p);\n            pg.append(l.release());\n        }\n        cout << endl;\n        pg.print();\n    ```\n\n5.  The implementation of the `word` class looks like this:\n\n    ```cpp\n    word::word(char const* srcp, int l, int spaces)\n        : ptr_(make_unique<char[]>(l+1)),\n        letters_(l),\n        spaces_(spaces)\n    {\n        char* dstp;\n        for(dstp = ptr_.get(); l > 0; --l)\n        {\n            *dstp++ = *srcp++ ;\n        }\n        *dstp = '\\0';\n    }\n    ```\n\n    构造函数初始化列表包括将一个`unique_ptr<>`变成一个`char`数组，该数组足够大以容纳单词的非空格字符。构造器主体是一个简单的循环，用于将字符从`srcp`复制到`ptr_`指向的缓冲区中。请注意，数组中有`l + 1`字符的空间，其中必须包含空终止符。在`for`循环中，`dstp`在循环外声明，因为它需要是活动的，以设置尾部空终止。如果在`for`语句中声明了`dstp`，它将超出`for`循环的右括号范围。\n\n6.  `word::to_string()`将单词的字符(后跟任何尾随空格)复制到`dstp`指向的缓冲区中。结尾增加了无效终止:\n\n    ```cpp\n    void word::to_string(char* dstp)\n    {\n        char* srcp = ptr_.get();\n        for (int letters = letters_; letters > 0; --letters)\n        {\n            *dstp++ = *srcp++ ;\n        }\n        for (int spaces = spaces_; spaces > 0; --spaces)\n        {\n            *dstp++ = ' ';\n        }\n        *dstp = '\\0';\n    }\n    ```\n\n7.  `size()`返回单词构造时保存的字母数加上空格数:\n\n    ```cpp\n    int word::size()\n    {\n        return letters_ + spaces_;\n    }\n    ```\n\n8.  `line`类的构造函数通过`str`输入字符串单步执行三个指针。`bp`是单词开头的指针。`ewp` ( **字尾指针**)从 bp 向前步进，直到第一个非字字符。`esp` ( **空格结束指针**)从`ewp`步进到第一个非空格字符。然后，创建一个新单词并将其附加到当前行。最后，`bp`前进到`esp`，循环重复:\n\n    ```cpp\n    line::line(char const* str)\n        : head_(nullptr), \n        next_(nullptr)\n    {\n        char const* bp; // pointer to beginning\n        char const* ewp;// pointer to end of word\n        char const* esp;// pointer to end of spaces\n        for (bp = str; *bp != '\\0'; bp = esp)\n        {\n            for (ewp = bp; *ewp != '\\0' && *ewp != ' '; ++ ewp)\n            {\n                // empty\n            }\n            for (esp = ewp; *esp != '\\0' && *esp == ' '; ++ esp)\n            {\n                // empty\n            }\n            append(new word(bp, ewp-bp, esp-ewp));\n        }\n    }\n    ```\n\n9.  `line`的析构函数很简单。`head_`拥有`word`实例列表。每个`word`从列表中删除，然后删除:\n\n    ```cpp\n    line::~line()\n    {\n        while (head_ != nullptr)\n        {\n            auto wp = head_;\n            head_ = head_->next_;\n            delete wp;\n        }\n    }\n    ```\n\n10.  `append()`类似于我们之前看到的链表的`append()`函数。它使用指针对指针的习惯用法来指向需要更新的指针:\n\n    ```cpp\n    void line::append(word* w)\n    {\n        word** wpp = &head_;\n        while((*wpp) != nullptr)\n        {\n            wpp = &((*wpp)->next_);\n        }\n        *wpp = w;\n    }\n    ```\n\n11.  `line::to_string()`使用`word::to_string()`将每个单词的文本放到`dstp`指向的缓冲区中:\n\n    ```cpp\n    void line::to_string(char* dstp)\n    {\n        for (word* wp = head_; wp != nullptr; wp = wp->next_)\n        {\n            wp->to_string(dstp);\n            dstp = dstp + wp->size();\n        }\n        *dstp = '\\0';\n    }\n    ```\n\n12.  `line::size()`遍历单词列表，将每个单词的大小相加。它为无效终止增加 1:\n\n    ```cpp\n    int line::size()\n    {\n        int size = 1;// for null terminator\n        for (word* wp = head_; wp != nullptr; wp = wp->next_)\n        {\n            size = size + wp->size();\n        }\n        return size;\n    }\n    ```\n\n13.  `page`的构造函数为空。这有一个初始化列表，将行列表的头节点设置为`nullptr` :\n\n    ```cpp\n    page::page():head_(nullptr) \n    {\n        // empty\n    }\n    ```\n\n14.  `page`的析构器和`line`的形态完全一样:\n\n    ```cpp\n    page::~page()\n    {\n        while (head_ != nullptr)\n        {\n            auto lp = head_;\n            head_ = head_->next_;\n            delete lp;\n        }\n    }\n    ```\n\n15.  `page::append()`与`line::append()`相同:\n\n    ```cpp\n    void page::append(line* lp)\n    {\n        line** lpp = &head_;\n        while((*lpp) != nullptr)\n        {\n            lpp = &((*lpp)->next_);\n        }\n        *lpp = lp;\n    }\n    ```\n\n16.  `print()`走`line`名单。对于每个`line`，`print()`创建一个动态缓冲区，其大小可容纳该`line`上单词的所有文本，然后要求`line::to_string()`填写`buffer`。最后`buffer`的内容打印在控制台上:\n\n    ```cpp\n    void page::print()\n    {\n        for (line* lp = head_; lp != nullptr; lp = lp->next_)\n        {\n            auto buffer = make_unique<char[]>(lp->size());\n            lp->to_string(buffer.get());\n            cout << '\\'' << buffer.get() << '\\'' << endl;\n        }\n    char const* book[] \n    {\n        \"What a piece of work is man,\",\n        \"  How noble in reason, how infinite in faculty,\",\n        \"In form and moving how express and admirable,\",\n        \"  In action how like an Angel, In apprehension how like a god.\",\n        \"The beauty of the world.    The paragon of animals.\",\n    };\n    ```\n\n17.  Compile and run the program if you haven't done so already. Its output looks like this:\n\n    ![Figure 7.12: Storing the words of a book using dynamic variables ](img/C14195_07_12.jpg)\n\n图 7.12:使用动态变量存储一本书的单词\n\n# 8.类和结构\n\n## 活动 8:创建视频剪辑类\n\n**解决方案**:\n\n1.  创建`VideoClip`课程大纲:\n\n    ```cpp\n    1\\. Create the VideoClip class outline:\n    #include <iostream> \n    #include <string> \n    using namespace std; \n    class VideoClip \n    {\n    public: \n    };\n\n    int main() \n    {\n        return 0; \n    }\n    ```\n\n2.  为视频长度和视频名称创建成员变量:\n\n    ```cpp\n    #include <iostream> \n    #include <string> \n    using namespace std; \n    class VideoClip \n    {\n    public: \n    float m_videoLength; \n    string m_videoName; \n    }; \n    int main() \n    {\n    return 0; \n    }\n    ```\n\n3.  编写一个默认构造函数，将视频长度和名称初始化为默认值:\n\n    ```cpp\n    #include <iostream>\n    #include <string>\n    using namespace std;\n    class VideoClip\n    {\n    public:\n        VideoClip()\n        {\n            m_videoLength = 0;\n            m_videoName = \"NOT SET\";\n        }\n        float m_videoLength;\n        string m_videoName;\n    };\n    int main()\n    {\n        return 0;\n    }\n    ```\n\n4.  编写一个参数化构造函数，将视频长度和名称设置为传入的参数:\n\n    ```cpp\n    #include <iostream>\n    #include <string>\n    using namespace std;\n    class VideoClip\n    {\n    public:\n        VideoClip()\n        {\n            m_videoLength = 0;\n            m_videoName = \"NOT SET\";\n        }\n        VideoClip(float videoLength, string videoName)\n        {\n            m_videoLength = videoLength;\n            m_videoName = videoName;\n        }\n        float m_videoLength;\n        string m_videoName;\n    };\n    int main()\n    {\n        return 0;\n    }\n    ```\n\n5.  创建一个数据`char`数组和数据大小成员变量，并在两个构造函数中初始化它们:\n\n    ```cpp\n    #include <iostream>\n    #include <string>\n    #include <cstring> \n    using namespace std;\n    class VideoClip\n    {\n    public:\n        VideoClip()\n        {\n            m_videoLength = 0;\n            m_videoName = \"NOT SET\";\n            m_dataLength = 0;\n            m_data = 0;\n        }\n        VideoClip(float videoLength, string videoName, const char* data)\n        {\n            m_videoLength = videoLength;\n            m_videoName = videoName;\n            m_dataLength= strlen(data);\n            m_data = new char[m_dataLength + 1];\n            strcpy(m_data, data); \n        }\n        float m_videoLength;\n        string m_videoName;\n        int m_dataLength;\n        char* m_data;\n    };\n    int main()\n    {\n        return 0;\n    }\n    ```\n\n6.  创建正确处理数据数组复制的复制构造函数:\n\n    ```cpp\n        VideoClip(const VideoClip& vc) \n        {\n            m_videoLength = vc.m_videoLength; \n            m_videoName = vc.m_videoName; \n            m_dataLength = vc.m_dataLength;\n            m_data = new char[m_dataLength + 1]; \n            strcpy(m_data, vc.m_data); \n        }\n        float m_videoLength;\n        string m_videoName;\n        int m_dataLength;\n        char* m_data;\n    };\n    int main()\n    {\n        return 0;\n    }\n    ```\n\n7.  创建一个复制赋值运算符重载，以正确处理数据数组的复制:\n\n    ```cpp\n        VideoClip& operator=(const VideoClip& rhs) \n        { \n            if(this != &rhs) \n            {\n                m_videoLength = rhs.m_videoLength; \n                m_videoName = rhs.m_videoName; \n                m_dataLength = rhs.m_dataLength;\n                char* newData = new char[m_dataLength]; \n                strcpy(newData, rhs.m_data); \n                delete[] m_data; \n                m_data = newData; \n            } \n            return *this; \n        } \n        float m_videoLength;\n        string m_videoName;\n\n        int m_dataLength;\n        char* m_data;\n    };\n    int main()\n    {\n        return 0;\n    }\n    ```\n\n8.  写一个删除分配的数据数组的析构函数:\n\n    ```cpp\n        ~VideoClip()\n        {\n            delete[] m_data; \n        }\n        float m_videoLength;\n        string m_videoName;\n        int m_dataLength;\n        char* m_data;\n    };\n    int main()\n    {\n        return 0;\n    }\n    ```\n\n9.  更新`main`功能，创建三个不同的`videoClip`实例，并输出它们的值:\n\n    ```cpp\n    int main()\n    {\n        VideoClip vc1(10.0f, \"Halloween (2019)\",                   \"dfhdhfidghirhgkhrfkghfkg\");\n        VideoClip vc2(20.0f, \"Halloween (1978)\", \"jkghdfjkhgjhgfjdfg\");\n        VideoClip vc3(50.0f, \"The Shining\", \"kotriothgrngirgr\");\n        cout << vc1.m_videoLength << \" \" << vc1.m_videoName << \" \"          << vc1.m_data << endl;\n        cout << vc2.m_videoLength << \" \" << vc2.m_videoName << \" \"          << vc2.m_data << endl;\n        cout << vc3.m_videoLength << \" \" << vc3.m_videoName << \" \"          << vc3.m_data << endl;\n        return 0;\n    }\n    ```\n\n10.  Test the copy constructor and copy assignment operators within the `main` function by initializing a video clip using an existing instance and also initializing an instance of a video clip with its constructor and then later assigning it to another existing instance:\n\n    ```cpp\n    int main()\n    {\n        VideoClip vc1(10.0f, \"Halloween (2019)\",                   \"dfhdhfidghirhgkhrfkghfkg\");\n        VideoClip vc2(20.0f, \"Halloween (1978)\", \"jkghdfjkhgjhgfjdfg\");\n        VideoClip vc3(50.0f, \"The Shining\", \"kotriothgrngirgr\");\n        cout << vc1.m_videoLength << \" \" << vc1.m_videoName << \" \"          << vc1.m_data << endl;\n        cout << vc2.m_videoLength << \" \" << vc2.m_videoName << \" \"          << vc2.m_data << endl;\n        cout << vc3.m_videoLength << \" \" << vc3.m_videoName << \" \"          << vc3.m_data << endl;\n        VideoClip vc4 = vc1;\n        vc2 = vc4;\n        cout << vc1.m_videoLength << \" \" << vc1.m_videoName << \" \"          << vc1.m_data << endl;\n        cout << vc2.m_videoLength << \" \" << vc2.m_videoName << \" \"          << vc2.m_data << endl;\n        cout << vc3.m_videoLength << \" \" << vc3.m_videoName << \" \"          << vc3.m_data << endl;\n        cout << vc4.m_videoLength << \" \" << vc4.m_videoName << \" \"          << vc4.m_data << endl;\n        return 0;\n    }\n    ```\n\n    当您运行完整的代码时，您将获得以下输出:\n\n![Figure 8.12: A possible output from the VideoClip class ](img/C14195_08_12.jpg)\n\n图 8.12:视频剪辑类的可能输出\n\n# 9.面向对象原则\n\n## 活动 9:一个基本的 RPG 战斗系统\n\n**解决方案**:\n\n1.  为角色、攻击和项目创建类，每个类中有一个`name`变量，可以在构造函数中设置:\n\n    ```cpp\n    #include <iostream> \n    #include <cstring> \n    using namespace std; \n    class Attack \n    {\n    public: \n        Attack(const char* name) \n        { \n            m_name = new char[strlen(name) + 1]; \n            strcpy(m_name, name); \n        } \n       ~Attack() \n        { \n            delete[] m_name; \n        } \n    private: \n        char* m_name; \n    };\n    class Item \n    {\n    public: \n        Item(const char* name) \n        { \n            m_name = new char[strlen(name) + 1]; \n            strcpy(m_name, name); \n         } \n       ~Item() \n        { \n            delete[] m_name; \n        } \n    private: \n        char* m_name; \n    }; \n    class Character \n    {\n    public: \n        Character(const char* name) \n        { \n            m_name = new char[strlen(name) + 1]; \n            strcpy(m_name, name); \n        } \n       ~Character() \n        { \n            delete[] m_name; \n        } \n    private: \n        char* m_name; \n    }; \n    int main() \n    {\n        return 0; \n    }\n    ```\n\n2.  给攻击一个攻击统计变量(`attackStat`)和物品一个治疗统计变量(`healStat`)。添加适当的吸气剂、沉降剂和施工添加剂:\n\n    ```cpp\n    class Attack \n    {\n    public: \n        Attack(const char* name, int attackStat) \n        { \n            m_name = new char[strlen(name) + 1]; \n            strcpy(m_name, name); \n            m_attackStat = attackStat; \n        } \n\n       ~Attack() \n        { \n            delete[] m_name; \n        } \n    int getAttackStat() const { return m_attackStat; } \n        char* getName() const { return m_name; } \n    private: \n        char* m_name; \n    int m_attackStat; \n    }; \n    class Item \n    {\n    public: \n        Item(const char* name, int healStat) \n        { \n            m_name = new char[strlen(name) + 1]; \n            strcpy(m_name, name); \n    m_healStat = healStat; \n        } \n       ~Item() \n        {\n            delete[] m_name; \n        } \n        int getHealStat() const { return m_healStat; } \n        char* getName() const { return m_name; } \n    private: \n        char* m_name; \n        int m_healStat; \n    }; \n    class Character \n    {\n    public: \n        Character(const char* name) \n        { \n            m_name = new char[strlen(name) + 1]; \n            strcpy(m_name, name); \n        } \n       ~Character() \n        { \n            delete[] m_name; \n        } \n    private: \n        char* m_name; \n    };\n    ```\n\n3.  让角色接受其构造函数中的一系列攻击和项目，并存储它们以便在需要使用时按名称查找:\n\n    ```cpp\n    class Character\n    {\n    public:\n        Character(const char* name, Attack* attacks, Item* items)\n        {\n            m_name = new char[strlen(name) + 1];\n            strcpy(m_name, name);\n            m_attacksLength = sizeof(attacks)/sizeof(&attacks[0]);\n            m_itemsLength = sizeof(items)/sizeof(&items[0]);\n            m_attacks = new Attack*[m_attacksLength];\n            m_items = new Item*[m_itemsLength];\n            int i = 0;\n            for(i = 0; i < m_attacksLength; i++)\n            {\n                Attack* attack = new Attack(attacks[i]);\n                m_attacks[0] = attack;\n            }\n            for(i = 0; i < m_itemsLength; i++)\n            {\n                Item* item = new Item(items[i]);\n                m_items[0] = item;\n            }\n        }\n       ~Character()\n        {\n            delete[] m_name;\n        }\n    private:\n        char* m_name;\n        Attack** m_attacks;\n        Item** m_items;\n        int m_attacksLength;\n        int m_itemsLength;\n    };\n    ```\n\n4.  在`Character`类中添加一个生命值变量，并创建函数来攻击其他角色，使用物品，并对攻击做出反应:\n\n    ```cpp\n    class Character \n    {\n    public: \n        Character(const char* name, Attack* attacks, Item* items) \n        { \n    m_health = 100; \n            m_name = new char[strlen(name) + 1]; \n            strcpy(m_name, name); \n            m_attacksLength = sizeof(attacks)/sizeof(&attacks[0]); \n            m_itemsLength = sizeof(items)/sizeof(&items[0]); \n            m_attacks = new Attack*[m_attacksLength]; \n            m_items = new Item*[m_itemsLength]; \n            int i = 0; \n            for(i = 0; i < m_attacksLength; i++) \n            {\n                Attack* attack = new Attack(attacks[i]); \n                m_attacks[0] = attack; \n            }\n            for(i = 0; i < m_itemsLength; i++) \n            { \n                Item* item = new Item(items[i]); \n                m_items[0] = item; \n            } \n        } \n        ~Character() \n        { \n            delete[] m_name; \n        } \n\n    void DoAttack(string moveName, Character& other) \n    { \n    other.DoDefend(GetAttackAmount(moveName)); \n        }\n    void UseItem(string itemName) \n        {\n    m_health += GetItemValue(itemName); \n        }\n    private: \n    void DoDefend(int attackValue) \n        {\n    m_health -= attackValue; \n        }\n    int GetAttackAmount(string attackName) \n        {\n    for(int i = 0; i < m_attacksLength; i++) \n            {\n    if(m_attacks[i]->getName() == attackName) \n                {\n    return m_attacks[i]->getAttackStat(); \n                }\n            }\n    return 0; \n        }\n    int GetItemValue(string itemName) \n        {\n    for(int i = 0; i < m_itemsLength; i++) \n            {\n    if(m_items[i]->getName() == itemName) \n    { \n    return m_items[i]->getHealStat(); \n    } \n            }\n    return 0; \n        }\n        char* m_name; \n        Attack** m_attacks; \n        Item** m_items; \n        int m_health; \n        int m_attacksLength; \n        int m_itemsLength; \n    };\n    ```\n\n5.  创建名为`strengthMultiplier`和`defenceMultiplier`的成员变量。这些应该会影响角色的攻防统计:\n\n    ```cpp\n    class Character \n    {\n    public: \n        Character(const char* name, int strengthMultiplier, int\n        defenceMultiplier, Attack* attacks, Item* items) \n        { \n            m_health = 100; \n\n            m_name = new char[strlen(name) + 1]; \n            strcpy(m_name, name); \n\n    m_strengthMultiplier = strengthMultiplier; \n    m_defenceMultiplier = defenceMultiplier; \n            m_attacksLength = sizeof(attacks)/sizeof(&attacks[0]); \n            m_itemsLength = sizeof(items)/sizeof(&items[0]); \n            m_attacks = new Attack*[m_attacksLength]; \n            m_items = new Item*[m_itemsLength]; \n            int i = 0; \n            for(i = 0; i < m_attacksLength; i++) \n            { \n                Attack* attack = new Attack(attacks[i]); \n                m_attacks[0] = attack; \n            } \n\n            for(i = 0; i < m_itemsLength; i++) \n            { \n                Item* item = new Item(items[i]); \n                m_items[0] = item; \n            } \n        } \n        ~Character() \n        { \n            delete[] m_name; \n            delete[] m_attacks; \n            delete[] m_items; \n        } \n        const char* getName() { return m_name; } \n\n        void DoAttack(string moveName, Character& other) \n        {\n            cout << m_name << \" attacks \" << other.getName()              << \" with \" << moveName << endl;\n    other.DoDefend(GetAttackAmount(moveName) *                        m_strengthMultiplier); \n        } \n        void UseItem(string itemName) \n        { \n            m_health += GetItemValue(itemName); \n        } \n    private: \n        void DoDefend(int attackValue) \n        {\n    int damage = attackValue / m_defenceMultiplier; \n    m_health -= damage; \n            cout << m_name << \" takes \" << damage << \" damage\" << endl; \n        } \n        int GetAttackAmount(string attackName) \n        {\n            for(int i = 0; i < m_attacksLength; i++) \n            {\n                if(m_attacks[i]->getName() == attackName) \n                {\n                    return m_attacks[i]->getAttackStat(); \n                }\n            }\n            return 0; \n        }\n        int GetItemValue(string itemName) \n        {\n            for(int i = 0; i < m_itemsLength; i++) \n            {\n                if(m_items[i]->getName() == itemName) \n                {\n                    return m_items[i]->getHealStat(); \n                }\n            }\n            return 0; \n        }\n        char* m_name; \n        Attack** m_attacks; \n        Item** m_items; \n        int m_health; \n    int m_strengthMultiplier; \n    int m_defenceMultiplier; \n        int m_attacksLength; \n        int m_itemsLength; \n    };\n    ```\n\n6.  在`Character`类中创建一个函数，将一个角色的名字和其他统计信息打印到控制台:\n\n    ```cpp\n    void Display()\n        {\n            cout << m_name << endl;\n            cout << \"Health = \" << m_health << endl;\n            cout << \"Strength Multiplier = \" << m_strengthMultiplier              << endl;\n            cout << \"Defence Multiplier = \" << m_defenceMultiplier              << endl;\n            cout << \"Attacks:\" << endl;\n            for(int i = 0; i < m_attacksLength; i++)\n            cout << m_attacks[i]->getName() << \" : \"              << m_attacks[igetAttackStat() << endl;\n            cout << \"Items:\" << endl;\n            for(int i = 0; i < m_itemsLength; i++)\n            cout << m_items[i]->getName() << \" : \"              << m_items[i]->getHealStat() << endl;\n        }\n    ```\n\n7.  用几个不同的字符测试主函数中的所有内容:\n\n    ```cpp\n    int main()\n    {\n        Attack billAttacks[] = { {\"Sword To The Face\", 20} };\n        Item billItems[] = { {\"Old Grog\", 20} };\n        Attack dragonAttacks[] = {{\"Flame Breath\", 50}};\n        Item dragonItems[] = {{\"Scale Oil\", 20}};\n        Character bill(\"Bill\", 10, 5, billAttacks, billItems);\n        bill.Display();\n        Character dragon(\"Dragon\", 10, 5, dragonAttacks, dragonItems);\n        dragon.Display();\n        bill.Display();\n        bill.DoAttack(\"Sword To The Face\", dragon);\n        dragon.Display();\n        dragon.DoAttack(\"Flame Breath\", bill);\n        bill.Display();\n        return 0;\n    }\n    ```\n\n8.  Run the complete code. You should obtain the following output:\n\n    ![Figure 9.11: RPG combat system ](img/C14195_09_111.jpg)\n\n图 9.11: RPG 战斗系统\n\n# 10.高级面向对象原则\n\n## 活动 10:百科全书应用\n\n**解决方案**:\n\n1.  首先包含应用所需的所有文件:\n\n    ```cpp\n    // Activity 10: Encyclopedia Application.\n    #include <iostream>\n    #include <string>\n    #include <vector>\n    ```\n\n2.  创建一个结构，`AnimalInfo`，它可以存储名称、产地、预期寿命和重量:\n\n    ```cpp\n    struct AnimalInfo\n    {\n        std::string name = \"\";\n        std::string origin = \"\";\n        int lifeExpectancy = 0;\n        float weight = 0;\n    };\n    ```\n\n3.  创建一个函数，以简洁的格式打印数据。命名为`PrintAnimalInfo` :\n\n    ```cpp\n    void PrintAnimalInfo(AnimalInfo info)\n    {\n        std::cout << \"Name: \" << info.name << std::endl;\n        std::cout << \"Origin: \" << info.origin << std::endl;\n        std::cout << \"Life Expectancy: \" << info.lifeExpectancy               << std::endl;\n        std::cout << \"Weight: \" << info.weight << std::endl;\n    }\n    ```\n\n4.  现在，为我们的动物创建基类。命名为`Animal`。它应该提供一个类型为`AnimalInfo`的成员变量，以及一个返回它的函数。请务必使用适当的访问修饰符:\n\n    ```cpp\n    class Animal\n    {\n    public:\n        AnimalInfo GetAnimalInfo() const { return animalInfo; };\n\n    protected:\n        AnimalInfo animalInfo;\n    };\n    ```\n\n5.  接下来，创建第一个派生类`Lion`。该类将从`Animal`继承，为最终类，并在其构造函数中填写`AnimalInfo`成员:\n\n    ```cpp\n    class Lion final : public Animal\n    {\n    public:\n        Lion()\n        {   \n            animalInfo.name = \"Lion\";\n            animalInfo.origin = \"Africa\";\n            animalInfo.lifeExpectancy = 12;\n            animalInfo.weight = 190;\n        }\n    };\n    ```\n\n6.  接下来，创建第二个派生类`Tiger`。填写相同数据:\n\n    ```cpp\n    class Tiger final : public Animal\n    {\n    public:\n        Tiger()\n        {\n            animalInfo.name = \"Tiger\";\n            animalInfo.origin = \"Africa\";\n            animalInfo.lifeExpectancy = 17;\n            animalInfo.weight = 220;\n        }\n    };\n    ```\n\n7.  创建最终的派生类`Bear`，同时填写`AnimalInfo`成员:\n\n    ```cpp\n    class Bear final : public Animal\n    {\n    public:\n        Bear()\n        {\n            animalInfo.name = \"Bear\";\n            animalInfo.origin = \"Eurasia\";\n            animalInfo.lifeExpectancy = 22;\n            animalInfo.weight = 270;\n        }\n    };\n    ```\n\n8.  定义`main`功能。声明一个指向基本`Animal`类型的指针向量，并添加每个动物衍生类型:\n\n    ```cpp\n    int main()\n    {\n        std::vector<Animal*> animals;\n        animals.push_back(new Lion());\n        animals.push_back(new Tiger());\n        animals.push_back(new Bear());\n    ```\n\n9.  输出应用标题:\n\n    ```cpp\n        std::cout << \"**Animal Encyclopedia**\\n\";\n    ```\n\n10.  为应用创建`main`外部循环，并向用户输出一条消息，提示他们选择一个索引:\n\n    ```cpp\n        bool bIsRunning = true;\n        while (bIsRunning)\n        {\n            std::cout << \"\\nSelect animal for more information\\n\\n\";\n    ```\n\n11.  向用户输出可能的选择。为此使用`for`循环，每个选项都应该包括一个索引和动物的名称。还包括一个选项，用户可以通过输入`-1` :\n\n    ```cpp\n            for (size_t i = 0; i < animals.size(); ++ i)\n            {\n                std::cout << i << \") \" << animals[i]->GetAnimalInfo().name                       << std::endl;\n            }\n            std::cout << \"\\n-1) Quit Application\\n\";\n    ```\n\n    退出应用\n12.  获取用户输入并将其转换为整数:\n\n    ```cpp\n            // Get user input\n            std::string input;\n            int userChoice;\n            getline(std::cin, input);\n            userChoice = std::stoi(input);\n    ```\n\n13.  检查用户是否输入了`-1`，从而想要关闭应用。如果他们这样做，请处理:\n\n    ```cpp\n            // Sanity user input\n            if (userChoice == -1)\n            {\n                bIsRunning = false;\n            }\n    ```\n\n14.  接下来，检查用户输入的索引是否无效。无效索引是小于`-1`且大于动物矢量`–1`大小的索引(因为索引从 0 开始，而不是从 1 开始)。如果他们这样做了，输出一条错误消息，让他们重新选择:\n\n    ```cpp\n            else if (userChoice < -1 || userChoice >                 ((int)animals.size() - 1))\n            {\n                std::cout << \"\\nInvalid Index. Please enter another.\\n\";\n            }\n    ```\n\n15.  如果用户输入一个有效的索引，调用前面创建的`PrintAnimalInfo`，传入你将从向量中得到的动物信息:\n\n    ```cpp\n            else\n            {\n                // Print animal info\n                std::cout << std::endl;\n                PrintAnimalInfo(animals[userChoice]->GetAnimalInfo());\n            }\n        }\n    ```\n\n16.  在主循环之外，清理指针。这包括删除它们的内存，将它们设置为`0`，然后清除向量:\n\n    ```cpp\n        // Cleanup.\n        for (size_t i = 0; i < animals.size(); ++ i)\n        {\n            delete animals[i];\n            animals[i] = nullptr;\n        }\n        animals.clear();\n    }\n    ```\n\n17.  运行完整的代码。您将获得以下输出:\n\n![Figure 10.19: Users can view information on various animals ](img/C14195_10_191.jpg)\n\n图 10.19:用户可以查看各种动物的信息\n\n这个应用利用遗传和多态性来简化我们动物类型的存储。通过存储指向它们的基类的指针，我们可以将它们存储在一个集合中，这意味着我们可以在一个循环中迭代它们，并以多种形式调用它们的共享成员。继承、多态性和强制转换是重要的概念，尤其是在我们构建更大、更灵活的应用时。和他们在一起舒服会让我们充分利用 C++。\n\n# 11.模板\n\n## 活动 11:创建通用堆栈\n\n**解决方案**:\n\n1.  使用通用队列示例作为基础编写一个通用堆栈:\n\n    ```cpp\n    #include <iostream>\n    #include <memory>\n    using namespace std;\n    template<class T>   \n    class Stack   \n    {  \n        public:   \n            Stack() { init(); } \n            explicit Stack(size_t numElements,                       const T& initialValue = T()) \n            {  \n                init(numElements, initialValue); \n            }\n            Stack(const Stack& q) { init(q.bottom(), q.top()); }\n            Stack& operator=(const Stack& rhs)\n            {\n                if (&rhs != this)\n                {\n                    destroy();\n                    init(rhs.bottom(), rhs.top());\n                }\n                return *this;\n            }\n           ~Stack() { destroy(); }\n            T* top() { return stackDataEnd - 1; }  \n            const T* top() const { return stackDataEnd - 1; }  \n            T* bottom() { return stackData; }  \n            const T* bottom() const { return stackData; }  \n            size_t size() const { return stackDataEnd - stackData; }  \n            bool empty() const { return size() == 0; }\n    ```\n\n2.  Alter the `pop()` function to handle a LIFO data structure:\n\n    ```cpp\n    void pop()\n        {\n            if (top() != 0)\n            {\n                alloc.destroy(top());\n                stackDataEnd -= 1;\n            }\n        }\n    ```\n\n    注意\n\n    正如活动简介中提到的，解决方案包括重用在*创建通用队列*部分中提供的代码。因此，在步骤 2 中只包括被改变的块。完整代码可以在这里找到:[https://packt.live/2r9XgYi](https://packt.live/2r9XgYi)。\n\n3.  在`main`功能中测试堆栈，输出数据测试堆栈是否正常工作:\n\n    ```cpp\n    Activity 11.cpp\n    104 int main() \n    105 {\n    106     Stack<int> testStack; \n    107     testStack.push(1); \n    108     testStack.push(2); \n    109     cout << \"stack contains values: \"; \n    110 \n    111     for (auto it = testStack.bottom(); it != testStack.top() + 1; ++ it) \n    112     { \n    113         cout << *it << \" \"; \n    114     } \n    115 \n    116     cout << endl; \n    117     cout << \"stack contains \" << testStack.size() << \" elements\" << endl; \n    118     testStack.pop(); \n    119     cout << \"stack contains values: \"; \n    120 \n    121     for (auto it = testStack.bottom(); it != testStack.top() + 1; ++ it) \n    122     { \n    123         cout << *it << \" \"; \n    124     } \n            //[…]\n    151     return 0; \n    152 }\n    The complete code for this step can be found at: https://packt.live/2r7Clp8\n    ```\n\n4.  当您成功运行完整的代码时，您将获得以下输出:\n\n![Figure 11.08: Final output of the activity ](img/C14195_11_081.jpg)\n\n图 11.08:活动的最终输出\n\n# 12.容器和迭代器\n\n## 活动 12:将 RPG 战斗转换为使用标准库容器\n\n**解决方案**:\n\n1.  更改`Attack`、`Item`和`Character`类以使用字符串代替字符数组(这里显示的是`Attack`类):\n\n    ```cpp\n    class Attack\n    {\n    public:\n\n        Attack(string name, int attackStat)\n        {\n            m_name = name;\n            m_attackStat = attackStat;\n        }\n        int getAttackStat() const { return m_attackStat; }\n        string getName() const { return m_name; }\n    private:\n        string m_name;\n        int m_attackStat;\n    };\n    ```\n\n2.  移除任何现在不需要的复制构造函数、析构函数和赋值实现(这里显示了`Item`类):\n\n    ```cpp\n    class Item\n    {\n    public:\n\n        Item(string name, int healStat)\n        {\n            m_name = name;\n            strcpy(m_name, name);\n            m_healStat = healStat;\n        }\n        int getHealStat() const { return m_healStat; }\n        string getName() const { return m_name; }\n    private:\n        string m_name;\n        int m_healStat;\n    };\n    ```\n\n3.  让`Character`类取`Attack`和`Item`的向量，而不是原始数组:\n\n    ```cpp\n    class Character\n    {\n    public:\n        Character(string name, int strengthMultiplier,               int defenceMultiplier, \n        vector<Attack> attacks, vector<Item> items)\n        {\n            m_health = 100;\n            m_name = name;\n            m_strengthMultiplier = strengthMultiplier;\n            m_defenceMultiplier = defenceMultiplier;\n            m_attacks.insert(m_attacks.begin(), attacks.begin(),                          attacks.end());\n            m_items.insert(m_items.begin(), items.begin(), items.end());\n        }\n    ```\n\n4.  实现`attack`和`defend`功能，使用向量代替数组，更新显示功能，使用向量:\n\n    ```cpp\n        void DoAttack(string moveName, Character& other) \n        {\n            cout << m_name << \" attacks \" << other.getName() << \" with \"              << moveName << endl; \n            other.DoDefend(GetAttackAmount(moveName) *                        m_strengthMultiplier); \n        }\n        void DoAttack(Character& other) \n        {\n            string attackName =         m_attacks[m_indexOfDefaultAttack].getName(); \n            cout << m_name << \" attacks \" << other.getName() << \" with \"              << attackName <<endl; \n            other.DoDefend(GetAttackAmount(attackName) *                        m_strengthMultiplier); \n        } \n        void UseItem(string itemName) \n        {\n            int itemValue = GetItemValue(itemName); \n            cout << m_name << \" uses \" << itemName << \" and gains \"              << itemValue << \"health\" << endl; \n            m_health += itemValue; \n        }\n        bool isDead() { return m_health <= 0; } \n        void Display() \n        { \n            cout << m_name << endl; \n            cout << \"Health = \" << m_health << endl; \n            cout << \"Strength Multiplier = \" << m_strengthMultiplier              << endl; \n            cout << \"Defence Multiplier = \" << m_defenceMultiplier              << endl; \n            cout << \"Attacks:\" << endl; \n            for(auto attack : m_attacks) \n                cout << attack.getName() << \" : \"                  << attack.getAttackStat() << endl; \n            cout << \"Items:\" << endl; \n            for(auto item : m_items) \n                cout << item.getName() << \" : \" << item.getHealStat()                  << endl; \n        } \n    private: \n        void DoDefend(int attackValue) \n        { \n            int damage = attackValue / m_defenceMultiplier; \n            m_health -= damage; \n            cout << m_name << \" takes \" << damage << \" damage\" << endl; \n        } \n        int GetAttackAmount(string attackName) \n        { \n            auto it = find_if(m_attacks.begin(), m_attacks.end(),                         [attackName](const Attack& attack){ return                          attack.getName() == attackName; }); \n            return (it != m_attacks.end()) ? (*it).getAttackStat() : 0; \n        }\n\n        int GetItemValue(string itemName) \n        { \n            auto it = find_if(m_items.begin(), m_items.end(),                         [itemName](const Item& item){ return item.                         getName() == itemName; }); \n            return (it != m_items.end()) ? (*it).getHealStat() : 0; \n        }\n        string m_name; \n        vector<Attack> m_attacks; \n        vector<Item> m_items; \n        int m_health; \n        int m_strengthMultiplier; \n        int m_defenceMultiplier; \n        int m_indexOfDefaultAttack; \n    }; \n    ```\n\n5.  在`main`功能中，实现一个队列，里面存放不同的`Character`类型供玩家对战:\n\n    ```cpp\n    int main()\n    {\n        // Bill the player\n        vector<Attack> billAttacks = { {\"Sword To The Face\", 20} };\n        vector<Item> billItems = { {\"Old Grog\", 50} };\n        Character bill(\"Bill\", 2, 2, billAttacks, billItems);\n        // Dragon\n        vector<Attack> dragonAttacks = {{\"Flame Breath\", 20}};\n        vector<Item> dragonItems = {{\"Scale Oil\", 20}};\n        Character dragon(\"Dragon\", 2, 1, dragonAttacks, dragonItems);\n        // Zombie\n        vector<Attack> zombieAttacks = {{\"Bite\", 50}};\n        vector<Item> zombieItems = {{\"Rotten Flesh\", 20}};\n        Character zombie(\"Zombie\", 1, 3, zombieAttacks, zombieItems);\n        // Witch\n        vector<Attack> witchAttacks = {{\"Super Spell\", 50}};\n        vector<Item> witchItems = {{\"Cure Potion\", 20}};\n        Character witch(\"Witch\", 1, 5, witchAttacks, witchItems);\n        queue<Character> monsters;\n        monsters.push(dragon);\n        monsters.push(zombie);\n        monsters.push(witch);\n    ```\n\n6.  与队列中的每个怪物战斗，直到队列为空，并显示一个`win`字符串。另外，允许使用物品和默认攻击:\n\n    ```cpp\n        bool playerTurn = true;    \n        bool gameOver = false; \n        cout << \"Bill finds himself trapped in a scary dungeon!             There seems to be a series of rooms, he enters             the first room...\" << endl; \n        while(!monsters.empty() && !gameOver) \n        { \n            Character currentMonster = monsters.front(); \n            cout << \"A monster appears, it looks like a \" \n                 << currentMonster.getName() << endl; \n            while(!currentMonster.isDead()) \n            {\n                cout << endl; \n                if(playerTurn) \n                { \n                    cout << \"bill's turn\" << endl; \n                    cout << \"Bill can press 1 and enter to use                         an item and 2 and enter to attack the                         monster.\" << endl; \n                    bool madeChoice = false; \n                    while(!madeChoice) \n                    { \n                        int choice; \n                        cin >> choice; \n                        switch(choice) \n                        { \n                            case 1: \n                                bill.UseItem(\"Old Grog\"); \n                                madeChoice = true; \n                            break; \n                            case 2: \n                                bill.DoAttack(currentMonster); \n                                madeChoice = true; \n                            break; \n                            default: \n                            break; \n                        } \n                    } \n                } \n                else \n                { \n                    cout << currentMonster.getName() << \"'s turn\" << endl;\n                    currentMonster.DoAttack(bill); \n                }\n                cout << \"Bills health is \" << bill.getHealth() << endl;\n                cout << currentMonster.getName() << \"'s health is \"                  << currentMonster.getHealth() << endl; \n                if(currentMonster.isDead()) \n                {\n                    cout << currentMonster.getName() << \" is defeated\"                      << endl; \n                    monsters.pop(); \n                }\n                if(bill.isDead()) \n                {\n                    gameOver = true; \n                    break; \n                }\n                playerTurn = !playerTurn; \n            }\n        }\n        if(monsters.empty()) \n        {\n            cout << \"You win\"; \n        }\n        if(gameOver) \n        {\n            cout << \"You lose\"; \n        } \n        return 0; \n    }\n    ```\n\n7.  运行完整的代码。您应该会收到以下输出:\n\n![Figure 12.14: Final output of the activity ](img/C14195_12_141.jpg)\n\n图 12.14 :活动最终输出\n\n# 13.C++ 中的异常处理\n\n## 活动 13:处理异常\n\n**解决方案**:\n\n1.  从活动开始时显示的示例程序开始:\n\n    ```cpp\n    #include <iostream>\n    using namespace std;\n    int main()\n    {\n        bool continue_flag;\n        do\n        {\n            continue_flag = do_something();\n        }\n        while (continue_flag == true);\n        return 0;\n    }\n    }\n    ```\n\n2.  `std::runtime_error`，传感器错误的异常信号，在`<stdexcept>`标题中定义，所以我们需要包括`<stdexcept>`。根据编译器的不同，我们可能还需要包含`<exception>`头:\n\n    ```cpp\n    #include <exception>\n    #include <stdexcept>\n    ```\n\n3.  在主循环中，用`try...catch`块替换对`do_something()`的调用。骨架`try...catch`块显示在这里:\n\n    ```cpp\n            try\n            {\n            }\n            catch (exception& e)\n            {\n            }\n    ```\n\n4.  在`try`块中，调用`reactor_safety_check()`并将其值保存在`continue_flag`变量中:\n\n    ```cpp\n                continue_flag = reactor_safety_check();\n    ```\n\n5.  增加一个捕捉`runtime_error`的`catch`子句。它必须出现在赶上 T3 的 T2 条款之前。这个`catch`子句可以是空的，但是如果它输出一条描述`exception` :\n\n    ```cpp\n            catch (runtime_error& e)\n            {\n                cout << \"caught runtime error \" << e.what() << endl;\n            }\n    ```\n\n    的消息可能是最好的\n6.  添加一个捕获所有其他 C++ 异常的`catch`子句。这些异常是意料之外的，所以调用`SCRAM()`关闭反应堆，然后调用`break`，结束封闭的`do`循环。代替`break`，将`continue_flag`设置为`false`会有相同的效果:\n\n    ```cpp\n            catch (...)\n            {\n                cout << \"caught unknown exception type\" << endl;\n                SCRAM();\n                break;\n            }\n    ```\n\n7.  添加一个捕获所有其他异常的`catch`子句。这是一个好主意，因为异常可能是任何类型的，我们不希望我们的反应堆安全检查在反应堆仍在运行的情况下退出。在本`catch`条款中，称呼`SCRAM()`，然后`break` :\n\n    ```cpp\n            catch (exception& e)\n            {\n                cout << \"caught unknown exception type\" << endl;\n                SCRAM();\n                break;\n            }\n    ```\n\n8.  在`try...catch`块之后，输出消息`\"main() exiting\"`，这样我们知道程序以受控方式停止:\n\n    ```cpp\n        cout << \"main() exiting\" << endl;\n    ```\n\n9.  在`main()`上方，插入一个名为`SCRAM()`的`void`函数。`SCRAM()`打印消息。这里有一个它可能看起来像什么的例子:\n\n    ```cpp\n    void SCRAM()\n    {\n        cout << \"SCRAM! I mean it. Get away from here!\" << endl;\n    }\n    ```\n\n10.  Add a `bool` function, `reactor_safety_check()`. It looks like this:\n\n    ```cpp\n    bool reactor_safety_check()\n    {\n        static int count = 0;\n        ++ count;\n        if (count % 17 == 0)\n        {\n            throw runtime_error(\"Sensor glitch\");\n        }\n        else if (count % 69 == 0)\n        {\n            throw 123;\n            //throw exception();\n        }\n        else if (count % 199 == 0)\n        {\n            return false;\n        }\n\n        return true;\n    }\n    ```\n\n    注意`reactor_safety_check()`可能会抛出一个`std::exception`或者某个意外类型的异常，你应该用这两种方式测试你的代码。\n\n11.  Compile and run the completed program. While different students' programs will produce somewhat different output, this program produces the following:\n\n    ![Figure 13.11: Final output of the activity ](img/C14195_13_111.jpg)\n\n图 13.11:活动的最终输出\n\n这是怎么回事？`do`循环调用`reactor_safety_check()`。大多数情况下，`reactor_safety_check()`正常返回，但有时会抛出异常，可能是因为传感器故障。报告此异常，允许继续执行，这导致循环重复调用`reactor_safety_check()`。我们的测试版`reactor_safety_check()`有时会调用一些其他的异常类型。当另一种异常发生时，该程序不知道该怎么办，所以它采取了唯一的行动，承诺不辐射 800 万伦敦人——它紧急关闭反应堆，打破循环。"
  },
  {
    "path": "docs/cpp-workshop/README.md",
    "content": "# C++ 工作室\n\n> 原书：[The C++ Workshop](https://libgen.rs/book/index.php?md5=5BA4B421A6BA3D7C3A23406BAB386EC0)\n> \n> 协议：[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)\n> \n> 阶段：机翻（1）\n> \n> 自豪地采用[谷歌翻译](https://translate.google.cn/)\n> \n> 让开源界提前感受入关。\n\n* [在线阅读](https://ccpp.apachecn.org)\n* [在线阅读（Gitee）](https://apachecn.gitee.io/apachecn-c-cpp-zh/)\n* [ApacheCN 学习资源](http://docs.apachecn.org/)\n\n## 贡献指南\n\n本项目需要校对，欢迎大家提交 Pull Request。\n\n> 请您勇敢地去翻译和改进翻译。虽然我们追求卓越，但我们并不要求您做到十全十美，因此请不要担心因为翻译上犯错——在大部分情况下，我们的服务器已经记录所有的翻译，因此您不必担心会因为您的失误遭到无法挽回的破坏。（改编自维基百科）\n\n## 联系方式\n\n### 负责人\n\n* [飞龙](https://github.com/wizardforcel): 562826179\n\n### 其他\n\n*   在我们的 [apachecn/apachecn-c-cpp-zh](https://github.com/apachecn/apachecn-c-cpp-zh) github 上提 issue.\n*   发邮件到 Email: `apachecn@163.com`.\n*   在我们的 [组织学习交流群](http://www.apachecn.org/organization/348.html) 中联系群主/管理员即可.\n\n## 赞助我们\n\n![](http://data.apachecn.org/img/about/donate.jpg)\n"
  },
  {
    "path": "docs/cpp-workshop/SUMMARY.md",
    "content": "+   [C++ 工作室](README.md)\n+   [零、前言](00.md)\n+   [一、您的第一个 C++ 应用](01.md)\n+   [二、控制流](02.md)\n+   [三、内置数据类型](03.md)\n+   [四、运算符](04.md)\n+   [五、指针和引用](05.md)\n+   [六、动态变量](06.md)\n+   [七、动态变量的所有权和寿命](07.md)\n+   [八、类和结构](08.md)\n+   [九、面向对象原则](09.md)\n+   [十、高级面向对象原则](10.md)\n+   [十一、模板](11.md)\n+   [十二、容器和迭代器](12.md)\n+   [十三、C++ 中的异常处理](13.md)\n+   [十四、附录](14.md)\n"
  },
  {
    "path": "docs/emb-prog-mod-cpp-cb/00.md",
    "content": "# 零、前言\n\n长期以来，嵌入式系统的开发要么需要纯 C 语言，要么需要汇编语言。 这其中有很多很好的理由。 硬件没有足够的资源来运行用高级编程语言(如 C++、Java 或 Python)编写的应用，但更重要的是，没有真正需要用这些语言编写软件。 有限的硬件资源限制了软件的复杂性，嵌入式应用的功能仍然相对简单，而 C 语言的能力足以实现它。\n\n随着硬件开发的进步，如今越来越多的嵌入式系统采用价格低廉但功能强大的片上系统(System-on-Chip，System-on-Chip)，能够运行 Linux 等通用多任务操作系统。\n\n不断增长的硬件能力要求更复杂的软件，而 C++ 越来越多地成为新的嵌入式系统的首选语言。 有了它的*，您就不必为不使用*方法付费了。它允许开发人员创建使用计算和内存资源的应用，就像用 C 编写的应用一样，但是它为开发人员提供了更多的工具来处理复杂性和更安全的资源管理，例如面向对象编程和 RAII 习惯用法。\n\n在 C 方面有丰富经验的经验丰富的嵌入式开发人员通常倾向于以类似的习惯方式用 C++ 编写代码，将这种语言视为 C 的面向对象扩展，C 是具有类的*C。 然而，现代 C++ 有自己的最佳实践和概念，如果使用得当，可以帮助开发人员避免常见的陷阱，并允许他们在几行代码中做很多事情。*\n\n另一方面，具有 C++ 经验进入嵌入式系统世界的开发人员应该意识到特定硬件平台和应用域的要求、限制和功能，并相应地设计他们的 C++ 代码。\n\n这本书的目标是弥合这一差距，并演示如何将现代 C++ 的特性和最佳实践应用于嵌入式系统的上下文中。\n\n# 这本书是写给谁的？\n\n本书面向希望用 C++ 构建高效嵌入式程序的开发人员和电子硬件、软件和片上系统工程师。\n\n嵌入式系统的世界是广阔的。 这本书试图涵盖其中的一种类型，运行 Linux 操作系统(如 Raspberry PI 或 BeagleBoard)的 SoC，简要介绍低级微控制器(如 Arduino)。\n\n要求熟悉 C++，但不需要深入的 C++ 知识或嵌入式系统经验。\n\n# 这本书涵盖了哪些内容\n\n[第 1 章](01.html)，*嵌入式系统基础*定义了什么是嵌入式系统，它们与其他系统有何不同，为什么需要特定的编程技术，以及为什么 C++ 很好，在很多情况下是嵌入式开发的最佳选择。 它概述了嵌入式开发人员在日常工作中遇到的限制和挑战：有限的系统资源和 CPU 性能、处理硬件错误以及远程调试。\n\n[第 2 章](02.html)，*设置环境*解释了嵌入式系统开发环境与 Web 或桌面应用开发的区别，并介绍了构建和目标系统、交叉编译和交叉工具包、串行控制台和远程外壳等概念。 它提供了为运行 Windows、MacOS 或 Linux 的最常见桌面配置设置虚拟化构建和目标主机的实用步骤。\n\n[第 3 章](03.html)*使用不同的体系结构*解释了如何在 C++ 代码中考虑目标系统的 CPU 体系结构和内存配置方面的重要差异。\n\n[第 4 章](04.html)，*处理中断*，涵盖了中断和中断服务例程的低级概念。 在现代操作系统中，即使是开发人员或设备驱动程序也必须使用操作系统提供的更高级别的 API。 这就是为什么我们使用 8051 单片机来探索中断技术的原因。\n\n[第 5 章](05.html)，*调试、日志记录和分析*，介绍了特定于基于 Linux 的嵌入式系统的调试技术，例如直接在目标板上运行 gdb、设置 gdbserver 以进行远程调试，以及日志记录对调试和故障根本原因分析的重要性。\n\n[第 6 章](06.html)，*内存管理*提供了几个内存分配的秘诀和最佳实践，对嵌入式系统的开发人员很有帮助。 我们讨论为什么在嵌入式应用中避免动态内存分配，以及可以考虑哪些替代方案来实现快速、确定性的内存分配。\n\n[第 7 章](07.html)，*多线程和同步*解释了如何使用 C++ 标准库提供的函数和类来实现高效的多线程应用，这些应用可以利用现代多核 CPU 的所有功能。\n\n[第 8 章](08.html)，*通信和序列化*，涵盖了进程间和系统间通信的概念、挑战和最佳实践，例如套接字、管道、共享内存和使用 FlatBuffers 库的内存高效序列化。 使用定义良好的异步协议将应用解耦为相互通信的独立组件，是在保持软件系统快速和容错的同时扩展软件系统的事实上的标准方式。\n\n[第 9 章](09.html)，*外部设备*解释了如何在 C++ 程序中使用各种外部设备。 虽然大多数设备通信 API 不依赖于特定的编程语言，但我们将学习如何使用 C++ 的强大功能来编写便于开发人员并帮助防止常见资源泄漏错误的包装器。\n\n[第 10 章](10.html)，*降低功耗*探讨了编写高能效应用和利用操作系统电源管理功能的最佳实践。 它为基于 Linux 的嵌入式系统提供了几个实用的配方，但是相同的概念可以扩展到任何操作系统和任何平台。\n\n[第 11 章](11.html)，*时间点和间隔*涵盖了与时间操作相关的各种主题，从测量间隔到添加延迟。 我们将了解标准 C++ Chrono 库提供的 API，以及如何有效地使用它来构建可移植的嵌入式应用。\n\n[第 12 章](12.html)，*错误处理和容错*探讨了用 C++ 编写的嵌入式应用错误处理的可能实现和最佳实践。 它解释了如何有效地使用 C++ 异常，并将其与传统错误代码和复杂返回类型等备选方法进行了比较。 它涉及到基本的容错机制，如看门狗计时器和心跳。\n\n[第 13 章](13.html)，*实时系统指南*，涵盖了实时系统的细节。 它简要描述了实时系统是如何定义的，以及存在哪些类型的实时系统。 它包含关于如何使应用的行为更具确定性的实用食谱，这是实时系统的关键要求。\n\n[第 14 章](14.html)，*《安全关键系统指南》*解释了什么是安全关键系统，以及它们与其他嵌入式系统的不同之处。 它涵盖了在安全关键型系统上工作时所需的开发方法和工具，从遵循 MISRA、AUTOSAR 或 JSF 等形式化编码指南到使用静态代码分析或正式软件验证工具。\n\n[第 15 章](15.html)，*微控制器编程*概述了编写、编译和调试微控制器 C++ 代码的基本概念。 我们将以广泛使用的 Arduino 板为例学习如何设置开发环境。\n\n# 为了最大限度地利用这本书\n\n嵌入式系统的开发意味着您的应用将与某种特定的硬件交互-特定的 SoC 平台、特定的微控制器或特定的外部设备。 有多种可能的硬件配置，以及使用这些硬件设置所需的专用操作系统或 IDE。\n\n这本书的目标是让每个人开始学习嵌入式系统的编程，而不需要在硬件上投入太多。 这就是为什么大多数配方都是针对在虚拟化 Linux 环境或仿真器中工作的原因。 然而，有些食谱可能需要物理硬件。 这些食谱被设计成可以在树莓 PI 或 Arduino 上运行，这两个平台使用最广泛，价格也不高，相对容易获得。\n\n| **书中介绍的软件/硬件** | **操作系统要求** |\n| Docker\n([HTTPS：//www.docker.com/products/docker-Desktop](https://www.docker.com/products/docker-desktop)) | \n\n*   Microsoft Windows 10 专业企业版 6411-13\n*   MacOS 10.13 or later\n*   Ubuntu Linux 16.04 or later\n*   Debian Linux Stretch(9)或 Buster(10)\n*   Fedora Linux 30 or later\n\n |\n| QEMU\n([https：//www.qemu.org/download/](https://www.qemu.org/download/)) | \n\n*   Windows 8 or later (32-bit or 64-bit)\n*   MacOS 10.7 or later\n*   Linux (various distributions)\n\n |\n| 覆盆子 PI 3 型 B+ |  |\n| Arduino UNO R3 或 ELEGOO UNO R3 |  |\n\n**如果您使用的是本书的数字版本，我们建议您自己键入代码或通过 GitHub 存储库(下一节提供的链接)访问代码。 这样做可以帮助您避免与复制和粘贴代码相关的任何潜在错误。**\n\n# 下载示例代码文件\n\n您可以从您的帐户[www.Packt.com](http://www.packt.com/)下载本书的示例代码文件。 如果您在其他地方购买了本书，您可以访问[www.Packtpub.com/support](https://www.packtpub.com/support)并注册，让文件直接通过电子邮件发送给您。\n\n您可以通过以下步骤下载代码文件：\n\n1.  登录或注册[www.Packt.com](http://www.packt.com/)。\n2.  选择支持选项卡。\n3.  单击 Code Downloads(代码下载)。\n4.  在搜索框中输入图书名称，然后按照屏幕上的说明进行操作。\n\n下载文件后，请确保使用以下最新版本解压缩或解压缩该文件夹：\n\n*   WinRar/7-用于 Windows 的 Zip\n*   适用于 Mac 的 Zipeg/iZip/UnRarX\n*   Linux 版 7-Zip/PeaZip\n\n该书的代码包也托管在 giHub 的[https://github.com/PacktPublishing/Embedded-Programming-with-Modern-CPP-Cookbook](https://github.com/PacktPublishing/Embedded-Programming-with-Modern-CPP-Cookbook)上。 如果代码有更新，它将在现有的 GitHub 存储库中进行更新。\n\n我们还在**[https://github.com/PacktPublishing/](https://github.com/PacktPublishing/)**上提供了丰富的图书和视频目录中的其他代码包。 看看他们！\n\n# 下载彩色图像\n\n我们还提供了一个 PDF 文件，其中包含本书中使用的屏幕截图/图表的彩色图像。 您可以在此处下载：[https://static.packt-cdn.com/downloads/9781838821043_ColorImages.pdf](https://static.packt-cdn.com/downloads/9781838821043_ColorImages.pdf)。\n\n# 使用的约定\n\n本书中使用了许多文本约定。\n\n`CodeInText`：指示文本中的代码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄。 下面是一个示例：“在`gdbserver`下运行`hello`应用。”\n\n代码块设置如下：\n\n```cpp\n#include <iostream>\n\nint main() {\n std::cout << \"Hello, world!\" << std::endl;\n return 0;\n}\n```\n\n当我们希望您注意代码块的特定部分时，相关行或项将以粗体显示：\n\n```cpp\n#include <iostream>\n\nint main() {\n std::cout << \"Hello, world!\" << std::endl;\n return 0;\n}\n```\n\n任何命令行输入或输出都如下所示：\n\n```cpp\n$ docker run -ti -v $HOME/test:/mnt ubuntu:bionic\n```\n\n**粗体**：表示您在屏幕上看到的新术语、重要单词或单词。 例如，菜单或对话框中的单词显示在文本中，如下所示。 下面是一个例子：“为 CMake 配置交叉编译的最佳方式是使用所谓的**工具链**文件。”\n\nWarnings or important notes appear like this. Tips and tricks appear like this.\n\n# 部分 / 派别 / 部门 / 章节\n\n在本书中，您可以找到几个频繁出现的标题(*正在准备*、*如何做……。* ，*它是如何工作的.。* ，*还有更多...。* 和*，另见*)。\n\n要给出关于如何完成食谱的明确说明，请使用以下部分：\n\n# 正在做好准备\n\n本节告诉您食谱中的预期内容，并介绍如何设置食谱所需的任何软件或任何初步设置。\n\n# How to do it…(如何做到这一点)\n\n本节包含遵循食谱所需的步骤。\n\n# 工作原理…\n\n这一节通常包含对上一节中发生的事情的详细解释。\n\n# 还有更多的…\n\n本部分包含有关食谱的其他信息，以便您更好地了解食谱。\n\n# 另请参阅\n\n本节提供了有关食谱的其他有用信息的有用链接。\n\n# 保持联系\n\n欢迎读者的反馈。\n\n**一般反馈**：如果您对本书的任何方面有疑问，请在邮件主题中提及书名，并向我们发送电子邮件至`customercare@packtpub.com`。\n\n**勘误表**：虽然我们已经竭尽全力确保内容的准确性，但错误还是会发生。 如果您在这本书中发现了错误，请向我们报告，我们将不胜感激。 请访问[www.Packtpub.com/support/errata](https://www.packtpub.com/support/errata)，选择您的图书，单击勘误表提交表链接，然后输入详细信息。\n\n**盗版**：如果您在互联网上遇到任何形式的非法复制我们的作品，请您提供地址或网站名称，我们将不胜感激。 请拨打`copyright@packt.com`与我们联系，并提供该材料的链接。\n\n**如果您有兴趣成为一名作者**：如果有一个您擅长的主题，并且您有兴趣撰写或投稿一本书，请访问[Auths.Packtpub.com](http://authors.packtpub.com/)。\n\n# 评论\n\n请留下评论。 一旦你阅读并使用了这本书，为什么不在你购买它的网站上留下评论呢？ 这样，潜在读者就可以看到并使用您不偏不倚的意见来做出购买决定，我们 Packt 可以了解您对我们产品的看法，我们的作者也可以看到您对他们的书的反馈。 谢谢!\n\n有关 Packt 的更多信息，请访问[Packt.com](http://www.packt.com/)。"
  },
  {
    "path": "docs/emb-prog-mod-cpp-cb/01.md",
    "content": "# 一、嵌入式系统基础\n\n嵌入式系统是将硬件和软件组件相结合以解决较大系统或设备中的特定任务的计算机系统。 与通用计算机不同，它们是高度专业化和优化的，只执行一项任务，但做得非常好。\n\n它们在我们周围无处不在，但我们很少注意到它们。 你几乎可以在每个家用电器或小工具中找到它们，比如微波炉、电视机、网络存储或智能恒温器。 您的汽车包含几个相互连接的嵌入式系统，用于处理刹车、燃油喷射和信息娱乐。\n\n在本章中，我们将讨论以下有关嵌入式系统的主题：\n\n*   探索嵌入式系统\n*   利用有限的资源工作\n*   查看性能影响\n*   使用不同的架构\n*   处理硬件错误\n*   使用 C++ 进行嵌入式开发\n*   远程部署软件\n*   远程运行软件\n*   日志记录和诊断\n\n# 探索嵌入式系统\n\n作为较大系统或设备的一部分，为解决特定问题而创建的每个计算机系统都是嵌入式系统。 即使是您的通用 PC 或笔记本电脑也包含许多嵌入式系统。 键盘、硬盘、网卡或 Wi-Fi 模块-每一个都是嵌入式系统，带有处理器(通常称为**微控制器**)和自己的软件(通常称为**固件**)。\n\n现在让我们深入了解嵌入式系统的不同功能。\n\n# 它们与桌面或 Web 应用有何不同？\n\n与台式机或服务器相比，嵌入式系统最显著的特点是其专门用于完成特定任务的硬件和软件的紧密耦合。\n\n嵌入式设备可以在各种物理和环境条件下工作。 它们中的大多数并不是专为在专用的有条件的数据中心或办公室工作而设计的。 它们必须在无法控制的环境中发挥作用，通常没有任何监督和维护。\n\n由于它们是专门化的，因此会精确计算硬件要求，以完成尽可能经济高效的任务。 因此，该软件的目标是以最少的储备或没有储备的方式利用 100%的可用资源。\n\n与常规台式机和服务器相比，嵌入式系统的硬件有很大的不同。 每个系统的设计都是个性化的。 它们可能需要非常具体的 CPU 和将其连接到存储器和外部硬件的原理图。\n\n嵌入式系统是为与外部硬件通信而设计的。 嵌入式程序的主要部分是检查状态、读取输入、发送数据或控制外部设备。 嵌入式系统没有用户界面是很常见的。 与在传统桌面或 Web 应用上执行相同操作相比，这使得开发、调试和诊断要困难得多。\n\n# 嵌入式系统的类型\n\n嵌入式系统涵盖了广泛的使用案例和技术-从用于自动驾驶或大规模存储系统的强大系统，到用于控制灯泡或 LED 显示器的微型微控制器。\n\n根据硬件的集成度和专业化程度，嵌入式系统大致可以分为以下几类：\n\n*   **微控制器**(**MCU**)\n*   **A****片上系统**(**SoC**)\n*   **专用集成电路**(**ASIC**)\n*   **现场可编程门阵列**(**FPGA**)\n\n# 微控制器\n\nMCU 是专为嵌入式应用设计的通用集成电路。 单个 MCU 芯片通常包含一个或多个 CPU、存储器和可编程输入/输出外部设备。 它们的设计允许它们直接与传感器或执行器连接，而无需添加任何额外组件。\n\nMCU 广泛应用于汽车发动机控制系统、医疗设备、遥控器、办公机器、家用电器、电动工具和玩具。\n\n它们的 CPU 各不相同，从简单的 8 位处理器到更复杂的 32 位甚至 64 位处理器。\n\nMCU 种类繁多，目前最常见的有以下几种：\n\n*   英特尔 MCS-51 或 8051 MCU。\n*   爱特梅尔公司的 AVR\n*   MicroChip Technology 的**可编程接口控制器**(**PIC**)\n*   各种基于 ARM 的 MCU\n\n# 片上系统\n\nSoC 是一种集成电路，它将解决某类特定问题所需的所有电子电路和部件组合在一块芯片上。\n\n它可能包含数字、模拟或混合信号功能，具体取决于应用。 将大多数电子部件集成在一块芯片上有两大好处：小型化和低功耗。 与集成度较低的硬件设计相比，SoC 需要的功耗要低得多。 硬件和软件层面的功耗优化使其能够创建在没有外部电源的情况下依靠电池工作数天、数月甚至数年的系统。 通常，它还集成了射频信号处理，再加上其紧凑的物理尺寸，使其成为移动应用的理想解决方案。 此外，SoC 通常用于汽车行业、可穿戴电子产品以及**物联网**(**IoT**)：\n\n![](img/5075c1bd-7414-4cbc-bfd7-19097332a3de.png)\n\nFigure 1.1: A Raspberry Pi Model B+\n\nRaspberry Pi 系列单板计算机就是基于 SoC 设计的系统示例。 B+型构建在 Broadcom BCM2837B0 SoC 之上，集成了一个基于 ARM 的四核 1.4 Hz CPU、1 GB 内存、一个网络接口控制器和四个以太网接口。\n\n该主板具有四个 USB 接口、一个用于启动操作系统和存储数据的 MicroSD 卡端口、以太网和 Wi-Fi 网络接口、HDMI 视频输出以及一个 40 针 GPIO 接口，用于连接自定义外部硬件。\n\n它随 Linux 操作系统一起提供，是教育和**DIY**项目的绝佳选择。\n\n# 专用集成电路\n\n**专用集成电路**或**ASIC**是由其制造商为特定用途定制的集成电路。 定制是一个昂贵的过程，但允许它们满足基于通用硬件的解决方案通常不可行的要求。 例如，现代高效比特币矿工通常建立在专用 ASIC 芯片之上。\n\n为了定义 ASIC 的功能，硬件设计人员使用一种硬件描述语言，如 Verilog 或 VHDL。\n\n# 现场可编程门阵列\n\n与 SoC、ASIC 和 MCU 不同，**现场可编程门阵列**或**FPGA**是可以在制造后在硬件级别重新编程的半导体器件。 它们基于通过可编程互连连接的**可配置逻辑块**(**CLB**)的矩阵。 开发人员可以对互连进行编程，以根据他们的要求执行特定功能。 FPGA 采用**硬件定义语言**(**HDL**)编程。 它允许实现数字功能的任意组合，以便非常快速和高效地处理大量数据。\n\n# 利用有限的资源工作\n\n一种常见的误解是，嵌入式系统基于的硬件比常规台式机或服务器硬件慢得多。 虽然情况通常如此，但并非总是如此。\n\n某些特定应用可能需要大量内存的大量计算能力。 例如，自动驾驶需要内存和 CPU 资源来实时处理来自使用人工智能算法的各种传感器的大量数据。 另一个例子是利用大量内存和资源进行数据缓存、复制和加密的高端存储系统。\n\n在任何一种情况下，嵌入式系统硬件的设计都是为了最小化整个系统的成本。 对于使用嵌入式系统的软件工程师来说，结果是资源稀缺。 他们应该利用所有可用的资源，非常认真地对待性能和内存优化。\n\n# 查看性能影响\n\n大多数嵌入式应用都针对性能进行了优化。 正如前面所讨论的，目标 CPU 被选择为经济高效的，开发人员可以利用它所能提供的所有计算能力。 另一个因素是与外部硬件的通信。 这通常需要精确而快速的反应时间。 因此，脚本、可解释的字节码语言(如 Python 或 Java)的空间有限。 大多数嵌入式程序都是用编译成本机代码的语言编写的，主要是 C 和 C++。\n\n为了实现最高性能，嵌入式程序利用了编译器的所有性能优化功能。 现代编译器非常擅长代码优化，其性能超过了熟练开发人员用汇编语言编写的代码。\n\n然而，工程师不能仅仅依靠编译器提供的性能优化。 为了实现最高效率，他们必须考虑目标平台的具体情况。 通常用于在 x86 平台上运行的桌面或服务器应用的编码实践对于 ARM 或 MIPS 等不同的体系结构可能是低效的。 目标体系结构的特定功能的利用通常会给程序带来显著的性能提升。\n\n# 使用不同的架构\n\n桌面应用的开发人员通常很少关注硬件架构。 首先，它们通常使用高级编程语言来隐藏这些复杂性，但代价是性能有所下降。 其次，在大多数情况下，他们的代码运行在 x86 架构上，他们通常认为 x86 的特性是理所当然的。 例如，它们可能假设`int`的大小是`32`位，但在许多情况下并非如此。\n\n嵌入式开发人员需要处理更多种类的体系结构。 即使他们不是用目标平台原生的汇编语言编写代码，他们也应该知道所有的 C 和 C++ 基础类型都是依赖于体系结构的；标准只保证`int`至少是`16`位。 他们还应该了解特定体系结构的特点，如**字符顺序**和**对齐**，并考虑到浮点或 64 位数字的操作在 x86 体系结构上相对便宜，在其他体系结构上可能要昂贵得多。\n\n# 字节顺序\n\n**Endianness**定义表示大数值的字节在内存中的存储顺序。\n\n有两种类型的字符顺序：\n\n*   **Big-endian**：首先存储最高有效字节。 将`0x01020304`32 位值存储在`ptr`地址如下：\n\n    | **内存**中的偏移量 | **值** |\n    | `ptr` | 。 0x01 |\n    | `ptr + 1` | 0x02 0x02 |\n    | `ptr + 2` | 0x03 0x03 |\n    | `ptr + 3` | 0x04 |\n\n大端架构的例子有 AVR32 和摩托罗拉 68000。\n\n*   **Little-endian**：首先存储最低有效字节。 将`0x01020304`32 位值存储在`ptr`地址如下：\n\n    | **内存**中的偏移量 | **值** |\n    | `ptr` | 。 0x04 |\n    | `ptr + 1` | 0x03 0x03 |\n    | `ptr + 2` | 0x02 0x02 |\n    | `ptr + 3` | 0x01 |\n\nX86 架构是小端的。\n\n*   **双字节顺序**：硬件支持可切换的字节顺序。 一些示例包括 PowerPC、ARMv3 和前面的示例。\n\n在与其他系统交换数据时，字节序尤其重要。 如果开发人员按原样发送`0x01020304`32 位整数，如果接收方的字符顺序与发送方的字符顺序不匹配，则可能会将其读取为`0x04030201`。 这就是数据应该**序列化**的原因。\n\n此 C++ 代码段可用于确定系统的字节顺序：\n\n```cpp\n#include <iostream>\nint main() {\n  union {\n    uint32_t i;\n    uint8_t c[4];\n  } data;\n  data.i = 0x01020304;\n  if (data.c[0] == 0x01) {\n    std::cout << \"Big-endian\" << std::endl;\n  } else {\n    std::cout << \"Little-endian\" << std::endl;\n  }\n}\n```\n\n# 对齐\n\n处理器不以字节为单位读写数据，而是以**个内存字**-与其数据地址大小匹配的区块为单位读取和写入数据。 32 位处理器使用 32 位字，64 位处理器使用 64 位字，依此类推。\n\n当字对齐时，读写效率最高-数据地址是字大小的倍数。 例如，对于 32 位架构，`0x00000004`地址是对齐的，而`0x00000005`是未对齐的。\n\n编译器自动对齐数据以实现最高效的数据访问。 当涉及到结构时，结果可能会让那些没有意识到对齐的开发人员感到惊讶：\n\n```cpp\n struct {\n\n    uint8_t c;\n\n    uint32_t i;\n\n  } a = {1, 1};\n\n  std::cout << sizeof(a) << std::endl;\n```\n\n前面的代码片段的输出是什么？ `uint8_t`的大小是`1`，`uint32_t`的大小是`4`。 开发人员可能会认为结构的大小是各个大小的总和。 然而，结果在很大程度上取决于目标架构。\n\n对于 x86，结果是`8`。 让我们在`i`之前再添加一个`uint8_t`字段：\n\n```cpp\nstruct {\n\n    uint8_t c;\n\n    uint8_t cc;\n\n    uint32_t i;\n\n  } a = {1, 1};\n\n  std::cout << sizeof(a) << std::endl;\n```\n\n结果仍然是`8`！ 编译器通过添加填充字节，根据对齐规则优化数据字段在结构中的位置。 规则依赖于体系结构，对于其他体系结构，结果可能会有所不同。 因此，如果没有序列化*，*，就不能在两个不同的系统之间直接交换结构，这将在[第 8 章](08.html)，*通信和序列化*中进行更深入的解释。\n\n除了 CPU，访问数据对齐对于通过硬件地址转换机制进行高效的内存映射也是至关重要的。 现代操作系统操作 4KB 的内存块或页面来将进程虚拟地址空间映射到物理内存。 在 4 KB 边界上对齐数据结构可以提高性能。\n\n# 固定宽度整数类型\n\nC 和 C++ 开发人员经常忘记基本数据类型(如`char`、`short`或`int`)的大小取决于体系结构。 为了使代码可移植，嵌入式开发人员通常使用固定大小的整数类型来显式指定数据字段的大小。\n\n最常用的数据类型如下：\n\n| **宽度** | **签名** | **无符号** |\n| 8 位 | `int8_t` | `uint8_t` |\n| 16 位 | `int16_t` | `uint16_t` |\n| 32 位 | `int32_t` | `uint32_t` |\n\n指针大小还取决于体系结构。 开发人员通常需要寻址数组的元素，由于数组在内部表示为指针，因此偏移量表示取决于指针大小。 `size_t`是一种特殊的数据类型，以独立于体系结构的方式表示偏移量和数据大小。\n\n# 处理硬件错误\n\n嵌入式开发人员工作的一个重要部分就是处理硬件。 与大多数应用开发人员不同，嵌入式开发人员不能依赖硬件。 硬件故障有不同的原因，嵌入式开发人员必须区分纯粹的软件故障和由硬件故障或故障引起的软件故障。\n\n# 硬件的早期版本\n\n嵌入式系统基于为特定用例设计和制造的专用硬件。 这意味着在开发嵌入式系统的软件时，其硬件还没有稳定和良好的测试。 当软件开发人员在其代码行为中遇到错误时，并不一定意味着存在软件错误，但可能是硬件工作不正常所致。\n\n很难对这类问题进行分类。 它们需要知识、直觉，有时还需要使用示波器来将问题的根源缩小到硬件。\n\n# 硬件不可靠\n\n硬件本质上是不可靠的。 每个硬件组件都有发生故障的可能性，开发人员应该意识到硬件随时可能出现故障。 存储在内存中的数据可能会因为内存故障而损坏。 通过通信信道传输的消息可能会因为外部噪声而改变。\n\n嵌入式开发人员已经为这些情况做好了准备。 它们使用校验和或**循环冗余校验**(**CRC**)码来检测并在可能的情况下纠正损坏的数据。\n\n# 环境条件的影响\n\n高温、低温、高湿度、振动、灰尘等环境因素会显著影响硬件的性能和可靠性。 虽然开发人员设计他们的软件来处理所有潜在的硬件错误，但通常的做法是在不同的环境中测试系统。 此外，环境条件的知识可以在对问题的根本原因进行分析时提供重要线索。\n\n# 使用 C++ 进行嵌入式开发\n\n多年来，绝大多数嵌入式项目都是使用 C 编程语言开发的。 这种语言非常适合嵌入式软件开发人员的需求。 它提供了功能丰富且方便的语法，但同时，它的级别相对较低，并且不会对开发人员隐藏平台细节。\n\n由于它的通用性、紧凑性和编译代码的高性能，它成为嵌入式世界事实上的标准开发语言。 C 语言的编译器适用于大多数(如果不是全部)体系结构；它们被优化以生成比手动编写的机器代码更高效的机器码。\n\n随着时间的推移，嵌入式系统的复杂性增加了，开发人员面临着 C 语言的局限性，最显著的是容易出错的资源管理和缺乏高级抽象。 用 C 语言开发复杂的应用需要花费大量的精力和时间。\n\n与此同时，C++ 也在不断发展，获得了新的功能并采用了编程技术，使其成为现代嵌入式系统开发人员的最佳选择。 这些新功能和新技术如下：\n\n*   你不用为你不用的东西付钱。\n*   面向对象的编程来计时代码的复杂性。\n*   **资源获取是初始化**(**RAII**)。\n*   例外。\n*   一个强大的标准库。\n*   线程和内存模型作为语言规范的一部分。\n\n# 你不用为你不用的东西付钱\n\nC++ 的座右铭之一是*你不用为你不用的东西付费*。 这种语言比 C 语言有更多的特性，但对于那些不使用的特性，它承诺零开销。\n\n以虚拟函数为例：\n\n```cpp\n#include <iostream>\n\nclass A {\n\npublic:\n\n  void print() {\n\n    std::cout << \"A\" << std::endl;\n\n  }\n\n};\n\nclass B: public A {\n\npublic:\n\n  void print() {\n\n    std::cout << \"B\" << std::endl;\n\n  }\n\n};\n\nint main() {\n\n  A* obj = new B;\n\n  obj->print();\n\n}\n```\n\n前面的代码将输出`A`，尽管`obj`指向`B`类的对象。 为了使其按预期工作，开发人员添加了一个关键字-`virtual`：\n\n```cpp\n#include <iostream>\n\nclass A {\n\npublic:\n\n  virtual void print() {\n\n    std::cout << \"A\" << std::endl;\n\n  }\n\n};\n\nclass B: public A {\n\npublic:\n\n  void print() {\n\n    std::cout << \"B\" << std::endl;\n\n  }\n\n};\n\nint main() {\n\n  A* obj = new B;\n\n  obj->print();\n\n}\n```\n\n在此更改之后，代码输出`B`，这是大多数开发人员期望得到的结果。 您可能会问，为什么 C++ 不强制每个方法在缺省情况下都是`virtual`。 Java 采用了这种方法，似乎没有任何缺点。\n\n原因是`virtual`函数不是免费的。 函数解析在运行时通过虚拟表(函数指针数组)执行。 它稍微增加了函数调用时间的开销。 如果您不需要动态多态性，则无需付费。 这就是 C++ 开发人员添加`virtual`键盘的原因，明确同意增加性能开销的功能。\n\n# 面向对象编程对代码复杂性进行计时\n\n随着嵌入式程序的复杂性不断增加，使用 C 语言提供的传统过程化方法来管理它们变得越来越困难。 如果您看一看大型的 C 项目，比如 Linux 内核，您会发现它采用了面向对象编程的许多方面。\n\nLinux 内核广泛使用封装，隐藏实现细节，并使用 C 结构提供对象接口。\n\n虽然可以用 C 编写面向对象的代码，但用 C++ 编写要容易得多、方便得多，因为在 C++ 中，编译器可以为开发人员完成所有繁重的任务。\n\n# 资源获取是初始化\n\n嵌入式开发人员大量使用操作系统提供的资源：内存、文件和网络套接字。 C 开发人员使用 API 函数对获取和释放资源；例如，`malloc`声明一个内存块，`free`将其返回给系统。 如果开发人员出于某种原因忘记调用`free`，那么这块内存就会泄漏。 内存泄漏或资源泄漏通常是用 C：\n\n```cpp\n#include <stdio.h>\n\n#include <unistd.h>\n\n#include <fcntl.h>\n\n#include <string.h>\n\nint AppendString(const char* str) {\n\n  int fd = open(\"test.txt\", O_CREAT|O_RDWR|O_APPEND);\n\n if (fd < 0) {\n\n    printf(\"Can't open file\\n\");\n\n    return -1;\n\n  }\n\n  size_t len = strlen(str);\n\n  if (write(fd, str, len) < len) {\n\n    printf(\"Can't append a string to a file\\n\");\n\n    return -1;\n\n  }\n\n  close(fd);\n\n  return 0;\n\n}\n```\n\n前面的代码看起来是正确的，但它包含几个严重的问题。 如果`write`函数返回错误或写入的数据比请求的少(这是正确的行为)，`AppendString`函数会记录错误并返回。 但是，如果它忘记关闭文件描述符，它就会泄漏。 随着时间的推移，越来越多的文件描述符泄漏，并且在某个时候，程序达到了打开文件描述符的极限，使得*对`open`函数的所有*调用都失败。\n\nC++ 提供了一个强大的编程习惯用法来防止资源泄漏：**rai**。 资源在对象构造函数中分配，在对象析构函数中释放。 这意味着仅在对象处于活动状态时才持有资源。 当对象被销毁时，它会自动释放：\n\n```cpp\n#include <fstream>\n\nvoid AppendString(const std::string& str) {\n\n  std::ofstream output(\"test.txt\", std::ofstream::app);\n\n  if (!output.is_open()){\n\n    throw std::runtime_error(\"Can't open file\");\n\n  }\n\n  output << str;\n\n}\n```\n\n请注意，此函数不会显式调用`close`。 文件在输出对象的析构函数中关闭，该函数在`AppendString`函数返回时自动调用。\n\n# 例外情况\n\n传统上，C 开发人员使用错误代码处理错误。 这种方法需要程序员的大量关注，并且是 C 程序中难以找到的错误的源泉。 忽略或忽略丢失的检查返回代码太容易了，从而掩盖了错误：\n\n```cpp\n#include <stdio.h>\n\n #include <unistd.h>\n\n #include <fcntl.h>\n\n #include <iostream>\n\n #include <fstream>\n\n char read_last_byte(const char* filename) {\n\n         char result = 0;\n\n         int fd = open(filename, O_RDONLY);\n\n         if (fd < 0) {\n\n                printf(\"Can't open file\\n\");\n\n                return -1;\n\n       } \n\n         lseek(fd, -1, SEEK_END);\n\n         size_t s = read(fd, &result, sizeof(result));\n\n         if (s != sizeof(result)) {\n\n                 printf(\"Can't read from file: %lu\\n\", s);\n\n                 close(fd);\n\n                 return -1;\n\n        } \n\n         close(fd);\n\n         return result;\n\n }\n```\n\n前面的代码至少有两个与错误处理相关的问题。 首先，不检查`lseek`函数调用的结果。 如果`lseek`返回错误，则函数将无法正常工作。 第二个问题更微妙，但更重要，也更难解决。 函数`read_last_byte`返回`-1`以指示错误，但它也是一个字节的有效值。 无法区分文件的最后一个字节是`0xFF`还是函数遇到错误。 要正确处理此情况，应按如下方式重新定义函数接口：\n\n```cpp\nint read_last_byte(const char* filename, char* result);\n```\n\n该函数在出现错误时返回`-1`，否则返回`0`。 结果存储在通过引用传递的`char`变量中。 虽然这个界面是正确的，但对开发人员来说并不像原来的界面那么方便。\n\n对于这类错误，最终随机崩溃的程序可能被认为是最好的结果。 如果它继续工作，悄悄地损坏数据或生成错误的结果，情况会更糟。\n\n除此之外，实现逻辑的代码和负责错误检查的代码是交织在一起的。 代码变得难以阅读和理解，因此更容易出错。\n\n尽管开发人员仍然可以继续使用返回代码，但在现代 C++ 中，推荐的错误处理方式是异常。 正确设计和正确使用异常可显著降低错误处理的复杂性，使代码具有可读性和健壮性。\n\n使用异常使用 C++ 编写的相同函数看起来要干净得多：\n\n```cpp\nchar read_last_byte2(const char* filename) {\n\n         char result = 0;\n\n         std::fstream file;\n\n         file.exceptions (\n\n                 std::ifstream::failbit | std::ifstream::badbit );\n\n         file.open(filename);\n\n         file.seekg(-1, file.end);\n\n         file.read(&result, sizeof(result));\n\n         return result;\n\n }\n```\n\n# 强大的标准库\n\nC++ 附带了一个功能丰富且功能强大的标准库。 许多需要 C 开发人员使用第三方库的函数现在都是标准 C++ 库的一部分。 这意味着更少的外部依赖，更稳定和可预测的行为，以及提高硬件架构之间的可移植性。\n\nC++ 标准库附带了构建在最常用数据结构(如数组、二叉树和哈希表)之上的容器。 这些容器是通用的，可以有效地满足开发人员的大部分日常需求。 开发人员不需要花费时间和精力来创建他们自己的、往往容易出错的基本数据结构实现。\n\n容器经过精心设计，最大限度地减少了对显式资源、分配或释放的需要，从而显著降低了内存或其他系统资源泄漏的可能性。\n\n标准库还提供了许多标准算法，如`find`、`sort`、`replace`、二进制搜索、集合运算和排列。 这些算法可以应用于任何公开集成器接口的容器。 与标准容器相结合，它们可以帮助开发人员专注于高级抽象，并用最少的额外代码将其构建在经过良好测试的功能之上。\n\n# 线程和内存模型作为语言规范的一部分\n\nC++ 11 标准引入了一个内存模型，该模型清楚地定义了多线程环境中 C++ 程序的行为。\n\n对于 C 语言规范，内存模型超出了范围。 该语言本身并不知道线程或并行执行语义。 这取决于第三方库(如 pthread)为多线程应用提供所有必要的支持。\n\nC++ 的早期版本遵循相同的原则。 多线程超出了语言规范的范围。 然而，具有支持指令重排序的多个流水线的现代 CPU 要求编译器的行为更具确定性。\n\n因此，现代的 C++ 规范明确定义了线程类、各种类型的锁和互斥锁、条件变量和原子变量。 这为嵌入式开发人员提供了一个强大的工具包来设计和实现能够利用现代多核 CPU 的所有功能的应用。 由于工具包是语言规范的一部分，因此这些应用具有确定性行为，并且可移植到所有支持的体系结构。\n\n# 远程部署软件\n\n嵌入式系统的软件部署通常是一个复杂的过程，应该仔细设计、实现和测试。 有两个主要挑战：\n\n*   嵌入式系统通常部署在操作员难以进入或不切实际的地方。\n*   如果软件部署失败，系统可能无法运行。 这将需要一名熟练的技术人员进行干预，并需要额外的恢复工具。 这是昂贵的，而且往往是不可能的。\n\n通过**空中**(**OTA**)更新的形式找到了连接到互联网的嵌入式系统的第一个挑战的解决方案。 系统定期连接到专用服务器并检查可用的更新。 如果找到软件的更新版本，则将其下载到设备并安装到永久存储器。\n\n这种方法被智能手机、**机顶盒**(**机顶盒**)电器、智能电视和连接到互联网的游戏机制造商广泛采用。\n\n在设计 OTA 更新时，系统架构师应该考虑影响整体解决方案可伸缩性和可靠性的许多因素。 例如，如果所有设备几乎同时检查更新，则会在更新服务器中产生较高的峰值负载，而使它们在所有其他时间都处于空闲状态。 随机化检查时间可以使负载均匀分布。 目标系统应设计为保留足够的永久内存，以便在应用更新映像之前下载完整的更新映像。 实现更新的软件映像下载的代码应该处理网络连接中断，并在连接恢复后恢复下载，而不是重新开始。 OTA 更新的另一个重要因素是安全性。 更新过程应该只接受正版更新图像。 更新由制造商进行加密签名，除非签名匹配，否则设备上运行的安装程序不会接受映像。\n\n嵌入式系统的开发人员意识到，更新可能会因为不同的原因而失败；例如，更新过程中停电。 即使更新成功完成，新版本的软件也可能不稳定并在启动时崩溃。 预计即使在这种情况下，系统也能够恢复。\n\n这是通过分离主要软件组件和引导加载程序来实现的。 引导加载器验证主要组件的一致性，例如操作系统内核和包含所有可执行文件、数据和脚本的根文件系统。 然后，它尝试运行操作系统。 如果出现故障，它将切换到以前的版本，该版本应该与新版本一起保存在永久存储器中。 硬件看门狗计时器用于检测和防止软件更新导致系统挂起的情况。\n\n在软件开发和测试期间使用 OTA 或完全镜像重新刷新是不切实际的。 它大大减慢了开发过程。 工程师使用其他方式将他们的软件版本部署到开发系统，例如远程外壳或网络文件系统，允许在开发人员的工作站和目标板之间共享文件。\n\n# 远程运行软件\n\n嵌入式系统被设计为使用硬件和软件组件的特定组合来解决特定问题。 这就是为什么系统中的所有软件组件都是为实现这一目标而量身定做的。 所有不必要的功能都会被禁用，所有自定义软件都会集成到引导序列中。\n\n用户不启动嵌入式程序；它们在系统引导时启动。 但是，在开发过程中，工程师需要在不重新启动系统的情况下运行他们的应用。\n\n根据目标平台的类型，执行此操作的方式不同。 对于功能强大的基于 SoC 并运行抢占式多任务操作系统(如 Linux)的系统，可以使用远程 shell 来完成。\n\n现代系统通常使用**安全外壳**(**SSH**)作为远程外壳。 目标系统运行 SSH 守护程序，等待传入连接。 开发人员使用客户端 SSH 程序(如 Linux 中的 SSH 或 Windows 中的 PuTTY)进行连接，以访问目标系统。 一旦连接，它们就可以像在本地计算机上一样使用嵌入式主板上的 Linux shell。\n\n远程运行该程序的常见工作流程如下：\n\n1.  使用交叉编译工具包在本地系统中构建可执行程序。\n2.  使用`scp`工具将其复制到远程系统。\n\n3.  使用 SSH 连接到远程系统，并从命令行运行可执行文件。\n4.  使用相同的 SSH 连接，分析程序输出。\n5.  当程序终止或被开发人员中断时，将其日志取回到开发人员的工作站进行深入分析。\n\nMCU 没有足够的资源用于远程外壳。 开发人员通常将编译后的代码直接上传到平台内存中，并从特定的内存地址启动代码执行。\n\n# 日志记录和诊断\n\n日志记录和诊断是任何嵌入式项目的一个重要方面。\n\n在许多情况下，使用交互式调试器是不可能或不实用的。 硬件状态可能在几毫秒内发生变化。 程序在断点处停止后，开发人员没有足够的时间对其进行分析。 对于高性能、多线程、时间敏感的嵌入式系统，收集详细的日志数据并使用工具进行分析和可视化是一种更好的方法。\n\n因为在大多数情况下资源是有限的，所以开发人员经常不得不做出权衡。 一方面，他们需要收集尽可能多的数据来确定故障的根本原因-无论是软件还是硬件、故障时硬件组件的状态以及系统处理的硬件和软件事件的准确计时。 另一方面，可用于日志的空间是有限的，每次写入日志都会影响整体性能。\n\n解决方案是在设备上本地缓冲日志数据，然后将其发送到远程系统进行详细分析。\n\n这种方法适用于嵌入式软件的开发。 然而，已部署系统的诊断需要更复杂的技术。\n\n许多嵌入式系统离线工作，无法方便地访问内部日志。 开发人员需要仔细设计和实现其他诊断和报告方式。 如果系统没有显示器，则通常使用 LED 指示灯或嘟嘟声来编码各种错误情况。 它们足以提供有关故障类别的信息，但在大多数情况下无法提供必要的详细信息来确定根本原因。\n\n嵌入式设备具有用于测试硬件组件的专用诊断模式。 通电后，几乎任何设备或设备都会执行**开机自检**(**POST**)，这会对硬件运行快速测试。 这些测试应该是快速的，并不涵盖所有测试场景。 这就是为什么许多设备都有隐藏的**服务模式**，开发者或现场工程师可以激活这些模式来执行更彻底的测试。\n\n# 简略的 / 概括的 / 简易判罪的 / 简易的\n\n在本章中，我们讨论了嵌入式软件的高级概述，以及它的不同之处，并了解了为什么以及如何在这一领域有效地使用 C++。"
  },
  {
    "path": "docs/emb-prog-mod-cpp-cb/02.md",
    "content": "# 二、设置环境\n\n要开始使用嵌入式系统，我们需要设置一个环境。 与我们用于桌面开发的环境不同，嵌入式编程环境需要两个系统：\n\n*   **构建系统**：用于编写代码的系统\n*   **目标系统**：您的代码将在其上运行的系统\n\n在本章中，我们将学习如何设置这两个系统并将它们连接在一起。 构建系统的配置可能有很大差异-可能有不同的操作系统、编译器和 IDE。 目标系统配置的差异甚至更大，因为每个嵌入式系统都是独一无二的。 此外，虽然您可以使用笔记本电脑或台式机作为构建系统，但您确实需要某种类型的嵌入式主板作为目标系统。\n\n要涵盖构建系统和目标系统的所有可能组合是不可能的。 相反，我们将只学习如何使用一种流行的配置：\n\n*   Ubuntu 18.04 作为构建系统\n*   以树莓派为目标系统\n\n我们将使用 Docker 在您的笔记本电脑或台式机上的虚拟环境中运行 Ubuntu。 Docker 支持 Windows、MacOS 和 Linux，但是，如果您已经在使用 Linux，您可以直接使用它，而无需在其上运行容器。\n\n我们将使用**Quick 模拟器**(**QEMU**)**来模拟 Raspberry PI 板。 这将教会我们如何在没有访问真实硬件的情况下为嵌入式主板构建应用。 考虑到在软件开发开始时目标硬件可能不可用，在仿真环境中执行开发的初始阶段是常见的，在许多情况下，这是唯一可能的实际解决方案。**\n\n **本章将介绍以下主题：\n\n*   在 Docker 容器中设置构建系统\n*   使用仿真器\n*   交叉编译\n*   连接到嵌入式系统\n*   调试嵌入式应用\n*   使用 gdbserver 进行远程调试\n*   使用 CMake 作为构建系统\n\n# 在 Docker 容器中设置构建系统\n\n在本食谱中，我们将设置一个 Docker 容器，以便在您的台式机或笔记本电脑上运行 Ubuntu 18.04。 您的计算机上运行什么操作系统并不重要，因为 Docker 支持 Windows、MacOS 和 Linux。 作为这个菜谱的结果，您将拥有一个在您的主机操作系统中运行的统一的、虚拟化的 Ubuntu Linux 构建系统。\n\n如果您的操作系统已经运行了 Ubuntu Linux，请随意跳到下一个食谱。\n\n# 怎么做……\n\n我们将在笔记本电脑或台式机上安装 Docker 应用，然后使用现成的 Ubuntu 镜像在虚拟环境中运行该操作系统：\n\n1.  在您的 Web 浏览器中，打开以下链接并按照说明为您的操作系统设置 Docker：\n2.  适用于 Windows：[https://docs.docker.com/docker-for-windows/install/](https://docs.docker.com/docker-for-windows/install/)\n3.  对于 MacOS：[https://docs.docker.com/docker-for-mac/install/](https://docs.docker.com/docker-for-mac/install/)\n4.  打开终端窗口(Windows 中的命令提示符，MacOS 中的终端应用)并运行以下命令以检查是否已正确安装：\n\n```cpp\n $ docker --version\n```\n\n5.  运行以下命令以使用 Ubuntu 映像：\n\n```cpp\n$ docker pull ubuntu:bionic\n```\n\n6.  创建工作目录。 在 MacOS、Linux Shell 或 Windows PowerShell 中，运行以下命令：\n\n```cpp\n $ mkdir ~/test \n```\n\n7.  现在，在容器中运行下载的镜像：\n\n```cpp\n$ docker run -ti -v $HOME/test:/mnt ubuntu:bionic\n```\n\n8.  接下来，运行`uname -a`命令以获取有关系统的信息：\n\n```cpp\n# uname -a\n```\n\n您现在处于一个虚拟的 Linux 环境中，我们将在本书的后续菜谱中使用该环境。\n\n# 它是如何运作的..。\n\n在第一步中，我们安装 Docker-一个虚拟化环境，允许独立的 Linux 操作系统在 Windows、MacOS 或 Linux 上运行。 这是一种分发和部署容器的便捷方式，容器统一封装您使用的任何操作系统所需的所有库和程序。\n\n安装 Docker 后，运行快速命令检查是否安装正确：\n\n![](img/79bd5436-f274-476e-b093-973afff3e233.png)\n\n检查安装后，我们需要从 Docker 存储库中获取现成的 Ubuntu 镜像。 Docker 镜像有标签；我们可以使用`bionic`标签查找 Ubuntu 版本 18.04：\n\n![](img/5d3e8e8d-05b8-4ae5-b232-5a85936edf50.png)\n\n下载图像需要时间。 获取映像后，我们可以创建一个目录，用于开发。 目录内容将在您的操作系统和运行在 Docker 中的 Linux 之间共享。 这样，您可以使用您喜欢的文本编辑器处理代码，但仍然可以使用 Linux 构建工具将代码编译成二进制可执行文件。\n\n然后，我们可以使用步骤 4 中获取的 Ubuntu 镜像启动 Docker 容器。`option -v $HOME/test:/mnt`命令行使在步骤 5 中创建的文件夹对 Ubuntu 可见为`/mnt`目录。 这意味着您在`~/test`目录中创建的所有文件都会自动出现在`/mnt`中。 `-ti`选项使容器具有交互性，使您可以访问 Linux shell 环境(Bash)：\n\n![](img/4702bc3f-9e14-4604-9fd1-aa4c28db0b03.png)\n\n最后，我们对`. uname`容器运行快速健全性检查，该检查显示有关 Linux 内核的信息，如下所示：\n\n![](img/d836a658-c6fa-4c21-bfd5-ce05d6bb5e53.png)\n\n尽管您的内核的确切版本可能不同，但我们可以看到我们运行的是 Linux，我们的体系结构是`x86`。 这意味着我们已经设置了构建环境，在那里我们将能够以统一的方式编译我们的代码，无论我们的计算机上运行什么操作系统。 但是，我们仍然不能运行编译后的代码，因为我们的目标架构是**Acorn RISC Machines**(**ARM**)，而不是`x86`。 在下一个食谱中，我们将学习如何设置模拟的 ARM 环境。\n\n# 还有更多的..。\n\nDocker 是一个功能强大且灵活的系统。 此外，它的存储库包含大量现成的图像，其中包含对大多数开发人员有用的工具。\n\n转到[https://hub.docker.com/search?q=&TYPE=IMAGE](https://hub.docker.com/search?q=&type=image)，浏览最受欢迎的图片。 您还可以使用关键字搜索图像，例如*Embedded*。\n\n# 使用仿真器\n\n使用真正的嵌入式电路板并不总是可行或实用的-硬件还没有准备好，或者电路板的数量有限。 仿真器帮助开发人员使用尽可能接近目标系统的环境，但不依赖于硬件可用性。 这也是开始学习嵌入式开发的最佳方式。\n\n在本食谱中，我们将学习如何设置 QEMU(硬件仿真器)并将其配置为模拟运行 Debian Linux 的基于 ARM 的嵌入式系统。\n\n# 怎么做……\n\n我们需要一个虚拟环境，与 Docker 不同，它可以用与我们计算机的体系结构不同的体系结构来模拟处理器：\n\n1.  导航到[https://www.qemu.org/download/](https://www.qemu.org/download/)，单击与您的操作系统(Linux、MacOS 或 Windows)匹配的选项卡，然后按照安装说明进行操作。\n2.  除非已存在测试目录，否则请创建一个测试目录：\n\n```cpp\n $ mkdir -p $HOME/raspberry\n```\n\n3.  下载以下文件并将其复制到您在上一步中创建的`~/raspberry`目录：\n    *   **Raspbian Lite zip-archive**：[http：//downloads.raspbercrypi.org/raspbian_lite/image/raspbian_lite-2019-07-12/2019-07-10-raspbian-buster-lite.zip](http://downloads.raspberrypi.org/raspbian_lite/img/raspbian_lite-2019-07-12/2019-07-10-raspbian-buster-lite.zip)\n    *   **内核映像**e：[HTTPS：//github.com/dhruvvyas90/qemu-rpi-kernel/raw/master/kernel-qemu-4.14.79-stretch](https://github.com/dhruvvyas90/qemu-rpi-kernel/raw/master/kernel-qemu-4.14.79-stretch)\n    *   **设备树 Blob**：[https://github.com/dhruvvyas90/qemu-rpi-kernel/raw/master/versatile-pb.dtb](https://github.com/dhruvvyas90/qemu-rpi-kernel/raw/master/versatile-pb.dtb)\n\n4.  将目录更改为`~/raspberry`并解压上一步下载的 Raspbian Lite zip 压缩文件。 它包含名为 `2019-07-10-raspbian-buster-lite.img`的单个文件。\n5.  打开终端窗口并运行 QEMU。 对于 Windows 和 Linux，命令行如下所示：\n\n```cpp\n$ qemu-system-arm -M versatilepb -dtb versatile-pb.dtb -cpu arm1176 -kernel kernel-qemu-4.14.79-stretch -m 256 -drive file=2019-07-10-raspbian-buster-lite.img,format=raw -append \"rw console=ttyAMA0 rootfstype=ext4 root=/dev/sda2 loglevel=8\" -net user,hostfwd=tcp::22023-:22,hostfwd=tcp::9090-:9090 -net nic -serial stdio\n```\n\n6.  应该会出现一个新窗口，显示 Linux 引导过程。 几秒钟后，将显示登录提示。\n7.  使用`pi`作为用户名，`raspberry`作为密码登录。 然后，键入以下命令：\n\n```cpp\n # uname -a\n```\n\n8.  检查命令的输出。 这表明我们的系统架构是`ARM`，而不是`x86`。 现在，我们可以使用此环境测试为 ARM 平台构建的应用。\n\n# 它是如何运作的..。\n\n在第一步中，我们安装 QEMU 仿真器。 如果没有可加载的代码镜像，这个虚拟机就没有多大用处。 然后，我们可以获取运行 Linux 操作系统所需的三个镜像：\n\n*   **Linux 根文件系统**：包含在 Raspberry PI 设备上使用的 Raspbian Linux 的快照\n*   **Linux 内核**\n*   **设备树 blob**：包含系统硬件组件的描述\n\n获取所有图像并将其放入`~/raspberry`目录后，我们运行 QEMU，将图像的路径作为命令行参数提供。 此外，我们还配置了虚拟网络，它允许我们从本机环境连接到在虚拟环境中运行的 Linux 系统。\n\nQEMU 启动后，我们可以看到一个带有 Linux 登录提示的窗口：\n\n![](img/1681dd1f-e0a3-43b8-a1aa-dbc92d5a9f73.png)\n\n登录系统后，我们可以通过运行`uname`命令来运行快速健全性检查：\n\n![](img/23196296-b663-4e7f-9a88-e4f588196eb3.png)\n\n与我们在前面的食谱*中运行的健全性检查类似，*在 Docker 容器*中设置构建系统，这表明我们运行的是 Linux 操作系统，但在本例中，我们可以看到目标架构是`ARM`。*\n\n# 还有更多的..。\n\nQEMU 是一个功能强大的处理器仿真器，除了 x86 和 ARM 之外，它还支持其他多种架构，例如 PowerPC、SPARC64、SPARC32 和**微处理器，没有互锁的流水线级**(**MIPS**)。 它之所以如此强大的一个方面是它的灵活性，因为它有许多配置选项。 请转到[https://qemu.weilnetz.de/doc/qemu-doc.html](https://qemu.weilnetz.de/doc/qemu-doc.html)，根据需要配置 QEMU。\n\n微控制器供应商也经常提供仿真器和仿真器。 开始针对特定硬件进行开发时，请检查可用的仿真选项，因为这可能会显著影响开发时间和工作量。\n\n# 交叉编译\n\n我们已经了解到嵌入式开发环境由两个系统组成：编写和构建代码的构建系统和运行代码的主机系统。\n\n我们现在设置了两个虚拟化环境：\n\n*   Docker 容器中的 Ubuntu Linux，它将成为我们的构建系统\n*   运行 Raspbian Linux 的 QEMU，它将成为我们的主机系统\n\n在本食谱中，我们将设置为 ARM 平台构建 Linux 应用所需的交叉编译工具，并构建一个简单的*Hello，world！*应用来测试设置。\n\n# 正在做好准备\n\n要设置交叉编译工具包，我们需要使用 Ubuntu Linux，我们在*设置 Docker 容器中的构建系统*食谱中设置了 Ubuntu Linux。\n\n我们还需要`~/test`目录来在操作系统和 Ubuntu 容器之间交换源代码。\n\n# 怎么做……\n\n让我们从创建一个简单的 C++ 程序开始，我们希望为我们的目标平台编译该程序：\n\n1.  在`~/test`目录中创建名为`hello.cpp`的文件。\n2.  使用您最喜欢的文本编辑器将以下代码片段添加到其中：\n\n```cpp\n#include <iostream>\n\nint main() {\n std::cout << \"Hello, world!\" << std::endl;\n return 0;\n}\n```\n\n3.  现在我们有了`Hello, world!`程序的代码，我们需要编译它。\n4.  切换到 Ubuntu(我们的构建系统)控制台。\n5.  通过运行以下命令获取可用于安装的最新软件包列表：\n\n```cpp\n# apt update -y\n```\n\n6.  从 Ubuntu 服务器获取包描述需要一些时间。 运行以下命令安装交叉编译工具：\n\n```cpp\n # apt install -y crossbuild-essential-armel\n```\n\n7.  您将看到要安装的软件包的长长列表。 按*Y*确认安装。作为健康检查，请运行不带参数的交叉编译器：\n\n```cpp\n# arm-linux-gnueabi-g++\n```\n\n8.  将目录更改为`/mnt`\n\n```cpp\n# cd /mnt\n```\n\n9.  我们在步骤 1 中创建的`hello.cpp`文件位于此处。 现在让我们构建它：\n\n```cpp\n # arm-linux-gnueabi-g++ hello.cpp -o hello\n```\n\n10.  此命令生成名为`hello`的可执行文件。 您可能会感到奇怪，为什么它没有任何扩展名。 在 UNIX 系统中，扩展是完全可选的，并且二进制可执行文件通常没有任何扩展名。 尝试运行该文件。 它应该会失败，并出现错误。\n11.  让我们使用`file`工具生成有关可执行二进制文件的详细信息。\n\n# 它是如何运作的..。\n\n在第一步中，我们创建了一个简单的*Hello，World！*C++ 程序。 我们将其放入`~/test`目录，这样就可以从运行 Linux 的 Docker 容器访问它。\n\n为了构建源代码，我们切换到了 Ubuntu shell。\n\n如果我们尝试运行标准的 Linux g++ 编译器来构建它，我们将得到构建平台的可执行文件，即 x86。 但是，我们需要 ARM 平台的可执行文件。 要构建它，我们需要一个可以在 x86 上运行的编译器版本，构建 ARM 代码。\n\n作为第一步，我们需要更新 Ubuntu 软件包发行版中提供的软件包信息：\n\n![](img/1973b89b-07cc-4125-a9fe-000c0969f05d.png)\n\n我们可以通过运行`apt-get install crossbuild-essential-armel`来安装该编译器以及一组相关工具：\n\n![](img/dc1bdb2c-8f41-4ec3-9073-ffd6ff23487d.png)\n\n步骤 9 中执行的快速健全性检查显示它已正确安装：\n\n![](img/2d99fc73-58e6-4651-8d87-e5bfe30c77e2.png)\n\n现在，我们需要使用交叉编译器构建`hello.cpp`。 它为 ARM 平台生成可执行文件，这就是我们在步骤 12 中尝试在构建系统中运行它失败的原因。\n\n为了确保它真的是 ARM 可执行文件，我们需要运行`file`命令。 其输出如下：\n\n![](img/7fbe1b85-47b4-4f1c-aada-898888a43b66.png)\n\n如您所见，该二进制文件是为 ARM 平台构建的，这就是它无法在构建系统上运行的原因。\n\n# 还有更多的..。\n\n存在许多适用于各种体系结构的交叉编译工具包。 其中一些可以在 Ubuntu 存储库中随时获得；有些可能需要手动安装。\n\n# 连接到嵌入式系统\n\n使用交叉编译器在构建系统上构建嵌入式应用后，应该将其传输到目标系统。 在基于 Linux 的嵌入式系统上实现这一点的最佳方式是使用网络连接和远程 shell。 **安全外壳**(**SSH**)因其安全性和通用性而被广泛使用。 它不仅允许您在远程主机上运行 shell 命令，还允许您使用加密加密和基于密钥的身份验证将文件从一台计算机复制到另一台计算机。\n\n在本食谱中，我们将学习如何使用 Secure Copy 将应用二进制文件复制到模拟的 ARM 系统，使用 SSH 连接到它，并在 SSH 中运行可执行文件。\n\n# 正在做好准备\n\n我们将使用我们在*使用仿真器*配方中设置的 Raspberry PI 仿真器作为我们的目标系统。 此外，我们还需要我们的 Ubuntu 构建系统和我们在*交叉编译*配方中构建的可执行文件`hello`。\n\n# 怎么做……\n\n我们将通过网络访问我们的目标系统。 QEMU 为仿真机器提供了一个虚拟网络接口，我们可以在不连接到真实网络的情况下使用它。 为此，我们需要找出要使用的 IP 地址，并确保 SSH 服务器在我们的虚拟环境中运行：\n\n在您的本机操作系统环境中，计算出您的计算机的 IP 地址。 打开终端窗口或 PowerShell。 在 MacOS 或 Linux 上运行`ifconfig`，或在 Windows 上运行`ipconfig`，并检查其输出。\n\n在接下来的步骤中，我们将使用`192.168.1.5`作为模板 IP 地址；您需要将其替换为您的实际 IP 地址。\n\n1.  通过运行以下命令切换到 Raspberry PI 仿真器并启用 SSH 服务：\n\n```cpp\n$ sudo systemctl start ssh\n```\n\n2.  切换到 Ubuntu 窗口，安装 SSH 客户端：\n\n```cpp\n# apt install -y ssh\n```\n\n3.  现在，我们可以将`hello`可执行文件复制到目标系统：\n\n```cpp\n# scp -P22023 /mnt/hello pi@192.168.1.5:~\n```\n\n4.  当要求输入密码时，键入`raspberry`。 切换回 Raspberry PI 仿真器窗口。 检查我们刚刚复制的可执行文件是否存在：\n\n```cpp\n$ ls hello\nhello\n```\n\n5.  现在，运行该程序：\n\n```cpp\n$ ./hello\n```\n\n正如我们所看到的，程序现在正在按预期运行。\n\n# 它是如何运作的..。\n\n在本配方中，我们使用 SSH 在两个虚拟环境(Docker 和 QEMU)之间设置数据交换。 为此，我们需要在目标系统(QEMU)上运行并接受连接的 SSH 服务器，以及在构建系统上启动连接的 SSH 客户端。\n\n在步骤 2 中，我们在构建系统上设置 SSH 客户端。 我们的目标系统在 QEMU 中运行，已经启动并运行了一台 SSH 服务器。 在*使用仿真器*配方期间，我们将 QEMU 配置为将连接从主机端口`22023`转发到虚拟机端口`22`，即 SSH。\n\n现在，我们可以使用`scp`使用安全网络连接将文件从构建系统复制到目标系统。 我们可以将为 QEMU 转发配置的系统 IP 地址(在步骤 1 中发现)和端口`22023`指定为`scp`要连接到的参数：\n\n![](img/bc75eea7-7ccb-43ea-9e30-f3f3857a007a.png)\n\n复制文件后，我们可以使用 SSH 登录到目标系统，使用与`scp`相同的 IP 地址、端口和用户名。 它会打开一个类似于本地控制台的登录提示符，在授权之后，我们会得到与本地终端相同的命令 shell。\n\n我们在上一步中复制的`hello`应用应该在`home`目录中可用。 我们在步骤 5 中通过运行`ls`命令检查了这一点。\n\n最后，我们可以运行应用：\n\n![](img/087a7c0c-6ec1-4a7f-bcba-097249ccd47a.png)\n\n当我们尝试在构建系统上运行它时，收到一个错误。 现在，输出是`Hello, world!`。 这正是我们所期望的，因为我们的应用是为 ARM 平台构建的，并且在 ARM 平台上运行。\n\n# 还有更多的..。\n\n尽管我们运行了连接到仿真系统的配方，但同样的步骤也适用于真正的嵌入式系统。 即使目标系统没有显示器，您也可以使用串行控制台连接设置 SSH。\n\n在本配方中，我们只将文件复制到目标系统。 除了复制之外，通常的做法是打开到嵌入式系统的交互式 SSH 会话。 通常，它比串行控制台使用起来更高效、更方便。 它的建立方式类似于`scp`：\n\n```cpp\n# ssh pi@192.168.1.5 -p22023\n```\n\nSSH 提供各种身份验证机制。 启用并设置公钥身份验证后，无需为每次复制或登录都键入密码。 这为开发人员提供了更快、更方便的开发过程。\n\n要了解有关 ss 键的更多信息，请访问[https://www.ssh.com/ssh/key/](https://www.ssh.com/ssh/key/)。\n\n# 调试嵌入式应用\n\n调试嵌入式应用在很大程度上取决于目标嵌入式系统的类型。 微控制器制造商通常为其**微控制器单元**(**MCU**)提供专用调试器，并使用**联合测试行动小组**(**JTAG**)协议为远程调试提供硬件支持。 它允许开发人员在 MCU 开始执行指令后立即调试微控制器代码。\n\n如果目标板运行 Linux，最实用的调试方法是使用广泛的调试输出，并使用 gdb 作为交互式调试器。\n\n在本食谱中，我们将学习如何在命令行调试器 gdb 中运行我们的应用。\n\n# 正在做好准备\n\n我们已经了解了如何将可执行文件传输到目标系统。 我们将使用*连接到嵌入式系统*配方作为起点，学习如何在目标系统上使用调试器。\n\n# 怎么做……\n\n我们已经学习了如何将应用复制到目标系统并在那里运行。 现在，让我们学习如何使用 gdb 开始在目标系统上调试应用。 在本菜谱中，我们将只学习如何调用调试器并在调试器环境中运行应用。 稍后将作为更高级、更实用的调试技术的基础：\n\n1.  切换到`QEMU`窗口。\n2.  如果您尚未登录，请使用`pi`作为用户名，使用`raspberry`作为密码进行登录。\n3.  运行以下命令：\n\n```cpp\n$ gdb ./hello\n```\n\n4.  这将打开`gdb`命令行。\n5.  键入`run`以运行应用：\n\n```cpp\n(gdb) run\n```\n\n5.  您应该在输出中看到`Hello, world`。\n6.  现在，运行`quit`命令，或者只运行`q`：\n\n```cpp\n(gdb) q\n```\n\n这将终止调试会话，并将我们返回到 Linux shell。\n\n# 它是如何运作的..。\n\n我们用于仿真的 Raspberry PI 映像附带了一个预装的 GNU 调试器，因此我们可以立即使用它。\n\n在`home`用户目录中，我们应该找到`hello`可执行文件，它是作为连接到嵌入式系统配方的*的一部分从我们的构建系统复制的。*\n\n我们运行`gdb`，将路径作为参数传递给`hello`可执行文件。 此命令打开`gdb`外壳，但不运行应用本身。 要运行它，我们键入`run`命令：\n\n![](img/7730a5e6-f9ba-4ae2-99c8-d775757b5df3.png)\n\n应用运行，在屏幕上打印`Hello world!`消息，然后终止。 但是，我们仍在调试器中。 要退出调试器，我们键入`quit`命令：\n\n![](img/4c14581e-4665-4e97-9cc1-b58a262cd717.png)\n\n您可以看到命令行提示符已更改。 这表明我们不再处于`gdb`环境中。 我们已经返回到 Raspberry PI Linux 的默认 shell 环境，我们在运行 gdb 之前使用该环境。\n\n# 还有更多的..。\n\n在这种情况下，预先安装了 GNU 调试器，但它可能不在您的实际目标系统中。 如果它是基于 Debian 的，您可以通过运行以下命令来安装它：\n\n```cpp\n# apt install gdb gdb-multiarch\n```\n\n在其他基于 Linux 的系统中，安装 gdb 需要不同的命令。 在许多情况下，您需要从源代码构建它并手动安装，类似于我们作为本章食谱的一部分构建和测试的`hello`应用。\n\n在本文中，我们只学习了如何使用 gdb 运行应用，gdb 是一个包含大量命令、技术和最佳实践的复杂工具。 我们将在[第 5 章](05.html)，*调试、日志记录和性能分析*中讨论其中的一些内容。\n\n# 使用 gdbserver 进行远程调试\n\n正如我们已经讨论过的，嵌入式开发环境通常包括两个系统-构建系统和目标系统(或仿真器)。 有时，由于远程通信的高延时，在目标系统上进行交互式调试是不切实际的。\n\n在这种情况下，开发人员可以使用 GDB 提供的远程调试支持。 在此设置中，使用**gdbserver**在目标系统上启动嵌入式应用。 开发人员在构建系统上运行 gdb，并通过网络连接到 gdbserver。\n\n在本食谱中，我们将学习如何使用 gdb 和 gdbserver 开始调试应用。\n\n# 正在做好准备\n\n在*连接到嵌入式系统*秘诀中，我们了解了如何使我们的应用在目标系统上可用。 我们将使用该配方作为学习远程调试技术的起点。\n\n# 怎么做……\n\n我们将安装并运行 gdbserver 应用，这将允许我们在构建系统上运行 gdb，并将所有命令转发到目标系统。 切换到 Raspberry PI 仿真器窗口。\n\n1.  使用`raspberry`密码以`pi`身份登录，除非您已经登录。\n2.  要安装 gdbserver，请运行以下命令：\n\n```cpp\n # sudo apt-get install gdbserver\n```\n\n3.  在`gdbserver`下运行`hello`应用：\n\n```cpp\n $ gdbserver 0.0.0.0:9090 ./hello\n```\n\n4.  切换到构建系统终端，将目录切换到`/mnt/hello`：\n\n```cpp\n # cd /mnt/hello\n```\n\n5.  安装`gdb-multiarch`软件包，该软件包为 ARM 平台提供必要的支持：\n\n```cpp\n # apt install -y gdb-multiarch\n```\n\n6.  接下来，运行`gdb`：\n\n```cpp\n # gdb-multiarch -q ./hello\n```\n\n7.  通过在`gdb`命令行中键入以下命令来配置远程连接(请确保将`192.168.1.5`替换为您的实际 IP 地址)：\n\n```cpp\n target remote 192.168.1.5:9090\n```\n\n8.  键入以下命令：\n\n```cpp\n continue\n```\n\n程序现在将运行。\n\n# 它是如何运作的..。\n\n在我们使用的 Raspberry PI 镜像中，默认情况下没有安装`gdbserver`。 因此，作为第一步，我们安装`gdbserver`：\n\n![](img/09463e0a-5205-4696-9876-eccdbfae92b5.png)\n\n安装完成后，我们运行`gdbserver`，传递需要调试的应用的名称、IP 地址和侦听传入连接的端口作为其参数。 我们使用`0.0.0.0`作为 IP 地址，表示我们希望接受任何 IP 地址上的连接：\n\n![](img/0dbebcae-bc8b-4787-8b91-684276dde525.png)\n\n然后，我们切换到构建系统并在那里运行`gdb`。 但是，我们没有直接在 gdb 中运行应用，而是指示`gdb`使用提供的 IP 地址和端口启动到远程主机的连接：\n\n![](img/4a3f076e-ed50-4130-8ebf-c13b191c319b.png)\n\n之后，您在`gdb`提示符下键入的所有命令都将传输到 gdbserver 并在那里执行。 当我们运行应用时，我们将在构建系统的`gdb`控制台中看到结果输出，即使我们运行 ARM 可执行文件：\n\n![](img/c6f43a8b-2667-4422-ad15-e67e910f560f.png)\n\n解释很简单--二进制文件在远程 ARM 系统上运行：我们的 Raspberry PI 仿真器。 这是在目标平台上调试应用的一种便捷方式，使您可以保持在构建系统更舒适的环境中。\n\n# 还有更多的..。\n\n确保您正在使用的 gdb 和 gdbserver 的版本匹配，否则它们之间的通信可能会出现问题。\n\n# 使用 CMake 作为构建系统\n\n在前面的食谱中，我们学习了如何编译由一个 C++ 文件组成的程序。 然而，真正的应用通常具有更复杂的结构。 它们可以包含多个源文件，依赖于其他库，并且可以拆分成独立的项目。\n\n我们需要一种方法来方便地为任何类型的应用定义构建规则。 CMake 是最广为人知、使用最广泛的工具之一，它允许开发人员定义高级规则并将其转换为较低级别的构建系统，如 Unix Make。\n\n在本食谱中，我们将学习如何设置 CMake 并为我们的*Hello，world！*应用创建一个简单的项目定义。\n\n# 正在做好准备\n\n如前所述，常见的嵌入式开发工作流包括两个环境：构建系统和目标系统。 CMake 是构建系统的一部分。 我们将使用 Ubuntu 构建系统作为起点，它是通过*在 Docker 容器*配方中设置构建系统而创建的。\n\n# 怎么做……\n\n1.  我们的构建系统还没有安装 CMake。 要安装它，请运行以下命令：\n\n```cpp\n # apt install -y cmake\n```\n\n2.  切换回您的本机操作系统环境。\n3.  在`~/test`目录中，创建子目录`hello`。 使用您喜欢的文本编辑器在`hello`子目录中创建名为`CMakeLists.txt`的文件。\n4.  输入以下行：\n\n```cpp\ncmake_minimum_required(VERSION 3.5.1)\nproject(hello)\nadd_executable(hello hello.cpp)\n```\n\n5.  保存文件并切换到 Ubuntu 控制台。\n6.  切换到`hello`目录：\n\n```cpp\n# cd /mnt/hello\n```\n\n7.  运行 CMake：\n\n```cpp\n # mkdir build && cd build && cmake ..\n```\n\n8.  现在，通过运行以下命令构建应用：\n\n```cpp\n# make\n```\n\n9.  使用`file`命令获取有关生成的可执行二进制文件的信息：\n\n```cpp\n# file hello\n```\n\n9.  如您所见，构建是 x86 平台的原生版本。 我们需要添加交叉编译支持。 切换回文本编辑器，打开`CMakeLists.txt`，并添加以下行：\n\n```cpp\nset(CMAKE_C_COMPILER /usr/bin/arm-linux-gnueabi-gcc)\nset(CMAKE_CXX_COMPILER /usr/bin/arm-linux-gnueabi-g++)\nset(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)\nset(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)\nset(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)\nset(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)\n```\n\n10.  保存并切换到 Ubuntu 终端。\n11.  再次运行`cmake`命令以重新生成构建文件：\n\n```cpp\n# cmake ..\n```\n\n12.  通过运行`make`构建代码：\n\n```cpp\n# make\n```\n\n13.  再次检查结果输出文件的类型：\n\n```cpp\n# file hello\n```\n\n现在，我们已经使用 CMake 为我们的目标系统构建了一个可执行文件。\n\n# 它是如何运作的..。\n\n首先，我们在构建系统中安装 CMake。 安装完成后，我们切换到本机环境来创建`CMakeLists.txt`。 此文件包含有关项目组成和属性的高级生成说明。\n\n我们将项目命名为*hello*，它从名为`hello.cpp`*的源文件创建名为`hello`的可执行文件。* 此外，我们还指定了构建应用所需的 CMake 的最低版本。\n\n在我们创建了项目定义之后，我们可以切换回构建系统外壳，并通过运行`make`来生成低级构建指令。\n\n通常的做法是创建一个专用的构建目录来保存我们所有的构建构件。 通过这样做，编译器生成的目标文件或 CMake 生成的文件不会污染源代码目录。\n\n在单个命令行中，我们创建一个`build`目录，切换到新创建的目录，然后运行 CMake。\n\n我们将父目录作为参数传递，让 CMake 知道在哪里查找`CMakeListst.txt`：\n\n![](img/14a5bf44-2185-478b-a144-94f6030053c3.png)\n\n默认情况下，CMake 为传统的 Unix`make`实用程序生成`Makefile`文件。 我们运行`make` 来实际构建应用：\n\n![](img/f5983eee-a06d-4d26-9441-8274013cf7be.png)\n\n它可以工作，但会生成为 x86 平台构建的可执行二进制文件，而我们的目标系统是 ARM：\n\n![](img/2f2aedf6-d441-4e80-a11a-4778c32d273e.png)\n\n为了解决这个问题，我们在`CMakeLists.txt`文件中添加了几个选项来配置交叉编译。 再次重复构建步骤，我们得到一个新的`hello`二进制文件，现在用于 ARM 平台：\n\n![](img/cfdf7699-32f3-41bf-a3fe-54adbf11e341.png)\n\n正如我们在`file`命令的输出中看到的那样，我们构建的是 ARM 平台的可执行文件，而不是用作构建平台的 x86。 这意味着该程序不会在构建机器上运行，但可以成功地复制到我们的目标平台上并在那里运行。\n\n# 还有更多的..。\n\n为 CMake 配置交叉编译的最佳方式是使用所谓的**工具链**文件。 工具链文件定义特定于特定目标平台的构建规则的所有设置和参数，例如编译器前缀、编译标志和在目标平台上预先构建的库的位置。 可以使用不同的工具链文件为不同的目标平台重建应用。 有关详细信息，请参阅[https://cmake.org/cmake/help/v3.6/manual/cmake-toolchains.7.html](https://cmake.org/cmake/help/v3.6/manual/cmake-toolchains.7.html)上的 Cmake 工具链文档。**"
  },
  {
    "path": "docs/emb-prog-mod-cpp-cb/03.md",
    "content": "# 三、使用不同的架构\n\n桌面应用的开发人员通常很少关注硬件架构。 首先，它们通常使用高级编程语言，以牺牲性能为代价隐藏这些复杂性。 其次，在大多数情况下，他们的代码运行在 x86 架构上，他们通常认为 x86 架构的特性是理所当然的。 例如，它们可能假设 int 的大小为 32 位，但在许多情况下并非如此。\n\n嵌入式开发人员需要处理更多种类的体系结构。 即使他们不是用目标平台原生的汇编语言编写代码，他们也应该知道所有的 C 和 C++ 基础类型都是依赖于体系结构的；标准只保证 int 至少是 16 位的。 他们还应该了解特定体系结构的特点，如字符顺序和对齐方式，并考虑到使用浮点或 64 位数字执行的操作在 x86 体系结构上相对便宜，在其他体系结构上可能要昂贵得多。\n\n由于他们的目标是从嵌入式硬件获得最大可能的性能，因此他们应该了解如何在内存中组织数据，以便最有效地利用 CPU 缓存和操作系统分页机制。\n\n在本章中，我们将介绍以下主题：\n\n*   探索固定宽度整数类型\n*   使用`size_t`类型\n*   检测平台的字节顺序\n*   转换字符顺序\n*   使用数据对齐\n*   使用填充结构\n*   将数据与高速缓存线对齐\n\n通过了解这些主题，我们将了解如何针对目标平台定制代码，以实现最高性能和可移植性。\n\n# 探索固定宽度整数类型\n\nC 和 C++ 开发人员经常忘记，基本数据类型(如 char、Short 和 int)的大小取决于体系结构。 同时，大多数硬件外设定义了关于用于数据交换的字段大小的具体要求。 为了使使用外部硬件或通信协议的代码可移植，嵌入式开发人员使用固定大小的整数类型，它显式指定数据字段的大小。\n\n一些最常用的数据类型如下所示：\n\n| **宽度** | **签名** | **无符号** |\n| 8 位 | `int8_t` | `uint8_t` |\n| 16 位 | `int16_t` | `uint16_t` |\n| 32 位 | `int32_t` | `uint32_t` |\n\n指针大小还取决于体系结构。 开发人员通常需要寻址数组的元素，由于数组在内部表示为指针，因此偏移量表示取决于指针的大小。 `size_t`是一种特殊的数据类型，因为它以独立于体系结构的方式表示偏移量和数据大小。\n\n在本食谱中，我们将学习如何在代码中使用固定大小的数据类型，使其可以跨体系结构移植。 这样，我们可以使我们的应用更快地与其他目标平台协同工作，并且只需更少的代码修改。\n\n# 怎么做……\n\n我们将创建一个模拟与外部设备进行数据交换的应用。 要执行此操作，请执行以下步骤：\n\n1.  在您的工作目录(即`~/test`)中，创建一个名为`fixed_types`的子目录。\n\n2.  使用您喜欢的文本编辑器在`fixed_types`子目录中创建名为`fixed_types.cpp`的文件。 将以下代码片段复制到`fixed_types.cpp`文件中：\n\n```cpp\n#include <iostream>\n\nvoid SendDataToDevice(void* buffer, uint32_t size) {\n  // This is a stub function to send data pointer by\n  // buffer.\n  std::cout << \"Sending data chunk of size \" << size << std::endl;\n}\n\nint main() {\n  char buffer[] = \"Hello, world!\";\n  uint32_t size = sizeof(buffer);\n  SendDataToDevice(&size, sizeof(size));\n  SendDataToDevice(buffer, size);\n  return 0;\n}\n```\n\n3.  在 loop 子目录中创建名为`CMakeLists.txt`的文件，内容如下：\n\n```cpp\ncmake_minimum_required(VERSION 3.5.1)\nproject(fixed_types)\nadd_executable(fixed_types fixed_types.cpp)\n\nset(CMAKE_SYSTEM_NAME Linux)\nset(CMAKE_SYSTEM_PROCESSOR arm)\n\nSET(CMAKE_CXX_FLAGS \"--std=c++ 11\")\nset(CMAKE_CXX_COMPILER /usr/bin/arm-linux-gnueabi-g++)\n\n```\n\n4.  构建应用并将生成的可执行二进制文件复制到目标系统。 使用[第 2 章](02.html)、*设置环境*中的食谱来实现此目的。\n5.  切换到目标系统的终端。 如果需要，请使用您的用户凭据登录。\n6.  运行二进制文件，看看它是如何工作的。\n\n# 它是如何运作的..。\n\n运行二进制文件时，您将看到以下输出：\n\n![](img/b2a222c6-a006-4c01-ad57-fa2463d6e63f.png)\n\n在这个简单的程序中，我们模拟与外部设备的通信。 因为我们没有真正的设备，所以`SendDataToDevice`函数只打印它应该发送到目标设备的数据大小。\n\n假设该设备可以对可变大小的数据块进行操作。 每个数据块都按其大小作为前缀，并编码为 32 位无符号整数。 这可以用以下几个方面来描述：\n\n| ==同步，由 Elderman 更正==@ELDER_MAN | **有效载荷** |\n| 0-4 字节 | 5-N 字节，其中 N 为大小 |\n\n在我们的代码中，我们将`size`声明为`uint32_t`：\n\n```cpp\n  uint32_t size = sizeof(buffer);\n```\n\n这意味着它在每个平台上都需要 32 位(16 位、32 位或 64 位)。\n\n现在，我们将向设备发送大小：\n\n```cpp\n  SendDataToDevice(&size, sizeof(size));\n```\n\n`SendDataToDevice`不发送实际数据；相反，它报告要发送的数据的大小。 如我们所见，大小为`4`字节，与预期不谋而合：\n\n```cpp\n  Sending data chunk of size 4\n```\n\n假设我们声明`int`数据类型，如下所示：\n\n```cpp\n  int size = sizeof(buffer);\n```\n\n在这种情况下，此代码只能在 32 位和 64 位系统上运行，并在 16 位系统上静默产生不正确的结果，因为这里的`sizeof(int)`是 16。\n\n# 还有更多的..。\n\n我们在这个配方中实现的代码不是完全可移植的，因为它没有考虑 32 位字中的字节顺序。 这个顺序称为**字节顺序**，它的含义将在本章后面讨论。\n\n# 使用 size_t 类型\n\n指针大小还取决于体系结构。 开发人员通常需要寻址数组的元素，而且由于数组在内部表示为指针，因此偏移量表示取决于指针的大小。\n\n例如，在 32 位系统中，指针为 32 位，与`int`相同。 但是，在 64 位系统中，`int`的大小仍然是 32 位，而指针是 64 位。\n\n`size_t`是一种特殊的数据类型，因为它以独立于体系结构的方式表示偏移量和数据大小。\n\n在本食谱中，我们将学习如何在处理数组时使用`size_t`。\n\n# 怎么做……\n\n我们将创建一个处理可变大小的数据缓冲区的应用。 如果需要，我们需要能够访问目标平台提供的任何内存地址。 要执行此操作，请执行以下步骤：\n\n1.  在您的工作目录(即`~/test`)中，创建一个名为`sizet`的子目录。\n2.  使用您喜欢的文本编辑器在`sizet`子目录中创建名为`sizet.cpp`的文件。 将以下代码片段复制到`sizet.cpp`文件中：\n\n```cpp\n#include <iostream>\n\nvoid StoreData(const char* buffer, size_t size) {\n  std::cout << \"Store \" << size << \" bytes of data\" << std::endl;\n}\n\nint main() {\n  char data[] = \"Hello,\\x1b\\a\\x03world!\";\n  const char *buffer = data;\n  std::cout << \"Size of buffer pointer is \" << sizeof(buffer) << std::endl;\n  std::cout << \"Size of int is \" << sizeof(int) << std::endl;\n  std::cout << \"Size of size_t is \" << sizeof(size_t) << std::endl;\n  StoreData(data, sizeof(data));\n  return 0;\n}\n```\n\n3.  在 loop 子目录中创建名为`CMakeLists.txt`的文件，内容如下：\n\n```cpp\ncmake_minimum_required(VERSION 3.5.1)\nproject(sizet)\nadd_executable(sizet sizet.cpp)\n\nset(CMAKE_SYSTEM_NAME Linux)\nset(CMAKE_SYSTEM_PROCESSOR arm)\n\nSET(CMAKE_CXX_FLAGS \"--std=c++ 11\")\nset(CMAKE_CXX_COMPILER /usr/bin/arm-linux-gnueabi-g++)\n\n```\n\n4.  构建应用并将生成的可执行二进制文件复制到目标系统。 使用[第 2 章](02.html)、*设置环境*中的配方来实现此目的。\n5.  切换到目标系统的终端。 如果需要，请使用您的用户凭据登录。\n6.  运行`sizet`应用可执行文件。\n\n# 它是如何运作的..。\n\n在本例中，我们模拟了一个在文件或数据库中存储任意数据的函数。 该函数接受指向数据和数据大小的指针。 但是我们应该用什么类型来表示尺寸呢？ 如果我们在 64 位系统中使用无符号整型，我们就人为地限制了函数只能处理最多 4 GB 数据的能力。\n\n为避免此类限制，我们使用`size_t`作为`size`的数据类型：\n\n```cpp\nvoid StoreData(const char* buffer, size_t size) {\n```\n\n大多数接受索引和大小的标准库 API 还处理`size_t`参数。 例如，将数据块从源缓冲区复制到目标缓冲区的`memcpy`C 函数声明如下：\n\n```cpp\nvoid *memset(void *b, int c, size_t len);\n```\n\n运行前面的代码会产生以下输出：\n\n![](img/79d7eb07-66fc-465b-b089-36506a38e18d.png)\n\n正如我们所看到的，目标系统上的指针大小是 64 位，尽管`int`的大小是 32 位。 在我们的程序中使用`size_t`允许它使用嵌入式电路板的所有内存。\n\n# 还有更多的..。\n\nC++ 标准定义了`std::size_t`类型。 它与普通 C`size_t`相同，只是它是在`std`名称空间中定义的。 在您的 C++ 代码中最好使用`std::size_t`，因为它是标准的一部分，但是`std::size_t`和`size_t`都是可以互换的。\n\n# 检测平台的字节顺序\n\n字节顺序定义表示大数值的字节在内存中的存储顺序。\n\n有两种类型的字符顺序：\n\n*   **Big-Endian**：首先存储最高有效字节。 32 位值*0x01020304*存储在`ptr`地址， 具体如下：\n\n    | **内存中的偏移量(字节)** | **值** |\n    | Ptr | 0x01 |\n    | Ptr+1。 | 0x02 0x02 |\n    | Ptr+2 | OX03 |\n    | Ptr+3 | 0x04 |\n\n    大端架构的示例包括 AVR32 和摩托罗拉 68000。\n\n*   **Little-Endian**：首先存储最低有效字节。 32 位值*0x01020304*存储在`ptr`地址， 具体如下：\n\n    | **内存中的偏移量(字节)** | **值** |\n    | Ptr | 0x04 |\n    | Ptr+1。 | 0x03 0x03 |\n    | Ptr+2 | 0x02 0x02 |\n    | Ptr+3 | 0x01 |\n\n    x86 架构是小端的。\n\n在与其他系统交换数据时，特别需要注意字符顺序。 如果开发人员发送一个 32 位整数，比如 0x01020304，如果接收方的字符顺序与发送方的字符顺序不匹配，则可能会将其读取为 0x04030201。 这就是数据应该序列化的原因。\n\n在本食谱中，我们将学习如何确定目标系统的字节顺序。\n\n# 怎么做……\n\n我们将创建一个简单的程序来检测目标平台的字节顺序。 要执行此操作，请执行以下步骤：\n\n1.  在您的工作目录(即`~/test`)中，创建一个名为`endianness`的子目录。\n2.  使用您喜欢的文本编辑器在 loop 子目录中创建一个名为`loop.cpp`的文件。 将以下代码片段复制到`endianness.cpp`文件中：\n\n```cpp\n#include <iostream>\n\nint main() {\n  union {\n    uint32_t i;\n    uint8_t c[4];\n  } data;\n  data.i = 0x01020304;\n  if (data.c[0] == 0x01) {\n    std::cout << \"Big-endian\" << std::endl;\n  } else {\n    std::cout << \"Little-endian\" << std::endl;\n  }\n}\n```\n\n3.  在 loop 子目录中创建名为`CMakeLists.txt`的文件，内容如下：\n\n```cpp\ncmake_minimum_required(VERSION 3.5.1)\nproject(endianness)\nadd_executable(endianness endianness.cpp)\n\nset(CMAKE_SYSTEM_NAME Linux)\nset(CMAKE_SYSTEM_PROCESSOR arm)\n\nSET(CMAKE_CXX_FLAGS \"--std=c++ 11\")\nset(CMAKE_CXX_COMPILER /usr/bin/arm-linux-gnueabi-g++)\n\n```\n\n4.  构建应用并将生成的可执行二进制文件复制到目标系统。 使用[第 2 章](02.html)、*设置环境*中的配方来实现此目的。\n5.  切换到目标系统的终端。 如果需要，请使用您的用户凭据登录。\n6.  运行二进制文件。\n\n# 它是如何运作的..。\n\n在这个配方中，我们利用 C 的`union`函数的功能将不同数据类型的表示映射到相同的内存空间。\n\n我们定义了一个具有两个数据字段的联合-一个 8 位整数数组和一个 32 位整数数组。 这些数据字段共享相同的内存，因此在一个字段中所做的更改会自动反映在另一个字段中：\n\n```cpp\n  union {\n    uint32_t i;\n    uint8_t c[4];\n  } data\n```\n\n接下来，我们为 32 位整数字段分配一个精心编制的值，其中每个字节都是预先知道的，并且与其他字节不同。 我们使用值为 1、2、3 和 4 的字节组成目标值。\n\n将该值分配给 32 位字段`i`时，它会自动将所有字段重写为`c`字节数组字段。 现在，我们可以读取数组的第一个元素，根据读取的内容，我们可以推断硬件平台的字节顺序。\n\n如果值为 1，这意味着第一个字节包含最高有效字节，因此体系结构是大端的。 否则，它是小端的。 当我们运行二进制文件时，它会产生以下输出：\n\n![](img/dca32ea5-56bc-4cb2-9515-285e8f26f7a2.png)\n\n正如我们所看到的，程序检测到我们的系统是小端的。 此技术可用于检测运行时的字符顺序，并相应地调整应用逻辑。\n\n# 还有更多的..。\n\n如今，最广泛使用的平台，如 x86 和**Acorn RISC Machine**(**ARM**)，都是小端的。 但是，您的代码不应该隐式假定系统的字节顺序。\n\n如果您需要在同一系统上运行的应用之间交换数据，那么坚持使用目标平台的字节顺序是安全的。 但是，如果您的应用需要通过网络协议或公共数据存储与其他系统交换数据，请考虑将您的二进制数据转换为公共字符顺序。\n\n基于文本的数据格式不存在字符顺序问题。 使用 JSON 格式表示与平台无关的、人类可读的数据。\n\n**Note**: Converting from a binary representation and back can be costly for your target embedded platform.\n\n# 转换字符顺序\n\n虽然序列化库在幕后处理字符顺序，但在某些情况下，开发人员可能希望自己实现轻量级通信协议。\n\n虽然 C++ 标准库不提供序列化函数，但开发人员可以利用这样一个事实，即在二进制网络协议中，字节顺序是定义的，并且始终是大端的。\n\n标准库提供了一组函数，可用于在当前平台(硬件)和大端(网络)字节顺序之间进行转换：\n\n*   `uint32_t`htonl(`uint32_t`value)：将`uint32_t`从硬件转换为网络订单\n*   `uint32_t`ntohl(`uint32_t`value)：将`uint32_t`从网络订单转换为硬件订单\n*   `uint16_t`htons(`uint16_t`值)：将`uint16_t`从硬件转换为网络订单\n*   `uint16_t`ntohl(`uint16_t`value)：将`uint16_t`从网络订单转换为硬件订单\n\n开发人员可以使用这些函数在不同平台上运行的应用之间交换二进制数据。\n\n在本指南中，我们将学习如何对字符串进行编码，以便它们可以在可能具有相同或不同字节顺序的两个系统之间进行交换。\n\n# 怎么做……\n\n在本食谱中，我们将创建两个应用：发送方和接收方。 发送方将为接收方写入数据，从而以独立于平台的方式对其进行编码。 要执行此操作，请执行以下步骤：\n\n1.  在您的工作目录(即`~/test`)中，创建一个名为`enconv`的子目录。\n2.  使用您喜欢的文本编辑器在`enconv`子目录中创建和编辑名为`sender.cpp`的文件。 包括所需的头文件，如下所示：\n\n```cpp\n#include <stdexcept>\n#include <arpa/inet.h>\n#include <fcntl.h>\n#include <stdint.h>\n#include <string.h>\n#include <unistd.h>\n```\n\n3.  然后，定义一个将数据写入文件描述符的函数：\n\n```cpp\nvoid WriteData(int fd, const void* ptr, size_t size) {\n  size_t offset =0;\n  while (size) {\n    const char *buffer = (const char*)ptr + offset;\n    int written = write(fd, buffer, size);\n    if (written < 0) {\n      throw std::runtime_error(\"Can not write to file\");\n    }\n    offset += written;\n    size -= written;\n  }\n  }\n```\n\n4.  现在，我们需要定义一个格式化和写入消息的函数，以及调用它的主函数：\n\n```cpp\nvoid WriteMessage(int fd, const char* str) {\n  uint32_t size = strlen(str);\n  uint32_t encoded_size = htonl(size);\n  WriteData(fd, &encoded_size, sizeof(encoded_size));\n  WriteData(fd, str, size);\n}\n\nint main(int argc, char** argv) {\n  int fd = open(\"envconv.data\", \n                 O_WRONLY|O_APPEND|O_CREAT, 0666);\n  for (int i = 1; i < argc; i++) {\n    WriteMessage(fd, argv[i]);\n  }\n}\n```\n\n5.  同样，使用相同的包含集创建名为`receiver.cpp`的文件：\n\n```cpp\n#include <stdexcept>\n#include <arpa/inet.h>\n#include <fcntl.h>\n#include <stdint.h>\n#include <string.h>\n#include <unistd.h>\n```\n\n6.  添加以下代码，该代码从文件描述符中读取数据：\n\n```cpp\nvoid ReadData(int fd, void* ptr, size_t size) {\n  size_t offset =0;\n  while (size) {\n    char *buffer = (char*)ptr + offset;\n    int received = read(fd, buffer, size);\n    if (received < 0) {\n      throw std::runtime_error(\"Can not read from file\");\n    } else if (received == 0) {\n      throw std::runtime_error(\"No more data\");\n    }\n    offset += received;\n    size -= received;\n  }\n  }\n```\n\n7.  现在，定义一个将读取消息的函数，以及调用它的 Main 函数：\n\n```cpp\nstd::string ReadMessage(int fd) {\n  uint32_t encoded_size = 0;\n  ReadData(fd, &encoded_size, sizeof(encoded_size));\n  uint32_t size = ntohl(encoded_size);\n  auto data = std::make_unique<char[]>(size);\n  ReadData(fd, data.get(), size);\n  return std::string(data.get(), size);\n}\n\nint main(void) {\n  int fd = open(\"envconv.data\", O_RDONLY, 0666);\n  while(true) {\n    try {\n      auto s = ReadMessage(fd);\n      std::cout << \"Read: \" << s << std::endl;\n    } catch(const std::runtime_error& e) {\n      std::cout << e.what() << std::endl;\n      break;\n    }\n  }\n }\n```\n\n8.  在 loop 子目录中创建名为`CMakeLists.txt`的文件，内容如下：\n\n```cpp\ncmake_minimum_required(VERSION 3.5.1)\nproject(conv)\nadd_executable(sender sender.cpp)\nadd_executable(receiver receiver.cpp)\n\nset(CMAKE_SYSTEM_NAME Linux)\nset(CMAKE_SYSTEM_PROCESSOR arm)\n\nSET(CMAKE_CXX_FLAGS \"--std=c++ 14\")\nset(CMAKE_CXX_COMPILER /usr/bin/arm-linux-gnueabi-g++)\n\n```\n\n9.  构建应用，并将生成的两个可执行二进制文件`sender`和`receiver`复制到目标系统。 使用[第 2 章](02.html)、*设置环境*中的食谱来实现此目的。\n10.  切换到目标系统的终端。 如果需要，请使用您的用户凭据登录。\n\n11.  运行`sender`二进制文件并传递两个命令行参数：`Hello`和`Worlds`。 这不会生成任何输出。\n12.  然后，打开接收器。\n13.  现在，检查用于数据交换的`sender`和`receiver`的文件内容。 它将是二进制格式，因此我们需要使用`xxd`工具将其转换为十六进制格式：\n\n```cpp\n$ xxd envconv.data \n0000000: 0000 0005 4865 6c6c 6f00 0000 0557 6f72 ....Hello....Wor\n0000010: 6c64 ld\n```\n\n14.  该文件包含两个字符串`hello`和`world`，按其大小作为前缀。 `size`字段始终以大端字节顺序存储，与体系结构无关。 这允许发送方和接收方在具有不同字符顺序的两台不同计算机上运行。\n\n# 它是如何运作的..。\n\n在这个配方中，我们创建了两个二进制文件，分别是发送方和接收方，它们模拟两个主机之间的数据交换。 我们不能对它们的字节顺序做出任何假设，这就是为什么数据交换格式必须是明确的。\n\n发送方和接收方交换大小可变的数据块。 我们将每个块编码为 4 字节整数，以便定义即将到来的块大小，后跟块内容。\n\n虽然发送器不会在屏幕上生成任何输出，但它会将编码的数据块保存在文件中。 当我们运行接收器时，它能够读取、解码和显示发送器保存的任何信息，如以下屏幕截图所示：\n\n![](img/2a6bdbd6-7451-47f5-8926-0ceaa8b67dcc.png)\n\n虽然我们在本地保持平台格式的块大小，但在发送时需要将其转换为统一的表示形式。 我们使用`htonl`函数来执行此操作：\n\n```cpp\n  uint32_t encoded_size = htonl(size);\n```\n\n此时，我们可以将编码大小写入输出流：\n\n```cpp\n  WriteData(fd, &encoded_size, sizeof(encoded_size));\n```\n\n该块的内容如下：\n\n```cpp\n  WriteData(fd, str, size);\n```\n\n接收器依次从输入流读取大小：\n\n```cpp\n uint32_t encoded_size = 0;\n ReadData(fd, &encoded_size, sizeof(encoded_size));\n```\n\n大小是编码的，在接收器使用`ntohl`函数将其转换为平台表示之前无法直接使用：\n\n```cpp\n uint32_t size = ntohl(encoded_size);\n```\n\n只有在这样做之后，它才会知道随后的块的大小，并且可以分配和读取它：\n\n```cpp\n auto data = std::make_unique<char[]>(size);\n ReadData(fd, data.get(), size);\n```\n\n由于序列化的`data`大小始终表示为大端，因此读取函数不需要假设写入数据的平台的字节顺序。 它可以处理来自任何处理器架构的数据。\n\n# 使用数据对齐\n\n处理器读写数据不是以字节为单位，而是以内存字(与其数据地址大小匹配的区块)为单位。 32 位处理器使用 32 位字，64 位处理器使用 64 位字，依此类推。\n\n当字对齐时，读写效率最高-数据地址是字大小的倍数。 例如，对于 32 位架构，地址 0x00000004 是对齐的，而 0x00000005 是未对齐的。 在 x86 平台上，访问未对齐的数据比访问对齐的数据慢。 然而，在 ARM 上，访问未对齐的数据会产生硬件异常并导致程序终止：\n\n```cpp\nCompilers align data automatically. When it comes to structures, the result may be surprising for developers who are not aware of alignment.\nstruct {\n    uint8_t c;\n    uint32_t i;\n} a = {1, 1};\n\nstd::cout << sizeof(a) << std::endl;\n```\n\n前面代码片段的输出是什么？`sizeof(uint8_t)`是 1，而`sizeof(uint32_t)`是 4。开发人员可能希望结构的大小是各个大小的总和；但是，结果在很大程度上取决于目标体系结构。\n\n对于 x86，结果是`8`。 让我们在`i`之前再添加一个`uint8_t`字段：\n\n```cpp\nstruct {\n    uint8_t c;\n uint8_t cc;\n    uint32_t i;\n} a = {1, 1};\n\nstd::cout << sizeof(a) << std::endl;\n```\n\n结果还是 8 分！ 编译器通过添加填充字节，根据对齐规则优化数据字段在结构中的位置。 规则取决于体系结构，对于其他体系结构，结果可能会有所不同。 因此，在没有*序列化的情况下，不能在两个不同的系统之间直接交换结构，*将在[第 8 章](08.html)，*通信和序列化*中进行详细说明。\n\n在本食谱中，我们将学习如何使用编译器隐式应用于对齐数据的规则来编写更高效的内存代码。\n\n# 怎么做……\n\n我们将创建一个分配结构数组的程序，并检查字段的顺序如何影响内存消耗。 要执行此操作，请执行以下步骤：\n\n1.  在您的工作目录(即`~/test`)中，创建一个名为`alignment`的子目录。\n2.  使用您喜欢的文本编辑器在 loop 子目录中创建一个名为`alignment.cpp`的文件。 添加所需的头部并定义两种数据类型，即`Category`和`ObjectMetadata1`：\n\n```cpp\n#include <iostream>\nenum class Category: uint8_t {\n  file, directory, socket\n};\nstruct ObjectMetadata1 {\n  uint8_t access_flags;\n  uint32_t size;\n  uint32_t owner_id;\n  Category category;\n};\n\n```\n\n3.  现在，让我们定义另一个名为`ObjectMetadata2`的数据类型，以及使用所有这些数据类型的代码：\n\n```cpp\nstruct ObjectMetadata2 {\n  uint32_t size;\n  uint32_t owner_id;\n  uint8_t access_flags;\n  Category category;\n};\n\nint main() {\n  ObjectMetadata1 object_pool1[1000];\n  ObjectMetadata2 object_pool2[1000];\n  std::cout << \"Poorly aligned:\" << sizeof(object_pool1) << std::endl;\n  std::cout << \"Well aligned:\" << sizeof(object_pool2) << std::endl;\n  return 0;\n}\n```\n\n4.  在 loop 子目录中创建名为`CMakeLists.txt`的文件，内容如下：\n\n```cpp\ncmake_minimum_required(VERSION 3.5.1)\nproject(alignment)\nadd_executable(alignment alignment.cpp)\n\nset(CMAKE_SYSTEM_NAME Linux)\nset(CMAKE_SYSTEM_PROCESSOR arm)\n\nSET(CMAKE_CXX_FLAGS \"--std=c++ 11\")\nset(CMAKE_CXX_COMPILER /usr/bin/arm-linux-gnueabi-g++)\n\n```\n\n5.  构建应用并将生成的可执行二进制文件复制到目标系统。 使用[第 2 章](02.html)、*设置环境*中的配方来实现此目的。\n6.  切换到目标系统的终端。 如果需要，请使用您的用户凭据登录。\n7.  运行二进制文件。\n\n# 它是如何运作的..。\n\n在我们的示例应用中，我们定义了两个数据结构`ObjectMetadata1`和`ObjectMetadata2`，它们将保存一些关于文件对象的元数据。 我们定义了四个表示对象的字段：\n\n*   **访问标志**：表示文件访问类型的位组合，例如读、写或执行。 所有位字段都打包到单个`uint8_t`字段中。\n*   **大小**：以 32 位无符号整数表示的对象大小。 它将支持的对象大小限制为 4 GB，但这足以证明正确数据对齐的重要性。\n*   **所有者 ID**：在我们的系统中标识用户的 32 位整数。\n*   **类别**：对象的类别。 这可以是文件、目录或套接字。 因为我们只定义了三个类别，所以`uint8_t`数据类型足以表示所有类别。 这就是我们使用`enum`类声明它们的原因：\n\n```cpp\nenum class Category: uint8_t {\n```\n\n`ObjectMetadata1`和`ObjectMetadata2`包含完全相同的字段；唯一的区别是它们在结构中的排序方式。\n\n现在，我们声明两个对象池。 两个池都包含 1,000 个对象；`object_pool1`以`ObjectMetadata1`结构保存元数据，而`object_pool2`使用`ObjectMetadata2`结构。 现在，让我们检查应用的输出：\n\n![](img/b9ba5450-5659-4cb2-a383-bd4285356c9a.png)\n\n这两个对象池在功能和性能方面是相同的。 然而，如果我们检查它们占用了多少内存，我们可以看到一个显著的差异：`object_pool1`比`object_pool2`大 4KB。 考虑到`object_pool2`的大小是 12KB，我们由于不注意数据对齐而浪费了 33%的内存。 在处理数据结构时要注意对齐和填充，因为不正确的字段排序可能会导致内存使用效率低下，就像`object_pool2`的情况一样。 使用这些简单的规则来组织您的数据字段，以使它们正确对齐：\n\n*   根据它们的大小对它们进行分组。\n*   从最大数据类型到最小数据类型对组进行排序。\n\n良好对齐的数据结构速度快、内存效率高，并且不需要实现任何额外的代码。\n\n# 还有更多的..。\n\n每个硬件平台都有自己的对齐要求，其中一些要求很棘手。 您可能需要参考目标平台编译器文档和最佳实践，以最大限度地利用硬件。 如果您的目标平台是 ARM，请考虑阅读[http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.faqs/ka15414.html](http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.faqs/ka15414.html)上有关调整预期的 ARM 技术文章。\n\n虽然结构中数据字段的正确对齐可以产生更紧凑的数据表示形式，但请注意性能方面的影响。 将一起使用的数据保存在同一内存区域称为**数据局部性**，可以显著提高数据访问性能。 适合同一高速缓存线的数据元素可以比跨越高速缓存线边界的元素快得多地被读取或写入。 在许多情况下，更可取的做法是以额外的内存使用为代价来获得性能提升。 我们将在*将数据与高速缓存线*对齐菜谱中更详细地回顾这项技术。\n\n# 使用填充结构\n\n在本食谱中，我们将学习如何定义数据成员之间没有任何填充字节的结构。 如果您的应用可以处理大量对象，这可能会显著减少应用使用的内存量。\n\n不过，请注意，这是有代价的。 未对齐的内存访问速度较慢，从而导致性能不佳。 对于某些体系结构，禁止非对齐访问，因此需要 C++ 编译器生成比对齐访问多得多的代码来访问数据字段。\n\n尽管打包结构可能会导致更高效的内存使用，但除非确实必要，否则请避免使用此技术。 它有太多隐含的限制，这些限制可能会在以后的应用中导致晦涩难懂的问题。\n\n将打包结构视为传输编码，并且仅使用它们在应用外部存储、加载或交换数据。 但是，即使在这些情况下，使用适当的数据序列化也是更好的解决方案。\n\n# 怎么做……\n\n在这个简单的应用中，我们将定义一个压缩结构数组，看看这对它所需的内存量有何影响。 要执行此操作，请执行以下步骤：\n\n1.  在您的工作目录(即`~/test`)中，创建`alignment`子目录的副本。 将其命名为`packed_alignment`。\n2.  通过将`__attribute__((packed))`添加到每个结构的定义来修改`alignment.cpp`文件：\n\n```cpp\nstruct ObjectMetadata1 {\n  uint8_t access_flags;\n  uint32_t size;\n  uint32_t owner_id;\n  Category category;\n} __attribute__((packed));\n\nstruct ObjectMetadata2 {\n  uint32_t size;\n  uint32_t owner_id;\n  uint8_t access_flags;\n  Category category;\n} __attribute__((packed));\n```\n\n3.  构建应用并将生成的可执行二进制文件复制到目标系统。 使用[第 2 章](02.html)、*设置环境*中的配方来实现此目的。\n4.  切换到目标系统的终端。 如果需要，请使用您的用户凭据登录。\n5.  运行二进制文件。\n\n# 它是如何运作的..。\n\n在此配方中，我们修改了*使用数据对齐*配方中的代码，为每个结构添加了一个压缩属性：\n\n```cpp\n} __attribute__((packed));\n```\n\n此属性指示编译器不要向结构添加填充字节，以符合目标平台的对齐要求。\n\n运行前面的代码会给出以下输出：\n\n![](img/521bd29e-1012-4d5a-b7fc-08a360183077.png)\n\n如果编译器不添加填充字节，则数据字段的顺序将变得无关紧要。 由于`ObjectMetadata1`和`ObjectMetadata2`结构具有完全相同的数据字段，因此它们的打包形式的大小变得相同。\n\n# 还有更多的..。\n\n`GNU Compiler Collection`(**GCC**)使开发人员可以使用其属性对数据布局进行大量控制。 您可以转到[GCC 类型属性](https://gcc.gnu.org/onlinedocs/gcc-9.1.0/gcc/Type-Attributes.html#Type-Attributes)页面，了解所有支持的属性及其含义。\n\n其他编译器提供类似的功能，但它们的 API 可能不同。 例如，Microsoft 编译器定义了`#pragma pack`编译器指令来声明压缩结构。 有关更多详细信息，请参阅[Pragma Pack Reference](https://docs.microsoft.com/en-us/cpp/preprocessor/pack?view=vs-2019)页面。\n\n# 将数据与高速缓存线对齐\n\n在本食谱中，我们将学习如何将数据结构与高速缓存线对齐。 数据对齐会显著影响系统的性能，特别是在多核系统中工作的多线程应用的情况下。\n\n首先，如果一起使用的数据位于同一缓存行中，则频繁访问这些数据的速度要快得多。 如果编程一致地先访问变量 A，然后访问变量 B，则处理器每次都必须使其缓存无效并重新加载，如果它们不在同一行中的话。\n\n其次，您不希望将不同线程独立使用的数据保留在同一缓存行中。 如果同一高速缓存线被不同的 CPU 核心修改，则需要高速缓存同步，这会影响使用共享数据的多线程应用的整体性能，因为在这种情况下，内存访问时间会显著增加。\n\n# 怎么做……\n\n我们将创建一个使用四种不同方法分配四个缓冲区的应用，以了解如何对齐静态和动态分配的内存。 要执行此操作，请执行以下步骤：\n\n1.  在您的工作目录(即`~/test`)中，创建一个名为`cache_align`的子目录。\n2.  使用您喜欢的文本编辑器在`cache_align`子目录中创建名为`cache_align.cpp`的文件。 将以下代码片段复制到`cache_align.cpp`文件中，以定义必要的常量和检测对齐的函数：\n\n```cpp\n#include <stdlib.h>\n#include <stdio.h>\n\nconstexpr int kAlignSize = 128;\nconstexpr int kAllocBytes = 128;\n\nconstexpr int overlap(void* ptr) {\n  size_t addr = (size_t)ptr;\n  return addr & (kAlignSize - 1);\n }\n```\n\n3.  现在，定义几个以不同方式分配的缓冲区：\n\n```cpp\nint main() {\n  char static_buffer[kAllocBytes];\n  char* dynamic_buffer = new char[kAllocBytes];\n\n  alignas(kAlignSize) char aligned_static_buffer[kAllocBytes];\n  char* aligned_dynamic_buffer = nullptr;\n  if (posix_memalign((void**)&aligned_dynamic_buffer,\n      kAlignSize, kAllocBytes)) {\n    printf(\"Failed to allocate aligned memory buffer\\n\");\n  }\n\n```\n\n4.  添加以下使用它们的代码：\n\n```cpp\n  printf(\"Static buffer address: %p (%d)\\n\", static_buffer,\n         overlap(static_buffer));\n  printf(\"Dynamic buffer address: %p (%d)\\n\", dynamic_buffer,\n         overlap(dynamic_buffer));\n  printf(\"Aligned static buffer address: %p (%d)\\n\", aligned_static_buffer,\n         overlap(aligned_static_buffer));\n  printf(\"Aligned dynamic buffer address: %p (%d)\\n\", aligned_dynamic_buffer,\n         overlap(aligned_dynamic_buffer));\n  delete[] dynamic_buffer;\n  free(aligned_dynamic_buffer);\n  return 0;\n  }\n```\n\n5.  在 loop 子目录中创建名为`CMakeLists.txt`的文件，内容如下：\n\n```cpp\ncmake_minimum_required(VERSION 3.5.1)\nproject(cache_align)\nadd_executable(cache_align cache_align.cpp)\n\nset(CMAKE_SYSTEM_NAME Linux)\nset(CMAKE_SYSTEM_PROCESSOR arm)\n\nSET(CMAKE_CXX_FLAGS \"-std=c++ 11\")\nset(CMAKE_CXX_COMPILER /usr/bin/arm-linux-gnueabi-g++)\n```\n\n6.  构建应用并将生成的可执行二进制文件复制到目标系统。 使用[第 2 章](02.html)、*设置环境*中的配方来实现此目的。\n7.  切换到目标系统的终端。 如果需要，请使用您的用户凭据登录。\n8.  运行二进制文件。\n\n# 它是如何运作的..。\n\n在第一个代码片段中，我们创建了两对内存缓冲区。 在每一对中，第一个缓冲区被分配给堆栈，而第二个缓冲区被分配给堆。\n\n第一对是使用标准 C++ 技术创建的。 堆栈上的静态缓冲区声明为数组：\n\n```cpp\n  char static_buffer[kAllocBytes];\n```\n\n要创建动态缓冲区，我们使用`new`C++ 关键字：\n\n```cpp\n  char* dynamic_buffer = new char[kAllocBytes];\n```\n\n在第二对中，我们创建内存对齐的缓冲区。 在堆栈上声明静态缓冲区类似于常规静态缓冲区。 我们使用一个附加属性`alignas`，它是在 C++ 11 中引入的，作为一种标准化的、独立于平台的方法来对齐内存中的数据：\n\n```cpp\n alignas(kAlignSize) char aligned_static_buffer[kAllocBytes];\n```\n\n此属性需要将对齐大小作为参数。 我们希望数据按高速缓存线边界对齐。 根据平台的不同，高速缓存线大小可能会有所不同。 最常见的大小为 32、64 和 128 字节。 使用 128 个字节可以使我们的缓冲区与其中任何一个对齐。\n\n没有对动态缓冲区执行相同操作的标准方法。 要在堆上分配内存，我们使用一个名为`posix_memalign`的 C 函数。 这仅在**可移植操作系统****接口**(**POSIX**)系统(大多数类 Unix)中可用，但这不需要 C++ 11 标准的支持：\n\n```cpp\n  if (posix_memalign((void**)&aligned_dynamic_buffer,\n kAlignSize, kAllocBytes)) {\n```\n\n`posix_memalign`类似于`malloc`，但有三个参数，而不是一个。 第二个参数是对齐大小，与 Align 属性的大小相同。 第三个是要分配的内存大小。 第一个参数用于返回指向分配的内存的指针。 与`malloc`不同，`posix_memalign`可能失败，不仅因为它不能分配内存，而且如果传递给函数的对齐大小不是 2 的幂。 `posix_memalign`返回错误代码作为其结果值，以帮助开发人员区分这两种情况。\n\n我们定义函数重叠，通过屏蔽所有对齐位来计算指针的未对齐部分：\n\n```cpp\n  size_t addr = (size_t)ptr;\n  return addr & (kAlignSize - 1);\n```\n\n当我们运行应用时，我们可以看到不同之处：\n\n![](img/007b6140-8b16-49a8-b066-43024d936300.png)\n\n第一对中的两个缓冲器的地址具有未对齐的部分，而第二对的地址是对齐的-未对齐的部分为零。 因此，对第二对缓冲区的元素的随机访问速度更快，因为所有这些元素都同时在高速缓存中可用。\n\n# 还有更多的..。\n\nCPU 访问数据对齐对于通过硬件地址转换机制高效地映射内存也至关重要。 现代操作系统操作 4KB 的内存块或页面来将进程的虚拟地址空间映射到物理内存。 在 4 KB 边界上对齐数据结构可以提高性能。\n\n我们在本配方中描述的相同技术可以应用于将数据与内存页面边界对齐。 但是，请注意，`posix_memalign`需要的内存可能是请求来满足此请求的内存的两倍。 对于较大的对齐块，这种内存开销增长可能非常显著。"
  },
  {
    "path": "docs/emb-prog-mod-cpp-cb/04.md",
    "content": "# 四、处理中断\n\n嵌入式应用的主要任务之一是与外部硬件外设进行通信。 使用输出端口向外设发送数据很容易理解。 然而，当涉及到阅读时，事情就变得更加复杂了。\n\n嵌入式开发人员必须知道何时可以读取数据。 由于外部设备位于处理器外部，因此随时可能发生这种情况。\n\n在本章中，我们将学习什么是中断以及如何处理它们。 在使用 8 位微控制器 8051 作为目标平台时，我们将了解以下主题：\n\n*   如何实现基本的中断处理\n*   如何使用定时器的中断在**M****微控制器单元**(**MCU**)的输出引脚上产生信号\n*   如何使用中断对 MCU 外部引脚上的事件进行计数\n*   如何使用中断在串行通道上通信\n\n我们将通过完成以下食谱来了解这些主题：\n\n*   实现中断服务例程\n*   使用 8 位自动重载模式生成 5 kHz 方形信号\n*   使用定时器 1 作为事件计数器对 1 Hz 脉冲进行计数\n*   串行收发数据\n\n理解如何处理中断的核心概念将帮助您实现响应迅速且高能效的嵌入式应用。\n\n不过，在此之前，我们将了解一些概念的背景知识。\n\n# 数据轮询数据轮询\n\n等待来自外部源的数据的第一种方法称为**轮询**。 应用定期查询外部设备的输入端口，以检查其是否有新数据。 它很容易实现，但也有很大的缺点。\n\n首先，它浪费处理器资源。 大多数民意调查电话报告说数据还不可用，我们需要继续等待。 由于这些调用不会导致某些数据处理，因此是对计算资源的浪费。 此外，轮询间隔应该足够短，以便快速响应外部事件。 开发人员应该在有效利用处理器能力和反应时间之间寻找折衷方案。\n\n其次，它使程序的逻辑错综复杂。 如果程序应该轮询事件，例如，每隔 5 毫秒轮询一次，那么它的任何子例程都不应该花费超过 5 毫秒的时间。 因此，开发人员人为地将代码拆分成更小的块，并组织它们之间的复杂切换，以允许轮询。\n\n# 中断服务例程\n\n中断是轮询的替代方案。 一旦外部设备有新数据，它就会在处理器中触发一个称为**中断**的事件。 顾名思义，它会中断执行指令的正常工作流。 处理器保存其当前状态并开始执行来自不同地址的指令，直到它遇到来自中断指令的返回。 然后，它读取保存的状态，以便从指令流被中断的那一刻开始继续执行该指令流。 此替代指令序列称为**中断服务例程**(**ISR**)。\n\n每个处理器都定义了自己的一组指令和约定来处理中断；但是，它们在处理中断时都使用相同的通用方法：\n\n*   中断由数字标识，从 0 开始。 这些数字映射到物理上对应于特定处理器引脚的硬件**中断请求线**(**IRQ**)。\n*   当 IRQ 线被激活时，处理器使用其编号作为中断向量阵列中的偏移量，以定位中断服务例程的地址。 中断向量阵列存储在存储器中的固定地址上。\n*   开发人员可以通过更新中断向量阵列中的条目来定义或重新定义 ISR。\n\n*   可以对处理器进行编程，以使能或禁用中断，无论是针对特定 IRQ 线路还是一次所有中断。 当中断被禁用时，处理器不会调用相应的 ISR，尽管 IRQ 线路的状态可以读取。\n*   根据物理引脚上的信号，可以对 IRQ 线进行编程以触发中断。 这可以处于信号的低电平、信号的高电平或边沿(这是从低到高或从高到低的转变)。\n\n# ISR 的一般注意事项\n\n由于中断处理是在硬件级别执行的，因此这种方法不会将处理器资源浪费在轮询上，并提供非常短的响应时间。 但是，开发人员应该了解它的细节，以避免将来出现严重或难以检测的问题。\n\n首先，同时处理多个中断，或者在仍然处理前一个中断的同时响应相同的中断，是很难实现的。 这就是执行 ISR 时禁用中断的原因。 这可以防止 ISR 被另一个中断中断，但这也意味着挂起中断的反应时间可能更长。 更糟糕的是，如果不快速重新启用中断，这可能会导致数据或事件丢失。\n\n为了避免这种情况，所有 ISR 都写得简短。 它们只做极少量的工作来读取或确认来自设备的数据。 复杂的数据分析和处理在 ISR 之外执行。\n\n# 8051 单片机中断\n\n8051 微控制器支持六个中断源-复位、两个硬件中断、两个定时器中断和一个串行通信中断：\n\n| **中断号** | **说明** | **以字节**为单位的偏移量 |\n|  | 重新设置 / 清零 / 复位 | 0 |\n| 0 | 外部中断 int0 | 3. |\n| 1. | 定时器 0(TF0) | 11. |\n| 2 个 | 外部中断 INT1 | 19 个 |\n| 3. | 计时器 1(TF1) | 27 |\n| 4. | 连续的 / 连续作案的 / 连载的 / 分期偿还的 | 36 |\n\n中断向量阵列位于地址 0；除 RESET 外的每个条目的大小为 8 字节。 虽然最小的 ISR 可以容纳 8 个字节，但通常情况下，条目包含将执行重定向到位于其他位置的实际 ISR 的代码。\n\n重置条目是特殊的。 它由复位信号激活，并立即跳转到主程序所在的地址。\n\n8051 定义了一个称为**中断启用**(**EA**)的特殊寄存器，用于启用和禁用中断。 其 8 位按以下方式分配：\n\n| **位** | **名称** | **含义** |\n| 0 | EX0 | 外部中断%0 |\n| 1. | ET0 | 定时器 0 中断 |\n| 2 个 | EX1 | 外部中断 1 |\n| 3. | ET1 | 定时器 1 中断 |\n| 4. | 萨尔瓦多 / 回声测深 | 串口中断 |\n| 5. | -你知道吗？ | 未使用 |\n| 6. | -你知道吗？ | 未使用 |\n| 7. | Electronics Arts 电子艺界游戏公司;Electronics Arts,电子艺界游戏公司 | 全局中断控制 |\n\n将这些位设置为 1 可使能相应的中断，设置为 0 则禁用这些中断。 EA 位使能或禁用所有中断。\n\n# 实现中断服务例程\n\n在本菜谱中，我们将学习如何为 8051 单片机定义中断服务例程。\n\n# 怎么做……\n\n请按照以下步骤完成本食谱：\n\n1.  切换到我们在[第 2 章](02.html)，*设置环境*中设置的构建系统。\n2.  确保安装了 8051 仿真器：\n\n```cpp\n# apt install -y mcu8051ide\n```\n\n3.  启动`mcu8051ide`并创建一个名为`Test`的新项目。\n\n4.  创建一个名为`test.c`的新文件，并将以下代码片段放入其中。 这会为每个定时器中断增加内部`counter`：\n\n```cpp\n#include<mcs51reg.h> \n\nvolatile int Counter = 0;\nvoid timer0_ISR (void) __interrupt(1) /*interrupt no. 1 for Timer0 */\n{ \n\n  Counter++ ;\n} \n\nvoid main(void) \n{ \n  TMOD = 0x03; \n  TH0 = 0x0; \n  TL0 = 0x0; \n  ET0 = 1; \n  TR0 = 1;\n  EA = 1;\n  while (1); /* do nothing */ \n} \n```\n\n5.  选择工具|编译以构建代码。 消息窗口将显示以下输出：\n\n```cpp\nStarting compiler ...\n\ncd \"/home/dev\"\nsdcc -mmcs51 --iram-size 128 --xram-size 0 --code-size 4096 --nooverlay --noinduction --verbose --debug -V --std-sdcc89 --model-small \"test.c\"\nsdcc: Calling preprocessor...\n+ /usr/bin/sdcpp -nostdinc -Wall -obj-ext=.rel -D__SDCC_NOOVERLAY -DSDCC_NOOVERLAY -D__SDCC_MODEL_SMALL -DSDCC_MODEL_SMALL -D__SDCC_FLOAT_REENT -DSDCC_FLOAT_REENT -D__SDCC=3_4_0 -DSDCC=340 -D__SDCC_REVISION=8981 -DSDCC_REVISION=8981 -D__SDCC_mcs51 -DSDCC_mcs51 -D__mcs51 -D__STDC_NO_COMPLEX__ -D__STDC_NO_THREADS__ -D__STDC_NO_ATOMICS__ -D__STDC_NO_VLA__ -isystem /usr/bin/../share/sdcc/include/mcs51 -isystem /usr/share/sdcc/include/mcs51 -isystem /usr/bin/../share/sdcc/include -isystem /usr/share/sdcc/include test.c\nsdcc: Generating code...\nsdcc: Calling assembler...\n+ /usr/bin/sdas8051 -plosgffwy test.rel test.asm\nsdcc: Calling linker...\nsdcc: Calling linker...\n+ /usr/bin/sdld -nf test.lk\n\nCompilation successful\n```\n\n6.  选择模拟器|启动/关闭菜单项以激活模拟器。\n7.  选择模拟器|动画以在慢速模式下运行程序。\n8.  切换到 C Variables 面板并向下滚动，直到显示 Counter Variable。\n9.  观察它是如何随着时间的推移而增加的：\n\n![](img/6bfb07eb-bdc2-4be0-a095-90ce3bda6141.png)\n\n如您所见，`Counter`变量的值字段现在是 74。\n\n# 它是如何运作的..。\n\n对于我们的示例应用，我们将使用 8051 微控制器的仿真器。 其中有几个是可用的；但是，我们将使用 MCU8051IDE，因为它可以在 Ubuntu 存储库中随时获得。\n\n我们将其作为常规的 Ubuntu 包安装，如下所示：\n\n```cpp\n# apt install -y mcu8051ide\n```\n\n这是一个 GUI IDE，需要 X Window 系统才能运行。 如果您使用 linux 或 windows 作为您的工作环境，请考虑直接从[https://sourceforge.net/projects/mcu8051ide/files/](https://sourceforge.net/projects/mcu8051ide/files/)安装和运行它。\n\n我们创建的简单程序定义了一个名为`Counter`的全局变量，如下所示*：*\n\n```cpp\nvolatile int Counter = 0;\n```\n\n这被定义为`volatile`，表示它可以在外部更改，编译器不应该试图优化代码来消除它。\n\n接下来，我们定义一个名为`timer0_ISR`*：*的简单函数\n\n```cpp\nvoid timer0_ISR (void) __interrupt(1)\n```\n\n它不接受任何参数，也不返回任何值。 它唯一做的事情就是递增`Counter`变量。 它是用一个称为`__interrupt(1)`的重要属性声明的，以让编译器知道它是一个中断处理程序，并且它服务于中断编号 1。编译器生成自动更新中断向量数组的相应条目的代码。\n\n定义 ISR 本身后，我们配置计时器的参数：\n\n```cpp\nTMOD = 0x03; \nTH0 = 0x0; \nTL0 = 0x0;\n```\n\n然后，我们打开计时器 0，如下所示：\n\n```cpp\nTR0 = 1;\n```\n\n以下命令启用定时器 0 的中断：\n\n```cpp\nET0 = 1; \n```\n\n以下代码启用所有中断：\n\n```cpp\nEA = 1;\n```\n\n此时，我们的 ISR 被定时器的中断周期性地激活。 由于所有工作都是在 ISR 内完成的，因此我们会运行一个无休止的循环，什么也不做：\n\n```cpp\nwhile (1); // do nothing \n```\n\n当我们在模拟器中运行前面的代码时，我们将看到`counter`变量的实际值随着时间的推移而变化，这表明我们的 ISR 正在被计时器激活。\n\n# 使用 8 位自动重载模式生成 5 kHz 方形信号\n\n在前面的配方中，我们了解了如何创建仅执行计数器递增的简单 ISR。 让我们让中断例程做一些更有用的事情。 在本食谱中，我们将学习如何对 8051 微控制器进行编程，使其产生具有给定频率的信号。\n\n8051 微控制器有两个定时器-定时器 0 和定时器 1-均使用两个特殊功能寄存器进行配置：**定时器模式**(**TMOD**)和**定时器控制**(**TCON**)。 定时器的值存储在定时器 0 的 TH0 和 TL0 定时器寄存器以及定时器 1 的 TH1 和 TL1 定时器寄存器中。\n\nTMOD 和 TCON 位具有特殊含义。 TMOD 寄存器的位定义如下：\n\n| **位** | **计时器** | **名称** | **目的** |\n| 0 | 0 | M0 | 定时器模式选择器-低位。 |\n| 1. | 0 | M1 型 | 定时器模式选择器-高位。 |\n| 2 个 | 0 | 计算机化 X 线体层照相术 | 计数器(1)或定时器(0)模式。 |\n| 3. | 0 | 门 / 大门 / 出入口 | 启用定时器 1，但仅当 INT0 的外部中断为高电平时。 |\n| 4. | 1. | M0 | 定时器模式选择器-低位。 |\n| 5. | 1. | M1 型 | 定时器模式选择器-高位。 |\n| 6. | 1. | 计算机化 X 线体层照相术 | 计数器(1)或定时器(0)模式。 |\n| 7. | 1. | 门 / 大门 / 出入口 | 启用定时器 1，但仅当 INT1 的外部中断为高电平时。 |\n\n低 4 位分配给定时器 0，高 4 位分配给定时器 1。\n\nM0 和 M1 位允许我们以四种模式之一配置定时器：\n\n| **模式** | **M0** | **M1** | **说明** |\n| 0 | 0 | 0 | 13 位模式。 TL0 或 TL1 寄存器包含低 5 位，TH0 或 TH1 寄存器包含相应计时器值的高 8 位。 |\n| 1. | 0 | 1. | 16 位模式。 TL0 或 TL1 寄存器包含低 8 位，TH0 或 TH1 寄存器包含相应计时器值的高 8 位。 |\n| 2 个 | 1. | 0 | 具有自动重新加载功能的 8 位模式。 TL0 或 TL1 包含相应的计时器值，而 TH0 或 TL1 包含重载值。 |\n| 3. | 1. | 1. | 定时器 0 的特殊 8 位模式 |\n\n**定时器****控件**(**TCON**)注册控件的定时器中断。 其位定义如下：\n\n| **位** | **名称** | **目的** |\n| 0 | IT0 | 外部中断 0 控制位。 |\n| 1. | IE0 | 外部中断 0 边缘标志。 当在 INT0 处接收到高至低沿信号时，设置为 1。 |\n| 2 个 | IT1 | 外部中断 1 控制位。 |\n| 3. | IE1 | 外部中断 1 边缘标志。 当 INT1 接收到高至低沿信号时，置 1。 |\n| 4. | TR0 | 运行计时器 0 的控制。 设置为 1 可启动，设置为 0 可停止计时器。 |\n| 5. | TF0 | 计时器 0 溢出。 当计时器达到其最大值时设置为 1。 |\n| 6. | TR1 | 运行计时器 1 的控制。设置为 1 可启动计时器，设置为 0 可停止计时器。 |\n| 7. | TF1 | 计时器 1 溢出。 当计时器达到其最大值时设置为 1。 |\n\n我们将使用 8051 定时器的特定模式，称为自动重新加载。 在此模式下，TL0(定时器 1 的 TL1)寄存器包含定时器值，而 TH0(定时器 1 的 TH1)包含重载值。 一旦 TL0 达到最大值 255，它就会产生溢出中断，并自动复位到重载值。\n\n# 怎么做……\n\n请按照以下步骤完成本食谱：\n\n1.  启动*mce8051ide*并创建一个名为`Test`的新项目。\n2.  创建一个名为`generator.c`的新文件，并将以下代码片段放入其中。 这将在 MCU 的`P0_0`引脚上产生 5 kHz 信号：\n\n```cpp\n#include<8051.h> \n\nvoid timer0_ISR (void) __interrupt(1) \n{ \n  P0_0 = !P0_0;\n} \n\nvoid main(void) \n{ \n  TMOD = 0x02;\n  TH0 = 0xa3; \n  TL0 = 0x0; \n  TR0 = 1;\n  EA = 1; \n  while (1); // do nothing \n}\n```\n\n3.  选择工具|编译以构建代码。\n4.  选择模拟器|启动/关闭菜单项以激活模拟器。\n5.  选择模拟器|动画以在慢速模式下运行程序。\n\n# 它是如何运作的..。\n\n以下代码定义计时器 0 的 ISR：\n\n```cpp\nvoid timer0_ISR (void) __interrupt(1) \n```\n\n在每次定时器中断时，我们翻转 P0 的输入输出寄存器的 0 位。 这将在 P0 输出引脚上有效地产生方波信号。\n\n现在，我们需要弄清楚如何对定时器进行编程，以生成具有给定频率的中断。 要生成 5 kHz 信号，我们需要用 10 kHz 频率翻转比特，因为每个波都由一个高相位和一个低相位组成。\n\n8051 单片机使用外部振荡器作为时钟源。 定时器单元将外部频率除以 12。对于通常用作 8051 的时间源的 11.0592 兆赫振荡器，定时器每隔 1/11059200*12=1.085 毫秒激活一次。\n\n我们的定时器 ISR 应该以 10 千赫的频率激活，或者每 100 毫秒激活一次，或者在每 100/1.085=92 个定时器滴答声之后激活。\n\n我们将定时器 0 编程为在模式 2 下运行，如下所示：\n\n```cpp\nTMOD = 0x02;\n```\n\n在此模式下，我们将定时器的复位值存储在 TH0 寄存器中。 ISR 由定时器溢出激活，定时器溢出在定时器计数器达到最大值后发生。 模式 2 是 8 位模式，这意味着最大值为 255。 要每 92 个刻度激活 ISR，自动重新加载值应为 255-92=163，或十六进制表示的`0xa3`。\n\n我们将自动重载值与初始定时器值一起存储在定时器寄存器中：\n\n```cpp\nTH0 = 0xa3; \nTL0 = 0x0;\n```\n\n定时器 0 被激活，如下所示：\n\n```cpp\nTR0 = 1;\n```\n\n然后，我们启用计时器中断：\n\n```cpp\nTR0 = 1;\n```\n\n最后，所有中断都被激活：\n\n```cpp\nEA = 1; \n```\n\n从现在开始，我们的 ISR 每 100 微秒调用一次，如以下代码所示：\n\n```cpp\nP0_0 = !P0_0;\n```\n\n这会翻转`P0`寄存器的`0`位，从而在相应的输出引脚上产生 5 kHz 方形信号。\n\n# 使用定时器 1 作为事件计数器对 1 Hz 脉冲进行计数\n\n8051 定时器具有双重功能。 当它们被时钟振荡器激活时，它们充当计时器。 但是，它们也可以由外部引脚上的信号脉冲激活，即充当计数器的 P3.4(定时器 0)和 P3.5(定时器 1)。\n\n在本食谱中，我们将学习如何对定时器 1 进行编程，使其对 8051 处理器的 P3.5 引脚的激活进行计数。\n\n# 怎么做……\n\n请按照以下步骤完成本食谱：\n\n1.  打开 mcu8051ide。\n2.  创建一个名为`Counters`的新项目。\n3.  创建一个名为`generator.c`的新文件，并将以下代码片段放入其中。 每次触发定时器中断时，这会递增计数器变量：\n\n```cpp\n#include<8051.h> \n\nvolatile int counter = 0;\nvoid timer1_ISR (void) __interrupt(3) \n{ \n  counter++ ;\n} \n\nvoid main(void) \n{ \n  TMOD = 0x60;\n  TH1 = 254; \n  TL1 = 254; \n  TR1 = 1;\n  ET1 = 1;\n  EA = 1; \n  while (1); // do nothing \n}\n```\n\n4.  选择工具|编译以构建代码。\n5.  打开 Virtual HW(虚拟硬件)菜单，然后选择 Simple(简单)键...。 进入。 将打开一个新窗口。\n6.  在 Simple Keypad 窗口中，将端口 3 和位 5 分配给第一个密钥。 然后，单击打开或关闭按钮将其激活：\n\n![](img/5c45f07b-20cb-4009-93d0-312fa4abe748.png)\n\n7.  选择模拟器|Start/Shutdown(模拟器|启动/关闭)菜单项以激活模拟器。\n8.  选择模拟器|动画以动画模式运行程序，该模式在调试器窗口中显示对特殊寄存器的所有更改。\n9.  切换到 Simple Keypad 窗口并单击第一个键。\n\n# 它是如何运作的..。\n\n在这个配方中，我们利用 8051 定时器的功能，使它们充当计数器。 我们定义中断服务例程的方式与定义普通定时器的方式完全相同。 由于我们使用定时器 1 作为计数器，因此使用中断行号`3`，如下所示：\n\n```cpp\nvoid timer1_ISR (void) __interrupt(3) \n```\n\n中断例程的主体很简单。 我们只递增`counter`变量。\n\n现在，让我们确保 ISR 是由外部源激活的，而不是由时钟振荡器激活的。 为此，我们通过将`TMOD`特殊功能寄存器的 C/T 位设置为 1 来配置定时器 1：\n\n```cpp\nTMOD = 0x60;\n```\n\n同一行将定时器 1 配置为在自动重新加载的模式 2-8 位模式下运行。 由于我们的目标是在每次外部引脚激活时调用中断例程，因此我们将自动重新加载和初始值设置为最大值`254`：\n\n```cpp\nTH1 = 254; \nTL1 = 254; \n```\n\n接下来，我们启用计时器 1：\n\n```cpp\n TR1 = 1;\n```\n\n然后，定时器 1 的所有中断都被激活，如下所示：\n\n```cpp\n ET1 = 1;\n EA = 1;\n```\n\n之后，我们可以进入不做任何事情的无限循环，因为所有工作都是在中断服务例程中完成的：\n\n```cpp\n while (1); // do nothing \n```\n\n此时，我们可以在仿真器中运行代码。 但是，我们需要配置外部事件源。 为此，我们利用了 MCU8051IDE 支持的虚拟外部硬件组件之一-虚拟小键盘。\n\n我们配置它的一个按键来激活 8051 的 P3.5 引脚。 当计时器 1 在计数模式下使用时，该引脚用作定时器 1 的信号源。\n\n现在，我们运行代码。 按下虚拟键可激活计数器。 一旦计时器值溢出，我们的 ISR 就会被触发，使`counter`变量递增。\n\n# 还有更多的..。\n\n在本食谱中，我们使用定时器 1 作为计数器。 同样的情况也可以应用于计数器 0。 在这种情况下，引脚 P3.4 应用作外部源。\n\n# 串行收发数据\n\n8051 微控制器带有内置的**通用异步接收器发射器**(**UART**)端口，用于串行数据交换。\n\n串行端口由称为**串行控制**(**SCON**)的**特殊功能寄存器**(**SFR**)控制。 其位定义如下：\n\n| **位** | **名称** | **目的** |\n| 0 | **RI**(**接收****中断**的缩写) | 完全接收到一个字节时由 UART 置位 |\n| 1. | **TI**(**发送****中断**的缩写) | 当一个字节传输完成时由 UART 置位 |\n| 2 个 | **RB8**(**接收****位****8**的缩写) | 以 9 位模式存储接收数据的第九位。 |\n| 3. | **TB8**(**发送位 8**的缩写) | 存储要以 9 位模式传输的第九位数据(见下文) |\n| 4. | **REN**(**接收器启用**的缩写) | 启用(1)或禁用(0)接收操作 |\n| 5. | **SM2**(启用多处理器) | 启用(1)或禁用(0)9 位模式的多处理器通信 |\n| 6. | **SM1**(串行模式，高位) | 定义串行通信模式 |\n| 7. | **SM0**(串行模式，低位) | 定义串行通信模式 |\n\n8051 UART 支持四种 m 种串行通信模式，均由 SM1 和 SM0 位定义：\n\n| **模式** | [[T0\\]SM0[] | [#T0#SM1#T1] | **说明** |\n| 0 | 0 | 0 | 移位寄存器，固定波特率 |\n| 1. | 0 | 1. | 8 位 UART，使用定时器 1 设置波特率 |\n| 2 个 | 1. | 0 | 9 位 UART，固定波特率 |\n| 3. | 1. | 1. | 9 位 UART，使用定时器 1 设置波特率 |\n\n在本菜谱中，我们将学习如何使用中断在使用 8 位 UART 模式和可编程波特率(模式 1)的串行端口上实现简单的数据交换。\n\n# 怎么做……\n\n请按照以下步骤完成本食谱：\n\n1.  打开 mcu8051ide 并创建一个新项目。\n2.  创建一个名为`serial.c`的新文件，并将以下代码片段复制到其中。 此代码将通过串行链路接收的字节复制到`P0`输出寄存器。 这与 MCU 上的通用输入/输出引脚相关联：\n\n```cpp\n#include<8051.h>\n\nvoid serial_isr() __interrupt(4) { \n    if(RI == 1) {\n        P0 = SBUF;\n        RI = 0;\n    }\n }\n\nvoid main() {\n    SCON = 0x50;\n    TMOD = 0x20;\n    TH1 = 0xFD;\n    TR1 = 1; \n    ES = 1;\n    EA = 1;\n\n    while(1);\n }\n```\n\n3.  选择工具|编译以构建代码。\n4.  选择模拟器|Start/Shutdown(模拟器|启动/关闭)菜单项以激活模拟器。\n\n# 它是如何运作的..。\n\n我们为中断线路`4`定义 ISR，串行端口事件触发中断线路`4`：\n\n```cpp\nvoid serial_isr() __interrupt(4)\n```\n\n一旦接收到完整字节并将其存储在**串行缓冲寄存器**(**SBUF**)中，立即调用中断例程。 我们的 ISR 实现只将接收到的字节复制到输入/输出端口，即`P0`：\n\n```cpp\nP0 = SBUF;\n```\n\n然后，它重置 RI 标志以使能即将到来的字节的中断。\n\n为了使中断按预期工作，我们配置了串行端口和定时器。 首先，配置串口，如下所示：\n\n```cpp\nSCON = 0x50;\n```\n\n根据上表，这意味着只有**串行控制寄存器**(**SCON**)的 SM1 和 REN 位被设置为 1，从而选择通信模式 1。这是一个 8 位 UARS，具有通过定时器 1 定义的波特率。然后，它启用接收器。\n\n由于波特率由定时器 1 定义，因此下一步是配置定时器，如下所示：\n\n```cpp\nTMOD = 0x20;\n```\n\n上述代码将计时器 1 配置为使用模式 2，即 8 位自动重新加载模式。\n\n将 0xFD 写入 TH1 寄存器会将波特率设置为 9600 bps。 然后，我们启用定时器 1、串行中断和所有中断。\n\n# 还有更多的..。\n\n数据传输可以以类似的方式实现。 如果将数据写入 SBUF 特殊寄存器，8051 UART 将开始传输。 一旦完成，将调用串行中断，并且 TI 标志将被设置为 1。"
  },
  {
    "path": "docs/emb-prog-mod-cpp-cb/05.md",
    "content": "# 五、调试、日志记录和性能分析\n\n对于任何类型的应用，调试和分析都是开发工作流的重要组成部分。 在嵌入式环境中，这些任务需要开发人员特别注意。 嵌入式应用运行在与开发人员的工作站可能非常不同的系统上，而且通常资源和用户界面功能有限。\n\n开发人员应该提前计划如何在开发阶段调试他们的应用，以及如何确定生产环境中问题的根本原因以及如何修复这些问题。\n\n通常，解决方案是使用目标设备的仿真器以及嵌入式系统供应商提供的交互式调试器。 然而，对于更复杂的系统，完整而准确的仿真几乎是不可行的，而远程调试是最可行的解决方案。\n\n在许多情况下，使用交互式调试器是不可能的，或者根本不实用。 程序在断点处停止后，硬件状态可能会在几毫秒内发生变化，开发人员没有足够的时间对其进行分析。 在这种情况下，开发人员必须使用大量日志记录进行根本原因分析。\n\n在本章中，我们将重点介绍基于**SoC**(**片上系统**的缩写)和运行 Linux 操作系统的更强大系统的调试方法。 我们将介绍以下主题：\n\n*   在**gdb**(**GNU Project Debugger 的缩写)**中运行应用\n*   使用断点\n*   使用核心转储\n\n*   使用 gdbserver 进行调试\n*   添加调试日志记录\n*   使用调试版本和发布版本\n\n这些基本的调试技术在使用本书中的食谱以及任何类型的嵌入式应用时都会有很大帮助。\n\n# 技术要求\n\n在本章中，我们将学习如何在**ARM**(**Acorn RISC Machines**)平台仿真器中调试嵌入式应用。 此时，您应该已经在笔记本电脑或台式机上运行的虚拟化 Linux 环境中配置了两个系统：\n\n*   Docker 容器中的 Ubuntu Linux 作为构建系统\n*   Debian Linux 在**QEMU**(**Quick EMUlato**)ARM 仿真器中作为目标系统\n\n要学习交叉编译的理论并设置开发环境，请参考[第 2 章](02.html)、*设置环境*中的配方。\n\n# 在 gdb 中运行您的应用\n\n在本食谱中，我们将学习如何在目标系统上的调试器中运行示例应用，并尝试一些基本的调试技术。\n\nGdb**是一个开源且广泛使用的交互式调试器。 与作为**集成开发环境**(**IDE**)产品一部分的大多数调试器不同，GDB 是一个独立的命令行调试器。 这意味着它不依赖于任何特定的 IDE。 正如您在示例中看到的，您可以使用纯文本编辑器处理应用的代码，同时仍然能够交互调试它、使用断点、查看变量和堆栈跟踪的内容，等等。**\n\n **GDB 的用户界面是极简主义的。 使用它的方式与使用 Linux 控制台的方式相同-通过键入命令并分析其输出。 这种简单性使其非常适合嵌入式项目。 它可以在没有图形子系统的系统上运行。 如果只能通过串行连接或 ssh shell 访问目标系统，则它特别方便。 由于它没有花哨的用户界面，因此可以在资源有限的系统上运行。\n\n在本食谱中，我们将使用一个因异常而崩溃的人工样本应用。 它不会记录任何有用的信息，并且异常消息过于模糊，无法确定崩溃的根本原因。 我们将使用 GDB 来确定问题的根本原因。\n\n# 怎么做……\n\n现在，我们将创建一个简单的应用，该应用在特定条件下崩溃：\n\n1.  在您的工作目录`~/test`中，创建一个名为`loop`的子目录。\n2.  使用您喜欢的文本编辑器在`loop`子目录中创建`loop.cpp`文件。\n3.  让我们将一些代码放到`loop.cpp`文件中。 我们首先介绍的内容包括：\n\n```cpp\n#include <iostream>\n#include <chrono>\n#include <thread>\n#include <functional>\n```\n\n4.  现在，我们定义了我们的程序将包含的三个函数。 第一个是`runner`：\n\n```cpp\nvoid runner(std::chrono::milliseconds limit,\n            std::function<void(int)> fn,\n            int value) {\n  auto start = std::chrono::system_clock::now();\n  fn(value);\n  auto end = std::chrono::system_clock::now();\n  std::chrono::milliseconds delta =\n      std::chrono::duration_cast<std::chrono::milliseconds>(end - start);\n  if (delta > limit) {\n    throw std::runtime_error(\"Time limit exceeded\");\n  }\n  }\n```\n\n5.  第二个函数是`delay_ms`：\n\n```cpp\nvoid delay_ms(int count) {\n  for (int i = 0; i < count; i++) {\n    std::this_thread::sleep_for(std::chrono::microseconds(1050));\n  }\n  }\n```\n\n6.  最后，我们添加入口点函数`main`：\n\n```cpp\nint main() {\n  int max_delay = 10;\n  for (int i = 0; i < max_delay; i++) {\n    runner(std::chrono::milliseconds(max_delay), delay_ms, i);\n  }\n  return 0;\n  }\n```\n\n7.  在`loop`子目录中创建名为`CMakeLists.txt`的文件，内容如下：\n\n```cpp\ncmake_minimum_required(VERSION 3.5.1)\nproject(loop)\nadd_executable(loop loop.cpp)\n\nset(CMAKE_SYSTEM_NAME Linux)\nset(CMAKE_SYSTEM_PROCESSOR arm)\n\nSET(CMAKE_CXX_FLAGS \"-g --std=c++ 11\")\n\nset(CMAKE_C_COMPILER /usr/bin/arm-linux-gnueabi-gcc)\nset(CMAKE_CXX_COMPILER /usr/bin/arm-linux-gnueabi-g++)\n\nset(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)\nset(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)\nset(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)\nset(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)\n```\n\n8.  现在，切换到构建系统终端，并通过运行以下命令将当前目录更改为`/mnt/loop`。\n\n```cpp\n$ cd /mnt/loop\n```\n\n9.  按如下方式构建应用：\n\n```cpp\n$ cmake . && make\n```\n\n10.  切换回您的本地环境，在`loop`子目录中找到`loop`输出文件，并通过 ssh 将其复制到目标系统。 使用用户帐户。 切换到目标系统终端。 如果需要，使用用户凭据登录。 现在，使用`gdb`运行`loop`可执行二进制文件：\n\n```cpp\n$ gdb ./loop\n```\n\n11.  调试器已启动，并显示命令行提示符(`gdb`)。 要运行该应用，请键入`run`命令：\n\n```cpp\n(gdb) run\n```\n\n12.  您可以看到，由于运行时异常，应用异常终止。 异常消息`Time limit exceeded`给了我们一个线索，但没有指出它是在什么特定条件下发生的。 让我们试着确定这一点。 首先，让我们检查一下崩溃应用的堆栈跟踪：\n\n```cpp\n(gdb) bt\n```\n\n13.  这显示了从顶层函数`main`到库函数`__GI_abort`的七个堆栈帧，库函数`__GI_abort`实际上终止了应用。 正如我们可以看到的，只有帧`7`和`6`属于我们的应用，因为只有它们是在`loop.cpp`中定义的。 让我们仔细看看`frame 6`，因为这是抛出异常的函数：\n\n```cpp\n(gdb) frame 6\n```\n\n14.  运行`list`命令查看附近的代码：\n\n```cpp\n(gdb) list\n```\n\n15.  正如我们所看到的，如果增量变量的值超过了限制变量的值，就会抛出异常。 但这些价值观是什么呢？ 以下是变量‘Delta’和‘Limit’的值，运行`info locals`命令即可解决此问题：\n\n```cpp\n(gdb) info locals\n```\n\n16.  我们在这里看不到 LIMIT 变量的值。 使用`info args`命令查看：\n\n```cpp\n(gdb) info args\n```\n\n17.  现在，我们可以看到极限是`10`，增量是`11`。 当调用函数时将`fn`参数设置为`delay_ms`函数，并将`value`参数的值设置为`7`，则会发生崩溃。\n\n# 它是如何运作的..。\n\n该应用是故意创建的，以便在某些情况下崩溃，并且没有提供足够的信息来确定这些情况。 该应用由两个主要函数组成-`runner`和`delay_ms`。\n\n`runner`函数接受三个参数-时间限制、一个参数的函数和函数参数值。 它运行作为参数提供的函数，将值传递给它，并测量运行时间。 如果时间超过了时间限制，则会抛出异常。\n\n`delay_ms`函数执行延迟。 但是，它的实现不正确，认为每毫秒由 1,100 微秒组成，而不是 1,000 微秒。\n\n`main`函数运行`loop`目录中的 Runner，提供 10 毫秒的固定值作为时间限制，并提供`delay_ms`作为运行函数，但会增加`value`参数的值。 在某些情况下，`delay_ms`函数超过时间限制，应用崩溃。\n\n首先，我们为 ARM 平台构建应用，并将其传输到仿真器运行：\n\n![](img/52c9bbb6-e17c-4961-a4da-0e45d3a73859.png)\n\n将`-g`参数传递给编译器非常重要。 此参数指示编译器将调试符号添加到生成的二进制文件中。 我们将其添加到`CMakeLists.txt`文件的`CMAKE_CXX_FLAGS`参数中，如下所示：\n\n```cpp\nSET(CMAKE_CXX_FLAGS \"-g --std=c++ 11\")\n```\n\n现在，我们运行调试器并将应用可执行文件名作为其参数进行传递：\n\n![](img/e3b7393c-2d4c-4b8d-a1dd-1f5ea2c2ced8.png)\n\n应用不会立即运行。 我们使用`run`gdb 命令启动它，并观察到它很快就崩溃了：\n\n![](img/66aacaec-d965-40bc-bac7-e0de3e630951.png)\n\n接下来，我们使用`backtrace`命令查看堆栈跟踪：\n\n![](img/b394f587-a045-48f3-8c60-299f1cbb9fc7.png)\n\n对堆栈跟踪的分析表明，`frame 6`应该为我们提供更多信息来揭示根本原因。 在接下来的步骤中，我们切换到`frame 6`并查看相关的代码片段：\n\n![](img/ee18e675-c717-4500-8d75-c88fd036ad38.png)\n\n接下来，我们分析局部变量和函数参数的值，以确定它们与时间限制的关系：\n\n![](img/a37ff8ac-9ac1-4185-9e4c-d641e7c05ff4.png)\n\n我们确定，当传递给`delay_ms`的值达到`7`，而不是`11`时，就会发生崩溃，这在正确实现延迟的情况下是可以预期的。\n\n# 还有更多的..。\n\nGdb 命令通常接受多个参数来微调其行为。 使用`help`gdb 命令了解有关每个命令的更多信息。 例如，下面是`help bt`命令的输出：\n\n![](img/96d294ad-4432-4294-bd38-b8480c1ccec6.png)\n\n这将显示有关`bt`命令的信息，该命令用于查看和分析堆栈跟踪。 同样，您可以获得有关 GDB 支持的所有其他命令的信息。\n\n# 使用断点\n\n在本食谱中，我们将在使用 gdb 时学习更高级的调试技术。 我们将使用相同的示例应用并使用断点来查找实际延迟对`delay_ms`参数值的依赖关系。\n\n在 GDB 中使用断点类似于在集成到 IDE 中的调试器中使用断点，唯一的区别是开发人员必须学会显式使用行号、文件名或函数名，而不是使用内置编辑器来导航源代码。\n\n这不如点击运行调试器方便，但其灵活性允许开发人员创建功能强大的调试方案。 在本食谱中，我们将学习如何在 GDB 中使用断点。\n\n# 怎么做……\n\n在这个配方中，我们将使用与第一个配方相同的环境和测试应用。 请参考在 gdb 配方中运行您的应用的*中的步骤 1 到 9，以构建应用并将其复制到目标系统：*\n\n1.  我们想调试我们的`runner`函数。 我们来看一下它的内容。 在 gdb shell 中，按如下方式运行程序：\n\n```cpp\n(gdb) list runner,delay_ms\n```\n\n2.  我们希望看到增量在每次迭代中是如何变化的。 让我们在该行设置一个断点：\n\n```cpp\n14 if (delta > limit) {\n```\n\n3.  使用`break 14`命令在第 14 行设置断点：\n\n```cpp\n(gdb) break 14\n```\n\n4.  现在，运行该程序：\n\n```cpp\n(gdb) run\n```\n\n5.  检查`delta`的值：\n\n```cpp\n(gdb) print delta \n$1 = {__r = 0}\n```\n\n6.  通过键入`continue`或仅键入`c`继续执行程序：\n\n```cpp\n(gdb) c\n```\n\n7.  再次检查`delta`的值：\n\n```cpp\n(gdb) print delta\n```\n\n8.  正如我们预期的那样，`delta`的值在每次迭代时都会增加，因为`delay_ms`需要越来越多的时间。\n9.  每次运行`print delta`都不方便。 让我们使用名为`command`的命令将其自动化：\n\n```cpp\n(gdb) command\n```\n\n10.  再次运行`c`。 现在，在每次停止后都会显示`delta`的值：\n\n```cpp\n(gdb) c\n```\n\n11.  但是，输出过于冗长。 让我们通过再次键入`command`并编写以下指令来使 gdb 输出静默。 现在，多次运行`c`或`continue`命令以查看差异：\n\n```cpp\n(gdb) command\nType commands for breakpoint(s) 1, one per line.\nEnd with a line saying just \"end\".\n>silent\n>print delta\n>end\n(gdb) c\n```\n\n12.  我们可以使用`printf`命令使输出更加简洁，如下所示：\n\n```cpp\n(gdb) command\nType commands for breakpoint(s) 1, one per line.\nEnd with a line saying just \"end\".\n>silent\n>printf \"delta=%d, expected=%d\\n\", delta.__r, value\n>end\n(gdb) c\n```\n\n现在，我们可以看到两个值，计算的延迟和预期的延迟，并可以看到它们随时间的变化情况。\n\n# 它是如何运作的..。\n\n在这个配方中，我们想要设置一个断点来调试`runner`函数。 由于 gdb 没有内置编辑器，我们需要知道行号来设置断点。 虽然我们可以直接从文本编辑器获得它，但另一种方法是查看 GDB 中的相关代码片段。 我们使用带有两个参数(函数名)的`gdb`命令列表来显示函数运行器的第一行和`delay_ms`函数的第一行之间的代码行。 这将有效地显示函数运行器的内容：\n\n![](img/482a8b75-c389-4e61-b14d-de3f5cd496b9.png)\n\n在*步骤 4*，使用`break 14`命令在第`14`行设置断点，然后运行程序。 执行在断点处停止：\n\n![](img/c22a841e-1537-4806-b134-47c6733547bc.png)\n\n我们使用`print`命令检查`delta`变量的值，并使用`continue`命令继续执行程序，由于在循环中调用了`runner`函数，因此它再次在同一断点处停止：\n\n![](img/3e1b1f31-812a-4201-8a63-1866a3febe7a.png)\n\n接下来，我们尝试一种更高级的技术。 我们定义了一组要在触发断点时执行的 gdb 命令。 我们从一个简单的`print`命令开始。 现在，每次继续执行时，我们都可以看到`delta`变量的值：\n\n![](img/55443211-11e7-43c4-a6c2-6a135a946c64.png)\n\n接下来，我们使用`silent`命令禁用辅助 gdb 输出，以使输出更加简洁：\n\n![](img/7175eb69-bc52-4946-b777-6277cae952a9.png)\n\n最后，我们使用`printf`命令用两个最有趣的变量格式化消息：\n\n![](img/7b3fe1fb-b927-4590-9e68-2172dde5d955.png)\n\n如您所见，GDB 为开发人员提供了很大的灵活性，即使缺少图形界面也能使调试变得舒适。\n\n# 还有更多的..。\n\n重要的是要记住，优化选项`-O2`和`-O3`可能会导致编译器完全删除某些代码行。 如果将断点设置为此类行，则永远不会触发这些断点。 若要避免此类情况，请关闭调试版本的编译器优化。\n\n# 使用核心转储\n\n在第一个配方中，我们了解了如何使用交互式命令行调试器确定应用崩溃的根本原因。 但是，也有应用在生产环境中崩溃的情况，在测试系统上运行 GDB 下的应用不可能或不切实际地重现相同的问题。\n\nLinux 提供了一种机制来帮助分析崩溃的应用，即使它们不是直接从 GDB 运行的。 当应用异常终止时，操作系统将其内存的映像保存到名为`core`的文件中。 在本食谱中，我们将学习如何配置 Linux 来为崩溃的应用生成核心转储，以及如何使用 GDB 进行分析。\n\n# 怎么做……\n\n我们将确定未在 GDB 中运行的应用崩溃的根本原因：\n\n1.  在这个配方中，我们将使用与第一个配方相同的环境和测试应用。 请参考第一个配方的*步骤 1*至*7*来构建应用并将其复制到目标系统。\n2.  首先，我们需要为崩溃的应用启用核心转储的生成。 在大多数 Linux 发行版中，此功能在默认情况下处于关闭状态。 运行`ulimit -c`命令检查当前状态：\n\n```cpp\n$ ulimit -c\n```\n\n3.  前面命令报告的值是要生成的核心转储的最大大小。 零表示没有核心转储。 要增加限制，我们需要首先获得超级用户权限。 运行`su -`命令。 提示输入`Password`时，键入`root`：\n\n```cpp\n$ su -\nPassword:\n```\n\n4.  运行`ulimit -c unlimited`命令以允许任何大小的核心转储：\n\n```cpp\n# ulimit -c unlimited\n```\n\n5.  现在，通过按*Ctrl*+*D*或运行`logout`命令退出根 shell。\n6.  前面的命令仅更改了超级用户的核心转储限制。 要将其应用于当前用户，请在用户外壳中再次运行相同的命令：\n\n```cpp\n$ ulimit -c unlimited\n```\n\n7.  确保更改了限制：\n\n```cpp\n$ ulimit -c\nunlimited\n```\n\n8.  现在，像往常一样运行应用：\n\n```cpp\n$ ./loop \n```\n\n9.  它将崩溃，并出现异常。 运行`ls`命令检查当前目录中是否创建了核心文件：\n\n```cpp\n$ ls -l core\n-rw------- 1 dev dev 536576 May 31 00:54 core\n```\n\n10.  现在，运行`gdb`，将可执行文件和`core`文件作为参数传递：\n\n```cpp\n$ gdb ./loop core\n```\n\n11.  在 gdb shell 中，运行`bt`命令查看堆栈跟踪：\n\n```cpp\n(gdb) bt\n```\n\n12.  您可以看到与从`gdb`内部运行的应用相同的堆栈跟踪。 但是，在本例中，我们可以看到核心转储的堆栈跟踪。\n13.  此时，我们可以使用与第一个配方中相同的调试技术来缩小崩溃原因的范围。\n\n# 它是如何运作的..。\n\n核心转储功能是 Linux 和其他类 Unix 操作系统的标准功能。 然而，并不是在所有情况下都创建核心文件是可行的。 由于核心文件是进程内存的快照，因此它们在文件系统中可能占到兆字节甚至千兆字节。 在许多情况下，这是不可接受的。\n\n开发人员需要明确指定操作系统允许生成的核心文件的最大大小。 在其他限制中，可以使用`ulimit`命令设置此限制。\n\n我们运行两次`ulimit`来移除限制，首先是超级用户 root，然后是普通用户/开发人员。 由于普通用户限制不能超过超级用户限制，因此需要两个阶段的过程。\n\n在取消了核心文件大小的限制之后，我们在没有 gdb 的情况下运行测试应用。 不出所料，它崩溃了。 崩溃后，我们可以看到在当前目录中创建了一个名为`core`的新文件。\n\n当我们运行应用时，它会崩溃。 正常情况下，我们无法追踪坠机的根本原因。 但是，由于我们启用了核心转储，操作系统会自动为我们创建一个名为`core`的文件：\n\n![](img/a897ffd1-0aa8-4f4d-b1d3-a9e2941e9e77.png)\n\n核心文件是所有进程内存的二进制转储，但如果没有其他工具，很难对其进行分析。 值得庆幸的是，GDB 提供了必要的支持。\n\n我们运行 gdb，传递两个参数-可执行文件的路径和核心文件的路径。 在此模式下，我们不从 gdb 内部运行应用。 在核心转储发生崩溃的那一刻，我们已经冻结了它的状态。 Gdb 使用可执行文件将`core`文件中寻址的内存绑定到函数和变量名：\n\n![](img/8f81cb30-8138-4cd4-8688-2db1c3152d52.png)\n\n因此，即使应用不是从调试器运行的，也可以在交互式调试器中分析崩溃的应用。 当我们调用`bt`命令时，gdb 会显示崩溃时刻的堆栈跟踪：\n\n![](img/56155e9f-ad93-4de6-b78e-4511160d4840.png)\n\n这样，即使应用最初没有在调试器中运行，我们也可以确定应用崩溃的根本原因。\n\n# 还有更多的..。\n\n对于嵌入式应用，使用 GDB 分析核心转储是一种广泛使用且有效的实践。 但是，要使用 gdb 的全部功能，应用构建时应该支持调试符号。\n\n但是，在大多数情况下，嵌入式应用在部署和运行时没有调试符号，以减小二进制大小。 在这种情况下，核心转储的分析变得更加困难，可能需要了解特定体系结构的汇编语言和数据结构实现的内部结构。\n\n# 使用 gdbserver 进行调试\n\n嵌入式开发环境通常涉及两个系统-构建系统和目标系统，或仿真器。 虽然 gdb 的命令行界面即使对于低性能的嵌入式系统也是一个很好的选择，但在许多情况下，由于远程通信的高延迟，在目标系统上进行交互调试是不切实际的。\n\n在这种情况下，开发人员可以使用 GDB 提供的远程调试支持。 在此设置中，使用 gdbserver 在目标系统上启动嵌入式应用。 开发人员在构建系统上运行 gdb，并通过网络连接到 gdbserver。\n\n在本食谱中，我们将学习如何使用 gdb 和 gdbserver 开始调试应用。\n\n# 准备好了..。\n\n按照[第 2 章](02.html)、*设置环境*、*中的*连接到嵌入式系统*配方，使`hello`应用在目标系统上可用。*\n\n *# 怎么做……\n\n我们将使用前面配方中使用的相同应用，但现在我们将在不同的环境中运行 gdb 和应用：\n\n1.  切换到目标系统窗口，键入*Ctrl*+*D*从现有用户会话注销。\n2.  以`user`身份登录，使用`user`密码。\n3.  在`gdbserver`下运行`hello`应用：\n\n```cpp\n$ gdbserver 0.0.0.0:9090 ./hello\n```\n\n4.  切换到构建系统终端，将目录切换到`/mnt`：\n\n```cpp\n# cd /mnt\n```\n\n5.  运行`gdb`，将应用二进制文件作为参数传递：\n\n```cpp\n# gdb -q hello\n```\n\n6.  通过在 gdb 命令行中键入以下命令来配置远程连接：\n\n```cpp\ntarget remote X.X.X.X:9090\n```\n\n7.  最后，键入`continue`命令：\n\n```cpp\n continue\n```\n\n程序现在运行，我们可以看到它的输出并进行调试，就像它在本地运行一样。\n\n# 它是如何运作的..。\n\n首先，我们以超级用户身份登录到我们的目标系统并安装 gdbserver，除非它已经安装。 安装完成后，我们使用用户凭据再次登录并运行 gdbserver，传递要调试的应用的名称、IP 地址和要侦听传入连接的端口作为其参数。\n\n然后，我们切换到构建系统并在那里运行 gdb。 但是，我们不是直接在 gdb 中运行应用，而是指示 gdb 使用提供的 IP 地址和端口发起到远程主机的连接。 之后，您在 gdb 提示符下键入的所有命令都将传输到 gdbserver 并在那里执行。\n\n# 添加调试日志记录\n\n日志记录和诊断是任何嵌入式项目的一个重要方面。 在许多情况下，使用交互式调试器是不可能或不切实际的。 程序在断点处停止后，硬件状态可能会在几毫秒内发生变化，开发人员没有足够的时间对其进行分析。 对于高性能、多线程、时间敏感的嵌入式系统，收集详细的日志数据并使用工具进行分析和可视化是一种更好的方法。\n\n日志记录本身会带来一定的延迟。 首先，格式化日志消息并将其放入日志流需要时间。 其次，日志流应该可靠地存储在永久存储器(如闪存卡或磁盘驱动器)中，或者发送到远程系统。\n\n在本食谱中，我们将学习如何使用日志记录而不是交互式调试来查找问题的根本原因。 我们将使用不同日志级别的系统来最小化日志记录带来的延迟。\n\n# 怎么做……\n\n我们将修改应用以输出对根本原因分析有用的信息：\n\n1.  转到您的工作目录`~/test`，并复制`loop`项目目录。 将副本命名为`loop2`。 将目录更改为`loop2`。\n2.  使用文本编辑器打开`loop.cpp`文件。\n3.  再添加一个`include`：\n\n```cpp\n#include <iostream>\n#include <chrono>\n#include <thread>\n#include <functional>\n\n#include <syslog.h>\n```\n\n4.  通过向`syslog`函数添加调用来修改`runner`函数，如以下代码片段中突出显示的那样：\n\n```cpp\nvoid runner(std::chrono::milliseconds limit,\n            std::function<void(int)> fn,\n            int value) {\n  auto start = std::chrono::system_clock::now();\n  fn(value);\n  auto end = std::chrono::system_clock::now();\n  std::chrono::milliseconds delta =\n      std::chrono::duration_cast<std::chrono::milliseconds>(end - start);\n syslog(LOG_DEBUG, \"Delta is %ld\",\n         static_cast<long int>(delta.count()));\n  if (delta > limit) {\n syslog(LOG_ERR, \n \"Execution time %ld ms exceeded %ld ms limit\",\n static_cast<long int>(delta.count()),\n static_cast<long int>(limit.count()));\n    throw std::runtime_error(\"Time limit exceeded\");\n  }\n}\n```\n\n5.  同样，更新`main`函数以初始化并最终确定`syslog`：\n\n```cpp\nint main() {\n openlog(\"loop3\", LOG_PERROR, LOG_USER);\n  int max_delay = 10;\n  for (int i = 0; i < max_delay; i++) {\n    runner(std::chrono::milliseconds(max_delay), delay_ms, i);\n  }\n closelog();\n  return 0;\n}\n```\n\n6.  切换到构建系统终端。 转到`/mnt/loop2`目录并运行程序：\n\n```cpp\n# cmake && make\n```\n\n7.  将生成的`binary`文件循环复制到目标系统并运行：\n\n```cpp\n$ ./loop \n```\n\n调试输出非常详细，并提供了更多上下文来查找问题的根本原因。\n\n# 它是如何运作的..。\n\n在本配方中，我们使用标准日志记录工具`syslog`添加了日志记录。 首先，我们通过调用`openlog`来初始化日志记录：\n\n```cpp\n openlog(\"loop3\", LOG_PERROR, LOG_USER);\n```\n\n接下来，我们将日志记录添加到`runner`函数。 有不同的日志记录级别可帮助筛选日志消息，从最严重到最不严重。 我们使用`LOG_DEBUG`级别记录`delta`值，该值指示运行器调用的函数实际运行多长时间：\n\n```cpp\n syslog(LOG_DEBUG, \"Delta is %d\", delta);\n```\n\n此级别用于记录详细信息，这些信息对应用调试有帮助，但在生产中运行应用时可能会被证明过于冗长。\n\n但是，如果增量超过限制，我们将使用`LOG_ERR`级别记录此情况，以指示此情况不应正常发生，这是一个错误：\n\n```cpp\n syslog(LOG_ERR, \n \"Execution time %ld ms exceeded %ld ms limit\",\n static_cast<long int>(delta.count()),\n static_cast<long int>(limit.count()));\n```\n\n在从应用返回之前，我们关闭日志记录以确保所有日志消息都已正确保存：\n\n```cpp\n closelog();\n```\n\n当我们在目标系统上运行应用时，我们可以在屏幕上看到我们的日志消息：\n\n![](img/fee9835c-ae1e-48c7-ac4c-7d5061ab539c.png)\n\n因为我们使用标准的 Linux 日志记录，所以我们还可以在系统日志中找到消息：\n\n![](img/2aafea69-e7e5-4925-a431-9efec515aca3.png)\n\n正如您所看到的，日志记录并不难实现，但它对在调试和正常操作期间查找应用中各种问题的根本原因非常有帮助。\n\n# 还有更多的..。\n\n有许多记录库和框架可能比标准记录器更适合于特定任务；例如，*Boost.Log*，位于[https://theboostcpplibraries.com/boost.log](https://theboostcpplibraries.com/boost.log)，以及*SPDLOG*，位于[https://github.com/gabime/spdlog](https://github.com/gabime/spdlog)，*Boost.Log*，位于[https://theboostcpplibraries.com/boost.log](https://theboostcpplibraries.com/boost.log)，以及*spdlog*，位于[https://github.com/gabime/spdlog](https://github.com/gabime/spdlog)。 与`syslog`的通用 C 接口相比，它们提供了更方便的 C++ 接口。 在开始处理项目时，请检查现有的日志库，并选择最适合您需求的一个。\n\n# 使用调试版本和发布版本\n\n正如我们在前面的食谱中了解到的，日志记录有相关的成本。 它引入延迟来格式化日志消息，并将其写入永久存储或远程系统。\n\n通过跳过将某些消息写入日志文件，使用日志级别有助于降低开销。 但是，消息通常在传递给`log`函数之前进行格式化。 例如，在出现系统错误的情况下，开发人员希望将系统报告的错误代码添加到日志消息中。 尽管字符串格式化通常比将数据写入文件的成本要低，但对于高负载系统或资源有限的系统来说，它可能仍然是一个问题。\n\n编译器添加的调试符号不会增加运行时开销。 但是，它们会增加生成的二进制文件的大小。 此外，编译器进行的性能优化可能会使交互式调试变得困难。\n\n在本食谱中，我们将学习如何通过分离调试和发布版本并使用 C 预处理器宏来避免运行时开销。\n\n# 怎么做……\n\n我们将修改前面配方中使用的应用的构建规则，使其具有两个构建目标-调试和发布：\n\n1.  转到您的工作目录`~/test`，并复制`loop2`项目目录。 将副本命名为`loop3`。 将目录更改为`loop3`。\n2.  使用文本编辑器打开`CMakeLists.txt`文件。 替换以下行：\n\n```cpp\nSET(CMAKE_CXX_FLAGS \"-g --std=c++ 11\")\n```\n\n3.  前面的一行需要替换为以下行：\n\n```cpp\nSET(CMAKE_CXX_FLAGS_RELEASE \"--std=c++ 11\")\nSET(CMAKE_CXX_FLAGS_DEBUG \"${CMAKE_CXX_FLAGS_RELEASE} -g -DDEBUG\")\n```\n\n4.  使用文本编辑器打开`loop.cpp`文件。 通过添加突出显示的行来修改文件：\n\n```cpp\n#include <iostream>\n#include <chrono>\n#include <thread>\n#include <functional>\n#include <cstdarg>\n\n#ifdef DEBUG\n#define LOG_DEBUG(fmt, args...) fprintf(stderr, fmt, args)\n#else\n#define LOG_DEBUG(fmt, args...)\n#endif\n\nvoid runner(std::chrono::milliseconds limit,\n            std::function<void(int)> fn,\n            int value) {\n  auto start = std::chrono::system_clock::now();\n  fn(value);\n  auto end = std::chrono::system_clock::now();\n  std::chrono::milliseconds delta =\n      std::chrono::duration_cast<std::chrono::milliseconds>(end - start);\n LOG_DEBUG(\"Delay: %ld ms, max: %ld ms\\n\",\n            static_cast<long int>(delta.count()),\n            static_cast<long int>(limit.count()));\n  if (delta > limit) {\n    throw std::runtime_error(\"Time limit exceeded\");\n  }\n}\n```\n\n5.  切换到构建系统终端。 转到`/mnt/loop3`目录并运行以下代码：\n\n```cpp\n# cmake -DCMAKE_BUILD_TYPE=Release . && make\n```\n\n6.  将生成的`loop`二进制文件复制到目标系统并运行：\n\n```cpp\n$ ./loop \n```\n\n7.  如您所见，应用不会生成任何调试输出。 现在让我们使用`ls -l`命令检查它的大小：\n\n```cpp\n$ ls -l loop\n-rwxr-xr-x 1 dev dev 24880 Jun 1 00:50 loop\n```\n\n8.  生成的二进制文件的大小为 24KB。 现在，让我们构建`Debug`版本并进行比较，如下所示：\n\n```cpp\n$ cmake -DCMAKE_BUILD_TYPE=Debug && make clean && make\n```\n\n9.  检查可执行文件的大小：\n\n```cpp\n$ ls -l ./loop\n-rwxr-xr-x 1 dev dev 80008 Jun 1 00:51 ./loop\n```\n\n10.  现在，可执行文件的大小为 80KB。 它比发布版本大三倍多。 以与之前相同的方式运行它：\n\n```cpp\n$ ./loop \n```\n\n如您所见，现在的输出不同了。\n\n# 它是如何运作的..。\n\n我们从用于*添加调试日志记录*配方的项目副本开始，并创建两个不同的构建配置：\n\n*   **Debug**：支持交互式调试和调试日志记录的配置\n*   **版本**：高度优化的配置，在编译时禁用所有调试支持\n\n为了实现它，我们利用了`CMake`提供的功能。 它支持开箱即用的不同构建类型。 我们只需要为发布和调试版本分别定义编译选项。\n\n我们为发布版本定义的唯一构建标志是要使用的 C++ 标准。 我们明确要求代码符合 C++ 11 标准：\n\n```cpp\nSET(CMAKE_CXX_FLAGS_RELEASE \"--std=c++ 11\")\n```\n\n对于调试版本，我们重用了与发布版本相同的标志，将它们引用为`${CMAKE_CXX_FLAGS_RELEASE}`，并添加了另外两个选项。 `-g`指示编译器向目标可执行二进制文件添加调试符号，`-DDEBUG`定义预处理器宏`DEBUG`。\n\n我们在`loop.cpp`的代码中使用`DEBUG`宏来在`LOG_DEBUG`宏的两个不同实现之间进行选择。\n\n如果定义了`DEBUG`，则将`LOG_DEBUG`扩展为调用`fprintf`函数，该函数在标准错误通道中执行实际记录。 但是，如果未定义`DEBUG`，则将`LOG_DEBUG`扩展为空字符串。 这意味着在这种情况下，`LOG_DEBUG`不会生成任何代码，因此不会增加任何运行时开销。\n\n我们在 Runner 函数体中使用`LOG_DEBUG`来记录实际延迟和限制值。 请注意，在`LOG_DEBUG`周围没有`if`-格式化和记录数据或什么也不做的决定不是由程序在运行时做出的，而是由代码预处理器在构建应用时做出的。\n\n要选择构建类型，我们调用`cmake`，将构建类型的名称作为命令行参数传递：\n\n```cpp\ncmake -DCMAKE_BUILD_TYPE=Debug\n```\n\n`CMake`仅生成`Make`文件来实际构建我们调用`make`所需的应用。 我们可以在单个命令行中组合这两个命令：\n\n```cpp\ncmake -DCMAKE_BUILD_TYPE=Release && make\n```\n\n当我们第一次构建和运行我们的应用时，我们选择发布版本。 因此，我们看不到任何调试输出：\n\n![](img/55077ea0-cd5e-411c-82f1-286108dc17f0.png)\n\n之后，我们使用调试构建类型重新构建我们的应用，并在运行它时看到不同的结果：\n\n![](img/719acbab-871a-4922-88e4-07915e394e61.png)\n\n使用调试和发布版本，您可以获得足够的信息来进行舒适的调试，但请确保生产版本不会有任何不必要的开销。\n\n# 还有更多的..。\n\n在复杂项目中的发布版本和调试版本之间切换时，请确保所有文件都已正确重新生成。 要做到这一点，最简单的方法是删除所有以前的构建文件。 当使用`make`时，这可以通过调用`make clean`命令来完成。\n\n它可以与`cmake`和`make`一起作为命令行的一部分添加：\n\n```cpp\ncmake -DCMAKE_BUILD_TYPE=Debug && make clean && make\n```\n\n将所有这三个命令合并到一行中可以使开发人员更加方便。***"
  },
  {
    "path": "docs/emb-prog-mod-cpp-cb/06.md",
    "content": "# 六、内存管理\n\n内存效率是嵌入式应用的主要要求之一。 由于目标嵌入式平台的性能和内存能力通常有限，因此开发人员需要知道如何以最有效的方式使用可用内存。\n\n令人惊讶的是，最有效的方法并不一定意味着使用最少的内存量。 由于嵌入式系统是专门化的，开发人员提前知道哪些应用或组件将在系统上执行。 除非在同一系统中运行的另一个应用可以使用额外的内存，否则在一个应用中节省内存不会带来任何收益。 这就是为什么嵌入式系统中内存管理的最重要特征是确定性或可预测性。 知道应用在任何负载下都可以使用 2 兆字节的内存比知道应用在大多数情况下可以使用 1 兆字节的内存，但偶尔需要 3 兆字节要重要得多。\n\n同样，可预测性也适用于内存分配和释放时间。 在许多情况下，嵌入式应用倾向于花费更多内存来实现确定性计时。\n\n在本章中，我们将学习几种在嵌入式应用中广泛使用的内存管理技术。 本章介绍的食谱如下：\n\n*   使用动态内存分配\n*   浏览对象池\n*   使用环形缓冲区\n*   使用共享内存\n*   使用专用内存\n\n这些方法将帮助您理解内存管理最佳实践，并可在应用中使用内存分配时作为构建块使用。\n\n# 使用动态内存分配\n\n动态内存分配是 C++ 开发人员的一种普遍做法，在 C++ 标准库中得到了广泛的应用，但在嵌入式系统环境中，它往往成为难以发现和难以避免的问题的根源。\n\n最值得注意的问题是时机。 内存分配的最坏情况时间是不受限制的；但是，嵌入式系统，特别是那些控制真实进程或设备的系统，通常需要在特定的时间内做出响应。\n\n另一个问题是支离破碎。 当分配和释放不同大小的内存块时，出现的内存区域在技术上是空闲的，但由于它们太小而无法满足应用请求，因此无法分配。 内存碎片会随着时间的推移而增长，并可能导致内存分配请求失败，尽管可用内存总量相当大。\n\n避免此类问题的一个简单而强大的策略是在编译时或启动时预先分配应用可能需要的所有内存。 然后，应用根据需要使用该内存。 此内存一旦分配，在应用终止之前永远不会被释放。\n\n这种方法的一个缺点是，应用分配的内存比此时实际使用的内存多，而不是让其他应用使用。 实际上，这对于嵌入式应用来说不是问题，因为它们在受控环境中运行，在该环境中，所有应用及其内存需求都是事先知道的。\n\n# 怎么做……\n\n在本食谱中，我们将学习如何预分配内存并在稍后的应用中使用：\n\n1.  在您的工作`~/test`目录中，创建一个名为`prealloc`的子目录。\n2.  使用您喜欢的文本编辑器在`prealloc`子目录中创建名为`prealloc.cpp`的文件。 将以下代码片段复制到`prealloc.cpp`文件中以定义`SerialDevice`类：\n\n```cpp\n#include <cstdint>\n#include <string.h>\n\nconstexpr size_t kMaxFileNameSize = 256;\nconstexpr size_t kBufferSize = 4096;\nconstexpr size_t kMaxDevices = 16;\n\nclass SerialDevice {\n    char device_file_name[256];\n    uint8_t input_buffer[kBufferSize];\n    uint8_t output_buffer[kBufferSize];\n    int file_descriptor;\n    size_t input_length;\n    size_t output_length;\n\n  public:\n    SerialDevice():\n      file_descriptor(-1), input_length(0), output_length(0) {}\n\n    bool Init(const char* name) {\n      strncpy(device_file_name, name, sizeof(device_file_name));\n    }\n\n    bool Write(const uint8_t* data, size_t size) {\n      if (size > sizeof(output_buffer)) {\n        throw \"Data size exceeds the limit\";\n      }\n      memcpy(output_buffer, data, size);\n    }\n\n    size_t Read(uint8_t* data, size_t size) {\n      if (size < input_length) {\n        throw \"Read buffer is too small\";\n      }\n      memcpy(data, input_buffer, input_length);\n      return input_length;\n    }\n};\n```\n\n3.  添加使用`SerialDevice`类的`main`函数：\n\n```cpp\nint main() {\n  SerialDevice devices[kMaxDevices];\n  size_t number_of_devices = 0;\n\n  uint8_t data[] = \"Hello\";\n  devices[0].Init(\"test\");\n  devices[0].Write(data, sizeof(data));\n  number_of_devices = 1;\n\n  return 0;\n}\n```\n\n4.  在`loop`子目录中创建名为`CMakeLists.txt`的文件，内容如下：\n\n```cpp\ncmake_minimum_required(VERSION 3.5.1)\nproject(prealloc)\nadd_executable(prealloc prealloc.cpp)\n\nset(CMAKE_SYSTEM_NAME Linux)\nset(CMAKE_SYSTEM_PROCESSOR arm)\n\nSET(CMAKE_CXX_FLAGS \"--std=c++ 17\")\nset(CMAKE_CXX_COMPILER /usr/bin/arm-linux-gnueabi-g++)\n```\n\n现在您可以构建和运行应用了。 它不输出任何数据，因为它的目的是演示我们如何在不知道设备数量和与设备交换的消息大小的情况下预先分配内存。\n\n# 它是如何运作的..。\n\n在本配方中，我们定义了封装与串行设备的数据交换的对象。 设备由可变长度的设备文件名字符串标识。 我们可以向设备发送和从设备接收长度可变的消息。\n\n由于我们只能在运行时发现连接到系统的设备数量，因此我们可能会在发现设备对象时创建它。 同样，由于我们不知道发送和接收的消息的大小，因此为消息动态分配内存是很自然的。\n\n相反，我们预先分配未初始化设备对象的数组：\n\n```cpp\n  SerialDevice devices[kMaxDevices];\n```\n\n反过来，每个对象预分配足够数量的内存来存储消息和设备文件名：\n\n```cpp\n  char device_file_name[kMaxFileNameSize];\n  uint8_t input_buffer[kBufferSize];\n  uint8_t output_buffer[kBufferSize];\n```\n\n我们使用局部变量来跟踪输入和输出缓冲区中的实际数据大小。 不需要跟踪文件名的大小，因为它应该是以零结尾的：\n\n```cpp\n  size_t input_length;\n  size_t output_length;\n```\n\n同样，我们跟踪发现的实际设备数量：\n\n```cpp\n  size_t number_of_devices = 0;\n```\n\n这样，我们就避免了动态内存分配。 然而，这是有代价的：我们人为地限制了设备的最大数量和我们支持的消息的最大大小。 其次，大量分配的内存从未被使用过。 例如，如果我们最多支持 16 个设备，而系统中只有 1 个设备，那么我们实际上只使用了已分配内存的 1/16。 如前所述，这对于嵌入式系统来说不是问题，因为所有应用及其需求都是预定义的。 没有任何应用可以从它可以分配的额外内存中受益。\n\n# 浏览对象池\n\n正如我们在本章的第一个配方中讨论的那样，预先分配应用使用的所有内存是一种有效的策略，可以帮助嵌入式应用避免与内存碎片和分配时间相关的各种陷阱。\n\n自组织存储器预分配的一个缺点是应用现在负责跟踪预分配的对象使用情况。\n\n对象池旨在通过提供通用且方便的接口来隐藏对象跟踪的负担，该接口类似于动态内存分配，但使用预先分配的数组中的对象。\n\n# 怎么做……\n\n在本食谱中，我们将创建一个简单的对象池实现，并学习如何在您的应用中使用它：\n\n1.  在您的工作`~/test`目录中，创建一个名为`objpool`的子目录。\n2.  使用您喜欢的文本编辑器在`objpool`子目录中创建`objpool.cpp`文件。 让我们定义一个模板化的`ObjectPool`类。 我们从私有数据成员和构造函数开始：\n\n```cpp\n#include <iostream>\n\ntemplate<class T, size_t N>\nclass ObjectPool {\n  private:\n    T objects[N];\n    size_t available[N];\n    size_t top = 0;\n  public:\n    ObjectPool(): top(0) {\n      for (size_t i = 0; i < N; i++) {\n        available[i] = i;\n      }\n    }\n```\n\n3.  现在，让我们添加一个从池中获取元素的方法：\n\n```cpp\n    T& get() {\n      if (top < N) {\n        size_t idx = available[top++ ];\n        return objects[idx];\n      } else {\n        throw std::runtime_error(\"All objects are in use\");\n      }\n    }\n```\n\n4.  接下来，我们添加一个将元素返回到池的方法：\n\n```cpp\n    void free(const T& obj) {\n      const T* ptr = &obj;\n      size_t idx = (ptr - objects) / sizeof(T);\n      if (idx < N) {\n        if (top) {\n          top--;\n          available[top] = idx;\n        } else {\n          throw std::runtime_error(\"Some object was freed more than once\");\n        }\n      } else {\n        throw std::runtime_error(\"Freeing object that does not belong to\n       the pool\");\n      }\n     }\n```\n\n5.  然后，用一个小函数来包装类定义，该函数返回池中请求的元素数量：\n\n```cpp\n    size_t requested() const { return top; }\n    };\n```\n\n6.  定义要存储在对象池中的数据类型，如以下代码所示：\n\n```cpp\nstruct Point {\n  int x, y;\n};\n```\n\n7.  然后添加使用对象池的代码：\n\n```cpp\nint main() {\n  ObjectPool<Point, 10> points;\n\n  Point& a = points.get();\n  a.x = 10; a.y=20;\n  std::cout << \"Point a (\" << a.x << \", \" << a.y << \") initialized, requested \"        <<\n    points.requested() << std::endl;\n\n  Point& b = points.get();\n  std::cout << \"Point b (\" << b.x << \", \" << b.y << \") not initialized, requested \" <<\n    points.requested() << std::endl;\n\n  points.free(a);\n  std::cout << \"Point a(\" << a.x << \", \" << a.y << \") returned, requested \" <<\n    points.requested() << std::endl;\n\n  Point& c = points.get();\n  std::cout << \"Point c(\" << c.x << \", \" << c.y << \") not intialized, requested \" <<\n    points.requested() << std::endl;\n\n  Point local;\n  try {\n    points.free(local);\n  } catch (std::runtime_error e) {\n    std::cout << \"Exception caught: \" << e.what() << std::endl;\n  }\n  }\n```\n\n8.  在`loop`子目录中创建名为`CMakeLists.txt`的文件，内容如下：\n\n```cpp\ncmake_minimum_required(VERSION 3.5.1)\nproject(objpool)\nadd_executable(objpool objpool.cpp)\n\nset(CMAKE_SYSTEM_NAME Linux)\nset(CMAKE_SYSTEM_PROCESSOR arm)\n\nSET(CMAKE_CXX_FLAGS \"--std=c++ 11\")\nset(CMAKE_CXX_COMPILER /usr/bin/arm-linux-gnueabi-g++)\n```\n\n9.  构建应用并将生成的可执行二进制文件复制到目标系统。 使用[第 2 章](02.html)，*设置环境*中的食谱来完成。\n10.  切换到目标系统终端。 如果需要，使用用户凭据登录。\n11.  运行二进制文件。\n\n# 它是如何运作的..。\n\n在这个应用中，我们使用与第一个配方中相同的概念(预先分配的对象的静态数组)；但是，我们将其包装到一个模板化的`ObjectPool`类中，以提供一个通用接口来处理不同类型的对象。\n\n我们的模板有两个参数-存储在`ObjectPool`类的实例中的对象的类或数据类型，以及池大小。 这些参数用于定义类的两个私有数据字段-对象数组和自由索引数组：\n\n```cpp\n     T objects[N];\n     size_t available[N];\n```\n\n由于模板参数是在编译时解析的，因此这些数组是静态分配的。 此外，该类有一个名为`top`的私有数据成员，它充当`available`数组中的索引，并指向下一个可用对象。\n\n可用数组包含`objects`数组中当前可供使用的所有对象的索引。 一开始，所有对象都是空闲的，可用数组中所有元素的索引填充到可用数组中：\n\n```cpp\n      for (size_t i = 0; i < N; i++) {\n        available[i] = i;\n      }\n```\n\n当应用需要从池中获取元素时，它会调用`get`方法。 此方法使用 top 变量获取池中下一个可用元素的索引：\n\n```cpp\n      size_t idx = available[top++ ];\n      return objects[idx];\n```\n\n当`top`索引达到数组的大小时，意味着不能再分配更多的元素，因此该方法抛出一个异常来指示错误情况：\n\n```cpp\n      throw std::runtime_error(\"All objects are in use\");\n```\n\n可以使用`free`将对象返回到池中。 首先，它根据元素的地址检测元素的索引。 索引计算为对象地址和池起始地址之间的差值。 由于池对象是连续存储在内存中的，因此我们可以很容易地筛选出相同类型的对象，但不能筛选出源自该池的对象：\n\n```cpp\n      const T* ptr = &obj;\n      size_t idx = (ptr - objects) / sizeof(T);\n```\n\n注意，因为`size_t`类型是无符号的，所以我们不需要检查结果索引是否小于零-这是不可能的。 如果我们尝试将一个对象返回到不属于它的池中，并且该对象的地址小于池的起始地址，那么它无论如何都会被视为正索引。\n\n如果我们返回的对象属于池，我们将更新顶部计数器，并将结果索引放入可用数组中以供进一步使用：\n\n```cpp\n  top--;\n  available[top] = idx;\n```\n\n否则，我们抛出一个异常，指示我们试图返回一个不是从该池中取出的对象：\n\n```cpp\n     throw std::runtime_error(\"Freeing object that does not belong to the pool\");\n```\n\n请求的方法用于跟踪池对象的使用情况。 它返回 top 变量，该变量有效地跟踪已声明但尚未返回池的对象的数量。\n\n```cpp\n     size_t requested() const { return top; }\n```\n\n让我们定义一个数据类型，并尝试使用池中的对象。 我们声明一个名为`Point`的结构，该结构包含两个`int`字段，如以下代码所示：\n\n```cpp\n struct Point {\n  int x, y;\n };\n```\n\n现在，我们创建一个大小为`10`的`Point`对象池：\n\n```cpp\n    ObjectPool<Point, 10> points;\n```\n\n我们从池中获取一个对象并填充其数据字段：\n\n```cpp\n Point& a = points.get();\n a.x = 10; a.y=20;\n```\n\n该程序会生成以下输出：\n\n![](img/aac88c6f-a95e-44b3-8e8c-3173dac428a9.png)\n\n输出的第一行根据请求报告一个对象。\n\n我们再请求一个对象并按原样打印其数据字段，而不进行任何初始化。 不出所料，池报告请求了两个对象。\n\n现在，我们将第一个对象返回到池中，并确保请求的对象数量减少。 我们还可以注意到，即使在将对象返回到池之后，我们也可以从其中读取数据。\n\n让我们再从池子里认领一件物品。 请求的计数增加，但请求的对象与我们在上一步返回的对象相同。\n\n我们可以看到，`Point c`在从池中取出后没有初始化，但是它的字段包含与`Point a`相同的值。 实际上，现在`a`和`c`是对池中同一对象的引用，因此修改变量`a`会影响变量`c`。 这是我们实现对象池的限制之一。\n\n最后，我们创建一个本地`Point`对象，并尝试将其返回到池中：\n\n```cpp\n  Point local;\n  try {\n    points.free(local);\n  } catch (std::runtime_error e) {\n    std::cout << \"Exception caught: \" << e.what() << std::endl;\n  }\n```\n\n预计它会因例外而失败，事实也的确如此。 在程序输出中，您可以看到`Exception caught: Freeing object that does not belong to the pool`消息。\n\n# 还有更多的..。\n\n尽管对象池的实现简化了对预分配对象的处理，但它也有许多限制。\n\n首先，所有对象都是从一开始就创建的。 因此，调用池的`get`方法不会触发对象构造函数，调用`free`方法也不会调用析构函数。 开发人员需要使用各种解决方法来初始化和取消初始化对象。\n\n一种可能的解决方法是定义目标对象的特殊方法，如`initialize`和`deinitialize`，它们将分别由`ObjectPool`类的`get`和`free`方法调用。 然而，这种方法将类的实现耦合到`ObjectPool`实现。 在本章的后面部分，我们将介绍克服这一限制的更高级技术。\n\n我们的池实现不会检测是否为一个对象多次调用了`free`方法。 这是一个错误，但它很常见，并且会导致难以调试的问题。 虽然在技术上可行，但它增加了实现的额外复杂性，这对于本例来说是不必要的。\n\n# 使用环形缓冲区\n\n环形缓冲区或循环缓冲区是嵌入式世界中广泛使用的数据结构。 它的工作方式是放置在固定大小内存阵列顶部的队列。 缓冲区可以包含固定数量的元素。 生成这些元素的函数按顺序逐个将它们放入缓冲区。 当到达缓冲区的末尾时，它切换到缓冲区的开头，就好像它的第一个元素跟在最后一个元素之后一样。\n\n在组织独立且不能相互等待的数据生产者和消费者之间的数据交换时，这种设计已被证明是非常高效的，这是嵌入式开发中的常见场景。 例如，当中断被禁用时，中断服务例程应该快速将来自设备的数据排队以进行进一步处理。 如果它落后，它就不能等待处理数据的函数。 同时，处理功能不需要与**中断服务例程**(**ISR**)完全同步；它可以一次处理多个元素，并在稍后赶上 ISR。\n\n这一点，再加上环可以静态预分配的事实，使得环缓冲区在许多情况下成为最佳选择。\n\n# 怎么做……\n\n在本食谱中，我们将学习如何在 C++ 数组之上创建和使用环形缓冲区：\n\n1.  在您的工作`~/test`目录中，创建一个名为`ringbuf`的子目录。\n2.  使用您喜欢的文本编辑器在`ringbuf`子目录中创建`ringbuf.cpp`文件。\n3.  从`private`数据字段开始定义`RingBuffer`类：\n\n```cpp\n#include <iostream>\n\ntemplate<class T, size_t N>\nclass RingBuffer {\n  private:\n    T objects[N];\n    size_t read;\n    size_t write;\n    size_t queued;\n  public:\n    RingBuffer(): read(0), write(0), queued(0) {}\n```\n\n4.  现在我们添加一个将数据推送到缓冲区的方法：\n\n```cpp\n    T& push() {\n      T& current = objects[write];\n      write = (write + 1) % N;\n      queued++ ;\n      if (queued > N) {\n        queued = N;\n        read = write;\n      }\n      return current;\n    }\n\n```\n\n5.  接下来，我们添加一个从缓冲区拉取数据的方法：\n\n```cpp\n    const T& pull() {\n      if (!queued) {\n        throw std::runtime_error(\"No data in the ring buffer\");\n      }\n      T& current = objects[read];\n      read = (read + 1) % N;\n      queued--;\n      return current;\n    }\n```\n\n6.  让我们添加一个小方法来检查缓冲区是否包含任何数据，并结束类定义：\n\n```cpp\nbool has_data() {\n  return queued != 0;\n}\n};\n```\n\n7.  定义了`RingBuffer`之后，我们现在可以添加使用它的代码。 首先，让我们定义要使用的数据类型：\n\n```cpp\nstruct Frame {\n  uint32_t index;\n  uint8_t data[1024];\n};\n```\n\n8.  其次，添加`main`函数并将`RingBuffer`的实例定义为其变量，以及尝试使用空缓冲区的代码：\n\n```cpp\nint main() {\n  RingBuffer<Frame, 10> frames;\n\n  std::cout << \"Frames \" << (frames.has_data() ? \"\" : \"do not \")\n      << \"contain data\" << std::endl;\n  try {\n    const Frame& frame = frames.pull();\n  } catch (std::runtime_error e) {\n    std::cout << \"Exception caught: \" << e.what() << std::endl;\n  }\n```\n\n9.  接下来，在缓冲区中添加使用五个元素的代码：\n\n```cpp\nfor (size_t i = 0; i < 5; i++) {\nFrame& out = frames.push();\nout.index = i;\nout.data[0] = 'a' + i;\nout.data[1] = '\\0';\n  }\nstd::cout << \"Frames \" << (frames.has_data() ? \"\" : \"do not \")\n<< \"contain data\" << std::endl;\nwhile (frames.has_data()) {\nconst Frame& in = frames.pull();\n    std::cout << \"Frame \" << in.index << \": \" << in.data << std::endl;\n  }\n```\n\n10.  在此之后，添加处理大量可以添加的元素的类似代码：\n\n```cpp\n    for (size_t i = 0; i < 26; i++) {\n    Frame& out = frames.push();\n    out.index = i;\n    out.data[0] = 'a' + i;\n    out.data[1] = '\\0';\n    }\n    std::cout << \"Frames \" << (frames.has_data() ? \"\" : \"do not \")\n      << \"contain data\" << std::endl;\n    while (frames.has_data()) {\n    const Frame& in = frames.pull();\n    std::cout << \"Frame \" << in.index << \": \" << in.data << std::endl;\n    }\n    }\n```\n\n11.  在`loop`子目录中创建名为`CMakeLists.txt`的文件，内容如下：\n\n```cpp\ncmake_minimum_required(VERSION 3.5.1)\nproject(ringbuf)\nadd_executable(ringbuf ringbuf.cpp)\n\nset(CMAKE_SYSTEM_NAME Linux)\nset(CMAKE_SYSTEM_PROCESSOR arm)\n\nSET(CMAKE_CXX_FLAGS \"--std=c++ 11\")\nset(CMAKE_CXX_COMPILER /usr/bin/arm-linux-gnueabi-g++)\n```\n\n12.  构建应用并将生成的可执行二进制文件复制到目标系统。 使用[第 2 章](02.html)，*设置环境*中的食谱来完成。\n13.  切换到目标系统终端。 如果需要，使用用户凭据登录。\n14.  运行二进制文件。\n\n# 它是如何运作的..。\n\n我们将环形缓冲区实现为具有三个私有数据字段的模板化 C++ 类：\n\n*   `objects`：类型为`T`的`N`元素的静态数组\n*   `read`：从中读取元素的索引\n*   `write`：要将元素写入的索引\n\n`RingBuffer`类公开三个公共方法：\n\n*   `push()`：将数据写入缓冲区\n*   `pull()`：从缓冲区读取数据\n*   `has_data()`：检查缓冲区是否包含数据\n\n让我们仔细看看它们是如何工作的。\n\n`push()`方法旨在由函数用于在缓冲区中存储数据。 与动态队列或动态堆栈的类似`push()`方法(接受将值存储为参数)不同，我们的实现不接受任何参数。 由于所有元素都是在编译时预分配的，因此它返回对缓冲区中要更新的值的引用。\n\n`push()`方法的实现很简单；它通过`write`索引获取指向元素的指针，然后推进`write`索引并增加存储在缓冲区中的元素数量。 请注意，当`write`索引达到大小限制时，如何使用除法余数运算符将`write`索引换行到数组的开头：\n\n```cpp\nT& current = objects[write];\nwrite = (write + 1) % N;\nqueued++ ;\n```\n\n如果我们试图推送超过`objects`数组容量所能处理的元素，会发生什么情况呢？ 这取决于我们计划存储在缓冲区中的数据的性质。 在我们的实现中，我们假设接收方对最近的数据感兴趣，并且如果不能赶上发送方，可以容忍中间数据的丢失。 如果接收方速度太慢，在接收方`read`数据之前，发送方运行多少圈都无关紧要：此时所有超过`N`步的数据都会被覆盖。 这就是为什么，一旦存储的元素数量超过`N`，我们就开始将`read`索引与`write`索引一起前进，以使它们正好保持`N`步的距离：\n\n```cpp\n if (queued > N) {\n  queued = N;\n  read = write;\n }\n```\n\n`pull()`方法由从缓冲区读取数据的函数使用。 与`push()`方法类似，它不接受任何参数，并返回对缓冲区中元素的引用。 不过，与`push()`方法不同的是，它返回一个常量引用(如下面的代码所示)，以指示它不应该修改缓冲区中的数据：\n\n```cpp\n const T& pull() {\n```\n\n首先，它检查缓冲区中是否有数据，如果缓冲区不包含元素，则抛出异常：\n\n```cpp\n  if (!queued) {\n   throw std::runtime_error(\"No data in the ring buffer\");\n  }\n```\n\n它通过读取索引获取对元素的引用，然后推进`read`索引，应用与`push()`方法对`write`索引相同的除法余数运算符：\n\n```cpp\n  read = (read + 1) % N;\n  queued--;\n```\n\n`has_data()`方法的实现很简单。 如果对象计数器为零，则返回`false`，否则返回`true`：\n\n```cpp\n  bool has_data() {\n  return queued != 0;\n  }\n```\n\n现在，让我们在行动中试一试。 我们声明一个简单的数据结构`Frame`，它模仿设备生成的数据。 它包含一个帧索引和一个不透明的数据缓冲区：\n\n```cpp\n  uint32_t index;\n  uint8_t data[1024];\n  };\n```\n\n我们定义一个容量为`10`个`frame` 类型元素的环形缓冲区：\n\n```cpp\n  RingBuffer<Frame, 10> frames;\n```\n\n让我们来看看程序输出：\n\n![](img/45ab92d8-96c5-42ce-aee0-b49bd991217a.png)\n\n首先，如预期的那样，我们尝试从空缓冲区读取并获得异常。\n\n然后，我们使用拉丁字母字符作为数据有效负载，将五个元素写入缓冲区：\n\n```cpp\n  for (size_t i = 0; i < 5; i++) {\n    Frame& out = frames.push();\n    out.index = i;\n    out.data[0] = 'a' + i;\n    out.data[1] = '\\0';\n  }\n```\n\n注意我们如何获取对元素的引用，然后就地更新它，而不是将`frame`的本地副本推入环形缓冲区。 然后我们读取缓冲区中的所有数据并将其打印在屏幕上：\n\n```cpp\n  while (frames.has_data()) {\n    const Frame& in = frames.pull();\n    std::cout << \"Frame \" << in.index << \": \" << in.data << std::endl;\n  }\n```\n\n程序输出表明，我们可以成功读取所有五个元素。 现在，我们尝试将拉丁字母表中的 26 个字母全部写入数组，远远超出了数组的容量。\n\n```cpp\n for (size_t i = 0; i < 26; i++) {\n    Frame& out = frames.push();\n    out.index = i;\n    out.data[0] = 'a' + i;\n    out.data[1] = '\\0';\n  }\n```\n\n然后，我们以与读取五种元素相同的方式读取数据。 读取成功，但我们只收到写入的最后 10 个元素；此时所有其他帧都已丢失并被覆盖。 这对于我们的示例应用并不重要，但对于许多其他应用来说可能是不可接受的。 确保数据不会丢失的最佳方法是保证接收方比发送方更频繁地激活。 有时，如果缓冲区中没有可用的数据，接收器将被激活，但为了避免数据丢失，这是可以接受的代价。\n\n# 使用共享内存\n\n在支持**MMU**(简写为**内存管理单元**)的硬件上运行的现代操作系统中，每个应用都作为一个进程运行，并将其内存与其他应用隔离。\n\n这种隔离带来了重要的可靠性好处。 一个应用不可能意外损坏另一个应用的内存。 同样，意外损坏自身内存并崩溃的应用可以由操作系统关闭，而不会影响系统中的其他应用。 将嵌入式系统的功能解耦到几个孤立的应用中，这些应用通过定义明确的 API 相互通信，显著降低了实现的复杂性，从而提高了稳定性。\n\n然而，与世隔绝是要付出代价的。 由于每个进程都有自己的独立地址空间，因此两个应用之间的数据交换意味着数据复制、上下文切换和操作系统内核同步机制的使用，这可能相对昂贵。\n\n共享内存是许多操作系统提供的一种机制，用于将某些内存区域声明为共享。 这样，应用可以在不复制的情况下交换数据。 这对于交换大型数据对象(例如视频帧或音频样本)尤其重要。\n\n# 怎么做……\n\n在本食谱中，我们将学习如何使用 Linux 共享内存 API 在两个或多个应用之间进行数据交换：\n\n1.  在您的工作`~/test`目录中，创建一个名为`shmem`的子目录。\n2.  使用您喜欢的文本编辑器在`shmem`子目录中创建`shmem.cpp`文件。 定义`SharedMem`类，从公共标头和常量开始：\n\n```cpp\n#include <algorithm>\n#include <iostream>\n#include <chrono>\n#include <thread>\n\n#include <sys/mman.h>\n#include <fcntl.h>\n#include <unistd.h>\n\nconst char* kSharedMemPath = \"/sample_point\";\nconst size_t kPayloadSize = 16;\n\nusing namespace std::literals;\n\ntemplate<class T>\nclass SharedMem {\n  int fd;\n  T* ptr;\n  const char* name;\n\n  public:\n```\n\n3.  然后，定义一个完成大部分工作的构造函数：\n\n```cpp\nSharedMem(const char* name, bool owner=false) {\nfd = shm_open(name, O_RDWR | O_CREAT, 0600);\nif (fd == -1) {\nthrow std::runtime_error(\"Failed to open a shared memory region\");\n}\nif (ftruncate(fd, sizeof(T)) < 0) {\nclose(fd);\nthrow std::runtime_error(\"Failed to set size of a shared memory \nregion\");\n};\nptr = (T*)mmap(nullptr, sizeof(T), PROT_READ | PROT_WRITE, \nMAP_SHARED, fd, 0);\nif (!ptr) {\nclose(fd);\n    throw std::runtime_error(\"Failed to mmap a shared memory region\");\n}\n    this->name = owner ? name : nullptr;\n    std::cout << \"Opened shared mem instance \" << name << std::endl;\n}\n```\n\n4.  添加析构函数的定义：\n\n```cpp\n    ~SharedMem() {\n      munmap(ptr, sizeof(T));\n      close(fd);\n      if (name) {\n        std::cout << \"Remove shared mem instance \" << name << std::endl;\n        shm_unlink(name);\n      }\n      }\n```\n\n5.  使用一个返回对共享对象的引用的小方法完成类定义：\n\n```cpp\n    T& get() const {\n      return *ptr;\n    }\n    };\n```\n\n6.  我们的`SharedMem`类可以使用不同的数据类型。 让我们声明一个我们想要使用的自定义数据结构：\n\n```cpp\nstruct Payload {\n  uint32_t index;\n  uint8_t raw[kPayloadSize];\n};\n```\n\n7.  现在添加将数据写入共享内存的代码：\n\n```cpp\nvoid producer() {\n  SharedMem<Payload> writer(kSharedMemPath);\n  Payload& pw = writer.get();\n  for (int i = 0; i < 5; i++) {\n    pw.index = i;\n    std::fill_n(pw.raw, sizeof(pw.raw) - 1, 'a' + i);\n    pw.raw[sizeof(pw.raw) - 1] = '\\0';\n    std::this_thread::sleep_for(150ms);\n  }\n}\n```\n\n8.  另外，添加从共享内存读取数据的代码：\n\n```cpp\nvoid consumer() {\n  SharedMem<Payload> point_reader(kSharedMemPath, true);\n  Payload& pr = point_reader.get();\n  for (int i = 0; i < 10; i++) {\n    std::cout << \"Read data frame \" << pr.index << \": \" << pr.raw << std::endl;\n    std::this_thread::sleep_for(100ms);\n  }\n  }\n```\n\n9.  添加`main`函数将所有内容绑定在一起，如以下代码所示：\n\n```cpp\nint main() {\n\n  if (fork()) {\n    consumer();\n  } else {\n    producer();\n  }\n  }\n```\n\n10.  在`loop`子目录中创建名为`CMakeLists.txt`的文件，内容如下：\n\n```cpp\ncmake_minimum_required(VERSION 3.5.1)\nproject(shmem)\nadd_executable(shmem shmem.cpp)\ntarget_link_libraries(shmem rt)\n\nset(CMAKE_SYSTEM_NAME Linux)\nset(CMAKE_SYSTEM_PROCESSOR arm)\n\nSET(CMAKE_CXX_FLAGS \"--std=c++ 14\")\nset(CMAKE_CXX_COMPILER /usr/bin/arm-linux-gnueabi-g++)\n```\n\n11.  构建应用并将生成的可执行二进制文件复制到目标系统。 使用[第 2 章](02.html)，*设置环境*中的食谱来完成。\n12.  切换到目标系统终端。 如果需要，使用用户凭据登录。\n13.  运行二进制文件。\n\n# 它是如何运作的..。\n\n在本配方中，我们使用**POSIX**(**Portable Operating System Interface**)API 来处理共享内存。 这是一个灵活且细粒度的 C API，有许多可以调优或配置的参数。 我们的目标是通过在底层 API 上实现一个更方便、类型安全的 C++ 包装器来隐藏它的复杂性。 我们将使用**RAII**(**资源获取的缩写是 Initialization**)习惯用法来确保所有分配的资源都被正确释放，并且我们的应用中不会有内存或文件描述符泄漏。\n\n我们定义了一个模板化的`SharedMem`类。 模板参数定义了存储在共享内存实例中的数据类型。 这样，我们使`SharedMem`类类型的实例变得安全。 与我们在应用代码中使用空指针和强制转换类型不同，C++ 编译器会自动为我们执行此操作：\n\n```cpp\ntemplate<class T>\nclass SharedMem {\n```\n\n所有共享内存分配和初始化都在`SharedMem`构造函数中实现。 它接受两个参数：\n\n*   共享内存对象名称\n*   所有权标志\n\nPOSIX 定义了一个`shm_open`API，其中共享内存对象由名称标识，类似于文件名。 这样，使用相同名称的两个独立进程可以引用相同的共享内存对象。 共享对象的生命周期是多少？ 当为同一对象名调用`shm_unlink`函数时，共享对象被销毁。 如果该对象由多个进程使用，则第一个调用`shm_open`的进程将创建该对象，其他进程将重用同一对象。 但它们中的哪一个要为它的删除负责呢？ 这就是所有权标志的用途。 当设置为`true`时，它表示`SharedMem`实例在被销毁时负责清除共享对象。\n\n构造函数顺序调用三个 POSIX API 函数。 首先，它使用`shm_open`创建一个共享对象。 虽然函数接受访问标志和文件权限作为参数，但我们始终使用读写访问模式，对当前用户进行读写访问：\n\n```cpp\nfd = shm_open(name, O_RDWR | O_CREAT, 0600);\n```\n\n接下来，我们使用`ftruncate`调用定义共享区域的大小。 为此，我们使用模板数据类型的大小：\n\n```cpp\nif (ftruncate(fd, sizeof(T)) < 0) {\n```\n\n最后，我们使用`mmap`函数将共享区域映射到我们的进程内存地址空间。 它返回一个指针，我们可以使用该指针引用数据实例：\n\n```cpp\nptr = (T*)mmap(nullptr, sizeof(T), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);\n```\n\n该对象将共享内存块的文件描述符和指向内存区域的指针作为其私有成员。 当对象被销毁时，析构函数释放它们。 如果设置了所有者标志，我们还会保留对象名称，以便可以将其删除：\n\n```cpp\nint fd;\nT* ptr;\nconst char* name;\n```\n\n`SharedMem`析构函数将共享内存对象从地址空间取消映射：\n\n```cpp\n munmap(ptr, sizeof(T));\n```\n\n如果对象是所有者，我们可以使用`shm_unlink`调用删除它。 请注意，我们不再需要 Owner 标志，因为名称设置为`nullptr`，除非对象是 Owner：\n\n```cpp\n if (name) {\n   std::cout << \"Remove shared mem instance \" << name << std::endl;\n shm_unlink(name);\n }\n```\n\n要访问共享数据，该类提供了一个简单的`get`方法。 它返回对存储在共享内存中的对象的引用：\n\n```cpp\n  T& get() const {\n      return *ptr;\n  }\n```\n\n让我们创建两个使用我们创建的共享内存 API 的独立进程。 我们使用 POSIX`fork`函数来派生子进程。 子流程将是数据生产者，父流程将是数据使用者：\n\n```cpp\n  if (fork()) {\n    consumer();\n  } else {\n    producer();\n  }\n```\n\n我们定义了`Payload`数据类型，生产者和消费者都使用该数据类型进行数据交换：\n\n```cpp\n  struct Payload {\n  uint32_t index;\n  uint8_t raw[kPayloadSize];\n  };\n```\n\n数据生成器创建一个`SharedMem`实例：\n\n```cpp\n  SharedMem<Payload> writer(kSharedMemPath);\n```\n\n它使用通过`get`方法接收到的引用，每 150 毫秒更新一次共享对象。 每次，它都会递增有效负载的索引字段，并使用与索引匹配的拉丁字母填充其数据。\n\n消费者和生产者一样简单。 它创建一个与生产者同名的`SharedMem`实例，但声明该对象的所有权。 这意味着它将负责删除它，如以下代码所示：\n\n```cpp\n  SharedMem<Payload> point_reader(kSharedMemPath, true);\n```\n\n运行应用并观察以下输出：\n\n![](img/714b3428-c62d-4794-b090-b8a3bd2a72ee.png)\n\n每隔 100 毫秒，应用就会从共享对象读取数据并将其打印到屏幕上。 在消费者输出中，我们可以看到它接收生产者写入的数据。 由于消费者周期和生产者周期的持续时间不匹配，我们可以看到，有时会读取相同的数据两次\n\n本例中有意省略的一个重要逻辑部分是生产者和消费者的同步。 因为它们作为独立的项目运行，所以不能保证生产者在消费者尝试读取数据时已经更新了任何数据。 以下是我们在结果输出中看到的内容：\n\n```cpp\nOpened shared mem instance /sample_point\nRead data frame 0: \nOpened shared mem instance /sample_point\n```\n\n我们可以看到，在生产者打开相同的对象之前，消费者打开了共享内存对象并读取了一些数据。\n\n同样，不能保证当消费者尝试读取数据字段时，生产者会完全更新数据字段。 我们将在下一章更详细地讨论这个主题。\n\n# 还有更多的..。\n\n共享内存本身是一种快速高效的进程间通信机制，但当与环形缓冲区结合使用时，它确实大放异彩。 通过将环形缓冲区放入共享内存，开发人员允许独立的数据生产者和数据消费者异步交换数据，并且同步开销最小。\n\n# 使用专用内存\n\n嵌入式系统通常在特定的存储器地址范围内提供对其外部设备的访问。 当程序访问此类区域中的地址时，它不会读取或写入内存中的值。 相反，数据被发送到设备或从映射到该地址的设备读取。\n\n该技术通常命名为**MMIO**(缩写为**内存映射输入**/**输出**)。 在本食谱中，我们将学习如何从用户空间 Linux 应用使用 MMIO 访问 Raspberry PI 的外部设备。\n\n# 怎么做……\n\nRaspberry PI 有许多可通过 MMIO 访问的外部设备。 为了演示 MMIO 的工作原理，我们的应用将访问系统计时器：\n\n1.  在您的工作`~/test`目录中，创建一个名为`timer`的子目录。\n2.  使用您喜欢的文本编辑器在`timer`子目录中创建名为`timer.cpp`的文件。\n3.  将所需的标头、常量和类型声明放入`timer.cpp`：\n\n```cpp\n#include <iostream>\n#include <chrono>\n#include <system_error>\n#include <thread>\n\n#include <fcntl.h>\n#include <sys/mman.h>\n\nconstexpr uint32_t kTimerBase = 0x3F003000;\n\nstruct SystemTimer {\n  uint32_t CS;\n  uint32_t counter_lo;\n  uint32_t counter_hi;\n};\n```\n\n4.  添加`main`函数，该函数包含程序的所有逻辑：\n\n```cpp\nint main() {\n\n  int memfd = open(\"/dev/mem\", O_RDWR | O_SYNC);\n  if (memfd < 0) {\n  throw std::system_error(errno, std::generic_category(),\n  \"Failed to open /dev/mem. Make sure you run as root.\");\n  }\n\n  SystemTimer *timer = (SystemTimer*)mmap(NULL, sizeof(SystemTimer),\n  PROT_READ|PROT_WRITE, MAP_SHARED,\n  memfd, kTimerBase);\n  if (timer == MAP_FAILED) {\n  throw std::system_error(errno, std::generic_category(),\n  \"Memory mapping failed\");\n  }\n\n  uint64_t prev = 0;\n  for (int i = 0; i < 10; i++) {\n   uint64_t time = ((uint64_t)timer->counter_hi << 32) + timer->counter_lo;\n   std::cout << \"System timer: \" << time;\n   if (i > 0) {\n   std::cout << \", diff \" << time - prev;\n    }\n    prev = time;\n    std::cout << std::endl;\n    std::this_thread::sleep_for(std::chrono::milliseconds(10));\n  }\n  return 0;\n }\n```\n\n5.  在`timer`子目录中创建名为`CMakeLists.txt`的文件，内容如下：\n\n```cpp\ncmake_minimum_required(VERSION 3.5.1)\nproject(timer)\nadd_executable(timer timer.cpp)\n\nset(CMAKE_SYSTEM_NAME Linux)\nset(CMAKE_SYSTEM_PROCESSOR arm)\n\nSET(CMAKE_CXX_FLAGS \"--std=c++ 11\")\n\nset(CMAKE_CXX_COMPILER /usr/bin/arm-linux-gnueabi-g++)\n```\n\n6.  现在您可以构建和运行应用了。\n\nPlease note that it should be run under `root` on a real Raspberry PI 3 device.\n\n# 它是如何运作的..。\n\n系统定时器是使用 MMIO 接口连接到处理器的外部设备。 这意味着它有一个专用的物理地址范围，每个地址都有特定的格式和用途。\n\n我们的应用使用表示为两个 32 位值的定时器计数器。 它们组合在一起，形成一个 64 位只读计数器，在系统运行时始终递增。\n\n对于 Raspberry PI 3，分配给系统定时器的物理内存地址范围偏移了以下值-`0x3F003000`(根据 Raspberry PI 硬件版本的不同，可能会有所不同)。 我们把它定义为一个常数。\n\n```cpp\nconstexpr uint32_t kTimerBase = 0x3F003000;\n```\n\n要访问区域内的各个字段，我们定义了一个`SystemTimer`结构：\n\n```cpp\nstruct SystemTimer {\n  uint32_t CS;\n  uint32_t counter_lo;\n  uint32_t counter_hi;\n};\n```\n\n现在，我们需要获取指向定时器地址范围的指针，并将其转换为指向`SystemTimer`的指针。 这样，我们就可以通过读取`SystemTimer`数据字段来访问计数器的地址。\n\n然而，有一个问题我们需要解决。 我们知道物理地址空间中的偏移量，但是我们的 Linux 应用在虚拟地址空间中工作。 我们需要找到一种将物理地址映射到虚拟地址的方法。\n\nLinux 使用特殊的`/proc/mem`文件提供对物理内存地址的访问。 由于它包含所有物理内存的快照，因此只能通过`root`访问。\n\n我们使用`open`函数将其作为常规文件打开：\n\n```cpp\nint memfd = open(\"/dev/mem\", O_RDWR | O_SYNC);\n```\n\n一旦文件打开，并且我们知道它的描述符，我们就可以将其映射到我们的虚拟地址空间。 我们不需要映射整个物理内存。 与计时器相关的区域就足够了；这就是为什么我们将系统计时器范围开始作为偏移量参数，将`SystemTimer`结构的大小作为大小参数：\n\n```cpp\nSystemTimer *timer = (SystemTimer*)mmap(NULL, sizeof(SystemTimer),\nPROT_READ|PROT_WRITE, MAP_SHARED, memfd, kTimerBase);\n```\n\n现在我们可以访问计时器字段了。 我们读取循环中的计时器计数器，并显示其当前值及其与前一个值的方差。 当我们以`root`身份运行应用时，会得到以下输出：\n\n![](img/aa941e90-c2ed-49d6-a79c-c813bc3b95aa.png)\n\n正如我们所看到的，从该内存地址读取将返回递增的值。 差值在 10,000 左右，而且相当恒定。 由于我们在计数器读取循环中添加了 10 毫秒的延迟，因此我们可以推断内存地址与计时器关联，而不是与常规内存关联，并且计时器计数器的粒度为 1 微秒。\n\n# 还有更多的..。\n\nRaspberry PI 有许多可通过 MMIO 访问的外部设备。 您可以在位于[https://www.raspberrypi.org/documentation/hardware/raspberrypi/bcm2835/BCM2835-ARM-Peripherals.pdf](https://www.raspberrypi.org/documentation/hardware/raspberrypi/bcm2835/BCM2835-ARM-Peripherals.pdf)的*BCM2835ARM 外设手册*中找到有关其地址范围和访问语义的详细信息\n\n请注意，开发人员在使用可由多个设备同时访问的内存时必须格外小心。 当内存可由多个处理器或同一处理器的多个内核访问时，您可能需要使用高级同步技术(如内存屏障)来避免同步问题。 我们将在下一章中讨论其中的一些问题。 如果使用**直接内存访问**(**DMA**)或 MMIO，事情会变得更加复杂。 由于 CPU 可能不知道外部硬件更改了内存，因此其高速缓存可能不同步，从而导致数据一致性问题。"
  },
  {
    "path": "docs/emb-prog-mod-cpp-cb/07.md",
    "content": "# 七、多线程和同步\n\n嵌入式平台跨越了计算能力的广阔版图。 有只有几千字节内存的微控制器；有功能强大的**片上系统**(**SoCS**)；有能同时运行多个应用的多核 CPU。\n\n随着更多的计算资源可供嵌入式开发人员使用，以及他们可以在其上构建更复杂的应用，多线程支持变得非常重要。 开发人员需要知道如何将他们的应用并行化，以有效地利用所有 CPU 核心。 我们将学习如何编写能够高效、安全地利用所有可用 CPU 核心的应用。\n\n在本章中，我们将介绍以下主题：\n\n*   探索 C++ 中的线程支持\n*   探索数据同步\n*   使用条件变量\n*   使用原子变量\n*   使用 C++ 内存模型\n*   探索无锁同步\n*   在共享内存中使用原子变量\n*   探索异步功能和未来\n\n这些方法可以用作构建您自己的高效多线程和多处理同步代码的示例。\n\n# 探索 C++ 中的线程支持\n\n在 C++ 11 之前，线程完全不属于 C++ 语言的范围。 开发人员可以使用特定于平台的库，例如 pthread 或 Win32**应用编程接口**(**API**)。 由于每个库都有自己的行为，将应用移植到另一个平台需要大量的开发和测试工作。\n\nC++ 11 引入了线程作为 C++ 标准的一部分，并定义了一组类来在其标准库中创建多线程应用。\n\n在本食谱中，我们将学习如何使用 C++ 在单个应用中生成多个并发线程。\n\n# 怎么做……\n\n在本食谱中，我们将学习如何创建两个并发运行的工作线程。\n\n1.  在您的`~/test`工作目录中，创建一个名为`threads`的子目录。\n2.  使用您喜欢的文本编辑器在`threads`子目录中创建`threads.cpp`文件。 将代码片段复制到`threads.cpp`文件中：\n\n```cpp\n#include <chrono>\n#include <iostream>\n#include <thread>\n\nvoid worker(int index) {\n  for (int i = 0; i < 10; i++) {\n    std::cout << \"Worker \" << index << \" begins\" << std::endl;\n    std::this_thread::sleep_for(std::chrono::milliseconds(50));\n    std::cout << \"Worker \" << index << \" ends\" << std::endl;\n    std::this_thread::sleep_for(std::chrono::milliseconds(1));\n  }\n}\n\nint main() {\n  std::thread worker1(worker, 1);\n  std::thread worker2(worker, 2);\n  worker1.join();\n  worker2.join();\n  std::cout << \"Done\" << std::endl;\n}\n```\n\n3.  在`loop`子目录下创建名为`CMakeLists.txt`的文件，内容如下：\n\n```cpp\ncmake_minimum_required(VERSION 3.5.1)\nproject(threads)\nadd_executable(threads threads.cpp)\n\nset(CMAKE_SYSTEM_NAME Linux)\nset(CMAKE_SYSTEM_PROCESSOR arm)\n\nSET(CMAKE_CXX_FLAGS \"--std=c++ 11\")\ntarget_link_libraries(threads pthread)\n\nset(CMAKE_CXX_COMPILER /usr/bin/arm-linux-gnueabi-g++)\n```\n\n您可以构建和运行应用。\n\n# 它是如何运作的..。\n\n在此应用中，我们定义了一个名为`worker`的函数。 为了使代码简单，它没有做太多有用的工作，只打印`Worker X`开始和`Worker X`结束 10 次，消息之间有 50 毫秒的延迟。\n\n在`main`函数中，我们创建了两个工作线程`worker1`和`worker2`：\n\n```cpp\n std::thread worker1(worker, 1);\n std::thread worker2(worker, 2);\n```\n\n我们将两个参数传递给线程构造函数：\n\n*   在线程中运行的函数。\n*   函数的参数。 由于我们将先前定义的`worker`函数作为线程函数传递，因此参数应该与其类型匹配-在我们的示例中，它是`int`。\n\n这样，我们定义了两个执行相同工作但索引不同的工作线程-`1`和`2`。\n\n创建线程后，它们立即开始运行；不需要调用任何其他方法来启动它们。 它们完全并发执行，正如我们从程序输出中看到的那样：\n\n![](img/5772496b-0c8a-4c02-96c1-b9494da7fe2c.png)\n\n我们的工作线程的输出是混合的，有时甚至是乱码的，比如`Worker Worker 1 ends2 ends`。 之所以会发生这种情况，是因为终端的输出也在并发工作。\n\n由于辅助线程是独立执行的，因此在创建辅助线程之后，主线程不会执行任何操作。 但是，如果主线程的执行到达`main`函数的末尾，则程序终止。 为了避免这种情况，我们为每个工作线程添加了对`join`方法的调用。 此方法会一直阻塞，直到线程终止。 这样，我们只在两个工作线程完成工作后才退出主程序。\n\n# 探索数据同步\n\n数据同步是任何处理多个执行线程的应用的重要方面。 不同的线程通常需要访问相同的变量或内存区域。 由两个或多个独立线程同时写入同一内存可能会导致数据损坏。 即使在另一个线程更新变量的同时读取变量也是危险的，因为在读取时它只能部分更新。\n\n为了避免这些问题，并发线程可以使用所谓的同步原语，即使对共享内存的访问具有确定性和可预测性的 API。\n\n与线程支持的情况类似，C++ 语言在 C++ 11 标准之前没有提供任何同步原语。 从 C++ 11 开始，许多同步原语作为标准的一部分被添加到 C++ 标准库中。\n\n在本食谱中，我们将学习如何使用互斥锁和锁保护来同步对变量的访问。\n\n# 怎么做……\n\n在前面的配方中，我们了解了如何完全并发运行两个工作线程，并注意到这可能会导致终端的输出出错。 我们将修改前面配方中的代码，使用互斥锁和锁保护添加同步，看看有什么不同。\n\n1.  在您的`~/test`工作目录中，创建一个名为`mutex`的子目录。\n2.  使用您喜欢的文本编辑器在`mutex`子目录中创建`mutex.cpp`文件。 将代码片段复制到`mutex.cpp`文件中：\n\n```cpp\n#include <chrono>\n#include <iostream>\n#include <mutex>\n#include <thread>\n\nstd::mutex m;\n\nvoid worker(int index) {\n  for (int i = 0; i < 10; i++) {\n    {\n std::lock_guard<std::mutex> g(m);\n std::cout << \"Worker \" << index << \" begins\" << std::endl;\n std::this_thread::sleep_for(std::chrono::milliseconds(50));\n std::cout << \"Worker \" << index << \" ends\" << std::endl;\n }\n    std::this_thread::sleep_for(std::chrono::milliseconds(1));\n  }\n}\n\nint main() {\n  std::thread worker1(worker, 1);\n  std::thread worker2(worker, 2);\n  worker1.join();\n  worker2.join();\n  std::cout << \"Done\" << std::endl;\n}\n```\n\n3.  在`loop`子目录下创建名为`CMakeLists.txt`的文件，内容如下：\n\n```cpp\ncmake_minimum_required(VERSION 3.5.1)\nproject(mutex)\nadd_executable(mutex mutex.cpp)\n\nset(CMAKE_SYSTEM_NAME Linux)\nset(CMAKE_SYSTEM_PROCESSOR arm)\n\nSET(CMAKE_CXX_FLAGS \"--std=c++ 11\")\ntarget_link_libraries(mutex pthread)\n\nset(CMAKE_CXX_COMPILER /usr/bin/arm-linux-gnueabi-g++)\n```\n\n您可以构建和运行应用。\n\n# 它是如何运作的..。\n\n在构建并运行应用之后，我们可以看到它的输出类似于线程应用的输出。 然而，也有明显的不同之处：\n\n![](img/36c850c0-19b0-49b4-a851-d1878279476c.png)\n\n首先，输出没有乱码。 其次，我们可以看到一个清晰的顺序--没有工人被另一个工人打断，每个开始后面跟着相应的结束。 区别在于突出显示的源代码片段。 我们创建一个全局`mutex m`：\n\n```cpp\nstd::mutex m;\n```\n\n然后，我们使用`lock_guard`来保护代码的关键部分，它从打印`Worker X begins`的行开始，到打印`Worker X ends`的行结束。\n\n`lock_guard`是互斥锁之上的包装器，它使用**RAII**(**Resource Acquisition 是 Initialization**)技术在定义锁对象时自动锁定构造函数中的相应互斥锁，并在到达其作用域结束后在析构函数中解锁它。 这就是为什么我们添加额外的花括号来定义临界区的范围：\n\n```cpp\n    {\n      std::lock_guard<std::mutex> g(m);\n      std::cout << \"Worker \" << index << \" begins\" << std::endl;\n      std::this_thread::sleep_for(std::chrono::milliseconds(50));\n      std::cout << \"Worker \" << index << \" ends\" << std::endl;\n    }\n```\n\n虽然可以通过调用互斥锁的 lock 和 unlock 方法来显式锁定和解锁互斥锁，但不建议这样做。 忘记解锁锁定的互斥会导致多线程同步问题，这些问题很难检测，也很难调试。 RAII 方法自动解锁互斥锁，使代码更安全、更易于阅读和理解。\n\n# 还有更多的..。\n\n线程同步的正确实现需要非常注意细节和透彻的分析。 多线程应用中的一个非常常见的问题是死锁。 这是一种线程被阻塞的情况，因为它正在等待另一个线程，而另一个线程又因为它在等待第一线程而被阻塞。 结果，两个线程被无限阻塞。\n\n如果同步需要两个或多个互斥锁，则会发生死锁。 C++ 17 引入了*std：：SCOPED_LOCK*，可从[https://en.cppreference.com/w/cpp/thread/scoped_lock](https://en.cppreference.com/w/cpp/thread/scoped_lock)获得，它是多个互斥锁的 RAII 包装器，有助于避免死锁。\n\n# 使用条件变量\n\n我们了解了如何从两个或多个线程同步对同一变量的同时访问。 线程访问变量的特定顺序并不重要；我们只防止同时读取和写入变量。\n\n线程等待另一个线程开始处理数据是一种常见的场景。 在这种情况下，当数据可用时，第一线程应该通知第二个线程。 它可以使用 C++ 从 C++ 11 标准开始支持的条件变量来完成。\n\n在本食谱中，一旦数据可用，我们将学习如何使用条件变量在单独的线程中激活数据处理。\n\n# 怎么做……\n\n我们将实现一个具有两个工作线程的应用，类似于我们在*探索数据同步*食谱中创建的应用。\n\n1.  在您的`~/test`工作目录中，创建一个名为`condvar`的子目录。\n2.  使用您喜欢的文本编辑器在`condvar`子目录中创建`condv.cpp`文件。\n\n3.  现在，我们在`condvar.cpp`中放置所需的标头并定义全局变量：\n\n```cpp\n#include <condition_variable>\n#include <iostream>\n#include <mutex>\n#include <thread>\n#include <vector>\n\nstd::mutex m;\nstd::condition_variable cv;\nstd::vector<int> result;\nint next = 0;\n```\n\n4.  定义全局变量后，我们添加`worker`函数，该函数类似于前面配方中的`worker`函数：\n\n```cpp\nvoid worker(int index) {\n  for (int i = 0; i < 10; i++) {\n    std::unique_lock<std::mutex> l(m);\n    cv.wait(l, [=]{return next == index; });\n    std::cout << \"worker \" << index << \"\\n\";\n    result.push_back(index);\n    next = next + 1;\n    if (next > 2) { next = 1; };\n    cv.notify_all();\n  }\n}\n```\n\n5.  最后，我们定义入口点-`main`函数：\n\n```cpp\nint main() {\n  std::thread worker1(worker, 1);\n  std::thread worker2(worker, 2);\n  {\n    std::lock_guard<std::mutex> l(m);\n    next = 1;\n  }\n  std::cout << \"Start\\n\";\n  cv.notify_all();\n  worker1.join();\n  worker2.join();\n  for (int e : result) {\n    std::cout << e << ' ';\n  }\n  std::cout << std::endl;\n}\n```\n\n6.  在`loop`子目录下创建名为`CMakeLists.txt`的文件，内容如下：\n\n```cpp\ncmake_minimum_required(VERSION 3.5.1)\ncmake_minimum_required(VERSION 3.5.1)\nproject(condvar)\nadd_executable(condvar condvar.cpp)\n\nset(CMAKE_SYSTEM_NAME Linux)\nset(CMAKE_SYSTEM_PROCESSOR arm)\n\nSET(CMAKE_CXX_FLAGS \"--std=c++ 11\")\ntarget_link_libraries(condvar pthread)\n\nset(CMAKE_CXX_COMPILER /usr/bin/arm-linux-gnueabi-g++)\n```\n\n您可以构建和运行应用。\n\n# 它是如何运作的..。\n\n与我们在*探索数据同步*配方中创建的应用类似，我们创建了两个工作线程`worker1`和`worker2`，它们使用相同的`worker`函数线程，只是`index`参数不同。\n\n除了将消息打印到控制台之外，工作线程还更新全局矢量结果。 每个 Worker 只需将其索引添加到其循环中的`result`变量中，如以下命令所示：\n\n```cpp\nstd::vector<int> result;\n```\n\n我们希望每个 Worker 只在轮流时将其索引添加到结果中-`worker 1`，然后是`worker 2`，然后是`worker 1`，依此类推。 没有同步是不可能做到这一点的；然而，使用互斥锁的简单同步是不够的。 它可以保证两个并发线程不会同时访问代码的同一临界区，但不能保证顺序。 在`worker 2`锁定互斥之前，`worker 1`可能会再次锁定互斥。\n\n为了解决排序问题，我们定义了一个`cv`条件变量和一个`next`整数变量：\n\n```cpp\nstd::condition_variable cv;\nint next = 0;\n```\n\n`next`变量包含工作者的索引。 它使用`0`进行初始化，并在`main`函数中设置为特定的工作者索引。 由于此变量是从多个线程访问的，因此我们在锁保护的保护下执行此操作：\n\n```cpp\n  {\n    std::lock_guard<std::mutex> l(m);\n    next = 1;\n  }\n```\n\n尽管工作线程在创建后开始执行，但它们都会立即在条件变量上被阻塞，等待`next`变量的值与其索引匹配。 条件变量需要`std::unique_lock`才能等待。 我们在调用`wait`方法之前创建它：\n\n```cpp\nstd::unique_lock<std::mutex> l(m);\ncv.wait(l, [=]{return next == index; });\n```\n\n虽然在`main`函数中将条件变量`cv`设置为`1`，但这是不够的。 我们需要显式通知等待条件变量的线程。 我们使用`notify_all`方法完成此操作：\n\n```cpp\ncv.notify_all();\n```\n\n这将唤醒所有等待的线程，并将它们的索引与`next`变量进行比较。 匹配的线程解除阻塞，所有其他线程再次进入休眠状态。\n\n活动线程向控制台写入一条消息并更新`result`变量。 然后，它更新`next`变量以选择下一个要激活的线程。 我们递增索引，直到其达到最大值，然后将其重置为`1`：\n\n```cpp\nnext = next + 1;\nif (next > 2) { next = 1; };\n```\n\n与`main`函数中的代码类似，在确定`next`线程的索引后，我们需要调用`notify_all`来唤醒所有线程，并让它们决定该轮到谁工作：\n\n```cpp\ncv.notify_all();\n```\n\n当工作线程工作时，`main`函数等待它们完成：\n\n```cpp\n worker1.join();\n worker2.join();\n```\n\n当所有工作线程完成时，将打印`result`变量的值：\n\n```cpp\n  for (int e : result) {\n    std::cout << e << ' ';\n  }\n```\n\n在构建并运行我们的程序之后，我们将获得以下输出：\n\n![](img/e9547f19-9f61-4307-bed0-e7fa66406e5a.png)\n\n正如我们所看到的，所有线程都是按预期顺序激活的。\n\n# 还有更多的..。\n\n在这个配方中，我们只使用了条件变量对象提供的几个方法。 除了简单的`wait`函数外，还有等待特定时间或等待到指定时间点的函数。 在[https://en.cppreference.com/w/cpp/thread/condition_variable](https://en.cppreference.com/w/cpp/thread/condition_variable)参考页面了解有关*C++ 条件变量类*的更多信息。\n\n# 使用原子变量\n\n原子变量之所以这样命名，是因为它们不能部分读取或写入。 例如，比较`Point`和`int`数据类型：\n\n```cpp\nstruct Point {\n  int x, y;\n};\n\nPoint p{0, 0};\nint b = 0;\n\np = {10, 10};\nb = 10;\n```\n\n在本例中，修改`p`变量相当于两个赋值：\n\n```cpp\np.x = 10;\np.y = 10;\n```\n\n这意味着读取`p`变量的任何并发线程都可能获得部分修改的数据，如`x=10`、`y=0`，这可能会导致难以检测和难以重现的错误计算。 这就是对此类数据类型的访问应该同步的原因。\n\n那么`b`变量呢？ 可以部分修改吗？ 答案是：是的，取决于平台。 但是，C++ 提供了一组数据类型和模板，以确保变量作为一个整体以原子方式一次全部更改。\n\n在本食谱中，我们将学习如何使用原子变量进行多线程同步。 由于原子变量不能部分修改，因此不需要使用互斥锁或其他昂贵的同步原语。\n\n# 怎么做……\n\n我们将创建一个应用，该应用派生两个工作线程来并发更新数据数组。 我们将使用原子变量而不是互斥锁来确保并发更新是安全的。\n\n1.  在您的`~/test`工作目录中，创建一个名为`atomic`的子目录。\n2.  使用您喜欢的文本编辑器在`atomic`子目录中创建一个`atomic.cpp`文件。\n\n3.  现在，我们放置所需的标头，并在`atomic.cpp`中定义全局变量：\n\n```cpp\n#include <atomic>\n#include <chrono>\n#include <iostream>\n#include <thread>\n#include <vector>\n\nstd::atomic<size_t> shared_index{0};\nstd::vector<int> data;\n```\n\n4.  定义全局变量后，我们添加`worker`函数。 它类似于前面配方中的`worker`函数，但除了`index`之外，它还有一个额外的参数-`timeout`：\n\n```cpp\nvoid worker(int index, int timeout) {\n  while(true) {\n  size_t worker_index = shared_index.fetch_add(1);\n  if (worker_index >= data.size()) {\n      break;\n  }\n  std::cout << \"Worker \" << index << \" handles \"\n              << worker_index << std::endl;\n  data[worker_index] = data[worker_index] * 2;\n    std::this_thread::sleep_for(std::chrono::milliseconds(timeout));\n  }\n  }\n```\n\n5.  最后，我们定义入口点-`main`函数：\n\n```cpp\nint main() {\n  for (int i = 0; i < 10; i++) {\n    data.emplace_back(i);\n  }\n  std::thread worker1(worker, 1, 50);\n  std::thread worker2(worker, 2, 20);\n  worker1.join();\n  worker2.join();\n  std::cout << \"Result: \";\n  for (auto& v : data) {\n    std::cout << v << ' ';\n  }\n  std::cout << std::endl;\n}\n```\n\n6.  在`loop`子目录下创建名为`CMakeLists.txt`的文件，内容如下：\n\n```cpp\ncmake_minimum_required(VERSION 3.5.1)\nproject(atomic)\nadd_executable(atomic atomic.cpp)\n\nset(CMAKE_SYSTEM_NAME Linux)\nset(CMAKE_SYSTEM_PROCESSOR arm)\n\nSET(CMAKE_CXX_FLAGS \"--std=c++ 11\")\ntarget_link_libraries(atomic pthread)\n\nset(CMAKE_CXX_COMPILER /usr/bin/arm-linux-gnueabi-g++)\n```\n\n您可以构建和运行应用。\n\n# 它是如何运作的..。\n\n我们正在创建一个使用多个工作线程更新数组所有元素的应用。 对于昂贵的更新操作，此方法可以在多核平台上带来显著的性能提升。\n\n困难在于在多个工作线程之间共享工作，因为每个工作线程处理数据元素可能需要不同的时间。\n\n我们使用`shared_index`原子变量来存储尚未被任何工作线程声明的下一个元素的索引。 此变量以及要处理的数组被声明为全局变量：\n\n```cpp\nstd::atomic<size_t> shared_index{0};\nstd::vector<int> data;\n```\n\n我们的`worker`函数类似于早期配方中的`worker`函数，但有重要区别。 首先，它有一个额外的参数`timeout`。 这用于模拟处理每个元素所需时间的差异。\n\n其次，我们的工作线程不是固定的迭代次数，而是在循环中运行，直到`shared_index`变量达到最大值。 这表示所有元素都已处理，工作进程可以终止。\n\n在每次迭代中，一个 Worker 读取`shared_index`的值。 如果有要处理的元素，它会将`shared_index`变量的值存储在局部`worker_index`变量中，同时递增`shared_index`变量。\n\n虽然可以按照与常规变量相同的方式使用原子变量-首先获取其当前值，然后递增变量-但这可能会导致争用情况。 两个辅助线程几乎可以同时读取该变量。 在本例中，它们都获得相同的值，然后开始处理相同的元素，相互干扰。 这就是为什么我们使用一种特殊的方法`fetch_add`，该方法递增变量并将其在递增前的值作为一个不可中断的操作返回：\n\n```cpp\nsize_t worker_index = shared_index.fetch_add(1);\n```\n\n如果`worker_index`变量达到数组的大小，则意味着所有元素都已处理，并且 Worker 可以终止：\n\n```cpp\nif (worker_index >= data.size()) {\n      break;\n}\n```\n\n如果`worker_index`变量有效，则工作器使用它通过此索引更新数组元素的值。 在我们的示例中，我们只需将其乘以`2`：\n\n```cpp\ndata[worker_index] = data[worker_index] * 2;\n```\n\n为了模拟昂贵的数据操作，我们使用自定义延迟。 延迟的持续时间由`timeout`参数确定：\n\n```cpp\nstd::this_thread::sleep_for(std::chrono::milliseconds(timeout));\n```\n\n在`main`函数中，我们将要处理的元素添加到数据向量中。 我们使用循环用 0 到 9 之间的数字填充向量：\n\n```cpp\nfor (int i = 0; i < 10; i++) {\n    data.emplace_back(i);\n}\n```\n\n在初始数据集准备好之后，我们创建两个工作线程，提供`index`和`timeout`参数。 使用工作线程的不同超时来模拟不同的性能：\n\n```cpp\n std::thread worker1(worker, 1, 50);\n std::thread worker2(worker, 2, 20);\n```\n\n然后，我们等待两个工作线程完成它们的作业，并将结果打印到控制台。 当我们构建和运行我们的应用时，我们会得到以下输出：\n\n![](img/c34579d8-b62e-4c9e-bcbe-a4441c2d5e89.png)\n\n正如我们所看到的，`Worker 2`比`Worker 1`处理了更多的元素，因为它的超时时间是 20 毫秒，而`Worker 1`是 50 毫秒。 此外，所有元素都按照预期进行了处理，没有遗漏和重复。\n\n# 还有更多的..。\n\n我们学习了如何使用整数原子变量。 虽然这种类型的原子变量是最常用的，但 C++ 也允许定义其他类型的原子变量，包括非整数类型，只要它们是普通的可复制、复制可构造和复制可赋值的。\n\n除了我们在示例中使用的`fetch_add`方法之外，原子变量还有其他类似的方法，可以帮助开发人员在单个操作中查询值和修改变量。 考虑使用这些方法来避免争用条件或使用互斥锁进行代价高昂的同步。\n\n在 C++ 20 中，原子变量接收`wait`、`notify_all`和`notify_one`方法，类似于条件变量的方法。 它们允许通过使用更高效、更轻量级的原子变量来实现以前需要条件变量的逻辑。\n\n有关原子变量的更多信息，请参见[https://en.cppreference.com/w/cpp/atomic/atomic](https://en.cppreference.com/w/cpp/atomic/atomic)。\n\n# 使用 C++ 内存模型\n\n从 C++ 11 标准开始，C++ 将用于线程和同步的 API 和原语定义为该语言的一部分。 具有多个处理器核心的系统中的存储器同步是复杂的，因为现代处理器可以通过重新排序指令来优化代码执行。 即使在使用原子变量时，也不能保证以所需的顺序修改或访问数据，因为编译器可以更改顺序。\n\n为了避免歧义，C++ 11 引入了内存模型，定义了并发访问内存区域的行为。 作为内存模型的一部分，C++ 定义了`std::memory_order`枚举，它向编译器提供有关预期访问模型的提示。 这有助于编译器以不干扰预期代码行为的方式优化代码。\n\n在本食谱中，我们将学习如何使用最简单的`std::memory_order`枚举形式来实现共享计数器变量。\n\n# 怎么做……\n\n我们正在实现一个应用，该应用具有一个共享计数器，该计数器由两个并发工作线程递增。\n\n1.  在您的`~/test`工作目录中，创建一个名为`memorder`的子目录。\n2.  使用您喜欢的文本编辑器在`atomic`子目录中创建`memorder.cpp`文件。\n3.  现在，我们在`memorder.cpp`中放置所需的标头并定义全局变量：\n\n```cpp\n#include <atomic>\n#include <chrono>\n#include <iostream>\n#include <thread>\n#include <vector>\n\nstd::atomic<bool> running{true};\nstd::atomic<int> counter{0};\n```\n\n4.  定义全局变量后，我们添加`worker`函数。 该函数仅递增计数器，然后在特定的时间间隔内休眠：\n\n```cpp\nvoid worker() {\n while(running) {\n counter.fetch_add(1, std::memory_order_relaxed);\n }\n }\n```\n\n5.  然后，我们定义我们的`main`函数：\n\n```cpp\nint main() {\n  std::thread worker1(worker);\n  std::thread worker2(worker);\n  std::this_thread::sleep_for(std::chrono::seconds(1));\n  running = false;\n  worker1.join();\n  worker2.join();\n  std::cout << \"Counter: \" << counter << std::endl;\n}\n```\n\n6.  在`loop`子目录下创建名为`CMakeLists.txt`的文件，内容如下：\n\n```cpp\ncmake_minimum_required(VERSION 3.5.1)\nproject(memorder)\nadd_executable(memorder memorder.cpp)\n\nset(CMAKE_SYSTEM_NAME Linux)\nset(CMAKE_SYSTEM_PROCESSOR arm)\n\nSET(CMAKE_CXX_FLAGS \"--std=c++ 11\")\ntarget_link_libraries(memorder pthread)\n\nset(CMAKE_CXX_COMPILER /usr/bin/arm-linux-gnueabi-g++)\n```\n\n您可以构建和运行应用。\n\n# 它是如何运作的..。\n\n在我们的应用中，我们将创建两个工作线程，它们将递增共享计数器，并让它们运行特定的时间量。\n\n作为第一步，我们定义两个全局原子变量`running`和`counter`：\n\n```cpp\nstd::atomic<bool> running{true};\nstd::atomic<int> counter{0};\n```\n\n`running`变量是一个二进制标志。 当它设置为`true`时，工作线程应该保持运行。 在它更改为`false`之后，工作线程应该终止。\n\n`counter`变量是我们的共享计数器。 工作线程将同时递增它。 我们使用在使用原子变量配方的*中已经使用的`fetch_add`方法。 它用于自动递增变量。 在此配方中，我们将一个额外的参数`std::memory_order_relaxed`传递给此方法：*\n\n```cpp\ncounter.fetch_add(1, std::memory_order_relaxed);\n```\n\n这一论点是一个提示。 虽然原子性和修改的一致性很重要，并且对于计数器的实现应该得到保证，但是并发存储器访问之间的顺序并不那么重要。 `std::memory_order_relaxed`为原子变量定义了这种内存访问。 将其传递给`fetch_add`方法允许我们针对特定的目标平台对其进行微调，以避免可能影响性能的不必要的同步延迟。\n\n在`main`函数中，我们创建了两个工作线程：\n\n```cpp\nstd::thread worker1(worker);\nstd::thread worker2(worker);\n```\n\n然后，主线程暂停 1 秒。 暂停后，主线程将`running`变量的值设置为`false`，表示工作线程应该终止：\n\n```cpp\nrunning = false;\n```\n\n在工作线程终止后，我们打印计数器的值：\n\n![](img/a33e028f-a5f8-4fe4-b857-23fd84788a3a.png)\n\n结果计数器值由传递给`worker`函数的超时间隔确定。 在我们的示例中，更改`fetch_add`方法中的内存顺序类型不会导致结果值发生明显变化。 但是，它可以提高使用原子变量的高并发应用的性能，因为编译器可以在不破坏应用逻辑的情况下对并发线程中的操作进行重新排序。 这种优化高度依赖于开发人员的意图，在没有开发人员提示的情况下无法自动推断。\n\n# 还有更多的..。\n\nC++ 内存模型和内存排序类型是复杂的主题，需要深入了解现代 CPU 如何访问内存并优化其代码执行。 *C++ 内存模型参考*，[https://en.cppreference.com/w/cpp/language/memory_model](https://en.cppreference.com/w/cpp/language/memory_model)提供了大量信息，是学习优化多线程应用的高级技术的良好起点。\n\n# 探索无锁同步\n\n在前面的配方中，我们了解了如何使用互斥锁和锁来同步多线程对共享数据的访问。 如果多个线程尝试运行受锁保护的代码的临界区，则一次只有一个线程可以执行此操作。 所有其他线程都必须等待，直到该线程离开临界区。\n\n但是，在某些情况下，可以在没有互斥锁和显式锁的情况下同步对共享数据的访问。 其想法是使用数据的本地副本进行修改，然后在单个、不可中断且不可分割的操作中更新共享副本。\n\n这种类型的同步取决于硬件。 目标处理器应提供某种形式的**比较和交换**(**CAS**)指令。 这将检查内存位置中的值是否与给定值匹配，并仅在匹配时才用新的给定值替换它。 因为它是单处理器指令，所以不能被上下文切换中断。 这使得它成为更复杂的原子操作的基本构建块。\n\n在本食谱中，我们将学习如何检查原子变量是无锁的，还是使用互斥锁或其他锁定操作实现的。 我们还将基于 C++ 11 的原子比较交换函数系列的示例(可从[https://en.cppreference.com/w/cpp/atomic/atomic_compare_exchange](https://en.cppreference.com/w/cpp/atomic/atomic_compare_exchange)获得)，实现自定义堆栈的无锁推送操作。\n\n# 怎么做……\n\n我们正在实现一个简单的`Stack`类，它提供一个名为`Push`的构造函数和函数。\n\n1.  在您的`~/test`工作目录中，创建一个名为`lockfree`的子目录。\n2.  使用您喜欢的文本编辑器在`lockfree`子目录中创建`lockfree.cpp`文件。\n3.  现在，我们放入所需的标头，并在`lockfree.cpp`文件中定义`Node`帮助器数据类型：\n\n```cpp\n#include <atomic>\n#include <iostream>\n\nstruct Node {\n  int data;\n  Node* next;\n};\n```\n\n4.  接下来，我们定义一个简单的`Stack`类。 这使用`Node`数据类型来组织数据存储：\n\n```cpp\nclass Stack {\n  std::atomic<Node*> head;\n\n  public:\n    Stack() {\n    std::cout << \"Stack is \" <<\n    (head.is_lock_free() ? \"\" : \"not \")\n    << \"lock-free\" << std::endl;\n    }\n\n   void Push(int data) {\n      Node* new_node = new Node{data, nullptr};\n      new_node->next = head.load();\n      while(!std::atomic_compare_exchange_weak(\n                &head,\n                &new_node->next,\n                new_node));\n    }\n    };\n```\n\n5.  最后，我们定义了一个简单的`main`函数，该函数创建`Stack`的一个实例并将一个元素推入其中：\n\n```cpp\nint main() {\n  Stack s;\n  s.Push(1);\n}\n```\n\n6.  在`loop`子目录下创建名为`CMakeLists.txt`的文件，内容如下：\n\n```cpp\ncmake_minimum_required(VERSION 3.5.1)\nproject(lockfree)\nadd_executable(lockfree lockfree.cpp)\n\nset(CMAKE_SYSTEM_NAME Linux)\nset(CMAKE_SYSTEM_PROCESSOR arm)\n\nSET(CMAKE_CXX_FLAGS \"--std=c++ 11\")\ntarget_link_libraries(lockfree pthread)\n\nset(CMAKE_CXX_COMPILER /usr/bin/arm-linux-gnueabi-g++)\n```\n\n您可以构建和运行应用。\n\n# 它是如何运作的..。\n\n我们创建了一个简单的应用，它实现了一个简单的整数值堆栈。 我们将堆栈的元素存储在动态内存中，对于每个元素，我们应该能够确定它后面的元素。\n\n为此，我们定义了一个具有两个数据字段的`Node`帮助器结构。 `data`字段存储元素的实际值，而`next`字段是指向堆栈中下一个元素的指针：\n\n```cpp\nint data;\nNode* next;\n```\n\n然后，我们定义了`Stack`类。 通常，堆栈包含两个操作：\n\n*   `Push`：将元素放在堆栈顶部\n*   `Pull`：从堆栈顶部获取元素\n\n为了跟踪堆栈的顶部，我们创建了一个`top`变量，该变量保存指向`Node`对象的指针。 它将是我们堆栈的首位：\n\n```cpp\nstd::atomic<Node*> head;\n```\n\n我们还定义了一个简单的构造函数，用于初始化`top`变量的值并检查它是否无锁。 在 C++ 中，原子变量可以使用原子**一致性、可用性和分区容错**(**CAP**)操作或使用常规互斥锁来实现。 这取决于目标 CPU：\n\n```cpp\n(head.is_lock_free() ? \"\" : \"not \")\n```\n\n在我们的应用中，我们只实现了`Push`方法，以演示如何以无锁的方式完成它。\n\n`Push`方法接受放在堆栈顶部的值。 为此，我们创建`Node`对象的新实例：\n\n```cpp\n Node* new_node = new Node{data, nullptr};\n```\n\n因为我们将元素放在堆栈的顶部，所以指向新创建的实例的指针应该分配给`top`变量，而`top`变量的旧值应该分配给新的`Node`对象的`next`指针。\n\n但是，直接这样做并不是线程安全的。 两个或多个线程可以同时修改`top`变量，从而导致数据损坏。 我们需要某种数据同步。 我们可以使用锁和互斥锁来做到这一点，但是也可以用一种无锁的方式来做到这一点。\n\n这就是为什么我们最初只更新下一个指针的原因。 因为我们的新`Node`对象还不是堆栈的一部分，所以我们可以在没有同步的情况下完成它，因为其他线程无法访问它：\n\n```cpp\nnew_node->next = head.load();\n```\n\n现在，我们需要将其添加为堆栈的新`top`变量。 我们使用`std::atomic_compare_exchange_weak`函数上的循环来完成此操作：\n\n```cpp\n      while(!std::atomic_compare_exchange_weak(\n                &head,\n                &new_node->next,\n                new_node));\n```\n\n此函数将`top`变量的值与存储在新元素的`next`指针中的值进行比较。 如果它们匹配，则用指向新节点的指针替换`top`变量的值，并返回`true`。 否则，它将`top`变量的值写入新元素的`next`指针并返回`false`。 由于我们在下一步更新了`next`指针以匹配`top`变量，因此只有在调用`std::atomic_compare_exchange_weak`函数之前另一个线程修改了它，才会发生这种情况。 最终，该函数将返回`true`，表示`top`头已使用指向我们的元素的指针进行了更新。\n\n函数的作用是：创建一个 Stack 实例，并将一个元素推入其中。 在输出中，我们可以看到底层实现是否无锁：\n\n![](img/1c7151c3-b9d3-44d2-afb2-8a5caa5119f2.png)\n\n对于我们的目标来说，实现是无锁的。\n\n# 还有更多的..。\n\n无锁同步是一个极其复杂的主题。 无锁数据结构和算法的开发需要大量的工作。 即使是使用无锁操作的简单`Push`逻辑的实现也不容易理解。 要对代码进行适当的分析和调试，还需要付出更大的努力。 通常，它会导致难以注意和难以实现的微妙问题。\n\n虽然无锁算法的实现可以提高应用的性能，但请考虑使用现有的无锁数据结构库之一，而不是自己编写。 例如，[Boost.Lockfree](https://www.boost.org/doc/libs/1_66_0/doc/html/lockfree.html)提供了一个可供您使用的无锁数据类型集合。\n\n# 在共享内存中使用原子变量\n\n我们了解了如何在多线程应用中使用原子变量实现两个或更多线程的同步。 但是，原子变量也可以用于同步作为单独进程运行的独立应用。\n\n我们已经知道如何使用共享内存在两个应用之间交换数据。 现在，我们可以结合这两种技术-共享内存和原子变量-来实现两个独立应用的数据交换和同步。\n\n# 怎么做……\n\n在本配方中，我们将修改在[章](06.html)，*内存管理*中创建的应用，用于在使用共享内存区域的两个处理器之间交换数据。\n\n1.  在您的`~/test`工作目录中，创建一个名为`shmatomic`的子目录。\n2.  使用您喜欢的文本编辑器在`shmatomic`子目录中创建`shmatomic.cpp`文件。\n3.  我们重用在`shmem`应用中创建的共享内存数据结构。 将公共标头和常量放入`shmatomic.cpp`文件：\n\n```cpp\n#include <atomic>\n#include <iostream>\n#include <chrono>\n#include <thread>\n\n#include <sys/mman.h>\n#include <fcntl.h>\n#include <unistd.h>\n\nconst char* kSharedMemPath = \"/sample_point\";\n```\n\n4.  接下来，开始定义模板化的`SharedMem`类：\n\n```cpp\ntemplate<class T>\nclass SharedMem {\n  int fd;\n  T* ptr;\n  const char* name;\n\n  public:\n```\n\n5.  该类将有一个构造函数、一个析构函数和一个 getter 方法。 让我们添加构造函数：\n\n```cpp\n    SharedMem(const char* name, bool owner=false) {\n      fd = shm_open(name, O_RDWR | O_CREAT, 0600);\n      if (fd == -1) {\n        throw std::runtime_error(\"Failed to open a shared\n        memory region\");\n      }\n      if (ftruncate(fd, sizeof(T)) < 0) {\n        close(fd);\n        throw std::runtime_error(\"Failed to set size of a shared\n        memory region\");\n      };\n      ptr = (T*)mmap(nullptr, sizeof(T), PROT_READ | PROT_WRITE, \n      MAP_SHARED, fd, 0);\n      if (!ptr) {\n        close(fd);\n        throw std::runtime_error(\"Failed to mmap a shared memory\n        region\");\n      }\n      this->name = owner ? name : nullptr;\n      }\n```\n\n6.  简单的析构函数和 getter 如下：\n\n```cpp\n~SharedMem() {\nmunmap(ptr, sizeof(T));\nclose(fd);\nif (name) {\nstd::cout << \"Remove shared mem instance \" << name << std::endl;\nshm_unlink(name);\n}\n}\n\nT& get() const {\nreturn *ptr;\n}\n};\n```\n\n7.  现在，我们定义将用于数据交换和同步的数据类型：\n\n```cpp\nstruct Payload {\nstd::atomic_bool data_ready;\nstd::atomic_bool data_processed;\nint index;\n};\n```\n\n8.  接下来，我们定义一个将生成数据的函数：\n\n```cpp\nvoid producer() {\n  SharedMem<Payload> writer(kSharedMemPath);\n  Payload& pw = writer.get();\nif (!pw.data_ready.is_lock_free()) {\nthrow std::runtime_error(\"Flag is not lock-free\");\n  }\nfor (int i = 0; i < 10; i++) {\npw.data_processed.store(false);\npw.index = i;\n    pw.data_ready.store(true);\nwhile(!pw.data_processed.load());\n}\n}\n```\n\n9.  紧跟其后的是使用数据的函数：\n\n```cpp\nvoid consumer() {\nSharedMem<Payload> point_reader(kSharedMemPath, true);\nPayload& pr = point_reader.get();\nif (!pr.data_ready.is_lock_free()) {\nthrow std::runtime_error(\"Flag is not lock-free\");\n}\nfor (int i = 0; i < 10; i++) {\n while(!pr.data_ready.load());\n    pr.data_ready.store(false);\nstd::cout << \"Processing data chunk \" << pr.index << std::endl;\n    pr.data_processed.store(true);\n}\n}\n```\n\n10.  最后，我们添加`main`函数，该函数将所有内容联系在一起：\n\n```cpp\nint main() {\n\nif (fork()) {\n    consumer();\n} else {\n    producer();\n}\n}\n```\n\n11.  在`loop`子目录下创建名为`CMakeLists.txt`的文件，内容如下：\n\n```cpp\ncmake_minimum_required(VERSION 3.5.1)\nproject(shmatomic)\nadd_executable(shmatomic shmatomic.cpp)\n\nset(CMAKE_SYSTEM_NAME Linux)\nset(CMAKE_SYSTEM_PROCESSOR arm)\n\nSET(CMAKE_CXX_FLAGS \"--std=c++ 11\")\ntarget_link_libraries(shmatomic pthread rt)\n\nset(CMAKE_CXX_COMPILER /usr/bin/arm-linux-gnueabi-g++)\n```\n\n您可以构建和运行应用。\n\n# 它是如何运作的..。\n\n在我们的应用中，我们重用了在[章](06.html)，*内存管理*中介绍的模板化`SharedMem`类。 此类用于在共享内存区域中存储特定类型的元素。 让我们快速回顾一下它是如何工作的。\n\n`SharedMem`类是**可移植操作系统接口**(**POSIX**)共享内存 API 之上的包装器。 它定义了三个私有数据字段来保存系统特定的处理程序和指针，并公开了一个由两个函数组成的公共接口：\n\n*   接受共享区域名称和所有权标志的构造函数\n*   返回对存储在共享内存中的对象的引用的`get`方法\n\n该类还定义了一个析构函数，该函数执行正确关闭共享对象所需的所有操作。 因此，`SharedMem`类可以通过 C++ RAII 习惯用法用于安全的资源管理。\n\n`SharedMem`类是模板化的类。 它由我们希望存储在共享内存中的数据类型参数化。 为此，我们定义了一个名为`Payload`的结构：\n\n```cpp\nstruct Payload {\n  std::atomic_bool data_ready;\n  std::atomic_bool data_processed;\n  int index;\n};\n```\n\n它有一个我们将用作数据交换字段的`index`整数变量，以及用于数据同步的两个原子布尔标志`data_ready`和`data_processed`。\n\n我们还定义了两个函数`producer`和`consumer`，它们将在单独的进程中工作，并使用共享内存区域在彼此之间交换数据。\n\n`producer`函数正在生成数据区块。 首先，它创建`SharedMem`类的一个实例，该实例由`Payload`数据类型参数化。 它将共享内存区的路径传递给`SharedMem`构造函数：\n\n```cpp\nSharedMem<Payload> writer(kSharedMemPath);\n```\n\n创建共享内存实例后，它将获取对存储在其中的有效负载数据的引用，并检查我们在`Payload`数据类型中定义的任何原子标志是否无锁：\n\n```cpp\nif (!pw.data_ready.is_lock_free()) {\n    throw std::runtime_error(\"Flag is not lock-free\");\n}\n```\n\n该函数在一个循环中产生 10 个数据块。 块的索引被放入有效载荷的`index`字段：\n\n```cpp\npw.index = i;\n```\n\n但是，除了将数据放入共享内存之外，我们还需要同步对该数据的访问。 这是我们使用原子旗的时候。\n\n对于每个迭代，在更新`index`字段之前，我们将重置`data_processed`标志。 在索引更新之后，我们设置`data ready`标志，这是告诉消费者新的数据块已准备好的指示器，并等待消费者处理数据。 我们循环直到`data_processed`标志变为`true`，然后转到下一个迭代：\n\n```cpp\npw.data_ready.store(true);\nwhile(!pw.data_processed.load());\n```\n\n`consumer`函数的工作方式与此类似。 因为它在单独的进程中工作，所以它通过使用相同的路径创建`SharedMem`类的实例来打开相同的共享内存区域。 我们还使`consumer`函数成为共享内存实例的所有者。 这意味着在其`SharedMem`实例被销毁后，它负责删除共享内存区：\n\n```cpp\nSharedMem<Payload> point_reader(kSharedMemPath, true);\n```\n\n与`producer`函数类似，`consumer`函数检查原子标志是否无锁，并进入数据消耗循环。\n\n对于每一次迭代，它都会在一个紧密的循环中等待，直到数据准备就绪：\n\n```cpp\nwhile(!pr.data_ready.load());\n```\n\n在`producer`函数将`data_ready`标志设置为`true`后，`consumer`函数可以安全地读取和处理数据。 在我们的实现中，它只将`index`字段打印到控制台。 数据处理后，`consumer`功能通过将`data_processed`标志设置为`true`来表示这一点：\n\n```cpp\npr.data_processed.store(true);\n```\n\n这将触发`producer`函数端的下一次数据生产迭代：\n\n![](img/96155edc-9e5c-42dc-b8d6-46969182a299.png)\n\n因此，我们可以看到经过处理的数据块的确定性输出，没有遗漏或重复；这在数据访问不同步的情况下很常见。\n\n# 探索异步功能和未来\n\n在多线程应用中处理数据同步是困难的、容易出错的，并且需要开发人员编写大量代码来正确调整数据交换和数据通知。 为了简化开发，C++ 11 引入了一种标准 API，用于以类似于常规同步函数调用的方式编写异步代码，并隐藏了大量同步复杂性。\n\n在本食谱中，我们将学习如何使用异步函数调用和期货在多线程中运行我们的代码，而实际上不需要额外的工作，从而实现数据同步。\n\n# 怎么做……\n\n我们将实现一个简单的应用，该应用在单独的线程中调用一个长时间运行的函数并等待其结果。 当函数运行时，应用可以继续处理其他计算。\n\n1.  在您的`~/test`工作目录中，创建一个名为`async`的子目录。\n2.  使用您喜欢的文本编辑器在`async`子目录中创建一个`async.cpp`文件。\n3.  将我们的应用代码放到`async.cpp`文件中，从公共头文件和我们的长时间运行的函数开始：\n\n```cpp\n#include <chrono>\n#include <future>\n#include <iostream>\n\nint calculate (int x) {\n  auto start = std::chrono::system_clock::now();\n  std::cout << \"Start calculation\\n\";\n  std::this_thread::sleep_for(std::chrono::seconds(1));\n  auto delta = std::chrono::system_clock::now() - start;\n  auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(delta);\n  std::cout << \"Done in \" << ms.count() << \" ms\\n\";\n  return x*x;\n}\n```\n\n4.  接下来，添加`test`函数，该函数调用长时间运行的函数：\n\n```cpp\nvoid test(int value, int worktime) {\n  std::cout << \"Request result of calculations for \" << value << std::endl;\n  std::future<int> fut = std::async (calculate, value);\n  std::cout << \"Keep working for \" << worktime << \" ms\" << std::endl;\n  std::this_thread::sleep_for(std::chrono::milliseconds(worktime));\n  auto start = std::chrono::system_clock::now();\n  std::cout << \"Waiting for result\" << std::endl;\n  int result = fut.get();\n  auto delta = std::chrono::system_clock::now() - start;\n  auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(delta);\n\n  std::cout << \"Result is \" << result\n            << \", waited for \" << ms.count() << \" ms\"\n            << std::endl << std::endl;\n}\n\n```\n\n5.  最后，添加一个`main`极简函数：\n\n```cpp\nint main ()\n{\n  test(5, 400);\n  test(8, 1200);\n  return 0;\n}\n```\n\n6.  在`loop`子目录下创建名为`CMakeLists.txt`的文件，内容如下：\n\n```cpp\ncmake_minimum_required(VERSION 3.5.1)\nproject(async)\nadd_executable(async async.cpp)\n\nset(CMAKE_SYSTEM_NAME Linux)\nset(CMAKE_SYSTEM_PROCESSOR arm)\n\nSET(CMAKE_CXX_FLAGS \"--std=c++ 14\")\ntarget_link_libraries(async pthread -static-libstdc++)\n\nset(CMAKE_CXX_COMPILER /usr/bin/arm-linux-gnueabi-g++)\n```\n\n您可以构建和运行应用。\n\n# 它是如何运作的..。\n\n在我们的应用中，我们定义了一个应该需要很长时间才能运行的`calculate`函数。 从技术上讲，我们的函数计算整数参数的平方，但我们添加了一个人为延迟，使其运行 1 秒。 我们使用`sleep_for`标准库函数向应用添加延迟：\n\n```cpp\nstd::this_thread::sleep_for(std::chrono::seconds(1));\n```\n\n除了计算之外，该函数还会记录开始工作的时间、完成的时间和花费的时间。\n\n接下来，我们定义了一个调用`calculate`函数的`test`函数，以演示异步调用是如何工作的。\n\n该函数有两个参数。 第一个参数是传递给`calculate`函数的值。 第二个参数是`test`函数在运行`calculate`函数之后和请求结果之前将要花费的时间。 通过这种方式，我们对函数可以与其请求的计算并行执行的有用工作进行建模。\n\n`test`函数通过在异步模式下运行`calculate`函数并向其传递第一个参数`value`开始工作：\n\n```cpp\nstd::future<int> fut = std::async (calculate, value);\n```\n\n`async`函数隐式产生一个线程并开始执行`calculate`函数。\n\n因为我们异步运行函数，所以结果还没有准备好。 相反，`async`函数返回`std::future`的一个实例，该对象将在结果可用时保存该结果。\n\n接下来，我们模拟有用的工作。 在我们的示例中，它是指定时间间隔的暂停。 在可以并行完成的工作完成之后，我们需要得到`calculate`函数的结果才能继续。 要请求结果，我们使用`std::future`对象的`get`方法，如下所示：\n\n```cpp\nint result = fut.get();\n```\n\n`get`方法会一直阻塞，直到结果可用。 然后，我们可以计算等待结果所花费的时间，并将结果和等待时间一起输出到控制台。\n\n在`main`函数中，我们运行`test`函数来评估两个场景：\n\n*   有用的工作比结果的计算花费更少的时间。\n*   有用的工作比结果的计算花费更多的时间。\n\n运行应用会产生以下输出。\n\n在第一个场景中，我们可以看到我们正在开始计算，然后在计算完成之前开始等待结果。 结果，`get`方法被阻塞了 600 毫秒，直到结果准备就绪：\n\n![](img/46a609c8-dcd7-4286-b46c-dc2341addc93.png)\n\n在第二个场景中，有用的工作花费了`1200`毫秒。 正如我们所看到的，在请求结果之前已经完成了计算，因此，`get`方法没有阻塞，并立即返回结果。\n\n# 还有更多的..。\n\n期货和异步函数为编写并行且易于理解的代码提供了强大的机制。 异步功能灵活，支持不同的执行策略。 承诺是另一种使开发人员能够克服异步编程复杂性的机制。 更多信息可在位于[[https://en.cppreference.com/w/cpp/thread/future](https://en.cppreference.com/w/cpp/thread/future)]的`std::future`、位于[[https://en.cppreference.com/w/cpp/thread/promise](https://en.cppreference.com/w/cpp/thread/promise)]的`std::promise`以及位于[[https://en.cppreference.com/w/cpp/thread/async](https://en.cppreference.com/w/cpp/thread/async)]的`std::async`的参考页中找到。"
  },
  {
    "path": "docs/emb-prog-mod-cpp-cb/08.md",
    "content": "# 八、通信和序列化\n\n复杂的嵌入式系统很少由单个应用组成。 将所有逻辑放在同一个应用中是脆弱的，容易出错，有时几乎不可行，因为系统的不同功能可能由不同的团队甚至不同的供应商开发。 这就是为什么隔离独立应用中的功能逻辑并使用定义良好的协议相互通信是扩展嵌入式软件的常用方法。 此外，这种隔离只需极少的修改即可用于与远程系统上托管的应用通信，从而使其更具可伸缩性。 我们将学习如何通过将应用的逻辑拆分成相互通信的独立组件来构建健壮且可伸缩的应用。\n\n在本章中，我们将介绍以下主题：\n\n*   在应用中使用进程间通信\n*   进程间通信机制探讨\n*   了解消息队列和发布者-订阅者模型\n*   使用 C++ lambdas 进行回调\n*   探索数据序列化\n*   使用 FlatBuffers 库\n\n本章中的菜谱将帮助您理解可伸缩和独立于平台的数据交换的基本概念。 它们可以用来实现从嵌入式系统到云或远程后端的数据传输，或者用来设计使用微服务架构的嵌入式系统。\n\n# 在应用中使用进程间通信\n\n大多数现代操作系统使用底层硬件平台提供的内存虚拟化支持来相互隔离应用进程。\n\n每个进程都有自己的虚拟地址空间，完全独立于其他应用的地址空间。 这给开发人员带来了巨大的好处。 由于应用的寻址进程是独立的，因此一个应用不会意外损坏另一个应用的内存。 因此，一个应用中的故障不会影响整个系统。 由于所有其他应用都继续工作，因此系统可以通过重新启动出现故障的应用来恢复。\n\n内存隔离的好处是要付出代价的。 由于一个进程不能访问另一个进程的内存，它需要使用专用的**A****应用接口**(**API**)进行数据交换，或者使用操作系统提供的**进程间通信**(**IPC**)。\n\n在本食谱中，我们将学习如何使用共享文件在两个进程之间交换信息。 它可能不是最高效的性能机制，但它无处不在，易于使用，足以满足各种实际用例。\n\n# 怎么做……\n\n在本食谱中，我们将创建一个示例应用，该应用创建两个流程。 一个进程生成数据，而另一个进程读取数据并将其打印到控制台：\n\n1.  在您的工作目录(`~/test`)中，创建一个名为`ipc1`的子目录。\n2.  使用您喜欢的文本编辑器在`ipc1`子目录中创建一个`ipc1.cpp`文件。\n3.  我们将定义两个模板化类来组织数据交换。 第一个类`Writer`用于将数据写入文件。 让我们将其定义放在`ipc1.cpp`文件中：\n\n```cpp\n#include <fstream>\n#include <iostream>\n#include <thread>\n#include <vector>\n\n#include <unistd.h>\n\nstd::string kSharedFile = \"/tmp/test.bin\";\n\ntemplate<class T>\nclass Writer {\n  private:\n    std::ofstream out;\n  public:\n    Writer(std::string& name):\n      out(name, std::ofstream::binary) {}\n\n    void Write(const T& data) {\n      out.write(reinterpret_cast<const char*>(&data), sizeof(T));\n    }\n};\n```\n\n4.  紧随其后的是`Reader`类的定义，该类负责从文件中读取数据：\n\n```cpp\ntemplate<class T>\nclass Reader {\n  private:\n    std::ifstream in;\n  public:\n    Reader(std::string& name) {\n      for(int count=10; count && !in.is_open(); count--) {\n        in.open(name, std::ifstream::binary);\n        std::this_thread::sleep_for(std::chrono::milliseconds(10));\n      }\n    }\n\n    T Read() {\n      int count = 10;\n      for (;count && in.eof(); count--) {\n        std::this_thread::sleep_for(std::chrono::milliseconds(10));\n      }\n\n      T data;\n      in.read(reinterpret_cast<char*>(&data), sizeof(data));\n      if (!in) {\n        throw std::runtime_error(\"Failed to read a message\");\n      }\n      return data;\n    }\n};\n```\n\n5.  接下来，我们定义将用于数据的数据类型：\n\n```cpp\nstruct Message {\n  int x, y;\n};\n\nstd::ostream& operator<<(std::ostream& o, const Message& m) {\n  o << \"(x=\" << m.x << \", y=\" << m.y << \")\";\n}\n```\n\n6.  为了将所有内容包装在一起，我们定义了`DoWrites`和`DoReads`函数，以及调用它们的`main`函数：\n\n```cpp\nvoid DoWrites() {\n  std::vector<Message> messages {{1, 0}, {0, 1}, {1, 1}, {0, 0}};\n  Writer<Message> writer(kSharedFile);\n  for (const auto& m : messages) {\n    std::cout << \"Write \" << m << std::endl;\n    writer.Write(m);\n  }\n}\n\nvoid DoReads() {\n  Reader<Message> reader(kSharedFile);\n  try {\n    while(true) {\n      std::cout << \"Read \" << reader.Read() << std::endl;\n    }\n  } catch (const std::runtime_error& e) {\n    std::cout << e.what() << std::endl;\n  }\n}\n\nint main(int argc, char** argv) {\n  if (fork()) {\n    DoWrites();\n  } else {\n    DoReads();\n  }\n}\n```\n\n7.  最后，创建一个`CMakeLists.txt`文件，其中包含程序的构建规则：\n\n```cpp\ncmake_minimum_required(VERSION 3.5.1)\nproject(ipc1)\nadd_executable(ipc1 ipc1.cpp)\n\nset(CMAKE_SYSTEM_NAME Linux)\nset(CMAKE_SYSTEM_PROCESSOR arm)\n\nSET(CMAKE_CXX_FLAGS \"--std=c++ 11\")\n\nset(CMAKE_CXX_COMPILER /usr/bin/arm-linux-gnueabi-g++)\n```\n\n现在您可以构建和运行应用了。\n\n# 它是如何运作的..。\n\n在我们的应用中，我们使用文件系统中的共享文件探索两个独立进程之间的数据交换。 一个进程将数据写入文件，另一个进程从同一文件读取数据。\n\n文件可以存储任何非结构化的字节序列。 在我们的应用中，我们利用 C++ 模板的功能来处理严格类型的 C++ 值，而不是原始字节流。 这种方法有助于编写干净、无错误的代码。\n\n我们从定义`Write`类开始。 它是一个简单的包装器，位于用于文件输入/输出的标准 C++ `fstream`类之上。 该类的构造函数仅打开一个文件流来写入以下内容：\n\n```cpp\nWriter(std::string& name):\n      out(name, std::ofstream::binary) {}\n```\n\n除了构造函数，该类只包含一个方法`Write`，该方法负责将数据写入文件。 由于文件 API 使用字节流操作，因此我们首先需要将模板化数据类型转换为原始字符缓冲区。 我们可以使用 C++ `reinterpret_cast`来实现这一点：\n\n```cpp\nout.write(reinterpret_cast<const char*>(&data), sizeof(T));\n```\n\n`Reader`类执行相反的工作-它读取由`Writer`类写入的数据。 它的构造函数稍微复杂一些。 因为在创建`Reader`类的实例时数据文件可能还没有准备好，所以构造函数会尝试循环打开它，直到打开尝试成功。 它进行`10`次尝试，每个尝试之间有 10 毫秒的暂停：\n\n```cpp\nfor(int count=10; count && !in.is_open(); count--) {\n        in.open(name, std::ifstream::binary);\n        std::this_thread::sleep_for(std::chrono::milliseconds(10));\n      }\n```\n\n`Read`方法将数据从输入流读取到临时值中，并将其返回给调用方。 与`Write`方法类似，我们使用`reinterpret_cast`将数据对象的内存作为原始字符缓冲区进行访问：\n\n```cpp\nin.read(reinterpret_cast<char*>(&data), sizeof(data));\n```\n\n我们还在`Read`方法中添加了一个等待循环，以等待`Write`写入数据。 如果我们到达文件末尾，则最多等待 1 秒以获取新数据：\n\n```cpp\n      for (;count && in.eof(); count--) {\n        std::this_thread::sleep_for(std::chrono::milliseconds(10));\n      }\n```\n\n如果此时文件中没有数据，或者在出现 I/O 错误的情况下，我们会抛出一个异常来指示：\n\n```cpp\n      if (!in) {\n        throw std::runtime_error(\"Failed to read a message\");\n      }\n```\n\nPlease note that we do not need to add any code to handle a situation where a file cannot be opened within 1 second, or data is not ready within one second. Both of these cases are handled by the same preceding code.\n\n现在已经实现了`Writer`和`Reader`类，我们可以为数据交换定义数据类型了。 在我们的应用中，我们将交换坐标，表示为`x`和`y`整数值。 我们的数据消息如下所示：\n\n```cpp\nstruct Message {\n  int x, y;\n};\n```\n\n为方便起见，我们覆盖了`Message`结构的`<<`运算符。 每当将`Message`的实例写入输出流时，它的格式为`(x, y)`：\n\n```cpp\nstd::ostream& operator<<(std::ostream& o, const Message& m) {\n  o << \"(x=\" << m.x << \", y=\" << m.y << \")\";\n}\n```\n\n准备就绪后，让我们编写用于数据交换的函数。 `DoWrites`函数定义一个包含四个坐标的矢量，并创建一个`Writer`对象：\n\n```cpp\n  std::vector<Message> messages {{1, 0}, {0, 1}, {1, 1}, {0, 0}};\n  Writer<Message> writer(kSharedFile);\n```\n\n然后，它在循环中写入所有坐标：\n\n```cpp\n  for (const auto& m : messages) {\n    std::cout << \"Write \" << m << std::endl;\n    writer.Write(m);\n  }\n```\n\n反过来，`DoReads`函数使用与其前面的`Writer`实例相同的文件名创建`Reader`类的实例。 它进入一个无穷无尽的循环，试图读取文件中的所有消息：\n\n```cpp\n while(true) {\n      std::cout << \"Read \" << reader.Read() << std::endl;\n    }\n```\n\n当没有更多的消息可用时，`Read`方法抛出一个异常来中断循环：\n\n```cpp\n  } catch (const std::runtime_error& e) {\n    std::cout << e.what() << std::endl;\n  }\n```\n\n函数的作用是：创建两个独立的进程，在其中一个进程中运行`DoWrites`，在另一个进程中运行`DoReads`。 运行应用后，我们将获得以下输出：\n\n![](img/8f5ce532-4e3c-401e-8716-87f43e5d0c8c.png)\n\n正如我们所看到的，写入者确实写了四个坐标，而读取器能够使用共享文件读取相同的四个坐标。\n\n# 还有更多的..。\n\n我们创建的应用尽可能简单，专注于严格类型的数据交换，并将数据同步和数据序列化排除在讨论范围之外。 我们将使用此应用作为更高级技术的基础，这些技术将在下面的食谱中描述。\n\n# 进程间通信机制探讨\n\n除了我们已经了解的共享文件之外，现代操作系统还提供了许多 IPC 机制，即：\n\n*   管道 / 烟斗 / 管 / 笛\n*   命名管道\n*   本地套接字\n*   网络套接字\n*   共享内存\n\n有趣的是，它们中的许多提供的 API 与我们在处理常规文件时使用的 API 完全相同。 因此，在这些类型的 IPC 之间切换是微不足道的，我们用来读写本地文件的代码也可以用来与远程网络主机上运行的应用通信。\n\n在本食谱中，我们将学习如何使用可移植操作系统接口(**POSIX**)命名管道在驻留在同一台计算机上的两个应用之间进行通信。\n\n# 正在做好准备\n\n我们将使用我们创建的应用的源代码作为*的一部分，在 Applications*配方中使用进程间通信作为本配方的起点。\n\n# 怎么做……\n\n在本食谱中，我们将从使用 IPC 常规文件的源代码开始。 我们将修改它以使用名为**命名管道**的 IPC 机制：\n\n1.  将`ipc1`目录的内容复制到名为`ipc2`的新目录中。\n2.  打开`ipc1.cpp`文件，在`#include <unistd.h>`之后再添加两个`include`实例：\n\n```cpp\n#include <unistd.h>\n#include <sys/types.h>\n#include <sys/stat.h>\n```\n\n3.  修改`Writer`类的`Write`方法，增加一行：\n\n```cpp\n    void Write(const T& data) {\n      out.write(reinterpret_cast<const char*>(&data), sizeof(T));\n out.flush();\n    }\n```\n\n4.  `Reader`类中的修改更为实质性。 构造函数和`Read`方法都会受到影响：\n\n```cpp\ntemplate<class T>\nclass Reader {\n  private:\n    std::ifstream in;\n  public:\n    Reader(std::string& name):\n      in(name, std::ofstream::binary) {}\n\n    T Read() {\n      T data;\n      in.read(reinterpret_cast<char*>(&data), sizeof(data));\n      if (!in) {\n        throw std::runtime_error(\"Failed to read a message\");\n      }\n      return data;\n    }\n};\n```\n\n5.  向`DoWrites`函数添加一个小更改。 唯一的区别是我们在发送每条消息后增加了 10 毫秒的延迟：\n\n```cpp\nvoid DoWrites() {\n  std::vector<Message> messages {{1, 0}, {0, 1}, {1, 1}, {0, 0}};\n  Writer<Message> writer(kSharedFile);\n  for (const auto& m : messages) {\n    std::cout << \"Write \" << m << std::endl;\n    writer.Write(m);\n std::this_thread::sleep_for(std::chrono::milliseconds(10));\n  }\n}\n```\n\n6.  最后，修改我们的`main`函数以创建命名管道，而不是常规文件：\n\n```cpp\nint main(int argc, char** argv) {\n int ret = mkfifo(kSharedFile.c_str(), 0600);\n if (!ret) {\n throw std::runtime_error(\"Failed to create named pipe\");\n }\n  if (fork()) {\n    DoWrites();\n  } else {\n    DoReads();\n  }\n}\n```\n\n现在您可以构建和运行应用了。\n\n# 它是如何运作的..。\n\n如您所见，我们对应用的代码进行了最少的更改。 所有读写数据的机制和 API 都保持不变。 关键的区别隐藏在一行代码后面：\n\n```cpp\n int ret = mkfifo(kSharedFile.c_str(), 0600);\n```\n\n该行创建名为`named pipe`的特殊类型的文件。 它看起来像一个普通文件-它有名称、权限属性和修改时间。 但是，它不存储任何实际数据。 写入此文件的所有内容都会立即传送到读取此文件的进程。\n\n这种差异有一系列后果。 由于文件中没有存储任何实际数据，因此在写入任何数据之前，所有读取尝试都会被阻止。 类似地，在读取器读取之前的数据之前，写入被阻止。\n\n因此，不再需要进行外部数据同步。 看一下`Reader`类实现。 它在构造函数或`Read`方法中没有重试循环。\n\n为了测试我们是否真的不需要使用任何额外的同步，我们在写入每条消息后添加了一个人为延迟：\n\n```cpp\n std::this_thread::sleep_for(std::chrono::milliseconds(10));\n```\n\n当我们构建和运行应用时，我们可以看到以下输出：\n\n![](img/6f71e499-25db-4757-96ab-21a9a119babf.png)\n\n尽管我们在`Reader`代码中没有添加任何延迟或检查，但每个`Write`方法后面都有正确的`Read`方法。 操作系统的 IPC 机制为我们透明地处理数据同步，从而产生更干净、更具可读性的代码。\n\n# 还有更多的..。\n\n如您所见，使用命名管道与使用常规函数一样简单。 套接字 API 是 IPC 的另一种广泛使用的机制。 它稍微复杂一些，但提供了更多的灵活性。 通过选择不同的传输层，开发人员可以将相同的套接字 API 用于本地数据交换和与远程主机的网络连接。\n\n有关套接字 API 的更多信息，请参见[http://man7.org/linux/man-pages/man7/socket.7.html](http://man7.org/linux/man-pages/man7/socket.7.html.)。\n\n# 了解消息队列和发布者-订阅者模型\n\nPOSIX 操作系统提供的大多数 IPC 机制都非常基础。 它们的 API 是使用文件描述符构建的，它们将输入和输出通道视为原始字节序列。\n\n然而，应用倾向于将特定长度和目的的数据片段用于数据交换消息。 尽管操作系统的 API 机制具有灵活性和通用性，但它们并不总是便于消息交换。 这就是在默认 IPC 机制之上构建专用库和组件以简化消息交换模式的原因。\n\n在本食谱中，我们将学习如何使用**发布者-订阅者**(**发布订阅**)模型在两个应用之间实现异步数据交换。\n\n该模型易于理解，被广泛用于设计为相互通信的独立、松散耦合组件的集合的软件系统的开发。 功能隔离和异步数据交换使我们能够构建灵活、可扩展和健壮的解决方案。\n\n在发布-订阅模型中，应用充当发布者、订阅者或两者兼而有之。 应用不需要向特定应用发送请求并期望它们响应，而是可以向特定主题发布消息或订阅以接收有关其感兴趣的主题的消息。 发布消息时，应用不关心有多少订阅者在收听该主题。 类似地，订阅者不知道哪个应用将发送关于特定主题的消息，或者何时期待该消息。\n\n# 怎么做……\n\n作为探索 IPC 配方的机制的*的一部分，我们创建的应用已经包含了许多构建块，我们可以重用它们来实现发布/订阅通信。*\n\n`Writer`类可以充当发布者，`Reader`类可以充当订阅者。 我们实现它们来处理将定义我们的消息的严格定义的数据类型。 我们在前面的配方中使用的命名管道机制在字节级别工作，不能保证消息自动传递。\n\n为了克服这一限制，我们将使用 POSIX 消息队列 API 而不是命名管道。 用于标识`Reader`和`Writer`都将在其构造函数中接受的消息队列的名称将用作主题：\n\n1.  将我们在前面的配方中创建的`ipc2`目录的内容复制到一个新目录中：`ipc3`。\n2.  让我们为 POSIX 消息队列 API 创建一个 C++ 包装器。 在编辑器中打开`ipc1.cpp`，添加所需的头文件和常量定义：\n\n```cpp\n#include <unistd.h>\n#include <signal.h>\n#include <fcntl.h>\n#include <sys/stat.h>\n#include <mqueue.h>\n\nstd::string kQueueName = \"/test\";\n```\n\n3.  然后，定义一个`MessageQueue`类。 它将消息队列句柄作为其私有数据成员保存。 通过 C++ RAII 成语，我们可以使用构造函数和析构函数以安全的方式管理句柄的打开和关闭：\n\n```cpp\nclass MessageQueue {\n  private:\n    mqd_t handle;\n  public:\n    MessageQueue(const std::string& name, int flags) {\n      handle = mq_open(name.c_str(), flags);\n      if (handle < 0) {\n        throw std::runtime_error(\"Failed to open a queue for \n         writing\");\n      }\n    }\n\n    MessageQueue(const std::string& name, int flags, int max_count, \n     int max_size) {\n      struct mq_attr attrs = { 0, max_count, max_size, 0 };\n      handle = mq_open(name.c_str(), flags | O_CREAT, 0666, \n       &attrs);\n      if (handle < 0) {\n        throw std::runtime_error(\"Failed to create a queue\");\n      }\n    }\n\n    ~MessageQueue() {\n      mq_close(handle);\n    }\n\n```\n\n4.  然后，我们定义两个向队列写入消息和从队列读取消息的简单方法：\n\n```cpp\n    void Send(const char* data, size_t len) {\n      if (mq_send(handle, data, len, 0) < 0) {\n        throw std::runtime_error(\"Failed to send a message\");\n      }\n    }\n\n    void Receive(char* data, size_t len) {\n      if (mq_receive(handle, data, len, 0) < len) {\n        throw std::runtime_error(\"Failed to receive a message\");\n      }\n    }\n};\n```\n\n5.  我们现在修改`Writer`和`Reader`类以使用新的 API。 我们的`MessageQueue`包装器完成了大部分繁重的任务，代码更改很少。 现在，`Writer`类如下所示：\n\n```cpp\ntemplate<class T>\nclass Writer {\n  private:\n    MessageQueue queue;\n  public:\n    Writer(std::string& name):\n      queue(name, O_WRONLY) {}\n\n    void Write(const T& data) {\n      queue.Send(reinterpret_cast<const char*>(&data), sizeof(data));\n    }\n};\n```\n\n6.  `Reader`类中的修改更为实质性。 我们让它充当订阅者，并将直接从队列获取和处理消息的逻辑封装到类中：\n\n```cpp\ntemplate<class T>\nclass Reader {\n  private:\n    MessageQueue queue;\n  public:\n    Reader(std::string& name):\n      queue(name, O_RDONLY) {}\n\n    void Run() {\n      T data;\n      while(true) {\n        queue.Receive(reinterpret_cast<char*>(&data), \n          sizeof(data));\n        Callback(data);\n      }\n    }\n\n  protected:\n    virtual void Callback(const T& data) = 0;\n};\n```\n\n7.  由于我们仍然希望尽可能保持`Reader`类的泛型，因此我们将定义一个从`Reader`派生的新类(`CoordLogger`)，以定义消息的具体处理：\n\n```cpp\nclass CoordLogger : public Reader<Message> {\n  using Reader<Message>::Reader;\n\n  protected:\n    void Callback(const Message& data) override {\n      std::cout << \"Received coordinate \" << data << std::endl;\n    }\n};\n```\n\n8.  `DoWrites`代码基本保持不变；唯一的变化是我们使用不同的常量来标识队列：\n\n```cpp\nvoid DoWrites() {\n  std::vector<Message> messages {{1, 0}, {0, 1}, {1, 1}, {0, 0}};\n  Writer<Message> writer(kQueueName);\n  for (const auto& m : messages) {\n    std::cout << \"Write \" << m << std::endl;\n    writer.Write(m);\n    std::this_thread::sleep_for(std::chrono::milliseconds(10));\n  }\n}\n```\n\n9.  由于消息处理逻辑已移至`Reader`和`CoordLogger`类，因此`DoReads`现在就像下面这样简单：\n\n```cpp\nvoid DoReads() {\n CoordLogger logger(kQueueName);\n logger.Run();\n}\n```\n\n10.  更新后的`main`函数如下：\n\n```cpp\nint main(int argc, char** argv) {\n  MessageQueue q(kQueueName, O_WRONLY, 10, sizeof(Message));\n  pid_t pid = fork();\n  if (pid) {\n    DoWrites();\n    std::this_thread::sleep_for(std::chrono::milliseconds(100));\n    kill(pid, SIGTERM);\n  } else {\n    DoReads();\n  }\n}\n```\n\n11.  最后，我们的应用需要与`rt`库链接。 我们通过在`CMakeLists.txt`文件中添加一行来完成此操作：\n\n```cpp\ntarget_link_libraries(ipc3 rt)\n```\n\n现在您可以构建和运行应用了。\n\n# 它是如何运作的..。\n\n在我们的应用中，我们重用了前面配方中创建的应用的大量代码*，探索 IPC*的机制。 要实现发布-订阅模式，我们需要进行两个重要的更改：\n\n*   使我们的 IPC 基于消息。 我们应该能够自动发送和接收消息。 一个发布者发送的消息不应破坏其他发布者发送的消息，订阅者应该能够整体阅读消息。\n*   让订阅者定义一旦有新消息可用就调用的回调。\n\n为了进行基于消息的通信，我们从命名管道切换到 POSIX 消息队列 API。 消息队列 API 不同于命名管道的常规基于文件的 API，这就是为什么我们在 Linux 标准库提供的纯 C 接口之上实现 C++ 包装器的原因。\n\n包装器的主要目标是使用**Resource Acquisition is Initialization**(**RAII**)习惯用法提供安全的资源管理。 我们通过定义通过调用`mq_open`获取队列处理程序的构造函数和使用`mq_close`释放队列处理程序的析构函数来实现这一点。 这样，当`MessageQueue`类的相应实例被销毁时，队列将自动关闭。\n\n包装类有两个构造函数。 一个构造函数用于打开现有队列。 它接受两个参数-队列名称和访问标志。 第二个构造函数用于创建新队列。 它接受两个附加参数-消息长度和队列中消息的最大大小。\n\n在我们的应用中，我们在`main`函数中创建一个队列，传递`10`作为队列中可以存储的消息数量。 `Message`结构的大小是队列中消息的最大大小：\n\n```cpp\n  MessageQueue q(kQueueName, O_WRONLY, 10, sizeof(Message));\n```\n\n然后，`DoWrites`和`DoReads`函数打开已创建的同名队列。\n\n由于我们的`MessageQueue`类的公共 API 类似于我们用于使用命名管道的 IPC 的`fstream`接口，因此只需对写入器和读取器进行最小的更改，即可使它们与另一种 IPC 机制一起工作。 我们使用`MessageQueue`的实例而不是`fstream`作为数据成员，保持其他逻辑不变。\n\n要让订阅者定义他们的回调方法，我们需要修改`Reader`类。 我们引入了`Run`方法，而不是读取并返回单个方法的`Read`方法。 它循环队列中所有可用的消息。 对于读取的每个方法，它都会调用一个回调方法：\n\n```cpp\n      while(true) {\n        queue.Receive(reinterpret_cast<char*>(&data), sizeof(data));\n        Callback(data);\n      }\n```\n\n我们的目标是保持`Reader`类对于不同类型的消息的泛型和可重用性。 然而，并不存在泛型回调这样的东西。 每个回调都是特定的，应该由`Reader`类的用户定义。\n\n解决这一矛盾的一种方法是使`Reader`成为抽象类。 我们将`Callback`方法定义为`virtual`函数：\n\n```cpp\n  protected:\n    virtual void Callback(const T& data) = 0;\n```\n\n现在，由于`Reader`是抽象的，我们不能创建该类的实例。 我们必须继承它，并在名为`CoordLogger`的派生类中提供`Callback`方法的定义：\n\n```cpp\n  protected:\n    void Callback(const Message& data) override {\n      std::cout << \"Received coordinate \" << data << std::endl;\n    }\n```\n\n请注意，由于`Reader`构造函数接受参数，因此我们还需要在继承的类中定义构造函数。 我们将使用 C++ 11 标准中添加的继承构造函数：\n\n```cpp\n  using Reader<Message>::Reader;\n```\n\n现在，有了能够处理`Message`类型消息的`CoordLogger`类，我们就可以在我们的`DoReads`实现中使用它了。 我们只需要创建该类的一个实例并调用它的`Run`方法：\n\n```cpp\n  CoordLogger logger(kQueueName);\n  logger.Run();\n```\n\n当我们运行应用时，我们会得到以下输出：\n\n![](img/39b06101-cfed-4a08-9bdd-53fe51e643b6.png)\n\n此输出与前面配方中的输出没有太大不同，但现在实现的可伸缩性更强。 `DoReads`方法不执行任何特定于消息的操作。 它唯一的任务是创建和运行订阅服务器。 所有数据处理都封装在特定的类中。 您可以在不更改应用体系结构的情况下添加、替换和组合发布者和订阅者。\n\n# 还有更多的..。\n\nPOSIX 消息队列 API 为消息队列提供了基本功能，但也有一些限制。 不可能使用一个消息队列向多个订阅者发送消息。 您必须为每个订阅者创建单独的队列，否则从队列中读取的订阅者中只有一个会收到消息。\n\n有许多精致的消息队列和发布-订阅中间件以外部库的形式提供。 ZeroMQ 是一个强大、灵活、同时也是轻量级的传输库。 这使得它成为使用数据交换的发布-订阅模型构建的嵌入式应用的理想选择。\n\n# 使用 C++ lambdas 进行回调\n\n在发布-订阅模型中，订阅者通常注册一个回调，当来自发布者的消息传递给订阅者时将调用该回调。\n\n在前面的配方中，我们创建了一种使用继承和抽象类注册回调的机制。 它不是 C++ 中唯一可用的机制。 C++ 中提供的 Lambda 函数(从 C++ 11 标准开始)可以用作替代解决方案。 这消除了定义派生类所需的大量样板代码，并且在大多数情况下，允许开发人员以更清晰的方式表达他们的意图。\n\n在本食谱中，我们将学习如何使用 C++ lambda 函数来定义回调。\n\n# 怎么做……\n\n我们将使用前面菜谱中的大部分代码，*了解消息队列和发布者-订阅者模型*。 我们将修改`Reader`类以接受回调作为参数。 通过这个修改，我们可以直接使用`Reader`，而不需要依赖继承来定义回调：\n\n1.  将我们在前面的配方中创建的`ipc3`目录的内容复制到一个新目录中：`ipc4`。\n2.  保持所有代码不变，但`Reader`类除外。 让我们将其替换为以下代码片段：\n\n```cpp\ntemplate<class T>\nclass Reader {\n  private:\n    MessageQueue queue;\n    void (*func)(const T&);\n  public:\n    Reader(std::string& name, void (*func)(const T&)):\n      queue(name, O_RDONLY), func(func) {}\n\n    void Run() {\n      T data;\n      while(true) {\n        queue.Receive(reinterpret_cast<char*>(&data), \n         sizeof(data));\n        func(data);\n      }\n    }\n};\n```\n\n3.  既然我们的`Reader`类已经更改，我们就可以更新`DoReads`方法了。 我们可以使用 lambda 函数定义回调处理程序并将其传递给`Reader`构造函数：\n\n```cpp\nvoid DoReads() {\n  Reader<Message> logger(kQueueName, [](const Message& data) {\n    std::cout << \"Received coordinate \" << data << std::endl;\n  });\n  logger.Run();\n}\n```\n\n4.  不再需要`CoordLogger`类，因此我们可以安全地将其从代码中完全删除。\n5.  您可以构建和运行应用。\n\n# 它是如何运作的..。\n\n在这个配方中，我们修改了前面定义的`Reader`类，以接受其构造函数中的一个附加参数。 此参数具有特定的数据类型-指向函数的指针，该函数将用作回调：\n\n```cpp\nReader(std::string& name, void (*func)(const T&)):\n```\n\n处理程序存储在数据字段中以备将来使用：\n\n```cpp\nvoid (*func)(const T&);\n```\n\n现在，`Run`方法每次读取消息时，都会调用存储在`func`字段中的函数，而不是我们需要覆盖的`Callback`方法：\n\n```cpp\nqueue.Receive(reinterpret_cast<char*>(&data), sizeof(data));\nfunc(data);\n```\n\n去掉`Callback`函数使`Reader`成为一个具体的类，我们可以直接创建它的实例。 但是，现在我们需要提供一个处理程序作为其构造函数的参数。\n\n使用纯 C 语言，我们必须定义一个`named`函数并将其名称作为参数传递。 使用 C++，这种方法也是可能的，但 C++ 还提供了匿名函数或 lambda 函数的机制，这些函数可以在适当的位置定义。\n\n在`DoReads`方法中，我们创建了一个 lambda 函数，并将其直接传递给`Reader`构造函数：\n\n```cpp\n  Reader<Message> logger(kQueueName, [](const Message& data) {\n std::cout << \"Received coordinate \" << data << std::endl;\n });\n```\n\n构建和运行应用会产生以下输出：\n\n![](img/6c9b5c11-a5ea-4454-8e09-6d3b1ff2e1cd.png)\n\n正如我们所看到的，它与我们在前面的菜谱中创建的应用的输出相同。 但是，我们使用更少的代码和更好的可读性来完成这项工作。\n\n应该明智地使用 lambda 函数。 如果保持最小，它们会使代码更具可读性。 如果函数超过五行，请考虑改用命名函数。\n\n# 还有更多的..。\n\nC++ 为处理类似函数的对象提供了灵活的机制，并将它们与参数绑定。 这些机制被广泛用于转发调用和构建函数适配器。 [https://en.cppreference.com/w/cpp/utility/functional](https://en.cppreference.com/w/cpp/utility/functional)上的*函数对象*页是深入理解这些主题的一个很好的起点。\n\n# 探索数据序列化\n\n我们已经在[第 3 章](03.html)、*《使用不同的体系结构》*中简要介绍了序列化的某些方面。 当涉及到数据交换时，序列化是至关重要的。 序列化的任务是以接收方应用可以明确读取的方式表示发送方应用发送的所有数据。 鉴于发送器和接收器可能运行在不同的硬件平台上，并通过各种传输链路连接-**传输控制协议**/**网际协议**(**TCP/IP**)网络、**串行外设接口**(**SPI**)总线或串行链路，这项任务就不那么简单了。\n\n根据需求，有许多不同的实现序列化的方法，这就是 C++ 标准库不提供现成的序列化的原因。\n\n在本食谱中，我们将学习如何在 C++ 应用中实现简单的泛型序列化和反序列化。\n\n# 怎么做……\n\n序列化的目标是以一种可以在另一个系统或另一个应用中正确解码的方式对任何数据进行编码。 开发人员面临的典型障碍如下：\n\n*   特定于平台的差异，例如数据对齐和字节顺序。\n*   分散在内存中的数据；例如，链表的元素可以位于彼此相距较远的位置。 由指针链接的断开连接的块的表示对于内存来说是自然的，但在将其传输到另一个进程时不能自动转换为字节序列。\n\n解决此问题的一般方法是让类定义将其内容转换为序列化形式并从序列化形式恢复类实例的函数。\n\n在我们的应用中，我们将重载输出流的`operator<<`和输入流的`operator>>`，以分别序列化和反序列化数据：\n\n1.  在您的`~/test`工作目录中，创建一个名为`stream`的子目录。\n2.  使用您喜欢的文本编辑器在`stream`子目录中创建`stream.cpp`文件。\n3.  从要序列化的数据结构的定义开始：\n\n```cpp\n#include <iostream>\n#include <sstream>\n#include <list>\n\nstruct Point {\n  int x, y;\n};\n\nstruct Paths {\n  Point source;\n  std::list<Point> destinations;\n};\n```\n\n4.  接下来，我们重载`<<`和`>>`操作符，这两个操作符分别负责将`Point`对象写入流和从流中读取对象。 对于`Point`数据类型，请输入以下内容：\n\n```cpp\nstd::ostream& operator<<(std::ostream& o, const Point& p) {\n  o << p.x << \" \" << p.y << \" \";\n  return o;\n}\n\nstd::istream& operator>>(std::istream& is, Point& p) {\n  is >> p.x;\n  is >> p.y;\n  return is;\n}\n```\n\n5.  后跟`Paths`对象的`<<`和`>>`重载运算符：\n\n```cpp\nstd::ostream& operator<<(std::ostream& o, const Paths& paths) {\n  o << paths.source << paths.destinations.size() << \" \";\n  for (const auto& x : paths.destinations) {\n    o << x;\n  }\n  return o;\n}\n\nstd::istream& operator>>(std::istream& is, Paths& paths) {\n  size_t size;\n  is >> paths.source;\n  is >> size;\n  for (;size;size--) {\n    Point tmp;\n    is >> tmp;\n    paths.destinations.push_back(tmp);\n  }\n  return is;\n}\n```\n\n6.  现在，让我们将所有内容包装在`main`函数中：\n\n```cpp\nint main(int argc, char** argv) {\n  Paths paths = {{0, 0}, {{1, 1}, {0, 1}, {1, 0}}};\n\n  std::stringstream in;\n  in << paths;\n  std::string serialized = in.str();\n  std::cout << \"Serialized paths into the string: [\"\n            << serialized << \"]\" << std::endl;\n\n  std::stringstream out(serialized);\n  Paths paths2;\n  out >> paths2;\n  std::cout << \"Original: \" << paths.destinations.size()\n            << \" destinations\" << std::endl;\n  std::cout << \"Restored: \" << paths2.destinations.size()\n            << \" destinations\" << std::endl;\n\n  return 0;\n}\n```\n\n7.  最后，创建一个`CMakeLists.txt`文件，其中包含程序的构建规则：\n\n```cpp\ncmake_minimum_required(VERSION 3.5.1)\nproject(stream)\nadd_executable(stream stream.cpp)\n\nset(CMAKE_SYSTEM_NAME Linux)\nset(CMAKE_SYSTEM_PROCESSOR arm)\n\nSET(CMAKE_CXX_FLAGS \"--std=c++ 11\")\n\nset(CMAKE_CXX_COMPILER /usr/bin/arm-linux-gnueabi-g++)\n```\n\n现在您可以构建和运行应用了。\n\n# 它是如何运作的..。\n\n在我们的测试应用中，我们定义了一个数据类型来表示从一个源点到多个目标点的路径。 我们有意使用分散在内存中的层次结构来演示如何以通用方式处理此问题。\n\n如果我们对性能没有特定的要求，序列化的可能方法之一是以文本格式存储数据。 除了简单之外，它还有两大优势：\n\n*   文本编码自动解决与字节顺序、对齐方式和整数数据类型大小相关的所有问题。\n*   它是人类可读的。 开发人员无需任何附加工具即可使用序列化数据进行调试。\n\n要使用文本表示，我们可以使用标准库提供的输入流和输出流。 它们已经定义了写入和读取格式化数字的函数。\n\n`Point`结构定义为两个整数值：`x`和`y`。 我们覆盖此数据类型的`operator<<`以写入`x`和`y`值，后跟空格。 这样，我们就可以在覆盖的`operator>>`操作中顺序读取它们。\n\n`Path`数据类型稍微复杂一些。 它包含目的地的链接列表。 由于列表的大小可能会有所不同，因此我们需要在序列化列表内容之前写入列表的实际大小，以便能够在反序列化过程中正确地恢复它：\n\n```cpp\n  o << paths.source << paths.destinations.size() << \" \";\n```\n\n因为我们已经覆盖了`<<`和`>>`运算符的`Point`方法，所以我们可以在`Paths`方法中使用它们。 这样，我们将`Point`对象写入流或从流中读取它们，而不知道它们的数据字段的内容。 递归处理分层数据结构：\n\n```cpp\n  for (const auto& x : paths.destinations) {\n    o << x;\n  }\n```\n\n最后，我们测试我们的序列化和反序列化实现。 我们创建`Paths`对象的示例实例：\n\n```cpp\nPaths paths = {{0, 0}, {{1, 1}, {0, 1}, {1, 0}}};\n```\n\n然后，我们使用`std::stringstream`数据类型将其内容序列化为字符串：\n\n```cpp\n  std::stringstream in;\n  in << paths;\n  std::string serialized = in.str();\n```\n\n接下来，我们创建一个空的`Path`对象，并将字符串的内容反序列化到其中：\n\n```cpp\n  Paths paths2;\n  out >> paths2;\n```\n\n最后，我们检查它们是否匹配。 当我们运行应用时，我们可以使用以下输出来执行此操作：\n\n![](img/f862eddd-5a3d-4234-8b4b-7edb0bf4c0ac.png)\n\n还原对象的`destinations`列表的大小与原始对象的`destinations`列表的大小匹配。 我们还可以看到序列化数据的内容。\n\n此示例说明如何为任何数据类型生成自定义序列化。 它可以在没有任何外部库的情况下完成。 但是，在性能和内存效率很重要的情况下，使用第三方序列化库将是更实际的方法。\n\n# 还有更多的..。\n\n从头开始实现序列化是很困难的。 位于[https://uscilab.github.io/cereal/](https://uscilab.github.io/cereal/)的谷物库和位于[https://www.boost.org/doc/libs/1_71_0/libs/serialization/doc/index.html](https://www.boost.org/doc/libs/1_71_0/libs/serialization/doc/index.html)的 Boost 库提供了一个基础，可以帮助您更快、更轻松地向应用添加序列化。\n\n# 使用 FlatBuffers 库\n\n序列化和反序列化是一个复杂的主题。 虽然临时序列化看起来简单明了，但很难使其泛型、易于使用和快速。 值得庆幸的是，有一些库可以处理所有这些复杂性。\n\n在本食谱中，我们将学习如何使用其中一个序列化库：FlatBuffers。 它的设计考虑到了嵌入式编程，使序列化和反序列化内存变得高效和快速。\n\nFlatBuffers 使用**接口定义语言**(**IDL**)定义数据模式。 该架构描述了我们需要序列化的数据结构的所有字段。 在设计模式时，我们使用名为**flatc**的特殊工具来生成特定编程语言的代码，在我们的示例中是 C++。\n\n生成的代码以序列化形式存储所有数据，并为开发人员提供所谓的**getter**和**setter**方法来访问数据字段。 Getter 动态执行反序列化。 以序列化形式存储数据使 FlatBuffer 真正提高了内存效率。 不需要额外的内存来存储序列化数据，而且在大多数情况下，反序列化的开销很低。\n\n在本食谱中，我们将学习如何开始在我们的应用中使用 FlatBuffer 进行数据序列化。\n\n# 怎么做……\n\nFlatBuffers 是一组工具和库。 在使用之前，我们需要下载并构建它：\n\n1.  从[https://codeload.github.com/google/flatbuffers/zip/master](https://codeload.github.com/google/flatbuffers/zip/master)下载最新的 FlatBuffer 存档，并将其解压到`test`目录中。 这将创建一个名为`flatbuffers-master`的新目录。\n2.  切换到构建控制台，将目录更改为`flatbuffers-master`，然后运行以下命令来构建和安装库和工具。 确保您以 root 身份运行。 如果没有，按*Ctrl*+*C*退出用户外壳：\n\n```cpp\n# cmake .\n# make\n# make install\n```\n\n现在，我们可以在应用中使用 FlatBuffers 了。 让我们重用我们在前面的一个食谱中创建的应用：\n\n3.  将`ipc4`目录的内容复制到新创建的名为`flat`的目录中。\n4.  创建名为`message.fbs`的文件，在编辑器中打开该文件，然后输入以下代码：\n\n```cpp\n struct Message {\n x: int;\n y: int;\n}\n```\n\n5.  通过运行以下命令从`message.fbs`生成 C++ 源代码：\n\n```cpp\n$ flatc --cpp message.fbs\n```\n\n这将创建一个名为`message_generated.h`的新文件。\n\n6.  在编辑器中打开`ipc1.cpp`。 在`mqueue.h`include 之后添加生成的`message_generated.h`文件的`include`指令：\n\n```cpp\n#include <mqueue.h>\n\n#include \"message_generated.h\"\n```\n\n7.  现在，去掉代码中声明的`Message`结构。 我们将改用在 FlatBuffers 架构文件中生成的结构。\n\n8.  由于 FlatBuffers 使用 getter 方法而不是直接访问结构字段，因此我们需要修改用于将点数据打印到控制台的重新定义的`operator<<`操作的主体。 更改很小-我们只向每个数据字段添加括号：\n\n```cpp\n std::ostream& operator<<(std::ostream& o, const Message& m) {\n  o << \"(x=\" << m.x() << \", y=\" << m.y() << \")\";\n}\n```\n\n9.  代码修改已经完成。 现在，我们需要更新构建规则以链接到 FlatBuffers 库。 打开`CMakeLists.txt`并输入以下行：\n\n```cpp\ncmake_minimum_required(VERSION 3.5.1)\nproject(flat)\nadd_executable(flat ipc1.cpp)\n\nset(CMAKE_SYSTEM_NAME Linux)\nset(CMAKE_SYSTEM_PROCESSOR arm)\n\nSET(CMAKE_CXX_FLAGS_RELEASE \"--std=c++ 11\")\nSET(CMAKE_CXX_FLAGS_DEBUG \"${CMAKE_CXX_FLAGS_RELEASE} -g -DDEBUG\")\ntarget_link_libraries(flat rt flatbuffers)\n\nset(CMAKE_C_COMPILER /usr/bin/arm-linux-gnueabi-gcc)\nset(CMAKE_CXX_COMPILER /usr/bin/arm-linux-gnueabi-g++)\n\nset(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)\nset(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)\nset(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)\nset(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)\n```\n\n10.  切换到构建控制台，然后切换到用户 shell：\n\n```cpp\n# su - user\n$\n```\n\n11.  构建并运行应用。\n\n# 它是如何运作的..。\n\nFlatBuffers 是一个外部库，它在 Ubuntu 包存储库中不可用，这就是为什么我们需要首先下载、构建和安装它。 安装完成后，我们可以在应用中使用它。\n\n我们使用使用 C++ lambdas for callback 配方为*创建的现有应用作为起点。 在该应用中，我们定义了一个名为`Message`的结构来表示我们用于 IPC 的数据类型。 我们将用 FlatBuffers 提供的新数据类型替换它。 这个新的数据类型将透明地执行所有必要的序列化和反序列化。*\n\n我们从代码中完全删除了`Message`结构的定义。 相反，我们生成一个新的头文件，名为`message_generated.h`。 此文件是从`message.fbs`FlatBuffers 架构文件生成的。 此架构文件定义了一个具有两个整数字段的结构-`x`和`y`：\n\n```cpp\n  x: int;\n  y: int;\n```\n\n此定义与前面的定义相同；唯一的区别是语法-FlatBuffers 的架构使用冒号将字段名称与字段类型分开。\n\n一旦通过`flatc`命令调用创建了`message_generated.h`，我们就可以在我们的代码中使用它了。 我们按如下方式添加适当的`include`：\n\n```cpp\n#include \"message_generated.h\"\n```\n\n生成的消息与我们之前使用的消息结构相同，但是正如我们前面讨论的那样，FlatBuffers 以序列化的形式存储数据，并且需要动态地反序列化数据。 这就是为什么我们必须使用`x()`访问器方法而不只是`x`和`y()`访问器方法而不是只使用`y`，而不是直接访问数据字段。\n\n我们使用直接访问消息数据字段的唯一位置是重写的`operator<<`操作。 我们添加圆括号将直接字段访问转换为调用 FlatBuffers getter 方法：\n\n```cpp\n  o << \"(x=\" << m.x() << \", y=\" << m.y() << \")\";\n```\n\n让我们构建并运行应用。 我们将看到以下输出：\n\n![](img/96e44321-cffb-4c7f-9989-b51539a077ff.png)\n\n输出与我们的自定义消息数据类型相同。 只需对代码进行少量修改，我们就可以将消息迁移到 FlatBuffer。 现在，我们可以在多台计算机上运行我们的发布器和订阅器-这些计算机可以有不同的体系结构-并确保每台计算机都能正确地解释消息。\n\n# 还有更多的..。\n\n除了 FlatBuffer 之外，还有许多其他序列化库和技术，每种技术都有其优缺点。 要更好地了解如何在应用中设计序列化，请参阅[https://isocpp.org/wiki/faq/serialization](https://isocpp.org/wiki/faq/serialization)上的*C++ 序列化常见问题*。"
  },
  {
    "path": "docs/emb-prog-mod-cpp-cb/09.md",
    "content": "# 九、外部设备\n\n与外部设备的通信是任何嵌入式应用的重要组成部分。 应用需要检查可用性和状态，并向各种设备发送数据和从各种设备接收数据。\n\n每个目标平台都是不同的，并且存在许多将外部设备连接到计算单元的方式。 然而，有几种硬件和软件接口已经成为与外部设备通信的行业标准。 在本章中，我们将学习如何使用直接连接到处理器引脚或通过串行接口连接的外部设备。 本章涵盖以下主题：\n\n*   控制通过 GPIO 连接的设备\n*   探索脉宽调制\n*   在 Linux 中使用 ioctl 访问实时时钟\n*   用 libgpiod 控制 GPIO 引脚\n*   控制 I2C 外部设备\n\n本章中的食谱涉及到与真实硬件的交互，并且打算在真实的 Raspberry Pi 板上运行。\n\n# 控制通过 GPIO 连接的设备\n\n**通用输入输出**(GPIO)是将外部设备连接到 CPU 的最简单方式。 每个处理器通常都有一定数量的引脚预留给一般用途。 这些管脚可以直接电连接到外部设备的管脚。 嵌入式应用可以通过改变配置用于输出的管脚的信号电平或通过读取输入管脚的信号电平来控制设备。\n\n信号电平的解释不遵循任何协议，并且由外部设备确定。 开发人员需要查阅设备数据手册才能对通信进行正确编程。\n\n这种类型的通信通常使用专用的设备驱动程序在内核端完成。 然而，这并不总是一个要求。 在本教程中，我们将学习如何从用户空间应用使用 Raspberry PI 板上的 GPIO 接口。\n\n# 怎么做……\n\n我们将创建一个简单的应用来控制连接到 Raspberry PI 板上的通用引脚的**发光二极管**(LED)：\n\n1.  在您的`~/test`工作目录中，创建一个名为`gpio`的子目录。\n2.  使用您喜欢的文本编辑器在`gpio`子目录中创建`gpio.cpp`文件。\n3.  将以下代码片段放入文件中：\n\n```cpp\n#include <chrono>\n#include <iostream>\n#include <thread>\n#include <wiringPi.h>\n\nusing namespace std::literals::chrono_literals;\nconst int kLedPin = 0;\n\nint main (void)\n{\n  if (wiringPiSetup () <0) {\n    throw std::runtime_error(\"Failed to initialize wiringPi\");\n  }\n\n  pinMode (kLedPin, OUTPUT);\n  while (true) {\n    digitalWrite (kLedPin, HIGH);\n    std::cout << \"LED on\" << std::endl;\n    std::this_thread::sleep_for(500ms) ;\n    digitalWrite (kLedPin, LOW);\n    std::cout << \"LED off\" << std::endl;\n    std::this_thread::sleep_for(500ms) ;\n  }\n  return 0 ;\n}\n```\n\n4.  创建包含我们程序的构建规则的`CMakeLists.txt`文件：\n\n```cpp\ncmake_minimum_required(VERSION 3.5.1)\nproject(gpio)\nadd_executable(gpio gpio.cpp)\ntarget_link_libraries(gpio wiringPi)\n```\n\n5.  按照[http://wiringpi.com/examples/blink/](http://wiringpi.com/examples/blink/)的*WiringPI 示例*部分中的说明将 LED 连接到树莓 PI 板。\n6.  设置到您的 Raspberry PI 板的 SSH 连接。 按照[https://www.raspberrypi.org/documentation/remote-access/ssh/](https://www.raspberrypi.org/documentation/remote-access/ssh/)的*覆盆子 PI 文档*部分的说明进行操作。\n7.  通过 SSH 将`gpio`文件夹的内容复制到 Raspberry PI 板。\n8.  通过 SSH 登录主板，然后构建并运行应用：\n\n```cpp\n$ cd gpio && cmake . && make && sudo ./gpio\n```\n\n您的应用应该会运行，并且您应该能够观察到 LED 闪烁。\n\n# 它是如何运作的..。\n\n覆盆子 PI 板有 40 个引脚(第一个型号为 26 个)，可使用**内存映射输入输出**(**MMIO**)机制进行编程。 MMIO 允许开发人员通过在系统的物理内存中读取或写入特定地址来查询或设置管脚的状态。\n\n在[第 6 章](06.html)，*内存管理*中的*使用专用内存*配方中，我们学习了如何访问 MMIO 寄存器。 在本食谱中，我们将把 MMIO 地址的操作卸载到专门库`wiringPi`。 它隐藏了内存映射和在幕后查找适当偏移量的所有复杂性，而是公开了一个干净的 API。\n\n这个库预装在 Raspberry PI 板上，因此为了简化构建过程，我们将直接在板上构建代码，而不是使用交叉编译。 与其他方法不同，我们的构建规则没有提到交叉编译器--我们将在主板上使用本机 ARM 编译器。 我们只向`wiringPi`库添加依赖项：\n\n```cpp\ntarget_link_libraries(gpio wiringPi)\n```\n\n此示例的代码是对闪烁 LED 的`wiringPi`示例的修改。 首先，我们初始化`wiringPi`库：\n\n```cpp\nif (wiringPiSetup () < 0) {\n    throw std::runtime_error(\"Failed to initialize wiringPi\");\n}\n```\n\n接下来，我们进入无穷无尽的循环。 在每次迭代中，我们将管脚设置为`HIGH`状态：\n\n```cpp\n    digitalWrite (kLedPin, HIGH);\n```\n\n在`500 ms`延迟之后，我们将相同的凹坑设置为`LOW`状态，并添加另一个延迟：\n\n```cpp\n digitalWrite (kLedPin, LOW);\n    std::cout << \"LED off\" << std::endl;\n std::this_thread::sleep_for(500ms) ;\n```\n\n我们将程序配置为使用管脚`0`，它对应于 Raspberry PI 的`BCM2835`芯片的`GPIO.0`或管脚`17`：\n\n```cpp\nconst int kLedPin = 0;\n```\n\n如果 LED 连接到此引脚，它将闪烁，亮起 0.5 秒，然后熄灭 0.5 秒。 通过调整循环中的延迟，您可以更改闪烁模式。\n\n由于程序进入死循环，我们可以随时通过在 SSH 控制台中按*Ctrl*+*C*来终止它；否则，它将永远运行。\n\n当我们运行应用时，我们只看到以下输出：\n\n![](img/9f3a257e-977a-4e63-97e3-c39452e84ce4.png)\n\n我们在转动 LED`on`或`off`时记录，但要检查程序是否正常工作，我们需要查看连接到引脚上的 LED。 如果我们按照接线说明操作，我们就能看到它是如何工作的。 程序运行时，板卡上的 LED 与程序输出同步闪烁：\n\n![](img/72b7b80d-860c-48ec-ba6c-bb4aaee983d5.png)\n\n我们能够控制直接连接到 CPU 引脚的简单设备，而无需编写复杂的设备驱动程序。\n\n# 探索脉宽调制\n\n数字引脚只能处于以下两种状态之一：`HIGH`或`LOW`。 连接到数字引脚的 LED 也只能相应地处于以下两种状态之一：`on`或`off`。 但有没有办法控制这款 LED 的亮度呢？ 是的，我们可以使用一种称为**脉宽调制**(**PWM**)的方法。\n\nPWM 背后的想法很简单。 我们通过周期性地打开或关闭电信号来限制其提供的功率。 这使得信号脉冲具有一定的频率，且功率大小与脉冲宽度成正比，即信号为`HIGH`的时间。\n\n例如，如果我们将引脚转到`HIGH`10 微秒，然后在循环中再将`LOW`转到 90 微秒，则连接到该引脚的设备将获得引脚始终为`HIGH`时所提供电力的 10%。\n\n在本食谱中，我们将学习如何使用 PWM 来控制连接到树莓 PI 板上的数字 GPIO 引脚的 LED 的亮度。\n\n# 怎么做……\n\n我们将创建一个简单的应用，用于逐渐改变连接到 Raspberry PI 板上通用引脚的 LED 的亮度：\n\n1.  在您的`~/test`工作目录中，创建一个名为`pwm`的子目录。\n2.  使用您喜欢的文本编辑器在`pwm`子目录中创建`pwm.cpp`文件。\n3.  让我们放入所需的`include`函数，并定义一个名为`Blink`的函数：\n\n```cpp\n#include <chrono>\n#include <thread>\n\n#include <wiringPi.h>\n\nusing namespace std::literals::chrono_literals;\n\nconst int kLedPin = 0;\n\nvoid Blink(std::chrono::microseconds duration, int percent_on) {\n    digitalWrite (kLedPin, HIGH);\n    std::this_thread::sleep_for(\n            duration * percent_on / 100) ;\n    digitalWrite (kLedPin, LOW);\n    std::this_thread::sleep_for(\n            duration * (100 - percent_on) / 100) ;\n}\n```\n\n4.  然后是`main`函数：\n\n```cpp\nint main (void)\n{\n  if (wiringPiSetup () <0) {\n    throw std::runtime_error(\"Failed to initialize wiringPi\");\n  }\n\n  pinMode (kLedPin, OUTPUT);\n\n  int count = 0;\n  int delta = 1;\n  while (true) {\n    Blink(10ms, count);\n    count = count + delta;\n    if (count == 101) {\n      delta = -1;\n    } else if (count == 0) {\n      delta = 1;\n    }\n  }\n  return 0 ;\n}\n```\n\n5.  创建包含我们程序的构建规则的`CMakeLists.txt`文件：\n\n```cpp\ncmake_minimum_required(VERSION 3.5.1)\nproject(pwm)\nadd_executable(pwm pwm.cpp)\ntarget_link_libraries(pwm wiringPi)\n```\n\n6.  按照[http://wiringpi.com/examples/blink/](http://wiringpi.com/examples/blink/.)的*WiringPI 示例*部分中的说明将 LED 连接到树莓 PI 板。\n7.  设置到您的 Raspberry PI 板的 SSH 连接。 按照[https://www.raspberrypi.org/documentation/remote-access/ssh/](https://www.raspberrypi.org/documentation/remote-access/ssh/)的*覆盆子 PI 文档*部分的说明进行操作。\n8.  通过 SSH 将`pwm`文件夹的内容复制到 Raspberry PI 板。\n9.  通过 SSH 登录主板，然后构建并运行应用：\n\n```cpp\n$ cd pwm && cmake . && make && sudo ./pwm\n```\n\n您的应用现在应该运行了，您可以看到 LED 在闪烁。\n\n# 它是如何运作的..。\n\n此配方重用代码来闪烁 LED 和前面配方中的原理图。 我们将此代码从`main`函数移至一个新函数`Blink`。\n\n`Blink`函数接受两个参数-`duration`和`percent_on`：\n\n```cpp\nvoid Blink(std::chrono::microseconds duration, int percent_on)\n```\n\n`duration`确定脉冲的总宽度(以微秒为单位)。 `percent_on`定义信号为`HIGH`时的时间与脉冲总持续时间的比率。\n\n实现很简单。 调用`Blink`时，它将管脚转到`HIGH`，并等待与`percent_on`成比例的时间量：\n\n```cpp\n    digitalWrite (kLedPin, HIGH);\n    std::this_thread::sleep_for(\n            duration * percent_on / 100);\n```\n\n之后，它将引脚转到`LOW`，并等待剩余时间：\n\n```cpp\n    digitalWrite (kLedPin, LOW);\n    std::this_thread::sleep_for(\n            duration * (100 - percent_on) / 100);\n```\n\n`Blink`是实现 PWM 的主要构件。 我们可以通过将`percent_on`从`0`更改为`100`来控制亮度，如果我们选择足够短的`duration`，我们将不会看到任何闪烁。\n\n等于或短于电视或监视器刷新率的持续时间就足够好了。 对于 60 Hz，持续时间为 16.6 毫秒。 为简单起见，我们使用 10 毫秒。\n\n接下来，我们将所有内容包装在另一个无限循环中，但现在它有另一个参数`count`：\n\n```cpp\n  int count = 0;\n```\n\n它随着每次迭代而更新，并在`0`和`100`之间反弹。 `delta`变量定义变化的方向(减少或增加)以及变化量，在我们的示例中始终为`1`：\n\n```cpp\n  int delta = 1;\n```\n\n当计数达到`101`或`0`时，方向改变：\n\n```cpp\n    if (count == 101) {\n      delta = -1;\n    } else if (count == 0) {\n      delta = 1;\n    }\n```\n\n在每次迭代中，我们调用`Blink`，将`10ms`作为脉冲传递，将`count`作为比率传递，该比率定义 LED 亮起的时间量，从而定义 LED 的亮度(如下图所示)：\n\n```cpp\n    Blink(10ms, count);\n```\n\n![](img/98a8f41e-0940-43fa-82fe-09b2c45f7fb0.png)\n\n由于更新频率很高，我们无法判断 LED 何时从打开状态变为关闭状态。\n\n当我们将所有东西连接起来并运行程序时，我们可以看到 LED 变得更亮或更暗。\n\n# 还有更多的..。\n\nPWM 在嵌入式系统中有着广泛的用途。 它是伺服控制和电压调节的常用机构。 使用*脉宽调制*维基百科页面(可从[https://en.wikipedia.org/wiki/Pulse-width_modulation](https://en.wikipedia.org/wiki/Pulse-width_modulation)获取)作为了解该技术的更多信息的起点。\n\n# 在 Linux 中使用 ioctl 访问实时时钟\n\n在前面的配方中，我们使用 MMIO 从用户空间 Linux 应用访问外部设备。 但是，此接口不是推荐的用户空间应用和设备驱动程序之间的通信方式。\n\n在类似 Unix 的操作系统(如 Linux)中，可以使用所谓的设备文件以与常规文件相同的方式访问大多数外部设备。 当应用打开设备文件时，它可以从该文件读取数据，从相应的设备获取数据，或者写入该文件，向该设备发送数据。\n\n在许多情况下，设备驱动程序不能处理非结构化数据流。 他们希望以请求和响应的形式组织数据交换，其中每个请求和响应都有特定和固定的格式。\n\n这种通信由`ioctl`系统调用涵盖。 它接受依赖于设备的请求代码作为其参数。 它还可以包含对请求数据进行编码或为输出数据提供存储的其他参数。 这些参数特定于特定设备和请求代码。\n\n在本食谱中，我们将学习如何在用户空间应用中使用`ioctl`与设备驱动程序进行数据交换。\n\n# 怎么做……\n\n我们将创建一个应用，从连接到 Raspberry PI 板的**实时时钟**(**RTC**)读取当前时间：\n\n1.  在您的`~/test`工作目录中，创建一个名为`rtc`的子目录。\n2.  使用您喜欢的文本编辑器在`rtc`子目录中创建`rtc.cpp`文件。\n3.  让我们将所需的`include`函数放入`rtc.cpp`文件中：\n\n```cpp\n#include <iostream>\n#include <system_error>\n\n#include <time.h>\n#include <unistd.h>\n#include <fcntl.h>\n#include <sys/ioctl.h>\n#include <linux/rtc.h>\n```\n\n4.  现在，我们定义一个名为`Rtc`的类，它封装与实时时钟设备的通信：\n\n```cpp\nclass Rtc {\n  int fd;\n  public:\n    Rtc() {\n      fd = open(\"/dev/rtc\", O_RDWR);\n      if (fd < 0) {\n        throw std::system_error(errno,\n            std::system_category(),\n            \"Failed to open RTC device\");\n      }\n    }\n\n    ~Rtc() {\n      close(fd);\n    }\n\n    time_t GetTime(void) {\n      union {\n        struct rtc_time rtc;\n        struct tm tm;\n      } tm;\n      int ret = ioctl(fd, RTC_RD_TIME, &tm.rtc);\n      if (ret < 0) {\n        throw std::system_error(errno,\n            std::system_category(),\n            \"ioctl failed\");\n      }\n      return mktime(&tm.tm);\n    }\n};\n```\n\n5.  定义类后，我们将一个简单的用法示例放入`main`函数：\n\n```cpp\nint main (void)\n{\n  Rtc rtc;\n  time_t t = rtc.GetTime();\n  std::cout << \"Current time is \" << ctime(&t)\n            << std::endl;\n\n  return 0 ;\n}\n```\n\n6.  创建包含我们程序的构建规则的`CMakeLists.txt`文件：\n\n```cpp\ncmake_minimum_required(VERSION 3.5.1)\nproject(rtc)\nadd_executable(rtc rtc.cpp)\n\nset(CMAKE_SYSTEM_NAME Linux)\nset(CMAKE_SYSTEM_PROCESSOR arm)\n\nset(CMAKE_CXX_COMPILER /usr/bin/arm-linux-gnueabi-g++)\n```\n\n7.  构建您的应用并将生成的`rtc`二进制文件复制到我们的 Raspberry PI 仿真器。\n\n# 它是如何运作的..。\n\n我们正在实现一个直接与连接到系统的硬件 RTC 对话的应用。 系统时钟和 RTC 之间存在差异。 系统时钟仅在系统运行时处于活动状态并保持不变。 当系统关机或进入休眠模式时，系统时钟将无效。 即使系统关闭，RTC 仍处于活动状态。 它维护用于在系统启动时配置系统时钟的实际时间。 此外，还可以对其进行编程，使其在睡眠模式下的特定时间唤醒系统。\n\n我们将与 RTC 驱动程序的所有通信封装到一个名为`Rtc`的类中。 与驱动程序的所有数据交换都通过`/dev/rtc`特殊设备文件。 在`Rtc`类构造函数中，我们打开设备文件并将生成的文件描述符存储在`fd`实例变量中：\n\n```cpp\n  fd = open(\"/dev/rtc\", O_RDWR);\n```\n\n类似地，析构函数用于关闭文件：\n\n```cpp\n    ~Rtc() {\n      close(fd);\n    }\n```\n\n由于设备在销毁`Rtc`实例后立即在析构函数中关闭，因此我们可以使用**Resource Acquisition is Initialization**(RAII)习惯用法在出现问题时抛出异常，而不会泄漏文件描述符：\n\n```cpp\n      if (fd < 0) {\n        throw std::system_error(errno,\n            std::system_category(),\n            \"Failed to open RTC device\");\n      }\n```\n\n我们的类只定义了一个成员函数-`GetTime`。 它是`RTC_RD_TIME``ioctl`调用之上的包装器。 此调用期望`rtc_time`结构返回当前时间。 它与我们将用来将 RTC 驱动程序返回的时间转换为 POSIX 时间戳格式的`tm`结构几乎相同，因此我们将它们放入与`union`数据类型相同的内存位置：\n\n```cpp\n      union {\n        struct rtc_time rtc;\n        struct tm tm;\n      } tm;\n```\n\n这样，我们可以避免将相同的字段从一个结构复制到另一个结构。\n\n一旦数据结构准备就绪，我们调用`ioctl`调用，将`RTC_RD_TIME`常量作为请求 ID 传递，并将指向我们的结构的指针作为存储数据的地址传递到：\n\n```cpp\n  int ret = ioctl(fd, RTC_RD_TIME, &tm.rtc);\n```\n\n一旦成功，`ioctl`将返回`0`。 在本例中，我们使用`mktime`函数将结果数据结构转换为`time_t`POSIX 时间戳格式：\n\n```cpp\n  return mktime(&tm.tm);\n```\n\n在`main`函数中，我们创建了`Rtc`类的一个实例，然后调用`GetTime`方法：\n\n```cpp\n  Rtc rtc;\n  time_t t = rtc.GetTime();\n```\n\n由于 POSIX 时间戳表示自 1970 年 1 月 1 日以来的秒数，因此我们使用`ctime`函数将其转换为友好的表示形式，并将结果输出到控制台：\n\n```cpp\n  std::cout << \"Current time is \" << ctime(&t)\n```\n\n当我们运行应用时，我们可以看到以下输出：\n\n![](img/b7640217-c2f3-4c53-b5b8-c7901c07760f.png)\n\n我们可以使用`ioctl`直接从硬件时钟读取当前时间。 `ioctl`API 在 Linux 嵌入式应用中广泛用于与设备通信。\n\n# 还有更多\n\n在我们的简单示例中，我们学习了如何只使用一个`ioctl`请求。 RTC 设备支持许多其他请求，这些请求可用于设置警报、更新时间和控制 RTC 中断。 有关更多详细信息，请参阅[https://linux.die.net/man/4/rtc](https://linux.die.net/man/4/rtc)上的*RTCioctl 文档*部分。\n\n# 用 libgpiod 控制 GPIO 引脚\n\n在前面的食谱中，我们了解了如何使用`ioctl`API 访问 RTC。 我们可以用它来控制 GPIO 引脚吗？ 答案是肯定的。 最近，Linux 中添加了一个通用 GPIO 驱动程序，以及一个用户空间库`libgpiod`，通过在通用`ioctl`API 之上添加一个便利层，简化了对连接到 GPIO 的设备的访问。 该接口允许嵌入式开发人员在任何基于 Linux 的平台上管理他们的设备，而无需编写设备驱动程序。 此外，它还为 C++ 提供开箱即用的绑定。\n\n因此，尽管`wiringPi`库因其易于使用的接口而仍被广泛使用，但它已被弃用。\n\n在本食谱中，我们将学习如何使用`libgpiod`C++ 绑定。 我们将使用相同的 LED 闪烁示例来查看`wiringPi`和`libgpiod`方法的不同和相似之处。\n\n# 怎么做……\n\n我们将使用新的`libgpiod`API 创建一个应用，使连接到 Raspberry PI 板的 LED 闪烁：\n\n1.  在您的`~/test`工作目录中，创建一个名为`gpiod`的子目录。\n2.  使用您喜欢的文本编辑器在`gpiod`子目录中创建`gpiod.cpp`文件。\n3.  将应用的代码放入`rtc.cpp`文件：\n\n```cpp\n#include <chrono>\n#include <iostream>\n#include <thread>\n\n#include <gpiod.h>\n#include <gpiod.hpp>\n\nusing namespace std::literals::chrono_literals;\n\nconst int kLedPin = 17;\n\nint main (void)\n{\n\n  gpiod::chip chip(\"gpiochip0\");\n  auto line = chip.get_line(kLedPin);\n  line.request({\"test\",\n                 gpiod::line_request::DIRECTION_OUTPUT, \n                 0}, 0);\n\n  while (true) {\n    line.set_value(1);\n    std::cout << \"ON\" << std::endl;\n    std::this_thread::sleep_for(500ms);\n    line.set_value(0);\n    std::cout << \"OFF\" << std::endl;\n    std::this_thread::sleep_for(500ms);\n  }\n\n  return 0 ;\n}\n```\n\n4.  创建包含我们程序的构建规则的`CMakeLists.txt`文件：\n\n```cpp\ncmake_minimum_required(VERSION 3.5.1)\nproject(gpiod)\nadd_executable(gpiod gpiod.cpp)\ntarget_link_libraries(gpiod gpiodcxx)\n```\n\n5.  按照[http://wiringpi.com/examples/blink/](http://wiringpi.com/examples/blink/)的*WiringPI 示例*部分中的说明将 LED 连接到树莓 PI 板。\n6.  设置到您的 Raspberry PI 板的 SSH 连接。 按照[https://www.raspberrypi.org/documentation/remote-access/](https://www.raspberrypi.org/documentation/remote-access/)的*覆盆子 PI 文档*部分的说明进行操作。\n7.  通过 SSH 将`gpio`文件夹的内容复制到 Raspberry PI 板。\n8.  安装`libgpiod-dev`包：\n\n```cpp\n$ sudo apt-get install gpiod-dev\n```\n\n9.  通过 SSH 登录主板，然后构建并运行应用：\n\n```cpp\n$ cd gpiod && cmake . && make && sudo ./gpiod\n```\n\n您的应用应该会运行，并且您可以看到 LED 闪烁。\n\n# 它是如何运作的..。\n\n我们的应用使用一种新的、推荐的方式访问 Linux 中的 GPIO 设备。 因为它是最近才添加的，所以需要安装最新版本的 Raspbian 发行版`buster`。\n\n`gpiod`库本身提供高级包装器，以便使用`ioctl`API 与 GPIO 内核模块通信。 该接口是为 C 语言设计的，其上还有一层用于 C++ 绑定的附加层。 这一层位于`libgpiocxx`库中，该库与 C 的`libgpiod`库一起是`libgpiod2`包的一部分。\n\n该库使用异常来报告错误，因此代码很简单，并且不会杂乱地检查返回代码。 而且，我们不需要费心释放捕获的资源；它是通过 C++ RAII 机制自动完成的。\n\n当应用启动时，它创建类芯片的一个实例，该实例用作 GPIO 通信的入口点。 其构造函数接受要使用的设备的名称：\n\n```cpp\n  gpiod::chip chip(\"gpiochip0\");\n```\n\n接下来，我们创建该线的一个实例，该实例表示特定的 GPIO 管脚：\n\n```cpp\n  auto line = chip.get_line(kLedPin);\n```\n\n请注意，与`wiringPi`实现不同，我们传递的是`17`管脚编号，因为`libgpiod`使用本机 Broadcom SOC 通道(**BCM**)管脚编号：\n\n```cpp\nconst int kLedPin = 17;\n```\n\n创建 LINE 实例后，我们需要配置所需的访问模式。 我们构造`line_request`结构的一个实例，传递消费者的名称(`\"test\"`)和一个常量，该常量指示管脚已配置为输出：\n\n```cpp\n  line.request({\"test\",\n                 gpiod::line_request::DIRECTION_OUTPUT, \n                 0}, 0);\n```\n\n之后，我们可以使用`set_value`方法更改引脚状态。 与`wiringPi`示例中一样，我们将`500ms`的管脚设置为`1`或`HIGH`，然后将循环中的另一个`500ms`设置回`0`或`LOW`：\n\n```cpp\n    line.set_value(1);\n    std::cout << \"ON\" << std::endl;\n    std::this_thread::sleep_for(500ms);\n    line.set_value(0);\n    std::cout << \"OFF\" << std::endl;\n    std::this_thread::sleep_for(500ms);\n```\n\n该程序的输出与通过 GPIO 配方连接的*控制设备的程序输出相同。 代码可能看起来更复杂，但新的 API 更通用，可以在任何 Linux 主板上运行，而不仅仅是 Raspberry PI。*\n\n# 还有更多的..。\n\n有关`libgpiod`和 GPIO 接口的更多信息，请参见[https://github.com/brgl/libgpiod](https://github.com/brgl/libgpiod)。\n\n# 控制 I2C 外部设备\n\n通过 GPIO 连接设备有一个缺点。 处理器可用于 GPIO 的管脚数量有限且相对较少。 当您需要使用大量设备或提供复杂功能的设备时，您可以很容易地用完引脚。\n\n一种解决方案是使用其中一条标准串行总线连接外部设备。 其中之一是**内部集成电路**(**I2C**)。 这是广泛用于连接各种低速设备，因为它简单，因为一个设备可以只用主机控制器上的两条线连接。\n\n该总线在硬件和软件层面都得到了很好的支持。 通过使用 I2C 外设，开发人员可以从用户空间应用控制它们，而无需编写复杂的设备驱动程序。\n\n在本食谱中，我们将学习如何在覆盆子 PI 板上使用 I2C 设备。 我们将使用流行而廉价的液晶显示器。 它有 16 个针脚，这使得它很难直接连接到树莓板。 然而，使用 I2C 背包时，它只需要四根线就可以连接。\n\n# 怎么做……\n\n我们将创建一个应用，在连接到 Raspberry PI 板的 1602 LCD 显示屏上显示文本：\n\n1.  在您的`~/test`工作目录中，创建一个名为`i2c`的子目录。\n2.  使用您喜欢的文本编辑器在`i2c`子目录中创建一个`i2c.cpp`文件。\n3.  将以下`include`指令和常量定义放入`i2c.cpp`文件：\n\n```cpp\n#include <thread>\n#include <system_error>\n\n#include <unistd.h>\n#include <fcntl.h>\n#include <errno.h>\n#include <sys/ioctl.h>\n#include <linux/i2c-dev.h>\n\nusing namespace std::literals::chrono_literals;\n\nenum class Function : uint8_t {\n  clear = 0x01,\n  home = 0x02,\n  entry_mode_set = 0x04,\n  display_control = 0x08,\n  cursor_shift = 0x10,\n  fn_set = 0x20,\n  set_ddram_addr = 0x80\n};\n\nconstexpr int En = 0b00000100;\nconstexpr int Rs = 0b00000001;\n\nconstexpr int kDisplayOn = 0x04;\nconstexpr int kEntryLeft = 0x02;\nconstexpr int kTwoLine = 0x08;\nconstexpr int kBacklightOn = 0x08;\n```\n\n4.  现在，我们定义了一个新类`Lcd`，它封装了显示控制逻辑。 我们从数据字段和`public`方法开始：\n\n```cpp\nclass Lcd {\n  int fd;\n\n  public:\n    Lcd(const char* device, int address) {\n      fd = open(device, O_RDWR);\n      if (fd < 0) {\n        throw std::system_error(errno,\n            std::system_category(),\n            \"Failed to open RTC device\");\n      }\n      if (ioctl(fd, I2C_SLAVE, address) < 0) {\n        close(fd);\n        throw std::system_error(errno,\n            std::system_category(),\n            \"Failed to aquire bus address\");\n      }\n      Init();\n    }\n\n    ~Lcd() {\n      close(fd);\n    }\n\n    void Clear() {\n      Call(Function::clear);\n      std::this_thread::sleep_for(2000us);\n    }\n\n    void Display(const std::string& text,\n                 bool second=false) {\n      Call(Function::set_ddram_addr, second ? 0x40 : 0);\n      for(char c : text) {\n        Write(c, Rs);\n      }\n    }\n```\n\n5.  它们之后是`private`方法。 首先使用低级帮助器方法：\n\n```cpp\nprivate:\n\n    void SendToI2C(uint8_t byte) {\n if (write(fd, &byte, 1) != 1) {\n throw std::system_error(errno,\n std::system_category(),\n \"Write to i2c device failed\");\n }\n    }\n\n    void SendToLcd(uint8_t value) {\n      value |= kBacklightOn;\n      SendToI2C(value);\n      SendToI2C(value | En);\n      std::this_thread::sleep_for(1us);\n      SendToI2C(value & ~En);\n      std::this_thread::sleep_for(50us);\n    }\n\n    void Write(uint8_t value, uint8_t mode=0) {\n      SendToLcd((value & 0xF0) | mode);\n      SendToLcd((value << 4) | mode);\n    }\n```\n\n6.  一旦定义了帮助器函数，我们将添加更高级别的方法：\n\n```cpp\n    void Init() {\n      // Switch to 4-bit mode\n      for (int i = 0; i < 3; i++) {\n        SendToLcd(0x30);\n        std::this_thread::sleep_for(4500us);\n      }\n      SendToLcd(0x20);\n\n      // Set display to two-line, 4 bit, 5x8 character mode\n      Call(Function::fn_set, kTwoLine);\n      Call(Function::display_control, kDisplayOn);\n      Clear();\n      Call(Function::entry_mode_set, kEntryLeft);\n      Home();\n    }\n\n    void Call(Function function, uint8_t value=0) {\n      Write((uint8_t)function | value);\n    }\n\n    void Home() {\n      Call(Function::home);\n      std::this_thread::sleep_for(2000us);\n    }\n};\n```\n\n7.  添加使用`Lcd`类的`main`函数：\n\n```cpp\nint main (int argc, char* argv[])\n{\n  Lcd lcd(\"/dev/i2c-1\", 0x27);\n  if (argc > 1) {\n    lcd.Display(argv[1]);\n    if (argc > 2) {\n      lcd.Display(argv[2], true);\n    }\n  }\n  return 0 ;\n}\n```\n\n8.  创建包含我们程序的构建规则的`CMakeLists.txt`文件：\n\n```cpp\ncmake_minimum_required(VERSION 3.5.1)\nproject(i2c)\nadd_executable(i2c i2c.cpp)\n```\n\n9.  根据下表将 1602LCD 显示屏的`i2c`背包上的针脚连接到 Raspberry PI 板上的针脚上：\n\n| **覆盆子皮针名** | **物理引脚编号** | **1602 I2C 引脚** |\n| GND | 6. | GND |\n| + 5v | 2 个 | VSS |\n| SDA.1 | 3. | SDA |\n| SCL.1 | 5. | SCL |\n\n10.  设置到您的 Raspberry PI 板的 SSH 连接。 按照[https://www.raspberrypi.org/documentation/remote-access/ssh/](https://www.raspberrypi.org/documentation/remote-access/ssh/)的*覆盆子 PI 文档*部分的说明进行操作。\n11.  登录 Raspberry 板卡，运行`raspi-config`工具启用`i2c`：\n\n```cpp\nsudo raspi-config\n```\n\n12.  在菜单中，选择接口选项|I2C|是。\n13.  重新启动主板以激活新设置。\n14.  通过 SSH 将`i2c`文件夹的内容复制到 Raspberry PI 板。\n15.  通过 SSH 登录主板，然后构建并运行应用：\n\n```cpp\n$ cd i2c && cmake . && make && ./i2c Hello, world!\n```\n\n您的应用应该会运行，并且您可以看到 LED 闪烁。\n\n# 它是如何运作的..。\n\n在本方案中，我们的外部设备-LCD 屏幕-通过 I2C 总线连接到电路板。 它是串行接口的一种形式，因此连接只需要四条物理线路。 然而，LCD 屏幕可以做的远不止一个简单的 LED。 这意味着用于控制它的通信协议也更加复杂。\n\n我们将只使用 1602 液晶屏提供的一小部分功能。 通信逻辑松散地基于 Arduino 的`LiquidCrystal_I2C`库，适用于 Raspberry Pi。\n\n我们定义了一个`Lcd`类，它在其私有方法中隐藏了 I2C 通信的所有复杂性和 1602 控制协议的细节。 除了构造函数和析构函数外，它只公开了两个公共方法：`Clear`和`Display`。\n\n在 Linux 中，我们通过设备文件与 I2C 设备通信。 要开始使用设备，我们需要使用常规的 OPEN 调用打开与 I2C 控制器对应的设备文件：\n\n```cpp\nfd = open(device, O_RDWR);\n```\n\n可能有多个设备连接到同一总线。 我们需要选择要与之通信的设备。 我们使用`ioctl`调用来执行此操作：\n\n```cpp\nif (ioctl(fd, I2C_SLAVE, address) < 0) {\n```\n\n此时，I2C 通信已配置完毕，我们可以通过将数据写入打开文件描述符来发出 I2C 命令。 然而，这些命令对于每个外部设备都是特定的。 因此，在通用 I2C 初始化之后，我们需要继续进行 LCD 初始化。\n\n我们将所有特定于 LCD 的初始化放入`Init`私有函数中。 它配置操作模式、行数和显示字符的大小。 为此，我们定义了帮助器方法、数据类型和常量。\n\n基本的辅助函数是`SendToI2C`。 这是一个简单的方法，将一个字节的数据写入为 I2C 通信配置的文件描述符中，并在出现错误时抛出异常：\n\n```cpp\n      if (write(fd, &byte, 1) != 1) {\n        throw std::system_error(errno,\n            std::system_category(),\n            \"Write to i2c device failed\");\n      }\n```\n\n在`SendToI2C`之上，我们定义了另一个帮助器方法`SendToLcd`。 它向 I2C 发送一个字节序列，形成 LCD 控制器可以解释的命令。 这包括设置不同的标志并处理数据块之间所需的延迟：\n\n```cpp\n      SendToI2C(value);\n      SendToI2C(value | En);\n      std::this_thread::sleep_for(1us);\n      SendToI2C(value & ~En);\n      std::this_thread::sleep_for(50us);\n```\n\nLCD 在 4 位模式下工作，这意味着发送到显示器的每个字节都需要两个命令。 我们定义了`Write`方法来为我们做这件事：\n\n```cpp\n      SendToLcd((value & 0xF0) | mode);\n      SendToLcd((value << 4) | mode);\n```\n\n最后，我们定义了设备支持的所有可能的命令，并将它们放入`Function`枚举类中。 可以使用`Call`帮助器函数以类型安全的方式调用函数：\n\n```cpp\n    void Call(Function function, uint8_t value=0) {\n      Write((uint8_t)function | value);\n    }\n```\n\n最后，我们使用这些助手函数定义公共方法来清除屏幕并显示字符串。\n\n由于通信协议的所有复杂性都封装在`Lcd`类中，因此我们的`main`函数相对简单。\n\n它创建类的一个实例，传入我们要使用的设备文件名和设备地址。 默认情况下，带 I2C 背包的 1620 LCD 具有`0x27`地址：\n\n```cpp\n  Lcd lcd(\"/dev/i2c-1\", 0x27);\n```\n\n`Lcd`类的构造函数执行所有初始化，实例一创建，我们就可以调用`Display`函数。 我们使用用户通过命令行参数传递的数据，而不是硬编码要显示的字符串。 第一个参数显示在第一行。 如果提供了第二个参数，它也会显示在显示屏的第二行中：\n\n```cpp\n    lcd.Display(argv[1]);\n    if (argc > 2) {\n      lcd.Display(argv[2], true);\n    }\n```\n\n我们的程序已经准备好了，我们可以把它复制到 Raspberry Pi 板上，然后在那里构建它。 但在运行之前，我们需要将显示器连线到电路板并启用 I2C 支持。\n\n我们使用`raspi-config`工具使能 I2C。 我们只需执行一次，但除非 I2C 之前未启用，否则需要重新启动：\n\n![](img/6ea36835-ced3-40ef-a8c8-70b0c08c2f71.png)\n\n最后，我们可以运行我们的应用了。 它将在 LCD 显示屏上显示以下输出：\n\n![](img/91251e94-ad99-47c6-b5c3-e5866ca97b1e.jpg)\n\n现在，我们知道如何从 Linux 用户空间程序控制通过 I2C 总线连接的设备。\n\n# 还有更多的..。\n\n有关使用 I2C 器件的更多信息，请参见*与 I2C 器件接口*页面，该页面位于[https://elinux.org/Interfacing_with_I2C_Devices](https://elinux.org/Interfacing_with_I2C_Devices.)。"
  },
  {
    "path": "docs/emb-prog-mod-cpp-cb/10.md",
    "content": "# 十、降低功耗\n\n嵌入式系统有很多需要电池供电的应用。 从小型**物联网**(**物联网**)从传感器收集数据并将其推送到云中进行处理的设备，到自动车辆和机器人-这些系统应该尽可能高能效，以便它们可以在没有稳定的外部电源的情况下长时间运行。\n\n能效是指对系统所有部件(从外部设备到存储器和处理器)的功耗进行智能控制。 功率控制的效率在很大程度上取决于硬件组件的选择和系统设计。 如果处理器不支持动态电压控制，或者外部设备在空闲时不能进入省电模式，那么在软件方面就无能为力了。 然而，如果硬件组件实现标准规范，例如**高级配置和电源接口**(**ACPI**)，那么电源管理的许多负担可以卸载到操作系统内核。\n\n在本章中，我们将探讨现代硬件平台的不同省电模式以及如何利用它们。 我们将学习如何管理外部设备的电源状态，并通过编写更高效的软件来降低处理器的功耗。\n\n我们将介绍以下主题：\n\n*   Linux 下的节电模式探索\n*   使用**RTC**(**实时时钟**的缩写)唤醒\n*   控制 USB 设备的自动挂起\n*   配置 CPU 频率\n*   使用事件等待\n*   使用 PowerTOP 分析功耗\n\n本章中的配方将帮助您有效地利用现代操作系统的节能功能，并编写针对电池供电设备进行优化的代码。\n\n# 技术要求\n\n要运行本章中的代码示例，您需要有 Raspberry PI Box 修订版 3 或更高版本。\n\n# Linux 下的节电模式探索\n\n当系统处于空闲状态并且没有工作要做时，可以将其置于休眠状态以节省电能。 与人类睡眠类似，在被外部事件(例如闹钟)唤醒之前，它什么也做不了。\n\nLinux 支持多种睡眠模式。 休眠模式的选择和可节省的电量取决于硬件支持以及进入该模式和从该模式唤醒所需的时间。\n\n支持的模式如下：\n\n*   **挂起到空闲**(**S2I**)：这是一种完全可以在软件中实现的轻度休眠模式，不需要硬件的任何支持。 设备被置于低功率模式，计时被挂起，以使处理器在节能空闲状态中花费更多时间。 系统被来自任何外部设备的中断唤醒。\n*   **STANDBY**：这类似于 S2I，但是通过使所有非引导 CPU 离线来提供更多的节能。 某些设备的中断可能会唤醒系统。\n*   **挂起至 RAM**(**STR**或**S3**)：系统的所有组件(内存除外)，包括 CPU，都进入低功耗模式。 系统状态一直保持在内存中，直到被来自有限设备集的中断唤醒。 此模式需要硬件支持。\n*   **休眠**或**挂起到磁盘**：这提供了最大的节能，因为所有系统组件都可以关闭电源。 进入此状态时，将拍摄内存快照并将其写入永久存储(磁盘或闪存)。 在那之后，系统就可以关闭了。 作为引导过程的一部分，在唤醒时，将恢复保存的快照，系统将恢复其工作。\n\n在本指南中，我们将学习如何查询特定系统支持的睡眠模式，以及如何切换到其中一种模式。\n\n# 怎么做……\n\n在本食谱中，我们将使用简单的 bash 命令访问在**QEMU**(**快速仿真器**的缩写)中运行的 Linux 系统支持的休眠模式。\n\n1.  按照[第 3 章](03.html)、*使用不同架构*中的说明运行 Raspberry Pi QEMU。\n2.  以用户`pi`身份登录，使用密码`raspberry`。\n3.  运行`sudo`以获得超级用户访问权限：\n\n```cpp\n$ sudo bash\n#\n```\n\n4.  要获取支持的休眠模式列表，请运行以下命令：\n\n```cpp\n # cat /sys/power/state\n```\n\n5.  现在切换到支持的模式之一：\n\n```cpp\n # echo freeze > /sys/power/state\n```\n\n6.  系统进入睡眠状态，但我们没有指示它如何唤醒。 现在关闭 QEMU 窗口。\n\n# 它是如何运作的..。\n\n电源管理是 Linux 内核的一部分；这就是为什么我们不能使用 Docker 容器来处理它。 Docker 虚拟化是轻量级的，并且使用主机操作系统的内核。\n\n我们也不能使用真正的 Raspberry PI 板，因为由于硬件限制，它根本不提供任何睡眠模式。 然而，QEMU 提供了完全虚拟化，包括我们用来模拟 Raspberry PI 的内核中的电源管理。\n\nLinux 通过 sysfs 接口提供对其电源管理功能的访问。 应用可以读写`/sys/power`目录中的文本文件。 Root 用户对电源管理功能的访问是受限的；这就是我们需要在登录到系统后获取 root shell 的原因：\n\n```cpp\n$ sudo bash\n```\n\n现在我们可以获得支持的睡眠模式列表。 为此，我们读取`/sys/power/state`文件：\n\n```cpp\n$ cat /sys/power/state\n```\n\n该文件由一行文本组成。 每个单词代表一种受支持的休眠模式，模式之间用空格分隔。 我们可以看到，QEMU 内核支持两种模式：`freeze`和`mem`：\n\n![](img/e12ba0b1-2558-41d6-83c6-8ad7026751c3.png)\n\n冻结表示我们在上一节中讨论的 S2I 状态。 `mem`的含义由`/sys/power/mem_sleep`文件的内容定义。 在我们的系统中，它只包含`[s2idle]`，表示与`freeze`相同的 S2I 状态。\n\n让我们将模拟器切换到`freeze`模式。 我们将单词`freeze`写入`/sys/power/state`，QEMU 窗口立即变黑并冻结：\n\n![](img/a3f7043f-286b-49d6-acc7-a05c553aa1dd.png)\n\n我们能够让模拟的 Linux 系统进入睡眠状态，但无法唤醒它--它无法理解中断的来源。 我们了解了不同的休眠模式以及使用它们的内核 API。 根据您的嵌入式系统的要求，您可以使用这些模式来降低功耗。\n\n# 还有更多的..。\n\n有关睡眠模式的更多信息，请参见位于[https://www.kernel.org/doc/html/v4.19/admin-guide/pm/sleep-states.html](https://www.kernel.org/doc/html/v4.19/admin-guide/pm/sleep-states.html.)的*Linux 内核指南*的相应部分。\n\n# 使用 RTC 唤醒\n\n在前面的配方中，我们可以让我们的 QEMU 系统进入睡眠状态，但无法唤醒它。 我们需要一种设备，它可以在系统的大部分内部组件断电时向系统发送中断。\n\n**RTC**(**实时时钟)**就是这样的设备之一。 它的功能之一是在系统关闭时保持内部时钟运行，为此，它有自己的电池。 RTC 的耗电量类似于电子表；它使用相同的 3V 电池，可以使用它的电源工作数年。\n\nRTC 可以作为闹钟工作，在给定时间向 CPU 发送中断。 这使其成为按时唤醒系统的理想设备。\n\n在本食谱中，我们将学习如何使用内置的 RTC 在特定时间唤醒 Linux 系统。\n\n# 怎么做……\n\n在此配方中，我们将提前 1 分钟设置唤醒时间，并使系统进入睡眠状态：\n\n1.  登录任何有 RTC 时钟的 Linux 系统-任何 Linux 笔记本电脑都可以工作。 不幸的是，Raspberry Pi 没有板载 RTC，在没有额外硬件的情况下无法唤醒。\n\n2.  使用`sudo`获取 root 权限：\n\n```cpp\n$ sudo bash\n#\n```\n\n3.  指示 RTC 在`1`分钟内唤醒系统：\n\n```cpp\n# date '+%s' -d '+1 minute' > /sys/class/rtc/rtc0/wakealarm\n```\n\n4.  使系统进入睡眠状态：\n\n```cpp\n# echo freeze > /sys/power/state\n```\n\n5.  请稍等片刻。 您的系统将会苏醒。\n\n# 它是如何运作的..。\n\n与 Linux 内核公开的许多其他函数一样，可以通过 sysfs 接口访问 RTC。 要设置将向系统发送唤醒中断的报警，我们需要向`/sys/class/rtc/rtc0/wakealarm`文件写入**POSIX**(**可移植操作系统接口**的缩写)时间戳。\n\nPOSIX 时间戳(我们将在[第 11 章](11.html)，*时间点和间隔*中详细讨论)定义为自大纪元(即 1970 年 1 月 1 日 00：00)以来经过的秒数。\n\n虽然我们可以使用`time`函数编写程序来读取当前时间戳，添加 60，并将结果写入`wakealarm`文件，但是我们可以使用 Unix shell 和`date`命令(在任何现代 Unix 系统上都可以使用)在一行中完成这一任务。\n\nDate 实用程序不仅可以使用不同的格式格式化当前时间，还可以解释不同格式的日期和时间。\n\n我们指示`date`解释时间字符串`+1 minute`，并使用格式化模式`%s`将其输出为 POSIX 时间戳。 我们将其标准输出重定向到`wakealarm`文件，有效地将其传递给 RTC 驱动程序：\n\n```cpp\ndate '+%s' -d '+1 minute' > /sys/class/rtc/rtc0/wakealarm\n```\n\n现在，知道警报将在 60 秒后响起，我们可以让系统进入睡眠状态。 与前面的配方一样，我们将所需的休眠模式写入`/sys/power/state`文件：\n\n```cpp\n# echo freeze > /sys/power/state\n```\n\n系统进入休眠状态。 您会注意到屏幕关闭了。 如果使用**Secure Shell**(**SSH**)连接到 Linux 计算机，命令行将冻结。 然而，一分钟后它就会苏醒，屏幕打开，终端再次响应。\n\n这项技术对于定期、不频繁地从传感器收集数据(例如每小时或每天)的任务非常有效。 系统大部分时间都处于断电状态，醒来时只是收集数据并存储或将其发送到云，然后再次进入休眠状态。\n\n# 还有更多的..。\n\n设置 RTC 报警的另一种方法是使用`rtcwake`实用程序。\n\n# 控制 USB 设备的自动挂起\n\n关闭外部设备是最有效的省电方法之一。 然而，当设备可以安全关闭时，并不总是很容易理解。 网卡或存储卡等外部设备可以执行内部数据处理；否则，在任意点对设备进行缓存和断电可能会导致数据丢失。\n\n为了缓解此问题，许多通过 USB 连接的外部设备可以在主机请求时切换到低功耗模式。 这样，他们就可以在进入挂起状态之前执行所有必要的步骤来安全地处理内部数据。\n\n由于 Linux 仅通过其 API 提供对外部设备的访问，因此它知道设备何时被应用和内核服务使用。 如果设备在一段时间内没有使用，Linux 内核中的电源管理系统可以指示设备自动进入省电模式-不需要来自用户空间应用的显式请求。 此功能称为**自动暂停**。 然而，内核允许应用控制设备的空闲时间，之后自动暂停生效。\n\n在本食谱中，我们将学习如何启用自动暂停和修改特定 USB 设备的自动暂停间隔。\n\n# 怎么做……\n\n我们将为连接到您的 Linux 设备的 USB 设备启用自动暂停并修改其自动暂停时间：\n\n1.  登录您的 Linux 机器(Raspberry PI、Ubuntu 和 Docker 容器不起作用)。\n2.  切换到 root 帐户：\n\n```cpp\n$ sudo bash\n#\n```\n\n3.  获取已连接的所有 USB 设备的当前`autosuspend`状态：\n\n```cpp\n# for f in /sys/bus/usb/devices/*/power/control; do echo \"$f\"; cat $f; done\n```\n\n4.  为其中一个设备启用`autosuspend`：\n\n```cpp\n# echo auto > /sys/bus/usb/devices/1-1.2/power/control\n```\n\n5.  读取设备的`autosuspend`间隔：\n\n```cpp\n# cat /sys/bus/usb/devices/1-1.2/power/autosuspend_delay_ms \n```\n\n6.  修改`autosuspend`间隔：\n\n```cpp\n# echo 5000 > /sys/bus/usb/devices/1-1.2/power/autosuspend_delay_ms \n```\n\n7.  检查设备的当前电源模式：\n\n```cpp\n# cat /sys/bus/usb/devices/1-1.2/power/runtime_status\n```\n\n可以使用标准文件 API 在 C++ 中编写相同的操作。\n\n# 它是如何运作的..。\n\nLinux 通过 sysfs 文件系统公开其电源管理 API，这使得使用标准文件读写操作读取任何设备的当前状态和修改其设置成为可能。 因此，我们可以使用任何支持基本文件操作的编程语言来控制 Linux 中的外部设备。\n\n为了简化我们的示例，我们将使用 Unix shell，但在必要时可以用 C++ 编写完全相同的逻辑。\n\n首先，我们检查所有连接的 USB 设备的`autosuspend`设置。 在 Linux 中，每个 USB 设备的参数都公开为`/sysfs/bus/usb/devices/`文件夹下的一个目录。 每个设备目录又有一组表示设备参数的文件。 所有与电源管理相关的参数都分组在`power`子目录中。\n\n要读取`autosuspend`的状态，我们需要读取设备`power`目录中的`control`文件。 使用 Unix 外壳通配符替换，我们可以读取所有 USB 设备的以下文件：\n\n```cpp\n# for f in /sys/bus/usb/devices/*/power/control; do echo \"$f\"; cat $f; done\n```\n\n对于与通配符匹配的每个目录，我们显示控制文件的完整路径及其内容。 结果取决于连接的设备，可能如下所示：\n\n![](img/ad39f854-2adc-4c82-93d8-22a61a3718a6.png)\n\n报告的状态可以是自动暂停或`on`。 如果状态报告为 autosuspend，则启用自动电源管理；否则，设备将始终处于打开状态。\n\n在我们的示例中，设备`usb1`、`1-1.1`和`1-1.2`处于打开状态。 让我们修改`1-1.2`的配置以使用 autosuspend。 为此，我们只需将字符串`_auto_`写入相应的`_control_`文件。\n\n```cpp\n# echo auto > /sys/bus/usb/devices/1-1.2/power/control\n```\n\n在所有设备上再次运行读取循环显示`1-1.2`设备现在处于`autosuspend`模式：\n\n![](img/a67c2bca-1a51-47ae-a018-07121f050716.png)\n\n什么时候停运？ 我们可以从`power`子目录中的`autosuspend_delay_ms`文件中读取：\n\n```cpp\n# cat /sys/bus/usb/devices/1-1.2/power/autosuspend_delay_ms \n```\n\n显示设备将在空闲`2000`毫秒后挂起：\n\n![](img/eca24651-a7e7-4029-9c0c-a9e827d52322.png)\n\n让我们将其更改为`5`秒。 我们在`autosuspend_delay_ms`文件中写入`5000`：\n\n```cpp\n# echo 5000 > /sys/bus/usb/devices/1-1.2/power/autosuspend_delay_ms \n```\n\n再次阅读它会显示新值被接受：\n\n![](img/7b6ee3c8-c017-4d68-9df7-b5343e3bf17d.png)\n\n现在让我们检查一下设备的当前电源状态。 我们可以从`runtime_status`文件中读取：\n\n```cpp\n# cat /sys/bus/usb/devices/1-1.2/power/runtime_status\n```\n\n状态报告为`active`：\n\n![](img/71e3495c-e054-41b5-bdf4-1265560fe78f.png)\n\nPlease note that the kernel does not control the power state of devices directly; it only requests them to change the state. Even if a device is requested to switch into suspend mode, it may refuse to do it for various reasons—for example, it may not support the power-saving mode at all.\n\n通过 sysfs 界面访问任何设备的电源管理设置是调整运行 Linux 操作系统的嵌入式系统功耗的有效方法。\n\n# 还有更多的..。\n\n没有直接的方法可以立即关闭 USB 设备；但是，在许多情况下，可以通过将`0`写入`autosuspend_delay_ms`文件来实现。 内核将零自动暂停间隔解释为对设备的立即挂起请求。\n\n关于 linux 中 usb 电源管理的更多细节可以在 linux 内核文档的相应部分中找到，可以在[https://www.kernel.org/doc/html/v4.13/driver-api/usb/power-management.html](https://www.kernel.org/doc/html/v4.13/driver-api/usb/power-management.html)上找到。\n\n# 配置 CPU 频率\n\nCPU 频率是决定系统性能和功耗的重要参数。 频率越高，CPU 每秒执行的指令就越多。 但这是有代价的。 更高的频率意味着更高的功耗，这反过来又意味着需要散失更多的热量，以避免处理器过热。\n\n现代处理器能够根据负载使用不同的工作频率。 对于计算密集型任务，它们使用最高频率来实现最高性能，但当系统大部分空闲时，它们会切换到较低频率以降低功耗和热影响。\n\n正确的频率选择由操作系统管理。 在本食谱中，我们将学习如何在 Linux 中设置 CPU 频率范围和选择频率调节器，以根据您的需要微调 CPU 频率。\n\n# 怎么做……\n\n我们将使用简单的 shell 命令来调整 Raspberry PI 盒上的 CPU 频率参数：\n\n1.  登录到 Raspberry PI 或其他非虚拟化 Linux 系统。\n2.  切换到 root 帐户：\n\n```cpp\n$ sudo bash\n#\n```\n\n3.  获取系统中所有可用 CPU 核心的当前频率：\n\n```cpp\n# cat /sys/devices/system/cpu/*/cpufreq/scaling_cur_freq\n```\n\n4.  获取 CPU 支持的所有频率：\n\n```cpp\n# cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_available_frequencies\n```\n\n5.  获取可用的 CPU 频率调节器：\n\n```cpp\n# cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_available_governors\n```\n\n6.  现在让我们检查一下当前使用的是哪个频率调节器：\n\n```cpp\n# cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor \n```\n\n7.  将 CPU 的最低频率调整为支持的最高频率：\n\n```cpp\n# echo 1200000 > /sys/devices/system/cpu/cpu0/cpufreq/scaling_min_freq\n```\n\n8.  再次显示当前频率以了解效果：\n\n```cpp\n# cat /sys/devices/system/cpu/*/cpufreq/scaling_cur_freq\n```\n\n9.  将最低频率调整为支持的最低频率：\n\n```cpp\n# echo 600000 > /sys/devices/system/cpu/cpu0/cpufreq/scaling_min_fre\n```\n\n10.  现在，让我们检查一下 CPU 频率如何取决于正在使用的调控器。 选择`performance`调速器，获取当前频率：\n\n```cpp\n# echo performance > /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor\n# cat /sys/devices/system/cpu/*/cpufreq/scaling_cur_freq\n```\n\n11.  选择`powersave`调速器并观察结果：\n\n```cpp\n# echo powersave > /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor\n# cat /sys/devices/system/cpu/*/cpufreq/scaling_cur_freq\n```\n\n您可以使用常规文件 API 在 C++ 中实现相同的逻辑。\n\n# 它是如何运作的..。\n\n与 USB 电源管理类似，CPU 频率管理系统 API 通过 sysfs 公开。 我们可以将其参数作为常规文本文件进行读取和修改。\n\n我们可以在`/sys/devices/system/cpu/`目录下找到与 CPU 核心相关的所有设置。 配置参数在以每个代码索引命名的子目录(如`cpu1`、`cpu2`等)中按 CPU 核心进行分组。\n\n我们感兴趣的是与 CPU 频率管理相关的几个参数，这些参数位于每个内核的`cpufreq`子目录中。 让我们读出所有可用内核的当前频率：\n\n```cpp\n# cat /sys/devices/system/cpu/*/cpufreq/scaling_cur_freq\n```\n\n我们可以看到，所有内核都有相同的频率，600 MHz(`cpufreq`子系统使用 kHz 作为频率测量单位)：\n\n![](img/6d9f305d-d3ca-47eb-8766-1b3fa5718836.png)\n\n接下来，我们计算出 CPU 支持的所有频率：\n\n```cpp\n# cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_available_frequencies\n```\n\nRaspberry PI 3 的 ARM 处理器仅支持两个频率，600 MHz 和 1.2 GHz：\n\n![](img/dd10cdcb-aff0-4f3d-9b17-7416aac38365.png)\n\n我们不能直接设置所需的频率。 Linux 通过所谓的**调控器**在内部管理 CPU 频率，并且只允许我们调整两个参数：\n\n*   可供调速器使用的频率范围\n*   调速器的类型\n\n虽然这看起来是一个限制，但这两个参数为实现相当复杂的策略提供了足够的灵活性。 让我们检查一下这两个参数的修改如何影响 CPU 频率。\n\n首先，让我们找出哪些调控器受支持，哪些调控器当前正在使用：\n\n![](img/52365c50-07d0-495b-942b-f45dc54dd619.png)\n\n当前调控器为`ondemand`*。* 它根据系统负载调整频率。 目前，Raspberry Pi 板相当空闲，因此它使用最低频率，600 MHz。 但是如果我们让最低频率等于最高频率呢？\n\n```cpp\n# echo 1200000 > /sys/devices/system/cpu/cpu0/cpufreq/scaling_min_freq\n```\n\n在我们更新了一个内核的`scaling_min_freq`参数后，所有内核的频率都被更改为最大值：\n\n![](img/ac1af6c9-2bce-4cfa-a881-8d99f9e5ebad.png)\n\n因为所有四个核心都属于同一个 CPU，所以我们不能单独更改它们的频率；更改一个核心的频率会影响所有核心。 然而，我们可以独立控制独立 CPU 的频率。\n\n现在我们将最低频率恢复到 600 MHz 并更改调速器。 我们选择了`performance`调控器，而不是调整频率的`ondemand`调控器，旨在无条件地提供最高性能：\n\n```cpp\necho performance > /sys/devices/system/cpu/cpu0/cpufreq/scaling_g;overnor\n```\n\n毫不奇怪，它将频率提高到了支持的最高频率：\n\n![](img/ac5ded8a-1be2-402d-9f5c-b475828d8ba9.png)\n\n另一方面，`powersave`调控器的目标是尽可能多地节省电能，因为它始终坚持支持的最低频率，而不管负载是什么：\n\n![](img/fc0f3d39-c055-4afe-b659-df0401954695.png)\n\n如您所见，通过调整频率范围和频率调节器，您可以根据系统的性质灵活地微调频率，并降低 CPU 的功耗。\n\n# 还有更多的..。\n\n除了`ondemand`、`performance`和`powersave`之外，还有其他调控器可以从用户空间应用中提供更灵活的 CPU 频率调优。 您可以在 Linux CPUFreq 的相应部分[https://www.kernel.org/doc/Documentation/cpu-freq/governors.txt](https://www.kernel.org/doc/Documentation/cpu-freq/governors.txt)中找到有关可用调控器及其属性的更多详细信息\n\n# 使用事件等待\n\n等待是软件开发中非常常见的模式。 应用必须等待用户输入或数据准备好进行处理。 嵌入式程序与外部设备通信，需要知道何时可以从设备读取数据，以及设备何时准备好接受数据。\n\n通常，开发人员使用轮询技术的变体来等待。 它们在循环中检查特定于设备的可用性标志，当设备将其设置为 true 时，它们继续读取或写入数据。\n\n虽然这种方法很容易实现，但从功耗的角度来看，它的效率很低。 当处理器不断忙于标志检查时，操作系统电源管理器不能将其置于更省电的模式。 基于负载，我们前面讨论的 Linux`ondemand`频率调控器甚至可以决定增加 CPU 频率，尽管这实际上是一种变相的等待。 此外，轮询请求可能会阻止目标设备或设备总线保持在省电模式，直到数据就绪。\n\n这就是为什么它应该依赖操作系统产生的中断和事件，而不是关注能效的轮询程序。\n\n在本食谱中，我们将学习如何使用操作系统事件来等待连接特定的 USB 设备。\n\n# 怎么做……\n\n我们将创建一个可以监视 USB 设备并等待特定设备出现的应用：\n\n1.  在您的工作`~/test`目录中，创建一个名为`udev`的子目录。\n2.  使用您喜欢的文本编辑器在`udev`子目录中创建`udev.cpp`文件。\n3.  将 Essential Includes 和`namespace`定义放入`udev.cpp`文件：\n\n```cpp\n#include <iostream>\n#include <functional>\n\n#include <libudev.h>\n#include <poll.h>\n\nnamespace usb {\n```\n\n4.  现在，让我们定义`Device`类：\n\n```cpp\nclass Device {\n  struct udev_device *dev{0};\n\n  public:\n    Device(struct udev_device* dev) : dev(dev) {\n    }\n\n    Device(const Device& other) : dev(other.dev) {\n      udev_device_ref(dev);\n    }\n\n    ~Device() {\n        udev_device_unref(dev);\n    }\n\n    std::string action() const { \n        return udev_device_get_action(dev);\n     }\n\n    std::string attr(const char* name) const {\n      const char* val = udev_device_get_sysattr_value(dev,\n             name);\n      return val ? val : \"\";\n    }\n};\n```\n\n5.  之后，添加`Monitor`类的定义：\n\n```cpp\nclass Monitor {\n  struct udev_monitor *mon;\n\n  public:\n    Monitor() {\n      struct udev* udev = udev_new();\n      mon = udev_monitor_new_from_netlink(udev, \"udev\");\n      udev_monitor_filter_add_match_subsystem_devtype(\n           mon, \"usb\", NULL);\n      udev_monitor_enable_receiving(mon);\n    }\n\n    Monitor(const Monitor& other) = delete;\n\n    ~Monitor() {\n      udev_monitor_unref(mon);\n    }\n\n    Device wait(std::function<bool(const Device&)> process) {\n      struct pollfd fds[1];\n      fds[0].events = POLLIN;\n      fds[0].fd = udev_monitor_get_fd(mon);\n\n      while (true) {\n          int ret = poll(fds, 1, -1);\n          if (ret < 0) {\n            throw std::system_error(errno, \n                std::system_category(),\n                \"Poll failed\");\n          }\n          if (ret) {\n            Device d(udev_monitor_receive_device(mon));\n            if (process(d)) {\n              return d;\n            };\n          }\n      }\n    }\n};\n};\n```\n\n6.  在`usb`名称空间中定义了`Device`和`Monitor`之后，添加一个简单的`main`函数来说明如何使用它们：\n\n```cpp\nint main() {\n  usb::Monitor mon;\n  usb::Device d = mon.wait([](auto& d) {\n    auto id = d.attr(\"idVendor\") + \":\" + \n              d.attr(\"idProduct\");\n    auto produce = d.attr(\"product\");\n    std::cout << \"Check [\" << id << \"] action: \" \n              << d.action() << std::endl;\n    return d.action() == \"bind\" && \n           id == \"8086:0808\";\n  });\n  std::cout << d.attr(\"product\")\n            << \" connected, uses up to \"\n            << d.attr(\"bMaxPower\") << std::endl;\n  return 0;\n}\n```\n\n7.  创建包含我们程序的构建规则的`CMakeLists.txt`文件：\n\n```cpp\ncmake_minimum_required(VERSION 3.5.1)\nproject(udev)\nadd_executable(usb udev.cpp)\ntarget_link_libraries(usb udev)\n```\n\n8.  使用`ssh`将`udev`目录复制到 Linux 机器上的主目录中。\n9.  登录到 Linux 机器，将目录更改为`udev`，然后使用`cmake`构建程序：\n\n```cpp\n$cd ~/udev; cmake. && make\n```\n\n现在您可以构建和运行应用了。\n\n# 它是如何运作的..。\n\n为了获得有关 USB 设备上事件的系统通知，我们使用了一个名为`libudev`的库。 它只提供一个纯 C 接口，所以我们创建了简单的 C++ 包装器来简化编码。\n\n对于我们的包装类，我们声明了名为`usb`的`namespace`：\n\n```cpp\nnamespace usb {\n```\n\n它包含两个类。 第一个类是`Device`，它为我们提供了一个指向名为`udev_device`的低级`libudev`对象的 C++ 接口。\n\n我们定义了一个从`udev_device`指针创建`Device`实例的构造函数和一个释放`udev_device`的析构函数。 在内部，`libudev`对其对象使用引用计数，因此我们的析构函数调用一个函数来减少`udev_device`的引用计数：\n\n```cpp\n    ~Device() {\n        udev_device_unref(dev);\n    }\n    Device(const Device& other) : dev(other.dev) {\n      udev_device_ref(dev);\n    }\n```\n\n这样，我们就可以复制`Device`个实例，而不会泄露内存或文件描述符。\n\n除了构造函数和析构函数外，`Device`类只有两个方法：`action`和`attr`。 `action`方法返回最新的 USB 设备操作：\n\n```cpp\n    std::string action() const { \n        return udev_device_get_action(dev);\n     }\n```\n\n`attr`方法返回与设备关联的任何 sysfs 属性：\n\n```cpp\n    std::string attr(const char* name) const {\n      const char* val = udev_device_get_sysattr_value(dev,\n             name);\n      return val ? val : \"\";\n    }\n```\n\n`Monitor`类也有一个构造函数和一个析构函数，但我们通过禁用复制构造函数使其不可复制：\n\n```cpp\n    Monitor(const Monitor& other) = delete;\n```\n\n构造函数使用静态变量初始化`libudev`实例，以确保它只初始化一次：\n\n```cpp\n      struct udev* udev = udev_new();\n```\n\n它还设置监控过滤器并启用监控：\n\n```cpp\n      udev_monitor_filter_add_match_subsystem_devtype(\n           mon, \"usb\", NULL);\n      udev_monitor_enable_receiving(mon);\n```\n\n`wait`方法包含最重要的监控逻辑。 它接受一个类似函数的`process`对象，该对象在每次检测到事件时被调用：\n\n```cpp\nDevice wait(std::function<bool(const Device&)> process) {\n```\n\n如果事件及其所源自的设备是我们需要的，则函数应返回`true`；否则，它返回`false`以指示`wait`应继续工作。\n\n在内部，`wait`函数创建用于将设备事件传递给程序的文件描述符：\n\n```cpp\n      fds[0].fd = udev_monitor_get_fd(mon);\n```\n\n然后它建立了监控环路。 尽管名为`poll`，但`poll`函数并不经常检查设备的状态；它等待指定文件描述符上的事件。 我们将`-1`作为超时传递，表示我们打算永远等待事件：\n\n```cpp\nint ret = poll(fds, 1, -1);\n```\n\n`poll`函数仅在出现错误或新的 USB 事件时返回。 我们通过抛出异常来处理错误情况：\n\n```cpp\n          if (ret < 0) {\n            throw std::system_error(errno, \n                std::system_category(),\n                \"Poll failed\");\n          }\n```\n\n对于每个事件，我们创建`Device`的一个新实例，并将其传递给`process`。 如果`process`返回`true`，我们退出等待循环，将`Device`的实例返回给调用方：\n\n```cpp\n            Device d(udev_monitor_receive_device(mon));\n            if (process(d)) {\n              return d;\n            };\n```\n\n让我们看看如何在我们的应用中使用这些类。 在`main`函数中，我们创建`Monitor`的一个实例并调用它的`wait`函数。 我们使用 lambda 函数来处理每个操作：\n\n```cpp\nusb::Device d = mon.wait([](auto& d) {\n```\n\n在 lambda 函数中，我们打印所有事件的信息：\n\n```cpp\n    std::cout << \"Check [\" << id << \"] action: \" \n              << d.action() << std::endl;\n```\n\n我们还检查特定操作和设备`id`：\n\n```cpp\n    return d.action() == \"bind\" && \n           id == \"8086:0808\";\n```\n\n找到后，我们会显示有关其功能和电源要求的信息：\n\n```cpp\n  std::cout << d.attr(\"product\")\n            << \" connected, uses up to \"\n            << d.attr(\"bMaxPower\") << std::endl;\n```\n\n最初运行此应用不会产生任何输出：\n\n![](img/981bab41-7f4d-4a76-9bd0-5e55b1811789.png)\n\n但是，一旦我们插入 USB 设备(在我的例子中是 USB 麦克风)，我们可以看到以下输出：\n\n![](img/d8c72d62-873a-42ca-9486-e57804844157.png)\n\n应用可以等待特定的 USB 设备，并在其连接后进行处理。 它依赖于操作系统提供的信息，在不进行繁忙循环的情况下完成此操作。 因此，当操作系统阻止`poll`调用时，应用大部分时间处于休眠状态。\n\n# 还有更多的..。\n\n`libudev`有许多 C++ 包装器。 您可以使用其中之一，也可以使用食谱中的代码创建自己的代码作为起点。\n\n# 使用 PowerTOP 分析功耗\n\n在运行多个用户空间和内核空间服务并同时控制多个外部设备的复杂操作系统(如 Linux)中，并不总是很容易找到可能导致过度耗电的组件。 即使发现效率低下，修复它也可能是困难的。\n\n解决方案之一是使用 POWER PROFILE 工具，如 PowerTOP。 它可以诊断 Linux 系统中的功耗问题，并允许用户调整系统参数以节省电力。\n\n在本食谱中，我们将学习如何在 Raspberry PI 系统上安装和使用 PowerTOP。\n\n# 怎么做……\n\n在本配方中，我们将在交互模式下运行 PowerTOP 并分析其输出：\n\n1.  以用户`pi`的身份使用密码`raspberry`登录到您的 Raspberry PI 系统。\n2.  运行`sudo`以获得超级用户访问权限：\n\n```cpp\n$ sudo bash\n#\n```\n\n3.  从存储库安装 PowerTOP：\n\n```cpp\n # apt-get install powertop\n```\n\n4.  停留在根 shell 中，运行 PowerTOP：\n\n```cpp\n # powertop\n```\n\nPowerTOP UI 将显示在您的终端中。 使用*Tab*键在其屏幕之间导航。\n\n# 它是如何运作的..。\n\nPowerTOP 是英特尔开发的一款工具，用于诊断 Linux 系统中的电源问题。 它是 Raspbian 发行版的一部分，可以使用`apt-get`命令进行安装：\n\n```cpp\n# apt-get install powertop\n```\n\n当我们在没有参数的情况下运行它时，它会以交互模式启动，并列出所有进程和内核任务，按照它们的功耗和它们生成的事件的频率排序。 正如我们在*Using Events for Waiting*配方中所讨论的，程序唤醒处理器的频率越高，它的能效就越低：\n\n![](img/8f7a54ae-1a79-4f1f-91ce-152fbfd006a0.png)\n\n使用*Tab*键，我们可以切换到其他报告模式。 例如，设备统计信息显示设备消耗的能量或 CPU 时间：\n\n![](img/2e968743-e8ab-4fe5-a55b-b05a3742586c.png)\n\n另一个有趣的标签是 Tunab。 PowerTOP 可以检查一些影响功耗的设置，并标记那些不是最优的设置：\n\n![](img/3140c865-d15a-48e7-80de-28bd1c5857a5.png)\n\n如您所见，其中两个 USB 设备被标记为`Bad`，因为它们不使用 autosuspend。 通过按*Enter*键，PowerTOP 启用自动暂停，显示可从脚本使用的命令行，使其成为永久性的。 启用 autosuspend 后，可调状态更改为`Good`：\n\n![](img/866eb512-32b4-4ea7-a5bd-5f42106a267c.png)\n\n可以调整许多系统参数以节省电力。 有时它们是显而易见的，比如在 USB 设备上使用 autosuspend。 有时它们不是这样的，例如在内核上使用超时，用于将文件缓存刷新到磁盘。 使用电源诊断和优化工具(如 PowerTOP)可帮助您调整系统以实现最高能效。\n\n# 还有更多的..。\n\n除了交互模式外，PowerTOP 还有其他模式可以帮助您优化功耗使用，例如校准、工作负载和自动调优。 有关 PowerTOP 特性、使用场景和结果解释的更多信息，请参阅位于[https://01.org/sites/default/files/page/powertop_users_guide_201412.pdf](https://01.org/sites/default/files/page/powertop_users_guide_201412.pdf)的*PowerTOP 用户指南**指南*。"
  },
  {
    "path": "docs/emb-prog-mod-cpp-cb/11.md",
    "content": "# 十一、时间点和间隔\n\n嵌入式应用处理物理世界中发生的事件和控制过程-这就是正确处理时间和延迟对它们至关重要的原因。 切换红绿灯；产生声音音调；来自多个传感器的数据同步-所有这些任务都依赖于适当的时间测量。\n\n普通 C 没有提供任何处理时间的标准函数。 预计应用开发人员将使用特定于目标操作系统(Windows、Linux 或 MacOS)的 Time API。 对于裸机嵌入式系统，开发人员必须基于特定于目标平台的低级计时器 API 创建自定义函数来处理时间。 因此，代码很难移植到其他平台。\n\n为了克服可移植性问题，C++(从 C++ 11 开始)定义了处理时间和时间间隔的数据类型和函数。 此 API 称为`std::chrono`库，可帮助开发人员在任何环境和任何目标平台上以统一的方式使用时间。\n\n在本章中，我们将学习如何在应用中使用时间戳、时间间隔和延迟。 我们将讨论一些与时间管理相关的常见陷阱，以及它们的适当解决方法。\n\n我们将介绍以下主题：\n\n*   探索 C++ 计时库\n*   测量时间间隔\n*   在延迟的情况下工作\n*   使用单调时钟\n*   使用**可移植操作系统接口**(**POSIX**)时间戳\n\n使用这些方法，您将能够编写在任何嵌入式平台上工作的时间处理的可移植代码。\n\n# 探索 C++ 计时库\n\n从 C++ 11 开始，C++ Chrono 库提供标准化的数据类型和函数来处理时钟、时间点和时间间隔。 在本食谱中，我们将探索 Chrono 库的基本功能，并学习如何处理时间点和时间间隔。\n\n我们还将学习如何使用 C++ 文字来表示更具可读性的时间间隔。\n\n# 怎么做……\n\n我们将创建一个简单的应用来创建三个时间点，并将它们相互比较。\n\n1.  在您的`~/test`工作目录中，创建一个名为`chrono`的子目录。\n2.  使用您喜欢的文本编辑器在`chrono`子目录中创建`chrono.cpp`文件。\n3.  将以下代码片段放入文件中：\n\n```cpp\n#include <iostream>\n#include <chrono>\n\nusing namespace std::chrono_literals;\n\nint main() {\n  auto a = std::chrono::system_clock::now();\n  auto b = a + 1s;\n  auto c = a + 200ms;\n\n  std::cout << \"a < b ? \" << (a < b ? \"yes\" : \"no\") << std::endl;\n  std::cout << \"a < c ? \" << (a < c ? \"yes\" : \"no\") << std::endl;\n  std::cout << \"b < c ? \" << (b < c ? \"yes\" : \"no\") << std::endl;\n\n  return 0;\n}\n```\n\n4.  创建包含我们程序的构建规则的`CMakeLists.txt`文件：\n\n```cpp\ncmake_minimum_required(VERSION 3.5.1)\nproject(chrono)\nadd_executable(chrono chrono.cpp)\n\nset(CMAKE_SYSTEM_NAME Linux)\nset(CMAKE_SYSTEM_PROCESSOR arm)\n\nSET(CMAKE_CXX_FLAGS \"--std=c++ 14\")\nset(CMAKE_CXX_COMPILER /usr/bin/arm-linux-gnueabi-g++)\n```\n\n现在您可以构建和运行应用了。\n\n# 它是如何运作的..。\n\n我们的应用创建了三个不同的时间点。 第一个是使用系统时钟的`now`函数创建的：\n\n```cpp\nauto a = std::chrono::system_clock::now();\n```\n\n通过添加`1`秒和`200`毫秒的固定时间间隔，从第一个派生出另外两个：\n\n```cpp\nauto b = a + 1s;\nauto c = a + 200ms;\n```\n\n请注意我们是如何指定数值旁边的时间单位的。 我们使用了一种名为 C++ 文字的功能。 Chrono 库为基本时间单位定义这样的文字。 要使用这些定义，我们添加了以下内容：\n\n```cpp\nusing namespace std::chrono_literals;\n```\n\n这是在我们的`main`函数之前添加的。\n\n接下来，我们将这些时间点相互比较：\n\n```cpp\nstd::cout << \"a < b ? \" << (a < b ? \"yes\" : \"no\") << std::endl;\nstd::cout << \"a < c ? \" << (a < c ? \"yes\" : \"no\") << std::endl;\nstd::cout << \"b < c ? \" << (b < c ? \"yes\" : \"no\") << std::endl;\n```\n\n当我们运行应用时，我们看到以下输出：\n\n![](img/00856d97-097c-4ff9-98ef-4ed42bfda18c.png)\n\n正如预期的那样，时间点`a`早于`b`和`c`，其中时间点`c`(即`a`+200 毫秒)早于`b`(`a`+1 秒)。 字符串有助于编写更具可读性的代码，C++ Chrono 提供了一组丰富的函数来处理时间。 我们将在接下来的食谱中学习如何使用它们。\n\n# 还有更多的..。\n\n有关计时库中定义的所有数据类型、模板和函数的信息，请参阅位于[https://en.cppreference.com/w/cpp/chrono](https://en.cppreference.com/w/cpp/chrono)的计时参考\n\n# 测量时间间隔\n\n每个与外部硬件交互或响应外部事件的嵌入式应用都必须处理超时和反应时间。 要正确做到这一点，开发人员需要能够以足够的精度测量时间间隔。\n\nC++ Chrono 库提供了一个`std::chrono::duration`模板化的类，用于处理任意跨度和精度的持续时间。 在本食谱中，我们将学习如何使用该类测量两个时间戳之间的时间间隔，并对照参考持续时间进行检查。\n\n# 怎么做……\n\n我们的应用将测量简单控制台输出的持续时间，并将其与循环中以前的值进行比较。\n\n1.  在您的`~/test`工作目录中，创建一个名为`intervals`的子目录。\n2.  使用您喜欢的文本编辑器在`intervals`子目录中创建一个`intervals.cpp`文件。\n3.  将以下代码片段复制到`intervals.cpp`文件中：\n\n```cpp\n#include <iostream>\n#include <chrono>\n\nint main() {\n  std::chrono::duration<double, std::micro> prev;\n  for (int i = 0; i < 10; i++) {\n    auto start = std::chrono::steady_clock::now();\n    std::cout << i << \": \";\n    auto end = std::chrono::steady_clock::now();\n    std::chrono::duration<double, std::micro> delta = end - start;\n    std::cout << \"output duration is \" << delta.count() <<\" us\";\n    if (i) {\n      auto diff = (delta - prev).count();\n      if (diff >= 0) {\n        std::cout << \", \" << diff << \" us slower\";\n      } else {\n        std::cout << \", \" << -diff << \" us faster\";\n      }\n    }\n    std::cout << std::endl;\n    prev = delta;\n  }\n  return 0;\n}\n```\n\n4.  最后，创建包含我们程序的构建规则的`CMakeLists.txt`文件：\n\n```cpp\ncmake_minimum_required(VERSION 3.5.1)\nproject(interval)\nadd_executable(interval interval.cpp)\n\nset(CMAKE_SYSTEM_NAME Linux)\nset(CMAKE_SYSTEM_PROCESSOR arm)\n\nSET(CMAKE_CXX_FLAGS \"--std=c++ 11\")\nset(CMAKE_CXX_COMPILER /usr/bin/arm-linux-gnueabi-g++)\n```\n\n现在您可以构建和运行应用了。\n\n# 它是如何运作的..。\n\n在应用循环的每次迭代中，我们测量一个输出操作的性能。 为此，我们在操作之前捕获一个时间戳，并在操作完成后捕获另一个时间戳：\n\n```cpp\n auto start = std::chrono::steady_clock::now();\n    std::cout << i << \": \";\n auto end = std::chrono::steady_clock::now();\n```\n\n我们使用 C++ 11`auto`让编译器推断时间戳的数据类型。 现在，我们需要计算这些时间戳之间的时间间隔。 用一个时间戳减去另一个时间戳就行了。 我们将结果变量显式定义为跟踪`double`值中的微秒的`std::chrono::duration`类：\n\n```cpp\n std::chrono::duration<double, std::micro> delta = end - start;\n```\n\n我们使用另一个相同类型的`duration`变量来保存前一个值。 在除第一个迭代之外的每个迭代中，我们计算这两个持续时间之间的差异：\n\n```cpp\n    auto diff = (delta - prev).count();\n```\n\n每次迭代时，持续时间和差值都会打印到终端。 当我们运行应用时，我们得到以下输出：\n\n![](img/ec323f6d-4496-4050-a609-dc90436a90c5.png)\n\n正如我们所看到的，现代 C++ 提供了在应用中处理时间间隔的方便方法。 多亏了重载运算符，可以很容易地获得两个时间点之间的持续时间，并可以对持续时间进行加、减或比较。\n\n# 还有更多的..。\n\n从 C++ 20 开始，Chrono 库支持将持续时间直接写入输出流，并从输入流中解析持续时间。 不需要显式地将持续时间序列化为整数值或浮点值。 这使得处理持续时间对于 C++ 开发人员来说更加方便。\n\n# 在延迟的情况下工作\n\n周期性数据处理是许多嵌入式应用中的一种常见模式。 代码不需要一直工作。 如果我们提前知道何时需要处理，应用或工作线程可能在大部分时间处于非活动状态，只有在需要时才会唤醒并处理数据。 它可以节省功耗，或者让设备上运行的其他应用在应用空闲时使用 CPU 资源。\n\n有几种技术可以组织定期处理。 运行带有延迟的循环的工作线程是其中最简单、最常见的一种。\n\nC++ 提供了向当前执行线程添加延迟的标准函数。 在本食谱中，我们将学习两种将延迟添加到应用中的方法，并讨论它们的优缺点。\n\n# 怎么做……\n\n我们将创建一个具有两个处理循环的应用。 这些循环使用不同的函数来暂停当前线程的执行。\n\n1.  在您的`~/test`工作目录中，创建一个名为`delays`的子目录。\n2.  使用您喜欢的文本编辑器在`delays`子目录中创建`delays.cpp`文件。\n3.  让我们首先添加第一个函数`sleep_for`，以及必要的包含内容：\n\n```cpp\n#include <iostream>\n#include <chrono>\n#include <thread>\n\nusing namespace std::chrono_literals;\n\nvoid sleep_for(int count, auto delay) {\n  for (int i = 0; i < count; i++) {\n    auto start = std::chrono::system_clock::now();\n    std::this_thread::sleep_for(delay);\n    auto end = std::chrono::system_clock::now();\n    std::chrono::duration<double, std::milli> delta = end - start;\n    std::cout << \"Sleep for: \" << delta.count() << std::endl;\n  }\n}\n```\n\n4.  后跟第二个函数`sleep_until`：\n\n```cpp\nvoid sleep_until(int count, \n                 std::chrono::milliseconds delay) {\n  auto wake_up = std::chrono::system_clock::now();\n  for (int i = 0; i < 10; i++) {\n    wake_up += delay;\n    auto start = std::chrono::system_clock::now();\n    std::this_thread::sleep_until(wake_up);\n    auto end = std::chrono::system_clock::now();\n    std::chrono::duration<double, std::milli> delta = end - start;\n    std::cout << \"Sleep until: \" << delta.count() << std::endl;\n  }\n}\n```\n\n5.  接下来，添加一个简单的`main`函数来调用它们：\n\n```cpp\nint main() {\n  sleep_for(10, 100ms);\n  sleep_until(10, 100ms);\n  return 0;\n}\n```\n\n6.  最后，创建一个`CMakeLists.txt`文件，其中包含程序的构建规则：\n\n```cpp\ncmake_minimum_required(VERSION 3.5.1)\nproject(delays)\nadd_executable(delays delays.cpp)\n\nset(CMAKE_SYSTEM_NAME Linux)\nset(CMAKE_SYSTEM_PROCESSOR arm)\n\nSET(CMAKE_CXX_FLAGS \"--std=c++ 14\")\nset(CMAKE_CXX_COMPILER /usr/bin/arm-linux-gnueabi-g++)\n```\n\n现在您可以构建和运行应用了。\n\n# 它是如何运作的..。\n\n在我们的应用中，我们创建了两个函数`sleep_for`和`sleep_until`。 它们几乎相同，除了`sleep_for`使用`std::this_thread::sleep_for`添加延迟，而`sleep_until`使用`std::this_thread::sleep_until`。\n\n让我们仔细看看`sleep_for`函数。 它有两个参数-`count`和`delay`。 第一个参数定义循环中的迭代次数，第二个参数指定延迟。 我们使用`auto`作为`delay`参数的数据类型，让 C++ 为我们推断实际的数据类型。\n\n函数体由单个循环组成：\n\n```cpp\n  for (int i = 0; i < count; i++) {\n```\n\n在每次迭代中，我们运行`delay`，并通过在`delay`之前和之后获取时间戳来测量其实际持续时间。 `std::this_thread::sleep_for`函数接受时间间隔作为参数：\n\n```cpp\n    auto start = std::chrono::system_clock::now();\n    std::this_thread::sleep_for(delay);\n    auto end = std::chrono::system_clock::now();\n```\n\n实际延迟以毫秒为单位，我们使用`double`值作为毫秒计数器：\n\n```cpp\nstd::chrono::duration<double, std::milli> delta = end - start;\n```\n\n`wait_until`函数仅略有不同。 它使用`std::current_thred::wait_until`函数，该函数接受一个时间点来唤醒，而不是一个时间间隔。 我们引入一个额外的`wake_up`变量来跟踪唤醒时间点：\n\n```cpp\nauto wake_up = std::chrono::system_clock::now();\n```\n\n最初，它被设置为当前时间，并且在每次迭代时，它将作为函数参数传递的延迟加到它的值中：\n\n```cpp\nwake_up += delay;\n```\n\n该函数的其余部分与`sleep_for`实现相同，但`delay`函数除外：\n\n```cpp\nstd::this_thread::sleep_until(wake_up);\n```\n\n我们运行这两个函数，使用相同的迭代次数和相同的延迟。 请注意，我们如何使用 C++ 字符串向函数传递毫秒数，以提高代码的可读性。 要使用字符串文字，我们添加了以下内容：\n\n```cpp\nsleep_for(10, 100ms);\nsleep_until(10, 100ms);\n```\n\n这是在函数定义之上完成的，如下所示：\n\n```cpp\nusing namespace std::chrono_literals;\n```\n\n不同的延迟功能会有什么不同吗？ 毕竟，我们在两种实现中都使用相同的延迟。 让我们运行代码并比较结果：\n\n![](img/f3b5c599-d2cd-4f38-9cdc-a5a908f9ce68.png)\n\n有趣的是，我们可以看到`sleep_for`的所有实际延迟都大于`100`毫秒，而`sleep_until`的一些结果低于此值。 我们的第一个函数`delay_for`没有考虑将数据打印到控制台所需的时间。 `sleep_for`当您确切知道需要等待多长时间时，`sleep_for`是一个很好的选择。 然而，如果您的目标是以特定的周期醒来，`sleep_until`可能是更好的选择。\n\n# 还有更多的..。\n\n`sleep_for`和`sleep_until`之间还有其他细微的区别。 系统计时器通常不太精确，可能会通过时间同步服务(如**网络时间协议****守护进程**(**ntpd**)进行调整。 这些时钟调整不会影响`sleep_for`，但`sleep_until`会将其考虑在内。 如果您的应用依赖于特定时间而不是时间间隔，则使用它；例如，如果您需要每秒在时钟显示上重新绘制数字。\n\n# 使用单调时钟\n\nC++ 计时库提供三种类型的时钟：\n\n*   系统时钟\n*   稳定时钟\n*   高分辨率时钟\n\n高分辨率时钟通常被实现为系统时钟或稳定时钟的别名。 然而，系统时钟和稳定时钟有很大的不同。\n\n系统时钟反映系统时间，因此不是单调的。 它可以随时通过**网络时间协议**(**NTP**)等时间同步服务进行调整，因此甚至可以倒退。\n\n这使得系统时钟不适合处理精确的持续时间。 稳定的时钟是单调的，它永远不会调整，也永远不会倒退。 此属性有其成本-它与挂钟时间无关，通常表示为自上次重新启动以来的时间。\n\n稳定时钟不应用于需要在重新启动后保持有效的持久时间戳-例如，序列化到文件中或保存到数据库中。 此外，稳定时钟不应用于任何涉及来自不同来源(如远程系统或外部设备)的时间的时间计算。\n\n在本食谱中，我们将学习如何使用稳定时钟来实现一个简单的软件监视器。 运行后台工作线程时，一定要知道它是工作正常还是因为编码错误或外部设备无响应而挂起。 线程定期更新时间戳，而监视例程将时间戳与当前时间进行比较，如果超过阈值，则执行特定的恢复操作。\n\n# 怎么做……\n\n在我们的应用中，我们将创建一个在后台运行的简单迭代函数，以及在主线程中运行的监视循环。\n\n1.  在您的`~/test`工作目录中，创建一个名为`monotonic`的子目录。\n2.  使用您喜欢的文本编辑器在`monotonic`子目录中创建`monotonic.cpp`文件。\n3.  让我们添加标题并定义例程使用的全局变量：\n\n```cpp\n#include <iostream>\n#include <chrono>\n#include <atomic>\n#include <mutex>\n#include <thread>\n\nauto touched = std::chrono::steady_clock::now();\nstd::mutex m;\nstd::atomic_bool ready{ false };\n```\n\n4.  后跟后台工作线程例程的代码：\n\n```cpp\nvoid Worker() {\n  for (int i = 0; i < 10; i++) {\n    std::this_thread::sleep_for(\n         std::chrono::milliseconds(100 + (i % 4) * 10));\n    std::cout << \"Step \" << i << std::endl;\n    {\n      std::lock_guard<std::mutex> l(m);\n      touched = std::chrono::steady_clock::now();\n    }\n  }\n  ready = true;\n}\n```\n\n5.  添加包含监控例程的`main`函数：\n\n```cpp\nint main() {\n  std::thread t(Worker);\n  std::chrono::milliseconds threshold(120);\n  while(!ready) {\n    auto now = std::chrono::steady_clock::now();\n    std::chrono::milliseconds delta;\n    {\n      std::lock_guard<std::mutex> l(m);\n      auto delta = now - touched;\n      if (delta > threshold) {\n        std::cout << \"Execution threshold exceeded\" << std::endl;\n      }\n    }\n    std::this_thread::sleep_for(std::chrono::milliseconds(10));\n\n  }\n  t.join();\n  return 0;\n}\n```\n\n6.  最后，创建包含我们程序的构建规则的`CMakeLists.txt`文件：\n\n```cpp\ncmake_minimum_required(VERSION 3.5.1)\nproject(monotonic)\nadd_executable(monotonic monotonic.cpp)\ntarget_link_libraries(monotonic pthread)\n\nset(CMAKE_SYSTEM_NAME Linux)\nset(CMAKE_SYSTEM_PROCESSOR arm)\n\nSET(CMAKE_CXX_FLAGS \"--std=c++ 11\")\nset(CMAKE_CXX_COMPILER /usr/bin/arm-linux-gnueabi-g++)\n\n```\n\n现在您可以构建和运行应用了。\n\n# 它是如何运作的..。\n\n我们的应用是多线程的-它由运行监视的主线程和后台工作线程组成。 我们使用三个全局变量进行同步。\n\n`touched`变量保存由`Worker`线程定期更新的时间戳。 由于时间戳由两个线程访问，因此需要保护访问。 为此，我们使用`m`互斥锁。 最后，为了指示工作线程已经完成了它的工作，使用了一个原子变量`ready`。\n\n工作线程是一个内部包含人为延迟的循环。 延迟是根据步数计算的，导致的延迟从 100 毫秒到 130 毫秒：\n\n```cpp\nstd::this_thread::sleep_for(\n         std::chrono::milliseconds(100 + (i % 4) * 10));\n```\n\n在每次迭代中，`Worker`线程更新时间戳。 锁保护用于同步对时间戳的访问：\n\n```cpp\n    {\n      std::lock_guard<std::mutex> l(m);\n      touched = std::chrono::steady_clock::now();\n    }\n```\n\n监控例程在`Worker`线程运行时循环运行。 在每次迭代中，它计算当前时间和上次更新之间的时间间隔：\n\n```cpp\n      std::lock_guard<std::mutex> l(m);\n      auto delta = now - touched;\n```\n\n如果大于阈值，该函数会打印一条警告消息，如下所示：\n\n```cpp\n      if (delta > threshold) {\n        std::cout << \"Execution threshold exceeded\" << std::endl;\n      }\n```\n\n在许多情况下，应用可以调用恢复功能来重置外部设备或重新启动线程。 我们在监控循环中添加`10`毫秒的延迟：\n\n```cpp\n    std::this_thread::sleep_for(std::chrono::milliseconds(10));\n```\n\n这有助于我们减少资源消耗，同时实现可接受的反应时间。 运行应用会产生以下输出：\n\n![](img/7962b124-e6ed-45a4-b2da-ffa84adf2d9b.png)\n\n我们可以在输出中看到几个警告，表明`worker`线程中的某些迭代花费的时间超过了阈值`120`毫秒。 这是可以预测的，因为`worker`函数是这样编写的。 重要的是，我们使用单调的`std::chrono::steady_clock`函数进行监控。 使用系统时钟可能会导致在时钟调整期间错误地调用恢复功能。\n\n# 还有更多的..。\n\nC++ 20 定义了几种其他类型的时钟，例如`gps_clock`，表示**全球定位系统**(**GPS**)时间，或`file_clock`，用于处理文件时间戳。 这些时钟可能是稳定的，也可能不是稳定的或单调的。 使用`is_steady`成员函数检查时钟是否单调。\n\n# 使用 POSIX 时间戳\n\n在基于 Unix 的操作系统中，POSIX 时间戳是时间的传统内部表示形式。 POSIX 时间戳定义为自纪元或协调世界时间(**UTC**)，1970 年 1 月 1 日以来的秒数。\n\n由于其简单性，这种表示被广泛用于网络协议、文件元数据或序列化。\n\n在本食谱中，我们将学习如何将 C++ 时间点转换为 POSIX 时间戳，并从 POSIX 时间戳创建 C++ 时间点。\n\n# 怎么做……\n\n我们将创建一个应用，该应用将时间点转换为 POSIX 时间戳，然后从该时间戳恢复时间点。\n\n1.  在您的`~/test`工作目录中，创建一个名为`timestamps`的子目录。\n2.  使用您喜欢的文本编辑器在`timestamps`子目录中创建`timestamps.cpp`文件。\n3.  将以下代码片段放入文件中：\n\n```cpp\n#include <iostream>\n#include <chrono>\n\nint main() {\n  auto now = std::chrono::system_clock::now();\n\n  std::time_t ts = std::chrono::system_clock::to_time_t(now);\n  std::cout << \"POSIX timestamp: \" << ts << std::endl;\n\n  auto restored = std::chrono::system_clock::from_time_t(ts);\n\n  std::chrono::duration<double, std::milli> delta = now - restored;\n  std::cout << \"Recovered time delta \" << delta.count() << std::endl;\n  return 0;\n}\n```\n\n4.  创建包含我们程序的构建规则的`CMakeLists.txt`文件：\n\n```cpp\ncmake_minimum_required(VERSION 3.5.1)\nproject(timestamps)\nadd_executable(timestamps timestamps.cpp)\n\nset(CMAKE_SYSTEM_NAME Linux)\nset(CMAKE_SYSTEM_PROCESSOR arm)\n\nSET(CMAKE_CXX_FLAGS \"--std=c++ 11\")\nset(CMAKE_CXX_COMPILER /usr/bin/arm-linux-gnueabi-g++)\n```\n\n现在您可以构建和运行应用了。\n\n# 它是如何运作的..。\n\n首先，我们使用系统时钟为当前时间创建一个 Time Point 对象：\n\n```cpp\nauto now = std::chrono::system_clock::now();\n```\n\n由于 POSIX 时间戳表示自纪元以来的时间，因此我们不能使用稳定时钟。 但是，系统时钟知道如何将其内部表示形式转换为 POSIX 格式。 为此，它提供了一个`to_time_t`静态函数：\n\n```cpp\nstd::time_t ts = std::chrono::system_clock::to_time_t(now);\n```\n\n结果被定义为具有类型`std::time_t`，但这是一个整数类型，而不是对象。 与时间点实例不同，我们可以将其直接写入输出流：\n\n```cpp\nstd::cout << \"POSIX timestamp: \" << ts << std::endl;\n```\n\n让我们尝试从这个整数时间戳恢复一个时间点。 我们使用`from_time_t`静态函数：\n\n```cpp\nauto restored = std::chrono::system_clock::from_time_t(ts);\n```\n\n现在，我们有两个时间戳。 它们是一样的吗？ 让我们计算并显示差值：\n\n```cpp\nstd::chrono::duration<double, std::milli> delta = now - restored;\nstd::cout << \"Recovered time delta \" << delta.count() << std::endl;\n```\n\n当我们运行应用时，我们会得到以下输出：\n\n![](img/1b9142d4-dd21-4eed-a8f0-d2457fd084f2.png)\n\n时间戳是不同的，但差异始终小于 1,000。 由于 POSIX 时间戳被定义为自纪元以来的秒数，因此我们丢失了精细粒度时间，如毫秒和微秒。\n\n尽管有这些限制，POSIX 时间戳仍然是一种重要且广泛使用的时间传输表示形式，我们学习了在需要时如何将它们转换为内部 C++ 表示形式。\n\n# 还有更多的..。\n\n在许多情况下，直接使用 POSIX 时间戳就足够了。 因为它们是用数字表示的，所以可以使用简单的数字比较来确定哪个时间戳是新的还是旧的。 同样，从一个时间戳中减去另一个时间戳得到它们之间的时间间隔(以秒为单位)。 如果性能是瓶颈，则此方法可能比与本机 C++ 时间点进行比较更可取。"
  },
  {
    "path": "docs/emb-prog-mod-cpp-cb/12.md",
    "content": "# 十二、错误处理和容错\n\n关于嵌入式软件，错误处理的重要性怎么估计都不为过。 嵌入式系统应该在不同的物理条件下在没有监督的情况下工作，例如控制可能发生故障转移或不总是提供可靠通信线路的外部外部设备。 在许多情况下，系统的故障要么代价高昂，要么就是不安全。\n\n在本章中，我们将了解帮助您编写可靠且容错的嵌入式应用的常见策略和最佳实践。\n\n在本章中，我们将介绍以下食谱：\n\n*   使用错误代码\n*   使用异常进行错误处理\n*   捕获异常时使用常量引用\n*   处理静态对象\n*   使用看门狗\n*   探索高可用性系统的心跳\n*   实现软件去抖动逻辑\n\n这些菜谱将帮助您理解错误处理设计的重要性，学习最佳实践，并避免该领域的陷阱。\n\n# 使用错误代码\n\n在设计新函数时，开发人员通常需要一种机制来指示函数由于某种错误而无法完成其工作。 它可能无效、从外部设备接收到意外结果或资源分配问题。\n\n报告错误状况的最传统和最广泛的方式之一是通过错误代码。 这是一种高效且无处不在的机制，不依赖于编程语言或操作系统。 由于其高效性、通用性和跨平台能力，在嵌入式软件开发中得到了广泛的应用。\n\n设计返回值或错误代码的函数接口可能很棘手，特别是当值和错误代码具有不同类型时。 在本食谱中，我们将探索设计这类函数接口的几种方法。\n\n# 怎么做……\n\n我们将创建一个简单的程序，其中包含名为`Receive`的函数的三个实现。 这三个实现具有相同的行为，但接口不同。 遵循以下步骤：\n\n1.  在您的工作目录(即`~/test`)中，创建一个名为`errcode`的子目录。\n2.  使用您喜欢的文本编辑器在`errcode`子目录中创建名为`errcode.cpp`的文件。\n3.  将第一个函数的实现添加到`errcode.cpp`文件：\n\n```cpp\n#include <iostream>\n\nint Receive(int input, std::string& output) {\n  if (input < 0) {\n    return -1;\n  }\n\n  output = \"Hello\";\n  return 0;\n}\n```\n\n4.  接下来，我们添加第二个实现：\n\n```cpp\nstd::string Receive(int input, int& error) {\n  if (input < 0) {\n    error = -1;\n    return \"\";\n  }\n  error = 0;\n  return \"Hello\";\n}\n```\n\n5.  `Receive`函数的第三个实现如下：\n\n```cpp\nstd::pair<int, std::string> Receive(int input) {\n  std::pair<int, std::string> result;\n  if (input < 0) {\n    result.first = -1;\n  } else {\n    result.second = \"Hello\";\n  }\n  return result;\n}\n```\n\n6.  现在，我们定义一个名为`Display`的帮助器函数来显示结果：\n\n```cpp\nvoid Display(const char* prefix, int err, const std::string& result) {\n  if (err < 0) {\n    std::cout << prefix << \" error: \" << err << std::endl;\n  } else {\n    std::cout << prefix << \" result: \" << result << std::endl;\n  }\n}\n```\n\n7.  然后，我们添加一个名为`Test`的函数，该函数调用所有三个实现：\n\n```cpp\nvoid Test(int input) {\n  std::string outputResult;\n  int err = Receive(input, outputResult);\n  Display(\" Receive 1\", err, outputResult);\n\n  int outputErr = -1;\n  std::string result = Receive(input, outputErr);\n  Display(\" Receive 2\", outputErr, result);\n\n  std::pair<int, std::string> ret = Receive(input);\n  Display(\" Receive 3\", ret.first, ret.second);\n}\n```\n\n8.  `main`函数将所有内容联系在一起：\n\n```cpp\nint main() {\n  std::cout << \"Input: -1\" << std::endl;\n  Test(-1);\n  std::cout << \"Input: 1\" << std::endl;\n  Test(1);\n\n  return 0;\n}\n```\n\n9.  最后，我们创建一个`CMakeLists.txt`文件，其中包含程序的构建规则：\n\n```cpp\ncmake_minimum_required(VERSION 3.5.1)\nproject(errcode)\nadd_executable(errcode errcode.cpp)\nset(CMAKE_SYSTEM_NAME Linux)\nset(CMAKE_SYSTEM_PROCESSOR arm)\n\nSET(CMAKE_CXX_FLAGS \"--std=c++ 11\")\nset(CMAKE_CXX_COMPILER /usr/bin/arm-linux-gnueabi-g++)\n```\n\n10.  现在您可以构建和运行应用了。\n\n# 它是如何运作的..。\n\n在我们的应用中，我们定义了从某个设备接收数据的函数的三种不同实现。 它应该以字符串的形式返回接收到的数据，但是在出现错误的情况下，它应该返回一个表示错误原因的整数错误代码。\n\n由于结果和错误代码具有不同的类型，因此我们不能对两者重用相同的值。 要在 C++ 中返回多个值，我们需要使用输出参数或创建复合数据类型。\n\n我们的实现探索了这两种策略。 我们使用 C++ 函数重载来定义同名但不同类型的参数和返回值的`Receive`函数。\n\n第一个实现返回错误代码并将结果存储在输出参数 Result 中：\n\n```cpp\nint Receive(int input, std::string& output)\n```\n\n输出参数是通过引用传递的字符串，用于让函数修改其内容。 第二个实现将参数反转。 它返回一个收到的字符串作为结果，并接受错误代码作为输出参数：\n\n```cpp\nstd::string Receive(int input, int& error)\n```\n\n因为我们希望从函数内部设置错误代码，所以我们也通过引用传递它。 最后，第三个实现在 C++ `pair`中组合并返回结果和错误代码：\n\n```cpp\nstd::pair<int, std::string> Receive(int input)\n```\n\n该函数始终创建一个`std::pair<int, std::string>`实例。 因为我们没有将任何值传递给它的构造函数，所以该对象是默认初始化的。 整数元素设置为`0`，字符串元素设置为空字符串。\n\n这种方法不需要`output`参数，可读性更好，但构造和销毁`pair`对象的开销略高。\n\n定义了所有三个实现后，我们将在`Test`函数中测试所有实现。 我们将相同的参数传递给每个实现并显示结果。 我们希望它们每一个都能产生相同的结果。\n\n有两次调用`Test`。 首先，我们将`-1`作为参数传递，这将触发错误路径，然后我们传递`1`，这将激活正常操作路径：\n\n```cpp\n  std::cout << \"Input: -1\" << std::endl;\n  Test(-1);\n  std::cout << \"Input: 1\" << std::endl;\n  Test(1);\n```\n\n当我们运行我们的程序时，我们看到以下输出：\n\n![](img/4e6428d7-7667-4b9f-8b0c-bc17d09787d0.png)\n\n所有三种实现都根据输入参数正确返回结果或错误代码。 您可以根据总体设计指导原则或个人喜好在应用中使用任何方法。\n\n# 还有更多的..。\n\n作为 C++ 17 标准的一部分，标准库中添加了一个名为`std::optional`的模板。 它可以表示可能缺失的可选值。 它可以用作可能失败的函数的返回值。 但是，它不能表示失败的原因，只能表示一个布尔值，指示该值是否有效。 有关更多信息，请查看位于[https://en.cppreference.com/w/cpp/utility/optional](https://en.cppreference.com/w/cpp/utility/optional)的`std::optional`参考。\n\n# 使用异常进行错误处理\n\n虽然错误代码仍然是嵌入式编程中最广泛使用的错误处理技术，但 C++ 提供了另一种用于此目的的机制，称为异常。\n\n异常旨在简化错误处理并使其更可靠。 使用错误代码时，开发人员必须检查每个函数的结果是否有错误，并将结果传播到调用函数。 这会用大量 if-Else 结构使代码变得混乱，使函数逻辑更加模糊。\n\n使用异常时，开发人员不需要在每次函数调用后检查错误。 异常通过调用堆栈自动传播，直到它们到达可以通过记录、重试或终止应用来正确处理它的代码。\n\n虽然异常是 C++ 标准库的默认错误处理机制，但与外部设备或底层操作系统层的通信仍然涉及错误代码。 在本食谱中，我们将学习如何使用`std::system_error`Exception 类将低级错误处理连接到 C++ 异常。\n\n# 怎么做……\n\n我们将创建一个简单的应用，它通过串行链路与设备通信。 遵循以下步骤：\n\n1.  在您的工作目录(即`~/test`)中，创建一个名为`except`的子目录。\n2.  使用您喜欢的文本编辑器在`ex``cept`子目录中创建名为`except.cpp`的文件。\n\n3.  将所需的包含内容放入`except.cpp`文件中：\n\n```cpp\n#include <iostream>\n#include <system_error>\n#include <fcntl.h>\n#include <unistd.h>\n```\n\n4.  接下来，我们定义一个将通信抽象到设备的`Device`类。 我们从构造函数和析构函数开始：\n\n```cpp\nclass Device {\n  int fd;\n\n  public:\n    Device(const std::string& deviceName) {\n      fd = open(deviceName.c_str(), O_RDWR);\n      if (fd < 0) {\n        throw std::system_error(errno, std::system_category(),\n                                \"Failed to open device file\");\n      }\n    }\n\n    ~Device() {\n      close(fd);\n    }\n\n```\n\n5.  然后，我们添加一个向设备发送数据的方法，如下所示：\n\n```cpp\n    void Send(const std::string& data) {\n      size_t offset = 0;\n      size_t len = data.size();\n      while (offset < data.size() - 1) {\n        int sent = write(fd, data.data() + offset, \n                         data.size() - offset);\n        if (sent < 0) {\n          throw std::system_error(errno, \n                                  std::system_category(),\n                                  \"Failed to send data\");\n        }\n        offset += sent;\n      }\n    }\n};\n```\n\n6.  定义类之后，我们添加`main`函数，该函数使用它：\n\n```cpp\nint main() {\n  try {\n    Device serial(\"/dev/ttyUSB0\");\n    serial.Send(\"Hello\");\n  } catch (std::system_error& e) {\n    std::cout << \"Error: \" << e.what() << std::endl;\n    std::cout << \"Code: \" << e.code() << \" means \\\"\" \n              << e.code().message()\n              << \"\\\"\" << std::endl;\n  }\n\n  return 0;\n}\n```\n\n7.  最后，我们创建一个`CMakeLists.txt`文件，其中包含程序的构建规则：\n\n```cpp\ncmake_minimum_required(VERSION 3.5.1)\nproject(except)\nadd_executable(except except.cpp)\n\nset(CMAKE_SYSTEM_NAME Linux)\nset(CMAKE_SYSTEM_PROCESSOR arm)\n\nSET(CMAKE_CXX_FLAGS \"--std=c++ 11\")\nset(CMAKE_CXX_COMPILER /usr/bin/arm-linux-gnueabi-g++)\n```\n\n8.  现在您可以构建和运行应用了。\n\n# 它是如何运作的..。\n\n我们的应用与通过串行链路连接的外部设备通信。 在 POSIX 操作系统中，与设备的通信类似于使用常规文件的操作，并且使用相同的 API；即`open`、`close`、`read`和`write`函数。\n\n所有这些函数都返回错误代码以指示各种错误情况。 我们没有直接使用它们，而是将通信包装在一个名为`Device`的类中。\n\n其构造函数尝试打开由`deviceName`构造函数参数引用的文件。 构造函数检查错误代码，如果指示错误，则创建并抛出`std::system_error`异常：\n\n```cpp\n  throw std::system_error(errno, std::system_category(),\n                          \"Failed to open device file\");\n```\n\n我们使用三个参数构造`std::system_error`实例。 第一个是我们想要包装在异常中的错误代码。 当`open`函数返回错误时，我们使用由`open`函数设置的`errno`变量的值。 第二个参数是错误类别。 因为我们使用特定于操作系统的错误代码，所以我们使用`std::system_category`的实例。 第一个参数是我们希望与异常关联的消息。 它可以是任何可以帮助我们识别错误(如果它发生)的东西。\n\n我们以类似的方式定义`Send`函数，该函数将数据发送到设备。 它是`write`系统函数的包装器，如果`write`返回错误，我们将创建并抛出一个`std::system_error`实例。 唯一的区别是消息字符串，因为我们希望在日志中区分这两种情况：\n\n```cpp\nthrow std::system_error(errno, std::system_category(),\n                         \"Failed to send data\");\n}\n```\n\n在定义了`Device`类之后，我们就可以使用它了。 我们不需要打开设备并检查错误，然后写入设备并再次检查错误，只需创建`Device`类的实例并向其发送数据：\n\n```cpp\nDevice serial(\"/dev/ttyUSB0\");\nserial.Send(\"Hello\");\n```\n\n所有错误处理都位于主逻辑之后的`catch`块中。 如果抛出系统错误，我们会将其记录到标准输出中。 此外，我们还打印嵌入在异常中的有关错误代码的信息：\n\n```cpp\n  } catch (std::system_error& e) {\n    std::cout << \"Error: \" << e.what() << std::endl;\n    std::cout << \"Code: \" << e.code() << \" means \\\"\" << e.code().message()\n        << \"\\\"\" << std::endl;\n  }\n```\n\n当我们构建和运行应用时，如果没有作为`/dev/ttyUSB0`连接的设备，它将显示以下输出：\n\n![](img/e6da888b-864b-412d-8c25-e35b1e4323e1.png)\n\n正如预期的那样，检测到了错误条件，我们可以看到所有必需的详细信息，包括底层操作系统错误代码及其描述。 请注意，使用包装器类与设备通信的代码整洁且可读。\n\n# 还有更多的..。\n\nC++ 标准库附带了许多预定义的异常和错误类别。 有关更多详细信息，请查看[https://en.cppreference.com/w/cpp/error](https://en.cppreference.com/w/cpp/error)上的 C++ 错误处理参考。\n\n# 捕获异常时使用常量引用\n\nC++ 异常为异常处理设计提供了强大的基础。 它们是灵活的，可以以多种不同的方式使用。 您可以引发任何类型的异常，包括指针和整数。 您可以通过值或引用捕获异常。 在选择数据类型时，错误的选择可能会导致性能下降或资源泄漏。\n\n在本食谱中，我们将分析潜在的陷阱，并学习如何在 CATCH 块中使用常量引用来高效、安全地处理错误。\n\n# 怎么做……\n\n我们将创建一个抛出和捕获自定义异常的示例应用，并分析数据类型选择如何影响效率。 遵循以下步骤：\n\n1.  在您的工作目录(即`~/test`)中，创建一个名为`catch`的子目录。\n2.  使用您喜欢的文本编辑器在`catch`子目录中创建名为`catch.cpp`的文件。\n3.  将`Error`类的定义放入`catch.cpp`文件中：\n\n```cpp\n#include <iostream>\n\nclass Error {\n  int code;\n\n  public:\n    Error(int code): code(code) {\n      std::cout << \" Error instance \" << code << \" was created\"\n                << std::endl;\n    }\n    Error(const Error& other): code(other.code) {\n      std::cout << \" Error instance \" << code << \" was cloned\"\n                << std::endl;\n    }\n    ~Error() {\n      std::cout << \" Error instance \" << code << \" was destroyed\"\n                << std::endl;\n    }\n};\n```\n\n4.  接下来，我们添加帮助器函数来测试抛出和处理错误的三种不同方式。 我们从通过值捕获异常的函数开始：\n\n```cpp\nvoid CatchByValue() {\n  std::cout << \"Catch by value\" << std::endl;\n  try {\n    throw Error(1);\n  }\n  catch (Error e) {\n    std::cout << \" Error caught\" << std::endl;\n  }\n}\n```\n\n5.  然后，我们添加一个抛出指针并通过指针捕获异常的函数，如下所示：\n\n```cpp\nvoid CatchByPointer() {\n  std::cout << \"Catch by pointer\" << std::endl;\n  try {\n    throw new Error(2);\n  }\n  catch (Error* e) {\n    std::cout << \" Error caught\" << std::endl;\n  }\n}\n```\n\n6.  接下来，我们添加一个使用`const`引用捕获异常的函数：\n\n```cpp\nvoid CatchByReference() {\n  std::cout << \"Catch by reference\" << std::endl;\n  try {\n    throw Error(3);\n  }\n  catch (const Error& e) {\n    std::cout << \" Error caught\" << std::endl;\n  }\n}\n```\n\n7.  在定义了所有帮助器函数之后，我们添加`main`函数将所有内容联系在一起：\n\n```cpp\nint main() {\n  CatchByValue();\n  CatchByPointer();\n  CatchByReference();\n  return 0;\n}\n```\n\n8.  我们将应用的构建规则放入`CMakeLists.txt`文件中：\n\n```cpp\ncmake_minimum_required(VERSION 3.5.1)\nproject(catch)\nadd_executable(catch catch.cpp)\nset(CMAKE_SYSTEM_NAME Linux)\nset(CMAKE_SYSTEM_PROCESSOR arm)\n\nSET(CMAKE_CXX_FLAGS \"--std=c++ 11\")\n\nset(CMAKE_CXX_COMPILER /usr/bin/arm-linux-gnueabi-g++)\n```\n\n9.  现在我们可以构建和运行应用了。\n\n# 它是如何运作的..。\n\n在我们的应用中，我们定义了一个名为`Error`的自定义类，我们将在引发和捕获异常时使用它。 该类提供了一个构造函数、一个复制构造函数和一个仅将信息记录到控制台的析构函数。 我们需要它来评估不同异常捕获方法的效率。\n\n`Error`类仅包含`code`数据字段，用于区分类的实例：\n\n```cpp\nclass Error {\n  int code;\n```\n\n我们评估了三种异常处理方法。 第一个，`CatchByValue`，是最直接的。 我们创建并抛出`Error`类的一个实例：\n\n```cpp\nthrow Error(1);\n```\n\n然后，我们通过值来捕捉它：\n\n```cpp\ncatch (Error e) {\n```\n\n第二个实现`CatchByPointer`使用`new`运算符动态创建`Error`的实例：\n\n```cpp\nthrow new Error(2);\n```\n\n我们使用指针来捕获异常：\n\n```cpp\ncatch (Error* e) {\n```\n\n最后，`CatchByReference`抛出一个类似于`CatchByValue`的异常，但它在捕获异常时使用对`Error`的`const`引用：\n\n```cpp\ncatch (const Error& e) {\n```\n\n这有什么不同吗？ 当我们运行我们的程序时，我们得到以下输出：\n\n![](img/0f5b8cf3-a4d9-4337-972f-32d2fdc7772c.png)\n\n正如您所看到的，当通过值捕获对象时，将创建异常对象的副本。 虽然这种低效在示例应用中并不严重，但在高负载应用中可能会导致性能问题。\n\n通过指针捕获异常并不是低效的，但是我们可以看到对象析构函数没有被调用，从而导致了内存泄漏。 这可以通过从`catch`块调用`delete`来避免，但这很容易出错，因为并不总是清楚谁负责销毁指针引用的对象。\n\n参考方法是最安全、最有效的方法。 没有内存泄漏和不必要的复制。 此外，设置引用常量会给编译器一个提示，即它不会被更改，因此可以在幕后更好地进行优化。\n\n# 还有更多的..。\n\n错误处理是一个复杂的领域，有许多最佳实践、提示和建议。 请考虑阅读[https://isocpp.org/wiki/faq/exceptions](https://isocpp.org/wiki/faq/exceptions)上的 C++ 异常和错误处理常见问题解答，以掌握您的异常处理技能。\n\n# 处理静态对象\n\n在 C++ 中，如果对象不能正确实例化，对象构造函数就会抛出异常。 通常，这不会导致任何问题。 源自堆栈上构造的对象或使用`new`关键字动态创建的对象的异常可以由创建该对象的代码周围的 try-catch 块处理。\n\n不过，对于静态对象来说，情况会变得更加复杂。 这样的对象是在执行进入`main`函数之前实例化的，因此它们不能包装在程序的 try-catch 块中。 C++ 编译器通过调用`std::terminate`函数来处理这种情况，该函数打印错误消息并终止程序。 即使异常不是致命的，也没有办法恢复。\n\n有几种方法可以避免落入这个陷阱。 一般来说，只应静态分配简单的整型数据类型。 如果您仍然需要一个复杂的静态对象，请确保其构造函数不会引发异常。\n\n在本食谱中，我们将学习如何实现静态对象的构造函数。\n\n# 怎么做……\n\n我们将创建一个分配指定内存量的自定义类，并静态分配该类的两个实例。 遵循以下步骤：\n\n1.  在您的工作目录(即`~/test`)中，创建一个名为`static`的子目录。\n2.  使用您喜欢的文本编辑器在`static`子目录中创建名为`static.cpp`的文件。\n3.  让我们定义一个名为`Complex`的类。 将其私有字段和构造函数放入`static.cpp`文件中：\n\n```cpp\n#include <iostream>\n#include <stdint.h>\n\nclass Complex {\n  char* ptr;\n\n  public:\n    Complex(size_t size) noexcept {\n      try {\n        ptr = new(std::nothrow) char[size];\n        if (ptr) {\n          std::cout << \"Successfully allocated \"\n                    << size << \" bytes\" << std::endl;\n        } else {\n          std::cout << \"Failed to allocate \"\n                    << size << \" bytes\" << std::endl;\n        }\n      } catch (...) {\n        // Do nothing\n      }\n    }\n```\n\n4.  然后，定义析构函数和`IsValid`方法：\n\n```cpp\n    ~Complex() {\n      try {\n        if (ptr) {\n          delete[] ptr;\n          std::cout << \"Deallocated memory\" << std::endl;\n        } else {\n          std::cout << \"Memory was not allocated\" \n                    << std::endl;\n        }\n      } catch (...) {\n        // Do nothing\n      }\n    }\n\n    bool IsValid() const { return nullptr != ptr; }\n};\n```\n\n5.  定义类之后，我们定义两个全局对象`small`和`large`，以及使用它们的`main`函数：\n\n```cpp\nComplex small(100);\nComplex large(SIZE_MAX);\nint main() {\n  std::cout << \"Small object is \" \n            << (small.IsValid()? \"valid\" : \"invalid\")\n            << std::endl;\n  std::cout << \"Large object is \" \n            << (large.IsValid()? \"valid\" : \"invalid\")\n            << std::endl;\n\n  return 0;\n}\n```\n\n6.  最后，我们创建一个`CMakeLists.txt`文件，其中包含程序的构建规则：\n\n```cpp\ncmake_minimum_required(VERSION 3.5.1)\nproject(static)\nadd_executable(static static.cpp)\nset(CMAKE_SYSTEM_NAME Linux)\nset(CMAKE_SYSTEM_PROCESSOR arm)\n\nSET(CMAKE_CXX_FLAGS \"--std=c++ 11\")\nset(CMAKE_CXX_COMPILER /usr/bin/arm-linux-gnueabi-g++)\n```\n\n7.  现在您可以构建和运行应用了。\n\n# 它是如何运作的..。\n\n在这里，我们定义了`Complex`类，并且我们打算静态地分配该类的实例。 为了安全起见，我们需要确保该类的构造函数和析构函数都不能抛出异常。\n\n但是，构造函数和析构函数都会调用可能引发异常的操作。 构造函数执行内存分配，而析构函数将日志写入标准输出。\n\n构造函数使用`new`运算符分配内存，如果无法分配内存，则会抛出`std::bad_alloc`异常。 我们使用`std::nothrow`常量来选择`new`的非抛出实现。 如果无法分配任何内存，`new`将返回`nullptr`，而不是抛出异常：\n\n```cpp\nptr = new(std::nothrow) char[size];\n```\n\n我们将构造函数的主体包装在`try`块中，以捕获所有异常。 `catch`块为空-如果构造函数失败，我们将无能为力：\n\n```cpp\n} catch (...) {\n        // Do nothing\n}\n```\n\n由于我们不允许任何异常传播到上层，因此我们使用 C++ 关键字(即`noexcept`)将构造函数标记为非抛出：\n\n```cpp\nComplex(size_t size) noexcept {\n```\n\n但是，我们需要知道是否正确创建了对象。 为此，我们定义了一个名为`IsValid`的方法。 如果内存已分配，则返回`true`，否则返回`false`：\n\n```cpp\nbool IsValid() const { return nullptr != ptr; }\n```\n\n析构函数执行相反的操作。 它释放内存，并将释放状态记录到控制台。 至于构造函数，我们不希望任何异常传播到上层，因此我们将析构函数体包装在 try-catch 块中：\n\n```cpp\n try {\n        if (ptr) {\n delete[] ptr;\n          std::cout << \"Deallocated memory\" << std::endl;\n        } else {\n          std::cout << \"Memory was not allocated\" << std::endl;\n        }\n      } catch (...) {\n        // Do nothing\n      }\n```\n\n现在，我们声明两个全局对象`small`和`large`。 全局对象是静态分配的。 对象的大小是以`small`对象将被正确分配的方式人工选择的，但是`large`对象的分配应该失败：\n\n```cpp\nComplex small(100);\nComplex large(SIZE_MAX);\n```\n\n在我们的`main`函数中，我们检查并打印对象是否有效：\n\n```cpp\n  std::cout << \"Small object is \" << (small.IsValid()? \"valid\" : \"invalid\")\n            << std::endl;\n  std::cout << \"Large object is \" << (large.IsValid()? \"valid\" : \"invalid\")\n            << std::endl;\n```\n\n当我们运行我们的程序时，我们看到以下输出：\n\n![](img/33ae692c-8dd9-4803-b71e-6bdfd2d91a90.png)\n\n正如我们所看到的，小对象被正确地分配和释放。 大型对象的初始化失败，但由于它被设计为不抛出任何异常，所以它没有导致应用的异常终止。 您可以对静态分配的对象使用类似的技术来编写健壮而安全的应用。\n\n# 使用看门狗\n\n嵌入式应用是为在没有监督的情况下工作而构建的。 这包括从错误中恢复的能力。 如果应用崩溃，它可以自动重启。 但是，如果应用因进入死循环或死锁而挂起，我们该怎么办呢？\n\n硬件或软件监视程序用于防止此类情况。 应用应该定期通知或*馈送*它们，以指示它们继续正常运行。 如果看门狗在特定时间间隔内未被馈送，它将终止应用或重新启动系统。\n\n存在许多不同的监视器实现，但它们的接口本质上是相同的。 它们提供应用可以用来重置看门狗定时器的功能。\n\n在本食谱中，我们将学习如何在 POSIX 信号子系统之上创建一个简单的软件看门狗。 同样的技术可用于硬件看门狗定时器或更复杂的软件看门狗服务。\n\n# 怎么做……\n\n我们将创建一个定义`Watchdog`类的应用，并提供其用法示例。 遵循以下步骤：\n\n1.  在您的工作目录(即`~/test`)中，创建一个名为`watchdog`的子目录。\n2.  使用您喜欢的文本编辑器在`watchdog`子目录中创建名为`watchdog.cpp`的文件。\n3.  将所需的包含内容放入`watchdog.cpp`文件中：\n\n```cpp\n#include <chrono>\n#include <iostream>\n#include <thread>\n\n#include <unistd.h>\n\nusing namespace std::chrono_literals;\n```\n\n4.  接下来，我们定义`Watchdog`类本身：\n\n```cpp\nclass Watchdog {\n  std::chrono::seconds seconds;\n\n  public:\n    Watchdog(std::chrono::seconds seconds):\n      seconds(seconds) {\n        feed();\n    }\n\n    ~Watchdog() {\n      alarm(0);\n    }\n\n    void feed() {\n      alarm(seconds.count());\n    }\n};\n```\n\n5.  添加`main`函数，作为我们的 Watchdog 的使用示例：\n\n```cpp\nint main() {\n  Watchdog watchdog(2s);\n  std::chrono::milliseconds delay = 700ms;\n  for (int i = 0; i < 10; i++) {\n    watchdog.feed();\n    std::cout << delay.count() << \"ms delay\" << std::endl;\n    std::this_thread::sleep_for(delay);\n    delay += 300ms;\n  }\n}\n```\n\n6.  添加包含我们程序的构建规则的`CMakeLists.txt`文件：\n\n```cpp\ncmake_minimum_required(VERSION 3.5.1)\nproject(watchdog)\nadd_executable(watchdog watchdog.cpp)\n\nset(CMAKE_SYSTEM_NAME Linux)\nset(CMAKE_SYSTEM_PROCESSOR arm)\n\nSET(CMAKE_CXX_FLAGS \"--std=c++ 14\")\n\nset(CMAKE_CXX_COMPILER /usr/bin/arm-linux-gnueabi-g++)\n```\n\n7.  现在您可以构建和运行应用了。\n\n# 它是如何运作的..。\n\n我们需要一种机制来在应用挂起时终止它。 虽然我们可以产生一个特殊的监视线程或进程，但还有另一种更简单的方法-POSIX 信号。\n\n在 POSIX 操作系统中运行的任何进程都可以接收许多信号。 为了向进程传递信号，操作系统停止进程的正常执行，并调用相应的信号处理程序。\n\n可以传递给进程的信号之一称为`alarm`，默认情况下，它的处理程序只是终止应用。 这正是我们实现看门狗所需要的。\n\n我们的`Watchdog`类的构造函数接受一个参数`seconds`：\n\n```cpp\nWatchdog(std::chrono::seconds seconds):\n```\n\n这是我们的看门狗的时间间隔，它会立即传递到`feed`方法以激活看门狗定时器：\n\n```cpp\nfeed();\n```\n\n`feed`方法调用设置计时器的 POSIX 函数`alarm`。 如果已设置计时器，则会使用新值更新计时器：\n\n```cpp\nvoid feed() {\n  alarm(seconds.count());\n}\n```\n\n最后，我们在析构函数中调用相同的`alarm`函数，通过传递值`0`来禁用计时器：\n\n```cpp\nalarm(0);\n```\n\n现在，我们每次调用`feed`函数时，都会移动进程接收`alarm`信号的时间。 但是，如果我们在计时器到期之前没有调用此函数，它将触发`alarm`处理程序，从而终止我们的进程。\n\n为了检验这一点，我们创建了一个简单的示例。 这是一个有 10 次迭代的循环。 在每次迭代中，我们显示一条消息并休眠特定的时间间隔。 该间隔最初为 700 毫秒，在每次迭代中增加 300 毫秒；例如，700 毫秒、1,000 毫秒、1,300 毫秒，依此类推：\n\n```cpp\ndelay += 300ms;\n```\n\n我们的看门狗设置为 2 秒间隔：\n\n```cpp\nWatchdog watchdog(2s);\n```\n\n让我们运行应用并检查它是如何工作的。 它会生成以下输出：\n\n![](img/b755a216-3c0d-4381-9129-554f07f472ba.png)\n\n正如我们所看到的，应用在第六次迭代后终止，在延迟超过看门狗间隔之后。 而且，由于异常终止，其返回码为非零。 如果应用是由另一个应用或脚本生成的，则这是该应用需要重新启动的指示符。\n\n看门狗技术是构建健壮的嵌入式应用的一种简单而高效的方法。\n\n# 探索高可用性系统的心跳\n\n在前面的食谱中，我们了解了如何使用看门狗计时器防止软件挂起。 可以使用类似的技术来实现高可用性系统，该系统由可以执行相同功能的一个或多个软件或硬件组件组成。 如果其中一个组件出现故障，另一个组件可以接管。\n\n当前处于活动状态的组件应该使用称为**心跳**的消息定期向其他被动组件通告其健康状态。 当它报告不健康状态或在特定时间内没有报告时，无源组件会检测到它并激活它自己。 当故障组件恢复时，它可以转换到被动模式，监视当前主动组件的故障，或者启动回切过程来声明主动状态。\n\n在本食谱中，我们将学习如何在我们的应用中实现简单的心跳监控器。\n\n# 怎么做……\n\n我们将创建一个定义`Watchdog`类的应用，并提供其用法示例。 遵循以下步骤：\n\n1.  在您的工作目录(即`~/test`)中，创建一个名为`heartbeat`的子目录。\n2.  使用您喜欢的文本编辑器在`heartbeat`子目录中创建名为`heartbeat.cpp`的文件。\n3.  将所需的包含内容放入`heatbeat.cpp`文件中：\n\n```cpp\n#include <chrono>\n#include <iostream>\n#include <system_error>\n#include <thread>\n\n#include <unistd.h>\n#include <poll.h>\n#include <signal.h>\n\nusing namespace std::chrono_literals;\n```\n\n4.  接下来，我们定义一个`enum`来报告活动工作人员的健康状态：\n\n```cpp\nenum class Health : uint8_t {\n  Ok,\n  Unhealthy,\n  ShutDown\n};\n```\n\n5.  现在，让我们创建一个封装心跳报告和监视的类。 我们从类定义、它的私有字段和它的构造函数开始：\n\n```cpp\nclass Heartbeat {\n  int channel[2];\n  std::chrono::milliseconds delay;\n\n  public:\n    Heartbeat(std::chrono::milliseconds delay):\n        delay(delay) {\n      int rv = pipe(channel);\n      if (rv < 0) {\n        throw std::system_error(errno,         \n                                std::system_category(),\n                                \"Failed to open pipe\");\n      }\n    }\n\n```\n\n6.  接下来，我们添加一个报告健康状态的方法：\n\n```cpp\n    void Report(Health status) {\n      int rv = write(channel[1], &status, sizeof(status));\n      if (rv < 0) {\n        throw std::system_error(errno, \n                        std::system_category(),\n                        \"Failed to report health status\");\n      }\n    }\n```\n\n7.  紧随其后的是运行状况监视方法：\n\n```cpp\n    bool Monitor() {\n      struct pollfd fds[1];\n      fds[0].fd = channel[0];\n      fds[0].events = POLLIN;\n      bool takeover = true;\n      bool polling = true;\n      while(polling) {\n        fds[0].revents = 0;\n        int rv = poll(fds, 1, delay.count());\n        if (rv) {\n          if (fds[0].revents & (POLLERR | POLLHUP)) {\n            std::cout << \"Polling error occured\" \n                      << std::endl;\n            takeover = false;\n            polling = false;\n            break;\n          }\n\n          Health status;\n          int count = read(fds[0].fd, &status, \n                           sizeof(status));\n          if (count < sizeof(status)) {\n            std::cout << \"Failed to read heartbeat data\" \n                      << std::endl;\n            break;\n          }\n          switch(status) {\n            case Health::Ok:\n              std::cout << \"Active process is healthy\" \n                        << std::endl;\n              break;\n            case Health::ShutDown:\n              std::cout << \"Shut down signalled\" \n                        << std::endl;\n              takeover = false;\n              polling = false;\n              break;\n            default:\n              std::cout << \"Unhealthy status reported\" \n                        << std::endl;\n              polling = false;\n              break;\n          }\n        } else if (!rv) {\n          std::cout << \"Timeout\" << std::endl;\n          polling = false;\n        } else {\n          if (errno != EINTR) {\n            std::cout << \"Error reading heartbeat data, retrying\" << std::endl;\n          }\n        }\n      }\n      return takeover;\n    }\n};\n```\n\n8.  定义心跳逻辑后，我们将创建一些函数，以便可以在测试应用中使用它：\n\n```cpp\nvoid Worker(Heartbeat& hb) {\n  for (int i = 0; i < 5; i++) {\n    hb.Report(Health::Ok);\n    std::cout << \"Processing\" << std::endl;\n    std::this_thread::sleep_for(100ms);\n  }\n  hb.Report(Health::Unhealthy);\n}\n\nint main() {\n  Heartbeat hb(200ms);\n  if (fork()) {\n    if (hb.Monitor()) {\n      std::cout << \"Taking over\" << std::endl;\n      Worker(hb);\n    }\n  } else {\n    Worker(hb);\n  }\n}\n```\n\n9.  接下来，我们添加一个`CMakeLists.txt`文件，其中包含程序的构建规则：\n\n```cpp\ncmake_minimum_required(VERSION 3.5.1)\nproject(heartbeat)\nadd_executable(heartbeat heartbeat.cpp)\n\nset(CMAKE_SYSTEM_NAME Linux)\nset(CMAKE_SYSTEM_PROCESSOR arm)\n\nSET(CMAKE_CXX_FLAGS \"--std=c++ 14\")\n\nset(CMAKE_CXX_COMPILER /usr/bin/arm-linux-gnueabi-g++)\n```\n\n10.  现在您可以构建和运行应用了。\n\n# 它是如何运作的..。\n\n心跳机制需要某种通信通道来让一个组件向其他组件报告其状态。 在围绕多个处理单元构建的系统中，最佳选择是通过套接字进行基于网络的通信。 我们的应用在单个节点上运行，我们可以改用一种本地 IPC 机制。\n\n我们将使用 POSIX 管道机制来传输心跳。 创建管道时，它提供两个用于通信的文件描述符-一个用于读取数据，另一个用于写入数据。\n\n除了交通运输，我们还需要选择接手的时间间隔。 如果监视进程在此间隔内没有收到心跳消息，它应该将另一个组件视为不健康或出现故障，并执行一些接管操作。\n\n我们首先定义应用可能的运行状况。 我们使用 C++ `enum class`严格键入统计信息，如下所示：\n\n```cpp\nenum class Health : uint8_t {\n  Ok,\n  Unhealthy,\n  ShutDown\n};\n```\n\n我们的应用很简单，只有三种状态：`Ok`、`Unhealthy`和`ShutDown`。 `ShutDown`状态表示活动进程将正常关闭，不需要执行接管操作。\n\n然后，我们定义了`Heartbeat`类，它封装了所有消息交换、运行状况报告和监视功能。\n\n它有两个数据字段，分别表示监视时间间隔和用于消息交换的 POSIX 管道：\n\n```cpp\n  int channel[2];\n  std::chrono::milliseconds delay;\n```\n\n构造函数创建管道，并在发生故障时抛出异常：\n\n```cpp\n int rv = pipe(channel);\n      if (rv < 0) {\n        throw std::system_error(errno,         \n                                std::system_category(),\n                                \"Failed to open pipe\");\n\n```\n\n运行状况报告方法是`write`函数的简单包装。 它将状态(表示为无符号 8 位整数值)写入管道的`write`文件描述符：\n\n```cpp\nint rv = write(channel[1], &status, sizeof(status));\n```\n\n监测方法比较复杂。 它使用 POSIX`poll`函数等待一个或多个文件描述符中的数据。 在我们的示例中，我们只对来自一个文件描述符的数据感兴趣-管道的读取端。 我们用文件描述符和我们感兴趣的事件类型填充`pol`使用的`fds`结构：\n\n```cpp\n      struct pollfd fds[1];\n      fds[0].fd = channel[0];\n      fds[0].events = POLLIN | POLLERR | POLLHUP;\n```\n\n两个布尔标志控制轮询循环。 `takeover`标志表示退出循环时是否应该执行接管操作，而`polling`标志表示循环是否应该存在：\n\n```cpp\n      bool takeover = true;\n      bool polling = true;\n```\n\n在循环的每次迭代中，我们使用`poll`函数轮询套接字中的新数据。 我们使用传入构造函数的监视间隔作为轮询超时：\n\n```cpp\n        int rv = poll(fds, 1, delay.count());\n```\n\n`poll`函数的结果表示以下三种可能结果之一：\n\n*   如果它大于零，我们就有新的数据可以从通信管道中读取。 我们从通信通道读取状态并对其进行分析。\n*   如果状态为`Ok`，我们将其记入日志并进行下一次轮询。\n*   如果状态为`ShutDown`，我们需要退出轮询循环，但也要防止`takeover`操作。 为此，我们相应地设置布尔标志：\n\n```cpp\n            case Health::ShutDown:\n              std::cout << \"Shut down signalled\"\n                        << std::endl;\n takeover = false;\n polling = false;\n```\n\n对于任何其他健康状态，我们将中断循环，并将`takeover`标志设置为`true`：\n\n```cpp\n              std::cout << \"Unhealthy status reported\"\n                        << std::endl;\n polling = false;\n```\n\n`poll`在超时时返回零。 与`Unhealthy`状态类似，我们需要中断循环并执行`takeover`操作：\n\n```cpp\n        } else if (!rv) {\n          std::cout << \"Timeout\" << std::endl;\n          polling = false;\n```\n\n最后，如果`poll`返回的值小于零，则表示出错。 系统调用失败的原因有几个，其中一个非常常见的原因是它被信号中断。 这不是真正的错误；我们只需要再次调用`poll`。 对于所有其他情况，我们编写日志消息并保持轮询。\n\n监视方法在监视循环运行时阻塞，它返回一个布尔值，让调用者知道是否应该执行接管操作：\n\n```cpp\n bool Monitor() {\n```\n\n现在，让我们尝试在一个玩具示例中使用这个类。 我们将定义一个`Worker`函数，该函数接受对`Heartbeat`实例的引用，并表示要完成的工作：\n\n```cpp\nvoid Worker(Heartbeat& hb) {\n```\n\n在内部循环的每次迭代中，`Worker`报告其健康状态：\n\n```cpp\nhb.Report(Health::Ok);\n```\n\n在某一时刻，它将其状态报告为`Unhealthy`：\n\n```cpp\n  hb.Report(Health::Unhealthy);\n```\n\n在`main`函数中，我们创建了一个轮询间隔为 200 毫秒的`Heartbeat`类的实例：\n\n```cpp\n  Heartbeat hb(200ms);\n```\n\n然后，我们产生两个独立的进程。 父进程开始监视，如果需要接管，则运行`Worker`方法：\n\n```cpp\n    if (hb.Monitor()) {\n      std::cout << \"Taking over\" << std::endl;\n      Worker(hb);\n    }\n```\n\n子对象只需运行`Worker`方法。 让我们运行应用并检查它是如何工作的。 它会生成以下输出：\n\n![](img/d55f961e-37da-4689-8aa0-f0d9f2e9a02a.png)\n\n正如我们所看到的，`Worker`方法报告它处理数据，并且监视器检测到它的状态为健康。 但是，在`Worker`方法将其状态报告为`Unhealthy`之后，监视器会立即检测到它，并再次重新运行工作器以继续处理。 此策略可用于构建更精细的运行状况监视和故障恢复逻辑，以便在您设计和开发的系统中实现高可用性。\n\n# 还有更多的..。\n\n在我们的示例中，我们使用了两个相同的组件，它们同时运行并相互监视。 但是，如果其中一个组件包含在特定条件下导致该组件发生故障的软件错误，则另一个相同的组件也很有可能也会出现此问题。 在安全关键型系统中，您可能需要开发两个完全不同的实现。 这种方法增加了成本和开发时间，但提高了系统的可靠性。\n\n# 实现软件去抖动逻辑\n\n嵌入式应用的常见任务之一是与外部物理控件(如按钮或开关)交互。 虽然这样的物体只有两种状态-开和关-但检测按钮或开关改变状态的时刻并不像看起来那么简单。\n\n当按下物理按钮时，需要一段时间才能牢固地建立联系。 在此期间，可能会触发虚假中断，就好像按钮在打开和关闭状态之间跳跃一样。 应用应该能够过滤掉虚假的转换，而不是对每个中断做出反应。 这称为**去弹**。\n\n虽然它可以在硬件级别实现，但最常见的方法是通过软件来实现。 在本食谱中，我们将学习如何实现一个简单而通用的去抖动函数，该函数可以与任何类型的输入一起使用。\n\n# 怎么做……\n\n我们将创建一个应用，该应用定义一个带有测试输入的通用去抖动函数。 通过将测试输入替换为实际输入，此功能可用于任何实际目的。 遵循以下步骤：\n\n1.  在您的工作目录(即`~/test`)中，创建一个名为`debounce`的子目录。\n2.  使用您喜欢的文本编辑器在`debounce`子目录中创建名为`debounce.cpp`的文件。\n3.  让我们将 Includes 和一个名为`debounce`的函数添加到`debounce.cpp`文件：\n\n```cpp\n#include <iostream>\n#include <chrono>\n#include <thread>\n\nusing namespace std::chrono_literals;\n\nbool debounce(std::chrono::milliseconds timeout, bool (*handler)(void)) {\n  bool prev = handler();\n  auto ts = std::chrono::steady_clock::now();\n  while (true) {\n    std::this_thread::sleep_for(1ms);\n    bool value = handler();\n    auto now = std::chrono::steady_clock::now();\n    if (value == prev) {\n      if (now - ts > timeout) {\n        break;\n      }\n    } else {\n      prev = value;\n      ts = now;\n    }\n  }\n  return prev;\n}\n```\n\n4.  然后，我们添加`main`函数，该函数显示如何使用它：\n\n```cpp\nint main() {\n  bool result = debounce(10ms, []() {\n    return true;\n  });\n  std::cout << \"Result: \" << result << std::endl;\n}\n```\n\n5.  添加包含我们程序的构建规则的`CMakeLists.txt`文件：\n\n```cpp\ncmake_minimum_required(VERSION 3.5.1)\nproject(debounce)\nadd_executable(debounce debounce.cpp)\n\nset(CMAKE_SYSTEM_NAME Linux)\nset(CMAKE_SYSTEM_PROCESSOR arm)\n\nSET(CMAKE_CXX_FLAGS \"--std=c++ 14\")\n\nset(CMAKE_CXX_COMPILER /usr/bin/arm-linux-gnueabi-g++)\n```\n\n6.  现在您可以构建和运行应用了。\n\n# 它是如何运作的..。\n\n我们的目标是检测按钮何时在打开和关闭状态之间停止跳动。 我们假设，如果在特定时间间隔内所有连续读取按钮状态的尝试都返回相同的值(打开或关闭)，我们就可以判断按钮是真正打开还是关闭。\n\n我们使用此逻辑来实现`debounce`函数。 由于我们希望使去抖动逻辑尽可能通用，因此该函数不应该知道如何读取按钮的状态。 这就是该函数接受两个参数的原因：\n\n```cpp\nbool debounce(std::chrono::milliseconds timeout, bool (*handler)(void)) {\n```\n\n第一个参数`timeout`定义了报告状态更改需要等待的特定时间间隔。 第二个参数`handler`是一个函数或类似函数的对象，它知道如何读取按钮的状态。 它被定义为指向不带参数的布尔函数的指针。\n\n函数的作用是：运行一个循环。 在每次迭代中，它调用处理程序来读取按钮的状态，并将其与前一个值进行比较。 如果值相等，则检查自最近一次状态更改以来的时间。 如果超过超时，我们将退出循环并返回：\n\n```cpp\nauto now = std::chrono::steady_clock::now();\n    if (value == prev) {\n      if (now - ts > timeout) {\n        break;\n      }\n```\n\n如果值不相等，我们将重置最近状态更改的时间并继续等待：\n\n```cpp\n} else {\n      prev = value;\n      ts = now;\n    }\n```\n\n为了最小化 CPU 负载并让其他进程执行一些工作，我们在两次读取之间添加了 1 毫秒的延迟。 如果该功能打算在不运行多任务操作系统的微控制器上使用，则不需要此延迟：\n\n```cpp\nstd::this_thread::sleep_for(1ms);\n```\n\n我们的`main`函数包含一个`debounce`函数的用法示例。 我们使用 C++ lambda 定义读取按钮的简单规则。 它始终返回`true`：\n\n```cpp\n  bool result = debounce(10ms, []() {\n return true;\n });\n```\n\n我们将`10ms`作为`debounce`超时传递。 如果我们运行我们的程序，我们将看到以下输出：\n\n![](img/15406ee1-e262-4eca-b5d6-f744a0738e85.png)\n\n`debounce`函数工作 10 毫秒并返回`true`，因为测试输入中没有虚假状态变化。 在实际输入的情况下，按钮状态可能需要更多时间才能稳定下来。 这种简单而有效的去抖动函数可以应用于各种实际输入。"
  },
  {
    "path": "docs/emb-prog-mod-cpp-cb/13.md",
    "content": "# 十三、实时系统指南\n\n实时系统是一类反应时间很关键的嵌入式系统。不及时反应的后果因不同的应用而异。根据严重性，实时系统分为以下几类:\n\n*   **硬实时**:错过期限不可接受，视为系统故障。这些通常是飞机、汽车和发电厂中的关键任务系统。\n*   **确定实时**:错过截止日期在极少数情况下是可以接受的。截止日期后，结果的有用性为零。想想直播服务。视频帧传送太晚只能丢弃。只要不经常发生，这是可以容忍的。\n*   **软实时**:错过期限可以接受。结果的有用性在截止日期后下降，导致整体质量下降，应该避免。这样的例子是捕获和同步来自多个传感器的数据。\n\n实时系统不一定要求超快。他们需要的是可预测的反应时间。如果一个系统可以在 10 毫秒内正常响应一个事件，但通常需要更长的时间，那么它就不是一个实时系统。如果系统保证在 1 秒内响应，这就构成了硬实时。\n\n确定性和可预测性是实时系统的主要特征。在这一章中，我们将探索不可预测行为的潜在来源以及减轻它们的方法。\n\n本章涵盖以下主题:\n\n*   在 Linux 中使用实时调度器\n*   使用静态分配的内存\n*   避免错误处理的异常\n*   探索实时操作系统\n\n本章中的方法将帮助您更好地理解实时系统的细节，并学习这种嵌入式系统的软件开发的一些最佳实践。\n\n# 在 Linux 中使用实时调度器\n\nLinux 是一个通用的操作系统，由于它的多功能性，被广泛应用于各种嵌入式设备中。它可以根据特定的硬件进行定制，并且是免费的。\n\nLinux 不是实时操作系统，也不是实现硬实时系统的最佳选择。但是，它可以有效地用于构建软实时系统，因为它为时间关键型应用提供了实时调度程序。\n\n在这个食谱中，我们将学习如何在我们的应用中使用 Linux 中的实时调度程序。\n\n# 怎么做...\n\n我们将创建一个使用实时调度程序的应用:\n\n1.  在工作目录`~/test`中，创建一个名为`realtime`的子目录。\n2.  使用您喜欢的文本编辑器在`realtime`子目录中创建一个`realtime.cpp`文件。\n3.  添加所有必要的包含和命名空间:\n\n```cpp\n#include <iostream>\n#include <system_error>\n#include <thread>\n#include <chrono>\n\n#include <pthread.h>\n\nusing namespace std::chrono_literals;\n```\n\n4.  接下来，添加一个配置线程以使用实时调度程序的函数:\n\n```cpp\nvoid ConfigureRealtime(pthread_t thread_id, int priority) {\n    sched_param sch;\n    sch.sched_priority = 20;\n    if (pthread_setschedparam(thread_id,\n                              SCHED_FIFO, &sch)) {\n        throw std::system_error(errno, \n                std::system_category(),\n                \"Failed to set real-time priority\");\n    }\n}\n```\n\n5.  接下来，我们定义一个线程函数，我们希望它以正常优先级运行:\n\n```cpp\nvoid Measure(const char* text) {\n    struct timespec prev;\n    timespec_get(&prev, TIME_UTC);\n    struct timespec delay{0, 10};\n    for (int i = 0; i < 100000; i++) {\n      nanosleep(&delay, nullptr);\n    }\n    struct timespec ts;\n    timespec_get(&ts, TIME_UTC);\n    double delta = (ts.tv_sec - prev.tv_sec) + \n        (double)(ts.tv_nsec - prev.tv_nsec) / 1000000000;\n    std::clog << text << \" completed in \" \n              << delta << \" sec\" << std::endl;\n}\n```\n\n6.  接下来是实时线程函数和启动两个线程的`main`函数:\n\n```cpp\nvoid RealTimeThread(const char* txt) {\n    ConfigureRealtime(pthread_self(), 1);\n    Measure(txt);\n}\n\nint main() {\n    std::thread t1(RealTimeThread, \"Real-time\");\n    std::thread t2(Measure, \"Normal\");\n    t1.join();\n    t2.join();\n}\n```\n\n7.  最后，我们创建一个包含程序构建规则的`CMakeLists.txt`文件:\n\n```cpp\ncmake_minimum_required(VERSION 3.5.1)\nproject(realtime)\nadd_executable(realtime realtime.cpp)\ntarget_link_libraries(realtime pthread)\n\nSET(CMAKE_CXX_FLAGS \"--std=c++ 14\") \nset(CMAKE_CXX_COMPILER /usr/bin/arm-linux-gnueabihf-g++)\n```\n\n8.  现在，您可以构建和运行该应用。\n\n# 它是如何工作的...\n\nLinux 有几个应用于应用进程和线程的调度策略。`SCHED_OTHER`是默认的 Linux 分时策略。它面向所有线程，不提供实时机制。\n\n在我们的应用中，我们使用另一个策略`SCHED_FIFO`。这是一个简单的调度算法。使用此调度程序的所有线程只能被具有更高优先级的线程抢占。如果线程进入睡眠状态，它将被放在具有相同优先级的线程队列的后面。\n\n具有`SCHED_FIFO`策略的线程的优先级总是高于具有`SCHED_OTHER`策略的任何线程的优先级，并且一旦`SCHED_FIFO`线程变得可运行，它就立即抢占正在运行的`SCHED_OTHER`线程。从实用的角度来看，如果系统中只有一个`SCHED_FIFO`线程在运行，它可以根据需要使用尽可能多的 CPU 时间。`SCHED_FIFO`调度器的确定性行为和高优先级使其非常适合实时应用。\n\n为了给线程分配实时优先级，我们定义了一个`ConfigureRealtime`函数。这接受两个参数——线程标识和所需的优先级:\n\n```cpp\nvoid ConfigureRealtime(pthread_t thread_id, int priority) {\n```\n\n该函数为`pthread_setschedparam`函数填充数据，该函数使用操作系统的低级应用编程接口来更改调度程序和线程的优先级:\n\n```cpp\n    if (pthread_setschedparam(thread_id,\n SCHED_FIFO, &sch)) {\n```\n\n我们定义了一个`Measure`函数，它运行一个繁忙的循环，调用一个`nanosleep`函数，参数要求它休眠 10 纳秒——时间太短，无法让另一个线程执行:\n\n```cpp\n    struct timespec delay{0, 10};\n    for (int i = 0; i < 100000; i++) {\n      nanosleep(&delay, nullptr);\n    }\n```\n\n该函数捕获循环前后的时间戳，并以秒为单位计算经过的时间:\n\n```cpp\n    struct timespec ts;\n    timespec_get(&ts, TIME_UTC);\n    double delta = (ts.tv_sec - prev.tv_sec) + \n        (double)(ts.tv_nsec - prev.tv_nsec) / 1000000000;\n```\n\n接下来，我们将`RealTimeThread`函数定义为`Measure`函数的包装器。这会将当前线程的优先级设置为实时，并立即调用`Measure`:\n\n```cpp\n    ConfigureRealtime(pthread_self(), 1);\n    Measure(txt);\n```\n\n在`main`函数中，我们启动两个线程，传递文本作为参数来区分它们的输出。如果我们在树莓 Pi 设备上运行该程序，我们可以看到以下输出:\n\n![](img/56a567f4-a5ee-43ce-8a70-8e6471fc11d3.png)\n\n实时线程花费的时间少了四倍，因为这不会被普通线程抢占。这种技术可以有效地用于满足 Linux 环境中的软实时要求。\n\n# 使用静态分配的内存\n\n正如在[第 6 章](06.html) *【内存管理】*中已经讨论过的，在实时系统中应该避免动态内存分配，因为通用内存分配器没有时间限制。虽然在大多数情况下，内存分配不会花费太多时间，但也不能保证。这对于实时系统是不可接受的。\n\n避免动态内存分配最直接的方法是用静态分配来代替。C++ 开发人员经常使用`std::vector`来存储元素序列。由于它与 C 数组的相似性，它高效且易于使用，并且它的接口与标准库中的其他容器一致。由于向量具有可变数量的元素，因此它们广泛使用动态内存分配。然而，在许多情况下，可以使用`std::array`类来代替`std::vector`。它有相同的接口，只是它的元素数量是固定的，所以它的实例可以静态分配。这使得它成为内存分配时间至关重要时`std::vector`的良好替代品。\n\n在本食谱中，我们将学习如何有效地使用`std::array`来表示固定大小的元素序列。\n\n# 怎么做...\n\n我们将创建一个应用，它使用 C++ 标准库算法的能力来生成和处理固定数据帧，而不使用动态内存分配:\n\n1.  在工作目录`~/test`中，创建一个名为`array`的子目录。\n2.  使用您喜欢的文本编辑器在`array`子目录中创建一个`array.cpp`文件。\n3.  向`array.cpp`文件添加包括和新类型定义:\n\n```cpp\n#include <algorithm>\n#include <array>\n#include <iostream>\n#include <random>\n\nusing DataFrame = std::array<uint32_t, 8>;\n```\n\n4.  接下来，我们添加一个生成数据帧的函数:\n\n```cpp\nvoid GenerateData(DataFrame& frame) {\n  std::random_device rd;\n std::generate(frame.begin(), frame.end(),\n [&rd]() { return rd() % 100; });\n}\n```\n\n5.  接下来是处理数据帧的函数:\n\n```cpp\nvoid ProcessData(const DataFrame& frame) {\n  std::cout << \"Processing array of \"\n            << frame.size() << \" elements: [\";\n  for (auto x : frame) {\n    std::cout << x << \" \";\n  }\n  auto mm = std::minmax_element(frame.begin(),frame.end());\n  std::cout << \"] min: \" << *mm.first\n            << \", max: \" << *mm.second << std::endl;\n}\n```\n\n6.  添加一个将数据生成和处理联系在一起的`main`功能:\n\n```cpp\nint main() {\n  DataFrame data;\n\n  for (int i = 0; i < 4; i++) {\n    GenerateData(data);\n    ProcessData(data);\n  }\n  return 0;\n}\n```\n\n7.  最后，我们创建一个包含程序构建规则的`CMakeLists.txt`文件:\n\n```cpp\ncmake_minimum_required(VERSION 3.5.1)\nproject(array)\nadd_executable(array array.cpp)\n\nset(CMAKE_SYSTEM_NAME Linux)\nset(CMAKE_SYSTEM_PROCESSOR arm)\n\nSET(CMAKE_CXX_FLAGS_RELEASE \"--std=c++ 17\") \nSET(CMAKE_CXX_FLAGS_DEBUG \"${CMAKE_CXX_FLAGS_RELEASE} -g -DDEBUG\") \n\nset(CMAKE_C_COMPILER /usr/bin/arm-linux-gnueabihf-gcc)\nset(CMAKE_CXX_COMPILER /usr/bin/arm-linux-gnueabihf-g++)\n\nset(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)\nset(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)\nset(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)\nset(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)\n```\n\n8.  现在，您可以构建和运行该应用。\n\n# 它是如何工作的...\n\n我们使用`std::array`模板来声明一个自定义的`DataFrame`数据类型。对于我们的示例应用，一个`DataFrame`是八个 32 位整数的序列:\n\n```cpp\nusing DataFrame = std::array<uint32_t, 8>;\n```\n\n现在，我们可以在函数中使用新的数据类型来生成和处理数据帧。由于数据帧是一个数组，我们通过引用`GenerateData`函数来传递它，以避免额外的复制:\n\n```cpp\nvoid GenerateData(DataFrame& frame) {\n```\n\n`GenerateData`用随机数填充数据帧。由于`std::array`与标准库中的其他容器具有相同的接口，我们可以使用标准算法使代码更短、可读性更强:\n\n```cpp\n std::generate(frame.begin(), frame.end(),\n [&rd]() { return rd() % 100; });\n```\n\n我们以类似的方式定义`ProcessData`函数。它也接受一个`DataFrame`，但它不应该修改它。我们使用常量引用来明确声明数据不会被修改:\n\n```cpp\nvoid ProcessData(const DataFrame& frame) {\n```\n\n`ProcessData`打印数据帧中的所有值，然后找到帧中的最小值和最大值。与内置数组不同，`std::arrays`在传递给函数时不会衰减为原始指针，因此我们可以使用基于范围的循环语法。您可能会注意到，我们没有将数组的大小传递给函数，也没有使用任何全局常数来查询它。它是`std::array`界面的一部分。它不仅减少了函数的参数数量，还确保我们在调用它时不会传递不正确的大小:\n\n```cpp\n  for (auto x : frame) {\n    std::cout << x << \" \";\n  }\n```\n\n为了找到最小值和最大值，我们使用标准库的`std::minmax_`元素函数，而不是编写自定义循环:\n\n```cpp\nauto mm = std::minmax_element(frame.begin(),frame.end());\n```\n\n在`main`函数中，我们创建了一个`DataFrame`的实例:\n\n```cpp\nDataFrame data;\n```\n\n然后，我们运行一个循环。每次迭代时，都会生成并处理一个新的数据帧:\n\n```cpp\nGenerateData(data);\nProcessData(data);\n```\n\n如果我们运行应用，我们会得到以下输出:\n\n![](img/5443008c-9e80-4ed5-818e-9b2df50b60c6.png)\n\n我们的应用生成了四个数据帧，并且只使用几行代码和静态分配的数据来处理数据。这使得`std::array`成为实时系统开发者的好选择。此外，与内置数组不同，我们的函数是类型安全的，我们可以在构建时检测并修复许多编码错误。\n\n# 还有更多...\n\nC++ 20 标准引入了一个新的函数`to_array`，允许开发人员从一维内置数组中创建`std::array`的实例。详见`to_array`参考页面([https://en.cppreference.com/w/cpp/container/array/to_array](https://en.cppreference.com/w/cpp/container/array/to_array))中的更多细节和示例。\n\n# 避免错误处理的异常\n\n异常机制是 C++ 标准不可分割的一部分。这是在 C++ 程序中设计错误处理的推荐方法。然而，它确实有一些限制，并不总是能让它被实时系统所接受，尤其是安全关键的系统。\n\nC++ 异常处理在很大程度上依赖于堆栈展开。一旦抛出异常，它就会通过调用堆栈向上传播到可以处理它的 catch 块。这意味着调用其路径中所有堆栈帧中所有本地对象的析构函数，很难确定和正式证明这个过程的最坏情况时间。\n\n这就是为什么安全关键系统的编码指南，如 MISRA 或 JSF，明确禁止使用异常进行错误处理。\n\n这并不意味着 C++ 开发人员必须回到传统的纯 C 错误代码。在本食谱中，我们将学习如何使用 C++ 模板来定义可以保存函数调用的结果或错误代码的数据类型。\n\n# 怎么做...\n\n我们将创建一个应用，它使用 C++ 标准库算法的能力来生成和处理固定数据帧，而不使用动态内存分配:\n\n1.  在工作目录`~/test`中，创建一个名为`expected`的子目录。\n2.  使用您喜欢的文本编辑器在`expected`子目录中创建一个`expected.cpp`文件。\n3.  向`expected.cpp`文件添加包括和新类型定义:\n\n```cpp\n#include <iostream>\n#include <system_error>\n#include <variant>\n\n#include <unistd.h>\n#include <sys/fcntl.h>\n\ntemplate <typename T>\nclass Expected {\n  std::variant<T, std::error_code> v;\n\npublic:\n  Expected(T val) : v(val) {}\n  Expected(std::error_code e) : v(e) {}\n\n  bool valid() const {\n    return std::holds_alternative<T>(v);\n  }\n\n  const T& value() const {\n    return std::get<T>(v);\n  }\n\n  const std::error_code& error() const {\n    return std::get<std::error_code>(v);\n  }\n};\n```\n\n4.  接下来，我们为 open POSIX 函数添加一个包装器:\n\n```cpp\nExpected<int> OpenForRead(const std::string& name) {\n  int fd = ::open(name.c_str(), O_RDONLY);\n  if (fd < 0) {\n    return Expected<int>(std::error_code(errno, \n                         std::system_category()));\n  }\n  return Expected<int>(fd);\n}\n```\n\n5.  添加显示如何使用`OpenForRead`包装器的`main`功能:\n\n```cpp\nint main() {\n  auto result = OpenForRead(\"nonexistent.txt\");\n  if (result.valid()) {\n    std::cout << \"File descriptor\"\n              << result.value() << std::endl;\n  } else {\n    std::cout << \"Open failed: \" \n              << result.error().message() << std::endl;\n  }\n  return 0;\n}\n```\n\n6.  最后，我们创建一个包含程序构建规则的`CMakeLists.txt`文件:\n\n```cpp\ncmake_minimum_required(VERSION 3.5.1)\nproject(expected)\nadd_executable(expected expected.cpp)\n\nset(CMAKE_SYSTEM_NAME Linux)\n#set(CMAKE_SYSTEM_PROCESSOR arm)\n\nSET(CMAKE_CXX_FLAGS \"--std=c++ 17\") \n\n#set(CMAKE_C_COMPILER /usr/bin/arm-linux-gnueabihf-gcc)\n#set(CMAKE_CXX_COMPILER /usr/bin/arm-linux-gnueabihf-g++)\n\nset(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)\nset(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)\nset(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)\nset(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)\n```\n\n7.  现在，您可以构建和运行该应用。\n\n# 它是如何工作的...\n\n在我们的应用中，我们创建了一个数据类型，它可以以类型安全的方式保存期望值或错误代码。C++ 17 提供了一个类型安全的联合类`std::variant,`，我们将把它用作模板类`Expected`的基础数据类型。\n\n`Expected`类封装了一个`std::variant`字段，该字段可以保存两种数据类型之一，模板化类型`T`或`std::error_code`，这是错误代码的标准 C++ 推广:\n\n```cpp\n  std::variant<T, std::error_code> v;\n```\n\n虽然可以直接使用`std::variant`工作，但是我们公开了一些让它更方便的公共方法。如果结果保持模板化类型，则`valid`方法返回`true`，否则返回`false`:\n\n```cpp\n  bool valid() const {\n    return std::holds_alternative<T>(v);\n  }\n```\n\n`value`和`error`方法分别用于访问返回值或错误代码:\n\n```cpp\n  const T& value() const {\n    return std::get<T>(v);\n  }\n\n  const std::error_code& error() const {\n    return std::get<std::error_code>(v);\n  }\n```\n\n一旦定义了`Expected`类，我们就创建一个使用它的`OpenForReading`函数。这将调用开放系统函数，并基于返回值创建一个保存文件描述符或错误代码的`Expected`实例:\n\n```cpp\n  if (fd < 0) {\n    return Expected<int>(std::error_code(errno, \n std::system_category()));\n  }\n  return Expected<int>(fd);\n```\n\n在`main`函数中，当我们对不存在的文件调用`OpenForReading`时，预计会失败。当我们运行应用时，我们可以看到以下输出:\n\n![](img/60b67f21-dfee-4227-8fa9-b3367d95f288.png)\n\n我们的`Expected`类允许我们编写可能返回错误代码的函数，并以类型安全的方式执行。编译时类型验证帮助开发人员避免了许多传统错误代码常见的问题，使我们的应用更加健壮和安全。\n\n# 还有更多...\n\n我们对`Expected`数据类型的实现是`std::expected`类的变体([http://www . open-STD . org/JT C1/sc22/wg21/docs/papers/2018/p 0323 r 7 . html](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0323r7.html))被提议标准化，但未被批准。`std::expected`的一个实现可以在[https://github.com/TartanLlama/expected](https://github.com/TartanLlama/expected)的 GitHub 上找到。\n\n# 探索实时操作系统\n\n正如本章已经讨论过的，Linux 不是一个实时系统。对于软实时任务来说，这是一个不错的选择，但尽管它提供了实时调度器，但其内核过于复杂，无法保证硬实时应用所需的确定性水平。\n\n时间关键型应用要么需要实时操作系统才能运行，要么设计和实现为在裸机上运行，根本没有操作系统。\n\n实时操作系统通常比通用操作系统如 Linux 简单得多。此外，它们需要针对特定的硬件平台进行定制，通常是微控制器。\n\n有许多实时操作系统，其中大多数是专有的，而不是免费的。FreeRTOS 是探索实时操作系统功能的良好起点。与大多数替代方案不同，它是开源的，可以免费使用，因为它是在麻省理工学院许可下发布的。它被移植到许多微控制器和小型微处理器上，但是即使您没有特定的硬件，Windows 和 POSIX 模拟器也是可用的。\n\n在这个食谱中，我们将学习如何下载和运行 FreeRTOS POSIX 模拟器。\n\n# 怎么做...\n\n我们将在构建环境中下载并构建一个 FreeRTOS 模拟器:\n\n1.  切换到你的 Ubuntu 终端，将当前目录改为`/mnt`:\n\n```cpp\n$ cd /mnt\n```\n\n2.  下载自由操作系统模拟器的源代码:\n\n```cpp\n$ wget -O simulator.zip http://interactive.freertos.org/attachments/token/r6d5gt3998niuc4/?name=Posix_GCC_Simulator_6.0.4.zip\n```\n\n3.  提取下载的档案:\n\n```cpp\n$ unzip simulator.zip\n```\n\n4.  将当前目录更改为`Posix_GCC_Simulator/FreeRTOS_Posix/Debug`:\n\n```cpp\n$ cd Posix_GCC_Simulator/FreeRTOS_Posix/Debug\n```\n\n5.  通过运行以下命令修复`makefile`中的小错误:\n\n```cpp\n$ sed -i -e 's/\\(.*gcc.*\\)-lrt\\(.*\\)/\\1\\2 -lrt/' makefile\n```\n\n6.  从源代码构建模拟器:\n\n```cpp\n$ make\n```\n\n7.  开始吧:\n\n```cpp\n$ ./FreeRTOS_Posix\n```\n\n此时，模拟器正在运行。\n\n# 它是如何工作的...\n\n我们已经知道，实时操作系统的内核通常比通用操作系统的内核简单得多。FreeRTOS 也是如此。\n\n由于这种简单性，内核可以作为通用操作系统(如 Linux 或 Windows)中的进程来构建和运行。当从另一个操作系统中使用时，它不再是真正的实时，而是可以作为一个起点来探索自由操作系统应用编程接口，并开始开发以后可以在目标硬件平台的实时环境中运行的应用。\n\n在这个食谱中，我们为 POSIX 操作系统下载并构建了 FreeRTOS 内核。\n\n构建阶段很简单。一旦代码被下载并从档案中提取出来，我们运行`make`，这就构建了一个可执行文件`FreeRTOS-POSIX`。在运行`make`命令之前，我们通过在 GCC 命令行的末尾放置`-lrt`选项来修复`makefile`中的一个错误。我们通过运行`sed`来做到这一点:\n\n```cpp\n$ sed -i -e 's/\\(.*gcc.*\\)-lrt\\(.*\\)/\\1\\2 -lrt/' makefile\n```\n\n运行应用会启动内核和预打包的应用:\n\n![](img/592082ae-35ae-405e-8d7d-fefe26872dae.png)\n\n我们能够在构建环境中运行自由操作系统。您可以更深入地了解它的代码库和文档，以更好地理解实时操作系统的内部和 API。\n\n# 还有更多...\n\n如果你在 Windows 环境下工作，有一个更好支持的 Windows 版本的 FreeRTOS 模拟器。可以从[https://www . FreeRTOS . org/FreeRTOS-Windows-Simulator-Emulator-for-Visual Studio-and-Eclipse-mingw . html](https://www.freertos.org/FreeRTOS-Windows-Simulator-Emulator-for-Visual-Studio-and-Eclipse-MingW.html)下载，附带文档和教程。"
  },
  {
    "path": "docs/emb-prog-mod-cpp-cb/14.md",
    "content": "# 十四、安全关键系统指南\n\n嵌入式系统对代码质量的要求通常高于其他软件领域。由于许多嵌入式系统不需要监控昂贵的工业设备，因此出错的代价很高。在软件或硬件故障可能导致人员受伤甚至死亡的安全关键系统中，该风险甚至更高。此类系统的软件必须遵循特定的指导原则，旨在最大限度地减少调试和测试阶段未发现错误的机会。\n\n在本章中，我们将通过以下方法探索安全关键系统的一些要求和最佳实践:\n\n*   使用所有函数的返回值\n*   使用静态代码分析器\n*   使用前置条件和后置条件\n*   探索代码正确性的形式验证\n\n这些方法将帮助您理解安全关键系统的要求和指南，以及用于认证和一致性测试的工具和方法。\n\n# 使用所有函数的返回值\n\nC 或 C++ 语言都不要求开发人员使用任何函数返回的值。定义一个返回整数的函数，然后在代码中调用它，忽略它的返回值，这是完全可以接受的。\n\n这种灵活性通常会导致难以诊断和修复的软件错误。最常见的情况是，函数返回错误代码。开发人员可能会忘记为经常使用且很少失败的函数添加错误条件检查，例如`close`。\n\n安全关键系统最广泛使用的编码标准之一是 MISRA。它定义了对 C 和 C++ 语言的要求——分别是 MISRA C 和 MISRA C++。最近推出的 Adaptive AUTOSAR 定义了汽车行业的编码指南。预计在不久的将来，自适应 AUTOSAR 指南将被用作更新后的 MISRA C++ 指南的基础。\n\n针对 C++ 的 MISRA 和 AUTOSAR 编码指南([https://www . AUTOSAR . org/file admin/user _ upload/standards/adaptive/17-03/AUTOSAR _ RS _ CPP 14 guidelines . pdf](https://www.autosar.org/fileadmin/user_upload/standards/adaptive/17-03/AUTOSAR_RS_CPP14Guidelines.pdf))都要求开发人员使用所有非 void 函数和方法返回的值。相应的规则定义如下:\n\n\"Rule A0-1-2 (required, implementation, automated): The value returned by a function having a non-void return type that is not an overloaded operator shall be used.\"\n\n在这个食谱中，我们将学习如何在我们的代码中使用这个规则。\n\n# 怎么做...\n\n我们将创建两个类，在一个文件中保存两个时间戳。一个时间戳指示实例创建的时间，而另一个时间戳指示实例销毁的时间。这对于代码分析非常有用，可以测量我们在一个函数或任何其他感兴趣的代码块中花费了多少时间。请遵循以下步骤:\n\n1.  在您的工作目录中，即`~/test`，创建一个名为`returns`的子目录。\n2.  使用您喜欢的文本编辑器在`returns`子目录中创建一个名为`returns.cpp`的文件。\n3.  将第一个类添加到`returns.cpp`文件中:\n\n```cpp\n#include <system_error>\n\n#include <unistd.h>\n#include <sys/fcntl.h>\n#include <time.h>\n\n[[nodiscard]] ssize_t Write(int fd, const void* buffer,\n                            ssize_t size) {\n  return ::write(fd, buffer, size);\n}\n\nclass TimeSaver1 {\n  int fd;\n\npublic:\n  TimeSaver1(const char* name) {\n    int fd = open(name, O_RDWR|O_CREAT|O_TRUNC, 0600);\n    if (fd < 0) {\n      throw std::system_error(errno,\n                              std::system_category(),\n                              \"Failed to open file\");\n    }\n    Update();\n  }\n\n  ~TimeSaver1() {\n    Update();\n    close(fd);\n  }\n\nprivate:\n  void Update() {\n    time_t tm;\n    time(&tm);\n    Write(fd, &tm, sizeof(tm));\n  }\n};\n```\n\n4.  接下来，我们添加第二个类:\n\n```cpp\nclass TimeSaver2 {\n  int fd;\n\npublic:\n  TimeSaver2(const char* name) {\n    fd = open(name, O_RDWR|O_CREAT|O_TRUNC, 0600);\n    if (fd < 0) {\n      throw std::system_error(errno,\n                              std::system_category(),\n                              \"Failed to open file\");\n    }\n    Update();\n  }\n\n  ~TimeSaver2() {\n    Update();\n    if (close(fd) < 0) {\n      throw std::system_error(errno,\n                              std::system_category(),\n                              \"Failed to close file\");\n    }\n  }\n\nprivate:\n  void Update() {\n    time_t tm = time(&tm);\n    int rv = Write(fd, &tm, sizeof(tm));\n    if (rv < 0) {\n      throw std::system_error(errno,\n                              std::system_category(),\n                              \"Failed to write to file\");\n    }\n  }\n};\n```\n\n5.  `main`函数创建两个类的实例:\n\n```cpp\nint main() {\n  TimeSaver1 ts1(\"timestamp1.bin\");\n  TimeSaver2 ts2(\"timestamp2.bin\");\n  return 0;\n}\n```\n\n6.  最后，我们创建一个包含程序构建规则的`CMakeLists.txt`文件:\n\n```cpp\ncmake_minimum_required(VERSION 3.5.1)\nproject(returns)\nadd_executable(returns returns.cpp)\n\nset(CMAKE_SYSTEM_NAME Linux)\nset(CMAKE_SYSTEM_PROCESSOR arm)\n\nSET(CMAKE_CXX_FLAGS \"--std=c++ 17\")\nset(CMAKE_CXX_COMPILER /usr/bin/arm-linux-gnueabi-g++)\n```\n\n7.  现在，您可以构建和运行该应用。\n\n# 它是如何工作的...\n\n我们现在已经创建了两个类，`TimeSaver1`和`TimeSaver2`，它们看起来几乎相同，做着相同的工作。两个类都在其构造函数中打开一个文件，并调用`Update`函数，该函数将时间戳写入打开的文件。\n\n类似地，它们的析构函数调用相同的`Update`函数来添加第二个时间戳并关闭文件描述符。\n\n然而`TimeSaver1`打破了 *A0-1-2* 规则，不安全。让我们仔细看看这个。其`Update`功能调用两个功能，`time`和`write`。这两个函数都可能失败，返回正确的错误代码，但是我们的实现忽略了它:\n\n```cpp\n    time(&tm);\n    Write(fd, &tm, sizeof(tm));\n```\n\n此外，`TimeSaver1`的析构函数通过调用`close`函数关闭打开的文件。这也可能失败，返回一个我们忽略的错误代码:\n\n```cpp\n    close(fd);\n```\n\n第二类`TimeSaver2`符合要求。我们将时间调用的结果分配给`tm`变量:\n\n```cpp\n    time_t tm = time(&tm);\n```\n\n如果`Write`返回错误，我们抛出异常:\n\n```cpp\n    int rv = Write(fd, &tm, sizeof(tm));\n    if (rv < 0) {\n      throw std::system_error(errno,\n                              std::system_category(),\n                              \"Failed to write to file\");\n    }\n```\n\n同样，如果`close`返回错误，我们抛出异常:\n\n```cpp\n    if (close(fd) < 0) {\n      throw std::system_error(errno,\n                              std::system_category(),\n                              \"Failed to close file\");\n    }\n```\n\n为了缓解这种问题，C++ 17 标准引入了一个名为`[[nodiscard]]`的特殊属性。如果一个函数用这个属性声明，或者它返回一个标记为`nodiscard`的类或枚举，如果它的返回值被丢弃，编译器应该显示一个警告。为了使用这个特性，我们在`write`函数周围创建了一个自定义包装器，并将其声明为`nodiscard`:\n\n```cpp\n[[nodiscard]] ssize_t Write(int fd, const void* buffer,\n                            ssize_t size) {\n  return ::write(fd, buffer, size);\n}\n```\n\n当我们构建应用时，我们可以在编译器输出中看到这一点，这也意味着我们有机会修复它:\n\n![](img/0d3ff757-ae38-48be-b05c-de4b55b2ed2c.png)\n\n事实上，编译器能够识别并报告我们代码中的另一个问题，我们将在下一个配方中讨论。\n\n如果我们构建并运行该应用，我们将看不到任何输出，因为所有的写入都指向文件。我们可以运行`ls`命令来检查程序是否产生结果，如下所示:\n\n```cpp\n$ ls timestamp*\n```\n\n由此，我们得到以下输出:\n\n![](img/bad36a2a-3f7f-40a6-855b-345fba095e31.png)\n\n不出所料，我们的程序创建了两个文件。它们应该是相同的，但它们不是。`TimeSaver1`创建的文件为空，说明其实现有问题。\n\n`TimeSaver2`生成的文件是有效的，但这是否意味着其实现是 100%正确的？不一定，我们将在下一个食谱中看到。\n\n# 还有更多...\n\n更多关于`[[nodiscard]]`属性的信息可以在它的参考页面上找到。从 C++ 20 开始，`nodiscard`属性可以包含一个字符串文字，解释为什么值不应该被丢弃；例如`[[nodiscard(\"Check for write errors\")]]`。\n\n重要的是要理解，遵守安全准则确实会使您的代码更安全，但并不能保证它。在`TimeSaver2`的实现中，我们使用`time`返回的值，但是不检查它是否有效。相反，我们无条件地写入输出文件。同样，如果`write`返回一个非零数字，它仍然可以向文件中写入比请求更少的数据。即使您的代码在形式上符合准则，它也可能包含相关的问题。\n\n# 使用静态代码分析器\n\n所有安全指南都被定义为对源代码或应用设计的大量特定要求。这些需求中的许多可以通过使用静态代码分析器来自动检查。\n\n**静态代码分析器**是可以分析源代码的工具，如果开发者检测到违反代码质量要求的代码模式，就会发出警告。在错误检测和预防方面，它们效率极高。因为它们可以在代码构建之前运行，所以在开发的最早阶段就修复了许多错误，而不涉及耗时的测试和调试过程。\n\n除了错误检测和预防，静态代码分析器还用于在认证过程中证明代码符合目标要求和指南。\n\n在本食谱中，我们将学习如何在应用中使用静态代码分析器。\n\n# 怎么做...\n\n我们将创建一个简单的程序，并运行众多开源代码分析器中的一个来检查潜在的问题。请遵循以下步骤:\n\n1.  转到`~/test/returns`目录，这是我们在之前的食谱中创建的。\n2.  从存储库中安装`cppcheck`工具。确保你在`root`账户下，而不是`user`账户下:\n\n```cpp\n# apt-get install cppcheck\n```\n\n3.  再次转到`user`账户:\n\n```cpp\n# su - user\n$\n```\n\n4.  对`returns.cpp`文件运行`cppcheck`:\n\n```cpp\n$ cppcheck --std=posix --enable=warning returns.cpp\n```\n\n5.  分析它的输出。\n\n# 它是如何工作的...\n\n代码分析器可以解析我们应用的源代码，并根据大量代表不良编码实践的模式对其进行测试。\n\n存在许多代码分析器，从开源和免费使用到企业使用的昂贵商业产品。\n\n在*中提到的 **MISRA** 编码标准使用所有功能的返回值*配方是一个商业标准。这意味着您需要购买许可证才能使用它，同样，也需要购买经过认证的代码分析器来测试代码是否符合 MISRA。\n\n出于学习目的，我们将使用名为`cppcheck`的开源代码分析器。它被广泛使用，并且已经包含在 Ubuntu 存储库中。我们可以用与任何其他 Ubuntu 包相同的方式安装它:\n\n```cpp\n# apt-get install cppcheck $ cppcheck --std=posix --enable=warning returns.cpp\n```\n\n现在，我们将源文件名作为参数传递。检查速度很快，会生成以下报告:\n\n![](img/659c3b78-ca64-474f-8917-0345f48808e4.png)\n\n正如我们所看到的，它在我们的代码中检测到了两个问题，甚至在我们试图构建它之前。第一期在我们更安全，增强的`TimeSaver2`班！为了使其符合 A0-1-2 的要求，我们需要检查`close`返回的状态码，如果出现错误就抛出异常。然而，我们在析构函数中这样做，破坏了 C++ 错误处理机制。\n\n代码分析器检测到的第二个问题是资源泄漏。这就解释了为什么`TimeSaver1`会生成空文件。打开文件时，我们不小心将文件描述符分配给了局部变量，而不是实例变量，即`fd`:\n\n```cpp\nint fd = open(name, O_RDWR|O_CREAT|O_TRUNC, 0600);\n```\n\n现在，我们可以修复它们并重新运行`cppcheck`以确保问题已经过去，并且没有引入新的问题。使用代码分析器作为开发工作流的一部分可以使您的代码更安全，性能更快，因为您可以在开发周期的早期阶段检测和预防问题。\n\n# 还有更多...\n\n虽然`cppcheck`是一个开源工具，但是它支持大量的 MISRA 检查。这并不能使它成为验证符合 MISRA 指南的认证工具，但可以让您了解您的代码离 MISRA 要求有多近，以及需要付出多少努力才能使其符合。\n\nMISRA 检查是作为附加组件实现的；您可以根据`cppcheck`([https://github.com/danmar/cppcheck/tree/master/addons](https://github.com/danmar/cppcheck/tree/master/addons))的 GitHub 存储库的加载项部分中的说明运行它。\n\n# 使用前置条件和后置条件\n\n在前面的食谱中，我们学习了如何在开发的早期阶段使用静态代码分析器来防止编码错误。另一个强大的防错工具是**契约编程**。\n\n契约式编程是一种实践，在这种实践中，开发人员为函数或模块的输入值、结果和中间状态明确定义契约或期望。虽然中间状态取决于实现，但是输入和输出值的契约可以被定义为公共接口的一部分。这些期望分别被称为**前提条件**和**前提条件**，有助于避免定义模糊的接口导致的编程错误。\n\n在这个食谱中，我们将学习如何在我们的 C++ 代码中定义前置条件和后置条件。\n\n# 怎么做...\n\n为了测试前置条件和后置条件是如何工作的，我们将部分重用我们在前面的食谱中使用的 **`TimeSaver1`** 类的代码。请遵循以下步骤:\n\n1.  在您的工作目录中，即`~/test`，创建一个名为`assert`的子目录。\n2.  使用您喜欢的文本编辑器在`assert`子目录中创建一个名为`assert.cpp`的文件。\n3.  将`TimeSaver1`类的修改版本添加到`assert.cpp`文件中:\n\n```cpp\n#include <cassert>\n#include <system_error>\n\n#include <unistd.h>\n#include <sys/fcntl.h>\n#include <time.h>\n\nclass TimeSaver1 {\n  int fd = -1;\n\npublic:\n  TimeSaver1(const char* name) {\n    assert(name != nullptr);\n    assert(name[0] != '\\0');\n\n    int fd = open(name, O_RDWR|O_CREAT|O_TRUNC, 0600);\n    if (fd < 0) {\n      throw std::system_error(errno,\n                              std::system_category(),\n                              \"Failed to open file\");\n    }\n    assert(this->fd >= 0);\n  }\n\n  ~TimeSaver1() {\n    assert(this->fd >= 0);\n    close(fd);\n  }\n};\n```\n\n4.  接下来是一个简单的`main`函数:\n\n```cpp\nint main() {\n  TimeSaver1 ts1(\"\");\n  return 0;\n}\n```\n\n5.  将构建规则放入`CMakeLists.txt`文件:\n\n```cpp\ncmake_minimum_required(VERSION 3.5.1)\nproject(assert)\nadd_executable(assert assert.cpp)\n\nset(CMAKE_SYSTEM_NAME Linux)\nset(CMAKE_SYSTEM_PROCESSOR arm)\n\nSET(CMAKE_CXX_FLAGS \"--std=c++ 11\")\nset(CMAKE_CXX_COMPILER /usr/bin/arm-linux-gnueabi-g++)\n```\n\n6.  现在，您可以构建和运行该应用。\n\n# 它是如何工作的...\n\n在这里，我们重用了上一个食谱中`TimeSaver1`类的一些代码。为了简单起见，我们去掉了`Update`方法，只留下了它的构造函数和析构函数。\n\n我们有意保留静态代码分析器在前面的配方中发现的相同错误，以检查前置条件和后置条件检查是否可以用于防止此类问题。\n\n我们的构造函数接受文件名作为参数。我们对文件名没有任何特别的限制，除了它应该是有效的。两个明显无效的文件名如下:\n\n*   作为名称的空指针\n*   空名字\n\n我们使用`assert`宏将这些规则作为先决条件:\n\n```cpp\nassert(name != nullptr);\nassert(name[0] != '\\0');\n```\n\n要使用这个宏，我们需要包含一个头文件，即`csassert`:\n\n```cpp\n#include <cassert>\n```\n\n接下来，我们使用文件名打开文件，并将其存储在`fd`变量中。我们将其分配给局部变量，即`fd`，而不是实例变量`fd`。这是我们想要检测的编码错误:\n\n```cpp\nint fd = open(name, O_RDWR|O_CREAT|O_TRUNC, 0600);\n```\n\n最后，我们在构造函数中加入后置条件。在我们的例子中，唯一的后置条件是实例变量`fd`应该有效:\n\n```cpp\nassert(this->fd >= 0);\n```\n\n请注意，我们如何在它前面加上这个前缀，以消除它与局部变量的歧义。同样，我们给析构函数添加一个前提条件:\n\n```cpp\nassert(this->fd >= 0);\n```\n\n我们在这里不添加任何后置条件，因为在析构函数返回后，实例不再有效。\n\n现在，让我们测试我们的代码。在`main`函数中，我们创建一个`TimeSaver1`的实例，传递一个空文件名作为参数:\n\n```cpp\nTimeSaver1 ts1(\"\");\n```\n\n构建并运行程序后，我们将看到以下输出:\n\n![](img/bcd001f1-c8c9-4e3f-bd48-a4dbc27177be.png)\n\n构造函数中的前提条件检查检测到违反合同并终止了应用。让我们将文件名更改为有效的文件名:\n\n```cpp\nTimeSaver1 ts1(\"timestamp.bin\");\n```\n\n我们再次构建并运行应用，得到不同的输出:\n\n![](img/2a162765-e45c-4207-a02c-fe63f35de7c1.png)\n\n现在，所有先决条件都已满足，但是我们违反了后置条件，因为我们未能更新实例变量`fd`。通过删除`fd`前的类型定义来更改第 16 行，如下所示:\n\n```cpp\nfd = open(name, O_RDWR|O_CREAT|O_TRUNC, 0600);\n```\n\n重新构建并再次运行程序会产生一个空输出:\n\n![](img/8a57bd09-8c9e-4004-91b6-1a39c806c0e2.png)\n\n这表明对输入参数和结果的所有期望都已满足。即使是最基本的形式，使用契约进行编程也能帮助我们避免两个编码问题。这就是为什么这项技术被广泛应用于软件开发的所有领域，尤其是安全关键系统。\n\n# 还有更多...\n\nC++ 20 标准中有望增加对契约式编程更详细的支持。然而，它被推迟到以后的标准。提案说明见 g .多斯·雷斯、J. D .加西亚、j .拉科斯、A .梅雷迪思、n .迈尔斯、b .斯特劳德普的论文*A Contract Design*([http://www . open-STD . org/JT C1/sc22/wg21/docs/papers/2016/p 0380 r 1 . pdf](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0380r1.pdf))。\n\n# 探索代码正确性的形式验证\n\n静态代码分析器和契约式编程方法帮助开发人员显著减少代码中的编码错误。然而，这在安全关键的软件开发中是不够的。正式证明软件组件的设计是正确的很重要。\n\n有许多相当复杂的方法可以做到这一点，还有自动化这个过程的工具。在这个食谱中，我们将探索一种正式的软件验证工具，叫做 CPAchecker([https://cpachecker.sosy-lab.org/index.php](https://cpachecker.sosy-lab.org/index.php))。\n\n# 怎么做...\n\n我们将下载并安装`CPAcheck`到我们的构建环境中，然后针对一个示例程序运行它。请遵循以下步骤:\n\n1.  打开包含您的构建环境的终端。\n2.  请确保您拥有根权限。如果没有，按下 *Ctrl* + *D* 退出*用户*会话返回*根*会话。\n3.  安装 Java 运行时:\n\n```cpp\n# apt-get install openjdk-11-jre\n```\n\n4.  切换到用户会话，将目录改为`/mnt`:\n\n```cpp\n# su - user\n$ cd /mnt\n```\n\n5.  下载并解压`CPACheck`档案，如下所示:\n\n```cpp\n$ wget -O - https://cpachecker.sosy-lab.org/CPAchecker-1.9-unix.tar.bz2 | tar xjf -\n```\n\n6.  将目录更改为`CPAchecker-1.9-unix`:\n\n```cpp\n$ cd CPAchecker-1.9-unix\n```\n\n7.  对示例文件运行`CPAcheck`:\n\n```cpp\n./scripts/cpa.sh -default doc/examples/example.c \n```\n\n8.  下载故意包含错误的示例文件:\n\n```cpp\n$ wget https://raw.githubusercontent.com/sosy-lab/cpachecker/trunk/doc/examples/example_bug.c\n```\n\n9.  对新示例运行检查器:\n\n```cpp\n./scripts/cpa.sh -default example_bug.c \n```\n\n10.  切换到网络浏览器，打开工具生成的`~/test/CPAchecker-1.9-unix/output/Report.html`报告文件。\n\n# 它是如何工作的...\n\n要运行`CPAcheck`，我们需要安装 Java 运行时。这在 Ubuntu 资源库中有，我们用`apt-get`来安装。\n\n下一步是下载`CPAcheck`本身。我们使用`wget`工具下载归档文件，并立即将其输入到`tar`实用程序进行提取。完成后，可以在`CPAchecker-1.9-unix`目录中找到该工具。\n\n我们使用一个预打包的示例文件来检查该工具的工作方式:\n\n```cpp\n./scripts/cpa.sh -default doc/examples/example.c\n```\n\n它生成以下输出:\n\n![](img/ff8fcef6-80fd-45a3-9eed-4785e0e00f6b.png)\n\n我们可以看到，该工具没有发现该文件的任何问题。`CPAcheck`档案中没有包含 bug 的类似文件，但我们可以从其网站下载:\n\n```cpp\n$ wget https://raw.githubusercontent.com/sosy-lab/cpachecker/trunk/doc/examples/example_bug.c\n```\n\n我们再次运行该工具，并获得以下输出:\n\n![](img/ab5c77a6-4eb2-4ef5-8dea-d5ad44974a53.png)\n\n现在，结果不同了:检测到一个错误。我们可以打开工具生成的 HTML 报告进行进一步分析。除了日志和统计数据之外，它还显示了流程自动化图:\n\n![](img/9fdfd67a-296b-404e-a6e3-ee5065fd6216.png)\n\n形式验证方法和工具很复杂，可以处理相对简单的应用，但它们保证了所有情况下应用逻辑的正确性。\n\n# 还有更多...\n\n你可以在 CPAchecker 的网站([https://cpachecker.sosy-lab.org/index.php](https://cpachecker.sosy-lab.org/index.php))上找到更多关于它的信息。"
  },
  {
    "path": "docs/emb-prog-mod-cpp-cb/15.md",
    "content": "# 十五、微控制器编程\n\n在前几章中，我们主要讨论了适用于相对强大的嵌入式系统的主题，这些嵌入式系统具有兆字节的内存并运行 Linux 操作系统。现在，我们将探索嵌入式系统领域的另一面——微控制器。\n\n正如我们在介绍中所讨论的，微控制器通常用于执行简单的、通常是实时的任务，例如收集数据或向特定设备提供高级应用编程接口。微控制器价格低廉，能耗低，可以在各种环境条件下工作，是物联网应用的完美选择。\n\n他们低成本的另一面是他们的能力。通常，它们具有以千字节为单位的板载内存，并且没有硬件内存映射。他们根本不运行任何操作系统，或者运行像 FreeRTOS 这样的简单实时操作系统。\n\n有许多型号的微控制器，是为特定应用量身定制的。在本章中，我们将学习如何使用 Arduino 开发环境。这些配方是为 Arduino UNO 板创建的，该板构建在 ATmega328 微控制器之上，该微控制器广泛用于教育和原型制作，但它们也适用于其他 Arduino 板。\n\n我们将涵盖以下主题:\n\n*   建立开发环境\n*   编译和上传程序\n*   调试微控制器代码\n\n这些食谱将有助于设置环境和开始微控制器的开发。\n\n# 建立开发环境\n\nArduino UNO 板附带了一个集成开发环境，即 IDE，称为 Arduino IDE。可以从[https://www.arduino.cc/](https://www.arduino.cc/)T2【网站】免费下载。\n\n在本食谱中，我们将学习如何设置和连接您的 Arduino 板。\n\n# 怎么做...\n\n我们将安装 Arduino IDE，将 Arduino UNO 板连接到您的计算机，然后在 IDE 和板之间建立通信:\n\n1.  在浏览器中，打开下载([https://www.arduino.cc/en/Main/Software](https://www.arduino.cc/en/Main/Software))页面，选择与您的操作系统相匹配的安装选项。\n2.  下载完成后，按照*入门*([https://www.arduino.cc/en/Guide/HomePage](https://www.arduino.cc/en/Guide/HomePage))页面的安装说明进行操作。\n3.  使用 USB 电缆将 Arduino 板连接到计算机。它会自动开机。\n4.  运行 Arduino IDE。\n5.  现在，我们需要在 IDE 和板之间建立通信。切换到 Arduino IDE 窗口。在应用菜单中，选择工具->端口。这将打开一个带有串行端口选项的子菜单。选择名称中有 Arduino 的那个。\n6.  在“工具”菜单中，单击“电路板”项目，然后选择 Arduino 电路板的型号。\n7.  选择工具->电路板信息菜单项。\n\n# 它是如何工作的...\n\nArduino 主板附带一个免费的 IDE，可以从制造商的网站下载。集成开发环境的安装很简单，与您的平台的任何其他软件的安装没有什么不同。\n\n所有代码都是在集成开发环境中编写、编译和调试的，但是生成的编译图像应该被刷新到目标板上并在那里执行。为此，集成开发环境应该能够与主板通信。\n\n主板通过通用串行总线连接到运行集成开发环境的计算机。USB 电缆不仅提供通信，还为电路板供电。板一连接到计算机，它就打开并开始工作。\n\n集成开发环境使用串行接口与主板通信。由于您的计算机上可能已经配置了多个串行端口，因此设置通信的步骤之一是选择一个可用的端口。通常是名字里有 Arduino 的那个:\n\n![](img/c1a46111-aa64-40fa-8b02-89e6f0e44b6c.png)\n\n最后，一旦选择了端口，我们就让 IDE 知道我们使用的 Arduino 板的类型。一旦完成，我们就可以检查板和 IDE 之间的通信是否真的有效。当我们调用板信息菜单项时，集成开发环境会显示一个对话框窗口，其中包含与连接的板相关的信息:\n\n![](img/16acebfc-27e7-4597-9f52-8beceb7b0020.png)\n\n如果对话框没有出现，这表明有问题。主板可能已断开或损坏，或者选择了错误的端口。否则，我们准备好构建和运行我们的第一个程序。\n\n# 还有更多...\n\n如果出现问题，请考虑阅读 Arduino 网站上的故障排除部分([https://www.arduino.cc/en/Guide/Troubleshooting](https://www.arduino.cc/en/Guide/Troubleshooting))。\n\n# 编译和上传程序\n\n在前面的食谱中，我们学习了如何设置开发环境。现在，让我们编译并运行我们的第一个程序。\n\nArduino UNO 板本身没有屏幕，但是我们需要一些方法来知道我们的程序正在做什么。然而，它确实有一个内置的发光二极管，我们可以通过我们的程序进行控制，而无需将任何外部设备连接到主板。\n\n在这个食谱中，我们将学习如何编译和运行一个程序，在 Arduino UNO 板上闪烁一个内置的 LED。\n\n# 怎么做...\n\n我们将编译一个集成开发环境附带的现有示例应用并上传到板上:\n\n1.  将 Arduino 板连接到您的计算机，并打开 Arduino IDE。\n2.  在 Arduino IDE 中，打开文件菜单，选择示例-> 01。基础->眨眼。\n3.  一个新的窗口将会打开。在此窗口中，单击上传按钮。\n4.  观察板上的内置指示灯如何开始闪烁。\n\n# 它是如何工作的...\n\nArduino 是一个广泛用于教育目的的平台。它被设计成易于使用，并附有一堆例子。对于我们的第一个程序，我们选择了一个不需要电路板与外部外设连接的应用。启动集成开发环境后，我们从可用的示例中选择了 Blink 应用，如下所示:\n\n![](img/9c4aa898-0a41-4617-b54b-6bee11bd551f.png)\n\n这将打开一个包含程序代码的窗口:\n\n![](img/e5b18011-15bc-4c0e-aed8-d2f26799737d.png)\n\n除了程序的源代码，我们还可以看到一个黑色的控制台窗口和一个状态栏，表明 Arduino UNO 板是通过`/dev/cu.usbmodem14101`串口连接的。设备名称取决于主板型号，端口名称在 Windows 或 Linux 中可能会有所不同。\n\n在源代码上面，我们可以看到几个按钮。第二个按钮是向右箭头，是上传按钮。一旦我们按下它，集成开发环境就开始构建应用，然后将生成的二进制文件上传到板上。我们可以在控制台窗口中看到构建状态:\n\n![](img/d0a8e609-5449-4491-866d-7708f48e4f68.png)\n\n应用在上传后立即启动。如果我们看一下主板，可以看到内置的黄色 LED 已经开始闪烁。我们能够构建并运行第一个 Arduino 应用。\n\n# 还有更多...\n\n上传后，您的程序存储在主板上的闪存中。如果您关闭主板电源，然后再次打开，即使没有运行 IDE，程序也会开始运行。\n\n# 调试微控制器代码\n\n与更强大的嵌入式平台如树莓 PI 相比，Arduino 的调试能力有限。Arduino IDE 不提供集成调试器，Arduino 板本身没有内置屏幕。然而，它确实有通用异步收发器，并提供了一个串行接口，可用于调试目的。\n\n在这个食谱中，我们将学习如何使用 Arduino 串行接口来调试和读取用户输入。\n\n# 怎么做...\n\n我们将为 Arduino 控制器实现一个简单的程序，该程序等待串行端口上的用户输入，并根据数据打开或关闭内置 LED:\n\n1.  打开 Arduino IDE，并在其文件菜单中选择新建。将显示一个新的“草图”窗口。\n2.  将以下代码片段粘贴到“草图”窗口中:\n\n```cpp\nvoid setup() {\n pinMode(LED_BUILTIN, OUTPUT);\n Serial.begin(9600);\n while (!Serial);\n}\n\nvoid loop() {\n  if (Serial.available() > 0) {\n      int inByte = Serial.read();\n      if (inByte == '1') {\n        Serial.print(\"Turn LED on\\n\");\n        digitalWrite(LED_BUILTIN, HIGH);\n      } else if (inByte == '0') {\n        Serial.print(\"Turn LED off\\n\");\n        digitalWrite(LED_BUILTIN, LOW); \n      } else {\n        Serial.print(\"Ignore byte \");\n        Serial.print(inByte);\n        Serial.print(\"\\n\");\n      }\n      delay(500);\n  }\n}\n```\n\n3.  单击上传按钮构建并运行代码。\n4.  在 Arduino IDE 的“工具”菜单中选择“串行监视器”。将出现串行监视器窗口。\n5.  在串行监视器窗口中，输入`1010110`。\n\n# 它是如何工作的...\n\n我们创建了一个新的 Arduino 草图，它由两个函数组成。第一个函数`setup`在程序启动时被调用，用于提供应用的初始配置。\n\n在我们的例子中，我们需要初始化串行接口。串行通信最重要的参数是其每秒比特数的速度。微控制器和集成开发环境应该同意使用相同的速度，否则通信将无法工作。默认情况下，串行监视器每秒使用 9600 位，我们在程序中使用该值:\n\n```cpp\nSerial.begin(9600);\n```\n\n不过，可以使用更高的通信速度。串行监视器在屏幕右下角有一个下拉菜单，允许选择其他速度。如果您决定使用其他速度，则应相应地修改代码。\n\n我们还配置了引脚 13，对应于内置发光二极管，用于输出:\n\n```cpp\npinMode(LED_BUILTIN, OUTPUT);\n```\n\n我们用常量`LED_BUILTIN`代替`13`，让代码更容易理解。第二个函数`loop`，定义了 Arduino 程序的一个无限循环。对于每次迭代，我们从串行端口读取一个字节:\n\n```cpp\nif (Serial.available() > 0) {\n      int inByte = Serial.read();\n```\n\n如果字节是`1`，我们打开 LED，写一条信息回串口:\n\n```cpp\n        Serial.print(\"Turn LED on\\n\");\n        digitalWrite(LED_BUILTIN, HIGH);\n```\n\n同样，对于`0`，我们关闭 LED:\n\n```cpp\n        Serial.print(\"Turn LED off\\n\");\n        digitalWrite(LED_BUILTIN, LOW); \n```\n\n所有其他值都会被忽略。从端口读取每个字节后，我们增加 500 微秒的延迟。这样，我们可以定义不同的眨眼模式。比如我们发`1001001`，LED 会先亮 0.5 秒，再灭 1 秒，再亮 0.5 秒，再灭 1 秒，最后再亮。\n\n如果我们运行代码并在串行监视器中输入`1001001`，我们可以看到以下输出:\n\n![](img/6d2f169e-1b5f-4686-8942-7f5faf28a277.png)\n\n指示灯按预期闪烁，除此之外，我们还可以在串行监视器中看到调试消息。这样，我们可以调试真实的、更复杂的应用。"
  },
  {
    "path": "docs/emb-prog-mod-cpp-cb/README.md",
    "content": "# 现代 C++ 嵌入式编程秘籍\n\n> 原书：[Embedded Programming with Modern C++ Cookbook](https://libgen.rs/book/index.php?md5=5F729908F617AC4C3BF4B93D739754A8)\n> \n> 协议：[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)\n> \n> 阶段：机翻（1）\n> \n> 自豪地采用[谷歌翻译](https://translate.google.cn/)\n> \n> 让开源界提前感受入关。\n\n* [在线阅读](https://ccpp.apachecn.org)\n* [在线阅读（Gitee）](https://apachecn.gitee.io/apachecn-c-cpp-zh/)\n* [ApacheCN 学习资源](http://docs.apachecn.org/)\n\n## 贡献指南\n\n本项目需要校对，欢迎大家提交 Pull Request。\n\n> 请您勇敢地去翻译和改进翻译。虽然我们追求卓越，但我们并不要求您做到十全十美，因此请不要担心因为翻译上犯错——在大部分情况下，我们的服务器已经记录所有的翻译，因此您不必担心会因为您的失误遭到无法挽回的破坏。（改编自维基百科）\n\n## 联系方式\n\n### 负责人\n\n* [飞龙](https://github.com/wizardforcel): 562826179\n\n### 其他\n\n*   在我们的 [apachecn/apachecn-c-cpp-zh](https://github.com/apachecn/apachecn-c-cpp-zh) github 上提 issue.\n*   发邮件到 Email: `apachecn@163.com`.\n*   在我们的 [组织学习交流群](http://www.apachecn.org/organization/348.html) 中联系群主/管理员即可.\n\n## 赞助我们\n\n![](http://data.apachecn.org/img/about/donate.jpg)\n"
  },
  {
    "path": "docs/emb-prog-mod-cpp-cb/SUMMARY.md",
    "content": "+   [现代 C++ 嵌入式编程秘籍](README.md)\n+   [零、前言](00.md)\n+   [一、嵌入式系统基础](01.md)\n+   [二、设置环境](02.md)\n+   [三、使用不同的架构](03.md)\n+   [四、处理中断](04.md)\n+   [五、调试、日志记录和性能分析](05.md)\n+   [六、内存管理](06.md)\n+   [七、多线程和同步](07.md)\n+   [八、通信和序列化](08.md)\n+   [九、外部设备](09.md)\n+   [十、降低功耗](10.md)\n+   [十一、时间点和间隔](11.md)\n+   [十二、错误处理和容错](12.md)\n+   [十三、实时系统指南](13.md)\n+   [十四、安全关键系统指南](14.md)\n+   [十五、微控制器编程](15.md)\n"
  },
  {
    "path": "docs/exp-cpp/00.md",
    "content": "# 零、前言\n\n这本书将向读者提供关于 C++ 17 和 C++ 20 标准的 C++ 程序的细节，以及它们是如何编译、链接和执行的。它还将涵盖内存管理如何工作，内存管理问题的最佳实践是什么，什么是类以及它们是如何实现的，编译器如何优化代码，以及编译器在支持类继承、虚函数和模板方面的方法是什么。\n\n这本书还将告诉读者如何应用内存管理、面向对象编程、并发性和设计模式来创建世界通用的生产应用。\n\n读者将学习高效数据结构和算法的内部细节，并了解如何测量和比较它们，以选择最适合特定问题的内容。\n\n这本书将帮助读者将系统设计技能和基本设计模式结合到 C++ 应用中。\n\n作为奖励，这本书还介绍了人工智能世界，包括使用 C++ 编程语言的机器学习基础知识。\n\n到本书结束时，读者应该有足够的信心使用高效的数据结构和算法来设计和构建真实世界的、可扩展的 C++ 应用。\n\n# 这本书是给谁的\n\n寻求找到与语言和程序结构相关的细节的 C++ 开发人员，或者试图通过挖掘程序的本质来设计可重用、可扩展的架构来提升自己的专业知识的开发人员，都将从这本书中受益。那些打算使用 C++ 17 和 C++ 20 的新特性设计高效数据结构和算法的开发人员也将受益。\n\n# 这本书涵盖了什么\n\n[第 1 章](01.html)*介绍* *构建 C++ 应用*，包含 C++ 世界、其应用以及语言标准最新更新的介绍。本章还包括对 C++ 涵盖的主题的概述，以及对代码编译、链接和执行阶段的介绍。\n\n[第二章](02.html)、*C++ 低级编程*，重点讨论 c++ 数据类型、数组、指针以及指针寻址和操作，以及条件、循环、函数、函数指针和结构的低级细节。本章还包括对结构的介绍。\n\n[第 3 章](03.html)、*面向对象编程的细节*，深入探讨了类和对象的结构，以及编译器如何实现对象生存期。本章结束时，读者将了解继承和虚拟函数的实现细节，以及 C++ 中 OOP 的基本内部细节。\n\n[第 4 章](04.html)、*理解和设计模板*，介绍 C++ 模板、模板函数示例、模板类、模板专门化以及模板元编程的一般知识。特性和元编程将结合 C++ 应用的魔力。\n\n[第 5 章](05.html)、*内存管理和智能指针*深入探讨了内存分区、分配和一般管理的细节，包括使用智能指针来避免潜在的内存泄漏。\n\n[第六章](06.html)、*挖掘 STL* 中的数据结构和算法，介绍数据结构及其 STL 实现。本章还包括数据结构的比较，以及用真实例子讨论正确的应用。\n\n[第 7 章](07.html)、*函数式编程*重点介绍函数式编程，这是一种不同的编程范式，让读者能够专注于代码的“函数”而不是“物理”结构。掌握函数式编程为开发人员提供了一项新技能，有助于为问题提供更好的解决方案。\n\n[第 8 章](08.html)、*并发和多线程*，重点介绍如何通过利用并发来让程序运行得更快。当一个高效的数据结构和高效的算法达到程序性能的极限时，并发性就来了。\n\n[第 9 章](09.html)、*设计并发数据结构*，重点是利用数据结构和并发性设计基于锁和无锁的并发数据结构。\n\n[第 10 章](10.html)、*设计世界就绪型应用*，重点是通过使用设计模式，将从前面章节中获得的知识融入到设计健壮的现实世界应用中。本章还包括通过设计亚马逊克隆来理解和应用领域驱动设计。\n\n[第 11 章](11.html)、*使用设计模式设计策略游戏*通过使用设计模式和最佳实践，将从前面章节中获得的知识融入到策略游戏的设计中。\n\n[第 12 章](12.html)、*联网与安全*介绍了 C++ 中的网络编程，以及如何利用网络编程技巧构建 dropbox 后端克隆。这几章还包括如何确保编码最佳实践的讨论。\n\n[第 13 章](13.html)、*调试和测试、*重点调试 C++ 应用和避免代码 bug 的最佳实践，应用静态代码分析以减少测试驱动开发和行为驱动开发的程序、介绍和应用中的问题。本章还讨论了行为驱动开发和 TDD 之间的区别以及用例。\n\n[第 14 章](14.html)、*带 Qt 的图形用户界面*，介绍了 Qt 库及其主要组件。本章还包括对 Qt 跨平台特性的理解，通过构建一个简单的桌面客户端继续 dropbox 示例。\n\n[第 15 章](15.html)、*在机器学习任务中使用 C++ 的*，简要介绍了人工智能的概念和该领域的最新发展。本章还包括机器学习和任务的介绍，如回归分析和聚类，以及如何建立一个简单的神经网络。\n\n[第 16 章](16.html)、*实现基于对话的搜索引擎*涉及应用前面所有章节的知识来设计被描述为*基于对话的*的高效搜索引擎，因为它通过询问(和学习)用户的相应问题来找到正确的文档。\n\n# 充分利用这本书\n\n基本的 C++ 经验，包括熟悉内存管理、面向对象编程以及基本的数据结构和算法，将是一个很大的优势。如果你渴望了解这个复杂的程序是如何在幕后工作的，也渴望了解 C++ 应用设计的编程概念和最佳实践的细节，那么你肯定应该继续阅读这本书。\n\n| **书中涉及的软件/硬件** | **操作系统要求** |\n| g++ 编译器 | Ubuntu Linux 是一个优势，但不是一个要求 |\n\n您还需要在您的计算机上安装 Qt 框架。详情见相关章节。\n\n在写这本书的时候，并不是所有的 C++ 编译器都支持所有新的 C++ 20 特性，考虑使用最新版本的编译器来测试本章中介绍的更多特性。\n\n# 下载示例代码文件\n\n你可以从你在[www.packt.com](http://www.packt.com)的账户下载这本书的示例代码文件。如果您在其他地方购买了这本书，您可以访问[www.packt.com/support](http://www.packt.com/support)并注册将文件直接通过电子邮件发送给您。\n\n您可以按照以下步骤下载代码文件:\n\n1.  登录或注册[www.packt.com](http://www.packt.com)。\n2.  选择“支持”选项卡。\n3.  点击代码下载和勘误表。\n4.  在搜索框中输入图书的名称，并按照屏幕指示进行操作。\n\n下载文件后，请确保使用最新版本的解压缩文件夹:\n\n*   视窗系统的 WinRAR/7-Zip\n*   zipeg/izp/un ARX for MAC\n*   适用于 Linux 的 7-Zip/PeaZip\n\n这本书的代码包也托管在 https://github.com/PacktPublishing/Expert-CPP 的 GitHub 上。如果代码有更新，它将在现有的 GitHub 存储库中更新。\n\n我们还有来自丰富的图书和视频目录的其他代码包，可在**[【https://github.com/PacktPublishing/】](https://github.com/PacktPublishing/)**获得。看看他们！\n\n# 下载彩色图像\n\n我们还提供了一个 PDF 文件，其中包含本书中使用的截图/图表的彩色图像。可以在这里下载:[https://static . packt-cdn . com/downloads/9781838552657 _ color images . pdf](https://static.packt-cdn.com/downloads/9781838552657_ColorImages.pdf)\n\n# 使用的约定\n\n本书通篇使用了许多文本约定。\n\n`CodeInText`:表示文本中的码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟网址、用户输入和推特句柄。下面是一个例子:“前面的代码用预先指定的值声明了两个`readonly`属性。”\n\n代码块设置如下:\n\n```cpp\nRange book = 1..4;\nvar res = Books[book] ;\nConsole.WriteLine($\"\\tElement of array using Range: Books[{book}] => {Books[book]}\");\n```\n\n当我们希望将您的注意力吸引到代码块的特定部分时，相关的行或项目以粗体显示:\n\n```cpp\nprivate static readonly int num1=5;\nprivate static readonly int num2=6;\n```\n\n任何命令行输入或输出都编写如下:\n\n```cpp\ndotnet --info\n```\n\n**粗体**:表示一个新的术语，一个重要的单词，或者你在屏幕上看到的单词。例如，菜单或对话框中的单词像这样出现在文本中。下面是一个示例:“从管理面板中选择系统信息。”\n\nWarnings or important notes appear like this. Tips and tricks appear like this.\n\n# 取得联系\n\n我们随时欢迎读者的反馈。\n\n**一般反馈**:如果你对这本书的任何方面有疑问，在你的信息主题中提到书名，发邮件给我们`customercare@packtpub.com`。\n\n**勘误表**:虽然我们已经尽了最大的努力来保证内容的准确性，但是错误还是会发生。如果你在这本书里发现了一个错误，如果你能向我们报告，我们将不胜感激。请访问[www.packt.com/submit-errata](http://www.packt.com/submit-errata)，选择您的图书，点击勘误表提交链接，并输入详细信息。\n\n**盗版**:如果您在互联网上遇到任何形式的我们作品的非法拷贝，如果您能提供我们的位置地址或网站名称，我们将不胜感激。请通过`copyright@packt.com`联系我们，并提供材料链接。\n\n**如果你有兴趣成为一名作者**:如果有一个你有专长的话题，你有兴趣写或者投稿一本书，请访问[authors.packtpub.com](http://authors.packtpub.com/)。\n\n# 复习\n\n请留下评论。一旦你阅读并使用了这本书，为什么不在你购买它的网站上留下评论呢？然后，潜在的读者可以看到并使用您不带偏见的意见来做出购买决定，我们在 Packt 可以了解您对我们产品的看法，我们的作者可以看到您对他们的书的反馈。谢谢大家！\n\n更多关于 Packt 的信息，请访问[packt.com](http://www.packt.com/)。"
  },
  {
    "path": "docs/exp-cpp/01.md",
    "content": "# 一、构建 C++ 应用简介\n\n编程语言因其程序执行模型而异；最常见的是解释和编译语言。编译器将源代码翻译成机器代码，计算机可以在没有中间支持系统的情况下运行。另一方面，解释语言代码需要支持系统、解释器和虚拟环境才能工作。\n\nC++ 是一种编译语言，它使程序比解释语言运行得更快。虽然 C++ 程序应该为每个平台编译，但解释程序可以跨平台运行。\n\n我们将讨论程序构建过程的细节，从处理源代码的阶段(由编译器完成)开始，到可执行文件(编译器的输出)的细节结束。我们还将了解为什么为一个平台构建的程序不能在另一个平台上运行。\n\n本章将涵盖以下主题:\n\n*   C++ 20 入门\n*   C++ 预处理器的详细信息\n*   在源代码编译的掩护下\n*   了解链接器及其功能\n*   加载和运行可执行文件的过程\n\n# 技术要求\n\n带有选项`-std=c++ 2a`的 g++ 编译器用于编译整个章节的示例。你可以在[https://github.com/PacktPublishing/Expert-CPP](https://github.com/PacktPublishing/Expert-CPP)找到本章使用的源文件。\n\n# C++ 20 入门\n\nC++ 经过多年的发展，现在已经有了一个全新的版本，C++ 20。自 C++ 11 以来，C++ 标准极大地扩展了语言特性集。让我们看看新的 C++ 20 标准中值得注意的特性。\n\n# 概念\n\n概念是 C++ 20 中的一个主要特性，它为类型提供了一组需求。概念背后的基本思想是模板参数的编译时验证。例如，要指定模板参数必须有默认构造函数，我们使用`default_constructible`概念的方式如下:\n\n```cpp\ntemplate <default_constructible T>\nvoid make_T() { return T(); }\n```\n\n在前面的代码中，我们遗漏了`typename`关键字。相反，我们设置了一个描述`template`函数的`T`参数的概念。\n\n我们可以说概念是描述其他类型的类型——可以说是元类型。它们允许模板参数的编译时验证以及基于类型属性的函数调用。我们将在[第 3 章](03.html)、*面向对象编程的细节*、[第 4 章](04.html)、*理解和设计模板*中详细讨论概念。\n\n# 协同程序\n\n协同程序是特殊的函数，能够在任何定义的执行点停止，并在以后恢复。Coroutines 用以下新关键词扩展了语言:\n\n*   `co_await`中止执行共同诉讼请求。\n*   `co_yield`暂停执行协同程序，同时还返回一个值。\n*   `co_return`类似于常规的`return`关键词；它完成协同并返回一个值。看看下面这个经典的例子:\n\n```cpp\ngenerator<int> step_by_step(int n = 0) {\n  while (true) {\n    co_yield n++ ;\n  }\n}\n```\n\n协同词与`promise`对象相关联。`promise`对象存储并提醒验尸官的*状态*。我们将在[第 8 章](08.html)、*并发和多线程*中深入探讨协同工作。\n\n# 范围\n\n`ranges`库提供了一种处理元素范围的新方法。要使用它们，您应该包含`<ranges>`头文件。让我们用一个例子来看看`ranges`。范围是有开始和结束的元素序列。它提供了一个`begin`迭代器和一个`end`哨兵。考虑以下整数向量:\n\n```cpp\nimport <vector>\n\nint main()\n{\n  std::vector<int> elements{0, 1, 2, 3, 4, 5, 6};\n}\n```\n\n带有范围适配器的范围(`|`运算符)提供了处理一系列元素的强大功能。例如，检查以下代码:\n\n```cpp\nimport <vector>\nimport <ranges>\n\nint main()\n{\n  std::vector<int> elements{0, 1, 2, 3, 4, 5, 6};\n  for (int current : elements | ranges::view::filter([](int e) { return \n   e % 2 == 0; }))\n  {\n    std::cout << current << \" \";\n  }\n}\n```\n\n在前面的代码中，我们使用`ranges::view::filter()`过滤了偶数的范围。注意应用于元素向量的范围适配器`|`。我们将在[第 7 章](07.html)、*功能编程*中讨论范围及其强大功能。\n\n# 更多 C++ 20 特性\n\nC++ 20 是 C++ 语言的一个新的大版本。它包含许多功能，使语言更加复杂和灵活。**概念**、**范围**和**相关**是本书将讨论的许多特性中的一些。\n\n最值得期待的特性之一是**模块**，它提供了声明模块以及在这些模块中导出类型和值的能力。您可以将模块视为头文件的改进版本，现在有了冗余的包含保护。我们将在本章中介绍 C++ 20 模块。\n\n除了 C++ 20 中添加的显著特性之外，还有一系列其他特性，我们将在整本书中讨论:\n\n*   宇宙飞船操作员:`operator<=>()`。操作员超载的详细程度现在可以通过利用`operator<=>()`来控制。\n*   `constexpr`征服了语言中越来越多的空间。C++ 20 现在有`consteval`功能、`constexpr std::vector`和`std::string`以及更多功能。\n*   数学常数，如`std::number::pi`、`std::number::log2e`。\n*   对线程库的主要更新，包括停止令牌和加入线程。\n*   迭代器概念。\n*   仅移动视图和其他功能。\n\n为了更好地理解一些新特性，并深入了解语言的本质，我们将从以前的版本开始介绍语言的核心。这将有助于我们找到新特性比旧特性更好的用途，也有助于支持传统的 C++ 代码。现在让我们从了解 C++ 应用构建过程开始。\n\n# 构建和运行程序\n\n您可以使用任何文本编辑器来编写代码，因为归根结底，代码只是文本。要编写代码，您可以自由选择简单的文本编辑器，如 *Vim* ，或者高级的**集成开发环境** ( **IDE** )如 *MS Visual Studio* 。情书和源代码唯一的区别是后者可能会被一个叫做**编译器**的特殊程序解释(虽然情书不能被编译成程序，但它可能会让你感到紧张不安)。\n\n为了区分纯文本文件和源代码，使用了一个特殊的文件扩展名。C++ 使用`.cpp`和`.h`扩展运行(你也可能偶尔会遇到`.cxx`和`.hpp`)。在进入细节之前，把编译器想象成一个把源代码翻译成可运行程序的工具，被称为可执行文件或者仅仅是一个**可执行文件**。从源代码制作可执行文件的过程称为**编译**。编译 C++ 程序是导致机器代码生成的一系列复杂任务。**机器码**是计算机的母语——这就是它被称为机器码的原因。\n\n通常，C++ 编译器解析和分析源代码，然后生成中间代码，优化它，最后在名为**目标文件**的文件中生成机器代码。您可能已经遇到了对象文件；它们有独立的扩展 Linux 中的`.o`和 Windows 中的`.obj`。创建的目标文件包含的不仅仅是计算机可以运行的机器代码。编译通常涉及几个源文件，编译每个源文件会产生一个单独的目标文件。然后，这些目标文件通过一个名为**链接器**的工具链接在一起，形成一个可执行文件。链接器使用存储在目标文件中的附加信息来正确链接它们(链接将在本章后面讨论)。\n\n下图描述了程序构建阶段:\n\n![](img/c5d04f78-da90-4c83-88ac-d152f1e9d2b1.png)\n\nC++ 应用构建过程包括三个主要步骤:**预处理**、**编译**和**链接**。所有这些步骤都是使用不同的工具完成的，但是现代编译器将它们封装在一个工具中，从而为程序员提供了一个单一且更简单的界面。\n\n生成的可执行文件保存在计算机的硬盘上。为了运行它，它应该被复制到主内存，内存。复制由另一个名为**加载器**的工具完成。加载程序是操作系统的一部分，它知道应该从可执行文件的内容中复制什么以及复制到哪里。将可执行文件加载到主内存后，原始可执行文件不会从硬盘上删除。\n\n程序的加载和运行由**操作系统** ( **操作系统**)完成。操作系统管理程序的执行，优先于其他程序，完成后卸载，等等。程序的运行副本被称为**过程**。进程是可执行文件的实例。\n\n# 理解预处理\n\n一个**预处理器**旨在处理源文件，使它们准备好编译。预处理器使用预处理器**指令**，如`#define`、`#include`等。指令不代表程序语句，但它们是预处理器的命令，告诉它如何处理源文件的文本。编译器无法识别这些指令，因此每当您在代码中使用预处理器指令时，预处理器都会在代码的实际编译开始之前相应地解析它们。例如，以下代码将在编译器开始编译之前被更改:\n\n```cpp\n#define NUMBER 41 \nint main() { \n  int a = NUMBER + 1; \n  return 0; \n}\n```\n\n使用`#define`指令定义的一切都被称为**宏**。预处理后，编译器获得以下形式的转换源:\n\n```cpp\nint main() { \n  int a = 41 + 1; \n  return 0;\n}\n```\n\n如前所述，预处理器只是处理文本，并不关心语言规则或其语法。使用预处理器指令，尤其是宏定义，就像前面的例子一样，`#define NUMBER 41`很容易出错，除非你意识到预处理器只是用`41` 替换`NUMBER`的任何出现，而没有将`41`解释为整数。对于预处理器，以下两行都有效:\n\n```cpp\nint b = NUMBER + 1; \nstruct T {}; // user-defined type \nT t = NUMBER; // preprocessed successfully, but compile error \n```\n\n这会产生以下代码:\n\n```cpp\nint b = 41 + 1\nstruct T {};\nT t = 41; // error line\n```\n\n当编译器开始编译时，发现赋值`t = 41`错误，因为有`no viable conversion from 'int' to 'T'`。\n\n使用语法正确但有逻辑错误的宏甚至是危险的:\n\n```cpp\n#define DOUBLE_IT(arg) (arg * arg) \n```\n\n预处理器将把`DOUBLE_IT(arg)`的任何出现替换为`(arg * arg)`，因此下面的代码将输出`16`:\n\n```cpp\nint st = DOUBLE_IT(4);\nstd::cout << st;\n```\n\n编译器将收到如下代码:\n\n```cpp\nint st = (4 * 4);\nstd::cout << st;\n```\n\n当我们使用复杂表达式作为宏参数时，就会出现问题:\n\n```cpp\nint bad_result = DOUBLE_IT(4 + 1); \nstd::cout << bad_result;\n```\n\n直觉上，这段代码会产生`25`，但事实是预处理器除了文本处理什么都不做，在这种情况下，它这样替换宏:\n\n```cpp\nint bad_result = (4 + 1 * 4 + 1);\nstd::cout << bad_result;\n```\n\n这样输出`9`，`9`显然不是`25`。\n\n若要修正宏定义，请在宏参数周围加上括号:\n\n```cpp\n#define DOUBLE_IT(arg) ((arg) * (arg)) \n```\n\n现在表达式将采用以下形式:\n\n```cpp\nint bad_result = ((4 + 1) * (4 + 1)); \n```\n\n强烈建议尽可能使用`const`声明而不是宏定义。\n\nAs a rule of thumb, avoid using macro definitions. Macros are error-prone and C++ provides a set of constructs that make the use of macros obsolete. \n\n如果我们使用`constexpr`函数，前面的例子将在编译时进行类型检查和处理:\n\n```cpp\nconstexpr int double_it(int arg) { return arg * arg; } \nint bad_result = double_it(4 + 1); \n```\n\n使用`constexpr`说明符可以在编译时计算函数的返回值(或变量值)。使用`const`变量可以更好地重写定义为`NUMBER`的示例:\n\n```cpp\nconst int NUMBER = 41; \n```\n\n# 头文件\n\n预处理器最常见的用法是`#include`指令，旨在将头文件包含在源代码中。头文件包含函数、类等的定义:\n\n```cpp\n// file: main.cpp \n#include <iostream> \n#include \"rect.h\"\nint main() { \n  Rect r(3.1, 4.05) \n  std::cout << r.get_area() << std::endl;\n}\n```\n\n假设头文件`rect.h`定义如下:\n\n```cpp\n// file: rect.h\nstruct Rect  \n{\nprivate:\n  double side1_;\n  double side2_;\npublic:\n  Rect(double s1, double s2);\n  const double get_area() const;\n};\n```\n\n实现包含在`rect.cpp`中:\n\n```cpp\n// file: rect.cpp\n#include \"rect.h\"\n\nRect::Rect(double s1, double s2)\n  : side1_(s1), side2_(s2)\n{}\n\nconst double Rect::get_area() const {\n  return side1_ * side2_;\n}\n```\n\n预处理器检查`main.cpp`和`rect.cpp`后，将`#include`指令替换为`main.cpp`的`iostream`和`rect.h`以及`rect.cpp`的`rect.h`的相应内容。C++ 17 引入了`__has_include`预处理器常量表达式。如果找到指定名称的文件，则`__has_include`评估为`1`，如果没有，则评估为`0`:\n\n```cpp\n#if __has_include(\"custom_io_stream.h\")\n#include \"custom_io_stream.h\"\n#else\n#include <iostream>\n#endif\n```\n\n声明头文件时，强烈建议使用所谓的 *include-guards* ( `#ifndef, #define, #endif`)避免双重声明错误。我们将很快介绍这项技术。同样，这些是预处理器指令，允许我们避免以下情况:类型`Square`在`square*.*h`中定义，它包括`rect.h`，以便从`Rect`派生`Square`:\n\n```cpp\n// file: square.h\n#include \"rect.h\"\nstruct Square : Rect {\n  Square(double s);\n};\n```\n\n将`main.cpp`中的`square.h`和`rect.h`都包含在内，导致两次包含`rect.h`:\n\n```cpp\n// file: main.cpp\n#include <iostream> \n#include \"rect.h\" \n#include \"square.h\"\n/* \n  preprocessor replaces the following with the contents of square.h\n*/\n// code omitted for brevity\n```\n\n预处理后，编译器会收到如下形式的`main.cpp`:\n\n```cpp\n// contents of the iostream file omitted for brevity \nstruct Rect {\n  // code omitted for brevity\n};\nstruct Rect {\n  // code omitted for brevity\n};\nstruct Square : Rect {\n  // code omitted for brevity\n};\nint main() {\n  // code omitted for brevity\n}\n```\n\n然后编译器会产生一个错误，因为它遇到两个类型为`Rect`的声明。头文件应通过以下方式使用 include-guards 来防止多个包含:\n\n```cpp\n#ifndef RECT_H \n#define RECT_H \nstruct Rect { ... }; // code omitted for brevity  \n#endif // RECT_H \n\n```\n\n预处理器第一次遇到表头时，`RECT_H`没有定义，`#ifndef`和`#endif`之间的一切都会相应处理，包括`RECT_H`定义。预处理器第二次在同一个源文件中包含同一个头文件时，会省略内容，因为`RECT_H`已经定义好了。\n\n这些 include-guards 是控制部分源文件编译的指令的一部分。所有的条件编译指令都是`#if`、`#ifdef`、`#ifndef`、`#else`、`#elif`和`#endif`。\n\n条件编译在许多情况下是有用的；其中之一就是在所谓的**调试**模式下记录函数调用。在发布程序之前，建议调试程序并测试逻辑缺陷。您可能希望看到调用某个函数后代码中发生了什么，例如:\n\n```cpp\nvoid foo() {\n  log(\"foo() called\");\n  // do some useful job\n}\nvoid start() {\n  log(\"start() called\");\n  foo();\n  // do some useful job\n}\n```\n\n每个函数调用`log()`函数，实现如下:\n\n```cpp\nvoid log(const std::string& msg) {\n#if DEBUG\n  std::cout << msg << std::endl;\n#endif\n}\n```\n\n如果定义了`DEBUG`，则`log()`功能将打印`msg`。如果编译启用`DEBUG`的项目(使用编译器标志，比如 g++ 中的`-D`，那么`log()`函数将打印传递给它的字符串；否则，它将一事无成。\n\n# 在 C++ 20 中使用模块\n\n模块修复了头文件令人讨厌的包含保护问题。我们现在可以去掉预处理宏了。模块包含两个关键词，`import`和`export`。要使用一个模块，我们`import`它。要用导出的属性声明一个模块，我们使用`export`。在列出使用模块的好处之前，让我们看一个简单的使用示例。下面的代码声明了一个模块:\n\n```cpp\nexport module test;\n\nexport int twice(int a) { return a * a; }\n```\n\n第一行声明名为`test`的模块。接下来，我们声明`twice()`功能并将其设置为`export`。这意味着我们可以拥有未导出的函数和其他实体，因此，它们在模块之外是私有的。通过导出实体，我们将其设置为`module`用户。要使用`module`，我们按照以下代码导入它:\n\n```cpp\nimport test;\n\nint main()\n{\n  twice(21);\n}\n```\n\n模块是人们期待已久的 C++ 特性，它在编译和维护方面提供了更好的性能。以下特性使模块在与常规头文件的竞争中更胜一筹:\n\n*   模块只导入一次，类似于自定义语言实现支持的预编译头。这大大减少了编译时间。未导出的实体对导入模块的翻译单元没有影响。\n*   通过允许您选择哪些单元应该导出，哪些不应该导出，模块允许表达代码的逻辑结构。模块可以捆绑成更大的模块。\n*   摆脱像前面描述的包含防护这样的变通方法。我们可以以任何顺序导入模块。人们不再担心宏观的重新定义。\n\n模块可以和头文件一起使用。我们可以在同一个文件中导入和包含标题，如下例所示:\n\n```cpp\nimport <iostream>;\n#include <vector>\n\nint main()\n{\n  std::vector<int> vec{1, 2, 3};\n  for (int elem : vec) std::cout << elem;\n}\n```\n\n创建模块时，您可以自由导出模块接口文件中的实体，并将实现移动到其他文件中。逻辑与管理`.h`和`.cpp`文件相同。\n\n# 理解编译\n\nC++ 编译过程由几个阶段组成。有些阶段旨在分析源代码，而其他阶段则生成并优化目标机器代码。下图显示了编译的各个阶段:\n\n![](img/c7f00317-dd05-467d-af51-d4c5a14858a4.png)\n\n让我们详细看看这些阶段。\n\n# 标记化\n\n编译器的分析阶段旨在将源代码分成称为标记的小单元。一个**标记**可以是一个单词，也可以只是一个符号，比如`=`(等号)。令牌是源代码的最小单位，为编译器携带有意义的值。例如，表达式`int a = 42;`将被分为代币`int`、`a`、`=`、`42`和`;`。表达式不仅仅被空格分割，因为下面的表达式被分割成相同的标记(尽管建议不要忘记操作数之间的空格):\n\n```cpp\nint a=42;\n```\n\n使用复杂的方法，使用正则表达式，将源代码拆分成标记。它被称为**l****exic analysis**，或者**tokens 化**(分为 token)。对于编译器来说，使用标记化输入提供了一种更好的方式来构建用于分析代码语法的内部数据结构。让我们看看如何。\n\n# 语法分析\n\n当谈到编程语言编译时，我们通常区分两个术语:语法和语义。语法是代码的结构；它定义了一些规则，通过这些规则，组合起来的标记在结构上是有意义的。例如， *day nice* 在英语中是一个语法正确的短语，因为它在两个标记中都不包含错误。**语义**另一方面，关注代码的实际含义。也就是说，*美好的一天*在语义上是不正确的，应该更正为*美好的一天*。\n\n语法分析是源分析的一个关键部分，因为标记将在语法和语义上进行分析，即它们是否具有符合一般语法规则的任何意义。以下面为例:\n\n```cpp\nint b = a + 0;\n```\n\n这对我们来说可能没有意义，因为给变量加零不会改变它的值，但是编译器不会在这里看逻辑意义——它会寻找代码的*语法正确性*(缺少分号、缺少右括号等等)。在编译的语法分析阶段检查代码的语法正确性。词法分析将代码分成标记；**语法分析**检查语法正确性，这意味着如果我们遗漏了一个分号，前面提到的表达式将产生语法错误:\n\n```cpp\nint b = a + 0\n```\n\ng++ 会抱怨`expected ';' at end of declaration`错误。\n\n# 语义分析\n\n如果前面的表达式类似于`it b = a + 0; `，编译器会将其分成标记`it`、`b`、`=`等。我们已经看到`it`是一个未知的东西，但是对于编译器来说，在这一点上是可以的。这会导致 g++ 中的编译错误`unknown type name \"it\"`。寻找表达式背后的含义是**语义分析**(解析)的任务。\n\n# 中间代码生成\n\n完成所有分析后，编译器生成中间代码，该代码是 C++ 的轻量级版本，主要是 C。\n\n```cpp\nclass A { \npublic:\n  int get_member() { return mem_; }\nprivate: \n  int mem_; \n};\n```\n\n分析代码后，将生成*中间代码*(这是一个抽象的例子，旨在展示中间代码生成的思想；编译器可能在实现上有所不同):\n\n```cpp\nstruct A { \n  int mem_; \n};\nint A_get_member(A* this) { return this->mem_; } \n```\n\n# 最佳化\n\n生成中间代码有助于编译器对代码进行优化。编译器经常尝试优化代码。优化不止一次完成。例如，下面的代码:\n\n```cpp\nint a = 41; \nint b = a + 1; \n```\n\n这将在编译过程中进行优化:\n\n```cpp\nint a = 41; \nint b = 41 + 1; \n```\n\n这将再次优化为以下内容:\n\n```cpp\nint a = 41; \nint b = 42; \n```\n\n一些程序员毫不怀疑，如今，编译器比程序员写得更好。\n\n# 机器代码生成\n\n编译器优化在中间代码和生成的机器代码中完成。那么当我们编译项目时是什么样的呢？在本章的前面，当我们讨论源代码的预处理时，我们看到了一个包含几个源文件的简单结构，包括两个头，`rect.h`和`square.h`，每个头都有自己的`.cpp`文件，以及包含程序入口点(`main()`函数)的`main.cpp` *、*。预处理后，以下单元作为编译器的输入:`main.cpp`、`rect.cpp`、`square.cpp`、*、*如下图所示:\n\n![](img/168390a7-7c88-4574-adb4-af6fce28f5ce.png)\n\n编译器将分别编译每一个。编译单元，也称为源文件，在某种程度上是相互独立的。编译器编译`main.cpp`时，调用的是`Rect`中的`get_area()`函数，不包括`main.cpp`中的`get_area()`实现。相反，它只是确保功能在项目中的某个地方实现。当编译器到达`rect*.*cpp`时，它不知道`get_area()`函数在某个地方被使用。\n\n以下是编译器在`main.cpp`通过预处理阶段后得到的结果:\n\n```cpp\n// contents of the iostream \nstruct Rect {\nprivate:\n  double side1_;\n  double side2_;\npublic:\n  Rect(double s1, double s2);\n  const double get_area() const;\n};\n\nstruct Square : Rect {\n  Square(double s);\n};\n\nint main() {\n  Rect r(3.1, 4.05);\n  std::cout << r.get_area() << std::endl;\n  return 0;\n}\n```\n\n分析`main.cpp`后，编译器生成如下中间代码(为了简单表达编译背后的思想，省略了很多细节):\n\n```cpp\nstruct Rect { \n  double side1_; \n  double side2_; \n};\nvoid _Rect_init_(Rect* this, double s1, double s2); \ndouble _Rect_get_area_(Rect* this); \n\nstruct Square { \n  Rect _subobject_; \n};\nvoid _Square_init_(Square* this, double s); \n\nint main() {\n  Rect r;\n  _Rect_init_(&r, 3.1, 4.05); \n  printf(\"%d\\n\", _Rect_get_area(&r)); \n  // we've intentionally replace cout with printf for brevity and \n  // supposing the compiler generates a C intermediate code\n  return 0;\n}\n```\n\n编译器将在优化代码时移除带有构造函数的`Square`结构(我们将其命名为`_Square_init_`，因为它从未在源代码中使用过。\n\n此时，编译器仅使用`main.cpp`操作，因此它看到我们调用了`_Rect_init_`和`_Rect_get_area_`函数，但没有在同一个文件中提供它们的实现。然而，由于我们事先提供了它们的声明，编译器信任我们，并且相信这些函数是在其他编译单元中实现的。基于这种信任和关于函数签名的最小信息(其返回类型、名称及其参数的数量和类型)，编译器生成一个包含`main.cpp`中的工作代码的目标文件，并以某种方式标记没有实现但被信任稍后解析的函数。解析由链接器完成。\n\n在下面的例子中，我们有生成的对象文件的简化变体，它包含两个部分——代码和信息。代码部分有每个指令的地址(十六进制值):\n\n```cpp\ncode: \n0x00 main\n 0x01 Rect r; \n  0x02 _Rect_init_(&r, 3.1, 4.05); \n  0x03 printf(\"%d\\n\", _Rect_get_area(&r)); \ninformation:\n  main: 0x00\n  _Rect_init_: ????\n  printf: ????\n  _Rect_get_area_: ????\n```\n\n看一下`information`部分。编译器用`????`标记代码段中所有未在同一编译单元中找到的函数。这些问号将被链接器在其他单元中找到的函数的实际地址替换。完成`main.cpp`后，编译器开始编译`rect.cpp`文件:\n\n```cpp\n// file: rect.cpp \nstruct Rect {\n  // #include \"rect.h\" replaced with the contents  \n  // of the rect.h file in the preprocessing phase \n  // code omitted for brevity \n};\nRect::Rect(double s1, double s2) \n  : side1_(s1), side2_(s2)\n{}\nconst double Rect::get_area() const { \n  return side1_ * side2_;\n} \n```\n\n遵循这里相同的逻辑，这个单元的编译产生以下输出(别忘了，我们还在提供抽象的例子):\n\n```cpp\ncode:  \n 0x00 _Rect_init_ \n  0x01 side1_ = s1 \n  0x02 side2_ = s2 \n  0x03 return \n  0x04 _Rect_get_area_ \n  0x05 register = side1_ \n  0x06 reg_multiply side2_ \n  0x07 return \ninformation: \n  _Rect_init_: 0x00\n  _Rect_get_area_: 0x04 \n```\n\n这个输出包含了所有函数的地址，所以不需要等待一些函数被解析。\n\n# 平台和目标文件\n\n我们刚才看到的抽象输出有点类似于编译器在编译一个单元后产生的实际目标文件结构。对象文件的结构取决于平台；例如在 *Linux 中，*以 *ELF* 格式表示( *ELF* 代表*可执行可链接格式*)。**平台**是执行程序的环境。在这种情况下，我们所说的平台是指计算机体系结构(更具体地说，*指令集体系结构*)和操作系统的结合。硬件和操作系统由不同的团队和公司设计和创建。他们每个人对设计问题都有不同的解决方案，这导致了平台之间的重大差异。平台在许多方面有所不同，这些差异也反映在可执行文件的格式和结构上。例如，Windows 系统中的可执行文件格式是**可移植可执行文件** ( **PE** )，它与 Linux 中的 ELF 格式有着不同的结构、编号和节的顺序。\n\n对象文件分为**部分**。对我们来说最重要的是代码段(标记为`.text`)和数据段(`.data`)。` .text`部分保存程序指令，`.data`部分保存指令使用的数据。数据本身可以分为几个部分，如*初始化*、*未初始化*、*只读*数据。\n\n除了`.text`和`.data`部分之外，对象文件的一个重要部分是**符号表**。符号表存储字符串(符号)到对象文件中位置的映射。在前面的例子中，编译器生成的输出有两部分，第二部分被标记为`information:`，它保存了代码中使用的函数的名称及其相对地址。这个`information:`是对象文件的实际符号表的抽象版本。符号表保存代码中定义的符号和代码中需要解析的符号。然后链接器使用这些信息将目标文件链接在一起，形成最终的可执行文件。\n\n# 引入链接\n\n编译器为每个编译单元输出一个目标文件。在前面的例子中，我们有三个`.cpp`文件，编译器产生了三个目标文件。链接器的任务是将这些目标文件组合成一个目标文件。将文件组合在一起会导致相对地址变化；例如，如果链接器将`rect.o`文件放在`main.o`之后，`rect.o`的起始地址将变为`0x04`，而不是`0x00`的前一个值:\n\n```cpp\ncode: \n 0x00 main\n  0x01 Rect r; \n  0x02 _Rect_init_(&r, 3.1, 4.05); \n  0x03 printf(\"%d\\n\", _Rect_get_area(&r)); \n 0x04 _Rect_init_ \n 0x05 side1_ = s1 \n 0x06 side2_ = s2 \n 0x07 return \n 0x08 _Rect_get_area_ \n 0x09 register = side1_ \n 0x0A reg_multiply side2_ \n 0x0B return \ninformation (symbol table):\n  main: 0x00\n  _Rect_init_: 0x04\n  printf: ????\n  _Rect_get_area_: 0x08 \n _Rect_init_: 0x04\n _Rect_get_area_: 0x08\n```\n\n链接器相应地更新符号表地址(在我们的例子中是`information:`部分)。如前所述，每个对象文件都有它的符号表，它将符号的字符串名称映射到它在文件中的相对位置(地址)。链接的下一步是解析对象文件中所有未解析的符号。\n\n现在链接器已经将`main.o`和`rect.o`组合在一起，它知道未解析符号的相对位置，因为它们现在位于同一个文件中。`printf`符号将以相同的方式解析，除了这次它将对象文件与标准库链接。所有的目标文件组合在一起后(为了简洁，我们省略了`square.o`的链接)，所有的地址都被更新，所有的符号都被解析，链接器输出最后一个可以被操作系统执行的目标文件。正如本章前面所讨论的，操作系统使用一种称为加载器的工具将可执行文件的内容加载到内存中。\n\n# 链接库\n\n库类似于可执行文件，但有一个主要区别:它没有`main()`函数，这意味着它不能作为常规程序调用。库用于组合可能在多个程序中重用的代码。例如，您已经通过包含`<iostream>`头将您的程序与标准库链接起来。\n\n库可以作为**静态**或**动态**库与可执行文件链接。当您将它们链接为静态库时，它们会成为最终可执行文件的一部分。操作系统还应该将动态链接库加载到内存中，以便为您的程序提供调用其函数的能力。假设我们想求一个函数的平方根:\n\n```cpp\nint main() {\n  double result = sqrt(49.0);\n}\n```\n\nC++ 标准库提供了`sqrt()`函数，该函数返回其参数的平方根。如果编译前面的例子，会产生一个错误，坚持说`sqrt`函数没有声明。我们知道，要使用标准库函数，我们应该包含相应的`<cmath>`头。但是头文件不包含函数的实现；它只是声明了函数(在`std`命名空间中)，然后包含在我们的源文件中:\n\n```cpp\n#include <cmath>\nint main() {\n  double result = std::sqrt(49.0);\n}\n```\n\n编译器将`sqrt`符号的地址标记为未知，链接器应该在链接阶段解析。如果源文件没有与标准库实现(包含库函数的目标文件)链接，链接器将无法解析它。\n\n如果链接是静态的，链接器生成的最终可执行文件将由我们的程序和标准库组成。另一方面，如果链接是动态的，链接器会在运行时标记要找到的`sqrt`符号。\n\n现在，当我们运行程序时，加载程序也加载动态链接到我们程序的库。它还将标准库的内容加载到内存中，然后解析`sqrt()`函数在内存中的实际位置。已经加载到内存中的同一个库也可以被其他程序使用。\n\n# 摘要\n\n在这一章中，我们谈到了 C++ 20 的许多新特性中的一些，现在准备深入探讨这种语言。我们讨论了构建 C++ 应用的过程及其编译阶段。这包括分析代码以检测语法和语法错误，生成中间代码以进行优化，最后生成目标文件，该文件将与其他生成的目标文件链接在一起以形成最终的可执行文件。\n\n在下一章中，我们将学习 C++ 数据类型、数组和指针。我们还将了解什么是指针，并了解条件句的低级细节。\n\n# 问题\n\n1.  编译器和解释器有什么区别？\n2.  列出程序编译阶段。\n3.  预处理器是做什么的？\n4.  链接器的任务是什么？\n5.  静态链接库和动态链接库有什么区别？\n\n# 进一步阅读\n\n更多信息请参考[https://www . Amazon . com/Advanced-C-computing-Milan-斯蒂凡诺维奇/dp/1430266678/](https://www.amazon.com/Advanced-C-Compiling-Milan-Stevanovic/dp/1430266678/) 上的*A*T2】高级 C 和 C++ 编译\n\nLLVM Essentials，https://www . packtpub . com/application-development/LLVM-Essentials"
  },
  {
    "path": "docs/exp-cpp/02.md",
    "content": "# 二、C++ 低级编程\n\n最初，C++ 被认为是 C 语言的继承者；然而，从那以后，它演变成了一些大的，有时可怕的，甚至不可救药的东西。随着最近的语言更新，它现在代表了一种复杂的野兽，需要时间和耐心来驯服。我们将从几乎每种语言都支持的基本构造开始这一章，例如数据类型、条件和循环语句、指针、结构和函数。我们将从一个低级系统程序员的角度来看这些结构，好奇一条简单的指令是如何被计算机执行的。在为更高级和抽象的主题(如面向对象编程)建立坚实的基础时，对这些基本结构的深入理解是必不可少的。\n\n在本章中，我们将了解以下方面的更多信息:\n\n*   程序执行的细节及其入口点\n*   `main()`函数的特殊性质\n*   函数调用和递归背后的复杂性\n*   内存段和寻址基础\n*   数据类型和变量如何驻留在内存中\n*   指针和数组\n*   条件句和循环的低级细节\n\n# 技术要求\n\n带有选项`--std=c++ 2a`的 g++ 编译器用于编译整个章节的示例。\n\n你可以在[https://github.com/PacktPublishing/Expert-CPP](https://github.com/PacktPublishing/Expert-CPP)找到本章用到的源文件。\n\n# 程序执行\n\n在[第 1 章](01.html)、*构建 C++ 应用*中，我们了解到编译器在编译源代码后会生成一个可执行文件。可执行文件包含可复制到计算机内存中的机器代码，由**中央处理器** ( **中央处理器**)运行。这种复制是由操作系统的内部工具加载程序完成的。所以**操作系统** ( **OS** )将程序的内容复制到内存中，并通过将其第一条指令传递给 CPU 来开始执行程序。\n\n# main()\n\n程序执行从`main()`功能开始，该功能是标准中规定的程序的*指定开始。输出`Hello, World!`消息的简单程序如下所示:*\n\n```cpp\n#include <iostream>\nint main() {\n  std::cout << \"Hello, World!\" << std::endl;\n  return 0;\n}\n```\n\n您可能在程序中遇到或使用了`main()`函数的参数。它有两个参数，`argc`和`argv,`允许从环境中传递字符串，通常被称为**命令行参数**。\n\n`argc`和`argv`这两个名字都是约定俗成的，你想要什么都可以代替。`argc`参数保存传递给`main()`函数的命令行参数的数量；`argv`论点支持以下论点:\n\n```cpp\n#include <iostream>\nint main(int argc, char* argv[]) {\n std::cout << \"The number of passed arguments is: \" << argc << std::endl;\n std::cout << \"Arguments are: \" << std::endl;\n for (int ix = 1; ix < argc; ++ ix) {\n   std::cout << argv[ix] << std::endl;\n }\n return 0;\n}\n```\n\n例如，我们可以使用以下参数编译并运行前面的示例:\n\n```cpp\n$ my-program argument1 hello world --some-option\n```\n\n这将向屏幕输出以下内容:\n\n```cpp\nThe number of passed arguments is: 5\nArguments are:\nargument1\nhello\nworld\n--some-option\n```\n\n当你看论点的数量时，你会注意到它是`5`。第一个参数始终是程序的名称；这就是为什么我们在示例中跳过了它，从数字`1`开始循环。\n\nRarely, you can see a widely supported but not standardized third argument, most commonly named `envp`. The type of `envp` is an array of `char` pointers and it holds the environment variables of the system.\n\n程序可以包含很多函数，但是程序的执行总是从`main()`函数开始，至少从程序员的角度来看是这样的。让我们试着编译以下代码:\n\n```cpp\n#include <iostream>\n\nvoid foo() {\n  std::cout << \"Risky foo\" << std::endl;\n}\n\n// trying to call the foo() outside of the main() function\nfoo();\n\nint main() {\n  std::cout << \"Calling main\" << std::endl;\n  return 0;\n}\n```\n\ng++ 在`foo();`调用`C++ requires a type specifier for all declarations`时引发错误。该调用被解析为声明，而不是要执行的指令。在`main()`之前我们尝试调用函数的方式对于经验丰富的开发人员来说可能看起来很傻，所以让我们尝试另一种方式。如果我们声明一个在初始化过程中调用函数的东西呢？在下面的例子中，我们定义了一个`BeforeMain`结构，构造函数打印了一条消息，然后我们在全局范围内声明了一个类型为`BeforeMain`的对象:\n\n```cpp\n#include <iostream>\n\nstruct BeforeMain {\n  BeforeMain() {\n std::cout << \"Constructing BeforeMain\" << std::endl;\n }\n};\n\nBeforeMain b;\n\nint main() {\n  std::cout << \"Calling main()\" << std::endl;\n  return 0;\n}\n```\n\n该示例成功编译，程序输出如下内容:\n\n```cpp\nConstructing BeforeMain\nCalling main()\n```\n\n如果我们在`BeforeMain`中添加一个成员函数并尝试调用它会怎么样？请参见下面的代码来理解这一点:\n\n```cpp\nstruct BeforeMain {\n  // constructor code omitted for brevity\n void test() {\n std::cout << \"test function\" << std::endl;\n }\n};\n\nBeforeMain b;\nb.test(); // compiler error\n\nint main() {\n  // code omitted for brevity\n}\n```\n\n对`test()`的调用不会成功。所以我们不能在`main()`之前调用函数，但是我们可以声明变量——默认情况下会被初始化的对象。所以在真正调用`main()`之前，肯定有一些东西会进行*初始化*。原来`main()`功能并不是一个程序的真正起点。程序的实际启动函数准备环境，即收集传递给程序的参数，然后调用`main()`函数。这是必需的，因为 C++ 支持需要在程序开始之前初始化的全局和静态对象，这意味着在调用`main()`函数之前。在 Linux 世界中，这个功能被称为`__libc_start_main`。编译器通过调用`__libc_start_main`来扩充生成的代码，而在调用`main()`函数之前，它可能调用也可能不调用其他初始化函数。抽象地说，假设前面的代码将被修改为类似于下面的内容:\n\n```cpp\nvoid __libc_start_main() {\n  BeforeMain b;\n  main();\n}\n__libc_start_main(); // call the entry point\n```\n\n在接下来的章节中，我们将更详细地研究入口点。\n\n# main()的特殊属性\n\n我们的结论是`main()`实际上不是程序的入口点，尽管标准规定它是指定的起点。编译器特别注意`main()`。它的行为类似于常规的 C++ 函数，但是除了是第一个被调用的函数之外，它还有其他特殊的属性。首先，它是唯一可以省略`return`语句的函数:\n\n```cpp\nint main() {\n  // works fine without a return statement\n}\n```\n\n返回值表示执行成功。通过返回`0`，我们的目的是告诉控件`main()`结束成功，所以如果控件到达结束时没有遇到对应的`return`语句，它将认为调用成功，效果与`return 0;`相同。\n\n`main()`函数的另一个有趣的性质是它的返回类型不能自动推导。不允许使用`auto`占位符类型说明符，它表示返回类型将从函数的`return`语句中推导出来。以下是常规函数的工作原理:\n\n```cpp\n// C++ 11\nauto foo() -> int {\n  std::cout << \"foo in alternative function syntax\" << std::endl;\n  return 0; } // C++ 14 auto foo() {\n  std::cout << \"In C++ 14 syntax\" << std::endl;\n  return 0;\n}\n```\n\n通过放置`auto`说明符，我们告诉编译器自动推导`return`类型。在 C++ 11 中，我们还将类型名放在箭头(`->`)之后，尽管第二种语法更短。考虑`get_ratio()`函数，它以整数形式返回标准比率:\n\n```cpp\nauto get_ratio(bool minimum) {\n  if (minimum) {\n return 12; // deduces return type int\n  }\n return 18; // fine: get_ratio's return type is already deduced to int\n}\n```\n\nTo successfully compile C++ code containing new features specified in C++ 11, C++ 14, C++ 17, or C++ 20, you should use proper compiler flags. When compiling with g++, use the `--std` flag and specify the standard version. The recommended value is **`--std=c++ 2a`**.\n\n这个例子编译成功，但是看看当我们用`main()`函数尝试同样的技巧时会发生什么:\n\n```cpp\nauto main() {\n  std::cout << get_ratio(true);\n  return 0;\n}\n```\n\n编译器将产生以下错误:\n\n*   `cannot initialize return object of type 'auto' with an rvalue of type 'int'`\n*   `'main' must return 'int'`。\n\n`main()`功能出现了一些奇怪的情况。这是因为`main()`函数允许省略`return`语句，但是对于编译器来说，`return`语句必须存在才能支持自动`return`类型推演。\n\n重要的是要记住，如果有多个`return`语句，它们必须都推导到同一个类型。让我们假设我们需要一个更新版本的函数，它返回一个整数值(如前面的例子所示)，并且如果指定，返回一个更精确的`float`值:\n\n```cpp\nauto get_ratio(bool precise = false) {\n  if (precise) {\n    // returns a float value\n    return 4.114f;\n  }\n  return 4; // returns an int value\n}\n```\n\n前面的代码编译不成功，因为有两个推导类型不同的`return`语句。\n\n# 康斯特布尔\n\n`constexpr`说明符声明可以在编译时计算该函数的值。同样的定义也适用于变量。名字本身由`const`和`expression`组成。这是一个有用的特性，因为它允许最大限度地优化您的代码。让我们看看下面的例子:\n\n```cpp\nint double_it(int number) {\n  return number * 2;\n}\n\nconstexpr int triple_it(int number) {\n  return number * 3;\n}\n\nint main() {\n  int doubled = double_it(42);\n  int tripled = triple_it(42);\n  int test{0};\n  std::cin >> test; \n  int another_tripled = triple_it(test);\n} \n```\n\n让我们看看编译器如何修改前面例子中的`main()`函数。假设编译器不会自行优化`double_it()`函数(例如，使其成为*内联*函数)，`main()`函数将采用以下形式:\n\n```cpp\nint main() {\n  int doubled = double_it(42);\n int tripled = 126; // 42 * 3  int test = 0;  std::cin >> test;\n  int another_tripled = triple_it(test);\n}\n```\n\n`constexpr`不保证函数值在编译时计算；但是，如果在编译时`constexpr`函数的输入是已知的，编译器就能够这样做。这就是为什么前面的例子直接转换为`tripled`变量的计算值`126`，而对`another_tripled`变量没有影响，因为编译器(和我们)都不知道输入。\n\n**C++ 20** introduces the `consteval` specifier, allowing you to insist on the compile-time evaluation of the function result. In other words, a `consteval` function produces a constant expression at compile time. The specifier makes the function an *immediate* one, which will produce an error if the function call cannot lead to a constant expression. The `main()` function cannot be declared as `constexpr`.\n\nC++ 20 还引入了`constinit`说明符。我们使用`constinit`来声明一个具有静态或线程存储持续时间的变量。我们将在[第 8 章](08.html)、*并发和多线程*中讨论线程存储持续时间。与`constinit`最显著的区别在于，我们可以将其用于没有`constexpr`析构函数的对象。这是因为`constexpr`要求对象有静态初始化和不断销毁。此外，`constexpr`使对象常量化，而`constinit`则没有。但是，`constinit`要求对象有静态初始化。\n\n# 递归\n\n`main()`的另一个特殊性质是不能递归调用。从操作系统的角度来看，`main()`函数是程序的入口点，所以再次调用就意味着一切重新开始；因此，禁止使用。然而，仅仅因为函数调用自身就调用函数是部分正确的。例如，`print_number()`函数调用自己并且从不停止:\n\n```cpp\nvoid print_number(int num) {\n std::cout << num << std::endl;\n print_number(num + 1); // recursive call\n}\n```\n\n调用`print_number(1)`功能会输出数字`1`、`2`、`3`等等。这更像是一个无限调用自己的函数，而不是一个正确的递归函数。我们应该增加一些属性来使`print_number()`函数成为一个有用的递归函数。首先，递归函数必须有一个基本情况，当进一步的函数调用停止时，这意味着递归停止传播。例如，如果我们想打印多达 100 个数字，我们可以为`print_number()`功能制作这样的场景:\n\n```cpp\nvoid print_number(int num) {\n if (num > 100) return; // base case\n  std::cout << num << std::endl;\n print_number(num + 1); // recursive call\n}\n```\n\n函数还有一个属性是递归的:解决最终会导致基本情况的小问题。在前面的例子中，我们已经通过解决函数的一个小问题，即打印一个数字，得到了它。打印完一个数字后，我们进入下一个小问题:打印下一个数字。最后，我们到了基本情况，我们完成了。函数调用自身没有任何魔力；可以把它想象成一个函数用相同的实现调用不同的函数。真正有趣的是递归函数如何影响程序的整体执行。让我们看一个从其他函数调用函数的简单例子:\n\n```cpp\nint sum(int n, int m) { return n + m; }\nint max(int x, int y) { \n  int res = x > y ? x : y; \n  return res;\n}\nint calculate(int a, int b) {\n  return sum(a, b) + max(a, b);\n}\n\nint main() {\n  auto result = calculate(11, 22);\n  std::cout << result; // outputs 55\n}\n```\n\n调用函数时，会为其参数和局部变量分配内存空间。程序从`main()`函数开始，在本例中，该函数通过传递文字值`11`和`22`来调用`calculate()`函数。控制*跳转*到`calculate()`功能，`main()`功能有点像*保持*；它等待直到`calculate()`功能返回继续执行。`calculate()`函数有两个参数，`a`和`b`；虽然我们对`sum()`、`max()`和`calculate()`的参数命名不同，但我们可以在所有函数中使用相同的名称。为这两个参数分配了内存空间。假设一个 int 占用 4 个字节的内存，因此`calculate()`函数至少需要 8 个字节才能成功执行。分配 8 个字节后，值`11`和`22`应复制到相应的位置(详见下图):\n\n![](img/094082a9-5d6c-4a64-8dcf-6480fc9f38e9.png)\n\n`calculate()`函数调用函数`sum()`和`max()`，并将其参数值传递给它们。相应地，它等待这两个函数依次执行，以便形成返回到`main()`的值。`sum()`和`max()`功能不同时调用。首先，调用`sum()`，将变量`a`和`b`的值复制到为`sum()`的参数分配的位置，命名为`n`和`m`，这两个变量总共也需要 8 个字节。请看下图，以便更好地理解这一点:\n\n![](img/04c7e009-48fb-448d-b6aa-1909c9f31f6d.png)\n\n计算并返回它们的总和。在函数完成并返回一个值后，内存空间被释放。这意味着变量`n`和`m`不再可访问，它们的位置可以重用。\n\nWe don't consider temporary variables at this point. We will revisit this example later to show the hidden details of function execution, including temporary variables and how to avoid them as much as possible.\n\n`sum()`返回值后，调用`max()`函数。它遵循相同的逻辑:内存被分配给参数`x`和`y`，以及`res`变量。我们有意将三元运算符`(?:)`的结果存储在`res`变量中，以使`max()`函数为本例分配更多空间。因此，总共有 12 个字节分配给`max()`功能。此时，x `main()`功能仍处于保持状态，等待`calculate()`，而`calculate()`又处于保持状态，等待`max()`功能完成(详见下图):\n\n![](img/1a6cd36b-9a99-4e1f-9f25-38221c7045e6.png)\n\n当`max()`完成时，分配给它的内存被释放，它的返回值被`calculate()`用来形成一个要返回的值。同样，当`calculate()`返回时，内存被释放，`main()`函数的局部变量结果将包含`calculate()`返回的值。\n\n`main()`函数随后完成工作，程序退出，也就是说，操作系统释放分配给程序的内存，以后可以将其重新用于其他程序。所描述的为函数分配和释放内存(解除分配)的过程是使用称为堆栈的概念来完成的。\n\nA stack is a data structure *adapter*, which has its rules to insert and access the data inside of it. In the context of function calls, the stack usually means a memory segment provided to the program that automatically manages itself following the rules of the stack data structure adapter. We will discuss this in more detail later in this chapter.\n\n回到递归，当函数调用自己时，内存应该分配给新调用的函数的参数和局部变量(如果有的话)。函数再次调用自己，这意味着堆栈将继续增长(为新函数提供空间)。我们调用同一个函数没关系；从栈的角度来看，每一个新的调用都是对一个完全不同的函数的调用，所以它会一边给它分配空间，脸上带着严肃的表情，一边吹着自己喜欢的歌。请看下图:\n\n![](img/bf484616-ddb5-4701-b9f2-f35a455c23b0.png)\n\n递归函数的第一次调用处于保持状态，等待同一个函数的第二次调用，后者又处于保持状态，等待第三次调用完成并返回一个值，后者又处于保持状态，依此类推。如果函数存在 bug 或者递归基数难以到达，栈迟早会过度增长，导致程序崩溃，原因称为**栈溢出**。\n\nThough recursion provides more elegant solutions to a problem, try to avoid recursion in your programs and use the iterative approach (loops). In mission-critical system development guidelines such as the navigation system of a Mars rover, using recursion is completely prohibited.\n\n在[第 1 章](01.html)*构建 C++ 应用*中，我们提到了协同程序。虽然我们将在本书的后面详细讨论它们，但是您应该注意到主函数不能是协同函数。\n\n# 使用数据\n\n当我们提到计算机内存时，我们默认考虑**随机存取存储器** ( **随机存取存储器**)，随机存取存储器也是静态随机存取存储器或动态随机存取存储器的统称；除非另有说明，否则我们将默认使用 DRAM。为了理清思路，让我们看一下下图，它展示了内存层次结构:\n\n![](img/547620d9-ff05-460c-b5ea-b029ffc5fb94.png)\n\n当我们编译一个程序时，编译器将最终的可执行文件存储在硬盘中。为了运行可执行文件，它的指令被加载到内存中，然后由中央处理器逐个执行。这使我们得出结论，任何需要执行的指令都应该在内存中。这是部分正确的。负责运行和监控程序的环境起着主要作用。\n\n我们编写的程序在托管环境中执行，托管环境在操作系统中。操作系统不是直接将程序的内容(其指令和数据，即进程)加载到内存中，而是加载到***虚拟内存中，这种机制既可以方便地处理进程，也可以在进程之间共享资源。每当我们提到一个进程被加载到的内存时，我们指的是虚拟内存，虚拟内存反过来又将*的内容映射到内存中。****\n\n***Most of the time, we use the terms RAM, DRAM, virtual memory, and memory interchangeably, considering virtual memory as an abstraction around the physical memory (the DRAM). \n\n让我们从介绍内存结构开始，然后研究内存中的数据类型。\n\n# 虚拟内存\n\n内存由许多盒子组成，每个盒子都能够存储指定数量的数据。我们将这些框称为*存储单元*，考虑到每个单元可以存储代表 8 位的 1 个字节。每个存储单元都是唯一的，即使它们存储相同的值。唯一性是通过对单元进行寻址来实现的，这样每个单元在存储器中都有其唯一的地址。第一个单元格的地址为 **0** ，第二个单元格为 **1** ，依此类推。\n\n下图显示了存储器的摘录，每个单元都有其唯一的地址和存储 1 字节数据的能力:\n\n![](img/17fe7523-4981-49a8-902e-a920b256354e.png)\n\n前面的图可以用来抽象地表示物理和虚拟存储器。拥有额外的抽象层的意义在于管理流程和提供比物理内存更多的功能。例如，操作系统可以执行比物理内存更大的程序。以电脑游戏为例，一个程序占用了几乎 2 GB 的空间，一台电脑的物理内存为 512 MB。虚拟内存允许操作系统通过从物理内存中卸载旧部件并映射新部件来逐部分加载程序。\n\n虚拟内存也更好地支持内存中有多个程序，从而支持多个程序的并行(或伪并行)执行。这也提供了共享代码和数据的有效使用，例如动态库。每当两个不同的程序需要使用同一个库时，这个库的单个实例就可能存在于内存中，并被两个程序使用，而它们彼此之间却不知道。请看下图，它描述了加载到内存中的三个程序:\n\n![](img/f09b9ec9-be4f-4cf1-a8ab-8ad6a6af1ae5.png)\n\n在上图中有三个正在运行的程序；每个程序都占用了虚拟内存中的一些空间。**我的程序**完全包含在物理内存中，而**计算器**和**文本编辑器**部分映射到它。\n\n# 演说\n\n如前所述，每个存储单元都有其唯一的**地址**，这是每个单元唯一性的保证。地址通常以*十六进制*形式表示，因为它更短，转换成**二进制**比十进制数字更快。加载到虚拟内存中的程序运行并看到*逻辑*地址。这些地址，也称为虚拟地址，是*伪造的*，由操作系统提供，操作系统在需要时将*转换为*物理地址。为了优化翻译，中央处理器提供了**翻译后备缓冲器**，这是其**内存管理单元** ( **内存管理单元**)的一部分。翻译后备缓冲器缓存虚拟地址到物理地址的最近翻译。因此，高效的地址转换是一项软件/硬件任务。我们将在[第 5 章](05.html)、*内存管理和智能指针*中深入探讨地址结构和翻译细节。\n\n地址的长度定义了系统可操作的总内存大小。当您遇到 32 位系统或 64 位系统之类的语句时，它实际上意味着地址的长度，也就是说，地址是 32 位或 64 位长。地址越长，内存越大。为了清楚起见，让我们比较一个 8 位长的地址和一个 32 位长的地址。如前所述，每个存储单元能够存储 1 字节的数据，并且具有唯一的地址。如果地址长度为 8 位，则第一个存储单元的地址全为零— **0000 0000** 。下一个小区的地址大一，就是 **0000 0001** 等等。\n\n可以用 8 位表示的最大值是 **1111 1111** 。那么，一个 8 位的地址长度能代表多少个存储单元呢？这个问题值得更详细的回答。1 位可以表示多少个不同的值？两个！为什么这样因为 1 位可以代表 **1** 或 **0** 。2 位可以表示多少个不同的值？嗯， **00** 是一个值， **01** 是另一个值， **10** ，最后， **11** 。因此，总共四个不同的值可以用 2 位来表示。让我们做一张桌子:\n\n![](img/c23f0a1a-555a-4752-95ea-7a5494a651aa.png)\n\n我们可以在这里看到一个模式。一个数中的每个位置(每个位)可以有两个值，所以我们可以通过求*2<sup>N</sup>T5】来计算 *N* 位所代表的不同值的个数；因此，8 位表示的不同值的个数为 *2 <sup>8</sup> = 256* 。这意味着一个 8 位系统最多可以寻址 256 个存储单元。另一方面，32 位系统能够寻址 *2 <sup>32</sup> = 4 个 294 967 296* 存储单元，每个存储 1 字节的数据，即存储 *4294967296 * 1 字节= 4 GB* 的数据。*\n\n# 数据类型\n\n拥有数据类型到底有什么意义？为什么我们不能用 C++ 编程，用一些`var`关键字来声明变量，忘记`short`、`long`、`int`、`char`、`wchar`等变量呢？嗯，C++ 确实支持一个类似的构造，我们在本章前面已经使用过的`auto`关键字，一个所谓的*占位符类型说明符*。它被命名为占位符，因为它确实是一个占位符。我们不能(也绝不能)声明一个变量，然后在运行时改变它的类型。以下代码可能是有效的 JavaScript 代码，但绝对不是有效的 C++ 代码:\n\n```cpp\nvar a = 12;\na = \"Hello, World!\";\na = 3.14;\n```\n\n想象一下，C++ 编译器可以编译这段代码。`a`变量应该分配多少字节的内存？在声明`var a = 12;`时，编译器可以将其类型推导为`int`并指定 4 字节的内存空间，但是当变量将其值更改为`Hello, World!`时，编译器必须重新分配空间，或者发明一个名为`std::string`类型的新隐藏变量`a1`。然后，编译器会尝试在代码中查找对变量的每一次访问，这些访问是以字符串的形式进行的，而不是以整数或双精度的形式，并用隐藏的`a1`替换变量。编译器可能会退出，开始问自己生命的意义。\n\n我们可以用 C++ 声明类似于前面代码的内容，如下所示:\n\n```cpp\nauto a = 12;\nauto b = \"Hello, World!\";\nauto c = 3.14;\n```\n\n前面两个示例的区别在于，第二个示例声明了三种不同类型的三个不同变量。前面的非 C++ 代码只声明了一个变量，然后给它分配了不同类型的值。在 C++ 中你不能改变变量的类型，但是编译器允许你使用`auto`占位符，并通过赋值来推导变量的类型。\n\n理解类型是在编译时推导出来的，而像 JavaScript 这样的语言允许您在运行时推导出类型，这一点至关重要。后者是可能的，因为这种程序在虚拟机等环境中运行，而运行 C++ 程序的唯一环境是操作系统。C++ 编译器必须生成一个有效的可执行文件，该文件可以复制到内存中，并且在没有支持系统的情况下运行。这迫使编译器事先知道变量的实际大小。知道大小对于生成最终的机器代码很重要，因为访问变量需要它的地址和大小，为变量分配内存空间需要它应该占用的字节数。\n\nC++ 类型系统将类型分为两大类:\n\n*   **基本型** ( `int`、`double`、`char`、`void`)\n*   **复合类型**(指针、数组、类)\n\n该语言甚至支持特殊的类型特征`std::is_fundamental`和`std::is_compound`，以找出类型的类别，例如:\n\n```cpp\n#include <iostream>\n#include <type_traits>\n\nstruct Point {\n  float x;\n  float y;\n};\n\nint main() {\n  std::cout << std::is_fundamental_v<Point> << \" \"\n            << std::is_fundamental_v<int> << \" \"\n            << std::is_compound_v<Point> << \" \"\n            << std::is_compound_v<int> << std::endl;\n}\n```\n\n我们使用了`std::is_fundamental_v`和`std::is_compound_v`辅助变量模板，定义如下:\n\n```cpp\ntemplate <class T>\ninline constexpr bool is_fundamental_v = is_fundamental<T>::value;\ntemplate <class T>\ninline constexpr bool is_compound_v = is_compound<T>::value;\n```\n\n程序输出:`0 1 1 0`。\n\nYou can use the `std::boolalpha` I/O manipulator before printing the type categories to print `true` or `false` instead of `1` or `0`.\n\n基本类型多为`int`或`double`等算术类型；甚至`char`式也是算术。它实际上包含一个数字而不是一个字符，例如:\n\n```cpp\nchar ch = 65;\nstd::cout << ch; // prints A\n```\n\n一个`char`变量保存 1 个字节的数据，这意味着它可以表示 256 个不同的值(因为 1 个字节是 8 位，8 位可以用*2<sup>8</sup>T5】的方式来表示一个数字)。如果我们使用其中一个位作为*符号*位，例如，允许类型也支持负值，会怎么样？这就给我们留下了 7 位来表示实际值，按照同样的逻辑，它允许我们表示 27 个不同的值，即 128 个(包括 0 个)不同的正数值和相同数量的负值。排除 0 给出了符号`char`的-127 到+127 的范围。这种有符号和无符号的表示适用于几乎所有的整数类型。*\n\n因此，无论何时遇到这种情况，例如，int 的大小是 4 字节，也就是 32 位，您应该已经知道可以用无符号表示法表示数字 0 到 2 <sup>32</sup> ，用有符号表示法表示值-2 <sup>31</sup> 到+2 <sup>31</sup> 。\n\n# 两颗北极指极星\n\nC++ 是一种独特的语言，因为它提供了对低级细节的访问，例如变量的地址。我们可以使用`&`运算符获取程序中声明的任何变量的地址，如图所示:\n\n```cpp\nint answer = 42;\nstd::cout << &answer;\n```\n\n这段代码将输出类似如下的内容:\n\n```cpp\n0x7ffee1bd2adc\n```\n\n请注意地址的十六进制表示。虽然这个值只是一个整数，但它被用来存储在一个称为指针的特殊变量中。指针只是一个能够存储地址值的变量，支持`*`运算符(解引用)，允许我们找到存储在地址的实际值。\n\n例如，为了存储前面示例中变量答案的地址，我们可以声明一个指针，并将地址分配给它:\n\n```cpp\nint* ptr = &answer;\n```\n\n变量答案声明为`int`，通常占用 4 字节的内存空间。我们已经同意每个字节都有自己唯一的地址。我们能得出答案变量有四个唯一的地址吗？好吧，有和没有。它确实获取了四个不同但连续的内存字节，但是当对变量使用地址运算符时，它会返回其第一个字节的地址。让我们看一下声明几个变量的部分代码，然后说明它们是如何放在内存中的:\n\n```cpp\nint ivar = 26;\nchar ch = 't';\ndouble d = 3.14;\n```\n\n数据类型的大小是由实现定义的，尽管 C++ 标准规定了每种类型支持的最小值范围。让我们假设实现为`int`提供 4 个字节，为 double 提供 8 个字节，为`char`提供 1 个字节。前面代码的内存布局应该如下所示:\n\n![](img/f7b01d3f-b5b7-43ec-be24-1612dd507b39.png)\n\n注意内存布局中的`ivar`；它驻留在四个连续的字节中。\n\n每当我们取一个变量的地址时，无论它是驻留在一个字节中还是多于一个字节中，我们都会得到该变量第一个字节的地址。如果大小不影响地址操作符背后的逻辑，那我们为什么要声明指针的类型呢？为了存储上例中`ivar`的地址，我们应该将指针声明为`int*`:\n\n```cpp\nint* ptr = &ivar;\nchar* pch = &ch;\ndouble* pd = &d;\n```\n\n下图描述了前面的代码:\n\n![](img/957b67f2-4da3-4fa0-8a68-cd0cdd02ec7d.png)\n\n事实证明，在使用指针访问变量时，指针的类型至关重要。C++ 提供了解引用操作符(指针名称前的`*`符号):\n\n```cpp\nstd::cout << *ptr; // prints 26\n```\n\n它基本上是这样工作的:\n\n1.  读取指针的内容\n2.  查找与指针中的地址相等的存储单元地址\n3.  返回存储在该存储单元中的值\n\n问题是，如果指针指向驻留在多个存储单元中的数据会怎么样？这就是指针的类型。当对指针解引用时，它的类型用于确定它应该从它所指向的存储单元开始读取和返回多少字节。\n\n既然我们知道指针存储了变量第一个字节的地址，我们实际上可以通过向前移动指针来读取变量的任何字节。我们应该记住，地址只是一个数字，所以在它上面加上或减去另一个数字就会产生另一个地址。如果我们用`char`指针指向一个整数变量呢？\n\n```cpp\nint ivar = 26;\nchar* p = (char*)&ivar;\n```\n\n当我们试图取消引用`p`指针时，它将只返回`ivar`的第一个字节。\n\n现在，如果我们想移动到`ivar`的下一个字节，我们将`1`添加到`char`指针:\n\n```cpp\n// the first byte\n*p;\n// the second byte\n*(p + 1);\n// the third byte\n*(p + 2);\n\n// dangerous stuff, the previous byte\n*(p - 1);\n```\n\n请看下图；它清楚地显示了我们如何访问`ivar`整数的字节:\n\n![](img/b595b562-70fe-45cf-990d-c1b6b939ff9d.png)\n\n如果要读取第一个或最后两个字节，可以使用短指针:\n\n```cpp\nshort* sh = (short*)&ivar;\nstd::cout << *sh; // print the value in the first two bytes of ivar\nstd::cout << *(sh + 1); // print the value in the last two bytes of ivar\n```\n\nYou should be careful with pointer arithmetics, as adding or subtracting a number will actually move the pointer by the defined size of the data type. Adding 1 to an `int` pointer will add `sizeof(int) * 1` to the actual address.\n\n指针的大小呢？如前所述，指针只是一个特殊的变量，因为它可以存储一个内存地址，并提供一个解引用运算符来返回位于该地址的数据。因此，如果指针只是一个变量，它也应该驻留在内存中。我们可能会认为`char`指针的大小小于`int`指针的大小，只是因为`char`的大小小于`int`的大小。\n\n问题是:存储在指针中的数据与指针指向的数据类型无关。`char`和`int`指针都存储变量的地址，所以定义指针的大小，要考虑地址的大小。地址的大小由我们工作的系统定义。例如，在 32 位系统中，地址长度为 32 位，在 64 位系统中，地址长度为 64 位。这使我们得出一个合乎逻辑的结论:指针的大小是相同的，不管它指向的数据类型是什么:\n\n```cpp\nstd::cout << sizeof(ptr) << \" = \" << sizeof(pch) << \" = \" << sizeof(pd);\n```\n\n它将在 32 位系统中输出`4 = 4 = 4`，在 64 位系统中输出`8 = 8 = 8`。\n\n# 内存段\n\n存储器由段组成，程序段在加载期间通过这些存储器段分布。这些是人为划分的内存地址范围，便于操作系统管理程序。二进制文件也分为段，如代码和数据。我们前面提到了代码和数据。节是链接器所需的二进制文件的一部分，它使用链接器正常工作所需的节，并将加载器所需的节组合成段。\n\n基本上，当我们从运行时的角度讨论二进制文件时，我们指的是段。数据段包含程序所需和使用的所有数据，代码段包含处理完全相同数据的实际指令。然而，当我们提到数据时，我们并不是指程序中使用的每一条数据。让我们看一下这个例子:\n\n```cpp\n#include <iostream>\nint max(int a, int b) { return a > b ? a : b; }\nint main() {\n  std::cout << \"The maximum of 11 and 22 is: \" << max(11, 22);\n}\n```\n\n前面程序的代码段由`main()`和`max()`函数的指令组成，其中`main()`使用`cout`对象的`operator<<`打印消息，然后调用`max()`函数。哪些数据实际驻留在数据段中？是否包含`max()`函数的`a`和`b`参数？事实证明，数据段中包含的唯一数据是字符串`The maximum of 11 and 22 is:`，以及其他静态、全局或常量数据。我们没有声明任何全局或静态变量，所以唯一的数据是提到的消息。\n\n有趣的是`11`和`22`的值。这些是文字值，这意味着它们没有地址；因此它们不位于存储器中的任何地方。如果它们不在任何地方，那么它们在程序中的位置的唯一合理解释就是它们位于代码段中。他们是`max()`召唤指令的一部分。\n\n那么`max()`函数的`a`和`b`参数呢？虚拟内存中负责存储具有自动存储持续时间的变量的段来了——堆栈。如前所述，堆栈自动处理局部变量和函数参数的内存空间分配/释放。调用`max()`函数时，参数`a`和`b`将位于堆栈中。一般来说，如果一个对象被称为具有自动存储持续时间，那么存储空间将在封闭块的开始被分配。因此，当调用该函数时，它的参数被推入堆栈:\n\n```cpp\nint max(int a, int b) {\n // allocate space for the \"a\" argument\n // allocate space for the \"b\" argument\n  return a > b ? a : b;\n // deallocate the space for the \"b\" argument\n // deallocate the space for the \"a\" argument\n}\n```\n\n当函数完成时，自动分配的空间将在封闭代码块的末尾释放。\n\nThe enclosing code block represents not only the function body but also the block of the conditional statements and loops. \n\n据说参数(或局部变量)从堆栈中弹出。 **Push** 和 **pop** 是堆栈上下文中使用的术语。通过*推动*将数据插入堆栈，通过*弹出*将数据从堆栈中取出。你可能遇到过**后进先出**这个术语，代表**后进先出**。这完美地描述了堆栈的推送和弹出操作。\n\n当程序运行时，操作系统提供固定大小的堆栈。堆栈的大小可以增长，如果增长到没有剩余空间的程度，就会因为堆栈溢出而崩溃。\n\n# 相助\n\n我们将堆栈描述为具有自动存储持续时间的变量管理器。*自动*这个词表明程序员不应该关心实际的内存分配和释放。只有预先知道数据的大小或数据的集合，才能实现自动存储持续时间。这样，编译器就知道函数参数和局部变量的数量和类型。在这一点上，它看起来非常好，但是程序倾向于处理动态数据——未知大小的数据。我们将在[第五章](05.html)、*内存管理和智能指针中详细学习动态内存管理；*现在，让我们看一下内存段的简化图，找出堆的用途:\n\n![](img/18f5425d-2362-474c-8e4c-3fd91432d3ad.png)\n\n程序使用堆段来请求比以前更多的内存空间。这是在运行时完成的，这意味着内存是在程序执行期间动态分配的。只要需要，程序就会向操作系统请求新的内存空间。操作系统实际上并不知道一个整数、一个用户定义的`Point`，甚至一组用户定义的`Point`是否需要内存。程序通过传递所需的实际字节大小来请求内存。例如，要为类型为`Point`的对象请求空间，`malloc()`功能可以如下使用:\n\n```cpp\n#include <cstdlib>\nstruct Point {\n  float x;\n  float y;\n};\n\nint main() {\n std::malloc(sizeof(Point));\n}\n```\n\nThe `malloc()` function came from the C language and to use it we need to include the `<cstdlib>` header file.\n\n`malloc()`函数分配一个连续的`sizeof(Point)`字节的内存空间——比如说 8 字节。然后，它返回该内存第一个字节的地址，因为这是提供空间访问的唯一方法。问题是，`malloc()`实际上并不知道我们是为一个`Point`对象还是一个`int`请求内存空间，它只是返回`void*`。`void*`存储分配内存的第一个字节的地址，但绝对不能因为`void`没有定义数据的大小，就通过解引用指针来获取实际数据。请看下图；它显示`malloc`在堆上分配内存:\n\n![](img/762fa3db-fc1e-4137-b71c-ee80cffd43f5.png)\n\n为了实际使用内存空间，我们需要将`void`指针转换为所需的类型:\n\n```cpp\nvoid* raw = std::malloc(sizeof(Point)); Point* p = static_cast<Point*>(raw); \n```\n\n或者，只需声明并用强制转换结果初始化指针:\n\n```cpp\nPoint* p = static_cast<Point*>(std::malloc(sizeof(Point))); \n```\n\nC++ 通过引入`new`运算符解决了这个难题，该运算符自动获取要分配的内存空间大小，并将结果转换为所需的类型:\n\n```cpp\nPoint* p = new Point;\n```\n\nDynamic memory management is a manual process; there is no similar construct to the stack that automatically deallocates the memory space if it is not required anymore. To manage the memory resource correctly, we should use the `delete` operator when we want to deallocate the space. We will find out the details in [Chapter 5](05.html) *Memory Management and Smart Pointers*.\n\n当我们访问`p`指向的`Point`对象的成员时会发生什么？取消引用`p`返回完整的`Point`对象，因此要更改成员`x`的值，我们应该执行以下操作:\n\n```cpp\n(*p).x = 0.24;\n```\n\n或者，更好的是，使用箭头操作符访问它:\n\n```cpp\np->x = 0.24;\n```\n\n我们将在[第 3 章](03.html)、*面向对象编程的细节中深入探讨用户定义的类型和结构。*\n\n# 数组\n\n数组是提供连续存储在内存中的数据集合的基本数据结构。许多适配器，如堆栈，都是使用数组实现的。它们的唯一性是数组元素都是同一类型的，这在访问数组元素时起着关键作用。例如，下面的声明创建了一个 10 个整数的数组:\n\n```cpp\nint arr[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};\n```\n\n数组的名称衰减为指向其第一个元素的指针。考虑到数组元素具有相同类型的事实，我们可以通过将指针前进到数组的第一个元素来访问数组的任何元素。例如，下面的代码打印数组的第三个元素:\n\n```cpp\nstd::cout << *(arr + 2);\n```\n\n第一个要素也是如此；以下三行代码做着同样的事情:\n\n```cpp\nstd::cout << *(arr + 0);\nstd::cout << *arr;\nstd::cout << arr[0];\n```\n\n为了确保`arr[2]`和`*(arr + 2)`做完全相同的事情，我们可以做以下事情:\n\n```cpp\nstd::cout << *(2 + arr);\n```\n\n将`2`移到`+`后面不会影响结果，所以下面的代码也是有效的:\n\n```cpp\nstd::cout << 2[arr];\n```\n\n它打印数组的第三个元素。\n\n数组元素在恒定时间内被访问，这意味着访问数组的第一个和最后一个元素需要相同的时间。这是因为每次我们访问数组元素时，我们都会执行以下操作:\n\n1.  通过添加相应的数值来推进指针\n2.  读取放置在结果指针处的存储单元的内容\n\n阵列的类型指示应该读取(或写入)多少存储单元。下图说明了访问:\n\n![](img/cbb9a70d-2cfc-471f-ae05-4d6e28d2ffca.png)\n\n当创建动态数组时，这个想法是至关重要的，动态数组是位于堆中而不是堆栈中的数组。正如我们已经知道的，从堆中分配内存给出了它的第一个字节的地址，所以访问第一个字节之外的元素的唯一机会是使用指针算法:\n\n```cpp\nint* arr = new int[10];\narr[4] = 2; // the same as *(arr + 4) = 2 \n```\n\n我们将在[第 6 章](06.html)、*深入研究 STL 中的数据结构和算法中讨论更多关于数组和其他数据结构的结构。*\n\n# 控制流\n\n几乎任何编程语言最基本的概念都是条件语句和循环。我们将详细探讨它们。\n\n# 条件式\n\n很难想象一个不包含条件语句的程序。检查函数的输入参数以确保它们的安全执行几乎是一种习惯。例如，`divide()`函数取两个参数，一个除以另一个，返回结果。很明显，我们需要确保除数不为零:\n\n```cpp\nint divide(int a, int b) {\n if (b == 0) {\n    throw std::invalid_argument(\"The divisor is zero\");\n  }\n  return a / b;\n}\n```\n\n条件句是编程语言的核心；毕竟，程序是行动和决策的集合。例如，下面的代码使用条件语句从两个输入参数中找出最大值:\n\n```cpp\nint max(int a, int b) {\n  int max;\n if (a > b) {\n    // the if block\n    max = a;\n } else {\n    // the else block\n    max = b;\n  }\n  return max;\n}\n```\n\n前面的例子为了表达`if` - `else`语句的用法，故意过于简化。然而，我们最感兴趣的是这样一个条件语句的实现。编译器遇到`if`语句时会生成什么？CPU 一个接一个地按顺序执行指令，指令就是做一件事的简单命令。我们可以在 C++ 这样的高级编程语言中单行使用复杂的表达式，而汇编指令则是一个周期只能做一个简单操作的简单命令:`move`、`add`、`subtract`等等。\n\n中央处理器从代码内存段中取出指令，对其进行解码，找出它应该做什么(移动数据、添加数字、减去数字)，然后执行该命令。\n\n为了以最快的速度运行，中央处理器将操作数和执行结果存储在名为**寄存器**的存储单元中。你可以把寄存器看作是中央处理器的临时变量。寄存器是位于中央处理器内的物理存储单元，因此与随机存取存储器相比，存取速度要快得多。要从汇编语言程序访问寄存器，我们使用它们指定的名称，如`rax`、`rbx`、`rdx`等。中央处理器的命令作用于寄存器，而不是内存单元；这就是为什么 CPU 必须将变量的内容从存储器复制到寄存器，执行操作并将结果存储在寄存器中，然后将寄存器的值复制回存储单元。\n\n例如，下面的 C++ 表达式只需要一行代码:\n\n```cpp\na = b + 2 * c - 1;\n```\n\n它看起来类似于下面的程序集表示(注释添加在分号之后):\n\n```cpp\nmov rax, b; copy the contents of \"b\" \n          ; located in the memory to the register rax\nmov rbx, c; the same for the \"c\" to be able to calculate 2 * c\nmul rbx, 2; multiply the value of the rbx register with \n          ; immediate value 2 (2 * c)\nadd rax, rbx; add rax (b) with rbx (2*c) and store back in the rax\nsub rax, 1; subtract 1 from rax\nmov a, rax; copy the contents of rax to the \"a\" located in the memory\n```\n\n条件语句建议跳过部分代码。例如，调用`max(11, 22)`意味着将省略`if`块。为了用汇编语言表达这一点，使用了跳转的思想。我们比较两个值，并根据结果跳转到代码的指定部分。我们给这个部分贴上标签，这样就可以找到指令集。例如，要跳过将`42`添加到寄存器`rbx`中，我们可以使用无条件跳转指令`jpm`将*跳转到标注为`UNANSWERED`的部分，如图所示:*\n\n```cpp\nmov rax, 2\nmov rbx, 0\njmp UNANSWERED\nadd rbx, 42; will be skipped\nUNANSWERED:\n  add rax, 1\n  ; ...\n```\n\n`jmp`指令执行无条件跳转；这意味着它在没有任何条件检查的情况下，在指定的标签上开始执行第一条指令。好消息是，中央处理器也提供了条件跳转。`max()`功能的主体将转换为以下汇编代码(简化)，其中`jg`和`jle`命令分别解释为大于的*跳转和小于或等于*的*跳转(基于使用`cmp`指令的比较结果):*\n\n```cpp\nmov rax, max; copy the \"max\" into the rax register\nmov rbx, a\nmov rdx, b\ncmp rbx, rdx; compare the values of rbx and rdx (a and b)\njg GREATER; jump if rbx is greater than rdx (a > b)\njl LESSOREQUAL; jump if rbx is lesser than\nGREATER:\n  mov rax, rbx; max = a\nLESSOREQUAL:\n  mov rax, rdx; max = b\n```\n\n在前面的代码中，标签`GREATER`和`LESSOREQUAL`表示之前实现的`max()`函数的`if`和`else`子句。\n\n# switch 语句\n\n像`switch`语句这样的条件使用相同的逻辑，如图所示:\n\n```cpp\nswitch (age) {\ncase 18:\n  can_drink = false;\n  can_code = true;\n  break;\ncase 21: \n  can_drink = true;\n  can_code = true;\n break;\ndefault: \n  can_drink = false;\n}\n```\n\n假设`rax`代表年龄，`rbx`代表`can_drink`，`rdx`代表`can_code`。前面的例子将转化为下面的组装说明(简化以表达基本思想):\n\n```cpp\ncmp rax, 18\nje CASE_18\ncmp rax, 21\nje CASE_21\nje CASE_DEFAULT\nCASE_18:\n  mov rbx, 0; cannot drink\n  mov rdx, 1; can code\n  jmp BEYOND_SWITCH; break\nCASE_21:\n mov rbx, 1\n mov rdx, 1\n jmp BEYOND_SWITCH\nCASE_DEFAULT:\n mov rbx, 0\nBEYOND_SWITCH:\n  ; ....\n```\n\n每个`break`语句都翻译成跳转到`BEYOND_SWITCH`标签，所以如果我们忘记了`break`关键字，例如`age`是`18`的情况下，执行也会通过`CASE_21`到达。这就是为什么你不应该忘记`break`声明。\n\n让我们找到一种避免在源代码中使用条件句的方法，既能使代码更短，又可能更快。我们将使用函数指针。\n\n# 用函数指针替换条件句\n\n之前，我们看了内存段，其中最重要的一段是代码段(也称为文本段)。该段包含程序映像，它是应该执行的程序指令。指令通常被分组到函数中，函数提供了一个唯一的名称，允许我们从其他函数中调用它们。函数位于可执行文件的代码段中。\n\n函数有它的地址。我们可以声明一个获取函数地址的指针，然后在以后使用它来调用该函数:\n\n```cpp\nint get_answer() { return 42; }\nint (*fp)() = &get_answer;\n// int (*fp)() = get_answer; same as &get_answer\n```\n\n函数指针的调用方式与原始函数相同:\n\n```cpp\nget_answer(); // returns 42\nfp(); // returns 42\n```\n\n假设我们正在编写一个程序，从输入中获取两个数字和一个字符，并对这些数字执行算术运算。操作由字符指定，无论是`+`、`-`、`*`还是`/`。我们实现了`add()`、`subtract()`、`multiply()`和`divide()`四个功能，并根据字符输入的值调用其中一个。\n\n我们将使用哈希表将操作的类型映射到指定的函数，而不是在一堆`if`语句或一个`switch`语句中检查字符的值:\n\n```cpp\n#include <unordered_map>\nint add(int a, int b) { return a + b; }\nint subtract(int a, int b) { return a - b; }\nint multiply(int a, int b) { return a * b; }\nint divide(int a, int b) { return (b == 0) ? 0 : a / b; }\n\nint main() {\n std::unordered_map<char, int (*)(int, int)> operations;\n operations['+'] = &add;\n operations['-'] = &subtract;\n operations['*'] = &multiply;\n operations['/'] = &divide;\n  // read the input \n  char op;\n  int num1, num2;\n  std::cin >> num1 >> num2 >> op;\n  // perform the operation, as follows\n operations[op](num1, num2);\n}\n\n```\n\n如您所见，`std::unordered_map`将`char`映射到定义为`(*)(int, int)`的函数指针。也就是说，它可以指向任何接受两个整数并返回一个整数的函数。\n\nThe hash table is represented by `std::unordered_map`, defined in the `<unordered_map>` header. We will discuss it in detail in  [Chapter 6](06.html), *Digging into Data Structures and Algorithms in STL*\n\n现在我们不需要写以下内容:\n\n```cpp\nif (op == '+') {\n  add(num1, num2);\n} else if (op == '-') {\n  subtract(num1, num2);\n} else if (op == '*') {\n  ...\n```\n\n相反，我们简单地调用由字符映射的函数:\n\n```cpp\noperations[op](num1, num2);\n```\n\nThough the use of a hash table is much prettier and looks more professional, you should take care of unexpected cases, such as invalid user input. \n\n# 作为类型的函数\n\n`unordered_map`的第二个参数是`int (*)(int, int)`，字面意思是指向取两个整数并返回一个整数的函数的指针。C++ 支持类模板`std::function`作为通用函数包装器，允许我们存储可调用对象，包括普通函数、lambda 表达式、函数对象等等。存储的对象被称为`std::function`的目标，如果没有目标，调用时会抛出`std::bad_function_call`异常。这有助于我们使`operations`哈希表接受任何可调用对象作为其第二个参数，并处理异常情况，如前面提到的无效字符输入。\n\n下面的代码块说明了这一点:\n\n```cpp\n#include <functional>\n#include <unordered_map>\n// add, subtract, multiply and divide declarations omitted for brevity\nint main() {\n  std::unordered_map<char, std::function<int(int, int)> > operations;\n  operations['+'] = &add;\n  // ...\n}\n```\n\n注意`std::function`的参数；它的形式是`int(int, int)`而不是`int(*)(int, int)`。使用`std::function`帮助我们处理异常情况。例如，调用`operations['x'](num1, num2);`将导致创建映射到角色`x`的空`std::function`。\n\n并且调用它将引发异常，因此我们可以通过正确处理调用来确保代码的安全:\n\n```cpp\n// code omitted for brevity\nstd::cin >> num1 >> num2 >> op;\ntry {\n operations[op](num1, num2);\n} catch (std::bad_function_call e) {\n  // handle the exception\n  std::cout << \"Invalid operation\";\n}\n```\n\n最后，我们可以使用 *lambda 表达式—* 未命名的函数，这些函数在适当的位置构造，并且能够捕获范围内的变量。例如，我们可以在将 lambda 表达式插入哈希表之前创建它，而不是声明前面的函数，然后将它们插入哈希表:\n\n```cpp\nstd::unordered_map<char, std::function<int(int, int)> > operations;\noperations['+'] = [](int a, int b) { return a + b; }\noperations['-'] = [](int a, int b) { return a * b; }\n// ...\nstd::cin >> num1 >> num2 >> op;\ntry {\n  operations[op](num1, num2);\n} catch (std::bad_functional_call e) {\n  // ...\n}\n```\n\nLambda 表达式将贯穿全书。\n\n# 环\n\n循环可能被认为是可重复的`if`语句，同样应该被翻译成 CPU 比较和跳转指令。例如，我们可以使用`while`循环计算从 0 到 10 的数字之和:\n\n```cpp\nauto num = 0;\nauto sum = 0;\nwhile (num <= 10) {\n  sum += num;\n  ++ num;\n}\n```\n\n这将转换为以下汇编代码(简化):\n\n```cpp\nmov rax, 0; the sum\nmov rcx, 0; the num\nLOOP:\n  cmp rbx, 10\n  jg END; jump to the END if num is greater than 10\n  add rax, rcx; add to sum\n  inc rcx; increment num\n  jmp LOOP; repeat\nEND:\n  ...\n```\n\nC++ 17 引入了可以在条件和循环中使用的 init 语句。在`while`循环外声明的`num`变量现在可以移入循环:\n\n```cpp\nauto sum = 0;\nwhile (auto num = 0; num <= 10) {\n  sum += num;\n  ++ num;\n}\n```\n\n同样的规则也适用于`if`语句，例如:\n\n```cpp\nint get_absolute(int num) {\n  if (int neg = -num; neg < 0) {\n    return -neg;\n  }\n  return num;\n}\n```\n\nC++ 11 引入了基于范围的`for`循环，使得语法更加清晰。例如，让我们使用新的`for`循环调用前面定义的所有算术运算:\n\n```cpp\nfor (auto& op: operations) {\n  std::cout << op.second(num1, num2);\n}\n```\n\n迭代`unordered_map`返回第一个和第二个成员的一对，第一个是键，第二个是映射到该键的值。C++ 17 让我们走得更远，允许我们编写如下同样的循环:\n\n```cpp\nfor (auto& [op, func]: operations) {\n  std::cout << func(num1, num2);\n}\n```\n\n了解编译器实际生成什么是设计和实现高效软件的关键。我们谈到了条件和循环的底层细节，这是几乎每个程序的基础。\n\n# 摘要\n\n在本章中，我们介绍了程序执行的细节。我们讨论了函数和具有一些特殊性质的`main()`函数。我们发现了递归是如何工作的，并且`main()`函数不能被递归调用。\n\n由于 C++ 是少数支持低级编程概念(如按地址访问内存字节)的高级语言之一，我们研究了数据如何驻留在内存中，以及如何在访问数据时结合指针。对于专业的 C++ 程序员来说，了解这些细节是必须的。\n\n最后，我们从汇编语言的角度讨论了条件句和循环。在本章中，我们介绍了 C++ 20 的特性。\n\n在下一章中，我们将了解更多关于**面向对象编程** ( **OOP** )的内容，包括语言对象模型的内部细节。我们将深入研究虚函数的细节，看看如何使用多态性。\n\n# 问题\n\n1.  `main()`功能有多少参数？\n2.  `constexpr`说明符是用来做什么的？\n3.  为什么建议使用迭代而不是递归？\n4.  栈和堆有什么区别？\n\n5.  如果声明为`int*`，那么`ptr`的大小是多少？\n6.  为什么对数组元素的访问被认为是恒定时间操作？\n7.  如果我们在`switch`语句的任何情况下忘记`break`关键字会发生什么？\n8.  如何将算术运算示例中的`multiply()`和`divide()`函数实现为λ表达式？\n\n# 进一步阅读\n\n关于本章所涉及的主题，您可以参考以下书籍了解更多信息: *C++ 高性能*，作者维克多·瑟尔和比约恩·安德里斯特([https://www.amazon.com/gp/product/1787120953](https://www.amazon.com/gp/product/1787120953))。***"
  },
  {
    "path": "docs/exp-cpp/03.md",
    "content": "# 三、面向对象编程的细节\n\n设计、实现和维护一个软件项目的困难来自于项目的复杂性。一个简单的计算器可以使用过程方法(即过程编程范例)编写，而银行账户管理系统太复杂，无法使用相同的方法实现。\n\nC++ 支持**面向对象编程(OOP)** ，这是一种基于将实体分解为存在于紧密互通的网络中的对象的范式。想象一下现实世界中拿遥控器换电视频道的简单场景。至少有三个不同的物体参与了这个动作:遥控器、电视，最重要的是你。为了用编程语言表达现实世界的对象及其关系，我们不必使用类、类继承、抽象类、接口、虚函数等等。上述特性和概念使设计和编码过程变得更加容易，因为它们允许我们以优雅的方式表达和分享想法，但它们不是强制性的。正如 C++ 的创造者比雅尼·斯特劳斯特鲁普所说，“不是每个程序都应该面向对象。”为了理解 OOP 范式的高级概念和特性，我们将尝试从幕后进行观察。在本书中，我们将深入探讨面向对象程序的设计。理解对象的本质及其关系，然后用它们来设计面向对象的程序，是本书的目标之一。\n\n在本章中，我们将详细了解以下主题:\n\n*   面向对象程序设计简介\n*   C++ 对象模型\n*   类关系，包括继承\n*   多态性\n*   有用的设计模式\n\n# 技术要求\n\n带有`-std=c++ 2a`选项的 g++ 编译器用于编译本章中的示例。\n\n你可以在[https://github.com/PacktPublishing/Expert-CPP](https://github.com/PacktPublishing/Expert-CPP)找到本章的源文件。\n\n# 理解对象\n\n大多数情况下，我们使用一组以某个名称分组的数据进行操作，从而形成一个**抽象**。像`is_military`、`speed`和`seats`这样的变量，如果分开来看，就没什么意义了。用`spaceship`将它们归类，改变了我们对存储在变量中的数据的感知方式。我们现在把许多变量打包成一个对象。为此，我们使用抽象；也就是说，我们从观察者的角度收集真实世界对象的个体属性。抽象是程序员工具链中的一个关键工具，因为它允许他们处理复杂性。C 语言引入了`struct`作为聚合数据的方式，如下面的代码所示:\n\n```cpp\nstruct spaceship {\n  bool is_military;\n  int speed;\n  int seats;\n};\n```\n\n数据分组对于面向对象编程来说有些必要。每组数据被称为一个对象。\n\n# 对象的低级细节\n\nC++ 尽力支持与 C 语言的兼容性。虽然 C 结构只是一个允许我们聚合数据的工具，但是 C++ 让它们等同于类，允许它们拥有构造函数、虚函数、继承其他结构等等。一个`struct`和一个`class`之间唯一的区别是默认的可见性修改器:结构的`public`和类的`private`。在类上使用结构通常没有区别，反之亦然。OOP 需要的不仅仅是数据聚合。为了充分理解面向对象程序设计，让我们找出如果我们只有提供数据聚合的简单结构，我们将如何合并面向对象程序设计范例。\n\n亚马逊或阿里巴巴等电子商务市场的核心实体是`Product`，我们以以下方式表示:\n\n```cpp\nstruct Product {\n  std::string name;\n  double price;\n  int rating;\n  bool available;\n};\n```\n\n如有必要，我们将向`Product`添加更多成员。`Product`类型对象的内存布局如下图所示:\n\n![](img/07ae95a7-5342-466b-84d1-60c5ffccb2a6.png)\n\n声明一个`Product`对象占用内存中的`sizeof(Product)`空间，而声明一个指向该对象的指针或引用占用存储地址所需的空间(通常为 4 或 8 字节)。请参见以下代码块:\n\n```cpp\nProduct book;\nProduct tshirt;\nProduct* ptr = &book;\nProduct& ref = tshirt;\n```\n\n我们可以将前面的代码描述如下:\n\n![](img/67e1849e-5135-4c27-ad35-ab9ef1638cfa.png)\n\n让我们从`Product`对象在内存中占据的空间开始。我们可以通过合计其成员变量的大小来计算`Product`对象的大小。`boolean`变量的大小是 1 字节。在 C++ 标准中没有规定`double`或`int`的确切尺寸。在 64 位机器中，`double`变量通常占用 8 字节，而`int`变量占用 4 字节。\n\n`std::string`的实现在标准中没有规定，所以它的大小取决于库的实现。`string`存储一个指向字符数组的指针，但它也可能存储分配的字符数，以便在调用`size()`时有效地返回。`std::string`的一些实现需要 8、24 或 32 字节的内存，但在我们的示例中，我们将坚持使用 24 字节。综上所述，`Product`的大小将如下:\n\n```cpp\n24 (std::string) + 8 (double) + 4 (int) + 1 (bool) = 37 bytes.\n```\n\n打印`Product`的尺寸输出不同的值:\n\n```cpp\nstd::cout << sizeof(Product);\n```\n\n它输出`40`而不是计算的 37 字节。冗余字节背后的原因是结构的填充，这是编译器用来优化对对象各个成员的访问的技术。**中央处理器**以固定大小的字读取内存。字的大小由中央处理器定义(通常是 32 或 64 位长)。如果数据是从一个字对齐的地址开始的，那么中央处理器能够立即访问数据。例如，`Product`的`boolean`数据成员需要 1 字节的内存，可以放在评级成员之后。事实证明，编译器会对齐数据以加快访问速度。让我们假设字长是 4 字节。这意味着，如果变量从一个可被 4 整除的地址开始，那么 CPU 将在没有冗余步骤的情况下访问该变量。编译器用额外的字节扩充了前面的结构，以便将成员与字边界地址对齐。\n\n# 对象的高级细节\n\n我们将对象视为代表抽象结果的实体。我们已经提到了观察者的角色，即基于问题域定义对象的程序员。程序员对此的定义代表了抽象的过程。让我们举一个电子商务市场及其产品的例子。两个不同的程序员团队可能对同一产品有不同的看法。实现网站的团队关心对网站访问者至关重要的对象的属性:购买者。我们之前在`Product`结构中展示的属性主要是针对网站访问者的，比如销售价格、产品评级等等。实现网站的程序员触及问题域，并验证定义`Product`对象所必需的属性。\n\n实现帮助管理仓库中产品的在线工具的团队关心对象的属性，这些属性在产品放置、质量控制和装运方面是必不可少的。这个团队实际上不应该关心产品的**评级**，甚至是它的**价格**。这个团队最关心的是产品的**重量**、**尺寸**、**条件**。下图显示了感兴趣的属性:\n\n![](img/d45f90f5-f11f-41e0-8392-1f11fd254d95.png)\n\n程序员在开始项目时应该做的第一件事是分析问题并收集需求。换句话说，他们应该熟悉*问题域*并定义*项目需求*。分析的过程导致定义对象及其类型，例如我们前面讨论的`Product`。为了从分析中获得适当的结果，我们应该在对象中思考，而通过在对象中思考，我们意味着考虑对象的三个主要属性:**状态**、**行为**和**身份**。\n\n# 状态\n\n每个对象都有一个状态，该状态可能与其他对象的状态不同，也可能不同。我们已经介绍了`Product`结构，它代表了物理(或数字)产品的抽象。`product`对象的所有成员共同代表该对象的状态。例如，`Product`包含`available`等成员，为布尔型；如果产品有货，它等于`true`。成员变量的值定义了对象的状态。如果为对象成员分配新值，其状态将会改变:\n\n```cpp\nProduct cpp_book; // declaring the object\n...\n// changing the state of the object cpp_book\ncpp_book.available = true;\ncpp_book.rating = 5;\n```\n\n对象的状态是其所有属性和值的组合。\n\n# 身份\n\n同一性是区别一个物体和另一个物体的东西。即使我们试图声明两个物理上无法区分的对象，它们的变量仍然会有不同的名称，即不同的标识:\n\n```cpp\nProduct book1;\nbook1.rating = 4;\nbook1.name = \"Book\";\nProduct book2;\nbook2.rating = 4;\nbook2.name = \"Book\";\n```\n\n上例中的对象具有相同的状态，但它们的不同之处在于我们对它们的称呼，即`book1`和`book2`。假设我们能够以某种方式创建同名的对象，如下面的代码所示:\n\n```cpp\nProduct prod;\nProduct prod; // won't compile, but still \"what if?\"\n```\n\n如果是这样的话，它们在内存中仍然会有不同的地址:\n\n![](img/a53517c2-6eb7-46a1-80ba-806d57324296.png)\n\n同一性是对象的一个基本属性，也是我们不能创建*空*对象的原因之一，例如:\n\n```cpp\nstruct Empty {};\n\nint main() {\n Empty e;\n  std::cout << sizeof(e);\n}\n```\n\n前面的代码不会像预期的那样输出`0`。标准中没有指定空对象的大小；编译器开发人员倾向于为这样的对象分配 1 个字节，尽管您可能会遇到 4 或 8 个字节。`Empty`的两个或多个实例在内存中应该有不同的地址，所以编译器必须确保对象将占用至少 1 字节的内存。\n\n# 行为\n\n在前面的例子中，我们将`5`和`4`分配给了`rating`成员变量。我们很容易通过给对象分配无效的值来使事情出乎意料地出错，如下所示:\n\n```cpp\ncpp_book.rating = -12;\n```\n\n`-12`就产品的评级而言是无效的，如果允许的话，会让用户感到困惑。我们可以通过提供**设置器**功能来控制对对象所做更改的行为:\n\n```cpp\nvoid set_rating(Product* p, int r) {\n  if (r >= 1 && r <= 5) {\n p->rating = r;\n }\n  // otherwise ignore\n}\n...\nset_rating(&cpp_book, -12); // won't change the state\n```\n\n一个对象对其他对象的请求起作用并做出反应。这些请求是通过函数调用来执行的，否则被称为**消息**:一个对象将一条消息传递给另一个对象。在前面的例子中，将相应的`set_rating`消息传递给`cpp_book`对象的对象代表我们在其中调用`set_rating()`函数的对象。在这种情况下，我们假设我们从`main()`调用函数，它实际上根本不代表任何对象。我们可以说它是全局对象，操作`main()`函数的对象，尽管在 C++ 中没有这样的实体。\n\n我们从概念上而不是物理上区分物体。这是思考物体的要点。面向对象编程的一些概念的物理实现并不规范，所以我们可以将`Product`结构命名为类，并声称`cpp_book`是`Product`的**实例**，它有一个名为`set_rating()`的成员函数。C++ 实现几乎做到了同样的事情:它提供了语法上方便的结构(类、可见性修饰符、继承等)，并将它们翻译成具有全局函数的简单结构，如前面例子中的`set_rating()`。现在，让我们深入研究 C++ 对象模型的细节。\n\n# 模仿班级\n\n结构允许我们对变量进行分组、命名和创建对象。类的思想是在对象中包含相应的操作，将适用于特定数据的数据和操作分组。例如，对于`Product`类型的对象，直接调用对象上的`set_rating()`函数是很自然的，而不是有一个单独的全局函数，通过指针获取`Product`对象并对其进行修改。然而，由于我们同意以 C 的方式使用结构，我们不能让它有成员函数。为了模仿使用 C 结构的类，我们必须将与`Product`对象一起工作的函数声明为全局函数，如以下代码所示:\n\n```cpp\nstruct Product {\n  std::string name;\n  double price;\n  int rating;\n  bool available;\n};\n\nvoid initialize(Product* p) {\n  p->price = 0.0;\n  p->rating = 0;\n  p->available = false;\n}\n\nvoid set_name(Product* p, const std::string& name) {\n  p->name = name;\n}\n\nstd::string get_name(Product* p) {\n  return p->name;\n}\n\nvoid set_price(Product* p, double price) {\n  if (price < 0 || price > 9999.42) return;\n  p->price = price;\n}\n\ndouble get_price(Product* p) {\n  return p->price;\n}\n\n// code omitted for brevity\n```\n\n要将结构用作类，我们应该以正确的顺序手动调用函数。例如，要使用具有正确初始化默认值的对象，我们必须首先调用`initialize()`函数:\n\n```cpp\nint main() {\n  Product cpp_book;\n initialize(&cpp_book);\n  set_name(&cpp_book, \"Mastering C++ Programming\");\n  std::cout << \"Book title is: \" << get_name(&cpp_book);\n  // ...\n}\n```\n\n这似乎是可行的，但是如果添加新的类型，前面的代码将很快变成一团乱麻。例如，考虑跟踪产品的`Warehouse`结构:\n\n```cpp\nstruct Warehouse {\n  Product* products;\n  int capacity;\n  int size;\n};\n\nvoid initialize_warehouse(Warehouse* w) {\n  w->capacity = 1000;\n  w->size = 0;\n  w->products = new Product[w->capacity];\n  for (int ix = 0; ix < w->capacity; ++ ix) {\n    initialize(&w->products[ix]); // initialize each Product object\n  }\n}\n\nvoid set_size(int size) { ... }\n// code omitted for brevity\n```\n\n第一个明显的问题是函数的命名。我们必须命名`Warehouse` `initialize_warehouse`的初始化函数，以避免与已经为`Product`声明的`initialize()`函数冲突。我们可以考虑为`Product`类型重新命名函数，以避免将来可能的冲突。接下来是功能混乱。现在，我们有一堆全局函数，随着新类型的增加，它们的数量会增加。如果我们添加一些类型的层次结构，这将更加难以管理。\n\n尽管编译器倾向于将类翻译成具有全局函数的结构，正如我们前面所展示的，C++ 和其他高级编程语言解决了这些和其他以前没有提到的问题，引入了具有平滑机制的类来将它们组织成层次结构。从概念上来说，关键字(`class`、`public`、或`private`)和机制(继承和多态性)是存在的，开发人员可以方便地组织他们的代码，但不会让编译器的生活变得更容易。\n\n# 与班级合作\n\n当处理对象时，类使事情变得容易得多。他们在 OOP 中做最简单的必要的事情:他们把数据和操作数据的函数结合起来。让我们用一个类及其强大的特性重写`Product`结构的例子:\n\n```cpp\nclass Product {\npublic:\n  Product() = default; // default constructor\n  Product(const Product&); // copy constructor\n  Product(Product&&); // move constructor\n\n  Product& operator=(const Product&) = default;\n  Product& operator=(Product&&) = default;\n  // destructor is not declared, should be generated by the compiler\npublic:\n  void set_name(const std::string&);\n  std::string name() const;\n  void set_availability(bool);\n  bool available() const;\n  // code omitted for brevity\n\nprivate:\n  std::string name_;\n  double price_;\n  int rating_;\n  bool available_;\n};\n\nstd::ostream& operator<<(std::ostream&, const Product&);\nstd::istream& operator>>(std::istream&, Product&);\n```\n\n类声明看起来更有组织性，尽管它公开了比我们用来定义类似结构更多的函数。下面是我们应该如何说明这个类:\n\n![](img/c8a27344-1681-4f83-97ea-90d8f78dda1a.png)\n\n上图有些特殊。如您所见，它有组织的部分，函数名称前的符号，等等。这种类型的图被称为**统一建模语言(UML)** 类图。UML 是一种标准化说明类及其关系的方法。第一部分是类名(粗体)，接下来是成员变量部分，然后是成员函数部分。函数名前面的`+`(加号)表示该函数是公共的。成员变量通常是私有的，但是，如果需要强调这一点，可以使用`-`(减号)。我们可以通过简单地说明这个类来省略所有细节，如下图所示:\n\n![](img/24e54100-c4fb-4c79-af94-8085e241f878.png)\n\n我们将在本书中使用 UML 图，并将根据需要引入新类型的图。在处理初始化、复制、移动、默认和删除的函数，当然还有运算符重载之前，让我们先搞清楚几件事。\n\n# 从编译器的角度来看\n\n首先，无论早期的类与之前引入的结构相比看起来有多可怕，编译器都会将其翻译成以下代码(为了简单起见，我们对其进行了略微修改):\n\n```cpp\nstruct Product {\n  std::string name_;\n  bool available_;\n  double price_;\n  int rating_;\n};\n\n// we forced the compiler to generate the default constructor\nvoid Product_constructor(Product&); \nvoid Product_copy_constructor(Product& this, const Product&);\nvoid Product_move_constructor(Product& this, Product&&);\n// default implementation\nProduct& operator=(Product& this, const Product&); \n// default implementation\nProduct& operator=(Product& this, Product&&); \n\nvoid Product_set_name(const std::string&);\n// takes const because the method was declared as const\nstd::string Product_name(const Product& this); \nvoid Product_set_availability(Product& this, bool b);\nbool Product_availability(const Product& this);\n\nstd::ostream& operator<<(std::ostream&, const Product&);\nstd::istream& operator>>(std::istream&, Product&);\n```\n\n基本上，编译器生成的代码与我们前面介绍的使用简单结构模拟类行为的方法相同。尽管编译器在实现 C++ 对象模型的技术和方法上有所不同，但前面的例子是编译器开发人员实践的流行方法之一。它平衡了访问对象成员(包括成员函数)的空间和时间效率。\n\n接下来，我们应该考虑编译器通过扩充和修改来编辑我们的代码。下面的代码声明了全局`create_apple()`函数，该函数创建并返回一个`Product`对象，其值特定于一个苹果。它还在`main()`函数中声明了一个图书对象:\n\n```cpp\nProduct create_apple() {\n Product apple;\n  apple.set_name(\"Red apple\");\n  apple.set_price(\"0.2\");\n  apple.set_rating(5);\n  apple.set_available(true);\n  return apple;\n}\n\nint main() {\n Product red_apple = create_apple();\n Product book;  Product* ptr = &book;\n  ptr->set_name(\"Alice in Wonderland\");\n  ptr->set_price(6.80);\n  std::cout << \"I'm reading \" << book.name() \n            << \" and I bought an apple for \" << red_apple.price()\n            << std::endl;\n}\n```\n\n我们已经知道，编译器修改类以将其转换为结构，并将成员函数移动到全局范围，每个成员函数都将类的引用(或指针)作为其第一个参数。为了支持客户端代码中的这些修改，它还应该修改对对象的所有访问。\n\nA line or lines of code that declare or use already declared class objects are referred to as **client code**.\n\n下面是我们如何假设编译器修改了前面的代码(我们使用了单词*假设*，因为我们试图引入一种编译器抽象的方法，而不是编译器特定的方法):\n\n```cpp\nvoid create_apple(Product& apple) {\n  Product_set_name(apple, \"Red apple\");\n  Product_set_price(apple, 0.2);\n  Product_set_rating(apple, 5);\n  Product_set_available(apple, true);\n  return;\n}\n\nint main() {\n  Product red_apple;\n Product_constructor(red_apple);\n create_apple(red_apple);\n  Product book;\n Product* ptr;\n Product_constructor(book);\n Product_set_name(*ptr, \"Alice in Wonderland\");\n Product_set_price(*ptr, 6.80);\n  std::ostream os = operator<<(std::cout, \"I'm reading \");\n  os = operator<<(os, Product_name(book));\n  os = operator<<(os, \" and I bought an apple for \");\n  os = operator<<(os, Product_price(red_apple));\n  operator<<(os, std::endl);\n  // destructor calls are skipped because the compiler \n  // will remove them as empty functions to optimize the code\n  // Product_destructor(book);\n  // Product_destructor(red_apple);\n}\n```\n\n编译器还优化了对`create_apple()`函数的调用，以避免创建临时对象。我们将在本章后面讨论编译器生成的不可见临时对象。\n\n# 初始化和销毁\n\n如前所示，对象的创建是一个两步过程:内存分配和初始化。内存分配是对象声明的结果。C++ 不关心变量的初始化；它分配内存(不管是自动的还是手动的)，然后就完成了。实际的初始化应该由程序员来完成，这就是为什么我们首先有一个构造函数。\n\n析构函数遵循同样的逻辑。如果我们跳过默认构造函数或析构函数的声明，编译器应该隐式生成它们，如果它们是空的，编译器也会删除它们(以消除对空函数的冗余调用)。如果声明了任何带有参数的构造函数，包括复制构造函数，编译器将不会生成默认构造函数。我们可以强制编译器隐式生成默认构造函数:\n\n```cpp\nclass Product {\npublic:\n Product() = default;\n  // ...\n};\n```\n\n我们也可以使用`delete`说明符强制其不生成编译器，如下所示:\n\n```cpp\nclass Product {\npublic:\n Product() = delete;\n  // ...\n};\n```\n\n这将禁止默认初始化的对象声明，即`Product p`；不会编译。\n\nDestructors are called in the order opposite to object declarations because the automatic memory allocation is managed by a stack and the stack, is a data structure adapter that follows the **last in, first out (****LIFO) **rule.\n\n对象初始化在其创建时发生。当对象不再可访问时，通常会发生破坏。当对象被分配到堆上时，后者可能很棘手。看看下面的代码；它在内存的不同范围和段中声明了四个`Product`对象:\n\n```cpp\nstatic Product global_prod; // #1\n\nProduct* foo() {\n  Product* heap_prod = new Product(); // #4\n  heap_prod->name = \"Sample\";\n  return heap_prod;\n}\n\nint main() {\n Product stack_prod; // #2\n  if (true) {\n    Product tmp; // #3\n    tmp.rating = 3;\n  }\n  stack_prod.price = 4.2;\n  foo();\n}\n```\n\n`global_prod`有一个静态存储时长，放在程序的全局/静态段；在调用`main()`之前进行初始化。当`main()`开始时，`stack_prod`被分配到堆栈上，当`main()`结束时将被销毁(函数的右大括号被认为是它的结束)。虽然条件表达式看起来很奇怪并且过于人为，但它是表达块范围的好方法。\n\n`tmp`对象也将在堆栈上分配，但其存储持续时间仅限于它已经声明的范围:当执行离开`if`块时，它将被自动销毁。这就是为什么堆栈上的变量有*自动存储持续时间*。最后，当调用`foo()`函数时，它声明`heap_prod`指针，该指针指向堆上分配的`Product`对象的地址。\n\n前面的代码包含内存泄漏，因为`heap_prod`指针(本身有自动存储持续时间)在执行到`foo()`结束时会被销毁，而堆上分配的对象不会受到影响。不要混淆指针和它所指向的实际对象:指针只包含对象的值，但它不代表对象。\n\nDon't forget to deallocate the memory that's dynamically allocated on the heap, either by manually calling the delete operator or using smart pointers. Smart pointers will be discussed in [Chapter 5](05.html), *Memory Management and Smart Pointers.*\n\n当函数结束时，堆栈上分配的参数和局部变量的内存将被释放，但当程序结束时，即`main()`函数结束后，`global_prod`将被销毁。当对象即将被销毁时，将调用析构函数。\n\n# 复制对象\n\n复制有两种:一种是对象的*深*复制，一种是对象的*浅*复制。该语言允许我们使用**复制构造器**和**赋值操作符**来管理复制初始化和对象赋值。这是程序员必备的功能，因为我们可以控制复制的语义。看看下面的例子:\n\n```cpp\nProduct p1;\nProduct p2;\np2.set_price(4.2);\np1 = p2; // p1 now has the same price\nProduct p3 = p2; // p3 has the same price\n```\n\n第`p1 = p2;`行是对赋值运算符的调用，而最后一行是对复制构造函数的调用。等号不应该让你混淆它是赋值还是复制构造函数调用。每次你看到一个声明后面跟着一个赋值，就认为它是一个复制结构。这同样适用于新的初始值设定项语法(`Product p3{p2};`)。\n\n编译器将生成以下代码:\n\n```cpp\nProduct p1;\nProduct p2;\nProduct_set_price(p2, 4.2);\noperator=(p1, p2);\nProduct p3;\nProduct_copy_constructor(p3, p2);\n```\n\n复制构造函数(和赋值运算符)的默认实现执行对象的成员复制，如下图所示:\n\n![](img/4a801266-0db0-4ca7-81e1-afa9b2da53a0.png)\n\n如果成员式副本产生无效副本，则需要自定义实现。例如，考虑以下`Warehouse`对象的副本:\n\n```cpp\nclass Warehouse {\npublic:\n  Warehouse() \n    : size_{0}, capacity_{1000}, products_{nullptr}\n  {\n    products_ = new Products[capacity_];\n  }\n\n  ~Warehouse() {\n    delete [] products_;\n  }\n\npublic:\n  void add_product(const Product& p) {\n    if (size_ == capacity_) { /* resize */ }\n    products_[size_++ ] = p;\n  }\n  // other functions omitted for brevity\n\nprivate:\n  int size_;\n  int capacity_;\n  Product* products_;\n};\n\nint main() {\n  Warehouse w1;\n  Product book;\n  Product apple;\n  // ...assign values to products (omitted for brevity)\n  w1.add_product(book);\n  Warehouse w2 = w1; // copy\n  w2.add_product(apple);\n  // something somewhere went wrong...\n}\n```\n\n前面的代码声明了两个`Warehouse`对象，然后两个不同的产品被添加到仓库中。虽然这个例子有点不自然，但它显示了默认复制实现的危险。下图向我们展示了代码中出现的错误:\n\n![](img/9cb93f9f-29a1-4e3d-a0df-d9b226e141d9.png)\n\n将 **w1** 分配给 **w2** 会产生以下结构:\n\n![](img/57f8bc17-d88a-4fd8-b03b-8263a6b20b2d.png)\n\n默认实现只是将`w1`的每个成员复制到`w2`。复制后，`w1`和`w2`的`products_`成员都指向堆中相同的位置。当我们给`w2`增加一个新的产品时，`w1`指向的数组受到影响。这是一个逻辑错误，可能会导致程序中出现未定义的行为。我们需要一个*深*而不是*浅*的副本；也就是说，我们实际上需要创建一个新的产品阵列，它有一个 w1 阵列的副本。\n\n复制构造函数和赋值操作符的自定义实现解决了*浅层*复制的问题:\n\n```cpp\nclass Warehouse {\npublic:\n  // ...\n  Warehouse(const Warehouse& rhs) {\n size_ = rhs.size_;\n capacity_ = rhs.capacity_;\n products_ = new Product[capacity_];\n for (int ix = 0; ix < size_; ++ ix) {\n products_[ix] = rhs.products_[ix];\n }\n }\n  // code omitted for brevity\n};  \n```\n\n复制构造函数的自定义实现创建一个新数组。然后，它一个接一个地复制源对象的数组元素，这样就消除了`product_`指针指向错误的内存地址。换句话说，我们通过创建一个新的数组来实现`Warehouse`对象的深度副本。\n\n# 移动物体\n\n临时对象在代码中无处不在。大多数时候，他们需要让代码按预期工作。例如，当我们将两个对象添加在一起时，会创建一个临时对象来保存`operator+`的返回值:\n\n```cpp\nWarehouse small;\nWarehouse mid;\n// ... some data inserted into the small and mid objects\nWarehouse large{small + mid}; // operator+(small, mid)\n```\n\n让我们来看看全局`operator+()`对于`Warehouse`对象的实现:\n\n```cpp\n// considering declared as friend in the Warehouse class\nWarehouse operator+(const Warehouse& a, const Warehouse& b) {\n  Warehouse sum; // temporary\n  sum.size_ = a.size_ + b.size_;\n  sum.capacity_ = a.capacity_ + b.capacity_;\n  sum.products_ = new Product[sum.capacity_];\n  for (int ix = 0; ix < a.size_; ++ ix) { sum.products_[ix] = a.products_[ix]; }\n  for (int ix = 0; ix < b.size_; ++ ix) { sum.products_[a.size_ + ix] = b.products_[ix]; }\n  return sum;\n}\n```\n\n前面的实现声明了一个临时对象，并在用必要的数据填充后返回它。上一个示例中的调用可以转换为以下内容:\n\n```cpp\nWarehouse small;\nWarehouse mid;\n// ... some data inserted into the small and mid objects\nWarehouse tmp{operator+(small, mid)};\nWarehouse large;\nWarehouse_copy_constructor(large, tmp);\n__destroy_temporary(tmp);\n```\n\nC++ 11 中引入的*移动语义*允许我们通过*将返回值移动到`Warehouse`对象中来跳过临时创建。为此，我们应该为`Warehouse`声明一个**移动构造器**，它可以*区分*临时对象并有效地处理它们:*\n\n```cpp\nclass Warehouse {\npublic:\n  Warehouse(); // default constructor\n  Warehouse(const Warehouse&); // copy constructor\n  Warehouse(Warehouse&&); // move constructor\n  // code omitted for brevity\n};\n```\n\n移动构造函数的参数是一个**右值引用** ( **& &** )。\n\n# 左值引用\n\n在理解为什么要首先引入右值引用之前，让我们先搞清楚`lvalues`、`references`和`lvalue-references`的事情。当变量是左值时，它可以被寻址，可以被指向，并且它有一个限定范围的存储持续时间:\n\n```cpp\ndouble pi{3.14}; // lvalue\nint x{42}; // lvalue\nint y{x}; // lvalue\nint& ref{x}; // lvalue-reference\n```\n\n`ref`是一个`lvalue reference`，一个可以被视为`const`指针的变量的同义词:\n\n```cpp\nint * const ref = &x;\n```\n\n除了通过引用修改对象的能力之外，我们还通过引用将重对象传递给函数，以优化和避免冗余的对象副本。例如，`Warehouse`的`operator+`通过引用获取两个对象*，从而使其复制对象的地址而不是完整的对象。*\n\n`Lvalue`引用在函数调用方面优化了代码，但是，为了优化临时引用，我们应该继续进行右值引用。\n\n# 右值引用\n\n我们不能将`lvalue`引用绑定到临时对象。以下代码无法编译:\n\n```cpp\nint get_it() {\n  int it{42};\n  return it;\n}\n...\nint& impossible{get_it()}; // compile error\n```\n\n我们需要声明一个`rvalue`引用，以便能够绑定到临时对象(包括文字值):\n\n```cpp\nint&& possible{get_it()};\n```\n\n`Rvalue`引用允许我们尽可能地跳过临时词的生成。例如，通过消除临时对象，将结果作为右值引用的函数运行得更快:\n\n```cpp\nvoid do_something(int&& val) {\n  // do something with the val\n}\n// the return value of the get_it is moved to do_something rather than copied\ndo_something(get_it()); \n```\n\n为了想象移动的效果，假设前面的代码将被翻译成下面的代码(只是为了获得移动的完整概念):\n\n```cpp\nint val;\nvoid get_it() {\n  val = 42;\n}\nvoid do_something() {\n  // do something with the val\n}\ndo_something();\n```\n\n在引入 move 之前，前面的代码如下所示(经过一些编译器优化):\n\n```cpp\nint tmp;\nvoid get_it() {\n  tmp = 42;\n}\nvoid do_something(int val) {\n  // do something with the val\n}\ndo_something(tmp);\n```\n\n当输入参数表示一个`rvalue`时，移动构造函数和移动操作符`=()`具有复制的效果，而不实际执行复制操作。这就是为什么我们也应该在类中实现这些新函数:这样我们就可以在任何有意义的地方优化代码。移动构造函数可以抓取源对象，而不是复制它，如下所示:\n\n```cpp\nclass Warehouse {\npublic:\n  // constructors omitted for brevity\n  Warehouse(Warehouse&& src)\n : size_{src.size_}, \n capacity_{src.capacity_},\n products_{src.products_}\n {\n src.size_ = 0;\n src.capacity_ = 0;\n src.products_ = nullptr;\n }\n};\n```\n\n我们没有创建一个新的`capacity_`大小的数组，然后复制`products_`数组的每个元素，而是抓住了指向该数组的指针。我们知道`src`对象是一个右值，它将很快被销毁，这意味着析构函数将被调用，析构函数将删除分配的数组。现在，我们从新创建的`Warehouse`对象指向分配的数组，这就是为什么我们不能让析构函数删除源数组。因此，我们给它分配`nullptr`，以确保析构函数将错过分配的对象。因此，由于移动构造函数，下面的代码将被优化:\n\n```cpp\nWarehouse large = small + mid;\n```\n\n`+`运算符的结果将被移动而不是复制。请看下图:\n\n![](img/d7d0904d-6549-4c3c-aadd-6bb1785dfa17.png)\n\n上图演示了如何将临时对象移动到大对象。\n\n# 关于运算符重载的注释\n\nC++ 提供了一种强大的机制来重载自定义类型的运算符。使用`+`运算符计算两个对象的和要比调用成员函数好得多。调用成员函数还包括在调用它之前记住它的名字。可能是`add`、`calculateSum`、`calculate_sum`或者别的什么。运算符重载允许在类设计中采用一致的方法。另一方面，重载操作符增加了代码中不必要的冗长。下面的代码片段代表了重载的比较运算符列表，以及`Money`类的加法和减法:\n\n```cpp\nconstexpr bool operator<(const Money& a, const Money& b) { \n  return a.value_ < b.value_; \n}\nconstexpr bool operator==(const Money& a, const Money& b) { \n  return a.value_ == b.value_; \n}\nconstexpr bool operator<=(const Money& a, const Money& b) { \n  return a.value_ <= b.value_; \n}\nconstexpr bool operator!=(const Money& a, const Money& b) { \n  return !(a == b); \n}\nconstexpr bool operator>(const Money& a, const Money& b) { \n  return !(a <= b); \n}\nconstexpr bool operator>=(const Money& a, const Money& b) { \n  return !(a < b); \n}\nconstexpr Money operator+(const Money& a, const Money& b) { \n  return Money{a.value_ + b.value_}; \n}\nconstexpr Money operator-(const Money& a, const Money& b) { \n  return Money{a.value_ - b.value_}; \n}\n```\n\n如您所见，前面的大多数函数直接访问`Money`实例的值成员。为了让它起作用，我们应该宣布他们为`Money`的朋友。以下是`Money`的样子:\n\n```cpp\nclass Money\n{\npublic:\n  Money() {}\n  explicit Money(double v) : value_{v} {}\n  // construction/destruction functions omitted for brevity\n\npublic:\n  friend constexpr bool operator<(const Money&, const Money&);\n friend constexpr bool operator==(const Money&, const Money&);\n friend constexpr bool operator<=(const Money&, const Money&);\n friend constexpr bool operator!=(const Money&, const Money&);\n friend constexpr bool operator>(const Money&, const Money&);\n friend constexpr bool operator>=(const Money&, const Money&);\n friend constexpr bool operator+(const Money&, const Money&);\n friend constexpr bool operator-(const Money&, const Money&);\n\nprivate:\n  double value_;\n}; \n```\n\n这个班级看起来很可怕。C++ 20 引入了宇宙飞船运算符，让我们可以跳过比较运算符的定义。`operator<=>()`，也称为三向比较运算符，请求编译器生成关系运算符。对于`Money`类，我们可以使用默认的`operator<=>()`，如下图所示:\n\n```cpp\nclass Money\n{\n  // code omitted for brevity\n friend auto operator<=>(const Money&, const Money&) = default;\n};\n```\n\n编译器将生成`==`、`!=`、`<`、`>`、`<=`、`>=`运算符。`spaceship`运算符减少了运算符的冗余定义，并提供了一种为所有生成的运算符实现通用行为的方法。当实现`spaceship`运算符的自定义行为时，我们应该注意运算符的返回值类型。它可以是以下之一:\n\n*   `std::strong_ordering`\n*   `std::weak_ordering`\n*   `std::partial_ordering`\n*   `std::strong_equality`\n*   `std::weak_equality`\n\n所有这些都在`<compare>`标题中定义。编译器根据三向运算符的返回类型生成运算符。\n\n# 封装和公共接口\n\n**封装**是面向对象编程中的一个关键概念。它允许我们对客户端代码隐藏对象的实现细节。以电脑键盘为例；它有字母、数字和符号的按键，如果我们按下它们，每个按键都会起作用。它的用法简单直观，隐藏了很多只有熟悉电子学的人才能处理的低级细节。想象一下一个没有按键的键盘——一个没有标记针脚的裸板。您必须猜测按下哪个键才能实现所需的组合键或文本输入。现在，想象一个没有引脚的键盘——你必须向相应的插座发送适当的信号，才能在特定符号的情况下按下*键。用户可能会因为没有标签而感到困惑，他们也可能会通过按压或向无效插座发送信号来错误地使用标签。我们所知的键盘通过封装实现细节来解决这个问题——就像程序员封装对象一样，这样他们就不会给用户加载冗余成员，并确保用户不会以错误的方式使用对象。*\n\n可见性修饰符通过允许我们定义任何成员的可访问性级别，在类中服务于这个目的。`private`修饰符禁止在客户端代码中使用`private`成员。这允许我们通过提供相应的成员功能来控制`private`成员的修改。一个`mutator`函数，很多人都熟悉它是一个 setter 函数，在针对特定类的特定规则测试了一个`private`成员的值之后，它会修改该值。这方面的一个例子可以在下面的代码中看到:\n\n```cpp\nclass Warehouse {\npublic:\n  // rather naive implementation\n  void set_size(int sz) {\n if (sz < 1) throw std::invalid_argument(\"Invalid size\");\n size_ = sz;\n }\n  // code omitted for brevity\nprivate:\n  int size_;\n};\n```\n\n通过`mutator`函数修改数据成员允许我们控制其值。实际的数据成员是私有的，这使得它无法从客户端代码访问，而类本身提供公共函数来更新或读取其私有成员的内容。这些函数以及构造函数通常被称为类的*公共接口*。程序员努力使类的公共界面对用户友好。\n\n看看下面这个类，它代表一个二次方程求解器:一个`ax<sup>2</sup> + bx + c = 0`形式的方程。解决方案之一是使用公式`D  = b2 - 4ac`找到一个判别式，然后基于判别式(D)的值计算`x`的值。下面的类提供了五个功能，分别用于设置`a`、`b`、`c`的值，找到判别式，求解并返回`x`的值:\n\n```cpp\nclass QuadraticSolver {\npublic:\n  QuadraticSolver() = default;\n  void set_a(double a);\n void set_b(double b);\n void set_c(double c);\n void find_discriminant();\n double solve(); // solve and return the x\nprivate:\n  double a_;\n  double b_;\n  double c_;\n  double discriminant_;\n};\n```\n\n公共接口包括前面提到的四个函数和默认构造函数。求解方程 *2x <sup>2</sup> + 5x - 8 = 0* ，我们应该这样使用`QuadraticSolver`:\n\n```cpp\nQuadraticSolver solver;\nsolver.set_a(2);\nsolver.set_b(5);\nsolver.set_c(-8);\nsolver.find_discriminant();\nstd::cout << \"x is: \" << solver.solve() << std::endl;\n```\n\n类的公共接口应该明智地设计；前面的例子显示了设计不良的迹象。用户必须知道协议，也就是调用函数的确切顺序。如果用户错过对`find_discriminant()`的调用，结果将是未定义或无效的。公共界面强制用户学习协议，按照正确的顺序调用函数，即设置`a`、`b`、`c`的值，然后调用`find_discriminant()`函数，最后调用`solve()`函数得到`x`的期望值。一个好的设计应该提供一个直观简单的公共界面。我们可以覆盖`QuadraticSolver`使其只有一个函数，该函数接受所有必要的输入值，计算判别式本身，并返回解:\n\n```cpp\nclass QuadtraticSolver {\npublic:\n  QuadraticSolver() = default;\n double solve(double a, double b, double c);\n};\n```\n\n前面的设计比上一个更直观。下面的代码演示了使用`QuadraticSolver`来求解方程， *2x2 + 5x - 8 = 0* :\n\n```cpp\nQuadraticSolver solver;\nstd::cout << solver.solve(2, 5, -8) << std::endl;\n```\n\n这里要考虑的最后一件事是二次方程可以用多种方法求解的想法。我们介绍的是通过找到判别式来实现的。我们应该考虑，将来，我们可以向类中添加更多的实现方法。更改函数的名称可能会增加公共接口的可读性，并确保类的未来更新。我们还应该注意到，前面例子中的`solve()`函数以`a`、`b`和`c`作为参数，我们不需要将它们存储在类中，因为解是直接在函数中计算的。\n\n很明显，仅仅为了能够访问`solve()`功能而声明`QuadraticSolver`的对象似乎是多余的一步。该类的最终设计如下所示:\n\n```cpp\nclass QuadraticSolver {\npublic:\n  QuadraticSolver() = delete;\n\n  static double solve_by_discriminant(double a, double b, double c);\n  // other solution methods' implementations can be prefixed by \"solve_by_\"\n};\n```\n\n我们将`solve()`函数重命名为`solve_by_discriminant()`，这也暴露了解决方案的底层方法。我们还使函数*成为静态的*，从而使它对用户可用，而无需声明类的实例。但是，我们也标记了默认构造函数*删除了*，这再次迫使用户不要声明对象:\n\n```cpp\nstd::cout << QuadraticSolver::solve_by_discriminant(2, 5, -8) << std::endl;\n```\n\n客户端代码现在使用类花费的精力更少了。\n\n# c++ 中的 struts\n\n结构几乎和 C++ 中的类一样。它们具有类的所有特性，您可以从结构继承类，反之亦然。`class`和`struct`之间唯一的区别是默认可见性。对于结构，默认可见性修饰符是公共的。这也与继承有关。例如，当您从另一个类继承一个类而不使用修饰符时，它会私下继承。`Base`以下阶级私底下继承:\n\n```cpp\nclass Base\n{\npublic:\n  void foo() {}\n};\n\nclass Derived : Base\n{\n  // can access foo() while clients of Derived can't\n};\n```\n\n遵循相同的逻辑，以下结构公开继承了`Base`:\n\n```cpp\nstruct Base\n{\n  // no need to specify the public section\n  void foo() {}\n};\n\nstruct Derived : Base\n{\n  // both Derived and clients of Derived can access foo()\n};\n```\n\n这同样适用于从结构继承的类。例如，`Derived`类如果没有直接指定，则从`Base`私有继承:\n\n```cpp\nstruct Base\n{\n  void foo() {}\n};\n\n// Derived inherits Base privately\nclass Derived: Base\n{\n  // clients of Derived can't access foo()\n};\n```\n\n在 C++ 中，结构和类是可以互换的，但是大多数程序员更喜欢将结构用于简单类型。C++ 标准对简单类型给出了更好的定义，并将它们称为**聚合**。如果类(结构)符合以下规则，则它是一个聚合:\n\n*   没有私有或受保护的非静态数据成员\n*   没有用户声明或继承的构造函数\n*   没有虚拟、私有或受保护的基类\n*   没有虚拟成员函数\n\n当你读完这一章后，这些规则中的大部分会更加清晰。以下结构是聚合的示例:\n\n```cpp\nstruct Person\n{\n  std::string name;\n  int age;\n  std::string profession;\n};\n```\n\n在深入研究继承和虚函数之前，让我们看看聚合在初始化时会带来什么好处。我们可以通过以下方式初始化`Person`对象:\n\n```cpp\nPerson john{\"John Smith\", 22, \"programmer\"};\n```\n\nC++ 20 提供了更奇特的方法来初始化聚合:\n\n```cpp\nPerson mary{.name = \"Mary Moss\", .age{22}, .profession{\"writer\"}};\n```\n\n请注意，我们是如何混合使用指示符初始化成员的。\n\n结构化绑定允许我们声明绑定到聚合成员的变量，如以下代码所示:\n\n```cpp\nconst auto [p_name, p_age, p_profession] = mary;\nstd::cout << \"Profession is: \" << p_profession << std::endl;\n```\n\n结构化绑定也适用于数组。\n\n# 阶级关系\n\n对象互通是面向对象系统的核心。关系是对象之间的逻辑链接。我们可以在对象类之间区分或建立适当关系的方式定义了系统设计的整体性能和质量。考虑`Product`和`Warehouse`类；他们处于一种叫做聚合的关系中，因为`Warehouse`包含`Products`，也就是`Warehouse`聚合`Products`:\n\n![](img/76b956c9-22ba-4ee7-af40-f4edc84ae8ca.png)\n\n就纯面向对象而言，有几种关系，如关联、聚合、组合、实例化、泛化等。\n\n# 聚集和组成\n\n我们在`Warehouse`类的例子中遇到了聚合。`Warehouse`类存储一系列产品。更一般地说，它可以被称为一个*关联*，但是为了强调确切的包含，我们使用了术语*聚合*或*组合*。在聚合的情况下，包含其他类的一个或多个实例的类可以在没有聚合的情况下被实例化。这意味着我们可以创建和使用一个`Warehouse`对象，而不必创建包含在`Warehouse`中的`Product`对象。\n\n聚合的另一个例子是`Car`和`Person`。一个`Car`可以包含一个`Person`对象(作为驾驶员或乘客)，因为它们是相互关联的，但是包含性不强。我们可以创建一个没有`Driver`的`Car`对象，如下所示:\n\n```cpp\nclass Person; // forward declaration\nclass Engine { /* code omitted for brevity */ };\nclass Car {\npublic:\n  Car();\n  // ...\nprivate:\n  Person* driver_; // aggregation\n  std::vector<Person*> passengers_; // aggregation\n  Engine engine_; // composition\n  // ...\n}; \n```\n\n强遏制由**成分**表示。对于`Car`示例，`Engine`类的一个对象需要构成一个完整的`Car`对象。在此物理表示中，`Engine`成员在创建`Car`时自动创建。\n\n下面是聚合和组合的 UML 表示:\n\n![](img/7e185cb8-02eb-4899-9520-8ca23b59015c.png)\n\n设计班级时，我们必须决定他们之间的关系。定义这两个类别之间的构成的最好方法是*has-一个*关系测试。A `Car`有-a `Engine`，因为汽车有发动机。任何时候你不能决定关系是否应该用构成来表达，就问 *has-a* 问题。聚集和组成有些相似；他们只是描述了联系的强度。对于聚合，正确的问题应该是*可以有一个*；例如，`Car`可以有一个驾驶员(属于`Person`类型)；也就是说，遏制力很弱。\n\n# 遗产\n\n**继承**是一个允许我们重用类的编程概念。编程语言提供了不同的继承实现，但是一般的规则总是成立的:类关系应该回答 *is-a* 的问题。比如一个`Car`就是-一个`Vehicle`，可以让我们从`Vehicle`继承`Car`:\n\n```cpp\nclass Vehicle {\npublic:\n  void move();\n};\n\nclass Car : public Vehicle {\npublic:\n  Car();\n  // ...\n};\n```\n\n`Car`现在具有源自`Vehicle`的`move()`成员功能。继承本身代表一种泛化/特化关系，其中父类(`Vehicle`)是泛化，子类(`Car`)是特化。\n\nThe parent class could be referred to as the base class or the superclass, while the child class could be referred to as the derived class or the subclass, respectively. \n\n只有在绝对必要的情况下，才应该考虑使用继承。正如我们前面提到的，类应该满足 *is-a* 关系，有时候，这有点棘手。考虑`Square`和`Rectangle`类。下面的代码以最简单的形式声明了`Rectangle`类:\n\n```cpp\nclass Rectangle {\npublic:\n  // argument checks omitted for brevity\n  void set_width(int w) { width_ = w; }\n  void set_height(int h) { height_ = h; }\n  int area() const { return width_ * height_; }\nprivate:\n  int width_;\n  int height_;\n};\n```\n\n`Square` *是-a* `Rectangle`，所以我们很容易从`Rectangle`继承它:\n\n```cpp\nclass Square : public Rectangle {\npublic:\n  void set_side(int side) {\n set_width(side);\n set_height(side);\n  }\n\n int area() { \n    area_ = Rectangle::area();\n    return area_; \n  }\nprivate:\n int area_;\n};\n```\n\n`Square`通过添加新的数据成员`area_`，并使用自己的实现覆盖`area()`成员函数，扩展了`Rectangle`。在实践中，`area_`和我们计算其价值的方式都是多余的；我们这样做是为了展示一个糟糕的类设计，并使`Square`在某种程度上扩展其父类。很快，我们会得出结论，在这种情况下，继承是一个糟糕的设计选择。`Square`是`Rectangle`，所以无论在哪里使用`Rectangle`，都应该作为`Rectangle`使用，如下图所示:\n\n```cpp\nvoid make_big_rectangle(Rectangle& ref) {\n  ref->set_width(870);\n  ref->set_height(940);\n}\n\nint main() {\n  Rectangle rect;\n  make_big_rectangle(rect);\n  Square sq;\n  // Square is a Rectangle\n  make_big_rectangle(sq);\n}\n```\n\n`make_big_rectangle()`函数引用了`Rectangle`，`Square`继承了它，所以给`make_big_rectangle()`函数发送一个`Square`对象是完全可以的；`Square`T9 是-a a `Rectangle`。这种用子类型成功替换类型的例子被称为**利科夫替换原则**。让我们找出为什么这种替代在实践中有效，然后决定我们是否通过从`Rectangle`继承`Square`而犯了设计错误(是的，我们犯了)。\n\n# 从编译器的角度看继承\n\n我们可以用下面的方式来描述我们之前声明的`Rectangle`类:\n\n![](img/d6008180-4ca7-4fb1-9983-255e24f7972d.png)\n\n当我们在`main()`函数中声明`rect`对象时，函数的局部对象所需的空间在堆栈中分配。调用`make_big_rectangle()`函数时，也遵循同样的逻辑。它没有局部参数；相反，它有一个`Rectangle&`类型的参数，其行为类似于指针:它占用存储内存地址所需的内存空间(在 32 位和 64 位系统中分别为 4 或 8 字节)。`rect`对象通过引用传递给`make_big_rectangle()`，这意味着`ref`参数引用了`main()`中的局部对象:\n\n![](img/e4df863f-4b4a-4a28-ae69-dffaab6c6b0a.png)\n\n这里有一个`Square`类的例子:\n\n![](img/6d479be2-8fce-47b9-937f-062bbb09e7af.png)\n\n如上图所示，`Square`对象包含`Rectangle`的**子对象**；它部分代表了一个`Rectangle`。在这个特殊的例子中，`Square`类没有用新的数据成员扩展矩形。\n\n`Square`对象被传递给`make_big_rectangle()`，尽管后者采用了`Rectangle&`类型的参数。我们知道访问底层对象时需要指针(引用)的类型。类型定义了应该从指针指向的起始地址读取多少字节。在这种情况下，`ref`存储在`main()`中声明的本地`rect`对象的起始地址的副本。当`make_big_rectangle()`通过`ref`访问成员函数时，它实际上调用了以`Rectangle`引用为第一参数的全局函数。该函数被翻译成如下内容(同样，为了简单起见，我们对其进行了略微修改):\n\n```cpp\nvoid make_big_rectangle(Rectangle * const ref) {\n  Rectangle_set_width(*ref, 870);\n  Rectangle_set_height(*ref, 940);\n}\n```\n\n取消引用`ref`意味着从`ref`指向的存储位置开始读取`sizeof(Rectangle)`字节。当我们将一个`Square`对象传递给`make_big_rectangle()`时，我们将`sq`(该`Square`对象)的起始地址分配给`ref`。这将正常工作，因为`Square`对象实际上包含一个`Rectangle`子对象。当`make_big_rectangle()`函数取消引用`ref`时，它只能访问对象的`sizeof(Rectangle)`字节，而*看不到实际`Square`对象的*附加字节。下图说明了子对象`ref`指向的部分:\n\n![](img/e80a6dd3-39ba-4f60-9d03-d5eb016642aa.png)\n\n从`Rectangle`继承`Square`几乎与声明两个结构相同，其中一个(子)包含另一个(父):\n\n```cpp\nstruct Rectangle {\n int width_;\n int height_;\n};\n\nvoid Rectangle_set_width(Rectangle& this, int w) {\n  this.width_ = w;\n}\n\nvoid Rectangle_set_height(Rectangle& this, int h) {\n  this.height_ = h;\n}\n\nint Rectangle_area(const Rectangle& this) {\n  return this.width_ * this.height_;\n}\n\nstruct Square {\n Rectangle _parent_subobject_;\n int area_; \n};\n\nvoid Square_set_side(Square& this, int side) {\n  // Rectangle_set_width(static_cast<Rectangle&>(this), side);\n Rectangle_set_width(this._parent_subobject_, side);\n  // Rectangle_set_height(static_cast<Rectangle&>(this), side);\n Rectangle_set_height(this._parent_subobject_, side);\n}\n\nint Square_area(Square& this) {\n  // this.area_ = Rectangle_area(static_cast<Rectangle&>(this));\n this.area_ = Rectangle_area(this._parent_subobject_); \n  return this.area_;\n}\n```\n\n前面的代码演示了编译器支持继承的方式。看看`Square_set_side`和`Square_area`函数的注释代码行。我们实际上并不坚持这种实现，但是它表达了编译器如何处理 OOP 代码的完整想法。\n\n# 合成与继承\n\nC++ 语言为我们提供了方便且面向对象的语法，这样我们就可以表达继承关系，但是编译器处理它的方式类似于组合而不是继承。实际上，只要适用，使用组合而不是继承会更好。`Square`级及其与`Rectangle`的关系被认为是一个糟糕的设计选择。原因之一是子类型替换原则，它允许我们以错误的方式使用`Square`:将其传递给一个函数，该函数将其修改为`Rectangle`而不是`Square`。这告诉我们*是-a* 关系不正确，因为`Square`毕竟不是`Rectangle`。它是一个`Rectangle`的改编而不是`Rectangle`本身，也就是说它实际上并不代表一个`Rectangle`；它使用它向类用户提供有限的功能。\n\n`Square`的用户不应该知道它可以作为`Rectangle`；否则，在某些时候，他们会向`Square`实例发送无效或不受支持的消息。无效消息的例子是对`set_width`或`set_height`函数的调用。`Square`实际上不应该支持两个不同的成员函数来分别修改它的边，但是它不能隐藏这一点，因为它宣布它继承了`Rectangle`:\n\n```cpp\nclass Square : public Rectangle {\n  // code omitted for brevity\n};\n```\n\n如果我们把修饰语从公共改为私人呢？C++ 支持公共和私有继承类型。它也支持受保护的继承。当从类私有继承时，子类打算使用父类，并且可以访问它的公共接口。然而，客户端代码并不知道它处理的是一个派生类。此外，从父类继承的公共接口对于子类的用户来说是私有的。似乎`Square`将继承转化为合成:\n\n```cpp\nclass Square : private Rectangle {\npublic:\n  void set_side(int side) {\n    // Rectangle's public interface is accessible to the Square\n    set_width(side);\n set_height(side);\n  }\n  int area() {\n    area_ = Rectangle::area();\n    return area_;\n  }\nprivate:\n  int area_;\n};\n```\n\n客户端代码无法访问从`Rectangle`继承的成员:\n\n```cpp\nSquare sq;\nsq.set_width(14); // compile error, the Square has no such public member\nmake_big_rectangle(sq); // compile error, can't cast Square to Rectangle\n```\n\n通过在`Square`的私有部分声明一个`Rectangle`成员也可以达到同样的效果:\n\n```cpp\nclass Square {\npublic: \n  void set_side(int side) {\n rectangle_.set_width(side);\n rectangle_.set_height(side);\n  }\n  int area() {\n area_ = rectangle_.area();\n    return area_;\n  }\nprivate:\n Rectangle rectangle_;\n  int area_;\n};\n```\n\n你要仔细分析使用场景，完整回答*是-a* 问题，才能毫无疑问地使用继承。每次遇到构图和继承之间的选择，选择构图。\n\n我们可以在私下继承时省略修饰语。类的默认访问修饰符是私有的，因此`class Square : private Rectangle {};`与`class Square : Rectangle {};`相同。相反，结构的默认修饰符是公共的。\n\n# 受保护的继承\n\n最后，我们有**保护的**访问修饰符。它指定类成员的访问级别(如果它们在类体中使用的话)。受保护成员对类用户是私有的，但对派生类是公共的。如果修饰符用于指定继承类型，它的行为类似于派生类用户的私有继承。私有继承向所有派生类用户隐藏基类的公共接口，而受保护的继承使派生类的后代可以访问它。\n\n很难想象你需要受保护的继承的场景，但是你应该把它看作一个在意想不到的明显设计中可能有用的工具。假设我们需要设计一个堆栈数据结构适配器。堆栈通常基于向量(一维数组)、链表或出列来实现。\n\nThe stack conforms to the LIFO rule, which states that the last element inserted into the stack will be accessed first. Similarly, the first element inserted into the stack will be accessed last. We will discuss data structures and data structure adapters in more detail in [Chapter 6](06.html), *Digging into Data Structures and Algorithms in STL*\n\n堆栈本身并不代表数据结构；它*位于*数据结构之上，并通过限制、修改或扩展其功能来适应其使用。以下是表示一维整数数组的`Vector`类的简单声明:\n\n```cpp\nclass Vector {\npublic:\n  Vector();\n  Vector(const Vector&);\n  Vector(Vector&&);\n  Vector& operator=(const Vector&);\n  Vector& operator=(Vector&&);\n  ~Vector();\n\npublic:\n  void push_back(int value);\n  void insert(int index, int value);\n  void remove(int index);\n  int operator[](int index);\n  int size() const;\n  int capacity() const;\n\nprivate:\n  int size_;\n  int capacity_;\n  int* array_;\n};\n```\n\n前面的`Vector`不是支持随机访问迭代器的 STL 兼容容器；它包含动态增加阵列的最低值。它可以通过以下方式声明和使用:\n\n```cpp\nVector v;\nv.push_back(4);\nv.push_back(5);\nv[1] = 2;\n```\n\n虽然`Vector`类提供`operator[]`，允许我们随机访问它的任何项目，`Stack`禁止随机访问。`Stack`提供了`push`和`pop`操作，这样我们就可以分别将一个值插入其底层数据结构和获取该值:\n\n```cpp\nclass Stack : private Vector {\npublic:\n  // constructors, assignment operators and the destructor are omitted for brevity\n void push(int value) {\n push_back(value);\n }\n int pop() {\n int value{this[size() - 1]};\n remove(size() - 1);\n return value;\n }\n};\n```\n\n`Stack`可以通过以下方式使用:\n\n```cpp\nStack s;\ns.push(5);\ns.push(6);\ns.push(3);\nstd::cout << s.pop(); // outputs 3\nstd::cout << s.pop(); // outputs 6\ns[2] = 42; // compile error, the Stack has no publicly available operator[] defined\n```\n\n栈*适配*`Vector`，提供两个成员函数，我们可以访问。私有继承允许我们使用`Vector`的全部功能，并对`Stack`用户隐藏继承信息。如果我们想继承`Stack`来创建它的高级版本呢？假设`AdvancedStack`类提供了`min()`函数，该函数以恒定时间返回堆栈中包含的最小值。\n\n私有继承禁止`AdvancedStack`使用`Vector`的公共接口，所以我们需要一种方法允许`Stack`子类使用它的基类，但是对类用户隐藏基类的存在。受保护的继承服务于这个目标，如下面的 coe 所示:\n\n```cpp\nclass Stack : protected Vector {\n  // code omitted for brevity\n};\n\nclass AdvancedStack : public Stack {\n  // can use the Vector\n};\n```\n\n通过从`Vector`继承`Stack`，我们允许`Stack`的子类使用`Vector`公共接口。但是`Stack`和`AdvancedStack`的用户都不能以`Vector`的身份访问它们。\n\n# 多态性\n\n**多态性**是面向对象编程中的另一个关键概念。它允许子类对从基类派生的函数有自己的实现。假设我们有`Musician`类，它有`play()`成员函数:\n\n```cpp\nclass Musician {\npublic:\n  void play() { std::cout << \"Play an instrument\"; }\n};\n```\n\n现在，我们来声明`Guitarist`类，它具有`play_guitar()`功能:\n\n```cpp\nclass Guitarist {\npublic:\n  void play_guitar() { std::cout << \"Play a guitar\"; }\n};\n```\n\n这是使用继承的明显情况，因为`Guitarist`只是尖叫着它*是-a* `Musician`。`Guitarist`自然不会通过增加新功能(如`play_guitar()`)来扩展`Musician`；相反，它应该提供从`Musician`派生的`play()`功能的自己的实现。为此，我们使用**虚拟功能**:\n\n```cpp\nclass Musician {\npublic:\n  virtual void play() { std::cout << \"Play an instrument\"; }\n};\n\nclass Guitarist : public Musician {\npublic:\n  void play() override { std::cout << \"Play a guitar\"; }\n};\n```\n\n现在，很明显很简单，`Guitarist`类为`play()`函数提供了自己的实现，客户端代码只需使用指向基类的指针就可以访问它:\n\n```cpp\nMusician armstrong;\nGuitarist steve;\nMusician* m = &armstrong;\nm->play();\nm = &steve;\nm->play();\n```\n\n前面的例子展示了多态性的作用。虽然虚函数的使用是自然而然的，但除非我们正确使用，否则它实际上没有多大意义。首先，`Musician`的`play()`功能根本不应该有任何实现。原因很简单:音乐家应该能够在一种具体的乐器上演奏，因为他们不能同时在一种以上的乐器上演奏。为了摆脱实现，我们通过分配`0`给它，将该函数设置为**纯虚函数**:\n\n```cpp\nclass Musician {\npublic:\n virtual void play() = 0;\n};\n```\n\n当客户端代码试图声明`Musician`的实例时，这会导致编译错误。当然，这肯定会导致编译错误，因为你不应该创建一个具有*未定义*函数的对象。`Musician`只有一个目的:它只能被其他类继承。存在要继承的类称为**抽象类**。实际上，`Musician`被称为一个**接口**而不是一个抽象类。抽象类是一个半接口半类，它可以有两种类型的函数:有和没有实现。\n\n回到我们的例子，让我们添加`Pianist`类，它也实现了`Musician`接口:\n\n```cpp\nclass Pianist : public Musician {\npublic: \n void play() override { std::cout << \"Play a piano\"; }\n};\n```\n\n为了表达多态性的全部力量，让我们假设我们在某个地方声明了一个函数，该函数返回一组音乐家，要么是吉他手，要么是钢琴家:\n\n```cpp\nstd::vector<Musician*> get_musicians();\n```\n\n从客户端代码的角度来看，将很难剖析`get_musicians()`函数的返回值，并找出对象的实际子类型是什么。它可能是`Guitarist`或`Pianist`，甚至是纯粹的`Musician`。重点是，客户不应该真正关心对象的实际类型，因为它知道集合包含音乐家，并且一个`Musician`对象具有`play()`功能。因此，要让它们发挥作用，客户端只需遍历集合，让每个音乐家演奏自己的乐器(每个对象都调用它的实现):\n\n```cpp\nauto all_musicians = get_musicians();\nfor (const auto& m: all_musicians) {\n m->play();\n}\n```\n\n前面的代码表达了多态性的全部力量。现在，让我们理解这种语言是如何在低层次上支持多态性的。\n\n# 引擎盖下的虚拟功能\n\n虽然多态性不限于虚函数，但我们将更详细地讨论它们，因为动态多态性是 C++ 中最流行的多态性形式。同样，更好地理解一个概念或技术的最好方法是自己实现它。无论我们在一个类中声明一个虚拟成员函数，还是它有一个带有虚拟函数的基类，编译器都会用一个额外的指针来扩充这个类。指针指向一个通常被称为虚拟函数表的表，或者简称为*虚拟表*。我们也称指针为*虚拟表指针*。\n\n假设我们正在为银行客户账户管理实现一个类子系统。假设银行要求我们根据账户类型进行提现。例如，储蓄账户允许一年兑现一次钱，而支票账户允许客户随时兑现钱。在不深入探讨`Account`类的任何不必要的细节的情况下，让我们声明将帮助我们理解虚拟成员函数的最低限度。我们来看看`Account`类的定义:\n\n```cpp\nclass Account\n{\npublic:\n virtual void cash_out() {\n // the default implementation for cashing out \n }  virtual ~Account() {}\nprivate:\n  double balance_;\n};\n```\n\n编译器将`Account`类转换成一个指向虚函数表的结构。下面的代码表示伪代码，解释当我们在类中声明虚函数时会发生什么。像往常一样，请注意，我们提供的是一般的解释，而不是编译器特定的实现(mangling 这个名称也是通用形式；例如，我们将`cash_out`改名为`Account_cash_out`):\n\n```cpp\nstruct Account\n{\n VTable* __vptr;\n  double balance_;\n};\n\nvoid Account_constructor(Account* this) {\n this->__vptr = &Account_VTable;\n}\n\nvoid Account_cash_out(Account* this) {\n  // the default implementation for cashing out\n}\n\nvoid Account_destructor(Account* this) {}\n```\n\n好好看看前面的伪代码。`Account`结构的第一个成员是`__vptr`。由于之前声明的`Account`类有两个虚函数，我们可以将虚拟表想象成一个数组，其中有两个指向虚拟成员函数的指针。请参见以下图示:\n\n```cpp\nVTable Account_VTable[] = {\n &Account_cash_out,\n &Account_destructor\n};\n```\n\n有了前面的假设，让我们看看当我们调用一个对象上的虚函数时，编译器会生成什么代码:\n\n```cpp\n// consider the get_account() function as already implemented and returning an Account*\nAccount* ptr = get_account();\nptr->cash_out();\n```\n\n下面是我们可以想象的编译器生成的代码，与前面的代码类似:\n\n```cpp\nAccount* ptr = get_account();\nptr->__vptr[0]();\n```\n\n虚函数在层次结构中使用时显示了它们的威力。`SavingsAccount`像这样继承自`Account`类:\n\n```cpp\nclass SavingsAccount : public Account\n{\npublic:\n void cash_out() override {\n // an implementation specific to SavingsAccount\n }\n  virtual ~SavingsAccount() {}\n};\n```\n\n当我们通过指针(或引用)调用`cash_out()`时，虚拟函数基于指针指向的目标对象被调用。例如，假设`get_savings_account()`返回一个`SavingsAccount`作为`Account*`。以下代码将调用`cash_out()`的`SavingsAccount`实现:\n\n```cpp\nAccount* p = get_savings_account();\np->cash_out(); // calls SavingsAccount version of the cash_out\n```\n\n以下是编译器为`SavingsClass`生成的内容:\n\n```cpp\nstruct SavingsAccount\n{\n  Account _parent_subobject_;\n  VTable* __vptr;\n};\n\nVTable* SavingsAccount_VTable[] = {\n  &SavingsAccount_cash_out,\n  &SavingsAccount_destructor,\n};\n\nvoid SavingsAccount_constructor(SavingsAccount* this) {\n  this->__vptr = &SavingsAccount_VTable;\n}\n\nvoid SavingsAccount_cash_out(SavingsAccount* this) {\n  // an implementation specific to SavingsAccount\n}\n\nvoid SavingsAccount_destructor(SavingsAccount* this) {}\n```\n\n所以，我们有两个不同的虚函数表。当我们创建一个`Account`类型的对象时，它的`__vptr`指向`Account_VTable`，而`SavingsAccount`类型的对象的`__vptr`指向`SavingsAccount_VTable`。让我们看看下面的代码:\n\n```cpp\np->cash_out();\n```\n\n前面的代码翻译成这样:\n\n```cpp\np->__vptr[0]();\n```\n\n现在，很明显`__vptr[0]`解析为正确的函数，因为它是通过`p`指针读取的。\n\n如果`SavingsAccount`没有覆盖`cash_out()`功能怎么办？在这种情况下，编译器只需将基类实现的地址放在与`SavingsAccount_VTable`相同的槽中，如下所示:\n\n```cpp\nVTable* SavingsAccount_VTable[] = {\n  // the slot contains the base class version \n  // if the derived class doesn't have an implementation\n &Account_cash_out,\n  &SavingsAccount_destructor\n};\n```\n\n编译器以不同的方式实现虚函数的表示和管理。有些实现甚至使用不同的模型，而不是我们前面介绍的模型。为了简单起见，我们引入了一种流行的方法，并以通用的方式表示它。现在，我们将看看在包含动态多态性的代码的引擎盖下发生了什么。\n\n# 设计模式\n\n设计模式是程序员最具表现力的工具之一。它们让我们能够以一种优雅且久经考验的方式解决设计问题。当你努力为你的类和它们的关系提供最好的设计时，一个著名的设计模式可能会拯救你。\n\n设计模式最简单的例子是**单例**。它为我们提供了一种只声明和使用类的一个实例的方法。比如，假设电商平台只有一个`Warehouse`。要访问`Warehouse`类，项目可能要求我们在许多源文件中包含并使用它。为了保持同步，我们应该让`Warehouse`成为一个单独的个体:\n\n```cpp\nclass Warehouse {\npublic:\n  static create_instance() {\n if (instance_ == nullptr) {\n instance_ = new Warehouse();\n }\n return instance_;\n }\n\n static remove_instance() {\n delete instance_;\n instance_ = nullptr;\n }\n\nprivate:\n  Warehouse() = default;\n\nprivate:\n  static Warehouse* instance_ = nullptr;\n};\n```\n\n我们声明了一个静态`Warehouse`对象和两个静态函数来创建和销毁相应的实例。每次用户试图以旧方式声明`Warehouse`对象时，私有构造函数都会导致编译错误。为了能够使用`Warehouse`，客户端代码必须调用`create_instance()`函数:\n\n```cpp\nWarehouse* w = Warehouse::create_instance();\nProduct book;\nw->add_product(book);\nWarehouse::remove_instance();\n```\n\n`Warehouse`的单例实现并不完整，只是引入设计模式的一个例子。我们将在本书中介绍更多的设计模式。\n\n# 摘要\n\n在本章中，我们讨论了面向对象编程的基本概念。我们谈到了类的底层细节和 C++ 对象模型的编译器实现。知道如何设计和实现类而不实际拥有类有助于以正确的方式使用类。\n\n我们还讨论了继承的必要性，并尝试在可能适用的地方使用组合而不是继承。C++ 支持三种类型的继承:公共继承、私有继承和受保护继承。所有这些类型在特定的类设计中都有应用。最后，我们通过引入一个大大提高客户端代码便利性的例子来理解多态性的用途和威力。\n\n在下一章中，我们将学习更多关于模板和模板元编程的知识，我们将把它们作为基础来深入研究一个名为概念的新的 C++ 20 特性。\n\n# 问题\n\n1.  物体的三个属性是什么？\n2.  移动物体而不是复制物体有什么好处？\n3.  C++ 中的结构和类有什么区别？\n4.  聚合关系和组合关系有什么区别？\n5.  私有继承和受保护继承有什么区别？\n6.  如果我们在其中定义一个虚函数，类的大小会受到什么影响？\n7.  使用 Singleton 设计模式有什么意义？\n\n# 进一步阅读\n\n有关更多信息，请参考:\n\n*   Grady Booch，*面向对象分析与设计*([https://www . Amazon . com/Object-Oriented-Analysis-Design-Applications-3rd/DP/020189551 x/](https://www.amazon.com/Object-Oriented-Analysis-Design-Applications-3rd/dp/020189551X/))\n*   斯坦利·利普曼，*c++ 对象模型内部*([https://www . Amazon . com/Inside-Object-Model-Stanley-利普曼/dp/0201834545/](https://www.amazon.com/Inside-Object-Model-Stanley-Lippman/dp/0201834545/) )"
  },
  {
    "path": "docs/exp-cpp/04.md",
    "content": "# 四、理解和设计模板\n\n模板是 C++ 的一个独特特性，通过它函数和类能够支持通用数据类型——换句话说，我们可以实现独立于特定数据类型的函数或类；例如，客户端可以请求`max()`函数来处理不同的数据类型。我们可以只实现一个`max()`并将数据类型作为参数传递，而不是使用函数重载来实现和维护许多类似的函数。此外，模板可以与多重继承和运算符重载一起工作，在 C++ 中创建强大的通用数据结构和算法，如**标准模板库** ( **STL** )。此外，模板还可以应用于编译时计算、编译时和运行时代码优化等。\n\n在本章中，我们将学习函数和类模板的语法、它们的实例化以及它们的专门化。然后，我们将介绍*变量*模板及其应用。接下来，我们将讨论模板参数以及用于实例化它们的相应参数。之后，我们将学习如何实现一种类型*特征*，以及如何使用这种类型的信息来优化算法。最后，我们将展示在程序执行时可以用来加速程序的技术，包括编译时计算、编译时代码优化和静态多态性。\n\n本章将涵盖以下主题:\n\n*   探索函数和类模板\n*   理解可变模板\n*   了解模板参数和参数\n*   什么是特质？\n*   模板元编程及其应用\n\n# 技术要求\n\n本章的代码可以在本书的 GitHub 资源库中找到:[https://github.com/PacktPublishing/Expert-CPP](https://github.com/PacktPublishing/Expert-CPP)。\n\n# 探索函数和类模板\n\n我们将从介绍函数模板的语法及其实例化、演绎和专门化开始这一部分。然后，我们将继续讨论类模板，看看类似的概念和例子。\n\n# 动机\n\n到目前为止，当我们定义了一个函数或类时，我们必须提供输入、输出和中间参数。例如，假设我们有一个函数执行两个 int 类型整数的加法。我们如何扩展它，使其处理所有其他基本数据类型，如浮点、双精度、char 等？一种方法是通过手动复制、粘贴和稍微修改每个函数来使用函数重载。另一种方法是定义一个宏来执行加法操作。这两种方法都有各自的副作用。\n\n此外，如果我们修复了一个 bug 或者为一种类型添加了一个新特性，并且这个更新需要在以后为所有其他重载函数和类完成，会发生什么呢？我们有没有更好的方法来处理这种情况，而不是使用这种愚蠢的复制粘贴替换方法？\n\n事实上，这是任何计算机语言都可能面临的一个一般性问题。由通用函数式编程**元语言** ( **ML** )于 1973 年首创，ML 允许编写公共函数或类型，这些函数或类型仅在使用时操作的类型集合上有所不同，从而减少了重复。后来受到**特许人寿保险公司** ( **CLU** )提供的参数化模块和 Ada 提供的泛型的启发，C++ 采用了模板概念，允许函数和类使用泛型类型进行操作。换句话说，它允许函数或类处理不同的数据类型，而不需要重写它们。\n\n实际上，从抽象的角度来看，C++ 函数或类模板(如 cookie cutters)充当了创建其他类似函数或类的模式。这背后的基本思想是创建一个函数或类模板，而不必指定某些或所有变量的确切类型。相反，我们使用占位符类型定义函数或类模板，称为**模板类型参数**。一旦我们有了函数或类模板，我们就可以通过使用已经在其他编译器中实现的算法来自动生成函数或类。\n\nC++ 中有三种模板:*函数*模板、*类*模板、*变量*模板。我们接下来会看看这些。\n\n# 功能模板\n\n函数模板定义了如何生成函数族。这里的族是指一组行为相似的函数。如下图所示，这包括两个阶段:\n\n*   创建函数模板；也就是如何写的规则。\n*   模板实例化；也就是说，用于从模板生成函数的规则:\n\n![](img/4ba0b575-0a51-403e-8a1f-1f3b03c37817.png)\n\nFunction template format\n\n在上图的**第一部分**中，我们讨论了将用于创建泛型类型的函数模板的格式，但是关于**专用模板**，我们也称其为**主模板**。然后在**第二部分**中，我们介绍了从模板生成函数的三种方式。最后，专门化*和*重载小节告诉我们如何为特殊类型定制**主模板**(通过改变其行为)。\n\n# 句法\n\n有两种方法可以定义函数模板，如下面的代码所示:\n\n```cpp\ntemplate <typename identifier_1, …, typename identifier_n > \nfunction_declaration;\n\ntemplate <class identifier_1,…, class identifier_n> \nfunction_declaration;\n```\n\n这里，`identifier_i (i=1,…,n)`是类型或类参数，`function_declaration`声明函数体部分。前面两个声明的唯一区别是关键字——一个使用`class`而另一个使用`typename`，但是两者具有相同的含义和行为。由于一个类型(如基本类型——int、float、double、enum、struct、union 等)不是一个类，引入`typename`关键字方法是为了避免混淆。\n\n例如，经典的求最大值函数模板`app_max()`，可以声明如下:\n\n```cpp\ntemplate <class T>\nT app_max (T a, T b) {\n  return (a>b?a:b);   //note: we use ((a)>(b) ? (a):(b)) in macros  \n}                     //it is safe to replace (a) by a, and (b) by b now\n```\n\n这个函数模板可以适用于许多数据类型或类，只要存在 *a > b* 表达式有效的可复制构造类型。对于用户定义的类，这意味着必须定义大于运算符(>)。\n\n注意，函数模板和模板函数是不同的东西。函数模板是指编译器用来生成函数的一种模板，所以编译器不会为其生成任何目标代码。另一方面，模板函数意味着来自函数模板的实例。由于它是一个函数，相应的目标代码由编译器生成。然而，最新的 C++ 标准文档建议避免使用不精确的术语模板函数。因此，我们将在本书中使用函数模板和成员函数模板。\n\n# 实例化\n\n由于我们可能有无限多的类型和类，函数模板的概念不仅节省了源代码文件中的空间，而且使代码更容易阅读和维护。然而，与为我们的应用中使用的不同数据类型编写单独的函数或类相比，它不会产生更小的目标代码。例如，考虑一个使用浮点和整数版本`app_max()`的程序:\n\n```cpp\ncout << app_max<int>(3,5) << endl;\ncout << app_max<float>(3.0f,5.0f) << endl;\n```\n\n编译器将在目标文件中生成两个新函数，如下所示:\n\n```cpp\nint app_max<int> ( int a, int b) {\n  return (a>b?a:b);\n}\n\nfloat app_max<float> (float a, float b) {\n  return (a>b?a:b);\n}\n```\n\n从函数模板声明创建函数新定义的过程称为**模板实例化**。在这个实例化过程中，编译器确定模板参数，并根据需要为应用生成实际的功能代码。通常有三种形式:*显式* *实例化**隐式实例化*和*模板扣除*。在接下来的部分中，让我们讨论每种形式。\n\n# 显式实例化\n\n许多非常有用的 C++ 函数模板可以在不使用显式实例化的情况下编写和使用，但是我们将在这里描述它们，以便您知道如果您需要它们，它们确实存在。首先，让我们看看 C++ 11 之前显式实例化的语法。有两种形式，如下面的代码所示:\n\n```cpp\ntemplate return-type \nfunction_name < template_argument_list > ( function_parameter-list ) ;\n\ntemplate return-type \nfunction_name ( function_parameter_list ) ;\n```\n\n显式实例化定义，也称为**指令**，强制特定类型的函数模板的实例化，而不考虑将来**将调用的模板函数**。显式实例化的位置可以在函数模板定义之后的任何地方，并且对于源代码中的给定参数列表，只允许出现一次。\n\n自 C++ 11 以来，显式实例化指令的语法如下。这里我们可以看到`extern`关键字加在`template`关键字之前:\n\n```cpp\nextern template return-type \nfunction_name < template_argument_list > (function_parameter_list ); \n(since C++ 11)\n\nextern template return-type \nfunction_name ( function_parameter_list ); (since C++ 11)\n```\n\n使用`extern`关键字可以防止该函数模板的隐式实例化(更多细节请参见下一节)。\n\n关于之前声明的`app_max()`函数模板，可以使用以下代码显式实例化:\n\n```cpp\ntemplate double app_max<double>(double, double); \ntemplate int app_max<int>(int, int);\n```\n\n也可以使用以下代码显式实例化它:\n\n```cpp\nextern template double app_max<double>(double, double);//(since c++ 11)\nextren template int app_max<int>(int, int);            //(since c++ 11)\n```\n\n这也可以用一种模板论证演绎的方式来完成:\n\n```cpp\ntemplate double f(double, double);\ntemplate int f(int, int);\n```\n\n最后，也可以这样做:\n\n```cpp\nextern template double f(double, double); //(since c++ 11)\nextern template int f(int, int);          //(since c++ 11)\n```\n\n此外，还有一些其他的显式实例化规则。如果您想了解更多，请参考*进一步阅读*部分【10】了解更多详情。\n\n# 隐式实例化\n\n当一个函数被调用时，该函数的定义需要存在。如果这个函数没有被显式地实例化，那么就达到了隐式实例化的方法，其中模板参数的列表需要被显式地提供或者从上下文中推导出来。以下程序的`Part A`提供了本目录中`app_max()`隐式实例化的一些示例:\n\n```cpp\n//ch4_2_func_template_implicit_inst.cpp\n#include <iostream>\ntemplate <class T>\nT app_max (T a, T b) { return (a>b?a:b); }\nusing namespace std;\nint main(){\n //Part A: implicit instantiation in an explicit way \n cout << app_max<int>(5, 8) << endl;       //line A \n cout << app_max<float>(5.0, 8.0) << endl; //line B\n cout << app_max<int>(5.0, 8) << endl;     //Line C\n cout << app_max<double>(5.0, 8) << endl;  //Line D\n\n //Part B: implicit instantiation in an argument deduction way\n cout << app_max(5, 8) << endl;           //line E \n cout << app_max(5.0f, 8.0f) << endl;     //line F \n\n //Part C: implicit instantiation in a confuse way\n //cout<<app_max(5, 8.0)<<endl;          //line G  \n return 0;\n}\n```\n\n第`A`、`B`、`C`和`D`行的隐式实例分别是`int app_max<int>(int,int)`、`float app_max<float>(float, float>)`、`int app_max<int>(int,int)`和`double app_max<double>(double, double)`。\n\n# 扣除\n\n当您调用模板函数时，编译器需要首先计算出模板参数，即使不是每个模板参数都被指定。大多数时候，它会从函数参数中推导出缺失的模板参数。例如，在前一个函数的 B 部分，当您在第`E`行调用`app_max(5, 8)`时，编译器将模板参数推导为 int 类型，`(int app_max<int>(int,int))`，因为输入参数`5`和`8`是整数。同样，线`F`将推导为浮动类型，即`float app_max<float>(float,float)`。\n\n但是，如果在实例化过程中出现混淆，会发生什么呢？例如，在前一个程序的`G`的注释行中，根据编译器的不同，它可能会调用`app_max<double>(double, double), app_max<int>(int, int)`，或者只是给出一个编译错误消息。帮助编译器推导类型的最好方法是通过显式给出模板参数来调用函数模板。在这种情况下，如果我们叫`app_max<double>(5, 8.0)`，任何混乱都会被解决。\n\nFrom the compiler's point of view, there are several ways to do template argument deduction – deduction from a function call, deduction from a type, auto type deduction, and non-deduced contexts [4]. However, from a programmer's point of view, you should never write fancy code to ill-use the concept of function template deduction to confuse other programmers such as line G in the previous example.\n\n# 专业化和超载\n\n专门化允许我们为一组给定的模板参数定制模板代码。它允许我们为特定的模板参数定义一个特殊的行为。专业化仍然是一个模板；您仍然需要实例化来获取真实的代码(由编译器自动执行)。\n\n在下面的示例代码中，主函数模板`T app_max(T a, T b)`将基于运算符 *a > b、*的返回返回`a`或`b`，但我们可以将其专门化为`T = std::string`，这样我们只比较`a`和`b`的 0 *-th* 元素；也就是`a[0] >b[0]`:\n\n```cpp\n//ch4_3_func_template_specialization.cpp\n#include <iostream>\n#include <string>\n\n//Part A: define a  primary template\ntemplate <class T> T app_max (T a, T b) { return (a>b?a:b); }\n\n//Part B: explicit specialization for T=std::string, \ntemplate <> std::string app_max<std::string> (std::string a, std::string b){ \n    return (a[0]>b[0]?a:b);\n}\n\n//part C: test function\nusing namespace std; \nvoid main(){\n string a = \"abc\", b=\"efg\";\n cout << app_max(5, 6) << endl; //line A \n cout << app_max(a, b) << endl; //line B \n\n //question: what's the output if un-comment lines C and D?\n //char *x = \"abc\", *y=\"efg\";     //Line C\n //cout << app_max(x, y) << endl; //line D\n}\n```\n\n前面的代码先定义了一个主模板，然后明确的将`T`专门化为`std::string`；也就是不去比较`a`和`b`的价值观，只关心`a[0]`和`b[0]`(其中`app_max()`的行为是专门化的)。在测试函数中，`line A`调用`app_max<int>(int,int)`，`line B`调用专门化版本，因为在推演的时候没有歧义。如果取消对`C`和`D`行的注释，将调用主函数模板`char* app_max<char > (char*, char*)`*，因为`char*`和`std::string`是不同的数据类型。*\n\n *本质上，专门化与函数重载解析有些冲突:编译器需要一种算法，通过在模板和重载函数之间找到正确的匹配来解决这种冲突。选择正确函数的算法包括以下两个步骤:\n\n1.  在常规函数和非专用模板之间执行重载解析。\n2.  如果选择了非专用模板，请检查是否存在更适合它的专用模板。\n\n例如，在下面的代码块中，我们声明了主函数(`line 0`)和专用函数模板(`lines 1-4`)，以及重载函数(`f()`的`lines 5-6)`):\n\n```cpp\ntemplate<typename T1, typename T2> void f( T1, T2 );// line 0\ntemplate<typename T> void f( T );                   // line 1\ntemplate<typename T> void f( T, T );                // line 2\ntemplate<typename T> void f( int, T* );             // line 3\ntemplate<> void f<int>( int );                      // line 4\nvoid f( int, double );                              // line 5\nvoid f( int );                                      // line 6\n```\n\n`f()`将在下面的代码块中被多次调用。基于前面的两步规则，我们可以在注释中显示选择了哪个函数。我们将在下面解释这样做的原因:\n\n```cpp\nint i=0; \ndouble d=0; \nfloat x=0;\ncomplex<double> c;\nf(i);      //line A: choose f() defined in line 6\nf(i,d);    //line B: choose f() defined in line 5\nf<int>(i); //line C: choose f() defined in line 4\nf(c);      //line D: choose f() defined in line 1\nf(i,i);    //line E: choose f() defined in line 2\nf(i,x);    //line F: choose f() defined in line 0\nf(i, &d);  //line G: choose f() defined in line 3\n\n```\n\n对于`lines A`和`line B`，由于`lines 5`和`line 6`中定义的`f()`是常规函数，所以它们的优先级最高，所以`f(i)`和`f(i,d)`会分别选择它们。对于`line C`，因为有专门的模板存在，所以从`line 4`生成的`f()`比从`line 1`创建的更加匹配。对于`line D`，由于`c`是`complex<double>`类型，只有`line 1`中定义的主功能模板匹配。`Line E`会选择`line 2`创建的`f()`，因为两个输入变量是同一类型。最后，`lines F`和`line G`将分别在`0`和`3`行中拾取从模板创建的功能。\n\n了解了功能模板之后，我们现在将继续讨论类模板。\n\n# 类模板\n\n类模板定义了一个类家族，它经常被用来实现一个容器。例如，C++ 标准库包含许多类模板，如`std::vector`、`std::map`、`std::deque`等。在 *OpenCV* 中，`cv::Mat`是一个非常强大的类模板，它可以处理 1D、2D 和内置数据类型的 3D 矩阵或图像，如`int8_t`、`uint8_t`、`int16_t`、`uint16_t`、`int32_t`、`uint32_t`、`float`、`double`等。\n\n类似于函数模板，如下图所示，类模板的概念包含模板创建语法、其专门化及其隐式和显式实例化:\n\n![](img/2f784eca-cdaf-490e-9514-942bf80883ac.png)\n\n在上图的**第一部分**中，通过一定的语法格式，我们可以为泛型类型创建一个类模板，也称为主模板，可以为具有不同成员函数和/或变量的特殊类型进行定制。一旦我们有了类模板，在**第二部分**中，编译器将根据应用的需求显式或隐式地将其实例化为模板类。\n\n现在，让我们看看创建类模板的语法。\n\n# 句法\n\n创建类模板的语法如下:\n\n```cpp\n[export] template <template_parameter_list> class-declaration \n```\n\n这里，我们有以下内容:\n\n*   `template_parameter-list`(参见*中的链接，进一步阅读*上下文【10】)是模板参数的非空逗号分隔列表，每个模板参数都是非类型参数、类型参数、模板参数或其中任何一个的参数包。\n*   `class-declaration`是用来声明一个类的部分，这个类包含一个类名和它在花括号中的主体。通过这样做，声明的类名也变成了模板名。\n\n例如，我们可以定义一个类模板`V`，使其包含各种 1D 数据类型:\n\n```cpp\ntemplate <class T>\nclass V {\npublic:\n  V( int n = 0) : m_nEle(n), m_buf(0) { creatBuf();}\n  ~V(){  deleteBuf();  }\n  V& operator = (const V &rhs) { /* ... */}\n  V& operator = (const V &rhs) { /* ... */}\n  T getMax(){ /* ... */ }\nprotected:\n  void creatBuf() { /* ... */}\n  void deleteBuf(){ /* ... */}\n\npublic:\n  int m_nEle;\n  T * m_buf;\n};\n```\n\n一旦我们有了这个类模板，编译器就可以在实例化过程中生成类。由于我们在*函数模板*小节中提到的原因，我们将避免在本书中使用不精确的术语`template`类。相反，我们将使用类模板。\n\n# 实例化\n\n考虑到我们在上一节中定义的类模板`V`，我们将假设后面会出现以下声明:\n\n```cpp\nV<char> cV;\nV<int>  iV(10);\nV<float> fV(5);\n```\n\n然后，编译器将创建`V`类的三个实例，如下所示:\n\n```cpp\nclass V<char>{\npublic:\n  V(int n=0);\n // ...\npublic:\n  int  m_nEle;\n  char *m_buf;\n};\nclass V<int>{\npublic:\n  V(int n=0);\n // ...\npublic:\n  int  m_nEle;\n  int *m_buf;\n};\nclass V<float>{\npublic:\n  V(int n = 0);\n  // ...\npublic:\n  int   m_nEle;\n  float *m_buf;\n};\n```\n\n类似于函数模板实例化，类模板实例化有两种形式——显式实例化和隐式实例化。让我们看看他们。\n\n# 显式实例化\n\n显式实例化的语法如下:\n\n```cpp\ntemplate class template_name < argument_list >;\nextern template class template_name < argument_list >;//(since C++ 11)\n```\n\n显式实例化定义强制实例化它们引用的类、结构或联合。在 C++ 0x 标准中，模板专门化或其成员的隐式实例化被抑制。类似于函数模板的显式实例化，这种显式实例化的位置可以在其模板定义之后的任何地方，并且只允许在一个文件中的整个程序中定义一次。\n\n此外，由于 C++ 11，隐式实例化步骤将被显式实例化声明(外部模板)绕过。这可以用来减少编译时间。\n\n回到模板类`V`，我们可以如下显式实例化它:\n\n```cpp\ntemplate class V<int>;\ntemplate class V<double>;\n```\n\n或者，我们可以执行以下操作(从 C++ 11 开始):\n\n```cpp\nextern template class V<int>;\nextern template class V<double>;\n```\n\n如果我们显式实例化一个函数或类模板，但程序中没有相应的定义，编译器将向我们显示一条错误消息，如下所示:\n\n```cpp\n//ch4_4_class_template_explicit.cpp\n#include <iostream>\nusing namespace std;\ntemplate <typename T>       //line A\nstruct A {\n  A(T init) : val(init) {}\n  virtual T foo();\n  T val;\n};                         //line B\n                           //line C \ntemplate <class T> //T in this line is template parameter\nT A<T>::foo() {    //the 1st T refers to function return type,\n                   //the T in <> specifies that this function's template\n                   //parameter is also the class template parameter\n  return val;\n}                        //line D\n\nextern template struct A<int>;  //line E\n#if 0                           //line F\nint A<int>::foo() {  \n    return val+1;    \n}                    \n#endif                         //line G\n\nint main(void) {\n  A<double> x(5);\n  A<int> y(5);\n  cout<<\"fD=\"<<x.foo()<<\",fI=\"<<y.foo()<< endl;\n  return 0;        //output: fD=5,fI=6\n}\n```\n\n在前面的代码块中，我们在行 A 和行 B 之间定义了一个类模板，然后我们实现了它的成员函数`foo()`，从`lines C`到`line D`。接下来，我们在`line E`为`int`类型显式实例化它。由于`lines F`和`line G`之间的代码块被注释掉了(这意味着对于这个显式的`int`类型实例化没有`foo()`的相应定义)，所以我们有一个链接错误。要解决这个问题，我们需要在`line F`用`#if 1`代替`#if 0`。\n\n最后，显式实例化声明还有一些附加限制，如下所示:\n\n*   **Static** :静态类成员可以命名，但是在显式实例化声明中不能允许静态函数。\n*   **内联**:内联函数在显式实例化声明中没有效果，内联函数是隐式实例化的。\n*   **类及其成员**:对于显式实例化一个类及其所有成员来说是不等价的。\n\n# 隐式实例化\n\n当引用一个模板类时，如果它没有被显式实例化或显式专门化，编译器将只根据需要从它的模板生成代码。这叫做**隐式实例化**，其语法如下:\n\n```cpp\nclass_name<argument list> object_name; //for non-pointer object \nclass_name<argument list> *p_object_name; //for pointer object\n```\n\n对于非指针对象，将实例化一个模板类并创建其对象，但只生成该对象使用的成员函数。对于指针对象，除非在程序中使用了成员，否则它不会被实例化。\n\n考虑下面的例子，我们在`ch4_5_class_template_implicit_inst.h`文件中定义了一个类模板`X`:\n\n```cpp\n//file ch4_5_class_template_implicit_inst.h\n#ifndef __CH4_5_H__ \n#define __CH4_5_H__ \n#include <iostream>\ntemplate <class T>\nclass X {\npublic:\n    X() = default;\n    ~X() = default;\n    void f() { std::cout << \"X::f()\" << std::endl; };\n    void g() { std::cout << \"X::g()\" << std::endl; };\n};\n#endif\n```\n\n然后，它包含在以下四个`cpp`文件中，每个文件中有`ain()`:\n\n```cpp\n//file ch4_5_class_template_implicit_inst_A.cpp\n#include \"ch4_5_class_template_implicit_inst.h\"\nvoid main()\n{\n    //implicit instantiation generates class X<int>, then create object xi\n    X<int>   xi ;  \n    //implicit instantiation generates class X<float>, then create object xf\n    X<float> xf;\n    return 0;  \n}\n```\n\n在`ch4_5_class_template_implicit_inst_A.cpp`中，编译器会隐式实例化`X<int>`和`X<float>`类，然后创建`xi`和`xf`对象。但是由于没有使用`X::f()`和`X::g()`，所以没有实例化。\n\n现在，我们来看看`ch4_5_class_template_implicit_inst_B.cpp`:\n\n```cpp\n//file ch4_5_class_template_implicit_inst_B.cpp\n#include \"ch4_5_class_template_implicit_inst.h\"\nvoid main()\n{\n    //implicit instantiation generates class X<int>, then create object xi\n    X<int> xi;    \n    xi.f();      //and generates function X<int>::f(), but not X<int>::g()\n\n    //implicit instantiation generates class X<float>, then create object\n    //xf and generates function X<float>::g(), but not X<float>::f()\n    X<float> xf;  \n    xf.g() ;   \n}\n```\n\n这里，编译器将隐式实例化`X<int>`类，创建`xi`对象，然后生成`X<int>::f()`函数，但不生成`X<int>::g()`。同样，它将实例化`X<float>`类，创建`xf`对象，并生成`X<float>::g()`函数，但不生成`X<float>::f()`。\n\n然后，我们有`ch4_5_class_template_implicit_inst_C.cpp`:\n\n```cpp\n//file ch4_5_class_template_implicit_inst_C.cpp\n#include \"ch4_5_class_template_implicit_inst.h\"\nvoid main()\n{\n   //inst. of class X<int> is not required, since p_xi is pointer object\n   X<int> *p_xi ;   \n   //inst. of class X<float> is not required, since p_xf is pointer object\n   X<float> *p_xf ; \n}\n```\n\n由于`p_xi`和`p_xf`是指针对象，所以不需要通过编译器实例化它们对应的模板类。\n\n最后，我们有`ch4_5_class_template_implicit_inst_D.cpp`:\n\n```cpp\n//file ch4_5_class_template_implicit_inst_D.cpp\n#include \"ch4_5_class_template_implicit_inst.h\"\nvoid main()\n{\n//inst. of class X<int> is not required, since p_xi is pointer object\n X<int> *p_xi; \n\n //implicit inst. of X<int> and X<int>::f(), but not X<int>::g()\n p_xi = new X<int>();\n p_xi->f(); \n\n//inst. of class X<float> is not required, since p_xf is pointer object\n X<float> *p_xf; \n p_xf = new X<float>();//implicit inst. of X<float> occurs here\n p_xf->f();            //implicit inst. X<float>::f() occurs here\n p_xf->g();            //implicit inst. of X<float>::g() occurs here\n\n delete p_xi;\n delete p_xf;\n}\n```\n\n这将隐式实例化`X<int>`和`X<int>::f()`，但不会实例化`X<int>::g()`；同样，对于`X<float>`，`X<float>::f()`和`X<float>::g()`将被实例化。\n\n# 专门化\n\n与函数专门化类似，当特定类型作为模板参数传递时，类模板的显式专门化为主模板定义了不同的实现。但是，它仍然是一个类模板，您需要通过实例化来获取真正的代码。\n\n例如，假设我们有一个`struct X`模板，可以存储任何数据类型的一个元素，它只有一个名为`increase()`的成员函数。但是对于 char 类型的数据，我们想要一个不同的`increase()`实现，并且需要添加一个名为`toUpperCase()`的新成员函数。因此，我们决定为该类型声明一个类模板专门化。我们按如下方式进行:\n\n1.  声明主类模板:\n\n```cpp\ntemplate <typename T>\nstruct X {\n  X(T init) : m(init) {}\n  T increase() { return ++ m; }\n  T m;\n};\n```\n\n这个步骤声明了一个主类模板，其中它的构造函数初始化`m`成员变量，`increase()`给`m`加一并返回它的值。\n\n2.  接下来，我们需要对 char 类型数据执行专门化:\n\n```cpp\ntemplate <>  //Note: no parameters inside <>, it tells compiler \n             //\"hi i am a fully specialized template\"\nstruct X<char> { //Note: <char> after X, tells compiler\n                 // \"Hi, this is specialized only for type char\"\n  X(char init) : m(init) {}\n  char increase() { return (m<127) ? ++ m : (m=-128); }\n  char toUpperCase() {\n    if ((m >= 'a') && (m <= 'z')) m += 'A' - 'a';\n    return m;\n  }\n  char m;\n};\n```\n\n该步骤创建了一个专门的(相对于主类模板)类模板，该模板带有一个附加的成员函数`toUpperCase()`，仅用于 char 类型数据。\n\n3.  现在，我们运行一个测试:\n\n```cpp\nint main() {\n X<int> x1(5);         //line A\n std::cout << x1.increase() << std::endl;\n\n X<char> x2('b');     //line B\n std::cout << x2.toUpperCase() << std::endl;\n return 0;\n}\n```\n\n最后，我们有一个`main()`函数来测试它。在第 A 行中，`x1`是一个已经从主模板`X<T>` *隐式实例化的对象。*由于`x1.m`的初始值为`5`，因此`6`将从`x1.increase()`返回。在`line B`中，`x2`是从专门化模板`X<char>`实例化的对象，`x2.m`的值在执行时为`b`。调用`x2.toUpperCase()`后，`B`将是返回值。\n\nThe complete code for this example can be found at `ch4_6_class_template_specialization.cpp`.\n\n总之，类模板显式专门化中使用的语法如下:\n\n```cpp\ntemplate <> class[struct] class_name<template argument list> { ... }; \n```\n\n这里，空模板参数列表`template <>`用于将其明确声明为模板专门化，`<template argument list>`是要专门化的类型参数。例如，在`ex4_6_class_template_specialization.cpp`中，我们使用以下内容:\n\n```cpp\ntemplate <> struct X<char> { ... };\n```\n\n在这里，`X`之后的`<char>`标识了我们要为其声明模板类专门化的类型。\n\n此外，当我们对一个模板类进行专门化时，它的所有成员——甚至那些在主模板中相同的成员——都必须被定义，因为在模板专门化过程中主模板没有继承概念。\n\n接下来，我们将了解部分专业化。这是显式专门化的一般陈述。与只有模板参数列表的显式专门化的格式相比，部分专门化需要模板参数列表和参数列表。对于模板实例化，如果用户的模板参数列表与模板参数的子集匹配，编译器将选择部分专门化模板。然后，编译器将根据部分专门化模板生成一个新的类定义。\n\n在下面的例子中，对于主类模板`A`，我们可以在参数列表中将它部分专门化为常量`T`。注意两者的参数表相同，都是`<typename T>`:\n\n```cpp\n//primary class template A\ntemplate <typename T>  class A{ /* ... */ }; \n\n//partial specialization for const T\ntemplate <typename T>  class A<const T>{ /* ... */ };  \n\n```\n\n在下面的例子中，主类模板`B`有两个参数:`<typename T1`和`typename T2 >`。我们通过`T1=int`对其进行部分专门化，保持`T2`不变:\n\n```cpp\n//primary class template B\ntemplate <typename T1, typename T2> class B{ /* ... */ };          \n\n//partial specialization for T1 = int\ntemplate <typename T2> class B<int, T2>{ /* ... */};  \n```\n\n最后，在下面的示例中，我们可以看到部分专门化中的模板参数数量不必与原始主模板中出现的参数数量相匹配。但是，模板参数的数量(出现在尖括号中类名的后面)必须与主模板中参数的数量和类型相匹配:\n\n```cpp\n//primary class template C: template one parameter\ntemplate <typename T> struct C { T type; };  \n\n//specialization: two parameters in parameter list \n//but still one argument (<T[N]>) in argument list\ntemplate <typename T, int N> struct C<T[N]>          \n{T type; };                                 \n```\n\n同样，类模板部分专门化仍然是类模板。您必须分别为其成员函数和数字变量提供定义。\n\n结束这一部分，让我们总结一下到目前为止我们所学到的东西。在下表中，您可以看到函数和类模板、它们的实例化和专门化之间的比较:\n\n|  | **功能模板** | **类模板** | **评论** |\n| 申报 | `template <class T1, class T2>``void f(T1 a, T2 b) { ... }` | `template <class T1, class T2>``class X { ... };` | 该声明定义了一个名为模板参数的函数/类模板`<class T1, class T2>`。 |\n| 明确的实例化 | `template void f <int, int >( int, int);`或者外部模板`void f <int, int >( int, int);`(从 C++ 11 开始) | `template class X<int, float>;`或者`extern template class X<int,float>;`(从 C++ 11 开始) | 实例化之后，现在有了函数/类，但它们被称为模板函数/类。 |\n| 含蓄的实例化 | {...`f(3, 4.5);``f<char, float>(120, 3.14);`} | {...`X<int,float> obj;``X<char, char> *p;`} | 当函数调用或类对象/指针被声明时，如果它没有被显式实例化，则使用隐式实例化方法。 |\n| 专门化 | `template <>``void f<int,float>(int a, float b)``{ ... }` | `template <>``class X <int, float>{ ... };` | 主模板的完全定制版本(无参数列表)仍需要实例化。 |\n| 部分专业化 | `template <class T>``void f<T,T>(T a, T b)``{ ... }` | `template <class T>``class X <T, T>{ ... };` | 主模板的部分定制版本(具有参数列表)仍然需要实例化。 |\n\n这里需要强调五个概念:\n\n*   **声明**:我们需要遵循用于定义函数或类模板的语法。此时，函数或类模板本身不是类型、函数或任何其他实体。换句话说，源文件中只有模板定义，没有生成可以编译成目标文件的代码。\n*   **隐式实例化**:任何代码要出现，必须实例化一个模板。在这个过程中，必须确定模板参数，这样编译器才能生成实际的函数或类。换句话说，它们是按需编译的，这意味着在给出具有特定模板参数的实例化之前，不会编译模板函数或类的代码。\n*   **显式实例化**:告诉编译器用给定的类型实例化模板，不管是否使用它们。通常，它用于提供库。\n*   ****【全特殊化】**** :这个没有参数表(全定制)；它只有一个参数列表。模板专门化最有用的一点是，您可以为特定的类型参数创建特殊的模板。\n*   **部分特殊化**:这和完全特殊化类似，只是零件参数表(部分定制)和零件实参表。\n\n# 理解可变模板\n\n在前一节中，我们学习了如何用固定数量的类型参数编写函数或类模板。但是自从 C++ 11 以来，标准的泛型函数和类模板可以接受可变数量的类型参数。这叫做**变量模板**，是*进一步阅读*上下文【6】中 C++ 的扩展。我们将通过查看示例来了解变量模板的语法和用法。\n\n# 句法\n\n如果函数或类模板采用零个或多个参数，可以定义如下:\n\n```cpp\n//a class template with zero or more type parameters\ntemplate <typename... Args> class X { ... };     \n\n//a function template with zero or more type parameters\ntemplate <typename... Args> void foo( function param list) { ...}                                                                      \n```\n\n这里，`<typename ... Args>`声明了一个参数包。注意这里，`Args`不是关键词；您可以使用任何有效的变量名。前面的类/函数模板可以采用任意数量的`typename`作为需要实例化的参数，如下所示:\n\n```cpp\nX<> x0;                       //with 0 template type argument\nX<int, std::vector<int> > x1; //with 2 template type arguments\n\n//with 4 template type arguments\nX<int, std::vector<int>, std::map<std::string, std::vector<int>>> x2; \n\n//with 2 template type arguments \nfoo<float, double>( function argument list ); \n\n//with 3 template type arguments\nfoo<float, double, std::vector<int>>( function argument list );\n```\n\n如果变量模板至少需要一个类型参数，则使用以下定义:\n\n```cpp\ntemplate <typename A, typename... Rest> class Y { ... }; \n\ntemplate <typename A, typename... Rest> \nvoid goo( const int a, const float b) { ....};\n```\n\n同样，我们可以使用以下代码实例化它们:\n\n```cpp\nY<int > y1;                                         \nY<int, std::vector<int>, std::map<std::string, std::vector<int>>> y2;\ngoo<int, float>(  const int a, const float b );                        \ngoo<int,float, double, std::vector<int>>(  const int a, const float b );      \n```\n\n在前面的代码中，我们分别使用一个和三个模板参数，从变量类模板的实例化创建了`y1`和`y2`对象。对于变量函数`goo`模板，我们将其实例化为两个模板函数，分别具有两个和三个模板参数。\n\n# 例子\n\n下面可能是最简单的例子，显示了一个变量模板，用于查找任何输入参数列表的最小值。这个例子使用递归的概念，直到到达`my_min(double n)`退出:\n\n```cpp\n//ch4_7_variadic_my_min.cpp\n//Only tested on g++ (Ubuntu/Linaro 7.3.0-27 ubuntu1~18.04)\n//It may have compile errors for other platforms\n#include <iostream>\n#include <math.h> \ndouble my_min(double n){\n  return n;\n}\ntemplate<typename... Args>\ndouble my_min(double n, Args... args){\n  return fmin(n, my_min(args...));\n}\nint main() {\n  double x1 = my_min(2);\n  double x2 = my_min(2, 3);\n  double x3 = my_min(2, 3, 4, 5, 4.7,5.6, 9.9, 0.1);\n  std::cout << \"x1=\"<<x1<<\", x2=\"<<x2<<\", x3=\"<<x3<<std::endl;\n  return 0;\n}\n```\n\n`printf()`变量函数可能是 C 或 C++ 中最有用、最强大的函数之一；然而，它不是类型安全的。在下面的代码块中，我们采用了经典的类型安全`printf()`示例来演示变量模板的有用性。一如既往，首先，我们需要定义一个基函数，`void printf_vt(const char *s)`，结束递归:\n\n```cpp\n//ch4_8_variadic_printf.cpp part A: base function - recursive end\nvoid printf_vt(const char *s)\n{\n  while (*s){\n    if (*s == '%' && *(++ s) != '%')\n      throw std::runtime_error(\"invalid format string: missing arguments\");\n     std::cout << *s++ ;\n  }\n}\n```\n\n然后，在其变量模板函数`printf_vt()`中，每当`%`被命中时，该值被打印，其余的被传递到其递归，直到到达基函数:\n\n```cpp\n//ch4_8_variadic_printf.cpp part B: recursive function\ntemplate<typename T, typename... Rest>\nvoid printf_vt(const char *s, T value, Rest... rest)\n{\n  while (*s) {\n    if (*s == '%' && *(++ s) != '%') {\n      std::cout << value;\n      printf_vt(s, rest...); //called even when *s is 0, \n      return;                //but does nothing in that case\n    }\n    std::cout << *s++ ;\n  }\n}\n```\n\n最后，我们可以使用以下代码测试并与传统的`printf()`进行比较:\n\n```cpp\n//ch4_8_variadic_printf.cpp Part C: testing\nint main() {\n  int x = 10;\n  float y = 3.6;\n  std::string s = std::string(\"Variadic templates\");\n  const char* msg1 = \"%s can accept %i parameters (or %s), x=%d, y=%f\\n\";\n  printf(msg1, s, 100, \"more\",x,y);  //replace 's' by 's.c_str()' \n                                     //to prevent the output bug\n  const char* msg2 = \"% can accept % parameters (or %); x=%,y=%\\n\";\n  printf_vt(msg2, s, 100, \"more\",x,y);\n  return 0;\n}\n```\n\n前面代码的输出如下:\n\n```cpp\np.]ï¿½U can accept 100 parameters (or more), x=10, y=3.600000\nVariadic templates can accept 100 parameters (or more); x=10,y=3.6\n```\n\n在第一行的开头，我们可以看到一些来自`printf()`的 ASCII 字符，因为`%s`对应的变量类型应该是一个指向字符的指针，但是我们给了它一个类型`std::string`。要解决这个问题，我们需要通过`s.c_str()`。但是，使用可变模板版本功能，我们没有这个问题。而且，我们只需要提供`%`，这就更好了——至少，是为了这个实现。\n\n总之，本节简要介绍了变量模板及其应用。变量模板提供了以下好处(从 C++ 11 开始):\n\n*   它是模板族的轻量级扩展。\n*   它展示了在不使用难看的模板和预处理器宏的情况下实现大量模板库的能力。因此，实现代码能够被理解和调试，并且还节省了编译时间。\n*   它支持`printf()`变量函数的类型安全实现。\n\n接下来，我们将探讨模板参数和参数。\n\n# 探索模板参数和参数\n\n在前两节中，我们学习了函数和类模板及其实例化。我们知道，在定义一个模板时，需要给出它的参数列表。当我们实例化它时，必须提供相应的参数列表。在本节中，我们将进一步研究这两个列表的分类和细节。\n\n# 模板参数\n\n回想一下下面的语法，它用于定义类/函数模板。`template`关键字后有一个`<>`符号，其中必须给出一个或多个模板参数:\n\n```cpp\n//class template declaration\ntemplate <*parameter-list*> class-declaration\n\n//function template declaration\ntemplate <parameter-list> function-declaration\n```\n\n参数列表中的参数可以是以下三种类型之一:\n\n*   `Non-type template parameter`:指引用静态实体的编译时常量值，如整数和指针。这些通常被称为非类型参数。\n*   `Type template parameter`:指内置类型名或用户自定义类。\n*   `Template template parameter`:表示参数为其他模板。\n\n我们将在下面的小节中更详细地讨论这些问题。\n\n# 非类型模板参数\n\n非类型模板参数的语法如下:\n\n```cpp\n//for a non-type template parameter with an optional name\ntype name(optional)\n\n//for a non-type template parameter with an optional name \n//and a default value\ntype name(optional)=default  \n\n//For a non-type template parameter pack with an optional name\ntype ... name(optional) (since C++ 11) \n```\n\n这里，`type`是以下类型之一——整型、枚举、指向对象或函数的指针、`lvalue`对对象或函数的引用、指向成员对象或成员函数的指针、`std::nullptr_t`(从 C++ 11 开始)。此外，我们可以将数组和/或函数类型放在模板声明中，但是它们会被数据和/或函数指针自动替换。\n\n以下示例显示了使用非类型模板参数`int N`的类模板。在`main()`中，我们实例化并创建一个对象，`x`，因此`x.a`有五个元素，初始值为`1`。将第四个元素值设置为`10`后，我们打印输出:\n\n```cpp\n//ch4_9_none_type_template_param1.cpp\n#include <iostream>\ntemplate<int N>\nclass V {\npublic:\n  V(int init) { \n    for (int i = 0; i<N; ++ i) { a[i] = init; } \n  }\n  int a[N];\n};\n\nint main()\n{\n  V<5> x(1); //x.a is an array of 5 int, initialized as all 1's \n  x.a[4] = 10;\n  for( auto &e : x.a) {\n    std::cout << e << std::endl;\n  }\n}\n```\n\n以下是使用`const char*`作为非类型模板参数的函数模板示例:\n\n```cpp\n//ch4_10_none_type_template_param2.cpp\n#include <iostream>\ntemplate<const char* msg>\nvoid foo() {\n  std::cout << msg << std::endl;\n}\n\n// need to have external linkage\nextern const char str1[] = \"Test 1\"; \nconstexpr char str2[] = \"Test 2\";\nextern const char* str3 = \"Test 3\";\nint main()\n{\n  foo<str1>();                   //line 1\n  foo<str2>();                   //line 2 \n  //foo<str3>();                 //line 3\n\n  const char str4[] = \"Test 4\";\n  constexpr char str5[] = \"Test 5\";\n  //foo<str4>();                 //line 4\n  //foo<str5>();                 //line 5\n  return 0;\n}\n```\n\n在`main()`中，我们成功地用`str1`和`str2`实例化了`foo()`，因为它们都是编译时常量值，并且有外部链接。然后，如果我们取消第 3-5 行的注释，编译器将报告错误消息。出现这些编译器错误的原因如下:\n\n*   **第 3 行** : `str3`不是常量变量，因此`str3`指向的值不能更改。然而`str3`的价值是可以改变的。\n*   **第 4 行** : `str4`不是`const char*`类型的有效模板参数，因为它没有链接。\n*   **第 5 行** : `str5`不是`const char*`类型的有效模板参数，因为它没有链接。\n\n非类型参数的另一个最常见的用法是数组的大小。如果您想了解更多，请前往[https://stackoverflow.com/questions/33234979](https://stackoverflow.com/questions/33234979)。\n\n# 类型模板参数\n\n类型模板参数的语法如下:\n\n```cpp\n//A type Template Parameter (TP) with an optional name\ntypename |class name(optional)               \n\n//A type TP with an optional name and a default\ntypename[class] name(optional) = default         \n\n//A type TP pack with an optional name\ntypename[class] ... name(optional) (since C++ 11) \n```\n\n**Note:**Here, we use the `typename` and `class` keywords interchangeably. Inside the body of the template declaration, the name of a type parameter is a `typedef-name`. When the template is instantiated, it aliases the type supplied.\n\n现在，让我们看一些例子:\n\n*   没有默认值的类型模板参数:\n\n```cpp\nTemplate<class T>               //with name\nclass X { /* ... */ };     \n\nTemplate<class >               //without name\nclass Y { /* ... */ };\n```\n\n*   默认的类型模板参数:\n\n```cpp\nTemplate<class T = void>    //with name \nclass X { /* ... */ };     \n\nTemplate<class = void >     //without name\nclass Y { /* ... */ };\n```\n\n*   类型模板参数包:\n\n```cpp\ntemplate<typename... Ts>   //with name\nclass X { /* ... */ };\n\ntemplate<typename... >   //without name\nclass Y { /* ... */ };\n\n```\n\n这个模板参数包可以接受零个或更多的模板参数，并且它只在 C++ 11 上起作用。\n\n# 模板模板参数\n\n模板模板参数的语法如下:\n\n```cpp\n//A template template parameter with an optional name\ntemplate <parameter-list> class *name*(optional) \n\n//A template template parameter with an optional name and a default\ntemplate <parameter-list> class *name*(optional) = default          \n\n//A template template parameter pack with an optional name\ntemplate <parameter-list> class ... *name*(optional) (since C++ 11)                                                                                               \n```\n\n**Note**: In template template parameter declaration, only the `class` keyword can be used; `typename` is not allowed. In the body of the template declaration, the name of a parameter is a `template-name`, and we need arguments to instantiate it.\n\n现在，假设您有一个函数充当对象列表的流输出运算符:\n\n```cpp\ntemplate<typename T>\nstatic inline std::ostream &operator << ( std::ostream &out, \n    std::list<T> const& v)\n{ \n    /*...*/ \n}\n```\n\n从前面的代码中，您可以看到，对于向量、双端队列和多种映射类型等序列容器，它们是相同的。因此，使用模板模板参数的概念，可以有一个操作符`<<`来统治它们。这方面的一个例子可以在`exch4_tp_c.cpp`中找到:\n\n```cpp\n/ch4_11_template_template_param.cpp (courtesy: https://stackoverflow.com/questions/213761)\n#include <iostream>\n#include <vector>\n#include <deque>\n#include <list>\nusing namespace std;\ntemplate<class T, template<class, class...> class X, class... Args>\nstd::ostream& operator <<(std::ostream& os, const X<T, Args...>& objs) {\n  os << __PRETTY_FUNCTION__ << \":\" << endl;\n  for (auto const& obj : objs)\n    os << obj << ' ';\n  return os;\n}\n\nint main() {\n  vector<float> x{ 3.14f, 4.2f, 7.9f, 8.08f };\n  cout << x << endl;\n\n  list<char> y{ 'E', 'F', 'G', 'H', 'I' };\n  cout << y << endl;\n\n  deque<int> z{ 10, 11, 303, 404 };\n  cout << z << endl;\n  return 0;\n}\n```\n\n前面程序的输出如下:\n\n```cpp\nclass std::basic_ostream<char,struct std::char_traits<char> > &__cdecl operator\n<<<float,class std::vector,class std::allocator<float>>(class std::basic_ostream\n<char,struct std::char_traits<char> > &,const class std::vector<float,class std:\n:allocator<float> > &):\n3.14 4.2 7.9 8.08\nclass std::basic_ostream<char,struct std::char_traits<char> > &__cdecl operator\n<<<char,class std::list,class std::allocator<char>>(class std::basic_ostream<cha\nr,struct std::char_traits<char> > &,const class std::list<char,class std::alloca\ntor<char> > &):\nE F G H I\nclass std::basic_ostream<char,struct std::char_traits<char> > &__cdecl operator\n<<<int,class std::deque,class std::allocator<int>>(class std::basic_ostream<char\n,struct std::char_traits<char> > &,const class std::deque<int,class std::allocat\nor<int> > &):\n10 11 303 404 \n```\n\n不出所料，每次调用的输出的第一部分是`pretty`格式的模板函数名，而第二部分输出每个容器的元素值。\n\n# 模板参数\n\n要实例化一个模板，所有的模板参数必须用它们相应的模板参数替换。参数要么是显式提供的，要么是从初始值设定项(对于类模板)推导出的，要么是从上下文(对于函数模板)推导出的，要么是默认的。由于有三类模板参数，我们也将有三个相应的模板参数。这些是模板非类型参数、模板类型参数和模板模板参数*。*除了这些，我们还将讨论默认模板参数。\n\n# 模板非类型参数\n\n回想一下，非类型模板参数指的是编译时常量值，如整数、指针和对静态实体的引用。模板参数列表中提供的非类型模板参数必须与这些值之一匹配。通常，非类型模板参数用于类初始化或类容器的大小规范。\n\n虽然对非类型实参的每种类型(整数和算术类型、指向对象/函数/成员的指针、`lvalue`引用参数等)的详细规则的讨论超出了本书的范围，但总的一般规则是模板非类型实参应该转换为相应模板参数的常量表达式。\n\n现在，让我们看看下面的例子:\n\n```cpp\n//part 1: define template with non-type template parameters\ntemplate<const float* p> struct U {}; //float pointer non-type parameter\ntemplate<const Y& b> struct V {};     //L-value non-type parameter\ntemplate<void (*pf)(int)> struct W {};//function pointer parameter\n\n//part 2: define other related stuff\nvoid g(int,float);   //declare function g() \nvoid g(int);         //declare an overload function of g() \nstruct Y {           //declare structure Y \n    float m1;\n    static float m2;\n};         \nfloat a[10]; \nY y; //line a: create a object of Y\n\n//part 3: instantiation template with template non-type arguments\nU<a> u1;      //line b: ok: array to pointer conversion\nU<&y> u2;     //line c: error: address of Y\nU<&y.m1> u3;  //line d: error: address of non-static member\nU<&y.m2> u4;  //line e: ok: address of static member\nV<y> v;       //line f: ok: no conversion needed\nW<&g> w;      //line g: ok: overload resolution selects g(int)\n```\n\n在前面的代码中，在`part 1`中，我们定义了三个具有不同非类型模板参数的模板结构。然后，在`part 2`中，我们声明了两个重载函数和`struct Y`。最后，在`part 3`中，我们研究了通过不同的非类型参数实例化它们的正确方法。\n\n# 模板类型参数\n\n与模板非类型参数相比，模板类型参数(对于类型模板参数)的规则很简单，要求必须是`typeid`。这里，`typeid`是一个标准的 C++ 运算符，在运行时返回类型标识信息。它基本上返回一个可以和其他`type_info`对象比较的`type_info`对象。\n\n现在，让我们看看下面的例子:\n\n```cpp\n//ch4_12_template_type_argument.cpp\n#include <iostream>\n#include <typeinfo>\nusing namespace std;\n\n//part 1: define templates\ntemplate<class T> class C  {}; \ntemplate<class T> void f() { cout << \"T\" << endl; }; \ntemplate<int i>   void f() { cout << i << endl; };     \n\n//part 2: define structures\nstruct A{};            // incomplete type \ntypedef struct {} B; // type alias to an unnamed type\n\n//part 3: main() to test\nint main() {\n  cout << \"Tid1=\" << typeid(A).name() << \"; \"; \n  cout << \"Tid2=\" << typeid(A*).name() << \"; \";    \n  cout << \"Tid3=\" << typeid(B).name()  << \"; \";\n  cout << \"Tid4=\" << typeid(int()).name() << endl;\n\n  C<A> x1;    //line A: ok,'A' names a type\n  C<A*> x2;   //line B: ok, 'A*' names a type\n  C<B> x3;    //line C: ok, 'B' names a type\n  f<int()>(); //line D: ok, since int() is considered as a type, \n              //thus calls type template parameter f()\n  f<5>();     //line E: ok, this calls non-type template parameter f() \n  return 0;\n}\n```\n\n在本例中，在`part 1`中，我们定义了三个类和函数模板:类模板 C 及其类型模板参数，两个函数模板分别具有一个类型模板参数和一个非类型模板参数。在`part 2`中，我们有一个不完整的`struct A`和一个未命名的类型`struct B`。最后，在`part 3`我们测试了它们。Ubuntu 18.04 中四个`typeid()`的输出如下:\n\n```cpp\nTid1=A; Tid2=P1A; Tid3=1B; Tid4=FivE\n```\n\n从 x86 MSVC 版本 19.24，我们有以下内容:\n\n```cpp\nTid1=struct A; Tid2=struct A; Tid3=struct B; Tid4=int __cdecl(void)\n```\n\n此外，由于`A`、A*、`B`和`int()`具有类型标识，所以从行 A 到 D 的代码段与模板类型类或函数链接。非类型模板参数函数模板只实例化 E 行，即`f()`。\n\n# 模板模板参数\n\n对于模板模板参数，其对应的模板参数是类模板的名称或模板别名。在查找与模板模板参数匹配的模板时，只考虑主类模板。\n\n这里，主模板指的是被专门化的模板。即使它们的参数列表可能匹配，编译器也不会考虑模板模板参数的任何部分专门化。\n\n以下是模板模板参数的示例:\n\n```cpp\n//ch4_13_template_template_argument.cpp\n#include <iostream>\n#include <typeinfo>\nusing namespace std;\n\n//primary class template X with template type parameters\ntemplate<class T, class U> \nclass X {\npublic:\n    T a;\n    U b;\n};\n\n//partially specialization of class template X\ntemplate<class U> \nclass X<int, U> {\npublic:\n    int a;  //customized a\n    U b;\n};\n\n//class template Y with template template parameter\ntemplate<template<class T, class U> class V> \nclass Y {\npublic:\n    V<int, char> i;\n    V<char, char> j;\n};\n\nY<X> c;\nint main() {\n    cout << typeid(c.i.a).name() << endl; //int\n    cout << typeid(c.i.b).name() << endl; //char\n    cout << typeid(c.j.a).name() << endl; //char\n    cout << typeid(c.j.b).name() << endl; //char\n    return 0;\n}\n```\n\n在这个例子中，我们定义了一个主类模板`X`及其专门化，然后是一个类模板`Y`，带有一个模板模板参数。接下来，我们使用模板参数`X`隐式实例化`Y`，并创建一个对象`c`。最后`main()`输出四个`typeid()`的名称，结果分别为`int`、`char`、`char`和`char`。\n\n# 默认模板参数\n\n在 C++ 中，通过传递参数来调用函数，参数由函数使用。如果在调用函数时没有传递参数，则使用默认值。类似于函数参数默认值，模板参数可以有默认参数。定义模板时，我们可以设置它的默认参数，如下所示:\n\n```cpp\n/ch4_14_default_template_arguments.cpp       //line 0\n#include <iostream>                          //line 1  \n#include <typeinfo>                          //line 2\ntemplate<class T1, class T2 = int> class X;  //line 3\ntemplate<class T1 = float, class T2> class X;//line 4\ntemplate<class T1, class T2> class X {       //line 5\npublic:                                      //line 6   \n T1 a;                                       //line 7\n T2 b;                                       //line 8  \n};                                           //line 9\nusing namespace std;\nint main() { \n X<int> x1;          //<int,int>\n X<float>x2;         //<float,int>\n X<>x3;              //<float,int>\n X<double, char> x4; //<double, char>\n cout << typeid(x1.a).name() << \", \" << typeid(x1.b).name() << endl;\n cout << typeid(x2.a).name() << \", \" << typeid(x2.b).name() << endl;\n cout << typeid(x3.a).name() << \", \" << typeid(x3.b).name() << endl;\n cout << typeid(x4.a).name() << \", \" << typeid(x4.b).name() << endl;\n return 0\n}\n```\n\n当我们设置模板参数的默认参数时，需要遵循某些规则:\n\n*   声明顺序很重要–默认模板参数的声明必须位于主模板声明的顶部。例如，在前面的示例中，您不能将第 3 行和第 4 行的代码移到第 9 行之后。\n*   如果一个参数有默认参数，那么它后面的所有参数也必须有默认参数。例如，以下代码不正确:\n\n```cpp\ntemplate<class U = char, class V, class W = int> class X { };  //Error \ntemplate<class V, class U = char,  class W = int> class X { }; //OK\n```\n\n*   不能在同一范围内给同一个参数两次默认参数。例如，如果使用以下代码，您将收到一条错误消息:\n\n```cpp\ntemplate<class T = int> class Y;\n\n//compiling error, to fix it, replace \"<class T = int>\" by \"<class T>\"\ntemplate<class T = int> class Y { \n    public: T a;  \n};\n```\n\n这里我们讨论了两个列表:`template_parameter_list`和`template_argument_list`。这些分别用于函数或类模板的创建和实例化*、**。* \n\n我们还了解了另外两个重要的规则:\n\n*   当我们定义一个类或函数模板时，我们需要给出它的`template_parameter_list`:\n\n```cpp\ntemplate <template_parameter_list> \nclass X { ... }\n\ntemplate <template_parameter_list> \nvoid foo( function_argument_list ) { ... } //assume return type is void\n```\n\n*   当我们实例化它们时，我们必须提供相应的`argument_list`:\n\n```cpp\nclass X<template_argument_list> x\nvoid foo<template_argument_list>( function_argument_list )\n```\n\n这两个列表中的参数或参数类型可以分为三类，如下表所示。请注意，虽然顶行是类模板，但这些属性也适用于函数模板:\n\n|  | **定义模板时****模板** **<模板 _ 参数 _ 列表>类 X <.../>** | **实例化模板时****类 X <模板 _ 参数 _ 列表> x** |\n| 非类型 | 此参数列表中的实体可以是下列之一:\n\n*   积分或枚举\n*   指向对象或函数的指针\n*   `lvalue`对对象的引用或`lvalue`对功能的引用\n*   指向成员的指针\n*   C++ 11 标准`::nullptr_t` C++ 11 结束\n\n | \n\n*   Non-typed parameters in this list are expressions whose values can be determined at compile time.\n*   Such parameters must be constant expressions, addresses of functions or objects with external links, or addresses of static class members.\n*   Non-type parameters are usually used to initialize classes or specify the size of class members.\n\n |\n| 类型 | 此参数列表中的实体可以是下列之一:\n\n*   必须以 typename 或 class 开头。\n*   在模板声明的主体中，类型参数的名称是`typedef-name`。当模板被实例化时，它为所提供的类型取别名。\n\n | \n\n*   The type of argument must have a `typeid` *.*\n*   It cannot be a local type, an unlinked type, an unnamed type or a combination of any of these types.\n\n |\n| 模板 | 此参数列表中的实体可以是下列之一:\n\n*   `template <parameter-list>`类名\n*   `template <parameter-list>`类...名称(可选)(从 C++ 11 开始)\n\n | 此列表中的模板参数是类模板的名称。 |\n\n在接下来的部分中，我们将探索如何在 C++ 中实现特征，并使用它们优化算法。\n\n# 探索特征\n\n泛型编程意味着编写在特定要求下可以处理任何数据类型的代码。这是软件工程行业中交付可重用高质量代码的最有效方式。然而，在泛型编程中，有时泛型还不够好。每当类型之间的差异过于复杂时，高效的泛型就很难优化公共实现。例如，在实现一个排序函数模板时，如果我们知道参数类型是一个链表而不是一个数组，那么将会实现一个不同的策略来优化性能。\n\n虽然模板专门化是克服这个问题的一种方法，但是它没有广泛地提供类型相关的信息。类型特征是一种用于收集类型信息的技术。在它的帮助下，我们可以做出更智能的决策，在泛型编程中开发高质量的优化算法。\n\n在本节中，我们将介绍如何实现类型特征，然后向您展示如何使用类型信息来优化算法。\n\n# 类型特征实现\n\n为了理解类型特征，我们将看看`boost::is_void`和`boost::is_pointer`的经典实现。\n\n# boost::is_void\n\n首先，我们来看一个最简单的特质类，`is_void`特质，由 boost 创建。它定义了一个用于实现默认行为的通用模板；也就是说，接受一个空类型，但其他任何东西都是空的。因此，我们有`is_void::value = false`:\n\n```cpp\n//primary class template is_void\ntemplate< typename T >\nstruct is_void{\n    static const bool value = false;  //default value=false \n};\n```\n\n然后，我们将其完全专门化为空型:\n\n```cpp\n//\"<>\" means a full specialization of template class is_void\ntemplate<> \nstruct is_void< void >{             //fully specialization for void\n    static const bool value = true; //only true for void type\n};\n```\n\n因此，我们有一个完整的性状类型，可以用来检测是否有任何给定的类型，`T`，`is_void`通过检查以下表达式:\n\n```cpp\nis_void<T>::value\n```\n\n接下来，让我们学习如何在`boost::is_pointer`性状中使用部分特化。\n\n# boost::is_pointer\n\n类似于`boost::avoid`特征，一个主类模板定义如下:\n\n```cpp\n//primary class template is_pointer\ntemplate< typename T > \nstruct is_pointer{\n    static const bool value = false;\n};\n```\n\n然后，它部分地专用于所有指针类型:\n\n```cpp\n//\"typename T\" in \"<>\" means partial specialization\ntemplate< typename T >   \nstruct is_pointer< T* >{ //<T*> means partial specialization only for type T* \n  static const bool value = true;  //set value as true\n};\n```\n\n现在，我们有了一个完整的性状类型，可以通过检查以下表达式来检测是否有任何给定的类型，`T`、`is_pointer`:\n\n```cpp\nis_pointer<T>::value\n```\n\n由于增强类型特征特性已经被正式引入到 C++ 11 标准库中，我们可以在下面的例子中显示`std::is_void`和`std::is_pointer`的用法，而不包括前面的源代码:\n\n```cpp\n//ch4_15_traits_boost.cpp\n#include <iostream>\n#include <type_traits>  //since C++ 11\nusing namespace std;\nstruct X {};\nint main()\n{\n cout << boolalpha; //set the boolalpha format flag for str stream.\n cout << is_void<void>::value << endl;          //true\n cout << is_void<int>::value << endl;           //false\n cout << is_pointer<X *>::value << endl;        //true\n cout << is_pointer<X>::value << endl;          //false\n cout << is_pointer<X &>::value << endl;        //false\n cout << is_pointer<int *>::value << endl;      //true\n cout << is_pointer<int **>::value << endl;     //true\n cout << is_pointer<int[10]>::value << endl;    //false\n cout << is_pointer< nullptr_t>::value << endl; //false\n}\n```\n\n前面的代码在开头为字符串流设置了`boolalpha`格式标志。通过这样做，所有的布尔值都是通过它们的文本表示来提取的，该文本表示为真或假。然后，我们用几个`std::cout`打印`is_void<T>::value`和`is_pointer<T>::value`T4 的数值。每个值的输出显示在相应的注释行的末尾。\n\n# 利用特征优化算法\n\n我们将使用一个经典的优化副本示例来展示类型特征的用法，而不是用一般的抽象方式来讨论这个主题。考虑称为`copy`的标准库算法:\n\n```cpp\ntemplate<typename It1, typename It2> \nIt2 copy(It1 first, It1 last, It2 out);\n```\n\n显然，我们可以为任何迭代器类型编写通用版本的`copy()`，也就是这里的`It1`和`It2`。然而，正如 boost 库的作者所解释的，在某些情况下，复制操作可以由`memcpy()`执行。如果满足以下所有条件，我们可以使用`memcpy()`:\n\n*   两种类型的迭代器`It1`和`It2`都是指针。\n*   `It1`和`It2`必须指向相同的类型，除了 const 和 volatile 限定符\n*   `It1`指向的类型必须提供一个简单的赋值运算符。\n\n这里，简单赋值运算符意味着该类型要么是标量类型，要么是以下类型之一:\n\n*   该类型没有用户定义的赋值运算符。\n*   该类型中没有数据成员的引用类型。\n*   必须在所有基类和数据成员对象中定义简单赋值运算符。\n\n这里，标量类型包括算术类型、枚举类型、指针、指向成员的指针，或者这些类型之一的常量或易失性限定版本。\n\n现在，让我们看看最初的实现。包括复印机类模板和用户界面功能两部分，即`copy()`:\n\n```cpp\nnamespace detail{\n//1\\. Declare primary class template with a static function template\ntemplate <bool b>\nstruct copier {\n    template<typename I1, typename I2>\n    static I2 do_copy(I1 first, I1 last, I2 out);\n};\n//2\\. Implementation of the static function template\ntemplate <bool b>\ntemplate<typename I1, typename I2>\nI2 copier<b>::do_copy(I1 first, I1 last, I2 out) {\n    while(first != last) {\n        *out = *first; \n         ++ out;\n         ++ first;\n    }\n    return out;\n};\n//3\\. a full specialization of the primary function template\ntemplate <>\nstruct copier<true> {\n    template<typename I1, typename I2>\n    static I2* do_copy(I1* first, I1* last, I2* out){\n        memcpy(out, first, (last-first)*sizeof(I2));\n        return out+(last-first);\n    }\n};\n}  //end namespace detail\n```\n\n正如注释行中提到的，前面的复印机类模板有两个静态函数模板——一个是主要的，另一个是完全专门化的。主节点进行逐元素硬拷贝，而完全专门化节点通过`memcpy()`一次拷贝所有元素:\n\n```cpp\n//copy() user interface \ntemplate<typename I1, typename I2>\ninline I2 copy(I1 first, I1 last, I2 out) {\n    typedef typename boost::remove_cv\n    <typename std::iterator_traits<I1>::value_type>::type v1_t;\n\n    typedef typename boost::remove_cv\n    <typename std::iterator_traits<I2>::value_type>::type v2_t;\n\n    enum{ can_opt = boost::is_same<v1_t, v2_t>::value\n                    && boost::is_pointer<I1>::value\n                    && boost::is_pointer<I2>::value\n                    && boost::has_trivial_assign<v1_t>::value \n   };\n   //if can_opt= true, using memcpy() to copy whole block by one \n   //call(optimized); otherwise, using assignment operator to \n   //do item-by-item copy\n   return detail::copier<can_opt>::do_copy(first, last, out);\n}\n```\n\n为了优化复制操作，前面的用户界面功能定义了两个`remove_cv`模板对象，`v1_t`和`v2_t`，然后评估`can_opt`是否为真。之后，调用`do_copy()`模板函数。通过使用 boost 实用程序库中发布的测试代码(`algo_opt_ examples.cpp`)，我们可以看到使用优化的实现有了显著的改进；也就是说，复制 char 或 int 类型的数据可能会快 8 到 3 倍。\n\n最后，让我们用以下要点来结束这一部分:\n\n*   除了类型之外，特征还能提供额外的信息。它是通过模板专门化实现的。\n*   按照惯例，特征总是作为结构实现的。用于实现特征的结构被称为特征类。\n*   比雅尼·斯特劳斯特鲁普说，我们应该把一个特性看作一个小对象，它的主要目的是携带信息，供另一个对象或算法用来确定策略或实现细节。*进一步* *阅读*上下文【4】\n*   斯科特·迈耶斯还总结说，我们应该使用性状类来收集关于类型的信息*进一步阅读*上下文【5】。\n*   特征可以帮助我们以有效/优化的方式实现通用算法。\n\n接下来，我们将探索 C++ 中的模板元编程。\n\n# 探索模板元编程\n\n一种计算机程序能够将其他程序视为其数据的编程技术被称为**元编程**。这意味着一个程序可以被设计成读取、生成、分析或转换其他程序，甚至在运行时修改自己。元编程的一种是编译器，它把一个文本格式的程序作为输入语言(C、Fortran、Java 等)，用输出语言产生另一个二进制机器码格式的程序。\n\nC++ **模板元编程** ( **TMP** )意味着使用模板在 C++ 中生成元程序。它有两个组件——必须定义一个模板，并且必须实例化一个已定义的模板。TMP 是图灵完备的，这意味着它有能力计算任何可计算的东西，至少在原则上是这样。另外，因为变量在 TMP 中都是不可变的(变量是常数)，所以递归而不是迭代被用来处理集合的元素。\n\n为什么我们需要 TMP？因为它可以在执行时间内加速我们的程序！但是由于优化世界中没有免费的午餐，我们为 TMP 支付的价格是更长的编译时间和/或更大的二进制代码大小。另外，不是每个问题都可以用 TMP 解决；它只在我们计算编译时不变的东西时有效；例如，找出所有小于常数整数的主数，常数整数的阶乘，展开常数循环或迭代，等等。\n\n从实用的角度来看，模板元编程具有解决以下三类问题的能力:编译时计算、编译时优化，以及通过避免运行时的虚拟表查找，用静态多态替换动态多态。在下面的小节中，我们将提供每个类别的示例来演示元编程是如何工作的。\n\n# 编译时计算\n\n通常，如果任务的输入和输出在编译时是已知的，我们可以使用模板元编程在编译期间进行计算，从而节省任何运行时开销和内存占用。这在实时高 CPU 利用率项目中非常有用。\n\n我们来看看阶乘函数，它计算`*n*!`。这是所有小于或等于 *n、*的正整数与 0 的乘积！=1。由于递归的概念，我们可以使用一个简单的函数来实现它，如下所示:\n\n```cpp\n//ch4_17_factorial_recursion.cpp\n#include <iostream>\nuint32_t f1(const uint32_t n) {\n  return (n<=1) ? 1 : n * f1(n - 1);\n}\n\nconstexpr uint32_t f2(const uint32_t n) {\n  return ( n<=1 )? 1 : n * f2(n - 1);\n}\n\nint main() {\n  uint32_t a1 = f1(10);         //run-time computation \n  uint32_t a2 = f2(10);         //run-time computation \n  const uint32_t a3 = f2(10);   //compile-time computation \n  std::cout << \"a1=\" << a1 << \", a2=\" << a2 << std::endl;\n}\n```\n\n`f1()`在运行时进行计算，而`f2()`可以在运行时或编译时进行计算，这取决于它的用法。\n\n同样，通过使用具有非类型参数、其专门化和递归概念的模板，该问题的模板元编程版本如下:\n\n```cpp\n//ch4_18_factorial_metaprogramming.cpp\n#include <iostream>\n//define a primary template with non-type parameters\ntemplate <uint32_t n> \nstruct fact {\n  ***const static uint32_t*** value = n * fact<n - 1>::value;\n  //use next line if your compiler does not support declare and initialize\n  //a constant static int type member inside the class declaration \n  //enum { value = n * fact<n - 1>::value }; \n};\n\n//fully specialized template for n as 0\ntemplate <> \nstruct fact<0> { \n    const static uint32_t value = 1;\n    //enum { value = 1 };\n};\nusing namespace std;\nint main() {\n    cout << \"fact<0>=\" << fact<0>::value << endl;   //fact<0>=1\n    cout << \"fact<10>=\" << fact<10>::value << endl; //fact<10>=3628800\n\n    //Lab: uncomments the following two lines, build and run \n    //     this program, what are you expecting? \n    //uint32_t m=5;\n    //std::cout << fact<m>::value << std::endl;\n}\n```\n\n这里，我们创建了一个带有非类型参数的类模板，像其他常量表达式一样，`const static uint32_t`或枚举常量的值在编译时进行计算。这个编译时评估约束意味着只有常量变量才有意义。此外，由于我们只使用类，静态对象是有意义的。\n\n当编译器看到模板的新参数时，它会创建模板的新实例。例如，当编译器看到`fact<10>::value`并试图创建一个参数为 10 的`fact`的实例时，结果发现`fact<9>`也必须被创建。对于`fact<9>`，需要`fact<8>`等等。最后编译器使用`fact<0>::value`(为 1)，编译时的递归终止。这个过程可以在下面的代码块中看到:\n\n```cpp\nfact<10>::value = 10* fact<9>::value;\nfact<10>::value = 10* 9 * fact<8>::value;\nfact<10>::value = 10* 9 * 8 * fact<7>::value;\n.\n.\n.\nfact<10>::value = 10* 9 * 8 *7*6*5*4*3*2*fact<1>::value;\nfact<10>::value = 10* 9 * 8 *7*6*5*4*3*2*1*fact<0>::value;\n...\nfact<10>::value = 10* 9 * 8 *7*6*5*4*3*2*1*1;\n```\n\n请注意，为了能够以这种方式使用模板，我们必须在模板参数列表中提供一个常量参数。这就是为什么如果取消最后两行代码的注释，会收到编译器的抱怨:`fact:template parameter n: m: a variable with non-static storage duration cannot be used as a non-type argument`。\n\n最后，让我们通过简单比较 **constexpr 函数** ( **CF** )和 TMP 来结束这一小节:\n\n*   **计算时间** : CF 在编译时或者运行时执行，这取决于它的用法，但是 TMP 只在编译时执行。\n*   **参数列表** : CF 只能取值，但是 TMP 可以同时取值和类型参数。\n*   **控制结构** : CF 可以使用递归、条件、循环，但是 TMP 只使用递归。\n\n# 编译时代码优化\n\n虽然前面的例子可以在编译时计算一个常数整数的阶乘，但是我们可以使用一个运行时循环来展开两个- *n* 向量的点积(其中 *n* 在编译时是已知的)。更传统的长度- *n* 向量的好处是展开循环是可行的，这导致非常优化的代码。\n\n例如，传统的点积函数模板可以通过以下方式实现:\n\n```cpp\n//ch4_19_loop_unoolling_traditional.cpp\n#include <iostream>\nusing namespace std;\ntemplate<typename T>\nT dotp(int n, const T* a, const T* b)\n{\n  T ret = 0;\n  for (int i = 0; i < n; ++ i) {\n      ret += a[i] * b[i];\n  }\n  return ret;\n}\n\nint main()\n{\n  float a[5] = { 1, 2, 3, 4, 5 };\n  float b[5] = { 6, 7, 8, 9, 10 };\n  cout<<\"dot_product(5,a,b)=\" << dotp<float>(5, a, b) << '\\n'; //130\n  cout<<\"dot_product(5,a,a)=\" << dotp<float>(5, a, a) << '\\n'; //55\n}\n```\n\n**循环展开**意味着如果我们可以将`dotp()`函数中的 for 循环优化为`a[0]*b[0] + a[1]*b[1] + a[2]*b[2] + a[3]*b[3] + a[4]*b[4]`，那么将节省更多的运行时计算。这正是元编程在下面的代码块中所做的:\n\n```cpp\n//ch4_20_loop_unroolling_metaprogramming.cpp\n#include <iostream>\n\n//primary template declaration\ntemplate <int N, typename T>    \nclass dotp {\npublic:\n  static T result(T* a, T* b) {\n    return (*a) * (*b) + dotp<N - 1, T>::result(a + 1, b + 1);\n  }\n};\n\n//partial specialization for end condition\ntemplate <typename T>   \nclass dotp<1, T> {\npublic:\n  static T result(T* a, T* b) {\n    return (*a) * (*b);\n  }\n};\n\nint main()\n{\n  float a[5] = { 1, 2, 3, 4, 5 };\n  float b[5] = { 6, 7, 8, 9, 10 };\n  std::cout << \"dot_product(5,a,b) = \" \n            << dotp<5, float>::result( a, b) << '\\n'; //130\n  std::cout << \"dot_product(5,a,a) = \" \n            << dotp<5,float>::result( a, a) << '\\n'; //55\n}\n```\n\n类似于阶乘元编程示例，在`dotp<5, float>::result( a, b)`语句中，实例化过程递归地执行以下计算:\n\n```cpp\ndotp<5, float>::result( a, b)\n= *a * *b + dotp<4,float>::result(a+1,b+1)\n= *a * *b + *(a+1) * *(b+1) + dotp<3,float>::result(a+2,b+2)\n= *a * *b + *(a+1) * *(b+1) + *(a+2) * *(b+2) \n  + dotp<2,float>::result(a+3,b+3)\n= *a * *b + *(a+1) * *(b+1) + *(a+2) * *(b+2) + *(a+3) * *(b+3) \n  + dotp<1,float>::result(a+4,b+4)\n= *a * *b + *(a+1) * *(b+1) + *(a+2) * *(b+2) + *(a+3) * *(b+3) \n  + *(a+4) * *(b+4)\n```\n\n由于 *N* 为 5，所以递归调用`dotp<n`、`float>::results()`模板函数四次，直到到达`dotp<1`、`float>::results()`。由`dotp<5`、`float>::result( a, b)`评估的最终表达式显示在前一个块的最后两行。\n\n# 静态多态性\n\n多态性意味着多个函数具有相同的名称。动态多态性允许用户在运行时确定要执行的实际函数方法(参见[第 3 章](03.html)、*面向对象编程的细节*，更多细节*)、*而*静态*多态性意味着要调用的实际函数(或者一般来说，要运行的实际代码)在编译时是已知的。默认情况下，C++ 通过检查参数的类型和/或数量，在编译时将函数调用与正确的函数定义相匹配。这个过程也叫**静态绑定**或**重载** *。*但是，通过使用虚函数，编译器在运行时也进行动态绑定或重写。\n\n例如，在下面的代码中，虚函数`alg()`在基函数`class B`和派生函数`class D`中定义。当我们使用派生对象指针`p`作为基类的实例指针时，`p->alg()`函数调用将调用在派生类中定义的派生`alg()`:\n\n```cpp\n//ch4_21_polymorphism_traditional.cpp\n#include <iostream>\nclass B{\npublic:\n    B() = default;\n    virtual void alg() { \n        std::cout << \"alg() in B\"; \n    }\n};\n\nclass D : public B{\npublic:\n    D() = default; \n    virtual void alg(){\n        std::cout << \"alg() in D\"; \n    }\n};\n\nint main()\n{\n    //derived object pointer p as an instance pointer of the base class\n    B *p = new D();\n    p->alg();       //outputs \"alg() in D\"\n    delete p;\n    return 0;\n}\n```\n\n但是，在多态行为不变并且可以在编译时确定的情况下，可以使用**奇怪重复的模板模式** ( **CRTP** )来实现静态多态，它模仿静态多态并在编译时解析绑定。因此，程序在运行时将不再检查`virtual-lookup-table`。下面的代码以静态多态性的方式实现了前面的示例:\n\n```cpp\n//ch4_22_polymorphism_metaprogramming.cpp\n#include <iostream>\ntemplate <class D> struct B {\n    void ui() {\n        static_cast<D*>(this)->alg();\n    }\n};\n\nstruct D : B<D> {\n    void alg() {\n        cout << \"D::alg()\" << endl;\n     }\n};\n\nint main(){\n    B<D> b;\n    b.ui();\n    return 0;\n}\n```\n\n总之，模板元编程的一般思想是让编译器在编译时做一些计算。这样，可以在一定程度上解决运行时开销。我们之所以能在编译时计算一些东西，是因为在运行时之前有些东西是不变的。\n\n正如在进一步阅读上下文[14]中提到的，C++ TMP 是一种在编译时执行计算任务的非常强大的方法。第一种方法并不容易，我们必须非常小心编译错误，因为模板树是展开的。从实用的角度来看，boost **元编程库** ( **MPL** )是一个很好的入门参考。它以通用的方式为算法、序列和元功能提供了编译时 TMP 框架。此外，C++ 17 中新的`std::variant`和`std::visit`特性也可以用于静态多态，用于没有相关类型共享接口继承类型的场景。\n\n# 摘要\n\n在本章中，我们讨论了 C++ 中与泛型编程相关的主题。从回顾 C 宏和函数重载开始，我们介绍了 C++ 模板的开发动机。然后，我们展示了带有固定数量参数的类和函数模板的语法，以及它们的专门化和实例化。自 C++ 11 以来，变量模板被标准泛型函数和类模板所接受。基于此，我们进一步将模板参数和参数分为三类:非类型模板参数/参数、类型模板参数/参数和模板模板参数/参数。\n\n我们还学习了特性和模板元编程。作为模板专门化的副产品，特性类可以为我们提供更多关于类型的信息。在类型信息的帮助下，最终，实现泛型算法的优化成为可能。类和/或函数模板的另一个应用是通过递归在编译时计算一些常量任务，这被称为模板元编程。它能够执行编译时计算和/或优化，并避免在运行时进行虚拟表查找。\n\n现在，您应该对模板有了深刻的理解。您应该能够在应用中创建自己的函数和类模板，并练习使用特性来优化您的算法，以及使用模板元编程来进行编译时计算以进行额外的优化\n\n在下一章中，我们将学习与内存和管理相关的主题，例如内存访问的概念、分配和取消分配技术，以及垃圾收集基础知识。这是 C++ 最独特的特性，因此每个 C++ 开发人员都必须理解。\n\n# 问题\n\n1.  宏的副作用是什么？\n2.  什么是类/函数模板？什么是模板类/函数？\n3.  什么是模板参数表？什么是模板参数列表？一旦我们有了一个类模板，我们就可以显式或隐式地实例化它。在什么样的场景下显式实例化是必要的？\n4.  C++ 中多态是什么意思？函数重载和函数重写有什么区别？\n5.  什么是类型特征？我们如何实现类型特征？\n6.  在`ch4_5_class_template_implicit_inst_B.cpp`文件中，我们说隐式实例化生成`X<int>`类，然后创建`xi`对象并生成`X<int>::f()`函数，但不生成`X<int>::g()`。如何验证`X<int>::g()`没有生成？\n7.  使用模板元编程，解决 *f(x，n) = x^n* 的问题，其中 *n* 是常量， *x* 是变量。\n8.  扩展到 n=10,100,10^3,10^4,10^6 的价值观，...，直到达到系统内存限制。比较编译时间、目标文件大小和运行 CPU 时间。\n\n# 进一步阅读\n\n如本章所述，查看以下来源，了解本章内容的更多信息:\n\n*   莫里斯·米尔纳(1975 年)。*具有自反和多态类型的可计算函数的逻辑。*证明和改进程序会议记录。\n*   [https://www . research . ed . AC . uk/portal/en/publications/a-logic-for-computable-functions-with-reflective-and-多态-type(9a 69331 e-b562-4061-8882-2a 89 a3 c 473 bb)。html](https://www.research.ed.ac.uk/portal/en/publications/a-logic-for-computable-functions-with-reflexive-and-polymorphic-types(9a69331e-b562-4061-8882-2a89a3c473bb).html)\n\n*   *柯蒂斯，多萝西(2009-11-06)。CLU 主页。*计算机科学与人工智能实验室编程方法论组。麻省理工学院。\n*   [http://www . pmg . csail . com . edu/clu . html](http://www.pmg.csail.mit.edu/CLU.html)\n\n*   *国际标准化组织发布的 Ada 2012* 技术勘误表。阿达资源协会。2016-01-29.\n*   https://www.adaic.org/2016/01/technical-corrigendum-for-ada-2012-published-by-iso/\n\n*   B.斯特鲁普、 *C++。*\n*   https://dl . ACM . org/doi/10.5555/1074100.1074189\n\n*   *S .迈耶斯，有效的 C++ 55 改进程序和设计的具体方法(第三版)，第 7 章。*\n*   [https://www . oreilly . com/library/view/effect-c-55/0321334876/](https://www.oreilly.com/library/view/effective-c-55/0321334876/)\n\n*   D. Gregor and J. Järvi (February 2008). *Variadic Templates for C++ 0x.*Journal of Object Technology. pp. 31–51\n\n    [http://www.jot.fm/issues/issue_2008_02/article2.pdf](http://www.jot.fm/issues/issue_2008_02/article2.pdf)\n\n*   [https://www.boost.org/](https://www.boost.org/)为型性状，单位测验等。\n\n*   [https://www . IBM . com/support/knowledge center/ssw _ IBM _ I _ 72/rzarg/templates . htm](https://www.ibm.com/support/knowledgecenter/ssw_ibm_i_72/rzarg/templates.htm)获取通用模板讨论。\n\n*   [代码分析工具 https://stack overflow . com/questions/546669/c-code-analysis-tool](https://stackoverflow.com/questions/546669/c-code-analysis-tool)。\n\n*   [https://en.cppreference.com](https://en.cppreference.com)为模板显式实例化。\n\n*   [http://www.cplusplus.com](http://www.cplusplus.com)为图书馆参考和使用实例。\n\n*   [http://www.drdobbs.com/cpp/c-type-traits/184404270](http://www.drdobbs.com/cpp/c-type-traits/184404270)为类型性状。\n\n*   [https://accu.org/index.php/journals/424](https://accu.org/index.php/journals/424)为模板元编程。\n\n*   [https://en.wikipedia.org/wiki/Template_metaprogramming](https://en.wikipedia.org/wiki/Template_metaprogramming)为模板元编程。\n\n*   K.生成编程:方法、工具和应用，第 10 章。\n\n*   名词（noun 的缩写）乔舒蒂斯；D. Gregor 和 D. Vandevoorde， *C++ 模板:完整指南(第二版)*，Addison-Wesley Professional 2017。*"
  },
  {
    "path": "docs/exp-cpp/05.md",
    "content": "# 五、内存管理和智能指针\n\n内存管理在 C++ 中是有代价的。忧心忡忡的程序员经常抱怨 C++ 的手工内存管理需求。虽然像 C#和 Java 这样的语言使用自动内存管理，但它会使程序比 C++ 运行得更慢。手动内存管理通常容易出错且不安全。正如我们在前面几章中已经看到的，程序代表数据和指令。几乎每个程序都在某种程度上使用计算机内存。很难想象一个有用的程序不需要内存分配。\n\n内存分配和释放从最简单的函数调用开始。调用一个函数通常意味着向它传递参数。函数需要空间来存储这些参数。为了让生活更轻松，它是自动处理的。当我们在代码中声明对象时，也会发生同样的自动分配。他们的寿命取决于他们所宣布的范围。每当它们超出范围时，就会自动解除分配。大多数编程语言为动态内存提供类似的自动释放功能。动态分配内存——与自动分配相反——是程序员用来标识根据需求请求新内存的代码部分的术语。例如，当客户数量增加时，这将用于存储客户对新内存空间的请求列表的程序中。为了区分不同类型的内存管理，不管是自动的还是手动的，程序员使用内存分段。一个程序使用几个内存段进行操作，堆栈、堆、只读段等等，尽管它们都具有相同的结构，并且是同一虚拟内存的一部分。\n\n大多数语言都提供了访问动态内存的简化方法，而不考虑其释放策略，将困难的工作留给了运行时支持环境。C++ 程序员必须处理内存管理的低级细节。无论是由于语言的哲学、结构还是年代，C++ 都不提供高级内存管理功能。因此，深刻理解内存结构及其管理是每个 C++ 程序员都必须要做的事情。现在让我们在本章中阐明内存和正确的内存管理技术背后的奥秘。\n\n在本章中，我们将涵盖以下主题:\n\n*   什么是内存，我们如何在 C++ 中访问它？\n*   详细的内存分配\n*   记忆管理技术和习惯用法\n*   垃圾收集基础知识\n\n# 技术要求\n\n带有选项`-std=c++ 2a`的`g++ `编译器用于编译整个章节的示例。\n\n你可以在[https://github.com/PacktPublishing/Expert-CPP](https://github.com/PacktPublishing/Expert-CPP)找到本章用到的源文件。\n\n# 理解计算机内存\n\n在最低层次的表示中，存储器是存储位状态的设备。假设我们正在发明一种可以存储一点信息的设备。如今，它似乎既无意义又神奇。发明已经被发明的东西是没有意义的，很久以前。这很神奇，因为现在的程序员拥有稳定的多功能环境，提供大量的库、框架和工具来创建程序，甚至不需要在幕后理解它们。声明一个变量或分配一个动态内存变得异常容易，如下面的代码片段所示:\n\n```cpp\nint var;\ndouble* pd = new double(4.2);\n```\n\n很难描述设备如何存储这些变量。为了解释这个神奇的过程，让我们试着设计一个存储信息的设备。\n\n# 设计存储器存储设备\n\n我们将使用电路、继电器和逻辑门来设计一个能够存储一位的简单设备。本节的目的是了解最低层次的内存结构。\n\n这里有一个简单的电路示例，你在物理课上应该很熟悉:\n\n![](img/e049c87e-55a0-431d-96bf-28b605e4c07e.png)\n\n它由连接电池和**灯泡**的**导线**组成。**电线**有一个**开关**，控制灯泡的状态。开关闭合时**灯泡**亮，否则熄灭。我们将在这个电路中增加两个或非门逻辑元件。“或非”是“非或”的缩写。它通常以下列方式表示:\n\n![](img/4072ad42-c9a2-495f-aee5-06d736ddb9c9.png)\n\n它有两个输入端(通向元件的导线)，每个输入端代表一个电信号。如果两个输入都是 0，我们说输出(从元件出来的线)是 1。这就是为什么我们称之为*而不是*，因为如果 OR 元素的任何一个输入为 1，它就会输出 1。前面的或非门元件简单地由两个继电器构成。继电器是一种开关，它使用电磁铁来闭合和断开触点。请看下图:\n\n![](img/99bb5ce8-b8cc-4b27-b767-ec65c76b7ddc.png)\n\n当**继电器**的两个**开关**都闭合时(表示**继电器**正在工作，拉下电路的**开关**，灯泡*关闭*。当我们将**开关**移动到两个**继电器**的打开位置时，灯泡打开*。上图是描述或非门的方法之一。此时，我们可以使用电线、灯泡、电池和继电器来创建逻辑元件。现在让我们来看看两个 NOR 元素的奇怪组合，它带来了一个有趣的发现:*\n\n *![](img/767e57ea-e066-4eef-b2eb-afc2868604c2.png)\n\n上图是一个 **R-S 触发器**的典型代表。 **R** 为*复位*， **S** 为*设定*。由前述方案构建的设备可以存储一位。输出 **Q** 是我们可以读取设备内容的导线。如果我们设置触发器来存储该位，输出将为 1。你应该仔细检查该图，想象将信号逐个或同时传递到它的输入端，并在 **Q** 处看到输出。当输入 **S** 为 1 时， **Q** 变为 1。当 **R** 为 1 时， **Q** 变为 0。这样我们*设置*或者*重置*位。只要我们向器件提供电流，它就会存储该位。\n\n现在想象一下，将许多先前设计的设备互连在一起，这样我们就可以存储不止一位的信息。通过这种方式，我们可以构建复杂的存储设备来存储字节甚至千字节(T2)数据。\n\n前面的装置类似于晶体管发明之前计算机中使用的装置。晶体管是一种能够存储比特的小得多的器件。晶体管的类型不同。现代设备不使用继电器；相反，它们集成了数百万个晶体管来存储和处理数据。一个**中央处理器** ( **中央处理器**)寄存器是一个利用晶体管来存储指定数量的位的设备的例子。通常，通用寄存器最多存储 64 位数据。但是，您不能仅使用寄存器来存储所有程序和数据。计算机内存的组织要复杂得多。现在让我们从更高层次的角度来研究计算机内存的层次结构。\n\n# 从更高层次的角度理解计算机内存\n\n了解计算机内存和数据存储的细节对于编写专业程序至关重要。当程序员提到*内存*这个术语时，大多数时候指的是虚拟内存。虚拟内存是由**操作系统** ( **操作系统**)支持的抽象，它控制并为进程提供内存空间。每个进程都有自己的地址空间，表示为几个段的集合。我们在[第 2 章](02.html)、*用 C++ 进行低级编程*中讨论了内存段有哪些，以及给定程序如何使用每一段。从程序员的角度来看，访问内存空间大多仅限于对象声明和使用。无论我们在堆栈、堆还是静态内存中声明一个对象，我们都访问相同的内存抽象——虚拟内存。虽然很复杂，但虚拟记忆让生活变得容易多了。直接使用物理内存更难，尽管这是程序员技能的一大进步。你至少应该知道有哪些内存存储单元，以及如何利用这些知识来编写更好的代码。\n\n在本节中，我们讨论了物理内存层次结构。我们称之为*层级*，因为较低级别的每个存储单元提供更快的访问，但空间更小。每一个连续更高的内存级别都会提供更多的空间来换取更慢的访问。\n\n我们讨论物理内存层次结构，因为它将帮助我们设计更好的代码。了解内存在每一个级别上是如何工作的，可以提高我们作为程序员的能力，让我们能够更好地组织数据操作。下图说明了内存层次结构:\n\n![](img/3485e166-6d8d-4a59-8fe8-da8e249cd123.png)\n\n寄存器是放置在中央处理器中最快可访问的存储单元。寄存器的数量是有限的，所以我们不能把所有的程序数据都保存在里面。另一方面，**动态随机存取存储器** ( **动态随机存取存储器**)能够存储程序的各种数据。由于其物理结构和与中央处理器的距离，从动态随机存取存储器访问数据需要更长的时间。中央处理器通过数据总线访问动态随机存取存储器，数据总线是一组在中央处理器和动态随机存取存储器之间传输数据的导线。为了向动态随机存取存储器控制器发出它将读取还是写入数据的信号，中央处理器使用控制总线。我们将 DRAM 称为*主存储器*。让我们详细看看内存层次结构。\n\n# 登记\n\n寄存器保存固定数量的数据。CPU 字长通常由寄存器的最大长度来定义，例如 8 字节或 4 字节。我们不能从 C++ 程序直接访问寄存器。\n\nC++ supports embedding assembly code using the `asm` declaration, for example, `asm(\"mov edx, 4\")`. It's a platform-specific and artificial augmentation to the code, so we don't suggest using it.\n\n在语言的旧版本中，我们可以在声明变量时使用`register`关键字:\n\n```cpp\nregister int num = 14;\n```\n\n修饰符指定编译器将变量存储在寄存器中。这样，它给了程序员一种虚假的代码优化感。\n\nCompilers are sophisticated tools translating higher-level C++ code into machine code. In the translation process, the code takes several transformations, including code optimizations. When programmers apply *tricks* to force the compiler to optimize a portion of the code, the compiler takes them as suggestions rather than commands. \n\n例如，如果变量放在寄存器中而不是动态随机存取存储器中，访问循环中的变量会更快。例如，以下循环访问对象一百万次:\n\n```cpp\nauto number{42};\nfor (int ix = 0; ix < 10000000; ++ ix) {\n int res{number + ix};\n  // do something with res\n}\n```\n\n我们知道`number`有自动存储时长(与`auto`关键字无关)，放在栈上。堆栈是虚拟内存中的一个段，虚拟内存是物理动态随机存取存储器上的一个抽象。访问寄存器中的对象比访问动态随机存取存储器中的对象要快得多。让我们假设从动态随机存取存储器读取`number`的值比从`register`读取慢五倍。使用`register`关键字优化前面的循环似乎是显而易见的，如图所示:\n\n```cpp\nregister auto number{42};\n// the loop omitted for code brevity\n```\n\n然而，如今编译器进行了更好的优化，因此对修饰符的需求已经随着时间的推移而减弱，现在它已经成为一种不推荐使用的语言特性。更好的优化是完全去掉`number`对象。\n\n例如，以下代码表示使用实际值而不是通过驻留在动态随机存取存储器中的变量来访问它的编译优化版本:\n\n```cpp\nfor (int ix = 0; ix < 1000000; ++ ix) {\n int res{42 + ix};\n  // do something with res\n}\n```\n\n虽然前面的例子可以说很简单，但是我们应该考虑编译期间发生的编译器优化。\n\n发现寄存器提高了我们对程序执行细节的理解。关键是，中央处理器执行的所有操作都是通过寄存器进行的，包括中央处理器应该解码和执行的指令都是使用特定的寄存器来访问的，该寄存器通常被称为**指令指针**。当我们运行程序时，中央处理器访问它的指令，并解码和执行它们。从主存储器读取数据和向存储器写入数据是通过从寄存器复制数据和向寄存器复制数据来实现的。通常，通用寄存器用于在中央处理器对数据执行操作时临时保存数据。下图描述了**中央处理器**的抽象视图及其通过总线与主存储器的交互:\n\n![](img/b98cbe03-4c6c-4b6c-acb8-5079eee59237.png)\n\n如您所见，中央处理器和动态随机存取存储器之间的通信是通过各种总线进行的。在[第 2 章](02.html)、*C++ 低级编程*中，我们讨论了 c++ 程序的低级表示——您应该快速浏览一下，以便更好地理解下面的示例。\n\n现在，让我们看看寄存器的作用。下面的 C++ 代码声明了两个变量，并将它们的总和存储在第三个变量中:\n\n```cpp\nint a{40}, b{2};\nint c{a + b};\n```\n\n为了执行求和指令，中央处理器将变量`a`和`b`的值移入其寄存器。计算总和后，它会将结果移入另一个寄存器。程序的汇编伪代码表示如下所示:\n\n```cpp\nmov eax, a\nmov ebx, b\nadd eax, ebx\n```\n\n编译器生成将每个变量映射到一个寄存器的代码并不是强制性的——寄存器的数量是有限的。你只需要记住，你应该保持定期访问的变量足够小，以适应其中一个寄存器。对于较大的对象，高速缓冲存储器会有所帮助。让我们看看如何。\n\n# 高速缓冲存储器\n\n缓存的想法在编程和计算机系统中很常见。加载到浏览器中的图像被缓存，以避免将来用户再次访问网站时向网络服务器请求下载。缓存使程序运行得更快。这个概念可以用多种形式来利用，包括单个函数。例如，下面的递归函数计算一个数的阶乘:\n\n```cpp\nlong factorial(long n) {\n  if (n <= 1) { return 1; }\n  return n * factorial(n - 1);\n}\n```\n\n该函数不记得其先前计算的值，因此以下调用分别导致五次和六次递归调用:\n\n```cpp\nfactorial(5); // calls factorial(4), which calls factorial(3), and so on\nfactorial(6); // calls factorial(5), which calls factorial(4), and so on\n```\n\n我们可以在每一步缓存已经计算好的值，方法是将它们存储在全局可访问的变量中，如图所示:\n\n```cpp\nstd::unordered_map<long, long> cache;\n\nlong factorial(long n) {\n  if (n <= 1) return 1;\n if (cache.contains(n)) return cache[n];\n cache[n] = n * factorial(n - 1);\n return cache[n];\n}\n```\n\n修改优化了对函数的进一步调用:\n\n```cpp\nfactorial(4);\n// the next line calls factorial(4), stores the result in cache[5], which then calls factorial(3)\n// and stores the result in cache[4] and so on\nfactorial(5);\nfactorial(6); // calls the factorial(5) which returns already calculated value in cache[5]\n```\n\n缓存的概念使阶乘函数运行得更快，同样，一个名为**缓存**的实际存储设备被放置在中央处理器内部。该设备存储最近访问的数据，以便更快地进一步访问该数据。下图描述了中央处理器内部的**寄存器**和**缓存内存**:\n\n![](img/3ab977f8-8e12-4e36-b903-b72c9da9b7ff.png)\n\n缓存大小通常在 2 KB 到 64 KB 之间(很少为 128 KB)。虽然对于 Photoshop 这样的应用来说，它似乎不够大，图像数据的大小可能远远大于缓存大小本身，但在许多情况下，它确实有所帮助。例如，假设我们在一个向量中存储了 1000 多个数字:\n\n```cpp\nstd::vector<int> vec;\nvec.push_back(1);\n...\nvec.push_back(9999);\n```\n\n以下代码打印矢量项目:\n\n```cpp\nfor (auto it: vec) {\n  std::cout << it;\n}\n// 1\n// 2\n// 3\n// ...\n// 9999\n```\n\n假设要打印该项目，**中央处理器**将其从内存复制到 rax 寄存器，然后调用运算符`<<`，后者将 rax 的值打印到屏幕上。在循环的每次迭代中，**中央处理器**将向量的下一项复制到 rax 寄存器中，并调用函数打印其值。每次复制操作都需要 **CPU** 将项目的地址放在**地址总线**上，并将**控制总线**设置为读取模式。**动态随机存取存储器**微控制器通过地址总线接收的地址访问数据，并将其值复制到数据总线，从而将数据发送到**中央处理器**。**中央处理器**将值导向 rax 寄存器，然后执行指令打印其值。下图显示了**中央处理器**和**动态随机存取存储器**之间的交互:\n\n![](img/38e99ee6-c6b8-4007-8a8b-3705975066fd.png)\n\n为了优化循环，中央处理器保持数据局部性的思想，即它将整个向量复制到缓存中，并从缓存中访问向量项，省略了对动态随机存取存储器的不必要的请求。在下图中，您可以看到通过数据总线从动态随机存取存储器接收的数据然后存储在**高速缓冲存储器**中:\n\n![](img/a638de34-c3d8-4c57-9edc-078fb8563b94.png)\n\n驻留在中央处理器中的缓存称为**1 级** ( **L1** ) **缓存**。这是最小的容量，位于中央处理器内部。很多架构都有**二级** ( **L2** ) **缓存**，它驻留在 CPU 之外(虽然比主内存近)，访问方式和 DRAM 一样。L2 缓存和动态随机存取存储器的区别在于物理结构和数据访问模式。L2 缓存代表**静态随机存取存储器** ( **静态随机存取存储器**)，比动态随机存取存储器快，但也贵得多。\n\nSome runtime environments leverage the idea of caching when implementing garbage collection. They separate the objects into categories based on their lifetime with objects that have the smallest lifetime, such as the ones allocated in the local scope of the code, being placed in the cache both to be accessed and deallocated faster.\n\n新级别的高速缓冲存储器用作较低级别的高速缓存。例如，L2 缓存用作一级缓存的缓存。当中央处理器遇到缓存未命中时，它会请求 L2 缓存，依此类推。\n\n# 主存储器\n\n动态随机存取存储器的物理结构迫使它刷新电荷以保持数据稳定，而静态随机存取存储器不需要像动态随机存取存储器那样刷新。我们称 DRAM 为主存储器，主要是因为程序被装入其中；操作系统维护虚拟内存并将其映射到动态随机存取存储器。所有的实际工作都是先通过主存进行的。\n\n正如我们已经讨论过的，主存储器代表一系列可寻址字节的数据。每个字节都有自己唯一的地址，并使用该地址进行访问。我们前面提到了中央处理器如何将数据地址放在地址总线上，从而让动态随机存取存储器微控制器获取请求的数据并通过数据总线发送。\n\n众所周知，操作系统引入虚拟内存作为物理内存的抽象。它将虚拟内存的内容映射到物理内存，物理内存涉及到 CPU 的**翻译后备缓冲区** ( **TLB** )。TLB 是高速缓冲存储器的另一种形式:它存储**虚拟存储器**到**物理存储器**的最近翻译，从而为将来的请求高速缓存它。如下图所示，**中央处理器**与 **TLB** 配合，以便将虚拟地址正确转换为物理地址:\n\n![](img/38b916ec-a8a1-40f4-aa32-f99bc44eaebf.png)\n\n虽然内存管理很复杂，但操作系统为我们提供了足够简单的抽象来管理程序所需的内存。我们能够使用堆栈自动分配它，或者在堆上动态分配它。自动内存分配实际上并不涉及很多问题和困难；我们只是声明对象，它们被放在堆栈上，然后每当执行离开作用域时自动移除。在动态内存的情况下(不要与前面提到的硬件动态随机存取存储器混淆)，分配和解除分配都应该手动完成，这为出错导致内存泄漏创造了可能性。\n\n# 永久存储\n\n当我们关闭计算机时，主存储器的内容被擦除(因为电荷不再被刷新)。为了即使在断电的情况下也能永久存储数据，电脑配备了**硬盘驱动器** ( **硬盘驱动器**)或**固态驱动器** ( **固态硬盘**)。从程序员的角度来看，永久存储器用于存储程序及其必要的数据。我们已经知道，为了运行一个程序，它应该被加载到主内存中，即从硬盘复制到动态随机存取存储器中。操作系统使用加载器处理它，并在内存中创建程序映像，通常称为进程。当程序完成或用户关闭时，操作系统将进程的地址范围标记为可自由使用。\n\n假设我们在学习 C++ 时使用文本编辑器写笔记。输入编辑器的文本驻留在主内存中，除非我们将其保存在硬盘上。注意这一点很重要，因为大多数程序跟踪最近的用户活动，并允许用户修改程序设置。为了在程序重新启动后保持用户修改设置的方式，程序将它们作为单独的*设置*文件存储在硬盘上。下次程序运行时，它首先从硬盘中读取相应的设置文件，并更新自己以应用最近对设置的修改。\n\n通常，与主存储器相比，永久存储器具有更大的容量，这使得将硬盘驱动器用作虚拟存储器的备份成为可能。操作系统可以维护虚拟内存并伪造其大小，使其大于物理动态随机存取存储器。例如，通过启动几个重量级应用，动态随机存取存储器的最大 2gb 容量可能会很快耗尽。但是，操作系统仍然可以通过用硬盘备份其额外空间来维护更大的虚拟内存。当用户在应用之间切换时，操作系统将超过字节的虚拟内存复制到硬盘驱动器，并将当前运行的应用映射到物理内存。\n\n这使得程序和操作系统运行得更慢，但允许我们保持它们打开，而不用考虑有限的主内存大小。现在让我们更深入地探讨一下 C++ 中的内存管理。\n\n# 内存管理的基础\n\n大多数情况下，内存管理过程中出现的问题发生在程序员忘记释放内存空间时。这会导致内存泄漏。几乎在每个程序中，内存泄漏都是一个普遍的问题。当程序为其数据请求新的存储空间时，操作系统将所提供的空间标记为**忙碌**。也就是说，程序的任何其他指令或任何其他程序都不能请求这个繁忙的存储空间。当程序的这一部分用完内存空间时，理想情况下，它必须通知操作系统移除繁忙标签，以便为其他人提供空间。一些语言提供了对动态分配内存的自动控制，让程序员担心应用的逻辑，而不是一直关心内存资源的释放。然而，C++ 假设程序员是负责任和聪明的(这并不总是如此)。动态分配的内存管理是程序员的责任。这就是为什么该语言同时提供“new”和“delete”操作符来处理内存空间，其中新的操作符分配内存空间，而 delete 操作符释放内存空间。换句话说，处理动态分配内存的理想代码如下所示:\n\n```cpp\nT* p = new T(); // allocate memory space\np->do_something(); // use the space to do something useful\ndelete p; // deallocate memory space\n```\n\n忘记调用删除操作符会使分配的内存空间*永远忙碌*。我们所说的*永远*是指只要程序在运行。现在想象一个总是在用户计算机上打开的网络浏览器。随着时间的推移，到处内存泄漏可能会导致内存不足，迟早用户必须重启程序，甚至更糟的是，重启操作系统。\n\n这个问题适用于我们使用的任何资源，无论是文件还是我们忘记关闭的套接字(更多关于套接字的信息请参见[第 12 章](12.html)、*网络和安全*)。为了解决这个问题，C++ 程序员使用了**资源获取是初始化** ( **RAII** )的习惯用法，表示应该在资源初始化时获取资源，这样可以在以后正确释放资源。让我们看看它在行动。\n\n# 内存管理的一个例子\n\n考虑以下函数，该函数动态分配一个 420 `shorts`的数组，从用户输入中读取它们的值，以升序打印它们，并解除分配该数组:\n\n```cpp\nvoid print_sorted() {\n  short* arr{new short[420]};\n  for (int ix = 0; ix < 420; ++ ix) {\n    std::cin >> arr[ix];\n  }\n  std::sort(arr, arr + 420);\n  for (int ix = 0; ix < 420; ++ ix) {\n    std::cout << arr[ix];\n  }\n  delete arr; // very bad!\n}\n```\n\n我们已经在前面的代码中犯了一个错误，使用了错误的`delete`操作符来释放内存。要解除分配一个数组，我们必须使用`delete[]`运算符，否则，代码会导致内存泄漏。下面是我们如何说明阵列的分配:\n\n![](img/073fb3c2-f36b-4002-bfc3-fecbeb34ecdf.png)\n\n假设我们用`delete`代替`delete[]`释放空间。它会将`arr`视为一个短指针，因此会删除从`arr`指针中包含的地址开始的前两个字节，如下图所示:\n\n![](img/50d84dee-769a-47cc-b579-5967570ec8ed.png)\n\n因此，现在我们移除了 420 个项目中的第一个项目，并将 419 `shorts`保留在堆中不动。每当我们需要堆上的新空间时，包含 419 **贱民**的那一小部分就不会再被重用了。尽管 new 和 delete 操作符家族是由实现定义的，但我们不应该真的希望最好的实现能够避免内存泄漏。\n\n让我们修改前面的代码，以正确释放为数组分配的内存，并确保消除输入负数的可能性:\n\n```cpp\nvoid print_sorted() {\n short* arr{new short[420]};\n  for (int ix = 0; ix < 420; ++ ix) {\n    std::cin >> arr[ix];\n if (arr[ix] < 0) return;\n  }\n  std::sort(arr, arr + 420);\n  // print the sorted array, code omitted for brevity\n delete[] arr;\n}\n```\n\n前面的修改是另一个可能的内存泄漏的例子，尽管为了简单起见，我们显然编写了丑陋的代码。关键是，只要用户输入一个负数，函数就会返回。这给我们留下了 420 个孤儿`shorts`应该以某种方式被释放。然而，对分配内存的唯一访问是`arr`指针，它在堆栈上声明，因此当函数返回时，它将被自动删除(指针变量，而不是指向它的内存空间)。为了消除内存泄漏的可能性，我们应该在函数退出之前简单地调用`delete[]`运算符:\n\n```cpp\nvoid print_sorted() {\n short* arr{new short[420]};\n  for(int ix = 0; ix < 420; ++ ix) {\n    std::cin >> arr[ix];\n if (arr[ix] < 0) {\n delete[] arr;\n return;\n }\n  }\n  // sort and print the sorted array, code omitted for brevity\n delete[] arr;\n}\n```\n\n代码变得有些难看，但它修复了内存泄漏。如果我们进一步修改函数，并使用第三方库函数对数组进行排序，会怎么样:\n\n```cpp\nimport <strange_sort.h>;\n\nvoid print_sorted() {\n  short* arr{new short[420]};\n  for (...) { /* code omitted for brevity */ }\n strange_sort::sort(arr, arr + 420);\n  // print the sorted array, code omitted for brevity\n  delete[] arr;\n}  \n```\n\n原来`strange_sort::sort`在数组项的值超过 420 时抛出异常(毕竟这也是为什么这是一个奇怪的排序)。如果异常没有被捕获，它将会出现在调用者函数中，除非它在某个地方被捕获或者程序崩溃。未捕获的异常导致堆栈展开，这导致`arr`变量(指针)的自动销毁，因此我们面临另一种内存泄漏的可能性。为了解决这个问题，我们可以将`strange_sort::sort`包裹在试捕块中:\n\n```cpp\ntry {\n  strange_sort::sort(arr, arr + 420);\n} catch (ex) { delete[] arr; }\n```\n\nC++ 程序员不断寻找处理内存泄漏的方法，例如 RAII 习惯用法和智能指针，我们将在下一节中讨论它们。\n\n# 使用智能指针\n\n有许多语言支持自动垃圾收集。例如，为对象获取的内存由运行时环境跟踪。它将在引用它的对象超出范围后释放内存空间。例如，考虑以下内容:\n\n```cpp\n// a code sample of the language (not-C++) supporting automated garbage collection\nvoid foo(int age) {\n  Person p = new Person(\"John\", 35);\n  if (age <= 0) { return; }\n  if (age > 18) {\n   p.setAge(18);\n  }\n  // do something useful with the \"p\"\n}\n// no need to deallocate memory manually\n```\n\n在前面的代码块中，`p`引用(通常，垃圾收集语言中的引用类似于 C++ 中的指针)指的是`new`运算符返回的内存位置。自动垃圾收集器管理由`new`操作符创建的对象的生存期。它还跟踪对该对象的引用。每当对象上没有引用时，垃圾收集器就会释放其空间。通过在 C++ 中使用 RAII 习惯用法，可能会达到类似的效果。让我们看看它在行动。\n\n# 利用 RAII 习语\n\n如前所述，RAII 习惯用法建议在初始化时获取资源。请看下面的课:\n\n```cpp\ntemplate <typename T>\nclass ArrayManager {\npublic:\n  ArrayManager(T* arr) : arr_{arr} {}\n  ~ArrayManager() { delete[] arr_; }\n\n  T& operator[](int ix) { return arr_[ix]; }\n\n  T* raw() { return arr_; }\n};\n```\n\n`print_sorted`功能现在可以使用`ArrayManager`正确释放分配的数组:\n\n```cpp\nvoid print_sorted() {\n ArrayManager<short> arr{new short[420]};\n  for (int ix = 0; ix < 420; ++ ix) {\n    std::cin >> arr[ix];\n  }\n  strange_sort::sort(arr.raw(), arr.raw() + 420);\n  for (int ix = 0; ix < 420; ++ ix) {\n    std::cout << arr[ix];\n  }\n}\n```\n\n我们建议使用标准容器，比如`std::vector`而不是`ArrayManager`，尽管这是 RAII 应用的一个很好的例子:在初始化时获取资源。我们创建了一个`ArrayManager`的实例，并用内存资源初始化了它。从这一点上，我们可以忘记它的释放，因为实际的释放发生在`ArrayManager`的析构函数中。当我们在堆栈上声明`ArrayManager`实例时，当函数返回或发生未捕获的异常时，它将被自动销毁，析构函数将被调用。\n\n在这种情况下，最好使用标准容器，所以让我们为单指针实现 RAII 习惯用法。以下代码为`Product`实例动态分配内存:\n\n```cpp\nProduct* apple{new Product};\napple->set_name(\"Red apple\");\napple->set_price(0.42);\napple->set_available(true);\n// use the apple\n// don't forget to release the resource\ndelete apple;\n```\n\n如果我们将 RAII 习惯用法应用于前面的代码，它将在适当的代码执行点释放资源:\n\n```cpp\nResourceManager<Product> res{new Product};\nres->set_name(\"Red apple\");\nres->set_price(0.42);\nres->set_available(true);\n// use the res the way we use a Product\n// no need to delete the res, it will automatically delete when gets out of the scope\n```\n\n`ResourceManager`类还应该重载操作符`*`和`->`，因为它必须像指针一样工作才能正确获取和管理指针:\n\n```cpp\ntemplate <typename T>\nclass ResourceManager {\npublic:\n  ResourceManager(T* ptr) : ptr_{ptr} {}\n  ~ResourceManager() { delete ptr_; }\n\n T& operator*() { return *ptr_; }\n T* operator->() { return ptr_; }\n};\n```\n\n`ResourceManager`类关心 C++ 中智能指针的思想。C++ 11 引入了几种智能指针。我们将它们命名为*智能*，因为它们包装资源并管理其自动解除分配。发生这种情况的唯一原因是，当对象被设置为销毁时，对象的析构函数将被调用。也就是说，我们使用通过对象动态分配的空间进行操作，具有自动存储持续时间。当处理程序对象超出范围时，它的析构函数执行必要的操作来释放底层资源。\n\n然而，智能指针可能会带来额外的问题。上一段讨论的简单智能指针有几个最终会出现的问题。例如，我们没有处理`ResourceManager`复制:\n\n```cpp\nvoid print_name(ResourceManager<Product> apple) {\n  std::cout << apple->name();\n}\n\nResourceManager<Product> res{new Product};\nres->set_name(\"Red apple\");\nprint_name(res);\nres->set_price(0.42);\n// ...\n```\n\n前面的代码导致了未定义的行为。下图显示了隐藏的问题:\n\n![](img/8a9ce9cc-43a7-4a58-a6f1-53b386cae052.png)\n\n**res** 和 **apple** 获得了相同的资源。每当其中一个超出范围( **apple** )时，底层资源就会被释放，这就给另一个`ResourceManager`实例留下了一个悬空的指针。当另一个`ResourceManager`实例超出范围时，它会尝试删除指针两次。通常，程序员知道他们在特定情况下需要的智能指针的种类。这就是为什么 C++ 提供了几种类型的智能指针，我们将进一步讨论。要在程序中使用它们，您应该导入`<memory>`头。\n\n# 标准::唯一 _ptr\n\n类似于我们之前实现的`ResourceManager`实例，`std::unique_ptr`代表一个基本的智能指针。例如，要使用这个智能指针管理`Product`对象，我们需要执行以下操作:\n\n```cpp\nstd::unique_ptr<Product> res{new Product};\nres->set_name(\"Red apple\");\n// res will delete its acquired resource when goes out of scope\n```\n\n注意我们如何访问`Product`成员功能`set_name`。我们将`res`物体视为具有`Pointer*`类型的东西。\n\n`unique_ptr`之所以被命名为唯一，是因为它提供了一种严格所有权的语义——它有义务摧毁被获取的对象。更有趣的是，`unique_ptr`是不能复制的。它没有复制构造函数或赋值运算符。这就是为什么它的**所有权**是*严格的*。当然，这并不意味着我们不能动一个`unique_ptr`班。在这种情况下，我们将所有权完全传递给唯一指针的另一个实例。\n\n智能指针的主要要求之一是保持它们的轻量级。我们肯定会同意这一点。虽然`unique_ptr`是一个有多个成员函数的完整类，但是它没有*用额外的数据成员污染*。它只是指向已分配对象的原始指针的包装器。我们可以通过调用`unique_ptr`的`release()`成员函数来访问该原始指针，如图所示:\n\n```cpp\nProduct* p = res.release();\n// now we should delete p manually to deallocate memory\n```\n\n注意`release()`函数不调用删除运算符。它只会归还所有权。调用`release()`函数后，`unique_ptr`不再拥有资源。要重用已经拥有资源的`unique_ptr`，应该使用`reset()`成员函数。它调用底层指针的删除操作符，*重置*唯一指针以供进一步使用。另一方面，如果想在不释放所有权的情况下获取底层对象，就应该调用`get()`成员函数:\n\n```cpp\nstd::unique_ptr<Product> up{new Product()};\nProduct* p = res.get();\n// now p also points to the object managed by up\n```\n\n我们不能在下面的场景中使用`unique_ptr`类，因为它不能被复制:\n\n```cpp\n// Don't do this\nvoid print_name(std::unique_ptr<Product> apple) {\n  std::cout << apple->name();\n}\nstd::unique_ptr<Product> res{new Product};\nres->set_name(\"Red apple\");\nprint_name(res); // bad code\nres->set_price(0.42);\n// ...\n```\n\n然而，这不是我们在前面的代码中寻找的。您可以认为前面的代码是一个糟糕的设计，因为它混淆了所有权细节。让我们继续讨论 C++ 中的下一个智能指针，它解决了将`unique_ptr`传递给函数的问题。\n\n# std::shared_ptr 和 std::weak_ptr\n\n我们需要一个智能指针来提供*共享所有权*。我们需要的是在 C++ 11 中作为`std::shared_ptr`引入的。实现共享所有权的智能指针更难，因为您应该注意资源的正确释放。例如，当前面代码块中的`print_name()`函数完成工作时，它的参数和局部对象将被销毁。销毁智能指针会导致对其拥有的资源进行适当的解除分配。智能指针如何知道该资源是否仍由另一个智能指针拥有？一种流行的解决方案是保持对资源的引用计数。`shared_ptr`类也是这样做的:它保持指向底层对象的指针数量，并在使用计数变为 0 时将其删除。因此，几个共享指针可以拥有同一个对象。\n\n现在，我们刚才讨论的例子应该改写如下:\n\n```cpp\nvoid print_name(std::shared_ptr<Product> apple) {\n  std::cout << apple->name();\n}\nstd::shared_ptr<Product> res{new Product};\nres->set_name(\"Red apple\");\nprint_name(res);\nres->set_price(0.42);\n// ...\n```\n\n调用`print_name()`函数后，共享指针的使用次数增加 1。当函数完成其工作时，它将减少 1，但不会释放托管对象。因为`res`对象还没有脱离范围。让我们稍微修改一下这个示例，打印对共享对象的引用计数:\n\n```cpp\nvoid print_name(std::shared_ptr<Product> apple) {\n  std::cout << apple.use_count() << \" eyes on the \" << apple->name();\n}\n\nstd::shared_ptr<Product> res{new Product};\nres->set_name(\"Red apple\");\nstd::cout << res.use_count() << std::endl;\nprint_name(res);\nstd::cout << res.use_count() << std::endl;\nres->set_price(0.42);\n// ...\n```\n\n前面的代码将在屏幕上打印以下内容:\n\n```cpp\n1\n2 eyes on the Red apple\n1\n```\n\n当最后一个`shared_ptr`超出范围时，也会破坏底层对象。但是，在共享指针之间共享对象时应该小心。以下代码显示了共享所有权的一个明显问题:\n\n```cpp\nstd::shared_ptr<Product> ptr1{new Product()};\nProduct* temp = ptr1.get();\nif (true) {\n  std::shared_ptr<Product> ptr2{temp};\n  ptr2->set_name(\"Apple of truth\");\n}\nptr1->set_name(\"Peach\"); // danger!\n```\n\n`ptr1`和`ptr2`都指向同一个物体，但彼此并不知晓。所以当我们通过`ptr2`修改`Product`对象时，会影响到`ptr1`。当`ptr2`超出范围时(在`if`语句后)，会破坏底层对象，该对象仍归`ptr1`所有。发生这种情况是因为我们通过传递原始的`temp`指针来使`ptr2`拥有对象。`ptr1`追踪不到。\n\n只有使用`std::shared_ptr`的复制构造函数或赋值运算符才能共享所有权。这样，如果另一个`shared_ptr`实例正在使用该对象，我们可以避免删除该对象。共享指针使用控制块实现共享所有权。每个共享指针包含两个指针，一个指向它所管理的对象，一个指向控制块。控制块表示包含资源使用计数的动态分配空间。它还包含其他几个对`shared_ptr`至关重要的东西，例如资源的`allocator`和`deleter`。我们将在下一节介绍分配器。`deleter`通常是常规的`delete`操作员。\n\n控制块还包含弱引用的数量。这样做是因为所拥有的资源也可能指向一个弱指针。`std::weak_ptr`是`std::shared_ptr`的小哥哥。它引用由`shared_ptr`实例管理的对象，但不拥有它。`weak_ptr`是在不拥有资源的情况下获取和使用`shared_ptr`拥有的资源的一种方式。但是，有一种方法可以使用`lock()`成员函数将`weak_ptr`实例转换为`shared_ptr`。\n\n`unique_ptr`和`shared_ptr`都可以用于管理动态分配的阵列。必须正确指定模板参数:\n\n```cpp\nstd::shared_ptr<int[]> sh_arr{new int[42]};\nsh_arr[11] = 44;\n```\n\n要访问底层数组的元素，我们使用共享指针的`[]`操作符。另外，请注意，在动态多态中使用智能指针不会有缺点。例如，假设我们有以下类层次结构:\n\n```cpp\nstruct Base\n{\n  virtual void test() { std::cout << \"Base::test()\" << std::endl; }\n}; \n\nstruct Derived : Base\n{\n  void test() override { std::cout << \"Derived::test()\" << std::endl; }\n};\n```\n\n以下代码按预期工作并将`Derived::test()`输出到屏幕:\n\n```cpp\nstd::unique_ptr<Base> ptr = std::make_unique_default_init<Derived>();\nptr->test();\n```\n\n虽然智能指针的使用可能会破坏指针的美观，但建议集中使用智能指针来避免内存泄漏。然而，值得注意的是，用智能指针替换所有指针，无论是`unique_ptr`指针还是`shared_ptr`指针，都不能解决所有的内存泄漏问题。它们也有缺点。考虑一种平衡的方法，或者更好的方法，在将问题和智能指针应用到问题之前，详细地彻底理解问题和智能指针本身。\n\n在 C++ 程序中管理内存是有代价的。我们讨论过的最重要的事情是内存空间的正确释放。该语言不支持自动内存释放，但值得一提的是垃圾收集器。然而，为了拥有一个完整的垃圾收集器，我们需要语言级别的支持。C++ 不提供这些。让我们试着用 C++ 模仿一个垃圾收集器。\n\n# 碎片帐集\n\n垃圾收集器是一个独立的模块，通常包含在可解释语言的运行时环境中。比如 C#和 Java 都有垃圾收集器，让程序员的生活轻松了很多。垃圾收集器跟踪代码中的所有对象分配，并在它们不再使用时解除分配。它之所以被称为**垃圾收集器**，是因为它在内存资源被使用后将其删除:它收集程序员留下的垃圾。\n\n据说 C++ 程序员不会在他们之后留下垃圾，这就是为什么该语言不支持垃圾收集器的原因。尽管程序员倾向于为这种语言辩护，声称它没有垃圾收集器，因为它是一种速度很快的语言，但事实是没有垃圾收集器它也能生存。\n\n像 C#这样的语言将程序编译成中间字节码表示，然后由运行时环境解释和执行。垃圾收集器是环境的一部分，它会主动跟踪所有对象分配。它是一只老练的野兽，会尽力在合理的时间内管理好记忆。下图描述了一个典型的运行时环境，它在垃圾收集器的监督下分配内存:\n\n![](img/2421ee02-8ebd-4784-8109-1c53318b8124.png)\n\n我们手动调用`delete`运算符来释放 C++ 中的内存空间，即使在使用智能指针时也是如此。智能指针只是获取对象，并在对象超出范围时将其删除。关键是，即使智能指针引入了一些半自动行为，它们仍然表现得好像程序员没有忘记在代码的指定点释放资源。垃圾收集器会自动这样做，并且通常使用单独的执行线程。它尽力不降低程序的实际执行速度。\n\n一些垃圾收集实现技术包括根据对象的生存期对其进行分类。分类使垃圾收集器访问对象，并在对象不再使用时释放内存空间。为了使这个过程更快，应该比持续时间更长的对象更频繁地访问持续时间短的对象。以下面的代码为例:\n\n```cpp\nstruct Garbage {\n  char ch;\n  int i;\n};\n\nvoid foo() {\n  Garbage* g1 = new Garbage();\n  if (true) {\n    Garbage* g2 = new Garbage();\n  }\n}\n\nint main() {\n  static Garbage* g3 = new Garbage();\n}\n```\n\n如果 C++ 有一个垃圾收集器，那么对象`g1`、`g2`和`g3`将在程序执行的不同时间段被删除。如果垃圾收集器根据它们的生命周期对它们进行分类，那么`g2`将具有最短的生命周期，并且应该首先被访问以释放它。\n\n要真正在 C++ 中实现垃圾收集器，我们应该让它成为程序的一部分。垃圾收集器应该首先负责分配内存来跟踪和删除它:\n\n```cpp\nclass GarbageCollector {\npublic:\n template <typename T>\n static T* allocate() { \n   T* ptr{new T()};\n objects_[ptr] = true;\n   return ptr;\n }\n\n static void deallocate(T* p) {\n   if (objects_[p]) {\n     objects_[p] = false;\n     delete p;\n   }\n } private:\n std::unordered_map<T*, bool> objects_;\n};\n```\n\n前面的类跟踪通过静态`allocate()`函数分配的对象。如果对象正在使用，则通过`deallocate()`功能删除。以下是`GarbageCollector`的用法:\n\n```cpp\nint* ptr = GarbageCollector::allocate<int>();\n*ptr = 42;\nGarbageCollector::deallocate(ptr);\n```\n\n实际上，这个类比智能指针更难管理内存。基本上，没有必要在 C++ 中实现垃圾收集器，因为智能指针几乎可以处理任何关于*自动*内存释放的场景。\n\n然而，让我们来看一个技巧，它将允许垃圾收集器正确地释放某个指针所指向的空间。在我们之前最简单的实现中，我们跟踪了我们提供给用户的所有指针。每个指针都指向堆上应该在程序执行的某个时刻释放的空间。在`GarbageCollector`中，我们将使用标准的`delete`运算符。问题是，它怎么知道应该释放多少字节？看看下面的例子:\n\n```cpp\nStudent* ptr = new Student;\nint* ip = new int{42};\n// do something with ptr and ip\ndelete ptr;\ndelete ip;\n```\n\n让我们假设一个`Student`实例占用 40 字节的内存，一个整数占用 4 字节。我们应该以某种方式将该信息传递给删除操作符。在前面的代码中，我们删除了`ptr`和`ip`，它们都指向不同大小的内存空间。那么它怎么知道在`ptr`的情况下 40 字节应该标记为空闲，在`ip`的情况下 4 字节应该标记为空闲呢？这个问题的解决方案不止一个，我们来看其中一个。\n\n每当我们分配内存时，新操作符都会将分配空间的大小放在实际内存空间之前，如下图所示:\n\n![](img/10cfb979-45e6-41f9-9d2f-7fcb0fff9408.png)\n\n该信息随后被`delete`操作符使用，该操作符通过读取放置在存储空间之前的相应字节来读取存储空间的大小。C++ 最关心的问题之一是管理数据集合的内存。在[第 6 章](06.html)、*挖掘 STL* 中的数据结构和算法中描述的 STL 容器，如`std::vector`和`std::list`，有不同的模型来处理内存。默认情况下，容器有一个指定的内存分配器来处理容器元素的内存分配和释放。让我们更详细地处理分配器。\n\n# 使用分配器\n\n分配器背后的思想是为容器内存管理提供控制。简单地说，分配器是 C++ 容器的高级垃圾收集器。虽然我们在容器内存管理的范围内讨论分配器，但是您肯定可以将这个想法扩展到一个通用的垃圾收集器。在这一节的开始，我们实现了一个设计糟糕的垃圾收集器。在检查分配器时，你会发现设计糟糕的`GarbageCollector`类和 C++ 中的默认分配器有很多相似之处。在`<memory>`中定义，默认分配器有两个基本功能–`allocate()`和`deallocate()`。`allocate()`功能定义如下:\n\n```cpp\n[[nodiscard]] constexpr T* allocate(std::size_t num);\n```\n\n`allocate()`功能为`T`类型的`num`对象获取空间。注意`[[nodiscard]]`属性——这意味着返回值不应该被调用者丢弃。否则，编译器将打印一条警告消息。\n\n让我们使用分配器来获取五个整数的空间:\n\n```cpp\nimport <memory>;\n\nint main()\n{\n  std::allocator<int> IntAlloc;\n  int* ptr = IntAlloc.allocate(5);\n  // construct an integer at the second position\n std::allocator_traits<IntAlloc>::construct(IntAlloc, ptr + 1, 42);\n  IntAlloc.deallocate(ptr, 5); // deallocate all\n}\n```\n\n注意我们如何使用`std::allocator_traits`在分配的空间中构建对象。下图显示了\n\n`deallocate()`功能定义如下:\n\n```cpp\nconstexpr void deallocate(T* p, std::size_t n)\n```\n\n在前面的代码片段中，我们通过传递`allocate()`函数返回的指针来使用`deallocate()`函数。\n\n您可能不会在您的项目中直接使用分配器，但是，每当您需要内存管理的自定义行为时，使用现有的或引入新的分配器可能会有所帮助。STL 容器使用分配器主要是因为它们在结构和行为上不同，这导致需要有专门的行为来进行内存分配和释放。我们将在下一章更详细地讨论 STL 容器。\n\n# 摘要\n\nC#等语言中的垃圾收集器是由环境提供的。它们与用户程序并行工作，并试图在程序看起来高效时清理程序。我们不能在 C++ 中做同样的事情；我们所能实现的就是直接在程序中实现一个垃圾收集器，提供一种半自动的方式来释放已使用的内存资源。自 C++ 11 以来，该语言中引入的智能指针恰当地涵盖了这种机制。\n\n内存管理是每个计算机程序的关键组成部分之一。程序应该能够在执行过程中动态请求内存。好的程序员理解内存管理的内在细节。这有助于他们设计和实现性能更高的应用。虽然手动内存管理被认为是一种优势，但在大型应用中，它往往会变得令人痛苦。在本章中，我们学习了如何使用智能指针来避免错误和处理内存释放。有了这个基本的理解，你应该在设计避免内存泄漏的程序时增加你的信心。\n\n在下一章中，我们将学习 STL，重点是数据结构和算法，并将深入研究它们的 STL 实现。除了比较数据结构和算法，我们还将介绍 C++ 20 中一个值得注意的新特性:概念。\n\n# 问题\n\n1.  解释计算机内存。\n2.  什么是虚拟内存？\n3.  哪些操作符用于内存分配和释放？\n4.  `delete`和`delete[]`有什么区别？\n5.  什么是垃圾收集器，为什么 C++ 不支持垃圾收集器？\n\n# 进一步阅读\n\n有关更多信息，请参考以下链接:\n\n*   每一个程序员都应该知道的关于内存的知识，作者:乌尔里希·德雷珀，作者:https://people.freebsd.org/~lstewart/articles/cpumemory.pdf·T2\n*   代码:计算机软硬件的隐藏语言，作者:Charles Petzold，网址:[https://www . Amazon . com/Code-Language-Computer-Hardware-Software/DP/0735611319/](https://www.amazon.com/Code-Language-Computer-Hardware-Software/dp/0735611319/)*"
  },
  {
    "path": "docs/exp-cpp/06.md",
    "content": "# 六、STL 中数据结构和算法的挖掘\n\n掌握数据结构对程序员来说至关重要。大多数时候存储数据的方式决定了应用的整体效率。例如，考虑一个电子邮件客户端。你可以设计一个电子邮件客户端，显示最新的 10 封电子邮件，它可以有最好的用户界面；显示 10 封最近的电子邮件几乎可以在任何设备上顺利工作。例如，在使用你的应用的两年内，你的电子邮件应用的用户将收到数十万封电子邮件。当用户需要搜索电子邮件时，您的数据结构知识将在其中发挥重要作用。你存储成千上万封电子邮件的方式以及你用来分类和搜索它们的方法(算法)将是你的程序区别于其他程序的地方。\n\n程序员在项目中努力寻找日常问题的最佳解决方案。使用成熟的数据结构和算法可以极大地改善程序员的工作。一个好程序最重要的特征之一是它的速度，我们通过设计新的算法或使用现有的算法来获得速度。\n\n最后，C++ 20 引入了**概念**来定义**元类型**—描述其他类型的类型。这种语言的强大特性使得数据架构变得完整。\n\nC++ **标准模板库** ( **STL** )涵盖了大量的数据结构和算法。我们将探索通过利用 STL 容器，使用数据结构有效组织数据的方法。然后我们将深入研究 STL 提供的算法实现。理解和使用 STL 容器中的概念至关重要，因为 C++ 20 通过引入迭代器概念对迭代器进行了很大的改进。\n\n本章将涵盖以下主题:\n\n*   数据结构\n*   STL 容器\n*   概念和迭代器\n*   掌握算法\n*   探索树和图形\n\n# 技术要求\n\n带有选项`-std=c++ 2a`的 g++ 编译器用于编译整个章节的示例。您可以在 https://github.com/PacktPublishing/Expert-CPP 的本书 GitHub 资源库中找到本章使用的源文件。\n\n# 数据结构\n\n作为一名程序员，您可能熟悉使用数组来存储和排序数据集合。程序员在他们的项目中大量使用数组以外的数据结构。了解并应用正确的数据结构可能会在程序性能中发挥重要作用。为了选择正确的数据结构，您需要更好地了解它们。一个显而易见的问题是，我们是否需要研究大量的数据结构——向量、链表、散列表、图表、树等等。为了回答这个问题，让我们设想一个场景，在这个场景中，对更好的数据结构的需求将自然变得明显。\n\n在介绍性内容中，我们提到了设计电子邮件客户端。让我们大致了解一下它的设计和实现过程中的基本任务。\n\n电子邮件客户端是一个列出从不同发件人处收到的电子邮件的应用。我们可以将其安装在台式电脑或智能手机上，或者使用浏览器版本。电子邮件客户端应用的主要任务包括发送和接收电子邮件。现在让我们假设我们正在设计一个足够简单的电子邮件客户端。就像在编程书籍中经常发生的那样，让我们假设我们使用了一些封装了发送和接收电子邮件工作的库。我们更愿意专注于设计专门用于存储和检索电子邮件的机制。电子邮件客户端用户应该能够查看应用的**收件箱**部分中的电子邮件列表。我们还应该考虑用户可能想要对电子邮件执行的操作。他们可以一个接一个地删除，或者一次删除很多。他们可以选择任何随机选择的电子邮件，并回复其发件人或将电子邮件转发给其他人。\n\n我们在[第 10 章](10.html)、*设计真实世界应用*中讨论软件设计过程和最佳实践。现在，让我们绘制一个描述电子邮件对象的简单结构，如下所示:\n\n```cpp\nstruct Email\n{\n  std::string subject;\n  std::string body;\n  std::string from;\n  std::chrono::time_point datetime;\n};\n```\n\n第一件应该困扰我们的事情是将一组电子邮件存储在一个容易访问的结构中。数组听起来可能不错。假设我们将所有传入的电子邮件存储在一个数组中，如下面的代码块所示:\n\n```cpp\n// let's suppose a million emails is the max for anyone\nconst int MAX_EMAILS = 1'000'000; \nEmail inbox[MAX_EMAILS];\n```\n\n我们可以以任何形式存储 10 封电子邮件，这不会影响应用的性能。然而，很明显，随着时间的推移，电子邮件的数量将会增长。对于每个新收到的电子邮件，我们将一个带有相应字段的`Email`对象推入`inbox`数组。数组的最后一个元素代表最近收到的电子邮件。因此，为了显示最近十封电子邮件的列表，我们需要读取并返回数组的最后十个元素。\n\n当我们试图操纵存储在`inbox`数组中的数千封电子邮件时，问题就出现了。如果我们想在所有的邮件中搜索`friend`这个词呢？我们必须扫描数组中的所有电子邮件，并在单独的数组中收集包含`friend`一词的邮件。请看下面的伪代码:\n\n```cpp\nstd::vector<Email> search(const std::string& word) {\n  std::vector<Email> search_results;  \n  for (all-million-emails) {\n    if (inbox[i].subject.contains(word)) {\n      search_results.push_back(inbox[i]);\n    }\n  }\n  return search_results;\n}\n```\n\n使用数组存储所有数据对于小集合来说已经足够了。在处理更大数据集的真实应用中，情况发生了巨大变化。使用特定数据结构的目的是使应用运行得更流畅。前面的例子显示了一个简单的问题:在电子邮件列表中搜索以匹配特定的值。在一封邮件中找到这个价值需要相当长的时间。\n\n如果我们假设一封电子邮件的主题字段可能由多达十个单词组成，那么在电子邮件主题中搜索一个特定的单词需要将该单词与主题中的所有单词进行比较。在*最坏的情况*下，没有匹配。我们强调最坏的情况，因为这是查找需要检查主题中每个单词的唯一情况。对几千或几十万封电子邮件做同样的事情会让用户等得不合理。\n\n就应用效率而言，为特定问题选择正确的数据结构至关重要。例如，假设我们使用哈希表将单词映射到电子邮件对象。每个单词将被映射到包含该单词的电子邮件对象列表。这种方法将提高搜索操作的效率，如下图所示:\n\n![](img/f7318cbc-fefa-41f1-a377-9bf8ebd60b26.png)\n\n`search()`函数将返回哈希表键引用的列表:\n\n```cpp\nstd::vector<Email> search(const std::string& word) {\n  return table[word];\n}\n```\n\n这种方法只需要处理每封收到的电子邮件，将其拆分成单词并更新哈希表。\n\nFor the sake of simplicity, we use `Email` objects as values rather than references. Note that it would be better to store pointers to `Email` in the vector.\n\n现在让我们来看看不同的数据结构及其应用。\n\n# 顺序数据结构\n\n开发人员使用的最常见的数据结构之一是动态增长的一维数组，通常称为向量。STL 提供了一个同名的容器:`std::vector`。向量背后的关键思想是它包含按顺序放置在内存中的相同类型的项目。例如，由 4 字节整数组成的向量将具有以下内存布局。每个方框代表一个四字节的空间。向量的索引位于下图的右侧:\n\n![](img/8f1961c0-dcd6-481f-8ae8-3ba43902ba49.png)\n\n向量的物理结构允许实时访问它的任何元素。\n\nWe should differentiate containers with their operations in order to apply them properly in specific problems. To do so we usually define the complexity of running time of their operations in relation to the number of elements in the container. For example, the vector's element access is defined as a constant time operation, which means that it takes the same number of instructions to fetch a vector item regardless of the vector length.\n\n访问向量的第一个元素和访问向量的第 100 个<sup>元素需要相同的工作量，因此，我们称之为恒定时间操作，也称为 ***O(1)* 操作**。</sup>\n\n虽然 vector 中的元素访问速度很快，但添加新元素有些棘手。每当我们在向量的末尾插入一个新的项时，我们也应该考虑向量的容量。当没有更多空间分配给向量时，它的大小应该会动态增长。看看下面这个带有`push_back()`功能的`Vector`类:\n\n```cpp\ntemplate <typename T>\nclass Vector\n{\npublic:\n  Vector() : buffer_{nullptr}, capacity_{2}, size_{0}\n  {\n    buffer_ = new T[capacity_]; // initializing an empty array\n  }\n  ~Vector() { delete [] buffer_; }\n  // code omitted for brevity\n\npublic:\n  void push_back(const T& item)\n {\n if (size_ == capacity_) {\n // resize\n }\n buffer_[size_++ ] = item;\n }\n  // code omitted for brevity\n};\n```\n\n在进入`push_back()`功能的实现之前，让我们看一下下图:\n\n![](img/11cb3eec-b2a8-4166-8fdf-a58cf516bf90.png)\n\n我们应该分配一个全新的数组，将旧数组的所有元素复制到新数组中，然后在新数组末尾的下一个空闲槽中添加新插入的元素。这在下面的代码片段中显示:\n\n```cpp\ntemplate <typename T>\nclass Vector\n{\npublic:\n  // code omitted for brevity\n  void push_back(const T& item)\n  {\n    if (size_ == capacity_) {\n capacity_ *= 2; // increase the capacity of the vector twice\n T* temp_buffer = new T[capacity_];\n      // copy elements of the old into the new\n for (int ix = 0; ix < size_; ++ ix) {\n temp_buffer[ix] = buffer_[ix];\n }\n delete [] buffer_; // free the old array\n buffer_ = temp_buffer; // point the buffer_ to the new array\n }\n    buffer_[size_++ ] = item;\n  }\n  // code omitted for brevity\n};\n```\n\n可以选择不同的大小调整因子–我们将其设置为`2`，这使得向量在满的时候增长两倍。因此，我们可以坚持认为，在大多数情况下，在向量的末尾插入一个新项目需要恒定的时间。它只是在空闲槽处添加项目，并增加其`private size_`变量。有时，添加新元素需要分配一个新的、更大的向量，并将旧的向量复制到新的向量中。对于这种情况，据说操作需要**摊销**恒定时间才能完成。\n\n当我们在向量的前面加上一个元素时，我们不能说同样的话。关键是，所有其他元素应该向右移动一个槽，以便为新元素释放一个槽，如下图所示:\n\n![](img/0f4021af-1ec3-4d9d-85ca-891a7e16e42a.png)\n\n下面是我们如何在`Vector`类中实现它:\n\n```cpp\n// code omitted for brevity\nvoid push_front(const T& item)\n{\n  if (size_ == capacity_) {\n    // resizing code omitted for brevity\n  }\n  // shifting all the elements to the right\n for (int ix = size_ - 1; ix > 0; --ix) {\n buffer_[ix] = buffer[ix - 1];\n }\n  // adding item at the front buffer_[0] = item;\n  size_++ ;\n}\n```\n\n在只需要在容器前面插入新元素的情况下，选择向量不是一个好的选择。这是应该考虑其他容器的例子之一。\n\n# 基于节点的数据结构\n\n基于节点的数据结构不占用连续的内存块。基于节点的数据结构为其元素分配节点，没有任何顺序——它们可能在内存中随机分布。我们将每个项目表示为链接到其他节点的节点。\n\n最流行和介绍性的基于节点的数据结构是链表。下图显示了双向链表的视觉结构:\n\n![](img/de263cb6-41ed-4f47-a59e-1a9e01261f64.png)\n\n链表和向量非常不同。它的一些运算速度更快，尽管它缺乏向量的紧凑性。\n\n简而言之，让我们在列表的前面实现元素插入。我们将把每个节点作为一个结构:\n\n```cpp\ntemplate <typename T>\nstruct node \n{\n  node(const T& it) : item{it}, next{nullptr}, prev{nullptr} {}\n  T item;\n  node<T>* next;\n  node<T>* prev;\n};\n```\n\n注意`next`成员——它指向同一个结构，这种方式允许将节点链接在一起，如上图所示。\n\n要实现一个链表，我们只需要保持一个指针指向它的第一个节点，通常称为链表的头部。在列表的前面插入一个元素很简单:\n\n```cpp\ntemplate <typename T>\nclass LinkedList \n{\n  // code omitted for brevity\npublic:\n  void push_front(const T& item) \n {\n node<T>* new_node = new node<T>{item};\n if (head_ != nullptr) {\n new_node->next = head_->next;\n if (head_->next != nullptr) {\n head_->next->prev = new_node;\n }\n }\n new_node->next = head_;\n head_ = new_node;\n }\nprivate:\n  node<T>* head_; \n};\n```\n\n在列表中插入元素时，我们应该考虑三种情况:\n\n*   如前所述，在列表前面插入一个元素需要以下步骤:\n\n![](img/06be3736-adbe-4388-9396-677b0a094a7f.png)\n\n*   在列表末尾插入一个元素如下图所示:\n\n![](img/6f88bf92-0a38-448d-a32c-8a92883f53ab.png)\n\n*   最后，在列表中间插入一个元素的操作如下:\n\n![](img/75876dd0-13a8-4b23-a1be-68ac50c50dd0.png)\n\n在前面的图中，将元素插入到向量中显然不同于将元素插入到列表中。你会如何在向量和列表之间做出选择？你应该专注于操作和它们的速度。例如，从向量中读取任何元素需要恒定的时间。我们可以在一个向量中存储一百万封电子邮件，并在 834，000 位置检索一封，而无需任何额外的努力。对于链表，操作是线性的。因此，如果您需要存储大部分是读取的而不是写入的数据集合，那么使用向量显然是一个合理的选择。\n\n在列表中的任何位置插入一个元素需要一个恒定时间的操作，而向量将努力在随机位置插入一个元素。因此，当您需要一个可以集中添加/删除数据的对象集合时，更好的选择是链表。\n\n我们还应该考虑高速缓存。向量具有良好的数据局部性。读取向量的第一个元素需要将第一个 *N 个*元素复制到缓存中。向量元素的进一步读取将会更快。我们不能对链表说同样的话。为了找出原因，让我们继续比较向量和链表的内存布局。\n\n# 内存中的容器\n\n正如您在前面几章中已经知道的，对象占用了提供给进程的一个内存段的内存空间。大多数时候，我们对堆栈或堆内存感兴趣。自动对象占用堆栈上的空间。以下两个声明都位于堆栈中:\n\n```cpp\nstruct Email \n{\n  // code omitted for brevity\n};\n\nint main() {\n  Email obj;\n  Email* ptr;\n}\n```\n\n虽然`ptr`代表指向`Email`对象的指针，但它会占用堆栈上的空间。它可以指向堆上分配的内存位置，但指针本身(存储内存位置地址的变量)驻留在堆栈上。在进一步研究向量和列表之前，理解和记住这一点至关重要。\n\n正如我们在本章前面所看到的，实现一个向量需要封装一个指向内部缓冲区的指针，该缓冲区代表指定类型的元素数组。当我们声明一个`Vector`对象时，它需要必要数量的堆栈内存来存储其成员数据。`Vector`班有以下三名成员:\n\n```cpp\ntemplate <typename T>\nclass Vector\n{\npublic:\n  // code omitted for brevity\n\nprivate:\n  int capacity_;\n  int size_;\n  T* buffer_;\n};\n```\n\n假设一个整数占用 4 字节，一个指针占用 8 字节，那么下面的`Vector`对象声明将占用至少 16 字节的堆栈内存:\n\n```cpp\nint main()\n{\n  Vector<int> v;\n}\n```\n\n下面是我们如何描绘前面代码的内存布局:\n\n![](img/5b07753c-2089-4701-a865-3e98d597197f.png)\n\n插入元素后，堆栈上向量的大小将保持不变。堆来到现场。`buffer_`数组指向使用`new[]`运算符分配的存储位置。例如，请看下面的代码:\n\n```cpp\n// we continue the code from previous listing\nv.push_back(17);\nv.push_back(21);\nv.push_back(74);\n```\n\n我们推送到向量的每个新元素都会占用堆上的空间，如下图所示:\n\n![](img/ffb6f27e-00ca-4b30-86b7-4cdfd6c1530e.png)\n\n每个新插入的元素位于`buffer_`数组的最后一个元素之后。这就是为什么我们可以说向量是一个缓存友好的容器。\n\n声明链表对象也会占用堆栈上的内存空间来存放其数据成员。如果我们讨论仅存储`head_`指针的简单实现，下面的列表对象声明将占用至少 8 字节的内存(仅用于`head_`指针):\n\n```cpp\nint main()\n{\n  LinkedList<int> list;\n}\n```\n\n下图描述了前面代码的内存布局:\n\n![](img/62daaf10-d88c-4439-a8e8-4bb85feb15e4.png)\n\n插入新元素会在堆上创建一个类型为`node`的对象。请看下面一行:\n\n```cpp\nlist.push_back(19);\n```\n\n以下是插入新元素后内存插图的变化:\n\n![](img/62c34472-4720-43f8-80c1-71ac1b5ab204.png)\n\n注意节点及其所有数据成员都驻留在堆上。该项目存储我们插入的值。当我们插入另一个元素时，将再次创建一个新节点。这一次，第一个节点的下一个指针将指向新插入的元素。新插入的节点的 prev 指针将指向列表的前一个节点。下图描述了插入第二个元素后链表的内存布局:\n\n![](img/080ab163-ffd0-4b7b-8ff8-2ba3e9dfed60.png)\n\n当我们在将元素插入列表之间的堆上分配一些随机对象时，会发生一件有趣的事情。例如，下面的代码将一个节点插入列表，然后为一个整数(与列表无关)分配空间。最后，它再次向列表中插入一个元素:\n\n```cpp\nint main()\n{\n  LinkedList<int> list;\n  list.push_back(19);\n  int* random = new int(129);\n  list.push_back(22);\n}\n```\n\n这个中间的随机对象声明破坏了列表元素的顺序，如下图所示:\n\n![](img/bff12ecb-958e-4b6b-95b1-d731f5a627a6.png)\n\n上图给了我们一个提示，列表不是一个缓存友好的容器，因为它的结构和元素的分配。\n\nPay attention to the memory overhead created by incorporating each new node into the code. We pay an additional 16 bytes (considering the pointer takes 8 bytes of memory) for one element. Thus, lists lose the game of optimal memory use to vectors.\n\n我们可以尝试通过在列表中引入预分配的缓冲区来解决这种情况。每个新节点的创建将通过**放置新的**操作符。然而，选择更适合感兴趣的问题的数据结构更明智。\n\n在现实世界的应用开发中，程序员很少实现自己的向量或链表。他们通常使用经过测试和稳定的库版本。C++ 为向量和链表提供了标准容器。此外，它为单链表和双链表提供了两个独立的容器。\n\n# STL 容器\n\nSTL 是一个强大的算法和容器的集合。虽然理解和实现数据结构对程序员来说是一项很好的技能，但是您不必在项目中每次需要时都实现它们。库提供商负责为我们实现稳定且经过测试的数据结构和算法。通过了解数据结构和算法的内部细节，我们在解决问题的同时，也在更好地选择 STL 容器和算法。\n\n前面讨论的向量和链表在 STL 中实现为`std::vector<T>`和`std::list<T>`，其中`T`是集合中每个元素的类型。除了类型，容器还采用第二个默认`template`参数作为分配器。例如，`std::vector`声明如下:\n\n```cpp\ntemplate <typename T, typename Allocator = std::allocator<T> >\nclass vector;\n```\n\n如前一章所介绍的，分配器处理容器元素的有效分配/解除分配。`std::allocator`是 STL 中所有标准容器的默认分配器。基于内存资源行为不同的更复杂的分配器是`std::pmr::polymorphic_allocator`。STL 提供`std::pmr::vector`作为别名模板，它使用多态分配器，定义如下:\n\n```cpp\nnamespace pmr {\n  template <typename T>\n  using vector = std::vector<T, std::pmr::polymorphic_allocator<T>>;\n}\n```\n\n现在让我们仔细看看`std::vector`和`std::list`。\n\n# 使用标准::矢量和标准::列表\n\n`std::vector`在`<vector>`表头定义。下面是最简单的用法示例:\n\n```cpp\n#include <vector>\n\nint main()\n{\n  std::vector<int> vec;\n  vec.push_back(4);\n  vec.push_back(2);\n  for (const auto& elem : vec) {\n    std::cout << elem;\n  }\n}\n```\n\n`std::vector`动态增长。我们应该考虑增长因素。当声明一个向量时，它有一些默认容量，在元素插入时会增加。每当元素的数量超过向量的容量时，它就会以一个给定的因子增加其容量(通常是其容量的两倍)。如果我们知道向量中所需元素的大致数量，我们可以通过使用`reserve()`方法为向量初始分配容量来优化其使用。例如，以下代码保留了 10，000 个元素的容量:\n\n```cpp\nstd::vector<int> vec;\nvec.reserve(10000);\n```\n\n它强制向量为 10，000 个元素分配空间，从而避免在元素插入期间调整大小(除非我们达到 10，000 个元素的阈值)。\n\n另一方面，如果我们遇到容量比向量中元素的实际数量大得多的情况，我们可以收缩向量来释放未使用的内存。我们需要调用`shrink_to_fit()`函数，如下例所示:\n\n```cpp\nvec.shrink_to_fit();\n```\n\n这降低了适应向量大小的能力。\n\n访问向量元素的方式与我们访问常规数组的方式相同，使用`operator[]`。然而，`std::vector`提供了两个访问其元素的选项。其中一种方法被认为是安全的方法，通过`at()`功能完成，如下所示:\n\n```cpp\nstd::cout << vec.at(2);\n// is the same as\nstd::cout << vec[2];\n// which is the same as\nstd::cout << vec.data()[2];\n```\n\n`at()`和`operator[]`的区别在于`at()`通过边界检查访问指定元素；也就是说，下面一行抛出`std::out_of_range`异常:\n\n```cpp\ntry {\n  vec.at(999999);\n} catch (std::out_of_range& e) { }\n```\n\n我们使用`std::list`的方式几乎相同。这些列表大多有类似的公共接口。在本章的后面，我们将讨论迭代器，它允许从特定的容器中抽象出来，这样我们就可以用一个向量替换一个列表，而不会有太大的损失。在此之前，让我们看看列表和向量的公共接口之间的区别。\n\n除了两个容器都支持的标准功能集，如`size()`、`resize()`、`empty()`、`clear()`、`erase()`等，列表还有`push_front()`功能，在列表前面插入一个元素。这是有效的，因为`std::list`代表一个双向链表。如下图所示，`std::list`也支持`push_back()`:\n\n```cpp\nstd::list<double> lst;\nlst.push_back(4.2);\nlst.push_front(3.14);\n// the list contains: \"3.14 -> 4.2\"\n```\n\n该列表支持在许多情况下派上用场的附加操作。例如，要合并两个排序列表，我们使用`merge()`方法。它采用另一个列表作为参数，并将其所有元素移动到当前列表。作为参数传递给`merge()`方法的列表在操作后变为空。\n\nThe STL also provides a singly linked list, represented by `std::forward_list`. To use it, you should include the `<forward_list>` header. As the singly linked list node has only one pointer, it's cheaper in terms of memory than the doubly linked list.\n\n`splice()`方法有点类似于`merge()`，只是它移动了作为参数提供的列表的一部分。通过移动，我们意味着将内部指针指向适当的列表节点。这对`merge()`和`splice()`都是如此。\n\n当我们使用容器来存储和操作复杂的对象时，复制元素的价格对程序的性能起着很大的作用。考虑以下表示三维点的结构:\n\n```cpp\nstruct Point\n{\n  float x;\n  float y;\n  float z;\n\n  Point(float px, float py, float pz)\n    : x(px), y(py), z(pz)\n  {}\n\n  Point(Point&& p)\n    : x(p.x), y(p.y), z(p.z)\n  {}\n};\n```\n\n现在，看看下面的代码，它将一个`Point`对象插入到一个向量中:\n\n```cpp\nstd::vector<Point> points;\npoints.push_back(Point(1.1, 2.2, 3.3));\n```\n\n构建一个临时对象，然后将其移动到向量的相应槽中。我们可以直观地表示如下:\n\n![](img/d94643e3-cbfa-4816-8059-4ac126c1bbcb.png)\n\n显然，向量预先占据了更多的空间来尽可能长时间地延迟调整大小的操作。当我们插入一个新元素时，向量会将其复制到下一个可用的槽中(如果空间已满，会重新分配更多空间)。我们可以使用这个未初始化的空间来创建一个新的元素。向量为此提供了`emplace_back()`函数。我们可以这样使用它:\n\n```cpp\npoints.emplace_back(1.1, 2.2, 3.3);\n```\n\n注意我们直接传递给函数的参数。下图描述了`emplace_back()`的使用:\n\n![](img/47e01350-abf8-4a83-8eba-70afe1301af7.png)\n\n`emplace_back()`通过`std::allocator_traits::construct()`构建元素。后者通常使用新操作符的位置在已经分配的未初始化空间中构造元素。\n\n`std::list`还提供了一种`emplace_front()`方法。这两个函数都返回对插入元素的引用。唯一的要求是元素的类型是`EmplaceConstructible`。对于向量，类型也应该是`MoveInsertable`。\n\n# 使用容器适配器\n\n您可能遇到过将堆栈和队列描述为数据结构(或者用 C++ 来说是*容器*)的情况。从技术上讲，它们不是数据结构，而是数据结构适配器。在 STL 中，`std::stack`和`std::queue`通过提供一个特殊的接口来访问容器来适配容器。术语*栈*几乎无处不在。到目前为止，我们已经使用它来描述具有自动存储持续时间的对象的内存段。由于其分配/解除分配策略，该段采用名称*堆栈*。\n\n我们说，每次声明对象时，都会将它们推送到堆栈中，并在销毁时弹出。对象以被推送的相反顺序弹出。这就是将内存段称为堆栈的原因。相同的**后进先出** ( **后进先出**)方法适用于堆栈适配器。`std::stack`提供的关键功能如下:\n\n```cpp\nvoid push(const value_type& value);\nvoid push(value_type&& value);\n```\n\n`push()`函数有效调用底层容器的`push_back()`。通常，堆栈是使用向量实现的。我们已经在[第 3 章](03.html)、*面向对象编程的细节*中讨论过这样的场景，当时我们引入了保护继承。`std::stack`有两个模板参数；其中之一就是集装箱。选择什么不重要，但一定要有`push_back()`会员功能。`std::stack`和`std::queue`的默认容器是`std::deque`。\n\n`std::deque`允许在其开头和结尾快速插入。它是一个类似于`std::vector`的索引顺序容器。德格这个名字代表*双头队列*。\n\n让我们看看 stack 的实际应用:\n\n```cpp\n#include <stack>\n\nint main()\n{\n  std::stack<int> st;\n  st.push(1); // stack contains: 1\n  st.push(2); // stack contains: 2 1\n  st.push(3); // stack contains: 3 2 1\n}\n```\n\n`push()`功能的更好替代方案是`emplace()`。因此，它调用底层容器的`emplace_back()`，在适当的位置构造元素。\n\n为了拉出元素，我们调用`pop()`函数。它不接受任何参数，也不返回任何内容，只是从堆栈中移除顶部元素。为了访问堆栈的顶部元素，我们调用`top()`函数。让我们修改前面的示例，在弹出所有堆栈元素之前打印它们:\n\n```cpp\n#include <stack>\n\nint main()\n{\n  std::stack<int> st;\n  st.push(1);\n  st.push(2);\n  st.push(3);\n  std::cout << st.top(); // prints 3\n  st.pop();\n  std::cout << st.top(); // prints 2\n  st.pop();\n  std::cout << st.top(); // prints 1\n  st.pop();\n  std::cout << st.top(); // crashes application\n}\n```\n\n`top()`函数返回对顶部元素的引用。它调用底层容器的`back()`函数。注意我们在空栈上调用的最后一个`top()`函数。我们建议您在调用空堆栈上的`top()`之前，使用`size()`检查堆栈的大小。\n\n`queue`是另一个行为与堆栈略有不同的适配器。队列背后的逻辑是先返回第一个插入的元素:它保持了**先进先出** ( **先进先出**)的原则。请看下图:\n\n![](img/04d19255-e43e-485b-af2f-6269d220bd0e.png)\n\n队列中插入和检索操作的正式名称是**查询**和**出列**。`std::queue`保持一致的方法，提供`push()`和`pop()`功能。要访问队列的第一个和最后一个元素，您应该使用`front()`和`back()`。两者都返回对元素的引用。下面是一个简单的用法示例:\n\n```cpp\n#include <queue>\n\nint main()\n{\n std::queue<char> q;\n  q.push('a');\n  q.push('b');\n  q.push('c');\n  std::cout << q.front(); // prints 'a'\n  std::cout << q.back(); // prints 'c'\n  q.pop();\n  std::cout << q.front(); // prints 'b'\n}\n```\n\n当您正确应用各种容器和适配器时，了解它们是非常有用的。为各种问题选择合适的容器并不是万能的。许多编译器使用堆栈来解析代码表达式。例如，使用堆栈很容易验证以下表达式中的括号:\n\n```cpp\nint r = (a + b) + (((x * y) - (a / b)) / 4);\n```\n\n试着练习一下。编写一个小程序，使用堆栈验证前面的表达式。\n\n队列的应用更加广泛。我们将在[第 11 章](11.html)、*中看到其中一个，使用设计模式*设计一个策略游戏，在这里我们设计一个策略游戏。\n\n另一个容器适配器是`std::priority_queue`。优先级队列通常采用平衡的、基于节点的数据结构，如最大堆或最小堆。在本章的最后，我们将研究树和图，看看优先级队列是如何工作的。\n\n# 迭代容器\n\n不可重复使用的集装箱的想法就像一辆不能驾驶的汽车。毕竟，容器是项目的集合。迭代容器元素的常见方法之一是使用普通的旧`for`循环:\n\n```cpp\nstd::vector<int> vec{1, 2, 3, 4, 5};\nfor (int ix = 0; ix < vec.size(); ++ ix) {\n  std::cout << vec[ix];\n}\n```\n\n容器为元素访问提供了一组不同的操作。例如，向量提供`operator[]`，而列表不提供。`std::list`有`front()`和`back()`方法，分别返回第一个和最后一个元素。如前所述，`std::vector`还提供了`at()`和`operator[]`。\n\n这意味着我们不能使用前面的循环来迭代列表元素。但是我们可以用基于范围的`for`循环遍历列表(和向量)，如下所示:\n\n```cpp\nstd::list<double> lst{1.1, 2.2, 3.3, 4.2};\nfor (auto& elem : lst) {\n  std::cout << elem;\n} \n```\n\n这可能看起来令人困惑，但诀窍隐藏在基于范围的`for`实现中。它使用`std::begin()`函数检索指向容器第一个元素的迭代器。\n\n**迭代器**是一个指向容器元素的对象，可以根据容器的物理结构前进到下一个元素。下面的代码声明了一个`vector`迭代器，并用指向`vector`开头的迭代器初始化它:\n\n```cpp\nstd::vector<int> vec{1, 2, 3, 4};\nstd::vector<int>::iterator it{vec.begin()};\n```\n\n容器提供了两个成员函数`begin()`和`end()`，分别将迭代器返回到容器的开头和结尾。下图显示了我们如何处理容器的开头和结尾:\n\n![](img/4a058f5f-c5de-47fb-94e4-a5e25dbf0440.png)\n\n使用基于范围的`for`迭代列表元素的前一个代码可以被认为如下:\n\n```cpp\nauto it_begin = std::begin(lst);\nauto it_end = std::end(lst);\nfor ( ; it_begin != it_end; ++ it_begin) {\n  std::cout << *it_begin;\n}\n```\n\n注意我们在前面的代码中使用的`*`运算符，它通过迭代器访问底层元素。我们认为迭代器是指向容器元素的聪明的 T2 指针。\n\nThe `std::begin()` and `std::end()` functions typically call the containers' `begin()` and `end()` methods, respectively. However, they are also applicable to regular arrays. \n\n容器迭代器确切地知道如何使用容器元素。例如，推进向量迭代器会将其移动到数组的下一个槽，而推进列表迭代器会使用相应的指针将其移动到下一个节点，如以下代码所示:\n\n```cpp\nstd::vector<int> vec;\nvec.push_back(4);\nvec.push_back(2);\nstd::vector<int>::iterator it = vec.begin();\nstd::cout << *it; // 4\nit++ ;\nstd::cout << *it; // 2\n\nstd::list<int> lst;\nlst.push_back(4);\nlst.push_back(2);\nstd::list<int>::iterator lit = lst.begin();\nstd::cout << *lit; // 4\nlit++ ;\nstd::cout << *lit; // 2\n```\n\n每个容器都有自己的迭代器实现；这就是为什么列表迭代器和向量迭代器有相同的接口，但行为不同。迭代器的行为由其*类别*定义。例如，向量的迭代器是随机访问迭代器，这意味着我们可以使用迭代器随机访问任何元素。下面的代码通过向向量的迭代器添加`3`来访问向量的第四个元素，如下所示:\n\n```cpp\nauto it = vec.begin();\nstd::cout << *(it + 3);\n```\n\nSTL 中有六个迭代器类别:\n\n*   投入\n*   输出(与输入相同，但支持写访问)\n*   向前\n*   双向的\n*   随机存取\n*   接触的\n\n**i** **nput 迭代器**提供读访问(通过调用`*`运算符)，并允许使用前缀和后缀增量运算符转发迭代器位置。一个输入迭代器不支持多次传递，也就是说，我们只能使用一个迭代器对容器进行一次迭代。另一方面，**正向迭代器**支持多遍。多遍支持意味着我们可以多次通过迭代器读取元素的值。\n\n**输出迭代器**不提供对元素的访问，但是它允许给元素赋值。具有多遍特性的输入迭代器和输出迭代器的组合构成了前向迭代器。然而，正向迭代器只支持增量操作，而**双向迭代器**支持将迭代器移动到任何位置。它们支持递减操作。例如，`std::list`支持双向迭代器。\n\n最后，**随机访问迭代器**允许*通过在迭代器中增加/减少一个数字来跳过*元素。迭代器将跳转到算术运算指定的位置。`std::vector`提供随机访问迭代器。\n\n每个类别都定义了可以应用于迭代器的操作集。例如，输入迭代器可以用来读取元素的值，并通过递增迭代器前进到下一个元素。另一方面，随机访问迭代器允许用任意值递增和递减迭代器，读取和写入元素的值，等等。\n\n本节到目前为止描述的所有特性的组合属于**连续迭代器**类别，它也期望容器是连续的。这意味着容器元素保证紧挨着另一个。连续容器的一个例子是`std::array`。\n\n像`distance()`这样的函数使用迭代器的信息来实现最快的执行结果。例如，两个双向迭代器之间的`distance()`函数需要线性执行时间，而随机访问迭代器的相同函数在恒定时间内运行。\n\n下面的伪代码演示了一个示例实现:\n\n```cpp\ntemplate <typename Iter>\nstd::size_type distance(Iter first, Iter second) {\n  if (Iter is a random_access_iterator) {\n    return second - first; \n  }\n  std::size_type count = 0;\n  for ( ; first != last; ++ count, first++) {}\n  return count;\n}\n```\n\n虽然前面例子中显示的伪代码运行良好，但是我们应该考虑在运行时检查迭代器的类别不是一个选项。它是在编译时定义的，所以我们需要使用模板专门化来为随机访问迭代器生成`distance()`函数。一个更好的解决方案是使用`<type_traits>`中定义的`std::is_same`类型特征:\n\n```cpp\n#include <iterator>\n#include <type_traits>\n\ntemplate <typename Iter>\ntypename std::iterator_traits<Iter>::difference_type distance(Iter first, Iter last)\n{\n  using category = std::iterator_traits<Iter>::iterator_category;\n  if constexpr (std::is_same_v<category, std::random_access_iterator_tag>) {\n    return last - first;\n  }\n  typename std::iterator_traits<Iter>::difference_type count;\n  for (; first != last; ++ count, first++) {}\n  return count;\n}\n```\n\n`std::is_same_v`是`std::is_same`的辅助模板，定义如下:\n\n```cpp\ntemplate <class T, class U>\ninline constexpr bool is_same_v = is_same<T, U>::value;\n```\n\n迭代器最重要的特性是在容器和算法之间提供松散的耦合:\n\n![](img/2d7a6c25-7b1f-4a4d-a3c1-80259c833393.png)\n\nSTL 基于这三个概念:容器、算法和迭代器。虽然向量、列表或任何其他容器是不同的，但它们有相同的目的:存储数据。\n\n另一方面，算法是处理数据的函数；大部分时间他们都在收集数据。算法定义通常表示指定处理容器元素应该采取的步骤的一般方式。例如，排序算法按升序或降序对容器元素进行排序。\n\n向量是连续的容器，而列表是基于节点的容器。对它们进行分类需要更深入地了解特定容器的物理结构。为了正确地对一个向量进行排序，应该为它实现一个单独的排序函数。同样的逻辑也适用于列表。\n\n迭代器将实现的多样性提升到了一个通用的水平。它们为库设计者提供了实现一个排序函数的能力，该函数将只处理迭代器，从容器类型中抽象出来。在 STL 中，`sort()`算法(在`<algorithm>`中定义)处理迭代器，我们可以用相同的函数对向量和列表进行排序:\n\n```cpp\n#include <algorithm>\n#include <vector>\n#include <list>\n...\nstd::vector<int> vec;\n// insert elements into the vector\nstd::list<int> lst;\n// insert elements into the list\n\nstd::sort(vec.begin(), vec.end());\nstd::sort(lst.begin(), lst.end());\n```\n\n本节中描述的迭代器现在被认为是遗留特性。C++ 20 引入了一个基于**概念**的新迭代器系统。\n\n# 概念和迭代器\n\nC++ 20 引入了**概念**作为其主要特性之一。除了概念，C++ 20 还有基于概念的新迭代器。尽管本章中讨论的迭代器现在被认为是遗留特性，但是已经使用它们编写了许多代码行。这就是为什么我们在继续新的迭代器概念之前首先介绍了它们。现在，让我们找出什么是概念以及如何使用它们。\n\n# 理解概念\n\n抽象在计算机编程中是必不可少的。我们在[第 3 章](03.html)、*面向对象编程的细节、*中引入了类，作为将数据和操作表示为抽象实体的一种方式。之后，在[第 4 章](04.html)、*理解和设计模板*中，我们深入到模板中，看到了如何通过为各种聚合类型重用它们来使类更加灵活。模板不仅提供了特定类型的抽象，还包含了实体和聚合类型之间的松散耦合。以`std::vector`为例。它提供了一个通用接口来存储和操作对象集合。我们可以很容易地声明三个不同的向量，它们将包含三种不同类型的对象，如下所示:\n\n```cpp\nstd::vector<int> ivec;\nstd::vector<Person> persons;\nstd::vector<std::vector<double>> float_matrix;\n```\n\n如果不是模板，我们将不得不对前面的代码进行如下操作:\n\n```cpp\nstd::int_vector ivec;\nstd::custom_vector persons; // supposing the custom_vector stores void* \nstd::double_vector_vector float_matrix;\n```\n\n虽然前面的代码是不可接受的，但是我们应该同意模板是泛型编程的基础这一事实。概念给泛型编程带来了更大的灵活性。现在可以对模板参数设置限制，检查约束，并在编译时发现不一致的行为。模板类声明具有以下形式:\n\n```cpp\ntemplate <typename T>\nclass Wallet\n{\n  // the body of the class using the T type\n};\n```\n\n注意前面代码块中的`typename`关键字。概念更进一步:它们允许用描述模板参数的类型描述来替换它。假设我们想让`Wallet`处理可以添加在一起的类型，也就是说，它们应该是*可添加的*。下面是如何使用一个概念来帮助我们在代码中实现这一点:\n\n```cpp\ntemplate <addable T>\nclass Wallet\n{\n  // the body of the class using addable T's\n};\n```\n\n因此，现在我们可以通过提供可添加的类型来创建`Wallet`实例。只要类型不满足约束，编译器就会抛出一个错误。看起来有点超自然。下面的代码片段声明了两个`Wallet`对象:\n\n```cpp\nclass Book \n{\n  // doesn't have an operator+\n  // the body is omitted for brevity\n};\n\nconstexpr bool operator+(const Money& a, const Money& b) { \n  return Money{a.value_ + b.value_}; \n}\n\nclass Money\n{\n  friend constexpr bool operator+(const Money&, const Money&);\n  // code omitted for brevity\nprivate:\n  double value_;\n};\n\nWallet<Money> w; // works fine\nWallet<Book> g; // compile error\n```\n\n`Book`类没有`+`操作符，因此`g`的构建会因为`template`参数类型限制而失败。\n\n概念的声明使用`concept`关键字完成，其形式如下:\n\n```cpp\ntemplate <*parameter-list*>\nconcept *name-of-the-concept* = *constraint-expression*;\n```\n\n如您所见，概念也是使用模板声明的。我们可以称它们为描述其他类型的类型。概念严重依赖**约束**。约束是指定模板参数要求的一种方式，如下所示，概念是一组约束。下面是我们如何实现前面的可添加概念:\n\n```cpp\ntemplate <typename T>\nconcept addable = requires (T obj) { obj + obj; }\n```\n\n标准概念在`<concepts>`标题中定义。\n\n我们也可以通过要求新概念支持其他概念来将几个概念组合成一个概念。为此，我们使用`&&`运算符。让我们看看迭代器如何利用概念，并带来一个结合其他概念的`incrementable`迭代器概念的例子。\n\n# 在 C++ 20 中使用迭代器\n\n在介绍完概念之后，很明显迭代器是最先充分利用它们的。迭代器及其类别现在被认为是遗留的，因为从 C++ 20 开始，我们使用迭代器概念，如 **`readable`** (它指定通过应用`*`运算符可以读取类型)和`writable`(它指定一个值可以被写入迭代器引用的对象)。如承诺的那样，让我们看看`<iterator>`标题中如何定义`incrementable`:\n\n```cpp\ntemplate <typename T>\nconcept incrementable = std::regular<T> && std::weakly_incrementable<T>\n            && requires (T t) { {t++} -> std::same_as<T>; };\n```\n\n所以`incrementable`概念要求类型为`std::regular`。这意味着它应该是默认可构造的，并且有一个复制构造器和`operator==()`。除此之外，`incrementable`概念要求类型为`weakly_incrementable`，这意味着该类型支持前后增量运算符，除了该类型不要求相等可比。这就是为什么`incrementable`加入`std::regular`要求类型是平等可比的。最后，加法`requires`约束指出了类型在增量后不应该改变的事实，也就是说，它应该与之前的类型相同。虽然`std::same_as`被表示为一个概念(在`<concepts>`中定义)，但在之前的版本中，我们习惯使用`<type_traits>`中定义的`std::is_same`。他们基本上做了同样的事情，但是 C++ 17 版本–`std::is_same_v`–冗长，带有额外的后缀。\n\n所以，我们现在不是指迭代器类别，而是指迭代器概念。除了我们前面介绍的概念，还应该考虑以下概念:\n\n*   `input_iterator`指定该类型允许读取其引用值，并且在**之前和之后都是可递增的**。\n*   `output_iterator`指定该类型的值可以被写入，并且该类型在**之前和之后都是可递增的**。\n*   `input_or_output_iterator`，不必要的长名字放在一边，指定类型是**可增量**，可以取消引用。\n*   `forward_iterator`指定类型为`input_iterator`，额外支持相等比较和多遍。\n\n*   `bidirectional_iterator`指定类型支持`forward_iterator`并额外支持向后移动。\n*   `random_access_iterator`指定类型为`bidirectional_iterator`，支持恒定时间推进和订阅。\n*   `contiguous_iterator`指定类型为`random_access_iterator`，指内存中连续的元素。\n\n它们几乎重复了我们之前讨论过的遗留迭代器，但是现在它们可以在声明模板参数时使用，这样编译器就可以处理剩下的部分。\n\n# 掌握算法\n\n如前所述，算法是接受一些输入、处理输入并返回输出的函数。通常，STL 环境中的算法意味着一个处理数据集合的函数。数据集合以容器的形式呈现，如`std::vector`、`std::list`等。\n\n选择一个有效的算法是程序员的日常工作。例如，使用二分搜索法算法搜索排序向量将比使用顺序搜索更有效。为了比较算法的效率，执行所谓的**渐近分析**，其考虑了算法相对于输入数据大小的速度。这意味着我们实际上不应该通过将两个算法应用于包含十个或一百个元素的容器来比较它们。\n\n当应用到足够大的*容器时，算法的实际差异就显现出来了，有一百万甚至十亿条记录。衡量算法的效率也称为验证其复杂性。你可能遇到过 *O(n)* 算法或者 *O(log N)* 算法。 *O()* 函数(发音为 *big-oh* )定义了算法的复杂度。*\n\n让我们看看搜索算法，并比较它们的复杂性。\n\n# 搜索\n\n在容器中搜索元素是一项常见的任务。让我们实现向量中元素的顺序搜索:\n\n```cpp\ntemplate <typename T>\nint search(const std::vector<T>& vec, const T& item)\n{\n  for (int ix = 0; ix < vec.size(); ++ ix) {\n    if (vec[ix] == item) {\n      return ix;\n    }\n  }\n  return -1; // not found\n}\n```\n\n这是一个简单的算法，它遍历一个向量并返回索引，在该索引处元素等于作为搜索关键字传递的值。我们称之为顺序搜索，因为它顺序扫描矢量元素。它的复杂度是线性的: *O(n)* 。为了衡量它，我们应该以某种方式定义算法找到结果所需的操作次数。假设向量包含 *n* 元素，下面的代码在搜索函数的每一行都包含一个关于其操作的注释:\n\n```cpp\ntemplate <typename T>\nint search(const std::vector<T>& vec, const T& item)\n{\n  for (int ix = 0;           // 1 copy\n       ix < vec.size;        // n + 1 comparisons \n       ++ ix)                 // n + 1 increments\n  {  \n    if (vec[ix] == item) {   // n comparisons\n      return ix;             // 1 copy\n    }\n  }\n  return -1;                 // 1 copy\n}\n```\n\n我们有三个复制操作， *n + 1* 和 *n* (即 *2n + 1* )比较， *n + 1* 增量操作。如果想要的元素在向量的第一个位置呢？在这种情况下，我们只扫描向量的第一个元素，然后从函数返回。\n\n然而，这并不意味着我们的算法如此高效，只需一步就能完成任务。为了衡量算法的复杂性，我们应该考虑最坏的情况:期望的元素要么不存在于向量中，要么位于向量的最后一个位置。下图显示了我们将要找到的元素的三个场景:\n\n![](img/89c34b2d-9597-4ea4-ac1a-a32ef5031eb7.png)\n\n我们应该只考虑最坏的情况，因为它也涵盖了所有其他情况。如果我们把算法的复杂性定义为最坏的情况，我们可以肯定它不会比最坏的情况慢。\n\n为了找出算法的复杂性，我们应该找到运算数量和输入大小之间的联系。在这种情况下，输入的大小就是容器的长度。让我们将副本表示为 A，将比较表示为 C，将增量运算表示为 I，这样我们就有了 3A + (2n + 1)C + (n + 1)I 运算。算法的复杂性定义如下:\n\n*O(3A + (2n + 1)C + (n + 1)I)*\n\n这可以通过以下方式简化:\n\n*   *O(3A + (2n + 1)C + (n + 1)I) =*\n*   *o(3a+2a NC+2c+ni+I)=*\n*   *O(n(2C + I) + (3A + C + I)) =*\n*   *O(n(2C + I))*\n\n最后， *O()的*属性允许我们去掉常数系数和较小的成员，因为实际的算法复杂度只和输入的大小有关，也就是 *n* ，我们得到的最终复杂度等于 *O(n)* 。换句话说，顺序搜索算法具有线性时间复杂度。\n\n如前所述，STL 的本质是通过迭代器连接容器和算法。这就是为什么顺序搜索实现不被认为是 STL 兼容的:因为它对输入参数有严格的限制。为了使它通用，我们应该考虑只使用迭代器来实现它。为了覆盖广泛的容器类型，使用前向迭代器。下面的代码在类型`Iter`上使用运算符，假设它是一个前向迭代器:\n\n```cpp\ntemplate <typename Iter, typename T>\nint search(Iter first, Iter last, const T& elem)\n{\n  for (std::size_t count = 0; first != last; first++, ++ count) {\n    if (*first == elem) return count;\n  }\n  return -1;\n}\n...\nstd::vector<int> vec{4, 5, 6, 7, 8};\nstd::list<double> lst{1.1, 2.2, 3.3, 4.4};\n\nstd::cout << search(vec.begin(), vec.end(), 5);\nstd::cout << search(lst.begin(), lst.end(), 5.5);\n```\n\n实际上，任何类型的迭代器都可以传递给`search()`函数。我们通过对迭代器本身应用操作来确保使用正向迭代器。我们只使用增量(向前移动)、读取(T1)操作符和严格比较(`==`和`!=`，这些都是向前迭代器所支持的。\n\n# 二进位检索\n\n另一方面是二分搜索法算法，它很容易解释。首先，它寻找向量的中间元素，并将搜索关键字与它进行比较，如果它相等，那么算法就完成了:它返回索引。否则，如果搜索关键字小于中间元素，它将前进到向量的左侧。如果搜索关键字大于中间元素，算法将前进到右侧子向量。\n\n为了让二分搜索法正确地处理一个向量，它应该被排序。二分搜索法的本质意味着将搜索关键字与向量元素进行比较，并前进到左侧或右侧子向量，与向量的中间元素相比，每个子向量包含更小或更大的元素。请看下图，它描述了二分搜索法算法的实际应用:\n\n![](img/c478e0fd-ae7e-4b99-8bec-7c288cd13272.png)\n\n二分搜索法算法有一个优雅的递归实现(尽管最好使用迭代实现)——看看下面的代码:\n\n```cpp\ntemplate <typename T>\nstd::size_t binsearch(const std::vector<T>& vec, const T& item, int start, int end)\n{\n  if (start > end) return -1;\n  int mid = start + (end - start) / 2;\n  if (vec[mid] == item) {\n    return mid; // found\n  }\n  if (vec[mid] > item) {\n    return binsearch(vec, item, start, mid - 1);\n  }\n  return binsearch(vec, item, mid + 1, end);\n}\n```\n\n注意中间元素计算。代替`(start + end) / 2;`，我们使用`start + (end - start) / 2;`技术只是为了避免二分搜索法实现中著名的 bug(假设我们没有留下其他 bug)。重点是，对于开始和结束的大值，它们的和(*开始+结束*)会产生整数溢出，这会使程序在某个时刻崩溃。\n\n现在让我们来看看二分搜索法的复杂性。很明显，在执行的每一步，源数组都会减半，这样我们就可以在下一步处理更小或更大的一半。这意味着最坏的情况是我们分割向量，直到只剩下一个元素或者没有元素。为了找到算法中的步数，我们应该找到关于向量大小的除法数。如果向量有 10 个元素，那么我们把它除，得到一个五个元素的子向量；通过再次划分它，我们得到了双元素子向量，最后，再次划分它将把我们带到单个元素。所以，对于 10 元向量，除法的个数是 3。对于 *n* 元素向量，除法的个数是 *log(n)* ，因为在每一步上， *n* 变成 *n/2* ，然后变成 *n/4* 等等。二分搜索法的复杂度是 *O(logn)* (也就是对数)。\n\nSTL 算法在`<algorithm>`头文件中定义；二分搜索法协定的执行所在。如果元素存在于容器中，STL 实现返回真。看看它的原型:\n\n```cpp\ntemplate <typename Iter, typename T>\nbool binary_search(Iter start, Iter end, const T& elem);\n```\n\nSTL 算法不直接使用容器，而是使用迭代器。这允许我们从特定的容器中进行抽象，并使用`binary_search()`让所有的容器支持一个前向迭代器。以下示例为向量和列表调用`binary_search()`函数:\n\n```cpp\n#include <vector>\n#include <list>\n#include <algorithm>\n...\nstd::vector<int> vec{1, 2, 3, 4, 5};\nstd::list<int> lst{1, 2, 3, 4};\nbinary_search(vec.begin(), vec.end(), 8);\nbinary_search(lst.begin(), lst.end(), 3);\n```\n\n`binary_search()`检查迭代器的类别，在随机访问迭代器的情况下，它使用二分搜索法算法的全部能力(否则，它退回到顺序搜索)。\n\n# 整理\n\n二分搜索法算法仅适用于已分拣的集装箱。对于计算机程序员来说，排序是一项众所周知的老任务，他们现在很少自己编写排序算法的实现。你可能已经使用`std::sort()`很多次了，甚至不关心它的实现。基本上，排序算法将一个集合作为输入，并返回一个新的排序集合(按照算法用户定义的顺序)。\n\n在众多排序算法中，最流行(甚至是最快)的是**快速排序**。任何排序算法的基本思想都是找到更小(或更大)的元素，并用更大(或更小)的元素交换它们，直到整个集合被排序。例如，选择排序在逻辑上将集合分为两部分，排序的和未排序的，其中排序的子数组最初是空的，如下所示:\n\n![](img/303865f2-ae26-44a9-bce2-0f8cefb9cc6f.png)\n\n该算法开始在未排序的子阵列中寻找最小的元素，并通过与未排序的子阵列的第一个元素进行交换将其放入排序的子阵列中。在每一步之后，排序的子阵列的长度增加一，而未排序的子阵列的长度减少，如下所示:\n\n![](img/995dc143-d05a-4a3d-b808-de3d058583c5.png)\n\n该过程一直持续到未排序的子阵列变空。\n\nSTL 提供`std::sort()`函数，使用两个随机访问迭代器:\n\n```cpp\n#include <vector>\n#include <algorithm>\n...\nstd::vector<int> vec{4, 7, -1, 2, 0, 5};\nstd::sort(vec.begin(), vec.end());\n// -1, 0, 2, 4, 5, 7\n```\n\n排序函数不能应用于`std::list`，因为它不支持随机访问迭代器。相反，您应该调用列表的`sort()`成员函数。尽管这与 STL 具有通用函数的想法相矛盾，但这样做是为了提高效率。\n\n`sort()`函数有第三个参数:比较函数，可以用来比较容器元素。假设我们在矢量中存储`Product`对象:\n\n```cpp\nstruct Product\n{\n  int price;\n  bool available;\n  std::string title;\n};\n\nstd::vector<Product> products;\nproducts.push_back({5, false, \"Product 1\"});\nproducts.push_back({12, true, \"Product 2\"});\n```\n\n要对容器进行正确排序，其元素必须支持小于运算符或`<`。我们应该为自定义类型定义相应的运算符。但是，如果我们为自定义类型创建一个单独的比较器函数，我们可以省略运算符定义，如下块所示:\n\n```cpp\nclass ProductComparator\n{\npublic:\n bool operator()(const Product& a, const Product& b) {\n return a.price > b.price;\n }\n};\n```\n\n将`ProductComparator`传递给`std::sort()`函数允许它比较矢量元素，而无需深入其元素类型的细节，如下所示:\n\n```cpp\nstd::sort(products.begin(), products.end(), ProductComparator{});\n```\n\n虽然这是一种很好的技术，但是使用 lambda 函数会更好，lambda 函数是匿名函数，非常适合前面的场景。我们可以这样覆盖它:\n\n```cpp\nstd::sort(products.begin(), products.end(), \n  [](const Product& a, const Product& b) { return a.price > b.price; })\n```\n\n前面的代码允许省略`ProductComparator`的声明。\n\n# 探索树和图形\n\n二分搜索法算法和排序算法结合在一起，产生了一个容器的想法，默认情况下，该容器保持项目排序。一个这样的容器是`std::set`，基于平衡树。在讨论平衡树本身之前，让我们先来看看二叉查找树，它是快速查找的完美选择。\n\n二叉查找树的思想是节点左边子树的值小于节点的值。相比之下，节点右侧子树的值大于节点的值。这里有一个二叉查找树的例子:\n\n![](img/9c361b58-ff03-4ab1-bffc-dd29595f2378.png)\n\n从上图中可以看到，值为 15 的元素位于左侧子树中，因为它小于 30(根元素)。另一方面，值为 60 的元素位于右边的子树中，因为它大于根元素。同样的逻辑也适用于其余的树元素。\n\n二叉树节点表示为包含该项和指向每个子节点的两个指针的结构。下面是树节点的示例代码表示:\n\n```cpp\ntemplate <typename T>\nstruct tree_node\n{\n  T item;\n  tree_node<T>* left;\n  tree_node<T>* right;\n};\n```\n\n在完全平衡的二叉查找树中，搜索、插入或移除一个元素需要 *O(logn)* 。STL 没有为树提供单独的容器，但是它有基于树实现的类似容器。例如，`std::set`容器基于一个平衡树，它以排序顺序唯一地存储元素:\n\n```cpp\n#include <set>\n...\nstd::set<int> s{1, 5, 2, 4, 4, 4, 3};\n// s has {1, 2, 3, 4, 5}\n```\n\n`std::map`也是基于平衡树的，但是这个提供了一个容器，将一个键映射到某个值，例如:\n\n```cpp\n#include <map>\n...\nstd::map<int, std::string> numbers;\nnumbers[3] = \"three\";\nnumbers[4] = \"four\";\n...\n```\n\n如前面的代码所示，函数`map` `numbers`将整数映射为字符串。因此，当我们告诉地图将`3`的值存储为键，将字符串`three`存储为值时，它会在其内部树中添加一个新节点，键等于`3`，值等于`three`。\n\n`set`和`map`运算是对数运算，这使得它在大多数情况下都是非常高效的数据结构。然而，接下来是更高效的数据结构。\n\n# 散列表\n\n哈希表是目前最快的数据结构。它基于向量索引的简单思想。想象一个包含列表指针的大向量:\n\n```cpp\nstd::vector<std::list<T> > hash_table;\n```\n\n访问向量元素需要恒定的时间。这是病媒的主要超级力量。哈希表允许我们使用任何类型作为容器的键。哈希表的基本思想是使用精心设计的哈希函数，为输入键生成唯一的索引。例如，当我们使用字符串作为哈希表键时，哈希表使用哈希函数生成哈希作为基础向量的索引值:\n\n```cpp\ntemplate <typename T>\nint hash(const T& key)\n{\n  // generate and return and efficient\n  // hash value from key based on the key's type\n}\n\ntemplate <typename T, typename U>\nvoid insert_into_hashtable(const T& key, const U& value)\n{\n  int index = hash(key);\n  hash_table[index].push_back(value); // insert into the list\n}\n```\n\n下面是我们如何说明哈希表:\n\n![](img/5623724b-8217-4b70-8fac-b52b713d8435.png)\n\n访问哈希表需要恒定的时间，因为它是基于向量进行操作的。虽然可能有不同的关键字会导致相同的哈希值，但是通过使用一个值列表作为向量元素(如上图所示)，这些冲突得到了解决。\n\nSTL 支持名为`std::unordered_map`的哈希表:\n\n```cpp\n#include <unordered_map>\n...\nstd::unordered_map<std::string, std::string> hashtable;\nhashtable[\"key1\"] = \"value 1\";\nhashtable[\"key2\"] = \"value 2\";\n...\n```\n\n要为提供的密钥生成哈希值，函数`std::unordered_map`使用在`<functional>`头中定义的`std::hash()`函数。您可以为哈希函数指定自定义实现。`std::unordered_map`的第三个`template`参数是哈希函数，默认为`std::hash`。\n\n# 图形\n\n二叉查找树的平衡本质是基于许多搜索索引的实现。例如，数据库系统使用称为 B 树的平衡树进行表索引。B 树不是*二进制*树，但它遵循相同的平衡逻辑，如下图所示:\n\n![](img/92b32da1-8667-447b-b087-481c79ac0dc4.png)\n\n另一方面，图表示没有适当顺序的连接节点:\n\n![](img/84949559-6f1a-41a6-8b34-746c60392218.png)\n\n假设我们正在建立一个社交网络，最终将脸书挤出市场。社交网络中的用户可以相互跟随，这可以表示为图形。例如，如果 A 跟在 B 后面，B 跟在 C 后面，而 C 既跟在 B 后面又同时跟在 A 后面，那么我们可以将这些关系表示为下图:\n\n![](img/afd3b4ab-05ed-448a-8612-d596cce84d88.png)\n\n一个节点在图中称为**顶点**。两个节点之间的连接称为一条**边**。实际上没有固定的图形表示，所以我们应该从几个中选择。让我们想想我们的社交网络——我们将如何表示用户 A 跟随用户 B 的信息？\n\n这里最好的选择之一是使用哈希表。我们可以将每个用户映射到他们关注的所有用户:\n\n![](img/ff9a742f-9d83-4b1d-b655-77d3f57fe938.png)\n\n图形实现变成了一个混合容器:\n\n```cpp\n#include <list>\n#include <unordered_map>\n\ntemplate <typename T>\nclass Graph\n{\npublic: \n  Graph();\n  ~Graph();\n  // copy, move constructors and assignment operators omitted for brevity\n\npublic:\n  void insert_edge(const T& source, const T& target);\n  void remove_edge(const T& source, const T& target);\n\n  bool connected(const T& source, const T& target);\n\nprivate:\n  std::unordered_map<T, std::list<T> > hashtable_;\n};\n```\n\n为了制作一个 STL 兼容的容器，让我们为这个图添加一个迭代器。虽然迭代一个图不是一个好主意，但是添加一个迭代器也不是一个坏主意。\n\n# 用线串\n\n字符串类似于向量:它们存储字符，它们公开迭代器，并且它们是容器。然而，它们有些不同，因为它们专门表达一种数据:字符串。下图将字符串 **hello，C++** 描述为以特殊的 **\\0** 字符结尾的字符数组:\n\n![](img/c99154de-7f04-4bfe-8ae5-b8ba7ed23ecb.png)\n\n特殊的 **\\0** 字符(也称为空字符)用作字符串终止符。编译器一个接一个地读取字符，直到遇到空字符。\n\n字符串的实现方式与我们在本章开头实现向量的方式相同:\n\n```cpp\nclass my_string\n{\npublic:\n my_string();\n // code omitted for brevity\n\npublic:\n void insert(char ch);\n // code omitted for brevity\n\nprivate:\n char* buffer_;\n int size_;\n int capacity_;\n};\n```\n\nC++ 有它强大的`std::string`类，它提供了一堆可以使用的函数。除了`std::string`成员函数外，`<algorithm>`中定义的算法也适用于字符串。\n\n# 摘要\n\n数据结构和算法是开发高效软件的关键。通过理解和利用本章中讨论的数据结构，您将拥有 C++ 20 的全部能力来使您的程序运行得更快。市场上更需要一个解决问题能力强的程序员，这不是什么秘密。解决问题的技巧首先是通过深入理解基本算法和数据结构获得的。正如您在本章中已经看到的，在搜索任务中利用二分搜索法算法使代码的运行速度比顺序算法快得多。高效的软件节省时间并提供更好的用户体验，这最终使您的软件成为现有软件的杰出替代品。\n\n在本章中，我们讨论了基本数据结构及其差异。我们学会了根据问题分析来使用它们。例如，由于链表元素访问操作的复杂性，在需要随机查找的问题中应用链表被认为是耗时的。在这种情况下，使用一个动态增长的向量更合适，因为它的常量时间元素访问。相反，在需要在容器前面快速插入的问题中使用一个向量比，例如，列表更昂贵。\n\n本章还介绍了算法和衡量效率的方法。我们比较了几个问题，以应用更好的算法来更有效地解决它们。\n\n在下一章中，我们将讨论 C++ 中的函数式编程。学习了 STL 的基本知识后，我们现在将在容器上应用函数式编程技术。\n\n# 问题\n\n1.  描述将元素插入到动态增长的向量中。\n2.  在链表前面插入元素和在向量前面插入元素有什么区别？\n3.  实现一个混合数据结构，将它的元素存储在一个向量和一个列表中。对于每个操作，选择操作实现最快的底层数据结构。\n4.  如果我们以递增的顺序插入 100 个元素，二叉查找树会是什么样子？\n5.  选择排序和插入排序算法有什么区别？\n6.  实现本章中描述的排序算法，称为计数排序。\n\n# 进一步阅读\n\n有关更多信息，请参考以下资源:\n\n*   *编程珍珠*作者乔恩·本特利，可从[https://www . Amazon . com/Programming-Pearls-2nd-乔恩-本特利/dp/0201657880/](https://www.amazon.com/Programming-Pearls-2nd-Jon-Bentley/dp/0201657880/) 获得\n*   *使用 C++ 的数据抽象和问题解决:墙壁和镜子*作者:Frank Carrano 和 Timothy Henry，可从[获得](https://www.amazon.com/Data-Abstraction-Problem-Solving-Mirrors/dp/0134463978/)\n*   *算法介绍*作者:科曼、莱瑟森、瑞文斯特和斯坦，可查阅[https://www . Amazon . com/Introduction-Algorithms-3rd-MIT-Press/DP/0262033844/](https://www.amazon.com/Introduction-Algorithms-3rd-MIT-Press/dp/0262033844/)\n*   *Wisnu Anggoro 编写的 C++ 数据结构和算法*，可从[https://www . packtpub . com/application-development/C-数据结构和算法](https://www.packtpub.com/application-development/c-data-structures-and-algorithms)获得"
  },
  {
    "path": "docs/exp-cpp/07.md",
    "content": "# 七、函数式编程\n\n**面向对象编程** ( **OOP** )为我们提供了一种思考对象的方式，从而用类及其关系来表达现实世界。函数式编程是一种完全不同的编程范式，因为它允许我们专注于*函数式*结构，而不是代码的*物理*结构。学习和使用函数式编程有两种用处。首先，这是一种新的范式，迫使你以非常不同的方式思考。解决问题需要有灵活的思维。依附于单一范式的人倾向于为任何问题提供相似的解决方案，而大多数优雅的解决方案需要更广泛的方法。掌握函数式编程为开发人员提供了一项新技能，帮助他们提供更好的问题解决方案。其次，使用函数式编程减少了软件中的 bug 数量。函数式编程独特方法的最大原因之一是:它将程序分解成函数，每个函数都不修改数据的状态。\n\n我们将在本章讨论函数式编程的基本模块，以及范围。在 C++ 20 中引入的范围为我们提供了一种编写算法的好方法，这样它们就可以处理数据集合。函数式编程的核心是编写算法，这样我们就可以按顺序将它们应用到这个数据集合中。这就是为什么我们将在本章中讨论范围。\n\n本章将涵盖以下主题:\n\n*   函数式编程导论\n*   范围库简介\n*   纯函数\n*   高阶函数\n*   更深入地研究递归\n*   函数式 C++ 中的元编程\n\n# 技术要求\n\ng++ 编译器连同`-std=c++ 2a`选项将用于编译本章中的示例。\n\n你可以在[https://github.com/PacktPublishing/Expert-CPP](https://github.com/PacktPublishing/Expert-CPP)找到本章的源文件。\n\n# 揭示函数式编程\n\n正如我们前面提到的，函数式编程是一种编程范式。在构建程序时，您可以将范例视为一种思维方式。C++ 是一种多范式语言。我们可以用它在过程范式中开发程序，也就是通过一个接一个地执行语句。在第三章，面向对象编程的细节中，我们讨论了面向对象的方法，包括将一个复杂的系统分解成相互通信的对象。另一方面，函数式编程鼓励我们将系统分解成函数而不是对象。它使用表达式而不是语句进行操作。基本上，您将某个东西作为输入，并将其传递给产生输出的函数。这可以用作另一个函数的输入。这在一开始看起来很简单，但是函数式编程包含了一些规则和实践，这些规则和实践在开始时感觉很难掌握。然而，当你做到这一点时，你的大脑会开启一种新的思维方式——功能性思维方式。\n\n为了更清楚地说明这一点，让我们从一个演示函数式编程本质的例子开始。假设我们得到了一个整数列表，需要计算其中偶数的个数。唯一的问题是有几个这样的载体。我们应该分别计算所有向量中的偶数，并产生一个结果，作为包含每个输入向量的计算结果的新向量。\n\n输入以矩阵的形式提供，即向量的向量。用 C++ 表达这一点的最简单方法是使用以下类型:\n\n```cpp\nstd::vector<std::vector<int>>\n```\n\n我们可以通过使用类型别名来进一步简化前面的代码，如下所示:\n\n```cpp\nusing IntMatrix = std::vector<std::vector<int>>;\n```\n\n下面是这个问题的说明。我们有一堆包含整数的向量，因此我们应该得到一个包含偶数计数的向量:\n\n![](img/6afeff6f-80a2-4fdc-a80e-d758cdcac856.png)\n\n请看下面的函数。它以整数向量的向量(也称为矩阵)作为自变量。该函数计算偶数的数量:\n\n```cpp\nstd::vector<int> count_all_evens(const IntMatrix& numbers)\n{\n  std::vector<int> even_numbers_count;\n  for (const auto& number_line: numbers) {\n    int even{0};\n for (const auto& number: number_line) {\n if (number % 2 == 0) {\n ++ even;\n }\n }\n even_numbers_count.push_back(even);\n  }\n  return even_numbers_count;\n}\n```\n\n前面的函数保留了一个单独的向量来存储每个向量的偶数计数。输入作为向量的向量提供，这就是为什么函数在第一个向量上循环以检索内部向量。对于每个检索到的向量，它会在其上循环，并在每次遇到向量中的偶数时递增计数器。完成每个向量的循环后，最终结果被推送到包含数字列表的向量。虽然您可能希望回到前面的例子，并使代码更好，但我们现在将继续，并将其分解为更小的函数。首先，我们将负责计算偶数的代码部分移到一个单独的函数中。\n\n我们把它命名为`count_evens`，如下:\n\n```cpp\nint count_evens(const std::vector<int>& number_line) {\n  return std::count_if(number_line.begin(), \n       number_line.end(), [](int num){return num % 2 == 0;});\n}\n```\n\n注意我们如何应用`count_if()`算法。它采用两个迭代器，分别放在容器的开头和结尾。它还接受第三个参数，*一元谓词*，为集合的每个元素调用该参数。我们传递了一个 lambda 作为一元谓词。您也可以使用任何其他可调用的实体，例如函数指针、`std::`函数等等。\n\n现在我们有了单独的计数函数，可以在原来的`count_all_evens()`函数中调用。`count_all_evens()`的以下实现用 C++ 表示函数式编程:\n\n```cpp\nstd::vector<int> count_all_evens(const std::vector<std::vector<int>>& numbers) {\n  return numbers | std::ranges::views::transform(count_evens);\n}\n```\n\n在深入研究前面的代码之前，让我们先就吸引我们眼球的第一件事达成一致——不是`|`运算符的怪异用法，而是代码的简洁性。将其与我们在本节开头介绍的代码版本进行比较。他们都做同样的工作，但是第二个——功能性的——做得更简洁。此外，请注意，该函数不会保持或更改任何状态。它没有副作用。这在函数编程中至关重要，因为函数必须是纯 T2 函数。它接受一个参数，然后处理它而不修改它，并返回一个新值(通常基于输入)。函数式编程的第一个挑战是将任务分解成更小的独立函数，这些函数很容易组合。\n\n尽管我们是从一个命令式的解决方案中得出函数式解决方案的，但在利用函数式编程范式时，这并不是使用它的正确方式。您应该改变您思考问题的方式和方法，而不是先编写命令式代码并修改它以获得功能性版本。你应该驯服功能性思考的过程。计算所有偶数的问题导致我们解决一个向量的问题。如果我们能找到一种方法来解决单个向量的问题，我们就能解决所有向量的问题。`count_evens()`函数取一个向量，产生一个单值，如下图所示:\n\n![](img/f23fba4f-7441-4785-94ef-d67480148d5e.png)\n\n在解决了一个向量的问题后，我们应该通过将解决方案应用于所有向量来处理原始问题。`std::transform()`函数本质上做了我们需要的事情:它接受一个可以应用于单个值的函数，并对其进行转换，以便处理一个集合。下图说明了我们如何使用它来实现一个函数(`count_all_evens`)，该函数可以处理来自一次只处理一个项目的函数(`count_evens`)的项目集合:\n\n![](img/9fa67593-d788-4281-a47a-f8e9a968a285.png)\n\n将更大的问题分解成更小的独立任务是函数式编程的核心。每一个功能都是专门做一个足够简单的任务，而没有意识到原来的问题。然后将函数组合在一起，从原始的初始输入生成一个转换项的集合。\n\n现在，`count_all_evens()`函数的最终版本利用了范围。让我们找出它们是什么以及如何使用它们，因为我们将在进一步的示例中需要它们。\n\n# 使用范围\n\n范围与视图相关联。我们将在本节中研究它们。我们在[第 6 章](06.html)、*中讨论了 STL 容器和算法，深入研究了 STL* 中的数据结构和算法。它们为我们提供了一种组合和处理对象集合的通用方法。正如您已经知道的，我们经常使用迭代器来遍历容器并处理它们的元素。迭代器是允许我们在算法和容器之间进行松散耦合的工具。\n\n例如，之前，我们将`count_if()`应用于向量，但是`count_if()`不知道它被应用于什么容器。看看下面`count_if()`的宣言:\n\n```cpp\ntemplate <typename InputIterator, typename UnaryPredicate>\nconstexpr typename iterator_traits<InputIterator>::difference_type\n  count_if(InputIterator first, InputIterator last, UnaryPredicate p);\n```\n\n可以看到，`count_if()`除了其特定于 C++ 的冗长声明外，并没有将容器作为参数。相反，它使用迭代器——特别是输入迭代器。\n\nAn input iterator supports iterating forward using the `++ ` operator and accessing each element using the `*` operator. We also can compare input iterators using the `==` and `!=` relationships.\n\n算法在不知道容器确切类型的情况下迭代容器。我们可以在任何有开始和结束的实体上使用`count_if()`，如下所示:\n\n```cpp\n#include <array>\n#include <iostream>\n#include <algorithm>\n\nint main()\n{\n  std::array<int, 4> arr{1, 2, 3, 4};\n auto res = std::count_if(arr.cbegin(), arr.cend(), \n [](int x){ return x == 3; });\n  std::cout << \"There are \" << res << \" number of elements equal to 3\";\n}\n```\n\n除了它们的共性之外，算法的组合也不好。通常，我们将一个算法应用于一个集合，并将该算法的结果存储为另一个集合，我们可以在以后以相同的方式将该集合应用于更多的算法。我们使用`std::transform()`将结果放入另一个容器中。例如，以下代码定义了产品向量:\n\n```cpp\n// consider the Product is already declared and has a \"name\", \"price\", and \"weight\"\n// also consider the get_products() is defined \n// and returns a vector of Product instances\n\nusing ProductList = std::vector<std::shared_ptr<Product>>;\nProductList vec{get_products()};\n```\n\n假设项目由不同的程序员团队开发，他们选择将产品名称保留为任意数字；例如，1 代表苹果，2 代表桃子，以此类推。这意味着`vec`将包含`Product`实例，每个实例在其`name`字段中都有一个数字字符(而名称的类型是`std::string`——这就是为什么我们将数字作为字符而不是整数值保存的原因)。现在，我们的任务是将产品名称从数字转换为完整字符串(`apple`、`peach`等)。对此我们可以使用`std::transform`:\n\n```cpp\nProductList full_named_products; // type alias has been defined above\nusing ProductPtr = std::shared_ptr<Product>;\nstd::transform(vec.cbegin(), vec.cend(), \n  std::back_inserter(full_named_products), \n  [](ProductPtr p){ /* modify the name and return */ });\n```\n\n执行上述代码后，`full_named_products`向量将包含具有完整产品名称的产品。现在，要过滤掉所有的苹果并将其复制到一个苹果向量中，我们需要使用`std::copy_if`:\n\n```cpp\nProductList apples;\nstd::copy_if(full_named_products.cbegin(), full_named_products.cend(),\n  std::back_inserter(apples), \n  [](ProductPtr p){ return p->name() == \"apple\"; });\n```\n\n前面代码示例的最大缺点之一是在引入范围之前缺乏良好的组合。范围为我们提供了一种处理容器元素和组合算法的优雅方式。\n\n简单地说，一个范围是一个可穿越的实体；也就是说，一个范围有一个`begin()`和一个`end()`，很像我们目前使用的容器。在这些术语中，每个 STL 容器都可以被视为一个范围。STL 算法被重新定义为将范围作为直接参数。通过这样做，它们允许我们将一个算法的结果直接传递给另一个算法，而不是将中间结果存储在局部变量中。例如，`std::transform`，我们之前使用了一个`begin()`和一个`end()`，如果应用于一个范围，有以下形式(下面的代码是伪代码)。通过使用范围，我们可以用以下方式重写前面的示例:\n\n```cpp\nProductList apples = filter(\n  transform(vec, [](ProductPtr p){/* normalize the name */}),\n  [](ProductPtr p){return p->name() == \"apple\";}\n);\n```\n\n别忘了导入`<ranges>`表头。转换函数将返回一个包含`Product`指针的范围，这些指针的名称被规范化；也就是说，数值被替换为字符串值。然后，过滤功能将获取结果并返回以`apple`为名称的产品范围。\n\nNote that we simplified these code examples by omitting `std::ranges::views` from in front of the `filter` and `transform` functions. Use them as `std::ranges::views::filter` and `std::ranges::views::transform`, accordingly.\n\n最后，我们在本章开头的示例中使用的重载运算符`**|**`，允许我们一起管理范围。这样，我们可以编写算法来产生最终结果，如下所示:\n\n```cpp\nProductList apples = vec | transform([](ProductPtr p){/* normalize the name */})\n                         | filter([](ProductPtr p){return p->name() == \"apple\";});\n```\n\n我们用管道代替嵌套函数调用。这一开始可能会令人困惑，因为我们曾经使用`|`运算符作为按位“或”运算符。每当您看到它应用于集合时，它都是指管道范围。\n\nThe `|` operator is inspired by the Unix shell pipe operator. In Unix, we can pipe the results of several processes together; for example, `ls -l | grep cpp | less` will find `cpp` in the result of the `ls` command and show the final result one screen at a time using the `less` program.\n\n正如我们已经说过的，范围是对集合的抽象。这并不意味着它是一个集合。这就是为什么前面的例子没有任何开销——它只是将一个范围从一个函数传递到另一个函数，这个范围只是提供了一个集合的开始和结束。此外，它允许我们访问底层的集合元素。下图说明了这个想法:\n\n![](img/fa625af2-3795-4fd4-b089-5a72113aa071.png)\n\n函数(或者**变换**或者**过滤**)返回一个范围结构，而不是一个集合。范围的`begin()`迭代器将指向源集合中满足谓词的元素。范围的迭代器是一个代理对象:它不同于常规的迭代器，因为它指向一个满足给定谓词的元素。我们有时称它们为**智能迭代器**，因为每次我们推进它(例如，通过递增)，它都会在集合中找到满足谓词的下一个元素。更有趣的是，迭代器的“智能”取决于我们应用于集合的函数类型。例如，`filter()`函数返回一个范围，该范围的增量运算符有智能迭代器。这主要是因为筛选器的结果可能包含比原始集合更少的元素。另一方面，Transform 不会返回元素数量减少的结果，它只是转换元素。这意味着由转换返回的范围对于递增/递减操作具有相同的功能，但是元素访问将有所不同。对于每次访问，该范围的智能迭代器将从原始集合中返回转换后的元素。换句话说，它只是为迭代器实现了`*()`操作符，类似于下面的代码片段:\n\n```cpp\nauto operator*()\n{\n  return predicate(*current_position);\n}\n```\n\n这样，我们就创建了一个新的集合*视图*，而不是一个新的转换元素集合。这同样适用于`filter`和其他功能。更有趣的是，范围视图利用了*懒惰评估*。对于前面的例子，即使我们有两个范围转换，结果也是通过一次计算得到的。\n\n在带有`transform`和`filter`的例子中，每个函数都定义了一个视图，但是它们不修改或评估任何东西。当我们将结果分配给结果集合时，向量是通过访问每个元素从视图中构造的。这就是评估发生的地方。\n\n就这么简单——范围为我们提供了带有惰性评估的函数组合。我们简单介绍了函数式编程中使用的工具集。现在，让我们来看看这个范例的好处。\n\n# 为什么使用函数式编程？\n\n首先，函数式编程引入了简洁性。与命令式代码相比，代码要短得多。它提供了简单但极具表现力的工具。代码越少，出现的 bug 就越少。\n\n函数不会发生任何变化，这使得并行化它们变得容易得多。这是并发程序的主要关注点之一，因为并发任务需要在它们之间共享可变数据。大多数情况下，您必须使用原语(如互斥体)显式同步线程。函数式编程将我们从显式同步中解放出来，我们可以在多个线程上运行代码，而无需修改它。在 [第八章](06.html)*挖掘数据结构*中，我们将详细讨论数据竞赛。\n\n功能范式认为所有功能都是*纯*；也就是说，不改变程序状态的函数。他们只是接受输入，以用户定义的方式进行转换，然后提供输出。一个纯函数为相同的输入生成相同的结果，与它被调用的次数无关。每当我们谈论函数式编程时，我们应该默认考虑所有纯函数。\n\n以下函数以`double`为输入，返回其平方:\n\n```cpp\ndouble square(double num) { return num * num; }\n```\n\n单独编写纯函数可能会感觉像是有意让程序运行得更慢。\n\nSome compilers, such as GCC, provide attributes that help the compiler optimize the code. For example, the `[[gnu::pure]]` attribute tells the compiler that the function can be considered a pure function. This will reassure the compiler that the function doesn't access any global variable and that the function's result depends solely on its input. \n\n有许多情况下*常规*功能可以带来更快的解决方案。然而，为了适应范式，你应该强迫自己进行功能性思考。例如，以下程序声明一个向量并计算其元素的平方根:\n\n```cpp\nvoid calc_square_roots(std::vector<double>& vec) \n{\n  for (auto& elem : vec) {\n    elem = std::sqrt(elem);\n  }\n}\n\nint main()\n{\n  std::vector<double> vec{1.1, 2.2, 4.3, 5.6, 2.4};\n calc_square_roots(vec);\n}\n```\n\n这里，我们通过引用传递向量。这意味着，如果我们在函数中更改它，我们就更改了原始集合。这显然不是一个纯函数，因为它改变了输入向量。另一种功能是返回新向量中的转换元素，保持输入不变:\n\n```cpp\nstd::vector<double> pure_calc_square_roots(const std::vector<double>& vec)\n{\n std::vector<double> new_vector;\n  for (const auto& elem : vec) {\n    new_vector.push_back(std::sqrt(elem));\n  }\n return new_vector;\n}\n```\n\n一个更好的功能思维的例子是解决一个更小的问题，并将其应用到集合中。在这种情况下，较小的问题是计算单个数字的平方根，这已经实现为`std::sqrt`。将其应用于集合是通过`std::ranges::views::transform`完成的，如下所示:\n\n```cpp\n#include <ranges>\n#include <vector>\n\nint main()\n{\n std::vector<double> vec{1.1, 2.2, 4.3, 5.6, 2.4};\n auto result = vec | std::ranges::views::transform(std::sqrt);\n}\n```\n\n正如我们已经知道的，通过使用范围，我们可以避免存储中间对象。在前面的例子中，我们将`transform`直接应用于向量。`transform`返回一个视图，但不是由源向量的变换元素组成的完整集合。当我们构建`result`向量时，元素的实际变换副本被制作。另外注意`std::sqrt`被认为是纯函数。\n\n我们在本章开头解决的例子为函数式编程提供了必要的视角。为了更好地掌握这一范式，我们应该熟悉它的原理。在下一节中，我们将深入研究函数式编程的原理，以便您更好地了解如何以及何时使用该范例。\n\n# 函数编程原理\n\n虽然函数范式很古老(它诞生于 20 世纪 50 年代)，但它并没有让编程界遭受风暴。如今，大多数主流范例包括命令式语言和面向对象语言。正如我们在本书和许多其他书中多次指出的，C++ 是一种**多范式语言**。这就是学习 C++ 的妙处；我们可以调整它以适应几乎所有的环境。掌握范式不是一件容易的事情。你必须去感受它并应用它，直到你最终开始从范式的角度去思考。之后，你会在几秒钟内看到常规任务的解决方案。\n\n如果你还记得你第一次学习面向对象编程的时候，你可能会想起那些让你在释放面向对象编程的真正潜力之前有点纠结的原则。函数式编程也是如此。在这一节中，我们将讨论函数式编程的基本概念，这将是进一步发展的基础。您可以应用(或者已经这样做了)这些概念中的一些，而无需实际使用功能范例。然而，试着花些精力去理解和应用以下每一条原则。\n\n# 纯函数\n\n正如我们之前提到的，一个*函数如果不变异状态*就是纯的。与非纯函数相比，纯函数可被视为性能较差；然而，它们很棒，因为它们避免了由于状态修改而在代码中出现的大多数错误。bug 在某种程度上与程序状态有关。显然，程序处理数据，所以它们组成了状态修改功能，为最终用户带来一些预期的结果。\n\n在面向对象编程中，我们将程序分解成对象，每个对象都有一个特殊特性的列表。OOP 中对象的基本特征之一是其*状态*。通过向对象发送消息(换句话说，调用它的方法)来修改对象的状态在 OOP 中至关重要。通常，成员函数调用会导致对象状态的修改。在函数式编程中，我们将代码组织成纯函数的集合，每个函数都有自己的目的，并且独立于其他函数。\n\n让我们看一个简单的例子，只是为了让这个概念清晰。假设我们在一个程序中处理用户对象，每个用户对象包含与用户相关的年龄。`User`类型在以下代码块中描述为`struct`:\n\n```cpp\nstruct User\n{\n  int age;\n  string name;\n  string phone_number;\n  string email;\n};\n```\n\n需要每年更新用户的年龄。假设我们有一个每年为每个`User`对象调用一次的函数。以下函数以一个`User`对象为输入，将其`age`增加`1`:\n\n```cpp\nvoid update_age(User& u)\n{\n  u.age = u.age + 1;\n}\n```\n\n`update_age()`功能通过引用获取输入，并更新原始对象。函数式编程不是这样。除了更新的`age`属性之外，下面的纯函数返回一个完全不同的`user`对象，该对象具有相同的属性，而不是通过引用获取原始对象并改变其值:\n\n```cpp\nUser pure_update_age(const User& u) // cannot modify the input argument\n{\n User tmp{u};\n  tmp.age = tmp.age + 1;\n  return tmp;\n}\n```\n\n虽然与`update_age()`相比，这种方法似乎效率不高，但它的优点之一是使操作非常清晰(这在我们调试代码时非常有用)。现在保证`pure_update_age()`不会修改原对象。我们可以修改前面的代码，使其按值接受对象。这样，我们将跳过创建`tmp`对象，因为参数本身代表一个副本:\n\n```cpp\nUser pure_update_age(User u) // u is the copy of the passed object\n{\n  u.age = u.age + 1;\n  return u;\n}\n```\n\n如果用相同的参数多次调用一个纯函数，那么它每次都必须返回相同的结果。下面的代码演示了当给定相同的输入时，我们的`pure_update_age()`函数返回相同的值:\n\n```cpp\nUser john{.age{21}, .name{\"John\"}};\n\nauto updated{pure_update_age(john)};\nstd::cout << updated.age; // prints 22\n\nupdated = pure_update_age(john);\nstd::cout << updated.age; // prints 22\n```\n\n对于一个函数来说，每次为相同的输入数据调用它时，它的行为都是相同的，这是一个很大的好处。这意味着我们可以通过将应用分解成更小的函数来设计应用的逻辑，每个函数都有一个精确而清晰的目的。然而，就额外的临时对象而言，纯函数存在开销。常规设计包括拥有一个包含程序状态的集中存储，程序状态由纯函数间接更新。每次纯函数调用后，函数都会将修改后的对象作为新对象返回，必要时可以存储。您可以将其视为调整代码以省略传递整个对象。\n\n# 高阶函数\n\n在函数编程中，函数被认为是*一级*对象(你可能也会遇到一级公民)。这意味着我们应该将它们视为对象，而不是一组指令。这对我们有什么不同？在这一点上，一个函数被当作一个对象来对待，唯一重要的是它能够传递给其他函数。将其他函数作为参数的函数称为**高阶** **函数**。\n\nC++ 程序员将一个函数传递给另一个函数并不少见。以下是如何用老派的方式做到这一点:\n\n```cpp\ntypedef  void (*PF)(int);\nvoid foo(int arg) \n{\n  // do something with arg\n}\n\nint bar(int arg, PF f)\n{\n f(arg);\n  return arg;\n}\n\nbar(42, foo);\n```\n\n在前面的代码中，我们声明了一个指向函数的指针。`PF`表示函数的类型定义，取一个整数参数，不返回值。前面的例子是将指针函数作为参数传递给其他函数的一种流行方式。我们把函数当作一个对象。然而，这取决于我们对*对象的理解。*\n\n在前几章中，我们将对象定义为具有状态的东西。这意味着，如果我们把一个函数当作一个对象，如果需要，我们也应该能够以某种方式改变它的状态。对于函数指针，情况并非如此。以下是将一个函数传递给另一个函数的更好方法:\n\n```cpp\nclass Function\n{\npublic:\n  void modify_state(int a) {\n    state_ = a;\n  }\n\n  int get_state() {\n    return state_;\n  }\n\n  void operator()() {\n // do something that a function would do\n }\nprivate:\n  int state_;\n};\n\nvoid foo(Function f)\n{\n f();\n  // some other useful code\n}\n```\n\n好好看看前面的代码。它声明了一个有重载`operator()`的类。每当我们重载一个类的操作符时，我们就使它成为*可调用的*。听起来很明显，任何可调用的东西都被当作一个函数。因此，一个类的对象有一个重载`operator()`可以被认为是一个函数(有时。它被称为*函子*。这在某种程度上就像一个技巧，因为我们没有让函数成为对象，而是让对象成为可调用的。然而，这让我们实现了我们想要的:一个有状态的函数。以下客户端代码演示了一个`Function`对象的状态:\n\n```cpp\nvoid foo(Function f)\n{\n  f();\n  f.modify_state(11);\n cout << f.get_state(); // get the state\n  f(); // call the \"function\"\n}\n```\n\n例如，通过这样做，我们可以跟踪该函数被调用了多少次。下面是一个跟踪呼叫数量的简单示例:\n\n```cpp\nclass Function\n{\npublic:\n void operator()() {    // some useful stuff ++ called_; \n  }\n\nprivate:\n  int called_ = 0;\n};\n```\n\n最后，在以下代码的`<functional>`标题中定义的`std::function`演示了定义高阶函数的另一种方式:\n\n```cpp\n#include <functional>\n\nvoid print_it(int a) {\n  cout << a;\n}\n\nstd::function<void(int)> function_object = print_it;\n```\n\n当`function_object`被调用时(使用`operator()`，它将调用委托给`print_it`函数。`std::function`封装任何函数，并允许它作为一个对象工作(并将其传递给其他函数)。\n\n前面例子中以其他函数作为参数的函数都是高阶函数的例子。返回函数的函数也称为高阶函数。综上所述，高阶函数是接受或返回另一个或多个函数的函数。看看下面的例子:\n\n```cpp\n#include <functional>\n#include <iostream>\n\nstd::function<int (int, int)> get_multiplier()\n{\n return [](int a, int b) { return a * b; };\n}\n\nint main()\n{\n auto multiply = get_multiplier();\n  std::cout << multiply(3, 5) << std::endl; // outputs 15\n}\n```\n\n`get_multiplier()`返回一个包裹在`std::function`中的λ。然后，我们调用结果，就像调用正则函数一样。`get_multiplier()`函数是一个高阶函数。我们可以使用更高阶的函数来实现**curry**，类似于我们在前面的例子中所做的。在函数式编程中，currying 是指我们让一个函数将几个参数转换成几个函数，每个函数只接受一个参数；比如把`multiply(3, 5)`做成`multiply(3)(5)`。以下是我们实现这一目标的方法:\n\n```cpp\nstd::function<int(int)> multiply(int a)\n{\n return [a](int b) { return a * b; };\n}\n\nint main()\n{\n  std::cout << multiply(3)(5) << std::endl;\n}\n```\n\n`multiply()`接受一个参数，并返回一个也接受单个参数的函数。注意λ捕获:它捕获`a`的值，这样它就可以在体内乘以`b`。\n\nCurrying is a reference to logician Haskell Curry. The Haskell, Brook, and Curry programming languages are also named after him.\n\ncurrying 最有用的特性之一是具有我们可以组合在一起的抽象函数。我们可以创建`multiply()`的专门版本，并将它们传递给其他函数，或者在适用的地方使用它们。这可以在下面的代码中看到:\n\n```cpp\nauto multiplyBy22 = multiply(22);\nauto fiveTimes = multiply(5);\n\nstd::cout << multiplyBy22(10); // outputs 220\nstd::cout << fiveTimes(4); // outputs 20\n```\n\n使用 STL 时，您一定使用了更高阶的函数。许多 STL 算法使用谓词来过滤或处理对象集合。例如，`std::find_if`函数找到满足传递的谓词对象的元素，如下例所示:\n\n```cpp\nstd::vector<int> elems{1, 2, 3, 4, 5, 6};\nstd::find_if(elems.begin(), elems.end(), [](int el) {return el % 3 == 0;});\n```\n\n`std::find_if`以一个 lambda 为谓词，为向量中的所有元素调用它。满足条件的元素将作为请求的元素返回。\n\n高阶函数的另一个例子是`std::transform`，这是我们在本章开头介绍的(不要与`ranges::view::transform`混淆)。让我们用它来将字符串转换成大写字母:\n\n```cpp\nstd::string str = \"lowercase\";\nstd::transform(str.begin(), str.end(), str.begin(), \n  [](unsigned char c) { return std::toupper(c); });\nstd::cout << str; // \"LOWERCASE\"\n```\n\n第三个参数是容器的开头，也是`std::transform`函数插入当前结果的地方。\n\n# 可折叠的\n\n折叠(或简化)是将一组值组合在一起以生成数量减少的结果的过程。大多数时候，我们谈论的是一个单一的结果。折叠抽象了迭代本质上是递归的结构的过程。例如，就元素访问而言，链表或向量具有递归性质。虽然向量的递归性质是有争议的，但我们将认为它是递归的，因为它允许我们通过重复递增索引来访问它的元素。为了处理这样的结构，我们通常会跟踪每一步的结果，并处理下一个项目，以便稍后与前一个结果相结合。根据我们处理集合元素的方向，折叠称为*左*或*右*折叠。\n\n例如，`std::accumulate`函数(高阶函数的另一个例子)是折叠功能的完美例子，因为它组合了集合中的值。看看下面这个简单的例子:\n\n```cpp\nstd::vector<double> elems{1.1, 2.2, 3.3, 4.4, 5.5};\nauto sum = std::accumulate(elems.begin(), elems.end(), 0);\n```\n\n函数的最后一个参数是累加器。这是初始值，应该用作集合第一个元素的前一个值。前面的代码计算向量元素的总和。这是`std::accumulate`功能的默认行为。正如我们前面提到的，它是一个高阶函数，这意味着一个函数可以作为它的参数传递。然后将对每个元素调用这个函数来产生期望的结果。例如，让我们找到我们之前声明的`elems`向量的乘积:\n\n```cpp\nauto product = std::accumulate(elems.begin(), elems.end(), 1, \n  [](int prev, int cur) { return prev * cur; });\n```\n\n它需要二进制运算；也就是有两个参数的函数。该操作的第一个参数是到目前为止计算的前一个值，而第二个参数是当前值。二进制运算的结果将是下一步的前一个值。前面的代码可以使用 STL 中的一个现有操作以简洁的方式重写:\n\n```cpp\nauto product = std::accumulate(elems.begin(), elems.end(), 1, \n std::multiplies<int>());\n```\n\n`std::accumulate`功能的更好替代是`std::reduce`功能。`reduce()`与`accumulate()`类似，只是不保持操作顺序；也就是说，它不一定按顺序处理集合元素。您可以将执行策略传递给`std::reduce`函数，并改变其行为，比如并行处理元素。下面是如何使用并行执行策略将 reduce 函数应用于上一个示例中的`elems`向量:\n\n```cpp\nstd::reduce(std::execution::par, elems.begin(), elems.end(), \n  1, std::multiplies<int>());\n```\n\n虽然`std::reduce`看起来比`std::accumulate`更快，但在非交换二进制运算中使用时要小心。\n\n折叠和递归齐头并进。递归函数还通过将问题分解成更小的任务并逐个解决来解决问题。\n\n# 深入递归\n\n我们已经在[第 2 章](02.html)、*c++ 低级编程*中讨论了递归函数的主要特性。让我们看看下面递归计算一个数的阶乘的简单例子:\n\n```cpp\nint factorial(int n)\n{\n  if (n <= 1) return 1;\n  return n * factorial(n - 1);\n}\n```\n\n与迭代函数相比，递归函数提供了优雅的解决方案。但是，您应该仔细考虑使用递归的决定。递归函数最常见的问题之一是堆栈溢出。\n\n# 头部递归\n\n头部递归是我们已经熟悉的正则递归。在前面的示例中，阶乘函数的行为类似于 head 递归函数，这意味着它在处理当前步骤的结果之前进行递归调用。看看阶乘函数的下面一行:\n\n```cpp\n...\nreturn n * factorial(n - 1);\n...\n```\n\n为了找到并返回乘积的结果，函数阶乘用一个简化的参数调用，即`(n - 1)`。这意味着产品(T1)操作员有点像*等待*并等待`factorial(n - 1)`返回其第二个参数。堆栈随函数递归调用的数量而增长。让我们尝试将递归阶乘实现与以下迭代方法进行比较:\n\n```cpp\nint factorial(int n) \n{\n  int result = 1;\n  for (int ix = n; ix > 1; --ix) {\n    result *= ix;\n  }\n  return result;\n}\n```\n\n这里的一个主要区别是，我们将产品每一步的结果存储在同一个变量(命名为`result`)中。考虑到这一点，让我们尝试分解阶乘函数的递归实现。\n\n很明显，每个函数调用都会占用堆栈上的指定空间。每个步骤的每个结果都应该存储在堆栈的某个地方。虽然我们知道它应该，甚至必须是同一个变量，但递归函数并不在乎；它为变量分配空间。常规递归函数的反直觉性促使我们找到一种解决方案，不知何故，它知道每个递归调用的结果应该存储在同一个地方。\n\n# 尾部递归\n\n尾部递归是我们在递归函数中处理的具有多个不必要变量的问题的解决方案。尾部递归函数的基本思想是在递归调用之前进行实际处理。下面是我们如何将阶乘函数转换成尾部递归函数:\n\n```cpp\nint tail_factorial(int n, int result)\n{\n  if (n <= 1) return result;\n  return tail_factorial(n - 1, n * result);\n}\n```\n\n注意函数的新参数。仔细阅读前面的代码，我们会对正在发生的尾部递归有一个基本的了解:处理是在递归调用之前完成的。在其体内再次调用`tail_factorial`之前，计算当前结果(`n * result`)并传递给它。\n\n虽然这个想法看起来并不吸引人，但是如果编译器支持**尾调用优化(TCO)** ，它确实是有效的。TCO 基本上包括知道阶乘函数的第二个参数(尾部)对于每次递归调用可以存储在相同的位置。这允许堆栈保持相同的大小，与递归调用的数量无关。\n\n说到编译器优化，我们不能省略模板元编程。我们在这里提到它是关于编译器优化的，因为我们可以将元编程视为可以对程序进行的最大优化。在编译时做计算总是比在运行时做好。\n\n# 函数式 C++ 中的元编程\n\n元编程可以被视为另一种编程范式。这是一种完全不同的编码方法，因为我们没有处理编程的常规过程。我们所说的常规过程是指程序在其生命周期中经历的三个阶段:编码、编译和运行。很明显，程序在执行时会做它应该做的事情。编译器通过编译和链接生成可执行文件。另一方面，元编程是在代码编译期间执行代码的地方。如果你是第一次和它打交道，这听起来可能很神奇。如果程序甚至还不存在，我们如何执行代码？回想一下我们在[第四章](04.html)、*了解和设计模板*中了解到的模板，我们知道编译器不止一次处理它们。在第一遍中，编译器定义模板类或函数中使用的必要类型和参数。接下来，编译器开始以我们熟悉的方式编译它们；也就是说，它生成一些将由链接器链接的代码，以生成最终的可执行文件。\n\n由于元编程是在代码编译期间发生的事情，我们应该已经知道使用了语言的哪些概念和结构。任何可以在编译时计算的东西都可以用作元编程构造，例如模板。\n\n下面是 C++ 中元编程的经典范例:\n\n```cpp\ntemplate <int N>\nstruct MetaFactorial\n{\n  enum {\n    value = N * MetaFactorial<N - 1>::value\n  };\n};\n\ntemplate <>\nstruct MetaFactorial<0>\n{\n  enum {\n    value = 1\n  };\n};\n\nint main() {\n  std::cout << MetaFactorial<5>::value; // outputs 120\n  std::cout << MetaFactorial<6>::value; // outputs 720\n}\n```\n\n为什么我们要为上一节用不到五行代码写的阶乘写这么多代码呢？原因在于它的效率。虽然编译代码需要多一点时间，但与普通的阶乘函数(递归或迭代实现)相比，它超级高效。这种效率背后的原因是阶乘的实际计算是在编译时进行的。也就是说，当可执行文件运行时，结果已经可以使用了。我们只是在运行程序时使用了计算值；运行时不进行计算。如果你是第一次看到这段代码，下面的解释会让你爱上元编程。\n\n让我们详细分解和分析前面的代码。首先，`MetaFactorial`模板是用一个带有`value`属性的单个`enum`声明的。选择这个`enum`仅仅是因为它的属性是在编译时计算的。所以，每当我们访问`MetaFactorial`的 value 属性时，它已经在编译时被计算(评估)了。看看枚举的实际值。它从同一个`MetaFactorial`类进行递归依赖:\n\n```cpp\ntemplate <int N>\nstruct MetaFactorial\n{\n  enum {\n value = N * MetaFactorial<N - 1>::value\n };\n};\n```\n\n你们中的一些人可能已经注意到了这里的诀窍。`MetaFactorial<N - 1>`与`MetaFactorial<N>`结构不同。尽管具有相同的名称，但每个具有不同类型或值的模板都作为单独的新类型生成。假设我们称之为:\n\n```cpp\nstd::cout << MetaFactorial<3>::value;\n```\n\n这里，努力工作的编译器为每个值生成三个不同的结构(下面是一些伪代码，表示我们应该如何描述编译器的工作):\n\n```cpp\nstruct MetaFactorial<3>\n{\n  enum {\n    value = 3 * MetaFactorial<2>::value\n  };\n};\n\nstruct MetaFactorial<2>\n{\n  enum {\n    value = 2 * MetaFactorial<1>::value;\n  };\n};\n\nstruct MetaFactorial<1>\n{\n  enum {\n    value = 1 * MetaFactorial<0>::value;\n  };\n};\n```\n\n在下一个过程中，编译器将生成的每个结构的值替换为它们各自的数值，如下面的伪代码所示:\n\n```cpp\nstruct MetaFactorial<3>\n{\n  enum {\n   value = 3 * 2\n  };\n};\n\nstruct MetaFactorial<2>\n{\n  enum {\n    value = 2 * 1\n  };\n};\n\nstruct MetaFactorial<1>\n{\n  enum {\n    value = 1 * 1\n  };\n};\n\n```\n\n然后，编译器移除未使用的生成结构，只留下`MetaFactorial<3>`，它再次仅用作`MetaFactorial<3>::value`。这也可以优化。通过这样做，我们得到以下结果:\n\n```cpp\nstd::cout << 6;\n```\n\n将此与我们的前一行进行比较:\n\n```cpp\nstd::cout << MetaFactorial<3>::value;\n```\n\n这就是元编程 F 的美妙之处——它是在编译时完成的，不会留下任何痕迹，就像忍者一样。编译需要更长的时间，但是与常规解决方案相比，程序的执行速度是最快的。我们建议您尝试实现其他成本高昂的计算的元版本，例如计算第 n <sup>个</sup>斐波那契数。这不像为*运行时*而不是*编译时*编码那么容易，但是你已经感受到了它的力量。\n\n# 摘要\n\n在这一章中，我们有了一个使用 C++ 的新视角。作为一种多范式语言，它可以作为一种函数式编程语言。\n\n我们学习了函数式编程的主要原理，例如纯函数、高阶函数和折叠。纯函数是不改变状态的函数。纯函数的优点之一是它们留下的 bug 更少，否则这些 bug 会因为状态突变而被引入。\n\n高阶函数是接受或返回其他函数的函数。除了在函数式编程中，C++ 程序员在处理 STL 时使用更高阶的函数。\n\n纯函数，连同更高阶的函数，允许我们将整个应用分解成一个大的*流水线*的函数。该装配线中的每个功能负责接收数据并返回原始数据的新的修改版本(不改变原始状态)。当这些功能结合在一起时，就提供了一系列协调良好的任务。\n\n在下一章中，我们将深入多线程编程，并讨论在 C++ 中引入的线程支持库组件。\n\n# 问题\n\n1.  列出范围的优势。\n2.  已知哪些函数是纯函数？\n3.  纯虚函数和纯函数在函数编程方面有什么区别？\n4.  什么是折叠？\n5.  尾部递归比头部递归有什么优势？\n\n# 进一步阅读\n\n有关本章内容的更多信息，请查看以下链接:\n\n*   *Wisnu Anggoro 学习 C++ 函数式编程*:[https://www . packtpub . com/application-development/Learning-C-Functional-Programming](https://www.packtpub.com/application-development/learning-c-functional-programming)\n*   *C++ 中的函数式编程:如何使用函数式技术改进你的 c++ 程序*作者:Ivan Cukic:[https://www . Amazon . com/Functional-Programming-Programming-Functional-technologies/DP/1617293814/](https://www.amazon.com/Functional-Programming-programs-functional-techniques/dp/1617293814/)"
  },
  {
    "path": "docs/exp-cpp/08.md",
    "content": "# 八、并发和多线程\n\n并发编程允许创建更高效的程序。C++ 很长一段时间没有内置的并发或多线程支持。现在它完全支持并发编程、线程、线程同步对象以及我们将在本章中讨论的其他功能。\n\n在语言更新为支持线程之前，程序员不得不使用第三方库。最流行的多线程解决方案之一是 **POSIX** ( **便携式操作系统接口**)线程。C++ 从 C++ 11 开始引入线程支持。它使语言更加健壮，适用于更广泛的软件开发领域。理解线程对于 C++ 程序员来说有些至关重要，因为他们倾向于压缩程序的每一个部分，以使其运行得更快。线程向我们介绍了一种完全不同的方法，通过并发运行函数来加快程序的速度。对每个 C++ 程序员来说，在基础水平上学习多线程是必须的。有许多程序无法避免使用多线程，例如网络应用、游戏和图形用户界面应用。本章将向您介绍 C++ 中的并发和多线程基础知识，以及并发代码设计的最佳实践。\n\n本章将涵盖以下主题:\n\n*   理解并发和多线程\n*   使用线程\n*   管理线程和共享数据\n*   设计并发代码\n*   使用线程池避免线程创建开销\n*   熟悉 C++ 20 中的协同程序\n\n# 技术要求\n\n带有`-std=c++ 2a`选项的 g++ 编译器用于编译本章中的示例。你可以在[https://github.com/PacktPublishing/Expert-CPP](https://github.com/PacktPublishing/Expert-CPP)找到本章使用的源文件。\n\n# 理解并发和多线程\n\n运行一个程序最简单的方式是由**中央处理器** ( **中央处理器**)一个接一个地执行它的指令。从前面几章中你已经知道，一个程序由几个部分组成，其中一个包含程序的指令。每个指令都被加载到一个中央处理器寄存器中，供中央处理器解码和执行。实际上，你用什么样的编程范式来产生一个应用并不重要；结果总是一样的——可执行文件包含机器代码。\n\n我们提到像 Java 和 C#这样的编程语言使用支持环境。但是，如果您在中间削减支持环境(通常是虚拟机)，最终执行的指令应该具有该特定 CPU 熟悉的形式和格式。对程序员来说，很明显，在任何情况下，中央处理器运行的语句顺序都是不混合的。例如，我们确信并且可以继续确信，以便下面的程序将分别输出`4`、`\"hello\"`和`5`:\n\n```cpp\nint a{4};\nstd::cout << a << std::endl;\nint b{a};\n++ b;\nstd::cout << \"hello\" << std::endl;\nb--;\nstd::cout << (b + 1) << std::endl;\n```\n\n我们可以保证`a`变量的值在我们打印到屏幕之前会被初始化。同样的，我们可以保证在减少`b`的值之前打印`\"hello\"`字符串，并且在将结果打印到屏幕之前计算`(b + 1)`总和。每条指令的执行可能涉及从内存中读取数据或向内存中写入数据。\n\n正如[第 5 章](05.html)、*内存管理和智能指针*中所介绍的，内存层次结构足够复杂，使得我们对程序执行的理解更加困难。例如，上例中的`int b{a};`行假设`a`的值从内存加载到中央处理器的寄存器中，然后将用于写入`b`的内存位置。这里的关键词是*位置*，因为它为我们承载了一点特殊的解读。更具体地说，我们谈论的是记忆位置。并发支持取决于语言的内存模型，也就是一组对内存并发访问的保证。虽然字节是最小的可寻址存储单元，但中央处理器处理数据中的字。也就是说，字是中央处理器从内存中读取或写入内存的最小单位。例如，我们考虑以下两个独立变量的声明:\n\n```cpp\nchar one;\nchar two;\n```\n\n如果这些变量被分配在同一个单词中(考虑到单词的大小大于`char`的大小)，读写任何一个变量都需要读取包含这两个变量的单词。对变量的并发访问可能会导致意外行为。这是需要内存模型保证的问题。C++ 内存模型保证两个线程可以访问和更新独立的内存位置，而不会相互干扰。内存位置是标量类型。标量类型是算术类型、指针、枚举或`nullptr_t`。非零长度的相邻位字段的最大序列也被认为是存储单元。一个经典的例子是下面的结构:\n\n```cpp\nstruct S\n{\n  char a;             // location #1\n  int b: 5;           // location #2\n  unsigned c: 11;\n  unsigned :0;        // :0 separates bit fields\n  unsigned d: 8;      // location #3\n  struct {\n    int ee: 8;\n  } e;                // location #4 \n};\n```\n\n对于前面的例子，两个线程访问同一个结构的独立内存位置不会相互干扰。那么，当谈到并发或多线程时，我们应该考虑什么？\n\n并发通常与多线程混淆。它们性质相似，但在细节上是不同的概念。为了使事情变得简单，只需将并发想象成两个运行时间交错在一起的操作。如果开始和结束时间在任意点交错，操作`A`与操作`B`同时运行，如下图所示:\n\n![](img/5a602270-5f05-4ec9-9d02-5ab13bbf4883.png)\n\n当两个任务并发运行时，它们不必并行运行。想象一下下面的情况:你一边上网一边看电视。虽然这不是一个好的做法，但是，让我们想象一下，你有一个最喜欢的电视节目，你不能错过，同时，你的朋友让你做一些关于蜜蜂的研究。你实际上不能同时专注于这两项任务；在任何固定的时刻，你的注意力要么被你正在看的节目吸引，要么被你在网上看到的关于蜜蜂的有趣事实吸引。你的注意力不时从节目转移到蜜蜂身上。\n\n就并发性而言，您正在同时执行两项任务。你的大脑给了节目一段时间:你看，享受，然后切换到文章，读几个句子，然后切换回节目。这是并发运行任务的一个简单例子。仅仅因为它们的开始和结束时间交错并不意味着它们同时运行。另一方面，你在做前面提到的任何任务时都会呼吸。呼吸发生在背景中；你的大脑不会把你的注意力从节目或文章转移到你的肺部来吸气或呼气。边看节目边呼吸就是平行跑任务的一个例子。这两个例子都向我们展示了并发的本质。\n\n那么，当您在计算机上运行多个应用时，会发生什么呢？它们是并行运行的吗？当然，它们是并发运行的，但是，实际的并行性取决于您的计算机硬件。大多数大众市场的计算机都由一个中央处理器组成。从前面的章节中我们知道，CPU 的主要工作是逐个运行应用的指令。单个 CPU 如何处理两个应用同时运行？为了理解这一点，我们应该了解过程。\n\n# 处理\n\n进程是运行在内存中的程序的映像。当我们启动一个程序时，OS 从硬盘读取程序的内容，复制到内存中，并将 CPU 指向程序的启动指令。该进程有自己的私有虚拟地址空间、堆栈和堆。两个过程不会以任何方式相互干扰。这是操作系统提供的保证。如果程序员的目标是**进程间通信** ( **IPC** )，那么这也让他们的工作变得非常困难。在本书中，我们不讨论低级硬件特性，但是您应该对我们运行程序时发生的事情有一个大致的了解。这真的取决于底层硬件——更具体地说，是中央处理器的种类和结构。中央处理器的数量、中央处理器内核的数量、高速缓冲存储器的级别以及中央处理器或其内核之间的共享高速缓冲存储器——所有这些都会影响操作系统运行和执行程序的方式。\n\n计算机系统中的 CPU 数量定义了真正并行运行的进程数量。如下图所示:\n\n![](img/249ef5f9-c6a5-43bf-bad3-7648ec848c90.png)\n\n当我们谈论多处理时，我们考虑一个允许多个进程同时运行的环境。棘手的部分来了。如果进程实际上同时运行，那么我们说它们并行运行。因此，并发不是并行，而并行意味着并发。\n\n如果系统只有一个中央处理器，进程会并发运行，但不会并行运行。操作系统通过一种叫做**上下文切换**的机制来管理这一点。上下文切换意味着暂时冻结进程的工作，复制进程当前使用的所有寄存器值，并存储进程的所有活动资源和值。当一个进程停止时，另一个进程获得运行的权利。在为第二个进程提供指定的时间后，操作系统开始为其切换上下文。同样，它复制了流程使用的所有资源。然后，开始前面的过程。在启动之前，操作系统会将资源和值复制回第一个进程使用的相应插槽，然后继续执行该进程。\n\n有趣的是，这些过程甚至没有意识到这一点。所描述的过程发生得如此之快，以至于用户实际上无法注意到操作系统中运行的程序实际上并没有同时运行。下图描述了由单个中央处理器运行的两个进程。当其中一个进程处于*活动状态*时，CPU 按顺序执行其指令，将任何中间数据存储在其寄存器中(你也应该像在游戏中一样考虑高速缓存)。另一个过程是*等待*操作系统提供其运行的时间部分:\n\n![](img/a385b09c-4de2-4fd5-8057-8831d0675c61.png)\n\n运行多个进程对于操作系统来说是一项复杂的工作。它管理进程的状态，定义哪个进程应该比其他进程占用更多的 CPU 时间，等等。在操作系统切换到另一个进程之前，每个进程都有固定的运行时间。对于一个过程，这个时间可以更长，而对于另一个过程，这个时间可以更短。调度过程使用优先级表进行。操作系统为优先级较高的进程提供了更多的时间，例如，系统进程的优先级高于用户进程。另一个例子是，监视网络运行状况的后台任务比计算器应用具有更高的优先级。当提供的时间片到了，操作系统启动上下文切换，即存储**进程 A** 的状态，稍后继续执行:\n\n![](img/1d6e1429-9787-443b-9ea6-2a591b51de4c.png)\n\n存储状态后，如下图所示，它切换到下一个进程来执行它:\n\n![](img/9ba4ecae-987c-4b5a-a51c-2790b12a3c5e.png)\n\n显然，如果**进程 B** 之前运行过，那么它的状态应该加载回 CPU。同样，当**进程 B** 的时间片(或时间段)到了，操作系统存储其状态并将**进程 A** 的状态加载回中央处理器(被操作系统暂停之前的状态):\n\n![](img/ed451d71-cf02-45f6-8929-484ecb4ed453.png)\n\n流程没有任何共同点——或者至少他们是这样认为的。每个正在运行的进程的行为就好像它在系统中是单独的一样。它拥有操作系统所能提供的所有资源。实际上，操作系统设法让进程彼此不知道，因此模拟每个进程的自由。最后，将**进程 A** 的状态加载回来后，CPU 继续执行其指令，就像什么都没发生一样:\n\n![](img/b44e37d7-6d77-4248-ad76-e94dc8ab76f4.png)\n\n**进程 B** 被冻结，直到有新的时间片可供其运行。\n\n运行多个进程的单个 CPU 类似于老师检查学生的试卷。老师一次只能检查一张试卷，尽管他们可以通过逐个检查每个考试的答案来引入一些并发性。首先，他们为一个学生检查第一个问题的答案，然后切换到第二个学生的测试的第一个答案，然后切换回第一个学生的第二个答案，以此类推。每当老师从一张试卷换到另一张试卷时，他们都会在停下来的地方记下问题的编号。这样，当他们回到同一篇论文时，他们就会知道从哪里开始。\n\n同样，操作系统在暂停一个进程以恢复另一个进程之前，会记下该进程的执行点。第二个进程可以(并且很可能会)使用暂停的进程使用的相同寄存器组。这迫使操作系统将第一个进程的寄存器值存储在某个地方，以便以后恢复。当操作系统暂停第二个进程以恢复第一个进程时，它会将已经保存的寄存器值加载回相应的寄存器。恢复的进程将不会注意到任何差异，并将像从未暂停一样继续工作。\n\n前面两段所描述的一切都与单 CPU 系统有关。在多 CPU 系统的情况下，系统中的每个 CPU 都有自己的一组寄存器。此外，每个中央处理器可以独立于其他中央处理器执行程序指令，这允许并行运行进程，而无需暂停和恢复它们。在这个例子中，一个有几个助手的老师类似于一个有三个中央处理器的系统。他们每个人可以检查一份试卷；他们都在随时检查三份不同的试卷。\n\n# 流程方面的挑战\n\n当流程需要以某种方式相互联系时，困难就出现了。假设一个过程应该计算一些东西，并将值传递给一个完全不同的过程。有几种方法可以实现 IPC——其中之一是使用进程间共享的内存段。下图描述了访问共享内存段的两个进程:\n\n![](img/ba6aa4ed-83da-4987-b27f-17429fa23ec4.png)\n\n一个进程将计算结果存储到内存中的共享段中，第二个进程从该段中读取结果。在我们前面的例子中，老师和他们的助手在一份共享的论文中分享他们的检查结果。另一方面，线程共享进程的地址空间，因为它们在进程的上下文中运行。当一个进程是一个程序时，线程是一个函数而不是程序。也就是说，一个进程必须至少有一个线程，我们称之为执行线程。线程是系统中运行的程序指令的容器，而进程封装线程并为其提供资源。我们最感兴趣的是线程及其编排机制。现在让我们亲自去见他们。\n\n# 线\n\n**线程**是进程范围内的一段代码，可以由操作系统调度程序调度。虽然进程是运行程序的映像，但是与利用多线程的项目相比，使用 IPC 管理多进程项目要困难得多，有时甚至毫无用处。程序处理数据，通常是数据的集合。访问、处理和更新数据是通过函数来完成的，这些函数要么是对象的方法，要么是组合在一起以实现最终结果的自由函数。在大多数项目中，我们处理成千上万的函数和对象。每个函数代表一串包装在一个合理名称下的指令，其他函数用来调用它。多线程旨在并发运行函数以获得更好的性能。\n\n例如，计算三个不同向量之和并打印它们的程序调用计算第一个向量之和、第二个向量之和以及最后一个向量之和的函数。这一切都是按顺序发生的。如果单个向量的处理需要一段时间，那么程序将在`3A`时间内运行。下面的代码演示了该示例:\n\n```cpp\nvoid process_vector(const std::vector<int>& vec) \n{\n // calculate the sum and print it\n}\n\nint main()\n{\n std::vector<int> vec1{1, 2, 3, 4, 5};\n std::vector<int> vec2{6, 7, 8, 9, 10};\n std::vector<int> vec3{11, 12, 13, 14, 15};\n process_vector(vec1); // takes A amount of time\n process_vector(vec2); // takes A amount of time\n process_vector(vec3); // takes A amount of time\n}\n```\n\n如果有一种方法可以同时对三个不同的向量运行同一个函数，那么在前面的例子中，整个程序只需要 A 的时间。执行线程，或者仅仅是线程，是并发运行任务的确切方式。任务，我们通常指的是一个函数，虽然你也应该记住`std::packaged_task`。同样，并发不应该与并行混淆。当我们讨论并发运行的线程时，您应该考虑前面讨论的进程的上下文切换。线程几乎也是如此。\n\n`std::packaged_task` is similar to `std::function`. It wraps a callable object—a function, lambda, function object, or bind expression. The difference with `std::packaged_task` is that it can be invoked asynchronously. There's more on that later in this chapter. \n\n每个进程都有一个执行线程，有时称为**主线程**。一个进程可以有多个线程，这就是我们所说的**多线程**。线程的运行方式几乎与进程相同。他们也有语境转换。\n\n线程彼此独立运行，但它们共享进程的大部分资源，因为所有线程都属于该进程。该进程占用硬件和软件资源，如中央处理器寄存器和内存段，包括它自己的堆栈和堆。虽然一个进程不与其他进程共享其堆栈或堆，但其线程必须使用该进程占用的相同资源。线程生命中发生的一切都发生在这个过程中。\n\n但是，线程不共享堆栈。每个线程都有自己的堆栈部分。这种隔离背后的原因依赖于这样一个事实，即线程只是一个函数，函数本身应该可以访问堆栈来管理其参数和局部变量的生命周期。当我们运行两个(或更多)单独运行的线程时，运行时应该以某种方式处理它们的边界。尽管它容易出错，但您可以将变量从一个线程传递到另一个线程(通过值或引用)。让我们假设我们启动了三个线程，为前面例子中的三个向量运行`process_vector()`函数。你应该想象一下，启动一个线程意味着*以某种方式复制*底层函数(它的变量，但不是指令)，并与任何其他线程分开运行。在这种情况下，同一个函数将被复制为三个不同的图像，并且每个图像都将独立于其他图像运行，因此每个图像都应该有自己的堆栈。另一方面，堆在线程之间共享。因此，基本上，我们得出以下结论:\n\n![](img/2837ebc0-a491-4359-aa66-f0e737feff43.png)\n\n与进程的情况一样，并发运行的线程不一定并行运行。每个线程都有一小部分 CPU 时间要运行，同样，从一个线程切换到另一个线程也有开销。每个暂停线程的状态应该存储在某个地方，以便以后恢复时恢复。中央处理器的内部结构决定了线程是否能够真正并行运行。CPU 内核的数量定义了能够真正并行运行的线程数量。\n\nThe C++ thread library provides the `hardware_concurrency()` function to find out the number of threads that can truly run concurrently. You can refer to this number when designing concurrent code. \n\n下图描述了两个各有四个内核的中央处理器。每个内核可以独立运行一个线程:\n\n![](img/82938768-6461-41e2-94dc-454295e6fd96.png)\n\n不仅两个进程并行运行，而且它们的线程也使用中央处理器内核并行运行。现在，如果我们有几个线程但只有一个单核 CPU，情况会如何改变？几乎与我们之前阐述的流程相同。请看下图——它描述了在一段时间内，中央处理器是如何执行**线程 1** 的:\n\n![](img/43a122ae-5378-4cb0-8770-cfea0b51cad2.png)\n\n当前活动的**进程 A** 有两个并发运行的线程。在每个指定的时间点，只执行一个线程。当**线程 1** 的时间片到了，执行**线程 2** 。与我们讨论的进程模型不同的是，线程共享进程的资源，如果我们不关心并发代码设计问题，这将导致不自然的行为。让我们深入研究 C++ 线程支持，并找出使用多线程时会出现什么问题。\n\n# 使用线程\n\n当 C++ 程序启动时，即`main()`函数开始执行时，可以创建并启动新的线程，这些线程将与主线程并发运行。要在 C++ 中启动一个线程，您应该声明一个线程对象，并将您想要并发运行的函数传递给主线程。以下代码演示了使用在`<thread>`中定义的`std::thread`声明和启动线程:\n\n```cpp\n#include <thread> #include <iostream>\n\nvoid foo() { std::cout << \"Testing a thread in C++\" << std::endl; }\n\nint main() \n{\n std::thread test_thread{foo};\n}\n```\n\n就这样。我们可以创建一个更好的例子来展示两个线程如何并发工作。假设我们在一个循环中同时打印数字，看看哪个线程打印什么:\n\n```cpp\n#include <thread>\n#include <iostream>\n\nvoid print_numbers_in_background() \n{\n auto ix{0};  // Attention: an infinite loop!\n while (true) {\n std::cout << \"Background: \" << ix++ << std::endl;\n }\n}\n\nint main()\n{\n std::thread background{print_numbers_in_background};\n  auto jx{0};\n  while (jx < 1000000) {\n    std::cout << \"Main: \" << jx++ << std::endl;\n  }\n}\n```\n\n前面的示例将打印两个输出，其中`Main:`和`Background:`前缀混合在一起。输出的摘录可能如下所示:\n\n```cpp\n...\nMain: 90\nMain: 91\nBackground: 149\nBackground: 150\nBackground: 151\nBackground: 152\nBackground: 153\nBackground: \nMain: 92\nMain: 93\n...\n```\n\n每当主线程完成它的工作(向屏幕打印一百万次)时，程序都想在不等待后台线程完成的情况下完成。这会导致程序终止。让我们看看我们应该如何修改前面的例子。\n\n# 等待线程\n\n`thread`类提供`join()`功能，如果你想等它完成。以下是之前示例的修改版本，等待`background`线程:\n\n```cpp\n#include <thread>\n#include <iostream>\n\nvoid print_numbers_in_background()\n{\n  // code omitted for brevity\n}\n\nint main()\n{\n  std::thread background{print_numbers_in_background};\n  // the while loop omitted for brevity\n background.join();\n}\n```\n\n正如我们之前已经讨论过的那样，`thread`函数是作为独立于其他线程的独立实体运行的——即使是启动它的线程。它不会等待它刚刚启动的线程，这就是为什么您应该显式地告诉调用者函数等待它完成。有必要发出信号，表明调用线程(主线程)正在等待线程在自己之前完成。\n\n`join()`函数的对称反义词是`detach()`函数。`detach()`功能表示调用方对等待线程完成不感兴趣。在这种情况下，线程可以有独立的生命。如图所示(好像已经 18 岁了):\n\n```cpp\nstd::thread t{foo};\nt.detach(); \n```\n\n虽然分离一个线程看起来很自然，但是有很多情况我们需要等待线程完成。例如，我们可以将调用者变量的局部传递给正在运行的线程。在这种情况下，我们不能让调用者分离线程，因为调用者可能比线程在其中开始的时间更早完成工作。为了清楚起见，我们来说明一下。**线程 1** 声明`loc`变量并将其传递给**线程 2** ，后者已经从**线程 1** 开始:\n\n![](img/8bcd907b-742d-4b19-9484-87822f730a68.png)\n\n将`loc`的地址传递给**线程 2** 如果**线程 1** 不加入的话容易出错。如果**线程 1** 在**线程 2** 之前完成其执行，那么通过其地址访问`loc`会导致未定义的行为:\n\n![](img/58cea33e-6366-4f28-9ac5-84e785da0452.png)\n\n现在已经没有这样的对象了，所以我们对这个程序最大的希望就是崩溃。这将导致意外的行为，因为正在运行的线程将无法再访问调用者的局部变量。您应该连接或分离一个线程。\n\n我们可以将任何可调用对象传递给`std::thread`。下面的示例显示了将 lambda 表达式传递给线程:\n\n```cpp\n#include <thread>\n\nint main() {\n  std::thread tl{[]{\n std::cout << \"A lambda passed to the thread\";\n }};\n  tl.join();\n}\n```\n\n此外，我们可以使用可调用对象作为线程参数。看看下面用被覆盖的`operator()`函数声明`TestTask`类的代码:\n\n```cpp\n#include <thread>\n\nclass TestTask\n{\npublic:\n  TestTask() = default;\n\n void operator()() {\n state_++ ;\n }\n\nprivate:\n  int state_ = 0;\n};\n\nint main() {\n  std::thread t{TestTask()};\n  t.join();\n}\n```\n\n函子(带有被覆盖的`operator()`函数的`TestTask`类)的优势之一是它存储状态信息的能力。函子是命令设计模式的完美实现，我们将在[第 11 章](11.html)、*中讨论使用设计模式*设计策略游戏。回到线程，让我们继续语言中的一个新的增加，它允许更好的方法来连接线程。\n\n# 使用 std::jthread\n\nC++ 20 引入了一个可连接的线程`std::jthread`。它提供了与提供的相同的接口`std::thread`，所以我们可以在代码中用 jthreads 替换所有线程。它实际上包装了`std::thread`，所以基本上它委托给包装好的线。\n\n如果你的编译器版本不支持`std::jthread`，你可以自由选择 **RAII** ( **资源获取就是初始化**)这个成语，它完全适用于线程。看看下面的代码:\n\n```cpp\nclass thread_raii\n{\npublic:\n  explicit thread_raii(std::thread& t)\n    : thread_(std::move(t))\n  {}\n\n  ~thread_raii() {\n    thread_.join();  \n  }\n\nprivate:\n  std::thread thread_;\n};\n\nvoid foo() {\n  std::cout << \"Testing thread join\";\n}\n\nint main() {\n std::thread t{foo};\n thread_raii r{t};\n  // will automatically join the thread\n}\n```\n\n但是，前面的代码缺少额外的检查，因为传递给 RAII 类的线程可能已经被分离了。为了查看线程是否可以连接，我们使用`joinable()`函数。我们应该这样覆盖`thread_raii`类:\n\n```cpp\nclass thread_raii\n{\npublic:\n  explicit thread_raii(std::thread& t)\n    : thread_(std::move(t))\n  {}\n\n ~thread_raii()\n {\n if (thread_.joinable()) {\n thread_.join();\n }\n }\nprivate:\n  std::thread thread_;\n};\n```\n\n析构函数首先测试线程是否可连接，然后调用`join()`函数。但是，与其处理习惯用法，关心线程在加入之前是否已经加入，我们更喜欢使用`std::jthread`。下面是我们如何使用之前声明的`TestTask`函数来实现这一点:\n\n```cpp\nstd::jthread jt{TestTask()};\n```\n\n就是这样——不需要调用`jt.join()`，一个新的合作可中断特性开箱即用，我们通过合并 jthread 来使用。我们说 jthread 是协作可中断的，因为它提供了`request_stop()`函数，该函数按照它的名字来做——请求线程停止。虽然这个请求实现是由实现定义的，但是这是一个不要永远等待线程的好方法。回想一下线程在无限循环中打印数字的例子。我们修改了主线程来等待它，这导致了永远等待它。以下是我们如何使用`std::jthread`修改线程以利用`request_stop()`功能:\n\n```cpp\nint main()\n{\n std::jthread background{print_numbers_in_background};\n  auto jx{0};\n  while (jx < 1000000) {\n    std::cout << \"Main: \" << jx << std::endl;\n  }\n  // The main thread is about to finish, so we request the background thread to stop\n background.request_stop();\n}\n```\n\n`print_numbers_in_background()`功能现在接收到一个请求，并可以相应地操作。现在，让我们看看如何将参数传递给线程函数。\n\n# 将参数传递给线程函数\n\n`std::thread`构造函数接受参数并将其转发给底层的`thread`函数。例如，为了将参数`4`和`2`传递给这里的`foo()`函数，我们将参数传递给`std::thread`构造函数:\n\n```cpp\nvoid foo(int one, int two) {\n  // do something\n}\n\nstd::thread t{foo, 4, 2};\n```\n\n参数`4`和`2`将作为第一个和第二个参数传递给`foo()`函数。\n\n以下示例说明了通过引用传递参数:\n\n```cpp\nclass big_object {};\n\nvoid make_changes(big_object&);\n\nvoid error_prone()\n{\n  big_object b;\n std::jthread t{make_changes, b};\n  // do something else\n}\n```\n\n为了理解我们为什么命名函数`error_prone`，我们应该知道线程构造器复制传递给它的值，然后将它们传递给带有`rvalue`引用的线程函数。这样做是为了处理仅移动类型。所以它会尝试用`rvalue`调用`make_changes()`函数，这样会编译失败(你不能把`rvalue`传递给需要非常数引用的函数)。我们需要包装需要在`std::ref:`中引用的参数\n\n```cpp\nstd::thread t{make_changes, std::ref(b)};\n```\n\n前面的代码强调参数应该通过引用传递。使用线程需要更加注意，因为有很多方法可以在程序中获得意想不到的结果或未定义的行为。让我们看看如何管理线程来产生更安全的多线程应用。\n\n# 管理线程和共享数据\n\n如前所述，如果线程的数量超过硬件支持的并行运行线程的数量，线程的执行包括暂停和恢复其中的一些。除此之外，线程的创建也有开销。处理一个项目中有许多线程的一个建议做法是使用线程池。\n\n线程池的概念在于缓存的概念。我们在某个容器中创建并保存线程以备后用。容器被称为池。例如，下面的向量表示一个简单的线程池:\n\n```cpp\n#include <thread>\n#include <vector>\n\nstd::vector<std::thread> pool;\n```\n\n每当我们需要一个新的线程时，我们使用一个已经在池中创建的线程，而不是声明相应的`std::thread`对象。当我们完成线程后，如果需要，我们可以将它推回到向量中，以便以后使用。当使用 10 个或更多线程时，这可以节省一些时间。一个恰当的例子是网络服务器。\n\nweb 服务器是一个程序，它等待传入的客户端连接，并为每个客户端创建一个单独的连接，以便独立于其他客户端进行处理。典型的网络服务器通常同时处理成千上万的客户端。每次与某个客户端建立新连接时，web 服务器都会创建一个新线程并处理客户端请求。下面的伪代码演示了 web 服务器传入连接管理的简单实现:\n\n```cpp\nvoid process_incoming_connections() {\n  if (new connection from client) {\n    t = create_thread(); // potential overhead\n    t.handle_requests(client);\n  }\n}\nwhile (true) {\n  process_incoming_connections();\n}\n```\n\n使用线程池时，前面的代码将避免在每次需要处理客户端请求时创建线程。新线程的创建需要操作系统进行额外且相当昂贵的工作。为了节省时间，我们使用了一种机制，省略了在每个请求上创建新线程。为了使池更好，让我们用一个队列替换它的容器。每当我们请求一个线程时，池将返回一个空闲线程，每当我们处理完一个线程时，我们将它推回到池中。线程池的简单设计如下所示:\n\n```cpp\n#include <queue>\n#include <thread>\n\nclass ThreadPool\n{\npublic:\n  ThreadPool(int number_of_threads = 1000) {\n    for (int ix = 0; ix < number_of_threads; ++ ix) {\n      pool_.push(std::thread());\n    }\n  }\n\n  std::thread get_free_thread() {\n    if (pool_.empty()) {\n      throw std::exception(\"no available thread\");\n    }\n    auto t = pool_.front();\n    pool_.pop();\n    return t;\n  }\n\n  void push_thread(std::thread t) {\n    pool_.push(t);\n  }\n\nprivate:\n  std::queue<std::thread> pool_;\n};\n```\n\n构造函数创建线程并将其推入队列。在下面的伪代码中，我们用`ThreadPool`代替了直接为客户端请求处理创建线程，我们之前已经看过了:\n\n```cpp\nThreadPool pool;\nvoid process_incoming_connections() {\n  if (new connection from client) {\n    auto t = pool.get_free_thread();\n    t.handle_request(client);\n  }\n}\n\nwhile (true) {\n  process_incoming_connections();\n}\n```\n\n假设`handle_request()`函数完成后将线程推回到池中，那么池就像一个连接线程的集中存储。尽管在前面的代码片段中显示的还远未准备好投入生产，但它传达了在密集型应用中使用线程池的基本思想。\n\n# 共享数据\n\n使用多线程的程序员害怕并尽可能避免竞争条件。想象两个函数同时处理相同的数据，如下所示:\n\n```cpp\nint global = 0;\n\nvoid inc() {\n  global = global + 1;\n}\n...\nstd::thread t1{inc};\nstd::thread t2{inc};\n```\n\n潜在的竞争条件正在发生，因为线程`t1`和`t2`正在用多个步骤修改同一个变量。在单一线程安全步骤中执行的任何操作都称为**原子操作**。在这种情况下，增加变量值不是原子操作，即使我们使用了增量运算符。\n\n# 使用互斥体保护共享数据\n\n为了保护共享数据，被称为**互斥体**的对象被广泛使用。互斥体是控制线程运行的对象。把线程想象成人类在一个接一个地处理数据。当一个线程锁定一个互斥体时，另一个线程等待，直到它完成数据并解锁互斥体。然后另一个线程锁定互斥体并开始处理数据。下面的代码演示了如何使用互斥体解决竞争条件的问题:\n\n```cpp\n#include <mutex>\n...\nstd::mutex locker;\nvoid inc() {\n  locker.lock();\n  global = global + 1;\n  locker.unlock();\n}\n...\nstd::thread t1{inc};\nstd::thread t2{inc};\n\n```\n\n当`t1`开始执行`inc()`时，它会锁定一个互斥体，这样可以避免任何其他线程访问全局变量，除非原线程没有解锁下一个线程。\n\nC++ 17 引入了一个锁保护，允许保护互斥体，以免忘记解锁它:\n\n```cpp\nstd::mutex locker;\nvoid inc() {\n  std::lock_guard g(locker);\n  global = global + 1;\n}\n```\n\n如果可能的话，最好使用语言提供的警卫。\n\n# 避免僵局\n\n互斥体出现了新的问题，比如**死锁**。当两个或多个线程锁定一个互斥体并等待另一个线程解锁另一个互斥体时，死锁是多线程代码的一种情况。\n\n避免死锁的常见建议是始终以相同的顺序锁定两个或多个互斥体。C++ 提供了`std::lock()`函数，服务于同样的目的。\n\n下面的代码说明了`swap`函数，它接受两个类型为`X`的参数。我们假设`X`有一个成员，`mt`，是互斥体。`swap`函数的实现首先锁定左对象的互斥体，然后锁定右对象的互斥体:\n\n```cpp\nvoid swap(X& left, X& right)\n{\n  std::lock(left.mt, right.mt);\n  std::lock_guard<std::mutex> lock1(left.mt, std::adopt_lock);\n  std::lock_guard<std::mutex> lock2(right.mt, std::adopt_lock);\n  // do the actual swapping\n}\n```\n\n为了避免死锁，请避免嵌套锁。也就是说，如果你已经持有一把锁，就不要获取它。如果不是这样，那么以固定的顺序获取锁。固定顺序可以让你避免僵局。\n\n# 设计并发代码\n\n当引入并发性时，项目的复杂性会急剧上升。与并发代码相比，处理顺序执行的同步代码要容易得多。许多系统通过引入事件驱动的开发概念，比如事件循环，来避免使用多线程。使用事件循环的目的是为异步编程引入一种可管理的方法。为了进一步理解这个概念，想象任何提供**图形用户界面** ( **图形用户界面**)的应用。每当用户点击任何图形用户界面组件，如按钮；字段中的类型；或者甚至移动鼠标，应用接收关于用户动作的所谓事件。无论是`button_press`、`button_release`、`mouse_move`还是任何其他事件，它都代表了一条信息，让应用做出正确的反应。一种流行的方法是结合一个事件循环来对用户交互过程中发生的任何事件进行排队。\n\n当应用忙于当前任务时，用户操作产生的事件会排队等待将来某个时间处理。处理包括调用附加到每个事件的处理函数。他们按照排队的顺序被叫去。\n\n在项目中引入多线程会带来额外的复杂性。您现在应该注意竞争条件和正确的线程处理，甚至可以使用线程池来重用线程对象。在顺序执行的代码中，您只关心代码。使用多线程，您现在更关心相同代码的执行方式。例如，像 singleton 这样的简单设计模式在多线程环境中表现不同。单例的经典实现如下所示:\n\n```cpp\nclass MySingleton\n{\npublic:\n static MySingleton* get_instance() {\n if (instance_ == nullptr) {\n instance_ = new MySingleton();\n }\n return instance_;\n }\n\n  // code omitted for brevity\nprivate:\n  static inline MySingleton* instance_ = nullptr;\n};\n```\n\n下面的代码启动两个线程，都使用`MySingleton`类:\n\n```cpp\nvoid create_something_unique() \n{\n MySingleton* inst = MySingleton::get_instance();\n  // do something useful\n}\n\nvoid create_something_useful() \n{\n  MySingleton* anotherInst = MySingleton::get_instance();\n  // do something unique\n}  \n\nstd::thread t1{create_something_unique};\nstd::thread t2{create_something_useful};\nt1.join();\nt2.join();\n// some other code\n```\n\n线程`t1`和`t2`都调用`MySingleton`类的`get_instance()`静态成员函数。有可能`t1`和`t2`都通过了空实例的检查，并且都执行了新的操作符。很明显，我们这里有比赛条件。在这种情况下，应该保护资源，即类实例，使其免受这种情况的影响。这里有一个明显的使用互斥体的解决方案:\n\n```cpp\nclass MySingleton\n{\npublic:\n  static MySingleton* get_instance() {\n std::lock_guard lg{mutex_};\n    if (instance_ == nullptr) {\n      instance_ = new MySingleton();\n    }\n    return instance_;\n  }\n\n  // code omitted for brevity\nprivate:\n static std::mutex mutex_;\n  static MySingleton* instance_;\n}\n```\n\n使用互斥体可以解决这个问题，但是会使函数运行得更慢，因为每次线程请求实例时，互斥体都会被锁定(这涉及到操作系统内核的额外操作)。正确的解决方案是使用双重检查锁定模式。它的基本思想是这样的:\n\n1.  `instance_`检查后锁定互斥体。\n2.  在互斥锁被锁定后再次检查`instance_`，因为另一个线程可能已经通过了第一次检查，等待互斥锁解锁。\n\n有关详细信息，请参见代码:\n\n```cpp\nstatic MySingleton* get_instance() {\n  if (instance_ == nullptr) {\n std::lock_guard lg{mutex_};\n if (instance_ == nullptr) {\n instance_ = new MySingleton();\n }\n  }\n  return instance_;\n}\n```\n\n几个线程可能会通过第一次检查，其中一个线程会锁定互斥体。只有一个线程可以调用新的运算符。但是，在解锁互斥体后，通过第一次检查的线程将尝试锁定它并创建实例。第二次检查是为了防止这种情况。前面的代码允许我们减少同步代码的性能开销。我们在这里提供的方法是为并发代码设计做准备的方法之一。\n\n并行代码设计在很大程度上是基于语言本身的能力。C++ 的发展是不可思议的。在其早期版本中，它没有内置的多线程支持。现在，它有了一个可靠的线程库，新的 C++ 20 标准为我们提供了更强大的工具，比如 coroutines。\n\n# 引入协同效应\n\n在谈到图形用户界面应用时，我们讨论了一个异步代码执行的例子。图形用户界面组件通过触发相应的事件对用户动作做出反应，这些事件被推入事件队列。然后，通过调用附加的处理函数来逐个处理这个队列。所描述的过程在循环中发生；这就是为什么我们通常称这个概念为事件循环。\n\n异步系统在输入/输出操作中非常有用，因为任何输入或输出操作都会在输入/输出调用时阻塞执行。例如，下面的伪代码从目录中读取一个文件，然后向屏幕输出一条欢迎消息:\n\n```cpp\nauto f = read_file(\"filename\");\ncout << \"Welcome to the app!\";\nprocess_file_contents(f);\n```\n\n附同步执行模式，我们知道消息欢迎来到 app！只有在`read_file()`功能执行完毕后才会打印。`process_file_contents()`只有在`cout`完成后才会被调用。当处理异步代码时，我们所知道的关于代码执行的一切开始变得不可识别。上例的以下修改版本使用`read_file_async()`函数异步读取文件内容:\n\n```cpp\nauto p = read_file_async(\"filename\");\ncout << \"Welcome to the app!\";\nprocess_file_contents(p); // we shouldn't be able to do this\n```\n\n考虑到`read_file_async()`是异步功能，消息欢迎来到 app！将比文件内容打印得更快。异步执行的本质允许我们在后台调用要执行的函数，这为我们提供了非阻塞的输入/输出。\n\n但是，我们处理函数返回值的方式略有变化。如果我们处理一个异步函数，它的返回值被认为是一个叫做**承诺**或者**承诺对象**的东西。这是异步函数完成时系统通知我们的方式。承诺对象有三种状态:\n\n*   悬而未决的\n*   被拒绝\n*   感到满足的\n\n如果函数已经完成，并且结果已经准备好被处理，那么承诺对象被认为已经实现。如果出现错误，承诺对象将处于拒绝状态。如果承诺没有被拒绝或履行，则处于待定状态。\n\nC++ 20 引入了 coroutines，作为对经典异步函数的补充。协同程序将代码的后台执行移动到下一个级别；它们允许功能在必要时暂停和恢复。想象一个函数读取文件内容并在中间停止，将执行上下文传递给另一个函数，然后继续读取文件直到结束。所以，在更深的潜水之前，考虑一个函数，如下所示:\n\n*   出发\n*   暂停\n*   重新开始\n*   完成\n\n要使一个函数成为协同函数，您可以使用关键字`co_await`、`co_yield`或`co_return`中的一个。`co_await`是告诉代码等待异步执行代码的构造。这意味着函数可以在该点暂停，并在结果准备好时恢复执行。例如，以下代码使用套接字从网络请求图像:\n\n```cpp\ntask<void> process_image()\n{\n  image i = co_await request_image(\"url\");\n  // do something useful with the image\n}\n```\n\n由于网络请求操作也被视为**输入/输出**操作，它可能会阻止代码的执行。为了防止阻塞，我们使用异步调用。前面例子中使用`co_await`的那一行是可以暂停函数执行的点。简单来说，当执行到达`co_await`的那一行时，会发生以下情况:\n\n1.  它退出该函数一段时间(直到没有准备好的数据)。\n2.  它从调用`process_image()`之前的位置继续执行。\n3.  然后它再次返回，在离开的地方继续执行`process_image()`。\n\n为了实现这一点，协同程序(函数`process_image()`是协同程序)的处理方式不同于 C++ 中常规函数的处理方式。花冠的一个有趣甚至令人惊讶的特征是它们是无叠层的。我们知道函数离不开栈。这就是函数在执行指令之前推送参数和局部变量的地方。另一方面，Coroutines 不是将任何东西推到堆栈中，而是将它们的状态保存在堆中，并在恢复时恢复它。\n\nThis is tricky because there are also stackful coroutines. Stackful coroutines, also referred to as **fibers**, have a separate stack. \n\n协同程序连接到调用方。在前面的例子中，调用`sprocess_image()`的函数将执行转移到协同程序，协同程序的暂停(也称为**产生**)将执行转移回调用者。正如我们所说的，堆用于存储协同程序的状态，但是实际的特定于函数的数据(参数和局部变量)存储在调用方的堆栈上。就是这样——协同程序与存储在调用者函数堆栈上的对象相关联。显然，花冠和它的目标一样长。\n\nCoroutines 可能会给人一种错误的印象，认为它增加了语言的冗余复杂性，但是它们的用例在改进使用异步输入/输出代码(如前面的例子)或懒惰计算的应用方面非常有用。也就是说，当我们不得不发明新的模式或在项目中引入复杂性来处理，例如，懒惰的计算时，我们现在可以通过在 C++ 中使用协同程序来改善我们的体验。请注意，异步输入/输出或懒惰计算只是协同应用的两个例子。外面还有更多。\n\n# 摘要\n\n在本章中，我们讨论了并发性的概念，并展示了并行性之间的区别。我们了解了进程和线程之间的区别，后者是我们感兴趣的。多线程允许我们更有效地管理程序，尽管它也带来了额外的复杂性。为了处理数据竞争，我们使用同步原语，比如互斥体。互斥体是一种锁定一个线程使用的数据的方法，以避免同时访问多个线程的相同数据而产生的无效行为。\n\n我们还介绍了输入/输出操作被认为是阻塞的想法，异步函数是使其非阻塞的方法之一。C++ 20 引入了作为代码异步执行的一部分的协同程序。\n\n我们学习了如何创建和启动线程。更重要的是，我们学习了如何管理线程之间的数据。在下一章中，我们将深入研究并发环境中使用的数据结构。\n\n# 问题\n\n1.  什么是并发？\n2.  并发和并行的区别是什么？\n3.  什么是流程？\n4.  进程和线程有什么区别？\n5.  编写代码来启动线程。\n6.  如何使单例模式线程安全？\n7.  重写`MySingleton`类，为返回的实例使用`std::shared_ptr`。\n8.  什么是协同，什么是`co_await`关键词？\n\n# 进一步阅读\n\n*   *安东尼·威廉姆斯，C++ 并发在行动*，[https://www . Amazon . com/C-Concurrency-Action-Anthony-Williams/DP/1617294691/](https://www.amazon.com/C-Concurrency-Action-Anthony-Williams/dp/1617294691/)"
  },
  {
    "path": "docs/exp-cpp/09.md",
    "content": "# 九、设计并发数据结构\n\n在前一章中，我们谈到了 C++ 中并发和多线程的基础。并行代码设计中最大的挑战之一是正确处理数据竞争。线程同步和编排不是一个容易掌握的话题，尽管我们可能认为它是最重要的话题。虽然我们可以在对数据竞争有丝毫怀疑的任何地方使用同步原语，如互斥体，但这不是我们建议的最佳实践。\n\n设计并发代码的更好方法是不惜一切代价避免锁。这不仅会提高应用的性能，还会使它比以前更安全。说起来容易做起来难——无锁编程是我们在本章中介绍的一个具有挑战性的主题。特别是，我们将进一步深入设计无锁算法和数据结构的基础。这是许多优秀开发人员不断研究的一个难题。我们将触及无锁编程的基础，这将让您了解如何以有效的方式构造代码。阅读本章后，您将能够更好地描绘数据竞赛的问题，并获得设计并发算法和数据结构所需的基本知识。这也可能有助于您的一般设计技能来构建容错系统。\n\n本章将涵盖以下主题:\n\n*   了解数据竞争和基于锁的解决方案\n*   在 C++ 代码中使用原子\n*   设计无锁数据结构\n\n# 技术要求\n\n带有`-std=c++ 2a`选项的 g++ 编译器用于编译本章中的示例。你可以在[https://github.com/PacktPublishing/Expert-CPP](https://github.com/PacktPublishing/Expert-CPP)找到本章使用的源文件。\n\n# 仔细看看数据竞赛\n\n正如已经多次指出的，数据竞争是程序员不惜一切代价试图避免的情况。在前一章中，我们讨论了死锁和避免死锁的方法。我们在前一章中使用的最后一个例子是创建一个线程安全的单例模式。假设我们使用一个类来创建数据库连接(一个经典的例子)。\n\n下面是跟踪数据库连接的模式的简单实现。每次需要访问数据库时保持单独的连接不是一个好的做法。相反，我们重用现有的连接来从程序的不同部分查询数据库:\n\n```cpp\nnamespace Db {\n  class ConnectionManager \n  {\n  public:\n    static std::shared_ptr<ConnectionManager> get_instance()\n {\n if (instance_ == nullptr) {\n instance_.reset(new ConnectionManager());\n }\n return instance_;\n }\n\n    // Database connection related code omitted\n  private:\n    static std::shared_ptr<ConnectionManager> instance_{nullptr};\n  };\n}\n```\n\n让我们更详细地讨论这个例子。在前一章中，我们引入了锁定来保护`get_instance()`函数免受数据竞争的影响。让我们详细说明我们这样做的原因。为了简化示例，我们对以下四条线感兴趣:\n\n```cpp\nget_instance()\n  if (_instance == nullptr)\n    instance_.reset(new)\n  return instance_;\n```\n\n现在，假设我们运行一个访问`get_instance()`函数的线程。我们将其命名为`Thread A`，它执行的第一行是条件语句，如图所示:\n\n```cpp\nget_instance()\n  if (_instance == nullptr)   <--- Thread A\n    instance_.reset(new)\n  return instance_;\n```\n\n它将一行行地执行指令。我们更感兴趣的是第二个线程(标记为`Thread B`)，它开始执行与`Thread A`并发的功能。在函数的并发执行过程中，可能会出现以下情况:\n\n```cpp\nget_instance()\n  if (_instance == nullptr)   <--- Thread B (checking)\n    instance_.reset(new)      <--- Thread A (already checked)\n  return instance_;\n```\n\n`Thread B`对比`instance_`和`nullptr`得到肯定的结果。`Thread A`通过了相同的检查，并将`instance_`设置为新对象。而从`Thread A`的角度来看一切都很好，只是通过了条件检查，重置`instances`，将继续下一行返回`instance_`。然而，就在`Thread B`的价值发生变化之前，它与`instance_`进行了比较。因此，`Thread B`也继续设置`instance_`的值:\n\n```cpp\nget_instance()\n  if (_instance == nullptr)   \n    instance_.reset(new)      <--- Thread B (already checked)\n  return instance_;           <--- Thread A (returns)\n```\n\n前面的问题是`Thread B`在已经设置好之后重置`instance_`。同样，我们将`get_instance()`视为单一操作；它由几个指令组成，每个指令由一个线程顺序执行。为了使两个线程不相互干扰，操作不应该由一个以上的指令组成。\n\n我们关注数据竞争的原因是前面代码块中显示的差距。线与线之间的间隙会让线相互干扰。当您使用同步原语(如互斥体)设计解决方案时，您应该描绘出您错过的所有间隙，因为解决方案可能不是正确的。以下修改使用了互斥体和前一章中讨论的`double-checked`锁定模式:\n\n```cpp\nstatic std::shared_ptr<ConnectionManager> get_instance()\n{\n  if (instance_ == nullptr) {\n    // mutex_ is declared in the private section\n std::lock_guard lg{mutex_};\n if (instance_ == nullptr) { // double-checking\n instance_.reset(new ConnectionManager());\n }\n  }\n  return instance_;\n}\n```\n\n以下是当两个线程试图访问`instance_`对象时发生的情况:\n\n```cpp\nget_instance()\n  if (instance_ == nullptr)     <--- Thread B\n    lock mutex                  <--- Thread A (locks the mutex)\n    if (instance_ == nullptr)\n      instance_.reset(new)\n    unlock mutex\n  return instance_\n```\n\n现在，即使两个线程都通过了第一次检查，其中一个也会锁定互斥体。当其中一个线程试图锁定互斥体时，另一个线程将重置实例。为了确保它没有被设置，我们使用了第二个检查(这就是为什么它被称为**双重检查锁定**):\n\n```cpp\nget_instance()\n  if (instance_ == nullptr)\n    lock mutex                  <--- Thread B (tries to lock, waits)\n    if (instance_ == nullptr)   <--- Thread A (double check)\n      instance_.reset(new)      \n    unlock mutex\n  return instance_\n```\n\n当`Thread A`完成设置`instance_`后，它会解锁互斥体，这样`Thread B`就可以继续锁定和重置`instance_`:\n\n```cpp\nget_instance()\n  if (instance_ == nullptr)\n    lock mutex                  <--- Thread B (finally locks the mutex)\n    if (instance_ == nullptr)   <--- Thread B (check is not passed)\n      instance_.reset(new)      \n    unlock mutex                <--- Thread A (unlocked the mutex)\n  return instance_              <--- Thread A (returns)  \n```\n\n根据经验，您应该始终从代码的字里行间寻找答案。两个语句之间总是有一个间隙，这个间隙会使两个或多个线程相互干扰。下一节将详细讨论一个递增数字的经典例子。\n\n# 同步增量\n\n几乎每本涉及线程同步主题的书都使用递增数字的经典示例作为数据竞赛示例。这本书也不例外。示例如下:\n\n```cpp\n#include <thread>\n\nint counter = 0;\n\nvoid foo()\n{\n counter++ ;\n}\n\nint main()\n{\n  std::jthread A{foo};\n  std::jthread B{foo};\n  std::jthread C{[]{foo();}};\n  std::jthread D{\n    []{\n      for (int ix = 0; ix < 10; ++ ix) { foo(); }\n    }\n  };\n}\n```\n\n我们增加了几个线程，使示例更加复杂。前面的代码只不过是使用四个不同的线程增加`counter`变量。乍一看，在任何时间点，只有一个线程递增`counter`。然而，正如我们在上一节中提到的，我们应该注意并寻找代码中的漏洞。`foo()`功能似乎少了一个。增量运算符的行为如下(作为伪代码):\n\n```cpp\nauto res = counter;\ncounter = counter + 1;\nreturn res;\n```\n\n现在，我们已经发现了本不该存在的空白。所以现在，在任何时间点，只有一个线程执行前面三条指令中的一条。也就是说，类似下面这样的事情是可能的:\n\n```cpp\nauto res = counter;     <--- thread A\ncounter = counter + 1;  <--- thread B\nreturn res;             <--- thread C\n```\n\n因此，例如，`thread B`可能会修改`counter`的值，而`thread A`会读取其先前的值。这意味着`thread A`将在`thread B`完成后为`counter`分配一个新的增量值。混乱带来了混乱，我们的大脑迟早会爆炸，试图理解操作的顺序。作为一个经典的例子，我们将继续使用线程锁定机制来解决这个问题。这里有一个流行的解决方案:\n\n```cpp\n#include <thread>\n#include <mutex>\n\nint counter = 0;\nstd::mutex m;\n\nvoid foo()\n{\n std::lock_guard g{m};\n  counter++ ;\n}\n\nint main()\n{\n  // code omitted for brevity\n}\n```\n\n到达`lock_guard`的线程首先锁定`mutex`，如下图所示:\n\n```cpp\nlock mutex;             <--- thread A, B, D wait for the locked mutex \nauto res = counter;     <--- thread C has locked the mutex\ncounter = counter + 1;\nunlock mutex;           *<--- A, B, D are blocked until C reaches here*\nreturn res;             \n```\n\n使用锁定的问题在于性能。理论上，我们使用线程来加速程序执行，更具体地说，数据处理。在大数据集合的情况下，使用多线程可能会显著提高程序的性能。但是，在多线程环境中，我们首先处理并发访问，因为用多个线程访问集合可能会导致集合损坏。例如，让我们看看线程安全的堆栈实现。\n\n# 实现线程安全堆栈\n\n从[第 6 章](06.html)、*中调用堆栈数据结构适配器，深入了解 STL* 中的数据结构和算法。我们将使用锁实现堆栈的线程安全版本。堆栈有两个基本操作，`push`和`pop`。它们都修改了容器的状态。如您所知，堆栈本身不是容器；它是一个适配器，包装了一个容器，并提供了一个合适的接口来访问。我们将通过引入线程安全将`std::stack`包装在一个新的类中。除了建造和破坏功能外，`std::stack`还提供以下功能:\n\n*   `top()`:访问栈顶元素\n*   `empty()`:如果堆栈为空，则返回真\n*   `size()`:返回当前堆栈的大小\n*   `push()`:在堆栈中插入一个新项目(在顶部)\n*   `emplace()`:在堆栈顶部的适当位置构造一个元素\n*   `pop()`:移除堆栈的顶部元素\n*   `swap()`:用另一个堆栈交换内容\n\n我们将保持简单，专注于线程安全的思想，而不是构建一个强大的全功能堆栈。这里主要关注的是修改底层数据结构的函数。我们的兴趣在于`push()`和`pop()`功能。如果几个线程相互干扰，这些函数可能会破坏数据结构。因此，下面的声明是表示线程安全堆栈的类:\n\n```cpp\ntemplate <typename T>\nclass safe_stack\n{\npublic:\n  safe_stack();\n  safe_stack(const safe_stack& other);\n  void push(T value); // we will std::move it instead of copy-referencing\n  void pop();\n  T& top();\n  bool empty() const;\n\nprivate:\n  std::stack<T> wrappee_;\n  mutable std::mutex mutex_;\n};\n```\n\n请注意，我们将`mutex_`声明为可变的，因为我们将其锁定在`empty()`常量函数中。可以说，这是一个比去掉`empty()`元素更好的设计选择。但是，现在您应该知道，对任何数据成员使用可变参数都表明我们做出了错误的设计选择。反正`safe_stack`的客户端代码不会太在意实现的内在细节；它甚至不知道堆栈使用互斥来同步并发访问。\n\n现在让我们看看它的成员函数的实现以及一个简短的描述。让我们从复制构造函数开始:\n\n```cpp\nsafe_stack::safe_stack(const safe_stack& other)\n{\n  std::lock_guard<std::mutex> lock(other.mutex_);\n  wrappee_ = other.wrappee_;\n}\n```\n\n请注意，我们锁定了另一个堆栈的互斥体。尽管看起来不公平，但我们需要确保在复制另一个堆栈时，它的底层数据不会被修改。\n\n接下来，我们来看看`push()`功能的实现。很明显很简单；我们锁定互斥体并将数据推入底层堆栈:\n\n```cpp\nvoid safe_stack::push(T value)\n{\n  std::lock_guard<std::mutex> lock(mutex_);\n  // note how we std::move the value\n  wrappee_.push(std::move(value));\n}\n```\n\n几乎所有的函数都以相同的方式合并线程同步:锁定互斥体、执行任务和解锁互斥体。这确保了在任何时候只有一个线程在访问数据。也就是说，为了保护数据不受竞争条件的影响，我们必须确保函数不变量不会被破坏。\n\nIf you are not a fan of typing long C++ type names such as `std::lock_guard<std::mutex>`, use the `using` keyword to make short aliases for types, for example, using `locker = std::guard<std::mutex>;`.\n\n现在，转到`pop()`函数，我们可以修改类声明，使`pop()`直接返回栈顶的值。我们这样做主要是因为我们不希望有人访问栈顶(带有引用)，然后从另一个线程中弹出数据。因此，我们将修改`pop()`函数来创建一个共享对象，然后返回堆栈元素:\n\n```cpp\nstd::shared_ptr<T> pop()\n{\n  std::lock_guard<std::mutex> lock(mutex_);\n  if (wrappee_.empty()) {\n    throw std::exception(\"The stack is empty\");\n  }\n  std::shared_ptr<T> top_element{std::make_shared<T>(std::move(wrappee_.top()))};\n  wrappee_.pop();\n  return top_element;\n}\n```\n\n注意`safe_stack`类的声明也应该根据`pop()`函数的修改而改变。而且，我们不再需要`top()`了。\n\n# 设计无锁数据结构\n\n如果保证至少有一个线程能够取得进展，那么我们就说它是一个无锁函数。与基于锁的函数相比，在基于锁的函数中，一个线程可以阻塞另一个线程，并且它们都可能在取得进展之前等待某种条件，无锁状态确保至少一个线程取得进展。我们说使用数据同步原语的算法和数据结构是阻塞的，也就是说，一个线程被挂起，直到另一个线程执行一个操作。这意味着线程在块被移除之前无法取得进展(通常是解锁互斥)。我们的兴趣在于不使用阻塞函数的数据结构和算法。我们称其中一些为无锁的，尽管我们应该区分非阻塞算法和数据结构的类型。\n\n# 使用原子类型\n\n在本章的前面，我们介绍了源代码行之间的差距是数据竞争的原因。每当你有一个包含多个指令的操作时，你的大脑应该提醒你一个可能的问题。然而，你如何努力使操作独立和单一并不重要；大多数情况下，如果不将操作分解为涉及多个指令的步骤，就无法实现任何目标。C++ 通过提供原子类型来拯救人类。\n\n首先，让我们理解为什么使用原子这个词。一般来说，我们把原子理解为不能分解成更小部分的东西。也就是说，原子操作是一个不能半途而废的操作:要么完成，要么不完成。原子操作的一个例子可能是整数的简单赋值:\n\n```cpp\nnum = 37;\n```\n\n如果两个线程访问这一行代码，它们都不会遇到这种半途而废的情况。换句话说，作业之间没有间隙。当然，如果`num`用用户定义的赋值运算符表示一个复杂的对象，同样的语句可能会有很多空白。\n\nAn atomic operation is an indivisible operation.\n\n另一方面，一个非原子的操作可能会被视为半途而废。经典的例子是我们前面讨论的增量操作。在 C++ 中，对原子类型的所有操作也是原子的。这意味着我们可以通过使用原子类型来避免行间的空白。在使用原子之前，我们可以通过使用互斥来创建原子操作。例如，我们可以考虑以下原子函数:\n\n```cpp\nvoid foo()\n{\n  mutex.lock();\n  int a{41};\n  int b{a + 1};\n  mutex.unlock();\n}\n```\n\n真正的原子操作和我们刚才做的假操作的区别在于原子操作不需要锁。这实际上是一个很大的区别，因为同步机制，如互斥体，包含了开销和性能损失。更准确地说，原子类型利用低级机制来确保指令的独立和原子执行。标准原子类型在`<atomic>`标题中定义。然而，标准原子类型也可能使用内部锁定。为了确保它们不使用内部锁定，标准库中的所有原子类型都公开了`is_lock_free()`函数。\n\nThe only atomic type that doesn't have the `is_lock_free()` member function is `std::atomic_flag`. The operations on this type are required to be lock-free. It's a Boolean flag and most of the time it is used as a base to implement other lock-free types.\n\n也就是说，如果对`obj`的操作是用原子指令直接完成的，则`obj.is_lock_free()`返回`true`。如果返回 false，则表示使用了内部锁定。还有:如果原子类型对所有支持的硬件都是无锁的，则`static constexpr`功能`is_always_lock_free()`返回`true`。由于函数是`constexpr`，它允许我们在编译时定义类型是否是无锁的。这是一个很大的进步，并且很好地影响了代码的组织和执行。例如，`std::atomic<int>::is_always_lock_free()`返回`true`，因为`std::atomic<int>`很可能总是无锁的。\n\nIn Greek, a means not and tomo means cut. The word atom comes from the Greek atomos, which translates to uncuttable. That is, by atomic we consider indivisible smallest units. We use atomic types and operations to avoid gaps between instructions.\n\n我们对原子类型使用专门化，例如，`std::atomic<long>`；但是，您可以参考下表来获得原子类型的更方便的名称。表的左侧列包含原子类型，右侧列包含其专门化:\n\n| **原子型** | **专精** |\n| `atomic_bool` | `std::atomic<bool>` |\n| `atomic_char` | `std::atomic<char>` |\n| `atomic_schar` | `std::atomic<signed char>` |\n| `atomic_uchar` | `std::atomic<unsigned char>` |\n| `atomic_int` | `std::atomic<int>` |\n| `atomic_uint` | `std::atomic<unsigned>` |\n| `atomic_short` | `std::atomic<short>` |\n| `atomic_ushort` | `std::atomic<unsigned short>` |\n| `atomic_long` | `std::atomic<long>` |\n| `atomic_ulong` | `std::atomic<unsigned long>` |\n| `atomic_llong` | `std::atomic<long long>` |\n| `atomic_ullong` | `std::atomic<unsigned long long>` |\n| `atomic_char16_t` | `std::atomic<char16_t>` |\n| `atomic_char32_t` | `std::atomic<char32_t>` |\n| `atomic_wchar_t` | `std::atomic<wchar_t>` |\n\n上表代表基本原子类型。常规类型和原子类型之间的根本区别是我们可以对它们应用的操作类型。现在让我们更详细地讨论原子操作。\n\n# 对原子类型的操作\n\n回想一下我们在上一节中讨论的差距。原子类型的目标是要么消除指令之间的间隙，要么提供负责将几条指令组合在一起并包装成一条指令的操作。以下是对原子类型的操作:\n\n*   `load()`\n*   `store()`\n*   `exchange()`\n*   `compare_exchange_weak()`\n*   `compare_exchange_strong()`\n*   `wait()`\n*   `notify_one()`\n*   `notify_all()`\n\n`load()`操作自动加载并返回原子变量的值。`store()`用提供的非原子参数自动替换原子变量的值。\n\n`load()`和`store()`都类似于非原子变量的常规读取和赋值操作。每当我们访问一个对象的值时，我们就执行一个读指令。例如，以下代码打印了`double`变量的内容:\n\n```cpp\ndouble d{4.2}; // \"store\" 4.2 into \"d\"\nstd::cout << d; // \"read\" the contents of \"d\"\n```\n\n在原子类型的情况下，类似的读取操作转换为:\n\n```cpp\natomic_int m;\nm.store(42);             // atomically \"store\" the value\nstd::cout << m.load();   // atomically \"read\" the contents \n```\n\n虽然前面的代码没有任何意义，但是我们包含了这个例子来表示处理原子类型的差异。访问原子变量应该通过原子操作来完成。以下代码表示`load()`、`store()`和`exchange()`功能的定义:\n\n```cpp\nT load(std::memory_order order = std::memory_order_seq_cst) const noexcept;\nvoid store(T value, std::memory_order order = \n            std::memory_order_seq_cst) noexcept;\nT exchange(T value, std::memory_order order = \n            std::memory_order_seq_cst) noexcept;\n```\n\n可以看到，还有一个名为`order`的类型为`std::memory_order`的附加参数。我们将很快描述它。`exchange()`函数由`store()`和`load()`函数组成，以原子方式用提供的参数替换该值，并原子方式获得前一个值。\n\n`compare_exchange_weak()`和`compare_exchange_strong()`功能的工作原理相似。以下是它们的定义:\n\n```cpp\nbool compare_exchange_weak(T& expected_value, T target_value, \n                           std::memory_order order = \n                            std::memory_order_seq_cst) noexcept;\nbool compare_exchange_strong(T& expected_value, T target_value,\n                            std::memory_order order =\n                             std::memory_order_seq_cst) noexcept;\n```\n\n他们将第一个参数(`expected_value`)与原子变量进行比较，如果它们相等，则用第二个参数(`target_value`)替换该变量。否则，它们会自动将值加载到第一个参数中(这就是它被引用传递的原因)。弱交易所和强交易所的区别在于`compare_exchange_weak()`被允许虚假失败(称为**虚假失败**，也就是说，即使`expected_value`等于基础值，函数也会将它们视为不相等。这样做是因为在某些平台上，它可以提高性能。\n\n从 C++ 20 开始增加了`wait()`、`notify_one()`、`notify_all()`功能。`wait()`函数阻塞线程，直到原子对象的值修改。它需要一个参数来与原子对象的值进行比较。如果这些值相等，它将阻塞线程。要手动解锁线程，我们可以调用`notify_one()`或`notify_all()`。两者的区别在于`notify_one()`至少解除一个被阻塞的操作，而`notify_all()`解除所有这样的操作。\n\n现在，让我们讨论一下在前面声明的原子类型成员函数中遇到的内存顺序。`std::memory_order`定义围绕原子操作的内存访问顺序。当多个线程同时读取和写入变量时，一个线程可以以不同于另一个线程存储它们的顺序读取这些变化。原子操作的默认顺序是顺序一致的顺序，这就是`std::memory_order_seq_cst`的作用。订单有几种类型，包括`memory_order_relaxed`、`memory_order_consume`、`memory_order_acquire`、`memory_order_release`、`memory_order_acq_rel`和`memory_order_seq_cst`。在下一节中，我们将设计一个使用原子类型和默认内存顺序的无锁堆栈。\n\n# 设计无锁堆栈\n\n设计堆栈时要记住的一个关键点是确保推送的值可以安全地从另一个线程返回。同样重要的是确保只有一个线程返回值。\n\n在前几节中，我们实现了一个基于锁的堆栈，它包装了`std::stack`。我们知道堆栈不是真正的数据结构，而是适配器。通常，在实现堆栈时，我们选择向量或链表作为其底层数据结构。让我们看一个基于链表的无锁栈的例子。将新元素推入堆栈包括创建一个新的列表节点，将其`next`指针设置为当前的`head`节点，然后将`head`节点设置为指向新插入的节点。\n\nIf you are confused by the terms head or next pointer, revisit [Chapter 6](06.html), *Digging into Data Structures and Algorithms in STL*, where we discussed linked lists in detail.\n\n在单线程环境中，所描述的步骤很好；但是，如果有多个线程修改堆栈，我们应该开始担心了。让我们找到`push()`操作的陷阱。当一个新元素被推入堆栈时，有三个主要步骤:\n\n1.  `node* new_elem = new node(data);`\n2.  `new_elem->next = head_;`\n3.  `head_ = new_elem;`\n\n在第一步中，我们声明将要插入到基础链表中的新节点。第二步描述我们将它插入到列表的前面——这就是为什么新节点的`next`指针指向`head_`。最后，由于`head_`指针代表列表的起点，我们应该重置它的值以指向新添加的节点，如步骤 3 中所做的。\n\n节点类型是我们在堆栈中用来表示列表节点的内部结构。以下是它的定义:\n\n```cpp\ntemplate <typename T>\nclass lock_free_stack\n{\nprivate:\n struct node {\n T data;\n node* next;\n node(const T& d) : data(d) {}\n }  node* head_;\n// the rest of the body is omitted for brevity\n};\n```\n\n我们建议您做的第一件事是在代码中寻找间隙——不是在前面的代码中，而是在我们描述的将新元素推入堆栈的步骤中。仔细看看。假设两个线程同时添加节点。步骤 2 中的一个线程设置新元素的下一个指针指向`head_`。另一个线程使`head_`指向另一个新元素。很明显，这可能会导致数据损坏。对于第 2 步和第 3 步来说，一个线程拥有相同的`head_`是至关重要的。为了解决步骤 2 和 3 之间的竞争情况，我们应该使用原子比较/交换操作来保证`head_`在我们之前读取其值时没有被修改。由于我们需要原子地访问头部指针，下面是我们如何修改`lock_free_stack`类中的`head_ member`:\n\n```cpp\ntemplate <typename T>\nclass lock_free_stack\n{\nprivate:\n  // code omitted for brevity\n std::atomic<node*> head_;  // code omitted for brevity\n};\n```\n\n以下是我们如何围绕原子`head_`指针实现无锁`push()`:\n\n```cpp\nvoid push(const T& data)\n{\n  node* new_elem = new node(data);\n  new_elem->next = head_.load();\n  while (!head_.compare_exchange_weak(new_elem->next, new_elem));\n}\n```\n\n我们使用`compare_exchange_weak()`来确保`head_`指针的值与我们存储在`new_elem->next`中的值相同。如果是，我们设置为`new_elem`。一旦`compare_exchange_weak()`成功，我们确定该节点已经成功插入到列表中。\n\n看看我们如何通过使用原子操作来访问节点。类型为`T` - `std::atomic<T*>`的指针的原子形式提供了相同的接口。除此之外，`std::atomic<T*>`还提供了指向算术运算`fetch_add()`和`fetch_sub()`的指针。他们对存储的地址进行原子加法和减法运算。这里有一个例子:\n\n```cpp\nstruct some_struct {};\nany arr[10];\nstd::atomic<some_struct*> ap(arr);\nsome_struct* old = ap.fetch_add(2);\n// now old is equal to arr\n// ap.load() is equal to &arr[2]\n```\n\n我们有意将指针命名为`old`，因为`fetch_add()`将数字加到指针的地址上，并返回`old`值。这就是为什么`old`指向的地址与`arr`指向的地址相同。\n\n在下一节中，我们将介绍更多关于原子类型的操作。现在，让我们回到我们的无锁堆栈。到`pop()`一个元素，也就是去掉一个节点，我们需要读取`head_`设置到`head_`的下一个元素，如下图:\n\n```cpp\nvoid pop(T& popped_element)\n{\n  node* old_head = head_;\n  popped_element = old_head->data;\n  head_ = head_->next;\n  delete old_head;\n}\n```\n\n现在，好好看看前面的代码。想象几个线程同时执行它。如果从堆栈中移除项目的两个线程读取相同的`head_`值会怎么样？这一点和其他一些比赛条件使我们实现了以下目标:\n\n```cpp\nvoid pop(T& popped_element)\n{\n  node* old_head = head_.load();\n  while (!head_.compare_exchange_weak(old_head, old_head->next));\n  popped_element = old_head->data;\n}\n```\n\n我们在前面的代码中应用了与`push()`函数几乎相同的逻辑。前面的代码并不完美；它应该得到加强。我们建议您努力修改它以消除内存泄漏。\n\n我们已经看到，无锁实现非常依赖原子类型和操作。我们在上一节中讨论的操作不是最终的。现在让我们发现更多的原子操作。\n\n# 更多原子操作\n\n在前一节中，我们在指向用户定义类型的指针上使用了`std::atomic<>`。也就是说，我们为列表节点声明了以下结构:\n\n```cpp\n// the node struct is internal to \n// the lock_free_stack class defined above\nstruct node\n{\n  T data;\n  node* next;\n};\n```\n\n节点结构是用户定义的类型。虽然在上一节中我们实例化了`std::atomic<node*>`，但是以同样的方式，我们可以为几乎任何用户定义的类型实例化`std::atomic<>`，也就是`std::atomic<T>`。但是需要注意的是`std::atomic<T>`的界面仅限于以下功能:\n\n*   `load()`\n*   `store()`\n*   `exchange()`\n*   `compare_exchange_weak()`\n*   `compare_exchange_strong()`\n*   `wait()`\n*   `notify_one()`\n*   `notify_all()`\n\n现在，让我们根据底层类型的具体情况，查看原子类型上可用操作的完整列表。\n\n`std::atomic<>`用整数类型(如整数或指针)实例化，除了前面列出的操作外，还有以下操作:\n\n*   `fetch_add()`\n*   `fetch_sub()`\n*   `fetch_or()`\n*   `fetch_and()`\n*   `fetch_xor()`\n\n另外，除了增量(`++ `)和减量(`--`)外，还有以下运算符:`+=`、`-=`、`|=`、`&=`和`^=`。\n\n最后，还有一种称为`atomic_flag`的特殊原子类型，有两种可用操作:\n\n*   `clear()`\n*   `test_and_set()`\n\n你应该考虑一下原子操作。`clear()`功能清除，而`test_and_set()`将值更改为`true`并返回前一个值。\n\n# 摘要\n\n在本章中，我们介绍了一个相当简单的堆栈设计示例。还有更复杂的例子需要研究和效仿。当我们讨论设计并发堆栈时，我们看了两个版本，其中一个代表无锁堆栈。与基于锁的解决方案相比，无锁数据结构和算法是程序员的最终目标，因为它们提供了避免数据竞争的机制，甚至无需同步资源。\n\n我们还介绍了原子类型和操作，您可以在项目中使用它们来确保指令不可分割。正如您已经知道的，如果一个指令是原子的，就不需要担心它的同步。我们强烈建议您继续研究该主题，并构建更加健壮和复杂的无锁数据结构。在下一章中，我们将看到如何设计全球通用的应用。\n\n# 问题\n\n1.  为什么我们要在多线程单例实现中检查两次实例？\n2.  在基于锁的堆栈的复制构造函数的实现中，我们锁定了另一个堆栈的互斥体。为什么呢？\n3.  什么是原子类型和原子操作？\n4.  为什么我们对原子类型使用`load()`和`store()`？\n5.  `std::atomic<T*>`支持哪些附加操作？\n\n# 进一步阅读\n\n*   *并发模式和最佳实践，作者:阿图尔·Khot*，网址:\n*   *Maya Posch*掌握 C++ 多线程，位于[https://www . packtpub . com/application-development/Mastering-C-多线程](https://www.packtpub.com/application-development/mastering-c-multithreading)"
  },
  {
    "path": "docs/exp-cpp/10.md",
    "content": "# 十、设计全球通用的应用\n\n在生产就绪项目中使用编程语言是学习语言本身的一个全新步骤。有时，本书中的简单例子可能会采取不同的方法，或者在现实世界的程序中面临许多困难。当理论遇到实践时，就是你学习语言的时候。C++ 也不例外。学习语法，解决一些书本上的问题，或者理解书本上有些简单的例子是不同的。当创建现实世界的应用时，我们面临不同范围的挑战，有时书籍缺乏理论来支持路上的实际问题。\n\n在本章中，我们将尝试用 C++ 介绍实用编程的基础知识，这将帮助您更好地处理现实世界的应用。复杂的项目需要大量的思考和设计。有时候，程序员不得不完全重写项目，从头开始，只是因为他们在开发之初做出了错误的设计选择。本章试图阐明软件设计的过程。您将学习如何为项目构建更好的架构。\n\n我们将在本章中讨论以下主题:\n\n*   了解项目开发生命周期\n*   设计模式及其应用\n*   领域驱动设计\n*   设计一个亚马逊克隆作为一个真实项目的例子\n\n# 技术要求\n\n带有`-std=c++ 2a`选项的 g++ 编译器用于编译本章中的示例。你可以在[https://github.com/PacktPublishing/Expert-CPP](https://github.com/PacktPublishing/Expert-CPP)找到本章使用的源文件。\n\n# 项目开发生命周期\n\n无论何时处理问题，都应该仔细考虑需求分析的过程。项目开发中最大的错误之一是在没有彻底分析问题本身的情况下就开始编码。\n\n想象一下，你的任务是创建一个计算器，一个允许用户对数字进行算术计算的简单工具。假设你神奇地按时完成了项目并发布了程序。现在，用户开始使用你的计算器，他们迟早会发现他们的计算结果没有超过整数的最大值。当他们抱怨这个问题时，你准备好用坚实的编码支持的论据来为自己(和你的创造)辩护，比如因为在计算中使用了`int`数据类型。对你和你的程序员同事来说，这完全可以理解，但最终用户就是不能接受你的论点。他们想要一个工具，允许求和一些足够大的数字，否则，他们根本不会使用你的程序。你开始在你的计算器的下一个版本上工作，这一次，你使用 longs 甚至定制实现的大数字。当你突然意识到同样的用户抱怨没有找到对数或数字指数的功能时，你自豪地将你的程序发送给等待他们鼓掌的用户。这似乎令人望而生畏，因为可能会有越来越多的功能请求和越来越多的投诉。\n\n虽然这个例子有点简单，但它完全涵盖了现实世界中通常发生的事情。甚至当你为你的程序实现了所有的功能，并且正在考虑休一个应得的长假时，用户也会开始抱怨程序中的错误。事实证明，有几种情况下，您的计算器会出现意外行为，并且不会给出或给出错误的结果。迟早，你会意识到，在向大众发布程序之前，真正需要的是适当的测试。\n\n我们将讨论在实际项目中应该考虑的主题。无论何时开始新项目，都应该考虑以下步骤:\n\n1.  需求收集和分析\n2.  规范创建\n\n3.  设计和测试计划\n4.  编码\n5.  测试和稳定\n6.  发布和维护\n\n前面的步骤并不是对每个项目都进行硬编码，尽管这可能被认为是每个软件开发团队为了成功发布产品而应该完成的最起码的工作。实际上，大多数步骤都被省略了，这是因为 IT 领域的每个人都最缺少的一件事——时间。但是，强烈建议遵循前面的步骤，因为从长远来看，最终会节省更多的时间。\n\n# 需求收集和分析\n\n这是创造稳定产品最关键的一步。程序员未能按时完成任务或在代码中留下大量 bug 的最常见原因之一是缺乏对项目的完整理解。\n\n领域知识非常重要，在任何情况下都不应该被忽略。你可能会幸运地开发与你非常熟悉的事物相关的项目。然而，你应该考虑到并不是每个人都像你一样幸运(嗯，你可能也有那么不幸)。\n\n想象一下，你正在做一个项目，自动分析和报告某个公司的股票交易。现在假设你对股票和股票交易一无所知。你不知道熊市或牛市，不知道交易交易的局限性，等等。你将如何成功完成这个项目？\n\n即使你知道股票市场和交易，你可能不知道你的下一个大项目领域。如果你的任务是设计和实施(有或没有团队)一个控制你所在城市气象站的项目会怎么样？开始项目时，你打算先做什么？\n\n您肯定应该从需求收集和分析开始。这只是一个过程，包括与客户沟通，并就项目提出许多问题。如果你不是和任何客户打交道，而是在一家产品公司工作，那么项目经理应该被视为客户。即使这个项目是你的主意，你是一个人在工作，你也应该把自己当成客户，尽管这听起来很荒谬，但还是要问自己很多问题(关于这个项目)。\n\n让我们假设我们要征服电子商务，并希望发布一种最终能在自己的业务中击败市场鲨鱼的产品。受欢迎和成功的电子商务市场是亚马逊、易贝、阿里巴巴和其他一些公司。我们应该把这个问题表述为*编写自己的亚马逊克隆*。我们应该做什么来收集项目的需求？\n\n首先，我们应该列出所有我们应该实现的特性，然后我们将确定优先级。例如，对于亚马逊克隆项目，我们可能会提出以下功能列表:\n\n*   创建产品。\n*   列出产品。\n*   购买产品。\n*   编辑产品详细信息。\n*   移除产品。\n*   按名称、价格范围和重量搜索产品。\n*   偶尔通过电子邮件提醒用户产品的可用性。\n\n应尽可能详细地描述特征；这将为开发人员(在这种情况下是你)解决问题。例如，创建产品应该由项目管理员或任何用户来完成。如果用户可以创建一个产品，它应该有限制，如果有的话。可能会有这样的情况，用户会错误地在我们的系统中创建数百个产品来增加他们唯一产品的可见性。\n\n在与客户沟通时，应陈述、讨论并最终确定细节。如果项目中只有你一个人，你是项目的客户，沟通就是*自己思考*项目需求的过程。\n\n当完成需求获取后，我们建议对每个特性进行优先级排序，并将它们分类为以下类别之一:\n\n*   必须有\n*   应该有\n*   很高兴有\n\n稍微思考一下并对前面的特性进行分类后，我们可以得出以下列表:\n\n*   创建产品[必须有]。\n*   列出产品[必须有]。\n*   购买产品[必须拥有]。\n\n*   编辑产品详细信息[应有]。\n*   移除产品[必须有]。\n*   按名称搜索产品[必须有]。\n*   按价格范围[应有]搜索产品。\n*   按重量搜索产品[很高兴拥有]。\n*   偶尔通过电子邮件提醒用户产品的可用性[很高兴拥有]。\n\n分类会让你对从哪里开始有一个基本的概念。程序员是贪婪的人；他们希望为他们的产品实现所有可能的特性。这肯定会失败。你应该先从最基本的特性开始——这就是为什么我们有几个很好拥有的特性。一些人开玩笑地坚持认为，应该将“很好拥有”的功能改名为“从来没有”的功能，因为在实践中，它们从来没有实现过。\n\n# 规范创建\n\n不是每个人都喜欢创建规范。大多数程序员讨厌这一步，因为它不是编码，而是写作。\n\n在收集项目需求之后，您应该创建一个文档，其中包括描述您的项目的每个细节。该规范有许多名称和类型。可以称之为**项目需求文档** ( **珠三角**)**功能规范**、**开发规范**等等。作为需求分析的结果，认真的程序员和团队会产生一个 PRD。这些严肃的人的下一步是创建功能规范以及开发规范等等。我们在一个名为**规范创建的步骤中合并了所有的文档。**\n\n由您和您的团队决定是否需要前面提到的任何子文档。更好的方法是有产品的视觉表示，而不是文本文档。无论您的文档采取什么形式，它都应该仔细地表示您在需求收集步骤中所取得的成果。为了对此有一个基本的了解，让我们试着记录一些我们之前收集的特性(我们将我们的项目称为*平台)*\n\n*   创建产品。具有管理员权限的平台用户可以创建产品。\n*   平台必须允许创建具有定义权限的用户。此时，应该有两种类型的用户，即普通用户和管理员用户。\n\n*   任何使用该平台的用户都必须能够看到可用产品的列表。\n*   产品应该有图片、价格、名称、重量和描述。\n*   为了购买产品，用户提供他们的卡的细节来兑现和产品装运的细节。\n*   每个注册用户都应该提供送货地址、信用卡详细信息和电子邮件帐户。\n\n清单可能会很长，实际上应该很长，因为清单越长，开发人员就越了解项目。\n\n# 设计和测试计划\n\n虽然我们坚持认为需求收集步骤是软件开发中最关键的，但是设计和测试计划也可以被认为是同样关键的步骤。如果你曾经没有先设计就开始了一个项目，你已经有了不可能的想法。尽管激励名言坚持认为没有什么是不可能的，但程序员确信至少有一件事是不可能的，那就是不先设计就成功完成一个项目。\n\n设计的过程是最有趣的一步；它迫使我们思考，画画，再思考，理清一切，重新开始。项目的许多特征是在设计时发现的。要设计一个项目，你应该从头开始。首先，列出项目中涉及的所有实体和过程。对于亚马逊克隆示例，我们可以列出以下实体和流程:\n\n*   用户\n*   注册和授权\n*   制品\n*   处理\n*   仓库(包含产品)\n*   装运\n\n这是一个高层次的设计——最终设计的起点。在本章中，我们将主要集中在项目的设计上。\n\n# 分解实体\n\n在列出关键实体和过程之后，我们开始将它们分解成更详细的实体，这些实体稍后将被转换成类。项目的设计草图更好。只需绘制包含实体名称的矩形，如果它们以某种方式连接在一起或者是同一个过程的一部分，则用箭头将它们连接起来。如果有一个过程包含实体 A 或由实体 A 开始，并在实体 B 完成或导致实体 B，您可以从实体 A 到实体 B 开始一个箭头。无论绘图有多好，这都是更好地理解项目的必要步骤。例如，请看下图:\n\n![](img/747174ee-fe32-4e39-930e-9add32033617.png)\n\n将实体和过程分解成类以及它们的相互通信是一门微妙的艺术，需要耐心和一致性。例如，让我们尝试为**用户**实体添加详细信息。如规范创建步骤中所述，注册用户应该提供递送地址、电子邮件地址和信用卡详细信息。让我们画一个代表用户的类图:\n\n![](img/62615d23-0f68-42b3-914a-380356d7ba16.png)\n\n有趣的问题来了:我们应该如何处理实体中包含的复杂类型？例如，用户的传递地址是一个复杂的类型。这不可能只是`string`，因为迟早我们可能需要根据用户的送货地址对他们进行分类，以做出最佳的发货。例如，如果用户的交货地址与包含所购产品的仓库地址位于不同的国家，则运输公司可能会让我们(或用户)损失一大笔钱。这是一个很好的场景，因为它引入了一个新问题，更新了我们对项目的理解。事实证明，我们应该处理用户订购的产品被分配到远离用户的特定仓库的情况。如果我们有许多仓库，我们应该选择离用户最近的包含所需产品的仓库。这些都是无法马上回答的问题，但这是设计项目的质量结果。否则，这些问题会在编码过程中出现，我们会比我们想象的更长时间地陷入其中。在任何已知的宇宙中，对该项目的初步估计都不会达到它的完成日期。\n\n现在，您将如何在`User`类中存储用户地址？简单的`std::string`就可以了，如下例所示:\n\n```cpp\nclass User\n{\npublic:\n  // code omitted for brevity\nprivate:\n  std::string address_;\n  // code omitted for brevity\n};\n```\n\n就其组成而言，地址是一个复杂的对象。地址可能由国家名称、国家代码、城市名称和街道名称组成，甚至可能包含纬度和经度。如果您需要找到离用户最近的仓库，后者非常好。制造更多的类型，让程序员的设计更加直观，这完全没问题。例如，以下结构可能非常适合表达用户的地址:\n\n```cpp\nstruct Address\n{\n  std::string country;\n  std::string city;\n  std::string street;\n  float latitude{};\n  float longitude{};\n};\n```\n\n现在，存储用户地址变得更加简单:\n\n```cpp\nclass User\n{\n  // code omitted for brevity\n  Address address_;\n}; \n```\n\n我们将在本章后面回到这个例子。\n\n设计项目的过程可能需要回到几个步骤来重申项目需求。在用前面的步骤阐明设计步骤之后，我们可以继续将项目分解成更小的组件。最好也创建交互图。\n\n像下面这样的交互图将描述诸如由**用户**进行的交易到**购买**产品的操作:\n\n![](img/0fc423b9-d3fb-4643-abb0-aadd1f55632b.png)\n\n测试计划也可以被认为是设计的一部分。它包括计划如何测试最终应用。例如，在此之前的步骤包括一个地址的概念，结果是，地址可以包含国家、城市等等。适当的测试应该包括检查是否可以在用户地址中成功设置国家值。虽然测试计划通常不被认为是程序员的任务，但是为项目做测试仍然是一个很好的实践。一个合适的测试计划会在设计项目时产生更多有用的信息。大多数输入数据处理和安全检查都是在测试计划中发现的。例如，在进行需求分析或编写功能规范时，对用户名或电子邮件地址设置严格的限制可能不是这样。测试计划关注这样的场景，并迫使开发人员负责数据检查。然而，大多数程序员都急于进入项目开发的下一步，即编码。\n\n# 编码\n\n如前所述，编码不是项目开发的唯一部分。在编码之前，您应该通过利用规范中预测的所有需求来仔细设计您的项目。在项目开发的前几个步骤彻底完成后，编码会容易得多，效率也会更高。\n\n一些团队实践**测试驱动开发(TDD)** ，这是产生更稳定的项目发布的一个很好的方法。TDD 的主要概念是在项目实施之前编写测试。这是程序员定义项目需求和回答开发过程中出现的进一步问题的好方法。\n\n假设我们正在为`User`类实现设置器。用户对象包含前面讨论的电子邮件字段，这意味着我们应该有一个`set_email()`方法，如下面的代码片段所示:\n\n```cpp\nclass User\n{\npublic:\n  // code omitted for brevity\n  void set_email(const std::string&);\n\nprivate: \n  // code omitted for brevity\n  std::string email_;\n};\n```\n\nTDD 方法建议在实现`set_email()`方法本身之前，为`set_email()`方法编写一个测试函数。假设我们有以下测试功能:\n\n```cpp\nvoid test_set_email()\n{\n  std::string valid_email = \"valid@email.com\";\n  std::string invalid_email = \"112%$\";\n  User u;\n  u.set_email(valid_email);\n  u.set_email(invalid_email);\n}\n```\n\n在前面的代码中，我们声明了两个`string`变量，其中一个变量包含一个无效的电子邮件地址值。甚至在运行测试函数之前，我们就知道，在数据输入无效的情况下，`set_email()`方法应该有所反应。常见的方法之一是抛出一个指示无效输入的异常。您也可以忽略`set_email`实现中的无效输入，并返回一个表示操作成功的`boolean`值。错误处理应该在项目中保持一致，并得到所有团队成员的同意。让我们考虑一下，我们将抛出一个异常，因此，当向方法传递一个无效值时，测试函数应该会得到一个异常。\n\n前面的代码应该重写，如下所示:\n\n```cpp\nvoid test_set_email()\n{\n  std::string valid_email = \"valid@email.com\";\n  std::string invalid_email = \"112%$\";\n\n  User u;\n  u.set_email(valid_email);\n  if (u.get_email() == valid_email) {\n    std::cout << \"Success: valid email has been set successfully\" << std::endl;\n  } else {\n    std::cout << \"Fail: valid email has not been set\" << std::endl;\n  }\n\n  try {\n    u.set_email(invalid_email);\n    std::cerr << \"Fail: invalid email has not been rejected\" << std::endl;\n  } catch (std::exception& e) {\n    std::cout << \"Success: invalid email rejected\" << std::endl;\n  }\n}\n```\n\n测试功能似乎完成了。每当我们运行测试函数时，它都会输出`set_email()`方法的当前状态。即使我们还没有实现`set_email()`功能，相应的测试功能也是向其实现细节迈进了一大步。我们现在有了函数应该如何对有效和无效数据输入做出反应的基本概念。我们可以添加更多种类的数据，以确保`set_email()`方法在其实现完成后将得到彻底测试。例如，我们可以用空字符串和长字符串来测试它。\n\n以下是`set_email()`方法的初步实现:\n\n```cpp\n#include <regex>\n#include <stdexcept>\n\nvoid User::set_email(const std::string& email)\n{\n  if (!std::regex_match(email, std::regex(\"(\\\\w+)(\\\\.|_)?(\\\\w*)@(\\\\w+)(\\\\.(\\\\w+))+\")) {\n    throw std::invalid_argument(\"Invalid email\");\n  }\n\n  this->email_ = email;\n}\n```\n\n在方法的初始实现之后，我们应该再次运行我们的测试函数，以确保实现符合定义的测试用例。\n\nWriting tests for your project is considered as a good coding practice. There are different types of tests, such as unit tests, regression tests, smoke tests, and so on. Developers should support unit test coverage for their projects.\n\n编码的过程是项目开发生命周期中最混乱的步骤之一。很难估计一个类或其方法的实现需要多长时间，因为大多数问题和困难都是在编码过程中出现的。本章开头描述的项目开发生命周期的前几个步骤倾向于涵盖大多数这些问题，并简化编码过程。\n\n# 测试和稳定\n\n项目完成后，应该进行适当的测试。通常，软件开发公司都有**质量保证** ( **QA** )工程师对项目进行一丝不苟的测试。\n\n在测试阶段验证的问题被转换成分配给程序员的相应任务来解决它们。问题可能会影响项目的发布，也可能被归类为次要问题。\n\n程序员的基本任务不是立即修复问题，而是找到问题的根本原因。为了简单起见，我们来看看`generate_username()`函数，它使用随机数结合电子邮件生成用户名:\n\n```cpp\nstd::string generate_username(const std::string& email)\n{\n  int num = get_random_number();\n  std::string local_part = email.substr(0, email.find('@'));\n  return local_part + std::to_string(num);\n}\n```\n\n`generate_username()`函数调用`get_random_number()`将返回值与电子邮件地址的本地部分相结合。本地部分是电子邮件地址中`@`符号之前的部分。\n\n质量保证工程师报告说，电子邮件本地部分的附加号码总是相同的。例如，对于电子邮件`john@gmail.com`，生成的用户名为`john42`，对于`amanda@yahoo.com`，生成的用户名为`amanda42`。因此，下一次有电子邮件`amanda@hotmail.com`的用户试图在系统中注册时，生成的用户名`amanda42`与已经存在的用户名冲突。对于测试人员来说，不知道项目的实现细节是完全可以的，所以他们将其报告为用户名生成功能中的一个问题。虽然您可能已经猜到真正的问题隐藏在`get_random_number()`功能中，但在某些情况下，问题总是在没有找到根本原因的情况下得到解决。解决问题的错误方法可能会改变`generate_username()`函数的实现。`generate_random_number()`功能也可能用于其他功能，这反过来会使所有调用`get_random_number()`的功能无法正常工作。虽然这个例子很简单，但深入思考并找到问题背后的真正原因是至关重要的。这种方法将在未来节省大量时间。\n\n# 发布和维护\n\n在通过修复所有关键和主要问题使项目稍微稳定之后，它就可以发布了。有时候，公司发布软件时会贴上“T0”测试版“T1”的标签，这样就为用户提供了一个借口，以防他们发现软件有问题。需要注意的是，很少有软件能够完美工作。发布之后，会出现更多的问题。因此，维护阶段就来了，这时开发人员正在进行修复和发布更新。\n\n程序员有时开玩笑说，发布和维护是永远无法实现的步骤。然而，如果你花足够的时间设计这个项目，发布它的第一个版本不会花太多时间。正如我们在上一节中已经介绍的，设计从需求收集开始。之后，我们花时间定义实体，分解它们，分解成更小的组件，编码，测试，最后发布它。作为开发人员，我们对设计和编码阶段更感兴趣。正如已经提到的，一个好的设计选择对进一步的项目开发有很大的影响。现在让我们仔细看看整体设计过程。\n\n# 深入设计过程\n\n如前所述，项目设计从列出一般实体开始，如设计电子商务平台时的用户、产品和仓库:\n\n![](img/b8991f30-493e-4e8f-84d7-02f1b58ed92e.png)\n\n然后我们将每个实体分解成更小的组件。为了使事情更清楚，将每个实体视为一个单独的类。当把一个实体看作一个类时，它在分解方面更有意义。例如，我们将`user`实体表示为一个类:\n\n```cpp\nclass User\n{\npublic:\n  // constructors and assignment operators are omitted for code brevity\n  void set_name(const std::string& name);\n  std::string get_name() const;\n  void set_email(const std::string&);\n  std::string get_email() const;\n  // more setters and getters are omitted for code brevity\n\nprivate:\n  std::string name_;\n  std::string email_;\n  Address address_;\n  int age;\n};\n```\n\n`User`类的类图如下:\n\n![](img/bd7b14fe-6cee-47bd-be57-e54cd658200b.png)\n\n然而，正如我们已经讨论过的那样，`User`类的地址字段可能被表示为一个单独的类型(`class`或`struct`，这还不重要)。无论是数据聚合还是复杂类型，类图都会进行以下更改:\n\n![](img/a62f3623-ed77-4cca-9d29-fffe8c9a0bfc.png)\n\n这些实体之间的关系将在设计过程中变得清晰。例如，**地址**本身不是一个实体，它是**用户**的一部分，也就是说，如果一个**用户**对象没有被实例化，它就不能有一个实例。然而，由于我们可能希望使用可重用代码，所以**地址**类型也可以用于仓库对象。也就是说**用户**和**地址**之间的关系是简单的聚合，而不是组合。\n\n展望未来，我们可以在讨论支付选项时对**用户**类型提出更多要求。该平台的用户应该能够插入支付产品的选项。在决定我们将如何在`User`类中表示支付选项之前，我们应该弄清楚那些选项到底是什么。让我们保持简单，假设支付选项包含信用卡号、持卡人姓名、到期日期和卡的安全代码。这听起来像是另一种数据聚合，所以让我们在一个结构中收集所有这些，如下所示:\n\n```cpp\nstruct PaymentOption\n{\n  std::string number;\n  std::string holder_name;\n  std::chrono::year_month expiration_date;\n  int code;\n};\n```\n\n注意前面结构中的`std::chrono::year_month`；它代表特定年份的特定月份，在 C++ 20 中引入。大多数支付卡只携带卡到期的月份和年份，所以这个`std::chrono::year_month`功能非常适合`PaymentOption`。\n\n所以，在设计`User`类的过程中，我们想出了一个新的类型，`PaymentOption`。一个用户可以有多个支付选项，所以`User`和`PaymentOption`的关系是一对多。现在让我们用这个新的聚合来更新我们的`User`类图(尽管在这种情况下我们使用合成):\n\n![](img/c85c2d3a-0d3f-4143-8879-b5cf71ef29dd.png)\n\n`User`和`PaymentOption`之间的依赖关系用下面的代码表示:\n\n```cpp\nclass User\n{\npublic:\n  // code omitted for brevity\n  void add_payment_option(const PaymentOption& po) {\n    payment_options_.push_back(op);\n  }\n\n  std::vector get_payment_options() const {\n    return payment_options_;\n  }\nprivate:\n  // code omitted for brevity\n  std::vector<PaymentOption> payment_options_;\n};\n```\n\n我们应该注意，即使用户可能设置了多个支付选项，我们也应该将其中一个标记为主要选项。这很棘手，因为我们可以将所有选项存储在一个向量中，但现在我们必须将其中一个选项作为主要选项。\n\n我们可以使用一对或`tuple`(如果喜欢的话)将向量中的一个选项映射为`boolean`值，指示它是否是主要的。下面的代码描述了前面介绍的`User`类中元组的用法:\n\n```cpp\nclass User\n{\npublic:\n  // code omitted for brevity\n  void add_payment_option(const PaymentOption& po, bool is_primary) {\n    payment_options_.push_back(std::make_tuple(po, is_primary));\n  }\n\n  std::vector<std::tuple<PaymentOption, boolean> > get_payment_options() const {\n    return payment_options_;\n  }\nprivate:\n  // code omitted for brevity\n  std::vector<std::tuple<PaymentOption, boolean> > payment_options_;\n};\n```\n\n我们可以通过以下方式利用类型别名来简化代码:\n\n```cpp\nclass User\n{\npublic:\n  // code omitted for brevity\n  using PaymentOptionList = std::vector<std::tuple<PaymentOption, boolean> >;\n\n  // add_payment_option is omitted for brevity\n  PaymentOptionList get_payment_options() const {\n    return payment_options_;\n  }\n\nprivate:\n  // code omitted for brevity\n  PaymentOptionList payment_options_;\n};\n```\n\n以下是班级用户如何为用户检索主要支付选项:\n\n```cpp\nUser john = get_current_user(); // consider the function is implemented and works\nauto payment_options = john.get_payment_options();\nfor (const auto& option : payment_options) {\n  auto [po, is_primary] = option;\n  if (is_primary) {\n    // use the po payment option\n  }\n}\n```\n\n我们在`for`循环中访问元组项时使用了结构化绑定。然而，在学习了关于数据结构和算法的章节之后，你现在意识到搜索主要支付选项是一个线性操作。每次我们需要检索主要支付选项时，遍历向量可能会被认为是一种糟糕的做法。\n\nYou might change the underlying data structure to make things run faster. For example, `std::unordered_map` (that is, a hash table) sounds better. However, it doesn't make things faster just because it has constant-time access to its elements. In this scenario, we should map a `boolean` value to the payment option. For all of the options except one, the `boolean` value is the same falsy value. It will lead to collisions in the hash table, which will be handled by chaining values together mapped to the same hash value. The only benefit of using a hash table will be constant-time access to the primary payment option. \n\n最后，我们得出最简单的解决方案，在类中单独存储主要支付选项。以下是我们应该如何重写`User`类中支付选项的处理部分:\n\n```cpp\nclass User\n{\npublic:\n  // code omitted for brevity\n  using PaymentOptionList = std::vector<PaymentOption>;\n  PaymentOption get_primary_payment_option() const {\n    return primary_payment_option_;\n  }\n\n  PaymentOptionList get_payment_options() const {\n    return payment_options_;\n  }\n\n  void add_payment_option(const PaymentOption& po, bool is_primary) {\n    if (is_primary) {\n      // moving current primary option to non-primaries\n      add_payment_option(primary_payment_option_, false);\n      primary_payment_option_ = po;\n      return;\n    }\n    payment_options_.push_back(po);\n  }\n\nprivate:\n  // code omitted for brevity\n  PaymentOption primary_payment_option_;\n  PaymentOptionList payment_options_;\n};\n```\n\n到目前为止，我们带您完成了定义支付选项存储方式的过程，只是为了展示伴随编码的设计过程。虽然我们已经为单一支付选项创建了许多版本，但这还不是最终版本。在支付选项向量中总是存在处理重复值的情况。每当您向用户添加付款选项作为主要选项，然后再添加另一个选项作为主要选项时，前一个选项会转到非主要列表。如果我们改变主意，再次添加旧的支付选项作为主要选项，它将不会从非主要列表中删除。\n\n所以，总是有机会深入思考，避免潜在的问题。设计和编码齐头并进；然而，你不应该忘记 TDD。在大多数情况下，在编码之前编写测试将帮助您发现大量用例。\n\n# 使用固体原理\n\n在你的项目设计中有很多原则和设计方法可以使用。保持设计简单总是更好的，然而，总的来说，有一些原则在几乎所有的项目中都是有用的。例如， **SOLID** 由五个原则组成，所有或部分原则对设计都是有用的。\n\nSOLID 代表以下原则:\n\n*   单一责任\n*   开-关\n*   利斯科夫替代\n*   界面分离\n*   依赖倒置\n\n让我们用例子来讨论每个原理。\n\n# 单一责任原则\n\n单一责任原则表述简单，即一个目标，一项任务。尽量减少你的对象的功能和它们的关系复杂性。让每个对象都有一个职责，即使把一个复杂的对象分解成更小更简单的组件并不总是容易的。单一责任是一个受环境限制的概念。它不是一个类中只有一个方法；它是关于让类或模块负责一件事。例如，我们之前设计的`User`类有一个职责:存储用户信息。然而，我们在`User`类中增加了支付选项，并强制其具有添加和删除支付选项的方法。我们还引入了一个主要的支付选项，这涉及到**用户**方法中的附加逻辑。我们可以朝两个方向前进。\n\n第一个建议将`User`类分解成两个独立的类。每个班级将有一个单一的责任。下面的类图描述了这个想法:\n\n![](img/711b0c46-dd8c-438a-904c-c1727528681d.png)\n\n其中一个将只存储用户基本信息，下一个将为用户存储支付选项。我们给它们分别命名为`UserInfo`和`UserPaymentOptions`。有些人可能喜欢这个新设计，但我们会坚持旧设计。原因如下。虽然`User`类包含用户信息和支付选项，但后者也代表一条信息。我们设置和获取支付选项的方式与设置和获取用户电子邮件的方式相同。所以，我们保持`User`类不变，因为它已经满足了单一责任原则。当我们在`User`类中添加支付功能时，这将打破平静。在这种情况下，`User`类将存储用户信息并进行支付交易。就单一责任原则而言，这是不可接受的，因此，我们不会这样做。\n\n单一责任原则也与职能有关。`add_payment_option()`方法有两个职责。如果函数的第二个(默认)参数为真，它将添加一个新的主要支付选项。否则，它会将新的付款选项添加到非主要选项列表中。最好有一个单独的方法来添加主要的支付选项。这样，每种方法都有一个单独的职责。\n\n# 开闭原则\n\n开-闭原则规定一个类应该是开放的，可以扩展，但不可以修改。这意味着每当您需要新功能时，最好扩展基本功能，而不是修改它。比如我们设计的电子商务应用的`Product`类。以下是`Product`类的简单示意图:\n\n![](img/7a087189-9c02-4c59-857f-38a419d1299d.png)\n\n每个`Product`对象有三个属性:**名称**、**价格**、**重量**。现在，想象一下在设计了`Product`类和整个电商平台之后，客户又有了新的需求。他们现在想购买数字产品，如电子书、电影和录音。除了产品的重量，一切都很好。现在可能有两种类型的产品——有形的和数字的——我们应该重新思考`Product`用法的逻辑。我们可以在`Product`中加入一个新的功能，如下面的代码所示:\n\n```cpp\nclass Product\n{\npublic:\n  // code omitted for brevity\n  bool is_digital() const {\n    return weight_ == 0.0;\n  }\n\n  // code omitted for brevity\n};\n```\n\n显然，我们修改了类——违背了开-闭原则。原则上说应该关闭类进行修改。应该可以延期。我们可以通过重新设计`Product`类并使其成为所有产品的抽象基类来实现这一点。接下来，我们再创建两个继承`Product`基类的类:`PhysicalProduct`和`DigitalProduct`。下面的类图描述了新的设计:\n\n![](img/39a14c81-c9c5-4dee-8924-e6cbccf9a257.png)\n\n从上图中可以看到，我们从`Product`类中移除了`weight_`属性。现在我们又多了两个班级，`PhysicalProduct`有一个`weight_`属性，`DigitalProduct`没有。相反，它有一个`file_path_`属性。这种方法满足了开-闭原则，因为现在所有的类都可以扩展了。我们使用继承来扩展类，下面的原则与此密切相关。\n\n# 利斯科夫替代原则\n\n利斯科夫替换原则是关于以正确的方式继承一个类型。简单地说，如果有一个函数接受某种类型的参数，那么同一个函数应该接受派生类型的参数。\n\nThe Liskov substitution principle is named after Barbara Liskov, a Turing Award winner and doctor of computer science.\n\n一旦你理解了继承和利斯科夫替代原理，你就很难忘记它。让我们继续开发`Product`类，并添加一个基于货币类型返回产品价格的新方法。我们可以用相同的货币单位存储价格，并提供一个将价格转换为指定货币的功能。下面是该方法的简单实现:\n\n```cpp\nenum class Currency { USD, EUR, GBP }; // the list goes further\n\nclass Product\n{\npublic:\n  // code omitted for brevity\n  double convert_price(Currency c) {\n    // convert to proper value\n  }\n\n  // code omitted for brevity\n};\n```\n\n过了一段时间，该公司决定对所有数字产品纳入终身折扣。现在，每个数字产品都有 12%的折扣。短时间内，我们在`DigitalProduct`类中添加了一个单独的函数，通过应用折扣返回转换后的价格。以下是它在`DigitalProduct`中的外观:\n\n```cpp\nclass DigitalProduct : public Product\n{\npublic:\n  // code omitted for brevity\n  double convert_price_with_discount(Currency c) {\n    // convert by applying a 12% discount\n  } \n};\n```\n\n设计上的问题很明显。在`DigitalProduct`实例上调用`convert_price()`将不起作用。更糟糕的是，客户端代码不能调用它。相反，它应该叫`convert_price_with_discount()`，因为所有的数字产品必须以 12%的折扣出售。该设计违背了利斯科夫替代原则。\n\n我们不应该破坏类的层次结构，而应该记住多态的美。以下是更好的版本:\n\n```cpp\nclass Product\n{\npublic:\n  // code omitted for brevity\n  virtual double convert_price(Currency c) {\n    // default implementation\n  }\n\n  // code omitted for brevity\n};\n\nclass DigitalProduct : public Product\n{\npublic:\n  // code omitted for brevity\n  double convert_price(Currency c) override {\n    // implementation applying a 12% discount\n  }\n\n  // code omitted for brevity\n};\n```\n\n如你所见，我们不再需要`convert_price_with_discount()`功能了。利斯科夫替代原则成立。然而，我们应该再次检查设计中的缺陷。让我们通过在基类中合并用于折扣计算的私有虚拟方法来使它变得更好。以下更新版本的`Product`类包含一个名为`calculate_discount()`的私有虚拟成员函数:\n\n```cpp\nclass Product\n{\npublic:\n  // code omitted for brevity\n  virtual double convert_price(Currency c) {\n    auto final_price = apply_discount();\n    // convert the final_price based on the currency\n  }\n\nprivate:\n virtual double apply_discount() {\n return getPrice(); // no discount by default\n }\n\n  // code omitted for brevity\n};\n```\n\n`convert_price()`函数调用私有的`apply_discount()`函数，该函数按原样返回价格。诀窍来了。我们在派生类中重写`apply_discount()`函数，如下面的`DigitalProduct`实现所示:\n\n```cpp\nclass DigitalProduct : public Product\n{\npublic:\n  // code omitted for brevity\n\nprivate:\n  double apply_discount() override {\n return getPrice() * 0.12;\n }\n\n  // code omitted for brevity\n};\n```\n\n我们不能在类外调用私有函数，但是我们可以在派生类中重写它。前面的代码展示了覆盖私有虚函数的好处。我们修改实现，保持接口不变。如果派生类不需要为折扣计算提供自定义功能，则不会重写它。另一方面，`DigitalProduct`需要在价格上打 12%的折扣后再转换。没有必要修改基类的公共接口。\n\nYou should consider rethinking the design of the `Product` class. It seems even better to call `apply_discount()` directly in `getPrice()`, hence always returning the latest effective price. Though at some point you should force yourself to stop.\n\n设计过程很有创意，有时并不令人满意。由于意想不到的新需求，重写所有代码并不罕见。我们使用原则和方法来最小化在实现新特性之后将会发生的重大变化。SOLID 的下一个原则是最佳实践之一，它将使您的设计更加灵活。\n\n# 界面分离原理\n\n接口分离原则建议将复杂的接口分成更简单的接口。这种隔离允许类避免实现它们不使用的接口。\n\n在我们的电子商务应用中，我们应该实现产品发货、替换和过期功能。产品的装运是将产品转移给买方。目前我们不关心装运细节。产品更换考虑在运输给买方后更换损坏或丢失的产品。最后，产品到期意味着扔掉那些在到期日之前没有售出的产品。\n\n我们可以自由实现前面介绍的`Product`类中的所有功能。然而，最终，我们会偶然发现一些无法运输的产品类型(例如，卖房子很少涉及运输给买家)。可能有不可替代的产品。例如，一幅原画即使丢失或损坏也不可能被替换。最后，数字产品永远不会过期。大多数情况下。\n\n我们不应该强迫客户端代码实现它不需要的行为。对于客户端，我们指的是实现行为的类。以下示例是一种不良做法，与接口隔离原则相矛盾:\n\n```cpp\nclass IShippableReplaceableExpirable\n{\npublic:\n  virtual void ship() = 0;\n  virtual void replace() = 0;\n  virtual void expire() = 0;\n};\n```\n\n现在，`Product`类实现了前面所示的接口。它必须为所有的方法提供一个实现。界面分离原理提出了以下模型:\n\n```cpp\nclass IShippable\n{\npublic:\n  virtual void ship() = 0;\n};\n\nclass IReplaceable\n{\npublic:\n  virtual void replace() = 0;\n};\n\nclass IExpirable\n{\npublic:\n  virtual void expire() = 0;\n};\n```\n\n现在，`Product`类跳过实现任何接口。它的派生类从特定类型派生(实现)。下面的示例声明了几种类型的产品类，每一种都支持前面介绍的有限数量的行为。请注意，为了代码简洁，我们省略了类的主体:\n\n```cpp\nclass PhysicalProduct : public Product {};\n\n// The book does not expire\nclass Book : public PhysicalProduct, public IShippable, public IReplaceable\n{\n};\n\n// A house is not shipped, not replaced, but it can expire \n// if the landlord decided to put it on sell till a specified date\nclass House : public PhysicalProduct, public IExpirable\n{\n};\n\nclass DigitalProduct : public Product {};\n\n// An audio book is not shippable and it cannot expire. \n// But we implement IReplaceable in case we send a wrong file to the user.\nclass AudioBook : public DigitalProduct, public IReplaceable\n{\n};\n```\n\n如果要将文件下载包装为发货，可以考虑对`AudioBook`实现`IShippable`。\n\n# 依赖倒置原则\n\n最后，依赖反转声明对象不应该强耦合。它允许切换到一个替代依赖很容易。例如，当用户购买产品时，我们会发送一张关于购买的收据。从技术上讲，发送回执有几种方式，即通过邮件打印发送、通过电子邮件发送，或者在平台的用户账号页面显示回执。对于后者，我们通过电子邮件或应用向用户发送通知，通知他们收据已准备好可供查看。请看下面打印收据的界面:\n\n```cpp\nclass IReceiptSender\n{\npublic:\n  virtual void send_receipt() = 0;\n};\n```\n\n假设我们已经在`Product`类中实现了`purchase()`方法，并且在它完成时，我们发送收据。代码的以下部分处理收据的发送:\n\n```cpp\nclass Product\n{\npublic:\n  // code omitted for brevity\n  void purchase(IReceiptSender* receipt_sender) {\n    // purchase logic omitted\n    // we send the receipt passing purchase information\n receipt_sender->send(/* purchase-information */);\n  }\n};\n```\n\n我们可以根据需要添加任意多的收据打印选项来扩展应用。下面的类实现了`IReceiptSender`接口:\n\n```cpp\nclass MailReceiptSender : public IReceiptSender\n{\npublic:\n  // code omitted for brevity\n  void send_receipt() override { /* ... */ }\n};\n```\n\n另外两个类——`EmailReceiptSender`和`InAppReceiptSender`——都实现了`IReceiptSender`。因此，要使用特定的收据，我们只需通过`purchase()`方法向`Product`注入依赖项，如下所示:\n\n```cpp\nIReceiptSender* rs = new EmailReceiptSender();\n// consider the get_purchasable_product() is implemented somewhere in the code\nauto product = get_purchasable_product();\nproduct.purchase(rs);\n```\n\n我们可以通过在`User`类中实现一个方法，返回具体用户所需的收据发送选项，从而更进一步。这将使类更加解耦。\n\n前面讨论的所有固体原理都是组成类的自然方式。坚持原则并不是强制性的，但是，如果你坚持原则，它会改善你的设计。\n\n# 使用领域驱动设计\n\n领域是计划的主题领域。我们正在讨论和设计一个以电子商务为主要概念，以其所有补充概念为领域的电子商务平台。我们建议您在项目中考虑领域驱动的设计。然而，这种方法并不是程序设计的灵丹妙药。\n\n在设计项目时，考虑三层架构的以下三层是很方便的:\n\n*   介绍会；展示会\n*   业务逻辑\n*   数据\n\n三层架构适用于客户机-服务器软件，例如我们在本章中设计的软件。表示层向用户提供与产品、购买和运输相关的信息。它通过向客户端发布结果来与其他层进行通信。这是一个客户端可以直接访问的层，例如网络浏览器。\n\n业务逻辑关心应用功能。例如，用户浏览由表示层提供的产品并决定购买其中一个。请求的处理是业务层的任务。在领域驱动的设计中，我们倾向于将领域级实体与其属性结合起来，以解决应用的复杂性。我们将用户视为`User`类的实例，将产品视为`Product`类的实例，以此类推。购买产品的用户被业务逻辑解释为创建`Order`对象的`User`对象，该对象又与`Product`对象相关。然后将`Order`对象绑定到与产品购买相关的`Transaction`对象。相应的购买结果通过表示层来表示。\n\n最后，数据层处理数据的存储和检索。从用户身份验证到产品购买，每个步骤都是从系统数据库中检索或记录的。\n\n将应用分成几层可以处理一般的复杂性。更好的方法是编排具有单一职责的对象。领域驱动设计将实体与没有概念标识的对象区分开来。后者被称为价值对象。例如，用户不区分每个唯一的交易；他们只关心交易所代表的信息。另一方面，用户对象具有`User`类(实体)形式的概念标识。\n\n使用其他对象(或不使用)在对象上允许的操作是命名服务。服务实际上是一种不依赖于特定对象的操作。比如用`set_name()`方法设置用户的名字，是一个不应该被认为是服务的操作。另一方面，用户购买产品是由服务封装的操作。\n\n最后，领域驱动的设计集中集成了**存储库**和**工厂**模式。存储库模式负责检索和存储域对象的方法。工厂模式创建域对象。如果需要的话，使用这些模式允许我们交换可选的实现。现在让我们找出设计模式在电子商务平台中的力量。\n\n# 利用设计模式\n\n设计模式是软件设计中常见问题的架构解决方案。需要注意的是，设计模式既不是方法，也不是算法。它们是架构构造，提供了一种组织类及其关系的方式，以在代码可维护性方面获得更好的结果。即使你以前没有使用过设计模式，你也很可能自己发明了一个。软件设计中往往会出现许多问题。例如，为现有库制作更好的界面是一种被称为**门面**的设计模式。设计模式有名字，以便程序员在对话或文档中使用它们。你应该很自然地用门面、工厂等与其他程序员聊天。\n\n我们之前提到，领域驱动的设计结合了存储库和工厂模式。现在让我们来看看它们是什么，以及它们如何在我们的设计工作中发挥作用。\n\n# 存储库模式\n\n正如 Martin Fowler 最好地描述的那样，存储库模式“*”使用类似于集合的接口来访问域对象*，从而在域和数据映射层之间进行中介。\n\n该模式提供了直接的数据操作方法，不需要直接使用数据库驱动程序。添加、更新、删除或选择数据自然适合应用领域。\n\n方法之一是创建一个提供必要功能的通用存储库类。一个简单的界面如下所示:\n\n```cpp\nclass Entity; // new base class\n\ntemplate <typename T, typename = std::enable_if_t<std::is_base_of_v<Entity, T>>>\nclass Repository\n{\npublic:\n T get_by_id(int);\n void insert(const T&);\n void update(const T&);\n void remove(const T&);\n std::vector<T> get_all(std::function<bool(T)> condition);\n};\n```\n\n我们在前面介绍了一个名为`Entity`的新类。`Repository`类处理实体，为了确保每个实体符合`Entity`的相同界面，它将`std::enable_if`和`std::is_base_of_v`一起应用于模板参数。\n\n`std::is_base_of_v` is a short representation for `std::is_base_of<>::value`. Also, `std::enable_if_t` replaces `std::enable_if<>::type`.\n\n`Entity`类就像下面的表示一样简单:\n\n```cpp\nclass Entity\n{\npublic:\n  int get_id() const;\n  void set_id(int);\nprivate:\n  int id_;\n};\n```\n\n每个业务对象都是一个`Entity`，因此，前面讨论的类应该更新为从`Entity`继承。例如，`User`类采用以下形式:\n\n```cpp\nclass User : public Entity\n{\n// code omitted for brevity\n};\n```\n\n因此，我们可以通过以下方式使用存储库:\n\n```cpp\nRepository<User> user_repo;\nUser fetched_user = user_repo.get_by_id(111);\n```\n\n前面的存储库模式是对这个主题的简单介绍，但是，您可以使它更加强大。它类似于门面模式。虽然使用外观模式的目的不是访问数据库，但最好还是用这个例子来解释。外观模式包装了一个或多个复杂的类，为客户端提供了一个简单的预定义接口来处理底层功能。\n\n# 工厂模式\n\n当程序员谈论工厂模式时，他们可能会混淆工厂方法和抽象工厂。这两种模式都是提供各种对象创建机制的创造性模式。我们来讨论工厂方法。它提供了在基类中创建对象的接口，并允许派生类修改将要创建的对象。\n\n现在是处理物流的时候了，工厂的方法会在这方面帮助我们。当您开发一个提供产品发货的电子商务平台时，您应该考虑到并非所有用户都生活在您的仓库所在的同一区域。因此，从仓库向买方运输产品时，您应该选择合适的运输方式。自行车、无人机、卡车等等。感兴趣的问题是设计一个灵活的物流管理系统。\n\n不同的交通工具需要不同的实现。然而，它们都符合一个接口。以下是`Transport`接口及其派生的特定传输实现的类图:\n\n![](img/9440ba69-a862-495f-b00d-4e2a4db2e746.png)\n\n上图中的每个具体类都提供了具体的交付实现。\n\n假设我们设计了以下负责物流相关动作的`Logistics`基类，包括选择合适的运输方式，如图所示:\n\n![](img/69a43738-0ce0-4c88-8f17-b5a8d36fec0f.png)\n\n前面应用的工厂方法允许灵活地添加新的运输类型以及新的物流方法。注意`createTransport()`方法返回一个指向`Transport`的指针。派生类重写方法，每个方法返回一个`Transport`子类，因此提供了一个特定的传输模式。这是可能的，因为子类返回派生类型，否则，当重写基类方法时，我们不能返回不同的类型。\n\n`Logistics`中的`createTransport()`如下图所示:\n\n```cpp\nclass Logistics \n{\npublic:\n Transport* getLogistics() = 0;\n  // other functions are omitted for brevity\n};\n```\n\n`Transport`类代表`Drone`、`Truck`和`Ship`的基类。这意味着我们可以创建每个的一个实例，并使用`Transport`指针引用它们，如图所示:\n\n```cpp\nTransport* ship_transport = new Ship();\n```\n\n这奠定了工厂模式的基础，因为例如`RoadLogistics`像这样覆盖`getLogistics()`:\n\n```cpp\nclass RoadLogistics : public Logistics\n{\npublic: \n  Truck* getLogistics() override {\n return new Truck();\n }\n}\n```\n\n注意函数的返回类型，是`Truck`而不是`Transport`。之所以有效，是因为`Truck`继承了`Transport`。此外，请参见对象创建如何与对象本身解耦。创建新对象是通过工厂完成的，这与前面讨论的 SOLID 原则保持一致。\n\n乍一看，利用设计模式会给设计带来额外的复杂性，这可能会令人困惑。然而，当实践设计模式时，你应该开发一个真正意义上的更好的设计，因为它们允许项目整体的灵活性和可扩展性。\n\n# 摘要\n\n软件开发需要细致的规划和设计。在本章中，我们了解到项目开发包括以下关键步骤:\n\n*   需求收集和分析:这包括理解项目的领域，讨论和最终确定应该实现的特性。\n*   规范创建:这包括记录需求和项目功能。\n*   设计和测试计划:这指的是从较大的实体开始设计项目，向下分解每个实体为一个独立的类，与项目中的其他类相关。这一步还包括计划如何测试项目。\n*   编码:这一步包括编写实现前面步骤中指定的项目的代码。\n*   测试和稳定:这意味着对照预先计划的用例和场景检查项目，以发现问题并修复它们。\n*   发布和维护:这是将我们带到项目发布和进一步维护的最后一步。\n\n项目设计对程序员来说是一项复杂的任务。他们应该提前考虑，因为部分特性是在开发过程中引入的。\n\n为了使设计灵活和健壮，我们讨论了导致更好的架构的原则和模式。我们已经学习了设计一个复杂的软件项目的过程。\n\n避免糟糕设计决策的最好方法之一是遵循已经设计好的模式和实践。您应该考虑在未来的项目中使用 SOLID 原则和成熟的设计模式。\n\n在下一章，我们将设计一个策略游戏。我们将熟悉更多的设计模式，并看到它们在游戏开发中的应用。\n\n# 问题\n\n1.  TDD 有什么好处？\n2.  UML 中交互图的目的是什么？\n3.  构图和聚合有什么区别？\n4.  你会如何描述利斯科夫替代原则？\n5.  假设给你上课`Animal`，上课`Monkey`。后者描述了一种在树上跳跃的特殊动物。从一个`Animal`类继承一个`Monkey`类是否违背了开闭原则？\n6.  将工厂方法应用于本章中讨论的`Product`类及其子类。\n\n# 进一步阅读\n\n有关更多信息，请参考:\n\n*   *面向对象分析与设计与应用*作者:Grady Booch，[https://www . Amazon . com/面向对象-分析-设计-应用-第 3 期/dp/020189551X/](https://www.amazon.com/Object-Oriented-Analysis-Design-Applications-3rd/dp/020189551X/)\n*   *设计模式:可重用面向对象软件的元素*作者:Erich Gamma 等人，[https://www . Amazon . com/Design-Patterns-Elements-可重用-面向对象/dp/0201633612/](https://www.amazon.com/Design-Patterns-Elements-Reusable-Object-Oriented/dp/0201633612/)\n\n*   *代码完成:软件构建实用手册*作者:史蒂夫·麦康奈尔，[https://www . Amazon . com/Code-Complete-实用-手册-构建/dp/0735619670/](https://www.amazon.com/Code-Complete-Practical-Handbook-Construction/dp/0735619670/)\n*   *领域驱动设计:解决软件核心的复杂性*作者:Eric Evans，[https://www . Amazon . com/领域驱动-设计-解决-复杂性-软件/dp/0321125215/](https://www.amazon.com/Domain-Driven-Design-Tackling-Complexity-Software/dp/0321125215/)"
  },
  {
    "path": "docs/exp-cpp/11.md",
    "content": "# 十一、使用设计模式设计策略游戏\n\n游戏开发是软件工程中最有趣的话题之一。C++ 因其高效性而被广泛应用于游戏开发中。然而，由于这种语言没有图形用户界面组件，它被用在后端。在本章中，我们将学习如何在后端设计一个策略游戏。我们将合并几乎所有我们在前面章节中学习的内容，包括设计模式和多线程。\n\n我们将要设计的游戏是一款名为**读者与干扰者**的策略游戏。在这里，玩家创建单位，称为读者，他们能够建造图书馆和其他建筑，以及士兵，他们保护这些建筑免受敌人的攻击。\n\n在本章中，我们将涵盖以下主题:\n\n*   游戏设计导论\n*   深入游戏设计过程\n*   使用设计模式\n*   设计游戏循环\n\n# 技术要求\n\n带有`-std=c++ 2a`选项的 g++ 编译器将用于编译本章中的示例。您可以在[https://github.com/PacktPublishing/Expert-CPP](https://github.com/PacktPublishing/Expert-CPP)找到本章将用到的源文件。\n\n# 游戏设计导论\n\n在这一章中，我们将设计一个策略游戏的后端，玩家可以在其中创建单位(工人、士兵)、建造建筑和对抗敌人。每当你设计一款游戏，无论是策略游戏还是第一人称射击游戏，都有几个基本的组成部分是相同的，比如游戏物理，用来让游戏给玩家带来更真实、更身临其境的感觉。\n\n游戏设计中有一些组件几乎在所有游戏中都重复出现，比如碰撞检测机制、音频系统、图形渲染等等。在设计游戏时，我们可以区分引擎和游戏，或者开发一个强绑定的应用，将引擎和游戏都表示为一个结果。单独设计游戏引擎可以让它扩展到其他版本，甚至可以用于其他游戏。毕竟，游戏有相同的机制和流程。他们最大的不同在于他们的情节。\n\n在设计游戏引擎时，您应该仔细规划将使用该引擎设计的游戏类型。尽管大多数基本特征是相同的。独立于游戏类型，3D 射击游戏和策略游戏是有区别的。在一个策略游戏中，玩家在一个大的游戏区域内战略性地部署单位。游戏世界是从自上而下的视角显示的。\n\n# 读者与干扰者游戏介绍\n\n游戏的基本思想很简单:玩家拥有的资源有限。这些资源可以用来为游戏角色建造建筑。我们命名角色单位，分为读者和士兵。读者是建造图书馆和其他建筑的聪明人。每个建成的图书馆最多可容纳 10 名读者。如果玩家将 10 名读者移入图书馆，在指定的时间后，图书馆会产生一名教授。教授是一个强大的单位，可以一次消灭三名敌兵。教授可以为玩家的士兵创造更好的武器。\n\n游戏从一栋已经建成的房子、两名士兵和三名读者开始。一所房子每 5 分钟产生一个新的读者。读者可以建造新房子，然后产生更多的读者。他们也可以建造生产士兵的兵营。\n\n玩家的目标是建立五个图书馆，每个图书馆至少培养出一名教授。玩家必须在游戏中保护他/她的建筑和读者免受敌人的攻击。敌人被称为**扰乱者**，因为他们的目标是扰乱读者的主要目标:在图书馆内学习。\n\n# 策略游戏组件\n\n正如我们之前提到的，我们的战略游戏将包括基本组件——读者和士兵(我们将把他们称为单位)、建筑和地图。游戏地图包含游戏中每个物体的坐标。我们将讨论一个游戏地图的较轻版本。现在，让我们利用我们的项目设计技能来分解游戏本身。\n\n游戏由以下角色单元组成:\n\n*   读者\n*   一名士兵\n*   教授\n\n它还包括以下建筑:\n\n*   Java library\n*   房子\n*   兵营\n\n现在，让我们讨论一下游戏每个组件的属性。游戏角色具有以下属性:\n\n*   生命值(一个整数，在敌人每次攻击后减少)\n*   力量(一个整数，定义一个单位可以对敌方单位造成的伤害)\n*   打字(读者、士兵或教授)\n\nlife 属性应该具有基于单位类型的初始值。例如，一个读者的初始生命值是 10，而一个士兵的生命值是 12。当在游戏中互动时，所有的单位都可能被敌方单位攻击。每次攻击都被描述为生命值的降低。我们减少生命值的数量是基于攻击者的力量值。例如，士兵的力量被设置为 3，这意味着士兵的每次攻击都会减少受害者 3 点生命值。当受害者生命值变为零时，角色单位将被摧毁。\n\n建筑也是如此。一个建筑有一个建造期限，它将完全由。一个完整的建筑也有生命值，敌人对建筑造成的任何伤害都会降低这些生命值。以下是建筑属性的完整列表:\n\n*   生命点\n*   类型\n\n*   施工持续时间\n*   单位生产持续时间\n\n单位生产持续时间是生产一个新的角色单位所需的时间。例如，一个兵营每 3 分钟产生一名士兵，一所房子每 5 分钟产生一名读者，当最后一名失踪的读者进入图书馆时，图书馆立即从 10 名读者中产生一名教授。\n\n现在我们已经定义了游戏组件，让我们来讨论它们之间的交互。\n\n# 组件之间的交互\n\n《读者与干扰者》游戏设计中的下一件重要事情是角色之间的互动。我们已经提到读者可以建造建筑。在游戏中，应该注意这个过程，因为每种类型的建筑都有其建造的持续时间。因此，如果读者忙于建筑过程，我们应该测量时间，以确保建筑在指定时间后准备就绪。然而，为了让游戏更好，我们应该考虑到不止一个读者可以参与构建过程。这应该会使建造一座建筑更快。比如一个工棚，一个读者 5 分钟建好，那么应该两个读者 2 分半钟建好，以此类推。这是游戏中复杂交互的一个例子，可以用下图来描述:\n\n![](img/e52d3229-e4d3-4f13-b697-607876bc466e.png)\n\nComplex interaction \n\n接下来是攻击处理。当一个单位被敌人攻击时，我们应该降低被告的生命值。被告本身可以攻击攻击者(为自己辩护)。每当有一个以上的攻击者或防御者时，我们应该相应地处理每个被攻击单位的生命点如何减少。我们还应该定义单位每次命中的持续时间。一个单位不应该很快击中另一个单位。为了让事情更自然一点，我们可能会在每次点击之间引入 1 秒或 2 秒的停顿。下图描述了一个简单的攻击交互。这将在本章后面用一个类交互图来代替:\n\n![](img/888a94bb-3bb8-4339-abab-f453c0a64778.png)\n\nSimple attack interaction\n\n一个更大的互动正在游戏中发生。游戏中有两组，其中一组由玩家控制。另一个由游戏本身自动控制。这意味着我们作为游戏设计者，有义务定义敌方部队的生命周期。游戏将自动创建读者，读者将被分配创建图书馆、兵营和房屋的任务。每个士兵都应该负责保卫建筑物和读者(人民)。有时，士兵们应该聚集在一起执行攻击任务。\n\n我们将设计一个平台，让玩家创造一个帝国；但是，游戏也应该创造敌人，让游戏完整。玩家将面临敌人的常规攻击，敌人将通过建造更多的建筑和生产更多的单位来进化。总的来说，我们可以使用下图来描述交互:\n\n![](img/e4dd9522-8e38-463f-855d-946467b87157.png)\n\nIllustration between the player and the automated player\n\n我们在设计游戏时会定期参考前面的图表。\n\n# 设计游戏\n\n虽然游戏不是一个典型的软件，但它的设计与常规的应用设计没有太大区别。我们将从主要实体开始，并将它们进一步分解为类及其关系。\n\n在前一节中，我们讨论了所有必要的游戏组件及其交互。我们根据项目开发生命周期进行了需求分析和收集。现在，我们开始设计游戏。\n\n# 设计字符单元\n\n下面的类图代表一个读者:\n\n![](img/b494e5af-c72e-493b-bd01-f51ffc39c83b.png)\n\n当我们浏览其他角色单元时，我们将为每个角色单元提供一个基类。每个特定单元将从该基类继承，并添加其特定属性(如果有)。以下是角色单元的完整类图:\n\n![](img/98c96e09-c293-4218-a3dc-f4f5e713e7a5.png)\n\n注意基类——它是一个接口，而不是一个普通的类。它定义了要在派生类中实现的纯虚函数。下面是`CharacterUnit`界面的代码:\n\n```cpp\nclass CharacterUnit\n{\npublic:\n  virtual void attack(const CharacterUnit&) = 0;\n  virtual void destroy() = 0;\n  virtual int get_power() const = 0;\n  virtual int get_life_points() const = 0;\n};\n```\n\n`attack()`法降低角色生命点，`destroy()`则破坏角色。摧毁不仅意味着从场景中移除角色，还意味着停止该单元正在进行的所有交互(如建造建筑、自卫等)。\n\n派生类为`CharacterUnit`接口类的纯虚函数提供了一个实现。让我们看看`Reader`字符单元的代码:\n\n```cpp\nclass Reader : public CharacterUnit\n{\npublic:\n  Reader();\n  Reader(const Reader&) = delete;\n  Reader& operator=(const Reader&) = delete;\n\npublic:\n  void attack(const CharacterUnit& attacker) override {\n    decrease_life_points_by_(attacker.get_power());\n  }\n\n  void destroy() override {\n    // we will leave this empty for now\n  }\n\n  int get_life_points() const override {\n    return life_points_;\n  }\n\n  int get_power() const override {\n    return power_;\n  }\n\nprivate:\n  void decrease_life_points_(int num) {\n    life_points_ -= num;\n    if (life_points_ <= 0) {\n      destroy();\n    }\n  }\n\nprivate:\n  int life_points_;\n  int power_;\n};\n```\n\n现在，我们可以通过以下任何方式声明来创建`Reader`单位:\n\n```cpp\nReader reader;\nReader* pr = new Reader();\nCharacterUnit* cu = new Reader();\n```\n\n我们将主要通过基本接口类来引用字符单元。\n\nPay attention to the copy constructor and the assignment operators. We intentionally marked them as deleted because we don't want to create units by copying others. We will use the `Prototype` pattern for that behavior. This will be discussed later in this chapter.\n\n在我们应该对不同类型的单元做同样的事情的场景中，拥有`CharacterUnit`界面是至关重要的。例如，假设我们必须计算两个士兵、一个读者和一个教授对一栋建筑可能造成的完全破坏。我们可以自由地将它们都称为`CharacterUnits`，而不是保留三个不同的引用来指代三种不同类型的单元。以下是如何:\n\n```cpp\nint calculate_damage(const std::vector<CharacterUnit*>& units)\n{\n  return std::reduce(units.begin(), units.end(), 0, \n            [](CharacterUnit& u1, CharacterUnit& u2) {\n                return u1.get_power() + u2.get_power();\n            }\n  );\n}\n```\n\n`calculate_damage()`功能从单元类型中抽象出来；它不关心读者或士兵。它只是调用`CharacterUnit`接口的`get_power()`方法，保证对特定对象有实现。\n\n我们将更新角色单元类。现在，让我们继续为建筑设计课程。\n\n# 设计建筑\n\n建筑物的类在公共接口方面类似于角色单元。例如，我们可以从如下定义房子的类别开始:\n\n```cpp\nclass House\n{\npublic:\n  House();\n  // copying will be covered by a Prototype\n  House(const House&) = delete;\n  House& operator=(const House&) = delete;\n\npublic:\n  void attack(const CharacterUnit&);\n  void destroy();\n  void build(const CharacterUnit&);\n  // ...\n\nprivate:\n  int life_points_;\n  int capacity_;\n  std::chrono::duration<int> construction_duration_;\n};\n```\n\n这里，我们使用`std::chrono::duration`为`House`施工持续时间保持一个时间间隔。它在`<chrono>`标题中被定义为刻度数和刻度周期，其中刻度周期是从一个刻度到下一个刻度的秒数。\n\n`House`类需要更多的细节，但是我们很快就会意识到我们需要一个用于所有建筑的基础接口(甚至是一个抽象类)。本章将要描述的建筑都有一些共同的行为。`Building`界面如下:\n\n```cpp\nclass IBuilding\n{\npublic:\n  virtual void attack(const CharacterUnit&) = 0;\n  virtual void destroy() = 0;\n  virtual void build(CharacterUnit*) = 0;\n  virtual int get_life_points() const = 0;\n};\n```\n\n注意`Building`前面的`I`前缀。许多开发人员建议为接口类使用前缀或后缀，以提高可读性。例如，`Building`可能已经被命名为`IBuilding`或`BuildingInterface`。我们将对前面描述的`CharacterUnit`使用相同的命名技术。\n\n`House`、`Barrack`和`Library`类实现了`IBuilding`接口，并且必须提供纯虚拟方法的实现。例如，`Barrack`类将如下所示:\n\n```cpp\nclass Barrack : public IBuilding\n{\npublic:\n  void attack(const ICharacterUnit& attacker) override {\n    decrease_life_points_(attacker.get_power());\n  }\n\n  void destroy() override {\n    // we will leave this empty for now\n  }\n\n  void build(ICharacterUnit* builder) override {\n    // construction of the building\n  }\n\n  int get_life_points() const override {\n    return life_points_;\n  }\n\nprivate:\n  int life_points_;\n  int capacity_;\n  std::chrono::duration<int> construction_duration_;\n};\n```\n\n让我们更详细地讨论一下施工工期的实施，此时，`std::chrono::`工期点，保留下来是为了提醒我们施工需要一定的时间。此外，请注意，课程的最终设计可能会在本章的过程中发生变化。现在，让我们找出我们将如何使游戏的组件相互作用。\n\n# 设计游戏控制器\n\n为角色单位和建筑设计类只是设计游戏本身的第一步。游戏中最重要的事情之一是设计这些组件之间的交互。我们应该仔细分析和设计案例，比如两个或更多的角色建造一座建筑。我们已经介绍了一个建筑的建造时间，但是我们没有考虑到一个建筑可能由多个读者建造(可以建造建筑的角色单元)。\n\n我们可以说，由两个读者建造的建筑应该比由一个读者建造的建筑快一倍。如果另一个读者加入了构建，我们应该重新计算持续时间。然而，我们应该限制可以建造同一栋建筑的读者数量。\n\n如果任何一个读者被敌人攻击，那应该会扰乱读者的建筑，这样他们就可以集中精力自卫。当读者停止在大楼上工作时，我们应该再次重新计算施工时间。这次袭击是另一个类似于建筑的案例。当一个角色受到攻击时，它应该通过反击来保护自己。每次命中都会降低角色的生命值。一个角色可能同时被多个敌方角色攻击。这会更快地降低他们的生命值。\n\n一个建筑有一个计时器，因为它周期性地产生一个角色。设计最重要的是游戏动态——也就是循环。在每个特定的时间范围内，游戏中都会发生一些事情。这可能是敌方士兵靠近，角色单位在建造什么，或者其他什么。一个动作的执行并不严格地与另一个不相关的动作的完成联系在一起。这意味着建筑的建造与人物的塑造同时发生。与大多数应用不同，游戏应该继续运行，即使用户没有提供任何输入。如果玩家没有执行动作，游戏不会冻结。角色单位可能会等待一个命令，但是建筑会继续做他们的工作——产生新的角色。同样，敌方玩家(自动玩家)为胜利而奋斗，从不停顿。\n\n# 并发操作\n\n游戏中的许多动作同时发生。正如我们刚刚讨论的，建筑的建造不应该因为一个没有参与建造的单位被敌人攻击而停止。如果敌人进攻，一个建筑不应该停止产生新的角色。这意味着我们应该为游戏中的许多对象设计并发行为。\n\n在 C++ 中实现并发的最好方法之一是使用线程。我们可以重新设计单位和建筑，使它们在基类中包含一个可重写的动作，该动作将在单独的线程中执行。让我们重新设计`IBuilding`，使它成为一个抽象类，拥有一个额外的`run()`虚拟函数:\n\n```cpp\nclass Building\n{\npublic:\n  virtual void attack(const ICharacterUnit&) = 0;\n  virtual void destroy() = 0;\n  virtual void build(ICharacterUnit*) = 0;\n  virtual int get_life_points() const = 0;\n\npublic:  \n void run() {\n std::jthread{Building::background_action_, this};\n }\n\nprivate:\n  virtual void background_action_() {\n // no or default implementation in the base class \n }\n};\n```\n\n注意`background_action_()`功能；这是私人的，但是虚拟的。我们可以在派生类中重写它。`run()`功能不是虚拟的；它在线程中运行私有实现。这里，派生类可能为`background_action_()`提供一个实现。当一个单元被指定建造建筑时，调用`build()`虚拟功能。`build()`功能将计算施工时间的工作委托给`run()`功能。\n\n# 游戏事件循环\n\n解决这个问题最简单的方法是定义一个事件循环。事件循环如下所示:\n\n```cpp\nwhile (true)\n{\n  processUserActions();\n  updateGame();\n}\n```\n\n即使用户(玩家)没有动作，游戏仍然通过调用`updateGame()`功能继续进行。请注意，前面的代码只是事件循环的一般介绍。如你所见，它无限循环，并在每次迭代中处理和更新游戏。\n\n每次循环迭代都会推进游戏的状态。如果用户操作处理需要很长时间，它可能会阻塞循环。游戏会暂时停止。我们通常以每秒**帧** ( **FPS** )来衡量游戏的速度。它的值越高，游戏就越流畅。\n\n我们需要设计在游戏过程中持续运行的游戏循环。重要的是以用户动作处理不会阻塞循环的方式来设计它。\n\n游戏循环照顾游戏中发生的一切，包括 AI。所谓 AI，我们指的是我们之前讨论过的敌方玩家的自动化。除此之外，游戏循环处理角色和建筑的动作，并相应地更新游戏的状态。\n\n在进入游戏循环设计之前，让我们了解几种设计模式，它们将帮助我们完成这项复杂的任务。毕竟游戏循环是另一种设计模式！\n\n# 使用设计模式\n\n使用**面向对象** **编程** ( **OOP** )范式设计游戏是很自然的。毕竟，一个游戏代表了一组相互强烈互动的物体。在我们的战略游戏中，我们有按单位建造的建筑。单位保护自己免受敌方单位的伤害，等等。这种相互交流导致了复杂性的增长。随着项目的发展和获得更多的特性，支持它将变得更加困难。对我们来说已经很明显了，设计是建筑项目中最重要(如果不是最重要)的部分之一。结合设计模式将极大地改善设计过程和项目支持。\n\n让我们检查一些在游戏开发中有用的设计模式。我们将从经典模式开始，然后讨论更多特定于游戏的模式。\n\n# 命令模式\n\n开发人员将设计模式分为创造类、结构类和行为类。命令模式是一种行为设计模式。行为设计模式主要关注在对象之间的通信中提供灵活性。在这种情况下，命令模式将一个动作封装在一个对象中，该对象包含必要的信息以及动作本身。这样，命令模式就像一个智能函数。用 C++ 实现它的最简单方法是为一个类重载`operator()`，如下所示:\n\n```cpp\nclass Command\n{\npublic:\n  void operator()() { std::cout << \"I'm a smart function!\"; }\n};\n```\n\n一个重载`operator()`的类有时被称为**函子**。前面的代码与下面的常规函数声明几乎相同:\n\n```cpp\nvoid myFunction() { std::cout << \"I'm not so smart!\"; }\n```\n\n调用正则函数和`Command`类的对象看起来很相似，如下所示:\n\n```cpp\nmyFunction();\nCommand myCommand;\nmyCommand();\n```\n\n每当我们需要为函数使用状态时，这两者之间的区别就显而易见了。为了存储常规函数的状态，我们使用静态变量。为了在对象中存储状态，我们使用对象本身。以下是我们如何跟踪过载操作员的呼叫数量:\n\n```cpp\nclass Command\n{\npublic:\n  Command() : called_(0) {}\n\n  void operator()() {\n    ++ called_;\n    std::cout << \"I'm a smart function.\" << std::endl;\n    std::cout << \"I've been called\" << called_ << \" times.\" << std::endl;\n  }\n\nprivate:\n  int called_;\n};\n```\n\n对于`Command`类的每个实例，调用次数是唯一的。下面的代码声明了`Command`的两个实例，并分别调用了两次和三次:\n\n```cpp\nCommand c1;\nCommand c2;\nc1();\nc1();\nc2();\nc2();\nc2();\n// at this point, c1.called_ equals 2, c2.called_ equals 3\n```\n\n现在，让我们尝试将这种模式应用到我们的策略游戏中。游戏的最终版本有一个图形界面，允许用户使用各种按钮和鼠标点击来控制游戏。比如让一个角色单位盖房子，而不是工棚，我们应该在游戏面板上选择对应的图标。让我们想象一个游戏面板，上面有游戏地图和一堆控制游戏动态的按钮。\n\n游戏向玩家提供以下命令:\n\n*   将字符单元从 A 点移动到 B 点\n*   攻击敌人\n*   建造一座建筑\n*   安顿好房子\n\n游戏命令的设计如下:\n\n![](img/03f3119a-3a41-4fd1-b3ac-f5245be1e74b.png)\n\n每个类封装了动作逻辑。客户端代码与处理操作无关。它使用命令指针进行操作，每个指针都指向具体的**命令**(如上图所示)。注意，我们只描述了玩家将要执行的命令。游戏本身使用命令在模块之间进行通信。自动命令的例子包括**运行**、**防御**、**死亡**和**创建**。以下是显示游戏中命令的大图:\n\n![](img/87e3d426-3977-49b5-ac4e-266dfcb3a93b.png)\n\n上述命令执行游戏过程中出现的任何上述事件。要收听这些事件，我们应该考虑使用观察者模式。\n\n# 观察者模式\n\n观察者模式是一种允许我们订阅对象状态变化的架构机制。我们说我们观察物体的变化。观察者模式也是一种行为设计模式。\n\n大多数策略游戏都包含了资源的概念。这可能是岩石、黄金、木头等等。例如，在建造一座建筑时，玩家必须花费 20 单位的木材、40 单位的岩石和 10 单位的黄金。最终，玩家将耗尽资源，不得不收集它们。这位玩家创造了更多的角色单位，并通过收集资源来完成任务——几乎就像现实生活中发生的那样。\n\n现在，假设我们在游戏中有类似的资源收集或消费活动。当玩家要求单位收集资源时，他们应该在每次收集到固定数量的资源时通知我们。玩家是所收集的**资源**事件的订阅者。\n\n建筑也是如此。一栋建筑产生一个角色——订户会收到通知。一个角色单元完成了建筑施工——订户会收到通知。在大多数情况下，订户是玩家。我们更新玩家仪表盘，让玩家的游戏状态保持最新；也就是说，玩家在玩游戏的时候监督有多少资源，有多少单位，有多少建筑。\n\nObserver 涉及实现一个类，该类存储其订阅者并调用事件上的指定函数。它由两个实体组成:订阅者和发布者。如下图所示，订户数量不限于一个:\n\n![](img/5441e6d9-b94b-435b-ac15-bf16bd0be9a3.png)\n\n例如，当一个角色单位被分配建造一座建筑时，它将不断努力建造它，除非它被停止。出现这种情况有多种原因:\n\n*   玩家决定取消建造建筑的过程。\n*   角色单位必须防御敌人的攻击并暂停建造过程。\n*   建筑已经完成，所以角色单位停止工作。\n\n玩家也希望在建造完成时得到通知，因为他们可能有计划让角色单位在完成建造过程后执行其他任务。我们可以设计构建过程，以便它在事件完成时通知它的侦听器(订阅者)。下面的类图还包括一个 Action 接口。将其视为命令模式的实现:\n\n![](img/2ae90c0d-17e8-4028-8bfa-e944ffac3e4a.png)\n\n开发关于观察者的类将我们引向一个点，在这个点上，游戏中几乎所有的实体都是订阅者、发布者或两者兼而有之。如果您遇到类似的场景，您可以考虑使用中介器——另一种行为模式。对象通过中介对象相互通信。触发事件的对象让中介知道它。然后，中介将消息传递给“订阅”对象状态的任何相关对象。下图是 Mediator 集成的简化版本:\n\n![](img/db974456-125d-46b8-bb44-65bf4da3fb29.png)\n\n每个对象都包含一个用于通知订阅者有关更改的中介。中介对象通常包含所有相互通信的对象。在一个事件中，每个对象通过中介通知相关方。例如，当构建完成时，它会触发中介器，而中介器又会通知所有订阅方。为了接收这些通知，每个对象都应该预先订阅给中介。\n\n# 飞行重量模式\n\nFlyweight 是一种结构设计模式。结构模式负责将对象和类组装成更大、更灵活的结构。Flyweight 允许我们通过共享对象的公共部分来缓存对象。\n\n在我们的策略游戏中，我们要处理许多呈现在屏幕上的对象。游戏过程中物体的数量会增加。玩家玩游戏的时间越长，他们创造的角色单位和建筑就越多(自动敌人也是如此)。游戏中的每个单元代表一个包含数据的独立对象。一个字符单元至少占用 16 字节的内存(用于它的两个整数数据成员和虚拟表指针)。\n\n当我们向单位添加额外的字段以便在屏幕上呈现它们时，情况会变得更糟；例如，它们的高度、宽度和子画面(代表渲染单位的图像)。游戏除了人物单元之外，还应该有辅助物品，让用户体验更好，比如树木、石头等装饰物品。在某个时候，我们会得出结论，我们有大量的对象要在屏幕上渲染，每个对象代表几乎相同的对象，但是它们的状态略有不同。Flyweight 模式在这里发挥了作用。对于角色单元，它的高度、宽度和精灵在所有单元中存储几乎相同的数据。\n\nFlyweight 模式建议将一个重物分解成两个:\n\n*   一个不可变的对象，它包含每个同类对象的相同数据\n*   一个可变对象，它唯一地将自己与其他对象区分开来\n\n例如，一个移动的角色单元有它自己的高度、长度和精灵，所有这些对所有角色单元都是重复的。因此，我们可以将这些属性表示为一个不可变的对象，所有对象的属性值都相同。然而，一个角色单元在屏幕上的位置可能与其他角色不同，当玩家命令该单元移动到其他地方或开始建造建筑时，该单元的位置会不断变化，直到到达终点。在每一步，都应该在屏幕上重新绘制单元。通过这样做，我们得到了以下设计:\n\n![](img/969db8dd-89fb-4fc8-91eb-c7c9a186c889.png)\n\n左侧是修改前的`CharacterUnit`，而右侧代表最近使用 Flyweight 模式进行的修改。游戏现在可以处理一堆`CharacterUnit`对象，而每个对象将存储对几个`UnitData`对象的引用。这样，我们节省了大量的内存。我们将每个单元特有的值存储在`CharacterUnit`对象中。这些值会随着时间而变化。尺寸和精灵是恒定的，所以我们可以用这些值保持一个单一的对象。这个不可变的数据被称为**内在状态**，而对象的可变部分(T4)被称为**外在**T9】状态。\n\n我们有意将数据成员移动到`CharacterUnit`，从而将其从一个接口重新设计为一个抽象类。正如我们在[第三章](03.html)、*面向对象编程的细节*中所讨论的，抽象类几乎与可能包含实现的接口相同。`move()`方法是所有类型单元默认实现的一个例子。这样，派生类只提供必要的行为，因为所有单元共享公共属性，例如生命点和力量。\n\n优化内存使用后，我们应该处理复制对象。游戏包括大量创造新的物体。每个建筑产生一个特定的角色单元；角色单位建造建筑，游戏世界本身渲染装饰元素(树木、岩石等)。现在，让我们尝试通过引入克隆功能来改进`CharacterUnit`。在本章的前面，我们有意删除了复制构造函数和赋值运算符。现在，是时候提供一种从现有对象创建新对象的机制了。\n\n# 原型模式\n\n这种模式允许我们独立于对象的类型来创建对象的副本。下面的代码代表了关于我们最近修改的`CharacterUnit`类的最终版本。我们还将添加新的`clone()`成员函数，以便包含原型模式:\n\n```cpp\nclass CharacterUnit\n{\npublic:\n  CharacterUnit() {}\n  CharacterUnit& operator=(const CharacterUnit&) = delete;\n  virtual ~Character() {}\n\n virtual CharacterUnit* clone() = 0;\n\npublic:\n  void move(const Point& to) {\n    // the graphics-specific implementation\n  }\n  virtual void attack(const CharacterUnit&) = 0;\n  virtual void destroy() = 0;\n\n  int get_power() const { return power_; }\n  int get_life_points() const { return life_points_; }\n\nprivate:\n  CharacterUnit(const CharacterUnit& other) {\n    life_points_ = other.life_points_;\n    power_ = other.power_;\n  }\n\nprivate:\n  int life_points_;\n  int power_;\n};\n```\n\n我们删除了赋值运算符，并将复制构造函数移到了私有部分。派生类覆盖`clone()`成员函数，如下所示:\n\n```cpp\nclass Reader : public CharacterUnit\n{\npublic:\n Reader* clone() override {\n return new Reader(*this);\n }\n\n // code omitted for brevity\n};\n```\n\n原型模式将克隆委托给对象。公共接口允许我们从对象的类中分离客户端代码。现在，我们可以克隆一个角色单位，而不知道它是`Reader`还是`Soldier`。请看下面的例子:\n\n```cpp\n// The unit can have any of the CharacterUnit derived types\nCharacterUnit* new_unit = unit->clone();\n```\n\n每当我们需要将对象转换为特定类型时，动态转换都可以很好地工作。\n\n在这一节中，我们讨论了许多有用的设计模式。如果你是这些模式的新手，这可能会显得有些难以承受；然而，正确地使用它们允许我们设计灵活和可维护的项目。让我们最终回到前面介绍的游戏循环。\n\n# 设计游戏循环\n\n策略游戏是变化最大的游戏之一。在每个时间点，许多动作同时发生。读者完成他们的建筑；兵营培养出一名士兵；一名士兵被敌人攻击；玩家命令单位移动、建造、攻击或奔跑；等等。游戏循环处理一切。通常，游戏引擎提供设计良好的游戏循环。\n\n当我们玩游戏时，游戏循环运行。正如我们已经提到的，循环处理玩家的动作，更新游戏状态，并且渲染游戏(使状态变化对玩家可见)。它在每次迭代中都这样做。循环还应该控制游戏的速率，也就是它的 FPS。游戏循环一次迭代的常用术语是帧，这就是为什么我们强调 FPS 是游戏的速度。例如，如果你设计一个以 60 FPS 运行的游戏，这意味着每帧大约需要 16 毫秒。\n\n以下代码在本章前面用于一个简单的游戏循环:\n\n```cpp\nwhile (true)\n{\n  processUserActions();\n  updateGame();\n}\n```\n\n如果没有长时间的用户操作要处理，前面的代码将快速运行。它将在快速机器上运行得更快。你的目标是坚持 16 毫秒为一帧。这可能需要我们在处理动作和更新游戏状态后等待一段时间，如下图所示:\n\n![](img/73b78d78-f5e3-4e4d-8398-eefd4d363046.png)\n\n每次更新都会将游戏时间提前固定的量，这需要固定的真实世界时间来处理。另一方面，如果一帧的处理时间超过指定的毫秒数，游戏就会变慢。\n\n游戏中发生的一切大部分都包含在游戏的更新部分，如上图所示。大多数情况下，更新可能需要一次执行几个操作。此外，正如我们前面提到的，我们必须在后台保留游戏中发生的一些操作的计时器。这主要取决于我们想要制作游戏的细节。例如，建造一座建筑可以表示为两种状态:初始状态和最终状态。\n\n就平面设计而言，这两种状态应该代表两种不同的图像。第一张图片包含了建筑的一些基本部分，可能包括几块岩石围绕着它，好像它正准备建造。下一张图片代表最终建造的建筑。当一个角色单元刚刚开始建造建筑时，我们向玩家展示第一个图像(周围有几块石头的基础)。当建筑完成后，我们用包含最终建筑的图像替换第一个图像。为了使这个过程更自然(更真实)，我们人为地使它花费更长的时间。这意味着我们在图像的两种状态之间保持一个持续 30 秒或更长的计时器。\n\n我们用最少的细节描述了最简单的情况。如果我们需要让游戏更详细，例如，通过渲染建筑在建造过程中的每一个变化，我们应该在代表建筑每一步的大量图像之间保留大量的计时器。再看一遍前面的图表。更新游戏后，我们等待 *N* 毫秒。等待更多的毫秒往往会使游戏的流程更接近现实生活。更新时间太长，玩家体验落后怎么办？在这种情况下，我们需要优化游戏，使其符合在用户体验方面最佳的时间框架。现在，假设更新游戏需要执行数百个以上的操作；玩家实现了一个繁荣的帝国；现在正在建造许多建筑，并且用许多士兵攻击敌人。\n\n一个角色单位的每一个动作，比如从一个点移动到另一个点，攻击一个敌方单位，建造一个建筑，等等，都会准时呈现在屏幕上。现在，如果我们同时在屏幕上呈现数百个单位的状态会怎么样？这就是我们使用多线程方法的地方。每个动作都包括独立修改一个对象的状态(一个对象是游戏中的任何单位，包括静态建筑)。\n\n# 摘要\n\n设计一个游戏是一项复杂的任务。我们可以将游戏开发视为一个独立的编程领域。游戏有不同的流派，策略游戏就是其中之一。策略游戏设计包括设计游戏组件，如单位和建筑。通常，战略游戏包括收集资源、建立帝国和打击敌人。游戏玩法涉及游戏组件之间的动态交流，比如角色单位建造建筑和收集资源，士兵保卫土地不受敌人侵犯等等。\n\n为了恰当地设计一个策略游戏，我们结合了面向对象的设计技巧和设计模式。设计模式在设计整个游戏及其组件如何交互方面起着巨大的作用。在本章中，我们讨论了命令模式，它封装了对象下的动作；观察者模式，用于订阅对象事件；以及用于将观察者提升到组件之间复杂交互的层次的 Mediator 模式。\n\n游戏最重要的部分是它的循环。游戏循环控制渲染、游戏状态的及时更新以及其他子系统。设计它需要使用事件队列和计时器。现代游戏使用网络，允许多个玩家通过互联网一起玩。\n\n在下一章中，我们将介绍 C++ 中的网络编程，这样您将拥有将网络融入游戏所需的技能。\n\n# 问题\n\n1.  重写私有虚函数的目的是什么？\n2.  描述命令设计模式。\n3.  Flyweight 模式如何节省内存使用？\n4.  观察者模式和中介模式有什么区别？\n5.  为什么我们把游戏循环设计成无限循环？\n\n# 进一步阅读\n\n*   *游戏开发模式和最佳实践:更好的游戏，更少的麻烦*作者:John P. Doran，Matt Casanova:[https://www . Amazon . com/Game-Development-Patterns-Best-Practices/DP/1787127834/](https://www.amazon.com/Game-Development-Patterns-Best-Practices/dp/1787127834/)。"
  },
  {
    "path": "docs/exp-cpp/12.md",
    "content": "# 十二、网络和安全\n\n网络编程变得越来越流行。大多数计算机都与互联网相连，现在越来越多的应用依赖于互联网。从可能需要互联网连接的简单程序更新到依赖稳定互联网连接的应用，网络编程正在成为应用开发的一个必要部分。\n\n直到最近的标准更新，C++ 语言才支持网络。网络支持被推迟到以后的标准，很可能会推迟到 C++ 23。然而，我们可以通过处理一个网络应用来提前为发布做准备。我们还将讨论网络的标准扩展，并看看在该语言中支持网络会是什么样子。本章将集中讨论联网的主要原理和驱动设备间通信的协议。作为一名程序员，设计一个网络应用是对你技能的极大补充。\n\n开发人员经常面临的主要问题之一是应用的安全性。无论是与正在处理的输入数据相关，还是用成熟的模式和实践进行编码，应用的安全性都必须是第一位的。这对于网络应用尤其重要。在本章中，我们还将深入研究 C++ 中安全编程的技术和最佳实践。\n\n我们将在本章中讨论以下主题:\n\n*   计算机网络导论\n*   C++ 中的套接字和套接字编程\n*   设计网络应用\n*   理解 C++ 程序中的安全问题\n*   在项目开发中利用安全编程技术\n\n# 技术要求\n\ng++ 编译器连同`-std=c++ 2a`选项将用于编译本章中的示例。\n\n你可以在[https://github.com/PacktPublishing/Expert-CPP](https://github.com/PacktPublishing/Expert-CPP)找到本章的源文件。\n\n# 用 C++ 发现网络编程\n\n两台计算机通过网络相互作用。计算机使用一种叫做**网络适配器**或**网络接口控制器**的特殊硬件组件连接到互联网。计算机上安装的操作系统提供了与网络适配器一起工作的驱动程序；也就是说，为了支持网络通信，计算机必须安装带有支持网络堆栈的操作系统的网络适配器。所谓堆栈，我们指的是数据从一台计算机传输到另一台计算机时所经历的修改层。例如，在浏览器上打开网站会呈现通过网络收集的数据。这些数据作为一系列 0 和 1 被接收，然后被转换成网络浏览器更容易理解的形式。分层在网络中至关重要。我们今天所知的网络通信由几个符合我们将在这里讨论的现场视察模型的层组成。网络接口控制器是支持**开放系统互连** ( **OSI** )模型的物理和数据链路层的硬件组件。\n\n现场视察模型旨在标准化各种设备之间的通信功能。设备在结构和组织上有所不同。这涉及到硬件和软件。例如，使用运行安卓操作系统的英特尔中央处理器的智能手机不同于运行苹果操作系统卡特琳娜的苹果笔记本电脑。区别不在于前述产品背后的名称和公司，而在于硬件和软件的结构和组织。为了消除网络通信中的差异，提出了一套标准化的协议和互通功能作为现场视察模型。我们前面提到的层如下:\n\n*   应用层\n*   表示层\n*   会话层\n*   传输层\n\n*   网路层\n*   数据链路层\n*   物理层\n\n更简化的模型包括以下四层:\n\n*   **应用**:处理特定应用的细节。\n*   **传输**:提供两台主机之间的数据传输。\n*   **网络** : 处理网络中数据包的传输。\n*   **链接**:这包括操作系统中的设备驱动，以及电脑内部的网络适配器。\n\n链接(或数据链接)层包括操作系统中的设备驱动程序，以及计算机中的网络适配器。\n\n为了理解这些层，让我们假设您正在使用桌面应用发送消息，例如 *Skype* 或*电报*。当您键入消息并点击发送按钮时，消息会通过网络到达目的地。在这种情况下，让我们假设您正在向您的朋友发送一条短信，该朋友的计算机上安装了相同的应用。从高层的角度来看，这似乎很简单，但是这个过程是复杂的，即使是最简单的消息在到达目的地之前也要经历很多转换。首先，当你点击发送按钮时，文本信息被转换成二进制形式。网络适配器使用二进制文件运行。它的基本功能是通过介质发送和接收二进制数据。除了通过网络发送的实际数据之外，网络适配器还应该知道数据的目的地址。目的地址是附加到用户数据的许多属性之一。我们所说的用户数据是指您键入并发送给朋友的文本。目标地址是您朋友计算机的唯一地址。键入的文本与目的地地址和发送到目标所需的其他信息打包在一起。您朋友的计算机(包括网络适配器、操作系统和消息传递应用)接收并解包数据。该包中包含的文本随后由消息传递应用呈现在屏幕上。\n\n本章开头提到的几乎每一个现场视察层都将其特定的报头添加到通过网络发送的数据中。下图描述了来自应用层的数据在被移动到目的地之前是如何与报头堆积在一起的:\n\n![](img/a757ca62-f6cd-41ca-a522-d59b4bb81220.png)\n\nOSI model\n\n请看上图中的第一行(第**应用层**)。**数据**是您输入消息应用以便发送给朋友的文本。在每一层中，一直到**物理层**，数据都封装有特定于现场视察模型每一层的报头。另一端的计算机接收并检索打包的数据。在每一层中，它会删除特定于该层的标头，并将包的其余部分向上移动到下一层。最后，数据到达你朋友的消息应用。\n\n作为程序员，我们最关心的是编写能够通过网络发送和接收数据的应用，而无需深入研究各层的细节。然而，我们需要对层如何在更高的层次上用头来扩充数据有一个最低限度的了解。让我们了解网络应用在实践中是如何工作的。\n\n# 幕后的网络应用\n\n安装在设备上的网络应用通过网络与安装在其他设备上的其他应用通信。在本章中，我们将讨论通过互联网协同工作的应用。下图显示了这种交流的高级概述:\n\n![](img/3e256446-4904-4374-be81-67b37eae5d24.png)\n\n最底层的通信是物理层，它通过介质传输数据位。在这种情况下，一种媒介是网线(也可以考虑无线通信)。用户应用从较低层次的网络通信中抽象出来。程序员需要的一切都是由操作系统提供的。操作系统实现网络通信的底层细节，如**传输控制协议** / **互联网协议** ( **TCP** / **IP** )套件。\n\n每当应用需要访问网络时，无论是局域网还是互联网，它都会请求操作系统提供一个访问点。操作系统通过使用网络适配器和与硬件通信的特定软件来提供网络网关。\n\n更详细的说明如下:\n\n![](img/000a23f6-9ba9-4102-8fe3-88292e377065.png)\n\n操作系统提供了与其网络子系统一起工作的应用编程接口。程序员应该关心的主要抽象是套接字。我们可以将套接字视为通过网络适配器发送其内容的文件。套接字是通过网络连接两台计算机的接入点，如下图所示:\n\n![](img/26dbd171-672b-494d-b565-81f297597189.png)\n\n从程序员的角度来看，套接字是一种允许我们在应用中通过网络实现数据的结构。套接字是发送或接收数据的连接点；也就是说，应用也通过套接字接收数据。操作系统根据请求为应用提供套接字。一个应用可以有多个套接字。客户端-服务器架构中的客户端应用通常使用单个套接字运行。现在，让我们详细研究套接字编程。\n\n# 使用套接字编程网络应用\n\n正如我们前面提到的，套接字是网络通信上的抽象。我们将它们视为常规文件——写入套接字的所有内容都由操作系统通过网络发送到目的地。通过网络接收到的所有信息都被写入套接字——同样，是由操作系统写入的。这样，操作系统为网络应用提供了双向通信。\n\n假设我们运行两个不同的网络应用。例如，我们打开网络浏览器上网，并使用消息应用(如 Skype)与朋友聊天。网络浏览器代表客户端-服务器网络架构中的客户端应用。在这种情况下，服务器是用请求的数据进行响应的计算机。例如，我们在网络浏览器的地址栏中键入一个地址，然后在屏幕上看到结果网页。每当我们访问一个网站时，网络浏览器都会向操作系统请求一个套接字。在编码方面，网络浏览器使用操作系统提供的应用编程接口创建一个套接字。我们可以用一个更具体的前缀来描述套接字:客户端套接字。为了让服务器处理客户端请求，运行 web 服务器的计算机必须侦听传入的连接；也就是说，服务器应用创建一个用于监听连接的服务器套接字。\n\n只要在客户机和服务器之间建立了连接，数据通信就可以继续进行。下图描述了对 facebook.com 的网络浏览器请求:\n\n![](img/025167ab-52c6-40b9-a864-ee63f96cdb67.png)\n\n注意上图中的一组数字。这个叫**互联网协议** ( **IP** ) **地址**。IP 地址是我们向设备传输数据所需的位置。有数十亿台设备连接到互联网。为了对它们进行独特的区分，每个设备都公开一个代表其地址的唯一数值。使用 IP 协议建立连接，这就是为什么我们称之为 IP 地址。一个 IP 地址由四组 1 字节长度的数字组成。它的点分十进制表示形式是 X.X.X.X，其中 X 是 1 字节的数字。每个位置的值范围从 0 到 255。更具体地说，它是一个版本 4 的 IP 地址。现代系统使用版本 6 地址，这是数字和字母的组合，提供了更广泛的可用地址值。\n\n创建套接字时，我们将本地计算机的 IP 地址分配给它；也就是说，我们将套接字绑定到地址。当使用套接字向网络中的另一个设备发送数据时，我们应该设置它的目的地址。目的地址由该设备上的另一个套接字持有。为了在两个设备之间建立连接，我们使用两个插座。可能会出现一个合理的问题——如果设备上运行着几个应用会怎么样？如果我们运行几个应用，每个应用都为自己创建了一个套接字，会怎么样？哪一个应该接收传入的数据？\n\n要回答这些问题，请仔细阅读前面的图表。您应该会在 IP 地址末尾的冒号后面看到一个数字。那叫**端口号**。端口号是一个 2 字节长的数字，由操作系统分配给套接字。由于 2 字节的长度限制，操作系统不能为套接字分配超过 65，536 个唯一端口号；也就是说，不能有超过 65，536 个同时运行的进程或线程通过网络进行通信(但是，有重用套接字的方法)。除此之外，还有为特定应用保留的端口号。这些端口称为众所周知的端口，范围从 0 到 1023。它们是为特权服务保留的。例如，HTTP 服务器的端口号是 80。这并不意味着它不能使用其他端口。\n\n让我们学习如何在 C++ 中创建套接字。我们将设计一个封装**可移植操作系统接口** ( **POSIX** )插座的包装类，也称为 **Berkeley** 或 **BSD** 插座。它有一套标准的套接字编程函数。网络编程的 C++ 扩展将是对该语言的巨大补充。工作草案包含有关网络接口的信息。我们将在本章后面讨论这个问题。在此之前，让我们尝试为现有的和低级别的库创建我们自己的网络包装器。当我们使用 POSIX 套接字时，我们依赖于操作系统的 API。操作系统提供了一个表示用于创建套接字、发送和接收数据等的函数和对象的应用编程接口。\n\nPOSIX 将套接字表示为文件描述符。我们几乎把它当作普通文件来使用。文件描述符遵循为数据输入/输出提供公共接口的 UNIX 哲学。以下代码使用`socket()`函数(在`<sys/socket.h>`标题中定义)创建一个套接字:\n\n```cpp\nint s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);\n```\n\n`socket()`功能的声明如下:\n\n```cpp\nint socket(int domain, int type, int protocol);\n```\n\n所以，`AF_INET`、`SOCK_STREAM`、`IPPROTO_TCP`都是数值。domain 参数指定套接字的协议系列。我们使用`AF_INET`来指定 IPv4 协议。对于 IPv6，我们使用`AF_INET6`。第二个参数指定套接字的类型，也就是说，它是面向流的还是数据报套接字。对于每种特定的类型，都应该相应地指定最后一个参数。在前面的例子中，我们用`IPPROTO_TCP`指定了`SOCK_STREAM`。**传输控制协议** ( **TCP** )代表了一种可靠的面向流的协议。这就是为什么我们将类型参数设置为`SOCK_STREAM`。在我们实现一个简单的套接字应用之前，让我们了解更多关于网络协议的信息。\n\n# 网络协议\n\n网络协议是定义应用之间相互通信的规则和数据格式的集合。例如，网络浏览器和网络服务器通过**超文本传输协议** ( **HTTP** )进行通信。HTTP 更像是一组规则，而不是传输协议。传输协议是所有网络通信的基础。传输协议的一个例子是 TCP。当我们提到 TCP/IP 套件时，我们指的是在 IP 上实现 TCP。我们可以将**互联网协议** ( **IP** )视为互联网通信的心脏。\n\n它提供主机到主机的路由和寻址。我们通过互联网发送或接收的一切都被打包成一个 *IP 包*。以下是 IPv4 数据包的外观:\n\n![](img/947b4431-3e7b-4324-a92c-42a6140bcb04.png)\n\nIP 报头重 20 字节。它结合了将数据包从源地址传送到目的地址所需的标志和选项。在 IP 协议领域，我们通常称一个数据包为数据报。每一层都有其特定的数据包术语。更细心的专家谈到将 TCP 段封装到 IP 数据报中。称它们为小包*完全没问题。*\n\n较高级别的每个协议都将元信息附加到通过网络发送和接收的数据中；例如，TCP 数据被封装在一个 IP 数据报中。除了这些元信息，协议还定义了在两个或更多设备之间完成数据传输时应该执行的基本规则和操作。\n\nYou can find more detailed information in specific documents called **Request for Comments **(**RFCs**). For example, RFC 791 describes the Internet Protocol, while RFC 793 describes the Transmission Control Protocol.\n\n许多流行的应用——文件传输、电子邮件、网络和其他——使用 TCP 作为它们的主要传输协议。例如，HTTP 协议定义了从客户端传输到服务器的消息的格式，反之亦然。实际的传输是使用传输协议进行的——在本例中是 TCP。然而，HTTP 标准并没有将 TCP 限制为唯一的传输协议。\n\n下图说明了在将数据传递到较低级别之前附加到数据的 TCP 头:\n\n![](img/84ddead5-264a-4848-8552-5bc1e8c1986f.png)\n\n注意源端口号和目的端口号。这些是区分操作系统中正在运行的进程的唯一标识符。另外，看看序列号和确认号。它们是特定于 TCP 的，用于传输可靠性。\n\n实际上，使用 TCP 是因为它具有以下特点:\n\n*   丢失数据的重传\n*   订单交货\n*   数据完整性\n*   拥塞控制和避免\n\n**IP** (互联网协议的简称)不可靠。它不关心丢失的数据包，这就是为什么 TCP 处理丢失数据包的重传。它用唯一的标识符标记每个数据包，该标识符应该由传输的另一端确认。如果发送方没有收到数据包的**确认码** ( **确认**)，协议将重新发送数据包(次数有限)。以正确的顺序接收数据包也很重要。TCP 对收到的数据包进行重新排序，以表示正确排序的信息。这就是为什么，在网上听音乐时，我们不听歌曲开头的结尾。\n\n数据包的重新传输可能会导致另一个被称为**网络拥塞**的问题。当节点无法足够快地发送数据包时，就会出现这种情况。数据包会被卡住一段时间，不必要的重传会增加它们的数量。TCP 的各种实现采用算法来避免拥塞。\n\n它维护一个拥塞窗口，这是决定可以发送的数据量的一个因素。TCP 使用慢启动机制，在初始化连接后缓慢增加拥塞窗口。虽然协议在相应的**评论请求** ( **RFC** )中进行了描述，但是有很多机制在操作系统中实现不同。\n\n栅栏的另一边是**用户数据报协议** ( **UDP** )。这两者的主要区别是 TCP 可靠。这意味着，在网络数据包丢失的情况下，它会重新发送相同的数据包，直到到达指定的目的地。由于其可靠性，通过 TCP 的数据传输被认为比使用 UDP 花费更长的时间。UDP 并不能保证我们能够正确无误地传送数据包。相反，开发人员应该注意重新发送、检查和验证数据传输。需要快速通信的应用往往依赖于 UDP。例如，视频通话应用或在线游戏使用 UDP 是因为它的速度。即使有几个包在传输过程中丢失，也不会影响用户体验。玩游戏或在视频聊天中与朋友交谈时出现小故障比等待几秒钟游戏或视频的下一帧要好。\n\nTCP 比 UDP 慢的主要原因之一是 TCP 的连接启动过程中步骤较多。下图显示了 TCP 中连接建立的过程，也称为三次握手:\n\n![](img/297eb191-d103-4058-b238-d114d4404673.png)\n\n客户端向服务器发送`SYN`数据包时，会选择一个随机数。服务器将该随机数递增 1，选择另一个随机数，然后用`SYN-ACK`数据包进行回复。客户端将从服务器接收到的两个数字递增 1，并通过向服务器发送最后一个`ACK`来完成握手。三次握手成功完成后，客户端和服务器可以相互传输数据包。这个连接建立过程适用于每个 TCP 连接。握手的细节对网络应用的开发者是隐藏的。我们创建套接字并开始监听传入的连接。\n\n请注意两种端点类型之间的区别。其中一个就是客户。在实现网络应用时，我们应该明确区分客户端和服务器，因为它们有不同的实现。这也与插座的类型有关。当创建服务器套接字时，我们让它监听传入的连接，而客户端不监听——它发出请求。下图描述了客户端和服务器的某些函数及其调用顺序:\n\n![](img/abc95252-99b1-40e7-8518-218dab92e194.png)\n\n当在代码中创建套接字时，我们指定套接字的协议和类型。当我们想要在两个端点之间建立可靠的连接时，我们会选择 TCP。有趣的是，我们可以使用传输协议，如 TCP，来构建我们自己的协议。假设我们定义了一种特殊的文档格式来发送和接收，以便有效地处理通信。例如，每个文档都应该以单词 PACKT 开头。HTTP 的工作原理是一样的。它使用 TCP 进行传输，并通过它定义通信格式。在 UDP 的情况下，我们还应该为通信设计和实现可靠性策略。上图显示了 TCP 如何在两个端点之间建立连接。客户端向服务器发送`SYN`请求。服务器用`SYN-ACK`响应来回答，让客户端知道继续握手没问题。最后，客户端向服务器回复一个`ACK`，声明连接已正式建立。他们想交流多久就交流多久。\n\n**Synchronize** (**SYN**) and ACK are protocol-defined terms that have become common in network programming. \n\nUDP 不是这样工作的。它将数据发送到目的地，而不用担心已建立的连接。如果使用 UDP 但需要一定的可靠性，应该自己实现；例如，通过检查数据的一部分是否到达目的地。要检查它，您可以等待目的地用自定义的`ACK`数据包回答。大多数面向可靠性的实现可能会重复已经存在的协议，例如 TCP。但是，有很多场景你并不需要它们；例如，您不需要拥塞避免，因为您不需要将同一个数据包发送两次。\n\n我们在前一章设计了一个策略游戏。假设游戏是在线的，你在和一个真正的对手玩，而不是一个自动的敌方玩家。游戏的每一帧都是基于通过网络接收的数据呈现的。如果我们努力使数据传输可靠，增加数据完整性，并确保没有数据包丢失，用户体验可能会因为玩家的不同步而受到伤害。这个场景非常适合使用 UDP。我们可以在没有重传策略的情况下实现数据传输，从而压缩游戏速度。当然，使用 UDP 并不强迫我们避免可靠性。在同样的场景中，我们可能需要确保玩家成功接收到数据包。例如，当玩家投降时，我们应该确保对手收到消息。因此，我们可以有条件的可靠性基于数据包的优先级。UDP 在网络应用中提供了灵活性和速度。\n\n让我们看一下一个 TCP 服务器应用的实现。\n\n# 设计网络应用\n\n与完全与网络相关的应用相比，使用需要网络连接的小型子系统设计应用的方法有所不同。后者的一个例子可能是用于文件存储和同步的客户机-服务器应用(如 Dropbox)。它由服务器和客户端组成，其中客户端安装为桌面或移动应用，也可以用作文件资源管理器。Dropbox 控制的系统中文件的每次更新都会立即与服务器同步。这样，您将始终将文件保存在云中，并且可以通过互联网连接在任何地方访问它们。\n\n我们将为文件存储和操作设计一个类似的简化服务器应用。服务器的主要任务如下:\n\n*   从客户端应用接收文件\n*   将文件存储在指定位置\n*   根据请求向客户端发送文件\n\n参考[第 10 章](10.html)、*设计全球通用的应用*，我们可以进入应用的以下顶层设计:\n\n![](img/45a77bf5-8c18-40ec-9537-129401650f37.png)\n\n上图中的每个矩形代表一个类或与特定任务相关的类的集合。例如，**存储管理器**处理与存储和检索文件相关的一切。在这一点上，它是否使用文件、位置、数据库等类与我们没有多大关系。\n\n**客户端管理器**是一个类或一组类，表示处理与验证或授权客户端(客户端，我们指的是客户端应用)相关的一切，保持与客户端的稳定连接，从客户端接收文件，向客户端发送文件，等等。\n\n我们在本章中特别强调了**网络**作为一个感兴趣的实体。与网络连接相关的一切，以及与客户端之间的数据传输，都是通过**网络**处理的。现在，让我们看看我们可以使用什么功能来设计网络类(为了方便起见，我们将称之为网络管理器)。\n\n# 使用 POSIX 套接字\n\n正如我们前面提到的，像`socket()`、`bind()`和`accept()`这样的函数是大多数 Unix 系统默认支持的库函数。之前我们收录了`<sys/socket.h>`文件。除此之外，我们还需要其他几个头文件。让我们实现经典的 TCP 服务器示例，并将其包装在文件传输应用服务器的网络模块中。\n\n正如我们前面提到的，服务器端开发与客户端开发的区别在于套接字的类型及其行为。虽然双方都使用套接字进行操作，但是服务器端套接字会持续监听传入的连接，而客户端套接字会启动与服务器的连接。对于等待连接的服务器套接字，我们创建一个套接字，并将其绑定到服务器的 IP 地址和客户端将尝试连接的端口号。下面的 C 代码表示 TCP 服务器套接字的创建和绑定:\n\n```cpp\nint s = socket(AF_INET, SOCK_STREAM, 0);\n\nstruct sockaddr_in server;\nserver.sin_family = AF_INET;\nserver.sin_port = htons(port);\nserver.sin_addr.s_addr = INADDR_ANY;\n\nbind(s, (struct sockaddr*)&server, sizeof(server));\n```\n\n第一个调用创建一个套接字。第三个参数设置为 0，这意味着将根据套接字的类型选择默认协议。类型作为第二个参数`SOCK_STREAM`传递，默认情况下使协议值等于`IPPROTO_TCP`。`bind()`功能将套接字与指定的 IP 地址和端口号绑定。我们在`sockaddr_in`结构中指定了它们，它结合了网络地址相关的细节。\n\nAlthough we skipped this in the preceding code, you should consider checking the calls to `socket()` and `bind()` functions (and other functions in POSIX sockets) against errors. Almost all of them return `-1` in the event of an error.\n\n另外，注意`htons()`功能。它负责将其参数转换为网络字节顺序。这个问题隐藏在计算机的设计方式中。一些机器(例如英特尔微处理器)使用**小端**字节排序，而其他机器使用**大端**排序。**小端**排序将最低有效字节放在第一位。**大端**排序将最高有效字节放在第一位。下图显示了两者之间的区别:\n\n![](img/16478fe6-bab4-4ef2-9d55-9473870f4425.png)\n\n网络字节顺序是独立于特定机器体系结构的惯例。`htons()`功能将提供的端口号从主机字节顺序(**小-** 或**大端**)转换为网络字节顺序(独立于机器)。\n\n就这样，插座准备好了。现在，我们应该指定它已为传入连接做好准备。为此，我们使用`listen()`函数:\n\n```cpp\nlisten(s, 5);\n```\n\n顾名思义，它监听传入的连接。传递给`listen()`函数的第二个参数指定了服务器在丢弃新的传入请求之前将排队的连接数。在前面的代码中，我们将`5`指定为最大值。在高负载环境中，我们会增加这个数字。最大数值由`<sys/socket.h>`标题中定义的`SOMAXCONN`常数指定。\n\n积压数量的选择(`listen()`功能的第二个参数)基于以下因素:\n\n*   如果短时间内连接请求的速率很高，积压数量应该有一个较大的值。\n*   服务器处理传入连接的持续时间。时间越短，积压值越小。\n\n当一个连接启动正在发生时，我们可以要么放弃它，要么接受它并继续处理该连接。这就是为什么我们在下面的代码片段中使用`accept()`函数:\n\n```cpp\nstruct sockaddr_in client;\nint addrlen;\nint new_socket = accept(s, (struct sockaddr_in*)&client, &addrlen);\n// use the new_socket\n```\n\n在前面的代码中需要考虑的两件事如下:\n\n*   首先，接受的套接字连接信息被写入客户端的`sockaddr_in`结构。我们可以从那个结构中收集关于客户的所有必要信息。\n*   接下来，注意`accept()`函数的返回值。这是一个新的套接字，用来处理来自特定客户端的请求。对`accept()`函数的下一次调用将返回另一个值，该值将代表具有单独连接的另一个客户端。我们应该妥善处理这个问题，因为`accept()`的电话不通；也就是说，它等待新的连接请求。我们将修改前面的代码，以便它接受在不同线程中处理的多个连接。\n\n前面代码中带有注释的最后一行表示`new_socket`可用于接收数据或向客户端发送数据。让我们看看如何实现这一点，然后开始设计我们的`Networking`类。要读取套接字接收的数据，我们需要使用`recv()`功能，如下所示:\n\n```cpp\nchar buffer[BUFFER_MAX_SIZE]; // define BUFFER_MAX_SIZE based on the specifics of the server\nrecv(new_socket, buffer, sizeof(buffer), 0);\n// now the buffer contains received data\n```\n\n`recv()`函数取一个`char*`缓冲区将数据写入其中。它在`sizeof(buffer)`停止写作。该函数的最后一个参数是我们可以设置用于读取的附加标志。您应该考虑多次调用该函数来读取大于`BUFFER_MAX_SIZE`的数据。\n\n最后，为了通过套接字发送数据，我们调用`send()`函数，如下所示:\n\n```cpp\nchar msg[] = \"From server with love\";\nsend(new_socket, msg, sizeof(msg), 0);\n```\n\n至此，我们已经涵盖了实现服务器应用所需的几乎所有功能。现在，让我们将它们包装在一个 C++ 类中，并结合多线程，这样我们就可以同时处理客户端请求。\n\n# 实现一个 POSIX 套接字包装类\n\n让我们设计并实现一个类，作为基于网络的应用的起点。该类的主界面如下所示:\n\n```cpp\nclass Networking\n{\npublic:\n  void start_server();\n\npublic:\n  std::shared_ptr<Networking> get_instance();\n  void remove_instance();\n\nprivate:\n  Networking();\n  ~Networking();\n\nprivate:\n  int socket_;\n  sockaddr_in server_;\n  std::vector<sockaddr_in> clients_;\n\nprivate:\n  static std::shared_ptr<Networking> instance_ = nullptr;\n  static int MAX_QUEUED_CONNECTIONS = 1;\n};\n```\n\n`Networking`类是单例是很自然的，因为我们希望单个实例监听传入的连接。拥有多个对象也很重要，每个对象代表与客户端的独立连接。让我们逐渐让班级设计变得更好。之前，我们看到每个新的客户端套接字都是在服务器套接字侦听并接受连接请求之后创建的。\n\n之后，我们可以通过新的客户端套接字发送或接收数据。服务器的操作类似于下图所示:\n\n![](img/1d1c32ac-afd0-44be-a927-e1e391ec20c8.png)\n\n也就是说，在接受每个传入的连接之后，我们将有一个单独的套接字用于连接。我们将它们存储在`Networking`类的`clients_`向量中。因此，我们可以在一个函数中编写创建服务器套接字、侦听和接受新连接的主要逻辑，如果需要，该函数可以并发工作。`start_server()`功能是服务器监听传入连接的起点。下面的代码块说明了这一点:\n\n```cpp\nvoid Networking::start_server()\n{\n  socket_ = socket(AF_INET, SOCK_STREAM, 0);\n  // the following check is the only one in this code snippet\n  // we skipped checking results of other functions for brevity, \n  // you shouldn't omit them in your code\n  if (socket_ < 0) { \n    throw std::exception(\"Cannot create a socket\");\n  }\n\n  struct sockaddr_in server;\n  server.sin_family = AF_INET;\n  server.sin_port = htons(port);\n  server.sin_addr.s_addr = INADDR_ANY;\n\n  bind(s, (struct sockaddr*)&server, sizeof(server));\n  listen(s, MAX_QUEUED_CONNECTIONS);\n // the accept() should be here\n}\n```\n\n现在，我们已经到了应该接受传入连接的时候了(参见前面代码片段中的注释)。我们这里有两个选择(其实不止两个选择，但我们只讨论其中两个)。我们可以将对`accept()`的调用直接放到`start_server()`函数中，或者我们可以实现一个单独的函数，只要适用，`Networking`类用户就会调用这个函数。\n\nIt's not a bad practice to have specific exception classes for each error case that we have in the project. The preceding code might be rewritten when considering custom exceptions. You can do that as a homework project.\n\n其中一个选项在`start_server()`函数中有`accept()`函数，它将每个新连接推入`clients_`向量，如下所示:\n\n```cpp\nvoid Networking::start_server()\n{\n  // code omitted for brevity (see in the previous snippet)\n  while (true) {\n    sockaddr_in client;\n    int addrlen;\n    int new_socket = accept(socket_, (sockaddr_in*)&client, &addrlen);\n    clients_.push_back(client);\n  }\n}\n```\n\n是的，我们使用了无限循环。这听起来可能很糟糕，但是只要服务器还在运行，它就必须接受新的连接。然而，我们都知道无限循环会阻止代码的执行；也就是说，它永远不会离开`start_server()`功能。我们将我们的网络应用作为一个项目进行了介绍，该项目至少有三个组件:客户端管理器、存储管理器和我们目前正在设计的组件-`Networking`类。\n\n一个组件的执行不能以不好的方式影响其他组件；也就是说，我们可以使用线程使一些组件在后台运行。在线程上下文中运行的`start_server()`函数是一个很好的解决方案，尽管我们现在应该关心我们在[第 8 章](08.html) *、并发和多线程*中讨论的同步问题。\n\n另外，注意前面循环的不完全性。接受连接后，将客户端数据推入`clients_`向量。我们应该考虑使用另一种结构，因为我们还需要存储套接字描述符以及客户端。我们可以使用`std::undordered_map`将套接字描述符映射到客户端连接信息，但是简单的`std::pair`或`std::tuple`就可以了。\n\n但是，让我们更进一步，创建一个表示客户端连接的自定义对象，如下所示:\n\n```cpp\nclass Client\n{\npublic:\n  // public accessors\n\nprivate:\n  int socket_;\n  sockaddr_in connection_info_;\n};\n```\n\n我们将修改`Networking`类，以便它存储一个`Client`对象的向量:\n\n```cpp\nstd::vector<Client> clients_;\n```\n\n现在，我们可以改变设计方法，让`Client`对象负责发送和接收数据:\n\n```cpp\nclass Client\n{\npublic:\n  void send(const std::string& data) {\n    // wraps the call to POSIX send() \n  }\n  std::string receive() {\n    // wraps the call to POSIX recv()\n  }\n\n  // code omitted for brevity \n};\n```\n\n更好的是，我们可以将一个`std::thread`对象附加到`Client`类，这样每个对象都可以在一个单独的线程中处理数据传输。但是，您应该注意不要让系统挨饿。传入连接的数量可能会急剧增加，服务器应用将会停滞不前。我们将在下一节讨论安全问题时讨论这个场景。建议您利用线程池来帮助我们重用线程，并控制程序中运行的线程数量。\n\n类的最终设计取决于我们接收和发送给客户端的数据类型。至少有两种不同的方法。其中之一是连接到客户端，接收必要的数据，然后关闭连接。第二种方法是实现客户端和服务器通信的协议。虽然听起来很复杂，但协议可能很简单。\n\n它还具有可扩展性，使应用更加健壮，因为随着项目的发展，您可以支持更多的功能。在下一节中，当我们讨论如何保护网络服务器应用时，我们将回到设计用于验证客户端请求的协议。\n\n# 保护 C++ 代码\n\n与许多语言相比，C++ 在安全编码方面有点难掌握。有很多指导方针提供了如何以及如何避免 C++ 程序中的安全风险的建议。我们在[第 1 章](01.html)、*构建 C++ 应用*中讨论的最受欢迎的问题之一是使用预处理器宏。我们使用的示例包含以下宏:\n\n```cpp\n#define DOUBLE_IT(arg) (arg * arg)\n```\n\n这个宏使用不当会导致难以发现的逻辑错误。在下面的代码中，程序员期望将`16`打印到屏幕上:\n\n```cpp\nint res = DOUBLE_IT(3 + 1);\nstd::cout << res << std::endl;\n```\n\n输出为`7`。这里的问题是`arg`参数周围缺少括号；也就是说，前面的宏应该重写如下:\n\n```cpp\n#define DOUBLE_IT(arg) ((arg) * (arg))\n```\n\n虽然这个例子很流行，但我们强烈建议尽可能避免使用宏。C++ 提供了大量可以在编译时处理的构造，例如`constexpr`、`consteval`和`constinit`——即使语句有`constexpr`替代。如果需要在代码中进行编译时处理，请使用它们。当然，还有模块，期待已久的语言补充。你应该更喜欢在任何地方使用带有无处不在的防护装置的模块`#include`:\n\n```cpp\nmodule my_module;\nexport int test;\n\n// instead of\n\n#ifndef MY_HEADER_H\n#define MY_HEADER_H\nint test\n#endif \n```\n\n它不仅更安全，而且更高效，因为模块只处理一次(我们可以将它们视为预编译头)。\n\n尽管我们不希望您因为安全问题而变得偏执，但您几乎应该处处小心。通过学习这种语言的怪癖和古怪之处，你可以避免这些问题中的大部分。此外，一个好的做法是使用最新的功能来替换或修复以前版本的缺点。例如，考虑以下`create_array()`函数:\n\n```cpp\n// Don't return pointers or references to local variables\ndouble* create_array()\n{\n  double arr[10] = {0.0};\n  return arr;\n}\n```\n\n`create_array()`函数的调用者留下一个指向不存在的数组的指针，因为`arr`有一个自动存储持续时间。如果需要，我们可以用更好的替代代码替换前面的代码:\n\n```cpp\n#include <array>\n\nstd::array<double> create_array()\n{\n  std::array<double> arr;\n  return arr;\n}\n```\n\n字符串被视为字符数组，是许多缓冲区溢出问题背后的原因。最常见的问题之一是将数据写入字符串缓冲区，而忽略其大小。在这方面，`std::string`类是 C 字符串的更安全的替代品。但是，在支持遗留代码时，使用`strcpy()`等函数时要小心，如下例所示:\n\n```cpp\n#include <cstdio>\n#include <cstring>\n\nint main()\n{\n  char small_buffer[4];\n  const char* long_text = \"This text is long enough to overflow small buffers!\";\n strcpy(small_buffer, long_text);\n}\n```\n\n考虑到在法律上，`small_buffer`应该在末尾有一个空终止符，它将只处理`long_text`字符串的前三个字符。然而，打电话给`strcpy()`后出现了以下情况:\n\n![](img/2e5109ce-bee4-4c01-b755-126f953aacc8.png)\n\n在实现网络应用时，您应该更加小心。来自客户端连接的大部分数据应该被正确处理，缓冲区溢出并不罕见。让我们学习如何使网络应用更加安全。\n\n# 保护网络应用\n\n在本书的前一部分，我们设计了一个使用套接字连接接收客户端数据的网络应用。除了大多数侵入系统的病毒来自外部世界这一事实之外，网络应用也有这种自然的趋势，即让计算机面对互联网上的各种威胁。首先，无论何时运行网络应用，系统中都存在一个开放端口。知道您的应用正在监听的确切端口的人可以通过伪造协议数据来入侵。我们将在这里主要讨论网络应用的服务器端；但是，这里的一些主题也适用于客户端应用。\n\n您应该做的第一件事是合并客户端授权和身份验证。这是两个容易混淆的术语。注意不要互换使用；它们是不同的:\n\n*   **认证**是验证客户端访问的过程。这意味着不是每个传入的连接请求都会立即得到服务。在与客户端之间传输数据之前，服务器应用必须确保客户端是已知的客户端。就像我们通过输入电子邮件和密码来访问社交网络平台一样，客户端的身份验证定义了客户端是否有权访问系统。\n\n*   **授权**则恰恰定义了客户端在系统中可以做什么。它是提供给特定客户端的一组权限。例如，我们在前一节中讨论的客户端应用能够将文件上传到系统。迟早，你可能想要合并付费订阅，并为付费客户提供更广泛的功能；例如，允许他们创建文件夹来组织文件。因此，当客户端请求创建文件夹时，我们可能希望授权该请求来发现客户端是否有权这样做。\n\n当客户端应用启动与服务器的连接时，服务器得到的只是连接细节(IP 地址、端口号)。为了让服务器知道谁是客户端应用的幕后黑手(实际用户)，客户端应用会发送用户的凭据。通常，此过程包括向用户发送唯一标识符(如用户名或电子邮件地址)以及访问系统的密码。然后，服务器根据其数据库检查这些凭据，并验证是否应该授予客户端访问权限。客户端和服务器之间的这种通信形式可能是简单的文本传输或格式化的对象传输。\n\n例如，服务器定义的协议可能要求客户端以下列形式发送一个 **JavaScript 对象符号** ( **JSON** )文档:\n\n```cpp\n{\n  \"email\": \"myemail@example.org\",\n  \"password\": \"notSoSIMPLEp4s8\"\n}\n```\n\n来自服务器的响应允许客户端继续或更新其用户界面，让用户知道操作的结果。在登录时使用任何 web 或网络应用时，您可能会遇到几种情况。例如，输入错误的密码可能会导致服务器返回`Invalid username or password`错误。\n\n除了这第一个必要步骤之外，验证来自客户端应用的每一条数据也是明智的。如果检查电子邮件字段的大小，可能很容易避免缓冲区溢出。例如，当客户端应用有意试图破坏系统时，可能会发送一个值非常大的 JSON 对象。那张支票由服务器承担。防止安全缺陷从数据验证开始。\n\n另一种形式的安全攻击是每秒钟从单个或多个客户端发出太多请求。例如，一个客户端应用在 1 秒钟内发出数百个身份验证请求，会导致服务器集中处理这些请求，并浪费资源来尝试为所有请求提供服务。最好检查客户端请求的速率，例如，将它们限制为每秒一个请求。\n\n这些形式的攻击(有意或无意)被称为**拒绝服务** ( **DOS** )攻击。更高级的 DOS 攻击形式是从多个客户端向服务器发出大量请求。这种形式叫做**分布式拒绝服务** ( **分布式拒绝服务**)攻击。一个简单的方法可能是将试图通过每秒发出多个请求来使系统崩溃的 IP 地址列入黑名单。作为网络应用的程序员，在开发应用时，您应该考虑这里描述的所有问题以及本书范围之外的许多其他问题。\n\n# 摘要\n\n在这一章中，我们介绍了用 C++ 设计网络应用。从第一个版本开始，C++ 就缺乏对网络的内置支持。C++ 23 标准计划最终将其引入该语言。\n\n我们首先介绍了网络的基础知识。完全理解网络需要很多时间，但是在以任何与网络相关的方式实现应用之前，每个程序员都必须知道几个基本概念。这些基本概念包括现场视察模型中的分层和不同类型的传输协议，如 TCP 和 UDP。对任何程序员来说，了解 TCP 和 UDP 之间的差异都是必要的。正如我们所知，TCP 在套接字之间建立了可靠的连接，套接字是程序员在开发网络应用时遇到的下一件事。这是两个应用实例的连接点。每当我们需要通过网络发送或接收数据时，我们应该定义一个套接字，并像处理常规文件一样处理它。\n\n我们在应用开发中使用的所有抽象和概念都由操作系统处理，最终由网络适配器处理。这是一种能够通过网络介质发送数据的设备。从媒体接收数据并不能保证安全。网络适配器接收来自介质的任何信息。为了确保我们正确处理传入的数据，我们还应该注意应用的安全性。本章的最后一节是关于编写安全代码和验证输入，以确保不会对程序造成伤害。保护您的程序是确保程序高质量的一个很好的步骤。开发程序的最好方法之一是彻底测试它们。你可能还记得，在[第 10 章](10.html)、*设计世界就绪应用*中，我们讨论了软件开发步骤，并解释了最重要的步骤之一解释了一旦编码阶段完成，测试程序。测试之后，你很可能会发现很多 bug。其中一些 bug 很难重现和修复，这就是调试的作用。\n\n下一章是关于用正确的方法测试和调试你的程序。\n\n# 问题\n\n1.  列出现场视察模型的所有七层。\n2.  端口号有什么意义？\n3.  为什么要在网络应用中使用套接字？\n4.  描述应该在服务器端执行的操作序列，以便使用 TCP 套接字接收数据。\n5.  TCP 和 UDP 有什么区别？\n6.  为什么不应该在代码中使用宏定义？\n7.  在实现服务器应用时，您如何区分不同的客户端应用？\n\n# 进一步阅读\n\n*   *TCP/IP 图解，第 1 卷:协议*T2 s，作者:r . Stevens:[https://www . Amazon . com/TCP-图解-协议-Addison-Wesley-Professional/DP/0321336313/](https://www.amazon.com/TCP-Illustrated-Protocols-Addison-Wesley-Professional/dp/0321336313/)\n*   *网络基础*，作者:戈登·戴维斯:[https://www . packtpub . com/cloud-Networking/Networking-基础](https://www.packtpub.com/cloud-networking/networking-fundamentals)"
  },
  {
    "path": "docs/exp-cpp/13.md",
    "content": "# 十三、调试和测试\n\n调试和测试在软件开发过程中扮演着极其重要的角色。测试有助于我们在调试和修复问题时发现问题。然而，如果我们在实施阶段遵循某些规则，许多潜在的缺陷是可以避免的。此外，由于测试过程非常昂贵，如果我们能够在需要人工测试之前使用某些工具自动分析软件，那就太好了。此外，我们应该在什么时候、如何以及测试什么软件也很重要。\n\n在本章中，我们将涵盖以下主题:\n\n*   了解问题的根本原因\n*   调试 C++ 程序\n*   理解静态和动态分析\n*   探索单元测试、TDD 和 BDD\n\n在本章中，我们将学习如何分析软件缺陷，如何使用 **GNU 调试器** ( **GDB** )工具调试程序，以及如何使用工具自动分析软件。我们还将学习**单元测试**、**测试驱动开发** ( **TDD** )、**行为驱动开发** ( **BDD** )的概念，以及如何在软件工程开发过程中练习使用它们。\n\n# 技术要求\n\n本章的代码可以在本书的 GitHub 资源库中找到:[https://github.com/PacktPublishing/Expert-CPP](https://github.com/PacktPublishing/Expert-CPP)。\n\n# 了解问题的根本原因\n\n在医学上，一个好的医生需要理解治疗症状和治愈疾病之间的区别。比如给手臂骨折的病人吃止痛药，只会带走症状；手术可能是帮助骨骼逐渐愈合的正确方法。\n\n**根本原因分析** ( **RCA** )是一个系统化的过程，用于确定问题的根本原因。借助相关的适当工具，它试图使用一组特定的步骤来确定问题的主要原因。通过这样做，我们可以确定以下内容:\n\n*   发生了什么事？\n*   怎么发生的？\n*   为什么会这样？\n*   将采用什么适当的方法来预防或减少它，使其不再发生？\n\nRCA 假设一个地方的动作触发另一个地方的另一个动作，以此类推。通过追溯行动链的开始，我们可以发现问题的根源，以及它是如何发展成我们的症状的。啊哈！这正是我们应该遵循的修复或减少软件缺陷的过程。在下面的小节中，我们将了解基本的 RCA 步骤，如何应用 RCA 过程来检测软件缺陷，以及 C++ 开发人员应该遵循哪些特定规则来防止软件中出现此类缺陷。\n\n# 驻地协调员评估概述\n\n通常，RCA 流程包含以下五个步骤:\n\n1.  **定义问题**:在这个阶段，我们可能会找到以下问题的答案:发生了什么？问题的症状是什么？问题发生在什么环境或条件下？\n2.  **收集数据**:做一个原因因素图，需要收集足够的数据。这一步可能既昂贵又耗时。\n\n3.  **制作因果因子图**:因果因子图提供了一个可视化的结构，我们可以用它来组织和分析收集到的数据。因果因素图只不过是一个带有逻辑测试的序列图，用来解释导致症状发生的事件。该图表流程应推动数据收集流程，直到调查人员对图表的完整性感到满意。\n4.  **识别根本原因**:通过检查因果因素图，我们可以制作一个称为**根本原因图**的决策图，以识别根本原因。\n5.  **建议并实施解决方案**:一旦确定了根本原因或多种原因，以下问题的答案可以帮助我们找到解决方案:我们可以做些什么来防止问题再次发生？解决方案将如何实施？谁来负责？实施该解决方案的成本或风险是什么？\n\nRCA 树形图是软件工程行业中最流行的因素图之一。以下是它的示例结构:\n\n![](img/e9263dd4-03a3-4449-90b4-6e7d6d187b5d.png)\n\n假设我们有一个问题有 **A** 、 **B** 和 **C** 症状。症状 **A** 可由事件 **A1** 或 **A2** 引起，症状 **B** 可由事件 **B1** 和 **B2** 或 **B3** 和 **B4** 引起，症状 **C** 由事件 **C1** 和 **C2** 引起。经过数据收集，我们发现症状 **A** 和 **C** 从未出现过，我们只有症状 **B** 。进一步分析可知，问题发生时并未涉及事件 **B1** 、 **B2** ，因此我们可以认定该问题发生的根本原因是因为事件 **B3** 或 **B4** 。\n\n如果软件有缺陷，而不是仅仅在故障点修复它，我们应该对它应用 RCA，并调查问题的原始根本原因。然后，问题的根本原因可以追溯到需求、设计、实现、验证和/或测试计划和输入数据。当找到问题的根源并加以解决后，软件的质量就可以得到提高，因此维护费用就会大大降低。\n\n我们刚刚学习了如何找到问题的根源，但是要记住*最好的防守就是好的进攻*。那么，与其分析和解决一个问题，不如我们能阻止它的发生呢？\n\n# 预防胜于治疗——一种良好的编码行为\n\n从成本的角度来看，IBM 的一项研究表明，假设需求和设计的总体成本为 1X，那么实现和编码过程将花费 5X，单元和集成测试将花费大约 10X，全面客户 beta 测试的成本将花费大约~15X，在产品发布后修复 bug 的成本占据大约 30X！因此，最小化代码缺陷是降低生产成本的最有效方法之一。\n\n虽然找到软件缺陷根本原因的通用方法非常重要，但是如果我们能够在实现阶段防止一些缺陷，那就更好了。为此，我们需要有良好的编码行为，这意味着必须遵循某些规则。这些规则可以分为低级和高级。低级规则可能包括以下项目:\n\n*   未初始化的变量\n*   整数除法\n*   误用`=`代替`==`\n*   可能将有符号变量赋给无符号变量\n*   在`switch`语句中缺少`break`\n*   复合表达式或函数调用中的副作用\n\n说到高级规则，我们有以下相关主题:\n\n*   接口\n*   资源管理\n\n*   内存管理\n*   并发\n\nB.Stroustrup 和 H. Sutter 在他们的实时文档 *C++ 核心指南(0.8 版)*中建议遵循这些规则，其中强调了静态类型安全和资源安全。他们还强调了范围检查的可能性，以避免取消 null-ptr 的引用、悬空指针和异常的系统使用。如果开发人员遵循这样的规则，这将导致他/她的代码是静态类型安全的，没有任何资源泄漏。此外，它不仅会捕获更多的编程逻辑错误，而且还会运行得更快。\n\n由于页面限制，我们将在本小节中只看几个例子。如果你想看更多的例子，请去[https://isocpp.github.io/CppCoreGuidelines](https://isocpp.github.io/CppCoreGuidelines)。\n\n# 未初始化的变量问题\n\n未初始化的变量是程序员最常犯的错误之一。当我们声明一个变量时，一定量的连续内存将被分配给它。如果没有初始化，它仍然有一些价值，但是没有确定性的方法来预测它。因此，当我们执行程序时，会出现不可预测的行为:\n\n```cpp\n//ch13_rca_uninit_variable.cpp\n#include <iostream>\nint main()\n{\n  int32_t x;\n  // ... //do something else but not assign value to x\n  if (x>0) {\n    std::cout << \"do A, x=\" << x << std::endl;\n  }\n  else {\n    std::cout << \"do B, x=\" << x << std::endl;\n  }\n  return 0;\n}\n```\n\n在前面的代码中，当`x`被声明时，操作系统将分配 4 个字节的未使用内存给它，这意味着`x`的值是驻留在该内存中的任何值。每次我们运行这个程序时，`x`的地址和值都可能不同。此外，一些编译器，如 Visual Studio，会在调试版本中将`x`的值初始化为`0`，但在发布版本中保持其未初始化状态。在这种情况下，我们在调试版本和发布版本中有完全不同的输出。\n\n# 复合表达式中的副作用\n\n当一个运算符、表达式、语句或函数完成计算时，它可能会被延长，或者可能会持续存在于它的复合中。这种持续存在有一些副作用，可能会导致一些未定义的行为。让我们看看下面的代码来理解这一点:\n\n```cpp\n//ch13_rca_compound.cpp\n#include <iostream>\nint f(int x, int y)\n{\n  return x*y;\n}\n\nint main()\n{\n  int x = 3;\n  std::cout << f(++ x, x) << std::endl; //bad,f(4,4) or f(4,3)?\n}\n```\n\n由于操作数求值顺序的未定义行为，前面代码的结果可能是 16 或 12。\n\n# 混合有符号和无符号问题\n\n通常情况下，二元运算符(`+`、`-`、`*`、`/`、`%`、`<`、`<=`、`>`、`>=`、`==`、`!=`、`&&`、`||`、`!`、`&`、`|`、`<<`、`>>`、`~`、`^`、`=`、`+=`、`-=`、`*=`、`/=`和`%=`如果两个操作数属于不同的类型，其中一个将被提升为与另一个相同的类型。粗略地说，6.3.1.1[ISO/IEC 9899:2011]小节给出了三个 C 标准转换规则:\n\n*   当我们混合相同等级的类型时，有符号的类型将被提升为无符号类型。\n*   当我们混合不同级别的类型时，如果排名较低的一方的所有值都可以由排名较高的一方表示，排名较低的一方将被提升到排名较高的类型。\n*   如果在前面的情况中，排名较低的类型的所有值都不能由排名较高的类型表示，那么将使用排名较高的类型的无符号版本。\n\n现在，让我们来看看传统的有符号整数减去无符号整数的问题:\n\n```cpp\n//ch13_rca_mix_sign_unsigned.cpp\n#include <iostream>\nusing namespace std;\nint main()\n{\n int32_t x = 10;\n uint32_t y = 20;\n uint32_t z = x - y; //z=(uint32_t)x - y\n cout << z << endl; //z=4294967286\\. \n}\n```\n\n在上例中，有符号的`int`将自动转换为无符号的`int`，结果为`uint32_t z` = `-10`。另一方面，由于`−10`不能表示为无符号的`int`值，其十六进制值`0xFFFFFFF6`在二进制补码机上将被解释为`UINT_MAX - 9`(即`4294967286`)。\n\n# 评价问题的顺序\n\n以下示例涉及构造函数中类成员的初始化顺序。因为初始化顺序是类成员在类定义中出现的顺序，所以最好将每个成员的声明分成不同的行:\n\n```cpp\n//ch13_rca_order_of_evaluation.cpp\n#include <iostream>\nusing namespace std;\n\nclass A {\npublic:\n  A(int x) : v2(v1), v1(x) {\n  };\n  void print() {\n    cout << \"v1=\" << v1 << \",v2=\" << v2 << endl;\n  };\nprotected:\n  //bad: the order of the class member is confusing, better\n  //separate it into two lines for non-ambiguity order declare   \n  int v1, v2; \n};\n\nclass B {\npublic:\n  //good: since the initialization order is: v1 -> v2, \n  //after this we have: v1==x, v2==x.\n  B(int x) : v1(x), v2(v1) {};\n\n  //wrong: since the initialization order is: v1 -> v2, \n  //after this we have: v1==uninitialized, v2==x. \n  B(float x) : v2(x), v1(v2) {};\n  void print() {\n    cout << \"v1=\" << v1 << \", v2=\" << v2 << endl;\n  };\n\nprotected:\n  int v1; //good, here the declaration order is clear\n  int v2;\n};\n\nint main()\n{\n  A a(10);\n  B b1(10), b2(3.0f);\n  a.print();  //v1=10,v2=10,v3=10 for both debug and release\n  b1.print(); //v1=10, v2=10 for both debug and release\n  b2.print(); //v1=-858993460,v2=3 for debug; v1=0,v2=3 for release.\n}\n```\n\n在类`A`中，虽然声明顺序是`v1 -> v2`，但是将它们放在一行会让其他开发人员感到困惑。在`B`类的第一个构造函数中，`v1`将被初始化为`x`，然后`v2`将被初始化为`v1`，因为它的声明顺序是`v1->v2`。但是在其第二个构造函数中，`v1`将首先被初始化为`v2`(此时，`v2`还没有初始化！)，则`v2`将由`x`初始化。这导致了`v1`在调试和发布版本中输出值的不同。\n\n# 编译时检查与运行时检查\n\n以下示例显示运行时检查(整数类型变量云的位数)可以由编译时检查代替:\n\n```cpp\n//check # of bits for int\n//courtesy: https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines\nint nBits = 0; // don't: avoidable code\nfor (int i = 1; i; i <<= 1){\n     ++ nBits;\n}\nif (nBits < 32){\n    cerr << \"int too small\\n\";\n}\n```\n\n由于`int`可以是 16 位，也可以是 32 位，这取决于操作系统，因此本例无法实现其试图实现的目标。我们应该使用`int32_t`或者用以下内容替换它:\n\n```cpp\n static_assert(sizeof(int) >= 4); //compile-time check\n```\n\n另一个例子是将`n`整数的最大数量读入一维数组:\n\n```cpp\nvoid read_into(int* p, int n); // a function to read max n integers into *p\n...\nint v[10];\nread_into(v, 100); //bad, off the end, but the compile cannot catch this error.\n```\n\n这可以通过`span<int>`来解决:\n\n```cpp\nvoid read_into( span<int> buf); // read into a range of integers\n...\nint v[10];\nread_into(v); //better, the compiler will figure out the number of elements\n```\n\n这里的一般规则是尽可能在编译时进行分析，而不是推迟到运行时。\n\n# 避免内存泄漏\n\n内存泄漏意味着分配的动态内存永远无法释放。在 C 语言中，我们使用`malloc()`和/或`calloc()`来分配内存，使用`free()`来释放内存。在 C++ 中，`new`运算符和`delete`或`delete []`运算符用于动态管理内存。虽然借助智能指针和**资源获取即初始化** ( **RAII** )可以降低内存泄漏的风险，但如果我们希望构建高质量的代码，仍然需要遵循一些规则。\n\n首先，最简单的内存管理方式是您自己的代码从不分配的内存。比如只要能写`T x;`就不要写`T* x = new T();`或者`shared_ptr<T> x(new T() );`。\n\n接下来，不要使用自己的代码管理内存，如下所示:\n\n```cpp\nvoid f_bad(){\n T* p = new T() ;\n  ...                 //do something with p\n delete p ;           //leak if throw or return before reaching this line \n}\n```\n\n相反，尝试使用 RAII，如下所示:\n\n```cpp\nvoid f_better()\n{\n std::auto_ptr<T> p(new T()) ; //other smart pointers is ok also\n ...                           //do something with p\n //will not leak regardless whether this point is reached or not\n}\n```\n\n然后用`unique_ptr`代替`shared_ptr`，除非需要分享其所有权，如下:\n\n```cpp\nvoid f_bad()\n{\n shared_ptr<Base> b = make_shared<Derived>();\n ...            \n} //b will be destroyed at here\n```\n\n由于`b`是本地使用，不需要复制，所以它的`refcount`永远是`1`。这意味着我们可以用一个`unique_ptr`来代替它:\n\n```cpp\nvoid f_better()\n{\n unique_ptr<Base> b = make_unique<Derived>();\n ...            //use b locally\n}               //b will be destroyed at here\n```\n\n最后，即使真的需要自己动态管理内存，如果有`std container`库类可用，也不要手动分配内存。\n\n在本节中，我们学习了如何使用 RCA 定位问题，以及如何通过编写最佳实践来防止问题。接下来，我们将学习如何使用调试器工具来控制程序的逐行执行，并在运行期间检查变量和表达式的值。\n\n# 调试 C++ 程序\n\n调试是发现和解决程序问题或缺陷的过程。这可能包括交互式调试、数据/控制流分析以及单元和集成测试。在本节中，我们将只关注交互式调试，这是一个使用断点逐行执行源代码的过程，同时显示正在使用的变量的值及其相应的内存地址。\n\n# 调试 C/C++ 程序的工具\n\n根据您的开发环境，C++ 社区中有许多可用的工具。以下列表显示了不同平台上最受欢迎的。\n\n*   Linux/Unix:\n    *   **GDB** :免费开源**命令行界面** ( **CLI** )调试器。\n    *   **Eclipse** :免费开源**集成开发环境** ( **IDE** )。它不仅支持调试，还支持编译、分析和智能编辑。\n    *   **Valgrind** :另一款开源动态分析工具；它有利于调试内存泄漏和线程错误。\n    *   **亲和**:商业**图形用户界面** ( **图形用户界面**)工具，为 **GDB** 、 **LLDB** 和 **LLVM 调试器**而构建。\n    *   **DDD** :针对 **GDB** 、 **DBX** 、 **JDB** 、 **XDB、**和 **Python** 的开源数据显示调试器，它以图形的形式显示数据结构。\n    *   **Emacs 模式下的 GDB**:一个开源的 GUI 工具，使用 GNU Emacs 在和 GDB 一起调试的时候可以查看和编辑源代码。\n    *   **KDevelop** :面向 C/C++、Objective-等编程语言的免费开源 IDE 和调试器工具。\n    *   **奈米弗**:一款开源工具，在 **GNOME** 桌面环境下运行良好。\n    *   **SlickEdit** :调试多线程和多处理器代码的好工具。\n*   Windows:\n    *   **Visual Studio** :带 GUI 的商业工具，社区版免费。\n    *   **GDB** :这个也可以在 **Cygwin** 或者 **MinGW** 的帮助下在 Windows 中运行。\n    *   **Eclipse** :其 **C++ 开发工具** ( **CDT** )可以通过工具链中的 MinGW GCC 编译器安装在 Windows 上。\n*   苹果电脑:\n    *   **LLDB** :这是 macOS 上 **Xcode** 中的默认调试器，支持桌面和 iOS 设备及其模拟器上的 C/C++ 和 Objective-C。\n    *   **GDB** :这个 CLI 调试器也用在 macOS 和 iOS 系统上。\n    *   **Eclipse** :这个使用 GCC 的免费 IDE 适用于 macOS。\n\n由于 GDB 可以在所有平台上运行，我们将在下面的小节中向您展示如何使用 GDB。\n\n# GDB 概况\n\nGDB 代表 GNU 调试器，它允许开发人员看到另一个程序在执行时内部发生了什么，或者另一个程序在崩溃时正在做什么。GDB 可以做以下四件主要的事情:\n\n*   启动一个程序，并指定任何可能影响其行为的东西。\n*   在给定的条件下停止程序。\n*   检查程序停止时发生了什么。\n*   运行程序时更改变量值。这意味着我们可以尝试一些东西来纠正一个 bug 的影响和/或继续学习另一个 bug 的副作用。\n\n请注意，涉及两个程序或可执行文件:一个是 GDB，而另一个是要调试的程序。由于这两个程序可以在同一台机器上运行，也可以在不同的机器上运行，因此我们可以进行三类调试，如下所示:\n\n*   **本机调试**:两个程序运行在同一台机器上。\n*   **远程调试** : GDB 在主机上运行，被调试的程序在远程机器上运行。\n*   **模拟器调试** : GDB 在主机上运行，被调试的程序在模拟器上运行。\n\n基于撰写本书时的最新版本(GDB v8.3)，GDB 支持的语言包括 C、C++、Objective-C、Ada、Assembly、D、Fortran、Go、OpenCL、Modula-2、Pascal 和 Rust。\n\n由于 GDB 是调试行业中最先进的工具，并且非常复杂，具有许多功能，因此不可能在本节中了解其所有功能。相反，我们将通过查看示例来研究最有用的特性。\n\n# GDB 的例子\n\n在练习这些示例之前，我们需要通过运行以下代码来检查`gdb`是否已经安装在我们的系统上:\n\n```cpp\n~wus1/chapter-13$ gdb --help \n```\n\n如果显示以下信息，我们将准备开始:\n\n```cpp\nThis is the GNU debugger. Usage:\n gdb [options] [executable-file [core-file or process-id]]\n gdb [options] --args executable-file [inferior-arguments ...]\n\n Selection of debuggee and its files:\n --args Arguments after executable-file are passed to inferior\n --core=COREFILE Analyze the core dump COREFILE.\n --exec=EXECFILE Use EXECFILE as the executable.\n ...\n```\n\n否则，我们需要安装它。让我们看看如何在不同的操作系统上安装它:\n\n*   对于基于 Debian 的 Linux:\n\n```cpp\n~wus1/chapter-13$ s*udo apt-get install build-essential* \n```\n\n*   对于基于红帽的 Linux:\n\n```cpp\n~wus1/chapter-13$***sudo yum install  build-essential***\n```\n\n*   对于 macOS:\n\n```cpp\n~wus1/chapter-13$***brew install gdb***\n```\n\nWindows 用户可以通过 MinGW 发行版安装 GDB。苹果电脑将需要任务化配置。\n\n然后，再次键入`gdb --help`检查是否安装成功。\n\n# 设置断点和检查变量值\n\n在下面的例子中，我们将学习如何设置断点、继续、单步执行或单步执行函数、打印变量的值，以及如何在`gdb`中使用帮助。源代码如下:\n\n```cpp\n//ch13_gdb_1.cpp\n#include <iostream>\nfloat multiple(float x, float y);\nint main()\n{\n float x = 10, y = 20;\n float z = multiple(x, y);\n printf(\"x=%f, y=%f, x*y = %f\\n\", x, y, z);\n return 0;\n}\n\nfloat multiple(float x, float y)\n{\n float ret = x + y; //bug, should be: ret = x * y;\n return ret;\n}\n```\n\n正如我们在[第 3 章](03.html)、*面向对象编程的细节中提到的，*让我们在调试模式下构建这个程序，如下所示:\n\n```cpp\n~wus1/chapter-13$ g++ -g ch13_gdb_1.cpp -o ch13_gdb_1.out\n```\n\n注意，对于 g++，`-g`选项意味着调试信息将包含在输出二进制文件中。如果我们运行这个程序，它将显示以下输出:\n\n```cpp\nx=10.000000, y=20.000000, x*y = 30.000000\n```\n\n现在，让我们用`gdb`来看看 bug 在哪里。为此，我们需要执行以下命令行:\n\n```cpp\n~wus1/chapter-13$ gdb ch13_gdb_1.out\n```\n\n通过这样做，我们将看到以下输出:\n\n```cpp\nGNU gdb (Ubuntu 8.1-0ubuntu3) 8.1.0.20180409-git\n Copyright (C) 2018 Free Software Foundation, Inc.\n License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>\n This is free software: you are free to change and redistribute it.\n There is NO WARRANTY, to the extent permitted by law. Type \"show copying\"\n and \"show warranty\" for details.\n This GDB was configured as \"aarch64-linux-gnu\".\n Type \"show configuration\" for configuration details.\n For bug reporting instructions, please see:\n <http://www.gnu.org/software/gdb/bugs/>.\n Find the GDB manual and other documentation resources online at:\n <http://www.gnu.org/software/gdb/documentation/>.\n For help, type \"help\".\n Type \"apropos word\" to search for commands related to \"word\"...\n Reading symbols from a.out...done.\n (gdb) \n```\n\n现在，让我们详细看看各种命令:\n\n*   `break`和`run`:如果我们输入`b main`或`break main`，按*进入*，在主功能处会插入一个`breakpoint`。然后，我们可以键入`run`或`r`开始调试程序。以下信息将显示在终端窗口中。在这里，我们可以看到我们的第一个`breakpoint`在源代码的第六行，并且被调试的程序已经暂停，以便等待新的命令:\n\n```cpp\n(gdb) b main\nBreakpoint 1 at 0x8ac: file ch13_gdb_1.cpp, line 6.\n(gdb) r\nStarting program: /home/nvidia/wus1/Chapter-13/a.out\n[Thread debugging using libthread_db enabled]\nUsing host libthread_db library \"/lib/aarch64-linux-gnu/libthread_db.so.1\". \n\nBreakpoint 1, main () at ch13_gdb_1.cpp:6\n6 float x = 10, y = 20;\n```\n\n*   `next`、`print`和`quit` : 该`n`或**`next`**命令将转到下一行代码。如果该行调用一个子程序，它不会进入该子程序；相反，它跳过呼叫，将其视为一条源线路。如果我们想显示一个变量的值，我们可以使用`p`或`print`命令，后跟变量的名称。最后，如果我们想退出`gdb`，可以使用`q`或`quit`命令。以下是运行这些操作后终端窗口的输出:****\n\n```cpp\n(gdb) n\n 7 float z = multiple(x, y);\n (gdb) p z\n $1 = 0\n (gdb) n\n 8 printf(\"x=%f, y=%f, x*y = %f\\n\", x, y, z);\n (gdb) p z\n $2 = 30\n (gdb) q\n A debugging session is active.\n Inferior 1 [process 29187] will be killed.\n Quit anyway? (y or n) y\n ~/wus1/Chapter-13$\n```\n\n*   `step`:现在我们来学习一下如何踏入`multiple()`功能，找到 bug。为此，我们需要使用`b`、`r`和`n`命令重新开始，首先到达 7 号线。然后，我们可以使用`s`或`step`命令进入`multiple()`功能。接下来，我们使用`n`命令到达第 14 行，`p`打印`ret`变量的值，即 30。在这一点上，我们已经弄清楚了，用`ahha the bug is at line 14!:`，而不是`x*y`，我们有一个错别字，就是`x+y`。以下代码块是这些命令的相应输出:\n\n```cpp\n~/wus1/Chapter-13$gdb ch13_gdb_1.out\n ...\n (gdb) b main\n Breakpoint 1 at 0x8ac: file ch13_gdb_1.cpp, line 6.\n (gdb) r\n The program being debugged has been started already.\n Start it from the beginning? (y or n) y\n Starting program: /home/nvidia/wus1/Chapter-13/a.out\n [Thread debugging using libthread_db enabled]\n Using host libthread_db library \"/lib/aarch64-linux-gnu/libthread_db.so.1\".                                                                                Breakpoint 1, main () at ch13_gdb_1.cpp:6\n 6 float x = 10, y = 20;\n (gdb) n\n 7 float z = multiple(x, y);\n (gdb) s\n multiple (x=10, y=20) at ch13_gdb_1.cpp:14\n 14 float s = x + y;\n (gdb) n\n 15 return s;\n (gdb) p s\n $1 = 30\n```\n\n*   `help`:最后，我们来学习一下`help`命令，结束这个小例子。当`gdb`启动时，我们可以使用`help`或`h`命令在其命令输入行中获取特定命令的使用信息。例如，下面的“终端”窗口总结了我们到目前为止学到的内容:\n\n```cpp\n(gdb) h b      \n Set breakpoint at specified location.\n break [PROBE_MODIFIER] [LOCATION] [thread THREADNUM] [if CONDITION]\n PROBE_MODIFIER shall be present if the command is to be placed in a\n probe point. Accepted values are `-probe' (for a generic, automatically\n guessed probe type), `-probe-stap' (for a SystemTap probe) or\n `-probe-dtrace' (for a DTrace probe).\n LOCATION may be a linespec, address, or explicit location as described\n below.\n  ....\n\n (gdb) h r\n Start debugged program.\n You may specify arguments to give it.\n Args may include \"*\", or \"[...]\"; they are expanded using the\n shell that will start the program (specified by the \"$SHELL\" environment\n variable). Input and output redirection with \">\", \"<\", or \">>\"\n are also allowed.\n\n (gdb) h s\n Step program until it reaches a different source line.\n Usage: step [N]\n Argument N means step N times (or till program stops for another reason).\n\n (gdb) h n\n Step program, proceeding through subroutine calls.\n Usage: next [N]\n Unlike \"step\", if the current source line calls a subroutine,\n this command does not enter the subroutine, but instead steps over\n the call, in effect treating it as a single source line.\n\n (gdb) h p\n Print value of expression EXP.\n Variables accessible are those of the lexical environment of the selected\n stack frame, plus all those whose scope is global or an entire file.\n\n (gdb) h h\n Print list of commands.\n (gdb) h help\n Print list of commands.\n (gdb) help h\n Print list of commands.\n (gdb) help help\n Print list of commands.\n```\n\n至此，我们已经了解了一些可以用来调试程序的基本命令。这些命令是`break`、`run`、`next`、`print`、`quit`、`step`和`help`。我们将在下一小节中学习函数和条件断点、观察点以及`continue`和`finish`命令。\n\n# 函数断点、条件断点、观察点以及继续和完成命令\n\n在本例中，我们将学习如何设置函数断点、条件断点以及使用`continue`命令。然后，我们将学习如何完成一个函数调用，而不需要一步一步地执行所有代码行。源代码如下:\n\n```cpp\n//ch13_gdb_2.cpp\n#include <iostream>\n\nfloat dotproduct( const float *x, const float *y, const int n);\nint main()\n{\n float sxx,sxy;\n float x[] = {1,2,3,4,5};\n float y[] = {0,1,1,1,1};\n\n sxx = dotproduct( x, x, 5);\n sxy = dotproduct( x, y, 5);\n printf( \"dot(x,x) = %f\\n\", sxx );\n printf( \"dot(x,y) = %f\\n\", sxy );\n return 0;\n}\n\nfloat dotproduct( const float *x, const float *y, const int n )\n{\n const float *p = x;\n const float *q = x;  //bug: replace x by y\n float s = 0;\n for(int i=0; i<n; ++ i, ++ p, ++ q){\n        s += (*p) * (*q);\n }\n return s;\n}\n```\n\n同样，在构建并运行`ch13_gdb_2.cpp`之后，我们得到以下输出:\n\n```cpp\n~/wus1/Chapter-13$ g++ -g ch13_gdb_2.cpp -o ch13_gdb_2.out\n~/wus1/Chapter-13$ ./ch13_gdb_2.out\ndot(x,x) = 55.000000\ndot(x,y) = 55.000000\n```\n\n既然`dot(x,x)`和`dot(x,y)`给我们的结果是一样的，那么这里肯定有问题。现在，让我们通过学习如何在`dot()`函数中设置断点来调试它:\n\n*   **函数断点**:要在函数的开头设置断点，我们可以使用`b function_name`命令。像往常一样，我们可以在输入时使用制表符补全。例如，假设我们键入以下内容:\n\n```cpp\n(gdb) b dot<Press TAB Key>\n```\n\n如果我们这样做，下面的命令行将自动弹出:\n\n```cpp\n(gdb) b dotproduct(float const*, float const*, int)\n```\n\n如果它是一个类的成员函数，则应该包含它的类名，如下所示:\n\n```cpp\n(gdb) b MyClass::foo(<Press TAB key>\n```\n\n*   **条件断点**:设置条件断点有几种方式:\n\n```cpp\n(gdb) b f.cpp:26 if s==0 //set a breakpoint in f.cpp, line 26 if s==0\n(gdb) b f.cpp:20 if ((int)strcmp(y, \"hello\")) == 0 \n```\n\n*   **列出并删除断点**:一旦我们设置了几个断点，我们就可以列出或删除它们，如下所示:\n\n```cpp\n(gdb) i b (gdb) delete breakpoints 1 (gdb) delete breakpoints 2-5\n```\n\n*   **移除使** **成为断点** **无条件**:由于每个断点都有一个数字，我们可以从断点处移除一个条件，就像这样:\n\n```cpp\n(gdb) cond 1         //break point 1 is unconditional now\n```\n\n*   **观察点**:当表达式的值发生变化时，观察点可以停止执行，而不必预测它可能发生在哪里(哪一行)。有三种观察点:\n    *   `watch` : `gdb`将在写入发生时断开\n    *   `rwatch` : `gdb`将在读取发生时断开\n    *   `awatch` : `gdb`将在写入或读取发生时断开\n\n下面的代码显示了一个这样的例子:\n\n```cpp\n(gdb) watch v                 //watch the value of variable v\n(gdb) watch *(int*)0x12345678 //watch an int value pointed by an address\n(gdb) watch a*b + c/d         // watch an arbitrarily complex expression\n```\n\n*   **继续**:当我们在断点处检查完变量值后，我们可以使用`continue`或`c`命令继续程序执行，直到调试器遇到断点、信号、错误或正常进程终止。\n*   **完成**:一旦我们进入一个函数，我们可能会想继续执行它，直到它返回到它的调用者行。这可以使用`finish`命令来完成。\n\n现在，让我们将这些命令放在一起调试`ch13_gdb_2.cpp`。以下是终端窗口的输出。为了方便起见，我们将其分为三个部分:\n\n```cpp\n//gdb output of example ch13_gdb_2.out -- part 1\n~/wus1/Chapter-13$ gdb ch13_gdb_2.out                     //cmd 1\n ...\n Reading symbols from ch13_gdb_2.out ... done.\n\n (gdb) b dotproduct(float const*, float const*, int)      //cmd 2\n Breakpoint 1 at 0xa5c: file ch13_gdb_2.cpp, line 20.\n (gdb) b ch13_gdb_2.cpp:24 if i==1                        //cmd 3\n Breakpoint 2 at 0xa84: file ch13_gdb_2.cpp, line 24.\n (gdb) i b                                                //cmd 4\n Num Type Disp Enb Address What\n 1 breakpoint keep y 0x0000000000000a5c in dotproduct(float const*, float const*, int) at ch13_gdb_2.cpp:20\n 2 breakpoint keep y 0x0000000000000a84 in dotproduct(float const*, float const*, int) at ch13_gdb_2.cpp:24\n stop only if i==1\n (gdb) cond 2                                            //cmd 5\n Breakpoint 2 now unconditional.\n (gdb) i b                                               //cmd 6\n Num Type Disp Enb Address What\n 1 breakpoint keep y 0x0000000000000a5c in dotproduct(float const*, float const*, int) at ch13_gdb_2.cpp:20\n 2 breakpoint keep y 0x0000000000000a84 in dotproduct(float const*, float const*, int) at ch13_gdb_2.cpp:24 \n```\n\n在第一部分中，我们有以下六个命令:\n\n*   `cmd 1`:我们从构建的可执行文件的参数`ch13_gdb_2.out`开始`gdb`。这将向我们简要显示它的版本和文档以及使用信息，然后告诉我们读取符号的过程已经完成，正在等待下一个命令。\n*   `cmd 2`:我们设置了`breakpoint`功能(在`dotproduct()`)。\n*   `cmd 3`:设置条件`breakpoint`。\n*   `cmd 4`:它列出了关于断点的信息，告诉我们有两个断点。\n*   `cmd 5`:我们把`breakpoint 2`设为`unconditional`。\n*   `cmd 6`:我们再次列出断点信息。此时，我们可以看到两个断点。这些分别位于`ch13_gdb_2.cp`文件的第 20 行和第 24 行。\n\n接下来，让我们看看第二部分的`gdb`输出:\n\n```cpp\n//gdb output of example ch13_gdb_2.out -- part 2 \n(gdb) r                                                //cmd 7\n Starting program: /home/nvidia/wus1/Chapter-13/ch13_gdb_2.out\n [Thread debugging using libthread_db enabled]\n Using host libthread_db library \"/lib/aarch64-linux-gnu/libthread_db.so.1\".\n\n Breakpoint 1, dotproduct (x=0x7fffffed68, y=0x7fffffed68, n=5) at ch13_gdb_2.cpp:20\n 20 const float *p = x;\n (gdb) p x                                            //cmd 8\n $1 = (const float *) 0x7fffffed68\n (gdb) c                                              //cmd 9 \n Continuing.\n\n Breakpoint 2, dotproduct (x=0x7fffffed68, y=0x7fffffed68, n=5) at ch13_gdb_2.cpp:24\n 24 s += (*p) * (*q);\n (gdb) p i                                           //cmd 10\n $2 = 0\n (gdb) n                                             //cmd 11\n 23 for(int i=0; i<n; ++ i, ++ p, ++ q){\n (gdb) n                                             //cmd 12\n\n Breakpoint 2, dotproduct (x=0x7fffffed68, y=0x7fffffed68, n=5) at ch13_gdb_2.cpp:24\n 24 s += (*p) * (*q);\n (gdb) p s                                           //cmd 13 \n $4 = 1\n (gdb) watch s                                       //cmd 14 \n Hardware watchpoint 3: s\n```\n\n第二部分包含以下 cmd:\n\n*   `cmd 7`:通过给出`run`命令，程序开始运行，并在第 20 行的第一个断点处停止。\n*   `cmd 8`:我们打印`x`的值，*显示其地址。*\n*   `cmd 9`:我们继续节目。一旦继续，它将在第 24 行的第二个断点处停止。\n*   `cmd 10`:打印`i`的数值，为`0`。\n*   `cmd 11-12`:我们使用`next`命令两次。此时，`s += (*p)  *  (*q)`语句被执行。\n*   `cmd 13`:打印`s`的数值，为`1`。\n*   `cmd 14`:我们打印`s`的值。\n\n最后，第三部分如下:\n\n```cpp\n//gdb output of example ch13_gdb_2.out -- part 3 \n(gdb) n                                             //cmd 15 \n  Hardware watchpoint 3: s\n\n Old value = 1\n New value = 5\n dotproduct (x=0x7fffffed68, y=0x7fffffed68, n=5) at ch13_gdb_2.cpp:23\n 23 for(int i=0; i<n; ++ i, ++ p, ++ q){\n (gdb) finish                                       //cmd 16\n Run till exit from #0 dotproduct (x=0x7fffffed68, y=0x7fffffed68, n=5) at ch13_gdb_2.cpp:23\n\n Breakpoint 2, dotproduct (x=0x7fffffed68, y=0x7fffffed68, n=5) at ch13_gdb_2.cpp:24\n 24 s += (*p) * (*q);\n (gdb) delete breakpoints 1-3                       //cmd 17\n (gdb) c                                            //cmd 18\n Continuing.\n\n dot(x,x) = 55.000000\n dot(x,y) = 55.000000\n [Inferior 1 (process 31901) exited normally]\n [Inferior 1 (process 31901) exited normally]\n (gdb) q                                           //cmd 19\n ~/wus1/Chapter-13$\n```\n\n在这一部分中，我们有以下命令:\n\n*   `cmd 15`:我们使用`next`命令查看如果执行下一次迭代`s`的值是多少。显示`s`的旧值为`1` (s = 1*1)，新值为`5` (s=1*1+2*2)。到目前为止，一切顺利！\n*   `cmd 16`:使用`finish`命令继续运行程序，直到退出该功能。\n*   `cmd 17`:我们删除断点 1 到 3。\n*   `cmd 18`:使用`continue`命令。\n*   `cmd 19`:我们退出`gdb`回到终端窗口。\n\n# 将 gdb 登录到文本文件中\n\n当处理长堆栈跟踪或多线程堆栈跟踪时，查看和分析终端窗口的`gdb`输出可能会不方便。但是，我们可以先将整个会话或特定输出记录到文本文件中，然后使用其他文本编辑器工具离线浏览。为此，我们需要使用以下命令:\n\n```cpp\n(gdb) set logging on\n```\n\n当我们执行这个命令时，`gdb`会将所有终端窗口输出保存到当前运行的`gdb`文件夹中名为`gdb.txt`的文本文件中。如果我们想停止日志记录，我们可以只键入以下内容:\n\n```cpp\n(gdb) set logging off\n```\n\nGDB 的一大优点是，我们可以根据需要打开和关闭 set log 命令多次，而不用担心被转储的文件名。这是因为所有的输出都连接到`gdb.txt`文件中。\n\n这里有一个返回`ch13_gdb_2.out`的例子，其中`gdb`输出被转储:\n\n```cpp\n~/wus1/Chapter-13$ gdb ch13_gdb_2.out           //cmd 1\n ...\nReading symbols from ch13_gdb_2.out...done.\n (gdb) set logging on                           //cmd 2\n Copying output to gdb.txt.\n (gdb) b ch13_gdb_2.cpp:24 if i==1              //cmd 3 \n Breakpoint 1 at 0xa84: file ch13_gdb_2.cpp, line 24.\n (gdb) r                                        //cmd 4 \n ...\n Breakpoint 1, dotproduct (x=0x7fffffed68, y=0x7fffffed68, n=5) at ch13_gdb_2.cpp:24\n 24 s += (*p) * (*q);\n (gdb) p i                                      //cmd 5 \n $1 = 1\n (gdb) p s                                      //cmd 6 \n $2 = 1\n (gdb) finish                                   //cmd 7 \n Run till exit from #0 dotproduct (x=0x7fffffed68, y=0x7fffffed68, n=5) at ch13_gdb_2.cpp:24\n 0x00000055555559e0 in main () at ch13_gdb_2.cpp:11\n 11 sxx = dotproduct( x, x, 5);\n Value returned is $3 = 55\n (gdb) delete breakpoints 1                    //cmd 8\n (gdb) set logging off                         //cmd 9\n Done logging to gdb.txt.\n (gdb) c                                       //cmd 10 \n Continuing.\n dot(x,x) = 55.000000\n dot(x,y) = 55.000000\n [Inferior 1 (process 386) exited normally]\n (gdb) q                                      //cmd 11\n ~/wus1/Chapter-13$ cat gdb.txt               //cmd 12\n```\n\n前面代码中使用的命令如下:\n\n*   `cmd 1`:启动`gdb`。\n*   `cmd 2`:我们将日志标志设置为开。此时，`gdb`表示输出将被复制到`gdb.txt`文件中。\n*   `cmd 3`:设置条件`break point`。\n*   `cmd 4`:我们运行程序，当到达第 24 行的条件`breakpoint`时，程序停止。\n*   `cmd 5`和`cmd 6`:我们打印`i`和`s`的值，接受。\n*   `cmd 7`:通过执行功能命令的单步执行，显示`sxx`为`55`(调用`sxx=dotproduct( x, x, 5))`后)，程序停在`sxy *=* dotproduct( x, y, 5).`行\n*   `cmd 8`:我们删除`breakpoint 1`。\n*   `cmd 9`:我们将日志标志设置为关闭。\n\n*   `cmd 10`:一旦给出继续指令，则退出`main`功能，`gdb`等待新的命令。\n*   `cmd 11`:我们输入`q `退出`gdb`。\n*   `cmd 12`:当它回到终端窗口时，我们通过在操作系统中运行`cat`命令来打印记录的`gdb.txt`文件的内容。\n\n到目前为止，我们已经学习了足够的 GDB 命令来调试一个程序。正如你可能已经注意到的，这很耗时，因此非常昂贵。有时，由于在错误的地方调试，情况会变得更糟。为了有效地调试，我们需要遵循正确的策略。我们将在下一小节中介绍这一点。\n\n# 实用调试策略\n\n由于调试是软件开发生命周期中成本最高的阶段，因此发现错误并修复它们是不可行的，尤其是对于大型复杂系统。但是，在实际过程中可以使用某些策略，其中一些策略如下:\n\n*   **用 printf()或者 std::cout** :这是老套的做事方式。通过向终端打印一些信息，我们可以检查变量值，并在何时何地执行各种日志配置文件，以便进一步分析。\n*   **使用调试器**:虽然学习使用 GDB 类型的调试器工具不是一蹴而就的，但是可以节省很多时间。所以，试着一步一步逐渐熟悉它。\n*   **重现 bug**:每当现场报告 bug 时，记录运行环境并输入数据。\n*   **转储日志文件**:应用应该将日志消息转储到文本文件中。当崩溃发生时，我们应该首先检查日志文件，看看是否发生了异常事件。\n*   **猜一猜**:大致猜一个 bug 的位置，然后证明是对是错。\n*   **分而治之**:即使在我们不知道有什么 bug 的最坏情况下，我们仍然可以使用**二分搜索法**策略来设置断点，然后缩小范围并最终定位它们。\n*   **简化**:始终从最简化的场景开始，逐步增加外设、输入模块等，直到 bug 可以重现。\n\n*   **源代码版本控制**:如果一个 bug 突然出现在一个发行版上，但是之前运行的很好，那就先做一个源代码树检查。可能有人做了改变！\n*   **不要放弃**:有些 bug 真的很难定位和/或修复，尤其是复杂的多团队参与的系统。暂时把它们放在一边，在回家的路上重新考虑一下——T2 啊哈时刻可能最终会出现。\n\n到目前为止，我们已经了解了使用 RCA 进行宏观级别的问题定位，以及我们可以遵循的防止问题发生的良好编码实践。此外，通过使用最先进的调试器工具，如 GDB，我们可以逐行控制程序的执行，以便我们可以在微观层面分析和修复问题。所有这些活动都是程序员集中和手动的。有什么自动工具可以帮助我们诊断程序的潜在缺陷吗？我们将在下一节中研究静态和动态分析。\n\n# 理解静态和动态分析\n\n在前几节中，我们学习了根本原因分析过程以及如何使用 GDB 来调试缺陷。这一节将讨论如何分析一个程序，有没有执行它。前者称为动态分析，后者称为静态分析。\n\n# 静态分析\n\n静态分析评估计算机程序的质量而不执行它。虽然这通常可以通过自动工具和代码审查/检查来检查源代码，但我们在这一部分将只关注自动工具。\n\n自动静态代码分析工具旨在根据一组或多组编码规则或准则来分析一组代码。通常情况下，人们会交替使用静态代码分析*、*静态分析或源代码分析。通过用每个可能的代码执行路径扫描整个代码库，我们可以在测试阶段之前发现许多潜在的错误。但是，它也有几个限制，如下所示:\n\n*   它会产生假阳性和假阴性警报。\n*   它只应用扫描算法内部实现的规则，其中一些规则可能会被主观解释。\n*   它无法找到运行时环境中引入的漏洞。\n*   它可以提供一种虚假的安全感，即一切都在解决之中。\n\n在商业和免费开源类别下，大约有 30 种自动 C/C++ 代码分析工具[9]。这些工具的名称包括 Clang、Clion、CppCheck、Eclipse、Visual Studio 和 GNU g++，仅举几个例子。作为例子，我们想介绍一下`**-**Wall`、`-Weffcc++ `和`-Wextra`选项，它们内置在 GNU 编译器 g++ [10]中:\n\n*   `-Wall`:这将启用所有施工警告，这对于某些用户来说是有问题的。这些警告很容易避免或修改，即使与宏结合使用也是如此。它还启用了 C ++ 方言选项和 Objective-C/C ++ 方言选项中描述的一些特定于语言的警告。\n*   `-Wextra`:顾名思义，它检查某些没有被`-Wall`检查的额外警告标志。将打印以下任何情况的警告信息:\n    *   指针与整数零和`<`、`<=`、`>`或`>=`操作数进行比较。\n    *   非枚举数和枚举数出现在条件表达式中。\n    *   模糊的虚拟基地。\n    *   订阅一个`register`类型的数组。\n    *   使用`register`类型变量的地址。\n    *   派生类的复制构造函数不初始化它的基类。注意(b)-(f)只是 C++ 的。\n\n*   `-Weffc++ `:检查是否违反了斯科特·迈耶斯撰写的*有效且更有效的 C++* 中建议的一些准则。这些准则包括以下内容:\n    *   为具有动态分配内存的类定义复制构造函数和赋值运算符。\n    *   在构造函数中，初始化优于赋值。\n    *   在基类中使析构函数虚拟化。\n    *   让`=`操作员返回一个参考到`*this`。\n    *   当必须返回对象时，不要试图返回引用。\n    *   区分递增和递减运算符的前缀和后缀形式。\n    *   切勿超载`&&`、`||`或`,`。\n\n为了探索这三个选项，让我们看下面的例子:\n\n```cpp\n//ch13_static_analysis.cpp\n#include <iostream>\nint *getPointer(void)\n{\n    return 0;\n}\n\nint &getVal() {\n    int x = 5;\n    return x;\n}\n\nint main()\n{\n    int *x = getPointer();\n    if( x> 0 ){\n        *x = 5;\n   }\n   else{\n       std::cout << \"x is null\" << std::endl;\n   }\n\n   int &y = getVal();\n   std::cout << y << std::endl;\n   return 0;\n}\n```\n\n首先，让我们在没有任何选项的情况下构建它:\n\n```cpp\ng++ -o ch13_static.out ch13_static_analysis.cpp \n```\n\n这可以成功构建，但是如果我们运行它，不出所料，它会崩溃，并显示一条**分段故障** ( **核心转储**)消息。\n\n接下来，让我们添加`-Wall`、`-Weffc++ `、和**`-Wextra`**选项并重建它:****\n\n```cpp\ng++ -Wall -o ch13_static.out ch13_static_analysis.cpp\ng++ -Weffc++ -o ch13_static.out ch13_static_analysis.cpp\ng++ -Wextra -o ch13_static.out ch13_static_analysis.cpp\n```\n\n`-Wall`和`-Weffc++ `都给了我们以下信息:\n\n```cpp\nch13_static_analysis.cpp: In function ‘int& getVal()’:\nch13_static_analysis.cpp:9:6: warning: reference to local variable ‘x’ returned [-Wreturn-local-addr]\nint x = 5;\n ^\n```\n\n这里，它抱怨在`int & getVal()`函数中(在`cpp`文件的第 9 行)，返回了对局部变量的引用。这是行不通的，因为一旦程序退出功能，`x`就是垃圾(`x`的寿命只限于功能范围内)。引用死变量没有任何意义。\n\n`-Wextra`给了我们以下信息:\n\n```cpp\n ch13_static_analysis.cpp: In function ‘int& getVal()’:\n ch13_static_analysis.cpp:9:6: warning: reference to local variable ‘x’ returned [-Wreturn-local-addr]\n int x = 5;\n ^\n ch13_static_analysis.cpp: In function ‘int main()’:\n ch13_static_analysis.cpp:16:10: warning: ordered comparison of pointer with integer zero [-Wextra]\n if( x> 0 ){\n ^\n```\n\n前面的输出显示`*-*Wextra`不仅给出了来自`-Wall`的警告，还检查了前面提到的六件事。在这个例子中，它警告我们在代码的第 16 行有一个指针和整数零之间的比较。\n\n现在我们知道了如何在编译时使用静态分析选项，我们将通过执行一个程序来看看动态分析。\n\n# 动态分析\n\n*动态分析*是*动态程序分析*的简短版本，它通过在真实或虚拟处理器上执行软件程序来分析软件程序的性能。与静态分析类似，动态分析也可以自动或手动完成。例如，单元测试、集成测试、系统测试和验收测试通常是人参与的动态分析过程。另一方面，内存调试、内存泄漏检测和分析工具，如 IBM purify、Valgrind 和 Clang 是自动动态分析工具。在这一小节中，我们将重点介绍自动动态分析工具。\n\n动态分析过程包含准备输入数据、启动测试程序、收集必要的参数和分析其输出等步骤。粗略地说，动态分析工具的机制是它们使用代码插装和/或模拟环境来在被分析的代码执行时对其执行检查。我们可以通过以下方式与程序进行交互:\n\n*   **源代码插装**:编译前在原始源代码中插入一个特殊的代码段。\n*   **目标代码插装**:一个特殊的二进制代码被直接添加到可执行文件中。\n*   **编译阶段插装**:通过特殊的编译器开关增加一个校验码。\n*   它不会改变源代码。相反，它使用特殊的执行阶段库来检测错误。\n\n动态分析有以下优点:\n\n*   没有假阳性或假阴性结果，因为将检测到模型无法预测的错误。\n*   它不需要源代码，这意味着专有代码可以由第三方机构测试。\n\n动态分析的缺点如下:\n\n*   它只检测与输入数据相关的路线上的缺陷。可能找不到其他缺陷。\n*   它一次只能检查一个执行路径。为了获得完整的图片，我们需要尽可能多地运行测试。这需要大量的计算资源。\n*   它无法检查代码的正确性。从错误的操作中得到正确的结果是可能的。\n*   在真正的处理器上执行不正确的代码可能会产生意想不到的结果。\n\n现在，让我们使用 Valgrind 来查找以下示例中给出的内存泄漏和越界问题:\n\n```cpp\n//ch13_dynamic_analysis.cpp\n#include <iostream>\nint main()\n{\n    int n=10;\n    float *p = (float *)malloc(n * sizeof(float));\n    for( int i=0; i<n; ++ i){\n        std::cout << p[i] << std::endl;\n    }\n    //free(p);  //leak: free() is not called\n    return 0;\n}\n```\n\n要使用 Valgrind 进行动态分析，需要执行以下步骤:\n\n1.  首先，我们需要安装`valgrind`。我们可以使用以下命令来实现这一点:\n\n```cpp\nsudo apt install valgrind //for Ubuntu, Debian, etc.\n```\n\n2.  一旦安装成功，我们可以通过将可执行文件作为参数以及其他参数传递来运行`valgrind`，如下所示:\n\n```cpp\nvalgrind --leak-check=full --show-leak-kinds=all --track-origins=yes \\\n --verbose --log-file=valgrind-out.txt ./myExeFile myArgumentList\n```\n\n3.  接下来，让我们构建这个程序，如下所示:\n\n```cpp\ng++ -o ch13_dyn -std=c++ 11 -Wall ch13_dynamic_analysis.cpp\n```\n\n4.  然后，我们运行`valgrind`，像这样:\n\n```cpp\nvalgrind --leak-check=full --show-leak-kinds=all --track-origins=yes \\\n --verbose --log-file=log.txt ./ch13_dyn\n```\n\n最后可以查看`log.txt`的内容。粗体和斜体线条表示内存泄漏的位置和大小。通过查看地址(`0x4844BFC`)及其对应的功能名称(`main()`)，我们可以看到这个`malloc`在`main()`功能中:\n\n```cpp\n... //ignore many lines at begining\n by 0x108A47: main (in /home/nvidia/wus1/Chapter-13/ch13_dyn)\n ==18930== Uninitialised value was created by a heap allocation\n ==18930== at 0x4844BFC: malloc (in /usr/lib/valgrind/vgpreload_memcheck-arm64-linux.so)\n ... //ignore many lines in middle\n ==18930== HEAP SUMMARY:\n ==18930== in use at exit: 40 bytes in 1 blocks\n ==18930== total heap usage: 3 allocs, 2 frees, 73,768 bytes allocated\n ==18930==\n ==18930== 40 bytes in 1 blocks are definitely lost in loss record 1 of 1\n ==18930== at 0x4844BFC: malloc (in /usr/lib/valgrind/vgpreload_memcheck-arm64-linux.so)\n ==18930==\n ==18930== LEAK SUMMARY:\n ==18930== definitely lost: 40 bytes in 1 blocks\n ==18930== indirectly lost: 0 bytes in 0 blocks\n ==18930== possibly lost: 0 bytes in 0 blocks\n ==18930== still reachable: 0 bytes in 0 blocks\n ==18930== suppressed: 0 bytes in 0 blocks\n```\n\n在这里，我们可以看到`malloc()`被调用来在地址`0x4844BFC`分配一些内存。堆摘要部分表明我们在`0x4844BFC`有 40 字节的内存损失。最后，泄漏总结部分显示，肯定有一块 40 字节的内存丢失。通过在`log.txt`文件中搜索`0x4844BFC`的地址值，我们最终发现在原始代码中没有`free(p)`行被调用。取消这条线的注释后，我们重新进行`valgrind`分析，因此泄漏问题现在不在报告中。\n\n总之，借助静态和动态分析工具，程序的潜在缺陷可以自动大大减少。然而，为了确保软件的质量，人类必须参与最终的测试和评估。现在，我们将探讨软件工程中的单元测试、测试驱动开发和行为驱动开发概念。\n\n# 探索单元测试、TDD 和 BDD\n\n在前一节中，我们学习了自动静态和动态程序分析。这一部分将集中在人参与的(准备测试代码)测试，这是动态分析的另一部分。这些是单元测试、测试驱动开发和行为驱动开发。\n\n单元测试假设如果我们已经有了一个单一的代码单元，那么我们需要编写一个测试驱动程序并准备输入数据来检查它的输出是否正确。之后，我们执行集成测试来一起测试多个单元，然后是验收测试，测试整个应用。由于集成和验收测试比单元测试更难维护，更与项目相关，因此在本书中涵盖它们是非常具有挑战性的。感兴趣的可以去[https://www.iso.org/standard/45142.html](https://www.iso.org/standard/45142.html)了解更多。\n\n与单元测试相反，TDD 认为我们应该先有测试代码和数据，开发一些代码并使其快速通过，最后重构直到客户满意。另一方面，BDD 的理念是，我们不应该测试一个程序的实现，而应该测试它期望的行为。为此，BDD 强调，参与软件生产的人员之间也应该建立一个交流平台和语言。\n\n我们将在下面的小节中详细讨论这些方法。\n\n# 单元测试\n\n单元是更大或更复杂的应用中的单个组件。通常，一个单元有它自己的用户界面，如一个函数、一个类或整个模块。单元测试是一种软件测试方法，用于确定代码单元的行为是否符合设计要求。单元测试的主要特征如下:\n\n*   它小而简单，编写和运行速度快，因此，它可以在早期开发周期中发现问题，因此可以轻松修复问题。\n*   因为它是独立于依赖项的，所以每个测试用例都可以并行运行。\n*   单元测试驱动程序帮助我们理解单元接口。\n*   当被测试的单元稍后被集成时，它极大地帮助集成和验收测试。\n*   它通常由开发人员准备和执行。\n\n虽然我们可以从头开始编写单元测试包，但是社区中已经开发了很多**单元测试框架** ( **UTFs** )。助推。Test、CppUnit、GoogleTest、Unit++、CxxTest 是最受欢迎的。这些 utf 通常提供以下功能:\n\n*   他们只需要最少的工作来建立一个新的测试。\n*   它们依赖于标准库并支持跨平台，这意味着它们易于移植和修改。\n*   它们支持测试夹具，这允许我们为几个不同的测试重用相同的对象配置。\n*   它们能很好地处理异常和崩溃。这意味着 UTF 可以报告异常，但不能报告崩溃。\n*   它们有很好的断言功能。每当断言失败时，就应该打印它的源代码位置和变量值。\n*   它们支持不同的输出，这些输出可以方便地由人类或其他工具进行分析。\n*   它们支持测试套件，每个套件可能包含几个测试用例。\n\n现在，让我们看一下 Boost UTF 的一个例子(从 1.59.0 版开始)。它支持三种不同的用法变体:单头变体、静态库变体和共享库变体。它包括四种类型的测试用例:无参数测试用例、数据驱动测试用例、模板测试用例和参数化测试用例。\n\n它还有七种类型的检查工具:`BOOST_TEST()`、`BOOST_CHECK()`、`BOOST_REQUIRE(`)、`BOOST_ERROR()`、`BOOST_FAIL()`、`BOOST_CHECK_MESSAGE( )`和`BOOST_CHECK_EQUAL()`。它支持夹具，并以多种方式控制测试输出。编写测试模块时，我们需要遵循以下步骤:\n\n1.  定义我们测试程序的名称。这将用于输出消息。\n2.  选择一种用法变体:仅标题、与静态链接或作为共享库。\n3.  选择一个测试用例并将其添加到测试套件中。\n4.  对测试过的代码进行正确性检查。\n5.  在每个测试用例之前初始化测试中的代码。\n6.  自定义报告测试失败的方式。\n7.  控制构建的测试模块的运行时行为，这也称为运行时配置。\n\n例如，以下示例涵盖了*步骤 1-4* 。如果您感兴趣，可以在[https://www . boost . org/doc/libs/1 _ 70 _ 0/libs/test/doc/html/index . html](https://www.boost.org/doc/libs/1_70_0/libs/test/doc/html/index.html)获得*步骤 5-7* 的示例:\n\n```cpp\n//ch13_unit_test1.cpp\n#define BOOST_TEST_MODULE my_test //item 1, \"my_test\" is module name\n#include <boost/test/included/unit_test.hpp> //item 2, header-only\n\n//declare we begin a test suite and name it \"my_suite \"\nBOOST_AUTO_TEST_SUITE( my_suite ) \n\n//item 3, add a test case into test suit, here we choose \n//        BOOST_AUTO_TEST_CASE and name it \"test_case1\" \nBOOST_AUTO_TEST_CASE(test_case1) {\n char x = 'a';\n BOOST_TEST(x);        //item 4, checks if c is non-zero\n BOOST_TEST(x == 'a'); //item 4, checks if c has value 'a'\n BOOST_TEST(x == 'b'); //item 4, checks if c has value 'b'\n}\n\n//item 3, add the 2nd test case\nBOOST_AUTO_TEST_CASE( test_case2 )\n{\n  BOOST_TEST( true );\n}\n\n//item 3, add the 3rd test case\nBOOST_AUTO_TEST_CASE( test_case3 )\n{\n  BOOST_TEST( false );\n}\n\nBOOST_AUTO_TEST_SUITE_END() //declare we end test suite\n```\n\n为此，我们可能需要安装 boost，如下所示:\n\n```cpp\nsudo apt-get install libboost-all-dev\n```\n\n然后，我们可以构建和运行它，如下所示:\n\n```cpp\n~/wus1/Chapter-13$ g++ -g  ch13_unit_test1.cpp \n~/wus1/Chapter-13$ ./a.out\n```\n\n前面的代码产生以下输出:\n\n```cpp\nRunning 3 test cases...\n ch13_unit_test1.cpp(13): error: in \"my_suite/test_case1\": check x == 'b' has failed ['a' != 'b']\n ch13_unit_test1.cpp(25): error: in \"my_suite/test_case3\": check false has failed\n\n *** 2 failures are detected in the test module \"my_test\"\n```\n\n这里我们可以看到`test_case1`和`test_case3`有故障。特别是在`test_case1`中，`x`的值不等于`b`，显然虚开的支票无法通过`test_case3`中的测试。\n\n# TimeDivisionDuplex 时分双工\n\n如下图所示，TDD 过程从编写失败的测试代码开始，然后添加/修改代码以让测试通过。之后，我们重构测试计划和代码，直到满足所有需求[16，17]。让我们看一下下图:\n\n![](img/48d849fe-1729-4c98-8497-c0500b7f3080.png)\n\n*第一步*是写一个失败的测试。TDD 不是先开发代码，而是先开始编写测试代码。因为我们还没有代码，我们知道，如果我们运行测试，它将失败。在这个阶段，定义了测试数据格式和接口，并设想了代码实现细节。\n\n*第 2 步*的目标是用最少的开发工作让测试尽快通过。我们不想完美地实现所有事情；我们只想让它通过测试。一旦它变成绿色，我们将有一些东西展示给客户，并告诉客户，此时客户可能会在看到初始产品后细化需求。然后，我们进入下一阶段。\n\n第三个阶段是重构。在这个阶段，我们可能会进去，看看，看看我们想改变什么，以及如何改变。\n\n对于传统开发人员来说，TDD 最难的是从编码->测试模式到测试->编码模式的思维转变。为了对测试套件有一个模糊的概念，J. Hartikainen 建议开发人员考虑以下五个步骤[18]来开始:\n\n1.  首先决定输入和输出。\n2.  选择类/函数签名。\n3.  只决定测试功能的一个微小方面。\n4.  实施测试。\n5.  实现代码。\n\n一旦我们完成了这个迭代，我们就可以逐渐重构它，直到实现整体的综合目标。\n\n# TDD 示例\n\n接下来，我们将通过一个案例研究的实施来演示 TDD 过程。在本研究中，我们将开发一个 Mat 类来执行 2D 矩阵代数，就像我们在 Matlab 中所做的那样。这是一个类模板，可以保存所有数据类型的 m 乘 n 矩阵。矩阵代数包括加、减、乘、除矩阵，还具有元素运算能力。\n\n我们开始吧。\n\n# 第一步——写一个失败的测试\n\n首先，我们只需要以下内容:\n\n*   根据给定的行数和列数创建一个`Mat`对象(默认值应该是 0 乘 0，这是一个空矩阵)。\n*   逐行打印其元素。\n*   从`rows()`和`cols()`得到矩阵大小。\n\n基于这些要求，我们可以使用失败的单元测试代码来提升 UTF，如下所示:\n\n```cpp\n// ch13_tdd_boost_UTF1.cpp\n#define BOOST_TEST_MODULE tdd_test\n#include <boost/test/included/unit_test.hpp>\n#include \"ch13_tdd_v1.h\"\n\nBOOST_AUTO_TEST_SUITE(tdd_suite)  //begin a test suite: \"tdd_suite\"\n\nBOOST_AUTO_TEST_CASE(test_case1) {\n  Mat<int> x(2, 3);            //create a 2 x 3 int matrix\n  x.print(\"int x=\");\n  BOOST_TEST(2 == x.rows());\n  BOOST_TEST(3 == x.cols());\n\n  Mat<float> y;              //create a 0 x 0 empty float matrix\n  y.print(\"float y=\");\n  BOOST_TEST(0 == y.rows());\n  BOOST_TEST(0 == y.cols());\n\n  Mat<char> z(1,10);       //create a 1 x 10 char matrix\n  z.print(\"char z=\");\n  BOOST_TEST(1 == z.rows());\n  BOOST_TEST(10 == z.cols());\n}\nBOOST_AUTO_TEST_SUITE_END() //end test suite\n```\n\n既然我们的测试代码已经准备好了，我们就可以开发代码了。\n\n# 步骤 2–开发代码以通过测试\n\n实现最小代码段的一种方法是通过前面的测试，如下所示:\n\n```cpp\n//file: ch13_tdd_v1.h\n#ifndef __ch13_TDD_V1__\n#define __ch13_TDD_V1__\n#include <iostream>\n#include <assert.h>\ntemplate< class T>\nclass Mat {\npublic:\n  Mat(const uint32_t m=0, const uint32_t n=0);\n  Mat(const Mat<T> &rhs) = delete;\n  ~Mat();\n\n  Mat<T>& operator = (const Mat<T> &x) = delete;\n\n  uint32_t rows() { return m_rows; }\n  uint32_t cols() { return m_cols; }\n  void print(const char* str) const;\n\nprivate:\n  void creatBuf();\n  void deleteBuf();\n  uint32_t m_rows; //# of rows\n  uint32_t m_cols; //# of cols\n  T* m_buf;\n};\n#include \"ch13_tdd_v1.cpp\"\n#endif\n```\n\n一旦我们有了前面的头文件，我们就可以开发它对应的`cpp`文件，如下所示:\n\n```cpp\n//file: ch13_tdd_v1.cpp\n#include \"ch13_tdd_v1.h\"\nusing namespace std;\n\ntemplate< class T>\nMat<T>::Mat(const uint32_t m, const uint32_t n)\n : m_rows(m)\n , m_cols(n)\n , m_buf(NULL)\n{\n creatBuf();\n}\n\ntemplate< class T>\nMat<T> :: ~Mat()\n{ \n deleteBuf(); \n}\n\ntemplate< class T>\nvoid Mat<T>::creatBuf()\n{\n uint32_t sz = m_rows * m_cols;\n if (sz > 0) {\n if (m_buf) { deleteBuf();}\n m_buf = new T[sz];\n assert(m_buf);\n }\n else {\n m_buf = NULL;\n }\n}\n\ntemplate< class T>\nvoid Mat<T>::deleteBuf()\n{\n if (m_buf) {\n delete[] m_buf;\n m_buf = NULL;\n }\n}\n\ntemplate< class T>\nvoid Mat<T> ::print(const char* str) const\n{\n cout << str << endl;\n cout << m_rows << \" x \" << m_cols << \"[\" << endl;\n const T *p = m_buf;\n for (uint32_t i = 0; i<m_rows; i++) {\n for (uint32_t j = 0; j < m_cols; j++) {\n cout << *p++ << \", \";\n }\n cout << \"\\n\";\n }\n cout << \"]\\n\";\n}\n```\n\n假设我们使用 g++ 构建并执行它，它支持`-std=c++ 11`或更高版本:\n\n```cpp\n~/wus1/Chapter-13$ g++ -g ch13_tdd_boost_UTF1.cpp~/wus1/Chapter-13$ a.out \n```\n\n这将导致以下输出:\n\n```cpp\nRunning 1 test case...\n int x=2 x 3[\n 1060438054, 1, 4348032,\n 0, 4582960, 0,\n ]\n float y=0 x 0[\n ]\n char z=1 x 10[\n s,s,s,s,s,s,s,s,s,s,\n ]\n```\n\n在`test_case1`中，我们创建了三个矩阵并测试了`rows()`、`cols()`和`print()`功能。第一个是 2x3 `int`型矩阵。由于没有初始化，其元素的值是不可预测的，这就是为什么我们可以从`print()`看到这些随机数。我们在这一点上也通过了`rows()`和`cols()`测试(两个`BOOST_TEST() calls`没有错误)。第二种是空浮点型矩阵；它的`print()`功能什么都不给，它的`cols()`和`rows()`都是零。最后，第三个是 1x10 `char`类型的未初始化矩阵。同样，这三个函数的所有输出都是预期的。\n\n# 第三步——重构\n\n目前为止，一切顺利——我们通过了测试！但是，在向我们的客户展示了前面的结果之后，他/她可能会要求我们再添加两个接口，如下所示:\n\n*   为所有元素创建一个具有给定初始值的 m×n 矩阵。\n*   相加`numel()`返回矩阵的元素总数。\n*   添加`empty()`，如果矩阵为零行或零列，则返回真，否则返回假。\n\n一旦我们将第二个测试用例添加到我们的测试套件中，整个重构的测试代码将如下所示:\n\n```cpp\n// ch13_tdd_Boost_UTF2.cpp\n#define BOOST_TEST_MODULE tdd_test\n#include <boost/test/included/unit_test.hpp>\n#include \"ch13_tdd_v2.h\"\n\n//declare we begin a test suite and name it \"tdd_suite\"\nBOOST_AUTO_TEST_SUITE(tdd_suite)\n\n//add the 1st test case\nBOOST_AUTO_TEST_CASE(test_case1) {\n  Mat<int> x(2, 3);\n  x.print(\"int x=\");\n  BOOST_TEST(2 == x.rows());\n  BOOST_TEST(3 == x.cols());\n\n  Mat<float> y;\n  BOOST_TEST(0 == y.rows());\n  BOOST_TEST(0 == y.cols());\n\n  Mat<char> z(1, 10);\n  BOOST_TEST(1 == z.rows());\n  BOOST_TEST(10 == z.cols());\n}\n\n//add the 2nd test case\nBOOST_AUTO_TEST_CASE(test_case2)\n{\n  Mat<int> x(2, 3, 10);\n  x.print(\"int x=\");\n  BOOST_TEST( 6 == x.numel() );\n  BOOST_TEST( false == x.empty() );\n\n  Mat<float> y;\n  BOOST_TEST( 0 == y.numel() );\n  BOOST_TEST( x.empty() ); //bug x --> y \n}\n\nBOOST_AUTO_TEST_SUITE_END() //declare we end test suite\n```\n\n下一步是修改代码以通过这个新的测试计划。为简洁起见，这里不打印`ch13_tdd_v2.h`和`ch13_tdd_v2.cpp`文件。你可以从这本书的 [GitHub](https://github.com/PacktPublishing/Expert-CPP) 资源库下载它们。构建并执行`ch13_tdd_Boost_UTF2.cpp`后，我们得到如下输出:\n\n```cpp\nRunning 2 test cases...\n int x=2x3[\n 1057685542, 1, 1005696,\n 0, 1240624, 0,\n ]\n int x=2x3[\n 10, 10, 10,\n 10, 10, 10,\n ]\n ../Chapter-13/ch13_tdd_Boost_UTF2.cpp(34): error: in \"tdd_suite/test_case2\": che\n ck x.empty() has failed [(bool)0 is false]\n```\n\n在第一个输出中，由于我们只是定义了一个 2x3 整数矩阵，并没有在`test_case1`中初始化它，所以未定义的行为——即六个随机数——被打印出来。第二个输出来自`test_case2`，这里`x`的所有六个元素都被初始化为`10`。在我们展示并讲述了前面的结果之后，我们的客户可能会要求我们添加其他新功能或修改现有功能。但是，经过几次迭代，最终，我们将到达*快乐点*并停止分解。\n\n既然我们已经了解了 TDD，我们将讨论 BDD。\n\n# BDD\n\n软件开发最困难的部分是与业务参与者、开发人员和质量分析团队沟通。由于误解或模糊的需求、技术争论和缓慢的反馈周期，项目很容易超出预算、错过最后期限或完全失败。\n\n(BDD) [20]是一个敏捷开发过程，有一套旨在减少沟通差距/障碍和其他浪费活动的实践。它还鼓励团队成员在生产生命周期中不断与现实世界的例子交流。\n\nBDD 包含两个主要部分:刻意发现和 TDD。为了让不同组织和团队的人理解所开发软件的正确行为，刻意发现阶段引入了*示例映射*技术，通过具体的示例让不同角色的人进行对话。这些例子将成为自动化测试和系统行为的活文档。在其 TDD 阶段，BDD 指定任何软件单元的测试都应该根据单元的期望行为来指定。\n\n针对不同的平台和编程语言，有几种 BDD 框架工具(JBehave、RBehave、Fitnesse、黄瓜[21]，等等)。一般来说，这些框架执行以下步骤:\n\n1.  阅读由业务分析师在审慎发现阶段准备的规范格式文档。\n2.  将文档转换成有意义的子句。每个单独的子句都能够被设置为质量保证的测试用例。开发人员也可以从子句中实现源代码。\n3.  为每个子句场景自动执行测试。\n\n总之，我们已经了解了关于应用开发管道中应该包括什么、何时、如何以及测试过程的策略。如下图所示，传统的 V 形[2]模型强调需求->设计->编码->测试的模式。TDD 认为开发过程应该由测试驱动，而 BDD 将来自不同背景和角色的人之间的交流添加到 TDD 框架中，并专注于行为测试:\n\n![](img/fcf1a324-da5d-4573-9146-c831408113a7.png)\n\n此外，单元测试强调在编码完成时测试单个组件。TDD 更侧重于如何在编写代码之前编写测试，然后通过下一级测试计划添加/修改代码。BDD 鼓励客户、业务分析师、开发人员和质量保证分析师之间的合作。虽然我们可以单独使用每一个，但在这个敏捷软件开发时代，我们真的应该将它们结合起来以获得最佳结果。\n\n# 摘要\n\n在本章中，我们简要介绍了软件开发过程中与测试和调试相关的主题。测试发现问题，根本原因分析有助于在宏观层面上定位问题。然而，良好的编程实践可以在早期阶段防止软件缺陷。此外，被称为 GDB 的命令行界面调试工具可以帮助我们设置断点并逐行执行程序，同时在程序运行时打印变量值。\n\n我们还讨论了自动分析工具和人工参与的测试过程。静态分析在不执行程序的情况下评估程序的性能。另一方面，动态分析工具可以通过执行程序来发现缺陷。最后，我们了解了软件开发管道中应该包含什么、什么时候以及如何包含测试过程的策略。单元测试强调在编码完成时测试单个组件。TDD 更关注如何在开发代码之前编写测试，然后通过下一级测试计划来重申这个过程。BDD 鼓励客户、业务分析师、开发人员和质量保证分析师之间的合作。\n\n在下一章中，我们将学习如何使用 Qt 为运行在 Linux、Windows、iOS 和 Android 系统上的跨平台应用创建**图形用户界面** ( **GUI** )程序。首先，我们将深入研究跨平台图形用户界面编程的基本概念。然后我们将介绍 Qt 及其小部件的概述。最后，通过一个案例，我们将学习如何使用 Qt 设计和实现一个网络应用。\n\n# 进一步阅读\n\n*   J.鲁尼和范登·赫维尔， *[初学者根本原因分析](http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.618.8544&rep=rep1&type=pdf)*质量进步，2004 年 7 月，第 45-53 页。\n*   T.软件问题根源分析方法。修订版，第 73 号，第 81 页，2011 年。\n*   K.A. Briski 等 *[最小化代码缺陷提高软件质量降低开发成本](ftp://ftp.software.ibm.com/software/rational/info/do-more/RAW14109USEN.pdf)*IBM Rational Software Analyzer 和 IBM Rational PurifyPlus 软件。\n*   [https://www . learncpp . com/CPP-programming/八字-c-programming-errors-the-编译器-不会-catch](https://www.learncpp.com/cpp-programming/eight-c-programming-mistakes-the-compiler-wont-catch) 。\n*   B.斯特鲁特普和 h .萨特， *C++ 核心指南*:[https://isocpp.github.io/CppCoreGuidelines](https://isocpp.github.io/CppCoreGuidelines)。\n\n*   [https://www.gnu.org/software/gdb/](https://www.gnu.org/software/gdb/)。\n*   [https://www . fayewilliams . com/2014/02/21/debug-for-初学者/](https://www.fayewilliams.com/2014/02/21/debugging-for-beginners/) 。\n*   [https://www.perforce.com/blog/qac/what-static-code-analysis](https://www.perforce.com/blog/qac/what-static-code-analysis)。\n*   [https://Linux . the . net/man/1/g++ ](https://linux.die.net/man/1/g++)。\n*   [https://www . embedded . com/static-vs-dynamic-analysis-for-secure-code-development-part-2/](https://www.embedded.com/static-vs-dynamic-analysis-for-secure-code-development-part-2/)。\n*   国际标准化组织/国际电工委员会/国际电工委员会 29119-1:2013 [*软件和系统工程–软件测试*](https://www.iso.org/standard/45142.html) 。\n*   [http://games from inside . com/exploring-the-c-unit-testing-framework-丛林](http://gamesfromwithin.com/exploring-the-c-unit-testing-framework-jungle)。\n*   [https://www . boost . org/doc/libs/1 _ 70 _ 0/libs/test/doc/html/index . html](https://www.boost.org/doc/libs/1_70_0/libs/test/doc/html/index.html)。\n*   K.Beck， *[通过实例进行测试驱动开发](https://www.amazon.com/Test-Driven-Development-Kent-Beck/dp/0321146530)，*由 Addison Wesley 出版，ISBN 978-0321146533。\n*   H.关于测试优先编程方法的有效性 ，Proc。美国电气和电子工程师协会传输。关于软件工程，31(1)。2005 年 1 月。\n*   [https://codeutopia . net/blog/2015/03/01/unit-testing-TDD-and-BDD](https://codeutopia.net/blog/2015/03/01/unit-testing-tdd-and-bdd)。\n*   [https://cucumber.io/blog/intro-to-bdd-and-tdd/](https://cucumber.io/blog/intro-to-bdd-and-tdd/)。\n*   D.北，介绍 BDD，[https://dannorth.net/introducing-bdd/](https://dannorth.net/introducing-bdd/)(2006 年 3 月)。\n*   D.北，东基奥等。艾尔，“[jbehave.org/team-list](https://jbehave.org/)”，2019 年 5 月。\n\n除此之外，您还可以看看以下来源(这些在本章中没有直接提及):\n\n*   B.Stroustrup 和 H. Sutter， *C++ 核心指南* : [。](https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines)\n*   G.*加速。测试*:[https://www . boost . org/doc/libs/1 _ 70 _ 0/libs/test/doc/html/index . html](https://www.boost.org/doc/libs/1_70_0/libs/test/doc/html/index.html)\n*   D.北，*引入 BDD*:[https://dannorth.net/introducing-bdd/](https://dannorth.net/introducing-bdd/)\n\n# 练习和问题\n\n1.  使用`gdb`功能断点、条件断点和`watchpoint`、`continue`和`finish`命令，调试`ch13_gdb_2.cpp`。\n2.  使用`g++ -c -Wall -Weffc++ -Wextra  x.cpp -o x.out`构建`cpp files ch13_rca*.cpp`。从他们的警告输出中你看到了什么？\n3.  为什么静态分析会产生假警报，而动态分析不会？\n4.  下载`ch13_tdd_v2.h/.cpp`并执行下一阶段重构。在这个阶段，我们将添加复制构造函数、赋值操作符和元素操作符，如`+`、`-`、`*`、`/`等。更具体地说，我们需要做以下事情:\n    1.  将第三个测试用例添加到我们的测试套件中，即`ch13_tdd_Boost_UTF2.cpp`。\n    2.  将这些函数的实现添加到文件中；例如`ch13_tdd_v2.h/.cpp`。\n    3.  运行测试套件来测试它们。********"
  },
  {
    "path": "docs/exp-cpp/14.md",
    "content": "# 十四、使用 Qt 的图形用户界面\n\nC++ 不提供**图形用户界面** ( **图形用户界面**)开箱即用的编程。首先，我们应该理解图形用户界面与特定的**操作系统** ( **操作系统**)紧密相关。您可以使用窗口应用编程接口在窗口中编程图形用户界面应用，或者使用特定于 Linux 的应用编程接口在 Linux 中编程图形用户界面应用，等等。每个操作系统都有自己特定形式的窗口和图形用户界面组件。\n\n我们在[第 1 章](01.html)、*构建 C++ 应用*中谈到了不同的平台及其差异。当讨论 GUI 编程时，平台之间的差异甚至更令人生畏。跨平台开发已经成为图形用户界面开发人员生活中的一大痛苦。他们必须专注于特定的操作系统。为其他平台实现相同的应用需要花费几乎相同的工作量。那是对时间和资源的不合理的巨大浪费。像 *Java* 这样的语言提供了一个在虚拟环境中运行应用的智能模型。这允许开发人员专注于一种语言和一个项目，因为环境负责在不同的平台上运行应用。这种方法的主要缺点之一是迫使用户安装虚拟机，并且与特定于平台的应用相比，执行时间较慢。\n\n为了解决这些问题，创建了 Qt 框架。在本章中，我们将了解 Qt 框架如何支持跨平台的图形用户界面应用开发。为此，您需要熟悉 Qt 及其关键特性。这将允许您使用您最喜欢的编程语言——c++ 开发图形用户界面应用。我们将从了解 Qt 的 GUI 开发方法开始，然后我们将介绍它的概念和特性，例如信号和插槽，以及 Model/View 编程。\n\n在本章中，我们将涵盖以下主题:\n\n*   跨平台图形用户界面编程基础\n*   Qt 核心组件\n*   使用 Qt 小部件\n*   使用 Qt 网络设计网络应用\n\n# 技术要求\n\n您需要安装最新的 Qt 框架来运行本章中的示例。我们建议使用 Qt Creator 作为项目的集成开发环境。要下载 Qt 以及相应的工具，请访问 [qt.io](https://www.qt.io/) 网站，选择框架的开源版本。本章代码可在:[https://github.com/PacktPublishing/Expert-CPP](https://github.com/PacktPublishing/Expert-CPP)找到。\n\n# 了解跨平台图形用户界面编程\n\n每个操作系统都有自己的应用编程接口。它特别与图形用户界面相关。当公司计划设计、实现和发布桌面应用时，他们应该决定关注什么平台。在一个平台上工作的开发团队将花费几乎相同的时间为另一个平台编写相同的应用。最大的原因是操作系统提供了不同的方法和应用编程接口。应用编程接口的复杂性也可能在按时实现应用方面发挥重要作用。例如，以下来自官方文档的片段显示了如何使用 C++ 在 Windows 中创建按钮:\n\n```cpp\nHWND hwndButton = CreateWindow(\n  L\"BUTTON\", // Predefined class; Unicode assumed      \n  L\"OK\", // Button text      \n  WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_DEFPUSHBUTTON, // Styles      \n  10, // x position      \n  10, // y position      \n  100, // Button width     \n  100, // Button height     \n  m_hwnd, // Parent window     \n  NULL, // No menu.     \n  (HINSTANCE)GetWindowLong(m_hwnd, GWL_HINSTANCE),     \n  NULL); // Pointer not needed.\n```\n\n解决 Windows GUI 编程需要您使用`HWND`、`HINSTACNCE`和许多其他奇怪的命名和混淆的组件。\n\n。NET 框架对 Windows 图形用户界面编程进行了大刀阔斧的改进。如果要支持 Windows 以外的 OS，使用前要三思。NET 框架。\n\n然而，为了支持多个操作系统，您仍然必须深入研究应用编程接口来实现相同的应用，以覆盖所有操作系统用户。下面的代码展示了一个在 Linux 中使用 *Gtk+* GUI 工具包创建按钮的例子:\n\n```cpp\nGtkWidget* button = gtk_button_new_with_label(\"Linux button\");\n```\n\n相比 Windows API，似乎更容易理解一点。但是，您应该深入到`GtkWidgets`和其他带有 *Gtk* 前缀的组件中，以了解更多关于它们的信息。\n\n正如我们已经提到的，跨平台语言，如 Java 和。NET Core 使用虚拟机在不同的平台上运行代码。Qt 框架使用基于平台的编译方法支持跨平台图形用户界面编程。让我们针对 C++ 语言来讨论这两种方法。\n\n# 使用 C++ 作为 Java\n\nJava 或 C#等语言有不同的编译模型。本书的第一章介绍了 C++ 编译模型。首先，我们认为 C++ 是一种完全可编译的语言，而 Java 维护的是一种混合模型。它将源代码编译成称为**字节码**的中间表示，然后虚拟机通过将其翻译成特定平台的机器代码来运行它。\n\n下图描述了 C++ 和 Java 编译模型之间的差异:\n\n![](img/c0552bd5-d588-48b5-b7ec-9491231fbe30.png)\n\n**Java 虚拟机** ( **JVM** )作为中间层。它对每个平台都有一个独特的实现。用户需要在运行 Java 程序之前安装虚拟机的具体实现。安装过程只发生一次。另一方面，C++ 程序被翻译成机器代码，在没有 JVM 等中间层环境的情况下运行。这就是为什么 C++ 应用通常更快的原因之一。当我们在某个平台上编译 C++ 程序时，编译器输出一个由特定于该平台的格式的指令组成的可执行文件。当我们将应用移动到另一个平台时，它就是无法运行。\n\n另一个平台不能识别它的格式，也不能识别它的指令(尽管它们在某些方面可能相似)。Java 方法通过呈现一些字节码来工作，这些字节码对于虚拟机的所有实现都是相同的。但是虚拟机确切地知道它们应该为作为输入提供的字节码生成哪个指令。如果安装了虚拟机，相同的字节码可以在许多计算机上运行。下图演示了 Java 应用编译模型:\n\n![](img/4395de50-bc9b-4255-a882-8203041b2429.png)\n\n如您所见，源代码被编译成字节码，可以在每个操作系统上运行。但是，每个操作系统都必须有自己的虚拟机实现。这意味着，如果我们安装了专门为任何操作系统实现的 JVM，我们就可以在该操作系统上运行 Java 应用。\n\n虽然 C++ 是一种跨平台语言，这意味着我们不会修改代码以在其他平台上编译它，但该语言不支持开箱即用的图形用户界面编程。如前所述，要对图形用户界面应用进行编程，我们需要直接从代码中访问操作系统应用编程接口。这使得 C++ 图形用户界面应用依赖于平台，因为您需要修改代码库才能在另一个平台上编译它。下图显示了图形用户界面如何破坏语言的跨平台特性:\n\n![](img/2cba1673-03ed-49cd-89b0-123652d0f46b.png)\n\n虽然应用的逻辑、名称和任务可能是相同的，但是它现在有三个不同的实现，有三个不同的可执行文件。为了将应用交付给最终用户，我们需要发现他们的操作系统并交付正确的可执行文件。在网上下载应用时，您可能会遇到类似的情况。他们提供基于操作系统的下载应用。这就是 Qt 出手相救的地方。让我们看看如何。\n\n# Qt 的跨平台模式\n\nQt 是一个流行的小部件工具包，用于创建图形用户界面应用。它还允许我们创建运行在各种系统上的跨平台应用。Qt 由以下模块组成:\n\n*   **Qt 核心**:核心类\n*   **Qt****GUI**:GUI 组件的基类\n\n*   **Qt 小部件**:用 C++ 小部件扩展 Qt 图形用户界面的类\n*   **Qt 多媒体**:音频、视频、广播和相机功能类\n*   **Qt 多媒体部件**:实现多媒体功能的类\n*   **Qt 网络**:网络编程类(本章我们会用到)\n*   **Qt 建模语言** ( **QML** ):一个用自定义用户界面构建应用的声明性框架\n*   **Qt SQL** :使用 SQL 进行数据库集成的类\n*   **Qt Quick 系列模块**:本书不会讨论的 QML 相关模块列表\n*   **Qt 测试**:单元测试 Qt 应用的类\n\n我们在程序中使用的每个模块都通过一个扩展名为`.pro`的项目文件连接到编译器。该文件描述了`qmake`构建应用所需的一切。 *qmake* 是一个旨在简化构建过程的工具。我们在项目的`.pro`文件中描述了项目组件(源代码、Qt 模块、库等等)。例如，一个使用 Qt Widgets 和 Qt Network 并由`main.cpp`和`test.cpp`文件组成的项目对于`.pro`文件将具有以下内容:\n\n```cpp\nQT += widgets\nQT += network\nSOURCES += test.cpp\nSOURCES += main.cpp\n```\n\n我们也可以在`.pro`文件中指定平台特定的来源，如下所示:\n\n```cpp\nQT += widgets\nQT += network\nSOURCES += test.cpp\nSOURCES += main.cpp\nwin32 {\n SOURCES += windows_specific.cpp\n}\nunix {\n SOURCES += linux_world.cpp\n}\n```\n\n当我们在 Windows 环境中构建应用时，`windows_specific.cpp`文件将参与构建过程。与此相反，在 Unix 环境中构建时，将包含`linux_world.cpp`文件，而忽略`windows_specific.cpp`文件。至此，我们来到了 Qt 应用的编译模型。\n\nQt 提供跨平台编程的强大能力的全部要点是元编译源代码；也就是说，在代码被传递给 C++ 编译器之前，Qt 编译器会通过引入或替换特定于平台的组件来清理它。例如，当我们使用按钮组件(`QPushButton`)时，如果在 Windows 环境中编译，它将被特定于 Windows 的按钮组件替换。这就是为什么`.pro`文件也可以包含针对项目的特定于平台的修改。下图描述了此编译:\n\n![](img/ea855027-5cfa-4dbb-861e-3c3c908077f2.png)\n\n元编译器通常被称为**元对象编译器** ( **MOC** )。这种方法的好处在于，产生的输出代表了我们在没有虚拟机的情况下运行的相同的机器代码。我们可以马上发送可执行文件。这种方法的缺点是，我们在不同的平台上有不同的可执行文件。然而，我们只编写一个应用——不需要使用不同的语言，不需要钻研特定于操作系统的应用接口，也不需要研究特定于操作系统的图形用户界面组件类名。正如 Qt 所说，*写一次，到处编译*。现在，让我们继续构建一个简单的 GUI 应用。\n\n# 编写简单的应用\n\n我们不会讨论本书前面提到的所有模块，因为这需要一本全新的书。更多信息，请参考本章末尾的*进一步阅读*部分。`main`功能如下:\n\n```cpp\n#include <QtWidgets>\n\nint main(int argc, char** argv)\n{\n  QApplication app(argc, argv);\n\n  QPushButton btn(\"Click me!\");\n  btn.show();\n\n  return app.exec();\n}\n```\n\n让我们看看代码中使用的各种组件。第一个是`QtWidgets`头文件。它包含小部件组件，我们可以使用这些组件为我们的应用构建细粒度的图形用户界面。接下来是`QPushButton`类，它代表一个可点击按钮的包装器。我们有意将它作为包装器引入这里，以便我们在本章后面讨论 Qt 程序的编译过程时能够解释它。以下是运行上述代码的结果:\n\n![](img/10a68757-4e71-4c70-a722-b05add3fda63.png)\n\n如您所见，我们只声明了`QPushButton`类，但它显示为一个窗口，带有操作系统标准的关闭和最小化按钮(在示例中，这是 macOS)。之所以这样，是因为`QPushButton`间接继承了`QWidget`，是一个带边框的小部件；也就是一扇窗户。这个按钮几乎占据了窗户的全部空间。我们可以调整窗口的大小，看看按钮是如何随之调整大小的。我们将在本章后面更详细地讨论小部件。\n\n当我们运行`app.exec()`时，图形用户界面就建立了。注意`app`对象的类型。这是一个`QApplication`的对象。这是 Qt 应用的起点。当我们调用`exec()`函数时，我们启动 Qt 的事件循环。为了理解图形用户界面应用的生命周期，我们对程序执行的理解应该有所改变。在[第七章](07.html)、*功能编程*之后，重新定义程序构建和执行的概念应该不会让你感到惊讶。这次没那么难。这里要知道的主要事情是，图形用户界面应用有一个与主程序一起运行的附加实体。这个实体被称为**事件循环**。\n\nRecall the event loop, which we discussed in [Chapter 11](11.html), *Designing a Strategy Game Using Design Patterns*. The game represents a program with visual components that the user intensively interacts with. The same relates to regular GUI applications with buttons, labels, and other graphical components.\n\n用户与应用交互，每个用户操作都被解释为一个事件。然后，每个事件都被推入队列。事件循环逐个处理这些事件。处理事件意味着调用附加到事件的特殊处理函数。例如，只要点击一个按钮，就调用`keyPressedEvent()`功能。这是一个虚拟函数，因此我们可以在设计自定义按钮时覆盖它，如以下代码所示:\n\n```cpp\nclass MyAwesomeButton : public QPushButton\n{\n  Q_OBJECT\npublic:\n void keyPressedEvent(QKeyEvent* e) override\n {\n // anything that we need to do when the button is pressed\n }\n};\n```\n\n事件的唯一参数是指向`QEvent`子类型`QKeyEvent`的指针。`QEvent`是 Qt 中所有事件类的基类。注意放在班级开始块后面的奇怪的`Q_OBJECT`。这是一个特定于 Qt 的宏，如果你想让它们被 Qt 的 MOC 发现，它应该放在你定制类的第一行。\n\n在下一节中，我们将介绍特定于 Qt 对象的信号和槽的机制。为了使我们的定制对象支持该机制，我们在类定义中放置`Q_OBJECT`宏。\n\n现在，让我们构建一个比简单按钮更大的东西。以下示例创建一个标题为`Mastering C++ `的窗口:\n\n```cpp\n#include <QtWidgets>\n\nint main(int argc, char** argv)\n{\n  QApplication app(argc, argv);\n QWidget window;\n window.resize(120, 100);\n window.setWindowTitle(\"Mastering C++\");\n window.show();\n\n  return app.exec();\n}\n```\n\n下面是我们通过执行前面的程序得到的结果:\n\n![](img/9d9f3077-f55d-4e9d-969c-943ce57ae816.png)\n\n标题被剪了；我们只能看到桅杆...掌握 C++ 的一部分。现在，如果我们手动调整它的大小或更改源代码，使其对于`resize()`函数的第二个参数具有更大的值，我们会得到以下结果:\n\n![](img/e176177a-de4c-4ab2-8043-e63400a3a368.png)\n\n`window`对象属于`QWidget`类型。`QWidget`是所有用户界面对象的中心类。无论何时想要创建自定义小部件或扩展现有小部件，都可以直接或间接从`QWidget`继承。它对每个用例都有很多功能。可以使用`move()`功能在屏幕中移动，可以通过调用`showFullScreen()`使窗口全屏，等等。在前面的代码中，我们调用了`resize()`函数，该函数采用宽度和高度来调整小部件的大小。此外，注意`setWindowTitle()`函数，它完全按照 tin 上说的做——它将传递的字符串参数设置为窗口的标题。在代码中使用字符串值时，最好使用`QApplication::translate()`函数。它使程序本地化变得更加容易，因为当语言设置改变时，Qt 会自动用正确的翻译替换文本。`QObject::tr()`提供了几乎相同的功能。\n\n`QObject` is the base class of all Qt types. In languages such as Java or C#, every object is directly or indirectly inherited from a generic type, mostly named `Object`. C++ doesn't incorporate a common base class. Qt, on the other hand, introduces `QObject`, which comes with the base functionality that all objects should support.\n\n既然我们已经触及了 Qt 应用开发的基础，让我们更深入地了解一下这个框架，并发现它的关键特性。\n\n# 发现 Qt\n\nQt 随着时间的推移而演变，在撰写本书时，它的版本是 5.14。它的第一个公开预发行版本于 1995 年公布。二十多年过去了，现在 Qt 有了很多强大的功能，几乎在所有平台上都有使用，包括安卓和 iOS 等移动系统。除了少数例外，我们可以自信地用 C++ 和 Qt 为所有平台编写功能齐全的 GUI 应用。这是一个巨大的游戏规则改变者，因为公司雇佣专门研究一种技术的小团队，而不是每个特定平台有几个团队。\n\n如果你是 Qt 的新手，强烈建议你尽可能地熟悉它(参见本章末尾的书籍参考)。除了图形用户界面框架提供的常规组件之外，Qt 还引入了几个新的或在框架中灵活实现的概念。一个这样的概念是使用信号和插槽的对象之间的通信。\n\n# 抓住信号和槽\n\nQt 引入了信号和槽的概念，作为对象之间灵活的通信机制。信号和槽的概念及其实现机制是 Qt 区别于其他图形用户界面框架的特征之一。在前几章中，我们讨论了观察者模式。这种模式的主要思想是让一个对象向其他对象(订阅者)通知一个事件。信号和时隙的机制类似于观察者模式的实现。这是一种对象通知另一个对象其变化的方式。Qt 提供了一个通用接口，可以通过将一个对象的信号连接到另一个对象的插槽来将对象连接在一起。信号和槽都是对象的常规成员函数。信号是在对象的指定操作上调用的函数。插槽是作为订户的另一个功能。它由信号函数调用。\n\n正如我们之前提到的，Qt 向我们介绍了所有对象的基本类型，`QObject`。支持信号和插槽的基本功能在`QObject`中实现。您在代码中声明的任何对象、`QWidget`、`QPushButton`和其他对象都继承自`QObject`，因此它们都支持信号和插槽。QObject 为我们提供了两个管理对象通信的功能。这些物体是`connect()`和`disconnect()`:\n\n```cpp\nbool connect(const QObject* sender, const char* signal, \n  const QObject* receiver, const char* method, \n  Qt::ConnectionType type = Qt::AutoConnect);\n\nbool disconnect(const QObject* sender, const char* signal, \n  const QObject* receiver, const char* method);\n```\n\n如您所见，`connect()`函数将`receiver`和`sender`对象作为参数。此外，它还采用信号和插槽的名称。`signal`与发送方相关，而`slot`是接收方提供的。下图显示了这一点:\n\n![](img/2583b4a4-6b3d-4aa9-b879-885fcb19b63b.png)\n\n在编写 Qt 应用时，使用信号和插槽进行操作将变得很自然，迟早，你会认为其他所有框架都会支持信号和插槽，因为它们很方便。另外，注意信号和槽在`connect()`和`disconnect()`功能中作为字符串处理。为了指定连接对象时的信号和插槽，我们使用了另外两个宏，`SIGNAL()`和`SLOT()`。从现在开始不再引入宏，我们保证。\n\n下面是我们如何将两个物体连接在一起。假设我们想要更改标签的文本(一个`QLabel`的实例)，这样当按钮被点击时它会收到一个信号。为此，我们将`QPushButton`的`clicked()`信号连接到`QLabel`的插槽，如下所示:\n\n```cpp\nQPushButton btn(\"Click me!\");\nQLabel lbl;\nlbl.setText(\"No signal received\");\nQObject::connect(&btn, SIGNAL(clicked()), &lbl, SLOT(setText(const QString&)));\n```\n\n前面的代码可能看起来有点冗长，但是你会习惯的。将其视为信号和插槽的便捷机制的价格。然而，前面的例子不会给我们所需的结果；也就是说，它不会将标签的文本设置为表示它收到了信号。我们应该以某种方式将该字符串传递给标签的插槽。`clicked()`信号对我们来说并不是这样。实现这一点的方法之一是扩展`QLabel`，使其实现一个自定义槽，将文本设置为`received a signal`。我们可以这样做:\n\n```cpp\nclass MyLabel : public QLabel\n{\nQ_OBJECT\npublic slots:\n  void setCustomText() { \n    this->setText(\"received a signal\");\n  }\n};\n```\n\n为了声明一个槽，我们指定了部分，就像我们在前面的代码中所做的那样。信号的声明方式几乎相同:通过用`signals:`指定一个部分。唯一的区别是信号不能是私有的或受保护的。我们只是宣布它们是:\n\n```cpp\nclass Example\n{\nQ_OBJECT:\npublic:\n  // member functions\npublic slots:\n  // public slots\nprivate slots:\n  // private slots\nsignals: // no public, private, or protected\n  // signals without any definition, only the prototype\n};\n```\n\n现在，我们应该只更新前面的代码，以便更改标签的信号(以及标签对象的类型):\n\n```cpp\nQPushButton btn(\"Click me!\");\nMyLabel lbl;\nlbl.setText(\"No signal received\");\nQOBject::connect(&btn, SIGNAL(clicked()), &lbl, SLOT(setCustomText()));\n```\n\n我们说当信号发出时会调用槽。您也可以在对象内部声明和发出信号。与信号和插槽相关的一个重要细节是，它们独立于图形用户界面事件循环。\n\n当发出信号时，立即执行连接的插槽。但是，我们可以通过传递`Qt::ConnectionType`之一作为`connect()`函数的第五个参数来指定连接的类型。它包含以下值:\n\n*   `AutoConnection`\n*   `DirectConnection`\n*   `QueuedConnection`\n*   `BlockingQueuedConnection`\n*   `UniqueConnection`\n\n在`DirectConnection`中，当信号发出时，槽立即被调用。另一方面，当使用`QueuedConnection`时，当执行返回到接收器对象线程的事件循环时，槽被调用。`BlockingQueuedConnection`类似于`QueuedConnection`，只是信号线程被阻塞，直到槽返回一个值。`AutoConnection`可以是`DirectConnection`也可以是`QueuedConnection`。类型由发出信号的时间决定。如果接收器和发射器在同一线程中，则使用`DirectConnection`；否则，连接到`QueuedConnection`。最后，`UniqueConnection`用于前面描述的任何连接类型。它使用按位“或”与其中一个进行组合。其唯一目的是使`connect()`功能在信号和线程之间已经建立连接的情况下失效。\n\n信号和槽形成了一个强大的机制，使 Qt 成为图形用户界面编程中一个杰出的框架。我们介绍的下一种机制在框架中很流行，它与我们在应用中操作数据的方式有关。\n\n# 理解模型/视图编程\n\n模型/视图编程源于**模型视图控制器** ( **MVC** )设计模式。该模式背后的主要思想是将您的问题分解成三个松散耦合的组件，如下所示:\n\n*   负责存储和操作数据的模型\n*   视图，负责呈现和可视化数据\n*   控制器，负责附加的业务逻辑，并从模型向视图提供数据\n\n通过它的演变，我们现在有了一种简化且更方便的编程方法，称为**模型/视图编程**。它类似于 MVC 模式，只是它通过使视图和模型更关心手边的功能而省略了控制器。我们可以说视图和控制器在模型/视图架构中结合在一起。看看下面的架构图:\n\n![](img/f039425a-e465-49bd-81db-34ca31d2918f.png)\n\n模型表示数据，数据与其源进行通信，并为体系结构中的其他组件提供方便的接口。模型的实现及其与其他组件的通信是基于手头的数据类型的。\n\n视图通过获取所谓的模型索引来获取对数据项的引用。视图可以检索数据并将其提供给模型。关键是，可以使用视图编辑数据项，委托扮演着与模型通信的角色，以保持数据同步。\n\n每个引入的组件——模型、视图和委托——都是由提供公共接口的抽象类定义的。在某些情况下，类还提供功能的默认实现。为了编写专门的组件，我们从抽象类中子类化。当然，模型、视图和委托使用信号和插槽进行通信，我们在上一节中已经介绍过了。\n\n当模型遇到数据变化时，它会通知视图。另一方面，来自视图的信号通知用户与呈现的数据项的交互。最后，来自委托的信号通知模型和视图关于数据编辑的状态。\n\n模型基于`QAbstractItemModel`类，该类定义了视图和委托使用的接口。Qt 提供了一组现有的模型类，我们无需修改就可以使用；但是，如果需要创建新模型，应该从`QAbstractItemModel`继承类。例如，`QStringListModel`、`QStandardItemModel`和`QFileSystemModel`类都是现成的，可以处理数据项。`QStringListModel`用于存储字符串项列表(表示为`QString`对象)。此外，还有用于处理 SQL 数据库的方便的模型类。`QSqlQueryModel`、`QSqlTableModel`和`QSqlRelationalTableModel`允许我们在模型/视图约定的上下文中访问关系数据库。\n\n视图和委托也有对应的抽象类，即`QAbstractItemView`和`QAbstractItemDelegate`。Qt 提供了可以立即使用的现有视图，如`QListView`、`QTableView`和`QTreeView`。这些是大多数应用处理的基本视图类型。`QListView`显示项目列表，`QTableView`在表格中显示数据，`QTreeView`在层次列表中显示数据。如果您想使用这些视图类，Qt 建议从`QAbstractListModel`或`QAbstractTableModel`继承您的定制模型，而不是子类化`QAbstractItemModel`。\n\n`QListView`、`QTreeView`、`QTableView`被认为是核心低级类。有更方便的类为 Qt 程序员新手提供更好的可用性-`QListWidget`、`QTreeWidget`和`QTableWidget`。我们将在本章的下一节中查看使用小部件的示例。在此之前，我们先来看一个简单的`QListWidget`动作的例子:\n\n```cpp\n#include <QListWidget>\n\nint main(int argc, char** argv)\n{\n  QApplication app(argc, argv);\n  QListWidget* listWgt{new QListWidget};\n  return app.exec();\n}\n```\n\n向列表小部件添加项目的方法之一是创建它们，我们可以通过将列表小部件设置为其所有者来实现。在下面的代码中，我们声明了三个`QListWidgetItem`对象，每个对象都有一个名称，并且与我们前面声明的列表小部件相关联:\n\n```cpp\nnew QListWidgetItem(\"Amat\", listWgt);\nnew QListWidgetItem(\"Samvel\", listWgt);\nnew QListWidgetItem(\"Leia\", listWgt);\n```\n\n或者，我们可以声明一个项目，然后将其插入列表小部件:\n\n```cpp\nQListWidgetItem* newName{new QListWidgetItem};\nnewName->setText(\"Sveta\");\nlistWgt->insertItem(0, newName);\n```\n\n`insertItem()`成员函数的第一个参数是要插入项目的`row`的数量。我们将`Sveta`项目放在列表的第一位。\n\n既然我们已经触及了行的概念，我们应该回到模型和它们的索引。该模型将数据封装为数据项的集合。模型中的每个项目都有一个由`QModelIndex`类指定的唯一索引。这意味着模型中的每一项都可以被相关的模型索引访问。要获得模型索引，我们需要使用`index()`功能。下图描述了一个以类似表的结构组织其数据的模型:\n\n![](img/394f992b-dd5a-4d0b-a1cf-ad364cf58851.png)\n\n视图使用此约定来访问模型中的数据项。但是，请注意，视图在如何向用户呈现数据方面不受限制。如何以方便用户的方式呈现数据取决于视图实现。下图显示了数据在模型中的组织方式:\n\n![](img/8cadf899-4035-46fc-9457-dc68738f5099.png)\n\n下面是我们如何使用模型索引访问第 1 行第 2 列的特定数据项:\n\n```cpp\nQModelIndex itemAtRow1Col2 = model->index(1, 2);\n```\n\n最后，让我们声明一个视图，并为其设置一个模型，以查看模型/视图编程的运行情况:\n\n```cpp\nQStringList lst;\nlst << \"item 1\" << \"item 2\" << \"item 3\";\n\nQStringListModel model;\nmodel.setStringList(lst);\n\nQListView lview;\nlview.setModel(model);\n```\n\n一旦我们熟悉了 Qt 提供的各种小部件，我们将在下一节继续这个例子。\n\n# 使用 Qt 小部件\n\n小部件是可视化的图形用户界面组件。如果一个小部件没有父部件，它将被视为一个窗口，或者称为**顶级小部件**。在本章的前面，我们在 Qt 中创建了最简单的窗口，如下面的代码所示:\n\n```cpp\n#include <QtWidgets>\n\nint main(int argc, char** argv)\n{\n  QApplication app(argc, argv);\n QWidget window;\n window.resize(120, 100);\n window.setWindowTitle(\"Mastering C++\");\n window.show();\n\n  return app.exec();\n}\n```\n\n如您所见，`window`对象没有父对象。事情是这样的，`QWidget`的构造器取另一个`QWidget`作为当前一个的父。因此，当我们声明一个按钮并希望它是我们的`window`对象的子对象时，我们按照以下方式进行:\n\n```cpp\n#include <QtWidgets>\n\nint main(int argc, char** argv)\n{\n  QApplication app(argc, argv);\nQWidget window;\n  window.resize(120, 100);\n  window.setWindowTitle(\"Mastering C++\");\n  window.show();\n\n QPushButton* btn = new QPushButton(\"Click me!\", &window);\n\n  return app.exec();\n}\n```\n\n观察`QPushButton`构造函数的第二个参数。我们传递了对`window`对象的引用作为其父对象。当父对象被销毁时，其子对象会自动被销毁。Qt 还支持许多其他小部件；让我们来看看其中的一些。\n\n# 常见 Qt 小部件\n\n在前一节中，我们介绍了`QPushButton`类，并声明它间接继承了`QWidget`类。为了创建一个窗口，我们使用了`QWidget`类。事实证明，QWidget 代表了呈现给屏幕的能力，它是所有 Widget 继承的基本类。它有很多属性和功能，比如`enabled`，一个布尔属性，如果小部件被启用，它就是真的。要访问它，我们使用`isEnabled()`和`setEnabled()`功能。为了控制小部件的大小，我们使用它的`height`和`width`，它们代表小部件的高度和宽度。为了得到他们的价值观，我们分别称之为`height()`和`width()`。要设置新的高度和宽度，我们应该使用`resize()`函数，该函数接受两个参数——宽度和高度。您也可以使用`setMinimumWidth()`、`setMinimumHeight()`、`setMaximumWidth()`和`setMaximumHeight()`功能控制小部件的最小和最大尺寸。当您在布局中设置小部件时，这可能会很有用(参见下一节)。除了属性和功能，我们主要对 QWidget 的公共槽感兴趣，具体如下:\n\n*   `close()`:关闭小部件。\n*   `hide()`:相当于`setVisible(false)`，此功能隐藏小部件。\n*   `lower()`和`raise()`:在父小部件的堆栈中移动小部件(底部或顶部)。每个小部件可以有一个父小部件。没有父小部件的小部件成为一个独立的窗口。我们可以使用`setWindowTitle()`和`setWindowIcon()`功能设置该窗口的标题和图标。\n*   `style`:属性保存小部件的样式。要修改它，我们使用`setStyleSheet()`函数，传递一个描述小部件样式的字符串。另一种方法是调用`setStyle()`函数，并传递一个封装样式相关属性的`QStyle`类型的对象。\n\nQt 小部件几乎具备了开箱即用的所有必要属性。您很少遇到必须构建自己的小部件的情况。然而，一些团队为他们的软件创建了一整套定制的小部件。如果你打算为你的程序定制外观，那很好。例如，您可以合并平面样式的小部件，这意味着您必须修改框架提供的默认小部件的样式。自定义小部件应该继承自`QWidget`(或其任何后代)，如下所示:\n\n```cpp\nclass MyWidget : public QWidget\n{}; \n```\n\n如果希望小部件公开信号和插槽，需要在类声明的开头使用`Q_OBJECT`宏。更新后的`MyWidget`类的定义如下:\n\n```cpp\nclass MyWidget : public QWidget\n{\nQ_OBJECT\npublic:\n  // public section\n\nsignals: \n  // list of signals\n\npublic slots:\n  // list of public slots\n};\n```\n\n正如您可能已经猜到的，信号没有访问修饰符，而插槽可以分为公共、私有和受保护的部分。正如我们之前提到的，Qt 提供了足够多的开箱即用的小部件。为了浏览小部件集，Qt 提供了一组将小部件组合在一起的例子。如果您已经安装了 Qt Creator(开发 Qt 应用的集成开发环境)，您应该能够一键浏览示例。以下是 Qt Creator 中的外观:\n\n![](img/477bcb65-d63d-444f-b137-11aa76371cd2.png)\n\n配置和运行地址簿示例将为我们提供以下界面:\n\n![](img/d0e7b66e-749a-4d36-9520-e04b702e354a.png)\n\n单击添加按钮将打开一个对话框，这样我们就可以向地址簿添加一个新条目，如下所示:\n\n![](img/5bd497c5-53ec-4f80-8c12-1b1143f90e84.png)\n\n添加几个条目后，主窗口会在一个表中显示这些条目，如下所示:\n\n![](img/91bc9193-20c4-4f05-8660-b43300552cd6.png)\n\n前面的截图显示了在一个应用中组合在一起的各种小部件。以下是我们在 GUI 应用开发中经常使用的一些最常见的小部件:\n\n*   `QCheckBox`:表示带有文本标签的复选框。\n*   `QDateEdit`:表示可以用来输入日期的小部件。如果想输入时间，也可以使用`QDateTimeEdit`。\n*   `QLabel`:文字显示。也用于显示图像。\n*   `QLineEdit`:单行编辑框。\n\n*   `QProgressBar`:渲染垂直或水平进度条。\n*   `QTabWidget`:作为选项卡式小部件的堆栈。这是许多组织器小部件之一。其他一些组织者是`QButtonGroup`、`QGroupBox`和`QStackedWidget`。\n\n前面的列表不是最终的，但它给出了 Qt 能力的基本概念。我们在这里使用的地址簿示例使用了许多这样的小部件。`QTabWidget`代表一个组织小部件。它将几个小部件组合在一起。组织小部件的另一种方法是使用布局。在下一节中，我们将向您介绍如何将小部件组织在一起。\n\n# 使用布局组合小部件\n\nQt 为我们提供了一个灵活简单的平台，在这里我们可以使用布局形式的小部件排列机制。这有助于我们确保小部件内部的空间得到有效利用，并提供友好的用户体验。\n\n让我们看看布局管理类的基本用法。使用布局管理类的优势在于，当容器小部件改变其大小时，它们会自动调整小部件的大小和位置。Qt 布局类的另一个优点是，它们允许我们通过编写代码而不是使用用户界面编辑器来排列小部件。虽然 Qt Creator 提供了一种很好的手工组合小部件的方式(在屏幕上拖放小部件)，但是大多数程序员在实际编写代码来安排小部件的外观和感觉时会感觉更舒服。假设您也喜欢后一种方法，我们将介绍以下布局类:\n\n*   `QHBoxLayout`\n*   `QVBoxLayout`\n*   `QGridLayout`\n*   `QFormLayout`\n\n所有这些类都继承自`QLayout`，几何管理的基类。`QLayout`是继承自`QObject`的抽象基类。不继承`QWidget`因为与渲染无关；相反，它负责组织应该呈现在屏幕上的小部件。您可能不需要实现自己的布局管理器，但是如果需要，您应该从`QLayout`继承您的类，并为以下函数提供实现:\n\n*   `addItem()`\n*   `sizeHint()`\n*   `setGeometry()`\n\n*   `itemAt()`\n*   `takeAt()`\n*   `minimumSize()`\n\n这里列出的类足以组成几乎任何复杂的小部件。更重要的是，我们可以将一种布局放入另一种布局中，从而形成更灵活的小部件组织。使用`QHBoxLayout`，我们可以从左到右水平组织小部件，如下图截图所示:\n\n![](img/41c40a4b-c3e7-48f1-b380-1e50488376d7.png)\n\n为了实现上述组织，我们需要使用以下代码:\n\n```cpp\nQWidget *window = new QWidget;\nQPushButton *btn1 = new QPushButton(\"Leia\");\nQPushButton *btn2 = new QPushButton(\"Patrick\");\nQPushButton *btn3 = new QPushButton(\"Samo\");\nQPushButton *btn4 = new QPushButton(\"Amat\");\n\nQHBoxLayout *layout = new QHBoxLayout;\nlayout->addWidget(btn1);\nlayout->addWidget(btn2);\nlayout->addWidget(btn3);\nlayout->addWidget(btn4);\n\nwindow->setLayout(layout);\nwindow->show();\n```\n\n看看我们在小部件上调用`setLayout()`函数的那一行。可以为每个小部件分配一个布局。没有容器，布局本身没有多大用处，因此我们需要将其设置为一个小部件，作为有组织的小部件(在我们的例子中是按钮)的容器。`QHBoxLayout`继承自`QBoxLayout`，后者有我们之前列出的另一个后代—`QVBoxLayout`。类似于`QHBoxLayout`但是垂直组织小部件，如下图所示:\n\n![](img/a74e6782-1f0b-4911-a676-6caff1a6e7cf.png)\n\n在前面的代码中，我们唯一需要做的就是将`QHBoxLayout`替换为`QVBoxLayout`，如下所示:\n\n```cpp\nQVBoxLayout* layout = new QVBoxLayout;\n```\n\n`GridLayout`允许我们将小部件组织成一个网格，如下图截图所示:\n\n![](img/76ec7f99-4a11-45e6-938a-9693af4e5f5f.png)\n\n下面是相应的代码块:\n\n```cpp\nQGridLayout *layout = new QGridLayout;\nlayout->addWidget(btn1, 0, 0);\nlayout->addWidget(btn2, 0, 1);\nlayout->addWidget(btn3, 1, 0);\nlayout->addWidget(btn4, 1, 1);\n```\n\n最后，与`QGridLayout`类似，`QFormLayout`在设计输入表单时更有帮助，因为它以两列描述的方式布局小部件。\n\n正如我们之前提到的，我们可以将一个布局组合成另一个布局。为此，我们需要使用`addItem()`功能，如下所示:\n\n```cpp\nQVBoxLayout *vertical = new QVBoxLayout;\nvertical->addWidget(btn1);\nvertical->addWidget(btn2);\n\nQHBoxLayout *horizontal = new QHBoxLayout;\nhorizontal->addWidget(btn3);\nhorizontal->addWidget(btn4);\n\nvertical->addItem(horizontal);\n\n```\n\n布局管理器足够灵活，可以构建复杂的用户界面。\n\n# 摘要\n\n如果你是 Qt 新手，这一章将作为框架的一般性介绍。我们谈到了图形用户界面应用开发的基础，并将 Java 方法与 Qt 方法进行了比较。使用 Qt 的最大好处之一是它支持跨平台开发。虽然 Java 也是这样做的，但是 Qt 通过生成平台本地的可执行文件超越了这一点。这使得用 Qt 编写的应用比包含虚拟机的替代程序快得多。\n\n我们还讨论了 Qt 的信号和槽作为对象间通信的灵活机制。通过使用这个，您能够在您的图形用户界面应用中设计复杂的通信机制。虽然我们在本章中看到了一些相当简单的例子，但是您可以自由地尝试使用信号和插槽的各种方式。我们还熟悉了常见的 Qt 小部件和布局管理机制。您现在有了一个基本的理解，允许您设计甚至是最复杂的图形用户界面布局。这意味着通过应用本章中介绍的技术和小部件，您可以自由地实现复杂的 Qt 应用。在下一章中，我们将讨论一个时下流行的话题——人工智能和机器学习。\n\n# 问题\n\n1.  为什么 Qt 不需要虚拟机？\n2.  `QApplication::exec()`功能是做什么的？\n3.  您将如何更改顶级小部件的标题？\n4.  给定`m`模型，您将如何访问第 2 行和第 3 列的项目？\n5.  给定`wgt`小部件，你会如何将其宽度改为 400，高度改为 450？\n6.  从`QLayout`继承创建自己的布局管理器类应该实现哪些功能？\n7.  如何将信号连接到插槽？\n\n# 进一步阅读\n\n*   *Qt5 C++ GUI 编程食谱*作者:李志英:[https://www . packtpub . com/application-development/Qt5-C-GUI-编程-食谱-第二版](https://www.packtpub.com/application-development/qt5-c-gui-programming-cookbook-second-edition)\n*   *掌握 Qt5* 作者:纪尧姆·耶戈，罗宾·佩娜:[https://www . packtpub . com/web-development/Mastering-Qt-5-第二版](https://www.packtpub.com/web-development/mastering-qt-5-second-edition)"
  },
  {
    "path": "docs/exp-cpp/15.md",
    "content": "# 十五、C++ 在机器学习任务中的应用\n\n**人工智能** ( **AI** )和**机器学习** ( **ML** )最近越来越流行。从简单的食品配送网站到复杂的工业机器人，人工智能已经被宣布为驱动软件和硬件的主要功能之一。虽然大多数时候，这些术语被用来让产品看起来更严肃，但一些公司正在深入研究并将人工智能纳入他们的系统。\n\n在我们进一步讨论之前，请考虑这一事实:从 C++ 程序员的角度来看，这一章是对 ML 的温和介绍。关于更全面的文献，请参考本章末尾的书单。在这一章中，我们将介绍人工智能和人工智能的概念。虽然最好有数学背景，但我们在本章中几乎不使用任何数学。如果你打算扩大自己的技能范围，潜心学习 ML，你必须先考虑学习数学。\n\n除了介绍概念之外，本章还提供了 ML 中的任务示例。我们将实现它们，并给你一个基本的想法，你应该如何研究和解决更复杂的任务。\n\n我们将在本章中讨论以下主题:\n\n*   人工智能和人工智能概论\n*   最大似然法的分类及应用\n*   设计用于计算的 C++ 类\n*   神经网络结构及其实现\n*   回归分析和聚类\n\n# 技术要求\n\n带有`-std=c++ 2a`选项的 g++ 编译器用于编译本章中的示例。你可以在[https://github.com/PacktPublishing/Expert-CPP](https://github.com/PacktPublishing/Expert-CPP)找到本章使用的源文件。\n\n# 人工智能导论\n\n人工智能最简单的定义是机器人像人类一样行动。这是机器展示的智慧。接下来是关于智力定义的讨论。我们如何为机器定义它，我们应该在什么水平上大声喊出我们正在与智能机器打交道？\n\n如果你不熟悉验证机器智能的不同测试，一种流行的方法是图灵测试。这个想法是让一个审讯者向两个人提问，其中一个是机器，另一个是人。如果审讯者不能明确区分这两者，机器应该被认为是智能的。\n\nThe Turing test is named after Alan Turing. The test was introduced in his paper *Computing Machinery and Intelligence* in 1950\\. He proposed using the imitation game to determine whether a machine thinks like a human.\n\n被审问的人在墙后，这样审问者就看不见他们了。审讯者然后向两个参与者提出几个问题。下图展示了询问器如何与人和机器通信，但却看不到它们:\n\n![](img/34dd7f9a-7753-4539-9a3a-526fa0f79ee3.png)\n\n当你开始涉足 AI 领域，智能的定义就越来越模糊。问题可以以任何形式向机器提问:文本、音频、视频等等。有许多东西可能永远不会在机器中出现，比如它们脸上的表情。有时候人们通过对方脸上的表情来了解对方的心情。你无法确定机器人是否会理解甚至能够模仿它脸上的情绪。没有人教会我们生气时要看起来很生气。没有人教会我们要有情绪。他们就在那里。很难说有一天，类似的东西是否会在机器上实现。\n\n当谈到人工智能时，大多数时候我们假设它是关于一个说话和行为类似于人类的机器人。但是当你作为一个程序员试着去解剖它的时候，你会遇到很多子领域，每一个都需要很多时间去理解。许多领域有许多任务正在进行中或处于早期研究阶段。以下是你在职业生涯中可能感兴趣的人工智能子领域:\n\n*   **计算机视觉**:通过分析物体的视觉表征，设计视觉物体识别和理解的算法。人类很容易在人群中发现一张熟悉的脸，但是为机器实现类似的功能可能需要很多时间来获得与人类相同的准确性。\n*   **自然语言处理** ( **NLP** ):机器对文本的语言分析。它在各个领域都有应用，比如机器翻译。想象一下，计算机完全理解人类书写的文本，这样我们就可以告诉它该做什么，而不是花几个月的时间学习一门编程语言。\n*   **知识推理** : 这似乎是机器智能行为的明显目标。知识推理关注的是让机器推理，并根据它们拥有的信息提供解决方案；例如，通过检查医疗状况来提供诊断。\n*   **ML** :机器在没有明确指令的情况下执行任务所使用的算法和统计模型的研究领域。ML 算法依赖模式和推理，而不是直接指令。也就是说，ML 允许机器独立完成工作，无需人工参与。\n\n让我们分别讨论前面的子字段，然后集中讨论 ML。\n\n# 计算机视觉\n\n计算机视觉是一个综合性的研究领域，有许多正在进行的研究项目。它关注几乎所有与视觉数据处理相关的事情。它在各个领域都有广泛的应用；例如，人脸识别软件处理来自遍布城市的各种摄像头的数据，以发现和确定犯罪嫌疑人，或者光学字符识别软件，从包含文本的图像中产生文本。结合一些**增强现实** ( **AR** )技术，该软件能够将图像中的文本翻译成用户熟悉的语言。\n\n这个领域的研究日益取得进展。结合人工智能系统，计算机视觉是让机器像我们一样感知世界的领域。然而，对我们来说，一个简单的任务在计算机视觉方面很难实现。例如，当我们在图像中看到一个物体时，我们很容易发现它的尺寸。例如，以下图像表示自行车的前视图:\n\n![](img/1e417264-3431-4087-a127-a74603e541bf.png)\n\n即使我们不提那是一辆自行车，人类也不难确定。对我们来说很明显，底部中心的黑色粗线是自行车的前轮。很难让计算机明白它是一个轮子。计算机所看到的只是像素的集合，其中一些像素具有相同的颜色:\n\n![](img/1f146977-eca9-4201-8f08-c4874284568e.png)\n\n除了了解自行车的轮子，还应该推断出这辆自行车一定有另一个在图像中不可见的轮子。同样，我们可能会猜测自行车的大致尺寸，而计算机从图像中确定它是一项综合任务。也就是说，在我们看来简单的事情可能会成为计算机视觉领域的一个真正挑战。\n\nWe suggest using the OpenCV library for computer vision tasks. It is a cross-platform library written in C and C++. OpenCV represents a set of functions aimed at real-time computer vision, including ,but not limited to, facial recognition, gesture recognition, motion understanding, motion tracking, and other features. \n\n计算机视觉中的典型任务包括对象识别、识别和检测。物体识别是从前面的图像中理解物体是车辆。识别是识别一个物体的单个实例，例如前面图像中的自行车车轮。物体检测任务可能包括从自行车图像中找到损坏的区域。所有这些任务与最大似然算法相结合，可能会构成一个全面的软件，以接近人类的方式理解其周围环境。\n\n# 自然语言处理\n\n另一个有趣的研究领域是自然语言处理。NLP 努力让计算机理解人类语言。更广义的方法是自动语音识别和自然语言理解；虚拟助手的一个关键特性。例如，今天，用你的手机聊天并让它在网上搜索某样东西已经不再神奇了。所有的过程都是由语音和文本分析中的复杂算法驱动的。下图显示了会话代理背后发生的过程的高级视图:\n\n![](img/2afbe1d3-4236-437b-8728-a7a864c33172.png)\n\n许多语言处理任务都与网络有关。处理用户输入以在网络上数百万个文档中进行搜索的搜索引擎是自然语言处理的顶级应用之一。在下一章中，我们将深入研究搜索引擎的设计和实现。搜索引擎设计的主要关注点之一是处理文本数据。搜索引擎不能只存储所有的网站，并响应用户的第一个匹配查询。自然语言处理中有许多复杂的任务。假设我们正在设计一个用文本文档输入的程序，我们应该在文档中输出句子。识别句子的开头和结尾是一项复杂的任务。下面的句子是一个简单的例子:\n\n```cpp\nI love studying C++. It's hard, but interesting. \n```\n\n程序将输出两句话:\n\n```cpp\nI love studying C++.\nIt's hard, but interesting.\n```\n\n就编码任务而言，我们只需搜索。(点)字符，并确保第一个单词以大写字母开头。如果其中一个句子有以下形式，程序会如何运行？\n\n```cpp\nI love studying C++!\n```\n\n由于在句子的结尾有一个感叹号，我们应该重新检查我们的程序，添加另一个识别句子结尾的规则。如果一个句子这样结束呢？\n\n```cpp\nIt's hard, but interesting...\n```\n\n一个接一个，越来越多的规则和定义被引入，以拥有一个功能齐全的句子提取器。在解决自然语言处理任务时，利用最大似然将我们推向更聪明的方向。\n\n另一个与语言相关的任务是机器翻译，它会自动将文档从一种语言翻译成另一种语言。此外，请注意，构建一个全面的自然语言处理系统将有利于其他研究领域，如知识推理。\n\n# 知识推理\n\n知识推理是让计算机以与人类相似的方式思考和推理。想象一下与机器对话，开始是这样的:\n\n```cpp\n[Human] Hello\n[Machine] Hello\n```\n\n我们可以对机器进行编程，以回答特定的问题或理解用户输入的复杂文本，但根据以前的经验让机器推理要困难得多。例如，以下推理是研究的目标之一:\n\n```cpp\n[Human] I was walking yesterday and it was raining.\n[Machine] Nice.\n[Human] I should dress warmer next time.\n[Machine] OK.\n[Human] I think I have a temperature.\n[Machine] Did you caught a cold yesterday?\n[Human] I guess so.\n```\n\n虽然发现感冒和下雨之间的联系似乎很容易，但程序需要花很大的努力才能推断出来。它一定把下雨和感冒联系在一起，把发烧和感冒联系在一起。它还应该记住以前的输入，以便智能地保持对话。\n\n前面提到的所有研究领域都是程序员可以深入研究的令人兴奋的领域。最后，就为每个特定应用设计算法和模型而言，ML 通常是所有其他领域的基础。\n\n# 机器语言(Machine Language)\n\nML 将我们带到了一个全新的水平，让机器像人类一样执行任务，甚至可能更好。与我们之前介绍的领域相比，ML 的目标是构建能够在没有特定指令的情况下完成事情的系统。在发明人工智能机器的旅程中，我们应该更仔细地观察人类的智能。孩子出生后，不会表现出聪明的行为，而是开始慢慢熟悉周围的世界。没有任何一个 1 个月大的孩子解微分方程或作曲的记录证据。就像孩子学习和发现世界一样，ML 关心的是建立直接执行任务的基础模型，而是能够学习如何去做。这就是将系统设置为执行预定义的指令和让它自己解决问题之间的根本区别。\n\n当一个孩子开始走路、拿东西、说话和提问时，他们就一步一步地获得了关于世界的知识。她或他拿起一本书，尝一尝它的味道，迟早会停止把书当成可食用的东西来咀嚼。几年过去了，孩子现在打开书的书页，在里面寻找图像和组成文字的小图形。又过了几年，孩子开始阅读它们。随着时间的推移，大脑变得越来越复杂，神经元之间的联系也越来越多。这个孩子变成了一个聪明的人。\n\n想象一个系统，里面有一些神奇的算法和模型。在给它喂了一堆数据后，它会越来越能理解，就像孩子通过处理视觉数据(通过他们的眼睛看)或气味或味道形式的输入数据来了解世界一样。后来，通过开发一种提问方式，孩子可以理解单词，并将这些单词与现实世界中的物体，甚至是无形的概念联系起来。ML 系统的作用方式几乎相同。他们处理输入数据，并产生一些符合我们预期结果的输出。下图说明了这个想法:\n\n![](img/e1574aeb-59e5-4890-a1cb-0c31e04315f3.png)\n\n现在让我们深入了解 ML。一如既往，理解新事物的最好方法是首先尝试实现它。\n\n# 理解 ML\n\nML 是一个很大的研究领域，有很多研究正在进行中，并且正在迅速扩展。要理解 ML，首先要理解学习的本质。思考和推理是使我们——人类——与众不同的关键概念。ML 的核心是使系统学习并使用知识来执行任务。你可能会想起你学习编程的第一步。我们确信这并不容易。你必须学习新的概念，建立抽象概念，让你的大脑明白在程序执行的情况下发生了什么。之后，你应该使用那些在初级读本中描述为关键字、指令、条件语句、函数、类等的小构件来构建复杂的系统。\n\n然而，ML 程序不同于我们通常创建的程序。看看下面的代码:\n\n```cpp\nint calculate()\n{\n  int a{14};\n  int b{27};\n  int c{a + b};\n  return c;\n}\n```\n\n简单的先例程序按照我们的指示去做。它包含几个简单的指令，导致变量`c`代表`a`和`b`的总和。我们可以修改该函数以接受用户输入，如下所示:\n\n```cpp\nint calculate(int a, int b)\n{\n  int c{a + b};\n  return c;\n}\n```\n\n前面的功能永远不会获得任何智能。我们调用`calculate()`函数多少次并不重要。我们提供什么数字作为输入也无关紧要。该函数表示一组指令。我们甚至可以说是硬编码指令的集合。也就是说，该函数永远不会修改自己的指令，以根据输入做出不同的行为。不过，我们可以引入一些逻辑；假设我们让它在每次收到负数时返回 0:\n\n```cpp\nint calculate(int a, int b)\n{\n  if (a < 0 && b < 0) {\n    return 0;\n  }\n  int c{a + b};\n  return c;\n}\n```\n\n条件语句引入了函数基于其输入做出的最简单的决策形式。我们可以添加越来越多的条件句，这样函数就会增长并有一个复杂的实现。然而，没有多少条件语句会使它变得更聪明，因为它不是代码自己想出来的东西。这就是我们在处理程序时面临的限制。他们不思考；他们按照我们的计划行动。我们是决定他们必须如何行为的人。他们总是服从。只要我们不引入 bug。\n\n现在，想象一个 ML 算法在运行。假设`calculate()`函数中有某种魔力，因此它根据输入返回一个值。假设它有以下形式:\n\n```cpp\nint calculate(int a, int b)\n{\n  // some magic\n  // return value \n}\n```\n\n现在，假设我们调用`calculate()`并传递`2`和`4`作为它的参数，希望它会计算它们的和并返回`6`。同样，想象一下我们能以某种方式告诉它结果是否是我们所期望的。过了一段时间，该函数的行为方式是，它理解如何使用这些输入值并返回它们的总和。下面我们正在构建的类代表了我们理解 ML 的第一步。\n\n# 设计一个可以学习的算法\n\n下面的类表示一个计算机器。它包含四个算术运算，希望我们提供它应该如何计算输入值的示例:\n\n```cpp\nstruct Example\n{\n  int input1;\n  int input 2;\n  int output;\n};\n\nclass CalculationMachine\n{\npublic:\n  using Examples = std::vector<Example>;\n  // pass calculation examples through the setExamples()\n void setExamples(const Examples& examples);\n\n  // the main function of interest\n  // returns the result of the calculation\n int calculate(int a, int b);\n\nprivate:\n  // this function pointer will point to \n  // one of the arithmetic functions below\n int (*fptr_)(int, int) = nullptr;\n\nprivate:\n  // set of arithmetic functions\n  static int sum(int, int);\n  static int subtract(int, int);\n  static int multiply(int, int);\n  static int divide(int, int);\n};\n```\n\n在使用`calculate()`函数之前，我们应该为`setExamples()`函数提供一个示例列表。以下是我们提供给`CalculationMachine`的示例:\n\n```cpp\n3 4 7\n2 2 4\n5 5 10\n4 5 9\n```\n\n每行的前两个数字代表输入参数；第三个数字是运算的结果。`setExamples()`函数是`CalculationMachine`如何学习使用正确的算术函数。同样的方式，我们可以从前面的例子中猜测发生了什么，同样的方式`CalculationMachine`试图找到最适合它的操作。它通过例子定义了调用`calculate()`时应该使用的功能。实现类似于以下内容:\n\n```cpp\nvoid CalculationMachine::setExamples(const Examples& examples)\n{\n  int sum_count{0};\n  int sub_count{0};\n  int mul_count{0};\n  int div_count{0};\n  for (const auto& example : Examples) {\n if (CalculationMachine.sum(example.input1, example.input2) == example.output) {\n ++ sum_count;\n }\n if (CalculationMachine.subtract(example.input1, example.input2) == example.output) {\n ++ sub_count;\n }\n    // the same for multiply() and divide()\n  }\n\n  // the function that has the maximum number of correct output results\n  // becomes the main function for called by calculate()\n  // fptr_ is assigned the winner arithmetic function\n}\n```\n\n从前面的示例中可以看出，该函数调用所有算术函数，并将它们的返回值与示例输出进行比较。每次结果正确时，它都会增加特定功能的正确答案数。最后，具有最大正确答案数的函数被分配给`calculate()`函数使用的`fptr_`，如下所示:\n\n```cpp\nint CalculationMachine::calculate(int a, int b)\n{\n  // fptr_ points to the sum() function\n return fptr_(a, b);\n}\n```\n\n我们设计了一个简单的学习算法。`setExamples()`功能可能会被重新命名为`setDataSet()`或`trainWithExamples()`或类似的东西。使用`CalculationMachine`的例子的要点是，我们定义了一个使用它的模型和算法，我们可以称之为 ML。它从数据中学习。或者，更好的是，它从经验中学习。我们提供给`CalculationMachine`的例子向量中的每一条记录都可以被视为一种经验。我们说计算的性能随着经验而提高。也就是说，我们提供的例子越多，它就越有信心选择正确的功能来执行任务。任务是根据两个输入参数计算值。学习的过程本身不是任务。学习是完成任务的基础。任务通常被描述为系统应该如何处理一个例子，其中一个例子是特征的集合。虽然用 ML 术语来说，一个例子被表示为一个向量(数学)，其中每个条目都是另一个特征，但是向量数据结构的选择只是一个巧合。由于最基本的原则之一是系统的训练，最大似然算法可以分为有监督的和无监督的。让我们检查它们的差异，然后建立 ML 系统的各种应用。\n\n# 最大似然法的分类\n\n下图说明了 ML 的分类:\n\n![](img/57c4aa6a-343d-43e9-8060-f24d62dee7b3.png)\n\nML 算法的分类取决于它们在学习过程中的经验类型。我们通常将示例集合称为*数据集*。一些书也使用术语*数据点*。数据集基本上是表示对目标系统有用的任何东西的数据集合。它可能包括一段时间的天气测量，一些公司股票的价格列表，或者任何其他数据。虽然数据集可能是未经处理的或所谓的原始数据，但也有数据集对每个包含的体验具有附加信息。在`CalculationMachine`示例中，我们使用了原始数据集，尽管我们已经对系统进行了编程，以识别前两个值是操作的操作数，第三个值是其结果。如前所述，我们将最大似然算法分为有监督的和无监督的。\n\n监督学习算法从标记数据集学习；也就是说，每条记录都包含描述数据的附加信息。`CalulcationMachine`是监督学习算法的一个例子。监督式学习也被称为由教练进行的**培训。教师使用数据集教授系统。**\n\n监督学习算法将能够在从提供的经验中学习后标记新的未知数据。下图对此进行了最佳描述:\n\n![](img/3c11cb39-8e0b-48ed-af2b-e498f0345c35.png)\n\n监督学习算法应用的一个很好的例子是电子邮件应用中的垃圾邮件过滤器。用户将电子邮件标记为垃圾邮件，然后系统会尝试在新收到的电子邮件中查找模式，以检测潜在的垃圾邮件。\n\n带有`CalculationMachine`的例子是监督学习的另一种情况。我们向它提供了以下数据集:\n\n```cpp\n3 4 7\n2 2 4\n5 5 10\n4 5 9\n```\n\n我们编程`CalculationMachine`读取前两个数字作为输入参数，第三个数字作为应用于输入的函数产生的输出。通过这种方式，我们提供了系统最终应该得到什么的必要信息。\n\n无监督学习算法甚至更复杂——它们处理包含一堆特征的数据集，然后试图找到特征的有用属性。无监督的学习算法大多独自定义数据集中的内容。在智能方面，无监督学习方法比有监督学习算法更符合智能生物的描述。相比之下，监督学习算法试图预测哪些输入值映射到输出值，而非监督算法执行几个操作来发现数据集中的模式。遵循上图中的相同关联，下图描述了无监督学习算法:\n\n![](img/3957f1b0-47f6-4f5f-9fd7-d789c436e981.png)\n\n无监督学习算法的应用示例是推荐系统。我们将在下一章讨论一个，我们设计一个网络搜索引擎。推荐系统分析用户活动以推荐类似的数据，例如电影推荐。\n\n从上图可以看出，还有*强化学习*。这是一类从错误中学习的算法。在学习系统和它的经验之间有一个反馈回路，以便强化学习算法与环境交互。它在开始时可能会犯很多错误，在处理反馈后，会自我纠正以改进算法。学习过程成为任务执行的一部分。假设`CalculationMachine`只接收输入的数字，不接收计算的结果。对于每一次体验，它将通过应用其中一个算术运算产生一个结果，然后接收一个反馈。假设它减去数字，然后根据反馈修改自己来计算总和。\n\n# 最大似然法的应用\n\n了解 ML 的分类有助于更好地将其应用于各种任务。ML 可以解决的任务范围很广。我们已经提到*分类*是用 ML 算法解决的任务之一。基本上，分类是对输入进行过滤和排序以指定输入所属类别的过程。用 ML 求解分类通常意味着它产生一个将输入映射到特定输出的函数。输出类的概率分布也是一种分类任务。分类任务的最佳示例之一是对象识别。输入是一组像素值(换句话说，一幅图像)，输出是识别图像中对象的值。想象一下，一个机器人可以识别不同种类的工具，并根据命令将它们交付给工人。；也就是说，一个在车库工作的机械师有一个辅助机器人，它能够识别螺丝刀，并根据命令将其带上。\n\n更具挑战性的是缺少输入的分类。在前面的例子中，这类似于让机器人带些东西来拧紧螺栓。当某些输入丢失时，学习算法必须使用多个函数才能获得成功的结果。例如，辅助机器人可能会先带钳子，然后拿出螺丝刀作为正确的解决方案。\n\n与分类类似的是*回归*，要求系统在给定一些输入的情况下预测一个数值。区别在于输出的格式。回归任务的一个例子是股票未来价格的预测。ML 的这些和其他应用正使它作为一个研究领域迅速发展。学习算法不仅仅是一个条件语句列表，就像它们最初可能感觉到的那样。它们基于模仿人脑神经元及其连接的更全面的结构。这就引出了下一节，对**人工神经网络** ( **神经网络**)的研究。\n\n# 神经网络\n\n神经网络被设计用来识别模式。它们是模仿人脑的；更具体地说，我们谈论大脑的神经元和它们的人工对应物——人工神经元。下图显示了人脑中的一个神经元:\n\n![](img/2ae9ed94-046b-41d3-b973-d49240e60d96.png)\n\n一个神经元通过*突触*与其他神经元交流。神经元的基本功能是处理一部分数据，并根据这些数据产生信号。用编程术语来说，神经元接受一组输入并产生一个输出。\n\n这就是为什么下图清楚地说明了为什么人工神经元与人脑神经元结构相似:\n\n![](img/c5ab77e8-d399-4ea8-8266-87af81434f8b.png)\n\n人工神经网络是自然神经网络的简化模型。它代表一组相互连接的节点，每个节点代表一个神经元之后的一个模型。每个节点连接都可以传递类似于生物大脑神经元中突触的信号。神经网络是一组有助于聚类和分类的算法。从上图可以看出，神经网络由三层组成:\n\n*   输入层\n*   隐蔽层\n*   输出层\n\n输入和输出层不言自明；初始输入是外部数据，例如图像、音频或文本文件。输出是任务的完成，例如文本内容的分类或图像中识别的对象。隐藏层是让网络产生合理结果的东西。输入到输出的转换通过隐藏层，隐藏层进行产生输出所需的大量分析、处理和修改。\n\n考虑前面的图表；它表明一个神经元可以有多个输入和输出连接。通常，每个连接都有一个权重来指定连接的重要性。上图中的分层告诉我们，每一层的神经元都与前一层和后一层的神经元相连。您应该注意到在输入和输出层之间可能有几个隐藏层。虽然输入和输出层的主要目的是读取外部数据并返回计算(或推导)的输出，但隐藏层的目的是通过学习进行适应。学习还包括调整连接和权重，以提高输出精度。这是 ML 发挥作用的部分。因此，如果我们创建一个复杂的神经网络，其中有几个隐藏的层准备学习和改进，我们就得到一个人工智能系统。例如，让我们检查聚类问题，然后继续进行回归分析。\n\n# 使聚集\n\n聚类是关于将一组对象分组，以将它们分布在相似的对象组中。也被称为**聚类分析**，它是一组旨在将相似对象分组在一起，产生聚类的技术和算法。最简单的说明性介绍是将一组彩色对象分组为由相同颜色的对象组成的不同组，如下所示:\n\n![](img/3ef57b1e-05f2-4d19-a942-b7d6b85c8d95.png)\n\n虽然我们在本章中讨论的是人工智能任务，但我们建议您首先尝试用您目前拥有的知识库来解决问题。也就是说，让我们考虑如何根据相似性对对象进行分类。首先，我们应该对物体的外观有一个基本的概念。在前面的示例中，对象`shape`、`color`、尺寸(`width`和`height`代表 2D 对象)等等。无需深入探讨，基本对象表示可能如下所示:\n\n```cpp\nstruct Object\n{\n  int color;\n  int shape;\n  int width;\n  int height;\n};\n```\n\n让我们考虑这样一个事实:颜色和形状的值在某个预定义值的范围内。我们可以使用枚举来提高可读性。聚类分析包括分析对象，以某种方式对它们进行分类。首先想到的是有一个接受对象列表的函数。让我们试着定义一个:\n\n```cpp\nusing objects_list = std::vector<Object>;\nusing categorized_table = std::unordered_map<int, objects_list>;\ncategorized_table clusterize(const objects_list& objects)\n{\n  // categorization logic \n}\n```\n\n考虑一下实施细节。我们需要定义聚类点。可能是颜色，也可能是形状的类型。具有挑战性的是，它可能是未知的。也就是说，为了以防万一，我们将每个属性的对象分类如下:\n\n```cpp\ncategorized_table clusterize(const objects_list& objects)\n{\n  categorized_table result;\n  for (const auto& obj : objects) {\n    result[obj.color].push_back(obj);\n    result[obj.shape].push_back(obj);\n  }\n  return result;\n}\n```\n\n具有相似颜色或形状的对象在哈希表中分组在一起。虽然前面的代码相当简单，但它承载了通过某种相似性标准对对象进行分组的基本思想。我们在前面的例子中所做的更可能被描述为硬聚类。一个对象要么属于集群，要么不属于集群。相反，软聚类(也称为**模糊聚类**)在一定程度上描述了一个对象属于一个聚类。\n\n例如，形状属性的对象相似性可以由应用于对象的函数的结果来定义。也就是说，函数定义了对象 A 和对象 B 是否具有相似的形状，如果，假设，对象 A 的形状是正方形，对象 B 的形状是菱形。这意味着我们应该更新前面例子中的逻辑，将对象与几个值进行比较，并将它们的形状定义为一个组。通过进一步发展这一思想，我们迟早会得出不同的聚类策略和算法，比如 K-means 聚类。\n\n# 回归分析\n\n回归分析关注的是找出一个值与另一个值的偏差。理解回归分析最简单的方法是通过数学中函数的图形。您可能还记得函数 f(x) = y 的图:\n\n![](img/378390fd-c0b2-4a73-86ea-cc2108a8afa0.png)\n\n对于`x`的每一个值，该函数都会为`y`产生一个固定值。回归分析有点类似于前面的图表，因为它涉及到寻找变量之间的关系。更具体地说，它估计一个因变量和几个自变量之间的关系。因变量也被称为**结果**，而自变量也被称为**特征**。特征的数量可能是一个。\n\n回归分析最常见的形式是线性回归。它看起来类似于前面的图表。下面是一个例子，代表了花费在测试程序上的时间和在发布版本中发现的错误数量之间的关系:\n\n![](img/85954804-fb7b-4621-8295-dd72845621b6.png)\n\n回归有两种类型:负回归是上图所示的回归，因为自变量减少，因变量增加。另一方面，正回归的自变量数值越来越大。\n\nML 中的回归分析被用作一种预测方法。你可以开发一个程序，根据因变量的值来预测结果。到目前为止，你已经猜到了，ML 是一个很大的领域，有着广泛的主题。虽然程序员倾向于尽可能少地使用数学，但是 ML 使它变得不可能。你仍然需要掌握一些数学科目来充分利用 ML。回归分析非常依赖数学统计。\n\n# C++ 和 ML\n\nML 更多的是数学而不是编程，这已经不是什么秘密了。计算机科学起源于数学，在早期，计算机科学家首先是数学家。你可能熟悉几位杰出的科学家，包括艾伦·图灵、约翰·冯·诺依曼、克劳德·香农、诺伯特·维纳、尼古拉斯·沃斯、唐纳德·克努特和许多其他人。他们都是对技术有着特殊热爱的数学家。在其发展过程中，计算机编程成为对新来者更友好的领域。在过去的二三十年里，一个计算机程序员不再被迫在开发有用的程序之前学习数学。语言演变成越来越多的高级工具，几乎每个人都可以编码。\n\n有很多框架让程序员的工作变得更容易。现在掌握某种框架或高级编程语言并创建一个新程序需要几周的时间。然而，节目往往会重复播放。现在构建一些东西并不难，因为有很多模式和最佳实践可以帮助我们。数学的作用被推了回来，越来越多的人成为程序员，甚至丝毫不需要使用数学。这实际上不是问题；这更像是技术进化的自然流程。归根结底，技术的目的是让人类生活得更舒适。工程师也是如此。虽然早在 20 世纪 60 年代，美国宇航局的工程师就使用计算机进行计算，但他们并不是我们今天所知道的计算机。那些人是真正的人，他们有一个特殊的专业，叫做“成为一台计算机”，尽管成为一台计算机意味着在数学和解方程方面比其他人快得多。\n\n现在我们是计算机科学新时代的一部分，数学又回来了。ML 工程师现在使用数学，就像数学家在 20 世纪 70 年代或 80 年代使用编程语言一样。现在，仅仅知道一种编程语言或一个框架来设计一种新的算法或将 ML 合并到您的应用中是不够的。你还应该至少在数学的一些子领域表现出色，比如线性代数、统计学和概率论。\n\n几乎同样的逻辑也适用于 C++。现代语言提供了大量现成的功能，而 C++ 开发人员仍在努力设计带有手动内存管理的完美程序。如果你对 ML 领域做一些快速的研究，你会发现大部分的库或者例子都在使用 Python。起初，这可能被视为在 ML 任务中使用的默认语言。然而，ML 工程师开始触及进化的新门槛——性能。这个门槛并不新鲜；许多工具仍然在需要性能的地方使用 C++ 语言。游戏开发、操作系统、关键任务系统和许多其他基础领域都在使用 C++(和 C)作为事实上的*标准。现在是 C++ 征服一个新领域的时候了。我们给读者的最好建议是学习 ML 和 C++ 两者，因为对于 ML 工程师来说，合并 C++ 以获得最佳性能正慢慢变得至关重要。*\n\n *# 摘要\n\n我们已经介绍了 ML 及其类别和应用。它是一个快速发展的研究领域，在建筑智能系统中有着大量的应用。我们将最大似然算法分为有监督的、无监督的和强化学习算法。每一个类别都有它们在解决诸如分类、聚类、回归和机器翻译等任务中的应用。\n\n我们已经实现了一个简单的学习算法，该算法基于作为输入提供的经验来定义计算函数。我们称之为数据集，用来训练系统。使用数据集(称为**经验**)进行训练是 ML 系统的关键属性之一。\n\n最后，介绍和讨论了人工神经网络在模式识别中的应用。ML 和神经网络在解决任务时是齐头并进的。这一章为你提供了这个领域的必要介绍以及几个任务的例子，这样你就可以花一些时间深入这个主题。这将有助于你对人工智能和人工智能有一个大致的了解，因为在现实世界的应用开发中，工程师越来越需要人工智能和人工智能。在下一章中，我们将学习如何实现一个基于对话的搜索引擎。\n\n# 问题\n\n1.  ML 是什么？\n2.  有监督和无监督学习算法有什么区别？\n3.  举一些 ML 应用的例子。\n4.  用一套不同的经验训练`CalculationMachine`类后，你会如何修改它来改变它的行为？\n5.  神经网络的目的是什么？\n\n# 进一步阅读\n\n*   *人工智能和机器学习基础知识*，网址为\n*   *机器学习基础知识*，网址为\n*   *算法交易的动手机器学习，*在[https://www . packtpub . com/大数据与商业智能/动手机器学习-算法交易](https://www.packtpub.com/big-data-and-business-intelligence/hands-machine-learning-algorithmic-trading)*"
  },
  {
    "path": "docs/exp-cpp/16.md",
    "content": "# 十六、实现基于对话的搜索引擎\n\n在这本书里我们已经走了这么远！我们已经学习了 C++ 应用开发的基础知识，并讨论了架构和设计全球通用的应用。我们还深入研究了数据结构和算法，它们是高效编程的核心。现在是时候利用所有这些技能来设计复杂的软件了，比如搜索引擎。\n\n随着互联网的普及，搜索引擎已经成为最受欢迎的产品。大多数用户从搜索引擎开始他们的网络之旅。各种网络搜索服务，如谷歌、百度、Yandex 等，接收的流量巨大，每天服务数万亿的请求。搜索引擎在不到一秒钟的时间内处理每个请求。尽管他们维护数以千计的服务器来处理负载，但他们高效处理的核心是数据结构和算法、数据架构策略和缓存。\n\n设计一个高效搜索系统的问题不仅仅出现在网络搜索引擎中。本地数据库、**客户关系管理** ( **客户关系管理**)系统、会计软件和其他需要强大的搜索功能。在本章中，我们将发现搜索引擎的基础知识，并讨论用于构建快速搜索引擎的算法和数据结构。您将了解网络搜索引擎通常如何工作，以及如何满足需要高处理能力的项目中使用的新数据结构。你也将建立信心走出去，建立自己的搜索引擎，将与现有的竞争。\n\n在本章中，我们将涵盖以下主题:\n\n*   理解搜索引擎的结构\n*   理解并设计一个倒排索引，用于将关键词映射到搜索引擎中的文档\n*   为搜索平台的用户设计和构建推荐引擎\n*   利用知识图设计基于对话的搜索引擎\n\n# 技术要求\n\n带有`-std=c++ 2a`选项的`g++ `编译器用于编译本章中的示例。您可以在[https://github.com/PacktPublishing/Expert-CPP](https://github.com/PacktPublishing/Expert-CPP)找到本章使用的源文件。\n\n# 理解搜索引擎的结构\n\n想象一下世界上数十亿个网页。在搜索引擎界面上输入一个单词或短语，不到一秒钟就能返回一长串结果。搜索引擎处理如此多网页的速度是不可思议的。它怎么这么快就找到正确的文档？为了回答这个问题，我们将做一个程序员能做的最明智的事情，设计一个我们自己的引擎。\n\n下图显示了搜索引擎背后的基本思想:\n\n![](img/e863777e-e6a1-428a-9543-34793e6ebfc4.png)\n\n**用户**使用搜索引擎的**用户界面**输入单词。**搜索引擎**扫描所有文档，过滤它们，按相关性排序，并以最快的速度响应用户。我们的主要兴趣在于网络搜索引擎的实现。寻找某样东西需要在数十亿个文档中进行搜索。\n\n让我们尝试设计一种方法来找到短语*你好，世界！*从数十亿个文档中(为了简洁起见，我们将网页称为文档)。扫描每个文档中的短语将花费大量时间。如果我们认为每个文档至少有 500 个单词，搜索一个特定的单词或单词组合将花费大量时间。事先扫描所有文件会更实际。这个扫描过程包括建立文档中每个单词出现的索引，并将信息存储在数据库中，该数据库也被称为**索引文档**。当用户输入短语时，搜索引擎将在其数据库中查找单词，并以满足查询的文档链接作为响应。\n\n在搜索文档之前，引擎验证用户的输入不会有什么坏处。用户在短语中出现错别字并不少见。除了错别字，如果引擎自动完成单词和短语，用户体验会好很多。例如，当用户键入*你好*时，引擎可能会建议搜索短语*你好，世界！*。一些搜索引擎跟踪用户，存储他们最近搜索的信息，他们用来发出请求的设备的细节，等等。比如用户搜索*如何重启电脑*如果搜索引擎知道用户的操作系统会得到更好的结果。如果是 Linux 发行版，搜索引擎会对搜索结果进行排序，这样描述基于 Linux 的计算机重启的文档会首先出现。\n\n我们还应该小心经常出现在网络上的新文档。后台工作可能会持续分析网络以找到新内容。我们称这个作业为**爬虫**，因为它抓取网页和索引文档。爬虫下载文档以便解析其内容并建立索引。已经编入索引的文档可能会被更新，甚至删除。因此，另一个后台工作应该负责定期更新现有文档。你可能会遇到术语**蜘蛛**来描述爬网解析文档的任务。\n\n以下更新的图表更详细地说明了搜索引擎的结构:\n\n![](img/80b2e7a9-acbb-4672-a83c-d3bce75bde7c.png)\n\n搜索有广泛的应用。想象一下最简单的搜索形式——在数组中找到一个单词:\n\n```cpp\nusing words = std::vector<std::string>;\nwords list = get_list_of_words(); // suppose the function is implemented\n\nauto find_in_words(const std::string& term)\n{\n  return std::find(list.begin(), list.end(), term);\n}\n```\n\n虽然前面的例子适用于最简单的搜索引擎，但真正的交易是设计一个可伸缩的搜索引擎。您不想通过搜索字符串数组来满足用户请求。相反，您应该努力实现一个可扩展的搜索引擎，能够搜索数百万个文档。这需要大量的思考和设计，因为一切都很重要，从数据结构的正确选择到数据处理的高效算法。现在让我们更详细地讨论搜索引擎的组件。我们将结合前几章学到的所有技能来设计一个好的搜索引擎。\n\n# 提供方便的用户界面\n\n关键是要投入时间和资源来构建一个细粒度的用户界面，以提供惊人的用户体验。这里的关键是简单。界面越简单，使用越好。我们将以占据市场主导地位的谷歌为例。它在页面中心有一个简单的输入字段。用户在字段中键入他们的请求，引擎会建议一些短语:\n\n![](img/51e853fb-bc2e-4002-b368-fbd3e49d7057.png)\n\n我们不认为用户是懒惰的人，但是提供一个建议列表是有帮助的，因为有时用户不知道他们正在寻找的确切术语。让我们专注于建议列表的结构和实现。毕竟，我们感兴趣的是解决问题，而不是设计漂亮的用户界面。我们不会在本章讨论用户界面设计；专注于搜索引擎的后端会更好。然而，在继续前进之前，我们应该考虑一件事。我们在这里实现的搜索引擎是基于对话框的。用户查询引擎，并可以从几个答案中进行选择，以缩小结果列表。例如，假设用户查询*一台电脑*，搜索引擎询问*一台台式机还是笔记本电脑？*。这大大减少了搜索结果，并为用户提供了更好的结果。我们将使用决策树来实现这一点。但是，在此之前，让我们先了解一下搜索引擎的复杂性。\n\n首先是**输入令牌化**的问题。这涉及到文档解析和搜索短语分析。您可能会构建一个很棒的查询解析器，它会因为用户在查询中犯了一个错误而崩溃。让我们看一下处理模糊查询的几种方法。\n\n# 处理查询中的错别字\n\n用户打字时打错字并不少见。虽然这看起来很简单，但对搜索引擎设计师来说却是一个真正的问题。如果用户键入 helo worl 而不是 hello world，搜索数百万个文档可能会得到意想不到的错误结果。你可能熟悉搜索引擎提供的自动暗示。例如，当我们输入错误时，谷歌搜索界面是这样的:\n\n![](img/3885d527-3de0-4b25-946d-c20714d9eb89.png)\n\n注意截图底部的两行。其中一个说显示 hello world 的结果，这表明搜索引擎已经假设用户键入了带有错别字的查询，并且已经主动显示了正确查询的结果。然而，仍然有可能用户确实想要搜索他们键入的确切单词。因此，用户体验提供了下一行作为搜索，而不是 helo worl。\n\n因此，在构建搜索引擎时，我们有几个问题要解决，首先是用户请求。首先，我们需要为用户输入文本提供一个方便的界面。界面也应该与它们交互以提供更好的结果。如前所述，这包括根据部分键入的单词提供建议。让搜索引擎与用户交互是用户界面的另一个改进，我们将在本章中讨论。\n\n接下来是检查错别字或不完整的单词，这不是一项容易的任务。在字典中保留所有单词的列表并比较用户键入的单词可能需要一段时间。要解决这个问题，使用特定的数据结构和算法是必须的。例如，当检查用户查询中的错别字时，找到单词之间的 **Levenshtein 距离**可能会有所帮助。Levenshtein 距离是一个单词中应该添加、删除或替换的字符数，以使其等于另一个单词。例如，单词*世界*和*世界*之间的 Levenshtein 距离为 1，因为从*世界*中移除字母 *d* 或在*世界*中添加 *d* 会使这些单词相等。单词*编码*和*坐在*之间的距离是 4，因为以下四个编辑将一个单词变成了另一个单词:\n\n1.  编码-> cod **t** ing(中间插入 **t**\n2.  co**d**ting->co**t**ting(用 **t** 代替 **d**\n3.  c**o**t->c**I**t(用 **i** 代替 **o**\n4.  **c** 拟合->T2 s 拟合(用 **s** 代替 **c**\n\n现在，想象一下，如果我们将每个用户的输入与成千上万个单词进行比较以找到最接近的单词，处理需要多长时间。另一种方法是使用大的(数据结构)特里(数据结构)预先发现可能的错别字。trie 是有序的搜索树，其中键是字符串。请看下面代表 trie 的图表:\n\n![](img/296999a5-2bd6-4b40-bc87-9fdf30532cc2.png)\n\n每个路径代表一个有效的单词。例如，a 节点指向 n 和 r 节点。注意 n 后面的#号，它告诉我们到这个节点的路径代表一个字，一个。但是，它继续指向 d，然后是另一个#，这意味着到此节点的路径代表另一个单词，以及。同样的逻辑也适用于 trie 的其余部分。例如，想象一下单词*世界*的 trie 部分:\n\n![](img/d174ec96-3f73-441a-8f25-6ebd22d5038a.png)\n\n当发动机遇到*或*时，它会经历前面的三个阶段。w 很好，o 也一样，直到单词 l 中倒数第二个字符之前，其他一切都很好。在上图中，l 之后没有终端节点，只有 d。这意味着我们确信不存在 *worl 这样的单词；*所以可能是*世界*。为了提供好的建议和检查错别字，我们应该有一个完整的用户语言词典。当你计划支持多种语言时，这就变得更加困难了。然而，虽然收集和存储字典可以说是一项容易的任务，但更难的任务是收集网络上的所有文档并相应地存储它们以执行快速搜索。收集和解析网站以构建搜索引擎数据库(如前所述)的搜索引擎工具、程序或模块称为爬虫。在更深入地了解我们存储这些网站页面的方式之前，让我们先快速了解一下爬虫的功能。\n\n# 抓取网站\n\n每次用户键入查询时都要搜索数百万个文档是不现实的。想象一下，一个搜索引擎解析网站，在用户点击系统用户界面上的搜索按钮后搜索用户查询。这需要很长时间才能完成。搜索引擎对网站的每个请求都需要一些时间。即使不到一毫秒(0.001 秒)，在用户等待查询完成时，分析和解析所有网站也需要很长时间。为了使事情更清楚，让我们假设访问和搜索一个网站大约需要 0.5 毫秒(即使在那时，这也是不合理的快)。这意味着搜索 100 万个网站大约需要 8 分钟。现在想象你打开谷歌搜索并进行查询——你会等 8 分钟吗？\n\n正确的方法是将所有信息存储在搜索引擎可以有效访问的数据库中。爬虫下载网站页面并将其存储为临时文档，直到进行解析和索引。一个复杂的爬虫程序也可以解析文档，使它们的格式对索引器来说更方便。这里重要的一点是，下载网页不是一次就能完成的动作。网页的内容可能会更新。此外，在此期间可能会出现新页面。因此，搜索引擎必须保持其数据库的最新状态。为此，它安排爬虫定期下载页面。智能爬虫可能会在将内容传递给索引器之前比较内容的差异。\n\n通常，爬虫作为多线程应用工作。开发人员应该注意让爬行尽可能快，因为保持数十亿个文档的最新不是一件容易的事情。正如我们已经提到的，搜索引擎不直接搜索文档。它在所谓的索引文件中执行搜索。尽管爬行是一项有趣的编码任务，但在本章中，我们将主要关注索引。下一节介绍搜索引擎中的索引。\n\n# 为文件编制索引\n\n搜索引擎的关键功能是索引。下图显示了如何处理爬虫下载的文档来构建索引文件:\n\n![](img/17ba8dc9-5ed9-428a-8b09-0db88bbcb4d4.png)\n\n该索引在上图中显示为**倒排索引**。如您所见，用户查询被定向到倒排索引。虽然我们在本章中交替使用术语**指数**和**倒排指数**，但是**倒排指数**是更准确的名称。首先，让我们看看搜索引擎的索引是什么。编制文档索引的全部原因是为了提供快速搜索功能。这个想法很简单:每次爬虫下载文档时，搜索引擎都会处理其内容，将其分成引用该文档的单词。这个过程叫做**令牌化**。假设我们有一个从维基百科下载的包含以下文本的文档(为简洁起见，我们仅举一个段落的一部分作为例子):\n\n```cpp\nIn 1979, Bjarne Stroustrup, a Danish computer scientist, began work on \"C with Classes\", the predecessor to C++. The motivation for creating a new language originated from Stroustrup's experience in programming for his PhD thesis. Stroustrup found that Simula had features that were very helpful for large software development...\n```\n\n搜索引擎将前面的文档分成单独的单词，如下所示(为简洁起见，此处仅显示前几个单词):\n\n```cpp\nIn\n1979\nBjarne\nStroustrup\na\nDanish\ncomputer\nscientist\nbegan\nwork\n...\n```\n\n将文档分成单词后，引擎会为文档中的每个单词分配一个**标识符** ( **ID** )。假设前面文档的标识为 1，下表显示单词指的是(出现在)标识为 1 的文档:\n\n| 在…里 | one |\n| One thousand nine hundred and seventy-nine | one |\n| Bjarne - 维基百科，自由的百科全书 | one |\n| 斯特鲁普 | one |\n| a | one |\n| 丹麦的 | one |\n| 计算机 | one |\n| 科学家 | one |\n| ... |  |\n\n可能有几个文档包含相同的单词，因此上一个表实际上可能看起来更像下面的表:\n\n| 在…里 | 1, 4, 14, 22 |\n| One thousand nine hundred and seventy-nine | 1, 99, 455 |\n| Bjarne - 维基百科，自由的百科全书 | 1, 202, 1314 |\n| 斯特鲁普 | 1, 1314 |\n| a | 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, ... |\n| 丹麦的 | 1, 99, 102, 103 |\n| 计算机 | 1, 4, 5, 6, 24, 38, ... |\n| 科学家 | 1, 38, 101, 3958, ... |\n\n下表表示倒排索引。它用爬虫下载的文档的 id 映射单词。现在，查找包含用户作为查询键入的单词的文档变得快得多。现在，当用户通过键入*计算机*查询引擎时，结果是基于从索引中检索到的 ID 生成的，即 1，4，5，6，24，38，...在前面的例子中。索引还有助于为更复杂的查询找到结果。例如，*计算机科学家*匹配以下文档:\n\n| 计算机 | **1** ，4，5，6，24， **38** ，... |\n| 科学家 | **1** 、 **38** 、101、3958、... |\n\n为了用包含这两个术语的文档来响应用户，我们应该找到引用文档的交集(参见上表中的粗体数字)，例如 1 和 38。\n\n请注意，用户查询在与索引匹配之前也会被标记化。标记化通常涉及单词规范化。如果不规范化，一个*计算机科学家*查询不会给出任何结果(注意查询中的大写字母)。让我们多了解一些这方面的知识。\n\n# 标记文档\n\n您可能还记得第 1 章、*构建 C++ 应用*中的标记化概念，我们在其中讨论了编译器如何通过将源文件标记化为称为标记的更小、不可分割的单元来解析源文件。搜索引擎以类似的方式解析和标记文档。\n\n我们不会对此进行过多的详细描述，但是您应该考虑到文档的处理方式意味着标记(在搜索引擎上下文中具有含义的不可分割的术语)被规范化。例如，我们看到的所有单词都是小写的。因此，索引表应该如下所示:\n\n| 在 | 1, 4, 14, 22 |\n| One thousand nine hundred and seventy-nine | 1, 99, 455 |\n| bjarne - 维基百科 | 1, 202, 1314 |\n| 斯特鲁普 | 1, 1314 |\n| a | 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, ... |\n| 丹麦的 | 1, 99, 102, 103 |\n| 计算机 | 1, 4, 5, 6, 24, 38, ... |\n| 科学家 | 1, 38, 101, 3958, ... |\n\n作为 C++ 程序员，你可能会对看到小写的 bjarne 或 stroustrup 感到不舒服。然而，当我们将用户输入与反向索引键匹配时，我们应该考虑用户输入可能没有我们期望的形式。因此，我们需要对用户输入应用相同的规则，以便它与倒排索引相匹配。\n\n接下来，注意一个。毫不夸张地说，这是每个文档中都出现的一个词。其他的例子还有中的*、*中的*、*中的*等等。我们称之为**停止词**；它们在实际处理之前被过滤掉。通常，搜索引擎会忽略它们，因此倒排索引会更新为以下形式:*\n\n| One thousand nine hundred and seventy-nine | 1, 99, 455 |\n| bjarne - 维基百科 | 1, 202, 1314 |\n| 斯特鲁普 | 1, 1314 |\n| 丹麦的 | 1, 99, 102, 103 |\n| 计算机 | 1, 4, 5, 6, 24, 38, ... |\n| 科学家 | 1, 38, 101, 3958, ... |\n\n您应该注意到规范化不仅仅是让单词小写。它还包括把单词变成它们的正常形式。\n\nNormalizing a word to its root form (or to its word stem) is also called **stemming**.\n\n看看我们在本节开头作为示例使用的文档中的以下句子:\n\n```cpp\nThe motivation for creating a new language originated from Stroustrup's experience in programming for his PhD thesis.\n```\n\ncreating、originated 和 Stroustrup 是规范化的，因此倒排索引将具有以下形式:\n\n| 动机 | one |\n| **创建** | one |\n| 新的 | one |\n| 语言 | one |\n| **起源** | one |\n| 斯特鲁普 | one |\n| 经验 | one |\n| 编程；编排 | one |\n| 博士 | one |\n| 论文 | one |\n\n另外，请注意，我们忽略了停止词，并且在上表中没有包括*。*\n\n *标记化是索引创建的第一步。除此之外，我们可以自由地以任何能使搜索更好的方式处理输入，如下一节所示。\n\n# 对结果进行排序\n\n相关性是搜索引擎最重要的特征之一。用与用户输入相匹配的文档进行响应是不够的。我们应该按照最相关的文档最先出现的方式对它们进行排序。\n\n一种策略是记录每个单词在文档中出现的次数。例如，描述一台计算机的文档可能包含单词 *computer* 的多次出现，如果用户搜索*一台计算机*，结果将首先显示包含最多*计算机*出现的文档。下面是一个示例索引表:\n\n| 计算机 | 1{18}, 4{13}, 899{3} |\n| 地图 | 4{9}, 1342{4}, 1343{2} |\n| 世界 | 12{1} |\n\n大括号中的值定义了文档中每个单词的出现次数。\n\n当向用户呈现搜索结果时，我们可以考虑许多因素。一些搜索引擎存储与用户相关的信息，以便以个性化的结果进行响应。甚至用户用来访问搜索引擎的程序(通常是网络浏览器)也可能改变搜索平台的结果。例如，用户在 Linux 操作系统上搜索*重新安装操作系统*时，由于浏览器向搜索引擎提供了操作系统类型和版本信息，因此得到的结果在列表顶部包含*重新安装 Ubuntu* 。然而，考虑到隐私问题，有些搜索引擎完全不使用个性化用户数据。\n\n文档的另一个属性是它的更新日期。新鲜的内容总是有更高的优先级。因此，当向用户返回文档列表时，我们也可以按照内容更新的顺序对它们进行重新排序。对文档相关排名的关注将我们带到下一节，我们将讨论推荐引擎。\n\n# 构建推荐引擎\n\n上一章我们在介绍**机器学习** ( **ML** )的同时，也介绍了**人工智能** ( **AI** )。推荐引擎可以被视为人工智能驱动的解决方案或条件语句的简单集合。构建一个接收用户数据并返回最能满足输入的选项的系统是一项复杂的任务。将 ML 合并到这样的任务中听起来很合理。\n\n但是，您应该考虑到这样一个事实，即推荐引擎可能包含一个规则列表，在将数据输出给最终用户之前，会根据这些规则对数据进行处理。推荐引擎可以在预期和意外的地方运行。例如，在亚马逊上浏览产品时，推荐引擎会根据我们当前正在查看的产品向我们推荐产品。电影数据库根据我们以前看过或评价过的电影推荐新电影。对许多人来说，这似乎出乎意料，但推荐引擎也运行在搜索引擎的后面。\n\n你可能对一些电商平台推荐产品的方式比较熟悉。大多数时候，建议窗格的标题类似于*购买了这个的顾客也购买了...*。回想一下我们在上一章中介绍的聚类分析。现在，如果我们试图理解这些建议是如何运作的，我们很可能会发现一些聚类算法。\n\n让我们简单地看一下，并尝试设计一些推荐机制。让我们以书店网站为例。约翰买了一本名为*掌握 Qt5* 的书，所以让我们把这些信息放在表格中如下:\n\n|  | 掌握 Qt5 |\n| 约翰 | 是 |\n\n接下来，约翰决定买一本 C++ 书，*掌握 C++ 编程*。莱娅买了一本名为《T2 设计模式》《T4》的书。卡尔买了三本书，分别叫做*学习 Python**掌握机器学习**用 Python 进行机器学习*。该表已更新，现在如下所示:\n\n|  | 掌握 Qt5 | 掌握 C++ 编程 | 设计模式 | 学习 Python | 掌握机器学习 | 用 Python 进行机器学习 |\n| 约翰 | 是 | 是 | 不 | 不 | 不 | 不 |\n| 发光酶免疫测定 | 不 | 不 | 是 | 不 | 不 | 不 |\n| 卡尔 | 不 | 不 | 不 | 是 | 是 | 是 |\n\n那么，现在让我们想象一下 Harut 访问该网站并购买前面列出的两本书:*学习 Python* 和*使用 Python 进行机器学习*。向他推荐*掌握 Qt5* 这本书合理吗？我们不这么认为。但是我们知道他买的书，我们也知道另外一个用户，卡尔，买了三本书，其中两本和 Harut 买的书一样。所以，向 Harut 推荐*掌握机器学习*可能是合理的，告诉他买了那另外两本书的顾客也买了这本。这是一个简单的例子，从高级角度说明了推荐引擎是如何工作的。\n\n# 使用知识图\n\n现在，让我们回到我们的搜索引擎。一位用户正在寻找一位杰出的计算机科学家——比如说，唐纳德·克努特。他们在搜索栏中输入姓名，然后从整个网络中获取结果，这些结果经过排序后可以提供最佳结果。让我们再次看看谷歌搜索。为了充分利用用户界面，谷歌向我们展示了一些关于搜索主题的简短信息。在这种情况下，它在结果网页的右侧显示了这位伟大科学家的几张照片和一些关于他的信息。以下是该部分的外观:\n\n![](img/703e4005-d07b-4044-8fdd-c249be7de3e0.png)\n\n这样，搜索引擎试图覆盖用户的基本需求，让他们更快地找到信息，甚至不必访问任何网站。在这种情况下，我们最感兴趣的是前面信息框下的意见箱。它的标题是“人们也在搜索”，以下是它的外观:\n\n![](img/97e5ef3e-1898-47e4-8368-92a7fcba26c5.png)\n\n这些是基于用户活动的推荐，比如说，艾伦·图灵，就在他们搜索唐纳德·克努特之后。这促使推荐引擎提出这样一个建议，如果有新的人在寻找唐纳德·克努特，他们可能也会对艾伦·图灵感兴趣。\n\n我们可以通过谷歌称之为**知识图**的东西来组织类似的建议机制。这是一个由节点组成的图，每个节点代表某个主题、人物、电影或任何其他可搜索的内容。图形数据结构是连接这些节点的节点和边的集合，如下图所示:\n\n![](img/45cb1241-4a3d-44db-9fde-8bb9a30d3c84.png)\n\n在知识图中，每个节点代表一个实体。所谓实体，我们指的是一座城市、一个人、一只宠物、一本书或几乎任何你能想象到的东西。现在，图中的边表示实体之间的连接。每个节点可以通过多个节点连接到另一个节点。例如，看看这两个节点:\n\n![](img/ca796132-cf5f-4ed1-b8f0-f703724ac5f5.png)\n\n这两个节点只包含文本。我们可能会猜测唐纳德·克努特是一个名字，《计算机编程的艺术》是某种艺术。构建知识图的本质是我们可以将每个节点与代表其类型的另一个节点相关联。下图扩展了上一个图表:\n\n![](img/5e6932f4-bec8-455f-8dd5-f15a92aa7946.png)\n\n看看我们添加的两个新节点。其中一个代表**人**，另一个代表**书**。更令人兴奋的是，我们将带有边的唐纳德·克努特节点连接到**人**节点，并将其标记为“是一种关系”。同样的，我们把**《计算机编程的艺术》**节点连接到书节点，所以我们可以说《计算机编程的艺术》是一本书。现在让我们把唐纳德·克努特和他写的书联系起来:\n\n![](img/e68dfd54-f84d-4ee7-89ef-5c187ee45498.png)\n\n所以，现在我们有了一个完整的关系，因为我们知道唐纳德·克努特是《计算机编程艺术》的作者，而这本书又代表了一本书。\n\n让我们再添加几个代表人的节点。下图显示了我们如何添加艾伦·图灵和彼得·韦兰节点:\n\n![](img/d433010e-be15-41f9-9669-d36aefa0590e.png)\n\n所以，艾伦·图灵和彼得·韦兰都是人。现在，如果这是搜索引擎知识库的一部分，那么它可以让我们很好地洞察用户的搜索意图。当我们为唐纳德·克努特打出结果时，我们知道这是关于一个人的。如果有必要，我们可以建议用户看一看我们在知识图中积累了知识的其他人。推荐搜索唐纳德·克努特的用户也看看艾伦·图灵和彼得·韦兰的页面，合理吗？好了，棘手的部分来了:虽然两个人都是人，但他们没有很强的联系。因此，我们需要一些额外的东西来定义两个不同的人之间的联系的相关性。看看图表中的以下新增内容:\n\n![](img/9d8c7081-485a-487e-9692-9f9e43c47eb0.png)\n\n现在很明显，唐纳德·克努特和艾伦·图灵有着相同的活动，表现为**计算机科学**节点，代表着**的研究领域**，而彼得·韦兰则是一个**虚构的角色。**所以，唯一让彼得·韦兰和唐纳德·克努特有关系的是，他们都是人。看看我们放在从人物节点到计算机科学节点的边上的数字。假设我们对从 **0** 到 **100** 的关系进行评分，后者表示关系最强。所以，我们给艾伦·图灵和唐纳德·克努特打了 99 分。我们应该省略《彼得·韦兰到计算机科学》的边缘，而不是放 **0** ，但我们这样做是为了显示对比。那些数字是重量。我们给边缘增加权重来强调连接性因素；也就是说，艾伦·图灵和唐纳德·克努特有着相同的东西，并且彼此有着强烈的关联。如果我们在知识图中添加**史蒂夫·乔布斯**作为新人，图会是这样的:\n\n![](img/39e8ff27-3843-4762-b607-d5af7f171c25.png)\n\n看看边缘的重量。史蒂夫·乔布斯在某种程度上与计算机科学有关，但他主要与**商人**和**影响者**节点有关。同样，我们现在可以看到，彼得·韦兰与史蒂夫·乔布斯的分享比与唐纳德·克努特的分享更多。现在，推荐引擎建议搜索唐纳德·克努特的用户也应该看看艾伦·图灵，这更能提供信息，因为他们都是人，都以相等或接近相等的权重连接到计算机科学。这是一个很好的例子，在搜索引擎中加入了这样的图表。我们接下来要做的是向您介绍如何使用类似的知识图来构建一个更智能的框架来提供相关的搜索结果。我们称之为基于对话的搜索。\n\n# 实现基于对话的搜索引擎\n\n最后，让我们着手设计我们的搜索引擎中能够为我们提供细粒度用户界面的部分。正如我们在本章开头提到的，基于对话的搜索引擎涉及到构建一个用户界面，向用户询问与他们的查询相关的问题。这种方法最适用于结果不明确的情况。例如，搜索唐纳德的用户可能会想到以下情况之一:\n\n*   伟大的计算机科学家唐纳德·克努特\n*   *唐老鸭*，卡通人物\n*   *唐纳德·邓恩*，虚构人物贾里德·邓恩的真名\n*   商人唐纳德·特朗普和第 45 任美国总统 T2\n\n前面的列表只是唐纳德搜索词潜在结果的一个小例子。现在，缺乏基于对话的方法的搜索引擎能做什么？它们提供了与用户输入最匹配的相关结果列表。例如，在我写这本书的时候，搜索唐纳德得到了一个网站列表，这些网站都与唐纳德·特朗普有关，尽管我心中有唐纳德·克努特。在这里，我们可以看到最佳匹配和用户最佳匹配之间的细线。\n\n搜索引擎收集大量数据用于个性化搜索结果。如果用户在网站开发领域工作，他们的大多数搜索请求都会以某种方式与该特定领域相关。这对于为用户提供更好的搜索结果非常有帮助。例如，拥有大量搜索历史的用户，主要由与网站开发相关的请求组成，在搜索 zepelin 时会获得更好、更集中的结果。理想的搜索引擎将提供链接到 Zeplin 应用的网站，用于构建网络用户界面，而对于其他用户，该引擎将提供名为齐柏林飞船的摇滚乐队的信息结果。\n\n设计基于对话的搜索引擎是为用户提供更好界面的下一步。现在，如果我们已经有了一个强大的知识库，构建起来就足够简单了。我们将使用上一节中描述的知识图的概念。让我们假设当用户键入一个搜索词时，我们从知识图中获取所有匹配的主题，并为用户提供一个潜在命中列表，如下图所示:\n\n![](img/2fcc30b2-7cd7-4d82-bd2b-7d3d5b733845.png)\n\n因此，用户现在可以更容易地选择主题，并节省回忆全名的时间。当用户键入查询时，来自知识图的信息可以(对于某些搜索引擎来说也是)合并到自动建议中。此外，我们将解决搜索引擎的主要组成部分。显然，这一章不能涵盖实现的每一个方面，但是我们将讨论的基本组件足以让您投入到自己的搜索引擎的设计和实现中。\n\n我们不会为搜索引擎的用户界面部分费心。我们最关心的是后端。当谈到应用的后端时，我们通常指的是用户不可见的部分。更具体地说，让我们看一下下图:\n\n![](img/1ef212ea-f319-4913-a62e-db2027a3059c.png)\n\n如您所见，大部分引擎位于后端。虽然用户界面可能感觉很简单，但它是整个搜索系统的重要组成部分。这就是用户开始旅程的地方，用户界面设计得越多，提供的体验越好，用户在搜索某样东西时的不适感就越少。我们将专注于后端；以下是我们将要讨论的几个主要模块:\n\n*   **查询解析器**:对用户查询进行分析，对单词进行规范化，并为查询中的每个术语收集信息，以便以后传递给查询处理器。\n*   **查询处理器**:使用索引和补充数据库检索与查询相关联的数据，并构建响应。\n*   **对话框生成器**:提供更多选项供用户搜索时选择。对话生成器是一个补充模块。发出请求的用户可以省略该对话框，或者使用它来进一步缩小搜索结果的范围。\n\n我们已经跳过了搜索引擎中常见的一些组件(例如爬虫)，取而代之的是，我们将专注于那些与基于对话的搜索引擎密切相关的组件。现在让我们从查询解析器开始。\n\n# 实现查询解析器\n\n查询解析器按照它的名字来做:T3 解析查询。作为查询解析器的基础任务，我们应该按空间划分单词。例如用户查询*泽普林最佳专辑*分为以下词条:`zeplin`、`best`、`album`。下面的类表示基本的查询解析器:\n\n```cpp\n// The Query and Token will be defined in the next snippet\nclass QueryParser\n{\npublic:\n  static Query parse(const std::string& query_string) {\n auto tokens = QueryParser::tokenize(query_string);\n    // construct the Query object and return\n    // see next snippet for details\n }\n\nprivate:\n  static std::vector<Token> tokenize(const std::string& raw_query) {\n    // return tokenized query string\n  }\n};\n```\n\n看看前面的`parse()`功能。这是班上唯一的公共功能。我们将添加更多从`parse()`函数调用的私有函数，以完全解析查询并获得作为`Query`对象的结果。`Query`表示包含查询信息的简单结构，如下所示:\n\n```cpp\nstruct Query\n{\n  std::string raw_query;\n  std::string normalized_query;\n  std::vector<Token> tokens;\n  std::string dialog_id; // we will use this in Dialog Generator\n};\n```\n\n`raw_query`是用户键入的查询的文本表示，而`normalized_query`是规范化后的同一个查询。比如用户打*好书，程序员应该读*。，`raw_query`是确切的文字，`normalized_query`是*程序员应该读的好书*。在下面的片段中，我们不使用`normalized_query`，但是当您完成实现时，您将需要它。我们还在`Token`向量中存储标记，其中`Token`是一个结构，如下所示:\n\n```cpp\nstruct Token\n{\n  using Word = std::string;\n  using Weight = int;\n  Word value;\n  std::unordered_map<Word, Weight> related;\n};\n```\n\n`related`属性表示一个单词列表，这些单词在语义上与令牌**相关**。如果两个词在概念上表达相似的意思，我们称它们为**语义相关**。比如 *best* 和 *good* 这两个词，或者*专辑*和*收藏*这两个词，可以认为是语义相关的。您可能已经猜到了哈希表值中权重的用途。我们用它来存储相似度的`Weight`。\n\nThe range for the **weight** is something that should be configured during the exploitation of the search engine. Let's suppose we chose the range to be between 0 to 99\\. The weight of the similarity of the words *best* and *good* could be expressed as a number near to 90, while the weight of the similarity of the words *album* and *collection* could deviate from 40 to 70\\. Choosing these numbers is tricky and they should be tuned in the course of the development and exploitation of the engine.\n\n最后，`Query`结构的`dialog_id`表示如果用户选择了生成器建议的路径，生成的对话框的标识。我们很快就会谈到这一点。现在让我们继续完成`parse()`功能。\n\n看看`QueryParser`类的以下新增内容:\n\n```cpp\nclass QueryParser\n{\npublic:\n  static Query parse(const std::string& query_string, \n                     const std::string& dialog_id = \"\")\n  {\n    Query qr;\n    qr.raw_query = query_string;\n    qr.dialog_id = dialog_id;\n    qr.tokens = QueryParser::tokenize(query_string);\n    QueryParser::retrieve_word_relations(qr.tokens);\n    return qr;\n  }\n\nprivate:\n  static std::vector<Token> tokenize(const std::string& raw_string) {\n    // 1\\. split raw_string by space\n    // 2\\. construct for each word a Token\n    // 3\\. return the list of tokens \n  }\n\n  static void retrieve_word_relations(std::vector<Token>& tokens) {\n    // for each token, request the Knowledge Base\n    // to retrieve relations and update tokens list\n  }\n};\n```\n\n虽然两个私有函数(`tokenize`和`retrieve_word_relations`)在前面的代码片段中没有实现，但基本思想是它们规范化并收集关于搜索查询的信息。在我们继续实现查询处理器之前，先看看前面的代码。\n\n# 实现查询处理器\n\n查询处理器执行搜索引擎的主要工作；也就是说，它从搜索索引中检索结果，并根据搜索查询用相关的文档列表进行响应。在本节中，我们还将介绍对话框的生成。\n\n正如您在上一节中看到的，查询解析器构建了一个包含标记和`dialog_id`的`Query`对象。我们将在查询处理器中使用这两者。\n\nIt is recommended to have a separate component for the dialog generator due to scalability concerns. For educational purposes, we will keep the implementation succinct, but you are free to redesign the dialog-based search engine and complete the implementation along with the crawler and other supplementary modules.\n\n`Query`对象中的标记用于向搜索索引发出请求，以便检索与每个单词相关联的文档集。以下是相应的`QueryProcessor`类的外观:\n\n```cpp\nstruct Document {\n  // consider this\n};\n\nclass QueryProcessor\n{\npublic:\n  using Documents = std::vector<Document>;\n  static Documents process_query(const Query& query) {\n if (!query.dialog_id.empty()) {\n // request the knowledge graph for new terms\n }\n // retrieve documents from the index\n // sort and return documents\n }\n};\n```\n\n考虑前面的代码片段作为实现的介绍。我们想表达`QueryProcessor`类的基本思想。它具有`process_query()`功能，根据查询参数中的标记从索引中检索文档。这里的关键角色是搜索索引。就快速查询而言，我们定义其结构的方式和存储文档的方式至关重要。同时，作为附加参数提供的对话框标识允许`process_query()`函数请求知识库(或知识图)检索与查询相关的更多相关标记。\n\n还必须考虑到`QueryProcessor`也负责生成对话框(也就是说，定义一组路径为用户提供可能的查询场景)。生成的对话框被发送给用户，当用户进行另一个查询时，使用的对话框通过我们已经看到的对话框标识与该查询相关联。\n\n虽然前面的实现大部分是介绍性的(因为代码的实际大小太大，无法放入章节中)，但它是您在设计和实现引擎时进一步前进的一个很好的基础。\n\n# 摘要\n\n从头开始构建搜索引擎是经验丰富的程序员的任务。我们在这本书里涉及了许多主题，并通过设计一个搜索引擎将它们结合在了这一章里。\n\n我们已经了解到，网络搜索引擎是由几个组件组成的复杂系统，例如爬虫、索引器和用户界面。爬虫负责定期检查网页以下载网页供搜索引擎索引。索引会产生一个大数据结构，称为倒排索引。倒排索引，或者仅仅是一个索引，是一种数据结构，它将单词与其所在的文档进行映射。\n\n接下来，我们定义了什么是推荐引擎，并试图为我们的搜索引擎设计一个简单的推荐引擎。推荐引擎与本章中讨论的搜索引擎的基于对话框的功能相连接。基于对话的搜索引擎旨在向用户提供有针对性的问题，以了解用户实际想要搜索的内容。\n\n我们从 C++ 的角度讨论了计算机科学中的各种主题，从而达到了本书的结尾。我们从 C++ 程序的细节开始，然后简单介绍了如何使用数据结构和算法高效地解决问题。了解一门编程语言并不足以在编程上取得成功。您需要解决需要数据结构、算法、多线程等密集技能的编码问题。此外，解决不同的编程范式可能会大大增强你对计算机科学的看法，并让你重新看待问题解决。在这本书里，我们已经谈到了几种编程范例，比如函数式编程。\n\n最后，正如你现在所知道的，软件开发不仅仅局限于编码。架构和设计项目是成功应用开发的关键步骤之一。第 [10](10.html) 、*设计全球通用的应用、*至 [16](16.html) 、*实现基于对话的搜索*等章节主要涉及设计真实世界应用的方法和策略。让这本书从 C++ 开发人员的角度成为你对编程世界的入门指南。通过开发更复杂的应用来发展你的技能，并与同事和那些刚刚开始职业生涯的人分享你的知识。学习新事物的最好方法之一是教它。\n\n# 问题\n\n1.  爬虫在搜索引擎中的作用是什么？\n2.  为什么我们称搜索索引为倒排索引？\n3.  索引前对单词进行标记的主要规则是什么？\n4.  推荐引擎的作用是什么？\n5.  什么是知识图？\n\n# 进一步阅读\n\n有关更多信息，请参考以下书籍:\n\n*信息检索简介*，*克里斯托弗·曼宁等*，[https://www . Amazon . com/Introduction-Information-Retrieval-Christopher-Manning/DP/0521865719/](https://www.amazon.com/Introduction-Information-Retrieval-Christopher-Manning/dp/0521865719/)*"
  },
  {
    "path": "docs/exp-cpp/17.md",
    "content": "# 十七、答案\n\n# 第一章\n\n1.  从源代码生成可执行文件的过程称为编译。编译 C++ 程序是导致机器代码生成的一系列复杂任务。通常，C++ 编译器解析和分析源代码，生成中间代码，优化它，最后在一个名为目标文件的文件中生成机器代码。另一方面，解释器不产生机器代码。相反，它逐行执行源代码中的指令。\n2.  首先是预处理，然后编译器通过解析代码、执行语法和语义分析来编译代码，之后生成中间代码。优化生成的中间代码后，编译器生成最终目标文件(包含机器代码)，然后可以将其与其他目标文件链接。\n3.  预处理器旨在处理源文件，使它们为编译做好准备。预处理器使用预处理器指令，如`#define`和`#include`。指令不代表程序语句，但它们是给预处理程序的命令，告诉它如何处理源文件的文本。编译器无法识别这些指令，因此每当您在代码中使用预处理器指令时，预处理器都会在代码的实际编译开始之前相应地解析它们。\n4.  编译器为每个编译单元输出一个目标文件。链接器的任务是将这些目标文件组合成一个目标文件。\n5.  库可以作为静态或动态库与可执行文件链接。当您将它们链接为静态库时，它们会成为最终可执行文件的一部分。操作系统还应该将动态链接库加载到内存中，以便为程序提供调用其函数的能力。\n\n# 第二章\n\n1.  通常，`main()`函数有两个参数，`argc`和`argv`，其中`argc`是程序的输入参数数量，`argv`构成这些输入参数。非常偶然的情况下，你可以看到一个被广泛支持但不规范的第三个论点，最常见的名字是`envp`。`envp`的类型是一个字符指针数组，它保存系统的环境变量。\n2.  `constexpr`说明符声明可以在编译时计算该函数的值。同样的定义也适用于变量。名称由`const`和表达式组成。\n3.  递归导致为函数调用分配额外的空间。与迭代解决方案相比，为函数和调用分配空间是昂贵的。\n4.  堆栈保存具有自动存储持续时间的对象；也就是说，程序员不关心内存中那些对象的构造和销毁。通常，堆栈用于函数参数和局部变量。另一方面，堆允许在程序执行期间分配新的内存。然而，正确地释放内存空间现在是程序员的责任。\n5.  指针的大小不取决于指针的类型，因为指针是一个代表内存中地址的值。地址的大小取决于系统。通常，它不是 32 位就是 64 位。因此，我们说指针的大小是 4 或 8 字节。\n6.  就项目位置而言，数组具有独特的结构。它们被连续地放置在内存中；第二项放在第一项之后，第三项放在第二项之后，依此类推。考虑到这个特性，以及数组由相同类型的元素组成这一事实，在任何位置访问一个项目都需要恒定的时间。\n7.  如果我们忘记了`case`语句中的`break`关键字，执行将传递到下一个`case`语句，而不检查其条件。\n8.  例如`operations['+'] = [](int a, int b) { return a + b; }`\n\n# 第三章\n\n1.  身份、状态和行为。\n2.  当移动对象而不是复制时，我们省略了临时变量的创建。\n3.  在 C++ 中，除了默认的访问修饰符之外，结构和类没有任何区别。这对于结构是公共的，对于类是私有的。\n4.  在聚合的情况下，包含其他类的一个或多个实例的类可以在没有聚合的情况下被实例化。另一方面，这篇作文表达了强烈的包容。\n5.  私有继承从派生类的客户端代码中隐藏继承的成员。受保护的继承也是如此，但允许链中的派生类访问这些成员。\n6.  通常，虚函数的引入导致用指向虚函数表的附加数据成员来扩充类。通常，这将为类对象增加 4 或 8 字节的空间(基于指针的大小)。\n7.  Singleton 设计模式允许构造类的单个实例。这在很多项目中很有帮助，在这些项目中，我们需要确保类的实例数量被限制在一个。例如，数据库连接类如果实现为 Singleton，效果最好。\n\n# 第四章\n\n1.  如果使用得当，宏是强大的工具。但是，以下方面限制了宏的使用。(1)不能调试宏；(2)宏观扩张会导致奇怪的副作用；(3)宏没有命名空间，所以如果您有一个宏与其他地方使用的名称冲突，您会在不想要的地方获得宏替换，这通常会导致奇怪的错误消息；(4)宏可能会影响你不知道的事情。详情请前往[https://stackoverflow.com/questions/14041453](https://stackoverflow.com/questions/14041453)。\n2.  类/函数模板是指一种用于生成模板类/函数的模板。它只是一个模板，而不是一个类/函数，因此编译器不会为它生成任何目标代码。模板类/函数是类/函数模板的一个实例。由于它是一个类/函数，相应的目标代码由编译器生成。\n\n3.  我们定义类/函数模板时，`template`关键字后有一个<>符号，其中必须给出一个或多个类型参数。< >中的类型参数称为模板参数表。当我们实例化一个类/函数模板时，所有的模板参数都必须用它们对应的模板参数来替换，这就是所谓的模板参数列表。\n\n隐式实例化按需发生。但是，当提供库文件(`.lib`)时，您不知道用户将来将使用什么类型的参数列表，因此，您需要显式实例化所有潜在的类型。\n\n4.  *多态性*是指某物以不同的形式存在。具体来说，在编程语言中，多态性意味着一些函数、操作或对象在不同的上下文中有几种不同的行为。在 C++ 中，多态有两种:动态多态和静态多态。动态多态性允许用户在运行时确定要执行的实际函数方法，而静态多态性意味着要调用的实际函数(或者一般来说，要运行的实际代码)在编译时是已知的。\n\n函数重载意味着用相同的名称定义函数，但是使用不同的参数集(不同的签名)。\n\n函数重写是子类重写父类中定义的虚拟方法的能力。\n\n5.  类型特征是一种用于收集类型信息的技术。在它的帮助下，我们可以做出更智能的决策来\n    在泛型编程中开发高质量的优化算法。类型特征可以通过部分或全部模板专门化来实现。\n6.  我们可以在`g()`中写一个错误语句，并构建代码。如果一个未使用的函数被实例化，编译器将报告错误，否则它将被成功构建。您可以在以下文件中找到示例代码:`ch4_5_class_template_implicit_inst_v2.h`和`ch4_5_class_template_implicit_inst_B_v2.cpp`，网址为[。/树/主/第 4 章。](https://github.com/PacktPublishing/Expert-CPP/tree/master/Chapter-4)\n7.  参见[中的`ch4_q7.cpp`。/树/主/第四章](https://github.com/PacktPublishing/Mastering-Cpp-Programming./tree/master/Chapter-4)。\n8.  这是一个实验室练习；不需要回答。\n\n# 第五章\n\n1.  计算机内存可以描述为一个单一的概念——动态随机存取存储器(动态随机存取存储器)或计算机包含的所有内存单元的组合，从寄存器和高速缓冲存储器开始，到硬盘结束。从程序员的角度来看，DRAM 最受关注，因为它保存着计算机中运行的程序的指令和数据。\n2.  虚拟内存是一种有效管理计算机物理内存的方法。通常，操作系统结合虚拟内存来处理程序的内存访问，并有效地将内存块分配给特定的程序。\n3.  在 C++ 中，我们使用`new`和`delete`运算符来分配和释放内存空间。\n4.  `delete`解除分配给单个对象的空间，而`delete[]`用于动态数组，并释放堆上数组的所有元素。\n5.  垃圾收集器是一种工具或一组工具和机制，用于在堆上自动释放资源。对于垃圾收集器，需要一个支持环境，例如虚拟机。C++ 直接编译成机器代码，无需支持环境即可运行。\n\n# 第六章\n\n1.  当向向量中插入新元素时，它被放置在向量中已经分配的空闲槽中。如果向量的大小和它的容量相等，这意味着向量对于新元素没有空闲槽。在这些(罕见的)情况下，向量会自动调整自身大小，这包括分配新的内存空间，并将现有元素复制到新的更大的空间。\n2.  当在链表的前面插入一个元素时，我们只创建新元素并更新列表指针，以有效地将新元素放入列表中。在向量的前面插入一个新元素需要所有的向量元素向右移动，为该元素释放一个槽。\n3.  参考 GitHub 中的章节源代码。\n4.  它看起来像一个链表。\n5.  选择排序搜索最大(或最小)元素，并用该最大(或最小)元素替换当前元素。插入排序将集合分成两个部分，遍历未排序的部分，并将其每个元素放在排序部分的适当槽中。\n6.  参考 GitHub 中的章节源代码。\n\n# 第七章\n\n1.  C++ 中的 ranges 库允许处理元素的范围，使用视图适配器来操作它们，这要高效得多，因为它们不会将整个集合存储为适配器结果。\n2.  如果一个函数不修改状态，那么它就是纯函数，并且对相同的输入产生相同的结果。\n3.  纯虚函数是没有实现的函数的特征。纯虚函数用于描述派生类的接口函数。函数式编程中的纯函数是那些不修改状态的函数。\n4.  折叠(或简化)是将一组值组合在一起以生成数量减少的结果的过程。\n5.  尾部递归允许编译器通过省略为每个递归调用分配新的内存空间来优化递归调用。\n\n# 第八章\n\n1.  如果两个操作的开始和结束时间在任意点交错，则两个操作同时运行。\n2.  并行性意味着任务同时运行，而并发性不会强制任务同时运行。\n3.  进程是程序的形象。它是加载到计算机内存中的程序指令和数据的组合。\n4.  线程是可由操作系统调度程序调度的进程范围内的一段代码，而进程是正在运行的程序的映像。\n5.  参考本章中的任何示例。\n6.  通过使用双重检查锁定。\n7.  请参考 GitHub 中章节的源代码。\n8.  C++ 20 引入了 coroutines，作为对经典异步函数的补充。Coroutines 将代码的后台执行移动到下一个级别。它们允许功能在必要时暂停和恢复。`co_await`是告诉代码等待异步执行代码的构造。这意味着函数可以在该点暂停，并在结果准备好时恢复执行。\n\n# 第九章\n\n1.  双重检查锁定是让 Singleton 模式在多线程环境中完美工作的一种方式。\n2.  这是一种确保在我们复制另一个堆栈的基础数据时不会被修改的方法。\n3.  原子操作是一个不可分割的操作，原子类型利用低级机制来确保指令的独立和原子执行。\n4.  `load()`和`store()`利用低级机制来确保写和读操作以原子方式完成。\n5.  除了`load()`、`store()`外，还有`exchange()`、`wait()`、`notify_one()`等操作。\n\n# 第十章\n\n1.  TDD 代表测试驱动开发，目的是在项目实际实现之前编写测试。这有助于更清楚地定义项目需求，并提前避免代码中的大多数错误。\n2.  交互图描绘了对象通信的确切过程。这允许开发人员在任何给定的时刻对实际的程序执行有一个高层次的了解。\n3.  在聚合的情况下，包含其他类的一个或多个实例的类可以在没有聚合的情况下被实例化。另一方面，作文表达了强烈的遏制。\n4.  简单来说，李斯科夫替换原理确保了任何以某种类型的对象为参数的函数，如果 K 扩展了 T，也会以 K 类型的对象为参数。\n5.  开-闭原则规定类应该开放扩展，封闭修改。在所述的例子中，`Animal`是开放扩展的，所以从`Animal`继承`monkey`类并不违背原则。\n6.  参考 GitHub 中的章节源代码。\n\n# 破产重组保护\n\n1.  重写私有虚函数允许通过保持公共接口不变来修改类的行为。\n2.  这是一种行为设计模式，其中一个对象封装了一个动作以及执行该动作所需的所有信息。\n3.  尽可能与其他对象共享数据。当我们有许多结构相似的对象时，跨对象共享重复的数据可以最大限度地减少内存的使用。\n4.  观察者向订阅者对象通知一个事件，而中介者在相互通信的对象之间扮演连接中枢的角色。\n5.  将游戏循环设计为无限循环是合理的，因为理论上，游戏可能永远不会结束，只有在玩家命令时才会结束。\n\n# 第十二章\n\n1.  物理、数据链路、网络、传输、会话、演示和应用。\n2.  端口号提供了一种区分在同一环境中运行的多个网络应用的方法。\n3.  套接字是抽象概念，为程序员提供了通过网络发送和接收数据的方法。\n4.  首先，我们需要用一个 IP 地址创建并绑定套接字。接下来，我们应该监听传入的连接，如果有，我们应该接受该连接以进一步处理数据通信。\n5.  TCP 是一种可靠的协议。它处理端点之间的强连接，还通过重新发送接收器未接收到的数据包来处理数据包丢失。另一方面，UDP 不可靠。几乎处理的每一个方面都落在程序员的肩上。UDP 的优势在于它的速度，因为它省略了握手、检查和数据包丢失处理。\n6.  宏定义会导致代码中难以发现的逻辑错误。用`const`表达式总比用宏好。\n7.  客户端应用必须具有唯一的标识符以及用于授权和/或认证它们的令牌(或密码)。\n\n# 第十三章\n\n1.  这是一个实验室练习；不需要回答。\n2.  以下输出来自 NVIDIA Jetson Nano 上的 Ubuntu 18.04:\n\n```cpp\nswu@swu-desktop:~/ch13$ g++ -c -Wall -Weffc++ -Wextra ch13_rca_compound.cpp\n ch13_rca_compound.cpp: In function ‘int main()’:\n ch13_rca_compound.cpp:11:17: warning: operation on ‘x’ may be undefined [-Wsequence-point]\n std::cout << f(++ x, x) << std::endl; //bad,f(4,4) or f(4,3)?\n ^~~\n\n```\n\n```cpp\nswu@swu-desktop:~/ch13$ g++ -c -Wall -Weffc++ -Wextra ch13_rca_mix_sign_unsigned.cpp\nnothing is detected \n```\n\n```cpp\nswu@swu-desktop:~/ch13$ g++ -c -Wall -Weffc++ -Wextra ch13_rca_order_of_evaluation.cpp\n ch13_rca_order_of_evaluation.cpp: In constructor ‘A::A(int)’:\n ch13_rca_order_of_evaluation.cpp:14:14: warning: ‘A::v3’ will be initialized after [-Wreorder]\n int v1, v2, v3;\n ^~\n ch13_rca_order_of_evaluation.cpp:14:6: warning: ‘int A::v1’ [-Wreorder]\n int v1, v2, v3;\n ^~\n ch13_rca_order_of_evaluation.cpp:7:2: warning: when initialized here [-Wreorder]\n A(int x) : v2(v1), v3(v2), v1(x) {\n ^\n ch13_rca_order_of_evaluation.cpp: In constructor ‘B::B(float)’:\n ch13_rca_order_of_evaluation.cpp:32:6: warning: ‘B::v2’ will be initialized after [-Wreorder]\n int v2;\n ^~\n ch13_rca_order_of_evaluation.cpp:31:6: warning: ‘int B::v1’ [-Wreorder]\n int v1; //good, here the declaration order is clear\n ^~\n ch13_rca_order_of_evaluation.cpp:25:2: warning: when initialized here [-Wreorder]\n B(float x) : v2(x), v1(v2) {};\n ^\n swu@swu-desktop:~/ch13$ g++ -c -Wall -Weffc++ -Wextra ch13_rca_uninit_variable.cpp\n ch13_rca_uninit_variable.cpp: In function ‘int main()’:\n ch13_rca_uninit_variable.cpp:7:2: warning: ‘x’ is used uninitialized in this function [-Wuninitialized]\n if (x) {\n ^~\n```\n\n3.  因为静态分析工具从它们的模型中预测错误，而动态分析工具通过程序的执行来检测错误。\n4.  请参考位于\n    [的示例代码`ch13_tdd_v3.h`、`ch13_tdd_v3.cpp`和`ch13_tdd_Boost_UTF3.cpp`。/树/主/第十三章](https://github.com/PacktPublishing/Mastering-Cpp-Programming./tree/master/Chapter-13)。\n\n# 第十四章\n\n1.  Qt 的编译模型允许省略虚拟机。它使用一个**元对象编译器** ( **MOC** )翻译成 C++，然后编译成特定平台的机器码。\n2.  `QApplication::exec()`是申请的起点。它启动 Qt 的事件循环。\n3.  通过调用`setWindowTitle()`。\n4.  `m->index (2, 3)`。\n5.  `wgt->resize (400, 450)`。\n6.  从`QLayout`继承时，应该提供`addItem()`、`sizeHint()`、`setGeometry()`、`itemAt()`、`takeAt()`和`minimumSize()`功能的实现。\n7.  通过使用`connect()`函数，该函数以源和目标对象以及信号和槽的名称作为参数。\n\n# 第十五章\n\n1.  **ML** 代表**机器学习**，是一个研究算法和统计模型的领域，计算机系统使用这些算法和统计模型来执行特定的任务，而不使用明确的指令，而是依靠模式和推理。\n2.  监督学习算法(也称为教师培训)从标记数据集学习；也就是说，每条记录都包含描述数据的附加信息。无监督学习算法甚至更复杂——它们处理包含一堆特征的数据集，然后试图找到特征的有用属性。\n3.  ML 应用包括机器翻译、自然语言处理、计算机视觉和电子邮件垃圾检测。\n4.  方法之一是为每个结果增加一个权重，如果减法运算的权重超过其他运算，它将成为主导运算。\n5.  神经网络的目的是识别模式。\n\n# 第十六章\n\n1.  爬虫下载网页并存储其内容，供搜索引擎索引。\n2.  我们称之为倒排索引，因为它将单词映射回它们在文档中的位置。\n3.  在索引之前，标记化将单词规范化。\n4.  推荐引擎验证并推荐适合特定请求的最佳结果。\n5.  知识图是其中节点是主题(知识)并且边是主题之间的连接的图。"
  },
  {
    "path": "docs/exp-cpp/README.md",
    "content": "# C++ 专家级编程\n\n> 原书：[Expert C++](https://libgen.rs/book/index.php?md5=CF70B1E4CDD32EA36B07C154F1D95374)\n> \n> 协议：[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)\n> \n> 阶段：机翻（1）\n> \n> 自豪地采用[谷歌翻译](https://translate.google.cn/)\n> \n> 让开源界提前感受入关。\n\n* [在线阅读](https://ccpp.apachecn.org)\n* [在线阅读（Gitee）](https://apachecn.gitee.io/apachecn-c-cpp-zh/)\n* [ApacheCN 学习资源](http://docs.apachecn.org/)\n\n## 贡献指南\n\n本项目需要校对，欢迎大家提交 Pull Request。\n\n> 请您勇敢地去翻译和改进翻译。虽然我们追求卓越，但我们并不要求您做到十全十美，因此请不要担心因为翻译上犯错——在大部分情况下，我们的服务器已经记录所有的翻译，因此您不必担心会因为您的失误遭到无法挽回的破坏。（改编自维基百科）\n\n## 联系方式\n\n### 负责人\n\n* [飞龙](https://github.com/wizardforcel): 562826179\n\n### 其他\n\n*   在我们的 [apachecn/apachecn-c-cpp-zh](https://github.com/apachecn/apachecn-c-cpp-zh) github 上提 issue.\n*   发邮件到 Email: `apachecn@163.com`.\n*   在我们的 [组织学习交流群](http://www.apachecn.org/organization/348.html) 中联系群主/管理员即可.\n\n## 赞助我们\n\n![](http://data.apachecn.org/img/about/donate.jpg)\n"
  },
  {
    "path": "docs/exp-cpp/SUMMARY.md",
    "content": "+   [C++ 专家级编程](README.md)\n+   [零、前言](00.md)\n+   [第一部分：C++ 编程的背后](sec1.md)\n\t+   [一、构建 C++ 应用简介](01.md)\n\t+   [二、C++ 低级编程](02.md)\n\t+   [三、面向对象编程的细节](03.md)\n\t+   [四、理解和设计模板](04.md)\n\t+   [五、内存管理和智能指针](05.md)\n+   [第二部分：设计健壮高效的应用](sec2.md)\n\t+   [六、STL 中数据结构和算法的挖掘](06.md)\n\t+   [七、函数式编程](07.md)\n\t+   [八、并发和多线程](08.md)\n\t+   [九、设计并发数据结构](09.md)\n\t+   [十、设计全球通用的应用](10.md)\n\t+   [十一、使用设计模式设计策略游戏](11.md)\n\t+   [十二、网络和安全](12.md)\n\t+   [十三、调试和测试](13.md)\n\t+   [十四、使用 Qt 的图形用户界面](14.md)\n+   [第三部分：人工智能世界中的 C++](sec3.md)\n\t+   [十五、C++ 在机器学习任务中的应用](15.md)\n\t+   [十六、实现基于对话的搜索引擎](16.md)\n+   [十七、答案](17.md)\n"
  },
  {
    "path": "docs/exp-cpp/sec1.md",
    "content": "# 第一部分：C++ 编程的背后\n\n在本节中，读者将学习 C++ 程序编译和链接的细节，并深入了解**面向对象编程** ( **OOP** )、模板和内存管理的细节。\n\n本节包括以下章节:\n\n*   [第 1 章](01.html)*介绍* *构建 C++ 应用*\n*   [第二章](02.html)、*c++ 低级编程*\n*   [第三章](03.html)*面向对象编程的细节*\n*   [第四章](04.html)、*理解和设计模板*\n*   [第 5 章，](05.html) *内存管理和智能指针*"
  },
  {
    "path": "docs/exp-cpp/sec2.md",
    "content": "# 第二部分：设计健壮高效的应用\n\n本节将集中讨论使用数据结构、算法和并发工具进行数据处理的效率。我们还将介绍基本的设计模式和最佳实践。\n\n本节包括以下章节:\n\n*   [第 6 章](06.html)、*挖掘 STL* 中的数据结构和算法\n*   [第七章](07.html)*功能编程*\n*   [第八章](08.html)、*并发和多线程*\n*   [第 9 章](09.html)，*设计并发数据结构*\n*   [第 10 章](10.html)，*设计全球通用的应用*\n*   [第 11 章](11.html)，*使用设计模式设计战略游戏*\n*   [第十二章](12.html)*网络与安全*\n*   [第十三章](13.html)*调试测试*\n*   [第 14 章](14.html)，*带 Qt 的图形用户界面*"
  },
  {
    "path": "docs/exp-cpp/sec3.md",
    "content": "# 第三部分：人工智能世界中的 C++\n\n这一部分是对人工智能和机器学习最新进展的概述。我们将使用 C++ 解决机器学习任务，并设计一个基于对话的搜索引擎。\n\n本节包括以下章节:\n\n*   [第十五章](15.html)，*在机器学习任务中使用 C++ 的*\n*   [第 16 章](16.html) *，实现基于对话的搜索引擎*"
  },
  {
    "path": "docs/exp-cpp-prog/00.md",
    "content": "# 零、新的 C++ 17 特性\n\n在本章中，我们将介绍以下食谱:\n\n*   使用结构化绑定来解包捆绑的返回值\n*   将变量范围限制为`if`和`switch`语句\n*   根据新的括号初始值设定项规则进行分析\n*   让构造函数自动推导出生成的模板类类型\n*   用 constexpr-if 简化编译时决策\n*   启用带有内联变量的纯头文件库\n*   用折叠表达式实现方便的助手函数\n\n# 介绍\n\nC++ 在 C++ 11、C++ 14 以及最近的 C++ 17 中有很多增加。到目前为止，与十年前相比，这是一种完全不同的语言。C++ 标准不仅使语言标准化，因为它需要被编译器理解，而且还使 C++ 标准模板库(STL)标准化。\n\n这本书用大量的例子解释了如何最好地使用 STL。但首先，本章将集中讨论最重要的新语言特性。掌握它们将极大地帮助您编写可读性、可维护性和表达性强的代码。\n\n我们将看到如何使用结构化绑定轻松访问对、元组和结构的单个成员，以及如何使用新的`if`和`switch`变量初始化功能来限制变量范围。语法歧义是由 C++ 11 用新的括号初始化语法引入的，括号初始化语法看起来与初始化列表相同，由*新的括号初始化规则*修复。模板类实例的确切的*类型*现在可以从实际的构造函数参数中*推导出来*，如果模板类的不同专门化将导致完全不同的代码，这现在可以很容易地用 constexpr-if 来表达。在许多情况下，使用新的*折叠表达式*，模板函数中变量参数包的处理变得更加容易。最后，在只有头文件的库中定义静态的全局可访问对象变得更加容易，因为它具有声明内联变量的新能力，这在以前只有函数才有可能。\n\n对于库的实现者来说，本章中的一些示例可能比实现应用的开发人员更有趣。虽然出于完整性的原因，我们将看一看这些特性，但是为了理解本书的其余部分，立即理解本章的所有示例并不太重要。\n\n# 使用结构化绑定来解包捆绑的返回值\n\nC++ 17 自带一个新功能，结合了语法糖和自动类型推演:**结构化绑定**。这些有助于将来自对、元组和结构的值赋给单个变量。在其他编程语言中，这也被称为**解包**。\n\n# 怎么做...\n\n应用结构化绑定以便从一个捆绑结构中分配多个变量总是一个步骤。我们先来看看在 C++ 17 之前是怎么做到的。然后，我们可以看一下多个例子，展示我们如何在 C++ 17 中做到这一点:\n\n*   访问`std::pair`的单个值:假设我们有一个数学函数`divide_remainder`，它接受一个*被除数*和一个*除数*参数，并返回两者的分数以及余数。它使用`std::pair`包返回这些值:\n\n```cpp\n        std::pair<int, int> divide_remainder(int dividend, int divisor);\n\n```\n\n考虑以下访问结果对的单个值的方式:\n\n```cpp\n        const auto result (divide_remainder(16, 3));\n        std::cout << \"16 / 3 is \" \n                  << result.first << \" with a remainder of \" \n                  << result.second << 'n';\n```\n\n我们现在可以将单个值赋给具有表达性名称的单个变量，而不是像前面的代码片段所示的那样，这样读起来更好:\n\n```cpp\n auto [fraction, remainder] = divide_remainder(16, 3);\n        std::cout << \"16 / 3 is \" \n                  << fraction << \" with a remainder of \"       \n                  << remainder << 'n';\n```\n\n*   结构化绑定也适用于`std::tuple`:我们来看下面的示例函数，它获取我们的在线股票信息:\n\n```cpp\n        std::tuple<std::string, \n                   std::chrono::system_clock::time_point, unsigned>\n        stock_info(const std::string &name);\n```\n\n将其结果分配给单个变量，就像前面的示例一样:\n\n```cpp\n const auto [name, valid_time, price] = stock_info(\"INTC\");\n```\n\n*   结构化绑定也适用于自定义结构:让我们假设如下结构:\n\n```cpp\n        struct employee {\n            unsigned id;\n            std::string name;\n            std::string role;\n            unsigned salary;\n        };\n```\n\n现在，我们可以使用结构化绑定来访问这些成员。我们甚至可以在一个循环中完成，假设我们有一个完整的向量:\n\n```cpp\n        int main()\n        {\n            std::vector<employee> employees {\n                /* Initialized from somewhere */};\n\n            for (const auto &[id, name, role, salary] : employees) {\n                std::cout << \"Name: \"   << name\n                          << \"Role: \"   << role\n                          << \"Salary: \" << salary << 'n';\n            }\n        }\n```\n\n# 它是如何工作的...\n\n结构化绑定总是以相同的模式应用:\n\n```cpp\nauto [var1, var2, ...] = <pair, tuple, struct, or array expression>;\n```\n\n*   变量列表`var1, var2, ...`必须与被赋值表达式包含的变量数量完全匹配。\n*   `<pair, tuple, struct, or array expression>`必须是下列之一:\n    *   安`std::pair`。\n    *   安`std::tuple`。\n    *   一`struct`。所有成员必须是*非静态的*并且定义在*同一个基类*中。第一个声明的成员分配给第一个变量，第二个成员分配给第二个变量，依此类推。\n    *   固定大小的数组。\n*   类型可以是`auto`、`const auto`、`const auto&`，甚至`auto&&`。\n\nNot only for the sake of *performance*, always make sure to minimize needless copies by using references when appropriate.\n\n如果我们在方括号之间写了太多的*或者*没有足够的*变量，编译器就会出错，告诉我们我们的错误:*\n\n```cpp\nstd::tuple<int, float, long> tup {1, 2.0, 3};\nauto [a, b] = tup; // Does not work\n```\n\n这个例子显然试图将一个有三个成员的元组变量填充到两个变量中。编译器立即对此感到窒息，并告诉我们我们的错误:\n\n```cpp\nerror: type 'std::tuple<int, float, long>' decomposes into 3 elements, but only 2 names were provided\nauto [a, b] = tup;\n```\n\n# 还有更多...\n\nSTL 中的许多基本数据结构都可以使用结构化绑定立即访问，而无需我们进行任何更改。例如，考虑打印一个`std::map`的所有项目的循环:\n\n```cpp\nstd::map<std::string, size_t> animal_population {\n    {\"humans\",   7000000000},\n    {\"chickens\", 17863376000},\n    {\"camels\",   24246291},\n    {\"sheep\",    1086881528},\n    /* … */\n};\n\nfor (const auto &[species, count] : animal_population) {\n    std::cout << \"There are \" << count << \" \" << species \n              << \" on this planet.n\";\n}\n```\n\n这个特殊的例子之所以有效，是因为当我们迭代一个`std::map`容器时，我们在每个迭代步骤中都得到了`std::pair<const key_type, value_type>`节点。确切地说，这些节点是使用结构化绑定功能来解包的(`key_type`是`species`字符串，`value_type`是种群数量`size_t`，以便在循环体中单独访问它们。\n\n在 C++ 17 之前，使用`std::tie`可以达到类似的效果:\n\n```cpp\nint remainder;\nstd::tie(std::ignore, remainder) = divide_remainder(16, 5);\nstd::cout << \"16 % 5 is \" << remainder << 'n';\n```\n\n此示例显示了如何将结果对解包为两个变量。`std::tie`不如结构化绑定强大，因为我们必须在之前定义所有想要绑定到*的变量。另一方面，这个例子显示了结构化绑定不具有的`std::tie`的强度:值`std::ignore`充当虚拟变量。结果的分数部分被分配给它，这导致该值被丢弃，因为在该示例中我们不需要它。*\n\nWhen using structured bindings, we don't have `tie` dummy variables, so we have to bind all the values to named variables. Doing so and ignoring some of them is efficient, nevertheless, because the compiler can optimize the unused bindings out easily.\n\n回到过去，`divide_remainder`功能可以通过以下方式实现，使用输出参数:\n\n```cpp\nbool divide_remainder(int dividend, int divisor, \n                      int &fraction, int &remainder);\n\n```\n\n访问它应该如下所示:\n\n```cpp\nint fraction, remainder;\nconst bool success {divide_remainder(16, 3, fraction, remainder)};\nif (success) {\n    std::cout << \"16 / 3 is \" << fraction << \" with a remainder of \" \n              << remainder << 'n';\n}\n```\n\n许多人仍然更喜欢这样，而不是返回像对、元组和结构这样的复杂结构，他们认为这样代码会更快，因为避免了这些值的中间副本。对于现代编译器来说，这不再是*了，因为现代编译器会优化中间副本。*\n\nApart from the missing language features in C, returning complex structures via return value was considered slow for a long time because the object had to be initialized in the returning function and then copied into the variable that should contain the return value on the caller side. Modern compilers support **return value optimization** (RVO), which enables for omitting intermediate copies.\n\n# 将变量范围限制为 if 和 switch 语句\n\n尽可能限制变量的范围是好的风格。然而，有时首先需要获得一些值，并且只有当它符合某个条件时，它才能被进一步处理。\n\n为此，C++ 17 附带了带有初始值设定项的`if`和`switch`语句。\n\n# 怎么做...\n\n在这个方法中，我们在两个支持的上下文中都使用了初始值设定项语法，以便查看它们如何整理我们的代码:\n\n*   `if`语句:假设我们想使用`std::map`的`find`方法在角色图中找到一个角色:\n\n```cpp\n       if (auto itr (character_map.find(c)); itr != character_map.end()) {\n           // *itr is valid. Do something with it.\n       } else {\n           // itr is the end-iterator. Don't dereference.\n       }\n       // itr is not available here at all\n\n```\n\n*   `switch`语句:这是从输入中获取一个字符，同时检查`switch`语句中的值以控制计算机游戏的方式:\n\n```cpp\n       switch (char c (getchar()); c) {\n           case 'a': move_left();  break;\n           case 's': move_back();  break;\n           case 'w': move_fwd();   break;\n           case 'd': move_right(); break;\n           case 'q': quit_game();  break;\n\n           case '0'...'9': select_tool('0' - c); break;\n\n           default:\n               std::cout << \"invalid input: \" << c << 'n';\n       }\n```\n\n# 它是如何工作的...\n\n带有初始值设定项的`if`和`switch`语句基本上只是语法糖。以下两个示例是等效的:\n\n*之前* C++ 17:\n\n```cpp\n{\n    auto var (init_value);\n    if (condition) {\n        // branch A. var is accessible\n    } else {\n        // branch B. var is accessible\n    }\n    // var is still accessible\n}\n```\n\n*自* C++ 17:\n\n```cpp\nif (auto var (init_value); condition) {\n    // branch A. var is accessible\n} else {\n    // branch B. var is accessible\n}\n// var is not accessible any longer\n```\n\n这同样适用于`switch`语句:\n\n在 C++ 17 之前:\n\n```cpp\n{\n    auto var (init_value);\n    switch (var) {\n    case 1: ...\n    case 2: ...\n    ...\n    }\n    // var is still accessible\n}\n```\n\n从 C++ 17 开始:\n\n```cpp\nswitch (auto var (init_value); var) {\ncase 1: ...\ncase 2: ...\n  ...\n}\n// var is not accessible any longer\n```\n\n这个特性对于保持变量的范围尽可能短非常有用。在 C++ 17 之前，这只能通过在代码周围使用额外的大括号来实现，正如 C++ 17 之前的示例所示。较短的生存期减少了作用域中的变量数量，这使我们的代码保持整洁，并且更容易重构。\n\n# 还有更多...\n\n另一个有趣的用例是关键部分的有限范围。考虑以下示例:\n\n```cpp\nif (std::lock_guard<std::mutex> lg {my_mutex}; some_condition) {\n    // Do something\n}\n```\n\n首先，创建一个`std::lock_guard`。这是一个接受互斥参数作为构造函数参数的类。它在构造函数中锁定互斥体，当它超出范围时，它在析构函数中再次解锁。这样就不可能*忘记*解锁互斥。在 C++ 17 之前，需要一对额外的大括号来确定它再次解锁的范围。\n\n另一个有趣的用例是弱指针的范围。请考虑以下几点:\n\n```cpp\nif (auto shared_pointer (weak_pointer.lock()); shared_pointer != nullptr) {\n    // Yes, the shared object does still exist\n} else {\n    // shared_pointer var is accessible, but a null pointer\n}\n// shared_pointer is not accessible any longer\n```\n\n这是另一个例子，我们会有一个无用的`shared_pointer`变量泄漏到当前范围内，尽管它在`if`条件块或嘈杂的额外括号之外有一个潜在的无用状态！\n\n当使用带有输出参数的*传统*应用编程接口时，带有初始值设定项的`if`语句特别有用:\n\n```cpp\nif (DWORD exit_code; GetExitCodeProcess(process_handle, &exit_code)) {\n    std::cout << \"Exit code of process was: \" << exit_code << 'n';\n}\n// No useless exit_code variable outside the if-conditional\n```\n\n`GetExitCodeProcess`是一个 Windows 内核 API 函数。它返回给定进程句柄的退出代码，但前提是该句柄有效。离开这个条件块后，变量就没用了，所以我们不再需要它在任何范围内。\n\n能够在`if`块内初始化变量显然在很多情况下非常有用，尤其是在处理使用输出参数的遗留 API 时。\n\nKeep your scopes tight using `if` and `switch` statement initializers. This makes your code more compact, easier to read, and in code refactoring sessions, it will be easier to move around.\n\n# 根据新的括号初始值设定项规则进行分析\n\nC++ 11 附带了新的大括号初始化语法`{}`。其目的是允许*聚合*初始化，但也允许通常的构造函数调用。不幸的是，当把这个语法和`auto`变量类型结合在一起时，表达错误的东西太容易了。C++ 17 附带了一组增强的初始化规则。在这个食谱中，我们将阐明如何在 C++ 17 中用哪种语法正确初始化变量。\n\n# 怎么做...\n\n变量一步初始化。使用初始值设定项语法，有两种不同的情况:\n\n*   使用大括号初始值设定项语法*而不使用* `auto`类型演绎:\n\n```cpp\n       // Three identical ways to initialize an int:\n       int x1 = 1;\n       int x2  {1};\n       int x3  (1);\n\n       std::vector<int> v1   {1, 2, 3}; // Vector with three ints: 1, 2, 3\n       std::vector<int> v2 = {1, 2, 3}; // same here\n       std::vector<int> v3   (10, 20);  // Vector with 10 ints, \n                                        // each have value 20\n```\n\n*   使用大括号初始化器语法*配合* `auto`类型推演:\n\n```cpp\n       auto v   {1};         // v is int\n       auto w   {1, 2};      // error: only single elements in direct \n                             // auto initialization allowed! (this is new)\n       auto x = {1};         // x is std::initializer_list<int>\n       auto y = {1, 2};      // y is std::initializer_list<int>\n       auto z = {1, 2, 3.0}; // error: Cannot deduce element type\n```\n\n# 它是如何工作的...\n\n没有`auto`类型演绎，至少在初始化常规类型时，大括号`{}`操作符没什么好惊讶的。初始化容器时，如`std::vector`、`std::list`等，括号初始化器将匹配该容器类的`std::initializer_list`构造器。它以*贪婪*的方式做到这一点，这意味着不可能匹配非聚合构造函数(与接受初始化列表的构造函数相比，非聚合构造函数是常见的构造函数)。\n\n`std::vector`例如，提供了一个特定的非聚合构造函数，它用相同的值任意填充许多项:`std::vector<int> v (N, value)`。写`std::vector<int> v {N, value}`时，选择`initializer_list`构造函数，用两项初始化向量:`N`和`value`。这是一个人们应该知道的特殊陷阱。\n\n与使用普通`()`括号调用构造函数相比，`{}`运算符的一个很好的细节是，它们不进行隐式类型转换:`int x (1.2);`和`int x = 1.2;`将通过静默舍入浮点值并将其转换为 int 来初始化`x`为值`1`。相比之下，`int x {1.2};`不会编译，因为它想让*和*完全匹配构造函数类型。\n\nOne can controversially argue about which initialization style is the best one.\nFans of the bracket initialization style say that using brackets makes it very explicit, that the variable is initialized with a constructor call, and that this code line is not reinitializing anything. Furthermore, using `{}` brackets will select the only matching constructor, while initializer lines using `()` parentheses try to match the closest constructor and even do type conversion in order to match.\n\nC++ 17 中引入的额外规则会影响`auto`类型演绎的初始化——虽然 C++ 11 会正确地使变量`auto x {123};`的类型成为只有一个元素的`std::initializer_list<int>`，但这很少是我们想要的。C++ 17 会把同一个变量变成`int`。\n\n经验法则:\n\n*   `auto var_name {one_element};`推断`var_name`与`one_element`同类型\n*   `auto var_name {element1, element2, ...};`无效，不编译\n*   `auto var_name = {element1, element2, ...};`演绎为一个`std::initializer_list<T>``T`与列表中所有元素的类型相同\n\nC++ 17 使得意外定义初始化列表变得更加困难。\n\nTrying this out with different compilers in C++ 11/C++ 14 mode will show that some compilers actually deduce `auto x {123};` to an `int`, while others deduce it to `std::initializer_list<int>`. Writing code like this can lead to problems regarding portability!\n\n# 让构造函数自动推导出生成的模板类类型\n\nC++ 中的许多类通常专门处理类型，这很容易从用户在构造函数调用中输入的变量类型中推断出来。然而，在 C++ 17 之前，这并不是一个标准化的特性。C++ 17 让编译器*自动*从构造函数调用中推导出模板类型。\n\n# 怎么做...\n\n一个非常方便的用例是构建`std::pair`和`std::tuple`实例。这些可以在一个步骤中专门化、实例化和专门化:\n\n```cpp\nstd::pair  my_pair  (123, \"abc\");       // std::pair<int, const char*>\nstd::tuple my_tuple (123, 12.3, \"abc\"); // std::tuple<int, double,\n                                        //            const char*>\n```\n\n# 它是如何工作的...\n\n让我们定义一个示例类，其中自动模板类型扣除将是有价值的:\n\n```cpp\ntemplate <typename T1, typename T2, typename T3>\nclass my_wrapper {\n    T1 t1;\n    T2 t2;\n    T3 t3;\n\npublic:\n    explicit my_wrapper(T1 t1_, T2 t2_, T3 t3_) \n        : t1{t1_}, t2{t2_}, t3{t3_}\n    {}\n\n    /* … */\n};\n```\n\n好吧，这只是另一个模板类。为了实例化它，我们之前必须编写以下内容:\n\n```cpp\nmy_wrapper<int, double, const char *> wrapper {123, 1.23, \"abc\"};\n```\n\n我们现在可以省略模板专门化部分:\n\n```cpp\nmy_wrapper wrapper {123, 1.23, \"abc\"};\n```\n\n在 C++ 17 之前，这只能通过实现 *make 函数助手*来实现:\n\n```cpp\nmy_wrapper<T1, T2, T3> make_wrapper(T1 t1, T2 t2, T3 t3)\n{\n    return {t1, t2, t3};\n}\n```\n\n使用这样的助手，有可能产生类似的效果:\n\n```cpp\nauto wrapper (make_wrapper(123, 1.23, \"abc\"));\n```\n\nThe STL already comes with a lot of helper functions such as that one: `std::make_shared`, `std::make_unique`, `std::make_tuple`, and so on. In C++ 17, these can now mostly be regarded as obsolete. Of course, they will be provided further for compatibility reasons.\n\n# 还有更多...\n\n我们刚刚了解到的是*隐式模板式推演*。在某些情况下，我们不能依赖隐式类型演绎。考虑以下示例类:\n\n```cpp\ntemplate <typename T>\nstruct sum {\n    T value;\n\n    template <typename ... Ts>\n    sum(Ts&& ... values) : value{(values + ...)} {}\n};\n```\n\n这个结构`sum`接受任意数量的参数，并使用一个 fold 表达式将它们加在一起(在本章稍后部分查看 fold 表达式配方，以获得关于 fold 表达式的更多细节)。结果总和保存在成员变量`value`中。现在的问题是，`T`是什么类型？如果我们不想显式地指定它，它肯定需要依赖于构造函数中提供的值的类型。如果我们提供字符串实例，它需要是`std::string`。如果我们提供整数，它需要是`int`。如果我们提供整数、浮点数和双精度数，编译器需要在不丢失信息的情况下找出适合所有值的类型。为了实现这一点，我们提供了一个*明确的扣除指南*:\n\n```cpp\ntemplate <typename ... Ts>\nsum(Ts&& ... ts) -> sum<std::common_type_t<Ts...>>;\n```\n\n这个推导指南告诉编译器使用`std::common_type_t`特性，它能够找出哪个公共类型适合所有的值。让我们看看如何使用它:\n\n```cpp\nsum s          {1u, 2.0, 3, 4.0f};\nsum string_sum {std::string{\"abc\"}, \"def\"};\n\nstd::cout << s.value          << 'n'\n          << string_sum.value << 'n';\n```\n\n在第一行中，我们用类型为`unsigned`、`double`、`int`和`float`的构造函数参数实例化了一个`sum`对象。`std::common_type_t`返回`double`作为常用类型，所以我们得到一个`sum<double>`实例。在第二行中，我们提供了一个`std::string`实例和一个 C 风格的字符串。遵循我们的推导指南，编译器构建了类型`sum<std::string>`的实例。\n\n运行该代码时，将打印`10`为数字和，`abcdef`为字符串*和*。\n\n# 用 constexpr-if 简化编译时决策\n\n在模板化代码中，根据模板专用的类型，经常需要以不同的方式做某些事情。C++ 17 附带了 constexpr-if 表达式，这大大简化了这种情况下的代码*。*\n\n *# 怎么做...\n\n在这个食谱中，我们将实现一个小助手模板类。它可以处理不同的模板类型专门化，因为它能够在某些段落中选择完全不同的代码，这取决于我们专门化它的类型:\n\n1.  编写代码中通用的部分。在我们的示例中，它是一个简单的类，支持使用`add`函数向类型`T`成员值添加类型`U`值:\n\n```cpp\n       template <typename T>\n       class addable\n       { \n           T val;\n\n       public:\n           addable(T v) : val{v} {}\n\n           template <typename U>\n           T add(U x) const {\n               return val + x;\n           }\n       };\n```\n\n2.  想象一下`T`型是`std::vector<something>`，而`U`型只是`int`。把一个整数加到整个向量上意味着什么？假设这意味着我们把整数加到向量的每一项上。这将循环进行:\n\n```cpp\n       template <typename U>\n       T add(U x) \n       {\n           auto copy (val); // Get a copy of the vector member\n           for (auto &n : copy) { \n               n += x;\n           }\n           return copy;\n       }\n```\n\n3.  下一步也是最后一步是*联合*两个世界。如果`T`是`U`项的向量，执行*循环*变量。如果不是，则执行*正常*添加:\n\n```cpp\n       template <typename U>\n       T add(U x) const {\n           if constexpr (std::is_same_v<T, std::vector<U>>) {\n               auto copy (val);\n               for (auto &n : copy) { \n                   n += x;\n               }\n               return copy;\n           } else {\n               return val + x;\n           }\n       }\n\n```\n\n4.  这个类现在可以使用了。让我们看看它与完全不同的类型配合得有多好，例如`int`、`float`、`std::vector<int>`和`std::vector<string>`:\n\n```cpp\n       addable<int>{1}.add(2);               // is 3\n       addable<float>{1.0}.add(2);           // is 3.0\n       addable<std::string>{\"aa\"}.add(\"bb\"); // is \"aabb\"\n\n       std::vector<int> v {1, 2, 3};\n       addable<std::vector<int>>{v}.add(10); \n           // is std::vector<int>{11, 12, 13}\n\n       std::vector<std::string> sv {\"a\", \"b\", \"c\"};\n       addable<std::vector<std::string>>{sv}.add(std::string{\"z\"}); \n           // is {\"az\", \"bz\", \"cz\"}\n```\n\n# 它是如何工作的...\n\n新的 constexpr-if 与通常的 if-else 构造完全一样。不同的是，它测试的条件必须在*编译时*进行评估。编译器从我们的程序中创建的所有运行时代码将不包含任何来自 constexpr-if 条件的分支指令。也可以说，它的工作方式类似于预处理器`#if`和`#else`文本替换宏，但对于这些宏，代码甚至不必在语法上格式良好。constexpr-if 构造的所有分支都需要是*句法格式良好的*，但不是的分支不需要是*语义有效的*。\n\n为了区分代码是否应该将值`x`添加到向量中，我们使用类型特征`std::is_same`。如果`A`和`B`属于同一类型，则表达式`std::is_same<A, B>::value`计算为布尔值`true`。我们的配方中使用的条件是`std::is_same<T, std::vector<U>>::value`，如果用户在`T = std::vector<X>`上专门化了类，并试图用类型为`U = X`的参数调用`add`，则该条件评估为`true`。\n\n当然，在一个 constexpr-if-else 块中可以有多个条件(注意`a`和`b`必须依赖于模板参数，而不仅仅是编译时常数):\n\n```cpp\nif constexpr (a) {\n    // do something\n} else if constexpr (b) {\n    // do something else \n} else {\n    // do something completely different\n}\n```\n\n有了 C++ 17，很多元编程的情况更容易表达和阅读。\n\n# 还有更多...\n\n为了说明 constexpr-if 构造对 C++ 的改进有多大，我们可以看看在 C++ 17 之前，同样的事情是如何实现的*:*\n\n```cpp\ntemplate <typename T>\nclass addable\n{\n    T val;\n\npublic:\n    addable(T v) : val{v} {}\n\n    template <typename U>\n std::enable_if_t<!std::is_same<T, std::vector<U>>::value, T>\n    add(U x) const { return val + x; }\n\n    template <typename U>\n std::enable_if_t<std::is_same<T, std::vector<U>>::value, \n                     std::vector<U>>\n    add(U x) const {\n        auto copy (val);\n        for (auto &n : copy) { \n            n += x;\n        }\n        return copy;\n    }\n};\n```\n\n在不使用 constexpr-if 的情况下，这个类适用于我们想要的所有不同类型，但是它看起来超级复杂。它是如何工作的？\n\n两个不同的 `add`函数的单独实现看起来很简单。这是它们的返回类型声明，这使它们看起来很复杂，并且包含一个技巧——如果`condition`是`true`，则表达式如`std::enable_if_t<condition, type>`计算为`type`。否则，`std::enable_if_t`表达式不评估任何东西。这通常会被认为是一个错误，但我们会看到为什么不是。\n\n对于第二个`add`功能，以*反转*的方式使用相同的条件。这样，对于两个实现中的一个，只能同时是`true`。\n\n当编译器看到同名的不同模板函数，必须从中选择一个时，一个重要的原则就起作用了: **SFINAE** ，代表**替换失败不是错误**。在这种情况下，这意味着如果不能从错误的模板表达式中推导出这些函数之一的返回值，编译器不会出错(如果其条件评估为`false`，则为`std::enable_if`)。它将简单地看得更远，并尝试其他*功能实现。这就是诀窍；这就是它的工作原理。*\n\n *真麻烦。很高兴看到这在 C++ 17 中变得如此容易。\n\n# 启用带有内联变量的纯头文件库\n\n虽然在 C++ 中总是可以内联声明单个函数*，但是 C++ 17 还允许我们内联声明*变量*。这使得实现*纯头文件*库变得更加容易，而这在以前只有使用变通方法才能实现。*\n\n *# 是怎么做到的...\n\n在这个方法中，我们创建了一个示例类，它可以作为一个典型的仅头库的成员。目标是给它一个静态成员，并使用`inline`关键字以全局可用的方式实例化它，这在 C++ 17 之前是不可能的:\n\n1.  `process_monitor`类应该包含一个静态成员，并且本身是全局可访问的，当包含在多个翻译单元中时，会产生双重定义的符号:\n\n```cpp\n       // foo_lib.hpp \n\n       class process_monitor { \n       public: \n           static const std::string standard_string \n               {\"some static globally available string\"}; \n       };\n\n       process_monitor global_process_monitor;\n```\n\n2.  如果我们现在将它包含在多个`.cpp`文件中，以便编译和链接它们，这将在链接器阶段失败。为了解决这个问题，我们添加了`inline`关键字:\n\n```cpp\n       // foo_lib.hpp \n\n       class process_monitor { \n       public: \n           static const inline std::string standard_string \n               {\"some static globally available string\"}; \n       };\n\n       inline process_monitor global_process_monitor;\n```\n\n瞧，就是这样！\n\n# 它是如何工作的...\n\nC++ 程序通常由多个 C++ 源文件组成(这些文件有`.cpp`或`.cc`就够了)。这些被单独编译成模块/目标文件(通常有。o 就够了)。最后一步是将所有模块/目标文件链接到一个可执行文件或共享/静态库。\n\n在链接阶段，如果链接器可以多次找到一个特定符号*的定义*，则认为是错误的。比方说，我们有一个带有签名的函数，比如`int foo();`。如果两个模块定义同一个函数，哪个是正确的？链接器不能只是掷骰子。嗯，有可能，但这很可能不是任何程序员都希望发生的事情。\n\n提供全局可用函数的传统方式是*在头文件中声明*，任何需要调用它们的 C++ 模块都会包含这些头文件。每一个功能的定义将被放入单独的模块文件中*。然后，这些与希望使用这些功能的模块链接在一起。这也叫做**一个定义规则** ( **ODR** )。为了更好地理解，请查看下图:*\n\n![](img/ee850b95-1991-4682-a5d1-1c7290509001.png)\n\n然而，如果这是唯一的方法，那么就不可能提供仅头库。只有头文件的库非常方便，因为它们只需要使用`#include`包含在任何 C++ 程序文件中，然后就可以立即使用。为了使用不仅仅是头文件的库，程序员还必须修改构建脚本，以便让链接器将库模块和他自己的模块文件链接在一起。尤其是对于只有很短功能的库，这是不必要的不舒服。\n\n对于这种情况，可以使用`inline`关键字进行例外处理，以便允许在不同模块中对同一符号进行多个定义。如果链接器发现多个符号具有相同的签名，但它们是内联声明的，它将只选择第一个符号，并相信其他符号具有相同的定义。所有相等的内联符号被定义为完全相等，这基本上是程序员的承诺*。*\n\n *关于我们的配方示例，链接器将在包含`foo_lib.hpp`的每个模块中找到`process_monitor::standard_string`符号。没有`inline`关键字，它不知道选择哪个，所以会中止并报告错误。这同样适用于`global_process_monitor`符号。哪一个是正确的？\n\n在声明两个符号`inline`后，它将只接受每个符号的第一次出现，*丢弃所有其他符号*。\n\n在 C++ 17 之前，唯一干净的方法是通过一个额外的 C++ 模块文件来提供这个符号，这将迫使我们的库用户在链接步骤中包含这个文件。\n\n`inline`关键字传统上也有*另一个*功能。它告诉编译器，它可以通过获取函数的实现并直接将其放在被调用的地方来*消除*函数调用。这样，调用代码少包含一个函数调用，这通常可以被认为是更快。如果函数非常短，则生成的程序集也将更短(假设执行函数调用、保存和恢复堆栈等的指令数量高于实际有效负载代码)。如果内联的函数很长，二进制文件的大小就会增加，这有时甚至不会导致更快的代码。\n因此，编译器将只使用`inline`关键字作为提示，并可能通过内联它们来消除函数调用。但是它也可以内联一些函数*，而不需要程序员将其声明为内联。*\n\n# 还有更多...\n\n在 C++ 17 之前，一个可能的解决方法是提供一个`static`函数，该函数返回对一个`static`对象的引用:\n\n```cpp\nclass foo {\npublic:\n    static std::string& standard_string() {\n        static std::string s {\"some standard string\"};\n        return s;\n    }\n};\n```\n\n这样，将头文件包含在多个模块中是完全合法的，但仍然可以在任何地方访问完全相同的实例。然而，对象是*而不是*在程序开始时立即构造的*，但只是在这个 getter 函数的第一次调用时。对于一些用例，这确实是一个问题。想象一下，我们希望静态的、全局可用的对象的构造函数在*程序开始*时做一些重要的事情(就像我们的示例库类一样)，但是由于 getter 在接近程序结束时被调用，这已经太晚了。*\n\n另一种变通方法是将非模板类`foo`做成模板类，这样它就可以从与模板相同的规则中获益。\n\n这两种策略在 C++ 17 中都可以避免。\n\n# 用折叠表达式实现方便的助手函数\n\n从 C++ 11 开始，就有可变模板参数包，可以实现接受任意多参数的函数。有时，这些参数都被组合成一个表达式，以便从中导出函数结果。这个任务在 C++ 17 中变得非常容易，因为它带有折叠表达式。\n\n# 怎么做...\n\n让我们实现一个函数，该函数接受任意多的参数并返回它们的和:\n\n1.  首先，我们定义它的签名:\n\n```cpp\n      template <typename ... Ts>\n      auto sum(Ts ... ts);\n```\n\n2.  所以，我们现在有了一个参数包`ts`，函数应该展开所有参数，并使用 fold 表达式将它们相加在一起。如果我们将任何运算符(【在本例中为 T1】)与`...`一起使用，以便将其应用于参数包的所有值，我们需要用括号将表达式括起来:\n\n```cpp\n      template <typename ... Ts>\n      auto sum(Ts ... ts)\n      {\n          return (ts + ...);\n      }\n```\n\n3.  我们现在可以这样称呼它:\n\n```cpp\n      int the_sum {sum(1, 2, 3, 4, 5)}; // Value: 15\n```\n\n4.  它不仅适用于`int`类型；我们可以用任何只实现`+`运算符的类型来称呼它，比如`std::string`:\n\n```cpp\n      std::string a {\"Hello \"};\n      std::string b {\"World\"};\n\n      std::cout << sum(a, b) << 'n'; // Output: Hello World\n```\n\n# 它是如何工作的...\n\n我们刚才所做的是一个简单的递归应用二进制运算符(`+`)到它的参数。这一般叫做*折叠*。C++ 17 自带**折叠表达式**，有助于用更少的代码表达相同的思想。\n\n这种表达叫做**一元折叠**。C++ 17 支持使用以下二进制运算符折叠参数包:`+`、`-`、`*`、`/`、`%`、`^`、`&`、`|`、`=`、`<`、`>`、`<<`、`>>`、`+=`、`-=`、`*=`、`/=`、`%=`、`^=`、`&=`、`|=`、`<<=`、`>>=`、`==`、`!=`\n\n顺便说一下，在我们的示例代码中，我们写`(ts + …)`还是`(… + ts)`并不重要；两者都有效。然而，在其他情况下可能存在相关的差异-如果`…`点位于操作员的*右侧*侧，则该折叠称为*右侧*折叠。如果他们在*左手*边，那就是一个*左*折。\n\n在我们的`sum`例子中，一元左折叠扩展到`1 + (2 + (3 + (4 + 5)))`，而一元右折叠将扩展到`(((1 + 2) + 3) + 4) + 5`。根据使用的操作员，这可能会有所不同。当添加数字时，它不会。\n\n# 还有更多...\n\n如果有人用*而不是*参数调用`sum()`，变量参数包不包含可以折叠的值。对于大多数操作者来说，这是一个错误(对于一些人来说，不是；我们将在一分钟后看到这一点)。然后，我们需要决定这应该是一个错误，还是一个空的总和应该导致一个特定的值。显而易见的想法是，无的和就是`0`。\n\n事情是这样的:\n\n```cpp\ntemplate <typename ... Ts>\nauto sum(Ts ... ts)\n{\n    return (ts + ... + 0);\n}\n```\n\n这样，`sum()`评估为`0`，`sum(1, 2, 3)`评估为`(1 + (2 + (3 + 0)))`。这种有初始值的褶皱叫做**二元褶皱**。\n\n同样，如果我们写`(ts + ... + 0)`或`(0 + ... + ts)`，它会工作，但是这使得二进制折叠再次变为二进制*右*折叠或二进制*左*折叠。查看下图:\n\n![](img/4c518bfa-0a12-435d-820f-0199ee897ce3.png)\n\n当使用二进制折叠来实现无参数情况时,*标识*元素的概念通常很重要——在这种情况下，向任何数字添加`0`都不会改变什么，这使得`0`成为标识元素。由于这个属性，我们可以用运算符`+`或`-`在任何一个 fold 表达式中添加一个`0`，如果参数包中没有参数，就会得到结果`0`。从数学的角度来看，这是正确的。从实现的角度来看，我们需要定义什么是正确的，这取决于我们需要什么。\n\n同样的原理也适用于乘法。这里，身份元素为`1`:\n\n```cpp\ntemplate <typename ... Ts>\nauto product(Ts ... ts)\n{\n    return (ts * ... * 1);\n}\n```\n\n`product(2, 3)`的结果为`6`，无参数的`product()`的结果为`1`。\n\n逻辑**、** ( `&&`)、**或** ( `||`)运算符自带*内置*标识元素。用`&&`折叠空参数包会得到`true`，用`||`折叠空参数包会得到`false`。\n\n当应用于空参数包时，默认为某个表达式的另一个运算符是逗号运算符(`,`)，然后默认为`void()`。\n\n为了点燃一些灵感，让我们看看更多的小助手，我们可以使用这个特性来实现。\n\n# 将范围与单个项目进行匹配\n\n一个函数来告诉某个范围是否包含至少一个我们作为变量参数提供的值，怎么样？\n\n```cpp\ntemplate <typename R, typename ... Ts>\nauto matches(const R& range, Ts ... ts)\n{\n    return (std::count(std::begin(range), std::end(range), ts) + ...);\n}\n```\n\n辅助函数使用 STL 中的`std::count`函数。该函数取三个参数:前两个参数是某个可迭代范围的*开始*和*结束*迭代器，作为第三个参数，它取一个*值*，该值将与该范围的所有项目进行比较。`std::count`方法然后返回范围内等于第三个参数的所有元素的数量。\n\n在我们的 fold 表达式中，我们总是将相同参数范围的*开始*和*结束*迭代器送入`std::count`函数。但是，作为第三个参数，每次我们将参数包中的另一个参数放入其中。最后，该函数将所有结果相加并返回给调用者。\n\n我们可以这样使用它:\n\n```cpp\nstd::vector<int> v {1, 2, 3, 4, 5};\n\nmatches(v,         2, 5);          // returns 2\nmatches(v,         100, 200);      // returns 0\nmatches(\"abcdefg\", 'x', 'y', 'z'); // returns 0\nmatches(\"abcdefg\", 'a', 'd', 'f'); // returns 3\n```\n\n我们可以看到，`matches`辅助函数是相当通用的——它可以直接在向量上甚至字符串上调用。它还可以在初始化列表上工作，在`std::list`、`std::array`、`std::set`等实例上工作！\n\n# 检查向集合中的多次插入是否成功\n\n让我们编写一个助手，在一个`std::set`中插入任意数量的变量参数，如果所有的插入都成功*则返回*:\n\n```cpp\ntemplate <typename T, typename ... Ts>\nbool insert_all(T &set, Ts ... ts)\n{\n    return (set.insert(ts).second && ...);\n}\n```\n\n那么，这是如何工作的呢？`std::set`的`insert`功能有以下签名:\n\n```cpp\nstd::pair<iterator, bool> insert(const value_type& value);\n```\n\n文档中说，当我们试图插入一个项目时，`insert`函数将返回一对中的一个`iterator`和一个`bool`变量。如果插入成功，`bool`值为`true`。如果成功，迭代器指向集合中的*新元素*。否则，迭代器指向*现有的*项，这将使*与要插入的项相冲突。*\n\n我们的助手函数在插入后访问`.second`字段，这只是反映成功或失败的`bool`变量。如果所有的插入都导致所有返回对中的`true`，那么所有的插入都是成功的。fold 表达式将所有插入结果与`&&`运算符组合在一起，并返回结果。\n\n我们可以这样使用它:\n\n```cpp\nstd::set<int> my_set {1, 2, 3};\n\ninsert_all(my_set, 4, 5, 6); // Returns true\ninsert_all(my_set, 7, 8, 2); // Returns false, because the 2 collides\n```\n\n请注意，如果我们尝试插入例如三个元素，但第二个元素已经不能插入，则`&& ...`折叠将短路并停止插入所有其他元素:\n\n```cpp\nstd::set<int> my_set {1, 2, 3};\n\ninsert_all(my_set, 4, 2, 5); // Returns false\n// set contains {1, 2, 3, 4} now, without the 5!\n\n```\n\n# 检查所有参数是否在一定范围内\n\n如果我们可以检查一个*变量是否在某个特定范围内，我们也可以使用 fold 表达式对多个*变量执行相同的操作:**\n\n```cpp\ntemplate <typename T, typename ... Ts>\nbool within(T min, T max, Ts ...ts)\n{\n    return ((min <= ts && ts <= max) && ...);\n}\n```\n\n表达式`(min <= ts && ts <= max)`确实说明了参数包的每个值是否在`min`和`max`(包括 `min`和`max`)之间。我们选择`&&`运算符将所有布尔结果简化为单个结果，如果所有单个结果都是`true`，则只有`true`。\n\n这是它在行动中的样子:\n\n```cpp\nwithin( 10,  20,  1, 15, 30);    // --> false\nwithin( 10,  20,  11, 12, 13);   // --> true\nwithin(5.0, 5.5,  5.1, 5.2, 5.3) // --> true\n```\n\n有趣的是，这个功能非常通用，因为它对我们使用的类型的唯一要求是它们是*可与`<=`操作符相媲美的*。而`std::string`也满足了这一要求，例如:\n\n```cpp\nstd::string aaa {\"aaa\"};\nstd::string bcd {\"bcd\"};\nstd::string def {\"def\"};\nstd::string zzz {\"zzz\"};\n\nwithin(aaa, zzz,  bcd, def); // --> true\nwithin(aaa, def,  bcd, zzz); // --> false\n```\n\n# 将多个项目推入一个向量\n\n还可以编写一个助手，它不会减少任何结果，但会处理多个同类操作。就像在不返回任何结果的`std::vector`中插入项目一样(`std::vector::insert()`通过抛出异常来发出错误信号):\n\n```cpp\ntemplate <typename T, typename ... Ts>\nvoid insert_all(std::vector<T> &vec, Ts ... ts)\n{\n    (vec.push_back(ts), ...);\n}\n\nint main()\n{\n    std::vector<int> v {1, 2, 3};\n    insert_all(v, 4, 5, 6);\n}\n```\n\n请注意，我们使用逗号(`,`)运算符将参数包扩展为单独的`vec.push_back(...)`调用，而不折叠实际结果。这个函数也可以很好地处理一个空的参数包，因为逗号操作符有一个隐式的标识元素，T2，它翻译成什么也不做。****"
  },
  {
    "path": "docs/exp-cpp-prog/01.md",
    "content": "# 一、容器\n\n我们将在本章介绍以下食谱:\n\n*   使用`std::vector`上的擦除-移除成语\n*   在 T2 时间从未排序的中删除项目\n*   快速或安全地访问`std::vector`实例\n*   保持`std::vector`实例排序\n*   高效且有条件地将项目插入`std::map`\n*   了解`std::map::insert` 的新插入提示语义\n*   有效修改`std::map`项的键\n*   使用自定义类型的`std::unordered_map`\n*   从用户输入中过滤重复项，并用`std::set`按字母顺序打印\n*   用`std::stack`实现一个简单的 RPN 计算器\n*   用`std::map`实现字频计数器\n*   用`std::set`实现一个寻找文本中超长句子的写作风格辅助工具\n*   使用`std::priority_queue`执行个人待办事项列表\n\n# 在 std::vector 上使用擦除-移除习惯用法\n\n很多 C++ 新手程序员了解到`std::vector`，它基本上像一个*自动增长数组*一样工作，就此打住。后来，他们只查找它的文档，以便了解如何做非常具体的事情，例如，*移除*物品。像这样使用 STL 容器只会触及它们对编写*干净*、*可维护、*和*快速*代码有多大帮助的表面。\n\n这一部分是关于从向量实例中间移除项目。当一个项目从向量中消失，并且位于其他项目之间的中间位置*时，它右边的所有项目必须*将*向左移动一个槽*(这使得该任务的运行时间成本在 *O(n)* 内)。许多新手程序员会使用*循环*来做这件事，因为这也不是一件很难做的事情。不幸的是，在这样做的时候，他们可能会忽略很多优化潜力。最后，手工制作的循环既没有比 STL 方式更快的*，也没有比 STL 方式更漂亮的*，我们接下来会看到。****\n\n **# 怎么做...\n\n在这一节中，我们用一些示例整数填充一个`std::vector`实例，然后从中删除一些特定的项目。我们这样做的方式被认为是从向量中移除多个项目的*正确的*方式。\n\n1.  当然，在我们做任何事情之前，我们需要包括一些标题。\n\n```cpp\n      #include <iostream>\n      #include <vector>\n      #include <algorithm>\n```\n\n2.  然后，我们声明我们正在使用名称空间`std`来节省一些打字时间。\n\n```cpp\n      using namespace std;\n```\n\n3.  现在我们创建一个整数向量，并用一些示例项填充它。\n\n```cpp\n      int main()\n      {\n          vector<int> v {1, 2, 3, 2, 5, 2, 6, 2, 4, 8};\n```\n\n4.  下一步是移除这些项目。我们移除什么？有多个`2`值。让我们移除它们。\n\n```cpp\n          const auto new_end (remove(begin(v), end(v), 2));\n```\n\n5.  有趣的是，这只是两步中的一步。向量仍然具有相同的大小。下一行使它实际上更短。\n\n```cpp\n          v.erase(new_end, end(v));\n```\n\n6.  让我们在这里停下来，以便将向量的内容打印到终端，然后继续。\n\n```cpp\n          for (auto i : v) {\n              cout << i << \", \";\n          }\n          cout << 'n';\n```\n\n7.  现在，让我们移除整个*类*的物品，而不是特定的*值*。为了做到这一点，我们首先定义一个谓词函数，它接受一个数字作为参数，并返回`true`，如果它是一个*奇数*数字。\n\n```cpp\n          const auto odd ([](int i) { return i % 2 != 0; });\n```\n\n8.  现在我们使用`remove_if`函数，并用谓词函数来填充它。我们不再像以前那样分两步走，而是一步到位。\n\n```cpp\n          v.erase(remove_if(begin(v), end(v), odd), end(v));\n```\n\n9.  现在所有奇数项都没了，但是矢量的*容量*还是老 10 元素。在最后一步中，我们还将它减少到向量的实际*当前*大小。请注意，这可能会导致向量代码分配一个新的内存块，该内存块可以容纳旧块中的所有项目并将其移动到新块中。\n\n```cpp\n          v.shrink_to_fit();\n```\n\n10.  现在，让我们在第二次删除项目后打印内容，就这样。\n\n```cpp\n          for (auto i : v) {\n              cout << i << \", \";\n          }\n          cout << 'n';\n      }\n```\n\n11.  编译和运行程序会从两种项目删除方法中产生以下两个输出行。\n\n```cpp\n      $ ./main \n      1, 3, 5, 6, 4, 8, \n      6, 4, 8,\n```\n\n# 它是如何工作的...\n\n食谱中变得明显的是，当从向量中间移除项目时，它们首先需要被*移除*，然后*删除*。至少我们使用的函数有这样的名字。诚然，这令人困惑，但让我们仔细看看它，以了解这些步骤的意义。\n\n从向量中移除所有`2`值的代码如下所示:\n\n```cpp\nconst auto new_end (remove(begin(v), end(v), 2));\nv.erase(new_end, end(v));\n```\n\n`std::begin`和`std::end`函数都接受一个向量实例作为参数，并返回给我们迭代器，这些迭代器指向第一个*项和最后一个项之后的*，就像在即将到来的图表中绘制的那样。**\n\n *将这些值和数值`2`输入到`std::remove`函数后，它将向前移动非`2`值，就像我们可以用手动编程循环那样。在此过程中，算法将严格保持所有非`2`值的顺序。快速看一下插图可能会有点混乱。在第 2 步中，仍然有一个值`2`，向量应该变得更短，因为有四个值`2`，它们都应该被删除。相反，初始数组中的`4`和`8`是重复的。那是什么？\n\n![](img/31fad71f-4671-4aae-8626-ff3f3785f7d1.png)\n\n让我们只看一下所有的项目，它们都在这个范围内，并且从图中的`begin`迭代器到`new_end`迭代器。`new_end`迭代器指向的项目是超出范围的第一个项目*，因此不包括在内。只关注那个区域(这些只是从`1`到包含`8`的项目)，我们意识到*这个*是*正确的*范围，从这个范围中`2`的所有值都被去掉了。*\n\n这就是`erase`调用起作用的地方:我们必须告诉向量，它不再认为从`new_end`到`end`的所有项目都是向量的项目。对于向量来说，这个顺序很容易理解，因为它只需将其`end`迭代器指向`new_end`的位置，就完成了。注意`new_end`是`std::remove`调用的返回值，所以我们可以直接使用它。\n\nNote that the vector did more magic than just moving an internal pointer. If that vector was a vector of more complicated objects, it would have called all the destructors of the to-be-removed items.\n\n之后，向量看起来像图表的第 3 步:现在它被认为*更小*。现在不在范围内的旧物品*还在记忆中*。\n\n为了让向量只占用它需要的内存，我们最后进行`shrink_to_fit`调用。在调用过程中，它会根据需要分配尽可能多的内存，移动所有项目，并删除我们不再需要的更大的块。\n\n在第 8 步中，我们定义了一个*谓词*函数，并且仅在一步中将其与`std::remove_if`一起使用。这是可行的，因为无论 remove 函数返回什么迭代器，在向量的 erase 函数中使用都是安全的。即使*没有找到奇数项*，函数也只做*没有做*，返回`end`迭代器。那么，像`v.erase(end, end);`这样的呼叫也没有任何作用，因此它是无害的。\n\n# 还有更多...\n\n`std::remove`功能也适用于其他容器。与`std::array`一起使用时，注意不支持调用`erase`的第二步，因为它们没有自动尺寸处理。仅仅因为`std::remove`有效地只移动项目，不执行它们的实际删除，它也可以用在不支持调整大小的数组等数据结构上。在数组的情况下，可以用哨兵值覆盖新结束迭代器的值，例如字符串的`''`。\n\n# 在 O(1)时间内从未排序的标准::向量中删除项目\n\n在`std::vector`中间的某个地方删除项目需要 *O(n)* 时间。这是因为移除一个项目产生的间隙必须通过将间隙后面的所有项目向左移动一个槽来填充。\n\n像这样移动物品，如果它们复杂和/或非常大并且包含许多物品，可能会很贵，但我们会保留它们的订单。如果保持顺序不重要，我们可以对此进行优化，如本节所示。\n\n# 怎么做...\n\n在本节中，我们将用一些示例数字填充一个`std::vector`实例，并实现一个快速移除函数，该函数在 *O(1)* 时间内移除向量中的任何项目。\n\n1.  首先，我们需要包含所需的头文件。\n\n```cpp\n      #include <iostream>\n      #include <vector>\n      #include <algorithm>\n```\n\n2.  然后，我们定义了一个主函数，在这个主函数中，我们用示例号来实例化一个向量。\n\n```cpp\n      int main()\n      {\n          std::vector<int> v {123, 456, 789, 100, 200};\n```\n\n3.  下一步是删除索引`2`处的值(当然是从零开始计数，所以是第三个数字`789`)。我们将用于该任务的功能尚未实现。我们稍后会做。然后，我们打印矢量的内容。\n\n```cpp\n          quick_remove_at(v, 2);\n\n          for (int i : v) {\n              std::cout << i << \", \";\n          }                                           \n          std::cout << 'n';\n```\n\n4.  现在，我们将删除另一个项目。它将是值`123`，假设我们不知道它的指数。因此，我们将使用`std::find`函数，该函数接受一个范围(向量)和一个值，然后搜索该值的位置。之后，它返回给我们一个指向`123`值的*迭代器*。我们将使用相同的`quick_remove_at`函数，但是这是一个*重载的*版本的*以前的*版本，它接受*迭代器*。也还没有实施。\n\n```cpp\n          quick_remove_at(v, std::find(std::begin(v), std::end(v), 123));\n\n          for (int i : v) {\n              std::cout << i << \", \";\n          }\n          std::cout << 'n';\n      }\n```\n\n5.  除了两个`quick_remove_at`功能，我们就完了。让我们实现这些。(请注意，它们至少应该在主函数之前声明。让我们在这里定义它们。)\n    这两个函数都接受对某个向量*的引用(在我们的例子中，是它的`int`值)，所以我们不知道用户会想出什么样的向量。对我们来说，它是`T`价值观的载体。我们使用的第一个`quick_remove_at`函数接受*索引*值，这些值是*数字*，所以界面如下图所示:*\n\n```cpp\n      template <typename T>\n      void quick_remove_at(std::vector<T> &v, std::size_t idx)\n      {\n```\n\n6.  现在来了食谱的肉-我们如何快速删除项目，而不会移动太多其他的？首先，我们简单地取向量中最后一项的值，并使用它来覆盖要删除的项。其次，我们切掉向量的最后一项。这是两个步骤。我们用一个小的健全性检查包围这个代码。如果索引值明显超出向量范围，我们什么也不做。否则，例如，代码会在空向量上崩溃。\n\n```cpp\n          if (idx < v.size()) {\n              v[idx] = std::move(v.back());\n              v.pop_back();\n          }\n      }\n```\n\n7.  `quick_remove_at`的另一个实现工作原理类似。它不接受数字索引，而是接受`std::vector<T>`的迭代器。以泛型方式获取其类型并不复杂，因为 STL 容器已经定义了这样的类型。\n\n```cpp\n      template <typename T>\n      void quick_remove_at(std::vector<T> &v, \n                           typename std::vector<T>::iterator it)\n      {\n\n```\n\n8.  现在，我们将访问迭代器指向的值。就像在另一个函数中一样，我们将用向量中的最后一个元素覆盖它。因为我们这次处理的不是数字索引，而是迭代器，所以如果迭代器的位置正常，我们需要稍微检查一下。如果它指向人工结束位置，我们不允许取消引用它。\n\n```cpp\n          if (it != std::end(v)) {\n```\n\n9.  在 if 块中，我们做了和以前一样的事情-我们用最后一个位置的项的值覆盖要移除的项-然后我们从向量中删除最后一个元素:\n\n```cpp\n              *it = std::move(v.back());\n              v.pop_back();\n          }\n      }\n```\n\n10.  就这样。编译和运行程序会产生以下输出:\n\n```cpp\n      $ ./main \n      123, 456, 200, 100,                           \n      100, 456, 200,\n```\n\n# 它是如何工作的...\n\n`quick_remove_at`功能可以快速移除物品，而不会接触太多其他物品。它以一种相对有创意的方式做到了这一点:它有点像*交换**实际项目，*，该项目应与矢量中的最后一个*项目一起移除。虽然最后一项与实际选择的项目没有*连接*，但它处于*特殊位置*:去掉最后一项就是*便宜*！向量的大小只需要缩小一个槽，就这样。在该步骤中不会移动任何项目。请看下图，它有助于想象这是如何发生的:*\n\n *![](img/91627e22-fdaf-41d9-a683-6c96f788f8b8.png)\n\n配方代码中的两个步骤如下所示:\n\n```cpp\nv.at(idx) = std::move(v.back());\nv.pop_back();\n```\n\n这是迭代器版本，看起来几乎相同:\n\n```cpp\n*it = std::move(v.back());\nv.pop_back();\n```\n\n逻辑上我们*交换*选中的项目和最后一个。但是代码不交换项目，它把最后一个移动到第一个上面。为什么呢？如果我们交换了项目，那么我们必须将所选项目存储在一个*临时*变量中，将最后一个项目移动到所选项目，然后将临时值再次存储在最后一个槽中。这似乎*没有用*，因为我们无论如何都要*删除*最后一项。\n\n好吧，好吧，所以交换是无用的，单向覆盖是更好的选择。看到这一点，我们可以认为这一步也可以用一个简单的`*it = v.back();`来完成，对吗？是的，这将是完全正确的*，但是想象一下我们在每个槽中存储了一些非常大的字符串，或者甚至是另一个向量或地图——在这种情况下，这个小赋值将导致非常昂贵的副本。中间的`std::move`调用只是一个*优化:*在*字符串*的示例中，字符串项内部指向*堆中的一个大字符串*。我们不需要复制它。相反，当*移动*一个字符串时，移动的目的地到达另一个的字符串数据的*点。移动源项目保持不变，但处于无用状态，这很好，因为我们无论如何都要移除它。**\n\n *# 以快速或安全的方式访问标准::矢量实例\n\n`std::vector`可能是 STL 中使用最广泛的容器，因为它像数组一样保存数据，并围绕这种表示增加了很多舒适性。然而，错误地访问向量仍然是危险的。如果一个向量包含 100 个元素，而我们的代码偶然试图访问索引 123 处的一个元素，这显然是不好的。这样的程序可能会崩溃，这可能是最好的情况，因为这种行为会使它非常明显地有一个 bug！如果它没有崩溃，我们可能会观察到程序只是时不时地表现得*奇怪*，这可能会导致比崩溃程序更令人头疼的问题。有经验的程序员可能会在任何直接索引向量访问之前添加一些检查。这样的检查并没有增加代码的可读性，很多人并不知道`std::vector`已经内置了绑定检查！\n\n# 怎么做...\n\n在本节中，我们将使用两种不同的方式来访问一个`std::vector`，然后看看我们如何利用它们来编写更安全的程序，而不降低可读性。\n\n1.  让我们包括所有需要的头文件，并用`1000`乘以值`123`填充一个示例向量，这样我们就可以访问一些东西:\n\n```cpp\n      #include <iostream>\n      #include <vector>\n\n      using namespace std;\n\n      int main()\n      {\n          const size_t container_size {1000};\n          vector<int> v (container_size, 123);\n```\n\n2.  现在，我们使用`[]`运算符访问超出边界的向量:\n\n```cpp\n         cout << \"Out of range element value: \" \n              << v[container_size + 10] << 'n';\n```\n\n3.  接下来，我们使用`at`函数访问它:\n\n```cpp\n          cout << \"Out of range element value: \" \n               << v.at(container_size + 10) << 'n';\n      }\n```\n\n4.  让我们运行程序，看看会发生什么。该错误消息是特定于 GCC 的。其他编译器会发出不同但相似的错误消息。第一次阅读以一种奇怪的方式成功了。它不会导致程序崩溃，但它是一个与`123`完全不同的*值*。我们看不到另一个访问的输出行，因为它有目的地崩溃了整个程序。如果那个出界通道是个意外，我们会更早发现它！\n\n```cpp\n      Out of range element value: -726629391\n      terminate called after throwing an instance of 'std::out_of_range'\n        what():  array::at: __n (which is 1010) >= _Nm (which is 1000)\n      Aborted (core dumped)\n```\n\n# 它是如何工作的...\n\n`std::vector`提供`[]`操作符和`at`功能，它们基本上做完全一样的工作。然而，`at`函数执行额外的边界检查，如果超过向量边界，则抛出*异常*。这在像我们这样的情况下非常有用，但也会让程序慢一点*。*\n\n *特别是在用索引成员做数值计算时，需要非常快，坚持`[]`索引访问是有利的。在任何其他情况下，`at`功能有助于发现通常性能损失可以忽略的缺陷。\n\nIt is good practice to use the `at` function by default. If the resulting code is too slow but has proven to be bug-free, the `[]` operator can be used in performance-sensitive sections instead.\n\n# 还有更多...\n\n当然，我们可以*处理*出界访问，而不是让整个应用*崩溃*。为了处理，我们*捕捉*异常，以防被`at`函数抛出。捕捉这样的异常很简单。我们只是用`try`块包围`at`调用，并在`catch`块中定义错误处理。\n\n```cpp\ntry {\n    std::cout << \"Out of range element value: \" \n              << v.at(container_size + 10) << 'n';\n} catch (const std::out_of_range &e) {\n     std::cout << \"Ooops, out of range access detected: \" \n               << e.what() << 'n';\n}\n```\n\nBy the way, `std::array` also provides an `at` function.\n\n# 保持标准::矢量实例排序\n\n数组和向量本身不会对它们的有效载荷对象进行排序。但是，如果我们需要，这并不意味着我们总是必须切换到数据结构，这是自动设计的。如果一个`std::vector`对于我们的用例来说是完美的，那么以*排序方式*向它添加项目仍然是非常简单和实用的。\n\n# 怎么做...\n\n在本节中，我们将使用随机单词填充一个`std::vector`，对其进行排序，然后插入更多的单词，同时保持向量的排序单词顺序不变。\n\n1.  让我们首先包括我们需要的所有标题。\n\n```cpp\n      #include <iostream>\n      #include <vector>\n      #include <string>\n      #include <algorithm>\n      #include <iterator> \n      #include <cassert>\n```\n\n2.  我们还声明，我们使用名称空间`std`是为了节省一些`std::`前缀:\n\n```cpp\n      using namespace std;\n```\n\n3.  然后我们写一个小的主函数，用一些随机字符串填充一个向量。\n\n```cpp\n      int main()\n      {\n          vector<string> v {\"some\", \"random\", \"words\", \n                            \"without\", \"order\", \"aaa\", \n                            \"yyy\"};\n```\n\n4.  接下来我们要做的是*排序*那个向量。让我们用一些断言和之前 STL 中的`is_sorted`函数来做，这表明向量实际上是*而不是之前排序的*，而是之后排序的*。*\n\n```cpp\n          assert(false == is_sorted(begin(v), end(v)));\n          sort(begin(v), end(v));\n          assert(true == is_sorted(begin(v), end(v)));\n```\n\n5.  现在，我们终于使用一个新的`insert_sorted`函数将一些随机单词添加到排序后的向量中，之后我们还需要实现这个函数。这些单词应该放在正确的位置，这样之后向量仍然会被排序:\n\n```cpp\n          insert_sorted(v, \"foobar\");\n          insert_sorted(v, \"zzz\");\n```\n\n6.  所以，现在让我们在源文件中稍微早一点实现`insert_sorted`。\n\n```cpp\n      void insert_sorted(vector<string> &v, const string &word)\n      {\n          const auto insert_pos (lower_bound(begin(v), end(v), word));\n          v.insert(insert_pos, word);\n      }\n```\n\n7.  现在，回到我们停止的主函数，我们现在可以继续打印向量，并看到插入过程工作正常:\n\n```cpp\n          for (const auto &w : v) { \n              cout << w << \" \";\n          }\n          cout << 'n';\n      }\n```\n\n8.  编译和运行该程序会产生以下排序良好的输出:\n\n```cpp\n      aaa foobar order random some without words yyy zzz\n```\n\n# 它是如何工作的...\n\n整个程序是围绕`insert_sorted`函数构建的，该函数完成这一部分的内容:对于任何新的字符串，它都定位在排序向量中必须插入的位置，以便*保持*向量中字符串的顺序。然而，我们假设向量之前被排序过。否则，这是行不通的。\n\n定位步骤由 STL 函数`lower_bound`完成，该函数接受三个参数。前两个表示基础范围的*开始*和*结束*。在这种情况下，范围是我们的单词向量。第三个参数是单词，应该插入。然后，该函数找到该范围内的第一个项目，该项目大于或等于第三个参数的*并返回指向它的迭代器。*\n\n手头有了合适的位置，我们把它交给了`std::vector`成员方法`insert`，它只接受两个参数。第一个参数是迭代器，它指向向量中应该插入第二个参数的位置。我们可以使用同样的迭代器，这看起来非常方便，它刚刚退出了`lower_bound`函数。第二个参数当然是要插入的项目。\n\n# 还有更多...\n\n`insert_sorted`功能相当通用。如果我们泛化其参数的类型，它也会作用于其他容器的有效载荷类型，甚至作用于其他容器，如`std::set`、`std::deque`、`std::list`等等！(请注意，set 有自己的`lower_bound`成员函数，其功能与`std::lower_bound`相同，但效率更高，因为它专门用于 set。)\n\n```cpp\ntemplate <typename C, typename T>\nvoid insert_sorted(C &v, const T &item)\n{\n    const auto insert_pos (lower_bound(begin(v), end(v), item));\n    v.insert(insert_pos, item);\n}\n```\n\n当试图将配方中的向量类型从`std::vector`切换到其他类型时，请注意并非所有容器都支持`std::sort`。该算法需要随机访问容器，例如`std::list`没有实现。\n\n# 高效且有条件地将项目插入标准::映射\n\n有时，我们希望用键值对填充地图，在填充地图时，我们可能会遇到两种不同的情况:\n\n1.  密钥尚不存在。创建一个新的键值对。\n2.  密钥已经存在。取*已有*项，*修改*项。\n\n我们可以天真地使用`map`的`insert`或`emplace`方法，看看它们是否成功。如果没有，我们有案例 2 并修改现有项目。在这两种情况下，插入并放置创建我们尝试插入的项目，在第二种情况下，新创建的项目被丢弃。在这两种情况下，我们都会得到一个无用的构造函数调用。\n\n从 C++ 17 开始，就有了`try_emplace`函数，它使我们只能在插入时有条件地创建项目。让我们实现一个程序，获取亿万富翁的名单，并构建一个地图，告诉我们每个国家的亿万富翁人数。除此之外，它还储存了每个国家最富有的人。我们的例子不会包含昂贵的创建项目，但是每当我们在现实项目中发现自己处于这样的情况时，我们知道如何用`try_emplace`来掌握它。\n\n# 怎么做...\n\n在这一部分，我们将实现一个应用，从亿万富翁的名单中创建一个地图。这张地图从每个国家映射到该国最富有的人的参考，以及一个显示该国有多少亿万富翁的计数器。\n\n1.  像往常一样，我们需要首先包含一些头，并且我们声明我们默认使用名称空间`std`。\n\n```cpp\n      #include <iostream>\n      #include <functional>\n      #include <list>\n      #include <map>\n\n      using namespace std;\n```\n\n2.  让我们为我们的列表定义一个代表亿万富翁项目的结构。\n\n```cpp\n      struct billionaire {\n          string name;\n          double dollars;\n          string country;\n      };\n```\n\n3.  在主功能中，我们首先定义亿万富翁的名单。世界上有许多亿万富翁，所以让我们用一些国家的一些最富有的人来构建一个有限的名单。此列表已经订购。这个排名实际上是从位于 https://www.forbes.com/billionaires/list/: T4 的 2017 年福布斯全球亿万富豪榜中获得的\n\n```cpp\n      int main()\n      {\n          list<billionaire> billionaires {\n              {\"Bill Gates\", 86.0, \"USA\"},\n              {\"Warren Buffet\", 75.6, \"USA\"},\n              {\"Jeff Bezos\", 72.8, \"USA\"},\n              {\"Amancio Ortega\", 71.3, \"Spain\"},\n              {\"Mark Zuckerberg\", 56.0, \"USA\"},\n              {\"Carlos Slim\", 54.5, \"Mexico\"},\n              // ...\n              {\"Bernard Arnault\", 41.5, \"France\"},\n              // ...\n              {\"Liliane Bettencourt\", 39.5, \"France\"},\n              // ...\n              {\"Wang Jianlin\", 31.3, \"China\"},\n              {\"Li Ka-shing\", 31.2, \"Hong Kong\"}\n              // ...\n          };\n```\n\n4.  现在，让我们定义地图。它从国家字符串映射到一对。这对组合包含了我们名单中每个国家第一个亿万富翁的(`const`)副本。这无疑是每个国家最富有的亿万富翁。这两个变量中的另一个变量是一个计数器，我们将为该国的每一个亿万富翁增加这个计数器。\n\n```cpp\n          map<string, pair<const billionaire, size_t>> m;\n```\n\n5.  现在，让我们浏览一下列表，并尝试为每个国家放置一对新的有效载荷。这两个包含一个我们目前正在关注的亿万富翁的参考和一个计数器值`1`。\n\n```cpp\n          for (const auto &b : billionaires) {\n              auto [iterator, success] = m.try_emplace(b.country, b, 1);\n```\n\n6.  如果这一步成功了，那么我们就不需要做其他事情了。我们为其提供构造函数参数`b, 1`的对已经被构造并插入到地图中。如果插入不成功，因为国家/地区密钥已经存在，则该对没有被构造。如果我们的亿万富翁结构非常大，这将为我们节省复制它的运行时成本。\n    但是，在不成功的情况下，我们仍然需要为这个国家增加计数器。\n\n```cpp\n              if (!success) {\n                  iterator->second.second += 1;\n              }\n          }\n```\n\n7.  好了，就这样。我们现在可以打印每个国家有多少亿万富翁，每个国家谁最富有。\n\n```cpp\n          for (const auto & [key, value] : m) {\n              const auto &[b, count] = value;\n\n              cout << b.country << \" : \" << count \n                   << \" billionaires. Richest is \"\n                   << b.name << \" with \" << b.dollars \n                   << \" B$n\";\n          }\n      }\n```\n\n8.  编译并运行程序会产生以下输出。(当然，输出是有限的，因为我们限制了我们的输入图。)\n\n```cpp\n      $ ./efficient_insert_or_modify\n      China : 1 billionaires. Richest is Wang Jianlin with 31.3 B$\n      France : 2 billionaires. Richest is Bernard Arnault with 41.5 B$\n      Hong Kong : 1 billionaires. Richest is Li Ka-shing with 31.2 B$\n      Mexico : 1 billionaires. Richest is Carlos Slim with 54.5 B$\n      Spain : 1 billionaires. Richest is Amancio Ortega with 71.3 B$\n      USA : 4 billionaires. Richest is Bill Gates with 86 B$\n```\n\n# 它是如何工作的...\n\n整个食谱围绕`std::map`的`try_emplace`功能展开，这是一个新的 C++ 17 加法。它有以下签名:\n\n```cpp\nstd::pair<iterator, bool> try_emplace(const key_type& k, Args&&... args);\n```\n\n因此，被插入的键是参数`k`，并且相关联的值是从参数包`args`构造的。如果我们成功地插入了项目，那么函数返回一个*迭代器*，它指向地图中的新节点，*将*与一个设置为`true`的布尔值配对。如果插入是*而不是*成功的，返回对中的布尔值被设置为`false`，迭代器指向新项目将与之冲突的项目。\n\n这个特征在我们的案例中非常有用——当我们第一次看到一个来自特定国家的亿万富翁时，这个国家还不是地图上的关键。在这种情况下，我们必须将*插入*，同时将一个新的计数器设置为`1`。如果我们*已经看到了一个来自特定国家的亿万富翁，我们必须获得其现有计数器的参考，以便增加它。这正是第 6 步中发生的情况:*\n\n```cpp\nif (!success) {\n    iterator->second.second += 1;\n}\n```\n\nNote that both the `insert` and `emplace` functions of `std::map` work exactly the same way. A crucial difference is that `try_emplace` will *not* construct the object associated with the key if the key already exists. This boosts the performance in case objects of that type are expensive to create.\n\n# 还有更多...\n\n如果我们将地图的类型从`std::map`切换到`std::unordered_map`，整个程序仍然有效。这样，我们可以简单地从一种实现切换到另一种实现，这种实现具有不同的性能特征。在这个食谱中，唯一可以观察到的区别是亿万富翁地图不再按字母顺序打印，因为哈希地图不会像搜索树那样对对象进行排序。\n\n# 了解 std::map::insert 的新插入提示语义\n\n在`std::map`中查找项目需要 *O(log(n))* 时间。这对于插入新项目是一样的，因为必须查找插入它们的位置。因此，天真地插入 *M* 新项目需要 *O(M * log(n))* 时间。\n\n为了提高效率，`std::map`插入函数接受一个可选的*插入提示*参数。插入提示基本上是一个迭代器，它指向要插入的项的未来位置附近。如果提示正确，那么我们得到*摊销*T5】O(1)插入时间。\n\n# 怎么做...\n\n在本节中，我们将在一个`std::map`中插入多个项目，并为此使用插入提示，以减少查找次数。\n\n1.  我们正在将字符串映射到数字，因此我们需要包含用于`std::map`和`std::string`的头文件。\n\n```cpp\n      #include <iostream>\n      #include <map>\n      #include <string>\n```\n\n2.  下一步是实例化一个已经包含一些示例字符的地图。\n\n```cpp\n      int main()\n      {\n          std::map<std::string, size_t> m {{\"b\", 1}, {\"c\", 2}, {\"d\", 3}};\n```\n\n3.  我们现在将插入多个项目，并且对于每个项目，我们将使用插入提示。由于我们一开始没有提示，我们将只做第一次插入，指向地图的`end`迭代器。\n\n```cpp\n          auto insert_it (std::end(m));\n```\n\n4.  我们现在将从字母表中向后插入项目，同时始终使用我们拥有的迭代器提示，然后将其重新初始化为`insert`函数的返回值。下一项将被插入到提示的前面。\n\n```cpp\n          for (const auto &s : {\"z\", \"y\", \"x\", \"w\"}) {\n              insert_it = m.insert(insert_it, {s, 1});\n          }\n```\n\n5.  为了展示它是如何*而不是*完成的，我们插入了一个字符串，它将被放在地图最左边的位置，但是给它一个完全*错误的*提示，它指向地图最右边的位置-`end`。\n\n```cpp\n          m.insert(std::end(m), {\"a\", 1});\n```\n\n6.  最后，我们只是打印我们所拥有的。\n\n```cpp\n          for (const auto & [key, value] : m) {\n              std::cout << \"\"\" << key << \"\": \" << value << \", \";\n          }\n          std::cout << 'n';\n      }\n```\n\n7.  这是我们编译和运行程序时得到的输出。显然，错误的插入提示并没有造成太大的伤害，因为地图排序仍然是正确的。\n\n```cpp\n      \"a\": 1, \"b\": 1, \"c\": 2, \"d\": 3, \"w\": 1, \"x\": 1, \"y\": 1, \"z\": 1,\n```\n\n# 它是如何工作的...\n\n在这个方法中，与普通映射插入的唯一区别是额外的提示迭代器。我们谈到了*正确*和*错误*的提示。\n\n一个*正确的*提示将指向一个现有的元素，该元素比要插入的元素大*，这样新插入的键将正好在提示之前*。如果这不适用于用户在插入过程中提供的提示，插入功能将返回到非优化插入，再次产生 *O(log(n))* 性能。**\n\n *对于第一次插入，我们得到了地图的`end`迭代器，因为我们没有更好的开始提示。在树中安装了一个“z”之后，我们知道安装“y”将在“z”的前面插入一个新项目，这使它有资格成为一个正确的提示。这也适用于“x”，如果在插入“y”后放入树中，以此类推。这就是为什么可以使用迭代器的原因，迭代器是由最后一个*插入的*返回给下一个*插入的*的。\n\nIt is important to know, that before C++ 11, insertion hints were considered correct when they pointed *before* the position of the newly inserted item.\n\n# 还有更多...\n\n有趣的是，一个错误的提示甚至不会破坏或打乱地图中项目的顺序，那么这到底是怎么回事，插入时间摊销 *O(1)* 是什么意思？\n\n`std::map`通常使用二叉查找树实现。将新关键字插入搜索树时，将从顶部开始与其他节点的关键字进行比较。如果键小于或大于一个节点的键，则搜索算法向左或向右分支，向下到下一个更深的节点。在这样做的同时，搜索算法将在到达当前树的最大深度的点停止，在该点它将放置带有其关键字的新节点。有可能这一步破坏了树的平衡，所以它也将纠正这一点，随后使用重新平衡算法作为内务处理任务。\n\n当我们用彼此直接相邻的键值将项目插入到树中时(就像整数`1`是整数`2`的邻居一样，因为没有其他整数适合它们之间)，它们也可以*经常*被插入到树中彼此相邻的位置。对于某个键和伴随的提示，很容易检查这是否为真。如果这种情况适用，搜索算法步骤可以省略，这样可以节省一些关键的运行时间。之后，重新平衡算法可能仍然必须运行。\n\n当这样的优化可以*经常*完成，但不能*总是*完成时，这仍然可以导致*平均*性能增益。有可能显示一个*导致的*运行时复杂性，它在多次*插入后稳定下来，然后它被称为**分摊复杂性**。*\n\n![](img/ba7cd62b-4541-4793-9475-24b490c8929b.png)\n\n如果插入提示错误，插入功能将简单地*放弃*提示，并使用搜索算法重新开始。这是正确的，但是显然*比*慢。\n\n# 有效修改标准::地图项目的关键字\n\n由于`std::map`数据结构从键映射到值的方式是键总是唯一的和排序的，因此用户不能修改已经插入的映射节点的键是至关重要的。为了防止用户修改完全排序的地图节点的关键项目，在关键类型中添加了`const`限定符。\n\n这种限制是完全合理的，因为它使用户更难错误地使用`std::map`。但是，如果我们真的需要更改一些地图项目的关键点，我们该怎么办？\n\n在 C++ 17 之前，我们必须从树中移除需要更改键值的项目，以便重新插入它们。这种方法的缺点是总是不必要地重新分配一些内存，这在性能方面听起来很糟糕。\n\n从 C++ 17 开始，我们可以移除并重新插入地图节点*而不需要*任何内存的重新分配。我们将在这个食谱中看到它是如何工作的。\n\n# 怎么做...\n\n我们实现了一个小应用，它在一个虚构的比赛中以一种`std::map`结构安排车手的位置。当车手在比赛中互相超越时，我们需要改变他们的位置键，这就是我们新的 C++ 17 方式。\n\n1.  让我们首先包含必要的头，并声明我们使用名称空间`std`。\n\n```cpp\n      #include <iostream>\n      #include <map>      \n\n      using namespace std;\n```\n\n2.  我们将打印操纵地图结构前后的比赛位置，所以让我们实现一个小助手函数。\n\n```cpp\n      template <typename M>\n      void print(const M &m)\n      {\n          cout << \"Race placement:n\";\n          for (const auto &[placement, driver] : m) {\n              cout << placement << \": \" << driver << 'n';\n          }\n      }\n```\n\n3.  在主函数中，我们实例化并初始化一个映射，该映射从表示驱动程序位置的整数值映射到包含驱动程序名称的字符串。我们也打印地图，因为我们将在接下来的步骤中修改它。\n\n```cpp\n      int main()\n      {\n          map<int, string> race_placement {\n              {1, \"Mario\"}, {2, \"Luigi\"}, {3, \"Bowser\"},\n              {4, \"Peach\"}, {5, \"Yoshi\"}, {6, \"Koopa\"},\n              {7, \"Toad\"}, {8, \"Donkey Kong Jr.\"}\n          };\n\n          print(race_placement);\n```\n\n4.  假设在一圈比赛中，布瑟出了点小事故，掉到了最后一名，小毛驴孔趁机从最后一名跳到了第三名。在这种情况下，我们首先需要从地图中提取它们的地图节点，因为这是操纵它们的键的唯一方法。`extract`函数是 C++ 17 的一个新特性。它从地图中移除物品，没有任何与分配相关的副作用。让我们也为这个任务打开一个新的范围。\n\n```cpp\n          {\n              auto a (race_placement.extract(3));\n              auto b (race_placement.extract(8));\n```\n\n5.  现在我们可以交换布瑟和小毛驴孔的钥匙了。虽然地图节点的键通常是不可变的，因为它们被声明为`const`，但是我们可以修改我们使用`extract`方法提取的项目的键。\n\n```cpp\n              swap(a.key(), b.key());\n```\n\n6.  `std::map`的`insert`方法在 C++ 17 中获得了一个新的重载，它接受提取节点的句柄，以便在不接触分配器的情况下插入它们。\n\n```cpp\n              race_placement.insert(move(a));\n              race_placement.insert(move(b));\n          }\n```\n\n7.  离开瞄准镜后，我们就完事了。我们打印新的比赛位置，让应用终止。\n\n```cpp\n          print(race_placement);\n      }\n```\n\n8.  编译并运行程序会产生以下输出。我们首先在新地图实例中看到种族位置，然后在交换布瑟和小孔驴的位置后再次看到它。\n\n```cpp\n      $ ./mapnode_key_modification \n      Race placement:\n      1: Mario\n      2: Luigi\n      3: Bowser\n      4: Peach\n      5: Yoshi\n      6: Koopa\n      7: Toad\n      8: Donkey Kong Jr.\n      Race placement:\n      1: Mario\n      2: Luigi\n      3: Donkey Kong Jr.\n      4: Peach\n      5: Yoshi\n      6: Koopa\n      7: Toad\n      8: Bowser\n```\n\n# 它是如何工作的...\n\n在 C++ 17 中，`std::map`得到了一个新的成员函数抽取。它有两种口味:\n\n```cpp\nnode_type extract(const_iterator position);\nnode_type extract(const key_type& x);\n```\n\n在这个配方中，我们使用了第二个，它接受一个键，然后找到并提取与该键参数匹配的映射节点。第一个接受迭代器，这意味着它比快*，因为它不需要搜索项目。*\n\n如果我们试图用第二种方法(使用键搜索的方法)提取一个不存在的项目，它会返回一个空的*`node_type`实例。`empty()`成员方法返回一个布尔值，告诉我们一个`node_type`实例是否为空。在空实例上访问任何其他方法都会导致未定义的行为。*\n\n *提取节点后，我们能够使用`key()`方法修改它们的密钥，这给了我们对密钥的非托管访问，尽管密钥通常是常量。\n\n请注意，为了再次将节点重新插入到地图中，我们必须将移动到`insert`功能中。这是有意义的，因为`extract`是为了避免不必要的复制和分配。请注意，当我们移动`node_type`实例时，这不会导致任何容器值的实际移动。\n\n# 还有更多...\n\n使用提取方法提取的地图节点实际上非常通用。我们可以从一个`map`实例中提取节点，并将其插入到任何其他`map`甚至`multimap`实例中。它也在`unordered_map`和`unordered_multimap`实例之间以及`set` / `multiset`和各自的`unordered_set` / `unordered_multiset`中工作。\n\n为了在不同的映射/集合结构之间移动项目，键、值和分配器的类型需要相同。请注意，即使是这种情况，我们也不能将节点从`map`移动到`unordered_map`，或者从`set`移动到`unordered_set`。\n\n# 将标准::无序 _ 映射用于自定义类型\n\n如果我们使用`std::unordered_map`而不是`std::map`，我们在选择应该使用的按键类型时有不同的自由度。`std::map`要求所有关键项目之间有一个自然的顺序。这样，可以对项目进行排序。但是，如果我们想要，例如，数学向量作为一个关键类型呢？对于此类类型，在较小的*`<`关系中没有*的含义，因为矢量`(0, 1)`并不比`(1, 0)`小*或大*。它们只是指向不同的方向。这对`std::unordered_map`来说完全没问题，因为它不会通过项目的较小/较大排序关系，而是通过*哈希值*来区分项目。我们唯一需要做的就是为自己的类型实现一个*散列函数*，以及一个等于* `==`运算符实现的*，它告诉两个对象是否相同。本节将在一个示例中演示这一点。****\n\n *# 怎么做...\n\n在本节中，我们将定义一个简单的`coord`结构，它没有*默认的*哈希函数，所以我们需要自己定义它。然后我们通过将`coord`值映射到数字来使用它。\n\n1.  我们首先包括打印和使用`std::unordered_map`所需的内容。\n\n```cpp\n      #include <iostream>\n      #include <unordered_map>\n```\n\n2.  然后我们定义自己的自定义结构，它不会被现有的散列函数轻易散列:\n\n```cpp\n      struct coord {\n          int x;\n          int y;\n      };\n```\n\n3.  我们不仅需要一个哈希函数来将该结构用作哈希映射的关键字，还需要一个比较运算符实现:\n\n```cpp\n      bool operator==(const coord &l, const coord &r)\n      {\n          return l.x == r.x && l.y == r.y;\n      }\n```\n\n4.  为了扩展 STL 自身的散列能力，我们将打开`std`命名空间并创建我们自己的`std::hash`模板结构专门化。它包含与其他哈希专门化相同的`using`类型别名子句。\n\n```cpp\n      namespace std\n      {\n\n      template <>\n      struct hash<coord>\n      {\n          using argument_type = coord;\n          using result_type   = size_t;\n```\n\n5.  这个`struct`的肉就是`operator()`的定义。我们只是在添加`struct coord`的数字成员值，这是一种糟糕的散列技术，但是为了展示如何实现它，它已经足够好了。一个好的散列函数试图在整个值范围内尽可能均匀地分布值，以减少*散列冲突*的数量。\n\n```cpp\n          result_type operator()(const argument_type &c) const\n          {\n              return static_cast<result_type>(c.x) \n                   + static_cast<result_type>(c.y);\n          }\n      };\n\n      }\n```\n\n6.  我们现在可以实例化一个新的`std::unordered_map`实例，它接受`struct coord`实例作为关键字，并将其映射到任意值。因为这个食谱是关于为`std::unordered_map`启用我们自己的类型，这已经差不多了。让我们用自己的类型实例化一个基于散列的地图，用一些项目填充它，并打印它的:\n\n```cpp\n      int main()\n      {\n\n          std::unordered_map<coord, int> m {{{0, 0}, 1}, {{0, 1}, 2}, \n                                            {{2, 1}, 3}};\n\n          for (const auto & [key, value] : m) {\n              std::cout << \"{(\" << key.x << \", \" << key.y \n                        << \"): \" << value << \"} \";\n          }\n          std::cout << 'n';\n      }\n```\n\n7.  编译和运行程序会产生以下输出:\n\n```cpp\n      $ ./custom_type_unordered_map\n      {(2, 1): 3} {(0, 1): 2} {(0, 0): 1}\n```\n\n# 它是如何工作的...\n\n通常，当我们实例化类似`std::unordered_map`的基于散列的映射实现时，我们会写道:\n\n```cpp\nstd::unordered_map<key_type, value_type> my_unordered_map;\n```\n\n编译器创建我们的`std::unordered_map`特殊化时，后台会发生很多神奇的事情，这一点不太明显。那么，让我们来看看它的完整模板类型定义:\n\n```cpp\ntemplate<\n    class Key,\n    class T,\n    class Hash      = std::hash<Key>,\n    class KeyEqual  = std::equal_to<Key>,\n    class Allocator = std::allocator< std::pair<const Key, T> >\n> class unordered_map;\n```\n\n前两种模板类型是我们用`coord`和`int`填充的，这是简单明了的部分。其他三种模板类型是可选的，因为它们会自动填充现有的标准模板类，这些模板类本身采用模板类型。我们选择前两个参数作为默认值。\n\n关于这个配方，`class Hash`模板参数是一个有趣的参数:当我们没有明确定义其他任何东西时，它将在`std::hash<key_type>`上专门化。STL 已经包含了许多类型的`std::hash`专门化，例如`std::hash<std::string>`、`std::hash<int>`、`std::hash<unique_ptr>`等等。这些类知道如何处理这样的特定类型，以便从中计算最佳哈希值。\n\n然而，STL 还不知道如何从我们的`struct coord`计算哈希值。所以我们所做的只是定义*另一个*专业化，它知道如何处理它。编译器现在可以浏览它知道的所有`std::hash`专门化的列表，并将找到我们的实现，使其与我们作为键类型提供的类型相匹配。\n\n如果我们没有添加一个新的`std::hash<coord>`专门化，而是将其命名为`my_hash_type`，我们仍然可以在下面的实例化行中使用它:\n\n```cpp\nstd::unordered_map<coord, value_type, my_hash_type> my_unordered_map;\n```\n\n这显然更容易键入，而且不如编译器自己找到正确的哈希实现时读起来好。\n\n# 从用户输入中过滤重复项，并使用 std::set 按字母顺序打印它们\n\n`std::set`是一个奇怪的容器:它的工作原理有点像`std::map`，但它只包含键作为值，没有键值对。因此，它很难用作将一种类型的值映射到另一种类型的方法。看似，只是因为它的用例不太明显，很多开发者甚至不知道它的存在。然后他们开始自己实施事情，虽然`std::set`在这些情况下会有很大的帮助。\n\n本节展示了如何将`std::set`用于一个示例中，在该示例中，我们收集了许多潜在的不同项目，以便*过滤*它们并输出一组*独特的*项目。\n\n# 怎么做...\n\n在本节中，我们将从标准输入中读取一串单词。所有*独特的*单词被放入一个`std::set`实例中。这样，我们就可以从流中枚举所有唯一的单词。\n\n1.  我们将使用几种不同的 STL 类型，为此我们需要包含多个头。\n\n```cpp\n      #include <iostream>\n      #include <set>\n      #include <string>\n      #include <iterator>\n```\n\n2.  为了节省一些打字时间，我们将声明我们正在使用命名空间`std`:\n\n```cpp\n      using namespace std;\n```\n\n3.  现在我们已经在编写实际的程序，它从`main`函数实例化一个存储字符串的`std::set`开始。\n\n```cpp\n      int main()\n      {\n          set<string> s;\n```\n\n4.  接下来要做的是获取用户输入。我们只是从标准输入中读取，并使用方便的`istream_iterator`来完成。\n\n```cpp\n          istream_iterator<string> it {cin};\n          istream_iterator<string> end;\n```\n\n5.  有了一对代表用户输入的`begin`和`end`迭代器，我们就可以用一个`std::inserter`来填充这个集合。\n\n```cpp\n          copy(it, end, inserter(s, s.end()));\n```\n\n6.  已经这样了。为了看看我们从标准输入中得到什么*独特的*单词，我们只打印我们的集合的内容。\n\n```cpp\n          for (const auto word : s) {\n              cout << word << \", \";\n          }\n          cout << 'n';\n      }\n```\n\n7.  让我们用下面的输入来编译和运行我们的程序。对于前面的输入，我们得到了下面的输出，其中所有的重复项都被剔除，唯一的单词按字母顺序排序。\n\n```cpp\n      $ echo \"a a a b c foo bar foobar foo bar bar\" | ./program\n      a, b, bar, c, foo, foobar,\n```\n\n# 它是如何工作的...\n\n这个节目由两个有趣的部分组成。第一部分是使用`std::istream_iterator`访问用户输入，第二部分是使用`std::copy`算法将其与我们的`std::set`实例结合，之后我们将其包装成一个`std::inserter`实例！可能看起来令人惊讶的是，只有一行代码完成了所有的工作*标记*输入，*将*放入按字母顺序排序的*集合中，*删除*所有重复项。*\n\n# std::istream_iterator\n\n当我们想从一个流中处理大量相同的*类型的数据时，这个类真的很有趣，这正是这个食谱中的情况:我们逐字解析整个输入，并以`std::string`实例的形式将其放入集合中。*\n\n *`std::istream_iterator`取一个模板参数。这就是我们想要的输入类型。我们选择`std::string`是因为我们假设文本单词，但是它也可能是`float`数字，例如。基本上可以是每一种可以写`cin >> var;`的类型。构造函数接受一个`istream`实例。标准输入由全局输入流对象`std::cin`表示，在这种情况下，这是一个可接受的`istream`参数。\n\n```cpp\nistream_iterator<string> it {cin};\n```\n\n我们已经实例化的输入流迭代器`it`能够做两件事:当它被解引用时(`*it`)，它产生当前的输入符号。当我们通过其模板参数向`std::string`键入迭代器时，该符号将是一个包含一个单词的字符串。当它递增(`++ it`)时，它将跳转到下一个单词，我们可以通过再次取消引用来访问它。\n\n但是等等，在我们再次取消引用它之前，我们需要在每次增量之后小心。如果标准输入为空*，迭代器必须*而不是*再次被取消引用。相反，我们应该终止循环，在循环中，我们取消引用迭代器来获取每个单词。中止条件，让我们知道迭代器变得无效，是与`end`迭代器的比较。如果`it == end`成立，我们就过了输入的终点。*\n\n *我们通过创建一个带有无参数标准构造函数的`std::istream_iterator`实例来创建结束迭代器。它的目的是作为比较的对应物，在每次迭代中作为中止条件:\n\n```cpp\nistream_iterator<string> end;\n```\n\n一旦`std::cin`空了，我们的`it`迭代器实例就会*注意到*并与`end`返回的`true`进行比较。\n\n# 标准::插入器\n\n我们在`std::copy`调用中使用`it`和`end`对作为*输入*迭代器。第三个参数必须是*输出*迭代器。为此，我们不能只拿`s.begin()`或`s.end()`。在一个空集合中，两者是相同的，所以我们甚至不允许*去引用*它，不管那是为了从它那里读取还是分配给它。\n\n这就是`std::inserter`发挥作用的地方。这是一个返回`std::insert_iterator`的函数，它的行为类似于迭代器，但做的事情不同于通常的迭代器。当我们增加它时，它什么也不做。当我们取消引用它并给它赋值时，它会取它所连接的容器，*将该值作为*新的*项插入其中！*\n\n通过`std::inserter`实例化`std::insert_iterator`时，需要两个参数:\n\n```cpp\nauto insert_it = inserter(s, s.end());\n```\n\n`s`是我们的集合，`s.end()`是一个迭代器，指向新项目应该插入的位置。对于我们开始的一个空集合，这和`s.begin()`一样有意义。当用于向量或列表等其他数据结构时，第二个参数对于定义插入迭代器插入新项的位置至关重要。\n\n# 把它放在一起\n\n最终，*所有*的动作都发生在`std::copy`呼叫期间:\n\n```cpp\ncopy(input_iterator_begin, input_iterator_end, insert_iterator);\n```\n\n这个调用通过输入迭代器从`std::cin`中拉出下一个单词标记，并将其推入我们的`std::set`中。之后，它递增两个迭代器，并检查输入迭代器是否等于输入端迭代器。如果不是，那么标准输入中还剩下单词，所以会*重复*。\n\n重复的单词会自动删除。如果集合中已经包含了一个特定的单词，再次添加它没有效果。这在`std::multiset`中是不同的，相反，它将接受重复。\n\n# 用标准::堆栈实现一个简单的 RPN 计算器\n\n`std::stack`是一个适配器类，它允许用户将对象*推到*上，就像在一堆真实的对象上一样，并再次将对象*从*上弹出。在本节中，我们围绕该数据结构构建了一个反向波兰符号(RPN)计算器，以展示如何使用它。\n\nRPN 是一种符号，可以用来以一种非常简单的解析方式表达数学表达式。在 RPN 中，`1 + 2`为`1 2 +`。首先是操作数，然后是操作。另一个例子:`(1 + 2) * 3`是 RPN 中的`1 2 + 3 *`，这已经说明了为什么它更容易解析，因为我们不需要*括号*来定义子表达式。\n\n![](img/c5365787-5e7f-4fab-afe2-ad3ae977ddb5.jpg)\n\n# 怎么做...\n\n在本节中，我们将从标准输入中读取 RPN 中的一个数学表达式，然后将其输入到一个对其求值的函数中。最后，我们将数字结果打印回给用户。\n\n1.  我们将使用 STL 中的许多助手，因此有几个包括:\n\n```cpp\n      #include <iostream>\n      #include <stack>\n      #include <iterator>\n      #include <map>\n      #include <sstream>\n      #include <cassert>\n      #include <vector>\n      #include <stdexcept>\n      #include <cmath>\n```\n\n2.  我们还声明我们正在使用名称空间`std`来节省我们的打字时间。\n\n```cpp\n      using namespace std;\n```\n\n3.  然后，我们立即开始实现我们的 RPN 解析器。它将接受一个迭代器对，这表示一个字符串形式的数学表达式的开始和结束，它将被一个标记一个标记地使用。\n\n```cpp\n      template <typename IT>\n      double evaluate_rpn(IT it, IT end)\n      {\n```\n\n4.  当我们遍历代币时，我们需要在途中记住所有的*操作数*，直到我们看到一个*操作*。这就是我们需要堆栈的地方。所有的数字都将被解析并保存在双精度浮点中，所以它将是一堆`double`值。\n\n```cpp\n          stack<double> val_stack;\n```\n\n5.  为了方便地访问堆栈上的元素，我们实现了一个助手。它通过从中提取最高的项来修改堆栈，然后返回该项。这样，我们可以在以后一步完成这项任务。\n\n```cpp\n          auto pop_stack ([&](){ \n              auto r (val_stack.top()); \n              val_stack.pop(); \n              return r; \n          });\n```\n\n6.  另一个准备是定义所有支持的数学运算。我们将它们保存在地图中，地图将每个操作标记与实际操作相关联。这些操作由可调用的 lambdas 表示，它接受两个操作数，例如，将它们相加或相乘，然后返回结果。\n\n```cpp\n          map<string, double (*)(double, double)> ops {\n              {\"+\", [](double a, double b) { return a + b; }},\n              {\"-\", [](double a, double b) { return a - b; }},\n              {\"*\", [](double a, double b) { return a * b; }},\n              {\"/\", [](double a, double b) { return a / b; }},\n              {\"^\", [](double a, double b) { return pow(a, b); }},\n              {\"%\", [](double a, double b) { return fmod(a, b); }},\n          };\n```\n\n7.  现在我们终于可以迭代输入了。假设输入迭代器给我们字符串，我们为每个标记输入一个新的`std::stringstream`，因为它可以解析数字。\n\n```cpp\n          for (; it != end; ++ it) {\n              stringstream ss {*it};\n```\n\n8.  现在每一个标记，我们都试图从中获得一个`double`值。如果成功，我们就有了一个*操作数*，我们将它推送到堆栈上。\n\n```cpp\n              if (double val; ss >> val) {\n                  val_stack.push(val);\n              }\n```\n\n9.  如果确实*没有*成功，那一定是操作符以外的东西；在这种情况下，它只能是一个*操作数*。知道我们支持的所有操作都是*二进制*，我们需要从堆栈中弹出最后两个*操作数。*\n\n```cpp\n              else {\n                  const auto r {pop_stack()};\n                  const auto l {pop_stack()};\n```\n\n10.  现在我们通过对已经发出字符串的迭代器`it`进行解引用来获得操作数。通过查询`ops`映射，我们得到一个 lambda 对象，它接受两个操作数`l`和`r`作为参数。\n\n```cpp\n                  try {\n                      const auto & op     (ops.at(*it));\n                      const double result {op(l, r)};\n                      val_stack.push(result);\n                  }\n```\n\n11.  我们用`try`子句包围了数学部分的应用，因此我们可以捕捉可能发生的异常。地图的`at`调用将抛出`out_of_range`异常，以防用户提供我们不知道的数学运算。在这种情况下，我们将重新抛出一个不同的异常，它表示`invalid argument`，并携带我们未知的操作字符串。\n\n```cpp\n                  catch (const out_of_range &) {\n                      throw invalid_argument(*it);\n                  }\n```\n\n12.  已经这样了。循环一结束，我们就得到堆栈上的最终结果。所以我们只返回那个。(此时，我们可以断言堆栈大小是否为 1。如果不是，那么就会有遗漏的操作。)\n\n```cpp\n              }\n          }\n\n          return val_stack.top();\n      }\n```\n\n13.  现在我们可以使用我们的小 RPN 解析器了。为了做到这一点，我们将标准输入包装成`std::istream_iterator`对，并将其输入到 RPN 解析器函数中。最后，我们打印结果:\n\n```cpp\n      int main()\n      {\n          try {\n              cout << evaluate_rpn(istream_iterator<string>{cin}, {}) \n                   << 'n';\n          }\n```\n\n14.  我们再次将该行包装到`try`子句中，因为用户输入仍然可能包含我们没有实现的操作。在这种情况下，我们必须捕获在这种情况下抛出的异常，并打印一条错误消息:\n\n```cpp\n          catch (const invalid_argument &e) {\n              cout << \"Invalid operator: \" << e.what() << 'n';\n          }\n      }\n```\n\n15.  编译完程序，我们就可以玩了。输入`\"3 1 2 + * 2 /\"`表示表达式`( 3 * (1 + 2) ) / 2`，并产生正确的结果:\n\n```cpp\n      $ echo \"3 1 2 + * 2 /\" | ./rpn_calculator\n      4.5\n```\n\n# 它是如何工作的...\n\n整个方法围绕着将操作数推到堆栈上，直到我们在输入中看到一个操作。在这种情况下，我们再次从堆栈中弹出最后两个操作数，对它们应用操作，并将结果再次推送到堆栈上。为了理解这个配方中的所有代码，重要的是要理解我们如何从输入中区分*操作数*和*操作*，我们如何处理我们的堆栈，以及我们如何选择和应用正确的数学操作。\n\n# 堆栈处理\n\n我们简单地使用`std::stack`的`push`功能将项目推送到堆栈上:\n\n```cpp\nval_stack.push(val);\n```\n\n从中弹出值看起来有点复杂，因为我们为此实现了一个 lambda，它捕获了对`val_stack`对象的引用。让我们看看相同的代码，通过一些更多的注释进行了增强:\n\n```cpp\nauto pop_stack ([&](){\n    auto r (val_stack.top()); // Get top value copy\n    val_stack.pop();          // Throw away top value\n    return r;                 // Return copy\n});\n```\n\n这个 lambda 是获取栈顶值所必需的，并且*在*的一个*步骤中从那里移除*。`std::stack`的界面设计方式不允许在单次*调用中进行。然而，定义一个 lambda 既快速又简单，所以我们现在可以得到这样的值:*\n\n```cpp\ndouble top_value {pop_stack()};\n```\n\n# 从用户输入中区分操作数和操作\n\n在`evaluate_rpn`的主循环中，我们从迭代器中取出当前字符串标记，然后看看它是否是操作数。如果字符串可以被解析成一个`double`变量，那么它就是一个数字，因此也是一个操作数。我们认为所有不容易被解析为数字的事物(例如`\"+\"`)都是*操作*。\n\n这个任务的裸代码框架如下:\n\n```cpp\nstringstream ss {*it};\nif (double val; ss >> val) {\n    // It's a number!\n} else {\n    // It's something else than a number - an operation!\n}\n```\n\n流操作符`>>`告诉我们它是否是一个数字。首先，我们把绳子包成一个`std::stringstream`。然后我们使用`stringstream`对象的能力从`std::string`流入`double`变量，这涉及到解析。如果解析*失败了*，我们知道它是这样做的，因为我们要求它把某个东西解析成一个数字，这个数字就是*没有数字*。\n\n# 选择和应用正确的数学运算\n\n当我们意识到当前用户输入的令牌不是数字后，我们只是假设它是一个操作，比如`+`或者`*`。然后我们查询我们的地图，我们称之为`ops`，查找那个操作，并返回给我们一个函数，这个函数接受两个操作数，并返回和，或乘积，或任何合适的值。\n\n地图本身的类型看起来相对复杂:\n\n```cpp\nmap<string, double (*)(double, double)> ops { ... };\n```\n\n它从`string`映射到`double (*)(double, double)`。后者是什么意思？该类型描述应为“*指针指向一个取两个双精度值的函数，并返回一个双精度值*”。想象一下`(*)`部分是函数的名字，比如在`double sum(double, double)`中，这样立刻更容易阅读。这里的技巧是我们的 lambda `[](double, double) { return /* some double */ }`可以转换成一个函数指针，它实际上与指针描述相匹配。不捕获任何东西的*通常可以转换成函数指针。*\n\n这样，我们可以方便地向地图询问正确的操作:\n\n```cpp\nconst auto & op     (ops.at(*it));\nconst double result {op(l, r)};\n```\n\n这个映射隐式地为我们做了另一个工作:如果我们说`ops.at(\"foo\")`，那么`\"foo\"`是一个有效的键值，但是我们没有存储任何这样命名的操作。在这种情况下，地图会抛出一个异常，我们在食谱中捕捉到了这个异常。每当我们捕捉到一个不同的异常时，我们都会重新抛出它，以便为这个错误案例提供一个描述性的含义。与`out of range`异常相比，用户将更清楚`invalid argument`异常意味着什么。请注意，`evaluate_rpn`函数的用户可能没有阅读过它的实现，因此可能根本不知道我们正在内部使用地图。\n\n# 还有更多...\n\n由于`evaluate_rpn`函数接受迭代器，所以用不同于标准输入流的输入来填充它是非常容易的。这使得测试或者适应不同的用户输入源变得非常容易。\n\n例如，从字符串流或字符串向量中给它提供迭代器，看起来像下面的代码，对此`evaluate_rpn`根本不需要改变:\n\n```cpp\nint main()\n{\n    stringstream s {\"3 2 1 + * 2 /\"};\n    cout << evaluate_rpn(istream_iterator<string>{s}, {}) << 'n';\n\n    vector<string> v {\"3\", \"2\", \"1\", \"+\", \"*\", \"2\", \"/\"};\n    cout << evaluate_rpn(begin(v), end(v)) << 'n';\n}\n```\n\nUse iterators wherever it makes sense. This makes your code very composable and reusable.\n\n# 用 std::map 实现字频计数器\n\n`std::map`在对某些东西进行分类以便收集关于该数据的统计数据时非常有用。通过将可修改的有效载荷对象附加到代表对象类别的每个键上，实现例如单词频率直方图是非常简单的。这就是我们在这一部分要做的。\n\n# 怎么做...\n\n在本节中，我们将阅读标准输入中的所有用户输入，例如，可能是包含文章的文本文件。我们将输入标记为单词，以便计算哪个单词出现的频率。\n\n1.  像往常一样，我们需要包含我们将要使用的数据结构中的所有头。\n\n```cpp\n      #include <iostream>\n      #include <map> \n      #include <vector> \n      #include <algorithm> \n      #include <iomanip>\n```\n\n2.  为了节省一些打字时间，我们声明使用命名空间`std`。\n\n```cpp\n      using namespace std;\n```\n\n3.  我们将使用一个助手函数来从单词中裁剪可能附加的逗号、点或冒号。\n\n```cpp\n      string filter_punctuation(const string &s)\n      {\n          const char *forbidden {\".,:; \"};\n          const auto  idx_start (s.find_first_not_of(forbidden));\n          const auto  idx_end   (s.find_last_not_of(forbidden));\n\n          return s.substr(idx_start, idx_end - idx_start + 1);\n      }\n```\n\n4.  现在我们从实际的程序开始。我们将收集一张地图，将我们看到的每个单词与该单词的频率计数器关联起来。此外，我们还维护了一个变量，它记录了迄今为止我们所看到的最长单词的大小，因此当我们在程序结束时打印单词频率表时，我们可以很好地缩进它。\n\n```cpp\n      int main()\n      {\n          map<string, size_t> words;\n          int max_word_len {0};\n```\n\n5.  当我们从`std::cin`流入一个`std::string`变量时，输入流会在途中删除空白。这样我们一个字一个字地得到输入。\n\n```cpp\n          string s;\n          while (cin >> s) {\n```\n\n6.  我们现在的单词，可能包含逗号、点或冒号，因为它可能在一个句子或类似句子的末尾。我们用之前定义的辅助函数过滤掉它。\n\n```cpp\n              auto filtered (filter_punctuation(s));\n```\n\n7.  万一这个词是目前为止最长的词，我们需要更新`max_word_len`变量。\n\n```cpp\n              max_word_len = max<int>(max_word_len, filtered.length());\n```\n\n8.  现在我们将在我们的`words`地图中增加该单词的计数器值。如果是第一次发生，在我们递增它之前，它是隐式创建的。\n\n```cpp\n              ++ words[filtered];\n          }\n```\n\n9.  循环结束后，我们知道我们将输入流中所有唯一的单词保存在`words`映射中，并与表示每个单词频率的计数器配对。地图使用单词作为关键字，并按照它们的*字母*顺序进行排序。我们想要的是打印所有按照*频率*排序的单词，所以频率最高的单词优先。为了实现这一点，我们将首先实例化一个向量，所有这些词频对都适合这个向量，并将它们从映射移动到向量。\n\n```cpp\n          vector<pair<string, size_t>> word_counts;\n          word_counts.reserve(words.size());\n          move(begin(words), end(words), back_inserter(word_counts));\n```\n\n10.  向量现在仍然包含所有词频对，其顺序与`words`图保持的顺序相同。现在我们重新排序，以开头出现频率最高的词，结尾出现频率最低的词为序。\n\n```cpp\n          sort(begin(word_counts), end(word_counts),\n              [](const auto &a, const auto &b) { \n                  return a.second > b.second; \n              });\n```\n\n11.  现在所有的数据都是有序的，所以我们把它推送到用户终端。使用`std::setw`流操纵器，我们以一种很好的缩进格式格式化数据，所以它看起来有点像一个表。\n\n```cpp\n          cout << \"# \" << setw(max_word_len) << \"<WORD>\" << \" #<COUNT>n\";\n          for (const auto & [word, count] : word_counts) {\n              cout << setw(max_word_len + 2) << word << \" #\" \n                   << count << 'n';\n          }\n      }\n```\n\n12.  编译程序后，我们可以将任何文本文件导入其中，以便获得频率表。\n\n```cpp\n      $ cat lorem_ipsum.txt | ./word_frequency_counter\n      #       <WORD> #<COUNT>\n                  et #574\n               dolor #302\n                 sed #273\n                diam #273\n                 sit #259\n               ipsum #259\n      ...\n```\n\n# 它是如何工作的...\n\n这个方法集中在收集一个`std::map`中的所有单词，然后将所有项目从地图中推出并放入一个`std::vector`中，然后进行不同的排序，以便打印数据。为什么呢？\n\n我们来看一个例子。当我们计算字符串`\"a a b c b b b d c c\"`中的词频时，我们会得到以下地图内容:\n\n```cpp\na -> 2\nb -> 4\nc -> 3\nd -> 1\n```\n\n然而，这不是我们想要呈现给用户的顺序。程序应该先打印`b`，因为它的频率最高。然后是`c`，然后是`a`，然后是`d`。遗憾的是，我们无法请求地图给我们关联值最高的“*键*，然后是关联值第二高的“*键*，以此类推。\n\n这里，向量开始发挥作用。我们键入的向量包含成对的字符串和计数器值。这样，当项目从地图中消失时，它可以将项目准确地保存在表单中。\n\n```cpp\nvector<pair<string, size_t>> word_counts;\n```\n\n然后我们使用`std::move`算法使用词频对填充向量。这样做的好处是，保留在堆上的字符串部分不会被复制，而是会从映射转移到向量。这样我们可以避免大量的复制。\n\n```cpp\nmove(begin(words), end(words), back_inserter(word_counts));\n```\n\nSome STL implementations use short string optimization--if the string is not too long, it will *not* be allocated on the heap and stored in the string object directly instead. In that case, a move is not faster. But moves are also never slower!\n\n下一个有趣的步骤是排序操作，它使用 lambda 作为自定义比较运算符:\n\n```cpp\nsort(begin(word_counts), end(word_counts),\n        [](const auto &a, const auto &b) { return a.second > b.second; });\n```\n\n排序算法将两两取项，并进行比较，这就是排序算法的作用。通过提供 lambda 函数，该比较不仅比较`a`是否小于`b`(这是默认实现)，还比较`a.second`是否大于`b.second`。请注意，所有对象都是字符串及其计数器值的*对*，通过编写`a.second`，我们可以访问单词的计数器值。这样，我们将所有高频词移向向量的开头，低频词移向后面。\n\n# 使用 std::multimap 实现一个写作风格助手工具，用于查找文本中的超长句子\n\n每当有很多物品需要进行排序存储，并且排序所依据的关键字可以多次出现时，`std::multimap`就是一个不错的选择。\n\n我们来找一个用例的例子:用德语写文字的时候，用很长的句子是可以的。用英语写课文的时候是*不是*。我们将实现一个工具，帮助德国作者分析他们的英语文本文件，重点是所有句子的长度。为了帮助作者改进文本风格，它会根据句子的长度对句子进行分组。这样，作者可以挑选最长的并分解它们。\n\n# 怎么做...\n\n在本节中，我们将从标准输入中读取所有用户输入，我们将通过整句话而不是单词来标记。然后我们将收集一个`std::multimap`中的所有句子，并与携带其长度的变量配对。之后，我们将所有的句子，按照它们的长度排序，输出给用户。\n\n1.  像往常一样，我们需要包含所有需要的头。`std::multimap`和`std::map`来自同一个表头。\n\n```cpp\n      #include <iostream>\n      #include <iterator>\n      #include <map>\n      #include <algorithm>\n```\n\n2.  我们使用了很多来自命名空间`std`的函数，所以我们自动声明它的使用。\n\n```cpp\n      using namespace std;\n```\n\n3.  当我们通过提取文本中点字符之间的内容来标记字符串时，我们将获得被空格(如空格、新的线符号等)包围的文本句子。这些会以错误的方式增加它们的大小，所以我们使用我们现在定义的辅助函数过滤掉它们。\n\n```cpp\n      string filter_ws(const string &s)\n      {\n          const char *ws {\" rnt\"};\n          const auto a (s.find_first_not_of(ws));\n          const auto b (s.find_last_not_of(ws));\n          if (a == string::npos) {\n              return {};\n          }\n          return s.substr(a, b);\n      }\n```\n\n4.  实际的句子长度计数函数应该取一个包含所有文本的巨型字符串，然后返回一个`std::multimap`，将排序后的句子长度映射到句子中。\n\n```cpp\n      multimap<size_t, string> get_sentence_stats(const string &content)\n      {\n```\n\n5.  我们首先声明`multimap`结构，它是返回值，以及一些迭代器。因为我们将有一个循环，我们需要一个`end`迭代器。然后我们使用两个迭代器来指向文本中连续的点。之间的一切都是一个文字句子。\n\n```cpp\n          multimap<size_t, string> ret;\n\n          const auto end_it (end(content));\n          auto it1 (begin(content));\n          auto it2 (find(it1, end_it, '.'));\n```\n\n6.  `it2`永远比`it1`远一个点。只要`it1`没有到达文末，我们就没事。第二个条件检查`it2`是否真的至少是进一步的一些字符。如果不是这样，它们之间就没有字符可读了。\n\n```cpp\n          while (it1 != end_it && distance(it1, it2) > 0) {\n```\n\n7.  我们从迭代器之间的所有字符创建一个字符串，并从它的开始和结束过滤所有空白，以便计算纯句子的长度。\n\n```cpp\n              string s {filter_ws({it1, it2})};\n```\n\n8.  有可能这个句子除了空白之外，不包含任何东西。在这种情况下，我们干脆放弃它。否则，我们通过确定有多少个单词来计算它的长度。这很容易，因为所有单词之间只有一个空格。然后我们把字数和句子一起保存在`multimap`里。\n\n```cpp\n              if (s.length() > 0) {\n                  const auto words (count(begin(s), end(s), ' ') + 1);\n                  ret.emplace(make_pair(words, move(s)));\n              }\n```\n\n9.  对于下一个循环迭代，我们将前导迭代器`it1`放在下一个句子的点字符上。下面的迭代器`it2`放在前导迭代器的*旧的*位置之后一个字符。\n\n```cpp\n              it1 = next(it2, 1);\n              it2 = find(it1, end_it, '.');\n          }\n```\n\n10.  循环结束后，`multimap`包含所有与其字数成对的句子，可以返回。\n\n```cpp\n          return ret;\n      }\n```\n\n11.  现在我们使用这个函数。首先，我们告诉`std::cin`不要跳过空格，因为我们希望句子中有空格。为了读取整个文件，我们从封装了`std::cin`的输入流迭代器中初始化一个`std::string`。\n\n```cpp\n      int main()\n      {\n          cin.unsetf(ios::skipws);\n          string content {istream_iterator<char>{cin}, {}};\n```\n\n12.  由于我们只需要`multimap`结果进行打印，所以我们将`get_sentence_stats`调用直接放入循环中，并用我们的字符串进行馈送。在循环体中，我们逐行打印项目。\n\n```cpp\n          for (const auto & [word_count, sentence] \n                   : get_sentence_stats(content)) {\n              cout << word_count << \" words: \" << sentence << \".n\";\n          }\n      }\n```\n\n13.  在编译完代码后，我们可以从任何文本文件中为应用提供文本。Lorem Ipsum 文本示例产生以下输出。对于包含许多句子的长文本，由于输出很长，它会先打印最短的句子，然后打印最长的句子。这样，我们首先看到最长的句子，因为终端通常会自动滚动到输出的末尾。\n\n```cpp\n      $ cat lorem_ipsum.txt | ./sentence_length\n      ...\n      10 words: Nam quam nunc, blandit vel, luctus pulvinar, \n      hendrerit id, lorem.\n      10 words: Sed consequat, leo eget bibendum sodales, \n      augue velit cursus nunc,.\n      12 words: Cum sociis natoque penatibus et magnis dis \n      parturient montes, nascetur ridiculus mus.\n      17 words: Maecenas tempus, tellus eget condimentum rhoncus, \n      sem quam semper libero, sit amet adipiscing sem neque sed ipsum.\n```\n\n# 它是如何工作的...\n\n整个食谱专注于将一个大字符串分解成文本句子，根据句子的长度进行评估，然后在`multimap`中排序。因为`std::multimap`本身很容易使用，程序复杂的部分是循环，循环迭代句子:\n\n```cpp\nconst auto end_it (end(content));\nauto it1 (begin(content));         // (1) Beginning of string\nauto it2 (find(it1, end_it, '.')); // (1) First '.' dot\n\nwhile (it1 != end_it && std::distance(it1, it2) > 0) {\n    string sentence {it1, it2};\n\n    // Do something with the sentence string...\n\n    it1 = std::next(it2, 1);      // One character past current '.' dot\n    it2 = find(it1, end_it, '.'); // Next dot, or end of string\n}\n```\n\n让我们看看下面的代码，它由三个句子组成:\n\n![](img/18c1c74b-9f55-4b94-b150-f5f08f678583.png)\n\n`it1`和`it2`总是一起通过绳子向前移动。这样他们总是指向*一句*的开头和结尾。`std::find`算法在这方面对我们帮助很大，因为它像“*一样从当前位置开始，然后将迭代器返回到下一个点字符。如果没有，返回结束迭代器*。”\n\n在我们提取一个句子字符串后，我们确定它包含多少单词，因此我们可以将其插入到`multimap.`中。我们使用单词的*数量*作为地图节点的*键*，字符串本身作为与之相关联的有效载荷对象。很容易出现长度相同的多个句子。这将使我们无法将它们全部插入一个`std::map`。但是既然我们使用了`std::multimap`，这就没有问题了，因为它可以轻松处理多个相同值的按键。它会将所有*排序后的*保持在一条直线上，这就是我们需要按长度枚举所有句子并输出给用户的。\n\n# 还有更多...\n\n将整个文件读入一个大字符串后，我们遍历该字符串并再次创建每个句子的副本。这是不必要的，因为我们也可以使用`std::string_view`，这将在本书后面介绍。\n\n另一种迭代获取两个连续点之间的字符串的方法是`std::regex_iterator`，这也将在本书的后一章中介绍。\n\n# 使用 std::priority_queue 实现个人待办事项列表\n\n`std::priority_queue`是另一个容器适配器类，如`std::stack`。它是另一个数据结构的包装器(默认情况下是`std::vector`)，并为它提供了一个类似队列的接口。这意味着项目可以逐步推入其中，并逐步再次弹出。先推入*的*，会先弹出*的*。这通常也缩写为**先进先出** ( **先进先出**)队列。这是堆栈的反面，最后一个推上的*项会先从其中弹出*。**\n\n **虽然我们只是描述了`std::queue`的行为，但这一部分展示了`std::priority_queue`是如何工作的。这个适配器是特殊的，因为它不仅考虑了先进先出的特性，而且还将其与优先级混合在一起。这意味着先进先出原则被分解为子先进先出队列，这些队列是按照项目的优先级排序的。\n\n# 怎么做...\n\n在这一部分，我们将建立一个廉价的*待办事项列表组织*结构。我们不解析用户输入是为了让这个程序简短并专注于`std::priority_queue`。因此，我们只是将一个具有优先级和描述的未排序的待办事项列表填充到一个优先级队列中，然后像从先进先出队列数据结构中读取它们一样，但根据单个项目的优先级进行分组。\n\n1.  我们需要先包含一些标题。`std::priority_queue`在头文件`<queue>`中。\n\n```cpp\n      #include <iostream>\n      #include <queue>\n      #include <tuple>\n      #include <string>\n```\n\n2.  我们如何将待办事项存储在优先队列中？问题是，我们不能添加项目并附加优先级。优先队列将尝试使用队列中所有项目的*自然顺序*。我们现在可以实现我们自己的`struct todo_item`，并给它一个优先级号和一个字符串待办事项描述，然后实现比较运算符`<`，以便使它们可排序。或者，我们可以只取`std::pair`，它使我们能够将两件事情聚合成一个类型，并自动为我们实现比较。\n\n```cpp\n      int main()\n      {\n          using item_type = std::pair<int, std::string>;\n```\n\n3.  我们现在有了一个新的类型`item_type`，它由一个整数优先级和一个字符串描述组成。因此，让我们实例化一个优先级队列，它维护这样的项目。\n\n```cpp\n          std::priority_queue<item_type> q;\n```\n\n4.  我们现在将使用具有不同优先级的不同项目来填充优先级队列。目标是提供一个*非结构化的*列表，然后优先级队列告诉我们*在*中要做什么*按照哪个顺序*。如果有漫画要看，有作业要做，当然作业一定要先做。不幸的是，`std::priority_queue`没有接受初始化列表的构造函数，我们可以从一开始就用它来填充队列。(使用向量或普通列表，它会以这种方式工作。)所以我们首先定义列表，并在下一步中插入它。\n\n```cpp\n          std::initializer_list<item_type> il {\n              {1, \"dishes\"},\n              {0, \"watch tv\"},\n              {2, \"do homework\"},\n              {0, \"read comics\"},\n          };\n```\n\n5.  我们现在可以轻松地遍历未排序的待办事项列表，并使用`push`函数一步一步地插入它们。\n\n```cpp\n          for (const auto &p : il) {\n              q.push(p);\n          }\n```\n\n6.  所有项目都是隐式排序的，因此我们有一个队列，它给出了优先级最高的项目。\n\n```cpp\n          while(!q.empty()) {\n              std::cout << q.top().first << \": \" << q.top().second << 'n';\n              q.pop();\n          }\n          std::cout << 'n';\n      }\n```\n\n7.  让我们编译并运行我们的程序。的确，它告诉我们，先做作业，洗碗后，终于可以看电视，看漫画了。\n\n```cpp\n      $ ./main\n      2: do homework\n      1: dishes\n      0: watch tv\n      0: read comics\n```\n\n# 它是如何工作的...\n\n`std::priority`列表非常好用。我们只使用了三个功能:\n\n1.  `q.push(item)`将一个项目推入队列。\n2.  `q.top()`返回对首先从队列中出来的项目的引用。\n3.  `q.pop()`删除队列中最前面的项目。\n\n但是物品订购是如何工作的呢？我们将一个优先级整数和一个待办事项描述字符串组合到一个`std::pair`中，得到自动排序。如果我们有一个`std::pair<int, std::string>`实例`p`，我们可以写`p.first`来访问*整数*部分，`p.second`来访问*字符串*部分。我们在打印所有待办事项的循环中做到了这一点。\n\n但是优先级队列是怎么推断出`{2, \"do homework\"}`比`{0, \"watch tv\"}`更重要的是*而不是我们告诉它比较数字部分的呢？*\n\n比较运算符`<`处理不同的情况。假设我们比较`left < right`和`left`和`right`是一对。\n\n1.  `left.first != right.first`，然后返回`left.first < right.first`。\n2.  `left.first == right.first`，然后返回`left.second < right.second`。\n\n这样，我们可以根据需要订购物品。唯一重要的是优先级是该对中的*第一*成员，描述是该对中的*第二*成员。否则，`std::priority_queue`会以一种看起来项目的字母顺序比它们的优先级更重要的方式对项目进行排序。(那样的话，*看电视*会被建议为*先做*的事情，*做作业*一段时间*再*。这对我们这些懒惰的人来说至少是件好事！)**************"
  },
  {
    "path": "docs/exp-cpp-prog/02.md",
    "content": "# 二、迭代器\n\n我们在本章中介绍了以下食谱:\n\n*   建立自己的可迭代范围\n*   使您自己的迭代器与 STL 迭代器类别兼容\n*   使用迭代器包装来填充通用数据结构\n*   根据迭代器实现算法\n*   使用反向迭代器适配器反过来迭代\n*   用迭代器哨兵终止范围内的迭代\n*   使用已检查的迭代器自动检查迭代器代码\n*   构建自己的 zip 迭代器适配器\n\n# 介绍\n\n迭代器是 C++ 中一个非常重要的概念。STL 的目标是尽可能灵活和通用，迭代器在这方面有很大的帮助。不幸的是，它们有时使用起来有点繁琐，这就是为什么许多新手*避开*它们，回到 *C 风格的 C++* 。一个避免迭代器的程序员基本上放弃了 STL 的一半潜力。这一章讨论迭代器，并很快阐明它们是如何工作的。快速的介绍可能还不够，但是*食谱*真的是为了给迭代器内部一个好的感觉。\n\n大多数容器类，还有老式的 C 风格数组，都以这样或那样的方式包含一个数据项的*范围*。许多处理大量数据项的日常任务并不关心如何获取这些数据。然而，例如，如果我们考虑一个整数数组和一个整数的*链表*，并且想要计算两个结构的所有项目的*和*，我们会得到两个不同的算法，如下所示:\n\n*   一种算法，通过检查数组的大小并将其总结如下来处理数组:\n\n```cpp\n      int sum {0};\n      for (size_t i {0}; i < array_size; ++ i) { sum += array[i]; }\n```\n\n*   另一种算法，通过迭代处理链表，直到它到达末尾:\n\n```cpp\n      int sum {0};\n      while (list_node != nullptr) { \n          sum += list_node->value; list_node = list_node->next; \n      }\n```\n\n都是关于*求和整数*的，但是我们打出来的字符百分比有多大，直接关系到*实际的*求和任务？其中一个是否与第三种数据结构一起工作，比如说`std::map`，或者我们必须实现它的另一个版本？没有迭代器，这将把我们引向荒谬的方向。\n\n只有在迭代器的帮助下，才有可能以通用的形式实现这一点:\n\n```cpp\nint sum {0};\nfor (int i : array_or_vector_or_map_or_list) { sum += i; }\n```\n\n这个漂亮而简短的所谓基于范围的`for`循环从 C++ 11 开始就存在了。它只是一个语法糖，扩展成类似于下面的代码:\n\n```cpp\n{ \n    auto && __range = array_or_vector_or_map_or_list ; \n    auto __begin = std::begin(__range);\n    auto __end   = std::end(__range);\n    for ( ; __begin != __end; ++ __begin) { \n        int i = *__begin; \n        sum += i;\n    } \n}\n```\n\n对于已经使用过迭代器的人来说，这是一顶旧帽子，对于没有使用过迭代器的人来说，这看起来完全是一种魔力。假设我们的整数向量如下所示:\n\n![](img/5fd26991-6353-4490-b01b-959c754fe5b5.png)\n\n`std::begin(vector)`命令与`vector.begin()`相同，并返回给我们一个指向第一项的迭代器(T4 1)。`std::end(vector)`与`vector.end()`相同，返回一个迭代器，该迭代器指向最后一个项之后的一个项*(经过 **5** )。*\n\n在每次迭代中，循环检查开始迭代器是否不等于结束迭代器。如果是，它将*取消引用*开始迭代器，从而访问它所指向的整数值。然后，它*递增*迭代器，重复与结束迭代器的比较，等等。在那一刻，当想象迭代器是普通的 *C* 风格的指针时，再次读取循环代码是有帮助的。事实上，普通的 C 风格指针也是一种有效的迭代器。\n\n# 迭代器类别\n\n迭代器有多个类别，它们有不同的限制。它们并不太难记住，只要记住一个类别需要的能力是从下一个强大的类别继承而来的。迭代器类别的全部意义在于，如果一个算法知道它在处理什么类型的迭代器，它就可以以一种优化的方式实现。这样，程序员可以向后靠，表达自己的意图，而编译器可以为给定的任务选择*最优实现*。\n\n让我们按照正确的顺序来看一下:\n\n![](img/d9d1c3e9-25b2-45d2-9630-3759bef7cb1d.png)\n\n# 输入迭代器\n\n输入迭代器只能在*读取它们所指向的*值时被取消引用。一旦它们被递增，它们所指向的最后一个值在递增过程中被*无效*。这意味着不可能多次迭代这样的范围。`std::istream_iterator`就是这一类的例子。\n\n# 向前迭代器\n\n前向迭代器与输入迭代器相同，但它们的不同之处在于它们所代表的范围可以多次迭代。`std::forward_list`迭代器就是一个例子。这样的列表只能向前*迭代，不能向后，但是我们想迭代多少次就迭代多少次。*\n\n *# 双向迭代器\n\n顾名思义，双向迭代器可以递增和递减，以便向前或向后迭代。例如，`std::list`、`std::set`和`std::map`的迭代器就支持这一点。\n\n# 随机存取迭代器\n\n随机访问迭代器允许一次跳过多个值，而不是单步执行。这是`std::vector`和`std::deque`的迭代器的情况。\n\n# 连续迭代器\n\n这个类别指定了所有前述的需求，加上被迭代的数据位于连续内存中的需求，就像它在数组中一样，或者`std::vector`。\n\n# 输出迭代器\n\n输出迭代器从其他类别中分离出来。这是因为迭代器可以是纯输出迭代器，它只能递增并用于*将*写入它所指向的数据。如果正在读取它们，该值将是未定义的。\n\n# 可变迭代器\n\n如果迭代器同时是输出迭代器和其他类别之一，那么它就是可变迭代器。它可以读写。如果我们从一个非常量的容器实例中获得一个迭代器，它通常就是这种类型的。\n\n# 建立自己的可迭代范围\n\n我们已经意识到迭代器是各种容器迭代的*标准接口*。我们只需要实现前缀增量操作符`++ `、去引用操作符`*`和对象比较操作符`==`，然后我们已经有了一个适合花哨的 C++ 11 基于范围的`for`循环的原语迭代器。\n\n为了更好地适应这一点，这个方法展示了如何实现一个迭代器，当遍历它时，它只发出一个数字范围。它没有任何容器结构或类似结构的支持。这些数字是在迭代时临时生成的。\n\n# 怎么做...\n\n在这个食谱中，我们将实现我们自己的迭代器类，然后，我们将遍历它:\n\n1.  首先，我们包括标题，它使我们能够打印到终端:\n\n```cpp\n      #include <iostream>\n```\n\n2.  我们的迭代器类将被称为`num_iterator`:\n\n```cpp\n      class num_iterator {\n```\n\n3.  它唯一的数据成员是一个整数。那个整数用于计数。构造函数用于初始化它。让构造函数*显式*一般是一种很好的形式，从另一个类型创建一个类型，避免*偶然*隐式转换。请注意，我们还为`position`提供了默认值。这使得`num_iterator`类的实例可以默认构造。虽然我们不会在整个配方中使用默认构造函数，但这非常重要，因为一些 STL 算法依赖于迭代器是默认可构造的:\n\n```cpp\n          int i;\n      public:\n\n          explicit num_iterator(int position = 0) : i{position} {}\n```\n\n4.  当取消引用我们的迭代器(`*it`)时，它将发出一个整数:\n\n```cpp\n          int operator*() const { return i; }\n```\n\n5.  递增迭代器(`++ it`)只会递增其内部计数器`i`:\n\n```cpp\n          num_iterator& operator++() {\n              ++ i;\n              return *this;\n          }\n```\n\n6.  一个`for`循环将比较迭代器和结束迭代器。如果它们*不相等*，它将继续迭代:\n\n```cpp\n          bool operator!=(const num_iterator &other) const {\n              return i != other.i;\n          }\n      };\n```\n\n7.  那是迭代器类。我们仍然需要一个中间对象来编写`for (int i : intermediate(a, b)) {...}`，然后它包含开始和结束迭代器，该迭代器被预编程为从`a`迭代到`b`。我们称之为`num_range`:\n\n```cpp\n      class num_range {\n```\n\n8.  它包含两个整数成员，表示迭代应该从哪个数字开始，哪个数字是最后一个数字之后的第一个数字。这意味着如果我们想从`0`迭代到`9`，`a`设置为`0`，`b`设置为`10`:\n\n```cpp\n          int a;\n          int b;\n\n      public:\n          num_range(int from, int to)\n              : a{from}, b{to}\n          {}\n```\n\n9.  我们只需要实现两个成员函数:`begin`和`end`函数。两者都返回指向数值范围开始和结束的迭代器:\n\n```cpp\n          num_iterator begin() const { return num_iterator{a}; }\n          num_iterator end()   const { return num_iterator{b}; }\n      };\n```\n\n10.  就这样。我们可以用它。让我们编写一个主函数，它只是在从`100`到`109`的范围内迭代，并打印它的所有值:\n\n```cpp\n      int main()\n      {\n          for (int i : num_range{100, 110}) {\n              std::cout << i << \", \";\n          }\n          std::cout << 'n';\n      }\n```\n\n11.  编译和运行程序会产生以下终端输出:\n\n```cpp\n      100, 101, 102, 103, 104, 105, 106, 107, 108, 109,\n```\n\n# 它是如何工作的...\n\n假设我们编写了以下代码:\n\n```cpp\nfor (auto x : range) { code_block; }\n```\n\n编译器将对其进行如下评估:\n\n```cpp\n{ \n    auto __begin = std::begin(range);\n    auto __end   = std::end(range);\n    for ( ; __begin != __end; ++ __begin) { \n        auto x = *__begin; \n        code_block\n    } \n}\n```\n\n查看这段代码时，很明显迭代器的唯一要求是以下三个操作符:\n\n*   `operator!=`:不对等比较\n*   `operator++ `:前缀增量\n*   `operator*`:取消引用\n\n范围的要求是它有一个`begin`和一个`end`方法，这两个方法返回两个迭代器，表示一个范围的开始和结束。\n\nIn this book, we're mostly using `std::begin(x)` instead of `x.begin()`. This is generally a good style because `std::begin(x)` automatically calls `x.begin()` if that member method is available. If `x` is an array that does not have a `begin()` method, `std::begin(x)` will automatically find out how to deal with it. The same applies to `std::end(x)`. User defined types that do not provide `begin()`/`end()` members do not work with `std::begin`/`std::end`.\n\n我们在这个食谱中所做的只是将一个简单的计数算法放入前向迭代器接口。实现一个迭代器和一个范围总是涉及到这个最小量的样板代码，这一方面可能有点烦人。另一方面，看看使用`num_range`的循环是非常有益的，因为它看起来如此*非常简单*！\n\nScroll back and have a thorough look on which of the methods of the iterator and the range class are `const`. Forgetting to make those functions `const` can make the compiler *reject* your code in a lot of situations because it is a common thing to iterate over `const` objects.\n\n# 使您自己的迭代器与 STL 迭代器类别兼容\n\n无论我们提出什么样的容器数据结构，为了有效地将它与所有的 STL 优点混合在一起，我们需要让它们提供迭代器接口。在最后一节中，我们学习了如何做到这一点，但是我们很快意识到*的一些* STL 算法*无法用我们的自定义迭代器很好地编译*。为什么呢？\n\n问题是，很多 STL 算法试图找出更多关于我们要求它们处理的迭代器的信息。不同的迭代器*类别*具有不同的能力，因此，实现相同的*算法可能有不同的可能性。例如，如果我们将*普通数字*从一个`std::vector`复制到另一个*，这可以通过快速`memcpy`调用来实现。如果我们从或向`std::list`复制数据，这将不再是*而不是*了，必须逐个单独复制项目。STL 算法的实现者对这种自动优化投入了大量的精力。为了帮助他们，我们可以给迭代器配备一些关于它们的*信息*。本节展示了如何实现这一点。\n\n# 怎么做...\n\n在本节中，我们将实现一个对数字进行计数的原始迭代器，并将其与 STL 算法一起使用，STL 算法最初不会用它进行编译。然后我们做必要的事情使它与 STL 兼容。\n\n1.  首先，我们需要像往常一样包含一些标题:\n\n```cpp\n      #include <iostream>\n      #include <algorithm>\n```\n\n2.  然后我们实现一个基本的计数迭代器，如前一节所述。当迭代它时，它会发出简单的递增整数。`num_range`充当方便的*开始*和*结束*迭代器提供者:\n\n```cpp\n      class num_iterator \n      {\n          int i;\n      public:\n\n          explicit num_iterator(int position = 0) : i{position} {}\n\n          int operator*() const { return i; }\n\n          num_iterator& operator++() {\n              ++ i;\n              return *this;\n          }\n\n          bool operator!=(const num_iterator &other) const {\n              return i != other.i;\n          }\n\n          bool operator==(const num_iterator &other) const {\n              return !(*this != other); \n          }\n      };\n\n      class num_range {\n          int a;\n          int b;\n\n      public:\n          num_range(int from, int to)\n              : a{from}, b{to}\n          {}\n\n          num_iterator begin() const { return num_iterator{a}; }\n          num_iterator end()   const { return num_iterator{b}; }\n      };\n```\n\n3.  为了保留`std::`名称空间前缀并保持代码可读，我们声明使用名称空间`std`:\n\n```cpp\n      using namespace std;\n```\n\n4.  现在让我们实例化一个从`100`到`109`的范围。注意值`110`是结束迭代器的位置。这意味着`110`是范围之外的第一个号*(这就是它从`100`到`109`的原因):*\n\n```cpp\n      int main()\n      {\n          num_range r {100, 110};\n```\n\n5.  而现在，我们用它搭配`std::minmax_element`。这个算法用两个成员返回给我们`std::pair`:一个迭代器指向范围内的最小值，另一个迭代器指向范围内的最大值。这些当然是`100`和`109`，因为这就是我们构建系列的方式:\n\n```cpp\n          auto [min_it, max_it] (minmax_element(begin(r), end(r)));\n          cout << *min_it << \" - \" << *max_it << 'n';\n      }\n```\n\n6.  编译代码会导致以下错误消息。是一些与`std::iterator_traits`有关的错误。稍后会有更多。在其他编译器和/或 STL 库实现上有*其他*错误或者*根本没有*错误，这可能会发生*。当版本 5.0.0(中继线 299766)出现此错误信息时:*\n\n *![](img/52d1b385-6f1a-4731-97a4-4389d0e9047b.png)\n\n7.  为了解决这个问题，我们需要激活迭代器类的迭代器特性功能。就在`num_iterator`定义之后，我们编写了下面的`std::iterator_traits`类型的模板结构专门化。它告诉 STL 我们的`num_iterator`属于类别前向迭代器，它迭代`int`值:\n\n```cpp\n      namespace std {\n        struct iterator_traits<num_iterator> {\n\n          using iterator_category = std::forward_iterator_tag;\n\n          using value_type = int;\n\n          using difference_type = void;\n\n          using pointer = int*;\n\n          using reference = int&;\n\n        };\n      }\n```\n\n8.  我们再编译一遍；我们可以看到它是有效的！最小/最大函数的输出如下，这正是我们所期望的:\n\n```cpp\n      100 - 109\n```\n\n# 它是如何工作的...\n\n一些 STL 算法需要知道它们所使用的迭代器类型的特征。有些人需要知道迭代器迭代的项目类型。这有不同的实现原因。\n\n然而，假设迭代器类型为`my_iterator`，所有 STL 算法都将通过`std::iterator_traits<my_iterator>`访问该类型信息。这个特性类包含多达五种不同的类型成员定义:\n\n*   `difference_type`:写`it1 - it2`是什么类型的结果？\n*   `value_type`:我们用`*it`访问的项目是什么类型的(注意这是【纯输出迭代器的 T2】)？\n*   `pointer`:指针必须是什么类型才能指向一个项目？\n*   `reference`:引用一个项目必须是什么类型？\n*   `iterator_category`:迭代器属于哪一类？\n\n`pointer`、`reference`和`difference_type`类型定义对我们的`num_iterator`没有意义，因为它没有迭代真实的*内存*值(我们只是*返回* `int`值，但是它们不像在数组中那样持久可用)。因此，最好不要定义它们，因为如果一个算法依赖于那些在内存中可引用的项目，那么当与我们的迭代器结合时，它可能会有问题。\n\n# 还有更多...\n\n直到 C++ 17，才鼓励让迭代器类型直接从`std::iterator<...>`继承，这会自动用所有类型定义填充我们的类。这仍然有效，但是从 C++ 17 开始就不鼓励了。\n\n# 使用迭代器适配器填充通用数据结构\n\n在很多情况下，我们想用海量数据填充任何容器，但是数据源和容器没有*通用接口*。在这种情况下，我们需要编写自己的手工算法，只处理如何将数据从源推到宿的问题。通常，这会分散我们对解决具体的 T4 问题的实际工作的注意力。\n\n由于 STL 提供的另一个抽象:**迭代器适配器**，在概念上不同的数据结构之间简单传输数据的任务可以用一行代码来实现。本节演示了其中一些的用法，以便给人一种它们有多有用的感觉。\n\n# 怎么做...\n\n在本节中，我们使用一些迭代器包装器只是为了展示它们的存在以及它们如何在日常编程任务中帮助我们。\n\n1.  我们需要首先包含一些标题:\n\n```cpp\n      #include <iostream>\n      #include <string>\n      #include <iterator>\n      #include <sstream>\n      #include <deque>\n```\n\n2.  声明我们使用命名空间`std`可以让我们在以后少打一些字:\n\n```cpp\n      using namespace std;\n```\n\n3.  我们从`std::istream_iterator`开始。我们专注于`int`。这样，它将尝试将标准输入解析为整数。例如，如果我们迭代它，它看起来就像是`std::vector<int>`。结束迭代器被实例化为相同类型，但没有任何构造函数参数:\n\n```cpp\n      int main()\n      {\n          istream_iterator<int> it_cin {cin};\n          istream_iterator<int> end_cin;\n```\n\n4.  接下来，我们实例化`std::deque<int>`并将标准输入中的所有整数复制到 deque 中。deque 本身不是一个迭代器，所以我们使用`std::back_inserter`辅助函数将其包装到`std::back_insert_iterator`中。这个特殊的迭代器包装器将对我们从标准输入中获得的每个项目执行`v.push_back(item)`。这样德格就自动成长了！\n\n```cpp\n          deque<int> v;\n\n          copy(it_cin, end_cin, back_inserter(v));\n```\n\n5.  在下一个练习中，我们使用`std::istringstream`将物品复制到德格的*中间*。因此，让我们首先以字符串的形式定义一些示例数字，并从中实例化 stream 对象:\n\n```cpp\n          istringstream sstr {\"123 456 789\"};\n```\n\n6.  然后，我们需要一个提示，在哪里插入 deque。它将是中间的，所以我们使用 deque 的开始指针，并将其馈送到`std::next`函数。这个函数的第二个参数表示它将返回一个由`v.size() / 2`步推进的迭代器，即*半*步。(我们将`v.size()`转换为`int`，因为`std::next`的第二个参数是用作第一个参数的迭代器的`difference_type`。在这种情况下，这是一个有符号整数类型。根据编译器标志，如果我们没有显式强制转换，编译器可能会在此时*警告*。)\n\n```cpp\n          auto deque_middle (next(begin(v), \n                                  static_cast<int>(v.size()) / 2));\n```\n\n7.  现在，我们可以一步一步地将解析的整数从输入字符串流复制到 deque 中。同样，流迭代器包装器的结束迭代器只是一个没有构造函数参数的空`std::istream_iterator<int>`(即代码行中的空`{}`括号)。deque 被包装到一个插入器包装器中，它是一个`std::insert_iterator`，使用`deque_middle`迭代器指向 deque 的中间:\n\n```cpp\n          copy(istream_iterator<int>{sstr}, {}, inserter(v, deque_middle));\n```\n\n8.  现在，让我们使用`std::front_insert_iterator`在德格的前面插入一些项目:\n\n```cpp\n          initializer_list<int> il2 {-1, -2, -3};\n          copy(begin(il2), end(il2), front_inserter(v));\n```\n\n9.  在最后一步中，我们将整个内容打印到用户外壳中。`std::ostream_iterator`的工作方式类似于输出迭代器，在我们的例子中，它只是将所有从其复制的整数转发到`std::cout`，然后在每个项目后追加`\", \"`:\n\n```cpp\n          copy(begin(v), end(v), ostream_iterator<int>{cout, \", \"});\n          cout << 'n';\n      }\n```\n\n10.  编译并运行程序会产生以下输出。你能识别哪个代码行插入了哪个号码吗？\n\n```cpp\n      $ echo \"1 2 3 4 5\" | ./main\n      -3, -2, -1, 1, 2, 123, 456, 789, 3, 4, 5,\n```\n\n# 它是如何工作的...\n\n在这一节中，我们使用了许多不同的迭代器适配器。它们都有一个共同点，那就是它们将一个对象包装到一个迭代器中，而这个迭代器本身并不是迭代器。\n\n# 标准::back_insert_iterator\n\n`back_insert_iterator`可以缠绕`std::vector`、`std::deque`、`std::list`等。它将调用容器的`push_back`方法，该方法将新项目*插入到现有项目的*之后。如果容器实例不够大，它将自动增长。\n\n# 标准::front_insert_iterator\n\n`front_insert_iterator`做的和`back_insert_iterator`完全一样，但是它调用容器的`push_front`方法，在所有现有的项目之前插入新的项目*。请注意，对于像`std::vector`这样的容器，这意味着所有现有项目都需要再移动一个插槽，以便为前面的新项目留出空间。*\n\n# std::insert_iterator\n\n这个迭代器适配器类似于其他插入器，但是能够在现有项目之间插入新项目*。构造这样一个包装器的`std::inserter`辅助函数需要两个参数。第一个参数是容器，第二个参数是迭代器，它指向要插入新项的位置。*\n\n# std::istream_iterator\n\n`istream_iterator`是另一个非常方便的适配器。它可以与任何`std::istream`对象(例如，可以是标准输入或文件)一起使用，并将尝试根据实例化它的模板参数解析来自该流对象的输入。在本节中，我们使用了`std::istream_iterator<int>(std::cin)`，它为我们从标准输入中提取整数。\n\n溪流的特别之处在于，我们经常无法提前知道溪流有多长。这就留下了一个问题，如果我们不知道流的终点在哪里，那么*迭代器将指向哪里？其工作方式是迭代器*知道*何时到达流的末尾。当它与结束迭代器进行比较时，它将有效地*而不是真正地*将自己与结束迭代器进行比较，而是如果流有任何标记*离开*则返回。这就是为什么结束迭代器构造函数不接受任何参数。*\n\n# std::ostream_iterator\n\n`ostream_iterator`和`istream_iterator`是一样的，但是它的工作原理正好相反:它不从的*输入*流中获取代币*，而是将代币*推入*的*输出*流中。`istream_iterator`的另一个区别是它的构造函数接受第二个参数，这是一个字符串，应该在每个项目之后被推入输出流。这很有用，因为这样我们可以在每个项目后打印一个分隔`\", \"`或一个新行。*\n\n# 根据迭代器实现算法\n\n迭代器通常通过*将*的*位置*从容器的一个项目移动到另一个项目来进行迭代。但是它们根本不需要迭代数据结构。迭代器也可以用来实现算法，在这种情况下，它们会在递增时计算下一个值(`++ it`)并在取消引用时返回该值(`*it`)。\n\n在本节中，我们通过以迭代器的形式实现斐波那契函数来演示这一点。斐波那契函数的递归定义如下:`F(n) = F(n - 1) + F(n - 2)`。它从`F(0) = 0`和`F(1) = 1`的起始值开始。这导致以下编号顺序:\n\n*   `F(0) = 0`\n*   `F(1) = 1`\n*   `F(2) = F(1) + F(0) = 1`\n*   `F(3) = F(2) + F(1) = 2`\n*   `F(4) = F(3) + F(2) = 3`\n*   `F(5) = F(4) + F(3) = 5`\n*   `F(6) = F(5) + F(4) = 8`\n*   ...等等\n\n如果我们以可调用函数的形式实现这一点，该函数返回任意数字的斐波那契值 *n* ，我们将得到递归的自调用函数，或者循环实现。这很好，但是如果我们写一些程序，其中必须以某种模式消耗斐波那契数，一个接一个，会怎么样呢？我们会有两种可能——要么我们重新计算每个新斐波那契数的所有递归调用，这是对计算时间的浪费，要么我们将最后两个斐波那契数保存为临时变量，并使用它们来计算下一个。在后一种情况下，我们重新实现了斐波那契算法循环实现。看起来我们最终会把*的*斐波那契代码和我们的实际代码混合在一起，这解决了一个不同的问题:\n\n```cpp\nsize_t a {0};\nsize_t b {1};\n\nfor (size_t i {0}; i < N; ++ i) {\n    const size_t old_b {b};\n    b += a;\n    a  = old_b;\n\n    // do something with b, which is the current fibonacci number\n}\n```\n\n迭代器是一种有趣的解决方法。如何将我们在基于循环的迭代斐波那契实现中所做的步骤包装在斐波那契值迭代器的前缀增量`++ `运算符实现中？正如本节所展示的，这非常容易。\n\n# 怎么做...\n\n在这一节中，我们集中实现一个迭代器，它在迭代斐波那契数列的同时，从数列中生成数字。\n\n1.  为了能够将斐波那契数打印到终端，我们需要首先包含一个标题:\n\n```cpp\n      #include <iostream>\n```\n\n2.  我们称之为斐波那契迭代器。它将携带一个成员`i`，该成员保存斐波那契序列中的索引位置，`a`和`b`将是保存最后两个斐波那契值的变量。如果用默认构造函数实例化，斐波那契迭代器将被初始化为值`F(0)`:\n\n```cpp\n      class fibit\n      {\n          size_t i {0};\n          size_t a {0};\n          size_t b {1};\n```\n\n3.  接下来，我们定义标准构造函数和另一个构造函数，它允许我们在任何斐波那契数步骤初始化迭代器:\n\n```cpp\n      public:\n          fibit() = default;\n\n          explicit fibit(size_t i_)\n              : i{i_}\n          {}\n```\n\n4.  当解引用我们的迭代器(`*it`)时，它只会发出当前步骤的斐波那契数:\n\n```cpp\n          size_t operator*() const { return b; }\n```\n\n5.  当递增迭代器(`++ it`)时，它会将其状态移动到下一个斐波那契数。该函数包含与基于循环的斐波那契实现相同的代码:\n\n```cpp\n          fibit& operator++() {\n              const size_t old_b {b};\n              b += a;\n              a = old_b;\n              ++ i;\n              return *this;\n          }\n```\n\n6.  在循环中使用时，递增的迭代器与结束迭代器进行比较，为此我们需要定义`!=`运算符。我们只是比较斐波那契迭代器当前所在的*步骤*，这使得为步骤`1000000`定义结束迭代器变得更容易，例如，因为我们不需要提前计算如此高的斐波那契数*:*\n\n```cpp\n          bool operator!=(const fibit &o) const { return i != o.i; }\n      };\n```\n\n7.  为了能够在基于范围的`for`循环中使用斐波那契迭代器，我们必须预先实现一个范围类。我们称之为`fib_range`，它的构造函数将接受一个参数，这个参数告诉我们想要迭代的斐波那契数列范围有多远:\n\n```cpp\n      class fib_range\n      {\n          size_t end_n;\n\n      public:\n          fib_range(size_t end_n_)\n              : end_n{end_n_}\n          {}\n```\n\n8.  其`begin`和`end`函数返回指向位置`F(0)`和`F(end_n)`的迭代器:\n\n```cpp\n          fibit begin() const { return fibit{}; }\n          fibit end()   const { return fibit{end_n}; }\n      };\n```\n\n9.  好了，现在让我们忘掉所有迭代器相关的样板代码。我们不需要再次接触它，因为我们现在有了一个助手类，它很好地向我们隐藏了所有的实现细节！让我们打印前 10 个斐波那契数:\n\n```cpp\n      int main()\n      {\n          for (size_t i : fib_range(10)) {\n              std::cout << i << \", \";\n          }\n          std::cout << 'n';\n      }\n```\n\n10.  编译和运行程序会产生以下 shell 输出:\n\n```cpp\n      1, 1, 2, 3, 5, 8, 13, 21, 34, 55,\n```\n\n# 还有更多...\n\n为了在 STL 中使用这个迭代器，它必须支持`std::iterator_traits`类。要了解如何做到这一点，请看一下*另一个*配方，它正好处理这个问题:*使你自己的迭代器与 STL 迭代器类别兼容。*\n\nTry to think in terms of iterators. This leads to very elegant code in many situations. Don't worry about performance: compilers find it *trivial* to optimize away the iterator-related boilerplate code!\n\n为了保持示例简单，我们对此没有做任何事情，但是如果我们确实将斐波那契迭代器发布为库，就会发现它存在可用性缺陷-使用构造函数参数创建的`fibit`实例将仅用作结束迭代器，因为它不包含有效的斐波那契值。我们小小的图书馆并不强制这种用法。修复它有不同的可能性:\n\n*   将`fibit(size_t i_)`构造函数设为私有，并将`fib_range`类声明为`fibit`类的朋友。这样，用户只能以正确的方式使用它。\n*   使用迭代器哨兵，以防止用户取消引用结束迭代器。看看我们介绍的方法:*用迭代器哨兵*终止范围内的迭代\n\n# 使用反向迭代器适配器反过来迭代\n\n有时候，反过来迭代一个范围是有价值的，不是向前，而是向后。基于范围的`for`循环以及所有的 STL 算法通常通过*递增*迭代器来迭代给定的范围，尽管向后迭代需要*递减*迭代器。当然，也可以将*迭代器包装成一个层，将*的增量*调用有效地转换成*的减量*调用。对于我们想要支持的每一种类型，这听起来都像是大量的样板代码。*\n\nSTL 提供了一个有用的*反向迭代器适配器*，帮助我们设置这样的迭代器。\n\n# 怎么做...\n\n在本节中，我们将以不同的方式使用反向迭代器，只是为了展示它们是如何使用的:\n\n1.  我们需要像往常一样，首先包含一些标题:\n\n```cpp\n      #include <iostream>\n      #include <list>\n      #include <iterator>\n```\n\n2.  接下来，我们声明我们使用名称空间`std`以便节省一些打字时间:\n\n```cpp\n      using namespace std;\n```\n\n3.  为了有东西可以迭代，让我们实例化一个整数列表:\n\n```cpp\n      int main()\n      {\n          list<int> l {1, 2, 3, 4, 5};\n```\n\n4.  现在让我们以相反的形式打印这些整数。为此，我们使用`std::list`的`rbegin`和`rend`函数遍历列表，并使用方便的`ostream_iterator`适配器通过标准输出将这些值推出:\n\n```cpp\n          copy(l.rbegin(), l.rend(), ostream_iterator<int>{cout, \", \"});\n          cout << 'n';\n```\n\n5.  如果容器不提供便利的`rbegin`和`rend`函数，但至少提供双向迭代器，`std::make_reverse_iterator`函数会有所帮助。它接受*普通*迭代器，并将其转换为*反向*迭代器:\n\n```cpp\n          copy(make_reverse_iterator(end(l)),\n               make_reverse_iterator(begin(l)),\n               ostream_iterator<int>{cout, \", \"});\n          cout << 'n';\n      }\n```\n\n6.  编译和运行我们的程序会产生以下输出:\n\n```cpp\n      5, 4, 3, 2, 1, \n      5, 4, 3, 2, 1,\n```\n\n# 它是如何工作的...\n\n为了能够将普通迭代器转换为反向迭代器，它必须至少支持双向迭代。这个要求由*双向*类或更高的迭代器来满足。\n\n一种反向迭代器类型*包含*一个普通迭代器，*完全模仿*它的接口，但是它*将*的增量操作重新连接为减量操作。\n\n下一个细节是关于迭代器的开始和结束位置。让我们看一下下图，它显示了一个保持在可迭代范围内的标准数字序列。如果序列从`1`到`5`，那么开始迭代器必须指向元素`1`，结束迭代器必须指向一个元素经过`5`:\n\n![](img/5d43dc84-63ca-4492-ad21-1e3278268727.png)\n\n定义反向迭代器时，`rbegin`迭代器必须指向`5`，而`rend`迭代器必须指向 `1`之前的元素*。把这本书翻过来，看它是否完全有意义。*\n\n如果我们想要自己的定制容器类支持反向迭代，我们不需要自己实现所有这些细节；我们只需使用`std::make_reverse_iterator`助手函数将正常迭代器包装成反向迭代器，它就可以为我们完成所有的运算符重新布线和偏移校正。\n\n# 用迭代器哨兵终止范围内的迭代\n\nSTL 算法和基于范围的`for`循环都假设迭代的开始和结束位置是预先已知的*。然而，在某些情况下，很难在迭代到达之前知道终点位置*。**\n\n *一个非常简单的例子是迭代普通的 C 风格字符串，其长度在*运行时*之前是未知的。迭代这些字符串的代码通常如下所示:\n\n```cpp\nfor (const char *c_ponter = some_c_string; *c_pointer != ''; ++ c_pointer) {\n    const char c = *c_pointer;\n    // do something with c\n}\n```\n\n将此放入基于范围的`for`循环的唯一方法似乎是将其包装到`std::string`中，该循环具有`begin()`和`end()`功能:\n\n```cpp\nfor (char c : std::string(some_c_string)) { /* do something with c */ }\n```\n\n然而，`std::string`的构造函数将在我们的`for`循环能够迭代整个字符串之前迭代它。从 C++ 17 开始，我们也有`std::string_view`，但是它的构造函数也会遍历字符串一次。对于*短*弦来说，这并不值得真正的麻烦，但这也只是一个问题*类的例子，*在*其他情况*下可以值得麻烦。当`std::istream_iterator`捕捉到来自`std::cin`的输入时，它也必须处理这个问题，因为当用户仍在输入*键时，它的结束迭代器不能真实地指向用户输入的结束。*\n\nC++ 17 带来了一个好消息，它不要求开始迭代器和结束迭代器是同一类型的。本节演示如何将这个*小规则变化*变成*大使用*。\n\n# 怎么做...\n\n在这一节中，我们将与 range 类一起构建一个迭代器，它使我们能够迭代长度未知的字符串，而无需提前找到*结束*位置*。*\n\n *1.  首先，一如既往，我们需要包含标题:\n\n```cpp\n      #include <iostream>\n```\n\n2.  迭代器 sentinel 是这一部分的核心元素。令人惊讶的是，它的类定义可以保持完全空白:\n\n```cpp\n      class cstring_iterator_sentinel {};\n```\n\n3.  现在我们实现迭代器。它将包含一个字符串指针，也就是我们迭代的*容器*:\n\n```cpp\n      class cstring_iterator {\n          const char *s {nullptr};\n```\n\n4.  构造函数只是将内部字符串指针初始化为用户提供的任何字符串。让我们将构造函数显式化，以防止从字符串到字符串迭代器的意外隐式转换:\n\n```cpp\n      public:\n          explicit cstring_iterator(const char *str)\n              : s{str}\n          {}\n```\n\n5.  当迭代器在某个点上解引用时，它将只返回这个位置的字符值:\n\n```cpp\n          char operator*() const { return *s; }\n```\n\n6.  递增迭代器只会增加字符串中的位置:\n\n```cpp\n          cstring_iterator& operator++() {\n              ++ s;\n              return *this;\n          }\n```\n\n7.  这是有趣的部分。我们实现`!=`运算符进行比较，因为它被 STL 算法和基于范围的`for`循环使用。但是，这一次，我们实现它不是为了将迭代器与其他*迭代器*进行比较，而是为了将迭代器与*哨兵*进行比较。当我们比较一个迭代器和另一个迭代器时，我们只能检查它们的内部字符串指针是否都指向同一个地址，这有点限制。通过与一个空的 sentinel 对象进行比较，我们可以执行一个完全不同的语义——我们检查迭代器指向的字符是否是一个终止的`''`字符，因为这代表了字符串的*结尾*！\n\n```cpp\n          bool operator!=(const cstring_iterator_sentinel) const {\n              return s != nullptr && *s != '';\n          }\n      };\n```\n\n8.  为了在基于范围的`for`循环中使用它，我们需要一个范围类，它发出开始和结束迭代器:\n\n```cpp\n      class cstring_range {\n          const char *s {nullptr};\n```\n\n9.  用户在实例化过程中唯一需要提供的是将被迭代的字符串:\n\n```cpp\n      public:\n          cstring_range(const char *str)\n              : s{str}\n          {}\n```\n\n10.  我们从`begin()`函数返回一个正常的`cstring_iterator`，它指向字符串的开头。从`end()`功能，我们只需返回*哨兵类型*。请注意，如果没有 sentinel 类型，我们还会返回一个迭代器，但是如果我们没有提前搜索它，我们应该从哪里知道字符串的结尾呢？\n\n```cpp\n          cstring_iterator begin() const { \n              return cstring_iterator{s}; \n          }\n          cstring_iterator_sentinel end() const { \n              return {}; \n          }\n      };\n```\n\n11.  就这样。我们可以立即使用它。来自用户的字符串是我们无法预先知道长度的输入的一个例子。为了迫使用户给出一些输入，如果用户在 shell 中启动程序时没有提供至少一个参数，我们将中止程序:\n\n```cpp\n      int main(int argc, char *argv[])\n      {\n          if (argc < 2) {\n              std::cout << \"Please provide one parameter.n\";\n              return 1;\n          }\n```\n\n12.  如果程序到现在还在执行，那么我们知道`argv[1]`包含一些用户字符串:\n\n```cpp\n          for (char c : cstring_range(argv[1])) {\n              std::cout << c;\n          }\n          std::cout << 'n';\n      }\n```\n\n13.  编译和运行程序会产生以下输出:\n\n```cpp\n      $ ./main \"abcdef\"\n      abcdef\n```\n\n循环打印我们刚刚输入的内容并不奇怪，因为这只是基于哨兵的迭代器范围实现的一个很小的例子。这种迭代终止方法将帮助您实现自己的迭代器，无论您在哪里遇到*与结束位置*的比较没有帮助的情况。\n\n# 使用已检查的迭代器自动检查迭代器代码\n\n不管迭代器有多有用，代表什么样的泛型接口，迭代器都很容易被*误用*，就像指针一样。当处理指针时，代码必须以这样一种方式编写:当它们指向无效的内存位置时，它*永远不会*去引用它们。迭代器也是如此，但是*有很多规则*规定迭代器什么时候有效，什么时候无效。通过稍微研究一下 STL 文档就可以很容易地了解到这些，但是仍然有可能编写有问题的代码。\n\n在最好的情况下，这种有缺陷的代码会在*开发人员*面前爆炸，而此时它正在*测试*，而不是在客户端机器上测试*。然而，在许多情况下，代码似乎只是默默工作，尽管它取消了悬空指针、迭代器等等的引用。在这种情况下，如果我们生成显示未定义行为的代码，我们希望成为*急切的警报*。*\n\n *幸好有帮手！GNU STL 实现有一个*调试模式*，GNU C++ 编译器以及 LLVM clang C++ 编译器都支持*附加库*，可以用来为我们生成*超敏感的*和*冗长的*二进制文件，这些文件会立即被各种各样的 bug 炸毁。这是*好用的*和*超级好用的*，我们会在这一节演示。微软 Visual C++ 标准库还提供了激活附加检查的可能性。\n\n# 怎么做...\n\n在本节中，我们将编写一个程序，故意访问无效的迭代器:\n\n1.  首先，我们包括标题。\n\n```cpp\n      #include <iostream>\n      #include <vector>\n```\n\n2.  现在，让我们实例化一个整数向量，并获得第一项的迭代器，值`1`。我们在向量上应用`shrink_to_fit()`是为了确保其容量确实是*`3`，因为其实现*可能会*分配比必要更多的内存作为一点储备，以使未来的项目插入更快:*\n\n```cpp\n      int main()\n      {\n          std::vector<int> v {1, 2, 3};\n          v.shrink_to_fit();\n\n          const auto it (std::begin(v));\n```\n\n3.  然后我们打印取消引用的迭代器，这完全没问题:\n\n```cpp\n          std::cout << *it << 'n';\n```\n\n4.  接下来，让我们给向量加上一个新的数字。由于向量不够大，无法接受另一个数字，它将自动增加其大小。它通过分配一个新的更大的内存块，将所有现有的项目移动到新的内存块，然后删除旧的内存块来实现:\n\n```cpp\n          v.push_back(123);\n```\n\n5.  现在，让我们通过这个迭代器再次打印向量中的`1`。这很糟糕。为什么呢？当向量将其所有值移动到新的内存块并丢弃旧的内存块时，它没有告诉迭代器这个变化。这意味着迭代器仍然指向旧的位置，我们无法知道从那以后它到底发生了什么:\n\n```cpp\n          std::cout << *it << 'n'; // bad bad bad!\n      }\n```\n\n6.  Compiling and running this program leads to a flawless execution. The app doesn't crash, but what it prints when dereferencing the invalidated pointer is pretty much random. Leaving it like this is pretty dangerous, but at this point, no one tells us about that bug if we don't see it ourselves:\n\n    ![](img/a81a4f4e-651a-463e-9a47-9f537f7ef4a6.png)\n\n7.  Debug flags come to the rescue! The *GNU* STL implementation supports a preprocessor macro called `_GLIBCXX_DEBUG`, which activates a lot of sanity checking code in the STL. This makes the program slower, but it *finds bugs*. We can activate it by adding a `-D_GLIBCXX_DEBUG` flag to our compiler command line, or define it in the head of the code file before the `include` lines. As you can see, it kills the app in the mactivate different sanitizers. Let's compile the code with clan useful (the activation flag for checked iterators with the Microsoft Visual C++ compiler is `/D_ITERATOR_DEBUG_LEVEL=1`):\n\n    ![](img/abbbeeff-f181-453d-b88f-a5d9477b5b2c.png)\n\n8.  The LLVM/clang implementation of the STL also has debug flags, but they serve the purpose of debugging *the STL* itself, not user code. For user code, you can activate different sanitizers. Let's compile the code with clang using the `-fsanitize=address -fsanitize=undefined` flags and see what happens:\n\n    ![](img/907bccd5-a08c-449e-b970-98f763f65587.png)\n\n*哇*这是对哪里出了问题的非常精确的描述。如果截屏没有被截断的话，它将会跨越这本书的*多个页面*。请注意，这不是一个哗众取宠的特性，因为它也适用于 GCC。\n\nIf you get runtime errors because some library is missing, then your compiler did not automatically ship with **libasan** and **libubsan**. Try to install them via your package manager or something similar.\n\n# 它是如何工作的...\n\n正如我们已经看到的，我们不需要在代码中改变任何东西来获得这种用于错误代码的*绊线*特性。它基本上是免费的*，只是在编译程序时在命令行上附加了一些编译器标志。*\n\n *该功能由*消毒剂*实现。杀毒软件通常由一个额外的编译器模块和一个运行时库组成。当杀毒程序被激活时，编译器会将*附加的* *信息*和*代码*添加到二进制文件中，这是我们程序的结果。在运行时，然后链接到程序二进制文件中的消毒库可以，例如，替换`malloc`和`free`函数，以便*分析*程序如何处理它获取的内存。\n\n消毒剂可以检测不同种类的细菌。仅举几个有价值的例子:\n\n*   **越界**:每当我们访问一个数组、向量或任何类似的超出其合法内存范围的东西时，就会触发这个事件。\n*   **释放后使用**:如果我们在堆内存被释放后引用堆内存，就会触发这个事件(我们在本节中已经这样做了)。\n*   **整数溢出**:如果一个整数变量通过计算不适合该变量的值而溢出，则会触发该事件。对于有符号整数，算术回绕是未定义的行为。\n*   **指针对齐**:有些架构如果内存中有奇怪的对齐，就不能访问内存。\n\n消毒剂可以检测到更多这样的错误。\n\n*不可行**总是*激活所有可用的消毒剂，因为它们会使程序*变慢*。但是，在您的*单元测试*和*集成测试*中始终激活消毒剂是一种好的风格。\n\n# 还有更多...\n\n针对不同的 bug 类别有很多不同的杀毒软件，目前都还在开发中。我们可以也应该在互联网上告知自己如何改进我们的测试二进制文件。GCC 和 LLVM 项目主页在其在线文档页面中列出了它们的消毒功能:\n\n*   [https://gcc . GNU . org/online docs/gcc/Instrumentation-options . html](https://gcc.gnu.org/onlinedocs/gcc/Instrumentation-Options.html)\n*   [http://clang.llvm.org/docs/index.html](http://clang.llvm.org/docs/index.html)(在目录中寻找*消毒剂*\n\n每一个程序员都应该意识到，并且*也应该一直这样做。不幸的是，令人担忧的是，许多公司并非如此，尽管错误代码是所有 T4 恶意软件和计算机病毒最重要的入口。*\n\n当你得到一份软件开发人员的新工作时，检查你的团队是否真的使用了所有可能的消毒方法。如果没有，你有独特的机会在上班的第一天修复重要的偷偷摸摸的 bug！\n\n# 构建自己的 zip 迭代器适配器\n\n不同的编程语言导致不同的编程风格。这是因为有不同的方式来表达事物，并且它们对于每个用例的优雅程度是不同的。这并不奇怪，因为每种语言都有特定的目标。\n\n一种非常特殊的编程风格是*纯* *函数编程*。它神奇地不同于 C 或 C++ 程序员习惯的*命令式*编程。虽然这种风格非常不同，但它在许多情况下都支持极其优雅的代码。\n\n这种优雅的一个例子是公式的实现，例如数学点积。给定两个数学向量，对它们应用点积意味着将向量中相同位置的数字成对相乘，然后将所有相乘的值相加。两个向量的点积`(a, b, c) * (d, e, f)`为`(a * e + b * e + c * f)`。当然，我们也可以用 C 和 C++ 做到这一点。它可能如下所示:\n\n```cpp\nstd::vector<double> a {1.0, 2.0, 3.0};\nstd::vector<double> b {4.0, 5.0, 6.0};\n\ndouble sum {0};\nfor (size_t i {0}; i < a.size(); ++ i) {\n    sum += a[i] * b[i];\n}\n// sum = 32.0\n```\n\n在那些可以认为*更优雅的*语言中是什么样子的？\n\n*Haskell* 是一种纯函数式语言，这就是如何用神奇的一行程序计算两个向量的点积:\n\n![](img/8fe01ef4-1b08-4026-b098-566e94367867.png)\n\n*Python* 不是一种纯粹的函数式语言，但它在某种程度上支持类似的模式，如下例所示:\n\n![](img/ae8dfd8b-886e-4c0f-9226-80d8f1619286.png)\n\nSTL 提供了一个称为`std::inner_product`的特定算法，它也在一行中解决了这个特定的问题。但关键是，在许多其他语言中，这样的代码只需在一行*中快速编写*，而无需*特定的库函数来支持这一确切目的。*\n\n *没有深入研究这种外来语法的解释，这两个例子中的一个重要共性是神奇的`zip`函数。它是做什么的？它采用两个向量`a`和`b`，并将其转换为一个*混合*向量。例如:`[a1, a2, a3]`和`[b1, b2, b3]`拉上拉链后会产生`[ (a1, b1), (a2, b2), (a3, b3) ]`。仔细看看；这真的很像拉链的工作原理！\n\n相关的一点是，现在有可能在一个组合范围内迭代*，在该范围内可以进行成对乘法，然后求和为一个累加器变量。Haskell 和 Python 示例中也发生了完全相同的情况，没有添加任何循环或索引变量噪声。*\n\n *不可能使 C++ 代码完全像 Haskell 或 Python 中那样优雅和通用，但是本节解释了如何使用迭代器实现类似的魔法，通过实现一个 *zip 迭代器*。计算两个向量的点积的示例问题由特定的库更优雅地解决，这超出了本书的范围。然而，本节试图展示基于迭代器的库通过提供极其通用的构建块在编写富于表现力的代码方面有多大帮助。\n\n# 怎么做...\n\n在本节中，我们将重新创建哈斯克尔或 Python 中的 *zip* 函数。它将被硬编码到`double`变量的向量中，以避免干扰迭代器机制:\n\n1.  首先，我们需要包含一些标题:\n\n```cpp\n      #include <iostream>\n      #include <vector>\n      #include <numeric>\n```\n\n2.  接下来，我们定义`zip_iterator`类。当迭代一个`zip_iterator`范围时，我们将在每个迭代步骤从两个容器中获得一对值。这意味着我们同时迭代两个容器:\n\n```cpp\n      class zip_iterator {\n```\n\n3.  zip 迭代器需要保存两个迭代器，每个容器一个迭代器:\n\n```cpp\n          using it_type = std::vector<double>::iterator;\n\n          it_type it1;\n          it_type it2;\n```\n\n4.  构造函数只是保存我们想要迭代的两个容器中的迭代器:\n\n```cpp\n      public:\n          zip_iterator(it_type iterator1, it_type iterator2)\n              : it1{iterator1}, it2{iterator2}\n          {}\n```\n\n5.  递增 zip 迭代器意味着递增两个成员迭代器:\n\n```cpp\n          zip_iterator& operator++() {\n              ++ it1;\n              ++ it2;\n              return *this;\n          }\n```\n\n6.  如果两个成员迭代器与另一个 zip 迭代器中的成员迭代器不相等，则两个 zip 迭代器不相等。通常，人们会使用逻辑或(`||`)来代替和(`&&`)，但是假设范围的长度不相等。在这种情况下，不可能同时匹配*和*两个结束迭代器。这样，当我们到达*或*范围的*第一个*结束迭代器时，我们可以中止循环:\n\n```cpp\n          bool operator!=(const zip_iterator& o) const {\n              return it1 != o.it1 && it2 != o.it2;\n          }\n```\n\n7.  等式比较运算符只是使用另一个运算符实现的，但是否定了结果:\n\n```cpp\n          bool operator==(const zip_iterator& o) const {\n              return !operator!=(o);\n          }\n```\n\n8.  取消对 zip 迭代器的引用可以在相同的位置访问两个容器的元素:\n\n```cpp\n          std::pair<double, double> operator*() const {\n              return {*it1, *it2};\n          }\n      };\n```\n\n9.  这是迭代器代码。我们需要使迭代器与 STL 算法兼容，因此我们为此定义了所需的类型特征样板代码。它基本上说这个迭代器只是一个前向迭代器，当被取消引用时，它会返回成对的双精度值。虽然我们在这个配方中没有使用`difference_type`，但是 STL 的不同实现可能需要它来编译:\n\n```cpp\n      namespace std {\n\n      template <>\n      struct iterator_traits<zip_iterator> {\n         using iterator_category = std::forward_iterator_tag;\n         using value_type = std::pair<double, double>;\n         using difference_type = long int;\n      };\n\n      }\n```\n\n10.  下一步是定义一个范围类，从它的`begin`和`end`函数返回 us zip 迭代器:\n\n```cpp\n      class zipper {\n          using vec_type = std::vector<double>;\n          vec_type &vec1;\n          vec_type &vec2;\n```\n\n11.  它需要引用两个现有的容器，以便从它们形成 zip 迭代器:\n\n```cpp\n      public:\n          zipper(vec_type &va, vec_type &vb)\n              : vec1{va}, vec2{vb}\n          {}\n```\n\n12.  `begin`和`end`函数只是馈送成对的开始和结束指针，以便从那里构造 zip 迭代器实例:\n\n```cpp\n          zip_iterator begin() const { \n              return {std::begin(vec1), std::begin(vec2)}; \n          }\n          zip_iterator end() const { \n              return {std::end(vec1), std::end(vec2)}; \n          }\n      };\n```\n\n13.  就像在 Haskell 和 Python 的例子中一样，我们定义了两个`double`值的向量。我们还定义了默认情况下在主函数中使用名称空间`std`:\n\n```cpp\n      int main()\n      {\n          using namespace std;\n          vector<double> a {1.0, 2.0, 3.0};\n          vector<double> b {4.0, 5.0, 6.0};\n```\n\n14.  拉链对象将它们组合成一个类似矢量的范围，在该范围内我们可以看到成对的`a`和`b`值:\n\n```cpp\n          zipper zipped {a, b};\n```\n\n15.  我们将使用`std::accumulate`将该范围的所有项目相加。我们不能直接这样做，因为这意味着我们总结了`std::pair<double, double>`的实例，对于这些实例，总和的概念没有定义。因此，我们将定义一个 helper lambda，它取一对，将其成员相乘，并将其添加到累加器中。`std::accumulate`非常适合带有这样一个签名的 lambdas:\n\n```cpp\n          const auto add_product ([](double sum, const auto &p) {\n             return sum + p.first * p.second;\n          });\n```\n\n16.  现在，我们将其与压缩范围的开始和结束迭代器对以及累加器变量的开始值`0.0`一起输入到`std::accumulate`，最终，累加器变量包含乘积之和:\n\n```cpp\n          const auto dot_product (accumulate(\n                  begin(zipped), end(zipped), 0.0, add_product));\n```\n\n17.  让我们打印点积结果:\n\n```cpp\n          cout << dot_product << 'n';\n      }\n```\n\n18.  编译和运行程序会产生正确的结果:\n\n```cpp\n      32\n```\n\n# 还有更多...\n\n好吧，对于一点语法糖来说，这是一个很大的工作量，它仍然没有哈斯克尔代码那样优雅。一个很大的缺陷是我们的小 zip 迭代器的硬编码特性——它只适用于双变量的`std::vector`范围。有了一点模板代码和一些类型特征，拉链可以做得更通用。这样，它可以组合列表和向量，或者去量化和映射，即使这些是专门针对完全不同的容器项目类型。\n\n为了真正正确地使这样的类通用，所需要的工作量和思想是不可低估的。幸运的是，这样的库已经存在。一个流行的非 STL 库是*增强* `zip_iterator`。它非常通用且易于使用。\n\n顺便说一句，如果你来这里是为了看用 C++ 做一个*点积*的最优雅的方法，而不是真的关心 zip-iterators 的概念，你应该看看`std::valarray`。自己看吧:\n\n```cpp\n#include <iostream>\n#include <valarray>\n\nint main()\n{\n    std::valarray<double> a {1.0, 2.0, 3.0};\n    std::valarray<double> b {4.0, 5.0, 6.0};\n\n    std::cout << (a * b).sum() << 'n';\n}\n```\n\n# 范围库\n\n有一个非常非常有趣的 C++ 库，它支持拉链和所有其他种类的魔法迭代器适配器、过滤器等等:*范围*库。它的灵感来自 Boost 范围库，在一段时间内，它看起来会进入 C++ 17，但不幸的是，我们将不得不等待*下一个*标准。之所以如此不幸，是因为它将通过从*泛型*和*简单的*代码块组成*复杂的*功能，大力*提高用 C++ 编写*表达性*和*快速*代码的可能性。*\n\n它的文档中有一些非常简单的例子:\n\n1.  计算从`1`到`10`所有数字的平方和:\n\n```cpp\n      const int sum = accumulate(view::ints(1)\n                               | view::transform([](int i){return i*i;})\n                               | view::take(10), 0);\n```\n\n2.  从一个数值向量中过滤掉所有不均匀的数字，并将其余的转换成字符串:\n\n```cpp\n      std::vector<int> v {1,2,3,4,5,6,7,8,9,10};\n\n      auto rng = v | view::remove_if([](int i){return i % 2 == 1;})\n                   | view::transform([](int i){return std::to_string(i);});\n\n      // rng == {\"2\"s,\"4\"s,\"6\"s,\"8\"s,\"10\"s};\n```\n\n如果你感兴趣并且等不及下一个 C++ 标准，可以看看[https://ericniebler.github.io/range-v3/](https://ericniebler.github.io/range-v3/)的范围文档。*********"
  },
  {
    "path": "docs/exp-cpp-prog/03.md",
    "content": "# 三、lambda 表达式\n\n我们将在本章介绍以下食谱:\n\n*   使用 lambda 表达式在运行时定义函数\n*   通过在`std::function`中包裹羔羊肉来增加多晶型\n*   通过串联组合函数\n*   使用逻辑连接创建复杂谓词\n*   用同一个输入调用多个函数\n*   使用`std::accumulate`和 lambdas 实现`transform_if`\n*   在编译时生成任何输入的笛卡尔乘积对\n\n# 介绍\n\nC++ 11 的一个重要新特性是**lambda 表达式**。在 C++ 14 和 C++ 17 中，lambda 表达式得到了一些新的增加，这使得它们更加强大。但是首先，*是什么*一个 lambda 表达式？\n\nLambda 表达式或 lambda 函数构造闭包。闭包是*未命名对象*的一个非常通用的术语，它可以像函数一样被*称为*。为了在 C++ 中提供这样的能力，这样的对象必须实现`()`函数调用运算符，无论有无参数。在 C++ 11 之前构建这样一个没有 lambda 表达式的对象可能仍然如下所示:\n\n```cpp\n#include <iostream>\n#include <string>\n\nint main() {\n    struct name_greeter {\n        std::string name;\n\n        void operator()() {\n            std::cout << \"Hello, \" << name << 'n'; \n        }\n    };\n\n    name_greeter greet_john_doe {\"John Doe\"};\n    greet_john_doe();\n}\n```\n\n`name_greeter`结构的实例显然带有一个字符串。请注意，这个结构类型和实例都不是未命名的，但是 lambda 表达式可以，正如我们将看到的。就闭包而言，我们会说它们*捕获了*一个字符串。当示例实例像没有参数的函数一样被调用时，它会打印`\"Hello, John Doe\"`，因为我们用这个名称构造了它。\n\n自从 C++ 11 以来，创建这样的闭包变得更加容易:\n\n```cpp\n#include <iostream>\n\nint main() {\n    auto greet_john_doe ([] {\n        std::cout << \"Hello, John Doen\"; \n    });\n\n    greet_john_doe();\n}\n```\n\n就这样。整个结构`name_greeter`被一个小的`[] { /* do something */ }`结构代替，一开始看起来可能有点像魔法，但是本章的第一节将在所有可能的变体中彻底解释它。\n\nLambda 表达式对使代码*通用*和*整洁*有很大帮助。它们可以用作非常通用的算法的参数，以便在处理特定的用户定义类型时专门化这些算法。它们还可以用来将工作包和数据包装在一起，以便在线程中运行，或者只是为了保存工作并推迟实际执行。自从 C++ 11 问世以来，越来越多的库使用 lambda 表达式，因为它们已经成为 C++ 中非常自然的事情。另一个用例是元编程，因为 lambda 表达式也可以在编译时计算。然而，我们并没有深入到*那个*方向，因为这将很快冲击这本书的范围。\n\n这一章确实严重依赖一些*函数式编程*模式，对于已经有经验但不熟悉这些模式的新手或程序员来说，这可能看起来很奇怪。如果您在即将到来的返回 lambda 表达式的食谱中看到 lambda 表达式，这些表达式再次返回 lambda 表达式，请不要太快感到沮丧或困惑。我们有点突破界限，以便为现代 C++ 做好准备，在现代 c++ 中，函数式编程模式越来越有规律地出现。如果下面食谱中的一些代码看起来有点太复杂，请慢慢理解。一旦你解决了这个问题，现实项目中复杂的 lambda 表达式就不会再让你困惑了。\n\n# 使用 lambda 表达式在运行时定义函数\n\n有了 lambda 表达式，我们可以封装代码以便以后调用它，这也可能在其他地方，因为我们可以复制它们。我们也可以封装代码，用稍微不同的参数多次调用它，而不必为该任务实现一个全新的函数类。\n\nlambda 表达式的语法在 C++ 11 中确实是新的，在 C++ 17 之前，它随着接下来的两个标准版本略有发展。在本节中，我们将看到 lambda 表达式可能是什么样子以及它们的含义。\n\n# 怎么做...\n\n我们将编写一个小程序，在其中使用 lambda 表达式，以便获得对它们的感觉:\n\n1.  Lambda 表达式不需要任何库支持，但是我们将向终端写入消息并使用字符串，因此我们需要这样的头:\n\n```cpp\n      #include <iostream>\n      #include <string>\n```\n\n2.  这次一切都发生在主函数中。我们定义了两个不带参数的函数对象，并用值`1`和`2`返回整数常数。请注意，return 语句被花括号`{}`包围，就像在普通函数中一样，表示无参数函数的`()`括号是*可选的，*我们不在第二个 lambda 表达式中提供它们。但是`[]`括号必须存在:\n\n```cpp\n      int main()\n      {\n          auto just_one ( [](){ return 1; } );\n          auto just_two ( []  { return 2; } );\n```\n\n3.  现在，我们可以通过写下保存变量的名称并附加括号来调用这两个函数对象。在这一行中，对于读者来说，它们与*正常功能*没有区别:\n\n```cpp\n          std::cout << just_one() << \", \" << just_two() << 'n';\n```\n\n4.  现在让我们忘掉这些，定义另一个函数对象，它被称为`plus`，因为它接受两个参数并返回它们的和:\n\n```cpp\n          auto plus ( [](auto l, auto r) { return l + r; } );\n```\n\n5.  这也很容易使用，就像任何其他二进制函数一样。因为我们将其参数定义为`auto`类型，所以它将与定义加号运算符`+`的任何东西一起工作，就像字符串一样:\n\n```cpp\n          std::cout << plus(1, 2) << 'n';\n          std::cout << plus(std::string{\"a\"}, \"b\") << 'n';\n```\n\n6.  我们不需要在变量中存储 lambda 表达式来使用它。我们也可以在适当的地方定义它*，然后将参数写在它后面的括号中`(1, 2)`:*\n\n```cpp\n          std::cout \n            << [](auto l, auto r){ return l + r; }(1, 2) \n            << 'n';\n```\n\n7.  接下来，我们将定义一个带有整数计数器值的闭包。每当我们调用它时，它都会增加计数器值并返回新值。为了告诉它有一个内部计数器变量，我们在括号内写`count = 0`告诉它有一个初始化为整数值`0`的变量`count`。为了允许它修改自己捕获的变量，我们使用`mutable`关键字，因为编译器不允许这样做:\n\n```cpp\n          auto counter (\n              [count = 0] () mutable { return ++ count; }\n          );\n```\n\n8.  现在，让我们调用函数对象五次，并打印它返回的值，这样我们就可以在后面看到不断增加的数值:\n\n```cpp\n          for (size_t i {0}; i < 5; ++ i) {\n              std::cout << counter() << \", \";\n          }\n          std::cout << 'n';\n```\n\n9.  我们也可以通过*引用*来获取现有变量，而不是给闭包一个自己的值副本。这样，捕获的变量可以通过闭包来增加，但是它仍然可以在外部访问。为了做到这一点，我们在括号之间写下`&a`，其中`&`表示我们只存储变量的*引用*，而不是*副本*:\n\n```cpp\n          int a {0};\n          auto incrementer ( [&a] { ++ a; } );\n```\n\n10.  如果这样的话，那么我们应该可以多次调用这个函数对象，然后观察它是否真的改变了变量`a`的值:\n\n```cpp\n          incrementer();\n          incrementer();\n          incrementer();\n\n          std::cout \n            << \"Value of 'a' after 3 incrementer() calls: \" \n            << a << 'n';\n```\n\n11.  最后一个例子是*巴结*。Currying 是指我们取一个可以接受某些参数的函数，存储在另一个函数对象中，这个函数对象接受的*参数比*参数少。在这种情况下，我们存储`plus`功能，只接受*一个*参数，我们将其转发给`plus`功能。另一个参数是值`10`，我们保存在函数对象中。这样，我们得到一个函数，我们称之为`plus_ten`，因为它可以将该值添加到它接受的单个参数中:\n\n```cpp\n          auto plus_ten ( [=] (int x) { return plus(10, x); } );\n          std::cout << plus_ten(5) << 'n';\n      }\n```\n\n12.  在编译和运行程序之前，再次检查代码，并尝试预见它将打印到终端上的内容。然后运行它，并检查实际输出:\n\n```cpp\n      1, 2\n      3\n      ab\n      3\n      1, 2, 3, 4, 5, \n      Value of a after 3 incrementer() calls: 3\n      15\n```\n\n# 它是如何工作的...\n\n我们刚才做的事情并不太复杂——我们添加了数字，然后递增并打印出来。我们甚至用一个函数对象来连接字符串，这个函数对象被实现来累加数字。但是对于那些还不知道 lambda 表达式语法的人来说，它可能看起来很混乱。\n\n那么，让我们先来看看 lambda 表达式的所有特性:\n\n![](img/8d0ec8da-5bcf-4a59-945e-35aeb40addfe.png)\n\n在一般情况下，我们通常可以省略其中的大部分，这样可以节省一些打字时间。最短的 lambda 表达式可能是`[]{}`。它不接受任何参数，不捕捉任何东西，本质上*什么也不做*。\n\n那么剩下的是什么意思呢？\n\n# 捕获列表\n\n指定我们是否捕获以及捕获什么。这样做有几种形式。有两种懒惰的变体:\n\n*   如果我们写`[=] () {...}`，我们通过值从外部捕获闭包引用的每个变量，这意味着值是*复制的*\n*   写`[&] () {...}`意味着闭包引用外部的所有东西都只被*引用*捕获，而*不会*导致复制。\n\n当然，我们可以为每个变量单独设置捕获设置。写`[a, &b] () {...}`意味着，我们通过*值*捕捉变量`a`，通过*引用*捕捉`b`。这是更多的打字工作，但通常这样冗长会更安全，因为我们不能意外地从外部捕获我们不想捕获的东西。\n\n在配方中，我们定义了一个 lambda 表达式，如下所示:`[count=0] () {...}`。在这种特殊情况下，我们没有从外部捕获任何变量，而是定义了一个新的变量`count`。它的类型是从我们初始化它的值推导出来的，即`0`，所以它是一个`int`。\n\n也可以通过值捕获一些变量，通过引用捕获一些变量，如:\n\n*   `[a, &b] () {...}`:通过复制捕捉`a`，通过引用捕捉`b`。\n*   `[&, a] () {...}`:通过复制捕获`a`，通过引用捕获任何其他使用的变量。\n*   `[=, &b, i{22}, this] () {...}`:通过引用捕获`b`，通过复制捕获`this`，用值`22`初始化新变量`i`，通过复制捕获任何其他使用的变量。\n\nIf you try to capture a member variable of an object, you cannot do this directly using `[member_a] () {...}`. Instead, you have to capture either `this` or `*this`.\n\n# 可变的(可选)\n\n如果函数对象应该能够*修改*通过复制* ( `[=]`)捕捉到的*变量，则必须定义`mutable`。这包括调用捕获对象的非常数方法。\n\n# constexpr （可选）\n\n如果我们将 lambda 表达式显式标记为`constexpr`，如果它不满足`constexpr`函数的标准，编译器将会*出错*出来。`constexpr`函数和 lambda 表达式的优点是，如果用编译时常量参数调用它们，编译器可以在编译时评估它们的结果。这导致二进制文件中的代码较少。\n\n如果我们没有显式地将 lambda 表达式声明为`constexpr`但是它符合这个要求，那么无论如何它都将隐式地为`constexpr`*。如果我们希望 lambda 表达式成为 T2 表达式，显式是有帮助的，因为如果我们做错了 T7，编译器会通过出错来帮助我们。*\n\n *# 异常属性(可选)\n\n这是指定函数对象在被调用并遇到错误情况时是否可以抛出异常的地方。\n\n# 返回类型(可选)\n\n如果我们想最终控制返回类型，我们可能不希望编译器自动为我们推导它。在这种情况下，我们可以直接写`[] () -> Foo {}`，告诉编译器我们真的会一直返回`Foo`类型。\n\n# 通过将 lambdas 封装到 std::函数中来添加多态\n\n假设我们想为某种值编写一个观察者函数，它有时可能会改变，然后通知其他对象；比如气压指示器，或者股票价格，或者类似的东西。每当值改变时，应该调用一个观察者对象列表，然后这些对象做出反应。\n\n为了实现这一点，我们可以在一个向量中存储一系列的观测器函数对象，这些对象都接受一个`int`变量作为参数，它代表观测值。我们不知道这些函数对象在用新值调用时具体会做什么，但我们也不在乎。\n\n函数对象的向量是什么类型的？如果我们用签名如`void f(int);`捕获指向*函数*的指针，那么`std::vector<void (*)(int)>`类型将是正确的。这确实也适用于任何不捕捉任何变量的 lambda 表达式，例如`[](int x) {...}`。但是一个能够捕捉某些东西的 lambda 表达式实际上是一个与普通函数完全不同的类型，因为它不仅仅是一个函数指针。是一个*对象*把一定量的数据和一个函数耦合起来！想想 C++ 之前的 11 次，那时没有 lambdas。类和结构是将数据与函数耦合的自然方式，如果更改类的数据成员类型，就会得到完全不同的类类型。只是*自然*一个向量不能用同一个类型名存储完全不同的类型。\n\n告诉用户只能保存不捕获任何东西的观察者函数对象是不好的，因为这极大地限制了用例的数量。我们如何允许用户存储任何类型的函数对象，只约束到调用接口，该接口接受一组特定的参数，这些参数代表应该观察的值？\n\n本节展示了如何使用`std::function`解决这个问题，它可以充当任何 lambda 表达式的多态包装器，无论它是否捕获了什么。\n\n# 怎么做...\n\n在这一节中，我们将创建几个 lambda 表达式，它们捕获的变量类型完全不同，但具有相同的函数调用签名。这些将使用`std::function`保存在一个向量中:\n\n1.  让我们首先做一些必要的包括:\n\n```cpp\n      #include <iostream>\n      #include <deque>\n      #include <list>\n      #include <vector>\n      #include <functional>\n```\n\n2.  我们实现了一个返回 lambda 表达式的小函数。它接受一个容器，并返回一个通过引用捕获该容器的函数对象。函数对象本身接受一个整数参数。每当该函数对象被输入一个整数时，它会将该整数追加到它捕获的容器中:\n\n```cpp\n      static auto consumer (auto &container){\n          return [&] (auto value) {\n              container.push_back(value);\n          };\n      }\n```\n\n3.  另一个小助手函数将打印我们作为参数提供的任何容器实例:\n\n```cpp\n      static void print (const auto &c)\n      {\n          for (auto i : c) {\n              std::cout << i << \", \";\n          }\n          std::cout << 'n';\n      }\n```\n\n4.  在主函数中，我们首先实例化一个`deque`、一个`list`和一个`vector`，它们都存储整数:\n\n```cpp\n      int main()\n      {\n          std::deque<int>  d;\n          std::list<int>   l;\n          std::vector<int> v;\n```\n\n5.  现在我们将`consumer`函数用于我们的容器实例，`d`、`l`和`v`:我们为这些容器生成消费者函数对象，并将它们全部存储在一个`vector`实例中。然后我们有一个存储三个函数对象的向量。这些函数对象各自捕获对其中一个容器对象的引用。这些容器对象是完全不同的类型，函数对象也是如此。然而，向量包含`std::function<void(int)>`的实例。所有的函数对象都被隐式包装成这样的`std::function`对象，然后存储在向量中:\n\n```cpp\n          const std::vector<std::function<void(int)>> consumers \n              {consumer(d), consumer(l), consumer(v)};\n```\n\n6.  现在，我们向所有数据结构提供 10 个整数值，方法是循环这些值，然后循环消费者函数对象，我们用这些值调用这些对象:\n\n```cpp\n          for (size_t i {0}; i < 10; ++ i) {\n              for (auto &&consume : consumers) {\n                  consume(i);\n              }\n          }\n```\n\n7.  所有三个容器现在应该包含相同的 10 个数值。让我们打印他们的内容:\n\n```cpp\n          print(d);\n          print(l);\n          print(v);\n      }\n```\n\n8.  编译和运行程序会产生以下输出，这正是我们所期望的:\n\n```cpp\n      $ ./std_function\n      0, 1, 2, 3, 4, 5, 6, 7, 8, 9, \n      0, 1, 2, 3, 4, 5, 6, 7, 8, 9, \n      0, 1, 2, 3, 4, 5, 6, 7, 8, 9, \n```\n\n# 它是如何工作的...\n\n这个食谱的复杂部分是下面一行:\n\n```cpp\nconst std::vector<std::function<void(int)>> consumers \n        {consumer(d), consumer(l), consumer(v)};\n```\n\n物体`d`、`l`、`v`各自包裹成一个`consumer(...)`呼叫。该调用返回函数对象，然后每个对象捕获对`d`、`l`和`v`之一的引用。虽然这些函数对象都接受`int`值作为参数，但是它们完全捕获*不同*变量的事实也使得它们完全不同*类型*。这就像试图将类型为`A`、`B`和`C`的变量填充到一个向量中，尽管这些类型没有任何共同之处*。*\n\n为了解决这个问题，我们需要找到一个*通用的*类型，可以存储非常*不同的*功能对象，也就是`std::function`。一个`std::function<void(int)>`对象可以存储任何函数对象或传统函数，接受一个整数参数，不返回任何内容。它使用多态将它的类型从底层函数对象类型中分离出来。假设我们写了这样的东西:\n\n```cpp\nstd::function<void(int)> f (\n    [&vector](int x) { vector.push_back(x); });\n```\n\n在这里，由 lambda 表达式构造的函数对象被包装成一个`std::function`对象，每当我们调用`f(123)`时，这就导致了一个*虚拟函数调用*，它被*重定向到里面的实际函数对象。*\n\n在存储函数对象时，`std::function`实例应用了一些智能。如果我们在 lambda 表达式中捕获越来越多的变量，它一定会变大。如果它的大小不是太大，`std::function`可以把它储存在自己里面。如果存储的函数对象太大，`std::function`将在堆上分配一大块内存，然后将大的函数对象存储在那里。这不会影响我们代码的功能，但是我们应该知道这一点，因为这会影响我们代码的*性能*。\n\nA lot of novice programmers think or hope that `std::function<...>` actually expresses the *type* of a lambda expression. No, it doesn't. It is a polymorphic library helper, which is useful for wrapping lambda expressions and erasing their type differences.\n\n# 通过串联组合函数\n\n很多任务真的不值得用完全定制的代码来实现。例如，让我们看一下程序员如何使用编程语言 Haskell 解决找出一个文本包含多少个唯一单词的任务。第一行定义了一个函数`unique_words`，第二行用一个示例字符串演示了它的用法:\n\n![](img/d12ee785-b9b9-4b8e-9ba5-8c42f81de022.png)\n\n哇，真短！在不过多解释 Haskell 语法的情况下，让我们看看代码是做什么的。它定义了一个名为`unique_words`的函数，该函数对其输入应用了一系列函数。它首先用`map toLower`将输入的所有字符映射为小写。这样，像`FOO`和`foo`这样的词就可以看作是同一个*词。然后，`words`功能将一个句子拆分成单独的单词，如从`\"foo bar baz\"`到`[\"foo\", \"bar\", \"baz\"]`。下一步是整理新的单词列表。这样，像`[\"a\", \"b\", \"a\"]`这样的单词序列就变成了`[\"a\", \"a\", \"b\"]`。现在，`group`功能接管。它将连续相等的单词分组到分组列表中，因此`[\"a\", \"a\", \"b\"]`变成了`[ [\"a\", \"a\"], [\"b\"] ]`。这项工作现在几乎完成了，因为我们现在只需要计算*有多少组*相等的单词，这正是`length`函数所做的。*\n\n这是一种*奇妙的*风格的编程，因为我们可以阅读*从右到左*发生了什么，因为我们只是，某种程度上，描述了一个转换管道。我们不需要关心各个部分是如何实现的(除非它们很慢或者有问题)。\n\n然而，我们在这里不是为了表扬 Haskell，而是为了提高我们的 C++ 技能。在 C++ 中也可以这样工作。我们不会完全达到 Haskell 示例的优雅，但我们仍然拥有最快的编程语言。这个例子解释了如何用 lambda 表达式模仿 C++ 中的*函数串联*。\n\n# 怎么做...\n\n在这一节中，我们定义了一些简单的玩具函数对象，*连接*它们，所以我们得到了一个单一的函数，它将简单的玩具函数一个接一个地应用到我们给它的输入中。为此，我们编写了自己的串联帮助函数:\n\n1.  首先，我们需要一些包括:\n\n```cpp\n      #include <iostream>\n      #include <functional>\n```\n\n2.  然后，我们实现助手函数`concat`，它任意取多个参数。这些参数将是函数，如`f`、`g`、`h`，结果将是对任何输入应用`f(g(h(...)))`的另一个函数对象:\n\n```cpp\n      template <typename T, typename ...Ts>\n      auto concat(T t, Ts ...ts)\n      {\n```\n\n3.  现在，事情变得有点复杂。当用户提供功能`f`、`g`、`h`时，我们将此评估为`f( concat(g, h) )`，再次扩展为`f( g( concat(h) ) )`，递归中止，得到`f( g( h(...) ) )`。这个表示这些用户函数串联的函数调用链由一个 lambda 表达式捕获，该表达式稍后可以获取一些参数`p`，然后将它们转发给`f(g(h(p)))`。这个 lambda 表达式就是我们返回的。`if constexpr`构造检查我们是否在递归步骤中，有多个函数要连接左边:\n\n```cpp\n          if constexpr (sizeof...(ts) > 0) {\n              return [=](auto ...parameters) { \n                  return t(concat(ts...)(parameters...)); \n              };\n          }\n```\n\n4.  如果我们在递归的*端*，编译器会选择`if constexpr`构造的另一个分支。在这种情况下，我们只返回函数`t`，因为它是唯一剩下的参数:\n\n```cpp\n          else {\n              return t;\n          }\n      }\n```\n\n5.  现在，让我们使用我们很酷的新函数连接帮助器来连接一些我们想要看到的函数。让我们从`main`函数开始，在这里我们定义了两个便宜的简单函数对象:\n\n```cpp\n      int main()\n      {\n          auto twice  ([] (int i) { return i * 2; });\n          auto thrice ([] (int i) { return i * 3; });\n```\n\n6.  现在让我们串联起来。我们用 STL 函数`std::plus<int>`连接我们的两个乘数函数对象，该函数接受两个参数并简单地返回它们的和。这样，我们得到了一个函数，它执行`twice(thrice(plus( a, b )))`。\n\n```cpp\n          auto combined (\n              concat(twice, thrice, std::plus<int>{})\n          );\n```\n\n7.  现在让我们使用它。`combined`函数现在看起来像一个普通的函数，编译器也能够连接这些函数，而没有任何不必要的开销:\n\n```cpp\n          std::cout << combined(2, 3) << 'n';\n      }\n```\n\n8.  编译并运行我们的程序会产生以下输出，这也是我们所期望的，因为`2 * 3 * (2 + 3)`就是`30`:\n\n```cpp\n      $ ./concatenation\n      30\n```\n\n# 它是如何工作的...\n\n这部分比较复杂的是`concat`功能。它看起来非常复杂，因为它将参数包`ts`解包为另一个 lambda 表达式，该表达式再次递归调用`concat`，参数较少:\n\n```cpp\ntemplate <typename T, typename ...Ts>\nauto concat(T t, Ts ...ts)\n{\n    if constexpr (sizeof...(ts) > 0) { \n        return [=](auto ...parameters) { \n            return t(concat(ts...)(parameters...)); \n        }; \n    } else {\n        return [=](auto ...parameters) { \n            return t(parameters...); \n        };\n    }\n}\n```\n\n让我们编写一个更简单的版本，它将*三个*函数串联起来:\n\n```cpp\ntemplate <typename F, typename G, typename H>\nauto concat(F f, G g, H h)\n{\n    return [=](auto ... params) {\n        return f( g( h( params... ) ) ); \n    };\n}\n```\n\n这看起来已经很相似了，但没那么复杂。我们返回一个 lambda 表达式，它捕获`f`、`g`和`h`。这个 lambda 表达式任意接受许多参数，并将它们转发给`f`、`g`和`h`的调用链。当我们写`auto combined (concat(f, g, h))`，然后用两个参数调用那个函数对象，比如`combined(2, 3)`，那么`2, 3`就由前面`concat`函数的`params`包来表示。\n\n再看复杂得多、通用的`concat`功能；我们唯一真正不同的是`f ( g( h( params... ) ) )`串联。取而代之的是，我们编写`f( concat(g, h) )(params...)`，它在下一次递归调用中计算为`f( g( concat(h) ) )(params...)`，然后最终得到`f( g( h( params... ) ) )`。\n\n# 使用逻辑连接创建复杂谓词\n\n当用泛型代码过滤数据时，我们最终定义了**谓词**，它们告诉我们想要什么数据，以及我们不想要什么数据。有时候谓词是不同谓词的*组合*。\n\n例如，当过滤字符串时，我们可以实现一个谓词，如果它的输入字符串*以`\"foo\"`开始*，则该谓词返回`true`。如果另一个谓词的输入字符串*以`\"bar\"`结束*，则该谓词可能返回 true。\n\n我们可以通过组合*来重用*谓词，而不是一直编写自定义谓词。如果我们想过滤以`\"foo\"`开头、以`\"bar\"`结尾的字符串，我们可以选择我们的*现有的*谓词和*谓词，并用一个逻辑*和*组合它们。在本节中，我们将使用 lambda 表达式，以便找到一种舒适的方法来实现这一点。*\n\n# 怎么做...\n\n我们将实现非常简单的字符串过滤器谓词，然后我们将它们与一个小的助手函数组合，该函数以通用的方式为我们完成组合。\n\n1.  一如既往，我们将首先包含一些标题:\n\n```cpp\n      #include <iostream>\n      #include <functional>\n      #include <string>\n      #include <iterator>\n      #include <algorithm>\n```\n\n2.  因为我们稍后会用到它们，所以我们实现了两个简单的谓词函数。第一个指示字符串是否以字符`'a'`开头，第二个指示字符串是否以字符`'b'`结尾:\n\n```cpp\n      static bool begins_with_a (const std::string &s)\n      {\n          return s.find(\"a\") == 0;\n      }\n\n      static bool ends_with_b (const std::string &s)\n      {\n          return s.rfind(\"b\") == s.length() - 1;\n      }\n```\n\n3.  现在，让我们实现一个助手函数，我们称之为`combine`。它以二进制函数作为第一个参数，例如，它可以是逻辑`AND`函数或逻辑`OR`函数。然后，它需要另外两个参数，这两个参数应该是两个谓词函数，然后进行组合:\n\n```cpp\n      template <typename A, typename B, typename F>\n      auto combine(F binary_func, A a, B b)\n      {\n```\n\n4.  我们只需返回一个 lambda 表达式，该表达式捕获新的谓词*组合*。它将一个参数转发给两个谓词，然后将两个谓词的结果放入二元函数并返回其结果:\n\n```cpp\n          return [=](auto param) {\n              return binary_func(a(param), b(param));\n          };\n      }\n```\n\n5.  让我们声明我们使用`std`名称空间来节省我们在`main`函数中的一些输入:\n\n```cpp\n      using namespace std;\n```\n\n6.  现在，让我们将两个谓词函数合并到另一个谓词函数中，它告诉给定的字符串是否以`a` *开头，*以`b`结尾，就像`\"ab\"`或`\"axxxb\"`一样。作为二元函数，我们选择`std::logical_and`。它是一个需要实例化的模板类，所以我们用花括号来实例化它。请注意，我们不提供模板参数，因为对于这个类，它默认为`void`。该类的这种专门化自动推导出所有参数类型:\n\n```cpp\n      int main()\n      {\n          auto a_xxx_b (combine(\n              logical_and<>{}, \n              begins_with_a, ends_with_b));\n```\n\n7.  我们迭代标准输入并将所有单词打印回终端，这满足了我们的断言:\n\n```cpp\n          copy_if(istream_iterator<string>{cin}, {},\n                  ostream_iterator<string>{cout, \", \"},\n                  a_xxx_b);\n          cout << 'n';\n      }\n```\n\n8.  编译并运行程序会产生以下输出。我们用四个词来填充程序，但是只有两个词满足谓词标准:\n\n```cpp\n      $ echo \"ac cb ab axxxb\" | ./combine\n      ab, axxxb, \n```\n\n# 还有更多...\n\nSTL 已经提供了一堆有用的功能对象，如`std::logical_and`、`std::logical_or`，以及许多其他对象，因此我们不需要在每个项目中重新实现它们。看看 C++ 参考资料，探索一下已经有的东西是个好主意:\n[http://en.cppreference.com/w/cpp/utility/functional](http://en.cppreference.com/w/cpp/utility/functional)\n\n# 用同一个输入调用多个函数\n\n任务很多，导致代码重复。使用 lambda 表达式可以很容易地消除大量重复的代码，并且可以非常快速地创建一个包装这些重复任务的 lambda 表达式助手。\n\n在这一节中，我们将使用 lambda 表达式将一个带有所有参数的调用转发给多个接收者。这将在中间没有任何数据结构的情况下发生，因此编译器有一个简单的任务来生成二进制文件而没有开销。\n\n# 怎么做...\n\n我们将编写一个 lambda 表达式助手，它将单个调用转发给多个对象，以及另一个 lambda 表达式助手，它将单个调用转发给其他函数的多个调用。在我们的示例中，我们将使用不同的打印机功能打印一封邮件:\n\n1.  让我们首先包含打印所需的 STL 标题:\n\n```cpp\n      #include <iostream>\n```\n\n2.  首先，我们实现`multicall`功能，这是这个食谱的核心。它接受任意数量的函数作为参数，并返回一个接受一个参数的 lambda 表达式。它将此参数转发给之前提供的所有函数。这样，我们可以定义`auto call_all (multicall(f, g, h))`，然后，`call_all(123)`引出一系列的调用，`f(123); g(123); h(123);`。这个函数看起来非常复杂，因为我们需要一个语法技巧，通过使用`std::initializer_list`构造函数将参数包`functions`扩展成一系列调用:\n\n```cpp\n      static auto multicall (auto ...functions)\n      {\n          return [=](auto x) {\n              (void)std::initializer_list<int>{\n                  ((void)functions(x), 0)...\n              };\n          };\n      }\n```\n\n3.  下一个助手接受一个函数`f`和一组参数`xs`。它用这些参数中的每一个来称呼`f`。这样，一个`for_each(f, 1, 2, 3)`呼叫会导致一系列呼叫:`f(1); f(2); f(3);`。该函数本质上使用相同的语法技巧将参数包`xs`扩展为一系列函数调用，就像前面的其他函数一样:\n\n```cpp\n      static auto for_each (auto f, auto ...xs) {\n          (void)std::initializer_list<int>{\n              ((void)f(xs), 0)...\n          };\n      }\n```\n\n4.  `brace_print`函数接受两个字符，并返回一个新的函数对象，该对象接受一个参数`x`。它将*打印*出来，周围是我们之前刚刚捕捉到的两个大字:\n\n```cpp\n      static auto brace_print (char a, char b) {\n          return [=] (auto x) {\n              std::cout << a << x << b << \", \";\n          };\n      }\n```\n\n5.  现在，我们终于可以在`main`功能中使用一切了。首先，我们定义函数`f`、`g`和`h`。它们表示接受值的打印函数，并且用不同的大括号/圆括号将它们打印出来。`nl`函数接受任何参数，只打印一个换行符:\n\n```cpp\n      int main()\n      {\n          auto f  (brace_print('(', ')'));\n          auto g  (brace_print('[', ']'));\n          auto h  (brace_print('{', '}'));\n          auto nl ([](auto) { std::cout << 'n'; });\n```\n\n6.  让我们使用我们的`multicall`助手将它们组合起来:\n\n```cpp\n          auto call_fgh (multicall(f, g, h, nl));\n```\n\n7.  对于我们提供的每个数字，我们希望看到它们被不同对的大括号/圆括号分别打印三次。这样，我们可以进行一次函数调用，并以对多功能的五次调用结束，多功能又对`f`、`g`、`h`和`nl`进行了四次调用。\n\n```cpp\n          for_each(call_fgh, 1, 2, 3, 4, 5);\n      }\n```\n\n8.  在编译和运行之前，考虑预期的输出:\n\n```cpp\n      $ ./multicaller\n      (1), [1], {1}, \n      (2), [2], {2}, \n      (3), [3], {3}, \n      (4), [4], {4}, \n      (5), [5], {5}, \n```\n\n# 它是如何工作的...\n\n我们刚刚实现的助手看起来非常复杂。这是因为我们用`std::initializer_list`展开参数包。我们为什么要使用这种数据结构？我们再来看看`for_each`:\n\n```cpp\nauto for_each ([](auto f, auto ...xs) {\n    (void)std::initializer_list<int>{\n        ((void)f(xs), 0)...\n    };\n});\n```\n\n这个函数的核心是`f(xs)`表达式。`xs`是一个参数包，我们需要*将其解包*，以便从中获取个体值，并将其馈送给个体`f`调用。不幸的是，我们不能仅仅用我们已经知道的`...`符号来写`f(xs)...`。\n\n我们可以做的是使用`std::initializer_list`构造一个值列表，它有一个变量构造器。像`return std::initializer_list<int>{f(xs)...};`这样的表达是有用的，但是它也有*的缺点*。让我们看一下`for_each`的一个实现，它就是这样做的，所以它看起来比我们现有的更简单:\n\n```cpp\nauto for_each ([](auto f, auto ...xs) {\n    return std::initializer_list<int>{f(xs)...};\n});\n```\n\n这很容易理解，但它的缺点如下:\n\n1.  它从所有的`f`调用中构造一个返回值的实际初始化列表。此时，我们不关心返回值。\n2.  它*返回*那个初始化列表，虽然我们想要一个*“火了就忘了”*函数，它什么也不返回*。*\n**   有可能`f`是一个函数，它甚至不返回任何东西，在这种情况下，它甚至不会编译。*\n\n *更复杂的`for_each`函数解决了所有这些问题。它做以下事情来实现这一点:\n\n1.  它不*返回*初始化列表，但是它*使用`(void)std::initializer_list<int>{...}`将整个表达式转换为`void`。*\n2.  在初始化式表达式中，它将`f(xs)...`包装成一个`(f(xs), 0)...`表达式。这导致返回值被*扔掉*，而`0`被放入初始化列表。\n3.  `(f(xs), 0)...`表达式中的`f(xs)`再次被强制转换为`void`，所以如果有返回值的话，真的不会在*的任何地方进行处理。*\n\n不幸的是，把所有这些放在一起会导致一个丑陋的构造，但是它确实工作正常，并且用各种各样的函数对象编译，不管它们是否返回任何东西或者返回什么。\n\n这项技术的一个很好的细节是，函数调用的应用顺序保证是按照*严格的顺序*进行的。\n\nCasting anything using the old C-style notation `(void)expression` is advised against because C++ has its own cast operators. We should have used `reinterpret_cast<void>(expression)` instead, but this would have decreased the *readability* of the code further.\n\n# 使用标准::累加和 lambdas 实现 transform_if\n\n大部分用过`std::copy_if`、`std::transform`的开发者可能已经问过自己，为什么没有`std::transform_if`。`std::copy_if`功能将项目从源范围复制到目标范围，但*跳过*用户定义的*谓词*功能未选择的项目。`std::transform`无条件地将所有项目从源范围复制到目标范围，但在两者之间进行转换。转换是由用户定义的函数提供的，它可以做一些简单的事情，比如将数字相乘或将项目转换成完全不同的类型。\n\n这样的功能现在已经有很长时间了，但是还有*还是*没有`std::transform_if`功能。在本节中，我们将实现这个函数。只需实现一个在范围内迭代的函数，同时复制谓词函数选择的所有项，并在它们之间进行转换，就可以很容易地做到这一点。然而，我们将利用这个机会深入研究 lambda 表达式。\n\n# 怎么做...\n\n我们将构建自己的`transform_if`函数，通过为`std::accumulate`提供正确的函数对象来工作:\n\n1.  我们需要像往常一样包括一些标题:\n\n```cpp\n      #include <iostream>\n      #include <iterator>\n      #include <numeric>\n```\n\n2.  首先，我们将实现一个名为`map`的函数。它接受输入转换函数作为参数，并返回一个函数对象，与`std::accumulate`配合使用效果很好:\n\n```cpp\n      template <typename T>\n      auto map(T fn)\n      {\n```\n\n3.  我们返回的是一个接受*约简*函数的函数对象。当用这样的 reduce 函数调用该对象时，它返回另一个函数对象，该对象接受一个*累加器*和一个输入参数。它调用这个累加器和`fn`转换后的输入变量的减少函数。如果这看起来很复杂，不要担心，我们稍后会把它放在一起，看看它是如何工作的:\n\n```cpp\n          return [=] (auto reduce_fn) {\n              return [=] (auto accum, auto input) {\n                  return reduce_fn(accum, fn(input));\n              };\n          };\n      }\n```\n\n4.  现在我们实现一个叫做`filter`的函数。它的工作方式与`map`功能完全相同，但输入*保持不变*，而`map`功能*使用变换功能对其进行变换。相反，我们接受一个谓词函数并且*跳过*输入变量而不减少它们，以防它们不被谓词函数接受:*\n\n```cpp\n      template <typename T>\n      auto filter(T predicate)\n      {\n```\n\n5.  这两个 lambda 表达式与`map`函数中的表达式具有完全相同的函数签名。唯一的区别是`input`参数保持不变。谓词函数用于区分我们是在输入上调用`reduce_fn`函数，还是在没有任何变化的情况下向前到达累加器:\n\n```cpp\n          return [=] (auto reduce_fn) {\n              return [=] (auto accum, auto input) {\n                  if (predicate(input)) {\n                      return reduce_fn(accum, input);\n                  } else {\n                      return accum;\n                  }\n              };\n          };\n      }\n```\n\n6.  现在让我们最终使用那些助手。我们实例化迭代器，让我们从标准输入中读取整数值:\n\n```cpp\n      int main()\n      {\n          std::istream_iterator<int> it {std::cin};\n          std::istream_iterator<int> end_it;\n```\n\n7.  然后我们定义一个谓词函数`even`，如果我们有一个*偶数*，它就返回`true`。变换函数`twice`将其整数参数乘以因子`2`:\n\n```cpp\n          auto even  ([](int i) { return i % 2 == 0; });\n          auto twice ([](int i) { return i * 2; });\n```\n\n8.  `std::accumulate`函数取一系列值，*累加*它们。在默认情况下，累加意味着用`+`运算符将*和*的值相加。我们想提供自己的积累功能。这样，我们就不会维护值的*和*。我们所做的是将范围的每个值分配给解引用的迭代器`it`，然后在*进一步推进*之后返回这个迭代器:\n\n```cpp\n          auto copy_and_advance ([](auto it, auto input) {\n              *it = input;\n              return ++ it;\n          });\n```\n\n9.  现在我们终于把碎片拼在一起了。我们迭代标准输入并提供输出`ostream_iterator`，输出将打印到终端。`copy_and_advance`函数对象通过向输出迭代器分配用户输入整数来处理输出迭代器。向输出迭代器分配有效的*打印*分配的项目。但是我们只想要用户输入的*甚至*数字，我们想要*乘以*。为此，我们将`copy_and_advance`功能包装到`even` *过滤器*中，然后包装到`twice` *映射器*中:\n\n```cpp\n          std::accumulate(it, end_it,\n              std::ostream_iterator<int>{std::cout, \", \"},\n              filter(even)(\n                  map(twice)(\n                      copy_and_advance\n                  )\n              ));\n          std::cout << 'n';\n      }\n```\n\n10.  编译和运行程序会产生以下输出。数值`1`、`3`、`5`因为不均匀而下降，数值`2`、`4`、`6`翻倍后打印:\n\n```cpp\n      $ echo \"1 2 3 4 5 6\" | ./transform_if\n      4, 8, 12, \n```\n\n# 它是如何工作的...\n\n这个配方看起来非常复杂，因为我们正在大量嵌套 lambda 表达式。为了了解这是如何工作的，我们先来看看`std::accumulate`的内部工作原理。这是它在典型的 STL 实现中的样子:\n\n```cpp\ntemplate <typename T, typename F>\nT accumulate(InputIterator first, InputIterator last, T init, F f)\n{\n    for (; first != last; ++ first) {\n        init = f(init, *first);\n    }\n    return init;\n}\n```\n\n函数参数`f`在这里做主要工作，而循环在用户提供的`init`变量中收集其结果。在一个常见的例子中，迭代器范围可以表示一个数字向量，例如`0, 1, 2, 3, 4`，而`init`值是`0`。`f`函数是一个二元函数，可以使用`+`运算符计算两个项目的*和*。\n\n在这个例子中，循环只是将所有的项目加到`init`变量中，比如在`init = (((0 + 1) + 2) + 3) + 4`中。这样写下来很明显`std::accumulate`只是一个通用的*折叠*功能。折叠一个范围意味着对一个累加器变量应用一个二进制运算，并逐步处理包含在该范围内的每一项(每个运算的结果就是下一项的累加器值)。由于这个功能如此通用，我们可以用它做各种事情，就像实现`std::transform_if`一样！`f`功能也称为*减少*功能。\n\n`transform_if`的一个非常直接的实现如下:\n\n```cpp\ntemplate <typename InputIterator, typename OutputIterator, \n          typename P, typename Transform>\nOutputIterator transform_if(InputIterator first, InputIterator last,\n                            OutputIterator out,\n                            P predicate, Transform trans)\n{\n    for (; first != last; ++ first) {\n        if (predicate(*first)) {\n            *out = trans(*first);\n            ++ out;\n        }\n    }\n    return out;\n}\n```\n\n这看起来很*类似于*`std::accumulate`，如果我们将参数`out`视为`init`变量，*不知何故*得到函数`f`来替代 if-construct 及其 body！\n\n我们真的做到了。我们用我们作为参数提供给`std::accumulate`的二元函数对象构造了 if-construct 及其主体:\n\n```cpp\nauto copy_and_advance ([](auto it, auto input) {\n    *it = input;\n    return ++ it;\n});\n```\n\n`std::accumulate`函数将`init`变量放入二元函数的`it`参数中。第二个参数是每个循环迭代步骤的源范围的当前值。*我们*提供了一个*输出迭代器*作为`std::accumulate.`的`init`参数。这样，`std::accumulate`不计算总和，而是将迭代的项目转发到另一个范围。这意味着我们只是重新实现了`std::copy`而没有任何谓词和转换。\n\n使用谓词的过滤是我们通过将`copy_and_advance`函数对象包装到另一个函数对象中而添加的，该对象使用谓词函数:\n\n```cpp\ntemplate <typename T>\nauto filter(T predicate)\n{\n    return [=] (auto reduce_fn) {\n        return [=] (auto accum, auto input) {\n            if (predicate(input)) {\n                return reduce_fn(accum, input);\n            } else {\n                return accum;\n            }\n        };\n    };\n}\n```\n\n这个构造刚开始看起来不太简单，但是看看`if`构造。如果`predicate`函数返回`true`，它会将参数转发给`reduce_fn`函数，在我们的例子中是`copy_and_advance`。如果谓词返回`false`，则`accum`变量，也就是`std::accumulate`的`init`变量，只是不变地返回。这实现了过滤操作的*跳过*部分。`if`构造位于内部 lambda 表达式中，该表达式具有与`copy_and_advance`函数相同的二进制函数签名，这使其成为合适的替代。\n\n现在我们能够*过滤*，但仍然没有*改造*。这是通过`map`功能助手完成的:\n\n```cpp\ntemplate <typename T>\nauto map(T fn)\n{\n    return [=] (auto reduce_fn) {\n        return [=] (auto accum, auto input) {\n            return reduce_fn(accum, fn(input));\n        };\n    };\n}\n```\n\n这段代码看起来简单多了。它再次包含一个内部 lambda 表达式，其签名与`copy_and_advance`相同，因此可以替换它。该实现只是转发输入值，但是*用`fn`函数转换二进制函数调用的**右*参数。\n\n后来，当我们使用这些助手时，我们编写了以下表达式:\n\n```cpp\nfilter(even)(\n    map(twice)(\n        copy_and_advance\n    )\n)\n```\n\n`filter(even)`调用捕获`even`谓词并给我们一个函数，该函数接受一个二进制函数以将其包装成另一个*二进制函数，该函数进行额外的*过滤*。`map(twice)`函数与`twice`变换函数相同，但是将二元函数`copy_and_advance`包装成另一个二元函数，后者总是*变换*正确的参数。*\n\n如果没有任何优化，我们将得到一个非常复杂的函数嵌套结构，它调用函数并且只做很少的工作。然而，对于编译器来说，优化所有代码是一项非常简单的任务。生成的二进制文件非常简单，就好像它是由`transform_if`更直接的实现产生的一样。以这种方式，我们在绩效方面不支付任何费用。但是我们得到的是一个非常好的函数组合，因为我们能够将`even`谓词和`twice`转换函数结合在一起，就像它们是*乐高*积木一样简单。\n\n# 在编译时生成任何输入的笛卡尔乘积对\n\nLambda 表达式结合参数包可用于复杂的任务。在本节中，我们将实现一个函数对象，该对象接受任意数量的输入参数，并使用*本身*生成该集合的**笛卡尔乘积**。\n\n笛卡儿积是一种数学运算。记为`A x B`，意为集合`A`和集合`B`的笛卡尔乘积。结果是另一个*单套*，包含套`A`和`B`的成对*所有*项目组合。操作的基本意思是，c *把 A 的每一项和 B* 的每一项组合起来。下图说明了操作:\n\n![](img/f83f3245-6b4c-4919-b137-17c2d6a11e7e.png)\n\n在上图中，如果`A = (x, y, z)`、`B = (1, 2, 3)`，那么笛卡尔积就是`(x, 1)`、`(x, 2)`、`(x, 3)`、`(y, 1)`、`(y, 2)`等等。\n\n如果我们确定`A`和`B`是同一个*集合，比如说`(1, 2)`，那么这个集合的笛卡尔积就是`(1, 1)`、`(1, 2)`、`(2, 1)`和`(2, 2)`。在某些情况下，这可能被宣布为*冗余*，因为可能不需要项目与*本身的组合*(如在`(1, 1)`中)或`(1, 2)`和`(2, 1)`的冗余组合。在这种情况下，可以用一个简单的规则过滤笛卡儿积。*\n\n *在本节中，我们将实现没有任何循环的笛卡儿积，但是使用 lambda 表达式和参数包解包。\n\n# 怎么做...\n\n我们实现了一个函数对象，它接受一个函数、`f`和一组参数。函数对象将*创建*参数集的笛卡儿积，*过滤掉*多余的部分，*调用*每个部分的`f`函数:\n\n1.  我们只需要包括打印所需的 STL 标题:\n\n```cpp\n      #include <iostream>\n```\n\n2.  然后，我们定义一个简单的助手函数，它打印一对值，我们开始实现`main`函数:\n\n```cpp\n      static void print(int x, int y)\n      {\n          std::cout << \"(\" << x << \", \" << y << \")n\";\n      }\n\n      int main()\n      {\n```\n\n3.  最难的部分从现在开始。我们首先为下一步要实现的`cartesian`函数实现一个助手。这个函数接受一个参数`f`，当我们以后使用它时，它将是`print`函数。其他参数为`x`和参数包`rest`。这些包含我们想要笛卡儿积的实际项目。看`f(x, rest)`的表达:对于`x=1`和`rest=2, 3, 4`，这将导致`f(1, 2); f(1, 3); f(1, 4);`等调用。`(x < rest)`测试用于消除生成的对中的冗余。我们将在后面更详细地讨论这一点:\n\n```cpp\n          constexpr auto call_cart (\n              [=](auto f, auto x, auto ...rest) constexpr {\n                  (void)std::initializer_list<int>{\n                      (((x < rest)\n                          ? (void)f(x, rest)\n                          : (void)0)\n                      ,0)...\n                  };\n              });\n```\n\n4.  `cartesian`函数是整个食谱中最复杂的一段代码。它接受参数包`xs`并返回一个捕获它的函数对象。返回的函数对象接受一个函数对象，`f`。\n    对于参数包`xs=1, 2, 3`，内部 lambda 表达式将生成以下调用:`call_cart(f, **1**, 1, 2, 3); call_cart(f, **2**, 1, 2, 3); call_cart(f, **3**, 1, 2, 3);`。从这个调用范围，我们可以生成我们需要的所有笛卡尔乘积对。\n    注意，我们使用`...`符号对`xs`参数包*进行了两次*的扩展，刚开始看起来很奇怪。第一次出现的`...`将整个`xs`参数包扩展为`call_cart`调用。第二次出现导致多个`call_cart`调用，第二个*参数不同:*\n\n```cpp\n          constexpr auto cartesian ([=](auto ...xs) constexpr {\n              return [=] (auto f) constexpr {\n                  (void)std::initializer_list<int>{\n                      ((void)call_cart(f, xs, xs...), 0)...\n                  };\n              };\n          });\n```\n\n5.  现在，让我们生成数值集`1, 2, 3`的笛卡儿积并打印对。如果没有冗余对，这将产生数量对、`(1, 2)`、`(2, 3)`和`(1, 3)`。如果我们忽略顺序，不希望一对中有相同的数字，就不可能有更多的组合。这意味着我们做*不是*想要`(1, 1)`，考虑`(1, 2)`和`(2, 1)`同一个*对*。\n    首先，我们让`cartesian`生成一个已经包含所有可能对的函数对象，并接受我们的打印函数。然后，我们用它来让所有这些对调用我们的`print`函数。\n    我们声明`print_cart`变量，`constexpr`，这样就可以保证它持有的函数对象(以及它生成的所有对)是在编译时创建的:\n\n```cpp\n          constexpr auto print_cart (cartesian(1, 2, 3));\n\n          print_cart(print);\n      }\n```\n\n6.  正如预期的那样，编译和运行会产生以下输出。通过移除`call_cart`函数中的`(x < xs)`条件来处理代码，并看到我们得到了具有冗余对和相同数量对的完全笛卡尔乘积:\n\n```cpp\n      $ ./cartesian_product\n      (1, 2)\n      (1, 3)\n      (2, 3)\n```\n\n# 它是如何工作的...\n\n这是另一个看起来非常复杂的 lambda 表达式构造。但是一旦我们彻底理解了这一点，我们就不会很快被任何 lambda 表达式所迷惑！\n\n那么，我们来详细了解一下。我们应该对需要发生的事情有一个清晰的认识:\n\n![](img/957b7794-331d-4b2d-958e-ac82ee95071d.png)\n\n这是三个步骤:\n\n1.  我们拿着我们的布景`1, 2, 3`并从中创作了三个新的布景。这些集合中的每一个的第一部分都是集合中连续的单个项目，第二部分是整个集合本身。\n2.  我们将第一个项目与集合中的每个项目相结合，从中获得尽可能多的*对*。\n3.  从这些结果对中，我们只选择那些*不冗余的*(例如`(1, 2)`和`(2, 1)`冗余的)和不相同编号的(例如`(1, 1)`)。\n\n现在，回到实现:\n\n```cpp\n constexpr auto cartesian ([=](auto ...xs) constexpr {\n     return [=](auto f) constexpr {\n         (void)std::initializer_list<int>{\n             ((void)call_cart(f, xs, xs...), 0)...\n         };\n     };\n });\n```\n\n内心的表达`call_cart(xs, xs...)`，恰恰代表着`(1, 2, 3)`分离成那些新的集合，比如`1, [1, 2, 3]`。完整的表达式，`((void)call_cart(f, xs, xs...), 0)...`和外面的另一个`...`对集合的每个值都进行这种分离，所以我们也得到`2, [1, 2, 3]`和`3, [1, 2, 3]`。\n\n第二步和第三步由`call_cart`完成:\n\n```cpp\nauto call_cart ([](auto f, auto x, auto ...rest) constexpr {\n    (void)std::initializer_list<int>{\n        (((x < rest)\n            ? (void)f(x, rest)\n            : (void)0)\n        ,0)...\n    };\n});\n```\n\n参数`x`始终包含从集合中选取的单个值，`rest`再次包含整个集合。我们先忽略`(x < rest)`的条件。在这里，表达式`f(x, rest)`与`...`参数包扩展一起生成函数调用`f(1, 1)`、`f(1, 2)`等，这导致对被打印。这是第二步。\n\n步骤 3 通过仅过滤掉适用`(x < rest)`的对来实现。\n\n我们制作了所有的 lambda 表达式和保存它们的变量`constexpr`。通过这样做，我们现在可以保证编译器将在编译时评估它们的代码，并编译一个已经包含所有数字对的二进制文件，而不是在运行时计算它们。请注意，只有当我们为 constexpr 函数提供的所有函数参数在编译时已经为所知时，才会出现*。*****"
  },
  {
    "path": "docs/exp-cpp-prog/04.md",
    "content": "# 四、STL 算法基础\n\n我们将在本章介绍以下食谱:\n\n*   将项目从容器复制到其他容器\n*   分类容器\n*   从容器中移除特定项目\n*   转换容器的内容\n*   在有序和无序向量中查找项目\n*   通过`std::clamp`将向量的值限制在特定的数值范围内\n*   用`std::search`定位字符串中的模式并选择最佳实现\n*   采样大向量\n*   生成输入序列的排列\n*   实现字典合并工具\n\n# 介绍\n\n当然，STL 不仅包含数据结构，还包含*算法*。虽然数据结构帮助*存储*和*以不同的方式维护*数据，具有不同的动机和目标，但算法将特定的*转换*应用于此类数据结构中的数据。\n\n让我们来看看一个标准任务，比如从一个向量中总结项目。这可以很容易地通过循环向量并把所有项目加到一个叫做`sum`的累加器变量中来实现:\n\n```cpp\n vector<int> v {100, 400, 200 /*, ... */ };\n\n int sum {0};\n for (int i : v) { sum += i; }\n\n cout << sum << 'n';\n```\n\n但是因为这是一个相当标准的任务，所以也有一个 STL 算法:\n\n```cpp\ncout << accumulate(begin(v), end(v), 0) << 'n';\n```\n\n在这种情况下，手工制作的循环变体不会太长，而且它也不会比一行写着“T0”的代码更难阅读。然而，在很多情况下，读一个 10 行的代码循环只是为了意识到“我只是必须研究整个循环才能理解它是做一个标准任务的吗，X？”，而不是看到一行代码，它使用一个标准算法，该算法的名称清楚地说明了它的功能，如`accumulate`、`copy`、`move`、`transform`或`shuffle`。\n\n基本思想是提供丰富多样的算法，供程序员日常使用，以减少重复实现它们的需要。这样，程序员就可以使用现成的算法实现，专注于*新的*问题，而不是把时间浪费在*已经通过 STL 解决的*问题上。另一个角度是正确性——如果一个程序员一次又一次地实现同一个东西一百次，这很可能会在一次或另一次尝试中引入一个轻微的*错误*。这将是完全不必要的，并且也非常*尴尬*例如，如果它是由一个同事在代码审查期间指出的，而与此同时，可以使用标准算法。\n\nSTL 算法的另一个要点是*效率*。许多 STL 算法提供同一算法的多个*专用*实现，这些实现根据它们所使用的*迭代器类型*而有所不同。例如，如果整数向量中的所有元素都应该归零，这可以通过 STL 算法`std::fill`来完成。因为向量的迭代器已经可以告诉编译器它在*连续的*内存上迭代，所以它可以选择使用 C 过程`memset`的`std::fill`的实现。如果程序员将容器类型从`vector`更改为`list`，那么 STL 算法就不能再使用`memset`了，必须遍历列表才能逐个将项目清零。如果程序员自己使用`memset`，实现将不必要地硬编码为使用向量或数组，因为大多数其他数据结构不会将其数据保存在连续的内存块中。在大多数情况下，试图变得聪明没有什么意义，因为 STL 的实现者可能已经实现了相同的想法，可以免费使用。\n\n让我们总结一下前面的几点。使用 STL 算法有利于:\n\n*   **可维护性**:算法的名称已经以一种直接的方式陈述了它们的作用。显式循环很少能像标准算法那样更好地阅读和理解数据结构。\n*   **正确性**:STL 已经被专家编写和评审过了，被这么多人使用和测试过了，你在重新实现它的复杂部分的时候，几乎不可能达到同样的正确程度。\n*   **效率**:默认情况下，STL 算法至少和大多数手工循环一样高效。\n\n大多数算法在*迭代器*上工作。迭代器如何工作的概念已经在[第 20 章](02.html)、*迭代器*中解释过了。在这一章中，我们将集中于对不同的问题使用 STL 算法，以便了解它们如何被有利地使用。展示*所有* STL 算法会让这本书变成一个非常无聊的 C++ 参考，尽管已经有一个公开的 C++ 参考。\n\n成为 STL 忍者的最好方法是让 C++ 引用一直在手边，或者至少保存在浏览器书签中。在解决一个任务时，每个程序员都应该带着心中的疑问看一看，“我的问题有没有 STL 算法？”，然后自己写代码。\n\n一个非常好且完整的 C++ 参考资料可在以下网址在线查看:\n\n[http://cppreference.com](http://cppreference.com)\n\n也可以下载离线查看。\n\nIn job interviews, good fluency with the STL algorithms is often regarded as an indicator of a strong knowledge of C++.\n\n# 将项目从容器复制到其他容器\n\n最重要的 STL 数据结构支持迭代器。这意味着至少可以通过`begin()`和`end()`函数获得迭代器，它们指向数据结构的底层有效载荷数据，并允许对该数据进行迭代。不管迭代什么样的数据结构，迭代看起来总是一样的。\n\n我们可以从向量、列表、字典、映射等中获取迭代器。使用迭代器适配器，我们甚至可以将迭代器作为文件、标准输入和标准输出的接口。此外，正如我们在上一章中看到的，我们甚至可以将迭代器接口包装在算法周围。现在，我们可以用迭代器访问一切，我们可以将它们与 STL 算法相结合，STL 算法接受迭代器作为参数。\n\n展示迭代器如何帮助抽象不同数据结构本质的一个非常好的方法是`std::copy`算法，它只是将一组迭代器中的项目复制到一个输出迭代器中。在使用这种算法的地方，底层数据结构的性质不再真正相关。为了演示这一点，我们将玩一点`std::copy`。\n\n# 怎么做...\n\n在本节中，我们将使用`std::copy`的不同变体:\n\n1.  让我们首先包含我们使用的数据结构所需的所有头。此外，我们声明使用`std`命名空间:\n\n```cpp\n       #include <iostream>\n       #include <vector>\n       #include <map>\n       #include <string>\n       #include <tuple>\n       #include <iterator>\n       #include <algorithm>\n\n       using namespace std;\n```\n\n2.  我们将在下面使用成对的整数值和字符串值。为了很好地打印它们，我们应该首先为它们重载`<<`流操作符:\n\n```cpp\n       namespace std {\n       ostream& operator<<(ostream &os, const pair<int, string> &p)\n       {\n           return os << \"(\" << p.first << \", \" << p.second << \")\";\n       }\n       }\n```\n\n3.  在`main`函数中，我们用一些默认值填充整数字符串对的`vector`。我们声明一个`map`变量，它将整数值与字符串值相关联:\n\n```cpp\n       int main()\n       {\n           vector<pair<int, string>> v {\n               {1, \"one\"}, {2, \"two\"}, {3, \"three\"}, \n               {4, \"four\"}, {5, \"five\"}};\n\n           map<int, string> m;\n```\n\n4.  现在，我们使用`std::copy_n`将向量前面的三个整数字符串对精确复制到地图上。因为矢量和地图是完全不同的数据结构，我们需要使用`insert_iterator`适配器从矢量转换项目。`std::inserter`功能为我们生产了这样一个适配器。请始终注意，使用像`std::copy_n`这样的算法结合插入迭代器是将项目复制/插入其他数据结构的最*通用*方式，但不是最快的*。使用数据结构特定的成员函数来插入项通常是最有效的方法:*\n\n```cpp\n           copy_n(begin(v), 3, inserter(m, begin(m)));\n```\n\n5.  之后让我们打印地图上的内容。在整本书中，我们经常使用`std::copy`功能打印容器的内容。`std::ostream_iterator`在这方面帮助很大，因为它允许我们将用户外壳的标准输出视为*另一个容器*，我们可以将数据复制到:\n\n```cpp\n           auto shell_it (ostream_iterator<pair<int, string>>{cout, \n                                                              \", \"});\n\n           copy(begin(m), end(m), shell_it);\n           cout << 'n';\n```\n\n6.  让我们为下一个实验再次清理地图。这一次，我们*将*项从矢量移动到地图上，这一次，是*所有*项:\n\n```cpp\n           m.clear();\n\n           move(begin(v), end(v), inserter(m, begin(m)));\n```\n\n7.  我们再次打印地图的新内容。此外，由于`std::move`是一个也改变数据*源*的算法，我们也将打印源向量。这样，我们可以看到它作为移动源时发生了什么:\n\n```cpp\n           copy(begin(m), end(m), shell_it);\n           cout << 'n';\n\n           copy(begin(v), end(v), shell_it);\n           cout << 'n';\n       }\n```\n\n8.  让我们编译并运行这个程序，看看它说了什么。前两行很简单。它们反映了应用`copy_n`和`move`算法后地图包含的内容。第三行很有趣，因为它显示我们用作移动源的向量中的字符串现在是空的。这是因为字符串的内容没有被复制，而是被有效地*移动了*(这意味着映射使用了堆内存中的字符串数据，该数据先前被向量 *)* 中的字符串对象引用。在重新分配之前，我们通常不应该访问作为移动源的项目，但是为了这个实验，让我们忽略它:\n\n```cpp\n      $ ./copying_items\n      (1, one), (2, two), (3, three), \n      (1, one), (2, two), (3, three), (4, four), (5, five), \n      (1, ), (2, ), (3, ), (4, ), (5, ),\n```\n\n# 它是如何工作的...\n\n由于`std::copy`是最简单的 STL 算法之一，所以它的实现非常短。让我们看看它是如何实现的:\n\n```cpp\ntemplate <typename InputIterator, typename OutputIterator>\nOutputIterator copy(InputIterator it, InputIterator end_it, \n                    OutputIterator out_it)\n{\n    for (; it != end_it; ++ it, ++ out_it) {\n        *out_it = *it;\n    }\n    return out_it;\n}\n```\n\n这看起来就像一个人天真地用手实现从一个可迭代范围到另一个范围的项目复制。在这一点上，人们也可能会问，“那么为什么不手工实现呢，这个循环足够简单，我甚至不需要返回值？”，这当然是个好问题。\n\n虽然`std::copy`不是使代码明显变短的最佳示例，但许多其他实现更复杂的算法是这样的。不明显的是这类 STL 算法的隐藏式自动优化。如果我们碰巧将`std::copy`用于将它们的项目存储在连续内存中的数据结构(如`std::vector`和`std::array`所做的那样)*和*项目本身是*普通可分配副本*，那么编译器将选择完全不同的实现(假设迭代器类型是指针):\n\n```cpp\ntemplate <typename InputIterator, typename OutputIterator>\nOutputIterator copy(InputIterator it, InputIterator end_it, \n                    OutputIterator out_it)\n{\n    const size_t num_items (distance(it, end_it));\n    memmove(out_it, it, num_items * sizeof(*it));\n    return it + num_items;\n}\n```\n\n这是`std::copy`算法的`memmove`变体在典型的 STL 实现中的简化版本。比标准循环版*快*，这次*，读起来也没那么好听。但是，如果用户的参数类型符合这种优化的要求，那么`std::copy`用户会自动从中受益。编译器为选择的算法选择最快的实现，而用户代码很好地表达了*算法做什么*，而没有用太多的*如何*的细节污染代码。*\n\n *STL 算法通常只是在*可读性*和*最佳实现*之间提供最佳折衷。\n\nTypes are usually trivially copy assignable if they only consist of one or multiple (wrapped by a class/struct) scalar types or classes, which can safely be moved using `memcopy`/`memmove` without the need to invoke a user-defined copy assignment operator.\n\n我们也用了`std::move`。它的工作原理与`std::copy`完全一样，但是它将`std::move(*it)`应用于循环中的源迭代器，以便将*左值*转换为*右值*。这使得编译器选择目标对象的移动赋值运算符，而不是复制赋值运算符。对于很多复杂的对象，这个*执行*比较好但是*破坏*源对象。\n\n# 分类容器\n\n对值进行排序是一项相当标准的任务，可以通过各种方式完成。每一个被不得不学习大多数现有排序算法(以及它们的性能和稳定性在考试中的权衡)所折磨的计算机科学学生都知道这一点。\n\n因为这是一个已经解决的问题，程序员不应该再浪费时间去解决它*，除非是为了学习的目的。*\n\n *# 怎么做...\n\n在这一节中，我们将玩`std::sort`和`std::partial_sort`:\n\n1.  首先，我们包括所有必要的内容，并声明我们使用`std`命名空间:\n\n```cpp\n       #include <iostream>\n       #include <algorithm>\n       #include <vector>\n       #include <iterator>\n       #include <random>       \n\n       using namespace std;\n```\n\n2.  我们将多次打印整数向量的状态，所以让我们通过编写一个小程序来简化这个任务:\n\n```cpp\n       static void print(const vector<int> &v)\n       {\n           copy(begin(v), end(v), ostream_iterator<int>{cout, \", \"});\n           cout << 'n';\n       }\n```\n\n3.  我们从包含一些示例数字的向量开始:\n\n```cpp\n       int main()\n       {\n           vector<int> v {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};\n```\n\n4.  因为我们将对向量进行多次洗牌，以便使用不同的排序函数，所以我们需要一个随机数生成器:\n\n```cpp\n           random_device rd;\n           mt19937 g {rd()};\n```\n\n5.  `std::is_sorted`功能告诉我们容器的内容是否排序。该行应打印`1`:\n\n```cpp\n           cout << is_sorted(begin(v), end(v)) << 'n';\n```\n\n6.  借助`std::shuffle`，我们围绕向量的内容进行摇动，以便稍后再次排序。前两个参数表示将被混洗的范围，第三个参数是随机数生成器:\n\n```cpp\n           shuffle(begin(v), end(v), g);\n```\n\n7.  `is_sorted`函数现在应该返回`false`，以便打印`0`，向量中的值应该相同但顺序不同。我们将在将两者再次打印到外壳后看到:\n\n```cpp\n           cout << is_sorted(begin(v), end(v)) << 'n';\n           print(v);\n```\n\n8.  现在，我们使用`std::sort`重新建立原始物品订购。同样的打印到终端现在应该再次给我们从开始排序:\n\n```cpp\n           sort(begin(v), end(v));\n\n           cout << is_sorted(begin(v), end(v)) << 'n';\n           print(v);\n```\n\n9.  另一个有趣的功能是`std::partition`。也许，我们不想对列表进行完全排序，因为前面只有小于某个值的项目就足够了。所以，让我们*分割*向量，以便将所有小于`5`的项目移到前面并打印出来:\n\n```cpp\n           shuffle(begin(v), end(v), g);\n\n           partition(begin(v), end(v), [] (int i) { return i < 5; });\n\n           print(v);\n```\n\n10.  下一个与排序相关的功能是`std::partial_sort`。我们可以使用它来对容器的内容进行排序，但只是在一定程度上。它会将向量前半部分中所有向量元素中最小的`N`按排序顺序排列。其余的将驻留在后半部分，不会排序:\n\n```cpp\n           shuffle(begin(v), end(v), g);\n           auto middle (next(begin(v), int(v.size()) / 2));\n           partial_sort(begin(v), middle, end(v));\n\n           print(v);\n```\n\n11.  如果我们要对一个没有*比较运算符的数据结构进行排序会怎么样？让我们定义一个，并制作这样的项目的向量:*\n\n```cpp\n           struct mystruct {\n               int a;\n               int b;\n           };\n\n           vector<mystruct> mv {{5, 100}, {1, 50}, {-123, 1000}, \n                                {3, 70}, {-10, 20}};\n```\n\n12.  `std::sort`函数可选地接受比较函数作为其第三个参数。让我们使用它，并为它提供这样的功能。为了证明这是可能的，我们通过他们的*第二*场`b`来比较他们。这样，它们将按照`mystruct::b`而不是`mystruct::a`的顺序出现:\n\n```cpp\n           sort(begin(mv), end(mv),\n                [] (const mystruct &lhs, const mystruct &rhs) {\n                    return lhs.b < rhs.b;\n                });\n```\n\n13.  最后一步是打印`mystruct`项的排序向量:\n\n```cpp\n           for (const auto &[a, b] : mv) {\n               cout << \"{\" << a << \", \" << b << \"} \";\n           }\n           cout << 'n';\n       }\n```\n\n14.  让我们编译并运行我们的程序。\n    第一个`1`来自初始化排序向量后的`std::is_sorted`调用。然后，我们对向量进行洗牌，从第二个`is_sorted`呼叫中得到一个`0`。第三行显示了洗牌后的所有矢量项目。下一个`1`是用`std::sort`再次排序后`is_sorted`调用的结果。\n    然后，我们再次对整个向量进行混洗，*使用`std::partition`对其进行分割*。我们可以看到，所有小于`5`的项目在向量中也在`5`的左边。所有大于`5`的项目都在其右侧。除此之外，他们似乎被洗牌了。\n    倒数第二行显示`std::partial_sort`的结果。直到中间的所有项目看起来都是严格排序的，但其余的没有。\n    在最后一行，我们可以看到`mystruct`实例的向量。它们严格按照*第二个*成员值排序:\n\n```cpp\n      $ ./sorting_containers \n      1\n      0\n      7, 1, 4, 6, 8, 9, 5, 2, 3, 10, \n      1\n      1, 2, 3, 4, 5, 6, 7, 8, 9, 10, \n      1, 2, 4, 3, 5, 7, 8, 10, 9, 6, \n      1, 2, 3, 4, 5, 9, 8, 10, 7, 6,\n      {-10, 20} {1, 50} {3, 70} {5, 100} {-123, 1000}\n```\n\n# 它是如何工作的...\n\n我们使用了不同的算法，这些算法与排序有关:\n\n| **算法** | **目的** |\n| `std::sort` | 接受一个范围作为参数，并对其进行简单排序。 |\n| `std::is_sorted` | 接受一个范围作为参数，并告知*该范围是否已排序。* |\n| `std::shuffle` | 这是排序的*反向*操作；它接受一个范围作为参数，*将它的项目打乱。* |\n| `std::partial_sort` | 接受一个范围作为参数，并接受另一个迭代器，该迭代器告知输入范围应该在哪里排序。在迭代器后面，其余的项目看起来没有排序。 |\n| `std::partition` | 接受一个范围和一个*谓词函数*。谓词函数返回`true`的所有项目都被移到范围的前面。其余的移到后面。 |\n\n对于没有比较运算符`<`实现的对象，可以提供自定义的比较函数。这些应该总是有一个签名，如`bool function_name(const T &lhs, const T &rhs)`，并且在执行过程中不应该有任何副作用。\n\n还有其他算法如`std::stable_sort`，也是排序但保留排序键和`std::stable_partition`相同的项目顺序。\n\n`std::sort` has different implementations for sorting. Depending on the nature of the iterator arguments, it is implemented as selection sort, insertion sort, merge sort, or completely optimized for a smaller number of items. On the user side, we usually do not even need to care.\n\n# 从容器中移除特定项目\n\n复制、转换和过滤可能是对数据范围最常见的操作。在这一节中，我们集中于过滤项目。\n\n从数据结构中过滤出项目，或者简单地删除特定的项目，对于不同的数据结构来说，效果完全不同。例如，在链表(如`std::list`)中，一个节点可以通过使它的前身指向它的后继来移除。在以这种方式从链接链中移除一个节点后，它可以返回给分配器。在连续存储数据结构(`std::vector`、`std::array`以及某种程度上的`std::deque`)时，只能通过用其他项目覆盖它们来移除项目。如果某个项目插槽被标记为要移除，则必须将它后面的所有项目再向前移动一个插槽，以填充间隙。这听起来很麻烦，但是如果我们想简单地从字符串中删除空白，例如，这应该可以在没有太多代码的情况下实现。\n\n当手头有任何一个数据结构时，我们并不真的想关心*如何*移除一个项目。它应该发生。这就是`std::remove`和`std::remove_if`能为我们做的。\n\n# 怎么做...\n\n我们将通过不同的方式移除项目来转换向量的内容:\n\n1.  让我们导入所有需要的头，并声明我们使用`std`命名空间:\n\n```cpp\n       #include <iostream>\n       #include <vector>\n       #include <algorithm>\n       #include <iterator>      \n\n       using namespace std;\n```\n\n2.  一个简短的打印助手函数将打印我们的矢量:\n\n```cpp\n       void print(const vector<int> &v)\n       {\n           copy(begin(v), end(v), ostream_iterator<int>{cout, \", \"});\n           cout << 'n';\n       }\n```\n\n3.  我们将从包含一些简单整数值的示例向量开始。我们还将打印它，这样我们就可以看到它如何随着我们稍后应用于它的功能而变化:\n\n```cpp\n       int main()\n       {\n           vector<int> v {1, 2, 3, 4, 5, 6};\n           print(v);\n```\n\n4.  现在让我们从向量中移除所有值为`2`的项目。`std::remove`移动其他项目，使向量中的一个值`2`消失。因为移除项目后向量的实际内容更短，`std::remove`返回给我们一个指向*新结束*的迭代器。新的结束迭代器和旧的结束迭代器之间的项目被认为是垃圾，所以我们告诉向量*删除*它们。我们用一个新的作用域包围这两个移除行，因为`new_end`迭代器无论如何都是无效的，所以它可以立即超出作用域:\n\n```cpp\n           {\n               const auto new_end (remove(begin(v), end(v), 2));\n               v.erase(new_end, end(v));\n           }\n           print(v);\n```\n\n5.  现在让我们去掉所有的奇数号。为了做到这一点，我们实现了一个谓词，它告诉我们一个数是否是奇数，并将其输入到`std::remove_if`函数中，该函数接受这样的谓词:\n\n```cpp\n           {\n               auto odd_number ([](int i) { return i % 2 != 0; });\n               const auto new_end (\n                   remove_if(begin(v), end(v), odd_number));\n               v.erase(new_end, end(v));\n           }\n           print(v);\n```\n\n6.  我们尝试的下一个算法是`std::replace`。我们用它来用值`123`覆盖`4`的所有值。`std::replace`函数也作为`std::replace_if`存在，它也接受谓词函数:\n\n```cpp\n           replace(begin(v), end(v), 4, 123);\n           print(v);\n```\n\n7.  让我们向向量中注入全新的值，并创建两个新的空向量，以便对它们进行另一个实验:\n\n```cpp\n           v = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};\n\n           vector<int> v2;\n           vector<int> v3;\n```\n\n8.  然后，我们再次为奇数实现一个谓词，并实现另一个谓词函数，如果一个数是偶数，它会告诉相反的情况:\n\n```cpp\n           auto odd_number  ([](int i) { return i % 2 != 0; });\n           auto even_number ([](int i) { return i % 2 == 0; });\n```\n\n9.  接下来的两行做了完全相同的事情。它们甚至将*值复制到向量`v2`和`v3`中。第一行使用`std::remove_copy_if`算法来实现，该算法将所有内容从一个源容器复制到另一个容器，该容器不满足谓词约束。另一行使用`std::copy_if`，它复制*完成谓词约束的所有内容:**\n\n```cpp\n           remove_copy_if(begin(v), end(v), \n                          back_inserter(v2), odd_number);\n           copy_if(begin(v), end(v), \n                   back_inserter(v3), even_number);\n```\n\n10.  现在打印两个向量应该会得到相同的输出:\n\n```cpp\n           print(v2);\n           print(v3);\n       }\n```\n\n11.  让我们编译并运行这个程序。第一个输出行显示了初始化后的向量。第二行显示去掉`2`的所有值后。下一行显示了移除所有奇数的结果。在第四行之前，我们将`4`的所有值替换为`123`。\n    最后两行显示矢量`v2`和`v3`:\n\n```cpp\n      $ ./removing_items_from_containers \n      1, 2, 3, 4, 5, 6, \n      1, 3, 4, 5, 6, \n      4, 6, \n      123, 6, \n      2, 4, 6, 8, 10, \n      2, 4, 6, 8, 10, \n```\n\n# 它是如何工作的...\n\n我们使用了不同的算法，这与过滤有关:\n\n| **算法** | **目的** |\n| `std::remove` | 接受范围和值作为参数，并移除任何出现的值。返回修改范围的新结束迭代器。 |\n| `std::replace` | 接受一个范围和两个值作为参数，并用第二个值替换所有出现的第一个值。 |\n| `std::remove_copy` | 接受一个范围、一个输出迭代器和一个值作为参数，并将所有不等于给定值的值从范围复制到输出迭代器。 |\n| `std::replace_copy` | 类似于`std::replace`但类似于`std::remove_copy`的工作。源范围没有改变。 |\n| `std::copy_if` | 工作方式与`std::copy`类似，但额外接受一个谓词函数作为参数，以便仅复制谓词接受的值，这使其成为一个*过滤器*函数。 |\n\nFor every one of the listed algorithms, there also exists an `*_if` version, which accepts a predicate function instead of a value, which then decides which values are to be removed or replaced.\n\n# 转换容器的内容\n\n如果说`std::copy`是最简单的适用于范围的 STL 算法，`std::transform`则是第二简单的 STL 算法。就像`copy`一样，它将项目从一个范围复制到另一个范围，但另外接受一个转换函数。该转换函数可以在输入类型的值被分配给目标范围中的项目之前改变输入类型的值。此外，它甚至可以构造一个完全不同的类型，如果源范围和目标范围的有效载荷项目类型不同，这将非常有用。它使用简单，但仍然非常有用，这使得它成为便携式日常程序中使用的普通标准组件。\n\n# 怎么做...\n\n在本节中，我们将使用`std::transform`来修改向量的项目，同时复制它们:\n\n1.  像往常一样，我们首先需要包含所有必要的头，为了节省我们的打字时间，我们声明我们使用`std`名称空间:\n\n```cpp\n       #include <iostream>\n       #include <vector>\n       #include <string>\n       #include <sstream>\n       #include <algorithm>\n       #include <iterator>       \n\n       using namespace std;\n```\n\n2.  带有一些简单整数的向量将作为示例源数据结构:\n\n```cpp\n       int main()\n       {\n           vector<int> v {1, 2, 3, 4, 5};\n```\n\n3.  现在，我们将所有项目复制到一个`ostream_iterator`适配器，以便打印它们。`transform`函数接受一个函数对象，该对象接受容器有效载荷类型的项目，并在每次复制操作期间转换它们。在这种情况下，我们计算每个数字项目的*方块*，因此代码将打印向量中项目的方块，而无需我们将它们存储在任何地方:\n\n```cpp\n           transform(begin(v), end(v), \n               ostream_iterator<int>{cout, \", \"},\n               [] (int i) { return i * i; });\n           cout << 'n';\n```\n\n4.  让我们做另一个转换。例如，从数字`3`中，我们可以生成一个可读性很好的字符串，比如`3^2 = 9`。下面的`int_to_string`函数对象就是使用`std::stringstream`对象实现的:\n\n```cpp\n           auto int_to_string ([](int i) {\n               stringstream ss;\n               ss << i << \"^2 = \" << i * i;\n               return ss.str();\n           });\n```\n\n5.  我们刚刚实现的函数从整数值返回字符串值。我们也可以说它将 T2 从整数映射到字符串。使用`transform`函数，我们可以将所有这样的映射从整数向量复制到字符串向量中:\n\n```cpp\n           vector<string> vs;\n\n           transform(begin(v), end(v), back_inserter(vs),\n                     int_to_string);\n```\n\n6.  打印完这些，我们就完成了:\n\n```cpp\n           copy(begin(vs), end(vs), \n                ostream_iterator<string>{cout, \"n\"});\n      }\n```\n\n7.  让我们编译并运行这个程序:\n\n```cpp\n      $ ./transforming_items_in_containers \n      1, 4, 9, 16, 25, \n      1^2 = 1\n      2^2 = 4\n      3^2 = 9\n      4^2 = 16\n      5^2 = 25\n```\n\n# 它是如何工作的...\n\n`std::transform`函数的工作方式与`std::copy`完全相同，但是在将值从源迭代器复制分配给目标迭代器时，它会在将结果分配给目标迭代器之前，将用户提供的转换函数应用于该值。\n\n# 在有序和无序向量中查找项目\n\n通常，我们需要告诉*在某个范围内是否存在某种物品。如果是这样，我们通常还需要修改它或访问与之相关的其他数据。*\n\n寻找物品有不同的策略。如果项目以排序的顺序出现，那么我们可以做一个二分搜索法，这比一个接一个地线性浏览项目要快。如果没有排序，我们又陷入了线性遍历。\n\n典型的 STL 搜索算法可以为我们做到这两点，所以了解它们和它们的特性是很好的。这一部分是关于简单线性搜索算法`std::find`、二分搜索法版本`std::equal_range`及其变体。\n\n# 怎么做...\n\n在本节中，我们将在一个小的示例数据集上使用线性和二分搜索法算法:\n\n1.  我们首先包含所有必要的头，并声明我们使用`std`命名空间:\n\n```cpp\n      #include <iostream>\n      #include <vector>\n      #include <list>\n      #include <algorithm>\n      #include <string>\n\n      using namespace std;\n```\n\n2.  我们的数据集将由`city`结构组成，这些结构只保存一个城市的名称及其人口数量:\n\n```cpp\n      struct city {\n          string name;\n          unsigned population;\n      };\n```\n\n3.  搜索算法需要能够比较一个项目和另一个项目，所以我们为`city`结构实例重载`==`运算符:\n\n```cpp\n      bool operator==(const city &a, const city &b) {\n          return a.name == b.name && a.population == b.population;\n      }\n```\n\n4.  我们还想打印`city`实例，所以我们重载了流操作符`<<`:\n\n```cpp\n      ostream& operator<<(ostream &os, const city &city) {\n          return os << \"{\" << city.name << \", \" \n                    << city.population << \"}\";\n      }\n```\n\n5.  搜索函数通常返回迭代器。这些迭代器指向找到的项，否则指向底层容器的结束迭代器。在最后一种情况下，我们不允许访问这样的迭代器。因为我们要打印我们的搜索结果，所以我们实现了一个函数，该函数向我们返回另一个函数对象，该对象封装了数据结构的结束迭代器。当用于打印时，它会将其迭代器参数与结束迭代器进行比较，然后打印该项，否则，只需`<end>`:\n\n```cpp\n      template <typename C>\n      static auto opt_print (const C &container)\n      {\n          return [end_it (end(container))] (const auto &item) {\n              if (item != end_it) {\n                  cout << *item << 'n';\n              } else {\n                  cout << \"<end>n\";\n              }\n          };\n      }\n```\n\n6.  我们从一些德国城市的示例向量开始:\n\n```cpp\n      int main()\n      {\n          const vector<city> c {\n              {\"Aachen\",        246000},\n              {\"Berlin\",       3502000},\n              {\"Braunschweig\",  251000},\n              {\"Cologne\",      1060000}\n          };\n```\n\n7.  使用这个助手，我们构建了一个城市打印机函数，它捕获了我们的城市向量`c`的结束迭代器:\n\n```cpp\n          auto print_city (opt_print(c));\n```\n\n8.  我们用`std::find`找到矢量中的项目，省去了科隆的城市项目。起初，这种搜索看起来毫无意义，因为我们得到的正是我们搜索的项目。但是我们之前并不知道它在向量中的位置，而`find`函数正好返回给我们。然而，例如，我们可以让我们重载的`city`结构的运算符`==`只比较城市名称，然后我们可以仅使用城市名称进行搜索，甚至不知道它的人口。但这不是一个好的设计。下一步，我们将以不同的方式进行:\n\n```cpp\n          {\n              auto found_cologne (find(begin(c), end(c), \n                  city{\"Cologne\", 1060000}));\n              print_city(found_cologne);\n          }\n```\n\n9.  在不知道一个城市的人口数量，也不篡改其`==`运算符的情况下，我们只能通过将其名称与向量内容进行比较来进行搜索。`std::find_if`函数接受谓词函数对象，而不是特定值。这样，当我们只知道科隆城市的名字时，我们可以搜索它:\n\n```cpp\n          {\n              auto found_cologne (find_if(begin(c), end(c), \n                  [] (const auto &item) {\n                      return item.name == \"Cologne\";\n                  }));\n              print_city(found_cologne);\n          }\n```\n\n10.  为了使搜索更加美观和有表现力，我们可以实现谓词构建器。`population_higher_than`函数对象接受一个群体大小，并返回一个函数，告诉我们`city`实例的群体是否大于捕获的值。让我们用它在我们的小例子集中搜索一个拥有 200 多万居民的德国城市。在给定的矢量内，那个城市只有柏林:\n\n```cpp\n          {\n              auto population_more_than ([](unsigned i) {\n                  return [=] (const city &item) { \n                      return item.population > i; \n                  };\n              });\n              auto found_large (find_if(begin(c), end(c), \n                  population_more_than(2000000)));\n              print_city(found_large);\n          }\n```\n\n11.  我们刚刚使用的搜索函数线性遍历我们的容器。因此，它们具有运行时复杂性 *O(n)* 。STL 还有二分搜索法函数，在*O(log(n)】*内工作。让我们生成一个新的示例数据集，它只是由一些整数值组成，并为此构建另一个`print`函数:\n\n```cpp\n          const vector<int> v {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};\n\n          auto print_int (opt_print(v));\n```\n\n12.  `std::binary_search`函数返回布尔值，只是告诉我们*如果*找到了一个项目，但是它会*而不是*返回项目本身。重要的是，我们正在搜索的容器是*分类的*，因为否则，二分搜索法不能正常工作:\n\n```cpp\n          bool contains_7 {binary_search(begin(v), end(v), 7)};\n          cout << contains_7 << 'n';\n```\n\n13.  为了获得我们正在搜索的项目，我们需要其他的 STL 函数。其中之一就是`std::equal_range`。它不返回我们找到的项目的迭代器，而是一对迭代器。第一个迭代器指向第一个不小于我们一直在寻找的值的项目*。第二个迭代器指向比它大*的第一个项目。在我们的范围内，从`1`到`10`，第一个迭代器指向实际的`7`，因为它是第一个项目，并不比`7`小。第二个迭代器指向`8`，因为它是第一个大于`7`的项目。如果我们有多个`7`值，两个迭代器实际上会表示一个*子范围*的项目:**\n\n```cpp\n          auto [lower_it, upper_it] (\n              equal_range(begin(v), end(v), 7));\n          print_int(lower_it);\n          print_int(upper_it);\n```\n\n14.  如果我们只需要一个迭代器；我们可以用`std::lower_bound`或者`std::upper_bound`。`lower_bound`函数只返回一个不小于我们搜索的第一项的迭代器。`upper_bound`函数返回一个迭代器到第一个大于我们搜索的项目:\n\n```cpp\n          print_int(lower_bound(begin(v), end(v), 7));\n          print_int(upper_bound(begin(v), end(v), 7));\n      }\n```\n\n15.  让我们编译并运行程序，看看输出是否符合我们的假设:\n\n```cpp\n      $ ./finding_items \n      {Cologne, 1060000}\n      {Cologne, 1060000}\n      {Berlin, 3502000}\n      1\n      7\n      8\n      7\n      8\n```\n\n# 它是如何工作的...\n\n这些是我们在本食谱中使用的搜索算法:\n\n| **算法** | **目的** |\n| `std::find` | 接受搜索范围和比较值作为参数。返回一个迭代器，该迭代器指向等于比较值的第一项。线性搜索。 |\n| `std::find_if` | 工作方式与`std::find`类似，但使用谓词函数而不是比较值。 |\n| `std::binary_search` | 接受搜索范围和比较值作为参数。执行二分搜索法运算，如果范围包含该值，则返回`true`。 |\n| `std::lower_bound` | 接受搜索范围和比较值，然后对不小于比较值的第一个项目*执行二分搜索法运算。返回指向该项的迭代器。* |\n| `std::upper_bound` | 工作方式与`std::lower_bound`类似，但返回第一个大于比较值的项的迭代器。 |\n| `std::equal_range` | 接受一个搜索范围和一个比较值，然后返回一对迭代器。第一个迭代器是`std::lower_bound`的结果，第二个迭代器是`std::upper_bound`的结果。 |\n\n所有这些函数都接受自定义比较函数作为可选的附加参数。这样，搜索可以定制，就像我们在食谱中做的那样。\n\n让我们仔细看看`std::equal_range`是如何工作的。假设我们有一个向量`v = {0, 1, 2, 3, 4, 5, 6, 7, 7, 7, 8}`，并调用`equal_range(begin(v), end(v), 7);`，以便为值`7`执行二分搜索法运算。当`equal_range`返回给我们一对下界和上界迭代器时，这些迭代器应该在之后表示范围`{7, 7, 7}`，因为在排序的向量中有这么多`7`的值。为了更清楚起见，请查看下图:\n\n![](img/8e00c5a5-38e8-4902-95ad-566642ea317b.png)\n\n首先，`equal_range`使用典型的二分搜索法方法，直到它进入不小于搜索值的值范围*。然后，它分成一个`lower_bound`调用和一个`upper_bound`调用，以便将它们的返回值捆绑成一对作为返回值。*\n\n *为了得到一个二分搜索法函数，它只返回符合要求的第一个项目，我们可以实现以下内容:\n\n```cpp\ntemplate <typename Iterator, typename T>\nIterator standard_binary_search(Iterator it, Iterator end_it, T value)\n{\n    const auto potential_match (lower_bound(it, end_it, value));\n    if (potential_match != end_it && value == *potential_match) {\n        return potential_match;\n    }\n    return end_it;\n}\n```\n\n该功能使用`std::lower_bound`查找不小于`value`的第一项。由此产生的`potential_match`可以指向三种不同的情况:\n\n*   无项目不小于`value`。在这种情况下，与`end_it`相同。\n*   不小于`value`的第一项也是比`value`大的*。因此，我们必须通过返回`end_it`来表明我们找到了*而不是*。*\n**   `potential_match`指向的项目等于`value`。所以，这不仅是一场*势*的比赛，更是一场*实*的比赛。所以我们可以退货。*\n\n *如果我们的类型`T`不支持`==`操作符，那它至少要支持二分搜索法的`<`操作符。然后，我们可以将对比改写为`!(value < *potential_match) && !(*potential_match < value)`。如果它既不小，也不大，那么它必须相等。\n\nSTL 没有开箱即用地提供这种功能的一个潜在原因是缺少关于存在多次命中可能性的知识，正如图中我们有多个`7`值。\n\nNote that data structures such as `std::map`, `std::set`, and so on have their *own* `find` functions. These are, of course, faster than the more general algorithms because they are tightly coupled with the data structure's implementation and data representation.\n\n# 使用标准::箝位将向量值限制在特定的数值范围内\n\n在许多应用中，我们从某个地方获取数字数据。在我们绘制或以其他方式处理它之前，可能需要对它进行归一化，因为这些值彼此相差很远。\n\n通常，这意味着对保存所有这些值的数据结构进行一点`std::transform`调用，结合一个简单的*缩放*函数。但是如果我们*不知道*值有多大或者有多小，我们需要首先浏览数据，以便为缩放函数找到正确的*尺寸*。\n\nSTL 包含用于此目的的有用功能:`std::minmax_element`和`std::clamp`。使用这些并结合一些 lambda 表达式胶水，我们可以很容易地执行这样的任务。\n\n# 怎么做...\n\n在本节中，我们将以两种不同的方式将向量值从示例数值范围归一化为归一化值，其中一种使用`std::minmax_element`，另一种使用`std::clamp`:\n\n1.  像往常一样，我们首先需要包含以下头，并声明我们使用`std`命名空间:\n\n```cpp\n       #include <iostream>\n       #include <vector>\n       #include <algorithm>\n       #include <iterator>       \n\n       using namespace std;\n```\n\n2.  我们实现了一个函数供以后使用，该函数接受一个范围的最小值和最大值，以及一个新的最大值，这样它就可以将旧范围的值投影到我们想要的较小范围。函数对象接受这样的值，并返回另一个函数对象，该函数对象正好执行该转换。为简单起见，新的最小值为`0`，因此无论旧数据有什么偏移，其归一化值都将始终相对于`0`。为了可读性，我们忽略了`max`和`min`可能具有相同值的可能性，这将导致除以零:\n\n```cpp\n       static auto norm (int min, int max, int new_max)\n       {\n           const double diff (max - min);\n           return [=] (int val) {\n               return int((val - min) / diff * new_max);\n           };\n       }\n```\n\n3.  另一个名为`clampval`的函数对象构建器返回一个函数对象，该函数对象捕获`min`和`max`值，并用这些值调用`std::clamp`，以便将它们的值限制在该范围内:\n\n```cpp\n       static auto clampval (int min, int max)\n       {\n           return [=] (int val) -> int {\n               return clamp(val, min, max);\n           };\n       }\n```\n\n4.  我们要标准化的数据是一个不同值的向量。例如，这可能是某种热度数据、景观高度或一段时间内的股价:\n\n```cpp\n       int main()\n       {\n           vector<int> v {0, 1000, 5, 250, 300, 800, 900, 321};\n```\n\n5.  为了能够对数据进行归一化，我们需要*最高的*和*最低的*值。`std::minmax_element`功能在这里帮助很大。它返回给我们一对迭代器，正好是这两个值:\n\n```cpp\n           const auto [min_it, max_it] (\n               minmax_element(begin(v), end(v)));\n```\n\n6.  我们将把第一个向量的所有值复制到第二个向量。让我们实例化第二个向量，并准备好接受与第一个向量中一样多的新项目:\n\n```cpp\n           vector<int> v_norm;\n           v_norm.reserve(v.size());\n```\n\n7.  使用`std::transform`，我们将值从第一个向量复制到第二个向量。在复制项目时，它们将被我们的规范化助手转换。旧向量的最小值和最大值是`0`和`1000`。归一化后的最小值和最大值分别为`0`和`255`:\n\n```cpp\n           transform(begin(v), end(v), back_inserter(v_norm),\n                     norm(*min_it, *max_it, 255));\n```\n\n8.  在我们实施另一个标准化策略之前，我们先打印我们现在所拥有的:\n\n```cpp\n           copy(begin(v_norm), end(v_norm), \n                ostream_iterator<int>{cout, \", \"});\n           cout << 'n';\n```\n\n9.  我们与另一个辅助工具`clampval`重复使用相同的归一化向量，该辅助工具*将*旧范围夹到最小`0`和最大`255`的范围内:\n\n```cpp\n           transform(begin(v), end(v), begin(v_norm), \n                     clampval(0, 255));\n```\n\n10.  打印完这些值后，我们就完成了:\n\n```cpp\n           copy(begin(v_norm), end(v_norm),\n                ostream_iterator<int>{cout, \", \"});\n           cout << 'n';\n       }\n```\n\n11.  让我们编译并运行这个程序。将值减少到从`0`到`255`的值，我们可以将它们用作 RGB 颜色代码的亮度值，例如:\n\n```cpp\n      $ ./reducing_range_in_vector \n      0, 255, 1, 63, 76, 204, 229, 81, \n      0, 255, 5, 250, 255, 255, 255, 255,\n```\n\n12.  当我们绘制数据时，我们得到以下图表。正如我们所看到的，我们用最小值和最大值之差除以值的方法是原始数据的线性变换。*夹住的*图形丢失了一些信息。这两种变化在不同的情况下都很有用:\n\n![](img/f35fa700-2874-4d52-a587-32e53adebcf0.png)\n\n# 它是如何工作的...\n\n除了`std::transform`之外，我们使用了两种算法:\n\n`std::minmax_element`简单地接受输入范围的开始和结束迭代器。它在范围内循环，记录最大和最小的元素。这些值成对返回，然后用于我们的缩放函数。\n\n相比之下，`std::clamp`函数不在可迭代范围内运行。它接受三个值:输入值、最小值和最大值。该函数的输出是输入值截止值，它位于允许的最小值和最大值之间。我们也可以写`max(min_val, min(max_val, x))`而不是`std::clamp(x, min_val, max_val)`。\n\n# 使用 std::搜索在字符串中定位模式并选择最佳实现\n\n在字符串中搜索一个字符串与在一个范围内找到一个*对象是稍微不同的问题。一方面，字符串当然也是一个可迭代的范围(字符)。另一方面，在一个字符串中找到一个字符串意味着在另一个*范围中找到一个范围。这伴随着每个潜在匹配位置的多次比较，所以我们需要一些其他的算法。**\n\n`std::string`已经包含了一个`find`函数，可以做我们正在谈论的事情；然而，我们将在这一节集中讨论`std::search`。虽然`std::search`可能主要用于字符串，但它适用于各种容器。`std::search`更有趣的特点是，自 C++ 17 以来，它有一个稍微不同的附加界面，允许简单地交换搜索算法本身。这些算法经过优化，可以由用户自由选择，这取决于在哪个用例中哪个更好。此外，如果我们想出比已经提供的更好的东西，我们可以实现自己的搜索算法并将其插入`std::search`。\n\n# 怎么做...\n\n我们将对字符串使用新的`std::search`函数，并尝试它与搜索器对象的不同变体:\n\n1.  首先，我们将包括所有必要的头，并声明我们使用`std`命名空间:\n\n```cpp\n       #include <iostream>\n       #include <string>\n       #include <algorithm>\n       #include <iterator>\n       #include <functional>       \n\n       using namespace std;\n```\n\n2.  我们将根据搜索算法返回给我们的位置打印子字符串，因此让我们为此实现一个小助手:\n\n```cpp\n       template <typename Itr>\n       static void print(Itr it, size_t chars)\n       {\n           copy_n(it, chars, ostream_iterator<char>{cout});\n           cout << 'n';\n       }\n```\n\n3.  一个 *lorem-ipsum 风格的*字符串将作为我们的示例字符串，我们将在其中搜索一个子字符串。在这种情况下，这是`\"elitr\"`:\n\n```cpp\n       int main()\n       {\n           const string long_string {\n               \"Lorem ipsum dolor sit amet, consetetur\"\n               \" sadipscing elitr, sed diam nonumy eirmod\"};\n           const string needle {\"elitr\"};\n```\n\n4.  旧的`std::search`接口接受字符串的开始/结束迭代器和子字符串的开始/结束迭代器，我们在字符串中搜索特定的子字符串。然后它返回一个迭代器，指向它能找到的子串。如果没有找到字符串，返回的迭代器将是结束迭代器:\n\n```cpp\n           {\n               auto match (search(begin(long_string), end(long_string),\n                                  begin(needle), end(needle)));\n               print(match, 5);\n           }\n```\n\n5.  `std::search`的 C++ 17 版本不接受两对迭代器，而是接受一对开始/结束迭代器和一个*搜索器*对象。`std::default_searcher`取我们在较大字符串中搜索的子串的开始/结束对迭代器:\n\n```cpp\n           {\n               auto match (search(begin(long_string), end(long_string),\n                   default_searcher(begin(needle), end(needle))));\n               print(match, 5);\n           }\n```\n\n6.  这种变化的意义在于，以这种方式切换搜索算法很容易。`std::boyer_moore_searcher`使用*博耶-摩尔搜索算法*进行更快的搜索:\n\n```cpp\n           {\n               auto match (search(begin(long_string), end(long_string),\n                   boyer_moore_searcher(begin(needle), \n                                        end(needle))));\n               print(match, 5);\n           }\n```\n\n7.  C++ 17 STL 附带了三种不同的搜索对象实现。第三个是 B *欧耶-摩尔-霍斯普搜索算法*的实现:\n\n```cpp\n           {\n               auto match (search(begin(long_string), end(long_string),\n                   boyer_moore_horspool_searcher(begin(needle), \n                                                 end(needle))));\n               print(match, 5);\n           }\n       }\n```\n\n8.  让我们编译并运行我们的程序。如果运行正确，我们应该到处都能看到相同的字符串:\n\n```cpp\n      $ ./pattern_search_string \n      elitr\n      elitr\n      elitr\n      elitr\n```\n\n# 它是如何工作的...\n\n为了得到完全相同的结果，我们使用了四种不同的方法来使用`std::search`。在什么情况下我们应该选择哪一个？\n\n假设我们搜索模式的大字符串叫做`s`，模式叫做`p`。然后，`std::search(begin(s), end(s), begin(p), end(p));`和`std::search(begin(s), end(s), default_searcher(begin(p), end(p));`做完全一样的事情。\n\n其他搜索器功能对象用更复杂的搜索算法实现:\n\n*   `std::default_searcher`:这将重定向到遗留的`std::search`实现\n*   `std::boyer_moore_searcher`:这使用*博耶-摩尔*搜索算法\n*   `std::boyer_moore_horspool_searcher`:这类似地使用*博耶-摩尔-霍斯普*算法\n\n是什么让其他算法如此特别？Boyer-Moore 算法的开发有一个特定的想法——搜索模式与字符串进行比较，从模式的*末端*开始，从右到左。如果搜索字符串*中的字符与重叠位置的模式中的字符不同*，并且*甚至没有出现在模式中*，那么很明显，模式可以通过其*全长*在搜索字符串上移动。请看下图，这发生在步骤 1 中。如果当前正在比较的字符与该位置的模式字符不同，但被模式包含在*中，那么算法知道需要向右移动多少个字符，以便至少与该字符正确对齐，然后，它从从右到左的比较开始。在图中，这发生在步骤 2 中。这样，与天真的搜索实现相比，博耶-摩尔算法可以省略大量不必要的比较:*\n\n![](img/2cefec94-bb6d-48ce-af86-cb86df918853.png)\n\n当然，如果它没有带来自己的*权衡*，这将成为新的默认搜索算法。它比默认算法更快，但它需要快速查找数据结构，以便确定搜索模式中包含哪些字符以及它们位于哪个偏移量。编译器将根据模式包含的底层类型选择不同的复杂实现(复杂类型的哈希映射和类型的原语查找表，如`char`)。最后，这意味着如果搜索字符串不太大，默认搜索实现将会更快。如果搜索本身需要一些相当长的时间，那么博耶-摩尔算法可以在*常数因子*的维度上带来性能提升。\n\n**博耶-摩尔-霍斯普**算法是博耶-摩尔算法的简化。它放弃了*不良字符*规则，如果找到模式字符串中没有的搜索字符串字符，将导致整个模式宽度的移动。这个决定的代价是*比 Boyer-Moore 的未修改版本*稍微慢一点，但是它的操作也需要*更少的数据结构*。\n\nDo not try to *reason* about which algorithm *should* be faster in a specific case. Always *measure* the performance of your code with data samples that are typical for your users and base your decision on the *results*.\n\n# 采样大向量\n\n当在某些情况下有非常大量的数值数据需要处理时，可能不可能在可行的时间内处理完。在这种情况下，可以对数据进行*采样*，以减少进一步处理的数据总量，然后*加快整个程序的速度。在其他情况下，这样做可能不是为了减少处理工作量，而是为了*保存*或*传输*数据。*\n\n一个天真的采样想法可能是只选择每个*N<sup>th</sup>T4】数据点。这在很多情况下可能没问题，但在信号处理中，例如，它*可能会*导致一种称为**混叠**的数学现象。如果每个样本之间的距离变化一个小的随机偏移，混叠可以减少。请看下图，图中显示了一个*极端情况*只是为了说明这一点——虽然原始信号由正弦波组成，但图上的三角形点是采样点，正好在每一个*第 100 个*数据点采样。不幸的是，信号在这些点上有相同的 y 值*！由这些点连接而成的图形看起来像一条完全直的*水平线*。然而，方形点显示了我们采样每个`100 + random(-15, +15)`点时得到的结果。在这里，信号看起来仍然与原始信号非常不同，但它至少没有像固定步长采样情况下那样完全*消失*。**\n\n`std::sample`功能不为固定偏移的采样点添加随机变更，而是选择完全随机的点；因此，它的工作原理与本例略有不同:\n\n![](img/9f2aae52-6c69-479c-9d10-af4a807faf4e.png)\n\n# 怎么做...\n\n我们将采样一个非常大的随机数据向量。这一随机数据呈正态分布。采样后，结果点仍应显示正态分布，我们将对此进行检查:\n\n1.  首先，我们需要包含我们使用的所有内容，并声明我们使用`std`命名空间，以便节省一些打字时间:\n\n```cpp\n       #include <iostream>\n       #include <vector>\n       #include <random>\n       #include <algorithm>\n       #include <iterator>\n       #include <map>\n       #include <iomanip>       \n\n       using namespace std;\n```\n\n2.  如果我们在自己的常量变量中配置算法的特定特性，就更容易玩转代码。这些是大随机向量的大小和我们要从中提取的样本数量:\n\n```cpp\n       int main()\n       {\n           const size_t data_points   {100000};\n           const size_t sample_points {100};\n```\n\n3.  大的、随机填充的向量应该从随机数生成器中获得数字，该生成器根据正态分布给出数字。任何正态分布都可以用平均值和平均值的标准偏差来表征:\n\n```cpp\n           const int    mean {10};\n           const size_t dev  {3};\n```\n\n4.  现在，我们设置随机生成器。首先，我们实例化一个随机设备，并调用它一次，为随机生成器的构造函数获取一个种子。然后，我们实例化一个将正态分布应用于随机输出的分布对象:\n\n```cpp\n           random_device rd;\n           mt19937 gen {rd()};\n           normal_distribution<> d {mean, dev};\n```\n\n5.  现在，我们实例化一个整数向量，并用大量随机数填充它。这是使用`std::generate_n`算法实现的，该算法将调用一个生成器函数对象，使用`back_inserter`迭代器将其返回值馈送到我们的向量中。生成器函数对象只是包装`d(gen)`表达式，该表达式从随机设备中获取一个随机数，并将其馈送到分布对象中:\n\n```cpp\n           vector<int> v;\n           v.reserve(data_points);\n\n           generate_n(back_inserter(v), data_points, \n               [&] { return d(gen); });\n```\n\n6.  现在，我们实例化另一个向量，它将包含更小的样本集:\n\n```cpp\n           vector<int> samples;\n           v.reserve(sample_points);\n```\n\n7.  `std::sample`算法的工作原理与`std::copy`相似，但它需要两个额外的参数:*样本数*，它将从输入范围中获取；以及*随机数生成器*对象，它将参考该对象来获取随机采样位置:\n\n```cpp\n           sample(begin(v), end(v), back_inserter(samples), \n                  sample_points, mt19937{random_device{}()});\n```\n\n8.  我们已经完成了采样。代码的其余部分用于显示目的。输入数据具有正态分布，如果采样算法运行良好，那么采样向量也应该呈现正态分布。为了查看还剩下多少正态分布，我们将打印以下值的*直方图*:\n\n```cpp\n           map<int, size_t> hist;\n\n           for (int i : samples) { ++ hist[i]; }\n```\n\n9.  最后，我们循环所有项目，以便打印直方图:\n\n```cpp\n           for (const auto &[value, count] : hist) {\n               cout << setw(2) << value << \" \"\n                    << string(count, '*') << 'n';\n           }    \n       }\n```\n\n10.  编译并运行程序后，我们看到采样向量仍然大致呈现正态分布的特征:\n\n![](img/429739c2-32fd-4b71-8557-d1af399d9b7d.png)\n\n# 它是如何工作的...\n\n`std::sample`算法是 C++ 17 自带的新算法。它的签名是这样的:\n\n```cpp\ntemplate<class InIterator, class OutIterator,\n         class Distance, class UniformRandomBitGenerator>\nOutIterator sample(InIterator first, InIterator last,\n                   SampleIterator out, Distance n, \n                   UniformRandomBitGenerator&& g);\n\n```\n\n输入范围由`first`和`last`迭代器表示，而`out`是输出运算符。这些迭代器的功能与`std::copy`完全相同；项目从一个范围复制到另一个范围。`std::sample`算法的特殊之处在于它将只复制输入范围的一部分，因为它只对`n`项进行采样。它在内部使用均匀分布，因此源范围内的每个数据点都以相同的概率被选择。\n\n# 生成输入序列的排列\n\n当测试必须处理输入序列的代码时，参数的顺序并不重要，测试它是否为该输入的所有可能排列产生相同的输出是有益的。例如，这样的测试可以检查自我实现的*排序*算法是否正确排序。\n\n不管出于什么原因我们需要某个取值范围的所有置换，`std::next_permutation`都可以方便地为我们做到。我们可以在可修改的范围内调用它，它会将其项目的*顺序*更改为下一个*字典排列*。\n\n# 怎么做...\n\n在本节中，我们将编写一个从标准输入中读取多个单词字符串的程序，然后我们将使用`std::next_permutation`生成并打印这些字符串的所有排列:\n\n1.  先有事情再说；我们包括所有必要的头，并声明我们使用`std`命名空间:\n\n```cpp\n      #include <iostream>\n      #include <vector>\n      #include <string>\n      #include <iterator>\n      #include <algorithm>      \n\n      using namespace std;\n```\n\n2.  我们从一个字符串向量开始，用整个标准输入来填充。下一步是*排序*向量:\n\n```cpp\n      int main()\n      {\n          vector<string> v {istream_iterator<string>{cin}, {}};\n          sort(begin(v), end(v));\n```\n\n3.  现在，我们在用户终端上打印矢量的内容。之后，我们称之为`std::next_permutation`。它系统地对向量进行洗牌，以生成其项目的排列，然后我们再次打印。一旦达到最后一个*排列，`next_permutation`将返回`false`:*\n\n```cpp\n          do {\n              copy(begin(v), end(v), \n                   ostream_iterator<string>{cout, \", \"});\n              cout << 'n';\n          } while (next_permutation(begin(v), end(v)));\n      }\n```\n\n4.  让我们编译并运行带有一些示例输入的函数:\n\n```cpp\n      $ echo \"a b c\" | ./input_permutations \n      a, b, c, \n      a, c, b, \n      b, a, c, \n      b, c, a, \n      c, a, b, \n      c, b, a,\n```\n\n# 它是如何工作的...\n\n`std::next_permutation`算法用起来有点怪。这是因为它只接受一对开始/结束迭代器，然后如果能够找到下一个置换就返回`true`。否则，返回`false`。但是*下一个排列*到底是什么意思？\n\n`std::next_permutation`查找项目的下一个字典顺序的算法工作如下:\n\n1.  找到最大的索引`i`，这样`v[i - 1] < v[i]`。如果没有，则返回`false`。\n2.  现在，找到最大的指数`j`，比如`j >= i`和`v[j] > v[i - 1]`。\n3.  *将`j`位置的物品和`i - 1`位置的物品互换*。\n4.  将项目的顺序从位置`i`反转到范围的末端。\n5.  返回`true`。\n\n我们从中得出的单独排列的顺序将总是以相同的顺序出现。为了查看所有可能的排列，我们首先对数组进行排序，因为例如，如果我们输入`\"c b a\"`，算法将立即终止*，因为这已经是*元素的最后一个字典顺序*。*\n\n *# 实现字典合并工具\n\n想象一下，我们有一个排序后的事物列表，别人拿出另一个排序后的事物列表，我们想互相分享列表。最好的办法是将这两个列表结合起来。这两个列表的组合也应该排序，因为这样，很容易查找特定的项目。\n\n这样的操作也叫做**合并**。为了合并两个已排序的项目范围，我们将直观地创建一个新的范围，并用两个列表中的项目填充它。对于每次项目转移，我们必须比较输入范围的最前面的项目，以便始终从输入剩余的项目中选择*最小的*。否则，输出范围将不再排序。下图更好地说明了这一点:\n\n![](img/a6528449-cc62-4b3f-b4bd-1900646d9175.png)\n\n`std::merge`算法正好可以为我们做到这一点，所以我们不需要瞎折腾太多。在本节中，我们将看到如何使用该算法。\n\n# 怎么做...\n\n我们将建立一个便宜的字典，从英语单词到德语翻译的一对一映射，并将它们存储在`std::deque`结构中。程序将从一个文件和一个标准输入中读取这样的字典，并在标准输出上再次打印一个大的合并字典。\n\n1.  这次要包含很多头，我们声明使用`std`命名空间:\n\n```cpp\n      #include <iostream>\n      #include <algorithm>\n      #include <iterator>\n      #include <deque>\n      #include <tuple>\n      #include <string>\n      #include <fstream>     \n\n      using namespace std;\n```\n\n2.  字典条目应该由从一种语言的字符串到另一种语言的字符串的对称映射组成:\n\n```cpp\n      using dict_entry = pair<string, string>;\n```\n\n3.  我们都要将这样的对打印到终端，并从用户输入中读取它们，因此我们需要重载`<<`和`>>`操作符:\n\n```cpp\n      namespace std {\n      ostream& operator<<(ostream &os, const dict_entry p)\n      {\n          return os << p.first << \" \" << p.second;\n      }\n\n      istream& operator>>(istream &is, dict_entry &p)\n      {\n          return is >> p.first >> p.second;\n      }\n\n      }\n```\n\n4.  一个接受任何输入流对象的帮助函数将帮助我们从它构建一个字典。它构造字典条目对的`std::deque`，并且它们都是从输入流中读取的，直到它为空。在退回之前，我们对其进行分类:\n\n```cpp\n      template <typename IS>\n      deque<dict_entry> from_instream(IS &&is)\n      {\n          deque<dict_entry> d {istream_iterator<dict_entry>{is}, {}};\n          sort(begin(d), end(d));\n          return d;\n      }\n```\n\n5.  我们从不同的输入流创建两个单独的字典数据结构。从`dict.txt`文件打开一个输入流，我们假设它存在。它包含一行一行的单词对。另一个流是标准输入:\n\n```cpp\n      int main()\n      {\n          const auto dict1 (from_instream(ifstream{\"dict.txt\"}));\n          const auto dict2 (from_instream(cin));\n```\n\n6.  由于辅助函数`from_instream`已经为我们整理了这两个字典，我们可以直接将它们输入到`std::merge`算法中。它通过其开始/结束迭代器对接受两个输入范围，并接受一个输出。输出将是用户外壳:\n\n```cpp\n          merge(begin(dict1), end(dict1),\n                begin(dict2), end(dict2),\n                ostream_iterator<dict_entry>{cout, \"n\"});\n      }\n```\n\n7.  我们现在可以编译程序，但是在运行它之前，我们应该用一些示例内容创建`dict.txt`文件。让我们用一些英语单词和它们的德语翻译来填充它:\n\n```cpp\n      car       auto\n      cellphone handy\n      house     haus\n```\n\n8.  现在，我们可以启动程序，同时将一些英语-德语翻译输入到它的标准输入中。输出是一个合并的仍然排序的字典，其中包含两个输入的翻译。我们可以用它创建一个新的字典文件:\n\n```cpp\n      $ echo \"table tisch fish fisch dog hund\" | ./dictionary_merge\n      car auto\n      cellphone handy\n      dog hund\n      fish fisch\n      house haus\n      table tisch\n```\n\n# 它是如何工作的...\n\n`std::merge`算法接受两对开始/结束迭代器，它们表示输入范围。这些范围必须是*排序的*。第五个参数是输出迭代器，它在合并过程中接受输入项。\n\n还有一种变体叫做`std::inplace_merge`。这个算法和另一个算法做的一样，但是它不需要输出迭代器，因为它的工作原理是*代替*，顾名思义。它需要三个参数:一个*开头的*迭代器，一个*中间的*迭代器，以及一个*结尾的*迭代器。这些迭代器必须引用同一数据结构中的所有数据。中间迭代器同时是第一个范围的结束迭代器和第二个范围的开始迭代器。这意味着该算法处理单个范围，该范围实际上由两个连续的范围组成，例如`{A, C, B, D}`。第一个子范围是`{A, C}`，第二个子范围是`{B, D}`。`std::inplace_merge`算法然后可以在相同的数据结构内合并两者，从而产生`{A, B, C, D}`。*********"
  },
  {
    "path": "docs/exp-cpp-prog/05.md",
    "content": "# 五、STL 算法的高级使用\n\n我们将在本章介绍以下食谱:\n\n*   使用 STL 算法实现 trie 类\n*   通过尝试实现搜索输入建议生成器\n*   用 STL 数值算法实现傅里叶变换公式\n*   计算两个向量的误差和\n*   实现 ASCII 曼德尔布罗渲染器\n*   构建我们自己的算法-拆分\n*   从标准算法中组合有用的算法-收集\n*   删除单词之间的连续空白\n*   压缩和解压缩字符串\n\n# 介绍\n\n在最后一章中，我们访问了基本的 STL 算法，并使用它们执行了简单的任务，以获得典型 STL 接口的感觉:大多数 STL 算法接受一个或多个以迭代器对形式的范围作为输入/输出参数。它们通常也接受谓词函数、自定义比较函数或转换函数。最后，它们通常会再次返回迭代器，因为这些迭代器通常会在之后被输入到其他算法中。\n\n虽然 STL 算法的目标是最小化，但它们的接口也尽量通用。这实现了最大的代码重用潜力，但看起来并不总是那么漂亮。一个熟悉所有算法的有经验的 C++ 程序员，如果试图用 STL 算法表达尽可能多的思想，那么他阅读别人的代码的时间会更长。这导致编码者和读者之间理解的最大化的共同点。程序员的大脑能够比理解一个复杂的循环更快地解析一个著名算法的名字，这个复杂的循环主要完成类似的工作，但在某些细节上略有不同。\n\n在这一点上，我们如此直观地使用 STL 数据结构，以至于我们可以很好地避免指针、原始数组和其他粗糙的遗留结构。下一步是提升我们对 STL 算法的理解，通过用众所周知的 STL 算法表达它们，我们可以避免使用手工制造的循环控制结构复合体。通常，这是一个真正的改进，因为代码变得更短、更可读，同时更通用、数据结构不可知。实际上总是有可能避免编写手工循环，而是将算法从`std`命名空间中取出，但有时，它确实会导致*笨拙的代码*。我们不会区分什么是尴尬的，什么不是；我们只探索可能性。\n\n在这一章中，我们将创造性地使用 STL 算法，以便寻找新的视野，并看看如何用现代 C++ 实现事物。在路上，我们将实现自己的类似 STL 的算法，这些算法可以很容易地与现有的数据结构和以同样方式设计的其他算法相结合。我们还将*结合*现有的 STL 算法，得到*新的*算法，这是以前没有的。这种组合算法允许在现有算法的基础上进行更复杂的算法，同时它们本身也非常短，并且以这种方式可读。在这个小小的旅程中，我们还将看到 STL 算法在哪些地方会受到可重用性或美观性的影响。只有当我们很好地了解所有的方法时，我们才能最好地决定哪种方法是正确的。\n\n# 使用 STL 算法实现 trie 类\n\n所谓的 **trie** 数据结构提供了一种以易于搜索的方式存储数据的有趣方式。当将文本的句子分割成单词列表时，通常可以将一些句子共有的前几个单词组合在一起。\n\n我们来看看下图，句子`\"hi how are you\"`和`\"hi how do you do\"`保存在一个树状的数据结构中。他们的第一个共同点是`\"hi how\"`，然后他们就像一棵树一样分裂开来:\n\n![](img/b42b8317-e512-4bac-bc05-cb4ce62421b7.png)\n\n因为 trie 数据结构结合了常见的前缀，所以也被称为*前缀树*。用 STL 已经给我们的东西实现这样的数据结构是非常容易的。这一部分集中于实现我们自己的 trie 类。\n\n# 怎么做...\n\n在这一节中，我们将实现我们自己的前缀树，仅由 STL 数据结构和算法组成。\n\n1.  我们将包括我们使用的 STL 部分的所有头，并声明我们默认使用`std`命名空间:\n\n```cpp\n      #include <iostream>\n      #include <optional>\n      #include <algorithm>\n      #include <functional>\n      #include <iterator>\n      #include <map>\n      #include <vector>\n      #include <string>\n\n      using namespace std;\n```\n\n2.  整个程序围绕着一个 trie，为此我们必须首先实现一个类。在我们的实现中，trie 基本上是一个递归地图。每个 trie 节点包含一个映射，该映射从有效载荷类型的实例`T`映射到下一个 trie 节点:\n\n```cpp\n      template <typename T>\n      class trie\n      {\n          map<T, trie> tries;\n```\n\n3.  插入新项目序列的代码很简单。用户提供一个开始/结束迭代器对，我们递归地遍历它。如果用户输入序列是`{1, 2, 3}`，那么我们在子树中查找`1`，然后在下一个子树中查找`2`，以便得到`3`的子树。如果之前不存在这些子集合中的任何一个，它们将由`std::map`的`[]`操作符隐式添加:\n\n```cpp\n      public:\n          template <typename It>\n          void insert(It it, It end_it) {\n              if (it == end_it) { return; }\n              tries[*it].insert(next(it), end_it);\n          }\n```\n\n4.  我们还定义了便利函数，使用户只需提供一个项目容器，然后自动查询迭代器:\n\n```cpp\n          template <typename C>\n          void insert(const C &container) {\n              insert(begin(container), end(container));\n          }\n```\n\n5.  为了允许用户编写`my_trie.insert({\"a\", \"b\", \"c\"});`，我们必须帮助编译器从那一行正确地推导出所有类型，所以我们添加了一个函数，它用一个`initializer_list`参数重载了插入接口:\n\n```cpp\n          void insert(const initializer_list<T> &il) {\n              insert(begin(il), end(il));\n          }\n```\n\n6.  我们还想看看 trie 中有什么，所以我们需要一个`print`函数。为了打印，我们可以在 trie 中进行深度优先搜索。在从根节点向下到第一个叶节点的过程中，我们记录了已经看到的所有有效负载项。这样，一旦我们到达叶子，我们就有了一个完整的序列，这是很容易打印的。当`tries.empty()`是`true`时，我们看到了一片叶子。在递归`print`调用之后，我们再次弹出最后添加的有效载荷项:\n\n```cpp\n          void print(vector<T> &v) const {\n              if (tries.empty()) {\n                  copy(begin(v), end(v), \n                       ostream_iterator<T>{cout, \" \"});\n                  cout << 'n';\n              }\n              for (const auto &p : tries) {\n                  v.push_back(p.first);\n                  p.second.print(v);\n                  v.pop_back();\n              }\n          }\n```\n\n7.  递归`print`函数传递一个对有效载荷项目的可打印列表的引用，但是用户应该在没有任何参数的情况下调用它。因此，我们定义了一个无参数`print`函数，它构造了帮助列表对象:\n\n```cpp\n          void print() const {\n              vector<T> v;\n              print(v);\n          }\n```\n\n8.  现在我们可以构建和打印尝试，我们可能想要搜索子树。其思想是，如果 trie 包含诸如`{a, b, c}`和`{a, b, d, e}`的序列，并且我们给它一个序列`{a, b}`，用于搜索，它将返回给我们包含`{c}`和`{d, e}`部分的子树。如果找到子树，我们返回一个`const`引用。如果 trie 不包含我们正在搜索的序列，则可能不存在这样的子树。在这种情况下，我们仍然需要返回*一些东西*。`std::optional`是一个很好的助手，因为如果没有匹配项，我们可以返回一个*空的*可选对象:\n\n```cpp\n          template <typename It>\n          optional<reference_wrapper<const trie>> \n          subtrie(It it, It end_it) const {\n              if (it == end_it) { return ref(*this); }\n              auto found (tries.find(*it));\n              if (found == end(tries)) { return {}; }\n\n              return found->second.subtrie(next(it), end_it);\n          }\n```\n\n9.  类似于`insert`方法，我们提供了一个单参数版本的`subtrie`方法，它自动从输入容器中获取迭代器:\n\n```cpp\n          template <typename C>\n          auto subtrie(const C &c) { \n              return subtrie(begin(c), end(c));\n          }\n      };\n```\n\n10.  已经这样了。让我们通过实例化一个专门用于`std::string`对象的 trie 来将新的 trie 类用于我们的主函数，并用一些示例内容填充它:\n\n```cpp\n      int main()\n      {\n          trie<string> t;\n\n          t.insert({\"hi\", \"how\", \"are\", \"you\"});\n          t.insert({\"hi\", \"i\", \"am\", \"great\", \"thanks\"});\n          t.insert({\"what\", \"are\", \"you\", \"doing\"});\n          t.insert({\"i\", \"am\", \"watching\", \"a\", \"movie\"});\n```\n\n11.  让我们先打印整个 trie:\n\n```cpp\n          cout << \"recorded sentences:n\";\n          t.print();\n```\n\n12.  然后我们获取所有以`\"hi\"`开头的输入句子的子树，并打印出来:\n\n```cpp\n          cout << \"npossible suggestions after \"hi\":n\";\n\n          if (auto st (t.subtrie(initializer_list<string>{\"hi\"})); \n              st) {\n              st->get().print();\n          }\n      }\n```\n\n13.  编译并运行该程序表明，当我们查询 trie 中的子树时，它确实只返回以`\"hi\"`开头的两个句子:\n\n```cpp\n      $ ./trie \n      recorded sentences:\n      hi how are you \n      hi i am great thanks \n      i am watching a movie \n      what are you doing \n\n      possible suggestions after \"hi\":\n      how are you \n      i am great thanks \n```\n\n# 它是如何工作的...\n\n有趣的是，单词序列*插入*的代码比*在子树中查找*给定单词序列的代码更短更简单。那么，让我们先来看看插入代码:\n\n```cpp\ntemplate <typename It>\nvoid trie::insert(It it, It end_it) {\n    if (it == end_it) { return; }\n    tries[*it].insert(next(it), end_it);\n}\n```\n\n这对迭代器`it`和`end_it`代表要插入的单词序列。`tries[*it]`元素在子树中查找序列中的第一个单词，然后，`.insert(next(it), end_it)`在该较低的子树上重新启动相同的功能，迭代器一个单词*进一步*前进。`if (it == end_it) { return; }`行只是中止递归。空`return`语句什么都不做*，刚开始有点诡异。所有的插入都发生在`tries[*it]`语句中。`std::map`的括号操作符`[]`或者返回给定键的现有项目，或者它*用该键创建一个*。关联的值(映射的类型是这个配方中的一个 trie)是从它的默认构造函数构造的。这样，每当我们查找未知单词时，我们都在*隐式创建*一个新的 trie 分支。*\n\n *在子树中查找看起来更复杂，因为我们无法在隐式代码中隐藏太多的*:*\n\n```cpp\ntemplate <typename It>\noptional<reference_wrapper<const trie>> \nsubtrie(It it, It end_it) const {\n    if (it == end_it) { return ref(*this); }\n    auto found (tries.find(*it));\n    if (found == end(tries)) { return {}; }\n\n    return found->second.subtrie(next(it), end_it);\n}\n```\n\n这段代码基本围绕`auto found (tries.find(*it));`语句展开。我们使用`find`，而不是使用括号运算符(`[]`)来查找下一个更深的 trie 节点。如果我们使用`[]`运算符进行查找，trie 会为我们创建*缺失项，这是*而不是我们在仅仅查找某项是否存在时想要的*！(对了，试着那样做。上课的方法是`const`，所以这是不可能的。这可能是一个相当大的救命稻草，有助于我们预防错误。)*\n\n另一个看起来可怕的细节是返回类型，`optional<reference_wrapper<const trie>>`。我们选择`std::optional`作为包装器，因为我们正在寻找的输入序列可能没有这样的子树。如果我们只插入`\"hello my friend\"`，就不会有`\"goodbye my friend\"`序列可查。在这种情况下，我们只返回`{}`，它给调用者一个空的可选对象。这仍然不能解释为什么我们用`reference_wrapper`而不是只写`optional<const trie &>`。这里的要点是，带有`trie&`类型成员变量的可选实例是不可重新分配的，因此不会编译。使用`reference_wrapper`实现引用会产生可重新分配的对象。\n\n# 通过尝试实现搜索输入建议生成器\n\n当在互联网上的搜索引擎中输入一些东西时，界面经常试图猜测完整的搜索查询会是什么样子。这种猜测通常基于过去流行的搜索查询。有时候，这样的搜索引擎猜测相当有趣，因为人们似乎会在搜索引擎中输入奇怪的查询。\n\n![](img/ca1d8fff-6f82-468a-8a56-2b41b86e20f9.png)\n\n在本节中，我们将使用我们在前面的食谱中实现的 trie 类，并构建一个小的搜索查询建议引擎。\n\n# 怎么做...\n\n在本节中，我们将实现一个终端应用，它接受一些输入，然后根据廉价的文本文件数据库，尝试猜测用户可能想要寻找什么:\n\n1.  一如既往，包含优先，我们定义使用`std`命名空间:\n\n```cpp\n      #include <iostream>\n      #include <optional>\n      #include <algorithm>\n      #include <functional>\n      #include <iterator>\n      #include <map>\n      #include <list>\n      #include <string>\n      #include <sstream>\n      #include <fstream>  \n\n      using namespace std;\n```\n\n2.  我们使用 trie 配方中的 trie 实现:\n\n```cpp\n      template <typename T>\n      class trie\n      {\n          map<T, trie> tries;\n\n      public:\n          template <typename It>\n          void insert(It it, It end_it) {\n              if (it == end_it) { return; }\n              tries[*it].insert(next(it), end_it);\n          }\n\n          template <typename C>\n          void insert(const C &container) {\n              insert(begin(container), end(container));\n          }\n\n          void insert(const initializer_list<T> &il) {\n              insert(begin(il), end(il));\n          }\n\n          void print(list<T> &l) const {\n              if (tries.empty()) {\n                  copy(begin(l), end(l), \n                       ostream_iterator<T>{cout, \" \"});\n                  cout << 'n';\n              }\n              for (const auto &p : tries) {\n                  l.push_back(p.first);\n                  p.second.print(l);\n                  l.pop_back();\n              }\n          }\n\n          void print() const {\n              list<T> l;\n              print(l);\n          }\n\n          template <typename It>\n          optional<reference_wrapper<const trie>>\n          subtrie(It it, It end_it) const {\n              if (it == end_it) { return ref(*this); }\n              auto found (tries.find(*it));\n              if (found == end(tries)) { return {}; }\n\n      return found->second.subtrie(next(it), end_it);\n          }\n\n          template <typename C>\n          auto subtrie(const C &c) const { \n              return subtrie(begin(c), end(c));\n          }\n      };\n```\n\n3.  让我们添加一个小助手函数，打印一行提示用户输入一些文本:\n\n```cpp\n      static void prompt()\n      {\n          cout << \"Next input please:n > \";\n      }\n```\n\n4.  在主功能中，我们打开一个文本文件，它充当我们的句子数据库。我们逐行读取文本文件，并将这些行输入到一个 trie 中:\n\n```cpp\n      int main()\n      {\n          trie<string> t;\n\n          fstream infile {\"db.txt\"};\n          for (string line; getline(infile, line);) {\n              istringstream iss {line};\n              t.insert(istream_iterator<string>{iss}, {});\n          }\n```\n\n5.  现在我们已经从文本文件中的内容构建了 trie，我们需要实现一个用户查询它的接口。我们提示用户输入一些文本并等待一整行输入:\n\n```cpp\n          prompt();\n          for (string line; getline(cin, line);) {\n              istringstream iss {line};\n```\n\n6.  有了这个文本输入，我们查询 trie 以便从中获得一个子树。如果我们在文本文件中已经有了这样的输入序列，那么我们就可以打印出如何继续输入，就像在搜索引擎建议功能中一样。如果我们没有找到匹配的子树，我们只告诉用户:\n\n```cpp\n              if (auto st (t.subtrie(istream_iterator<string>{iss}, {})); \n                  st) {\n                  cout << \"Suggestions:n\";\n                  st->get().print();\n              } else {\n                  cout << \"No suggestions found.n\";\n              }\n```\n\n7.  之后，我们再次打印提示文本，等待下一行用户输入。就这样。\n\n```cpp\n              cout << \"----------------n\";\n              prompt();\n          }\n      }\n```\n\n8.  在考虑启动程序之前，我们需要在`db.txt`中填入一些内容。输入可以是任何东西，甚至不需要排序。每一行文本都是一个 trie 序列:\n\n```cpp\n      do ghosts exist\n      do goldfish sleep\n      do guinea pigs bite\n      how wrong can you be\n      how could trump become president\n      how could this happen to me\n      how did bruce lee die\n      how did you learn c++\n      what would aliens look like\n      what would macgiver do\n      what would bjarne stroustrup do\n      ...\n```\n\n9.  我们需要创建`db.txt`才能运行程序。其内容可能如下所示:\n\n```cpp\n      hi how are you \n      hi i am great thanks \n      do ghosts exist\n      do goldfish sleep\n      do guinea pigs bite\n      how wrong can you be\n      how could trump become president\n      how could this happen to me\n      how did bruce lee die\n      how did you learn c++\n      what would aliens look like\n      what would macgiver do\n      what would bjarne stroustrup do\n      what would chuck norris do\n      why do cats like boxes\n      why does it rain\n      why is the sky blue\n      why do cats hate water\n      why do cats hate dogs\n      why is c++ so hard\n```\n\n10.  编译和运行程序并输入一些输入，如下所示:\n\n```cpp\n      $ ./word_suggestion \n      Next input please:\n       > what would\n      Suggestions:\n      aliens look like \n      bjarne stroustrup do \n      chuck norris do \n      macgiver do \n      ----------------\n      Next input please:\n       > why do\n      Suggestions:\n      cats hate dogs \n      cats hate water \n      cats like boxes \n      ----------------\n      Next input please:\n       > \n```\n\n# 它是如何工作的...\n\n上一份食谱解释了 trie 的工作原理，但是我们如何填充它以及如何查询它在这里看起来有点奇怪。让我们仔细看看用文本数据库文件的内容填充空 trie 的代码片段:\n\n```cpp\nfstream infile {\"db.txt\"};\nfor (string line; getline(infile, line);) {\n    istringstream iss {line};\n    t.insert(istream_iterator<string>{iss}, {});\n}\n```\n\n循环用文本文件的内容逐行填充字符串`line`。然后，我们将字符串复制到一个`istringstream`对象中。从这样一个输入流对象，我们可以创建一个`istream_iterator`，这很有用，因为我们的 trie 不仅接受一个容器实例来查找子树，而且主要接受迭代器。这样，我们不需要构造一个向量或一个单词列表，并且可以直接使用字符串。最后一部分不必要的内存分配可以通过*将`line`的内容移动到`iss`来避免。遗憾的是，`std::istringstream`没有提供接受`std::string`值进行*移动*的构造函数。然而，它将*复制*它的输入字符串。*\n\n当读取用户输入以在 trie 中查找它时，我们使用完全相同的策略，但是我们不使用输入*文件*流。我们用`std::cin`代替。这对我们的用例来说完全一样，因为`trie::subtrie`和`trie::insert`一样使用迭代器。\n\n# 还有更多...\n\n可以将*计数器变量*添加到 trie 的每个节点。这样，就可以计算*前缀在某些输入中出现的频率。由此，我们可以*根据出现频率对我们的建议进行排序，这实际上是搜索引擎的工作。智能手机触摸屏文本输入的单词建议也可以通过这种方式实现。**\n\n这个修改留给读者做练习。\n\n# 用 STL 数值算法实现傅里叶变换公式\n\n**傅里叶变换**是信号处理中非常重要和著名的公式。它是在将近 200 年前发明的，但是有了计算机，它的用例数量真的激增了。它被用于音频/图像/视频压缩、音频过滤器、医学成像设备、在移动中听音乐时识别音乐曲目的手机应用等。\n\n由于一般数值应用场景的广阔性(当然不仅仅是因为傅立叶变换)，STL 也试图在数值计算的环境中有用。傅立叶变换只是其中的一个例子，但也是一个棘手的例子。公式本身如下所示:\n\n![](img/ecd82744-2cc6-4b76-9f93-5f03df78a598.jpg)\n\n它描述的转变基本上是一个*的总和*。总和的每个元素是输入信号向量的数据点和表达式 *exp(-2 * i *的乘积...)*。这背后的数学对于不懂复数的人(或者只是不喜欢数学的人)来说有点吓人，但也不是真的需要完全理解数学才能*实现*它。当仔细查看公式时，它表示求和符号使用循环变量`j`在信号的每个数据点(长度为`N`个元素)上循环。变量`k`是另一个循环变量，因为傅立叶变换不是用来计算单个值，而是一个向量值。在这个向量中，每个数据点代表某个重复波频率的强度和相位，它是或不是原始信号的一部分。当用手动循环实现这一点时，我们将得到类似如下的代码:\n\n```cpp\ncsignal fourier_transform(const csignal &s) { \n    csignal t(s.size()); \n    const double pol {-2.0 * M_PI / s.size()};\n\n    for (size_t k {0}; k < s.size(); ++ k) { \n        for (size_t j {0}; j < s.size(); ++ j) { \n            t[k] += s[j] * polar(1.0, pol * k * j); \n        }\n    } \n    return t; \n}\n```\n\n`csignal`类型可以是复数的`std::vector`向量。对于复数，有一个`std::complex` STL 类，帮助表示那些。`std::polar`功能基本上执行 *exp(-i * 2 *...)*部分。\n\n这已经很好了，但是我们将使用 STL 工具来实现它。\n\n# 怎么做...\n\n在本节中，我们将实现傅立叶变换及其后向变换，然后用它来变换一些信号:\n\n1.  首先，我们包括所有的头，并声明我们使用`std`命名空间:\n\n```cpp\n      #include <iostream>\n      #include <complex>\n      #include <vector>\n      #include <algorithm>\n      #include <iterator>\n      #include <numeric>\n      #include <valarray>\n      #include <cmath>      \n\n      using namespace std;\n```\n\n2.  信号的数据点是一个复数，应由`std::complex`表示，专用于`double`类型。这样，类型别名`cmplx`代表两个耦合的`double`值，代表复数的*实数*和*虚数*部分。一个完整的信号是这些项的向量，我们将其别名为`csignal`类型:\n\n```cpp\n      using cmplx   = complex<double>;\n      using csignal = vector<cmplx>;\n```\n\n3.  为了迭代一个向上计数的数字序列，我们从数字迭代器配方中取出*数字迭代器*。公式中的变量`k`和`j`应迭代这些序列:\n\n```cpp\n      class num_iterator {\n          size_t i;\n      public:\n          explicit num_iterator(size_t position) : i{position} {}\n\n          size_t operator*() const { return i; }\n\n          num_iterator& operator++() {\n              ++ i;\n              return *this;\n          }\n\n          bool operator!=(const num_iterator &other) const {\n              return i != other.i;\n          }\n      };\n```\n\n4.  傅立叶变换函数将只取一个信号并返回一个新信号。返回的信号代表输入信号的傅立叶变换。由于从傅里叶变换信号到原始信号的反向变换非常相似，我们提供了一个可选的`bool`参数，该参数选择变换方向。注意`bool`参数通常是不好的做法，尤其是如果我们在一个函数签名中使用多个`bool`参数。为了简洁起见，我们这里只有一个。\n    我们做的第一件事是用初始信号的大小分配一个新的信号向量:\n\n```cpp\n      csignal fourier_transform(const csignal &s, bool back = false)\n      {\n          csignal t (s.size());\n```\n\n5.  公式中有两个因素，看起来总是一样的。让我们用它们自己的变量来包装它们:\n\n```cpp\n          const double pol {2.0 * M_PI * (back ? -1.0 : 1.0)};\n          const double div {back ? 1.0 : double(s.size())};\n```\n\n6.  `std::accumulate`算法是执行总结项目的公式的合适选择。我们将在一系列向上计数的数值上使用`accumulate`。从这些值，我们可以形成每一步的总和。`std::accumulate`算法在每一步调用一个二元函数。该函数的第一个参数是在前面步骤中已经计算过的`sum`部分的当前值，其第二个参数是范围中的下一个值。我们在当前位置查找信号`s`的值，并将其乘以复因子`pol`。然后，我们返回新的部分和。二进制函数被包装到另一个 lambda 表达式中，因为我们将为每个 T7 调用使用不同的 T6 值。因为这是一个二维循环算法，所以内 lambda 代表内环，外 lambda 代表外环:\n\n```cpp\n          auto sum_up ([=, &s] (size_t j) {\n              return [=, &s] (cmplx c, size_t k) {\n                  return c + s[k] * \n                      polar(1.0, pol * k * j / double(s.size()));\n              };\n          });\n```\n\n7.  傅立叶变换的内环部分现在由`std::accumulate`执行。对于算法的每个`j`位置，我们计算位置 *i = 0 的所有和的和...N* 。这个想法被包装成一个 lambda 表达式，我们将对结果傅立叶变换向量中的每个数据点执行该表达式:\n\n```cpp\n          auto to_ft ([=, &s](size_t j){\n              return accumulate(num_iterator{0}, \n                                num_iterator{s.size()}, \n                                cmplx{},\n                                sum_up(j))\n                  / div;\n          });\n```\n\n8.  在此之前，没有执行任何傅立叶代码。我们只准备了大量的功能代码，现在就付诸行动。一个`std::transform`调用将产生值 *j = 0...N* ，这是我们的外环。转换后的值都进入向量`t`，然后返回给调用者:\n\n```cpp\n          transform(num_iterator{0}, num_iterator{s.size()}, \n                    begin(t), to_ft);\n\n          return t;\n      }\n```\n\n9.  我们将实现一些功能，帮助我们为信号生成设置功能对象。第一个是余弦信号发生器。它返回一个 lambda 表达式，该表达式可以用作为参数提供的周期长度生成余弦信号。信号本身可以是任意长度的，但它有固定的周期长度。`N`的周期长度意味着信号将在`N`步后重复自身。lambda 表达式不接受任何参数。我们可以反复调用它，每次调用，它都会返回下一个时间点的信号数据点:\n\n```cpp\n      static auto gen_cosine (size_t period_len){\n          return [period_len, n{0}] () mutable { \n              return cos(double(n++) * 2.0 * M_PI / period_len); \n          };\n      }\n```\n\n10.  我们要产生的另一个信号是方波。它在值`-1`和`+1`之间振荡，除此之外没有其他值。公式看起来很复杂，但它只是将线性向上计数值`n`转换为`+1`和`-1`，振荡周期长度为`period_len`。\n    注意，我们这次将`n`初始化为与`0`不同的值。这样，我们的方波开始于其输出值开始于`+1`的阶段:\n\n```cpp\n      static auto gen_square_wave (size_t period_len)\n      {\n          return [period_len, n{period_len*7/4}] () mutable {\n              return ((n++ * 2 / period_len) % 2) * 2 - 1.0;\n          };\n      }\n```\n\n11.  通过分配一个新的向量并用重复信号发生器函数调用产生的值填充它，可以从这样的发生器产生一个实际的信号。`std::generate`做这项工作。它接受一个开始/结束迭代器对和一个生成器函数。对于每个有效的迭代器位置，它都执行`*it = gen()`。通过将此代码打包成一个函数，我们可以轻松生成信号矢量:\n\n```cpp\n      template <typename F>\n      static csignal signal_from_generator(size_t len, F gen)\n      {\n          csignal r (len);\n          generate(begin(r), end(r), gen);\n          return r;\n      }\n```\n\n12.  最后，我们需要打印结果信号。我们可以简单地通过将其值复制到输出流迭代器中来打印信号，但是我们需要首先转换数据，因为我们信号的数据点是复杂的值对。此时，我们只对每个数据点的真实价值部分感兴趣；因此，我们通过一个`std::transform`调用抛出它，这个调用只提取这一部分:\n\n```cpp\n      static void print_signal (const csignal &s)\n      {\n          auto real_val ([](cmplx c) { return c.real(); });\n          transform(begin(s), end(s), \n                    ostream_iterator<double>{cout, \" \"}, real_val);\n          cout << 'n';\n      }\n```\n\n13.  傅立叶公式现在已经实现了，但是我们还没有要转换的信号。这就是我们在主要功能中所做的。让我们首先定义一个所有信号都符合的标准信号长度。\n\n```cpp\n      int main()\n      {\n          const size_t sig_len {100};\n```\n\n14.  现在让我们生成信号，转换它们，并打印它们，这发生在接下来的三个步骤中。第一步是产生余弦信号和方波信号。两者具有相同的总信号长度和周期长度:\n\n```cpp\n          auto cosine      (signal_from_generator(sig_len, \n                 gen_cosine(     sig_len / 2)));\n          auto square_wave (signal_from_generator(sig_len,\n                 gen_square_wave(sig_len / 2)));\n```\n\n15.  我们现在有一个余弦函数和一个方波信号。为了在它们中间产生第三个，我们取方波信号并计算其傅里叶变换(保存在`trans_sqw`向量中)。方波的傅里叶变换有一种特殊的形式，我们将对它进行一点处理。从索引`10`到`(signal_length - 10)`的所有项目都设置为`0.0`。其余部分保持不变*。将这种改变的傅立叶变换转换回信号时间表示将会给我们一个不同的信号。我们将看到最终的结果:*\n\n```cpp\n          auto trans_sqw (fourier_transform(square_wave));\n\n          fill (next(begin(trans_sqw), 10), prev(end(trans_sqw), 10), 0);\n          auto mid (fourier_transform(trans_sqw, true));\n```\n\n16.  现在我们有三个信号:`cosine`、`mid`和`square_wave`。对于每个信号，我们打印信号本身及其傅里叶变换。整个程序的输出将由六行非常长的双值列表组成:\n\n```cpp\n          print_signal(cosine);\n          print_signal(fourier_transform(cosine));\n\n          print_signal(mid);\n          print_signal(trans_sqw);\n\n          print_signal(square_wave);\n          print_signal(fourier_transform(square_wave));\n      }\n```\n\n17.  编译和运行程序会导致终端充满大量的数值。如果我们绘制输出，我们会得到如下图像:\n\n![](img/e2b5e05b-3c87-44b4-aebd-ada36d752a21.png)\n\n# 它是如何工作的...\n\n这个程序包含两个复杂的部分。一个是傅立叶变换本身，另一个是可变 lambda 表达式信号的生成。\n\n让我们先集中讨论傅立叶变换。raw 循环实现的核心(我们没有在实现中使用它，但在介绍中看到了它)如下所示:\n\n```cpp\nfor (size_t k {0}; k < s.size(); ++ k) {\n    for (size_t j {0}; j < s.size(); ++ j) {\n        t[k] += s[j] * polar(1.0, pol * k * j / double(s.size()));\n    }\n}\n```\n\n使用 STL 算法，`std::transform`和`std::accumulate`，我们编写了代码，可以总结为以下伪代码:\n\n```cpp\ntransform(num_iterator{0}, num_iterator{s.size()}, ...\n    accumulate((num_iterator0}, num_iterator{s.size()}, ...\n        c + s[k] * polar(1.0, pol * k * j / double(s.size()));\n```\n\n与循环变量相比，结果完全相同。这可以说是一个例子，严格使用 STL 算法不会导致更好的代码。然而，这种算法实现对于数据结构的选择是不可知的。它也适用于列表(尽管在我们的情况下这没有太大意义)。另一个好处是 C++ 17 STL 算法很容易*并行化*(我们将在本书的另一章中研究)，而原始循环必须被重组以支持多处理(除非我们使用像 *OpenMP* 这样的外部库，但是这些确实为我们重组了循环)。\n\n另一个复杂的部分是信号产生。我们再来看看`gen_cosine`:\n\n```cpp\nstatic auto gen_cosine (size_t period_len)\n{\n    return [period_len, n{0}] () mutable {\n        return cos(double(n++) * 2.0 * M_PI / period_len);\n    };\n}\n```\n\nlambda 表达式的每个实例代表一个函数对象，该对象在每次调用时都会修改自己的状态。其状态由变量`period_len`和`n`组成。`n`变量是每次调用都会修改的变量。信号在每个时间点都有不同的值，`n++ `代表增加的时间点。为了从中获得实际的信号向量，我们创建了助手`signal_from_generator`:\n\n```cpp\ntemplate <typename F>\nstatic auto signal_from_generator(size_t len, F gen)\n{\n    csignal r (len);\n    generate(begin(r), end(r), gen);\n    return r;\n}\n```\n\n这个助手分配一个具有选择长度的信号向量，并调用`std::generate`用数据点填充它。对于向量`r`的每一项，它都调用函数对象`gen`一次，这正是我们可以用`gen_cosine`创建的那种自修改函数对象。\n\nUnfortunately, the STL way does *not* make this code more elegant. As soon as the ranges library joins the STL club (which is hopefully the case with C++ 20), this will most probably change.\n\n# 计算两个向量的误差和\n\n计算目标值和实际值之间的数值*误差*有不同的可能性。测量由许多数据点组成的信号之间的差异通常包括循环和减去相应的数据点，等等。\n\n计算信号`a`和信号`b`之间误差的一个简单公式如下:\n\n![](img/a1db597b-9f14-48c2-bd5f-0a24b3019643.jpg)\n\n对于每一个 *i* ，它计算 *a[i] - b[i]* ，对该差值进行平方(这样，负差值和正差值变得可比较)，最后将这些值相加。这又是一种可以使用循环的情况，但是出于有趣的原因，我们将使用 STL 算法来实现。好消息是，我们通过这种方式免费获得了数据结构独立性。我们的算法将适用于向量和类似列表的数据结构，在这些数据结构中，直接索引是不可能的。\n\n# 怎么做...\n\n在本节中，我们将创建两个信号，并计算它们的误差和:\n\n1.  一如既往，include 语句是第一位的。然后，我们声明使用`std`命名空间:\n\n```cpp\n      #include <iostream>\n      #include <cmath>\n      #include <algorithm>\n      #include <numeric>\n      #include <vector>\n      #include <iterator>      \n\n      using namespace std;\n```\n\n2.  我们将计算两个信号的误差和。这两个信号将是正弦波和它的副本，但值类型不同-原始正弦波保存在`double`变量的向量中，它的副本保存在`int`变量的向量中。因为将一个值从一个`double`变量复制到一个`int`变量会在小数点后减少它的小数部分，所以我们有一些*损失*。我们来命名一下`double`值的向量`as`，代表*模拟信号*，`int`值的向量`ds`，代表*数字信号*。然后，误差总和将告诉我们实际损失有多大:\n\n```cpp\n      int main()\n      {\n          const size_t sig_len {100};\n          vector<double> as (sig_len); // a for analog\n          vector<int>    ds (sig_len); // d for digital\n```\n\n3.  为了产生正弦波信号，我们用一个*可变的*计数器值`n`实现一个小的 lambda 表达式。我们可以随时调用它，每次调用，它都会返回正弦波下一个时间点的值。`std::generate`调用用生成的信号填充信号向量，`std::copy`调用随后将所有值从`double`变量的向量复制到`int`变量的向量:\n\n```cpp\n          auto sin_gen ([n{0}] () mutable { \n              return 5.0 * sin(n++ * 2.0 * M_PI / 100); \n          });\n\n          generate(begin(as), end(as), sin_gen);\n          copy(begin(as), end(as), begin(ds));\n```\n\n4.  让我们首先打印信号，因为这样，它们可以稍后绘制:\n\n```cpp\n          copy(begin(as), end(as), \n               ostream_iterator<double>{cout, \" \"});\n          cout << 'n';\n          copy(begin(ds), end(ds), \n               ostream_iterator<double>{cout, \" \"});\n          cout << 'n';\n```\n\n5.  现在，对于实际误差和，我们使用`std::inner_product`，因为它可以很容易地适用于计算信号向量的每两个对应元素之间的差异。它将遍历两个范围，在范围中相同的对应位置选取项目，计算它们之间的差异，将其平方，并累积结果:\n\n```cpp\n          cout << inner_product(begin(as), end(as), begin(ds), \n                                0.0, std::plus<double>{},\n                                [](double a, double b) { \n                                    return pow(a - b, 2); \n                                }) \n               << 'n';\n      }\n```\n\n6.  编译和运行程序给了我们两行长的信号输出和第三行，第三行包含单个输出值，这是两个信号之间的误差。错误为`40.889`。如果我们以连续的方式计算误差，首先针对第一对项目，然后针对前两对项目，然后针对前三对项目，以此类推，然后我们得到累积误差曲线，如图所示，该曲线在绘制的图表上可见:\n\n![](img/33d6fd3e-c17d-4f36-a4fc-69a3a83d4804.png)\n\n# 它是如何工作的...\n\n在这个食谱中，我们填充了循环两个向量的任务，得到它们对应值之间的差值，对它们求平方，最后将它们相加成一个`std::inner_product`调用。在路上，我们自己编写的唯一代码是 lambda 表达式`[](double a, double b) { return pow(a - b, 2); }`，它取其参数之差并将其平方。\n\n浏览一下`std::inner_product`的一个可能实现，就可以知道这是为什么以及如何工作的:\n\n```cpp\ntemplate<class InIt1, class InIt2, class T, class F, class G>\nT inner_product(InIt1 it1, InIt1 end1, InIt2 it2, T val,\n                F bin_op1, G bin_op2)\n{\n    while (it1 != end1) {\n        val = bin_op1(val, bin_op2(*it1, *it2));\n        ++ it1;\n        ++ it2;\n    }\n    return value;\n}\n```\n\n该算法接受第一个范围的一对开始/结束迭代器，以及第二个范围的另一个开始迭代器。在我们的例子中，它们是我们想要从中计算误差和的向量。下一个字符是初始值`val`。我们已经将其初始化为`0.0`。然后，算法接受两个二元函数，即`bin_op1`和`bin_op2`。\n\n在这一点上，我们可能会意识到这个算法真的很类似于`std::accumulate`。唯一不同的是`std::accumulate`只在*一个*范围内工作。如果我们用`*it`交换`bin_op2(*it1, *it2)`语句，那么我们就基本上恢复了`accumulate`算法。因此，我们可以将`std::inner_product`视为`std::accumulate`的一个版本，即*将*拉成一对输入范围。\n\n在我们的例子中，*拉链*功能是`pow(a - b, 2)`，就这样。对于另一个函数`bin_op1`，我们选择了`std::plus<double>`，因为我们希望所有的方块加在一起。\n\n# 实现 ASCII 曼德尔布罗渲染器\n\n1975 年，数学家贝诺特·曼德尔布罗创造了术语**分形**。分形是一个数学图形或集合，有一定的有趣的数学性质，但最终看起来只是一件艺术品，分形放大后也看起来*无限* *重复*。最受欢迎的分形之一是*曼德勃罗集*，可以在下面的海报上看到:\n\n![](img/f18ecb5f-e3af-4184-b68c-1c99bd5cd4c5.jpg)\n\n曼德尔布罗集合的图片可以通过迭代一个特定的公式来生成:\n\n![](img/628bf694-e1cc-48c2-a1dd-d110a7d8fa6d.jpg)\n\n变量 *z* 和 *c* 是*复数*数。曼德尔布罗集合由所有这样的 *c* 值组成，如果公式*应用得足够频繁，则公式*会收敛到这些值。这是海报的彩色部分。有些值收敛得比较早，有些值收敛得比较晚，所以可以用不同的颜色可视化。有些根本不融合——这些被涂成黑色。\n\nSTL 附带了有用的`std::complex`类，我们将尝试在没有显式循环的情况下实现公式，只是为了更好地了解 STL。\n\n# 怎么做...\n\n在这一节中，我们将打印墙上海报中的相同图像，作为终端中的一小段 ASCII 艺术:\n\n1.  首先，我们包括所有的头，并声明我们使用`std`命名空间:\n\n```cpp\n      #include <iostream>\n      #include <algorithm>\n      #include <iterator>\n      #include <complex>\n      #include <numeric>\n      #include <vector>      \n\n      using namespace std;\n```\n\n2.  曼德尔布罗集和公式对复数运算。因此，我们定义一个类型别名`cmplx`为类`std::complex`，专门处理双值。\n\n```cpp\n      using cmplx = complex<double>;\n```\n\n3.  可以用大约 20 行代码将 ASCII 曼德勃罗图像的所有代码组合在一起，但是我们将以单独的形式实现每个逻辑步骤，然后最终组装所有步骤。第一步是实现一个从整数坐标缩放到浮点坐标的函数。我们在开始时拥有的是终端上的字符位置的列和行。我们想要的是曼德尔布罗集坐标系中的复杂类型坐标。为此，我们实现了一个函数，该函数接受描述用户终端坐标系几何形状的参数，以及我们要转换到的系统。这些值用于构建 lambda 表达式，然后返回。lambda 表达式接受一个`int`坐标并返回一个`double`坐标:\n\n```cpp\n      static auto scaler(int min_from, int max_from, \n                         double min_to, double max_to)\n      {\n          const int    w_from   {max_from - min_from};\n          const double w_to     {max_to - min_to};\n          const int    mid_from {(max_from - min_from) / 2 + min_from};\n          const double mid_to   {(max_to - min_to) / 2.0 + min_to};\n\n          return [=] (int from) {\n              return double(from - mid_from) / w_from * w_to + mid_to;\n          };\n      }\n```\n\n4.  现在我们可以在一维上变换点，但是曼德勃罗集合存在于二维坐标系中。为了从一个`(x, y)`坐标系转换到另一个坐标系，我们结合了一个 x 缩放器和一个 y 缩放器，并根据它们的输出构建了一个`cmplx`实例:\n\n```cpp\n      template <typename A, typename B>\n      static auto scaled_cmplx(A scaler_x, B scaler_y)\n      {\n          return [=](int x, int y) {\n              return cmplx{scaler_x(x), scaler_y(y)};\n          };\n      }\n```\n\n5.  在能够将坐标转换到正确的维度之后，我们现在可以实现曼德勃罗公式。我们现在实现的函数对终端窗口或线性平面变换的概念一无所知，所以我们可以专注于曼德勃罗数学。我们将`z`平方，并在一个循环中添加`c`，直到其`abs`值小于`2`。对于某些坐标，这种情况从来不会发生，所以如果迭代次数超过`max_iterations`，我们也会跳出循环。最后，我们返回在`abs`值收敛之前必须进行的迭代次数:\n\n```cpp\n      static auto mandelbrot_iterations(cmplx c)\n      {\n          cmplx z {};\n          size_t iterations {0};\n          const size_t max_iterations {1000};\n          while (abs(z) < 2 && iterations < max_iterations) {\n              ++ iterations;\n              z = pow(z, 2) + c;\n          }\n          return iterations;\n      }\n```\n\n6.  我们现在可以从主函数开始，在主函数中，我们定义了终端尺寸，并实例化了一个函数对象`scale`，它可以缩放我们在两个轴上的坐标值:\n\n```cpp\n      int main()\n      {\n          const size_t w {100};\n          const size_t h {40};\n\n          auto scale (scaled_cmplx(\n              scaler(0, w, -2.0, 1.0),\n              scaler(0, h, -1.0, 1.0)\n          ));\n```\n\n7.  为了对整个图像进行一维迭代，我们编写了另一个接受一维`i`坐标的变换函数。它根据我们假设的字符行宽度计算`(x, y)`坐标。在将`i`分解为行号和列号后，它用我们的`scale`函数对它们进行转换，并返回复坐标。\n\n```cpp\n          auto i_to_xy ([=](int i) { return scale(i % w, i / w); });\n```\n\n8.  我们现在可以做的是从一维坐标(T0)类型，通过二维坐标(T1)类型，转换到曼德勃罗集合坐标(T2)类型，然后从那里计算迭代次数(再次是 T3 类型)。让我们将所有这些结合在一个函数中，为我们建立这个调用链:\n\n```cpp\n          auto to_iteration_count ([=](int i) { \n              return mandelbrot_iterations(i_to_xy(i));\n          });\n```\n\n9.  现在我们可以设置所有的数据。我们假设生成的 ASCII 图像宽为`w`个字符，高为`h`个字符。这可以保存在具有`w * h`元素的一维向量中。我们使用`std::iota`用值范围 *0 填充该向量...(w*h - 1)* 。这些数字可以作为我们构建的转换函数范围的输入源，我们刚刚将其封装在`to_iteration_count`中:\n\n```cpp\n          vector<int> v (w * h);\n          iota(begin(v), end(v), 0);\n          transform(begin(v), end(v), begin(v), to_iteration_count);\n```\n\n10.  基本上就是这样。现在我们有了`v`向量，我们用一维坐标初始化了它，但是它被曼德勃罗迭代计数器覆盖了。由此，我们现在可以打印一个漂亮的图像。我们可以将终端窗口`w`做成字符宽，这样我们就不需要在中间打印换行符。但是我们也可以创造性地滥用*`std::accumulate`来为我们做分割线。`std::accumulate`使用二进制函数缩小范围。我们为它提供了一个二进制函数，它接受一个输出迭代器(我们将在下一步中将其链接到终端)和一个范围内的单个值。如果迭代次数大于 50，我们将该值打印为`*`字符。否则，我们只打印一个空格字符。如果我们在*行尾*(因为计数器变量`n`可被`w`整除)，我们打印一个换行符:*\n\n```cpp\n          auto binfunc ([w, n{0}] (auto output_it, int x) mutable {\n              *++ output_it = (x > 50 ? '*' : ' ');\n              if (++ n % w == 0) { ++ output_it = 'n'; }\n              return output_it;\n          });\n```\n\n11.  通过在输入范围上调用`std:accumulate`，结合我们的二进制打印功能和一个`ostream_iterator`，我们可以将计算出的曼德勃罗集刷新到终端窗口:\n\n```cpp\n          accumulate(begin(v), end(v), ostream_iterator<char>{cout}, \n                     binfunc);\n      }\n```\n\n12.  编译并运行该程序会得到以下输出，它看起来像最初的详细曼德勃罗图像，但形式很简单:\n\n![](img/eb4a8203-0f69-4c38-b98e-213ea3541018.png)\n\n# 它是如何工作的...\n\n整个计算是在对一维数组的调用期间进行的:\n\n```cpp\nvector<int> v (w * h);\niota(begin(v), end(v), 0);\ntransform(begin(v), end(v), begin(v), to_iteration_count);\n```\n\n到底发生了什么，为什么会这样？`to_iteration_count`功能基本上是一条从`i_to_xy`经过`scale`到`mandelbrot_iterations`的调用链。下图说明了转换步骤:\n\n![](img/cd9a2b82-10aa-4236-b3bd-a087eb34f71f.png)\n\n这样，我们就可以用一维数组的索引作为输入，得到这个数组点所代表的二维平面的点上的 Mandelbrot 公式迭代次数。好的一点是，这三个转换彼此完全不可知。具有这种关注点分离的代码可以很好地测试，因为每个组件都可以单独测试，而不需要其他组件。这样，很容易发现和修复 bug，或者只是对其正确性进行推理。\n\n# 构建我们自己的算法-拆分\n\n在某些情况下，现有的 STL 算法是不够的。但是没有什么阻碍我们实现我们自己的。在解决一个具体问题之前，我们应该坚定地思考它，以便意识到许多问题可以用一般的方法来解决。如果我们在解决自己的问题时定期堆积一些新的库代码，那么当我们的程序员同事有类似的问题需要解决时，我们也在帮助他们。关键是要知道什么时候它足够通用，什么时候不要追求比需要的更多的通用——否则我们最终会得到一种新的通用语言。\n\n在这个食谱中，我们正在实现一个算法，我们称之为`split`。它可以在特定值每次出现时拆分任意范围的项，并将由此产生的块复制到输出范围中。\n\n# 怎么做...\n\n在本节中，我们将实现我们自己的类似 STL 的算法`split`，然后我们通过拆分一个示例字符串来检查它:\n\n1.  首先，我们包括一些 STL 库部分，并声明我们使用`std`命名空间:\n\n```cpp\n      #include <iostream>\n      #include <string>\n      #include <algorithm>\n      #include <iterator>\n      #include <list>      \n\n      using namespace std;\n```\n\n2.  本节围绕的整个算法是`split`。它接受一对开始/结束输入迭代器和一个输出迭代器，这使得它最初类似于`std::copy`或`std::transform`。其他参数为`split_val`和`bin_func`。`split_val`参数是我们在输入范围内搜索的值，它代表我们切割输入间隔的一个分割点。`bin_func`参数是一个转换一对迭代器的函数，这对迭代器标记了这种分割块子范围的开始和结束。我们使用`std::find`迭代输入范围，因此我们从`split_val`值的出现跳到另一个出现。当把一个长字符串拆分成单个单词时，我们会从一个空格字符跳到另一个空格字符。对于每个拆分值，我们都会停下来形成一个块，并将其输入输出范围:\n\n```cpp\n      template <typename InIt, typename OutIt, typename T, typename F>\n      InIt split(InIt it, InIt end_it, OutIt out_it, T split_val, \n                 F bin_func)\n      {\n          while (it != end_it) {\n              auto slice_end (find(it, end_it, split_val));\n              *out_it++ = bin_func(it, slice_end);\n\n              if (slice_end == end_it) { return end_it; }\n              it = next(slice_end);\n          }\n          return it;\n      }\n```\n\n3.  让我们使用新的算法。我们构造了一个要拆分的字符串。标记最后一个块的结束和下一个块的开始的项目应该是破折号`'-'`:\n\n```cpp\n      int main()\n      {\n          const string s {\"a-b-c-d-e-f-g\"};\n```\n\n4.  每当算法在一对迭代器上调用它的`bin_func`时，我们都想从它构造一个新的字符串:\n\n```cpp\n          auto binfunc ([](auto it_a, auto it_b) {\n              return string(it_a, it_b);\n          });\n```\n\n5.  输出范围将是一串`std::list`。我们现在可以称之为`split`算法，与所有其他 STL 算法相比，它具有相似的设计:\n\n```cpp\n          list<string> l;\n          split(begin(s), end(s), back_inserter(l), '-', binfunc);\n```\n\n6.  为了看看我们得到了什么，让我们打印新的字符串分块列表:\n\n```cpp\n          copy(begin(l), end(l), ostream_iterator<string>{cout, \"n\"});\n      }\n```\n\n7.  编译并运行程序会产生以下输出。它不再包含破折号，并显示它已经隔离了单个单词(当然，在我们的示例字符串中，这些单词只是单个字符):\n\n```cpp\n      $ ./split \n      a\n      b\n      c\n      d\n      e\n      f\n      g\n```\n\n# 它是如何工作的...\n\n`split`算法的工作方式与`std::transform`相似，因为它接受一对输入范围的开始/结束迭代器和一个输出迭代器。它对输入范围做了一些事情，最终导致对输出迭代器的赋值。除此之外，它接受一个名为`split_val`的项目值和一个二元函数。让我们重新审视整个实现，以充分理解它:\n\n```cpp\ntemplate <typename InIt, typename OutIt, typename T, typename F>\nInIt split(InIt it, InIt end_it, OutIt out_it, T split_val, F bin_func)\n{\n    while (it != end_it) {\n        auto slice_end (find(it, end_it, split_val));\n        *out_it++ = bin_func(it, slice_end);\n\n        if (slice_end == end_it) { return end_it; }\n        it = next(slice_end);\n    }\n    return it;\n}\n```\n\n循环要求迭代，直到输入范围结束。在每次迭代中，使用`std::find`调用来查找输入范围中的下一个元素，该元素等于`split_val`。在我们的例子中，该元素是破折号字符(`'-'`)，因为我们希望在所有破折号位置拆分输入字符串。下一个破折号位置现在保存在`slice_end`中。在循环迭代之后，`it`迭代器被放在下一个经过分割位置的项目上。这样，循环直接从一个项目跳到另一个项目，而不是跳过每个项目。\n\n在这个星座中，迭代器`it`指向最后一个切片的开始，而`slice_end`指向最后一个切片的结束。这两个迭代器组合在一起，标记了恰好代表两个破折号之间的一个片段的子范围的开始和结束。在一个字符串中，`\"foo-bar-baz\"`，这意味着我们有三次循环迭代，每次我们得到一对迭代器，它们围绕一个单词。但是我们实际上并不需要迭代器，而是`substrings`。二元函数`bin_func`为我们做到了这一点。当我们调用`split`时，我们给了它以下二元函数:\n\n```cpp\n[](auto it_a, auto it_b) {\n    return string(it_a, it_b);\n}\n```\n\n`split`函数通过`bin_func`抛出每对迭代器，然后将其送入输出迭代器。我们实际上从`bin_func`得到字符串实例，这导致了`\"foo\"`、`\"bar\"`和`\"baz\"`:\n\n# 还有更多...\n\n实现我们自己的字符串分割算法的一个有趣的替代方法是实现一个同样的*迭代器*。我们现在不打算实现这样的迭代器，但是让我们简单地看一下这样的场景。\n\n迭代器需要在每次增量时在分隔符之间跳转。每当它被取消引用时，它需要从它当前指向的迭代器位置创建一个字符串对象，这可以使用我们之前使用的二进制函数`binfunc`来完成。\n\n如果我们有一个名为`split_iterator`的迭代器类，而不是算法`split`，用户代码将如下所示:\n\n```cpp\nstring s {\"a-b-c-d-e-f-g\"};\nlist<string> l;\n\nauto binfunc ([](auto it_a, auto it_b) {\n    return string(it_a, it_b);\n});\n\ncopy(split_iterator{begin(s), end(s), ‘-‘, binfunc},{}, back_inserter(l));\n```\n\n这种方法的缺点是实现迭代器通常比单个函数更复杂。此外，迭代器代码中有许多微妙的边缘会导致错误，因此迭代器解决方案需要更繁琐的测试。另一方面，将这样的迭代器与其他 STL 算法结合起来非常简单。\n\n# 从标准算法中组合有用的算法-收集\n\nSTL 算法可组合性的一个很好的例子是`gather`。当时 Adobe Systems 的首席科学家肖恩·帕朗特(Sean Parent)推广了这种算法，因为它既有用又简短。它的实现方式，是 STL 算法合成思想的理想海报子。\n\n`gather`算法对任意项目类型的范围进行操作。它以这样一种方式修改项目的顺序，即特定的项目聚集在调用者选择的特定位置周围。\n\n# 怎么做...\n\n在本节中，我们将实现`gather`算法及其一个额外的变体。之后，我们看看如何使用它:\n\n1.  首先，我们添加所有的 STL include 语句。然后，我们声明使用`std`命名空间:\n\n```cpp\n      #include <iostream>\n      #include <algorithm>\n      #include <string>\n      #include <functional>      \n\n      using namespace std;\n```\n\n2.  `gather`算法是标准算法组合的一个很好的例子。`gather`接受一个开始/结束迭代器对，和另一个迭代器`gather_pos`，它位于两者之间。最后一个参数是谓词函数。使用这个谓词函数，算法将把所有*做的*满足谓词的项目推到`gather_pos`迭代器附近。项目运动的实施由`std::stable_partition`完成。`gather`算法的返回值是一对迭代器。这些迭代器是从`stable_partition`调用返回的，这样，它们标记了现在收集的范围的开始和结束:\n\n```cpp\n      template <typename It, typename F>\n      pair<It, It> gather(It first, It last, It gather_pos, F predicate)\n      {\n          return {stable_partition(first, gather_pos, not_fn(predicate)),\n                  stable_partition(gather_pos, last, predicate)};\n      }\n```\n\n3.  聚集的另一个变体是`gather_sort`。它的工作方式基本与`gather`相同，但不接受一元谓词函数；它接受二进制比较函数。这样就可以收集到`gather_pos`附近的数值，这些数值出现在*最小的*或*最大的*处:\n\n```cpp\n      template <typename It>\n\n      void gather_sort(It first, It last, It gather_pos)\n\n      {\n\n        using T = typename std::iterator_traits<It>::value_type;\n\n        stable_sort(first, gather_pos, greater<T>{});\n\n        stable_sort(gather_pos, last, less<T>{});\n\n      }\n```\n\n4.  让我们使用这些算法。我们从一个谓词开始，它告诉给定的字符参数是否是`'a'`字符。我们构造了一个字符串，它由非常交错的`'a'`和`'_'`字符组成:\n\n```cpp\n      int main()\n      {\n          auto is_a ([](char c) { return c == 'a'; });\n          string a {\"a_a_a_a_a_a_a_a_a_a_a\"};\n```\n\n5.  我们构造一个迭代器，它指向新字符串的中间。让我们在上面调用`gather`看看会发生什么。`'a'`字符应该聚集在中间之后:\n\n```cpp\n          auto middle (begin(a) + a.size() / 2);\n\n          gather(begin(a), end(a), middle, is_a);\n          cout << a << 'n';\n```\n\n6.  让我们再次调用`gather`，但是这一次，`gather_pos`迭代器不在中间而是开始:\n\n```cpp\n          gather(begin(a), end(a), begin(a), is_a);\n          cout << a << 'n';\n```\n\n7.  在第三次调用中，我们围绕结束迭代器收集项目:\n\n```cpp\n          gather(begin(a), end(a), end(a), is_a);\n          cout << a << 'n';\n```\n\n8.  随着`gather`的最后一次召唤，我们再次尝试聚集中间所有`'a'`角色。这不会像预期的那样起作用，我们稍后会看到原因:\n\n```cpp\n          // This will NOT work as naively expected\n          gather(begin(a), end(a), middle, is_a);\n          cout << a << 'n';\n```\n\n9.  我们用下划线字符和一些数值构造另一个字符串。在该输入序列上，我们应用`gather_sort`。`gather_pos`迭代器是字符串的中间，二进制比较函数是`std::less<char>`:\n\n```cpp\n          string b {\"_9_2_4_7_3_8_1_6_5_0_\"};\n          gather_sort(begin(b), end(b), begin(b) + b.size() / 2, \n                      less<char>{});\n          cout << b << 'n';\n      }\n```\n\n10.  编译和运行程序会产生以下有趣的输出。前三行看起来像是预期的，但是第四行看起来像是`gather`对字符串做了*什么也没做*。\n    在最后一行，我们可以看到`gather_short`函数的结果。这些数字朝两个方向排序:\n\n```cpp\n      $ ./gather \n      _____aaaaaaaaaaa_____\n      aaaaaaaaaaa__________\n      __________aaaaaaaaaaa\n      __________aaaaaaaaaaa\n      _____9743201568______\n```\n\n# 它是如何工作的...\n\n最初，`gather`算法很难掌握，因为它很短，但任务看似复杂。因此，让我们一步步来:\n\n![](img/fcb79a46-e8cb-4732-aee3-ba7818d713d7.png)\n\n1.  初始状态是一系列项目，我们为它们提供了一个谓词函数。在图中，我们的谓词函数返回`true`的所有项目都用*灰色*绘制。迭代器`a`和`c`标记整个范围，迭代器`b`指向一个*枢轴*元素。枢轴元素是我们想要围绕其*聚集*所有灰色项目的元素。\n2.  `gather`算法在范围`[a, b)`上调用`std::stable_partition`，在此过程中，它使用了一个*否定的*版本的谓词。它否定谓词，因为`std::stable_partition`将谓词返回`true`的所有项目移到*前面*。我们希望发生完全相反的情况。\n\n3.  另一个`std::stable_partition`调用完成了，但是，这一次，在范围上，`[b, c)`和*没有*否定谓词。灰色项目被移动到输入范围的前面，这意味着它们都被移动到`b`指向的枢轴元素。\n4.  项目现在聚集在`b`周围，算法将迭代器返回到现在连续的灰色项目范围的开始和结束。\n\n我们在同一个范围内多次调用`gather`。首先，我们收集了范围中间的所有项目。然后我们收集了范围内的`begin()`和`end()`周围的物品。这些案例很有趣，因为它们总是导致*的一个`std::stable_partition`呼叫在一个*空的*范围内操作，这导致*没有动作*。*\n\n我们用范围的参数`(begin, end, middle)`做了最后一次调用来再次聚集，但没有成功。为什么呢？起初，这看起来像一个 bug，但实际上，它不是。\n\n想象一下字符范围`\"aabb\"`，以及谓词函数`is_character_a`，这仅适用于`'a'`项——如果我们用指向字符范围中间的第三个迭代器来调用它，我们会观察到同样的*错误*。原因是第一个`stable_partition`调用将在子范围`\"aa\"`上运行，而另一个`stable_partition`调用将在范围`\"bb\"`上运行。这一系列的通话不可能产生`\"baab\"`，这是我们最初天真的希望。\n\nIn order to get what we want in the last case, we could use `std::rotate(begin, begin + 1, end);`\n\n`gather_sort`修改与`gather`基本相同。唯一不同的是，它不接受一元*谓词*函数，而是接受二元*比较*函数，就像`std::sort`一样。它没有两次呼叫`std::stable_partition`，而是两次呼叫`std::stable_sort`。\n\n比较函数的否定不能用`not_fn`来完成，就像我们在`gather`算法中所做的一样，因为`not_fn`对二进制函数不起作用。\n\n# 删除单词之间的连续空白\n\n因为字符串通常是从用户输入中读取的，所以它们可能包含任意格式，并且经常需要清理。这方面的一个例子是包含太多空白的字符串。\n\n在这一节中，我们将实现一个光滑的空白过滤算法，从字符串中移除多余的空白，但保留单个空白字符不变。我们把那个算法叫做`remove_multi_whitespace`，它的界面看起来会很像 STL。\n\n# 怎么做...\n\n在本节中，我们将实现`remove_multi_whitespace`算法，并了解其工作原理:\n\n1.  像往常一样，我们先做一些 includes，然后声明我们默认使用`std`命名空间:\n\n```cpp\n      #include <iostream>\n      #include <string>\n      #include <algorithm>      \n\n      using namespace std;\n```\n\n2.  我们实现了一种新的 STL 风格的算法`remove_multi_whitespace`。该算法删除了聚集出现的空格，但没有单个空格。这意味着一根弦线`\"a b\"`保持不变，但是一根像\n    `\"a b\"`这样的弦收缩到`\"a b\"`。为了实现这一点，我们使用带有自定义二元谓词函数的`std::unique`。`std::unqiue`在一个可重复的范围内行走，并且总是观察连续的有效载荷项目对。然后询问谓词函数两项是否相等。如果是，则`std::unique`移除其中一个。之后，该范围不包含具有相邻相等项目的子范围。通常在这种情况下应用的谓词函数可以判断两个项目是否相等。我们要做的是，给`std::unique`一个谓词，它告诉是否有两个连续的*空格*，以便移除这些空格。就像`std::unique`一样，我们接受一对开始/结束迭代器，然后返回一个指向范围新结束的迭代器:\n\n```cpp\n      template <typename It>\n      It remove_multi_whitespace(It it, It end_it)\n      {\n          return unique(it, end_it, [](const auto &a, const auto &b) {\n              return isspace(a) && isspace(b);\n          });\n      }\n```\n\n3.  已经这样了。让我们构造一个包含一些不必要空白的字符串:\n\n```cpp\n      int main()\n      {\n          string s {\"fooo     bar    t   baz\"};\n\n          cout << s << 'n';\n```\n\n4.  现在，我们在字符串上使用*擦除-移除成语*，以去除多余的空白字符:\n\n```cpp\n          s.erase(remove_multi_whitespace(begin(s), end(s)), end(s));\n\n          cout << s << 'n';\n      }\n```\n\n5.  编译和运行程序会产生以下输出:\n\n```cpp\n      $ ./remove_consecutive_whitespace \n      fooo     bar        baz\n      fooo bar baz\n```\n\n# 它是如何工作的...\n\n我们解决了问题的全部复杂性，没有任何循环或手动比较项目。我们只提供了一个谓词函数，它告诉我们两个给定的字符是否是*空白*字符。然后我们把这个谓词输入到`std::unique`和*噗*中，所有多余的空白都消失了。虽然这一章也包含了一些食谱，在这些食谱中，我们不得不更多地使用 STL 算法来表达我们的程序，但是这个算法是一个非常好且简短的例子。\n\n这个有趣的组合是如何详细工作的？我们先来看看`std::unique`的一个可能实现:\n\n```cpp\ntemplate<typename It, typename P>\nIt unique(It it, It end, P p)\n{\n    if (it == end) { return end; }\n\n    It result {it};\n    while (++ it != end) {\n        if (!p(*result, *it) && ++ result != it) {\n            *result = std::move(*it);\n        }\n    }\n    return ++ result;\n}\n```\n\n循环遍历范围项，而它们不满足谓词条件。当一个项目满足谓词时，它会将这样一个项目移动一个项目到旧位置，即谓词最后一次触发的位置。不接受附加谓词函数的版本`std::unique`检查两个相邻项是否相等。这样，它就可以删除重复的 T4 字符，例如，将 T1 转换为 T2。\n\n我们想要的不是抹去所有重复的*字符，而是重复的*空格*。因此，我们的谓词没有说*“两个自变量字符相等”*，而是说“*两个自变量字符都是空白字符”*。*\n\n最后要注意的是`std::unique`和`remove_multi_whitespace`都没有真正从底层字符串中移除字符项。它们只根据字符串的语义移动字符串中的字符，并告诉新的结束位置。从新的结尾到旧的结尾删除所有现在已经过时的字符仍然必须完成。这就是我们写下以下内容的原因:\n\n```cpp\ns.erase(remove_multi_whitespace(begin(s), end(s)), end(s));\n```\n\n这遵循了我们已经从向量和列表中知道的*擦除-移除*习惯用法。\n\n# 压缩和解压缩字符串\n\n本节讨论的是编码面试中一个相对流行的任务。基本思想是一个函数，它取一个类似`\"aaaaabbbbbbbccc\"`的字符串，并将其转换为一个更短的字符串`\"a5b7c3\"`。之所以是`\"a5\"`是因为有五个`'a'`字。然后是`\"b7\"`因为有七个`'b'`字。这是一个非常简单的*压缩*算法。对于普通文本来说，它的效用降低了，因为普通语言通常不会如此重复，以至于在这种压缩方案下，它的文本表示会变得更短。然而，即使我们必须在没有计算机的白板上完成，它也相对容易实现。棘手的部分是，如果程序从一开始就没有很好的结构，那么很容易编写出有问题的代码。处理字符串通常不是一件困难的事情，但是如果使用遗留的 C 风格格式化函数，实现缓冲区溢出错误的机会在*周围潜伏很多*。\n\n让我们尝试一种 STL 方法来使用这个简单的方案实现字符串压缩和解压缩。\n\n# 怎么做...\n\n在本节中，我们将为字符串实现简单的`compress`和`decompress`函数:\n\n1.  我们首先包含一些 STL 库，然后我们声明使用`std`命名空间:\n\n```cpp\n      #include <iostream>\n      #include <string>\n      #include <algorithm>\n      #include <sstream>\n      #include <tuple>      \n\n      using namespace std;\n```\n\n2.  对于我们廉价的压缩算法，我们试图找到包含相同字符范围的文本块，并单独压缩它们。每当我们从一个字符串位置开始时，我们都希望找到包含不同字符的第一个位置。我们用`std::find`找到范围内的第一个字符，这个字符与当前位置的字符不同。之后，我们返回一个元组，该元组包含第一个不同项的迭代器、填充当前范围的字符变量`c`，以及该子范围包含的出现次数:\n\n```cpp\n      template <typename It>\n      tuple<It, char, size_t> occurrences(It it, It end_it)\n      {\n          if (it == end_it) { return {it, '?', 0}; }\n\n          const char c {*it};\n          const auto diff (find_if(it, end_it, \n                           [c](char x) { return c != x; }));\n\n          return {diff, c, distance(it, diff)};\n      }\n```\n\n3.  `compress`算法不断调用`occurrences`函数。这样，我们从一个相同的角色组跳到另一个。`r << c << n`行将字符推入输出流，然后显示它在这部分输入字符串中出现的次数。输出是一个字符串流，它会随着我们的输出自动增长。最后，我们从中返回一个字符串对象，它包含压缩的字符串:\n\n```cpp\n      string compress(const string &s)\n      {\n          const auto end_it (end(s));\n          stringstream r;\n\n          for (auto it (begin(s)); it != end_it;) {\n              const auto [next_diff, c, n] (occurrences(it, end_it));\n              r << c << n;\n              it = next_diff;\n          }\n\n          return r.str();\n      }\n```\n\n4.  `decompress`方法的工作原理类似，但要简单得多。它不断尝试从输入流中获取一个字符值，然后是下面的数字。根据这两个值，它可以构造一个包含数字所表示的字符的字符串。最后，我们再次从输出流中返回一个字符串。对了，这个`decompress`功能是*不安全*。它很容易被利用。你能猜到吗，怎么猜到的？我们将在后面讨论这个问题:\n\n```cpp\n      string decompress(const string &s)\n      {\n          stringstream ss{s};\n          stringstream r;\n\n          char c;\n          size_t n;\n          while (ss >> c >> n) { r << string(n, c); }\n\n          return r.str();\n      }\n```\n\n5.  在我们的主函数中，我们构造了一个具有大量重复的简单字符串，算法在这个字符串上运行得非常好。让我们打印压缩版本，然后是压缩和再次解压缩的版本。最后，我们应该得到与最初构建的字符串相同的字符串:\n\n```cpp\n      int main()\n      { \n          string s {\"aaaaaaaaabbbbbbbbbccccccccccc\"};\n          cout << compress(s) << 'n';\n          cout << decompress(compress(s)) << 'n';\n      }\n```\n\n6.  编译和运行程序会产生以下输出:\n\n```cpp\n      $ ./compress\n      a9b9c11\n      aaaaaaaaabbbbbbbbbccccccccccc\n```\n\n# 它是如何工作的...\n\n这个程序基本围绕两个功能:`compress`和`decompress`。\n\n解压缩函数非常简单，因为它只包含变量声明、一行代码(它实际上做了一些事情)和下面的 return 语句。做某事的代码行如下:\n\n```cpp\nwhile (ss >> c >> n) { r << string(n, c); }\n```\n\n它从字符串流`ss`中连续读取字符`c`和计数器变量`n`。`stringstream`类在这一点上对我们隐藏了很多字符串解析的魔力。成功后，它会在字符串流中构建一个解压缩的字符串块，最终的结果字符串可以从该字符串块返回给`decompress`的调用者。如果`c = 'a'`和`n = 5`，表达式`string(n, c)`将产生一个包含内容的字符串，`\"aaaaa\"`。\n\n压缩功能更复杂。我们还为它编写了一个小助手函数。我们称之为辅助函数`occurences`。那么，我们先来看一下`occurrences`。下图显示了它的工作原理:\n\n![](img/888717aa-d626-4a9c-a675-414518c45197.png)\n\n`occurences`函数接受两个参数:一个指向一个范围内字符序列开始的迭代器和该范围的结束迭代器。使用`find_if`，它会找到第一个与最初指向的字符不同的字符。在图中，这是迭代器，`diff`。新位置和旧迭代器位置之间的区别在于相等项目的数量(图中`diff - it`等于 **6** )。计算完这些信息后，`diff`迭代器可以被重用，以便执行下一次搜索。因此，我们将`diff`、子范围的字符以及子范围的长度打包成一个元组并返回。\n\n有了像这样排列的信息，我们可以从一个子范围跳到另一个子范围，并将中间结果推入压缩的目标字符串:\n\n```cpp\nfor (auto it (begin(s)); it != end_it;) { \n    const auto [next_diff, c, n] (occurrences(it, end_it)); \n    r << c << n; \n    it = next_diff; \n}\n```\n\n# 还有更多...\n\n第四步，我们提到`decompress`功能不安全。事实上，它很容易被利用。\n\n想象一下下面的输入字符串:`\"a00000\"`。压缩它将产生子串`\"a1\"`，因为只有一个字符，`'a'`。接下来是五次`'0'`，这将导致`\"05\"`。这一起导致压缩的字符串`\"a105\"`。不幸的是，这个压缩字符串表示*是字符`'a'`“*的 105 倍。这与我们的初始输入字符串无关。更糟糕的是，如果我们解压缩它，我们会从一个六个字符的字符串变成一个 105 个字符的字符串。想象一下更大的数字也是如此——用户可以很容易地*炸毁*我们的堆使用，因为我们的算法没有为这样的输入做好准备。\n\n为了防止这种情况，例如`compress`功能可以拒绝带有数字的输入，或者它可以用一种特殊的方式屏蔽它们。`decompress`算法可以采用另一个条件，它对结果字符串的大小设置了一个上限。我把这个留给你做练习。****"
  },
  {
    "path": "docs/exp-cpp-prog/06.md",
    "content": "# 六、字符串、流类和正则表达式\n\n我们将在本章介绍以下食谱:\n\n*   创建、连接和转换字符串\n*   修剪字符串开头和结尾的空白\n*   获得`std::string`的舒适性，无需花费构建`std::string`对象的成本\n*   从用户输入中读取值\n*   计算文件中的所有单词\n*   用输入输出流操纵器格式化您的输出\n*   从文件输入初始化复杂对象\n*   从`std::istream`迭代器填充容器\n*   使用`std::ostream`迭代器进行通用打印\n*   将输出重定向到特定代码段的文件\n*   从`std::char_traits`继承创建自定义字符串类\n*   使用正则表达式库标记输入\n*   根据上下文动态打印不同的数字，非常舒服\n*   从`std::iostream`错误中捕捉可读异常\n\n# 介绍\n\n本章专门讨论任意数据的字符串处理、解析和打印。对于此类作业，STL 提供其*输入/输出流库*。该库基本上由以下类组成，每个类都用灰色框表示:\n\n![](img/11e3bdf6-16ba-4b13-a4f1-07d22b52f7b8.png)\n\n箭头显示了类的继承方案。这在一开始可能看起来很难，但是我们将在本章中使用这些类的大部分，并逐个类地熟悉它们。在查看 C++ STL 文档中的那些类时，我们不会直接用这些*确切的*名称找到它们。这是因为图中的名称是我们作为应用程序员看到的，但它们实际上大多只是带有`basic_`类名前缀的类的类型定义(例如，我们将更容易在 STL 文档中搜索`basic_istream`而不是`istream`)。`basic_*`输入输出流类是可以专门用于不同字符类型的模板。图中的类专门研究`char`值。我们将在整本书中使用这些专门化。如果我们在这些类名前面加上`w`字符，我们会得到`wistream`、`wostream`等等——例如，这些是`wchar_t`而不是`char`的专门化类型定义。\n\n在图的顶部，我们看到`std::ios_base`。我们基本上不会直接使用它，但它是为了完整性而列出的，因为所有其他类都从它继承。下一个特殊化是`std::ios`，它体现了一个维护数据流的对象的思想，可以处于*好的*状态，运行*空的*数据状态(EOF)，或者某种*失败的*状态。\n\n我们将实际使用的第一个专门化是`std::istream`和`std::ostream`。`\"i\"`和`\"o\"`前缀代表输入和输出。在我们最早的 C++ 编程时代，我们已经在最简单的例子中以对象`std::cout`和`std::cin`(但也是`std::cerr`)的形式看到了它们。这些是这些类的实例，它们总是全局可用的。我们通过`ostream`进行数据输出，通过`istream`进行输入。\n\n从`istream`和`ostream`继承的一个类是`iostream`。它结合了输入和输出能力。当我们理解了由`istream`、`ostream`和`iostream`组成的三个组中的所有类都可以使用时，我们基本上也准备好立即使用以下所有类:\n\n`ifstream`、`ofstream`和`fstream`分别继承自`istream`、`ostream`和`iostream`，但提升了它们将输入/输出从计算机的*文件系统*重定向到文件的能力。\n\n`istringstream`、`ostringstream`和`iostringstream`的工作原理非常相似。它们有助于在内存中构建字符串，和/或从中消费数据。\n\n# 创建、连接和转换字符串\n\n即使是很久以前的 C++ 程序员也会知道`std::string`。虽然字符串处理在 C 语言中既繁琐又痛苦，尤其是在解析、连接、复制等方面，`std::string`在简单性和安全性方面迈出了真正的一步。\n\n多亏了 C++ 11，当我们想要将所有权转移到其他函数或数据结构时，我们甚至不再需要复制字符串，因为我们可以*移动*它们。这样，在大多数情况下不会有太多开销。\n\n在过去的几个标准增量中，`std::string`到处都有一些新特性。C++ 17 中全新的是`std::string_view`。我们将两者都玩一会儿(但还有另一个食谱，它更专注于`std::string_view`-唯一的特性)，以了解它们以及它们在 C++ 17 时代是如何工作的。\n\n# 怎么做...\n\n在本节中，我们将创建字符串和字符串视图，并使用它们进行基本的连接和转换:\n\n1.  像往常一样，我们首先包含头文件，并声明我们使用`std`命名空间:\n\n```cpp\n      #include <iostream>\n      #include <string>\n      #include <string_view>\n      #include <sstream>\n      #include <algorithm>      \n\n      using namespace std;\n```\n\n2.  让我们首先创建字符串对象。最明显的方法是实例化类`string`的对象`a`。我们通过给构造函数一个 C 风格的字符串来控制它的内容(它将在编译后作为包含字符的静态数组嵌入到二进制文件中)。构造函数将复制它，并将其作为字符串对象`a`的内容。或者，我们可以使用字符串文字运算符`\"\"s`，而不是从 C 风格的字符串初始化它。它会动态创建一个字符串对象。用它来构造对象`b`，我们甚至可以使用自动类型演绎:\n\n```cpp\n      int main()\n      {\n          string a { \"a\"  };\n          auto   b ( \"b\"s );\n```\n\n3.  我们刚刚创建的字符串是*将它们的输入从构造函数参数复制到自己的缓冲区中。为了不抄袭，而是*引用*底层字符串，我们可以使用`string_view`实例。这个类确实也有一个文字运算符，叫做`\"\"sv`:*\n\n```cpp\n          string_view c { \"c\"   };\n          auto        d ( \"d\"sv );\n```\n\n4.  好了，现在让我们玩一下我们的弦乐和弦乐视图。对于这两种类型，`std::ostream`类都有`operator<<`重载，因此可以轻松打印:\n\n```cpp\n          cout << a << \", \" << b << 'n';\n          cout << c << \", \" << d << 'n';\n```\n\n5.  字符串类重载`operator+`，所以我们可以*添加*两个字符串，并得到它们的串联结果。这样，`\"a\" + \"b\"`就产生了`\"ab\"`。以这种方式连接`a`和`b`很容易。有了`a`和`c`，就没那么容易了，因为 c 不是一个`string`，而是一个`string_view`。我们必须先从`c`中取出字符串，这可以通过从`c`中构造一个新的字符串，然后将其添加到`a`中来完成。在这一点上，人们可能会问，“等等，你为什么要把`c`复制到一个中间字符串对象中，只是为了把它添加到`a`中？你不能用`c.data()`来避免那个副本吗？”这是一个不错的想法，但它有一个缺陷- `string_view`实例不必携带以零结尾的字符串。这是一个可能导致缓冲区溢出的问题:\n\n```cpp\n          cout << a + b << 'n';\n          cout << a + string{c} << 'n';\n```\n\n6.  让我们创建一个新的字符串，它包含我们刚刚创建的所有字符串和字符串视图。通过使用`std::ostringstream`，我们可以*将*任何变量打印到行为与`std::cout`完全相同的流对象中，但是它不会打印到 shell 中。相反，它会打印到一个*字符串缓冲区*中。在我们使用`operator<<`对所有变量进行流处理并在它们之间留有一些分隔空间之后，我们可以使用`o.str()`从这些变量中构造并打印一个新的字符串对象:\n\n```cpp\n          ostringstream o;\n\n          o << a << \" \" << b << \" \" << c << \" \" << d;\n          auto concatenated (o.str());\n          cout << concatenated << 'n';\n```\n\n7.  例如，我们现在还可以通过将新字符串的所有字母转换为大写来转换它。将小写字符映射到大写字符并保持其他字符不变的 C 库函数`toupper`已经可用，并且可以与`std::transform`组合，因为字符串基本上也是带有`char`项的可迭代容器对象:\n\n```cpp\n          transform(begin(concatenated), end(concatenated), \n                    begin(concatenated), ::toupper);\n          cout << concatenated << 'n';\n      }\n```\n\n8.  编译和运行程序会产生以下输出，这正是我们所期望的:\n\n```cpp\n      $ ./creating_strings \n      a, b\n      c, d\n      ab\n      ac\n      a b c d\n      A B C D\n```\n\n# 它是如何工作的...\n\n显然，字符串可以像数字一样用`+`运算符相加，但这与数学无关，而是导致*串联*字符串。为了和`string_view`混合，我们需要先转换到`std::string`。\n\n但是，真正需要注意的是，在代码中混合字符串和字符串视图时，我们绝不能假设一个`string_view`后面的底层字符串是*零终止*！这就是为什么我们宁愿写`\"abc\"s + string{some_string_view}`而不写`\"abc\"s + some_string_view.data()`。除此之外，`std::string`提供了一个成员函数，`append`，它可以处理`string_view`实例，但是它改变了字符串，而不是返回一个附加了字符串视图内容的新字符串。\n\n`std::string_view` is useful, but be cautious when mixing it with strings and string functions. We cannot assume that they are zero-terminated, which breaks things quickly in a standard string environment. Fortunately, there are often proper function overloads, which can deal with them the right way.\n\n如果我们想通过格式化等方式进行复杂的字符串连接，那么我们不应该在字符串实例上逐个进行。`std::stringstream`、`std::ostringstream`和`std::istringstream`类更适合这种情况，因为它们在追加时增强了内存管理，并提供了我们通常从流中知道的所有格式化特性。`std::ostringstream`类是我们在本节中选择的，因为我们要创建一个字符串，而不是解析它。一个`std::istringstream`实例可以从一个现有的字符串实例化，然后我们可以轻松地解析成其他类型的变量。如果要两者结合，`std::stringstream`就是完美的全才。\n\n# 修剪字符串开头和结尾的空白\n\n特别是当从用户输入中获取字符串时，它们经常被不需要的空白所污染。在另一个方法中，我们删除了单词之间多余的空白。\n\n现在让我们来看看被空格包围的字符串，并去掉它。`std::string`有一些很好的助手功能来完成这项工作。\n\nAfter reading this recipe that shows how to do this with plain string objects, make sure to also read the following recipe. There we will see how to avoid unnecessary copies or data modifications with the new `std::string_view` class.\n\n# 怎么做...\n\n在这一节中，我们将编写一个助手函数来识别字符串中周围的空白，并返回一个没有空白的副本，然后我们将简单地测试它。\n\n1.  和往常一样，头包含和使用指令排在第一位:\n\n```cpp\n      #include <iostream>\n      #include <string>\n      #include <algorithm>\n      #include <cctype>\n\n      using namespace std;\n```\n\n2.  我们修剪字符串周围空白的函数对现有字符串进行常量引用。它将返回一个没有任何周围空白的新字符串:\n\n```cpp\n      string trim_whitespace_surrounding(const string &s)\n      {\n```\n\n3.  `std::string`提供了两个方便的功能，对我们帮助很大。第一个是`string::find_first_not_of`，它接受一个包含我们想要跳过的所有字符的字符串。当然，这是空白，意思是字符空间`' '`、制表符`'t'`和新行`'n'`。它返回给我们第一个非空白字符位置。如果字符串中只有空格，则返回`string::npos`。这意味着，如果我们从中删除空白，就只剩下一个空字符串。因此，在这种情况下，让我们返回一个空字符串:\n\n```cpp\n          const char whitespace[] {\" tn\"};\n          const size_t first (s.find_first_not_of(whitespace));\n          if (string::npos == first) { return {}; }\n```\n\n4.  我们现在知道新字符串必须从哪里开始，但我们还不知道它必须从哪里结束。因此，我们使用另一个方便的字符串函数`string::find_last_not_of`。它将返回字符串中最后一个没有空格的字符位置:\n\n```cpp\n          const size_t last (s.find_last_not_of(whitespace));\n```\n\n5.  使用`string::substr`，我们现在可以返回字符串中被空格包围但没有空格的部分。该函数采用两个参数——字符串中第一个*位置*和该位置后面的*字符数*:\n\n```cpp\n          return s.substr(first, (last - first + 1));\n      }\n```\n\n6.  就这样。让我们编写一个主函数，在这个函数中，我们创建一个字符串，用各种各样的空白包围一个文本句子，以便对它进行修剪:\n\n```cpp\n      int main()\n      {\n          string s {\" tn string surrounded by ugly\"\n                    \" whitespace tn \"};\n```\n\n7.  我们打印字符串的未修剪和修剪版本。通过用括号将字符串括起来，可以更明显地看出在修剪之前哪些空白属于它:\n\n```cpp\n          cout << \"{\" << s << \"}n\";\n          cout << \"{\" \n               << trim_whitespace_surrounding(s) \n               << \"}n\";\n      }\n```\n\n8.  编译和运行程序会产生我们期望的输出:\n\n```cpp\n      $ ./trim_whitespace \n      {  \n        string surrounded by ugly whitespace    \n         }\n      {string surrounded by ugly whitespace}\n```\n\n# 它是如何工作的...\n\n在本节中，我们使用了`string::find_first_not_of`和`string::find_last_not_of`。这两个函数都接受一个 C 风格的字符串，该字符串作为搜索不同字符时应该跳过的字符列表。如果我们有一个携带字符串的字符串实例，`\"foo bar\"`，我们在上面调用`find_first_not_of(\"bfo \")`，它将返回值`5`，因为`'a'`字符是第一个不在`\"bfo \"`字符串中的字符。参数字符串中字符的顺序并不重要。\n\n同样的功能存在于反向逻辑中，尽管我们在本食谱中没有使用它们:`string::find_first_of`和`string::find_last_of`。\n\n类似于基于迭代器的函数，我们需要检查这些函数是否返回了字符串中的实际位置或表示它们没有*而不是*找到满足约束的字符位置的值。如果没有找到，他们会返回`string::npos`。\n\n根据我们在助手函数中从这些函数中检索到的字符位置，我们使用`string::substring`构建了一个不带空格的子字符串。这个函数接受一个相对偏移量和一个字符串长度，然后用自己的内存返回一个新的字符串实例，它只包含那个子字符串。比如`string{\"abcdef\"}.substr(2, 2)`会给我们返回一个新的字符串`\"cd\"`。\n\n# 无需构造 std::string 对象即可获得 std::string 的舒适性\n\n`std::string`类是一个非常有用的类，因为它大大简化了字符串的处理。一个缺陷是，如果我们想要传递子串，我们需要传递一个指针和一个长度变量，两个迭代器，或者子串的一个副本。我们在前面的方法中做到了这一点，我们通过获取不包含周围空白的子字符串范围的副本来移除字符串周围的空白。\n\n如果我们想把一个字符串或者一个子字符串传递给一个甚至不支持`std::string`的库，我们只能提供一个原始的字符串指针，这有点令人失望，因为这让我们回到了过去的 C 天。就像子串问题一样，原始指针不携带关于字符串长度的信息。这样，就必须实现一组指针和一个字符串长度。\n\n简单来说，这正是`std::string_view`的含义。它从 C++ 17 开始可用，并提供了一种将指向某个字符串的指针与该字符串的大小配对的方法。它体现了为数据数组提供引用类型的思想。\n\n如果我们设计的函数以前接受`std::string`实例作为参数，但没有以要求字符串实例重新分配保存实际字符串有效负载的内存的方式改变它们，我们现在可以使用`std::string_view`并与 STL 不可知的库更兼容。我们可以让其他库提供其复杂字符串实现背后的有效负载字符串的`string_view`视图，然后在我们的 STL 代码中使用它。这样，`string_view`类就充当了一个最小且有用的接口，可以在不同的库之间共享。\n\n另一个很酷的地方是`string_view`可以作为较大字符串对象的子串的非复制引用。有很多种利用它获利的可能性。这一节我们就来玩玩`string_view`来感受一下它的起伏。我们还将看到如何通过调整字符串视图来隐藏字符串周围的空白，而不是修改或复制实际的字符串。这种方法避免了不必要的复制或数据修改。\n\n# 怎么做...\n\n我们将实现一个依赖于某些`string_view`特性的函数，然后，我们看到我们可以向其中输入多少种不同的类型:\n\n1.  标题包括和使用指令优先:\n\n```cpp\n      #include <iostream>\n      #include <string_view>\n\n      using namespace std;\n```\n\n2.  我们实现了一个接受`string_view`作为唯一参数的函数:\n\n```cpp\n      void print(string_view v)\n      {\n```\n\n3.  在对输入字符串进行任何操作之前，我们会删除任何前导和尾随空格。我们不打算更改字符串，而是通过将字符串缩小到字符串的实际非空白部分来查看字符串上的*。`find_first_not_of`函数将查找字符串中的第一个字符，该字符不是空格(`' '`)，不是制表符(`'t'`)，也不是换行符(`'n'`)。使用`remove_prefix`，我们将内部`string_view`指针前进到第一个非空白字符。如果字符串只包含空格，`find_first_not_of`函数返回值`npos`，即`size_type(-1)`。由于`size_type`是一个无符号变量，这可以归结为一个非常大的数字。因此，我们取两者中较小的一个:`words_begin`或字符串视图的大小:*\n\n```cpp\n          const auto words_begin (v.find_first_not_of(\" tn\"));\n          v.remove_prefix(min(words_begin, v.size()));\n```\n\n4.  我们对尾随空白做同样的事情。`remove_suffix`缩小视图的大小变量:\n\n```cpp\n          const auto words_end (v.find_last_not_of(\" tn\"));\n          if (words_end != string_view::npos) {\n              v.remove_suffix(v.size() - words_end - 1);\n          }\n```\n\n5.  现在我们可以打印字符串视图及其长度:\n\n```cpp\n          cout << \"length: \" << v.length()\n               << \" [\" << v << \"]n\";\n      }\n```\n\n6.  在我们的主函数中，我们通过向新的`print`函数提供完全不同的参数类型来玩转它。首先，我们从`argv`指针给它一个运行时`char*`字符串。在运行时，它包含我们的可执行文件的文件名。然后，我们给它一个空的`string_view`实例。然后，我们用一个 C 风格的静态字符串和一个`\"\"sv`文字来填充它，这就动态地为我们构建了一个`string_view`。最后，我们给它一个`std::string`。好的一点是，这些参数都没有为了调用`print`函数而被修改或复制。不会发生堆分配。对于许多和/或大型字符串，这非常有效:\n\n```cpp\n      int main(int argc, char *argv[])\n      {\n          print(argv[0]);\n          print({});\n          print(\"a const char * array\");\n          print(\"an std::string_view literal\"sv);\n          print(\"an std::string instance\"s);\n```\n\n7.  我们没有测试空白删除功能。所以，让我们给它一个有很多前导和尾随空格的字符串:\n\n```cpp\n          print(\" tn foobar n t \");\n```\n\n8.  另一个很酷的特点是，字符串`string_view`让我们可以访问不一定是*零终止*。如果我们构造一个没有尾随零的字符串，如`\"abc\"`，则`print`函数仍然可以安全地处理它，因为`string_view`还携带它所指向的字符串的大小:\n\n```cpp\n          char cstr[] {'a', 'b', 'c'};\n          print(string_view(cstr, sizeof(cstr)));\n      }\n```\n\n9.  编译并运行程序会产生以下输出。所有的字符串都被正确处理。我们填充了大量前导和尾随空白的字符串被正确过滤，没有零终止的`abc`字符串也被正确打印，没有任何缓冲区溢出:\n\n```cpp\n      $ ./string_view \n      length: 17 [./string_view]\n      length: 0 []\n      length: 20 [a const char * array]\n      length: 27 [an std::string_view literal]\n      length: 23 [an std::string instance]\n      length: 6 [foobar]\n      length: 3 [abc]\n```\n\n# 它是如何工作的...\n\n我们刚刚看到，我们可以调用一个接受`string_view`参数的函数，该参数基本上是字符串形式的，因为它以连续的方式存储字符。*在我们的任何`print`通话中，都没有复制基础字符串*。\n\n有趣的是，在我们的`print(argv[0])`调用中，字符串视图自动确定了字符串长度，因为按照惯例，这是一个以零结尾的字符串。反过来，我们不能假设通过计数项目的数量直到到达零终止符，可以确定`string_view`实例的数据长度。正因为如此，我们必须始终小心使用`string_view::data()`将指针指向字符串视图数据的位置。通常的字符串函数大多假设零终止，因此，对于指向字符串视图有效负载的原始指针，缓冲区溢出会非常严重。使用已经期望字符串视图的接口总是更好。\n\n除此之外，我们已经从`std::string`获得了很多我们知道的豪华界面。\n\nUse `std::string_view` for passing strings or substrings where you want to avoid copies or heap allocations, without losing the comfort of string classes. But be aware of the fact that `std::string_view` drops the assumption that strings are zero terminated.\n\n# 从用户输入中读取值\n\n这本书里的很多食谱从一个输入源读取值，比如标准输入或者一个文件，然后用它做一些事情。这一次，我们只专注于阅读，并了解更多关于错误处理的知识，如果从流中阅读一些东西并没有很好地进行，并且我们需要处理它，而不是终止整个程序，这一点就变得很重要。\n\n在这个食谱中，我们将只从用户输入中读取，但是一旦我们知道如何做到这一点，我们也知道如何从任何其他流中读取。用户输入通过`std::cin`读取，这本质上是一个输入流对象，例如`ifstream`和`istringstream`的实例。\n\n# 怎么做...\n\n在本节中，我们将把用户输入读入不同的变量，并了解如何处理错误，以及如何将输入进行更复杂的标记化，使之成为有用的块:\n\n1.  这次我们只需要`iostream`了。因此，让我们包含这个单独的头，并声明我们默认使用`std`命名空间:\n\n```cpp\n      #include <iostream>\n\n      using namespace std;\n```\n\n2.  让我们首先提示用户输入两个数字。我们将把它们解析成一个`int`和一个`double`变量。用户可以用空格将它们分开。例如，`1 2.3`是一个有效的输入:\n\n```cpp\n      int main()\n      {\n          cout << \"Please Enter two numbers:n> \";\n          int x;\n          double y;\n```\n\n3.  解析和错误检查在我们的`if`分支的条件部分同时完成。只有当这两个数字都能被解析时，它们对我们才有意义，我们才会打印它们:\n\n```cpp\n          if (cin >> x >> y) {\n              cout << \"You entered: \" << x \n                   << \" and \" << y << 'n';\n```\n\n4.  如果解析由于任何原因没有成功，我们会告诉用户解析进行得不顺利。`cin`流对象现在处于*失败状态*，并且在我们再次清除失败状态之前不会给我们其他输入。为了以后能够解析一个新的输入，我们调用`cin.clear()`并丢弃到目前为止收到的所有输入。删除是通过`cin.ignore`完成的，在这里我们指定删除最大数量的字符，直到我们最终看到一个换行符，这个换行符也被删除。之后的一切又是有趣的输入:\n\n```cpp\n          } else {\n              cout << \"Oh no, that did not go well!n\";\n              cin.clear();\n              cin.ignore(\n                  std::numeric_limits<std::streamsize>::max(),\n                  'n');\n          }\n```\n\n5.  现在让我们请求一些其他的输入。我们让用户输入姓名。由于名称可以由多个由空格分隔的单词组成，空格字符不再是一个好的分隔符。因此，我们使用`std::getline`，它接受一个流对象，如`cin`，一个它将输入复制到其中的字符串引用，以及一个分隔字符。让我们选择逗号(`,`)作为分隔字符。通过不仅仅单独使用`cin`和使用`cin >> ws`作为`getline`的流参数，我们可以让`cin`去掉任何名称前的任何前导空格。在每个循环步骤中，我们打印当前名称，但是如果名称为空，我们将退出循环:\n\n```cpp\n          cout << \"now please enter some \"\n                  \"comma-separated names:n> \";\n\n          for (string s; getline(cin >> ws, s, ',');) {\n              if (s.empty()) { break; }\n              cout << \"name: \"\" << s << \"\"n\";\n          }\n      }\n```\n\n6.  编译和运行程序会得到下面的输出，其中我们假设只输入有效的输入。数字是`\"1 2\"`，解析正确，然后我们输入一些名字，然后也正确列出。以两个连续逗号形式输入的空名称退出循环:\n\n```cpp\n      $ ./strings_from_user_input \n      Please Enter two numbers:\n      > 1 2\n      You entered: 1 and 2\n      now please enter some comma-separated names:\n      > john doe,  ellen ripley,       alice,    chuck norris,,\n      name: \"john doe\"\n      name: \"ellen ripley\"\n      name: \"alice\"\n      name: \"chuck norris\"\n```\n\n7.  当再次运行程序时，在开始输入错误的数字时，我们看到程序正确地接受了另一个分支，丢弃了错误的输入，并正确地继续使用名称侦听。摆弄一下`cin.clear()`和`cin.ignore(...)`线，看看这是如何篡改姓名读码的:\n\n```cpp\n      $ ./strings_from_user_input\n      Please Enter two numbers:\n      > a b\n      Oh no, that did not go well!\n      now please enter some comma-separated names:\n      > bud spencer, terence hill,,\n      name: \"bud spencer\"\n      name: \"terence hill\"\n```\n\n# 它是如何工作的...\n\n我们在这一部分做了一些复杂的输入检索。首先值得注意的是，我们总是同时进行检索和错误检查。\n\n表达式`cin >> x`的结果再次引用了`cin`。这样，我们就可以写出`cin >> x >> y >> z >> ...`。同时，可以在`if`条件等布尔上下文中使用，将其转换为布尔值。布尔值告诉我们最后一次读取是否成功。这就是为什么我们能够写`if (cin >> x >> y) {...}`。\n\n例如，如果我们试图读取一个整数，但是输入包含`\"foobar\"`作为下一个标记，那么将它解析成整数是不可能的，并且流对象进入*失败状态*。这只是解析尝试的关键，而不是整个程序。可以先重置它，然后再尝试其他东西。在我们的食谱程序中，我们试图在读取两个数字的尝试可能失败后读取一个名字列表。在尝试读取这些数字失败的情况下，我们使用`cin.clear()`将`cin`恢复到工作状态。但是，它的内部光标仍然在我们键入的内容上，而不是数字上。为了删除这个旧的输入并清除名称输入的管道，我们使用了非常长的表达式`cin.ignore(std::numeric_limits<std::streamsize>::max(), 'n');`。这对于清除缓冲区中的任何内容都是必要的，因为当我们向用户请求名称列表时，我们希望从一个真正新鲜的缓冲区开始。\n\n以下循环一开始可能看起来也很奇怪:\n\n```cpp\nfor (string s; getline(cin >> ws, s, ',');) { ... }\n```\n\n在`for`循环的条件部分，我们使用`getline`。`getline`函数接受输入流对象、作为输出参数的字符串引用和分隔符。默认情况下，分隔符是换行符。在这里，我们将其定义为逗号(`,`)字符，因此列表中的所有名称，如`\"john, carl, frank\"`，都是单独读取的。\n\n目前为止，一切顺利。但是提供`cin >> ws`功能作为流对象意味着什么呢？这使得`cin`首先刷新所有空格，这些空格位于下一个非空格字符之前和最后一个逗号之后。回顾`\"john, carl, frank\"`的例子，我们会得到子字符串`\"john\"`、`\" carl\"`和`\" frank\"`，而不使用`ws`。注意`carl`和`frank`不必要的前导空格字符？由于我们对输入流的`ws`预处理，这些实际上消失了。\n\n# 计算文件中的所有单词\n\n假设我们读了一个文本文件，我们想计算文本中的字数。我们定义一个单词是空白字符之间的字符范围。我们怎么做？\n\n例如，我们可以计算空格的数量，因为单词之间必须有空格。在句子`\"John has a funny little dog.\"`中，我们有五个空格字符，所以我们可以说有六个单词。\n\n如果我们有一个有空白噪音的句子，比如`\" John has t anfunny little dog .\"`？这个字符串中有太多不必要的空格，甚至不仅仅是空格。从这本书的其他食谱中，我们已经学会了如何去除这些多余的空白。因此，我们可以先将字符串预处理成正常的句子形式，然后应用计算空格字符的策略。是的，这是可行的，但是有一个更简单的方法。为什么我们不应该使用 STL 已经提供给我们的东西呢？\n\n除了为这个问题找到一个优雅的解决方案之外，我们将让用户选择是从标准输入还是文本文件中计算单词。\n\n# 怎么做...\n\n在本节中，我们将编写一个单行函数来计算输入缓冲区的字数，并让用户选择输入缓冲区的读取位置:\n\n1.  让我们首先包含所有必要的头，并声明我们使用`std`命名空间:\n\n```cpp\n      #include <iostream>\n      #include <fstream>\n      #include <string>\n      #include <algorithm>\n      #include <iterator>      \n\n      using namespace std;\n```\n\n2.  我们的`wordcount`函数接受输入流，例如`cin`。它创建了一个`std::input_iterator`迭代器，标记出流中的字符串，然后将它们提供给`std::distance`。`distance`参数接受两个迭代器作为参数，并试图确定从一个迭代器位置到另一个位置需要多少递增步骤。对于*随机访问*迭代器，这很简单，因为它们实现了数学差分运算(`operator-`)。这样的迭代器可以像指针一样相互相减。然而，一个`istream_iterator`是一个*向前*迭代器，并且必须被推进，直到它等于结束迭代器。最终，需要的步骤数就是单词数:\n\n```cpp\n      template <typename T>\n      size_t wordcount(T &is)\n      {\n          return distance(istream_iterator<string>{is}, {});\n      }\n```\n\n3.  在我们的主功能中，我们让用户选择输入流是`std::cin`还是输入文件:\n\n```cpp\n      int main(int argc, char **argv)\n      {\n          size_t wc;\n```\n\n4.  如果用户在 shell 中与一个文件名(如`$ ./count_all_words some_textfile.txt`)一起启动程序，那么我们从`argv`命令行参数数组中获取该文件名并打开它，以便将新的输入文件流送入`wordcount`:\n\n```cpp\n          if (argc == 2) {\n              ifstream ifs {argv[1]};\n              wc = wordcount(ifs);\n```\n\n5.  如果用户在没有任何参数的情况下启动程序，我们假设输入来自标准输入:\n\n```cpp\n          } else {\n              wc = wordcount(cin);\n          }\n```\n\n6.  已经这样了，所以我们只打印变量`wc`中保存的字数:\n\n```cpp\n          cout << \"There are \" << wc << \" wordsn\";\n      };\n```\n\n7.  让我们编译并运行这个程序。首先，我们从没有任何文件参数的标准输入中馈送程序。我们可以通过管道发送带有一些单词的回声呼叫，或者启动程序，从键盘输入一些单词。在后一种情况下，我们可以通过按 *Ctrl* + *D* 来停止输入。这就是一些单词在程序中的呼应方式:\n\n```cpp\n      $ echo \"foo bar baz\" | ./count_all_words \n      There are 3 words\n```\n\n8.  当以源代码文件作为输入启动程序时，它将计算它包含多少个单词:\n\n```cpp\n      $ ./count_all_words count_all_words.cpp\n      There are 61 words\n```\n\n# 它是如何工作的...\n\n没什么好说的了；因为这个程序非常短，所以在实现它时已经解释了大部分内容。我们可以详细说明的一件事是，我们以完全可互换的方式使用了`std::cin`和`std::ifstream`实例。`cin`为`std::istream`型，`std::ifstream`继承自`std::istream`。看看本章开头的类继承图。这样，即使在运行时，它们也是完全可互换的。\n\nKeep your code modular by using stream abstractions. This helps decouple source code parts and makes your code easy to test because you can just inject any other matching type of stream.\n\n# 用输入输出流操纵器格式化您的输出\n\n在许多情况下，仅仅打印出字符串和数字是不够的。有时，数字需要打印为十进制数，有时是十六进制数，有时甚至是八进制数。有时候我们想在十六进制数字前面看到一个`\"0x\"`前缀，有时候不是。\n\n打印浮点数时，我们可能还想对很多事情产生影响。十进制值应该总是以相同的精度打印吗？它们应该被打印出来吗？或者，我们想要一个科学符号？\n\n除了科学呈现和十六进制、八进制等，我们还希望以整齐的形式呈现用户输出。例如，一些输出可以安排在表格中，以使其尽可能易读。\n\n当然，对于输出流，所有这些都是可能的。当*解析输入流中的*值时，其中一些设置也很重要。在这个食谱中，我们会通过玩弄这些所谓的**输入输出操纵器**来获得一种感觉。有时候，它们看起来很棘手，所以我们也会进入一些细节。\n\n# 怎么做...\n\n在本节中，我们将打印格式设置变化很大的数字，以便熟悉输入/输出操纵器:\n\n1.  首先，我们包括所有必要的头，并声明我们默认使用`std`命名空间:\n\n```cpp\n      #include <iostream>\n      #include <iomanip>\n      #include <locale>      \n\n      using namespace std;\n```\n\n2.  接下来，我们定义一个助手函数，打印一个不同样式的整数值。它接受一个填充宽度和一个填充字符，默认设置为空格`' '`:\n\n```cpp\n      void print_aligned_demo(int val, \n                              size_t width, \n                              char fill_char = ' ')\n      {\n```\n\n3.  通过`setw`，我们可以设置打印一个数字输出的最小字符数。例如，如果我们打印宽度为`6`的`123`，我们会得到`\" 123\"`，或者`\"123 \"`。我们可以通过`std::left`、`std::right`和`std::internal`来控制填充发生在哪边。当以十进制形式打印数字时，`internal`看起来与`right`相同。但是如果我们打印数值`0x1`，比如宽度为`6`，宽度为`internal`，则得到`\"0x 6\"`。`setfill`操纵器定义将用于填充的字符。我们将尝试不同的风格:\n\n```cpp\n          cout << \"================n\";\n          cout << setfill(fill_char);\n          cout << left << setw(width) << val << 'n';\n          cout << right << setw(width) << val << 'n';\n          cout << internal << setw(width) << val << 'n';\n      }\n```\n\n4.  在主函数中，我们开始使用刚刚实现的函数。首先，我们打印值`12345`，宽度为`15`。我们这样做了两次，但是第二次，我们使用了`'_'`字符作为填充:\n\n```cpp\n      int main()\n      {\n          print_aligned_demo(123456, 15);\n          print_aligned_demo(123456, 15, '_');\n```\n\n5.  之后，我们以与之前相同的宽度打印值`0x123abc`。但是，在此之前，我们应用`std::hex`和`std::showbase`来告诉输出流对象`cout`它应该以十六进制格式打印数字，并且应该在它们前面加上`\"0x\"`，这样很明显它们将被解释为十六进制:\n\n```cpp\n          cout << hex << showbase;\n          print_aligned_demo(0x123abc, 15);\n```\n\n6.  我们可以用`oct`做同样的事情，它告诉`cout`使用八进制打印数字。`showbase`仍然有效，因此`0`将被添加到每个打印的号码前:\n\n```cpp\n          cout << oct;\n          print_aligned_demo(0123456, 15);\n```\n\n7.  有了`hex`和`uppercase`，我们得到了`\"0x\"`打印的大写的`'x'`。`'0x123abc'`中的`'abc'`也是大写的:\n\n```cpp\n          cout << \"A hex number with upper case letters: \"\n               << hex << uppercase << 0x123abc << 'n';\n```\n\n8.  如果我们想再次以十进制格式打印`100`，我们必须记住我们之前已经将流切换到了`hex`。通过使用`dec`，我们可以将其恢复正常:\n\n```cpp\n          cout << \"A number: \" << 100 << 'n';\n          cout << dec;\n\n          cout << \"Oops. now in decimal again: \" << 100 << 'n';\n```\n\n9.  我们还可以配置布尔值的打印方式。默认情况下，`true`打印为`1`，`false`打印为`0`。借助`boolalpha`，我们可以将其设置为文本表示:\n\n```cpp\n          cout << \"true/false values: \" \n               << true << \", \" << false << 'n';\n          cout << boolalpha\n               << \"true/false values: \"\n               << true << \", \" << false << 'n';\n```\n\n10.  让我们看看`float`和`double`类型的浮点变量。如果我们打印一个数字，比如`12.3`，当然会打印为`12.3`。如果我们有一个像`12.0`这样的数字，那么输出流将会丢失小数点，我们可以用`showpoint`来改变它。使用此选项，小数点始终显示:\n\n```cpp\n          cout << \"doubles: \"\n               << 12.3 << \", \"\n               << 12.0 << \", \"\n               << showpoint << 12.0 << 'n';\n```\n\n11.  浮点数的表示可以是科学的，也可以是固定的。`scientific`表示数字是*归一化*成这样的形式，第一位数字是小数点前唯一的数字，然后打印指数，需要把数字乘回实际大小。例如，值`300.0`将被打印为`\"3.0E2\"`，因为`300`等于`3.0 * 10^2`。`fixed`恢复到正常的小数点表示法:\n\n```cpp\n          cout << \"scientific double: \" << scientific \n               << 123000000000.123 << 'n';\n          cout << \"fixed      double: \" << fixed \n               << 123000000000.123 << 'n';\n```\n\n12.  除了符号，我们还可以决定浮点数的打印精度。让我们创建一个非常小的值，并在小数点后打印 10 位数字，在小数点后仅打印一位数字:\n\n```cpp\n          cout << \"Very precise double: \" \n               << setprecision(10) << 0.0000000001 << 'n';\n          cout << \"Less precise double: \" \n               << setprecision(1)  << 0.0000000001 << 'n';\n      }\n```\n\n13.  编译和运行程序会产生以下冗长的输出。前四个输出块来自打印助手函数，该函数篡改了`setw`和`left` / `right` / `internal`修饰符。之后，我们使用了基本表示、布尔表示和浮点格式的大小写。玩这些游戏来熟悉它们是个好主意:\n\n```cpp\n      $ ./formatting \n      ================\n      123456         \n               123456\n               123456\n      ================\n      123456_________\n      _________123456\n      _________123456\n      ================\n      0x123abc       \n             0x123abc\n      0x       123abc\n      ================\n      0123456        \n              0123456\n              0123456\n      A hex number with upper case letters: 0X123ABC\n      A number: 0X64\n      Oops. now in decimal again: 100\n      true/false values: 1, 0\n      true/false values: true, false\n      doubles: 12.3, 12, 12.0000\n      scientific double: 1.230000E+11\n      fixed      double: 123000000000.123001\n      Very precise double: 0.0000000001\n      Less precise double: 0.0\n```\n\n# 它是如何工作的...\n\n所有这些，有时相当长，`<< foo << bar`流表达式真的很混乱，如果读者不清楚它们每一个做什么的话。因此，让我们看一下现有格式修饰符的表格。它们都将被置于`input_stream >> modifier`或`output_stream << modifier`表达式中，然后影响以下输入或输出:\n\n| **符号** | **表示** |\n| `setprecision(int n)` | 打印或解析浮点值时设置精度参数。 |\n| `showpoint` / `noshowpoint` | 启用或禁用浮点数的小数点打印，即使它们没有任何小数位。 |\n| `fixed` / `scientific` / `hexfloat` / `defaultfloat` | 数字可以用固定样式(最直观的一种)或科学样式打印。`fixed`和`scientific`代表这些模式。`hexfloat`激活两种模式，以十六进制浮点表示法格式化浮点数。`defaultfloat`停用两种模式。 |\n| `showpos` / `noshowpos` | 启用或禁用打印正浮点值的`'+'`前缀。 |\n| `setw(int n)` | 准确阅读或书写`n`字符。读取时，这会截断输入。打印时，如果输出短于`n`字符，则应用填充。 |\n| `setfill(char c)` | 应用填充时(见`setw`)，用字符值`c`填充输出。默认为空格(`' '`)。 |\n| `internal` / `left` / `right` | `left`和`right`控制固定宽度打印的填充位置(参见`setw`)。`internal`将填充字符放在整数及其负号、十六进制前缀和十六进制打印值或货币单位和值之间的中间。 |\n| `dec` / `hex` / `oct` | 可以以十进制、十六进制和八进制为基础系统打印和解析整数值。 |\n| `setbase(int n)` | 这是`dec` / `hex` / `oct`的数字同义函数，如果与`10` / `16` / `8`值一起使用，它们是等价的。其他值将基本选项重置为`0`，这将再次导致十进制打印，或基于输入的前缀进行解析。 |\n| `quoted(string)` | 打印带引号的字符串或从带引号的输入中进行分析，然后删除引号。`string`可以是字符串类实例，也可以是 C 风格的字符数组。 |\n| `boolalpha` / `noboolalpha` | 将布尔值打印或解析为/来自字母表示法，而不是`1` / `0`字符串。 |\n| `showbase` / `noshowbase` | 打印或解析数字时启用或禁用基本前缀。对于`hex`，这是`0x`；对于`octal`来说是`0`。 |\n| `uppercase` / `nouppercase` | 打印浮点和十六进制值时，启用或禁用大写或字母字符。 |\n\n熟悉它们的最好方法是稍微研究一下它们的种类，然后和它们一起玩。\n\n然而，当玩它们的时候，我们可能已经注意到这些修改器中的大多数看起来都是*粘性的*，而其中的一些并不是这样。粘性意味着一旦应用，它们似乎会永远影响输入/输出*，直到它们再次复位。这张表中唯一不粘的是`setw`和`quoted`。它们只影响输入/输出中的下一项。这一点很重要，因为如果我们打印一些带有特定格式的输出，我们应该在之后整理我们的流对象格式设置，因为来自不相关代码的下一个输出可能看起来很疯狂。同样的情况也适用于输入解析，在这种情况下，输入/输出操纵器选项可能会出错。*\n\n *我们实际上并没有使用其中的任何一个，因为它们与格式化没有任何关系，但是出于完整性的原因，我们还应该看看其他一些流状态操纵器:\n\n| **符号** | **表示** |\n| `skipws` / `noskipws` | 启用或禁用输入流跳过空白的功能 |\n| `unitbuf` / `nounitbuf` | 在任何输出操作后启用或禁用立即输出缓冲区刷新 |\n| `ws` | 可用于输入流，跳过流头的任何空白 |\n| `ends` | 将字符串终止`''`字符写入流中 |\n| `flush` | 立即清除输出缓冲区中的任何内容 |\n| `endl` | 将`'n'`字符插入输出流并刷新输出 |\n\n从这些来看，只有`skipws` / `noskipws`和`unitbuf` / `nounitbuf`出现粘性。\n\n# 从文件输入初始化复杂对象\n\n读入单个整数、浮点和单词串真的很容易，因为输入流对象的`>>`运算符对所有这些类型都是重载的，输入流方便地为我们丢弃所有中间的空白。\n\n但是，如果我们想要从输入流中读取一个更复杂的结构，并且如果我们需要读取包含多个单词的字符串(由于空白跳过，它们通常会被分块为单个单词)，该怎么办？\n\n对于任何类型，都有可能提供另一个输入流`operator>>`重载，我们来看看怎么做。\n\n# 怎么做...\n\n在本节中，我们将定义一个自定义数据结构，并提供从输入流中读取这些项目作为标准输入的工具:\n\n1.  我们需要首先包含一些头，为了方便起见，我们声明默认使用`std`命名空间:\n\n```cpp\n      #include <iostream>\n      #include <iomanip>\n      #include <string>\n      #include <algorithm>\n      #include <iterator>\n      #include <vector>      \n\n      using namespace std;\n```\n\n2.  作为一个复杂对象的例子，我们定义一个`city`结构。城市应有名称、人口数量和地理坐标:\n\n```cpp\n      struct city {\n          string name;\n          size_t population;\n          double latitude;\n          double longitude;\n      };\n```\n\n3.  为了能够从串行输入流中读取这样的城市，我们需要重载流功能`operator>>`。在这个运算符中，我们首先跳过所有带有`ws`的前导空格，因为我们不希望空格污染城市名称。然后，我们阅读一整行文本输入。这意味着在输入文件中，有一整行文本只包含一个城市对象的名称。然后，在一个换行符之后，会出现一个由空格分隔的数字列表，指示人口、地理纬度和经度:\n\n```cpp\n      istream& operator>>(istream &is, city &c)\n      {\n          is >> ws;\n          getline(is, c.name);\n          is >> c.population \n             >> c.latitude \n             >> c.longitude;\n          return is;\n      }\n```\n\n4.  在我们的主要功能中，我们创建了一个可以容纳一系列城市项目的向量。我们用`std::copy`填充。复制调用的输入是`istream_iterator`范围。通过给它`city`结构类型作为模板参数，它将使用我们刚刚实现的`operator>>`函数重载:\n\n```cpp\n      int main()\n      {\n          vector<city> l;\n\n          copy(istream_iterator<city>{cin}, {}, \n               back_inserter(l));\n```\n\n5.  为了查看我们的城市解析是否正确，我们打印了我们在列表中得到的内容。输入/输出格式，`left << setw(15) <<`，导致城市名被空格填充，因此我们得到可读性很好的输出:\n\n```cpp\n          for (const auto &[name, pop, lat, lon] : l) {\n              cout << left << setw(15) << name\n                   << \" population=\" << pop\n                   << \" lat=\" << lat\n                   << \" lon=\" << lon << 'n';\n          }\n      }\n```\n\n6.  我们将向程序提供的文本文件如下所示。有四个示例城市有其人口数量和地理坐标:\n\n```cpp\n      Braunschweig\n      250000 52.268874 10.526770\n      Berlin\n      4000000 52.520007 13.404954\n      New York City\n      8406000 40.712784 -74.005941\n      Mexico City\n      8851000 19.432608 -99.133208\n```\n\n7.  编译并运行程序会产生以下输出，这是我们所期望的。尝试篡改输入文件，在城市名称前添加一些不必要的空白，以便查看它是如何被过滤掉的:\n\n```cpp\n      $ cat cities.txt  | ./initialize_complex_objects\n      Braunschweig    population=250000 lat=52.2689 lon=10.5268\n      Berlin          population=4000000 lat=52.52 lon=13.405\n      New York City   population=8406000 lat=40.7128 lon=-74.0059\n      Mexico City     population=8851000 lat=19.4326 lon=-99.1332\n```\n\n# 它是如何工作的...\n\n这又是一个简单的食谱。我们唯一做的就是创建一个新的结构`city`，然后我们重载了这个类型的`std::istream`迭代器`operator>>`，就这样。这已经使我们能够使用`istream_iterator<city>`从标准输入中反序列化城市项目。\n\n关于错误检查，可能还有一个悬而未决的问题。为此，我们再来看看`operator>>`的实现:\n\n```cpp\n      istream& operator>>(istream &is, city &c)\n      {\n          is >> ws;\n          getline(is, c.name);\n          is >> c.population >> c.latitude >> c.longitude;\n          return is;\n      }\n```\n\n我们正在阅读许多不同的东西。如果其中一个失败了，下一个没有，会发生什么？这是否意味着我们可能正在读取令牌流中具有错误“偏移量”的所有以下项目？不，这不可能。一旦无法从输入流中解析这些项中的一项，输入流对象就会进入错误状态，并拒绝进一步解析任何内容。这意味着，如果例如`c.population`或`c.latitude`不能被解析，剩余的`>>`操作数只是“通过”，我们将这个运算符函数范围留给一个半反序列化的城市对象。\n\n在调用者方面，当我们写`if (input_stream >> city_object)`时，我们会收到这个通知。当用作条件表达式时，这样的流表达式被隐式转换为布尔值。如果输入流对象处于错误状态，则返回`false`。知道我们可以重置流并做任何合适的事情。\n\n在这个食谱中，我们没有自己编写这样的`if`条件句，因为我们让`std::istream_iterator<city>`进行反序列化。这个迭代器类的`operator++ `实现也在解析时检查错误。如果出现任何错误，它将拒绝进一步迭代。在这种状态下，当与结束迭代器比较时，它返回`true`，这使得`copy`算法终止。这样，我们就安全了。\n\n# 从 std::istream 迭代器填充容器\n\n在上一个食谱中，我们学习了如何从输入流中组装复合数据结构，然后用它们填充列表或向量。\n\n这一次，我们通过从标准输入中填充一个`std::map`来使它变得更难一点。这里的问题是，我们不能仅仅用值填充单个结构，并将其推回到像列表或向量这样的线性容器中，因为`map`将其有效负载分为键和值部分。然而，正如我们将看到的，这并不完全不同。\n\n在研究了这个方法之后，我们会对将复杂的数据结构从字符流序列化和反序列化到字符流感到很舒服。\n\n# 怎么做...\n\n我们将定义另一个结构，就像上一个配方一样，但这次我们将把它填充到一个映射中，这使它变得更加复杂，因为这个容器从键映射到值，而不是只保存列表中的所有值:\n\n1.  首先，我们包括所有需要的头，并声明我们默认使用`std`命名空间:\n\n```cpp\n      #include <iostream>\n      #include <iomanip>\n      #include <map>\n      #include <iterator>\n      #include <algorithm>\n      #include <numeric>      \n\n      using namespace std;\n```\n\n2.  我们想维护一个小小的互联网迷因数据库。假设一个模因有一个名字，一个描述，以及它诞生或发明的年份。我们将它们保存在`std::map`中，其中名称是键，其他信息作为与键相关联的值聚集在一个结构中:\n\n```cpp\n      struct meme {\n          string description;\n          size_t year;\n      };\n```\n\n3.  我们先忽略这个键，只为`struct meme`实现一个流`operator>>`函数重载。我们假设描述用引号括起来，后面是年份。这看起来像文本文件中的`\"some description\" 2017`。由于描述被引号包围，它可以包含空白，因为我们知道引号之间的一切都属于它。通过使用`is >> quoted(m.description)`阅读，引号会自动用作分隔符，并在之后被删除。这很方便。就在那之后，我们读了年号:\n\n```cpp\n      istream& operator>>(istream &is, meme &m) {\n          return is >> quoted(m.description) >> m.year;\n      }\n```\n\n4.  好了，现在我们把迷因的名字作为地图的关键考虑进去。为了在地图中插入一个模因，我们需要一个`std::pair<key_type, value_type>`实例。`key_type`当然是`string`，而`value_type`就是`meme`。名称也允许包含空格，所以我们使用相同的`quoted`包装作为描述。`p.first`是名称，`p.second`是与之关联的整个`meme`结构。它将被输入到我们刚刚实现的另一个`operator>>`实现中:\n\n```cpp\n      istream& operator >>(istream &is, \n                           pair<string, meme> &p) {\n          return is >> quoted(p.first) >> p.second;\n      }\n```\n\n5.  好了，就这样。让我们编写一个主函数，它实例化一个映射，并填充该映射。因为我们重载了流函数`operator>>`，`istream_iterator`可以直接处理这个类型。我们让它从标准输入中反序列化我们的模因项，并使用一个`inserter`迭代器将它们输入到映射中:\n\n```cpp\n      int main()\n      {\n          map<string, meme> m;\n\n          copy(istream_iterator<pair<string, meme>>{cin},\n               {},\n               inserter(m, end(m)));\n```\n\n6.  在我们打印我们所拥有的之前，让我们首先找出地图中最长的*模因名称是什么。我们用`std::accumulate`来表示这个。它获得一个初始值`0u` ( `u`表示无符号)，并将逐元素访问地图，以便*将它们合并在一起。就`accumulate`而言，合并通常意味着*增加*。在我们的例子中，我们不需要任何数字*和*，只需要最大的字符串长度。为了得到这个结果，我们提供了`accumulate`一个助手函数`max_func`，它获取当前最大大小变量(因为字符串长度是无符号的，所以必须是`unsigned`)，并将其与当前项目的 meme 名称字符串的长度进行比较，以便获取两个值的最大值。每个元素都会发生这种情况。`accumulate`函数的最终返回值是最大模因名称长度:**\n\n```cpp\n          auto max_func ([](size_t old_max, \n                            const auto &b) {\n              return max(old_max, b.first.length());\n          });\n          size_t width {accumulate(begin(m), end(m), \n                                   0u, max_func)};\n```\n\n7.  现在，让我们快速循环浏览地图并打印每个项目。我们使用`<< left << setw(width)`获得一个漂亮的类似表格的打印:\n\n```cpp\n          for (const auto &[meme_name, meme_desc] : m) {\n              const auto &[desc, year] = meme_desc;\n\n              cout << left << setw(width) << meme_name\n                   << \" : \" << desc\n                   << \", \" << year << 'n';\n          }\n      }\n```\n\n8.  就这样。我们需要一个小的互联网迷因数据库文件，所以让我们用一些例子来填充一个文本文件:\n\n```cpp\n      \"Doge\" \"Very Shiba Inu. so dog. much funny. wow.\" 2013\n      \"Pepe\" \"Anthropomorphic frog\" 2016\n      \"Gabe\" \"Musical dog on maximum borkdrive\" 2016\n      \"Honey Badger\" \"Crazy nastyass honey badger\" 2011\n      \"Dramatic Chipmunk\" \"Chipmunk with a very dramatic look\" 2007\n```\n\n9.  使用示例 meme 数据库编译和运行程序会产生以下输出:\n\n```cpp\n      $ cat memes.txt | ./filling_containers \n      Doge              : Very Shiba Inu. so dog. much funny. wow., 2013\n      Dramatic Chipmunk : Chipmunk with a very dramatic look, 2007\n      Gabe              : Musical dog on maximum borkdrive, 2016\n      Honey Badger      : Crazy nastyass honey badger, 2011\n      Pepe              : Anthropomorphic frog, 2016\n```\n\n# 它是如何工作的...\n\n这个食谱有三个特色菜。一个是我们没有从一个连续的字符流中填充一个法向量或者一个列表，而是像`std::map`这样一个更复杂的容器。另一个是我们使用了那些神奇的`quoted`流操纵器。最后一个是`accumulate`调用，它找出最大的键串大小。\n\n让我们从`map`部分开始。我们的`struct meme`只包含一个`description`字段和`year`。互联网模因的名称不属于这种结构，因为它被用作地图的关键。当我们在地图中插入一些东西时，我们可以为`std::pair`提供一个键类型和一个值类型。这就是我们所做的。我们首先为`struct meme`实现了流`operator>>`，然后为`pair<string, meme>`做了同样的事情。然后我们使用`istream_iterator<**pair<string, meme>**>{cin}`从标准输入中获取这些项目，并使用`inserter(m, end(m))`将其输入到地图中。\n\n当我们从流中反序列化 meme 项时，我们允许名称和描述包含空白。这很容易实现，尽管我们每个模因只用了一行，因为我们引用了 T2 的那些字段。行格式的示例如下:`\"Name with spaces\" \"Description with spaces\" 123`\n\n在处理输入和输出中的引用字符串时，`std::quoted`是一个很大的帮助。如果我们有一个字符串，`s`，使用`cout << quoted(s)`打印它会把它放在引号中。如果我们从流中反序列化一个字符串，例如，通过`cin >> quoted(s)`，它将读取下一个引号，用后面的内容填充字符串，并继续，直到它看到下一个引号，无论涉及多少空白。\n\n最后一个看起来奇怪的东西是我们累积呼叫中的`max_func`:\n\n```cpp\nauto max_func ([](size_t old_max, const auto &b) {\n    return max(old_max, b.first.length());\n});\n\nsize_t width {accumulate(begin(m), end(m), 0u, max_func)};\n```\n\n显然，`max_func`接受了一个`size_t`参数和另一个`auto-`类型的参数，结果是地图上的一个`pair`项目。起初这看起来很奇怪，因为大多数二进制约简函数接受相同类型的参数，然后通过某种操作将它们合并在一起，就像`std::plus`所做的那样。在这种情况下，真的不同，因为我们没有合并实际的`pair`项目。我们只从每一对中挑选键串长度，*去掉*剩下的，然后用`max`函数减少得到的`size_t`值。\n\n在累加调用中，`max_func`的第一次调用获得了我们最初作为左侧参数提供的`0u`值，以及对右侧第一对项目的引用。这会产生一个`max(0u, string_length)`返回值，它是*下一个*调用中的左参数，下一个配对项作为右参数，依此类推。\n\n# 带有标准::ostream 迭代器的通用打印\n\n用输出流打印任何东西都非常容易，因为 STL 已经为最基本的类型提供了许多有用的`operator<<`重载。这样，包含这种类型项目的数据结构可以很容易地使用`std::ostream_iterator`类打印出来，我们在本书中已经经常这样做了。\n\n在本食谱中，我们将集中讨论如何使用自定义类型来实现这一点，以及我们还可以做些什么来通过模板类型选择来操作打印，而不需要在调用方进行太多的代码。\n\n# 怎么做...\n\n我们将通过启用与新的自定义类的组合来玩`std::ostream_iterator`，并查看它的隐式转换功能，这可以帮助我们进行打印:\n\n1.  首先是包含文件，然后我们声明默认使用`std`命名空间:\n\n```cpp\n      #include <iostream>\n      #include <vector>\n      #include <iterator>\n      #include <unordered_map>\n      #include <algorithm>      \n\n      using namespace std;\n```\n\n2.  让我们实现一个转换函数，它将数字映射到字符串。对于值`1`，返回`\"one\"`，对于值`2`，返回`\"two\"`，依此类推:\n\n```cpp\n      string word_num(int i) {\n```\n\n3.  我们用需要的映射来填充哈希映射，以便以后访问它们:\n\n```cpp\n          unordered_map<int, string> m {\n              {1, \"one\"}, {2, \"two\"}, {3, \"three\"},\n              {4, \"four\"}, {5, \"five\"}, //...\n          };\n```\n\n4.  现在，我们可以用参数`i`来填充哈希映射的`find`函数，并返回它找到的内容。如果它没有找到任何东西，因为没有给定数字的翻译，我们返回字符串，`\"unknown\"`:\n\n```cpp\n          const auto match (m.find(i));\n          if (match == end(m)) { return \"unknown\"; }\n          return match->second;\n      };\n```\n\n5.  我们稍后将玩的另一个东西是`struct bork`。它只包含一个整数，也可以从整数隐式构造。它有一个`print`函数，该函数接受输出流引用并根据其成员整数`borks`的值重复打印`\"bork\"`字符串:\n\n```cpp\n      struct bork {\n          int borks;\n\n          bork(int i) : borks{i} {}\n\n          void print(ostream& os) const {\n              fill_n(ostream_iterator<string>{os, \" \"}, \n                     borks, \"bork!\"s);\n          }\n      };\n```\n\n6.  为了方便使用`bork::print`，我们为流对象重载`operator<<`，因此每当`bork`对象被流式传输到输出流时，它们会自动调用`bork::print`:\n\n```cpp\n      ostream& operator<<(ostream &os, const bork &b) {\n          b.print(os);\n          return os;\n      }\n```\n\n7.  现在我们终于可以开始实现实际的主功能了。我们最初只是用一些示例值创建一个向量:\n\n```cpp\n      int main()\n      {\n          const vector<int> v {1, 2, 3, 4, 5};\n```\n\n8.  类型为`ostream_iterator`的对象需要一个模板参数，该参数表示它们可以打印哪种类型的变量。如果写`ostream_iterator<**T**>`，以后会用`ostream& operator(ostream&, const **T**&)`打印。例如，这正是我们之前为`bork`类型实现的。这次我们只是打印整数，所以是`ostream_iterator<**int**>`。应使用`cout`进行打印，因此我们将其作为构造器参数提供。我们在一个循环中遍历向量，并将每一项`i`分配给解引用的输出迭代器。STL 算法也是这样使用流迭代器的:\n\n```cpp\n          ostream_iterator<int> oit {cout};\n\n          for (int i : v) { *oit = i; }\n          cout << 'n';\n```\n\n9.  我们刚刚生成的迭代器的输出很好，但是它打印的数字没有任何分隔符。如果我们想在所有打印的项目之间分隔一点空白，我们可以提供一个自定义的间隔字符串作为输出流迭代器的构造函数的第二个参数。这样打印的是`\"1, 2, 3, 4, 5, \"`而不是`\"12345\"`。不幸的是，我们不能轻易地告诉它在最后一个数字之后删除逗号空格字符串，因为迭代器在到达它之前不知道它的结尾:\n\n```cpp\n          ostream_iterator<int> oit_comma {cout, \", \"};\n\n          for (int i : v) { *oit_comma = i; }\n          cout << 'n';\n```\n\n10.  为输出流迭代器分配项目以便打印它们并不是一种错误的使用方式，但这并不是它们被发明的目的。想法是将它们与算法结合使用。最简单的就是`std::copy`。我们可以提供向量的开始和结束迭代器作为输入范围，输出流迭代器作为输出迭代器。它将打印矢量的所有数字。让我们用输出迭代器来做这件事，然后将输出与我们之前编写的循环进行比较:\n\n```cpp\n          copy(begin(v), end(v), oit);\n          cout << 'n';\n\n          copy(begin(v), end(v), oit_comma);\n          cout << 'n';\n```\n\n11.  还记得把数字映射成字符串的函数`word_num`吗，比如`1`到`\"one\"`、`2`到`\"two\"`等等？是的，我们也可以用它们来印刷。我们只需要使用一个输出流操作符，它是专门用于`string`的模板，因为我们不再打印整数了。我们使用`std::transform`而不是`std::copy`，因为它允许我们在将输入范围复制到输出范围之前，对输入范围中的每个项目应用转换函数:\n\n```cpp\n          transform(begin(v), end(v), \n                    ostream_iterator<string>{cout, \" \"}, \n                    word_num);\n          cout << 'n';\n```\n\n12.  本程序最后一行输出最后将`struct bork`投入使用。我们可以，但不能为`std::transform`提供转换功能。相反，我们可以只创建一个输出流迭代器，它专门处理`std::copy`调用中的`bork`类型。这导致`bork`实例隐式地*从输入范围整数创建。这会给我们一些有趣的输出:*\n\n```cpp\n          copy(begin(v), end(v), \n               ostream_iterator<bork>{cout, \"n\"});\n      }\n```\n\n13.  编译并运行程序会产生以下输出。前两行和后两行完全一样，这也是我们怀疑的。然后，我们得到一行漂亮的、写好的数字字符串，后面是许多`bork!`字符串。这些出现在多行中，因为我们使用了一个`\"n\"`分隔符字符串来代替空格:\n\n```cpp\n      $ ./ostream_printing \n      12345\n      1, 2, 3, 4, 5, \n      12345\n      1, 2, 3, 4, 5, \n      one two three four five \n      bork! \n      bork! bork! \n      bork! bork! bork! \n      bork! bork! bork! bork! \n      bork! bork! bork! bork! bork! \n```\n\n# 它是如何工作的...\n\n我们已经看到`std::ostream_iterator`实际上只是一个*语法黑客，*将打印的行为挤压到迭代器的形式和语法中。递增这样的迭代器没有任何作用。取消对它的引用只会返回一个代理对象，该对象的赋值操作符将其参数转发给输出流。\n\n专用于类型`T`(如在`ostream_iterator<T>`中)的输出流迭代器适用于所有提供了`ostream& operator<<(ostream&, const T&)`实现的类型。\n\n`ostream_iterator`总是试图通过模板参数调用其专用类型的`operator<<`。如果允许，它将尝试隐式转换类型。当我们迭代一系列`A`类型的项目，但我们将这些项目复制到`output_iterator<B>`实例时，如果`A`可以隐式转换为`B`，这将会起作用。我们对`struct bork`做了完全相同的事情:一个`bork`实例可以从整数值隐式转换。这就是为什么在用户外壳上抛出大量`\"bork!\"`字符串如此容易的原因。\n\n如果隐式转换不可能，我们可以自己做，使用`std::transform`，这是我们结合`word_num`函数做的。\n\nNote that it is, in general, *bad style* to allow implicit conversions for custom types because this is a common *source of bugs* that are really hard to find later. In our example use case, the implicit constructor is more useful than dangerous because the class is used for nothing else but printing.\n\n# 将输出重定向到特定代码段的文件\n\n`std::cout`提供了一种非常好的方式来打印我们想要的任何东西，无论何时，因为它使用简单，易于扩展，并且可以全局访问。即使我们想打印特殊的消息，比如错误消息，我们想从正常消息中隔离出来，我们也可以只使用`std::cerr`，它与`cout`相同，但是打印到标准的错误通道，而不是标准的输出通道。\n\n有时我们可能会有一些更复杂的伐木欲望。比方说，我们想要*将*一个函数的输出重定向到一个文件，或者我们想要*静音*一个函数的输出，而完全不改变函数。也许，这是一个我们无法访问源代码的库函数。也许，它从来没有被设计成写入文件，但我们希望它的输出在一个文件中。\n\n确实可以重定向流对象的输出。在这个食谱中，我们将看到如何以一种非常简单和优雅的方式做到这一点。\n\n# 怎么做...\n\n我们将实现一个助手类，解决重定向流的问题，并使用构造函数/析构函数魔法再次恢复重定向。然后我们看看如何使用它:\n\n1.  这次我们只需要输入、输出和文件流的头。我们将`std`命名空间声明为查找的默认命名空间:\n\n```cpp\n      #include <iostream>\n      #include <fstream>     \n\n      using namespace std;\n```\n\n2.  我们实现一个类，它保存一个文件流对象和一个指向流缓冲区的指针。作为流对象的`cout`有一个内部的流缓冲区，我们可以简单的交换。当我们交换它的时候，我们可以保存它以前的样子，所以我们可以*撤销*以后的任何改变。我们可以在 C++ 引用中查找它的类型，但是我们也可以使用`decltype`来找出`cout.rdbuf()`返回的类型。这通常不是所有情况下的好做法，但在这种情况下，它只是一种指针类型:\n\n```cpp\n      class redirect_cout_region\n      {\n          using buftype = decltype(cout.rdbuf());\n\n          ofstream ofs;\n          buftype  buf_backup;\n```\n\n3.  我们类的构造函数接受一个文件名字符串作为它的唯一参数。文件名用于初始化文件流成员`ofs`。初始化后，我们可以将其作为新的流缓冲区送入`cout`。接受新缓冲区的同一个函数也返回一个指向旧缓冲区的指针，所以我们可以保存它以便以后恢复它:\n\n```cpp\n      public:\n          explicit \n          redirect_cout_region (const string &filename)\n              : ofs{filename}, \n                buf_backup{cout.rdbuf(ofs.rdbuf())}\n          {}\n```\n\n4.  默认构造函数的作用与其他构造函数相同。不同的是，它不打开任何文件。将默认构造的文件流缓冲区送入`cout`流缓冲区会导致`cout`有点像*去激活*。它将只是*放下*它的输入我们给它打印。这在某些情况下也很有用:\n\n```cpp\n          redirect_cout_region()\n              : ofs{}, \n                buf_backup{cout.rdbuf(ofs.rdbuf())}\n          {}\n```\n\n5.  析构函数只是恢复我们的变化。当这个类的一个对象超出范围时，`cout`的流缓冲区又是原来的那个:\n\n```cpp\n          ~redirect_cout_region() { \n              cout.rdbuf(buf_backup); \n          }\n      };\n```\n\n6.  让我们模拟一个*输出重的*函数，这样我们以后就可以玩它了:\n\n```cpp\n      void my_output_heavy_function()\n      {\n          cout << \"some outputn\";\n          cout << \"this function does really heavy workn\";\n          cout << \"... and lots of it...n\";\n          // ...\n      }\n```\n\n7.  在主函数中，我们首先产生一些完全正常的输出:\n\n```cpp\n      int main()\n      {\n          cout << \"Readable from normal stdoutn\";\n```\n\n8.  现在我们打开另一个作用域，在这个作用域中我们做的第一件事是用一个文本文件参数实例化我们的新类。默认情况下，文件流以读写模式打开文件，因此它会为我们创建这个文件。尽管我们使用`cout`进行打印，但以下任何输出现在都将被重定向到该文件:\n\n```cpp\n          {\n              redirect_cout_region _ {\"output.txt\"};\n              cout << \"Only visible in output.txtn\";\n              my_output_heavy_function();\n          }\n```\n\n9.  离开作用域后，文件被关闭，输出再次重定向到正常的标准输出。现在让我们打开另一个范围，在其中实例化同一个类，但是通过它的默认构造函数。这样，以下打印的文本行在任何地方都不可见。它将被丢弃:\n\n```cpp\n          {\n              redirect_cout_region _;\n              cout << \"This output will \"\n                      \"completely vanishn\";\n          }\n```\n\n10.  在离开这个范围之后，我们的标准输出被恢复，最后一行文本输出将在 shell 中再次可读:\n\n```cpp\n          cout << \"Readable from normal stdout againn\";\n      }\n```\n\n11.  编译和运行程序会产生我们期望的输出。在 shell 中只能看到输出的第一行和最后一行:\n\n```cpp\n      $ ./log_regions \n      Readable from normal stdout\n      Readable from normal stdout again\n```\n\n12.  我们可以看到，一个新的文件`output.txt`已经被创建，并且包含了第一个作用域的输出。第二个作用域的输出完全消失:\n\n```cpp\n      $ cat output.txt \n      Only visible in output.txt\n      some output\n      this function does really heavy work\n      ... and lots of it...\n```\n\n# 它是如何工作的...\n\n每个流对象都有一个作为前端的内部缓冲区。这种缓冲剂是可交换的。如果我们有一个流对象`s`，并想将其缓冲区保存到一个变量`a`中，并安装一个新的缓冲区`b`，这看起来像下面这样:`a = s.rdbuf(b)`。恢复它可以简单地用`s.rdbuf(a)`来完成。\n\n这正是我们在这个食谱中所做的。另一件很酷的事情是，我们可以把那些帮手堆起来:\n\n```cpp\n{\n    cout << \"print to standard outputn\";\n\n    redirect_cout_region la {\"a.txt\"};\n    cout << \"print to a.txtn\";\n\n    redirect_cout_region lb {\"b.txt\"};\n    cout << \"print to b.txtn\";\n}\ncout << \"print to standard output againn\";\n```\n\n这是因为对象是按照与其构造相反的顺序被析构的。这种利用对象的构造和破坏之间的紧密耦合的模式背后的概念被称为**资源获取是初始化** ( **RAII** )。\n\n有一件非常重要的事情应该被提及——类成员变量的*初始化顺序*:\n\n```cpp\nclass redirect_cout_region {\n    using buftype = decltype(cout.rdbuf());\n\n    ofstream ofs;\n    buftype  buf_backup;\n\npublic:\n    explicit \n    redirect_cout_region(const string &filename)\n        : ofs{filename}, \n          buf_backup{cout.rdbuf(ofs.rdbuf())}\n    {}\n\n...\n```\n\n如我们所见，成员`buf_backup`是由依赖于`ofs`的表达式构成的。这显然意味着`ofs`需要在`buf_backup`之前初始化。有趣的是，这些成员的初始化顺序并不取决于初始化列表项的顺序。初始化顺序只取决于*成员声明*的顺序！\n\nIf one class member variable needs to be initialized after another member variable, they *must* also appear in that order in the class member declaration. The order of their appearance in the initializer list of the constructor is not critical.\n\n# 通过继承 std::char_traits 创建自定义字符串类\n\n`std::string`非常有用。然而，只要人们需要一个语义略有不同的字符串类来处理字符串，一些人就倾向于编写他们自己的*字符串类。*\n\n *编写自己的字符串类很少是个好主意，因为安全的字符串处理很难。幸运的是，`std::string`只是模板类`std::basic_string`的一个专门化的 typedef。这个类包含了所有复杂的内存处理内容，但是它没有对字符串的复制、比较等方式强加任何策略。这是通过接受包含特征类的模板参数导入到`basic_string`中的东西。\n\n在这个食谱中，我们将看到如何构建我们自己的特性类，以及如何在不重新实现任何东西的情况下创建自定义字符串。\n\n# 怎么做...\n\n我们将实现两个不同的自定义字符串类:`lc_string`和`ci_string`。第一个类从任何字符串输入中构造小写字符串。另一个类不转换任何字符串，但它可以进行不区分大小写的字符串比较:\n\n1.  让我们首先包含几个必要的头，然后声明我们默认使用`std`命名空间:\n\n```cpp\n      #include <iostream>\n      #include <algorithm>\n      #include <string>      \n\n      using namespace std;\n```\n\n2.  然后我们重新实现`std::tolower`函数，这个函数已经在`<cctype>`中定义了。已经存在的功能还好，但不是`constexpr`。然而，从 C++ 17 开始，一些`string`函数就成为了`constexpr`，我们希望能够在我们自己的自定义字符串特征类中利用这一点。该函数将大写字符映射为小写字符，其他字符保持不变:\n\n```cpp\n      static constexpr char tolow(char c) {\n          switch (c) {\n          case 'A'...'Z': return c - 'A' + 'a';\n          default:        return c;\n          }\n      }\n```\n\n3.  `std::basic_string`类接受三个模板参数:基础字符类型、字符特征类和分配器类型。我们只是在这一节中更改字符特征类，因为它定义了字符串的行为。为了只重新实现不同于普通字符串的内容，我们公开继承了标准的 traits 类:\n\n```cpp\n      class lc_traits : public char_traits<char> {\n      public:\n```\n\n4.  我们的类接受输入字符串，但将其转换为小写。有一个函数，是按字符来做这个的，所以我们可以在这里放自己的`tolow`函数。这个函数就是`constexpr`，这就是为什么我们给自己重新实现了一个`constexpr` `tolow`函数:\n\n```cpp\n          static constexpr \n          void assign(char_type& r, const char_type& a ) {\n              r = tolow(a);\n          }\n```\n\n5.  另一个函数负责将整个字符串复制到自己的内存中。我们使用`std::transform`调用将所有字符从源字符串复制到内部目标字符串，同时将每个字符映射到其小写版本:\n\n```cpp\n          static char_type* copy(char_type* dest, \n                                 const char_type* src, \n                                 size_t count) {\n              transform(src, src + count, dest, tolow);\n              return dest;\n          }\n      };\n```\n\n6.  另一个特性有助于构建一个字符串类，有效地将字符串转换为小写。我们将编写另一个特性，保持实际的字符串有效负载不变，但是在比较字符串时不区分大小写。我们再次继承了现有的标准角色特征类，这一次，我们重新定义了一些其他成员函数:\n\n```cpp\n      class ci_traits : public char_traits<char> {\n      public:\n```\n\n7.  `eq`功能告知两个字符是否相等。我们也这样做，但是我们比较它们的小写版本。这样`'A'`就等于`'a'`:\n\n```cpp\n          static constexpr bool eq(char_type a, char_type b) {\n              return tolow(a) == tolow(b);\n          }\n```\n\n8.  `lt`功能告知`a`的值是否小于`b`的值。我们对此应用了正确的逻辑运算符，仅在两个字符的小写之后:\n\n```cpp\n          static constexpr bool lt(char_type a, char_type b) {\n              return tolow(a) < tolow(b);\n          }\n```\n\n9.  后两个函数用于字符输入，后两个函数用于字符串输入。`compare`功能的工作原理类似于老派的`strncmp`功能。如果两个字符串在`count`定义的长度内相等，则返回`0`。如果它们不同，它将返回一个负数或正数，这表明哪个输入字符串在字典序上更小。当然，计算每个位置的两个字符之间的差异必须在它们的小写版本上完成。好的一点是，从 C++ 14 开始，整个循环代码就成为了一个`constexpr`函数的一部分:\n\n```cpp\n          static constexpr int compare(const char_type* s1,\n                                       const char_type* s2,\n                                       size_t count) {\n              for (; count; ++ s1, ++ s2, --count) {\n                  const char_type diff (tolow(*s1) - tolow(*s2));\n                  if      (diff < 0) { return -1; }\n                  else if (diff > 0) { return +1; }\n              }\n              return 0;\n          }\n```\n\n10.  我们需要为不区分大小写的字符串类实现的最后一个函数是`find`。对于给定的输入字符串`p`和长度`count`，它会找到字符的位置`ch`。然后，它返回一个指向该字符第一次出现的指针，如果没有，则返回`nullptr`。该功能中的比较必须使用`tolow`“眼镜”来完成，以使搜索不区分大小写。可惜我们不能用`std::find_if`，因为不是`constexpr`，必须自己写一个循环:\n\n```cpp\n          static constexpr \n          const char_type* find(const char_type* p,\n                                size_t count,\n                                const char_type& ch) {\n              const char_type find_c {tolow(ch)};\n\n              for (; count != 0; --count, ++ p) {\n                  if (find_c == tolow(*p)) { return p; }\n              }\n\n              return nullptr;\n          }\n      };\n```\n\n11.  好了，这就是特质。既然我们现在已经有了它们，我们可以定义两个新的字符串类类型。`lc_string`表示*小写字母串*。`ci_string`表示*不区分大小写的字符串*。这两个班与`std::string`的区别仅在于他们的性格特征班:\n\n```cpp\n      using lc_string = basic_string<char, lc_traits>;\n      using ci_string = basic_string<char, ci_traits>;\n```\n\n12.  为了让输出流接受这些新类进行打印，我们需要快速重载流`operator<<`:\n\n```cpp\n      ostream& operator<<(ostream& os, const lc_string& str) {\n          return os.write(str.data(), str.size());\n      }\n\n      ostream& operator<<(ostream& os, const ci_string& str) {\n          return os.write(str.data(), str.size());\n      }\n```\n\n13.  现在我们终于可以开始实施实际的计划了。让我们实例化一个普通字符串、一个小写字符串和一个不区分大小写的字符串，并立即打印它们。它们在终端上都应该看起来正常，但是小写字符串应该都是小写的:\n\n```cpp\n      int main()\n      {\n          cout << \"   string: \" \n               << string{\"Foo Bar Baz\"} << 'n'\n               << \"lc_string: \" \n               << lc_string{\"Foo Bar Baz\"} << 'n'\n               << \"ci_string: \"\n               << ci_string{\"Foo Bar Baz\"} << 'n';\n```\n\n14.  为了测试不区分大小写的字符串，我们可以实例化两个基本相等但某些字符大小写不同的字符串。当进行真正不区分大小写的比较时，它们看起来应该是相等的:\n\n```cpp\n          ci_string user_input {\"MaGiC PaSsWoRd!\"};\n          ci_string password   {\"magic password!\"};\n```\n\n15.  因此，让我们比较它们，如果它们匹配，就打印出来:\n\n```cpp\n          if (user_input == password) {\n              cout << \"Passwords match: \"\" << user_input\n                   << \"\" == \"\" << password << \"\"n\";\n          }\n      }\n```\n\n16.  编译和运行这个程序会给我们带来预期的结果。当我们第一次以不同的类型打印同一个字符串三次时，我们得到了不变的结果，但是`lc_string`实例都是小写的。这两个字符串只有字符大小写不同，对它们的比较确实是成功的，并为我们提供了正确的输出:\n\n```cpp\n      $ ./custom_string \n         string: Foo Bar Baz\n      lc_string: foo bar baz\n      ci_string: Foo Bar Baz\n      Passwords match: \"MaGiC PaSsWoRd!\" == \"magic password!\"\n```\n\n# 它是如何工作的...\n\n对于初学者来说，我们所做的所有子类化和函数重新实现看起来肯定有点疯狂。所有的函数签名来自哪里，我们*神奇地*知道我们需要重新实现？\n\n我们先来看看`std::string`到底从何而来:\n\n```cpp\ntemplate <\n    class CharT, \n    class Traits    = std::char_traits<CharT>, \n    class Allocator = std::allocator<CharT>\n    > \nclass basic_string;\n```\n\n`std::string`实际上是一个`std::basic_string<char>`，并扩展到`std::basic_string<char, std::char_traits<char>, std::allocator<char>>`。好吧，这是一个很长的类型描述，但它意味着什么？所有这些的要点是，字符串不仅可以基于单字节`char`项，还可以基于其他更大的类型。这支持字符串类型，它可以处理比典型的美国 ASCII 字符集更多的内容。这不是我们现在要调查的事情。\n\n然而`char_traits<char>`类包含`basic_string`运行所需的算法。知道如何比较、查找和复制字符和字符串。\n\n`allocator<char>`类也是一个 traits 类，但是它的特殊工作是处理字符串分配和解除分配。这在此时对我们来说并不重要，因为默认行为满足了我们的需求。\n\n如果我们想要一个字符串类有不同的行为，我们可以尝试从`basic_string`和`char_traits`已经提供的东西中尽可能多地重用。这就是我们所做的。我们实现了两个名为`case_insentitive`和`lower_caser`的`char_traits`子类，并通过使用它们作为标准`char_traits`类型的替代品来配置两个全新的字符串类型。\n\nIn order to explore what other possibilities there are to adapt `basic_string` to your own needs, look up the C++ STL documentation for `std::char_traits` and see what other functions it has that can be reimplemented.\n\n# 使用正则表达式库标记输入\n\n当以复杂的方式解析或转换字符串或将其分解成块时，*正则表达式*是一个很大的帮助。在许多编程语言中，它们已经内置，因为它们非常有用和方便。\n\n如果你还不知道正则表达式，可以看看*维基百科*关于正则表达式的文章。它们肯定会扩展你的视野，因为很容易看出它们在解析任何类型的文本时有多有用。例如，正则表达式可以测试电子邮件地址字符串或 IP 地址字符串是否有效，从遵循复杂模式的大字符串中查找和提取子字符串，等等。\n\n在这个食谱中，我们将从一个 HTML 文件中提取所有链接，并为用户列出它们。代码将会非常短，因为我们从 C++ 11 开始就在 C++ STL 中内置了正则表达式支持。\n\n# 怎么做...\n\n我们将定义一个检测链接的正则表达式，并将它应用于一个 HTML 文件，以便漂亮地打印该文件中出现的所有链接:\n\n1.  让我们首先包含所有必要的头，并声明我们默认使用`std`命名空间:\n\n```cpp\n      #include <iostream>\n      #include <iterator>\n      #include <regex>\n      #include <algorithm>\n      #include <iomanip>      \n\n      using namespace std;\n```\n\n2.  稍后我们将生成一个由字符串组成的可迭代范围。这些字符串总是成对出现在链接和链接描述中。因此，让我们编写一个小助手函数，它可以很好地打印以下内容:\n\n```cpp\n      template <typename InputIt>\n      void print(InputIt it, InputIt end_it)\n      {\n          while (it != end_it) {\n```\n\n3.  在每个循环步骤中，我们将迭代器递增两次，并复制它们包含的链接和链接描述。为了安全起见，在两个迭代器解引用之间，我们添加了另一个保护`if`分支，检查我们是否过早到达了可迭代范围的末尾:\n\n```cpp\n              const string link {*it++};\n              if (it == end_it) { break; }\n              const string desc {*it++};\n```\n\n4.  现在，让我们用漂亮的修饰形式打印带有描述的链接，就是这样:\n\n```cpp\n              cout << left << setw(28) << desc \n                   << \" : \" << link << 'n';\n          }\n      }\n```\n\n5.  在主功能中，我们阅读来自标准输入的所有内容。为此，我们通过输入流迭代器从整个标准输入中构造一个字符串。为了防止标记化，因为我们希望整个用户输入保持原样，所以我们使用`noskipws`。此修饰符停用空白跳过和标记化:\n\n```cpp\n      int main()\n      {\n          cin >> noskipws;\n          const std::string in {istream_iterator<char>{cin}, {}};\n```\n\n6.  现在我们需要定义一个正则表达式来描述我们假设的 HTML 链接的外观。正则表达式中的括号`()`定义了组。这些是我们想要访问的链接的部分——它链接到的网址及其描述:\n\n```cpp\n          const regex link_re {\n              \"<a href=\"([^\"]*)\"[^<]*>([^<]*)</a>\"};\n```\n\n7.  `sregex_token_iterator`级的观感与`istream_iterator`级相同。我们给它整个字符串作为可迭代的输入范围和我们刚刚定义的正则表达式。还有第三个参数`{1, 2}`，它是整数值的初始化列表。它定义了我们想要从它捕获的表达式中迭代组 1 和组 2:\n\n```cpp\n          sregex_token_iterator it {\n              begin(in), end(in), link_re, {1, 2}};\n```\n\n8.  现在我们有了一个迭代器，如果它找到链接和链接描述，就会发出链接和链接描述。我们将它与一个相同类型的默认构造迭代器一起提供给我们之前实现的`print`函数:\n\n```cpp\n          print(it, {});\n      }\n```\n\n9.  编译并运行该程序会给出以下输出。我在 ISO C++ 主页上运行`curl`程序，它只是从网上下载一个 HTML 页面。当然，也可以写`cat some_html_file.html | ./link_extraction`。我们使用的正则表达式基本上是硬编码在 HTML 文档中链接外观的固定假设上的。您可以使用它来使它更通用:\n\n```cpp\n      $ curl -s \"https://isocpp.org/blog\" | ./link_extraction \n      Sign In / Suggest an Article : https://isocpp.org/member/login\n      Register                     : https://isocpp.org/member/register\n      Get Started!                 : https://isocpp.org/get-started\n      Tour                         : https://isocpp.org/tour\n      C++ Super-FAQ                : https://isocpp.org/faq\n      Blog                         : https://isocpp.org/blog\n      Forums                       : https://isocpp.org/forums\n      Standardization              : https://isocpp.org/std\n      About                        : https://isocpp.org/about\n      Current ISO C++ status       : https://isocpp.org/std/status\n      (...and many more...)\n```\n\n# 它是如何工作的...\n\n正则表达式(简称*正则表达式*)极其有用。它们可能看起来很神秘，但值得了解它们是如何工作的。如果我们手动进行匹配，一个简短的正则表达式可以省去我们写很多行代码。\n\n在这个方法中，我们首先实例化了一个 regex 类型的对象。我们为它的构造函数提供了一个描述正则表达式的字符串。一个非常简单的正则表达式是`\".\"`，它匹配每个字符的*，因为一个点是正则表达式通配符。如果我们写`\"a\"`，那么这仅匹配`'a'`字符。如果我们写`\"ab*\"`，那么这意味着“一个`a`，零个或任意多个`b`字符”。等等。正则表达式是另一个大话题，维基百科和其他网站或文献上有很好的解释。*\n\n让我们再看看我们的正则表达式，它与我们假设的 HTML 链接相匹配。一个简单的 HTML 链接可以看起来像`<a href=\"some_url.com/foo\">A great link</a>`。我们想要`some_url.com/foo`部分，还有`A great link`。所以我们想出了下面的正则表达式，它包含用于匹配子串的*组*:\n\n![](img/f6d75901-b27a-455d-bbb6-118f376bef15.png)\n\n整场比赛本身永远是**0 组**。在这种情况下，这是完整的`<a href ..... </a>`字符串。引用的`href`-包含链接到的网址的部分是**组 1** 。正则表达式中的`( )`括号定义了这样一个，另一个是`<a ...>`和`</a>`之间的部分，包含链接描述。\n\n有各种接受 regex 对象的 STL 函数，但是我们直接使用了一个 regex token 迭代器适配器，这是一个高级抽象，在引擎盖下使用`std::regex_search`以便自动化重复的匹配工作。我们这样实例化它:\n\n```cpp\nsregex_token_iterator it {begin(in), end(in), link_re, {1, 2}};\n```\n\n开始和结束部分表示我们的输入字符串，正则表达式标记迭代器将对其进行迭代并匹配所有链接。当然，是我们为匹配链接而实现的复杂正则表达式。`{1, 2}`部分是下一个看起来复杂的东西。它指示标记迭代器在每次完全匹配时停止，首先产生组 1，然后在迭代器递增后产生组 2，再次递增后，它将最终搜索字符串中的下一个匹配。这种有些智能的行为真的让我们少了一些代码行。\n\n让我们看看另一个例子，以确保我们得到了这个想法。让我们想象一下正则表达式，`\"a(b*)(c*)\"`。它将匹配包含一个`a`字符、一个或任意多个`b`字符、一个或任意多个`c`字符的字符串:\n\n```cpp\nconst string s {\" abc abbccc \"};\nconst regex re {\"a(b*)(c*)\"};\n\nsregex_token_iterator it {begin(s), end(s), re, {1, 2}};\n\nprint( *it ); // prints b\n++ it;\nprint( *it ); // prints c\n++ it;\nprint( *it ); // prints bb\n++ it;\nprint( *it ); // prints ccc\n```\n\n还有`std::regex_iterator`类，它发出的子串是正则表达式匹配之间的*。*\n\n# 根据上下文动态打印不同的数字，非常舒服\n\n在上一个食谱中，我们学习了如何用输出流格式化输出。同时，我们意识到两个事实:\n\n*   大多数输入输出操纵器都是*粘性的*，所以我们必须在使用后恢复它们的效果，以免篡改其他不相关的代码，这些代码也会打印出来\n*   如果我们必须设置输入/输出操纵器的长链，以便只打印几个带有特定格式的变量，这可能会非常繁琐，而且看起来不太可读\n\n很多人因为这样的原因不喜欢 I/O 流，甚至在 C++ 中，他们仍然使用`printf`来格式化自己的字符串。\n\n在这个食谱中，我们将看到如何在没有太多输入/输出操纵器噪音的情况下动态格式化类型。\n\n# 怎么做...\n\n我们将要实现一个类，`format_guard`，它可以自动恢复任何格式设置。此外，我们添加了一个包装类型，它可以包含任何值，但是当它被打印时，它会得到特殊的格式，而不会给我们带来输入/输出操纵器噪音:\n\n1.  首先，我们包含一些头，并声明我们使用`std`命名空间:\n\n```cpp\n      #include <iostream>\n      #include <iomanip>      \n\n      using namespace std;\n```\n\n2.  为我们整理流格式状态的助手类叫做`format_guard`。它的构造函数保存了`std::cout`当前设置的格式化标志。它的析构函数将它们恢复到调用构造函数时的状态。这有效地撤销了在以下两者之间应用的任何格式设置:\n\n```cpp\n      class format_guard {\n          decltype(cout.flags()) f {cout.flags()};\n\n      public:\n          ~format_guard() { cout.flags(f); }\n      };\n```\n\n3.  另一个小帮手类是`scientific_type`。因为它是一个类模板，所以它可以将任何负载类型包装为成员变量。它基本上什么都不做:\n\n```cpp\n      template <typename T>\n      struct scientific_type {\n          T value;\n\n          explicit scientific_type(T val) : value{val} {}\n      };\n```\n\n4.  我们可以为之前包装到`scientific_type`中的任何类型定义完全自定义的格式设置，因为如果我们为其重载流`operator>>`，则流库在打印这些类型时会执行完全不同的代码。这样，我们可以用科学浮点表示法打印科学值，如果它们有正值，则使用大写格式和显式`+`前缀。我们也使用我们的`format_guard`类，以便在再次离开该功能时整理我们的所有设置:\n\n```cpp\n      template <typename T>\n      ostream& operator<<(ostream &os, const scientific_type<T> &w) {\n          format_guard _;\n          os << scientific << uppercase << showpos;\n          return os << w.value;\n      }\n```\n\n5.  在主功能中，我们将首先玩`format_guard`类。我们打开一个新的范围，首先获取类的一个实例，然后我们将一些野生格式标志应用到`std::cout`:\n\n```cpp\n      int main()\n      {\n          {\n              format_guard _;\n              cout << hex << scientific << showbase << uppercase;\n\n              cout << \"Numbers with special formatting:n\";\n              cout << 0x123abc << 'n';\n              cout << 0.123456789 << 'n';\n          }\n```\n\n6.  在我们打印了一些启用了许多格式标志的数字后，我们再次离开了范围。发生这种情况时，`format_guard`的析构函数整理了格式。为了测试这一点，我们再次打印完全相同的数字*。它们应该看起来不同:*\n\n```cpp\n          cout << \"Same numbers, but normal formatting again:n\";\n          cout << 0x123abc << 'n';\n          cout << 0.123456789 << 'n';\n```\n\n7.  现在我们将`scientific_type`投入使用。让我们连续打印三个浮点数。我们将第二个数字包装在`scientific_type`中。这样，它以我们特殊的科学风格打印，但它前后的数字得到默认格式。同时，我们避免难看的格式化线*噪音*:\n\n```cpp\n          cout << \"Mixed formatting: \"\n               << 123.0 << \" \"\n               << scientific_type{123.0} << \" \"\n               << 123.456 << 'n';\n      }\n```\n\n8.  编译并运行程序会产生以下结果。前两个数字以特定格式打印。接下来的两个数字以默认格式出现，这向我们表明我们的`format_guard`工作得很好。最后一行的三个数字看起来也和预期的一样。中间只有一个有`scientific_type`的格式，其余有默认格式:\n\n```cpp\n      $ ./pretty_print_on_the_fly \n      Numbers with special formatting:\n      0X123ABC\n      1.234568E-01\n      Same numbers, but normal formatting again:\n      1194684\n      0.123457\n      Mixed formatting: 123 +1.230000E+02 123.456\n```\n\n# 从 std::iostream 错误中捕获可读异常\n\n在本章的*无*食谱中，我们使用了*例外*来捕捉错误。虽然这当然是可能的，但是毫无例外地处理流对象已经非常方便了。如果我们试图解析 10 个值，但这在中间的某个地方失败了，整个流对象会将自己设置为失败状态，并停止进一步的解析。这样，我们就不会遇到从流中错误的偏移量解析变量的危险。我们可以只做条件句的解析，比如`if (cin >> foo >> bar >> ...)`。如果失败了，我们会处理的。在`try { ... } catch ...`块中包含解析似乎不是很有利。\n\n事实上，在 C++ 中出现异常之前，C++ I/O 流库就已经存在了。后来增加了异常支持，这可能是为什么它们不是流库中一流的受支持特性的一个解释。\n\n为了在流库中使用异常，我们必须单独配置每个流对象，以便在它将自己设置为失败状态时抛出异常。不幸的是，异常对象中的错误解释并没有完全标准化，我们可以稍后捕捉到这些错误解释。正如我们将在本节中看到的，这将导致实际上没有帮助的错误消息。如果我们真的想对流对象使用异常，我们可以*另外*轮询 C 库中的文件系统错误状态，以获得一些额外的信息。\n\n在这一节中，我们将编写一个可能以不同方式失败的程序，处理那些有异常的程序，并看看以后如何从这些程序中挤出更多的信息。\n\n# 怎么做...\n\n我们将实现一个打开文件的程序(可能会失败)，然后我们将从中读取一个整数(也可能会失败)。我们通过激活异常来实现这一点，然后看看如何处理这些异常:\n\n1.  首先，我们包含一些头，并声明我们使用`std`命名空间:\n\n```cpp\n      #include <iostream>\n      #include <fstream>\n      #include <system_error>\n      #include <cstring>      \n\n      using namespace std;\n```\n\n2.  如果我们想使用带有异常的流对象，我们必须首先启用它们。为了让文件流对象在我们允许它访问的文件不存在或者存在解析错误时抛出异常，我们需要在异常掩码中设置一些失败位。如果我们后来做了一些失败的事情，它将触发一个异常。通过激活`failbit`和`badbit`，我们可以为文件系统错误和解析错误启用异常:\n\n```cpp\n      int main()\n      {\n          ifstream f;\n          f.exceptions(f.failbit | f.badbit);\n```\n\n3.  现在我们可以打开一个`try`块并访问一个文件。如果打开文件成功，我们会尝试从中读取一个整数。只有当这两个步骤都成功时，我们才打印整数:\n\n```cpp\n          try {\n              f.open(\"non_existant.txt\");\n\n              int i;\n              f >> i;\n\n              cout << \"integer has value: \" << i << 'n';\n          }\n```\n\n4.  在错误的两种预期可能性中，都会抛出一个`std::ios_base::failure`的实例。这个对象有一个`what()`成员函数，它应该解释是什么触发了异常。不幸的是，这个消息的标准化被遗漏了，它没有给出太多的信息。但是，我们至少可以区分是否存在*文件系统*问题(例如，因为文件不存在)或格式*解析*问题。甚至在 C++ 发明之前，全局变量`errno`就已经存在了，它被设置为一个错误值，我们现在可以检查一下。`strerror`函数将错误号翻译成人类可读的字符串。如果`errno`是`0`，至少没有文件系统错误:\n\n```cpp\n          catch (ios_base::failure& e) {\n              cerr << \"Caught error: \";\n              if (errno) {\n                  cerr << strerror(errno) << 'n';\n              } else {\n                  cerr << e.what() << 'n';\n              }\n          }\n      }\n```\n\n5.  编译程序并在两个不同的场景中运行它会产生以下输出。如果要打开的文件确实存在，但是无法从中解析一个整数，我们会收到一条`iostream_category`错误消息:\n\n```cpp\n      $ ./readable_error_msg \n      Caught error: ios_base::clear: unspecified iostream_category error\n```\n\n6.  如果文件*不*存在，我们将从`strerror(errno)`收到不同的消息:\n\n```cpp\n      $ ./readable_error_msg\n      Caught error: No such file or directory\n```\n\n# 它是如何工作的...\n\n我们已经看到，我们可以使用`s.exceptions(s.failbit | s.badbit)`为每个流对象`s`启用异常。这意味着，没有办法使用`std::ifstream`实例的构造函数来打开文件，如果我们想在无法打开文件时获得异常:\n\n```cpp\nifstream f {\"non_existant.txt\"};\nf.exceptions(...); // too late for an exception\n\n```\n\n这是一个遗憾，因为异常实际上承诺，与老派的 C 风格代码相比，它们使错误处理变得不那么笨拙，后者充满了大量的`if`分支，每一步后都会处理错误。\n\n如果我们试图激发各种原因导致流失败，我们会意识到没有不同的异常被抛出。这样，我们只能在得到错误时找出*，而不能找出*具体有什么*错误(这当然是*不是*对*通用*中异常处理的真实情况，而是对 STL 流库的情况)。这就是我们额外咨询`errno`值的原因。这个全局变量是一个古老的构造，在没有 C++ 或一般异常的旧时代已经被使用。*\n\n如果任何与系统相关的函数出现错误情况，它能够将`errno`变量设置为除`0`以外的其他值(`0`描述没有错误)，然后调用者能够读取该错误号并查找其值的含义。唯一的问题是，当我们有一个多线程应用，并且所有线程都使用可以设置这个错误变量的函数时，*它的*错误值是多少？如果我们读取它，即使没有错误，它也可能携带错误值，因为在不同线程*中运行的一些*或其他*系统功能可能遇到了错误。幸运的是，这个缺陷从 C++ 11 开始就消失了，在 c++ 11 中，进程中的每个线程都看到自己的`errno`变量。*\n\n在不详细说明一个古老的错误指示方法的起伏的情况下，当基于系统的事情(如文件流)触发异常时，它可以给我们提供有用的额外信息。异常告诉我们*什么时候*发生的，`errno`可以告诉我们*如果发生在系统层面*发生了什么。*****"
  },
  {
    "path": "docs/exp-cpp-prog/07.md",
    "content": "# 七、工具类\n\n在本章中，我们将介绍以下食谱:\n\n*   使用`std::ratio`在不同时间单位之间转换\n*   用`std::chrono`在绝对时间和相对时间之间转换\n*   用`std::optional`安全地发出故障信号\n*   对元组应用函数\n*   用`std::tuple`快速组成数据结构\n*   用`std::any`代替`void*`更安全\n*   用`std::variant`存储不同类型\n*   用`std::unique_ptr`自动处理资源\n*   通过`std::shared_ptr`自动处理共享堆内存\n*   处理指向共享对象的弱指针\n*   使用智能指针简化遗留 API 的资源处理\n*   共享同一对象的不同成员值\n*   生成随机数并选择合适的随机数引擎\n*   生成随机数并让 STL 形成特定的分布\n\n# 介绍\n\n本章专门介绍对解决非常具体的任务非常有用的工具类。其中一些确实非常有用，以至于我们将来很可能会在任何 C++ 程序片段中经常看到它们，或者至少已经看到它们散布在本书的所有其他章节中。\n\n前两个食谱是关于测量和获取*时间*。我们还将看到如何在不同的时间单位之间转换，以及如何在时间点之间跳转。\n\n然后，我们将看一下`optional`、`variant`和`any`类型(都是 C++ 14 和 C++ 17 附带的)以及另外五个食谱中的一些`tuple`技巧。\n\n自 C++ 11 以来，我们还获得了复杂的智能指针类型，即`unique_ptr`、`shared_ptr`和`weak_ptr`，它们在*内存管理*方面提供了非常有效的帮助，这就是为什么我们将在五个食谱中专门介绍它们。\n\n最后，我们将看到关于生成*随机数*的 STL 库部分的全景。除了学习 STL 随机引擎最重要的特性，我们还将学习如何将整形应用于随机数，以获得符合我们实际需求的分布。\n\n# 使用标准::比率在不同时间单位之间转换\n\n自从 C++ 11 以来，STL 包含了一些新的类型和功能，用于获取、测量和显示时间。库的这一部分存在于`std::chrono`命名空间中，并且有一些复杂的细节。\n\n在本食谱中，我们将集中于测量时间跨度以及如何在单位之间转换测量结果，例如秒、毫秒和微秒。STL 提供了工具，使我们能够定义自己的时间单位，并在它们之间无缝转换。\n\n# 怎么做...\n\n在这一部分，我们将编写一个小的*游戏*，提示用户输入一个特定的单词。用户需要在键盘上输入该单词的时间以多个时间单位进行测量和显示:\n\n1.  首先，我们需要包含所有必要的标题。为了方便起见，我们声明我们默认使用`std`命名空间:\n\n```cpp\n      #include <iostream>\n      #include <chrono>\n      #include <ratio>\n      #include <cmath>\n      #include <iomanip>\n      #include <optional>      \n\n      using namespace std;\n```\n\n2.  `chrono::duration`作为持续时间的类型，通常指秒的倍数或分数。所有的 STL 持续时间单位指的是整数类型的持续时间专门化。在这个食谱中，我们将专攻`double`。在接下来的食谱中，我们将更加关注已经内置到 STL 中的现有时间单位定义:\n\n```cpp\n      using seconds = chrono::duration<double>;\n```\n\n3.  一毫秒是一秒的一小部分，所以我们用秒来定义这个单位。`ratio_multiply`模板参数将 STL 预定义的`milli`因子应用于`seconds::period`，这给了我们想要的分数。`ratio_multiply`模板基本上是倍率的元编程功能:\n\n```cpp\n      using milliseconds = chrono::duration<\n          double, ratio_multiply<seconds::period, milli>>;\n```\n\n4.  微秒也是如此。一毫秒是一秒的分数，一微秒是一秒的分数:\n\n```cpp\n      using microseconds = chrono::duration<\n          double, ratio_multiply<seconds::period, micro>>;\n```\n\n5.  现在我们将实现一个函数，该函数从用户输入中读取一个字符串，并测量用户键入输入花费的时间。它不接受任何参数，并向我们返回用户输入字符串以及经过的时间，捆绑成一对:\n\n```cpp\n      static pair<string, seconds> get_input()\n      {\n          string s;\n```\n\n6.  我们需要从用户输入发生期间的开始和之后开始计算时间。拍摄时间快照如下所示:\n\n```cpp\n          const auto tic (chrono::steady_clock::now());\n```\n\n7.  用户输入的实际捕获现在发生了。如果我们没有成功，我们只返回一个默认初始化的元组。呼叫者将看到他得到了一个空的输入字符串:\n\n```cpp\n          if (!(cin >> s)) {\n              return {{}, {}};\n          }\n```\n\n8.  在成功的情况下，我们继续拍摄另一个时间快照。然后我们返回输入字符串和两个时间点之间的差值。请注意，两者都是绝对时间点，但通过计算差异，我们得到一个持续时间:\n\n```cpp\n          const auto toc (chrono::steady_clock::now());\n\n          return {s, toc - tic};\n      }\n```\n\n9.  现在让我们实现实际的程序。我们循环直到用户正确输入输入字符串。在每个循环步骤中，我们要求用户输入字符串`\"C++ 17\"`，然后调用我们的`get_input`函数:\n\n```cpp\n      int main()\n      {\n          while (true) {\n              cout << \"Please type the word \"C++ 17\" as\"\n                      \" fast as you can.n> \";\n\n              const auto [user_input, diff] = get_input();\n```\n\n10.  然后我们检查输入。如果输入为空，我们将其解释为退出整个程序的请求:\n\n```cpp\n              if (user_input == \"\") { break; }\n```\n\n11.  如果用户正确输入`\"C++ 17\"`，我们表示祝贺，然后打印用户正确输入单词所需的时间。`diff.count()`方法以浮点数的形式返回秒数。如果我们使用原始的 STL `seconds`持续时间类型，那么我们会得到一个*舍入的*整数值，而不是分数。在调用`count()`之前，通过向毫秒或微秒`constructor`提供我们的`diff`变量，我们可以将相同的值转换为不同的单位:\n\n```cpp\n              if (user_input == \"C++ 17\") {\n                  cout << \"Bravo. You did it in:n\" \n                       << fixed << setprecision(2)\n                       << setw(12) << diff.count() \n                       << \" seconds.n\"\n                       << setw(12) << milliseconds(diff).count()\n                       << \" milliseconds.n\"\n                       << setw(12) << microseconds(diff).count()\n                       << \" microseconds.n\";\n                  break;\n```\n\n12.  如果用户在输入中有错别字，我们让他再试一次:\n\n```cpp\n              } else {\n                  cout << \"Sorry, your input does not match.\"\n                          \" You may try again.n\";\n              }\n          }\n      }\n```\n\n13.  编译和运行程序会产生以下输出。一开始，程序会因为一个错别字，反复要求输入正确的单词。正确键入单词后，它会显示我们用三种不同的时间单位键入单词所需的时间:\n\n```cpp\n      $ ./ratio_conversion \n      Please type the word \"C++ 17\" as fast as you can.\n      > c+17\n      Sorry, your input does not match. You may try again.\n      Please type the word \"C++ 17\" as fast as you can.\n      > C++ 17\n      Bravo. You did it in:\n              1.48 seconds.\n           1480.10 milliseconds.\n        1480099.00 microseconds.\n```\n\n# 它是如何工作的...\n\n虽然这一部分是关于不同时间单位之间的转换，但我们首先必须从三个可用的时钟对象中选择一个。在`std::chrono`命名空间中，一般有`system_clock`、`steady_clock`和`high_resolution_clock`之间的选择。它们之间有什么区别？让我们仔细看看:\n\n| **时钟** | **特征** |\n| `system_clock` | 这代表全系统实时的*墙*时钟。如果我们想获得当地时间，这是正确的选择。 |\n| `steady_clock` | 这个时钟保证是*单调的*。这意味着它永远不会倒退任何时间。当其他时钟的时间被最小限度地校正时，或者甚至当时间在冬季和夏季之间切换时，这种情况也可能发生。 |\n| `high_resolution_clock` | 这是 STL 实现能够提供的最细粒度时钟周期的时钟。 |\n\n由于我们测量了从一个绝对时间点到另一个绝对时间点的时间距离或持续时间(我们在变量`tic`和`toc`中捕捉到的)，我们对这些时间点是否是全局偏斜的不感兴趣。即使时钟晚了或提前了 112 年 5 小时 10 分钟 1 秒(或其他)，这也不会影响它们之间的*差异。唯一重要的是，在我们保存时间点`tic`之后和保存时间点`toc`之前，不得对时钟进行微调(这种情况在许多系统中时有发生)，因为这会扭曲我们的测量。对于这些要求，`steady_clock`是最优选择。它的实现可以基于处理器的时间戳计数器，自系统启动以来，该计数器总是单调递增。*\n\n好了，现在有了正确的时间对象选择，我们可以通过`chrono::steady_clock::now()`节省时间点。`now`函数返回一个`chrono::time_point<chrono::steady_clock>`类型的值。两个这样的值之间的差异(如在`toc - tic`中)是*时间跨度*，或`chrono::duration`类型的*持续时间*。因为这是这个部分的中心类型，所以现在有点复杂了。让我们仔细看看`duration`的模板类型界面:\n\n```cpp\ntemplate<\n    class Rep, \n    class Period = std::ratio<1> \n> class duration;\n```\n\n我们可以改变的参数叫做`Rep`和`Period`。`Rep`很容易解释:这只是用来保存时间值的数值变量类型。对于现有的 STL 时间单位，这通常是`long long int`。在这个食谱中，我们选择了`double`。由于我们的选择，我们可以默认以秒为单位保存时间值，然后将其转换为毫秒或微秒。如果我们在`chrono::seconds`类型中保存`1.2345`秒的持续时间，那么它将被舍入到一整秒。这样，我们将不得不节省`chrono::microseconds`中`tic`和`toc`之间的时间差，然后可以转换成更细粒度的单元。通过我们对`Rep`的`double`选择，我们可以上下转换，并且只损失最小的精度，在这个例子中这并没有坏处。\n\n我们将`Rep = double`用于所有时间单位，因此它们仅在`Period`参数的选择上有所不同:\n\n```cpp\nusing seconds      = chrono::duration<double>;\nusing milliseconds = chrono::duration<double, \n ratio_multiply<seconds::period, milli>>;\nusing microseconds = chrono::duration<double, \n ratio_multiply<seconds::period, micro>>;\n```\n\n虽然`seconds`是最简单的描述单位，因为它和`Period = ratio<1>`一起工作，其他的都要调整。由于一毫秒是千分之一秒，我们将`seconds::period`(这只是对`Period`参数的一个 getter 函数)与`milli`相乘，后者是`std::ratio<1, 1000>`的类型别名(`std::ratio<a, b>`代表小数值`a/b`)。`ratio_multiply`类型基本上是一个*编译时函数*，它代表一个比率类型与另一个比率类型相乘得到的类型。\n\n可能这听起来太复杂了，我们来看一个例子:`ratio_multiply<ratio<2, 3>, ratio<4, 5>>`结果在`ratio<8, 15>`因为`(2/3) * (4/5) = 8/15`。\n\n我们得到的类型定义相当于以下定义:\n\n```cpp\nusing seconds      = chrono::duration<double, ratio<1, 1>>;\nusing milliseconds = chrono::duration<double, ratio<1, 1000>>;\nusing microseconds = chrono::duration<double, ratio<1, 1000000>>;\n```\n\n将这些类型排列在一起，很容易在它们之间转换。如果我们有一个`seconds`类型的持续时间`d`，我们可以通过另一个类型的构造函数，也就是`milliseconds(d)`，把它转换成`milliseconds`。\n\n# 还有更多...\n\n在其他教程或书籍中，每当转换持续时间时，您可能会遇到`duration_cast`。例如，如果我们有一个类型为`chrono::milliseconds`的持续时间值，并且想要将其转换为`chrono::hours`，我们确实需要编写`duration_cast<chrono::hours>(milliseconds_value)`，因为这些单位依赖于*整数*类型。从细粒度单元转换到不太细粒度的单元会导致*精度损失*在这种情况下，这就是为什么我们需要一个`duration_cast`。对于基于`double`或`float`的持续时间单位，这是不需要的。\n\n# 使用 std::chrono 在绝对时间和相对时间之间转换\n\n直到 C++ 11，取挂钟时间*只是打印*还是挺麻烦的，因为 C++ 没有自己的时间库。总是有必要调用 C 库的函数，这看起来很过时，因为这种调用可以很好地封装到它们自己的类中。\n\n从 C++ 11 开始，STL 提供了`chrono`库，这使得与时间相关的任务更容易实现。\n\n在这个食谱中，我们将采用当地时间，打印出来，并通过添加不同的时间偏移来玩，这是一件非常舒适的事情。\n\n# 怎么做...\n\n我们将保存当前时间并打印它。此外，我们的程序将为保存的时间点添加不同的偏移量，并打印结果时间点:\n\n1.  典型的包含线优先；然后，我们声明我们默认使用`std`命名空间:\n\n```cpp\n      #include <iostream>\n      #include <iomanip>\n      #include <chrono>      \n\n      using namespace std;\n```\n\n2.  我们将打印绝对时间点。这些将以`chrono::time_point`类型模板的形式出现，所以我们将为它重载输出流操作符。打印时间点的日期和/或时间部分有不同的方法。我们将只使用`%c`标准格式。当然，我们也可以只打印时间，只打印日期，只打印年份，或者我们想到的任何东西。在我们最终应用`put_time`之前，不同类型之间的所有转换看起来有点笨拙，但是我们只做了一次:\n\n```cpp\n      ostream& operator<<(ostream &os, \n                    const chrono::time_point<chrono::system_clock> &t)\n      {\n          const auto tt   (chrono::system_clock::to_time_t(t));\n          const auto loct (std::localtime(&tt));\n          return os << put_time(loct, \"%c\");\n      }\n```\n\n3.  `seconds`、`minutes`、`hours`等已经有了 STL 类型定义。我们现在添加`days`类型。这很容易；我们只需要通过引用`hours`来专门化`chrono::duration`模板并与`24`相乘，因为一整天有 24 个小时:\n\n```cpp\n      using days = chrono::duration<\n          chrono::hours::rep,\n          ratio_multiply<chrono::hours::period, ratio<24>>>;\n```\n\n4.  为了能够以最优雅的方式用天数的倍数来表示持续时间，我们可以定义自己的`days`文字运算符。现在，我们可以写`3_days`来构造一个代表三天的值:\n\n```cpp\n      constexpr days operator \"\"_days(unsigned long long h)\n      {\n          return days{h};\n      }\n```\n\n5.  在实际的程序中，我们将拍摄一个时间快照，然后简单地打印出来。这是非常容易和舒适的，因为我们已经为此实现了正确的运算符重载:\n\n```cpp\n      int main()\n      {\n          auto now (chrono::system_clock::now());\n\n          cout << \"The current date and time is \" << now << 'n';\n```\n\n6.  将当前时间保存在`now`变量中后，我们可以在其中添加任意的持续时间，并打印出来。让我们在当前时间的基础上增加 12 小时，并打印 12 小时后的时间:\n\n```cpp\n          chrono::hours chrono_12h {12};\n\n          cout << \"In 12 hours, it will be \"\n               << (now + chrono_12h)<< 'n';\n```\n\n7.  通过声明我们在默认情况下使用`chrono_literals`名称空间，我们解锁了小时、秒等所有现有的持续时间文字。这样，我们可以优雅地打印 12 小时 15 分钟前，或者 7 天前的时间:\n\n```cpp\n          using namespace chrono_literals;\n\n          cout << \"12 hours and 15 minutes ago, it was \"\n               << (now - 12h - 15min) << 'n'\n               << \"1 week ago, it was \"\n               << (now - 7_days) << 'n';\n      }\n```\n\n8.  编译并运行程序会产生以下输出。因为我们使用`%c`作为时间格式化的格式字符串，所以我们得到了一个特定格式的相当完整的描述。通过玩不同格式的字符串，我们可以得到任何我们喜欢的格式。请注意，时间格式不是上午/下午 12 小时，而是 24 小时，因为该应用运行在欧洲系统上:\n\n```cpp\n $ ./relative_absolute_times \n      The current date and time is Fri May  5 13:20:38 2017\n      In 12 hours, it will be Sat May  6 01:20:38 2017\n      12 hours and 15 minutes ago, it was Fri May  5 01:05:38 2017\n      1 week ago, it was Fri Apr 28 13:20:38 2017\n```\n\n# 它是如何工作的...\n\n我们从`std::chrono::system_clock`获得了当前时间点。这个 STL 时钟类是唯一一个可以将其时间点值转换为时间结构的类，该时间结构可以显示为人类可读的时间描述字符串。\n\n为了打印这样的时间点，我们对输出流实现了`operator<<`:\n\n```cpp\nostream& operator<<(ostream &os, \n                    const chrono::time_point<chrono::system_clock> &t)\n{\n    const auto tt   (chrono::system_clock::to_time_t(t));\n    const auto loct (std::localtime(&tt));\n    return os << put_time(loct, \"%c\");\n}\n```\n\n这里首先发生的是，我们从`chrono::time_point<chrono::system_clock>`转换到`std::time_t`。这种类型的值可以转换成本地挂钟相关的时间值，我们用`std::localtime`来实现。这个函数向我们返回一个指向转换值的指针(不用担心这个指针后面的内存的维护；它是一个没有在堆上分配的静态对象)，现在我们终于可以打印它了。\n\n`std::put_time`函数接受这样一个对象和一个时间格式字符串。`\"%c\"`显示标准日期时间字符串，如`Sun Mar 12 11:33:40 2017`。我们也可以写`\"%m/%d/%y\"`；那么程序就会以`03/12/17`的格式打印时间。现有格式字符串修饰符的整个列表很长，但是在在线 C++ 参考中，它被很好地记录到了最大程度。\n\n除了打印，我们还在时间点上添加了时间偏移。这很简单，因为我们可以将持续时间表示为 *12 小时 15 分钟*表示为`12h + 15min`。`chrono_literals`命名空间已经为小时(`h`)、分钟(`min`)、秒(`s`)、毫秒(`ms`)、微秒(`us`)和纳秒(`ns`)提供了便利的类型文字。\n\n将这样的持续时间值添加到时间点值会创建一个新的时间点值，因为这些类型具有正确的`operator+`和`operator-`重载，这就是添加和显示时间偏移如此简单的原因。\n\n# 使用标准::可选安全地发出故障信号\n\n当一个程序与外部世界交流并依赖于它从那里获得的价值时，那么各种各样的失败都可能发生。\n\n这意味着，每当我们编写一个应该返回值的函数，但也可能失败时，这必须反映在函数接口的一些变化中。我们有几种可能。让我们看看如何设计将返回字符串的函数的接口，但这也可能失败:\n\n*   使用指示成功的返回值和输出参数:`bool get_string(string&);`\n*   如果出现故障，返回可设置为`nullptr`的指针(或智能指针):`string* get_string();`\n*   在失败的情况下抛出异常，让函数签名非常简单:`string get_string();`\n\n所有这些方法都有不同的优缺点。从 C++ 17 开始，有一种新的类型可以用不同的方式解决这样的问题:`std::optional`。可选值的概念来自纯粹的函数式编程语言(它们有时被称为`Maybe`类型)，可以产生非常优雅的代码。\n\n我们可以将`optional`包裹在自己的类型周围，以表示*空*或*错误的*值。在这个食谱中，我们将学习如何做。\n\n# 怎么做...\n\n在这一节中，我们将实现一个程序，从用户那里读取整数并对它们求和。因为用户总是可以输入随机的东西，而不是数字，我们将看到`optional`如何改进我们的错误处理:\n\n1.  首先，我们包括所有需要的头，并声明我们使用`std`命名空间:\n\n```cpp\n      #include <iostream>\n      #include <optional>     \n\n      using namespace std;\n```\n\n2.  让我们定义一个整数类型，其中，*可能，*包含一个值。`std::optional`型正是这么做的。通过将任何类型包装到`optional`中，我们给它一个额外的可能状态，这反映了它当前没有*值*:\n\n```cpp\n      using oint = optional<int>;\n```\n\n3.  通过定义可选的整数类型，我们可以表示通常返回整数的函数也可能失败。如果我们从用户输入中获取一个整数，这可能会失败，因为即使我们要求用户输入整数，他也可能不会总是输入整数。在这种情况下，返回一个可选的整数是完美的。如果读取一个整数成功，我们将其输入`optional<int>`构造函数。否则，我们返回一个默认构造的可选值，它表示失败或空:\n\n```cpp\n      oint read_int()\n      {\n          int i;\n          if (cin >> i) { return {i}; }\n          return {};\n      }\n```\n\n4.  我们能做的不仅仅是从可能失败的函数中返回整数。如果我们计算两个可选整数之和呢？只有当两个操作数都包含一个实际值时，才能得到一个实数值和。在任何其他情况下，我们返回一个空的可选变量。这个函数需要多一点解释:通过将`optional<int>`变量、`a`和`b`隐式转换为布尔表达式(通过编写`!a`和`!b`，我们可以知道它们是否包含实际值。如果有，我们可以像指针或迭代器一样访问它们，只需用`*a`和`*b`取消它们的引用即可:\n\n```cpp\n      oint operator+(oint a, oint b)\n      {\n          if (!a || !b) { return {}; }\n\n          return {*a + *b};\n      }\n```\n\n5.  将普通整数添加到可选整数遵循相同的逻辑:\n\n```cpp\n      oint operator+(oint a, int b)\n      {\n          if (!a) { return {}; }\n\n          return {*a + b};\n      }\n```\n\n6.  现在让我们编写一个程序，用可选整数做一些事情。我们让用户输入两个数字:\n\n```cpp\n      int main()\n      {\n          cout << \"Please enter 2 integers.n> \";\n\n          auto a {read_int()};\n          auto b {read_int()};\n```\n\n7.  然后，我们将这些输入数字相加，再将值 10 加到它们的总和中。由于`a`和`b`是可选整数，`sum`也是可选整数类型变量:\n\n```cpp\n          auto sum (a + b + 10);\n```\n\n8.  如果`a`和/或`b`不包含值，那么`sum`也不可能包含值。现在可选整数的好处是我们不需要显式检查`a`和`b`。当我们总结空选项时发生的事情是完全理智和明确的行为，因为我们以安全的方式为这些类型定义了`operator+`。这样，我们可以任意添加许多可能为空的可选整数，并且我们只需要检查得到的可选值。如果它包含一个值，那么我们可以安全地访问和打印它:\n\n```cpp\n          if (sum) {\n             cout << *a << \" + \" << *b << \" + 10 = \"\n                  << *sum << 'n';\n```\n\n9.  如果用户输入非数字内容，我们会出错:\n\n```cpp\n          } else {\n             cout << \"sorry, the input was \"\n                     \"something else than 2 numbers.n\";\n          }\n      }\n```\n\n10.  就这样。当我们编译并运行该程序时，我们会得到以下输出:\n\n```cpp\n      $ ./optional \n      Please enter 2 integers.\n      > 1 2\n      1 + 2 + 10 = 13\n```\n\n11.  再次运行程序并输入非数字内容会产生我们为这种情况准备的错误消息:\n\n```cpp\n      $ ./optional \n      Please enter 2 integers.\n      > 2 z\n      sorry, the input was something else than 2 numbers.\n```\n\n# 它是如何工作的...\n\n使用`optional`工作一般都很简单方便。如果我们想把可能失败或可选性的概念附加到任何类型的`T`上，我们可以把它包装成`std::optional<T>`，就这样。\n\n每当我们从某个地方得到这样一个值时，我们都要检查它是处于空状态还是包含一个实值。`bool optional::has_value()`功能为我们做到了这一点。如果它返回`true`，我们可以访问该值。通过`T& optional::value()`可以获得可选项目的价值。\n\n不要总是写`if (x.has_value()) {...}`和`x.value()`，我们也可以写`if (x) {...}`和`*x`。`std::optional`类型定义了到`bool`和`operator*`的显式转换，处理可选类型类似于处理指针。\n\n另一个很好了解的得心应手的操作员助手是`optional`的`operator->`过载。如果我们有一个`struct Foo { int a; string b; }`类型，并想通过`optional<Foo>`变量`x`访问它的一个成员，那么我们可以写`x->a`或`x->b`。当然，首先要检查`x`是否真的有价值。\n\n如果我们试图访问一个可选值，即使它没有值，那么它会抛出`std::logic_error`。这样，就有可能在不总是检查可选值的情况下处理大量可选值。使用`try-` `catch`子句，我们可以用以下形式编写代码:\n\n```cpp\ncout << \"Please enter 3 numbers:n\";\n\ntry {\n    cout << \"Sum: \" \n         << (*read_int() + *read_int() + *read_int()) \n         << 'n';\n} catch (const std::bad_optional_access &) {\n    cout << \"Unfortunately you did not enter 3 numbersn\";\n}\n```\n\n`std::optional`的另一个噱头是`optional::value_or`。如果我们想取一个可选的值，当它处于空状态时，回落到默认值，那么这是有帮助的。`x = optional_var.value_or(123)`用一句简洁的话完成这项工作，其中`123`是回退默认值。\n\n# 对元组应用函数\n\n从 C++ 11 开始，STL 提供`std::tuple`。这种类型允许我们偶尔将多个值捆绑到一个变量中，并将其传递给周围的人。元组的概念在许多编程语言中已经存在了很长时间，本书中的一些食谱已经专门介绍了这种类型，因为它的用途非常广泛。\n\n然而，我们有时会得到捆绑在元组中的值，然后需要用它们各自的成员调用函数。为每个函数参数单独解包成员是非常乏味的(如果我们在某个地方引入了一个错别字，就会容易出错)。繁琐的表单看起来是这样的:`func(get<0>(tup), get<1>(tup), get<2>(tup), ...);`。\n\n在本食谱中，您将学习如何以优雅的方式对元组进行值打包和解包，以便调用一些不了解元组的函数。\n\n# 怎么做...\n\n我们将实现一个程序，该程序对元组进行打包和解包。然后，我们将看到如何使用元组中的值调用对元组一无所知的函数:\n\n1.  首先，我们包含了很多头，并声明我们使用`std`命名空间:\n\n```cpp\n      #include <iostream>\n      #include <iomanip>\n      #include <tuple>\n      #include <functional>\n      #include <string>\n      #include <list>      \n\n      using namespace std;\n```\n\n2.  让我们首先定义一个函数，该函数接受描述学生的多个参数并打印它们。许多遗留或 C 函数接口看起来都很相似。：\n\n```cpp\n      static void print_student(size_t id, const string &name, double gpa)\n      {\n          cout << \"Student \" << quoted(name) \n               << \", ID: \"   << id \n               << \", GPA: \"  << gpa << 'n';\n      }\n```\n\n3.  在实际的程序中，我们动态地定义一个元组类型，并用有意义的学生数据填充它:\n\n```cpp\n      int main()\n      {\n          using student = tuple<size_t, string, double>;\n          student john {123, \"John Doe\"s, 3.7};\n```\n\n4.  为了打印这样一个对象，我们可以将其分解为其单个成员，并用这些单个变量调用`print_student`:\n\n```cpp\n          {\n              const auto &[id, name, gpa] = john;\n              print_student(id, name, gpa);\n          }\n          cout << \"-----n\";\n```\n\n5.  让我们以学生元组初始化列表的形式创建一整套学生:\n\n```cpp\n          auto arguments_for_later = {\n              make_tuple(234, \"John Doe\"s,  3.7),\n              make_tuple(345, \"Billy Foo\"s, 4.0),\n              make_tuple(456, \"Cathy Bar\"s, 3.5),\n          };\n```\n\n6.  我们仍然可以相对舒适地打印它们，但是为了分解元组，我们需要关心这样的元组有多少元素。如果我们必须编写这样的代码，那么我们还必须对其进行重组，以防函数调用接口发生变化:\n\n```cpp\n          for (const auto &[id, name, gpa] : arguments_for_later) {\n              print_student(id, name, gpa);\n          }\n          cout << \"-----n\";\n```\n\n7.  我们可以做得更好。甚至不知道`print_student`的参数类型或学生元组中的成员数量，我们就可以使用`std::apply`直接将元组的内容转发给函数。该函数接受一个函数指针或一个函数对象和一个元组，然后*解包*元组，以便调用以元组成员为参数的函数:\n\n```cpp\n          apply(print_student, john);\n          cout << \"-----n\";\n```\n\n8.  当然，这在循环中也很有效:\n\n```cpp\n          for (const auto &args : arguments_for_later) {\n              apply(print_student, args);\n          }\n          cout << \"-----n\";\n      }\n```\n\n9.  编译和运行程序表明这两种方法都有效，正如我们假设的那样:\n\n```cpp\n      $ ./apply_functions_on_tuples \n      Student \"John Doe\", ID: 123, GPA: 3.7\n      -----\n      Student \"John Doe\", ID: 234, GPA: 3.7\n      Student \"Billy Foo\", ID: 345, GPA: 4\n      Student \"Cathy Bar\", ID: 456, GPA: 3.5\n      -----\n      Student \"John Doe\", ID: 123, GPA: 3.7\n      -----\n      Student \"John Doe\", ID: 234, GPA: 3.7\n      Student \"Billy Foo\", ID: 345, GPA: 4\n      Student \"Cathy Bar\", ID: 456, GPA: 3.5\n      -----\n```\n\n# 它是如何工作的...\n\n`std::apply`是一个编译时助手，帮助我们对代码中处理的类型更加不可知。\n\n假设我们有一个值为`(123, \"abc\"s, 456.0)`的元组`t`。这个元组的类型是`tuple<int, string, double>`。另外，假设我们有一个带有签名`int f(int, string, double)`的函数`f`(类型也可以是引用)。\n\n然后，我们可以写`x = apply(f, t)`，这会产生一个函数调用，`x = f(123, \"abc\"s, 456.0)`。`apply`方法甚至还给我们`f`返回的东西。\n\n# 用 std::tuple 快速组合数据结构\n\n让我们看一下我们很可能已经知道的元组的基本用例。为了捆绑一些变量，我们可以定义如下结构:\n\n```cpp\nstruct Foo {\n    int a;\n    string b;\n    float c;\n};\n```\n\n我们也可以定义一个元组，而不是像前面的例子那样定义一个结构:\n\n```cpp\nusing Foo = tuple<int, string, float>;\n```\n\n我们可以使用类型列表中该类型的索引号来访问它的项目。为了访问元组的第一个成员`t`，我们可以使用`std::get<0>(t)`来访问我们编写的第二个成员`std::get<1>`，以此类推。如果索引号太大，那么编译器甚至会安全地出错。\n\n在整本书中，我们已经使用了 C++ 17 对元组的分解能力。它们允许我们通过编写`auto [a, b, c] = some_tuple`来快速分解一个元组，以便访问它的单个项目。\n\n组合和分解单个数据结构并不是我们对元组唯一能做的事情。我们还可以连接或拆分元组，或者做各种各样的魔术。在这个食谱中，我们将利用这些能力来学习如何做。\n\n# 怎么做...\n\n在这一节中，我们将编写一个可以动态打印任何元组的程序。除此之外，我们将编写一个函数，可以将*元组压缩在一起:*\n\n1.  我们需要首先包含一些头，然后声明我们默认使用`std`命名空间:\n\n```cpp\n      #include <iostream>\n      #include <tuple>\n      #include <list>\n      #include <utility>\n      #include <string>\n      #include <iterator>\n      #include <numeric>\n      #include <algorithm>      \n\n      using namespace std;\n```\n\n2.  因为我们将处理元组，所以显示它们的内容会很有趣。因此，我们现在将实现一个非常通用的函数，它可以打印任何由可打印类型组成的元组。该函数接受一个输出流引用`os`，它将用于进行实际打印，以及一个变量参数列表，它携带所有元组成员。我们将所有的参数分解为第一个元素，并将其放入参数`v`，其余的参数存储在参数包`vs...`中:\n\n```cpp\n      template <typename T, typename ... Ts>\n      void print_args(ostream &os, const T &v, const Ts &...vs)\n      {\n          os << v;\n```\n\n3.  如果参数包`vs`中还有参数，则使用`initializer_list`扩展技巧将这些参数与`\", \"`交错打印。您在[第 21 章](03.html)、*lambda 表达式*中了解到此技巧:\n\n```cpp\n          (void)initializer_list<int>{((os << \", \" << vs), 0)...};\n      }\n```\n\n4.  例如，我们现在可以通过编写`print_args(cout, 1, 2, \"foo\", 3, \"bar\")`来打印任意组参数。但是这和元组还没有关系。为了打印元组，我们通过实现在任何元组专门化上匹配的模板函数，为元组的任何情况重载流输出运算符`<<`:\n\n```cpp\n      template <typename ... Ts>\n      ostream& operator<<(ostream &os, const tuple<Ts...> &t)\n      {\n```\n\n5.  现在有点复杂了。我们首先使用一个任意接受许多参数的 lambda 表达式。无论何时调用它，它都会在这些参数前面加上`os`参数，然后用新的参数列表调用`print_args`。这意味着对`capt_tup(...some parameters...)`的呼叫会导致对`print_args(os, ...some parameters...)`的呼叫:\n\n```cpp\n          auto print_to_os ([&os](const auto &...xs) {\n              print_args(os, xs...);\n          });\n```\n\n6.  现在我们可以进行真正的元组解包魔术了。我们使用`std::apply`来解包元组。然后，所有值将从元组中取出，并作为函数的函数参数进行排列，我们提供函数作为第一个参数。这只是意味着，如果我们有一个元组，`t = (1, 2, 3)`，并调用`apply(capt_tup, t)`，那么这将导致一个函数调用，`capt_tup(1, 2, 3)`，这又导致函数调用，`print_args(os, 1, 2, 3)`。这正是我们所需要的。作为一个很好的补充，我们用括号将打印括起来:\n\n```cpp\n          os << \"(\";\n          apply(print_to_os, t);\n          return os << \")\";\n      }\n```\n\n7.  好了，现在我们写了一些复杂的代码，当我们想要打印一个元组时，这些代码会让我们的生活变得更容易。但是我们可以用元组做更多的事情。例如，让我们编写一个函数，接受一个可迭代的范围，比如一个向量或一个数字列表，作为参数。然后，该函数将在该范围内迭代，然后向我们返回该范围内所有数字的*和*，并将其与所有值的*最小值*、所有值的*最大值*以及它们的数值*平均值*捆绑在一起。通过将这四个值打包成一个元组，我们可以将它们作为单个对象返回，而无需定义额外的结构类型:\n\n```cpp\n      template <typename T>\n      tuple<double, double, double, double>\n      sum_min_max_avg(const T &range)\n      {\n```\n\n8.  `std::minmax_element`函数返回给我们一对迭代器，分别指向输入范围的最小值和最大值。`std::accumulate`方法汇总其输入范围内的所有值。这是我们返回适合元组的四个值所需要的全部内容！\n\n```cpp\n          auto min_max (minmax_element(begin(range), end(range)));\n          auto sum     (accumulate(begin(range), end(range), 0.0));\n          return {sum, *min_max.first, *min_max.second, \n                  sum / range.size()};\n      }\n```\n\n9.  在实现主程序之前，我们将实现最后一个神奇的助手函数。我称之为魔法是因为它一开始看起来确实很复杂，但是在了解了它的工作原理后，它会变成一个非常光滑和好的助手。它将压缩两个元组。这意味着如果我们给它一个元组`(1, 2, 3)`，和另一个元组`('a', 'b', 'c')`，它将返回一个元组`(1, 'a', 2, 'b', 3, 'c')`:\n\n```cpp\n      template <typename T1, typename T2>\n      static auto zip(const T1 &a, const T2 &b)\n      {\n```\n\n10.  现在我们得到了这个配方中最复杂的代码行。我们创建一个函数对象`z`，它接受任意数量的参数。然后，它返回另一个函数对象，该对象捕获参数包`xs`中的所有这些参数，但也接受任意数量的参数。让我们把这件事考虑一下。在这个内部函数对象中，我们可以访问参数包形式的参数列表`xs`和`ys`。现在让我们看看我们实际上是如何处理这些参数包的。表达式`make_tuple(xs, ys)...`按项目分组参数包。这意味着如果我们有`xs = 1, 2, 3`和`ys = 'a', 'b', 'c'`，这将产生一个新的参数包，`(1, 'a'), (2, 'b'), (3, 'c')`。这是一个逗号分隔的三元组列表。为了将它们全部分组到一个*元组中，我们使用`std::tuple_cat`，它接受任意数量的元组，并将它们重新打包成一个元组。这样我们得到了一个很好的`(1, 'a', 2, 'b', 3, 'c')`元组:*\n\n```cpp\n          auto z ([](auto ...xs) {\n              return [xs...](auto ...ys) {\n                  return tuple_cat(make_tuple(xs, ys) ...);\n              };\n          });\n```\n\n11.  最后一步是从输入元组`a`和`b`中展开所有的值，并将它们推入`z`。表达式`apply(z, a)`将来自`a`的所有值放入参数包`xs`，`apply(..., b)`将`b`的所有值放入参数包`ys`。得到的元组是大的压缩元组，我们将其返回给调用者:\n\n```cpp\n          return apply(apply(z, a), b);\n      }\n```\n\n12.  我们在助手/库代码中投入了大量的行。让我们现在终于把它投入使用。首先，我们构造一些任意元组。`student`包含学生的身份证、姓名和平均绩点。`student_desc`包含以人类可读形式描述这些字段含义的字符串。`std::make_tuple`是一个非常好的助手，因为它自动推导出所有参数的类型，并创建一个合适的元组类型:\n\n```cpp\n      int main()\n      {\n          auto student_desc (make_tuple(\"ID\", \"Name\", \"GPA\"));\n          auto student      (make_tuple(123456, \"John Doe\", 3.7));\n```\n\n13.  让我们打印我们所拥有的。这非常简单，因为我们刚刚为此实现了正确的`operator<<`重载:\n\n```cpp\n          cout << student_desc << 'n'\n               << student      << 'n';\n```\n\n14.  我们也可以用`std::tuple_cat`将元组动态分组，然后像这样打印出来:\n\n```cpp\n          cout << tuple_cat(student_desc, student) << 'n';\n```\n\n15.  我们还可以用我们的`zip`函数创建一个新的*压缩的*元组，并打印出来:\n\n```cpp\n          auto zipped (zip(student_desc, student));\n          cout << zipped << 'n';\n```\n\n16.  别忘了我们的`sum_min_max_avg`功能。我们创建一个包含一些数字的初始化列表，并将其输入到这个函数中。为了让它更复杂一点，我们创建了另一个同样大小的元组，其中包含一些描述字符串。通过压缩这些元组，我们得到了一个很好的交错输出，正如我们在运行程序时所看到的:\n\n```cpp\n          auto numbers = {0.0, 1.0, 2.0, 3.0, 4.0};\n          cout << zip(\n                  make_tuple(\"Sum\", \"Minimum\", \"Maximum\", \"Average\"),\n                  sum_min_max_avg(numbers))\n               << 'n';\n      }\n```\n\n17.  编译并运行程序会产生以下输出。前两行只是单独的`student`和`student_desc`元组。第 3 行是我们通过使用`tuple_cat`得到的元组合成。第 4 行包含压缩的学生元组。在最后一行，我们看到了上次创建的数值列表的总和、最小值、最大值和平均值。因为有了拉链，所以很容易看出每个价值意味着什么:\n\n```cpp\n      $ ./tuple\n      (ID, Name, GPA)\n      (123456, John Doe, 3.7)\n      (ID, Name, GPA, 123456, John Doe, 3.7)\n      (ID, 123456, Name, John Doe, GPA, 3.7)\n      (Sum, 10, Minimum, 0, Maximum, 4, Average, 2)\n```\n\n# 它是如何工作的...\n\n这一部分的一些代码确实很复杂。我们为元组编写了一个`operator<<`实现，它看起来非常复杂，但是支持所有类型的元组，元组本身由可打印类型组成。然后我们实现了`sum_min_max_avg`函数，它只返回一个元组。另一件非常复杂的事情是函数`zip`。\n\n最简单的部分是`sum_min_max_avg`。重点是，当我们定义一个返回实例`tuple<Foo`、`Bar`、`Baz> f()`的函数时，我们可以在那个函数中写`return {foo_instance, bar_instance, baz_instance};`来构造这样一个元组。如果你在理解我们在`sum_min_max_avg`函数中使用的 STL 算法时有困难，那么你可能想看一下本书的[第 22 章](04.html)、 *STL 算法基础*，我们已经在那里仔细看过了。\n\n另一个代码是如此复杂，以至于我们将特定的助手专用于他们自己的子部分:\n\n# 元组的运算符<<\n\n在我们接触输出流的`operator<<`之前，我们已经实现了`print_args`功能。由于其可变的参数性质，它接受任何数量和类型的参数，只要第一个是`ostream`实例:\n\n```cpp\ntemplate <typename T, typename ... Ts>\nvoid print_args(ostream &os, const T &v, const Ts &...vs)\n{\n    os << v;\n\n    (void)initializer_list<int>{((os << \", \" << vs), 0)...};\n}\n```\n\n该函数打印第一个项目`v`，然后打印参数包`vs`中的所有其他项目。我们单独打印第一个项目，因为我们希望所有项目都与`\", \"`交错，但我们不希望这个字符串在整个列表的前面或后面(如`\"1, 2, 3, \"`或`\", 1, 2, 3\"`)。我们在[第 21 章](03.html)、*lambda 表达式*中了解了`initializer_list`扩展技巧，在配方*中用同一个输入*调用多个功能。\n\n有了这个函数，我们就有了打印元组所需的一切。我们的`operator<<`实现如下:\n\n```cpp\ntemplate <typename ... Ts>\nostream& operator<<(ostream &os, const tuple<Ts...> &t)\n{\n    auto capt_tup ([&os](const auto &...xs) {\n        print_args(os, xs...);\n    });\n\n    os << \"(\";\n    apply(capt_tup, t);\n    return os << \")\";\n}\n```\n\n我们首先要做的是定义函数对象，`capt_tup`。当我们呼叫`capt_tup(foo, bar, whatever)`时，这导致呼叫，`print_args(**os,** foo, bar, whatever)`。这个函数对象唯一做的事情就是将输出流对象`os`添加到它的变量列表中。\n\n之后，我们使用`std::apply`来从元组`t`中解包所有项目。如果这一步看起来太复杂，请看看这一步之前的食谱，这是专门用来演示`std::apply`是如何工作的。\n\n# 元组的压缩函数\n\n`zip`函数接受二元组，但是看起来非常复杂，尽管它有一个非常简洁的实现:\n\n```cpp\ntemplate <typename T1, typename T2>\nauto zip(const T1 &a, const T2 &b)\n{\n    auto z ([](auto ...xs) {\n        return [xs...](auto ...ys) {\n            return tuple_cat(make_tuple(xs, ys) ...);\n        };\n    });\n    return apply(apply(z, a), b);\n}\n```\n\n为了更好地理解这段代码，假设元组`a`携带值`1, 2, 3`，元组`b`携带值`'a', 'b', 'c'`。\n\n在这种情况下，调用`apply(z, a)`会导致函数调用`z(1, 2, 3)`，该函数调用会返回另一个函数对象，该对象捕获参数包`xs`中的那些值`1, 2, 3`。当这个函数对象被`apply(z(1, 2, 3), b)`调用时，它得到值`'a', 'b', 'c'`，并被填入参数包`ys`。这和我们直接叫`z(1, 2, 3)('a', 'b', 'c')`基本一样。\n\n好了，现在我们有了`xs = (1, 2, 3)`和`ys = ('a', 'b', 'c')`，接下来会发生什么？`tuple_cat(make_tuple(xs, ys) ...)`这个表达有以下魔力:看一下图表:\n\n![](img/54ce6577-8405-4849-a5bb-40c6235b6d5b.png)\n\n首先，来自`xs`和`ys`的项目通过两两交错被压缩在一起。这种“成对交错”发生在`make_tuple(xs, ys) ...`表达式中。这最初只会产生一个包含两个条目的变量列表。为了得到*一个大的*元组，我们对它们应用`tuple_cat`，然后我们最终得到一个大的串联元组，它以交错的方式包含初始元组的所有成员。\n\n# 将 void*替换为 std::任何更多类型安全\n\n可能会发生这样的情况:我们希望在变量中存储任何*类型的项目。对于这样一个变量，我们需要能够检查它是否包含*任何东西*，如果包含，我们需要能够区分*它包含什么*。所有这些都需要以类型安全的方式进行。*\n\n过去，我们基本上能够在一个`void*`指针中存储指向各种对象的指针。仅仅一个`void`类型的指针不能告诉我们它指向什么样的对象，所以我们需要手工制作一些额外的机制来告诉我们应该期待什么。这样的代码很快会导致古怪的外观和不安全的代码。\n\nC++ 17 对 STL 的另一个补充是`std::any`类型。它被设计用来保存任何类型的变量，并提供能够进行类型安全检查和访问的设施。\n\n在这个食谱中，我们将玩这个实用类型，以获得它的感觉。\n\n# 怎么做...\n\n我们将实现一个功能，试图能够打印一切。它使用`std::any`作为参数类型:\n\n1.  首先，我们包括一些必要的头，并声明我们使用`std`命名空间:\n\n```cpp\n      #include <iostream>\n      #include <iomanip>\n      #include <list>\n      #include <any>\n      #include <iterator>     \n\n      using namespace std;\n```\n\n2.  为了减少以下程序中尖括号语法的数量，我们为`list<int>`定义了一个别名，我们稍后会用到:\n\n```cpp\n      using int_list = list<int>;\n```\n\n3.  让我们实现一个声称可以打印任何东西的函数。承诺是它以`std::any`变量的形式打印任何作为参数提供的内容:\n\n```cpp\n      void print_anything(const std::any &a)\n      {\n```\n\n4.  我们首先需要检查的是参数是否包含任何东西或者它是否只是一个空的`any`实例。如果它是空的，那么试图找出如何打印它是没有意义的:\n\n```cpp\n          if (!a.has_value()) {\n              cout << \"Nothing.n\";\n```\n\n5.  如果它不是空的，我们可以尝试将其与不同类型进行比较，直到看到匹配。首先要尝试的类型是`string`。如果是`string`，我们可以使用`std::any_cast`将`a`转换为`string`类型的引用，然后打印出来。出于表面原因，我们将字符串放在引号中:\n\n```cpp\n          } else if (a.type() == typeid(string)) {\n              cout << \"It's a string: \"\n                   << quoted(any_cast<const string&>(a)) << 'n';\n```\n\n6.  如果不是`string`，可能是`int`。如果此类型匹配，我们可以使用`any_cast<int>`获得实际的`int`值:\n\n```cpp\n          } else if (a.type() == typeid(int)) {\n              cout << \"It's an integer: \"\n                   << any_cast<int>(a) << 'n';\n```\n\n7.  `std::any`不仅适用于`string`和`int`这样的简单类型。我们还可以将整个地图或列表或任何组合的复杂数据结构放入`any`变量中。让我们看看输入是否是整数列表，如果是，我们可以像打印列表一样打印它:\n\n```cpp\n          } else if (a.type() == typeid(int_list)) {\n              const auto &l (any_cast<const int_list&>(a));\n\n              cout << \"It's a list: \";\n              copy(begin(l), end(l), \n                   ostream_iterator<int>{cout, \", \"});\n              cout << 'n';\n```\n\n8.  如果这些类型都不匹配，我们就没有类型猜测了。在这种情况下，让我们放弃，告诉用户我们不知道如何打印:\n\n```cpp\n          } else {\n              cout << \"Can't handle this item.n\";\n          }\n      }\n```\n\n9.  在主函数中，我们现在可以用任意类型调用这个函数。我们可以使用`{}`用一个空的`any`变量来调用它，或者用一个字符串`\"abc\"`或一个整数来填充它。因为`std::any`可以从这样的类型隐式构造，所以没有语法开销。我们甚至可以构造一个完整的列表，并将其放入这个函数中:\n\n```cpp\n      int main()\n      {\n          print_anything({});\n          print_anything(\"abc\"s);\n          print_anything(123);\n          print_anything(int_list{1, 2, 3});\n```\n\n10.  如果我们要将复制成本非常高的对象放入一个`any`变量，我们也可以执行一个*原位*构造。让我们用列表类型来试试这个。`in_place_type_t<int_list>{}`表达式是一个空对象，它给了`any`的构造函数足够的信息来知道我们要构造什么。第二个参数`{1, 2, 3}`只是一个初始化列表，它将被输入到`any`变量中嵌入的`int_list`进行构造。这样，我们可以避免不必要的复制或移动:\n\n```cpp\n          print_anything(any(in_place_type_t<int_list>{}, {1, 2, 3}));\n      }\n```\n\n11.  编译并运行程序会产生以下输出，这正是我们所期望的:\n\n```cpp\n      $ ./any \n      Nothing.\n      It's a string: \"abc\"\n      It's an integer: 123\n      It's a list: 1, 2, 3, \n      It's a list: 1, 2, 3, \n```\n\n# 它是如何工作的...\n\n`std::any`类型在一个方面与`std::optional`相似——它有一个`has_value()`方法来判断一个实例是否携带值。但除此之外，它可以包含字面上的*任何东西*，所以处理起来比`optional`更复杂。\n\n在访问一个`any`变量的内容之前，我们需要找出*它携带的*类型，然后*将*转换为该类型。\n\n通过比较`x.type() == typeid(T)`，可以确定`any`实例是否具有类型`T`值。如果这个比较结果是`true`，那么我们可以使用`any_cast`来获取内容。\n\n注意`any_cast<T>(x)`返回`x`内部`T`值的*副本*。如果我们想要一个*引用*为了避免复杂对象的复制，我们需要使用`any_cast<T&>(x)`。这就是我们在本节代码中访问内部`string`或`list<int>`对象时所做的。\n\nIf we cast an instance of `any` to the wrong type, it will throw an `std::bad_any_cast` exception.\n\n# 用 std::variant 存储不同类型\n\nC++ 中不仅有`struct`和`class`原语，让我们可以组成类型。如果我们想表达某个变量可以包含某个类型`A`或者某个类型`B`(或者`C`，或者其他什么)，我们可以使用`union`。联合的问题在于，它们不能告诉我们它们实际上被初始化为它们可以持有的类型。\n\n考虑以下代码:\n\n```cpp\nunion U { \n    int    a;\n    char  *b; \n    float  c;\n};\n\nvoid func(U u) { std::cout << u.b << 'n'; }\n```\n\n如果我们用一个通过成员`a`初始化为保存整数的并集调用`func`函数，没有什么能阻止我们访问它，就好像它被初始化为通过成员`b`存储一个指向字符串的指针一样。各种各样的错误都可能从这样的代码中传播出来。在我们开始用一个辅助变量来打包我们的联合之前，为了获得一些安全性，我们可以直接使用 C++ 17 附带的`std::variant`。\n\n`variant`是一种*新学校*，类型安全，高效的联合类型。它不使用堆，因此与基于联合的手工解决方案一样节省空间和时间，因此我们不必自己实现它。它可以存储除引用、数组或`void`类型之外的任何内容。\n\n在这个食谱中，我们将构建一个受益于`variant`的例子，以便了解如何使用 STL 的这个酷的新添加。\n\n# 怎么做...\n\n让我们实现一个知道类型`cat`和`dog`的程序，它存储了猫和狗的混合列表，而不使用任何运行时多态:\n\n1.  首先，我们包括所有需要的头，并定义我们使用`std`命名空间:\n\n```cpp\n      #include <iostream>\n      #include <variant>\n      #include <list>\n      #include <string>\n      #include <algorithm>      \n\n      using namespace std;\n```\n\n2.  接下来，我们实现两个具有相似功能但不以任何其他方式相互关联的类，这与例如从相同接口或相似接口继承的类形成对比。第一类是`cat`。一个`cat`对象有名字可以说*喵*:\n\n```cpp\n      class cat {\n          string name;\n\n      public:\n          cat(string n) : name{n} {}\n\n          void meow() const {\n              cout << name << \" says Meow!n\";\n          }\n      };\n```\n\n3.  另一类是`dog`。一`dog`对象当然不是说*喵*而是*汪汪*:\n\n```cpp\n      class dog {\n          string name;\n\n      public:\n          dog(string n) : name{n} {}\n\n          void woof() const {\n              cout << name << \" says Woof!n\";\n          }\n      };\n```\n\n4.  现在我们可以定义一个`animal`类型，它只是`std::variant<dog, cat>`的一个类型别名。这基本上与老牌工会相同，但具有`variant`提供的所有额外功能:\n\n```cpp\n      using animal = variant<dog, cat>;\n```\n\n5.  在编写主程序之前，我们先实现两个助手。一个助手是动物谓词。通过调用`is_type<cat>(...)`或`is_type<dog>(...)`，我们可以找出动物变体实例是持有`cat`还是`dog`。该实现只调用`holds_alternative`，这是变体类型的通用谓词函数:\n\n```cpp\n      template <typename T>\n      bool is_type(const animal &a) {\n          return holds_alternative<T>(a);\n      }\n```\n\n6.  第二个助手是一个充当函数对象的结构。它是一个双重功能对象，因为它实现了两次`operator()`。一种实现是接受狗的重载，另一种是接受猫的重载。对于这些类型，它只调用`woof`或`meow`函数:\n\n```cpp\n      struct animal_voice\n      {\n          void operator()(const dog &d) const { d.woof(); }\n          void operator()(const cat &c) const { c.meow(); }\n      };\n```\n\n7.  让我们使用这些类型和助手。首先，我们定义一个`animal`变体实例列表，并用猫和狗填充它:\n\n```cpp\n      int main()\n      {\n          list<animal> l {cat{\"Tuba\"}, dog{\"Balou\"}, cat{\"Bobby\"}};\n```\n\n8.  现在，我们打印列表的内容三次，每次打印的方式不同。一种方法是使用`variant::index()`。因为`animal`是`variant<dog, cat>`的别名，`0`的返回值意味着该变量持有一个`dog`实例。索引`1`表示是一个`cat`。变体专门化中类型的顺序是这里的关键。在开关盒块中，我们访问带有`get<T>`的变体，以便获得内部的实际`cat`或`dog`实例:\n\n```cpp\n          for (const animal &a : l) {\n              switch (a.index()) {\n              case 0: \n                  get<dog>(a).woof();\n                  break;\n              case 1:\n                  get<cat>(a).meow();\n                  break;\n              }\n          }\n          cout << \"-----n\";\n```\n\n9.  除了使用类型的数字索引，我们还可以显式地要求每个类型。`get_if<dog>`返回一个指向内部`dog`实例的`dog`类型的指针。如果里面没有`dog`实例，那么指针就是`null`。这样，我们可以尝试不同的类型，直到我们最终成功:\n\n```cpp\n          for (const animal &a : l) {\n              if (const auto d (get_if<dog>(&a)); d) {\n                  d->woof();\n              } else if (const auto c (get_if<cat>(&a)); c) {\n                  c->meow();\n              }\n          }\n          cout << \"-----n\";\n```\n\n10.  最后也是最优雅的方式是`variant::visit`。这个函数接受一个函数对象和一个变量实例。函数对象必须为变量可以容纳的所有可能类型实现不同的重载。我们之前实现了一个带有正确`operator()`重载的结构，所以我们可以在这里使用它:\n\n```cpp\n          for (const animal &a : l) {\n              visit(animal_voice{}, a);\n          }\n          cout << \"-----n\";\n```\n\n11.  最后，我们将统计变体列表中的猫和狗的数量。`is_type<T>`谓词可以在`cat`和`dog`上专门化，然后可以与`std::count_if`结合使用，向我们返回这种类型的实例数量:\n\n```cpp\n          cout << \"There are \"\n               << count_if(begin(l), end(l), is_type<cat>)\n               << \" cats and \"\n               << count_if(begin(l), end(l), is_type<dog>)\n               << \" dogs in the list.n\";\n      }\n```\n\n12.  首先编译并运行该程序，产生三次打印的相同列表。在这之后，我们看到`is_type`谓词与`count_if`结合起来工作得很好:\n\n```cpp\n      $ ./variant \n      Tuba says Meow!\n      Balou says Woof!\n      Bobby says Meow!\n      -----\n      Tuba says Meow!\n      Balou says Woof!\n      Bobby says Meow!\n      -----\n      Tuba says Meow!\n      Balou says Woof!\n      Bobby says Meow!\n      -----\n      There are 2 cats and 1 dogs in the list.\n```\n\n# 它是如何工作的...\n\n`std::variant`类型有点类似于`std::any`，因为两者都可以保存不同类型的对象，我们需要在运行时区分它们到底保存了什么，然后才能尝试访问它们的内容。\n\n另一方面，`std::variant`与`std::any`的不同之处在于，我们必须以模板类型列表的形式声明它应该能够存储什么。`std::variant<A, B, C>` *的一个实例必须包含一个类型为`A`、`B`或`C`的实例。不存在持有*不持有*的可能性，这意味着`std::variant`没有*期权性*的概念。*\n\n类型的变体`variant<A, B, C>`模拟了一个联合类型，如下所示:\n\n```cpp\nunion U {\n    A a;\n    B b;\n    C c;\n};\n```\n\n联合的问题在于，我们需要构建自己的机制来区分它是用`A`、`B`还是`C`变量初始化的。`std::variant`型可以为我们做到这一点，没有太多的麻烦。\n\n在本节的代码中，我们使用了三种不同的方法来处理变量的内容。\n\n第一种方式是`variant`的`index()`功能。对于变体类型`variant<A, B, C>`，如果其被初始化为持有`A`类型，则可以返回索引`0`，或者对于`B`可以返回`1`，对于`C`可以返回`2`，对于更复杂的变体则可以以此类推。\n\n接下来是`get_if<T>`功能。它接受一个变量对象的地址，并返回一个指向其内容的`T`类型的指针。如果`T`类型错误，那么这个指针将是`null`指针。也可以在变量上调用`get<T>(x)`来获取对其内容的引用，但是如果没有成功，该函数将抛出一个异常(在执行这样的`get`-强制转换之前，可以使用布尔谓词`holds_alternative<T>(x)`来检查正确的类型)。\n\n访问变体的最后一种方式是`std::visit`功能。它接受一个函数对象和一个`variant`实例。`visit`函数然后检查变量的内容是哪种类型，然后调用函数对象的右`operator()`重载。\n\n正是为了这个目的，我们实现了`animal_voice`类型，因为它可以与`visit`和`variant<dog, cat>`结合使用:\n\n```cpp\nstruct animal_voice\n{\n    void operator()(const dog &d) const { d.woof(); }\n    void operator()(const cat &c) const { c.meow(); }\n};\n```\n\n`visit`-访问变体的方式可以被认为是最优雅的方式，因为实际访问变体的代码段不需要被硬编码为变体可以持有的类型。这使得我们的代码更容易扩展。\n\nThe claim that a `variant` type cannot hold *no* value was not completely true. By adding the `std::monostate` type to its type list, it can indeed be initialized to hold *no* value.\n\n# 自动处理带有标准::唯一 _ptr 的资源\n\n自 C++ 11 以来，STL 提供了智能指针，真正有助于跟踪动态内存及其处理。甚至在 C++ 11 之前，就有一个名为`auto_ptr`的类已经可以进行自动内存处置，但是很容易用错方式。\n\n但是有了 C++ 11 代智能指针，我们很少需要自己写`new`和`delete`，这确实是一件好事。智能指针是自动内存管理的一个闪亮的例子。如果我们用`unique_ptr`来维护动态分配的对象，我们基本上不会发生内存泄漏，因为一旦它被破坏，这个类会自动调用它所维护的对象上的`delete`。\n\n一个唯一的指针表示它所指向的对象的所有权，并遵循它的职责，即如果不再使用它，则再次释放它的内存。这个类有可能永远解除我们的内存泄漏(至少和它的同伴`shared_ptr`和`weak_ptr`一起，但是在这个食谱中，我们只专注于`unique_ptr`)。最好的一点是，与带有原始指针和手动内存管理的代码相比，它没有给空间和运行时性能带来额外开销*。(好吧，在破坏它所指向的对象后，它仍然在内部将其内部原始指针设置为`nullptr`，这不能总是被优化掉。不过，大多数管理动态内存的手动编写的代码也是如此。)*\n\n *在这个食谱中，我们将看一看`unique_ptr`以及如何使用它。\n\n# 怎么做...\n\n我们将编写一个程序，向我们展示`unique_ptr`如何通过创建一个自定义类型来处理内存，该自定义类型在构建和销毁内存时添加一些调试消息。然后，我们将使用独特的指针，维护动态分配的实例:\n\n1.  首先，我们包含必要的头，并声明我们使用`std`命名空间:\n\n```cpp\n      #include <iostream>\n      #include <memory>  \n\n      using namespace std;\n```\n\n2.  我们将使用`unique_ptr`为我们要管理的对象实现一个小类。它的构造函数和析构函数打印到终端，所以我们可以看到后来它实际上被自动删除的时候:\n\n```cpp\n      class Foo\n      {\n      public:\n          string name;\n\n          Foo(string n)\n              : name{move(n)}\n          { cout << \"CTOR \" << name << 'n'; }\n\n          ~Foo() { cout << \"DTOR \" << name << 'n'; }\n      };\n```\n\n3.  为了了解接受唯一指针作为参数的函数有什么限制，我们只实现了一个。它通过打印名称来处理“T2”Foo 项目。请注意，虽然独特的指针是智能的、无开销的、舒适安全的，但它们仍然可以是`null`。这意味着在取消引用它们之前，我们仍然必须检查它们:\n\n```cpp\n      void process_item(unique_ptr<Foo> p)\n      {\n          if (!p) { return; }\n\n          cout << \"Processing \" << p->name << 'n';\n      }\n```\n\n4.  在主函数中，我们将打开另一个作用域，在堆上创建两个`Foo`对象，并用唯一的指针管理这两个对象。我们使用`new`运算符在堆上显式创建第一个，然后将其放入`unique_ptr<Foo>`变量`p1`的构造函数中。我们通过用参数调用`make_unique<Foo>`来创建唯一的指针`p2`，否则我们将直接给出`Foo`的构造函数。这是更优雅的方式，因为我们可以使用自动类型扣除，并且我们第一次可以访问对象时，它已经由`unique_ptr`管理:\n\n```cpp\n      int main()\n      {\n          {\n              unique_ptr<Foo> p1 {new Foo{\"foo\"}};\n              auto            p2 (make_unique<Foo>(\"bar\"));\n          }\n```\n\n5.  在我们离开作用域之后，这两个对象都被立即析构，并且它们的内存被释放到堆中。现在我们来看看`process_item`功能以及如何和`unique_ptr`一起使用。如果我们构造一个新的`Foo`实例，由函数调用中的`unique_ptr`管理，那么它的生命期就缩减到函数的范围内。`process_item`返回时，物体被摧毁:\n\n```cpp\n          process_item(make_unique<Foo>(\"foo1\"));\n```\n\n6.  如果我们想用一个在调用之前已经存在的对象调用`process_item`，那么我们需要*转移所有权*，因为那个函数按值取一个`unique_ptr`，这意味着调用它会导致一个副本。但是`unique_ptr`不能复制，只能*移动*。让我们创建两个新的`Foo`对象，并将其中一个移动到`process_item`中。通过后面查看终端输出，我们会看到`foo2`在`process_item`返回时被破坏，因为我们将所有权转移给了它。`foo3`将继续生存，直到主功能恢复:\n\n```cpp\n          auto p1 (make_unique<Foo>(\"foo2\"));\n          auto p2 (make_unique<Foo>(\"foo3\"));\n\n          process_item(move(p1));\n\n          cout << \"End of main()n\";\n      }\n```\n\n7.  让我们编译并运行这个程序。首先我们看到`foo`和`bar`的构造函数和析构函数调用。它们确实在程序离开附加范围后被销毁了。请注意，对象的销毁顺序与其创建顺序相反。下一个构造函数行来自`foo1`，这是我们在`process_item`调用期间创建的项目。它确实在函数调用后立即被销毁。然后我们创造了`foo2`和`foo3`。`foo2`在我们转移所有权的`process_item`呼叫后立即被销毁。相比之下，另一项`foo3`在主功能最后一行代码后被销毁:\n\n```cpp\n      $ ./unique_ptr \n      CTOR foo\n      CTOR bar\n      DTOR bar\n      DTOR foo\n      CTOR foo1\n      Processing foo1\n      DTOR foo1\n      CTOR foo2\n      CTOR foo3\n      Processing foo2\n      DTOR foo2\n      End of main()\n      DTOR foo3\n```\n\n# 它是如何工作的...\n\n用`std::unique_ptr`处理堆对象真的很简单。在我们初始化了一个唯一的指针来保存指向某个对象的指针之后，就没有办法了我们可以不小心*忘记*在某个代码路径上删除它。\n\n如果我们给一个唯一的指针分配一些新的指针，那么它将总是首先删除它所指向的旧对象，然后存储新指针。在一个唯一的指针变量`x`上，我们也可以调用`x.reset()`直接删除它所指向的对象，而不需要分配新的指针。通过`x = new_pointer`重新分配的另一个等效替代方案是`x.reset(new_pointer)`。\n\nThere is indeed one single way to release an object from the management of `unique_ptr` without deleting it. The `release` function does that, but using this function is not advisable in most situations.\n\n由于指针在实际被取消引用之前需要被检查，因此它们以一种能够模仿原始指针的方式重载正确的运算符。像`if (p) {...}`和`if (p != nullptr) {...}`这样的条件执行方式与我们检查原始指针的方式相同。\n\n取消唯一指针的引用可以通过`get()`函数来完成，该函数返回一个指向可以取消引用的对象的原始指针，或者直接通过`operator*`来完成，该函数再次模拟原始指针。\n\n`unique_ptr`的一个重要特点是其实例不能被*复制*而是可以被*从一个`unique_ptr`变量移动到另一个*变量。这就是为什么我们必须将现有的唯一指针移入`process_item`函数。如果我们能够复制一个唯一的指针，那么这将意味着被指向的对象被两个唯一的指针所拥有，尽管这与一个唯一的指针的设计相矛盾，该指针是底层对象的唯一的所有者。\n\nSince there are data structures, such as `unique_ptr` and `shared_ptr`, there is rarely any reason to create heap objects directly with `new` and `delete` them manually. Use such classes wherever you can! Especially `unique_ptr` imposes *no* overhead at runtime.\n\n# 使用 std::shared_ptr 自动处理共享堆内存\n\n在上一个食谱中，我们学习了如何使用`unique_ptr`。这是一个非常有用和重要的类，因为它帮助我们管理动态分配的对象。但是，它只能处理*单*的所有权。不可能让*多个*对象拥有同一个动态分配的对象，因为这样就不清楚以后谁必须删除它。\n\n指针类型`shared_ptr`就是专门为这种情况设计的。共享指针可以经常被*任意复制*。内部引用计数机制跟踪有多少对象仍然保持指向有效负载对象的指针。只有最后一个超出范围的共享指针才会调用有效负载对象上的`delete`。这样，我们可以确保不会出现内存泄漏，因为对象在使用后会被自动删除。同时，我们可以确定它们不会被删除得太早，或者太频繁(每个创建的对象都必须只被删除一次*)。*\n\n *在本食谱中，您将学习如何使用`shared_ptr`自动管理多个所有者之间共享的动态对象，并查看与`unique_ptr`相比有何不同:\n\n# 怎么做...\n\n为了深入了解`shared_ptr`的用法和原理，我们将编写一个类似于`unique_ptr`食谱中编写的程序:\n\n1.  首先，我们只包含必要的头，并声明我们默认使用`std`命名空间:\n\n```cpp\n      #include <iostream>\n      #include <memory>      \n\n      using namespace std;\n```\n\n2.  然后我们定义一个小助手类，它帮助我们看到它的实例实际上是何时被创建和销毁的。我们将使用`shared_ptr`管理它的实例:\n\n```cpp\n      class Foo\n      {\n      public:\n          string name;\n\n          Foo(string n)\n              : name{move(n)}\n          { cout << \"CTOR \" << name << 'n'; }\n\n          ~Foo() { cout << \"DTOR \" << name << 'n'; }\n      };\n```\n\n3.  接下来，我们实现一个函数，该函数通过值获取指向`Foo`实例*的共享指针。通过值接受共享指针作为参数比通过引用接受它们更有趣，因为在这种情况下，它们需要被复制，这将改变它们的内部引用计数器，正如我们将看到的:*\n\n```cpp\n      void f(shared_ptr<Foo> sp)\n      {\n          cout << \"f: use counter at \" \n               << sp.use_count() << 'n';\n      }\n```\n\n4.  在主函数中，我们声明了一个空的共享指针。默认情况下，它实际上是一个`null`指针:\n\n```cpp\n      int main()\n      {\n          shared_ptr<Foo> fa;\n```\n\n5.  接下来，我们打开另一个范围并实例化两个`Foo`对象。我们使用`new`操作符创建第一个，然后将其输入到新的`shared_ptr`的构造器中。然后我们使用`make_shared<Foo>`创建第二个实例，它根据我们提供的参数创建一个`Foo`实例。这是更优雅的方法，因为我们可以使用自动类型演绎，当我们有机会第一次访问它时，对象已经被管理了。这一点与`unique_ptr`食谱非常相似:\n\n```cpp\n          {\n              cout << \"Inner scope beginn\";\n\n              shared_ptr<Foo> f1 {new Foo{\"foo\"}};\n              auto            f2 (make_shared<Foo>(\"bar\"));\n```\n\n6.  因为共享指针可以共享，所以它们需要跟踪有多少方共享它们。这可以通过内部参考计数器或*使用*计数器来完成。我们可以使用`use_count`打印它的值。此时的值正好是`1`，因为我们还没有复制。我们可以将`f1`复制到`fa`，这样就增加了`2`的使用计数器。\n\n```cpp\n              cout << \"f1's use counter at \" << f1.use_count() << 'n';\n              fa = f1;\n              cout << \"f1's use counter at \" << f1.use_count() << 'n';\n```\n\n7.  当我们离开作用域时，共享指针`f1`和`f2`被销毁。`f1`变量的引用计数器再次递减到`1`，使得`fa`成为`Foo`实例的唯一所有者。当`f2`被破坏时，其参考计数器递减至`0`。在这种情况下，`shared_ptr`指针的析构函数将在该对象上调用`delete`，这将释放它:\n\n```cpp\n          }\n          cout << \"Back to outer scopen\";\n\n          cout << fa.use_count() << 'n';\n```\n\n8.  现在，让我们以两种不同的方式用共享指针调用`f`函数。一开始，我们通过抄袭`fa`天真地称之为。`f`功能将打印参考计数器具有值`2`。在对`f`的第二次调用中，我们将指针移动到函数中。这使得`f`成为对象的唯一拥有者:\n\n```cpp\n          cout << \"first f() calln\";\n          f(fa);\n          cout << \"second f() calln\";\n          f(move(fa));\n```\n\n9.  在`f`被返回后，`Foo`实例立即被销毁，因为我们不再拥有它的所有权。因此，当主函数返回时，所有对象已经被销毁:\n\n```cpp\n          cout << \"end of main()n\";\n      }\n```\n\n10.  编译并运行程序会产生以下输出。一开始，我们看到`\"foo\"`和`\"bar\"`被创造出来。我们复制`f1`(指向`\"foo\"`)后，它的参考计数器增加到`2`。离开作用域时，`\"bar\"`被销毁，因为作为销毁对象的共享指针是唯一的所有者。输出中的单`1`是`fa`的参考计数，现在是`\"foo\"`的唯一拥有者。之后，我们调用了两次函数`f`。第一次调用，我们把`fa`复制到里面，这又给了它一个`2`的引用计数器。在第二次调用时，我们将其移入`f`，这并没有改变其引用计数器。而且，由于此时`f`是`\"foo\"`的唯一拥有者，所以在`f`离开范围后，该物件立即被销毁。这样，在`main`最后一行打印后，不会破坏其他堆对象:\n\n```cpp\n      $ ./shared_ptr\n      Inner scope begin\n      CTOR foo\n      CTOR bar\n      f1's use counter at 1\n      f1's use counter at 2\n      DTOR bar\n      Back to outer scope\n      1\n      first f() call\n      f: use counter at 2\n      second f() call\n      f: use counter at 1\n      DTOR foo\n      end of main()\n```\n\n# 它是如何工作的...\n\n在构造和删除对象时，`shared_ptr`的工作方式基本类似于`unique_ptr`。构建共享指针的工作方式类似于创建唯一指针(尽管有一个函数`make_shared`创建共享对象作为`unique_ptr`指针的`make_unique`函数的挂件)。\n\n与`unique_ptr`的主要区别在于我们可以复制`shared_ptr`实例，因为共享指针与它们管理的对象一起维护一个所谓的*控制块*。控制块包含一个指向有效载荷对象的指针和一个参考计数器或*使用*计数器。如果有`N`数量的`shared_ptr`实例指向该对象，那么使用计数器也有值`N`。每当`shared_ptr`实例被析构时，它的析构器递减这个内部使用计数器。指向这样一个对象的最后一个共享指针将在其销毁期间达到使用计数器递减到`0`的条件。这就是共享指针实例，它调用有效载荷对象上的`delete`操作符！这样，我们不可能遭受内存泄漏，因为对象的使用计数是自动跟踪的。\n\n为了更详细地说明这一点，让我们看一下下图:\n\n![](img/4d305c80-4368-4efd-945c-8a4debac7d23.png)\n\n在步骤 1 中，我们有两个`shared_ptr`实例来管理类型为`Foo`的对象。使用计数器值`2`。然后，`shared_ptr2`被销毁，使用计数器递减到`1`。`Foo`实例尚未销毁，因为还有另一个共享指针。在步骤 3 中，最后一个共享指针也被销毁。这导致使用计数器递减到`0`。步骤 4 紧接在步骤 3 之后。控制块和`Foo`的实例都被销毁，它们的内存被释放到堆中。\n\n配备`shared_ptr`和`unique_ptr`，我们可以自动处理大部分动态分配的对象，再也不用担心内存泄露了。然而，有一个重要的注意事项需要考虑——假设我们在堆上有两个对象，它们包含指向彼此的共享指针，而其他一些共享指针从其他地方指向其中一个对象。如果该外部共享指针超出范围，那么两个对象仍然具有使用的计数器，其值*非零*，因为它们彼此引用*。这会导致*内存泄漏*。在这种情况下不应该使用共享指针，因为这样的循环引用链会阻止这样的对象的使用计数器到达`0`。*\n\n *# 还有更多...\n\n请看下面的代码。如果你被告知它包含潜在的*内存泄漏*怎么办？\n\n```cpp\nvoid function(shared_ptr<A>, shared_ptr<B>, int);\n// \"function\" is defined somewhere else\n\n// ...somewhere later in the code:\nfunction(new A{}, new B{}, other_function());\n```\n\n“内存泄漏在哪里？”有人可能会问，因为新分配的对象`A`和`B`会立即被输入到`shared_ptr`类型中，*然后是*类型，所以我们不会出现内存泄漏。\n\n是的，的确，一旦在`shared_ptr`实例中捕捉到指针，我们就不会发生内存泄漏。这个问题有点难以理解。\n\n当我们调用函数`f(x(), y(), z())`时，编译器需要先汇编调用`x()`、`y()`和`z()`的代码，这样它就可以将它们的返回值转发给`f`。结合之前的例子，让我们非常不好的是，编译器可以以任何顺序执行这些对`x`、`y`和`z`的函数调用。\n\n回过头来看这个例子，如果编译器决定先调用`new A{}`，再调用`other_function()`，然后调用`new B{}`，最后将这些函数的结果送入`function`的方式来构造代码，会发生什么？如果`other_function()`抛出一个异常，我们会得到一个内存泄漏，因为我们在堆上还有一个非托管对象`A`，因为我们刚刚分配了它，但是没有机会把它交给`shared_ptr`的管理。不管我们怎么抓到异常，对象的句柄都是*没了*，我们*不能删除*它！\n\n有两种简单的方法可以避免这个问题:\n\n```cpp\n// 1.)\nfunction(make_shared<A>(), make_shared<B>(), other_function());\n\n// 2.)\nshared_ptr<A> ap {new A{}};\nshared_ptr<B> bp {new B{}};\nfunction(ap, bp, other_function());\n```\n\n这样，对象就已经被`shared_ptr`管理了，不管之后谁抛出什么异常。\n\n# 处理指向共享对象的弱指针\n\n在关于`shared_ptr`的食谱中，我们了解到共享指针是多么有用和容易使用。与`unique_ptr`一起，它们为需要管理动态分配对象的代码带来了宝贵的改进。\n\n每当我们复制`shared_ptr`时，我们递增它的内部参考计数器。只要我们持有共享指针副本，被指向的对象就不会被删除。但是，如果我们想要某种*弱*指针，这种指针能让我们只要物体存在就能找到它，但不能阻止它的毁灭，那该怎么办？那么，我们如何确定这个物体是否仍然存在呢？\n\n在这种情况下，`weak_ptr`就是我们的同伴。使用起来比`unique_ptr`和`shared_ptr`稍微复杂一点，但是按照这个食谱，我们就可以使用了。\n\n# 怎么做...\n\n我们将实现一个用`shared_ptr`实例维护对象的程序，然后，我们混合在`weak_ptr`中，看看这如何改变智能指针内存处理的行为:\n\n1.  首先，我们包含必要的头，并声明我们默认使用`std`命名空间:\n\n```cpp\n      #include <iostream>\n      #include <iomanip>\n      #include <memory>      \n\n      using namespace std;\n```\n\n2.  接下来，我们实现一个类，该类在其析构函数实现中打印一条消息。这样，我们可以简单地在程序输出中检查一个项目何时被实际销毁:\n\n```cpp\n      struct Foo {\n          int value;\n\n          Foo(int i) : value{i} {}\n          ~Foo() { cout << \"DTOR Foo \" << value << 'n'; }\n      };\n```\n\n3.  让我们实现一个打印弱指针信息的函数，这样我们就可以在程序的不同点打印弱指针的状态。`weak_ptr`的`expired`功能告诉我们它所指向的对象是否还真的存在，因为持有一个指向对象的弱指针并不能延长它的寿命！`use_count`计数器告诉我们当前有多少`shared_ptr`实例指向有问题的对象:\n\n```cpp\n      void weak_ptr_info(const weak_ptr<Foo> &p)\n      {\n          cout << \"---------\" << boolalpha\n               << \"nexpired:   \" << p.expired()\n               << \"nuse_count: \" << p.use_count()\n               << \"ncontent:   \";\n```\n\n4.  如果要访问实际对象，需要调用`lock`函数。它向我们返回一个指向该对象的共享指针。如果对象*不再存在*，那么我们从中获得的共享指针实际上就是一个`null`指针。我们需要检查它，然后我们就可以访问它:\n\n```cpp\n          if (const auto sp (p.lock()); sp) {\n              cout << sp->value << 'n';\n          } else {\n              cout << \"<null>n\";\n          }\n      }\n```\n\n5.  让我们在主函数中实例化一个空的弱指针，并打印它的内容，当然，首先是空的:\n\n```cpp\n      int main()\n      {\n          weak_ptr<Foo> weak_foo;\n\n          weak_ptr_info(weak_foo);\n```\n\n6.  在一个新的作用域中，我们用`Foo`类的一个新实例来实例化一个新的共享指针。然后我们将其复制到弱指针。请注意，这不会增加共享指针的引用计数。引用计数器是`1`，因为只有一个*共享*指针:\n\n```cpp\n          {\n              auto shared_foo (make_shared<Foo>(1337));\n              weak_foo = shared_foo;\n```\n\n7.  让我们在*离开*作用域之前调用弱指针函数，同样，在离开作用域之后调用*。`Foo`实例应该立即被销毁，*虽然*有一个弱指针指向它:*\n\n```cpp\n              weak_ptr_info(weak_foo);\n          }\n\n          weak_ptr_info(weak_foo);\n      }\n```\n\n8.  编译和运行程序产生的输出是`weak_ptr_info`函数的三倍。在第一次调用中，弱指针为空。在第二次调用中，它已经指向我们创建的`Foo`实例，并且能够在*锁定*实例后取消引用它。在第三次调用之前，我们离开内部范围，这将触发`Foo`实例的析构函数，正如我们所期望的那样。之后，再也无法通过弱指针获取被删除的`Foo`项的内容，弱指针正确识别其已过期:\n\n```cpp\n      $ ./weak_ptr \n      ---------\n      expired:   true\n      use_count: 0\n      content:   <null>\n      ---------\n      expired:   false\n      use_count: 1\n      content:   1337\n      DTOR Foo 1337\n      ---------\n      expired:   true\n      use_count: 0\n      content:   <null>\n```\n\n# 它是如何工作的...\n\n弱指针为我们提供了一种指向由共享指针维护的对象而不增加其使用计数器的方法。好的，一个原始指针可以做同样的事情，但是一个原始指针不能告诉我们它是否悬空。弱指针可以！\n\n为了理解弱指针作为共享指针的补充是如何工作的，让我们直接跳到一个图解:\n\n![](img/583fb0b1-7b58-4bab-ac2a-41fa81b5a685.png)\n\n流程类似于配方中关于共享指针的图表。在步骤 1 中，我们有两个共享指针和一个指向类型为`Foo`的对象的弱指针。虽然有三个对象指向它，但只有共享指针操作它的使用计数器，这就是为什么它有值`2`。弱指针只操纵控制块的*弱计数器*。在步骤 2 和 3 中，共享指针实例被销毁，这逐步导致`0`的使用计数器。在步骤 4 中，这导致`Foo`对象被删除，但是控制块*停留在*那里。弱指针仍然需要控制块来区分它是否摆动。只有当仍然指向控制块*的最后一个* *弱*指针也超出范围时，控制块才会被删除。\n\n我们也可以说一个悬空的弱指针已经*过期了*。为了检查这个属性，我们可以询问`weak_ptr`指针的`expired`方法，该方法返回一个布尔值。如果是`true`，那么我们就不能取消引用弱指针，因为不再有对象可以取消引用了。\n\n为了取消对弱指针的引用，我们需要调用`lock()`。这既安全又方便，因为这个函数会返回一个共享指针。只要我们持有这个共享指针，它后面的对象就不会消失，因为我们通过锁定它来增加使用计数器。如果对象在`lock()`调用前不久被删除，那么它返回的共享指针实际上是一个`null`指针。\n\n# 使用智能指针简化遗留 API 的资源处理\n\n智能指针(`unique_ptr`、`shared_ptr`、`weak_ptr`)非常有用，一般来说，可以有把握地说，程序员应该*总是*使用这些指针，而不是手动分配和释放内存。\n\n但是如果不能使用`new`操作符分配对象和/或不能使用`delete`再次释放对象怎么办？许多遗留库自带分配/销毁功能。这似乎是一个问题，因为我们了解到智能指针依赖于`new`和`delete`。如果特定类型对象的创建和/或销毁依赖于特定工厂函数的 deleter 接口，这是否会阻止我们获得智能指针的巨大好处？\n\n一点也不。在这个配方中，我们将看到，我们只需要对智能指针执行非常少的定制，就可以让它们遵循特定的过程来分配和销毁特定的对象。\n\n# 怎么做...\n\n在本节中，我们将定义一个不能直接用`new`分配的类型，也不能使用`delete`再次释放。由于这阻止了它直接与智能指针一起使用，我们对`unique_ptr`和`smart_ptr`的实例进行了必要的小修改:\n\n1.  像往常一样，我们首先包含必要的头，并声明我们默认使用`std`命名空间:\n\n```cpp\n      #include <iostream>\n      #include <memory>\n      #include <string>      \n\n      using namespace std;\n```\n\n2.  接下来，我们声明一个类，它的构造函数和析构函数声明为`private`。这样，我们模拟了这样一个问题，即我们必须访问特定的函数来创建和销毁它的实例:\n\n```cpp\n      class Foo\n      {\n          string name;\n\n          Foo(string n)\n              : name{n}\n          { cout << \"CTOR \" << name << 'n'; }\n\n          ~Foo() { cout << \"DTOR \" << name << 'n';}\n```\n\n3.  静态方法`create_foo`和`destroy_foo`，然后创建并销毁`Foo`实例。他们使用原始指针。这模拟了一个遗留 C API 的情况，它阻止我们直接用普通的`shared_ptr`指针来使用它们:\n\n```cpp\n      public:\n          static Foo* create_foo(string s) { \n             return new Foo{move(s)};\n          }\n\n          static void destroy_foo(Foo *p) { delete p; }\n      };\n```\n\n4.  现在，让我们通过`shared_ptr`来管理这样的对象。当然，我们可以将从`create_foo`获得的指针放入共享指针的构造函数中。只有破坏是棘手的，因为`shared_ptr`的默认删除者会做错。诀窍是我们可以给`shared_ptr`一个*自定义删除器*。删除函数或可调用对象所需的函数签名已经与`destroy_foo`函数相同。如果我们需要调用来销毁对象的函数更复杂，我们可以简单地将其包装成一个 lambda 表达式:\n\n```cpp\n      static shared_ptr<Foo> make_shared_foo(string s)\n      {\n          return {Foo::create_foo(move(s)), Foo::destroy_foo};\n      }\n```\n\n5.  注意`make_shared_foo`返回一个通常的`shared_ptr<Foo>`实例，因为给它一个自定义删除器并没有改变它的类型。这是因为`shared_ptr`用虚函数调用来隐藏这样的细节。唯一指针不会带来任何开销，这使得相同的技巧对它们来说不可行。在这里，我们需要改变`unique_ptr`的类型。作为第二个模板参数，我们给它`void (*)(Foo*)`，这正是函数的指针类型，`destroy_foo`:\n\n```cpp\n      static unique_ptr<Foo, void (*)(Foo*)> make_unique_foo(string s)\n      {\n          return {Foo::create_foo(move(s)), Foo::destroy_foo};\n      }\n```\n\n6.  在主函数中，我们只是实例化了一个共享指针和一个唯一指针实例。在程序输出中，我们将看到它们是否真的被正确地自动销毁:\n\n```cpp\n      int main()\n      {\n          auto ps (make_shared_foo(\"shared Foo instance\"));\n          auto pu (make_unique_foo(\"unique Foo instance\"));\n      }\n```\n\n7.  编译并运行程序会产生以下输出，幸运的是，这正是我们所期望的:\n\n```cpp\n      $ ./legacy_shared_ptr \n      CTOR shared Foo instance\n      CTOR unique Foo instance\n      DTOR unique Foo instance\n      DTOR shared Foo instance\n```\n\n# 它是如何工作的...\n\n通常，`unique_ptr`和`shared_ptr`只要应该破坏他们维护的对象，就在内部指针上调用`delete`。在这一节中，我们构造了一个类，这个类既不能用`x = new Foo{123}`以 C++ 的方式分配，也不能直接用`delete x`来析构。\n\n`Foo::create_foo`函数只是返回一个普通的原始指针到一个新构造的`Foo`实例，所以这不会导致进一步的问题，因为智能指针无论如何都可以处理原始指针。\n\n我们要处理的问题是，如果默认的方式是*而不是*正确的方式，我们需要教`unique_ptr`和`shared_ptr`如何*破坏*一个对象。\n\n在这一点上，两种智能指针类型略有不同。为了给`unique_ptr`定义一个自定义删除器，我们必须改变它的类型。因为`Foo`删除器的类型签名是`void Foo::destroy_foo(Foo*);`，所以维护`Foo`实例的`unique_ptr`的类型必须是`unique_ptr<Foo, void (*)(Foo*)>`。现在，它可以保存一个指向`destroy_foo`的函数指针，我们在`make_unique_foo`函数中将其作为第二个构造函数参数提供。\n\n如果给`unique_ptr`一个自定义删除函数迫使我们改变它的类型，为什么我们能够用`shared_ptr` *做同样的事情而不用*改变它的类型？我们唯一要做的就是给`shared_ptr`第二个构造函数参数，仅此而已。为什么`unique_ptr`不能像`shared_ptr`一样轻松？\n\n仅仅提供`shared_ptr`某种可调用的 deleter 对象而不改变共享指针的类型如此简单的原因在于共享指针的性质，它维护一个控制块。共享指针的控制块是一个具有虚函数的对象。这意味着标准共享指针的控制块与带有自定义删除器的共享指针的控制块的类型相比*不同*！当我们想要一个唯一的指针使用一个自定义的删除程序时，这就改变了唯一指针的类型。当我们希望共享指针使用自定义删除器时，这将改变内部*控制块*的类型，这对于我们来说是不可见的，因为这种差异隐藏在虚拟函数接口后面。\n\n有可能*用唯一的指针做同样的事情，但是，这意味着它们会有一定的运行时开销。这不是我们想要的，因为独特的指针保证在运行时完全没有*开销。**\n\n *# 共享同一对象的不同成员值\n\n让我们想象一下，我们正在维护一个指向某个复杂的、合成的和动态分配的对象的共享指针。然后，我们想要启动一个新的线程，对这个复杂对象的一个成员执行一些耗时的工作。如果我们现在想释放这个共享指针，当另一个线程还在访问它时，这个对象将被删除。如果我们不想给线程对象一个指向整个复杂对象的指针，因为那样会破坏我们漂亮的接口，或者因为其他原因，这是否意味着我们现在必须进行手动内存管理？\n\n不可以。可以使用共享指针，一方面指向大型共享对象的成员，另一方面为整个初始对象执行自动内存管理。\n\n在这个例子中，我们将创建这样一个场景(没有线程来保持它的简单性)，以便对`shared_ptr`的这个便利特性有所感受。\n\n# 怎么做...\n\n我们将定义一个由多个成员组成的结构。然后，我们在由共享指针维护的堆上分配这个结构的一个实例。从这个共享指针中，我们获得了更多不指向实际对象而是指向其成员的共享指针:\n\n1.  我们首先包含必要的头，然后声明我们默认使用`std`命名空间:\n\n```cpp\n      #include <iostream>\n      #include <memory>\n      #include <string>      \n\n      using namespace std;\n```\n\n2.  然后我们定义一个有不同成员的类。我们将让共享指针指向各个成员。为了能够看到类何时被创建和销毁，我们让它的构造函数和析构函数打印消息:\n\n```cpp\n      struct person {\n          string name;\n          size_t age;\n\n          person(string n, size_t a)\n              : name{move(n)}, age{a}\n          { cout << \"CTOR \" << name << 'n'; }\n\n          ~person() { cout << \"DTOR \" << name << 'n'; }\n      };\n```\n\n3.  让我们定义共享指针，这些指针具有指向`person`类实例的`name`和`age`成员变量的正确类型:\n\n```cpp\n      int main()\n      {\n          shared_ptr<string> shared_name;\n          shared_ptr<size_t> shared_age;\n```\n\n4.  接下来，我们输入一个新的范围，创建这样一个 person 对象，并让一个共享指针管理它:\n\n```cpp\n          {\n              auto sperson (make_shared<person>(\"John Doe\", 30));\n```\n\n5.  然后，我们让前两个共享指针指向它的名称和年龄成员。诀窍是我们使用`shared_ptr`的特定构造函数，它接受共享指针和指向共享对象成员的指针。这样，我们可以在不指向对象本身的情况下管理对象！\n\n```cpp\n              shared_name = shared_ptr<string>(sperson, &sperson->name);\n              shared_age  = shared_ptr<size_t>(sperson, &sperson->age);\n          }\n```\n\n6.  离开范围后，我们打印此人的姓名和年龄值。只有在对象仍然被分配的情况下，这才是合法的:\n\n```cpp\n          cout << \"name: \"  << *shared_name\n               << \"nage: \" << *shared_age << 'n';\n      }\n```\n\n7.  编译并运行程序会产生以下输出。从析构函数消息中，我们看到当我们通过成员指针访问人的名字和年龄值时，对象确实还活着并被分配了！\n\n```cpp\n      $ ./shared_members \n      CTOR John Doe\n      name: John Doe\n      age:  30\n      DTOR John Doe\n```\n\n# 它是如何工作的...\n\n在本节中，我们首先创建了一个管理动态分配的`person`对象的共享指针。然后我们制作了另外两个指向人物对象的智能指针，但是它们都没有直接指向人物对象本身，而是指向其成员`name`和`age`。\n\n为了总结我们刚刚创建的场景，让我们看一下下图:\n\n![](img/835d6935-4712-4ccb-9e5c-8ff335816245.png)\n\n注意`shared_ptr1`直接指向`person`对象，而`shared_name`和`shared_age`指向同一对象的`name`和`age`成员。显然，它们仍然管理对象的整个生命周期。这是可能的，因为内部控制块指针仍然指向同一个控制块，不管各个共享指针指向什么子对象。\n\n在这种情况下，控制块的使用计数为`3`。这样，`shared_ptr1`被销毁时`person`对象不会被销毁，因为其他共享指针仍然拥有该对象。\n\n当创建这样的指向共享对象成员的共享指针实例时，语法看起来有点奇怪。为了获得指向共享人姓名成员的`shared_ptr<string>`，我们需要写下以下内容:\n\n```cpp\nauto sperson (make_shared<person>(\"John Doe\", 30));\nauto sname   (shared_ptr<string>(sperson, &sperson->name));\n```\n\n为了获得指向共享对象成员的特定指针，我们用我们想要访问的成员的类型专门化来实例化共享指针。这就是我们写`shared_ptr<**string**>`的原因。然后，在构造函数中，我们首先提供维护`person`对象的原始共享指针，作为第二个参数，新共享指针将在我们取消引用它时使用的对象地址。\n\n# 生成随机数并选择合适的随机数引擎\n\n为了得到随机数，不管是出于什么目的，C++ 程序员在 C++ 11 之前基本上都是使用 C 库的`rand()`函数。从 C++ 11 开始，就出现了一整套*套*随机数生成器，它们服务于不同的目的，具有不同的特性。\n\n这些生成器不是完全不言自明的，所以我们将在本食谱中查看所有这些生成器。最终，我们将看到它们在哪些方面有所不同，如何选择正确的，并且我们很可能永远不会使用它们。\n\n# 怎么做...\n\n我们将实现一个程序，打印一个随机生成器生成的数字直方图。然后，我们将通过这个过程运行所有的 STL 随机数生成器引擎，并从结果中学习。这个程序包含许多重复的行，所以从互联网上的代码库中复制源代码，而不是手动键入所有重复的代码，可能是有利的。\n\n1.  首先，我们包含所有必要的头，然后声明我们默认使用`std`命名空间:\n\n```cpp\n      #include <iostream>\n      #include <string>\n      #include <vector>\n      #include <random>\n      #include <iomanip>\n      #include <limits>\n      #include <cstdlib>\n      #include <algorithm>      \n\n      using namespace std;\n```\n\n2.  然后我们实现一个助手函数，帮助我们维护和打印每种随机数引擎的一些统计数据。它接受两个参数:*分区数量*和*样本数量*。我们将立即看到这些是为了什么。随机发生器的类型通过模板参数`RD`定义。我们在这个函数中做的第一件事是为生成器返回的数字定义一个别名类型。我们还确保至少有 10 个分区:\n\n```cpp\n      template <typename RD>\n      void histogram(size_t partitions, size_t samples)\n      {\n          using rand_t = typename RD::result_type;\n          partitions = max<size_t>(partitions, 10);\n```\n\n3.  接下来，我们实例化一个类型为`RD`的实际生成器实例。然后，我们定义一个叫`div`的除数变量。所有随机数引擎都发出从`0`到`RD::max()`范围内的随机数。函数参数`partitions`允许调用者选择我们将每个随机数范围划分成多少个分区。通过将最大可能值除以分区数，我们知道每个分区有多大:\n\n```cpp\n          RD rd;\n          rand_t div ((double(RD::max()) + 1) / partitions);\n```\n\n4.  接下来，我们实例化一个计数器变量向量。它和我们的分区数量一样大。然后，我们从随机引擎中得到的随机值和变量`samples`所说的一样多。表达式`rd()`从生成器中获取一个随机数，并转换其内部状态，为返回下一个随机数做准备。通过将每个随机数除以`div`，我们得到它所属的分区数，并且可以在计数器向量中增加正确的计数器:\n\n```cpp\n          vector<size_t> v (partitions);\n          for (size_t i {0}; i < samples; ++ i) { \n              ++ v[rd() / div];\n          }\n```\n\n5.  现在我们有了一个漂亮的样本值的粗粒度直方图。为了打印它，我们需要多了解一点它的实际计数器值。让我们使用`max_element`算法提取它的最大值。然后我们将这个最大的计数器值除以`100`。这样我们就可以用`max_div`来划分所有的计数器值，在不超过`100`宽度的情况下，在终端上打印很多星星。如果最大计数器包含一个小于`100`的数，因为我们没有使用那么多样本，我们使用`max`来获得`1`的最小除数:\n\n```cpp\n          rand_t max_elm (*max_element(begin(v), end(v)));\n          rand_t max_div (max(max_elm / 100, rand_t(1)));\n```\n\n6.  现在让我们把直方图打印到终端。每个分区在终端上都有自己的线路。通过将其计数器值除以`max_div`并打印这么多星号符号`'*'`，我们得到适合终端的直方图线:\n\n```cpp\n          for (size_t i {0}; i < partitions; ++ i) {\n              cout << setw(2) << i << \": \"\n                   << string(v[i] / max_div, '*') << 'n';\n          }\n      }\n```\n\n7.  好了，就这样。现在转到主程序。我们让用户定义应该使用多少分区和样本:\n\n```cpp\n      int main(int argc, char **argv)\n      {\n          if (argc != 3) {\n              cout << \"Usage: \" << argv[0] \n                   << \" <partitions> <samples>n\";\n              return 1;\n          }\n```\n\n8.  然后我们从命令行读取这些变量。当然，命令行由字符串组成，我们可以使用`std::stoull` ( `stoull`是 **s** 的缩写)字符串**到****u**n 指定 **l** ong **l** ong:\n\n```cpp\n          size_t partitions {stoull(argv[1])};\n          size_t samples    {stoull(argv[2])};\n```\n\n9.  现在我们在 STL 提供的每个随机数引擎上调用我们的直方图助手函数。这使得这个食谱非常长而且重复。最好从网上复制这个例子。这个程序的输出看起来真的很有趣。我们从`random_device`开始。该设备试图将随机性平均分配给所有可能的值:\n\n```cpp\n          cout << \"random_device\" << 'n';\n          histogram<random_device>(partitions, samples);\n```\n\n10.  我们尝试的下一个随机引擎是`default_random_engine`。这种类型指的是哪种引擎是特定于实现的。它可以是以下任意一款发动机:\n\n```cpp\n          cout << \"ndefault_random_engine\" << 'n';\n          histogram<default_random_engine>(partitions, samples);\n```\n\n11.  然后我们在所有其他发动机上进行试验:\n\n```cpp\n          cout << \"nminstd_rand0\" << 'n';\n          histogram<minstd_rand0>(partitions, samples);\n          cout << \"nminstd_rand\" << 'n';\n          histogram<minstd_rand>(partitions, samples);\n\n          cout << \"nmt19937\" << 'n';\n          histogram<mt19937>(partitions, samples);\n          cout << \"nmt19937_64\" << 'n';\n          histogram<mt19937_64>(partitions, samples);\n\n          cout << \"nranlux24_base\" << 'n';\n          histogram<ranlux24_base>(partitions, samples);\n          cout << \"nranlux48_base\" << 'n';\n          histogram<ranlux48_base>(partitions, samples);\n\n          cout << \"nranlux24\" << 'n';\n          histogram<ranlux24>(partitions, samples);\n          cout << \"nranlux48\" << 'n';\n          histogram<ranlux48>(partitions, samples);\n\n          cout << \"nknuth_b\" << 'n';\n          histogram<knuth_b>(partitions, samples);\n      }\n```\n\n12.  编译和运行程序会产生有趣的结果。我们会看到一长串的输出，我们会看到所有的随机引擎都有不同的特性。让我们首先运行带有`10`分区且只有`1000`样本的程序:\n\n![](img/ff7076af-b140-497a-9d23-c4453a43415f.png)\n\n13.  然后，我们再次运行相同的程序。这次还是`10`分区但是`1,000,000`样本。很明显，当我们从直方图中提取更多样本时，直方图看起来更加清晰*。这是一个重要的观察:*\n\n *![](img/8178317e-d792-4766-be8a-d4e0e6a06d3c.png)\n\n# 它是如何工作的...\n\n一般来说，任何随机数生成器在使用前都需要实例化为一个对象。生成的对象可以像没有参数的函数一样被调用，因为它重载了`operator()`。每一次调用都会产生一个新的随机数。就这么简单。\n\n在这一节中，我们编写了一个比这复杂得多的程序，以便获得更多关于随机数生成器的信息。请通过用不同的命令行参数启动程序来处理生成的程序，并了解以下事实:\n\n*   我们采集的样本越多，分区计数器就越相等。\n*   各个引擎之间的分区计数器的不平等性差异很大。\n*   对于大量的样本，很明显各个随机引擎的*性能*是不同的。\n*   多次使用少量样本运行程序。分布模式始终看起来*一样*-随机引擎反复产生*一样的*随机数序列，这意味着它们根本不是随机的*。这种引擎被称为*确定性*，因为它们的随机数是可以预测的。唯一的例外是`std::random_device`。*\n\n *我们可以看到，有几个特点需要考虑。对于大多数标准应用来说，`std::default_random_engine`就足够了。密码学或类似安全敏感主题的专家会明智地在他们使用的引擎之间做出选择，但对于我们普通程序员来说，当我们编写带有一些随机性的应用时，这并不太重要。\n\n我们应该从这个食谱中记住以下三个事实:\n\n1.  通常情况下，`std::default_random_engine`对于普通应用来说是一个不错的默认选择。\n2.  如果我们真的需要非确定性随机数，`std::random_device`就给我们提供了这样的。\n3.  我们可以给任意随机引擎的构造器输入一个来自`std::random_device`的*实数*随机数(或者可能是系统时钟的时间戳)，以便让它每次产生不同的随机数。这叫*播种*。\n\nNote that `std::random_device` *can* possibly fall back to one of the deterministic engines if the library has no support for nondeterministic random engines.\n\n# 生成随机数并让 STL 形成特定的分布\n\n在上一个食谱中，我们学习了一些关于 STL 随机数引擎的知识。以这种或那种方式生成随机数通常只是工作的一半。\n\n另一个问题是，我们需要这些数字做什么？我们是在程序化地“抛硬币”吗？人们习惯用`rand() % 2`来做这件事，这就产生了`0`和`1`的值，然后可以映射到*头*或*尾*。很公平；为此我们不需要库(尽管随机性专家知道，仅仅使用随机数的最低几个位并不总是能产生高质量的随机数)。\n\n如果我们想做一个模具呢？那么，我们当然可以写`(rand() % 6) + 1`，以表示擀模后的结果。如此简单的任务仍然不需要紧迫的库。\n\n如果我们想对发生概率为 66%的事情进行建模会怎么样？好吧，那我们可以想出一个类似`bool yesno = (rand() % 100 > 66)`的公式。(哦等等，应该是`>=`，还是`>`正确？)\n\n除此之外，我们如何模拟一个双方概率不完全相同的*不公平*死亡？或者我们如何对更复杂的分布建模？这样的问题可以很快演变成科学任务。为了专注于我们的主要问题，让我们看看 STL 已经提供了什么来帮助我们。\n\nSTL 包含十多种分布算法，可以根据特定需求塑造随机数。在本食谱中，我们将非常简要地看一下所有的食谱，并仔细看看最常用的食谱。\n\n# 怎么做...\n\n我们将生成随机数，塑造它们，并将它们的分布模式打印到终端。通过这种方式，我们可以了解所有这些因素，并了解最重要的因素，这在我们需要考虑随机性来建模特定事物时非常有用:\n\n1.  首先，我们包含所有需要的头，并声明我们使用`std`命名空间:\n\n```cpp\n      #include <iostream>\n      #include <iomanip>\n      #include <random>\n      #include <map>\n      #include <string>\n      #include <algorithm>     \n\n      using namespace std;\n```\n\n2.  对于 STL 提供的每个分布，我们将打印一个直方图，以便查看其特征，因为每个分布看起来都非常特殊。它接受一个分布作为一个参数，并接受从中抽取的样本数。然后，我们实例化默认的随机引擎和地图。该映射从我们从分布中获得的值映射到计数器，该计数器计算该值出现的频率。我们总是实例化一个随机引擎的原因是，对于仍然需要由随机引擎生成的随机数，所有分布只是用作*整形函数*:\n\n```cpp\n      template <typename T>\n      void print_distro(T distro, size_t samples)\n      {\n          default_random_engine e;\n          map<int, size_t> m;\n```\n\n3.  我们根据`samples`变量的要求获取尽可能多的样本，并将它们输入地图计数器。这样，我们得到一个漂亮的直方图。单独调用`e()`会从随机引擎中获得一个原始随机数，`distro(e)`通过分布对象对随机数进行整形。\n\n```cpp\n          for (size_t i {0}; i < samples; ++ i) {\n              m[distro(e)] += 1;\n          }\n```\n\n4.  为了获得适合终端窗口的终端输出，我们需要知道*最大的*计数器值。`max_element`函数通过比较映射中所有相关的计数器并向我们返回一个最大计数器节点的迭代器来帮助我们找到最大值。了解该值后，我们可以确定需要用什么值来划分所有计数器值，以便将输出放入终端窗口:\n\n```cpp\n          size_t max_elm (max_element(begin(m), end(m),\n              [](const auto &a, const auto &b) { \n                   return a.second < b.second; \n              })->second);\n          size_t max_div (max(max_elm / 100, size_t(1)));\n```\n\n5.  现在，我们循环浏览地图，并为所有具有显著尺寸的计数器打印一条星号符号`'*'`。我们放弃其他的，因为一些分发引擎将数字分散在如此大的域中，以至于它会完全淹没我们的终端窗口:\n\n```cpp\n          for (const auto [randval, count] : m) {\n              if (count < max_elm / 200) { continue; }\n\n              cout << setw(3) << randval << \" : \"\n                   << string(count / max_div, '*') << 'n';\n          }\n      }\n```\n\n6.  在主函数中，我们检查用户是否正好为我们提供了一个参数，该参数告诉我们要从每个分布中抽取多少样本。如果用户没有提供或提供了多个参数，我们就会出错。\n\n```cpp\n      int main(int argc, char **argv)\n      {\n          if (argc != 2) {\n              cout << \"Usage: \" << argv[0] \n                   << \" <samples>n\";\n              return 1;\n          }\n```\n\n7.  我们使用`std::stoull`将命令行参数字符串转换为数字:\n\n```cpp\n          size_t samples {stoull(argv[1])};\n```\n\n8.  首先，我们尝试`uniform_int_distribution`和`normal_distribution`。这些是最典型的需要随机数的分布。每一个在学校把随机作为数学话题的人很可能已经听说过这些了。均匀分布接受两个值，表示随机值分布范围的下限和上限。通过选择`0`和`9`，我们将获得(包括)`0`和`9`之间同样频繁出现的值。正态分布接受一个*平均值*和一个*标准导数*作为自变量:\n\n```cpp\n          cout << \"uniform_int_distributionn\";\n          print_distro(uniform_int_distribution<int>{0, 9}, samples);\n\n          cout << \"normal_distributionn\";\n          print_distro(normal_distribution<double>{0.0, 2.0}, samples);\n```\n\n9.  另一个真正有趣的分布是`piecewise_constant_distribution`。它接受两个输入范围作为参数。第一个范围包含表示区间界限的数字。通过将其定义为`0, 5, 10, 30`，我们得到一个从`0`到`4`的区间，然后是一个从`5`到`9`的区间，最后一个从`10`到`29`的区间。另一个输入范围定义输入范围的权重。通过将这些权重设置为`0.2, 0.3, 0.5`，区间被随机数字命中，概率为 20%、30%和 50%。在每个间隔内，所有值都以相同的概率命中:\n\n```cpp\n          initializer_list<double> intervals {0, 5, 10, 30};\n          initializer_list<double> weights {0.2, 0.3, 0.5};\n          cout << \"piecewise_constant_distributionn\";\n          print_distro(\n              piecewise_constant_distribution<double>{\n                  begin(intervals), end(intervals), \n                  begin(weights)}, \n             samples);\n```\n\n10.  `piecewise_linear_distribution`的构造类似，但其重量特性完全不同。对于每个区间边界点，都有一个权重值。在从一个边界到另一个边界的过渡中，概率被线性插值。我们使用相同的间隔列表，但使用不同的权重值列表。\n\n```cpp\n          cout << \"piecewise_linear_distributionn\";\n          initializer_list<double> weights2 {0, 1, 1, 0};\n          print_distro(\n              piecewise_linear_distribution<double>{\n                  begin(intervals), end(intervals), begin(weights2)}, \n              samples);\n```\n\n11.  伯努利分布是另一个重要的分布，因为它只以特定的概率分布*是/否*、*命中/未命中*或*头尾*值。其输出值仅为`0`或`1`。另一个有趣的分布是`discrete_distribution`，它在很多情况下都很有用。在我们的例子中，我们将其初始化为离散值`1, 2, 4, 8`。这些值被解释为可能输出值`0`到`3`的权重:\n\n```cpp\n          cout << \"bernoulli_distributionn\";\n          print_distro(std::bernoulli_distribution{0.75}, samples);\n\n          cout << \"discrete_distributionn\";\n          print_distro(discrete_distribution<int>{{1, 2, 4, 8}}, samples);\n```\n\n12.  还有很多不同的其他分发引擎。它们非常特殊，在非常特殊的情况下非常有用。如果你从未听说过他们，他们*可能*不适合你。然而，由于我们的程序将产生良好的分布直方图，出于好奇，我们将打印它们:\n\n```cpp\n          cout << \"binomial_distributionn\";\n          print_distro(binomial_distribution<int>{10, 0.3}, samples);\n          cout << \"negative_binomial_distributionn\";\n          print_distro(\n              negative_binomial_distribution<int>{10, 0.8}, \n              samples);\n          cout << \"geometric_distributionn\";\n          print_distro(geometric_distribution<int>{0.4}, samples);\n          cout << \"exponential_distributionn\";\n          print_distro(exponential_distribution<double>{0.4}, samples);\n          cout << \"gamma_distributionn\";\n          print_distro(gamma_distribution<double>{1.5, 1.0}, samples);\n          cout << \"weibull_distributionn\";\n          print_distro(weibull_distribution<double>{1.5, 1.0}, samples);\n          cout << \"extreme_value_distributionn\";\n          print_distro(\n              extreme_value_distribution<double>{0.0, 1.0}, \n              samples);\n          cout << \"lognormal_distributionn\";\n          print_distro(lognormal_distribution<double>{0.5, 0.5}, samples);\n          cout << \"chi_squared_distributionn\";\n          print_distro(chi_squared_distribution<double>{1.0}, samples);\n          cout << \"cauchy_distributionn\";\n          print_distro(cauchy_distribution<double>{0.0, 0.1}, samples);\n          cout << \"fisher_f_distributionn\";\n          print_distro(fisher_f_distribution<double>{1.0, 1.0}, samples);\n          cout << \"student_t_distributionn\";\n          print_distro(student_t_distribution<double>{1.0}, samples);\n      }\n```\n\n13.  编译并运行程序会产生以下输出。让我们首先用每个分布的`1000`样本运行程序:\n\n![](img/5ba7d1ee-ef21-4b2d-ae4f-6d68d5bc6ab5.png)\n\n14.  对每个分布的`1,000,000`样本进行的另一次运行显示，对于每个分布，直方图显得更加清晰和典型。但是我们也看到了哪些是慢的，哪些是快的，而它们是在生成的:\n\n![](img/0297daeb-5434-4bf4-8aa6-7f23b5036413.png)\n\n# 它是如何工作的...\n\n虽然我们通常不太关心随机数引擎，但只要它速度快并产生尽可能随机的数字，分布就是我们*应该*明智选择的东西，这取决于我们喜欢解决(或创造)的问题。\n\n为了使用任何分布，我们首先需要从它实例化一个分布对象。我们已经看到，不同的发行版采用不同的构造函数参数。在配方描述中，我们稍微简单地介绍了一些分发引擎，因为它们中的大多数都太特殊和/或太复杂，无法在此介绍。但是不用担心，它们都在 C++ STL 文档中有详细的记录。\n\n然而，一旦我们实例化了一个分布，我们就可以像接受随机引擎对象作为其唯一参数的函数一样调用它。接下来发生的是，分发引擎从随机引擎中获取一个随机值，应用一些神奇的整形(当然这完全取决于分发引擎的选择)，然后返回给我们一个*整形的*随机值。这导致了完全不同的直方图，就像我们在执行程序后看到的那样。\n\n了解不同发行版的最全面的方法是*用我们刚刚写的程序玩*。除此之外，让我们总结一下最重要的发行版。对于我们程序中出现但不在下表中的所有发行版，如果您感兴趣，请参考 C++ STL 文档:\n\n| **分布** | **描述** |\n| `uniform_int_distribution` | 此分布接受一个下限和一个上限值作为构造函数参数。那么，它确实给了我们随机数，这些随机数总是落在(包括)这些界限之间的区间内。这个区间中每个值的概率都是一样的，这就给了我们一个*平坦*形状的直方图。例如，这种分布代表滚动一个*模具*，因为模具的每一侧都有相同的概率发生。 |\n| `normal_distribution` | 正态分布，或称高斯分布，实际上在自然界中随处可见。其 STL 版本接受一个平均值和一个标准偏差值作为构造函数参数，并在直方图中形成一个类似*屋顶*的形状。如果我们比较人类或其他动物的体型或智商，或者学生的成绩，我们会意识到这些数字也是正态分布的。 |\n| `bernoulli_distribution` | 如果我们想抛硬币或得到是/否的答案，伯努利分布是完美的。它只发出值`0`或`1`，其唯一的构造函数参数是`1`值的概率。 |\n| `discrete_distribution` | 如果我们只想要一组非常有限的离散值，并且想要定义每个单个值的概率，那么离散分布是有趣的。它的构造器获取一个权重列表，并根据权重发出带有概率的随机数。如果我们想对随机分布的血型进行建模，其中只有四种不同的血型具有特定的概率，那么这个引擎就是完美的匹配。 |******"
  },
  {
    "path": "docs/exp-cpp-prog/08.md",
    "content": "# 八、并行性和并发性\n\n在本章中，我们将介绍以下食谱:\n\n*   使用标准算法自动并行化代码\n*   让程序休眠特定的时间\n*   启动和停止线程\n*   与`std::unique_lock`和`std::shared_lock`执行异常安全共享锁定\n*   避免与`std::scoped_lock`的僵局\n*   同步并发`std::cout`使用\n*   通过`std::call_once`安全推迟初始化\n*   使用`std::async`将任务的执行推到后台\n*   用`std::condition_variable`实现生产者/消费者习惯用法\n*   用`std::condition_variable`实现多生产者/消费者习惯用法\n*   使用`std::async`并行化 ASCII 曼德尔布罗渲染器\n*   用`std::future`实现微小的自动并行化库\n\n# 介绍\n\n在 C++ 11 之前，C++ 不太支持并行化。这并不意味着启动、控制、停止和同步线程是不可能的，但是使用特定于操作系统的库是必要的，因为线程本质上与操作系统相关。\n\n借助 C++ 11，我们获得了`std::thread`，它支持跨所有操作系统的基本可移植线程控制。为了同步线程，C++ 11 还引入了互斥类和舒适的 RAII 风格的锁包装器。除此之外，`std::condition_variable`允许线程间灵活的事件通知。\n\n其他一些真正有趣的增加是`std::async`和`std::future` -我们现在可以将任意的普通函数包装到`std::async`调用中，以便在后台异步执行它们。这种包装的函数返回`std::future`对象，这些对象承诺稍后包含函数的结果，所以我们可以在等待它的到来之前做一些其他的事情。\n\nSTL 的另一个实际上巨大的改进是*执行策略*，它可以添加到已经存在的 69 个*算法中。这种添加意味着，我们只需向旧程序中现有的标准算法调用添加一个执行策略参数，就可以获得并行化，而无需复杂的重写。*\n\n在这一章中，我们将通过所有这些补充来了解关于它们的最重要的事情。之后，我们将对 C++ 17 STL 中的并行化支持有足够的监督。我们没有涵盖所有的细节，但最重要的。从这本书获得的概述有助于快速理解其余的并行编程机制，您可以随时在线查阅 C++ 17 STL 文档。\n\n最后，本章包含两个奖励食谱。在一个方法中，我们将并行化来自[第 23 章](05.html)、*STL 算法的高级使用*的 Mandelbrot ASCII 渲染器，只做最小的改变。在最后一个方法中，我们将实现一个小型库，帮助隐式和自动地并行化复杂的任务。\n\n# 使用标准算法自动并行化代码\n\nC++ 17 为并行性带来了一个真正的主要扩展:e .标准算法的执行策略。69 种算法被扩展为接受执行策略，以便在多个内核上并行运行，甚至支持向量化。\n\n对于用户来说，这意味着如果我们已经在任何地方使用 STL 算法，我们将免费获得一个不错的并行化奖励。我们只需在现有的 STL 算法调用中添加一个执行策略参数，就可以轻松地为我们的应用提供后续的并行化。\n\n在这个食谱中，我们将实现一个简单的程序(使用不太严重的用例场景)，该程序将多个 STL 算法调用排列在一起。在使用这些的同时，我们将看到使用 C++ 17 执行策略以便让它们运行多线程是多么容易。在本节的最后几个小节中，我们将详细了解不同的执行策略。\n\n# 怎么做...\n\n在这一节中，我们将编写一个使用一些标准算法的程序。这个程序本身更像是一个真实场景的例子，而不是真实工作场景的例子。在使用这些标准算法时，我们嵌入了执行策略，以加快代码的速度:\n\n1.  首先，我们需要包含一些头，并声明我们使用`std`命名空间。`execution`表头是新的；它附带了 C++ 17:\n\n```cpp\n      #include <iostream>\n      #include <vector>\n      #include <random>\n      #include <algorithm>\n      #include <execution>      \n\n      using namespace std;\n```\n\n2.  为了举例，我们将声明一个谓词函数，它告诉一个数是否是奇数。我们稍后将使用它:\n\n```cpp\n      static bool odd(int n) { return n % 2; }\n```\n\n3.  让我们首先在主函数中定义一个大向量。我们会用大量数据填充它，这样就需要一些时间来对它进行计算。这个代码的执行速度会有很大的变化*，这取决于执行这个代码的计算机。较小/较大的矢量大小在不同的计算机上可能更好:*\n\n```cpp\n      int main()\n      {\n          vector<int> d (50000000);\n```\n\n4.  为了获得向量的大量随机数据，让我们实例化一个随机数生成器和一个分布，并将它们打包在一个可调用的对象中。如果这看起来很奇怪，请先看看[第 25 章](07.html)、*效用类*中处理随机数生成器和分布的食谱:\n\n```cpp\n          mt19937 gen;\n          uniform_int_distribution<int> dis(0, 100000);\n\n          auto rand_num ([=] () mutable { return dis(gen); });\n```\n\n5.  现在，让我们使用`std::generate`算法用随机数据填充向量。这个算法有一个新的 C++ 17 版本，可以采用一种新的参数:执行策略。我们在这里放入`std::par`，允许自动并行化这段代码。通过这样做，我们允许多个线程开始一起填充向量，如果计算机有多个 CPU，这将减少执行时间，这通常是现代计算机的情况:\n\n```cpp\n          generate(execution::par, begin(d), end(d), rand_num);\n```\n\n6.  `std::sort`的方法应该也已经很熟悉了。C++ 17 版本也支持定义执行策略的附加参数:\n\n```cpp\n          sort(execution::par, begin(d), end(d));\n```\n\n7.  这同样适用于`std::reverse`:\n\n```cpp\n          reverse(execution::par, begin(d), end(d));\n```\n\n8.  然后我们用`std::count_if`来统计向量中所有的奇数。我们甚至可以通过再次添加执行策略来实现并行化！\n\n```cpp\n          auto odds (count_if(execution::par, begin(d), end(d), odd));\n```\n\n9.  整个程序没有做任何真正的科学工作，因为我们只是想看看如何并行化标准算法，但是让我们在最后打印一些东西:\n\n```cpp\n          cout << (100.0 * odds / d.size()) \n               << \"% of the numbers are odd.n\";\n      }\n```\n\n10.  编译并运行该程序会给出以下输出。在这一点上，有趣的是，当使用没有执行策略的算法时，与所有其他执行策略相比，执行速度有何不同。这样做是留给读者的练习。试试看；可用的执行策略有`seq`、`par`和`par_vec`。我们应该为它们中的每一个获得不同的执行时间:\n\n```cpp\n      $ ./auto_parallel\n      50.4% of the numbers are odd.\n```\n\n# 它是如何工作的...\n\n特别是因为这个食谱没有用任何复杂的现实问题解决方案分散我们的注意力，我们能够完全专注于标准的库函数调用。很明显，它们的并行版本与经典的顺序版本几乎没有什么不同。它们的区别仅在于*一个额外的*参数，即*执行政策*。\n\n让我们看一下调用并回答三个核心问题:\n\n```cpp\ngenerate(execution::par, begin(d), end(d), rand_num);\nsort(    execution::par, begin(d), end(d));\nreverse( execution::par, begin(d), end(d));\n\nauto odds (count_if(execution::par, begin(d), end(d), odd));\n```\n\n# 我们可以这样并行化哪些 STL 算法？\n\n在 C++ 17 标准中，69 个现有的 STL 算法被升级为支持并行，还有 7 个新算法也支持并行。虽然这样的升级对于实现来说可能是相当具有侵略性的，但是在它们的接口方面并没有太大的变化——它们都有一个额外的`ExecutionPolicy&& policy`参数，仅此而已。这并不是说*不是*意味着我们*总是*必须提供一个执行政策论证。只是他们*另外*支持接受执行政策作为他们的第一个论点。\n\n这是 69 个升级的标准算法。还有从一开始就支持执行策略的七个新的(以*粗体*突出显示):\n\n| `std::adjacent_difference`\n`std::adjacent_find`\n`std::all_of`\n`std::any_of`\n`std::copy`\n`std::copy_if`\n`std::copy_n`\n`std::count`\n`std::count_if`\n`std::equal` `**std::exclusive_scan**`\n`std::fill`\n`std::fill_n`\n\n`std::find_first_of`\n`std::find_if`\n`**std::for_each**`\n`**std::for_each_n**``std::generate`\n`std::generate_n`\n`std::includes` `**std::inclusive_scan**`\n | `std::inplace_merge` `std::is_heap` `std::is_heap_until`\n`std::is_partitioned`\n`std::is_sorted`\n`std::is_sorted_until`\n`std::lexicographical_compare`\n`std::max_element`\n`std::merge`\n`std::min_element`\n`std::minmax_element`\n`std::mismatch`\n`std::move`\n\n`std::partial_sort`\n`std::partial_sort_copy`\n\n`std::partition_copy`\n`std::remove`\n`std::remove_copy`\n\n`std::remove_if`\n`std::replace` | `std::replace_if`\n`std::reverse`\n`std::reverse_copy`\n`std::rotate`\n`std::rotate_copy`\n`std::search`\n`std::search_n`\n`std::set_difference`\n`std::set_intersection`\n`std::set_symmetric_difference`\n`std::set_union`\n`std::sort`\n`std::stable_partition`\n\n`std::transform`\n`**std::transform_inclusive_scan**` `**std::transform_reduce**`\n`std::uninitialized_copy``std::uninitialized_copy_n`\n`std::uninitialized_fill`\n`std::uninitialized_fill_n`\n`std::unique` |\n\n升级这些算法是个好消息！我们的旧程序越多地使用 STL 算法，我们就越容易追溯性地向它们添加并行性。请注意，这并不意味着这种变化会使每个程序自动地加快“than 次，因为多道程序比这复杂得多。\n\n然而，我们现在可以以一种非常优雅的、独立于操作系统的方式并行化标准任务，而不是使用`std::thread`、`std::async`或通过包含外部库来设计我们自己的复杂并行算法。\n\n# 这些执行策略是如何工作的？\n\n执行策略告诉我们允许标准算法调用自动并行化的策略。\n\n`std::execution`命名空间中存在以下三种策略类型:\n\n| **政策** | **表示** |\n| --- | --- |\n| `sequenced_policy` | 该算法必须以类似于没有执行策略的原始算法的顺序形式执行。全局可用实例的名称为`std::execution::seq`。 |\n| `parallel_policy` | 该算法可以用以并行方式共享工作的多个线程来执行。全局可用实例的名称为`std::execution::par`。 |\n| `parallel_unsequenced_policy` | 该算法可以在多个线程共享工作的情况下执行。除此之外，还允许对代码进行矢量化。在这种情况下，由于向量化，容器访问可以在线程之间以及同一线程内交错进行。全局可用实例的名称为`std::execution::par_unseq`。 |\n\n执行政策对我们意味着特定的约束。特定约束越严格，我们可以允许的并行化策略措施就越多:\n\n*   并行算法*使用的所有元素访问函数不得*导致*死锁*或*数据竞争*\n*   在并行和向量化的情况下，所有访问函数*不得使用任何类型的阻塞同步*\n\n只要我们遵守这些规则，我们就应该避免使用并行版本的 STL 算法带来的错误。\n\nNote that just using parallel STL algorithms correctly does not always lead to guaranteed speedup. Depending on the problem we try to solve, the problem size, and the efficiency of our data structures and other access methods, measurable speedup will vary very much or not occur at all. *Multiprogramming is still hard.*\n\n# 矢量化是什么意思？\n\n矢量化是中央处理器和编译器都需要支持的功能。让我们快速浏览一下一个简单的示例，以简要了解什么是矢量化及其工作原理。假设我们想从一个非常大的向量中总结数字。此任务的简单实现如下所示:\n\n```cpp\nstd::vector<int> v {1, 2, 3, 4, 5, 6, 7 /*...*/};\n\nint sum {std::accumulate(v.begin(), v.end(), 0)};\n```\n\n编译器最终将从`accumulate`调用中生成一个循环，如下所示:\n\n```cpp\nint sum {0};\nfor (size_t i {0}; i < v.size(); ++ i) {\n    sum += v[i];\n}\n```\n\n从这一点出发，在允许并启用矢量化的情况下，编译器可以生成以下代码。该循环在一个循环步骤中执行四个累积步骤，并且迭代次数减少了四倍。为了简单起见，如果向量不包含`N * 4`元素，则该示例不处理余数:\n\n```cpp\nint sum {0};\nfor (size_t i {0}; i < v.size() / 4; i += 4) {\n    sum += v[i] + v[i+1] + v[i + 2] + v[i + 3];\n}\n// if v.size() / 4 has a remainder, \n// real code has to deal with that also.\n```\n\n它为什么要这样做？许多中央处理器只需一步，就能提供执行数学运算的指令，如`sum += v[i] + v[i+1] + v[i + 2] + v[i + 3];`。将尽可能多的*数学运算压入尽可能少的*指令是目标，因为这加快了程序的速度。**\n\n自动矢量化很难，因为编译器需要在一定程度上理解我们的程序，以便使我们的程序更快，但又不影响其*正确性*。至少，我们可以通过尽可能频繁地使用标准算法来帮助编译器，因为对于编译器来说，这些算法比具有复杂数据流依赖关系的复杂手工循环更容易掌握。\n\n# 让程序休眠特定的时间\n\nC++ 11 提供了一种控制线程的简单可行的方法。它引入了`this_thread`命名空间，其中包括只影响调用方线程的函数。它包含两个不同的函数，允许让一个线程休眠一定的时间，所以我们不再需要为这样的任务使用任何外部的或操作系统相关的库。\n\n在这个食谱中，我们专注于如何将线程挂起一定的时间，或者如何让它们进入*睡眠*。\n\n# 怎么做...\n\n我们将编写一个简短的程序，让主线程休眠一定的时间:\n\n1.  让我们首先包含所有需要的头，并声明我们将使用`std`和`chrono_literals`名称空间。`chrono_literals`命名空间包含用于创建时间跨度值的简便缩写:\n\n```cpp\n      #include <iostream>\n      #include <chrono>\n      #include <thread>      \n\n      using namespace std;\n      using namespace chrono_literals;\n```\n\n2.  让我们立即让主线程休眠 5 秒 300 毫秒。感谢`chrono_literals`，我们可以用非常易读的格式表达这一点:\n\n```cpp\n      int main()\n      {\n          cout << \"Going to sleep for 5 seconds\"\n                  \" and 300 milli seconds.n\";\n\n          this_thread::sleep_for(5s + 300ms);\n```\n\n3.  最后一个睡眠声明是`relative`。我们也可以表达`absolute`睡眠请求。让我们一直睡到时间点，也就是*现在的*加上`3`秒:\n\n```cpp\n          cout << \"Going to sleep for another 3 seconds.n\";\n\n          this_thread::sleep_until(\n              chrono::high_resolution_clock::now() + 3s);\n```\n\n4.  在退出程序之前，让我们打印一些其他的东西来表示第二个睡眠周期的结束:\n\n```cpp\n          cout << \"That's it.n\";\n      }\n```\n\n5.  编译和运行程序会产生以下结果。Linux、Mac 和其他类似 UNIX 的操作系统提供`time`命令，它接受另一个命令以便执行它并停止它所花费的时间。用`time`运行我们的程序显示它运行了`8.32`秒，这大致是我们让程序休眠的`5.3`和`3`秒。运行程序时，可以计算打印行到达终端之间的时间:\n\n```cpp\n      $ time ./sleep \n      Going to sleep for 5 seconds and 300 milli seconds.\n      Going to sleep for another 3 seconds.\n      That's it.\n\n      real 0m8.320s\n      user 0m0.005s\n      sys  0m0.003s\n```\n\n# 它是如何工作的...\n\n`sleep_for`和`sleep_until`函数已经被添加到 C++ 11 中，并且位于`std::this_thread`命名空间中。它们会在特定的时间内阻塞当前线程(而不是整个进程或程序)。线程被阻塞时不会消耗 CPU 时间。它只是被操作系统置于非活动状态。当然，操作系统会提醒自己再次唤醒线程。最好的一点是，我们不需要关心我们的程序在哪个操作系统上运行，因为 STL 从我们这里抽象出了这个细节。\n\n`this_thread::sleep_for`功能接受一个`chrono::duration`值。在最简单的情况下，这只是`1s`或`5s + 300ms`，就像在我们的示例代码中一样。为了获得如此好的时间跨度文字，我们需要声明`using namespace std::chrono_literals;`。\n\n`this_thread::sleep_until`功能接受`chrono::time_point`而不是时间跨度。如果我们希望让线程休眠到某个特定的挂钟时间，这是很舒服的。\n\n唤醒的时间只有在操作系统允许的情况下才是准确的。对于大多数操作系统来说，这通常是足够精确的*，但是如果某些应用需要纳秒级的粒度，这可能会变得困难。*\n\n *另一种短时间让线程休眠的可能性是`this_thread::yield`。它接受*否*的参数，这意味着我们无法知道线程的执行延迟了多长时间。原因是这个函数并没有真正实现休眠或驻留线程的概念。它只是以一种协作的方式告诉操作系统，它可以重新调度任何其他进程的任何其他线程。如果没有，那么线程将立即再次执行。由于这个原因，`yield`通常比只睡很少但特定的时间有用。\n\n# 启动和停止线程\n\nC++ 11 附带的另一个功能是`std::thread`类。它提供了一种干净简单的方式来启动和停止线程，而不需要任何外部库，也不需要知道操作系统是如何实现的。这些都包含在 STL 中。\n\n在这个食谱中，我们将实现一个启动和停止线程的程序。有一些小细节需要知道线程一旦启动后该做什么，所以我们也将讨论这些细节。\n\n# 怎么做...\n\n我们将启动多个线程，看看当我们释放多个处理器内核来同时执行部分代码时，我们的程序会如何运行:\n\n1.  首先，我们只需要包含两个头，然后我们声明使用`std`和`chrono_literals`名称空间:\n\n```cpp\n      #include <iostream>\n      #include <thread>      \n\n      using namespace std;\n      using namespace chrono_literals;\n```\n\n2.  为了启动一个线程，我们需要能够告诉它应该执行什么代码。那么，让我们定义一个可以执行的函数。函数是线程天然的潜在入口点。示例函数接受一个参数`i`，它充当线程标识。这样我们就可以知道哪个打印行来自哪个线程。此外，我们使用线程标识让所有线程等待不同的时间，因此我们可以确保它们不会试图在完全相同的时间使用`cout`。如果他们这样做了，那将会破坏输出。本章中的另一个方法专门处理这个问题:\n\n```cpp\n      static void thread_with_param(int i)\n      {\n          this_thread::sleep_for(1ms * i);\n\n          cout << \"Hello from thread \" << i << 'n';\n\n          this_thread::sleep_for(1s * i);\n\n          cout << \"Bye from thread \" << i << 'n';\n      }\n```\n\n3.  在主功能中，我们可以，只是出于好奇，使用`std::thread::hardware_concurrency`打印出可以同时运行多少个线程。这取决于机器真正有多少内核，以及 STL 实现支持多少内核。这意味着在其他每台计算机上，这可能是一个不同的数字:\n\n```cpp\n      int main()\n      {\n          cout << thread::hardware_concurrency()\n               << \" concurrent threads are supported.n\";\n```\n\n4.  让我们现在终于开始线程。每一个都有不同的标识，我们开始三个线程。当用诸如`thread t {f, x}`的表达式实例化一个线程时，这会导致新线程调用`f(x)`。这样，我们可以为每个线程赋予`thread_with_param`函数不同的参数:\n\n```cpp\n          thread t1 {thread_with_param, 1};\n          thread t2 {thread_with_param, 2};\n          thread t3 {thread_with_param, 3};\n```\n\n5.  由于这些线程是自由运行的，当它们完成工作时，我们需要再次停止它们。我们使用`join`功能来实现。它将阻塞调用线程，直到我们试图加入的线程返回:\n\n```cpp\n          t1.join();\n          t2.join();\n```\n\n6.  连接的替代方法是*分离*。如果我们不调用`join`或分离，`thread`对象的析构函数一执行，整个应用就会被大量的烟雾和噪音终止。通过调用`detach`，我们告诉`thread`我们真的想让线程 3 继续运行，即使它的`thread`实例被破坏:\n\n```cpp\n          t3.detach();\n```\n\n7.  在退出主功能和整个程序之前，我们打印另一条消息:\n\n```cpp\n          cout << \"Threads joined.n\";\n      }\n```\n\n8.  编译和运行代码显示了以下输出。我们可以看到我的机器有八个 CPU 核心。然后，我们看到来自所有线程的 *hello* 消息，但是 *bye* 消息仅来自我们实际加入的两个线程。线程 3 仍处于 3 秒的等待期，但在第二个线程完成等待 2 秒后，整个程序确实已经终止。这样，我们就看不到来自线程 3 的 bye 消息，因为它被简单地杀死了，没有任何完成的机会(也没有噪音):\n\n```cpp\n      $ ./threads \n      8 concurrent threads are supported.\n      Hello from thread 1\n      Hello from thread 2\n      Hello from thread 3\n      Bye from thread 1\n      Bye from thread 2\n      Threads joined.\n```\n\n# 它是如何工作的...\n\n启动和停止线程是一件非常简单的事情。多道程序设计开始变得复杂，线程需要一起工作(共享资源、相互等待等等)。\n\n为了启动一个线程，我们首先需要一些将由它执行的函数。这个函数不需要特别，因为一个线程实际上可以执行每个函数。让我们确定一个启动线程并等待其完成的最小示例程序:\n\n```cpp\nvoid f(int i) { cout << i << 'n'; }\n\nint main()\n{\n    thread t {f, 123};\n    t.join();\n}\n```\n\n`std::thread`的构造函数调用接受一个函数指针或一个可调用对象，后跟应该与函数调用一起使用的参数。当然，也可以在不接受任何参数的函数上启动线程。\n\n如果系统有多个 CPU 内核，那么线程可以并行运行*和*并发运行。并行和并发有什么区别？如果计算机只有一个中央处理器内核，那么可能会有许多线程并行运行，但绝不会并发运行，因为一个中央处理器内核一次只能运行一个线程。然后线程以交错的方式运行，每个线程执行一秒钟的某些部分，然后暂停，然后下一个线程获得一个时间片(对于人类用户来说，这看起来像是它们同时运行)。如果它们不需要共享一个中央处理器内核，那么它们可以并发运行，就像在*真正同时运行*一样。\n\n在这一点上，我们对以下细节绝对*无法控制*:\n\n*   共享一个中央处理器内核时线程交错的*顺序*。\n*   一个线程的*优先级*，或者哪一个比另一个更重要。\n*   事实上，线程实际上是分布在所有的中央处理器内核中的，或者如果操作系统只是将它们固定在同一个内核中。虽然机器有 100 多个内核，但我们所有的线程都只在一个内核上运行，这确实是*可能的*。\n\n大多数操作系统也提供了控制多道程序设计的这些方面的可能性，但是在这一点上，这些特性并不包含在 STL 中。\n\n但是，我们可以启动和停止线程，并告诉它们何时处理什么以及何时暂停。这对一大类应用来说应该足够了。我们在这一节中所做的是我们启动了三个额外的线程。之后，我们*加入了*大部分，而*分离了最后一个*。让我们用简单的图表总结一下发生了什么:\n\n![](img/6b848126-e59e-4cc1-baa0-6f31962879a3.png)\n\n从上到下阅读这个图表，它显示了一个时间点，在这个时间点，我们将程序工作流分成总共四个线程。我们启动了三个额外的线程来做一些事情(即等待和打印)，但是在启动线程之后，执行主功能的主线程仍然没有工作。\n\n每当一个线程完成了它开始执行的函数时，它将从这个函数返回。然后，标准库会做一些整理工作，导致线程从操作系统的时间表中移除，并可能导致其销毁，但我们不需要担心。\n\n我们*唯一需要*担心的就是*加入*。当一个线程在另一个`thread`对象上调用函数`x.join()`时，它被置于睡眠状态，直到线程`x`返回。请注意，如果线程被困在一个无尽的循环中，我们就倒霉了！如果我们想让一个线程继续生存，直到它决定终止自己，我们可以称之为`x.detach()`。这样做之后，我们就不再对线程有外部控制了。无论我们决定什么-我们*必须*始终*连接*或*分离*螺纹。如果我们不做这两者之一，`thread`对象的析构函数将调用`std::terminate()`，这将导致应用突然关闭。\n\n当我们的主函数返回时，整个应用当然会终止。然而，与此同时，我们的分离线程`t3`在向终端打印其*再见*消息之前仍在睡觉。操作系统并不在乎——它只是终止了我们的整个程序，而没有等待那个线程完成。这是我们需要考虑的事情。如果额外的线程必须完成一些重要的事情，我们必须让主函数*等待*完成它。\n\n# 使用 std::unique_lock 和 std::shared_lock 执行异常安全共享锁定\n\n由于线程的操作是一个与操作系统支持密切相关的事情，并且 STL 为此提供了良好的与操作系统无关的接口，因此为线程之间的*同步*提供 STL 支持也是明智的。这样，我们不仅可以在没有外部库的情况下启动和停止线程，还可以用来自单一统一库(STL)的抽象来同步它们。\n\n在这个食谱中，我们将看看 STL 互斥类和 RAI 锁抽象。当我们在具体的配方实现中使用其中的一些时，我们还将获得 STL 提供的更多同步助手的概述。\n\n# 怎么做...\n\n我们将编写一个程序，在其*独占*和*共享*模式下使用一个`std::shared_mutex`实例，看看这意味着什么。此外，我们自己不调用锁定和解锁功能，而是使用 RAII 助手通过自动解锁进行锁定:\n\n1.  首先，我们需要包含所有必要的标题。因为我们一直将 STL 函数和数据结构与时间文字一起使用，所以我们声明使用`std`和`chrono_literal`名称空间:\n\n```cpp\n      #include <iostream>\n      #include <shared_mutex>\n      #include <thread>\n      #include <vector>      \n\n      using namespace std;\n      using namespace chrono_literals;\n```\n\n2.  整个程序围绕一个共享互斥体，为了简单起见，让我们定义一个全局实例:\n\n```cpp\n      shared_mutex shared_mut;\n```\n\n3.  我们将使用`std::shared_lock`和`std::unique_lock` RAII 助手。为了让它们的名字看起来不那么笨拙，我们为它们定义了简短的类型别名:\n\n```cpp\n      using shrd_lck = shared_lock<shared_mutex>;\n      using uniq_lck = unique_lock<shared_mutex>;\n```\n\n4.  在开始使用主函数之前，我们定义了两个辅助函数，它们都试图在*独占*模式下锁定互斥体。这个函数将在共享互斥体上实例化一个`unique_lock`实例。第二个构造函数参数`defer_lock`告诉对象保持锁未锁定。否则，它的构造函数会试图锁定互斥体，然后阻塞直到成功。然后我们在`exclusive_lock`对象上调用`try_lock`。这个调用将立即返回，它的布尔返回值告诉我们它是否获得了锁，或者互斥锁是否已经在其他地方被锁定:\n\n```cpp\n      static void print_exclusive()\n      {\n          uniq_lck l {shared_mut, defer_lock};\n\n          if (l.try_lock()) {\n              cout << \"Got exclusive lock.n\";\n          } else {\n              cout << \"Unable to lock exclusively.n\";\n          }\n      }\n```\n\n5.  另一个助手函数也试图以独占模式锁定互斥体。它会一直锁着直到拿到锁。然后我们通过抛出一个异常来模拟一些错误情况(它只携带一个简单的整数，而不是一个更复杂的异常对象)。虽然这导致我们持有锁定互斥体的上下文立即退出，但是互斥体将被干净地再次释放。那是因为`unique_lock`的析构函数在任何情况下都会通过设计释放锁:\n\n```cpp\n      static void exclusive_throw()\n      {\n          uniq_lck l {shared_mut};\n          throw 123;\n      }\n```\n\n6.  现在来看主要功能。首先，我们打开另一个范围并实例化一个`shared_lock`实例。它的构造函数立即在`shared`模式下锁定互斥体。我们将在接下来的步骤中了解这意味着什么:\n\n```cpp\n      int main()\n      {\n          {\n              shrd_lck sl1 {shared_mut};\n\n              cout << \"shared lock once.n\";\n```\n\n7.  现在我们打开另一个作用域，并在同一个互斥体上实例化第二个`shared_lock`实例。我们现在有两个`shared_lock`实例，它们都持有互斥体的共享锁。事实上，我们可以在同一个互斥体上任意实例化多个`shared_lock`实例。然后我们调用`print_exclusive`，试图在*独占*模式下锁定互斥。这不会成功，因为它已经锁定在*共享*模式:\n\n```cpp\n              {\n                  shrd_lck sl2 {shared_mut};\n\n                  cout << \"shared lock twice.n\";\n\n                  print_exclusive();\n              }\n```\n\n8.  离开最新的作用域后，`shared_lock` `sl2`的析构函数释放对互斥体的共享锁。`print_exclusive`函数将再次失败，因为互斥体仍处于共享锁模式:\n\n```cpp\n              cout << \"shared lock once again.n\";\n\n              print_exclusive();\n\n          }\n          cout << \"lock is free.n\";\n```\n\n9.  同样离开另一个范围后，所有`shared_lock`对象被销毁，互斥体再次处于解锁状态。*现在*我们终于可以在独占模式下锁定互斥了。我们先打电话给`exclusive_throw`，然后打电话给`print_exclusive`来解决这个问题。请记住，我们在`exclusive_throw`中抛出了一个异常。但是因为`unique_lock`是一个 RAII 对象，它给了我们异常安全，所以不管我们从`exclusive_throw`怎么返回，互斥体都会被再次解锁。这样`print_exclusive`就不会阻塞错误锁定的互斥体:\n\n```cpp\n          try {\n              exclusive_throw();\n          } catch (int e) {\n              cout << \"Got exception \" << e << 'n';\n          }\n\n          print_exclusive();\n      }\n```\n\n10.  编译并运行代码会产生以下输出。前两行显示我们获得了两个共享锁实例。那么`print_exclusive`功能无法在独占模式下锁定互斥体。离开内部范围并解锁第二个共享锁后，`print_exclusive`功能仍然失败。在离开了最终再次释放互斥体的另一个范围之后，`exclusive_throw`和`print_exclusive`终于能够锁定互斥体了:\n\n```cpp\n      $ ./shared_lock \n      shared lock once.\n      shared lock twice.\n      Unable to lock exclusively.\n      shared lock once again.\n      Unable to lock exclusively.\n      lock is free.\n      Got exception 123\n      Got exclusive lock.\n```\n\n# 它是如何工作的...\n\n当查看 C++ 文档时，首先有点困惑的是有不同的互斥类和 RAII 锁助手。在查看我们的具体代码示例之前，让我们总结一下 STL 为我们提供了什么。\n\n# 互斥类\n\n术语互斥体代表 **mut** ual **ex** clusion。为了防止并发运行的线程以可能导致数据损坏的非协调方式改变同一个对象，我们可以使用互斥对象。STL 提供了不同专业的互斥类。它们都有一个共同点，那就是它们有一个`lock`和一个`unlock`方法。\n\n每当一个线程第一个对一个之前没有被锁定的互斥体调用`lock()`时，它就拥有这个互斥体。此时，其他线程将阻塞它们的`lock`调用，直到第一个线程再次调用`unlock`。`std::mutex`恰恰可以做到这一点。\n\nSTL 中有许多不同的互斥类:\n\n| **类型名称** | **描述** |\n| --- | --- |\n| `mutex` | 带有`lock`和`unlock`方法的标准互斥体。提供额外的非阻塞`try_lock`方法。 |\n| `timed_mutex` | 与互斥体相同，但提供了额外的`try_lock_for`和`try_lock_until`方法，允许*超时*，而不是永远阻塞。 |\n| `recursive_mutex` | 与`mutex`相同，但是如果一个线程已经锁定了它的一个实例，它可以在同一个互斥对象上多次调用`lock`而不会阻塞。它是在拥有名为`unlock`的线程后发布的，就像它经常名为`lock`一样。 |\n| `recursive_timed_mutex` | 提供`timed_mutex`和`recursive_mutex`的功能。 |\n| `shared_mutex` | 这个互斥体在这方面是特殊的，它可以锁定在*独占*模式和*共享*模式。在独占模式下，它显示与标准互斥类相同的行为。如果一个线程在共享模式下锁定它，其他线程也有可能在共享模式下锁定它。最后一个共享模式锁所有者一释放它，它就会被解锁。当锁在共享模式下被锁定时，不可能获得独占所有权。这和`shared_ptr`的行为很像，只是它不管理内存，而是锁所有权。 |\n| `shared_timed_mutex` | 将`shared_mutex`和`timed_mutex`的功能结合起来，用于独占和共享模式。 |\n\n# 锁定类\n\n只要线程只是锁定一个互斥体，访问一些并发保护的对象并再次解锁互斥体，一切都很好很容易。一旦一个健忘的程序员在锁定互斥体后错过了在某个地方解锁互斥体，或者当互斥体仍然被锁定时抛出了一个异常，事情看起来就会变得很糟糕。在最好的情况下，程序只是立即挂起，并且丢失的解锁调用被快速识别。然而，这种错误与内存泄漏非常相似，内存泄漏也发生在缺少显式`delete`调用的时候。\n\n关于内存管理，我们有`unique_ptr`、`shared_ptr`和`weak_ptr`。这些助手提供了非常方便的方法来避免内存泄漏。互斥体也有这样的助手。最简单的就是`std::lock_guard`。它可以如下使用:\n\n```cpp\nvoid critical_function()\n{\n    lock_guard<mutex> l {some_mutex};\n\n    // critical section\n}\n```\n\n`lock_guard`元素的构造函数接受一个互斥体，并立即调用`lock`。整个构造函数调用将阻塞，直到它获得互斥锁。销毁后，它会再次解锁互斥锁。这样很难让`lock` / `unlock`循环出错，因为它是自动发生的。\n\nC++ 17 STL 提供了以下不同的 RAI 锁助手。它们都接受一个模板参数，该参数应该与互斥体的类型相同(尽管由于 C++ 17，编译器可以自己推导出该类型):\n\n| **名称** | **描述** |\n| --- | --- |\n| `lock_guard` | 这个类只提供了一个构造函数和一个析构函数，它们分别是`lock`和`unlock`互斥体。 |\n| `scoped_lock` | 类似于`lock_guard`，但是在其构造函数中支持任意多的互斥。将在析构函数中以相反的顺序释放它们。 |\n| `unique_lock` | 以独占模式锁定互斥体。构造函数还接受指示它超时的参数，而不是永远阻塞锁调用。也有可能根本不锁定互斥体，或者假设它已经被锁定，或者只*尝试*锁定互斥体。额外的方法允许在`unique_lock`锁的有效期内锁定和解锁互斥锁。 |\n| `shared_lock` | 与`unique_lock`相同，但所有操作都以共享模式应用于互斥体。 |\n\n而`lock_guard`和`scoped_lock`有死简单的接口，只由构造函数和析构函数组成，`unique_lock`和`shared_lock`更复杂，但也更通用。我们将在本章后面的食谱中看到，如果不是简单的锁区域，它们还可以如何使用。\n\n现在让我们回到食谱代码。虽然我们只在单线程上下文中运行代码，但是我们已经看到了如何使用锁助手。`shrd_lck`类型别名代表`shared_lock<shared_mutex>`，允许我们在共享模式下多次锁定一个实例。只要`sl1`和`sl2`存在，没有`print_exclusive`调用能够在独占模式下锁定互斥。这还是很简单的。\n\n现在让我们来看看主函数后面的排他锁函数:\n\n```cpp\nint main()\n{\n    {\n        shrd_lck sl1 {shared_mut};\n        {\n            shrd_lck sl2 {shared_mut};\n\n            print_exclusive();\n        }\n        print_exclusive();\n    }\n\n    try {\n        exclusive_throw();\n    } catch (int e) {\n        cout << \"Got exception \" << e << 'n';\n    }\n    print_exclusive();\n}\n```\n\n一个重要的细节是从`exclusive_throw`返回后，`print_exclusive`函数能够再次锁定互斥体，尽管`exclusive_throw`由于抛出异常而没有干净地退出。\n\n我们再来看看`print_exclusive`，因为它使用了一个奇怪的构造函数调用:\n\n```cpp\nvoid print_exclusive()\n{\n    uniq_lck l {shared_mut, defer_lock};\n\n    if (l.try_lock()) {\n        // ...\n    }\n}\n```\n\n在本程序中，我们不仅提供了`shared_mut`而且还提供了`defer_lock`作为`unique_lock`的构造函数参数。`defer_lock`是一个空的全局对象，可以用来选择一个不同的`unique_lock`的构造函数，它只是不锁定互斥体。这样做，我们以后就能调用`l.try_lock()`了，不堵。如果互斥已经被锁定，我们可以做些别的事情。如果真的有可能拿到锁，我们还有析构函数在后面整理。\n\n# 使用 std::scoped_lock 避免死锁\n\n如果在道路交通中发生了死锁，它们看起来会像下面的情况:\n\n![](img/e02fbc8d-bc97-496b-b388-f0ecbea0e5e5.png)\n\n为了让交通再次畅通，我们要么需要一台大型起重机，从街道十字路口的中心随机挑选一辆车，并将其移走。如果这是不可能的，那么我们需要足够多的司机合作。死锁可以通过一个方向的所有司机向后行驶几米来解决，为其他司机继续行驶腾出空间。\n\n在多线程程序中，这种情况当然需要程序员严格避免。然而，当程序非常复杂时，在这方面很容易失败。\n\n在这个方法中，我们将编写故意引发死锁情况的代码。然后我们将看到如何编写获取导致其他代码陷入死锁的相同资源的代码，但是使用 C++ 17 附带的新的 STL 锁类`std::scoped_lock`，以避免这个错误。\n\n# 怎么做...\n\n本节的代码包含两对应该由并发线程执行的函数，它们以互斥体的形式获取两个资源。一对引发僵局，另一对避免僵局。在主功能中，我们将试用它们:\n\n1.  让我们首先包含所有需要的头，并声明我们使用名称空间`std`和`chrono_literals`:\n\n```cpp\n      #include <iostream>\n      #include <thread>\n      #include <mutex>      \n\n      using namespace std;\n      using namespace chrono_literals;\n```\n\n2.  然后，我们实例化两个互斥对象，这是我们遇到死锁所需要的:\n\n```cpp\n      mutex mut_a;\n      mutex mut_b;\n```\n\n3.  为了用两种资源挑起僵局，我们需要两种功能。一个函数试图锁定互斥体 A，然后锁定互斥体 B，而另一个函数将以相反的顺序锁定。通过让这两个函数在锁之间休眠一段时间，我们可以确保这段代码永远阻塞在死锁上。(这是为了演示。如果我们反复启动一个没有一些睡眠线的程序，它可能会成功运行而不会出现死锁。)\n    请注意，我们不使用`'n'`字符来打印换行符，而是使用`endl`。`endl`不仅执行换行，还会刷新`cout`的流缓冲区，因此我们可以确定打印不会被聚集和推迟:\n\n```cpp\n      static void deadlock_func_1()\n      {\n          cout << \"bad f1 acquiring mutex A...\" << endl;\n          lock_guard<mutex> la {mut_a};\n\n          this_thread::sleep_for(100ms);\n\n          cout << \"bad f1 acquiring mutex B...\" << endl;\n          lock_guard<mutex> lb {mut_b};\n\n          cout << \"bad f1 got both mutexes.\" << endl;\n      }\n```\n\n4.  正如在最后一步中承诺的那样，`deadlock_func_2`看起来与`deadlock_func_1`完全相同，但是它以相反的顺序锁定互斥体 A 和 B:\n\n```cpp\n      static void deadlock_func_2()\n      {\n          cout << \"bad f2 acquiring mutex B...\" << endl;\n          lock_guard<mutex> lb {mut_b};\n\n          this_thread::sleep_for(100ms);\n\n          cout << \"bad f2 acquiring mutex A...\" << endl;\n          lock_guard<mutex> la {mut_a};\n\n          cout << \"bad f2 got both mutexes.\" << endl;\n      }\n```\n\n5.  现在，我们为刚刚实现的这两个函数编写一个无死锁的变体。他们使用类`scoped_lock`，这个类锁定了我们作为构造函数参数提供的所有互斥。它的析构器再次解锁它们。在锁定互斥体的同时，它在内部为我们应用了一种避免死锁的策略。请注意，两个函数仍然以相反的顺序使用互斥体 A 和 B:\n\n```cpp\n      static void sane_func_1()\n      {\n          scoped_lock l {mut_a, mut_b};\n\n          cout << \"sane f1 got both mutexes.\" << endl;\n      }\n\n      static void sane_func_2()\n      {\n          scoped_lock l {mut_b, mut_a};\n\n          cout << \"sane f2 got both mutexes.\" << endl;\n      }\n```\n\n6.  在主功能中，我们将经历两个场景。首先，我们在多线程环境中使用 *sane* 函数:\n\n```cpp\n      int main()\n      {\n          {\n              thread t1 {sane_func_1};\n              thread t2 {sane_func_2};\n\n              t1.join();\n              t2.join();\n          }\n```\n\n7.  然后，我们使用不使用任何死锁避免策略的引发死锁的函数:\n\n```cpp\n          {\n              thread t1 {deadlock_func_1};\n              thread t2 {deadlock_func_2};\n\n              t1.join();\n              t2.join();\n          }\n      }\n```\n\n8.  编译并运行程序会产生以下输出。前两行显示*神智清醒*锁定功能场景工作正常，两个功能永远不阻塞返回。另外两个函数陷入了死锁。我们可以看出这是一个死锁，因为我们看到打印行告诉我们单个线程试图锁定互斥体 A 和 B，然后永远等待*。两者都没有达到成功锁定两个互斥体的程度。我们可以让这个程序运行几个小时，几天，几年，什么都不会发生。\n    该应用需要从外部取消，例如通过按键 *Ctrl* + *C* :*\n\n```cpp\n      $ ./avoid_deadlock \n      sane f1 got both mutexes\n      sane f2 got both mutexes\n      bad f2 acquiring mutex B...\n      bad f1 acquiring mutex A...\n      bad f1 acquiring mutex B...\n      bad f2 acquiring mutex A...\n```\n\n# 它是如何工作的...\n\n通过实现故意导致死锁的代码，我们已经看到了这种不想要的场景发生得有多快。在一个大型项目中，多个程序员编写的代码需要共享一组公共的互斥保护资源，所有程序员在锁定和解锁互斥时都需要遵守*相同的顺序*。虽然这样的策略或规则确实很容易遵循，但也很容易忘记。这个问题的另一个术语是*锁定顺序反转*。\n\n`scoped_lock`在这种情况下是真正的帮助。它附带了 C++ 17，工作方式与`lock_guard`和`unique_lock`相同:它的构造函数执行锁定，它的析构函数执行互斥锁的解锁。`scoped_lock`的特长是可以通过*多个*互斥体做到这一点。\n\n`scoped_lock`使用`std::lock`函数，该函数应用特殊算法，对提供的所有互斥体执行一系列`try_lock`调用，以防止死锁。因此，在同一组锁上使用`scoped_lock`或调用`std::lock`是完全安全的，但顺序不同。\n\n# 同步并发标准::cout 使用\n\n多线程程序的一个不便之处在于，我们必须切实保护它们修改的每一个*数据结构，用互斥或其他措施来防止不受控制的并发修改。*\n\n *一种通常用于打印的数据结构是`std::cout`。如果多个线程并发访问`cout`，那么输出会以有趣的混合模式出现在终端上。为了防止这种情况，我们需要编写自己的函数，以并发安全的方式打印。\n\n我们将学习如何提供一个`cout`包装器，它由最少的代码组成，并且使用起来和`cout`一样舒服。\n\n# 怎么做...\n\n在本节中，我们将实现一个从多个线程并发打印到终端的程序。为了防止由于并发而导致的消息乱码，我们实现了一个小的助手类来同步线程之间的打印:\n\n1.  一如既往，首先包括:\n\n```cpp\n      #include <iostream>\n      #include <thread>\n      #include <mutex>\n      #include <sstream>\n      #include <vector>      \n\n      using namespace std;\n```\n\n2.  然后我们实现我们的助手类，我们称之为`pcout`。`p`代表*并行*，因为它在并行上下文中以同步的方式工作。想法是`pcout`公开继承`stringstream`。这样我们就可以在它的实例上使用`operator<<`。一旦`pcout`实例被销毁，它的析构函数就锁定一个互斥体，然后打印`stringstream`缓冲区的内容。我们将在下一步中了解如何使用它:\n\n```cpp\n      struct pcout : public stringstream {\n          static inline mutex cout_mutex;\n\n          ~pcout() {\n              lock_guard<mutex> l {cout_mutex};\n              cout << rdbuf();\n              cout.flush();\n          }\n      };\n```\n\n3.  现在让我们编写两个可以由附加线程执行的函数。两者都接受线程标识作为参数。那么，它们唯一的区别就是第一个简单的用`cout`进行打印。另一个看起来几乎一样，但是它没有直接使用`cout`，而是实例化了`pcout`。这个实例是一个临时对象，只为这一行代码而存在。执行完所有`operator<<`调用后，内部字符串流中会填充我们想要打印的内容。然后调用`pcout`实例的析构函数。我们已经看到析构函数的作用:它锁定所有`pcout`实例共享的特定互斥体，并打印:\n\n```cpp\n      static void print_cout(int id)\n      {\n          cout << \"cout hello from \" << id << 'n';\n      }\n\n      static void print_pcout(int id)\n      {\n           pcout{} << \"pcout hello from \" << id << 'n';\n      }\n```\n\n4.  让我们试试看。首先我们要用`print_cout`，它只是用`cout`进行打印。我们启动 10 个并发打印字符串的线程，并等待它们完成:\n\n```cpp\n      int main()\n      {\n          vector<thread> v;\n\n          for (size_t i {0}; i < 10; ++ i) {\n              v.emplace_back(print_cout, i);\n          }\n\n          for (auto &t : v) { t.join(); }\n```\n\n5.  然后我们用`print_pcout`函数做同样的事情:\n\n```cpp\n          cout << \"=====================n\";\n\n          v.clear();\n          for (size_t i {0}; i < 10; ++ i) {\n              v.emplace_back(print_pcout, i);\n          }\n\n          for (auto &t : v) { t.join(); }\n      }\n```\n\n6.  编译并运行程序会产生以下结果。如我们所见，前 10 张照片完全乱码了。这就是`cout`在没有锁定的情况下并发使用时的样子。节目的最后 10 行是`print_pcout`行，没有任何乱码的迹象。我们可以看到它们是从不同的线程中打印出来的，因为每次我们再次运行程序时，它们的顺序都是随机的:\n\n![](img/c9208c3b-6c0c-4187-aaba-dd51820c5c5e.png)\n\n# 它是如何工作的...\n\n好了，我们已经构建了这个*“cout 包装器”*，它自动序列化并发打印尝试。它是如何工作的？\n\n让我们按照我们的`pcout`助手手动完成的步骤来做，不需要任何魔法。首先，它实例化一个字符串流，并接受我们输入的输入:\n\n```cpp\nstringstream ss;\nss << \"This is some printed line \" << 123 << 'n';\n```\n\n然后它锁定一个全局可用的互斥体:\n\n```cpp\n{\n    lock_guard<mutex> l {cout_mutex};\n```\n\n在这个锁定的范围内，它访问字符串流`ss`的内容，打印它，并通过离开范围再次释放互斥体。`cout.flush()`行告诉流对象立即打印到终端。如果没有这一行，程序可能会运行得更快，因为多行打印的行可以在以后的一次运行中集中打印。在我们的食谱中，我们希望立即看到所有输出行，因此我们使用`flush`方法:\n\n```cpp\n    cout << ss.rdbuf();\n    cout.flush();\n}\n```\n\n好吧，如果我们不得不一次又一次地做同样的事情，这很简单，但是写起来很乏味。我们可以将`stringstream`实例化缩短如下:\n\n```cpp\nstringstream{} << \"This is some printed line \" << 123 << 'n';\n```\n\n这将实例化一个字符串流对象，将我们想要打印的所有内容输入其中，然后再次对其进行析构。字符串流的生存期缩短到只有这一行。之后，我们无法再打印它，因为我们无法访问它。哪个代码是最后一个能够访问流内容的代码？是`stringstream`的析构器。\n\n我们不能修改`stringstream`实例的成员方法，但是我们可以通过继承将自己的类型包装在它周围来扩展它们:\n\n```cpp\nstruct pcout : public stringstream {\n    ~pcout() {\n        lock_guard<mutex> l {cout_mutex};\n        cout << rdbuf();\n        cout.flush();\n    }\n};\n```\n\n这个类*仍然是*一个字符串流，我们可以像使用任何其他字符串流一样使用它。唯一不同的是，它会锁定一个互斥体，并使用`cout`打印自己的缓冲区。\n\n我们还将`cout_mutex`对象作为静态实例移动到结构`pcout`中，因此我们将两者捆绑在一个地方。\n\n# 使用 std::call_once 安全地推迟初始化\n\n有时我们有特定的代码段，可以由多个线程在并行上下文中运行，义务是一些*设置代码*必须在执行实际函数之前恰好执行一次。一个简单的解决方案是，在程序进入可以不时执行并行代码的状态之前，只执行现有的设置函数。\n\n这种方法的缺点如下:\n\n*   如果并行函数来自库，用户一定不要忘记调用设置函数。这并没有让图书馆更容易使用。\n*   如果设置函数在某种程度上是昂贵的，并且在需要该设置的并行函数甚至不总是被使用的情况下，它甚至可能不需要被执行，那么我们需要决定何时/是否运行它的代码。\n\n在这个食谱中，我们将看一下`std::call_once`，这是一个以简单易用且优雅含蓄的方式为我们解决这个问题的助手函数。\n\n# 怎么做...\n\n我们将编写一个程序，用完全相同的代码启动多个线程。尽管它们被编程为执行完全相同的代码，但我们的示例设置函数只会被调用一次:\n\n1.  首先，我们需要包含所有必要的标题:\n\n```cpp\n      #include <iostream>\n      #include <thread>\n      #include <mutex>\n      #include <vector>     \n\n      using namespace std;\n```\n\n2.  稍后我们将使用`std::call_once`。为了使用它，我们需要某个地方的`once_flag`实例。在特定功能上使用`call_once`的所有线程的同步都需要它:\n\n```cpp\n      once_flag callflag;\n```\n\n3.  只能执行一次的函数如下。它只打印一个感叹号:\n\n```cpp\n      static void once_print()\n      {\n          cout << '!';\n      }\n```\n\n4.  所有线程都将执行打印功能。我们做的第一件事就是通过函数`std::call_once`调用函数`once_print`。`call_once`需要我们之前定义的变量`callflag`。它将使用它来编排线程:\n\n```cpp\n      static void print(size_t x)\n      {\n          std::call_once(callflag, once_print);\n          cout << x;\n      }\n```\n\n5.  好了，现在让我们开始 10 个线程，它们都使用`print`功能:\n\n```cpp\n      int main()\n      {\n          vector<thread> v;\n\n          for (size_t i {0}; i < 10; ++ i) {\n              v.emplace_back(print, i);\n          }\n\n          for (auto &t : v) { t.join(); }\n          cout << 'n';\n      }\n```\n\n6.  编译和运行会产生以下输出。首先，我们从`once_print`功能看到感叹号。然后我们看到所有线程标识。`call_once`不仅确保`once_print`只被叫过一次。此外，它还同步了所有线程，因此在执行 `once_print`之前不会打印任何标识*:*\n\n```cpp\n      $ ./call_once\n      !1239406758\n```\n\n# 它是如何工作的...\n\n`std:call_once`像屏障一样工作。它维护对函数(或可调用对象)的访问。第一个到达它的线程开始执行函数。在完成之前，到达`call_once`线的任何其他螺纹都被阻塞。第一个线程从函数返回后，所有其他线程也被释放。\n\n为了组织这个小编排，需要一个变量，其他线程可以从中确定它们是否必须等待以及何时再次释放。这就是我们的变量`once_flag callflag;`的意义。每一行`call_once`还需要一个`once_flag`实例作为参数，前置一个只能调用一次的函数。\n\n另一个很好的细节是:如果发生了`call_once` *中被选中执行函数的线程由于抛出了一些*异常*而失败*，那么下一个线程被允许再次执行函数。发生这种情况是希望下次不会抛出异常。\n\n# 使用 std::async 将任务的执行推到后台\n\n每当我们想要在后台执行一些代码时，我们可以简单地启动一个新的线程来执行这些代码。当这种情况发生时，我们可以做些别的事情，然后等待结果。很简单:\n\n```cpp\nstd::thread t {my_function, arg1, arg2, ...};\n// do something else\nt.join(); // wait for thread to finish\n```\n\n但随之而来的不便就开始了:`t.join()`并没有给我们`my_function`的返回值。为了做到这一点，我们需要编写一个调用`my_function`的函数，并将它的返回值存储在某个变量中，这个变量对于我们启动新线程的第一个线程也是可访问的。如果这种情况反复发生，那么这就代表了我们不得不一次又一次地编写大量的样板代码。\n\n从 C++ 11 开始，我们就有了`std::async`，它可以为我们完成这项工作，而不仅仅是这项工作。在这个食谱中，我们将编写一个简单的程序，使用异步函数调用同时做多件事情。由于`std::async`比单独的那个更强大一点，我们将仔细看看它的不同方面。\n\n# 怎么做...\n\n我们将实现一个同时做多件不同事情的程序，但是我们使用`std::async`和`std::future`来代替显式启动线程:\n\n1.  首先，我们包括所有必要的头，并声明我们使用`std`命名空间:\n\n```cpp\n      #include <iostream>\n      #include <iomanip>\n      #include <map>\n      #include <string>\n      #include <algorithm>\n      #include <iterator>\n      #include <future>      \n\n      using namespace std;\n```\n\n2.  我们实现了三个功能，这些功能与并行性无关，但可以完成有趣的任务。第一个函数接受一个字符串，并创建该字符串中所有字符的直方图:\n\n```cpp\n      static map<char, size_t> histogram(const string &s)\n      {\n          map<char, size_t> m;\n\n          for (char c : s) { m[c] += 1; }\n\n          return m;\n      }\n```\n\n3.  第二个函数也接受一个字符串并返回它的排序副本:\n\n```cpp\n      static string sorted(string s)\n      {\n          sort(begin(s), end(s));\n          return s;\n      }\n```\n\n4.  第三个计算它接受的字符串中有多少元音:\n\n```cpp\n      static bool is_vowel(char c)\n      {\n          char vowels[] {\"aeiou\"};\n          return end(vowels) != \n                 find(begin(vowels), end(vowels), c);\n      }\n\n      static size_t vowels(const string &s)\n      {\n          return count_if(begin(s), end(s), is_vowel);\n      }\n```\n\n5.  在主函数中，我们将整个标准输入读入一个字符串。为了不把输入分割成文字，我们停用`ios::skipws`。这样，无论输入包含多少空格，我们都会得到一个大字符串。之后我们在结果字符串上使用`pop_back`，因为我们得到了一个终止`''`字符的字符串，这种方式太多了:\n\n```cpp\n      int main()\n      {\n          cin.unsetf(ios::skipws);\n          string input {istream_iterator<char>{cin}, {}};\n          input.pop_back();\n```\n\n6.  现在让我们从之前实现的所有函数中获取返回值。为了加快超长输入的执行速度，我们异步*启动它们。`std::async`函数接受一个策略、一个函数以及该函数的参数。我们称`histogram`、`sorted`、`vowels`和`launch::async`为政策(我们稍后会看到这意味着什么)。所有函数都获得相同的输入字符串作为参数:*\n\n```cpp\n          auto hist        (async(launch::async, \n                                  histogram, input));\n          auto sorted_str  (async(launch::async, \n                                  sorted,    input));\n          auto vowel_count (async(launch::async, \n                                  vowels,    input));\n```\n\n7.  `async`调用会立即返回，因为它们实际上并不执行我们的功能。相反，他们建立了同步结构，这将在以后获得函数调用的结果。结果现在由额外的线程同时计算。与此同时，我们可以自由地做任何我们想做的事情，因为我们可以在以后重拾这些价值观。返回值`hist`、`sorted_str`和`vowel_count`属于函数`histogram`、`sorted`和`vowels`返回的类型，但是它们被`std::async`包装在一个`future`类型中。这种类型的对象表示它们将在某个时间点包含它们的值。通过在它们上面使用`.get()`，我们可以制作主功能块，直到值到达，然后使用它们进行打印:\n\n```cpp\n          for (const auto &[c, count] : hist.get()) {\n              cout << c << \": \" << count << 'n';\n          }\n\n          cout << \"Sorted string: \" \n               << quoted(sorted_str.get()) << 'n'\n               << \"Total vowels: \"  \n               << vowel_count.get()        << 'n';\n      }\n```\n\n8.  编译和运行代码如下所示。我们使用了一个简短的示例字符串，它并不值得并行化，但是为了这个示例，代码仍然是并发执行的。此外，与简单的顺序版本相比，程序的整体结构没有太大变化:\n\n```cpp\n      $ echo \"foo bar baz foobazinga\" | ./async \n       : 3\n      a: 4\n      b: 3\n      f: 2\n      g: 1\n      i: 1\n      n: 1\n      o: 4\n      r: 1\n      z: 2\n      Sorted string: \"   aaaabbbffginoooorzz\"\n      Total vowels: 9\n```\n\n# 它是如何工作的...\n\n如果我们不使用`std::async`的话，串行非并行代码可能看起来就这么简单:\n\n```cpp\nauto hist        (histogram(input));\nauto sorted_str  (sorted(   input));\nauto vowel_count (vowels(   input));\n\nfor (const auto &[c, count] : hist) {\n    cout << c << \": \" << count << 'n';\n}\ncout << \"Sorted string: \" << quoted(sorted_str) << 'n';\ncout << \"Total vowels: \"  << vowel_count        << 'n';\n```\n\n为了并行化代码，我们所做的唯一一件事如下。我们将这三个函数调用包装成`async(launch::async, ...)`调用。这样，这三个函数就不会被我们当前运行的主线程执行。相反，`async`启动新的线程，并让它们同时执行这些功能。这样，我们只需执行启动另一个线程的开销，就可以继续下一行代码，而所有工作都在后台进行:\n\n```cpp\nauto hist        (async(launch::async, histogram, input));\nauto sorted_str  (async(launch::async, sorted,    input));\nauto vowel_count (async(launch::async, vowels,    input));\n\nfor (const auto &[c, count] : hist.get()) {\n    cout << c << \": \" << count << 'n';\n}\ncout << \"Sorted string: \" \n     << quoted(sorted_str.get()) << 'n'\n     << \"Total vowels: \"  \n     << vowel_count.get()        << 'n';\n```\n\n例如`histogram`返回给我们一个地图实例，`async(..., histogram, ...)`返回给我们一个之前包裹在`future`对象中的地图。这个`future`对象是一个空的*占位符*，直到执行`histogram`函数的线程返回。然后，生成的地图被放入`future`对象中，这样我们就可以最终访问它。`get`功能让我们可以访问封装的结果。\n\n让我们看看另一个最小的例子。考虑以下代码片段:\n\n```cpp\nauto x (f(1, 2, 3));\ncout << x;\n```\n\n除了编写前面的代码，我们还可以执行以下操作:\n\n```cpp\nauto x (async(launch::async, f, 1, 2, 3));\ncout << x.get();\n```\n\n基本上就是这样。在标准 C++ 中，在后台执行任务可能从来没有这么容易过。还有一件事需要解决:`launch::async`是什么意思？`launch::async`是定义启动策略的标志。有两个策略标志允许三个星座:\n\n| **政策选择** | **表示** |\n| --- | --- |\n| `launch::async` | 该函数保证由另一个线程执行。 |\n| `launch::deferred` | 该函数由同一个线程执行，但稍后(*懒评估*)。当`get`或`wait`在未来被调用时，执行就发生了。如果两者都没有发生*，则该功能根本不被称为*。** |\n| `launch::async &#124; launch::deferred` | 设置了这两个标志后，STL 的`async`实现可以自由选择应该遵循哪个策略。如果没有提供策略，这是默认选择。 |\n\nBy just calling `async(f, 1, 2, 3)` without a policy argument, we automatically select *both* policies. The implementation of `async` is then free to choose which policy to employ. This means that we cannot be *sure* that another thread is started at all, or if the execution is just deferred in the current thread.\n\n# 还有更多...\n\n确实还有最后一件事我们应该知道。假设，我们编写如下代码:\n\n```cpp\nasync(launch::async, f);\nasync(launch::async, g);\n```\n\n这可能是因为在并发线程中执行函数`f`和`g`(在本例中，我们不关心它们的返回值)，然后同时做不同的事情。当运行这样的代码时，我们会注意到代码*在这个调用上阻塞了*，这很可能不是我们想要的。\n\n那么它为什么会阻塞呢？`async`不都是无阻塞异步调用吗？是的，但是有一个特殊的特性:如果通过`launch::async`策略的异步调用获得了未来，那么它的析构函数执行*阻塞等待*。\n\n这意味着这个短示例中的*和*异步调用都被阻塞了，因为它们返回的期货的生命周期在同一行结束！我们可以通过在寿命更长的变量中捕获它们的返回值来解决这个问题。\n\n# 用 std::condition_variable 实现生产者/消费者习惯用法\n\n在这个食谱中，我们将实现一个典型的具有多线程的生产者/消费者程序。总的想法是，有一个线程产生项目，并将它们放入队列。然后还有另一个线程消耗这样的项目。如果没有要生产的东西，生产者线程就会休眠。如果队列中没有要消费的项目，消费者就会休眠。\n\n由于两个线程都可以访问的队列在一个项目被产生或消费时也会被两个线程修改，所以它需要被互斥体保护。\n\n另一个需要考虑的问题是:如果队列中没有商品，消费者会怎么做？它是否每秒轮询一次队列，直到看到新项目？这是没有必要的，因为我们可以让消费者等待由生产者触发的唤醒*事件*，只要有新的项目。\n\nC++ 11 为这类事件提供了一个很好的数据结构`std::condition_variable`。在这个食谱中，我们将实现一个简单的生产者/消费者应用来利用这一点。\n\n# 怎么做...\n\n我们将实现一个简单的生产者/消费者程序，它在自己的线程中运行单个值生产者，在另一个线程中运行单个消费者线程:\n\n1.  首先，我们需要执行所有需要的操作，包括:\n\n```cpp\n      #include <iostream>\n      #include <queue>\n      #include <tuple>\n      #include <condition_variable>\n      #include <thread>      \n\n      using namespace std;\n      using namespace chrono_literals;\n```\n\n2.  我们实例化一个简单数值的队列，并将其称为`q`。生产者将把价值推入其中，消费者将从中获取价值。为了同步两者，我们需要一个互斥体。除此之外，我们还实例化了一个`condition_variable` `cv`。变量`finished`将是生产者告诉消费者不再有价值的方式:\n\n```cpp\n      queue<size_t>      q;\n      mutex              mut;\n      condition_variable cv;\n      bool               finished {false};\n```\n\n3.  让我们首先实现生产者函数。它接受一个参数`items`，该参数限制了产品的最大数量。在一个简单的循环中，它将为每个项目休眠 100 毫秒，这模拟了一些计算*复杂性*。然后我们锁定同步队列访问的互斥体。成功生产并插入队列后，我们称之为`cv.notify_all()`。该功能唤醒消费者。稍后我们将在消费者端看到这是如何工作的:\n\n```cpp\n      static void producer(size_t items) {\n          for (size_t i {0}; i < items; ++ i) {\n              this_thread::sleep_for(100ms);\n              {\n                  lock_guard<mutex> lk {mut};\n                  q.push(i);\n              }\n              cv.notify_all();\n          }\n```\n\n4.  在产生所有项目之后，我们再次锁定互斥体，因为我们要改变设置`finished`位。然后我们再次调用`cv.notify_all()`:\n\n```cpp\n          {\n              lock_guard<mutex> lk {mut};\n              finished = true;\n          }\n          cv.notify_all();\n      }\n```\n\n5.  现在我们可以实现消费函数了。它不需要参数，因为它会盲目地消耗，直到队列清空。在只要`finished`没有设置就执行的循环中，它将首先锁定保护队列和`finished`标志的互斥体。一旦有了锁，它就用锁和 lambda 表达式作为参数调用`cv.wait`。lambda 表达式是一个谓词，它告诉生产者线程是否还活着，以及队列中是否有要消费的东西:\n\n```cpp\n      static void consumer() {\n          while (!finished) {\n              unique_lock<mutex> l {mut};\n\n              cv.wait(l, [] { return !q.empty() || finished; });\n```\n\n6.  `cv.wait`调用解锁锁并等待，直到谓词函数描述的条件成立。然后，它再次锁定互斥体，并消耗队列中的所有内容，直到它显示为空。如果生产者还活着，它将再次遍历循环。否则，它将终止，因为`finished`被设置，这是生产者发出不再生产物品的信号的方式:\n\n```cpp\n              while (!q.empty()) {\n                  cout << \"Got \" << q.front() \n                       << \" from queue.n\";\n                  q.pop();\n              }\n          }\n      }\n```\n\n7.  在主函数中，我们启动一个生产 10 个项目的生产者线程和一个消费者线程。然后我们等到他们完成并终止程序:\n\n```cpp\n      int main() {\n          thread t1 {producer, 10};\n          thread t2 {consumer};\n          t1.join();\n          t2.join();\n          cout << \"finished!n\";\n      }\n```\n\n8.  编译并运行程序会产生以下输出。当程序执行时，我们可以看到每行之间有一些时间(100 毫秒)，因为项目的生产需要一些时间:\n\n```cpp\n      $ ./producer_consumer\n      Got 0 from queue.\n      Got 1 from queue.\n      Got 2 from queue.\n      Got 3 from queue.\n      Got 4 from queue.\n      Got 5 from queue.\n      Got 6 from queue.\n      Got 7 from queue.\n      Got 8 from queue.\n      Got 9 from queue.\n      finished!\n```\n\n# 它是如何工作的...\n\n在这个食谱中，我们只是启动了两个线程。第一个线程产生项目并将它们放入队列。另一个从队列中取出项目。每当这些线程中的一个以任何方式接触到队列时，它都会锁定两个线程都可以访问的公共互斥体`mut`。这样，我们确保了不会发生两个线程同时操作队列状态的情况。\n\n除了队列和互斥体之外，我们一般声明了生产者-消费者事件中涉及的四个变量:\n\n```cpp\nqueue<size_t>      q;\nmutex              mut;\ncondition_variable cv;\nbool               finished {false};\n```\n\n变量`finished`很容易解释。当生产者生产完固定数量的产品时，它被设置为`true`。当消费者看到这个变量是`true`时，它会消耗队列中的最后一个项目并停止消耗。但是`condition_variable`T4 是干什么的呢？我们在两个不同的上下文中使用了`cv`。其中一个上下文是*等待一个特定的条件*，另一个上下文是*发出该条件的信号*。\n\n等待特定条件的消费者方是这样的。使用者线程在一个块上循环，该块首先将互斥体`mut`锁定在`unique_lock`中。然后它叫`cv.wait`:\n\n```cpp\nwhile (!finished) {\n    unique_lock<mutex> l {mut};\n\n cv.wait(l, [] { return !q.empty() || finished; });\n\n    while (!q.empty()) {\n        // consume\n    }\n}\n```\n\n该代码在某种程度上*相当于下面的替代代码。我们将很快详细说明为什么它不是真正相同的:*\n\n```cpp\nwhile (!finished) {\n    unique_lock<mutex> l {mut};\n\n while (q.empty() && !finished) {\n l.unlock();\n l.lock();\n }\n\n    while (!q.empty()) {\n        // consume\n    }\n}\n```\n\n这意味着我们通常首先获取锁，然后检查我们的场景:\n\n1.  有可以消费的物品吗？然后保持锁，消耗，释放锁，重新开始。\n2.  否则，如果没有*可消耗物品*但是生产者仍然*活着，*释放互斥体给生产者一个增加物品到队列的机会。然后，尝试再次锁定它，希望情况发生变化，我们可以看到情况 1。\n\n`cv.wait`线不等同于`while (q.empty() && ... )`构造的真正原因是，我们不能简单地循环一个`l.unlock(); l.lock();`循环。如果生产者线程在一段时间内处于非活动状态，那么这将导致互斥锁的持续锁定和解锁，这是没有意义的，因为它不必要地消耗了 CPU 周期。\n\n类似`cv.wait(lock, predicate)`的表情会等到`predicate()`返回`true`。但它不是通过不断解锁和锁定`lock`来做到这一点的。为了唤醒阻塞`condition_variable`对象的`wait`调用的线程，另一个线程必须在同一对象上调用`notify_one()`或`notify_all()`方法。只有这样，等待线程才会被踢出休眠状态，以检查`predicate()`是否成立。\n\n检查谓词的`wait`调用的好处是，如果有一个*虚假的*唤醒调用，线程将立即再次进入睡眠状态。这意味着，如果我们有太多的 notify 调用，它不会真正损害程序流(但可能会影响性能)。\n\n在制作方方面，我们只是在制作方将一个项目插入队列并制作出最后一个项目并将`finished`标志设置为`true`后调用`cv.notify_all()`。这足以引导消费者。\n\n# 用 std::condition_variable 实现多生产者/消费者习惯用法\n\n让我们从上一个食谱中拾起生产者/消费者的问题，让它变得更复杂一点:我们让*多个*生产者生产物品，*多个*消费者消费物品。除此之外，我们还定义了队列不得超过最大大小。\n\n这种方式不仅消费者在队列中没有商品时要不时睡觉，而且生产者在队列中有足够多的*商品时也要不时睡觉。*\n\n *我们将看到如何用多个`std::condition_variable`对象来解决这个问题，并将使用它们的方式与上一个食谱中略有不同。\n\n# 怎么做...\n\n在本节中，我们将实现一个与之前的配方一样的程序，但这次有多个生产者和多个消费者:\n\n1.  首先，我们需要包含所有需要的头，我们声明我们使用命名空间`std`和`chrono_literals`:\n\n```cpp\n      #include <iostream>\n      #include <iomanip>\n      #include <sstream>\n      #include <vector>\n      #include <queue>\n      #include <thread>\n      #include <mutex>\n      #include <condition_variable>\n      #include <chrono>     \n\n      using namespace std;\n      using namespace chrono_literals;\n```\n\n2.  然后，我们从本章的另一个配方中实现同步打印助手，因为我们将进行大量并发打印:\n\n```cpp\n      struct pcout : public stringstream {\n          static inline mutex cout_mutex;\n          ~pcout() {\n              lock_guard<mutex> l {cout_mutex};\n              cout << rdbuf();\n          }\n      };\n```\n\n3.  所有生产者将值写入同一队列，所有消费者也将从该队列中取出值。除了队列之外，我们还需要一个互斥体来保护队列，以及一个标志来判断生产是否在某个时刻停止:\n\n```cpp\n      queue<size_t> q;\n      mutex         q_mutex; \n      bool          production_stopped {false};\n```\n\n4.  我们将在这个程序中使用两种不同的`condition_variables`。在单个生产者/消费者配方中，我们有一个`condition_variable`告诉我们队列中有新的项目。在这种情况下，我们让它变得更复杂一点。我们希望生产商生产，直到队列中包含一定数量的*库存*物品。如果达到库存数量，他们将*休眠*。通过这种方式，`go_consume`变量可以用来唤醒消费者，然后消费者又可以用`go_produce`变量唤醒生产者:\n\n```cpp\n      condition_variable go_produce;\n      condition_variable go_consume;\n```\n\n5.  生产者函数接受生产者标识号、要生产的产品总数和库存限制作为参数。然后它进入自己的生产循环。在那里，它首先锁定队列的互斥体，并在`go_produce.wait`调用中再次解锁。它等待队列大小低于`stock`阈值的条件:\n\n```cpp\n      static void producer(size_t id, size_t items, size_t stock)\n      {\n          for (size_t i = 0; i < items; ++ i) {\n              unique_lock<mutex> lock(q_mutex);\n              go_produce.wait(lock, \n                  [&] { return q.size() < stock; });\n```\n\n6.  生产者被唤醒后，它产生一个项目并将其推入队列。队列值由表达式`id * 100 + i`计算得出。这样我们以后可以看到是哪个制作人制作的，因为数字中的百是制作人 ID。我们还将生产事件打印到终端。打印的格式可能看起来很奇怪，但它将与终端中的消费者输出很好地保持一致:\n\n```cpp\n              q.push(id * 100 + i);\n\n              pcout{} << \"   Producer \" << id << \" --> item \"\n                      << setw(3) << q.back() << 'n';\n```\n\n7.  生产后，我们可以唤醒沉睡的消费者。90 毫秒的睡眠时间模拟生产物品需要一些时间:\n\n```cpp\n              go_consume.notify_all();\n              this_thread::sleep_for(90ms);\n           }\n\n           pcout{} << \"EXIT: Producer \" << id << 'n';\n      }\n```\n\n8.  现在转到只接受消费者标识作为参数的消费者函数。如果生产没有停止，或者队列不是空的，它将继续等待项目。如果队列为空，但生产尚未停止，则可能很快会有新项目:\n\n```cpp\n      static void consumer(size_t id)\n      {\n           while (!production_stopped || !q.empty()) {\n               unique_lock<mutex> lock(q_mutex);\n```\n\n9.  锁定队列互斥体后，我们再次解锁，以便等待`go_consume`事件变量。lambda 表达式参数描述了当队列包含项时，我们希望从等待调用中返回。第二个论点`1s`告诉我们，我们不想永远等待。如果时间超过 1 秒，我们希望退出等待功能。我们可以区分`wait_for`函数是因为谓词条件成立而返回，还是因为超时而退出，因为超时时它会返回`false`。如果队列中有新的项目，我们会使用它们并将此事件打印到终端:\n\n```cpp\n               if (go_consume.wait_for(lock, 1s, \n                       [] { return !q.empty(); })) {\n                   pcout{} << \"                  item \"\n                           << setw(3) << q.front() \n                           << \" --> Consumer \"\n                           << id << 'n';\n                   q.pop();\n```\n\n10.  在项目消费之后，我们通知生产者并休眠 130 毫秒，以模拟消费项目也很耗时:\n\n```cpp\n                  go_produce.notify_all();\n                  this_thread::sleep_for(130ms);\n              }\n          }\n\n          pcout{} << \"EXIT: Producer \" << id << 'n';\n      }\n```\n\n11.  在主函数中，我们为工作线程实例化一个向量，为消费线程实例化另一个向量:\n\n```cpp\n      int main()\n      {\n          vector<thread> workers;\n          vector<thread> consumers;\n```\n\n12.  然后我们生成三个生产者线程和五个消费者线程:\n\n```cpp\n          for (size_t i = 0; i < 3; ++ i) {\n              workers.emplace_back(producer, i, 15, 5);\n          }\n\n          for (size_t i = 0; i < 5; ++ i) {\n              consumers.emplace_back(consumer, i);\n          }\n```\n\n13.  我们首先让生产者线程完成。所有人一回来，我们就设置`production_stopped`标志，也将带领消费者完成。我们需要收集这些，然后我们可以退出该计划:\n\n```cpp\n          for (auto &t : workers)   { t.join(); }\n          production_stopped = true;\n          for (auto &t : consumers) { t.join(); }\n      }\n```\n\n14.  编译和运行程序会产生以下输出。输出很长，这就是为什么在这里被截断。我们可以看到生产者时不时地睡觉，让消费者吃光一些物品，直到他们最终再次生产。改变生产者/消费者的等待时间，以及操纵生产者/消费者的数量和库存项目是很有趣的，因为这完全改变了输出模式:\n\n```cpp\n      $ ./multi_producer_consumer\n         Producer 0 --> item   0\n         Producer 1 --> item 100\n                        item   0 --> Consumer 0\n         Producer 2 --> item 200\n                        item 100 --> Consumer 1\n                        item 200 --> Consumer 2\n         Producer 0 --> item   1\n         Producer 1 --> item 101\n                        item   1 --> Consumer 0\n      ...\n         Producer 0 --> item  14\n      EXIT: Producer 0\n         Producer 1 --> item 114\n      EXIT: Producer 1\n                        item  14 --> Consumer 0\n         Producer 2 --> item 214\n      EXIT: Producer 2\n                        item 114 --> Consumer 1\n                        item 214 --> Consumer 2\n      EXIT: Consumer 2\n      EXIT: Consumer 3\n      EXIT: Consumer 4\n      EXIT: Consumer 0\n      EXIT: Consumer 1\n```\n\n# 它是如何工作的...\n\n这个食谱是前面食谱的延伸。我们实现了一个将`M`生产者与`N`消费者同步的程序，而不是只将一个生产者与一个消费者同步。除此之外，不仅消费者会在没有剩余商品的情况下睡觉，商品队列变得*太长*时，生产者也会睡觉。\n\n当多个消费者等待同一个队列填满时，这通常也适用于来自一个生产者/一个消费者场景的消费者代码。只要只有一个线程锁定保护队列的互斥体，然后从其中取出项目，代码就是安全的。有多少线程同时等待锁并不重要。这同样适用于生产者，因为在这两种情况下，唯一重要的事情是队列永远不会被一个以上的线程同时访问。\n\n因此，让这个程序比仅仅运行一个生产者/一个消费者的例子更复杂的是，当项目队列长度达到某个阈值时，我们就让生产者线程停止。为了满足这一要求，我们用自己的`condition_variable`实现了两种不同的信号:\n\n1.  `go_produce`表示队列没有完全填满到最大值，生产者可能会再次填满。\n2.  `go_consume`表示队列达到最大长度，消费者可以再次自由消费物品。\n\n这样，生产者将项目填充到队列中，并向消费线程发送`go_consume`事件信号，消费线程在下面一行等待:\n\n```cpp\nif (go_consume.wait_for(lock, 1s, [] { return !q.empty(); })) {\n    // got the event without timeout\n}\n```\n\n另一方面，生产者在下面的线上等待，直到他们被允许再次生产:\n\n```cpp\ngo_produce.wait(lock, [&] { return q.size() < stock; });\n```\n\n一个有趣的细节是，我们不会让消费者永远等待*。在`go_consume.wait_for`调用中，我们额外添加了 1 秒的超时参数。这是消费者的退出机制:如果队列空了一秒钟以上，可能就没有活跃的生产者了。*\n\n *为了简单起见，代码试图将队列长度*始终保持在最大值*。一个更复杂的程序可以让消费者线程发送唤醒通知，只有当队列只有最大长度的一半大时*。这样，生产者将在队列再次清空之前被唤醒，但不会在队列中仍然有足够的项目时被不必要地提前唤醒。*\n\n *`condition_variable`为我们优雅地解决的一种情况如下:如果消费者发出`go_produce`通知，可能会有一大群生产商竞相生产下一件商品。如果只有一个项目丢失，那么将只有一个生产者生产它。如果所有的生产者总是在`go_produce`事件一触发就生产一个项目，我们经常会看到队列被填满超过其允许的最大值的情况。\n\n让我们想象一下这样一种情况，我们在队列中有`(max - 1)`个项目，并且想要生产一个新项目，以便队列再次被填满。无论一个消费线程调用`go_produce.notify_one()`(只唤醒一个等待线程)还是`go_produce.notify_all()`(唤醒所有*等待线程)，我们都保证只有一个生产者线程会退出`go_produce.wait`调用，因为对于所有其他生产者线程来说，`q.size() < stock`等待条件在被唤醒后一旦获得互斥体就不再成立。*\n\n *# 使用 std::async 并行化 ASCII 曼德勃罗渲染器\n\n还记得[第 23 章](05.html)*中的 *ASCII 曼德勃罗渲染器*吗？在这个配方中，我们将使用线程来加快它的计算时间。*\n\n首先，我们将修改原始程序中限制每个选定坐标迭代次数的线。这将使程序*比我们在终端上实际显示的更慢*并且其结果*比我们在终端上实际显示的更精确*，但是我们有一个很好的并行化示例目标。\n\n然后，我们将对程序进行小的修改，看看整个程序如何运行得更快。修改后，程序运行`std::async`和`std::future`。为了完全理解这个食谱，理解原始程序是至关重要的。\n\n# 怎么做...\n\n在本节中，我们采用了我们在[第 23 章](05.html)、*高级使用 STL 算法*中实现的 ASCII 曼德勃罗分形渲染器。首先，我们将通过增加计算限制来使计算花费更多的时间。然后，为了并行化程序，我们只对程序做了四个小改动，从而获得了一些加速:\n\n1.  为了遵循这些步骤，最好只是从另一个食谱中复制整个程序。然后按照以下步骤中的说明进行所有需要的调整。与原程序的所有差异都在*粗体*中突出显示。\n    第一个变化是增加了一个表头，`<future>`:\n\n```cpp\n      #include <iostream>\n      #include <algorithm>\n      #include <iterator>\n      #include <complex>\n      #include <numeric>\n      #include <vector>\n #include <future>      \n\n      using namespace std;\n```\n\n2.  `scaler`和`scaled_cmplx`功能不需要任何改变:\n\n```cpp\n      using cmplx = complex<double>;\n\n      static auto scaler(int min_from, int max_from,\n                         double min_to, double max_to)\n      {\n          const int w_from {max_from - min_from};\n          const double w_to {max_to - min_to};\n          const int mid_from {(max_from - min_from) / 2 + min_from};\n          const double mid_to {(max_to - min_to) / 2.0 + min_to};\n\n          return [=] (int from) {\n              return double(from - mid_from) / w_from * w_to + mid_to;\n          };\n      }\n\n      template <typename A, typename B>\n      static auto scaled_cmplx(A scaler_x, B scaler_y)\n      {\n          return [=](int x, int y) {\n              return cmplx{scaler_x(x), scaler_y(y)};\n          };\n      }\n```\n\n3.  在函数`mandelbrot_iterations`中，我们将增加迭代次数，以使程序计算量更大:\n\n```cpp\n      static auto mandelbrot_iterations(cmplx c)\n      {\n          cmplx z {};\n          size_t iterations {0};\n          const size_t max_iterations {100000};\n          while (abs(z) < 2 && iterations < max_iterations) {\n              ++ iterations;\n              z = pow(z, 2) + c;\n          }\n          return iterations;\n      }\n```\n\n4.  那么我们就有了主功能的一部分，不再需要任何改变:\n\n```cpp\n      int main()\n      {\n          const size_t w {100};\n          const size_t h {40};\n\n          auto scale (scaled_cmplx(\n              scaler(0, w, -2.0, 1.0),\n              scaler(0, h, -1.0, 1.0)\n          ));\n\n          auto i_to_xy ([=](int x) { \n              return scale(x % w, x / w); \n          });\n```\n\n5.  在`to_iteration_count`函数中，我们不再直接调用`mandelbrot_iterations(x_to_xy(x))`，而是使用`std::async`进行异步调用:\n\n```cpp\n          auto to_iteration_count ([=](int x) {\n              return async(launch::async,\n mandelbrot_iterations, i_to_xy(x));\n          });\n```\n\n6.  在最后一次改变之前，函数`to_iteration_count`返回给我们一个特定坐标收敛曼德勃罗算法所需的迭代次数。现在它返回一个`future`变量，该变量稍后将包含相同的值，因为它是异步计算的。正因为如此，我们需要一个保存所有未来值的向量，所以我们只需添加一个。我们提供的输出迭代器`transform`作为第三个参数，必须是新输出向量`r`的开始迭代器:\n\n```cpp\n          vector<int> v (w * h);\n vector<future<size_t>> r (w * h);\n          iota(begin(v), end(v), 0);\n          transform(begin(v), end(v), begin(r), \n                    to_iteration_count);\n```\n\n7.  为我们完成所有打印的`accumulate`调用不再将`size_t`值作为其第二个参数，而是`future<size_t>`值。我们需要将它调整为这种类型(如果我们从一开始就使用`auto&`作为它的类型，那么这甚至没有必要)，然后我们需要调用`x.get()`，我们之前刚刚访问了`x`，以便等待值到达:\n\n```cpp\n          auto binfunc ([w, n{0}] (auto output_it, future<size_t> &x) \n                  mutable {\n              *++ output_it = (x.get() > 50 ? '*' : ' ');\n              if (++ n % w == 0) { ++ output_it = 'n'; }\n              return output_it;\n          });\n\n          accumulate(begin(r), end(r), \n                     ostream_iterator<char>{cout}, binfunc);\n      }\n```\n\n8.  编译和运行给了我们和以前一样的输出。唯一有趣的区别是执行速度。如果我们也增加程序原始版本的迭代次数，那么并行版本的计算速度应该会更快。在我的计算机上，有四个带超线程的中央处理器内核(这导致了 8 个虚拟内核)，我用 GCC 和 clang 得到了不同的结果。加速最好的是`5.3`，最差的是`3.8`。当然，结果也会因机器而异。\n\n# 它是如何工作的...\n\n首先理解整个程序是至关重要的，因为很明显，所有的 CPU 密集型工作都发生在主函数的一行代码中:\n\n```cpp\ntransform(begin(v), end(v), begin(r), to_iteration_count);\n```\n\n向量`v`包含所有映射到复坐标的索引，然后用曼德勃罗算法依次迭代。每次迭代的结果保存在向量`r`中。\n\n在原程序中，这是消耗计算分形图像所有处理时间的单行。所有在它之前的代码只是设置工作，所有在它之后的代码只是为了打印。这意味着并行化这一行是提高性能的关键。\n\n并行化的一种可能方法是将从`begin(v)`到`end(v)`的整个线性范围分成相同大小的块，并在所有内核中均匀分布。这样，所有内核将分担工作量。如果我们使用具有并行执行策略的并行版本的`std::transform`，情况将完全如此。不幸的是，这不是解决*问题的正确策略，因为曼德勃罗集合中的每一个点都显示了非常独立的迭代次数。*\n\n *我们这里的方法是稍后将代表终端上单独打印的字符单元的每一个单独的向量项变成异步计算的`future`值。由于源向量和目标向量都是很大的`w * h`项，这意味着`100 * 40`在我们的例子中，我们有一个 4000 个未来值的向量，它们是异步计算的。如果我们的系统有 4000 个中央处理器内核，那么这将意味着我们启动 4000 个线程，这些线程实际上同时执行曼德勃罗迭代。在具有更少内核的正常系统中，每个内核中的中央处理器只会处理一个接一个的异步项目。\n\n虽然`to_iteration_count`异步版本的`transform`调用本身并不进行*计算*，而是设置线程和未来对象，但它实际上立即返回。由于迭代耗时太长，程序的原始版本在这一点上被阻止了。\n\n该程序的并行版本当然也会在某个地方阻塞*。在终端上打印我们所有值的函数必须从期货内部访问结果。为了做到这一点，它在所有值上调用`x.get()`。这就是诀窍:当它等待打印第一个值时，会同时计算许多其他值。所以如果第一个未来的`get()`召唤回来，下一个未来可能也已经准备好印刷了！*\n\n *万一`w * h`导致更多的数字，在创建和同步所有这些期货时将会有一些可测量的开销。在这种情况下，开销不会太大。在我搭载英特尔 i7 处理器的笔记本电脑上，该处理器具有 4 个超线程(T2)内核(可产生 8 个虚拟内核)，该程序的并行版本运行速度比原始程序快 3-5 倍以上。理想的并行化将使其速度提高 8 倍。当然，这种加速在不同的计算机之间会有所不同，因为这取决于很多因素。\n\n# 用 std::future 实现小型自动并行化库\n\n大多数复杂的任务可以分解成子任务。从所有子任务中，我们可以画出一个**有向无环图** ( **DAG** )来描述哪个子任务依赖于哪些其他子任务来完成更高级别的任务。例如，让我们假设我们想要产生字符串`\"foo bar foo bar this that \"`，我们只能通过创建单个单词并将它们与其他单词或它们自己连接起来来实现这一点。假设这个功能由三个基本功能`create`、`concat`和`twice`提供。\n\n考虑到这一点，我们可以绘制下面的 DAG，可视化它们之间的依赖关系，以便获得最终结果:\n\n![](img/0648ebd7-03e5-4c9c-8ee2-8ed58cb25773.png)\n\n在代码中实现这一点时，很明显，一切都可以在一个 CPU 内核上以串行方式实现。或者，所有不依赖于其他子任务或已经完成的其他子任务的子任务都可以在多个中央处理器内核上同时执行*。*\n\n *编写这样的代码可能看起来很乏味，即使使用`std::async`也是如此，因为子任务之间的依赖关系需要建模。在这个食谱中，我们将实现两个小的库助手函数，帮助将正常函数`create`、`concat`和`twice`转换为异步工作的函数。有了这些，我们将找到一个真正优雅的方法来建立依赖图。在执行过程中，为了尽可能快地计算结果，图形会以一种看似智能的*方式并行化。*\n\n# 怎么做...\n\n在本节中，我们将实现一些函数来模拟相互依赖的计算密集型任务，并让它们尽可能并行运行:\n\n1.  让我们首先包括所有必要的标题:\n\n```cpp\n      #include <iostream>\n      #include <iomanip>\n      #include <thread>\n      #include <string>\n      #include <sstream>\n      #include <future>      \n\n      using namespace std;\n      using namespace chrono_literals;\n```\n\n2.  我们需要同步对`cout`的并发访问，所以让我们使用本章中其他配方的同步助手:\n\n```cpp\n      struct pcout : public stringstream {\n          static inline mutex cout_mutex;\n\n          ~pcout() {\n              lock_guard<mutex> l {cout_mutex};\n              cout << rdbuf();\n              cout.flush();\n          }\n      };\n```\n\n3.  现在让我们实现三个转换字符串的函数。第一个函数将从一个 C 字符串创建一个`std::string`对象。我们让它休眠 3 秒钟，以模拟字符串创建是计算密集型的:\n\n```cpp\n      static string create(const char *s)\n      {\n          pcout{} << \"3s CREATE \" << quoted(s) << 'n';\n          this_thread::sleep_for(3s);\n          return {s};\n      }\n```\n\n4.  下一个函数接受两个字符串对象作为参数，并返回它们的串联。我们给它 5 秒钟的等待时间来模拟这是一项耗时的任务:\n\n```cpp\n      static string concat(const string &a, const string &b)\n      {\n          pcout{} << \"5s CONCAT \"\n                  << quoted(a) << \" \"\n                  << quoted(b) << 'n';\n          this_thread::sleep_for(5s);\n          return a + b;\n      }\n```\n\n5.  最后一个计算量大的函数接受一个字符串，并将其与自身连接起来。执行此操作需要 3 秒钟:\n\n```cpp\n      static string twice(const string &s)\n      {\n          pcout{} << \"3s TWICE \" << quoted(s) << 'n';\n          this_thread::sleep_for(3s);\n          return s + s;\n      }\n```\n\n6.  我们现在已经可以在一个串行程序中使用这些函数，但是我们想获得一些优雅的自动并行化。让我们为此实现一些助手。*请注意*，下面三个功能看起来真的很复杂。`asynchronize`接受一个函数`f`并返回一个捕获它的可调用对象。我们可以用任意数量的参数调用这个可调用对象，然后它会在返回给我们的另一个可调用对象中与`f`一起捕获这些参数。最后一个可调用对象可以在没有参数的情况下调用。然后，它会异步调用`f`及其捕获的所有参数:\n\n```cpp\n      template <typename F>\n      static auto asynchronize(F f)\n      {\n          return [f](auto ... xs) {\n              return [=] () {\n                  return async(launch::async, f, xs...);\n              };\n          };\n      }\n```\n\n7.  下一个函数将被我们在下一步中声明的函数使用。它接受一个函数`f`，并将其捕获在返回的可调用对象中。该对象可以用许多未来的对象来调用。然后，它将调用所有期货的`.get()`，对它们应用`f`，并返回结果:\n\n```cpp\n      template <typename F>\n      static auto fut_unwrap(F f)\n      {\n          return [f](auto ... xs) {\n              return f(xs.get()...);\n          };\n      }\n```\n\n8.  最后一个辅助函数也接受一个函数`f`。它返回一个捕获`f`的可调用对象。该可调用对象可以用任意数量的可调用对象作为参数来调用，它返回与另一个可调用对象中的`f`一起捕获的参数。然后可以在没有参数的情况下调用最终的可调用对象。然后它会调用在`xs...`包中捕获的所有可调用对象。这些需要用`fut_unwrap`拆开包装的退货期货。使用`std::async`对来自期货的真实值的真实函数`f`的未来展开和实际应用再次异步发生:\n\n```cpp\n      template <typename F>\n      static auto async_adapter(F f)\n      {\n          return [f](auto ... xs) {\n              return [=] () {\n                  return async(launch::async, \n                               fut_unwrap(f), xs()...);\n              };\n          };\n      }\n```\n\n9.  好吧，这可能有点疯狂，有点像电影*《盗梦空间》*，因为 lambda 表达式返回 lambda 表达式。稍后我们将详细讨论这个巫毒法典。现在让我们取函数`create`、`concat`和`twice`并使它们异步。函数`async_adapter`使一个完全正常的函数等待未来的参数并返回未来的结果。这是一种从同步到异步世界的翻译包装。我们将其应用于`concat`和`twice`。我们必须在`create`上使用`asynchronize`，因为它将返回一个未来，但我们将用真实的价值而不是未来来喂养它。任务依赖链必须以`create`调用开始:\n\n```cpp\n      int main()\n      {\n          auto pcreate (asynchronize(create));\n          auto pconcat (async_adapter(concat));\n          auto ptwice  (async_adapter(twice));\n```\n\n10.  现在，我们已经自动并行化了与原始同步函数同名的函数，但带有`p`前缀。现在让我们建立一个复杂的示例依赖树。首先，我们创建字符串`\"foo \"`和`\"bar \"`，我们立即将其连接到`\"foo bar \"`。然后使用`twice`将该字符串与其自身连接起来。然后我们创建字符串`\"this \"`和`\"that \"`，并将其连接到`\"this that \"`。最后，我们将结果连接到`\"foo bar foo bar this that \"`。结果应保存在变量`callable`中。然后最后调用`callable().get()`开始计算并等待其返回值，以便也打印出来。在我们调用`callable()`之前没有计算，在这个调用之后，所有的魔法都开始了:\n\n```cpp\n          auto result (\n              pconcat(\n                  ptwice(\n                      pconcat(\n                          pcreate(\"foo \"),\n                          pcreate(\"bar \"))),\n                  pconcat(\n                      pcreate(\"this \"),\n                      pcreate(\"that \"))));\n\n          cout << \"Setup done. Nothing executed yet.n\";\n\n          cout << result().get() << 'n';\n      }\n```\n\n11.  编译运行程序显示所有`create`调用同时执行，然后执行其他调用。看起来他们被聪明地安排好了。整个程序运行 16 秒。如果这些步骤没有并行执行，需要 30 秒才能完成。请注意，我们需要一个至少有四个 CPU 内核的系统，以便能够同时执行所有`create`调用。如果系统的 CPU 内核较少，那么一些调用将不得不共享 CPU，这当然会消耗更多的时间:\n\n```cpp\n      $ ./chains \n      Setup done. Nothing executed yet.\n      3s CREATE \"foo \"\n      3s CREATE \"bar \"\n      3s CREATE \"this \"\n      3s CREATE \"that \"\n      5s CONCAT \"this \" \"that \"\n      5s CONCAT \"foo \" \"bar \"\n      3s TWICE  \"foo bar \"\n      5s CONCAT \"foo bar foo bar \" \"this that \"\n      foo bar foo bar this that\n```\n\n# 它是如何工作的...\n\n没有任何`async`和`future`魔法的这个程序的普通系列版本看起来如下:\n\n```cpp\nint main()\n{\n    string result {\n        concat(\n            twice(\n                concat(\n                    create(\"foo \"),\n                    create(\"bar \"))),\n            concat(\n                create(\"this \"),\n                create(\"that \"))) };\n\n    cout << result << 'n';\n}\n```\n\n在这个食谱中，我们编写了帮助函数`async_adapter`和`asynchronize`，它们帮助我们从`create`、`concat`和`twice`创建新的函数。我们称这些新的异步函数为`pcreate`、`pconcat`和`ptwice`。\n\n让我们先忽略`async_adapter`和`asynchronize`实现的复杂性，以便先看看这给我们带来了什么。\n\n串行版本看起来类似于以下代码:\n\n```cpp\nstring result {concat( ... )};\ncout << result << 'n';\n```\n\n并行版本类似于以下内容:\n\n```cpp\nauto result (pconcat( ... ));\ncout << result().get() << 'n';\n```\n\n好了，现在我们进入复杂的部分。并行化结果的类型不是`string`，而是返回`future<string>`的可调用对象，我们可以在上面调用`get()`。起初这看起来确实很疯狂。\n\n那么，我们到底是如何以及为什么会有返回期货的可调用对象的呢？我们的`create`、`concat`、`twice`方法的问题是*慢*。(好吧，我们人为地让它们变慢，因为我们试图模拟消耗大量 CPU 时间的现实生活任务)。但是我们发现描述数据流的依赖树有可以并行执行的独立部分。让我们看一下两个示例时间表:\n\n![](img/8745f882-ba89-481e-bfcd-06203d12370f.png)\n\n在左侧，我们看到一个*单核*时间表。所有的函数调用都必须一个接一个地完成，因为我们只有一个中央处理器。也就是说，当`create`花费 3 秒、`concat`花费 5 秒、`twice`花费 3 秒时，需要 30 秒才能得到最终结果。\n\n在右侧，我们看到一个*并行调度*，在函数调用之间的依赖关系允许的情况下，尽可能多的并行完成。在一个有四个核心的理想世界中，我们可以同时创建所有子字符串，然后将它们连接起来，以此类推。获得最佳并行计划结果的最短时间是 16 秒。如果我们不能让函数调用自己更快，我们就不能走得更快。只需四个 CPU 内核，我们就可以达到这个执行时间。我们明显地达到了最佳计划。它是如何工作的？\n\n我们可以天真地编写以下代码:\n\n```cpp\nauto a (async(launch::async, create, \"foo \"));\nauto b (async(launch::async, create, \"bar \"));\nauto c (async(launch::async, create, \"this \"));\nauto d (async(launch::async, create, \"that \"));\nauto e (async(launch::async, concat, a.get(), b.get()));\nauto f (async(launch::async, concat, c.get(), d.get()));\nauto g (async(launch::async, twice, e.get()));\nauto h (async(launch::async, concat, g.get(), f.get()));\n```\n\n这对于`a`、`b`、`c`和`d`来说是一个好的开始，它们首先代表了四个子串。这些是在后台异步创建的。不幸的是，这段代码阻塞在我们初始化`e`的那一行。为了连接`a`和`b`，我们需要在两者上调用`get()`，这*阻塞*直到这些值*准备好*。显然，这不是一个好主意，因为并行化在第一次`get()`调用时停止并行化。我们需要一个更好的策略。\n\n好了，让我们展开我们编写的复杂助手函数。第一个是`asynchronize`:\n\n```cpp\ntemplate <typename F>\nstatic auto asynchronize(F f)\n{\n    return [f](auto ... xs) {\n        return [=] () {\n            return async(launch::async, f, xs...);\n        };\n    };\n}\n```\n\n当我们有一个函数`int f(int, int)`时，我们可以执行以下操作:\n\n```cpp\nauto f2 ( asynchronize(f) );\nauto f3 ( f2(1, 2) );\nauto f4 ( f3() );\nint result { f4.get() };\n```\n\n`f2`是我们异步版本的`f`。它可以用和`f`一样的论点来称呼，因为它*模仿* `f`。然后它返回一个可调用的对象，我们保存在`f3`中。`f3`现在捕获`f`和参数`1, 2`，但它还没有调用任何东西。这只是关于捕捉。\n\n当我们现在呼唤`f3()`的时候，那么我们终于得到了未来，因为`f3()`做着`async(launch::async, **f, 1, 2**);`的呼唤！从这个意义上来说，`f3`的语义是“*”把捕获的函数和参数一起扔进`std::async`。*”。\n\n不接受任何参数的内部 lambda 表达式为我们提供了间接性。有了它，我们可以设置并行调度的工作，但不需要调用任何阻塞的东西，*还*。在复杂得多的函数`async_adapter`中，我们遵循同样的原则:\n\n```cpp\ntemplate <typename F>\nstatic auto async_adapter(F f)\n{\n    return [f](auto ... xs) {\n        return [=] () {\n            return async(launch::async, fut_unwrap(f), xs()...);\n        };\n    };\n}\n```\n\n这个函数也确实首先返回一个模拟`f`的函数，因为它接受相同的参数。然后，该函数返回一个可调用对象，该对象同样不接受任何参数。然后这个可调用对象最终不同于其他辅助函数。\n\n`async(launch::async, fut_unwrap(f), xs()...);`线是什么意思？`xs()...`部分意味着，所有保存在包`xs`中的参数都被假定为可调用对象(就像我们一直在创建的对象！)，所以都是不带参数的调用。那些我们一直在生产的可调用对象本身会产生未来的价值，我们可以称之为`get()`。这就是`fut_unwrap`发挥作用的地方:\n\n```cpp\ntemplate <typename F>\nstatic auto fut_unwrap(F f)\n{\n    return [f](auto ... xs) {\n        return f(xs.get()...);\n    };\n}\n```\n\n`fut_unwrap`只是将一个函数`f`转换成一个接受一系列参数的函数对象。该函数对象然后调用所有的*上的`.get()`，然后最终将它们转发给`f`。*\n\n慢慢消化这一切。当我们在主函数中使用这个时，`auto result (pconcat(...));`调用链只是构造了一个包含所有函数和所有参数的大型可调用对象。此时尚未完成`async`呼叫。然后，当我们呼叫`result()`时，我们*释放了一个小雪崩`async`和`.get()`的呼叫，它们以正确的顺序出现，不会彼此阻塞。事实上，在并非所有`async`呼叫都已发出之前，不会发生`get()`呼叫。*\n\n最后，我们终于可以在`result()`返回的未来值上调用`.get()`，在那里我们有了最终的字符串。*************"
  },
  {
    "path": "docs/exp-cpp-prog/09.md",
    "content": "# 九、文件系统\n\n在本章中，我们将介绍以下食谱:\n\n*   实现路径规格化器\n*   从相对路径获取规范文件路径\n*   列出目录中的所有文件\n*   实现类似 grep 的文本搜索工具\n*   实现自动文件重命名器\n*   实现磁盘使用计数器\n*   计算文件类型的统计信息\n*   实现一个工具，通过用符号链接替换重复项来减小文件夹大小\n\n# 介绍\n\n如果我们没有一个库来帮助我们，那么使用文件系统路径总是乏味的，因为我们需要处理许多情况。\n\n有些路径是*绝对的*，有些是*相对的*，也许它们甚至不直接，因为它们还包含`.`(当前目录)和`..`(父目录)间接。然后，与此同时，不同的操作系统使用斜线`/`来分隔目录(Linux、MacOS 和不同的 UNIX 衍生产品)，或者使用反斜杠(Windows)。当然还有不同类型的文件。\n\n因为其他处理文件系统相关事务的程序都需要这样的功能，所以在 C++ 17 STL 中拥有新的文件系统库是很棒的。最棒的是，它对不同操作系统的工作方式相同，因此我们不必为支持不同操作系统的程序版本编写不同的代码。\n\n在本章中，我们将首先看到`path`类是如何工作的，因为它是这个库中其他任何东西的核心。然后，我们将看到使用`directory_iterator`和`recursive_directory_iterator`类是多么强大但又简单，同时我们用文件做有用的事情。最后，我们将使用一些小而简单的示例工具来完成一些与文件系统相关的实际任务。从这一点来看，构建更复杂的工具将变得容易。\n\n# 实现路径规格化器\n\n我们从一个非常简单的关于`std::filesystem::path`类的例子和一个帮助函数开始这一章，该函数智能地规范化文件系统路径。\n\n这个方法的结果是一个小应用，它采用任何文件系统路径，并以规范化的形式向我们返回相同的路径。规范化意味着我们得到的绝对路径不包含`.`或`..`路径间接。\n\n在实现它的同时，我们还将看到在使用文件系统库的这个基本部分时，我们需要注意哪些细节。\n\n# 怎么做...\n\n在本节中，我们将实现一个程序，它只接受一个文件系统路径作为命令行参数，然后以规范化的形式打印它。\n\n1.  包含优先，然后我们声明使用命名空间`std`和`filesystem`。\n\n```cpp\n      #include <iostream>\n      #include <filesystem>      \n\n      using namespace std;\n      using namespace filesystem;\n```\n\n2.  在主函数中，我们检查用户是否提供了命令行参数。如果不是这样，我们会打印出如何使用该程序。如果提供了一个路径，我们就从它实例化一个`filesystem::path`对象。\n\n```cpp\n      int main(int argc, char *argv[])\n      {\n          if (argc != 2) {\n              cout << \"Usage: \" << argv[0] << \" <path>n\";\n              return 1;\n          }\n\n          const path dir {argv[1]};\n```\n\n3.  因为我们可以从任何字符串实例化`path`对象，所以我们不能确定路径是否真的存在于计算机的文件系统中。为此，我们可以使用`filesystem::exists`功能。如果没有，我们就再次出错。\n\n```cpp\n          if (!exists(dir)) {\n              cout << \"Path \" << dir << \" does not exist.n\";\n              return 1;\n          }\n```\n\n4.  好吧，在这一点上，我们非常确定用户提供了一些*现有的*路径，知道我们可以要求它的规范化版本，然后我们打印出来。`filesystem::canonical`返回给我们另一个`path`对象。我们可以直接打印出来，但是`<<`运算符的`path`类型重载用引号将路径括起来。为了避免这种情况，我们可以通过其`.c_str()`或`.string()`方法打印路径。\n\n```cpp\n          cout << canonical(dir).c_str() << 'n';\n      }\n```\n\n5.  让我们编译程序并玩它。当我们在相对路径`\"src\"`上的我的主目录中执行时，它会打印完整的绝对路径。\n\n```cpp\n      $ ./normalizer src\n      /Users/tfc/src\n```\n\n6.  当我们再次在我的主目录中运行程序，但给它一个古怪的相对路径描述，首先进入我的`Desktop`文件夹，然后使用`..`再次步出，然后进入`Documents`文件夹，再次步出，为了最终进入`src`目录，程序打印出与之前相同的*路径！*\n\n```cpp\n      $ ./normalizer Desktop/../Documents/../src\n      /Users/tfc/src\n```\n\n# 它是如何工作的...\n\n作为`std::filesystem`的开胃菜，这个食谱仍然相当简单明了。我们从包含文件系统路径描述的字符串中初始化了一个`path`对象。`std::filesystem::path`类在我们使用文件系统库时起着非常重要的作用，因为大多数函数和类都与它相关。\n\n使用`filesystem::exists`函数，我们能够检查路径是否真的存在。到目前为止，我们还不能确定这一点，因为确实有可能创建与现有文件系统对象无关的`path`对象。`exists`只接受一个`path`实例，如果它真的存在，就返回`true`。这个函数已经能够自己确定我们给它的是绝对路径还是相对路径，这让它使用起来非常舒服。\n\n最后，我们在目录上使用`filesystem::canonical`，以便以规范化的形式打印出来。\n\n```cpp\npath canonical(const path& p, const path& base = current_path());\n```\n\n`canonical`接受一个路径，作为可选的第二个参数，它接受另一个路径。如果`p`是相对路径，则第二条路径`base`在路径`p`之前。之后，`canonical`尝试移除任何`.`和`..`路径间接。\n\n打印时，我们在规范化路径上使用了`.c_str()`方法。其原因是输出流的`operator<<`过载包围了带引号的路径，这可能不是我们一直想要的。\n\n# 还有更多...\n\n`canonical`如果我们要规范化的路径不存在，抛出`filesystem_error`类型异常。为了防止这种情况，我们用`exists`检查了文件系统路径。但是这种检查真的足以避免出现未处理的异常吗？号码\n\n`exists`和`canonical`都可以抛出`bad_alloc`例外。如果这些打击了我们，人们可能会说这个项目无论如何都是注定要失败的。如果在我们检查文件是否存在和将其规范化之间，有人重命名或删除了底层文件，那么将会出现一个更为严重，也更有可能发生的问题！在这种情况下，`canonical`会抛出一个`filesystem_error`，虽然我们之前检查过文件的存在。\n\n大多数文件系统函数都有一个额外的重载，它采用相同的参数，但也有一个`std::error_code`引用。\n\n```cpp\npath canonical(const path& p, const path& base = current_path());\npath canonical(const path& p, error_code& ec);\npath canonical(const std::filesystem::path& p,\n               const std::filesystem::path& base,\n               std::error_code& ec );\n\n```\n\n这样，我们可以选择是用`try` - `catch`构造包围文件系统函数调用，还是手动检查错误。请注意，这只会改变与*文件系统相关的*错误的行为！不管有没有`ec`参数，如果系统内存不足，还是会抛出更基本的异常，例如`bad_alloc`。\n\n# 从相对路径获取规范文件路径\n\n在上一个食谱中，我们已经规范化了路径。`filesystem::path`类当然能够做更多的事情，而不仅仅是保持和检查路径。它还帮助我们轻松地从字符串中组成路径，并再次分解它们。\n\n在这一点上，`path`确实已经将操作系统的细节从我们身边抽象了出来，但是也有某些情况我们仍然需要记住这些细节。\n\n我们将看到如何通过处理绝对和相对路径来处理路径及其合成/分解。\n\n# 怎么做...\n\n在本节中，我们将使用绝对路径和相对路径来查看`path`类及其辅助函数的优势。\n\n1.  首先，我们包括所有必要的头，并声明我们使用命名空间`std`和`sfilesystem`。\n\n```cpp\n      #include <iostream>\n      #include <filesystem>     \n\n      using namespace std;\n      using namespace filesystem;\n```\n\n2.  然后，我们声明一个示例路径。此时，它所引用的文本文件是否真实存在并不重要。但是，如果基础文件不存在，有些函数会抛出异常。\n\n```cpp\n      int main()\n      {\n          path p {\"testdir/foobar.txt\"};\n```\n\n3.  我们现在来看看四个不同的文件系统库函数。`current_path`返回程序当前执行的路径，*工作目录*。`absolute`接受像我们的路径`p`这样的相对路径，并返回整个文件系统中的绝对非组合路径。`system_complete`在 Linux、MacOS 或类似 UNIX 的操作系统上的表现与`absolute`几乎相同。在 Windows 上，我们将获得由磁盘卷号附加前置的绝对路径(例如，`\"C:\"`)。`canonical`再次执行与`absolute`相同的操作，但随后会额外删除任何`\".\"`(该目录的缩写为*)或`\"..\"`(该目录的缩写为*“向上一个目录”*)间接引用。我们将在以下步骤中使用这种间接方法:*\n\n```cpp\n          cout << \"current_path      : \" << current_path()\n               << \"nabsolute_path   : \" << absolute(p)\n               << \"nsystem_complete : \" << system_complete(p)\n               << \"ncanonical(p)    : \" << canonical(p)\n               << 'n';\n```\n\n4.  `path`类的另一个好处是它重载了`/`运算符。这样，我们可以使用`/`连接文件夹名称和文件名，并由此组成路径。让我们尝试一下，打印一个合成的路径。\n\n```cpp\n          cout << path{\"testdir\"} / \"foobar.txt\" << 'n';\n```\n\n5.  让我们一起玩`canonical`和组合路径。通过给`canonical`一个相对路径如`\"foobar.txt\"`和一个合成绝对路径`current_path() / \"testdir\"`，它应该会返回给我们现有的绝对路径。在另一个召唤中，我们给它我们的路径`p`(也就是`\"testdir/foobar.txt\"`)并给它一个绝对的路径`current_path()`，它指引我们进入`\"testdir\"`并再次上升。这个应该和`current_path()`一样，因为间接。在这两个调用中，`canonical`应该返回给我们相同的绝对路径。\n\n```cpp\n          cout << \"canonical testdir     : \"\n               << canonical(\"foobar.txt\", \n                            current_path() / \"testdir\")\n               << \"ncanonical testdir 2 : \"\n               << canonical(p, current_path() / \"testdir/..\") \n               << 'n';\n```\n\n6.  我们还可以测试两条非规范路径的等价性。`equivalence`规范化路径，它接受这些路径作为参数，如果它们描述了相同的路径，则返回`true`。对于这个测试，路径必须真的*存在，*否则抛出异常。\n\n```cpp\n          cout << \"equivalence: \"\n               << equivalent(\"testdir/foobar.txt\",\n                            \"testdir/../testdir/foobar.txt\") \n               << 'n';\n      }\n```\n\n7.  编译并运行程序会产生以下输出。`current_path()`返回我笔记本电脑上的主文件夹，因为我从那里执行了应用。我们的相对路径`p`已经被`absolute_path`、`system_complete`和`canonical`添加到这个目录中。我们看到`absolute_path`和`system_complete`在我的系统上产生完全相同的路径，因为它是一个 Mac(在 Linux 上也是一样的)。在 Windows 机器上，`system_complete`会在前面加上`\"C:\"`，或者工作目录所在的任何驱动器。\n\n```cpp\n      $ ./canonical_filepath\n      current_path    : \"/Users/tfc\"\n      absolute_path   : \"/Users/tfc/testdir/foobar.txt\"\n      system_complete : \"/Users/tfc/testdir/foobar.txt\"\n      canonical(p)    : \"/Users/tfc/testdir/foobar.txt\"\n      \"testdir/foobar.txt\"\n      canonical testdir   : \"/Users/tfc/testdir/foobar.txt\"\n      canonical testdir 2 : \"/Users/tfc/testdir/foobar.txt\"\n      equivalence: 1\n```\n\n8.  在我们的短程序中，我们不处理任何异常。如果我们删除`testdir`目录中的`foobar.txt`文件，那么程序会因异常而中止执行。`canonical`功能要求路径存在。还有一个`weakly_canonical`功能没有这个要求。\n\n```cpp\n      $ ./canonial_filepath \n      current_path    : \"/Users/tfc\"\n      absolute_path   : \"/Users/tfc/testdir/foobar.txt\"\n      system_complete : \"/Users/tfc/testdir/foobar.txt\"\n terminate called after throwing an instance of \n      'std::filesystem::v1::__cxx11::filesystem_error'\n        what():  filesystem error: cannot canonicalize: \n        No such file or directory [testdir/foobar.txt] [/Users/tfc]\n```\n\n# 它是如何工作的...\n\n这个方法的目标是看看动态合成新路径有多容易。这主要是因为`path`类有一个方便的`/`运算符重载。除此之外，文件系统函数与相对和绝对路径以及包含`.`和`..`间接寻址的路径相处得很好。\n\n有相当多的函数返回`path`实例的一部分，有或没有转换。我们不打算在这里列出所有的函数，因为浏览一下 C++ 引用是最好的方法。\n\n然而，`path`类的成员函数可能值得仔细看看。让我们看看`path`的哪个成员函数返回了路径的哪个部分。下图还显示了 Windows 路径与 UNIX/Linux 路径的细微差别。\n\n![](img/9c9ab3d9-e0c4-41d0-b90c-2de3c0075dd2.png)\n\n可以看到图中显示了`path`的成员函数返回到一个*绝对*路径。对于*相对*路径，`root_path`、`root_name`和`root_directory`为空。`relative_path`然后只要返回路径，如果它已经是相对的。\n\n# 列出目录中的所有文件\n\n当然，每一个提供文件系统支持的操作系统都带有某种工具，它只列出文件系统目录中的所有文件*。最简单的例子是 Linux、MacOS 和其他 UNIX 相关操作系统上的`ls`命令。在 DOS 和 Windows 下，都有`dir`命令。两者都列出目录中的所有文件，并提供补充信息，如文件大小、权限等。*\n\n *然而，重新实现这样一个工具也是一个很好的标准任务，可以继续进行目录和文件遍历。所以，我们就这么做吧！\n\n我们自己的`ls` / `dir`工具将能够按名称列出目录中的所有项目，指示有哪种项目，列出它们的访问权限标志，并显示它们在文件系统上占用的字节数。\n\n# 怎么做...\n\n在这一节中，我们将实现一个小工具，列出任何用户提供的目录中的所有文件。它不仅会列出文件名，还会列出文件名的类型、大小和访问权限。\n\n1.  首先，我们需要包含一些头，并声明我们默认使用名称空间`std`和`filesystem`。\n\n```cpp\n      #include <iostream>\n      #include <sstream>\n      #include <iomanip>\n      #include <numeric>\n      #include <algorithm>\n      #include <vector>\n      #include <filesystem>      \n\n      using namespace std;\n      using namespace filesystem;\n```\n\n2.  我们需要的一个辅助功能是`file_info`。它接受一个`directory_entry`对象引用并从中提取路径，以及一个包含文件类型和权限信息的`file_status`对象(使用`status`函数)。最后，如果是常规文件，它还会提取条目的大小。对于目录或其他特殊文件，我们显然会返回一个大小为`0`的文件。所有这些信息被打包成一个元组。\n\n```cpp\nstatic tuple<path, file_status, size_t> \n      file_info(const directory_entry &entry)\n      {\n          const auto fs (status(entry));\n          return {entry.path(),\n                  fs,\n                  is_regular_file(fs) ? file_size(entry.path()) : 0u};\n      }\n```\n\n3.  我们需要的另一个辅助功能是`type_char`。路径不能只代表目录和简单的文本/二进制文件。操作系统提供了多种其他类型的抽象，例如以所谓的字符/块文件形式的硬件设备接口。STL 文件系统库为它们提供了许多谓词函数。这样我们可以返回字母`'d'`用于目录，字母`'f'`用于常规文件，等等。\n\n```cpp\n      static char type_char(file_status fs)\n      {\n          if      (is_directory(fs))      { return 'd'; }\n          else if (is_symlink(fs))        { return 'l'; }\n          else if (is_character_file(fs)) { return 'c'; }\n          else if (is_block_file(fs))     { return 'b'; }\n          else if (is_fifo(fs))           { return 'p'; }\n          else if (is_socket(fs))         { return 's'; }\n          else if (is_other(fs))          { return 'o'; }\n          else if (is_regular_file(fs))   { return 'f'; }\n\n          return '?';\n      }\n```\n\n4.  我们需要的另一个助手是`rwx`功能。它接受一个`perms`变量(它只是文件系统库中的一个`enum`类类型)，并返回一个描述文件权限设置的字符串，比如`\"rwxrwxrwx\"`。第一组`\"rwx\"`字符描述了文件所有者的 ***r** ead、 **w** rite 和 e**x**execution*权限。下一组描述了属于文件所属的*用户组*的所有用户的相同权限。最后一个字符组描述了其他人对文件的访问权限。`\"rwxrwxrwx\"`之类的字符串表示每个人都可以通过任何方式访问对象。`\"rw-r--r--\"`表示只有所有者可以读取和修改文件，而其他任何人只能读取。\n    我们只需要用这样的读/写/执行字符值组成一个字符串，一个许可一个许可。lambda 表达式帮助我们进行重复的工作，检查`perms`变量`p`是否包含特定的所有者位，然后返回`'-'`或正确的字符。\n\n```cpp\n      static string rwx(perms p)\n      {\n          auto check ([p](perms bit, char c) {\n              return (p & bit) == perms::none ? '-' : c; \n          });\n\n          return {check(perms::owner_read,   'r'),\n                  check(perms::owner_write,  'w'),\n                  check(perms::owner_exec,   'x'),\n                  check(perms::group_read,   'r'),\n                  check(perms::group_write,  'w'),\n                  check(perms::group_exec,   'x'),\n                  check(perms::others_read,  'r'),\n                  check(perms::others_write, 'w'),\n                  check(perms::others_exec,  'x')};\n      }\n```\n\n5.  最后，最后一个 helper 函数接受一个完整的文件大小，并将其转换为更易于阅读的形式。我们只是忽略了周期，而划分数字，并把他们地板到最近的千，兆，或千兆边界。\n\n```cpp\n      static string size_string(size_t size)\n      {\n          stringstream ss;\n          if        (size >= 1000000000) { \n              ss << (size / 1000000000) << 'G'; \n          } else if (size >= 1000000)    { \n              ss << (size / 1000000) << 'M';\n          } else if (size >= 1000)       { \n              ss << (size / 1000) << 'K'; \n          } else { ss << size << 'B'; }\n\n          return ss.str();\n      }\n```\n\n6.  现在我们终于可以实现主功能了。我们首先检查用户是否在命令行中提供了路径。如果他没有，我们就取当前目录“`.`”。然后，我们检查目录是否存在。如果没有，我们不可能列出任何文件。\n\n```cpp\n      int main(int argc, char *argv[])\n      {\n          path dir {argc > 1 ? argv[1] : \".\"};\n\n          if (!exists(dir)) {\n              cout << \"Path \" << dir << \" does not exist.n\";\n              return 1;\n          }\n```\n\n7.  现在，我们将使用文件信息元组填充一个`vector`，就像我们的第一个助手函数`file_info`从`directory_entry`对象返回一样。我们实例化一个`directory_iterator`并给它的构造函数`path`对象，这是我们在最后一步中创建的。在使用目录迭代器进行迭代时，我们将`directory_entry`对象转换为文件信息元组，并将它们插入到向量中。\n\n```cpp\n          vector<tuple<path, file_status, size_t>> items;\n\n          transform(directory_iterator{dir}, {},\n              back_inserter(items), file_info);\n```\n\n8.  现在我们已经将所有信息保存在矢量项中，并且可以使用我们编写的所有辅助函数简单地打印出来。\n\n```cpp\n          for (const auto &[path, status, size] : items) {\n              cout << type_char(status) \n                   << rwx(status.permissions()) << \" \"\n                   << setw(4) << right << size_string(size) \n                   << \" \" << path.filename().c_str() \n                   << 'n';\n          }\n      }\n```\n\n9.  使用 C++ 文档脱机版本中的文件路径编译和运行项目会产生以下输出。我们看到文件夹只包含目录和普通文件，因为只有`'d'`和`'f'`条目作为所有输出行的第一个字符。这些文件有不同的访问权限，当然大小也不同。请注意，文件是按照名称的字母顺序出现的，但是我们不能真正依赖于此，因为 C++ 17 标准不要求字母顺序。\n\n```cpp\n      $ ./list ~/Documents/cpp_reference/en/cpp\n      drwxrwxr-x    0B  algorithm\n      frw-r--r--   88K  algorithm.html\n      drwxrwxr-x    0B  atomic\n      frw-r--r--   35K  atomic.html\n      drwxrwxr-x    0B  chrono\n      frw-r--r--   34K  chrono.html\n      frw-r--r--   21K  comment.html\n      frw-r--r--   21K  comments.html\n      frw-r--r--  220K  compiler_support.html\n      drwxrwxr-x    0B  concept\n      frw-r--r--   67K  concept.html\n      drwxr-xr-x    0B  container\n      frw-r--r--  285K  container.html\n      drwxrwxr-x    0B  error\n      frw-r--r--   52K  error.html\n```\n\n# 它是如何工作的...\n\n在这个方法中，我们遍历文件，对于每个文件，我们检查它的状态和大小。虽然我们所有的每文件操作都相当简单明了，但是我们实际的目录遍历看起来有点神奇。\n\n为了遍历我们的目录，我们只是实例化了一个`directory_iterator`，然后迭代它。使用文件系统库遍历目录非常简单。\n\n```cpp\nfor (const directory_entry &e : directory_iterator{dir}) {\n    // do something\n}\n```\n\n关于这门课，除了以下几点，没什么好说的了:\n\n*   它访问目录的每个元素一次\n*   目录元素的迭代顺序未指定\n*   目录元素`.`和`..`已经被过滤掉了\n\n然而，可能值得注意的是`directory_iterator`似乎同时是一个*迭代器*和一个*可迭代范围*。为什么呢？在我们刚刚看到的最小`for`循环示例中，它被用作可迭代范围。在实际的配方代码中，我们像迭代器一样使用它:\n\n```cpp\ntransform(directory_iterator{dir}, {},\n          back_inserter(items), file_info);\n```\n\n事实是，它只是一个迭代器类类型，但是`std::begin`和`std::end`函数为这个类型提供了重载。这样我们可以在这种迭代器上调用`begin`和`end`函数，它们会再次返回给我们迭代器。乍一看这可能很奇怪，但它使这个类更有用。\n\n# 实现类似 grep 的文本搜索工具\n\n大多数操作系统都配备了某种本地搜索引擎。用户可以用一些键盘快捷键启动它，然后只需输入他们正在寻找的本地文件。\n\n在这些功能出现之前，命令行用户已经使用`grep`或`awk`等工具搜索过文件。用户只需输入“`grep -r foobar .`”，该工具将递归搜索当前目录，找到包含`\"foobar\"`字符串的任何文件。\n\n在这个食谱中，我们将实现这样一个应用。我们的小 grep 克隆将只从命令行接受一个模式，然后递归搜索我们在应用启动时所在的目录。然后，它将打印与我们的模式匹配的每个文件的名称。模式匹配将逐行应用，因此我们也可以打印文件匹配模式的确切行号。\n\n# 怎么做...\n\n我们将实现一个小工具，在文件中搜索用户提供的文本模式。该工具的工作原理类似于 UNIX 工具`grep`，但为了简单起见，不会那么成熟和强大。\n\n1.  首先，我们需要包含所有必要的头，并声明我们使用命名空间`std`和`filesystem`。\n\n```cpp\n      #include <iostream>\n      #include <fstream>\n      #include <regex>\n      #include <vector>\n      #include <string>\n      #include <filesystem>      \n\n      using namespace std;\n      using namespace filesystem;\n```\n\n2.  我们首先实现一个助手函数。它接受一个文件路径和一个描述我们正在寻找的模式的正则表达式对象。然后，我们实例化一个`vector`，它应该包含匹配的行号对及其内容。我们实例化一个输入文件流对象，从中我们将一行行地读取内容并对其进行模式匹配。\n\n```cpp\n      static vector<pair<size_t, string>> \n      matches(const path &p, const regex &re)\n      {\n          vector<pair<size_t, string>> d;\n          ifstream is {p.c_str()};\n```\n\n3.  我们使用`getline`功能逐行遍历文件。`regex_search`如果字符串包含我们的模式，则返回`true`。如果是这种情况，那么我们把行号和字符串放入向量中。最后，我们返回所有收集到的匹配。\n\n```cpp\n          string s;\n          for (size_t line {1}; getline(is, s); ++ line) {\n              if (regex_search(begin(s), end(s), re)) {\n                  d.emplace_back(line, move(s));\n              }\n          }\n\n          return d;\n      }\n```\n\n4.  在主函数中，我们首先检查用户是否提供了可以用作模式的命令行参数。如果没有，我们就会出错。\n\n```cpp\n      int main(int argc, char *argv[])\n      {\n          if (argc != 2) {\n              cout << \"Usage: \" << argv[0] << \" <pattern>n\";\n              return 1;\n          }\n```\n\n5.  接下来，我们从输入模式中构造一个正则表达式对象。如果模式不是有效的正则表达式，这将导致异常。如果出现这样的异常，我们会抓住它并出错。\n\n```cpp\n          regex pattern;\n\n          try { pattern = regex{argv[1]}; }\n          catch (const regex_error &e) {\n              cout << \"Invalid regular expression provided.n\";\n              return 1;\n          }\n```\n\n6.  现在，我们终于可以遍历文件系统并寻找模式匹配。我们使用`recursive_directory_iterator`来迭代工作目录中的所有文件。它的工作原理与上一个食谱中的`directory_iterator`完全一样，但它也下降到子目录中。这样我们就不用管理递归了。在每个条目上，我们都调用我们的助手函数`matches`。\n\n```cpp\n          for (const auto &entry :\n                recursive_directory_iterator{current_path()}) {\n              auto ms (matches(entry.path(), pattern));\n```\n\n7.  对于每个匹配项(如果有的话)，我们打印文件路径、行号和匹配行的完整内容。\n\n```cpp\n              for (const auto &[number, content] : ms) {\n                  cout << entry.path().c_str() << \":\" << number\n                       << \" - \" << content << 'n';\n              }\n          }\n      }\n```\n\n8.  让我们准备一个名为`\"foobar.txt\"`的文件，其中包含一些我们可以搜索的测试行。\n\n```cpp\n      foo\n      bar\n      baz\n```\n\n9.  编译和运行会产生以下输出。我在笔记本电脑的`/Users/tfc/testdir`文件夹中启动了这个应用，首先是模式`\"bar\"`。在该目录中，它找到了我们的`foobar.txt`文件的第二行和位于`testdir/dir1`的另一个文件`\"text1.txt\"`。\n\n```cpp\n      $ ./grepper bar\n      /Users/tfc/testdir/dir1/text1.txt:1 - foo bar bla blubb\n      /Users/tfc/testdir/foobar.txt:2 - bar\n\n```\n\n10.  再次启动应用，但这次是模式`\"baz\"`，它找到了我们的示例文本文件的第三行。\n\n```cpp\n      $ ./grepper baz\n      /Users/tfc/testdir/foobar.txt:3 - baz\n```\n\n# 它是如何工作的...\n\n设置和使用一个正则表达式来过滤文件的内容当然是这个食谱的主要任务。然而，让我们把注意力集中在`recursive_directory_iterator`上，因为过滤递归迭代的文件只是我们在这个食谱中使用这个特殊迭代器类的动机。\n\n就像`directory_iterator`，`recursive_directory_iterator`迭代一个目录的元素。正如它的名字所表明的，它的专长是递归地做这件事。每当它遇到一个属于*目录*的文件系统元素时，它会为这个路径产生一个`directory_entry`实例，但是为了迭代它的子节点，它也会向下进入这个路径。\n\n`recursive_directory_iterator`有一些有趣的成员函数:\n\n*   `depth()`:这告诉我们迭代器目前已经下降到子目录中多少层。\n*   `recursion_pending()`:这告诉我们迭代器是否会在当前指向的元素之后下降。\n*   `disable_recursion_pending()`:如果迭代器当前指向的是它将要下放到的目录，可以调用这个函数来防止迭代器下放到下一个子目录。这意味着如果我们过早的调用*，调用这个方法没有效果。*\n**   `pop()`:这将中止当前的递归级别，并在目录层次结构中上升一个级别以继续。*\n\n *# 还有更多...\n\n另一个需要了解的是`directory_options`枚举类。`recursive_directory_iterator`的构造函数确实接受这种类型的值作为第二个参数。我们一直在隐式使用的默认值是`directory_options::none`。其他值有:\n\n*   `follow_directory_symlink`:这允许递归迭代器跟随到目录的符号链接\n*   `skip_permission_denied`:这告诉迭代器跳过否则会导致错误的目录，因为文件系统拒绝访问权限\n\n这些选项可以与`|`运算符组合使用。\n\n# 实现自动文件重命名器\n\n这个食谱的动机是我经常遇到的一种情况。当从节假日收集图片文件时，例如，从不同的朋友以及同一文件夹中的不同照片设备收集图片文件时，文件结尾通常会有所不同。有些 JPEG 文件有`.jpg`扩展名，有些有`.jpeg`，有些甚至有`.JPEG`。\n\n有些人可能更喜欢将所有扩展都同质化。用一个命令重命名所有文件会很有用。与此同时，我们可以删除空格`' '`并用下划线`'_'`来代替，例如。\n\n在这个食谱中，我们将实现这样一个工具，并将其称为`renamer`。它将接受一系列输入模式及其替代品，如下所示:\n\n```cpp\n$ renamer jpeg jpg JPEG jpg\n```\n\n在这种情况下，重命名器将递归地遍历当前目录，并在所有文件名中搜索模式`jpeg`和`JPEG`。它将用`jpg`代替这两者。\n\n# 怎么做...\n\n我们将实现一个工具，递归扫描目录中的所有文件，并将它们的文件名与模式匹配。所有匹配项都将替换为用户提供的令牌，受影响的文件也将相应地重命名。\n\n1.  首先，我们需要包含一些头，并声明我们使用名称空间`std`和`filesystem`。\n\n```cpp\n      #include <iostream>\n      #include <regex>\n      #include <vector>\n      #include <filesystem>      \n\n      using namespace std;\n      using namespace filesystem;\n```\n\n2.  我们实现了一个简短的助手函数，它接受字符串形式的输入文件路径和一系列替换对。每个替换对由一个模式及其替换组成。在循环替换范围时，我们使用`regex_replace`向它输入输入字符串，并让它返回转换后的字符串。之后，我们返回结果字符串。\n\n```cpp\n      template <typename T>\n      static string replace(string s, const T &replacements)\n      {\n          for (const auto &[pattern, repl] : replacements) {\n              s = regex_replace(s, pattern, repl);\n          }\n\n          return s;\n      }\n```\n\n3.  在主函数中，我们首先验证命令行。我们接受*对*中的命令行参数，因为我们想要模式和它们的替换。`argv`的第一个元素始终是可执行名称。这意味着如果用户提供至少一对或更多，那么`argc`必须是*奇数*并且不小于`3`。\n\n```cpp\n      int main(int argc, char *argv[])\n      {\n          if (argc < 3 || argc % 2 != 1) {\n              cout << \"Usage: \" << argv[0] \n                   << \" <pattern> <replacement> ...n\";\n              return 1;\n          }\n```\n\n4.  一旦我们检查到有成对的输入，我们将用这些来填充一个向量。\n\n```cpp\n          vector<pair<regex, string>> patterns;\n\n          for (int i {1}; i < argc; i += 2) {\n              patterns.emplace_back(argv[i], argv[i + 1]);\n          }\n```\n\n5.  现在我们可以遍历文件系统了。为了简单起见，我们将应用的当前路径定义为要迭代的目录。\n    对于每个目录条目，我们提取其到`opath`变量的原始路径。然后，我们只取文件名而不取路径的其余部分，并根据我们之前收集的模式和替换列表对其进行转换。我们复制一份`opath`，称之为`rpath`，并用新的文件名替换它的文件名部分。\n\n```cpp\n          for (const auto &entry :\n                recursive_directory_iterator{current_path()}) {\n              path opath {entry.path()};\n              string rname {replace(opath.filename().string(),\n                                    patterns)};\n\n              path rpath {opath};\n              rpath.replace_filename(rname);\n```\n\n6.  对于所有受我们的模式影响的文件，我们打印并重命名它们。如果替换模式产生的文件名已经存在，我们不能继续。让我们跳过这些文件。当然，我们也可以在路径上附加一些数字或其他东西来解决名称冲突。\n\n```cpp\n              if (opath != rpath) {\n                  cout << opath.c_str() << \" --> \" \n                       << rpath.filename().c_str() << 'n';\n                  if (exists(rpath)) {\n                      cout << \"Error: Can't rename.\"\n                              \" Destination file exists.n\";\n                  } else {\n                      rename(opath, rpath);\n                  }\n              }\n          }\n      }\n```\n\n7.  在示例目录中编译和运行程序会产生以下输出。我在目录中放了一些 JPEG 图片，但是给了它们不同的名字结尾`jpg`、`jpeg`和`JPEG`。然后，我用模式`jpeg`和`JPEG`执行程序，并选择`jpg`作为两者的替代。结果是一个具有同质文件扩展名的文件夹。\n\n```cpp\n      $ ls\n      birthday_party.jpeg   holiday_in_dubai.jpg  holiday_in_spain.jpg \n      trip_to_new_york.JPEG\n      $ ../renamer jpeg jpg JPEG jpg\n      /Users/tfc/pictures/birthday_party.jpeg --> birthday_party.jpg\n      /Users/tfc/pictures/trip_to_new_york.JPEG --> trip_to_new_york.jpg\n      $ ls\n      birthday_party.jpg   holiday_in_dubai.jpg holiday_in_spain.jpg\n      trip_to_new_york.jpg\n```\n\n# 实现磁盘使用计数器\n\n我们已经实现了一个类似于 Linux/MacOS 上的`ls`或者 Windows 上的`dir`的工具，但是就像这些工具一样，它不打印*目录*的文件大小。\n\n为了得到相当于一个目录的大小，我们必须深入到它里面，并总结它包含的所有文件的大小。\n\n在这个食谱中，我们将实现一个工具，它可以做到这一点。该工具可以在任何文件夹上运行，并将汇总所有目录条目的累积大小。\n\n# 怎么做...\n\n在本节中，我们将实现一个应用，它遍历一个目录并列出每个条目的文件大小。这对于常规文件来说很简单，但是如果我们看到的目录条目本身就是一个目录，那么我们必须查看它并总结它所包含的所有文件的大小。\n\n1.  首先，我们需要包含所有必要的头，并声明我们使用命名空间`std`和`filesystem`。\n\n```cpp\n      #include <iostream>\n      #include <sstream>\n      #include <iomanip>\n      #include <numeric>\n      #include <filesystem>      \n\n      using namespace std;\n      using namespace filesystem;\n```\n\n2.  然后我们实现一个助手函数，它接受一个`directory_entry`作为参数，并返回它在文件系统中的大小。如果不是目录，我们简单返回`file_size`计算的文件大小。\n\n```cpp\n      static size_t entry_size(const directory_entry &entry)\n      {\n          if (!is_directory(entry)) { return file_size(entry); }\n```\n\n3.  如果它是一个目录，我们需要遍历它的所有条目并计算它们的大小。如果我们再次遇到子目录，我们最终会递归调用自己的`entry_size`辅助函数。\n\n```cpp\n          return accumulate(directory_iterator{entry}, {}, 0u,\n              [](size_t accum, const directory_entry &e) {\n                  return accum + entry_size(e);\n              });\n      }\n```\n\n4.  为了更好的可读性，我们使用与本章其他食谱相同的`size_string`功能。它只是把大文件分成更短更好的文件来读取带有 kilo、mega 或 giga 后缀的字符串。\n\n```cpp\n      static string size_string(size_t size)\n      {\n          stringstream ss;\n          if        (size >= 1000000000) { \n              ss << (size / 1000000000) << 'G'; \n          } else if (size >= 1000000)    { \n              ss << (size / 1000000) << 'M'; \n          } else if (size >= 1000)       { \n              ss << (size / 1000) << 'K'; \n          } else { ss << size << 'B'; }\n\n          return ss.str();\n      }  \n```\n\n5.  我们在主函数中需要做的第一件事是检查用户是否在命令行上提供了文件系统路径。如果不是这样，我们只取当前文件夹。在继续之前，我们检查它是否存在。\n\n```cpp\n      int main(int argc, char *argv[])\n      {\n          path dir {argc > 1 ? argv[1] : \".\"};\n\n          if (!exists(dir)) {\n              cout << \"Path \" << dir << \" does not exist.n\";\n              return 1;\n          }\n```\n\n6.  现在，我们可以遍历所有目录条目，并打印它们的大小和名称。\n\n```cpp\n          for (const auto &entry : directory_iterator{dir}) {\n              cout << setw(5) << right \n                   << size_string(entry_size(entry))\n                   << \" \" << entry.path().filename().c_str() \n                   << 'n';\n          }\n      }\n```\n\n7.  编译和运行程序会产生以下结果。我在 C++ 离线参考中的一个文件夹中启动了它。因为它也包含子文件夹，所以我们的递归文件大小摘要帮助器会立即有所帮助。\n\n```cpp\n      $ ./file_size ~/Documents/cpp_reference/en/\n        19M c\n        12K c.html\n       147M cpp\n        17K cpp.html\n        22K index.html\n        22K Main_Page.html\n```\n\n# 它是如何工作的...\n\n整个程序围绕在常规文件上使用`file_size`展开。如果程序看到一个目录，它会递归地向下进入该目录，并对其所有条目调用`file_size`。\n\n我们所做的唯一区分是直接调用`file_size`还是需要递归策略的事情是询问`is_directory`谓词。这对于只包含常规文件和目录的目录非常有效。\n\n尽管我们的示例程序很简单，但由于未处理的异常，它会在以下情况下崩溃:\n\n*   `file_size`只对常规文件和符号链接有效。它在任何其他情况下都会引发异常。\n*   虽然`file_size`在符号链接上起作用，但是如果我们在*断开的*符号链接上调用它，它*仍然会抛出一个异常。*\n\n为了使这个示例配方程序更加成熟，我们需要针对错误类型的文件和异常处理进行更多的防御编程。\n\n# 计算文件类型的统计信息\n\n在上一个食谱中，我们实现了一个工具，列出了任何目录的所有成员的大小。\n\n在这个食谱中，我们也将递归地计算大小，但是这次我们将把每个文件的大小累加到它们的文件名*扩展名*中。这样，我们可以向用户打印一个表格，列出我们拥有的每种文件类型的文件数量，以及这些文件类型的平均大小。\n\n# 怎么做...\n\n在本节中，我们将实现一个小工具，它递归地遍历给定的目录。在此过程中，它会计算所有文件的数量和大小，并按扩展名分组。最后，它打印该目录中存在哪些文件扩展名，每个扩展名有多少，以及它们的平均文件大小。\n\n1.  我们需要包含必要的头，我们声明我们使用命名空间`std`和`filesystem`。\n\n```cpp\n      #include <iostream>\n      #include <sstream>\n      #include <iomanip>\n      #include <map>\n      #include <filesystem>     \n\n      using namespace std;\n      using namespace filesystem;\n```\n\n2.  `size_string`功能已经在其他食谱中有所帮助。它将文件大小转换为人类可读的字符串。\n\n```cpp\n      static string size_string(size_t size)\n      {\n          stringstream ss;\n          if        (size >= 1000000000) { \n              ss << (size / 1000000000) << 'G'; \n          } else if (size >= 1000000)    { \n              ss << (size / 1000000) << 'M'; \n          } else if (size >= 1000)       { \n              ss << (size / 1000) << 'K';\n          } else { ss << size << 'B'; }\n\n          return ss.str();\n      }\n```\n\n3.  然后，我们实现一个助手函数，该函数接受一个`path`对象作为其参数，并遍历该路径中的所有文件。在途中，它会收集一个映射中的所有信息，该映射从文件扩展名映射到包含具有相同扩展名的所有文件的总数和累积大小的对。\n\n```cpp\n      static map<string, pair<size_t, size_t>> ext_stats(const path &dir)\n      {\n          map<string, pair<size_t, size_t>> m;\n\n          for (const auto &entry :\n                recursive_directory_iterator{dir}) {\n```\n\n4.  如果一个目录条目本身就是一个目录，我们就跳过它。在这一点上跳过它并不意味着我们没有递归地进入它。`recursive_directory_iterator`仍然是这样，但是我们不想看目录条目本身。\n\n```cpp\n              const path        p  {entry.path()};\n              const file_status fs {status(p)};\n\n              if (is_directory(fs)) { continue; }\n```\n\n5.  接下来，我们提取目录条目字符串的扩展部分。如果它没有扩展名，我们就跳过它。\n\n```cpp\n              const string ext {p.extension().string()};\n\n              if (ext.length() == 0) { continue; }\n```\n\n6.  接下来，我们计算我们正在查看的文件的大小。然后，我们在地图中查找这个扩展的聚合对象。如果此时还没有，则隐式创建。我们只需增加文件数量，并将文件大小添加到大小累加器中。\n\n```cpp\n              const size_t size {file_size(p)};\n\n              auto &[size_accum, count] = m[ext];\n\n              size_accum += size;\n              count      += 1;\n          }\n```\n\n7.  之后，我们归还地图。\n\n```cpp\n          return m;\n      }\n```\n\n8.  在主函数中，我们要么从命令行获取用户提供的路径，要么获取当前目录。当然，我们需要检查它是否存在，因为否则继续下去是没有意义的。\n\n```cpp\n      int main(int argc, char *argv[])\n      {\n          path dir {argc > 1 ? argv[1] : \".\"};\n\n          if (!exists(dir)) {\n              cout << \"Path \" << dir << \" does not exist.n\";\n              return 1;\n          }\n```\n\n9.  我们可以立即迭代`ext_stats`给我们的地图。因为地图中的`accum_size`项包含所有扩展名相同的文件的总和，所以在打印之前，我们将这个总和除以此类文件的总数。\n\n```cpp\n          for (const auto &[ext, stats] : ext_stats(dir)) {\n              const auto &[accum_size, count] = stats;\n\n              cout << setw(15) << left << ext << \": \"\n                   << setw(4) << right << count \n                   << \" items, avg size \"\n                   << setw(4) << size_string(accum_size / count)\n                   << 'n';\n          }\n      }\n```\n\n10.  编译并运行程序会产生以下输出。我从离线 C++ 引用中给了它一个文件夹作为命令行参数。\n\n```cpp\n      $ ./file_type ~/Documents/cpp_reference/\n      .css           :    2 items, avg size  41K\n      .gif           :    7 items, avg size 902B\n      .html          : 4355 items, avg size  38K\n      .js            :    3 items, avg size   4K\n      .php           :    1 items, avg size 739B\n      .png           :   34 items, avg size   2K\n      .svg           :   53 items, avg size   6K\n      .ttf           :    2 items, avg size 421K\n```\n\n# 实现一个工具，通过用符号链接替换重复项来减小文件夹大小\n\n有很多以各种方式压缩数据的工具。文件打包算法/格式最著名的例子是 ZIP 和 RAR。这类工具试图通过减少内部冗余来减小文件的大小。\n\n在压缩档案中的文件之前，减少磁盘使用的一个非常简单的方法就是*删除* *重复的*文件。在这个食谱中，我们将实现一个递归抓取目录的小工具。爬行时，它会寻找内容相同的文件。如果它找到这样的文件，它将删除除一个以外的所有重复文件。所有删除的文件将被指向当前唯一文件的符号链接所替代。这无需任何压缩即可节省空间，同时保留所有数据。\n\n# 怎么做...\n\n在这一节中，我们将实现一个小工具，找出目录中哪些文件是彼此重复的。有了这些知识，它将删除除一个文件之外的所有重复文件，并用符号链接替换它们，从而减小文件夹的大小。\n\nMake sure to have a *backup* of your system's data. We will be playing with STL functions that remove files. A simply *misspelled* path in such a program can lead to a program that greedily removes too many files in unwanted ways.\n\n1.  首先，我们需要包含必要的头，然后我们声明我们默认使用名称空间`std`和`filesystem`。\n\n```cpp\n      #include <iostream>\n      #include <fstream>\n      #include <unordered_map>\n      #include <filesystem>      \n\n      using namespace std;\n      using namespace filesystem;\n```\n\n2.  为了找出哪些文件彼此重复，我们将构建一个哈希映射，从文件内容的哈希映射到生成该哈希的第一个文件的路径。对 MD5 或 SHA 变体等文件使用生产哈希算法会是一个更好的主意。为了保持配方干净简单，我们只需将整个文件读入一个字符串，然后使用`unordered_map`已经用于字符串的相同哈希函数对象来计算哈希。\n\n```cpp\n      static size_t hash_from_path(const path &p)\n      {\n          ifstream is {p.c_str(), \n                       ios::in | ios::binary};\n          if (!is) { throw errno; }\n\n          string s;\n\n          is.seekg(0, ios::end);\n          s.reserve(is.tellg());\n          is.seekg(0, ios::beg);\n\n          s.assign(istreambuf_iterator<char>{is}, {});\n\n          return hash<string>{}(s);\n      }\n```\n\n3.  然后，我们实现构造这样一个哈希映射并删除重复项的函数。它递归地遍历目录及其子目录。\n\n```cpp\n      static size_t reduce_dupes(const path &dir)\n      {\n          unordered_map<size_t, path> m;\n          size_t count {0};\n\n          for (const auto &entry :\n                recursive_directory_iterator{dir}) {\n```\n\n4.  对于每个目录条目，它检查它本身是否是一个目录。跳过所有目录项目。对于每个文件，我们生成它的哈希值，并尝试将其插入到哈希映射中。如果哈希映射已经包含相同的哈希，那么这意味着我们已经插入了一个具有相同哈希的文件。这意味着我们刚刚发现了一个复制品！如果在插入过程中发生冲突，`try_emplace`返回的第二个值是`false`。\n\n```cpp\n              const path p {entry.path()};\n\n              if (is_directory(p)) { continue; }\n\n              const auto &[it, success] =\n                  m.try_emplace(hash_from_path(p), p);\n```\n\n5.  使用来自`try_emplace`的返回值，我们可以告诉用户我们刚刚插入了一个文件，因为我们第一次看到了它的散列。如果我们发现了一个副本，我们会告诉用户它是哪个文件的副本，然后删除它。删除后，我们创建一个符号链接来替换副本。\n\n```cpp\n              if (!success) {\n                  cout << \"Removed \" << p.c_str()\n                       << \" because it is a duplicate of \"\n                       << it->second.c_str() << 'n';\n\n                  remove(p);\n                  create_symlink(absolute(it->second), p);\n                  ++ count;\n              }\n```\n\n6.  在文件系统迭代之后，我们返回删除并替换为符号链接的文件数量。\n\n```cpp\n          }\n\n          return count;\n      }\n```\n\n7.  在主函数中，我们确保用户在命令行上提供了一个目录，并且这个目录存在。\n\n```cpp\n      int main(int argc, char *argv[])\n      {\n          if (argc != 2) {\n              cout << \"Usage: \" << argv[0] << \" <path>n\";\n              return 1;\n          }\n\n          path dir {argv[1]};\n\n          if (!exists(dir)) {\n              cout << \"Path \" << dir << \" does not exist.n\";\n              return 1;\n          }\n```\n\n8.  我们现在唯一需要做的就是在这个目录上调用`reduce_dupes`并打印它删除了多少文件。\n\n```cpp\n          const size_t dupes {reduce_dupes(dir)};\n\n          cout << \"Removed \" << dupes << \" duplicates.n\";\n      }\n```\n\n9.  在包含一些重复文件的示例目录中编译和运行程序如下所示。我使用`du`工具在启动我们的程序之前和之后检查文件夹大小，以证明该方法是有效的。\n\n```cpp\n      $ du -sh dupe_dir\n      1.1M dupe_dir\n\n      $ ./dupe_compress dupe_dir\n      Removed dupe_dir/dir2/bar.jpg because it is a duplicate of \n      dupe_dir/dir1/bar.jpg\n      Removed dupe_dir/dir2/base10.png because it is a duplicate of \n      dupe_dir/dir1/base10.png\n      Removed dupe_dir/dir2/baz.jpeg because it is a duplicate of \n      dupe_dir/dir1/baz.jpeg\n      Removed dupe_dir/dir2/feed_fish.jpg because it is a duplicate of \n      dupe_dir/dir1/feed_fish.jpg\n      Removed dupe_dir/dir2/foo.jpg because it is a duplicate of \n      dupe_dir/dir1/foo.jpg\n      Removed dupe_dir/dir2/fox.jpg because it is a duplicate of \n      dupe_dir/dir1/fox.jpg\n      Removed 6 duplicates.\n\n      $ du -sh dupe_dir\n      584K dupe_dir\n```\n\n# 它是如何工作的...\n\n我们使用`create_symlink`函数来使文件系统入口指向文件系统中的另一个文件。这样我们就可以避免重复的文件。我们也可以使用`create_hard_link`设置硬链接。在语义上，这是相似的，但是硬链接比软链接有其他的技术含义。不同的文件系统格式可能根本不支持硬链接，或者只支持引用同一文件的一定数量的硬链接。另一个问题是硬链接不能从一个文件系统链接到另一个文件系统。\n\n但是除了实现细节，在使用`create_symlink`或者`create_hard_link`的时候还有一个*明目张胆的错误*来源。以下几行包含一个错误。你能马上发现它吗？\n\n```cpp\npath a {\"some_dir/some_file.txt\"};\npath b {\"other_dir/other_file.txt\"};\nremove(b);\ncreate_symlink(a, b);\n```\n\n在执行这个程序的时候没有什么不好的事情发生，但是符号链接会被*破坏*。符号链接指向`\"some_dir/some_file.txt\"`，这是错误的。问题是它真的应该指向`\"/absolute/path/some_dir/some_file.txt\"`，或者`\"../some_dir/some_file.txt\"`。`create_symlink`调用使用一个正确的绝对路径，如果我们这样写的话:\n\n```cpp\ncreate_symlink(absolute(a), b);\n```\n\n`create_symlink` does not check whether the path we are linking to is *correct*.\n\n# 还有更多...\n\n我们已经注意到我们的散列函数太简单了。为了保持这个食谱的简单和没有外部依赖，我们选择了这种方式。\n\n我们的哈希函数有什么问题？实际上有两个问题:\n\n*   我们把整个文件读入一个字符串。这对大于系统内存的文件来说是灾难性的。\n*   C++ 散列函数特性`hash<string>`很可能不是为这种散列设计的。\n\n如果我们正在寻找一个更好的散列函数，我们应该选择一个快速的，内存友好的，并确保没有两个真正大但不同的文件得到相同的散列。后一个要求可能是最重要的。如果我们确定一个文件是另一个文件的副本，尽管它们不包含相同的数据，那么删除后肯定会有一些*数据丢失*。\n\n例如，更好的哈希算法是 MD5 或 SHA 变体之一。例如，为了在我们的程序中访问这些函数，我们可以使用 OpenSSL 加密应用编程接口。****"
  },
  {
    "path": "docs/exp-cpp-prog/README.md",
    "content": "# C++ 专家级编程\n\n> 原书：[Expert C++ Programming](https://libgen.rs/book/index.php?md5=CF70B1E4CDD32EA36B07C154F1D95374)\n> \n> 协议：[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)\n> \n> 阶段：机翻（1）\n> \n> 自豪地采用[谷歌翻译](https://translate.google.cn/)\n> \n> 让开源界提前感受入关。\n\n* [在线阅读](https://ccpp.apachecn.org)\n* [在线阅读（Gitee）](https://apachecn.gitee.io/apachecn-c-cpp-zh/)\n* [ApacheCN 学习资源](http://docs.apachecn.org/)\n\n## 贡献指南\n\n本项目需要校对，欢迎大家提交 Pull Request。\n\n> 请您勇敢地去翻译和改进翻译。虽然我们追求卓越，但我们并不要求您做到十全十美，因此请不要担心因为翻译上犯错——在大部分情况下，我们的服务器已经记录所有的翻译，因此您不必担心会因为您的失误遭到无法挽回的破坏。（改编自维基百科）\n\n## 联系方式\n\n### 负责人\n\n* [飞龙](https://github.com/wizardforcel): 562826179\n\n### 其他\n\n*   在我们的 [apachecn/apachecn-c-cpp-zh](https://github.com/apachecn/apachecn-c-cpp-zh) github 上提 issue.\n*   发邮件到 Email: `apachecn@163.com`.\n*   在我们的 [组织学习交流群](http://www.apachecn.org/organization/348.html) 中联系群主/管理员即可.\n\n## 赞助我们\n\n![](http://data.apachecn.org/img/about/donate.jpg)\n"
  },
  {
    "path": "docs/exp-cpp-prog/SUMMARY.md",
    "content": "+   [C++ 专家级编程](README.md)\n+   [零、新的 C++ 17 特性](00.md)\n+   [一、容器](01.md)\n+   [二、迭代器](02.md)\n+   [三、lambda 表达式](03.md)\n+   [四、STL 算法基础](04.md)\n+   [五、STL 算法的高级使用](05.md)\n+   [六、字符串、流类和正则表达式](06.md)\n+   [七、工具类](07.md)\n+   [八、并行性和并发性](08.md)\n+   [九、文件系统](09.md)\n"
  },
  {
    "path": "docs/game-dev-proj-ue/00.md",
    "content": "# 零、前言\n\n# 关于书\n\n游戏开发可以是一种创造性的满足爱好，也可以是一条全职的职业道路。这也是一个令人兴奋的方法来提高你的 C++ 技能，并将其应用于参与性和挑战性的项目。\n\n*带虚幻引擎的游戏开发项目*从作为游戏开发者入门所需的基本技能开始。游戏设计的基本原理将被清楚地解释，并用实际的练习来演示。然后，你将把你所学到的东西运用到具有挑战性的活动中。\n\n这本书首先介绍了虚幻编辑器和关键概念，如演员、蓝图、动画、继承和玩家输入。然后，您将进入三个项目中的第一个:构建躲避球游戏。在这个项目中，你将探索线条痕迹、碰撞、投射物、用户界面和音效，结合这些概念来展示你的新技能。\n\n然后，您将进入第二个项目；一个侧滚游戏，在这里你将实现包括动画混合，敌人 AI，产卵对象和收藏品的概念。最后一个项目是一个 FPS 游戏，在这里你将涵盖创建多人环境背后的关键概念。\n\n在这本虚幻引擎 4 游戏开发书的结尾，你将有信心和知识开始你自己的创意 UE4 项目，并把你的想法变成现实。\n\n## 关于作者\n\n*哈玛德·福兹*是 BIG insensitive 的领先游戏开发商(虚幻引擎)。\n\n*贡萨洛·马克斯*从 6 岁开始就是一名活跃的游戏玩家。他曾在一家葡萄牙初创公司 Sensei Tech 担任自由职业者，在那里他利用 UE4 开发了一个内部系统，以产生有助于机器学习的数据集。\n\n*大卫·佩雷拉*在 1998 年开始了这个游戏开发生涯，在那里他学会了使用 Clickteam 的《游戏工厂》，并开始制作自己的小游戏。大卫要感谢以下这些人:*我要感谢我的女朋友、我的家人和我的朋友在这次旅程中对我的支持。这本书献给我的祖母特蕾莎...!\").*\n\n*德文·雪莉*是一家名为《人能飞》的游戏工作室的技术设计师，致力于他们最新的用虚幻引擎 4 构建的 IP。Devin 在 Advancing Technology 大学学习游戏开发和游戏设计，并于 2012 年获得游戏设计学士学位。\n\n## 观众\n\n这本书适合任何想开始使用 UE4 进行游戏开发的人。对于任何以前使用过虚幻引擎并想巩固、提高和应用自己技能的人来说，它也将非常有用。为了更好地掌握本书中解释的概念，您必须事先了解 C++ 的基础知识，并理解变量、函数、类、多态性和指针。为了与本书中使用的 IDE 完全兼容，建议使用 Windows 系统。\n\n## 关于章节\n\n*第一章**虚幻引擎介绍*，探索虚幻引擎编辑器。将向您介绍编辑器的界面，了解如何在一个级别上操作参与者，了解蓝图可视化脚本语言的基础知识，并发现如何创建网格可以使用的材质资产。\n\n*第二章*、*使用虚幻引擎*介绍虚幻引擎游戏基础，以及如何创建 C++ 项目和设置项目的内容文件夹。您还将被介绍动画的主题。\n\n*第 3 章*、*角色类组件和蓝图设置*，向您介绍虚幻角色类，以及对象继承的概念和如何使用输入映射。\n\n*第四章*、*玩家输入*，介绍玩家输入的话题。通过使用动作映射和轴映射，您将学习如何将按键或触摸输入与游戏中的动作(如跳跃或移动)相关联。\n\n*第五章*、*线迹*，开始了一个叫躲避球的新项目。在本章中，您将了解线迹的概念以及它们在游戏中的各种使用方式。\n\n*第六章*、*碰撞物体*，探讨物体碰撞的话题。您将了解碰撞组件、碰撞事件和物理模拟。你还将学习计时器、投射物运动组件和物理材料的主题。\n\n*第 7 章*、 *UE4 实用工具*，教你如何实现虚幻引擎中可用的一些有用的实用工具，包括演员组件、接口和蓝图函数库，这将有助于保持你的项目结构良好，并且对于加入你团队的其他人来说是可接近的。\n\n*第八章*、*用户界面*，探讨游戏 UI 的话题。您将学习如何使用虚幻引擎的用户界面系统 UMG 制作菜单和平视显示器，以及如何使用进度条显示玩家角色的健康状况。\n\n*第九章*、*视听元素*，介绍虚幻引擎中声音和粒子效果的话题。您将学习如何将声音文件导入到项目中，并将它们用作 2D 和 3D 声音，以及如何将现有的粒子系统添加到游戏中。最后，将会有一个新的关卡，使用最后几章中建立的所有游戏机制来结束躲避球项目。\n\n*第 10 章*、*创造一个超视频 Scroller 游戏*，分解了超视频 Scroller 游戏项目的游戏机制。您将通过 Epic Games Launcher 创建 C++ SideScroller 项目模板，并通过操作默认人体模型骨架和导入自定义骨架网格来学习动画的基本概念。\n\n*第 11 章*、*混合空间 1D、键绑定和状态机*，向您介绍了使用混合空间 1D 和动画状态机开发平滑动画混合的工具。您还将跳转到 C++ 代码，在键绑定和角色移动组件的帮助下开发玩家角色的冲刺机制。\n\n*第 12 章*、*动画混合与蒙太奇*，向您介绍动画蓝图中的动画蒙太奇和动画混合功能，以开发玩家的角色投掷动画。您将了解动画片段，并使用每个骨骼的分层混合来正确混合角色的移动动画和投掷动画。\n\n*第十三章*、*敌方人工智能*，涵盖 AI 以及如何利用行为树和黑板开发 AI。您将实现一个人工智能，使用您将开发的蓝图执行程序沿着自定义路径巡逻。\n\n*第 14 章**产卵玩家抛射物*，向你介绍 Anim notifications 以及如何在游戏世界中产卵物体。您将实现一个自定义的动画通知，在投掷动画的特定帧产生玩家投射物。你也将为玩家投射物开发功能，允许这个投射物摧毁敌人的人工智能。\n\n*第 15 章*、*收藏品、充能器和皮卡*，演示了如何创建一个操纵玩家移动的自定义药剂充能器，以及一个玩家角色的可收藏硬币。您还将通过开发一个简单的用户界面来统计玩家发现的收藏品数量，从而了解更多关于 UMG 的信息。\n\n*第 16 章*、*多人游戏基础知识*，向您介绍重要的多人游戏概念，如服务器-客户端架构、连接、参与者所有权、角色和变量复制。您还将学习如何制作 2D 混合空间以及如何使用“变换修改骨骼”节点。你将开始一个多人 FPS 项目，创建一个可以行走、跳跃、上下看的角色，并且有两个复制的属性:生命值和护甲。\n\n*第 17 章*、*远程过程调用*，介绍了远程过程调用，如何在虚幻引擎 4 中使用枚举，如何使用双向循环数组索引。您还将通过添加武器和弹药的概念来扩展多人 FPS 项目。\n\n*第 18 章*、*多人游戏*中的游戏框架类，是本书的最后一章，解释了多人游戏中游戏框架类存在于何处，如何使用游戏状态和玩家状态类，以及如何实现一些有用的内置功能。您还将看到如何在游戏模式中使用匹配状态和其他概念。最后，您将通过添加死亡、重生、记分牌、死亡限制和拾取的概念来完成多人 FPS 项目。\n\n## 惯例\n\n文本中的码字、文件夹名、文件名、文件扩展名、路径名、虚拟网址和用户输入如下所示:\n\n“打开`Project Settings`，转到`Engine`部分的`Collision`小节。”\n\n您在屏幕上看到的单词，例如菜单或对话框中的单词，也会出现在文本中，如下所示:\n\n点击`New Object Channel`按钮，命名为`Dodgeball`，将其`Default Response`设置为`Block`\n\n代码块设置如下:\n\n```cpp\nif (bCanSeePlayer)\n{\n  //Start throwing dodgeballs\n  GetWorldTimerManager().SetTimer(ThrowTimerHandle,this,  &AEnemyCharacter::ThrowDodgeball,ThrowingInterval,true,  ThrowingDelay);\n}\n```\n\n新术语、缩写和重要词汇如下所示:“在本章中，我们将介绍**远程过程调用** ( **RPC** s)，这是另一个重要的多人游戏概念，允许服务器在客户端上执行功能，反之亦然。”\n\n## 开始之前\n\n本节将指导您完成安装和配置步骤，为您设置必要的工作环境。\n\n## 安装 Visual Studio\n\n因为我们将在使用虚幻引擎 4 时使用 C++ 语言，所以我们需要一个能够轻松与引擎协同工作的 **IDE** ( **集成开发环境**)。Visual Studio Community 是您在 Windows 上可用于此目的的最佳集成开发环境。如果您使用的是 macOS 或 Linux，您将不得不使用另一个 IDE，例如 Visual Studio Code、QT Creator 或 Xcode(仅在 macOS 上可用)。\n\n本书中给出的说明是特定于 Windows 上的 Visual Studio Community 的，因此如果您使用的是不同的操作系统和/或 IDE，那么您需要研究如何在您的工作环境中设置这些功能。在本节中，您将了解 Visual Studio 的安装，这样您就可以轻松编辑 UE4 的 C++ 文件:\n\n1.  前往[https://visualstudio.microsoft.com/downloads](https://visualstudio.microsoft.com/downloads)的 Visual Studio 下载网页。我们将在本书(4.24.3)中使用的虚幻引擎 4 版本的推荐 Visual Studio 社区版本是 Visual Studio Community 2019。一定要下载那个版本。\n2.  当你这样做的时候，打开你刚刚下载的可执行文件。它最终会将您带到下面的窗口，在这里您可以选择您的 Visual Studio 安装的模块。在这里，你需要勾选`Game Development with C++ `模块，然后点击窗口右下角的`Install`按钮。单击该按钮后，Visual Studio 将开始下载和安装。安装完成后，它可能会要求您重新启动电脑。重新启动电脑后，应安装 Visual Studio 并准备使用。\n3.  When you run Visual Studio for the first time, you may see a few windows, the first one of which is the login window. If you have a Microsoft Outlook/Hotmail account, you should use that account to log in, otherwise, you can skip login by clicking `Not now, maybe later`.\n\n    注意\n\n    如果您不输入电子邮件地址，在 Visual Studio 锁定之前，您只有 30 天的时间来使用它，您必须输入电子邮件地址才能继续使用它。\n\n4.  之后，会要求你选择一个配色方案。`Dark`主题是最受欢迎的，也是我们将在本节中使用的主题。\n\n最后，可以选择`Start Visual Studio`选项。但是，当您这样做时，您可以再次关闭它。在本书的前几章中，我们将深入研究如何使用 Visual Studio。\n\n## 史诗游戏发射器\n\n要访问虚幻引擎 4，您需要下载史诗游戏启动器，可通过以下链接获得:[https://www.unrealengine.com/get-now](https://www.unrealengine.com/get-now)。此链接将允许您下载适用于 Windows 和 macOS 的史诗游戏启动器。如果你使用 Linux，你必须下载虚幻引擎源代码，并从源代码编译它–[https://docs . unrealengine . com/en-US/GettingStarted/downloadinunreal Engine](https://docs.unrealengine.com/en-US/GettingStarted/DownloadingUnrealEngine):\n\n1.  在这里，你必须选择`Publishing License`选项，然后点击它下面的`SELECT`按钮。该许可证将允许您使用 UE4 创建可以直接发布给用户的项目(例如，在数字游戏商店中)。然而，`Creators License`不允许你直接向你的最终用户发布你的作品。\n2.  之后，您将被要求接受这些条款和条件，一旦您接受这些条款和条件，一个`.msi`文件将被下载到您的计算机上。下载完成后打开这个`.msi`文件，会提示你安装史诗游戏启动器。按照安装说明，然后启动史诗游戏启动器。当您这样做时，您应该会看到一个登录屏幕。\n3.  If you already have an account, you can simply log in with your existing credentials. If you don't, you'll have to sign up for an Epic Games account by clicking the `Sign Up` text at the bottom.\n\n    使用您的帐户登录后，您应该会看到`Home`标签。从那里，你可以点击显示`Unreal Engine`的文字进入`Unreal Engine`标签。\n\n4.  当你做到这一点时，你将会看到`Unreal Engine`标签。虚幻引擎选项卡充当虚幻引擎资源的中心。从该页面，您将能够访问以下内容:\n    *   `News`页面，在这里你可以看到所有最新的虚幻引擎新闻。\n    *   `Youtube`频道，在这里你可以观看几十个教程和直播流，详细介绍几个不同的虚幻引擎主题。\n    *   `AnswerHub`页面，在该页面上，您可以看到、询问和回答虚幻引擎社区提出和回答的问题。\n    *   `Forums`页面，在这里你可以访问虚幻引擎论坛。\n    *   `Roadmap`页面，在该页面上，您将能够访问虚幻引擎路线图，包括该引擎过去版本中提供的功能，以及当前正在为未来版本开发的功能。\n5.  在史诗游戏启动器的顶部，在虚幻引擎选项卡中，您可以看到其他几个选项卡，如`Unreal Engine`选项卡(您当前看到的子选项卡)`Learn`选项卡和`Marketplace`选项卡。让我们看看这些虚幻引擎子选项卡。\n6.  `Learn`选项卡将允许您访问几个与学习如何使用虚幻引擎 4 相关的资源。从这里，您将能够访问`Get Started with Unreal Engine 4`页面，该页面将带您进入一个页面，允许您选择如何开始学习虚幻引擎 4。\n7.  您还可以访问`Documentation`页面，该页面包含对引擎源代码中使用的类的引用，以及`Unreal Online Learning`页面，该页面包含关于虚幻引擎 4 特定主题的几门课程。\n8.  `Learn`标签的右边是`Marketplace`标签。该选项卡向您展示了虚幻引擎社区成员制作的几个资产和代码插件。在这里，你可以找到 3D 资产、音乐、关卡和代码插件，这些将帮助你推进和加速游戏的发展。\n9.  最后，在`Marketplace`标签的右边，我们有`Library`标签。在这里，您将能够浏览和管理您所有的虚幻引擎版本安装、虚幻引擎项目和您的市场资产库。因为我们还没有这些东西，这些部分都是空的。让我们改变这一点。\n10.  点击`ENGINE VERSIONS`文本右侧的黄色加号。这应该会出现一个新的图标，在那里你可以选择你想要的虚幻引擎版本。\n11.  Throughout this book, we'll be using version `4.24.3` of Unreal Engine. After you've selected that version, click the `Install` button:\n\n    ![Figure 0.1: The icon that allows you to install Unreal Engine 4.24.3  ](img/B16183_00_01.jpg)\n\n    图 0.1:允许您安装虚幻引擎 4.24.3 的图标\n\n12.  After you've done this, you'll be able to choose the installation directory for this Unreal Engine version, which will be of your choosing, and you should then click the `Install` button again.\n\n    注意\n\n    如果您在安装 4.24 版本时遇到问题，请确保以最短的路径将其安装在您的 D 驱动器上(也就是说，不要试图将其安装在太深的文件夹中，并确保这些文件夹有短名称)。\n\n13.  This will result in the installation of Unreal Engine 4.24.3 starting. When the installation is done, you can launch the editor by clicking the `Launch` button of the version icon:\n\n    ![Figure 0.2: The version icon once installation has finished  ](img/B16183_00_02.jpg)\n\n图 0.2:安装完成后的版本图标\n\n## 代码包\n\n你可以在[https://packt.live/38urh8v](https://packt.live/38urh8v)的 GitHub 上找到这本书的代码文件。在这里，您将找到练习代码、活动解决方案、图像和任何其他资产，例如完成本书中实用元素所需的数据集。\n\n## 取得联系\n\n我们随时欢迎读者的反馈。\n\n**综合反馈**:如果您对本书有任何疑问，请在留言主题中提及书名，发邮件至[customercare@packtpub.com](mailto:customercare@packtpub.com)。\n\n**勘误表**:虽然我们已经尽了最大的努力来保证内容的准确性，但是错误还是会发生。如果你在这本书里发现了错误，如果你能向我们报告，我们将不胜感激。请访问[www.packtpub.com/support/errata](http://www.packtpub.com/support/errata)并填写表格。\n\n**盗版**:如果您在互联网上遇到任何形式的我们作品的非法拷贝，如果您能提供我们的位置地址或网站名称，我们将不胜感激。请联系我们在[copyright@packt.com](mailto:copyright@packt.com)与材料的链接。\n\n**如果你有兴趣成为一名作者**:如果有一个你有专长的话题，你有兴趣写或者投稿一本书，请访问[authors.packtpub.com](http://authors.packtpub.com)。\n\n## 请留言评论\n\n在亚马逊上留下详细、公正的评论，让我们知道你的想法。我们感谢所有反馈，这有助于我们继续生产优秀的产品，并帮助有抱负的开发人员提高技能。请抽出几分钟时间发表您的想法，这对我们有很大影响。"
  },
  {
    "path": "docs/game-dev-proj-ue/01.md",
    "content": "# 一、虚幻引擎介绍\n\n概述\n\n本章将介绍虚幻引擎编辑器。您将了解编辑器的界面；如何在级别中添加、移除和操作对象；如何使用虚幻引擎的蓝图可视化脚本语言；以及如何结合网格使用材料。\n\n到本章结束时，您将能够导航虚幻引擎编辑器，创建您自己的演员，在关卡中操纵他们，并创建素材。\n\n# 简介\n\n欢迎来到*虚幻引擎*游戏开发项目。如果这是你第一次使用**虚幻引擎 4** ( **UE4** )，这本书将支持你开始使用市场上最受欢迎的游戏引擎之一。你将发现如何建立你的游戏开发技能，以及如何通过创建自己的视频游戏来表达自己。如果你已经尝试使用 UE4，这本书将帮助你进一步发展你的知识和技能，这样你就可以更容易和有效地构建游戏。\n\n游戏引擎是一个软件应用，允许你从头开始制作视频游戏。它们的功能集差异很大，但通常允许您导入多媒体文件，如 3D 模型、图像、音频和视频，并通过使用编程来操作这些文件，其中您可以使用编程语言，如 C++、Python 和 Lua 等。\n\n虚幻引擎 4 使用两种主要的编程语言，C++ 和蓝图，后者是一种可视化脚本语言，允许你做 C++ 也允许的大部分事情。虽然我们将在本书中教授一点蓝图，但我们将主要关注 C++，因此希望您对该语言有一个基本的了解，包括诸如*变量*、*函数*、*类*、*继承*和*多态性*等主题。在适当的地方，我们会在整本书中提醒您这些主题。\n\n用虚幻引擎 4 制作的热门电子游戏包括*堡垒之夜*、*最终幻想七重制*、*边境地带 3* 、*星球大战:绝地堕落阶*、*齿轮 5* 和*盗贼之海*等。所有这些都具有非常高的视觉保真度，是众所周知的，并且拥有或曾经拥有数百万玩家。\n\n在下面的链接中，你可以看到一些用虚幻引擎 4 制作的伟大游戏:[https://www.youtube.com/watch?v=lrPc2L0rfN4](https://www.youtube.com/watch?v=lrPc2L0rfN4)。这个展示将向您展示虚幻引擎 4 允许您制作的各种游戏，包括视觉和游戏风格。\n\n如果你想有一天制作视频中展示的游戏，或者以任何方式为它们做贡献，你已经朝着这个方向迈出了第一步。\n\n我们现在从第一步开始，开始学习虚幻引擎编辑器。我们将学习它的界面，如何操作关卡内部的对象，如何创建我们自己的对象，如何使用蓝图脚本语言，以及主要的游戏事件是做什么的，以及如何为网格创建材质。\n\n让我们从学习如何在第一个练习中创建一个新的虚幻引擎 4 项目开始这一章。\n\n注意\n\n在继续本章之前，请确保您已经安装了*前言*中提到的所有必要软件。\n\n## 练习 1.01:创建虚幻引擎 4 项目\n\n在第一个练习中，我们将学习如何创建一个新的虚幻引擎 4 项目。UE4 有预定义的项目模板，允许您实现项目的基本设置。在本练习中，我们将使用`Third Person`模板项目。\n\n以下步骤将帮助您完成本练习:\n\n1.  安装虚幻引擎 4.24 版本后，点击版本图标的`Launch`按钮启动编辑器。\n2.  完成后，您将看到引擎的“项目”窗口，该窗口将显示您可以打开和处理的现有项目，并为您提供创建新项目的选项。因为我们还没有项目，`Recent Projects`部分会是空的。要创建一个新项目，你首先必须选择`Project Category`，在我们的例子中是`Games`。\n3.  选择该选项后，点击`Next`按钮。之后，您将看到项目模板窗口。此窗口将显示虚幻引擎中所有可用的项目模板。当创建一个新的项目时，您可以选择添加一些现成的资产和代码，然后根据自己的喜好进行修改，而不是让这个项目一开始就空着。不同类型的游戏有几种项目模板，但在这种情况下，我们希望使用`Third Person`项目模板。\n4.  Select that template and click the `Next` button, which should take you to the `Project Settings` window.\n\n    在此窗口中，您可以选择几个与项目相关的选项:\n\n    *   `Blueprint or C++ `:选择是否希望能够添加 C++ 类。默认选项可能是`Blueprint`，但是在我们的例子中，我们想要选择`C++ `选项。\n    *   `Quality`:选择是希望项目拥有高质量的图形还是高性能。您可以将该选项设置为`Maximum Quality`。\n    *   `Raytracing`:选择是启用还是禁用光线追踪。光线追踪是一种新颖的图形渲染技术，它允许您通过模拟光线在数字环境中的路径来渲染对象。尽管这种技术在性能方面相当昂贵，但它也提供了更逼真的图形，尤其是在照明方面。可以设置为`disabled`。\n    *   `Target Platforms`:选择你希望这个项目运行的主要平台。将该选项设置为`Desktop/Console`。\n    *   `Starter Content`:选择是否希望这个项目附带一套额外的基础资产。将该选项设置为`With Starter Content`。\n    *   `Location and Name`:在窗口的底部，你可以选择你的项目在电脑上的存储位置和名称。\n5.  确保所有选项都设置为预期值后，单击`Create Project`按钮。这将导致根据您设置的参数创建项目，可能需要几分钟时间才能完成。\n\n现在让我们通过执行下一节中的步骤开始学习虚幻引擎 4，在这里我们将学习使用编辑器的一些基础知识。\n\n# 认识不真实\n\n现在将向您介绍虚幻引擎编辑器，这是熟悉虚幻引擎 4 的一个基本主题。\n\n当您的项目完成生成后，您应该会看到虚幻引擎编辑器自动打开。这个屏幕很可能是您在使用虚幻引擎时看到最多的屏幕，所以习惯它很重要。\n\n让我们分解一下在编辑器窗口中看到的内容:\n\n![Figure 1.1: The Unreal Engine editor divided in its six main windows ](img/B16183_01_01.jpg)\n\n图 1.1:虚幻引擎编辑器分为六个主窗口\n\n1.  `Content Browser`: The window that occupies the majority of the bottom of the screen is the `Content Browser`. This window will let you browse and manipulate all the files and assets located inside your project's folder. As was mentioned at the start of the chapter, Unreal Engine will allow you to import several types of multimedia files, and `Content Browser` is the window that will allow you to browse and edit them in their respective sub-editors. Whenever you create an Unreal Engine project, it will always generate a `Content` folder. This folder will be the **root directory** of the `Content Browser`, meaning you can only browse files inside that folder. You can see the directory you're currently browsing inside `Content Browser` by looking at the top of it, which, in our case, is `Content -> ThirdPersonCPP`.\n\n    如果你点击`Filters`按钮左边的图标，在`Content Browser`的最左边，你将能够看到`Content`文件夹的目录层次。该目录视图允许您在项目的`Content`文件夹中选择、展开和折叠单个目录:\n\n    ![Figure 1.2: Content Browser's directory view ](img/B16183_01_02.jpg)\n\n    图 1.2:内容浏览器的目录视图\n\n2.  `Viewport`:在屏幕的正中央，你可以看到`Viewport`窗口。这将向您显示当前级别的内容，并允许您浏览您的级别以及在其中添加、移动、移除和编辑对象。它还包含关于视觉滤镜、对象滤镜(您可以看到哪些对象)和您的级别中的照明的几个不同参数。\n3.  `World Outliner`:在屏幕右上角，你会看到`World Outliner`。这将允许您快速列出和操作您所在级别的对象。`Viewport`和`World Outliner`携手合作，让你管理自己的水平，前者会向你展示它的样子，后者会帮助你管理和组织它。与`Content Browser`类似，`World Outliner`允许你在目录中组织你所在级别的对象，不同的是`Content Browser`显示你项目中的*资产*，而`World Outliner`显示你所在级别的*对象*。\n4.  The `Details` panel and `World Settings`: At the far right of the screen, below `World Outliner`, you'll be able to see two windows – the `Details` panel and the `World Settings` window. The `Details` window allows you to edit the properties of an object that you selected in your level. As there are no objects selected in the screenshot, it is empty. However, if you select any object in your level by *left-clicking* on it, its properties should appear in this window, as shown in the following screenshot:\n\n    ![Figure 1.3: The Details tab ](img/B16183_01_03.jpg)\n\n    图 1.3:详细信息选项卡\n\n    `World Settings`窗口允许您设置您的级别的整体设置，而不是单个对象的设置。在这里，您可以更改诸如 Kill Z(您希望对象被破坏的高度)和所需的照明设置等内容:\n\n    ![Figure 1.4: The World Settings window ](img/B16183_01_04.jpg)\n\n    图 1.4:世界设置窗口\n\n5.  `Toolbar`: At the top of the screen you'll see the editor `Toolbar`, where you'll be able to save your current level, access the project and editor settings, and play your level, among other things.\n\n    注意\n\n    我们将只使用这些工具栏中的一些按钮，即`Save Current`、`Settings`、`Blueprints`、`Build`和`Play`按钮。\n\n6.  `Modes`:在屏幕的最左边，你会看到`Modes`窗口。它将允许您将对象拖动到您的级别，例如立方体和球体、光源以及为各种目的设计的其他类型的对象。\n\n现在我们已经了解了虚幻引擎编辑器的主窗口，让我们看看如何管理这些窗口。\n\n# 编辑器窗口\n\n正如我们所看到的，虚幻引擎编辑器由许多窗口组成，所有窗口都是可调整大小、可移动的，并且在它们的顶部有一个相应的选项卡。你可以点击并按住一个窗口的标签并拖动它，以便将它移动到其他地方。您可以通过右键单击标签并选择`Hide`选项来隐藏标签标签:\n\n![Figure 1.5: How to hide a tab ](img/B16183_01_05.jpg)\n\n图 1.5:如何隐藏选项卡\n\n如果标签标签已经被隐藏，你可以通过点击该窗口左上角的*黄色三角形*让它们重新出现，如下图所示:\n\n![Figure 1.6: The yellow triangle that allows you to show a window's tab ](img/B16183_01_06.jpg)\n\n图 1.6:允许您显示窗口选项卡的黄色三角形\n\n请记住，您可以通过单击编辑器左上角的`Window`按钮来浏览和打开编辑器中所有可用的窗口，包括刚才提到的窗口。\n\n另一件你应该知道的非常重要的事情是如何从编辑器内部发挥你的水平(也称为 **PIE** )。在编辑器`Toolbar`的右边，你会看到`Play`按钮。如果您点按它，您将开始在编辑器中播放当前打开的级别。\n\n一旦点击`Play`，就可以使用 *W* 、 *A* 、 *S* 、 *D* 键移动玩家角色，空格键跳跃，移动`Mouse`旋转摄像头:\n\n![Figure 1.7: The level being played inside the editor ](img/B16183_01_07.jpg)\n\n图 1.7:编辑器中正在播放的级别\n\n然后您可以按下 *Esc* 键(退出)，以停止播放该级别。\n\n现在我们已经习惯了编辑器的一些窗口，让我们更深入地看看`Viewport`窗口。\n\n# 视口导航\n\n我们在上一节中提到`Viewport`窗口将允许你可视化你的水平，以及操纵它里面的物体。因为这是您可以使用的一个非常重要的窗口，并且有很多功能，所以我们将在本节中了解更多信息。\n\n在我们开始了解`Viewport`窗口之前，让我们快速了解一下**等级**。在 UE4 中，级别表示对象的**集合**，以及它们的位置和属性。`Viewport`窗口将始终向您显示当前所选级别的内容，在本例中，该级别已经创建并与`Third Person`模板项目一起生成。在这个关卡中，你将能够看到四个墙壁物体、一个地面物体、一组楼梯和其他一些高架物体，以及由 UE4 人体模型代表的玩家角色。您可以通过从`Content Browser`打开多个级别来创建多个级别并在它们之间切换。\n\n为了操作和浏览当前选择的级别，您必须使用`Viewport`窗口。按住窗口内的*鼠标左键*，向左移动鼠标**向右移动*即可水平旋转摄像头，向前移动鼠标**向后移动*即可前后移动摄像头。按住*鼠标右键*也可以达到类似的效果，只是向前移动鼠标*和向后移动*时相机会垂直旋转，可以水平和垂直旋转相机。****\n\n ***此外，您也可以通过点击并按住*鼠标右键*(鼠标左键的*也可以，但使用它进行移动并没有那么有用，因为旋转相机时没有那么大的自由度)并使用 *W* 和 *S* 键向前和向后移动， *A* 和 *D* 键向侧面移动， *E 【T14**\n\n如果你看`Viewport`窗口的右上角，你会看到一个小的摄像头图标，旁边有一个数字，这将允许你改变摄像头在`Viewport`窗口中的移动速度。\n\n您可以在`Viewport`窗口中做的另一件事是更改其可视化设置。您可以通过单击当前显示`Lit`的按钮来更改`Viewport`窗口中的可视化类型，该按钮将向您显示不同照明和其他类型可视化过滤器的所有可用选项。\n\n如果您点击`Perspective`按钮，您将可以选择从透视图和正交视图中查看您的关卡，后者可以帮助您更快地构建关卡。\n\n现在让我们继续讨论在您的级别中操纵对象(也称为演员)的主题。\n\n# 操纵演员\n\n在虚幻引擎中，所有可以放置在一个级别中的对象都被称为演员。在电影中，演员是扮演角色的人，但在 UE4 中，你在关卡中看到的每一个物体，包括墙壁、地板、武器和角色，都是演员。\n\n每个演员都必须拥有所谓的`Transform`属性，它是三个东西的集合:\n\n*   **位置**:一个`Vector`属性，表示该演员在 *X* 、 *Y、*和 *Z* 轴中的位置。一个向量只是一个包含三个浮点数的元组，每个浮点数代表一个点在每个轴上的位置。\n*   **旋转**:表示该演员沿 *X* 、 *Y、*和 *Z* 轴旋转的`Rotator`属性。旋转器也是一个有三个浮点数的元组，每个轴上有一个浮点数表示旋转的角度。\n*   **比例**:一个`Vector`属性，表示该演员在 *X* 、 *Y、*和 *Z* 轴的级别中的比例(表示大小)。这也是三个浮点数的集合，每个轴中的比例值一个。\n\n演员可以在一个级别中移动、旋转和缩放，这将相应地修改他们的`Transform`属性。为此，通过左键单击选择您所在级别的任何对象。您应该会看到**移动**工具出现:\n\n![Figure 1.8: The Move tool, which allows you to move an Actor in the level  ](img/B16183_01_08.jpg)\n\n图 1.8:移动工具，它允许你在关卡中移动一个参与者\n\n移动工具是一个三轴小控件，允许您同时在任意轴上移动对象。移动工具的红色箭头(指向上图左侧)代表 *X* 轴，绿色箭头(指向上图右侧)代表 *Y* 轴，蓝色箭头(指向上图)代表 *Z* 轴。如果您单击并按住这些箭头中的任意一个，然后在关卡中拖动它们，您将在关卡中沿着该轴移动您的演员。如果您点按将两个箭头连接在一起的控制柄，您将同时沿着这两个轴移动角色，如果您点按所有箭头交叉点处的白色球体，您将沿着所有三个轴自由移动角色:\n\n![Figure 1.9: An actor being moved on the Z axis using the Move tool ](img/B16183_01_09.jpg)\n\n图 1.9:使用移动工具在 Z 轴上移动演员\n\n“移动”工具将允许您在层级中移动一个演员，但是如果您想要旋转或缩放一个演员，您需要分别使用“旋转”和“缩放”工具。您可以通过分别按下 *W* 、 *E* 和 *R* 键在移动、旋转和缩放工具之间切换。按下 *E* 切换到旋转工具:\n\n![Figure 1.10: The Rotate tool, which allows you to rotate an Actor ](img/B16183_01_10.jpg)\n\n图 1.10:旋转工具，它允许你旋转一个演员\n\n如预期的那样，旋转工具将允许您在您的级别中旋转演员。您可以*单击并按住*任意弧线，以围绕其相关轴旋转演员。红色弧线(上图左上)将围绕 *X* 轴旋转演员，绿色弧线(上图右上)将围绕 *Y* 轴旋转演员，蓝色弧线(上图下中心)将围绕 *Z* 轴旋转演员:\n\n![Figure 1.11: A cube before and after being rotated 30 degrees around the X axis  ](img/B16183_01_11.jpg)\n\n图 1.11:绕 X 轴旋转 30 度前后的立方体\n\n请记住，物体围绕 *X* 轴的旋转通常被指定为**滚动**，其围绕 *Y* 轴的旋转通常被指定为**俯仰**，其围绕 *Z* 轴的旋转通常被指定为**偏航**。\n\n最后，我们有缩放工具。按下 *R* 切换到:\n\n![Figure 1.12: The Scale tool ](img/B16183_01_12.jpg)\n\n图 1.12:缩放工具\n\n缩放工具将允许您在 *X* 、 *Y* 和 *Z* 轴上增加和减少演员的比例(大小)，其中红色手柄(上图左侧)将在 *X* 轴上缩放演员，绿色手柄(上图右侧)将在 *Y* 轴上缩放演员，蓝色手柄(上图上部)将在 *Z* 轴上缩放演员\n\n![Figure 1.13: A character Actor before and after being scaled on all three axes  ](img/B16183_01_13.jpg)\n\n图 1.13:在所有三个轴上缩放前后的角色演员\n\n您也可以通过单击`Viewport`窗口顶部的以下图标在移动、旋转和缩放工具之间切换:\n\n![Figure 1.14: The Move, Rotate, and Scale tool icons ](img/B16183_01_14.jpg)\n\n图 1.14:移动、旋转和缩放工具图标\n\n此外，您可以通过移动、旋转和缩放工具图标右侧的网格捕捉选项来更改移动、旋转和缩放对象的增量。通过按下当前橙色的按钮，您可以完全禁用捕捉，通过按下显示当前捕捉增量的按钮，您可以更改这些增量:\n\n![Figure 1.15: The grid snapping icons for moving, rotating, and scaling ](img/B16183_01_15.jpg)\n\n图 1.15:用于移动、旋转和缩放的网格捕捉图标\n\n现在，您已经知道如何操作已经存在于您的级别中的演员，让我们在下一个练习中学习如何在我们的级别中添加和移除演员。\n\n## 练习 1.02:添加和移除演员\n\n在本练习中，我们将在我们的级别中添加和移除演员。\n\n当涉及到向您的级别添加演员时，有两种主要方法可以做到这一点:从`Content Browser`拖动资产，或者从`Modes`窗口的放置模式拖动默认资产。\n\n以下步骤将帮助您完成本练习:\n\n1.  If you go to the `ThirdPersonCPP -> Blueprints` directory inside `Content Browser`, you will see the `ThirdPersonCharacter` Actor. If you drag that asset to your level using the *left mouse button*, you will be able to add an instance of that Actor to it, and it will be placed wherever you let go of the *left mouse button*:\n\n    ![Figure 1.16: Dragging an instance of the ThirdPersonCharacter Actor to our level ](img/B16183_01_16.jpg)\n\n    图 1.16:将第三个人角色参与者的实例拖到我们的级别\n\n2.  You can similarly drag an Actor from the `Modes` window to your level as well:\n\n    ![Figure 1.17: Dragging a Cylinder Actor to our level ](img/B16183_01_17.jpg)\n\n    图 1.17:拖动一个圆柱体演员到我们的水平\n\n3.  In order to delete an Actor, you can simply select the Actor and press the *Delete* key. You can also *right-click* on an Actor to take a look at the many other options available to you regarding that Actor.\n\n    注意\n\n    虽然我们不会在本书中讨论这个主题，但是为了原型的目的，开发人员可以用简单的盒子和几何图形填充他们的级别的方法之一是 BSP 画笔。这些可以很快塑造成你想要的形状，因为你建立你的水平。想了解更多关于 BSP 笔刷的信息，请访问本页:[https://docs.unrealengine.com/en-US/Engine/Actors/Brushes](https://docs.unrealengine.com/en-US/Engine/Actors/Brushes)。\n\n至此，我们结束了本练习，并学习了如何在我们的级别中添加和移除参与者。\n\n现在我们已经知道如何导航`Viewport`窗口，让我们了解蓝图演员。\n\n# 蓝图演员\n\n在 UE4 中，蓝图这个词可以用来指两种不同的东西:UE4 的可视化脚本语言或特定类型的资产，也称为蓝图类或蓝图资产。\n\n正如我们之前提到的，Actor 是一个可以放置在关卡中的对象。该对象可以是 C++ 类的实例，也可以是蓝图类的实例，两者都必须从 Actor 类继承(直接或间接)。那么，你可能会问，C++ 类和蓝图类有什么区别？有几个:\n\n*   如果将编程逻辑添加到 C++ 类中，您将能够访问比创建蓝图类时更高级的引擎功能。\n*   在蓝图类中，您可以轻松地查看和编辑该类的可视组件，如 3D 网格或触发器盒碰撞，以及修改在暴露给编辑器的 C++ 类中定义的属性，这使得管理这些属性变得更加容易。\n*   在蓝图类中，您可以很容易地引用项目中的其他资产，而在 C++ 中，您也可以这样做，但是不那么简单和灵活。\n*   在蓝图可视化脚本上运行的编程逻辑在性能方面比 C++ 类慢。\n*   It's simple to have more than one person work on a C++ class simultaneously without conflicts in a source version platform, whereas with a Blueprint class, which is interpreted as a binary file instead of a text file, this will cause conflicts in your source version platform if two different people edit the same Blueprint class.\n\n    注意\n\n    如果你不知道什么是源版本平台，这就是几个开发人员如何在同一个项目上工作，并让它随着其他开发人员所做的工作而更新。在这些平台中，不同的人通常可以同时编辑同一个文件，只要他们编辑该文件的不同部分，并且仍然接收到其他程序员所做的更新，而不会影响您对同一文件的工作。最受欢迎的源代码版本平台之一是 GitHub。\n\n请记住，蓝图类可以从 C++ 类继承，也可以从另一个蓝图类继承。\n\n最后，在我们继续创建我们的第一个蓝图类之前，您应该知道的另一件重要的事情是，您可以在 C++ 类中编写编程逻辑，然后创建一个从该类继承的蓝图类，但是如果您在 C++ 类中指定，也可以访问它的属性和方法。您可以使用蓝图脚本语言，让蓝图类编辑在 C++ 类中定义的属性，以及调用和重写函数。我们将在这本书里做一些这样的事情。\n\n现在您对蓝图类有了更多的了解，让我们在下一个练习中创建自己的蓝图类。\n\n## 练习 1.03:创建蓝图演员\n\n在这个简短的练习中，我们将学习如何创建一个新的蓝图演员。\n\n以下步骤将帮助您完成本练习:\n\n1.  Go to the `ThirdPersonCPP -> Blueprints` directory inside `Content Browser` and *right-click* inside it. The following window should pop up:\n\n    ![Figure 1.18: The options window that appears when you right-click inside content browser ](img/B16183_01_18.jpg)\n\n    图 1.18:在内容浏览器中右键单击时出现的选项窗口\n\n    此选项菜单包含您可以在 UE4 中创建的资产类型(蓝图只是一种资产类型，还有其他类型的资产，如`Level`、`Material`和`Sound`)。\n\n2.  Click the `Blueprint Class` icon to create a new Blueprint class. When you do, you will be given the option to choose the C++ or Blueprint class that you want to inherit from:\n\n    ![Figure 1.19: The Pick Parent Class window that pops up when you  create a new Blueprint class ](img/B16183_01_19.jpg)\n\n    图 1.19:创建新蓝图类时弹出的选择父类窗口\n\n3.  从该窗口中选择第一个类`Actor`类。之后，您将自动选择新蓝图类的文本，以便根据您的需要对其进行命名。命名这个蓝图类`TestActor`并按`Enter`键接受这个名字。\n\n完成这些步骤后，您将已经创建了蓝图类，因此已经完成了本练习。创建该资产后，用鼠标左键*双击该资产，打开蓝图编辑器。*\n\n *# 蓝图编辑器\n\n蓝图编辑器是虚幻引擎编辑器中的一个子编辑器，专门用于蓝图类。在这里，您将能够编辑蓝图类或其父类的属性和逻辑，以及它们的视觉外观。\n\n当您打开一个 Actor 蓝图类时，您应该会看到蓝图编辑器。这是允许您在 UE4 中编辑蓝图类的窗口。让我们了解一下您当前看到的窗口:\n\n![Figure 1.20: The Blueprint editor window is broken down into five parts ](img/B16183_01_20.jpg)\n\n图 1.20:蓝图编辑器窗口分为五个部分\n\n1.  `Viewport`: Front and center in the editor you have the `Viewport` window. This window, similar to the `Level Viewport` window that we already learned about, will allow you to visualize your Actor and edit its components. Every actor can have several Actor Components, some of which have a visual representation, such as Mesh Components and Collision Components. We'll be talking about Actor Components in more depth in later chapters.\n\n    从技术上讲，这个中心窗口包含三个选项卡，其中只有一个是`Viewport`窗口，但是我们将在处理这个编辑器的界面后讨论另一个重要的选项卡`Event Graph`。第三个标签是`Construction Script`窗口，我们不会在本书中涉及。\n\n2.  `Components`:在编辑器的左上角，有`Components`窗口。如前所述，参与者可以有几个参与者组件，这个窗口将允许您在蓝图类中添加和删除这些参与者组件，以及访问在它继承的 C++ 类中定义的参与者组件。\n3.  `My Blueprint`:在编辑器的左下方，有`My Blueprint` 窗口。这将允许您浏览、添加和删除在这个蓝图类及其继承的 C++ 类中定义的变量和函数。请记住，蓝图有一种特殊的功能，称为**事件**，用于表示游戏中发生的事件。你应该在这个窗口看到其中的三个:`BeginPlay`、`ActorBeginOverlap`和`Tick`。我们将在几个段落中讨论这些。\n4.  `Details`:在编辑器的右边，有`Details`窗口。类似于编辑器的`Details`窗口，该窗口将向您显示当前选定的参与者组件、函数、变量、事件或该蓝图类的任何其他单个元素的属性。如果您当前没有选择任何元素，此窗口将为空。\n5.  `Toolbar`:在编辑器的顶部中央有`Toolbar`窗口。这个窗口将允许你编译你在这个蓝图类中写的代码，保存它，在`Content Browser`中找到它，并且访问这个类的设置，等等。\n\n通过查看蓝图编辑器的右上角，可以看到蓝图类的父类。如果您单击父类的名称，您将通过虚幻引擎编辑器转到相应的蓝图类，或者通过 Visual Studio 转到 C++ 类。\n\n此外，您可以通过单击蓝图编辑器左上角的`File`选项卡并选择`Reparent Blueprint`选项来更改蓝图类的父类，这将允许您指定该蓝图类的新父类。\n\n既然我们已经了解了蓝图编辑器的基础知识，让我们来看看它的事件图。\n\n# 事件图\n\n`Event Graph`窗口是您编写所有蓝图可视化脚本代码、创建变量和函数以及访问该类父类中声明的其他变量和函数的地方。\n\n如果您选择`Event Graph`选项卡，您应该可以在`Viewport`选项卡的右侧看到该选项卡，您将看到`Event Graph`窗口，而不是`Viewport`窗口。点击`Event Graph`选项卡，会出现如下窗口:\n\n![Figure 1.21: The Event Graph window, showing three disabled events ](img/B16183_01_21.jpg)\n\n图 1.21:显示三个禁用事件的事件图窗口\n\n您可以通过按住*鼠标右键*并在图形内拖动来导航`Event Graph`，您可以通过滚动*鼠标滚轮*来放大和缩小，您可以通过单击*鼠标左键*或按住选择节点区域来从图形中选择节点。\n\n你也可以在`Event Graph`窗口内*右键*进入蓝图的动作菜单，可以在`Event Graph`中进行动作，包括获取和设置变量、调用函数或事件等。\n\n蓝图中脚本的工作方式是使用引脚连接节点。有几种类型的节点，如变量、函数和事件。您可以通过引脚连接这些节点，引脚有两种类型:\n\n1.  **Execution pins**: These will dictate the order in which the nodes will be executed. If you want node 1 to be executed and then node 2 to be executed, you link the output execution pin of node 1 to the input execution pin of node 2, as shown in the following screenshot:\n\n    ![Figure 1.22: The output execution pin of the Event OnReset node being connected  to the input execution pin of the setter node for MyVar ](img/B16183_01_22.jpg)\n\n    图 1.22:重置节点上事件的输出执行引脚连接到我的变量的设置节点的输入执行引脚\n\n2.  **Variable pins**: These work as parameters (also known as input pins), at the left of the node, and return values (also known as output pins), at the right side of the node, representing a value of a certain type (integer, float, Boolean, and others):\n\n    ![Figure 1.23: The Get Scalar Parameter Value function call node, which has two input variable pins and one output variable pin ](img/B16183_01_23.jpg)\n\n图 1.23:获取标量参数值函数调用节点，它有两个输入变量引脚和一个输出变量引脚\n\n让我们通过下一个练习更好地理解这一点。\n\n## 练习 1.04:创建蓝图变量\n\n在本练习中，我们将看到如何通过创建`Boolean`类型的新变量来创建蓝图变量。\n\n在蓝图中，变量的工作方式类似于 C++ 中使用的变量。你可以创造它们，获得它们的价值，并设定它们。\n\n以下步骤将帮助您完成本练习:\n\n1.  To create a new Blueprint variable, head to the `My Blueprint` window and click the `+ Variable` button:\n\n    ![Figure 1.24: The + Variable button being highlighted in the My Blueprint window, which allows you to create a new Blueprint variable ](img/B16183_01_24.jpg)\n\n    图 1.24:在“我的蓝图”窗口中突出显示的+变量按钮，允许您创建一个新的蓝图变量\n\n2.  After that, you'll automatically be allowed to name your new variable. Name this new variable `MyVar`:\n\n    ![Figure 1.25: Naming the new variable MyVar ](img/B16183_01_25.jpg)\n\n    图 1.25:命名新变量 MyVar\n\n3.  Compile your Blueprint by clicking the `Compile` button on the left side of the `Toolbar` window. If you now take a look at the `Details` window, you should see the following:\n\n    ![Figure 1.26: The MyVar variable settings in the Details window ](img/B16183_01_26.jpg)\n\n    图 1.26:详细信息窗口中的变量设置\n\n4.  Here, you'll be able to edit all the settings related to this variable, the most important ones being `Variable Name`, `Variable Type`, and its `Default Value` at the end of the settings. Boolean variables can have their value changed by clicking the gray box to their right:\n\n    ![Figure 1.27: The variable types available from the Variable Type drop-down menu ](img/B16183_01_27.jpg)\n\n    图 1.27:变量类型下拉菜单中可用的变量类型\n\n5.  You can also drag a getter or setter for a variable inside the `My Blueprint` tab into the `Event Graph` window:\n\n    ![Figure 1.28: Dragging the MyVar into the Event Graph window and choosing  whether to add a getter or setter ](img/B16183_01_28.jpg)\n\n    图 1.28:将 MyVar 拖到事件图窗口中，并选择是添加一个 getter 还是 setter\n\n    Getters 是包含变量当前值的节点，setters 是允许您更改变量值的节点。\n\n6.  To allow a variable to be editable in each of the instances of this Blueprint class, you can click the eye icon to the right of that variable inside the `My Blueprint` window:\n\n    ![Figure 1.29: Clicking the eye icon to expose a variable and allow it to be instance-editable ](img/B16183_01_29.jpg)\n\n    图 1.29:点击眼睛图标来展示一个变量，并允许它是实例可编辑的\n\n7.  You can then drag an instance of this class to your level, select that instance, and see the option to change that variable's value in the `Details` window of the editor:\n\n    ![Figure 1.30: The exposed MyVar variable that can be edited through the  Details panel of that object ](img/B16183_01_30.jpg)\n\n图 1.30:公开的 MyVar 变量，可以通过该对象的细节面板进行编辑\n\n至此，我们结束了本练习，现在知道如何创建我们自己的蓝图变量。现在让我们看看如何在下一个练习中创建蓝图函数。\n\n## 练习 1.05:创建蓝图功能\n\n在本练习中，我们将创建第一个蓝图函数。在蓝图中，函数和事件相对相似，唯一的区别是事件只有一个输出引脚，通常是因为它是从蓝图类外部调用的:\n\n![Figure 1.31: An event (left), a pure function call that doesn't need execution pins (middle), and a normal function call (right) ](img/B16183_01_31.jpg)\n\n图 1.31:一个事件(左)、一个不需要执行引脚的纯函数调用(中间)和一个普通函数调用(右)\n\n以下步骤将帮助您完成本练习:\n\n1.  Click the `+ Function` button inside the `My Blueprint` window:\n\n    ![Figure 1.32: The + Function button being hovered over, which will create a new function ](img/B16183_01_32.jpg)\n\n    图 1.32:悬停在+ Function 按钮上，这将创建一个新的函数\n\n2.  命名新功能`MyFunc`。\n3.  Compile your Blueprint by clicking the `Compile` button in the `Toolbar` window:\n\n    ![Figure 1.33: The Compile button ](img/B16183_01_33.jpg)\n\n    图 1.33:编译按钮\n\n4.  If you now take a look at the `Details` window, you should see the following:\n\n    ![Figure 1.34: The Details panel after selecting the MyFunc function and adding  an input and output pin ](img/B16183_01_34.jpg)\n\n    图 1.34:选择 MyFunc 函数并添加输入和输出引脚后的详细信息面板\n\n    在这里，您可以编辑与该功能相关的所有设置，最重要的是设置末尾的`Inputs`和`Outputs`。这将允许您指定该函数必须接收并将返回的变量。\n\n    最后，您可以从`My Blueprint`窗口通过*点击*来编辑该功能。这将在中央窗口中打开一个新的选项卡，允许您指定该功能的作用。在这种情况下，这个函数每次被调用时都会简单地返回`false`:\n\n    ![Figure 1.35: The contents of the MyFunc function, receiving a Boolean parameter,  and returning a Boolean type ](img/B16183_01_35.jpg)\n\n    图 1.35:my func 函数的内容，接收一个布尔参数，并返回一个布尔类型\n\n5.  要保存我们对此蓝图类所做的修改，请单击工具栏上`Compile`按钮旁边的`Save`按钮。或者，您可以选择该选项，以便每次成功编译时蓝图都会自动保存。\n\n完成这些步骤后，您现在知道如何创建自己的蓝图函数了。现在让我们看一下我们将在本章后面使用的蓝图节点。\n\n# 浮点乘法节点\n\n蓝图包含更多与变量或函数无关的节点。一个这样的例子是算术节点(即加法、减法、乘法等等。).如果你在蓝图动作菜单上搜索`float * float`，你会发现*浮点乘法*节点:\n\n![Figure 1.36: The Float Multiplication node  ](img/B16183_01_36.jpg)\n\n图 1.36:浮点乘法节点\n\n该节点允许您输入两个或多个浮点参数(您可以通过单击`Add pin`文本右侧的`+`图标添加更多参数)，并输出所有参数的乘法结果。我们将在本章的活动中使用这个节点。\n\n# 开始播放并打勾\n\n现在让我们来看看 UE4 中最重要的两个事件:`BeginPlay`和`Tick`。\n\n如前所述，事件通常从蓝图类外部调用。在`BeginPlay`事件的情况下，当这个蓝图类的一个实例被放置在关卡中并且开始玩关卡时，或者当这个蓝图类的一个实例在玩游戏时被动态产生时，这个事件被调用。您可以将`BeginPlay`事件视为将在该蓝图的实例上调用的第一个事件，您可以将其用于初始化。\n\nUE4 要知道的另一个重要事件是`Tick`事件。您可能知道，游戏以一定的帧速率运行，最常见的是 30 FPS(每秒帧数)或 60 FPS:这意味着游戏每秒将渲染游戏的更新图像 30 或 60 次。`Tick`事件将在游戏每次这样做时被调用，这意味着如果游戏以 30 FPS 运行，`Tick`事件将每秒被调用 30 次。\n\n转到您的蓝图类的`Event Graph`窗口，通过选择所有事件并单击`Delete`键删除三个灰色事件，这将导致`Event Graph`窗口变空。之后，*在`Event Graph` 窗口中右键单击*，输入`BeginPlay`，通过单击`Enter`键或在蓝图操作菜单中单击该选项选择`Event BeginPlay`节点。这将导致该事件被添加到`Event Graph`窗口:\n\n![Figure 1.37: The BeginPlay event being added to the Event Graph window through the Blueprint Actions menu  ](img/B16183_01_37.jpg)\n\n图 1.37:开始播放事件通过蓝图操作菜单被添加到事件图窗口\n\n*右键点击`Event Graph`窗口内的*，输入`Tick`，选择`Event Tick`节点。这将导致该事件被添加到`Event Graph`窗口:\n\n![Figure 1.38: The Tick event ](img/B16183_01_38.jpg)\n\n图 1.38:滴答事件\n\n与`BeginPlay`事件不同，`Tick`事件将使用参数`DeltaTime`调用。此参数是一个浮点数，指示自渲染最后一帧以来经过的时间。如果您的游戏以 30 FPS 运行，这意味着渲染的每个帧之间的时间间隔(增量时间)平均为 1/30 秒，约为 0.033 秒(33.33 毫秒)。如果帧 1 被渲染，然后帧 2 被渲染 0.2 秒，那么帧 2 的增量时间将是 0.2 秒。如果帧 3 在帧 2 之后 0.1 秒被渲染，则帧 3 的增量时间将是 0.1 秒，以此类推。\n\n但是`DeltaTime`参数为什么这么重要呢？让我们看看下面的场景:您有一个蓝图类，每当使用`Tick`事件渲染一帧时，它在 *Z* 轴上的位置都会增加 1 个单位。然而，你面临一个问题:玩家有可能以不同的帧率运行你的游戏，比如 30 FPS 和 60 FPS。以 60 FPS 运行游戏的玩家将导致`Tick`事件被调用的次数是以 30 FPS 运行游戏的玩家的两倍，蓝图类将因此而以两倍的速度移动。这就是增量时间发挥作用的地方:因为以 60 FPS 运行的游戏将以较低的增量时间值调用`Tick`事件(渲染的帧之间的间隔要小得多)，所以您可以使用该值来更改 *Z* 轴上的位置。虽然在 60 FPS 的游戏中，`Tick`事件被调用的次数是两倍，但是它的增量时间是这个值的一半，所以它是平衡的。这将导致两个以不同帧速率玩游戏的玩家得到相同的结果。\n\n注意\n\n如果您想要一个使用增量时间移动的蓝图，您可以通过将增量时间乘以您想要它每秒移动的单位数来使它移动得更快或更慢(例如，如果您想要一个蓝图在 *Z* 轴上每秒移动 3 个单位，您可以告诉它每帧移动`3 * DeltaTime`个单位)。\n\n现在让我们尝试另一个练习，它将包括使用蓝图节点和大头针。\n\n## 练习 1.06:在 Z 轴上偏移测试器类\n\n在本练习中，当游戏开始时，您将使用`BeginPlay`事件来偏移(移动) *Z* 轴上的`TestActor`。\n\n以下步骤将帮助您完成本练习:\n\n1.  打开`TestActor`蓝图类。\n2.  使用`Blueprint Actions`菜单，将`Event BeginPlay`节点添加到图形中，如果它还没有出现的话。\n3.  添加`AddActorWorldOffset`功能，并将`BeginPlay`事件的输出执行引脚连接到该功能的输入执行引脚。该功能负责在预定轴( *X* 、 *Y* 和 *Z* )中移动演员，并接收以下参数:\n    *   `Target`:应该调用该函数的 Actor，将是调用该函数的 Actor。默认行为是在调用该函数的 Actor 上调用该函数，这正是我们想要的，并使用`self`属性显示。\n    *   `DeltaLocation`:我们希望在三个轴(X、Y 和 z)的每一个轴上抵消该演员的量\n    *   We won't be getting into the other two parameters, `Sweep` and `Teleport`, so you can leave them as is. They are both Boolean types and should be left as `false`:\n\n        ![Figure 1.39: The BeginPlay event calling the AddActorWorldOffset function ](img/B16183_01_39.jpg)\n\n图 1.39:调用 AddActorWorldOffset 函数的 BeginPlay 事件\n\n1.  Split the `Delta Location` input pin, which will cause this `Vector` property to be split into three float properties. You can do this to any variable type that is comprised of one or more subtypes (you wouldn't be able to do this to the float type because it's not comprised of any variable subtypes) by *right-clicking* on them and selecting `Split Struct Pin`:\n\n    ![Figure 1.40: The Delta Location parameter being split from a vector into three floats ](img/B16183_01_40.jpg)\n\n    图 1.40:增量位置参数被从一个向量分割成三个浮点数\n\n2.  用鼠标左键点击*，输入数字，然后按*回车*键，将`Delta Location`的`Z`属性设置为`100`单位。这将导致我们的`TestActor`在游戏开始时在 *Z* 轴上向上移动`100`个单位。*\n**   Add a cube shape to your `TestActor`, using the `Components` window, so that we can see our Actor. You can do this by clicking the `+ Add Component` button, typing `Cube`, and then selecting the first option under the `Basic Shapes` section:\n\n    ![Figure 1.41: Adding a cube shape ](img/B16183_01_41.jpg)\n\n    图 1.41:添加立方体形状\n\n    *   点击`Compile`按钮，编译并保存你的蓝图类。*   Go back to the level's `Viewport` window and place an instance of your `TestActor` Blueprint class inside the level, if you haven't done so already:\n\n    ![Figure 1.42: Adding an instance of TestActor to the level ](img/B16183_01_42.jpg)\n\n    图 1.42:向级别添加一个测试器实例\n\n    *   When you play the level, you should notice that the `TestActor` we added to the level is in a more elevated position:\n\n    ![Figure 1.43: The TestActor increasing its position on the Z axis when the game starts ](img/B16183_01_43.jpg)\n\n    图 1.43:游戏开始时，测试器在 Z 轴上增加位置\n\n    *   进行这些修改后，通过按下 *Ctrl + S* 或单击编辑器`Toolbar`上的`Save Current`按钮，保存对我们级别所做的更改。*\n\n *在本练习中，您已经学习了如何使用自己的蓝图脚本逻辑创建第一个 Actor 蓝图类。\n\n注意\n\n本次演练最终结果的`TestActor`蓝图资产和`Map`资产均可在此找到:[https://packt.live/3lfYOa9](https://packt.live/3lfYOa9)。\n\n现在我们已经完成了这一步，让我们进一步了解一下`ThirdPersonCharacter`蓝图课程。\n\n# 第三个人物蓝图类\n\n我们来看看`ThirdPersonCharacter`蓝图类，它是代表玩家控制的角色的蓝图，再来看看它包含的 Actor Components。\n\n转到`Content Browser`内的`ThirdPersonCPP -> Blueprints`目录，打开`ThirdPersonCharacter`资产:\n\n![Figure 1.44: The ThirdPersonCharacter Blueprint class  ](img/B16183_01_44.jpg)\n\n图 1.44:第三个人角色蓝图类\n\n在上一节中，我们介绍了蓝图编辑器中的`Components`窗口，我们提到了**演员组件**。\n\n参与者组件是必须存在于参与者内部的实体，允许您将参与者的逻辑扩展到几个不同的参与者组件中。在这个蓝图中，我们可以看到有四个可视化的参与者组件:\n\n*   骨骼网格组件，显示 UE4 人体模型\n*   相机组件，显示玩家可以从哪里看到游戏\n*   一个箭头组件，它允许我们看到角色面对的地方(这主要用于开发目的，而不是在玩游戏的时候)\n*   胶囊组件，指定该角色的碰撞范围\n\n如果您查看`Components`窗口，您将看到比我们在`Viewport`窗口中看到的演员组件更多的演员组件。这是因为一些 Actor 组件没有可视化表示，纯粹由 C++ 或蓝图代码组成。我们将在下一章和*第九章*、*视听元素*中深入探讨演员组件。\n\n如果你看一下这个蓝图类的`Event Graph`窗口，你会发现它本质上是空的，就像我们用我们的`TestActor`蓝图类看到的一样，尽管它有一些相关的逻辑。这是因为该逻辑是在 C++ 类中定义的，而不是在这个蓝图类中。我们将在下一章研究如何做到这一点。\n\n为了解释这个蓝图类的骨骼网格组件，我们应该首先谈论网格和材质。\n\n# 网格和材料\n\n对于计算机来说，要在视觉上表示一个三维物体，它需要两样东西:一个三维网格和一种材料。\n\n## 网格\n\n三维网格允许您指定对象的大小和形状，就像这个代表猴子头的网格:\n\n![Figure 1.45: A 3D mesh of a monkey's head ](img/B16183_01_45.jpg)\n\n图 1.45:猴子头部的三维网格\n\n网格由几个顶点、边和面组成。顶点只是一个三维坐标，具有 *X* 、 *Y、*和 *Z* 位置；边是两个顶点之间的连接(即一条线)；面是三个或更多个边的连接。在上图中，您可以看到网格的各个顶点、边和面，其中每个面的颜色介于白色和黑色之间，具体取决于面反射的光量。如今，视频游戏可以渲染成千上万个顶点的网格，以至于你无法区分单个顶点，因为有太多的顶点靠得很近。\n\n## 材料\n\n另一方面，材质允许您指定如何表示网格。它们允许您指定网格的颜色，在其表面绘制纹理，甚至操纵其单个顶点。\n\n在撰写本书时，创建网格在 UE4 中还没有得到适当的支持，应该在另一个软件中完成，例如 Blender 或 Autodesk Maya，因此我们在这里不会详细讨论这个问题。但是，我们将学习如何为现有网格创建材质。\n\n在 UE4 中，您可以通过从 Actor Component 类继承而来的网格组件来添加网格。有几种类型的网格组件，但最重要的两种是静态网格组件，用于没有动画的网格(例如，立方体、静态级别几何图形)，以及骨骼网格组件，用于有动画的网格(例如，播放运动动画的角色网格)。正如我们之前看到的，`ThirdPersonCharacter`蓝图类包含一个骨骼网格组件，因为它用于表示播放运动动画的角色网格。在下一章中，我们将学习如何将网格等资产导入到我们的 UE4 项目中。\n\n现在让我们在下一个练习中看看 UE4 中的材料。\n\n# 操纵 UE4 中的材料\n\n在本节中，我们将了解材料在 UE4 中是如何工作的。\n\n回到你的`Level Viewport`窗口，选择这个`Cube`对象:\n\n![Figure 1.46: The Cube object, next to the text saying Third Person on the floor ](img/B16183_01_46.jpg)\n\n图 1.46:立方体对象，在文本旁边写着第三人称在地板上\n\n看看`Details`窗口，在这里你可以看到与这个对象的`Static Mesh`组件相关的网格和材质:\n\n![Figure 1.47: The Static Mesh and Materials (Element 0) properties of the Cube  object's Static Mesh component ](img/B16183_01_47.jpg)\n\n图 1.47:立方体对象的静态网格组件的静态网格和材质(元素 0)属性\n\n注意\n\n请记住，网格可以有多种材质，但必须至少有一种。\n\n单击`Content Browser`中要带到该材料位置的`Material`属性旁边的*视镜*图标。该图标适用于编辑器中对任何资源的引用，因此您可以对被引用为立方体对象的`Static Mesh`的资源执行相同的操作:\n\n![Figure 1.48: The looking glass icon (left), which takes you to that  asset's location in Content Browser (right) ](img/B16183_01_48.jpg)\n\n图 1.48:视镜图标(左)，它将您带到内容浏览器中该资产的位置(右)\n\n*用鼠标左键双击*该资产，在`Material`编辑器中打开该资产。让我们打破`Material editor`中的窗户:\n\n![Figure 1.49: The Material editor window broken down into five parts ](img/B16183_01_49.jpg)\n\n图 1.49:材料编辑器窗口分为五个部分\n\n1.  `Graph`:编辑器中的前方和中央，有`Graph`窗口。类似于蓝图编辑器的`Event Graph`窗口，`Material`编辑器的图形也是基于节点的，在这里你也可以找到通过管脚连接的节点，虽然这里你找不到执行管脚，只有输入和输出管脚。\n2.  `Palette`:在屏幕的右边，你会看到`Palette`窗口，在这里你可以搜索所有可以添加到`Graph`窗口的节点。您也可以在蓝图编辑器的`Event Graph`窗口中通过右键单击`Graph`窗口中的*并键入您想要添加的节点来完成此操作。*\n**   `Viewport`:在屏幕左上角，你会看到`Viewport`窗口。在这里，您将能够预览材质的结果，以及它将如何出现在一些基本形状上，如球体、立方体和平面。*   `Details`:在屏幕的左下角，你会看到`Details`窗口，类似于蓝图编辑器，你可以在`Graph`窗口看到这个`Material`资产或者当前选中节点的详细信息。*   `Toolbar`:在屏幕的上边缘，你会看到`Toolbar`窗口，在这里你可以应用和保存对你的素材所做的更改，以及执行与`Graph`窗口相关的几个动作。*\n\n *在 UE4 中的每个材质编辑器中，您会发现一个名为`Material`资源的节点，通过将该节点的引脚插入其他节点，您可以指定与其相关的几个参数。\n\n在这种情况下，您可以看到有一个名为`0.7`的节点被插入到`Roughness`引脚。这个节点是一个`Constant`节点，它允许你指定一个与之相关的号码——在这个例子中是`0.7`。您可以创建单个数字的常量节点、2 个向量(例如，`(1, 0.5)`)、3 个向量(例如，`(1, 0.5, 4)`)和 4 个向量(例如，`(1,0.5, 4, 0)`)。要创建这些节点，您可以在按住`1`、`2`、`3`或`4`数字键的同时，用鼠标左键*按下`Graph`窗口。*\n\n材料有几个输入参数，所以让我们来看一些最重要的参数:\n\n*   `BaseColor`:这个参数简单来说就是材质的颜色。通常，常数或纹理样本用于连接到此引脚，以使对象具有特定颜色或映射到特定纹理。\n*   `Metallic`:这个参数将决定你的物体看起来有多像金属表面。您可以通过连接一个范围从 0(非金属性)到 1(非常金属性)的常数单个数字节点来实现这一点。\n*   `Specular`:这个参数将决定你的物体会反射多少光。您可以通过连接一个范围从 0(不反射任何光)到 1(反射所有光)的常数单个数字节点来实现这一点。如果你的物体已经很有金属质感，你会发现很少或没有区别。\n*   `Roughness`: This parameter will dictate how much the light that your object reflects will be scattered (the more the light scatters, the less clear this object will reflect what's around it). You can do this by connecting a constant single number node that ranges from 0 (the object essentially becomes a mirror) to 1 (the reflection on this object is blurry and unclear).\n\n    注意\n\n    要了解更多类似上述输入的信息，请访问。\n\nUE4 还允许您将图像(`.jpeg`、`.png`)作为`Texture`资产导入，然后可以使用`Texture Sample`节点在材质中引用这些图像:\n\n![Figure 1.50: The Texture Sample node, which allows you to specify a texture and use  it or its individual color channels as pins ](img/B16183_01_50.jpg)\n\n图 1.50:纹理样本节点，它允许你指定一个纹理，并使用它或它的单个颜色通道作为管脚\n\n注意\n\n在下一章中，我们将了解如何将文件导入 UE4。\n\n为了创建一个新的`Material`资产，你可以通过在`Content Browser`内你想要创建新资产的目录上*右键*来创建，这将允许你选择创建哪个资产，然后选择`Material`。\n\n现在你知道如何在 UE4 中创建和操纵材质了。\n\n现在让我们跳到本章的活动，这将是本书的第一个活动。\n\n## 活动 1.01:在 Z 轴上无限推进测试器\n\n在本活动中，您将使用`TestActor`的`Tick`事件在 *Z* 轴上无限移动它，而不是在游戏开始时只做一次。\n\n以下步骤将帮助您完成本活动:\n\n1.  打开`TestActor`蓝图类。\n2.  将`Event Tick`节点添加到蓝图的`Event Graph`窗口。\n3.  添加`AddActorWorldOffset`功能，拆分其`DeltaLocation`引脚，并将`Tick`事件的输出执行引脚连接到该功能的输入执行引脚，类似于我们在*练习 1.01* 、*创建虚幻引擎 4 项目*中所做的。\n4.  向`Event Graph`窗口添加*浮点乘法*节点。\n5.  将`Tick`事件的`Delta Seconds`输出引脚连接到*浮点乘法*节点的第一个输入引脚。\n6.  创建一个`float`类型的新变量，称之为`VerticalSpeed,`，并将其默认值设置为`25`。\n7.  向`Event Graph`窗口的`VerticalSpeed`变量添加一个 getter，并将其引脚连接到*浮点乘法*节点的第二个输入引脚。之后，将*浮点乘法*节点的输出引脚连接到`AddActorWorldOffset`功能的`Delta Location Z`引脚。\n8.  删除`BeginPlay`事件和与之相关的`AddActorWorldOffset`功能，这两个都是我们在*练习 1.01* 、*创建虚幻引擎 4 项目*中创建的。\n9.  Play the level and notice our `TestActor` rising from the ground and up into the air over time:\n\n    ![Figure 1.51: The TestActor propelling itself vertically ](img/B16183_01_51.jpg)\n\n图 1.51:测试器垂直推进\n\n完成这些步骤后，我们就结束了这个活动——这是本书的第一个活动。我们现在已经整合了在蓝图编辑器的`Event Graph`窗口中添加和移除节点，并使用`Tick`事件及其`DeltaSeconds`属性来创建游戏逻辑，以保持不同帧速率下的一致性。\n\n注意\n\n这个活动的解决方案可以在:[https://packt.live/338jEBx](https://packt.live/338jEBx)找到。\n\n`TestActor`蓝图资产可以在这里找到:[https://packt.live/2U8pAVZ](https://packt.live/2U8pAVZ)。\n\n# 总结\n\n通过完成这一章，您已经通过学习虚幻引擎 4 迈出了游戏开发旅程的第一步。现在，您已经知道如何在虚幻引擎编辑器中导航，如何操作关卡中的角色，如何创建自己的角色，如何使用蓝图脚本语言，以及如何在虚幻引擎 4 中表示 3D 对象。\n\n希望你能意识到前方有一个充满可能性的世界，天空是你使用这个游戏开发工具所能创造的东西的极限。\n\n在下一章中，您将重新创建本章中从头开始自动生成的项目模板。您将学习如何创建自己的 C++ 类，然后创建可以操作其父类中声明的属性的蓝图类，以及如何将角色网格和动画导入虚幻引擎 4，并熟悉其他动画相关资产，如*动画蓝图*。******"
  },
  {
    "path": "docs/game-dev-proj-ue/02.md",
    "content": "# 二、使用虚幻引擎\n\n概述\n\n本章将重点介绍虚幻引擎中的许多基本概念和特性。将向您展示如何创建 C++ 项目，如何执行一些基本调试，以及如何处理特定于角色的动画。\n\n到本章结束时，您将能够创建 C++ 模板项目，能够在 Visual Studio 中调试代码，了解文件夹结构和涉及的最佳实践，最后，能够根据角色的状态设置角色动画。\n\n# 简介\n\n在前一章中，我们学习了史诗游戏启动器的基础知识，以及虚幻编辑器的基础知识。除了探索第一人称模板之外，我们还了解了如何使用对象以及基本的蓝图。在本章中，我们将通过探索第三人称模板以及使用输入和动画来建立这些基础。\n\n游戏开发可以用各种各样的语言完成，比如 C、C++、Java、C#，甚至 Python。虽然每种语言都有优点和缺点，但我们将在本书中使用 C++ 作为虚幻引擎中使用的主要编程语言。\n\n在本章中，我们将让您了解如何在 UE4 中创建 C++ 项目和基本级别调试。能够调试代码非常重要，因为它有助于开发人员处理 bug。提供的工具非常方便，对任何虚幻引擎开发人员来说都是必不可少的。\n\n接下来，我们将近距离接触在虚幻引擎中创建游戏和体验的核心课程。您将探索游戏模式和相关的课堂概念，然后进行练习，以获得对这一点的实际理解。\n\n本章的最后一节是关于动画的。几乎每一个游戏都有动画，有些是非常基本的，但有些是非常高级的，包括迷人的细节，这些是游戏体验的关键。虚幻引擎提供了几个工具，你可以用来创建和处理动画，包括动画蓝图，它有复杂的图形和一个状态机。\n\n# 创建和设置空白 C++ 项目\n\n在每个项目开始时，您可能希望从 Epic 提供的任何模板(包含准备执行的基本代码)开始，并在此基础上进行构建。大多数/某些时候，你可能需要建立一个空白或空的项目，你可以按照你的要求来塑造和雕刻。我们将在下面的练习中学习如何做到这一点。\n\n## 练习 2.01:创建一个空的 C++ 项目\n\n在本练习中，您将学习如何从 Epic 提供的模板创建一个空的 C++ 项目。这将成为您未来许多 C++ 项目的基础。\n\n以下步骤将帮助您完成本练习:\n\n1.  从史诗游戏启动器中启动虚幻引擎 4.24。\n2.  点击`Games`部分，点击`Next`。\n3.  确定选择了`Blank`项目模板，点击`Next`。\n4.  Click the `Blueprint` section dropdown and select `C++ `.\n\n    注意\n\n    确保项目文件夹和项目名称分别用适当的目录和名称指定。\n\n    一切设置完毕后，点击`Create Project`按钮。在这种情况下，我们的项目目录位于名为`UnrealProjects`的文件夹中，该文件夹位于`E`驱动器中。项目名称设置为`MyBlankProj`(建议您使用这些名称和项目目录，但如果您愿意，也可以使用自己的名称)。\n\n    注意\n\n    项目名称中不能有任何空格。最好让虚幻目录尽可能靠近驱动器的根目录(以避免在创建资产或将资产导入项目的工作目录时遇到 256 个字符的路径限制等问题；对于小项目来说，这可能没问题，但是对于更大规模的项目来说，其中的文件夹层次结构可能会变得过于复杂，这一步很重要)。\n\n    您会注意到，在完成生成代码和创建项目文件之后，该项目及其 Visual Studio 解决方案(`.sln`)文件将被打开。\n\n    注意\n\n    确保将 Visual Studio 解决方案配置设置为“开发编辑器”，并将解决方案平台设置为 Win64 以进行桌面开发:\n\n    ![Figure 2.1: Visual Studio deployment settings ](img/B16183_02_01.jpg)\n\n图 2.1: Visual Studio 部署设置\n\n通过完成这个练习，我们现在知道了如何在 UE4 上创建一个空的 C++ 项目，以及它的注意事项。\n\n在下一节中，我们将讨论一下文件夹结构，以及虚幻开发者使用的最基本和最常用的文件夹结构格式。\n\n# 虚幻引擎中的内容文件夹结构\n\n在你的项目目录中(`E:/UnrealProjects/MyBlankProj` *，在我们的例子中是*)，你会看到一个`Content`文件夹。这是项目用于不同类型资产和项目相关数据(包括蓝图)的主要文件夹。C++ 代码进入项目的`Source`文件夹。请注意，最佳做法是直接通过虚幻编辑器创建新的 C++ 代码文件，因为这样可以简化过程，减少错误。\n\n有许多不同的策略可以用来组织`Content`文件夹中的数据。最基本和容易理解的是使用文件夹名称来描述里面的内容类型。因此，`Content`文件夹目录结构可能类似于[https://packt.live/3lCVFkR](https://packt.live/3lCVFkR)的示例。在本例中，您可以看到每个文件都被明确地放在第一级代表其类型的文件夹的名称下，随后的级别进一步将其分组到有意义的文件夹中。\n\n注意\n\n所有蓝图都应该在其名称前加前缀`BP`(以区别于虚幻引擎使用的默认蓝图)。其余的前缀是可选的(但是，最好用前面显示的前缀来格式化它们)。\n\n在下一节中，我们将研究 Visual Studio 解决方案。\n\n# 使用 Visual Studio 解决方案\n\n虚幻引擎中的每个 C++ 项目都有一个 Visual Studio 解决方案。这反过来驱动所有代码，并为开发人员提供在运行状态下设置执行逻辑和调试代码的能力。\n\n### 解决方案分析\n\nVisual Studio 解决方案(。`sln`)在项目目录中生成的文件包含整个项目以及添加到其中的任何相关代码。\n\n让我们看看 Visual Studio 中存在的文件。*双击*的。`sln`文件，在 Visual Studio 内打开。\n\n在`Solution Explorer`中，你会看到两个名为`Engine`和`Games`的项目。\n\n### 发动机项目\n\n在基础级别，虚幻引擎本身是一个 Visual Studio 项目，并且有自己的解决方案文件。这包含了在虚幻引擎中协同工作的所有代码和第三方集成。这个项目中的所有代码都称为“源代码”。\n\n引擎项目由外部依赖项、配置、插件、着色器和当前用于该项目的虚幻引擎源代码组成。您可以随时浏览`UE4 -> Source`文件夹查看任何引擎代码。\n\n注意\n\n由于虚幻引擎是开源的，Epic 允许开发人员查看和编辑源代码以满足他们的需求。但是，您不能编辑通过史诗游戏启动器安装的虚幻引擎版本的源代码。为了能够在源代码中进行和构建更改，您需要下载虚幻引擎的源版本，该版本可以通过 GitHub 找到。您可以使用以下指南下载虚幻引擎的源版本:[https://docs . unrealengine . com/en-US/GettingStarted/downloadinunrealgene/index . html](https://docs.unrealengine.com/en-US/GettingStarted/DownloadingUnrealEngine/index.html)\n\n下载后，还可以参考以下指南编译/构建新下载的引擎:[https://docs . unrealengine . com/en-US/Programming/Development/building unrealengine/index . html](https://docs.unrealengine.com/en-US/Programming/Development/BuildingUnrealEngine/index.html)\n\n### 游戏项目\n\n在`Games`目录下是您项目名称的解决方案文件夹。展开后，您会发现一组文件夹。您将关注以下内容:\n\n*   **配置文件夹**:携带为项目和构建设置的所有配置(这些配置也可以选择具有特定于平台的设置(如视窗、安卓、iOS、Xbox 或 PS))。\n*   **插件文件夹**:这是一个可选的文件夹，在添加任何第三方插件时创建(从 Epic Marketplace 下载或通过互联网获得)。这个文件夹将包含与这个项目相关的插件的所有源代码。\n*   **源文件夹**:这是我们将要使用的主文件夹。它将包含构建目标文件，以及项目的所有源代码。以下是源文件夹中默认文件的描述:\n*   **目标和构建文件**:这些(如下图截图所示)包含指定虚幻构建工具(*构建你的游戏的程序*)的代码，你将使用它来构建你的游戏。它包含任何需要添加到游戏中的额外模块，以及其他与构建相关的设置。默认情况下，有两个目标文件(一个用于虚幻编辑器，另一个用于由它们的名称描述的构建)，它们以`.Target.cs`扩展名结尾，还有一个构建文件以`Build.cs`结尾。\n*   **项目名称代码文件(。cpp &。h)** :默认情况下，这些文件是为每个项目创建的，包含用于运行默认游戏模块代码的代码。\n*   **项目名称游戏模式库代码文件(。cpp &。h)** :默认创建一个空的项目游戏模式库。大多数情况下不常用。\n*   **ProjectName.uproject 文件**:包含用于提供项目基本信息的描述符以及与之相关的插件列表。\n\n## D 在 Visual Studio 中沸腾代码\n\nVisual Studio 借助代码中的断点提供了强大的调试功能。它使用户能够在特定的代码行暂停游戏，这样开发人员就可以看到变量的当前值，并以受控的方式遍历代码和游戏(可以逐行进行，逐函数进行，等等)。\n\n当您的游戏项目中有很多变量和代码文件，并且您希望看到变量的值以逐步的方式被更新和使用来调试代码、找出存在的问题并解决它们时，这是非常有用的。调试是任何开发人员工作的一个基本过程，只有经过多次连续的调试、分析和优化周期，项目才能得到足够的完善以进行部署。\n\n现在，您已经了解了 Visual Studio 解决方案的基本概念，接下来我们将介绍一个关于它的实际练习。\n\n## E 练习 2.02:调试第三人称模板代码\n\n在本练习中，您将使用虚幻引擎的第三人称模板创建一个项目，并将在 Visual Studio 中调试代码。我们将研究这个模板项目的`Character`类中一个名为`BaseTurnRate`的变量的值。当我们一行行地遍历代码时，我们将看到值是如何更新的。\n\n以下步骤将帮助您完成本练习:\n\n1.  从史诗游戏启动器启动虚幻引擎。\n2.  点击`Games`部分，点击`Next`。\n3.  选择`Third Person`，点击`Next`。\n4.  选择 C++，将项目名称设置为`ThirdPersonDebug`，点击`Create Project`按钮。\n5.  Now, close Unreal Editor, go to the Visual Studio solution, and open the `ThirdPersonDebugCharacter.cpp` file:\n\n    ![Figure 2.2: ThirdPersonDebugCharacter.cpp file location ](img/B16183_02_02.jpg)\n\n    图 2.2:thirdpersondebugcharacter . CPP 文件位置\n\n6.  *Left-click* on the bar on the left-hand side of line `18`. A red dot icon should appear on it (*you can toggle it off by clicking on it again*):\n\n    ![Figure 2.3: Collision capsule init code ](img/B16183_02_03.jpg)\n\n    图 2.3:碰撞舱初始化代码\n\n    这里，我们得到了角色的`capsule`组件(在*第三章*、*角色类组件和蓝图设置*中有进一步的解释)，默认情况下，它是根组件。然后，我们调用它的`InitCapsuleSize`方法，该方法接受两个参数:`InRadius`浮点和`InHalfHeight`浮点。\n\n7.  Make sure the solution configuration setting in VS is set to `Development Editor` and click on the `Local Windows Debugger` button:\n\n    ![Figure 2.4: Visual Studio build settings ](img/B16183_02_04.jpg)\n\n    图 2.4: Visual Studio 构建设置\n\n8.  Wait until you're able to see the following window in the bottom-left corner:\n\n    注意\n\n    如果窗口没有弹出，可以通过打开`Debug` > `Windows` > `Autos`下的`Autos`手动打开窗口。另外，你也可以使用`locals`。\n\n    ![Figure 2.5: Visual Studio variable watch window ](img/B16183_02_05.jpg)\n\n    图 2.5: Visual Studio 可变观察窗口\n\n    `this`显示对象本身。对象包含它存储的变量和方法，通过扩展它，我们能够看到整个对象及其变量在当前代码执行行的状态。\n\n9.  展开`this`，然后`ACharacter`，然后`CapsuleComponent`。在这里，您可以看到`CapsuleHalfHeight = 88.0`和`CapsuleRadius = 34.0`变量的值。在第`18`行旁边，也就是红点最初所在的地方，你会看到一个箭头。这意味着代码在第`17`行的末尾，还没有执行第`18`行。\n10.  Click the `Step Into` button to go to the next line of code (*Shortcut: F11*). `Step Into` will move into code inside the function (if present) on the line. On the other hand, `Step Over` will just execute the current code and move to the next line. Since there is no function on the current line, `Step Into` will mimic the `Step Over` functionality.\n\n    ![Figure 2.6: Debug step into ](img/B16183_02_06.jpg)\n\n    图 2.6:调试步骤\n\n11.  Notice that the arrow has moved to line `21` and that the variables have been updated. `CapsuleHalfHeight = 96.0` and `CapsuleRadius = 42.0` are highlighted in red. Also, notice that the `BaseTurnRate` variable is initialized to `0.0`:\n\n    ![Figure 2.7: BaseTurnRate initial value ](img/B16183_02_07.jpg)\n\n    图 2.7:基本周转率初始值\n\n12.  Step in (*F11*) once again to go to line `22`. Now, the `BaseTurnRate` variable has a value of `45.0` and `BaseLookUpRate` is initialized to `0.0`, as shown in the following screenshot:\n\n    ![Figure 2.8: BaseTurnRate updated value ](img/B16183_02_08.jpg)\n\n    图 2.8:基本周转率更新值\n\n13.  再次进入( *F11* )进入`27`线。现在，`BaseLookUpRate`变量的值为`45.0`。\n\n同样，我们鼓励您介入并调试代码的其他部分，不仅是为了熟悉调试器，也是为了了解代码在幕后是如何工作的。\n\n通过完成本练习，您已经学习了如何在 Visual Studio 中设置调试点，以及如何在某个点停止调试，然后在观察对象及其变量值的同时继续逐行调试。这对于任何开发人员来说都是一个重要的方面，许多人经常使用这个工具来消除代码中令人讨厌的 bug，尤其是当有大量代码流并且变量数量相当大的时候。\n\n注意\n\n在任意点，可以使用顶部菜单栏上的以下按钮停止调试、重新启动调试或继续剩余的代码:\n\n![Figure 2.9: Debugging tools in Visual Studio ](img/B16183_02_09.jpg)\n\n图 2.9:Visual Studio 中的调试工具\n\n现在，我们将考虑将资产导入到虚幻项目中。\n\n# 导入所需资产\n\n虚幻引擎为用户提供了导入各种文件类型的能力，以便用户自定义他们的项目。有几个导入选项可供开发人员调整和使用，以匹配他们所需的设置。\n\n游戏开发人员经常导入的一些常见文件类型是场景的 FBX、网格、动画(从玛雅和其他类似软件导出)、电影文件、图像(主要用于用户界面)、纹理、声音、CSV 文件中的数据和字体。这些文件可以从 Epic Marketplace 或任何其他方式(如互联网)获得，并在项目中使用。\n\n通过将资产拖放到`Content`文件夹中，或者通过点击`Content Browser`中的`Import`按钮，可以导入资产。\n\n现在，让我们进行一个练习，学习如何导入 FBX 文件，并看看这是如何完成的。\n\n## 练习 2.03:导入角色 FBX 文件\n\n本练习将着重于从 FBX 文件导入三维模型。FBX 文件及其材质、动画和纹理被广泛用于导出和导入三维模型。\n\n以下步骤将帮助您完成本练习:\n\n1.  Download the `SK_Mannequin.FBX`, `ThirdPersonIdle.FBX`, `ThirdPersonRun.FBX` and `ThirdPersonWalk.FBX` files from the `Chapter02` -> `Exercise2.03` -> `ExerciseFiles` directory, which can be found on GitHub.\n\n    注意\n\n    `ExerciseFiles`目录可以在 GitHub 上找到，链接如下:[https://packt.live/2IiqTzq](https://packt.live/2IiqTzq)。\n\n2.  打开我们在*练习 2.01* 、*中创建的空白项目，创建一个空的 C++ 项目*。\n3.  In the `Content Browser` interface of the project, click `Import`:\n\n    ![Figure 2.10: Content Browser Import button ](img/B16183_02_10.jpg)\n\n    图 2.10:内容浏览器导入按钮\n\n4.  浏览到我们在*第一步*下载的文件目录，选择`SK_Mannequin.FBX`，点击`Open`按钮。\n5.  确认`Import Animations`按钮为**未勾选**，点击`Import All`按钮。你可能会在这里得到一个警告`There are no smoothing groups`。你可以暂时忽略这个。至此，您已经成功地从 FBX 文件导入了骨架网格。现在，我们需要导入它的动画。\n6.  再次点击`Import`按钮，浏览到我们在*步骤 1* 中创建的文件夹，选择`ThirdPersonIdle.fbx`、`ThirdPersonRun.fbx`和`ThirdPersonWalk.fbx`。然后点击`Open`按钮。\n7.  Make sure the skeleton is set to the one you imported in *Step 5* and click `Import All`:\n\n    ![Figure 2.11: Animation FBX Import Options ](img/B16183_02_11.jpg)\n\n    图 2.11:动画 FBX 导入选项\n\n8.  现在可以看到`Content Browser`里面的三个动画(`ThirdPersonIdle`、`ThirdPersonRun`、`ThirdPersonWalk`)。\n9.  If you *double-click* on `ThirdPersonIdle`, you'll notice that the left arm is hanging down. This means that there's a retargeting issue. When the animations are imported separately from the skeleton, the Unreal Engine internally maps all the bones from the animation to the skeleton but sometimes that results in a glitch. We're now going to resolve this glitch.\n\n    ![Figure 2.12: ThirdPersonIdle UE4 mannequin animation glitch ](img/B16183_02_12.jpg)\n\n    图 2.12:thidppersonidle UE 4 人体模型动画突波\n\n10.  Open the `SK_Mannequin` Skeletal Mesh and open the `Skeleton Tree` tab if not open previously.\n\n    ![Figure 2.13: SK_Mannequin Skeleton Tree tab select ](img/B16183_02_13.jpg)\n\n    图 2.13:选择 SK _ 人体模型骨骼树选项卡\n\n11.  Under `Options` enable the `Show Retargeting Options` checkbox.\n\n    ![Figure 2.14: Enabling retargeting options ](img/B16183_02_14.jpg)\n\n    图 2.14:启用重定目标选项\n\n12.  现在在骨骼树内部，减少`spine_01`、`thigh_l`和`thigh_r`骨骼，以获得更好的可见性。\n13.  现在选择`spine_01`、`thigh_l`和`thigh_r`骨骼。*右键点击*，在菜单中点击`Recursively Set Translation Retargeting Skeleton`按钮。这将解决我们之前遇到的骨骼平移问题。\n14.  Re-open the `ThirdPersonIdle` `Animation` to verify the hanging arm has been fixed.\n\n    ![Figure 2.15: Fixed ThirdPersonIdle Animation ](img/B16183_02_15.jpg)\n\n图 2.15:修复了第三个人空闲动画\n\n注意\n\n您可以通过以下链接在`Chapter02` - > `Exercise2.03` - > `Ex2.03-Completed.rar`目录中找到 GitHub 上完整的练习代码文件:[https://packt.live/2U8AScR](https://packt.live/2U8AScR)\n\n提取`.rar`文件后，*双击*文件。你会看到一个提示询问`Would you like to rebuild now?`。点击那个提示上的`Yes`，这样它就可以构建必要的中间文件，之后它应该会在虚幻编辑器中自动打开项目。\n\n通过完成本练习，您已经了解了如何导入资源，更具体地说，是如何将 FBX 骨骼网格和动画数据导入到项目中。这对许多游戏开发人员的工作流程至关重要，因为资产是整个游戏的构建模块。\n\n在下一节中，我们将研究用于创建游戏的虚幻核心类，它们对于创建游戏或体验有多重要，以及如何在项目中使用它们。\n\n# 虚幻游戏模式类\n\n考虑一种情况，你希望能够暂停你的游戏。能够暂停游戏所需的所有逻辑和实现都将放在一个类中。这个职业将负责处理玩家进入游戏时的游戏流程。游戏流程可以是游戏中发生的任何动作或一组动作。例如，游戏暂停、播放和重启被认为是简单的游戏流程动作。同样，在多人游戏的情况下，我们要求所有与网络相关的游戏逻辑放在一起。这正是游戏模式类的目的。\n\n游戏模式(Game Mode)是一个驱动游戏逻辑并将游戏相关规则强加给玩家的类。它基本上包含了当前正在玩的游戏的信息，包括游戏变量和事件，这将在本章后面提到。游戏模式可以容纳所有游戏对象的管理器，它是一个单独的类，可以被游戏中的任何对象或抽象类直接访问。\n\n与所有其他类一样，游戏模式类可以在蓝图或 C++ 中扩展。这可以包括额外的功能和逻辑，这可能需要让玩家了解游戏中发生的事情。\n\n让我们看一下游戏模式类中的一些示例游戏逻辑:\n\n*   限制允许进入游戏的玩家数量\n*   控制新连接玩家的产卵位置和玩家控制器逻辑\n*   记录游戏分数\n*   跟踪游戏的输赢情况\n*   实施游戏结束/重启游戏场景\n\n在下一节中，我们将看看游戏模式提供的默认类。\n\n### 游戏模式默认职业\n\n除了本身，游戏模式还使用几个类来实现游戏逻辑。它允许您为以下默认值指定类:\n\n*   **游戏会话类**:处理登录审批等管理员级游戏流程。\n*   **游戏状态类**:处理游戏的状态，让客户端可以看到游戏内部发生的事情。\n*   **玩家控制职业**:用来拥有和控制棋子的主职业。可以被认为是决定做什么的大脑。\n*   **玩家状态类**:保存游戏内玩家的当前状态。\n*   **HUD 类**:处理显示给玩家的用户界面。\n*   **默认卒类**:玩家控制的主要演员。这本质上是玩家角色。\n*   **观众类**:作为`DefaultPawn`类的子类，观众棋子类指定负责观看比赛的棋子。\n*   **回放观众玩家控制器**:游戏内负责在回放过程中操控回放的玩家控制器。\n*   **服务器 stat 复制器类**:负责复制服务器 Stat 网络数据。\n\n您可以按原样使用默认类，也可以为自定义实现和行为指定自己的类。这些类将与游戏模式一起工作，并且将自动运行，而不会被放置在世界内部。\n\n## 游戏事件\n\n就多人游戏而言，当许多玩家进入游戏时，处理逻辑以允许他们进入游戏、保持他们的状态以及允许他们查看其他玩家的状态和处理他们的交互变得至关重要。\n\n游戏模式为你提供了几个可以被覆盖的事件来处理这样的多人游戏逻辑。以下事件对于网络功能和能力(它们最常用于此)特别有用:\n\n*   `On Post Log In`:该事件在玩家成功登录游戏后调用。从这一点开始，在 Player Controller 类上调用复制逻辑(用于多人游戏中的联网)是安全的。\n*   `Handle Starting New Player`:这个事件在`On Post Log In`事件之后调用，可以用来定义新进入的玩家会发生什么。默认情况下，它为新连接的玩家创建一个棋子。\n*   `SpawnDefaultPawnAtTransform`:此事件触发游戏内实际的棋子产卵。新连接的玩家可以在特定的变换中或者在关卡中预设的玩家开始位置产生(可以通过将玩家开始从模型窗口拖放到世界中来添加)。\n*   `On Logout`:当玩家离开游戏或者被破坏时，这个事件被调用。\n*   `On Restart Player`:这个事件叫玩家重生。与`SpawnDefaultPawnAtTransform`类似，玩家可以在特定的变换或预先指定的位置(使用玩家开始位置)被重新分配。\n\n## 联网\n\n游戏模式类不会复制到任何客户端或加入的玩家。它的范围只限于产生它的服务器。本质上，客户机-服务器模型规定客户机只作为服务器上正在玩的游戏的输入。因此，游戏逻辑不应该存在于客户端，而应该只存在于服务器。\n\n## 游戏模式基础对游戏模式\n\n从 4.14 版本开始，Epic 引入了`AGameModeBase`类，作为所有游戏模式类的父类。本质上是`AGameMode`类的简化版。\n\n然而，游戏模式类包含一些额外的功能，更适合多人射击类游戏，因为它实现了匹配状态的概念。默认情况下，游戏模式库包含在新的基于模板的项目中。\n\n游戏模式还包含一个状态机，处理和跟踪玩家的状态。\n\n# 级别\n\n在游戏中，关卡是游戏的一部分。由于许多游戏相当大，它们被分成不同的级别。一个感兴趣的等级被加载到游戏中供玩家玩，然后当他们玩完后，另一个等级可以被加载进来(而当前的等级将被加载出去)，这样玩家就可以继续玩了。要完成一个游戏，玩家通常需要完成一组特定的任务才能进入下一关，最终完成游戏。\n\n游戏模式可以直接应用于关卡。该关卡在加载后，将使用分配的游戏模式类来处理该特定关卡的所有逻辑和游戏性，并覆盖该关卡项目的游戏模式。这可以在打开关卡后使用`World Settings`标签来应用。\n\n级别蓝图是随级别运行的蓝图，但不能在级别范围之外访问。`Get Game Mode`节点可以在任意蓝图(包括关卡蓝图)中访问游戏模式。这可以在以后转换到你的游戏模式类，以获得对它的引用。\n\n注意\n\n一个等级只能分配一个游戏模式类别。然而，单个游戏模式类可以被分配到多个级别，以模仿相似的功能和逻辑。\n\n## 虚幻典当类\n\n《虚幻》中的`Pawn`类，是最基本的可以附身的演员类(不是玩家就是 AI)。它还图形化地表示了游戏中的玩家/机器人。这个类中的代码应该与游戏实体有关，包括交互、移动和能力逻辑。玩家在游戏中的任何时候仍然只能拥有一枚棋子。此外，玩家可以在游戏过程中*失去*一枚棋子，*拥有*另一枚棋子。\n\n### 默认棋子\n\n虚幻引擎给开发人员一个`DefaultPawn`类(继承自基础`Pawn`类)。在`Pawn`类之上，这个类包含额外的代码，允许它在世界范围内移动，就像你在游戏的编辑器版本中一样。\n\n### 看客卒\n\n一些游戏提供了观看游戏的功能。假设你在等待一个朋友完成他们的游戏，然后加入你，所以你继续观看他们的游戏。这让你能够通过摄像头观察玩家正在玩的游戏，你可以四处移动来查看玩家或游戏。有些游戏还提供了可以穿越回过去的壮观模式，来展示游戏中发生在过去或游戏中任何一点的特定动作。\n\n顾名思义，这是一种特殊类型的棋子，提供观看游戏的示例功能。它包含了这样做所需的所有基本工具(例如旁观者棋子移动组件)。\n\n## 虚幻玩家控制类\n\n玩家控制器类可以被认为是玩家。本质上是棋子的*灵魂*。玩家控制器接受用户的输入，并将其提供给棋子和其他类，供玩家与游戏交互。但是，在学习本课程时，您必须注意以下几点:\n\n*   与棋子不同，玩家在一个关卡中只能代表一个玩家控制器。(就像你乘电梯旅行一样。在一个电梯里，你只能控制那个电梯，但是你可以离开它，进入另一个电梯来控制那个电梯。)\n*   玩家控制器在整个游戏中持续存在，但棋子可能不会(例如，在战斗游戏中，玩家角色可能会死亡并重生，但玩家控制器将保持不变)。\n*   由于棋子的临时性质和玩家控制器的永久性质，开发人员需要记住哪些代码应该添加到哪个类中。\n\n让我们通过下一个练习更好地理解这一点。\n\n## 练习 2.04:设置游戏模式、玩家控制器和棋子\n\n本练习将使用我们在*练习 2.01* 、*中创建的空白项目创建一个空的 C++ 项目*。我们将向游戏中添加我们的游戏模式、玩家控制器和`Pawn`类，并将测试我们的代码是否在蓝图中工作。\n\n以下步骤将帮助您完成本练习:\n\n1.  打开我们在*练习 2.01* 、*中创建的项目，创建一个空的 C++ 项目*。\n2.  *右键点击`Content Browser`内的*，选择`Blueprint Class`。\n3.  Under the `All Classes` section, find and select the `Game Mode` class:\n\n    ![Figure 2.16: Selecting the Game Mode class ](img/B16183_02_16.jpg)\n\n    图 2.16:选择游戏模式类别\n\n4.  将其名称设为`BP_MyGameMode`。\n5.  重复*步骤 2-4* ，从`Common Classes`部分下选择`Pawn`类，如上图所示。将该类的名称设置为`BP_MyPawn`。\n6.  Repeat *Steps 2-4* and select the `Player Controller` class under the `Common Classes` section, as shown in the preceding screenshot. Set the name of this class to `BP_MyPC`:\n\n    ![Figure 2.17: Game Mode, Pawn, and Player Controller names ](img/B16183_02_17.jpg)\n\n    图 2.17:游戏模式、棋子和玩家控制器名称\n\n7.  Open `BP_MyGameMode` and open the `Event Graph` tab:\n\n    ![Figure 2.18: Event Graph tab in Blueprint ](img/B16183_02_18.jpg)\n\n    图 2.18:蓝图中的事件图选项卡\n\n8.  *Left-click* and drag from the white pin in the `Event BeginPlay` node and then release the *left mouse button* to gain an `Options` menu. Type `print` and select the `print` node highlighted in the list:\n\n    ![Figure 2.19: Print String node (Blueprint) ](img/B16183_02_19.jpg)\n\n    图 2.19:打印字符串节点(蓝图)\n\n9.  在置于`In String`参数下的结果`Print String`节点中，键入`My Game Mode has started!`。\n10.  现在，按下顶部菜单栏上的`Compile`和`Save`按钮。\n11.  对`BP_MyPawn`和`BP_MyPC`类重复*步骤 7-10* ，分别将`In String`参数设置为`My Pawn has started!`和`My PC has started!`。\n12.  Finally, open the `World Settings` tab, and under the `Game Mode` section, use the dropdown to set the `GameMode Override`, `Default Pawn Class`, and `Player Controller Class` options to our respective classes:\n\n    ![Figure 2.20: World Settings and Game Mode setup ](img/B16183_02_20.jpg)\n\n    图 2.20:世界设置和游戏模式设置\n\n13.  Click `Play` to play your game and see the three print statements on the top. This means that the current `GameMode Override`, `Default Pawn Class`, and `Player Controller Class` options have been set to your specified classes and are running their code:\n\n    ![Figure 2.21: Output prints ](img/B16183_02_21.jpg)\n\n图 2.21:输出打印\n\n注意\n\n您可以在 GitHub 上的`Chapter02` - > `Exercise2.04` - > `Ex2.04-Completed.rar`目录中找到已完成的练习代码文件，链接如下:[https://packt.live/3k7nS1K](https://packt.live/3k7nS1K)\n\n提取`.rar`文件后，*双击*文件。你会看到一个提示询问`Would you like to rebuild now?`。点击那个提示上的`Yes`，这样它就可以构建必要的中间文件，之后它应该会在虚幻编辑器中自动打开项目。\n\n现在您已经知道了基本类以及它们在虚幻中的工作方式，在下一节中，我们将研究动画，涉及哪些过程，以及它们是如何完成的。接下来我们将做一个练习。\n\n# 动画\n\n动画对于增加游戏的生命和丰富性至关重要。优秀的动画是区分一般游戏与优秀游戏、优秀游戏与最佳游戏的主要因素之一。视觉保真度让游戏玩家保持兴奋和沉浸在游戏中，因此动画是虚幻引擎中所有游戏和体验的核心部分。\n\n注意\n\n本章试图涵盖动画基础知识。在*第 13 章*、*混合空间 1D、键绑定和状态机*中会有更深入的动画制作方法。\n\n## 动画蓝图\n\n动画蓝图是一种特定的蓝图，允许您控制骨骼网格的动画。它为用户提供了一个专门用于动画相关任务的图表。在这里，您可以定义计算骨骼姿势的逻辑。\n\n注意\n\n骨骼网格是一种基于骨骼的网格，它有骨骼，所有骨骼聚集在一起形成网格，而静态网格(顾名思义)是一种不可动画化的网格。骨骼网格通常用于角色和类似生命的物体(例如玩家英雄)，而静态网格用于基本或无生命的物体(例如墙壁)。\n\n动画蓝图提供两种图形:`EventGraph`和`AnimGraph`。\n\n## 事件图\n\n动画蓝图中的事件图提供了与动画相关的设置事件，正如我们在*第 1 章*、*虚幻引擎介绍*中所了解的，这些事件可用于变量操作和逻辑。事件图主要用于动画蓝图中，以更新混合空间值，进而驱动`AnimGraph`内的动画。这里使用的最常见事件如下:\n\n*   **蓝图初始化动画:**用于初始化动画。\n*   **蓝图更新动画:**此事件每帧执行一次，使开发人员能够根据需要执行计算并更新其值:\n\n![Figure 2.22: Animation Event Graph ](img/B16183_02_22.jpg)\n\n图 2.22:动画事件图\n\n在前面的截图中，您可以看到默认的事件图。这里有`Event Blueprint Update Animation`和`Try Get Pawn Owner`节点。在*练习 2.04* 、*设置游戏模式、玩家控制器和棋子*中，您创建了新节点并将其附加到图形中以完成一些有意义的任务。\n\n## 动漫图\n\nAni m Graph 专用于并负责每帧播放动画和输出骨骼的最终姿势。它为开发人员提供了执行不同逻辑的特殊节点。例如，“混合”节点接受多个输入，并用于决定当前在执行中使用哪个输入。这个决定通常依赖于一些外部输入(例如阿尔法值)。\n\nAnim Graph 通过跟踪正在使用的节点上的执行引脚之间的执行流程来评估节点。\n\n在下面的截图中，你可以在图上看到一个单独的`Output Pose`节点。这是动画的最终姿势输出，将在游戏中的相关骨骼网格上可见。我们将在*练习 2.05* 、*创建人体模型动画*中使用它:\n\n![Figure 2.23: Animation AnimGraph ](img/B16183_02_23.jpg)\n\n图 2.23:动画动画\n\n## 状态机\n\n您已经学习了如何设置动画节点和逻辑，但是缺少了一个必不可少的组件。谁来决定何时播放或执行特定的动画或逻辑？这就是状态机出现的地方。例如，玩家可能需要从蹲伏姿势转换为站立姿势，因此动画需要更新。代码将调用动画蓝图，访问状态机，并让它知道动画的状态需要更改，从而实现平滑的动画过渡。\n\n状态机由状态和规则组成，可以认为它们描述了动画的状态。状态机在特定时间总是处于一种状态。当满足某些条件(由规则定义)时，从一种状态转换到另一种状态。\n\n## 过渡规则\n\n每个转换规则包含一个名为`Result`的布尔节点。如果布尔值为真，则可以发生转换，反之亦然:\n\n![Figure 2.24: Transition Rules ](img/B16183_02_24.jpg)\n\n图 2.24:过渡规则\n\n## 混合空间\n\n当你获得一堆动画时，你可以创建一个状态机并运行这些动画。但是，当您需要从一个动画过渡到另一个动画时，会出现一个问题。如果你简单地切换动画，它会出现故障，因为新动画的开始姿势可能不同于旧动画的结束姿势。\n\n混合空间是用于根据不同动画的 alpha 值在它们之间进行插值的特殊资源。这反过来消除了毛刺问题，并在两个动画之间进行插值，从而快速平滑地改变动画。\n\n混合空间可以在一维(称为混合空间 1D)或二维(称为混合空间)中创建。它们分别基于一个或两个输入混合任意数量的动画。\n\n## 练习 2.05:创建人体模型动画\n\n现在，您已经了解了大多数与动画相关的概念，我们将通过向默认人体模型添加一些动画逻辑来进行实际操作。我们将创建一个混合空间 1D，一个状态机，和动画逻辑。\n\n我们在这里的目标是创建一个我们角色的运行动画，从而深入了解动画是如何工作的，以及它们在 3D 世界中与实际角色绑定的方式。\n\n以下步骤将帮助您完成本练习:\n\n1.  Download and extract all the contents of the `Chapter02` -> `Exercise2.05` -> `ExerciseFiles` directory, which can be found on GitHub. You can extract this to any directory you're comfortable with using on your machine.\n\n    注意\n\n    `ExerciseFiles`目录可以在 GitHub 上找到，链接如下:[https://packt.live/32tIFGJ](https://packt.live/32tIFGJ)。\n\n2.  *双击*`CharAnim.uproject`文件开始项目。\n3.  按下`Play`。使用键盘的 *W* 、 *A* 、 *S* 、 *D* 键移动，*空格键*跳跃。请注意，目前人体模型上没有动画。\n4.  在`Content`文件夹中，浏览至`Content`->-`Mannequin`->`Animations`。\n5.  *右键单击`Content`文件夹中的*，从`Animation`部分选择`Blend Space 1D`。\n6.  选择`UE4_Mannequin_Skeleton`。\n7.  将新创建的文件重命名为`BS_IdleRun`。\n8.  *双击* `BS_IdleRun`打开。\n9.  Under the `Asset Details` tab, inside the `Axis Settings` section, expand the `Horizontal Axis` section and set `Name` to `Speed` and `Maximum Axis Value` to `375.0`:\n\n    ![Figure 2.25: Blend Space 1D Axis Settings ](img/B16183_02_25.jpg)\n\n    图 2.25:混合空间 1D 轴设置\n\n10.  前往`Sample Interpolation`部分，将`Target Weight Interpolation Speed Per Sec`设置为`5.0`。\n11.  Drag and drop the `ThirdPersonIdle`, `ThirdPersonWalk`, and `ThirdPersonRun` animations into the graph separately:\n\n    ![Figure 2.26: Blend Space previewer ](img/B16183_02_26.jpg)\n\n    图 2.26:混合空间预览器\n\n12.  Under the `Asset Details` tab, in `Blend Samples`, set the following variable values:\n\n    ![Figure 2.27: Blend Samples ](img/B16183_02_27.jpg)\n\n    图 2.27:混合样本\n\n13.  点击`Save`并关闭此`Asset`。\n14.  *右键单击`Content`文件夹中的*，从`Animation`部分选择`Animation Blueprint`。\n15.  In the `Target Skeleton` section, select `UE4_Mannequin_Skeleton` and then click the `OK` button:\n\n    ![Figure 2.28: Creating the Animation Blueprint asset ](img/B16183_02_28.jpg)\n\n    图 2.28:创建动画蓝图资产\n\n16.  命名文件`Anim_Mannequin`并按*进入*。\n17.  *双击*新创建的`Anim_Mannequin`文件。\n18.  接下来，进入`Event Graph`选项卡。\n19.  Create a `boolean` variable called `IsInAir?` by clicking the `+` icon in the variable section on the bottom left side. Be sure to assign the proper type:\n\n    ![Figure 2.29: Adding variables ](img/B16183_02_29.jpg)\n\n    图 2.29:添加变量\n\n20.  创建一个名为`Speed`的浮点变量。\n21.  Drag off the `Try Get Pawn Owner` return value node and type in `Is Valid`. Select the bottom one:\n\n    ![Figure 2.30: Event Graph Is Valid node ](img/B16183_02_30.jpg)\n\n    图 2.30:事件图是有效节点\n\n22.  Connect the `Exec` pin from the `Event Blueprint Update Animation` node to the `Is Valid` node:\n\n    ![Figure 2.31: Connecting nodes ](img/B16183_02_31.jpg)\n\n    图 2.31:连接节点\n\n23.  从`Try Get Pawn Owner`节点，使用`Get Movement Component`节点。\n24.  From the node obtained in *Step 22*, get the `Is Falling` node and connect the Boolean return value to a set node for the `Is in Air?` Boolean. Connect the `SET` node exec pin with the `Is Valid` exec pin:\n\n    ![Figure 2.32: Is in Air Boolean setup ](img/B16183_02_32.jpg)\n\n    图 2.32:处于空中布尔设置\n\n25.  From the `Try Get Pawn Owner` node, use the `Get Velocity` node, get its `VectorLength`, and connect the output to the `A Variable Set` node of `Speed`:\n\n    ![Figure 2.33: Speed Boolean setup ](img/B16183_02_33.jpg)\n\n    图 2.33:速度布尔设置\n\n26.  接下来，前往`Anim Graph`选项卡。\n27.  *Right-click* anywhere inside `AnimGraph`, type `state machine`, and click on `Add New State Machine`:\n\n    ![Figure 2.34: The Add New State Machine option ](img/B16183_02_34.jpg)\n\n    图 2.34:添加新状态机选项\n\n28.  确保选中该节点，然后按 *F2* 将其重命名为`MannequinStateMachine`。\n29.  Connect the output pin of `MannequinStateMachine` to the input pin for the `Output Pose` node and click the compile button on the top bar:\n\n    ![Figure 2.35: Configuring the State Machine result in the Output Pose node ](img/B16183_02_35.jpg)\n\n    图 2.35:在输出姿势节点中配置状态机结果\n\n30.  *双击*节点进入状态机。您将看到一个`Entry`节点。将与其连接的状态将成为人体模型的默认状态。在这个练习中，这将是我们的`Idle Animation`。\n31.  *右键单击状态机内部的空白区域*，从菜单中选择`Add State`。按 *F2* 将其重命名为`Idle/Run`。\n32.  Drag from the icon next to the `Entry` text, point it inside the `Idle/Run` node, and then release it to connect it:\n\n    ![Figure 2.36: Connecting Added State to Entry ](img/B16183_02_36.jpg)\n\n    图 2.36:将添加的状态连接到条目\n\n33.  *双击`Idle/Run`状态下的*将其打开。\n34.  From the `Asset Browser` menu in the bottom-right corner, select and drag the `BS_IdleRun` Animation onto the graph. Get the `Speed` variable from the `Variable` section on the left and connect it, as shown here:\n\n    ![Figure 2.37: Idle/Run state setup ](img/B16183_02_37.jpg)\n\n    图 2.37:空闲/运行状态设置\n\n35.  Head back to `MannequinStateMachine` by clicking on its breadcrumb in the top banner:\n\n    ![Figure 2.38: State Machine navigation breadcrumb ](img/B16183_02_38.jpg)\n\n    图 2.38:状态机导航面包屑\n\n36.  从`Asset Browser`菜单中，将`ThirdPersonJump_Start`动画拖放到图形中。改名`Jump_Start`。\n37.  Repeat *Step 35* for `ThirdPersonJump_Loop` and `ThirdPerson_Jump` and rename them `Jump_Loop` and `Jump_End`, respectively:\n\n    ![Figure 2.39: State setup ](img/B16183_02_39.jpg)\n\n    图 2.39:状态设置\n\n38.  打开`Jump_Start`状态。点击`Play ThirdPersonJump_Start`节点。*取消勾选`Settings`部分的* `Loop Animation`。\n39.  打开`Jump_Loop`状态，点击`Play ThirdPersonJump_Loop`节点。将`Play Rate`设置为`0.75`。\n40.  打开`Jump_End`状态，点击`Play ThirdPerson_Jump`节点。*取消选中*中的`Loop Animation`布尔。\n41.  Since we can shift from `Idle/Run` to `Jump_Start`, drag from the `Idle/Run` state and drop it to the `Jump_Start` state. Similarly, `Jump_Start` leads to `Jump_Loop`, then to `Jump_End`, and finally back to `Idle/Run`.\n\n    拖放箭头以设置状态机，如下所示:\n\n    ![Figure 2.40: State connections ](img/B16183_02_40.jpg)\n\n    图 2.40:状态连接\n\n42.  *Double-click* the `Idle/Run` to `Jump_Start` transition rule icon and connect the output of the `Is in Air?` variable to the result:\n\n    ![Figure 2.41: Idle/Run to Jump_Start transition rule setup ](img/B16183_02_41.jpg)\n\n    图 2.41:空闲/运行到跳转 _ 开始转换规则设置\n\n43.  Open the `Jump_Start` to `Jump_Loop` transition rule. Get the `Time Remaining (ratio)` node for `ThirdPersonJump_Start` and check whether it is less than `0.1`. Connect the resulting bool to the result:\n\n    ![Figure 2.42: Jump_Start to Jump_End transition rule setup ](img/B16183_02_42.jpg)\n\n    图 2.42:跳转开始到跳转结束转换规则设置\n\n44.  Open the `Jump_Loop` to `Jump_End` transition rule. Connect the output of the inverse of the `Is in Air?` variable to the result:\n\n    ![Figure 2.43: Jump_Loop to Jump_End transition rule setup ](img/B16183_02_43.jpg)\n\n    图 2.43:跳转循环到跳转结束转换规则设置\n\n45.  Open the `Jump_End` to `Idle/Run` transition rule. Get the `Time Remaining (ratio)` node for `ThirdPerson_Jump` and check whether it is less than `0.1`. Connect the resulting bool to the result:\n\n    ![Figure 2.44: Jump_End to Idle/Run transition rule setup ](img/B16183_02_44.jpg)\n\n    图 2.44:跳转结束到空闲/运行转换规则设置\n\n46.  关闭动画蓝图。\n47.  在`Content`文件夹中，浏览至`Content` - > `ThirdPersonBP` - > `Blueprints folder`打开`ThirdPersonCharacter`蓝图。\n48.  Select `Mesh` in the `Components` tab:\n\n    ![Figure 2.45: Mesh component ](img/B16183_02_45.jpg)\n\n    图 2.45:网格组件\n\n49.  In the `Details` tab, set `Anim Class` to the `Animation Blueprint` class that you created:\n\n    ![Figure 2.46: Specifying the Animation Blueprint in the Skeletal Mesh component ](img/B16183_02_46.jpg)\n\n    图 2.46:在骨骼网格组件中指定动画蓝图\n\n50.  关闭蓝图。\n51.  Play the game again and notice the animations.\n\n    下面应该是你实现的输出。如您所见，我们的角色正在运行，并且正在显示运行动画:\n\n    ![Figure 2.47: Character running animation ](img/B16183_02_47.jpg)\n\n图 2.47:角色运行动画\n\n注意\n\n您可以在 GitHub 上的`Chapter02` - > `Exercise2.05` - > `Ex2.05-Completed.rar`目录中找到完整的练习代码文件，链接如下:[https://packt.live/3kdIlSL](https://packt.live/3kdIlSL)\n\n提取`.rar`文件后，*双击*文件。你会看到一个提示询问`Would you like to rebuild now?`。点击那个提示上的`Yes`，这样它就可以构建必要的中间文件，之后它应该会在虚幻编辑器中自动打开项目。\n\n通过完成本练习，您已经了解了如何创建状态机、混合空间 1D、动画蓝图，以及如何将其与角色的骨骼网格绑定在一起。您还研究了播放速率、过渡速度和过渡状态，帮助您了解动画世界是如何错综复杂地联系在一起的。\n\n我们通过了解状态机如何用于表示动画状态以及动画状态之间的转换来开始这一部分。接下来，我们要知道混合空间 1D 如何在这些过渡之间给我们混合。动画蓝图使用所有这些来决定角色的当前动画是什么。现在，让我们在一个活动中将所有这些概念结合起来。\n\n## 活动 2.01:将动画链接到角色\n\n假设，作为一名虚幻游戏开发人员，你已经获得了一个角色骨骼网格及其动画，你的任务是将它们集成到一个项目中。为了做到这一点，在本活动中，您将创建一个动画蓝图、状态机和一个新角色的混合空间 1D。通过完成本练习，您应该能够在虚幻引擎中处理动画，并将它们链接到骨骼网格。\n\n活动项目文件夹包含一个第三人称模板项目，以及一个新字符`Ganfault`。\n\n注意\n\n这个角色及其动画是从[mixamo.com](http://mixamo.com)下载的。这些已经放在我们 GitHub 存储库的`Content` - > `Ganfault`文件夹中:[https://packt.live/35eCGrk](https://packt.live/35eCGrk)\n\n*Mixamo.com*是一个销售带有动画的 3D 角色的网站，也是一个仅面向 3D 模型的资产市场。除付费模型外，它还包含一个免费模型库。\n\n以下步骤将帮助您完成本活动:\n\n1.  为行走/跑步动画创建混合空间 1D，并设置动画蓝图。\n2.  接下来，转到`Content`->- `ThirdPersonBP`-> `Blueprints`，打开`ThirdPersonCharacter`蓝图。\n3.  单击左侧的骨骼网格组件，在右侧的`Details`选项卡中，用`Ganfault`替换`SkeletalMesh`参照。\n4.  Similarly, update the `Animations Blueprint` section of the skeletal mesh component with the Animation Blueprint you created for `Ganfault`.\n\n    注意\n\n    对于状态机，只实现空闲/运行和跳转状态。\n\n完成本练习后，行走/跑步和跳跃动画应该可以正常工作，如以下输出所示:\n\n![Figure 2.48: Activity 2.01 expected output (Left: Run; Right: Jump) ](img/B16183_02_48.jpg)\n\n图 2.48:活动 2.01 预期输出(左:运行；右:跳)\n\n注意\n\n这个活动的解决方案可以在:[https://packt.live/338jEBx](https://packt.live/338jEBx)找到。\n\n通过完成本练习，您现在知道了如何在虚幻引擎中导航项目、调试代码和使用动画。您还可以理解状态机，它代表动画状态和过渡中使用的混合空间 1D 之间的过渡。现在，您可以根据游戏事件和输入将动画添加到 3D 模型中。\n\n# 总结\n\n总结本章，我们首先学习了如何创建一个空项目。然后，我们学习了文件夹结构以及如何在项目目录中组织文件。之后，我们看了基于模板的项目。我们还学习了如何在代码中设置断点，这样我们就可以在游戏运行时观察变量值并调试整个对象，这将有助于我们发现并消除代码中的错误。\n\n此后，我们看到了游戏模式、玩家棋子和玩家控制器是如何在虚幻引擎中建立游戏流程(代码的执行顺序)的相关类，以及它们是如何在项目中建立的。\n\n最后，我们向动画基础过渡，并与状态机、混合空间 1D 和动画蓝图合作，根据键盘输入使我们的角色在游戏中动画化(行走/奔跑和跳跃)。\n\n在这一章中，我们更加熟悉了虚幻引擎中对游戏开发至关重要的强大工具。虚幻的游戏模式及其默认职业是在虚幻引擎中制作任何游戏或体验所必需的。此外，动画给你的角色带来了活力，有助于在游戏中增加沉浸感。所有游戏工作室都有动画、角色和游戏逻辑，因为这些是驱动任何游戏的核心组件。这些技能将在你的游戏开发旅程中帮助你无数次。\n\n在下一章中，我们将讨论虚幻引擎中的`Character`类，它的组件，以及如何扩展该类进行额外的设置。你将进行各种练习，然后是一项活动。"
  },
  {
    "path": "docs/game-dev-proj-ue/03.md",
    "content": "# 三、角色类组件和蓝图设置\n\n概观\n\n本章将重点介绍 C++ 中的`Character`类。您将看到如何在 C++ 中扩展`Character`类，然后通过继承在蓝图中进一步扩展这个新创建的`Character`类。您还将处理玩家输入和一些移动逻辑。\n\n到本章结束时，您将能够理解类继承在 UE4 中是如何工作的，以及如何利用它来实现您的优势。您还将能够使用轴和动作输入映射，这是驱动玩家相关输入逻辑的关键。\n\n# 简介\n\n在前一章中，我们学习了如何创建空项目和导入文件，使用哪个文件夹结构，以及如何处理动画。在本章中，我们将探讨使用虚幻引擎时您将使用的一些其他关键工具和功能。\n\n游戏开发人员在构建游戏功能时，通常需要使用某些工具来节省时间和精力。虚幻引擎强大的对象继承能力为开发人员提供了提高效率所需的优势。开发人员还可以交替使用 C++ 和蓝图，并在开发游戏时使用它们来造福自己。\n\n开发人员获得的另一个增值好处是能够扩展代码以供项目后期使用。假设你的客户在旧需求的基础上有了新的需求(就像大多数游戏工作室一样)。现在，为了扩展功能，开发人员只需继承一个类，并向其中添加更多功能，就可以快速获得结果。这非常强大，在很多情况下都会派上用场。\n\n在这一章中，我们将讨论虚幻`Character`类，创建 C++ 代码，然后在蓝图中扩展它，最后用它来创建一个游戏中的角色。\n\n# 虚幻人物类\n\n在我们谈论虚幻`Character`类之前，让我们简单地谈谈继承的概念。如果您习惯于使用 C++ 或其他类似的语言，您应该已经熟悉了这个概念。继承是一个类从另一个类中获得特征和行为的过程。可以扩展 C++ 类来创建一个新的类——派生类，该类保留基类的属性，并允许修改这些属性或添加新的特性。这方面的一个例子是`Character`类。\n\nA `Character`类是一种特殊类型的棋子，是虚幻`Pawn`类的后代。在`Pawn`类的基础上，默认情况下`Character`类拥有一些移动能力，以及一些为角色添加移动的输入。作为标准，`Character`类让用户能够让角色在创造的世界中行走、奔跑、跳跃、飞行和游泳。\n\n由于一个`Character`类是`Pawn`类的扩展，它包含了棋子的所有代码/逻辑，开发人员可以扩展这个类来增加更多的功能。当扩展`Character`类时，它的现有组件作为继承组件被传递到扩展类。(在这种情况下，是胶囊组件、箭头组件和网格)。\n\n注意\n\n无法删除继承的组件。它们的设置可能会更改，但是添加到基类的组件将始终出现在扩展类中。在这种情况下，基类是`Pawn`类，而扩展(或子)类是`Character`类。\n\n`Character`类提供以下继承组件:\n\n*   **胶囊组件**:这是根组件，作为层级内其他组件附着的“原点”。这个组件也可以用于碰撞，并采取胶囊的形式，逻辑上概述了许多角色形式(特别是人形)。\n*   **箭头组件**:这提供了一个指向层级前面的简单箭头。默认情况下，这在游戏开始时设置为`hide`，但可以调整为可见。如果需要，这个组件可以用于调试和调整游戏逻辑。\n*   **骨骼网格组件**:这是`Character`类中开发人员最关心的主要组件。骨骼网格是角色将采取的形式，可以在这里与所有相关变量一起设置，包括动画、碰撞等。\n\n大多数开发人员通常更喜欢用 C++ 编写游戏和角色逻辑代码，并将该类扩展到蓝图，以便他们可以执行其他简单的任务，例如将资产连接到该类。因此，例如，开发人员可以创建一个从`Character`类继承的 C++ 类，编写该类中的所有移动和跳转逻辑，然后用蓝图扩展该类，在蓝图中，开发人员用所需的资产(例如骨骼网格和动画蓝图)更新组件，并可选地将附加功能编码到蓝图中。\n\n## 扩展字符类\n\n当被 C++ 或蓝图继承时，`Character`类被扩展。这个扩展的`Character`类将是`Character`类的子类(*将被称为其父类*)。类扩展是面向对象编程的一个强大部分，类可以扩展到很深的层次。\n\n## 练习 3.01:创建和设置第三人称角色 C++ 类\n\n在本练习中，您将基于`Character`类创建一个 C++ 类。您还将初始化变量，这些变量将在扩展此`Character`类的类的默认值中设置。\n\n以下步骤将帮助您完成本练习:\n\n1.  启动虚幻引擎，选择`Games`类别，点击`Next`按钮。\n2.  选择`Blank`并点击`Next`按钮。\n3.  选择`C++ `作为项目类型，将项目名称设置为`MyThirdPerson`，选择合适的项目目录，点击`Create Project`按钮。\n4.  *右键点击`Content Browser`界面的*，点击`New C++ Class`按钮:\n5.  在打开的对话框中，选择`Character`作为类别类型，点击`Next`按钮。\n6.  命名为`MyThirdPersonChar`，点击`Create Class`按钮。\n7.  Upon doing so, Visual Studio will open the `MyThirdPersonChar.cpp` and `MyThirdPersonChar.h` tabs.\n\n    注意\n\n    在某些系统上，可能需要以管理员权限运行虚幻引擎编辑器，才能使用新创建的 C++ 文件自动打开 Visual Studio 解决方案。\n\n8.  Open the `MyThirdPersonChar.h` tab and add the following code under the `GENERATED_BODY()` text:\n\n    ```cpp\n    // Spring arm component which will act as a placeholder for   the player camera\n    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category =   MyTPS_Cam, meta = (AllowPrivateAccess = \"true\"))\n    class USpringArmComponent* CameraBoom;\n    // Follow camera\n    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category =   MyTPS_Cam, meta = (AllowPrivateAccess = \"true\"))\n    class UCameraComponent* FollowCamera;\n    ```\n\n    在前面的代码中，我们声明了两个组件:`Camera`本身和`Camera boom`，它们在距离玩家一定距离处充当相机的占位符。这些组件将在*步骤 11* 的构造器中初始化。\n\n9.  在`MyThirdPersonChar.h`文件的`#include \"CoreMinimal.h\"`下的包含部分添加以下内容:\n\n    ```cpp\n    #include \"GameFramework/SpringArmComponent.h\"\n    #include \"Camera/CameraComponent.h\"\n    ```\n\n10.  Now, head over to the `MyThirdPersonChar.cpp` tab and add the following includes after the `#include MyThirdPersonChar.h` code:\n\n    ```cpp\n    #include \"Components/CapsuleComponent.h\"\n    #include \"GameFramework/CharacterMovementComponent.h\"\n    ```\n\n    在前面的代码片段中，代码将相关的类添加到类中，这意味着我们现在可以访问它的方法和定义。\n\n11.  In the `AMyThirdPersonChar::AMyThirdPersonChar()` function, add the following lines:\n\n    ```cpp\n    // Set size for collision capsule\n    GetCapsuleComponent()->InitCapsuleSize(42.f, 96.0f);\n    // Don't rotate when the controller rotates. Let that just   affect the camera.\n    bUseControllerRotationPitch = false;\n    bUseControllerRotationYaw = false;\n    bUseControllerRotationRoll = false;\n    // Configure character movement\n    GetCharacterMovement()->bOrientRotationToMovement = true;\n    // Create a camera boom (pulls in towards the   player if there is a collision)\n    CameraBoom =   CreateDefaultSubobject<USpringArmComponent>(TEXT(\"CameraBoom\"));\n    CameraBoom->SetupAttachment(RootComponent);\n    CameraBoom->TargetArmLength = 300.0f;\n    CameraBoom->bUsePawnControlRotation = true; \n    // Create a camera that will follow the character\n    FollowCamera =   CreateDefaultSubobject<UCameraComponent>(TEXT(\"FollowCamera\"));\n    FollowCamera->SetupAttachment(CameraBoom,   USpringArmComponent::SocketName);\n    FollowCamera->bUsePawnControlRotation = false;\n    ```\n\n    截取的前一段代码的最后一行将设置相机，使其旋转与棋子的旋转绑定。这意味着相机应该随着与该棋子相关的玩家控制器的旋转而旋转。\n\n12.  Head back to the Unreal Engine project and click the `Compile` button in the top bar:\n\n    ![Figure 3.1: Compile button on the Unreal Editor top bar ](img/B16183_03_01.jpg)\n\n图 3.1:虚幻编辑器顶部栏上的编译按钮\n\n右下方应该会出现一条`Compile Complete!`信息。\n\n注意\n\n您可以在 GitHub 上的`Chapter03` - > `Exercise3.01`目录中找到已完成的练习代码文件，链接如下:[https://packt.live/3khFrMt](https://packt.live/3khFrMt)。\n\n提取`.rar`文件后，*双击*文件。你会看到一个提示询问`Would you like to rebuild now?`。点击那个提示上的`Yes`，这样它就可以构建必要的中间文件，之后它应该会在虚幻编辑器中自动打开项目。\n\n通过完成本练习，您已经学会了如何扩展`Character`课程。您还学习了如何初始化`Character`类的默认组件，以及如何从虚幻编辑器中编译更新的代码。接下来，您将学习如何扩展您在蓝图中创建的 C++ 类，以及为什么在许多情况下这是可行的。\n\n# 用蓝图扩展 C++ 类\n\n如前所述，大多数开发人员将 C++ 代码逻辑扩展到蓝图，以便将其与他们将使用的资产联系起来。与在代码中查找和设置资产相比，这样做是为了实现简单的资产分配。此外，它使开发人员能够使用强大的蓝图功能，如时间轴、事件和现成的宏，并结合他们的 C++ 代码，以实现用 C++ 和蓝图开发的最大好处。\n\n到目前为止，我们已经制作了一个 C++ `Character`类。在其中，我们设置了组件和移动功能。现在，我们想要指定将要在我们的类中使用的资产，以及添加输入和移动能力。为此，使用蓝图进行扩展并在那里设置选项更容易。这是我们将在下一个练习中做的。\n\n## 练习 3.02:用蓝图扩展 C++\n\n在本练习中，您将学习如何扩展使用蓝图创建的 C++ 类，以便在预先存在的 C++ 代码的基础上添加蓝图代码。您还将添加输入键绑定，负责移动角色。\n\n以下步骤将帮助您完成本练习:\n\n1.  Download and extract all the contents of the `Chapter03` *|* `Exercise3.02` *|* `ExerciseFiles` directory, which can be found on GitHub.\n\n    注意\n\n    `ExerciseFiles`目录可以在 GitHub 上找到，链接如下:[https://packt.live/2GO0dG8](https://packt.live/2GO0dG8)。\n\n2.  浏览到我们在*练习 3.01* 、*中创建的`MyThirdPerson`项目内的`Content`文件夹，创建并设置第三人称角色 C++ 类*。\n3.  Copy the `MixamoAnimPack` folder we created in *Step 1* and paste it into the `Content` folder directory we opened in *Step 2*, as shown in the following screenshot:\n\n    注意\n\n    `MixamoAnimPack`资产是通过以下链接从 Epic 市场获得的:[https://www . unrealengine . com/market/en-US/product/mix amo-animation-pack](https://www.unrealengine.com/marketplace/en-US/product/mixamo-animation-pack)。\n\n    ![Figure 3.2: MixamoAnimPack placed in the project directory ](img/B16183_03_02.jpg)\n\n    图 3.2:放置在项目目录中的 MixamoAnimPack\n\n4.  打开项目。*右键点击`Content Browser`界面内的*，点击`Blueprint Class`。\n5.  In the `Search` dialogue, type `GameMode`, *right-click* the class matching the name, and click the `Select` button. Have a look at the following screenshot:\n\n    ![Figure 3.3: Creating the GameMode class ](img/B16183_03_03.jpg)\n\n    图 3.3:创建游戏模式类\n\n6.  说出我们在*第 6 步*中创建的蓝图。\n7.  现在，重复*步骤 5* 。\n8.  在`Search`框中，输入`MyThirdPersonChar`，选择类别，然后在`Select`按钮上右键*。*\n**   说出我们在*第 9 步*中创建的蓝图。*   In the `World Settings` tab, click the `None` option next to `GameMode Override` and select `BP_GameMode`:\n\n    ![Figure 3.4: Specifying Game Mode in World Settings ](img/B16183_03_04.jpg)\n\n    图 3.4:在世界设置中指定游戏模式\n\n    *   Set `Default Pawn Class` to `BP_MyTPC`:\n\n    ![Figure 3.5: Specifying Default Pawn Class in Game Mode ](img/B16183_03_05.jpg)\n\n    图 3.5:在游戏模式中指定默认棋子类别\n\n    *   打开`BP_MyTPC`，点击左侧`Components`标签层次中的`Mesh (Inherited)`组件。*   In the `Details` tab, find the `Mesh` section and set `Skeletal Mesh` to `Maximo_Adam`.\n\n    注意\n\n    网格和动画将在*第 13 章*、*混合空间 1D、键绑定和状态机*中详细介绍。\n\n    *   In the `Details` tab, find the `Animation` section and set `Anim Class` to `MixamoAnimBP_Adam_C`. You'll note that this class name gets suffixed with `_C` when selected. This is basically the instance of the blueprint created by UE4\\. Blueprints, in a working project/build, usually get suffixed this way to differentiate between a Blueprint Class and an instance of that class.\n\n    ![Figure 3.6: Setting up Anim Class and Skeletal Mesh ](img/B16183_03_06.jpg)\n\n    图 3.6:设置动画类和骨骼网格\n\n    *   从最上面的菜单，进入`Edit`下拉菜单，点击`Project Settings`。*   Click on the `Input` section, which can be found in the `Engine` section:\n\n    ![Figure 3.7: Input section of Project Settings ](img/B16183_03_07.jpg)\n\n    图 3.7:项目设置的输入部分\n\n    *   In the `Bindings` section, click the `+` icon next to `Axis Mappings` and expand the section.\n\n    注意\n\n    操作映射是执行的单次按键操作，如跳转、划线或运行，而轴映射是分配的浮点值，将根据用户的按键返回浮点值。这在游戏手柄控制器或虚拟现实控制器的情况下更为相关，在这种情况下，模拟拇指操纵杆开始发挥作用。在这种情况下，它将返回拇指棒状态的浮动值，这对于管理玩家移动或相关功能非常重要。\n\n    *   将`NewAxisMapping_0`重命名为`MoveForward`。*   在`MoveForward`部分，点击下拉菜单，选择`W`。*   单击`MoveForward`图标旁边的`+`图标，添加另一个字段。*   将新字段设置为`S`。将其刻度设置为`-1.0`(因为我们想用`S`键向后移动)。*   Create another axis mapping by repeating *Step 18*, name it `MoveRight`, and add two fields – `A` with `-1.0` for the scale and `D` with `1.0` for the scale:\n\n    ![Figure 3.8: Movement Axis Mappings ](img/B16183_03_08.jpg)\n\n    图 3.8:运动轴映射\n\n    *   Open `BP_MyTPC` and click the `Event Graph` tab:\n\n    ![Figure 3.9: Event Graph tab ](img/B16183_03_09.jpg)\n\n    图 3.9:事件图选项卡\n\n    *   *Right-click* anywhere inside the graph, type `MoveForward`, and select the first node option:\n\n    ![Figure 3.10: MoveForward Axis Event ](img/B16183_03_10.jpg)\n\n    图 3.10:前移轴事件\n\n    *   *Right-click* inside the graph, search for `Get Control Rotation`, and select the first node option.\n\n    注意\n\n    由于与玩家相关联的摄像机可以选择不显示棋子的偏航、滚动或俯仰，`Get Control Rotation`使棋子完全瞄准旋转。这在许多计算中都很有用。\n\n    *   *左键单击*，从`Get Control Rotation`节点的`Return Value`拖动，搜索`Break Rotator`，选择。*   *右键点击图内*，搜索`Make Rotator`，选择第一个节点选项。*   Connect the `Z` (*Yaw*) node from `Break Rotator` to the `Z` (*Yaw*) node of the `Make Rotator` node.\n\n    注意\n\n    使`Rotator`用俯仰、滚转和偏航值创建一个旋转器，而断开旋转器将旋转器拆分成它的组件(滚转、俯仰和偏航)。\n\n    *   *左键单击*，从`Make Rotator`节点的`Return Value`拖动，搜索`Get` `Forward Vector`，选择。*   *左键单击*，从`Get Forward Vector`节点的`Return Value`拖动，搜索`Add Movement Input`，选择。*   将`InputAxis MoveForward`节点的`Axis Value`节点连接到`Add Movement Input`节点的`Scale Value`节点。*   最后，将白色`Execution`引脚从`InputAxis MoveForward`节点连接到`Add Movement Input`节点。*   *右键点击图内*，搜索`InputAxis MoveRight`，选择第一个节点选项。*   *左键单击*，从`Make Rotator`节点的`Return Value`拖动，搜索`Get Right Vector`，选择。*   *左键单击*，从`Get Right Vector`节点的`Return Value`拖动，搜索`Add Movement Input`，选择。*   将`InputAxis MoveRight`节点的`Axis Value`引脚连接到我们上一步创建的`Add Movement Input`节点的`Scale Value`引脚。*   Finally, connect the `white Execution` pin from the `InputAxis MoveRight` node to the `Add Movement Input` node we added in *Step 36*:\n\n    ![Figure 3.11: Movement logic ](img/B16183_03_11.jpg)\n\n    图 3.11:运动逻辑\n\n    *   Now, head to the `Viewport` tab. Here, you will see that the character's front is not pointing in the direction of the arrow and that the character is displaced above the capsule component. Click on the `Mesh` component and select the object translation node located at the top of the viewport. Then, drag the arrows on the Mesh to adjust it so that the feet align with the bottom of the capsule component and the Mesh is rotated to point toward the arrow:\n\n    ![Figure 3.12: Translation Rotation and Scale Selector section ](img/B16183_03_12.jpg)\n\n    图 3.12:平移旋转和缩放选择器部分\n\n    一旦字符在胶囊中对齐，它将显示为以下屏幕截图:\n\n    ![Figure 3.13: Mesh adjusted within the capsule component ](img/B16183_03_13.jpg)\n\n    图 3.13:胶囊组件内调整的网格\n\n    *   在`Toolbar`菜单中，按下`Compile`按钮，然后按下`Save`。*   Go back to the map tab and press the `Play` button to view your character in-game. Use the *W*, *A*, *S*, and *D* keys to move around.\n\n    注意\n\n    您可以在 GitHub 上的`Chapter03` - > `Exercise3.02`目录中找到已完成的练习代码文件，链接如下:[https://packt.live/3keGxIU](https://packt.live/3keGxIU)。\n\n    提取`.rar`文件后，双击`.uproject`文件。你会看到一个提示询问`Would you like to rebuild now?`。点击那个提示上的`Yes`，这样它就可以构建必要的中间文件，之后它应该会在虚幻编辑器中自动打开项目。* \n\n *通过完成本练习，您现在能够理解如何使用蓝图扩展 C++ 代码，以及为什么这在许多情况下对开发人员有利。您还学习了如何添加输入映射，以及如何使用它们来驱动与玩家相关的输入逻辑。\n\n在本章的活动中，您将结合从本章之前的练习中获得的技能，并扩展您在*活动 2.01 中完成的项目，将动画链接到*第 2 章*、*的角色*活动，使用虚幻引擎*。这将允许您在自己创建的蓝图上进行构建，并查看它如何映射到现实场景。\n\n## 活动 3.01:在动画项目中用蓝图扩展 C++ 角色类\n\n现在，您已经创建了一个 C++ 类，并使用蓝图对其进行了扩展，现在是时候在现实场景中将这两个概念结合起来了。在这个活动中，你的目的是让我们的角色从*活动 2.01* 、 *Mixamo 角色动画*，可以在*第二章，* *使用虚幻引擎，*使用键盘上的*空格键*键跳转。但是，您需要在 C++ 中从头开始创建`Character`类，然后稍后用蓝图扩展它以达到最终目标。\n\n以下步骤将帮助您完成本活动:\n\n1.  从*活动 2.01* 、 *Mixamo 人物动画*开启项目。\n2.  用 C++ 创建一个`Character`类来初始化角色变量，包括与玩家相关的摄像机。\n3.  将跳转输入映射到项目设置中的*空格键*键。\n4.  Extend the created C++ class with a blueprint to add the associated assets and jump functionality.\n\n    预期产出:\n\n    当你按下*空格键*键时，角色应该能够跳跃。该级别应该使用扩展 C++ `Character`类的蓝图:\n\n    ![Figure 3.14: Ganfault jump activity expected output ](img/B16183_03_14.jpg)\n\n图 3.14: Ganfault 跳转活动预期输出\n\n注意\n\n这个活动的解决方案可以在:[https://packt.live/338jEBx](https://packt.live/338jEBx)找到。\n\n通过完成本练习，您已经了解了在蓝图中扩展 C++ 代码以实现功能和逻辑的场景。这种 C++ 和蓝图的结合是游戏开发人员拥有的最强大的工具，可以在虚幻引擎中创建精湛而独特的游戏。\n\n# 总结\n\n在本章中，您学习了如何创建一个 C++ `Character`类，向其中添加初始化器代码，然后使用蓝图对其进行扩展以设置资产并添加附加代码。\n\n结果遵循 C++ 代码以及蓝图代码，并且可以在任何有目的的场景中使用。\n\n您还学习了如何设置映射到 *W* 、 *A* 、 *S* 和 *D* 键的轴映射来移动玩家(这是许多游戏中的默认移动映射)。你还学会了如何让角色在游戏中跳跃。\n\n在下一章中，您将深入探讨输入映射以及如何在虚幻编辑器中使用移动预览器。这将有助于您创建具有映射到游戏和玩家逻辑的可靠输入的游戏。它还将允许你快速测试你的游戏在手机上的外观和感觉，所有这些都在虚幻编辑器中。*"
  },
  {
    "path": "docs/game-dev-proj-ue/04.md",
    "content": "# 四、玩家输入\n\n概述\n\n本章将讨论玩家输入的主题。我们将学习如何将支持触摸的设备的按键或触摸输入与游戏中的动作(如跳跃或移动)相关联。\n\n本章结束时，您将了解`Action Mappings`和`Axis Mappings`，如何创建和修改它们，如何收听这些映射中的每一个，如何在按下和释放它们时执行游戏中的动作，以及如何像在移动设备上玩游戏一样预览您的游戏。\n\n# 简介\n\n在前一章中，我们创建了从`Character`类继承的 C++ 类，并添加了所有必要的`Actor`组件，以便能够从角色的角度看到游戏，以及能够看到角色本身。然后我们创建了一个继承自那个 C++ 类的`Blueprint`类，以便可视化地设置它所有必要的组件。我们还简要了解了动作和轴映射。\n\n在本章中，我们将更深入地讨论这些主题，并介绍它们的 C++ 用法。我们将了解玩家输入在 UE4 中是如何工作的，引擎如何处理输入事件(*键的按下和释放*)，以及如何在我们的游戏中使用它们来控制逻辑。\n\n让我们从了解 UE4 如何抽象玩家按下的按键来开始这一章，以便更容易地通知您这些事件。\n\n注意\n\n在本章中，我们将使用我们在上一章中创建的`Character`蓝图的替代版本，称为`BP_MyTPC`。本章的版本将有默认的 UE4 人体模型网格，而不是来自 Mixamo 的网格。\n\n# 输入动作和轴\n\n玩家输入是视频游戏区别于其他娱乐媒体的地方:它们是交互式的。视频游戏要想互动，必须考虑玩家的输入。许多游戏通过允许玩家控制一个虚拟角色来实现这一点，这个虚拟角色根据玩家按下的键和按钮来作用于它所在的虚拟世界，这正是我们在本章中要做的。\n\n现在大多数游戏开发工具都允许你将按键抽象成动作和轴，这允许你将一个名字(例如，*跳跃*)与几个不同的玩家输入(按下一个按钮，轻弹拇指棒，等等)相关联。“动作”和“轴”的区别在于“动作”用于二进制输入(可以按下或释放的输入，如键盘上的键)，而“轴”用于标量或连续的输入(即，可以有一个值范围，如拇指棒，可以在 *x* 和 *y* 轴上从`–1`到`1`。\n\n例如，如果你正在制作一个赛车游戏，你越往下拉游戏手柄的右触发按钮，汽车加速越快，这将是一个`Axis`，因为它的值可以从`0`到`1`不等。然而，如果你想让玩家暂停游戏，那将是一个动作，因为它只需要知道玩家是否按下了某个键。\n\n通常，当玩家明确按下*空格键*键时让玩家角色跳跃并不是一个很好的主意，而是当按下*跳跃*动作时让玩家跳跃。这个*跳跃*动作可以在其他地方编辑相关的键，这样开发者和玩家都可以很容易地改变哪个键导致玩家角色跳跃。这就是 UE4 允许你指定玩家输入事件的方式(虽然你也可以听到明确的按键，但这通常不是最好的方式)。\n\n打开你的 UE4 项目，进入`Project Settings`窗口。您可以点击编辑器左上角的`Edit`然后选择`Project Settings…`，或者点击编辑器`Toolbar`中的`Settings`然后选择`Project Settings…`\n\n此窗口将允许您在各种类别中修改与项目相关的几个设置。如果您向下滚动`Project Settings`的左边缘，您应该会在`Engine`类别下找到`Input`选项，该选项将带您进入项目的输入设置。单击此选项。\n\n当您这样做时，您应该会在窗口的右边看到输入设置，在这里您可以访问项目的`Action Mappings`和`Axis Mappings`，以及其他内容:\n\n![Figure 4.1: The Action and Axis Mappings available in the Input settings window ](img/B16183_04_01.jpg)\n\n图 4.1:输入设置窗口中可用的动作和轴映射\n\n`Action Mappings`属性允许您指定项目中的动作列表(例如，*跳转*动作)及其对应的键(例如，*空格键*键)。\n\n`Axis Mappings`允许您做同样的事情，但是对于没有二进制值(按下或释放)而是具有连续值的按键，比如控制器上的拇指棒，其值可以从`–1`到 *x* 和 *y* 轴上的`1`，或者控制器上的触发按钮，其值可以从`0`到`1`。\n\n例如，考虑一个 Xbox One 控制器，它可以分为以下几部分:\n\n*   `Left analog stick`，通常用于游戏中控制移动\n*   `Dpad`，可用于控制运动，也有多种其他用途\n*   `Right analog stick`，通常用于控制摄像头和查看视角\n*   `Face buttons (X, Y, A, and B)`，根据游戏可以有多种用途，但通常允许玩家在游戏世界中执行动作\n*   `Bumpers and Triggers (LB, RB, LT, and RT)`，可用于瞄准射击或加速制动等动作\n\n如果愿意，也可以将二进制键设置为轴；例如，为游戏手柄拇指棒(其值从`–1`到`1`的连续键)和键盘上的两个二进制键( *W* 和 *S* )设置玩家角色的移动。\n\n在本章中，我们将了解如何做到这一点。\n\n当我们在*第一章*、*虚幻引擎介绍*中生成`Third Person`模板项目时，它附带了一些已经配置好的输入，分别是 *W* 、 *A* 、 *S* 、 *D* 键，还有运动的`left thumbstick`和跳跃的`Space Bar`键、`gamepad bottom face`按钮。\n\n现在让我们在下一个练习中添加新的`Action`和`Axis Mappings`。\n\n## 练习 4.01:创建跳跃动作和移动轴\n\n在本练习中，我们将为*跳跃*动作添加一个新的`Action Mapping`，并为*运动*动作添加几个新的`Axis Mappings`。\n\n为此，请遵循以下步骤:\n\n1.  打开`Input Settings`菜单。\n2.  Press the `+` icon to the right of the `Action Mappings` property to create a new `Action Mapping`:\n\n    ![Figure 4.2: Adding a new Action Mapping ](img/B16183_04_02.jpg)\n\n    图 4.2:添加新的动作映射\n\n3.  When you do so, you should see a new `Action Mapping` called `NewActionMapping_0` mapped to the `None` key (*meaning it's not mapped to any key*):\n\n    ![Figure 4.3: The default settings of a new Action Mapping ](img/B16183_04_03.jpg)\n\n    图 4.3:新动作映射的默认设置\n\n4.  Change the name of this mapping to `Jump` and the key associated with it to the `Spacebar` key.\n\n    要更改映射到此操作的键，您可以单击当前设置为`None`键的下拉属性，键入`Space Bar`，并选择第一个选项:\n\n    ![Figure 4.4: The key drop-down menu (top) where the Space  Bar key is being selected (bottom) ](img/B16183_04_04.jpg)\n\n    图 4.4:选择空格键的键下拉菜单(顶部)(底部)\n\n5.  You can specify whether or not you want this action to be executed when the player presses the specified key while holding one of the modifier keys – `Shift`, `Ctrl`, `Alt`, or `Cmd`, by checking each of their appropriate checkboxes. You can also remove this key from this `Action Mapping` by clicking the `X` icon:\n\n    ![Figure 4.5: The key drop-down menu and the options to specify modifier keys  and removing this key from this Action Mapping ](img/B16183_04_05.jpg)\n\n    图 4.5:键下拉菜单以及指定修饰键和从动作映射中移除该键的选项\n\n6.  To add a new key to an `Action Mapping`, you can simply click the `+` icon next to the name of that `Action Mapping`, and to remove an `Action Mapping` altogether, you can click the `x` icon next to it:\n\n    ![Figure 4.6: The name of the Action Mapping, with the + and x icons next to it ](img/B16183_04_06.jpg)\n\n    图 4.6:动作映射的名称，旁边有+和 x 图标\n\n    现在让我们用一个控制器按钮映射到这个`Action Mapping`。\n\n    因为大多数游戏手柄在非常相似的地方有相同的按键，所以 UE4 使用`Gamepad`前缀将它们的大部分按键抽象为通用术语。\n\n7.  Add a new key to this `Action Mapping` and set that new key to be the `Gamepad Face Button Bottom` key. If you're using an Xbox controller, this will be the `A` button, and if you're using a PlayStation controller, this will be the `X` button:\n\n    ![Figure 4.7: The Gamepad Face Button Bottom key added to the Jump Action Mapping ](img/B16183_04_07.jpg)\n\n    图 4.7:游戏手柄面板按钮底部按键添加到跳跃动作映射中\n\n    现在我们已经建立了我们的`Jump` `Action Mapping`，让我们建立我们的`Movement Axis Mapping`。\n\n8.  Click the `+` icon next to the `Axis Mappings` property to add a new `Axis Mapping`. This new `Axis Mapping` will be used to move the character left and right. Name it `MoveRight` and assign to it the `Gamepad Left Thumbstick X-Axis` key, so that the player can use the *x* axis of the left thumbstick to move the character left and right:\n\n    ![Figure 4.8: The MoveRight Axis Mapping with the Gamepad Left  Thumbstick X-Axis key associated with it ](img/B16183_04_08.jpg)\n\n    图 4.8:带有游戏手柄左指杆 X 轴键的移动右轴映射\n\n    如果你看向我们指定的键的右边，而不是修饰键，你应该会看到那个键的`Scale`属性。此属性将允许您反转轴，以便当玩家向右倾斜拇指操纵杆时，玩家向左移动，反之亦然，同时增加或减少轴的灵敏度。\n\n    为了允许玩家使用键盘键左右移动(键盘键可以是按下的，也可以是释放的，并且没有连续的值，不像拇指棒)，我们必须在它们的刻度上添加两个具有反转值的键。\n\n    在这个`Axis Mapping,`上再加两个键，第一个是`D`键，`1`的`Scale`，第二个是`A`键，`–1`的`Scale`。这将导致角色在玩家按下`D`键时向右移动，在玩家按下`A`键时向左移动:\n\n    ![Figure 4.9: The MoveRight Axis Mapping with both the Gamepad and keyboard keys ](img/B16183_04_09.jpg)\n\n    图 4.9:带有游戏手柄和键盘键的右轴映射\n\n9.  After doing this, add another `Axis Mapping` with the name of `MoveForward` with the `Gamepad Left Thumbstick Y-Axis`, `W`, and `S` keys, the latter having a `Scale` of `–1`. This axis will be used to move the character forward and backward:\n\n    ![Figure 4.10: The MoveForward Axis Mapping ](img/B16183_04_10.jpg)\n\n图 4.10:前进轴映射\n\n随着这些步骤的完成，我们已经完成了本章的第一个练习，您已经学习了如何在 UE4 中指定`Action`和`Axis` `Mappings`，允许您抽象哪些键负责哪些游戏中的动作。\n\n现在让我们来看看 UE4 是如何处理玩家输入并在游戏中进行处理的。\n\n# 处理玩家输入\n\n让我们考虑一种情况，玩家按下*跳跃*动作，该动作与`Spacebar`键相关联，让玩家角色跳跃。从玩家按下`Spacebar`键的那一刻到游戏让玩家角色跳跃的那一刻，相当多的事情不得不将这两个事件联系起来。\n\n让我们看看从一个事件到另一个事件的所有必要步骤:\n\n1.  `Hardware Input`:玩家按下`Spacebar`键。UE4 将会听到这个按键事件。\n2.  `PlayerInput`类:按键被按下或释放后，该类会将该按键转化为动作或轴。如果有一个动作或轴与该键相关联，它将通知所有正在监听该动作的类它刚刚被按下、释放或更新。在这种情况下，它会知道`Spacebar`键与*跳跃*动作相关联。\n3.  `Player Controller`类:这是第一个接收这些事件的类，因为它被用来代表游戏中的玩家。\n4.  `Pawn`类:这个类(以及继承它的`Character`类)也可以监听那些事件，只要它们被玩家控制器拥有。如果是，它将在该类之后接收这些事件。在本章中，我们将使用我们的`Character` C++ 类来收听动作和轴事件。\n\n现在我们知道了 UE4 是如何处理玩家输入的，让我们来看看`DefaultInput.ini`文件以及它是如何工作的。\n\n# 默认输入.ini\n\n如果你使用文件浏览器进入你的项目目录，然后打开它的`Config`文件夹，你会在里面找到一些`.ini`文件，其中一个应该是`DefaultInput.ini`文件。顾名思义，这个文件保存了输入相关属性的主要设置和配置。\n\n在本章的第一个练习中，我们编辑了项目的`Input`设置，实际上发生的是编辑器正在写入和读取`DefaultInput.ini`文件。\n\n在您选择的文本编辑器中打开此文件。它包含了很多属性，但是我们现在要看的是`Action Mappings`和`Axis Mappings`的列表。在文件的末尾，您应该会看到，例如，在该文件中指定了*跳转*动作:\n\n```cpp\n+ActionMappings=(ActionName=\"Jump\",bShift=False,bCtrl=False,  bAlt=False,bCmd=False,Key=SpaceBar)\n+ActionMappings=(ActionName=\"Jump\",bShift=False,bCtrl=False,  bAlt=False,bCmd=False,Key=Gamepad_FaceButton_Bottom)\n```\n\n您还可以看到指定了一些轴，例如`MoveRight`轴:\n\n```cpp\n+AxisMappings=(AxisName=\"MoveRight\",Scale=1.000000,  Key=Gamepad_LeftX)\n+AxisMappings=(AxisName=\"MoveRight\",Scale=1.000000,Key=D)\n+AxisMappings=(AxisName=\"MoveRight\",Scale=-1.000000,Key=A)\n```\n\n您可以直接编辑该文件来添加、修改和删除`Action Mappings`和`Axis Mappings`，而不是编辑项目的`Input Settings`，尽管这不是一种非常用户友好的方式。请记住，当您将项目打包为可执行文件时，该文件也将可用，这意味着玩家将能够根据自己的喜好编辑该文件。\n\n现在让我们看看在下一个练习中如何用 C++ 听`Action Mappings`和`Axis Mappings`。\n\n## 练习 4.02:听动作和轴\n\n在本练习中，我们将通过使用 C++ 将那些动作和轴绑定到我们的角色类中的特定函数，向角色类注册我们在上一节中创建的动作和轴。\n\n要让`Player Controller`或`Character`收听动作和轴，主要方法是使用`SetupPlayerInputComponent`功能注册`Action`和`Axis`代表。`MyThirdPersonChar`类应该已经有了这个函数的声明和实现。让我们的角色类按照以下步骤来听这些事件:\n\n1.  在 Visual Studio 中打开`MyThirdPersonChar`类头文件，确保有一个名为`SetupPlayerInputComponent`的`protected`函数的声明，该函数不返回任何内容，并接收一个`class UInputComponent* PlayerInputComponent`属性作为参数。该功能应标记为`virtual`和`override` :\n\n    ```cpp\n    virtual void SetupPlayerInputComponent(class UInputComponent*   PlayerInputComponent) override;\n    ```\n\n2.  打开这个类的源文件，确保这个函数有一个实现:\n\n    ```cpp\n    void AMyThirdPersonChar::SetupPlayerInputComponent(class   UInputComponent* PlayerInputComponent)\n    {\n    }\n    ```\n\n3.  在其实现中，首先调用`PlayerInputComponent`属性的`BindAction`函数。该功能将允许该类监听特定的动作，在本例中为`Jump`动作。它接收以下参数:\n    *   `FName ActionName`–我们想听的动作名称；在我们的例子中，`Jump`动作。\n    *   `EInputEvent InputEvent`–我们想听的具体按键事件，可以按下、释放、双击等等。在我们的例子中，我们想听按下的事件，我们可以通过使用`IE_Pressed`值来指定。\n    *   `UserClass* Object`–回调函数将被调用的对象；在我们的例子中，`this`指针。\n    *   `FInputActionHandlerSignature::TUObjectMethodDelegate< UserClass >::FMethodPtr Func` – This property is a bit wordy, but is essentially a pointer to the function that will be called when this event happens, which we can specify by typing `&` followed by the class's name, followed by `::`, followed by the function's name. In our case, we want this to be the existing `Jump` function belonging to the `Character` class, so we'll specify it with `&ACharacter::Jump`:\n\n        ```cpp\n        PlayerInputComponent->BindAction(\"Jump\", IE_Pressed, this,   &ACharacter::Jump);\n        ```\n\n        注意\n\n        所有用来监听动作的函数必须不接收参数，除非你使用`Delegates`，这不在本书的讨论范围之内。\n\n4.  为了告诉我们的角色停止跳跃，您必须复制这一行，然后将新行的输入事件更改为`IE_Released`，并且调用的函数改为`Character`类的`StopJumping`函数:\n\n    ```cpp\n    PlayerInputComponent->BindAction(\"Jump\", IE_Released, this,   &ACharacter::StopJumping);\n    ```\n\n5.  因为我们将使用`InputComponent`类，所以我们需要包含它:\n\n    ```cpp\n    #include \"Components/InputComponent.h\"\n    ```\n\n6.  现在我们正在听`Jump`动作，并且在执行该动作时让角色跳跃，让我们继续它的移动。在类的头文件中，添加一个名为`MoveRight`的`protected`函数的声明，该函数不返回任何内容，并接收一个`float Value`参数。这是当`MoveRight`轴的值更新时将调用的函数:\n\n    ```cpp\n    void MoveRight(float Value);\n    ```\n\n7.  在类的源文件中，添加这个函数的实现，我们将从检查`Controller`属性是否有效(不是`nullptr`)以及`Value`属性是否与`0` :\n\n    ```cpp\n    void AMyThirdPersonChar::MoveRight(float Value)\n    {\n      if (Controller != nullptr && Value != 0.0f)\n      {\n      }\n    }\n    ```\n\n    不同开始\n8.  如果这两个条件都成立，我们将希望使用`AddMovementInput`功能移动我们的角色。此函数的参数之一是您希望角色移动的方向。要计算这个方向，我们需要做两件事:\n    *   Get the camera's rotation on the *z* axis (yaw), so that we move the character relative to where the camera is looking. To achieve this, we can create a new `FRotator` property with a value of `0` for pitch (rotation along the *y* axis) and roll (rotation along the *x* axis) and the value of the camera's current yaw for the property's yaw. To get the camera's yaw value, we can call the Player Controller's `GetControlRotation` function and then access its `Yaw` property:\n\n        ```cpp\n        const FRotator YawRotation(0, Controller->  GetControlRotation().Yaw, 0);\n        ```\n\n        注意\n\n        `FRotator`属性的构造函数接收`Pitch`值，然后是`Yaw`值，然后是`Roll`值。\n\n    *   Get the resulting rotation's right vector and store it in an `FVector Direction` property. You can get a rotator's Right Vector by calling the `KistemMathLibrary` object's `GetRightVector` function. A rotator or vector's right vector is simply its perpendicular vector that points to its right. The result of this will be a vector that points to the right of where the camera is currently facing:\n\n        ```cpp\n        const FVector Direction =   UKismetMathLibrary::GetRightVector(YawRotation);\n        ```\n\n        我们现在可以调用`AddMovementInput`函数，将`Direction`和`Value`属性作为参数传递:\n\n        ```cpp\n        AddMovementInput(Direction, Value);\n        ```\n\n9.  因为我们将同时使用`KismetMathLibrary`和`Controller`对象，我们需要将它们包含在这个源文件的顶部:\n\n    ```cpp\n    #include \"Kismet/KismetMathLibrary.h\"\n    #include \"GameFramework/Controller.h\"\n    ```\n\n10.  After listening to the `Jump` action, inside this class's `SetupPlayerInputComponent` function, listen to the `MoveRight` axis by calling the `PlayerInputComponent` property's `BindAxis` function. This function is used to listen to an Axis instead of an Action, and the only difference between its parameters and the `BindAction` function's parameters is that it doesn't need to receive an `EInputState` parameter. Pass as parameters to this function `\"MoveRight\"`, the `this` pointer, and this class's `MoveRight` function:\n\n    ```cpp\n    PlayerInputComponent->BindAxis(\"MoveRight\", this,   &AMyThirdPersonChar::MoveRight);\n    ```\n\n    注意\n\n    所有用于监听轴的函数都必须接收一个`float`属性作为参数，除非您使用`Delegates`，这不在本书的讨论范围内。\n\n    现在我们来听听这节课的`MoveForward`轴:\n\n11.  在类的头文件中，添加一个类似于`MoveRight`函数的声明，但将其命名为`MoveForward`而不是:\n\n    ```cpp\n    void MoveForward(float Value);\n    ```\n\n12.  在类的源文件中，给这个新的`MoveForward`函数添加一个实现。将`MoveRight`函数的实现复制到这个新的实现中，但是将对`KismetMathLibrary`对象的`GetRightVector`函数的调用替换为对其`GetForwardVector`函数的调用。这将使用代表摄像机正对的方向的矢量，而不是它的右矢量，它正对着它的右侧:\n\n    ```cpp\n    void AMyThirdPersonChar::MoveForward(float Value)\n    {\n      if (Controller != nullptr && Value != 0.0f)\n      {\n        const FRotator YawRotation(0, Controller->  GetControlRotation().Yaw, 0);\n        const FVector Direction = UKismetMathLibrary::GetForwardVector(YawRotation);\n        AddMovementInput(Direction, Value);\n      }\n    }\n    ```\n\n13.  在`SetupPlayerInputComponent`函数的实现中，复制监听`MoveRight`轴的代码行，用`\"MoveForward\"`替换第一个参数，用指向`MoveForward`函数的指针替换最后一个参数:\n\n    ```cpp\n    PlayerInputComponent->BindAxis(\"MoveForward\", this,   &AMyThirdPersonChar::MoveForward);\n    ```\n\n14.  现在编译你的代码，打开编辑器，打开你的`BP_MyTPS`蓝图资产。删除`InputAction Jump`事件，以及与之连接的节点。对`InputAxis MoveForward`和`InputAxis MoveRight`事件也是如此。我们将在 C++ 中复制这个逻辑，并且需要移除它的蓝图功能，以便在处理输入时没有冲突。\n15.  Now, play the level. You should be able to move the character using the keyboard's `W,` `A`, `S`, and `D` keys or the controller's left thumbstick, as well as jumping with the `Spacebar` key or `gamepad face button bottom`:\n\n    ![Figure 4.11: The player character moving  ](img/B16183_04_11.jpg)\n\n图 4.11:玩家角色移动\n\n完成所有这些步骤后，您就完成了本练习。现在你知道如何在 UE4 中使用 C++ 收听`Action`和`Axis`事件了。\n\n注意\n\n您可以使用`PlayerInputComponent`属性的`BindKey`功能来收听特定的按键，而不是收听特定的`Action`或`Axis`。该函数接收与`BindAction`函数相同的参数，除了第一个参数，它应该是一个键而不是`FName`。您可以使用`EKeys`枚举后跟`::`来指定键。\n\n现在，我们已经设置了让角色移动和跳跃所需的所有逻辑，让我们添加负责围绕角色旋转相机的逻辑。\n\n# 围绕角色转动摄像机\n\n相机是游戏中极其重要的一部分，因为它们决定了玩家在整个游戏过程中将看到什么以及如何看到你的游戏。说到第三人称游戏，这个项目就是这种情况，摄像头不仅能让你看到他们周围的世界，还能看到你控制的角色。无论角色是受到伤害、摔倒还是其他什么，玩家都必须知道他们控制的角色的状态，并且能够让相机朝向他们选择的方向，这一点很重要。\n\n就像每一个现代的第三人称游戏一样，我们总是让摄像机围绕我们的玩家角色旋转。为了让我们的相机围绕我们的角色旋转，在*第 2 章*、*中设置了`Camera`和`Spring Arm`组件并使用虚幻引擎*后，让我们继续添加两个新的`Axis Mappings`，第一个名为`Turn`，与`Gamepad Right Thumbstick X-Axis`和`MouseX`键相关联，第二个名为`LookUp`，与`Gamepad Right Thumbstick Y-Axis`和`MouseY`键相关联，后一个键的刻度为`-1`。\n\n这些`Axis Mappings`将分别用于让玩家左右看和上下看:\n\n![Figure 4.12: The Turn and LookUp Axis Mappings ](img/B16183_04_12.jpg)\n\n图 4.12:转向和查找轴映射\n\n现在让我们添加负责用玩家的输入转动相机的 C++ 逻辑。\n\n转到`MyThirdPersonChar`类的`SetupPlayerInputComponent`功能实现，复制负责监听`MoveRight`轴或`MoveForward`轴的线两次。在第一个复制行中，将第一个参数更改为`\"Turn\"`，最后一个参数更改为`Pawn`类的`AddControllerYawInput`功能，而第二个复制行的第一个参数应为`\"LookUp\"`，最后一个参数应为`Pawn`类的`AddControllerPitchInput`功能。\n\n这两个功能分别负责围绕 *z* (左右转动)和 *y* (上下看)轴添加旋转输入:\n\n```cpp\nPlayerInputComponent->BindAxis(\"Turn\", this,   &APawn::AddControllerYawInput);\nPlayerInputComponent->BindAxis(\"LookUp\", this,   &APawn::AddControllerPitchInput);\n```\n\n如果您编译了本节中所做的更改，打开编辑器并播放关卡，现在您应该能够通过旋转鼠标或倾斜控制器的右指杆来移动摄像机:\n\n![Figure 4.13: The camera is rotated around the player ](img/B16183_04_13.jpg)\n\n图 4.13:相机围绕播放器旋转\n\n这就结束了根据玩家的输入围绕玩家角色旋转相机的逻辑。在下一个练习中，我们将广泛关注安卓和 iOS 等移动平台的话题。\n\n## 移动平台\n\n由于最近技术的进步，大多数人现在可以使用智能手机和平板电脑等价格合理的移动设备。这些设备虽然很小，但仍然有相当大的处理能力，现在可以做笔记本电脑和台式电脑等大型设备做的许多事情。其中之一就是玩电子游戏。\n\n因为移动设备比其他可以玩视频游戏的设备更实惠、功能更丰富，所以有很多人在上面玩游戏。因此，值得考虑为安卓和 iOS 这两大移动应用商店等移动平台开发视频游戏。\n\n现在让我们在下一个练习中看看如何在虚拟移动设备上预览我们的游戏。\n\n## 练习 4.03:手机预览\n\n在本练习中，我们将使用`Mobile Preview`来玩我们的游戏，看看在移动设备上玩我们的游戏是什么感觉。在此之前，我们必须进入`Android Platform`设置。\n\n看看下面的步骤:\n\n1.  Open the `Project Settings` window and scroll down its left edge until you find the `Android` option beneath the `Platforms` category. Click that option. You should see the following to the right of the categories:\n\n    ![Figure 4.14: The Android Platform window warning that the project  currently isn’t configured for that platform ](img/B16183_04_14.jpg)\n\n    图 4.14:安卓平台窗口警告项目当前没有为该平台配置\n\n2.  This warning is letting you know that the project has not yet been configured for Android. To change that, click the `Configure Now` button inside the *red warning*. When you do, it should be turned into a green warning, letting you know that the platform is configured:\n\n    ![Figure 4.15: The Android platform window notifying you that the project  is configured for this platform ](img/B16183_04_15.jpg)\n\n    图 4.15:安卓平台窗口，通知您项目是为此平台配置的\n\n3.  After you've done this, you can close `Project Settings`, click the arrow next to the `Play` button in the editor's toolbar, and select the `Mobile Preview` option you see available:\n\n    ![Figure 4.16: The Mobile Preview option under the Play button  ](img/B16183_04_16.jpg)\n\n图 4.16:播放按钮下的移动预览选项\n\n这将导致引擎开始加载这个预览，以及编译所有必要的着色器，这应该需要几分钟的时间。\n\n完成后，您应该会看到以下内容:\n\n![Figure 4.17: The Mobile Preview window playing the game as if on an Android device  ](img/B16183_04_17.jpg)\n\n图 4.17:移动预览窗口像在安卓设备上一样玩游戏\n\n这个预览看起来应该类似于编辑器中的普通预览，但有几个显著的区别:\n\n*   视觉逼真度降低了。因为移动平台不具备与个人电脑和游戏机相同的计算能力，所以考虑到这一点，视觉质量会降低。除此之外，高端平台提供的一些渲染功能在移动平台中根本不受支持。\n*   Two added virtual joysticks at the *lower-left* and *lower-right* corner of the screen, which work similarly to those of a controller, where the left joystick controls the character's movement and the right joystick controls the camera's rotation.\n\n    此窗口充当移动屏幕，其中鼠标是您的手指，因此如果您使用鼠标左键按住左操纵杆，然后拖动它，这将导致操纵杆在屏幕上移动，从而使角色移动，如下图所示:\n\n    ![Figure 4.18: The character is moved using the left virtual joystick  ](img/B16183_04_18.jpg)\n\n图 4.18:使用左虚拟操纵杆移动角色\n\n就这样，我们通过学习如何在安卓移动平台上预览我们的游戏并验证其输入是否正常来结束这一章。\n\n现在让我们跳到下一个练习，我们将添加触摸输入，使玩家角色跳跃。\n\n## 练习 4.04:添加触摸屏输入\n\n在本练习中，我们将继续我们之前的练习，使玩家角色在触摸屏设备上玩游戏时，当玩家点击屏幕时开始跳跃。\n\n要将触摸屏输入添加到我们的游戏中，请按照以下步骤操作:\n\n1.  转到`MyThirdPersonChar`类的头文件，为不返回任何内容并接收`ETouchIndex::Type FingerIndex`和`FVector Location`参数的受保护函数添加两个声明，第一个声明指示触摸屏幕的手指的索引(无论是第一个、第二个还是第三个触摸屏幕的手指)，第二个声明指示屏幕上被触摸的位置。说出其中一个功能`TouchBegin`和另一个功能`TouchEnd` :\n\n    ```cpp\n    void TouchBegin(ETouchIndex::Type FingerIndex, FVector Location);\n    void TouchEnd(ETouchIndex::Type FingerIndex, FVector Location);\n    ```\n\n2.  在`MyThirdPersonChar`类的源文件中，添加这两个函数的实现，其中`TouchBegin`函数将调用`Jump`函数，`TouchEnd`函数将调用`StopJumping`函数。这将导致我们的角色在玩家触摸屏幕时开始跳跃，在他们停止触摸屏幕时停止跳跃:\n\n    ```cpp\n    void AMyThirdPersonChar::TouchBegin(ETouchIndex::Type   FingerIndex, FVector Location)\n    {\n      Jump();\n    }\n    void AMyThirdPersonChar::TouchEnd(ETouchIndex::Type   FingerIndex, FVector Location)\n    {\n      StopJumping();\n    }\n    ```\n\n3.  转到`SetupPlayerInputComponent`函数的实现，对`PlayerInputComponent`的`BindTouch`函数添加两个调用，将屏幕被触摸的事件绑定到一个函数。除了第一个参数`ActionName`之外，该函数接收与`BindAction`函数相同的参数。在第一次函数调用中，传递输入事件`IE_Pressed`、`this`指针和该类的`TouchBegin`函数作为参数，在第二次调用中，传递输入事件`IE_Released`、`this`指针和该类的`TouchEnd`函数:\n\n    ```cpp\n    PlayerInputComponent->BindTouch(IE_Pressed, this,   &AMyThirdPersonChar::TouchBegin);\n    PlayerInputComponent->BindTouch(IE_Released, this,   &AMyThirdPersonChar::TouchEnd);\n    ```\n\n4.  Preview the game using `Mobile Preview`, just like we did in the previous exercise. If you use the left mouse button to click the middle of the screen, the player character should jump:\n\n    ![Figure 4.19: The character jumping after clicking the middle of the screen ](img/B16183_04_19.jpg)\n\n图 4.19:点击屏幕中间后的角色跳跃\n\n由此，我们得出结论，如果玩家在触摸屏设备上玩，只要玩家触摸屏幕，我们的角色就会跳跃。现在，我们已经学习了如何向游戏中添加输入，并将这些输入与游戏中的动作(如跳跃和移动玩家角色)相关联，让我们在下一个活动中从头到尾为我们的游戏添加一个新的`Walk`动作，从而巩固我们在本章中学到的内容。\n\n## 活动 4.01:为我们的角色添加行走逻辑\n\n在当前游戏中，当我们使用移动键时，我们的角色默认运行，但是我们需要降低角色的速度并使其行走。\n\n因此，在本练习中，我们将添加逻辑，当我们按住键盘上的`Shift`键或`Gamepad Face Button Right`键(Xbox 控制器为`B`，PlayStation 控制器为`O`)移动角色时，会使角色行走。此外，我们还将在移动平台上预览它。\n\n为此，请遵循以下步骤:\n\n1.  通过`Project Settings`窗口打开`Input Settings`。\n2.  添加一个名为`Walk`的新`Action Mapping`，并将其与`Left Shift`和`Gamepad Face Button Right`键相关联。\n3.  打开`MyThirdPersonChar`类的头文件，为两个不返回任何内容也不接收任何参数的`protected`函数添加声明，这两个函数分别叫做`BeginWalking`和`StopWalking`。\n4.  在类的源文件中添加这两个函数的实现。在`BeginWalking`功能的实现中，通过相应地修改`CharacterMovementComponent`属性的`MaxWalkSpeed`属性，将角色的速度更改为其值的 40%。要访问`CharacterMovementComponent`属性，请使用`GetCharacterMovement`功能。\n5.  `StopWalking`功能的实现将与`BeginWalking`功能的实现相反，这将使角色的行走速度提高 250%。\n6.  Bind the `Walk` action to the `BeginWalking` function when that action is pressed, and to the `StopWalking` function when it is released.\n\n    完成这些步骤后，您应该可以通过按住键盘的*左移位*键或控制器的*面部按钮右*按钮来让您的角色行走，这会降低其速度并略微改变其动画。\n\n    ![Figure 4.20: The character running (left) and walking (right) ](img/B16183_04_20.jpg)\n\n    图 4.20:角色奔跑(左)和行走(右)\n\n7.  Let's now preview our game on a mobile platform, as we did in *Exercise 4.03*, *Previewing on Mobile*, and drag the left analog stick just slightly to get our character to walk slowly. The result should look similar to the following screenshot:\n\n    ![Figure 4.21: The character walking in the mobile preview ](img/B16183_04_21.jpg)\n\n图 4.21:移动预览中的人物行走\n\n我们的活动到此结束。只要玩家保持`Walk`动作，我们的角色现在应该可以慢慢行走了。\n\n注意\n\n这个活动的解决方案可以在:[https://packt.live/338jEBx](https://packt.live/338jEBx)找到。\n\n# 总结\n\n在本章中，您学习了如何添加、移除和修改`Action Mappings`和`Axis Mappings`，这为您在确定哪些键触发特定动作或轴、如何聆听它们以及当它们被按下和释放时如何执行游戏内逻辑时提供了一些灵活性。\n\n既然你知道如何处理玩家的输入，你就可以允许玩家与你的游戏互动，并提供视频游戏如此出名的代理。\n\n在下一章，我们将从零开始制作我们自己的游戏。它将被称为`Dodgeball`，由玩家控制一个试图逃离向其投掷躲避球的敌人的角色组成。在那一章中，我们将有机会开始学习许多重要的主题，重点是碰撞。"
  },
  {
    "path": "docs/game-dev-proj-ue/05.md",
    "content": "# 五、线条痕迹\n\n概述\n\n本章将是一个名为躲避球的新游戏项目的开始，我们将从头开始创建一个基于碰撞概念的力学游戏。在本章中，您将修改第三人称模板项目，使其具有自上而下的视角。然后，将向您介绍线条痕迹，这是游戏开发中的一个关键概念，并了解它们的潜力和用例。\n\n到本章结束时，您将能够通过执行不同类型的线条追踪来使用 UE4 的内置线条追踪功能(在其他游戏开发工具中也称为光线投射或光线追踪)；创建您自己的跟踪通道；以及修改对象对每个跟踪通道的响应。\n\n# 简介\n\n在前面的章节中，我们学习了如何重现虚幻引擎团队提供给我们的第三人称模板项目，以便理解 UE4 工作流程和框架的一些基本概念。\n\n在本章中，您将从头开始创建另一个游戏。在这个游戏中，玩家将从自上而下的角度控制一个角色(*类似于金属齿轮固体 1、2、3 等游戏*)。自上而下的视角意味着玩家控制一个角色，看起来就像被人看不起一样，通常摄像机的旋转是固定的(摄像机不旋转)。在我们的游戏中，玩家角色必须从 A 点到 B 点，而不会被遍布整个关卡的敌人扔向玩家的躲避球击中。这个游戏中的关卡本质上将是迷宫一样的，玩家将有多条路径可供选择，所有这些路径都将有敌人试图向玩家投掷躲避球。\n\n我们将在本章中讨论的具体主题是线跟踪(单个和多个)、扫描跟踪、跟踪通道和跟踪响应。第一节，我们先来了解一下*碰撞*在电竞世界里是什么。\n\n# 碰撞\n\n碰撞基本上是两个物体相互接触的点(例如，两个物体碰撞，一个物体撞到一个角色，一个角色走进一堵墙，等等)。大多数游戏开发工具都有自己的一套功能，允许碰撞和物理存在于游戏中。这组特征被称为**物理引擎**，负责与碰撞相关的一切。它负责执行线轨迹，检查两个对象是否相互重叠，阻止彼此的移动，从墙上反弹等等。当我们要求游戏执行或通知我们这些碰撞事件时，游戏本质上是要求物理引擎执行它，然后向我们显示这些碰撞事件的结果。\n\n在你将要构建的`Dodgeball`游戏中，需要考虑碰撞的例子包括检查敌人是否能够看到玩家(这将使用本章中介绍的线条追踪来实现)，在一个行为类似躲避球的物体上模拟物理，检查是否有任何东西阻挡玩家角色的移动，等等。\n\n碰撞是大多数游戏最重要的方面之一，因此理解它对于开始游戏开发至关重要。\n\n在我们开始构建基于碰撞的特性之前，我们首先需要建立新的`Dodgeball`项目，以便支持我们将要实现的游戏机制。这个过程从下一节描述的步骤开始:*项目设置*。\n\n# 项目设置\n\n让我们从创建我们的虚幻引擎项目开始这一章:\n\n1.  `Launch` UE4。选择`Games`项目类别，然后按`Next`。\n2.  选择`Third Person template`，然后按`Next`。\n3.  确保第一个选项设置为`C++ `而不是`Blueprint`。\n4.  Select the location of the project according to your preference and name your project `Dodgeball`, then press `Create Project`.\n\n    生成项目后，您应该会在屏幕上看到以下内容:\n\n    ![Figure 5.1: Dodgeball project loaded up ](img/B16183_05_01.jpg)\n\n    图 5.1:躲避球项目加载完毕\n\n5.  代码生成完成，项目打开后，关闭 UE4 编辑器，在 Visual Studio 中打开生成的第三人称角色类`DodgeballCharacter`的文件，如下图所示:\n\n![Figure 5.2: Files generated in Visual studio ](img/B16183_05_02.jpg)\n\n图 5.2:在 Visual studio 中生成的文件\n\n如前所述，您的项目将具有自上而下的视角。鉴于我们是从第三人称模板开始这个项目的，在我们把它变成一个自上而下的游戏之前，我们必须改变一些事情。这将主要涉及更改现有字符类中的一些代码行。\n\n## 练习 5.01:将躲避球角色转换为自上而下的视角\n\n在本练习中，您将对生成的`DodgeballCharacter`类进行必要的更改。请记住，它目前采用第三人称视角，角色的旋转由玩家的输入决定(*即鼠标或右模拟杆*)。\n\n在本练习中，您将把它改为自上而下的视角，无论玩家如何输入，视角都保持不变，相机总是从上方跟随角色。\n\n以下步骤将帮助您完成本练习:\n\n1.  前往`DodgeballCharacter`类的构造函数，更新`CameraBoom`属性，如以下步骤所述。\n2.  将`CameraBoom`的属性`TargetArmLength`改为`900.0f`，以增加摄像头与玩家之间的距离:\n\n    ```cpp\n    // The camera follows at this distance behind the character\n    CameraBoom->TargetArmLength = 900.0f;\n    ```\n\n3.  接下来，使用`SetRelativeRotation`功能，添加一条将相对音高设置为`-70`的线，这样摄像机就可以俯视玩家了。`FRotator`建造商的参数分别是*俯仰*、*偏航*和*滚转*:\n\n    ```cpp\n    //The camera looks down at the player\n    CameraBoom->SetRelativeRotation(FRotator(-70.f, 0.f, 0.f));\n    ```\n\n4.  将`bUsePawnControlRotation`更改为`false`，这样相机的旋转就不会被玩家的移动输入所改变:\n\n    ```cpp\n    // Don't rotate the arm based on the controller\n    CameraBoom->bUsePawnControlRotation = false;\n    ```\n\n5.  Add a line that sets `bInheritPitch`, `bInheritYaw`, and `bInheritRoll` to `false`, so that the camera's rotation isn't changed by the character's orientation:\n\n    ```cpp\n    // Ignore pawn's pitch, yaw and roll\n    CameraBoom->bInheritPitch = false;\n    CameraBoom->bInheritYaw = false;\n    CameraBoom->bInheritRoll = false;\n    ```\n\n    在我们做了这些修改之后，我们将移除角色的跳跃能力(我们不希望玩家那么容易从躲避球中逃脱)，并从玩家的旋转输入中旋转相机。\n\n6.  转到`DodgeballCharacter's`源文件中的`SetupPlayerInputComponent`功能，删除以下代码行，以删除跳转能力:\n\n    ```cpp\n    // REMOVE THESE LINES\n    PlayerInputComponent->BindAction(\"Jump\", IE_Pressed, this,   &ACharacter::Jump);\n    PlayerInputComponent->BindAction(\"Jump\", IE_Released, this,   Acharacter::StopJumping);\n    ```\n\n7.  Next, add the following lines in order to remove the player's rotation input:\n\n    ```cpp\n    // REMOVE THESE LINES\n    PlayerInputComponent->BindAxis(\"Turn\", this,   &APawn::AddControllerYawInput);\n    PlayerInputComponent->BindAxis(\"TurnRate\", this,   &ADodgeballCharacter::TurnAtRate);\n    PlayerInputComponent->BindAxis(\"LookUp\", this,   &APawn::AddControllerPitchInput);\n    PlayerInputComponent->BindAxis(\"LookUpRate\", this,   &ADodgeballCharacter::LookUpAtRate);\n    ```\n\n    这个步骤是可选的，但是为了保持代码干净，您应该删除`TurnAtRate`和`LookUpAtRate`函数的声明和实现。\n\n8.  最后，完成这些更改后，从 Visual Studio 运行项目。\n9.  When the editor has loaded, play the level. The camera's perspective should look like this and should not rotate based on the player's input or the character's rotation:\n\n    ![Figure 5.3: Locked camera rotation to a top-down perspective ](img/B16183_05_03.jpg)\n\n图 5.3:将相机旋转锁定到自上而下的视角\n\n这就是本章的第一个练习，也是你新项目`Dodgeball`的第一步。\n\n接下来，您将创建`EnemyCharacter`类。当玩家在视野中时，这个角色将是向玩家投掷躲避球的敌人。但这里出现的问题是:敌人怎么知道它能不能看到玩家角色？\n\n这将通过**线迹**(也称为**光线投射**或**光线追踪**的力量来实现，您将在下一节中看到。\n\n# 线条痕迹\n\n任何游戏开发工具最重要的特性之一是它能够执行线条追踪。这些可通过该工具使用的物理引擎获得。\n\n线迹是一种让游戏告诉你在游戏世界中两点之间是否有任何东西的方式。游戏将在你指定的这两个点之间*发射一条射线*，并返回被击中的物体(如果有的话)、它们被击中的位置、角度等等。\n\n在下图中，您可以看到线轨迹的表示，其中我们假设对象`1`被忽略，而对象`2`被检测到，这是由于它们的轨迹通道属性(在以下段落中进一步解释):\n\n![Figure 5.4: A Line Trace being executed from point A to point B ](img/B16183_05_04.jpg)\n\n图 5.4:从点 A 到点 B 执行的线跟踪\n\n在*图 5.4* 中:\n\n*   虚线表示击中对象之前的线迹。\n*   箭头表示线迹的方向。\n*   虚线表示击中对象后的线条轨迹。\n*   带条纹的圆圈代表线条痕迹的撞击点。\n*   大方块表示在线迹路径中的两个对象(对象`1`和`2`)。\n\n我们注意到只有物体`2`被线迹击中，没有物体`1`，虽然它也在线迹的路径上。这是由于对对象`1`的跟踪通道属性所做的假设，这将在本章后面讨论。\n\n线迹用于许多游戏功能，例如:\n\n*   检查武器开火时是否击中目标\n*   突出显示玩家可以在角色观看时与之交互的项目\n*   当玩家角色绕过拐角时，自动旋转相机\n\n线迹的一个常见且重要的特征是**迹线通道**。执行行跟踪时，您可能希望只检查特定类型的对象，这就是跟踪通道的用途。它们允许您指定在执行行跟踪时要使用的过滤器，以便它不会被不需要的对象阻塞。例如:\n\n*   您可能只想执行行跟踪来检查可见的对象。这些物体会阻塞`Visibility`追踪通道。例如，不可见的墙，也就是游戏中用来阻挡玩家移动的不可见的几何图形，将不可见，因此不会阻挡`Visibility`追踪通道。\n*   您可能只想执行行跟踪来检查可以交互的对象。这些物体会阻塞`Interaction`追踪通道。\n*   你可能只想执行一个线追踪来检查可以在游戏世界中移动的棋子。这些物体会阻塞`Pawn`追踪通道。\n\n您可以指定不同对象对不同跟踪通道的反应，以便只有一些对象阻止特定的跟踪通道，而其他对象忽略它们。在我们的例子中，我们想知道敌人和玩家角色之间是否有任何东西，这样我们就知道敌人是否能看到玩家。为此，我们将使用线追踪，通过使用`Tick`事件检查任何阻挡敌人视线到玩家角色的东西。\n\n在下一节中，我们将使用 C++ 创建`EnemyCharacter`类。\n\n# 创建 EnemyCharacter C++ 类\n\n在我们的`Dodgeball`游戏中，`EnemyCharacter`类将不断地观察玩家角色，如果他们在视野之内的话。这就是后来会向玩家投掷躲避球的同一个职业；然而，我们将把它留到下一章。在这一章中，我们将集中讨论允许我们的敌人角色看玩家的逻辑。\n\n那么，让我们开始吧:\n\n1.  右键点击编辑器内的`Content Browser`，选择`New C++ Class`。\n2.  选择`Character`类作为父类。\n3.  命名新类`EnemyCharacter`。\n\n在 Visual Studio 中创建了类并打开了它的文件之后，让我们在它的`header`文件中添加`LookAtActor`函数声明。这个函数应该是`public`，不返回任何东西，只接收`AActor* TargetActor`参数，这将是它应该面对的演员。看看下面的代码片段，它显示了这个函数:\n\n```cpp\n// Change the rotation of the character to face the given actor\nvoid LookAtActor(AActor* TargetActor);\n```\n\n注意\n\n即使我们只希望敌人看玩家的角色，为了执行好的软件开发实践，我们将把这个函数再抽象一点，允许`EnemyCharacter`看任何一个 Actor，因为允许一个 Actor 看另一个 Actor 或者看玩家角色的逻辑是完全一样的。\n\n请记住，您不应该在编写代码时创建不必要的限制。如果你可以写类似的代码，同时允许更多的可能性，你应该这样做，如果这不会使你的程序逻辑过于复杂的话。\n\n继续往前走，如果`EnemyCharacter`看不到`Target` `Actor`，就不应该在看。为了检查敌人是否能看到演员，应该看一下`LookAtActor`函数，它将调用另一个函数，`CanSeeActor`函数。这是你在下一个练习中要做的。\n\n## 练习 5.02:创建执行行跟踪的 CanSeeActor 函数\n\n在本练习中，我们将创建`CanSeeActor`功能，该功能将返回敌方角色是否能看到给定的演员。\n\n以下步骤将帮助您完成本练习:\n\n1.  Create the declaration for the `CanSeeActor` function in the header file of the `EnemyCharacter` class, which will return a `bool` and receive a `const Actor* TargetActor` parameter, which is the Actor we want to look at. This function will be a `const` function, because it doesn't change any of the class's attributes, and the parameter will also be `const` because we won't need to modify any of its properties; we'll only need to access them:\n\n    ```cpp\n    // Can we see the given actor\n    bool CanSeeActor(const AActor* TargetActor) const;\n    ```\n\n    现在，让我们进入有趣的部分，即执行行跟踪。\n\n    为了调用与线路追踪相关的函数，我们必须用`GetWorld`函数获取敌人的当前世界。但是，我们没有在这个文件中包含`World`类，所以让我们在下面的步骤中这样做:\n\n    注意\n\n    任何参与者都可以访问`GetWorld`功能，该功能将返回参与者所属的`World`对象。请记住，为了执行线跟踪，世界是必要的。\n\n2.  Open the `EnemyCharacter` source file and find the following code line:\n\n    ```cpp\n    #include \"EnemyCharacter.h\"\n    ```\n\n    在前一行代码之后添加以下一行:\n\n    ```cpp\n    #include \"Engine/World.h\"\n    ```\n\n3.  Next, create the implementation of the `CanSeeActor` function in the `EnemyCharacter` source file, where you'll start by checking whether our `TargetActor` is a `nullptr`. If it is, we return `false`, given that we have no valid Actor to check our sight to:\n\n    ```cpp\n    bool AEnemyCharacter::CanSeeActor(const AActor * TargetActor)   const\n    {\n      if (TargetActor == nullptr)\n      {\n        return false;\n      }\n    }\n    ```\n\n    接下来，在添加我们的 Line Trace 函数调用之前，我们需要设置一些必要的参数；我们将在以下步骤中实现这些。\n\n4.  After the previous `if` statement, create a variable to store all the necessary data relative to the results of the Line Trace. Unreal already has a built-in type for this called the `FHitResult` type:\n\n    ```cpp\n    // Store the results of the Line Trace\n    FHitResult Hit;\n    ```\n\n    这是我们将发送给我们的行跟踪函数的变量，它将用执行的行跟踪的相关信息填充它。\n\n5.  为我们的线迹的`Start`和`End`位置创建两个`FVector`变量，并将它们分别设置为我们敌人的当前位置和我们目标的当前位置:\n\n    ```cpp\n    // Where the Line Trace starts and ends\n    FVector Start = GetActorLocation();\n    FVector End = TargetActor->GetActorLocation();\n    ```\n\n6.  Next, set the Trace Channel we wish to compare against. In our case, we want to have a `Visibility` Trace Channel specifically designated to indicate whether an object blocks another object's view. Luckily for us, such a Trace Channel already exists in UE4, as shown in the following code snippet:\n\n    ```cpp\n    // The trace channel we want to compare against\n    ECollisionChannel Channel = ECollisionChannel::ECC_Visibility;\n    ```\n\n    `ECollisionChannel` `enum`代表所有可用于比较的可能跟踪通道。我们将使用代表`Visibility`跟踪通道的`ECC_Visibility`值。\n\n7.  Now that we've set up all our necessary parameters, we can finally call the `LineTrace` function, `LineTraceSingleByChannel`:\n\n    ```cpp\n    // Execute the Line Trace\n    GetWorld()->LineTraceSingleByChannel(Hit, Start, End,   Channel);\n    ```\n\n    这个函数会考虑我们发送给它的参数，执行 Line Trace，并通过修改我们的`Hit`变量返回它的结果。\n\n    在我们继续之前，还有几件事我们需要考虑。\n\n    如果线迹从我们的敌人角色内部开始，这就是我们的情况，这意味着线迹很可能会直接击中我们的敌人角色，并就此停止，因为我们的角色可能会阻挡`Visibility`跟踪通道。为了解决这个问题，我们需要告诉行跟踪忽略它。\n\n8.  使用内置的`FCollisionQueryParams`类型，它允许我们给我们的线轨迹更多的选择:\n\n    ```cpp\n    FCollisionQueryParams QueryParams;\n    ```\n\n9.  Now, update the `Line Trace` to ignore our enemy, by adding itself to the list of Actors to ignore:\n\n    ```cpp\n    // Ignore the actor that's executing this Line Trace\n    QueryParams.AddIgnoredActor(this);\n    ```\n\n    我们还应该将我们的目标添加到我们要忽略的演员列表中，因为我们不想知道它是否会阻塞`EnemySight`频道；我们只是想知道敌人和玩家角色之间的某些东西是否会阻塞那个通道。\n\n10.  将目标参与者添加到要忽略的参与者列表中，如以下代码片段所示:\n\n    ```cpp\n    // Ignore the target we're checking for\n    QueryParams.AddIgnoredActor(TargetActor);\n    ```\n\n11.  接下来，将我们的`FCollisionQueryParams`作为`LineTraceSingleByChannel`函数的最后一个参数添加到线路轨迹中:\n\n    ```cpp\n    // Execute the Line Trace\n    GetWorld()->LineTraceSingleByChannel(Hit, Start, End, Channel,   QueryParams);\n    ```\n\n12.  Finalize our `CanSeeActor` function, by returning whether the Line Trace hits anything or not. We can do that by accessing our `Hit` variable and checking whether there was a blocking hit, using the `bBlockingHit` property. If there was, that means we can't see our `TargetActor`. This can be achieved with the following code snippet:\n\n    ```cpp\n    return !Hit.bBlockingHit;\n    ```\n\n    注意\n\n    虽然我们不需要从`Hit`结果中获得更多信息，但是除了是否有阻塞命中之外，`Hit`变量可以为我们提供更多关于线轨迹的信息，例如:\n\n    通过访问`Hit.GetActor()`功能，获得被线轨迹击中的演员的信息(如果没有击中演员，则为`nullptr`)\n\n    通过访问`Hit.GetComponent()`功能，关于被线跟踪命中的执行元组件的信息(如果没有执行元组件被命中，则为`nullptr`)\n\n    通过访问`Hit.Location`变量获得命中位置的信息\n\n    命中的距离可以通过访问`Hit.Distance`变量找到\n\n    线迹击中物体的角度，可以通过访问`Hit.ImpactNormal`变量找到\n\n最后，我们的`CanSeeActor`功能完成了。我们现在知道如何执行线跟踪，我们可以用它来分析敌人的逻辑。\n\n通过完成本练习，我们已经完成了`CanSeeActor`功能；我们现在可以回到`LookAtActor`功能。然而，有一些东西我们应该首先看:可视化我们的线轨迹。\n\n# 可视化线条轨迹\n\n当创建利用行跟踪的新逻辑时，在执行行跟踪时实际可视化它是非常有用的，这是行跟踪函数不允许您做的事情。为了做到这一点，我们必须使用一组助手调试函数，这些函数可以在运行时动态绘制对象，如线、立方体、球体等。\n\n然后让我们添加一个我们的线轨迹可视化。为了使用调试功能，我们必须做的第一件事是在最后一行`include`下面添加以下`include`:\n\n```cpp\n#include \"DrawDebugHelpers.h\"\n```\n\n我们将希望调用`DrawDebugLine`函数来可视化线迹，这需要以下输入，非常类似于线迹函数接收的输入:\n\n1.  电流`World`，我们将提供`GetWorld`功能\n2.  线的`Start`和`End`点，与`LineTraceSingleByChannel`功能相同\n3.  游戏中需要的线条颜色，可以设置为`Red`\n\n然后，我们可以在我们的 Line Trace 函数调用下面添加`DrawDebugLine`函数调用，如下面的代码片段所示:\n\n```cpp\n// Execute the Line Trace\nGetWorld()->LineTraceSingleByChannel(Hit, Start, End, Channel,   QueryParams);\n// Show the Line Trace inside the game\nDrawDebugLine(GetWorld(), Start, End, FColor::Red);\n```\n\n这将允许您在执行时可视化行跟踪，这非常有用。\n\n注意\n\n如果您觉得有必要，还可以指定更多的可视化线条跟踪属性，例如其寿命和厚度。\n\n有许多`DrawDebug`功能可以用来绘制立方体、球体、圆锥体、甜甜圈，甚至自定义网格。\n\n既然我们可以执行和可视化我们的线轨迹，让我们在`LookAtActor`函数中使用我们在上一个练习中创建的`CanSeeActor`函数。\n\n## 练习 5.03:创建 LookAtActor 函数\n\n在本练习中，我们将创建我们的`LookAtActor`函数的定义，这将改变敌人的旋转，使其面对给定的演员。\n\n以下步骤将帮助您完成练习:\n\n1.  在`EnemyCharacter`源文件中创建`LookAtActor`函数定义。\n2.  首先检查我们的`TargetActor`是否是`nullptr`，如果是(因为无效)，则立即不返回任何内容，如下面的代码片段所示:\n\n    ```cpp\n    void AEnemyCharacter::LookAtActor(AActor * TargetActor)\n    {\n      if (TargetActor == nullptr)\n      {\n        return;\n      }\n    }\n    ```\n\n3.  Next, we want to check whether we can see our Target Actor, using our `CanSeeActor` function:\n\n    ```cpp\n    if (CanSeeActor(TargetActor))\n    {\n    }\n    ```\n\n    如果这个`if`的说法是真的，那就意味着我们可以看到 Actor，我们将设置我们的旋转方式，让我们面对那个 Actor。幸运的是，UE4 中已经有一个功能允许我们这样做:功能`FindLookAtRotation`。该功能将接收高程中的两点作为输入，即点 A(T2 点)和点 B(T3 点)，并返回起点处的对象必须进行的旋转，以便面向终点处的对象。\n\n4.  为了使用该功能，包括`KismetMathLibrary`，如下面的代码片段所示:\n\n    ```cpp\n    #include \"Kismet/KismetMathLibrary.h\"\n    ```\n\n5.  `FindLookAtRotation`功能必须接收一个`Start`和`End`点，分别是我们敌人的位置和我们目标演员的位置:\n\n    ```cpp\n    FVector Start = GetActorLocation();\n    FVector End = TargetActor->GetActorLocation();\n    // Calculate the necessary rotation for the Start point to   face the End point\n    FRotator LookAtRotation =   UKismetMathLibrary::FindLookAtRotation(Start, End);\n    ```\n\n6.  Finally, set your enemy character's rotation to the same value as our `LookAtRotation`:\n\n    ```cpp\n    //Set the enemy's rotation to that rotation\n    SetActorRotation(LookAtRotation);\n    ```\n\n    `LookAtActor`功能到此为止。\n\n    现在最后一步是在 Tick 事件内部调用`LookAtActor`函数，将玩家角色作为`TargetActor`发送，也就是我们要看的 Actor。\n\n7.  要获取玩家当前控制的角色，我们可以使用`GameplayStatics`对象。和其他 UE4 对象一样，我们必须首先包含它们:\n\n    ```cpp\n    #include \"Kismet/GameplayStatics.h\"\n    ```\n\n8.  Next, head to your Tick function's body and call the `GetPlayerCharacter` function from `GameplayStatics`:\n\n    ```cpp\n    // Fetch the character currently being controlled by the   player\n    ACharacter* PlayerCharacter =   UGameplayStatics::GetPlayerCharacter(this, 0);\n    ```\n\n    该功能接收以下输入:\n\n    *   世界上下文对象，本质上是属于我们当前世界的对象，用于让函数知道要访问哪个世界对象。这个世界上下文对象可以简单地是`this`指针。\n    *   玩家指数，假设我们的游戏是单人游戏，我们可以安全地假设是`0`(第一个玩家)。\n9.  接下来，调用`LookAtActor`函数，发送我们刚刚获取的玩家角色:\n\n    ```cpp\n    // Look at the player character every frame\n    LookAtActor(PlayerCharacter);\n    ```\n\n10.  本练习的最后一步是在 Visual Studio 中编译您的更改。\n\n现在你已经完成了这个练习，你的`EnemyCharacter`类已经具备了面对玩家角色的所有必要逻辑，如果在视野范围内，我们可以开始创建`EnemyCharacter`蓝图类了。\n\n# 创建敌人角色蓝图类\n\n既然我们已经完成了我们的`EnemyCharacter` C++ 类的逻辑，我们必须创建我们的蓝图类，它派生自:\n\n1.  在编辑器中打开我们的项目。\n2.  转到`Content Browser`中`ThirdPersonCPP`文件夹内的`Blueprints`文件夹。\n3.  *右键点击*，选择新建蓝图类的选项。\n4.  展开`Pick Parent Class`窗口底部附近的`All Classes`选项卡，搜索我们的`EnemyCharacter` C++ 类，并选择它作为父类。\n5.  命名蓝图类`BP_EnemyCharacter`。\n6.  打开蓝图类，从`Components`选项卡中选择`SkeletalMeshComponent`(称为`Mesh`)，将其`Skeletal Mesh`属性设置为`SK_Mannequin`，其`Anim Class`属性设置为`ThirdPerson_AnimBP`。\n7.  将`SkeletalMeshComponent`的*偏航*改变为`-90º`(在 *z 轴*上)并将其在 *z 轴*上的位置改变为`-83`单位。\n8.  在你设置好蓝图类之后，它的网格设置应该和我们的`DodgeballCharacter`蓝图类非常相似。\n9.  Drag an instance of the `BP_EnemyCharacter` class to your level, in a location near an object that can block its line of sight, such as this location (the selected character is `EnemyCharacter`):\n\n    ![Figure 5.5: Dragging the BP_EnemyCharacter class into the level ](img/B16183_05_05.jpg)\n\n    图 5.5:将 BP _ EnemyCharacter 类拖动到级别中\n\n10.  Now we can finally play the game and verify that our enemy does look at our player character whenever it's within view:\n\n    ![Figure 5.6: Enemy character with a clear view of the player using a Line Trace ](img/B16183_05_06.jpg)\n\n    图 5.6:使用线条追踪清晰观察玩家的敌方角色\n\n11.  我们还可以看到，只要不在视野范围内，敌人就会停止看到玩家，如图*图 5.7* :\n\n![Figure 5.7: Enemy losing sight of the player ](img/B16183_05_07.jpg)\n\n图 5.7:敌人看不见玩家\n\n这就是我们的逻辑。在下一节中，我们将研究扫掠轨迹。\n\n# 扫掠痕迹\n\n在我们继续我们的项目之前，了解线条轨迹的一个变体很重要，它是**扫掠轨迹**。虽然我们不会在我们的项目中使用它们，但是了解它们以及如何使用它们是很重要的。\n\n线迹基本上*在两点之间发射光线*，而扫迹将模拟*在直线上两点之间投掷物体*。被*投掷的物体*是模拟的(游戏中实际不存在)，可以有各种形状。在扫掠轨迹中，`Hit`位置将是虚拟对象(我们称之为**形状**)撞击另一个对象的第一个点，如果它是从起点抛向终点的话。扫掠轨迹的形状可以是长方体、球体或胶囊。\n\n这是从点`A`到点`B`的扫掠轨迹的表示，其中我们假设对象`1`由于其轨迹通道属性而被忽略，使用一个盒子形状:\n\n![Figure 5.8: Representation of a Sweep Trace ](img/B16183_05_08.jpg)\n\n图 5.8:扫描轨迹的表示\n\n从*图 5.8* 中，我们注意到以下情况:\n\n*   从 A 点到 b 点执行的扫描轨迹，使用盒形\n*   虚线框表示扫描轨迹在碰到对象之前的情况。\n*   虚线框表示击中对象后的扫掠轨迹。\n*   条纹圆表示扫掠轨迹与对象`2`的碰撞点，这是扫掠轨迹框形状的表面与对象`2`的表面相互碰撞的点。\n*   大方块表示线扫轨迹路径中的两个对象(对象`1`和`2`)。\n*   由于基于其跟踪通道属性的假设，对象`1`在扫描跟踪中被忽略。\n\n在某些情况下，扫描轨迹比常规的线轨迹更有用。让我们以我们的敌人角色为例，他会扔躲避球。如果我们想为玩家添加一种方法，让玩家不断地想象敌人扔出的下一个躲避球会落在哪里，那可以通过扫掠追踪来更好地实现:我们会用我们的躲避球的形状(一个球体)朝着我们的玩家进行扫掠追踪，检查撞击点，并在那个撞击点上显示一个球体，玩家可以看到这个球体。如果“横扫追踪”击中某处的墙壁或角落，玩家会知道，如果敌人在那一刻投掷躲避球，那就是它首先击中的地方。出于同样的目的，您可以使用简单的线跟踪，但是设置必须相当复杂才能获得相同质量的结果，这就是为什么在这种情况下扫掠跟踪是更好的解决方案。\n\n现在让我们快速了解一下如何在代码中进行扫描跟踪。\n\n## 练习 5.04:执行扫描轨迹\n\n在本练习中，我们将在代码中实现扫描跟踪。虽然我们不会在我们的项目中使用它，但是通过执行这个练习，您将会熟悉这样的操作。\n\n转至前几节中创建的`CanSeeActor`功能的末尾，并按照以下步骤操作:\n\n1.  The function responsible for the Sweep Trace is `SweepSingleByChannel`, which is available within UE4 and requires the following parameters as inputs:\n\n    一个`FHitResult`类型，用来存储扫描的结果(我们已经有一个这样的类型，所以没有必要再创建一个这样的类型的变量):\n\n    ```cpp\n    // Store the results of the Line Trace\n    FHitResult Hit;\n    ```\n\n    `Start`和`End`点的扫描(我们已经有了这两个，所以没有必要创建另一个这种类型的变量):\n\n    ```cpp\n    // Where the Sweep Trace starts and ends\n    FVector Start = GetActorLocation();\n    FVector End = TargetActor->GetActorLocation();\n    ```\n\n2.  使用形状的预期旋转，其形式为`FQuat`类型(表示四元数)。在这种情况下，通过访问`FQuat``Identity`属性:\n\n    ```cpp\n    // Rotation of the shape used in the Sweep Trace\n    FQuat Rotation = FQuat::Identity; \n    ```\n\n    ，它在所有轴上都被设置为`0`旋转\n3.  现在，使用预期的跟踪通道对其进行比较(我们已经有了其中的一个，所以没有必要创建另一个这种类型的变量):\n\n    ```cpp\n    // The trace channel we want to compare against\n    ECollisionChannel Channel = ECollisionChannel::ECC_Visibility;\n    ```\n\n4.  最后，通过调用`FcollisionShape` `MakeBox`函数并为其提供我们想要的长方体形状的半径(在所有三个轴上)，使用长方体形状进行扫掠追踪。这显示在下面的代码片段中:\n\n    ```cpp\n    // Shape of the object used in the Sweep Trace\n    FCollisionShape Shape = FCollisionShape::MakeBox(FVector(20.f,   20.f, 20.f));\n    ```\n\n5.  接下来，调用`SweepSingleByChannel`函数:\n\n    ```cpp\n    GetWorld()->SweepSingleByChannel(Hit,\n                                     Start,\n                                     End,\n                                     Rotation,\n                                     Channel,\n                                     Shape);\n    ```\n\n完成这些步骤后，我们就完成了“扫掠轨迹”练习。考虑到我们不会在我们的项目中使用扫描轨迹，您应该注释掉`SweepSingleByChannel`函数，这样我们的`Hit`变量就不会被修改并丢失我们的线轨迹的结果。\n\n现在我们已经完成了“扫掠轨迹”部分，让我们回到我们的`Dodgeball`项目，学习如何更改对象对轨迹通道的响应。\n\n## 改变能见度轨迹响应\n\n在我们当前的设置中，每个可见的对象都会阻挡`Visibility`追踪通道；然而，如果我们想改变一个对象是否完全阻塞了那个通道呢？为了做到这一点，我们必须改变组件对该通道的响应。请看下面的例子:\n\n1.  We select the cube that we've been using to block the enemy's sight in our level as shown in *Figure 5.9*:\n\n    ![Figure 5.9: Default spawn of the character ](img/B16183_05_09.jpg)\n\n    图 5.9:角色的默认版本\n\n2.  Then, you go to the `Collision` section of this object's `Details Panel` (its default place in the `Editor`'s interface):\n\n    ![Figure 5.10: Collision tab in the Details Panel in Unreal ](img/B16183_05_10.jpg)\n\n    图 5.10:虚幻中细节面板的碰撞标签\n\n3.  在这里，你会发现几个与碰撞相关的选项。我们现在要关注的是`CollisionPresets`选项。其电流值为`Default`；但是，我们想根据自己的喜好进行更改，所以我们会点击下拉框，将其值更改为`Custom`。\n4.  Once you do this, you'll notice a whole group of new options pops up:\n\n    ![Figure 5.11: Collision Preset set to Custom ](img/B16183_05_11.jpg)\n\n    图 5.11:碰撞预设设置为自定义\n\n    这组选项允许您指定该对象如何响应线轨迹和对象碰撞，以及碰撞对象的类型。\n\n    你应该注意的选项是`Visibility`。你会注意到它被设置为`Block`，但是你也可以设置为`Overlap`和`Ignore`。\n\n    此时，立方体正在阻挡`Visibility`追踪通道，这就是为什么我们的敌人在这个立方体后面看不到角色的原因。但是，如果我们将对象对`Visibility`跟踪通道的响应更改为`Overlap`或`Ignore`，对象将不再阻止检查可见性的行跟踪(这是您刚刚用 C++ 编写的行跟踪的情况)。\n\n5.  Let's change the cube's response to the `Visibility` channel to `Ignore`, and then play the game. You'll notice that the enemy is still looking toward the player character, even when it's behind the cube:\n\n    ![Figure 5.12: Enemy character looking through an object at the player ](img/B16183_05_12.jpg)\n\n    图 5.12:敌人角色透过一个物体看着玩家\n\n    这是因为立方体不再阻挡`Visibility`追踪通道，所以敌人正在执行的线追踪在试图到达玩家角色时不再命中任何东西。\n\n    现在，我们已经了解了如何更改对象对特定跟踪通道的响应，让我们将立方体对`Visibility`通道的响应更改回`Block`。\n\n    然而，有一件事值得一提:如果我们将立方体对`Visibility`通道的响应设置为`Overlap`，而不是`Ignore`，结果将是一样的。但是为什么会这样，有这两种反应的目的是什么？为了解释这一点，我们将看看多线轨迹。\n\n    ## 多线轨迹\n\n    在使用*练习 5.02* 、*中的`CanSeeActor`函数创建执行线迹*的 CanSeeActor 函数时，您可能会对我们使用的线迹函数的名称`LineTraceSingleByChannel`感到疑惑，特别是它为什么使用单词 *Single* 。原因是因为你也可以处决`LineTraceMultiByChannel`。\n\n    但是这两条线迹有什么不同呢？\n\n    虽然单线跟踪将在它击中一个对象后停止检查阻挡它的对象，并告诉我们那是它击中的对象，但是多线跟踪可以检查被同一线跟踪击中的任何对象。\n\n    单线跟踪将:\n\n    *   忽略线跟踪正在使用的跟踪通道上响应设置为`Ignore`或`Overlap`的对象\n    *   当它发现一个其响应设置为`Block`的对象时停止\n\n但是，多线追踪不会忽略其响应设置为`Overlap`的对象，而是将它们作为在线追踪期间找到的对象添加，并且仅当它找到阻挡所需追踪通道的对象时(*或当它到达终点*时停止。在下图中，您将看到正在执行的多行跟踪的图示:\n\n![Figure 5.13: A Multi Line Trace being executed from point A to point B ](img/B16183_05_13.jpg)\n\n图 5.13:从 A 点到 B 点执行的多线跟踪\n\n在*图 5.13* 中，我们注意到以下情况:\n\n*   虚线表示在遇到阻挡它的对象之前的线轨迹。\n*   虚线表示线迹碰到阻挡它的对象后的线迹。\n*   条纹圆圈代表线条追踪的冲击点，在这种情况下，只有最后一个是阻挡命中。\n\n就输入而言，`LineTraceSingleByChannel`和`LineTraceMultiByChannel`功能之间的唯一区别是后者必须接收一个`TArray<FHitResult>`输入，而不是单个`FHitResult`。所有其他输入都是相同的。\n\n当模拟具有强穿透力的子弹的行为时，多线轨迹非常有用，这些子弹可以在完全停止之前穿过几个对象。请记住，您也可以通过调用`SweepMultiByChannel`功能来执行多扫描轨迹。\n\n注意\n\n关于`LineTraceSingleByChannel`函数，您可能想知道的另一件事是`ByChannel`部分。这种区别与使用跟踪通道有关，与对象类型相反。您可以通过调用`LineTraceSingleByObjectType`函数来执行使用对象类型而不是跟踪通道的线跟踪，该函数也可以从世界对象中获得。对象类型与我们将在下一章讨论的主题相关，所以我们暂时不会详细讨论这个函数。\n\n## 摄像机跟踪通道\n\n当更改我们的立方体对`Visibility`跟踪通道的响应时，您可能已经注意到另一个现成的跟踪通道:`Camera`。\n\n此通道用于指定对象是否阻挡摄像机弹簧臂和与其关联的角色之间的视线。为了在行动中看到这一点，我们可以将一个对象拖到我们的水平，并将其放置在相机和我们的玩家角色之间。\n\n请看下面的例子；我们从复制`floor`物体开始。\n\n注意\n\n您可以通过按住 *Alt* 键并向任意方向拖动*移动工具*的箭头之一，轻松地在关卡中复制对象。\n\n![Figure 5.14: Floor object being selected ](img/B16183_05_14.jpg)\n\n图 5.14:选择楼层对象\n\n1.  Next, we change its `Transform` values as shown in the following figure:\n\n    ![Figure 5.15: Updating the Transform values ](img/B16183_05_15.jpg)\n\n    图 5.15:更新转换值\n\n2.  Now when you play your game, you'll notice that when the character goes under our duplicated floor object, you won't lose sight of the player's character, but rather the spring arm will cause the camera to move down until you can see the character:\n\n    ![Figure 5.16: Changes in the camera angle ](img/B16183_05_16.jpg)\n\n    图 5.16:相机角度的变化\n\n3.  为了了解当物体没有阻挡`Camera`追踪通道时，弹簧臂的行为有何不同，请将我们复制的地板对`Camera`通道的响应更改为`Ignore`，然后再次播放关卡。会发生的是，当我们的角色在复制的地板下时，我们会看不到角色。\n\n完成这些步骤后，您可以看到`Camera`通道用于指定当某个对象与该对象相交时，该对象是否会导致弹簧臂将相机移近播放器。\n\n既然我们知道如何使用现有的跟踪通道，如果我们想创建自己的跟踪通道呢？\n\n## 练习 5.05:创建自定义敌人追踪通道\n\n正如我们之前讨论过的，UE4 有两个现成的跟踪通道:`Visibility`和`Camera`。第一个是通用通道，我们可以使用它来指定哪些对象阻挡了对象的视线，而第二个通道允许我们指定一个对象是否阻挡了相机的弹簧臂和与其关联的角色之间的视线。\n\n但是我们如何创建自己的跟踪通道呢？这就是我们将在本练习中研究的内容。我们将创建一个新的`EnemySight`追踪通道，并使用它来检查敌人是否能看到玩家角色，而不是内置的`Visibility`通道:\n\n1.  按编辑器左上角的`Edit`按钮打开`Project Settings`，进入`Collision`部分。在那里你会找到`Trace Channels`部分。它目前是空的，因为我们还没有创建任何自己的跟踪通道。\n2.  选择`New Trace Channel`选项。应该会弹出一个窗口，让您可以选择命名新频道，并根据项目中的对象设置其默认响应。命名我们新的追踪通道`EnemySight`并将其默认响应设置为`Block`，因为我们希望大多数对象都这样做。\n3.  After you've created the new Trace Channel, we must go back to our `EnemyCharacter` C++ class and change the trace we're comparing against in our Line Trace:\n\n    ```cpp\n    // The trace channel we want to compare against\n    ECollisionChannel Channel = ECollisionChannel::ECC_Visibility;\n    ```\n\n    鉴于我们不再使用`Visibility`频道，我们必须参考我们的新频道，但我们如何做到这一点？\n\n    在你的项目目录中，你会找到`Config`文件夹。该文件夹包含多个与您的项目相关的`ini`文件，如`DefaultGame.ini`、`DefaultEditor.ini`、`DefaultEngine.ini`等。其中每一个都包含几个将在项目加载时初始化的属性。属性由名称-值对(`property=value`)设置，您可以根据需要更改它们的值。\n\n4.  When we created our `EnemySight` channel, the project's `DefaultEngine.ini` file was updated with our new Trace Channel. Somewhere in that file, you'll find this line:\n\n    ```cpp\n    +DefaultChannelResponses=(Channel=ECC_GameTraceChannel1,  DefaultResponse=ECR_Block,bTraceType=True,bStaticObject=False,  Name=\"EnemySight\")\n    ```\n\n    注意\n\n    前面的代码行可以在这里高亮显示:[https://packt.live/3eFpz5r](https://packt.live/3eFpz5r)。\n\n    这一行表示有一个名为`EnemySight`的自定义跟踪通道，它的默认响应是 Block，最重要的是，它可以在 C++ 中使用我们之前提到的碰撞`enum``ECollisionChannel`的`ECC_GameTraceChannel1`值。这是我们将在代码中引用的通道:\n\n    ```cpp\n    // The trace channel we want to compare against\n    ECollisionChannel Channel =   ECollisionChannel::ECC_GameTraceChannel1;\n    ```\n\n5.  验证我们的敌人的行为在我们做了所有的改变后保持不变。这意味着敌人必须仍然面对玩家的角色，只要它在所述敌人的视野之内。\n\n通过完成本练习，我们现在知道如何为任何期望的目的制作我们自己的跟踪通道。\n\n回到 ou r 敌方角色，还是有办法可以提升其逻辑的。现在，当我们获取敌人的位置作为线迹的起点时，该点在敌人臀部周围的某个地方，因为这是演员的原点。然而，人们的眼睛通常不在那里，让人形角色从臀部而不是头部看没有多大意义。\n\n所以，让我们改变这一点，让我们的敌人角色检查它是否从眼睛而不是臀部开始看到玩家角色。\n\n## 活动 5.01:创建视觉源属性\n\n在这个活动中，我们将改进敌人的逻辑，以确定它是否应该看玩家。目前，在我们的`BP_EnemyCharacter`蓝图中，正在进行的确定是*从我们角色的臀部周围拍摄*(`0,0,0`)的线迹，我们希望这更有意义一点，所以我们将使线迹从靠近敌人眼睛的某个地方开始。那我们开始吧。\n\n以下步骤将帮助您完成活动:\n\n1.  在我们的`EnemyCharacter` C++ 类中声明一个名为`SightSource`的新`SceneComponent`。请务必使用`VisibleAnywhere`、`BlueprintReadOnly`、`Category = LookAt`和`meta = (AllowPrivateAccess = \"true\")`标签将其声明为`UPROPERTY`。\n2.  使用`CreateDefaultSubobject`功能在`EnemyCharacter`构造器中创建该组件，并将其附加到`RootComponent`上。\n3.  将`CanSeeActor`功能中线迹的开始位置改为`SightSource`组件的位置，而不是演员的位置。\n4.  打开`BP_EnemyCharacter`蓝图类，将`SightSource`组件的位置更改为敌人头部`(10, 0, 80)`的位置，就像在*创建敌人角色蓝图类*部分中所做的那样，更改为`BP_EnemyCharacter`的`SkeletalMeshComponent`属性。\n\n**提示**:这可以从`Editor Panel`中的`Transform`选项卡中实现，如*图 5.17* 所示。\n\n![Figure 5.17: Updating the SightSource component’s values ](img/B16183_05_17.jpg)\n\n图 5.17:更新视觉源组件的值\n\n预期产出:\n\n![Figure 5.18: Expected output showing the updated Line Trace from the hip to the eye ](img/B16183_05_18.jpg)\n\n图 5.18:显示从臀部到眼睛的更新线迹的预期输出\n\n注意\n\n这个活动的解决方案可以在:[https://packt.live/338jEBx](https://packt.live/338jEBx)找到。\n\n通过完成本活动，我们已经更新了我们的`EnemyCharacter`的`SightSource`属性。\n\n# 总结\n\n通过完成本章，您已经为您的皮带添加了一个新工具:线迹。您现在知道如何执行线跟踪和扫掠跟踪，包括单个和多个；如何更改对象对特定跟踪通道的响应；以及如何创建自己的跟踪通道。\n\n在接下来的章节中，你会很快意识到这些是游戏开发中必不可少的技能，你会在未来的项目中很好地利用它们。\n\n现在我们知道了如何使用线轨迹，我们已经为下一步做好了准备，这就是对象碰撞。在下一章中，您将学习如何设置对象之间的碰撞，以及如何使用碰撞事件来创建自己的游戏逻辑。你将创建躲避球演员，这将受到实时物理模拟的影响；墙演员，它会阻挡角色的动作和躲避球；以及当玩家接触到游戏时负责结束游戏的演员。"
  },
  {
    "path": "docs/game-dev-proj-ue/06.md",
    "content": "# 六、碰撞物体\n\n概观\n\n在这一章中，我们将通过给我们的游戏添加更多的机制和对象来继续我们在上一章中介绍的基于碰撞的游戏。首先，我们将从上一章开始，介绍对象碰撞。您将学习如何使用碰撞盒、碰撞触发器、重叠事件、碰撞事件和物理模拟。您还将学习如何使用计时器、投射物运动组件和物理材料。\n\n# 简介\n\n在前一章中，我们遇到了碰撞的一些基本概念，即线条痕迹和扫掠痕迹。我们学习了如何执行不同类型的线跟踪，如何创建自己的自定义跟踪通道，以及如何更改对象对特定通道的响应方式。您在上一章中学到的许多东西将在本章中使用，我们将在本章中了解对象碰撞。\n\n在本章中，我们将继续通过添加围绕物体碰撞的游戏机制来建立自上而下的`Dodgeball`游戏。我们将创建**躲避球演员**，它将充当从地板和墙壁上反弹的躲避球；一个**墙演员**，会遮挡所有物体；一个**鬼壁演员**，只会挡住玩家，不会挡住敌人的视线或者躲避球；以及**胜利箱演员**，当玩家进入胜利箱时将结束游戏，代表关卡结束。\n\n在开始创建我们的`Dodgeball`类之前，我们将在下一节复习对象碰撞的基本概念。\n\n# UE4 中的物体碰撞\n\n每个游戏开发工具都必须有一个物理引擎来模拟多个对象之间的碰撞，如前一章所述。碰撞是如今发布的大多数游戏的支柱，无论是 2D 还是 3D。在许多游戏中，这是玩家作用于环境的主要方式，无论是奔跑、跳跃还是射击，环境都会通过让玩家着陆、被击中等方式做出相应的反应。毫不夸张地说，如果没有模拟碰撞，根本不可能制作很多游戏。\n\n因此，让我们从碰撞组件开始，了解物体碰撞在 UE4 中是如何工作的，以及我们可以使用它的方式。\n\n# 碰撞部件\n\n在 UE4 中，有两种类型的组件可以影响和被影响碰撞；它们如下:\n\n*   网状物\n*   形状对象\n\n**网格**可以像立方体一样简单，也可以像拥有数万个顶点的高分辨率角色一样复杂。网格的碰撞可以通过在网格旁边导入到 UE4 中的自定义文件来指定(这不在本书的范围内)，也可以由 UE4 自动计算并由您自定义。\n\n通常，保持碰撞网格尽可能简单(几个三角形)是一个好的做法，这样物理引擎可以在运行时有效地计算碰撞。可能发生碰撞的网格类型如下:\n\n*   静态网格\n*   骨骼网格\n*   程序网格\n*   等等\n\n**形状对象**，是以线框模式表示的简单网格，用于通过引起和接收碰撞事件来表现为碰撞对象。\n\n注意\n\n线框模式是游戏开发中常用的可视化模式，通常用于调试目的，它允许您看到没有任何面或纹理的网格–只能通过它们的边看到它们，这些边由它们的顶点连接。当我们向演员添加一个形状组件时，你会看到什么是线框模式。\n\n请注意，形状对象本质上是不可见的网格，它们的三种类型如下:\n\n*   盒子碰撞(C++ 中的盒子组件)\n*   球体碰撞(C++ 中的球体组件)\n*   Capsule Collider (Capsule Component in C++)\n\n    注意\n\n    有一个类，所有提供几何和碰撞的组件都继承自这个类，这就是`Primitive`组件。该组件是包含任何几何图形的所有组件的基础，网格组件和形状组件就是这种情况。\n\n那么，这些组件如何碰撞，当它们碰撞时会发生什么？我们将在下一节“碰撞事件”中看到这一点。\n\n# 碰撞事件\n\n假设有两个物体相互碰撞。可能会发生两件事:\n\n*   它们相互重叠，就好像另一个对象不在那里，在这种情况下，调用`Overlap`事件。\n*   它们相互碰撞并阻止对方继续前进，在这种情况下`Block`事件被称为。\n\n在前一章中，我们学习了如何改变对象对特定`Trace`通道的响应。在这个过程中，我们了解到物体的反应可以是`Block`、`Overlap`或`Ignore`。\n\n现在，让我们看看在碰撞过程中，这些反应会发生什么。\n\n**阻挡**:两个物体只有对另一个物体的反应都设置为`Block`时才会互相阻挡；\n\n*   两个对象的`OnHit`事件都将被调用。每当两个物体在碰撞时挡住彼此的路径时，就会调用此事件。如果其中一个对象正在模拟物理，该对象必须将其`SimulationGeneratesHitEvents`属性设置为`true`。\n*   两个物体会物理地阻止彼此继续它们的进程。\n\n请看下图，该图显示了两个对象被抛出并相互弹开的示例:\n\n![Figure 6.1: Object A and Object B blocking each other ](img/B16183_06_01.jpg)\n\n图 6.1:对象 A 和对象 B 相互阻塞\n\n**重叠**:两个物体如果不相互遮挡，也没有一个物体忽略另一个物体，两个物体就会重叠；\n\n*   如果两个对象的`GenerateOverlapEvents`属性都设置为`true`，那么它们的`OnBeginOverlap`和`OnEndOverlap`事件将被调用。当一个对象开始和停止与另一个对象重叠时，分别调用这些重叠事件。如果其中至少有一个没有将此属性设置为`true`，则它们都不会调用这些事件。\n*   这些对象的行为就像另一个对象不存在一样，并且会相互重叠。\n\n举个例子，假设玩家的角色走进一个标志关卡结束的触发框，这个触发框只对玩家的角色做出反应。\n\n请看下图，该图显示了两个对象相互重叠的示例:\n\n![Figure 6.2: Object A and Object B overlapping each other ](img/B16183_06_02.jpg)\n\n图 6.2:对象 A 和对象 B 相互重叠\n\n**忽略**:如果两个对象中至少有一个忽略了另一个，那么这两个对象就会互相忽略:\n\n*   两个对象上都不会调用任何事件。\n*   类似于`Overlap`响应，对象会表现得好像另一个对象不存在一样，并且会相互重叠。\n\n两个对象相互忽略的一个例子是，当玩家角色以外的对象进入标志关卡结束的触发框时，触发框只对玩家角色做出反应。\n\n注意\n\n可以看看上图，两个物体相互重叠的地方，了解**忽略**。\n\n下面的表格有助于您理解两个对象必须具有的必要响应，以便触发前面描述的情况:\n\n![Figure 6.3: Resulting responses on objects based on Block, Overlap, and Ignore ](img/B16183_06_03.jpg)\n\n图 6.3:基于块、重叠和忽略的对象响应结果\n\n根据此表，假设您有两个对象-对象 A 和对象 B:\n\n*   如果对象 A 将其对对象 B 的响应设置为`Block`，而对象 B 将其对对象 A 的响应设置为`Block`，它们将相互`Block`。\n*   如果对象 A 将其对对象 B 的响应设置为`Block`，而对象 B 将其对对象 A 的响应设置为`Overlap`，它们将相互`Overlap`。\n*   If Object A has set its response to Object B to `Ignore` and Object B has set its response to Object A to `Overlap`, they will `Ignore` each other.\n\n    注意\n\n    你可以在这里找到 UE4 碰撞交互的完整参考资料:[https://docs . unrealengine . com/en-US/Engine/Physics/conflict/Overview](https://docs.unrealengine.com/en-US/Engine/Physics/Collision/Overview)。\n\n物体之间的碰撞有两个方面:\n\n【时间】物理学【时间】T1:所有与物理模拟相关的碰撞，例如球受重力影响而从地板和墙壁上反弹。\n\n游戏内碰撞的物理模拟响应，可以是:\n\n*   两个物体继续它们的轨迹，好像另一个物体不在那里(没有物理碰撞)。\n*   两个物体碰撞并改变它们的轨迹，通常其中至少有一个物体继续运动，也就是说，阻挡了彼此的路径。\n\n**查询**:查询可以分为两个方面的碰撞，如下:\n\n*   与游戏调用的对象碰撞相关的事件，您可以使用这些事件来创建附加逻辑。这些事件与我们之前提到的相同:\n*   `OnHit`事件\n*   `OnBeginOverlap`事件\n*   `OnEndOverlap`事件\n*   游戏内碰撞的物理反应，可以是:\n*   两个物体继续运动，就好像另一个物体不在那里一样(没有物理碰撞)\n*   两个物体相互碰撞并阻挡对方的道路\n\n物理方面的物理响应听起来可能类似于查询方面的物理响应；然而，尽管这些都是物理反应，但它们会导致对象行为不同。\n\n“物理”方面的物理响应(物理模拟)仅适用于对象模拟物理时(例如，受重力影响、从墙壁和地面反弹等)。例如，这样的物体，当碰到墙壁时，会反弹回来，继续向另一个方向移动。\n\n另一方面，来自查询方面的物理响应适用于所有不模拟物理的对象。当由代码控制时(例如，通过使用`SetActorLocation`功能或通过使用角色移动组件)，对象可以在不模拟物理的情况下移动。在这种情况下，根据您使用的移动对象及其属性的方法，当对象碰到墙壁时，它将简单地停止移动，而不是弹回。这是因为你只是告诉物体向某个方向移动，而有东西挡住了它的路径，所以物理引擎不允许那个物体继续移动。\n\n在下一节中，我们将研究碰撞通道。\n\n# 碰撞通道\n\n在前一章，我们看了一下现有的 Trace channel(*可见度**相机*)并学习了如何制作自己的定制通道。现在，您已经了解了跟踪通道，现在是时候讨论对象通道了，也称为对象类型。\n\n虽然跟踪通道仅用于线跟踪，但对象通道用于对象碰撞。您可以为每个`Object`通道指定一个“目的”，就像跟踪通道一样，如棋子、静态对象、物理对象、投射体等。然后，您可以指定希望每个对象类型如何通过阻止、重叠或忽略该类型的对象来响应所有其他对象类型。\n\n# 碰撞属性\n\n现在我们已经了解了碰撞是如何工作的，让我们回到上一章中选择的立方体的碰撞设置，在那里我们更改了它对可见性通道的响应。\n\n这个立方体可以在下面的截图中看到:\n\n![Figure 6.4: Cube blocking the SightSource of the enemy ](img/B16183_06_04.jpg)\n\n图 6.4:立方体挡住敌人的视线来源\n\n在编辑器中打开级别，选择立方体并进入其详细信息面板的`Collision`部分:\n\n![Figure 6.5: The changes in the level editor ](img/B16183_06_05.jpg)\n\n图 6.5:级别编辑器中的变化\n\n在这里，我们可以看到一些对我们很重要的选项:\n\n*   `SimulationGeneratesHitEvents`，当物体模拟物理时，允许调用`OnHit`事件(我们将在本章后面讨论)。\n*   `GenerateOverlapEvents`，允许调用`OnBeginOverlap`和`OnEndOverlap`事件。\n*   `CanCharacterStepUpOn`，可以让角色轻松登上这个物体。\n*   `CollisionPresets`，允许我们指定该对象如何响应每个碰撞通道。\n\n让我们将`CollisionPresets`值从`Default`更改为`Custom`，并查看显示的新选项:\n\n![Figure 6.6: Changes in Collision Presets ](img/B16183_06_06.jpg)\n\n图 6.6:碰撞预设的变化\n\n这些选项中的第一个是`CollisionEnabled`属性。它允许您指定要考虑碰撞的哪些方面:查询、物理、两者或无。同样，物理碰撞与物理模拟有关(该对象是否会被模拟物理的其他对象考虑)，而查询碰撞与碰撞事件以及对象是否会阻止彼此的移动有关:\n\n![Figure 6.7: Collision Enabled for Query and Physics ](img/B16183_06_07.jpg)\n\n图 6.7:为查询和物理启用冲突\n\n第二个选项是`ObjectType`属性。这与跟踪通道概念非常相似，但专门用于对象碰撞，最重要的是，它规定了这是什么类型的碰撞对象。UE4 附带的对象类型值如下:\n\n*   `WorldStatic`:不动的物体(建筑物、构筑物等)\n*   `WorldDynamic`:可以移动的物体(由代码触发移动的物体，玩家可以拾取移动的物体，等等)\n*   `Pawn`:用于可以在关卡中控制和移动的棋子\n*   `PhysicsBody`:用于模拟物理的物体\n*   `Vehicle`:用于车辆对象\n*   `Destructible`:用于可破坏的网格\n\n如前所述，您也可以创建自己的自定义对象类型(这将在本章后面提到)，类似于您可以如何创建自己的跟踪通道(*，这在上一章*中有介绍)。\n\n我们最后的选择与`Collision Responses`有关。假定这个`Cube`对象有默认的碰撞选项，所有的响应都被设置为`Block`，这意味着这个对象将阻挡所有的线轨迹和所有阻挡`WorldStatic`对象的对象，假定这是这个对象的类型。\n\n由于碰撞属性有如此多的不同组合，UE4 允许您以碰撞预设的形式对碰撞属性值进行分组。\n\n回到`CollisionPresets`属性，当前设置为`Custom`，*点击*，可以看到所有可能的选项。现有的一些`Collision Presets`如下:\n\n**无碰撞**:用于不受任何碰撞影响的物体:\n\n*   `Collision Enabled` : `NoCollision`\n*   `Object Type` : `WorldStatic`\n*   答复:不相关\n*   示例:纯视觉和远距离的对象，例如玩家永远无法触及的对象\n\n**阻挡所有**:用于静止的物体，阻挡所有其他物体:\n\n*   `Collision Enabled` : `Query`和`Physics`\n*   `Object Type` : `WorldStatic`\n*   响应:`Block`所有通道\n*   示例:靠近玩家角色并阻止其移动的对象，例如地板和墙壁，它们将始终保持静止\n\n**全部重叠**:用于静态对象，并与所有其他对象重叠:\n\n*   `Collision Enabled`:仅限`Query`\n*   `Object Type` : `WorldStatic`\n*   响应:`Overlap`所有通道\n*   示例:放置在关卡中的触发框，该关卡将始终保持静止\n\n**阻挡所有动态**:类似于`Block All`预设，但是对于在游戏过程中可能改变变换的动态对象(`Object Type` : `WorldDynamic`)\n\n**重叠所有动态**:类似于`Overlap All`预设，但是对于在游戏过程中可能改变变换的动态对象(`Object Type` : `WorldDynamic`)\n\n**棋子**:用于棋子和人物:\n\n*   `Collision Enabled` : `Query`和`Physics`\n*   `Object Type` : `Pawn`\n*   响应:`Block`所有通道，`Ignore`能见度通道\n*   示例:玩家角色和不可玩角色\n\n**物理演员**:用于模拟物理的物体:\n\n*   `Collision Enabled` : `Query`和`Physics`\n*   `Object Type` : `PhysicsBody`\n*   响应:`Block`所有通道\n*   示例:受物理影响的对象，例如从地板和墙壁反弹的球\n\n就像其他碰撞属性一样，您也可以创建自己的碰撞预设。\n\n注意\n\n你可以在这里找到 UE4 碰撞响应的完整参考资料:[https://docs . unrealengine . com/en-US/Engine/Physics/conflict/Reference](https://docs.unrealengine.com/en-US/Engine/Physics/Collision/Reference)。\n\n现在我们已经了解了碰撞的基本概念，让我们开始创建`Dodgeball`类。下一个练习将引导你去做。\n\n## 练习 6.01:创建躲避球课\n\n在本练习中，我们将创建我们的`Dodgeball`类，它将被我们的敌人投掷并从地板和墙壁上反弹，就像一个真正的躲避球一样。\n\n在我们真正开始创建`Dodgeball` C++ 类及其逻辑之前，我们应该为它设置所有必要的冲突设置。\n\n以下步骤将帮助您完成本练习:\n\n1.  打开我们的`Project Settings`，进入`Engine`部分的`Collision`小节。目前没有对象通道，所以您需要创建一个新的。\n2.  按下`New Object Channel`按钮，命名为`Dodgeball`，将其`Default Response`设置为`Block`。\n3.  完成后，展开`Preset`部分。在这里，你可以找到 UE4 中所有可用的默认预设。如果您选择其中一个并按下`Edit`选项，您可以更改`Preset`碰撞的设置。\n4.  通过按下`New`选项创建您自己的`Preset`。我们希望我们的`Dodgeball` `Preset`设置如下:\n    *   `Name` : `Dodgeball`\n    *   `CollisionEnabled` : `Collision Enabled (Query and Physics)`(我们希望在物理模拟和碰撞事件中考虑这一点)\n    *   `Object Type` : `Dodgeball`\n    *   `Collision Responses`:大部分选项选择*遮挡*，但是*忽略*摄像头和`EnemySight`(我们不希望躲避球遮挡摄像头或者敌人的视线)\n5.  Once you've selected the correct options, press `Accept`.\n\n    现在`Dodgeball`类的碰撞设置已经设置好了，让我们创建`Dodgeball` C++ 类。\n\n6.  在`Content Browser`内，*右键点击*，选择`New C++ Class`。\n7.  选择`Actor`作为父类。\n8.  选择`DodgeballProjectile`作为类的名称(我们的项目已经命名为`Dodgeball`，所以我们不能把这个新类也命名为那个)。\n9.  在 Visual Studio 中打开`DodgeballProjectile`类文件。我们要做的第一件事是添加躲避球的碰撞组件，所以我们将在我们的类头中添加一个`SphereComponent`(*演员组件属性通常是私有的* ):\n\n    ```cpp\n    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category =   Dodgeball, meta = (AllowPrivateAccess = \"true\"))\n    class USphereComponent* SphereComponent;\n    ```\n\n10.  Next, include the `SphereComponent` class at the top of our source file:\n\n    ```cpp\n    #include \"Components/SphereComponent.h\"\n    ```\n\n    注意\n\n    请记住，所有头文件包含必须在. generated.h include 之前。\n\n    现在，转到`DodgeballProjectile`类的构造函数，在其源文件中，并执行以下步骤。\n\n11.  创建`SphereComponent`对象:\n\n    ```cpp\n    SphereComponent = CreateDefaultSubobject<USphereComponent>(TEXT(\"Sphere   Collision\"));\n    ```\n\n12.  将其`radius`设置为`35`单位:\n\n    ```cpp\n    SphereComponent->SetSphereRadius(35.f);\n    ```\n\n13.  将其`Collision Preset`设置为我们创建的`Dodgeball`预设:\n\n    ```cpp\n    SphereComponent->SetCollisionProfileName(FName(\"Dodgeball\"));\n    ```\n\n14.  我们希望`Dodgeball`模拟物理，所以通知这个组件，如下面的代码片段所示:\n\n    ```cpp\n    SphereComponent->SetSimulatePhysics(true);\n    ```\n\n15.  We want the `Dodgeball` to call the `OnHit` event while simulating physics, so call the `SetNotifyRigidBodyCollision` function in order to set that to `true` (this is the same as the `SimulationGeneratesHitEvents` property that we saw in the `Collision` section of an object's properties):\n\n    ```cpp\n    //Simulation generates Hit events\n    SphereComponent->SetNotifyRigidBodyCollision(true);\n    ```\n\n    我们也会想听听`SphereComponent`的`OnHit`事件。\n\n16.  在`DodgeballProjectile`类的头文件中为`OnHit`事件被触发时将被调用的函数创建一个声明。这个功能应该叫`OnHit`。应该是`public`，什么都不返回(`void`)，有`UFUNCTION`宏，接收一些参数，按照这个顺序:\n    *   `UPrimitiveComponent* HitComp`:被击中的属于这个演员的部件。基本组件是具有`Transform`属性和某种几何图形的参与者组件(例如，`Mesh`或`Shape`组件)。\n    *   `AActor* OtherActor`:碰撞中涉及的另一个演员。\n    *   `UPrimitiveComponent* OtherComp`:被击中的属于对方演员的部件。\n    *   `FVector NormalImpulse`:物体被击中后移动的方向，以及用多大的力(通过检查矢量的大小)。此参数仅对模拟物理的对象非零。\n    *   `FHitResult& Hit`: The data of the `Hit` resulting from the collision between this object and the other object. As we saw in the previous chapter, it contains properties such as the location of the `Hit`, its normal, which component and actor it hit, and so on. Most of the relevant information is already available to us through the other parameters, but if you need more detailed information, you can access this parameter:\n\n        ```cpp\n        UFUNCTION()\n        void OnHit(UPrimitiveComponent* HitComp, AActor* OtherActor,   UPrimitiveComponent* OtherComp, FVector NormalImpulse, const   FHitResult& Hit);\n        ```\n\n        将`OnHit`函数的实现添加到类的源文件中，并且在该函数中，至少在目前，当躲避球击中玩家时摧毁它。\n\n17.  Cast the `OtherActor` parameter to our `DodgeballCharacter` class and check if the value is not a `nullptr`. If it's not, which means that the other actor we hit is a `DodgeballCharacter`, we'll destroy this `DodgeballProjectile` actor:\n\n    ```cpp\n    void ADodgeballProjectile::OnHit(UPrimitiveComponent *   HitComp, AActor * OtherActor, UPrimitiveComponent *   OtherComp, FVector NormalImpulse, const FHitResult & Hit)\n    {\n      if (Cast<ADodgeballCharacter>(OtherActor) != nullptr)\n      {\n        Destroy();\n      }\n    }\n    ```\n\n    假设我们引用的是`DodgebalCharacter`类，我们需要将它包含在这个类的源文件的顶部:\n\n    ```cpp\n    #include \"DodgeballCharacter.h\"\n    ```\n\n    注意\n\n    在下一章中，我们将改变这个功能，这样我们就可以让躲避球在摧毁自己之前先伤害玩家。当我们谈论演员组件时，我们将这样做。\n\n18.  Head back to the `DodgeballProjectile` class's constructor and add the following line at the end in order to listen to the `OnHit` event of `SphereComponent`:\n\n    ```cpp\n    // Listen to the OnComponentHit event by binding it to our   function\n    SphereComponent->OnComponentHit.AddDynamic(this,   &ADodgeballProjectile::OnHit);\n    ```\n\n    这将把我们创建的`OnHit`函数绑定到这个`SphereComponent` `OnHit`事件(因为这是一个演员组件，这个事件被称为`OnComponentHit`，这意味着我们的函数将与那个事件一起被调用。\n\n19.  Lastly, make `SphereComponent` this actor's `RootComponent`, as shown in the following code snippet:\n\n    ```cpp\n    // Set this Sphere Component as the root component,\n    // otherwise collision won't behave properly\n    RootComponent = SphereComponent;\n    ```\n\n    注意\n\n    为了让一个移动的演员在碰撞中表现正确，无论是否模拟物理，演员的主要碰撞成分通常必须是它的`RootComponent`。\n\n    例如，`Character`类的`RootComponent`是一个胶囊碰撞器组件，因为那个演员会四处移动，而那个组件是角色与环境碰撞的主要方式。\n\n    现在我们已经添加了`DodgeballProjectile` C++ 类的逻辑，让我们继续创建我们的蓝图类。\n\n20.  编译您的更改并打开编辑器。\n21.  进入内容浏览器中的`Content` > `ThirdPersonCPP` > `Blueprints`目录，右键点击，新建一个蓝图类。\n22.  展开`All Classes`部分，搜索`DodgeballProjectile`类，然后将其设置为父类。\n23.  命名新蓝图类`BP_DodgeballProjectile`。\n24.  打开这个新的蓝图类。\n25.  Notice the wireframe representation of the `SphereCollision` component in the actor's Viewport window (this is hidden by default during the game, but you can change that property in this component's `Rendering` section by changing its `HiddenInGame` property):\n\n    ![Figure 6.8: Visual wireframe representation of the SphereCollision component ](img/B16183_06_08.jpg)\n\n    图 6.8:球体生态网格组件的可视化线框表示\n\n26.  Now, add a new `Sphere` mesh as a child of the existing `Sphere Collision` component:\n\n    ![Figure 6.9: Adding a Sphere mesh ](img/B16183_06_09.jpg)\n\n    图 6.9:添加球体网格\n\n27.  Change its scale to `0.65`, as shown in the following screenshot:\n\n    ![Figure 6.10: Updating the scale ](img/B16183_06_10.jpg)\n\n    图 6.10:更新比例\n\n28.  Set its `Collision Presets` to `NoCollision`:\n\n    ![Figure 6.11: Updating Collision Presets to NoCollision ](img/B16183_06_11.jpg)\n\n    图 6.11:将碰撞预设更新为无碰撞\n\n29.  Finally, open our level and place an instance of the `BP_DodgeballProjectile` class near the player (this one was placed at a height of 600 units):\n\n    ![Figure 6.12: Dodgeball bouncing on the ground ](img/B16183_06_12.jpg)\n\n图 6.12:躲避球在地上弹跳\n\n做完这些后，再玩关卡。你会注意到躲避球会受到重力的影响，在静止之前会离开地面几次。\n\n通过完成本练习，您已经创建了一个行为类似于物理对象的对象。\n\n现在，您知道如何创建自己的碰撞对象类型，使用`OnHit`事件，以及更改对象的碰撞属性。\n\n注意\n\n上一章我们简单提到了`LineTraceSingleByObjectType`。现在我们已经知道了对象碰撞是如何工作的，我们可以简单地提到它的用途:当执行一个检查跟踪通道的线跟踪时，您应该使用`LineTraceSingleByChannel`功能；当执行检查`Object`通道(对象类型)的线跟踪时，您应该使用`LineTraceSingleByObjectType`功能。应该明确的是，与`LineTraceSingleByChannel`函数不同，该函数不会检查阻挡特定对象类型的对象，而是检查那些特定对象类型的对象。这两个函数具有完全相同的参数，并且跟踪通道和对象通道都可以通过`ECollisionChannel`枚举获得。\n\n但是如果你想让球从地板上反弹更多次呢？如果你想让它更有弹性呢？这就是物理材料的来源。\n\n# 物理材料\n\n在 UE4 中，您可以通过“物理材料”自定义对象在模拟物理时的行为方式。为了进入这种新型资产，让我们创建自己的:\n\n1.  在`Content`文件夹内创建一个名为`Physics`的新文件夹。\n2.  *在该文件夹中右键单击`Content Browser`上的*，在`Create Advanced Asset`部分下，转到`Physics`子部分并选择`Physical Material`。\n3.  命名这种新的物理材料`PM_Dodgeball`。\n4.  Open the asset and take a look at the available options.\n\n    ![Figure 6.13: Asset options ](img/B16183_06_13.jpg)\n\n图 6.13:资产选项\n\n我们应该注意的主要选项如下:\n\n*   `Friction`:这个属性从`0`到`1`指定摩擦力会对这个物体产生多大的影响(`0`表示这个物体会像在冰上一样滑动，`1`表示这个物体会像一块口香糖一样粘在一起)。\n*   `Restitution`(也称*弹跳*):该属性从`0`到`1`并指定与另一个物体碰撞后将保持多少速度(`0`表示该物体永远不会从地面反弹，而`1`表示该物体将长时间反弹)。\n*   `Density`:此属性指定该对象的密度(即相对于其网格的重量)。两个物体的大小可以相同，但是如果一个物体的密度是另一个物体的两倍，那就意味着它的重量是另一个物体的两倍。\n\n为了让我们的`DodgeballProjectile`对象表现得更接近真实的躲避球，它必须承受相当大的摩擦(默认值是`0.7`，这已经足够高了)并且非常有弹性。让我们将该物理材料的`Restitution`属性增加到`0.95`。\n\n完成此操作后，打开`BP_DodgeballProjectile`蓝图类，并将球体碰撞组件在其`Collision`部分内的物理材质更改为我们刚刚创建的材质`PM_Dodgeball`:\n\n![Figure 6.14: Updating the BP_DodgeballProjectile Blueprint class ](img/B16183_06_14.jpg)\n\n图 6.14:更新 BP _ 躲避球投射蓝图类\n\n注意\n\n确保你添加到关卡中的躲避球演员的实例也有这个物理材料。\n\n如果你再玩一次我们在*练习 6.01* 、*创建躲避球类*中创建的关卡，你会注意到我们的`BP_DodgeballProjectile`现在会在静止之前从地面反弹几次，表现得更像一个真正的躲避球。\n\n做了所有这些，我们只是错过了一件事，让我们的`Dodgeball`演员表现得像一个真正的躲避球。现在，我们没有办法扔掉它。因此，让我们通过创建一个投射物运动组件来解决这个问题，这是我们将在下一个练习中做的事情。\n\n在前面的章节中，当我们复制第三人称模板项目时，我们了解到 UE4 附带的`Character`类有一个`CharacterMovementComponent`。这个 actor 组件允许一个 actor 以各种方式在级别中移动，并且有许多属性允许您根据自己的偏好对其进行自定义。然而，还有另一个同样常用的运动部件:`ProjectileMovementComponent`。\n\n`ProjectileMovementComponent` actor 组件用于将投射物的行为归于一个 actor。它允许你设置初始速度，重力，甚至一些物理模拟参数，如`Bounciness`和`Friction`。然而，鉴于我们的`Dodgeball Projectile`已经在模拟物理，我们将使用的唯一属性是`InitialSpeed`。\n\n## 练习 6.02:向躲避球投射体添加投射体移动组件\n\n在本练习中，我们将在`DodgeballProjectile`上添加一个`ProjectileMovementComponent`，使其具有初始水平速度。我们这样做是为了让它能被我们的敌人抛出，而不只是垂直落下。\n\n以下步骤将帮助您完成本练习:\n\n1.  向`DodgeballProjectile`类的头文件添加一个`ProjectileMovementComponent`属性:\n\n    ```cpp\n    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category =   Dodgeball, meta = (AllowPrivateAccess = \"true\"))\n    class UProjectileMovementComponent* ProjectileMovement;\n    ```\n\n2.  在类的源文件顶部包含`ProjectileMovementComponent`类:\n\n    ```cpp\n    #include \"GameFramework/ProjectileMovementComponent.h\"\n    ```\n\n3.  在类构造函数的末尾，创建`ProjectileMovementComponent`对象:\n\n    ```cpp\n    ProjectileMovement = CreateDefaultSubobject<UProjectileMovementComponent>(TEXT(\"Pro   jectile Movement\"));\n    ```\n\n4.  然后，将其`InitialSpeed`设置为`1500`单位:\n\n    ```cpp\n    ProjectileMovement->InitialSpeed = 1500.f;\n    ```\n\n完成后，编译您的项目并打开编辑器。为了演示躲避球的初始速度，降低其在 *Z* 轴上的位置，并将其放在玩家后面(*这一个被放置在 200 单位的高度*):\n\n![Figure 6.15: Dodgeball moving along the X axis ](img/B16183_06_15.jpg)\n\n图 6.15:躲避球沿 X 轴移动\n\n当你玩关卡时，你会注意到躲避球开始向它的 *X* 轴(*红色箭头*)移动:\n\n这样，我们就可以结束我们的练习了。我们的`DodgeballProjectile`现在表现得像一个真正的躲避球。它落下，反弹，然后被抛出。\n\n我们项目的下一步将是给我们的`EnemyCharacter`添加逻辑，这样它就可以向玩家扔躲避球，但是在我们解决这个问题之前，我们必须解决计时器的概念。\n\n# 计时器\n\n考虑到电子游戏的性质和它们强烈基于事件的事实，每个游戏开发工具都必须有一种方法让你在事情发生之前造成延迟或等待时间。例如，当你玩在线死亡匹配游戏时，你的角色可以死亡然后重生，通常，重生事件不会在你的角色死亡的瞬间发生，而是在几秒钟后。有许多情况下，你希望某件事发生，但只是在一定时间后。我们的`EnemyCharacter`将会是这种情况，每隔几秒就会投掷闪避球。这种延迟或等待时间可以通过定时器来实现。\n\n一个**定时器**允许你在一定时间后调用一个函数。您可以选择以一个时间间隔循环函数调用，也可以在循环开始前设置一个延迟。如果你想让计时器停止，你也可以这样做。\n\n我们将使用计时器，以便我们的敌人每`X`时间扔一个闪避球，无限期地，只要它能看到玩家角色，然后当敌人不能再看到它的目标时停止那个计时器。\n\n在我们开始给我们的`EnemyCharacter`类添加逻辑，让它向玩家扔闪避球之前，我们应该看一下另一个话题，那就是如何催生演员。\n\n# 产卵演员\n\n在*第 1 章**虚幻引擎介绍*中，你通过编辑器学会了如何在关卡中放置你创建的一个演员，但是如果你想在游戏进行的时候将那个演员放置在关卡中呢？这就是我们现在要看的。\n\n与大多数其他游戏开发工具一样，UE4 允许您在游戏本身运行时在游戏中放置一个演员。这个过程叫做**产卵**。为了在 UE4 中产生一个参与者，我们需要调用`SpawnActor`函数，该函数可从`World`对象获得(如前所述，我们可以使用`GetWorld`函数访问该对象)。但是`SpawnActor`函数有几个参数需要传递，如下所示:\n\n*   一个`UClass*`属性，它让函数知道将要产生的对象的类。该属性可以是 C++ 类，可通过`NameOfC++ Class::StaticClass()`函数获得，也可以是蓝图类，可通过`TSubclassOf`属性获得。一般来说，最好不要直接从 C++ 类中派生出参与者，而是创建一个蓝图类并生成它的实例。\n*   `TSubclassOf`属性是一种在 C++ 中引用蓝图类的方式。它用于引用 C++ 代码中的一个类，这个类可能是一个蓝图类。您用模板参数声明了一个`TSubclassOf`属性，它是类必须继承的 C++ 类。在下一个练习中，我们将了解如何在实践中使用这个属性。\n*   要么是`FTransform`属性，要么是`FVector`和`FRotator`属性，这将指示我们想要生成的对象的位置、旋转和缩放。\n*   一个可选的`FActorSpawnParameters`属性，允许你指定更多特定于产卵过程的属性，比如谁导致了行为人产卵(也就是`Instigator`)，如果它产卵的位置被其他对象占据了，那么如何处理对象产卵，这可能会导致重叠或者阻塞事件等等。\n\n`SpawnActor`函数会将一个实例返回给这个函数产生的执行元。假设它也是一个模板函数，您可以这样调用它:您可以直接使用模板参数接收对您生成的参与者类型的引用:\n\n```cpp\nGetWorld()->SpawnActor<NameOfC++ Class>(ClassReference,   SpawnLocation, SpawnRotation);\n```\n\n在这种情况下，`SpawnActor`函数被调用，在这里我们生成了一个`NameOfC++ Class`类的实例。这里，我们提供了对具有`ClassReference`属性的类的引用，以及分别使用`SpawnLocation`和`SpawnRotation`属性生成的角色的位置和旋转。\n\n您将在*练习 6.03* 、*向敌人角色*中学习如何应用这些属性。\n\n不过，在我们继续练习之前，我想简单地提一下`SpawnActor`函数的一个变体，它也可能派上用场:`SpawnActorDeferred`函数。虽然`SpawnActor`函数将创建您指定的对象的一个实例，然后将其放置在世界中，但这个新的`SpawnActorDeferred`函数将创建您想要的对象的一个实例，并且仅在您调用演员的`FinishSpawning`函数时将其放置在世界中。\n\n例如，假设我们想在闪避球产生的那一刻改变它的`InitialSpeed`。如果我们使用`SpawnActor`功能，躲避球有可能会在我们设置其`InitialSpeed`属性之前开始移动。然而，通过使用`SpawnActorDeferred`功能，我们可以创建一个闪避球的实例，然后将其`InitialSpeed`设置为我们想要的任何值，并且只有通过调用新创建的闪避球的`FinishSpawning`功能才能将其放置在世界上，该功能的实例由`SpawnActorDeferred`功能返回给我们。\n\n现在我们知道了如何在世界上产生一个演员，也知道了计时器的概念，我们可以将负责投掷闪避球的逻辑添加到我们的`EnemyCharacter`类中，这就是我们在下一个练习中要做的。\n\n## 练习 6.03:给敌人角色添加投掷逻辑\n\n在本练习中，我们将把负责投掷我们刚刚创建的躲避球演员的逻辑添加到我们的`EnemyCharacter`类中。\n\n在 Visual Studio 中打开该类的文件以便开始。我们将从修改我们的`LookAtActor`函数开始，这样我们就可以保存告诉我们是否可以看到玩家的值，并使用它来管理我们的计时器。\n\n按照以下步骤完成本练习:\n\n1.  在`EnemyCharacter`类的头文件中，将`LookAtActor`函数的返回类型从`void`改为`bool` :\n\n    ```cpp\n    // Change the rotation of the character to face the given   actor\n    // Returns whether the given actor can be seen\n    bool LookAtActor(AActor* TargetActor);\n    ```\n\n2.  在函数的实现中，在类的源文件中做同样的事情，同时在我们调用`CanSeeActor`函数的`if`语句的末尾返回`true`。此外，在第一个`if`语句中返回`false`，在这里我们检查`TargetActor`是否是`nullptr`并且在函数的末尾:\n\n    ```cpp\n    bool AEnemyCharacter::LookAtActor(AActor * TargetActor)\n    {\n      if (TargetActor == nullptr) return false;\n      if (CanSeeActor(TargetActor))\n      {\n        FVector Start = GetActorLocation();\n        FVector End = TargetActor->GetActorLocation();\n        // Calculate the necessary rotation for the Start point to   face the End point\n        FRotator LookAtRotation = UKismetMathLibrary::FindLookAtRotation(Start, End);\n        //Set the enemy's rotation to that rotation\n        SetActorRotation(LookAtRotation);\n        return true;\n      }\n      return false;\n    }\n    ```\n\n3.  接下来，添加两个`bool`属性，`bCanSeePlayer`和`bPreviousCanSeePlayer`，在你类的头文件中设置为`protected`，分别代表从敌方角色的角度是否可以在这一帧看到玩家，以及在最后一帧是否可以看到玩家:\n\n    ```cpp\n    //Whether the enemy can see the player this frame\n    bool bCanSeePlayer = false;\n    //Whether the enemy could see the player last frame\n    bool bPreviousCanSeePlayer = false;\n    ```\n\n4.  然后，转到您的类的`Tick`函数实现，并将`bCanSeePlayer`的值设置为`LookAtActor`函数的返回值。这将取代之前对`LookAtActor`功能的调用:\n\n    ```cpp\n    // Look at the player character every frame\n    bCanSeePlayer = LookAtActor(PlayerCharacter);\n    ```\n\n5.  之后，将`bPreviousCanSeePlayer`的值设置为`bCanSeePlayer`的值:\n\n    ```cpp\n    bPreviousCanSeePlayer = bCanSeePlayer;\n    ```\n\n6.  在前两行之间，添加一条`if`语句，检查`bCanSeePlayer`和`bPreviousCanSeePlayer`的值是否不同。这将意味着要么我们看不到玩家最后一帧，现在我们可以，要么我们可以看到玩家最后一帧，现在我们不能:\n\n    ```cpp\n    bCanSeePlayer = LookAtActor(PlayerCharacter);\n    if (bCanSeePlayer != bPreviousCanSeePlayer)\n    {\n    }\n    bPreviousCanSeePlayer = bCanSeePlayer;\n    ```\n\n7.  在这个`if`语句中，如果我们能看到玩家，我们想启动一个计时器，如果我们不能再看到玩家，我们想停止这个计时器:\n\n    ```cpp\n    if (bCanSeePlayer != bPreviousCanSeePlayer)\n    {\n      if (bCanSeePlayer)\n      {\n        //Start throwing dodgeballs\n      }\n      else\n      {\n        //Stop throwing dodgeballs\n      }\n    }\n    ```\n\n8.  为了启动计时器，我们需要在类的头文件中添加以下属性，这些属性都可以是`protected`:\n    *   一个`FTimerHandle`属性，负责识别我们要启动哪个定时器。它基本上作为一个特定定时器的标识符:\n\n        ```cpp\n        FTimerHandle ThrowTimerHandle;\n        ```\n\n    *   一个`float`属性，表示投掷躲避球之间等待的时间(间隔)，这样我们就可以循环计时器。我们给它一个默认值`2`秒:\n\n        ```cpp\n        float ThrowingInterval = 2.f;\n        ```\n\n    *   另一个`float`属性，表示计时器开始循环之前的初始延迟。让我们给它一个默认值`0.5`秒:\n\n        ```cpp\n        float ThrowingDelay = 0.5f;\n        ```\n\n    *   A function to be called every time the timer ends, which we will create and call `ThrowDodgeball`. This function doesn't return anything and doesn't receive any parameters:\n\n        ```cpp\n        void ThrowDodgeball();\n        ```\n\n        在我们调用适当的函数来启动计时器之前，我们需要在源文件中为负责的对象`FTimerManager`添加一个`#include`。\n\n        每个`World`都有一个定时器管理器，可以启动和停止定时器，并访问与它们相关的功能，如它们是否仍然活跃，运行多长时间等:\n\n        ```cpp\n        #include \"TimerManager.h\"\n        ```\n\n9.  现在，使用`GetWorldTimerManager`功能进入当前世界的计时器管理器:\n\n    ```cpp\n    GetWorldTimerManager()\n    ```\n\n10.  接下来，调用定时器管理器的`SetTimer`功能，如果我们能看到玩家角色，以便启动负责投掷闪避球的定时器。`SetTimer`功能接收以下参数:\n    *   代表所需计时器的`FTimerHandle`:`ThrowTimerHandle`。\n    *   要调用的函数所属的对象:`this`。\n    *   要调用的函数，必须在它的名字前面加上`&ClassName::`来指定，结果是`&AEnemyCharacter::ThrowDodgeball`。\n    *   计时器的速率或间隔:`ThrowingInterval`。\n    *   这个计时器是否会循环:`true`。\n    *   The delay before this timer starts looping: `ThrowingDelay`.\n\n        以下代码片段包含这些参数:\n\n        ```cpp\n        if (bCanSeePlayer)\n        {\n          //Start throwing dodgeballs\n          GetWorldTimerManager().SetTimer(ThrowTimerHandle,this,  &AEnemyCharacter::ThrowDodgeball,ThrowingInterval,true,  ThrowingDelay);\n        }\n        ```\n\n11.  If we can no longer see the player and we want to stop the timer, we can do so using the `ClearTimer` function. This function only needs to receive an `FTimerHandle` property as a parameter:\n\n    ```cpp\n    else\n    {\n      //Stop throwing dodgeballs\n      GetWorldTimerManager().ClearTimer(ThrowTimerHandle);\n    }\n    ```\n\n    唯一剩下的就是实现`ThrowDodgeball`功能。该功能将负责产生一个新的`DodgeballProjectile`演员。为了做到这一点，我们需要一个对我们想要生成的类的引用，这个类必须从`DodgeballProjectile`继承，所以接下来我们需要做的是使用`TSubclassOf`对象创建适当的属性。\n\n12.  在`EnemyCharacter`头文件中创建`TSubclassOf`属性，可以是`public` :\n\n    ```cpp\n    //The class used to spawn a dodgeball object\n    UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category =   Dodgeball)\n    TSubclassOf<class ADodgeballProjectile> DodgeballClass;\n    ```\n\n13.  因为我们将使用`DodgeballProjectile`类，我们还需要将它包含在`EnemyCharacter`源文件中:\n\n    ```cpp\n    #include \"DodgeballProjectile.h\"\n    ```\n\n14.  然后，在源文件中的`ThrowDodgeball`函数实现中，首先检查该属性是否为`nullptr`。如果是，我们`return`立即:\n\n    ```cpp\n    void AEnemyCharacter::ThrowDodgeball()\n    {\n      if (DodgeballClass == nullptr)\n      {\n        return;\n      }\n    }\n    ```\n\n15.  Next, we will be spawning a new actor from that class. Its location will be `40` units in front of the enemy and its rotation will be the same as the enemy. In order to spawn the Dodgeball in front of the enemy character, we'll need to access the enemy's `ForwardVector` property, which is a unitary `FVector` (*meaning that its length is 1*) that indicates the direction an actor is facing, and multiply it by the distance at which we want to spawn our dodgeball, which is `40` units:\n\n    ```cpp\n    FVector ForwardVector = GetActorForwardVector();\n    float SpawnDistance = 40.f;\n    FVector SpawnLocation = GetActorLocation() + (ForwardVector *   SpawnDistance);\n    //Spawn new dodgeball\n    GetWorld()->SpawnActor<ADodgeballProjectile>(DodgeballClass,   SpawnLocation, GetActorRotation());\n    ```\n\n    我们需要对`EnemyCharacter`类进行的修改到此结束。在我们设置完这个逻辑的蓝图之前，让我们快速修改一下我们的`DodgeballProjectile`类。\n\n16.  在 Visual Studio 中打开`DodgeballProjectile`类的源文件。\n17.  Within its `BeginPlay` event, set its `LifeSpan` to `5` seconds. This property, which belongs to all actors, dictates how much longer they will remain in the game before being destroyed. By setting our dodgeball's `LifeSpan` to `5` seconds on its `BeginPlay` event, we are telling UE4 to destroy that object 5 seconds after it's spawned (*or, if it's already been placed in the level, 5 seconds after the game starts*). We will do this so that the floor isn't filled with dodge balls after a certain amount of time, which would make the game unintentionally difficult for the player:\n\n    ```cpp\n    void ADodgeballProjectile::BeginPlay()\n    {\n      Super::BeginPlay();\n\n      SetLifeSpan(5.f);\n    }\n    ```\n\n    现在我们已经完成了与`EnemyCharacter`类的躲避球投掷逻辑相关的 C++ 逻辑，让我们编译我们的更改，打开编辑器，然后打开我们的`BP_EnemyCharacter`蓝图。在那里，前往`Class Defaults`面板，将`DodgeballClass`房产的价值更改为`BP_DodgeballProjectile`:\n\n    ![Figure 6.16: Updating the Dodgeball Class ](img/B16183_06_16.jpg)\n\n图 6.16:更新躲避球类\n\n完成此操作后，您可以移除我们在关卡中放置的`BP_DodgeballProjectile`类的现有实例，如果它仍然存在的话。\n\n现在，我们可以发挥我们的水平。你会注意到，敌人几乎会立即开始向玩家投掷闪避球，并且只要玩家角色在视野中，就会继续这样做:\n\n![Figure 6.17: Enemy character throwing dodgeballs if the player is in sight ](img/B16183_06_17.jpg)\n\n图 6.17:如果玩家在视线范围内，敌方角色投掷躲避球\n\n至此，我们已经完成了`EnemyCharacter`的闪避投球逻辑。你现在知道如何使用计时器了，计时器是任何游戏程序员的必备工具。\n\n# 墙壁\n\n我们项目的下一步是创建`Wall`类。我们将有两种类型的墙:\n\n*   一面普通的墙，它会挡住敌人的视线，玩家角色和闪避球。\n*   一面鬼墙，只会挡住玩家角色，无视敌人的视线和闪避球。您可能会在特定类型的益智游戏中发现这种类型的碰撞设置。\n\n在下一个练习中，我们将创建这两个 Wall 类。\n\n## 练习 6.04:创建墙类\n\n在本练习中，我们将创建代表普通`Wall`和`GhostWall`的`Wall`类，它们只会阻挡玩家角色的移动，而不会阻挡敌人的视线或他们投掷的闪避球。\n\n让我们从正常的`Wall`课开始。这个 C++ 类将基本上是空的，因为它唯一需要的是一个网格，以便反射投射物并阻挡敌人的视线，这将通过它的蓝图类来添加。\n\n以下步骤将帮助您完成本练习:\n\n1.  打开编辑器。\n2.  在内容浏览器的左上角，按下绿色的`Add New`按钮。\n3.  选择顶部的第一个选项；`Add Feature or Content Pack`。\n4.  会出现一个新窗口。选择`Content Packs`标签，然后选择`Starter Content`包装，然后按下`Add To Project`按钮。这将向项目中添加一些基本资产，我们将在本章和后面的一些章节中使用这些资产。\n5.  创建一个新的 C++ 类，名为`Wall`，以`Actor`类为父类。\n6.  接下来，在 Visual Studio 中打开该类的文件，并添加一个`SceneComponent`作为我们的 Wall 的`RootComponent`:\n    *   `Header`文件如下:\n\n        ```cpp\n        private:\n        UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Wall,   meta = (AllowPrivateAccess = \"true\"))\n        class USceneComponent* RootScene;\n        ```\n\n    *   `Source`文件如下:\n\n        ```cpp\n        AWall::AWall()\n        {\n          // Set this actor to call Tick() every frame.  You can turn   this off to improve performance if you don't need it.\n          PrimaryActorTick.bCanEverTick = true;\n          RootScene = CreateDefaultSubobject<USceneComponent>(TEXT(\"Root\"));\n          RootComponent = RootScene;\n        }\n        ```\n\n7.  编译您的代码并打开编辑器。\n8.  接下来，转到内容浏览器中的`Content` > `ThirdPersonCPP` > : `Blueprints`目录，创建一个继承自`Wall`类的新蓝图类，将其命名为`BP_Wall`，并打开该资产。\n9.  添加静态网格组件，并将其`StaticMesh`属性设置为`Wall_400x300`。\n10.  将其`Material`属性设置为`M_Metal_Steel`。\n11.  Set the Static Mesh Component's location on the *X* axis to `–200` units (*so that the mesh is centered relative to our actor's origin*):\n\n    ![Figure 6.18: Updating the Static Mesh Component's location ](img/B16183_06_18.jpg)\n\n图 6.18:更新静态网格组件的位置\n\n这是您的蓝图类的视口应该是什么样子:\n\n![Figure 6.19: Blueprint class's Viewport Wall ](img/B16183_06_19.jpg)\n\n图 6.19:蓝图类的视口墙\n\n注意\n\n一般来说，当不需要碰撞组件时，最好添加一个`SceneComponent`作为对象的`RootComponent`，以便允许其子组件具有更大的灵活性。\n\n演员的`RootComponent`不能修改其位置或旋转，这就是为什么，在我们的例子中，如果我们在 Wall C++ 类中创建了一个静态网格组件，并将其设置为根组件，而不是使用场景组件，我们将很难偏移它。\n\n现在我们已经建立了常规的`Wall`类，让我们创建我们的`GhostWall`类。因为这些类没有任何逻辑设置，我们只是将`GhostWall`类创建为`BP_Wall`蓝图类的子类，而不是我们的 C++ 类。\n\n1.  *右键单击`BP_Wall`资产的*，选择`Create Child Blueprint Class`。\n2.  命名新蓝图`BP_GhostWall`。\n3.  打开它。\n4.  更改静态网格组件的碰撞属性:\n    *   将其`CollisionPreset`设置为`Custom`。\n    *   将其对`EnemySight`和`Dodgeball`频道的响应更改为`Overlap`。\n5.  Change the Static Mesh Component's `Material` property to `M_Metal_Copper`.\n\n    你的`BP_GhostWall`视窗现在应该是这样的:\n\n    ![Figure 6.20: Creating the Ghost Wall ](img/B16183_06_20.jpg)\n\n图 6.20:创建幽灵墙\n\n现在，您已经创建了这两个沃尔演员，请将他们放入关卡中进行测试。将它们的变换设置为以下变换值:\n\n*   墙:`Location` : `(-710, 120, 130)`\n*   Ghost Wall: `Location`: `(-910, -100, 130)`; `Rotation`: `(0, 0, 90)`:\n\n    ![Figure 6.21: Updating the Ghost Wall's locations and rotation  ](img/B16183_06_21.jpg)\n\n图 6.21:更新幽灵墙的位置和旋转\n\n最终结果应该是这样的:\n\n![Figure 6.22: Final outcome with the Ghost Wall and the Wall ](img/B16183_06_22.jpg)\n\n图 6.22:鬼墙和墙的最终结果\n\n你会注意到，当你将你的角色隐藏在普通`Wall`(右边的那个)后面时，敌人不会向玩家投掷闪避球；然而，当你试图将你的角色隐藏在`GhostWall`(左边的那个)后面时，即使敌人无法通过，敌人也会向角色投掷躲避球，他们会像墙不存在一样穿过墙！\n\n我们的练习到此结束。我们制造了我们的`Wall`演员，他们要么表现正常，要么无视敌人的视线和躲避球！\n\n# 胜利箱\n\n我们项目的下一步将是创造`VictoryBox`演员。考虑到玩家已经击败了关卡，当玩家角色进入游戏时，这个演员将负责结束游戏。为了做到这一点，我们将使用`Overlap`事件。下面的练习将帮助我们理解胜利盒子。\n\n## 练习 6.05:创建维多利亚盒子类\n\n在本练习中，我们将创建`VictoryBox`类，当玩家角色输入时，该类将结束游戏。\n\n以下步骤将帮助您完成本练习:\n\n1.  创建一个从 actor 继承的新 C++ 类，并将其称为`VictoryBox`。\n2.  在 Visual Studio 中打开该类的文件。\n3.  创建一个新的`SceneComponent`属性，它将被用作`RootComponent`，就像我们对我们的`Wall` C++ 类所做的那样:\n    *   `Header`文件:\n\n        ```cpp\n        private:\n        UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category =   VictoryBox, meta = (AllowPrivateAccess = \"true\"))\n        class USceneComponent* RootScene;\n        ```\n\n    *   `Source`文件:\n\n        ```cpp\n        AVictoryBox::AVictoryBox()\n        {\n          // Set this actor to call Tick() every frame.  You can turn   this off to improve performance if you don't need it.\n          PrimaryActorTick.bCanEverTick = true;\n          RootScene =   CreateDefaultSubobject<USceneComponent>(TEXT(\"Root\"));\n          RootComponent = RootScene;\n        }\n        ```\n\n4.  在头文件中声明一个`BoxComponent`，用于检查与玩家角色的重叠事件，也应该是`private` :\n\n    ```cpp\n    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category =   VictoryBox, meta = (AllowPrivateAccess = \"true\"))\n    class UBoxComponent* CollisionBox;\n    ```\n\n5.  在类的源文件中包含`BoxComponent`文件:\n\n    ```cpp\n    #include \"Components/BoxComponent.h\"\n    ```\n\n6.  创建`RootScene`组件后，创建`BoxComponent`，也应该是`private` :\n\n    ```cpp\n    RootScene = CreateDefaultSubobject<USceneComponent>(TEXT(\"Root\"));\n    RootComponent = RootScene;\n    CollisionBox =   CreateDefaultSubobject<UBoxComponent>(TEXT(\"Collision Box\"));\n    ```\n\n7.  使用`SetupAttachment`功能将其连接到`RootComponent`:\n\n    ```cpp\n    CollisionBox->SetupAttachment(RootComponent);\n    ```\n\n8.  将其`BoxExtent`属性设置为所有轴上的`60`单位。这将使`BoxComponent`的规模增加一倍`(120 x 120 x 120)` :\n\n    ```cpp\n    CollisionBox->SetBoxExtent(FVector(60.0f, 60.0f, 60.0f));\n    ```\n\n9.  使用`SetRelativeLocation`功能:\n\n    ```cpp\n    CollisionBox->SetRelativeLocation(FVector(0.0f, 0.0f,   120.0f));\n    ```\n\n    将其在 *Z* 轴上的相对位置偏移`120`个单位\n10.  Now, you will require a function that will listen to the `BoxComponent`'s `OnBeginOverlap` event. This event will be called whenever an object enters the `BoxComponent`. This function must be preceded by the `UFUNCTION` macro, be `public`, return nothing, and have the following parameters:\n\n    ```cpp\n    UFUNCTION()\n    void OnBeginOverlap(UPrimitiveComponent* OverlappedComp,   AActor* OtherActor, UPrimitiveComponent* OtherComp, int32   OtherBodyIndex, bool bFromSweep, const FHitResult&   SweepResult);\n    ```\n\n    参数如下:\n\n    *   `UPrimitiveComponent* OverlappedComp`:重叠的属于这个演员的组件。\n    *   `AActor* OtherActor`:参与重叠的另一个演员。\n    *   `UPrimitiveComponent* OtherComp`:重叠的属于另一个行动者的成分。\n    *   `int32 OtherBodyIndex`:被命中的图元中的项目的索引(通常对实例化静态网格组件有用)。\n    *   `bool bFromSweep`:重叠是否源于扫掠痕迹。\n    *   `FHitResult& SweepResult`: The data of the Sweep Trace resulting from the collision between this object and the other object.\n\n        注意\n\n        虽然我们不会在这个项目中使用`OnEndOverlap`事件，但您很可能迟早会需要使用它，所以下面是该事件所需的函数签名，它看起来与我们刚刚了解到的非常相似:\n\n        `UFUNCTION()`\n\n        `void OnEndOverlap(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex);`\n\n11.  接下来，我们需要将这个函数绑定到`BoxComponent`的`OnComponentBeginOverlap`事件:\n\n    ```cpp\n    CollisionBox->OnComponentBeginOverlap.AddDynamic(this,   &AVictoryBox::OnBeginOverlap);\n    ```\n\n12.  Within our `OnBeginOverlap` function implementation, we're going to check whether the actor we overlapped is a `DodgeballCharacter`. Because we'll be referencing this class, we also need to include it:\n\n    ```cpp\n    #include \"DodgeballCharacter.h\" \n    void AVictoryBox::OnBeginOverlap(UPrimitiveComponent *   OverlappedComp, AActor * OtherActor, UPrimitiveComponent *   OtherComp, int32 OtherBodyIndex, bool bFromSweep, const   FHitResult & SweepResult)\n    {\n      if (Cast<ADodgeballCharacter>(OtherActor))\n      {\n      }\n    }\n    ```\n\n    如果我们重叠的演员是一个`DodgeballCharacter`，我们要退出游戏。\n\n13.  我们将为此目的使用`KismetSystemLibrary`。`KismetSystemLibrary`类包含在您的项目中通用的有用函数:\n\n    ```cpp\n    #include \"Kismet/KismetSystemLibrary.h\"\n    ```\n\n14.  In order to quit the game, we will call `KismetSystemLibrary`'s `QuitGame` function. This function receives the following:\n\n    ```cpp\n    UKismetSystemLibrary::QuitGame(GetWorld(),\n      nullptr,\n      EQuitPreference::Quit,\n      true);\n    ```\n\n    前面代码片段中的重要参数解释如下:\n\n    *   一个`World`对象，我们可以用`GetWorld`功能访问。\n    *   一个`PlayerController`对象，我们将其设置为`nullptr`。我们这样做是因为这个函数会自动找到一个。\n    *   一个`EQuitPreference`对象，意思是我们想要结束游戏的方式，要么退出，要么只是把它作为一个后台进程。我们会想实际退出游戏，而不仅仅是把它作为一个后台进程。\n    *   A `bool`, which indicates whether we want to ignore the platform's restrictions when it comes to quitting the game, which we will set to `true`.\n\n        接下来，我们将创建我们的蓝图类。\n\n15.  编译你的修改，打开编辑器，进入`Content Browser`里面的`Content` → `ThirdPersonCPP` → `Blueprint`目录，新建一个继承自`VictoryBox`的蓝图类，命名为`BP_VictoryBox`。打开该资产并进行以下修改:\n    *   添加新的静态网格组件。\n    *   将其`StaticMesh`属性设置为`Floor_400x400`。\n    *   将其`Material`属性设置为`M_Metal_Gold`。\n    *   在所有三个轴上将其刻度设置为`0.75`单位。\n    *   Set its location to `(-150, -150, 20)`, on the *X*, *Y*, and *Z* axes, respectively.\n\n        完成这些更改后，蓝图的“视口”选项卡应该如下所示:\n\n        ![Figure 6.23: Victory box placed in the Blueprint's Viewport tab ](img/B16183_06_23.jpg)\n\n图 6.23:放置在蓝图视口选项卡中的胜利框\n\n将蓝图放入您的级别，测试它的功能:\n\n![Figure 6.24: Victory Box blueprint in the level for testing ](img/B16183_06_24.jpg)\n\n图 6.24:测试级别的胜利箱蓝图\n\n如果你玩了关卡，踩在金盘上(和碰撞框重叠)，你会注意到游戏突然结束，正如预期的那样。\n\n就这样，我们结束了`VictoryBox`课！现在，您知道如何在自己的项目中使用重叠事件了。您可以使用这些事件创建多种游戏机制，因此祝贺您完成本练习。\n\n我们现在已经非常接近本章的结尾，在这里我们将完成一个新的活动，但是首先，我们需要对我们的`DodgeballProjectile`类进行一些修改，即在它的`ProjectileMovementComponent`中添加一个 getter 函数，这将在下一个练习中进行。\n\ngetter 函数是一个只返回特定属性而不做其他事情的函数。这些函数通常被标记为内联的，这意味着当代码编译时，对该函数的调用将简单地被其内容替换。它们通常也被标记为`const`，因为它们不修改类的任何属性。\n\n## 练习 6.06:在闪避弹中添加项目移动组件获取器函数\n\n在本练习中，我们将向`DodgeballProjectile`类的`ProjectileMovement`属性添加一个 getter 函数，以便其他类可以访问它并修改它的属性。我们将在本章的活动中做同样的事情。\n\n为此，您需要遵循以下步骤:\n\n1.  在 Visual Studio 中打开`DodgeballProjectile`类的头文件。\n2.  Add a new `public` function called `GetProjectileMovementComponent`. This function will be an inline function, which in UE4's version of C++ is replaced with the `FORCEINLINE` macro. The function should also return a `UProjectileMovementComponent*` and be a `const` function:\n\n    ```cpp\n    FORCEINLINE class UProjectileMovementComponent*   GetProjectileMovementComponent() const\n    {\n      return ProjectileMovement;\n    }\n    ```\n\n    注意\n\n    对特定函数使用`FORCEINLINE`宏时，不能将该函数的声明添加到头文件中，也不能将其实现添加到源文件中。两者必须在头文件中同时完成，如前所示。\n\n至此，我们结束这个快速练习。在这里，我们已经给我们的`DodgeballProjectile`类添加了一个简单的`getter`函数，我们将在本章的活动中使用这个函数，我们将用`SpawnActorDeferred`函数替换`EnemyCharacter`类中的`SpawnActor`函数。这将允许我们在生成一个实例之前安全地编辑我们的`DodgeballProjectile`类的属性。\n\n## 活动 6.01:用 EnemyCharacter 中指定的 SpawnActor 替换 SpawnActor 函数\n\n在本活动中，您将更改 EnemyCharacter 的`ThrowDodgeball`功能，以便使用`SpawnActorDeferred`功能而不是`SpawnActor`功能，这样我们就可以在生成`DodgeballProjectile`之前更改其`InitialSpeed`。\n\n以下步骤将帮助您完成本活动:\n\n1.  在 Visual Studio 中打开`EnemyCharacter`类的源文件。\n2.  转到`ThrowDodgeball`功能的实现。\n3.  因为`SpawnActorDeferred`函数不能只接收种子位置和旋转属性，而必须接收`FTransform`属性，所以我们需要在调用该函数之前创建一个这样的属性。让我们称之为`SpawnTransform`并按此顺序发送产卵轮换和位置，作为其构造器的输入，这将分别是这个敌人的轮换和`SpawnLocation`属性。\n4.  然后，将`SpawnActor`函数调用更新为`SpawnActorDeferred`函数调用。代替发送产卵位置和产卵旋转作为它的第二和第三个参数，用我们刚刚创建的`SpawnTransform`属性替换它们，作为第二个参数。\n5.  Make sure you save the return value of this function call inside a `ADodgeballProjectile*` property called `Projectile`.\n\n    完成此操作后，您将成功创建一个新的`DodgeballProjectile`对象。然而，我们仍然需要改变它的`InitialSpeed`属性，并实际上衍生它\n\n6.  调用`SpawnActorDeferred`函数后，调用`Projectile`属性的`GetProjectileMovementComponent`函数，该函数返回其射弹运动分量，并将其`InitialSpeed`属性更改为`2200`单位。\n7.  因为我们将在`EnemyCharacter`类中访问属于投射物运动组件的属性，所以我们需要包含该组件，就像我们在*练习 6.02* 、*中所做的那样，向躲避球投射物*添加投射物运动组件。\n8.  更改了`InitialSpeed`属性的值后，剩下要做的唯一事情就是调用`Projectile`属性的`FinishSpawning`函数，该函数将接收我们创建的`SpawnTransform`属性作为参数。\n9.  After you've done this, compile your changes and open the editor.\n\n    预期产出:\n\n    ![Figure 6.25: Dodgeball thrown at the player ](img/B16183_06_25.jpg)\n\n图 6.25:向玩家投掷躲避球\n\n注意\n\n这个活动的解决方案可以在:[https://packt.live/338jEBx](https://packt.live/338jEBx)找到。\n\n通过完成本活动，您已经巩固了`SpawnActorDeferred`功能的使用，并知道如何在未来的项目中使用它。\n\n# 总结\n\n在本章中，您学习了如何使用物理模拟影响对象，创建自己的对象类型和碰撞预设，使用`OnHit`、`OnBeginOverlap`和`OnEndOverlap`事件，更新对象的物理材质，以及使用计时器。\n\n现在，您已经学习了碰撞主题的这些基本概念，您将能够在创建自己的项目时提出新的和创造性的方法来使用它们。\n\n在下一章中，我们将了解 Actor 组件、接口和蓝图函数库，它们对于保持项目的复杂性可管理性和高度模块化非常有用，从而允许您轻松地将一个项目的部分内容添加到另一个项目中。"
  },
  {
    "path": "docs/game-dev-proj-ue/07.md",
    "content": "# 八、用户界面\n\n概观\n\n在这一章中，我们将继续我们在过去几章中一直在做的基于躲避球的游戏。我们将继续这个项目，学习游戏界面(用户界面的缩写)及其一些形式，即菜单和平视显示器。在这一章的最后，你将能够使用 UE4 的游戏 UI 系统 UMG 来制作一个带有可交互按钮的菜单，以及一个通过进度条显示玩家角色当前健康点的 HUD。\n\n# 简介\n\n在前一章中，我们了解了通用实用程序，它允许您通过使用蓝图函数库、参与者组件和接口来正确地组织和组织项目中的代码和资产。\n\n在这一章中，我们将深入到游戏用户界面的话题，这是几乎每个视频游戏中都存在的东西。游戏 UI 是向玩家展示信息的主要方式之一，比如他们还剩下多少生命，武器里有多少子弹，他们携带的是哪种武器等等，并允许玩家通过选择是否继续游戏、创建新游戏、选择他们想在哪个级别玩等等来与游戏进行交互。这主要以图像和文本的形式显示给玩家。\n\n**用户界面**或**ui**通常添加在游戏渲染的顶部，这意味着它们在游戏中看到的所有其他东西的前面，并且表现为图层(您可以像在 Photoshop 中一样将它们添加到另一个的顶部)。但是，有一个例外: *diegetic UI* 。这种类型的用户界面并没有分层到游戏的屏幕上，而是存在于游戏本身内部。这方面的一个很好的例子可以在游戏*死亡空间*中找到，在游戏世界中，你可以在第三人称视角中控制一个角色，并通过观察他们背部的装置来查看他们的生命值。\n\n# 游戏界面\n\n游戏 UI 通常有两种不同的类型:**菜单**和**hud**。\n\n菜单是用户界面面板，允许玩家通过按下输入设备上的按钮或按键与其交互。\n\n这可以通过许多不同的菜单来实现，包括以下菜单:\n\n*   主菜单，玩家可以选择是否继续游戏、创建新游戏、退出游戏等等\n*   等级选择菜单，玩家可以选择玩哪个等级\n*   和许多其他选择\n\nhud 是在游戏过程中出现的 UI 面板，它给玩家提供了他们应该一直知道的信息，比如他们还剩下多少生命，他们可以使用哪些特殊能力等等。\n\n在本章中，我们将介绍游戏界面，并为我们的游戏制作菜单和平视显示器。\n\n注意\n\n我们不会在这里讨论 diegetic UI，因为它超出了本书的范围。\n\n那么我们如何着手在 UE4 中创建一个游戏 UI 呢？主要方法是使用**虚幻运动图形** ( **UMG** )，这是一个工具，允许你制作一个以菜单和平视显示器为特色的游戏用户界面(在 UE4 术语中也称为小部件)，并将其添加到屏幕上。\n\n让我们在下一节跳到这个话题。\n\n# 绕过基础知识\n\n在 UE4 中，创建游戏 UI 的主要方式是使用 UMG 工具。这个工具可以让你制作一个**小部件**形式的游戏界面，可以使用 UMG 创建。它将允许你通过 UMG 的`Designer`标签，以可视化的方式轻松编辑你的游戏界面，同时也允许你通过 UMG 的`Graph`标签，为你的游戏界面添加功能。\n\n小部件是 UE4 允许你代表游戏用户界面的方式。Widgets 可以是基本的 UI 元素，例如`Buttons`、`Text`元素和`Images`，但是它们也可以组合起来创建更复杂和完整的 Widgets，例如菜单和 hud，这正是我们在本章中将要做的。\n\n让我们在下一个练习中使用 UMG 工具在 UE4 中创建我们的第一个小部件。\n\n## 练习 8.01:创建小部件蓝图\n\n在本练习中，我们将创建我们的第一个小部件蓝图，并学习 UMG 的基本元素以及如何使用它们来创建游戏用户界面。\n\n以下步骤将帮助您完成本练习:\n\n1.  为了创建我们的第一个 Widget，打开编辑器，进入`Content Browser`里面的`ThirdPersonCPP -> Blueprints`文件夹，*右键点击*。\n2.  Go to the very last section, `User Interface`, and select `Widget Blueprint`.\n\n    选择此选项将创建一个新的`Widget Blueprint`，这是 UE4 中一个小部件资产的名称。\n\n3.  Name this Widget `TestWidget` and open it. You will see the interface for editing a Widget Blueprint, where you'll be creating your own Widgets and UI. Here's a breakdown of all the tabs present in this window:\n\n    ![Figure 8.1: The Widget Blueprint editor broken down into six windows ](img/B16183_08_01.jpg)\n\n    图 8.1:小部件蓝图编辑器分为六个窗口\n\n    上图中选项卡的详细信息如下:\n\n    *   `Palette`–该选项卡显示了您可以添加到小部件中的所有单个用户界面元素。这包括`Buttons`、`Text Boxes`、`Images`、`Sliders`、`Check Boxes`等等。\n    *   `Hierarchy`–该选项卡显示了当前小部件中存在的所有用户界面元素。如您所见，目前我们的层次结构中只有一个`Canvas Panel`元素。\n    *   `Designer`–该选项卡根据层次结构中存在的元素及其布局方式，向您展示小部件的视觉外观。因为我们当前在小部件中仅有的元素没有可视化表示，所以这个选项卡当前是空的。\n    *   `Details`–该选项卡显示您当前选择的用户界面元素的属性。如果选择现有的`Canvas Panel`元素，前面截图中的所有选项都会出现。\n    *   因为这个资产是一个`Widget Blueprint`，所以这两个按钮可以让你在`Designer view`和`Graph view`之间切换，前者是截图中呈现的那个，后者看起来完全像一个普通的蓝图类的窗口。\n    *   `Animation`–这两个选项卡都与 Widget 动画相关。小部件蓝图允许您随着时间的推移对用户界面元素的属性进行动画化，包括它们的`position`、`scale`、`color`等。左边的选项卡允许您在右边的选项卡中创建和选择要编辑的动画，在这里您可以编辑它们随着时间的推移会影响哪些属性。\n4.  Let's now look at some of the available UI elements in our `Widget`, starting with the existing `Canvas Panel`.\n\n    `Canvas Panels`通常被添加到小部件蓝图的根目录，因为它们允许您将用户界面元素拖动到`Designer`选项卡中的任何位置。这样，您可以按照自己的意愿布局这些元素:在屏幕中心、左上角、屏幕底部中心，等等。现在让我们将另一个非常重要的用户界面元素拖到我们的小部件中:一个`Button`。\n\n5.  In the `Palette` tab, find the `Button` element and drag it into our `Designer` tab (hold the left mouse button while you drag):\n\n    ![Figure 8.2: A Button element being dragged from the Palette window  into the Designer window ](img/B16183_08_02.jpg)\n\n    图 8.2:一个按钮元素被从调色板窗口拖到设计器窗口\n\n    完成此操作后，您可以通过拖动按钮周围的小白点来将按钮调整到您想要的大小(请记住，您只能对画布面板内的元素执行此操作):\n\n    ![Figure 8.3: The result of resizing a UI element using the white dots around it ](img/B16183_08_03.jpg)\n\n    图 8.3:使用白点调整用户界面元素大小的结果\n\n    另一种在`Widget`中互相拖动元素的方法是将它们拖动到`Hierarchy`标签中，而不是`Designer`标签中。\n\n6.  Now drag a `Text` element inside our `Button`, but this time, use the `Hierarchy` tab:\n\n    ![Figure 8.4: Dragging a Text element from the Palette window into the Hierarchy window ](img/B16183_08_04.jpg)\n\n    图 8.4:将文本元素从调色板窗口拖到层次窗口\n\n    `Text`元素可以包含您指定的文本，具有您可以在`Details`面板中修改的特定大小和字体。使用`Hierarchy`选项卡将`Text`元素拖到`Button`中后，`Designer`选项卡应该是这样的:\n\n    ![Figure 8.5: The Button element in the Designer tab, after we add a Text element as its child ](img/B16183_08_05.jpg)\n\n    图 8.5:在我们添加一个文本元素作为它的子元素之后，设计器选项卡中的按钮元素\n\n    让我们改变这个`Text`块的几个属性。\n\n7.  Select it either in the `Hierarchy` tab or the `Designer` tab and take a look at the `Details` panel:\n\n    ![Figure 8.6: The Details panel, showing the properties of the Text element we added ](img/B16183_08_06.jpg)\n\n    图 8.6:细节面板，显示了我们添加的文本元素的属性\n\n    在这里你可以找到几个你喜欢的属性。现在，我们只想关注其中两个:文本的`Content`和它的`Color and Opacity`。\n\n8.  Update the `Content` of the `Text` element from `Text Block` to `Button 1`:\n\n    ![Figure 8.7: Changing the Text property of the Text element to Button 1 ](img/B16183_08_07.jpg)\n\n    图 8.7:将文本元素的文本属性更改为按钮 1\n\n    接下来，我们把它的`Color and Opacity`从`White`改成`Black`。\n\n9.  点击`Color and Opacity`属性，查看弹出的窗口`Color Picker`。每当您在 UE4 中编辑`Color`属性时，此窗口都会弹出。它允许您以多种不同的方式输入颜色，包括色轮、`Saturation`和`Value`条、`RGB`和`HSV`值滑块，以及几个更多的选项。\n10.  For now, change the color from white to black by dragging the `Value` bar (the one that goes from white to black from top to bottom) all the way to the bottom and then pressing `OK`:\n\n    ![Figure 8.8: Selecting the color black in the Color Picker window ](img/B16183_08_08.jpg)\n\n    图 8.8:在颜色选择器窗口中选择黑色\n\n11.  After these changes, this is what the button should look like:\n\n    ![Figure 8.9: The Button element after we change the Text element's Text  property and its color ](img/B16183_08_09.jpg)\n\n图 8.9:在我们更改文本元素的文本属性及其颜色后的按钮元素\n\n至此，我们结束了本章的第一个练习。现在，您已经了解了 UMG 的一些基本知识，例如如何将`Button`和`Text`元素添加到您的小部件中。\n\n在我们进入下一个练习之前，让我们先了解一下主播。\n\n# 锚\n\n正如你可能知道的，视频游戏是在许多不同的屏幕尺寸和不同的分辨率下进行的。因此，确保您创建的菜单能够有效地适应所有这些不同的分辨率非常重要。这是**主播**的主要目的。\n\n锚点允许您通过指定希望用户界面元素占据的屏幕比例，来指定用户界面元素的大小如何随着屏幕分辨率的变化而变化。使用锚点，您可以让用户界面元素始终位于屏幕的左上角，或者始终占据屏幕的一半，而不管该屏幕的大小和分辨率如何。\n\n随着屏幕大小或分辨率的变化，您的小部件将相对于其锚点进行缩放和移动。只有`Canvas Panel`的直接子元素可以有一个锚点，当您选择所述元素时，您可以通过`Designer`选项卡中的白色花状形状`Anchor Medallion`来可视化该锚点:\n\n![Figure 8.10: The Anchor Medallion at the top left of the outline shown  in the Designer window ](img/B16183_08_10.jpg)\n\n图 8.10:设计器窗口中显示的轮廓左上角的锚牌\n\n默认情况下，锚点会折叠到左上角，这意味着您无法控制按钮随着分辨率的变化而缩放的方式，所以让我们在下一个练习中更改它。\n\n## 练习 8.02:编辑 UMG 主播\n\n在本练习中，我们将更改小部件中的锚点，以使按钮的大小和形状适应各种屏幕分辨率和大小。\n\n以下步骤将帮助您完成本练习:\n\n1.  Select the Button we created in the previous exercise, then head to the `Details` panel and press the very first property you see, the `Anchors` property. Here you'll be able to see the `Anchor` presets, which will align the UI element according to the pivots shown.\n\n    我们希望我们的按钮位于屏幕中央。\n\n2.  Click on the pivot that's at the center of the screen:\n\n    ![Figure 8.11: The Button's Anchors property, with the center Anchor outlined in a box ](img/B16183_08_11.jpg)\n\n    图 8.11:按钮的锚点属性，中心锚点在一个框中\n\n    你会看到我们的`Anchor Medallion`现在已经改变了位置:\n\n    ![Figure 8.12: The Anchor Medallion after we change the Button's Anchor to the center ](img/B16183_08_12.jpg)\n\n    图 8.12:我们将按钮的锚改为中心后的锚牌\n\n    现在`Anchor Medallion`位于屏幕中心，我们仍然无法控制按钮在不同分辨率下的缩放方式，但至少我们知道它会相对于屏幕中心进行缩放。\n\n    为了让我们的按钮在屏幕上居中，我们还必须将按钮的位置更改为在屏幕的中心。\n\n3.  Repeat the previous step of picking the center Anchor, but this time, before you select it, hold the *Ctrl* key in order to snap the Button's position to this Anchor. After you click it, release the *Ctrl* key. This should be the result:\n\n    ![Figure 8.13: The Button element being moved near its selected Anchor in the center ](img/B16183_08_13.jpg)\n\n    图 8.13:按钮元素被移动到中心选定的锚点附近\n\n    从前面的截图中可以看到，我们的按钮已经改变了位置，但是它还没有在屏幕上正确居中。这是因为它的`Alignment`。\n\n    `Alignment`属性是类型`Vector2D`(一个具有两个`float`属性:`X`和`Y`的元组)，并指示相对于其总大小的用户界面元素的中心。默认情况下，它被设置为`(0,0)`，这意味着元素的中心是它的左上角，这解释了前面截图中的结果。可以一直走到`(1,1)`，右下角。在这种情况下，假设我们希望对齐使按钮居中，我们希望它是`(0.5, 0.5)`。\n\n4.  In order to update a UI element's alignment when picking an `Anchor` point, you have to hold the *Shift* key and repeat the previous step. Alternately, to update both the position and the alignment of the button, picking the center `Anchor` point while holding both the *Ctrl* and *Shift* keys will do the job. This should then be the result:\n\n    ![Figure 8.14: The Button element being centered relative to its selected  Anchor in the center ](img/B16183_08_14.jpg)\n\n    图 8.14:按钮元素相对于其在中心选择的锚点居中\n\n    此时，当改变屏幕的分辨率时，我们知道这个按钮将始终保持在屏幕的中心。然而，为了保持按钮相对于分辨率的大小，我们需要做一些更多的修改。\n\n5.  Drag the bottom-right *petal* of the `Anchor Medallion` all the way to the bottom-right corner of the button:\n\n    ![Figure 8.15: Dragging the lower-right petal of the Anchor Medallion  to update the Button element's Anchor ](img/B16183_08_15.jpg)\n\n    图 8.15:拖动锚点徽章的右下角花瓣来更新按钮元素的锚点\n\n6.  Drag the top-left *petal* of the `Anchor Medallion` all the way to the top-left corner of the button:\n\n    ![Figure 8.16: Dragging the upper-left petal of the Anchor Medallion  to update the Button element's Anchor ](img/B16183_08_16.jpg)\n\n    图 8.16:拖动锚点徽章的左上角花瓣来更新按钮元素的锚点\n\n    注意\n\n    当改变`Anchor`时，你在按钮周围看到的百分比是元素在屏幕上所占的空间百分比。例如，看最后一张截图，我们可以看到按钮占据了 *X* 坐标上微件空间的`11.9%`和 *Y* 坐标上微件空间的`8.4%`。\n\n    移动锚点徽章*花瓣*时，按住 *Ctrl* 键，可以将用户界面元素的大小设置为锚点的大小。\n\n    现在，我们的按钮将最终适应不同的屏幕尺寸和分辨率，因为这些改变了它的锚。\n\n    您也可以使用`Details`面板，通过使用`Anchor Medallion`并移动按钮来手动编辑我们刚刚编辑的所有属性:\n\n    ![Figure 8.17: The properties we changed using the Anchor Medallion, shown  in the Details window ](img/B16183_08_17.jpg)\n\n    图 8.17:我们使用锚牌更改的属性，显示在详细信息窗口中\n\n    最后，我们需要知道如何在`Designer`选项卡中可视化不同分辨率的小部件。\n\n7.  Drag the double arrow at the bottom right of the outlined box inside the `Designer` tab:\n\n    ![Figure 8.18: The double arrow at the bottom right of the outlined  box inside the Designer tab ](img/B16183_08_18.jpg)\n\n图 8.18:设计器选项卡内轮廓框右下角的双箭头\n\n通过拖动双箭头，您可以将`Canvas`调整到您想要的任何屏幕分辨率。在下面的截图中，您将看到各种设备最常用的分辨率，并且您可以在每种设备中预览您的小部件:\n\n![Figure 8.19: The resolutions we can choose to preview in the Designer window ](img/B16183_08_19.jpg)\n\n图 8.19:我们可以选择在设计器窗口中预览的分辨率\n\n注意\n\n你可以在[上找到 UMG 主播的完整参考。](https://docs.unrealengine.com/en-US/Engine/UMG/UserGuide/Anchors)\n\n我们的练习到此结束。您已经了解了锚和调整小部件以适应不同的屏幕大小和分辨率。\n\n现在我们已经了解了 UMG 的一些基本知识，让我们看看如何为这个小部件蓝图创建一个小部件 C++ 类，这是我们在下一个练习中要做的。\n\n## 练习 8.03:创建 RestartWidget C++ 类\n\n在本练习中，我们将学习如何创建一个 Widget C++ 类，我们创建的 Widget 蓝图将从该类继承。当玩家在我们的`Dodgeball`游戏中死亡时，它会被添加到屏幕上，这样玩家就可以选择重启关卡。这个小部件将有一个按钮，当玩家点击它时，它将重启关卡。\n\n本练习的第一步是将 UMG 相关模块添加到我们的项目中。虚幻引擎由几个不同的模块组成，在每个项目中，你必须指定你将要使用的模块。我们的项目在生成源代码文件时附带了一些通用模块，但是我们还需要添加一些。\n\n以下步骤将帮助您完成本练习:\n\n1.  打开`Dodgeball.build.cs`文件，这是一个 C#文件，而不是 C++ 文件，位于项目的`Source`文件夹中。\n2.  Open the file, and you'll find the `AddRange` function from the `PublicDependencyModuleNames` property being called. This is the function that tells the engine which modules this project intends to use. As a parameter, an array of strings is sent, with the names of all the intended modules for the project. Given that we intend on using UMG, we'll need to add the UMG-related modules: `UMG`, `Slate`, and `SlateCore`:\n\n    ```cpp\n    PublicDependencyModuleNames.AddRange(new string[] { \"Core\",   \"CoreUObject\", \"Engine\", \"InputCore\", \"HeadMountedDisplay\",   \"UMG\", \"Slate\", \"SlateCore\" });\n    ```\n\n    现在我们已经通知引擎我们将使用 UMG 模块，让我们创建我们的 Widget C++ 类:\n\n3.  打开虚幻编辑器。\n4.  右键单击内容浏览器并选择`New C++ Class`。\n5.  将`Show All Classes`复选框设置为`true`。\n6.  搜索`UserWidget`类，并选择该类作为新类的父类。\n7.  Name the new C++ class `RestartWidget`.\n\n    在 Visual Studio 中打开文件后，按照以下步骤开始修改我们的 Widget C++ 类:\n\n8.  The first thing we'll add to this class is a `public` `class UButton*` property called `RestartButton`, which represents the Button the player will press in order to restart the level. You will want it to be bound to a Button in the Blueprint class that inherits from this class, by using the `UPROPERTY` macro with the `BindWidget` meta tag. This will force that Widget Blueprint to have a `Button` called `RestartButton` that we can access in C++ through this property and then freely edit its properties, such as the size and position, in the Blueprint:\n\n    ```cpp\n    UPROPERTY(meta = (BindWidget))\n    class UButton* RestartButton;\n    ```\n\n    注意\n\n    如果从这个 C++ 类继承的 Widget 蓝图没有相同类型和名称的元素，使用`BindWidget`元标记将导致编译错误。如果您不想发生这种情况，您必须将`UPROPERTY`标记为可选的`BindWidget`像这样:`UPROPERTY(meta = (BindWidget, OptionalWidget = true))`这将使绑定该属性成为可选的，并且在编译小部件蓝图时不会导致编译错误。\n\n    接下来，我们要添加玩家点击`RestartButton`时会调用的功能，这将重启关卡。我们将使用`GameplayStatics`对象的`OpenLevel`功能，然后发送当前级别的名称。\n\n9.  在 Widget 类的头文件中，添加一个名为`OnRestartClicked`的`protected`函数的声明，该函数不返回任何内容，也不接收任何参数。该功能必须标记为`UFUNCTION` :\n\n    ```cpp\n    protected:\n    UFUNCTION()\n    void OnRestartClicked();\n    ```\n\n10.  在类的源文件中，为`GameplayStatics`对象添加一个`include`:\n\n    ```cpp\n    #include \"Kismet/GameplayStatics.h\"\n    ```\n\n11.  然后，为我们的`OnRestartClicked`函数添加一个实现:\n\n    ```cpp\n    void URestartWidget::OnRestartClicked()\n    {\n    }\n    ```\n\n12.  Inside its implementation, call the `GameplayStatics` object's `OpenLevel` function. This function receives as parameters a world context object, which will be the `this` pointer, and the name of the level, which we'll have to fetch using the `GameplayStatics` object's `GetCurrentLevelName` function. This last function must also receive a world context object, which will also be the `this` pointer:\n\n    ```cpp\n    UGameplayStatics::OpenLevel(this,   FName(*UGameplayStatics::GetCurrentLevelName(this)));\n    ```\n\n    注意\n\n    对`GameplayStatics`对象的`GetCurrentLevelName`函数的调用必须以`*`开头，因为它返回一个`FString`，UE4 的字符串类型，并且必须取消引用才能传递给`FName`构造函数。\n\n    下一步将绑定该功能，当玩家按下`RestartButton`时调用该功能:\n\n13.  为了做到这一点，我们必须覆盖一个属于`UserWidget`类的函数，叫做`NativeOnInitialized`。这个函数只被调用一次，类似于 Actor 的`BeginPlay`函数，这使得它适合做我们的设置。在小部件类的头文件\n\n    ```cpp\n    virtual void NativeOnInitialized() override;\n    ```\n\n    中添加带有`virtual`和`override`关键字的`public` `NativeOnInitialized`函数的声明\n14.  接下来，在类的源文件中，添加这个函数的实现。在里面，调用它的`Super`函数，添加一个`if`语句，检查我们的`RestartButton`和`nullptr`是否不同:\n\n    ```cpp\n    void URestartWidget::NativeOnInitialized()\n    {\n      Super::NativeOnInitialized();\n      if (RestartButton != nullptr)\n      {\n      }\n    }\n    ```\n\n15.  如果`if`语句为真，我们希望将我们的`OnRestartClicked`功能绑定到按钮的`OnClicked`事件。我们可以通过访问按钮的`OnClicked`属性并调用其`AddDynamic`函数来实现，将我们想要调用该函数的对象(即`this`指针)和要调用的函数(即`OnRestartClicked`函数:\n\n    ```cpp\n    if (RestartButton != nullptr)\n    {\n      RestartButton->OnClicked.AddDynamic(this,   &URestartWidget::OnRestartClicked);\n    }\n    ```\n\n    的指针)作为参数发送出去\n16.  Because we're accessing functions related to the `Button` class, we'll also have to include it:\n\n    ```cpp\n    #include \"Components/Button.h\"\n    ```\n\n    注意\n\n    当玩家用鼠标按下并释放按钮时，会调用按钮的`OnClicked`事件。还有其他与按钮相关的事件，包括`OnPressed`事件(当玩家按下按钮时)`OnReleased`事件(当玩家释放按钮时)`OnHover`和`OnUnhover`事件(当玩家分别开始和停止将鼠标悬停在该按钮上时)。\n\n    `AddDynamic`函数必须接收一个指向标有`UFUNCTION`宏的函数的指针作为参数。如果没有，您将在调用该函数时得到一个错误。这就是为什么我们用`UFUNCTION`宏标记了`OnRestartClicked`功能。\n\n    完成这些步骤后，编译您的更改并打开编辑器。\n\n17.  打开您之前创建的`TestWidget`小部件蓝图。我们希望将这个小部件蓝图与我们刚刚创建的`RestartWidget`类相关联，因此我们需要对它进行修复。\n18.  From the Widget Blueprint's `File` tab, select the `Reparent Blueprint` option and choose the `RestartWidget` C++ class as its new parent class:\n\n    ![Figure 8.20: Reparenting the TestWidget's class to RestartWidget ](img/B16183_08_20.jpg)\n\n图 8.20:将测试小部件的类重新解析为重启小部件\n\n您会注意到小部件蓝图现在有一个与我们在 C++ 类中创建的`BindWidget`元标记相关的编译错误:\n\n![Figure 8.21: Compiler errors after setting the parent class to the RestartWidget class ](img/B16183_08_21.jpg)\n\n图 8.21:将父类设置为 RestartWidget 类后的编译器错误\n\n这是因为 C++ 类找不到任何名为`RestartButton`的`Button`属性。\n\n为了解决这个问题，我们需要将小部件蓝图中的`Button`元素重命名为`RestartButton`:\n\n![Figure 8.22: Renaming the Button element to RestartButton ](img/B16183_08_22.jpg)\n\n图 8.22:将 Button 元素重命名为 RestartButton\n\n完成此操作后，关闭小部件蓝图，将其名称从`TestWidget`更改为`BP_RestartWidget`，与上一步相同。\n\n我们的 Widget 类的创建到此结束。您现在知道如何将一个小部件 C++ 类连接到一个小部件蓝图，这是在 UE4 中处理游戏用户界面的一个非常重要的步骤。\n\n接下来我们需要做的是创建我们的`Player Controller` C++ 类，它将负责实例化我们的`RestartWidget`并将其添加到屏幕上。我们将在下面的练习中这样做。\n\n## 练习 8.04:创建向屏幕添加 RestartWidget 的逻辑\n\n在本练习中，我们将创建负责将新创建的`RestartWidget`添加到屏幕的逻辑。当玩家死亡时，它会出现在屏幕上，这样他们就可以选择重启关卡。\n\n为了做到这一点，我们必须创建一个新的`Player Controller` C++ 类，您可以通过以下步骤来完成:\n\n1.  打开虚幻编辑器。\n2.  *右键单击`Content Browser`上的*，选择`New C++ Class`。\n3.  搜索`Player Controller`类，并选择该类作为新类的父类。\n4.  命名新的 C++ 类`DodgeballPlayerController`。\n5.  Open the class's files in Visual Studio.\n\n    当我们的玩家耗尽生命值时，`DodgeballCharacter`类将访问这个`Player Controller`类，并调用一个将`RestartWidget`添加到屏幕上的函数。遵循以下步骤来实现这一点。\n\n    为了知道要添加到屏幕上的小部件的类(将是小部件蓝图，而不是小部件 C++ 类)，我们需要使用`TSubclassOf`类型。\n\n6.  In the class's header file, add a `public` `TSubclassOf<class URestartWidget>` property called `BP_RestartWidget`. Be sure to make it a `UPROPERTY` with the `EditDefaultsOnly` tag so that we can edit it in the Blueprint class:\n\n    ```cpp\n    public:\n    UPROPERTY(EditDefaultsOnly)\n    TSubclassOf<class URestartWidget> BP_RestartWidget;\n    ```\n\n    为了实例化这个小部件并将其添加到屏幕上，我们需要保存对它的引用。\n\n7.  Add a new `private` variable of type `class URestartWidget*` and call it `RestartWidget`. Be sure to make it a `UPROPERTY` function with no tags:\n\n    ```cpp\n    private:\n    UPROPERTY()\n    class URestartWidget* RestartWidget;\n    ```\n\n    注意\n\n    虽然这个属性在蓝图类中不应该是可编辑的，但是我们必须使这个引用成为`UPROPERTY`，否则垃圾收集器将会破坏这个变量的内容。\n\n    接下来我们需要的是一个负责将我们的 Widget 添加到屏幕上的功能。\n\n8.  添加一个名为`ShowRestartWidget` :\n\n    ```cpp\n    void ShowRestartWidget();\n    ```\n\n    的函数声明，该函数不返回任何内容，也不接收任何参数\n9.  现在，转到我们班的源文件。首先，在`RestartWidget`类中添加一个 include:\n\n    ```cpp\n    #include \"RestartWidget.h\"\n    ```\n\n10.  然后，添加我们的`ShowRestartWidget`函数的实现，我们将从检查我们的`BP_RestartWidget`变量是否不是`nullptr` :\n\n    ```cpp\n    void ADodgeballPlayerController::ShowRestartWidget()\n    {\n      if (BP_RestartWidget != nullptr)\n      {\n      }\n    }\n    ```\n\n    开始\n11.  If that variable is valid (different than `nullptr`), we want to pause the game using the `SetPause` function of `Player Controller`. This will make sure that the game stops until the player decides to do something (which in our case will be pressing the button that restarts the level):\n\n    ```cpp\n    SetPause(true);\n    ```\n\n    接下来我们要做的是改变输入模式。在 UE4 中，有三种输入模式:`Game Only`、`Game and UI`、`UI Only`。如果您的`Input`模式包括`Game`，这意味着玩家角色和玩家控制器将通过`Input Actions`接收输入。如果您的`Input`模式包括`UI`，这意味着屏幕上的小部件将接收来自玩家的输入。当我们在屏幕上显示这个小部件时，我们不希望玩家角色收到任何输入。\n\n12.  Hence, update to the `UI Only` `Input` Mode. You can do this by calling the `Player Controller` `SetInputMode` function and passing the `FInputModeUIOnly` type as a parameter:\n\n    ```cpp\n    SetInputMode(FInputModeUIOnly());\n    ```\n\n    在这之后，我们要显示鼠标光标，这样玩家就可以看到他们将鼠标悬停在哪个按钮上。\n\n13.  我们将通过将`Player Controller`的`bShowMouseCursor`属性设置为`true` :\n\n    ```cpp\n    bShowMouseCursor = true;\n    ```\n\n    来实现\n14.  现在，我们实际上可以使用`Player Controller`的`CreateWidget`函数实例化我们的 Widget，将 C++ Widget 类作为模板参数传递，在我们的例子中是`RestartWidget`，然后将`Owning Player`作为正常参数传递，T3 是拥有这个 Widget 的`Player Controller`，我们将使用`this`指针发送，Widget 类将是我们的`BP_RestartWidget`属性:\n\n    ```cpp\n    RestartWidget = CreateWidget<URestartWidget>(this,   BP_RestartWidget);\n    ```\n\n15.  在我们实例化小部件后，我们将希望使用小部件的`AddToViewport`功能将其添加到屏幕上:\n\n    ```cpp\n    RestartWidget->AddToViewport();\n    ```\n\n16.  我们的`ShowRestartWidget`功能到此结束。但是，我们还需要创建将`RestartWidget`从屏幕上移除的功能。在类的头文件中，为一个函数添加一个声明，就像`ShowRestartWidget`函数一样，但是这次调用了`HideRestartWidget` :\n\n    ```cpp\n    void HideRestartWidget();\n    ```\n\n17.  在类的源文件中，添加`HideRestartWidget`函数的实现:\n\n    ```cpp\n    void ADodgeballPlayerController::HideRestartWidget()\n    {\n    }\n    ```\n\n18.  在这个函数中，我们应该做的第一件事是通过调用其`RemoveFromParent`函数从屏幕中移除 Widget，并使用`Destruct`函数\n\n    ```cpp\n    RestartWidget->RemoveFromParent();\n    RestartWidget->Destruct();\n    ```\n\n    将其销毁\n19.  然后，我们想使用我们在前面的函数中使用的`SetPause`函数来解包游戏:\n\n    ```cpp\n    SetPause(false);\n    ```\n\n20.  And finally, set the `Input` Mode to `Game Only` and hide the mouse cursor the same way we did in the previous function (this time we pass the `FInputModeGameOnly` type instead):\n\n    ```cpp\n    SetInputMode(FInputModeGameOnly());\n    bShowMouseCursor = false;\n    ```\n\n    我们的`Player Controller` C++ 类的逻辑到此结束。接下来我们应该做的是调用将我们的 Widget 添加到屏幕上的函数。\n\n21.  转到`DodgeballCharacter`类的源文件，将`include`关键字添加到我们新创建的`DodgeballPlayerController` :\n\n    ```cpp\n    #include \"DodgeballPlayerController.h\"\n    ```\n\n    中\n22.  在`DodgeballCharacter`类对`OnDeath_Implementation`函数的实现中，用以下内容替换对`QuitGame`函数的调用:\n    *   使用`GetController`功能获取角色的玩家控制器。您需要将结果保存在名为`PlayerController`的类型为`DodgeballPlayerController*`的变量中。因为该函数将返回一个类型为`Controller`的变量，所以您还需要将其转换为我们的`PlayerController`类:\n\n        ```cpp\n        ADodgeballPlayerController* PlayerController = Cast<ADodgeballPlayerController>(GetController());\n        ```\n\n    *   Check whether the `PlayerController` variable is valid. If it is, call its `ShowRestartWidget` function:\n\n        ```cpp\n        if (PlayerController != nullptr)\n        {\n          PlayerController->ShowRestartWidget();\n        }\n        ```\n\n        在这些修改之后，我们剩下要做的最后一件事就是调用将我们的 Widget 隐藏在屏幕之外的函数。打开`RestartWidget`类的源文件，实现以下修改。\n\n23.  在`DodgeballPlayerController`上添加`include`，它包含我们将要调用的函数:\n\n    ```cpp\n    #include \"DodgeballPlayerController.h\"\n    ```\n\n24.  在`OnRestartClicked`函数实现内部，在调用`OpenLevel`函数之前，我们必须使用`GetOwningPlayer`函数获取 Widget 的`OwningPlayer`，类型为`PlayerController`，并将其转换为`DodgeballPlayerController`类:\n\n    ```cpp\n    ADodgeballPlayerController* PlayerController =   Cast<ADodgeballPlayerController>(GetOwningPlayer());\n    ```\n\n25.  然后，如果`PlayerController`变量有效，我们调用它的`HideRestartWidget`函数:\n\n    ```cpp\n    if (PlayerController != nullptr)\n    {\n      PlayerController->HideRestartWidget();\n    }\n    ```\n\n完成所有这些步骤后，关闭编辑器，编译您的更改并打开编辑器。\n\n你已经完成了这个练习。我们已经添加了所有必要的逻辑来将我们的`RestartWidget`添加到屏幕上，剩下唯一要做的就是创建我们新创建的`DodgeballPlayerController`的蓝图类，我们将在下一个练习中进行。\n\n## 练习 8.05:设置躲避球游戏控制器蓝图类\n\n在本练习中，我们将创建我们的`DodgeballPlayerController`的蓝图类，以便指定我们想要添加到屏幕上的小部件，并告诉 UE4 在游戏开始时使用这个蓝图类。\n\n为此，请遵循以下步骤:\n\n1.  进入内容浏览器中的`ThirdPersonCPP` - > `Blueprints`目录，右键点击，新建一个蓝图类。\n2.  搜索`DodgeballPlayerController`类并选择它作为父类。\n3.  将此蓝图类重命名为`BP_DodgeballPlayerController`。之后，打开这个蓝图资产。\n4.  Go to its `Class Defaults` tab and set the class's `BP_RestartWidget` property to the `BP_RestartWidget` Widget Blueprint we created.\n\n    现在，我们唯一要做的就是确保这个`Player Controller`蓝图类正在游戏中使用。\n\n    为了做到这一点，我们必须再走几步。\n\n5.  Go to the `ThirdPersonCPP` -> `Blueprints` directory in the `Content Browser`, *right-click* on it and create a new Blueprint class. Search for the `DodgeballGameMode` class and select it as the parent class, then rename this `Blueprint` class to `BP_DodgeballGameMode`.\n\n    这个类负责告诉游戏对于游戏的每个元素使用哪些类，比如使用哪个`Player Controller` 类等等。\n\n6.  Open the asset, go to its `Class Defaults` tab, and set the class's `PlayerControllerClass` property to the `BP_DodgeballPlayerController` class we created:\n\n    ![Figure 8.23: Setting the PlayerControllerClass property to BP_DodgeballPlayerController ](img/B16183_08_23.jpg)\n\n    图 8.23:将 PlayerController 类属性设置为 BP _ DodgeballPlayerController\n\n7.  Close the asset and select the `Blueprints` drop-down option inside the editor toolbar that is at the top of the `Level Viewport` window. From there, select `Game Mode` (which should currently be set to `DodgeballGameMode`) `-> Select GameModeBase Class -> BP_DodgeballGameMode`. This will tell the editor to use this new `Game Mode` in all levels.\n\n    现在，玩游戏，让你的角色被躲避球击中`3`次。第三次之后，你应该会看到游戏暂停并显示`BP_RestartWidget`:\n\n    ![Figure 8.24: Our BP_RestartWidget being added to the screen after the player  runs out of health points ](img/B16183_08_24.jpg)\n\n图 8.24:我们的 BP_RestartWidget 在玩家耗尽生命值后被添加到屏幕上\n\n当你用鼠标点击`Button 1`时，你应该会看到电平重置为初始状态:\n\n![Figure 8.25: The level restarts after the player presses the button  shown in the previous screenshot ](img/B16183_08_25.jpg)\n\n图 8.25:玩家按下上一张截图所示的按钮后，关卡重启\n\n我们的练习到此结束。现在您知道如何创建小部件并在游戏中展示它们了。这是你成为一名熟练游戏开发者的旅程中的另一个关键步骤。\n\n在我们继续下一个练习之前，让我们看看下一部分中的进度条。\n\n# 进度条\n\n电子游戏可以表示角色状态(如健康、耐力等)的方式之一是通过**进度条**，我们将使用进度条向玩家传达他们的角色有多健康。进度条本质上是一种形状，通常是矩形，可以填充和清空，以便向玩家显示特定属性的进度。如果你想让玩家看到他们角色的生命值只有最大值的一半，你可以通过显示进度条为半满来实现。这正是我们在这一部分要做的。这个进度条将是我们躲避球游戏的平视显示器中唯一的元素。\n\n为了创建这个`Health Bar`，我们首先需要创建我们的抬头显示器小部件。打开编辑器，进入内容浏览器里面的`ThirdPersonCPP` - > `Blueprints`目录，右键新建`User Interface`类别的`Widget Blueprint`类。命名这个新的小部件蓝图`BP_HUDWidget`。之后，打开这个新的小部件蓝图。\n\nUE4 中的进度条只是另一个 UI 元素，就像`Buttons`和`Text`元素一样，这意味着我们可以将其从`Palette`选项卡拖到我们的`Designer`选项卡中。请看下面的例子:\n\n![Figure 8.26: Dragging a Progress Bar element into the Designer window ](img/B16183_08_26.jpg)\n\n图 8.26:将进度条元素拖到设计器窗口中\n\n起初，这个进度条可能看起来像一个按钮；但是，它包含两个对进度条很重要的特定属性:\n\n*   `Percent`-允许您指定进度条的进度，从`0`到`1`\n*   `Bar Fill Type`–允许您指定进度栏的填充方式(从左到右、从上到下等):\n\n![Figure 8.27: The Progress Bar's Percent and Bar Fill Type properties ](img/B16183_08_27.jpg)\n\n图 8.27:进度条的百分比和条填充类型属性\n\n如果您将`Percent`属性设置为`0.5`，您应该会看到进度条相应地更新，以填充其长度的一半:\n\n![Figure 8.28: The Progress Bar filled halfway to the right ](img/B16183_08_28.jpg)\n\n图 8.28:进度条填充到右边一半\n\n继续之前，将`Percent`属性设置为`1`。\n\n现在让我们将进度条的颜色从蓝色(默认颜色)更改为红色。为此，请转到`Details`选项卡，在`Appearance`类别中，将`Fill Color and Opacity`属性设置为红色(`RGB(1,0,0)`):\n\n![Figure 8.29: The Progress Bar's Color being changed to red ](img/B16183_08_29.jpg)\n\n图 8.29:进度条的颜色变为红色\n\n完成此操作后，进度条现在应该使用红色作为填充颜色。\n\n为了结束进度条的设置，让我们更新它的位置、大小和锚点。按照以下步骤实现:\n\n1.  在插槽 `(Canvas Panel Slot)`类别中，展开`Anchors`属性，并将其属性设置为以下值:\n    *   `Minimum`:`X`轴上的`0.052`和`Y`轴上的`0.083`\n    *   `Maximum`:`X`轴上的`0.208`和`Y`轴上的`0.116`\n2.  将`Offset Left`、`Offset Top`、`Offset Right`和`Offset Bottom`属性设置为`0`。\n\n您的进度条现在应该如下所示:\n\n![Figure 8.30: The Progress Bar after all the modifications in this section have been completed  ](img/B16183_08_30.jpg)\n\n图 8.30:完成本节所有修改后的进度条\n\n至此，我们可以结束进度条的话题了。我们的下一步将是添加所有必要的逻辑来使用进度条作为健康条，通过更新玩家角色健康旁边的`Percent`属性。我们将在下一个练习中完全这样做。\n\n## 练习 8.06:创建健康栏 C++ 逻辑\n\n在本练习中，我们将添加所有必要的 C++ 逻辑，以便随着玩家角色健康状况的变化更新平视显示器内的进度条。\n\n为此，请遵循以下步骤:\n\n1.  打开编辑器，创建一个继承自`UserWidget`的新 C++ 类，类似于我们在*练习 8.03* 、*中创建 RestartWidget C++ 类*的方式，但这次称之为`HUDWidget`。这将是 C++ 类，将用于我们的抬头显示器小部件。\n2.  在`HUDWidget`类的头文件中，添加一个名为`HealthBar`的类型为`class UProgressBar*`的新`public`属性。这种类型用于表示进度条，就像我们在前面部分中在 C++ 中创建的进度条一样。请务必使用`BindWidget`标签将该属性声明为`UPROPERTY`函数:\n\n    ```cpp\n    UPROPERTY(meta = (BindWidget))\n    class UProgressBar* HealthBar;\n    ```\n\n3.  为名为`UpdateHealthPercent`的`public`函数添加一个声明，该函数不返回任何内容，并接收一个`float HealthPercent`属性作为参数。将调用该函数来更新进度条的`Percent`属性:\n\n    ```cpp\n    void UpdateHealthPercent(float HealthPercent);\n    ```\n\n4.  在`HUDWidget`类的源文件中，添加`UpdateHealthPercent`函数的实现，该函数将调用`HealthBar`属性的`SetPercent`函数，传递`HealthPercent`属性作为参数:\n\n    ```cpp\n    void UHUDWidget::UpdateHealthPercent(float HealthPercent)\n    {\n      HealthBar->SetPercent(HealthPercent);\n    }\n    ```\n\n5.  Because we'll be using the `ProgressBar` C++ class, we'll need to add an `include` to it at the top of the class's source file:\n\n    ```cpp\n    #include \"Components/ProgressBar.h\"\n    ```\n\n    下一步将为我们负责将`HUDWidget`添加到屏幕上的`Player Controller`添加所有必要的逻辑。为此，请执行以下步骤:\n\n6.  Inside the `DodgeballPlayerController` class's header file, add a `public` property of type `TSubclassOf<class UHUDWidget>` called `BP_HUDWidget`. Make sure to mark it as a `UPROPERTY` function with the `EditDefaultsOnly` tag.\n\n    该属性将允许我们在`DodgeballPlayerController`蓝图类中指定我们想要用作平视显示器的小部件:\n\n    ```cpp\n    UPROPERTY(EditDefaultsOnly)\n    TSubclassOf<class UHUDWidget> BP_HUDWidget;\n    ```\n\n7.  添加另一个名为`HUDWidget`的属性，这次是`private`，类型为`class UHUDWidget*`。标记为`UPROPERTY`，但没有任何标签:\n\n    ```cpp\n    UPROPERTY()\n    class UHUDWidget* HUDWidget;\n    ```\n\n8.  为`BeginPlay`功能添加`protected`声明，并将其标记为`virtual`和`override` :\n\n    ```cpp\n    virtual void BeginPlay() override;\n    ```\n\n9.  Add a declaration for a new `public` function, called `UpdateHealthPercent`, which returns nothing and receives a `float HealthPercent` as a parameter.\n\n    这个函数将由我们的玩家角色类调用，以更新我们的平视显示器中的健康栏:\n\n    ```cpp\n    void UpdateHealthPercent(float HealthPercent);\n    ```\n\n10.  现在转到`DodgeballPlayerController`类的源文件。首先给我们的`HUDWidget`类增加一个`include`:\n\n    ```cpp\n    #include \"HUDWidget.h\"\n    ```\n\n11.  然后，添加`BeginPlay`函数的实现，首先调用`Super`对象的`BeginPlay`函数:\n\n    ```cpp\n    void ADodgeballPlayerController::BeginPlay()\n    {\n      Super::BeginPlay();\n    }\n    ```\n\n12.  该函数调用后，检查`BP_HUDWidget`属性是否有效。如果是，用`UHUDWidget`模板参数调用`CreateWidget`函数，并传递`Owning Player`、`this`和小部件类`BP_HUDWidget`作为参数。请务必将`HUDWidget`属性设置为该函数调用的返回值:\n\n    ```cpp\n    if (BP_HUDWidget != nullptr)\n    {\n      HUDWidget = CreateWidget<UHUDWidget>(this, BP_HUDWidget);\n    }\n    ```\n\n13.  设置`HUDWidget`属性后，调用其`AddToViewport`函数:\n\n    ```cpp\n    HUDWidget->AddToViewport();\n    ```\n\n14.  Lastly, add the implementation for the `UpdateHealthPercent` function, where we'll check if the `HUDWidget` property is valid and, if it is, call its `UpdateHealthPercent` function and pass the `HealthPercent` property as a parameter:\n\n    ```cpp\n    void ADodgeballPlayerController::UpdateHealthPercent(float   HealthPercent)\n    {\n      if (HUDWidget != nullptr)\n      {\n        HUDWidget->UpdateHealthPercent(HealthPercent);\n      }\n    }\n    ```\n\n    现在我们已经添加了负责将平视显示器添加到屏幕上并允许其更新的逻辑，我们需要对其他类进行一些修改。按照以下步骤进行操作。\n\n    目前我们在前一章创建的`Health`界面只有`OnDeath`事件，每当一个对象生命值耗尽时就会调用这个事件。为了在玩家每次受到伤害时更新我们的健康栏，我们需要允许我们的`HealthInterface`类在这种情况发生时通知一个对象。\n\n15.  打开`HealthInterface`类的头文件，并添加一个声明，类似于我们在*练习 7.04* 、*创建健康界面类*中为`OnDeath`事件所做的声明，但这次是为`OnTakeDamage`事件。只要有物体受到伤害，就会调用该事件:\n\n    ```cpp\n    UFUNCTION(BlueprintNativeEvent, Category = Health)\n    void OnTakeDamage();\n    virtual void OnTakeDamage_Implementation() = 0;\n    ```\n\n16.  Now that we have added this event to our `Interface` class, let's add the logic that calls that event: open the `HealthComponent` class's source file and, inside its implementation of the `LoseHealth` function, after subtracting the `Amount` property from the `Health` property, check whether the `Owner` implements the `Health` interface and, if it does, call its `OnTakeDamage` event. Do this the same way we already did later in that same function for our `OnDeath` event, but this time simply change the name of the event to `OnTakeDamage`:\n\n    ```cpp\n    if (GetOwner()->Implements<UHealthInterface>())\n    {\n      IHealthInterface::Execute_OnTakeDamage(GetOwner());\n    }\n    ```\n\n    因为我们的生命值栏会要求玩家角色的生命值百分比，所以我们需要执行以下操作:\n\n17.  在我们的`HealthComponent`中添加一个`public`函数，它只返回这样的内容:在`HealthComponent`类的头文件中，为一个返回`float`的`FORCEINLINE`函数添加一个声明。这个函数应该叫`GetHealthPercent`，是一个`const`函数。它的实现将简单地包括返回`Health`属性除以`100`，我们将假设这是一个对象在我们的游戏中可以拥有的最大生命值:\n\n    ```cpp\n    FORCEINLINE float GetHealthPercent() const { return Health /   100.f; }\n    ```\n\n18.  现在转到`DodgeballCharacter`类的头文件，为一个名为`OnTakeDamage_Implementation`的`public` `virtual`函数添加一个声明，该函数不返回任何内容，也不接收任何参数。标记为`virtual`和`override`:T6\n19.  In the `DodgeballCharacter` class's source file, add an implementation for the `OnTakeDamage_Implementation` function we just declared. Copy the content of the `OnDeath_Implementation` function to this new function's implementation, but do this change: instead of calling the `ShowRestartWidget` function of `PlayerController`, call its `UpdateHealthPercent` function, and pass the return value of the `HealthComponent` property's `GetHealthPercent` function as a parameter:\n\n    ```cpp\n    void ADodgeballCharacter::OnTakeDamage_Implementation()\n    {\n      ADodgeballPlayerController* PlayerController =   Cast<ADodgeballPlayerController>(GetController());\n      if (PlayerController != nullptr)\n      {\n        PlayerController->UpdateHealthPercent(HealthComponent-  >GetHealthPercent());\n      }\n    }\n    ```\n\n    本练习的代码设置到此结束。完成这些更改后，编译代码，打开编辑器并执行以下操作:\n\n20.  打开`BP_HUDWidget`小部件蓝图并将其准备到`HUDWidget`类，就像您在*练习 8.03* 、*创建 RestartWidget C++ 类*中所做的一样。\n21.  这会导致编译错误，您可以通过将进度条元素重命名为`HealthBar`来修复这个错误。\n22.  Close this Widget Blueprint, open the `BP_DodgeballPlayerController` Blueprint class and set its `BP_HUDWidget` property to the `BP_HUDWidget` Widget Blueprint:\n\n    ![Figure 8.31: Setting the BP_HUDWidget property to BP_HUDWidget ](img/B16183_08_31.jpg)\n\n图 8.31:将 BP_HUDWidget 属性设置为 BP_HUDWidget\n\n完成这些更改后，播放该级别。你应该注意到屏幕左上角的`Health Bar`:\n\n![Figure 8.32: The Progress Bar shown at the top left of the screen ](img/B16183_08_32.jpg)\n\n图 8.32:屏幕左上角显示的进度条\n\n当玩家角色被躲避球击中时，你应该注意到`Health Bar`被清空了:\n\n![Figure 8.33: The Progress Bar being emptied as the Player Character loses health points ](img/B16183_08_33.jpg)\n\n图 8.33:当玩家角色失去生命值时进度条被清空\n\n至此，我们结束了本练习，在本练习中，您已经学习了在屏幕上添加平视显示器和在游戏中更新平视显示器的所有必要步骤。\n\n## 活动 8.01:改进 RestartWidget\n\n在本活动中，我们将在我们的`RestartWidget`读数中添加一个`Text`元素`Game Over`，以便玩家知道他们刚刚输掉了比赛；增加`Exit`按钮，允许玩家退出游戏；并且更新我们现有按钮的文本到`Restart`以便玩家知道当他们点击那个按钮时会发生什么。\n\n以下步骤将帮助您完成本活动:\n\n1.  打开`BP_RestartWidget`小部件蓝图。\n2.  将新的`Text`元素拖到现有的`Canvas Panel`元素中。\n3.  修改`Text`元素的属性:\n    *   展开`Anchors`属性，将其`Minimum`设置为`X`轴上的`0.291`和`Y`轴上的`0.115`，将其`Maximum`设置为`X`轴上的`0.708`和`Y`轴上的`0.255`。\n    *   将`Offset Left`、`Offset Top`、`Offset Right`和`Offset Bottom`属性设置为`0`。\n    *   将`Text`属性设置为`GAME OVER`。\n    *   将`Color and Opacity`属性设置为红色:`RGBA(1.0, 0.082, 0.082, 1.0)`。\n    *   展开`Font`属性，将其`Size`设置为`100`。\n    *   将`Justification`属性设置为`Align Text Center`。\n4.  选择`RestartButton`属性中的另一个`Text`元素，将其`Text`属性更改为`Restart`。\n5.  复制`RestartButton`属性，并将副本名称更改为`ExitButton`。\n6.  将`ExitButton`属性中`Text`元素的`Text`属性更改为`Exit`。\n7.  展开`ExitButton`属性的`Anchor`属性，将其`Minimum`设置为 *X* 轴上的`0.44`和 *Y* 轴上的`0.615`，将其`Maximum`设置为 *X* 轴上的`0.558`和 *Y* 轴上的`0.692`。\n8.  Set the `ExitButton` properties of `Offset Left`, `Offset Top`, `Offset Right`, and `Offset Bottom` to `0`.\n\n    完成这些更改后，我们需要添加负责处理`ExitButton`属性点击的逻辑，这将退出游戏:\n\n9.  保存对`BP_RestartWidget`小部件蓝图所做的更改，并在 Visual Studio 中打开`RestartWidget`类的头文件。在这个文件中，添加一个名为`OnExitClicked`的`protected`函数的声明，该函数不返回任何内容，也不接收任何参数。务必将其标记为`UFUNCTION`。\n10.  复制现有的`RestartButton`属性，但将其改为`ExitButton`。\n11.  在`RestartWidget`类的源文件中，为`OnExitClicked`函数添加一个实现。将`OnBeginOverlap`函数的内容从`VictoryBox`类的源文件复制到`OnExitClicked`函数中，但是删除正在对`DodgeballCharacter`类执行的强制转换。\n12.  在`NativeOnInitialized`函数实现中，将我们创建的`OnExitClicked`函数绑定到`ExitButton`属性的`OnClicked`事件，就像我们在*练习 8.03* 、*创建 RestartWidget C++ 类*中对`RestartButton`属性所做的那样。\n\n本活动的代码设置到此结束。编译您的更改，打开编辑器，然后打开`BP_RestartWidget`并编译它，只是为了确保没有由于`BindWidget`标签而导致的编译错误。\n\n一旦你这样做了，再玩一次关卡，让玩家角色被三个躲避球击中，注意`Restart`小部件会随着我们的新修改出现:\n\n![Figure 8.34: The updated BP_RestartWidget being shown after the player  runs out of health points ](img/B16183_08_34.jpg)\n\n图 8.34:更新后的 BP_RestartWidget 在玩家耗尽生命值后显示\n\n按下`Restart`键，应该可以重播关卡，按下`Exit`键，游戏应该结束。\n\n我们的活动到此结束。您已经巩固了使用`Widget`蓝图和更改其元素属性的基础知识，现在可以开始制作自己的菜单了。\n\n注意\n\n这个活动的解决方案可以在:[https://packt.live/338jEBx](https://packt.live/338jEBx)找到。\n\n# 总结\n\n这一章结束了，你现在已经学会了如何在 UE4 中制作游戏用户界面，理解菜单和平视显示器等东西。您已经看到了如何操作小部件蓝图的用户界面元素，包括`Buttons`、`Text`元素和`Progress Bars`；有效地与 Anchors 合作，这有助于让您的游戏 UI 优雅地适应多个屏幕；听 C++ 中的鼠标事件，比如`OnClick`事件，用它来创建自己的游戏逻辑；以及如何将您创建的小部件添加到屏幕上，或者在特定的事件中，或者让它们一直存在。\n\n在下一章中，我们将通过添加声音和粒子效果等视听元素来完善我们的躲避球游戏，并提高一个新的水平。"
  },
  {
    "path": "docs/game-dev-proj-ue/08.md",
    "content": "# 九、视听元素\n\n概观\n\n在这一章中，我们将完成我们在过去四章中一直在做的躲避球游戏。我们将通过添加音效、粒子效果和创建另一个关卡来结束这一章，这一次玩家必须遵循一个实际的路径来完成它。到本章结束时，您将能够在 UE4 项目中添加 2D 和 3D 音效以及粒子效果。\n\n# 简介\n\n在前一章中，我们学习了游戏 UI 以及如何创建用户界面(也称为小部件)并将其添加到屏幕上。\n\n在这一章中，我们将学习如何为我们的游戏添加音频和粒子效果。这两个方面都将提高我们游戏的质量，并为玩家带来更沉浸式的体验。\n\n视频游戏中的声音可以是音效(也称为 SFX)或音乐的形式。音效使你周围的世界更加可信和生动，而音乐有助于为你的游戏定调。这两个方面对你的游戏都非常重要。\n\n在*反击* ( *CS: GO* )这样的竞技游戏中，声音也是极其重要的，因为玩家需要听到周围的声音，比如枪声和脚步声，以及它们来自哪个方向，从而尽可能多地收集周围的信息。\n\n粒子效果很重要，原因和音效很重要一样:它们让你的游戏世界更可信、更沉浸。\n\n让我们从学习音频在 UE4 中的工作原理开始这一章。\n\n# UE4 中的音频\n\n任何游戏必不可少的组成部分之一就是声音。声音使你的游戏更加可信和沉浸式，这将为你的玩家提供更好的体验。电子游戏通常有两种声音:\n\n*   2D 声音\n*   3D 声音\n\n2D 声音不考虑听众的距离和方向，而 3D 声音的音量可以更高或更低，并根据玩家的位置向右或向左平移。2D 声音通常用于音乐，而 3D 声音通常用于音效。主要声音文件类型有`.wav`和`.mp3`。\n\n以下是 UE4 中与音频相关的一些资产和类别:\n\n*   `Sound Base`:表示包含音频的资产。这个类主要用于 C++ 和 bluetooth 中引用一个可以播放的音频文件。\n*   `Sound Wave`:表示已经导入 UE4 的音频文件。继承自`Sound Base`。\n*   `Sound Cue`:音频资产，可以包含与诸如衰减(音量如何随着听众距离的变化而变化)、循环、混音和其他音频相关功能相关的逻辑。它继承自`Sound Base`。\n*   `Sound Class`:允许您将音频文件分成组并管理其某些设置(如音量和音高)的资产。例如，在`SFX` `Sound Class`中对所有与音效相关的声音进行分组，在`Dialogue` `Sound Class`中对所有角色对话进行分组，等等。\n*   `Sound Attenuation`:允许您指定 3D 声音行为的资产；例如，在什么距离它将开始降低音量，在什么距离它将变得听不见(听不见)，如果它的音量将随着距离的增加而线性或指数变化，等等。\n*   `Audio Component`:演员组件，可以管理音频文件及其属性的播放。用于设置声音的连续播放，如背景音乐。\n\n在 UE4 中，我们可以像导入任何其他资产一样导入现有的声音:或者通过将文件从窗口文件浏览器拖到`Content Browser`中，或者通过单击`Content Browser`中的`Import`按钮。我们将在下一个练习中这样做。\n\n## 练习 9.01:导入音频文件\n\n在本练习中，您将把计算机中现有的声音文件导入 UE4。这个音频文件将在躲避球从表面反弹时播放。\n\n注意\n\n如果您没有音频文件(无论是`.mp3`还是`.wav`文件)来完成本练习，您可以通过以下链接下载`.mp3`或`.wav`文件:[https://www . freesoundeffects . com/free-track/bounce-1-468901/](https://www.freesoundeffects.com/free-track/bounce-1-468901/)。\n\n将该文件保存为`BOUNCE.wav`。\n\n获得音频文件后，请按照下列步骤操作:\n\n1.  打开编辑器。\n2.  Go to the `Content` folder inside the `Content Browser` interface and create a new folder called `Audio`:\n\n    ![Figure 9.1: The Audio folder in the Content Browser ](img/B16183_09_01.jpg)\n\n    图 9.1:内容浏览器中的音频文件夹\n\n3.  转到刚刚创建的`Audio`文件夹。\n4.  将音频文件导入此文件夹。您可以通过*将*音频文件从`Windows File Explorer`拖到`Content Browser`来实现。\n5.  After you've done this, a new asset should appear with the name of your audio file, which you can listen to when clicking on it:\n\n    ![Figure 9.2: The imported audio file ](img/B16183_09_02.jpg)\n\n    图 9.2:导入的音频文件\n\n6.  Open this asset. You should see many properties available for editing. However, we'll be focusing solely on some of the properties inside the `Sound` category:\n\n    ![Figure 9.3: The Sound asset’s settings ](img/B16183_09_03.jpg)\n\n    图 9.3:声音资产的设置\n\n    以下属性在`Sound`类别中可用:\n\n    *   `Looping`:这个声音在播放时是否会循环。\n    *   `Volume`:这个声音的音量。\n    *   `Pitch`:这个声音的音高。音调越高，频率越高，声音的音调也就越高。\n    *   `Class`: The `Sound Class` of this sound.\n\n        我们唯一要改变的属性是`Class`属性。我们可以使用 UE4 附带的现有的`Sound`类之一，但是让我们为躲避球创建我们自己的`Sound Class`，以便为我们的游戏创建一组新的声音。\n\n7.  转到`Content Browser`界面内的`Audio`文件夹。\n8.  *右键*，转到`Sounds`类别(倒数第二个类别)，再转到`Classes`类别，选择`Sound Class`。这将创造一个新的`Sound Class`资产。重命名该资产`Dodgeball`。\n9.  Open your imported sound asset and set its `Class` property to `Dodgeball`:\n\n    ![Figure 9.4: Changing the Class property to the Dodgeball Sound Class ](img/B16183_09_04.jpg)\n\n图 9.4:将 Class 属性更改为躲避球声音类\n\n现在这个导入的声音资产属于一个特定的类别，你可以在同一个`Sound Class`中分组与躲避球相关的其他声音效果，并通过那个`Sound Class`编辑它们的属性，包括`Volume`、`Pitch`和许多其他的。\n\n这样，我们就可以结束我们的练习了。您已经学习了如何将声音导入到项目中，以及如何更改它们的基本属性。现在，让我们继续下一个练习，在我们的游戏中，每当躲避球从表面反弹时，我们都会发出声音。\n\n## 练习 9.02:当躲避球从表面反弹时发出声音\n\n在本练习中，我们将为我们的`DodgeballProjectile`类添加必要的功能，以便当躲避球从表面反弹时会发出声音。\n\n为此，请遵循以下步骤:\n\n1.  关闭编辑器并打开 Visual Studio。\n2.  在`DodgeballProjectile`类的头文件中，添加一个名为`BounceSound`的受保护的`class USoundBase*`属性。该属性应为`UPROPERTY`并带有`EditDefaultsOnly`标签，以便在蓝图中编辑:\n\n    ```cpp\n    // The sound the dodgeball will make when it bounces off of a   surface\n    UPROPERTY(EditAnywhere, Category = Sound)\n    class USoundBase* BounceSound;\n    ```\n\n3.  完成后，转到`DodgeballProjectile`类的源文件，为`GameplayStatics`对象添加一个包含:\n\n    ```cpp\n    #include \"Kismet/GameplayStatics.h\"\n    ```\n\n4.  Then, at the beginning of the class's implementation of the `OnHit` function, before the cast to the `DodgeballCharacter` class, check whether our `BounceSound` is a valid property (different than `nullptr`) and whether the magnitude of the `NormalImpulse` property is greater than `600` units (we can access the magnitude by calling its `Size` function).\n\n    正如我们在*第八章*、*用户界面*中看到的那样，`NormalImpulse`属性表示在闪避球被击中后将改变其轨迹的力的方向和大小。之所以要检查它的量级是否大于一定量，是因为当闪避球开始失去动量，每秒几次弹离地板时，我们不想每秒几次打`BounceSound`；否则，它会产生大量噪音。因此，我们将检查躲避球受到的冲击是否大于这个量，以确保这不会发生。如果这两件事都是真的，我们就称这个`GameplayStatics`物体为`PlaySoundAtLocation`。该功能负责播放 3D 声音。它接收五个参数:\n\n    *   一个世界上下文对象，我们将其作为`this`指针传递。\n    *   一个`SoundBase`物业，这将是我们的`HitSound`物业。\n    *   声音的来源，我们将使用`GetActorLocation`功能传递。\n    *   `VolumeMultiplier`，我们将通过值`1`来传递。该值指示播放该声音时音量的高低。例如,`2`的值意味着它的体积是它的两倍。\n    *   `PitchMultiplier`, which indicates how much higher or lower the pitch of this sound will be when it's played. We'll be passing this value by using the `FMath` object's `RandRange` function, which receives two numbers as parameters and returns a random number between those two. To randomly generate a number between `0.7` and `1.3`, we'll be calling this function with these values as parameters.\n\n        看看下面的代码片段:\n\n        ```cpp\n        if (BounceSound != nullptr && NormalImpulse.Size() > 600.0f)\n        {\n          UGameplayStatics::PlaySoundAtLocation(this, BounceSound,   GetActorLocation(), 1.0f, FMath::RandRange(0.7f, 1.3f));\n        }\n        ```\n\n        注意\n\n        负责播放 2D 音的功能也可以从`GameplayStatics`对象获得，叫做`PlaySound2D`。该功能将接收与`PlaySoundAtLocation`功能相同的参数，除了第三个参数，即声音的来源。\n\n5.  编译这些更改，然后打开虚幻编辑器。\n6.  Open the `BP_DodgeballProjectile` Blueprint, go to its `Class Defaults` tab, and set the `BounceSound` property to the Sound asset you imported:\n\n    ![Figure 9.5: Setting the BounceSound property to our imported sound ](img/B16183_09_05.jpg)\n\n    图 9.5:将 BounceSound 属性设置为我们导入的声音\n\n7.  Play the level again and enter the enemy character's line of sight. You should notice a sound playing with different pitch values every time the dodgeball thrown by the enemy character hits a wall or the floor (not the player character):\n\n    ![Figure 9.6: The player character causing the enemy character to throw dodgeballs ](img/B16183_09_06.jpg)\n\n图 9.6:玩家角色导致敌方角色投掷躲避球\n\n如果发生这种情况，恭喜您–您已经使用 UE4 成功播放了声音！如果您听不到正在播放的声音，请确保它是可听到的(它的音量水平您可以听到)。\n\n然而，你可能会注意到的另一件事是，声音总是以相同的音量播放，不管角色离弹跳的躲避球有多远:声音不是以 3D 播放的，而是在 2D 播放的。要使用 UE4 播放 3D 声音，我们必须了解声音衰减资产。\n\n# 声音衰减\n\n要在 UE4 中以 3D 方式播放声音，您必须创建一个声音衰减资产，正如我们在本章第一节中提到的那样。“声音衰减”资产将允许您指定希望特定声音如何随着其与听众的距离增加而改变音量。请看下面的例子。\n\n打开虚幻编辑器，进入`Content Browser`界面内的`Audio`文件夹，*右键*，进入`Sounds`类别，选择`Sound Attenuation`。命名这个新资产`BounceAttenuation`:\n\n![Figure 9.7: Creating the Sound Attenuation asset ](img/B16183_09_07.jpg)\n\n图 9.7:创建声音衰减资产\n\n打开此`BounceAttenuation`资产。\n\n声音衰减资产有很多设置；然而，我们将主要关注`Attenuation Distance`部分的几个设置:\n\n*   `Inner Radius`:这个`float`属性允许我们指定声音在什么距离开始降低音量。如果声音在小于该值的距离播放，音量不会受到影响。将该属性设置为`200`单位。\n*   `Falloff Distance`:这个 float 属性允许我们指定我们希望声音听不见的距离。如果声音播放的距离大于这个值，我们就听不到了。声音的音量会根据它与听众的距离以及它是更靠近`Inner Radius`还是`Falloff Distance`而变化。将该属性设置为`1500`单位:\n\n![Figure 9.8: The Sound Attenuation asset settings ](img/B16183_09_08.jpg)\n\n图 9.8:声音衰减资产设置\n\n把这个想象成玩家周围的两个圆，小的圆是内圆(半径值为`Inner Radius`)，大的圆是衰减圆(半径值为`Falloff Distance`)。如果声音来自内环内部，则会以全音量播放，而来自衰减环外部的声音则根本不会播放。\n\n注意\n\n您可以在这里找到有关声音衰减资产的更多信息:\n\n[https://docs . unrelingen . com/en-us/engine/audio/distance modeling 衰减](https://docs.unrealengine.com/en-US/Engine/Audio/DistanceModelAttenuation)。\n\n现在您已经了解了声音衰减资产，让我们继续下一个练习，我们将把躲避球从地面反弹时播放的声音转换为 3D 声音。\n\n## 练习 9.03:将弹跳声音变成 3D 声音\n\n在本练习中，我们将把上一练习中添加的躲避球弹离地面时发出的声音转换为 3D 声音。这意味着当躲避球从一个表面反弹时，它发出的声音的音量会有所不同，这取决于它离玩家的距离。我们这样做是为了当躲避球离得很远时，音量会很低，而当它离得很近时，音量会很高。\n\n要使用我们在上一节中创建的`BounceAttenuation`资产，请执行以下步骤:\n\n1.  转到`DodgeballProjectile`的头文件，添加一个名为`BounceSoundAttenuation`的`protected` `class USoundAttenuation*`属性。这个属性应该是一个`UPROPERTY`，并且有`EditDefaultsOnly`标记，这样就可以在蓝图中编辑:\n\n    ```cpp\n    // The sound attenuation of the previous sound\n    UPROPERTY(EditAnywhere, Category = Sound)\n    class USoundAttenuation* BounceSoundAttenuation;\n    ```\n\n2.  转到其源文件中`OnHit`函数的`DodgeballProjectile`类实现，并在对`PlaySoundAtLocation`函数的调用中添加以下参数:\n    *   `StartTime`，我们将通过值`0`来传递。该值指示声音开始播放的时间。如果声音持续 2 秒，我们可以通过传递一个值`1`，让这个声音从 1 秒开始。我们传递一个值`0`让声音从一开始就播放。\n    *   `SoundAttenuation`, to which we'll pass our `BounceSoundAttenuation` property:\n\n        ```cpp\n        UGameplayStatics::PlaySoundAtLocation(this, BounceSound,   GetActorLocation(), 1.0f, 1.0f, 0.0f,   BounceSoundAttenuation);\n        ```\n\n        注意\n\n        虽然我们只想传递额外的`SoundAttenuation`参数，但我们也必须传递它之前的所有其他参数。\n\n3.  编译这些更改，然后打开编辑器。\n4.  Open the `BP_DodgeballProjectile` Blueprint, go to its `Class Defaults` tab, and set the `BounceSoundAttenuation` property to our `BounceAttenuation` asset:\n\n    ![Figure 9.9: Setting the BoundSoundAttenuation property to the BounceAttenuation asset ](img/B16183_09_09.jpg)\n\n    图 9.9:将 BoundSoundAttenuation 属性设置为 BoundSoundAttenuation 资产\n\n5.  Play the level again and enter the enemy character's line of sight. You should now notice that the sound that plays every time the dodgeball thrown by the enemy character hits a wall or the floor will be played at different volumes, depending on its distance, and that you won't hear it if the dodgeball is far away:\n\n    ![Figure 9.10: The player character causing the enemy character to throw dodgeballs ](img/B16183_09_10.jpg)\n\n图 9.10:玩家角色导致敌方角色投掷躲避球\n\n这样，我们就可以结束这个练习了。你现在知道如何使用 UE4 播放 3D 声音了。我们将在下一个练习中为我们的游戏添加背景音乐。\n\n## 练习 9.04:为我们的游戏添加背景音乐\n\n在本练习中，我们将在游戏中添加背景音乐。我们将通过创建一个带有音频组件的新 Actor 来实现这一点，正如我们前面提到的，它适用于播放背景音乐。为此，请遵循以下步骤:\n\n1.  下载位于[https://packt.live/3pg21sQ](https://packt.live/3pg21sQ)的音频文件，导入到`Content Browser`界面的`Audio`文件夹中，就像我们在*练习 9.01* 、*导入一个音频文件*一样。\n2.  *在`Content Browser`界面内右键*，新建一个以`Actor`类为父类的 C++ 类。命名这个新班级`MusicManager`。\n3.  当此类的文件生成并且 Visual Studio 已自动打开时，请关闭编辑器。\n4.  在`MusicManager`类的头文件中，添加一个名为`AudioComponent`的新的`class UAudioComponent*`类型的`protected`属性。将此作为`UPROPERTY`并添加`VisibleAnywhere`和`BlueprintReadOnly`标签:\n\n    ```cpp\n    UPROPERTY(VisibleAnywhere, BlueprintReadOnly)\n    class UAudioComponent* AudioComponent;\n    ```\n\n5.  在`MusicManager`类的源文件中，为`AudioComponent`类添加一个`include`:\n\n    ```cpp\n    #include \"Components/AudioComponent.h\"\n    ```\n\n6.  在该类的构造函数中，将`bCanEverTick`属性更改为`false` :\n\n    ```cpp\n    PrimaryActorTick.bCanEverTick = false;\n    ```\n\n7.  在这一行之后，添加一个新的，通过调用`CreateDefaultSubobject`函数并传递`UAudioComponent`类作为模板参数和`\"Music Component\"`作为普通参数来创建`AudioComponent`类:\n\n    ```cpp\n    AudioComponent =   CreateDefaultSubobject<UAudioComponent>(TEXT(\"Music   Component\"));\n    ```\n\n8.  进行这些更改后，编译您的代码并打开编辑器。\n9.  进入`Content Browser`界面的`ThirdPersonCPP` - > `Blueprints`文件夹，新建一个继承自`MusicManager`类的蓝图类。命名为`BP_MusicManager`。\n10.  Open this asset, select its `Audio` component, and set that component's `Sound` property to your imported sound:\n\n    ![Figure 9.11: The Sound property being updated ](img/B16183_09_11.jpg)\n\n    图 9.11:声音属性正在更新\n\n11.  将`BP_MusicManager`类的一个实例拖动到级别中。\n12.  Play the level. You should notice the music start playing when the game starts and it should also loop automatically when it reaches the end (this is done thanks to the Audio component).\n\n    注意\n\n    音频组件会自动循环播放任何声音，因此无需更改声音资产的`Looping`属性。\n\n完成所有这些步骤后，我们就完成了本练习。你现在知道如何给你的游戏添加简单的背景音乐了。\n\n现在，让我们跳到下一个主题，粒子系统。\n\n# 粒子系统\n\n让我们谈谈许多电子游戏的另一个非常重要的元素:粒子系统。\n\n在视频游戏术语中，粒子本质上是 3D 空间中可以用图像表示的位置。粒子系统是许多粒子的集合，可能具有不同的图像、形状、颜色和大小。在下图中，您将找到在 UE4 中制作的两个粒子系统的示例:\n\n![Figure 9.12: Two different Particle Systems in UE4 ](img/B16183_09_12.jpg)\n\n图 9.12:UE4 中两种不同的粒子系统\n\n左边的粒子系统被认为是电火花，可能来自一根被切割的电缆，现在短路了，而右边的粒子系统被认为是火。虽然左边的粒子系统比较简单，但是可以看出右边的这个里面有不止一种类型的粒子，可以组合在同一个系统中。\n\n注意\n\nUE4 有两种不同的工具来创建粒子系统:`Cascade`和`Niagara`。Cascade 是从 UE4 开始就存在的工具，而 Niagara 是一个更新、更复杂的系统，从 2020 年 5 月开始才可以投入生产，截止到虚幻引擎 4.25 版本。\n\n在 UE4 中创建粒子系统不在本书的讨论范围内，但建议您使用尼亚加拉瀑布，因为它是引擎的新成员。\n\n在本章中，我们将只使用 UE4 中已经包含的粒子系统，但是如果您想创建自己的粒子系统，这些链接将为您提供关于 Cascade 和 Niagara 的更多信息:\n\n层叠:[https://docs . unrealengine . com/en-US/Engine/Rendering/pieces systems/层叠](https://docs.unrealengine.com/en-US/Engine/Rendering/ParticleSystems/Cascade)\n\n[https://www . YouTube . com/playlist？列表= plzlv _ n0 _ O1 gydlyb 3 lvfjyicbbe 8 nqr 8t](https://www.youtube.com/playlist?list=PLZlv_N0_O1gYDLyB3LVfjYIcbBe8NqR8t)\n\n尼亚加拉:[https://docs . unrealengine . com/en-US/Engine/Niagara/emiteredorreference/index . html](https://docs.unrealengine.com/en-US/Engine/Niagara/EmitterEditorReference/index.html)\n\n[https://docs . unrealengine . com/en-US/Engine/nigara/quick start](https://docs.unrealengine.com/en-US/Engine/Niagara/QuickStart)\n\n我们将在下一个练习中学习如何将粒子系统添加到我们的游戏中。在本章中，我们将简单地使用 UE4 团队已经制作的现有粒子系统。\n\n## 练习 9.05:当躲避球击中玩家时产生粒子系统\n\n在本练习中，我们将了解如何在 UE4 中生成粒子系统。在这种情况下，当敌人投掷的躲避球击中玩家时，我们将产生`explosion`粒子系统。\n\n为此，请遵循以下步骤:\n\n1.  关闭编辑器并打开 Visual Studio。\n2.  In the `DodgeballProjectile` class's header file, add a protected `class UParticleSystem*` property called `HitParticles`.\n\n    `UParticleSystem`类型是 UE4 中粒子系统的名称。请确保将其设为`UPROPERTY`并给它添加`EditDefaultsOnly`标签，以便在蓝图类中进行编辑:\n\n    ```cpp\n    // The particle system the dodgeball will spawn when it hits   the player\n    UPROPERTY(EditAnywhere, Category = Particles)\n    class UParticleSystem* HitParticles;\n    ```\n\n3.  In the `DodgeballProjectile` class's source file, inside its implementation of the `OnHit` function. Before the call to the `Destroy` function, check whether our `HitParticles` property is valid. If it is, call the `GameplayStatics` object's `SpawnEmitterAtLocation` function.\n\n    这个函数将产生一个演员，该演员将扮演我们作为参数传递的粒子系统。它接收以下参数:\n\n    *   一个`World`对象，我们将使用`GetWorld`函数传递它。\n    *   一个`UParticleSystem*`物业，这将是我们的`HitParticles`物业。\n    *   The `FTransform` of the actor that will play the Particle System, which we'll pass using the `GetActorTransform` function:\n\n        ```cpp\n        if (HitParticles != nullptr)\n        {\n          UGameplayStatics::SpawnEmitterAtLocation(GetWorld(),   HitParticles, GetActorTransform());\n        }\n        ```\n\n        注意\n\n        虽然我们不会在这个项目中使用它，但是`GameplayStatics`对象中还有另一个与生成粒子系统相关的功能，那就是`SpawnEmitterAttached`功能。这个函数将生成一个粒子系统，并将其附加到一个演员身上，如果你想让一个移动的物体发光，这样粒子系统将一直附加到那个物体上，这可能会很有用。\n\n4.  编译这些更改，然后打开编辑器。\n5.  Open the `BP_DodgeballProjectile` Blueprint, go to its `Class Defaults` tab, and set the `HitParticles` property to the `P_Explosion` Particle System asset:\n\n    ![Figure 9.13: Setting the HitParticles property to P_Explosion ](img/B16183_09_13.jpg)\n\n    图 9.13:将“击中粒子”属性设置为“爆炸”\n\n6.  Now, play the level and let your player character get hit by a dodgeball. You should now see the explosion Particle System being played:\n\n    ![Figure 9.14: The explosion particle system being played when the dodgeball hits the player ](img/B16183_09_14.jpg)\n\n图 9.14:当躲避球击中玩家时，爆炸粒子系统正在播放\n\n这个练习到此结束。你现在知道如何在 UE4 中玩粒子系统了。粒子系统将增加你的游戏视觉天赋，使它更具视觉吸引力。\n\n在下一个活动中，我们将通过在躲避球击中玩家时播放声音来巩固我们在 UE4 中播放音频的知识。\n\n## 活动 9.01:闪避球击中玩家时发出声音\n\n在本活动中，我们将创建每当玩家角色被躲避球击中时负责播放声音的逻辑。在电子游戏中，以多种方式向玩家传递关键信息是非常重要的，因此除了更改玩家角色的健康栏之外，我们还将在玩家被击中时播放声音，以便玩家知道角色正在受到伤害。\n\n为此，请遵循以下步骤:\n\n1.  Import a sound file that will be played when the player character gets hit into the `Audio` folder inside the `Content Browser` interface.\n\n    注意\n\n    如果没有声音文件，可以使用[https://www.freesoundeffects.com/free-track/punch-426855/](https://www.freesoundeffects.com/free-track/punch-426855/)的可用文件。\n\n2.  打开`DodgeballProjectile`类的头文件。添加一个`SoundBase*`属性，就像我们在*练习 9.02* 中所做的那样，*当躲避球从表面反弹时发出声音*，但这次称之为`DamageSound`。\n3.  打开`DodgeballProjectile`类的源文件。在`OnHit`函数的实现中，在你破坏了玩家角色之后，在你调用`Destroy`函数之前，检查`DamageSound`属性是否有效。如果是，调用`GameplayStatics`对象的`PlaySound2D`函数(在*练习 9.02 中提到，* *当躲避球从表面反弹时发出声音*，将`this`和`DamageSound`作为参数传递给该函数调用。\n4.  编译您的更改并打开编辑器。\n5.  Open the `BP_DodgeballProjectile` Blueprint and set its `DamageSound` property to the sound file you imported at the start of this activity.\n\n    当你玩关卡时，你应该注意到每次玩家被躲避球击中时，你都会听到你输入的声音被播放:\n\n    ![Figure 9.15: A sound should play when the player character gets hit ](img/B16183_09_15.jpg)\n\n图 9.15:当玩家角色被击中时，应该会发出声音\n\n随着这些步骤的完成，您已经完成了本活动，并巩固了在 UE4 中播放 2D 和 3D 声音的用途。\n\n注意\n\n这个活动的解决方案可以在:[https://packt.live/338jEBx](https://packt.live/338jEBx)找到。\n\n现在，让我们通过学习一点关于层次设计的概念来结束这一章。\n\n# 水平设计\n\n从*第五章*、*线迹*开始，和我们的躲避球游戏有关，我们增加了相当多的游戏机制和玩法机会，以及一些视听元素，这些都在这一章处理。现在我们有了所有这些游戏元素，我们必须把它们集合到一个玩家可以从头玩到尾的水平。为此，让我们了解一下关卡设计和关卡封锁。\n\n关卡设计是一门特殊的游戏设计学科，专注于在游戏中建立关卡。关卡设计者的目标是制作一个有趣的关卡，通过使用为该游戏构建的游戏机制向玩家介绍新的游戏概念，包含良好的节奏(动作密集和放松的游戏序列的良好平衡)等等。\n\n为了测试关卡的结构，关卡设计者将首先构建一个所谓的**关卡封锁**。这是一个非常简单和精简的关卡版本，使用了最终关卡将包含的大部分元素，但是它只使用简单的形状和几何图形。这样做的原因是，如果需要更改级别的某些部分，修改级别会更容易、更省时:\n\n![Figure 9.16: An example of a level blockout made in UE4 using BSP Brushes ](img/B16183_09_16.jpg)\n\n图 9.16:在 UE4 中使用 BSP 笔刷进行关卡封锁的例子\n\n注意\n\n需要注意的是，Level Design 是自己特定的游戏开发技巧，值得自己出书，其中有不少，但潜入这个话题就不在本书范围内了。\n\n在下一个练习中，我们将使用前几章中构建的机制构建一个简单的关卡封锁。\n\n## 练习 9.06:建立关卡封锁\n\n在本练习中，我们将创建一个新的关卡封锁，它将包含一些结构，玩家将从关卡中的某个地方开始，必须通过一系列障碍才能到达关卡的终点。我们将使用我们在最后几章中构建的所有机制和对象来创建一个玩家能够完成的级别。\n\n虽然在本练习中，我们将为您提供一个解决方案，但我们鼓励您释放您的创造力，提出您的解决方案，因为在这种情况下没有正确或错误的答案。\n\n要开始本练习，请执行以下步骤:\n\n1.  打开编辑器。\n2.  转到您的`Content Browser`中的`ThirdPersonCPP` - > `Maps`文件夹，复制`ThirdPersonExampleMap`资产，并将其命名为`Level1`。您可以通过选择资产并按下 *Ctrl* + *W* 或右键单击资产并选择`Duplicate`(第三个选项)来完成此操作。\n3.  打开新创建的`Level1`地图。\n4.  删除地图内具有网格的所有对象，以下对象除外:\n    *   玩家角色\n    *   敌人角色(注意两个角色看起来是一样的)\n    *   地板物体\n    *   我们创建的两个墙对象\n    *   The Victory Box object\n\n        请记住，与照明和声音相关的资产应保持不变。\n\n5.  按下`Build`按钮，为`Level1`建立照明。该按钮位于编辑器窗口顶部的`Toolbar`中的`Play`按钮左侧。\n6.  Once you've followed these steps, you should have an empty floor with just the objects you'll be needing for this level (the ones mentioned in *Step 4*). Here's the `Level1` map before and after you followed *Steps 4 and 5*, respectively:\n\n    ![Figure 9.17: Before deleting the required objects ](img/B16183_09_17.jpg)\n\n    图 9.17:在删除所需对象之前\n\n    删除对象后，您的楼层应该如下所示:\n\n    ![Figure 9.18: After deleting the required objects  ](img/B16183_09_18.jpg)\n\n    图 9.18:删除所需对象后\n\n    因为建立一个级别，即使是一个简单的级别，也需要很多步骤和说明，你只会看到几个可能级别的截图，并再次被鼓励提出你自己的。\n\n7.  In this case, we have simply used the existing `EnemyCharacter`, `Wall`, and `GhostWall` objects and duplicated them several times to create a simple layout that the player can traverse from start to finish. We also moved the `VictoryBox` object so that it matches the new level's end location:\n\n    ![Figure 9.19: The created level – isometric view ](img/B16183_09_19.jpg)\n\n图 9.19:创建的水平等轴测视图\n\n在自顶向下的视图中可以看到该级别，如下所示:\n\n![Figure 9.20: The created level – top-down view with the player  character marked with an arrow ](img/B16183_09_20.jpg)\n\n图 9.20:创建的关卡——自上而下的视图，玩家角色用箭头标记\n\n一旦你对结果感到满意，这意味着你已经完成了你的躲避球游戏，现在可以请你的朋友和家人玩它，看看他们怎么想。干得好——你离掌握游戏开发的艺术又近了一步！\n\n# 额外功能\n\n在我们结束本章之前，这里有一些关于你在这个躲避球项目中下一步可以做什么的建议:\n\n*   让我们在前一章创建的普通`Wall`职业不会挡住敌人的视线。这样，敌人总是会向玩家扔躲避球，应该还是会被挡住不能穿过这道墙。\n*   增加一个新的功能，可以让玩家想象敌人角色投掷的躲避球将首先冲击哪里，使用扫掠痕迹的概念。\n*   增加一种新的墙，可以阻挡玩家角色、敌人角色和躲避球，但也会从躲避球中受到伤害，并在生命值耗尽时被摧毁。\n\n扩大这个项目的范围有很多可能性。我们鼓励您使用所学的技能，并做进一步的研究，以构建新的功能并增加游戏的复杂性。\n\n# 总结\n\n你现在已经完成了躲避球游戏项目。在本章中，您学习了如何通过播放音频和使用粒子系统来为游戏添加润色。你现在知道如何将 2D 和 3D 声音添加到你的游戏中，以及一些你可以使用的工具。现在，您可以尝试在游戏中添加更多的声音效果，例如当敌人角色第一次看到您时的特殊声音效果(例如在“金属齿轮固体”中)、脚步声音效果或胜利声音效果。\n\n您还使用您在最后几章中制作的所有工具构建了一个层次，从而将我们在这个项目中构建的所有逻辑推向了顶峰。\n\n在下一章中，我们将开始一个新项目:`SuperSideScroller`游戏。在那个项目中，你将被介绍到诸如启动、收藏品、敌人**人工智能** ( **人工智能**)、角色动画等主题。你将创建一个侧滚平台游戏，你控制一个必须完成一关的角色，收集宝石，并使用加电来躲避敌人。您将了解的两个最重要的主题是 UE4 的行为树和黑板，它们为人工智能系统提供燃料，以及动画蓝图，它允许您管理角色的动画。"
  },
  {
    "path": "docs/game-dev-proj-ue/09.md",
    "content": "# 十、创建`SuperSideScroller`游戏\n\n概观\n\n在本章中，我们将为新的`SuperSideScroller`游戏设置项目。你将被介绍到一个侧滚游戏的不同方面，包括电源，收藏品和敌人的人工智能，所有这些我们将在我们的项目中使用。您还将了解游戏开发中的角色动画管道，并了解如何操纵我们游戏角色的移动。\n\n到本章结束时，您将能够创建一个侧滚项目，操作我们角色的默认人体模型骨架，导入角色和动画，并创建角色和动画蓝图。\n\n# 简介\n\n到目前为止，我们已经学习了很多关于虚幻引擎，C++ 编程，以及一般的游戏开发技术和策略。在前几章中，我们讨论了诸如碰撞、跟踪、如何在虚幻引擎 4 中使用 C++ 以及蓝图可视化脚本系统等主题。除此之外，我们还获得了骨骼、动画和动画蓝图的重要知识，我们将在即将到来的项目中使用这些知识。\n\n对于我们最新的项目`SuperSideScroller`，我们将使用许多与前几章相同的概念和工具来开发我们的游戏功能和系统。碰撞、输入和抬头显示器等概念将是我们项目的重点；然而，我们也将深入到涉及动画的新概念中，以重现流行的侧滚游戏的机制。最后的项目将是我们在本书中所学的一切的高潮。\n\n有无数的侧滚游戏的例子可以作为这个项目的参考。最近，一些流行的侧滚游戏包括了诸如 *Celeste* 、*空心骑士*和*铲骑士*等标题，但侧滚/平台化游戏流派背后也有着深厚而丰富的历史，我们将在本章中讨论。\n\n# 项目分解\n\n让我们考虑一下著名的*超级马里奥兄弟*的例子，它于 1985 年在**任天堂娱乐系统** ( **NES** )控制台上发布。这款游戏由任天堂创造，宫本茂设计。对于不熟悉这个系列的人来说，一般的想法是这样的:玩家控制马里奥，马里奥必须穿越蘑菇王国的许多危险的障碍物和生物，希望从邪恶的布瑟国王库帕手中救出桃子公主。\n\n注意\n\n为了更好地了解游戏的工作原理，欢迎在[https://supermariobros.io/](https://supermariobros.io/)免费在线玩。关于整个*超级马里奥兄弟*系列的更深入的维基可以在这里找到:[https://www.mariowiki.com/Super_Mario_Bros](https://www.mariowiki.com/Super_Mario_Bros)。\n\n以下是这类游戏的核心特征和机制:\n\n1.  **Two-Dimensional Movement**: The player can only move in the *x* and *y* directions, using a 2D coordinate system. Refer to *Figure 10.1* to see a comparison of 2D and 3D coordinate systems if you are unfamiliar with them. Although our `SuperSideScroller` game will be in 3D and not pure 2D, the movement of our Character will work identically to that of Mario, only supporting vertical and horizontal movement:\n\n    ![Figure 10.1: A comparison of 2D and 3D coordinate vectors ](img/B16183_10_01.jpg)\n\n    图 10.1:2D 和三维坐标向量的比较\n\n2.  **跳跃**:跳跃是任何平台游戏最关键的方面之一，我们的`SuperSideScroller`游戏也不会有什么不同。有许多不同的游戏，如*塞莱斯特*、*空心骑士*和*超级肉仔*，如前所述，使用跳跃功能——所有这些都在 2D。\n3.  **角色加电**:没有角色加电，很多侧滚游戏就失去了混乱感和可玩性。例如，在游戏 *Ori 和盲林*中，开发者引入了不同的角色能力来改变游戏的玩法。三级跳或空中冲刺等能力为关卡的导航提供了多种可能性，并允许关卡设计者根据玩家的移动能力创建有趣的布局。\n4.  **Enemy AI**: Enemies with various abilities and behaviors are introduced to add a layer of challenge for the player, on top of the challenge of navigating the level solely through the use of the available movement mechanics.\n\n    注意\n\n    游戏中 AI 可以通过哪些方式与玩家互动？比如*上古卷轴 V:天缘*中，各个城镇乡村都有 AI 角色可以和玩家对话，曝光历史等造世界元素，向玩家出售物品，甚至给玩家任务。\n\n5.  **收藏品**:很多游戏都支持这种或那种形式的收藏品；*刺猬索尼克*有戒指，*棘轮&叮当*有收集螺栓。我们的`SuperSideScroller`游戏将允许玩家收集硬币。\n\n现在我们已经评估了我们想要支持的游戏机制，我们可以分解每个机制的功能，因为它与我们的`SuperSideScroller`以及我们需要做什么来实现这些功能相关。\n\n# 玩家角色\n\n在虚幻引擎 4 中使用`Side Scroller`游戏项目模板时，几乎所有我们想要的角色功能都是默认给我们的。\n\n注意\n\n在撰写本文时，我们使用的是虚幻引擎 4.24.2 版本；使用另一个版本的引擎可能会导致编辑器、工具以及您的逻辑在以后的工作方式上出现一些差异，所以请记住这一点。\n\n现在，让我们在下面的练习中开始创建我们的项目。\n\n## 练习 10.01:创建侧滚项目并使用角色移动组件\n\n在本练习中，您将使用`Side Scroller`模板设置虚幻引擎 4。这个练习将帮助你开始我们的游戏。\n\n以下步骤将帮助您完成练习:\n\n1.  首先，打开史诗游戏启动器，导航到左侧选项底部的`Unreal Engine`选项卡，选择顶部的`Library`选项。\n2.  接下来，您将看到一个窗口，要求您打开现有项目或创建某个类别的新项目。这些选项中有`Games`类别；为我们的项目选择此选项。选择项目类别后，系统会提示您选择项目的模板。\n3.  Next, click on the `Side Scroller` option because we want our game to use 3D Skeletal Meshes and animations, and not just 2D textures, flipbooks, and other features of the Paper2D toolset.\n\n    注意\n\n    一定要选择正确的`Side Scroller`选项，因为虚幻引擎 4 有两种侧滚项目:`Side Scroller`和`2D Side Scroller`。\n\n    我们将在本练习后不久讨论这两个项目模板之间的主要差异。\n\n    最后，我们需要设置我们的项目设置。\n\n4.  选择将项目建立在`C++ `而不是`Blueprints`的基础上，纳入`Starter Content`，使用`Desktop/Console`作为我们的平台。剩余的项目设置可以保留为默认值。选择项目的位置和名称`SuperSideScroller`，并将项目保存在您选择的适当目录中。\n5.  After these settings are applied, select `Create Project`. When it's done compiling the engine, both the Unreal Editor and Visual Studio will open, and we can get started.\n\n    ![Figure 10.2: The Unreal Engine editor should now be open ](img/B16183_10_02.jpg)\n\n    图 10.2:虚幻引擎编辑器现在应该打开了\n\n    接下来，我们继续操作存在于默认`SideScroller`角色内部的角色移动组件，看看这如何影响角色。`Character Movement`组件只能在`Character`类中实现，允许两足化身通过*行走*、*跳跃*、*飞行*和*游泳*的方式移动。该组件还内置了多人游戏所必需的网络复制功能。\n\n6.  In `Content Browser`, navigate to the `/SideScrollerCPP/Blueprints/` directory and find the `SideScrollerCharacter` Blueprint:\n\n    ![Figure 10.3: The default SideScrollerCharacter Blueprint selected inside Content Browser ](img/B16183_10_03.jpg)\n\n    图 10.3:在内容浏览器中选择的默认侧边浏览器角色蓝图\n\n7.  Double *left-click* the `Blueprint` asset to open the `Blueprint`. Sometimes, if the `Blueprint` does not have any graph logic, you will see what is shown in *Figure 10.4*. If you see this, just *left-click* on `Open Full Blueprint Editor`:\n\n    ![Figure 10.4: When a Blueprint has no graph logic ](img/B16183_10_04.jpg)\n\n    图 10.4:当蓝图没有图形逻辑时\n\n8.  打开角色`Blueprint`后，我们可以在`Components`选项卡中*左键单击*`CharacterMovement(Inherited)`组件，查看该组件的参数。\n9.  现在，在`Details`面板下，我们可以访问几十个影响角色移动的参数。在`Character Movement: Walking`类别中，我们有`Max Walk Speed`参数。将此值从`600.0f`更改为`2000.0f`。\n10.  Lastly, compile and save our Character `Blueprint`. Now, if we play in the editor, we can observe how fast our player Character is moving:\n\n    ![Figure 10.5: If we play in the editor, we can see that our Character moves much faster ](img/B16183_10_05.jpg)\n\n图 10.5:如果我们在编辑器中玩，我们可以看到我们的角色移动得更快\n\n现在你已经完成了练习，你已经亲身体验了你对玩家角色如何移动的控制！尝试更改`Max Walk Speed`的值，并观察游戏中这些更改如何影响角色。\n\n## 边滚对 2D 边滚\n\n让我们在这里花一点时间了解一下`2D Side Scroller`项目模板和`Side Scroller`模板的主要区别。`2D Side Scroller`模板使用由虚幻引擎 4 构建的 Paper2D 系统，该系统通过纹理、精灵和纸质动画书利用基于纹理的动画。\n\n注意\n\n有关 Paper2D 的更多详细信息，请参考以下文档:[https://docs . unrealengine . com/en-US/Engine/Paper2D/index . html](https://docs.unrealengine.com/en-US/Engine/Paper2D/index.html)。\n\n有足够的关于 Paper2D 的材料来证明它是自己的教科书，所以我们将不再讨论这个话题。`Side Scroller`模板，然而，几乎是相同的 2D 版本，除了我们使用的是三维动画骨架，而不是 2D 动画。\n\n现在，让我们继续，看看执行我们的第一个活动来操纵玩家角色的跳跃动作。\n\n## 活动 10.01:让我们的角色跳得更高\n\n在本练习中，我们将操作默认`Side Scroller`角色蓝图的`CharacterMovement`组件中存在的新参数(`jump`，以观察这些属性如何影响我们的角色移动。\n\n我们将实施从*练习 10.01* 、*中学习到的创建侧滚项目和使用角色移动组件*，并将其应用于如何创建我们的角色启动和角色的一般移动感觉。\n\n以下步骤将帮助您完成活动:\n\n1.  前往`SideScrollerCharacter`蓝图，在`CharacterMovement`组件中找到`Jump Z Velocity`参数。\n2.  将该参数从默认的`1000.0` f 值更改为`2000.0` f 值。\n3.  编译保存`SideScrollerCharacter`蓝图，在编辑器中播放。使用键盘上的空格键观察我们的角色能跳多高。\n4.  在编辑器中停止播放，返回`SideScrollerCharacter`蓝图，将`Jump Z Velocity`从`2000.0` f 值更新为`200.0` f 值\n5.  再次编译并保存蓝图，在编辑器中播放，观看角色跳跃。\n\n**预期输出**:\n\n![Figure 10.6: The expected output with the jumping Character ](img/B16183_10_06.jpg)\n\n图 10.6:带有跳跃字符的预期输出\n\n注意\n\n这个活动的解决方案可以在:[https://packt.live/338jEBx](https://packt.live/338jEBx)找到。\n\n现在我们已经完成了这个活动，我们对`CharacterMovement`组件参数的一些变化如何影响我们的玩家角色有了更好的理解。当我们需要给角色一些基本的动作行为，比如`Walking Speed`和`Jump Z Velocity`来达到我们想要的角色感觉时，我们可以在后面使用这个。继续之前，请将“跳跃 Z 速度”参数恢复为默认值 1000.0f\n\n我们还将记住这些参数，当我们在我们的项目后期开发我们的玩家角色启动。\n\n# 我们的侧滚游戏的特点\n\n现在让我们花一些时间来展示我们将要设计的游戏的细节。这些特性中的许多将在后面的章节中实现，但是现在是为项目规划远景的好时机。\n\n## 敌方角色\n\n玩`SuperSideScroller`项目的时候应该注意到一点，默认没有敌人 AI 提供给你。所以，让我们讨论一下我们想要支持的敌人的类型，以及他们将如何工作。我们的`SuperSideScroller`项目将支持一种敌人类型。\n\n敌人会有基本的来回移动模式，不会支持任何攻击；只有通过与玩家角色碰撞，他们才能造成任何伤害。然而，我们需要为敌人的人工智能设置两个位置之间的移动，接下来，我们需要决定人工智能是否应该改变位置。他们应该不断地在不同地点之间移动，还是应该在选择新地点之前暂停一下？\n\n最后，我们决定我们的人工智能是否应该时刻知道玩家在哪里。如果玩家来到我们敌人的某个范围内，敌人是否应该知道这一点，并积极地向玩家最后知道的位置移动？\n\n在*第 13 章* *敌方人工智能*中，我们将使用虚幻引擎 4 中可用的工具来开发这个 AI 逻辑。\n\n## 上电\n\n`SuperSideScroller`游戏项目将支持一种加电方式，即玩家可以从环境中获得药剂。这个药剂加电会增加玩家的移动速度和玩家可以跳跃的最大高度。这些效果在移除之前只会持续很短的时间。\n\n记住您在*练习 10.01* 、*创建侧滚项目并使用角色移动组件*和*活动 10.01* 、*使我们的角色跳得更高*中关于`CharacterMovement`组件的实现，您可以开发一个改变重力对角色影响的启动，这将允许有趣的新方法来导航关卡和打击敌人。\n\n## 可收藏\n\n电子游戏中的收藏品有不同的用途。在某些情况下，收藏品被用作购买升级、物品和其他商品的货币形式。在其他情况下，收藏品可以提高你的分数，或者在收集到足够多的收藏品时奖励你。对于`SuperSideScroller`游戏项目来说，硬币的作用只有一个:给玩家一个目标，在不被敌人破坏的情况下，尽可能多的收集硬币。\n\n让我们把收藏品的主要方面分解一下:\n\n*   收藏品需要与我们的玩家互动；这意味着我们需要使用碰撞检测来让玩家收集它，并为我们的 UI 添加信息。\n*   收藏品需要一个视觉静态网格表示，这样玩家就可以在关卡中识别它。\n\n我们`SuperSideScroller`项目的最后一个元素是砖块。砖块将用于`SuperSideScroller`游戏的以下目的:\n\n*   砖块被用作关卡设计的一个元素。砖块可以用来进入原本无法到达的区域；敌人可以被放置在不同的砖块上，以提供游戏的变化。\n*   砖块可以装可收集的硬币。这给了玩家一个动力去尝试看看哪些积木包含收藏品，哪些不包含。\n\n## 抬头显示器\n\nHUD 用户界面可用于根据游戏类型和您支持的机制向玩家显示重要的相关信息。对于`SuperSideScroller`项目，会有一个 HUD 元素，会向玩家显示自己收集了多少硬币。该 UI 在玩家每次收集硬币时都会更新，玩家毁灭时会重置回`0`。\n\n现在，我们已经列出了一些细节，作为这个项目的一部分，我们将继续进行动画制作。\n\n# 动画中的步骤\n\n明确一点，这本书不打算涵盖动画。我们不会讨论和学习如何使用 3D 软件工具制作动画，如 3D 工作室 Max、Maya 或 Blender。但是，我们将学习如何将这些资产导入虚幻引擎，如何在引擎中使用动画资产，以及如何使用可用的动画工具集来使我们的角色栩栩如生。\n\n# 角色动画管道\n\n出于本书的目的，我们将只关注 3D 动画以及动画如何在虚幻引擎 4 中工作；然而，简要讨论许多行业中用于创建角色及其动画的管道是很重要的。\n\n## 概念阶段\n\n第一个阶段是开发我们想要创建的角色的概念，并在以后制作动画。在 2D，这几乎都是通过手工或使用电脑程序(如 Photoshop)来完成的。在开始建模过程之前，3D 建模者可以更容易地参考角色的外观和相对大小。下面，我们看到一个不同姿势的简笔画角色的基本例子。请注意角色是如何以不同方式设定的:\n\n![Figure 10.7: A very simple example of a 2D Character concept ](img/B16183_10_07.jpg)\n\n图 10.7:2D 角色概念的一个非常简单的例子\n\n## 三维建模阶段\n\n一旦角色概念完成，管道就可以进入下一个阶段:制作角色的三维模型。模型通常是在 3D Studio Max 或 Maya 等程序中制作的，但这款软件相对昂贵，除非你有学生证，而且更常用于专业环境。\n\n在不深入讨论 3D 建模复杂性的重要细节的情况下，我们需要知道的是，3D 艺术家使用计算机软件来操纵 3D 空间中称为顶点的点来创建对象。这些物体然后被雕刻成我们的角色或环境的形状。\n\n## 索具阶段\n\n一旦最终的角色模型完成，就可以开始装配过程了。用来模拟角色的软件通常是用来装备角色的。装配意味着构建一系列骨骼，形成角色骨骼的框架。\n\n就人形角色而言，我们通常会看到头部的骨骼，沿着脊柱、臀部、腿部等等；但是骨架可以根据你制作的角色类型而变化。大象的骨骼结构将与人类完全不同。同样的装备也可以应用于不同的角色。\n\n## 动画\n\n一旦我们有了我们的角色和骨骼的层次结构，是时候让动画师把这个网格和动画结合起来了。\n\n3D 动画最基本的形式是随着时间的推移对骨骼的操纵。记录骨骼位置、旋转和比例随时间变化的过程就是动画的结果。动画完成后，我们可以从 3D 软件中导出资产，并将其导入引擎。\n\n## 资产进出口\n\n当我们有了我们的 3D 角色网格，它的骨骼装备，和它的动画，是时候从 3D 软件中导出这些资产，并将它们导入虚幻引擎 4 中了。重要的是要注意，在角色、装备和动画上工作的艺术家将不断地将进行中的资产导出到引擎中，以更好地了解游戏中看到的最终结果。我们将在本章稍后的*活动 10.03* 、*导入更多自定义动画来预览角色运行*及其附带练习中实现这一点。\n\n## 练习 10.02:探索角色编辑器并操纵默认人体模型骨架权重\n\n现在我们对动画管道有了更好的理解，让我们继续深入了解`Side Scroller`模板项目中给我们的默认人体模型骨骼网格。\n\n我们在这里的目标是了解更多关于默认骨骼网格和在角色编辑器中给我们的工具，以便我们更好地理解骨骼、骨骼权重和骨骼在虚幻引擎 4 中的工作方式。\n\n以下步骤将帮助您完成练习:\n\n1.  打开虚幻引擎编辑器，导航至`Content Browser`。\n2.  Navigate to the `/Mannequin/Character/Mesh/` folder and open the `UE4_Mannequin_Skeleton` asset:\n\n    ![Figure 10.8: The UE4_Mannequin_Skeleton asset is highlighted and visible here ](img/B16183_10_08.jpg)\n\n    图 10.8:UE4 _ 人体模型 _ 骨骼资产在此突出显示并可见\n\n    打开骨架资产后，我们会看到`Persona Editor`:\n\n    ![Figure 10.9: The Persona Editor ](img/B16183_10_09.jpg)\n\n    图 10.9:角色编辑器\n\n    让我们简单地分解一下人物角色的框架编辑器:\n\n    *   在左侧(*标有 1* ，我们可以看到骨骼中存在的骨骼层次。这是在这个角色的装配过程中制作的骨架。`root`骨，顾名思义，是骨骼层次的根。这意味着对这块骨头的变革将影响到层级结构中的所有骨头。从这里，我们可以选择一个骨骼或一部分骨骼，并查看它们在角色网格上的位置。\n    *   接下来，我们看到骨骼网格预览窗口(*标有 2* )。它向我们展示了我们的角色网格，还有几个额外的选项，我们可以打开它们来预览我们的骨骼和重量绘画。\n    *   在右侧(*用 3* 标记)，我们有基本的变换选项，可以修改单个骨骼或骨骼组。在下一个练习中，我们还将利用其他可用的设置。现在我们对它和我们正在看的东西有了更多的了解，让我们看看我们人体模型上的真实骨骼是什么样子的。\n3.  Navigate to `Character`, as shown in *Figure 10.10*:\n\n    ![Figure 10.10: The Character options menu gives you the ability to display the Skeleton of the mannequin over the mesh itself ](img/B16183_10_10.jpg)\n\n    图 10.10:角色选项菜单使您能够在网格本身上显示人体模型的骨架\n\n4.  From the drop-down menu, select the `Bones` option. Then, make sure the option for `All Hierarchy` is selected. With this option selected, you will see the outlining Skeleton rendering above the mannequin mesh:\n\n    ![Figure 10.11: The Skeleton overlayed on top of the mannequin Skeletal Mesh ](img/B16183_10_11.jpg)\n\n    图 10.11:覆盖在人体模型骨骼网格顶部的骨骼\n\n5.  现在，隐藏网格并简单预览骨架层次，我们可以禁用`Mesh`属性:\n    *   导航至`Character`，从下拉菜单中选择`Mesh`选项。\n    *   Deselect the option for `Mesh` and the result should be what we see below:\n\n        ![Figure 10.12: The skeletal hierarchy of the default Character ](img/B16183_10_12.jpg)\n\n图 10.12:默认角色的骨架层次结构\n\n出于本练习的目的，让我们重新打开`Mesh`可见性，以便同时看到网格和骨架层次。\n\n最后，一起看看我们默认角色的权重缩放。\n\n1.  To preview this, navigate to `Character` and, from the drop-down menu, select the `Mesh` option. Then, select the option for `Selected Bone Weight` toward the bottom in the section labeled `Mesh Overlay Drawing`:\n\n    ![Figure 10.13: Drop-down option to show the selected bone weight  of a bone for the mannequin ](img/B16183_10_13.jpg)\n\n    图 10.13:显示人体模型选定骨骼重量的下拉选项\n\n2.  Now, if we select a bone or a group of bones from our hierarchy, we can see how each bone affects a certain area of our mesh:\n\n    ![Figure 10.14: This is the weight scaling for the spine_03 bone ](img/B16183_10_14.jpg)\n\n    图 10.14:这是脊椎 _03 骨骼的重量比例\n\n    您会注意到，当我们预览特定骨骼的权重缩放时，骨骼网格的不同部分会有一系列颜色。这是直观显示的重量比例，而不是数字。像`red`、`orange`和`yellow`这样的颜色表示骨骼的权重较大，这意味着这些颜色中网格的高亮区域将受到更大的影响。在`blue`、`green`和`cyan`地区，它们仍然会受到影响，但不会那么严重。最后，没有叠加高光的区域将完全不受选定骨骼操作的影响。请记住骨骼的层次结构，因为即使左臂没有覆盖颜色，当您旋转、缩放和移动`spine_03`骨骼时，它仍然会受到影响，因为手臂是`spine_03`骨骼的子骨骼。请参考下图，了解手臂与脊柱的连接方式:\n\n    ![Figure 10.15: The clavicle_l and clavicle_r bones are children of the spine_03 bone ](img/B16183_10_15.jpg)\n\n    图 10.15:锁骨 _l 和锁骨 _r 骨是脊柱 _03 骨的孩子\n\n    让我们继续操作人体模型骨骼网格上的一个骨骼，看看这些变化如何影响它的动画。\n\n3.  In the Persona Editor, *left-click* the `thigh_l` bone in the skeletal hierarchy:\n\n    ![Figure 10.16: Here, the thigh_l bone is selected ](img/B16183_10_16.jpg)\n\n    图 10.16:这里选择了大腿骨\n\n    选择`thigh_l`骨骼后，我们可以清楚地看到权重缩放将如何影响网格的其他部分。此外，由于骨骼的结构，对该骨骼的任何修改都不会影响网格的上半身:\n\n    ![Figure 10.17: You can see that on the skeletal bone hierarchy, the thigh_l bone  is a child of the pelvis bone ](img/B16183_10_17.jpg)\n\n    图 10.17:你可以看到在骨骼层次上，大腿骨是骨盆骨的子骨\n\n4.  Using the knowledge from earlier chapters, change the Local Location, Local Rotation, and Scale values to offset the transform of the `thigh_l` bone. The image below shows an example of values to use.\n\n    ![Figure 10.18: The thigh_l values updated ](img/B16183_10_18.jpg)\n\n    图 10.18:更新了 thigh_l 值\n\n    对骨骼变换进行更改后，您会看到人体模型的左腿完全改变，看起来很可笑:\n\n    ![Figure 10.19: The left leg of the Mannequin Character is completely changed ](img/B16183_10_19.jpg)\n\n    图 10.19:人体模型角色的左腿完全改变\n\n5.  接下来，在`Details`面板中，前往标签为`Preview Scene Settings`的标签。*左键点击*这个标签，你会看到新的选项，显示一些默认参数和一个`Animation`部分。\n6.  使用`Animation`部分预览动画，以及动画如何受到骨骼更改的影响。对于`Preview Controller`参数，将其更改为`Use Specific Animation`选项。通过这样做，将出现一个标记为`Animation`的新选项。`Animation`参数允许我们选择与角色骨骼关联的动画进行预览。\n7.  接下来，在下拉菜单中左键单击，选择`ThirdPersonWalk`动画。\n8.  Finally, now you can see the mannequin Character playing the walking animation, but their left leg is completely misplaced and mis-scaled:\n\n    ![Figure 10.20: Preview of the updated animation for the mannequin Character ](img/B16183_10_20.jpg)\n\n图 10.20:人体模型角色更新动画的预览\n\n在继续之前，确保将`thigh_l`骨骼恢复到其原始的局部位置、局部旋转和缩放；否则，向前移动的动画看起来不正确。\n\n现在，您已经完成了我们第二个练习的最后一部分，您已经亲身体验了骨骼如何影响角色和动画。\n\n现在，让我们继续执行第二个活动，在人体模型角色上操纵不同的骨骼，并观察应用不同动画的结果。\n\n## 活动 10.02:骨骼操纵和动画\n\n在本活动中，我们将实践我们所获得的关于在默认人体模型上操纵骨骼的知识，以影响动画在骨骼上的播放方式。\n\n以下步骤将帮助您完成本活动:\n\n1.  选择将影响整个骨骼的骨骼。\n2.  更改此骨骼的比例，使角色为其原始大小的一半。使用这些值将`Scale`更改为(`X=0.500000, Y=0.500000, Z=0.500000`)。\n3.  Apply the running animation to this Skeletal Mesh from the `Preview Scene Settings` tab and observe the animation for the half-size Character:\n\n    以下是预期输出:\n\n    ![Figure 10.21: Character that is halved in size performing the running animation ](img/B16183_10_21.jpg)\n\n图 10.21:执行运行动画的减半的角色\n\n注意\n\n这个活动的解决方案可以在:[https://packt.live/338jEBx](https://packt.live/338jEBx)找到。\n\n完成本练习后，您现在已经掌握了骨骼和骨骼网格的骨骼操作如何影响动画应用的实用知识。您还看到了骨骼重量缩放的第一手效果。\n\n# 虚幻引擎 4 中的动画\n\n让我们分解动画的主要方面，因为它们在虚幻引擎中发挥作用。关于本节主题的更多深入信息，可以在可直接从 Epic Games 获得的文档中找到:[https://docs.unrealengine.com/en-US/Engine/Animation](https://docs.unrealengine.com/en-US/Engine/Animation)。\n\n## 骨骼\n\n骨架是虚幻引擎在外部 3D 软件中制作的角色装备的表示；我们在*活动 10.02* 、*骨骼骨骼操纵和动画*中看到了这一点。关于骨骼，我们已经讨论过的不多了，但主要的收获是，一旦骨骼在引擎中，我们就可以查看骨骼层次结构，操纵每个单独的骨骼，并添加称为套接字的对象。套接字允许我们做的是将对象附着到角色的骨骼上，我们可以使用这些套接字来附着网格等对象，并在不破坏骨骼变换的情况下操纵套接字的变换。在第一人称射击游戏中，通常会制作一个武器插座，并将其连接到适当的手上。\n\n## 骨骼网格\n\n骨骼网格是一种特定的网格，它结合了三维角色模型和组成骨骼的骨骼层次结构。静态网格和骨架网格的主要区别在于，骨架网格是使用动画的对象所必需的，而静态网格由于缺少骨架而不能使用动画。我们将在下一章中更多地研究我们的主要角色骨骼网格，但是我们将在本章稍后的*活动 10.03* 、*中导入我们的主要角色骨骼网格来预览角色运行*。\n\n## 动画序列\n\n最后，动画序列是可以在特定骨骼网格上播放的单个动画；它所应用的网格由将动画导入引擎时选择的骨架决定。我们将在*活动 10.03* 、*中一起查看导入我们自己的角色骨骼网格和单个动画资产，以导入更多自定义动画来预览角色运行*。\n\n我们的动画序列中包括一个时间轴，它允许我们逐帧预览动画，并带有暂停、循环、倒带等附加控制:\n\n![Figure 10.22: The animation sequence timeline and preview window ](img/B16183_10_22.jpg)\n\n图 10.22:动画序列时间线和预览窗口\n\n在下面的练习中，您将导入自定义角色和动画。自定义角色将包括骨骼网格和骨骼，动画将作为动画序列导入。\n\n## 练习 10.03:导入和设置角色和动画\n\n在我们最后的练习中，我们将导入我们的自定义角色和一个单一的动画，我们将用于`SuperSideScroller`游戏的主要角色，以及创建必要的角色蓝图和动画蓝图。\n\n注意\n\n这一章包括了一组文件在一个名为`Assets`的文件夹中，我们将把这些文件导入引擎。这些资产来自米夏莫:[https://www.mixamo.com/](https://www.mixamo.com/)；请随意创建一个帐户，并查看那里提供的免费 3D 角色和动画内容。\n\n`Assets`内容可在我们的 GitHub 上获得:[https://packt.live/2IcXIOo](https://packt.live/2IcXIOo)。\n\n以下步骤将帮助您完成练习:\n\n1.  前往虚幻编辑器。\n2.  In `Content Browser`, create a new folder named `MainCharacter`. Within this folder, create two new folders called `Animation` and `Mesh`. Our `Content Browser` tab should now look like the image below:\n\n    ![Figure 10.23: Folders added in the MainCharacter directory in Content Browser ](img/B16183_10_23.jpg)\n\n    图 10.23:在内容浏览器的主字符目录中添加的文件夹\n\n3.  接下来，导入我们的角色网格。在我们创建的`Mesh`文件夹中，*右键单击*并选择`Import`选项，将打开文件浏览器菜单。导航到本章附带的保存`Assets`文件夹的目录，在`Character Mesh`文件夹中找到`MainCharacter.fbx`资源，例如`\\Assets\\Character Mesh\\MainCharacter.fbx`，然后打开该文件。\n4.  选择该资产时，将出现“FBX 导入选项”窗口。确保在各自的复选框中将`Skeletal Mesh`和`Import Mesh`的选项设置为`check`，并将其他选项设置为默认设置。\n5.  Lastly, we can select the `Import` option so that our FBX asset will be imported into the engine. This will include the necessary materials created within the FBX; a Physics Asset, which will automatically be created for us and assigned to the `Skeletal Mesh`; and the `Skeleton Asset`.\n\n    注意\n\n    忽略导入`FBX`文件时可能出现的任何警告；它们不重要，不会影响我们的项目向前发展。\n\n    现在我们有了角色，让我们导入一个动画。\n\n6.  在`MainCharacter`文件夹目录的`Animation`文件夹中，再次*右键单击*，选择`Import`选项。\n7.  Navigate to the directory where you saved the `Assets` folder that accompanies this chapter and locate the `Idle.fbx` asset inside the `Animations/Idle` folder – for example, `\\Assets\\Animations\\Idle\\Idle.fbx` – and open that file.\n\n    选择此资产时，将出现一个与我们导入角色骨骼网格时几乎相同的窗口。因为这个资源只是一个动画，而不是骨骼网格/骨骼，我们没有像以前一样的选项，但是有一个关键参数我们需要正确设置:`Skeleton`。\n\n    我们的`FBX`导入选项的`Mesh`类别下的`Skeleton`参数告诉动画应用于哪个骨骼。没有这个参数设置，我们就不能导入我们的动画，将动画应用到错误的骨骼可能会产生灾难性的结果，或者导致动画无法完全导入。幸运的是，我们的项目很简单，我们已经导入了我们的角色骨骼网格和骨骼。\n\n8.  Select `MainCharacter_Skeleton` and choose the option at the bottom, `Import`; leave all other parameters set to their defaults.\n\n    ![Figure 10.24: The settings when importing the Idle.fbx animation ](img/B16183_10_24.jpg)\n\n    图 10.24:导入空闲动画时的设置\n\n    现在我们知道导入自定义角色网格和动画。理解这两种资产的导入过程至关重要，在下一个活动中，您将面临导入剩余动画的挑战。让我们继续这个练习，为`SuperSideScroller`游戏的主要角色创建角色蓝图和动画蓝图。\n\n    现在，尽管 Side Scroller 模板项目确实包含了我们角色的蓝图和其他资产，如动画蓝图，但为了组织和作为游戏开发人员的良好实践，我们将希望创建这些资产的自己的版本。\n\n9.  Create a new folder under our `MainCharacter` directory in `Content Browser` and name this folder `Blueprints`. In this directory, create a new Blueprint based on the `SideScrollerCharacter` class under `All Classes`. Name this new Blueprint `BP_SuperSideScroller_MainCharacter`:\n\n    ![Figure 10.25: The SideScrollerCharacter class to be used  as the parent class for our Character Blueprint ](img/B16183_10_25.jpg)\n\n    图 10.25:将用作我们的角色蓝图的父类的侧边角色类\n\n10.  In our `Blueprints` directory, *right-click* in an empty area of `Content Browser`, hover over the `Animation` option, and select `Animation Blueprint`:\n\n    ![Figure 10.26: The Animation Blueprint option under the Animation category ](img/B16183_10_26.jpg)\n\n    图 10.26:动画类别下的动画蓝图选项\n\n11.  After we select this option, a new window will appear. This new window requires us to apply a parent class and a Skeleton to our Animation Blueprint. In our case, use `MainCharacter_Skeleton`, select OK and name the Animation Blueprint asset `AnimBP_SuperSideScroller_MainCharacter`:\n\n    ![Figure 10.27: The settings we need when creating our Animation Blueprint ](img/B16183_10_27.jpg)\n\n    图 10.27:创建动画蓝图时我们需要的设置\n\n12.  When we open our Character Blueprint, `BP_SuperSideScroller_MainCharacter`, and select the `Mesh` component, we will find a handful of parameters that we can change:\n\n    ![Figure 10.28: The SuperSideScroller Character Blueprint using the mannequin Skeletal Mesh ](img/B16183_10_28.jpg)\n\n    图 10.28:使用人体模型骨骼网格的超视频角色蓝图\n\n13.  Under the `Mesh` category, we have the option to update the `Skeletal Mesh` used. Find our `MainCharacter` Skeletal Mesh and assign it to this parameter:\n\n    ![Figure 10.29: The settings we need for our Mesh component to properly  use our new Skeletal Mesh and our Animation Blueprint ](img/B16183_10_29.jpg)\n\n    图 10.29:我们的网格组件正确使用我们新的骨骼网格和动画蓝图所需的设置\n\n    当仍然在我们的角色蓝图中并且选择了`Mesh`组件时，我们可以在`Mesh`类别的正上方找到`Animation`类别。幸运的是，默认情况下`Animation Mode`参数已经设置为`Use Animation Blueprint`，这是我们需要的设置。\n\n14.  现在将`Anim`类参数分配给我们新的动画蓝图`AnimBP_SuperSideScroller_MainCharacter`。最后，回到我们默认的`SideScrollerExampleMap`等级，用我们新的角色蓝图替换默认角色。\n15.  Next, make sure that we have `BP_SuperSideScroller_MainCharacter` selected in our `Content Browser` and then *right-click* on the default Character in our map and choose to replace it with our new Character:\n\n    ![Figure 10.30: With the Character Blueprint selected in Content Browser, we can simply right-click on the default Character in the level and replace it with our new Character ](img/B16183_10_30.jpg)\n\n    图 10.30:在内容浏览器中选择角色蓝图后，我们可以简单地右键单击该级别中的默认角色，并用我们的新角色替换它\n\n16.  With our new Character in the level, we can now play in the editor and move around the level. The result should look something like the image below; our Character in the default T-pose is moving around the level environment:\n\n    ![Figure 10.31: You now have the custom Character running around the level ](img/B16183_10_31.jpg)\n\n图 10.31:你现在有了自定义角色在关卡中运行\n\n随着我们最后一个练习的完成，您现在已经完全理解了如何导入自定义骨骼网格和动画。此外，您还学习了如何从头开始创建角色蓝图和动画蓝图，以及如何使用这些资源创建`SuperSideScroller`角色的基础。\n\n让我们进入本章的最后一项活动，您将被要求导入角色的剩余动画，并在角色编辑器中预览正在运行的动画。\n\n## 活动 10.03:导入更多自定义动画以预览角色运行\n\n本活动旨在导入剩余的动画，例如为玩家角色运行，并在角色骨架上预览运行的动画，以确保其看起来正确。\n\n活动结束时，所有玩家角色动画都将导入到项目中，您将准备好使用这些动画在下一章中让玩家角色栩栩如生。\n\n以下步骤将帮助您完成活动:\n\n1.  提醒一下，我们需要导入的所有动画资产都存在于`\\Assets\\Animations`目录中，无论您在哪里保存了原始的`zip`文件夹。导入`MainCharacter/Animation`文件夹中的所有剩余动画。导入剩余的动画资产的工作方式与在*练习 10.03* 、*导入和设置角色和动画*中导入`Idle`动画的方式相同。\n2.  导航到`MainCharacter`骨架，应用上一步导入的`Running`动画。\n3.  Finally, with the `Running` animation applied, preview the Character animation in the Persona Editor.\n\n    以下是预期输出:\n\n    ![Figure 10.32: The expected output of the Character with additional custom imported assets ](img/B16183_10_32.jpg)\n\n图 10.32:带有额外定制导入资产的角色的预期输出\n\n注意\n\n这个活动的解决方案可以在:[https://packt.live/338jEBx](https://packt.live/338jEBx)找到。\n\n随着这个最终活动的完成，您现在已经亲身体验了将自定义骨骼和动画资产导入虚幻引擎 4 的过程。导入过程，无论您要导入的资产类型是什么，在游戏行业都很常见，让您适应它至关重要。\n\n# 总结\n\n随着玩家角色骨骼、骨骼网格和动画导入到引擎中，我们可以进入下一章，您将准备角色移动并更新动画蓝图，以便角色可以在关卡中移动时制作动画。\n\n从本章的练习和活动中，您学习了如何使用骨骼和骨骼来制作角色的动画和操纵角色。有了将动画导入和应用到虚幻引擎 4 的第一手经验，您现在对动画管道有了很好的理解，从角色概念到为项目导入的最终资产。\n\n此外，您已经了解了我们将在下一章中使用的主题，例如角色运动动画混合的混合空间。随着`SuperSideScroller`项目模板的创建和玩家角色的准备，在下一章中，让我们继续使用动画蓝图制作角色的动画。"
  },
  {
    "path": "docs/game-dev-proj-ue/10.md",
    "content": "# 十一、混合空间 1D、按键绑定和状态机\n\n概观\n\n本章首先创建混合空间资产，根据玩家角色的速度，允许从空闲到行走，最后到跑步的运动动画混合。然后，我们将实现新的键映射，并在 C++ 中使用这些映射来为玩家角色编写游戏功能，比如冲刺。最后，我们将在我们的角色动画蓝图中创建一个新的动画状态机，以便玩家动画可以在移动和跳跃之间平滑过渡。\n\n在这一章的最后，你将让`SuperSideScroller`玩家角色在环境中移动时正确地制作动画，并以游戏感觉最好的方式移动。这意味着玩家将支持一个空闲、行走和冲刺动画，同时也支持跳跃所需的动画。\n\n# 简介\n\n在前一章中，我们对动画和您的`SuperSideScroller`项目的游戏设计开发进行了高层次的了解。您只获得了项目本身开发的开始步骤。您还准备了玩家“角色”的动画蓝图、角色蓝图，并导入了所有必需的骨骼和动画资产。\n\n此时，角色可以在关卡周围移动，但停留在 T-Pose 中，完全没有动画效果。这可以通过为玩家角色创建一个新的混合空间来解决，这将在本章的第一个练习中完成。一旦混合空间完成，您将在角色动画蓝图中实现这一点，以便角色在移动时制作动画。\n\n在本章中，您将使用许多新的功能、资产类型和变量来实现玩家角色的期望移动。其中包括`Animation Blueprint`中的`Try Get Pawn Owner`功能、`1D Blend space asset`类型和项目配置文件中的`Input Bindings`。\n\n让我们开始这一章，首先学习混合空间，然后创建你的混合空间资产，你将需要，以便让玩家角色动画移动。\n\n# 混合空间\n\n混合空间，顾名思义，允许您基于一个或多个条件在多个动画之间混合。混合空间用于不同类型的电子游戏，但是，更多的时候，在游戏中，玩家可以看到整个角色。当玩家只能看到角色手臂时，通常不会使用混合空间，例如在虚幻引擎 4 中提供的第一人称模板项目中，如下所示:\n\n![Figure 11.1: The first-person perspective of the default character in the First-Person project template in Unreal Engine 4\\. ](img/B16183_11_01.jpg)\n\n图 11.1:虚幻引擎 4 中第一人称项目模板中默认角色的第一人称视角。\n\n这在第三人称游戏中更常见，在第三人称游戏中，需要使用混合空间来平滑混合角色的基于运动的动画。虚幻引擎 4 中提供的第三人称模板项目就是一个很好的例子，如下所示:\n\n![Figure 11.2: The third-person perspective of the default character, in the First-Person project template in Unreal Engine 4 ](img/B16183_11_02.jpg)\n\n图 11.2:默认角色的第三人称视角，在虚幻引擎 4 的第一人称项目模板中\n\n混合空间允许玩家角色基于一个变量或一组变量在动画之间混合。例如*《最后的我们》*中的*乔尔*的情况，他的移动动画是基于他移动的速度，这个速度是由玩家通过控制器棒(或操纵杆)提供的。随着速度的提高，他的动画从行走更新为跑步，然后是冲刺。这就是我们在这一章中试图用我们的性格达到的目的。\n\n打开`/Mannequin/Animations/ThirdPerson_IdleRun_2D`创建`Side Scroller`项目模板时，我们来看看虚幻引擎提供的混合空间资产。这是为`Side Scroller`人体模型骨骼网格创建的 1D 混合空间资产，因此玩家角色可以根据角色的速度在空闲、行走和跑步动画之间平滑混合。\n\n如果您勾选`Persona`，在左侧的`Asset Details`面板中，您将看到带有`Horizontal Axis`参数的`Axis Settings`类别，其中我们有这个轴的设置，它本质上是一个变量，我们可以在我们的动画蓝图中引用。请参考下图查看`Persona`内的`Axis Settings`。\n\n![Figure 11.3: Shown here are the axis settings for the 1D Blend Space ](img/B16183_11_03.jpg)\n\n图 11.3:这里显示的是 1D 混合空间的轴设置\n\n在预览窗口的下方，我们还会看到一个小图，点沿着直线从左到右排列；其中一个点将被突出显示`green`，而其他点为`white`。我们可以*左键单击*并沿着水平轴拖动这个`green`点，根据其值预览混合动画。在速度`0`下，我们的角色在`Idle`中，当我们沿着轴移动预览时，动画将开始混合行走，然后是`Running`。请参考下图查看单轴图。\n\n![Figure 11.4: Highlighted here is the key frame timeline of the 1D Blend Space ](img/B16183_11_04.jpg)\n\n图 11.4:这里突出显示的是 1D 混合空间的关键帧时间线\n\n在下一节中，我们将看到 1D 混合空间与普通混合空间的对比。\n\n## 1D 混合空间与普通混合空间\n\n在继续讨论 1D 混合空间之前，让我们花点时间来区分一下虚幻引擎 4 中混合空间和 1D 混合空间的主要区别。\n\n*   虚幻中的混合空间资产由两个变量控制，由混合空间图形的 *X* 和 *Y* 轴表示。\n*   另一方面，1D 混合空间仅支持一个轴。\n\n试着把它想象成一个 2D 图。正如您所知，每个轴都有自己的方向，您可以更好地想象为什么以及何时需要使用此混合空间，而不是仅支持单个轴的 1D 混合空间。\n\n比方说，你想让玩家角色左右扫射，同时也支持前后移动。如果您将此运动绘制在一个图表上，它将如下图所示:\n\n![Figure 11.5: This is what a Blend Space movement would look like on a simple graph ](img/B16183_11_05.jpg)\n\n图 11.5:这是混合空间运动在简单图表上的样子\n\n现在，想象玩家角色的移动，记住游戏是一个`Side Scroller`的事实。角色不会支持左右扫射或前后移动。玩家角色将只需要在一个方向上制作动画，因为默认情况下`Side Scroller`角色向移动方向旋转。必须只支持一个方向，这就是为什么你使用 1D 混合空间，而不是普通的混合空间。\n\n我们需要为我们的主要角色设置这种类型的混合空间资产，并将混合空间用于相同的目的，用于基于运动的动画混合。在下一个练习中，让我们从使用自定义动画资产一起创建混合空间资产开始。\n\n## 练习 11.01:创建角色运动 1D 混合空间\n\n为了让玩家角色在移动时有动画效果，您需要首先创建一个混合空间，如前所述。\n\n在本练习中，您将创建混合空间资产，添加空闲动画，并更新`CharacterMovement`组件，以便指定与混合空间相对应的适当行走速度值。\n\n以下步骤将帮助您完成练习:\n\n1.  导航到`Content Browser`中的`/MainCharacter/Animation`文件夹，您在上一章中导入的所有新动画都位于该文件夹中。\n2.  现在，*右键单击`Content Browser`主区域的*，从下拉菜单中，将鼠标悬停在`Animation`选项上，并从其附加下拉菜单中，通过左键单击*选择`Blend Space 1D`。*\n**   Make sure to select `MainCharacter_Skeleton`, and not `UE4_Mannequin_Skeleton`, as the skeleton for the Blend Space.\n\n    注意\n\n    如果应用了不正确的骨架，当为需要骨架的资源(如混合空间或动画蓝图)选择骨架时，混合空间对于玩家角色及其自定义骨架网格将不起作用。在这里，您要告诉这个资产它与哪个骨架兼容。通过这样做，在混合空间的情况下，您可以使用为此骨架制作的动画，从而确保一切都与其他一切兼容。\n\n    *   命名这个混合空间资产`SideScroller_IdleRun_1D`。*   Next, open the `SideScroller_IdleRun_1D` Blend Space asset. You can see the single-axis graph below the preview window:\n\n    ![Figure 11.6: The editing tool used to create Blend Spaces in Unreal Engine 4 ](img/B16183_11_06.jpg)\n\n    图 11.6:用于在虚幻引擎 4 中创建混合空间的编辑工具\n\n    在编辑器的左侧，您有包含`Axis Settings`类别的`Asset Details`面板。在这里，您将标记轴，并提供一个最小和最大浮动值，该值稍后将在`Animation Blueprint`中对玩家角色有用。请参考下图查看`Horizontal Axis`的默认值设置。\n\n    ![Figure 11.7: The axis settings that affect the axis of the Blend Space ](img/B16183_11_07.jpg)\n\n    图 11.7:影响混合空间轴的轴设置\n\n    *   Now, rename the `Horizontal Axis` as `Speed`:\n\n    ![Figure 11.8: The horizontal axis is now named Speed ](img/B16183_11_08.jpg)\n\n    图 11.8:横轴现在被命名为速度\n\n    *   The next step is to establish `Minimum Axis Value` and `Maximum Axis Value`. You will want the minimum value to be `0.0f`, which is set by default, because the player character will be in `Idle` when he is not moving at all.\n\n    但是`Maximum Axis Value`呢？这个有点棘手，因为你需要记住以下几点:\n\n    *   您将支持角色的冲刺行为，该行为允许玩家在按住*左移位*键盘按钮时移动得更快。释放后，玩家将恢复默认行走速度。\n    *   The walking speed to match the characters' `Max Walk Speed` parameter of the `CharacterMovementComponent`.\n\n        在设置`Maximum Axis Value`之前，需要将角色的`Max Walk Speed`设置为适合`SuperSideScroller`游戏的值。\n\n        *   For this, navigate to `/Game/MainCharacter/Blueprints/` and open the `BP_SuperSideScroller_MainCharacter` blueprint:\n\n    ![Figure 11.9: The directory of the SuperSideScroller main character blueprint ](img/B16183_11_09.jpg)\n\n    图 11.9:SuperSideScroller 主要角色蓝图的目录\n\n    *   Select the `Character Movement` component and, in the `Details` panel, under the `Character Movement: Walking` category, find the `Max Walk Speed` parameter and set this value to `300.0f`.\n\n    设置`Max Walk Speed`参数后，返回`SideScroller_IdleRun_1D`混合空间并设置`Maximum Axis Value`参数。如果行走速度是`300.0f`，最大值应该是多少？请记住，您将支持玩家角色的冲刺，该最大值需要大于行走速度。\n\n    *   将`Maximum Axis Value`参数更新为`500.0f`值。*   最后，将`Number of Grid Divisions`参数设置为`5`值。这样做的原因是，当使用分区时，每个网格点之间的`100`单位间距使其更容易使用，因为`Maximum Axis Value`是`500.0f`。当您沿网格应用移动动画时，这在网格点捕捉的情况下非常有用。*   Leave the remaining properties set as their defaults:\n\n    ![Figure 11.10: The final axis settings for the Blend Space ](img/B16183_11_10.jpg)\n\n    图 11.10:混合空间的最终轴设置\n\n    您在这里使用这些设置所做的是告诉混合空间使用`0.0f`和`500.0f`之间的传入浮点值来混合您将在下一步中放置的动画和活动。通过将网格划分为`5`分区，您可以沿着轴图以正确的浮点值轻松添加所需的动画。\n\n    让我们通过向轴图添加第一个动画`Idle`动画来继续创建混合空间。\n\n    *   网格的右边是`Asset Browser`标签。请注意，资产列表包括您在*第 12 章*、*动画混合和蒙塔格斯*中导入的玩家角色的所有动画。这是因为您在创建混合空间时选择了`MainCharacter_Skeleton`资源。*   Next, *left-click* and drag the `Idle` animation to our grid at position `0.0`:\n\n    ![Figure 11.11: Dragging the Idle animation to the grid position 0.0 ](img/B16183_11_11.jpg)* \n\n *图 11.11:将空闲动画拖动到网格位置 0.0\n\n请注意，当将此动画拖到网格时，它将捕捉到网格点。一旦动画被添加到混合空间，玩家角色将从其默认的丁字姿势改变，并开始播放`Idle`动画:\n\n![Figure 11.12: With the Idle animation added to the 1D Blend Space,  the player character begins to animate ](img/B16183_11_12.jpg)\n\n图 11.12:随着空闲动画被添加到 1D 混合空间，玩家角色开始制作动画\n\n完成本练习后，您现在已经了解了如何创建 1D 混合空间，更重要的是，您知道了 1D 混合空间和普通混合空间之间的区别。此外，您知道在玩家角色移动组件和混合空间之间对齐值的重要性，以及为什么您需要确保行走速度与混合空间中的值适当相关。\n\n现在让我们进入本章的第一个活动，您将在混合空间中应用剩余的`Walking`和`Running`动画，就像添加`Idle`动画一样。\n\n## 活动 11.01:将行走和跑步动画添加到混合空间\n\n到目前为止，1D 运动混合空间融合得很好，但是你错过了行走和跑步动画。在本练习中，您将通过将这些动画以对主要角色有意义的适当水平轴值添加到混合空间来完成混合空间。\n\n使用从*练习 11.01* 、*获得的知识创建角色移动 1D 混合空间*，执行以下步骤完成角色移动混合空间:\n\n1.  从*练习 11.01* 、*继续，创建角色移动 1D 混合空间*，返回`Asset Browser`。\n2.  现在，将`Walking`动画添加到水平网格位置`300.0f`。\n3.  Finally, add the `Running` animation to the horizontal grid position `500.0f`.\n\n    注意\n\n    记住可以*左键点击*沿着网格轴拖动绿色预览网格点，看动画是如何基于轴值融合在一起的，所以要注意角色动画预览窗口，确保看起来正确。\n\n预期产出如下:\n\n![Figure 11.13: The Running animation in the Blend Space ](img/B16183_11_13.jpg)\n\n图 11.13:混合空间中的跑步动画\n\n此活动完成后，您将拥有一个功能性的混合空间，该空间根据代表玩家角色速度的横轴值混合从`Idle`到`Walking`到`Running`的角色移动动画。\n\n注意\n\n这个活动的解决方案可以在:[https://packt.live/338jEBx](https://packt.live/338jEBx)找到。\n\n# 主角动画蓝图\n\n随着动画被添加到混合空间，你应该能够四处走动，并看到那些动画在工作，对不对？嗯，没有。如果你选择编辑器中的播放，你会注意到主角仍然以丁字姿势移动。原因是因为您还没有告诉动画蓝图使用我们的混合空间资产，这将在本章稍后进行。\n\n## 动画蓝图\n\n在开始使用您在上一章中创建的动画蓝图之前，让我们简要讨论一下这种类型的蓝图是什么，以及它的主要功能是什么。动画蓝图是一种蓝图类型，允许您控制骨骼和骨骼网格的动画，在本例中，是您在上一章中导入的玩家角色骨骼和网格。\n\n动画蓝图分为两个主要图形:\n\n*   事件图\n*   动漫图\n\n事件图的工作方式和普通蓝图一样，你可以使用事件、函数和变量来编写游戏逻辑。另一方面，动画图形对于动画蓝图来说是独一无二的，这是您使用逻辑来确定骨骼和骨骼网格在任何给定帧的最终姿势的地方。在这里，您可以使用状态机、动画片段、混合空间和其他动画相关节点等元素，然后输出到角色的最终动画。\n\n看看下面的例子(你可以跟着做)。\n\n打开`MainCharacter/Blueprints`目录下的`AnimBP_SuperSideScroller_MainCharacter`动画蓝图。\n\n默认情况下，`AnimGraph`应该会打开，在这里你可以看到角色预览，我们的`Asset Browser`标签，以及主图。正是在这个`AnimGraph`中，你将实现你刚刚创建的混合空间，以便让玩家角色在关卡中移动时正确地制作动画。\n\n让我们从下一个练习开始，我们将在这里进行，并了解更多关于动画蓝图的信息。\n\n## 练习 11.02:将混合空间添加到角色动画蓝图\n\n在本练习中，您将向动画蓝图中添加混合空间，并准备必要的变量，以帮助根据玩家角色的移动速度控制该混合空间。让我们从添加混合空间到`AnimGraph`开始。\n\n以下步骤将帮助您完成本练习:\n\n1.  Add the Blend Space to `AnimGraph` by finding the `Asset Browser` on the right-hand side, and *left-click* and drag the `SideScroller_IdleRun_1D` Blend Space asset into `AnimGraph`.\n\n    请注意，该混合空间节点的变量输入标记为`Speed`，就像混合空间内的水平轴一样。请参考*图 11.14* 查看`Asset Browser`中的融合空间。\n\n    注意\n\n    如果要以不同的方式命名`Horizontal Axis`，新名称将显示为混合空间的输入参数。\n\n    ![Figure 11.14: Asset Browser gives you access to all animation assets related to the MainCharacter_Skeleton ](img/B16183_11_14.jpg)\n\n    图 11.14:资产浏览器允许您访问与 MainCharacter_Skeleton 相关的所有动画资产\n\n2.  Next, connect the `Output Pose` asset of the Blend Space node to the `Result` pin of the `Output Pose` node. Now, the animation pose in the preview shows the character in the `Idle` animation pose:\n\n    ![Figure 11.15: You now have limited control of the Blend Space and can manually enter values into the Speed parameter to update the character movement animations ](img/B16183_11_15.jpg)\n\n    图 11.15:您现在对混合空间的控制有限，可以手动输入速度参数值来更新角色移动动画\n\n3.  If you use `PIE,`(**Play-In-Editor**), the player character will be moving around, but will play the `Idle` animation instead of remaining in the T-Pose:\n\n    ![Figure 11.16: The player character now plays the Idle animation in-game ](img/B16183_11_16.jpg)\n\n    图 11.16:玩家角色现在在游戏中播放空闲动画\n\n    现在，您可以使用我们的`Speed`输入变量来控制我们的混合空间。有了使用混合空间的能力，您需要一种方法来存储角色的移动速度，并将该值传递给混合空间的`Speed`输入参数。这是您需要做的:\n\n4.  Navigate to the `Event Graph` of our animation blueprint. By default, there will be the `Event Blueprint Update Animation` event and a pure `Try Get Pawn Owner` function. Please refer to *Figure 11.17* to see the default setup of `Event Graph`. The event is updated each frame that the animation is updated, and returns the **Delta Time** between each frame update and the owning pawn of this animation blueprint. You need to make sure that the owning pawn is of the `SuperSideScroller` player character blueprint class before attempting to get any more information.\n\n    ![Figure 11.17: Animation blueprints include this event and function pair  by default to use in your Event Graph ](img/B16183_11_17.jpg)\n\n    图 11.17:动画蓝图默认包括这个事件和函数对，用于事件图中\n\n    注意\n\n    虚幻引擎 4 中的`Pure`和`Impure`函数的主要区别在于`Pure` 函数意味着它包含的逻辑不会修改正在使用它的类的变量或成员。在`Try Get Pawn Owner`的情况下，它只是返回了对动画蓝图的`Pawn`所有者的引用。`Impure`函数没有这种含义，可以自由修改它想要的任何变量或成员。\n\n5.  Get the `Return Value` from the `Try Get Pawn Owner` function and, from the `Context Sensitive` menu that appears, search for the cast to `SuperSideScrollerCharacter`:\n\n    ![Figure 11.18: The context-sensitive menu finds the related function or variable on which basis actions can be taken on the object you are checking from ](img/B16183_11_18.jpg)\n\n    图 11.18:上下文相关菜单找到相关的函数或变量，在此基础上可以对您正在检查的对象采取操作\n\n6.  Connect the execution output pin from `Event Blueprint Update Animation` to the execution input pin of the cast:\n\n    ![Figure 11.19: Inside the Event Graph, use the Try Get Pawn Owner function to cast the returned Pawn object to the SuperSideScrollerCharacter class ](img/B16183_11_19.jpg)\n\n    图 11.19:在事件图中，使用 Try Get 典当所有者函数将返回的典当对象转换为 SuperSideScrollerCharacter 类\n\n    你创建的角色蓝图继承自`SuperSideScrollerCharacter`类。由于这个动画蓝图的拥有棋子是你的`BP_SuperSideScroller_MainCharacter`角色蓝图，并且这个蓝图继承自`SuperSideScrollerCharacter`类，因此施法功能将会成功执行。\n\n7.  Next, store the returned value from the cast to its own variable; that way, we have a reference to it in case we need to use it again in our animation blueprint. Refer to *Figure 11.20* and make sure to name this new variable `MainCharacter`.\n\n    注意\n\n    `Promote to Variable`的上下文相关下拉菜单中有一个选项，允许您将任何有效值类型存储到自己的变量中。\n\n    ![Figure 11.20: As long as the cast is successful, you will want to keep  track of the owning character ](img/B16183_11_20.jpg)\n\n    图 11.20:只要演员成功，你就会想要跟踪拥有的角色\n\n8.  Now, to track the character's speed, use the `Get Velocity` function from the `MainCharacter` variable. Every object from the `Actor` class has access to this function that returns the magnitude and direction vector that the object is moving in:\n\n    ![Figure 11.21: The GetVelocity function can be found under Utilities/Transformation ](img/B16183_11_21.jpg)\n\n    图 11.21:获取速度函数可以在工具/转换下找到\n\n9.  From `Get Velocity`, you can use the `VectorLength` function to get the actual speed:\n\n    ![Figure 11.22: The VectorLength function returns the magnitude of the vector,  but not the direction ](img/B16183_11_22.jpg)\n\n    图 11.22:向量长度函数返回向量的大小，但不返回方向\n\n10.  `Return Value` from the `VectorLength` function can then be promoted to its own variable named `Speed`:\n\n    ![Figure 11.23: Every actor has the Get Velocity function that returns the magnitude and direction of the actor's movement ](img/B16183_11_23.jpg)\n\n图 11.23:每个演员都有 Get Velocity 函数，该函数返回演员运动的幅度和方向\n\n在本练习中，您可以使用`GetVelocity`功能获得玩家角色速度。从`GetVelocity`函数返回的矢量给出了确定实际速度的矢量长度。通过将该值存储在`Speed`变量中，您现在可以在动画蓝图的`AnimGraph`中引用该值来更新您的混合空间，这将在下一个练习中进行。\n\n# 速度矢量\n\n在进入下一步之前，让我们解释一下当你得到角色的速度时你在做什么，并将那个向量的向量长度提升到`Speed`变量。\n\n什么是速度？速度是一个给定**大小**和**方向**的矢量。换个角度想想，矢量可以像*箭头*一样画出来。箭头的*长度代表**大小**，或*强度*，*箭头*方向代表**方向**。所以，如果你想知道玩家角色移动的速度，你会想要得到那个向量的长度。当我们在返回的速度向量上使用`GetVelocity`函数和`VectorLength`函数时，这正是你正在做的；你得到了我们性格中`Speed`变量的值。这就是为什么您将该值存储在变量中，并使用它来控制混合空间，如下图所示，这是矢量的一个示例。其中一个方向为正(右)方向，大小为`100`，另一个方向为负(左)方向，大小为`35`。*\n\n![Figure 11.24: Figure showing two different vectors ](img/B16183_11_24.jpg)\n\n图 11.24:显示两个不同向量的图\n\n## 练习 11.03:将混合空间添加到角色动画蓝图\n\n现在您已经更好地理解了`Vectors`以及如何存储上一练习中玩家角色的`Speed`变量，您可以按照以下步骤将速度应用到您在本章前面创建的 1D 混合空间。\n\n以下步骤将帮助您完成练习:\n\n1.  导航至`AnimBP_SuperSideScroller_MainCharacter`动画蓝图中的`AnimGraph`。\n2.  Use the `Speed` variable to update the Blend Space in real time in the `AnimGraph` by *left-clicking* and dragging the `Speed` variable onto the graph, and connecting the variable to the input of the `Blendspace Player` function:\n\n    ![Figure 11.25: You can now use the Speed variable to update the Blend Space in every frame when the animation is updated ](img/B16183_11_25.jpg)\n\n    图 11.25:现在可以在动画更新时使用速度变量来更新每一帧中的混合空间\n\n3.  Next, compile the animation blueprint.\n\n    你现在可以根据玩家角色的速度更新混合空间。使用`PIE`时，可以看到`Idle`中的人物，移动时可以看到`Walking`动画中的人物:\n\n    ![Figure 11.26: The player character is finally able to walk around in the level ](img/B16183_11_26.jpg)\n\n图 11.26:玩家角色终于可以在关卡中走动了\n\n最后，主要角色使用基于移动速度的移动动画。在下一个活动中，您将更新角色移动组件，以便可以从混合空间预览角色运行动画。\n\n## 活动 11.02:游戏中预览运行动画\n\n随着动画蓝图的更新和获得玩家角色的速度，您现在可以在游戏中预览`Idle`和`Walking`动画。\n\n在本活动中，您将更新玩家角色蓝图的`CharacterMovement`组件，以便您也可以在游戏中预览`Running`动画。\n\n为此，请执行以下步骤:\n\n1.  导航并打开`BP_SuperSideScroller_MainCharacter`玩家角色蓝图。\n2.  进入`CharacterMovement`组件。\n3.  将`Max Walk Speed`参数修改为`500.0`的值，以便您的角色可以快速移动，将其动画从`Idle`混合到`Walking`，最后混合到`Running`。\n\n在本活动结束时，您将允许玩家角色达到一个速度，允许您在游戏中预览`Running`动画。\n\n预期产出如下:\n\n![Figure 11.27: The player character running ](img/B16183_11_27.jpg)\n\n图 11.27:玩家角色运行\n\n注意\n\n这个活动的解决方案可以在:[https://packt.live/338jEBx](https://packt.live/338jEBx)找到。\n\n现在您已经处理了玩家角色从`Idle`到`Walk`以及最后到`Run`的移动混合，让我们进入下一步，添加允许玩家角色通过冲刺移动得更快的功能。\n\n# 输入绑定\n\n每一个游戏都需要玩家输入，无论是键盘上的 *W* 、 *A* 、 *S* 、 *D* 等移动玩家角色的按键，还是控制器上的拇指棒；这就是电子游戏成为互动体验的原因。虚幻引擎 4 允许我们将键盘、鼠标、游戏手柄和其他类型的控件映射到标记的动作或轴，然后您可以在蓝图或 C++ 中引用这些动作或轴来实现角色或游戏功能。需要指出的是，每个唯一的动作或轴映射可以有一个或多个键绑定，并且同一个键绑定可以用于多个映射。输入绑定保存在一个名为`DefaultInput.ini`的初始化文件中，可以在项目目录的`Config`文件夹中找到。\n\n注意\n\n输入绑定可以直接从`DefaultInput.ini`文件编辑，也可以通过编辑器本身的`Project Settings`编辑；后者更容易访问，并且在编辑时不容易出错。\n\n让我们为玩家角色的`Sprint`功能添加一个新的输入绑定。\n\n## 练习 11.04:增加短跑和投掷的输入\n\n随着玩家角色在关卡中移动，您现在将为玩家角色实现一个从基础`SuperSideScrollerCharacter` C++ 类派生的唯一角色类。这样做的原因是为了以后可以轻松区分玩家角色和敌人的职业，而不是仅仅依靠独特的蓝图职业。\n\n在创建独特的 C++ 角色类时，您将实现*冲刺*行为，以允许玩家角色根据需要进行*行走*和*冲刺*。\n\n让我们首先通过为`Sprint`添加输入绑定来实现`Sprinting`机制:\n\n1.  导航到编辑器顶部工具栏上的`Edit`选项，从下拉列表中选择`Project Settings`。\n2.  在`Project Settings`内，导航至左侧`Engine`类别下的`Input`选项。默认情况下，虚幻引擎提供的`Side Scroller`模板项目附带了`Jump`的动作映射，键 *W* 、*向上箭头键*、*空格键*、*游戏手柄面板按钮底部*与之绑定。\n3.  Add new `Action Mapping` by *left-clicking* on the `+` button next to `Action Mappings`. Label this mapping `Sprint` and add two keys for its controls; `Left Shift` and `Gamepad Right Shoulder`. Please refer to the figure below for the updated bindings.\n\n    ![Figure 11.28: The Jump and Sprint Action Mappings applied to key bindings ](img/B16183_11_28.jpg)\n\n    图 11.28:应用于键绑定的跳跃和冲刺动作映射\n\n    随着`Sprint`输入绑定的到位，你需要基于`SuperSideScroller`角色类为玩家角色创建一个新的 C++ 类。\n\n4.  返回编辑器，导航至`File`，从下拉列表中选择`New C++ Class`选项。\n5.  The new player character class will inherit from the `SuperSideScrollerCharacter` parent class because this base class has a majority of the functionality needed for the player character. After selecting the parent class, *left-click* on `Next`. Please refer to the following image to see how to find the `SuperSideScrollerCharacter` class.\n\n    ![Figure 11.29: Selecting the SuperSideScrollerCharacter parent class ](img/B16183_11_29.jpg)\n\n    图 11.29:选择 SuperSideScrollerCharacter 父类\n\n6.  Name this new class `SuperSideScroller_Player`. Leave the path as the default that Unreal Engine provides for you, unless you have a need to adjust the file directory of this new class. After naming the new class and selecting the directory to save the class in, *left-click* `Create Class`.\n\n    选择`Create Class`后，虚幻引擎会为你生成源文件和头文件，Visual Studio 会自动打开这些文件。您会注意到头文件和源文件几乎都是空的。这是可以的，因为您是从`SuperSideScrollerCharacter`类继承的，并且您想要的大部分逻辑是在该类中完成的。\n\n7.  In `SuperSideScroller_Player`, you will add only the functionality you need on top of what you inherit. You can view the line where the inheritance is taking place inside `SuperSideScroller_Player.h`:\n\n    ```cpp\n    class SUPERSIDESCROLLER_API ASuperSideScroller_Player : public ASuperSideScrollerCharacter\n    ```\n\n    这个类声明是说新的`ASuperSideScroller_Player`类继承了`ASuperSideScrollerCharacter`类。\n\n通过完成本练习，您可以为`Sprint`机械师添加必要的`Input Binding`，然后可以在 C++ 中引用并用于允许玩家冲刺。现在你已经为玩家角色创建了 C++ 类，你可以用`Sprint`功能更新代码，但是首先你需要更新`Blueprint`角色和动画蓝图来引用这个新类。让我们在下一个练习中这样做。\n\n当你为一个新班级准备蓝图时会发生什么？每个蓝图都继承自父类。在大多数情况下，这是`Actor`，但是在你的角色蓝图的情况下，它的父类是`SuperSideScrollerCharacter`。从父类继承允许蓝图继承该类的功能和变量，以便可以在蓝图级别重用逻辑。\n\n例如，当从`SuperSideScrollerCharacter`类继承时，蓝图继承诸如`CharacterMovement`组件和`Mesh`骨骼网格组件的组件，然后可以在蓝图中修改这些组件。\n\n## 练习 11.05:重绘人物蓝图\n\n现在你已经为玩家角色创建了一个新的角色类，你需要更新`BP_SuperSideScroller_MainCharacter`蓝图，使用`SuperSideScroller_Player`类作为它的父类。如果没有，那么添加到新类中的任何逻辑都不会影响蓝图中制作的角色。\n\n按照以下步骤为新角色类准备蓝图:\n\n1.  导航至`/Game/MainCharacter/Blueprints/`打开`BP_SuperSideScroller_MainCharacter`蓝图。\n2.  选择工具栏上的`File`选项，从下拉菜单中选择`Reparent Blueprint`选项。\n3.  When selecting the `Reparent Blueprint` option, Unreal will ask for the new class to reparent the blueprint to. Search for `SuperSideScroller_Player` and select the option from the dropdown by *left-clicking*.\n\n    一旦您为蓝图选择了新的父类，虚幻将重新加载蓝图并重新编译它，这两种情况都将自动发生。\n\n    注意\n\n    将蓝图重编到新的父类时要小心，因为这可能会导致编译错误或设置被擦除或恢复到类默认值。虚幻引擎将显示在重新分配到新类后编译蓝图后可能出现的任何警告或错误。如果有蓝图逻辑引用新父类中不再存在的变量或其他类成员，通常会出现这些警告和错误。即使没有编译错误，在继续工作之前，最好确认您对蓝图所做的任何逻辑或设置在重新编译之后仍然存在。\n\n    现在你的角色蓝图已经被正确地重新分配到新的`SuperSideScroller_Player`类，你还需要更新`AnimBP_SuperSideScroller_MainCharacter`动画蓝图，以确保你在使用`Try Get Pawn Owner`功能时正在转换到正确的类。\n\n4.  接下来，导航到`/MainCharacter/Blueprints/`目录，打开`AnimBP_SuperSideScroller_MainCharacter`动画蓝图。\n5.  Open `Event Graph`. From the `Return Value` of the `Try Get Pawn Owner` function, search for `Cast` to `SuperSideScroller_Player`:\n\n    ![Figure 11.30: Instead of casting to the base SuperSideScrollerCharacter class, you can cast to the new SuperSideScroller_Player class ](img/B16183_11_30.jpg)\n\n    图 11.30:您可以转换到新的 SuperSideScroller_Player 类，而不是转换到基本的 SuperSideScrollerCharacter 类\n\n6.  You can then connect the output as a `SuperSideScroller_Player` cast to the `MainCharacter` variable. This works because the `MainCharacter` variable is of the `SuperSideScrollerCharacter` type and the new `SuperSideScroller_Player` class inherits from that class:\n\n    ![Figure 11.31: You can still use the MainCharacter variable because SuperSideScroller_Player is based on the SuperSideScrollerCharacter due to inheritance ](img/B16183_11_31.jpg)\n\n图 11.31:您仍然可以使用 MainCharacter 变量，因为由于继承，SuperSideScroller_Player 基于 SuperSideScrollerCharacter\n\n现在`BP_SuperSideScroller_MainCharacter`角色蓝图和`AnimBP_SuperSideScroller_MainCharacter`动画蓝图都引用了你的新`SuperSideScroller_Player`类，现在可以安全地进入 C++ 并编写角色冲刺功能了。\n\n## 练习 11.06:编码角色冲刺功能\n\n随着新的`SuperSideScroller_Player`类引用在之前练习的蓝图中正确实现，是时候开始编写允许玩家角色冲刺的功能了。\n\n执行以下步骤将`Sprinting`机械师添加到角色中:\n\n1.  首先要注意的是`SuperSideScroller_Player`类的构造函数。导航回 Visual Studio，打开`SuperSideScroller_Player.h`头文件。\n2.  在本练习的后面，您将使用`constructor`函数来设置变量的初始值。目前，它将是一个空的构造函数。确保在`public`访问修饰符标题下进行声明，如以下代码所示:\n\n    ```cpp\n    //Constructor\n    ASuperSideScroller_Player();\n    ```\n\n3.  With the constructor declared, create the constructor function definition in the `SuperSideScroller_Player.cpp` source file:\n\n    ```cpp\n    ASuperSideScroller_Player::ASuperSideScroller_Player()\n    {\n    }\n    ```\n\n    有了构造函数，是时候创建`SetupPlayerInputComponent`函数了，这样您就可以使用前面创建的键绑定来调用`SuperSideScroller_Player`类中的函数。\n\n    `SetupPlayerInputComponent`函数是字符类默认内置的函数，所以需要用`override`说明符声明为`virtual`函数。这告诉虚幻，你正在使用这个函数，并打算在这个新类中重新定义它的功能。确保在`Protected`访问修饰符标题下进行声明。\n\n4.  The `SetupPlayerInputComponent` function requires an object of the `UInputComponent` class to be passed into the function, like so:\n\n    ```cpp\n    protected:\n    //Override base character class function to setup our player   input component\n    virtual void SetupPlayerInputComponent(class UInputComponent*   PlayerInputComponent) override;\n    ```\n\n    `UInputComponent* PlayerInputComponent`变量继承自我们的`ASuperSideScroller_Player()`类派生的`UCharacter`基类，因此必须用作`SetupPlayerInputComponent()`函数的输入参数。使用任何其他名称都会导致编译错误。\n\n5.  Now, in the source file, create the definition of the `SetupPlayerInputComponent` function. In the body of the function, we will use the `Super` keyword to call it:\n\n    ```cpp\n    //Not always necessary, but good practice to call the function in   the base class with Super.\n    Super::SetupPlayerInputComponent(PlayerInputComponent);\n    ```\n\n    `Super`关键字使我们能够调用`SetupPlayerInputComponent`父方法。准备好`SetupPlayerInputComponent`功能后，您需要包含以下头文件，以便在没有任何编译错误的情况下继续本练习:\n\n    *   `#include \"Components/InputComponent.h\"`\n    *   `#include \"GameFramework/CharacterMovementComponent.h\"`\n\n        您需要包含输入组件的头，以便将键映射绑定到您接下来将要创建的 sprint 函数。`Character Movement`组件的标题对于冲刺功能是必需的，因为您将根据玩家是否冲刺来更新`Max Walk Speed`参数。以下代码是玩家角色需要包含的所有标题:\n\n        ```cpp\n        #include \"SuperSideScroller_Player.h\"\n        #include \"Components/InputComponent\"\n        #include \"GameFramework/CharacterMovementComponent.h\"\n        ```\n\n        在`SuperSideScroller_Player`类的源文件中包含必要的标题后，你现在可以创建冲刺函数来让玩家角色移动得更快。让我们从声明所需的变量和函数开始。\n\n6.  在`SuperSideScroller_Player`类头文件中的`Private`访问修饰符下，声明一个名为`bIsSprinting`的新布尔变量。该变量将被用作故障保险，以便在对移动速度进行任何更改之前确定玩家角色是否正在冲刺:\n\n    ```cpp\n    private:\n    //Bool to control if we are sprinting. Failsafe.\n    bool bIsSprinting;\n    ```\n\n7.  Next, declare two new functions, `Sprint();` and `StopSprinting();`. These two functions will not take any arguments and will not return anything. Declare the functions under the `Protected` access modifier:\n\n    ```cpp\n    //Sprinting\n    void Sprint();\n    //StopSprinting\n    void StopSprinting();\n    ```\n\n    当玩家*按住*映射到绑定的`Sprint`键时会调用`Sprint();`功能，当玩家*释放*映射到绑定的键时会调用`StopSprinting()`功能。\n\n8.  从`Sprint();`函数的定义开始。在`SuperSideScroller_Player`类的源文件中，创建该函数的定义，如下所示:\n\n    ```cpp\n    void ASuperSideScroller_Player::Sprint()\n    {\n    }\n    ```\n\n9.  在函数中，您首先要检查`bIsSprinting`变量的值。如果玩家是**而不是**冲刺，也就是说`bIsSprinting`是`False`，那就继续剩下的功能。\n10.  Within the `If` statement, set the `bIsSprinting` variable to `True`. Then, you can access the `GetCharacterMovement()` function and modify the `MaxWalkSpeed` parameter. Set `MaxWalkSpeed` to `500.0f`. Remember that the `Maximum Axis Value` parameter of the movement Blend Space is `500.0f`. This means that the player character will reach the speed necessary to use the `Running` animation:\n\n    ```cpp\n    void ASuperSideScroller_Player::Sprint()\n    {\n        if (!bIsSprinting)\n          {\n            bIsSprinting = true;\n            GetCharacterMovement()->MaxWalkSpeed = 500.0f;\n          }\n    }\n    ```\n\n    `StopSprinting()`函数看起来和你刚刚写的`Sprint()`函数几乎一样，但是它的工作方式相反。你首先要检查玩家*是不是*冲刺，意思是`bIsSprinting`是`True`。如果是这样，继续该函数的其余部分。\n\n11.  Inside the `If` statement, set `bIsSprinting` to `False`. Then, access the `GetCharacterMovement()` function to modify `MaxWalkSpeed`. Set `MaxWalkSpeed` back to `300.0f`, the default speed for the player character walking. This means that the player character will reach only the speed necessary for the `Walking` animation:\n\n    ```cpp\n    void ASuperSideScroller_Player::StopSprinting()\n    {\n       if (bIsSprinting)\n        {\n         bIsSprinting = false;\n          GetCharacterMovement()->MaxWalkSpeed = 300.0f;\n        }\n    }\n    ```\n\n    现在您已经有了冲刺所需的函数，是时候将这些函数绑定到您之前创建的动作映射了。为此，在`SetupPlayerInputComponent`功能中执行以下步骤。\n\n12.  Let's begin by binding the `Sprint()` function. Inside the `SetupPlayerInputComponent` function, use the `PlayerInputComponent` variable that is passed to the function so as to call the `BindAction` function.\n\n    `BindAction`需要的参数如下:\n\n    *   您在本练习前面设置的`Project Settings`中所写的动作映射的名称，在本例中为`Sprint`。\n    *   The enumerator value of the `EInputEvent` type that you want to use for this binding; in this case you will use `IE_Pressed` because this binding will be for when the `Sprint` key(s) are pressed.\n\n        注意\n\n        有关`EInputEvent`枚举器类型的更多信息，请参考 Epic Games 的以下文档:[https://docs . unrealengine . com/en-US/API/Runtime/Engine/Engine/EInputEvent/index . html](https://docs.unrealengine.com/en-US/API/Runtime/Engine/Engine/EInputEvent/index.html)。\n\n    *   对类对象的引用。在这种情况下，它是`ASuperSideScroller_Player`类，但是您可以使用`this`关键字，它代表相同的东西。\n    *   A delegate to the function that you want to call when this action happens.\n\n        产生的函数调用如下所示:\n\n        ```cpp\n        //Bind pressed action Sprint to your Sprint function\n        PlayerInputComponent->BindAction\"Sprint\", IE_Pressed, this,   &ASuperSideScroller_Player::Sprint);\n        ```\n\n13.  You will do the same thing for the `StopSprinting()` function, but this time you need to use the `IE_Released` enumerator value, and reference the `StopSprinting` function:\n\n    ```cpp\n    //Bind released action Sprint to your StopSprinting function\n    PlayerInputComponent->BindAction(\"Sprint\", IE_Released, this,   &ASuperSideScroller_Player::StopSprinting);\n    ```\n\n    将`Action Mappings`绑定到 sprint 函数后，您最不需要做的事情就是从`Character Movement`组件设置`bIsSprinting`变量和`MaxWalkSpeed`参数的默认初始值。\n\n14.  在你的`SuperSideScroller_Player`类的源文件中的`constructor`函数中，添加`bIsSprinting = false`行。这个变量被构造为 false，因为默认情况下玩家角色不应该冲刺。\n15.  Finally, set the `MaxWalkSpeed` parameter of the character movement component to `300.0f` by adding the line `GetCharacterMovement()->MaxWalkSpeed = 300.0f`. Please review the following code:\n\n    ```cpp\n    ASuperSideScroller_Player::ASuperSideScroller_Player()\n    {\n      //Set sprinting to false by default.\n       bIsSprinting = false;\n      //Set our max Walk Speed to 300.0f\n       GetCharacterMovement()->MaxWalkSpeed = 300.0f;\n    }\n    ```\n\n    随着变量的初始化被添加到构造函数中，`SuperSideScroller_Player`类就完成了。返回虚幻引擎，左键点击工具栏上`Compile`按钮上的*。这将重新编译代码，并执行编辑器的热重新加载。*\n\n    *在重新编译和热重装编辑器后，您可以在编辑器中播放并看到您的劳动成果。基础移动行为和以前一样，但是现在如果你在控制器上按住*左移*或者*游戏手柄右肩*，玩家角色就会冲刺并开始播放`Running`动画。*\n\n    *![Figure 11.32: The player character can now sprint ](img/B16183_11_32.jpg)*\n\n *图 11.32:玩家角色现在可以冲刺了\n\n随着玩家角色能够冲刺，让我们进入下一个活动，您将以非常相似的方式实现基础`Throw`功能。\n\n## 活动 11.03:实施投掷输入\n\n这款游戏的特点之一是玩家可以向敌人投掷炮弹。在本章中，您不会创建投射体或实现动画，但是您将设置键绑定和 C++ 实现，以便在下一章中使用。\n\n在本练习中，您需要为`Throw`投射功能设置键绑定，并在 C++ 中实现一个调试日志，当玩家按下映射到`Throw`的键时，请执行以下操作。\n\n1.  在输入绑定中向`Project Settings`添加新的`Throw`输入。命名此绑定`ThrowProjectile`并将其绑定到*鼠标左键*和*游戏手柄右触发器*。\n2.  在 Visual Studio 中，为`SuperSideScroller_Player`的头文件添加一个新的函数。命名该功能`ThrowProjectile()`。这将是一个没有参数的空函数。\n3.  Create the definition in the source file of the `SuperSideScroller_Player` class. In the definition of this function, use `UE_LOG` to print a message letting you know that the function is being called successfully.\n\n    注意\n\n    您可以在这里了解更多关于`UE_LOG`的信息:[https://www . ue4 community . wiki/Legacy/Logs，_ Printing _ Messages _ To _ Yourself _ During _ Runtime](https://www.ue4community.wiki/Legacy/Logs,_Printing_Messages_To_Yourself_During_Runtime)。\n\n本活动结束时的预期结果是，当您使用*鼠标左键*或*游戏手柄右触发*时，`Output Log`中会出现一个日志，让您知道`ThrowProjectile`功能被成功调用。稍后你将使用这个功能来产生你的投射物。\n\n预期产出如下:\n\n![Figure 11.33: The expected output log ](img/B16183_11_33.jpg)\n\n图 11.33:预期的输出日志\n\n注意\n\n这个活动的解决方案可以在:[https://packt.live/338jEBx](https://packt.live/338jEBx)找到。\n\n完成此活动后，您现在可以在*第 13 章*、*敌人人工智能*中创建玩家投射物。您现在也有了在游戏中添加新的键映射的知识和经验，并且在 C++ 中实现了利用这些映射来实现游戏功能的功能。现在，您将继续更新玩家角色移动，以允许玩家跳跃时跳跃动画正确播放。\n\n# 动画状态机\n\n现在，让我们了解一下在虚幻引擎 4 和动画中状态机是什么。状态机是一种将动画或动画集分类到其自身状态的方法。状态可以被认为是玩家角色在特定时间所处的状态。玩家目前正在行走吗？玩家在跳吗？在很多第三人称游戏如*《我们最后的人》*中，这是将运动、跳跃、蹲伏和攀爬动画分离成自己的状态。当在玩游戏时满足某些条件时，每个状态都是可访问的。条件可以包括玩家是否在跳跃、玩家角色的速度以及玩家是否处于蹲下状态。状态机的工作是使用称为**转换规则**的逻辑决策在每个状态之间转换。当你用多个相互交织的转换规则创建多个状态时，状态机开始看起来像一个网络。\n\n请看下图，看状态机如何寻找`ThirdPerson_AnimBP`动画蓝图。\n\n注意\n\n状态机的大致概述可以在这里找到:[https://docs . unrealengine . com/en-US/Engine/Animation/state machines/Overview/index . html](https://docs.unrealengine.com/en-US/Engine/Animation/StateMachines/Overview/index.html)\n\n![Figure 11.34: The state machine of ThirdPerson_AnimBP that is included with the SideScroller project template ](img/B16183_11_34.jpg)\n\n图 11.34:SideScroller 项目模板中包含的 ThirdPerson_AnimBP 的状态机\n\n在玩家角色的状态机的情况下，这个状态机将处理默认玩家移动和跳跃的状态。目前，玩家角色只需使用由角色速度控制的混合空间来制作动画。在下一个练习中，您将创建一个新的状态机，并将移动混合空间逻辑移动到该状态机中自己的状态。让我们从创建新的状态机开始。\n\n## 练习 11.07:玩家角色移动和跳跃状态机\n\n在本练习中，您将实现一个新的动画状态机，并将现有的运动混合空间集成到状态机中。此外，您将设置玩家跳跃开始时的状态，以及跳跃过程中玩家在空中时的状态。\n\n让我们从添加这个新的状态机开始:\n\n1.  导航至`/MainCharacter/Blueprints/`目录，打开`AnimBP_SuperSideScroller_MainCharacter`动画蓝图。\n2.  在`AnimGraph`中，*在图的空白处右击*，在上下文相关搜索中搜索`state machine`，找到`Add New State Machine`选项。命名这个新的状态机`Movement`。\n3.  Now, instead of plugging the output pose of the `SideScroller_IdleRun` Blend Space, we can connect the output post of the new state machine, `Movement`, to the output pose of the animation:\n\n    ![Figure 11.35: The new Movement state machine replaces the old Blend Space ](img/B16183_11_35.jpg)\n\n    图 11.35:新的运动状态机取代了旧的混合空间\n\n    将一个空的状态机连接到动画蓝图的`Output Pose`中，将会出现下面显示的警告。所有这一切意味着该状态机内没有任何事情发生，结果对`Output Pose`无效。不用担心；接下来你会解决这个问题。\n\n    ![Figure 11.36: The empty state machine results in compile warnings ](img/B16183_11_36.jpg)\n\n    图 11.36:空状态机导致编译警告\n\n4.  *Double left-click* on the `Movement` state machine to open the state machine itself. The image below shows what this looks like.\n\n    ![Figure 11.37: This is the empty state machine that is created ](img/B16183_11_37.jpg)\n\n    图 11.37:这是创建的空状态机\n\n    您将从添加一个新的状态开始，该状态将处理角色之前所做的事情；`Idle`、`Walking`和`Running`。\n\n5.  From the `Entry` point, *left-click* and drag out to open the context-sensitive search. You will notice that there are only two options – `Add Conduit` and `Add State`. For right now, you will add a new state and name this state `Movement`. Refer to the following images to see the `Movement` state created.\n\n    ![Figure 11.38: Inside the state machine, you need to add a new state that will handle the movement Blend Space you created earlier ](img/B16183_11_38.jpg)\n\n    图 11.38:在状态机内部，您需要添加一个新的状态来处理您之前创建的移动混合空间\n\n    ![Figure 11.39: The new Movement state ](img/B16183_11_39.jpg)\n\n    图 11.39:新的运动状态\n\n6.  Copy and paste the logic you had where you connected the `Speed` variable to the `SideScroller_IdleRun` Blend Space into the new `Movement` state created in the last step. Connect it to the `Result` pin of the `Output Animation Pose` node of this state:\n\n    ![Figure 11.40: Connecting the output pose of the Blend Space  to the output pose of this state ](img/B16183_11_40.jpg)\n\n图 11.40:将混合空间的输出姿态连接到该状态的输出姿态\n\n现在，如果您重新编译动画蓝图，您将首先注意到您之前看到的警告现在已经消失了。这是因为你添加了一个新的状态输出一个动画到`Output Animation Pose`而不是有一个空的状态机。\n\n通过完成这个练习，您已经构建了第一个状态机。虽然很简单，但你现在是在告诉角色默认进入并使用`Movement`状态。如果你现在做 PIE，你会看到玩家角色现在像制造状态机之前一样移动。这意味着您的状态机正在运行，您可以继续下一步，添加跳转所需的初始状态。让我们从创建`JumpStart`状态开始。\n\n## 过渡规则\n\n管道是一种方式，告诉每个状态，在什么条件下，它可以从一个状态过渡到另一个状态。在这种情况下，转换规则被创建为`Movement`和`JumpStart`状态之间的连接。这再次由状态之间的连接的方向箭头指示。工具提示提到了术语*转换规则*，这意味着您需要使用布尔值来定义这些状态之间的转换将如何发生。\n\n![Figure 11.41: There needs to be a transition rule to go from movement  to the start of the character's jump ](img/B16183_11_41.jpg)\n\n图 11.41:从移动到角色跳跃的开始需要有一个过渡规则\n\n## 练习 11.08:向状态机添加状态和转换规则\n\n在从玩家角色的默认移动混合空间过渡到跳跃动画开始的情况下，您需要知道玩家何时决定跳跃。这可以使用玩家角色的`Character Movement`组件中一个叫做`IsFalling`的有用函数来完成。您将需要跟踪玩家当前是否正在下落，以便过渡到跳跃和跳出跳跃。最好的方法是将`IsFalling`函数的结果存储在自己的变量中，就像你跟踪玩家速度时所做的那样。\n\n以下步骤将帮助您完成本练习:\n\n1.  回到状态机本身的概述中，*左键单击*，从`Movement`状态的边缘拖动，再次打开上下文菜单。\n2.  Select the option to `Add State` and name this state `JumpStart`. When you do this, Unreal will automatically connect these states and implement an empty `Transition Rule` for you:\n\n    ![Figure 11.42: The Transition Rule that Unreal automatically creates  for you when connecting two states ](img/B16183_11_42.jpg)\n\n    图 11.42:虚幻在连接两个状态时自动为你创建的转换规则\n\n3.  Navigate back to `Event Graph` inside the animation blueprint, where you had used the Event Blueprint update animation event to store the `Speed` of the player character.\n\n    ![Figure 11.43: EventGraph of the SuperSideScroller player animation blueprint ](img/B16183_11_43.jpg)\n\n    图 11.43:SuperSideScroller 播放器动画蓝图的事件图\n\n4.  Create a getter variable for the `MainCharacter` and access the `Character Movement` component. From the `Character Movement` component, *left-click* and drag to access the context-sensitive menu. Search for `IsFalling`:\n\n    ![Figure 11.44: How to find the IsFalling function ](img/B16183_11_44.jpg)\n\n    图 11.44:如何找到 IsFalling 函数\n\n5.  The character movement component can tell you whether the player character is currently in the air with the help of the `IsFalling` function:\n\n    ![Figure 11.45: The character movement component showing the state of player character ](img/B16183_11_45.jpg)\n\n    图 11.45:显示玩家角色状态的角色移动组件\n\n6.  From the `Return Value` Boolean of the `IsFalling` function, *left-click* and drag to search for the `Promote to Variable` option from the context-sensitive menu. Name this variable `bIsInAir`. When promoting to a variable, the Return Value output pin should automatically connect to the input pin of the newly promoted variable. If it does not, remember to connect them.\n\n    ![Figure 11.46: A new variable, bIsInAir, that contains the value of the IsFalling function ](img/B16183_11_46.jpg)\n\n    图 11.46:一个新的变量，bIsInAir，它包含 IsFalling 函数的值\n\n    现在您正在存储玩家的状态以及他们是否在下降，这是`Movement`和`JumpStart`状态之间的过渡规则的完美候选。\n\n7.  In the `Movement State` machine, *double left-click* on the `Transition Rule` to enter its graph. You will find only one output node, `Result`, with the parameter `Can Enter Transition`. All you need to do here is use the `bIsInAir` variable and connect it to that output. Now, the `Transition Rule` is saying that if the player is in the air, the transition between the `Movement` state and the `JumpStart` states can happen.\n\n    ![Figure 11.47: When in the air, the player will transition to the start of the jumping animation ](img/B16183_11_47.jpg)\n\n    图 11.47:在空中时，玩家将过渡到跳跃动画的开始\n\n    当`Movement`和`JumpStart`状态之间的`Transition Rule`就绪后，剩下要做的就是告诉`JumpStart`状态使用哪个动画。\n\n8.  From the state machine graph, *double left-click* on the `JumpStart` state to enter its graph. From `Asset Browser`, *left-click* and drag the `JumpingStart` animation to the graph:\n\n    ![Figure 11.48: Make sure to have the JumpingStart animation selected in Asset Browser before left-clicking and dragging it into the state ](img/B16183_11_48.jpg)\n\n    图 11.48:在左键单击并将其拖入状态之前，确保在资产浏览器中选择了跳转开始动画\n\n9.  Connect the output of the `Play JumpingStart` node to the `Result` pin of the `Output Animation Pose` node:\n\n    ![Figure 11.49: Connecting the JumpingStart animation to the Output  Animation Pose of the JumpStart state ](img/B16183_11_49.jpg)\n\n    图 11.49:将跳转开始动画连接到跳转开始状态的输出动画姿势\n\n    在可以进入下一个状态之前，在`JumpingStart`动画节点上有需要更改的设置。\n\n10.  *左键点击`Play JumpingStart`动画节点上的*，更新`Details`面板，设置如下:\n    *   `Loop Animation = False`\n    *   `Play Rate = 2.0`\n\n        请参考下图查看`Play JumpingStart`动画节点的最终设置。\n\n        ![Figure 11.50: Due to the slowness of the JumpStart animation, increasing the play rate will result in a smoother jumping animation overall ](img/B16183_11_50.jpg)\n\n图 11.50:由于跳跃开始动画的缓慢，增加播放速率将导致整体上更平滑的跳跃动画\n\n您正在将`Loop Animation`参数设置为`False`，因为没有理由让此动画循环；它在任何情况下都应该只播放一次。这个动画循环的唯一方式是玩家角色以某种方式陷入这种状态，但这永远不会发生，因为你将创建下一个状态。将`Play Rate`设置为`3.0`的原因是动画本身`JumpingStart`对于你正在制作的游戏来说太长了。动画让角色剧烈地弯曲膝盖，并在一秒多的时间里向上跳跃。对于`JumpStart`状态，您希望角色更快地播放此动画，以便它更流畅，并提供到下一状态的更平滑过渡；第`JumpLoop`。\n\n一旦玩家角色开始`JumpStart`动画，在该动画中有一个时间点，玩家在空中，应该转换到一个新的状态。这个新的状态会循环，直到玩家不再在空中，可以过渡到结束跳跃的最终状态。接下来，让我们创建这个新的状态，它将从`JumpStart`状态转换。\n\n1.  From the state machine graph, *left-click* and drag from the `JumpStart` state and select the `Add State` option. Name this new state `JumpLoop`. As before, Unreal will automatically provide you with a `Transition Rule` between these states that you will add to in the next exercise. Finally, recompile the Animation Blueprint and ignore any warnings that may appear under Compiler Results.\n\n    ![Figure 11.51: Creating another state that will handle the animation of the character while in the air after the initial jump start ](img/B16183_11_51.jpg)\n\n图 11.51:创建另一个状态，在初始跳跃开始后，在空中处理角色的动画\n\n通过完成本练习，您已经添加并连接了自己的`JumpStart`和`JumpLoop`状态。这些状态中的每一个都通过一个`Transition Rule`连接起来，现在你可以更好地理解状态机中的状态是如何通过每个转换规则中建立的规则从一个状态转换到另一个状态的。\n\n在下一个练习中，您将更多地了解如何通过函数`Time Remaining Ratio`从`JumpStart`状态转换到`JumpLoop`状态。\n\n## 练习 11.09:剩余时间比率功能\n\n为了让`JumpStart`状态平稳过渡到`JumpLoop`状态，你需要花点时间想清楚你希望这种过渡如何工作。根据`JumpStart`和`JumpLoop`动画的工作方式，最好在`JumpStart`动画上经过指定时间后在`JumpLoop`动画中过渡。这样，`JumpLoop`状态在`JumpStart`动画播放的`X`秒后流畅播放。\n\n为此，请执行以下步骤:\n\n1.  *在`JumpStart`和`JumpLoop`之间的`Transition Rule`上双击*打开其图形。您将在此应用的`Transition Rule`是检查`JumpingStart`动画还剩多少时间。这样做是因为`JumpingStart`动画中还有一定百分比的时间剩余，您可以放心地假设玩家在空中，准备过渡到`JumpingLoop`动画状态。\n2.  To do this, first make sure that the `JumpingStart` animation is selected in `Asset Browser`, and then *right-click* in `Event Graph` of the `Transition Rule` and find the `Time Remaining Ratio` function.\n\n    让我们花点时间来谈谈`Time Remaining Ratio`功能以及它在做什么。这个函数返回一个介于`0.0f`和`1.0f`之间的浮点数，告诉你在指定的动画中还有多少时间。值`0.0f`和`1.0f`可以直接转换为百分比值，以便更容易考虑。在`JumpingStart`动画的情况下，为了成功过渡到`JumpingLoop`状态，您可能想知道是否剩余了不到 60%的动画。这就是你现在要做的。\n\n3.  From the `Return Value` float output parameter of the `Time Remaining Ratio` function, search for the `Less Than comparative operative` node from the context-sensitive search menu. Since you are working with a returned value between `0.0f` and `1.0f`, in order to know whether less than 60% of the animation remains, you need to compare this returned value with a value of `0.6f`. The final result is as follows:\n\n    ![Figure 11.52: You will need to know how much time is left in the JumpingStart animation before transitioning to the JumpLoop animation ](img/B16183_11_52.jpg)\n\n    图 11.52:在转换到跳转循环动画之前，您需要知道跳转开始动画还剩多少时间\n\n    有了这个`Transition Rule`，剩下要做的就是将`JumpLoop`动画添加到`JumpLoop`状态。\n\n4.  In the `Movement` state machine, *double left-click* on the `JumpLoop` state to enter its graph. With the `JumpLoop` animation asset selected in `Asset Browser`, *left-click* and drag it onto the graph. Connect its output to the `Result` input of `Output Animation Pose`, as shown below. The default settings of the `Play JumpLoop` node will remain unchanged.\n\n    ![Figure 11.53: The JumpLoop animation connected to Output  Animation Pose of the new state ](img/B16183_11_53.jpg)\n\n图 11.53:连接到新状态的输出动画姿势的跳跃循环动画\n\n在`JumpLoop`状态下`JumpLoop`动画就位，现在可以编译动画蓝图和 PIE 了。你会注意到运动和冲刺动画仍然存在，但是当你试图跳跃时会发生什么？玩家角色开始`JumpStart`状态，并在空中播放`JumpLoop`动画。这很棒，状态机正在工作，但是当玩家角色到达地面并且不再在空中时会发生什么？玩家角色不会转换回`Movement`状态，这是有道理的，因为你还没有添加`JumpEnd`的状态或者`JumpLoop`和`JumpEnd`之间的转换，以及从`JumpEnd`回到`Movement`状态。您将在下一个活动中这样做。玩家角色陷入`JumpLoop`状态的例子见下文:\n\n![Figure 11.54: The player character can now play the JumpingStart animation and the JumpLoop animation, but cannot transition back to the default movement ](img/B16183_11_54.jpg)\n\n图 11.54:玩家角色现在可以播放跳跃开始动画和跳跃循环动画，但是不能转换回默认移动\n\n通过完成本练习，您可以使用`Time Remaining Ratio`功能成功地从`JumpStart`状态转换到`JumpLoop`状态。这个功能可以让你知道一个动画播放了多长时间，有了这个信息，你的状态机就转换到了`JumpLoop`状态。玩家现在可以成功地从默认的`Movement`状态转换到`JumpStart`状态，然后再转换到`JumpLoop`状态，从而产生一个有趣的问题。玩家现在被卡在`JumpLoop`状态，因为状态机不包含返回`Movement`状态的转换。让我们在下一个活动中解决这个问题。\n\n## 活动 11.04:完成运动和跳跃状态机\n\n随着状态机完成了一半，是时候添加跳转结束时的状态，以及允许您从`JumpLoop`状态转换到这个新状态的转换规则，以及从这个新状态转换回`Movement`状态。\n\n执行以下操作完成`Movement`状态机:\n\n1.  为从`JumpLoop`过渡的`Jump End`添加新状态。命名这个州`JumpEnd`。\n2.  将`JumpEnd`动画添加到新的`JumpEnd`状态。\n3.  根据`JumpEnd`动画以及我们希望在`JumpLoop`、`JumpEnd`和`Movement`状态之间转换的速度，考虑像修改`JumpStart`动画那样修改动画的参数。`loop animation`参数需要设置为`False`，`Play Rate`参数需要设置为`3.0`。\n4.  基于`bIsInAir`变量，添加从`JumpLoop`状态到`JumpEnd`状态的`Transition Rule`。\n5.  基于`JumpEnd`动画的`Time Remaining Ratio`功能，将`JumpEnd`状态添加到`Movement`状态。(看`JumpStart`到`JumpLoop`过渡规则)。\n\n在本活动结束时，您将拥有一个功能齐全的运动状态机，该状态机允许玩家角色空闲、行走和冲刺，并且能够在跳跃开始时、在空中和着陆时正确跳跃和制作动画。\n\n预期产出如下:\n\n![Figure 11.55: Player character with idle, walk, sprint, and jump animation ](img/B16183_11_55.jpg)\n\n图 11.55:具有空闲、行走、冲刺和跳跃动画的玩家角色\n\n注意\n\n这个活动的解决方案可以在:[https://packt.live/338jEBx](https://packt.live/338jEBx)找到。\n\n完成这个活动，你就完成了玩家角色的移动状态机。通过添加剩余的`JumpEnd`状态和`Transition Rules`以从`JumpLoop`状态转换到状态，并从`JumpEnd`状态转换回`Movement`状态，您成功地创建了您的第一个动画状态机。现在，你可以在地图上跑来跑去，跳到高架平台上，同时正确地制作动画，并在移动和跳跃状态之间转换。\n\n# 总结\n\n创建了玩家移动混合空间，并且玩家角色动画蓝图使用状态机从移动过渡到跳跃，您已经准备好进入下一章，您将准备所需的动画片段、动画蒙太奇，并更新仅使用角色上半身的投掷动画的动画蓝图。\n\n从本章的练习和活动中，您学习了如何创建 1D 混合空间，该空间允许使用玩家角色的速度来控制动画的混合，从而平滑混合基于运动的动画，例如空转、行走和跑步。\n\n此外，您还学习了如何将新的键绑定集成到项目设置中，并在 C++ 中绑定这些键，以启用角色游戏机制，如冲刺和投掷。\n\n最后，您学习了如何在角色动画蓝图中实现您自己的动画状态机，以便玩家能够在运动动画之间转换，转换到跳跃的各种状态，然后再次返回运动。有了所有这些逻辑，在下一章中，让我们继续创建允许玩家角色播放投掷动画的资产和逻辑，并为敌人设置基类。**"
  },
  {
    "path": "docs/game-dev-proj-ue/11.md",
    "content": "# 十二、动画混合和蒙太奇\n\n概观\n\n在本章结束时，您将能够使用`Animation Montage`工具，使用您在*第 10 章*、*中导入的`Throw`动画序列创建一个独特的投掷动画，创建一个超视频滚动游戏*。有了这个蒙太奇，您将创建和使用动画插槽，这将允许您在动画蓝图中为玩家角色混合动画。您还将了解如何使用混合节点来有效地混合角色的移动和投掷动画。\n\n在完成玩家角色动画后，你将为敌人 AI 创建所需的职业和资产，并了解更多关于材料和`Material Instances`的信息，这将为这个敌人赋予独特的视觉颜色，以便在游戏中区分。最后，敌人将准备好*第 13 章*、*敌人人工智能*，在这里你将开始创建人工智能行为逻辑。\n\n# 简介\n\n在最后一章中，您可以通过在`Blend Space`中实现运动动画并在动画蓝图中使用`Blend Space`根据玩家的速度驱动动画来使玩家角色栩栩如生。然后，您可以基于玩家的输入在 C++ 中实现功能，以允许角色冲刺。最后，您利用动画状态机内置的动画蓝图来驱动角色的运动状态和跳跃状态，以允许行走和跳跃之间的流畅转换。\n\n随着玩家角色动画蓝图和状态机的工作，是时候通过实现角色的`Throw`动画来引入动画蒙太奇和动画插槽了。在本章中，您将了解更多关于动画混合的信息，了解虚幻引擎如何通过创建一个`Animation Montage`来处理多个动画的混合，并为玩家的投掷动画使用一个新的**动画插槽**。从那里，你将通过实现`Save Cached Pose`和`Layered blend per bone`等新功能来使用玩家动画蓝图中的动画槽，这样玩家就可以将你在上一章中处理的运动动画与你将在本章中实现的新投掷动画正确混合。\n\n让我们从了解什么是动画蒙太奇和动画插槽以及它们如何用于角色动画开始。\n\n# 动画混合、动画片段和动画蒙太奇\n\n动画混合是尽可能无缝地在骨架网格上的多个动画之间转换的过程。你已经熟悉了动画混合的技术，因为你在*第 11 章*、*混合空间 1D、键绑定和状态机*中为玩家角色创建了`Blend Spaces`资源。在这个`Blend Space`中，角色在`Idle`、`Walking`和`Running`动画之间流畅地融合。现在，您将通过探索和实现新的添加技术来扩展这一知识，将角色的运动动画与投掷动画相结合。通过使用`Anim Slot`，你将投掷动画发送到一组上半身骨骼及其儿童骨骼，以允许运动和投掷动画同时应用，而不会对其他骨骼产生负面影响。但是首先，让我们更多地谈谈动画蒙太奇。\n\n动画蒙太奇是一个非常强大的资产，允许您组合多个动画，并将这些组合的动画分割成所谓的**部分**。然后，片段可以按特定顺序单独播放，甚至循环播放。\n\n动画蒙太奇也很有用，因为你可以从蓝图或 C++ 通过蒙太奇控制动画；这意味着你可以根据正在播放的动画片段调用逻辑、更新变量、复制数据等等，或者如果有`Notifies`在蒙太奇内被调用。C++ 中有`UAnimInstance`对象，可以用来调用`UAnimInstance::Montage_Play`之类的函数，可以从 C++ 中访问和播放蒙太奇。\n\n注意\n\n这个方法将在*第 14 章*、*生成玩家抛射物*中使用，当你开始给游戏添加光鲜的时候。更多关于 C++ 中虚幻引擎 4 如何处理动画和`Notifies`的信息，可以在[https://docs . unrealengine . com/en-US/API/Runtime/Engine/Animation/animannotify/UAnimNotifyState/index . html](https://docs.unrealengine.com/en-US/API/Runtime/Engine/Animation/AnimNotifies/UAnimNotifyState/index.html)找到。\n\n在本章的第一个练习中，您将学习更多关于`Notifies`的知识，并且您将在*第 14 章*、*生成玩家投射物*中编码自己的通知状态。\n\n下图显示了动画蒙太奇的`Persona`编辑器。然而，这将在*练习 12.01* 、*设置动画蒙太奇*中进一步分解:\n\n![Figure 12.1: The Persona editor, which opens when editing an Animation Montage ](img/B16183_12_01.jpg)\n\n图 12.1:编辑动画蒙太奇时打开的角色编辑器\n\n就像在动画序列中一样，动画蒙太奇允许沿着动画一部分的时间线触发`Notifies`，然后可以触发声音、粒子效果和事件。`Event` `Notifies`将允许我们从蓝图或 C++ 调用逻辑。史诗游戏在其文档中提供了一个武器重装`Animation Montage`的例子，该武器重装在`reload start`、`reload loop`和`reload complete`的动画之间分割。通过拆分这些动画并将`Notifies`应用于`sounds`和`events`，开发人员可以根据内部变量完全控制`reload loop`将播放多长时间，并控制动画过程中要播放的任何附加声音或效果。\n\n最后，动画蒙太奇支持所谓的**动画槽**。动画片段允许您对动画或一组动画进行分类，这些动画稍后可以在动画蓝图中引用，以允许基于片段的独特混合行为。这意味着您可以定义一个动画槽，以后可以在动画蓝图中使用，以允许使用该槽的动画以任何您想要的方式混合在基础移动动画的顶部；在我们的情况下，只影响玩家角色的上半身而不影响下半身。\n\n让我们从第一个练习中为玩家角色的`Throw`动画创建`Animation Montage`开始。\n\n## 练习 12.01:设置动画蒙太奇\n\n你需要为玩家角色做的最后一件事是设置动画槽，它将把这个动画单独归类为上身动画。您将在动画蓝图中结合混合功能使用这个动画槽，以允许玩家角色投掷投射物，同时在移动和跳跃时仍然正确地为下半身制作动画。\n\n到本练习结束时，玩家角色将只能用上半身玩`Throw`动画，而下半身仍将使用您在上一章中定义的`movement animation`。\n\n让我们从为角色创建`Animation Montage`开始，在那里抛出并设置动画槽:\n\n1.  首先，导航到`/MainCharacter/Animation`目录，这是所有动画资源的位置。\n2.  现在，在内容浏览器中右键单击，并将鼠标悬停在可用下拉菜单中的`Animation`选项上。\n3.  然后，*左键单击*从出现的附加下拉菜单中选择`Animation Montage`选项。\n4.  就像创建其他基于动画的资产一样，例如`Blend Spaces`或`Animation Blueprints`，虚幻引擎会要求您为此`Animation Montage`分配一个`Skeleton`对象。在这种情况下，选择`MainCharacter_Skeleton`。\n5.  Name the new `Animation Montage` `AM_Throw`. Now, *double-left-click* to open the montage:\n\n    ![Figure 12.2: You have successfully created an Animation Montage asset ](img/B16183_12_02.jpg)\n\n图 12.2:您已经成功创建了动画蒙太奇资产\n\n当你打开`Animation Montage`资源时，你会看到一个类似的编辑器布局，就像你打开`Animation Sequence`一样。有一个`Preview`窗口，显示默认 T 姿势的主要角色骨骼，但是一旦你添加动画到这个蒙太奇，骨骼将更新以反映这些变化。\n\n完成本练习后，您已经成功地为`Super SideScroller`项目创建了一个`Animation Montage`资产。现在是时候了解更多关于动画蒙太奇以及如何添加所需的`Throw`动画和动画槽，以便将`Throw`动画与现有的角色移动动画混合。\n\n# 动画剪辑\n\n请看下图:\n\n![Figure 12.3: The animation Preview window in the Animation Montage Persona editor ](img/B16183_12_03.jpg)\n\n图 12.3:动画蒙太奇角色编辑器中的动画预览窗口\n\n在`Preview`窗口下方，除了其他部分，还有主蒙太奇时间线；让我们从上到下评估这些部分:\n\n*   **蒙太奇**:`Montage`部分是动画的集合，可以添加一个或多个动画到。您也可以在时间轴上的任意点上右键单击*来创建**部分**。部分允许您将蒙太奇的不同部分划分为它们自己的独立部分，可以在`Sections`区域进行引用和操作。*\n*   **片段**:如前所述，片段可以让你设置单个动画序列的播放顺序，以及一个片段是否应该循环播放。\n\n出于投掷蒙太奇的目的，您不需要使用此功能，因为您将只在此蒙太奇中使用一个动画:\n\n![Figure 12.4: The Preview window and the Montage and Sections areas ](img/B16183_12_04.jpg)\n\n图 12.4:预览窗口以及蒙太奇和部分区域\n\n*   **元素计时**:`Elemental Timing`部分给你一个蒙太奇的预览和蒙太奇不同方面的顺序。`Notifies`、`Montage`部分的回放顺序和其他元素将在这里直观显示，让您快速预览蒙太奇将如何工作。\n*   **通知** : `Notifies`让你能够在动画时间框架中添加点，然后可以通知其他系统执行动作或从蓝图和 C++ 调用逻辑。通知选项，如`Play Sound`或`Play Particle Effect`，允许您在动画中的特定时间播放声音或粒子。一个例子是在武器的重装动画中；您可以在动画的时间线中添加 notify，以便在重新加载的精确时刻播放重新加载声音。稍后在本项目中，当您实施投掷投射物时，您将使用这些`Notifies`:\n\n![Figure 12.5: The Element Timing and Notifies areas ](img/B16183_12_05.jpg)\n\n图 12.5:元素定时和通知区域\n\n现在您已经熟悉了动画蒙太奇的界面，可以按照下一个练习将`Throw`动画添加到蒙太奇中。\n\n## 练习 12.02:在蒙太奇中添加投掷动画\n\n现在，您已经更好地了解了什么是动画蒙太奇以及这些资源是如何工作的，现在是时候将`Throw`动画添加到您在*练习 12.01* 、*中创建的蒙太奇中设置动画蒙太奇*。虽然您将只向该蒙太奇添加一个动画，但需要强调的是，您可以向蒙太奇添加多个独特的动画，然后可以回放。现在，让我们从添加您在*第 10 章**中导入到项目中的`Throw`动画开始，创建一个超视频滚动游戏*:\n\n在`Asset Browser`中，找到`Throw`动画资产。然后，*左键单击*并将其拖到`Montage`部分下的时间线上:\n\n![Figure 12.6: The Asset Browser window with animation-based assets ](img/B16183_12_06.jpg)\n\n图 12.6:带有基于动画的资产的资产浏览器窗口\n\n一旦动画被添加到动画蒙太奇中，`Preview`窗口中的角色骨架将更新以反映这一变化，并开始播放动画:\n\n![Figure 12.7: The player character begins to animate ](img/B16183_12_07.jpg)\n\n图 12.7:玩家角色开始动画化\n\n现在`Throw`动画已经添加到动画蒙太奇中，您可以继续创建`Anim Slot`。\n\n`Anim Slot Manager`标签应该停靠在右侧的`Asset Browser`标签旁边。如果您没有看到`Anim Slot Manager`选项卡，您可以通过导航到`Animation Montage`编辑器窗口顶部工具栏中的`Window`选项卡来访问它。在那里，*左键点击*选择`Anim Slot Manager`选项，窗口出现。\n\n完成本练习后，您已经将`Throw`动画添加到新的动画蒙太奇中，并且可以通过`Persona`回放动画以预览其在编辑器中的外观。\n\n现在，在本章稍后添加您自己独特的动画插槽用于动画混合之前，您可以继续了解更多关于动画插槽和`Anim Slot Manager`的信息。\n\n# 动漫吃角子老虎管理器\n\n`Anim Slot Manager`顾名思义，就是你管理你的`Anim Slots`的地方。在该选项卡中，您可以创建新的**组**，这允许您更好地组织插槽。例如，您可以通过在`Add Group`选项上左键单击*并将其标记为`Face`来创建一个`Group`，以向其他人阐明该组中的槽会影响角色的面部。默认情况下，虚幻引擎为您提供了一个名为`DefaultGroup`的`Group`和一个名为`DefaultSlot`的`Anim Slot`，它们在该组中。*\n\n *让我们创建一个新的动漫槽。\n\n## 练习 12.03:添加新动漫槽\n\n现在你对 Anim Slot 和`Anim Slot Manager`有了更好的了解，你可以按照这些步骤创建一个新的 Anim Slot，你将称之为`Upper Body`。一旦创建了这个新的槽，就可以在动画蓝图中使用和引用它来处理动画混合，这将在后面的练习中进行。\n\n让我们通过执行以下操作来创建动画插槽:\n\n1.  在`Anim Slot Manager`*中，左键单击`Add Slot`选项上的*。\n2.  When adding a new slot, Unreal will ask you to give this `Anim Slot` a name. Name this slot `Upper Body`. Anim Slot naming is important, much like naming any other assets and parameters, because you will be referencing this slot in the Animation Blueprint later.\n\n    随着动画槽的创建，您现在可以更新用于`Throw`蒙太奇的槽。\n\n3.  In the `Montage` section, there is a drop-down menu that displays the applied `Anim Slot`; by default, it's set to `DefaultGroup.DefaultSlot`. *Left-click*, and from the drop-down menu, select `DefaultGroup.Upper Body`:\n\n    ![Figure 12.8: The new Anim Slot will appear in the drop-down list ](img/B16183_12_08.jpg)\n\n    图 12.8:新的动漫槽将出现在下拉列表中\n\n    注意\n\n    更改`Anim Slot`后，你可能会注意到玩家角色停止动画，回到 T 姿势。别担心——如果发生这种情况，只需关闭`Animation Montage`并重新打开即可。一旦重新打开，角色将再次播放`Throw`动画。\n\n    随着你的`Anim Slot`被创建并在`Throw`蒙太奇中就位，现在是你更新动画蓝图的时候了，这样玩家角色就知道这个位置，并根据它正确地制作动画。\n\n4.  导航至`/MainCharacter/Blueprints/`目录中的`AnimBP_SuperSideScroller_MainCharacter`资产。\n5.  通过*双击*并打开`Anim Graph`打开该资产。\n\n完成本练习后，您已经使用`Anim Slot Manager`创建了您的第一个动画片段，该片段在动画蒙太奇中可用。有了这个槽，现在可以在玩家角色动画蓝图中使用和引用它来处理混合`Throw`动画和您在上一章中实现的运动动画所需的动画混合。在此之前，您需要了解更多关于动画蓝图中`Save Cached Pose`节点的信息。\n\n# 保存缓存的姿势\n\n有些情况下，当处理复杂的动画和角色时，需要引用状态机在多个位置输出的姿势。如果你还没有注意到，你的`Movement`状态机的输出姿态不能连接到一个以上的其他节点。这就是`Save Cached Pose`节点派上用场的地方；它允许您缓存或存储一个姿势，然后可以同时在多个位置引用该姿势。你需要用它来为上半身动画设置新的动画槽。\n\n我们开始吧。\n\n## 练习 12.04:保存运动状态机的缓存姿势\n\n要有效地将`Throw`动画(使用您在上一练习中创建的`Upper Body Anim Slot`)与已经为玩家角色准备好的运动动画混合，您需要能够引用动画蓝图中的`Movement`状态机。为此，请执行以下操作来实现动画蓝图中的`Save Cached Pose`节点:\n\n1.  In `Anim Graph`, *right-click* and search for `New Save Cached Pose`. Name this `Movement Cache`:\n\n    ![Figure 12.9: The Pose will be evaluated once per frame and then cached ](img/B16183_12_09.jpg)\n\n    图 12.9:姿态将每帧评估一次，然后缓存\n\n2.  Now, instead of connecting your `Movement` state machine directly to the output pose, connect it to the cache node:\n\n    ![Figure 12.10: The Movement state machine is being cached ](img/B16183_12_10.jpg)\n\n    图 12.10:正在缓存移动状态机\n\n3.  With the `Movement` state machine pose being cached, all you have to do now is reference it. This can be done by searching for the `Use Cached Pose` node.\n\n    注意\n\n    所有缓存的姿势都将显示在上下文菜单中。只需确保选择缓存的姿势，并使用您在*步骤 1* 中给它的名称。\n\n4.  With the cached pose node available, connect it to `Output Pose` of the `AnimGraph`:\n\n    ![Figure 12.11: This is identical to having the Movement state  machine directly connected to Output Pose ](img/B16183_12_11.jpg)\n\n图 12.11:这等同于将运动状态机直接连接到输出姿态\n\n你现在会注意到，在*第 4 步*之后，主角会正确地动画化，并在最后一章之后按照你的预期移动。这证明`Movement`状态机的缓存工作正常。下图显示了玩家角色在动画蓝图的预览窗口中回到他的`Idle`动画中。\n\n现在您已经使`Movement`状态机的缓存工作，您将使用该缓存基于您创建的`Anim Slot`来混合骨架中的动画:\n\n![Figure 12.12: The main character is animating as expected ](img/B16183_12_12.jpg)\n\n图 12.12:主角正在按预期制作动画\n\n完成本练习后，您现在可以在动画蓝图中的任何位置引用缓存的`Movement`状态机姿势。有了这个辅助工具，你现在可以使用一个叫做`Layered blend per bone`的函数，使用缓存的姿势开始在缓存的运动姿势和`Upper Body`动画槽之间进行混合。\n\n# 每个骨骼的分层混合\n\n您将在这里使用的混合动画的节点是`Layered blend per bone`。该节点屏蔽了角色骨架上的一组骨骼，以便动画忽略这些骨骼。\n\n在我们的玩家角色和`Throw`动画的情况下，你将屏蔽掉下半身，这样只有上半身动画。目标是能够同时执行投掷和移动动画，并使这些动画融合在一起；否则，当您执行投掷时，移动动画将完全中断。\n\n## 练习 12.05:将动画与上半身动画槽混合\n\n`Layered blend per bone`功能允许我们将`Throw`动画与您在上一章中实现的运动动画混合，并让您控制`Throw`动画对玩家角色骨骼的影响程度。\n\n在本练习中，您将使用`Layered blend per bone`功能在播放`Throw`动画时完全遮挡角色的下半身，使其不影响下半身的角色移动动画。\n\n让我们从添加`Layered blend per bone`节点开始，讨论它的输入参数和设置:\n\n1.  Inside the Animation Blueprint, *right-click* and search for `Layered blend per bone` in the `Context Sensitive` search.\n\n    *图 12.13* 显示了`Layered blend per bone`节点及其参数。\n\n    *   第一个参数`Base Pose`是角色的基础姿势；在这种情况下，`Movement`状态机的缓存姿态将是基础姿态。\n    *   第二个参数是你想在`Base Pose`上面叠加的`Blend Pose 0`节点；请记住，选择`Add Pin`将创建附加的`Blend Pose`和`Blend Weights`参数。目前，您将只使用一个`Blend Pose`节点。\n    *   The last parameter is `Blend Weights`, which is how much `Blend Pose` will affect `Base Pose` on a scale from `0.0` to `1.0` as an alpha:\n\n        ![Figure 12.13: The Layered blend per bone node ](img/B16183_12_13.jpg)\n\n图 12.13:每个骨节点的分层混合\n\n在将任何东西连接到此节点之前，您需要向其属性添加一个层。\n\n1.  *Left-click* to select the node and navigate to `Details`. You will need to *left-click* on the arrow next to `Layer Setup` to find the first index, `0`, of this setup. *Left-click* on `+` next to `Branch Filters` to create a new filter.\n\n    这里还有两个参数，即:\n\n    *   `Bone Name`: The bone to specify where the blending will take place and determine the child hierarchy of bones masked out. In the case of the main character skeleton for this project, set `Bone Name` to `Spine`. *Figure 12.14* shows how the `Spine` bone and its children are unassociated with the lower body of the main character. This can be seen in the `Skeleton` asset, `MainCharacter_Skeleton`:\n\n        ![Figure 12.14: The Spine bone and its children are associated  with the upper body of the main character ](img/B16183_12_14.jpg)\n\n图 12.14:脊椎骨骼及其子骨骼与主要角色的上半身相关联\n\n*   `Blend Depth`:骨骼及其子体受动画影响的深度。`0`的值不会影响所选骨骼的根孩子。\n*   `Mesh Space Rotation Blend`: Determines whether or not to blend bone rotations in `mesh space` or in `local space`. `Mesh Space` rotation refers to the skeletal mesh's bounding box as its base rotation, while `Local Space` rotation refers to the local rotation of the bone name in question. In this case, we want the rotation blend to occur in mesh space, so we will set this parameter to true.\n\n    混合将传播到骨骼的所有子骨骼，以停止特定骨骼上的混合，将它们添加到数组中，并使它们的混合深度值`0`。最终结果如下:\n\n    ![Figure 12.15: You can set up multiple layers with one blend node ](img/B16183_12_15.jpg)\n\n图 12.15:您可以用一个混合节点设置多个层\n\n1.  With the settings in place on the `Layered blend per bone` node, you can connect the `Movement Cache` cached pose into the `Base Pose` node of the layered blend. Make sure you connect the output of the `Layered blend per bone` node to `Output Pose` of the Animation Blueprint:\n\n    ![Figure 12.16: Add the cached pose for the Movement state machine  to the Layered blend per bone node ](img/B16183_12_16.jpg)\n\n    图 12.16:将运动状态机的缓存姿势添加到每个骨骼节点的分层混合中\n\n    现在是时候使用您之前创建的动画槽来通过`Layered blend per bone`节点过滤使用该槽的动画了。\n\n2.  *Right-click* in the `AnimGraph` and search for `DefaultSlot`. *Left-click* to select the `Slot` node and navigate to `Details`. There, you will find the `Slot Name` property. *Left-click* on this drop-down to find and select the `DefaultGroup.Upper Body` slot.\n\n    更改`Slot Name`属性时，`Slot`节点将更新以表示该新名称。`Slot`节点需要一个源姿态，它将再次引用`Movement`状态机。这意味着您需要为`Movement Cache`姿势创建另一个`Use Cached Pose`节点。\n\n3.  Connect the cached pose into the source of the `Slot` node:\n\n    ![Figure 12.17: Filtering the cached Movement pose through the Anim Slot ](img/B16183_12_17.jpg)\n\n    图 12.17:通过动画槽过滤缓存的运动姿势\n\n4.  All that is left to do now is connect the `Upper Body` slot node to the `Blend Pose 0` input. Then, connect the final pose of `Layered blend per bone` to the result of the `Output Pose` Animation Blueprint:\n\n    ![Figure 12.18: The final setup of the main character's Animation Blueprint ](img/B16183_12_18.jpg)\n\n图 12.18:主角动画蓝图的最终设置\n\n随着动画槽和`Layered blend per bone`节点在主角的动画蓝图中就位，你终于完成了主角的动画部分。\n\n接下来，让我们简单讨论一下动画混合对于`Throw`动画的重要性以及`Throw`动画将用于什么，然后继续进行*练习 12.06* 、*预览投掷动画*，您将在游戏中预览`Throw`动画。\n\n# 投掷动画\n\n到目前为止，您已经做了大量工作来确保`Throw`动画与您在上一章的动画蓝图中设置的`Movement`动画正确融合。这种努力背后的主要原因是为了在同时执行多个动画时确保角色的视觉保真度。您将直接了解在前面的练习和活动中错误设置动画混合的视觉后果。\n\n回到`Throw`动画，每一款现代电子游戏都以这样或那样的形式实现动画融合，只要艺术方向和游戏机制需要这样的功能。一个特别使用动画的现代游戏系列的例子是由顽皮狗 T4 开发的未知 T2 系列。\n\n如果你不熟悉这个系列，可以在这里观看最新一期的完整玩法:[https://www.youtube.com/watch?v=5evF_funE8A](https://www.youtube.com/watch?v=5evF_funE8A)。\n\n*未知的*系列做得非常好的是使用数千种动画和混合技术，给玩家角色一种不可思议的*真实感*、*重量*和*运动*，当你玩游戏的时候感觉真的很好。虽然`Super SideScroller`项目不会像现在这样完美，但你正在学习制作令人难以置信的视频游戏动画所需的基础知识:\n\n## 练习 12.06:预览投掷动画\n\n在前面的练习中，通过使用`Save Cached Pose`和`Layered blend per bone`节点，您做了大量工作来允许玩家角色的`Movement`动画和`Throw`动画之间的动画混合。执行以下步骤，在游戏中预览`Throw`动画，看看你的劳动成果:\n\n1.  导航到`/MainCharacter/Blueprints/`目录，打开角色的`BP_SuperSideScroller_MainCharacter`蓝图。\n2.  如果你还记得，在最后一章你用`ThrowProjectile`的名字创造了`Input Action`来投掷。\n3.  Inside `Event Graph` of the character's Blueprint, *right-click* and search for `ThrowProjectile` in the `Context Sensitive` drop-down search. Select it with a *left-click* to create the event node in the graph.\n\n    有了这个事件，你需要一个功能，当玩家使用*鼠标左键*投掷时，你可以玩一个`Animation Montage`。\n\n4.  *Right-click* in `Event Graph` and search for `Play Montage`. Make sure not to confuse this with a similar function `Play Anim Montage`.\n\n    `Play Montage`功能需要两个重要的输入:\n\n    *   `Montage to Play`\n    *   `In Skeletal Mesh Component`\n\n        我们先来处理`Skeletal Mesh Component`。\n\n5.  The player character has a `Skeletal Mesh Component` that can be found in the Components tab labeled `Mesh`. Left-click and drag out a `Get` reference to this variable and connect it to the `In Skeletal Mesh Component` input of this function:\n\n    ![Figure 12.19: The mesh of the player character connected  to the In Skeletal Mesh Component input ](img/B16183_12_19.jpg)\n\n    图 12.19:玩家角色的网格连接到骨骼网格组件输入\n\n    现在要做的最后一件事就是告诉这个功能该玩哪个蒙太奇。幸运的是，这个项目中只有一个蒙太奇:`AM_Throw`。\n\n6.  *左键单击`Montage to Play`输入下的下拉菜单中的*，左键单击*选择`AM_Throw`。*\n7.  Finally, connect the `Pressed` execution output of the `ThrowProjectile` event to the execution input pin of the `Play Montage` function:\n\n    ![Figure 12.20: When the player presses the ThrowProjectile input actions, the AM_Throw montage will be played ](img/B16183_12_20.jpg)\n\n    图 12.20:当玩家按下投掷弹输入动作时，会播放 AM_Throw 蒙太奇\n\n8.  现在，当你点击你的*鼠标左键*，玩家角色将进行投掷`Animation Montage`。\n\n现在请注意，您可以如何在投掷的同时行走和奔跑，并且每个动画混合在一起，以免相互干扰:\n\n![Figure 12.21: The player character can now move and throw ](img/B16183_12_21.jpg)\n\n图 12.21:玩家角色现在可以移动和投掷\n\n不用担心使用*鼠标左键*动作反复播放`Throw`蒙太奇时可能会看到的任何 bugs 这些问题将在您实现投射体时得到解决，投射体将在本项目的后面章节中抛出。现在，你只想知道在`Anim Slot`和`Animation Blueprint`上完成的工作给出了动画混合的期望结果。\n\n让我们继续`SuperSideScroller`项目，现在创建 C++ 类、蓝图和设置敌人所需的材料，以便在下一章中使用。\n\n# 超级侧滚游戏敌人\n\n随着玩家角色在移动和执行`Throw`动画时正确地动画化，是时候谈论`SuperSideScroller`游戏将会出现的敌人类型了。我们会有一个简单类型的敌人。\n\n这个敌人会有一个基本的来回移动模式，不会支持任何攻击；只有与玩家角色发生碰撞，它才能造成伤害。\n\n在接下来的练习中，你将在 C++ 中为第一个敌人类型设置基础敌人类，并配置敌人的蓝图和动画蓝图，为*第 13 章*、*敌人人工智能*做准备，在这里你将实现这个敌人的 AI。为了效率和时间，你将使用`SideScroller`模板中虚幻引擎 4 已经为敌人提供的资产。这意味着您将使用默认人体模型资产的骨骼、骨骼网格、动画和动画蓝图。让我们从创建第一个敌人类开始。\n\n## 练习 12.07:创建敌人基础 C++ 类\n\n本练习的目标是从头开始创建一个新的敌人职业，并在开发人工智能时让敌人准备好在*第 13 章*、*敌人人工智能*中使用。首先，按照以下步骤在 C++ 中创建新的敌人类:\n\n1.  在编辑器中，导航至`File`并选择`New C++ Class`开始创建新的敌人职业。\n2.  接下来，在尝试搜索一个类之前，确保选中`Choose Parent Class`窗口提示顶部的`Show All Classes`框。然后，搜索`SuperSideScrollerCharacter`、*左键点击*选择其为父类。\n3.  Lastly, you need to give this class a name and select a directory. Name this class `EnemyBase` and do not change the directory path. When ready, *left-click* on the `Create Class` button to have Unreal Engine create the new class for you.\n\n    当你创建一个新的类时，虚幻引擎会自动为你打开 Visual Studio，准备好`.cpp`和`.h`文件。目前，您不会对代码进行任何更改，因此请关闭 Visual Studio。\n\n    接下来让我们在内容浏览器中为敌人资产创建文件夹结构。\n\n4.  Head back to the Unreal Engine 4 editor, navigate to the content browser, and create a new folder called `Enemy`:\n\n    ![Figure 12.22: New folders are created by right-clicking on existing  folders and selecting New Folder ](img/B16183_12_22.jpg)\n\n    图 12.22:通过右键单击现有文件夹并选择新建文件夹来创建新文件夹\n\n5.  在`Enemy`文件夹中，创建另一个名为`Blueprints`的文件夹，您将在其中为敌人创建并保存蓝图资产。\n6.  In the `/Enemy/Blueprints` directory, *right-click* and select `Blueprint Class`. From `Pick Parent Class`, search for the new C++ class you just made, `EnemyBase`, as shown:\n\n    ![Figure 12.23: Now, the new EnemyBase class is available for you to create a Blueprint from ](img/B16183_12_23.jpg)\n\n    图 12.23:现在，新的 EnemyBase 类可供您创建蓝图\n\n7.  命名这个`BP_Enemy`。\n\n现在你有了第一个使用`EnemyBase`类作为父类的敌人`Blueprint`，是时候处理`Animation Blueprint`了。您将使用`SideScroller`模板项目中虚幻引擎提供给您的默认`Animation Blueprint`。按照下一个练习中的步骤创建现有`Animation Blueprint`的副本，并将其移动到`/Enemy/Blueprints`目录。\n\n## 练习 12.08:创建和应用敌人动画蓝图\n\n在上一个练习中，您使用`EnemyBase`类作为父类为第一个敌人创建了一个`Blueprint`。在本练习中，您将使用动画蓝图。\n\n以下步骤将帮助您完成本练习:\n\n1.  导航至`/Mannequin/Animations`目录，找到`ThirdPerson_AnimBP`资产。\n2.  现在，复制`ThirdPerson_AnimBP`资产。复制资产有两种方法:\n    *   在内容浏览器中选择所需的资产，然后按下 *CTRL* + *W* 。\n    *   *在内容浏览器中右键单击所需资产上的*，并从下拉菜单中选择`Duplicate`。\n3.  现在，*左键点击*，将该重复资产拖动到`/Enemy/Blueprints`目录中，松开*左键点击*选择要移动的选项。\n4.  Name this duplicate asset `AnimBP_Enemy`. It is best to create a duplicate of an asset that you can later modify if you so desire without risking the functionality of the original:\n\n    随着敌人`Blueprint`和`Animation Blueprint`的创建，是时候更新敌人蓝图，使用默认的`Skeletal Mesh`人体模型和新的`Animation Blueprint`副本了。\n\n5.  导航至`/Enemy/Blueprints`并打开`BP_Enemy`。\n6.  Next, navigate to the `Mesh` component and select it to access its `Details` panel. First, assign `SK_Mannequin` to the `Skeletal Mesh` parameter, as shown:\n\n    ![Figure 12.24: You will use the default SK_Mannequin skeletal mesh for the new enemy ](img/B16183_12_24.jpg)\n\n    图 12.24:你将为新敌人使用默认的 SK_Mannequin 骨骼网格\n\n7.  Now you need to apply the `AnimBP_Enemy Animation Blueprint` to the `Mesh` component. Navigate to the `Animation` category of the `Mesh` component's `Details` panel, and under `Anim Class`, assign `AnimBP_Enemy`:\n\n    ![Figure 12.25: Assign the new AnimBP_Enemy Animation Blueprint as the Anim  Class for the enemy character ](img/B16183_12_25.jpg)\n\n    图 12.25:为敌方角色分配新的动画蓝图作为动画类\n\n8.  最后，在`Preview`窗口预览角色时，你会注意到角色网格的位置和旋转不正确。通过将`Mesh`组件的`Transform`属性设置为以下内容来解决此问题:\n    *   `Location` : ( `X` = `0.000000`、`Y` = `0.000000`、`Z` = `-90.000000`)\n    *   `Rotation`:(侧倾= `0.000000`、俯仰= `0`、偏航= `-90.000000`)\n    *   `Scale`: (`X` = `1.000000`, `one` = `1.000000`, `Z` = `1.000000`)\n\n        `Transform`设置将出现如下:\n\n        ![Figure 12.26: These are the transform settings so that your character  is positioned and rotated correctly ](img/B16183_12_26.jpg)\n\n图 12.26:这些是变换设置，以便你的角色被正确定位和旋转\n\n下图显示了到目前为止`Mesh`组件的设置。请确保您的设置与此处显示的匹配:\n\n![Figure 12.27: The settings for the Mesh component of your enemy character ](img/B16183_12_27.jpg)\n\n图 12.27:敌人角色的网格组件的设置\n\n这里要做的最后一件事是创建一个人体模型的主要材料`Material Instance`，这样这个敌人就可以有一个独特的颜色，有助于将其与其他敌人类型区分开来。\n\n让我们首先了解更多关于材料和`Material Instances`的知识。\n\n# 材料和材料实例\n\n在进入下一个练习之前，我们需要先简单讨论一下什么是材料和`Material Instances`，然后你才能使用这些资产并将其应用到新的敌人角色上。虽然这本书更侧重于使用虚幻引擎 4 开发游戏的技术方面，但重要的是你要知道，从表面上看，什么是材料和`Material Instances`以及它们是如何在电子游戏中使用的。\n\n注意\n\n有关材料的更多信息，请参考以下史诗游戏文档:[https://docs . unrealengine . com/en-US/Engine/Rendering/Materials/index . html](https://docs.unrealengine.com/en-US/Engine/Rendering/Materials/index.html)。\n\n材质是一种可以应用于网格的资源，它可以控制网格在游戏中的外观。`Material`编辑器让您可以控制最终视觉结果的许多部分，包括对参数的控制，如`Textures`、`Emissive`和`Specular`等。下图显示了应用了`M_UE4Man_Body``Material`资源的默认人体模型骨骼网格:\n\n![Figure 12.28: The default mannequin skeletal mesh with a basic Material applied ](img/B16183_12_28.jpg)\n\n图 12.28:应用了基本材质的默认人体模型骨骼网格\n\n一个`Material Instance`是一个`Material`的延伸，在这里你不能访问或控制`Material Instance`来源的基地`Material`，但是你可以控制`Material`的创造者向你展示的参数。许多参数可以从`Material Instances`内部暴露给你使用。\n\n虚幻引擎为我们提供了一个名为`M_UE4Man_ChestLogo`的`Side Scroller`模板项目中的`Material Instance`的例子，可以在`/Mannequin/Character/Materials/`目录中找到。下图显示了基于母材质`M_UE4Man_Body`给`Material Instance`的曝光参数集。最重要的参数是`Vector`参数，称为`BodyColor`。您将在下一个练习中创建的`Material Instance`中使用该参数，给敌人一个独特的颜色:\n\n![Figure 12.29: The list of parameters for the M_UE4Man_ChestLogo Material Instance asset ](img/B16183_12_29.jpg)\n\n图 12.29:M _ ue4 man _ ChestLogo 材质实例资产的参数列表\n\n## 练习 12.09:创建和应用敌方材料实例\n\n现在您已经对什么是材质和材质实例有了基本的了解，是时候从`M_UE4ManBody`资产创建自己的`Material Instance`了。有了这个`Material Instance`，你将调整`BodyColor`参数，给敌人角色一个独特的视觉表现。让我们从创造新的`Material Instance`开始。\n\n以下步骤将帮助您完成本练习:\n\n1.  导航到`/Mannequin/Character/Materials`目录，找到默认人体模型角色`M_UE4ManBody`使用的`Material`。\n2.  A `Material Instance` can be created by *right-clicking* on the `Material` asset, `M_UE4Man_Body`, and *left-clicking* on the `Create Material Instance` option. Name this asset `MI_Enemy01`.\n\n    ![Figure 12.30: Any material can be used to create a Material Instance ](img/B16183_12_30.jpg)\n\n    图 12.30:任何材质都可以用来创建材质实例\n\n    在`Enemy`文件夹中创建新文件夹`Materials`。*左键单击*并将`Material Instance`拖动到`/Enemy/Materials`目录，将资产移动到这个新文件夹:\n\n    ![Figure 12.31: Rename the Material Instance MI_Enemy ](img/B16183_12_31.jpg)\n\n    图 12.31:重命名材料实例 MI _ 敌人\n\n3.  *Double-left-click* the `Material Instance` and find the `Details` panel on the left-hand side. There, you will find a `Vector Parameter` property called `BodyColor`. Make sure the checkbox is checked to enable this parameter, and then change its value to a red color. Now, `Material Instance` should be colored red, as shown:\n\n    ![Figure 12.32: Now, the enemy material is red ](img/B16183_12_32.jpg)\n\n    图 12.32:现在，敌方材料为红色\n\n4.  Save the `Material Instance` asset and navigate back to the `BP_Enemy01` Blueprint. Select the `Mesh` component and update the `Element 0` material parameter to `MI_Enemy`:\n\n    ![Figure 12.33: Assigning the new Material Instance asset, MI_Enemy,  to Element 0 of the materials for the Mesh component ](img/B16183_12_33.jpg)\n\n    图 12.33:为网格组件的材料元素 0 分配新的材料实例资产 MI _ 敌人\n\n5.  Now, the first enemy type is visually ready and has the appropriate `Blueprint` and Animation Blueprint assets prepared for the next chapter, where you will develop its AI:\n\n    ![Figure 12.34: The final enemy character set up ](img/B16183_12_34.jpg)\n\n图 12.34:最终的敌人角色设置\n\n完成本练习后，您现在已经创建了一个`Material Instance`，并将其应用于敌方角色，使其具有独特的视觉表现。\n\n让我们通过一个简短的活动来结束这一章，该活动将帮助您更好地理解使用早期练习中使用的`Layered blend per bone`节点的动画混合。\n\n## 活动 12.01:更新混合权重\n\n在*练习 12.06* 、*预览投掷动画*结束时，您可以混合移动动画和`Throw`动画，以便它们可以同时播放，而不会对彼此产生负面影响。结果是玩家角色在行走或跑步时正确地制作动画，同时也在上半身执行`Throw`动画。\n\n在本练习中，您将实验`Layered blend per bone`节点的混合偏置值和参数，以更好地理解动画混合的工作原理。\n\n以下步骤将帮助您完成活动:\n\n1.  Update the `Blend Weights` input parameter of the `Layered blend per bone` node so that there is absolutely no blending of the `Throw` animation additive pose with the base movement pose. Try using values here such as `0.0f` and `0.5f` to compare the differences in the animation.\n\n    注意\n\n    完成后，确保将该值返回到`1.0f`，以免影响您在上一练习中设置的混合。\n\n2.  更新`Layered blend per bone`节点的设置，改变哪个骨骼受融合影响，这样整个角色的身体都受融合影响。从`MainCharacter_Skeleton`资产骨架层次中的根骨开始是个好主意。\n3.  Keeping the settings from the previous step in place, add a new array element to the branch filters and, in this new array element, add the bone name and a blend depth value of `–1.0f`, which allows only the character's left leg to continue to animate the movement correctly when blending the `Throw` animation.\n\n    注意\n\n    完成此活动后，请确保将`Layered blend per bone`节点的设置恢复到您在第一个练习结束时设置的值，以确保角色动画中不会丢失任何进度。\n\n预期产出如下:\n\n![Figure 12.35: Output showing the entire character body affected ](img/B16183_12_35.jpg)\n\n图 12.35:显示整个角色身体受影响的输出\n\n![Figure 12.36: The left leg continues to animate the movement correctly  when blending the Throw animation ](img/B16183_12_36.jpg)\n\n图 12.36:当混合投掷动画时，左腿继续正确地制作运动动画\n\n![Figure 12.37: The character's right leg animating while moving  with the end of the Throw animation ](img/B16183_12_37.jpg)\n\n图 12.37:角色的右腿随着投掷动画的结束而移动\n\n注意\n\n这个活动的解决方案可以在:[https://packt.live/338jEBx](https://packt.live/338jEBx)找到。\n\n在结束本活动之前，请将`Layered blend per bone`设置返回到您在*练习 12.05* 、*将动画与上半身动画槽*混合结束时设置的值。如果您不将这些值恢复到它们的原始设置，在接下来的章节中，动画在即将进行的练习和活动中的效果将会不同。您可以手动设置回原始值，也可以通过以下链接参考具有这些设置的文件:[https://packt.live/2GKGMxM](https://packt.live/2GKGMxM)。\n\n完成本活动后，您现在可以更好地理解动画混合是如何工作的，以及混合权重如何使用`Layered blend per bone`节点影响添加姿势对基本姿势的影响。\n\n注意\n\n在这个项目中有很多你没有使用过的动画混合技术，强烈建议你研究一下这些技术，从[https://docs . unrealengine . com/en-US/Engine/Animation/Animation blending/index . html](https://docs.unrealengine.com/en-US/Engine/Animation/AnimationBlending/index.html)的文档开始。\n\n# 总结\n\n敌人用 C++ 类、蓝图和材料设置好了，你就可以进入下一章了，在这一章中，你将利用虚幻引擎 4 中的行为树等系统为这个敌人创建人工智能。\n\n从本章的练习和活动中，您学习了如何创建允许播放动画的`Animation Montage`。你还学会了如何在这个蒙太奇中设置一个动画槽，为玩家角色的上半身分类。\n\n接下来，您学习了如何使用`Use Cached Pose`节点缓存状态机的输出姿势，以便该姿势可以在多个实例中被引用，用于更复杂的动画蓝图。然后，通过学习`Layered blend per bone`功能，您可以使用动画槽将基本运动姿势与`Throw`动画的附加层混合在一起。\n\n最后，您通过创建 C++ 类、蓝图和其他资产来整合敌人的基础，以便他们为下一章做好准备。敌人准备好了，让我们继续创建敌人的人工智能，这样它就可以与玩家互动。*"
  },
  {
    "path": "docs/game-dev-proj-ue/12.md",
    "content": "# 十三、敌方人工智能\n\n概观\n\n本章首先简要回顾了敌方人工智能在`SuperSideScroller`游戏中的表现。从那里，你将在虚幻引擎 4 的上下文中学习控制器，并学习如何创建人工智能控制器。然后，你将通过在游戏的主关卡中添加一个 Nav Mesh 来了解更多关于虚幻引擎 4 中 AI 导航的知识。\n\n到本章结束时，你将能够创建一个敌人可以移动的可导航空间。你还可以创建一个敌人的人工智能棋子，并使用黑板和行为树在不同的位置导航。最后，你将知道如何创建和实现一个玩家投射类，并添加视觉元素。\n\n# 简介\n\n在上一章中，您使用动画混合为玩家角色添加了分层动画，并结合了动画插槽、动画蓝图和混合功能，如“每骨骼分层混合”。\n\n在这一章中，你将学习如何使用导航网格在游戏世界内部创建一个敌人可以进入的可导航空间。定义一个关卡的可导航空间对于允许**人工智能** ( **AI** )访问并移动到你的关卡的特定区域至关重要。\n\n接下来，你将创建一个敌人的人工智能棋子，它可以使用虚幻引擎 4 中的人工智能工具在游戏世界中的巡逻点位置之间导航，包括*黑板*和*行为树*。\n\n您还将学习如何使用导航网格在游戏世界中创建一个敌人可以移动的可导航空间。定义一个关卡的可导航空间对于允许人工智能访问和移动到你的关卡的特定区域至关重要。\n\n最后，你将学习如何在 C++ 中创建一个玩家投射物类，以及如何实现`OnHit()`碰撞事件函数来识别和记录投射物何时击中游戏世界中的一个对象。除了创建该类，您还将创建该玩家投射类的蓝图，并向玩家投射添加视觉元素，例如静态网格。\n\n`SuperSideScroller`游戏终于要开始了，到本章结束时，你将处于一个很好的位置，可以进入*第 14 章*、*生成玩家抛射物*，在这里你将处理为游戏添加波兰元素，比如 SFX 和 VFX。\n\n本章主要重点是拿你在*第 12 章*、*动画融合和蒙塔格斯*中创建的 C++ 敌人类，用 AI 将这个敌人复活。虚幻引擎 4 使用许多不同的工具来实现人工智能，例如人工智能控制器、黑板和行为树，所有这些您都将在本章中学习和使用。在你进入这些系统之前，让我们花点时间了解一下人工智能在近代史上是如何在游戏中使用的。自从超级马里奥兄弟*时代以来，人工智能已经有了很大的发展。*\n\n *# 敌方 AI\n\n什么是 AI？这个术语可以有很多含义，这取决于它使用的领域和背景，所以让我们用一种对视频游戏主题有意义的方式来定义它。\n\n**AI** 是一个实体，它知道自己的环境，并执行有助于最佳实现其预期目的的选择。人工智能使用所谓的**有限状态机**根据从用户或其环境接收的输入在多个状态之间切换。例如，视频游戏 AI 可以根据其当前的健康状况在进攻状态和防守状态之间切换。\n\n在《虚幻引擎 4》开发的 *Hello Neighbor* 、 *Alien: Isolation* 等游戏中，AI 的目标是尽可能高效地找到玩家，但也要遵循开发者定义的一些预定模式，确保玩家能够智取。*你好邻居*通过让它从玩家过去的行为中学习，并试图基于它所学到的知识智胜玩家，为它的 AI 增加了一个非常有创意的元素。\n\n你可以在这个视频中找到游戏发行商的人工智能工作原理的详细信息，这里:[https://www.youtube.com/watch?v=Hu7Z52RaBGk](https://www.youtube.com/watch?v=Hu7Z52RaBGk)。\n\n有趣有趣的 AI 对任何游戏都至关重要，根据你正在制作的游戏，这可能意味着一个非常复杂或非常简单的 AI。您将为`SuperSideScroller`游戏创建的人工智能不会像前面提到的那样复杂，但它将满足我们正在寻求创建的游戏的需求。\n\n让我们来分析一下敌人的行为:\n\n*   敌人将是一个非常简单的敌人，有一个基本的来回移动模式，不会支持任何攻击；只有与玩家角色发生碰撞，他们才能造成任何伤害。\n*   然而，我们需要为敌人人工智能设置移动的位置。\n*   接下来，我们决定人工智能是否应该改变位置，是否应该不断地在位置之间移动，还是应该在选择一个新的位置移动之间暂停？\n\n对我们来说幸运的是，虚幻引擎 4 为我们提供了广泛的工具，我们可以使用它们来开发如此复杂的人工智能。然而，在我们的项目中，我们将使用这些工具来创建一个简单的敌人类型。让我们从讨论什么是虚幻引擎 4 中的人工智能控制器开始。\n\n# 人工智能控制器\n\n我们来讨论一下**玩家控制器**和 **AI 控制器**的主要区别。这两个角色都来自基础**控制器类**，控制器用来控制**棋子**或**角色**，以控制所述棋子或角色的动作。\n\n虽然玩家控制器依赖于实际玩家的输入，但人工智能控制器将人工智能应用于他们拥有的角色，并根据人工智能设定的规则对环境做出响应。通过这样做，人工智能可以根据玩家和其他外部因素做出智能决策，而无需实际玩家明确告诉它这样做。同一个 AI 棋子的多个实例可以共享同一个 AI 控制器，同一个 AI 控制器可以跨不同的 AI 棋子类使用。AI 和虚幻引擎 4 里面的所有演员一样，都是通过`UWorld`类衍生出来的。\n\n注意\n\n您将在*第 14 章**中了解更多关于`UWorld`类的信息，生成玩家投射物*，但作为参考，请在此处阅读更多信息:[https://docs . unrealengine . com/en-US/API/Runtime/Engine/Engine/UWorld/index . html](https://docs.unrealengine.com/en-US/API/Runtime/Engine/Engine/UWorld/index.html)。\n\n玩家控制器和人工智能控制器最重要的方面是他们将控制的棋子。让我们了解更多关于人工智能控制器如何处理这一点。\n\n## 自动拥有人工智能\n\n像所有控制器一样，人工智能控制器必须拥有一个*棋子*。在 C++ 中，您可以使用以下函数来占有棋子:\n\n```cpp\nvoid AController::Possess(APawn* InPawn)\n```\n\n您也可以使用以下函数取消典当:\n\n```cpp\nvoid AController::UnPossess()\n```\n\n还有`void AController::OnPossess(APawn* InPawn)`和`void AController::OnUnPossess()`函数，分别在调用`Possess()`和`UnPossess()`函数时调用。\n\n说到人工智能，尤其是在虚幻引擎 4 的环境中，有两种方法可以让人工智能控制器拥有人工智能棋子或角色。让我们看看这些选项:\n\n*   `Placed in World`:这个第一种方法就是你在这个项目中会如何处理 AI；你将手动将这些敌方演员放入你的游戏世界，一旦游戏开始，AI 将负责处理剩下的部分。\n*   `Spawned`:第二个方法只是稍微复杂一点，因为它需要一个显式的函数调用，无论是在 C++ 还是蓝图中，来`Spawn`一个指定类的实例。`Spawn Actor`方法需要少数参数，包括`World`对象和`Transform`参数，如`Location`和`Rotation`，以确保所产生的实例被正确产生。\n*   `Placed in World or Spawned`:如果你不确定你想用哪种方法，安全的选择是`Placed in World or Spawned`；这样，这两种方法都受到支持。\n\n出于`SuperSideScroller`游戏的目的，您将使用`Placed In World`选项，因为您将创建的人工智能将被手动放置在游戏级别。\n\n## 练习 13.01:实现人工智能控制器\n\n在敌方棋子可以做任何事情之前，它需要被一个 AI 控制者附身。这也需要在人工智能执行任何逻辑之前发生。本练习将在虚幻引擎 4 编辑器中进行。在本练习结束时，您将创建一个人工智能控制器，并将其应用于您在上一章中创建的敌人。让我们从创建人工智能控制器角色开始。\n\n以下步骤将帮助您完成本练习:\n\n1.  前往`Content Browser`界面，导航至`Content/Enemy`目录。\n2.  *右键单击`Enemy`文件夹中的*，选择`New Folder`选项。命名这个新文件夹`AI`。在新的`AI`文件夹目录中，*右键单击*，选择`Blueprint Class`选项。\n3.  在`Pick Parent Class`对话框中，展开`All Classes`，手动搜索`AIController`类。\n4.  *Left-click* this class option and then *left-click* on the green `Select` option at the bottom to create a new `Blueprint` from this class. Please refer to the following screenshot to know where to find the `AIController` class. Also, take note of the tooltip that appears when hovering over the class option; it contains useful information about this class from the developers:\n\n    ![Figure 13.1: The AIController asset class, as found in the Pick Parent Class dialogue box ](img/B16183_13_01.jpg)\n\n    图 13.1:在“选择父类”对话框中找到的控制者资产类\n\n5.  With this new `AIController Blueprint` created, name this asset `BP_AIControllerEnemy`.\n\n    随着人工智能控制器的创建和命名，是时候将这个资产分配给你在上一章制作的第一个敌人蓝图了。\n\n6.  直接导航到`/Enemy/Blueprints`找到`BP_Enemy`。*双击*打开此蓝图。\n7.  在第一个敌人`Blueprint`的`Details`面板中，有一个标有`Pawn`的部分。在这里，您可以为`Pawn`或`Character`的人工智能功能设置不同的参数。\n8.  `AI Controller Class`参数决定了，顾名思义，该敌人使用哪个 AI 控制器。*左键单击下拉菜单中的*，找到并选择您之前制作的人工智能控制器；也就是`BP_AIController_Enemy`。\n\n完成这个练习后，敌方人工智能现在知道使用哪个人工智能控制器了。这是至关重要的，因为它在人工智能控制器中，人工智能将使用和执行您将在本章稍后创建的行为树。\n\n人工智能控制器现在被分配给敌人，这意味着你几乎准备好开始为这个人工智能开发实际的智能了。在此之前，还有一个重要的话题需要讨论，那就是导航网格。\n\n# 导航网格\n\n任何人工智能最关键的方面之一，尤其是在视频游戏中，是以复杂的方式在环境中导航的能力。在虚幻引擎 4 中，有一种方法可以让引擎告诉人工智能环境的哪些部分是可导航的，哪些部分是不可导航的。这是通过**导航网格**或**导航网格**完成的。\n\n术语“网格”在这里有误导性，因为它是通过编辑器中的一个卷实现的。我们需要一个导航网格，这样我们的人工智能就可以有效地导航游戏世界的可玩范围。我们将在下面的练习中一起添加一个。\n\n虚幻引擎 4 还支持`Dynamic Navigation Mesh`，当动态对象在环境中移动时，它允许导航网格实时更新。这导致人工智能识别环境中的这些变化，并适当地更新它们的路径/导航。本书不涉及此内容，但您可以通过`Project Settings -> Navigation Mesh -> Runtime Generation`访问配置选项。\n\n## 练习 13.02:为人工智能敌人实现导航网格体积\n\n在本练习中，您将向`SideScrollerExampleMap`添加导航网格，并探索导航网格在虚幻引擎 4 中的工作方式。您还将学习如何根据游戏需要参数化该音量。本练习将在虚幻引擎 4 编辑器中进行。\n\n在本练习结束时，您将对导航网格有更深入的了解。在本练习之后的活动中，您还将能够在自己的水平上实现该音量。让我们从增加导航网格体积开始。\n\n以下步骤将帮助您完成本练习:\n\n1.  如果您还没有打开地图，请通过导航到`File`并在`Open Level`选项上左键单击来打开`SideScrollerExampleMap`。在`Open Level`对话框中，导航至`/SideScrollerCPP/Maps`找到`SideScrollerExampleMap`。用*左键点击*选择该地图，然后*左键点击底部的* `Open`打开地图。\n2.  打开地图，导航到右侧找到`Modes`面板。`Modes`面板是一组易于访问的演员类型，如`Volumes`、`Lights`、 `Geometry`等。在`Volumes`类别下，你会发现`Nav Mesh Bounds Volume`选项。\n3.  *Left-click* and drag this volume into the map/scene. By default, you will see the outline of the volume in the editor. Press the `P` key to visualize the `Navigation` area that the volume encompasses, but make sure that the volume is intersecting with the ground geometry in order to see the green visualization, as shown in the following screenshot:\n\n    ![Figure 13.2: Areas outlined in green are perceived as navigable by the engine and the AI ](img/B16183_13_02.jpg)\n\n    图 13.2:引擎和人工智能认为绿色区域是可导航的\n\n    `Nav Mesh`体积就位后，让我们调整它的形状，使体积延伸到水平仪的整个区域。之后，您将学习如何根据游戏目的调整`Nav Mesh`音量的参数。\n\n4.  *Left-click* to select `NavMeshBoundsVolume` and navigate to its `Details` panel. There is a section labeled `Brush Settings` that allows you to adjust the shape and size of the volume. Find the values that fit best for you. Some suggested settings are `Brush Type: Additive`, `Brush Shape: Box`, `X: 3000.0`, `Y: 3000.0`, and `Z: 3000.0`.\n\n    注意当`NavMeshBoundsVolume`的形状和尺寸发生变化时，`Nav Mesh`会调整并重新计算可导航区域。这可以在下面的截图中看到。您还会注意到，上层平台不可导航；稍后您将修复此问题:\n\n    ![Figure 13.3: Now, NavMeshBoundsVolume extends to the entire playable  area of the example map ](img/B16183_13_03.jpg)\n\n图 13.3:现在，NavMeshBoundsVolume 扩展到示例地图的整个可玩区域\n\n通过完成本练习，您已经将第一个`NavMeshBoundsVolume`演员放入游戏世界，并使用调试键`'P'`，在默认地图中可视化了可导航区域。接下来，您将了解更多关于`RecastNavMesh`演员的信息，该演员也是在将`NavMeshBoundsVolume`放入关卡时创建的。\n\n# 重铸导航网格\n\n当你添加`NavMeshBoundsVolume`时，你可能已经注意到另一个演员被自动创建:一个名为`RecastNavMesh-Default`的`RecastNavMesh`演员。这个`RecastNavMesh`充当导航网格的“大脑”，因为它包含调整导航网格所需的参数，这些参数直接影响人工智能如何导航给定的区域。\n\n下面的截图显示了这个资产，从`World Outliner`标签可以看到:\n\n![Figure 13.4: The RecastNavMesh actor, as seen from the World Outliner tab ](img/B16183_13_04.jpg)\n\n图 13.4:从“世界大纲视图”选项卡中看到的 RecastNavMesh 执行元\n\n注意\n\n`RecastNavMesh`中有很多参数存在，我们在这本书中只涵盖重要的参数。更多信息，请查看[。](https://docs.unrealengine.com/en-US/API/Runtime/NavigationSystem/NavMesh/ARecastNavMesh/index.html)\n\n现在只有两个主要部分对您很重要:\n\n1.  `Display`:`Display`部分，顾名思义，只包含影响`NavMeshBoundsVolume`生成的导航区域可视化调试显示的参数。建议您尝试切换此类别下的每个参数，看看它如何影响生成的导航网格的显示。\n2.  `Generation`:`Generation`类别包含一组值，作为导航网格如何生成的规则集，并确定几何图形的哪些区域可导航，哪些区域不可导航。这里有很多选择，这可能会使这个概念非常令人生畏，但是让我们讨论这个类别下的一些参数:\n    *   `Cell Size`指的是 Nav Mesh 能够在一个区域内生成可导航空间的精度。在本练习的下一步中，您将更新该值，因此您将看到这如何实时影响可导航区域。\n    *   `Agent Radius`指将在该区域导航的演员的半径。在你的游戏中，这里设置的半径是半径最大的角色的碰撞部分的半径。\n    *   `Agent Height`指将在该区域导航的演员的高度。在你的游戏中，这里设置的高度是半高最大的角色碰撞部分的半高。可以乘以`2.0f`得到全高。\n    *   `Agent Max Slope`指的是你的游戏世界中可以存在的斜坡的倾斜角度。默认情况下，该值为`44`度，这是一个参数，除非您的游戏要求它发生变化，否则您将保持不变。\n    *   `Agent Max Step Height`指的是台阶的高度，关于楼梯台阶，可以由人工智能导航。很像`Agent Max Slope`，这是一个参数，除非你的游戏特别要求改变这个值，否则你很可能会置之不理。\n\n现在，您已经了解了重铸导航网格参数，让我们在下一个练习中将这些知识付诸实践，我们将指导您更改其中的一些参数。\n\n## 练习 13.03:重新计算导航网格体积参数\n\n现在你在关卡中有了`Nav Mesh`音量，是时候改变`Recast Nav Mesh`演员的参数了，这样 Nav Mesh 就可以让敌方 AI 在比其他人更瘦的平台上导航。本练习将在虚幻引擎 4 编辑器中进行。\n\n以下步骤将帮助您完成本练习:\n\n1.  You will be updating `Cell Size` and `Agent Height` so that they fit the needs of your character and the accuracy needed for the Nav Mesh:\n\n    ```cpp\n    Cell Size: 5.0f\n    Agent Height: 192.0f\n    ```\n\n    下面的截图显示，由于我们对`Cell Size`进行了更改，上层平台现在可以导航了:\n\n    ![Figure 13.5: Changing Cell Size from 19.0f to 5.0f allows for the narrow  upper platforms to be navigable ](img/B16183_13_05.jpg)\n\n图 13.5:将单元格大小从 19.0 更改为 5.0 允许狭窄的上层平台可导航\n\n有了自己的`Nav Mesh`设置的`SuperSideScrollerExampleMap`，你现在可以继续前进，为敌人创建人工智能逻辑。在此之前，完成以下活动来创建您自己的关卡，并使用它自己独特的布局和`NavMeshBoundsVolume`演员，您可以在本项目的剩余部分中使用。\n\n## 活动 13.01:创建新级别\n\n现在您已经将`NavMeshBoundsVolume`添加到示例地图中，是时候创建您自己的地图来完成`Super SideScroller`游戏的其余部分了。通过创建您自己的地图，您将更好地了解`NavMeshBoundsVolume`和`RecastNavMesh`的属性如何影响它们所处的环境。\n\n注意\n\n在继续本活动的解决方案之前，如果您需要一个适用于`SuperSideScroller`游戏剩余章节的示例关卡，那么不要担心——这一章附带了`SuperSideScroller.umap`资源，以及一个名为`SuperSideScroller_NoNavMesh`的地图，其中不包含`NavMeshBoundsVolume`。你可以用`SuperSideScroller.umap`作为如何创造自己水平的参考，或者获得如何提高自己水平的想法。你可以在这里下载地图:[https://packt.live/3lo7v2f](https://packt.live/3lo7v2f)。\n\n执行以下步骤创建简单的地图:\n\n1.  创建一个`New Level`。\n2.  命名这个等级`SuperSideScroller`。\n3.  使用本项目`Content Browser`界面默认提供的静态网格资源，创建一个不同高程的有趣空间进行导航。将你的玩家角色`Blueprint`加到等级，确保被`Player Controller 0`附体。\n4.  将`NavMeshBoundsVolume`演员添加到您的级别，并调整其尺寸，使其适合您创建的空间。在为本活动提供的示例图中，尺寸设置应分别为 *X* 、 *Y* 和 *Z* 轴中的`1000.0`、`5000.0`和`2000.0`。\n5.  确保按下`P`键启用`NavMeshBoundsVolume`的调试可视化。\n6.  Adjust the parameters of the `RecastNavMesh` actor so that `NavMeshBoundsVolume` works well for your level. In the case of the provided example map, the `Cell Size` parameter is set to `5.0f`, `Agent Radius` is set to `42.0f`, and `Agent Height` is set to `192.0f`. Use these values as a reference.\n\n    预期产出:\n\n    ![Figure 13.6: SuperSideScroller map ](img/B16183_13_06.jpg)\n\n图 13.6:超视频滚动地图\n\n在本活动结束时，您将拥有一个包含所需`NavMeshBoundsVolume`和`RecastNavMesh`演员设置的级别。这将允许我们将在即将到来的练习中开发的人工智能正常运行。同样，如果您不确定关卡应该是什么样子，请参考提供的示例地图`SuperSideScroller.umap`。现在，是时候开始为`SuperSideScroller`游戏开发人工智能了。\n\n注意\n\n这个活动的解决方案可以在:[https://packt.live/338jEBx](https://packt.live/338jEBx)找到。\n\n# 行为树和黑板\n\n行为树和黑板一起工作，允许我们的人工智能遵循不同的逻辑路径，并基于各种条件和变量做出决策。\n\n一个**行为树** ( **BT** )是一个可视化脚本工具，允许你根据某些因素和参数告诉一个棋子该做什么。例如，BT 可以根据人工智能是否能看到玩家来告诉人工智能移动到某个位置。\n\n举一个 BTs 和黑板在游戏中如何使用的例子，我们来看看用虚幻引擎 4 开发的游戏《战争 5 的*齿轮》*。《战争齿轮 5》和整个《战争齿轮》系列中的人工智能总是试图包抄玩家，或者迫使玩家脱离掩护。为了做到这一点，人工智能逻辑的一个关键组成部分是知道玩家是谁，以及玩家在哪里。黑板中有一个玩家的参考变量和一个存储玩家位置的位置向量。决定如何使用这些变量以及人工智能将如何使用这些信息的逻辑在行为树中执行。\n\n黑板是您定义一组变量的地方，这些变量是让行为树执行操作并使用这些值进行决策所必需的。\n\n行为树是您创建希望人工智能执行的任务的地方，例如移动到某个位置，或执行您创建的自定义任务。像虚幻引擎 4 中的许多编辑器内工具一样，行为树在很大程度上是一种非常直观的脚本体验。\n\n**黑板**是定义变量的地方，也称为**键**，然后将被**行为树**引用。您在这里创建的键可以在**任务**、**服务**和**装饰器**中使用，根据您希望人工智能如何工作来服务不同的目的。下面的屏幕截图显示了一组变量 Keys 的示例，这些变量可以被其关联的行为树引用。\n\n如果没有黑板，行为树将无法在不同的任务、服务或装饰者之间传递和存储信息，从而使其变得无用:\n\n![Figure 13.7: An example set of variables inside a Blackboard that  can be accessed in the behavior tree ](img/B16183_13_07.jpg)\n\n图 13.7:可以在行为树中访问的 Blackboard 中的一组示例变量\n\n**行为树**由一组**对象**组成，即**复合**、**任务**、**装饰者**和**服务**，它们共同定义人工智能将如何基于您设置的条件和逻辑流进行行为和响应。所有的行为树都从逻辑流开始的根开始；这不能修改，并且只有一个执行分支。让我们更详细地看看这些对象:\n\n## 复合材料\n\n复合节点的功能是告诉行为树如何执行任务和其他操作。下面的截图显示了虚幻引擎默认给你的合成节点的完整列表:选择器、序列和简单并行。\n\n复合节点还可以附加装饰器和服务，以便在执行行为树分支之前应用可选条件:\n\n![Figure 13.8: The full list of Composite nodes – Selector, Sequence, and Simple Parallel ](img/B16183_13_08.jpg)\n\n图 13.8:复合节点的完整列表——选择器、序列和简单并行\n\n*   `Selector`: The Selector composite node executes its children from left to right and will stop executing when one of the children Tasks succeeds. Using the example shown in the following screenshot, if the `FinishWithResult` task is successful, the parent Selector succeeds, which will cause the Root to execute again and `FinishWithResult` to execute once more. This pattern will continue until `FinishWithResult` fails. The Selector will then execute `MakeNoise`. If `MakeNoise` fails, the `Selector` fails, and the Root will execute again. If the `MakeNoise` task succeeds, then the Selector will succeed, and the Root will execute again. Depending on the flow of the behavior tree, if the Selector fails or succeeds, the next composite branch will begin to execute. In the following screenshot, there are no other composite nodes, so if the Selector fails or succeeds, the Root node will be executed again. However, if there were a Sequence composite node with multiple Selector nodes underneath, each Selector would attempt to successfully execute its children. Regardless of success or failure, each Selector will attempt execution sequentially:\n\n    ![Figure 13.9: An example of how a Selector Composite node can be used in a behavior tree ](img/B16183_13_09.jpg)\n\n图 13.9:如何在行为树中使用选择器复合节点的例子\n\n请注意，添加任务和`Composite`节点时，您会注意到每个节点右上角的数值。这些数字表示这些节点的执行顺序。该模式遵循*顶部*到*底部*、*左侧*到*右侧*的范式，这些值有助于您跟踪订单。任何断开的任务或`Composite`节点将被赋予一个值`–1`，以指示它未被使用。\n\n*   `Sequence`: The `Sequence` composite node executes its children from left to right and will stop executing when one of the children Tasks fails. Using the example shown in the following screenshot, if the `Move To` task is successful, then the parent Sequence node will execute the `Wait` task. If the `Wait` task is successful, then the Sequence is successful, and `Root` will execute again. If the `Move To` task fails, however, the Sequence will fail and `Root` will execute again, causing the `Wait` task to never execute:\n\n    ![Figure 13.10: An example of how a Sequence Composite node  can be used in a behavior tree ](img/B16183_13_10.jpg)\n\n图 13.10:如何在行为树中使用序列复合节点的示例\n\n*   `Simple Parallel`: The `Simple Parallel` composite node allows you to execute a `Task` and a new standalone branch of logic simultaneously. The following screenshot shows a very basic example of what this will look like. In this example, a task used to `Wait` for `5` seconds is being executed at the same time as a new `Sequence` of Tasks is being executed:\n\n    ![Figure 13.11: An example of how a Selector Composite node can be used in a behavior tree ](img/B16183_13_11.jpg)\n\n图 13.11:如何在行为树中使用选择器复合节点的例子\n\n`Simple Parallel`复合节点也是其`Details`面板中唯一有参数的`Composite`节点，即`Finish Mode`。有两种选择:\n\n*   `Immediate`:设置为`Immediate`时，一旦主任务完成，简单并行将成功完成。在这种情况下，`Wait`任务完成后，背景树序列将中止，整个`Simple Parallel`将再次执行。\n*   `Delayed`:设置为`Delayed`时，一旦后台树完成执行，任务完成，简单并行将成功完成。在这种情况下，`Wait`任务将在`5`秒后完成，但整个`Simple Parallel`将等待`Move To`和`PlaySound`任务执行后再重新启动。\n\n## 任务\n\n这些是我们的人工智能可以执行的任务。虚幻为我们提供了默认使用的内置任务，但我们也可以在蓝图和 C++ 中创建自己的任务。这包括一些任务，比如告诉我们的人工智能到一个特定的位置，甚至告诉人工智能发射它的武器。了解您可以使用蓝图创建自己的自定义任务也很重要。让我们简单讨论一下你将用来为敌方角色开发人工智能的两个任务:\n\n*   `Move To Task`:这是行为树中比较常用的 Tasks 之一，在本章接下来的练习中，你会用到这个任务。`Move To task`根据给定的位置，使用导航系统告诉人工智能如何移动以及移动到哪里。你会用这个任务告诉 AI 敌人去哪里。\n*   `Wait Task`:这是行为树中另一个常用的任务，因为如果逻辑需要，它允许任务执行之间有延迟。这可以用来让人工智能在移动到新位置之前等待几秒钟。\n\n## 装饰人员\n\n`Decorators`是可以添加到任务或`Composite`节点的条件，例如允许分支逻辑发生的`Sequence`或`Selector`。例如，我们可以有一个`Decorator`来检查敌人是否知道玩家的位置。如果是这样，我们可以告诉敌人向最后一个已知地点移动。如果没有，我们可以告诉我们的人工智能生成一个新的位置，并转而移动到那里。同样重要的是要知道，您可以使用蓝图创建自己的自定义装饰器。\n\n让我们也简单讨论一下你将用来为敌人角色开发人工智能的装饰器——装饰器。这决定了受控棋子是否在装饰器本身中指定的位置。这将有助于您确保行为树不会执行，直到您知道人工智能已经到达其给定的位置。\n\n## 服务\n\n`Services`的工作方式很像 Decorators，因为它们可以与`Tasks`和`Composite`节点链接。主要区别在于`Service`允许我们根据服务中定义的时间间隔执行节点分支。同样重要的是，您可以使用蓝图创建自己的定制服务。\n\n## 练习 13.04:创建人工智能行为树和黑板\n\n现在，您已经对行为树和黑板有了一个概述，本练习将指导您创建这些资产，告诉人工智能控制器使用您创建的行为树，并将黑板分配给行为树。您将在此创建的黑板和行为树资产将用于`SuperSideScroller`游戏。本练习将在虚幻引擎 4 编辑器中进行。\n\n以下步骤将帮助您完成本练习:\n\n1.  在`Content Browser`界面内，导航至`/Enemy/AI`目录。这与您创建人工智能控制器的目录相同。\n2.  在该目录下，*在`Content Browser`界面空白区域内右键*，导航至`Artificial Intelligence`选项，选择`Behavior Tree`创建`Behavior Tree`资产。命名这个资产`BT_EnemyAI`。\n3.  In the same directory as the previous step, *right-click* again within the blank area of the `Content Browser` interface, navigate to the `Artificial Intelligence` option, and select `Blackboard` to create the `Blackboard` asset. Name this asset `BB_EnemyAI`.\n\n    在我们继续告诉人工智能控制器运行这个新的行为树之前，让我们首先将黑板分配给这个行为树，以便它们正确连接。\n\n4.  在`Content Browser`界面双击资产，打开`BT_EnemyAI`。打开后，导航到右侧的`Details`面板，找到`Blackboard Asset`参数。\n5.  *左键单击该参数的下拉菜单*，找到您之前创建的`BB_EnemyAI` `Blackboard`资产。在关闭行为树之前编译并保存它。\n6.  Next, open the AI Controller `BP_AIController_Enemy` asset by *double-clicking* it inside the `Content Browser` interface. Inside the controller, *right-click* and search for the `Run Behavior Tree` function.\n\n    `Run Behavior Tree`函数非常简单:给控制器分配一个`Behavior Tree`，函数返回行为树是否成功开始执行。\n\n7.  Lastly, connect the `Event BeginPlay` event node to the execution pin of the `Run Behavior Tree` function and assign `Behavior Tree asset BT_EnemyAI`, which you created earlier in this exercise:\n\n    。\n\n    ![Figure 13.12: Assigning the BT_EnemyAI behavior tree ](img/B16183_13_12.jpg)\n\n图 13.12:分配 BT_EnemyAI 行为树\n\n完成本练习后，敌方 AI 控制器现在知道运行`BT_EnemyAI`行为树，并且该行为树知道使用名为`BB_EnemyAI`的 Blackboard 资产。有了这些，你可以开始使用行为树逻辑来开发人工智能，这样敌人角色就可以在关卡中移动了。\n\n## 练习 13.05:创建新的行为树任务\n\n本练习的目标是为敌人人工智能开发一个人工智能任务，该任务将允许角色在你的等级中的`Nav Mesh` 体积范围内找到一个随机移动的点。\n\n虽然`SuperSideScroller`游戏只允许二维移动，但还是让 AI 在你在*活动 13.01* 、*创建新关卡*中创建的关卡的 3D 空间内的任何地方移动，然后努力将敌人约束到二维。\n\n按照以下步骤为敌人创建新任务:\n\n1.  首先，打开您在上一个练习中创建的 Blackboard 资产`BB_EnemyAI`。\n2.  *Left-click* on the `New Key` option at the top-left of `Blackboard` and select the `Vector` option. Name this vector `MoveToLocation`. You will use this `vector` variable to track the next move for the AI as it decides where to move to.\n\n    为了这个敌人 AI 的目的，你将需要创建一个新的`Task`，因为虚幻里面当前可用的任务不适合敌人行为的需要。\n\n3.  导航并打开您在上一练习中创建的`Behavior Tree`资产`BT_EnemyAI`。\n4.  *在顶部工具栏的`New Task`选项上左键单击*。新建`Task`时，会自动为您打开任务资产。但是，如果您已经创建了一个任务，当选择`New Task`选项时，将出现一个选项下拉列表。在处理这个`Task`的逻辑之前，您将重命名资产。\n5.  关闭`Task`资产窗口，导航至`/Enemy/AI/`，这是保存`Task`的地方。默认情况下，提供的名称为`BTTask_BlueprintBase_New`。重命名该资产`BTTask_FindLocation`。\n6.  新的`Task`资产命名后，*双击*打开`Task Editor`。新任务的蓝图图将完全为空，并且不会向您提供任何要在图中使用的默认事件。\n7.  *右键单击图中的*，在上下文相关搜索中，找到`Event Receive Execute AI`选项。\n8.  *Left-click* the `Event Receive Execute AI` option to create the event node in the `Task` graph, as shown in the following screenshot:\n\n    ![Figure 13.13: Event Receive Execute AI returns both the Owner  Controller and the Controlled Pawn ](img/B16183_13_13.jpg)\n\n    图 13.13:事件接收执行人工智能返回所有者控制器和受控棋子\n\n    注意\n\n    `Event Receive Execute AI`事件将让您访问**所有者控制者**和**受控棋子**。在接下来的步骤中，您将使用受控棋子来完成此任务。\n\n9.  每个`Task`都需要调用`Finish Execute`函数，以便`Behavior Tree`资产知道何时可以移动到下一个`Task`或从树上分支。*右键单击图中的*，通过上下文相关搜索搜索`Finish Execute`。\n10.  *Left-click* the `Finish Execute` option from the context-sensitive search to create the node inside the Blueprint graph of your `Task`, as shown in the following screenshot:\n\n    ![Figure 13.14: The Finish Execute function, which has a Boolean parameter that determines whether the Task is successful ](img/B16183_13_14.jpg)\n\n    图 13.14:完成执行函数，它有一个布尔参数，决定任务是否成功\n\n    你需要的下一个函数叫做`GetRandomLocationInNavigableRadius`。顾名思义，该函数返回可导航区域定义半径内的随机矢量位置。这将允许敌人角色找到随机位置并移动到这些位置。\n\n11.  *Right-click* in the graph and search for `GetRandomLocationInNavigableRadius` inside the context-sensitive search. *Left-click* the `GetRandomLocationInNavigableRadius` option to place this function inside the graph.\n\n    有了这两个功能，并且`Event Receive Execute AI`准备好了，是时候获取敌方 AI 的随机位置了。\n\n12.  From the `Controlled Pawn` output of `Event Receive Execute AI`, find the `GetActorLocation` function via the context-sensitive search:\n\n    ![Figure 13.15: The enemy pawn's location will serve as the origin  of the random point selection ](img/B16183_13_15.jpg)\n\n    图 13.15:敌人棋子的位置将作为随机点选择的起点\n\n13.  Connect the vector return value from `GetActorLocation` to the `Origin` vector input parameter of the `GetRandomLocationInNavigableRadius` function, as shown in the following screenshot. Now, this function will use the enemy AI pawn's location as the origin for determining the next random point:\n\n    ![Figure 13.16: Now, the enemy pawn location will be used as the origin  of the random point vector search ](img/B16183_13_16.jpg)\n\n    图 13.16:现在，敌方棋子位置将作为随机点矢量搜索的原点\n\n14.  Next, you need to tell the `GetRandomLocationInNavigableRadius` function the `Radius` in which to check for the random point in the navigable area of the level. Set this value to `1000.0f`.\n\n    其余参数`Nav Data`和`Filter Class`可以保持原样。现在您从`GetRandomLocationInNavigableRadius`获得了一个随机位置，您需要能够将该值存储在您在本练习前面创建的`Blackboard`向量中。\n\n15.  要获得对`Blackboard`向量变量的引用，需要在这个`Task`内部创建一个新的变量，它属于`Blackboard Key Selector`类型。创建这个新变量并命名为`NewLocation`。\n16.  现在你需要将这个变量变成一个`Public`变量，这样它就可以暴露在行为树里面了。*左键单击“眼睛”图标上的*，使眼睛可见。\n17.  With the `Blackboard Key Selector` variable ready, *left-click* and drag out a `Getter` of this variable. Then, pull from this variable and search for `Set Blackboard Value as Vector`, as shown in the following screenshot:\n\n    ![Figure 13.17: Set Blackboard Value has a variety of different types to support the different variables that can exist inside the Blackboard ](img/B16183_13_17.jpg)\n\n    图 13.17:设置黑板值有多种不同的类型来支持黑板内部可能存在的不同变量\n\n18.  Connect the `RandomLocation` output vector from `GetRandomLocationInNavigableRadius` to the `Value` vector input parameter of `Set Blackboard Value as Vector`. Then, connect the execution pins of these two function nodes. The result will look as follows:\n\n    ![Figure 13.18: Now, the Blackboard vector value is assigned this new random location ](img/B16183_13_18.jpg)\n\n    图 13.18:现在，黑板向量值被分配给这个新的随机位置\n\n    最后，您将使用`GetRandomLocationInNavigableRadius`函数的`Return Value`布尔输出参数作为确定`Task`是否成功执行的手段。\n\n19.  Connect the Boolean output parameter to the `Success` input parameter of the `Finish Execute` function and connect the execution pins of the `Set Blackboard Value as Vector` and `Finish Execute` function nodes. The following screenshot shows the final result of the `Task` logic:\n\n    ![Figure 13.19: The final setup for the Task ](img/B16183_13_19.jpg)\n\n图 13.19:任务的最终设置\n\n注意\n\n您可以在以下链接找到前面的全分辨率截图，以便更好地查看:[https://packt.live/3lmLyk5](https://packt.live/3lmLyk5)。\n\n通过完成本练习，您已经使用虚幻引擎 4 中的蓝图创建了第一个自定义`Task`。你现在有一个任务，在你等级的`Nav Mesh Volume`的可导航范围内找到一个随机位置，使用敌人棋子作为这次搜索的起点。在下一个练习中，你将在行为树中实现这个新的`Task`，并看到敌人的人工智能在你的等级周围移动。\n\n## 练习 13.06:创建行为树逻辑\n\n本练习的目标是在行为树中实现您在上一练习中创建的新`Task`，以便让敌人 AI 在您所在级别的导航空间内找到一个随机位置，然后移动到该位置。您将使用`Composite`、`Task`和`Services`节点的组合来完成此行为。本练习将在虚幻引擎 4 编辑器中进行。\n\n以下步骤将帮助您完成本练习:\n\n1.  首先，打开你在*练习 13.04* 、*中创建的行为树，创建 AI 行为树和黑板*，也就是`BT_EnemyAI`。\n2.  在此`Behavior Tree`、*中，左键单击*并从`Root`节点的底部拖动，从上下文相关搜索中选择`Sequence`节点。结果将是连接到`Sequence`复合节点的`Root`。\n3.  接下来，从`Sequence`节点，*左键单击*并拖动，调出上下文菜单。在此菜单中，搜索您在上一次创建的`BTTask_FindLocation`。\n4.  By default, the `BTTask_FindLocation` task should automatically assign the `New Location` key selector variable to the `MovetoLocation` vector variable from `Blackboard`. If this doesn't happen, you can assign this selector manually in the `Details` panel of the Task.\n\n    现在，`BTTask_FindLocation`将从`Blackboard`将`NewLocation`选择器分配给`MovetoLocation`矢量变量。这意味着从任务返回的随机位置将被分配给`Blackboard`变量，并且您可以在其他任务中引用该变量。\n\n    现在，您找到了一个有效的随机位置，并将该位置分配给`Blackboard`变量，即`MovetoLocation`，您可以使用`Move To`任务告诉人工智能移动到该位置。\n\n5.  *Left-click* and pull from the `Sequence` composite node. Then, from the context-sensitive search, find the `Move To` task. Your `Behavior Tree` will now look as follows:\n\n    ![Figure 13.20: After selecting the random location, the Move To task  will let the AI move to this new location ](img/B16183_13_20.jpg)\n\n    图 13.20:选择随机位置后，移动到任务会让 AI 移动到这个新位置\n\n6.  By default, the `Move To` task should assign `MoveToLocation` as its `Blackboard Key` value. If it doesn't, select the task. In its `Details` panel, you will find the `Blackboard Key` parameter, which is where you can assign the variable. While in the `Details` panel, also set `Acceptable Radius` to `50.0f`.\n\n    现在，行为树使用`BTTask_FindLocation`自定义任务找到随机位置，并告诉人工智能使用`MoveTo`任务移动到该位置。这两个任务通过引用名为`MovetoLocation`的`Blackboard`向量变量相互传递位置。\n\n    这里要做的最后一件事是在`Sequence`复合节点上添加一个`Decorator`，这样可以确保在再次执行树查找并移动到新位置之前，敌人角色不在随机位置。\n\n7.  *右键单击`Sequence`顶部区域的*，选择`Add Decorator`。从下拉菜单中，左键单击*并选择`Is at Location`。*\n8.  既然`Blackboard`里面已经有了矢量参数，这个`Decorator`应该会自动将`MovetoLocation`指定为`Blackboard Key`。通过选择`Decorator`并确保`Blackboard Key`分配给`MovetoLocation`来验证这一点。\n9.  With the Decorator in place, you have completed the behavior tree. The final result will look as follows:\n\n    ![Figure 13.21: The final setup for the behavior tree for the AI enemy ](img/B16183_13_21.jpg)\n\n    图 13.21:人工智能敌人行为树的最终设置\n\n    这个行为树告诉人工智能使用`BTTask_FindLocation`找到一个随机位置，并将这个位置分配给名为`MovetoLocation`的 Blackboard 值。当这个任务成功时，行为树将执行`MoveTo`任务，这将告诉人工智能移动到这个新的随机位置。序列被包裹在一个`Decorator`中，确保敌人的人工智能在再次执行之前处于`MovetoLocation`，就像人工智能的安全网一样。\n\n10.  在你可以测试新的人工智能行为之前，确保在你的关卡中放置一个`BP_Enemy AI`，如果你之前的练习和活动中还没有的话。\n11.  Now, if you use `PIE`, or `Simulate`, you will see the enemy AI run around the map and move to random locations within `Nav Mesh Volume`:\n\n    ![Figure 13.22: The enemy AI will now move from location to location ](img/B16183_13_22.jpg)\n\n图 13.22:敌人的人工智能现在将从一个位置移动到另一个位置\n\n注意\n\n可以有一些情况是敌方 AI 不会移动。这可能是由于`GetRandomLocationInNavigableRadius`功能没有返回`True`造成的。这是一个已知问题，如果发生，请重新启动编辑器，然后重试。\n\n通过完成这个练习，你已经创建了一个功能齐全的行为树，允许敌人 AI 使用`Nav Mesh Volume`找到并移动到你等级的可导航范围内的一个随机位置。您在上一练习中创建的任务允许您找到这个随机点，而`Move To`任务允许人工智能角色向这个新位置移动。\n\n由于`Sequence`复合节点是如何工作的，每个任务都必须成功完成才能进行下一个任务，所以首先敌人成功找到一个随机位置，然后向这个位置移动。只有当`Move To`任务完成后，整个行为树才会重新开始，选择一个新的随机位置。\n\n现在，你可以继续下一个活动，你将添加到这个行为树，以便让人工智能在选择一个新的随机点之间等待，这样敌人就不会不断移动。\n\n## 活动 13.02: AI 移动到玩家位置\n\n在之前的练习中，通过使用自定义的`Task`和`MoveTo`任务，你可以让人工智能敌人角色移动到`Nav Mesh Volume`范围内的任意位置。\n\n在本练习中，您将继续上一练习并更新行为树。你将通过使用`Decorator`来利用`Wait`任务，并创建自己的新自定义任务，以使人工智能跟随玩家角色并每隔几秒钟更新其位置。\n\n以下步骤将帮助您完成本活动:\n\n1.  在您在上一个练习中创建的`BT_EnemyAI`行为树中，您将从您停止的地方继续并创建一个新的任务。通过从工具栏中选择`New Task`并选择`BTTask_BlueprintBase`来完成。命名这个新任务`BTTask_FindPlayer`。\n2.  在`BTTask_FindPlayer`任务中，创建一个名为`Event Receive Execute AI`的新事件。\n3.  找到`Get Player Character`功能，获取玩家的参考；确保使用`Player Index 0`。\n4.  从玩家角色中，调用`Get Actor Location`功能，以找到玩家的当前位置。\n5.  在此任务中创建一个新的黑板键`Selector`变量。命名这个变量`NewLocation`。\n6.  *左键单击*并将`NewLocation`变量拖动到图形中。从这个变量中，搜索作为`Vector`的`Set Blackboard Value`函数。\n7.  将`Set Blackboard Value`作为`Vector`函数连接到事件的`Receive Execute AI`节点的执行引脚。\n8.  增加`Finish Execute`功能，保证布尔`Success`参数为`True`。\n9.  最后，将`Set Blackboard Value`作为`Vector`功能连接到`Finish Execute`功能。\n10.  保存并编译任务`Blueprint`，返回`BT_EnemyAI`行为树。\n11.  用新的`BTTask_FindPlayer`任务替换`BTTask_FindLocation`任务，这样这个新任务就是`Sequence`复合节点下的第一个任务。\n12.  按照自定义`BTTask_FindLocation`和`Move To`任务，在`Sequence`复合节点下添加一个新的`PlaySound`任务作为第三个任务。\n13.  在`Sound to Play`参数中，添加`Explosion_Cue SoundCue`资产。\n14.  给`PlaySound`任务添加一个`Is At Location`装饰器，并确保`MovetoLocation`键被分配给这个`Decorator`。\n15.  在`PlaySound`任务之后的`Sequence`复合节点下添加一个新的`Wait`任务作为第四个任务。\n16.  Set the `Wait` task to wait `2.0f` seconds before completing successfully.\n\n    预期产出如下:\n\n    ![Figure 13.23: Enemy AI following the player and updating to the player  location every 2 seconds ](img/B16183_13_23.jpg)\n\n图 13.23:敌人 AI 跟随玩家，每 2 秒更新一次玩家位置\n\n敌方 AI 角色将移动到玩家在关卡导航空间中最后一个已知位置，并在每个玩家位置之间暂停`2.0f`秒。\n\n注意\n\n这个活动的解决方案可以在:[https://packt.live/338jEBx](https://packt.live/338jEBx)找到。\n\n完成本活动后，您已经学会创建一个新的任务，该任务允许人工智能找到玩家位置并移动到玩家最后已知的位置。在进入下一组练习之前，移除`PlaySound`任务，并用您在*练习 13.05* 、*创建新行为树任务*中创建的`BTTask_FindLocation`任务替换`BTTask_FindPlayer`任务。请参考*练习 13.05、* *新建行为树任务*、*练习 13.06* 、*创建行为树逻辑*，确保行为树返回正确。在接下来的练习中，您将使用`BTTask_FindLocation`任务。\n\n在下一个练习中，您将通过开发一个新的`Blueprint`参与者来解决这个问题，该参与者将允许您设置人工智能可以走向的特定位置。\n\n## 练习 13.07:创建敌方巡逻地点\n\nAI 敌人角色目前的问题是他们可以在 3D 可导航空间内自由移动，因为行为树允许他们在该空间内找到一个随机位置。相反，人工智能需要被赋予巡逻点，你可以在编辑器中指定和改变。然后它会随机选择其中一个巡逻点进行移动。这是你将为`SuperSideScroller`游戏做的:创建敌人 AI 可以移动到的巡逻点。本练习将向您展示如何使用简单的*蓝图*演员创建这些巡逻点。本练习将在虚幻引擎 4 编辑器中进行。\n\n以下步骤将帮助您完成本练习:\n\n1.  首先，导航到`/Enemy/Blueprints/`目录。这是您将创建新的`Blueprint`演员的地方，该演员将用于人工智能巡逻点。\n2.  在该目录中，*右键单击*，通过*左键单击菜单中的*选择`Blueprint Class`选项。\n3.  From the `Pick Parent Class` menu prompt, *left-click* the `Actor` option to create a new `Blueprint` based on the `Actor` class:\n\n    ![Figure 13.24: The Actor class is the base class for all objects that  can be placed or spawned in the game world ](img/B16183_13_24.jpg)\n\n    图 13.24:Actor 类是游戏世界中所有可以放置或衍生的对象的基类\n\n4.  Name this new asset `BP_AIPoints` and open this `Blueprint` by *double-clicking* the asset in the `Content Browser` interface.\n\n    注意\n\n    `Blueprints`的界面和其他系统有很多相同的特性和布局，比如`Animation Blueprints`和`Tasks`，所以这些你应该都很熟悉。\n\n5.  导航到蓝图界面左侧的`Variables`选项卡，在`+Variable`按钮上左键单击。命名这个变量`Points`。\n6.  从`Variable Type`下拉菜单中，*左键单击*并选择`Vector`选项。\n7.  接下来，你需要将这个向量变量设为`Array`，这样你就可以存储多个巡逻地点。*左键单击`Vector`旁边的黄色图标*，左键单击选择`Array`选项。\n8.  设置`Points`向量变量的最后一步是启用`Instance Editable`和`Show 3D Widget`:\n    *   `Instance Editable`参数允许这个向量变量在被放入一个级别时在参与者上公开可见，允许这个参与者的每个实例都有这个变量可供编辑。\n    *   `Show 3D Widget` allows you to position the vector value by using a visible 3D transform widget in the editor viewport. You will see what this means in the next steps of this exercise. It is also important to note that the `Show 3D Widget` option is only available for variables that involve an actor transform, such as `Vectors` and `Transforms`.\n\n        随着简单的演员设置，是时候将演员放入关卡并开始设置*巡逻点*位置了。\n\n9.  Add the `BP_AIPoints` actor Blueprint into your level, as shown in the following screenshot:\n\n    ![Figure 13.25: The BP_AIPoints actor now in the level ](img/B16183_13_25.jpg)\n\n    图 13.25:BP _ AIPointS 参与者现在处于该级别\n\n10.  选择`BP_AIPoints`演员后，导航至其`Details`面板，找到`Points`变量。\n11.  Next, you can add a new element to the vector array by *left-clicking* on the `+` symbol, as shown here:\n\n    ![Figure 13.26: You can have many elements inside of an array, but the larger the array, the more memory is allocated ](img/B16183_13_26.jpg)\n\n    图 13.26:一个数组中可以有许多元素，但是数组越大，分配的内存就越多\n\n12.  When you add a new element to the vector array, you will see a 3D widget appear that you can then *left-click* to select and move around the level, as shown here:\n\n    ![Figure 13.27: The first Patrol Point vector location ](img/B16183_13_27.jpg)\n\n    图 13.27:第一个巡逻点矢量位置\n\n    注意\n\n    当您更新代表矢量数组元素的 3D 小部件的位置时，3D 坐标将在`Points`变量的`Details`面板中更新。\n\n13.  Finally, add as many elements into the vector array as you would like for the context of your level. Keep in mind that the positions of these patrol points should line up so that they make a straight line along the horizontal axis, parallel to the direction in which the character will move. The following screenshot shows the setup in the example `SideScroller.umap` level included in this exercise:\n\n    ![Figure 13.28: The example Patrol Point path, as seen  in the SideScroller.umap example level ](img/B16183_13_28.jpg)\n\n    图 13.28:巡视点路径的例子，如在 SideScroller.umap 示例级别中看到的\n\n14.  继续重复最后一步，创建多个巡逻点，并根据您认为合适的方式定位 3D 小部件。您可以使用提供的`SideScroller.umap`示例级别作为如何设置这些`Patrol Points`的参考。\n\n完成本练习后，您已经创建了一个新的`Actor`蓝图，其中包含一组`Vector` 位置，现在您可以使用编辑器中的 3D 小部件手动设置这些位置。有了手动设置*巡逻点*位置的能力，你可以完全控制人工智能可以移动到哪里，但是有一个问题。没有从这个数组中选择一个点并将其传递给行为树以便人工智能可以在这些*巡逻点*之间移动的功能。在设置此功能之前，让我们了解更多关于向量和向量变换的知识，因为这些知识将在下一个练习中被证明是有用的。\n\n# 矢量变换\n\n在你进入下一个练习之前，了解向量变换是很重要的，更重要的是，`Transform Location`函数的作用。说到演员的位置，有两种对其位置的思考方式:世界空间和局部空间。演员在世界空间中的位置是其相对于世界本身的位置；更简单地说，这是您将实际演员放入关卡的位置。演员的本地位置是其相对于自身或父演员的位置。\n\n让我们把`BP_AIPoints`演员作为世界空间和局部空间是什么的一个例子。`Points`数组的每个位置都是局部空间向量，因为它们是相对于`BP_AIPoints`角色本身的世界空间位置的位置。下面的截图显示了`Points`数组中的向量列表，如前面的练习所示。这些值是相对于您所在级别中`BP_AIPoints`演员位置的位置:\n\n![Figure 13.29: The local-space position Vectors of the Points array, relative  to the world-space position of the BP_AIPoints actor ](img/B16183_13_29.jpg)\n\n图 13.29:点数组的局部空间位置向量，相对于 BP_AIPoints 参与者的世界空间位置\n\n为了让敌方 AI 移动到这些`Points`的正确世界空间位置，你需要使用一个叫做`Transform Location`的函数。该函数接受两个参数:\n\n*   `T`:这是提供的`Transform`，用于将向量位置参数从局部空间转换为世界空间值。\n*   `Location`:这就是要从局部空间转换到世界空间的`location`。\n\n然后，这个向量转换的结果作为函数的返回值返回。在下一个练习中，您将使用该函数从`Points`数组中返回随机选择的向量点，并将该值从局部空间向量转换为世界空间向量。然后，这个新的世界空间向量将被用来告诉敌人人工智能相对于世界移动到哪里。让我们现在就实现它。\n\n## 练习 13.08:选择数组中的随机点\n\n现在您已经有了更多关于矢量和矢量变换的信息，您可以继续这个练习，在这里您将创建一个简单的`Blueprint`函数来选择一个*巡逻点*矢量位置，并使用一个名为`Transform Location`的内置函数将其矢量从局部空间值变换为世界空间值。通过返回向量位置的世界空间值，您可以将该值传递给*行为树*，以便人工智能移动到正确的位置。本练习将在虚幻引擎 4 编辑器中进行。\n\n以下步骤将帮助您完成本练习。让我们从创建新函数开始:\n\n1.  导航回`BP_AIPoints`蓝图，通过在蓝图编辑器左侧的`Functions`类别旁边左键单击按钮`+`创建新功能。命名该功能`GetNextPoint`。\n2.  在为该功能添加逻辑之前，通过*在`Functions`类别下左键单击*选择该功能，以访问其`Details`面板。\n3.  在`Details`面板中，启用`Pure`参数，使该功能标记为`Pure Function`。在为玩家角色制作动画蓝图时，您在*第 11 章*、*混合空间 1D、键绑定和状态机*中了解了`Pure Functions`；同样的事情正在这里发生。\n4.  Next, the `GetNextPoint` function needs to return a vector that the behavior tree can use to tell the enemy AI where to move to. Add this new output by *left-clicking* on the `+` symbol under the `Outputs` category of the `Details` function. Make the variable of type `Vector` and give it the name `NextPoint`, as shown in the following screenshot:\n\n    ![Figure 13.30: Functions can return multiple variables of different types,  depending on the needs of your logic ](img/B16183_13_30.jpg)\n\n    图 13.30:函数可以返回不同类型的多个变量，这取决于您的逻辑需求\n\n5.  When adding an `Output` variable, the function will automatically generate a `Return` node and place it into the function graph, as shown in the following screenshot. You will use this output to return the new vector patrol point for the enemy AI to move to:\n\n    ![Figure 13.31: The automatically generated Return Node for the function, including the NewPoint vector output variable ](img/B16183_13_31.jpg)\n\n    图 13.31:该函数自动生成的返回节点，包括新点向量输出变量\n\n    现在功能基础已经完成，让我们开始添加逻辑。\n\n6.  In order to pick a random position, first, you need to find the length of the `Points` array. Create a `Getter` of the `Points` vector and from this vector variable, *left-click* and drag to search for the `Length` function, as shown in the following screenshot:\n\n    ![Figure 13.32: The Length function is a pure function that returns the length of the array ](img/B16183_13_32.jpg)\n\n    图 13.32:Length 函数是一个返回数组长度的纯函数\n\n7.  With the integer output of the `Length` function, *left-click* and drag out to use the context-sensitive search to find the `Random Integer` function, as shown in the following screenshot. The `Random Integer` function returns a random integer between `0` and `Max value`; in this case, this is the `Length` of the `Points` vector array:\n\n    ![Figure 13.33: Using Random Integer will allow the function to return  a random vector from the Points vector array ](img/B16183_13_33.jpg)\n\n    图 13.33:使用随机整数将允许函数从点向量数组返回一个随机向量\n\n    到目前为止，您正在生成一个介于`0`和`Points`向量数组的长度之间的随机整数。接下来，您需要在返回的`Random Integer`的索引位置找到`Points`向量数组的元素。\n\n8.  通过创建一个新的`Getter of the Points`向量数组来实现这一点。然后，*左键点击*并拖动搜索`Get (a copy)`功能。\n9.  Next, connect the Return Value of the `Random Integer` function to the input of the `Get (a copy)` function. This will tell the function to choose a random integer and use that integer as the index to return from the `Points` vector array.\n\n    现在您从`Points`向量数组中获得了一个随机向量，您需要使用`Transform Location`函数将位置从局部空间转换为世界空间向量。\n\n    正如您已经了解的那样，`Points`数组中的向量是相对于级别中`BP_AIPoints`参与者位置的局部空间位置。因此，您需要使用`Transform Location`功能将随机选择的局部空间向量转换为世界空间向量，以便 AI 敌人移动到正确的位置。\n\n10.  *左键单击*并从`Get (a copy)`功能的矢量输出中拖动，通过上下文相关搜索，找到`Transform Location`功能。\n11.  将`Get (a copy)`功能的矢量输出连接到`Transform Location`功能的`Location`输入。\n12.  最后一步是使用蓝图执行元本身的变换作为`Transform Location`函数的`T`参数。通过右键单击图中的*并通过上下文相关搜索，找到`GetActorTransform`功能并将其连接到`Transform Location`参数`T`。*\n13.  Finally, connect the `Return Value` vector from the `Transform Location` function and connect it to the `NewPoint` vector output of the function:\n\n    ![Figure 13.34: The final logic set up for the GetNextPoint function ](img/B16183_13_34.jpg)\n\n图 13.34:GetNextPoint 函数的最终逻辑设置\n\n注意\n\n您可以在以下链接找到前面的全分辨率截图，以便更好地查看:[https://packt.live/35jlilb](https://packt.live/35jlilb)。\n\n通过完成本练习，您已经在`BP_AIPoints`参与者中创建了一个新的蓝图函数，该函数从`Points`数组变量中获取随机索引，使用`Transform Location`函数将其转换为世界空间向量值，并返回该新向量值。你将在`BTTask_FindLocation`任务里面，在 AI 行为树里面使用这个功能，这样敌人就会移动到你设置的一个点。在你这样做之前，敌方人工智能需要一个`BP_AIPoints`演员的参考，这样它就知道可以选择和移动到哪些点。我们将在下面的练习中这样做。\n\n## 练习 13.09:参考巡逻点演员\n\n现在`BP_AIPoints`行动者有了一个从其矢量巡逻点数组返回随机变换位置的函数，你需要让敌方 AI 在关卡中引用这个行动者，这样它就知道要引用哪些巡逻点了。要做到这一点，你将添加一个新的`Object Reference`变量到敌人角色蓝图中，并分配你之前在你的关卡中放置的`BP_AIPoints`演员。本练习将在虚幻引擎 4 编辑器中进行。让我们从添加*对象引用*开始。\n\n注意\n\n一个`Object Reference Variable`存储对特定类对象或参与者的引用。有了这个引用变量，您就可以访问这个类公开的变量、事件和函数。\n\n以下步骤将帮助您完成本练习:\n\n1.  导航至`/Enemy/Blueprints/`目录，在`Content Browser`界面双击打开敌方角色蓝图`BP_Enemy`。\n2.  创建一个`BP_AIPoints`类型的新变量，并确保变量类型为`Object Reference`。\n3.  为了引用您的级别中现有的`BP_AIPoints`演员，您需要通过启用`Instance Editable`参数使上一步的变量成为`Public Variable`。命名这个变量`Patrol Points`。\n4.  Now that you have the object reference set, navigate to your level and select your enemy AI. The following screenshot shows the enemy AI placed in the provided example level; that is, `SuperSideScroller.umap`. If you don't have an enemy placed in your level, please do so now:\n\n    注意\n\n    将一个敌人放入一个关卡的工作原理与虚幻引擎 4 中的其他演员相同。*左键点击*，将敌方 AI 蓝图从内容浏览器界面拖拽到关卡中。\n\n    ![Figure 13.35: The enemy AI placed in the example level SuperSideScroller.umap ](img/B16183_13_35.jpg)\n\n    图 13.35:敌人的人工智能被放置在示例级超视频中\n\n5.  从其`Details`面板中，找到`Default` 类别下的`Patrol Points`变量。这里要做的最后一件事是分配我们已经在*练习 13.07* 、*创建敌人巡逻位置*中放置的`BP_AIPoints`演员。通过*左键单击`Patrol Points`变量的下拉菜单*并从列表中找到演员来完成。\n\n这个练习完成后，你所在等级的敌方 AI 现在有了对你所在等级`BP_AIPoints`演员的引用。有了有效的参考，敌人 AI 可以使用这个演员来决定在`BTTask_FindLocation`任务内部移动哪组点。现在剩下要做的就是更新`BTTask_FindLocation`任务，让它使用这些点，而不是找到一个随机的位置。\n\n## 练习 13.10:更新 BTTask_FindLocation\n\n完成敌方 AI 巡逻行为的最后一步是替换`BTTask_FindLocation`内部的逻辑，使其使用`BP_AIPoints`演员的`GetNextPoint`功能，而不是在你所在等级的可导航空间内寻找随机位置。本练习将在虚幻引擎 4 编辑器中进行。\n\n提醒一下，在开始之前，回头看看*练习 13.05* 、*创建新行为树任务*结束时`BTTask_FindLocation`任务是什么样子的。\n\n以下步骤将帮助您完成本练习:\n\n1.  The first thing to do is take the returned `Controlled Pawn` reference from `Event Receive Execute AI` and cast it to `BP_Enemy`, as shown in the following screenshot. This way, you can access the `Patrol Points` object reference variable from the previous exercise:\n\n    ![Figure 13.36: Casting also ensures that the returned Controlled Pawn  is of the BP_Enemy class type ](img/B16183_13_36.jpg)\n\n    图 13.36:施法还能确保返回的受控棋子属于敌人类\n\n2.  接下来，您可以通过*左键单击*并从演员表下的`As BP Enemy`引脚拖动到`BP_Enemy`并通过上下文相关搜索找到`Patrol Points`来访问`Patrol Points`对象引用变量。\n3.  从`Patrol Points`参考中，您可以*左键单击*并拖动以搜索您在*练习 13.08* 、*中创建的`GetNextPoint`功能选择阵列中的随机点*。\n4.  现在，您可以将`GetNextPoint`功能的`NextPoint`矢量输出参数连接到`Set Blackboard Value as Vector`功能，并将 cast 的执行引脚连接到`Set Blackboard Value as Vector`功能。现在，每次执行`BTTask_FindLocation`任务时，都会设置一个新的随机巡逻点。\n5.  最后，将`Set Blackboard Value as Vector`功能连接到`Finish Execute`功能，并将`Success`参数手动设置为`True`，这样，如果演职成功，该任务将始终成功。\n6.  As a failsafe, create a duplicate of `Finish Execute` and connect to the `Cast Failed` execution pin of the `Cast` function. Then, set the `Success` parameter to `False`. This will act as a failsafe so that if, for any reason, `Controlled Pawn` is not of the `BP_Enemy` class, the task will fail. This is a good debugging practice to ensure the functionality of the task for its intended AI class:\n\n    ![Figure 13.37: It is always good practice to account for any casting failures in your logic ](img/B16183_13_37.jpg)\n\n图 13.37:考虑逻辑中的任何转换失败总是一种好的做法\n\n注意\n\n您可以在以下链接找到前面的全分辨率截图，以便更好地查看:[https://packt.live/3n58THA](https://packt.live/3n58THA)。\n\n随着`BTTask_FindLocation`任务更新为在敌人中使用来自`BP_AIPoints`演员参考的随机巡逻点，敌人 AI 现在将在巡逻点之间随机移动。\n\n![Figure 13.38: The enemy AI now moving between the patrol point locations in the level ](img/B16183_13_38.jpg)\n\n图 13.38:敌人人工智能现在在关卡中巡逻点位置之间移动\n\n完成这个练习后，敌人 AI 现在使用对关卡中`BP_AIPoints`演员的引用来找到并移动到关卡中的巡逻点。关卡中敌人角色的每个实例都可以有自己对`BP_AIPoints`角色的另一个唯一实例的引用，或者可以共享同一个实例引用。这取决于你希望每个敌人的人工智能在整个关卡中如何移动。\n\n# 玩家投射物\n\n在本章的最后一节，你将专注于创建玩家投射物的基础，它可以用来消灭敌人。目标是创建适当的演员类，向该类引入所需的碰撞和射弹运动组件，并为射弹的运动行为设置必要的参数。\n\n为了简单起见，玩家射弹不会使用重力，一击就能消灭敌人，射弹本身打到任何表面都会被消灭；例如，它不会从墙上反弹。玩家投射物的主要目标是拥有一个玩家可以在整个关卡中繁殖并用来消灭敌人的投射物。在本章中，您将设置基本的框架功能，而在*第 14 章*、*中，您将添加声音和视觉效果。让我们从创建玩家投射类开始。*\n\n## 练习 13.11:创建玩家投射物\n\n到目前为止，我们一直在虚幻引擎 4 编辑器中努力创建我们的敌人人工智能。对于玩家投射物，我们将使用 C++ 和 Visual Studio 来创建这个新类。玩家投射物将允许玩家摧毁放置在关卡中的敌人。这种射弹寿命短，速度快，会与敌人和环境相撞。\n\n本练习的目标是为玩家投射体设置基本 actor 类，并开始概述投射体头文件中所需的功能和组件。\n\n以下步骤将帮助您完成本练习:\n\n1.  First, you will need to create a new C++ class using the `Actor` class as the parent class for the player projectile. Next, name this new actor class `PlayerProjectile` and *left-click* on the `Create Class` option at the bottom-right of the menu prompt.\n\n    创建新类后，Visual Studio 将为该类生成所需的源文件和头文件，并为您打开这些文件。actor 基类附带了一些默认函数，玩家投射不需要这些函数。\n\n2.  Find the following lines of code inside the `PlayerProjectile.h` file and remove them:\n\n    ```cpp\n    protected:\n      // Called when the game starts or when spawned\n      virtual void BeginPlay() override;\n    public:\n      // Called every frame\n      virtual void Tick(float DeltaTime) override;\n    ```\n\n    这些代码行代表了默认情况下包含在每个基于 Actor 的类中的`Tick()`和`BeginPlay()`函数的声明。`Tick()`功能在每一帧都被调用，并允许你在每一帧上执行逻辑，这可能会变得昂贵，这取决于你正在尝试做什么。当该演员初始化，游戏开始时，调用`BeginPlay()`功能。这可以用来在演员一进入这个世界就对其执行逻辑。这些功能正在被删除，因为它们不是`Player Projectile`所需要的，只会把代码弄得一团糟。\n\n3.  After removing these lines from the `PlayerProjectile.h` header file, you can now remove the following lines from the `PlayerProjectile.cpp` source files as well:\n\n    ```cpp\n    // Called when the game starts or when spawned\n    void APlayerProjectile::BeginPlay()\n    {\n      Super::BeginPlay();\n    }\n    // Called every frame\n    void APlayerProjectile::Tick(float DeltaTime)\n    {\n      Super::Tick(DeltaTime);\n    }\n    ```\n\n    这些代码行代表您在上一步中删除的两个函数的函数实现；即`Tick()`和`BeginPlay()`。同样，这些被删除是因为它们对`Player Projectile`没有任何作用，只是给代码增加了混乱。此外，如果没有`PlayerProjectile.h`头文件中的声明，如果您试图按原样编译这段代码，您将会收到一个编译错误。剩下的唯一函数是投射类的构造函数，您将在下一个练习中使用它来初始化投射的组件。现在您已经从`PlayerProjectile`类中删除了不必要的代码，让我们添加射弹所需的功能和组件。\n\n4.  Inside the `PlayerProjectile.h` header file, add the following components. Let's discuss these components in detail:\n\n    ```cpp\n    public:\n      //Sphere collision component\n      UPROPERTY(VisibleDefaultsOnly, Category = Projectile)\n      class USphereComponent* CollisionComp;\n\n    private:\n      //Projectile movement component\n      UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Movement, meta =   (AllowPrivateAccess = \"true\"))\n      class UProjectileMovementComponent* ProjectileMovement;\n      //Static mesh component\n      UPROPERTY(VisibleDefaultsOnly, Category = Projectile)\n      class UStaticMeshComponent* MeshComp;\n    ```\n\n    您在这里添加了三个不同的组件。第一个是碰撞组件，您将使用它来识别与敌人和环境资产的碰撞。下一个组件是投射物运动组件，您应该从上一个项目中熟悉它。这将允许射弹表现得像射弹。最后的组件是静态网格组件。你将使用这个来给这个投射物一个视觉表示，这样它就可以在游戏中被看到。\n\n5.  Next, add the following function signature code to the `PlayerProjectile.h` header file, under the `public` access modifier:\n\n    ```cpp\n    UFUNCTION()\n    void OnHit(UPrimitiveComponent* HitComp, AActor* OtherActor,   UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult&   Hit);\n    ```\n\n    这个最终的事件声明将允许玩家投射物从您在上一步中创建的`CollisionComp`组件中响应`OnHit`事件。\n\n6.  Now, in order to have this code compile, you will need to implement the function from the previous step in the `PlayerProjectile.cpp` source file. Add the following code:\n\n    ```cpp\n    void APlayerProjectile::OnHit(UPrimitiveComponent* HitComp, AActor*   OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const   FHitResult& Hit)\n    {\n    }\n    ```\n\n    `OnHit`事件为您提供了许多关于发生碰撞的信息。您将在下一个练习中使用的最重要的参数是`OtherActor`参数。`OtherActor`参数将告诉您这个`OnHit`事件响应的演员。这将让你知道另一个演员是否是敌人。当炮弹击中敌人时，你将利用这些信息消灭他们。\n\n7.  最后，导航回虚幻引擎编辑器，左键点击`Compile`选项编译新代码。\n\n完成本练习后，您现在已经为`Player Projectile`课准备好了框架。该类具有`Projectile Movement`、`Collision`和`Static Mesh`所需的组件，以及为`OnHit`碰撞准备的事件签名，以便射弹可以识别与其他演员的碰撞。\n\n在下一个练习中，您将继续为`Player Projectile`定制和启用参数，以便它按照您需要的方式为`SuperSideScroller`项目工作。\n\n## 练习 13.12:初始化玩家投射物设置\n\n现在`PlayerProjectile`类的框架已经就位，是时候用投射体所需的默认设置来更新这个类的构造函数了，这样它就可以按照您想要的方式移动和表现了。为此，您需要初始化`Projectile Movement`、`Collision`和`Static Mesh`组件。\n\n以下步骤将帮助您完成本练习:\n\n1.  打开 Visual Studio，导航至`PlayerProjectile.cpp`源文件。\n2.  Before adding any code to the constructor, include the following files inside the `PlayerProjectile.cpp` source file:\n\n    ```cpp\n    #include \"GameFramework/ProjectileMovementComponent.h\"\n    #include \"Components/SphereComponent.h\"\n    #include \"Components/StaticMeshComponent.h\"\n    ```\n\n    这些头文件将允许您分别初始化和更新射弹运动组件、球体碰撞组件和静态网格组件的参数。如果不包含这些文件，`PlayerProjectile`类就不知道如何处理这些组件以及如何访问它们的函数和参数。\n\n3.  By default, the `APlayerProjectile::APlayerProjectile()` constructor function includes the following line:\n\n    ```cpp\n    PrimaryActorTick.bCanEverTick = true;\n    ```\n\n    这一行代码可以完全删除，因为它不是玩家投射体所必需的。\n\n4.  In the `PlayerProjectile.cpp` source file, add the following lines to the `APlayerProjectile::APlayerProjectile()` constructor:\n\n    ```cpp\n    CollisionComp = CreateDefaultSubobject   <USphereComponent>(TEXT(\"SphereComp\"));\n    CollisionComp->InitSphereRadius(15.0f);\n    CollisionComp->BodyInstance.SetCollisionProfileName(\"BlockAll\");\n    CollisionComp->OnComponentHit.AddDynamic(this, &APlayerProjectile::OnHit);\n    ```\n\n    第一行初始化球体碰撞组件，并将其分配给您在上一个练习中创建的`CollisionComp`变量。`Sphere Collision Component`有一个参数叫做`InitSphereRadius`。默认情况下，这将决定碰撞执行器的大小或半径；在这种情况下，`15.0f`的值很有效。接下来，将碰撞组件的`Collision Profile Name`设置为`BlockAll`，从而将碰撞轮廓设置为`BlockAll`，这意味着该碰撞组件在与其他物体碰撞时将响应`OnHit`。最后，您添加的最后一行允许`OnComponentHit`事件使用您在上一个练习中创建的功能进行响应:\n\n    ```cpp\n    void APlayerProjectile::OnHit(UPrimitiveComponent* HitComp, AActor*   OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const   FHitResult& Hit)\n    {\n    }\n    ```\n\n    这意味着当碰撞组件从碰撞事件中接收到`OnComponentHit`事件时，它将使用该功能进行响应；然而，这个功能目前是空的。在本章的后面，您将向该函数添加代码。\n\n5.  最后一件与`Collision Component`相关的事情是将这个组件设置为玩家投射物演员的`root`组件。在*第 4 步*的代码行之后，在构造函数中添加以下代码行:\n\n    ```cpp\n    // Set as root component\n    RootComponent = CollisionComp;\n    ```\n\n6.  With the collision component set up and ready, let's move on to the `Projectile Movement` component. Add the following lines to the constructor:\n\n    ```cpp\n    // Use a ProjectileMovementComponent to govern this projectile's movement\n    ProjectileMovement =   CreateDefaultSubobject<UProjectileMovementComponent>\n    (TEXT(\"ProjectileComp\"))  ;\n    ProjectileMovement->UpdatedComponent = CollisionComp;\n    ProjectileMovement->ProjectileGravityScale = 0.0f;\n    ProjectileMovement->InitialSpeed = 800.0f;\n    ProjectileMovement->MaxSpeed = 800.0f;\n    ```\n\n    第一行初始化`Projectile Movement Component`，并将其分配给您在上一练习中创建的`ProjectileMovement`变量。接下来，我们将`CollisionComp`设置为射弹运动组件的更新组件。我们这样做的原因是因为`Projectile Movement`组件将使用演员的`root`组件作为要移动的组件。然后，你正在将弹体的重力刻度设置为`0.0f`，因为玩家弹体应该不会受到重力的影响；该行为应该允许射弹以相同的速度、相同的高度行进，并且不受重力的影响。最后，将`InitialSpeed`和`MaxSpeed`参数设置为`500.0f`。这将允许射弹立即以这个速度开始运动，并在其寿命期间保持这个速度。玩家的投射物将不支持任何形式的加速运动。\n\n7.  With the projectile movement component initialized and set up, it is time to do the same for `Static Mesh Component`. Add the following code after the lines from the previous step:\n\n    ```cpp\n    MeshComp = CreateDefaultSubobject<UStaticMeshComponent>(TEXT(\"MeshComp\"));\n    MeshComp->AttachToComponent(RootComponent,   FAttachmentTransformRules::KeepWorldTransform);\n    ```\n\n    第一行初始化`Static Mesh Component`，并将其分配给您在上一练习中创建的`MeshComp`变量。然后，使用名为`FAttachmentTransformRules` 的结构将此静态网格组件附加到`RootComponent`上，以确保`Static Mesh Component`在此练习的*步骤 5* 开始的附加过程中保持其世界变换。\n\n    注意\n\n    你可以在这里找到更多关于`FAttachmentTransformRules`结构的信息。\n\n8.  最后，让我们给`Player Projectile`一个`3`秒的初始寿命，这样在这个时间之后，如果炮弹没有碰撞到任何东西，它就会自动被摧毁。在构造函数的末尾添加以下代码:\n\n    ```cpp\n    InitialLifeSpan = 3.0f;\n    ```\n\n9.  最后，导航回虚幻引擎编辑器，左键点击`Compile`选项编译新代码。\n\n通过完成本练习，您已经为`Player Projectile`奠定了基础，这样就可以在编辑器中将其创建为*蓝图*演员。所有三个必需的组件都已初始化，并包含您想要的该射弹的默认参数。我们现在需要做的就是从这个类创建*蓝图*来查看它的等级。\n\n## 活动 13.03:创建玩家射弹蓝图\n\n为了结束本章，您将从新的`PlayerProjectile`类中创建`Blueprint`执行元，并自定义该执行元，以便它使用`Static Mesh Component`的占位符形状进行调试。这可以让你在游戏世界中看到投射物。然后，您将在`PlayerProjectile.cpp`源文件内的`APlayerProjectile::OnHit`函数中添加一个`UE_LOG()`函数，这样您就可以确保当射弹接触到关卡中的对象时调用该函数。您需要执行以下步骤:\n\n1.  在`Content Browser`界面内，在`/MainCharacter`目录中创建新文件夹`Projectile`。\n2.  在此目录中，从您在*练习 13.11* 、*创建玩家投射体*中创建的`PlayerProjectile`类创建一个新蓝图。命名这个蓝图`BP_PlayerProjectile`。\n3.  打开`BP_PlayerProjectile`并导航至其组件。选择`MeshComp`组件以访问其设置。\n4.  将`Shape_Sphere`网格添加到`MeshComp` 组件的静态网格参数中。\n5.  更新`MeshComp`的变换，使其适合`Scale and Location of the CollisionComp`组件。使用以下值:\n\n    ```cpp\n    Location:(X=0.000000,Y=0.000000,Z=-10.000000)\n    Scale: (X=0.200000,Y=0.200000,Z=0.200000)\n    ```\n\n6.  编辑并保存`BP_PlayerProjectile`蓝图。\n7.  在 Visual Studio 中导航到`PlayerProjectile.cpp`源文件，找到`APlayerProjectile::OnHit`功能。\n8.  在该功能中，执行`UE_LOG`调用，使记录的线路为`LogTemp`、`Warning log level`，并显示文本`HIT`。`UE_LOG`在*第 11 章*、*混合空间 1D、键绑定和状态机*中有所涉及。\n9.  编译您的代码更改，并导航到您在上一练习中放置`BP_PlayerProjectile`参与者的级别。如果你没有把这个演员加到关卡中，现在就加。\n10.  测试前，确保在`Window`选项中打开输出日志。从`Window`下拉菜单中，将鼠标悬停在`Developers Tools`选项上，然后*左键单击*选择`Output Log`。\n11.  Use `PIE` and watch out for the log warning inside `Output Log` when the projectile collides with something.\n\n    以下是预期输出:\n\n    ![Figure 13.39: Scale of the MeshComp better fits the size of the Collision Comp ](img/B16183_13_39.jpg)\n\n图 13.39:网格组件的比例更适合碰撞组件的大小\n\n日志警告应如下所示:\n\n![Figure 13.40: When the projectile hits an object, the text HIT is shown in the Output Log ](img/B16183_13_40.jpg)\n\n图 13.40:当射弹击中一个物体时，文本 HIT 显示在输出日志中\n\n随着这个最终活动的完成，`Player Projectile`为下一章做好了准备，当玩家使用`Throw`动作时，你将产生这个投射物。你将更新`APlayerProjectile::OnHit`功能，使其摧毁与之碰撞的敌人，成为玩家对抗敌人的有效进攻工具。\n\n注意\n\n这个活动的解决方案可以在:[https://packt.live/338jEBx](https://packt.live/338jEBx)找到。\n\n# 总结\n\n在本章中，您学习了如何使用虚幻引擎 4 提供的人工智能工具的不同方面，包括黑板、行为树和人工智能控制器。结合自定义创建的任务和虚幻引擎 4 提供的默认任务，以及装饰器，你可以让敌人的人工智能在你添加到自己的导航网格的边界内导航。\n\n除此之外，你创建了一个新的蓝图演员，允许你使用`Vector`数组变量添加巡逻点。然后你给这个角色添加了一个新的函数，随机选择这些点中的一个，将其位置从本地空间转换到世界空间，然后返回这个新值供敌方角色使用。\n\n有了随机选择一个巡逻点的能力，你更新了自定义`BTTask_FindLocation`任务找到并移动到选中的巡逻点，让敌人可以从每个巡逻点随机移动。这使得敌人的人工智能角色与玩家和环境的互动达到了一个全新的水平。\n\n最后，你创造了玩家可以用来消灭环境中的敌人的玩家投射物。你利用了`Projectile Movement Component`和`Sphere Component`来允许投射物运动，并识别和响应环境中的碰撞。\n\n玩家投射物处于功能状态，是时候进入下一章了，当玩家使用`Throw`动作时，你将使用`Anim Notifies`来产生投射物。*"
  },
  {
    "path": "docs/game-dev-proj-ue/13.md",
    "content": "# 十四、产生玩家投射物\n\n概观\n\n在本章中，您将了解`Anim Notifies`和`Anim States`，它们可以在动画蒙太奇中找到。您将使用 C++ 编写自己的`Anim Notify`代码，并在`Throw`动画蒙太奇中实现该通知。最后，您将了解视觉和听觉效果，以及这些效果如何在游戏中使用。\n\n到本章结束时，您将能够在蓝图和 C++ 中玩动画蒙太奇，并知道如何使用 C++ 和`UWorld`类将对象繁殖到游戏世界中。游戏中的这些元素将被赋予音频和视觉成分，作为一层额外的润色，你的`SuperSideScroller`玩家角色将能够投掷摧毁敌人的投射物。\n\n# 简介\n\n在上一章中，你通过创建一个行为树来让敌人从你创建的`BP_AIPoints`演员中随机选择点，从而在敌人的人工智能方面取得了很大的进步。这给了`SuperSideScroller`游戏更多的生命，因为你现在可以有多个敌人在你的游戏世界里移动。此外，您还学习了虚幻引擎 4 中可用的不同工具，这些工具一起用于制造各种复杂程度的人工智能。这些工具包括`Navigation Mesh`、行为树和黑板。\n\n现在你有敌人在你的关卡中跑来跑去，你需要允许玩家用你在上一章结束时开始创造的玩家投射物来击败这些敌人。\n\n在本章中，您将学习如何使用`UAnimNotify`类在`Throw`动画蒙太奇的特定帧中生成玩家投射物。您还将学习如何将这个新的通知添加到蒙太奇本身，以及如何将一个新的`Socket`添加到主角色骨骼中，投射物将从该骨骼中产生。最后，您将学习如何使用`Particle Systems`和`SoundCues`为游戏增加一层视觉和听觉的润色。\n\n让我们从学习`Anim Notifies`和`Anim Notify States`开始这一章。之后，你将通过创建你自己的`UAnimNotify`职业来弄脏你的手，这样你就可以在`Throw`动画蒙太奇中产生玩家投射物。\n\n# 动画通知和动画通知状态\n\n当创建精致复杂的动画时，动画师和程序员需要有一种方法在动画中添加自定义事件，以允许额外的效果、层和功能发生。虚幻引擎 4 中的解决方案是使用`Anim Notifies`和`Anim Notify States`。\n\n`Anim Notify`和`Anim Notify State`的主要区别在于`Anim Notify State`拥有`Anim Notify`没有的三个截然不同的事件。这些事件分别是`Notify Begin`、`Notify End`、`Notify Tick`，都可以在蓝图或者 C++ 中使用。当涉及到这些事件时，虚幻引擎 4 会确保以下行为:\n\n*   `Notify State`永远从`Notify Begin Event`开始。\n*   `Notify State`永远以`Notify End Event`结束。\n*   `Notify Tick Event`将永远发生在`Notify Begin`和`Notify End`事件之间。\n\n然而，`Anim Notify`是一个简单得多的版本，它只使用一个函数`Notify()`，允许程序员向 notify 本身添加功能。它以*开火而忘记*的心态工作，这意味着你不需要担心在`Notify()`事件的开始、结束或中间的任何地方会发生什么。正是由于`Anim Notify`的这种简单性，并且由于我们不需要`Anim Notify State`中包含的事件，我们将使用`Anim Notify`来为超级侧滚游戏生成玩家抛射物。\n\n在继续下面的练习之前，您将在 C++ 中创建自己的自定义`Anim Notify`，让我们简单讨论一下虚幻引擎 4 默认提供的现有`Anim Notifies`的一些示例。默认`Anim Notifies`状态的完整列表可以在下面的截图中看到:\n\n![Figure 14.1: The full list of default Anim Notifies provided in Unreal Engine 4 ](img/B16183_14_01.jpg)\n\n图 14.1:虚幻引擎 4 中提供的默认动画通知的完整列表\n\n本章后面会用到两个`Anim Notifies`:`Play Particle Effect`和`Play Sound`。让我们更详细地讨论这两个问题，以便您在使用时熟悉它们:\n\n*   `Play Particle Effect`: The `Play Particle Effect` notify, as the name suggests, allows you to spawn and play a particle system at a certain frame of your animation. As shown in the following screenshot, you have options to change the VFX being used, such as updating the `location`, `rotation`, and `scale` settings of the particle. You can even attach the particle to a specified `Socket Name` if you so choose:\n\n    ![Figure 14.2: The Details panel of the Play Particle Effect notify, which  allows you to customize the particle ](img/B16183_14_02.jpg)\n\n图 14.2:播放粒子效果通知的详细信息面板，允许您自定义粒子\n\n注意\n\n视觉效果，简称 VFX，是任何游戏的关键元素。在虚幻引擎 4 中，视觉效果是使用编辑器中名为*级联*的工具创建的。自虚幻引擎 4.20 版本以来，一个名为*尼亚加拉*的新工具作为免费插件被引入，以提高 VFX 的制作质量和流水线。你可以在这里了解更多关于*尼亚加拉*的信息。\n\n游戏中使用的一个非常常见的例子是，当玩家行走或奔跑时，使用这种类型的通知在他们的脚下产生污垢或其他效果。能够指定这些效果在动画的哪一帧产生是非常强大的，并且允许你为你的角色创建令人信服的效果。\n\n*   `Play Sound`: The `Play Sound` notify allows you to play a `Soundcue` or `Soundwave` at a certain frame of your animation. As shown in the following screenshot, you have options to change the sound being used, update its `volume` and `pitch` values, and even have the sound follow the owner of the sound via attaching it to a specified `Socket Name`:\n\n    ![Figure 14.3: The Details panel of the Play Sound notify, which  ](img/B16183_14_03.jpg)\n\n图 14.3:播放声音通知的详细信息面板，允许您自定义声音\n\n很像`Play Particle Effect` notify 给出的例子，`Play Sound` notify 也可以常用于在角色移动时播放脚步声。通过控制动画时间线上可以播放声音的确切位置，可以创建可信的声音效果。\n\n虽然您不会使用`Anim Notify States`，但至少知道默认情况下您可以使用的选项仍然很重要，如下图截图所示:\n\n![Figure 14.4: The full list of default Anim Notify States provided to you in Unreal Engine 4 ](img/B16183_14_04.jpg)\n\n图 14.4:虚幻引擎 4 中提供给你的默认动漫通知状态的完整列表\n\n注意\n\n动画序列中不可用的两个`Notify`状态是*蒙太奇通知窗口*和*禁用根动作*状态，如前面的截图所示。有关通知的更多信息，请参考以下文档:[docs . unrealengine . com/en-US/Engine/Animation/sequence/Notifies/index . html](http://docs.unrealengine.com/en-US/Engine/Animation/Sequences/Notifies/index.html)。\n\n现在您对`Anim Notify`和`Anim Notify State`更加熟悉了，让我们继续下一个练习，您将在 C++ 中创建自己的自定义`Anim Notify`，您将使用它来生成玩家投射物。\n\n## 练习 14.01:创建立陶宛通知类\n\n`SuperSideScroller`游戏中玩家角色将拥有的主要进攻能力是玩家可以向敌人投掷的弹丸。在前一章中，你设置了投射物的框架和基本功能，但是现在，玩家没有办法使用它。为了使产卵或投掷的投射物令人信服，您需要创建一个自定义的`Anim Notify`，然后将其添加到`Throw`动画蒙太奇中。这个`Anim Notify`会让玩家知道是时候产卵了。\n\n执行以下操作创建新的`UAnimNotify`类:\n\n1.  在虚幻引擎 4 中，导航至`File`选项，*左键单击*选择`New C++ Class`选项。\n2.  在`Choose Parent Class`对话窗口中，搜索`AnimNotify`和*，左键单击*`AnimNotify`选项。然后，*左键单击*`Next`选项来命名新类。\n3.  命名这个新类`Anim_ProjectileNotify`。命名后，*左键点击*选择`Create Class`选项，虚幻引擎 4 在 Visual Studio 中重新编译并热重加载新类。bOnce Visual Studio 打开后，你将同时拥有头文件`Anim_ProjectileNotify.h`和源文件`Anim_ProjectileNotify.cpp`。\n4.  The `UAnimNotify` base class has one function that needs to be implemented inside your class:\n\n    ```cpp\n    virtual void Notify(USkeletalMeshComponent* MeshComp,   UAnimSequenceBase* Animation); \n    ```\n\n    当在时间线上点击 notify 时，会自动调用该函数。通过重写该函数，您将能够向 notify 添加自己的逻辑。该功能还允许您访问拥有通知的`Skeletal Mesh`组件和当前正在播放的动画序列。\n\n5.  Next, let's add the override declaration of this function to the header file. In the header file `Anim_ProjectileNotify.h`, add the following code underneath the `GENERATED_BODY()`:\n\n    ```cpp\n    public:  virtual void Notify(USkeletalMeshComponent*   MeshComp,UAnimSequenceBase* Animation) override;\n    ```\n\n    既然已经在头文件中添加了函数，那么就该在`Anim_ProjectileNotify`源文件内部定义函数了。\n\n6.  Inside the `Anim_ProjectileNotify.cpp` source file, define the function and add a `UE_LOG()` call that prints the text `\"Throw Notify\"`, as shown in the following code:\n\n    ```cpp\n    void UAnim_ProjectileNotify::Notify(USkeletalMeshComponent*   MeshComp, UAnimSequenceBase* Animation)\n    {\n      UE_LOG(LogTemp, Warning, TEXT(\"Throw Notify\"));\n    }\n    ```\n\n    现在，您只需使用这个`UE_LOG()`调试工具，就可以知道当您在下一个练习中将这个 notify 添加到`Throw`动画蒙太奇时，这个函数正在被正确调用。\n\n在本练习中，您通过添加以下函数创建了实现自己的`AnimNotify`类所需的基础:\n\n```cpp\nNotify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation)\n```\n\n在该功能中，您使用`UE_LOG()`打印输出日志中的自定义文本`\"Throw Notify\"`，以便您知道该通知工作正常。\n\n在本章的后面，您将更新这个函数，以便它调用逻辑来生成玩家投射物，但是首先，让我们将新的通知添加到`Throw`动画蒙太奇中。\n\n## 练习 14.02:将通知添加到投掷蒙太奇中\n\n现在您已经有了`Anim_ProjectileNotify`通知，是时候将此通知添加到`Throw`动画蒙太奇中，以便它实际上对您有用。\n\n在本练习中，您将把`Anim_ProjectileNotify`添加到`Throw`蒙太奇的时间线中，在动画的确切帧处，您将期望投射体产生。\n\n完成以下步骤来实现这一点:\n\n1.  Back inside Unreal Engine, navigate to the `Content Browser` interface and go to the `/MainCharacter/Animation/` directory. Inside this directory, *double-click* the `AM_Throw` asset to open the `Animation Montage` editor.\n\n    在`Animation Montage`编辑器的最底部，你会找到动画的时间线。默认情况下，当动画播放时，您会观察到*红色条*将沿着时间线移动。\n\n2.  *Left-click* this `red` bar and manually move it to the 22nd `frame`, as close as you can, as shown in the following screenshot:\n\n    ![Figure 14.5: The red colored bar allows you to manually position  notifies anywhere on the timeline ](img/B16183_14_05.jpg)\n\n    图 14.5:红色条允许您手动将通知放置在时间线上的任何位置\n\n    `Throw`动画的第 22 帧是投掷中你期望投射物产生并被玩家投掷的准确时刻。以下截图显示了投掷动画的框架，如在`Persona`内的编辑器中所见:\n\n    ![Figure 14.6: The exact moment the player projectile should spawn ](img/B16183_14_06.jpg)\n\n    图 14.6:玩家投射物应该诞生的确切时刻\n\n3.  Now that you know the position on the timeline that the notify should be played, you can now *right-click* on the thin `red` line within the `Notifies` timeline.\n\n    这将显示一个弹出窗口，您可以在其中添加`Notify`或`Notify State`。在某些情况下，`Notifies`时间线可能会崩溃，很难找到；只需左键点击`Notifies`即可在折叠和展开之间切换。\n\n4.  选择`Add Notify`，从提供的选项中，找到并选择`Anim Projectile Notify`。\n5.  After selecting to add `Anim Projectile Notify` to the Notifies timeline, you will see the following:\n\n    ![Figure 14.7: Anim_ProjectileNotify successfully added to the Throw Animation Montage ](img/B16183_14_07.jpg)\n\n    图 14.7:动画 _ 项目通知成功添加到投掷动画蒙太奇\n\n6.  在`Throw`动画蒙太奇时间轴上`Anim_ProjectileNotify`通知到位后，保存蒙太奇。\n7.  如果`Output Log`窗口不可见，请通过导航至`Window`选项重新启用该窗口，并将鼠标悬停在该窗口上以查看`Developer Tools`。找到`Output Log`和*选项，左键点击*启用。\n8.  现在，使用`PIE`，一旦进入游戏，使用*鼠标左键*开始播放`Throw`蒙太奇。\n\n在动画中添加 notify 的位置，您将看到调试日志文本`Throw Notify`出现在输出日志中。\n\n大家可能还记得*第 12 章*、*动画融合与蒙太奇*中，给玩家角色蓝图`BP_SuperSideScroller_MainCharacter`增加了`Play Montage`功能。为了在虚幻引擎 4 的上下文中学习 C++，在接下来的练习中，您将把这个逻辑从蓝图移动到 C++。这是为了让玩家角色的基本行为不会过于依赖蓝图脚本。\n\n完成本练习后，您已成功将自定义`Anim Notify`类`Anim_ProjectileNotify`添加到`Throw`动画蒙太奇中。这个通知是在你期望从玩家手里扔出一枚炮弹的精确时刻添加的。由于您在*第 12 章*、*动画混合和蒙太奇*中为玩家角色添加了蓝图逻辑，因此当使用*鼠标左键*调用`InputAction`事件`ThrowProjectile`时，您可以播放此`Throw`动画蒙太奇。在从在蓝图中播放投掷动画蒙太奇过渡到在 C++ 中播放蒙太奇之前，让我们进一步讨论播放动画蒙太奇。\n\n# 播放动画蒙太奇\n\n正如您在*第 12 章*、*动画混合和蒙太奇*中所学的，这些项目对于允许动画师将单个动画序列组合成一个完整的蒙太奇非常有用。通过将蒙太奇分成自己独特的部分，并添加粒子和声音通知，动画师和动画程序员可以制作复杂的蒙太奇集，处理动画的所有不同方面。\n\n但是一旦动画蒙太奇准备好了，我们如何在一个角色身上播放这个蒙太奇呢？您已经熟悉了第一种方法，即通过蓝图。\n\n## 在蓝图中播放动画蒙太奇\n\n在蓝图中，`Play Montage`功能可供您使用，如下图所示:\n\n![Figure 14.8: The Play Montage function in Blueprints ](img/B16183_14_08.jpg)\n\n图 14.8:蓝图中的播放蒙太奇功能\n\n您已经使用该功能播放了`AM_Throw`动画蒙太奇。该功能要求必须在`Skeletal Mesh`组件上播放蒙太奇，并且要求播放动画蒙太奇。\n\n其余参数是可选的，这取决于蒙太奇的工作方式。让我们快速了解一下这些参数:\n\n*   `Play Rate`:参数`Play Rate`可以增加或减少动画蒙太奇的播放速度。为了加快播放速度，您可以增加该值；否则，您将降低较慢播放速度的值。\n*   `Starting Position`:`Starting Position`参数允许您设置蒙太奇时间线的开始位置，以秒为单位，蒙太奇将从该位置开始播放。例如，在有 3 秒时间线的动画蒙太奇中，您可以选择在`1.0f`位置开始蒙太奇，而不是在`0.0f`位置。\n*   `Starting Section`:参数`Starting Section`可以让你告诉动画蒙太奇从特定的部分开始。根据蒙太奇的设置方式，您可以为蒙太奇的不同部分创建多个部分。例如，霰弹枪武器重装动画蒙太奇将包括一个用于重装的初始移动部分，一个用于实际子弹重装的环形部分，以及一个用于重新装备武器以便准备再次开火的最终部分。\n\n谈到`Play Montage`功能的输出，您有几个不同的选项:\n\n*   `On Completed`:动画蒙太奇播放完毕，完全融合出来后，调用`On Completed`输出。\n*   `On Blend Out`:当动画蒙太奇开始融合时，调用`On Blend Out`输出。这可能发生在`Blend Out Trigger Time`期间，或者如果蒙太奇提前结束。\n*   `On Interrupted`:当蒙太奇开始融合时，由于该蒙太奇被另一个试图在同一骨架上播放的蒙太奇打断，调用`On Interrupted`输出。\n*   `On Notify Begin & On Notify End`:如果您使用的是动画蒙太奇中`Notifies`类别下的`Montage Notify`选项，则会调用`On Notify Begin`和`On Notify End`输出。`Montage` `Notify`的名称通过`Notify Name`参数返回。\n\n## 用 C++ 播放动画蒙太奇\n\n在 C++ 端，你只需要知道一件事，那就是`UAnimInstance::Montage_Play()`函数。该功能需要播放动画蒙太奇、回放蒙太奇的播放速率、`EMontagePlayReturnType`类型的值、用于确定播放蒙太奇的开始位置的`float`值以及用于确定播放该蒙太奇是否应该停止或中断所有蒙太奇的`Boolean`值。\n\n虽然您不会更改`EMontagePlayReturnType`的默认参数，即`EMontagePlayReturnType::MontageLength`，但了解该枚举器存在的两个值仍然很重要:\n\n*   `Montage Length`:值`Montage Length`返回蒙太奇本身的长度，以秒为单位。\n*   `Duration`: The `Duration` value returns the play duration of the montage, which is equal to the length of the montage, divided by the play rate.\n\n    注意\n\n    有关`UAnimMontage`类的更多详细信息，请参考以下文档:\n\n在下一个练习中，您将了解更多关于播放动画蒙太奇的 C++ 实现。\n\n## 练习 14.03:用 C++ 播放投掷动画\n\n现在你已经通过蓝图和 C++ 更好地理解了在虚幻引擎 4 中播放动画蒙太奇，是时候将播放`Throw`动画蒙太奇的逻辑从蓝图迁移到 C++ 了。这一变化背后的原因是因为蓝图逻辑作为占位符方法被放置到位，以便您可以预览`Throw`蒙太奇。这本书是一个更侧重于游戏开发的 C++ 指南，因此，学习如何在代码中实现这个逻辑是很重要的。\n\n让我们首先从蓝图中移除逻辑，然后继续在玩家角色类中用 C++ 重新创建逻辑。\n\n以下步骤将帮助您完成本练习:\n\n1.  导航到玩家角色蓝图`BP_SuperSideScroller_MainCharacter`，可以在以下目录中找到:`/MainCharacter/Blueprints/`。*双击*该资产打开。\n2.  Inside this Blueprint, you will find the `InputAction ThrowProjectile` event and the `Play Montage` function that you created to preview the `Throw` Animation Montage, as shown in the following screenshot. Delete this logic and then recompile and save the player character Blueprint:\n\n    ![Figure 14.9: You no longer need this placeholder logic inside the player character Blueprint ](img/B16183_14_09.jpg)\n\n    图 14.9:玩家角色蓝图中不再需要这个占位符逻辑\n\n3.  现在，使用`PIE`并使用*鼠标左键*尝试与玩家角色投掷。你会观察到玩家角色不再播放`Throw`动画蒙太奇。让我们通过在 C++ 中添加所需的逻辑来解决这个问题。\n4.  在 Visual Studio 中打开玩家角色的头文件，`SuperSideScroller_Player.h`。\n5.  The first thing you need to do is create a new variable for the player character that will be used for the `Throw` animation. Add the following code under the `Private` access modifier:\n\n    ```cpp\n    UPROPERTY(EditAnywhere)\n    class UAnimMontage* ThrowMontage;\n    ```\n\n    现在你有了一个代表`Throw`动画蒙太奇的变量，是时候在`SuperSideScroller_Player.cpp`文件中添加播放蒙太奇的逻辑了。\n\n6.  Before you can make the call to `UAnimInstance::Montage_Play()`, you need to add the following `include` directory to the existing list at the top of the source file in order to have access to this function:\n\n    ```cpp\n    #include \"Animation/AnimInstance.h\"\n    ```\n\n    从*第九章**视听元素*中我们知道，玩家角色已经有了一个叫`ThrowProjectile`的功能，只要按下*鼠标左键*就会被调用。提醒一下，这是 C++ 中绑定发生的地方:\n\n    ```cpp\n    //Bind pressed action ThrowProjectile to your ThrowProjectile   function\n    PlayerInputComponent->BindAction(\"ThrowProjectile\", IE_Pressed,   this, &ASuperSideScroller_Player::ThrowProjectile);\n    ```\n\n7.  Update `ThrowProjectile` so that it plays `ThrowMontage`, which you set up earlier in this exercise. Add the following code to the `ThrowProjectile()` function. Then, we can discuss what is happening here:\n\n    ```cpp\n    void ASuperSideScroller_Player::ThrowProjectile()\n    {\n      if (ThrowMontage)\n      {\n        bool bIsMontagePlaying = GetMesh()->GetAnimInstance()->      Montage_IsPlaying(ThrowMontage);\n        if (!bIsMontagePlaying)\n        {\n          GetMesh()->GetAnimInstance()->Montage_Play(ThrowMontage,         2.0f);\n        }\n        }    }\n    ```\n\n    第一行是检查`ThrowMontage`是否有效；如果我们没有分配有效的动画蒙太奇，继续逻辑是没有意义的，并且在进一步的函数调用中使用空对象也是危险的，因为它可能导致崩溃。接下来，我们将声明一个新的布尔变量，称为`bIsMontagePlaying`，它决定了`ThrowMontage`是否已经在玩家角色的骨骼网格上玩了。进行此检查是因为`Throw`动画蒙太奇不应在已经播放时播放；如果玩家反复按下*鼠标左键*，将导致动画中断。\n\n    接下来是`If`语句，检查`ThrowMontage`是否有效，蒙太奇是否没有播放。只要满足这些条件，继续前进，播放动画蒙太奇是安全的。\n\n8.  在`If`语句中，您告诉玩家的骨骼网格以`1.0f`的播放速率播放`ThrowMontage`动画蒙太奇。使用`1.0f`值，以便动画蒙太奇以其预期的速度回放。大于`1.0f`的值会使蒙太奇回放更快，而小于`1.0f`的值会使蒙太奇回放更慢。您所了解的其他参数，如开始位置或`EMontagePlayReturnType`参数，可以留在它们的`defaults.Head`处。回到虚幻引擎 4 编辑器中，像过去一样重新编译代码。\n9.  代码重新编译成功后，导航回玩家角色蓝图`BP_SuperSideScroller_MainCharacter`，可以在以下目录找到:`/MainCharacter/Blueprints/`。*双击*该资产打开。\n10.  在玩家角色的`Details`面板中，你现在会看到你添加的`Throw Montage`参数。\n11.  *Left-click* on the drop-down menu for the `Throw Montage` parameter to find the `AM_Throw` montage. *Left-click* again on the `AM_Throw` option to select it for this parameter. Please refer to the following screenshot to see how the variable should be set up:\n\n    ![Figure 14.10: Now, the Throw Montage is assigned the AM_Throw montage ](img/B16183_14_10.jpg)\n\n    图 14.10:现在，投掷蒙太奇被分配了 AM_Throw 蒙太奇\n\n12.  Recompile and save the player character blueprint. Then, use `PIE` to spawn the player character and use the *left mouse button* to play `Throw Montage`. The following screenshot shows this in action:\n\n    ![Figure 14.11: The player character is now able to perform the Throw animation again ](img/B16183_14_11.jpg)\n\n图 14.11:玩家角色现在可以再次执行投掷动画\n\n通过完成本练习，您已经学习了如何为玩家角色添加`Animation Montage`参数，以及如何在 C++ 中播放蒙太奇。除了在 C++ 中播放`Throw` 动画蒙太奇之外，您还添加了通过检查蒙太奇是否已经在播放来控制`Throw`动画播放频率的功能。这样做，可以防止玩家滥发`Throw`输入，导致动画中断或不完整播放。\n\n注意\n\n尝试将`Animation Montage`的播放速率从`1.0f`设置为`2.0f`，并重新编译代码。观察提高动画的播放速率如何影响玩家对动画的观感。\n\n# 游戏世界和产卵对象\n\n当涉及到向游戏世界中产卵对象时，实际上是代表你的级别的`World`对象处理所述对象的创建。您可以将`UWorld`类对象视为代表您的级别的单个顶级对象。\n\n`UWorld`类可以做很多事情，例如从世界中产生和移除对象，检测级别何时被更改或流入/流出，甚至执行线跟踪来帮助对象间检测。为了这一章，我们将集中讨论生成对象。\n\n`UWorld`类有`SpawnActor()`函数的多种变体，这取决于您想要如何生成对象，或者您在生成该对象的上下文中可以通过哪些参数访问。要考虑的三个一致参数如下:\n\n*   `UClass`:`UClass`参数只是你想要在其中繁殖的对象的类。\n*   `FActorSpawnParameters`: This is a struct of variables that give the spawned object more context and references to what has spawned it. For a list of all of the variables included within this struct, please refer to this article from the Unreal Engine 4 Community Wiki: https://www.ue4community.wiki/Actor#Spawn\n\n    让我们简单讨论一下`FActorSpawnParameters`中包含的一个更关键的变量:`Owner`演员。`Owner`是催生这个对象的演员，在玩家角色和投射物的情况下，明确引用玩家作为投射物的拥有者将非常重要。这背后的原因，尤其是在这个游戏的背景下，就是你不希望弹丸与它的`Owner`发生碰撞；你想让这个投射物完全忽略拥有者，这样它只能和敌人或者关卡环境碰撞。\n\n*   `Transform`:向世界产卵一个物体时，世界需要知道这个演员的`location`、`rotation`、`scale`属性才能产卵。在`SpawnActor()`功能的一些模板中，需要传递完整的`Transform`，而在其他模板中，`Location`和`Rotation`需要单独传递。\n\n在继续生成玩家投射物之前，让我们在玩家角色的`Skeleton`中设置`Socket`位置，以便在`Throw`动画期间投射物可以从*玩家的手*中生成。\n\n## 练习 14.04:创建投射物产卵窝\n\n为了生成玩家投射物，你需要确定投射物将在哪个`Transform`中生成，同时主要关注`Location`和`Rotation`，而不是`Scale`。\n\n在本练习中，您将在玩家角色的`Skeleton`上创建一个新的`Socket`，然后您可以在代码中引用该新的`Socket`，以便获得产生投射物的位置。\n\n让我们开始吧:\n\n1.  在虚幻引擎 4 中，导航到`Content Browser`界面，找到`/MainCharacter/Mesh/`目录。\n2.  In this directory, find the `Skeleton` asset; that is, `MainCharacter_Skeleton.uasset`. *Double-click* to open this `Skeleton`.\n\n    为了确定投射物应该在哪里产卵的最佳位置，我们需要添加`Throw`动画蒙太奇作为骨骼的预览动画。\n\n3.  在`Details`面板中的`Animation`类别下，找到`Preview Controller`参数并选择`Use Specific Animation`选项。\n4.  Next, *left-click* on the drop-down menu to find and select the `AM_Throw` Animation Montage from the list of available animations.\n\n    现在，玩家角色的`Skeleton`将开始预览`Throw`动画蒙太奇，如下图截图所示:\n\n    ![Figure 14.12: The player character previewing the Throw Animation Montage ](img/B16183_14_12.jpg)\n\n    图 14.12:玩家角色预览投掷动画蒙太奇\n\n    如果你回忆起*练习 14.02* ，*在投掷蒙太奇*中添加了通知，你在`Throw`动画的第 22 帧添加了`Anim_ProjectileNotify`。\n\n5.  Using the timeline at the bottom of the `Skeleton` editor, move the `red` bar to as close to the 22nd frame as you can. Please refer to the following screenshot:\n\n    ![Figure 14.13: The same 22nd frame in which you added Anim_ProjectileNotify i n an earlier exercise ](img/B16183_14_13.jpg)\n\n    图 14.13:在前面的练习中添加了 Anim_ProjectileNotify 的第 22 帧\n\n    在`Throw`动画的第 22 帧，玩家角色应该如下所示:\n\n    ![Figure 14.14: At the 22nd frame of the Throw Animation Montage, the character’s hand is in position to release a projectile ](img/B16183_14_14.jpg)\n\n    图 14.14:在投掷动画蒙太奇的第 22 帧，角色的手处于释放投射物的位置\n\n    如你所见，玩家角色将从他们的右手投掷弹丸，所以新的`Socket`应该附着在*的右手*上。让我们看看玩家角色的骨架层次，如下图所示:\n\n    ![Figure 14.15: The RightHand bone found within the hierarchy  of the player character’s skeleton ](img/B16183_14_15.jpg)\n\n    图 14.15:在玩家角色骨骼层次中找到的右手骨\n\n6.  从骨骼层次中，找到`RightHand`骨骼。这可以在`RightShoulder`骨骼层次结构下找到。\n7.  *Right-click* on the `RightHand` bone and *left-click* the `Add Socket` option from the list of options that appear. Name this socket `ProjectileSocket`.\n\n    另外，当添加新的`Socket`时，整个`RightHand`的层次将扩展，新的插座将出现在底部。\n\n8.  With `ProjectileSocket` selected, use the `Transform` widget gizmo to position this `Socket` at the following location:\n\n    ```cpp\n    Location = (X=12.961717,Y=25.448450,Z=-7.120584)\n    ```\n\n    最终结果应该如下所示:\n\n    ![Figure 14.16: The final position of ProjectileSocket at the 22nd frame of the Throw Animation in world space. ](img/B16183_14_16.jpg)\n\n    图 14.16:世界空间中投掷动画第 22 帧的投影插座最终位置。\n\n    如果你的小控件看起来有点不同，那是因为上图显示的是世界空间中的插座位置，而不是本地空间。\n\n9.  现在`ProjectileSocket`已经定位在你想要的位置，保存`MainCharacter_Skeleton`资产。\n\n随着这个练习的完成，你现在知道玩家投射物将从哪里产生了。既然你在预览中使用了`Throw`动画蒙太奇，并且使用了相同的第 22 帧动画，你就知道这个位置会根据`Anim_ProjectileNotify`什么时候开火来修正。\n\n现在，让我们继续在 C++ 中生成玩家投射体。\n\n## 练习 14.05:准备产卵器()功能\n\n现在你已经有了`ProjectileSocket`并且现在有了一个可以产生玩家投射物的位置，让我们添加产生玩家投射物所需的代码。\n\n在本练习结束时，您将拥有准备生成投射体的功能，并且它将准备从`Anim_ProjectileNotify`类调用。\n\n请执行以下步骤:\n\n1.  从 Visual Studio 中，导航到`SuperSideScroller_Player.h`头文件。\n2.  You need a class reference variable to the `PlayerProjectile` class. You can do this using the variable template class type known as `TSubclassOf`. Add the following code to the header file, under the `Private` access modifier:\n\n    ```cpp\n    UPROPERTY(EditAnywhere)\n    TSubclassOf<class APlayerProjectile> PlayerProjectile;\n    ```\n\n    现在你已经准备好了变量，是时候声明你将用来产生投射体的函数了。\n\n3.  在 void `ThrowProjectile()`函数和`Public`访问修饰符的声明下添加以下函数声明:\n\n    ```cpp\n    void SpawnProjectile();\n    ```\n\n4.  Before preparing the definition of the `SpawnProjectile()` function, add the following `include` directories to the list of includes in the `SuperSideScroller_Player.cpp` source file:\n\n    ```cpp\n    #include \"PlayerProjectile.h\"\n    #include \"Engine/World.h\"\n    #include \"Components/SphereComponent.h\"\n    ```\n\n    您需要包括`PlayerProjectile.h`，因为它是为了引用射弹类的碰撞成分而需要的。接下来，使用`Engine/World.h` include 是使用`SpawnActor()`功能和访问`FActorSpawnParameters`结构所必需的。最后，你需要使用`Components/SphereComponent.h`包含来更新玩家投射物的碰撞部分，这样它就会忽略玩家。\n\n5.  Next, create the definition of the `SpawnProjectile()` function at the bottom of the `SuperSideScroller_Player.cpp` source file, as shown here:\n\n    ```cpp\n    void ASuperSideScroller_Player::SpawnProjectile()\n    {\n    }\n    ```\n\n    这个函数需要做的第一件事就是检查`PlayerProjectile`类变量是否有效。如果这个对象无效，那么继续尝试并派生它是没有意义的。\n\n6.  Update the `SpawnProjectile()` function so that it looks as follows:\n\n    ```cpp\n    void ASuperSideScroller_Player::SpawnProjectile()\n    {\n      if(PlayerProjectile)\n        {\n        }\n    }\n    ```\n\n    现在，如果`PlayerProjectile`对象有效，你会想要获得玩家当前存在的`UWorld`对象，并确保这个世界有效，然后继续。\n\n7.  Update the `SpawnProjectile()` function to the following:\n\n    ```cpp\n    void ASuperSideScroller_Player::SpawnProjectile()\n    {\n      if(PlayerProjectile)\n        {\n          UWorld* World = GetWorld();\n          if (World)\n            {\n            }\n        }\n    }\n    ```\n\n    此时，您已经进行了安全检查，以确保`PlayerProjectile`和`UWorld`都有效，因此现在可以安全地尝试生成射弹。首先要做的是声明一个`FactorSpawnParameters`类型的新变量，并将玩家指定为所有者。\n\n8.  Add the following code within the most recent `if` statement so that the `SpawnProjectile()` function looks like this:\n\n    ```cpp\n    void ASuperSideScroller_Player::SpawnProjectile()\n    {\n      if(PlayerProjectile)\n        {\n          UWorld* World = GetWorld();\n          if (World)\n            {\n              FActorSpawnParameters SpawnParams;\n              SpawnParams.Owner = this; \n            }\n        }\n    }\n    ```\n\n    如您之前所知，来自`UWorld`对象的`SpawnActor()`函数调用将需要`FActorSpawnParameters`结构作为衍生对象初始化的一部分。在玩家投射物的情况下，可以使用`this`关键字作为投射物拥有者的玩家角色类的参考。当你在投射物产生后更新它的碰撞时，这将在这个函数的后面派上用场。\n\n9.  Next, you need to handle the `Location` and `Rotation` parameters of the `SpawnActor()` function. Add the following lines under the latest line, `SpawnParams.Owner = this`:\n\n    ```cpp\n    FVector SpawnLocation = this->GetMesh()-  >GetSocketLocation(FName(\"ProjectileSocket\"));\n    FRotator Rotation = GetActorForwardVector().Rotation();\n    ```\n\n    在第一行中，您声明了一个名为`SpawnLocation`的新`FVector`变量。该向量使用您在上一练习中创建的`ProjectileSocket`插座的`Socket`位置。从`GetMesh()`函数返回的`Skeletal Mesh`组件包含一个名为`GetSocketLocation()`的函数，该函数将使用传入的`FName`返回插座的位置；在这种情况下，名称`ProjectileSocket`。\n\n    在第二行，您正在声明一个名为`Rotation`的新`FRotator`变量。该值被设置为玩家的前进向量，转换成一个`Rotator`容器。这将确保旋转，或者换句话说，玩家投射物将产生的方向，将在玩家的前面，并且它将远离玩家。\n\n    现在，所有产生射弹所需的参数都准备好了。\n\n10.  Add the following line underneath the code from the previous step:\n\n    ```cpp\n    APlayerProjectile* Projectile = World-  >SpawnActor<APlayerProjectile>(PlayerProjectile, SpawnLocation,   Rotation, SpawnParams);\n    ```\n\n    `World->SpawnActor()`函数将返回一个你试图在其中繁殖的类的对象；在这种情况下，`APlayerProjectile`。这就是为什么你要在实际产卵前添加`APlayerProjectile* Projectile`。然后，传递`SpawnLocation`、`Rotation`和`SpawnParams`参数，以确保射弹在您想要的位置和方式下产卵。\n\n11.  Finally, you can add the player character to the array of actors to ignore on the player projectile by adding the following lines of code:\n\n    ```cpp\n    if (Projectile)\n    {\n      Projectile->CollisionComp->    MoveIgnoreActors.Add(SpawnParams.Owner);\n    }\n    ```\n\n    现在你已经有了射弹的参考，这一行正在更新`CollisionComp`组件，这样玩家，或者`SpawnParams.Owner`，就被添加到了`MoveIgnoreActors`阵中。这个演员阵列在移动时会被投射物的碰撞所忽略，这是完美的，因为这个投射物不应该与投掷它的玩家发生碰撞。\n\n12.  返回编辑器重新编译新添加的代码。代码编译成功后，本练习就完成了。\n\n随着这个练习的完成，你现在有了一个功能，可以生成玩家角色内部分配的玩家投射类。通过为投射体和世界的有效性添加安全检查，您可以确保如果产生了一个对象，它就是有效世界中的有效对象。\n\n接下来，您为`UWorld SpawnActor()`功能设置适当的`location`、`rotation`和`FActorSpawnParameters`参数，以确保玩家投射物在正确的位置产生，基于前一练习中的插座位置，具有适当的方向，使其远离玩家，并以玩家角色作为其`Owner`。\n\n现在，是时候更新`Anim_ProjectileNotify`源文件，让它生成投射物了。\n\n## 练习 14.06:更新 Anim_ProjectileNotify 类\n\n你已经准备好了允许玩家投射物产生的功能，但是你还没有在任何地方调用这个功能。回到*练习 14.01* 、*创建一个 UAnim 通知类*，你创建了`Anim_ProjectileNotify`类，而在*练习 14.02* 、*将通知添加到投掷蒙太奇*中，你将此通知添加到`Throw`动画蒙太奇中。\n\n现在是时候更新`Uanim` `Notify`类了，这样它就可以调用`SpawnProjectile()`函数了。\n\n为此，请执行以下操作:\n\n1.  In Visual Studio, open the `Anim_ProjectileNotify.cpp` source file.\n\n    在源文件中，您有以下代码:\n\n    ```cpp\n    #include \"Anim_ProjectileNotify.h\"\n    void UAnim_ProjectileNotify::Notify(USkeletalMeshComponent*   MeshComp, UAnimSequenceBase* Animation)\n    {\n      UE_LOG(LogTemp, Warning, TEXT(\"Throw Notify\"));\n    }\n    ```\n\n2.  从`Notify()`功能中移除`UE_LOG()`线。\n3.  Next, add the following `include` lines underneath `Anim_ProjectileNotify.h`:\n\n    ```cpp\n    #include \"Components/SkeletalMeshComponent.h\"\n    #include \"SuperSideScroller/SuperSideScroller_Player.h\"\n    ```\n\n    您需要包含`SuperSideScroller_Player.h`头文件，因为它是调用您在上一个练习中创建的`SpawnProjectile()`函数所必需的。我们还包含了`SkeletalMeshComponent.h`，因为我们将在`Notify()`函数中引用这个组件，所以最好也包含在这里。\n\n    `Notify()`函数传入一个对拥有的`Skeletal Mesh`的引用，标记为`MeshComp`。您可以使用骨骼网格，通过使用`GetOwner()`功能并将返回的演员转换到您的`SuperSideScroller_Player`类来获取对玩家角色的引用。我们接下来要做这个。\n\n4.  在`Notify()`功能中，增加以下一行代码:\n\n    ```cpp\n    ASuperSideScroller_Player* Player =   Cast<ASuperSideScroller_Player>(MeshComp->GetOwner());\n    ```\n\n5.  现在您已经有了对玩家的引用，在调用`SpawnProjectile()`函数之前，您需要添加`Player`变量的有效性检查。在前一步的代码行后添加以下代码行:\n\n    ```cpp\n    if (Player)\n    {\n      Player->SpawnProjectile();\n    }\n    ```\n\n6.  Now that the `SpawnProjectile()` function is being called from the `Notify()` function, return to the editor to recompile and hot-reload the code changes you have made.\n\n    在你能够使用`PIE`跑来跑去并投掷玩家投射物之前，你需要分配上一个练习中的`Player Projectile`变量。\n\n7.  在`Content Browser`界面，导航到`/MainCharacter/Blueprints`目录找到`BP_SuperSideScroller_MainCharacter`蓝图。*双击*打开蓝图。\n8.  在`Details`面板中，`Throw Montage`参数下面，你会发现`Player Projectile`参数。*左键点击*该参数的下拉选项，找到`BP_PlayerProjectile`。在该选项上左键单击将其分配给`Player Projectile`变量。\n9.  重新编译并保存`BP_SuperSideScroller_MainCharacter`蓝图。\n10.  Now, use `PIE` and use the *left mouse button*. The player character will play the `Throw` animation and the player projectile will spawn.\n\n    请注意，射弹是从你创建的`ProjectileSocket`功能中衍生出来的，它会远离玩家。下面的截图显示了这一点:\n\n    ![Figure 14.17: The player can now throw the player projectile ](img/B16183_14_17.jpg)\n\n图 14.17:玩家现在可以投掷玩家射弹了\n\n完成这个练习后，玩家现在可以投掷玩家射弹了。玩家的投射物，在当前状态下，对敌人无效，只是在空中飞行。在`Throw` 动画蒙太奇、`Anim_ProjectileNotify`类和玩家角色之间花费了大量的移动部件来让玩家投掷弹丸。\n\n在接下来的练习中，你将更新玩家投射物，使其摧毁敌人，并发挥额外的效果，如粒子和声音。\n\n# 毁灭演员\n\n到目前为止，在这一章中，我们已经把大量的焦点放在了游戏世界中的演员的培养或创造上；玩家角色使用`UWorld`职业来产生投射物。虚幻引擎 4 和它的基础`Actor`类有一个默认的功能，你可以用它来摧毁或移除游戏世界中的一个演员:\n\n```cpp\nbool AActor::Destroy( bool bNetForce, bool bShouldModifyLevel )\n```\n\n在`/Source/Runtime/Engine/Actor.cpp`目录中找到`Actor.cpp`源文件，就可以在 Visual Studio 中找到这个功能的完整实现。这个功能存在于从`Actor`类延伸出来的所有类中，在虚幻引擎 4 的情况下，它存在于所有可以在游戏世界中产生或放置的类中。更明确地说，`EnemyBase`和`PlayerProjectile`两个班都是*班的*孩子，因此可以被消灭。\n\n进一步查看`AActor::Destroy()`功能，您会发现下面一行:\n\n```cpp\nWorld->DestroyActor( this, bNetForce, bShouldModifyLevel );\n```\n\n我们将不进一步详细讨论`UWorld`类为了毁灭一个演员到底做了什么，但重要的是要强调这样一个事实，即`UWorld`类负责创造和毁灭世界内部的演员。请随意深入挖掘源代码引擎代码，以找到更多关于`UWorld`类如何处理演员的毁灭和繁殖的信息。\n\n现在你有了更多关于虚幻引擎 4 如何处理游戏世界中演员的毁灭和移除的上下文，我们将自己为敌人角色实现这一点。\n\n## 练习 14.07:创建毁灭者()函数\n\n`Super SideScroller`游戏的主要玩法是玩家在关卡周围移动，用弹丸消灭敌人。在项目的这一点上，你已经处理了玩家的移动并产生了玩家投射物。然而，射弹还不能消灭敌人。\n\n为了使这个功能到位，我们将从给`EnemyBase`类添加一些逻辑开始，这样它就知道如何处理它的破坏，并在它与玩家投射物碰撞时将其从游戏中移除。\n\n完成以下步骤来实现这一点:\n\n1.  首先，导航到 Visual Studio，打开`EnemyBase.h`头文件。\n2.  In the header file, create the declaration of a new function called `DestroyEnemy()` under the `Public` access modifier, as shown here:\n\n    ```cpp\n    public:\n      void DestroyEnemy();\n    ```\n\n    确保这个函数定义写在类定义的`GENERATED_BODY()`下面。\n\n3.  将这些更改保存到头文件中，打开`EnemyBase.cpp`源文件，以便添加该功能的实现。\n4.  Below the `#include` lines, add the following function definition:\n\n    ```cpp\n    void AEnemyBase::DestroyEnemy()\n    {\n    }\n    ```\n\n    目前，这个功能将非常简单。您所需要做的就是从基础`Actor`类中调用继承的`Destroy()`函数。\n\n5.  更新`DestroyEnemy()`功能，使其看起来像这样:\n\n    ```cpp\n    void AEnemyBase::DestroyEnemy()\n    {\n      Destroy();\n    }\n    ```\n\n6.  这个函数完成后，保存源文件并返回编辑器，这样您就可以重新编译并热重新加载代码。\n\n随着这个练习的完成，敌人角色现在有了一个功能，只要你选择，就可以轻松处理演员的毁灭。`DestroyEnemy()`功能是可以公开访问的，这样就可以被其他类调用，这在以后处理玩家抛射物的销毁时会派上用场。\n\n你之所以创建自己独特的摧毁敌人行动者的功能，是因为你将在本章的后面使用这个功能，当 VFX 和 SFX 被玩家投射物摧毁时，将它们添加到敌人中。\n\n在继续讨论敌人毁灭的抛光元素之前，让我们在玩家投射类中实现一个类似的功能，这样它也可以被摧毁。\n\n## 练习 14.08:摧毁射弹\n\n现在敌人角色可以通过你在上一个练习中实现的新`DestroyEnemy()`功能来处理被摧毁的情况，是时候对玩家投射物进行同样的操作了。\n\n在这个练习结束时，玩家投射物将有自己独特的功能来处理自己的毁灭和从游戏世界中移除。\n\n让我们开始吧:\n\n1.  在 Visual Studio 中，打开播放器弹丸的头文件；也就是`PlayerProjectile.h`。\n2.  在`Public`访问修饰符下，添加如下函数声明:\n\n    ```cpp\n    void ExplodeProjectile();\n    ```\n\n3.  接下来，打开玩家投射物的源文件；也就是`PlayerProjectile.cpp`。\n4.  Underneath the void `APlayerProjectile::OnHit` function, add the definition of the `ExplodeProjectile()` function:\n\n    ```cpp\n    void APlayerProjectile::ExplodeProjectile()\n    {\n    }\n    ```\n\n    目前，该功能的工作方式与上一练习中的`DestroyEnemy()`功能相同。\n\n5.  将继承的`Destroy()`函数添加到新的`ExplodeProjectile()`函数中，比如:\n\n    ```cpp\n    void APlayerProjectile::ExplodeProjectile()\n    {\n      Destroy();\n    }\n    ```\n\n6.  这个函数完成后，保存源文件并返回编辑器，这样您就可以重新编译并热重新加载代码。\n\n随着这个练习的完成，玩家投射物现在有了一个功能，无论你选择什么时候，它都可以轻松处理演员的毁灭。你需要创建自己独特的函数来处理摧毁玩家投射物行动者的原因与你创建`DestroyEnemy()`函数的原因是一样的——当玩家投射物与另一个行动者碰撞时，你将在本章的后面使用这个函数向玩家投射物添加 VFX 和 SFX。\n\n现在你已经有了在玩家投射体和敌人角色中实现`Destroy()`功能的经验，是时候把这两个元素放在一起了。\n\n在下一个活动中，你将启用玩家投射物，以便在敌人角色碰撞时摧毁他们。\n\n## 活动 14.01:射弹摧毁敌人\n\n现在玩家投射物和敌人角色都可以处理被摧毁的情况，是时候多走一步，让玩家投射物在碰撞时摧毁敌人角色了。\n\n为此，请执行以下步骤:\n\n1.  将`EnemyBase.h`头文件的`#include`语句添加到`PlayerProjectile.cpp`源文件的顶部。\n2.  在 void `APlayerProjectile::OnHit()`函数中，创建一个新的`AEnemyBase*`类型的变量，并调用这个变量`Enemy`。\n3.  将`APlayerProjectile::OnHit()`函数的`OtherActor`参数转换为`AEnemyBase*`类，并将`Enemy`变量设置为该转换的结果。\n4.  使用`if()`语句检查`Enemy`变量的有效性。\n5.  如果`Enemy`有效，从这个`Enemy`调用`DestroyEnemy()`功能。\n6.  在`if()`块之后，调用`ExplodeProjectile()`功能。\n7.  保存对源文件的更改，并返回到虚幻引擎 4 编辑器。\n8.  Use `PIE` and then use the player projectile against an enemy to observe the results.\n\n    预期产出如下:\n\n    ![Figure 14.18: The player throwing the projectile ](img/B16183_14_18.jpg)\n\n图 14.18:投掷弹丸的玩家\n\n当射弹击中敌人时，敌人角色被摧毁，如下图所示:\n\n![Figure 14.19: The projectile and enemy destroyed ](img/B16183_14_19.jpg)\n\n图 14.19:射弹和敌人被摧毁\n\n这项活动完成后，玩家投射物和敌人角色在相互碰撞时可以被摧毁。此外，每当另一个演员触发其`APlayerProjectile::OnHit()`功能时，玩家投射物将被摧毁。\n\n至此，`Super SideScroller`游戏的一个主要元素已经完成:玩家投射物产卵，敌人与投射物碰撞时被消灭。你可以观察到消灭这些演员非常简单，玩家也不太感兴趣。\n\n这就是为什么，在本章即将到来的练习中，你将分别学习更多关于视觉和听觉效果，或者 VFX 和 SFX 的知识。你也将实施这些关于敌人角色和玩家投射物的元素。\n\n现在，敌人角色和玩家投射物都可以被摧毁，让我们简单讨论一下什么是 VFX 和 SFX，以及它们将如何影响这个项目。\n\n注意\n\n这个活动的解决方案可以在:[https://packt.live/338jEBx](https://packt.live/338jEBx)找到。\n\n# 视觉和听觉效果\n\n粒子系统等视觉效果和声音提示等声音效果在视频游戏中发挥着重要作用。它们在系统、游戏机制甚至基本动作的基础上增加了一定程度的润色，使这些元素变得更有趣或更令人愉悦。\n\n让我们从理解视觉效果开始，然后是音频效果。\n\n**视觉效果(VFX)**\n\n在虚幻引擎 4 的背景下，视觉效果由所谓的**粒子系统**组成。粒子系统由发射器组成，发射器由模块组成。在这些模块中，您可以使用材质、网格和数学模块来控制发射器的外观和行为。最终的结果可能是任何事情，从火把，或雪落下，到雨，灰尘，等等。\n\n注意\n\n您可以在这里了解更多信息:[https://docs . unrealengine . com/en-US/Resources/Showcases/Effects/index . html](https://docs.unrealengine.com/en-US/Resources/Showcases/Effects/index.html)。\n\n**音效(SFX)**\n\n在虚幻引擎 4 的上下文中，音频效果由声波和声音提示的组合组成:\n\n*   声波是`.wav`可以导入虚幻引擎 4 的音频格式文件。\n*   Sound Cues combine Sound Wave audio files with other nodes such as Oscillator, Modulator, and Concatenator to create unique and complex sounds for your game.\n\n    注意\n\n    您可以在这里了解更多信息:[https://docs . unrealengine . com/en-US/Engine/Audio/sound clues/node reference/index . html](https://docs.unrealengine.com/en-US/Engine/Audio/SoundCues/NodeReference/index.html)。\n\n我们以 Valve 开发的游戏*传送门 2* 为例。\n\n在*传送门 2* 中，玩家使用传送门枪发射两个传送门:一个*橙色*和一个*蓝色*。这些入口允许玩家穿越间隙，将物体从一个位置移动到另一个位置，并利用其他简单的机制来创建复杂的谜题。这些传送门的使用，发射传送门的音效，以及这些传送门的视觉 VFX 让游戏玩起来更加愉快。如果你对游戏不熟悉，请在这里观看完整的演练:[https://www.youtube.com/watch?v=ZFqk8aj4-PA](https://www.youtube.com/watch?v=ZFqk8aj4-PA)。\n\n注意\n\n关于声音和声音设计的重要性的进一步阅读，请参考以下 Gamasutra 文章:[https://www . Gamasutra . com/view/news/318157/7 _ games _ worthy _ study _ for _ theory _ sound _ design . PHP](https://www.gamasutra.com/view/news/318157/7_games_worth_studying_for_their_excellent_sound_design.php)。\n\n在虚幻引擎 4 的背景下，VFX 最初是使用名为 **Cascade** 的工具创建的，艺术家可以结合使用`materials`、`static meshes`和`math`为游戏世界创建有趣且令人信服的效果。本书不会深入探讨这个工具是如何工作的，但是你可以在这里找到关于 Cascade 的信息:[https://www . UE4 community . wiki/Legacy/Introduction _ to _ Particles _ in _ UE4 _-_ 2 _-_ Cascade _ at _ a _ sketch](https://www.ue4community.wiki/Legacy/Introduction_to_Particles_in_UE4_-_2_-_Cascade_at_a_Glance)。\n\n在引擎的更新版本中，从 4.20 更新开始，有一个名为**尼亚加拉**的插件可以创建视觉效果。`Niagara`与 Cascade 不同，它使用了一个类似于蓝图的系统，在这个系统中，您可以可视化地编写效果的行为脚本，而不是使用一组具有预定义行为的预设模块。你可以在这里找到更多关于尼亚加拉的信息。\n\n在*第九章*、*视听元素*中，您了解到了更多关于音频以及音频在虚幻引擎 4 中的处理方式。现在需要知道的是虚幻引擎 4 使用`.wav`文件格式将音频导入引擎。从那里，您可以直接使用`.wav`文件，在编辑器中称为声波，或者您可以将这些资产转换为声音提示，这允许您在声波之上添加音频效果。\n\n最后，在接下来的练习中，有一个重要的课程需要了解，这个课程叫做`UGameplayStatics`。这是虚幻引擎中的一个静态类，可以从 C++ 和蓝图中使用，它提供了各种有用的游戏相关功能。在接下来的练习中，您将使用以下两个功能:\n\n```cpp\nUGameplayStatics::SpawnEmitterAtLocation\nUGameplayStatics:SpawnSoundAtLocation\n```\n\n这两种功能的工作方式非常相似；它们都需要一个`World`上下文对象来产生效果，粒子系统或音频来产生效果，以及产生效果的位置。在下一个练习中，你将使用这些功能为敌人产生摧毁效果。\n\n## 练习 14.09:消灭敌人时增加效果\n\n在本练习中，您将向本章和练习中包含的项目添加新内容。这包括粒子 VFX 和声音 SFX，以及它们所需的所有资产。然后，你将更新`EnemyBase`类，这样它就可以使用音频和粒子系统参数来添加当敌人被玩家投射物摧毁时所需的抛光层。\n\n在这个练习结束时，你将有一个敌人，当它与玩家的投射物碰撞时，会在视觉和听觉上被摧毁。\n\n让我们开始吧:\n\n1.  首先，我们需要从`Action RPG`项目中迁移特定的资产，这可以在`Unreal Engine Launcher`的`Learn`选项卡中找到。\n2.  From `Epic Games Launcher`, navigate to the `Learn` tab and, under the `Games` category, you will find `Action RPG`:\n\n    注意\n\n    在本章后面的练习中，您将从 Action RPG 项目中获取额外的资产，因此您应该保持该项目打开，以避免重复打开该项目。\n\n3.  左键单击`Action RPG`游戏项目，然后左键单击`Create Project`选项。\n4.  从这里，选择引擎版本 4.24，并选择将项目下载到哪个目录。然后，*左键点击*`Create`按钮开始安装项目。\n5.  一旦`Action RPG`项目下载完毕，导航至`Epic Games Launcher`的`Library`选项卡，找到`My Projects`部分下的`ActionRPG`。\n6.  *双击*`ActionRPG`项目，在虚幻引擎编辑器中打开。\n7.  在编辑器中，在`Content Browser`界面找到`A_Guardian_Death_Cue`音频资产。*右键点击*该资产，选择`Asset Actions`，然后选择`Migrate`。\n8.  选择`Migrate`后，会出现`A_Guardian_Death_Cue`中引用的所有资产。这包括所有音频类和声波文件。从`Asset Report` 对话窗口中选择`OK`。\n9.  接下来，您需要导航到您的`Super SideScroller`项目的`Content`文件夹，然后*左键单击* `Select Folder`。\n10.  一旦迁移过程完成，您将在编辑器中收到通知，告知您迁移已成功完成。\n11.  Do the same migration steps for the `P_Goblin_Death` VFX asset. The two primary assets you are adding to the project are as follows:\n\n    ```cpp\n    A_Guardian_Death_Cue\n    P_Goblin_Death\n    ```\n\n    `P_Goblin_Death`粒子系统资源引用包含在`Effects`目录中的附加资源，如材料和纹理，而`A_Guardian_Death_Cue`引用包含在`Assets`目录中的附加声波资源。\n\n12.  After migrating these folders into your `Content` directory, open the Unreal Engine 4 editor of your `SuperSideScroller` project to find the new folders included in your project's `Content Browser`.\n\n    你将用于摧毁敌人角色的粒子叫做`P_Goblin_Death`，可以在`/Effects/FX_Particle/`目录中找到。你用来摧毁敌方角色的声音叫做`A_Guardian_Death_Cue`，可以在`/img/Sounds/Creatures/Guardian/`目录中找到。现在您需要的资产已经导入到编辑器中，让我们继续代码。\n\n13.  打开 Visual Studio，导航到敌方基类的头文件；也就是`EnemyBase.h`。\n14.  添加以下`UPROPERTY()`变量。这将代表敌人被消灭时的粒子系统。确保这是在`Public`访问修饰符\n\n    ```cpp\n    UPROPERTY(EditAnywhere, BlueprintReadOnly)\n    class UParticleSystem* DeathEffect;\n    ```\n\n    下声明的\n15.  Add the following `UPROPERTY()` variable. This will represent the sound for when the enemy is destroyed. Make sure this is declared under the `Public` access modifier:\n\n    ```cpp\n    UPROPERTY(EditAnywhere, BlueprintReadOnly)\n    class USoundBase* DeathSound;\n    ```\n\n    定义了这两个属性之后，让我们继续前进，添加在敌人被消灭时产生和使用这些效果所需的逻辑。\n\n16.  Inside the source file for the enemy base class, `EnemyBase.cpp`, add the following includes for the `UGameplayStatics` and `UWorld` classes:\n\n    ```cpp\n    #include \"Kismet/GameplayStatics.h\"\n    #include \"Engine/World.h\"\n    ```\n\n    当敌人被消灭时，你将使用`UGameplayStatics`和`UWorld`职业将声音和粒子系统繁殖到世界中。\n\n17.  在`AEnemyBase::DestroyEnemy()`功能中，你有一行代码:\n\n    ```cpp\n    Destroy();\n    ```\n\n18.  Add the following line of code above the `Destroy()` function call:\n\n    ```cpp\n    UWorld* World = GetWorld();\n    ```\n\n    在尝试生成粒子系统或声音之前，有必要定义`UWorld`对象，因为需要`World`上下文对象。\n\n19.  接下来，使用`if()`语句检查刚刚定义的`World`对象的有效性:\n\n    ```cpp\n    if(World)\n    {\n    }\n    ```\n\n20.  Within the `if()` block, add the following code to check the validity of the `DeathEffect` property, and then spawn this effect using the `SpawnEmitterAtLocation` function from `UGameplayStatics`:\n\n    ```cpp\n    if(DeathEffect)\n    {\n        UGameplayStatics::SpawnEmitterAtLocation(World,       DeathEffect, GetActorTransform());\n    }\n    ```\n\n    在尝试派生或操作对象之前，您应该确保对象是有效的，这一点怎么强调都不为过。通过这样做，您可以避免引擎崩溃。\n\n21.  After the `if(DeathEffect)` block, perform the same validity check of the `DeathSound` property and then spawn the sound using the `UGameplayStatics::SpawnSoundAtLocation` function:\n\n    ```cpp\n    if(DeathSound)\n    {\n        UGameplayStatics::SpawnSoundAtLocation(World,       DeathSound, GetActorLocation());\n    }\n    ```\n\n    在调用`Destroy()`函数之前，您需要检查`DeathEffect`和`DeathSound`属性是否都有效，如果有效，使用适当的`UGameplayStatics`函数生成这些效果。这确保了无论任何一个属性是否有效，敌人角色仍然会被摧毁。\n\n22.  现在`AEnemyBase::DestroyEnemy()`函数已经被更新来产生这些效果，返回到虚幻引擎 4 编辑器来编译和热重载这些代码变化。\n23.  在`Content Browser`界面内，导航至`/Enemy/Blueprints/`目录。*双击*`BP_Enemy`资产将其打开。\n24.  在敌人蓝图的`Details`面板中，你会发现`Death Effect`和`Death Sound`属性。在`Death Effect`属性的下拉列表中左键单击，找到`P_Goblin_Death`粒子系统。\n25.  接下来，在`Death Effect`参数下，*左键单击`Death Sound`属性下拉列表中的*，找到`A_Guardian_Death_Cue`声音提示。\n26.  现在这些参数已经更新并分配了正确的效果，编译并保存敌人蓝图。\n27.  Using `PIE`, spawn the player character and throw a player projectile at an enemy. If an enemy is not present in your level, please add one. When the player projectile collides with the enemy, the VFX and SFX you added will play, as shown in the following screenshot:\n\n    ![Figure 14.20: Now, the enemy explodes and gets destroyed in a blaze of glory ](img/B16183_14_20.jpg)\n\n图 14.20:现在，敌人在荣耀的火焰中爆炸并被摧毁\n\n随着这个练习的完成，当敌人角色被玩家投掷物摧毁时，它会播放一个粒子系统和一个声音提示。这给游戏增加了一层很好的抛光，并且它让消灭敌人变得更令人满意。\n\n在下一个练习中，您将为播放器投射体添加一个新的粒子系统和音频组件，以便它在空中飞行时看起来和听起来更有趣。\n\n## 练习 14.10:为玩家投掷物添加效果\n\n在当前状态下，玩家投射物按照预期的方式运行；它在空中飞行，与游戏世界中的物体碰撞，然后被摧毁。然而，从视觉上看，玩家投射物只是一个具有普通白色纹理的球。\n\n在本练习中，您将通过添加粒子系统和音频组件来为玩家投掷物添加一层抛光，以便投掷物使用起来更愉快。\n\n完成以下步骤来实现这一点:\n\n1.  Much like the previous exercises, we will need to migrate assets from the `Action RPG` project to our `Super SideScroller` project. Please refer to *Exercise 14.09*, *Adding Effects When the Enemy Is Destroyed*, on how to install and migrate assets from the `Action RPG` project.\n\n    您要添加到项目中的两个主要资产如下:\n\n    ```cpp\n    P_Env_Fire_Grate_01\n    A_Ambient_Fire01_Cue\n    ```\n\n    `P_Env_Fire_Grate_01`粒子系统资产引用了包含在`Effects`目录中的附加资产，例如材质和纹理，而`A_Ambient_Fire01_Cue`引用了包含在`Assets`目录中的附加声波和声音衰减资产。\n\n    你将用于玩家投射物的粒子叫做`P_Env_Fire_Grate_01`，可以在`/Effects/FX_Particle/`目录中找到。这与上一练习中`P_Goblin_Death` VFX 使用的目录相同。你将用于玩家投射的声音叫做`A_Ambient_Fire01_Cue`，可以在`/img/Sounds/Ambient/`目录中找到。\n\n2.  *在`Action RPG`项目的`Content Browser`界面中右键单击这些资产中的每一个*，选择`Asset Actions`，然后选择`Migrate`。\n3.  Make sure to choose the directory of the `Content` folder for your `Super SideScroller` project before confirming the migration.\n\n    现在所需的资产已经迁移到我们的项目中，让我们继续创建玩家投射类。\n\n4.  打开 Visual Studio，导航到玩家投射类的头文件；也就是`PlayerProjectile.h`。\n5.  在`Private`访问修饰符下，在`UStaticMeshComponent* MeshComp`类组件的声明下，添加以下代码来声明玩家投射体的新音频组件:\n\n    ```cpp\n    UPROPERTY(VisibleDefaultsOnly, Category = Sound)\n    class UAudioComponent* ProjectileMovementSound;\n    ```\n\n6.  Next, add the following code underneath the declaration of the audio component in order to declare a new particle system component:\n\n    ```cpp\n    UPROPERTY(VisibleDefaultsOnly, Category = Projectile)\n    class UParticleSystemComponent* ProjectileEffect;\n    ```\n\n    这些效果将成为玩家投射物的组成部分，而不是使用蓝图中可以定义的属性，比如在敌人角色类中。这是因为这些效果应该附加到射弹的碰撞部分，以便在投掷时随着射弹穿过水平面而移动。\n\n7.  With these two components declared in the header file, open the source file for the player projectile and add the following includes to the list of `include` lines at the top of the file:\n\n    ```cpp\n    #include \"Components/AudioComponent.h\"\n    #include \"Engine/Classes/Particles/ParticleSystemComponent.h\"\n    ```\n\n    您需要引用音频组件和粒子系统类，以便使用`CreateDefaultSubobject`功能创建这些子对象，并将这些组件附加到`RootComponent`。\n\n8.  添加以下行以创建`ProjectileMovementSound`组件的默认子对象，并将该组件附加到`RootComponent` :\n\n    ```cpp\n    ProjectileMovementSound = CreateDefaultSubobject<UAudioComponent>  (TEXT(\"ProjectileMovementSound\"));\n      ProjectileMovementSound->AttachToComponent(RootComponent,   FAttachmentTransformRules::KeepWorldTransform);\n    ```\n\n9.  接下来，添加以下行，以便为`ProjectileEffect`组件创建默认子对象，并将该组件附加到`RootComponent` :\n\n    ```cpp\n    ProjectileEffect = CreateDefaultSubobject<UParticleSystemComponent>(TEXT(\"Projectile   Effect\"));\n    ProjectileEffect->AttachToComponent(RootComponent,   FAttachmentTransformRules::KeepWorldTransform);\n    ```\n\n10.  现在您已经创建、初始化了这两个组件并将其附加到`RootComponent`上，请返回到虚幻引擎 4 编辑器重新编译并热重新加载这些代码更改。\n11.  From the `Content Browser` interface, navigate to the `/MainCharacter/Projectile/` directory. Find the `BP_PlayerProjectile` asset and *double-click* it to open the Blueprint.\n\n    在`Components`选项卡中，您将找到使用前面的代码添加的两个新组件。观察这些组件是否连接到`CollisionComp`组件，也称为`RootComponent`。\n\n12.  *Left-click* to select the `ProjectileEffect` component and, within the `Details` panel, assign the `P_Env_Fire_Grate_01` VFX asset to this parameter, as shown in the following screenshot:\n\n    ![Figure 14.21: Now, you can apply the P_Env_fire_Grate_01 VFX asset to the  Particle System component you added earlier ](img/B16183_14_21.jpg)\n\n    图 14.21:现在，您可以将 P_Env_fire_Grate_01 VFX 资产应用到之前添加的粒子系统组件中\n\n13.  Before assigning the audio component, let's adjust the `Transform` of the `ProjectileEffect` VFX asset. Update the `Rotation` and `Scale` parameters of the `Transform` for the VFX so that they match what is shown in the following screenshot:\n\n    ![Figure 14.22: The updated Transform of the particle system component  so that it fits better with the projectile ](img/B16183_14_22.jpg)\n\n    图 14.22:粒子系统组件的更新变换，使其更好地适应射弹\n\n14.  Navigate to the `Viewport` tab within the Blueprint to view these changes to the `Transform`. `ProjectileEffect` should look as follows:\n\n    ![Figure 14.23: Now, the fire VFX has been scaled and rotated appropriately ](img/B16183_14_23.jpg)\n\n    图 14.23:现在，火 VFX 已经被适当地缩放和旋转\n\n15.  现在 VFX 已经设置好了，*左键单击`ProjectileMovementSound`组件并将`A_Ambient_Fire01_Cue`分配给该组件。*\n16.  Save and recompile the `BP_PlayerProjectile` Blueprint. Use `PIE` and observe that when you throw the projectile, it now shows the VFX asset and plays the assigned sound:\n\n    ![Figure 14.24: The player projectile now has a VFX and an SFX as it flies through the air ](img/B16183_14_24.jpg)\n\n图 14.24:玩家投掷物在空中飞行时现在有一个 VFX 和一个 SFX\n\n完成这个练习后，玩家投射物现在有了一个 VFX 和一个 SFX，当它在空中飞行时可以一起玩。这些元素使射弹栩栩如生，使射弹使用起来更加有趣。\n\n由于 VFX 和 SFX 是作为射弹的部件制造的，所以当射弹被摧毁时，它们也会被摧毁。\n\n在下一个练习中，您将在`Throw`动画蒙太奇中添加粒子通知和声音通知，以便在玩家投掷玩家投射物时提供更多的冲击力。\n\n## 练习 14.11:添加 VFX 和 SFX 通知\n\n到目前为止，您已经通过 C++ 实现了游戏的波兰元素，这是一种有效的实现方式。为了丰富多彩，并扩展您对虚幻引擎 4 工具集的了解，本练习将指导您如何使用动画蒙太奇中的通知在动画中添加粒子系统和音频。我们开始吧！\n\n与前面的练习非常相似，我们需要将资产从`Action RPG`项目迁移到我们的`Super SideScroller`项目。请参考*练习 14.09* 、*消灭敌人时添加效果*，了解如何从`Action RPG`项目安装和迁移资产。请执行以下步骤:\n\n1.  Open the `ActionRPG` project and navigate to the `Content Browser` interface.\n\n    您要添加到项目中的两个主要资产如下:\n\n    ```cpp\n    P_Skill_001\n    A_Ability_FireballCast_Cue\n    ```\n\n    `P_Skill_001`粒子系统资产引用*材质*和*纹理*等包含在`Effects`目录中的附加资产，而`A_Ability_FireballCast_Cue`引用包含在`Assets`目录中的附加*声波*资产。\n\n    投掷弹丸时你将为玩家使用的粒子叫做`P_Skill_001`，可以在`/Effects/FX_Particle/`目录中找到。这与之前练习中`P_Goblin_Death`和`P_Env_Fire_Grate_01` VFX 资产使用的目录相同。你用来破坏敌人角色的声音叫做`A_Ambient_Fire01_Cue`，可以在`/img/Sounds/Ambient/`目录中找到。\n\n2.  *在`Action RPG`项目的`Content Browser`界面中右键单击这些资产中的每一个*，选择`Asset Actions`，然后选择`Migrate`。\n3.  Make sure to choose the directory of the `Content` folder for your `Super SideScroller` project before confirming the migration.\n\n    现在您需要的资产已经迁移到您的项目中，让我们继续向`AM_Throw`资产添加所需的通知。在继续本练习之前，请确保返回到您的`Super SideScroller`项目。\n\n4.  从`Content Browser`界面，导航至`/MainCharacter/Animation/`目录。找到`AM_Throw`资产，*双击*打开。\n5.  在`Animation Montage`编辑器中心的预览窗口下方，找到`Notifies`部分。这就是你在本章前面添加`Anim_ProjectileNotify`的部分。\n6.  To the right of the `Notifies` track, you will find a `+` sign that allows you to use additional notify tracks. *Left-click* to add a new track, as shown in the following screenshot:\n\n    ![Figure 14.25: It is useful to add multiple tracks to the timeline in order to keep things organized when adding multiple notifies ](img/B16183_14_25.jpg)\n\n    图 14.25:为了在添加多个通知时保持事情有条不紊，在时间线中添加多个轨道是很有用的\n\n7.  在与`Anim_ProjectileNotify`相同的帧中，*右键单击您在上一步中创建的新轨迹内的*。从`Add Notify`列表中，*左键单击*选择`Play Particle Effect`。\n8.  Once created, *left-click* to select the new notify and access its `Details` panel. In `Details`, add the `P_Skill_001` VFX asset to the `Particle System` parameter.\n\n    在你添加了这个新的 VFX 之后，你会注意到 VFX 几乎被放在底部，玩家角色的脚在那里，但不是你想要的地方。这个 VFX 应该直接放在地板上，或者角色的底部。下面的截图展示了这个位置:\n\n    ![Figure 14.26: The location of the particle notify is not on the ground ](img/B16183_14_26.jpg)\n\n    图 14.26:粒子通知的位置不在地面上\n\n    为了解决这个问题，你需要给玩家角色骨架增加一个新的`Socket`。\n\n9.  导航至`/MainCharacter/Mesh/`目录。*双击`MainCharacter_Skeleton`资产打开*。\n10.  从左侧的`Skeleton`骨骼层次中，*右键单击`Hips`骨骼上的*，左键单击*选择`Add Socket`选项。命名这个新插座`EffectSocket`。*\n11.  *Left-click* this socket from the hierarchy of bones in order to view its current location. By default, its location is set to the same position as the `Hips` bone. The following screenshot shows this location:\n\n    ![Figure 14.27: The default location of this socket is in the center of the player skeleton ](img/B16183_14_27.jpg)\n\n    图 14.27:这个插座的默认位置在玩家骨架的中心\n\n    使用`Transform`小控件，移动`EffectSocket`的位置，使其位置设置如下:\n\n    ```cpp\n    (X=0.000000,Y=100.000000,Z=0.000000)\n    ```\n\n    这个位置会更靠近地面和玩家角色的脚。最终位置可以在下面的截图中看到:\n\n    ![Figure 14.28: Moving the socket location to the base of the player skeleton ](img/B16183_14_28.jpg)\n\n    图 14.28:将插座位置移动到玩家骨架的底部\n\n12.  现在你已经有了粒子通知的位置，回到`AM_Throw`动画蒙太奇。\n13.  Within the `Details` panel of the `Play Particle Effect` notify, there is the `Socket Name` parameter. Use `EffectSocket` as the name.\n\n    注意\n\n    如果`EffectSocket`没有通过自动完成功能出现，关闭并重新打开动画蒙太奇。一旦重新打开，`EffectSocket`选项应该会出现。\n\n14.  Lastly, the scale of the particle effect is a little too big, so adjust the scale of the projectile so that its value is as follows:\n\n    ```cpp\n    (X=0.500000,Y=0.500000,Z=0.500000)\n    ```\n\n    现在，当粒子效果通过此通知播放时，其位置和比例将是正确的，如下所示:\n\n    ![Figure 14.29: The particle now plays at the base of the player character skeleton ](img/B16183_14_29.jpg)\n\n    图 14.29:粒子现在在玩家角色骨架的底部播放\n\n15.  若要添加`Play Sound`通知，请在`Notifies`时间线部分添加新的轨道；你现在应该总共有三个。\n16.  On this new track, and at the same frame position as both the `Play Particle Effect` and `Anim_ProjectileNotify` notifies, *right-click* and select the `Play Sound` notify from the `Add Notify` selection. The following screenshot shows where to find this notify:\n\n    ![Figure 14.30: The Play Sound notify that you learned about earlier in this chapter ](img/B16183_14_30.jpg)\n\n    图 14.30:播放声音通知您在本章前面已经了解到\n\n17.  接下来，*左键点击*选择`Play Sound`通知并进入其`Details`面板。\n18.  From the `Details` panel, find the `Sound` parameter and assign `A_Ability_FireballCast_Cue`.\n\n    有了指定的声音，当`Throw`动画回放时，你会看到 VFX 的戏，你会听到声音。`Notifies`轨迹应如下所示:\n\n    ![Figure 14.31: The final notify set up on the Throw Animation Montage timeline ](img/B16183_14_31.jpg)\n\n    图 14.31:投掷动画蒙太奇时间线上的最终通知设置\n\n19.  保存`AM_Throw`资产，使用`PIE`投掷玩家弹丸。\n20.  Now, when you throw the projectile, you will see the particle notify play the `P_Skill_001` VFX and you will hear the `A_Ability_FireballCast_Cue` SFX. The result will look as follows:\n\n    ![Figure 14.32: Now, when the player throws the projectile, powerful VFX and SFX are played ](img/B16183_14_32.jpg)\n\n图 14.32:现在，当玩家投掷投射物时，会打出强大的 VFX 和 SFX\n\n完成最后一个练习后，当玩家投掷投射物时，玩家现在可以玩强大的 VFX 和 SFX。这给了投掷动画更多的力量，感觉像是玩家角色在使用大量的能量来投掷弹丸。\n\n在接下来的最后一个活动中，你将使用你从最后几个练习中获得的知识，在玩家投射物被摧毁时，将 VFX 和 SFX 添加到其中。\n\n## 活动 14.02:增加炮弹被摧毁时的效果\n\n在这最后一个活动中，你将使用你从给玩家投掷物和敌人角色添加 VFX 和 SFX 元素中获得的知识来为投掷物与物体碰撞时创建爆炸效果。我们增加这种额外爆炸效果的原因是为了在炮弹与环境物体碰撞时摧毁炮弹的基础上增加一层抛光。如果玩家的抛射物击中一个物体并消失而没有玩家的任何听觉或视觉反馈，这看起来会很尴尬和不合适。\n\n您将向玩家投射物添加粒子系统和声音提示参数，并在投射物与对象碰撞时产生这些元素。\n\n执行以下步骤以实现预期的输出:\n\n1.  在`PlayerProjectile.h`头文件中，添加一个新的粒子系统变量和一个新的声音基础变量。\n2.  命名粒子系统变量`DestroyEffect`，命名声音基础变量`DestroySound`。\n3.  在`PlayerProjectile.cpp`源文件中，将`UGameplayStatics`的包含添加到包含列表中。\n4.  更新`APlayerProjectile::ExplodeProjectile()`功能，使其现在同时生成`DestroyEffect`和`DestroySound`对象。返回到虚幻引擎 4 编辑器，重新编译新的 C++ 代码。在`BP_PlayerProjectile`蓝图中，将默认情况下已经包含在您的项目中的`P_Explosion` VFX 指定给射弹的`Destroy Effect`参数。\n5.  将默认情况下已经包含在项目中的`Explosion_Cue` SFX 分配给投射体的`Destroy Sound`参数。\n6.  保存并编译玩家投射物蓝图。\n7.  Use `PIE` to observe the new player projectile's destruction VFX and SFX.\n\n    预期产出如下:\n\n    ![Figure 14.33: Projectile VFX and SFX ](img/B16183_14_33.jpg)\n\n图 14.33:射弹 VFX 和 SFX\n\n完成这项活动后，您现在有了在游戏中添加波兰元素的经验。您不仅通过 C++ 代码添加了这些元素，还通过虚幻引擎 4 中的其他工具添加了元素。在这一点上，你有足够的经验将粒子系统和音频添加到你的游戏中，而不必担心如何实现这些功能。\n\n注意\n\n这个活动的解决方案可以在:[https://packt.live/338jEBx](https://packt.live/338jEBx)找到。\n\n# 总结\n\n在这一章中，你学到了很多关于视觉和听觉效果在游戏开发世界中的重要性。使用 C++ 代码和通知的组合，您能够为玩家投射体和敌人角色碰撞带来游戏功能，并通过添加 VFX 和 SFX 为该功能增加一层润色。除此之外，你还在虚幻引擎 4 中学习了对象是如何产生和毁灭的。\n\n此外，您还从蓝图和 C++ 中了解了动画蒙太奇是如何播放的。通过将播放`Throw`动画蒙太奇的逻辑从蓝图迁移到 C++，您学习了这两种方法是如何工作的，以及如何在您的游戏中使用这两种实现。\n\n通过使用 C++ 添加新的动画通知，您可以将此通知添加到`Throw`动画蒙太奇中，这允许玩家生成您在上一章中创建的玩家投射体。通过使用`UWorld->SpawnActor()`功能，并为玩家骨骼添加一个新的插座，您可以在`Throw`动画的精确帧和您想要的精确位置生成玩家投射物。\n\n最后，您学习了如何在投掷动画蒙太奇中使用`Play Particle Effect`和`Play Sound`通知将 VFX 和 SFX 添加到玩家投掷物中。这一章给了你机会去了解虚幻引擎 4 中存在的不同方法，当你在游戏中使用 VFX 和 SFX 的时候。\n\n现在玩家可以投掷投射物并摧毁敌人角色，是时候为游戏实施最后一套机制了。在下一章中，你将创建玩家可以收集的收藏品，你还将为玩家创建一个能在短时间内提高玩家运动力学的动力。"
  },
  {
    "path": "docs/game-dev-proj-ue/14.md",
    "content": "# 十五、收藏品、加强和拾取\n\n概观\n\n在这一章中，我们将为玩家创造可收集的硬币和药剂。此外，我们将使用虚幻运动图形用户界面设计器为可收集的硬币设计用户界面，或者更常见的 UMG。最后，我们将制作砖块，将这些收藏品藏在里面。到本章结束时，你将能够在关卡环境中为玩家角色实现收藏品和启动。\n\n# 简介\n\n在前一章中，您创建了玩家投射体，并在`Throw`动画期间使用`Anim Notifies`来生成玩家投射体。玩家投射物将作为玩家的主要进攻游戏机制，在整个关卡中用来对抗敌人。由于虚幻引擎 4 提供的默认`Anim Notifies`和你自己定制的`Anim_ProjectileNotify`职业的结合，玩家弹丸机械师看起来和感觉都很棒。\n\n我们需要开发的最后一套机制是硬币收集和药剂启动。让我们简要地分析一下收藏品和启动是如何影响其他游戏的，以及它们将为我们的`SuperSideScroller`游戏带来什么。\n\n**硬币收藏品**\n\n收藏品给了玩家一个彻底探索关卡的动力。在许多游戏中，比如*空心骑士*，收藏品也是一种货币形式，可以用来为你的角色和物品购买升级。在其他情况下，更经典的平台，如超级马里奥或索尼克，收藏品可以在玩家穿越关卡时提高玩家的分数。\n\n在当今的游戏格局中，人们期望游戏包含成就。收藏品是将成就融入游戏的好方法；例如，收集一个关卡或整个游戏中所有硬币的成就。对于`SuperSideScroller`游戏，硬币收藏品将作为玩家探索游戏关卡以找到尽可能多的硬币的令人满意的手段。\n\n**药水加电**\n\n加电给玩家提供了永久或暂时的优势来对抗敌人或玩家必须穿越的环境。有许多利用加电的游戏例子，其中最著名的是 Metroid 系列。Metroid 使用加电来让玩家探索新的区域并与更强的敌人战斗。\n\n加电也是将成就融入游戏的另一种方式。例如，你可以有一个成就来摧毁特定数量的敌人与特定的权力。对于`SuperSideScroller`游戏，药水加电将作为一种手段，通过提高玩家的移动速度和跳跃高度来提高玩家在关卡环境中的导航能力。\n\n在这一章中，你将学习如何使用 C++ 创建硬币收藏品和药水加电，为`SuperSideScroller`游戏增加更多的游戏性。这些游戏元素将从你将创建的同一个基础`actor`类中派生出来。您还将为收藏品和电源添加视觉和听觉元素，使它们更加精美。\n\n为了让硬币收藏和药剂加电对玩家来说更具视觉趣味性，我们将为这些演员添加旋转组件，以吸引玩家对他们的注意力。这是`URotatingMovementComponent`非常有用的地方；它允许我们以一种非常优化和直接的方式向演员添加旋转，而不是编写我们自己的逻辑来处理演员的不断旋转。让我们从了解这个组件开始。\n\n# 尿调节运动组件\n\n`URotatingMovementComponent`是虚幻引擎 4 中存在的少数运动组件之一。单从`SuperSideScroller`游戏项目来看`CharacterMovementComponent` 和`ProjectileMovementComponent`你已经很熟悉了，`RotatingMovementComponent`也不过如此——另一个动作组件。作为复习，动作组件允许不同类型的动作发生在他们所属的演员或角色身上。\n\n注意\n\n`CharacterMovementComponent`，可以控制角色的移动速度和跳跃高度等移动参数，在*第 10 章*、*创建超视频 Scroller Game* 中有介绍，当时你创建了`SuperSideScroller`玩家角色。`ProjectileMovementComponent`允许你为演员添加基于投射物的移动功能，如速度和重力，在你开发玩家投射物时，在*第 14 章*、*生成玩家投射物*中有所介绍。\n\n`RotatingMovementComponent`与`CharacterMovementComponent`相比是一个非常简单的运动组件，那是因为它只涉及到旋转`RotatingMovementComponent`所属的演员；仅此而已。`RotatingMovementComponent`基于定义的`Rotation Rate`、枢轴平移和在局部空间或世界空间中使用旋转的选项来执行组件的连续旋转。\n\n此外，`RotatingMovementComponent`比其他轮换演员的方法更有效，例如通过蓝图中的`Event Tick`或`Timelines`。\n\n注意\n\n更多关于运动组件的信息可以在这里找到:[https://docs . unrealengine . com/en-US/Engine/Components/Movement/index . html # rotating Movement component](https://docs.unrealengine.com/en-US/Engine/Components/Movement/index.html#rotatingmovementcomponent)。\n\n我们将使用`RotatingMovementComponent`来允许硬币收集和药剂加电沿着偏航轴原地旋转。这种旋转将吸引玩家对收藏品的注意，并给他们一个视觉提示，即收藏品很重要。\n\n现在你对`RotatingMovementComponent`有了更好的理解，让我们继续创建`PickableActor_Base`类，这是硬币收集和药剂加电的来源。\n\n## 练习 15.01:创建可选择的 actor _ 基类并添加尿统计移动组件\n\n在本练习中，您将创建`PickableActor_Base`演员类，该类将被用作可收集硬币和药剂加电的基础类。您还将从这个 C++ 基类创建一个蓝图类来预览`URotatingMovementComponent`是如何工作的。按照以下步骤完成本练习:\n\n注意\n\n在整个`SuperSideScroller`游戏项目中，您已经无数次执行了以下许多步骤，因此将有有限的图像来帮助指导您。只有在引入一个新概念的时候，才会有一个伴随的形象。\n\n1.  在虚幻引擎 4 编辑器中，*左键单击编辑器左上角的*`File`选项，*左键单击`New C++ Class`选项。*\n2.  在`Choose Parent Class`窗口中，选择`Actor`选项，然后在该窗口底部的`Next`按钮上*左键单击*。\n3.  命名这个类`PickableActor_Base`并保持默认的`Path`目录不变。然后，选择该窗口底部的`Create Class`按钮。\n4.  选择`Create Class`按钮后，虚幻引擎 4 将重新编译项目代码，并自动打开带有`PickableActor_Base`类头文件和源文件的 Visual Studio。\n5.  默认情况下，`Actor`类在头文件中为您提供了`virtual void Tick(float DeltaTime) override;`函数声明。出于`PickableActor_Base`类的目的，我们将不需要`Tick`函数，因此从`PickableActor_Base.h`头文件中删除该函数声明。\n6.  Next, you will also need to remove the function from the `PickableActor_Base.cpp` file; otherwise, you will receive a compile error. In this source file, find and remove the following code:\n\n    ```cpp\n    void PickableActor_Base::Tick(float DeltaTime)\n    {\n      Super::Tick(DeltaTime);\n    }\n    ```\n\n    注意\n\n    在许多情况下，使用`Tick()`功能进行移动更新会导致性能问题，因为`Tick()`功能在每一帧都被调用。相反，尝试使用`Gameplay Timer`功能以指定的时间间隔执行特定的更新，而不是在每一帧上。您可以在这里了解更多关于`Gameplay Timers`的信息:[。](https://docs.unrealengine.com/en-US/Programming/UnrealArchitecture/Timers/index.html)\n\n7.  Now, it is time to add the components that the `PickableActor_Base` class requires. Let's start with `USphereComponent`, which you will use to detect overlap collision with the player. Add the following code underneath the `Protected` access modifier inside the `PickableActor_Base.h` header file:\n\n    ```cpp\n    UPROPERTY(VisibleDefaultsOnly, Category = PickableItem)\n    class USphereComponent* CollisionComp;\n    ```\n\n    `USphereComponent`的宣言到现在你应该很熟悉了；我们在之前的章节中已经这样做了，比如*第 16 章*、*多人基础*，当时我们创建了`PlayerProjectile`类。\n\n8.  接下来，在`USphereComponent`的声明下面添加下面的代码来创建一个新的`UStaticMeshComponent`。这将用于直观地表示可收集的硬币或药剂的启动:\n\n    ```cpp\n    UPROPERTY(VisibleDefaultsOnly, Category = PickableItem)\n    class UStaticMeshComponent* MeshComp;\n    ```\n\n9.  最后，在`UStaticMeshComponent`的声明下添加以下代码，创建一个新的`URotatingMovementComponent`。这将用于给可收集的硬币和药剂加电简单旋转运动:\n\n    ```cpp\n    UPROPERTY(VisibleDefaultsOnly, Category = PickableItem)\n    class URotatingMovementComponent* RotationComp;\n    ```\n\n10.  现在您已经在`PickableActor_Base.h`头文件中声明了组件，导航到`PickableActor_Base.cpp`源文件，以便您可以为这些添加的组件添加所需的`#includes`。在源文件顶部的第一个`#include \"PickableActor_Base.h\"`后添加以下几行:\n\n    ```cpp\n    #include \"Components/SphereComponent.h\"\n    #include \"Components/StaticMeshComponent.h\"\n    #include \"GameFramework/RotatingMovementComponent.h\"\n    ```\n\n11.  现在您已经有了组件所需的`include`文件，您可以在`APickableActor_Base::APickableActor_Base()`构造函数中添加必要的代码来初始化这些组件:\n\n    ```cpp\n    APickableActor_Base::APickableActor_Base()\n    {\n    }\n    ```\n\n12.  首先，通过向`APickableActor_Base::APickableActor_Base()`添加以下代码来初始化`USphereComponent`组件变量`CollisionComp`:\n\n    ```cpp\n    CollisionComp = CreateDefaultSubobject   <USphereComponent>(TEXT(\"SphereComp\"));\n    ```\n\n13.  接下来，通过在上一步提供的代码下面添加以下代码，用默认的球体半径`30.0f`初始化【T0:\n\n    ```cpp\n    CollisionComp->InitSphereRadius(30.0f);\n    ```\n\n14.  由于玩家角色需要与该组件重叠，您需要添加以下代码，以便在默认情况下，`USphereComponent`具有`Overlap All Dynamic`的碰撞设置:\n\n    ```cpp\n    CollisionComp->BodyInstance.SetCollisionProfileName(\"OverlapAllDynamic\");\n    ```\n\n15.  最后，`CollisionComp USphereComponent`应该是这个演员的根成分。添加以下代码进行分配:\n\n    ```cpp\n    RootComponent = CollisionComp;\n    ```\n\n16.  Now that `CollisionComp USphereComponent` has been initialized, let's do the same for `MeshComp UStaticMeshComponent`. Add the following code. After, we'll discuss what the code is doing for us:\n\n    ```cpp\n    MeshComp = CreateDefaultSubobject<UStaticMeshComponent>(TEXT(\"MeshComp\"));\n    MeshComp->AttachToComponent(RootComponent,   FAttachmentTransformRules::KeepWorldTransform);\n    MeshComp->SetCollisionEnabled(ECollisionEnabled::NoCollision);\n    ```\n\n    第一行使用`CreateDefaultSubobject()`模板函数初始化`MeshComp UStaticMeshComponent`。接下来，您将使用`AttachTo()`功能将`MeshComp`附加到您为`CollisionComp`制作的根组件上。最后，`MeshComp UStaticMeshComponent`在默认情况下不应该有任何冲突，所以您正在使用`SetCollisionEnabled()`函数并传入`ECollisionEnable::NoCollision`枚举值。\n\n17.  最后，我们可以通过添加以下代码来初始化【T0:\n\n    ```cpp\n    RotationComp =   CreateDefaultSubobject<URotatingMovementComponent>(TEXT(\"RotationComp\"));\n    ```\n\n18.  初始化所有组件后，编译 C++ 代码并返回到虚幻引擎 4 编辑器。编译成功后，您将继续为`PickableActor_Base`创建蓝图类。\n19.  在`Content Browser`窗口中，通过右键单击`Content`文件夹中的并选择`New Folder`选项，创建一个名为`PickableItems`的新文件夹。\n20.  在`PickableItems`文件夹中，*右键单击*，选择`Blueprint Class`。从`Pick Parent Class`窗口，搜索`PickableActor_Base`类和*左键单击* `Select`创建新蓝图。\n21.  命名此蓝图`BP_PickableActor_Base`和*双击*打开蓝图。\n22.  In the `Components` tab, select `MeshComp Static Mesh Component` and assign the `Shape_Cone` static mesh to the `Static Mesh` parameter in the `Details` panel. Please refer to the following screenshot:\n\n    ![Figure 15.1: The Shape_Cone mesh assigned to MeshComp UStaticMeshComponent of the BP_Pickable_Base actor class](img/B16183_15_01.jpg)\n\n    图 15.1:分配给 BP _ 可选择 _ 基本执行元类的网格组件的形状 _ 圆锥网格\n\n23.  接下来，选择`RotationComp` `URotatingMovementComponent`，在`Details`面板的`Rotating Component`类别下找到`Rotation Rate`参数。\n24.  Set `Rotation Rate` to the following values:\n\n    ```cpp\n    (X=100.000000,Y=100.000000,Z=100.000000)\n    ```\n\n    这些值决定了演员每秒沿着每个轴旋转的速度。这意味着锥形演员将在每个轴上以每秒 100 度的速度沿着每个轴旋转。\n\n25.  编译`PickableActor_Base`蓝图，把这个演员加到你的级别。\n26.  Now, if you use PIE and look at the `PickableActor_Base` actor in the level, you will see that it is now rotating. Please refer to the following screenshot:\n\n    ![Figure 15.2: Now, the Cone mesh rotates along all the axes, as per the values we added to the Rotation Rate window of URotatingMovementComponent ](img/B16183_15_02.jpg)\n\n图 15.2:现在，圆锥体网格沿着所有的轴旋转，就像我们添加到泌尿运动组件的旋转速率窗口中的值一样\n\n注意\n\n您可以在这里找到本练习的资产和代码:[https://packt.live/3njhwyt](https://packt.live/3njhwyt)。\n\n完成本练习后，您已经创建了`PickableActor_Base`类所需的基本组件，并学习了如何实现和使用`URotatingMovementComponent`。随着`PickableActor_Base`类的准备，以及`URotatingMovementComponent`在蓝图演员上的实现，我们可以通过添加重叠检测功能来完成这个类，销毁可收集的演员，并在演员被玩家拾取时产生音频效果。在以下活动中，您将添加`PickableActor_Base`课程所需的其余功能。\n\n## 活动 15.01:玩家重叠检测和在 PickableActor _ Base 中产生效果\n\n现在`PickableActor_Base`类已经有了所有需要的组件，并且有了初始化组件的构造函数，是时候添加其功能的剩余方面了。这些将在本章后面的硬币收藏和药剂启动中继承。这个额外的功能包括玩家重叠检测，破坏可收集的演员，并产生一个音频效果，给玩家反馈，它已经被成功地拿起。执行以下步骤添加功能，当收藏品与播放器重叠时，允许播放`USoundBase`类对象:\n\n1.  在`PickableActor_Base`类中创建一个新的函数，将对玩家的引用作为输入参数。调用此功能`PlayerPickedUp`。\n2.  创建一个名为`BeginOverlap()`的新`UFUNCTION`。继续之前，请确保包含该功能所需的所有输入参数。参考*第 6 章*、*碰撞物体*，在`VictoryBox`类中使用了这个功能。\n3.  为`USoundBase`类添加一个新的`UPROPERTY()`，并将其命名为`PickupSound`。\n4.  在`PickableActor_Base.cpp`源文件中，为`BeginOverlap()`和`PlayerPickedUp()`函数创建定义。\n5.  现在，在源文件的顶部添加`SuperSideScroller_Player`类和`GameplayStatics`类所需的`#include`文件。\n6.  在`BeginOverlap()`功能中，使用该功能的`OtherActor`输入参数创建对玩家的引用。\n7.  在`PlayerPickedUp()`函数中，为`GetWorld()`函数返回的`UWorld*`对象创建一个变量。\n8.  使用`UGameplayStatics`库在`PickableActor_Base`演员的位置生成`PickUpSound`。\n9.  然后，调用`Destroy()`函数，让演员被毁灭，从这个世界上消失。\n10.  最后，在`APickableActor_Base::APickableActor_Base()`构造函数中，将`CollisionComp`的`OnComponentBeginOverlap`事件绑定到`BeginOverlap()`函数。\n11.  从`Epic Games Launcher`的`Learn`选项卡下载安装`Unreal Match 3`项目。使用您在*第 14 章*、*中获得的知识，将`Match_Combo`声波资产从该项目迁移到您的`SuperSideScroller`项目中，生成玩家投射体*。\n12.  将此声音应用于`BP_PickableActor_Base` 蓝图的`PickupSound`参数。\n13.  编译蓝图，如果你的等级中没有蓝图，现在在你的等级中添加`BP_PickableActor_Base`演员。\n14.  In `PIE`, have your character overlap with the `BP_PickableActor_Base` actor.\n\n    预期产出:\n\n    ![Figure 15.3: The BP_PickableActor_Base object can be overlapped  and picked up by the player ](img/B16183_15_03.jpg)\n\n图 15.3:BP _ pickle actor _ Base 对象可以被玩家重叠拾取\n\n注意\n\n这个活动的解决方案可以在:[https://packt.live/338jEBx](https://packt.live/338jEBx)找到。\n\n完成本活动后，您已经证明了您对如何将`OnBeginOverlap()`功能添加到您的参与者类以及如何使用该功能为您的参与者执行逻辑的了解。在`PickableActor_Base`的情况下，我们添加了逻辑，它将产生一个定制的声音并摧毁演员。\n\n现在`PickableActor_Base`类已经设置好了，是时候开发可收集的硬币和由此衍生的启动药剂类了。硬币收藏类将继承您刚刚创建的`PickableActor_Base`类。它将覆盖关键功能，如`PlayerPickedUp()`功能，这样当玩家捡到收藏品时，我们可以为其实现独特的逻辑。除了覆盖继承的父级`PickableActor_Base`类的功能之外，硬币收藏类还将拥有自己独特的属性集，例如其当前硬币价值和独特的拾音。在下一个练习中，我们将一起创建硬币收藏类。\n\n## 练习 15.02:创建可选择的参与者 _ 可收集的类\n\n在本练习中，您将创建`PickableActor_Collectable`类，该类将从您在*练习 15.01* 、*中创建的`PickableActor_Base`类派生，创建可选择演员 _ 基础类并添加泌尿运动组件*，并在*活动 15.01* 、*中完成可选择演员 _ 基础中的玩家重叠检测和产卵效果。这个职业将作为玩家在关卡中可以收集的主要可收集硬币。按照以下步骤完成本练习:*\n\n1.  在虚幻引擎 4 编辑器中，*左键单击编辑器左上角的*`File`选项，*左键单击`New C++ Class`选项。*\n2.  在`Choose Parent Class`窗口中，选择`PickableActor_Base`选项，然后在该窗口底部的`Next`按钮上*左键单击*。\n3.  命名这个类`PickableActor_Collectable`并保持默认的`Path`目录不变。然后，选择该窗口底部的`Create Class`按钮。\n4.  选择`Create Class`按钮后，虚幻引擎 4 将重新编译项目代码，并自动打开带有`PickableActor_Collectable`类头文件和源文件的 Visual Studio。\n5.  By default, the `PickableActor_Collectable.h` header file has no declared functions or variables within its class declaration. You will need to add the override for the `BeginPlay()` function underneath a new `Protected Access Modifier`. Add the following code:\n\n    ```cpp\n    protected:\n      virtual void BeginPlay() override;\n    ```\n\n    我们覆盖`BeginPlay()`函数的原因是`URotatingMovementComponent`需要演员初始化并使用`BeginPlay()`来正确旋转演员。因此，我们需要创建这个函数的重写声明，并在源文件中创建一个基本定义。然而，首先，我们需要覆盖`PickableActor_Base`父类的另一个重要函数。\n\n6.  Override the `PlayerPickedUp()` function from the `PickableActor_Base` parent class by adding the following code under `Protected Access Modifier`:\n\n    ```cpp\n    virtual void PlayerPickedUp(class ASuperSideScroller_Player* Player)   override;\n    ```\n\n    因此，我们说我们将使用并覆盖`PlayerPickedUp()`函数的功能。\n\n7.  Lastly, create a new integer called `UPROPERTY()` that will hold the value that the coin collectible will have; in this case, it will have a value of `1`. Add the following code to do this:\n\n    ```cpp\n    public:\n      UPROPERTY(EditAnywhere, Category = Collectable)\n      int32 CollectableValue = 1;\n    ```\n\n    在这里，我们正在创建整数变量，该变量将在蓝图中可访问，默认值为`1`。如果你这样选择，用`EditAnywhere UPROPERTY()`关键字，你可以改变一个硬币收藏品的价值。\n\n8.  现在，我们可以进入`PickableActor_Collectable.cpp`源文件，创建被覆盖的`PlayerPickedUp()`函数的定义。在源文件中添加以下代码:\n\n    ```cpp\n    void APickableActor_Collectable::PlayerPickedUp(class   ASuperSideScroller_Player* Player)\n    {\n    }\n    ```\n\n9.  For now, we need to make a call to the `PlayerPickedUp()` parent function by using the `Super` keyword. Add the following code to the `PlayerPicked()` function:\n\n    ```cpp\n    Super::PlayerPickedUp(Player);\n    ```\n\n    使用`Super::PlayerPickedUp(Player)`对父函数的调用将确保您在`PickableActor_Base`类中创建的功能被调用。您可能还记得，父类中的`PlayerPickedUp()`函数调用生成`PickupSound`声音对象并销毁该演员。\n\n10.  接下来，通过添加以下代码在源文件中创建`BeginPlay()`函数的定义:\n\n    ```cpp\n    void APickableActor_Collectable::BeginPlay()\n    {\n    }\n    ```\n\n11.  在 C++ 中，这里要做的最后一件事是使用`Super`关键字再次调用`BeginPlay()`父函数。将以下代码添加到`PickableActor_Collectable`类内的`BeginPlay()`函数中:\n\n    ```cpp\n    Super::BeginPlay();\n    ```\n\n12.  Compile the C++ code and return to the editor.\n\n    注意\n\n    您可以在以下链接找到本练习的资产和代码:[https://packt.live/35fRN3E](https://packt.live/35fRN3E)。\n\n现在您已经成功编译了`PickableActor_Collectable`类，您已经创建了硬币收藏品所需的框架。在下面的活动中，您将从这个类创建一个蓝图，并最终确定硬币可收集的演员。\n\n## 活动 15.02:最终确定可选择的演员 _ 可收集的演员\n\n现在`PickableActor_Collectable`类已经拥有了它需要的所有必要的继承功能和独特属性，是时候从这个类创建蓝图并添加一个`Static Mesh`，更新它的`URotatingMovementComponent`，并对`PickUpSound`属性应用一个声音了。执行以下步骤最终确定`PickableActor_Collectable`演员:\n\n1.  从`Epic Games Launcher`开始，在`Engine Feature Samples`类别下的`Learn`选项卡中找到`Content Examples`项目。\n2.  从`Content Examples`项目创建并安装一个新项目。\n3.  将`SM_Pickup_Coin`资产及其所有引用的资产从`Content Examples`项目迁移到您的`SuperSideScroller`项目。\n4.  在`Content Browser`窗口的`Content/PickableItems`目录中创建新文件夹，并将其命名为`Collectable`。\n5.  在这个新的`Collectable`文件夹中，从您在*练习 15.02**中创建的`PickableActor_Collectable`类创建一个新的蓝图，创建可选择的 _ Collectable 类*。命名这个新蓝图`BP_Collectable`。\n6.  在此蓝图中，将`MeshComp`组件的`Static Mesh`参数设置为您在本活动前面导入的`SM_Pickup_Coin`网格。\n7.  接下来，将`Match_Combo`声音资产添加到收藏品的`PickupSound`参数中。\n8.  最后，更新`RotationComp`组件，使演员以每秒 90 度的速度沿 Z 轴旋转。\n9.  编译蓝图，将`BP_Collectable`放入你的关卡，使用 PIE。\n10.  Overlap the player character with the `BP_Collectable` actor and observe the results.\n\n    预期产出:\n\n    ![Figure 15.4: The coin collectible rotates and can be overlapped by the player ](img/B16183_15_04.jpg)\n\n图 15.4:可收集的硬币旋转，玩家可以重叠\n\n注意\n\n这个活动的解决方案可以在:[https://packt.live/338jEBx](https://packt.live/338jEBx)找到。\n\n完成本活动后，您已经证明了您知道如何将资产迁移到您的虚幻项目中，以及如何使用和更新`URotatingMovementComponent`来满足硬币收藏品的需求。既然硬币收集演员已经完成，是时候给玩家添加功能了，这样玩家就可以记录他们收集了多少硬币。\n\n首先，我们将创建逻辑，使用`UE_LOG`计算硬币，然后在游戏的用户界面上使用 UMG 实现硬币计数器。\n\n# 使用用户设备日志记录变量\n\n在*第 11 章*、*混合空间 1D、键绑定和状态机*中，我们使用并了解了`UE_LOG`功能，以便记录玩家应该何时投掷弹丸。然后我们使用*第 13 章*、*敌方人工智能*中的`UE_LOG`功能，记录玩家弹丸击中物体的时间。`UE_LOG`是一个强大的日志工具，我们可以使用它在玩游戏时将 C++ 函数中的重要信息输出到编辑器内的`Output Log`窗口中。到目前为止，我们只记录了`FStrings`在`Output Log`窗口显示一般文本，以知道我们的函数被调用。现在，是时候学习如何记录变量，以便调试玩家已经收集了多少硬币。\n\n注意\n\n在带有虚幻引擎 4 的 C++ 中还有另一个有用的调试功能，称为`AddOnScreenDebugMessage`。你可以在这里了解更多关于这个函数的信息。\n\n当创建`TEXT()`宏使用的`FString`语法时，我们可以添加格式说明符来记录不同类型的变量。我们将只讨论如何为整数变量添加格式说明符。\n\n注意\n\n通过阅读以下文档，您可以找到关于如何指定其他变量类型的更多信息:[https://www.ue4community.wiki/Logging#Logging_an_FString](https://www.ue4community.wiki/Logging#Logging_an_FString)。\n\n这是`UE_LOG()`经过`FString \"Example Text\"`时的样子:\n\n```cpp\nUE_LOG(LogTemp, Warning, TEXT(\"Example Text\"));\n```\n\n这里有`Log Category`、`Log Verbose Level`和实际的`FString`、 `\"Example Text\"`，显示在日志中。要记录一个整数变量，您需要在`TEXT()`宏内将`%d`添加到您的`FString`中，然后在`TEXT()`宏外添加整数变量名，用逗号分隔。这里有一个例子:\n\n```cpp\nUE_LOG(LogTemp, Warning, TEXT(\"My integer variable %d), MyInteger);\n```\n\n格式说明符由`%`符号标识，每个变量类型都有一个与之对应的指定字母。在整数的情况下，使用字母`d`。在下一个练习中，您将使用这种记录整数变量的方法来记录玩家收集的硬币数量。\n\n## 练习 15.03:追踪玩家的硬币数量\n\n在本练习中，您将创建必要的属性和功能，允许您跟踪玩家在整个关卡中收集了多少硬币。在本章的后面，您将使用这个跟踪来显示使用 UMG 的玩家。按照以下步骤完成本练习:\n\n1.  在 Visual Studio 中，找到并打开`SuperSideScroller_Player.h`头文件。\n2.  Under `Private Access Modifier`, create a new `int` variable called `NumberofCollectables`, as shown here:\n\n    ```cpp\n    int32 NumberofCollectables;\n    ```\n\n    这将是一个私人财产，将记录玩家目前收集的硬币数量。您将创建一个返回该整数值的公共函数。出于安全原因，我们这样做是为了确保没有其他类可以修改该值。\n\n3.  Next, under the existing `public` access modifier, create a new `UFUNCTION()` using the `BlueprintPure` keyword called `GetCurrentNumberOfCollectables()`. This function will return an `int`. The following code adds this as an inline function:\n\n    ```cpp\n    UFUNCTION(BlueprintPure)\n    int32 GetCurrentNumberofCollectables() { return NumberofCollectables; };\n    ```\n\n    我们正在使用`UFUNCTION()`和`BlueprintPure`关键字将该功能暴露给蓝图，以便我们以后可以在 UMG 使用它。\n\n4.  Declare a new `void` function, under the `public` access modifier, called `IncrementNumberofCollectables()` that takes in a single integer parameter called `Value`:\n\n    ```cpp\n    void IncrementNumberofCollectables(int32  Value);\n    ```\n\n    这是你用来记录玩家收集了多少硬币的主要功能。我们还将添加一些安全措施，以确保该值永不为负。\n\n5.  声明`IncrementNumberofCollectables()`函数后，让我们在`SuperSideScroller_Player.cpp`源文件中创建这个函数的定义。\n6.  编写以下代码来创建`IncrementNumberofCollectables`函数的定义:\n\n    ```cpp\n    void ASuperSideScroller_Player::IncrementNumberofCollectables(int32 Value)\n    {\n    }\n    ```\n\n7.  The main case to handle here is if the integer value that's passed into this function is less than or equal to `0`. In this case, we do not want to bother incrementing the `NumberofCollectables` variable. Add the following code to the `IncrementNumberofCollectables()` function:\n\n    ```cpp\n    if(Value== 0)\n    {\n      return;\n    }\n    ```\n\n    该`if()`语句表示如果`value`输入参数小于或等于`0`，则功能结束。随着`IncrementNumberofCollectables()`功能返回`void`，以这种方式使用`return`关键字是完全可以的。\n\n    我们添加这个检查是为了确保传递到`IncrementNumberofCollectables()`函数的`value`参数既不是 0 也不是负数，因为建立良好的编码实践很重要；这保证了所有可能的结果都得到处理。在实际的开发环境中，可能会有设计师或其他程序员试图使用`IncrementNumberofCollectables()`函数，并试图传入一个负值，或者等于 0 的值。如果函数没有考虑到这些可能性，那么在开发的后期就有可能出现错误。\n\n8.  现在我们已经处理了`value`小于或等于`0`的边缘情况，让我们继续使用`else()`语句来增加`NumberofCollectables`的功能。在上一步的`if()`语句下添加以下代码:\n\n    ```cpp\n    else\n    {\n      NumberofCollectables += Value;\n    }\n    ```\n\n9.  Next, let's log `NumberofCollectables` using `UE_LOG` and the knowledge we learned about logging variables. Add the following code after the `else()` statement to properly log `NumberofCollectables`:\n\n    ```cpp\n    UE_LOG(LogTemp, Warning, TEXT(\"Number of Coins: %d\"), NumberofCollectables);\n    ```\n\n    有了这个`UE_LOG()`，我们正在制作一个更健壮的日志来跟踪硬币的数量。这为用户界面的工作方式奠定了基础。这是因为我们在本章后面通过使用 UMG 将相同的信息记录给玩家。\n\n    添加了`UE_LOG()`之后，我们需要做的就是调用`PickableActor_Collectable`类内部的`IncrementNumberofCollectables()`函数。\n\n10.  在`PickableActor_Collectable.cpp`源文件中，添加如下标题:\n\n    ```cpp\n    #include \"SuperSideScroller_Player.h\"\n    ```\n\n11.  接下来，在`PlayerPickedUp()`函数中，在`Super::PlayerPickedUp(Player)`行前添加以下函数调用:\n\n    ```cpp\n    Player->IncrementNumberofCollectables(CollectableValue);\n    ```\n\n12.  现在我们的`PickableActor_Collectable`类正在调用我们玩家的`IncrementNumberofCollectables`函数，重新编译 C++ 代码并返回到虚幻引擎 4 编辑器。\n13.  在 UE4 编辑器中，通过*左键单击* `Window`打开`Output Log`窗口，然后将鼠标悬停在`Developer Tools`选项上。从该附加下拉菜单中，选择`Output Log`。\n14.  现在，添加多个`BP_Collectable`演员到你的级别，然后使用 PIE。\n15.  When you overlap over each coin collectible, observe the `Output Log` window to find that each time you collect a coin, the `Output Log` window will show you how many coins you've collected.\n\n    注意\n\n    您可以在这里找到本练习的资产和代码:[https://packt.live/36t6xM5](https://packt.live/36t6xM5)。\n\n完成这个练习后，您现在已经完成了开发跟踪玩家收集的硬币数量的 UI 元素所需的一半工作。下半部分将使用在 UMG 内部的这项活动中开发的功能，在屏幕上向玩家显示这些信息。为此，我们需要了解更多关于虚幻引擎 4 里面的 UMG。\n\n# 绕过\n\nUMG，或虚幻运动图形用户界面设计器，是虚幻引擎 4 的主要工具，用于为菜单、游戏中的 HUD 元素(如健康栏)以及您可能想要呈现给玩家的其他用户界面等创建用户界面。\n\n在`SuperSideScroller`游戏中，我们将只在*练习 15.04、* *中使用`Text`小部件来构建我们的`Coin Collection UI`，创建硬币计数器用户界面 HUD 元素*。我们将在下一节中了解更多关于`Text`小部件的信息。\n\n# 文本小部件\n\n`Text`小部件是现存的较简单的小部件之一。这是因为它只允许您向用户显示文本信息并自定义该文本的视觉效果。几乎每一个游戏都以这样或那样的方式使用文本向玩家显示信息。例如，Overwatch 使用基于文本的用户界面向玩家显示关键的比赛数据。如果不使用文本，将很难——甚至不可能——向玩家传达关键的统计数据，例如造成的总伤害、玩游戏的总时间以及许多其他数据。\n\n`Text`小部件出现在 UMG 的`Palette`标签中。当您在`Canvas`面板中添加`Text`部件时，默认情况下，它会显示文本`Text Block`。您可以通过将您的文本添加到小部件的`Text`参数来自定义该文本。或者，您可以使用`Function Binding`来显示可以引用内部或外部变量的更健壮的文本。`Function Binding`应该在需要显示可以改变的信息时使用；这可以是代表玩家分数、玩家有多少钱的文本，或者在我们的例子中，玩家已经收集的硬币数量:\n\n您将使用`Text`小部件的`Function Binding`功能，使用您在*练习 15.03* 、*中创建的`GetCurrentNumberofCollectables()`功能来显示玩家收集的硬币数量。*\n\n现在我们在`Canvas`面板中有了`Text`小部件，是时候将这个小部件定位在我们需要的位置了。为此，我们将利用锚。\n\n## 锚\n\n锚点用于定义小部件在`Canvas`面板上的期望位置。定义完成后，`Anchor`将通过不同的平台设备(如手机、平板电脑和电脑)确保小部件以不同的屏幕尺寸保持该位置。没有锚点，小部件的位置会在不同的屏幕分辨率之间变得不一致，这是永远不希望的。\n\n注意\n\n关于主播的更多信息，请参考以下文档:[https://docs . unrealengine . com/en-US/Engine/UMG/user guide/Anchors/index . html](https://docs.unrealengine.com/en-US/Engine/UMG/UserGuide/Anchors/index.html)。\n\n对于我们的`Coin Collection UI`和您将使用的`Text`小部件来说，`Anchor`点将位于屏幕的左上角。您还将从这个`Anchor`点添加一个位置偏移，以便玩家更容易看到和阅读文本。在继续创建我们的`Coin Collection UI`之前，让我们了解一下`Text Formatting`，您将使用它向玩家显示当前收集的硬币数量。\n\n## 文本格式\n\n很像我们在 C++ 中可以使用的`UE_LOG()`宏，蓝图提供了一个类似的解决方案来显示文本和格式化文本，以允许向其中添加自定义变量。`Format Text`功能接收标记为`Format`的单一文本输入，并返回`Result`文本输出。这可用于显示信息:\n\n![Figure 15.5: The Format Text function allows us to customize the text using formatted arguments that can be passed in ](img/B16183_15_05.jpg)\n\n图 15.5:格式化文本函数允许我们使用可以传入的格式化参数来自定义文本\n\n`Format Text`函数使用`{}`符号来表示可以传递到字符串中的参数，而不是像`UE_LOG()`那样使用`%`符号。在`{}`符号之间，需要添加一个参数名；这可以是你想要的任何东西，但它应该代表论点是什么。请参考下面截图中显示的示例:\n\n![Figure 15.6: Here, we are passing an example integer into the formatted text ](img/B16183_15_06.jpg)\n\n图 15.6:这里，我们将一个示例整数传递给格式化文本\n\n`Format Text`函数仅支持`Byte`、`Integer`、`Float`、`Text`或`EText Gender`变量类型，因此如果您试图将任何其他类型的变量作为参数传递到函数中，则必须将其转换为支持的类型之一。\n\n注意\n\n`Format Text`功能也用于`Text Localization`，可以为你的游戏支持多种语言。关于如何在 C++ 和蓝图中做到这一点的更多信息可以在这里找到:[。](https://docs.unrealengine.com/en-US/Gameplay/Localization/Formatting/index.html)\n\n在下一个练习中，您将在 UMG 结合使用`Format Text`功能和`Text`小部件，我们将创建`Coin Counter UI`小部件来显示玩家已经收集的硬币数量。您还将使用`Anchors`将`Text`部件定位在屏幕左上角。\n\n## 练习 15.04:创建硬币计数器用户界面抬头显示器元素\n\n在本练习中，您将创建 UMG 用户界面资产，该资产将显示和更新玩家收集的硬币数量。您将使用您在*练习 15.02* 、*中创建的`GetCurrentNumberofCollectables()`内联函数创建一个简单的`Text`小部件在屏幕上显示该值。按照以下步骤完成:*\n\n1.  让我们从在`Content Browser`窗口内创建一个名为`UI`的新文件夹开始。通过在编辑器中浏览器目录顶部的`Content`文件夹上右键单击*并选择`New Folder`来完成此操作。*\n2.  在新的`/Content/UI`目录中，*右键单击*，不选择`Blueprint Class`，而是将鼠标悬停在该列表底部的`User Interface`选项上，然后*左键单击*`Widget Blueprint`选项。\n3.  命名这个新的`Widget Blueprint` `BP_UI_CoinCollection`，然后*双击*资产打开 UMG 编辑器。\n4.  By default, the `Widget` panel is empty, and you will find an empty hierarchy on the left-hand side, as shown in the following screenshot:\n\n    ![Figure 15.7: The Widget panel Hierarchy outlines how the different  elements of the UI are layered with one another ](img/B16183_15_07.jpg)\n\n    图 15.7:小部件面板层次结构概述了用户界面的不同元素是如何相互分层的\n\n5.  Above the `Hierarchy` tab is the `Palette` tab, which lists all the available widgets you can use inside your UI. We will only focus on the `Text` widget, which is listed under the `Common` category. Do not mistake this option with the Rich Text Block widget.\n\n    注意\n\n    有关 UMG 境内所有可用`Widgets`的更详细参考，请阅读史诗游戏的以下文档:[https://docs . unrealengine . com/en-US/Engine/UMG/user guide/WidgetTypeReference/index . html](https://docs.unrealengine.com/en-US/Engine/UMG/UserGuide/WidgetTypeReference/index.html)。\n\n6.  Add the `Text` widget to the `UI` panel by either *left-clicking* and dragging the `Text` widget from the `Palette` tab to the `Hierarchy` tab underneath the `Canvas` panel root, or by *left-clicking* and dragging the `Text` widget directly into the `Canvas` panel itself in the middle of the UMG editor.\n\n    在更改这个小部件的文本之前，我们需要更新它的锚点、位置和字体大小，以适应我们向玩家显示信息的需要。\n\n7.  With the `Text` widget selected, you will see many options under its `Details` panel to customize this text. The first thing to do here is anchor the `Text` widget to the top-left corner of the `Canvas` panel. *Left-click* on the `Anchors` dropdown and select the top-left anchoring option, as shown in the following screenshot:\n\n    ![Figure 15.8: By default, there are options to anchor a widget  at different locations of the screen ](img/B16183_15_08.jpg)\n\n    图 15.8:默认情况下，在屏幕的不同位置有锚定小部件的选项\n\n    锚定允许小部件保持其在`Canvas`面板内的期望位置，而不管不同的屏幕尺寸。\n\n    现在`Text`小部件被锚定到了左上角，我们需要设置它相对于这个锚的位置，这样就有一个偏移量来更好地定位和可读性文本。\n\n8.  在`Details`面板中，`Anchors`选项下方是`Position X`和`Position Y`的参数。将这两个参数设置为`100.0f`。\n9.  Next, enable the `Size To Content` parameter so that the size of the `Text` widget will automatically resize itself, depending on the size of the text it is displaying, as shown in the following screenshot:\n\n    ![Figure 15.9: The Size To Content parameter will ensure that the Text widget  will display its full content and not be cut off ](img/B16183_15_09.jpg)\n\n    图 15.9:内容大小参数将确保文本小部件显示其全部内容，并且不会被截断\n\n10.  我们在这里需要做的最后一件事是更新`Text`小部件使用的字体大小。在`Text`小部件的`Details`面板的`Appearance`选项卡下，您将找到`Size`参数。将该值设置为`48`。\n11.  The final `Text` widget will look like this:\n\n    ![Figure 15.10: The Text widget is now anchored to the top-left of the Canvas panel, with a small relative offset and a larger font for better readability for the player ](img/B16183_15_10.jpg)\n\n    图 15.10:文本小部件现在固定在画布面板的左上角，相对偏移较小，字体较大，以便于玩家阅读\n\n    现在我们已经按照我们需要的方式定位和调整了`Text`小部件的大小，让我们给文本添加一个新的绑定，这样它将自动更新并匹配玩家收藏数量的值。\n\n12.  选择`Text`部件后，在`Details`面板的`Content`类别下找到`Text`参数。在那里，你会找到`Bind`选项。\n13.  *Left-click* the `Bind` option and select `Create Binding`. When doing this, the new `Function Binding` will be created automatically and be given the name `GetText_0`. Please refer to the following screenshot:\n\n    ![Figure 15.11: It is important to always rename the bind functions  because their default names are too generic ](img/B16183_15_11.jpg)\n\n    图 15.11:始终重命名绑定函数很重要，因为它们的默认名称太通用了\n\n14.  重命名该功能`Get Number of Collectables`。\n15.  Before continuing with this function, create a new object reference variable called `Player` that's of the `SuperSideScroller_Player` type. Make this variable `Public` and exposable on spawn by enabling both the `Instance Editable` and `Expose on Spawn` parameters of the variable, as shown in the following screenshot:\n\n    ![Figure 15.12: The Player variable should now have the Instance Editable  and Expose on Spawn parameters enabled ](img/B16183_15_12.jpg)\n\n    图 15.12:玩家变量现在应该启用实例可编辑和产卵时暴露参数\n\n    通过使`Player`变量`Public`在产卵时暴露，您将能够在创建小部件并将其添加到屏幕时分配该变量。我们将在*练习 15.05* 、*中为玩家界面*添加硬币计数器用户界面。\n\n    现在我们有了对`SuperSideScroller_Player`的引用变量，让我们继续`Get Number of Collectables`绑定函数。\n\n16.  将`Player`变量的`Getter`添加到`Get Number of Collectables`函数中。\n17.  From this variable, *left-click* and drag and from the context-sensitive drop-down menu, and find and select the `Get Current Number of Collectables` function. Please refer to the following screenshot:\n\n    ![Figure 15.13: The Get Current Numberof Collectables C++ function  you created in Exercise 15.03 ](img/B16183_15_13.jpg)\n\n    图 15.13:您在练习 15.03 中创建的获取当前收藏数量 C++ 函数\n\n18.  Next, *left-click* and drag out the `Return Value` text parameter of the `Get Number of Collectables` to `Return Node`. From the context-sensitive drop-down menu, search for and select the `Format Text` option, as shown in the following screenshot:\n\n    ![Figure 15.14: Now, we can create customized and formatted text  to suit the needs of the text ](img/B16183_15_14.jpg)\n\n    图 15.14:现在，我们可以创建定制和格式化的文本来满足文本的需求\n\n19.  Within the `Format Text` function, add the following text:\n\n    ```cpp\n    Coins: {coins}\n    ```\n\n    请参考以下截图:\n\n    ![Figure 15.15: Now, there is a new input argument to the formatted  text that we can use to display custom information ](img/B16183_15_15.jpg)\n\n    图 15.15:现在，格式化文本有了一个新的输入参数，我们可以用它来显示定制信息\n\n    请记住，使用`{}`符号表示一个文本参数，它允许您将变量传递到文本中。\n\n20.  Finally, connect the int `Return Value` of the `GetCurrentNumberofCollectables()` function to the wildcard `coins` input pin of the `Format Text` function, as shown here:\n\n    ![Figure 15.16: Now, the Text widget will update automatically based on the updated value returned from the Get Current Numberof Collectables function ](img/B16183_15_16.jpg)\n\n    图 15.16:现在，文本小部件将根据从获取当前收藏数函数返回的更新值自动更新\n\n21.  Compile and save the `BP_UI_CoinCollection` widget Blueprint.\n\n    注意\n\n    您可以在这里找到本练习的资产和代码:[https://packt.live/3eQJjTU](https://packt.live/3eQJjTU)。\n\n完成本练习后，您已经创建了显示玩家当前收集的硬币数量所需的`UI UMG`小部件。通过使用`GetCurrentNumberofCollectables()` C++ 函数和`Text`小部件的绑定功能，用户界面将始终根据收集的硬币数量更新其值。在下一个练习中，我们将把这个用户界面添加到玩家的屏幕上，但是首先，我们将简要了解如何在玩家屏幕上添加和移除 UMG。\n\n# 添加和创建 UMG 用户小部件\n\n现在我们已经在 UMG 创建了硬币收集用户界面，是时候学习如何在玩家屏幕上添加和移除用户界面了。通过将硬币收集用户界面添加到玩家屏幕，用户界面对玩家变得可见，并且可以随着玩家收集硬币而更新。\n\n在蓝图中，有一个叫做`Create Widget`的函数，如下图截图所示。没有指定的班级，会贴上`Construct None`的标签，但不要让这个迷惑了你:\n\n![Figure 15.17: The Create widget as it is by default, without a class applied ](img/B16183_15_17.jpg)\n\n图 15.17:默认情况下的创建小部件，没有应用类\n\n该功能需要创建`User`小部件的类，并且需要一个`Player Controller`作为该用户界面的拥有者。该函数随后将衍生的用户小部件作为其`Return Value`返回，然后您可以使用`Add to Viewport`函数将其添加到玩家的视口中。`Create Widget`函数只实例化小部件对象；它不会将这个小部件添加到播放器的屏幕上。正是`Add to Viewport`功能使这个小部件在玩家的屏幕上可见:\n\n![Figure 15.18: Add to Viewport function with ZOrder ](img/B16183_15_18.jpg)\n\n图 15.18:用 ZOrder 添加到视口功能\n\n视口是覆盖你的游戏世界视图的游戏屏幕，在多个 UI 元素需要上下重叠的情况下，它使用所谓的`ZOrder`来确定覆盖深度。默认情况下，`Add to Viewport`功能会将`User`小部件添加到屏幕上，并使其充满整个屏幕；也就是说，除非调用`Set Desired Size In Viewport`函数来设置手动填充的大小:\n\n![Figure 15.19: The Size parameter determines the desired size of the passed in User widget ](img/B16183_15_19.jpg)\n\n图 15.19:大小参数决定了传入的用户小部件的期望大小\n\n在 C++ 中，你还有一个叫做`CreateWidget()`的函数:\n\n```cpp\ntemplate<typename WidgetT, typename OwnerT>\nWidgetT * CreateWidget\n(\n  OwnerT * OwningObject,\n  TSubclassOf < UUserWidget > UserWidgetClass,\n  FName WidgetName\n)\n```\n\n`CreateWidget()`功能可通过`UserWidget`类获得，可在`/Engine/Source/Runtime/UMG/Public/Blueprint/UserWidget.h`中找到。\n\n这方面的一个例子可以在*第 8 章*、*用户界面*中找到，在这里您使用`CreateWidget()`功能创建`BP_HUDWidget`:\n\n```cpp\nHUDWidget = CreateWidget<UHUDWidget>(this, BP_HUDWidget);\n```\n\n有关 C++ 中`CreateWidget()`功能的更多信息，请参考*第 8 章*、*用户界面*和*练习 8.06、* *创建健康栏 C++ 逻辑*。\n\n该函数的工作原理与蓝图函数几乎相同，因为它采用了`Owning Object`参数，很像蓝图函数的`Owning Player`参数，并且需要创建`User Widget`类。C++ `CreateWidget()`函数还接受一个`FName`参数来表示小部件的名称。\n\n既然我们已经了解了向播放器屏幕添加用户界面的方法，让我们来测试一下这些知识。在下面的练习中，您将实现`Create Widget`和`Add to Viewport`蓝图功能，以便我们可以将我们在*练习 15.04* 、*创建硬币计数器用户界面 HUD 元素*中创建的硬币收集用户界面添加到玩家屏幕上。\n\n## 练习 15.05:将硬币计数器 UI 添加到玩家屏幕\n\n在本练习中，您将创建一个新的`Player Controller`类，以便您可以使用玩家控制器将`BP_UI_CoinCollection`小部件蓝图添加到玩家屏幕上。从那里，你还将创建一个新的`Game Mode`职业，并将这个游戏模式应用到`SuperSideScroller`项目中。执行以下步骤完成本练习:\n\n1.  在虚幻引擎 4 编辑器中，导航至`File`，然后导航至`New C++ Class`。\n2.  在`Choose Parent Class`对话框窗口中，找到并选择`Player Controller`选项。\n3.  命名新的`Player Controller`类`SuperSideScroller_Controller`，然后*左键点击*`Create Class`按钮。Visual Studio 将自动生成并打开`SuperSideScroller_Controller`类的源文件和头文件，但目前，我们将停留在虚幻引擎 4 编辑器中。\n4.  在`Content Browser`窗口的`MainCharacter`文件夹目录下，新建一个名为`PlayerController`的文件夹。\n5.  In the `PlayerController` folder, *right-click* and create a new `Blueprint Class` using the new `SuperSideScroller_Controller` class. Please refer to the following screenshot:\n\n    ![Figure 15.20: Finding the new SuperSideScroller_Controller class  to create a new Blueprint from ](img/B16183_15_20.jpg)\n\n    图 15.20:找到新的 SuperSideScroller_Controller 类来创建新的蓝图\n\n6.  Name this new Blueprint `BP_SuperSideScroller_PC` and then *double-left-click* the asset to open it.\n\n    要将`BP_UI_CoinCollection`小部件添加到屏幕上，我们需要使用`Add to Viewport`功能和`Create Widget`功能。我们希望在玩家角色被玩家控制器`Possessed`激活后，用户界面被添加到玩家的屏幕上。\n\n7.  *Right-click* inside the Blueprint graph and from the context-sensitive menu, find the `Event On Possess` option and *left-click* to add it to the graph. Please refer to the following screenshot:\n\n    ![Figure 15.21: The Event On Possess option will be called each time  this controller class possesses a new pawn ](img/B16183_15_21.jpg)\n\n    图 15.21:每次这个控制器类拥有一个新棋子时，都会调用占有事件选项\n\n    `Event On Possess`事件节点返回`Possessed Pawn`。我们将使用这个棋子进入我们的`BP_UI_CoinCollection UI Widget`，但是首先，我们需要`Cast To``SuperSideScroller_Player`类。\n\n8.  *Left-click* and drag from the output the `Possessed Pawn` parameter of the `Event On Possess` node. Then, search for and find the `Cast to SuperSideScroller_Player` node. Please refer to the following screenshot:\n\n    ![Figure 15.22: We need to Cast To SuperSideScroller_Player to ensure we are casting to the right player character class ](img/B16183_15_22.jpg)\n\n    图 15.22:我们需要转换到超级玩家角色类，以确保我们转换到正确的玩家角色类\n\n9.  现在，*右键单击*，搜索`Create Widget`功能，将其添加到蓝图图形中。\n10.  From the drop-down class parameter, find and assign the `BP_UI_CoinCollection` asset you created in *Exercise 15.04*, *Creating the Coin Counter UI HUD Element*. Please refer to the following screenshot:\n\n    ![Figure 15.23: The Create Widget function will create a new UI object  using the UMG class passed into it ](img/B16183_15_23.jpg)\n\n    图 15.23:创建小部件函数将使用传递给它的 UMG 类创建一个新的用户界面对象\n\n    将`Class`参数更新到`BP_UI_CoinCollection`类后，您会注意到`Create Widget`函数将更新以显示您创建的`Player`变量，设置为`Exposed on Spawn`。\n\n11.  *Right-click* in the Blueprint graph to search for and find the `Self` reference variable from the context-sensitive drop-down menu. Connect the `Self` object variable to the `Owning Player` parameter of the `Create Widge`t function, as shown in the following screenshot:\n\n    ![Figure 15.24: The Owning Player input parameter is of the Player Controller type ](img/B16183_15_24.jpg)\n\n    图 15.24:拥有玩家输入参数属于玩家控制器类型\n\n    `Owning Player`参数是指将显示并拥有该 UI 对象的`Player Controller`类型。由于我们将这个用户界面添加到`SuperSideScroller_Controller`蓝图中，我们可以只使用`Self`引用变量传递到函数中。\n\n12.  Next, pass in the returned `SuperSideScroller_Player` variable from the `Cast` node to the `Player` input node of the `Create Widget` function. Then, connect the execution pins of the `Cast` node and the `Create Widget` function, as shown in the following screenshot:\n\n    ![Figure 15.25: If the Cast To SuperSideScroller_Player is valid, we can create the BP_UI_CoinCollection widget and pass in the player that has been possessed ](img/B16183_15_25.jpg)\n\n    图 15.25:如果 Cast To SuperSideScroller_Player 有效，我们可以创建 BP_UI_CoinCollection 小部件并传入已经拥有的播放器\n\n    注意\n\n    您可以在以下链接找到前面的全分辨率截图，以便更好地查看:[https://packt.live/3f89m99](https://packt.live/3f89m99)。\n\n13.  *再次右键单击蓝图图内的*，搜索并找到`Add to Viewport`功能，即可将其放置在图中。\n14.  将`Create Widget`功能的输出`Return Value`参数连接到`Add to Viewport`功能的`Target`输入参数；不要更改`ZOrder`参数。\n15.  Lastly, connect the execution pins of the `Create Widget` and `Add to Viewport` functions, as shown here:\n\n    ![Figure 15.26: After creating the BP_UI_CoinCollection widget, we can  add it to the player viewport ](img/B16183_15_26.jpg)\n\n    图 15.26:在创建了 BP_UI_CoinCollection 小部件之后，我们可以将其添加到播放器视口中\n\n    注意\n\n    您可以在以下链接找到前面的全分辨率截图，以便更好地查看:[https://packt.live/2UwufBd](https://packt.live/2UwufBd)。\n\n    现在玩家控制器将`BP_UI_CoinCollection`小部件添加到玩家的视口中，我们需要创建一个`GameMode`蓝图并将`BP_SuperSideScroller_MainCharacter`和`BP_SuperSideScroller_PC`类都应用到这个游戏模式中。\n\n16.  在`Content Browser`窗口中，通过*右键单击`Content`文件夹并选择`New Folder`来创建新文件夹。命名这个文件夹`GameMode`。*\n17.  接下来，*右键点击*，选择`Blueprint Class`开始创建游戏模式蓝图。在`Pick Parent Class`对话框窗口中，搜索并找到`All Classes`下的`SuperSideScrollerGameMode`。\n18.  Name this new `GameMode` Blueprint `BP_SuperSideScroller_GameMode`. *Double-left-click* this asset to open it.\n\n    `GameMode`蓝图包含一个类列表，您可以使用自己的唯一类进行自定义。目前，我们只会担心`Player Controller Class`和`Default Pawn Class`。\n\n19.  *左键单击`Player Controller Class`下拉菜单中的*，找到并选择您在本练习前面创建的`BP_SuperSideScroller_PC`蓝图。\n20.  Then, *left-click* the `Default Pawn Class` dropdown to find and select the `BP_SuperSideScroller_MainCharacter` Blueprint.\n\n    现在我们有了一个自定义`GameMode`，它利用了我们的自定义`Player Controller`和`Player Character`类，让我们将这个游戏模式添加到`Project Settings`窗口，这样当使用 PIE 和烹饪项目的构建时，游戏模式将被默认使用。\n\n21.  从虚幻引擎 4 编辑器中，导航至屏幕顶部的`Edit`选项。*左键点击*这个选项，从下拉菜单中找到并选择`Project Settings`选项。\n22.  在`Project Settings`窗口的左侧，您会看到一个分类列表，分为几个部分。在`Project`部分下，*左键单击`Maps & Modes`类别的*。\n23.  In the `Maps & Modes` section, you have a handful of parameters related to your project's default maps and game mode. At the top of this section, you have the `Default GameMode` option. *Left-click* this dropdown to find and select the `SuperSideScroller_GameMode` Blueprint you created earlier in this exercise.\n\n    注意\n\n    对`Maps & Modes`部分所做的更改会自动保存并写入`DefaultEngine.ini`文件，该文件可以在项目的`Config`文件夹中找到。`Default GameMode`可以通过更新`GameMode Override`参数来覆盖每一级，该参数可以在你的级别的`World Settings`窗口中找到。\n\n24.  Close the `Project Settings` window and return to your level. Use PIE and start collecting coins. Observe that the `BP_UI_CoinCollection` widget is shown and updated each time you collect a coin, as shown in the following screenshot:\n\n    ![Figure 15.27: Now, every coin you collect will appear on the player UI ](img/B16183_15_27.jpg)\n\n图 15.27:现在，你收集的每一枚硬币都会出现在玩家界面上\n\n注意\n\n您可以在这里找到本练习的资产和代码:[https://packt.live/2JRfSFz](https://packt.live/2JRfSFz)。\n\n完成本练习后，您已经创建了显示玩家当前收集的硬币数量所需的`UI UMG`小部件。通过使用`GetCurrentNumberofCollectables()` C++ 函数和`Text`小部件的绑定功能，用户界面将始终根据收集的硬币数量更新其值。\n\n到目前为止，我们专注于可收集的硬币，并允许玩家收集这些硬币，并将收集的硬币总数添加到玩家的用户界面中。现在，我们将专注于药剂加电，并在短时间内给予玩家移动速度和跳跃高度的增加。为了实现这个功能，我们首先需要研究计时器。\n\n# 计时器\n\n虚幻引擎 4 中的计时器允许您在延迟后或每 X 秒执行一次操作。在`SuperSideScroller`药剂加电的情况下，一个计时器将用于恢复玩家的移动，并在 8 秒后跳转到它们的默认值。\n\n注意\n\n在蓝图中，除了计时器句柄之外，您还可以使用延迟节点来实现相同的结果。然而，在 C++ 中，计时器是实现延迟和重复逻辑的最佳手段。\n\n定时器由存在于`UWorld`对象中的`Timer Manager`或`FTimerManager`管理。您将从`FTimerManager`类中使用两个主要功能，称为`SetTimer()`和`ClearTimer()`:\n\n```cpp\nvoid SetTimer\n(\n    FTimerHandle & InOutHandle,\n    TFunction < void )> && Callback,\n    float InRate,\n    bool InbLoop,\n    float InFirstDelay\n)\nvoid ClearTimer(FTimerHandle& InHandle)\n```\n\n你可能已经注意到，在这两个功能中，都有一个必选的`FTimerHandle`。这个手柄用来控制你设置的定时器。使用这个手柄，您可以暂停、恢复、清除甚至延长计时器。\n\n`SetTimer()`功能在初始设置时还有其他参数帮助你自定义这个`Timer`。`Timer`完成后将调用回调函数，如果`InbLoop`参数为`True`，将无限期继续调用回调函数，直到定时器停止。`InRate`参数是定时器本身的持续时间，而`InFirstDelay`是在定时器开始为`InRate`计时之前应用于定时器的初始延迟。\n\n`FTimerManager`类的头文件可以在这里找到:`/Engine/Source/Runtime/Engine/Public/TimerManager.h`。\n\n注意\n\n您可以通过阅读这里的文档来了解更多关于计时器和`FTimerHandle`的信息。\n\n在下面的练习中，你将在`SuperSideScroller_Player`类中创建自己的`FTimerHandle`，并使用它来控制药剂加电对玩家的影响持续多长时间。\n\n## 练习 15.06:为玩家添加药剂加电行为\n\n在本练习中，您将创建药剂启动背后的逻辑以及它将如何影响玩家角色。您将利用计时器和计时器手柄来确保上电效果只持续很短的时间。按照以下步骤完成:\n\n1.  在 Visual Studio 中，导航并打开`SuperSideScroller_Player.h`头文件。\n2.  Under `our Private Access Modifier`, add a new variable of the `FTimerHandle` type and name it `PowerupHandle`:\n\n    ```cpp\n    FTimerHandle PowerupHandle;\n    ```\n\n    这个计时器句柄将负责跟踪自启动以来已经过去了多长时间。这将允许我们控制药剂启动效果持续的时间。\n\n3.  Next, add a Boolean variable under our `Private Access Modifier` called `bHasPowerupActive`:\n\n    ```cpp\n    bool bHasPowerupActive;\n    ```\n\n    我们在更新`Sprint()`和`StopSprinting()`功能的时候会用到这个布尔变量，保证我们根据上电是否活跃来适当更新玩家的冲刺移动速度。\n\n4.  Next, declare a new void function called `IncreaseMovementPowerup()` under our `Public Access Modifier`:\n\n    ```cpp\n    void IncreaseMovementPowerup();\n    ```\n\n    这是将从药剂加电类调用的功能，为玩家启用加电效果。\n\n5.  Finally, you need to create a function that handles when the power-up effects end. Create a function called `EndPowerup()` under `Protected Access Modifier`:\n\n    ```cpp\n    void EndPowerup();\n    ```\n\n    声明了所有必要的变量和函数后，是时候开始定义这些新函数并处理播放器上电的影响了。\n\n6.  导航至`SuperSideScroller_Player.cpp`源文件。\n7.  首先，在源文件顶部添加头文件`#include \"TimerManager.h\"`；为了使用`Timers`，我们将需要这个类。\n8.  通过在源文件中添加以下代码来定义`IncreaseMovementPowerup()`函数:\n\n    ```cpp\n    void ASuperSideScroller_Player::IncreaseMovementPowerup()\n    {\n    }\n    ```\n\n9.  调用这个函数时，我们首先要做的就是将`bHasPowerupActive`变量设置为`true`。将以下代码添加到`IncreaseMovementPowerup()`功能中:\n\n    ```cpp\n    bHasPowerupActive = true;\n    ```\n\n10.  Next, add the following code to increase both the `MaxWalkSpeed` and `JumpZVelocity` components of the player character movement component:\n\n    ```cpp\n    GetCharacterMovement()->MaxWalkSpeed = 500.0f;\n    GetCharacterMovement()->JumpZVelocity = 1500.0f;\n    ```\n\n    这里，我们将`MaxWalkSpeed`从默认的`300.0f`值更改为`500.0f`。大家可能还记得，默认的冲刺速度也是`500.0f`。我们将在本练习的后面部分讨论这个问题，以便在启动时提高冲刺速度。\n\n11.  To take advantage of timers, we need to get a reference to the `UWorld` object. Add the following code:\n\n    ```cpp\n    UWorld* World = GetWorld();\n    if (World)\n    {\n    }\n    ```\n\n    正如我们之前在这个项目中多次做的那样，我们使用`GetWorld()`函数来获取对`UWorld`对象的引用，并将该引用保存在其变量中。\n\n12.  Now that we have the reference to the `World` object and have performed a validity check, it is safe to use the `TimerManager` to set the power-up timer. Add the following code within the `if()` statement shown in the previous step:\n\n    ```cpp\n    World->GetTimerManager().SetTimer(PowerupHandle, this,   &ASuperSideScroller_Player::EndPowerup, 8.0f, false);\n    ```\n\n    这里，您正在使用`TimerManager`类设置计时器。`SetTimer()`功能取`FTimerHandle`组件使用；在这种情况下，您创建的`PowerupHandle`变量。接下来，我们需要通过使用`this` 关键字来传递对玩家类的引用。然后，我们需要提供定时器结束后要调用的回调函数，这种情况下就是`&ASuperSideScroller_Player::EndPowerup`函数。`8.0f`代表定时器的持续时间；你可以随意调整，但现在，8 秒钟就可以了。最后，还有一个参数决定这个计时器是否应该循环；在这种情况下，它不应该。\n\n13.  为`EndPowerup()`函数创建函数定义:\n\n    ```cpp\n    void ASuperSideScroller_Player::EndPowerup()\n    {\n    }\n    ```\n\n14.  调用`EndPowerup()`函数的第一件事是将`bHasPowerupActive`变量设置为`false`。在`EndPowerup()`功能中添加以下代码:\n\n    ```cpp\n    bHasPowerupActive = false;\n    ```\n\n15.  Next, change the `MaxWalkSpeed` and `JumpZVelocity` parameters of the character movement component back to their default values. Add the following code:\n\n    ```cpp\n    GetCharacterMovement()->MaxWalkSpeed = 300.0f;\n    GetCharacterMovement()->JumpZVelocity = 1000.0f;\n    ```\n\n    这里，我们将角色移动组件的`MaxWalkSpeed`和`JumpZVelocity`参数都更改为默认值。\n\n16.  同样，为了利用计时器并清除计时器来处理`PowerupHandle`，我们需要获取对`UWorld`对象的引用。添加此代码:\n\n    ```cpp\n    UWorld* World = GetWorld();\n    if (World)\n    {\n    }\n    ```\n\n17.  Finally, we can add the code to clear the timer handle's `PowerupHandle`:\n\n    ```cpp\n    World->GetTimerManager().ClearTimer(PowerupHandle);\n    ```\n\n    通过使用`ClearTimer()`功能并传入`PowerupHandle`，我们确保该定时器不再有效，不会再影响玩家。\n\n    现在我们已经创建了处理加电效果和与效果相关的计时器的功能，我们需要更新`Sprint()`和`StopSprinting()`功能，以便它们也考虑到加电激活时玩家的速度。\n\n18.  Update the `Sprint()` function to the following:\n\n    ```cpp\n    void ASuperSideScroller_Player::Sprint()\n    {\n      if (!bIsSprinting)\n      {\n        bIsSprinting = true;\n        if (bHasPowerupActive)\n        {\n          GetCharacterMovement()->MaxWalkSpeed = 900.0f;\n        }\n        else\n        {\n          GetCharacterMovement()->MaxWalkSpeed = 500.0f;\n        }\n      }\n    }\n    ```\n\n    这里，我们正在更新`Sprint()`函数，以考虑`bHasPowerupActive`是否为真。如果这个变量为真，那么我们在从`500.0f`冲刺到`900.0f`时增加`MaxWalkSpeed`，如下图所示:\n\n    ```cpp\n    if (bHasPowerupActive)\n    {\n      GetCharacterMovement()->MaxWalkSpeed = 900.0f;\n    }\n    ```\n\n    如果`bHasPowerupActive`为假，那么我们将`MaxWalkSpeed`增加到`500.0f`，就像我们默认的那样。\n\n19.  Update the `StopSprinting()` function to the following:\n\n    ```cpp\n    void ASuperSideScroller_Player::StopSprinting()\n    {\n      if (bIsSprinting)\n      {\n        bIsSprinting = false;\n        if (bHasPowerupActive)\n        {\n          GetCharacterMovement()->MaxWalkSpeed = 500.0f;\n        }\n        else\n        {\n          GetCharacterMovement()->MaxWalkSpeed = 300.0f;\n        }\n      }\n    }\n    ```\n\n    这里，我们正在更新`StopSprinting()`函数，以考虑`bHasPowerupActive`是否为真。如果这个变量为真，那么我们将`MaxWalkSpeed`值设置为`500.0f`而不是`300.0f`，如下图所示:\n\n    ```cpp\n    if (bHasPowerupActive)\n    {\n      GetCharacterMovement()->MaxWalkSpeed = 500.0f;\n    }\n    ```\n\n    如果`bHasPowerupActive`为假，那么我们将`MaxWalkSpeed`设置为`300.0f`，就像我们默认的那样。\n\n20.  Finally, all we need to do is recompile the C++ code.\n\n    注意\n\n    您可以在这里找到本练习的资产和代码:[https://packt.live/3eP39yL](https://packt.live/3eP39yL)。\n\n完成这个练习后，你已经在玩家角色中创建了药剂启动效果。加电既增加了玩家的默认移动速度，也增加了他们的跳跃高度。此外，加电的效果提高了短跑速度。通过使用定时器手柄，您可以控制上电效果持续的时间。\n\n现在，是时候创建药剂启动演员了，这样我们就可以在游戏中有一个这个启动的代表。\n\n## 活动 15.03:创造药剂启动演员\n\n现在`SuperSideScroller_Player`职业处理药剂加电的效果，是时候创建药剂加电职业和蓝图了。本活动的目的是创建药剂加电类，从`PickableActor_Base`类继承，实现重叠功能以授予您在*练习 15.06* 、*中实现的移动效果，为玩家*添加药剂加电行为，并为药剂加电创建蓝图演员。执行以下步骤创建药剂启动类和药剂蓝图演员:\n\n1.  创建一个从`PickableActor_Base`类继承的新 C++ 类，并命名这个新类`PickableActor_Powerup`。\n2.  为`BeginPlay()`和`PlayerPickedUp()`函数添加覆盖函数声明。\n3.  为`BeginPlay()`函数创建函数定义。在`BeginPlay()`函数中，添加对父类函数的调用。\n4.  为`PlayerPickedUp()`函数创建函数定义。在`PlayerPickedUp()`函数中，添加对`PlayerPickedUp()`父类函数的调用。\n5.  接下来，为`SuperSideScroller_Player`类添加必要的`#include`文件，这样我们就可以引用玩家类及其功能。\n6.  在`PlayerPickedUp()`函数中，使用函数本身的`Player`输入参数对`IncreaseMovementPowerup()`进行函数调用。\n7.  从`Epic Games Launcher`开始，在`Games`类别下的`Learn`选项卡中找到`Action RPG`项目。使用它来创建和安装新项目。\n8.  将`A_Character_Heal_Mana_Cue`和`SM_PotionBottle`资产以及它们的所有引用资产从`Action RPG`项目迁移到您的`SuperSideScroller`项目。\n9.  在`PickableItems` 目录下的`Content Browser`窗口中创建一个名为`Powerup`的新文件夹。基于`PickableActor_Powerup`类在该目录中创建新蓝图，并将该资产命名为`BP_Powerup`。\n10.  在`BP_Powerup`中，更新`MeshComp`组件，以便使用`SM_PotionBottle`静态网格。\n11.  接下来，添加`A_Character_Heal_Mana_Cue`，将其导入为`Pickup Sound`参数。\n12.  最后，更新`RotationComp`组件，使演员围绕`Pitch`轴每秒旋转 60 度，围绕`Yaw`轴每秒旋转 180 度。\n13.  Add `BP_Powerup` to your level and use PIE to observe the results when overlapping with the power-up.\n\n    预期产出:\n\n    ![Figure 15.28: The potion power-up now has a nice visual representation and can be overlapped by the player to enable its power-up effects ](img/B16183_15_28.jpg)\n\n图 15.28:药剂加电现在有了一个很好的视觉表现，可以被玩家叠加来启用它的加电效果\n\n注意\n\n这个活动的解决方案可以在:[https://packt.live/338jEBx](https://packt.live/338jEBx)找到。\n\n完成本练习后，您可以测试您的知识，创建一个新的 C++ 类，该类继承自`PickableActor_Base`类并覆盖`PlayerPickedUp()`函数以添加自定义逻辑。通过从玩家类添加对`IncreaseMovementPowerup()`功能的调用，当与演员重叠时，您可以为玩家添加移动启动效果。然后，通过使用自定义网格、材质和音频资源，您可以将蓝图演员从`PickableActor_Powerup`类中激活。\n\n现在我们已经创建了硬币收藏品和药剂启动，我们需要在项目中实现一个新的游戏特性:`Brick`类。在《超级马里奥》等游戏中，砖块包含隐藏的硬币和电源，供玩家寻找。这些砖块也是到达高架平台和楼层内区域的一种手段。在我们的`SuperSideScroller`项目中，`Brick`类将为玩家提供隐藏的硬币收藏品，并通过使用砖块作为到达难以到达的位置的路径来让玩家到达关卡区域。所以，在下一节中，我们将创建`Brick`类，它需要被打破才能找到隐藏的硬币。\n\n## 练习 15.07:创建砖块类\n\n现在我们已经创建了硬币收藏品和药剂加电，是时候创建`Brick`类了，它将包含玩家的隐藏硬币。砖块是`SuperSideScroller`项目的最终游戏元素。在本练习中，您将创建`Brick`类，它将被用作`SuperSideScroller`游戏项目的平台化机制的一部分，但也是玩家寻找收藏品的一种方式。按照以下步骤创建这个`Brick`类及其蓝图:\n\n1.  在虚幻引擎 4 编辑器中，导航至`File`，然后导航至`New C++ Class`。\n2.  在`Choose Parent Class`对话框窗口中，找到并选择`Actor`类。\n3.  Name this class `SuperSideScroller_Brick` and *left-click* `Create Class`. Visual Studio and Unreal Engine will recompile the code and open this class for you.\n\n    默认情况下，`SuperSideScroller_Brick`类带有`Tick()`功能，但是我们不需要这个功能给`Brick`类。继续之前，从`SuperSideScroller_Brick.h`头文件中删除`Tick()`的函数声明，并从`SuperSideScroller_Brick.cpp`源文件中删除函数定义。\n\n4.  在`SuperSideScroller_Brick.h`文件的`Private Access Modifier`下，添加以下代码来声明一个新的`UStaticMeshComponent* UPROPERTY()`函数来表示我们游戏世界中的砖块:\n\n    ```cpp\n    UPROPERTY(VisibleDefaultsOnly, Category = Brick)\n    class UStaticMeshComponent* BrickMesh;\n    ```\n\n5.  接下来，我们需要创建一个`UBoxComponent UPROPERTY()`来处理与玩家角色的碰撞。添加以下代码，在我们的`Private Access Modifier`下添加该组件:\n\n    ```cpp\n    UPROPERTY(VisibleDefaultsOnly, Category = Brick)\n    class UBoxComponent* BrickCollision;\n    ```\n\n6.  Create the `UFUNCTION()` declaration for the `OnHit()` function under our `Private Access Modifier`. This will be used to determine when `UBoxComponent` is hit by the player:\n\n    ```cpp\n    UFUNCTION()\n    void OnHit(UPrimitiveComponent* HitComp, AActor* OtherActor,   UprimitiveComponent* OtherComp, FVector NormalImpulse,   const FHitResult& Hit);\n    ```\n\n    注意\n\n    回想一下，您在开发*第十三章**敌方人工智能*中的`PlayerProjectile`类时，为这个项目使用了`OnHit()`功能。有关`OnHit()`功能的更多信息，请查阅该章。\n\n7.  Next, create a new Boolean `UPROPERTY()` under our `Private Access Modifier` using the `EditAnywhere` keyword called `bHasCollectable`:\n\n    ```cpp\n    UPROPERTY(EditAnywhere)\n    bool bHasCollectable;\n    ```\n\n    这个布尔值将决定砖块是否包含玩家可以收集的硬币。\n\n8.  Now, we need a variable that holds how many coin collectibles are available within this brick for the player. We will do this by creating an integer variable called `Collectable Value`. Make this a `UPROPERTY()`, under the `private access modifier`, with the `EditAnywhere` keyword, and give it a default value of `1`, as shown here:\n\n    ```cpp\n    UPROPERTY(EditAnywhere)\n    int32 CollectableValue = 1;\n    ```\n\n    砖块将需要包含一个独特的声音和粒子系统，以便当砖块被玩家破坏时，它有一个很好的抛光层。接下来我们将添加这些属性。\n\n9.  在`SuperSideScroller_Brick.h`头文件中创建新的`Public Access Modifier`。\n10.  接下来，使用`EditAnywhere`和`BlueprintReadOnly`关键字为`USoundBase`类的一个变量创建一个新的`UPROPERTY()`。命名这个变量`HitSound`，如下图:\n\n    ```cpp\n    UPROPERTY(EditAnywhere, BlueprintReadOnly)\n    class USoundBase* HitSound;\n    ```\n\n11.  Then, create a new `UPROPERTY()` using the `EditAnywhere` and `BlueprintReadOnly` keywords for a variable of the `UParticleSystem` class. Make sure to put this under the `public access modifier`, and name this variable `Explosion`, as shown here:\n\n    ```cpp\n    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Brick)\n    class UParticleSystem* Explosion;\n    ```\n\n    现在我们已经拥有了`Brick`类的所有必要属性，让我们进入`SuperSideScroller_Brick.cpp`源文件，在这里我们将初始化组件。\n\n12.  让我们从为`StaticMeshComponent`和`BoxComponent`添加以下`#include`目录开始。将以下代码添加到源文件的`#include`列表中:\n\n    ```cpp\n    #include \"Components/StaticMeshComponent.h\"\n    #include \"Components/BoxComponent.h\"\n    ```\n\n13.  首先，通过向`ASuperSideScroller_Brick::ASuperSideScroller_Brick()`构造函数添加以下代码来初始化`BrickMesh`组件:\n\n    ```cpp\n    BrickMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT(\"BrickMesh\"));\n    ```\n\n14.  接下来，`BrickMesh`组件应该有碰撞，这样玩家就可以走在上面进行平台游戏。为确保默认情况下发生这种情况，添加以下代码将碰撞设置为`\"BlockAll\"` :\n\n    ```cpp\n    BrickMesh->SetCollisionProfileName(\"BlockAll\");\n    ```\n\n15.  最后，`BrickMesh`组件将作为`Brick`参与者的根组件。添加以下代码来完成此操作:\n\n    ```cpp\n    RootComponent = BrickMesh;\n    ```\n\n16.  现在，在构造函数中添加以下代码来初始化我们的`BrickCollision UBoxComponent` :\n\n    ```cpp\n    BrickCollision = CreateDefaultSubobject<UBoxComponent>  (TEXT(\"BrickCollision\"));\n    ```\n\n17.  就像`BrickMesh`组件一样，`BrickCollision`组件也需要将其碰撞设置为`\"BlockAll\"`，以便接收我们将在本练习后面添加的`OnHit()`回调事件。添加以下代码:\n\n    ```cpp\n    BrickCollision->SetCollisionProfileName(\"BlockAll\");\n    ```\n\n18.  接下来，需要将`BrickCollision`组件附着到`BrickMesh`组件上。我们可以通过添加以下代码来做到这一点:\n\n    ```cpp\n    BrickCollision->AttachToComponent(RootComponent,   FAttachmentTransformRules::KeepWorldTransform);\n    ```\n\n19.  在完成`BrickCollision`组件的初始化之前，我们需要为`OnHit()`函数添加函数定义。在源文件中添加以下定义:\n\n    ```cpp\n    void ASuperSideScroller_Brick::OnHit(UPrimitiveComponent* HitComp, AActor*   OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const   FHitResult& Hit)\n    {\n    }\n    ```\n\n20.  现在我们已经定义了`OnHit()`函数，我们可以将`OnComponentHit`回调分配给`BrickCollision`组件。将以下代码添加到构造函数中:\n\n    ```cpp\n    BrickCollision->OnComponentHit.AddDynamic(this,   &ASuperSideScroller_Brick::OnHit);\n    ```\n\n21.  编译`SuperSideScroller_Brick`类的 C++ 代码，返回虚幻引擎 4 编辑器。\n22.  在`Content Browser`窗口中，*右键单击`Content`文件夹中的*，选择 `New Folder`选项。命名这个文件夹`Brick`。\n23.  *右键单击`Brick`文件夹内的*，选择`Blueprint Class`。在`Pick Parent Class`对话框窗口的`All Classes`搜索栏中，搜索并选择`SuperSideScroller_Brick`类别。\n24.  命名这个新蓝图`BP_Brick`，然后*双击*资产将其打开。\n25.  从`Components`选项卡中选择`BrickMesh`组件，并将其`Static Mesh`参数设置为`Shape_Cube`网格。\n26.  With the `BrickMesh` component still selected, set the `Element 0` material parameter to `M_Brick_Clay_Beveled`. `M_Brick_Clay_Beveled` is a material provided by Epic Games by default when creating a new project. It can be found within the `StarterContent` directory, in the `Content Browser` window.\n\n    我们最后需要对`BrickMesh`组件做的是调整它的比例，使其符合玩家角色的需求，以及`SuperSideScroller`游戏项目的平台化机制。\n\n27.  With the `BrickMesh` component selected, make the following change to its `Scale` parameter:\n\n    ```cpp\n    (X=0.750000,Y=0.750000,Z=0.750000)\n    ```\n\n    现在`BrickMesh`组件是正常大小的`75%`，当我们将演员放入游戏世界时，以及当我们在关卡中开发有趣的平台化部分时，`Brick`演员对于我们设计师来说将变得更容易管理。\n\n    这里的最后一步是更新`BrickCollision`组件的位置，使其只有部分碰撞从`BrickMesh`组件的底部突出。\n\n28.  Select the `BrickCollision` component from the `Components` tab and update its `Location` parameter to the following values:\n\n    ```cpp\n    (X=0.000000,Y=0.000000,Z=30.000000)\n    ```\n\n    `BrickCollision`组件现在应定位如下:\n\n    ![Figure 15.29: Now, the BrickCollision component is just barely outside  the BrickMesh component ](img/B16183_15_29.jpg)\n\n图 15.29:现在，砖块碰撞组件刚好在砖块网格组件之外\n\n我们正在对`BrickCollision`组件的位置进行这种调整，以便玩家在砖块下跳跃时只能击中`UBoxComponent`。通过使其稍微超出`BrickMesh`组件，我们可以更好地控制这一点，并确保该组件不会被玩家以任何其他方式击中。\n\n注意\n\n您可以在这里找到本练习的资产和代码:[https://packt.live/3kr7rh6](https://packt.live/3kr7rh6)。\n\n完成本练习后，您可以为`SuperSideScroller_Brick`类创建基础框架，并组合蓝图参与者来表示游戏世界中的砖块。通过添加立方体网格和砖块材料，您为砖块添加了良好的视觉效果。在下面的练习中，您将把剩余的 C++ 逻辑添加到砖块中。这将允许玩家摧毁砖块并获得收藏品。\n\n## 练习 15.08:添加砖块类 C++ 逻辑\n\n在前面的练习中，您通过添加必要的组件和创建`BP_Brick`蓝图参与者来创建`SuperSideScroller_Brick`类的基础框架。在本练习中，您将在*练习 15.07* 、*的 C++ 代码之上添加创建砖块类*，以将逻辑授予`Brick`类。这将允许砖块给玩家硬币收藏品。执行以下步骤来完成此操作:\n\n1.  To begin, we need to create a function that will add the collectible to the player. Add the following function declaration to the `SuperSideScroller_Brick.h` header file, under our `Private Access Modifier`:\n\n    ```cpp\n    void AddCollectable(class ASuperSideScroller_Player* Player);\n    ```\n\n    我们希望传入对`SuperSideScroller_Player`类的引用，以便我们可以从该类调用`IncrementNumberofCollectables()`函数。\n\n2.  Next, create a void function declaration called `PlayHitSound()` under our `Private Access Modifier`:\n\n    ```cpp\n    void PlayHitSound();\n    ```\n\n    `PlayHitSound()`功能将负责生成您在*练习 15.07* 、*中创建的`HitSound`属性，创建砖块类*。\n\n3.  Finally, create another void function declaration called `PlayHitExplosion()` under our `Private Access Modifier`:\n\n    ```cpp\n    void PlayHitExplosion();\n    ```\n\n    `PlayHitExplosion()`功能将负责生成您在*练习 15.07* 、*中创建的`Explosion` 属性，创建砖块类*。\n\n    在头文件中声明了`SuperSideScroller_Brick`类所需的剩余函数后，让我们继续在源文件中定义这些函数。\n\n4.  At the top of the `SuperSideScroller_Brick.cpp` source file, add the following `#includes` to the list of `include` directories that already exist for this class:\n\n    ```cpp\n    #include \"Engine/World.h\"\n    #include \"Kismet/GameplayStatics.h\"\n    #include \"SuperSideScroller_Player.h\"\n    ```\n\n    包含`World`和`GameplayStatics`类对于生成砖块的`HitSound`和`Explosion`效果是必要的。包括`SuperSideScroller_Player`类需要调用`IncrementNumberofCollectables()`类函数。\n\n5.  让我们从`AddCollectable()`函数的函数定义开始。添加以下代码:\n\n    ```cpp\n    void ASuperSideScroller_Brick::AddCollectable(class   ASuperSideScroller_Player* Player)\n    {\n    }\n    ```\n\n6.  现在，使用`Player`函数输入参数\n\n    ```cpp\n    Player->IncrementNumberofCollectables(CollectableValue);\n    ```\n\n    调用`IncrementNumberofCollectables()`函数\n7.  对于`PlayHitSound()`函数，在从`UGameplayStatics`类对`SpawnSoundAtLocation`进行函数调用之前，您需要获取对`UWorld*`对象的引用并验证`HitSound`属性是否有效。这是一个你做过很多次的过程，所以这是整个功能代码:\n\n    ```cpp\n    void ASuperSideScroller_Brick::PlayHitSound()\n    {\n      UWorld* World = GetWorld();\n      if (World)\n      {\n        if (HitSound)\n        {\n          UGameplayStatics::SpawnSoundAtLocation(World, HitSound,         GetActorLocation());\n        }\n      }\n    }\n    ```\n\n8.  Just like the `PlayHitSound()` function, the `PlayHitExplosion()` function will work in an almost similar way, and it's a process you have done many times in this project. Add the following code to create the function definition:\n\n    ```cpp\n    void ASuperSideScroller_Brick::PlayHitExplosion()\n    {\n      UWorld* World = GetWorld();\n      if (World)\n      {\n        if (Explosion)\n        {\n          UGameplayStatics::SpawnEmitterAtLocation(World, Explosion,         GetActorTransform());\n        }\n      }\n    }\n    ```\n\n    定义了这些功能之后，让我们更新`OnHit()`功能，这样如果玩家真的击中了`BrickCollision`组件，我们就可以衍生出`HitSound`和`Explosion`，还可以给玩家的收藏增加一枚可收藏的硬币。\n\n9.  首先，在`OnHit()`函数中，创建一个名为`ASuperSideScroller_Player`类型的新变量`Player`，该变量等于该函数的`OtherActor`输入参数的`Cast`，如下图所示:\n\n    ```cpp\n    ASuperSideScroller_Player* Player =   Cast<ASuperSideScroller_Player>(OtherActor);\n    ```\n\n10.  接下来，我们只想在`Player`有效且`bHasCollectable`为`True`的情况下继续该功能。增加以下`if()`声明:\n\n    ```cpp\n    if (Player && bHasCollectable)\n    {\n    }\n    ```\n\n11.  如果满足`if()`语句中的条件，那就是我们需要调用`AddCollectable()`、`PlayHitSound()`和`PlayHitExplosion()`功能的时候。确保在`AddCollectable()`功能中也输入`Player`变量:\n\n    ```cpp\n    AddCollectable(Player);\n    PlayHitSound();\n    PlayHitExplosion();\n    ```\n\n12.  最后添加函数调用破坏`if()`语句里面的砖块:\n\n    ```cpp\n    Destroy();\n    ```\n\n13.  根据我们的需要定义`OnHit()`函数，重新编译 C++ 代码，但不要返回到虚幻引擎 4 编辑器。\n14.  对于砖块爆炸的 VFX 和 SFX，我们将需要从`Epic Games Launcher`迁移两个独立项目中的资产，这两个项目是我们可以使用的:`Blueprints` 项目和`Content Examples`项目。\n15.  使用您从以前练习中获得的知识，使用 4.24 版引擎下载并安装这些项目。这两个项目都可以在`Engine Feature Samples`类别下的`Learn`选项卡中找到。\n16.  安装完成后，打开`Content Examples`项目，在`Content Browser`窗口中找到`P_Pixel_Explosion`资产。\n17.  *右键点击*该资产，然后选择`Asset Actions`，再选择`Migrate`。将该资产及其所有引用的资产迁移到您的`SuperSideScroller`项目中。\n18.  一旦该资产成功迁移，关闭`Content Examples`项目并打开`Blueprints`项目。\n19.  从`Blueprints`项目的`Content Browser`窗口，找到`Blueprints_TextPop01`资产。\n20.  *Right-click* this asset, then select `Asset Actions`, and then `Migrate`. Migrate this asset and all its referenced assets into your `SuperSideScroller` project.\n\n    将这些资产迁移到您的项目后，返回到您的`SuperSideScroller`项目的虚幻引擎 4 编辑器。\n\n21.  导航至`Content Browser`窗口中的`Brick`文件夹，双击`BP_Brick`资源的将其打开。\n22.  在演员的`Details`面板中，找到`Super Side Scroller Brick`部分，将`HitSound`参数设置为您导入的`Blueprints_TextPop01`声波。\n23.  接下来，将导入的`P_Pixel_Explosion`粒子添加到`Explosion`参数中。\n24.  重新编译`BP_Brick`蓝图，在你的关卡中增加两个这样的角色。\n25.  Set one of the bricks so that the `bHasCollectable` parameter is `True`; set the other to `False`. Please refer to the following screenshot:\n\n    ![Figure 15.30: This Brick actor is set to have a collectible spawn ](img/B16183_15_30.jpg)\n\n    图 15.30:这个 Brick 演员将会有一个可收集的种子\n\n26.  Using PIE, observe the differences in behavior between the two brick actors when you attempt to hit the bottom of the brick with the character's head when jumping, as shown in the following screenshot:\n\n    ![Figure 15.31: Now, the player can hit the brick and it will be destroyed ](img/B16183_15_31.jpg)\n\n图 15.31:现在，玩家可以击中砖块，砖块将被摧毁\n\n当`bHasCollectable`为`True`时，`SuperSideScroller_Brick`会玩我们的`HitSound`，产卵`Explosion`粒子系统，给玩家加一枚可收藏的硬币，被消灭。\n\n注意\n\n您可以在这里找到本练习的资产和代码:[https://packt.live/3pjhoAv](https://packt.live/3pjhoAv)。\n\n完成本练习后，您现在已经完成了`SuperSideScroller`游戏项目的游戏机制开发。现在，`SuperSideScroller_Brick`类既可以用于平台游戏，也可以用于我们想要的游戏硬币收集机制。\n\n现在砖块可以被摧毁，隐藏的硬币可以被收集，我们为`SuperSideScroller`游戏项目设定的所有游戏元素都完成了。\n\n# 总结\n\n在本章中，您将对您的知识进行测试，为`SuperSideScroller`游戏项目创建剩余的游戏机制。使用 C++ 和蓝图的组合，你开发了药剂加电和硬币，供玩家在关卡中收集。此外，通过使用您从*第 14 章*、*中获得的知识来生成玩家投射物*，您为这些可收集的物品添加了独特的音频和视觉资产，从而为游戏增添了一层精美的光泽。\n\n你学习并利用了虚幻引擎 4 中的`UMG UI`系统，创建了一个简单而有效的 UI 反馈系统来显示玩家已经收集的硬币数量。通过使用`Text`小部件的绑定功能，您可以用玩家当前收集的硬币数量来更新用户界面。最后，你创建了一个`Brick`类，利用你从`SuperSideScroller`项目中学到的知识为玩家隐藏硬币，以便他们可以收集和找到它们。\n\n`SuperSideScroller`项目是一个广泛的项目，扩展了虚幻引擎 4 中可用的许多工具和实践。在*第 10 章*、*创建超视频 Scroller 游戏*中，我们导入了自定义骨骼和动画资产，用于开发玩家角色的动画蓝图。在*第 11 章*、*混合空间 1D、键绑定和状态机*中，我们使用了`Blend spaces`来允许玩家角色在空闲、行走和冲刺动画之间混合，同时还使用了一个`Animation State Machine`来处理玩家角色的跳跃和移动状态。然后我们学习了如何使用角色移动组件来控制玩家的移动和跳跃高度。\n\n在*第 12 章*、*动画混合和蒙太奇*中，我们通过使用`Layered Blend per Bone`功能和`Saved Cached Poses`了解了更多关于`Animation Blueprints`内部动画混合的信息。通过为玩家角色投掷动画的上半身动画添加新的`AnimSlot`，我们能够让玩家动作动画和投掷动画平滑地融合在一起。在*第 13 章**敌方人工智能*中，我们使用行为树和黑板的健壮系统为敌方开发 AI 行为。我们创建了我们自己的`Task`，这将允许敌人的人工智能从一个定制的蓝图移动到中间点，我们也开发了这个蓝图来确定人工智能的巡逻点。\n\n在*第 14 章**产卵玩家投射物*中，我们学习了如何为玩家角色的投掷创建一个`Anim Notify`以及如何在我们的`Animation Montage`中实现这个通知来产卵玩家投射物。然后，我们学习了如何创建投射物以及如何使用`Projectile Movement Component`让玩家的投射物在游戏世界中移动。\n\n最后，在本章中，我们学习了如何使用`UMG`工具集为硬币收藏品创建 UI，以及如何操纵我们的`Character Movement Component`为玩家创建药剂加电。最后，你创建了一个`Brick`类，可以用来隐藏硬币供玩家寻找和收集。\n\n这个总结只是真正地触及了我们在`SuperSideScroller`项目中学到和完成的东西的表面。在您继续之前，这里有一些挑战供您测试您的知识并扩展项目:\n\n1.  增加一个新的能量，降低玩家角色的重力。导入一个自定义网格和音频资产，使这次加电与您制作的药剂加电相比具有独特的外观。\n2.  当玩家角色收集到 10 个硬币时，给玩家加电。\n3.  实现当玩家与人工智能重叠时允许玩家被摧毁的功能。包括当这种情况发生时能够让玩家重生。\n4.  增加另一个使玩家免疫的加电，这样当他们与敌人重叠时就不会被摧毁。(事实上，当一个敌人与这个加电重叠时，它可以消灭敌人。)\n5.  使用您为`SuperSideScroller` 项目开发的所有游戏元素，创建一个新的关卡，利用这些元素打造一个有趣的平台化竞技场。\n6.  当玩家在该区域导航时，添加多个具有有趣巡逻点的敌人来挑战玩家。\n7.  将加电装置放在难以到达的地方，这样玩家就需要提高他们的平台技能来获得它们。\n8.  为玩家制造危险的陷阱，如果玩家从地图上掉下来，就会破坏玩家的功能。\n\n在下一章中，您将了解多人游戏、服务器-客户端架构的基础知识，以及在虚幻引擎 4 中用于多人游戏的游戏框架类。你将利用这些知识来扩展虚幻引擎 4 中的多人 FPS 项目。"
  },
  {
    "path": "docs/game-dev-proj-ue/15.md",
    "content": "# 十六、多人游戏基础\n\n概观\n\n在这一章中，你将被介绍一些重要的多人游戏概念，以便使用虚幻引擎 4 的网络框架为你的游戏增加多人游戏支持。\n\n到本章结束时，您将了解基本的多人游戏概念，如服务器-客户端体系结构、连接和参与者所有权，以及角色和变量复制。您将能够实现这些概念来创建一个自己的多人游戏。您还可以制作一个 2D 混合空间，它允许您在 2D 网格中的动画之间进行混合。最后，您将学习如何在运行时使用`Transform (Modify) Bone`节点来控制骨骼网格骨骼。\n\n# 简介\n\n在前一章中，我们完成了`SuperSideScroller`游戏，并使用了 1D 混合空间、动画蓝图和动画蒙太奇。在本章中，我们将在这些知识的基础上学习如何使用虚幻引擎为游戏添加多人游戏功能。\n\n多人游戏在过去的十年里发展了很多。诸如堡垒之夜、PUBG、传奇联盟、火箭联盟、Overwatch 和 CS: GO 等游戏在游戏社区获得了大量的人气，并取得了巨大的成功。如今，几乎所有的游戏都需要某种多人游戏体验，才能更加贴切和成功。\n\n究其原因，是它在现有游戏玩法的基础上增加了一层新的可能性，比如可以在合作模式(*也叫 co-op 模式*)下和朋友一起玩，或者和来自世界各地的人对战，大大增加了一款游戏的寿命和价值。\n\n在下一个主题中，我们将讨论多人游戏的基础。\n\n# 多人基础\n\n你可能在玩游戏的时候经常听到多人游戏这个术语，但是它对游戏开发者来说意味着什么呢？多人游戏，在现实中，只是服务器和其连接的客户端之间通过网络(*互联网或局域网*)发送的一组指令，目的是给玩家一种共享世界的错觉。\n\n要做到这一点，服务器需要能够与客户端对话，但也需要能够与客户端对话(客户端到服务器)。这是因为客户端通常会影响游戏世界，所以他们需要一种方法能够在玩游戏时通知服务器他们的意图。\n\n服务器和客户端之间这种来回通信的一个例子是当玩家在游戏中试图发射武器时。请看下图，它显示了客户端-服务器交互:\n\n![Figure 16.1: Client-server interaction when a player wants to fire  a weapon in a multiplayer game](img/B16183_16_01.jpg)\n\n图 16.1:当玩家想要在多人游戏中发射武器时的客户端-服务器交互\n\n我们来看看*图 16.1* 中显示了什么:\n\n1.  玩家按住*鼠标左键*，该玩家的客户端告诉服务器想要发射武器。\n2.  服务器通过检查以下内容来验证玩家是否可以发射武器:\n    *   如果玩家还活着\n    *   如果玩家装备了武器\n    *   如果玩家有足够的弹药\n3.  如果所有验证都有效，则服务器将执行以下操作:\n    *   运行逻辑来扣除弹药\n    *   在服务器上生成投射执行元，它会自动发送给所有客户端\n    *   在所有客户端中的角色实例上播放 fire 动画，以确保所有客户端之间的同步性，这有助于推销这是同一个世界的想法，尽管事实并非如此\n4.  如果任何验证失败，服务器会告诉特定的客户端该做什么:\n    *   玩家死了，不要做任何事\n    *   玩家没有装备武器–不要做任何事情\n    *   玩家没有足够的弹药-播放空的咔哒声\n\n请记住，如果您希望您的游戏支持多人游戏，那么强烈建议您在开发周期中尽快这样做。如果你尝试运行一个多人模式的单人项目，你会注意到有些功能可能*只是*工作，但可能大部分功能都不能正常工作或如预期的那样。\n\n其原因是，当你在单人模式下执行游戏时，代码会在本地即时运行，但当你将多人模式加入到等式中时，你就加入了外部因素，比如一个权威服务器，它会在网络上与客户端进行具有延迟的对话，正如你在*图 16.1* 中看到的那样。\n\n为了让一切正常工作，您需要将现有代码分解为以下内容:\n\n*   只在服务器上运行的代码\n*   只在客户端运行的代码\n*   运行在两者上的代码，这可能需要很多时间，这取决于你的单人游戏的复杂性\n\n为了给游戏增加多人支持，虚幻引擎 4 自带了一个已经内置的非常强大且带宽高效的网络框架，采用了权威的服务器-客户端架构。\n\n下面是它的工作原理:\n\n![Figure 16.2: Server-client architecture in Unreal Engine 4 ](img/B16183_16_02.jpg)\n\n图 16.2:虚幻引擎 4 中的服务器-客户端架构\n\n在*图 16.2* 中，可以看到服务器-客户端架构在虚幻引擎 4 中是如何工作的。每个玩家控制一个客户端，该客户端使用**双向连接**与服务器通信。服务器以一种游戏模式(*只存在于服务器*中)运行一个特定的关卡，控制信息流，让客户端在游戏世界中可以看到并相互交互。\n\n注意\n\n多人游戏可能是一个非常高级的话题，所以接下来的几章将作为一个介绍来帮助你理解要点，但它不会是一个深入的了解。因此，为了简单起见，可能会省略一些概念。\n\n在下一节中，我们将研究服务器。\n\n# 服务器\n\n服务器是架构中最关键的部分，因为它负责处理大部分工作并做出重要决策。\n\n以下是服务器主要职责的概述:\n\n1.  **创建和管理共享世界实例**:服务器在特定的级别和游戏模式下运行自己的游戏实例(*这将在后面的章节*中介绍)，并将作为所有连接的客户端之间的共享世界。正在使用的级别可以在任何时间点更改，如果适用，服务器可以自动将所有连接的客户端一起带来。\n2.  **Handling client join and leave requests**: If a client wants to connect to a server, it needs to ask for permission. To do this, the client sends a join request to the server, through a direct IP connection (*explained in the next section*) or an online subsystem such as Steam. Once the join request reaches the server, it will perform some validations to determine whether the request is accepted or rejected.\n\n    但是，您应该知道服务器拒绝加入游戏的请求有几个原因。最常见的情况是服务器已经满负荷，不能再接收更多的客户端，或者客户端使用的是过时的游戏版本。如果服务器接受请求，那么具有连接的玩家控制器被分配给客户端，并且游戏模式中的`PostLogin`功能被调用。从那时起，客户端将进入游戏，并成为共享世界的一部分，玩家将能够看到并与其他客户端互动。如果一个客户端在任何时间点断开连接，那么将通知所有其他客户端，并调用游戏模式下的`Logout`功能。\n\n3.  **Spawning the actors that all of the clients need to know about**: If you want to spawn an actor that exists in all of the clients, then you need to do that on the server. The reason for this is the server has the authority and is the only one that can tell each client to create its own instance of that actor.\n\n    这是多人游戏中产生角色的最常见方式，因为大多数角色需要存在于所有客户端中。这方面的一个例子是加电，所有客户端都可以看到并与之交互。\n\n4.  **运行关键玩法逻辑**:为了保证游戏对所有客户端都是公平的，关键玩法逻辑只需要在服务器端执行即可。如果客户端负责处理生命值的扣除，那将是非常可利用的，因为玩家可以使用一个工具在内存中一直将当前生命值更改为 100%，这样玩家就永远不会在游戏中死亡。\n5.  **处理变量复制**:如果您有一个复制的变量(*在本章*中介绍)，那么它的值应该只在服务器上更改。这将确保所有客户端的值都自动更新。您仍然可以更改客户端上的值，但它将始终被服务器上的最新值替换，以防止作弊并确保所有客户端同步。\n6.  **处理来自客户端**的 RPC:服务器需要处理来自客户端的远程过程调用(*第 17 章*、*远程过程调用*)。\n\n现在您已经知道了服务器的功能，我们可以讨论一下在虚幻引擎 4 中创建服务器的两种不同方式。\n\n## 专用服务器\n\n专用服务器仅运行服务器逻辑，因此您不会看到游戏运行时的典型窗口，在该窗口中，您作为本地玩家控制角色。此外，如果您使用`-log`命令提示符运行专用服务器，您将有一个控制台窗口，记录服务器上发生的相关信息，例如客户端是否已连接或断开连接等。作为开发人员，您也可以使用`UE_LOG`宏记录自己的信息。\n\n使用专用服务器是为多人游戏创建服务器的一种非常常见的方式，由于它比监听服务器(下一节中介绍的*更轻量级)，您可以将其托管在服务器堆栈上，并保持其运行。*\n\n要在虚幻引擎 4 中启动专用服务器，可以使用以下命令参数:\n\n*   Run the following command to start a dedicated server inside an editor through a shortcut or Command Prompt:\n\n    ```cpp\n    <UE4 Install Folder>\\Engine\\Binaries\\Win64\\UE4Editor.exe   <UProject Location> <Map Name> -server -game -log\n    ```\n\n    这里有一个例子:\n\n    ```cpp\n    C:\\Program Files\\Epic   Games\\UE_4.24\\Engine\\Binaries\\Win64\\UE4Editor.exe   D:\\TestProject\\TestProject.uproject TestMap -server -game -log\n    ```\n\n*   A packaged project requires a special build of the project built specifically to serve as a dedicated server.\n\n    注意\n\n    您可以通过访问[https://allars blog . com/2015/11/06/support-special-servers/](https://allarsblog.com/2015/11/06/support-dedicated-servers/)和[https://www . ue4 community . wiki/special _ Server _ Guide _(Windows)](https://www.ue4community.wiki/Dedicated_Server_Guide_(Windows))了解更多关于设置打包专用服务器的信息。\n\n## 监听服务器\n\n监听服务器同时充当服务器和客户端，因此您也将有一个窗口，在这里您可以作为客户端使用这种服务器类型玩游戏。它还有一个优势，那就是它是让服务器运行的最快方式，但是它没有专用服务器那么轻量级，所以可以同时连接的客户端数量会受到限制。\n\n要启动侦听服务器，可以使用以下命令参数:\n\n*   Run the following command to start a dedicated server inside an editor through a shortcut or Command Prompt:\n\n    ```cpp\n    <UE4 Install Folder>\\Engine\\Binaries\\Win64\\UE4Editor.exe   <UProject Location> <Map Name>?Listen -game\n    ```\n\n    这里有一个例子:\n\n    ```cpp\n    C:\\Program Files\\Epic   Games\\UE_4.24\\Engine\\Binaries\\Win64\\UE4Editor.exe   D:\\TestProject\\TestProject.uproject TestMap?Listen -game\n    ```\n\n*   A packaged project (development builds only) requires a special build of the project built specifically to serve as a dedicated server:\n\n    ```cpp\n    <Project Name>.exe <Map Name>?Listen -game\n    ```\n\n    这里有一个例子:\n\n    ```cpp\n    D:\\Packaged\\TestProject\\TestProject.exe TestMap?Listen –game\n    ```\n\n在下一节中，我们将讨论客户。\n\n# 客户\n\n客户端是架构中最简单的部分，因为大多数参与者在服务器上都有权限，所以在这些情况下，工作将在服务器上完成，客户端只需服从它的命令。\n\n以下是客户主要职责的概述:\n\n1.  **从服务器强制执行变量复制**:服务器通常对客户端知道的所有参与者拥有权限，因此当服务器上复制变量的值发生变化时，客户端也需要强制执行该值。\n2.  **处理来自服务器**的 RPC:客户端需要处理从服务器发送的远程过程调用(包含在*第 17 章*、*远程过程调用*中)。\n3.  **模拟时预测运动**:当客户端模拟一个演员时(*将在本章后面的*中介绍)，它需要根据演员的速度本地预测它将会在哪里。\n4.  **Spawning the actors that only a client needs to know about**: If you want to spawn an actor that only exists on a client, then you need to do that on that specific client.\n\n    这是生成参与者的最不常见的方式，因为很少有希望参与者只存在于客户端的情况。这方面的一个例子是你在多人生存游戏中看到的放置预览演员，玩家控制一面半透明版本的墙，其他玩家在实际放置之前看不到它。\n\n客户端可以通过不同的方式加入服务器。以下是最常见的方法列表:\n\n*   Using the Unreal Engine 4 console (by default is the *`* key) to open it and type:\n\n    ```cpp\n    Open <Server IP Address>\n    ```\n\n    例如:\n\n    ```cpp\n    Open 194.56.23.4\n    ```\n\n*   Using the `Execute Console Command` Blueprint node. An example is as follows:\n\n    ![Figure 16.3: Joining a server with an example IP with the Execute Console Command node ](img/B16183_16_03.jpg)\n\n图 16.3:使用执行控制台命令节点加入一个带有示例 IP 的服务器\n\n*   Using the `ConsoleCommand` function in `APlayerController` as follows:\n\n    ```cpp\n    PlayerController->ConsoleCommand(\"Open <Server IP Address>\");\n    ```\n\n    这里有一个例子:\n\n    ```cpp\n    PlayerController->ConsoleCommand(\"Open 194.56.23.4\");\n    ```\n\n*   Using the editor executable through a shortcut or Command Prompt:\n\n    ```cpp\n    <UE4 Install Folder>\\Engine\\Binaries\\Win64\\UE4Editor.exe   <UProject Location> <Server IP Address> -game\n    ```\n\n    这里有一个例子:\n\n    `C:\\Program Files\\Epic Games\\UE_4.24\\Engine\\Binaries\\Win64\\UE4Editor.exe D:\\TestProject\\TestProject.uproject 194.56.23.4 -game`\n\n*   Using a packaged development build through a shortcut or Command Prompt:\n\n    ```cpp\n    <Project Name>.exe  <Server IP Address>\n    ```\n\n    这里有一个例子:\n\n    `D:\\Packaged\\TestProject\\TestProject.exe 194.56.23.4`\n\n在下面的练习中，我们将在多人游戏中测试虚幻引擎 4 附带的第三人称模板。\n\n## 练习 16.01:测试多人游戏中的第三人称模板\n\n在本练习中，我们将创建一个第三人称模板项目，并在多人游戏中使用。\n\n以下步骤将帮助您完成练习。\n\n1.  Create a new `Third Person` template project using `Blueprints` called `TestMultiplayer` and save it to a location of your choosing.\n\n    一旦创建了项目，它就应该打开编辑器。我们现在将在多人游戏中测试这个项目，看看它的表现:\n\n2.  在编辑器中，`Play`按钮的右侧，有一个箭头指向下方的选项。点击它，你会看到一个选项列表。在`Multiplayer Options`部分，您可以配置您想要使用多少个客户端，以及您是否想要一个专用服务器。\n3.  不勾选`Run Dedicated Server`，将`Number of Players`改为`3`，点击`New Editor Window (PIE)`。\n4.  You should see three windows on top of each other representing the three clients:\n\n    ![Figure 16.4: Launching three client windows with a listen server ](img/B16183_16_04.jpg)\n\n    图 16.4:使用监听服务器启动三个客户端窗口\n\n    如您所见，这有点混乱，所以让我们更改窗口的大小。按下键盘上的 *Esc* 停止播放。\n\n5.  再次点击`Play`按钮旁边的向下箭头，选择最后一个选项`Advanced Settings`。\n6.  搜索`Game Viewport Settings`部分。将`New Viewport Resolution`更改为`640x480`并关闭`Editor Preferences`标签。\n7.  Play the game again and you should see the following:\n\n    ![Figure 16.5: Launching three client windows using a 640x480 resolution with a listen server  ](img/B16183_16_05.jpg)\n\n图 16.5:使用 640x480 分辨率和监听服务器启动三个客户端窗口\n\n一旦你开始玩，你会注意到窗口的标题栏写着`Server`、`Client 1`和`Client 2`。由于您可以在`Server`窗口中控制一个字符，这意味着我们正在运行一个**监听服务器**，其中服务器和客户端在同一个窗口中运行。当这种情况发生时，您应该将窗口标题解释为`Server + Client 0`，而不仅仅是`Server`，以避免混淆。\n\n完成本练习后，您现在有了一个运行一个服务器和三个客户端的设置(`Client 0`、`Client 1`和`Client 2`)。\n\n注意\n\n当多个窗口同时运行时，您会注意到一次只能将输入焦点放在一个窗口上。要将焦点转移到另一个窗口，只需按 *Shift* + *F1* 即可失去当前输入焦点，然后只需点击您想要关注的新窗口即可。\n\n如果你在其中一个窗口玩游戏，你会注意到你可以移动和跳跃，其他客户端也可以看到。\n\n一切正常的原因是，角色类附带的角色移动组件会自动为您复制位置、旋转和下落状态(用于显示您是否在跳跃)。如果你想添加一个自定义行为，比如一个攻击动画，你不能只告诉客户端在按下一个键的时候在本地播放一个动画，因为这对其他客户端不起作用。这就是为什么你需要服务器，作为一个中介，告诉所有的客户端在一个客户端按键的时候播放动画。\n\n# 套装版\n\n一旦你完成了这个项目，最好打包它(如前几章所述的*，这样我们就有了一个不使用虚幻引擎编辑器的纯独立版本，它将运行得更快，更轻量级。*\n\n以下步骤将帮助您创建打包版本的*练习 16.01* ，*测试多人文件*中的第三人模板:\n\n1.  前往`File` - > `Package Project` - > `Windows` - > `Windows (64-bit)`。\n2.  选择一个文件夹来放置打包的构建，并等待它完成。\n3.  转到选中的文件夹，打开里面的`WindowsNoEditor`文件夹。\n4.  *右键点击`TestMultiplayer.exe`上的*，选择`Create Shortcut`。\n5.  重命名新快捷方式`Run Server`。\n6.  *右键点击*，选择`Properties`。\n7.  在目标上，追加`ThirdPersonExampleMap?Listen -server`，这将使用`ThirdPersonExampleMap`创建一个监听服务器。你应该以这个结束:\n\n    ```cpp\n    \"<Path>\\WindowsNoEditor\\TestMultiplayer.exe\"   ThirdPersonExampleMap?Listen -server\n    ```\n\n8.  点击`OK`运行快捷方式。\n9.  你应该得到一个 Windows 防火墙提示，所以允许它。\n10.  让服务器保持运行，回到文件夹，从`TestMultiplayer.exe`创建另一个快捷方式。\n11.  改名`Run Client`。\n12.  *右键点击*，选择`Properties`。\n13.  在目标上，附加`127.0.0.1`，这是您的本地服务器的 IP。你应该以`\"<Path>\\WindowsNoEditor\\TestMultiplayer.exe\" 127.0.0.1`结束。\n14.  点击`OK`运行快捷方式。\n15.  您现在已连接到侦听服务器，因此可以看到彼此的角色。\n16.  每次点击`Run Client`快捷方式，都会给服务器增加一个新的客户端，这样就可以让几个客户端在同一台机器上运行。\n\n在下一节中，我们将关注连接和所有权。\n\n# 联系和所有权\n\n在虚幻引擎中使用多人游戏时，需要理解的一个重要概念是连接。当一个客户端加入一个服务器时，它将获得一个新的**玩家控制器**，并有一个与之相关的连接。\n\n如果一个参与者没有与服务器的有效连接，那么该参与者将不能执行复制操作，例如变量复制(本章后面的*)或调用 RPC(在*第 17 章，* *远程过程调用*)。*\n\n *如果玩家控制器是唯一拥有连接的参与者，那么这是否意味着它是唯一可以执行复制操作的地方？不，这就是`AActor`中定义的`GetNetConnection`功能发挥作用的地方。\n\n在对一个 actor 进行复制操作(比如变量复制或者调用 RPC)时，虚幻框架会通过调用其上的`GetNetConnection()`函数来获取 actor 的连接。如果连接有效，则复制操作将被处理，如果无效，则不会发生任何事情。`GetNetConnection()`最常见的实现来自`APawn`和`AActor`。\n\n让我们来看看`APawn`类是如何实现`GetNetConnection()`函数的，该函数通常用于字符:\n\n```cpp\nclass UNetConnection* APawn::GetNetConnection() const\n{\n  // if have a controller, it has the net connection\n  if ( Controller )\n  {\n    return Controller->GetNetConnection();\n  }\n  return Super::GetNetConnection();\n}\n```\n\n前面的实现是虚幻引擎 4 源代码的一部分，它将首先检查棋子是否有有效的控制器。如果控制器有效，那么它将使用它的连接。如果控制器无效，那么它将使用`GetNetConnection()`功能的父实现，在`AActor`上:\n\n```cpp\nUNetConnection* AActor::GetNetConnection() const\n{\n  return Owner ? Owner->GetNetConnection() : nullptr;\n}\n```\n\n前面的实现也是虚幻引擎 4 源代码的一部分，它将检查参与者是否有有效的所有者。如果有，它会使用所有者的连接；如果没有，它将返回一个无效的连接。那么这个`Owner`变量是什么呢？每个演员都有一个名为`Owner`的变量(可以通过调用`SetOwner`函数来设置其值)，该变量显示哪个演员*拥有*它，因此您可以将其视为父演员。\n\n在`GetNetConnection()`的这个实现中使用所有者的连接将像一个层次结构一样工作。如果在所有者层次结构中向上移动时，它发现某个所有者是播放器控制器或由播放器控制器控制，那么它将具有有效的连接，并且能够处理复制操作。请看下面的例子。\n\n注意\n\n在监听服务器中，由客户端控制的角色的连接总是无效的，因为该客户端已经是服务器的一部分，因此不需要连接。\n\n想象一个武器演员被放在世界上，它只是坐在那里。在这种情况下，武器不会有拥有者，所以如果武器试图做任何复制操作，比如变量复制或调用 RPC，什么都不会发生。\n\n但是，如果客户拿起武器，用角色的值在服务器上调用`SetOwner`，那么武器现在将有一个有效的连接。这样做的原因是因为武器是一个行动者，所以为了得到它的连接，它会使用`GetNetConnection()`的`AActor`实现，这个实现会返回它的拥有者的连接。既然业主是客户的性格，那就用`APawn`的`GetNetConnection()`实现。角色有一个有效的播放器控制器，所以这是函数返回的连接。\n\n这里有一个图表来帮助你理解这个逻辑:\n\n![Figure 16.6: Connections and ownership example of a weapon actor ](img/B16183_16_06.jpg)\n\n图 16.6:武器角色的连接和所有权示例\n\n让我们理解无效所有者的要素:\n\n*   `AWeapon`不会覆盖`GetNetConnection`功能，所以要获取武器的连接，会调用找到的第一个实现，也就是`AActor::GetNetConnection`。\n*   `AActor::GetNetConnection`的实现调用其所有者`GetNetConnection`。由于没有所有者，连接无效。\n\n有效的所有者包括以下要素:\n\n*   `AWeapon`不覆盖`GetNetConnection`函数，所以要得到它的连接，它会调用找到的第一个实现，也就是`AActor::GetNetConnection`。\n*   `AActor::GetNetConnection`的实现调用其所有者`GetNetConnection`。既然主人是拿起武器的人物，那上面就会叫`GetNetConnection`。\n*   `ACharacter`不覆盖`GetNetConnection`函数，所以要得到它的连接，它会调用找到的第一个实现，也就是`APawn::GetNetConnection`。\n*   The implementation of `APawn::GetNetConnection` uses the connection from the owning player controller. Since the owning player controller is valid, then it will use that connection for the weapon.\n\n    注意\n\n    为了使`SetOwner`按预期工作，它需要在授权上执行，在大多数情况下，授权意味着服务器。如果只在客户端执行`SetOwner`，它仍然无法执行复制操作。\n\n# 角色\n\n当您在服务器上生成一个执行元时，将在服务器上创建一个执行元版本，在每个客户端上创建一个执行元版本。由于同一个演员在游戏的不同实例上有不同的版本(`Server`、`Client 1`、`Client 2`等等)，所以知道哪个版本的演员是哪个很重要。这将允许我们知道在这些实例中可以执行什么逻辑。\n\n为了帮助解决这种情况，每个参与者都有以下两个变量:\n\n*   **本地角色**:演员在当前游戏实例中的角色。例如，如果角色是在服务器上产生的，而当前的游戏实例也是服务器，那么这个版本的角色就有权限，所以你可以在上面运行更关键的游戏逻辑。通过调用`GetLocalRole()`函数来访问。\n*   **远程角色**:演员在远程游戏实例上的角色。例如，如果当前的游戏实例是服务器，那么它返回参与者在客户端的角色，反之亦然。通过调用`GetRemoteRole()`函数来访问。\n\n`GetLocalRole()`和`GetRemoteRole()`函数的返回类型是`ENetRole`，这是一个枚举，可以有以下可能的值:\n\n*   `ROLE_None`:演员没有角色，因为没有被复制。\n*   `ROLE_SimulatedProxy`:当前游戏实例没有对参与者的权限，也没有通过玩家控制器来控制它。这意味着它的运动将通过使用演员速度的最后值来模拟/预测。\n*   `ROLE_AutonomousProxy`:当前游戏实例没有对角色的权限，但是它由玩家控制器控制。这意味着我们可以根据玩家的输入向服务器发送更准确的运动信息，而不仅仅是使用演员速度的最后一个值。\n*   `ROLE_Authority`:当前游戏实例对演员拥有完全的权限。这意味着，如果执行元在服务器上，对执行元的复制变量所做的更改将被视为每个客户端需要通过变量复制强制执行的值。\n\n让我们看看下面的示例代码片段:\n\n```cpp\nENetRole MyLocalRole = GetLocalRole();\nENetRole MyRemoteRole = GetRemoteRole();\nFString String;\nif(MyLocalRole == ROLE_Authority)\n{\n  if(MyRemoteRole == ROLE_AutonomousProxy)\n  {\n    String = «This version of the actor is the authority and\n    it›s being controlled by a player on its client»;\n  }\n  else if(MyRemoteRole == ROLE_SimulatedProxy)\n  {\n    String = «This version of the actor is the authority but \n    it›s not being controlled by a player on its client»;\n  }\n}\nelse String = \"This version of the actor isn't the authority\";\nGEngine->AddOnScreenDebugMessage(-1, 0.0f, FColor::Red, String);\n```\n\n前面的代码片段将本地角色和远程角色的值分别存储到`MyLocalRole`和`MyRemoteRole`中。之后，它将在屏幕上打印不同的消息，这取决于该版本的演员是权威还是由客户端的玩家控制。\n\n注意\n\n重要的是要明白，如果一个演员有一个`ROLE_Authority`的本地角色，并不意味着它在服务器上；这意味着它是在最初产生演员的游戏实例上，因此对其拥有权限。\n\n如果一个客户端产生了一个演员，即使服务器和其他客户端不知道，它的本地角色仍然是`ROLE_Authority`。多人游戏中的大部分演员将由服务器产生；这就是为什么很容易误解权威总是指服务器。\n\n以下表格有助于您理解演员在不同场景中的角色:\n\n![Figure 16.7: Roles that an actor can have in different scenarios ](img/B16183_16_07.jpg)\n\n图 16.7:演员在不同场景中可以扮演的角色\n\n在上表中，您可以看到演员在不同场景中的角色。\n\n让我们分析每个场景，并解释为什么演员有这个角色:\n\n**服务器上产生的演员**\n\n演员在服务器上繁衍，所以服务器版本的那个演员会有`ROLE_Authority`的本地角色和`ROLE_SimulatedProxy`的远程角色，也就是客户端版本的演员的本地角色。对于客户端版本的演员，其本地角色将是`ROLE_SimulatedProxy`，远程角色将是`ROLE_Authority`，这是服务器演员版本的本地角色。\n\n**客户端产生的演员**\n\n演员是在客户端上衍生出来的，所以客户端版本的那个演员会有`ROLE_Authority`的本地角色和`ROLE_SimulatedProxy`的远程角色。由于该执行元没有在服务器上产生，因此它将只存在于产生它的客户端上，因此在服务器和其他客户端上不会有该执行元的版本。\n\n**服务器上产生的玩家拥有的棋子**\n\n棋子是在服务器上产生的，因此服务器版本的棋子将具有本地角色`ROLE_Authority`和远程角色`ROLE_AutonomousProxy`，后者是客户端版本棋子的本地角色。对于客户端版本的棋子，其本地角色将是`ROLE_AutonomousProxy`，因为它由`PlayerController`和远程角色`ROLE_Authority`控制，后者是服务器棋子版本的本地角色。\n\n**客户端上产生的玩家拥有的棋子**\n\n棋子是在客户端上产生的，因此客户端版本的棋子将具有本地角色`ROLE_Authority`和远程角色`ROLE_SimulatedProxy`。因为棋子没有在服务器上产生，所以它将只存在于产生它的客户机上，所以在服务器和其他客户机上不会有这个棋子的版本。\n\n## 练习 16.02:实现所有权和角色\n\n在本练习中，我们将创建一个使用第三人称模板作为基础的 C++ 项目。\n\n创建一个名为`OwnershipTestActor`的新参与者，它有一个静态网格组件作为根组件，在每一个勾号上，它将执行以下操作:\n\n*   在权限上，它将检查在某个半径内哪个字符最接近它(由名为`OwnershipRadius`的`EditAnywhere`变量配置)，并将该字符设置为其所有者。当半径内无人物时，则拥有者为`nullptr`。\n*   显示其本地角色、远程角色、所有者和连接。\n*   编辑`OwnershipRolesCharacter`并覆盖`Tick`功能，使其显示本地角色、远程角色、所有者和连接。\n*   创建一个名为`OwnershipRoles.h`的新头文件，该文件包含`ROLE_TO_String`宏，该宏将`ENetRole`转换为`Fstring`变量。\n\n以下步骤将帮助您完成练习:\n\n1.  使用名为`OwnershipRoles`的`C++ `创建一个新的`Third Person`模板项目，并将其保存到您选择的位置。\n2.  一旦创建了项目，它就应该打开编辑器和 Visual Studio 解决方案。\n3.  使用编辑器，创建一个名为`OwnershipTestActor`的新 C++ 类，该类从`Actor`派生。\n4.  一旦编译完成，Visual Studio 应该会弹出新创建的`.h`和`.cpp`文件。\n5.  关闭编辑器并返回到 Visual Studio。\n6.  In Visual Studio, open the `OwnershipRoles.h` file and add the following macro:\n\n    ```cpp\n    #define ROLE_TO_STRING(Value) FindObject<UEnum>(ANY_PACKAGE,   TEXT(\"ENetRole\"), true)->GetNameStringByIndex((int32)Value)\n    ```\n\n    这个宏将把我们从`GetLocalRole()`函数和`GetRemoteRole()`得到的`ENetRole`枚举转换成`FString`。它的工作方式是通过虚幻引擎的反射系统找到`ENetRole`枚举类型，然后将`Value`参数转换成`FString`变量，这样就可以在屏幕上打印出来。\n\n7.  现在，打开`OwnershipTestActor.h`文件。\n8.  Declare the protected variables for the static mesh component and the ownership radius as shown in the following code snippet:\n\n    ```cpp\n    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category =   \"Ownership Test Actor\")\n    UStaticMeshComponent* Mesh;\n    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = \"Ownership   Test Actor\")\n    float OwnershipRadius = 400.0f;\n    ```\n\n    在前面的代码片段中，我们声明了静态网格组件和`OwnershipRadius`变量，这允许您配置所有权的半径。\n\n9.  接下来，删除`BeginPlay`的声明，将构造函数和`Tick`函数声明移到保护区。\n10.  Now, open the `OwnershipTestActor.cpp` file and add the required header files as mentioned in the following code snippet:\n\n    ```cpp\n    #include \"DrawDebugHelpers.h\"\n    #include \"OwnershipRoles.h\"\n    #include \"OwnershipRolesCharacter.h\"\n    #include \"Components/StaticMeshComponent.h\"\n    #include \"Kismet/GameplayStatics.h\"\n    ```\n\n    在前面的代码片段中，我们包含了`DrawDebugHelpers.h`，因为我们将调用`DrawDebugSphere`和`DrawDebugString`函数。我们包括`OwnershipRoles.h`、`OwnershipRolesCharacter.h`和`StaticMeshComponent.h`，以便`.cpp`文件了解这些类。我们最后包含`GameplayStatics.h`，因为我们将调用`GetAllActorsOfClass`函数。\n\n11.  在构造函数定义中，创建静态网格组件，并将其设置为根组件:\n\n    ```cpp\n    Mesh = CreateDefaultSubobject<UStaticMeshComponent>(\"Mesh\");\n    RootComponent = Mesh;\n    ```\n\n12.  仍然在构造函数中，将`bReplicates`设置为`true`来告诉虚幻引擎，该参与者复制并且应该存在于所有客户端中:\n\n    ```cpp\n    bReplicates = true;\n    ```\n\n13.  删除`BeginPlay`功能定义。\n14.  在`Tick`函数中，绘制一个调试球来帮助可视化所有权半径，如下面的代码片段所示:\n\n    ```cpp\n    DrawDebugSphere(GetWorld(), GetActorLocation(), OwnershipRadius,   32, FColor::Yellow);\n    ```\n\n15.  仍然在`Tick`功能中，创建将在所有权半径内获得最近的`AOwnershipRolesCharacter`的权限特定逻辑，如果与当前不同，则将其设置为所有者:\n\n    ```cpp\n    if (HasAuthority())\n    {\n      AActor* NextOwner = nullptr;\n      float MinDistance = OwnershipRadius;\n      TArray<AActor*> Actors;\n      UGameplayStatics::GetAllActorsOfClass(this,    AOwnershipRolesCharacter::StaticClass(), Actors);\n      for (AActor* Actor : Actors)\n      {\n    const float Distance = GetDistanceTo(Actor);\n        if (Distance <= MinDistance)\n        {\n          MinDistance = Distance;\n          NextOwner = Actor;\n        }\n      }\n      if (GetOwner() != NextOwner)\n      {\n        SetOwner(NextOwner);\n      }\n    }\n    ```\n\n16.  仍然在`Tick`函数中，转换本地/远程角色的值(使用我们之前创建的`ROLE_TO_STRING`宏)、当前所有者以及与字符串的连接:\n\n    ```cpp\n    const FString LocalRoleString = ROLE_TO_STRING(GetLocalRole());\n    const FString RemoteRoleString = ROLE_TO_STRING(GetRemoteRole());\n    const FString OwnerString = GetOwner() != nullptr ? GetOwner()-  >GetName() : TEXT(\"No Owner\");\n    const FString ConnectionString = GetNetConnection() != nullptr ?   TEXT(\"Valid Connection\") : TEXT(\"Invalid Connection\");\n    ```\n\n17.  To finalize the `Tick` function, use `DrawDebugString` to display onscreen the strings we converted in the previous step:\n\n    ```cpp\n    const FString Values = FString::Printf(TEXT(\"LocalRole =   %s\\nRemoteRole = %s\\nOwner = %s\\nConnection = %s\"),   *LocalRoleString, *RemoteRoleString, *OwnerString,   *ConnectionString);\n    DrawDebugString(GetWorld(), GetActorLocation(), Values, nullptr,   FColor::White, 0.0f, true);\n    ```\n\n    注意\n\n    您可以使用`AActor`中定义的`HasAuthority()`助手函数，而不是不断使用`GetLocalRole() == ROLE_Authority`来检查参与者是否有权限。\n\n18.  接下来，打开`OwnershipRolesCharacter.h`并将`Tick`功能声明为受保护:\n\n    ```cpp\n    virtual void Tick(float DeltaTime) override;\n    ```\n\n19.  现在，打开`OwnershipRolesCharacter.cpp`并包含头文件，如下面的代码片段所示:\n\n    ```cpp\n    #include \"DrawDebugHelpers.h\"\n    #include \"OwnershipRoles.h\"\n    ```\n\n20.  实现`Tick`功能:\n\n    ```cpp\n    void AOwnershipRolesCharacter::Tick(float DeltaTime)\n    {\n      Super::Tick(DeltaTime);\n    }\n    ```\n\n21.  将本地/远程角色的值(使用我们之前创建的`ROLE_TO_STRING`宏)、当前所有者和连接转换为字符串:\n\n    ```cpp\n    const FString LocalRoleString = ROLE_TO_STRING(GetLocalRole());\n    const FString RemoteRoleString = ROLE_TO_STRING(GetRemoteRole());\n    const FString OwnerString = GetOwner() != nullptr ? GetOwner()-  >GetName() : TEXT(\"No Owner\");\n    const FString ConnectionString = GetNetConnection() != nullptr ?   TEXT(\"Valid Connection\") : TEXT(\"Invalid Connection\");\n    ```\n\n22.  Use `DrawDebugString` to display onscreen the strings we converted in the previous step:\n\n    ```cpp\n    const FString Values = FString::Printf(TEXT(\"LocalRole =   %s\\nRemoteRole = %s\\nOwner = %s\\nConnection = %s\"), *LocalRoleString, *RemoteRoleString, *OwnerString,   *ConnectionString);\n    DrawDebugString(GetWorld(), GetActorLocation(), Values, nullptr,   FColor::White, 0.0f, true);\n    ```\n\n    最后，我们可以测试这个项目。\n\n23.  运行代码，等待编辑器完全加载。\n24.  在从`OwnershipTestActor`派生的`Content`文件夹中创建一个名为`OwnershipTestActor_BP`的新蓝图。设置`Mesh`使用立方体网格，并在世界上放置它的一个实例。\n25.  转到`Multiplayer Options`，将客户端数量设置为`2`。\n26.  将窗口大小设置为`800x600`。\n27.  Play using `New Editor Window (PIE)`.\n\n    您应该会得到以下输出:\n\n![Figure 16.8: Expected result on the server and Client 1 window ](img/B16183_16_08.jpg)\n\n图 16.8:服务器和客户端 1 窗口的预期结果\n\n通过完成本练习，您将更好地了解连接和所有权是如何工作的。这些都是需要了解的重要概念，因为与复制相关的一切都依赖于它们。\n\n下次当您看到一个参与者没有执行复制操作时，您将知道您需要首先检查它是否有一个**有效连接**和一个**所有者**。\n\n现在，让我们分析服务器和客户端窗口中显示的值。\n\n## 服务器窗口\n\n看看上一个练习中`Server`窗口的输出截图:\n\n![Figure 16.9: The Server window ](img/B16183_16_09.jpg)\n\n图 16.9:服务器窗口\n\n注意\n\n上面写着`Server Character`、`Client 1 Character`、`Ownership Test Actor`的文字不是原截图的一部分，添加是为了帮助大家理解哪个角色哪个演员。\n\n在前面的截图中，可以看到`Server Character`、`Client 1 Character`，以及`Ownership Test`立方体的演员。\n\n我们先来分析一下`Server Character`的数值。\n\n## 服务器通道特征\n\n这是监听服务器正在控制的角色。与该字符相关的值如下:\n\n*   `LocalRole = ROLE_Authority`:因为这个角色是在服务器上衍生出来的，是当前的游戏实例。\n*   `RemoteRole = ROLE_SimulatedProxy`:因为这个角色是服务器上衍生出来的，所以其他客户端应该只模拟它。\n*   `Owner = PlayerController_0`:因为这个角色是由监听服务器的客户端控制的，监听服务器使用第一个`PlayerController`实例`PlayerController_0`。\n*   `Connection = Invalid Connection`:因为我们是监听服务器的客户端，所以不需要连接。\n\n接下来，我们将在同一个窗口中查看`Client 1 Character`。\n\n## 客户端 1 字符\n\n这就是`Client 1`所控制的人物。与该字符相关的值如下:\n\n*   `LocalRole = ROLE_Authority`:因为这个角色是在服务器上衍生出来的，是当前的游戏实例。\n*   `RemoteRole = ROLE_AutonomousProxy`:因为这个角色是在服务器上产生的，但是被另一个客户端控制了。\n*   `Owner = PlayerController_1`:因为这个角色正在被另一个客户端控制，这个客户端使用了第二个`PlayerController`实例`PlayerController_1`。\n*   `Connection = Valid Connection`:因为这个角色正在被另一个客户端控制，所以需要连接到服务器。\n\n接下来，我们将在同一个窗口中观察`OwnershipTest`演员。\n\n## 船东试船演员\n\n这是将它的所有者设置为某个所有权半径内最接近的角色的多维数据集执行元。与此参与者关联的值如下:\n\n*   `LocalRole = ROLE_Authority`:因为这个演员是放在关卡中，在服务器上衍生出来的，是当前的游戏实例。\n*   `RemoteRole = ROLE_SimulatedProxy`:因为这个演员是在服务器中衍生出来的，但是它没有被任何客户端控制。\n*   `Owner`和`Connection`的值将基于最接近的字符。如果所有权半径内没有角色，则他们将具有`No Owner`和`Invalid Connection`的值。\n\n现在，让我们看看`Client 1`窗口:\n\n![Figure 16.10: The Client 1 window ](img/B16183_16_10.jpg)\n\n图 1 6.10:客户端 1 窗口\n\n## 客户端 1 窗口\n\n`Client 1`窗口的值将与`Server`窗口完全相同，除了`LocalRole`和`RemoteRole`的值将被反转，因为它们总是相对于您所在的游戏实例。\n\n另一个例外是，服务器角色没有所有者，其他连接的客户端没有有效的连接。原因是客户端不存储玩家控制器和其他客户端的连接，只有服务器存储，但这将在*第 18 章*、*多人游戏*中的游戏框架类中有更深入的介绍。\n\n在下一节中，我们将研究变量复制。\n\n# 变量复制\n\n服务器保持客户端同步的方法之一是使用变量复制。它的工作方式是，服务器中的变量复制系统每秒每特定次数(在`AActor::NetUpdateFrequency`变量中为每个参与者定义，该变量也暴露给蓝图)将检查客户端中是否有任何需要用最新值更新的复制变量(下一节中解释的*)。*\n\n如果变量满足所有复制条件，则服务器将向客户端发送更新并强制实施新值。\n\n例如，如果您有一个复制的`Health`变量，并且客户端使用黑客工具将该变量的值从`10`设置为`100`，则复制系统将从服务器强制执行真实值，并将其更改回`10`，这将使黑客无效。\n\n只有在以下情况下，才会将变量发送到客户端进行更新:\n\n*   该变量被设置为复制。\n*   服务器上的值已更改。\n*   客户端上的值不同于服务器上的值。\n*   该参与者已启用复制。\n*   参与者是相关的，并且满足所有复制条件。\n\n需要考虑的重要一点是，决定变量是否应该复制的逻辑每秒只执行`AActor::NetUpdateFrequency`次。换句话说，在您更改服务器上的变量值后，服务器不会立即向客户端发送更新请求。它将仅在变量复制系统执行时发送该请求，这是每秒`AActor::NetUpdateFrequency`次，并且它已经确定来自客户端的值不同于来自服务器的值。\n\n例如，如果有一个整数复制一个名为`Test`的变量，该变量的默认值为`5`。如果您在服务器上调用一个将`Test`设置为`3`的函数，并在下一行将其更改为`8`，那么只有后一个更改会向客户端发送更新请求。原因是这两个变化是在`NetUpdateFrequency`间隔之间进行的，所以当变量复制系统执行时，当前值是`8`，由于它不同于客户端的值(仍然是`5`，所以它会更新它们。如果不将其设置为`8`，而是将其设置回`5`，则不会向客户端发送任何更改。\n\n## 重复变量\n\n在虚幻引擎中，任何可以使用`UPROPERTY`宏的变量都可以设置为复制，你可以使用两个说明符来实现。\n\n**复制**\n\n如果你只想说一个变量被复制了，那么你就用`Replicated`说明符。\n\n请看下面的例子:\n\n```cpp\nUPROPERTY(Replicated) \nfloat Health = 100.0f; \n```\n\n在前面的代码片段中，我们声明了一个名为`Health`的浮点变量，就像我们通常做的那样。不同的是，我们添加了`UPROPERTY(Replicated)`来告诉虚幻引擎`Health`变量将被复制。\n\n**重新通知**\n\n如果你想说一个变量被复制并在每次更新时调用一个函数，那么你可以使用`ReplicatedUsing=<Function Name>`说明符。请看下面的例子:\n\n```cpp\nUPROPERTY(ReplicatedUsing=OnRep_Health) \nfloat Health = 100.0f;\nUFUNCTION() \nvoid OnRep_Health()\n{\n  UpdateHUD(); \n}\n```\n\n在前面的代码片段中，我们声明了一个名为`Health`的浮点变量。不同的是，我们增加了`UPROPERTY(ReplicatedUsing=OnRep_Health)`来告诉虚幻引擎这个变量将被复制，并且每次更新它都会调用`OnRep_Health`函数，在这个特定的例子中，它会调用一个函数来更新`HUD`。\n\n通常，回调函数的命名方案是`OnRepNotify_<Variable Name>`或`OnRep_<Variable Name>`。\n\n注意\n\n`ReplicatingUsing`说明符中使用的函数需要标记为`UFUNCTION()`。\n\n**获取终身复制产品**\n\n除了将变量标记为复制之外，您还需要在参与者的`cpp`文件中实现`GetLifetimeReplicatedProps`函数。需要考虑的一点是，一旦您至少有一个复制变量，这个函数就在内部声明，所以您不应该在参与者的头文件中声明它。这个函数的目的是告诉你每个复制的变量应该如何复制。您可以通过在每个要复制的变量上使用`DOREPLIFETIME`宏及其变体来实现这一点。\n\n**doreplitime**\n\n此宏告诉复制系统，复制的变量(作为参数输入)将复制到所有没有复制条件的客户端。\n\n以下是它的语法:\n\n```cpp\nDOREPLIFETIME(<Class Name>, <Replicated Variable Name>); \n```\n\n请看下面的例子:\n\n```cpp\nvoid AVariableReplicationActor::GetLifetimeReplicatedProps(TArray<   FLifetimeProperty >& OutLifetimeProps) const\n{\n  Super::GetLifetimeReplicatedProps(OutLifetimeProps);\n  DOREPLIFETIME(AVariableReplicationActor, Health);\n}\n```\n\n在前面的代码片段中，我们使用`DOREPLIFETIME`宏告诉复制系统,`AVariableReplicationActor`类中的`Health`变量将在没有额外条件的情况下复制。\n\n**doreplitime _ CONDITION**\n\n此宏告诉复制系统，复制的变量(作为参数输入)将只复制到满足条件(作为参数输入)的客户端。\n\n以下是语法:\n\n```cpp\nDOREPLIFETIME_CONDITION(<Class Name>, <Replicated Variable Name>,   <Condition>); \n```\n\n条件参数可以是以下值之一:\n\n*   `COND_InitialOnly`:变量只会复制一次，与初始复制一样。\n*   `COND_OwnerOnly`:变量只会复制给行为人的所有者。\n*   `COND_SkipOwner`:变量不会复制给行为人的所有者。\n*   `COND_SimulatedOnly`:变量只会复制给正在模拟的演员。\n*   `COND_AutonomousOnly`:变量只会复制给自主的行动者。\n*   `COND_SimulatedOrPhysics`:该变量将只复制给正在模拟的演员或`bRepPhysics`设置为真的演员。\n*   `COND_InitialOrOwner`:变量只会复制一次，初始复制还是复制给行为人的所有者。\n*   `COND_Custom`:只有当变量的`SetCustomIsActiveOverride`布尔条件(用于`AActor::PreReplication`函数)为真时，该变量才会复制。\n\n请看下面的例子:\n\n```cpp\nvoid AVariableReplicationActor::GetLifetimeReplicatedProps(TArray<   FLifetimeProperty >& OutLifetimeProps) const\n{\n  Super::GetLifetimeReplicatedProps(OutLifetimeProps);\n  DOREPLIFETIME_CONDITION(AVariableReplicationActor, Health,     COND_OwnerOnly);\n}\n```\n\n在前面的代码片段中，我们使用`DOREPLIFETIME_CONDITION`宏来告诉复制系统，`AVariableReplicationActor`类中的`Health`变量将只为该参与者的所有者复制。\n\n注意\n\n还有更多`DOREPLIFETIME`宏可用，但本书不会涉及。要查看所有变体，请查看虚幻引擎 4 源代码中的`UnrealNetwork.h`文件。请参见以下网址的说明:[。](https://docs.unrealengine.com/en-US/GettingStarted/DownloadingUnrealEngine/index.html)\n\n## 练习 16.03:使用复制、重新通知、doreplifitme 和 DOREPLIFETIME _ CONDITION 复制变量\n\n在本练习中，我们将创建一个 C++ 项目，该项目使用第三人称模板作为基础，并向角色添加两个变量，这两个变量以以下方式复制:\n\n*   变量`A`是一个将使用`Replicated UPROPERTY`说明符和`DOREPLIFETIME`宏的浮点数。\n*   变量`B`是一个整数，将使用`ReplicatedUsing UPROPERTY`说明符和`DOREPLIFETIME_CONDITION`宏。\n\n以下步骤将帮助您完成练习:\n\n1.  使用名为`VariableReplication`的`C++ `创建一个新的`Third Person`模板项目，并将其保存到您选择的位置。\n2.  一旦创建了项目，它就应该打开编辑器和 Visual Studio 解决方案。\n3.  关闭编辑器并返回到 Visual Studio。\n4.  打开`VariableReplicationCharacter.h`文件。\n5.  接下来，在`VariableReplicationCharacter.generated.h`之前包含`UnrealNetwork.h`头文件，它有我们将要使用的`DOREPLIFETIME`宏的定义:\n\n    ```cpp\n    #include \"Net/UnrealNetwork.h\"\n    ```\n\n6.  使用各自的复制说明符\n\n    ```cpp\n    UPROPERTY(Replicated) \n    float A = 100.0f; \n    UPROPERTY(ReplicatedUsing = OnRepNotify_B) \n    int32 B; \n    ```\n\n    ，将受保护变量`A`和`B`声明为`UPROPERTY`\n7.  宣布`Tick`功能受保护:\n\n    ```cpp\n    virtual void Tick(float DeltaTime) override;\n    ```\n\n8.  既然我们已经将变量`B`声明为`ReplicatedUsing = OnRepNotify_B`，那么我们还需要将受保护的`OnRepNotify_B`回调函数声明为`UFUNCTION` :\n\n    ```cpp\n    UFUNCTION() \n    void OnRepNotify_B(); \n    ```\n\n9.  现在，打开`VariableReplicationCharacter.cpp`文件，包括标题`Engine.h`，这样我们就可以使用`AddOnScreenDebugMessage`功能，以及`DrawDebugHelpers.h`，这样我们就可以使用`DrawDebugString`功能:\n\n    ```cpp\n    #include \"Engine/Engine.h\"\n    #include \"DrawDebugHelpers.h\"\n    ```\n\n10.  实现`GetLifetimeReplicatedProps`功能:\n\n    ```cpp\n    void AVariableReplicationCharacter::GetLifetimeReplicatedProps(TArray<   FLifetimeProperty >& OutLifetimeProps) const \n    {\n      Super::GetLifetimeReplicatedProps(OutLifetimeProps);\n    }\n    ```\n\n11.  将其设置为`A`变量，该变量将在没有任何额外条件的情况下复制:\n\n    ```cpp\n    DOREPLIFETIME(AVariableReplicationCharacter, A);\n    ```\n\n12.  将其设置为`B`变量，该变量将只复制给该参与者的所有者:\n\n    ```cpp\n    DOREPLIFETIME_CONDITION(AVariableReplicationCharacter, B,   COND_OwnerOnly);\n    ```\n\n13.  实现`Tick`功能:\n\n    ```cpp\n    void AVariableReplicationCharacter::Tick(float DeltaTime) \n    {\n      Super::Tick(DeltaTime);\n    }\n    ```\n\n14.  Next, run the authority-specific logic that adds `1` to `A` and `B`:\n\n    ```cpp\n    if (HasAuthority()) \n    { \n      A++ ; \n      B++ ; \n    } \n    ```\n\n    因为这个字符会在服务器上产生，所以只有服务器会执行这个逻辑。\n\n15.  在字符位置显示`A`和`B`的值:\n\n    ```cpp\n    const FString Values = FString::Printf(TEXT(\"A = %.2f    B =   %d\"), A, B); \n    DrawDebugString(GetWorld(), GetActorLocation(), Values, nullptr,   FColor::White, 0.0f, true);\n    ```\n\n16.  Implement the `RepNotify` function for variable `B`, which displays on the screen a message saying that the `B` variable was changed to a new value:\n\n    ```cpp\n    void AVariableReplicationCharacter::OnRepNotify_B() \n    {\n      const FString String = FString::Printf(TEXT(\"B was changed by     the server and is now %d!\"), B); \n      GEngine->AddOnScreenDebugMessage(-1, 0.0f, FColor::Red,String); \n    }\n    ```\n\n    最后，您可以测试项目:\n\n17.  运行代码，等待编辑器完全加载。\n18.  转到`Multiplayer Options`，将客户端数量设置为`2`。\n19.  将窗口大小设置为`800x600`。\n20.  使用`New Editor Window (PIE)`播放。\n\n完成本练习后，您将能够在每个客户端上进行游戏，您会注意到角色正在显示各自的`A`和`B`值。\n\n现在，让我们分析一下`Server`和`Client 1`窗口中显示的值。\n\n## 服务器窗口\n\n在`Server`窗口中，你有`Server Character`的值，它是由服务器控制的角色，在后台，你有`Client 1 Character`的值:\n\n![Figure 16.11: The Server window ](img/B16183_16_11.jpg)\n\n图 16 .11:服务器窗口\n\n可以观察到的输出如下:\n\n*   `Server``Character`–`A = 674.00 B = 574`\n*   `Client 1``Character`–`A = 670.00 B = 570`\n\n在这个特定的时间点上，`Server` `Character`的值为`674`代表`A``574`代表`B`。之所以`A`和`B`数值不同，是因为`A`从`100`开始，`B`从`0`开始，这是`A++ `和`B++ `的`574`刻度之后的正确数值。\n\n至于为什么`Client 1` `Character`和服务器角色没有相同的值，那是因为`Client 1`是在服务器之后稍微创建的，所以在这种情况下，计数会被`A++ `和`B++ `的`4`滴答关闭。\n\n接下来，我们将看到`Client 1`窗口。\n\n## 客户端 1 窗口\n\n在`Client 1`窗口中，你有`Client 1 Character`的值，它是由`Client 1`控制的角色，在后台，你有`Server Character`的值:\n\n![Figure 16.12: The Client 1 window ](img/B16183_16_12.jpg)\n\n图 16.12:客户端 1 窗口\n\n可以观察到的输出如下:\n\n*   `Server``Character`–`A = 674.00 B = 0`\n*   `Client 1``Character`–`A = 670.00 B = 570`\n\n`Client 1 Character`具有来自服务器的正确值，因此变量复制正在按预期工作。如果看`Server Character`，`A`就是`674`，没错，但是`B`就是`0`。原因是`A`使用的是`DOREPLIFETIME`，没有添加任何额外的复制条件，所以每次服务器上的变量发生变化，它都会复制变量，让客户端保持最新。\n\n另一方面，变量`B`将`DOREPLIFETIME_CONDITION`与`COND_OwnerOnly`一起使用，由于`Client 1`不是拥有`Server Character`的客户端(*监听服务器的客户端是*)，因此该值不会被复制，并且与`0`的默认值保持不变。\n\n如果你回到代码，将`B`的复制条件改为使用`COND_SimulatedOnly`而不是`COND_OwnerOnly`，你会注意到结果会在`Client 1 window`反转。`B`的价值会为`Server Character`复制，但不会为自己的性格复制。\n\n注意\n\n之所以在`Server`窗口而不是客户端窗口显示`RepNotify`消息，是因为在编辑器中播放时，两个窗口共享相同的过程，因此在屏幕上打印文本不会准确。为了获得正确的行为，您需要运行游戏的打包版本。\n\n# 2D 混合空间\n\n在*第 2 章*、*使用虚幻引擎*中，我们创建了一个 1D 混合空间，根据速度轴的值在角色的运动状态(*空闲、行走和奔跑*)之间进行混合。对于那个特定的例子，它工作得非常好，因为你只需要一个轴，但是如果我们希望角色也能够扫射，那么我们就不能真正做到这一点。\n\n为了探索这种情况，虚幻引擎允许您创建 2D 混合空间。概念几乎完全相同；唯一的区别是你有一个额外的动画轴，所以你不仅可以在水平方向上混合，也可以在垂直方向上混合。\n\n## 练习 16.04:创建运动 2D 混合空间\n\n在本练习中，我们将创建一个使用两个轴而不是一个轴的混合空间。纵轴为`Speed`，在`0`和`800`之间。横轴为`Direction`，代表棋子的速度和旋转/前进矢量之间的相对角度(`-180 to 180`)。\n\n下图将帮助您计算本练习中的方向:\n\n![Figure 16.13: Direction values based on the angle between the forward  vector and the velocity ](img/B16183_16_13.jpg)\n\n图 16.13:基于前向矢量和速度之间角度的方向值\n\n在上图中，您可以看到如何计算方向。前向矢量表示角色当前面对的方向，数字表示如果前向矢量指向该方向，它将与速度矢量形成的角度。如果角色朝某个方向看，并且你按了一个键将角色向右移动，那么速度向量将垂直于向前的向量。这意味着角度是 90 度，这就是我们的方向。\n\n如果我们按照这个逻辑设置我们的 2D 混合空间，我们可以根据角色的移动角度使用正确的动画。\n\n以下步骤将帮助您完成练习:\n\n1.  使用名为`Blendspace2D`的`Blueprints`创建一个新的`Third Person`模板项目，并将其保存到您选择的位置。\n2.  一旦创建了项目，它就应该打开编辑器。\n3.  接下来，您将导入运动动画。在编辑器中，转到`Content\\Mannequin\\Animations`文件夹。\n4.  点击`Import`按钮。\n5.  进入`Chapter16\\Exercise16.04\\Assets`文件夹，选择所有`fbx`文件，点击`Open`按钮。\n6.  在导入对话框中，确保选择角色的骨骼并点击`Import All`按钮。\n7.  将所有新文件保存在`Assets`文件夹中。\n8.  点击`Add New`按钮，选择`Animation -> Blend Space`。\n9.  接下来，选择角色的骨骼。\n10.  重命名混合空间`BS_Movement`并将其打开。\n11.  Create the horizontal `Direction` axis `(-180 to 180)` and the vertical `Speed` axis `(0 to 800)` as shown in the following figure:\n\n    ![Figure 16.14: 2D Blend Space Axis Settings ](img/B16183_16_14.jpg)\n\n    图 16.14: 2D 混合空间轴设置\n\n12.  将`Idle_Rifle_Ironsights`动画拖到`5`网格条目上，其中`Speed`是`0`。\n13.  拖动`Walk_Fwd_Rifle_Ironsights`动画，其中`Speed`是`800`，`Direction`是`0`。\n14.  拖动`Walk_Lt_Rifle_Ironsights`动画，其中`Speed`是`800`，`Direction`是`-90`。\n15.  Drag the `Walk_Rt_Rifle_Ironsights` animation where `Speed` is `800` and `Direction` is `90`.\n\n    您应该会得到一个混合空间，可以通过按住*移动*并移动鼠标来预览。\n\n16.  现在，在`Asset Details`面板上，将`Target Weight Interpolation Speed Per Sec`变量设置为`5`，使插值更加平滑。\n17.  保存并关闭混合空间。\n18.  现在，更新动画蓝图以使用新的混合空间。\n19.  转到`Content\\Mannequin\\Animations`并打开第三人称模板附带的文件–`ThirdPerson_AnimBP`。\n20.  接下来，转到事件图，创建一个名为`Direction`的新浮点变量。\n21.  Set the value of `Direction` with the result of the `Calculate Direction` function, which calculates the angle (-180º to 180º) between the pawn's `velocity` and `rotation`:\n\n    ![Figure 16.15: Calculating the Speed and Direction to use on the 2D Blend Space ](img/B16183_16_15.jpg)\n\n    图 16.15:计算用于 2D 混合空间的速度和方向\n\n    注意\n\n    您可以在以下链接找到前面的全分辨率截图，以便更好地查看:[https://packt.live/3pAbbAl](https://packt.live/3pAbbAl)。\n\n22.  In `AnimGraph`, go to the `Idle/Run` state where the old 1D Blend Space is being used, as shown in the following screenshot:\n\n    ![Figure 16.16: Idle/run state in the AnimGraph ](img/B16183_16_16.jpg)\n\n    图 16.16:动画中的空闲/运行状态\n\n23.  Replace that Blend Space with `BS_Movement` and use the `Direction` variable like so:\n\n    ![Figure 16.17: 1D Blend Space has been replaced by the new 2D Blend Space ](img/B16183_16_17.jpg)\n\n    图 16.17: 1D 混合空间已被新的 2D 混合空间取代\n\n24.  保存并关闭动画蓝图。现在你需要更新角色。\n25.  转到`Content\\ThirdPersonBP\\Blueprints`文件夹，打开`ThirdPersonCharacter`。\n26.  在角色的`Details`面板上，将`Use Controller Rotation Yaw`设置为`true`，这将使角色的`Yaw`旋转始终面向控制旋转的偏航。\n27.  转到角色移动组件，将`Max Walk Speed`设置为`800`。\n28.  将`Orient Rotation to Movement`设置为`false`，这将防止角色向运动方向旋转。\n29.  保存并关闭角色蓝图。\n\n如果你现在用两个客户端玩游戏，移动角色，它会前后走动，但也会扫射，如下图截图所示:\n\n![Figure 16.18: Expected output on the server and Client 1 windows ](img/B16183_16_18.jpg)\n\n图 16.18:服务器和客户端 1 窗口上的预期输出\n\n通过完成本练习，您将更好地理解如何创建 2D 混合空间，它们是如何工作的，以及与仅使用常规 1D 混合空间相比它们所提供的优势。\n\n在下一节中，我们将研究如何变换角色的骨骼，以便我们可以根据相机的俯仰来上下旋转玩家的躯干。\n\n# 变换(修改)骨骼\n\n在我们继续之前，有一个非常有用的节点可以在动画中使用，叫做`Transform (Modify) Bone`节点，它允许你在*运行时*平移、旋转和缩放骨骼。\n\n您可以在`AnimGraph`中添加它，方法是在空白区域用鼠标右键单击，键入`transform modify`，然后从列表中选择该节点。如果你点击`Transform (Modify) Bone`节点，你会在`Details`面板上有很多选项。\n\n这里解释了每个选项的作用。\n\n*   The `Bone to Modify` option will tell the node what bone is going to be transformed.\n\n    在该选项之后，有三个部分代表每个变换操作(`Translation`、`Rotation`和`Scale`)。在每个部分中，您可以执行以下操作:\n\n*   `Translation, Rotation, Scale`: This option will tell the node how much of that specific transform operation you want to apply. The final result will depend on the mode (*covered in the next section*) you have selected.\n\n    有两种方法可以设置该值:\n\n*   设置一个恒定值，如(`X=0.0,Y=0.0,Z=0.0`)\n*   使用变量，因此它可以在运行时更改。要启用此功能，您需要采取以下步骤(此示例针对`Rotation`，但相同的概念适用于`Translation`和`Scale`):\n\n1.  Click the checkbox next to the constant value and make sure it is checked. Once you do that, the text boxes for the constant value will disappear.\n\n    ![Figure 16.19: Check the checkbox ](img/B16183_16_19.jpg)\n\n图 16.19:选中复选框\n\n`Transform (Modify) Bone`将添加一个输入，以便您可以插入您的变量:\n\n![Figure 16.20: Variable used as an input on the Transform (Modify) Bone node ](img/B16183_16_20.jpg)\n\n图 16.20:用作变换(修改)骨骼节点输入的变量\n\n**设置模式**\n\n这将告诉节点如何处理该值。您可以从以下三个选项中选择一个:\n\n*   `Ignore`:不要用提供的值做任何事情。\n*   `Add to Existing`:抓取骨骼的当前值，并将提供的值加入其中。\n*   `Replace Existing`: Replace the current value of the bone with the supplied value.\n\n    **设置空间**\n\n    这将定义节点应用转换的空间。您可以从以下四个选项中选择一个:\n\n*   `World Space`:变换会发生在世界空间。\n*   `Component Space`:变换将发生在骨骼网格组件空间。\n*   `Parent Bone Space`:变换将发生在所选骨骼的父骨骼空间。\n*   `Bone Space`:变换会发生在选中骨骼的空间。\n\n最后但同样重要的是，您有`Alpha`，这是一个允许您控制要应用的变换量的值。例如，如果您将`Alpha`值作为一个浮点值，那么您将有以下不同值的行为:\n\n*   如果`Alpha`为 0.0，则不应用任何变换。\n*   如果`Alpha`是 0.5，那么它只会应用一半的变换。\n*   如果`Alpha`为 1.0，则应用整个变换。\n\n在下一个练习中，我们将使用`Transform (Modify) Bone`节点来启用来自*练习 16.04* 、*创建运动 2D 混合空间*的角色，以基于相机的旋转上下查看。\n\n## 练习 16.05:创建一个上下看的角色\n\n在本练习中，我们将复制来自*练习 16.04* 、*的项目，创建一个运动 2D 混合空间*，并使角色能够基于相机的旋转上下查看。为了实现这一点，我们将使用`Transform (Modify) Bone`节点，根据相机的音高旋转组件空间中的`spine_03`骨骼。\n\n以下步骤将帮助您完成练习:\n\n1.  首先，您需要从*练习 16.04* 、*复制并重命名项目，创建运动 2D 混合空间*。\n2.  从*练习 16.04* 、*复制`Blendspace2D`项目文件夹，创建运动 2D 混合空间*，将其粘贴到新文件夹中，并将其重命名为`TransformModifyBone`。\n3.  Open the new project folder, rename the `Blendspace2D.uproject` file `TransformModifyBone.uproject`, and open it.\n\n    接下来，您将更新动画蓝图。\n\n4.  转到`Content\\Mannequin\\Animations`打开`ThirdPerson_AnimBP`。\n5.  Go to the `Event Graph`, create a float variable called `Pitch`, and set it with the Pitch of the subtraction (or delta) between the pawn's rotation and the base aim rotation, as shown in the following figure:\n\n    ![Figure 16.21: Calculating the Pitch ](img/B16183_16_21.jpg)\n\n    图 16.21:计算间距\n\n    作为使用`Break Rotator`节点的替代方法，您可以在`Return Value`上的上单击鼠标右键*，然后选择`Split Struct Pin`。*\n\n    注意\n\n    `Break Rotator`节点允许您将一个`Rotator`变量分成三个浮动变量，分别代表`Pitch`、`Yaw`和`Roll`。当您想要访问每个单独组件的值时，或者如果您只想处理一个或两个组件，而不想处理整个旋转时，这非常有用。\n\n    考虑到`Split Struct Pin`选项只有在`Return` `Value`没有连接到任何东西时才会出现。一旦您进行了拆分，它将为`Roll`、`Pitch`和`Yaw`创建三条单独的线，就像断开一样，但没有额外的节点。\n\n    您应该会得到以下结果:\n\n    ![Figure 16.22: Calculating the Pitch to look up using the Split Struct Pin option ](img/B16183_16_22.jpg)\n\n    图 16.22:使用拆分结构引脚选项计算要查找的间距\n\n    这个逻辑利用棋子的旋转，从摄像头的旋转中减去，得到`Pitch`中的差值，如下图所示:\n\n    ![Figure 16.23: How to calculate the Delta Pitch ](img/B16183_16_23.jpg)\n\n    图 16.23:如何计算增量间距\n\n6.  Next, go to `AnimGraph` and add a `Transform (Modify) Bone` node with the following settings:\n\n    ![Figure 16.24: Settings for the Transform (Modify) Bone node ](img/B16183_16_24.jpg)\n\n    图 16.24:变换(修改)骨骼节点的设置\n\n    在前面的截图中，我们已经将`Bone to Modify`设置为`spine_03`，因为那是我们想要旋转的骨骼。我们还将`Rotation Mode`设置为`Add to Existing`，因为我们希望保留动画中的原始旋转，并为其添加偏移。其余选项需要有默认值。\n\n7.  Connect the `Transform (Modify) Bone` node to the `State Machine` and the `Output Pose`, as shown in the following screenshot:\n\n    ![Figure 16.25: Transform (Modify) Bone connected to the Output Pose ](img/B16183_16_25.jpg)\n\n图 16.25:变换(修改)连接到输出姿势的骨骼\n\n在上图中，您看到了完整的`AnimGraph`，这将允许角色通过基于相机间距旋转`spine_03`骨骼来上下查看。`State Machine`将是起点，从那里需要转换成组件空间，以便能够使用`Transform (Modify) Bone`节点，该节点在转换回本地空间后将连接到`Output Pose`节点。\n\n注意\n\n我们将`Pitch`变量连接到`Roll`的原因是骨骼中的骨骼是这样内部旋转的。您也可以在输入参数上使用`Split Struct Pin`，因此您不必添加`Make Rotator`节点。\n\n如果你用两个客户端测试项目，将鼠标*上移*和*下移*到其中一个角色上，你会注意到它会上下俯仰，如下图截图所示:\n\n![Figure 16.26: Character mesh pitching up and down, based on the camera rotation ](img/B16183_16_26.jpg)\n\n图 16.26:基于相机旋转的角色网格上下俯仰\n\n通过完成最后的练习，您将了解如何使用动画蓝图中的`Transform (Modify) Bone`节点在运行时修改骨骼。这个节点可以在各种场景中使用，因此它可能对您非常有用。\n\n在下一个活动中，您将通过创建我们将用于多人 FPS 项目的角色来测试所学的一切。\n\n## 活动 16.01:为多人 FPS 项目创建角色\n\n在本活动中，您将为我们将在接下来几章中构建的多人 FPS 项目创建角色。角色会有一些不同的机制，但是对于这个活动，你只需要创建一个可以行走、跳跃、上下看的角色，并且有两个复制的属性:生命值和护甲。\n\n以下步骤将帮助您完成活动:\n\n1.  创建一个名为`MultiplayerFPS`的`Blank C++ `项目，不包含起始内容。\n2.  从`Activity16.01\\Assets folder`导入骨骼网格和动画，并将它们分别放置在`Content\\Player\\Mesh`和`Content\\Player\\Animations`文件夹中。\n3.  将以下声音从`Activity16.01\\Assets`文件夹导入`Content\\Player\\Sounds`:\n    *   `Jump.wav`:用`Play Sound`动画通知在`Jump_From_Stand_Ironsights`动画上播放这个声音。\n    *   `Footstep.wav`:使用`Play Sound` anim notify，在每次行走动画中，每次脚踩地板时播放此声音。\n    *   `Spawn.wav`:在字符中的`SpawnSound`变量上使用。\n4.  通过重新定位骨骼并创建一个名为`Camera`的插座来设置骨骼网格，该插座是头部骨骼的子节点，其相对位置为(`X=7.88, Y=4.73, Z=-10.00`)。\n5.  在名为`BS_Movement`的`Content\\Player\\Animations`中创建一个 2D 混合空间，使用导入的运动动画和`5`的`Target Weight Interpolation Speed Per Sec`。\n6.  使用在*第 4 章*、*玩家输入*中获得的知识，在`Project Settings`中创建输入映射:\n    *   跳转(动作映射)–*空格键*\n    *   向前移动(轴映射)–*W*(刻度`1.0`)和 *S* (刻度`-1.0`)\n    *   向右移动(轴映射)–*A*(刻度`-1.0`)和 *D* (刻度`1.0`)\n    *   旋转(轴映射)–鼠标 *X* (缩放`1.0`)\n    *   向上看(轴映射)–鼠标 *Y* (缩放`-1.0`)\n7.  创建一个名为`FPSCharacter`的 C++ 类，它执行以下操作:\n    *   源自`Character`类。\n    *   在`Camera`插座上有一个连接到骨骼网格的相机组件，并将`pawn control rotation`设置为`true`。\n    *   对于`health`和`armor`有只复制给所有者的变量。\n    *   有最大`health`和`armor`的变量，以及护甲吸收伤害的百分比。\n    *   有一个构造器，用于初始化摄像机，禁用嘀嗒声，并将`Max Walk Speed`设置为`800`，将`Jump Z Velocity`设置为`600`。\n    *   在`BeginPlay`上，播放产卵声音，有权限的话用`max health`初始化`health`。\n    *   创建并绑定函数来处理输入操作和轴。\n    *   具有添加/删除/设置健康的功能。这也保证了角色死亡的情况。\n    *   Has functions to add/set/absorb armor. The armor absorption reduces the armor based on the `ArmorAbsorption` variable and changes the damage value based on the formula:\n\n        *伤害=(伤害* (1 -装甲吸收))-FMath::Min(remaininggarmor，0)；*\n\n8.  在`Content\\Player\\Animations`中创建一个名为`ABP_Player`的动画蓝图，它有一个带有以下状态的`State Machine`:\n    *   `Idle/Run`:将`BS_Movement`与`Speed`和`Direction`变量一起使用\n    *   `Jump`: Plays the jump animation and transitions from the `Idle/Run` states when the `Is Jumping` variable is `true`\n\n        它还使用`Transform (Modify) Bone`根据相机的音高使角色上下俯仰。\n\n9.  使用在*第 15 章*、*收藏品、电源和皮卡*中获得的知识，在`Content\\UI`中创建一个名为`UI_HUD`的`UMG`小部件，以`Health: 100`和`Armor: 100`的格式显示人物的`Health`和`Armor`。\n10.  在`Content\\Player`中创建一个从`FPSCharacter`派生的名为`BP_Player`的蓝图，并将网格组件设置为具有以下值:\n    *   使用`SK_Mannequin`骨骼网格\n    *   使用`ABP_Player`动画蓝图\n    *   设置`Location`等于( *X=0.0，Y=0.0，Z=-88.0* )\n    *   Set `Rotation` to be equal to (*X=0.0, Y=0.0, Z=-90.0*)\n\n        同样，在`Begin Play`事件上，它需要创建`UI_HUD`的小部件实例，并将其添加到视口中。\n\n11.  在`Content\\Blueprints`中创建一个从`MultiplayerFPSGameModeBase`派生的名为`BP_GameMode`的蓝图，它将使用`BP_Player`作为`DefaultPawn`类。\n12.  在`Content\\Maps`中创建一个名为`DM-Test`的测试地图，并将其设置为`Project Settings`中的默认地图。\n\n预期产出:\n\n结果应该是一个项目，其中每个客户将有一个第一人称角色，可以移动，跳跃，并环顾四周。这些动作也将被复制，因此每个客户端将能够看到另一个客户端的角色正在做什么。\n\n每个客户端还会有一个显示生命值和护甲值的平视显示器。\n\n![Figure 16.27: Expected output ](img/B16183_16_27.jpg)\n\n图 16.27:预期产出\n\n注意\n\n这个活动的解决方案可以在:[https://packt.live/338jEBx](https://packt.live/338jEBx)找到。\n\n最终的结果应该是两个可以看到彼此移动、跳跃和四处张望的角色。每个客户端还显示其角色的生命值和护甲值。\n\n通过完成本练习，您应该对服务器-客户端体系结构、变量复制、角色、2D 混合空间和`Transform (Modify) Bone`节点的工作方式有了一个很好的了解。\n\n# 总结\n\n在本章中，我们了解了一些关键的多人游戏概念，例如服务器-客户端架构如何工作、服务器和客户端的职责、监听服务器如何比专用服务器更快地设置，但不像轻量级服务器那样快、所有权和连接、角色和变量复制。\n\n我们还学习了一些有用的动画技术，例如如何使用 2D 混合空间，它允许您有一个双轴网格在动画之间混合，以及变换(修改)骨骼节点，它能够在运行时修改骨骼网格的骨骼。为了完成这一章，我们创建了一个第一人称多人游戏项目，在这个项目中，你可以看到可以行走、看和跳跃的角色，这将是我们在接下来几章中将要研究的多人第一人称射击游戏项目的基础。\n\n在下一章中，我们将学习如何使用 RPC，它允许客户端和服务器在彼此上执行功能。我们还将介绍如何在编辑器中使用枚举，以及如何使用双向循环数组索引，这允许您在数组中前后循环，当超出限制时循环返回。*"
  },
  {
    "path": "docs/game-dev-proj-ue/16.md",
    "content": "# 十七、远程过程调用\n\n概观\n\n在这一章中，你将被介绍到远程过程调用，这是虚幻引擎 4 的网络框架的另一个重要的多人概念。您还将学习如何在虚幻引擎 4 中使用枚举，以及如何使用双向循环数组索引，这是一种帮助您双向迭代数组并能够在超出其索引限制时循环的方法。\n\n到本章结束时，您将了解远程过程调用是如何工作的，以使服务器和客户端在彼此上执行逻辑。您还将能够向虚幻引擎 4 编辑器公开枚举，并使用双向循环数组索引在数组之间循环。\n\n# 简介\n\n在前一章中，我们介绍了一些重要的多人游戏概念，包括服务器-客户端体系结构、连接和所有权、角色和变量复制。我们还看到了与专用服务器相比，监听服务器的安装速度更快，但重量却没有那么轻。我们用这些知识创造了一个基本的第一人称射击游戏角色，可以行走、跳跃和环顾四周。\n\n在本章中，我们将介绍**远程过程调用** ( **RPC** s)，这是另一个重要的多人游戏概念，允许服务器在客户端上执行功能，反之亦然。到目前为止，我们已经了解到变量复制是服务器和客户端之间的一种通信形式，但这还不够，因为服务器可能需要在客户端上执行特定的逻辑，而不涉及更新变量值。客户端还需要一种方法来告诉服务器它的意图，这样服务器就可以验证该操作，并让其他客户端知道它。这将确保多人游戏世界是同步的，我们将在本章中更详细地探讨这一点。我们还将介绍如何在虚幻引擎 4 中使用枚举，以及双向循环数组索引，这有助于您双向迭代数组，并在超出其索引限制时循环。\n\n在第一个主题中，我们将研究远程过程控制。\n\n# 远程过程调用\n\n我们已经在*第 16 章*、*多人游戏基础知识*中介绍了变量复制，虽然这是一个非常有用的特性，但由于两个主要原因，它在允许在远程机器(客户端到服务器或服务器到客户端)中执行自定义代码方面有点受限:\n\n*   第一个是变量复制严格来说是一种服务器到客户端的通信形式，因此客户端没有办法使用变量复制来告诉服务器通过更改变量值来执行一些自定义逻辑。\n*   第二个原因是变量复制，顾名思义，是由变量的值驱动的，所以即使变量复制允许客户端到服务器的通信，也需要您在客户端更改一个变量的值来触发服务器上的一个`RepNotify`功能来运行定制逻辑，这不是很实用。\n\n为了解决这个问题，虚幻引擎 4 支持 RPC。RPC 就像一个可以定义和调用的普通函数一样工作，但是它不是在本地执行，而是在远程机器上执行。使用 RPC 的主要目标是有可能在远程机器上执行特定的逻辑，而不是直接绑定到变量。为了能够使用 RPC，请确保在已打开复制的参与者中定义它们。\n\nRPC 有三种类型，每种类型都有不同的用途:\n\n*   服务器 RPC\n*   多播 RPC\n*   客户端 RPC\n\n让我们详细介绍这三种类型，并解释何时应该使用它们:\n\n## 服务器 RPC\n\n每次希望服务器在定义了 RPC 的参与者上运行函数时，都要使用服务器 RPC。有两个主要原因让您想要这样做:\n\n*   第一个是出于安全考虑，因为在制作多人游戏，尤其是竞技类游戏时，你总是要假设客户端会试图作弊。确保没有作弊的方法是强制客户端在服务器上执行对游戏性至关重要的功能。\n*   第二个原因是同步性，因为关键的游戏逻辑只在服务器上执行，这意味着重要的变量只在那里被改变，这将触发变量复制逻辑在客户端改变时更新它们。\n\n这方面的一个例子是当客户的角色试图发射武器时。因为客户总是有可能试图欺骗，所以你不能只在本地执行火力武器逻辑。正确的方法是让客户端调用服务器 RPC，服务器 RPC 告诉服务器通过确保角色有足够的弹药和武器装备来验证`Fire`动作，等等。如果一切正常，那么它将扣除弹药变量，最后，它将执行一个多播 RPC ( *将在下一个 RPC 类型*中介绍)，该类型将告诉所有客户端在该角色上播放火灾动画。\n\n### 申报\n\n要声明服务器 RPC，可以在`UFUNCTION`宏上使用`Server`说明符。请看下面的例子:\n\n```cpp\nUFUNCTION(Server, Reliable, WithValidation)\nvoid ServerRPCFunction(int32 IntegerParameter, float FloatParameter,   AActor* ActorParameter); \n```\n\n在前面的代码中，`UFUNCTION`宏使用了`Server`说明符来声明该函数是一个服务器 RPC。您可以像普通函数一样在服务器 RPC 上拥有参数，但是有一些注意事项将在本主题后面解释，以及`Reliable`和`WithValidation`说明符的用途。\n\n### 执行\n\n要执行服务器 RPC，可以从定义它的参与者实例上的客户端调用它。看看下面的例子:\n\n```cpp\nvoid ARPCTest::CallMyOwnServerRPC(int32 IntegerParameter)\n{\n  ServerMyOwnRPC(IntegerParameter);\n}\nvoid ARPCTest::CallServerRPCOfAnotherActor(AAnotherActor* OtherActor)\n{\n  if(OtherActor != nullptr)\n  {\n    OtherActor->ServerAnotherActorRPC();\n  }\n}\n```\n\n第一个代码片段实现了`CallMyOwnServerRPC`函数，该函数使用整数参数调用在自己的`ARPCTest`类中定义的`ServerMyOwnRPC` RPC 函数。这将在该参与者实例的服务器版本上执行`ServerMyOwnRPC`函数的实现。\n\n第二个代码片段实现了`CallServerRPCOfAnotherActor`函数，该函数在`OtherActor`实例上调用`AAnotherActor`中定义的`ServerAnotherActorRPC` RPC 函数，只要它有效。这将在服务器版本的`OtherActor`实例上执行`ServerAnotherActorRPC`功能的实现。\n\n### 有效连接\n\n从客户端调用服务器 RPC 时需要考虑的一件重要事情是，定义它的参与者需要有一个有效的连接。如果您试图在没有有效连接的参与者上调用服务器 RPC，那么什么也不会发生。您必须确保该参与者或者是一个玩家控制器，或者是被一个玩家控制器所拥有(如果适用的话，*)，或者是其拥有的参与者具有有效的连接。*\n\n *## 组播 RPC\n\n当您希望服务器告诉所有客户端在定义了 RPC 的参与者上运行一个函数时，可以使用多播 RPC。\n\n这方面的一个例子是当客户的角色试图发射武器时。在客户端调用服务器 RPC 请求发射武器的许可，并且服务器已经处理了该请求(所有验证都已检查完毕，弹药已扣除，并且线轨迹/射弹已处理完毕)之后，我们需要进行多播 RPC，以便该特定角色的所有实例都播放发射动画。这将确保角色将始终播放消防动画，而与哪个客户端正在查看角色无关。\n\n### 申报\n\n要声明多播 RPC，您需要在`UFUNCTION`宏上使用`NetMulticast`说明符。请看下面的例子:\n\n```cpp\nUFUNCTION(NetMulticast)\nvoid MulticastRPCFunction(int32 IntegerParameter, float   FloatParameter, AActor* ActorParameter); \n```\n\n在前面的代码中，`UFUNCTION`宏使用了`NetMulticast`说明符，表示下面的函数是一个多播 RPC。您可以像普通函数一样在多播 RPC 上设置参数，但与服务器 RPC 有相同的注意事项。\n\n### 执行\n\n要执行多播 RPC，需要从定义它的参与者实例上的服务器调用它。看看下面的例子:\n\n```cpp\nvoid ARPCTest::CallMyOwnMulticastRPC(int32 IntegerParameter)\n{\n  MulticastMyOwnRPC(IntegerParameter);\n}\nvoid ARPCTest::CallMulticastRPCOfAnotherActor(AAnotherActor*   OtherActor)\n{\n  if(OtherActor != nullptr)\n  {\n    OtherActor->MulticastAnotherActorRPC();\n  }\n}\n```\n\n第一个代码片段实现了`CallMyOwnMulticastRPC`函数，该函数使用整数参数调用在自己的`ARPCTest`类中定义的`MulticastMyOwnRPC` RPC 函数。这将在该参与者实例的所有客户端版本上执行`MulticastMyOwnRPC`功能的实现。\n\n第二个代码片段实现了`CallMulticastRPCOfAnotherActor`函数，该函数在`OtherActor`实例上调用`AAnotherActor`中定义的`MulticastAnotherActorRPC` RPC 函数，只要它有效。这将在所有客户端版本的`OtherActor`实例上执行`MulticastAnotherActorRPC`功能的实现。\n\n## 客户端 RPC\n\n当您只想在定义了客户端 RPC 的执行元的所属客户端上运行函数时，可以使用客户端 RPC。要设置所属客户端，您需要在服务器上调用“设置所有者”，并使用客户端的播放器控制器进行设置。\n\n这方面的一个例子是，当一个角色被炮弹击中时，他会发出痛苦的声音，只有客户才能听到。通过从服务器调用客户端 RPC，声音将只在拥有的客户端上播放，因此其他客户端听不到。\n\n### 申报\n\n要声明客户端 RPC，您需要在`UFUNCTION`宏上使用`Client`说明符。请看下面的例子:\n\n```cpp\nUFUNCTION(Client)\nvoid ClientRPCFunction(int32 IntegerParameter, float FloatParameter,   AActor* ActorParameter); \n```\n\n在前面的代码中，`UFUNCTION`宏使用了`Client`说明符，表示下面的函数是一个客户端 RPC。您可以像普通函数一样在客户端 RPC 上设置参数，但要注意与服务器 RPC 和多播 RPC 相同的事项。\n\n### 执行\n\n要执行客户端 RPC，您可以从定义它的执行元实例上的服务器调用它。看看下面的例子:\n\n```cpp\nvoid ARPCTest::CallMyOwnClientRPC(int32 IntegerParameter)\n{\n  ClientMyOwnRPC(IntegerParameter);\n}\nvoid ARPCTest::CallClientRPCOfAnotherActor(AAnotherActor* OtherActor)\n{\n  if(OtherActor != nullptr)\n  {\n    OtherActor->ClientAnotherActorRPC();\n  }\n}\n```\n\n第一个代码片段实现了`CallMyOwnClientRPC`函数，该函数使用整数参数调用在自己的`ARPCTest`类中定义的`ClientMyOwnRPC` RPC 函数。这将在该参与者实例的所属客户端版本上执行`ClientMyOwnRPC`功能的实现。\n\n第二个代码片段实现了`CallClientRPCOfAnotherActor`函数，该函数在`OtherActor`实例上调用`AAnotherActor`中定义的`ClientAnotherActorRPC` RPC 函数，只要它有效。这将在拥有客户端版本的`OtherActor`实例上执行`ClientAnotherActorRPC`功能的实现。\n\n## 使用 RPC 时的重要注意事项\n\nRPC 非常有用，但是在使用它们时，您需要考虑一些事情，例如:\n\n**实施**\n\nRPC 的实现与典型函数的实现略有不同。不要像平时那样实现函数，应该只实现它的`_Implementation`版本，即使没有在头文件中声明。请看下面的例子:\n\n**服务器 RPC:**\n\n```cpp\nvoid ARPCTest::ServerRPCTest_Implementation(int32 IntegerParameter,   float FloatParameter, AActor* ActorParameter)\n{\n}\n```\n\n在前面的代码片段中，我们实现了`ServerRPCTest`函数的`_Implementation`版本，它使用了三个参数。\n\n**组播 RPC:**\n\n```cpp\nvoid ARPCTest::MulticastRPCTest_Implementation(int32 IntegerParameter,   float FloatParameter, AActor* ActorParameter)\n{\n}\n```\n\n在前面的代码片段中，我们实现了`MulticastRPCTest`函数的`_Implementation`版本，它使用了三个参数。\n\n**客户端 RPC:**\n\n```cpp\nvoid ARPCTest::ClientRPCTest_Implementation(int32 IntegerParameter,   float FloatParameter, AActor* ActorParameter)\n{\n}\n```\n\n在前面的代码片段中，我们实现了`ClientRPCTest`函数的`_Implementation`版本，它使用了三个参数。\n\n从前面的例子中可以看出，与您正在实现的 RPC 的类型无关，您应该只实现函数的`_Implementation`版本，而不是普通版本，如下面的代码片段所示:\n\n```cpp\nvoid ARPCTest::ServerRPCFunction(int32 IntegerParameter, float   FloatParameter, AActor* ActorParameter)\n{\n}\n```\n\n在前面的代码中，我们定义了`ServerRPCFunction`的正常实现。如果您像这样实现 RPC，您会得到一个错误，说它已经实现了。这样做的原因是，当你在头文件中声明 RPC 函数时，虚幻引擎 4 会自动在内部创建正常的实现，稍后会调用`_Implementation`版本。如果创建普通实现的版本，构建将会失败，因为它会找到同一个函数的两个实现。要解决这个问题，只需确保只实现 RPC 的`_Implementation`版本。\n\n接下来，我们转到名称前缀。\n\n**名称前缀**\n\n在虚幻引擎 4 中，最好在 RPC 前面加上相应的类型。请看下面的例子:\n\n*   一个叫 RPCFunction 的**服务器 RPC** 应该叫`ServerRPCFunction`。\n*   一个叫做 RPCFunction 的**组播 RPC** 应该命名为`MulticastRPCFunction`。\n*   一个名为的**客户端 RPC 应该命名为`ClientRPCFunction`。**\n\n**返回值**\n\n因为 RPC 的调用和执行通常是在不同的机器上完成的，所以您不能有返回值，所以它总是需要是空的。\n\n**覆盖**\n\n您可以通过在子类中声明和实现`_Implementation`函数而不使用`UFUNCTION`宏来覆盖 RPC 的实现，以扩展或绕过父类的功能。这里有一个例子:\n\n父类的声明:\n\n```cpp\nUFUNCTION(Server)\nvoid ServerRPCTest(int32 IntegerParameter); \n```\n\n在前面的代码片段中，我们有`ServerRPCTest`函数父类的声明，它使用一个整数参数。\n\n子类上的重写声明:\n\n```cpp\nvirtual void ServerRPCTest_Implementation(int32 IntegerParameter)   override;\n```\n\n在前面的代码片段中，我们覆盖了子类头文件中`ServerRPCTest_Implementation`函数的声明。该函数的实现就像任何其他覆盖一样，如果您仍然想执行父功能，可以调用`Super::ServerRPCTest_Implementation`。\n\n**支持的参数类型**\n\n使用 RPC 时，您可以像添加任何其他函数一样添加参数。目前支持最常见的类型，包括`bool`、`int32`、`float`、`FString`、`FName`、`TArray`、`TSet`、`TMap`。你要注意的类型是任何`UObject`类或子类的指针，尤其是演员。\n\n如果你用一个 actor 参数创建一个 RPC，那么这个 actor 也需要存在于远程机器上，否则就是`nullptr`。另一个需要考虑的重要事项是，每个版本的参与者的实例名称可能不同。这意味着，如果您使用 actor 参数调用一个 RPC，那么调用 RPC 时该 actor 的实例名可能与在远程计算机上执行 RPC 时的实例名不同。这里有一个例子可以帮助你理解这一点:\n\n![Figure 17.1: The listen server and two clients running ](img/B16183_17_01.jpg)\n\n图 17.1:监听服务器和两个客户端正在运行\n\n在前面的示例中，您可以看到三个客户端正在运行(其中一个是监听服务器)，并且每个窗口都显示所有字符实例的名称。如果您查看 Client 1 窗口，其受控字符实例称为`ThirdPersonCharacter_C_0`，但在 Server 窗口中，该等效字符称为`ThirdPersonCharacter_C_1`。这意味着，如果客户端 1 调用服务器 RPC 并将其`ThirdPersonCharacter_C_0`作为参数传递，那么当 RPC 在服务器上执行时，参数将是`ThirdPersonCharacter_C_1`，这是该机器中等效字符的实例名。\n\n**在目标机器上执行远程过程控制**\n\n您可以直接在其目标机器上调用 RPC，它仍然会执行。换句话说，您可以在服务器上调用服务器 RPC 并执行，也可以在客户端上调用多播/客户端 RPC，但在这种情况下，它将只在调用 RPC 的客户端上执行逻辑。无论哪种方式，在这些情况下，你都应该直接调用`_Implementation`版本，这样执行逻辑会更快。\n\n这样做的原因是`_Implementation`版本只是保存要执行的逻辑，没有常规调用那样的通过网络创建和发送 RPC 请求的开销。\n\n看看下面这个在服务器上拥有权限的参与者的例子:\n\n```cpp\nvoid ARPCTest::CallServerRPC(int32 IntegerParameter)\n{\n  if(HasAuthority())\n  {\n    ServerRPCFunction_Implementation(IntegerParameter);\n  }\n  else ServerRPCFunction(IntegerParameter);\n}\n```\n\n在前面的例子中，您有`CallServerRPC`函数，它以两种不同的方式调用`ServerRPCFunction`。如果演员已经在服务器上，那么它调用`ServerRPCFunction_Implementation`，这将跳过前面提到的开销。\n\n如果参与者不在服务器上，则使用`ServerRPCFunction`执行常规调用，这增加了创建和通过网络发送 RPC 请求所需的开销。\n\n**验证**\n\n当您定义一个 RPC 时，您可以选择使用一个额外的函数来检查在调用 RPC 之前是否有任何无效的输入。这是为了避免在输入由于欺骗或其他原因无效时处理 RPC。\n\n要使用验证，您需要在`UFUNCTION`宏中添加`WithValidation`说明符。当您使用该说明符时，您将被迫实现该函数的`_Validate`版本，这将返回一个布尔值，说明 RPC 是否可以执行。\n\n请看下面的例子:\n\n```cpp\nUFUNCTION(Server, WithValidation)\nvoid ServerSetHealth(float NewHealth);\n```\n\n在前面的代码中，我们已经声明了一个名为`ServerSetHealth`的经过验证的服务器 RPC，它为`Health`的新值取了一个浮点参数。至于实现，如下所示:\n\n```cpp\nbool ARPCTest::ServerSetHealth_Validate(float NewHealth)\n{\n  return NewHealth <= MaxHealth;\n}\nvoid ARPCTest::ServerSetHealth_Implementation(float NewHealth)\n{\n  Health = NewHealth;\n}\n```\n\n在前面的代码中，我们实现了`_Validate`功能，它将检查新的健康是否小于或等于健康的最大值。如果客户端试图用`200`黑调用`ServerSetHealth`，而`MaxHealth`是`100`，那么 RPC 不会被调用，这就阻止了客户端用某个范围之外的值来改变健康。如果`_Validate`功能返回`true`，则照常调用`_Implementation`功能，用`NewHealth`的值设置`Health`。\n\n**可靠性**\n\n当您声明一个 RPC 时，您需要使用`UFUNCTION`宏中的`Reliable`或`Unreliable`说明符。下面简单介绍一下他们的工作:\n\n*   `Reliable`:当您想要确保 RPC 被执行时使用，通过重复请求直到远程机器确认它的接收。这应该只用于非常重要的 RPC，例如执行关键的游戏逻辑。下面是一个如何使用它的例子:\n\n    ```cpp\n    UFUNCTION(Server, Reliable)\n    void ServerReliableRPCFunction(int32 IntegerParameter); \n    ```\n\n*   `Unreliable`: Used when you don't care whether the RPC is executed due to bad network conditions, such as playing a sound or spawning a particle effect. This should only be used for RPCs that aren't very important or are called very frequently to update values, since it wouldn't matter if one call missed because it's updating very often. Here is an example of how to use it:\n\n    ```cpp\n    UFUNCTION(Server, Unreliable)\n    void ServerUnreliableRPCFunction(int32 IntegerParameter);\n    ```\n\n    注意\n\n    关于 RPC 的更多信息，请访问[https://docs . unrealengine . com/en-US/game play/Networking/Actors/RPC/index . html](https://docs.unrealengine.com/en-US/Gameplay/Networking/Actors/RPCs/index.html)。\n\n在下一个练习中，您将看到如何实现不同类型的 RPC。\n\n## 练习 17.01:使用远程过程调用\n\n在本练习中，我们将创建一个使用`Third Person`模板的 C++ 项目，并以以下方式对其进行扩展:\n\n*   添加一个 fire timer 变量，防止客户端在 fire 动画期间滥发 fire 按钮。\n*   添加一个新的默认为`5`的弹药整数变量，并复制到所有客户端。\n*   添加一个`Fire Anim montage`，当服务器告诉客户端该镜头有效时播放。\n*   添加一个`No Ammo Sound`，当服务器告诉客户端他们没有足够的弹药时会播放。\n*   每次玩家按下*鼠标左键*，客户端都会执行一个可靠且经过验证的服务器 RPC，检查角色是否有足够的弹药。如果是这样，它将从弹药变量中减去 1，并调用一个不可靠的多播 RPC，在每个客户端播放火灾动画。如果它没有弹药，那么它将执行一个不可靠的客户端 RPC，该 RPC 将播放只有拥有客户端才能听到的`No Ammo Sound`。\n\n以下步骤将帮助您完成练习:\n\n1.  使用名为`RPC`的`C++ `创建一个新的`Third Person`模板项目，并将其保存到您选择的位置。\n2.  一旦创建了项目，它就应该打开编辑器和 Visual Studio 解决方案。\n3.  关闭编辑器并返回到 Visual Studio。\n4.  打开`RPCCharacter.h`文件，包括`UnrealNetwork.h`头文件，该文件有我们将要使用的`DOREPLIFETIME_CONDITION`宏的定义:\n\n    ```cpp\n    #include \"Net/UnrealNetwork.h\"\n    ```\n\n5.  声明受保护的计时器变量，以防止客户端滥发`Fire`动作:\n\n    ```cpp\n    FTimerHandle FireTimer;\n    ```\n\n6.  声明受保护的复制弹药变量，以`5`射击开始:\n\n    ```cpp\n    UPROPERTY(Replicated)\n    int32 Ammo = 5;\n    ```\n\n7.  接下来，声明角色触发时将播放的受保护动画蒙太奇变量:\n\n    ```cpp\n    UPROPERTY(EditDefaultsOnly, Category = \"RPC Character\")\n    UAnimMontage* FireAnimMontage;\n    ```\n\n8.  声明当角色没有弹药时将播放的受保护声音变量:\n\n    ```cpp\n    UPROPERTY(EditDefaultsOnly, Category = \"RPC Character\")\n    USoundBase* NoAmmoSound;\n    ```\n\n9.  超越`Tick`功能:\n\n    ```cpp\n    virtual void Tick(float DeltaSeconds) override;\n    ```\n\n10.  声明将处理按下*鼠标左键* :\n\n    ```cpp\n    void OnPressedFire();\n    ```\n\n    的输入功能\n11.  宣布可靠且经过验证的服务器 RPC 启动:\n\n    ```cpp\n    UFUNCTION(Server, Reliable, WithValidation, Category = \"RPC   Character\")\n    void ServerFire();\n    ```\n\n12.  声明不可靠的多播 RPC 将在所有客户端上播放火灾动画:\n\n    ```cpp\n    UFUNCTION(NetMulticast, Unreliable, Category = \"RPC Character\")\n    void MulticastFire();\n    ```\n\n13.  声明不可靠的客户端 RPC 将只在拥有的客户端播放声音:\n\n    ```cpp\n    UFUNCTION(Client, Unreliable, Category = \"RPC Character\")\n    void ClientPlaySound2D(USoundBase* Sound);\n    ```\n\n14.  现在，打开`RPCCharacter.cpp`文件，包括`DrawDebugHelpers.h`、`GameplayStatics.h`、`TimerManager.h`和【T4:\n\n    ```cpp\n    #include \"DrawDebugHelpers.h\"\n    #include \"Kismet/GameplayStatics.h\"\n    #include \"TimerManager.h\"\n    #include \"Engine/World.h\"\n    ```\n\n15.  在构造器结束时，启用`Tick`功能:\n\n    ```cpp\n    PrimaryActorTick.bCanEverTick = true;\n    ```\n\n16.  实现`GetLifetimeReplicatedProps`功能，以便`Ammo`变量将复制到所有客户端:\n\n    ```cpp\n    void ARPCCharacter::GetLifetimeReplicatedProps(TArray<   FLifetimeProperty >& OutLifetimeProps) const\n    {\n      Super::GetLifetimeReplicatedProps(OutLifetimeProps);\n      DOREPLIFETIME(ARPCCharacter, Ammo);\n    }\n    ```\n\n17.  接下来，执行`Tick`功能，显示`Ammo`变量的值:\n\n    ```cpp\n    void ARPCCharacter::Tick(float DeltaSeconds)\n    {\n      Super::Tick(DeltaSeconds);\n      const FString AmmoString = FString::Printf(TEXT(\"Ammo = %d\"),     Ammo);\n      DrawDebugString(GetWorld(), GetActorLocation(), AmmoString,     nullptr, FColor::White, 0.0f, true);\n    }\n    ```\n\n18.  在`SetupPlayerInputController`功能结束时，将`Fire`动作绑定到`OnPressedFire`功能:\n\n    ```cpp\n    PlayerInputComponent->BindAction(\"Fire\", IE_Pressed, this,   &ARPCCharacter::OnPressedFire);\n    ```\n\n19.  实现处理*鼠标左键*按下的功能，调用消防服务器 RPC:\n\n    ```cpp\n    void ARPCCharacter::OnPressedFire()\n    {\n      ServerFire();\n    }\n    ```\n\n20.  实现消防服务器 RPC 验证功能:\n\n    ```cpp\n    bool ARPCCharacter::ServerFire_Validate()\n    {\n      return true;\n    }\n    ```\n\n21.  实现消防服务器 RPC 实现功能:\n\n    ```cpp\n    void ARPCCharacter::ServerFire_Implementation()\n    {\n\n    }\n    ```\n\n22.  现在，如果自从我们发射最后一发子弹以来，射击计时器仍然有效，则添加逻辑以中止该功能:\n\n    ```cpp\n    if (GetWorldTimerManager().IsTimerActive(FireTimer))\n    {\n      return;\n    }\n    ```\n\n23.  检查角色是否有弹药。如果没有，则只在控制角色的客户端播放`NoAmmoSound`，并中止功能:\n\n    ```cpp\n    if (Ammo == 0)\n    {\n      ClientPlaySound2D(NoAmmoSound);\n      return;\n    }\n    ```\n\n24.  扣除弹药并安排`FireTimer`变量，防止在播放火力动画时滥发此功能:\n\n    ```cpp\n    Ammo--;\n    GetWorldTimerManager().SetTimer(FireTimer, 1.5f, false);\n    ```\n\n25.  调用火组播 RPC，让所有客户端播放火动画:\n\n    ```cpp\n    MulticastFire();\n    ```\n\n26.  实现火组播 RPC，会播放火动画蒙太奇:\n\n    ```cpp\n    void ARPCCharacter::MulticastFire_Implementation()\n    {\n      if (FireAnimMontage != nullptr)\n      {\n        PlayAnimMontage(FireAnimMontage);\n      }\n    }\n    ```\n\n27.  Implement the Client RPC that plays a 2D sound:\n\n    ```cpp\n    void ARPCCharacter::ClientPlaySound2D_Implementation(USoundBase*   Sound)\n    {\n      UGameplayStatics::PlaySound2D(GetWorld(), Sound);\n    }\n    ```\n\n    最后，您可以在编辑器中启动项目。\n\n28.  编译代码，等待编辑器完全加载。\n29.  Go to `Project Settings`, go to `Engine`, then `Input`, and add the `Fire` action binding:\n\n    ![Figure 17.2: Adding the new Fire action binding ](img/B16183_17_02.jpg)\n\n    图 17.2:添加新的 Fire 动作绑定\n\n30.  关闭`Project Settings`。\n31.  在`Content Browser`中，转到`Content\\Mannequin\\Animations`文件夹。\n32.  Click the `Import` button, go to the `Exercise17.01\\Assets` folder and import the `ThirdPersonFire.fbx` file, and then make sure it's using the `UE4_Mannequin_Skeleton` skeleton.\n\n    注意\n\n    前面提到的`Assets`文件夹可以在我们位于[https://packt.live/36pEvAT](https://packt.live/36pEvAT)的 GitHub 存储库中找到。\n\n33.  打开新动画，在细节上，面板找到`Enable Root Motion`选项并将其设置为真。这将防止角色在播放动画时移动。\n34.  保存并关闭`ThirdPersonFire`。\n35.  *右键单击`Content Browser`上的* `ThirdPersonFire`，选择`Create -> AnimMontage`。\n36.  将`AnimMontage`重命名为`ThirdPersonFire_Montage`。\n37.  The `Animations` folder should look like this:\n\n    ![Figure 17.3: The animations folder for the Mannequin ](img/B16183_17_03.jpg)\n\n    图 17.3:人体模型的动画文件夹\n\n38.  打开`ThirdPerson_AnimBP`再打开`AnimGraph`。\n39.  *Right-click* on an empty part of the graph, add a `DefaultSlot` node (to be able to play the animation montage), and connect it between `State Machine` and `Output Pose`. You should get the following output:\n\n    ![Figure 17.4: The AnimGraph of the character ](img/B16183_17_04.jpg)\n\n    图 17.4:角色的动画\n\n40.  保存并关闭`ThirdPerson_AnimBP`。\n41.  在`Content Browser`中，转到`Content`文件夹，新建一个名为`Audio`的文件夹，并将其打开。\n42.  点击`Import`按钮，进入`Exercise17.01\\Assets`文件夹，导入`noammo.wav`，保存。\n43.  转到`Content\\ThirdPersonCPP\\Blueprints`打开`ThirdPersonCharacter`蓝图。\n44.  在类默认中，设置`No Ammo Sound`使用`noammo`，设置`Fire Anim Montage`使用`ThirdPersonFire_Montage`。\n45.  保存并关闭`ThirdPersonCharacter`。\n46.  进入多人选项，将客户端数量设置为`2`。\n47.  Set the window size to 800x600 and play using PIE.\n\n    您应该会得到以下输出:\n\n    ![Figure 17.5: The end result of the exercise ](img/B16183_17_05.jpg)\n\n图 17.5:练习的最终结果\n\n完成这个练习，就可以在每个客户端上玩了，每次按下*鼠标左键*，客户端的角色就会播放`Fire Anim`蒙太奇，所有客户端都能看到，其弹药也会减少`1`。如果你试图在弹药是`0`的时候开火，那个客户端会听到`No Ammo Sound`而不会做开火动画，因为服务器没有调用组播 RPC。如果你尝试垃圾邮件的消防按钮，你会注意到，它只会触发一个新的消防一旦动画完成。\n\n在下一节中，我们将研究枚举，枚举在游戏开发中用于许多不同的事情，例如管理角色的状态(无论是空闲、行走、攻击还是死亡等)，或者为装备槽数组中的每个索引(头部、主要武器、次要武器、躯干、手、腰带、裤子等)分配一个人性化的名称。\n\n# 枚举\n\n枚举是一种用户定义的数据类型，它包含一个整数常量列表，其中每个项都有一个由您分配的人性化名称，这使得代码更容易阅读。举个例子，我们可以用一个整数变量来表示一个角色可以处于的不同状态–`0`表示它是空闲的，`1`表示它正在行走，等等。这种方法的问题是，当你开始编写像`if(State == 0)`这样的代码时，你会变得很难记住`0`是什么意思，尤其是如果你有很多状态，而没有使用一些文档或注释来帮助你记忆。要解决这个问题，您应该使用枚举，在那里您可以编写像`if(State == EState::Idle)`这样的代码，这要明确得多，也更容易理解。\n\n在 C++ 中，有两种枚举类型，旧的原始枚举和 C++ 11 中引入的新枚举类。如果您想在编辑器中使用 C++ 枚举，您的第一直觉可能是以典型的方式来做，这是通过声明一个变量或一个使用枚举作为参数的函数，分别带有`UPROPERTY`或`UFUNCTION`。\n\n问题是，如果你尝试这样做，你会得到一个编译错误。看看下面的例子:\n\n```cpp\nenum class ETestEnum : uint8\n{\n  EnumValue1,\n  EnumValue2,\n  EnumValue3\n};\n```\n\n在前面的代码片段中，我们声明了一个名为`ETestEnum`的枚举类，它有三个可能的值:`EnumValue1`、`EnumValue2`和`EnumValue3`。\n\n之后，尝试以下任一示例:\n\n```cpp\nUPROPERTY()\nETestEnum TestEnum;\nUFUNCTION()\nvoid SetTestEnum(ETestEnum NewTestEnum) { TestEnum = NewTestEnum; }\n```\n\n在前面的代码片段中，我们声明了一个`UPROPERTY`变量和`UFUNCTION`函数，它们在类中使用了`ETestEnum`枚举。如果您尝试编译，您将获得以下编译错误:\n\n```cpp\nerror : Unrecognized type 'ETestEnum' - type must be a UCLASS, USTRUCT   or UENUM\n```\n\n注意\n\n在虚幻引擎 4 中，最好在枚举名称前加上字母`E`。例如`EWeaponType`和`EAmmoType`。\n\n发生此错误是因为当您试图使用`UPROPERTY`或`UFUNCTION`宏向编辑器公开类、结构或枚举时，您需要分别使用`UCLASS`、`USTRUCT`和`UENUM`宏将其添加到虚幻引擎 4 反射系统中。\n\n注意\n\n您可以通过访问以下链接了解更多关于虚幻引擎 4 反射系统的信息:[https://www . un realengine . com/en-US/blog/虚幻-property-system-reflection](https://www.unrealengine.com/en-US/blog/unreal-property-system-reflection) 。\n\n记住这些知识，修复之前的错误很简单，所以只需执行以下操作:\n\n```cpp\nUENUM()\nenum class ETestEnum : uint8\n{\n  EnumValue1,\n  EnumValue2,\n  EnumValue3\n};\n```\n\n在下一节中，我们将看看`TEnumAsByte`类型。\n\n## 保留字节\n\n如果您想要向使用原始枚举的引擎公开一个变量，那么您需要使用`TEnumAsByte`类型。如果您使用原始枚举(而不是枚举类)声明一个`UPROPERTY`变量，您将得到一个编译错误。\n\n请看下面的例子:\n\n```cpp\nUENUM()\nenum ETestRawEnum\n{\n  EnumValue1,\n  EnumValue2,\n  EnumValue3\n};\n```\n\n如果使用`ETestRawEnum`声明`UPROPERTY`变量，如下所示:\n\n```cpp\nUPROPERTY()\nETestRawEnum TestRawEnum;\n```\n\n您会得到这个编译错误:\n\n```cpp\nerror : You cannot use the raw enum name as a type for member   variables, instead use TEnumAsByte or a C++ 11 enum class with an   explicit underlying type.\n```\n\n要修复此错误，您需要用`TEnumAsByte<>`包围变量的枚举类型，在本例中为`ETestRawEnum`，如下所示:\n\n```cpp\nUPROPERTY()\nTEnumAsByte<ETestRawEnum> TestRawEnum;\n```\n\n## 插入\n\n当您使用`UENUM`宏向虚幻引擎反射系统添加枚举时，这将允许您对枚举的每个值使用`UMETA`宏。`UMETA`宏，就像其他宏如`UPROPERTY`或`UFUNCTION`一样，可以使用说明符通知虚幻引擎 4 如何处理该值。以下是最常用的`UMETA`说明符列表:\n\n### 显示名称\n\n此说明符允许您为枚举值定义一个新名称，当枚举值显示在编辑器中时，该名称更容易读取。\n\n看看下面的例子:\n\n```cpp\nUENUM()\nenum class ETestEnum : uint8\n{\n  EnumValue1 UMETA(DisplayName = \"My First Option\",\n  EnumValue2 UMETA(DisplayName = \"My Second Option\",\n  EnumValue3 UMETA(DisplayName = \"My Third Option\"\n};\n```\n\n让我们声明以下变量:\n\n```cpp\nUPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = \"Test\")\nETestEnum TestEnum;\n```\n\n然后，当你打开编辑器查看`TestEnum`变量时，你会看到一个下拉列表，其中`EnumValue1`、`EnumValue2,`和`EnumValue3`分别被`My First Option`、`My Second Option,`和`My Third Option`替换。\n\n### 隐藏\n\n此说明符允许您从下拉列表中隐藏特定的枚举值。当有一个枚举值，您只想在 C++ 中使用，而不想在编辑器中使用时，通常会使用这个枚举值。\n\n看看下面的例子:\n\n```cpp\nUENUM()\nenum class ETestEnum : uint8\n{\n  EnumValue1 UMETA(DisplayName = \"My First Option\"),\n  EnumValue2 UMETA(Hidden),\n  EnumValue3 UMETA(DisplayName = \"My Third Option\")\n};\n```\n\n让我们声明以下变量:\n\n```cpp\nUPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = \"Test\")\nETestEnum TestEnum;\n```\n\n然后，当你打开编辑器，查看`TestEnum`变量时，你会看到一个下拉列表。您应该注意到`My Second Option`没有出现在下拉列表中，因此无法选择。\n\n注意\n\n有关所有 UMETA 说明符的更多信息，请访问[https://docs . unrealengine . com/en-US/Programming/unrealararchitecture/Reference/Metadata/# enummetadata 说明符](https://docs.unrealengine.com/en-US/Programming/UnrealArchitecture/Reference/Metadata/#enummetadataspecifiers)。\n\n在下一节中，我们将查看`UENUM`宏的`BlueprintType`说明符。\n\n## 蓝色打印类型\n\n该`UENUM`说明符将枚举暴露给蓝图。这意味着在下拉列表中会有一个该枚举的条目，在为函数创建新变量或输入/输出时使用，如下例所示:\n\n![Figure 17.6: Setting a variable to use the ETestEnum variable type. ](img/B16183_17_06.jpg)\n\n图 17.6:设置一个变量来使用 ETestEnum 变量类型。\n\n它还将显示您可以在编辑器中的枚举上调用的其他函数，如本例所示:\n\n![Figure 17.7: List of additional functions available when using BlueprintType ](img/B16183_17_07.jpg)\n\n图 17.7:使用 BlueprintType 时可用的附加功能列表\n\n### 最大\n\n使用枚举时，通常想知道它有多少值。在虚幻引擎 4 中，标准的做法是添加`MAX`作为最后一个值，这个值会自动隐藏在编辑器中。\n\n看看下面的例子:\n\n```cpp\nUENUM()\nenum class ETestEnum : uint8\n{\n  EnumValue1,\n  EnumValue2,\n  EnumValue3,\n  MAX\n};\n```\n\n如果想知道`ETestEnum`在 C++ 中有多少个值，只需要做以下几个即可:\n\n```cpp\nconst int32 MaxCount = (int32)ETestEnum::MAX;\n```\n\n这是因为 C++ 中的枚举内部存储为数字，其中第一个值是`0`，第二个值是`1`，依此类推。这意味着只要`MAX`是最后一个值，它就会一直有枚举中值的总数。需要考虑的重要一点是，为了让`MAX`给你正确的值，你不能改变枚举的内部编号顺序，比如:\n\n```cpp\nUENUM()\nenum class ETestEnum : uint8\n{\n  EnumValue1 = 4,\n  EnumValue2 = 78,\n  EnumValue3 = 100,\n  MAX\n};\n```\n\n在这种情况下，`MAX`将是`101`，因为它将使用紧挨着前一个值的数字，即`EnumValue3 = 100`。\n\n使用`MAX`只意味着在 C++ 中使用，而不是在编辑器中使用，因为`MAX`值隐藏在蓝图中，如前所述。要获取蓝图中枚举的条目数，您应该在`UENUM`宏中使用`BlueprintType`说明符，以便在上下文菜单中公开一些有用的功能。之后，您只需要在上下文菜单中键入枚举的名称。如果选择`Get number of entries in ETestEnum`选项，您将拥有一个返回枚举条目数的函数。\n\n在下一个练习中，您将在虚幻引擎 4 编辑器中使用 C++ 枚举。\n\n## 练习 17.02:在虚幻引擎 4 编辑器中使用 C++ 枚举\n\n在本练习中，我们将创建一个使用`Third Person`模板的新 C++ 项目，并添加以下内容:\n\n*   一种叫做`EWeaponType`的计数，包含`3`武器——手枪、猎枪和火箭发射器。\n*   一个名为`EAmmoType`的枚举，包含`3`种弹药类型——子弹、炮弹和火箭。\n*   一个名为`Weapon`的变量，使用`EWeaponType`来判断当前武器的类型。\n*   一个名为`Ammo`的整数数组变量，保存每种类型的弹药量，用值`10`初始化。\n*   当玩家按下 *1* 、 *2* 、 *3* 键时，会将`Weapon`分别设置为`Pistol`、`Shotgun`和`Rocket Launcher`。\n*   当玩家按下*鼠标左键*时，这将消耗当前武器的弹药。\n*   每调用一次`Tick`功能，角色就会显示当前武器类型以及等效弹药类型和数量。\n\n以下步骤将帮助您完成练习:\n\n1.  Create a new `Third Person` template project using `C++ ` called `Enumerations` and save it to a location of your choosing.\n\n    一旦创建了项目，它就应该打开编辑器和 Visual Studio 解决方案。\n\n2.  关闭编辑器并返回到 Visual Studio。\n3.  打开`Enumerations.h`文件。\n4.  创建一个名为`ENUM_TO_INT32`的宏，将枚举转换为`int32`数据类型:\n\n    ```cpp\n    #define ENUM_TO_INT32(Value) (int32)Value\n    ```\n\n5.  创建一个名为`ENUM_TO_FSTRING`的宏，该宏将获取`enum`数据类型的显示名称，并将其转换为`FString`数据类型:\n\n    ```cpp\n    #define ENUM_TO_FSTRING(Enum, Value) FindObject<UEnum>(ANY_PACKAGE, TEXT(Enum), true)-  >GetDisplayNameTextByIndex((int32)Value).ToString()\n    ```\n\n6.  申报枚举`EWeaponType``EAmmoType`:\n\n    ```cpp\n    UENUM(BlueprintType)\n    enum class EWeaponType : uint8\n    {\n      Pistol UMETA(Display Name = «Glock 19»),\n      Shotgun UMETA(Display Name = «Winchester M1897»),\n      RocketLauncher UMETA(Display Name = «RPG»),    \n      MAX\n    };\n    UENUM(BlueprintType)\n    enum class EAmmoType : uint8\n    {\n      Bullets UMETA(DisplayName = «9mm Bullets»),\n      Shells UMETA(Display Name = «12 Gauge Shotgun Shells»),\n      Rockets UMETA(Display Name = «RPG Rockets»),\n      MAX\n    };\n    ```\n\n7.  打开`EnumerationsCharacter.h`文件，包括`Enumerations.h`标题:\n\n    ```cpp\n    #include \"Enumerations.h\"\n    ```\n\n8.  声明保存所选武器武器类型的受保护`Weapon`变量:\n\n    ```cpp\n    UPROPERTY(BlueprintReadOnly, Category = \"Enumerations Character\")\n    EWeaponType Weapon;\n    ```\n\n9.  声明受保护的`Ammo`阵列，该阵列保存每种类型的弹药数量:\n\n    ```cpp\n    UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category =   \"Enumerations Character\")\n    TArray<int32> Ammo;\n    ```\n\n10.  声明`Begin Play`和`Tick`功能的受保护覆盖:\n\n    ```cpp\n    virtual void BeginPlay() override;\n    virtual void Tick(float DeltaSeconds) override;\n    ```\n\n11.  声明受保护的输入功能:\n\n    ```cpp\n    void OnPressedPistol();\n    void OnPressedShotgun();\n    void OnPressedRocketLauncher();\n    void OnPressedFire();\n    ```\n\n12.  打开`EnumerationsCharacter.cpp`文件，包括`DrawDebugHelpers.h`标题:\n\n    ```cpp\n    #include \"DrawDebugHelpers.h\"\n    ```\n\n13.  在`SetupPlayerInputController`函数的末尾绑定新的动作绑定，如下面的代码片段所示:\n\n    ```cpp\n    PlayerInputComponent->BindAction(\"Pistol\", IE_Pressed, this,   &AEnumerationsCharacter::OnPressedPistol);\n    PlayerInputComponent->BindAction(\"Shotgun\", IE_Pressed, this,   &AEnumerationsCharacter::OnPressedShotgun);\n    PlayerInputComponent->BindAction(\"Rocket Launcher\", IE_Pressed,   this, &AEnumerationsCharacter::OnPressedRocketLauncher);\n    PlayerInputComponent->BindAction(\"Fire\", IE_Pressed, this,   &AEnumerationsCharacter::OnPressedFire);\n    ```\n\n14.  接下来，为执行父逻辑的`BeginPlay`实现覆盖，但也用`EAmmoType`枚举中的条目数初始化`Ammo`数组的大小。数组中的每个位置也将被初始化为值`10` :\n\n    ```cpp\n    void AEnumerationsCharacter::BeginPlay()\n    {\n      Super::BeginPlay();\n      const int32 AmmoCount = ENUM_TO_INT32(EAmmoType::MAX);\n      Ammo.Init(10, AmmoCount);\n    }\n    ```\n\n15.  执行`Tick` :\n\n    ```cpp\n    void AEnumerationsCharacter::Tick(float DeltaSeconds)\n    {\n      Super::Tick(DeltaSeconds);\n    }\n    ```\n\n    的覆盖\n16.  将`Weapon`变量转换为`int32`，将`Weapon`变量转换为`FString` :\n\n    ```cpp\n    const int32 WeaponIndex = ENUM_TO_INT32(Weapon);\n    const FString WeaponString = ENUM_TO_FSTRING(\"EWeaponType\",   Weapon);\n    ```\n\n17.  Convert the ammo type to an `FString` and get the ammo count for the current weapon:\n\n    ```cpp\n    const FString AmmoTypeString = ENUM_TO_FSTRING(\"EAmmoType\",   Weapon);\n    const int32 AmmoCount = Ammo[WeaponIndex];\n    ```\n\n    我们使用`Weapon`来获取弹药类型字符串，因为`EAmmoType`中的条目与等效的`EWeaponType`的弹药类型相匹配。换句话说，`Pistol = 0`使用`Bullets = 0`，`Shotgun = 1`使用`Shells = 1`，`RocketLauncher = 2`使用`Rockets = 2`，所以这是一个 1 对 1 的映射，我们可以使用对我们有利的映射。\n\n18.  在角色的位置显示当前武器的名称及其对应的弹药类型和弹药数量，如以下代码片段所示:\n\n    ```cpp\n    const FString String = FString::Printf(TEXT(\"Weapon = %s\\nAmmo   Type = %s\\nAmmo Count = %d\"), *WeaponString, *AmmoTypeString,   AmmoCount);\n    DrawDebugString(GetWorld(), GetActorLocation(), String, nullptr,   FColor::White, 0.0f, true);\n    ```\n\n19.  实现装备输入功能，将`Weapon`变量设置为相应的值:\n\n    ```cpp\n    void AEnumerationsCharacter::OnPressedPistol()\n    {\n      Weapon = EWeaponType::Pistol;\n    }\n    void AEnumerationsCharacter::OnPressedShotgun()\n    {\n      Weapon = EWeaponType::Shotgun;\n    }\n    void AEnumerationsCharacter::OnPressedRocketLauncher()\n    {\n      Weapon = EWeaponType::RocketLauncher;\n    }\n    ```\n\n20.  实现火力输入功能，该功能将使用武器索引获取相应的弹药类型计数并减去`1`，只要结果值大于或等于 0:\n\n    ```cpp\n    void AEnumerationsCharacter::OnPressedFire()\n    {\n      const int32 WeaponIndex = ENUM_TO_INT32(Weapon);\n      const int32 NewRawAmmoCount = Ammo[WeaponIndex] - 1;\n      const int32 NewAmmoCount = FMath::Max(NewRawAmmoCount, 0);\n      Ammo[WeaponIndex] = NewAmmoCount;\n    }\n    ```\n\n21.  编译代码并运行编辑器。\n22.  Go to `Project Settings` and then `Engine`, then `Input`, and add the new action `bindings`:\n\n    ![Figure 17.8: Adding the Pistol, Shotgun, Rocket Launcher, and Fire bindings ](img/B16183_17_08.jpg)\n\n    图 17.8:添加手枪、猎枪、火箭发射器和火力绑定\n\n23.  关闭`Project Settings`。\n24.  Play in `New Editor Window (PIE)` in single-player mode (one client and dedicated server disabled):\n\n    ![Figure 17.9: The end result of the exercise ](img/B16183_17_09.jpg)\n\n图 17.9:练习的最终结果\n\n完成本练习后，您将能够使用 *1* 、 *2* 和 *3* 键选择当前武器。你会注意到每一个勾号都会显示当前武器的类型以及对应的弹药类型和弹药数量。如果你按下开火键，这将会扣除当前武器的弹药数量，但永远不会低于`0`。\n\n在下一节中，您将看到双向循环数组索引。\n\n# 双向环形阵列索引\n\n有时，当您使用数组存储信息时，您可能希望以双向循环的方式迭代它。这方面的一个例子是射手游戏中的上一个/下一个武器逻辑，其中你有一个带有武器的数组，你希望能够在特定方向上循环通过它们，当你到达第一个或最后一个索引时，你希望分别循环回到最后一个和第一个索引。此示例的典型方式如下:\n\n```cpp\nAWeapon * APlayer::GetPreviousWeapon()\n{\n  if(WeaponIndex - 1 < 0)\n  {\n    WeaponIndex = Weapons.Num() - 1;\n  }\n  else WeaponIndex--;\n  return Weapons[WeaponIndex];\n}\nAWeapon * APlayer::GetNextWeapon()\n{\n  if(WeaponIndex + 1 > Weapons.Num() - 1)\n  {\n    WeaponIndex = 0;\n  }\n  else WeaponIndex++ ;\n  return Weapons[WeaponIndex];\n}\n```\n\n在前面的代码中，如果新的武器索引超出了武器阵列的限制，我们会调整武器索引以循环返回，这可能发生在两种情况下。第一种情况是玩家装备了库存的最后一件武器，并要求下一件武器。在这种情况下，它应该回到第一件武器。\n\n第二种情况是玩家装备了库存中的第一件武器，并要求获得前一件武器。在这种情况下，它应该去最后一个武器。\n\n虽然示例代码可以工作，但要解决这样一个微不足道的问题仍然需要大量的代码。为了改进这段代码，有一个数学公式可以帮助您在一个函数中自动考虑这两种情况。它被称为模(在 C++ 中由`%`运算符表示)，它给出了两个数字之间除法的余数。\n\n那么我们如何使用模来进行双向循环数组索引呢？让我们用模重写前面的例子:\n\n```cpp\nAWeapon * APlayer::GetNewWeapon(int32 Direction)\n{\n  const int32 WeaponCount = Weapons.Num();\n  const int32 NewIndex = WeaponIndex + Direction;\n  const in32 ClampedNewIndex = NewIndex % WeaponCount;\n  WeaponIndex = (ClampedNewIndex + WeaponCount) % WeaponCount;\n  return Weapons[WeaponIndex];\n}\n```\n\n这是新版本，你可以马上看出它有点难理解，但它功能更强、更紧凑。如果您不使用变量来存储每个操作的中间值，您可能可以用一两行代码来完成整个函数。\n\n让我们分解前面的代码片段:\n\n`const int WeaponCount = Weapons.Num()`:我们需要知道数组的大小来确定它应该回圈到`0`的索引。换句话说，如果`WeaponCount = 4`，那么数组有`0`、`1`、`2`和`3`的索引，所以这告诉我们索引 4 是应该回到`0`的截止索引。\n\n`const int32 NewIndex = WeaponIndex + Direction`:这是新的原始索引，没有将其限制到数组的极限。`Direction`变量用于指示我们想要在数组中导航的偏移量，如果我们想要上一个索引，这是`-1`，如果我们想要下一个索引，这是`1`。\n\n`const int32 ClampedNewIndex = NewIndex % WeaponCount`:由于模属性，这将确保`NewIndex`在`0`到`WeaponCount - 1`的区间内。\n\n如果`Direction`总是`1`，那么`ClampedNewIndex`就足够满足我们的需求了。问题是，模运算在负值时不太好用，当`WeaponIndex`为`0``Direction`为`-1`时会出现这种情况，这会导致`NewIndex`为`-1`。为了解决这个限制，我们需要做一些额外的计算。\n\n`WeaponIndex = (ClampedNewIndex + WeaponCount) % WeaponCount`:这将把`WeaponCount`加到`ClampedNewIndex`上使其为正，并再次应用模得到正确的箝位指数，从而解决了这个问题。\n\n`return Weapons[WeaponIndex]`:这将武器返回到计算出的`WeaponIndex`索引位置。\n\n让我们看一个实际的例子来帮助您想象所有这些是如何工作的:\n\n**武器** =\n\n*   [0]刀\n*   [1]手枪\n*   [2]猎枪\n*   [3]火箭发射器\n\n**WeaponCount** = `Weapons.Num() = 4`。\n\n我们假设`WeaponIndex = 3`和`Direction = 1`。\n\n然后:\n\n`NewIndex` = *武器数量+方向* = 3 + 1 = 4\n\n`ClampedIndex` = *新指数% WeaponCount* = 4 % 4 = 0\n\n`WeaponIndex` = *(夹紧指数+武器数量)%武器数量* = (0 + 4) % 4 = 0\n\n在这个例子中，武器索引的起始值是`3`(这是火箭发射器)，我们想要下一个武器(因为`Direction`是`1`)。执行计算，`WeaponIndex`现在将成为`0`(也就是刀)。这是我们想要的行为，因为我们有 4 个武器，所以我们循环回到这种情况下，由于`Direction`是`1`，我们可以只使用`ClampedIndex`而不做额外的计算。\n\n让我们使用不同的值再次调试它。\n\n我们假设`WeaponIndex = 0`和`Direction = -1`:\n\n`NewIndex` = *武器指数+方向* = 0 + -1 = -1\n\n`ClampedIndex` = *新指数% WeaponCount* = -1 % 4 = -1\n\n`WeaponIndex` = *(夹紧指数+武器数量)%武器数量* = (-1 + 4) % 4 = 3\n\n在这个例子中，武器索引的起始值是 0(也就是刀)，我们想要上一个武器(因为`Direction`是- `1`)。做计算，`WeaponIndex`现在将是 3(这是火箭发射器)。这是我们想要的行为，因为我们有 4 件武器，所以我们绕回 3 件。在这种具体情况下，`NewIndex`是负数，所以不能只用`ClampedIndex`；我们需要做额外的计算来得到正确的值。\n\n## 练习 17.03:使用双向循环数组索引在枚举之间循环\n\n在本练习中，我们将使用来自*练习 17.02* 、*的项目，在虚幻引擎 4 编辑器*中使用 C++ 枚举，并添加两个新的动作映射来循环武器。鼠标滚轮向上将转到上一个武器类型，鼠标滚轮向下将转到下一个武器类型。\n\n以下步骤将帮助您完成练习:\n\n1.  First, open the Visual Studio project from *Exercise 17.02*, *Using C++ Enumerations in the Unreal Engine 4 Editor*.\n\n    接下来，您将更新`Enumerations.h`并添加一个宏，该宏将以非常方便的方式处理双向数组循环，如下所示。\n\n2.  打开`Enumerations.h`并添加`GET_CIRCULAR_ARRAY_INDEX`宏，该宏将应用我们之前已经介绍过的模公式:\n\n    ```cpp\n    #define GET_CIRCULAR_ARRAY_INDEX(Index, Count) (Index % Count +   Count) % Count\n    ```\n\n3.  打开`EnumerationsCharacter.h`并声明武器循环的新输入功能:\n\n    ```cpp\n    void OnPressedPreviousWeapon();\n    void OnPressedNextWeapon();\n    ```\n\n4.  声明`CycleWeapons`函数，如下代码片段所示:\n\n    ```cpp\n    void CycleWeapons(int32 Direction);\n    ```\n\n5.  打开`EnumerationsCharacter.cpp`并在`SetupPlayerInputController`功能中绑定新动作绑定:\n\n    ```cpp\n    PlayerInputComponent->BindAction(\"Previous Weapon\", IE_Pressed,   this, &AEnumerationsCharacter::OnPressedPreviousWeapon);\n    PlayerInputComponent->BindAction(\"Next Weapon\", IE_Pressed, this,   &AEnumerationsCharacter::OnPressedNextWeapon);\n    ```\n\n6.  Now, implement the new input functions, as shown in the following code snippet:\n\n    ```cpp\n    void AEnumerationsCharacter::OnPressedPreviousWeapon()\n    {\n      CycleWeapons(-1);\n    }\n    void AEnumerationsCharacter::OnPressedNextWeapon()\n    {\n      CycleWeapons(1);\n    }\n    ```\n\n    在前面的代码片段中，我们定义了处理`Previous Weapon`和`Next Weapon`的动作映射的函数。每个功能都使用`CycleWeapons`功能，前一个武器的方向为`-1`，下一个武器的方向为`1`。\n\n7.  Implement the `CycleWeapons` functions, which does the bi-directional cycling using the `Direction` parameter based on the current weapon index:\n\n    ```cpp\n    void AEnumerationsCharacter::CycleWeapons(int32 Direction)\n    {\n      const int32 WeaponIndex = ENUM_TO_INT32(Weapon);\n      const int32 AmmoCount = Ammo.Num();\n      const int32 NextRawWeaponIndex = WeaponIndex + Direction;\n      const int32 NextWeaponIndex = GET_CIRCULAR_ARRAY_INDEX(NextRawWeaponIndex , AmmoCount);\n      Weapon = (EWeaponType)NextWeaponIndex;\n    }\n    ```\n\n    在前面的代码片段中，我们实现了`CycleWeapons`函数，该函数使用模运算符根据提供的方向计算下一个有效的武器索引。\n\n8.  编译代码并运行编辑器。\n9.  Go to `Project Settings` and then to `Engine`, then `Input`, and add the new action `bindings`:\n\n    ![Figure 17.10: Adding the Previous Weapon and Next Weapon bindings ](img/B16183_17_10.jpg)\n\n    图 17.10:添加上一个武器和下一个武器绑定\n\n10.  关闭`Project Settings`。\n11.  Now, play in `New Editor Window (PIE)` in single-player mode (one client and dedicated server disabled):\n\n    ![Figure 17.11: The end result of the exercise ](img/B16183_17_11.jpg)\n\n图 17.11:练习的最终结果\n\n完成本练习后，您将能够使用鼠标滚轮在武器之间循环。如果你选择火箭发射器，用鼠标滚轮向下移动到下一个武器，它会回到手枪。如果你用鼠标滚轮向下移动到前一个武器，选择手枪，它会回到火箭发射器。\n\n在下一个活动中，您将在我们在*第 16 章**多人基础*中开始的多人 FPS 项目中加入武器和弹药的概念。\n\n## 活动 17.01:为多人 FPS 游戏添加武器和弹药\n\n在本活动中，您将把武器和弹药的概念添加到我们在上一章活动中开始的多人 FPS 项目中。您需要使用本章中介绍的不同类型的 RPC 来完成本练习。\n\n以下步骤将帮助您完成本活动:\n\n1.  从*活动 16.01* 、*打开`MultiplayerFPS`项目，为多人 FPS 项目*创建角色。\n2.  创建一个名为`Upper Body`的新`AnimMontage`槽。\n3.  Import the animations (`Pistol_Fire.fbx`, `MachineGun_Fire.fbx`, and `Railgun_Fire.fbx`) from the `Activity17.01\\Assets` folder to `Content\\Player\\Animations`.\n\n    注意\n\n    资产文件夹`Activity17.01\\Assets`可以在我们位于[https://packt.live/2It4Plb](https://packt.live/2It4Plb)的 GitHub 存储库中找到。\n\n4.  Create an anim montage for `Pistol_Fire`, `MachineGun_Fire`, and `Railgun_Fire` and make sure they have the following configurations:\n\n    **手枪 _ 射击 _ 蒙太奇**:第`0.01`个`Blend In`时间，第`0.1`个`Blend Out`时间，确保使用`Upper Body`槽。\n\n    **机枪 _ 火力 _ 蒙太奇**:`0.01`的`Blend In`时间，`0.1`的`Blend Out`时间，确保使用上身槽。\n\n    **轨道炮 _ 火力 _ 蒙太奇**:确保使用`Upper Body`槽。\n\n5.  将`Activity17.01\\Assets`文件夹中的`SK_Weapon.fbx`、`NoAmmo.wav`、`WeaponChange.wav`和`Hit.wav`导入`Content\\Weapons`。\n6.  将`Pistol_Fire_Sound.wav`从`Activity17.01\\Assets`导入到`Content\\Weapons\\Pistol`，并在`Pistol_Fire`动画中的`AnimNotify`播放声音上使用。\n7.  创建一个简单的绿色材料称为`M_Pistol`，并将其放在`Content\\Weapons\\Pistol`上。\n8.  将`MachineGun_Fire_Sound.wav`从`Activity17.01\\Assets`导入到`Content\\Weapons\\MachineGun`，并在`MachineGun_Fire`动画中的`AnimNotify`播放声音上使用。\n9.  创建一个简单的红色材料称为`M_MachineGun`，并将其放在`Content\\Weapons\\MachineGun`上。\n10.  将`Railgun_Fire_Sound.wav`从`Activity17.01\\Assets`导入到`Content\\Weapons\\Railgun`，并在`Railgun_Fire`动画中的`AnimNotify`播放声音上使用。\n11.  创建一个简单的白色材料称为`M_Railgun`，并将其放在`Content\\Weapons\\Railgun`上。\n12.  编辑`SK_Mannequin`骨骼网格，从`hand_r`创建一个名为`GripPoint`的插座，相对位置( *X=-10.403845，Y=6.0，Z=-3.124871* )和相对旋转( *X=0.0，Y=0.0，Z=90.0* )。\n13.  使用在*第 4 章*、*玩家输入*中获得的知识，在`Project Settings`中添加以下输入映射:\n    *   火(动作映射):鼠标左键\n    *   以前的武器(动作映射):鼠标滚轮向上\n    *   下一个武器(动作映射):鼠标滚轮向下\n    *   手枪(动作映射):1\n    *   机枪(动作映射):2\n    *   轨道炮(动作映射):3\n14.  在`MultiplayerFPS.h`中，创建`ENUM_TO_INT32(Enum)`宏，该宏对`int32`和`GET_CIRCULAR_ARRAY_INDEX(Index, Count)`进行枚举，使用双向循环数组索引将索引转换为在`0`间隔内的索引和`-1`的计数。\n15.  Create a header file called `EnumTypes.h`, which holds the following enumerations:\n\n    **EwaPointype**:手枪、机枪、轨道炮、MAX\n\n    **电子配置删除**:单个，自动\n\n    **弹药类型**:子弹、子弹、最大值\n\n16.  创建一个从`Actor`类派生的 C++ 类`Weapon`，该类有一个称为`Mesh`的骨架网格组件作为根组件。就变量而言，它存储了名称、武器类型、弹药类型、射击模式、hitscan 走多远、hitscan 命中时造成多大伤害、射速、射击时使用的动画蒙太奇以及没有弹药时播放的声音。在功能方面，它需要能够启动火(也可以停止火，因为自动开火模式)，检查玩家是否可以开火。如果可以的话，它会在所有客户端播放火灾动画，并在相机位置和方向上以提供的长度拍摄一条线迹，以损坏它所击中的演员。如果它没有弹药，它将只在拥有的客户端上播放声音。\n17.  编辑`FPSCharacter`以支持`Fire`、`Previous/Next Weapon`、`Pistol`、`Machine Gun`和`Railgun`的新映射。就变量而言，它需要存储每种类型的弹药数量、当前装备的武器、所有武器类别和衍生实例、击中另一名玩家时的声音以及更换武器时的声音。在功能方面，它需要能够装备/循环/添加武器，管理弹药(添加、移除和获取)，处理角色受损时，在所有客户端上播放动画蒙太奇，并在拥有的客户端上播放声音。\n18.  从`AWeapon`创建`BP_Pistol`，将其放在`Content\\Weapons\\Pistol`上，并用以下值配置:\n    *   骨骼网格:`Content\\Weapons\\SK_Weapon`\n    *   材料:`Content\\Weapons\\Pistol\\M_Pistol`\n    *   名称:`Pistol Mk I`\n    *   武器类型:`Pistol`，弹药类型:`Bullets`，射击模式:`Automatic`\n    *   命中扫描范围:`9999.9`，命中扫描伤害:`5.0`，射速:`0.5`\n    *   火灵装配:`Content\\Player\\Animations\\Pistol_Fire_Montage`\n    *   无声音:`Content\\Weapons\\NoAmmo`\n19.  从`AWeapon`创建`BP_MachineGun`并将其放置在`Content\\Weapons\\MachineGun`上，并使用以下值进行配置:\n    *   骨骼网格:`Content\\Weapons\\SK_Weapon`\n    *   材料:`Content\\Weapons\\MachineGun\\M_MachineGun`\n    *   名称:`Machine Gun Mk I`\n    *   武器类型:`Machine Gun`，弹药类型:`Bullets`，射击模式:`Automatic`\n    *   命中扫描范围:`9999.9`，命中扫描伤害:`5.0`，射速:`0.1`\n    *   火灵装配:`Content\\Player\\Animations\\MachineGun_Fire_Montage`\n    *   无声音:`Content\\Weapons\\NoAmmo`\n20.  从`AWeapon`创建`BP_Railgun`并将其放置在`Content\\Weapons\\Railgun`上，并使用以下值进行配置:\n    *   骨骼网格:`Content\\Weapons\\SK_Weapon`\n    *   材料:`Content\\Weapons\\Railgun\\M_Railgun`\n    *   名称:轨道炮`Mk I`，武器类型:`Railgun`，氨型:`Slugs`，射击模式:`Single`\n    *   命中扫描范围:`9999.9`，命中扫描伤害:`100.0`，射速:`1.5`\n    *   火灵装配:`Content\\Player\\Animations\\Railgun_Fire_Montage`\n    *   无弹药声:`Content\\Weapons\\NoAmmo`\n21.  用以下值配置`BP_Player`:\n    *   武器等级(指数 0: `BP_Pistol`、指数 1: `BP_MachineGun`、指数 2: `BP_Railgun`)。\n    *   点击声音:`Content\\Weapons\\Hit`。\n    *   武器换声:`Content\\Weapons\\WeaponChange`。\n    *   使网格组件阻挡可见通道，这样它就可以被武器的命中扫描击中。\n    *   编辑`ABP_Player`以在`spine_01`骨骼上使用启用了`Mesh Space Rotation Blend`的`Layered Blend Per Bone`节点，以便上身动画使用上身槽。\n    *   Edit `UI_HUD` so that it displays a white dot crosshair in the middle of the screen and the current weapon and ammo count under the Health and Armor indicators:\n\n        ![Figure 17.12: The expected result of the activity ](img/B16183_17_12.jpg)\n\n图 17.12:活动的预期结果\n\n结果应该是一个项目，每个客户将有武器弹药，并将能够使用它们来射击和伤害其他玩家。您还可以通过使用 *1* 、 *2* 和 *3* 键以及上下鼠标滚轮选择上一个和下一个武器来选择武器。\n\n注意\n\n这个活动的解决方案可以在:[https://packt.live/338jEBx](16.html)找到。\n\n# 总结\n\n在本章中，您学习了如何使用 RPC 来允许服务器和客户端在彼此上执行逻辑。我们还通过使用`UENUM`宏学习了枚举在虚幻引擎 4 中的工作原理，以及如何使用双向循环数组索引，这有助于您在两个方向上迭代数组，并在超出其索引限制时循环。\n\n随着本章活动的完成，你将有一个基本的可玩游戏，玩家可以互相射击和切换武器，但是我们还可以添加更多的东西来使它更加有趣。\n\n在下一章中，我们将了解多人游戏中最常见的游戏框架类的实例，以及玩家状态和游戏状态类，这些我们还没有涉及到。我们还将介绍多人游戏模式中使用的一些新概念，以及一些有用的通用内置功能。*"
  },
  {
    "path": "docs/game-dev-proj-ue/17.md",
    "content": "# 十八、多人游戏中的游戏框架类\n\n概观\n\n在这一章中，你将学习多人游戏中游戏框架类的实例。您还将学习如何使用游戏状态和玩家状态类，以及游戏模式中的一些新概念，包括匹配状态。我们还将介绍一些有用的内置功能，可以在不同类型的游戏中使用。\n\n到本章结束时，您将能够使用游戏状态和玩家状态类来存储关于游戏和特定玩家的信息，任何客户端都可以访问这些信息。您还将知道如何充分利用游戏模式类和其他相关功能。\n\n# 简介\n\n在前一章中，我们介绍了*远程过程调用*，它允许服务器和客户端在彼此上执行远程功能。我们还介绍了枚举和*双向循环数组索引*。\n\n在这一章中，我们将看看最常见的游戏框架类，看看它们在多人环境中的实例。理解这一点很重要，这样您就知道在特定的游戏实例中可以访问哪些实例。这方面的一个例子是，只有服务器应该能够访问游戏模式实例，所以如果你在玩堡垒之夜，玩家不应该能够访问它并修改游戏规则。\n\n在本章中，我们还将介绍游戏状态和玩家状态类。顾名思义，这些存储关于游戏状态和每个玩游戏的玩家的信息。最后，在本书的最后，我们将介绍游戏模式中的一些新概念，以及一些有用的内置功能。\n\n我们将从多人游戏中游戏框架类如何工作开始。\n\n# 多人游戏中的游戏框架类\n\n虚幻引擎 4 附带了一个游戏框架，这是一组允许你更容易地创建游戏的类。游戏框架通过提供大多数游戏中存在的内置通用功能来做到这一点，例如定义游戏规则(游戏模式)的方式，以及控制角色(玩家控制器和棋子/角色类)的方式。当在多人游戏环境中创建一个游戏框架类的实例时，它可以存在于服务器、客户端和拥有者客户端上，拥有者客户端的玩家控制器是该实例的所有者。这意味着游戏框架类的实例总是属于以下类别之一:\n\n*   **仅服务器**:类的实例将只存在于服务器上。\n*   **服务器和客户端**:类的实例将存在于服务器和客户端上。\n*   **服务器和拥有客户端**:类的实例将存在于服务器和拥有客户端上。\n*   **仅拥有客户端**:类的实例将只存在于拥有客户端上。\n\n请看下图，它显示了游戏框架中最常见的类的类别和用途:\n\n![Figure 18.1: The most common gameplay framework classes divided into categories ](img/B16183_18_01.jpg)\n\n图 18.1:最常见的游戏框架分类\n\n让我们更详细地了解一下上图中的每个类:\n\n*   **游戏模式(仅限服务器)**:游戏模式类定义了游戏的规则，其实例只能被服务器访问。如果客户端试图访问它，实例将总是无效的，以防止客户端更改游戏规则。\n*   **游戏状态(服务器和客户端)**:游戏状态类存储游戏的状态，服务器和客户端都可以访问它的实例。游戏状态将在未来的主题中更深入地讨论。\n*   **玩家状态(服务器和客户端)**:玩家状态类存储玩家的状态，其实例可以被服务器和客户端访问。玩家状态将在未来的主题中有更深入的讨论。\n*   **棋子(服务器和客户端)**:棋子类是玩家的可视化表示，其实例可以被服务器和客户端访问。\n*   **PlayerController(服务器和拥有客户端)**:玩家控制器类代表玩家的意图，这个意图被中继到当前拥有的棋子上，它的实例只能在服务器和拥有客户端上访问。出于安全原因，客户端无法访问其他客户端的播放器控制器，因此它们应该使用服务器进行通信。如果客户端使用除`0`以外的索引调用`UGameplayStatics::GetPlayerController`函数(这将返回其播放器控制器)，则返回的实例将始终无效。这意味着服务器是唯一可以访问所有播放器控制器的地方。您可以通过调用`AController::IsLocalController`函数来确定一个玩家控制器实例是否在它自己的客户端中。\n*   **HUD(仅限拥有客户端)**:HUD 类作为即时模式在屏幕上绘制基本形状和文字。因为它用于用户界面，所以它的实例只在拥有它的客户机上可用，因为服务器和其他客户机不需要知道它。\n*   **UMG 小部件(仅拥有客户端)**:UMG 小部件类用于在屏幕上显示复杂的 UI。因为它用于用户界面，所以它的实例只在拥有它的客户机上可用，因为服务器和其他客户机不需要知道它。\n\n为了帮助你理解这些概念，我们可以用 Dota 2 作为例子。游戏模式定义了游戏有不同的阶段(*赛前为英雄挑选，实际游戏，赛后与胜者*)，最终目标是摧毁对方队伍的远古。因为这是一个对游戏性至关重要的类，所以不能允许客户端访问它:\n\n*   游戏状态存储经过的时间，无论是白天还是晚上，每个团队的得分等等，所以服务器和客户端需要能够访问它。\n*   玩家状态存储了玩家的名字、选择的英雄和击杀/死亡/辅助比率，所以服务器和客户端需要能够访问它。\n*   棋子将是英雄、信使、幻象等等，由玩家控制，因此服务器和客户端需要能够访问它。\n*   玩家控制器将输入信息传递给被控制的棋子，因此只有服务器和拥有它的客户端才需要能够访问它。\n*   用户界面类(`HUD`和`User`小部件)将显示拥有客户端的所有信息，因此只需要在那里访问。\n\n在下一个练习中，您将显示最常见的游戏框架类的实例值。\n\n## 练习 18.01:显示游戏框架实例值\n\n在本练习中，我们将创建一个使用第三人称模板的新 C++ 项目，并添加以下内容:\n\n*   在拥有的客户端上，播放器控制器创建一个简单的 UMG 小部件并将其添加到视口中，该小部件显示菜单实例的名称。\n*   On the tick function, the character displays the value of its own instance (as a pawn) as well as whether it has a valid instance for the game mode, game state, player state, player controller, and HUD.\n\n    注意\n\n    如果需要，您可以参考*第 1 章*、*虚幻引擎介绍*，来回顾一下勾选功能。\n\n以下步骤将帮助您完成练习:\n\n1.  使用名为`GFInstances` ( *)的`C++ `创建一个新的`Third Person`模板项目，并将其保存在您选择的位置。一旦创建了项目，它就应该打开编辑器和 Visual Studio 解决方案。*\n2.  在编辑器中，创建一个名为`GFInstancePlayerController`的新`C++ `类，该类源自`PlayerController`。等待编译结束，关闭编辑器，然后返回 Visual Studio。\n3.  打开`GFInstancesCharacter.h`文件，声明`Tick`功能的保护覆盖:\n\n    ```cpp\n    virtual void Tick(float DeltaSeconds) override;\n    ```\n\n4.  打开`GFInstancesCharacter.cpp`文件，包括`DrawDebugHelpers.h`和`PlayerController.h` :\n\n    ```cpp\n    #include \"DrawDebugHelpers.h\"\n    #include \"GameFramework/PlayerController.h\"\n    ```\n\n5.  实现`Tick`功能:\n\n    ```cpp\n    void AGFInstancesCharacter::Tick(float DeltaSeconds)\n    {\n      Super::Tick(DeltaSeconds);\n    }\n    ```\n\n6.  Get the instances for the game mode, game state, player controller, and HUD:\n\n    ```cpp\n    AGameModeBase* GameMode = GetWorld()->GetAuthGameMode();\n    AGameStateBase* GameState = GetWorld()->GetGameState();\n    APlayerController* PlayerController =   Cast<APlayerController>(GetController());\n    AHUD* HUD = PlayerController != nullptr ? PlayerController-  >GetHUD() : nullptr;\n    ```\n\n    在前面的代码片段中，我们将游戏模式、游戏状态、玩家控制器和 HUD 的实例存储在单独的变量中，这样我们就可以检查它们是否有效。\n\n7.  Create a string for each gameplay framework class:\n\n    ```cpp\n    const FString GameModeString = GameMode != nullptr ?   TEXT(\"Valid\") : TEXT(\"Invalid\");\n    const FString GameStateString = GameState != nullptr ?   TEXT(\"Valid\") : TEXT(\"Invalid\");\n    const FString PlayerStateString = GetPlayerState() != nullptr ?   TEXT(\"Valid\") : TEXT(\"Invalid\");\n    const FString PawnString = GetName();\n    const FString PlayerControllerString = PlayerController !=   nullptr ? TEXT(\"Valid\") : TEXT(\"Invalid\");\n    const FString HUDString = HUD != nullptr ? TEXT(\"Valid\") :   TEXT(\"Invalid\");\n    ```\n\n    在这里，我们创建字符串来存储棋子的名称以及其他游戏框架实例是否有效。\n\n8.  Display each string on the screen:\n\n    ```cpp\n    const FString String = FString::Printf(TEXT(\"Game Mode = %s\\nGame   State = %s\\nPlayerState = %s\\nPawn = %s\\nPlayer Controller =   %s\\nHUD = %s\"), *GameModeString, *GameStateString,   *PlayerStateString, *PawnString, *PlayerControllerString,   *HUDString);\n    DrawDebugString(GetWorld(), GetActorLocation(), String, nullptr,   FColor::White, 0.0f, true);\n    ```\n\n    在这个代码片段中，我们打印了前面代码中创建的字符串，这些字符串指示棋子的名称以及其他游戏框架实例是否有效。\n\n9.  Before we can move on to the `AGFInstancesPlayerController` class, we need to tell Unreal Engine that we want to use UMG functionality in order to be able to use the `UUserWidget` class. To do this, we need to open `GFInstances.Build.cs` and add `UMG` to the `PublicDependencyModuleNames` string array, like so:\n\n    ```cpp\n    PublicDependencyModuleNames.AddRange(new string[] { \"Core\",   \"CoreUObject\", \"Engine\", \"InputCore\", \"HeadMountedDisplay\",   \"UMG\" });\n    ```\n\n    如果您试图编译并从添加新模块中获得错误，那么请清理并重新编译您的项目。如果这不起作用，请尝试重新启动您的 IDE。\n\n10.  打开`GFInstancesPlayerController.h`并添加受保护的变量来创建 UMG 小部件:\n\n    ```cpp\n    UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = \"GF   Instance Player Controller\")\n    TSubclassOf<UUserWidget> MenuClass;\n    UPROPERTY()\n    UUserWidget* Menu;\n    ```\n\n11.  宣布`BeginPlay`功能的保护覆盖:\n\n    ```cpp\n    virtual void BeginPlay() override;\n    ```\n\n12.  打开`GFInstancesPlayerController.cpp`并包含`UserWidget.h` :\n\n    ```cpp\n    #include \"Blueprint/UserWidget.h\"\n    ```\n\n13.  实现`BeginPlay`功能:\n\n    ```cpp\n    void AGFInstancePlayerController::BeginPlay()\n    {\n      Super::BeginPlay();\n    }\n    ```\n\n14.  如果不是拥有的客户端或者菜单类无效，中止该功能:\n\n    ```cpp\n    if (!IsLocalController() || MenuClass == nullptr)\n    {\n      return;\n    }\n    ```\n\n15.  创建小部件并将其添加到视口:\n\n    ```cpp\n    Menu = CreateWidget<UUserWidget>(this, MenuClass);\n    if (Menu != nullptr)\n    {\n      Menu->AddToViewport(0);\n    }\n    ```\n\n16.  编译并运行代码。\n17.  在`Content Browser`中，转到`Content`文件夹，新建一个名为`UI`的文件夹，并将其打开。\n18.  创建一个名为`UI_Menu`的新小部件蓝图并打开它。\n19.  将名为`tbText`的`Text Block`添加到根画布面板，并通过单击“详细信息”面板顶部其名称旁边的复选框`Is Variable`将其设置为变量。\n20.  将`tbText`设置为`Size To Content`至`true`。\n21.  Go to the `Graph` section and, in `Event Graph`, implement the `Event Construct` in the following manner:\n\n    ![Figure 18.2: The Event Construct that displays the name of the UI_Menu instance ](img/B16183_18_02.jpg)\n\n    图 18.2:显示用户界面菜单实例名称的事件构造\n\n    注意\n\n    您可以在以下链接找到前面的全分辨率截图，以便更好地查看:[https://packt.live/38wvSr5](https://packt.live/38wvSr5)。\n\n22.  保存并关闭`UI_Menu`。\n23.  转到`Content`文件夹，创建一个名为`BP_PlayerController`的蓝图，该蓝图源自`GFInstancesPlayerController`。\n24.  打开`BP_PlayerController`，设置`Menu` `Class`使用`UI_Menu`。\n25.  保存并关闭`BP_PlayerController`。\n26.  转到`Content`文件夹，创建一个名为`BP_GameMode`的蓝图，该蓝图源自`GFInstancesGameMode`。\n27.  打开`BP_GameMode`，设置`Player Controller` `Class`使用`BP_PlayerController`。\n28.  保存并关闭`BP_GameMode`。\n29.  转到`Project Settings`，从左侧面板中选择`Maps & Modes`，在`Project`类别中。\n30.  将`Default` `GameMode`设置为使用`BP_GameMode`。\n31.  Close `Project Settings`.\n\n    最后，您可以测试项目。\n\n32.  运行代码，等待编辑器完全加载。\n33.  转到`Multiplayer Options`，将客户端数量设置为`2`。\n34.  将窗口大小设置为`800x600`。\n35.  在`New Editor Window (PIE)`中播放。\n\n完成本练习后，您将能够在每个客户端上进行游戏。您会注意到角色正在显示游戏模式、游戏状态、玩家状态、玩家控制器和抬头显示器的实例是否有效。它还显示棋子实例的名称。\n\n现在，让我们分析一下`Server`和`Client 1`窗口中显示的值。让我们先从`Server`窗口开始。\n\n## 服务器窗口\n\n在`Server`窗口，你有`Server Character`的值，在后台，你有`Client 1 Character`的值。左上角应该可以看到`Server Character`、`Client 1 Character`和`UI_Menu` UMG 小部件。UMG 小部件实例只为`Server Character`的玩家控制器创建，因为它是该窗口中唯一一个实际控制角色的玩家控制器。\n\n我们先来分析一下`Server Character`的数值。\n\n## 服务器 er 字符\n\n这是监听服务器控制的角色，该服务器也集成了一个可以玩游戏的客户端。该字符上显示的值如下:\n\n*   **游戏模式=有效**因为游戏模式实例只存在于服务器中，也就是当前的游戏实例。\n*   **游戏状态=有效**因为游戏状态实例存在于客户端和服务器端，也就是当前的游戏实例。\n*   **玩家状态=有效**因为玩家状态实例存在于客户端和服务器端，也就是当前的游戏实例。\n*   **棋子= ThirdPersonCharacter_2** 因为棋子实例存在于客户端和服务器端，也就是当前的游戏实例。\n*   **玩家控制器=有效**因为玩家控制器实例存在于拥有的客户端和服务器上，这是当前的游戏实例。\n*   **抬头显示器=有效**因为抬头显示器实例只存在于拥有的客户端上，情况就是这样。\n\n接下来，我们将在同一个窗口中查看`Client 1 Character`。\n\n## Clie nt 1 字符\n\n这就是`Client 1`所控制的人物。该字符上显示的值如下:\n\n*   **游戏模式=有效**因为游戏模式实例只存在于服务器中，也就是当前的游戏实例。\n*   **游戏状态=有效**因为游戏状态实例存在于客户端和服务器端，也就是当前的游戏实例。\n*   **玩家状态=有效**因为玩家状态实例存在于客户端和服务器端，也就是当前的游戏实例。\n*   **棋子= ThirdPersonCharacter_0** 因为棋子实例存在于客户端和服务器端，也就是当前的游戏实例。\n*   **玩家控制器=有效**因为玩家控制器实例存在于拥有的客户端和服务器上，这是当前的游戏实例。\n*   **HUD =无效**因为 HUD 实例只存在于拥有的客户端上，而事实并非如此。\n\n## 客户端 1 窗口\n\n在`Client 1`窗口中，你有`Client 1 Character`的值，在后台，你有`Server Character`的值。你应该会看到`Client 1 Character,` `Server Character`，以及左上角的`UI_Menu` UMG 小部件。UMG 小部件实例只为`Client 1 Character`的玩家控制器创建，因为它是该窗口中唯一一个实际控制角色的玩家控制器。\n\n我们先来分析一下`Client 1 Character`的数值。\n\n## 客户端 1 字符\n\n这就是`Client 1`所控制的人物。该字符上显示的值如下:\n\n*   **游戏模式=无效**因为游戏模式实例只存在于服务器中，并不是当前的游戏实例。\n*   **游戏状态=有效**因为游戏状态实例存在于服务器和客户端，是当前的游戏实例。\n*   **玩家状态=有效**因为玩家状态实例存在于服务器和客户端，也就是当前的游戏实例。\n*   **棋子= ThirdPersonCharacter_0** 因为棋子实例存在于服务器和客户端，也就是当前的游戏实例。\n*   **玩家控制器=有效**因为玩家控制器实例存在于服务器和所属客户端，也就是当前的游戏实例。\n*   **抬头显示器=有效**因为抬头显示器实例只存在于拥有的客户端上，情况就是这样。\n\n接下来，我们将在同一个窗口中查看`Server Character`。\n\n## 服务器角色\n\n这是监听服务器正在控制的角色。该字符上显示的值如下:\n\n*   **游戏模式=无效**因为游戏模式实例只存在于服务器中，并不是当前的游戏实例。\n*   **游戏状态=有效**因为游戏状态实例存在于服务器和客户端，是当前的游戏实例。\n*   **玩家状态=有效**因为玩家状态实例存在于服务器和客户端，也就是当前的游戏实例。\n*   **棋子= ThirdPersonCharacter_2** 因为棋子实例存在于服务器和客户端，也就是当前的游戏实例。\n*   **玩家控制器=无效**因为玩家控制器实例存在于服务器和所属客户端，而不是当前的游戏实例。\n*   **HUD =无效**因为 HUD 实例只存在于拥有的客户端上，而事实并非如此。\n\n通过完成这个练习，您应该更好地理解游戏框架类的每个实例在哪里存在，在哪里不存在。接下来，我们将介绍玩家状态和游戏状态类，以及一些关于游戏模式和有用的内置功能的附加概念。\n\n# 游戏模式、玩家状态和游戏状态\n\n到目前为止，我们已经涵盖了游戏框架中的大多数重要类，包括游戏模式、玩家控制器和棋子。在本章中，我们将介绍玩家状态、游戏状态和游戏模式的一些附加概念，以及一些有用的内置功能。\n\n## 游戏模式\n\n我们已经讨论了游戏模式及其工作原理，但还有几个概念尚未涉及。\n\n### 建造师\n\n要设置默认的类值，可以使用如下构造函数:\n\n```cpp\nATestGameMode::ATestGameMode()\n{\n  DefaultPawnClass = AMyCharacter::StaticClass();\n  PlayerControllerClass = AMyPlayerController::StaticClass();\n  PlayerStateClass = AMyPlayerState::StaticClass();\n  GameStateClass = AMyGameState::StaticClass();\n}\n```\n\n前面的代码让你指定当我们使用这种游戏模式时产生棋子、玩家控制器、玩家状态和游戏状态时使用哪些类。\n\n**获取游戏模式实例**\n\n如果要访问游戏模式实例，需要使用以下代码从`GetWorld`函数获取:\n\n```cpp\nAGameModeBase* GameMode = GetWorld()->GetAuthGameMode();\n```\n\n前面的代码允许您访问当前的游戏模式实例，这样您就可以运行函数并查阅某些变量的值。您必须确保只在服务器上调用它，因为出于安全原因，这在客户端上是无效的。\n\n**匹配状态**\n\n到目前为止，我们只使用了`AGameModeBase`类，这是框架中最基本的游戏模式类，虽然对于某些类型的游戏来说已经足够了，但是有些情况下您需要更多一点的功能。这方面的一个例子是，如果我们想做一个大厅系统，只有当所有球员都标记他们准备好了，比赛才会开始。这个例子不可能用`AGameModeBase`类来做。对于这些情况，最好使用`AGameMode`类，它是`AGameModeBase`的子类，通过使用匹配状态来增加对多人游戏的支持。匹配状态的工作方式是使用在给定时间只能处于以下状态之一的状态机:\n\n*   `EnteringMap`:这是世界还在加载，演员还没滴答走的开始状态。一旦世界完成装载，它将转换到`WaitingToStart`状态。\n*   `WaitingToStart`:这个状态是在世界加载完毕，演员在滴答作响的时候设定的，虽然因为游戏还没开始，玩家的棋子不会产生。当状态机进入该状态时，会调用`HandleMatchIsWaitingToStart`函数。如果`ReadyToStartMatch`函数返回`true`或者如果在代码中的某处调用了`StartMatch`函数，状态机将转换到`InProgress`状态。\n*   `InProgress`:这个状态就是实际游戏发生的地方。当状态机进入这种状态时，会为玩家催生棋子，调用世界上所有演员上的`BeginPlay`，调用`HandleMatchHasStarted`函数。如果`ReadyToEndMatch`功能返回`true`或者如果在代码中的某处调用了`EndMatch`功能，状态机将转换到`WaitingPostMatch`状态。\n*   `WaitingPostMatch`:比赛结束时设置此状态。当状态机进入该状态时，会调用`HandleMatchHasEnded`函数。在这种状态下，演员仍然打勾，但新玩家不能加入。当它开始卸载世界时，它将过渡到`LeavingMap`状态。\n*   `LeavingMap`:这个状态是在卸载世界的时候设置的。当状态机进入该状态时，会调用`HandleLeavingMap`函数。当状态机开始加载新级别时，它将转换到`EnteringMap`状态。\n*   `Aborted`:这是一种失败状态，只能通过调用`AbortMatch`函数来设置，该函数用于标记出现了阻止比赛发生的错误。\n\n为了帮助您更好地理解这些概念，我们可以再次以 Dota 2 为例:\n\n*   `EnteringMap`:加载地图时状态机会处于这种状态。\n*   `WaitingToStart`:一旦地图加载完毕，玩家在挑选自己的英雄，状态机就会处于这种状态。`ReadyToStartMatch`功能会检查是否所有玩家都选择了自己的英雄；如果他们有，那么比赛就可以开始了。\n*   `InProgress`:游戏实际进行的时候状态机会处于这种状态。玩家控制他们的英雄去农场和其他玩家战斗。`ReadyToEndMatch`功能会不断检查每个远古的健康状况，看是否有一个被破坏；如果是的话，比赛就结束了。\n*   `WaitingPostMatch`:当游戏结束，你正在看到被摧毁的远古，并显示每个玩家的最终分数时，状态机将处于这种状态。\n*   `LeavingMap`:状态机卸载地图时会处于这种状态。\n*   `Aborted`:如果其中一个选手在初始阶段连接失败，状态机就会处于这种状态，从而中止整场比赛。\n\n**让玩家重生**\n\n当玩家死亡，你想重生时，你通常有两个选择。第一个选项是重用同一个棋子实例，手动将其状态重置回默认值，并将其传送到重生位置。第二种选择是摧毁棋子并产生一个新的棋子，这个棋子已经重置了状态。如果您更喜欢后一个选项，那么`AGameModeBase::RestartPlayer`功能会为您处理为某个玩家控制器生成新棋子实例的逻辑，并将其放在玩家开始处。\n\n需要考虑的重要一点是，该函数只有在玩家控制器还没有拥有棋子的情况下才会产生一个新的棋子实例，所以在调用`RestartPlayer`之前一定要销毁被控制的棋子。\n\n看看下面的例子:\n\n```cpp\nvoid ATestGameMode::OnDeath(APlayerController* VictimController)\n{\n  if(VictimController == nullptr)\n  {\n    return;\n  }\n\n  APawn* Pawn = VictimController->GetPawn();\n  if(Pawn != nullptr)\n  {\n    Pawn->Destroy();\n  }\n\n  RestartPlayer(VicitimController);\n}\n```\n\n在前面的代码中，我们有`OnDeath`函数，该函数获取死亡玩家的玩家控制器，销毁其控制的棋子，并调用`RestartPlayer`函数在玩家开始时产生一个新的实例。默认情况下，所使用的玩家启动演员将始终与第一次衍生的玩家相同。如果你想让该功能在随机玩家启动时产生，那么你需要覆盖`AGameModeBase::ShouldSpawnAtStartSpot`功能并强制其为`return false`，如下所示:\n\n```cpp\nbool ATestGameMode::ShouldSpawnAtStartSpot(AController* Player)\n{\n  return false;\n}\n```\n\n前面的代码将使游戏模式使用随机玩家开始，而不是总是使用相同的。\n\n注意\n\n有关游戏模式的更多信息，请访问[https://docs . unrealengine . com/en-US/game play/Framework/game mode/# game modes](https://docs.unrealengine.com/en-US/Gameplay/Framework/GameMode/#gamemodes)和[https://docs . unrealengine . com/en-US/API/Runtime/Engine/game Framework/AGameMode/index . html](https://docs.unrealengine.com/en-US/API/Runtime/Engine/GameFramework/AGameMode/index.html)。\n\n## 玩家状态\n\n玩家状态类存储玩家的状态，例如当前得分、死亡/死亡和捡到的硬币。在多人模式下，它主要用于存储其他客户端需要了解的玩家信息，因为他们无法访问其玩家控制器。最广泛使用的内置变量是`PlayerName`、`Score`和`Ping`，它们分别给你玩家的名字、分数和 ping。\n\n多人射击游戏的记分牌条目是如何使用玩家状态的一个很好的例子，因为每个客户端都需要知道所有玩家的名字、死亡和 pings。玩家状态实例可以通过以下方式访问:\n\n**控制器::播放器状态**\n\n该变量具有与控制器相关联的播放器状态，并且它只能由服务器和拥有它的客户端访问。以下示例将演示如何使用变量:\n\n```cpp\nAPlayerState* PlayerState = Controller->PlayerState;\n```\n\n**控制器::get layerstate()**\n\n该函数返回与控制器相关联的播放器状态，并且它只能由服务器和拥有它的客户端访问。这个函数还有一个模板版本，所以你可以把它转换成你自己的自定义玩家状态类。以下示例将演示如何使用该函数的默认版本和模板版本:\n\n```cpp\n// Default version\nAPlayerState* PlayerState = Controller->GetPlayerState();\n// Template version\nATestPlayerState* MyPlayerState = Controller->GetPlayerState<ATestPlayerState>();\n```\n\n**apawn::get layerstate()**\n\n该函数返回与拥有棋子的控制器相关的玩家状态，服务器和客户端可以访问该状态。这个函数还有一个模板版本，所以你可以把它转换成你自己的自定义玩家状态类。以下示例将演示如何使用该函数的默认版本和模板版本:\n\n```cpp\n// Default version\nAPlayerState* PlayerState = Pawn->GetPlayerState();\n// Template version\nATestPlayerState* MyPlayerState = Pawn-  >GetPlayerState<ATestPlayerState>();\n```\n\n上面的代码演示了两种使用`GetPlayerState`函数的方法。您可以使用默认的`APlayerState`版本或为您自动转换的模板版本。\n\n蹲下::播放器阵列\n\n该变量存储每个玩家的玩家状态实例，可以在服务器和客户端上访问。以下示例将演示如何使用该变量:\n\n```cpp\nTArray<APlayerState*> PlayerStates = GameState->PlayerArray;\n```\n\n为了帮助您更好地理解这些概念，我们可以再次以 Dota 2 为例。玩家状态至少有以下变量:\n\n**姓名**:玩家姓名\n\n**英雄**:选中的英雄\n\n**生命值**:英雄的生命值\n\n**法力**:英雄的法力\n\n**属性**:英雄属性\n\n**等级**:英雄当前所处的等级\n\n**击杀/死亡/辅助**:玩家的击杀/死亡/辅助比例\n\n注意\n\n关于玩家状态的更多信息，请访问[https://docs . unrealengine . com/en-US/API/Runtime/Engine/game framework/aplayer state/index . html](https://docs.unrealengine.com/en-US/API/Runtime/Engine/GameFramework/APlayerState/index.html)。\n\n## 游戏状态\n\n游戏状态类存储游戏的状态，包括比赛经过的时间和赢得游戏所需的分数。它主要用于多人模式下存储其他客户端需要了解的游戏信息，因为他们无法访问游戏模式。最广泛使用的变量是`PlayerArray`，这是一个数组，里面有每个连接的客户端的玩家状态。多人射击游戏上的记分牌是如何使用游戏状态的一个很好的例子，因为每个客户端都需要知道需要多少次击杀才能获胜，以及每个玩家的名字和 ping。\n\n可以通过以下方式访问游戏状态实例:\n\n**超世界::GetGameState()**\n\n这个函数返回与世界相关的游戏状态，可以在服务器和客户端访问。这个函数还有一个模板版本，所以你可以把它转换成你自己的自定义游戏状态类。以下示例将演示如何使用该函数的默认版本和模板版本:\n\n```cpp\n// Default version\nAGameStateBase* GameState = GetWorld()->GetGameState();\n// Template version\nAMyGameState* MyGameState = GetWorld()->GetGameState<AMyGameState>();\n```\n\n**阿加梅辩论::游戏状态**\n\n此变量具有与游戏模式相关联的游戏状态，并且只能在服务器上访问。以下示例将演示如何使用变量:\n\n```cpp\nAGameStateBase* GameState = GameMode->GameState;\n```\n\n**琼浆玉液:GetGameState()**\n\n该函数返回与游戏模式相关的游戏状态，并且只能在服务器上访问。这个函数还有一个模板版本，所以你可以把它转换成你自己的自定义游戏状态类。以下示例将演示如何使用该函数的默认版本和模板版本:\n\n```cpp\n// Default version\nAGameStateBase* GameState = GameMode->GetGameState<AGameStateBase>();\n// Template version\nAMyGameState* MyGameState = GameMode->GetGameState<AMyGameState>();\n```\n\n为了帮助您更好地理解这些概念，我们可以再次以 Dota 2 为例。游戏状态会有以下变量:\n\n**经过时间**:比赛进行了多久\n\n**辐射击杀**:辐射队击杀了多少可怕的英雄\n\n**可怕的杀戮**:可怕的队伍杀死了多少光芒四射的英雄\n\n**昼/夜计时器**:用于判断是白天还是黑夜\n\n注意\n\n有关游戏状态的更多信息，请访问[https://docs . unrealengine . com/en-US/game play/Framework/game mode/# game state](https://docs.unrealengine.com/en-US/Gameplay/Framework/GameMode/#gamestate)和[https://docs . unrealengine . com/en-US/API/Runtime/Engine/game Framework/AGameState/index . html](https://docs.unrealengine.com/en-US/API/Runtime/Engine/GameFramework/AGameState/index.html)。\n\n## 有用的内置功能\n\n虚幻引擎 4 内置了很多有用的功能。以下是一些在开发游戏时有用的功能和组件的示例:\n\n**无效因素:结束播放(常量:结束播放原因:类型:结束播放原因)**\n\n这个函数在演员已经停止播放时调用，与`BeginPlay`函数相反。你有`EndPlayReason`参数，它告诉你为什么演员停止播放(如果它被破坏了，如果你停止了 PIE，等等)。看看下面的例子，它在屏幕上打印出演员已经停止演奏的事实:\n\n```cpp\nvoid ATestActor::EndPlay(const EEndPlayReason::Type EndPlayReason)\n{\n  Super::EndPlay(EndPlayReason);\n  const FString String = FString::Printf(TEXT(«The actor %s has just     stopped playing\"), *GetName());\n  GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Red, String);\n}\n```\n\n**无效攻击::着陆(持续攻击结果&命中)**\n\n当玩家在空中后降落在一个表面时，这个函数被调用。看看下面的例子，当玩家在表面上着陆时，它会发出声音:\n\n```cpp\nvoid ATestCharacter::Landed(const FHitResult& Hit)\n{\n  Super::Landed(Hit);\n  UGameplayStatics::PlaySound2D(GetWorld(), LandSound);\n}\n```\n\n**bool uwworld::server travel(const fstab ring&furl，bool bAbsolute，bool bshouldskipgameotify)**\n\n这个函数将使服务器加载一个新的地图，并把所有连接的客户端一起带来。这不同于使用其他加载地图的方法，比如`UGameplayStatics::OpenLevel`函数，因为它不会把客户端带来；它只是将地图加载到服务器上，然后断开客户端。\n\n需要考虑的一件重要事情是，server travel 只能在打包版本中正常工作，因此在编辑器中播放时不会将客户端带在身边。看看下面的例子，它获取当前的地图名称，并使用 server travel 来重新加载它并带来连接的客户端:\n\n```cpp\nvoid ATestGameModeBase::RestartMap()\n{\n  const FString URL = GetWorld()->GetName();\n  GetWorld()->ServerTravel(URL, false, false);\n}\n```\n\n**void TArray::Sort(const 谓语 _CLASS &谓语)**\n\n`TArray`数据结构附带了`Sort`函数，该函数允许您使用`lambda`函数对数组的值进行排序，该函数返回值`A`是否应该先排序，然后是值`B`。看看下面的例子，它从最小值到最大值对整数数组进行排序:\n\n```cpp\nvoid ATestActor::SortValues()\n{\n  TArray<int32> SortTest;\n  SortTest.Add(43);\n  SortTest.Add(1);\n  SortTest.Add(23);\n  SortTest.Add(8);\n  SortTest.Sort([](const int32& A, const int32& B) { return A < B; });\n}\n```\n\n前面的代码将按值[43，1，23，8]从最小到最大[1，8，23，43]对`SortTest`数组进行排序。\n\n**void aactor::felloutofworld(const udmagetype&dmgtype)**\n\n在虚幻引擎 4 中，有一个概念叫做`Kill Z`，它是`Z`中某个值上的一个平面(设置在`World Settings`面板中)，如果一个演员低于那个`Z`值，它会调用`FellOutOfWorld`函数，默认情况下，这个函数会破坏这个演员。看看下面的例子，它在屏幕上打印出演员从世界上消失的事实:\n\n```cpp\nvoid AFPSCharacter::FellOutOfWorld(const UDamageType& DmgType)\n{\n  Super::FellOutOfWorld(DmgType);\n  const FString String = FString::Printf(TEXT(\"The actor %s has fell     out of the world\"), *GetName());\n  GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Red, String);\n}\n```\n\n**尿静化运动组件**\n\n该组件按照在`RotationRate`变量中定义的每个轴上的特定速率，沿时间旋转拥有的参与者。要使用它，您需要包含以下标题:\n\n```cpp\n#include \"GameFramework/RotatingMovementComponent.h\"\n```\n\n声明组件变量:\n\n```cpp\nUPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = \"Test Actor\")\nURotatingMovementComponent* RotatingMovement;\n```\n\n最后，在 actor 构造函数中初始化它，如下所示:\n\n```cpp\nRotatingMovement = CreateDefaultSubobject   <URotatingMovementComponent>(\"Rotating Movement\");\nRotatingMovement->RotationRate = FRotator(0.0, 90.0f, 0);\n```\n\n在前面的代码中，`RotationRate`被设置为在`Yaw`轴上每秒旋转`90`度。\n\n## 练习 18.02:制作简单的多人皮卡游戏\n\n在本练习中，我们将创建一个使用第三人称模板的新 C++ 项目，并添加以下内容:\n\n*   在拥有的客户端上，玩家控制器创建一个 UMG 小部件并将其添加到视口中，为每个玩家显示分数，从最高到最低排序，以及它收集了多少个拾取。\n*   创建一个简单的拾取演员类，给拾取它的玩家 10 分。拾波器也将在`Yaw`轴上每秒旋转 90 度。\n*   将`Kill Z`设置为`-500`，使玩家重生，每次从世界上掉下来，丢 10 分。\n*   当没有更多的皮卡可用时，游戏将结束。一旦游戏结束，所有角色都将被摧毁，5 秒钟后，服务器将进行一次服务器旅行调用，以重新加载相同的地图，并带来连接的客户端。\n\n以下步骤将帮助您完成练习:\n\n1.  使用名为`Pickups`的`C++ `创建一个新的`Third Person`模板项目，并将其保存到您选择的位置。\n2.  Once the project has been created, it should open the editor as well as the Visual Studio solution.\n\n    现在，让我们创建将要使用的新 C++ 类:\n\n3.  创建从`Actor`派生的`Pickup`类。\n4.  创建从`GameState`派生的`PickupsGameState`类。\n5.  创建从`PlayerState`派生的`PickupsPlayerState`类。\n6.  创建从`PlayerController`派生的`PickupsPlayerController`类。\n7.  Close the editor and open Visual Studio.\n\n    接下来，我们来学习`Pickup`课。\n\n8.  打开`Pickup.h`并清除所有现有功能。\n9.  声明受保护的`Static Mesh` 组件名为`Mesh` :\n\n    ```cpp\n    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category =   \"Pickup\")\n    UStaticMeshComponent* Mesh;\n    ```\n\n10.  声明被保护的旋转运动部件，称为`RotatingMovement` :\n\n    ```cpp\n    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category =   \"Pickup\")\n    class URotatingMovementComponent* RotatingMovement;\n    ```\n\n11.  声明受保护的`PickupSound`变量:\n\n    ```cpp\n    UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category =   \"Pickup\")\n    USoundBase* PickupSound;\n    ```\n\n12.  声明受保护的构造函数并`BeginPlay`覆盖:\n\n    ```cpp\n    APickup();\n    virtual void BeginPlay() override;\n    ```\n\n13.  声明受保护的`OnBeginOverlap`功能:\n\n    ```cpp\n    UFUNCTION()\n    void OnBeginOverlap(UPrimitiveComponent* OverlappedComp, AActor*   OtherActor, UPrimitiveComponent* OtherComp, int32   OtherBodyIndex, bool bFromSweep, const FHitResult& Hit);\n    ```\n\n14.  打开`Pickup.cpp`，包括`PickupsCharacter.h`、`PickupsGameState.h`、`StaticMeshComponent.h`、【T4:\n\n    ```cpp\n    #include \"PickupsCharacter.h\"\n    #include \"PickupsGameState.h\"\n    #include \"Components/StaticMeshComponent.h\"\n    #include \"GameFramework/RotatingMovementComponent.h\"\n    ```\n\n15.  在构造函数中，初始化`Static Mesh`组件，使其与所有组件重叠，重叠时调用`OnBeginOverlap`函数:\n\n    ```cpp\n    Mesh = CreateDefaultSubobject<UStaticMeshComponent>(\"Mesh\");\n    Mesh->SetCollisionProfileName(\"OverlapAll\");\n    RootComponent = Mesh;\n    ```\n\n16.  仍然在构造器中，初始化旋转运动组件以在`Yaw`轴上每秒旋转`90`度:\n\n    ```cpp\n    RotatingMovement = CreateDefaultSubobject   <URotatingMovementComponent>(\"Rotating Movement\");\n    RotatingMovement->RotationRate = FRotator(0.0, 90.0f, 0);\n    ```\n\n17.  要最终确定构造函数，请启用复制并禁用`Tick`功能:\n\n    ```cpp\n    bReplicates = true;\n    PrimaryActorTick.bCanEverTick = false;\n    ```\n\n18.  实现`BeginPlay`功能，将开始重叠事件绑定到`OnBeginOverlap`功能:\n\n    ```cpp\n    void APickup::BeginPlay()\n    {\n      Super::BeginPlay();\n      Mesh->OnComponentBeginOverlap.AddDynamic(this,     &APickup::OnBeginOverlap);\n    }\n    ```\n\n19.  Implement the `OnBeginOverlap` function, which checks whether the character is valid and has authority, removes the pickup on the game state, plays the pickup sound on the owning client, adds `10` points and the pickup to the character. Once all of that is done, the pickup destroys itself.\n\n    ```cpp\n    void APickup::OnBeginOverlap(UPrimitiveComponent* OverlappedComp,   AActor* OtherActor, UPrimitiveComponent* OtherComp, int32   OtherBodyIndex, bool bFromSweep, const FHitResult& Hit)\n    {\n      APickupsCharacter* Character =     Cast<APickupsCharacter>(OtherActor);\n      if (Character == nullptr || !HasAuthority())\n      {\n        return;\n      }\n      APickupsGameState* GameState =     Cast<APickupsGameState>(GetWorld()->GetGameState());\n      if (GameState != nullptr)\n      {\n        GameState->RemovePickup();\n      }\n      Character->ClientPlaySound2D(PickupSound);\n      Character->AddScore(10);\n      Character->AddPickup();\n      Destroy();\n    }\n    ```\n\n    接下来，我们将学习`PickupsGameState`课。\n\n20.  打开`PickupsGameState.h`并声明受保护的复制整数变量`PickupsRemaining`，该变量告诉所有客户端还有多少皮卡在该级别:\n\n    ```cpp\n    UPROPERTY(Replicated, BlueprintReadOnly)\n    int32 PickupsRemaining;\n    ```\n\n21.  宣布`BeginPlay`功能的保护覆盖:\n\n    ```cpp\n    virtual void BeginPlay() override;\n    ```\n\n22.  声明受保护的`GetPlayerStatesOrderedByScore`功能:\n\n    ```cpp\n    UFUNCTION(BlueprintCallable)\n    TArray<APlayerState*> GetPlayerStatesOrderedByScore() const;\n    ```\n\n23.  实现公共`RemovePickup`功能，从`PickupsRemaining`变量\n\n    ```cpp\n    void RemovePickup() { PickupsRemaining--; }\n    ```\n\n    中删除一个皮卡\n24.  执行公共`HasPickups`功能，返回是否还有皮卡剩余:\n\n    ```cpp\n    bool HasPickups() const { return PickupsRemaining > 0; }\n    ```\n\n25.  打开`PickupsGameState.cpp`，包括`Pickup.h`、`GameplayStatics.h`、`UnrealNetwork.h`、【T4:\n\n    ```cpp\n    #include \"Pickup.h\"\n    #include \"Kismet/GameplayStatics.h\"\n    #include \"Net/UnrealNetwork.h\"\n    #include \"GameFramework/PlayerState.h\"\n    ```\n\n26.  实现`GetLifetimeReplicatedProps`功能，使`PickupRemaining`变量复制到所有客户端:\n\n    ```cpp\n    void APickupsGameState::GetLifetimeReplicatedProps(TArray<   FLifetimeProperty >& OutLifetimeProps) const\n    {\n      Super::GetLifetimeReplicatedProps(OutLifetimeProps);\n      DOREPLIFETIME(APickupsGameState, PickupsRemaining);\n    }\n    ```\n\n27.  通过获取世界上所有的皮卡:\n\n    ```cpp\n    void APickupsGameState::BeginPlay()\n    {\n      Super::BeginPlay();\n      TArray<AActor*> Pickups;\n      UGameplayStatics::GetAllActorsOfClass(this,     APickup::StaticClass(), Pickups);\n      PickupsRemaining = Pickups.Num();\n    }\n    ```\n\n    ，实现`BeginPlay`覆盖功能并设置`PickupsRemaining`的值\n28.  Implement the `GetPlayerStatesOrderedByScore` function, which duplicates the `PlayerArray` variable and sorts it so that the players with the highest scores show up first:\n\n    ```cpp\n    TArray<APlayerState*> APickupsGameState::GetPlayerStatesOrderedByScore() const\n    {\n      TArray<APlayerState*> PlayerStates(PlayerArray);\n      PlayerStates.Sort([](const APlayerState& A, const APlayerState&     B) { return A.Score > B.Score; });\n      return PlayerStates;\n    }\n    ```\n\n    接下来，我们来学习`PickupsPlayerState`课。\n\n29.  打开`PickupsPlayerState.h`，声明受保护的复制整型变量`Pickups`，表示一个玩家收集了多少个皮卡:\n\n    ```cpp\n    UPROPERTY(Replicated, BlueprintReadOnly)\n    int32 Pickups;\n    ```\n\n30.  实现公共`AddPickup`功能，为`Pickups`变量\n\n    ```cpp\n    void AddPickup() { Pickups++ ; }\n    ```\n\n    增加一个拾音器\n31.  打开`PickupsPlayerState.cpp`并包含`UnrealNetwork.h` :\n\n    ```cpp\n    #include \"Net/UnrealNetwork.h\"\n    ```\n\n32.  Implement the `GetLifetimeReplicatedProps` function and make the `Pickups` variable replicate to all clients:\n\n    ```cpp\n    void APickupsPlayerState::GetLifetimeReplicatedProps(TArray<   FLifetimeProperty >& OutLifetimeProps) const\n    {\n      Super::GetLifetimeReplicatedProps(OutLifetimeProps);\n      DOREPLIFETIME(APickupsPlayerState, Pickups);\n    }\n    ```\n\n    接下来，我们来学习`PickupsPlayerController`课。\n\n33.  打开`PickupsPlayerController.h`并声明受保护的`ScoreboardMenuClass`变量，这使得我们想要用于记分板的 UMG 小部件能够被选择:\n\n    ```cpp\n    UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = \"Pickup   Player Controller\")\n    TSubclassOf<class UUserWidget> ScoreboardMenuClass;\n    ```\n\n34.  声明受保护的`ScoreboardMenu`变量，该变量存储我们在`BeginPlay`函数变量上创建的记分板 UMG 小部件实例:\n\n    ```cpp\n    UPROPERTY()\n    class UUserWidget* ScoreboardMenu;\n    ```\n\n35.  宣布`BeginPlay`功能的保护覆盖:\n\n    ```cpp\n    virtual void BeginPlay() override;\n    ```\n\n36.  打开`PickupsPlayerController.cpp`并包含`UserWidget.h` :\n\n    ```cpp\n    #include \"Blueprint/UserWidget.h\"\n    ```\n\n37.  Implement the `BeginPlay` override function, which, for the owning client, creates and adds the scoreboard UMG widget to the viewport:\n\n    ```cpp\n    void APickupsPlayerController::BeginPlay()\n    {\n      Super::BeginPlay();\n      if (!IsLocalController() || ScoreboardMenuClass == nullptr)\n      {\n        return;\n      }\n      ScoreboardMenu = CreateWidget<UUserWidget>(this,     ScoreboardMenuClass);\n      if (ScoreboardMenu != nullptr)\n      {\n        ScoreboardMenu->AddToViewport(0);\n      }\n    }\n    ```\n\n    现在，让我们编辑`PickupsGameMode`类:\n\n38.  打开`PickupsGameMode.h`，将`GameModeBase.h`的`include`替换为`GameMode.h` :\n\n    ```cpp\n    #include \"GameFramework/GameMode.h\"\n    ```\n\n39.  使类派生自`AGameMode`而不是`AGameModeBase` :\n\n    ```cpp\n    class APickupsGameMode : public AGameMode\n    ```\n\n40.  声明受保护的游戏状态变量`MyGameState`，它将实例保存到`APickupsGameState`类:\n\n    ```cpp\n    UPROPERTY()\n    class APickupsGameState* MyGameState;\n    ```\n\n41.  将构造函数移动到受保护区域。\n42.  宣布`BeginPlay`功能的保护覆盖:\n\n    ```cpp\n    virtual void BeginPlay() override;\n    ```\n\n43.  宣布`ShouldSpawnAtStartSpot`功能的保护覆盖:\n\n    ```cpp\n    virtual bool ShouldSpawnAtStartSpot(AController* Player)   override;\n    ```\n\n44.  声明游戏模式匹配状态功能的受保护覆盖:\n\n    ```cpp\n    virtual void HandleMatchHasStarted() override;\n    virtual void HandleMatchHasEnded() override;\n    virtual bool ReadyToStartMatch_Implementation() override;\n    virtual bool ReadyToEndMatch_Implementation() override;\n    ```\n\n45.  声明受保护的`RestartMap`功能:\n\n    ```cpp\n    void RestartMap();\n    ```\n\n46.  打开`PickupsGameMode.cpp`，包括`GameplayStatics.h`、`PickupGameState.h`、`Engine/World.h`、`TimerManager.h`、`Engine.h` :\n\n    ```cpp\n    #include \"Kismet/GameplayStatics.h\"\n    #include \"PickupsGameState.h\"\n    #include \"Engine/World.h\"\n    #include \"Engine/Public/TimerManager.h\"\n    #include \"Engine/Engine.h\"\n    ```\n\n47.  实现`BeginPlay`覆盖功能，存储`APickupGameState`实例:\n\n    ```cpp\n    void APickupsGameMode::BeginPlay()\n    {\n      Super::BeginPlay();\n      MyGameState = GetGameState<APickupsGameState>();\n    }\n    ```\n\n48.  实现`ShouldSpawnAtStartSpot`覆盖功能，这表明我们希望玩家在随机玩家开始时重生，而不是总是在同一个玩家身上重生:\n\n    ```cpp\n    bool APickupsGameMode::ShouldSpawnAtStartSpot   (AController* Player)\n    {\n      return false;\n    }\n    ```\n\n49.  实现`HandleMatchHasStarted`超控功能，打印到屏幕上，通知玩家游戏已经开始:\n\n    ```cpp\n    void APickupsGameMode::HandleMatchHasStarted()\n    {\n      Super::HandleMatchHasStarted();\n      GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Green, \"The     game has started!\");\n    }\n    ```\n\n50.  实现`HandleMatchHasEnded`覆盖功能，打印到屏幕上，通知玩家游戏已经结束，销毁所有角色，并安排计时器重启地图:\n\n    ```cpp\n    void APickupsGameMode::HandleMatchHasEnded()\n    {\n      Super::HandleMatchHasEnded();\n      GEngine->AddOnScreenDebugMessage(-1, 2.0f, FColor::Red, \"The     game has ended!\");\n      TArray<AActor*> Characters;\n        UGameplayStatics::GetAllActorsOfClass(this,     APickupsCharacter::StaticClass(), Characters);\n      for (AActor* Character : Characters)\n      {\n        Character->Destroy();\n      }\n      FTimerHandle TimerHandle;\n      GetWorldTimerManager().SetTimer(TimerHandle, this,     &APickupsGameMode::RestartMap, 5.0f);\n    }\n    ```\n\n51.  执行`ReadyToStartMatch_Implementation`超越功能，表示比赛可以直接开始:\n\n    ```cpp\n    bool APickupsGameMode::ReadyToStartMatch_Implementation()\n    {\n      return true;\n    }\n    ```\n\n52.  执行`ReadyToEndMatch_Implementation`超控功能，当游戏状态没有剩余拾取时，表示比赛结束:\n\n    ```cpp\n    bool APickupsGameMode::ReadyToEndMatch_Implementation()\n    {\n      return MyGameState != nullptr && !MyGameState->HasPickups();\n    }\n    ```\n\n53.  Implement the `RestartMap` function, which indicates that the server travels to the same level and brings all clients along (*only in the packaged version*):\n\n    ```cpp\n    void APickupsGameMode::RestartMap()\n    {\n      GetWorld()->ServerTravel(GetWorld()->GetName(), false, false);\n    }\n    ```\n\n    现在，让我们编辑`PickupsCharacter`类。\n\n54.  打开`PickupsCharacter.h`并声明下降着陆保护声音变量:\n\n    ```cpp\n    UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category =   \"Pickups Character\")\n    USoundBase* FallSound;\n    UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category =   \"Pickups Character\")\n    USoundBase* LandSound;\n    ```\n\n55.  声明受保护的`override`功能:\n\n    ```cpp\n    virtual void EndPlay(const EEndPlayReason::Type EndPlayReason)   override;\n    virtual void Landed(const FHitResult& Hit) override;\n    virtual void FellOutOfWorld(const UDamageType& DmgType) override;\n    ```\n\n56.  声明给玩家状态增加分数和拾取的公共功能:\n\n    ```cpp\n    void AddScore(const float Score);\n    void AddPickup();\n    ```\n\n57.  声明在所属客户端播放声音的公共客户端 RPC:\n\n    ```cpp\n    UFUNCTION(Client, Unreliable)\n    void ClientPlaySound2D(USoundBase* Sound);\n    ```\n\n58.  打开`PickupsCharacter.cpp`，包括`PickupsPlayerState.h`、`GameMode.h`、`GameplayStatics.h` :\n\n    ```cpp\n    #include \"PickupsPlayerState.h\"\n    #include \"GameFramework/GameMode.h\"\n    #include \"Kismet/GameplayStatics.h\"\n    ```\n\n59.  执行`EndPlay`覆盖功能，如果角色被破坏，播放坠落声:\n\n    ```cpp\n    void APickupsCharacter::EndPlay(const EEndPlayReason::Type   EndPlayReason)\n    {\n      Super::EndPlay(EndPlayReason);\n      if (EndPlayReason == EEndPlayReason::Destroyed)\n      {\n        UGameplayStatics::PlaySound2D(GetWorld(), FallSound);\n      }\n    }\n    ```\n\n60.  执行`Landed`超越功能，播放落地声音:\n\n    ```cpp\n    void APickupsCharacter::Landed(const FHitResult& Hit)\n    {\n      Super::Landed(Hit);\n      UGameplayStatics::PlaySound2D(GetWorld(), LandSound);\n    }\n    ```\n\n61.  实现`FellOutOfWorld`覆盖功能，存储控制器，从分数中移除`10`点，破坏角色(使控制器无效)，并告知游戏模式使用之前的控制器重启玩家:\n\n    ```cpp\n    void APickupsCharacter::FellOutOfWorld(const UDamageType&   DmgType)\n    {\n      AController* PreviousController = Controller;\n      AddScore(-10);\n      Destroy();\n      AGameMode* GameMode = GetWorld()->GetAuthGameMode<AGameMode>();\n      if (GameMode != nullptr)\n      {\n        GameMode->RestartPlayer(PreviousController);\n      }\n    }\n    ```\n\n62.  实现`AddScore`功能，在玩家状态\n\n    ```cpp\n    void APickupsCharacter::AddScore(const float Score)\n    {\n      APlayerState* MyPlayerState = GetPlayerState();\n      if (MyPlayerState != nullptr)\n      {\n        MyPlayerState->Score += Score;\n      }\n    }\n    ```\n\n    下给`Score`变量加一个分数\n63.  实现`AddPickup`功能，在我们的自定义玩家状态\n\n    ```cpp\n    void APickupsCharacter::AddPickup()\n    {\n      APickupsPlayerState* MyPlayerState =     GetPlayerState<APickupsPlayerState>();\n      if (MyPlayerState != nullptr)\n      {\n        MyPlayerState->AddPickup();\n      }\n    }\n    ```\n\n    中为`Pickup`变量添加一个拾取\n64.  实现`ClientPlaySound2D_Implementation`功能，在所属客户端播放声音:\n\n    ```cpp\n    void APickupsCharacter::ClientPlaySound2D_Implementation(USoundBase*   Sound)\n    {\n      UGameplayStatics::PlaySound2D(GetWorld(), Sound);\n    }\n    ```\n\n65.  Open `Pickups.Build.cs` and add the `UMG` module to `PublicDependencyModuleNames`, like so:\n\n    ```cpp\n    PublicDependencyModuleNames.AddRange(new string[] { \"Core\",   \"CoreUObject\", \"Engine\", \"InputCore\", \"HeadMountedDisplay\",   \"UMG\" });\n    ```\n\n    如果您试图编译并从添加新模块中获得错误，那么请清理并重新编译您的项目。如果这不起作用，请尝试重新启动您的 IDE。\n\n66.  Compile and run the code until the editor loads.\n\n    首先，让我们导入声音文件。\n\n67.  在`Content Browser`中，创建并转到`Content\\Sounds`文件夹。\n68.  从`Exercise18.02\\Assets`文件夹导入 `Pickup.wav`、`Footstep.wav`、`Jump.wav`、`Land.wav`和`Fall.wav`。\n69.  Save the new files.\n\n    接下来，让我们将`Play Sound`动画通知添加到角色的一些动画中。\n\n70.  打开位于`Content\\Mannequin\\Animations`的`ThirdPersonJump_Start animation`，使用`Jump`声音在`0`框添加一个`Play Sound`动画通知。\n71.  保存并关闭`ThirdPersonJump_Start`。\n72.  打开位于`Content\\Mannequin\\Animations`的`ThirdPersonRun`动画，在时间 0.24 秒和 0.56 秒添加两个`Play Sound`动画通知。\n73.  保存并关闭`ThirdPersonRun`。\n74.  打开位于`Content\\Mannequin\\Animations`的`ThirdPersonWalk`动画，在时间 0.24 秒和 0.79 秒添加两个`Play Sound`动画通知。\n75.  Save and close `ThirdPersonWalk`.\n\n    现在，让我们为角色蓝图设置声音。\n\n76.  打开位于`Content\\ThirdPersonCPP\\Blueprints`的`ThirdPersonCharacter`蓝图，设置`Fall` `Sound`和`Land` `Sound`分别使用声音`Fall`和`Land`。\n77.  Save and close `ThirdPersonCharacter`.\n\n    现在，让我们创建皮卡的蓝图。\n\n78.  创建并打开`Content\\Blueprints`文件夹。\n79.  创建一个从`Pickup`类派生的名为`BP_Pickup`的新蓝图，并将其打开。\n80.  Configure the `Static Mesh` component in the following way:\n\n    ```cpp\n    Scale = 0.5, 0.5, 0.5\t\n    Static Mesh = Engine\\BasicShapes\\Cube\n    Material Element 0 = CubeMaterial\n    ```\n\n    注意\n\n    要显示引擎内容，您需要转到静态网格下拉菜单右下角的“查看选项”，并确保“显示引擎内容”标志设置为真。\n\n81.  将`Pickup` `Sound`变量设置为使用`Pickup`声音。\n82.  Save and close `BP_Pickup`.\n\n    接下来，让我们创建记分板 UMG 小部件。\n\n83.  创建并转到`Content\\UI`文件夹。\n84.  创建名为`UI_Scoreboard_Header`的新小部件蓝图:\n    *   在根画布面板上添加名为`tbName`的文本块，其中`Is Variable`设置为`true`、`Size To Content`设置为`true`、`Text`设置为`Player Name`、`Color and Opacity`设置为使用颜色`green`。\n    *   在根画布面板上添加名为`tbScore`的文本块，其中`Is Variable`设置为`true`、`Position X = 500`、`Alignment = 1.0, 0.0`、`Size To Content`设置为`true`、`Text`设置为`Score`，而`Color and Opacity`设置为使用颜色`green`。\n    *   在根画布面板上添加名为`tbPickups`的文本块，其中`Is Variable`设置为`true`、`Position X = 650`、`Alignment = 1.0, 0.0`、`Size To Content`设置为`true`、`Text`设置为`Pickups`，而`Color and Opacity`设置为使用颜色`green`。\n85.  从`Hierarchy`面板中，选择三个新的文本块并复制它们。\n86.  保存并关闭`UI_Scoreboard_Header`。\n87.  回到`Content\\UI`，新建一个名为`UI_Scoreboard_Entry`的 UMG 小部件，打开它。\n88.  将复制的文本块粘贴在根画布面板上，改为`white`而不是`green`，并使它们都是变量。\n89.  Go to the `Graph` section and create the `Player State` variable with the following configuration:\n\n    ![Figure 18.3: Creating the Player State variable ](img/B16183_18_03.jpg)\n\n    图 18.3:创建玩家状态变量\n\n90.  Go back to the Designer section and create a bind for `tbName` that does the following:\n\n    ![Figure 18.4: Displaying the player name ](img/B16183_18_04.jpg)\n\n    图 18.4:显示玩家姓名\n\n    注意\n\n    您可以在以下链接找到前面的全分辨率截图，以便更好地查看:[https://packt.live/3pCk9Nt](https://packt.live/3pCk9Nt)。\n\n91.  Create a bind for `tbScore` that does the following:\n\n    ![Figure 18.5: Displaying the player score ](img/B16183_18_05.jpg)\n\n    图 18.5:显示玩家得分\n\n    注意\n\n    您可以在以下链接找到前面的全分辨率截图，以便更好地查看:[https://packt.live/3nuckYv](https://packt.live/3nuckYv)。\n\n92.  Create a bind for `tbPickups` that does the following:\n\n    ![Figure 18.6: Displaying the pickups count ](img/B16183_18_06.jpg)\n\n    图 18.6:显示提货数量\n\n    注意\n\n    您可以在以下链接找到前面的全分辨率截图，以便更好地查看:[https://packt.live/36pEGMz](https://packt.live/36pEGMz)。\n\n93.  Create a pure function called `Get Typeface` that does the following:\n\n    ![Figure 18.7: Determining whether the entry should be displayed in bold or regular ](img/B16183_18_07.jpg)\n\n    图 18.7:确定条目应该以粗体还是常规显示\n\n    注意\n\n    您可以在以下链接找到前面的全分辨率截图，以便更好地查看:[https://packt.live/2JW9Zam](https://packt.live/2JW9Zam)。\n\n    在前面的代码中，我们使用了一个选择节点，它可以通过从返回值中拖动一根线并将其释放到空白空间来创建，并在过滤器上键入“select”。从那里，我们从列表中选择选择节点。在这个特定的函数中，我们使用选择节点来选择我们将要使用的字体的名称，所以如果玩家状态的棋子与拥有小部件的棋子不同，它应该返回`Regular`，如果是，则返回`Bold`。我们这样做是为了用粗体突出显示玩家状态条目，以便玩家知道他们的条目是什么。\n\n94.  Implement `Event Construct` in the following way:\n\n    ![Figure 18.8: The Event Graph that sets the text for the name, score, and pickups count ](img/B16183_18_08.jpg)\n\n    图 18.8:为名称、分数和提货数量设置文本的事件图\n\n    注意\n\n    您可以在以下链接找到前面的全分辨率截图，以便更好地查看:[https://packt.live/2JOdP58](https://packt.live/2JOdP58)。\n\n    在前面的代码中，我们设置了`tbName`、`tbScore`和`tbPickups`的字体来使用`Bold`字体来突出显示哪个记分板条目相对于当前客户端的玩家。对于其余的玩家，使用`Regular`字样。\n\n95.  保存并关闭`UI_Scoreboard_Entry`。\n96.  回到`Content\\UI`然后创建一个名为`UI_Scoreboard`的新 UMG 小部件并打开它。\n97.  在根画布面板上添加一个名为`vbScoreboard`的垂直框，并启用`Size To Content`。\n98.  向名为`tbGameInfo`的`vbScoreboard`添加一个文本块，其`Text`值默认为`Game Info`。\n99.  转到`Graph`部分，创建一个名为`Pickups Game State`类型的新变量`Game State`。\n100.  Implement `Event Construct` in the following way:\n\n    ![Figure 18.9: The Event Construct that sets a timer to update  the scoreboard every 0.5 seconds ](img/B16183_18_09.jpg)\n\n    图 18.9:设置计时器每 0.5 秒更新一次记分板的事件构造\n\n    注意\n\n    您可以在以下链接找到前面的全分辨率截图，以便更好地查看:[https://packt.live/3kemyu0](https://packt.live/3kemyu0)。\n\n    在前面的代码中，我们获取了游戏状态实例，更新了记分板，并安排了一个计时器每 0.5 秒自动更新一次记分板。\n\n101.  Go back to the designer section and make the following bind for `vbScoreboard`:\n\n    ![Figure 18.10: Displaying the number of pickups remaining in the world ](img/B16183_18_10.jpg)\n\n    图 18.10:显示世界上剩余的皮卡数量\n\n    注意\n\n    您可以在以下链接找到前面的全分辨率截图，以便更好地查看:[https://packt.live/38xUDTE](https://packt.live/38xUDTE)。\n\n102.  Add a vertical box to `vbScoreboard` called `vbPlayerStates` with `Is Variable` set to `true` and a top padding of `50`, so you should have the following:\n\n    ![Figure 18.11: The UI_Scoreboard widget hierarchy ](img/B16183_18_11.jpg)\n\n    图 18.11:用户界面记分板小部件层次结构\n\n103.  Go back to the Graph section and implement the `Update Scoreboard` event in the following way:\n\n    ![Figure 18.12: The update scoreboard function, which clears and recreates the entry widgets ](img/B16183_18_12.jpg)\n\n    图 18.12:更新记分板功能，清除并重新创建条目小部件\n\n    注意\n\n    您可以在以下链接找到前面的全分辨率截图，以便更好地查看:[https://packt.live/3pf8EeN](https://packt.live/3pf8EeN)。\n\n    在前面的代码中，我们执行了以下操作:\n\n    *   清除`vbPlayerStates`中所有先前的条目。\n    *   创建记分板标题条目并将其添加到`vbPlayerStates`。\n    *   循环遍历所有按分数排序的玩家状态，为每个状态创建一个条目，并将其添加到`vbPlayerStates`中。\n104.  Save and close `UI_Scoreboard`.\n\n    现在，让我们为玩家控制器创建蓝图。\n\n105.  转到`Content\\Blueprints`并创建一个名为`BP_PlayerController`的新蓝图，该蓝图源自`PickupPlayerController`类。\n106.  打开新蓝图，设置`Scoreboard Menu` `Class`使用`UI_Scoreboard`。\n107.  Save and close `BP_PlayerController`.\n\n    接下来，让我们为游戏模式创建蓝图。\n\n108.  Go to `Content\\Blueprints` and create a new blueprint called `BP_GameMode` that is derived from the `PickupGameMode` class, open it, and change the following variables:\n\n    ```cpp\n    Game State Class = PickupsGameState\n    Player Controller Class = BP_PlayerController\n    Player State Class = PickupsPlayerState\n    ```\n\n    接下来，让我们配置`Project Settings`使用新的游戏模式。\n\n109.  转到`Project Settings`，从左侧面板中选择`Maps & Modes`，在`Project`类别中。\n110.  设置`Default GameMode`使用`BP_GameMode`。\n111.  Close `Project Settings`.\n\n    现在，让我们修改主级别。\n\n112.  确保你已经打开了`ThirdPersonExampleMap`，位于`Content\\ThirdPersonCPP\\Maps`。\n113.  添加一些立方体演员作为平台，并确保他们之间有间隙，以迫使玩家跳到他们身上，并可能从水平下降。\n114.  在地图的不同部分添加几个玩家开始的演员。\n115.  添加至少 50 个`BP_Pickup`实例，并将它们分布在整个地图上。\n116.  Here is an example of a possible way of configuring the map:\n\n    ![Figure 18.13: An example of the map configuration ](img/B16183_18_13.jpg)\n\n    图 18.13:地图配置示例\n\n117.  运行代码，等待编辑器完全加载。\n118.  转到`Multiplayer Options`，将客户端数量设置为`2`。\n119.  将窗口大小设置为`800x600`。\n120.  在`New Editor Window (PIE)`中播放:\n\n![Figure 18.14: The listen Server and Client 1 picking up cubes in the world ](img/B16183_18_14.jpg)\n\n图 18.14:监听服务器和客户端 1 拾取世界上的立方体\n\n一旦你完成了这个练习，你将能够在每个客户端上玩，你会注意到角色可以收集拾音器，并通过与它们重叠来获得`10`点。如果一个角色从关卡中掉落，它将在随机玩家开始时重生，并失去`10`点。\n\n一旦所有皮卡都被收集完毕，游戏将结束，在`5`秒后，它将执行一次服务器旅行来重新加载相同的等级，并将所有客户端都带上(*仅在打包版本*中)。您还可以看到，用户界面显示了关卡中还有多少拾取器，以及记分板，其中包含每个玩家的姓名、分数和拾取器的信息。\n\n## 活动 18.01:在多人 FPS 游戏中增加死亡、重生、记分牌、击杀限制和拾取\n\n在本活动中，您将为角色添加死亡、重生的概念以及使用拾音器的能力。我们还将添加一种检查记分板的方法和游戏的致命限制，以便它有一个最终目标。\n\n以下步骤将帮助您完成本活动:\n\n1.  从*活动 17.01* 、*打开`MultiplayerFPS`项目，为多人 FPS 游戏*添加武器和弹药。编译代码并运行编辑器。\n2.  接下来，您将创建我们需要的 C++ 类。创建一个名为`FPSGameState`的 C++ 类，该类从`GameState`类派生而来，有一个 kill limit 变量和一个返回 kill 排序的玩家状态的函数。\n3.  创建一个名为`FPSPlayerState`的 C++ 类，这个类是从`PlayerState`类派生出来的，存储一个玩家的击杀次数和死亡次数。\n4.  创建一个名为`PlayerMenu`的 C++ 类，这个类是从`UserWidget`类派生出来的，有一些`BlueprintImplementableEvent`功能可以切换记分板可见性，设置记分板可见性，当有玩家被杀时通知。\n5.  创建一个名为`FPSPlayerController`的 C++ 类，它从`APlayerController`派生而来，在拥有的客户端上创建`PlayerMenu` UMG 小部件实例。\n6.  创建一个名为`Pickup`的 C++ 类，它是从`Actor`类派生出来的，有一个在`Yaw`轴上每秒旋转 90 度的静态网格，玩家可以在重叠处拾取。一旦被拾取，它将播放拾取声音，并禁用碰撞和可见性。一定时间后，它会变得可见，并能够再次碰撞。\n7.  创建一个名为`AmmoPickup`的 C++ 类，这个类是从`Pickup`类派生出来的，给玩家增加一定量的一种弹药类型。\n8.  创建一个名为`ArmorPickup`的 C++ 类，由`Pickup`类派生而来，给玩家增加一定的护甲。\n9.  创建一个名为`HealthPickup`的 C++ 类，由`Pickup`类派生而来，为玩家增加一定的生命值。\n10.  创建一个名为`WeaponPickup`的 C++ 类，由`Pickup`类派生而来，给玩家增加一定的武器类型。如果玩家已经拥有武器，它会增加一定数量的弹药。\n11.  编辑`FPSCharacter`类，使其执行以下操作:\n    *   角色受损后，会检查是否死亡。如果它死了，它会记录杀手角色的杀戮和角色的死亡，以及玩家的重生。如果角色没有死，它会在拥有它的客户身上播放痛苦的声音。\n    *   当角色死亡并执行`EndPlay`功能时，应该会摧毁其所有武器实例。\n    *   如果角色从世界上掉落，它会记录玩家的死亡并重生。\n    *   如果玩家按下*标签*键，将切换记分板菜单的可见性。\n12.  编辑`MultiplayerFPSGameModeBase`类，使其执行以下操作:\n    *   存储赢得游戏所需的击杀次数。\n    *   使用新的玩家控制器、玩家状态和游戏状态类。\n    *   使其实现匹配状态功能，以便匹配立即开始，如果有玩家拥有所需的击杀次数，则匹配结束。\n    *   当比赛结束时，它将在 5 秒钟后执行服务器到同一级别的旅行。\n    *   通过将杀死(当被另一个玩家杀死时)和死亡添加到各自的玩家状态中来处理玩家死亡的情况，以及在玩家随机开始时对该玩家进行重生。\n13.  从`Activity18.01\\Assets`进口`AmmoPickup.wav`到`Content\\Pickups\\Ammo`。\n14.  从`AAmmoPickup`创建`BP_PistolBullets_Pickup`，将其放入`Content\\Pickups\\Ammo`，并用以下值配置:\n    *   比例尺:`(X=0.5, Y=0.5, Z=0.5)`\n    *   静态网格:`Engine\\BasicShapes\\Cube`\n    *   材料:`Content\\Weapon\\Pistol\\M_Pistol`\n    *   弹药类型:`Pistol Bullets`，弹药数量:`25`\n    *   拾音:`Content\\Pickup\\Ammo\\AmmoPickup`\n15.  从`AAmmoPickup`创建`BP_MachineGunBullets_Pickup`，将其放入`Content\\Pickups\\Ammo`，并用以下值配置:\n    *   比例尺:`(X=0.5, Y=0.5, Z=0.5)`\n    *   静态网格:`Engine\\BasicShapes\\Cube`\n    *   材料:`Content\\Weapon\\MachineGun\\M_MachineGun`\n    *   弹药类型:`Machine Gun Bullets`，弹药数量:`50`\n    *   拾音:`Content\\Pickup\\Ammo\\AmmoPickup`\n16.  从`AAmmoPickup`创建`BP_Slugs_Pickup`，将其放入`Content\\Pickups\\Ammo`，并用以下值配置:\n    *   比例尺:`(X=0.5, Y=0.5, Z=0.5)`\n    *   静态网格:`Engine\\BasicShapes\\Cube`\n    *   材料:`Content\\Weapon\\Railgun\\M_Railgun`\n    *   弹药类型:`Slugs`，弹药数量:`5`\n    *   拾音:`Content\\Pickup\\Ammo\\AmmoPickup`\n17.  从`Activity18.01\\Assets`进口`ArmorPickup.wav`到`Content\\Pickups\\Armor`。\n18.  在`Content\\Pickups\\Armor`中创建素材`M_Armor`，将`Base Color`设置为`blue`，将`Metallic`设置为`1`。\n19.  从`AArmorPickup`创建`BP_Armor_Pickup`，将其放入`Content\\Pickups\\Armor`，并用以下值配置:\n    *   比例尺:`(X=1.0, Y=1.5, Z=1.0)`\n    *   静态网格:`Engine\\BasicShapes\\Cube`\n    *   材料:`Content\\Pickup\\Armor\\M_Armor`\n    *   护甲数量:`50`\n    *   拾音:`Content\\Pickup\\Armor\\ArmorPickup`\n20.  从`Activity18.01\\Assets`进口`HealthPickup.wav`到 `Content\\Pickups\\Health`。\n21.  在`Content\\Pickups\\Health`中创建素材`M_Health`，将`Base Color`设置为`blue`，将`Metallic` / `Roughness`设置为`0.5`。\n22.  从`AHealthPickup`创建`BP_Health_Pickup`，将其放入`Content\\Pickups\\Health`，并用以下值配置:\n    *   静态网格:`Engine\\BasicShapes\\Sphere`\n    *   材料:`Content\\Pickup\\Health\\M_Health`\n    *   健康金额:`50`\n    *   拾音:`Content\\Pickup\\Health\\HealthPickup`\n23.  从`Activity18.01\\Assets`进口`WeaponPickup.wav`到 `Content\\Pickups\\Weapon`。\n24.  从`AWeaponPickup`创建`BP_Pistol_Pickup`，将其放入`Content\\Pickups\\Weapon`，并用以下值配置:\n    *   静态网格:`Content\\Pickup\\Weapon\\SM_Weapon`\n    *   材料:`Content\\Weapon\\Pistol\\M_Pistol`\n    *   武器类型:`Pistol`，弹药量:`25`\n    *   拾音:`Content\\Pickup\\Weapon\\WeaponPickup`\n25.  从`AWeaponPickup`创建`BP_MachineGun_Pickup`，将其放入`Content\\Pickups\\Weapon`，并用以下值配置:\n    *   静态网格:`Content\\Pickup\\Weapon\\SM_Weapon`\n    *   材料:`Content\\Weapon\\MachineGun\\M_MachineGun`\n    *   武器类型:`Machine Gun`，弹药量:`50`\n    *   拾音:`Content\\Pickup\\Weapon\\WeaponPickup`\n26.  从`AWeaponPickup`创建`BP_Pistol_Pickup`，将其放入`Content\\Pickups\\Weapon`，并用以下值配置:\n    *   静态网格:`Content\\Pickup\\Weapon\\SM_Weapon`\n    *   材料:`Content\\Weapon\\Railgun\\M_Railgun`\n    *   武器类型:`Railgun`，弹药量:`5`\n    *   拾音:`Content\\Pickup\\Weapon\\WeaponPickup`\n27.  从`Activity18.01\\Assets`进口`Land.wav`和`Pain.wav`到 `Content\\Player\\Sounds`。\n28.  编辑`BP_Player`使其使用`Pain`和`Land`声音，并删除所有在`Begin Play`事件中创建`UI_HUD`实例并将其添加到视口的节点。\n29.  在`Content\\UI`中创建一个名为`UI_Scoreboard_Entry`的 UMG 部件，显示`AFPSPlayerState`的名称、死亡人数和等级。\n30.  创建一个名为`UI_Scoreboard_Header`的 UMG 小部件，显示名称、死亡数和 ping 的标题。\n31.  创建一个名为`UI_Scoreboard`的 UMG 小部件，显示游戏状态下的击杀限制，一个以`UI_Scoreboard_Header`为第一个条目的垂直框，然后在游戏状态实例中为每个`AFPSPlayerState`添加一个`UI_Scoreboard_Entry`。垂直框将通过计时器每 0.5 秒更新一次，方法是清除其子框并再次添加它们。\n32.  编辑`UI_HUD`使其添加一个名为`tbKilled`的新文本块，该文本块以设置为`Hidden`的`Visibility`开始。当玩家杀人时，会使文本块可见，显示被杀玩家的名字，1 秒后隐藏。\n33.  从`UPlayerMenu`创建一个名为`UI_PlayerMenu`的新蓝图，并将其放置在`Content\\UI`中。使用带有索引`0`中的`UI_HUD`实例和索引`1`中的`UI_Scoreboard`实例的小部件切换器。在事件图中，确保覆盖在 C++ 中被设置为`BlueprintImplementableEvent`的`Toggle Scoreboard`、`Set Scoreboard Visibility`和`Notify Kill`事件。`Toggle Scoreboard`事件在`0`和`1`之间切换小部件切换器的活动索引，`Set Scoreboard Visibility`事件将小部件切换器的活动索引设置为`0`或`1`，`Notify Kill`事件告诉`UI_HUD`实例设置文本并淡出动画。\n34.  从`AFPSPlayerController`创建`BP_PlayerController`，放入`Content`文件夹，设置`PlayerMenuClass`变量使用`UI_PlayerMenu`。\n35.  编辑`BP_GameMode`并设置`Player Controller Class`使用`BP_PlayerController`。\n36.  在`Project Settings`的`Input`部分，使用`TAB`键创建一个名为`Scoreboard`的动作映射。\n37.  Edit the `DM-Test` level so that you have at least three new player starts placed in different locations, `Kill Z` to `-500` in `World Settings`, and an instance placed of every different pickup.\n\n    预期产出:\n\n    ![Figure 18.15: The expected output of the activity ](img/B16183_18_15.jpg)\n\n图 18.15:活动的预期输出\n\n结果应该是一个项目，每个客户的角色可以拿起，使用和切换三种不同的武器。如果一个角色杀死了另一个角色，它应该记录杀死和死亡，并对随机玩家开始时死亡的角色进行重生。你应该有一个记分牌，显示每个玩家的名字、死亡人数、死亡人数和等级。一个角色可以从关卡中掉落，只能算作死亡，并在随机玩家开始时重生。角色还应该能够在关卡中拾取不同的拾取物来获得弹药、护甲、生命值和武器。通过显示记分牌和服务器在 5 秒钟后移动到同一水平，当达到杀死限制时，游戏应该结束。\n\n注意\n\n这个活动的解决方案可以在:[https://packt.live/338jEBx](https://packt.live/338jEBx)找到。\n\n# 总结\n\n在本章中，您了解到游戏框架类的实例存在于某些游戏实例中，但不存在于其他游戏实例中。拥有这些知识将有助于您了解在特定的游戏实例中可以访问哪些实例。您还学习了游戏状态和玩家状态类的目的，以及学习游戏模式的新概念和一些有用的内置功能。\n\n在这一章的最后，你已经做了一个基本但实用的多人射击游戏，可以作为一个基础来建立。你可以添加新的武器，弹药类型，射击模式，皮卡，等等，让它更加功能齐全和有趣。\n\n看完这本书，你现在应该对如何使用虚幻引擎 4 让你自己的游戏变得真实有了更好的了解。我们在这本书里讨论了很多话题，从简单的到更高级的。您首先学习了如何使用不同的模板创建项目，以及如何使用蓝图来创建参与者和组件。然后，您看到了如何通过导入和设置所需的资产，设置动画蓝图和混合空间，创建自己的游戏模式和角色，以及定义和处理输入，从头开始创建一个功能完整的第三人模板。\n\n然后你继续你的第一个项目；一个简单的隐形游戏，使用游戏物理和碰撞，投射物运动组件，演员组件，界面，蓝图函数库，UMG，声音和粒子效果。接下来，您学习了如何使用人工智能、动画蒙太奇和可破坏网格创建一个简单的侧滚游戏。最后，您发现了如何使用网络框架附带的服务器-客户端体系结构、变量复制和 RPC 来创建第一人称多人射击游戏，并了解了玩家状态、游戏状态和游戏模式类的工作原理。\n\n通过从事使用引擎不同部分的各种项目，您现在对虚幻引擎 4 的工作原理有了很强的理解，虽然这是本书的结尾，但这只是您使用虚幻引擎 4 进入游戏开发世界之旅的开始。"
  },
  {
    "path": "docs/game-dev-proj-ue/README.md",
    "content": "# UE 游戏开发项目\n\n> 原书：[Game Development Projects With Unreal Engine](https://libgen.rs/book/index.php?md5=94A55E03CF12218F228A1B6260EE60D2)\n> \n> 协议：[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)\n> \n> 阶段：机翻（1）\n> \n> 自豪地采用[谷歌翻译](https://translate.google.cn/)\n> \n> 让开源界提前感受入关。\n\n* [在线阅读](https://ccpp.apachecn.org)\n* [在线阅读（Gitee）](https://apachecn.gitee.io/apachecn-c-cpp-zh/)\n* [ApacheCN 学习资源](http://docs.apachecn.org/)\n\n## 贡献指南\n\n本项目需要校对，欢迎大家提交 Pull Request。\n\n> 请您勇敢地去翻译和改进翻译。虽然我们追求卓越，但我们并不要求您做到十全十美，因此请不要担心因为翻译上犯错——在大部分情况下，我们的服务器已经记录所有的翻译，因此您不必担心会因为您的失误遭到无法挽回的破坏。（改编自维基百科）\n\n## 联系方式\n\n### 负责人\n\n* [飞龙](https://github.com/wizardforcel): 562826179\n\n### 其他\n\n*   在我们的 [apachecn/apachecn-c-cpp-zh](https://github.com/apachecn/apachecn-c-cpp-zh) github 上提 issue.\n*   发邮件到 Email: `apachecn@163.com`.\n*   在我们的 [组织学习交流群](http://www.apachecn.org/organization/348.html) 中联系群主/管理员即可.\n\n## 赞助我们\n\n![](http://data.apachecn.org/img/about/donate.jpg)\n"
  },
  {
    "path": "docs/game-dev-proj-ue/SUMMARY.md",
    "content": "+   [UE 游戏开发项目](README.md)\n+   [零、前言](00.md)\n+   [一、虚幻引擎介绍](01.md)\n+   [二、使用虚幻引擎](02.md)\n+   [三、角色类组件和蓝图设置](03.md)\n+   [四、玩家输入](04.md)\n+   [五、线条痕迹](05.md)\n+   [六、碰撞物体](06.md)\n+   [八、用户界面](07.md)\n+   [九、视听元素](08.md)\n+   [十、创建`SuperSideScroller`游戏](09.md)\n+   [十一、混合空间 1D、按键绑定和状态机](10.md)\n+   [十二、动画混合和蒙太奇](11.md)\n+   [十三、敌方人工智能](12.md)\n+   [十四、产生玩家投射物](13.md)\n+   [十五、收藏品、加强和拾取](14.md)\n+   [十六、多人游戏基础](15.md)\n+   [十七、远程过程调用](16.md)\n+   [十八、多人游戏中的游戏框架类](17.md)\n"
  },
  {
    "path": "docs/handson-cpp-game-ani-prog/00.md",
    "content": "# 零、前言\n\n现代游戏动画有点像黑色艺术。没有太多可用的资源来详细说明如何构建轨迹驱动的动画系统，或者像对偶四元数蒙皮这样的高级主题。这就是本书想要填补的空白。这本书的目标是阐明动画编程的黑色艺术，让每个人都能接触到这个话题。\n\n这本书采取“从理论到实现”的方法，在这里你将学习每个首先讨论的主题的理论。一旦你理解了这个理论，你就会实施它来获得实践经验。\n\n这本书的重点是动画编程的概念和实现细节，而不是所使用的语言或图形应用编程接口。通过关注这些基本概念，您将能够实现一个动画系统，而不管语言或图形应用编程接口如何。\n\n# 这本书是给谁的\n\n这本书是为想学习如何构建现代动画系统的程序员准备的。跟随这本书的唯一要求是熟悉 C++。除此之外，从如何打开一个新窗口，到创建一个 OpenGL 上下文，渲染一个动画模型，以及高级动画技术，一切都涵盖在内。\n\n# 这本书涵盖了什么\n\n[*第一章*](01.html#_idTextAnchor013) *【创建游戏窗口】*介绍了如何新建 visual studio 项目，创建 Win32 窗口，设置 OpenGL 3.3 渲染上下文，启用 vsynch。本书的代码示例是针对 OpenGL 3.3 编译的。所有的 OpenGL 代码都兼容最新版本的 OpenGL 和 OpenGL 4.6。\n\n[*第二章*](02.html#_idTextAnchor026) *【实现向量】*，涵盖游戏动画编程的向量数学。\n\n[*第三章*](03.html#_idTextAnchor048) *【实现矩阵】*，讨论游戏动画编程的矩阵数学。\n\n[*第四章*](04.html#_idTextAnchor069) *【实现四元数】*解释了如何使用四元数数学进行游戏动画编程。\n\n[*第 5 章*](05.html#_idTextAnchor094) *【实现变换】*解释了如何将位置、旋转和缩放组合成一个变换对象。这些变换对象可以按层次结构排列。\n\n[*第 6 章*](06.html#_idTextAnchor104) *【构建抽象渲染器】*向您展示了如何在 OpenGL 3.3 之上创建抽象层。本书的其余部分将使用这种抽象进行渲染。通过使用抽象，我们可以专注于动画编程的核心概念，而不是用来实现它的应用编程接口。抽象层以 OpenGL 3.3 为目标，但代码对 OpenGL 4.6 也有效。\n\n[*第七章*](07.html#_idTextAnchor128) *，了解 glTF 文件格式*，介绍 glTF 文件格式。glTF 是大多数 3D 内容创建工具都支持的标准开放文件格式。能够加载一个通用的格式将让你加载几乎任何创作工具创作的动画。\n\n[*第 8 章*](08.html#_idTextAnchor142) *创建曲线、帧*和轨迹，介绍了如何插值曲线以及如何使用关键点为存储在层次结构中的变换设置动画。\n\n[*第九章*](09.html#_idTextAnchor155) *【实现动画剪辑】*，讲解如何实现动画剪辑。动画剪辑会随着时间的推移修改变换层次结构。\n\n[*第 10 章*](10.html#_idTextAnchor167) *【网格蒙皮】*介绍了如何对网格进行变形，使其与动画剪辑采样生成的姿势相匹配。\n\n[*第十一章*](11.html#_idTextAnchor185) *【优化动画流水线】*为大家展示了如何优化动画流水线的各个部分，让其更快更有制作准备。\n\n[*第 12 章*](12.html#_idTextAnchor204) *，动画之间的融合*，解释了如何将两个动画姿势融合在一起。这种技术可以用来在两个动画之间平滑切换，没有任何视觉爆音。\n\n[*第十三章*](13.html#_idTextAnchor217) *，实现逆运动学*，讲述了如何使用逆运动学让动画与环境互动。例如，您将学习如何使动画角色的脚在不平坦的地形上不穿透地面。\n\n[*第十四章*](14.html#_idTextAnchor235) *，使用对偶四元数进行蒙皮*，涵盖了游戏动画的对偶四元数数学。可以使用双四元数来避免动画关节处的挤压。\n\n[*第 15 章*](15.html#_idTextAnchor249) *【渲染实例化人群】*展示了如何将动画数据编码到纹理中，并将姿势生成移动到顶点着色器中。您将使用此技术使用实例化来渲染大量人群。\n\n# 为了充分利用这本书\n\n为了充分利用这本书，需要一些 C++ 的经验。你不需要成为一个强硬的 C++ 大师，但是你应该能够调试简单的 C++ 问题。有一些 OpenGL 的经验是一个优势，但不是必需的。没有使用高级的 C++ 特性。提供的代码根据 C++ 11 或最新版本进行编译。\n\n本书中的代码是针对 OpenGL 3.3 Core 编写的。本书呈现的 OpenGL 代码是向前兼容的；发布时 OpenGL 的最高兼容版本是 4.6。在 [*第 6 章*](06.html#_idTextAnchor104) 中，构建一个抽象渲染器，你将在 OpenGL 之上实现一个薄的抽象层。在本书的剩余部分，您将针对这个抽象层进行编码，而不是直接使用 OpenGL。\n\n所展示的代码应该可以在几乎任何运行 Windows 10 或更新版本的笔记本电脑上编译和运行。本书唯一需要遵循的硬件要求是能够运行 Visual Studio 2019 或更高版本的计算机。\n\nVisual Studio 2019 的最低硬件要求是:\n\n*   Windows 10，版本 1703 或更高版本\n*   1.8 Ghz 或更快的处理器\n*   2GB 内存\n\n这些要求可以在网上找到:https://docs . Microsoft . com/en-us/visualstudio/releases/2019/system-requirements\n\n下载示例代码文件\n\n你可以从你在[http://www.packt.com](http://www.packt.com)的账户下载这本书的示例代码文件。如果您在其他地方购买了这本书，您可以访问[http://www.packt.com/support](http://www.packt.com/support)并注册将文件直接通过电子邮件发送给您。\n\n您可以按照以下步骤下载代码文件:\n\n1.  登录或注册[http://www.packt.com](http://www.packt.com)。\n2.  选择“支持”选项卡。\n3.  点击代码下载和勘误表。\n4.  在搜索框中输入图书的名称，并按照屏幕上的说明进行操作。\n\n下载文件后，请确保使用最新版本的解压缩文件夹:\n\n*   视窗系统的 WinRAR/7-Zip\n*   zipeg/izp/un ARX for MAC\n*   适用于 Linux 的 7-Zip/PeaZip\n\n这本书的代码包也在 GitHub 上托管在[https://GitHub . com/packt publishing/Game-Animation-Programming](https://github.com/PacktPublishing/Game-Animation-Programming)。如果代码有更新，它将在现有的 GitHub 存储库中更新。\n\n我们还有来自 https://github.com/PacktPublishing/丰富的书籍和视频目录的其他代码包。看看他们！\n\n## 使用的约定\n\n本书通篇使用了许多文本约定。\n\n`CodeInText`:表示文本中的码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟网址、用户输入和推特句柄。比如；\"将下载的`WebStorm-10*.dmg`磁盘映像文件作为另一个磁盘安装到您的系统中.\"\n\n代码块设置如下:\n\n```cpp\npublic:\n    Pose();\n    Pose(const Pose& p);\n    Pose& operator=(const Pose& p);\n    Pose(unsigned int numJoints);\n```\n\n任何命令行输入或输出都编写如下:\n\n```cpp\n# cp /usr/src/asterisk-addons/configs/cdr_mysql.conf.sample\n     /etc/asterisk/cdr_mysql.conf\n```\n\n**粗体:**表示一个新的术语，一个重要的单词，或者你在屏幕上看到的单词，例如在菜单或对话框中，也像这样出现在文本中。例如:“从管理面板中选择**系统信息**\n\n注意\n\n警告或重要注意事项是这样出现的。\n\n提示和技巧是这样出现的。\n\n# 取得联系\n\n我们随时欢迎读者的反馈。\n\n一般反馈:如果您对本书的任何方面有疑问，请在您的信息主题中提及书名，并通过`customercare@packtpub.com`向我们发送电子邮件。\n\n与作者取得联系:与该书作者加博尔取得联系的最佳方式是在推特上:`@gszauer`。\n\n勘误表:虽然我们已经尽了最大努力来确保内容的准确性，但错误还是会发生。如果你在这本书里发现了一个错误，如果你能向我们报告，我们将不胜感激。请访问，[http://www.packt.com/submit-errata](http://www.packt.com/submit-errata)，选择您的图书，点击勘误表提交链接，并输入详细信息。\n\n盗版:如果您在互联网上遇到任何形式的我们作品的非法拷贝，如果您能提供我们的位置地址或网站名称，我们将不胜感激。请联系我们在 copyright@packt.com 与链接的材料。\n\n如果你有兴趣成为一名作者:如果有一个你有专长的话题，你有兴趣写或写一本书，请访问[http://authors.packtpub.com](http://authors.packtpub.com/)。\n\n## 评论\n\n请留下评论。一旦你阅读并使用了这本书，为什么不在你购买它的网站上留下评论呢？然后，潜在的读者可以看到并使用您不带偏见的意见来做出购买决定，我们在 Packt 可以了解您对我们产品的看法，我们的作者可以看到您对他们的书的反馈。谢谢大家！\n\n更多关于 Packt 的信息，请访问[packt.com](https://www.packtpub.com/)。"
  },
  {
    "path": "docs/handson-cpp-game-ani-prog/01.md",
    "content": "# 一、创建游戏窗口\n\n在本章中，您将设置一个简单的 Win32 窗口，并将一个 OpenGL 上下文绑定到它。在本书中，您将使用 OpenGL 3.3 Core。实际的 OpenGL 代码将非常少。\n\n大多数特定于 OpenGL 的代码将被抽象成助手对象和函数，这将允许您专注于动画，而不是任何特定的图形 API。您将在 [*第 6 章*](06.html#_idTextAnchor104) *中编写抽象层，构建一个抽象渲染器*，但是现在，创建一个准备好被绘制的窗口很重要。\n\n到本章结束时，您应该能够执行以下操作:\n\n*   打开一个 Win32 窗口\n*   创建并绑定一个 OpenGL 3.3 核心上下文\n*   用高兴加载 OpenGL 3.3 核心函数\n*   为创建的窗口启用 vsynch\n*   了解本书的可下载示例\n\n# 技术要求\n\n为了遵循本书中的代码，您需要一台安装了最新版本的 Visual Studio 的运行 Windows 10 的计算机。所有可下载的代码示例都是使用 Visual Studio 2019 构建的。可以从[https://visualstudio.microsoft.com/](https://visualstudio.microsoft.com/)下载 Visual Studio。\n\n你可以在[https://GitHub . com/packt publishing/Game-Animation-Programming](https://github.com/PacktPublishing/Game-Animation-Programming)上找到这本书的所有示例代码。\n\n# 创建一个空项目\n\n在本书中，你将尽可能多地从头开始创建代码。正因为如此，外部依赖会很少。要开始，请按照以下步骤在 Visual Studio 中创建新的空白 C++ 项目:\n\n1.  Open Visual Studio and create a new project by going to **File**|**New**|**Project**:\n\n    ![Figure 1.1: Creating a new Visual Studio project ](img/Figure_1.1_B16191.jpg)\n\n    图 1.1:创建一个新的 Visual Studio 项目\n\n2.  You will see your project templates on the left-hand side of the window that pops up. Navigate to **Installed**|**Visual C++**|**Other**. Then, select **Empty Project**:\n\n    ![Figure 1.2: Creating an empty C++ project ](img/Figure_1.2_B16191.jpg)\n\n    图 1.2:创建一个空的 C++ 项目\n\n3.  输入项目名称并选择项目位置。最后，点击**创建**。\n\n![Figure 1.3: Specifying a new project name ](img/Figure_1.3_B16191.jpg)\n\n图 1.3:指定新的项目名称\n\n如果你已经遵循了前面的步骤，你应该有一个新的空白项目。在本章的剩余部分，您将添加一个应用框架和一个支持 OpenGL 的窗口。\n\n# 创建应用类\n\n很难维持杂乱的窗口输入功能。相反，你需要创建一个抽象的`Application`类。该类将包含一些基本功能，如`Initialize`、`Update`、`Render`和`Shutdown`。本书提供的所有代码示例都将建立在`Application`基类之上。\n\n创建新文件，`Application.h`。下面的代码示例提供了`Application`类的声明。将此声明添加到新创建的`Application.h`文件中:\n\n```cpp\n#ifndef _H_APPLICATION_\n#define _H_APPLICATION_\nclass Application {\nprivate:\n    Application(const Application&);\n    Application& operator=(const Application&);\npublic:\n    inline Application() { }\n    inline virtual ~Application() { }\n    inline virtual void Initialize() { }\n    inline virtual void Update(float inDeltaTime) { }\n    inline virtual void Render(float inAspectRatio) { }\n    inline virtual void Shutdown() { }\n};\n#endif\n```\n\n`Initialize`、`Update`、`Render`和`Shutdown`功能是应用的生命周期。所有这些函数都将直接从 Win32 窗口代码中调用。`Update`和`Render`拿说事。要更新帧，需要知道当前帧和最后一帧之间的增量时间。要渲染帧，必须知道窗口的纵横比。\n\n生命周期功能是虚拟的。本书可下载资料中的每一章都有一个例子，它是`Application`类的子类，演示了该章中的一个概念。\n\n接下来，您将向项目添加一个 OpenGL 加载器。\n\n# 添加一个 OpenGL 加载器\n\n有一些本章所依赖的外部代码，叫做`glad`。当你在 Windows 上创建一个新的 OpenGL 上下文时，它是用一个遗留的 OpenGL 上下文创建的。OpenGL 的扩展机制会让你利用这个遗留的上下文来创建一个新的现代上下文。\n\n一旦创建了现代上下文，您将需要获取指向所有 OpenGL 函数的函数指针。函数需要加载`wglGetProcAdress`，T0 返回一个函数指针。\n\n以这种方式加载每个 OpenGL 函数将非常耗时。这就是拥有一个 OpenGL 加载器的好处；`glad`会为你做好这一切工作。OpenGL 加载器是一个库或一些代码，调用 OpenGL API 定义的函数上的`wglGetProcAdress`。\n\nWindows 上有几个 OpenGL 加载器。；本书将使用`glad`。`glad`是一个只有几个文件的小库。它有一个简单的 API 你调用一个函数，就可以访问所有的 OpenGL 函数。`glad`拥有基于网络的界面；你可以在[https://glad.dav1d.de/](https://glad.dav1d.de/)找到。\n\n重要说明\n\n使用 X Windows 系统时，比如很多流行的 Linux 发行版，加载 OpenGL 函数的函数是`glXGetProcAddress`。和 Windows 一样，Linux 也有 OpenGL 加载器。不是所有的操作系统都需要一个 OpenGL 加载器；例如，macOS、iOS 和 Android 不需要加载程序。iOS 和安卓都在 OpenGL ES 上运行。\n\n# 变得高兴\n\n你可以从基于网络的发电机 https://glad.dav1d.de/获得`glad`:\n\n1.  Go to the site, select **Version 3.3** from the **gl** dropdown, and select **Core** from the **Profile** dropdown:\n\n    ![Figure 1.4: Configuring glad ](img/Figure_1.4_B16191.jpg)\n\n    图 1.4:配置高兴\n\n2.  滚动至底部，点击**生成**按钮。这将开始下载一个包含所有必需代码的 ZIP 文件。\n\n本书介绍的代码与 OpenGL 版或更高版本向前兼容。如果您想使用更新的 OpenGL 版本，如 4.6，请将 API 下的 GL 下拉列表更改为所需版本。在下一节中，您将把这个 ZIP 文件的内容添加到您的主项目中。\n\n## 为项目添喜\n\n一旦`glad.zip`被下载，提取其内容。将以下文件从 ZIP 文件添加到您的项目中。目录结构不需要维护；所有这些文件都可以放在一起:\n\n*   `src/glad.c`\n*   `include/glad/glad.h`\n*   `include/KHR/khrplatform.h`\n\n这些文件将作为普通项目文件包含在内，您不必设置`include`路径，但这确实意味着需要编辑文件的内容:\n\n1.  Open `glad.c` and find the following #include:\n\n    `#include <glad/glad.h>`\n\n2.  Replace the `include` path with the relative path of `glad.h`:\n\n    `#include \"glad.h\"`\n\n3.  Similarly, open `glad.h` and find the following #include:\n\n    `#include <KHR/khrplatform.h>`\n\n4.  Replace the `include` path with the relative path of `khrplatform.h`:\n\n    `#include \"khrplatform.h\"`\n\n`glad`现在应该是添加到项目中，应该没有编译错误。在下一节中，您将开始实现 Win32 窗口。\n\n# 创建窗口\n\n在本节中，您将创建一个窗口。这意味着您将直接使用 Win32 API 调用来打开一个窗口，并从代码中控制其生命周期。您还将设置一个可以在窗口旁边运行的调试控制台，这对于查看日志非常有用。\n\n重要说明\n\n对 Win32 API 的深入讨论超出了本书的范围。有关任何 Win32 APIs 的更多信息，请参考位于[https://docs.microsoft.com/en-us/windows/win32/api/](https://docs.microsoft.com/en-us/windows/win32/api/)的微软开发者网络(MSDN)。\n\n为了让日志记录更容易一些，两个窗口将在调试模式下同时打开。一个是标准的 Win32 窗口，另一个是用于查看日志的控制台窗口。这可以通过有条件地设置链接器来实现。在调试模式下，应用应该链接到控制台子系统。在发布模式下，它应该链接到窗口子系统。\n\n可以通过项目的属性或使用`#pragma`注释在代码中设置链接器子系统。一旦子系统设置到控制台，就可以从`main`调用`WinMain`功能，这将启动一个附加到控制台的窗口。\n\n附加的链接器操作，如链接到外部库，也可以从代码中完成。您将使用`#pragma`命令与 OpenGL 链接。\n\n通过创建新文件`WinMain.cpp`开始窗口实现。这个文件将包含所有的窗口逻辑。然后，执行以下操作:\n\n1.  将以下代码添加到文件的开头。它创建`#define`常量，通过包含`<windows.h>` :\n\n    ```cpp\n    #define _CRT_SECURE_NO_WARNINGS\n    #define WIN32_LEAN_AND_MEAN\n    #define WIN32_EXTRA_LEAN\n    #include \"glad.h\"\n    #include <windows.h>\n    #include <iostream>\n    #include \"Application.h\"\n    ```\n\n    来减少引入的代码量\n2.  窗口输入函数和窗口事件处理函数都需要正向声明。这是我们需要打开新窗口的两个 Win32 函数:\n\n    ```cpp\n    int WINAPI WinMain(HINSTANCE, HINSTANCE, PSTR, int);\n    LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);\n    ```\n\n3.  使用`#pragma`注释在代码中链接到`OpenGL32.lib`，而不是通过项目的属性窗口。将以下代码添加到`WinMain.cpp` :\n\n    ```cpp\n    #if _DEBUG\n        #pragma comment( linker, \"/subsystem:console\" )\n        int main(int argc, const char** argv) {\n            return WinMain(GetModuleHandle(NULL), NULL,\n                    GetCommandLineA(), SW_SHOWDEFAULT);\n        }\n    #else\n        #pragma comment( linker, \"/subsystem:windows\" )\n    #endif\n    #pragma comment(lib, \"opengl32.lib\")\n    ```\n\n现在，需要声明几个 OpenGL 函数。创建一个现代的 OpenGL 上下文是通过`wglCreateContextAttribsARB`完成的，但是没有引用这个函数。这是需要通过`wglGetProcAddress`加载的功能之一，因为它是一个扩展功能。\n\n`wglCreateContextAttribsARB`的功能签名可以在`wglext.h`中找到。`wglext.h`标题由 Khronos 托管，可以在 https://www.khronos.org/registry/OpenGL/index_gl.php 的 OpenGL 注册表中在线找到。\n\n不需要包含整个`wglext.h`头文件；你只需要与创造现代环境相关的功能。下面的代码是直接从文件中复制的。它包含相关`#define`常数和函数指针类型的声明:\n\n```cpp\n#define WGL_CONTEXT_MAJOR_VERSION_ARB     0x2091\n#define WGL_CONTEXT_MINOR_VERSION_ARB     0x2092\n#define WGL_CONTEXT_FLAGS_ARB             0x2094\n#define WGL_CONTEXT_CORE_PROFILE_BIT_ARB  0x00000001\n#define WGL_CONTEXT_PROFILE_MASK_ARB      0x9126\ntypedef HGLRC(WINAPI* PFNWGLCREATECONTEXTATTRIBSARBPROC) \n             (HDC, HGLRC, const int*);\n```\n\n前面的代码为`wglCreatecontextAttribsARB`定义了一个函数指针类型。除此之外，还需要`#define`常量来创建 OpenGL 3.3 Core 上下文。本书样本将启用`vsynch`，可通过`wglSwapIntervalEXT`完成。\n\n正如您所猜测的，这个函数也需要使用 OpenGL 的扩展机制来加载。它还需要两个额外的支持功能:`wglGetExtensionStringEXT`和`wglGetSwapIntervalEXT`。所有这三个功能都可以在`wgl.h`找到，它由 Khronos 在之前链接的 OpenGL 注册表中托管。\n\n不包括`wgl.h`，在`WinMain.cpp`中增加以下代码。代码为`wglGetExtensionStringEXT`、`wglSwapIntervalEXT`和`wglGetSwapIntervalEXT`定义函数指针签名，复制自`wgl.h`:\n\n```cpp\ntypedef const char* \n        (WINAPI* PFNWGLGETEXTENSIONSSTRINGEXTPROC) (void);\ntypedef BOOL(WINAPI* PFNWGLSWAPINTERVALEXTPROC) (int);\ntypedef int (WINAPI* PFNWGLGETSWAPINTERVALEXTPROC) (void);\n```\n\n前面的代码是使用 OpenGL 所需的。通常是复制代码，而不是直接包含这些头。在下一节中，您将开始处理实际的窗口。\n\n## 全局变量\n\n简单的窗口清理需要两个全局变量:一个指向当前运行的应用的指针和一个指向全局 OpenGL **顶点数组对象** ( **VAO** )的句柄。每个抽奖呼叫都有自己的 VAO，而不是在整个样本期间都有一个。\n\n为此，请创建以下全局变量:\n\n```cpp\nApplication* gApplication = 0;\nGLuint gVertexArrayObject = 0;\n```\n\n在本书的其余部分，不会有其他全局变量。全局变量会使程序状态更难跟踪。这两者存在的原因是，当应用稍后关闭时，可以很容易地引用它们。接下来，您将开始实现`WinMain`功能以打开一个新窗口。\n\n## 开窗\n\n接下来，需要实现窗口录入功能，`WinMain`。该函数将负责创建窗口类、注册窗口类和打开新窗口:\n\n1.  通过创建`Application`类的新实例并将其存储在全局指针中来开始`WinMain`的定义:\n\n    ```cpp\n    int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE \n                       hPrevInstance, PSTR szCmdLine, \n                       int iCmdShow) {\n    gApplication = new Application();\n    ```\n\n2.  接下来，需要填写`WNDCLASSEX`的一个实例。这里面没有什么特别的东西；这只是一个标准的窗口定义。唯一需要注意的是`WndProc`功能设置是否正确:\n\n    ```cpp\n        WNDCLASSEX wndclass;\n        wndclass.cbSize = sizeof(WNDCLASSEX);\n        wndclass.style = CS_HREDRAW | CS_VREDRAW;\n        wndclass.lpfnWndProc = WndProc;\n        wndclass.cbClsExtra = 0;\n        wndclass.cbWndExtra = 0;\n        wndclass.hInstance = hInstance;\n        wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);\n        wndclass.hIconSm = LoadIcon(NULL, IDI_APPLICATION);\n        wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);\n        wndclass.hbrBackground = (HBRUSH)(COLOR_BTNFACE + 1);\n        wndclass.lpszMenuName = 0;\n        wndclass.lpszClassName = \"Win32 Game Window\";\n        RegisterClassEx(&wndclass);\n    ```\n\n3.  一个新的应用窗口将在监视器的中心启动。为此，使用`GetSystemMetrics`找到屏幕的宽度和高度。然后，围绕屏幕中心将`windowRect`调整到所需的大小:\n\n    ```cpp\n        int screenWidth = GetSystemMetrics(SM_CXSCREEN);\n        int screenHeight = GetSystemMetrics(SM_CYSCREEN);\n        int clientWidth = 800;\n        int clientHeight = 600;\n        RECT windowRect;\n        SetRect(&windowRect, \n                (screenWidth / 2) - (clientWidth / 2), \n                (screenHeight / 2) - (clientHeight / 2), \n                (screenWidth / 2) + (clientWidth / 2), \n                (screenHeight / 2) + (clientHeight / 2));\n    ```\n\n4.  要计算出窗口的大小，而不仅仅是客户区域，需要知道窗口的样式。下面的代码示例创建一个可以最小化或最大化但不能调整大小的窗口。要调整窗口大小，请使用按位“或”(`|`)运算符，并定义`WS_THICKFRAME`:\n\n    ```cpp\n        DWORD style = (WS_OVERLAPPED | WS_CAPTION | \n            WS_SYSMENU | WS_MINIMIZEBOX | WS_MAXIMIZEBOX); \n        // | WS_THICKFRAME to resize\n    ```\n\n5.  一旦定义了所需的窗户样式，调用`AdjustWindowRectEx`功能来调整 clien t 矩形的大小，以包括其大小中的所有窗户装饰。当最终尺寸已知时，`CreateWindowEx`可用于创建实际窗口。创建窗口后，存储对其设备上下文的引用:\n\n    ```cpp\n        AdjustWindowRectEx(&windowRect, style, FALSE, 0);\n        HWND hwnd = CreateWindowEx(0, wndclass.lpszClassName, \n                    \"Game Window\", style, windowRect.left, \n                    windowRect.top, windowRect.right - \n                    windowRect.left, windowRect.bottom - \n                    windowRect.top, NULL, NULL, \n                    hInstance, szCmdLine);\n        HDC hdc = GetDC(hwnd);\n    ```\n\n6.  现在已经创建了窗口，接下来您将创建一个 OpenGL 上下文。为此，您首先需要找到正确的像素格式，然后将其应用于窗口的设备上下文。下面的代码向您展示了如何做到这一点:\n\n    ```cpp\n        PIXELFORMATDESCRIPTOR pfd;\n        memset(&pfd, 0, sizeof(PIXELFORMATDESCRIPTOR));\n        pfd.nSize = sizeof(PIXELFORMATDESCRIPTOR);\n        pfd.nVersion = 1;\n        pfd.dwFlags = PFD_SUPPORT_OPENGL | PFD_DRAW_TO_WINDOW \n                      | PFD_DOUBLEBUFFER;\n        pfd.iPixelType = PFD_TYPE_RGBA;\n        pfd.cColorBits = 24;\n        pfd.cDepthBits = 32;\n        pfd.cStencilBits = 8;\n        pfd.iLayerType = PFD_MAIN_PLANE;\n        int pixelFormat = ChoosePixelFormat(hdc, &pfd);\n        SetPixelFormat(hdc, pixelFormat, &pfd);\n    ```\n\n7.  设置好像素格式后，使用`wglCreateContext`创建一个临时的 OpenGL 上下文。这个临时上下文只需要获取一个指向`wglCreateContextAttribsARB`的指针，它将被用来创建一个现代上下文:\n\n    ```cpp\n        HGLRC tempRC = wglCreateContext(hdc);\n        wglMakeCurrent(hdc, tempRC);\n        PFNWGLCREATECONTEXTATTRIBSARBPROC\n           wglCreateContextAttribsARB = NULL;\n        wglCreateContextAttribsARB =\n           (PFNWGLCREATECONTEXTATTRIBSARBPROC)\n           wglGetProcAddress(\"wglCreateContextAttribsARB\");\n    ```\n\n8.  一个临时的 OpenGL 上下文存在并且被绑定，所以接下来调用`wglCreateContextAttribsARB`函数。该函数将返回一个 OpenGL 3.3 Core 上下文概要文件，将其绑定，并删除遗留上下文:\n\n    ```cpp\n        const int attribList[] = {\n            WGL_CONTEXT_MAJOR_VERSION_ARB, 3,\n            WGL_CONTEXT_MINOR_VERSION_ARB, 3,\n            WGL_CONTEXT_FLAGS_ARB, 0,\n            WGL_CONTEXT_PROFILE_MASK_ARB,\n            WGL_CONTEXT_CORE_PROFILE_BIT_ARB,\n            0, };\n        HGLRC hglrc = wglCreateContextAttribsARB(\n                           hdc, 0, attribList);\n        wglMakeCurrent(NULL, NULL);\n        wglDeleteContext(tempRC);\n        wglMakeCurrent(hdc, hglrc);\n    ```\n\n9.  当 OpenGL 3.3 Core 上下文激活时，`glad`可用于加载所有 OpenGL 3.3 Core 功能。打电话给`gladLoadGL`做这件事:\n\n    ```cpp\n        if (!gladLoadGL()) {\n            std::cout << \"Could not initialize GLAD\\n\";\n        }\n        else {\n            std::cout << \"OpenGL Version \" << \n            GLVersion.major << \".\" << GLVersion.minor <<\n              \"\\n\";\n        }\n    ```\n\n10.  现在应该初始化一个 OpenGL 3.3 核心上下文，加载所有的 OpenGL 核心函数。接下来，您将在窗口上启用`vsynch`。`vsynch`不是内置功能；这是一个扩展，因此需要向`wglGetExtensionStringEXT`询问对它的支持。`vsynch`的延长线是`WGL_EXT_swap_control`。检查是否在扩展字符串列表中:\n\n    ```cpp\n        PFNWGLGETEXTENSIONSSTRINGEXTPROC\n           _wglGetExtensionsStringEXT =\n           (PFNWGLGETEXTENSIONSSTRINGEXTPROC)\n           wglGetProcAddress(\"wglGetExtensionsStringEXT\");\n        bool swapControlSupported = strstr(\n             _wglGetExtensionsStringEXT(), \n             \"WGL_EXT_swap_control\") != 0;\n    ```\n\n11.  如果`WGL_EXT_swap_control`分机可用，需要加载。实际功能是`wglSwapIntervalEXT`，可以在`wgl.h`找到。将参数传递给`wglSwapIntervalEXT`会打开`vsynch` :\n\n    ```cpp\n        int vsynch = 0;\n        if (swapControlSupported) {\n            PFNWGLSWAPINTERVALEXTPROC wglSwapIntervalEXT = \n                (PFNWGLSWAPINTERVALEXTPROC)\n                wglGetProcAddress(\"wglSwapIntervalEXT\");\n            PFNWGLGETSWAPINTERVALEXTPROC \n                wglGetSwapIntervalEXT =\n                (PFNWGLGETSWAPINTERVALEXTPROC)\n                wglGetProcAddress(\"wglGetSwapIntervalEXT\");\n            if (wglSwapIntervalEXT(1)) {\n                std::cout << \"Enabled vsynch\\n\";\n                vsynch = wglGetSwapIntervalEXT();\n            }\n            else {\n                std::cout << \"Could not enable vsynch\\n\";\n            }\n        }\n        else { // !swapControlSupported\n            cout << \"WGL_EXT_swap_control not supported\\n\";\n        }\n    ```\n\n12.  只需要多做一点家务就可以完成设置一个支持 OpenGL 的窗口。OpenGL 3.3 内核要求所有绘制调用都绑定一个 VAO。您将创建一个绑定在`WinMain`中的全局 VAO，而不是为每个绘制调用创建一个 VAO，并且在窗口被破坏之前永不解除绑定。下面的代码创建这个 VAO 并绑定它:\n\n    ```cpp\n        glGenVertexArrays(1, &gVertexArrayObject);\n        glBindVertexArray(gVertexArrayObject);\n    ```\n\n13.  调用`ShowWindow`和`UpdateWindow`功能显示当前窗口；这也是初始化全局应用的好地方。根据应用的`Initialize`功能最终完成的工作量，窗口可能会出现一点冻结:\n\n    ```cpp\n        ShowWindow(hwnd, SW_SHOW);\n        UpdateWindow(hwnd);\n        gApplication->Initialize();\n    ```\n\n14.  现在，您已经准备好实现实际的游戏循环了。您需要跟踪最后一帧时间来计算帧之间的增量时间。除了游戏逻辑之外，循环还需要通过查看当前消息堆栈并相应地发送消息来处理窗口事件:\n\n    ```cpp\n        DWORD lastTick = GetTickCount();\n        MSG msg;\n        while (true) {\n            if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {\n                if (msg.message == WM_QUIT) {\n                    break;\n                }\n                TranslateMessage(&msg);\n                DispatchMessage(&msg);\n            }\n    ```\n\n15.  处理完窗口事件后，`Application`实例需要更新和渲染。首先，找到最后一帧和这一帧之间的时间差，将其转换为秒。例如，以 60 FPS 运行的游戏应该有 16.6 毫秒的增量时间，或 0.0166 秒:\n\n    ```cpp\n            DWORD thisTick = GetTickCount();\n            float dt = float(thisTick - lastTick) * 0.001f;\n            lastTick = thisTick;\n            if (gApplication != 0) {\n                gApplication->Update(dt);\n            }\n    ```\n\n16.  渲染当前运行的应用只需要稍微多一点内务处理。用`glViewport`设置每帧的 OpenGL 视口，并清除颜色、深度和模板缓冲。除此之外，在渲染之前，请确保所有 OpenGL 状态都是正确的。这意味着绑定了正确的 VAO，启用了深度测试和人脸剔除，并设置了合适的点大小:\n\n    ```cpp\n            if (gApplication != 0) {\n                RECT clientRect;\n                GetClientRect(hwnd, &clientRect);\n                clientWidth = clientRect.right - \n                              clientRect.left;\n                clientHeight = clientRect.bottom - \n                               clientRect.top;\n                glViewport(0, 0, clientWidth, clientHeight);\n                glEnable(GL_DEPTH_TEST);\n                glEnable(GL_CULL_FACE);\n                glPointSize(5.0f);\n                glBindVertexArray(gVertexArrayObject);\n                glClearColor(0.5f, 0.6f, 0.7f, 1.0f);\n                glClear(GL_COLOR_BUFFER_BIT | \n                GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);\n                float aspect = (float)clientWidth / \n                               (float)clientHeight;\n                gApplication->Render(aspect);\n            }\n    ```\n\n17.  在当前`Application`实例更新并渲染后，需要显示后台缓冲区。这是通过调用`SwapBuffers`来完成的。如果启用了`vsynch`，则需要在`SwapBuffers` :\n\n    ```cpp\n            if (gApplication != 0) {\n                SwapBuffers(hdc);\n                if (vsynch != 0) {\n                    glFinish();\n                }\n            }\n    ```\n\n    之后立即调用`glFinish`\n18.  窗口期到此为止。窗口循环退出后，从`WinMain`功能返回是安全的:\n\n    ```cpp\n        } // End of game loop\n        if (gApplication != 0) {\n            std::cout << \"Expected application to \n                          be null on exit\\n\";\n            delete gApplication;\n        }\n        return (int)msg.wParam;\n    }\n    ```\n\n如果您想使用 3.3 以外的 OpenGL 版本，请调整步骤 8 中出现的`attribList`变量中的主值和次值。即使写了`WinMain`函数，你还是无法编译这个文件；它会失败，因为`WndProc`从未被定义过。`WndProc`功能处理诸如鼠标移动或调整窗口大小等事件。在下一节中，您将实现`WndProc`功能。\n\n## 创建事件处理程序\n\n为了让窗口正常运行，甚至编译应用，此时，必须定义事件处理函数`WndProc`。这里的实现将非常简单，主要集中在如何破坏窗口:\n\n1.  开始在`WinMain.cpp` :\n\n    ```cpp\n    LRESULT CALLBACK WndProc(HWND hwnd, UINT iMsg, \n                        WPARAM wParam, LPARAM lParam) {\n        switch (iMsg) {\n    ```\n\n    中实现`WndProc`功能\n2.  当收到`WM_CLOSE`消息时，需要关闭`Application`类并发出破坏窗口消息。一旦应用关闭，别忘了删除它:\n\n    ```cpp\n        case WM_CLOSE:\n            if (gApplication != 0) {\n                gApplication->Shutdown();\n                delete gApplication;\n                gApplication = 0;\n                DestroyWindow(hwnd);\n            }\n            else {\n                std::cout << \"Already shut down!\\n\";\n            }\n            break;\n    ```\n\n3.  当收到销毁消息时，需要释放窗口的 OpenGL 资源。这个就是删除全局顶点数组对象，然后删除 OpenGL 上下文:\n\n    ```cpp\n        case WM_DESTROY:\n            if (gVertexArrayObject != 0) {\n                HDC hdc = GetDC(hwnd);\n                HGLRC hglrc = wglGetCurrentContext();\n                glBindVertexArray(0);\n                glDeleteVertexArrays(1, &gVertexArrayObject);\n                gVertexArrayObject = 0;\n                wglMakeCurrent(NULL, NULL);\n                wglDeleteContext(hglrc);\n                ReleaseDC(hwnd, hdc);\n                PostQuitMessage(0);\n            }\n            else {\n                std::cout << \"Multiple destroy messages\\n\";\n            }\n            break;\n    ```\n\n4.  绘画和擦除背景信息可以安全忽略，因为 OpenGL 正在管理窗口的渲染。如果收到的消息不是已处理的消息之一，将其转发到默认窗口消息功能:\n\n    ```cpp\n        case WM_PAINT:\n        case WM_ERASEBKGND:\n            return 0;\n        }\n        return DefWindowProc(hwnd, iMsg, wParam, lParam);\n    }\n    ```\n\n现在您已经编写了 windows 事件循环，您应该能够编译并运行一个空白窗口。在下一节中，您将探索这本书的可下载示例。\n\n# 探索样本\n\n本书提供的所有代码都可以在该书的可下载内容中找到。有一个大样本，称为`AllChapters`，它包括单个应用中的每个样本。有一个`Bin` ZIP 文件，其中包含一个预编译的`AllChapters`示例的可执行文件。\n\n每个章节也有单独的文件夹，包含多个子文件夹。每一章都包含`Sample00`，这是书中写的代码，没有额外的内容。随后编号的样本会添加内容。\n\n`AllChapters`样本看起来与单个章节文件夹中的样本有点不同。该应用使用努克拉尔([https://github.com/vurtun/nuklear](https://github.com/vurtun/nuklear))来显示其用户界面。用户界面显示的部分是屏幕右上角的统计计数器。看起来是这样的:\n\n![Figure 1.5: Stats counter for the AllChapters sample ](img/Figure_1.5_B16191.jpg)\n\n图 1.5:所有章节示例的统计计数器\n\n顶部的框包含一些关于应用打开时的显示的一般信息。该信息包含显示频率、`vsynch`是否启用、帧预算以毫秒为单位。\n\n向下的第二个框包含高级帧计时。如果在最后 60 帧中有过时的帧，显示的时间将变为红色。有些陈旧的框架是不可避免的；如果帧速率下降到 59.9，文本将在一秒钟内显示红色。偶尔在这里看到红色是可以的；如果数字是红色的，这只是一个问题。\n\n向下的第三个盒子包含两个 GPU 计时器；这些测量样本在 GPU 上运行的速度。这对于调试任何繁重的绘制调用都很有用。最后一个框包含 CPU 计时器，这有助于找出问题的哪个阶段有瓶颈。\n\n重要说明\n\n在本书中，您将使用 C++ `stl`容器。标准库在调试模式下有点慢，主要是由于错误检查。最好只在发布模式下分析任何样本。\n\n这些例子应该能很好地展示你将在接下来的章节中学到什么。它们也为您提供了一个比较代码的例子。\n\n# 总结\n\n在本章中，您探讨了设置新 Win32 窗口的过程。一个 OpenGL 3.3 核心上下文被设置为渲染到窗口，并启用`vsynch`。您已经了解了 OpenGL 加载器以及`glad`如何加载所有相关的 OpenGL 函数。\n\n这个窗口将成为你的基础；所有未来的示例都基于您在本章中创建的框架。在下一章中，您将开始探索渲染和动画所需的一些数学知识。"
  },
  {
    "path": "docs/handson-cpp-game-ani-prog/02.md",
    "content": "# 二、实现向量\n\n在这一章中，你将学习向量数学的基础知识。在本书余下的部分中，你将编写的大部分代码都依赖于对向量的深刻理解。向量将用于表示位移和方向。\n\n到本章结束时，您将实现一个健壮的向量库，并且能够执行各种向量操作，包括组件式和非组件式操作。\n\n我们将在本章中讨论以下主题:\n\n*   引入向量\n*   创建向量\n*   了解组件式操作\n*   理解非组件式操作\n*   插值向量\n*   比较向量\n*   Exploring more vectors\n\n    重要信息:\n\n    在本章中，您将学习如何以直观、可视化的方式实现向量，这种方式更多地依赖于代码，而不是数学公式。如果你对数学公式感兴趣或者想尝试一些互动的例子，可以去[https://gabormakesgames.com/vectors.html](https://gabormakesgames.com/vectors.html)。\n\n# 导入载体\n\n什么是向量？向量是数字的 n 元组。它代表一个位移量和一个方向。向量的每个元素通常用下标表示，如 *(V* 0 *、V* 1 *、V* 2 *、… V* N *)* 。在游戏中，向量通常有两个、三个或四个分量。\n\n例如，三维向量测量三个唯一轴上的位移: *x* 、 *y* 和 *z* 。向量的元素通常用它们所代表的轴而不是索引来下标。 *(V* X *、V* Y *、V* Z *)* 和 *(V* 0 *、V* 1 *、V* 2 *)* 可互换使用。\n\n当可视化向量时，它们通常被绘制为箭头。箭头底部的位置无关紧要，因为向量测量的是位移，而不是位置。箭头的末端跟随箭头在每个轴上的位移。\n\n例如，下图中的所有箭头代表相同的向量:\n\n![Figure 2.1: Vector (2, 5) drawn in multiple locations ](img/Figure_2.1_B16191.jpg)\n\n图 2.1:在多个位置绘制的向量(2，5)\n\n每个箭头都具有相同的长度并指向相同的方向，无论它位于何处。在下一节中，您将开始实现将在本书剩余部分中使用的向量结构。\n\n# 创建向量\n\n向量将被实现为结构，而不是类。向量结构将包含一个匿名联合，允许向量的组成部分作为一个数组或单个元素被访问。\n\n要声明`vec3`结构和函数头，创建一个新文件`vec3.h`。在此文件中声明新的`vec3`结构。`vec3`结构需要三个构造函数——一个默认构造函数，一个将每个组件作为一个元素，一个将指针指向一个浮点数组:\n\n```cpp\n#ifndef _H_VEC3_\n#define _H_VEC3_\nstruct vec3 {\n    union {\n        struct  {\n            float x;\n            float y;\n            float z;\n        };\n        float v[3];\n    };\n    inline vec3() : x(0.0f), y(0.0f), z(0.0f) { }\n    inline vec3(float _x, float _y, float _z) :\n        x(_x), y(_y), z(_z) { }\n    inline vec3(float *fv) :\n        x(fv[0]), y(fv[1]), z(fv[2]) { }\n};\n#endif \n```\n\n`vec3`结构中的匿名联合允许使用`.x`、`.y`和`.z`符号访问数据，或者使用`.v`作为连续数组访问数据。在继续实现在`vec3`结构上工作的函数之前，您需要考虑比较浮点数以及是否使用ε值。\n\n## ε\n\n比较浮点数很困难。不是直接比较两个浮点数，而是需要用一个ε来比较。ε是一个任意小的正数，这是两个数必须被认为是不同数的最小差。在`vec3.h`中声明一个ε常数:\n\n```cpp\n#define VEC3_EPSILON 0.000001f\n```\n\n重要提示:\n\n你可以在[https://bitbashing.io/comparing-floats.html](https://bitbashing.io/comparing-floats.html)了解更多关于浮点比较的知识\n\n创建`vec3`结构并定义`vec3`ε后，您就可以开始执行一些常见的向量操作了。在下一节中，您将从学习和实现几个组件式操作开始。\n\n# 了解组件式操作\n\n几个向量运算只是组件式运算。分量操作是对向量的每个分量或两个向量的相似分量执行的操作。相似组件是具有相同下标的组件。您将实现的组件式操作如下:\n\n*   向量加法\n*   向量减法\n*   向量缩放\n*   乘法向量\n*   点积\n\n让我们更详细地看看其中的每一个。\n\n## 向量加法\n\n将两个向量相加得到第三个向量，它具有两个输入向量的组合位移。向量加法是一种分量运算；要执行它，您需要添加类似的组件。\n\n要可视化两个向量的相加，请在第一个向量的顶端绘制第二个向量的底部。接下来，画一个从第一个向量的底部到第二个向量的顶端的箭头。此箭头表示相加后的向量:\n\n![](img/Figure_2.2_B16191.jpg)\n\n图 2.2:向量加法\n\n要在代码中实现向量加法，请添加输入向量的相似分量。创建新文件，`vec3.cpp`。这是定义与`vec3`结构相关的函数的地方。别忘了包括`vec3.h`。过载`+ operator`执行向量加法。别忘了给`vec3.h`添加功能签名:\n\n```cpp\nvec3 operator+(const vec3 &l, const vec3 &r) {\n    return vec3(l.x + r.x, l.y + r.y, l.z + r.z);\n}\n```\n\n当考虑向量加法时，记住向量代表位移。当添加两个向量时，结果是两个输入向量的组合位移。\n\n## 向量减法\n\n与添加向量一样，减去向量也是一个分量操作。你可以把减去向量想象成把第二个向量的负数加到第一个向量上。当可视化为箭头时，减法从第二个向量的末端指向第一个向量的末端。\n\n若要从视觉上减去向量，请将两个向量放置在同一原点。从第二个箭头的尖端到第一个箭头的尖端画一个向量。结果箭头是减法结果向量:\n\n![Figure 2.3: Vector subtraction ](img/Figure_2.3_B16191.jpg)\n\n图 2.3:向量减法\n\n要实现向量减法，请减去类似的分量。通过重载`vec3.cpp`中的`-`运算符来实现减法功能。别忘了给`vec3.h`添加功能声明:\n\n```cpp\nvec3 operator-(const vec3 &l, const vec3 &r) {\n    return vec3(l.x - r.x, l.y - r.y, l.z - r.z);\n}\n```\n\n步骤和逻辑与向量加法非常相似。把向量减法想象成加一个负向量可能会有帮助。\n\n## 缩放向量\n\n当一个向量被缩放时，它只在的大小上变化，而不是方向上。与加法和减法一样，缩放是一个组件式操作。与加法和减法不同，向量是由标量而不是另一个向量来缩放的。\n\n从视觉上看，缩放后的向量指向与原始向量相同的方向，但长度不同。下图显示了两个向量: *(2，1)* 和 *(2，4)* 。两个向量共享同一个方向，但第二个向量的大小较长:\n\n![Figure 2.4: Vector scaling ](img/Figure_2.4_B16191.jpg)\n\n图 2.4:向量缩放\n\n要实现向量缩放，请将向量的每个分量乘以给定的标量值。\n\n通过重载`vec3.cpp`中的`*`运算符来实现缩放功能。别忘了给`vec3.h`添加功能声明:\n\n```cpp\nvec3 operator*(const vec3 &v, float f) {\n    return vec3(v.x * f, v.y * f, v.z * f);\n}\n```\n\n否定一个向量可以通过 *-1* 缩放向量来完成。当否定一个向量时，该向量保持其大小但改变其方向。\n\n## 乘法向量\n\n向量乘法可以认为是一个非均匀尺度。不是用标量来缩放向量的每个分量，而是用另一个向量的相似分量来缩放向量的每个分量。\n\n您可以通过重载`vec3.cpp`中的`*`运算符来实现向量乘法。别忘了给`vec3.h`添加功能声明:\n\n```cpp\nvec3 operator*(const vec3 &l, const vec3 &r) {\n    return vec3(l.x * r.x, l.y * r.y, l.z * r.z);\n}\n```\n\n两个向量相乘产生的结果将具有不同的方向和大小。\n\n## 点积\n\n点积是用来衡量两个向量有多相似。给定两个向量，点积返回标量值。点积的结果具有以下性质:\n\n*   如果向量指向同一个方向，则为正。\n*   如果向量指向相反的方向，则为负。\n*   如果向量垂直，则为 *0* 。\n\n如果两个输入向量都有单位长度(您将在本章的*法向向量*部分了解单位长度向量)，点积的范围将是 *-1* 到 *1* 。\n\n两个向量 *A* 和 *B* 之间的点积等于 *A* 的长度乘以 *B* 的长度乘以两个向量之间角度的余弦:\n\n![](img/Formula_02_001.jpg)\n\n计算点积最简单的方法是对输入向量中相似分量的积求和:\n\n*![](img/Formula_02_002.png)*\n\n在`vec3.cpp`中实现`dot`功能。别忘了给`vec3.h`添加功能定义:\n\n```cpp\nfloat dot(const vec3 &l, const vec3 &r) {\n    return l.x * r.x + l.y * r.y + l.z * r.z;\n}\n```\n\n点积是电子游戏中最常用的操作之一。它通常用于检查角度和照明计算。\n\n使用点积，您已经实现了向量的常见分量操作。接下来，您将了解一些可以在向量上执行的非组件式操作。\n\n# 理解非组件式操作\n\n并非所有向量运算都是分量式的；有些运算需要更多的数学运算。在本节中，您将学习如何实现非基于组件的通用向量操作。这些操作如下:\n\n*   如何求向量的长度\n*   法向量是什么\n*   如何归一化向量\n*   如何求两个向量之间的角度\n*   如何投射向量，什么是拒绝\n*   如何反映向量\n*   什么是叉积以及如何实现它\n\n让我们更详细地看看每一个。\n\n## 向量长度\n\n向量表示方向和大小；向量的大小就是它的长度。求向量长度的公式来自三角学。在下图中，二维向量被分解成平行和垂直分量。注意这是如何形成直角三角形的，向量是斜边:\n\n![Figure 2.5: A vector broken down into parallel and perpendicular components ](img/Figure_2.5_B16191.jpg)\n\n图 2.5:分解成平行和垂直分量的向量\n\n直角三角形斜边的长度可以用勾股定理求出，*A*2*+B*2*= C*2。只需添加一个 *Z* 组件—*X*2*+Y*2*+Z*2*=长度* 2，该功能就扩展到了三维。\n\n你可能已经注意到了这里的一个模式；向量的平方长度等于其分量之和。这可以表示为点积— *长度* 2 *(A) =点(A，A)* :\n\n重要提示:\n\n求向量的长度涉及到平方根运算，在可能的情况下应该避免。当检查向量的长度时，检查可以在平方空间中进行，以避免平方根。例如，如果要检查向量 *A* 的长度是否小于 *5* ，则可以表示为*(点(A，A) < 5 * 5)* 。\n\n1.  为了实现平方长度函数，对向量的每个分量求平方的结果求和。在`vec3.cpp`中实现`lenSq`功能。别忘了把功能声明添加到`vec3.h` :\n\n    ```cpp\n    float lenSq(const vec3& v) {\n        return v.x * v.x + v.y * v.y + v.z * v.z;\n    }\n    ```\n\n2.  To implement the length function, take the square root of the result of the square length function. Take care not to call `sqrtf` with `0`. Implement the `lenSq` function in `vec3.cpp`. Don't forget to add the function declaration to `vec3.h`:\n\n    ```cpp\n    float len(const vec3 &v) {\n        float lenSq = v.x * v.x + v.y * v.y + v.z * v.z;\n        if (lenSq < VEC3_EPSILON) {\n            return 0.0f;\n        }\n        return sqrtf(lenSq);\n    }\n    ```\n\n    重要提示:\n\n    你可以通过取两个向量之间差的长度来找到它们之间的距离。例如，*浮动距离= len(vec1 - vec2)* 。\n\n## 归一化向量\n\n长度为 *1* 的向量称为法向向量(或单位向量)。通常，单位向量用于表示没有大小的方向。两个单位向量的点积将始终落在 *-1* 至 *1* 范围内。\n\n除了 *0* 向量之外，任何向量都可以通过按其长度的倒数缩放向量来归一化:\n\n1.  在`vec3.cpp`中实现`normalize`功能。别忘了把功能声明添加到`vec3.h` :\n\n    ```cpp\n    void normalize(vec3 &v) {\n        float lenSq = v.x * v.x + v.y * v.y + v.z * v.z;\n        if (lenSq < VEC3_EPSILON) { return; }\n        float invLen = 1.0f / sqrtf(lenSq);    \n        v.x *= invLen;\n        v.y *= invLen;\n        v.z *= invLen;\n    }\n    ```\n\n2.  在`vec3.cpp`中实现`normalized`功能。别忘了把功能声明添加到`vec3.h` :\n\n    ```cpp\n    vec3 normalized(const vec3 &v) {\n        float lenSq = v.x * v.x + v.y * v.y + v.z * v.z;\n        if (lenSq < VEC3_EPSILON) { return v; }\n        float invLen = 1.0f / sqrtf(lenSq);\n        return vec3(\n            v.x * invLen,\n            v.y * invLen,\n            v.z * invLen\n        );\n    }\n    ```\n\n`normalize`函数引用一个向量，并对其进行适当的归一化。另一方面，`normalized`函数采用恒定参考，不修改输入向量。相反，它返回一个新向量。\n\n## 向量之间的角度\n\n如果两个向量是单位长度，它们之间的角度是它们的点积的余弦:\n\n![](img/Formula_02_003.jpg)\n\n如果两个向量没有归一化，点积需要除以两个向量长度的乘积:\n\n![](img/Formula_02_004.jpg)\n\n为了找到实际的角度，而不仅仅是它的余弦，我们需要取两边余弦的倒数，这就是反余弦函数:\n\n![](img/Formula_02_005.jpg)\n\n在`vec3.cpp`中实现`angle`功能。别忘了给`vec3.h`添加功能声明:\n\n```cpp\nfloat angle(const vec3 &l, const vec3 &r) {\n    float sqMagL = l.x * l.x + l.y * l.y + l.z * l.z;\n    float sqMagR = r.x * r.x + r.y * r.y + r.z * r.z;\n    if (sqMagL<VEC3_EPSILON || sqMagR<VEC3_EPSILON) {\n        return 0.0f;\n    }\n    float dot = l.x * r.x + l.y * r.y + l.z * r.z;\n    float len = sqrtf(sqMagL) * sqrtf(sqMagR);\n    return acosf(dot / len);\n}\n```\n\n重要提示:\n\n`acosf`函数以弧度为单位返回角度。要将弧度转换为度数，乘以`57.2958f`。要将度数转换为弧度，乘以`0.0174533f`。\n\n## 向量投影和排斥\n\n将向量 *A* 投影到向量 *B* 上产生一个新向量，该向量在 *B* 方向上的长度为 *A* 。可视化向量投影的一个好方法是想象向量 *A* 正在向量 *B* 上投射阴影，如图所示:\n\n![Figure 2.6: Vector A casting a shadow onto vector B ](img/Figure_2.6_B16191.jpg)\n\n图 2.6:向量 A 投射阴影到向量 B 上\n\n要计算 *A* 到 *B* ( *投影* B *A* )的投影，向量 *A* 必须分解为相对于向量 *B* 的平行和垂直分量。平行分量是 *A* 在 *B* 方向上的长度——这是投影。垂直分量是从 *A* 中减去的平行分量——这是剔除:\n\n![Figure 2.7: Vector projection and rejection showing parallel and perpendicular vectors ](img/Figure_2.7_B16191.jpg)\n\n图 2.7:显示平行和垂直向量的向量投影和剔除\n\n如果被投影到的向量(在本例中，向量 *B* )是法向向量，那么求 *B* 方向上的 *A* 的长度就是 *A* 和 *B* 之间的简单点积。但是，如果两个输入向量都没有归一化，点积需要除以向量 *B* 的长度(投影到的向量)。\n\n现在 *A* 相对于 *B* 的平行分量是已知的，向量 *B* 可以通过这个分量来缩放。同样，如果 *B* 不是单位长度，结果将需要除以向量 *B* 的长度。\n\n拒绝是投射的对立面。要找到 *A* 到 *B* 的拒绝，从向量 *A* 中减去 *A* 到 *B* 的投影:\n\n1.  在`vec3.cpp`中实现`project`功能。别忘了把功能声明添加到`vec3.h` :\n\n    ```cpp\n    vec3 project(const vec3 &a, const vec3 &b) {\n        float magBSq = len(b);\n        if (magBSq < VEC3_EPSILON) {\n            return vec3();\n        }\n        float scale = dot(a, b) / magBSq;\n        return b * scale;\n    }\n    ```\n\n2.  在`vec3.cpp`中实现`reject`功能。别忘了在`vec3.h` :\n\n    ```cpp\n    vec3 reject(const vec3 &a, const vec3 &b) {\n        vec3 projection = project(a, b);\n        return a - projection;\n    }\n    ```\n\n    申报该功能\n\n向量投影和拒绝通常用于游戏编程。重要的是它们在健壮的向量库中实现。\n\n## 向量反射\n\n向量反射可以表示两种事物之一:镜像反射或反弹反射。下图显示了不同类型的反射:\n\n![Figure 2.8: A comparison of the mirror and bounce reflections ](img/Figure_2.8_B16191.jpg)\n\n图 2.8:反射镜和反射镜的比较\n\n反弹反射比镜面反射更有用、更直观。要进行反弹投影，请将向量 *A* 投影到向量 *B* 上。这将产生一个指向反射相反方向的向量。否定这个投影，从向量 a 中减去两次。下图演示了这一点:\n\n![Figure 2.9: Visualizing a bounce reflection ](img/Figure_2.9_B16191.jpg)\n\n图 2.9:可视化反弹反射\n\n在`vec3.cpp`中实现`reflect`功能。别忘了给`vec3.h`添加功能声明:\n\n```cpp\nvec3 reflect(const vec3 &a, const vec3 &b) {\n    float magBSq = len(b);\n    if (magBSq < VEC3_EPSILON) {\n        return vec3();\n    }\n    float scale = dot(a, b) / magBSq;\n    vec3 proj2 = b * (scale * 2);\n    return a - proj2;\n}\n```\n\n向量反射对物理和 AI 有用。我们不需要使用反射进行动画，但是如果需要的话，实现这个功能就好了。\n\n## 叉积\n\n当给定两个输入向量时，叉积返回垂直于两个输入向量的第三个向量。叉积的长度等于两个向量形成的平行四边形的面积。\n\n下图从视觉上展示了叉积的样子。输入向量不必相隔 90 度，但这样更容易可视化:\n\n![Figure 2.10: Visualizing the cross product ](img/Figure_2.10_B16191.jpg)\n\n图 2.10:可视化交叉产品\n\n寻找叉积涉及一些矩阵数学，这将在下一章更深入地讨论。现在，您需要创建一个 3x3 矩阵，上面一行是结果向量。第二行和第三行应该用输入向量填充。结果向量的每个分量的值是矩阵中该元素的次数值。\n\n3x3 矩阵中一个元素的次幂到底是多少？它是一个更小的 2x2 子矩阵的行列式。假设您想找到第一个分量的值，忽略第一行和第一列，这将产生一个较小的 2x2 子矩阵。下图显示了每个组件的较小子矩阵:\n\n![Figure 2.11: The submatrix for each component ](img/Figure_2.11_B16191.jpg)\n\n图 2.11:每个组件的子矩阵\n\n要找到 2x2 矩阵的行列式，需要交叉相乘。将左上角和右下角的元素相乘，然后减去右上角和左下角元素的乘积。下图显示了结果向量的每个元素:\n\n![Figure 2.12: The determinant of each component in the result vector ](img/Figure_2.12_B16191.jpg)\n\n图 2.12:结果向量中每个分量的行列式\n\n在`vec3.cpp`中实现`cross`产品。别忘了给`vec3.h`添加功能声明:\n\n```cpp\nvec3 cross(const vec3 &l, const vec3 &r) {\n    return vec3(\n        l.y * r.z - l.z * r.y,\n        l.z * r.x - l.x * r.z,\n        l.x * r.y - l.y * r.x\n    );\n}\n```\n\n点积与两个向量之间角度的余弦有关系，叉积与两个向量之间角度的正弦有关系。两个向量之间的叉积的长度为两个向量长度的乘积，用两个向量之间角度的正弦值表示:\n\n![](img/Formula_02_006.jpg)\n\n在下一节中，您将学习如何使用三种不同的技术在向量之间进行插值。\n\n# 插值向量\n\n通过缩放两个向量之间的差值并将结果加回原始向量，可以对两个向量进行线性插值。这种线性插值通常缩写为`lerp`。到`lerp`的量是介于 *0* 和 *1* 之间的归一化值；该归一化值通常由字母 *t* 表示。下图显示了两个向量之间的`lerp`以及 *t* 的几个值:\n\n![Figure 2.13: Linear interpolation ](img/Figure_2.13_B16191.jpg)\n\n图 2.13:线性插值\n\n当 *t = 0* 时，插值向量与起始向量相同。当 *t = 1* 时，插值向量与结束向量相同。\n\n在`vec3.cpp`中实现`lerp`功能。别忘了给`vec3.h`添加功能声明:\n\n```cpp\nvec3 lerp(const vec3 &s, const vec3 &e, float t) {\n    return vec3(\n        s.x + (e.x - s.x) * t,\n        s.y + (e.y - s.y) * t,\n        s.z + (e.z - s.z) * t\n    );\n}\n```\n\n在两个向量之间进行线性插值将总是采用从一个向量到另一个向量的最短路径。有时候，最短的路不是最好的路；相反，您可能需要沿着最短的弧在两个向量之间进行插值。在最短弧上插值称为球面线性插值(`slerp`)。下图显示了 *t* 的几个值的`slerp`和`lerp`过程之间的差异:\n\n![Figure 2.14: Comparing slerp and lerp ](img/Figure_2.14_B16191.jpg)\n\n图 2.14:比较 slerp 和 lerp\n\n要实现`slerp`，找到两个输入向量之间的角度。假设角度已知，`slerp`的公式如下\n\n![](img/Formula_02_007.jpg)\n\n在`vec3.cpp`中实现`slerp`功能。别忘了给`vec3.h`添加函数声明。注意当 *t* 的值接近 *0* 时，因为`slerp`会产生意想不到的结果。当 *t* 的值接近 *0* 时，退回到`lerp`或正常化 lerp ( `nlerp`)(接下来将介绍):\n\n```cpp\nvec3 slerp(const vec3 &s, const vec3 &e, float t) {\n    if (t < 0.01f) {\n        return lerp(s, e, t);\n    }\n    vec3 from = normalized(s);\n    vec3 to = normalized(e);\n    float theta = angle(from, to);\n    float sin_theta = sinf(theta);\n    float a = sinf((1.0f - t) * theta) / sin_theta;\n    float b = sinf(t * theta) / sin_theta;\n    return from * a + to * b;\n}\n```\n\n最后要覆盖的插值方法是`nlerp`。`nlerp`近似于`slerp`。与`slerp`不同，`nlerp`的速度不是恒定的。`nlerp`比`slerp`快很多，更容易实现；将`lerp`的结果正常化即可。下图比较了`lerp`、`slerp`和`nlerp`，其中 *t = 0.25* :\n\n![Figure 2.15: Comparing lerp, slerp, and nlerp ](img/Figure_2.15_B16191.jpg)\n\n图 2.15:比较 lerp、slerp 和 nlerp\n\n在`vec3.cpp`中实现`nlerp`功能。别忘了给`vec3.h`添加功能声明:\n\n```cpp\nvec3 nlerp(const vec3 &s, const vec3 &e, float t) {\n    vec3 linear(\n        s.x + (e.x - s.x) * t,\n        s.y + (e.y - s.y) * t,\n        s.z + (e.z - s.z) * t\n    );\n    return normalized(linear);\n}\n```\n\n一般来说`nlerp`是比`slerp`更好的选择。这是一个非常接近的近似值，而且计算起来要便宜得多。唯一有意义的使用`slerp`来代替的时间是如果需要恒定的插值速度。在本书中，你将使用`lerp`和`nlerp`在向量之间插值。\n\n在下一节中，您将学习如何使用ε值来比较等式和不等式的向量。\n\n# 比较向量\n\n需要实现的最后一个操作是向量比较。比较是一个组件式操作；必须使用ε来比较每个元素。测量两个向量是否相同的另一种方法是减去它们。如果它们相等，减去它们会得到一个没有长度的向量。\n\n让`vec3.cpp`的`==`和`!=`操作员超载。别忘了给`vec3.h`添加函数声明:\n\n```cpp\nbool operator==(const vec3 &l, const vec3 &r) {\n    vec3 diff(l - r);\n    return lenSq(diff) < VEC3_EPSILON;\n}\nbool operator!=(const vec3 &l, const vec3 &r) {\n    return !(l == r);\n}\n```\n\n重要提示:\n\n找到正确的ε值用于比较操作是困难的。在本章中，您将`0.000001f`声明为ε。这个值是一些反复试验的结果。要了解更多关于比较浮点值的信息，请查看[https://bitbashing.io/comparing-floats.html](https://bitbashing.io/comparing-floats.html)。\n\n在下一节中，您将实现两个和四个分量的向量。这些向量将仅用作存储数据的方便方式；他们实际上不需要在他们身上实现任何数学运算。\n\n# 探索更多向量\n\n在本书后面的某个时候，你也需要利用二分量和四分量向量。二分量向量和四分量向量不需要定义任何数学函数，因为它们将专门用作容器，用于将数据传递给图形处理器。\n\n与您实现的三分量向量不同，二分量向量和四分量向量需要同时作为整数向量和浮点向量存在。为了避免代码重复，这两种结构都将使用模板来实现:\n\n1.  创建一个新文件`vec2.h`，并添加`vec2`结构的定义。所有的`vec2`构造函数都是内联的；不需要`cpp`文件。`TVec2`结构是模板化的，`typedef`用于声明`vec2`和`ivec2` :\n\n    ```cpp\n    template<typename T>\n    struct TVec2 {\n        union {\n            struct {\n                T x;\n                T y;\n            };\n            T v[2];\n        };\n        inline TVec2() : x(T(0)), y(T(0)) { }\n        inline TVec2(T _x, T _y) :\n            x(_x), y(_y) { }\n        inline TVec2(T* fv) :\n            x(fv[0]), y(fv[1]) { }\n    };\n    typedef TVec2<float> vec2;\n    typedef TVec2<int> ivec2;\n    ```\n\n2.  同样，创建一个`vec4.h`文件，它将保存`vec4`结构:\n\n    ```cpp\n    template<typename T>\n    struct TVec4 {\n        union {\n            struct {\n                T x;\n                T y;\n                T z;\n                T w;\n            };\n            T v[4];\n        };\n        inline TVec4<T>(): x((T)0),y((T)0),z((T)0),w((T)0){}\n        inline TVec4<T>(T _x, T _y, T _z, T _w) :\n            x(_x), y(_y), z(_z), w(_w) { }\n        inline TVec4<T>(T* fv) :\n            x(fv[0]), y(fv[ ]), z(fv[2]), w(fv[3]) { }\n    };\n    typedef TVec4<float> vec4;\n    typedef TVec4<int> ivec4;\n    typedef TVec4<unsigned int> uivec4;\n    ```\n\n`vec2`、`ivec2`、`vec4`和`ivec4`结构的声明都与`vec3`结构的声明非常相似。所有这些结构都可以使用组件下标或作为指向内存线性数组的指针来访问。它们也都有类似的构造函数。\n\n# 总结\n\n在本章中，您已经学习了创建健壮动画系统所需的向量数学。动画是一个数学很重的话题；你在这一章学到的技能是完成这本书其余部分所必需的。您实现了三分量向量的所有常见向量运算。`vec2`和`vec4`结构不像`vec3`那样有完整的实现，但是它们只用于向 GPU 发送数据。\n\n在下一章中，您将通过学习矩阵来继续了解更多与游戏相关的数学知识。"
  },
  {
    "path": "docs/handson-cpp-game-ani-prog/03.md",
    "content": "# 三、实现矩阵\n\n在游戏动画的上下文中，矩阵表示仿射变换。它将点从一个空间线性映射到另一个空间。网格由顶点表示，顶点只是空间中的点。这些顶点通过乘以一个矩阵来移动。\n\n在本章中，您将学习矩阵数学以及如何用代码实现矩阵。到本章结束时，您将已经构建了一个健壮的矩阵库，可以在任何项目中使用。矩阵很重要；它们在图形管道中扮演着重要角色。不使用矩阵很难渲染任何东西。\n\n您只需要实现一个正方形的 4 x 4 矩阵。到本章结束时，您应该能够执行以下操作:\n\n*   理解什么是矩阵\n*   了解列主矩阵存储\n*   将矩阵相乘\n*   逆矩阵\n*   使用矩阵变换点和向量\n*   Understand how to create matrices to view a three-dimensional world\n\n    重要信息\n\n    在本章中，您将实现一个 4 x 4 矩阵。矩阵实现将依赖代码来演示概念，而不是通过数学定义的格式。如果你对矩阵背后的形式数学感兴趣，看看[https://gabormakesgames.com/matrices.html](https://gabormakesgames.com/matrices.html)。\n\n# 技术要求\n\n本章书籍的可下载资料中提供了两个示例。`Sample00`显示了本章中所写的矩阵代码。`Sample01`显示了使用显式低阶矩阵来实现矩阵反函数的替代实现。\n\n# 什么是矩阵？\n\n矩阵是数字的二维数组。正方形矩阵是宽度和高度相同的矩阵。在本章中，您将实现一个 4 x 4 矩阵；也就是有四行四列的矩阵。这个矩阵的元素将以线性阵列的形式存储。\n\n一个 4×4 的矩阵可以被认为是四个向量，每个向量有四个分量，或者是一个`vec4s`的数组。如果向量代表矩阵的列，则矩阵是列主矩阵。如果向量代表矩阵的行，则它是行主向量。\n\n假设一个 4×4 矩阵包含字母表中的字母 *A，B，C，D … P* ，它可以被构造成一个行或列主矩阵。这在下面的*图 3.1* 中有所展示:\n\n![Figure 3.1: Comparing row- and column-major matrices ](img/Figure_3.1_B16191.jpg)\n\n图 3.1:比较行和列主矩阵\n\n大多数数学书籍和 OpenGL 都使用列主矩阵。在本章中，您还将实现列主矩阵。理解矩阵中的内容很重要。矩阵的对角线包含缩放信息，最后一列包含平移:\n\n![Figure 3.2: What is stored in a matrix? ](img/Figure_3.2_B16191.jpg)\n\n图 3.2:矩阵中存储了什么？\n\n上面的 3×3 子矩阵包含三个向量；每一个都是矩阵旋转的基本向量。基本向量是存储在矩阵中的向上、向右和向前的方向。您可能已经注意到旋转和缩放组件在矩阵中占据相同的空间。\n\n## 矩阵存储\n\n现在您已经知道矩阵布局将是列矩阵，下一个问题是如何存储实际的矩阵。矩阵存储是一个令人困惑的话题。\n\n因为矩阵是以线性阵列的形式存储在内存中的，所以让我们弄清楚哪些元素应该放在哪里。行主矩阵一次一行地存储在存储器中。主列矩阵一次存储一列。\n\n因为行主矩阵和列主矩阵都包含相同的向量，所以最终的线性映射最终是相同的，与矩阵的主矩阵无关。下面的*图 3.3* 演示了这一点:\n\n![Figure 3.3: Matrix storage mapping to a linear array ](img/Figure_3.3_B16191.jpg)\n\n图 3.3:矩阵存储到线性阵列的映射\n\n您将要构建的矩阵类是具有列存储的列主矩阵；这意味着矩阵的物理内存布局与其元素的逻辑布局之间会有差异。将具有线性内存布局的矩阵视为行矩阵很容易，但请记住，这些行中的每一行实际上都是一列。\n\n重要说明\n\n二维网格到线性存储的典型映射是`\"row * numberOfColumns + column\"`。这种映射不适用于存储列主矩阵。当查看矩阵时，第 2 列第 3 行的元素应该具有 7 的线性索引，但是之前的映射得到了 14。相反，为了考虑列主存储，映射公式是`\"column * numberOfRows + row\"`。\n\n理解矩阵如何存储在内存中很重要，它将影响数据如何存储以及 API 如何访问这些数据。在下一节中，您将开始实现矩阵结构。\n\n# 创建矩阵\n\n在本节中，您将创建一个新的 4 x 4 矩阵。这个矩阵将被存储为一个 16 元素的浮点数组。联合将用于以易于使用的方式访问矩阵中的数据:\n\n重要说明\n\n单位矩阵是一种特殊的矩阵，它将任何东西乘以单位矩阵，得到原始矩阵。单位矩阵没有映射。恒等式矩阵除了主对角线之外的所有元素都包含 0，主对角线完全由 1 组成。\n\n1.  创建新文件，`mat4.h`。需要这个文件来声明`mat4`结构。\n2.  将以下结构声明添加到`mat4.h`中，它通过声明一个由 16 个元素组成的平面数组作为联合的第一个成员来启动联合:\n\n    ```cpp\n    struct mat4 {\n        union {\n            float v[16];\n    ```\n\n3.  联盟的下一个成员是`vec4`变量的结构。每个`vec4`变量代表矩阵的一列；它们以存储在这些列中的基向量命名:\n\n    ```cpp\n            struct {\n                vec4 right;\n                vec4 up;\n                vec4 forward;\n                vec4 position;\n            };\n    ```\n\n4.  基于基向量按元素访问成员可能很有用。以下结构包含命名对；第一个字母代表基向量，第二个字母代表该向量的分量:\n\n    ```cpp\n            struct { \n            //         row 1    row 2    row 3    row 4\n            /*col 1*/float xx;float xy;float xz;float xw;\n            /*col 2*/float yx;float yy;float yz;float yw;\n            /*col 3*/float zx;float zy;float zz;float zw;\n            /*col 4*/float tx;float ty;float tz;float tw;\n            };\n    ```\n\n5.  下一个结构将允许您使用列-行表示法访问矩阵:\n\n    ```cpp\n            struct {\n               float c0r0;float c0r1;float c0r2;float c0r3;\n               float c1r0;float c1r1;float c1r2;float c1r3;\n               float c2r0;float c2r1;float c2r2;float c2r3;\n               float c3r0;float c3r1;float c3r2;float c3r3;\n            };\n    ```\n\n6.  最终的结构将允许您使用行列符号来访问矩阵:\n\n    ```cpp\n            struct {\n               float r0c0;float r1c0;float r2c0;float r3c0;\n               float r0c1;float r1c1;float r2c1;float r3c1;\n               float r0c2;float r1c2;float r2c2;float r3c2;\n               float r0c3;float r1c3;float r2c3;float r3c3;\n            };\n        }; // End union\n    ```\n\n7.  添加一个`inline`构造函数来创建身份矩阵:\n\n    ```cpp\n        inline mat4() :\n           xx(1), xy(0), xz(0), xw(0),\n           yx(0), yy(1), yz(0), yw(0),\n           zx(0), zy(0), zz(1), zw(0),\n           tx(0), ty(0), tz(0), tw(1) {}\n    ```\n\n8.  添加一个`inline`构造函数，它将从一个浮点数组中创建一个矩阵:\n\n    ```cpp\n        inline mat4(float *fv) :\n           xx( fv[0]), xy( fv[1]), xz( fv[2]), xw( fv[3]),\n           yx( fv[4]), yy( fv[5]), yz( fv[6]), yw( fv[7]),\n           zx( fv[8]), zy( fv[9]), zz(fv[10]), zw(fv[11]),\n           tx(fv[12]), ty(fv[13]), tz(fv[14]), tw(fv[15]) { }\n    ```\n\n9.  添加一个`inline`构造函数，通过指定矩阵中的每个元素来创建矩阵:\n\n    ```cpp\n        inline mat4(\n            float _00, float _01, float _02, float _03,\n            float _10, float _11, float _12, float _13,\n            float _20, float _21, float _22, float _23,\n            float _30, float _31, float _32, float _33) :\n            xx(_00), xy(_01), xz(_02), xw(_03),\n            yx(_10), yy(_11), yz(_12), yw(_13),\n            zx(_20), zy(_21), zz(_22), zw(_23),\n            tx(_30), ty(_31), tz(_32), tw(_33) { }\n    }; // end mat4 struct\n    ```\n\n你刚才声明的矩阵结构是最终的`mat4`结构；匿名联合提供了五种不同的访问矩阵数据的方式。矩阵数据可以作为平面数组、四列存储为`vec4`或三种助记符之一来访问。这三个助记符使用它们的基本向量、它们的行和列或它们的列和行来命名元素。\n\n接下来，您将开始处理在`mat4`结构上运行的函数。您将实现常见的矩阵操作，如添加、缩放和乘法矩阵，并了解如何使用矩阵来变换向量和点。\n\n# 常用矩阵运算\n\n在本节中，您将学习如何实现一些常见的矩阵运算。这些操作将在本书后面的章节中用于显示动画模型。具体来说，本节将介绍如何比较、相加、缩放和相乘矩阵，以及如何使用矩阵变换向量和点。\n\n## 比较矩阵\n\n比较矩阵是一项组件式操作。只有当两个矩阵的所有成分都相同时，它们才是相同的。要比较两个矩阵，循环遍历并比较它们的所有组成部分。由于您正在比较浮点数，所以应该使用ε。\n\n创建新文件，`mat4.cpp`。在此文件中实现矩阵等式和不等式运算符。等式运算符应该检查两个矩阵是否相同；不等式运算符返回等式运算符的反义词。别忘了给`mat4.h`添加函数声明:\n\n```cpp\nbool operator==(const mat4& a, const mat4& b) {\n    for (int i = 0; i < 16; ++ i) {\n        if (fabsf(a.v[i] - b.v[i]) > MAT4_EPSILON) {\n            return false;\n        }\n    }\n    return true;\n}\nbool operator!=(const mat4& a, const mat4& b) {\n    return !(a == b);\n}\n```\n\n重要说明\n\n`MAT4_EPSILON`常数应在`mat4.h`中定义。`0.000001f`是一个很好的使用默认值。\n\n当按组件比较矩阵时，您正在检查文字相等性。还有其他定义矩阵等式的方法；例如，无论形状如何，都可以使用两个矩阵的行列式来比较它们的体积。矩阵行列式将在本章后面讨论。\n\n在下一节中，您将学习如何将矩阵相加。\n\n## 添加矩阵\n\n两个矩阵可以通过分量相加。要将两个矩阵相加，请将它们各自的分量相加，并将结果存储在新的矩阵中。矩阵加法可以与标量乘法一起使用，在多个矩阵之间进行插值或混合。稍后，您将学习如何使用此属性实现动画蒙皮。\n\n在`mat4.cpp`中实现矩阵加法功能。别忘了给`mat4.h`添加函数声明:\n\n```cpp\nmat4 operator+(const mat4& a, const mat4& b) {\n    return mat4(\n        a.xx+b.xx, a.xy+b.xy, a.xz+b.xz, a.xw+b.xw,\n        a.yx+b.yx, a.yy+b.yy, a.yz+b.yz, a.yw+b.yw,\n        a.zx+b.zx, a.zy+b.zy, a.zz+b.zz, a.zw+b.zw,\n        a.tx+b.tx, a.ty+b.ty, a.tz+b.tz, a.tw+b.tw\n    );\n}\n```\n\n矩阵添加很简单，但它将在显示动画网格中发挥重要作用。在下一节中，您将学习如何通过标量值来缩放矩阵。\n\n## 缩放矩阵\n\n矩阵可以用浮点数来缩放；这种缩放是组件式操作。要缩放矩阵，请将每个元素乘以提供的浮点数。\n\n在`mat4.cpp`中实现矩阵缩放。别忘了给`mat4.h`添加函数声明:\n\n```cpp\nmat4 operator*(const mat4& m, float f) {\n    return mat4(\n        m.xx * f, m.xy * f, m.xz * f, m.xw * f,\n        m.yx * f, m.yy * f, m.yz * f, m.yw * f,\n        m.zx * f, m.zy * f, m.zz * f, m.zw * f,\n        m.tx * f, m.ty * f, m.tz * f, m.tw * f\n    );\n}\n```\n\n缩放矩阵，然后将它们相加，可以让你在两个矩阵之间“学习”或“混合”，只要两个矩阵都表示线性变换。在下一节中，您将学习如何将矩阵相乘。\n\n## 矩阵乘法\n\n矩阵乘法将两个矩阵的变换合并成一个矩阵。两个矩阵只有在内维相同的情况下才能相乘。以下是一些例子:\n\n*   一个 4 x **4** 和一个 **4** x 4 矩阵可以相乘，因为两个内部尺寸都是 4。\n*   一个 4 x **4** 和一个 **4** x 1 矩阵可以相乘，因为两个内部尺寸都是 4。\n*   一个 4 x **4** 和一个 **1** x 4 矩阵不能相乘，因为内部尺寸 4 和 1 不匹配。\n\n矩阵乘法得到的矩阵将矩阵的外部维数相乘。以下是一个例子:\n\n*   一个 **4** x 4 和一个 **4** 矩阵将产生一个 4 x 4 矩阵。\n*   一个 **4** x 4 和一个 4 x **1** 矩阵将产生一个 4 x 1 矩阵。\n*   一个 **1** x 4 和一个 4 x **2** 矩阵将产生一个 1 x 2 矩阵。\n\n假设有两个矩阵， *A* 和 *B* 。矩阵 *A* 在 *X* 轴上平移 10 个单位。矩阵 *B* 绕 *Y* 轴旋转 30 度。如果要相乘的矩阵为 *A * B* ，得到的矩阵将围绕 *Y* 轴旋转 30 度，然后在 *X* 轴上平移 10 个单位。\n\n矩阵乘法不是累积的。考虑最后一个例子，而是乘以 *B * A* 。将 *B * A* 相乘时，生成的矩阵将在 *X* 轴上平移 10 个单位，然后围绕 *Y* 轴旋转 30 度。乘法顺序很重要； *A * B* 和 *B * A* 不一样。\n\n这就提出了一个新问题——矩阵应该以什么顺序相乘？如果 *M = A * B * C* ，那么这些矩阵按什么顺序串联？ *A* 、 *B* ，然后 *C* 或 *C* 、 *B* ，然后 *A* ？如果是 *A* 、 *B* ，然后是 *C* ，矩阵乘法定义为从左到右。但如果是 *C* 、 *B* ，然后是 *A* ，矩阵乘法是从右向左的。\n\n为了与 OpenGL 保持一致，在本章中，您将实现从右向左乘法。但是两个矩阵如何相乘呢？矩阵的每个元素都有一行和一列。任何元素的结果值都是左矩阵中该行和右矩阵中该列的点积。\n\n例如，假设您想在两个矩阵相乘时找到第 2 行第 3 列中元素的值。这意味着从左侧矩阵取第 2 行的点积，从右侧矩阵取第 3 列的点积。*图 3.4* 演示了这一点:\n\n![Figure 3.4: Multiplying matrices ](img/Figure_3.4_B16191.jpg)\n\n图 3.4:乘法矩阵\n\n在上图中，您可能已经注意到，尽管矩阵是以列为主，但元素的下标首先显示为行，然后显示为列。下标引用矩阵的物理拓扑；它与矩阵中存储的内容或矩阵的布局无关。下标指数保持不变，不管矩阵是什么主。执行以下步骤来实现矩阵乘法:\n\n1.  为了缩短乘法矩阵的代码，您需要创建一个助手宏。这个宏将假设有两个矩阵，`a`和`b`。宏将取两个数字，`a`行，`b`列，点在一起，结果赌两者的点积。在`mat4.cpp` :\n\n    ```cpp\n    #define M4D(aRow, bCol) \\\n        a.v[0 * 4 + aRow] * b.v[bCol * 4 + 0] + \\\n        a.v[1 * 4 + aRow] * b.v[bCol * 4 + 1] + \\\n        a.v[2 * 4 + aRow] * b.v[bCol * 4 + 2] + \\\n        a.v[3 * 4 + aRow] * b.v[bCol * 4 + 3]\n    ```\n\n    中定义`M4D`宏\n2.  在`M4D`宏就位后，在`mat4.cpp`中实现矩阵乘法功能。别忘了给`mat4.h`添加函数声明。请记住，例如`(2, 1)`元素应该取矩阵`a`中行`2`和矩阵`b`列`1`的点积:\n\n    ```cpp\n    mat4 operator*(const mat4 &a, const mat4 &b) {\n       return mat4(\n          M4D(0,0), M4D(1,0), M4D(2,0), M4D(3,0),//Col 0\n          M4D(0,1), M4D(1,1), M4D(2,1), M4D(3,1),//Col 1\n          M4D(0,2), M4D(1,2), M4D(2,2), M4D(3,2),//Col 2\n          M4D(0,3), M4D(1,3), M4D(2,3), M4D(3,3) //Col 3\n       );\n    }\n    ```\n\n矩阵乘法最重要的特性是它将两个矩阵中编码的变换组合成一个矩阵。这很有用，因为您可以预乘某些矩阵，以减少每帧的乘法次数。接下来，您将了解矩阵如何将其变换数据应用于向量和点。\n\n# 变换向量和点\n\n转换点和向量的方式与乘法矩阵相同。事实上，被变换的向量可以被认为是一个 4 列 1 行的矩阵。这意味着变换向量是一个 4×4 和 4×1 矩阵相乘的问题。\n\n当矩阵变换向量时，它会影响向量的方向和比例。当矩阵变换一个点时，它只是在空间中平移该点。那么，向量和点有什么区别呢？向量的 *w* 分量是 *0* ，点的 *W* 分量是 *1* 。以下步骤将指导您实现矩阵向量乘法:\n\n1.  为了使矩阵向量乘法更容易理解，您需要再次创建一个宏。这个宏将取一个矩阵的行，并对提供的列向量执行该行的点积。在`mat4.cpp` :\n\n    ```cpp\n    #define M4V4D(mRow, x, y, z, w) \\\n        x * m.v[0 * 4 + mRow] + \\\n        y * m.v[1 * 4 + mRow] + \\\n        z * m.v[2 * 4 + mRow] + \\\n        w * m.v[3 * 4 + mRow]\n    ```\n\n    中执行`M4VD`宏\n2.  在`M4V4D`宏就位后，在`mat4.cpp`中实现矩阵向量乘法功能。别忘了给`mat4.h` :\n\n    ```cpp\n    vec4 operator*(const mat4& m, const vec4& v) {\n        return vec4(\n            M4V4D(0, v.x, v.y, v.z, v.w),\n            M4V4D(1, v.x, v.y, v.z, v.w),\n            M4V4D(2, v.x, v.y, v.z, v.w),\n            M4V4D(3, v.x, v.y, v.z, v.w) \n        );\n    }\n    ```\n\n    添加功能定义\n3.  本书中的大部分数据将存储为三分量向量，而不是四分量向量。不需要每次需要通过矩阵变换时都创建一个新的四分量向量；相反，您将为这种情况创建一个专门的函数。\n4.  在`mat4.cpp`中定义新功能:`transformVector`。别忘了给`mat4.h`添加函数声明。该函数将采用`vec3`并使用提供的矩阵对其进行变换，假设向量代表方向和大小:\n\n    ```cpp\n    vec3 transformVector(const mat4& m, const vec3& v) {\n        return vec3(\n            M4V4D(0, v.x, v.y, v.z, 0.0f),\n            M4V4D(1, v.x, v.y, v.z, 0.0f),\n            M4V4D(2, v.x, v.y, v.z, 0.0f) \n        );\n    }\n    ```\n\n5.  接下来，在`mat4.cpp`中定义`transformPoint`功能。它应该将向量和矩阵相乘，假设向量的 W 分量是 1:\n\n    ```cpp\n    vec3 transformPoint(const mat4& m, const vec3& v) {\n        return vec3(\n            M4V4D(0, v.x, v.y, v.z, 1.0f),\n            M4V4D(1, v.x, v.y, v.z, 1.0f),\n            M4V4D(2, v.x, v.y, v.z, 1.0f)\n        );\n    }\n    ```\n\n6.  为`transformPoint`定义一个过载，该过载需要一个额外的 *W* 组件。 *W* 组件是一个引用—它是一个读写组件。执行该功能后，如果输入向量为`vec4` :\n\n    ```cpp\n    vec3 transformPoint(const mat4& m, const vec3& v, float& w) {\n        float _w = w;\n        w = M4V4D(3, v.x, v.y, v.z, _w);\n        return vec3(\n            M4V4D(0, v.x, v.y, v.z, _w),\n            M4V4D(1, v.x, v.y, v.z, _w),\n            M4V4D(2, v.x, v.y, v.z, _w)\n        );\n    }\n    ```\n\n    ，则 *w* 组件保存 *W* 的值\n\n在本书的其余部分，大多数数据存储在`vec3`结构中。这意味着将使用`transformVector`和`transformPoint`，而不是重载的乘法运算符。这应该有助于减少关于被转换的数据是什么的模糊性。接下来，您将学习如何反转矩阵。\n\n# 矩阵求逆\n\n矩阵乘以它的逆矩阵总是会得到单位矩阵。逆矩阵与非逆矩阵的映射相反。不是所有的矩阵都有逆矩阵。只有行列式非零的矩阵才能求逆。\n\n矩阵求逆是一个重要的操作；用于变换要在屏幕上显示的三维对象的视图矩阵是相机位置和旋转的倒数。倒排矩阵变得重要的另一个地方是蒙皮，将在 [*第 10 章*](10.html#_idTextAnchor167) *【网格蒙皮】*中介绍。\n\n求矩阵的逆相当复杂，因为它需要其他支持函数(如转置和调整)。在本节中，您将首先构建这些支持函数，然后在它们全部构建完成后构建反函数。首先，我们需要转置矩阵。\n\n## 转置\n\n若要转置矩阵，请将矩阵的每个元素翻转到主对角线上。例如， *2，1* 元素将变成 *1，2* 元素。两个下标相同的元素，如 *1，1* ，将保持不变:\n\n1.  在`mat4.cpp`中实现`transpose`功能。别忘了把功能声明添加到`mat4.h` :\n\n    ```cpp\n    #define M4SWAP(x, y) \\\n        {float t = x; x = y; y = t; }\n    void transpose(mat4 &m) {\n        M4SWAP(m.yx, m.xy);\n        M4SWAP(m.zx, m.xz);\n        M4SWAP(m.tx, m.xw);\n        M4SWAP(m.zy, m.yz);\n        M4SWAP(m.ty, m.yw);\n        M4SWAP(m.tz, m.zw);\n    }\n    ```\n\n2.  在`mat4.cpp`中创建`transposed`功能。`transposed`函数修改传递给它的矩阵。别忘了给`mat4.h` :\n\n    ```cpp\n    mat4 transposed(const mat4 &m) {\n        return mat4(\n            m.xx, m.yx, m.zx, m.tx,\n            m.xy, m.yy, m.zy, m.ty,\n            m.xz, m.yz, m.zz, m.tz,\n            m.xw, m.yw, m.zw, m.tw\n        );\n    }\n    ```\n\n    添加功能声明\n\n如果您需要将矩阵从行主转换为列主，或者反过来转换，那么转换矩阵非常有用。在下一节中，你将学习如何计算方阵的行列式。\n\n## 低阶矩阵的行列式和次行列式\n\n要找到 4×4 矩阵的行列式，首先了解低阶矩阵的行列式和次行列式是什么是很重要的。行列式函数是递归的；为了求一个 4×4 矩阵的行列式，我们还需要求几个 3×3 和 2×2 矩阵的行列式。\n\n矩阵的行列式总是标量值；只有方阵才有行列式。如果矩阵被转置，矩阵的行列式保持不变。\n\n在接下来的几节中，你将学习如何找到 2×2 矩阵的行列式，任何大小矩阵的子矩阵，以及任何大小矩阵的余因子。这些方法是拉普拉斯展开的构造块，您将使用它们来寻找任何大小矩阵的行列式。\n\n### 2×2 行列式\n\n要求 2×2 矩阵的行列式，就要减去对角元素的乘积。下图说明了这一点:\n\n![Figure 3.5: A 2 x 2 matrix and the formula for the determinant ](img/Figure_3.5_B16191.jpg)\n\n图 3.5:一个 2×2 矩阵和行列式公式\n\n### 较小的\n\n矩阵中的每个元素都有一个次元素。元素的次元素是一个小矩阵的行列式，它去掉了元素的行和列。例如，考虑一个 3×3 的矩阵——元素 *2，1* 的次数值是多少？\n\n首先，从矩阵中删除第 2 行和第 1 列。这将导致更小的 2×2 矩阵。这个 2×2 矩阵的行列式是元素 *2，1* 的次项。下图说明了这一点:\n\n![Figure 3.6: The minor of element 2, 1 in a 3 x 3 matrix ](img/Figure_3.6_B16191.jpg)\n\n图 3.6:3×3 矩阵中元素 2，1 的次元素\n\n这个公式也适用于高维矩阵。例如，4×4 矩阵中一个元素的次元素是一些较小的 3×3 矩阵的行列式。次元素矩阵是一个矩阵，其中每个元素都是输入矩阵中相应元素的次元素。\n\n### 辅因子\n\n要找到矩阵的辅因子，首先要计算未成年人的矩阵。未成年人矩阵已知后，将矩阵中的每一个元素 *(i，j)* 乘以 *-1* 到 *i + j* 的幂。Add -1(i+j) 功率的值形成了一个方便的棋盘图案， *+* 始终位于左上角:\n\n![Figure 3.7: A checkerboard pattern of -1 to the i + j power ](img/Figure_3.7_B16191.jpg)\n\n图 3.7:I+j 次方为-1 的棋盘图案\n\n上图显示了 Add -1(i+j)创建的棋盘模式。请注意，模式总是从左上角的正元素开始。\n\n### 拉普拉斯展开\n\n任何方阵的行列式(如果有的话)都可以通过拉普拉斯展开来求。要执行此操作，首先要找到辅助因子矩阵。接下来，将原始矩阵第一行中的每个元素乘以辅助因子矩阵第一行中的对应元素。行列式是这些乘法的总和:\n\n![](img/Formula_03_001.jpg)\n\n### 恳求\n\n在你能反转一个矩阵之前，最后的操作是找到一个矩阵的下标。矩阵的附属是辅因子矩阵的转置。实现 adjugate 很简单，因为您已经知道如何找到矩阵的辅因子以及如何转置矩阵。\n\n### 相反的\n\n要求矩阵的逆，用矩阵的行列式除矩阵的辅助项。由于标量矩阵除法没有定义，您需要将 adjugate 乘以行列式的倒数。\n\n重要说明\n\n在本章中，您将构建一个矩阵乘法函数，该函数使用宏来避免对低阶矩阵的需要。本书可下载资料中的`Chapter03/Sample01`示例提供了一个利用低阶矩阵的实现，并且更容易通过调试器来完成。\n\n要实现一个矩阵反函数，你首先需要能够找到一个 4×4 矩阵的行列式和下标。这两个函数都依赖于能够在矩阵中找到元素的次元素:\n\n1.  在`mat4.cpp`中创建新的宏。这个宏将找到矩阵中一个元素的次元素，给定一个浮点数组，从矩阵中剪切出三行三列:\n\n    ```cpp\n    #define M4_3X3MINOR(x, c0, c1, c2, r0, r1, r2) \\\n       (x[c0*4+r0]*(x[c1*4+r1]*x[c2*4+r2]-x[c1*4+r2]* \\\n       x[c2*4+r1])-x[c1*4+r0]*(x[c0*4+r1]*x[c2*4+r2]- \\\n       x[c0*4+r2]*x[c2*4+r1])+x[c2*4+r0]*(x[c0*4+r1]* \\\n       x[c1*4+r2]-x[c0*4+r2]*x[c1*4+r1]))\n    ```\n\n2.  定义`M4_3X3MINOR`宏后，在`mat4.cpp`中实现`determinant`功能。由于行列式会将每个元素乘以辅助因子，因此需要否定一些值。别忘了给`mat4.h` :\n\n    ```cpp\n    float determinant(const mat4& m) {\n       return  m.v[0] *M4_3X3MINOR(m.v, 1, 2, 3, 1, 2, 3)  \n             - m.v[4] *M4_3X3MINOR(m.v, 0, 2, 3, 1, 2, 3)  \n             + m.v[8] *M4_3X3MINOR(m.v, 0, 1, 3, 1, 2, 3)  \n             - m.v[12]*M4_3X3MINOR(m.v, 0, 1, 2, 1, 2, 3); \n    }\n    ```\n\n    添加功能声明\n3.  接下来，执行`mat4.cpp`中的`adjugate`功能。别忘了给`mat4.h`添加函数声明。使用`M4_3X3MINOR`宏找到未成年人矩阵，然后否定合适的元素，创建辅因子矩阵。最后，返回辅因子矩阵的转置:\n\n    ```cpp\n    mat4 adjugate(const mat4& m) {\n       //Cof (M[i, j]) = Minor(M[i, j]] * pow(-1, i + j)\n       mat4 cofactor;\n       cofactor.v[0] = M4_3X3MINOR(m.v, 1, 2, 3, 1, 2, 3);\n       cofactor.v[1] =-M4_3X3MINOR(m.v, 1, 2, 3, 0, 2, 3);\n       cofactor.v[2] = M4_3X3MINOR(m.v, 1, 2, 3, 0, 1, 3);\n       cofactor.v[3] =-M4_3X3MINOR(m.v, 1, 2, 3, 0, 1, 2);\n       cofactor.v[4] =-M4_3X3MINOR(m.v, 0, 2, 3, 1, 2, 3);\n       cofactor.v[5] = M4_3X3MINOR(m.v, 0, 2, 3, 0, 2, 3);\n       cofactor.v[6] =-M4_3X3MINOR(m.v, 0, 2, 3, 0, 1, 3);\n       cofactor.v[7] = M4_3X3MINOR(m.v, 0, 2, 3, 0, 1, 2);\n       cofactor.v[8] = M4_3X3MINOR(m.v, 0, 1, 3, 1, 2, 3);\n       cofactor.v[9] =-M4_3X3MINOR(m.v, 0, 1, 3, 0, 2, 3);\n       cofactor.v[10]= M4_3X3MINOR(m.v, 0, 1, 3, 0, 1, 3);\n       cofactor.v[11]=-M4_3X3MINOR(m.v, 0, 1, 3, 0, 1, 2);\n       cofactor.v[12]=-M4_3X3MINOR(m.v, 0, 1, 2, 1, 2, 3);\n       cofactor.v[13]= M4_3X3MINOR(m.v, 0, 1, 2, 0, 2, 3);\n       cofactor.v[14]=-M4_3X3MINOR(m.v, 0, 1, 2, 0, 1, 3);\n       cofactor.v[15]= M4_3X3MINOR(m.v, 0, 1, 2, 0, 1, 2);\n       return transposed(cofactor);\n    }\n    ```\n\n4.  现在`determinant`和`adjugate`功能已经完成，4×4 矩阵的`inverse`功能的实现应该很简单。在`mat4.cpp`实现`inverse`功能。别忘了给`mat4.h`加上功能声明:\n\n    ```cpp\n    mat4 inverse(const mat4& m) {\n        float det = determinant(m);\n\n        if (det == 0.0f) {\n            cout << \" Matrix determinant is 0\\n\";\n            return mat4();\n        }\n        mat4 adj = adjugate(m);\n        return adj * (1.0f / det);\n    }\n    ```\n\n5.  `inverse`函数采用一个常数矩阵引用，并返回一个新的矩阵，该矩阵是所提供矩阵的逆矩阵。在`mat4.cpp`实现`invert`便利功能。这个便利函数将内联反转矩阵，修改参数。别忘了给`mat4.h` :\n\n    ```cpp\n    void invert(mat4& m) {\n        float det = determinant(m);\n        if (det == 0.0f) {\n            std::cout << \"Matrix determinant is 0\\n\";\n            m = mat4();\n            return;\n        }\n        m = adjugate(m) * (1.0f / det);\n    }\n    ```\n\n    添加功能声明\n\n矩阵求逆是一个相对昂贵的功能。只对位置和旋转进行编码的矩阵可以更快地进行反转，因为 3×3 旋转矩阵的反转与其转置相同。\n\n在下一节实现`lookAt`函数时，你将学习如何实现这个快速逆。\n\n# 创建摄像机矩阵\n\n矩阵也用于相机变换，包括透视变换。透视变换将平截头体映射到 NDC 空间。NDC 空间通常在所有轴上都有-1 到+1 的范围。与世界/眼睛坐标不同，NDC 空间是左撇子。\n\n在本节中，您将学习如何创建相机变换矩阵。第一个相机矩阵是一个平截头体，看起来像一个尖端被切掉的金字塔。平截头体代表摄像机可见的一切。您还将学习如何创建不同的投影，以及如何实现“查看”功能，以便轻松创建视图矩阵。\n\n## 平截头体\n\n视觉上，平截头体看起来像一个顶端被切掉的金字塔。平截头体有六条边；它代表了相机可以看到的空间。在`mat4.cpp`中创建`frustum`功能。该函数采用左、右、底、顶、近和远值:\n\n```cpp\nmat4 frustum(float l, float r, float b, \n             float t, float n, float f) {\n    if (l == r || t == b || n == f) {\n        std::cout << \"Invalid frustum\\n\";\n        return mat4(); // Error\n    }\n    return mat4(\n        (2.0f * n) / (r - l),0, 0, 0,\n        0,  (2.0f * n) / (t - b), 0, 0,\n        (r+l)/(r-l), (t+b)/(t-b), (-(f+n))/(f-n), -1,\n        0, 0, (-2 * f * n) / (f - n), 0\n    );\n}\n```\n\n重要说明\n\n推导平截头体矩阵的细节超出了本书的范围。有关如何导出函数的更多信息，请查看[http://www.songho.ca/opengl/gl_projectionmatrix.html](http://www.songho.ca/opengl/gl_projectionmatrix.html)。\n\n`frustum`函数可以用来构造视平截头体，但函数参数不直观。在下一节中，您将学习如何从更直观的参数创建视图截锥。\n\n## 视角\n\n透视矩阵是根据视野(通常以度为单位)、纵横比以及远近距离构建的。这是一种创建视图平截头体的简单方法。\n\n在`mat4.cpp`中实现`perspective`功能。别忘了给`mat4.h`添加功能声明:\n\n```cpp\nmat4 perspective(float fov, float aspect, float n,float f){\n    float ymax = n * tanf(fov * 3.14159265359f / 360.0f);\n    float xmax = ymax * aspect;\n    return frustum(-xmax, xmax, -ymax, ymax, n, f);\n}\n```\n\n`perspective`功能将在本书剩余部分的几乎所有视觉图形演示中使用。这是创建视图平截头体的一种非常方便的方法。\n\n## 正字法\n\n正投影没有透视效果。正交投影线性映射到 NDC 空间。正交投影常用于二维游戏。它经常被用来实现等角透视。\n\n在`mat4.cpp`中实现`ortho`功能。别忘了给`mat4.h`添加功能声明:\n\n```cpp\nmat4 ortho(float l, float r, float b, float t, \n           float n, float f) {\n    if (l == r || t == b || n == f) {\n        return mat4(); // Error\n    }\n    return mat4(\n        2.0f / (r - l), 0, 0, 0,\n        0, 2.0f / (t - b), 0, 0,\n        0, 0, -2.0f / (f - n), 0,\n        -((r+l)/(r-l)),-((t+b)/(t-b)),-((f+n)/(f-n)), 1\n    );\n}\n```\n\n正交视图投影通常用于显示用户界面或其他二维元素。\n\n## 看\n\n视图矩阵是相机变换(相机的位置、旋转和缩放)的逆矩阵。您将实现一个直接生成该矩阵的`lookAt`函数，而不是必须创建摄像机的变换矩阵，然后将其反转。\n\n一个`lookAt`功能通常需要一个`position`，摄像机正在观察的`target point`和一个参考`up direction`。剩下的工作是找到反转的基向量，并计算出位置。\n\n因为基向量是正交的，所以它们的逆与它们的转置相同。位置可以通过将位置列向量的点积与反转的基向量相减来计算。\n\n在`mat4.cpp`中实现`lookAt`功能。别忘了给`mat4.h`添加函数声明。请记住，视图矩阵将游戏世界映射到正的 *Z* 轴:\n\n```cpp\nmat4 lookAt(const vec3& position, const vec3& target, \n            const vec3& up) {\n    vec3 f = normalized(target - position) * -1.0f;\n    vec3 r = cross(up, f); // Right handed\n    if (r == vec3(0, 0, 0)) {\n        return mat4(); // Error\n    }\n    normalize(r);\n    vec3 u = normalized(cross(f, r)); // Right handed\n    vec3 t = vec3(\n        -dot(r, position),\n        -dot(u, position),\n        -dot(f, position)\n    );\n    return mat4(\n        // Transpose upper 3x3 matrix to invert it\n        r.x, u.x, f.x, 0,\n        r.y, u.y, f.y, 0,\n        r.z, u.z, f.z, 0,\n        t.x, t.y, t.z, 1\n    );\n}\n```\n\n`lookAt`函数是构建视图矩阵最方便的方式。本书其余部分的所有代码示例都将使用`lookAt`功能来设置视图矩阵。\n\n# 总结\n\n在本章中，您学习了处理四维方阵所需的数学知识，并实现了一个可重用的矩阵库。矩阵通常用于编码变换信息；它们几乎在图形流水线的每一步都被用来在屏幕上显示模型。\n\n在下一章中，您将学习使用四元数对旋转数据进行编码。"
  },
  {
    "path": "docs/handson-cpp-game-ani-prog/04.md",
    "content": "# 四、实现四元数\n\n在这一章中，你将学习四元数。四元数用于编码旋转。四元数是以*x*I*+y*j*+z*k*+w*形式表示的复数。想起 *i、* *j* ，\n\n和 *k* 作为占位符，每个占位符代表一个三维轴。 *w* 是一个实数。虽然四元数不直接编码角度轴对，但很容易想到它们\n\n就像那样——绕任意轴的旋转。\n\n到本章结束时，您应该对什么是四元数以及如何使用它们有了很好的理解，并且您已经在代码中实现了一个健壮的四元数类。本章将涵盖以下主题:\n\n*   创建四元数的不同方法\n*   检索四元数的角度和轴\n*   基本组件式操作\n*   两个四元数的长度和点积\n*   反转四元数\n*   组合四元数\n*   用四元数变换向量\n*   四元数之间的插值\n*   转换四元数和矩阵\n\n为什么四元数很重要？大多数人形动画只使用旋转来创建，不需要平移或缩放。例如，想想肘关节。肘部的自然运动只是旋转。如果你想平移肘部通过空间，你旋转肩膀。四元数对旋转进行编码，插值效果很好。\n\n重要信息:\n\n在本章中，您将使用直观的代码优先方法实现四元数。如果你对四元数背后更正式的数学感兴趣，看看[https://gabormakesgames.com/quaternions.html](https://gabormakesgames.com/quaternions.html)。\n\n# 创建四元数\n\n四元数用于编码旋转数据。在代码中，四元数将有四个组成部分。它们类似于`vec4`，因为它们有`x`、`y`、`z`和`w`成分。\n\n和`vec4`一样，`w`组件排在最后。\n\n`quat`结构应该有两个构造函数。默认构造函数创建一个单位四元数`(0, 0, 0, 1)`。`(0, 0, 0, 1)`的身份四元数就像`1`。任何数字乘以`1`都保持不变。同样，任何四元数乘以单位四元数都保持不变:\n\n创建一个新文件`quat.h`，来声明四元数结构。`quat`结构将在本书的其余部分中用来表示旋转:\n\n```cpp\n#ifndef _H_QUAT_\n#define _H_QUAT_\n#include \"vec3.h\"\n#include \"mat4.h\"\nstruct quat {\n   union {\n       struct {\n           float x;\n           float y;\n           float z;\n           float w;\n       };\n       struct {\n           vec3 vector;\n           float scalar;\n       };\n       float v[4];\n   };\n   inline quat() :\n       x(0), y(0), z(0), w(1) { }\n   inline quat(float _x, float _y, float _z, float _w)\n               : x(_x), y(_y), z(_z), w(_w) {}\n};\n#endif\n```\n\n`quat`结构中的匿名联合将允许您通过`X`、`Y`、`Z`和`W`下标符号，以向量和标量对或浮点值数组的形式访问四元数中的数据。\n\n接下来，你将学习如何开始创建四元数。\n\n## 角轴\n\n四元数通常使用旋转轴和角度来创建。通过 *θ* 绕轴的旋转可以在球体上表示为，其长度在垂直于旋转轴的平面上为![](img/Formula_04_001.png)。正角度产生围绕轴的逆时针旋转。\n\n创建新文件，`quat.cpp`。在`quat.cpp`实现`angleAxis`功能。别忘了给`quat.h`添加功能声明:\n\n```cpp\n#include \"quat.h\"\n#include <cmath>\nquat angleAxis(float angle, const vec3& axis) {\n    vec3 norm = normalized(axis);\n    float s = sinf(angle * 0.5f);\n    return quat(norm.x * s,\n                norm.y * s,\n                norm.z * s,\n                cosf(angle * 0.5f)\n    );\n}\n```\n\n为什么![](img/Formula_04_002.png)？一个四元数可以跟踪两个完整的旋转，也就是 *720* 度。这使得四元数*的周期为 720* 度。sin/cos 的周期为 *360* 度。将 *θ* 除以 *2* 将四元数的范围映射到 sin/cos 的范围。\n\n在本节中，您学习了旋转的角度和轴是如何在中编码的\n\n四元数。在下一节中，您将学习如何构建角度和轴\n\n用于两个向量之间的旋转，并将其编码为四元数。\n\n## 创建从一个向量到另一个向量的旋转\n\n任何两个单位的向量都可以表示球体上的点。这些点之间的最短弧位于包含点和球体中心的平面上。这架飞机\n\n垂直于这两个向量之间的旋转轴。\n\n为了找到旋转轴，标准化输入向量。求输入向量的叉积。这是旋转轴。求输入向量之间的角度。从 [*第二章*](02.html#_idTextAnchor026)*实现向量*开始，两个向量之间的夹角公式为![](img/Formula_04_003.png)。由于两个输入向量都是归一化的，这简化为![](img/Formula_04_004.png)，这意味着 *θ* 的余弦是输入向量的点积:\n\n![](img/Formula_04_005.png)\n\n大家会从 [*第二章*](02.html#_idTextAnchor026)*实现向量*中回想起，点积与两个向量夹角的余弦有关系，叉积与两个向量夹角的正弦有关系。创建四元数时，点积和叉积具有以下属性:\n\n![](img/Formula_04_006.png)\n\n![](img/Formula_04_007.png)\n\n叉积可以扩展为 *x* 、 *y* 和 *z* 分量，前面的等式开始看起来像是从角度和旋转轴创建四元数的代码。找到两个向量之间的角度将是昂贵的，但是半角可以在不知道角度是什么的情况下计算。\n\n要找到半角，找到 *v1* 和 *v2* 输入向量之间的中间向量。使用 *v1* 和这个中间向量构造一个四元数。这将创建导致所需旋转的四元数。\n\n有一种边缘情况——当 *v1* 和 *v2* 平行时会发生什么？或者如果 *v1== -v2* ？用于寻找旋转轴的叉积将产生一个 *0* 向量。如果出现这种边缘情况，请找到两个向量之间最垂直的向量，以创建纯四元数。\n\n执行以下步骤实现`fromTo`功能的:\n\n1.  开始在`quat.cpp`中实现`fromTo`功能，并将功能声明添加到`quat.h`中。首先标准化`from`和`to`向量，确保它们不是同一个向量:\n\n    ```cpp\n    quat fromTo(const vec3& from, const vec3& to) {\n       vec3 f = normalized(from);\n       vec3 t = normalized(to);\n       if (f == t) {\n          return quat();\n       }\n    ```\n\n2.  接下来，检查两个向量是否相反。如果是，可以使用`from`向量的最正交轴来创建纯四元数:\n\n    ```cpp\n       else if (f == t * -1.0f) {\n          vec3 ortho = vec3(1, 0, 0);\n          if (fabsf(f.y) <fabsf(f.x)) {\n             ortho = vec3(0, 1, 0);\n          }\n          if (fabsf(f.z)<fabs(f.y) && fabs(f.z)<fabsf(f.x)){\n             ortho = vec3(0, 0, 1);\n          }\n          vec3 axis = normalized(cross(f, ortho));\n          return quat(axis.x, axis.y, axis.z, 0);\n       }\n    ```\n\n3.  最后，在`from`和`to`向量之间创建一个半向量。用半向量和起始向量的叉积计算旋转轴，用两者的点积求旋转角度:\n\n    ```cpp\n       vec3 half = normalized(f + t); \n       vec3 axis = cross(f, half);\n       return quat(axis.x, axis.y, axis.z, dot(f, half));\n    }\n    ```\n\n`fromTo`函数是创建四元数最直观的方法之一。接下来，您将学习如何检索定义四元数的角度和轴。\n\n# 检索四元数数据\n\n因为四元数可以从一个角度和一个轴创建，所以期望能够从四元数中检索相同的角度和轴是合理的。要检索旋转轴，请规范化四元数的向量部分。旋转角度是实分量反余弦的两倍。\n\n在`quat.cpp`中实现`getAngle`和`getAxis`功能，并在`quat.h`中为两者添加功能声明:\n\n```cpp\nvec3 getAxis(const quat& quat) {\n    return normalized(vec3(quat.x, quat.y, quat.z));\n}\nfloat getAngle(const quat& quat) {\n    return 2.0f * acosf(quat.w);\n}\n```\n\n能够检索定义四元数的角度和轴将需要稍后进行一些四元数操作。\n\n接下来，您将了解通常在四元数上执行的组件式操作。\n\n# 常用四元数运算\n\n像向量一样，四元数也有分量运算。普通的\n\n组件式操作是加法、减法、乘法或减法\n\n四元数。分量四元数乘法乘以一个四元数\n\n通过单个标量值。\n\n由于这些函数是组件式的，它们只是对输入四元数的类似组件执行适当的操作。在`quat.cpp`中实现这些功能，并在`quat.h`中为每个功能添加声明:\n\n```cpp\nquat operator+(const quat& a, const quat& b) {\n    return quat(a.x+b.x, a.y+b.y, a.z+b.z, a.w+b.w);\n}\nquat operator-(const quat& a, const quat& b) {\n    return quat(a.x-b.x, a.y-b.y, a.z-b.z, a.w-b.w);\n}\nquat operator*(const quat& a, float b) {\n    return quat(a.x * b, a.y * b, a.z * b, a.w * b);\n}\nquat operator-(const quat& q) {\n    return quat(-q.x, -q.y, -q.z, -q.w);\n}\n```\n\n这些组件式操作本身没有太多实际用途。它们是构建其余四元数功能的构建块。接下来，您将学习比较四元数的不同方法。\n\n# 比较操作\n\n比较两个四元数可以通过组件的方式完成。两个四元数可以表示相同的旋转，即使它们在组件级别上不相同。这是因为四元数及其逆四元数旋转到同一个点，但它们采用不同的路线:\n\n1.  让`quat.cpp`的`==`和`!=`操作员超载。将这些功能的声明添加到`quat.h` :\n\n    ```cpp\n    bool operator==(const quat& left, const quat& right) {\n        return (fabsf(left.x - right.x) <= QUAT_EPSILON &&\n                fabsf(left.y - right.y) <= QUAT_EPSILON &&\n                fabsf(left.z - right.z) <= QUAT_EPSILON &&\n                fabsf(left.w - right.w) <= QUAT_EPSILON);\n    }\n    bool operator!=(const quat& a, const quat& b) {\n        return !(a == b);\n    }\n    ```\n\n2.  为了测试两个四元数是否代表相同的旋转，需要测试两者之间的绝对差异。在`quat.cpp`中实现`sameOrientation`功能。将功能声明添加到`quat.h` :\n\n    ```cpp\n    bool sameOrientation(const quat&l, const quat&r) {\n        return (fabsf(l.x - r.x) <= QUAT_EPSILON  &&\n                fabsf(l.y - r.y) <= QUAT_EPSILON  &&\n                fabsf(l.z - r.z) <= QUAT_EPSILON  &&\n                fabsf(l.w - r.w) <= QUAT_EPSILON) ||\n               (fabsf(l.x + r.x) <= QUAT_EPSILON  &&\n                fabsf(l.y + r.y) <= QUAT_EPSILON  &&\n                fabsf(l.z + r.z) <= QUAT_EPSILON  &&\n                fabsf(l.w + r.w) <= QUAT_EPSILON);\n    }\n    ```\n\n大多数情况下，您会希望使用等式运算符来比较四元数。`sameOrientation`函数没有那么有用，因为如果四元数被反转，四元数的旋转可以改变。\n\n在下一节中，您将学习如何实现四元数点积。\n\n# 点积\n\n像向量一样，点积度量两个四元数有多相似。实现与向量实现相同。将相似的分量相乘，并对结果求和。\n\n在`quat.cpp`中实现四元数点积函数，并将其声明添加到`quat.h`中:\n\n```cpp\nfloat dot(const quat& a, const quat& b) {\n    return a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w;\n}\n```\n\n像向量一样，四元数的长度是四元数与其自身的点积。在下一节中，您将学习如何找到四元数的平方长度和长度。\n\n# 长度和平方长度\n\n像向量一样，四元数的平方长度与四元数与其自身的点积相同。四元数的长度是平方长度的平方根:\n\n1.  在`quat.cpp`实现`lenSq`功能，在`quat.h`声明功能:\n\n    ```cpp\n    float lenSq(const quat& q) {\n      return q.x * q.x + q.y * q.y + q.z * q.z + q.w * q.w;\n    }\n    ```\n\n2.  在`quat.cpp`中实现`len`功能。别忘了把功能声明添加到`quat.h` :\n\n    ```cpp\n    float len(const quat& q) {\n      float lenSq = q.x*q.x + q.y*q.y + q.z*q.z + q.w*q.w;\n      if (lenSq< QUAT_EPSILON) {\n         return 0.0f;\n      }\n      return sqrtf(lenSq);\n    }\n    ```\n\n代表旋转的四元数应该总是具有长度 *1* 。在下一节中，您将了解单位四元数，它的长度总是为 *1* 。\n\n# 单位四元数\n\n四元数可以像向量一样归一化。规范化四元数仅表示旋转，非规范化四元数会引入偏斜。在游戏动画的上下文中，四元数应该规范化，以避免给变换增加偏斜。\n\n若要规范化四元数，请将四元数的每个分量除以其长度。结果四元数的长度将为 *1* 。这可以通过以下方式实现:\n\n1.  在`quat.cpp`中实现`normalize`功能，在`quat.h`中声明:\n\n    ```cpp\n    void normalize(quat& q) {\n       float lenSq = q.x*q.x + q.y*q.y + q.z*q.z + q.w*q.w;\n       if (lenSq < QUAT_EPSILON) { \n          return; \n       }\n       float i_len = 1.0f / sqrtf(lenSq);\n       q.x *= i_len;\n       q.y *= i_len;\n       q.z *= i_len;\n       q.w *= i_len;\n    }\n    ```\n\n2.  在`quat.cpp`实现`normalized`功能，在`quat.h`申报:\n\n    ```cpp\n    quat normalized(const quat& q) {\n       float lenSq = q.x*q.x + q.y*q.y + q.z*q.z + q.w*q.w;\n       if (lenSq < QUAT_EPSILON) {\n          return quat();\n       }\n       float il = 1.0f / sqrtf(lenSq); // il: inverse length\n       return quat(q.x * il, q.y * il, q.z * il,q.w * il);\n    }\n    ```\n\n有一种快速的方法可以反转任何单位四元数。在下一节中，你将学习如何找到四元数的共轭和逆，以及当涉及到单位四元数时它们之间的关系。\n\n# 共轭和逆\n\n游戏大多使用规范化的四元数，这在反转四元数时很方便。归一化四元数的逆是它的共轭。共轭\n\n四元数翻转其旋转轴:\n\n1.  在`quat.cpp`实现`conjugate`功能，记得在`quat.h`声明功能:\n\n    ```cpp\n    quat conjugate(const quat& q) {\n        return quat(\n            -q.x,\n            -q.y,\n            -q.z,\n             q.w\n        );\n    }\n    ```\n\n2.  四元数的正确逆是共轭除以四元数的平方长度。在`quat.cpp`中实现四元数`inverse`功能。将功能声明添加到`quat.h` :\n\n    ```cpp\n    quat inverse(const quat& q) {\n       float lenSq = q.x*q.x + q.y*q.y + q.z*q.z + q.w*q.w;\n       if (lenSq < QUAT_EPSILON) { \n          return quat(); \n       }\n       float recip = 1.0f / lenSq;\n       return quat(-q.x * recip,\n                   -q.y * recip,\n                   -q.z * recip,\n                    q.w * recip\n       );\n    }\n    ```\n\n如果你需要知道一个四元数是否归一化，检查平方长度。归一化四元数的平方长度总是 *1* 。如果四元数被归一化，它的共轭和逆是相同的。这意味着你可以使用更快的`conjugate`功能，而不是`inverse`功能。在下一节中，您将学习如何将两个四元数相乘。\n\n# 乘法四元数\n\n两个四元数可以通过相乘连接在一起。和矩阵一样，操作是从右向左进行；首先应用右四元数的旋转，然后应用左四元数的旋转。\n\n假设你有两个四元数， *q* 和 *p* 。分别用`0`、`1`、`2`和`3`下标，分别对应`X`、`Y`、`Z`和`W`组件。这些四元数可以用 *ijk* 符号表示，如图所示:\n\n![](img/Formula_04_008.png)\n\n![](img/Formula_04_009.png)\n\n要将这两个四元数相乘，请将 *p* 的分量分配给 *q* 的分量。分发真实组件很简单。将 *p* 3 分发给 *q* 如下所示:\n\n![](img/Formula_04_010.png)\n\n虚部的分布看起来非常相似。实部和虚部分开组合；虚部的顺序很重要。例如，将 *i* 的 *p* 分配给 *q* 如下所示:\n\n![](img/Formula_04_011.png)\n\n将 *p* 完全分配给 *q* 如下图所示:\n\n![](img/Formula_04_012.png)\n\n![](img/Formula_04_013.png)\n\n![](img/Formula_04_014.png)\n\n![](img/Formula_04_015.png)\n\n开始简化虚数平方的情况。虚数的平方根是 *-1* 。如果把 *-1* 升到 *-1* 的幂，结果也是 *-1* 。这意味着任何一个 *i* 2、 *j* 2 或 *k* 2 的实例都可以被 *-1* 代替，就像这样:\n\n![](img/Formula_04_016.png)\n\n![](img/Formula_04_017.png)\n\n![](img/Formula_04_018.png)\n\n![](img/Formula_04_019.png)\n\n剩下的虚数呢？当谈到四元数时，\n\n*ijk= -1* ，这些分量的平方值也是 *-1* ，这意味着\n\n*I*2*= j*2*= k*2*= ijk*。四元数的这个性质可以用来简化方程的其余部分。\n\n以 *jk* 为例。从 *ijk= -1* 开始，尝试将 *jk* 隔离到等式的一边。\n\n为此，将两边乘以 *i* ，给你留下 *i(ijk)= -i* 。分发 *i* ，剩下的就是 *i* 2 *jk= -i* 。你已经知道 *i* 2 的值是 *-1* 。用它来代替\n\n*-jk= -i* 。将两边乘以 *-1* ，你就找到了*JK-JK = I*的值。\n\n*ki* 和 *ij* 的值可以用类似的方式找到；它们是 *ki=j* 和 *k=ij* 。现在可以用 *j* 、 *ij* 替换 *ki* 的任何实例，用 *k* 替换 *jk* 用 *i* 替换。替换这些值会给您留下以下信息:\n\n![](img/Formula_04_020.png)\n\n![](img/Formula_04_021.png)\n\n![](img/Formula_04_022.png)\n\n![](img/Formula_04_019.png)\n\n剩下的虚数是*ik**Ji*和 *kj* 。和叉积一样，顺序很重要: *ik= -ki* 。由此可以假设 *ik= -j* 、 *ji= -k* 、 *kj= -1* 。替换这些值留给您以下内容:\n\n![](img/Formula_04_024.png)\n\n![](img/Formula_04_025.png)\n\n![](img/Formula_04_026.png)\n\n![](img/Formula_04_027.png)\n\n虚部不同的数不能加在一起。重新排列前面的公式，使类似的虚部彼此相邻。这就产生了四元数乘法的最终方程:\n\n![](img/Formula_04_028.png)\n\n![](img/Formula_04_029.png)\n\n![](img/Formula_04_030.png)\n\n![](img/Formula_04_031.png)\n\n要在代码中实现该公式，请将下标 *ijk* 符号更改回带有`X`、`Y`、`Z`和`W`下标的向量符号。在`quat.cpp`实现四元数乘法函数，别忘了给`quat.h`增加函数声明:\n\n```cpp\nquat operator*(const quat& Q1, const quat& Q2) {\n   return quat( \n       Q2.x*Q1.w + Q2.y*Q1.z - Q2.z*Q1.y + Q2.w*Q1.x,\n      -Q2.x*Q1.z + Q2.y*Q1.w + Q2.z*Q1.x + Q2.w*Q1.y,\n       Q2.x*Q1.y - Q2.y*Q1.x + Q2.z*Q1.w + Q2.w*Q1.z,\n      -Q2.x*Q1.x - Q2.y*Q1.y - Q2.z*Q1.z + Q2.w*Q1.w\n   );\n}\n```\n\n查看前面的代码时，请注意四元数的实部有一个正分量，但向量部分有一个负分量。重新排列四元数，使负数总是最后一个。用向量符号写下来:\n\n*qp*x*= p*x*q*w*+p*w*q*x*+p*y*q*z*-p*z*q*y\n\n*qp*y =*p*y*q*w+*p*w*q*y+*p*z*q*x-*p*x*q*z\n\n*qp*z =*p*z*q*w+*p*w*q*z+*p*x*q*y-*p*y*q*x\n\n*qp*w =*p*w*q*w-*p*x*q*x-*p*y*q*y-*p*z*q*z\n\n在上式中有两个有趣的部分。如果仔细观察前三行的最后两列，带减法的列就是叉积。前两列只是用其他四元数的标量部分来缩放每个四元数的向量部分。\n\n如果你看最后一行，点积和点积的负数在一起。最后一行基本上是将两个四元数的实部相乘，然后减去它们向量部分的点积。这意味着替代乘法实现可能如下所示:\n\n```cpp\nquat operator*(const quat& Q1, const quat& Q2) {\n  quat result;\n  result.scalar = Q2.scalar * Q1.scalar -\n  dot(Q2.vector, Q1.vector);\n  result.vector = (Q1.vector * Q2.scalar) +\n  (Q2.vector * Q1.scalar)+cross(Q2.vector, Q1.vector);\n  return result;\n}\n```\n\n最初的实现性能更好一点，因为它不需要调用其他函数。本书的示例代码将使用第一个实现。\n\n接下来，你将学习如何用四元数变换向量。\n\n# 变换向量\n\n要将一个向量和一个四元数相乘，首先必须将向量转化为一个纯四元数。什么是纯四元数？它是一个四元数，其`W`分量为`0`，向量部分归一化。假设你有一个四元数， *q* ，和一个向量， *v* 。首先把 *v* 变成纯四元数，表示为 *v* ':\n\n![](img/Formula_04_035.png)\n\n![](img/Formula_04_036.png)\n\n接下来，将 *q* 乘以 *v* ，然后将结果乘以 *q* 的倒数。该乘法的结果是纯四元数，其向量部分包含旋转向量。四元数如下:\n\n![](img/Formula_04_038.png)\n\n为什么 *v* 乘以 *q* 再乘以 *q* -1？乘以 *q* 将使向量旋转两倍于 *q* 的旋转。乘以 *q* -1 会使向量回到预期范围。这个公式可以进一步简化。\n\n推导这个公式超出了本书的范围。给定一个四元数， *q* ，并且\n\n一个向量， *v* ，简化的向量四元数乘法公式如下。\n\n*q* v 指四元数的向量部分， *q* s 指实(或标量)部分:\n\n![](img/Formula_04_042.png)\n\n在`quat.cpp`中执行前面的四元数向量乘法公式。别忘了给`quat.h`添加功能声明:\n\n```cpp\nvec3 operator*(const quat& q, const vec3& v) {\n    return q.vector * 2.0f * dot(q.vector, v) +\n        v * (q.scalar * q.scalar - dot(q.vector, q.vector)) +\n        cross(q.vector, v) * 2.0f * q.scalar;\n}\n```\n\n将一个向量乘以一个四元数将总是产生一个被四元数旋转的向量。在下一节中，您将学习四元数之间的插值。\n\n# 插值四元数\n\n四元数可以用类似于向量的方式进行插值。四元数插值用于动画显示两个关键帧之间的旋转。因为大多数骨骼动画是通过随时间旋转关节来实现的，所以在四元数之间进行插值将是\n\n非常普通的手术。\n\n## 邻里\n\n四元数代表旋转，而不是方向。从球体的一部分旋转到另一部分可以通过两次旋转中的一次来实现。旋转可以采用最短或最长的弧线。一般来说，让四元数沿着最短的弧行进是可取的。在两个四元数之间进行插值时，会选择哪条路径——最短的弧还是最长的？\n\n这个问题叫做睦邻问题。为了解决这个问题，检查被插值的四元数的点积。如果点积为正，将采用较短的弧线。如果点积为负，将采用更长的弧。\n\n如果点积为负，如何修正插值取最短弧？答案是否定其中一个四元数。以下代码示例提供了四元数邻域覆盖的示例:\n\n```cpp\nquat SampleFunction(const quat& a, const quat& b) {\n    if (dot(a, b) < 0.0f) {\n        b = -b;\n    }\n    return slerp(a, b, 0.5f);\n}\n```\n\n在四元数之间进行插值时，只需要来进行邻域四元数。接下来，您将学习如何混合线性插值(`lerp`)、归一化线性插值(`nlerp`)和球面线性插值(`slerp`)四元数。请记住，这些函数期望四元数已经在其期望的邻域内。\n\n## 了解混合功能\n\n将两个或多个四元数混合在一起时，每个四元数按某个权重值进行缩放，然后将结果缩放的四元数相加在一起。所有输入四元数的所有权重必须加起来达到 *1* 。\n\n如果所有的输入四元数都是单位长度的，那么得到的四元数也是单位长度的。这个函数达到了与`lerp`相同的结果，但它不是真正的`lerp`函数，因为四元数仍然在圆弧上运动。为了避免混淆，这个函数将被称为`mix`，而不是`lerp`。\n\n`mix`函数假设输入四元数在期望的邻域内。在`quat.cpp`实现`mix`功能，别忘了给`quat.h`添加功能声明:\n\n```cpp\nquat mix(const quat& from, const quat& to, float t) {\n    return from * (1.0f - t) + to * t;\n}\n```\n\n## 了解 nlerp 功能\n\n`nlerp`四元数之间是球面插值的一个快速而良好的近似。它的实现与`vec3`类的`nlerp`实现几乎相同。\n\n与`mix`类似，`nlerp`也假设输入向量在期望的邻域内。在`quat.cpp`中实现`nlerp`功能，别忘了给`quat.h`添加功能声明:\n\n```cpp\nquat nlerp(const quat& from, const quat& to, float t) {\n    return normalized(from + (to - from) * t);\n}\n```\n\n## slep 简介\n\n`slerp`仅在要求速度一致时使用。大多数情况下，`nlerp`会是比较好的插值方法。根据插值步长的不同，`slerp`最终可能会回落到`nlerp`位置。\n\n为了在两个四元数之间进行球面插值，在两个四元数之间创建一个δ四元数。调整三角形四元数的角度，然后使用四元数乘法将其与起始四元数连接起来。\n\n四元数的角度如何调整？若要调整四元数的角度，请将其提高到所需的幂。例如，要将四元数调整到只旋转一半，可以将其提升到 *0.5* 的幂。\n\n## 功率\n\n要将四元数提升到某个幂，需要将其分解为一个角度和一个轴。然后，角度可以通过幂来调整，并且可以根据调整后的角度和轴来构建新的四元数。如果一个四元数围绕 *v* 轴旋转一个 *θ* 角度，将其提升到某个功率， *t* ，将按如下方式完成:\n\n![](img/Formula_04_044.png)\n\n执行`quat.cpp`中的`power operator`。别忘了给`quat.h`添加功能声明:\n\n```cpp\nquat operator^(const quat& q, float f) {\n    float angle = 2.0f * acosf(q.scalar);\n    vec3 axis = normalized(q.vector);\n    float halfCos = cosf(f * angle * 0.5f);\n    float halfSin = sinf(f * angle * 0.5f);\n    return quat(axis.x * halfSin,\n                axis.y * halfSin,\n                axis.z * halfSin,\n                halfCos\n    );\n}\n```\n\n## 实施 slerp\n\n既然你知道了如何将四元数升到幂，实现`slerp`就变得简单了。如果开始和结束四元数非常接近，`slerp`往往会产生意想不到的结果。如果起点和终点四元数靠得很近，回到`nlerp`上。\n\n要在两个四元数之间进行插值，请找到从开始旋转到结束旋转的增量四元数。这个增量四元数是插值路径。将角度提高到两个四元数之间插值的幂(通常表示为 *t* )并将起始四元数相乘。\n\n在`quat.cpp`中实现`slerp`功能。别忘了给`quat.h`添加函数声明。与其他插值函数一样，`slerp`假设被插值的四元数位于期望的邻域内:\n\n```cpp\nquat slerp(const quat& start, const quat& end, float t) {\n    if (fabsf(dot(start, end)) > 1.0f - QUAT_EPSILON) {\n        return nlerp(start, end, t);\n    }\n    quat delta = inverse(start) * end;\n    return normalized((delta ^ t) * start);\n}\n```\n\n`slerp`的输入向量应该归一化，这意味着你可以在`slerp`函数中使用`conjugate`代替`inverse`。大多数情况下，`nlerp`会用到`slerp`以上。在下一节中，您将学习如何创建指向特定方向的四元数。\n\n# 看旋转\n\n给定一个方向和一个方向向上的参考，可以创建一个四元数，以正确的方向看向方向。该函数将被称为`lookRotation`—而不是`lookAt`，以避免与矩阵`lookAt`函数混淆。\n\n要实现`lookRotation`功能，找到一个旋转到所需方向的四元数。为此，在世界`forward`向量 *(0，0，1)* 和`desired direction`之间创建一个四元数。这个四元数将旋转到`right`目标，但不考虑`up`可能是什么方向。\n\n要修正这个四元数的`up`方向，首先要找到一个垂直于当前正向和期望的`up`方向的向量。这可以通过取两个向量的叉积来实现。\n\n这个叉积的结果将用于构造三个正交向量——前向向量、这个新向量和一个指向上的向量。你刚找到的那个会指向右边。\n\n接下来，你需要找到一个既垂直于`forward`方向又垂直于`right`方向的向量；这将是正交`up`向量。要找到这个向量，取方向和这个`right`向量的叉积，结果就是物体空间`up`向量。\n\n找到一个从所需的`up`向量旋转到对象`up`向量的四元数。将旋转到目标方向的四元数和从`desired up`旋转到`object up`的四元数相乘。\n\n在`quat.cpp`中实现`lookRotation`功能。别忘了给`quat.h`添加功能声明:\n\n```cpp\nquat lookRotation(const vec3& direction, const vec3& up) {\n    // Find orthonormal basis vectors\n    vec3 f = normalized(direction); // Object Forward\n    vec3 u = normalized(up); // Desired Up\n    vec3 r = cross(u, f); // Object Right\n    u = cross(f, r); // Object Up\n    // From world forward to object forward\n    quat worldToObject = fromTo(vec3(0, 0, 1), f); \n    // what direction is the new object up?\n    vec3 objectUp = worldToObject * vec3(0, 1, 0);\n    // From object up to desired up\n    quat u2u = fromTo(objectUp, u);\n    // Rotate to forward direction first\n    // then twist to correct up\n    quat result = worldToObject * u2u; \n    // Don't forget to normalize the result\n    return normalized(result);\n}\n```\n\n矩阵`lookAt`函数创建一个视图矩阵，它是相机变换的逆。这意味着`lookAt`的旋转和`lookRotation`的结果将是彼此的逆。在下一节中，您将学习如何将矩阵转换为四元数以及将四元数转换为矩阵。\n\n# 在四元数和矩阵之间转换\n\n由于矩阵和四元数都可以用来编码旋转数据，因此能够在它们之间进行转换将非常有用。为了使两者之间的转换更容易，您必须开始考虑根据基向量的旋转，这些基向量表示 *x* 、 *y* 和 *z* 轴。\n\n4×4 矩阵的上 3×3 子矩阵包含三个基向量。第一列是`right`向量，第二列是`up`向量，第三列是`forward`向量。仅使用`forward`和`up`向量，`lookRotation`函数可用于将矩阵转换为四元数。\n\n要将四元数转换为矩阵，只需将世界基向量乘以四元数，世界基向量是世界的 *x* 、 *y* 和 *z* 轴。将结果向量存储在矩阵的适当部分:\n\n1.  在`quat.cpp`中实现`quatToMat4`功能。别忘了把功能声明添加到`quat.h` :\n\n    ```cpp\n    mat4 quatToMat4(const quat& q) {\n        vec3 r = q * vec3(1, 0, 0);\n        vec3 u = q * vec3(0, 1, 0);\n        vec3 f = q * vec3(0, 0, 1);\n        return mat4(r.x, r.y, r.z, 0,\n                    u.x, u.y, u.z, 0,\n                    f.x, f.y, f.z, 0,\n                    0  , 0  , 0  , 1\n        );\n    }\n    ```\n\n2.  矩阵使用一些相同的组件存储旋转和缩放数据。为了解决这个问题，需要对基向量进行归一化，并使用叉积来确保得到的向量是正交的。在`quat.cpp`中实现`mat4ToQuat`功能，别忘了给`quat.h`添加功能声明:\n\n    ```cpp\n    quat mat4ToQuat(const mat4& m) {\n        vec3 up = normalized(vec3(m.up.x, m.up.y, m.up.z));\n        vec3 forward = normalized(\n             vec3(m.forward.x, m.forward.y, m.forward.z));\n        vec3 right = cross(up, forward);\n        up = cross(forward, right);\n        return lookRotation(forward, up);\n    }\n    ```\n\n当您需要将旋转数据传递给着色器时，能够将四元数转换为矩阵将会很有用。着色器不知道什么是四元数，但它们有处理矩阵的内置功能。将矩阵转换为四元数对于调试和外部数据源仅提供矩阵旋转的情况非常有用。\n\n# 总结\n\n在本章中，您实现了一个健壮的四元数库。四元数对本书的其余部分很重要，因为所有动画旋转数据都被记录为四元数。您学习了如何创建四元数和常见的四元数运算，将四元数与乘法相结合，通过四元数变换向量，插值四元数和效用函数来创建给定向前和向上方向的四元数，以及在矩阵和四元数之间进行转换。\n\n在下一章中，您将使用向量、矩阵和四元数的组合知识来定义变换对象。"
  },
  {
    "path": "docs/handson-cpp-game-ani-prog/05.md",
    "content": "# 五、实现转换\n\n在本章中，您将实现一个保存位置、旋转和缩放数据的结构。这种结构是一种转变。变换从一个空间映射到另一个空间。位置、旋转和缩放也可以存储在 4x4 矩阵中，那么为什么要使用显式转换结构而不是矩阵呢？答案是插值。矩阵插值不好，但变换结构可以。\n\n在两个矩阵之间进行插值是困难的，因为旋转和缩放存储在矩阵的相同分量中。正因为如此，在两个矩阵之间进行插值不会得到你所期望的结果。变换通过分别存储位置、旋转和缩放组件来解决这个问题。\n\n在本章中，您将实现一个转换结构以及您需要能够在转换中执行的常见操作。到本章结束时，您应该能够执行以下操作:\n\n*   理解什么是转变\n*   了解如何组合变换\n*   在变换和矩阵之间转换\n*   Understand how to apply transforms to points and vectors\n\n    重要信息\n\n    在本章中，您将实现一个表示位置、旋转和缩放的变换结构。要了解更多关于变换，它们如何与矩阵相关，以及它们如何适应游戏层次，请查看[http://gabormakesgames.com/transforms.html](http://gabormakesgames.com/transforms.html)。\n\n# 创建变换\n\n转换是简单的结构。变换包含位置、旋转和缩放。位置和比例是向量，旋转是四元数。转换可以分层组合，但是这种父子关系不应该是实际转换结构的一部分。以下步骤将指导您创建转换结构:\n\n1.  创建新文件，`Transform.h`。声明转换结构需要这个文件。\n2.  开始在这个新文件中声明`Transform`结构。从变换的属性开始-`position`、`rotation`和`scale` :\n\n    ```cpp\n    struct Transform {\n        vec3 position;\n        quat rotation;\n        vec3 scale;\n    ```\n\n3.  创建一个接受位置、旋转和缩放的构造函数。该构造函数应该将这些值分配给转换结构的适当成员:\n\n    ```cpp\n    Transform(const vec3& p, const quat& r, const vec3& s) :\n        position(p), rotation(r), scale(s) {}\n    ```\n\n4.  空白变换应该没有位置或旋转，比例为 1。默认情况下，`scale`组件将被创建为`(0, 0, 0)`。为了解决这个问题，`Transform`结构的默认构造函数需要将`scale`初始化为正确的值:\n\n    ```cpp\n        Transform() :\n            position(vec3(0, 0, 0)),\n            rotation(quat(0, 0, 0, 1)),\n            scale(vec3(1, 1, 1))\n        {}\n    }; // End of transform struct\n    ```\n\n`Transform`结构相当简单；它的所有成员都是公开的。变换有位置、旋转和缩放。默认构造函数将位置向量设置为 *0* ，旋转四元数设置为恒等式，比例向量设置为 *1* 。默认构造函数创建的转换无效。\n\n在下一节中，您将学习如何以类似于矩阵或四元数的方式组合变换。\n\n# 组合变换\n\n以骨骼为例。在每个关节处，可以放置一个变换来描述关节的运动。当你旋转你的肩膀时，附着在那个肩膀上的肘部也会移动。要将肩部变换应用于所有连接的关节，每个关节上的变换必须与其父关节的变换相结合。\n\n变换可以像矩阵和四元数一样组合，两个变换的效果可以组合成一个变换。为了保持一致，组合变换应该保持从右到左的组合顺序。与矩阵和四元数不同，这个`combine`函数不会作为乘法函数实现。\n\n组合两个变换的缩放和旋转很简单——将它们相乘。组合位置有点难。组合位置也需要受到`rotation`和`scale`组件的影响。当找到组合位置时，记住变换的顺序:首先缩放，其次旋转，最后平移。\n\n创建新文件，`Transform.cpp`。实现`combine`功能，别忘了给`Transform.h`添加功能声明:\n\n```cpp\nTransform combine(const Transform& a, const Transform& b) {\n    Transform out;\n    out.scale = a.scale * b.scale;\n    out.rotation = b.rotation * a.rotation;\n    out.position = a.rotation * (a.scale * b.position);\n    out.position = a.position + out.position;\n    return out;\n}\n```\n\n在后面的章节中，`combine`函数将被用来组织变换成一个层次。在下一节中，您将学习如何反转变换，这同样类似于反转矩阵和四元数。\n\n# 反转变换\n\n你已经知道变换从一个空间映射到另一个空间。可以反转映射，将变换映射回原始空间。与矩阵和四元数一样，变换也可以反过来。\n\n反转刻度时，请记住 0 不能反转。标度为 0 的情况需要特殊处理\n\n执行`Transform.cpp`中的`inverse`变换方法。别忘了在`Transform.h`申报方法:\n\n```cpp\nTransform inverse(const Transform& t) {\n    Transform inv;\n    inv.rotation = inverse(t.rotation);\n    inv.scale.x = fabs(t.scale.x) < VEC3_EPSILON ? \n                  0.0f : 1.0f / t.scale.x;\n    inv.scale.y = fabs(t.scale.y) < VEC3_EPSILON ? \n                  0.0f : 1.0f / t.scale.y;\n    inv.scale.z = fabs(t.scale.z) < VEC3_EPSILON ? \n                  0.0f : 1.0f / t.scale.z;\n    vec3 invTrans = t.position * -1.0f;\n    inv.position = inv.rotation * (inv.scale * invTrans);\n    return inv;\n}\n```\n\n反转变换可以消除一个变换对另一个变换的影响。考虑一个角色通过一个关卡。一旦关卡结束，在开始下一个关卡之前，您可能需要将角色移回原点。你可以用字符的倒数乘以字符的变换。\n\n在下一节中，您将学习如何将两个或多个变换混合在一起。\n\n# 混合变换\n\n您有代表两个特定时间点的关节的变换。要使模型看起来有动画效果，需要在这些帧的变换之间进行插值或混合。\n\n可以在向量和四元数之间进行插值，这是变换的基础。所以也可以在变换之间进行插值。该操作通常称为混合或混合，而不是插值。将两个变换混合在一起时，线性插值输入变换的位置、旋转和缩放。\n\n在`Transform.cpp`中实现`mix`功能。别忘了在`Transform.h`声明功能:\n\n```cpp\nTransform mix(const Transform& a,const Transform& b,float t){\n    quat bRot = b.rotation;\n    if (dot(a.rotation, bRot) < 0.0f) {\n        bRot = -bRot;\n    }\n    return Transform(\n        lerp(a.position, b.position, t),\n        nlerp(a.rotation, bRot, t),\n        lerp(a.scale, b.scale, t));\n}\n```\n\n能够将变换混合在一起对于创建动画之间的平滑过渡非常重要。在这里，您实现了变换之间的线性混合。在下一节中，您将学习如何将`transform`转换为`mat4`。\n\n# 将变换转换为矩阵\n\n着色器程序可以很好地处理矩阵。它们没有转换结构的本地表示。您可以将转换代码移植到 GLSL，但这不是最好的解决方案。相反，你可以将一个变换转换成一个矩阵，然后将它作为一个着色器统一提交。\n\n由于变换对可以存储在矩阵中的数据进行编码，因此可以将变换转换成矩阵。要将一个变换转换成一个矩阵，矩阵需要用向量来表示。\n\n首先，通过将全局基向量的方向乘以变换的旋转来找到基向量。接下来，按变换的比例缩放基向量。这产生了填充上面的 3×3 子矩阵的最终基向量。该位置直接进入矩阵的最后一列。\n\n执行`Transform.cpp`中的【从 T0】方法。别忘了给`Transform.h`添加功能声明:\n\n```cpp\nmat4 transformToMat4(const Transform& t) {\n    // First, extract the rotation basis of the transform\n    vec3 x = t.rotation * vec3(1, 0, 0);\n    vec3 y = t.rotation * vec3(0, 1, 0);\n    vec3 z = t.rotation * vec3(0, 0, 1);\n    // Next, scale the basis vectors\n    x = x * t.scale.x;\n    y = y * t.scale.y;\n    z = z * t.scale.z;\n    // Extract the position of the transform\n    vec3 p = t.position;\n    // Create matrix\n    return mat4(\n        x.x, x.y, x.z, 0, // X basis (& Scale)\n        y.x, y.y, y.z, 0, // Y basis (& scale)\n        z.x, z.y, z.z, 0, // Z basis (& scale)\n        p.x, p.y, p.z, 1  // Position\n    );\n}\n```\n\n图形应用编程接口处理矩阵，而不是变换。在后面的章节中，变换将在被发送到着色器之前被转换成矩阵。在下一节中，您将学习如何做相反的事情，即将矩阵转换为变换。\n\n# 将矩阵转换为变换\n\n外部文件格式可能将转换数据存储为矩阵。例如，glTF 可以将节点的变换存储为位置、旋转和缩放，或者存储为单个 4x4 矩阵。为了使转换代码健壮，您需要能够将矩阵转换为转换。\n\n将矩阵转换为变换比将变换转换为矩阵更困难。提取矩阵的旋转很简单；您已经实现了一个将 4x4 矩阵转换为四元数的函数。提取位置也很简单；将矩阵的最后一列复制到向量中。提取刻度更加困难。\n\n回想一下，变换的操作顺序是缩放、旋转，然后平移。这意味着，如果您有三个矩阵——分别代表比例、旋转和平移的 *S* 、 *R* 和 *T* ，它们将组合成一个变换矩阵，如下所示:\n\n*M = SRT*\n\n要求尺度，首先忽略矩阵的平移部分， *M* (将平移向量清零)。这就剩下 *M = SR* 了。要去除矩阵的旋转分量，将 *M* 乘以 *R* 的倒数。这应该只剩下比例部分。不完全是。结果会留下一个包含比例和一些倾斜信息的矩阵。\n\n我们从这个尺度倾斜矩阵中提取尺度的方法是简单地将主对角线作为尺度倾斜矩阵。虽然这在大多数情况下是可行的，但并不完美。获取的比例应被视为有损比例，因为该值也可能包含倾斜数据，这使得比例不准确。\n\n重要说明\n\n可以将矩阵分解为平移、旋转、缩放、倾斜和行列式的符号。然而，这种分解是昂贵的，并且不太适合实时应用。要了解更多信息，请查看肯·舒梅克和汤姆·达夫在[的*矩阵动画和极坐标分解*。](https://research.cs.wisc.edu/graphics/Courses/838-s2002/Papers/polar-decomp.pdf)\n\n在`Transform.cpp`中实现`toTransform`功能。别忘了给`Transform.h`添加功能声明:\n\n```cpp\nTransform mat4ToTransform(const mat4& m) {\n    Transform out;\n    out.position = vec3(m.v[12], m.v[13], m.v[14]);\n    out.rotation = mat4ToQuat(m);\n    mat4 rotScaleMat(\n        m.v[0], m.v[1], m.v[2], 0,\n        m.v[4], m.v[5], m.v[6], 0,\n        m.v[8], m.v[9], m.v[10], 0,\n        0, 0, 0, 1\n    );\n    mat4 invRotMat = quatToMat4(inverse(out.rotation));\n    mat4 scaleSkewMat = rotScaleMat * invRotMat;\n    out.scale = vec3(\n        scaleSkewMat.v[0], \n        scaleSkewMat.v[5], \n        scaleSkewMat.v[10]\n    );\n    return out;\n}\n```\n\n重要的是你能够将矩阵转换成变换，因为你并不总是控制你所处理的数据的格式。例如，模型格式可能存储矩阵而不是变换。\n\n到目前为止，你可能已经注意到变换和矩阵通常可以做同样的事情。在下一节中，您将学习如何使用变换来变换点和向量，类似于如何使用矩阵来完成。\n\n# 变换点和向量\n\n`Transform`结构可以用来在空间中移动点和向量。想象一个球上下弹跳。球的弹跳来源于`Transform`结构，但是你怎么知道球的各个顶点往哪里移动呢？您需要使用`Transform`结构(或矩阵)变换所有顶点，以正确显示球。\n\n使用变换修改点和向量就像组合两个变换。要变换一个点，首先应用缩放，然后应用旋转，最后应用变换的平移。要变换向量，请遵循相同的步骤，但不要添加位置:\n\n1.  在`Transform.cpp`中实现`transformPoint`功能。别忘了把功能声明添加到`Transform.h` :\n\n    ```cpp\n    vec3 transformPoint(const Transform& a, const vec3& b) {\n        vec3 out;\n        out = a.rotation * (a.scale * b);\n        out = a.position + out;\n        return out;\n    }\n    ```\n\n2.  在`Transform.cpp`中实现`transformVector`功能。别忘了把功能声明添加到`Transform.h` :\n\n    ```cpp\n    vec3 transformVector(const Transform& a, const vec3& b) {\n        vec3 out;\n        out = a.rotation * (a.scale * b);\n        return out;\n    }\n    ```\n\n`transformPoint`函数的作用与矩阵和点相乘的作用相同，只是一步一个脚印。首先应用`scale`，然后应用`rotation`，最后应用`translation`。当你在处理一个向量而不是一个点时，同样的顺序适用，除了平移被忽略。\n\n# 总结\n\n在本章中，您学习了如何将转换实现为包含位置、旋转和缩放的离散结构。在许多方面，`Transform`类保存的数据与通常存储在矩阵中的数据相同。\n\n您学习了如何在变换之间组合、反转和混合，以及如何使用变换来移动点和旋转向量。变换将是向前发展的关键，因为它们被用来制作游戏模型的骨架动画。\n\n你需要一个明确的`Transform`结构的原因是矩阵不能很好地插值。插值变换对动画非常重要。这是您创建中间姿势以显示两个给定关键帧的方式。\n\n在下一章中，您将学习如何在 OpenGL 之上编写一个光抽象层，以使以后章节中的渲染更加容易。"
  },
  {
    "path": "docs/handson-cpp-game-ani-prog/06.md",
    "content": "# 六、构建抽象渲染器\n\n这本书关注的是动画，而不是渲染。然而，渲染动画模型很重要。为了避免陷入任何特定的图形 API，在本章中，您将在 OpenGL 之上构建一个抽象层。这将是一个薄的抽象层，但它将让你在后面的章节中处理你的动画，而不必做任何特定于 OpenGL 的事情。\n\n您将在本章中实现的抽象渲染器非常轻量级。它没有很多功能，只有那些你需要显示动画模型的功能。这应该使得将渲染器移植到其他 API 变得简单。\n\n到本章结束时，您应该能够使用将要创建的抽象渲染代码向窗口渲染一些调试几何图形。在更高的层次上，您将学习以下内容:\n\n*   如何创建着色器\n*   如何在缓冲区中存储网格数据\n*   如何将这些缓冲区绑定为着色器属性\n*   如何向着色器发送统一数据\n\n*   如何使用索引缓冲区呈现\n*   如何加载纹理\n*   基本 OpenGL 概念\n*   创建和使用简单着色器\n\n# 技术要求\n\n对 OpenGL 的一些熟悉将使这一章更容易理解。OpenGL、照明模型和着色器技巧不在本书的讨论范围之内。有关这些主题的更多信息，请查看[https://learnopengl.com/](https://learnopengl.com/)。\n\n# 使用着色器\n\n抽象层最重要的部分是`Shader`类。要绘制某些东西，必须绑定一个着色器，并为其附加一些属性和制服。着色器描述了被绘制的对象应该如何变换和着色，而属性定义了被绘制的对象。\n\n在本节中，您将实现一个`Shader`类，该类可以编译顶点和片段着色器。`Shader`类也将返回统一和属性索引。\n\n## 着色器类声明\n\n在实现`Shader`类时，您将需要声明几个受保护的助手函数。这些函数将保持类的公共 API 干净；它们用于将文件读入字符串或调用 OpenGL 代码来编译着色器:\n\n1.  创建一个新文件来声明`Shader`类；称之为`Shader.h`。`Shader`类应该有一个 OpenGL 着色器对象的句柄，以及属性和统一索引的映射。这些字典有一个关键字字符串(属性或统一的名称)和一个值`unsigned int`(统一或属性的索引):\n\n    ```cpp\n    class Shader {\n    private:\n        unsigned int mHandle;\n        std::map<std::string, unsigned int> mAttributes;\n        std::map<std::string, unsigned int> mUniforms;\n    ```\n\n2.  `Shader`类的复制构造函数和赋值运算符应该被禁用。`Shader`类不打算被值复制，因为它持有一个 GPU 资源的句柄:\n\n    ```cpp\n    private:\n        Shader(const Shader&);\n        Shader& operator=(const Shader&);\n    ```\n\n3.  接下来，需要在`Shader`类中声明辅助函数。`ReadFile`功能将文件内容读入`std::string`。`CompileVertexShader`和`CompileFragmentShader`函数编译着色器源代码并返回一个 OpenGL 句柄。`LinkShader`功能将两个着色器链接到一个着色器程序中。`PopulateAttribute`和`PopulateUniform`功能将填写属性和统一字典:\n\n    ```cpp\n    private:\n        std::string ReadFile(const std::string& path);\n        unsigned int CompileVertexShader(\n                         const std::string& vertex);\n        unsigned int CompileFragmentShader(\n                         const std::string& fragment);\n        bool LinkShaders(unsigned int vertex, \n                         unsigned int fragment);\n        void PopulateAttributes();\n        void PopulateUniforms();\n    ```\n\n4.  该类的默认构造函数将创建一个空的`Shader`对象。重载构造函数将调用`Load`方法，该方法从文件中加载着色器并编译它们。析构函数将释放`Shader`类持有的 OpenGL 着色器句柄:\n\n    ```cpp\n    public:\n        Shader();\n        Shader(const std::string& vertex, \n               const std::string& fragment);\n        ~Shader();\n        void Load(const std::string& vertex, \n                  const std::string& fragment);\n    ```\n\n5.  在使用着色器之前，需要将其与`Bind`函数绑定。同样，不再使用后，可以与`UnBind`功能解除绑定。`GetAttribute`和`GetUniform`函数在适当的字典中执行查找。`GetHandle`函数返回着色器的 OpenGL 句柄:\n\n    ```cpp\n        void Bind();\n        void UnBind();\n        unsigned int GetAttribute(const std::string& name);\n        unsigned int GetUniform(const std::string& name);\n        unsigned int GetHandle();\n    };\n    ```\n\n现在`Shader`类声明已经完成，您将在下一节中实现它。\n\n## 实现着色器类\n\n创建一个新文件`Shader.cpp`，在中实现`Shader`类。`Shader`类实现对调用者隐藏了几乎所有实际的 OpenGL 代码。因为大多数 OpenGL 调用都是这样抽象的，在后面的章节中，你只需要直接调用抽象层，而不是 OpenGL 函数。\n\n本书通篇使用统一数组。当着色器中遇到统一数组时(例如`modelMatrices[120]`)，由`glGetActiveUniform`返回的统一名称是数组的第一个元素。在这个例子中，那就是`modelMatrices[0]`。当遇到统一数组时，您希望遍历所有数组索引并获得每个元素的显式统一索引，但也希望存储没有任何下标的统一名称:\n\n1.  两个`Shader`构造函数都必须通过调用`glCreateProgram`来创建一个新的着色器程序句柄。接受两个字符串的构造函数变量用字符串调用`Load`函数。由于`mHandle`始终是程序句柄，析构函数需要删除句柄:\n\n    ```cpp\n    Shader::Shader() {\n        mHandle = glCreateProgram();\n    }\n    Shader::Shader(const std::string& vertex, \n                   const std::string& fragment) {\n        mHandle = glCreateProgram();\n        Load(vertex, fragment);\n    }\n    Shader::~Shader() {\n        glDeleteProgram(mHandle);\n    }\n    ```\n\n2.  `ReadFile`助手功能使用`std::ifstream`将文件转换成字符串，将文件的内容读入`std::stringstream`。字符串流可用于将文件内容作为字符串返回:\n\n    ```cpp\n    std::string Shader::ReadFile(const std::string& path) {\n        std::ifstream file;\n        file.open(path);\n        std::stringstream contents;\n        contents << file.rdbuf();\n        file.close();\n        return contents.str();\n    }\n    ```\n\n3.  `CompileVertexShader`函数是用于编译 OpenGL 顶点着色器的样板代码。首先，用`glCreateShader`创建着色器对象，然后用`glShaderSource`设置着色器的源。最后用`glCompileShader`编译着色器。用`glGetShaderiv`检查错误:\n\n    ```cpp\n    unsigned int Shader::CompileVertexShader(\n                                   const string& vertex) {\n        unsigned int v = glCreateShader(GL_VERTEX_SHADER);\n        const char* v_source = vertex.c_str();\n        glShaderSource(v, 1, &v_source, NULL);\n        glCompileShader(v);\n        int success = 0;\n        glGetShaderiv(v, GL_COMPILE_STATUS, &success);\n        if (!success) {\n            char infoLog[512];\n            glGetShaderInfoLog(v, 512, NULL, infoLog);\n            std::cout << \"Vertex compilation failed.\\n\";\n            std::cout << \"\\t\" << infoLog << \"\\n\";\n            glDeleteShader(v);\n            return 0;\n        };\n        return v;\n    }\n    ```\n\n4.  `CompileFragmentShader`功能与`CompileVertexShader`功能几乎相同。唯一的真正的区别是`glCreateShader`的参数，表示您正在创建一个片段着色器，而不是顶点着色器:\n\n    ```cpp\n    unsigned int Shader::CompileFragmentShader(\n                              const std::string& fragment) {\n        unsigned int f = glCreateShader(GL_FRAGMENT_SHADER);\n        const char* f_source = fragment.c_str();\n        glShaderSource(f, 1, &f_source, NULL);\n        glCompileShader(f);\n        int success = 0;\n        glGetShaderiv(f, GL_COMPILE_STATUS, &success);\n        if (!success) {\n            char infoLog[512];\n            glGetShaderInfoLog(f, 512, NULL, infoLog);\n            std::cout << \"Fragment compilation failed.\\n\";\n            std::cout << \"\\t\" << infoLog << \"\\n\";\n            glDeleteShader(f);\n            return 0;\n        };\n        return f;\n    }\n    ```\n\n5.  `LinkShaders`辅助函数也是样板。将着色器附加到构造器创建的着色器程序句柄。通过调用`glLinkProgram`链接着色器，并用`glGetProgramiv`检查错误。一旦着色器被链接，您只需要程序；可以使用`glDeleteShader` :\n\n    ```cpp\n    bool Shader::LinkShaders(unsigned int vertex, \n                             unsigned int fragment) {\n        glAttachShader(mHandle, vertex);\n        glAttachShader(mHandle, fragment);\n        glLinkProgram(mHandle);\n        int success = 0;\n        glGetProgramiv(mHandle, GL_LINK_STATUS, &success);\n        if (!success) {\n            char infoLog[512];\n            glGetProgramInfoLog(mHandle, 512, NULL, infoLog);\n            std::cout << \"ERROR: Shader linking failed.\\n\";\n            std::cout << \"\\t\" << infoLog << \"\\n\";\n            glDeleteShader(vertex);\n            glDeleteShader(fragment);\n            return false;\n        }\n        glDeleteShader(vertex);\n        glDeleteShader(fragment);\n        return true;\n    }\n    ```\n\n    删除单个着色器对象\n6.  `PopulateAttributes`函数枚举存储在着色器程序中的所有属性，然后将它们存储为键值对，其中键是属性的名称，值是其位置。您可以使用`glGetProgramiv`函数计算着色器程序中活动属性的数量，将`GL_ACTIVE_ATTRIBUTES`作为参数名称。然后，通过索引遍历所有的属性，使用`glGetActiveAttrib`获取每个属性的名称。最后，调用`glGetAttribLocation`获取每个属性的位置:\n\n    ```cpp\n    void Shader::PopulateAttributes() {\n        int count = -1;\n        int length;\n        char name[128];\n        int size;\n        GLenum type;\n        glUseProgram(mHandle);\n        glGetProgramiv(mHandle, GL_ACTIVE_ATTRIBUTES, \n                       &count);\n        for (int i = 0; i < count; ++ i) {\n            memset(name, 0, sizeof(char) * 128);\n            glGetActiveAttrib(mHandle, (GLuint)i, 128, \n                              &length, &size, &type, name);\n            int attrib = glGetAttribLocation(mHandle, name);\n            if (attrib >= 0) {\n                mAttributes[name] = attrib;\n            }\n        }\n        glUseProgram(0);\n    }\n    ```\n\n7.  `PopulateUniforms`辅助函数与`PopulateAttributes`辅助函数非常相似。`glGetProgramiv`需要以`GL_ACTIVE_UNIFORMS`为参数名，需要调用`glGetActiveUniform`和`glGetUniformLocation` :\n\n    ```cpp\n    void Shader::PopulateUniforms() {\n        int count = -1;\n        int length;\n        char name[128];\n        int size;\n        GLenum type;\n        char testName[256];\n        glUseProgram(mHandle);\n        glGetProgramiv(mHandle, GL_ACTIVE_UNIFORMS, &count);\n        for (int i = 0; i < count; ++ i) {\n            memset(name, 0, sizeof(char) * 128);\n            glGetActiveUniform(mHandle, (GLuint)i, 128, \n                               &length, &size, &type, name);\n            int uniform=glGetUniformLocation(mHandle, name);\n            if (uniform >= 0) { // Is uniform valid?\n    ```\n\n8.  当遇到有效的制服时，需要判断制服是否为数组。为此，在统一名称中搜索数组括号(`[`)。如果找到括号，制服就是一个数组:\n\n    ```cpp\n    std::string uniformName = name;\n    // if name contains [, uniform is array\n    std::size_t found = uniformName.find('[');\n    if (found != std::string::npos) {\n    ```\n\n9.  如果遇到统一数组，从`[`开始擦除字符串中的所有内容。这将只给你留下统一的名字。然后，进入一个循环，试图通过将`[ + index + ]`附加到统一名称来检索数组中的每个索引。一旦找到第一个无效索引，打破循环:\n\n    ```cpp\n    uniformName.erase(uniformName.begin() + \n         found, uniformName.end());\n         unsigned int uniformIndex = 0;\n         while (true) {\n               memset(testName,0,sizeof(char)*256);\n                   sprintf(testName, \"%s[%d]\", \n                               uniformName.c_str(), \n                               uniformIndex++);\n                       int uniformLocation = \n                               glGetUniformLocation(\n                               mHandle, testName);\n                       if (uniformLocation < 0) {\n                          break;\n                       }\n                       mUniforms[testName]=uniformLocation;\n                    }\n                }\n    ```\n\n10.  此时，`uniformName`包含制服的名称。如果该制服是一个数组，则名称的`[0]`部分已被删除。将统一索引按名称存储在`mUniforms` :\n\n    ```cpp\n                mUniforms[uniformName] = uniform;\n            }\n        }\n        glUseProgram(0);\n    }\n    ```\n\n11.  最后的辅助函数是`Load`函数，负责加载实际的着色器。这个函数接受两个字符串，它们要么是文件名，要么是内嵌着色器定义。一旦着色器被读取，调用`Compile`、`Link`和`Populate`辅助函数来加载着色器:\n\n    ```cpp\n    void Shader::Load(const std::string& vertex, \n                      const std::string& fragment) {\n        std::ifstream f(vertex.c_str());\n        bool vertFile = f.good();\n        f.close();\n        f = std::ifstream(vertex.c_str());\n        bool fragFile = f.good();\n        f.close();\n        std::string v_source = vertex;\n        if (vertFile) {\n            v_source = ReadFile(vertex);\n        }\n        std::string f_source = fragment;\n        if (fragFile) {\n            f_source = ReadFile(fragment);\n        }\n        unsigned int vert = CompileVertexShader(v_source);\n        unsigned int f = CompileFragmentShader(f_source);\n        if (LinkShaders(vert, frag)) {\n            PopulateAttributes();\n            PopulateUniforms();\n        }\n    }\n    ```\n\n12.  `Bind`功能需要将当前着色器程序设置为活动状态，而`UnBind`应确保没有`Shader`对象处于活动状态。`GetHandle`帮助器功能将 OpenGL 手柄返回到`Shader`对象:\n\n    ```cpp\n    void Shader::Bind() {\n        glUseProgram(mHandle);\n    }\n    void Shader::UnBind() {\n        glUseProgram(0);\n    }\n    unsigned int Shader::GetHandle() {\n        return mHandle;\n    }\n    ```\n\n13.  最后，您需要一种检索属性和制服绑定槽的方法。`GetAttribute`功能将检查属性图中是否存在给定的属性名称。如果是，则返回代表它的整数。如果不是，则返回`0`。`0`是一个有效的属性索引，因此如果出现错误，也会记录一条错误消息:\n\n    ```cpp\n    unsigned int Shader::GetAttribute(\n                            const std::string& name) {\n        std::map<std::string, unsigned int>::iterator it =\n                                    mAttributes.find(name);\n        if (it == mAttributes.end()) {\n            cout << \"Bad attrib index: \" << name << \"\\n\";\n            return 0;\n        }\n        return it->second;\n    }\n    ```\n\n14.  `GetUniform`功能的实现几乎与`GetAttribute`功能相同，除了代替属性地图，它在统一地图上工作:\n\n    ```cpp\n    unsigned int Shader::GetUniform(const std::string& name){\n        std::map<std::string, unsigned int>::iterator it =\n                                      mUniforms.find(name);\n        if (it == mUniforms.end()) {\n            cout << \"Bad uniform index: \" << name << \"\\n\";\n            return 0;\n        }\n        return it->second;\n    }\n    ```\n\n`Shader`类有检索制服和属性索引的方法。在下一节中，您将开始实现一个`Attribute`类来保存传递给着色器的顶点数据。\n\n# 使用缓冲区(属性)\n\n属性是图形管道中的逐顶点数据。顶点由属性组成。例如，一个顶点有一个位置和一个法线，这两个都是属性。最常见的属性如下:\n\n*   位置:通常在局部空间\n*   法线:顶点指向的方向\n*   UV 或纹理坐标:纹理上的归一化( *x* ， *y* )坐标\n*   颜色:表示顶点颜色的`vector3`\n\n属性可以有不同的数据类型。在本书中，您将实现对整数、浮点和向量属性的支持。对于向量属性，将支持二维、三维和四维向量。\n\n## 属性类声明\n\n创建新文件，`Attribute.h`。`Attribute`类将在这个新文件中声明。`Attribute`班将以为模板。这将确保如果一个属性意味着是`vec3`，您不会意外地将`vec2`载入其中:\n\n1.  属性类将包含两个成员变量，一个用于 OpenGL 属性句柄，一个用于计算`Attribute`类包含多少数据。由于属性数据存在于 GPU 上，并且您不希望同一数据有多个句柄，因此复制构造函数和`assignment operator`应该被禁用:\n\n    ```cpp\n    template<typename T>\n    class Attribute {\n    protected:\n        unsigned int mHandle;\n        unsigned int mCount;\n    private:\n        Attribute(const Attribute& other);\n        Attribute& operator=(const Attribute& other);\n    ```\n\n2.  `SetAttribPointer`功能是特殊的，因为它需要为支持的每种属性实现一次。这将在`.cpp`文件中明确完成，稍后:\n\n    ```cpp\n    void SetAttribPointer(unsigned int slot);\n    ```\n\n3.  将属性类的构造函数和析构函数声明为公共函数:\n\n    ```cpp\n    public:\n    Attribute();\n    ~Attribute();\n    ```\n\n4.  `Attribute`类需要一个`Set`函数，这个函数会将一组数据上传到 GPU。数组中的每个元素代表一个顶点的属性。我们需要一种从着色器定义的绑定槽中绑定和解除绑定属性的方法，以及属性计数和句柄的访问器:\n\n    ```cpp\n        void Set(T* inputArray, unsigned int arrayLength);\n        void Set(std::vector<T>& input);\n        void BindTo(unsigned int slot);\n        void UnBindFrom(unsigned int slot);\n        unsigned int Count();\n        unsigned int GetHandle();\n    };\n    ```\n\n现在您已经声明了`Attribute`类，您将在下一节中实现它。\n\n## 实现属性类\n\n创建新文件，`Attribtue.cpp`。您将在此文件中实现`Attribute`类，如下所示:\n\n1.  `Attribute`类是模板化的，但是它的函数没有一个被标记为内联的。每个属性类型的模板专门化将存在于`Attribute.cpp`文件中。为整数、浮点、`vec2`、`vec3`、`vec4`和`ivec4`类型添加专门化:\n\n    ```cpp\n    template Attribute<int>;\n    template Attribute<float>;\n    template Attribute<vec2>;\n    template Attribute<vec3>;\n    template Attribute<vec4>;\n    template Attribute<ivec4>;\n    ```\n\n2.  构造函数应该生成一个 OpenGL 缓冲区，并将其存储在`Attribute`类的句柄中。析构函数负责释放`Attribute`类持有的句柄:\n\n    ```cpp\n    template<typename T>\n    Attribute<T>::Attribute() {\n        glGenBuffers(1, &mHandle);\n        mCount = 0;\n    }\n    template<typename T>\n    Attribute<T>::~Attribute() {\n        glDeleteBuffers(1, &mHandle);\n    }\n    ```\n\n3.  `Attribute`类有两个简单的 getters，一个用来检索计数，一个用来检索 OpenGL 句柄。计数表示总共有多少属性:\n\n    ```cpp\n    template<typename T>\n    unsigned int Attribute<T>::Count() {\n        return mCount;\n    }\n    template<typename T>\n    unsigned int Attribute<T>::GetHandle() {\n        return mHandle;\n    }\n    ```\n\n4.  `Set`函数取一个数组和一个长度。然后，它绑定`Attribute`类保留的缓冲区，并使用`glBufferData`用数据填充缓冲区。`Set`有一个方便的函数，用向量引用代替数组。它调用实际的`Set`函数:\n\n    ```cpp\n    template<typename T>\n    void Attribute<T>::Set(T* inputArray, \n                           unsigned int arrayLength) {\n        mCount = arrayLength;\n        unsigned int size = sizeof(T);\n        glBindBuffer(GL_ARRAY_BUFFER, mHandle);\n        glBufferData(GL_ARRAY_BUFFER, size * mCount, \n                     inputArray, GL_STREAM_DRAW);\n        glBindBuffer(GL_ARRAY_BUFFER, 0);\n    }\n    template<typename T>\n    void Attribute<T>::Set(std::vector<T>& input) {\n        Set(&input[0], (unsigned int)input.size());\n    }\n    ```\n\n5.  `SetAttribPointer`功能包装`glVertesAttribPointer`或`glVertesAttribIPointer`。根据`Attribute`类的类型，参数和要调用的函数是不同的。要消除任何歧义，请为所有支持的模板类型提供显式实现。首先执行`int`、`ivec4`和`float`类型:\n\n    ```cpp\n    template<>\n    void Attribute<int>::SetAttribPointer(unsigned int s) {\n       glVertexAttribIPointer(s, 1, GL_INT, 0, (void*)0);\n    }\n    template<>\n    void Attribute<ivec4>::SetAttribPointer(unsigned int s){\n       glVertexAttribIPointer(s, 4, GL_INT, 0, (void*)0);\n    }\n    template<>\n    void Attribute<float>::SetAttribPointer(unsigned int s){\n       glVertexAttribPointer(s,1,GL_FLOAT,GL_FALSE,0,0);\n    }\n    ```\n\n6.  接下来执行`vec2`、`vec3`和`vec4`类型。这些都与`float`型非常相似。唯一不同的是`glVertexAttribPointer`的第二个论点:\n\n    ```cpp\n    template<>\n    void Attribute<vec2>::SetAttribPointer(unsigned int s) {\n       glVertexAttribPointer(s,2,GL_FLOAT,GL_FALSE,0,0);\n    }\n    template<>\n    void Attribute<vec3>::SetAttribPointer(unsigned int s){\n       glVertexAttribPointer(s,3,GL_FLOAT,GL_FALSE,0,0);\n    }\n    template<>\n    void Attribute<vec4>::SetAttribPointer(unsigned int s){\n       glVertexAttribPointer(s,4,GL_FLOAT,GL_FALSE,0,0);\n    }\n    ```\n\n7.  `Attribute`类的最后两个函数需要将属性绑定和解除绑定到`Shader`类中指定的插槽。由于`glVertexAttribPointer`函数基于`Attribute`类的模板类型而不同，`Bind`将调用`SetAttribPointer`辅助函数:\n\n    ```cpp\n    template<typename T>\n    void Attribute<T>::BindTo(unsigned int slot) {\n        glBindBuffer(GL_ARRAY_BUFFER, mHandle);\n        glEnableVertexAttribArray(slot);\n        SetAttribPointer(slot);\n        glBindBuffer(GL_ARRAY_BUFFER, 0);\n    }\n    template<typename T>\n    void Attribute<T>::UnBindFrom(unsigned int slot) {\n        glBindBuffer(GL_ARRAY_BUFFER, mHandle);\n        glDisableVertexAttribArray(slot);\n        glBindBuffer(GL_ARRAY_BUFFER, 0);\n    }\n    ```\n\n`Attribute`每个顶点的数据变化。你还需要设置另一种类型的数据:制服。与属性不同，制服在着色器程序的整个执行过程中保持不变。您将在下一部分实施制服。\n\n# 穿着制服工作\n\n与属性不同，制服是不变的数据；它们被设置一次。对于处理的所有顶点，统一的值保持不变。制服可以创建为数组，这是您将在后面的章节中用来实现网格蒙皮的功能。\n\n像`Attribute`类一样，`Uniform`类也将被模板化。然而，与属性不同的是，永远不会有`Uniform`类的实例。它只需要公共静态函数。对于每种统一类型，有三个函数:一个用于设置单个统一值，一个用于设置统一值数组，还有一个方便函数用于设置值数组，但使用向量进行输入。\n\n## 统一类申报\n\n创建新文件，`Uniform.h`。您将在这个新文件中实现`Uniform`类。`Uniform`类永远不会被实例化，因为这个类不会有任何实例。禁用构造函数并复制构造函数、赋值运算符和析构函数。该类将拥有一个静态`Set`函数的三个重载。需要为每个模板类型指定`Set`功能:\n\n```cpp\ntemplate <typename T>\nclass Uniform {\nprivate:\n  Uniform();\n  Uniform(const Uniform&);\n  Uniform& operator=(const Uniform&);\n  ~Uniform();\npublic:\n  static void Set(unsigned int slot, const T& value);\n  static void Set(unsigned int slot,T* arr,unsigned int len);\n  static void Set(unsigned int slot, std::vector<T>& arr);\n};\n```\n\n你刚刚完成了`Uniform`班的申报。在下一节中，您将开始实现`Uniform`类。\n\n## 实现统一类\n\n创建新文件，`Uniform.cpp`。您将在这个新文件中实现`Uniform`类。像`Attribute`类一样，`Uniform`类也是模板化的。\n\n在 OpenGL 中，制服是用`glUniform***`系列函数设置的。整数、浮点数、向量、矩阵等等都有不同的函数。您希望为这些类型中的每一种提供`Set`方法的实现，但是避免编写几乎相同的代码。\n\n为了避免编写几乎相同的代码，您将声明一个# `define`宏。这个宏将采用三个参数——要调用的 OpenGL 函数、统一类的模板类型和 OpenGL 函数的数据类型:\n\n1.  添加以下代码来定义支持的统一类型的模板规范:\n\n    ```cpp\n    template Uniform<int>;\n    template Uniform<ivec4>;\n    template Uniform<ivec2>;\n    template Uniform<float>;\n    template Uniform<vec2>;\n    template Uniform<vec3>;\n    template Uniform<vec4>;\n    template Uniform<quat>;\n    template Uniform<mat4>;\n    ```\n\n2.  您只需要为每种类型实现其中一个`Set`方法——一个需要数组和长度的方法。其他`Set`方法重载是为了方便。实现两个便利重载——其中一个用于设置单个统一，另一个用于设置向量。两个重载都应该只调用`Set`函数:\n\n    ```cpp\n    template <typename T>\n    void Uniform<T>::Set(unsigned int slot,const T& value){\n        Set(slot, (T*)&value, 1);\n    }\n    template <typename T>\n    void Uniform<T>::Set(unsigned int s,std::vector<T>& v){\n        Set(s, &v[0], (unsigned int)v.size());\n    }\n    ```\n\n3.  创建`UNIFORM_IMPL`宏。第一个参数将是调用哪个 OpenGL 函数，第二个参数是正在使用的类型的结构，最后一个参数是相同结构的数据类型。`UNIFORM_IMPL`宏将这些信息组合成一个函数声明:\n\n    ```cpp\n    #define UNIFORM_IMPL(gl_func, tType, dType) \\\n    template<> void Uniform<tType>::Set(unsigned int slot,\\\n                       tType* data, unsigned int length) {\\\n        gl_func(slot, (GLsizei)length, (dType*)&data[0]); \\\n    }\n    ```\n\n4.  为每种统一的数据类型调用`UNIFORM_IMPL`宏，生成合适的`Set`函数。这种方法唯一不起作用的数据类型是`mat4` :\n\n    ```cpp\n    UNIFORM_IMPL(glUniform1iv, int, int)\n    UNIFORM_IMPL(glUniform4iv, ivec4, int)\n    UNIFORM_IMPL(glUniform2iv, ivec2, int)\n    UNIFORM_IMPL(glUniform1fv, float, float)\n    UNIFORM_IMPL(glUniform2fv, vec2, float)\n    UNIFORM_IMPL(glUniform3fv, vec3, float)\n    UNIFORM_IMPL(glUniform4fv, vec4, float)\n    UNIFORM_IMPL(glUniform4fv, quat, float)\n    ```\n\n5.  矩阵的`Set`功能需要手动指定；否则，`UNIFORM_IMPL`宏将不起作用。这是因为`glUniformMatrix4fv`函数接受了一个额外的布尔参数，询问矩阵是否应该转置。将转置布尔设置为`false` :\n\n    ```cpp\n    template<> void Uniform<mat4>::Set(unsigned int slot, \n            mat4* inputArray, unsigned int arrayLength) {\n        glUniformMatrix4fv(slot, (GLsizei)arrayLength, \n                           false, (float*)&inputArray[0]);\n    }\n    ```\n\n在本节中，您在制服概念的基础上构建了一个抽象层。在下一节中，您将实现类似于属性的索引缓冲区。\n\n# 使用索引缓冲区\n\n索引缓冲区是一种属性。与属性不同，索引缓冲区绑定到`GL_ELEMENT_ARRAY_BUFFER`并且可以是用于绘制图元。因此，您将在自己的类中实现索引缓冲区，而不是重用`Attribute`类。\n\n## 索引缓冲区类声明\n\n创建新文件，`IndexBuffer.h`。您将把`IndexBuffer`类的声明添加到这个新文件中。像一个`Attribute`对象一样，`IndexBuffer`将包含一个 OpenGL 句柄和一个计数，两者都有 getter 函数。\n\n需要禁用复制构造函数和赋值运算符，以避免多个`IndexBuffer`对象引用同一个 OpenGL 缓冲区。`Set`函数接受一个无符号整数数组和数组的长度，但是也有一个方便的重载接受一个向量:\n\n```cpp\nclass IndexBuffer {\npublic:\n    unsigned int mHandle;\n    unsigned int mCount;\nprivate:\n    IndexBuffer(const IndexBuffer& other);\n    IndexBuffer& operator=(const IndexBuffer& other);\npublic:\n    IndexBuffer();\n    ~IndexBuffer();\n    void Set(unsigned int* rr, unsigned int len);\n    void Set(std::vector<unsigned int>& input);\n    unsigned int Count();\n    unsigned int GetHandle();\n};\n```\n\n在本节中，您声明了一个新的`IndexBuffer`类。在下一节中，您将开始实现实际的索引缓冲区。\n\n## 实现索引缓冲类\n\n索引缓冲区允许您使用索引几何图形渲染模型。想一个人体模型；网格中几乎所有的三角形都将被连接。这意味着许多三角形可能共享一个顶点。不是存储每个顶点，而是只存储唯一的顶点。索引到唯一顶点列表的缓冲区，即索引缓冲区，用于从唯一顶点创建三角形，如下所示:\n\n1.  创建新文件，`IndexBuffer.cpp`。您将在这个文件中实现`IndexBuffer`类。构造器需要生成一个新的 OpenGL 缓冲区，析构器需要删除该缓冲区:\n\n    ```cpp\n    IndexBuffer::IndexBuffer() {\n        glGenBuffers(1, &mHandle);\n        mCount = 0;\n    }\n    IndexBuffer::~IndexBuffer() {\n        glDeleteBuffers(1, &mHandle);\n    }\n    ```\n\n2.  用于计数的 getter 函数和`IndexBuffer`对象内部的 OpenGL 句柄是微不足道的:\n\n    ```cpp\n    unsigned int IndexBuffer::Count() {\n        return mCount;\n    }\n    unsigned int IndexBuffer::GetHandle() {\n        return mHandle;\n    }\n    ```\n\n3.  `IndexBuffer`类的`Set`功能需要绑定`GL_ELEMENT_ARRAY_BUFFER`。除此之外，逻辑与属性相同:\n\n    ```cpp\n    void IndexBuffer::Set(unsigned int* inputArray, unsigned int arrayLengt) {\n        mCount = arrayLengt;\n        unsigned int size = sizeof(unsigned int);\n        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mHandle);\n        glBufferData(GL_ELEMENT_ARRAY_BUFFER, size * mCount, inputArray, GL_STATIC_DRAW);\n        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);\n    }\n    void IndexBuffer::Set(std::vector<unsigned int>& input) {\n        Set(&input[0], (unsigned int)input.size());\n    }\n    ```\n\n在本节中，您围绕索引缓冲区构建了一个抽象。在下一节中，您将学习如何使用索引缓冲区和属性来渲染几何图形。\n\n# 渲染几何图形\n\n您有处理顶点数据、制服和索引缓冲区的类，但没有任何代码来绘制它们。绘图将由四个全局功能处理。你会有两个`Draw`功能和两个`DrawInstanced`功能。您可以使用或不使用索引缓冲区来绘制几何图形。\n\n创建新文件，`Draw.h`。您将在该文件中实现`Draw`功能，如下所示:\n\n1.  声明一个`enum`类，该类定义了应该用于绘制的图元。大多数情况下，您只需要线条、点或三角形，但一些附加类型可能会有用:\n\n    ```cpp\n    enum class DrawMode {\n        Points,\n        LineStrip,\n        LineLoop,\n        Lines,\n        Triangles,\n        TriangleStrip,\n        TriangleFan\n    };\n    ```\n\n2.  接下来，声明`Draw`功能。`Draw`函数有两个重载——一个采用索引缓冲区和绘制模式，另一个采用顶点计数和绘制模式:\n\n    ```cpp\n    void Draw(IndexBuffer& inIndexBuffer, DrawMode mode);\n    void Draw(unsigned int vertexCount, DrawMode mode);\n    ```\n\n3.  像`Draw`一样，声明两个`DrawInstanced`函数。这些函数有一个相似的签名，但是有一个额外的参数——`instanceCount`。这个`instanceCount`变量控制将渲染多少几何实例:\n\n    ```cpp\n    void DrawInstanced(IndexBuffer& inIndexBuffer, \n             DrawMode mode, unsigned int instanceCount);\n    void DrawInstanced(unsigned int vertexCount, \n             DrawMode mode, unsigned int numInstances);\n    ```\n\n创建新文件，`Draw.cpp`。您将在此文件中实现与图形相关的功能，如下所示:\n\n1.  您需要能够将`DrawMode`枚举转换为`GLenum`。我们将使用静态助手功能来实现这一点。这个函数唯一需要做的就是弄清楚输入绘制模式是什么，并返回适当的`GLenum`值:\n\n    ```cpp\n    static GLenum DrawModeToGLEnum(DrawMode input) {\n        switch (input) {\n            case DrawMode::Points: return  GL_POINTS;\n            case DrawMode::LineStrip: return GL_LINE_STRIP;\n            case DrawMode::LineLoop: return  GL_LINE_LOOP;\n            case DrawMode::Lines: return  GL_LINES;\n            case DrawMode::Triangles: return  GL_TRIANGLES;\n            case DrawMode::TriangleStrip: \n                           return  GL_TRIANGLE_STRIP;\n            case DrawMode::TriangleFan: \n                           return   GL_TRIANGLE_FAN;\n        }\n        cout << \"DrawModeToGLEnum unreachable code hit\\n\";\n        return 0;\n    }\n    ```\n\n2.  进行顶点计数的`Draw`和`DrawInstanced`函数实现起来很简单。`Draw`需要调用`glDrawArrays`，`DrawInstanced`需要调用`glDrawArraysInstanced` :\n\n    ```cpp\n    void Draw(unsigned int vertexCount, DrawMode mode) {\n        glDrawArrays(DrawModeToGLEnum(mode), 0, vertexCount);\n    }\n    void DrawInstanced(unsigned int vertexCount, \n         DrawMode mode, unsigned int numInstances) {\n        glDrawArraysInstanced(DrawModeToGLEnum(mode), \n                              0, vertexCount, numInstances);\n    }\n    ```\n\n3.  获取索引缓冲区的`Draw`和`Drawinstanced`函数需要将索引缓冲区绑定到`GL_ELEMENT_ARRAY_BUFFER`，然后调用`glDrawElements`和【T4:\n\n    ```cpp\n    void Draw(IndexBuffer& inIndexBuffer, DrawMode mode) {\n        unsigned int handle = inIndexBuffer.GetHandle();\n        unsigned int numIndices = inIndexBuffer.Count();\n        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, handle);\n        glDrawElements(DrawModeToGLEnum(mode), \n                       numIndices, GL_UNSIGNED_INT, 0);\n        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);\n    }\n    void DrawInstanced(IndexBuffer& inIndexBuffer, \n             DrawMode mode, unsigned int instanceCount) {\n        unsigned int handle = inIndexBuffer.GetHandle();\n        unsigned int numIndices = inIndexBuffer.Count();\n        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, handle);\n        glDrawElementsInstanced(DrawModeToGLEnum(mode),\n            numIndices, GL_UNSIGNED_INT, 0, instanceCount);\n        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);\n    }\n    ```\n\n到目前为止，您已经编写了加载着色器、创建和绑定 GPU 缓冲区以及将制服传递给着色器的代码。既然绘图代码也已经实现，就可以开始显示几何图形了。\n\n在下一节中，您将学习如何使用纹理使渲染的几何图形看起来更有趣。\n\n# 处理纹理\n\n你将在这本书里写的所有着色器都假设被渲染的物体的漫射颜色来自于一个纹理。纹理将从`.png`文件加载。所有图像加载将通过`stb_image`完成。\n\n`Stb`是单文件公共领域库的集合。我们只使用图像加载器；你可以在[https://github.com/nothings/stb](https://github.com/nothings/stb)的 GitHub 上找到整个`stb`系列。\n\n## 添加 stb_image\n\n你将使用`stb_image`来加载纹理。您可以从[https://github.com/nothings/stb/blob/master/stb_image.h](https://github.com/nothings/stb/blob/master/stb_image.h)获得头文件的副本。将`stb_image.h`头文件添加到项目中。\n\n创建新文件，`stb_image.cpp`。这个文件只需要声明`stb_image`实现宏并包含头文件。应该是这样的:\n\n```cpp\n#define STB_IMAGE_IMPLEMENTATION\n#include \"stb_image.h\"\n```\n\n## 纹理类声明\n\n创建新文件，`Texture.h`。您将在该文件中声明`Texture`类。`Texture`级只需要一个几个重要的功能。它需要能够从文件中加载纹理，将纹理索引绑定到统一索引，并停用纹理索引。\n\n除了核心函数之外，该类应该有一个默认构造函数、一个获取文件路径的便利构造函数、一个析构函数和一个包含在`Texture`类内部的 OpenGL 句柄的获取器。应该禁用复制构造函数和赋值操作符，以避免两个`Texture`类引用同一个 OpenGL 纹理句柄:\n\n```cpp\nclass Texture {\nprotected:\n    unsigned int mWidth;\n    unsigned int mHeight;\n    unsigned int mChannels;\n    unsigned int mHandle;\nprivate:\n    Texture(const Texture& other);\n    Texture& operator=(const Texture& other);\npublic:\n    Texture();\n    Texture(const char* path);\n    ~Texture();\n    void Load(const char* path);\n    void Set(unsigned int uniform, unsigned int texIndex);\n    void UnSet(unsigned int textureIndex);\n    unsigned int GetHandle();\n};\n```\n\n## 实现纹理类\n\n创建新文件，`Texture.cpp`。`Texture`类的定义将包含在这个文件中。`Texture`类的默认构造器需要将所有成员变量设置为`0`，然后生成一个 OpenGL 句柄。\n\n`Load`函数可能是`Texture`类中最重要的函数；它负责加载图像文件。图像文件的实际解析将由`stbi_load`处理:\n\n1.  便利构造器生成一个新的句柄，然后调用`Load`函数，该函数将初始化其余的类成员变量，因为`Texture`类的每个实例都持有一个有效的纹理句柄:\n\n    ```cpp\n    Texture::Texture() {\n        mWidth = 0;\n        mHeight = 0;\n        mChannels = 0;\n        glGenTextures(1, &mHandle);\n    }\n    Texture::Texture(const char* path) {\n        glGenTextures(1, &mHandle);\n        Load(path);\n    }\n    Texture::~Texture() {\n        glDeleteTextures(1, &mHandle);\n    }\n    ```\n\n2.  `stbi_load`获取图像文件的路径，并引用图像中通道的宽度、高度和数量。最后一个参数指定每个像素的组件数量。通过将其设置为`4`，所有纹理都加载了 RGBA 通道。接下来，使用`glTexImage2D`将纹理上传到图形处理器，使用`glGenerateMipmap`为图像生成合适的纹理贴图。将环绕模式设置为重复:\n\n    ```cpp\n    void Texture::Load(const char* path) {\n        glBindTexture(GL_TEXTURE_2D, mHandle);\n        int width, height, channels;\n        unsigned char* data = stbi_load(path, &width, \n                                        &height, \n                                        &channels, 4);\n        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, \n           height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);\n        glGenerateMipmap(GL_TEXTURE_2D);\n        stbi_image_free(data);\n        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, \n                        GL_REPEAT);\n        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, \n                        GL_REPEAT);\n        glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,\n                        GL_NEAREST_MIPMAP_LINEAR);\n        glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,\n                        GL_LINEAR);\n        glBindTexture(GL_TEXTURE_2D, 0);\n        mWidth = width;\n        mHeight = height;\n        mChannels = channels;\n    }\n    ```\n\n3.  `Set`函数需要激活一个纹理单元，将`Texture`类包含的句柄绑定到该纹理单元，然后设置指定的统一索引来包含当前绑定的纹理单元。`Unset`功能解除当前纹理与指定纹理单位的绑定:\n\n    ```cpp\n    void Texture::Set(unsigned int uniformIndex, \n                      unsigned int textureIndex) {\n        glActiveTexture(GL_TEXTURE0 + textureIndex);\n        glBindTexture(GL_TEXTURE_2D, mHandle);\n        glUniform1i(uniformIndex, textureIndex);\n    }\n    void Texture::UnSet(unsigned int textureIndex) {\n        glActiveTexture(GL_TEXTURE0 + textureIndex);\n        glBindTexture(GL_TEXTURE_2D, 0);\n        glActiveTexture(GL_TEXTURE0);\n    }\n    ```\n\n4.  `GetHandle` getter 函数很简单:\n\n    ```cpp\n    unsigned int Texture::GetHandle() {\n        return mHandle;\n    }\n    ```\n\n`Texture`类将始终使用相同的 mipmap 级别和包装参数加载纹理。对于本书中的样本来说，这应该足够了。您可能想尝试为这些属性添加吸气剂和设置剂。\n\n在下一节中，您将实现顶点和片段着色器程序，这是绘制某些东西所需的最后一步。\n\n# 简单着色器\n\n渲染抽象完成。在绘制任何东西之前，你需要编写着色器来指导如何绘制东西。在本节中，您将编写一个顶点和一个片段着色器。碎片着色器将在本书的其余部分中使用，本书后面部分中使用的顶点着色器将是这里介绍的着色器的变体。\n\n## 顶点着色器\n\n顶点着色器负责将模型的每个顶点通过模型、视图和投影管道，并将任何所需的光照数据传递给片段着色器。创建新文件，`static.vert`。您将在这个文件中实现顶点着色器。\n\n顶点着色器采用三种统一的格式——模型、视图和投影矩阵。变换一个顶点需要这些制服。每个单独的顶点由三个属性组成——位置、法线和一些纹理坐标。\n\n顶点着色器向片段着色器输出三个变量，即世界空间中的法线和片段位置以及纹理坐标:\n\n```cpp\n#version 330 core\nuniform mat4 model;\nuniform mat4 view;\nuniform mat4 projection;\nin vec3 position;\nin vec3 normal;\nin vec2 texCoord;\nout vec3 norm;\nout vec3 fragPos;\nout vec2 uv;\nvoid main() {\n    gl_Position = projection * view * model * \n                  vec4(position, 1.0);\n\n    fragPos = vec3(model * vec4(position, 1.0));\n    norm = vec3(model * vec4(normal, 0.0f));\n    uv = texCoord;\n}\n```\n\n这是一个最小顶点着色器；它仅通过模型视图和投影管道放置顶点。该着色器可用于显示静态几何图形或 CPU 蒙皮网格。在下一节中，您将实现一个片段着色器。\n\n## 片段着色器\n\n创建新文件，`lit.frag`。该文件中的片段着色器将在本书的其余部分中使用。一些章节将引入新的顶点着色器，但是片段着色器将一直保持这个。\n\n片段着色器从纹理中获取对象的漫射颜色，然后应用单向光。灯光模型只是 *N* 点 *L* 。由于光线没有环境术语，模型的某些部分可能显示为全黑:\n\n```cpp\n#version 330 core\nin vec3 norm;\nin vec3 fragPos;\nin vec2 uv;\nuniform vec3 light;\nuniform sampler2D tex0;\nout vec4 FragColor;\nvoid main() {\n    vec4 diffuseColor = texture(tex0, uv);\n    vec3 n = normalize(norm);\n    vec3 l = normalize(light);\n    float diffuseIntensity = clamp(dot(n, l), 0, 1);\n    FragColor = diffuseColor * diffuseIntensity;\n}\n```\n\n重要信息:\n\n想了解更多关于 OpenGL 中灯光模型的吗？前往[https://learnopengl.com/Lighting/Basic-Lighting](https://learnopengl.com/Lighting/Basic-Lighting)。\n\n这是一个简单的片段着色器；漫射颜色是通过采样纹理获得的，强度是简单的定向光。\n\n# 总结\n\n在本章中，您学习了如何在 OpenGL API 之上编写抽象层。大部分情况下，在本书的剩余部分中，您将使用这些类来绘制东西，但是一些零星的 OpenGL 调用可能会在我们的代码中到处出现。\n\n以这种方式抽象 OpenGL 将让未来的章节专注于动画，而不必担心底层的 API。将这个应用编程接口移植到其他后端应该也很简单。\n\n本章有两个示例——`Chapter06/Sample00`，这是到目前为止使用的代码，以及`Chapter06/Sample01`，这显示了一个简单的纹理和照明平面旋转到位。`Sample01`是一个很好的例子，说明如何使用你到目前为止编写的代码。\n\n`Sample01`还包括一个效用类`DebugDraw`，本书不会涉及。该类位于`DebugDraw.h`和`DebugDraw.cpp`。`DebugDraw`类可以用一个简单的应用编程接口快速绘制调试线。`DebugDraw`班效率不是很高；它只是用来调试的。\n\n在下一章中，您将开始探索 glTF 文件格式。glTF 是一种标准格式，可以存储网格和动画数据。这是本书其余部分将使用的格式。"
  },
  {
    "path": "docs/handson-cpp-game-ani-prog/07.md",
    "content": "# 七、探索 glTF 文件格式\n\n在本章中，我们将探索 glTF，一种包含显示动画模型所需的所有内容的文件格式。这是大多数三维内容创建应用可以导出的标准格式，允许您加载任意模型。\n\n本章重点介绍文件格式本身。后面的章节将集中在实现加载 glTF 文件的相关部分。到本章结束时，您应该对 glTF 文件格式有了坚实的了解。\n\n本章将着重于培养以下技能:\n\n*   了解 glTF 文件中包含哪些数据\n*   使用 cgltf 实现 glTF 加载\n*   学习如何从 Blender 导出 glTF 文件\n\n# 技术要求\n\n本章将涵盖加载和显示动画模型所需的 glTF 文件的每个概念。然而，这一章并不是文件格式的完整指南。在阅读本章之前，请花几分钟时间阅读[https://www.khronos.org/files/gltf20-reference-guide.pdf](https://www.khronos.org/files/gltf20-reference-guide.pdf)的参考指南，以熟悉 glTF 格式。\n\n您将使用 cgltf([https://github.com/jkuhlmann/cgltf](https://github.com/jkuhlmann/cgltf))来解析 gltf 文件。如果 glTF 文件显示不正确，它可能是一个坏文件。如果您怀疑某个文件可能是坏的，请在[https://gltf-viewer.donmccurdy.com/](https://gltf-viewer.donmccurdy.com/)的 glTF 参考查看器中进行检查。\n\n# 探索 glTF 文件的存储方式\n\nglTF 文件存储为纯文本 JSON 文件或更紧凑的二进制表示。纯文本变体通常有一个`.gltf`扩展名，而二进制变体通常有一个`.glb`扩展名。\n\n可能有多个文件。glTF 文件可以选择嵌入大块的二进制数据——甚至是纹理——也可以选择将它们存储在外部文件中。这反映在 Blender3D 的 glTF 导出选项的以下截图中:\n\n![Figure 7.1: Blender3D’s glTF export options ](img/Figure_7.1_B16191.jpg)\n\n图 7.1: Blender3D 的 glTF 导出选项\n\n提供的样本文件本书的可下载内容存储为 glTF 嵌入文件(`.gltf`)。这是 glTF 的纯文本变体，可以用任何文本编辑器进行检查。更重要的是，它是一个需要跟踪的单一文件。即使本书提供的文件是 glTF 嵌入式格式，最终代码也将支持加载二进制格式和单独的文件(`.bin`)。\n\n现在，您已经探索了存储 glTF 文件的不同方式，让我们准备好了解存储在 glTF 文件中的内容。glTF 文件旨在存储整个场景，而不仅仅是单个模型。在下一节中，您将探索 glTF 文件的预期用途。\n\n## glTF 文件存储的是场景，而不是模型\n\n重要的是要知道，glTF 文件意味着代表整个三维场景，而不仅仅是一个单一的动画模型。因此，glTF 支持您不需要在动画中使用的功能，例如相机和 PBR 材质。对于动画，我们只关心使用一小部分支持的功能。让我们概述一下它们是什么。\n\nglTF 文件可以包含不同类型的网格。它包含静态网格，比如道具。这些网格仅由它们所附着的节点的动画来移动；它可以包含变形目标。变形动画可以用于面部表情等。\n\nglTF 文件也可以包含蒙皮网格。这些是您将用于动画角色的网格。蒙皮网格描述模型的顶点如何受到模型的变换层次(或骨架)的影响。使用蒙皮网格，网格的每个顶点都可以绑定到层次结构中的关节。随着层级动画化，网格变形。\n\n事实上，glTF 旨在描述一个场景，而不是一个单一的模型，这将使一些加载代码有点棘手。在下一节中，您将从高级角度开始探索 glTF 文件的实际内容。\n\n# 探索 glTF 格式\n\nglTF 文件的根是场景。一个 glTF 文件可以包含一个或多个场景。场景包含一个或多个节点。一个节点可以有皮肤、网格、动画、相机、灯光或混合权重。网格、皮肤和动画都在缓冲区中存储大量信息。要访问缓冲区，它们包含一个包含缓冲区视图的访问器，缓冲区视图又包含缓冲区。\n\n通过文本提供的描述可能很难理解。下图说明了所描述的文件布局。由于 glTF 是一种场景描述格式，所以有相当多的数据类型我们不必在意。下一节将探讨这些问题:\n\n![Figure 7.2: The contents of a glTF file ](img/Figure_7.2_B16191.jpg)\n\n图 7.2:glTF 文件的内容\n\n现在，您已经对存储在 glTF 文件中的内容有了一个的概念，接下来的部分将探讨蒙皮动画所需的文件格式部分。\n\n## 动画需要的部分\n\n使用 glTF 文件加载动画模型时，文件所需的组件是场景、节点、网格和皮肤。这是一个可以使用的小子集；下图中突出显示了这些位及其关系。这些数据类型之间的关系可以描述如下:\n\n![Figure 7.3: Parts of a glTF file used for skinned animation ](img/Figure_7.3_B16191.jpg)\n\n图 7.3:用于蒙皮动画的部分 glTF 文件\n\n上图省略了每个数据结构中的大部分数据，而是只关注实现蒙皮动画所需的内容。在下一节中，我们将探索 glTF 文件的哪些部分不需要蒙皮动画。\n\n## 动画不需要的部分\n\n要实现蒙皮动画，您不需要灯光、相机、材质、纹理、图像和采样器。在下一节中，您将探索如何从 glTF 文件中实际读取数据。\n\n## 访问数据\n\n访问数据变得有点棘手，但是不太难。网格、皮肤和动画对象都包含一个 glTF 访问器。这个**访问器**引用了一个**缓冲区视图**，而缓冲区视图引用了一个**缓冲区**。下图展示了这种关系:\n\n![Figure 7.4: Accessing data in a glTF file ](img/Figure_7.4_B16191.jpg)\n\n图 7.4:访问 glTF 文件中的数据\n\n给定这三个独立的步骤，如何访问缓冲区数据？在下一节中，您将学习如何使用缓冲区视图以及最后的访问器从缓冲区中解释数据。\n\n### 缓冲器\n\n把一个缓冲区想象成一个 OpenGL 缓冲区。它只是一个大的线性数组。这类似于你在 [*第 6 章*](06.html#_idTextAnchor104)*构建抽象渲染器*中构建的`Attributes`类。`Attributes`类的`Set`函数调用`glBufferData`，其签名如下:\n\n```cpp\nvoid glBufferData(GLenum target, GLsizeiptr size, \n                  void * data, GLenum usage);\n```\n\nglTF 中的一个缓冲区包含调用`glBufferData`函数所需的所有信息。它包含一个大小、一个空指针和可选的偏移量，这些偏移量只修改源指针和大小。把 glTF 缓冲区想象成用数据填充 OpenGL 缓冲区所需的一切。\n\n在下一节中，您将学习如何将缓冲区视图与缓冲区结合使用。\n\n### 缓冲视图\n\n缓冲区只是一些大块的数据。缓冲区中存储的内容没有上下文。这就是缓冲区视图的作用。缓冲区视图描述缓冲区中的内容。如果一个缓冲区包含`glBufferData`的信息，那么一个缓冲区视图包含一些调用`glVertexAttribPointer`的参数。`glVertexAttribPointer`功能有以下签名:\n\n```cpp\nvoid glVertexAttribPointer(GLuint index, GLint size, \n                           GLenum type, GLboolean normalized,\n                           GLsizei stride, void * pointer);\n```\n\n缓冲区视图包含`type`，它决定视图是顶点缓冲区还是索引缓冲区。这很重要，因为顶点缓冲区绑定到`GL_ARRAY_BUFFER`，但是索引缓冲区绑定到`GL_ELEMENT_ARRAY_BUFFER`。在 [*第 6 章*](06.html#_idTextAnchor104)*构建抽象渲染器*中，我们为这些不同的缓冲区类型构建了两个不同的类。\n\n与缓冲区一样，缓冲区视图也包含一些可选的偏移量，这些偏移量进一步修改源指针的位置及其大小。在下一节中，您将探索如何使用描述缓冲区视图内容的访问器。\n\n### 存取器\n\n存取器存储更高级别的信息。最重要的是，访问者描述了您正在处理的数据类型，如`scalar`、`vec2`、`vec3`或`vec4`。`glVertexAttribPointer`的`size`论证就是利用这个数据确定的。\n\n访问者回答诸如数据是否被规范化以及数据的存储模式是什么之类的问题。除了缓冲区和缓冲区视图已经包含的信息之外，访问器还包含附加的偏移量、大小和步幅信息。\n\n下一节将演示如何将数据从 glTF 文件加载到线性标量数组中。\n\n### 例子\n\n即使有了访问器、缓冲区视图和缓冲区布局的关系，解析数据仍然可能有点混乱。为了稍微弄清楚一点，让我们探索一下如何将一个访问器转换为浮点值的平面列表。以下代码旨在作为示例；本书的其余部分不会用到它:\n\n```cpp\nvector<float> GetPositions(const GLTFAccessor& accessor) {\n    // Accessors and sanity checks\n    assert(!accessor.isSparse);\n    const GLTFBufferView& bufferView = accessor.bufferView;\n    const GLTFBuffer& buffer = bufferView.buffer;\n    // Resize result\n    // GetNumComponents Would return 3 for a vec3, etc.\n    uint numComponents = GetNumComponents(accessor); \n    vector<float> result;\n    result.resize(accessor.count * numComponents);\n    // Loop trough every element in the accessor\n    for (uint i = 0; i < accessor.count; ++ i) {\n        // Find where in the buffer the data actually starts\n        uint offset = accessor.offset + bufferView.offset;\n        uint8* data = buffer.data;\n        data += offset + accessor.stride * i;\n        // Loop trough every component of current element\n        float* target = result[i] * componentCount;\n        for (uint j = 0; j < numComponents; ++ j) {\n            // Omitting normalization \n            // Omitting different storage types\n            target[j] = data + componentCount * j;\n        } // End loop of every component of current element\n    } // End loop of every accessor element\n    return result;\n}\n```\n\n解析 glTF 文件的代码会变得冗长；在前面的代码示例中，已经解析了 glTF 文件。加载 glTF 文件的大部分工作实际上是解析二进制或 JSON 数据。在下一节中，我们将探讨如何使用 cgltf 库来解析 gltf 文件。\n\n# 探索 cgltf\n\n在最后一节中，我们探讨了如何将 glTF 访问器转换为浮点数的线性数组。该代码省略了一些更复杂的任务，例如标准化数据或处理不同的存储类型。\n\n提供的示例代码还假设数据已经解析出 JSON(或二进制)格式。编写一个 JSON 解析器不在本书的范围内，但是处理 glTF 文件却不是。\n\n为了帮助管理加载 glTF 文件的一些复杂性，以及避免从头开始编写 JSON 解析器，下一节将教您如何使用 cgltf 加载 JSON 文件。Cgltf 是单头 glTF 加载库；你可以在 https://github.com/jkuhlmann/cgltf 的 GitHub 上找到它。在下一节中，我们将开始将 cgltf 集成到我们的项目中。\n\n## 整合 cgltf\n\n要将 cgltf 集成到项目中，请从位于[https://github.com/jkuhlmann/cgltf/blob/master/cgltf.h](https://github.com/jkuhlmann/cgltf/blob/master/cgltf.h)的 GitHub 下载头文件。然后，将这个头文件添加到项目中。接下来，向项目中添加一个新的`.c`文件，并将其命名为`cgltf.c`。该文件应包含以下代码:\n\n```cpp\n#pragma warning(disable : 26451)\n#define _CRT_SECURE_NO_WARNINGS\n#define CGLTF_IMPLEMENTATION\n#include \"cgltf.h\"\n```\n\nCGLTF 现已集成到项目中。在本章中，您将实现解析 glTF 文件的代码。如何将 glTF 文件的内容加载到运行时数据中，将在后面的章节中随着该运行时数据的代码的编写而介绍。在下一节中，我们将学习如何实现 glTF 解析代码。\n\n### 创建 glTF 加载程序\n\n在这一节中，我们将探讨如何使用 cgltf 加载一个 glTF 文件。将文件加载到运行时数据结构`cgltf_data`中的代码很简单。在以后的章节中，您将学习如何解析这个`cgltf_data`结构的内容。\n\n要加载一个文件，需要创建一个`cgltf_options`的实例。您不需要设置任何选项标志；只需为所有成员值实例化带有`0`的`cgltf_options`结构。接下来，声明一个`cgltf_data`指针。这个指针将被传递到的地址是`cgltf_parse_file`。在`cgltf_parse_file`填充了`cgltf_data`结构之后，您就可以解析文件的内容了。要稍后释放`cgltf_data`结构，请调用`cgltf_free`:\n\n1.  创建一个包含`cgltf.h`的新文件`GLTFLoader.h`。为`LoadGLTFFile`和`FreeGLTFFile`函数添加函数声明:\n\n    ```cpp\n    #ifndef _H_GLTFLOADER_\n    #define _H_GLTFLOADER_\n    #include \"cgltf.h\"\n    cgltf_data* LoadGLTFFile(const char* path);\n    void FreeGLTFFile(cgltf_data* handle);\n    #endif\n    ```\n\n2.  创建新文件，`GLTFLoader.cpp`。该函数采用一条路径并返回一个`cgltf_data`指针。在内部，该函数调用`cgltf_parse_file`从文件中加载 glTF 数据。`cgltf_load_buffers`用于加载任何外部缓冲区数据。最后，`cgltf_validate`确保刚加载的 glTF 文件有效:\n\n    ```cpp\n    cgltf_data* LoadGLTFFile(const char* path) {\n        cgltf_options options;\n        memset(&options, 0, sizeof(cgltf_options));\n        cgltf_data* data = NULL;\n        cgltf_result result = cgltf_parse_file(&options, \n                                            path, &data);\n        if (result != cgltf_result_success) {\n            cout << \"Could not load: \" << path << \"\\n\";\n            return 0;\n        }\n        result = cgltf_load_buffers(&options, data, path);\n        if (result != cgltf_result_success) {\n            cgltf_free(data);\n            cout << \"Could not load: \" << path << \"\\n\";\n            return 0;\n        }\n        result = cgltf_validate(data);\n        if (result != cgltf_result_success) {\n            cgltf_free(data);\n            cout << \"Invalid file: \" << path << \"\\n\";\n            return 0;\n        }\n        return data;\n    }\n    ```\n\n3.  同样在`GLTFLoader.cpp`中实现`FreeGLTFFile`功能。这个功能很简单；如果输入指针不是`null`，它需要调用`cgltf_free`:\n\n    ```cpp\n    void FreeGLTFFile(cgltf_data* data) {\n        if (data == 0) {\n            cout << \"WARNING: Can't free null data\\n\";\n        }\n        else {\n            cgltf_free(data);\n        }\n    }\n    ```\n\n在后面的章节中，您将通过引入加载网格、姿势和动画的功能来扩展 glTF `Loader`功能。在下一节中，您将探索如何从 Blender3D 导出 glTF 文件。\n\n# 探索样本资产\n\n您将在本书中使用的示例文件是 CC0，来自四元数体上的公共领域许可资产。你可以在[http://quaternius.com/assets.html](http://quaternius.com/assets.html)找到类似风格的附加资产。\n\n此外，后面的章节还包括来自 GDQuest 的开放三维人体模型的截图，可在 https://github.com/GDQuest/godot-3d-mannequin 获得麻省理工学院的许可。\n\n有些资产已经有了 glTF 格式，但有些可能有`.blend`、`.fbx`或其他格式。当这种情况发生时，很容易将模型导入 Blender 并导出一个 glTF 文件。下一节将指导您从 Blender 导出 glTF 文件。\n\n## 从搅拌机导出\n\nBlender 是一个免费的、三维的内容创作工具。你可以从[https://www.blender.org/](https://www.blender.org/)下载搅拌机。以下说明是为 Blender 2.8 编写的，但是它们在更新的版本中也应该是一样的。\n\n如果您正在导入的模型已经是一个`.blend`文件，只需双击它，它就会加载到 Blender 中。\n\n如果模型的格式不同，如`.DAE`或`.FBX`，则需要导入。为此，打开 Blender，您应该会看到默认的场景加载。这个默认场景有一个立方体、一个光源和一个摄像机:\n\n![Figure 7.5: A default Blender3D scene ](img/Figure_7.5_B16191.jpg)\n\n图 7.5:默认的混合 3D 场景\n\n左键点击选择立方体，然后将鼠标悬停在三维视口上，点击*删除*键删除立方体。左键点击相机选择并点击*删除*键删除。对光也这样做。\n\n你现在应该有一个空场景。从**文件**菜单中，选择**文件** | **导入**并选择合适的模型格式进行导入。找到您的文件并双击它以导入它。模型导入后，选择**文件** | **导出 glTF 2.0** 。将导出格式设置为 glTF(文本文件)或 glb(二进制文件)。\n\n# 总结\n\n在本章中，您学习了什么是 glTF 文件，glTF 格式的哪些部分对蒙皮动画有用，以及如何使用 cglTF 加载 glTF 文件。如果格式还是有点混乱，不用担心；当您开始解析 cgltf 文件中的各种数据时，这将更有意义。使用 cgltf 将让您专注于将 gltf 数据转换为有用的运行时结构，而不必担心手动解析 JSON 文件。在下一章中，您将通过实现曲线、帧和轨迹来开始实现动画的构建块。"
  },
  {
    "path": "docs/handson-cpp-game-ani-prog/08.md",
    "content": "# 八、创建曲线、帧和轨迹\n\n在 2000 年代早期，游戏通常采用在诸如 Blender 或 Maya 等 3D 内容创建工具中创作的动画，回放动画，并以设定的时间间隔对动画中每个关节的变换进行采样。一旦动画被采样，游戏的运行时间在采样帧之间线性插值。\n\n虽然这是可行的(并且在 glTF 文件中是可行的)，但它不是回放动画的最准确的方式。它通过包含实际上不需要存在的帧来浪费内存。在 3D 内容创建工具中，使用曲线创建动画，如下图所示:\n\n![Figure 8.1: The Blender 3D curve editor](img/Figure_8.1_B16191.jpg)\n\n图 8.1:搅拌机三维曲线编辑器\n\n现代游戏和动画系统直接评估这些曲线。直接评估动画曲线可以节省内存，但是就处理能力而言，曲线要贵一些。到本章结束时，您应该能够执行以下操作:\n\n*   了解三次贝塞尔样条以及如何计算它们\n*   了解三次埃尔米特样条以及如何计算它们\n*   了解常见的插值方法\n*   能够创建立方体、线性和恒定的关键帧\n*   了解关键帧是如何组成立方体、线性或恒定轨迹的\n*   能够评估三次、线性和恒定轨迹\n*   能够将三个独立的轨道组合成一个变换轨道\n\n# 理解三次贝塞尔样条\n\n要实现游戏动画，需要对曲线有一定的了解。让我们从基础开始——三次贝塞尔样条。贝塞尔样条有两个插值点和两个有助于生成曲线的控制点。这就是三次贝塞尔样条的样子:\n\n![Figure 8.2: A cubic Bézier spline](img/Figure_8.2_B16191.jpg)\n\n图 8.2:三次贝塞尔样条\n\n给定两点和两个控制，曲线是如何生成的？让我们探索在给定时间 **t** 内插曲线。先从 **P1** 到 **C1** ，从 **C1** 到 **C2** ，从 **C2** 到 **P2** 画线。然后，用 **t** 的值沿这些直线线性插值:\n\n![Figure 8.3: Linearly interpolating between points and control points](img/Figure_8.3_B16191.jpg)\n\n图 8.3:点和控制点之间的线性插值\n\n从 **P1** 到 **C1** 的插值点为 **A** ，从 **C2** 到 **P2** 的插值点为 **B** ，从 **C1** 到 **C2** 的插值点为 **C** 。接下来需要重复这个过程，从 **A** 到 **C** 再从 **C** 到 **B** 画线插补。让我们称这些新插值的点为 E 和 F:\n\n![Figure 8.4: Linearly interpolating the results of figure 8.3](img/Figure_8.4_B16191.jpg)\n\n图 8.4:对图 8.3 的结果进行线性插值\n\n再重复一次，从 **E** 到 **F** 画一条线，并沿着这条线通过 **t** 进行插值。我们把叫做结果点 **R** 。这个点，即 **R** ，位于贝塞尔样条上的某个地方。如果您要计算从 *t=0* 到 *t=1* 的所有点，您可以绘制曲线:\n\n![Figure 8.5: Linearly interpolating the results of figure 8.4](img/Figure_8.5_B16191.jpg)\n\n图 8.5:对图 8.4 的结果进行线性插值\n\n让我们探索绘制贝塞尔样条所需的代码。贝塞尔样条不会在本书的任何其他地方使用，因此不需要在本书的其余部分中执行以下代码:\n\n1.  首先，您需要定义什么是贝塞尔样条。创建一个包含两个点和两个控制点的新模板类:\n\n    ```cpp\n    template<typename T>\n    class Bezier {\n    public:\n        T P1; // Point 1\n        T C1; // Control 1\n        T P2; // Point 2\n        T C2; // Control 2\n    };\n    ```\n\n2.  接下来，实现`Interpolate`功能。该函数使用贝塞尔样条参考和值`t`来插值样条。假设`t`大于等于`0`且小于等于【T4:\n\n    ```cpp\n    template<typename T>\n    inline T Interpolate(Bezier<T>& curve, float t) {\n        T A = lerp(curve.P1, curve.C1, t);\n        T B = lerp(curve.C2, curve.P2, t);\n        T C = lerp(curve.C1, curve.C2, t);\n        T D = lerp(A, C, t);\n        T E = lerp(C, B, t);\n        T R = lerp(D, E, t);\n        return R;\n    }\n    ```\n\n以下代码示例演示了如何使用贝塞尔类和`Interpolate`函数绘制贝塞尔样条:\n\n1.  首先，您需要创建将要绘制的数据:\n\n    ```cpp\n    Bezier<vec3> curve;\n    curve.P1 = vec3(-5, 0, 0);\n    curve.P2 = vec3(5, 0, 0);\n    curve.C1 = vec3(-2, 1, 0);\n    curve.C2 = vec3(2, 1, 0);\n\n    vec3 red = vec3(1, 0, 0);\n    vec3 green = vec3(0, 1, 0);\n    vec3 blue = vec3(0, 0, 1);\n    vec3 magenta = vec3(1, 0, 1);\n    ```\n\n2.  接下来，绘制点和手柄:\n\n    ```cpp\n    // Draw all relevant points\n    DrawPoint(curve.P1, red);\n    DrawPoint(curve.C1, green);\n    DrawPoint(curve.P2, red);\n    DrawPoint(curve.C2, green);\n    // Draw handles\n    DrawLine(curve.P1, curve.C1, blue);\n    DrawLine(curve.P2, curve.C2, blue);\n    ```\n\n3.  最后，绘制样条线:\n\n    ```cpp\n    // Draw the actual curve\n    // Resolution is 200 steps since last point is i + 1\n    for (int i = 0; i < 199; ++ i) {\n        float t0 = (float)i / 199.0f;\n        float t1 = (float)(i + 1) / 199.0f;\n        vec3 thisPoint = Interpolate(curve, t0);\n        vec3 nextPoint = Interpolate(curve, t1);\n        DrawLine(thisPoint, nextPoint, magenta);\n    }\n    ```\n\n在前面的示例代码中，您可以看到您可以通过使用六个线性插值来实现贝塞尔`Interpolate`函数。为了理解贝塞尔样条是如何工作的，您需要将`lerp`函数扩展到实际情况。线性插值，`lerp(a, b, t)`，扩展到`(1-t) * a + t * b`:\n\n1.  重写`Interpolate`函数，以便扩展所有`lerp`调用:\n\n    ```cpp\n    template<typename T>\n    inline T Interpolate(const Bezier<T>& curve, float t) {\n        T A = curve.P1 * (1.0f - t) + curve.C1 * t;\n        T B = curve.C2 * (1.0f - t) + curve.P2 * t;\n        T C = curve.C1 * (1.0f - t) + curve.C2 * t;\n        T D = A * (1.0f - t) + C * t;\n        T E = C * (1.0f - t) + B * t;\n        T R = D * (1.0f - t) + E * t;\n        return R;\n    }\n    ```\n\n2.  什么都没变，只是不再需要调用`lerp`函数。这适用于任何数据类型`T`，只要定义了`T operator*(const T& t, float f)`。让我们试着在数学意义上简化它。不使用`A`、`B`、`C`、`D`、`E`和`R`变量，而是将这些方程扩展为如下:\n\n    ```cpp\n    ((P1 * (1 - t) + C1 * t) * (1 - t) + (C1 * (1 - t) \n    + C2 * t) * t) * (1 - t) + ((C1 * (1 - t) + C2 * t) \n    * (1 - t) + (C2 * (1 - t) + P2 * t) * t) * t\n    ```\n\n3.  这相当于手工内联所有的`lerp`函数。生成的代码有点难读:\n\n    ```cpp\n    template<typename T>\n    inline T Interpolate(const Bezier<T>& c, float t) {\n       return \n         ((c.P1 * (1.0f - t) + c.C1 * t) * (1.0f - t) + \n         (c.C1 * (1.0f - t) + c.C2 * t) * t) * (1.0f - t) \n         + ((c.C1 * (1.0f - t) + c.C2 * t) * (1.0f - t) + \n         (c.C2 * (1.0f - t) + c.P2 * t) * t) * t;\n    }\n    ```\n\n4.  为什么要经历这些麻烦？为了简化数学，让我们从组合相似的术语开始:\n\n    ```cpp\n    -P1t3 + 3P1t2 - 3P1t + P1 + 3C1t3 - 6C1t2 + 3C1t - 3C2t3 + 3C2t2 + P2t3\n    ```\n\n5.  现在，这开始看起来像一个等式！这个简化的方程也可以用代码表示:\n\n    ```cpp\n    template<typename T>\n    inline T Interpolate(const Bezier<T>& curve, float t) {\n        return\n            curve.P1 * (t * t * t) * -1.0f +\n            curve.P1 * 3.0f * (t * t) -\n            curve.P1 * 3.0f * t +\n            curve.P1 +\n            curve.C1 * 3.0f * (t * t * t) -\n            curve.C1 * 6.0f * (t * t) +\n            curve.C1 * 3.0f * t -\n            curve.C2 * 3.0f * (t * t * t) +\n            curve.C2 * 3.0f * (t * t) +\n            curve.P2 * (t * t * t);\n    }\n    ```\n\n6.  通过分离一些术语来进一步简化:\n\n    ```cpp\n    P1( -t3 + 3t2 - 3t + 1) +\n    C1( 3t3 - 6t2 + 3t)+\n    C2(-3t3 + 3t2)+\n    P2(  t3)\n    ```\n\n7.  在代码中，这是，表示如下:\n\n    ```cpp\n    template<typename T>\n    inline T Interpolate(const Bezier<T>& c, float t) {\n        float ttt = t * t * t;\n        float tt = t * t;\n        return \n        c.P1 * (-1.0f * ttt + 3.0f * tt - 3.0f * t + 1.0f) +\n        c.C1 * (3.0f * ttt - 6.0f * tt + 3.0f * t) +\n        c.C2 * (-3.0f * ttt + 3.0f * tt) +\n        c.P2 * ttt;\n    }\n    ```\n\n8.  再次简化功能:\n\n    ```cpp\n    P1((1-t)3) +\n    C1(3(1-t)2t) +\n    C2(3(1-t)t2) +\n    P2(t3)\n    ```\n\n9.  最终简化的代码如下:\n\n    ```cpp\n    template<typename T>\n    inline T Interpolate(const Bezier<T>& curve, float t) {\n        return curve.P1 * ((1 - t) * (1 - t) * (1 - t)) +\n                curve.C1 * (3.0f * ((1 - t) * (1 - t)) * t) +\n                curve.C2 * (3.0f * (1 - t) * (t * t)) +\n                curve.P2 *(t * t * t);\n    }\n    ```\n\n如果你用范围从`0`到`1`的 *t* 绘制出这些最终的方程，你会得到下面的图表:\n\n![Figure 8.6: The basis functions of a Bézier spline](img/Figure_8.6_B16191.jpg)\n\n图 8.6:贝塞尔样条的基函数\n\n这些是三次贝塞尔样条的点基函数。它们表示样条的值如何随时间变化。例如，P1 的影响力随着时间的推移而下降；在 *t=0* 时，影响是完全的——它的值为 1。然而，当 *t=1* 时，P1 的影响力已经消失——它的值为 0。\n\n在本节中，您已经完成了简化贝塞尔样条评估函数的练习，以得到样条的基函数。使用贝塞尔样条，很容易遵循这个逻辑，因为您可以从一个简单易懂的实现开始，它只使用六个 lerp 函数。对于其他曲线，没有容易的起点。\n\n在下一节中，我们将探索另一种类型的三次样条——三次埃尔米特样条。使用您在本节中学习的知识，您将能够仅使用基函数图来实现埃尔米特求值函数。\n\n# 理解三次埃尔米特样条\n\n游戏动画中最常见的样条曲线类型是三次埃尔米特样条曲线 T2。与贝塞尔不同，埃尔米特样条不使用空间中的点进行控制；相反，它使用沿着样条线的点的切线。与贝塞尔样条一样，您仍然有四个值，但是它们的解释不同。\n\n使用埃尔米特样条，您没有两个点和两个控制点；相反，你有两个点和两个斜率。斜率也称为切线，在本章的其余部分，斜率和切线术语将互换使用。埃尔米特样条的点基函数如下所示:\n\n![Figure 8.7: The point basis functions of Hermite splines](img/Figure_8.7_B16191.jpg)\n\n图 8.7:埃尔米特样条的点基函数\n\n当给定点基函数时，可以实现类似于贝塞尔插值函数的样条评估函数:\n\n```cpp\ntemplate<typename T>\nT Hermite(float t, T& p1, T& s1, T& p2, T& s2) {\n   return \n      p1 * ((1.0f + 2.0f * t) * ((1.0f - t) * (1.0f - t))) +\n      s1 * (t * ((1.0f - t) * (1.0f - t))) +\n      p2 * ((t * t) * (3.0f - 2.0f * t)) +\n      s2 * ((t * t) * (t - 1.0f));\n}\n```\n\n可以在贝塞尔样条和埃尔米特样条之间切换，但这超出了动画需要了解的范围。一些 3D 内容创建应用(如 Maya)允许动画师使用埃尔米特样条线创建动画，而其他应用(如 Blender 3D)则使用贝塞尔曲线。\n\n了解这些函数是如何工作的很有用，不管是哪一个驱动你的动画系统。当然，曲线类型更多，但贝塞尔曲线和埃尔米特曲线是最常见的。\n\nglTF 文件格式支持常数、线性和三次插值类型。您刚刚学习了如何进行三次插值，但是仍然需要实现常数插值和线性插值。\n\n# 插值类型\n\n定义动画曲线时，通常遵循三种插值方法之一——常数、线性或三次插值。三次曲线可以使用任何三次方程来表示，例如贝塞尔曲线(这是 Blender 使用的)或埃尔米特样条曲线(这是 Maya 使用的)。这本书使用埃尔米特样条来表示三次曲线。\n\n一条**恒定曲线**保持的值不变，直到下一个关键帧。有时，这种类型的曲线被称为阶跃曲线。从视觉上看，恒定曲线如下所示:\n\n![Figure 8.8: A constant curve](img/Figure_8.8_B16191.jpg)\n\n图 8.8:恒定曲线\n\n一条**线性曲线**以线性方式(即直线)在两帧之间插入。正如您在前面的采样曲线近似示例中看到的，如果线性轨迹的样本足够接近，它也可以开始近似其他类型的曲线。线性曲线如下所示:\n\n![Figure 8.9: A linear curve](img/Figure_8.9_B16191.jpg)\n\n图 8.9:线性曲线\n\n一条**三次曲线**可以让你根据数值和切线定义一条曲线。三次曲线的好处是可以用很少的数据表达复杂的曲线。缺点是插值变得有点昂贵。三次曲线如下所示(切线是从关键帧出来的线):\n\n![Figure 8.10: A cubic curve](img/Figure_8.10_B16191.jpg)\n\n图 8.10:三次曲线\n\n插值类型可以表示为一个简单的`enum`类。创建新文件— `Interpolation.h`。添加表头保护并添加以下`enum`类声明:\n\n```cpp\nenum class Interpolation { \n    Constant, \n    Linear, \n    Cubic \n};\n```\n\n这也是 glTF 支持的三种插值类型。在下一节中，您将通过创建一个保存关键帧数据的`Frame`结构来开始实现动画轨迹。\n\n# 创建框架结构\n\n什么是数据框架？取决于插值类型。如果插值是常数(步长)或线性的，则帧只是一个时间和值。当插值为三次插值时，还需要存储切线。\n\n埃尔米特曲线是由埃尔米特样条连接而成的。每个控制点由时间、值、引入切线和引出切线组成。如果用控制点之前的点来评估控制点，则使用引入切线。如果用控制点后面的点来计算控制点，则使用引出切线。\n\n存储在帧中的时间值是标量，但是数据和切线呢？这些值应该是标量、向量还是四元数？为了做出这个决定，你必须考虑如何将一组帧组织成一条曲线。\n\n有两种策略可供选择。您可以创建一个标量曲线对象，其中数据和切线是标量值。然后，当需要向量曲线时，可以将几个标量曲线对象组合成一个向量曲线对象。\n\n拥有标量轨迹并从中合成高阶轨迹的优势在于，向量或四元数曲线的每个分量都可以进行不同的插值。它还可以节省内存，因为曲线的每个部分可能有不同的帧数。缺点是额外的实现工作。\n\n另一种策略是拥有专门的帧和曲线类型，例如标量帧、向量帧和四元数帧。同样，您可以创建单独的类来表示标量曲线、向量曲线和四元数曲线。\n\n使用专用框架和曲线的优点是易于实现。您可以利用模板来避免编写重复的代码。glTF 文件也以这种方式存储动画轨迹。缺点是记忆；曲线的每个部分都需要有相同数量的关键帧。\n\n在本书中，您将实现显式的帧和曲线(轨迹)。`Frame`类将包含时间、值以及入切线和出切线。如果插值类型不需要切线，可以直接忽略。帧可以是任意大小(如标量、向量 2、向量 3、四等)。它包含的时间总是标量，但值和切线长度可以是任何值:\n\n1.  创建新文件，`Frame.h`。将`Frame`类的声明添加到这个新文件中。`Frame`类需要值和进出切线的数组，以及时间的标量。使用模板指定每个框架的大小:\n\n    ```cpp\n    template<unsigned int N>\n    class Frame {\n    public:\n        float mValue[N];\n        float mIn[N];\n        float mOut[N];\n        float mTime;\n    };\n    ```\n\n2.  为常用帧类型创建`typedef`数据类型:\n\n    ```cpp\n    typedef Frame<1> ScalarFrame;\n    typedef Frame<3> VectorFrame;\n    typedef Frame<4> QuaternionFrame;\n    ```\n\n您刚刚实现的`Frame`类用于在动画轨迹中存储关键帧。动画轨迹是关键帧的集合。在下一节中，您将学习如何实现`Track`类。\n\n# 创建赛道类\n\n一个`Track`类是一个帧的集合。对轨道进行插值会返回轨道的数据类型；结果是轨迹在特定时间点定义的任何曲线上的值。一个轨道必须至少有两帧可以插入。\n\n正如在*创建框架结构*一节中提到的，通过遵循本书中的示例，您将实现显式的框架和轨迹类型。标量、向量和四元数轨迹将有单独的类别。这些类是模板化的，以避免编写重复的代码。例如，`vec3`轨迹包含`Frame<3>`类型的帧。\n\n因为轨迹具有显式类型，所以如果不同时向 *Y* 和 *Z* 组件添加关键帧，就无法在`vec3`轨迹的 *X* 组件中创建关键帧。\n\n如果你有一个不变的组件，这会消耗更多的内存。例如，请注意下图中 *Z* 组件有许多帧，即使它是一条直线，两帧应该就足够了。这不是一个很大的交易；占用的额外内存微不足道:\n\n![Figure 8.11: The components of a vec3 track](img/Figure_8.11_B16191.jpg)\n\n图 8.11:vec3 轨道的组件\n\n对于蒙皮网格渲染，动画轨迹始终为关节变换设置动画。但是，动画轨迹也可以用来制作游戏中其他值的动画，例如灯光的强度或在二维精灵之间切换以获得动画书效果。在下一节中，您将创建一个新的头文件，并开始声明实际的`Track`类。\n\n## 申报赛道等级\n\n轨道是帧的集合。`Frame`类是模板化的，所以`Track`类也需要模板化。`Track`类采用两个模板参数——第一个是类型(预计为`float`、`vec3`、`quat`等)，另一个是类型包含的组件数量:\n\n1.  `Track`类只需要两个成员——一个帧向量和一个插值类型。创建一个新文件`Track.h`，并将`Track`类的声明添加到该文件中:\n\n    ```cpp\n    template<typename T, int N>\n    class Track {\n    protected:\n        std::vector<Frame<N>> mFrames;\n        Interpolation mInterpolation;\n    ```\n\n2.  `Track`类只需要一个默认的构造函数来初始化`mInterpolation`变量。生成的复制构造函数、赋值操作符和析构函数都可以:\n\n    ```cpp\n    public:\n        Track();\n    ```\n\n3.  为轨道的帧数、插值类型以及开始和结束时间创建 getter 和 setter 函数:\n\n    ```cpp\n        void Resize(unsigned int size);\n        unsigned int Size();\n        Interpolation GetInterpolation();\n        void SetInterpolation(Interpolation interp);\n        float GetStartTime();\n        float GetEndTime();\n    ```\n\n4.  `Track`类需要一种在给定时间时对轨迹进行采样的方法。这个`Sample`方法应该取一个时间值，以及轨道是否循环。重载`[] operator`以检索对框架的引用:\n\n    ```cpp\n        T Sample(float time, bool looping);\n        Frame<N>& operator[](unsigned int index);\n    ```\n\n5.  接下来，您需要声明一些助手函数。轨迹可以是常数、线性或立方。只有一个`Sample`函数需要处理这三种情况。与其创建一个庞大的、难以理解的函数，不如为每种插值类型创建一个辅助函数:\n\n    ```cpp\n    protected:\n        T SampleConstant(float time, bool looping);\n        T SampleLinear(float time, bool looping);\n        T SampleCubic(float time, bool looping);\n    ```\n\n6.  添加一个辅助函数来计算埃尔米特样条:\n\n    ```cpp\n        T Hermite(float time, const T& p1, const T& s1, \n                  const T& p2, const T& s2);\n    ```\n\n7.  添加一个函数来检索给定时间的帧索引。这是请求时间之前的最后一帧。此外，添加一个助手函数，该函数获取轨道范围之外的输入时间，并将其调整为轨道上的有效时间:\n\n    ```cpp\n        int FrameIndex(float time, bool looping);\n        float AdjustTimeToFitTrack(float t, bool loop);\n    ```\n\n8.  您将需要一种方法来将一个浮动数组(帧内的数据)转换为轨道的模板类型。该功能专用于每种类型的赛道:\n\n    ```cpp\n        T Cast(float* value); // Will be specialized\n    };\n    ```\n\n9.  与`Frame`类一样，为常见的`Track`类型添加`typedef`数据类型:\n\n    ```cpp\n    typedef Track<float, 1> ScalarTrack;\n    typedef Track<vec3, 3> VectorTrack;\n    typedef Track<quat, 4> QuaternionTrack;\n    ```\n\n`Track`类的 API 很小，使得该类易于使用。但是`Track`类有很多隐藏的复杂性；毕竟，这个类是你正在构建的动画系统的核心。在下一节中，您将开始实施实际的`Track`课程。\n\n## 实施赛道课程\n\n`Track`类是模板化的，但它不打算在动画系统之外使用。将`float`、`vec3`和`quat`轨道的模板定义添加到`Track.cpp`。这使得编译器在 CPP 文件中为这些模板生成代码:\n\n```cpp\ntemplate Track<float, 1>;\ntemplate Track<vec3, 3>;\ntemplate Track<quat, 4>;\n```\n\n对于角色动画来说，`vec3`和`quat`轨迹类型就是你所需要的。如果需要添加新的轨道类型，不要忘记将模板类型添加到`Track.cpp`文件中。在下一节中，您将开始实现助手函数来加载跟踪数据。\n\n### 实现助手函数\n\n`Track`类是模板化的，以避免为所有轨道类型编写重复的代码。但是，有些功能需要特定于`Track`类的类型。除了`Cast`函数之外，所有类型特定的函数都驻留在一个新的命名空间— `TrackHelpers`中。\n\n这些辅助函数不是`Track`类的一部分；它们依赖函数重载来确保调用正确版本的帮助函数。这些助手类的关键职责之一是确保四元数被规范化，并且在正确的邻域内。因为这段代码插入了四元数，所以邻居关系是一个问题:\n\n1.  对于要线性插值的轨迹，需要创建适用于每种轨迹类型的插值函数。向`Track.cpp`添加以下辅助函数，为轨迹可能包含的每种数据类型提供正确的插值方法。这些函数属于`TrackHelpers`命名空间:\n\n    ```cpp\n    namespace TrackHelpers {\n       inline float Interpolate(float a, float b, float t) {\n           return a + (b - a) * t;\n       }\n       inline vec3 Interpolate(const vec3& a, const vec3& b,\n                               float t) {\n           return lerp(a, b, t);\n       }\n       inline quat Interpolate(const quat& a, const quat& b,\n                               float t) {\n           quat result = mix(a, b, t);\n           if (dot(a, b) < 0) { // Neighborhood\n               result = mix(a, -b, t);\n           }\n           return normalized(result); //NLerp, not slerp\n       }\n    ```\n\n2.  插值埃尔米特样条时，如果输入类型是四元数，则需要对结果进行归一化。您可以创建仅规范化四元数的辅助函数，而不是提供埃尔米特函数的四元数规范:\n\n    ```cpp\n       inline float AdjustHermiteResult(float f) {\n          return f;\n       }\n       inline vec3 AdjustHermiteResult(const vec3& v) {\n          return v;\n       }\n       inline quat AdjustHermiteResult(const quat& q) {\n          return normalized(q);\n       }\n    ```\n\n3.  还需要有一个通用的`Neighborhood`操作来确保两个四元数在正确的邻域内。该函数对其他数据类型不做任何操作:\n\n    ```cpp\n       inline void Neighborhood(const float& a, float& b){}\n       inline void Neighborhood(const vec3& a, vec3& b){}\n       inline void Neighborhood(const quat& a, quat& b) {\n          if (dot(a, b) < 0) {\n             b = -b;\n          }\n       }\n    }; // End Track Helpers namespace\n    ```\n\n这些辅助函数存在的原因是为了避免必须制作插值函数的特殊版本。相反，通用插值函数调用这些辅助方法，函数重载确保调用正确的函数。这确实意味着，如果您添加新类型的轨道，您需要添加新的助手函数。在下一节中，您将开始实现一些`Track`功能。\n\n### 实现跟踪功能\n\n在本节中，您将开始实现`Track`类的成员功能。`Track`类有几个不重要的函数，要么需要调用辅助函数，要么只是 getter 和 setter 函数。首先用这些函数开始实现`Track`类:\n\n1.  `Track`构造器需要设置轨迹的插值类型。轨道开始和结束时间的 getter 和 setter 函数是简单的 getter 函数:\n\n    ```cpp\n    template<typename T, int N>\n    Track<T, N>::Track() {\n        mInterpolation = Interpolation::Linear;\n    }\n    template<typename T, int N>\n    float Track<T, N>::GetStartTime() {\n        return mFrames[0].mTime;\n    }\n    template<typename T, int N>\n    float Track<T, N>::GetEndTime() {\n        return mFrames[mFrames.size() - 1].mTime;\n    }\n    ```\n\n2.  `Sample`功能需要调用`SampleConstant`、`SampleLinear`或`SampleCubic`，具体取决于赛道类型。`[]` `operator`返回对指定帧的引用:\n\n    ```cpp\n    template<typename T, int N>\n    T Track<T, N>::Sample(float time, bool looping) {\n        if (mInterpolation == Interpolation::Constant) {\n            return SampleConstant(time, looping);\n        }\n        else if (mInterpolation == Interpolation::Linear) {\n            return SampleLinear(time, looping);\n        }\n        return SampleCubic(time, looping);\n    }\n    template<typename T, int N>\n    Frame<N>& Track<T, N>::operator[](unsigned int index) {\n        return mFrames[index];\n    }\n    ```\n\n3.  `Resize`和`Size`函数是围绕帧向量大小的简单获取器和设置器:\n\n    ```cpp\n    template<typename T, int N>\n    void Track<T, N>::Resize(unsigned int size) {\n        mFrames.resize(size);\n    }\n    template<typename T, int N>\n    unsigned int Track<T, N>::Size() {\n        return mFrames.size();\n    }\n    ```\n\n4.  轨道的插值类型也有简单的获取和设置功能:\n\n    ```cpp\n    template<typename T, int N>\n    Interpolation Track<T, N>::GetInterpolation() {\n        return mInterpolation;\n    }\n    template<typename T, int N>\n    void Track<T, N>::SetInterpolation(Interpolation interpolation) {\n        mInterpolation = interpolation;\n    }\n    ```\n\n5.  `Hermite`函数实现了本章*理解三次埃尔米特样条*一节中介绍的基本函数。第二点可能需要被`Neighborhood`辅助函数否定。四元数也需要规范化。相邻和正常化都是由助手函数执行的:\n\n    ```cpp\n    template<typename T, int N>\n    T Track<T, N>::Hermite(float t, const T& p1, const T& s1,\n                           const T& _p2, const T& s2) {\n        float tt = t * t;\n        float ttt = tt * t;\n        T p2 = _p2;\n        TrackHelpers::Neighborhood(p1, p2);\n        float h1 = 2.0f * ttt - 3.0f * tt + 1.0f;\n        float h2 = -2.0f * ttt + 3.0f * tt;\n        float h3 = ttt - 2.0f * tt + t;\n        float h4 = ttt - tt;\n        T result = p1 * h1 + p2 * h2 + s1 * h3 + s2 * h4;\n        return TrackHelpers::AdjustHermiteResult(result);\n    }\n    ```\n\n在接下来的部分中，你将实现`Track`类中一些更难的功能，从`FrameIndex`功能开始。\n\n### 实现框架索引功能\n\n`FrameIndex`函数以时间为自变量；它应该在那一次之前立即返回框架(在左边)。该行为根据轨道是否打算循环采样而变化。按照以下步骤实现`FrameIndex`功能:\n\n1.  如果轨道只有一帧或更少，则无效。如果遇到无效轨道，返回`-1`:T1\n2.  如果轨道被采样为循环，则需要调整输入时间，使其介于开始帧和结束帧之间。这意味着您需要知道轨道第一帧的时间、轨道第一帧的时间以及轨道的持续时间:\n\n    ```cpp\n        if (looping) {\n            float startTime = mFrames[0].mTime;\n            float endTime = mFrames[size - 1].mTime;\n            float duration = endTime - startTime;\n    ```\n\n3.  由于轨道正在循环，需要调整`time`，使其在有效范围内。为此，通过减去开始时间并以持续时间对结果求模，使`time`相对于持续时间。如果`time`为负，则添加持续时间。别忘了把开始时间加回`time` :\n\n    ```cpp\n            time = fmodf(time - startTime, \n                         endTime - startTime);\n            if (time < 0.0f) {\n                time += endTime - startTime;\n            }\n            time = time + startTime;\n        }\n    ```\n\n4.  如果轨道没有循环，任何小于起始帧的`time`值都应箝位到`0`，任何大于倒数第二帧的`time`值都应箝位到倒数第二帧的索引:\n\n    ```cpp\n        else {\n            if (time <= mFrames[0].mTime) {\n                return 0;\n            }\n            if (time >= mFrames[size - 2].mTime) {\n                return (int)size - 2;\n            }\n        }\n    ```\n\n5.  现在时间在有效范围内，循环每一帧。最接近时间(但更少)的帧是应该返回其索引的帧。该帧可以通过在轨迹的帧中向后循环并返回第一个索引来找到，该索引的时间小于查找的时间:\n\n    ```cpp\n        for (int i = (int)size - 1; i >= 0; --i) {\n            if (time >= mFrames[i].mTime) {\n                return i;\n            }\n        }\n        // Invalid code, we should not reach here!\n        return -1;\n    } // End of FrameIndex\n    ```\n\n如果轨道没有循环，并且时间大于最后一帧的时间，则使用倒数第二帧的索引。为什么是倒数第二帧而不是最后一帧？`Sample`功能总是需要一个当前帧和下一帧，下一帧是通过将`1`添加到`FrameIndex`功能的结果中找到的。当`time`等于最后一帧的时间时，需要插值的两帧仍然是倒数第二帧和最后一帧。\n\n在下一节中，您将实现`AdjustTimeToFitTrack`功能。此函数用于确保任何采样时间都有有效值。有效值是轨道开始时间和结束时间之间的任何时间。\n\n### 实现 AdjustTimeToFitTrack 功能\n\n下一个要实现的功能是`AdjustTimeToFitTrack`。当给定时间后，该功能需要将时间调整到轨道的开始/结束帧范围内。当然，这取决于轨道是否循环。执行以下步骤实现`AdjustTimeToFitTrack`功能:\n\n1.  如果轨道少于一帧，则该轨道无效。如果使用了无效轨道，返回`0` :\n\n    ```cpp\n    template<typename T, int N>\n    float Track<T, N>::AdjustTimeToFitTrack(float time, \n                                            bool looping) {\n        unsigned int size = (unsigned int)mFrames.size();\n        if (size <= 1) { \n            return 0.0f; \n        }\n    ```\n\n2.  查找轨道的开始时间、结束时间和持续时间。开始时间是第一帧的时间，结束时间是最后一帧的时间，持续时间是两者之差。如果轨道有`0`持续时间，则无效—返回`0` :\n\n    ```cpp\n        float startTime = mFrames[0].mTime;\n        float endTime = mFrames[size - 1].mTime;\n        float duration = endTime - startTime;\n        if (duration <= 0.0f) { \n            return 0.0f; \n        }\n    ```\n\n3.  如果轨道循环，根据轨道的持续时间调整时间:\n\n    ```cpp\n        if (looping) {\n            time = fmodf(time - startTime, \n                         endTime - startTime);\n            if (time < 0.0f) {\n                time += endTime - startTime;\n            }\n            time = time + startTime;\n        }\n    ```\n\n4.  如果轨道没有循环，将时间限制在第一帧或最后一帧。返回调整后的时间:\n\n    ```cpp\n        else {\n            if (time <= mFrames[0].mTime) { \n                time = startTime;  \n            }\n            if (time >= mFrames[size - 1].mTime) { \n                time = endTime; \n            }\n        }\n        return time;\n    }\n    ```\n\n`AdjustTimeToFitTrack`功能很有用，因为它将动画采样时间保持在范围内。该函数旨在当动画的播放时间改变时调用。考虑以下示例:\n\n```cpp\nTrack<float, 1> t;\nfloat mAnimTime = 0.0f;\nvoid Update(float dt) { // dt: delta time of frame\n    mAnimTime = t. AdjustTimeToFitTrack (mAnimTime + dt);\n}\n```\n\n在本例中，每当调用`Update`函数时，`mAnimTime`变量都会由帧的`deltaTime`增加。但是，因为递增的时间在分配之前被传递到`AdjustTimeToFitTrack`，所以它永远不会有无效的动画时间值。\n\n在下一节中，您将实现`Track`类的`Cast`功能。`Cast`函数用于获取一个浮点数组，并将其转换为`Track`类的模板类型。\n\n### 实现强制转换功能\n\n`Cast`功能是专用的；需要为每种类型的轨道提供一种实现方式。`Cast`函数接受一个浮点数组，并返回模板化类型`T`，属于`Track`类。支持的类型有`float`、`vec3`、`quat`:\n\n```cpp\ntemplate<> float Track<float, 1>::Cast(float* value) {\n    return value[0];\n}\ntemplate<> vec3 Track<vec3, 3>::Cast(float* value) {\n    return vec3(value[0], value[1], value[2]);\n}\ntemplate<> quat Track<quat, 4>::Cast(float* value) {\n    quat r = quat(value[0], value[1], value[2], value[3]);\n    return normalized(r);\n}\n```\n\n这个`Cast`函数很重要，因为它可以将存储在`Frame`类中的`float`数组转换成`Frame`类所代表的数据类型。例如`Frame<3>`被铸造成`vec3`。在下面的部分中，当采样一个`Track`类时，您将使用`Cast`函数返回正确的数据类型。\n\n### 恒定轨道采样\n\n在本节中，您将为一个`Track`类实现三个采样函数中的第一个—恒定采样函数。要进行恒定(步长)采样，使用`FrameIndex`辅助工具根据时间找到帧。确保该帧有效，然后将该帧的值转换为正确的数据类型并返回:\n\n```cpp\ntemplate<typename T, int N>\nT Track<T, N>::SampleConstant(float t, bool loop) {\n    int frame = FrameIndex(t, loop);\n    if (frame < 0 || frame >= (int)mFrames.size()) {\n        return T();\n    }\n    return Cast(&mFrames[frame].mValue[0]);\n}\n```\n\n常量采样通常用于可见性标志之类的东西，变量的值从一帧到下一帧的变化是有意义的，而不需要任何真正的插值。在下一节中，您将学习如何实现线性轨迹采样。线性采样很常见；大多数内容创建应用都提供“采样”导出选项，用于导出线性插值轨道。\n\n### 线性轨道采样\n\n第二种采样类型、**线性采样**，在两帧之间进行插值。这个函数需要找到当前帧和下一帧，然后找到两帧之间的增量时间。因为`FrameIndex`功能，你永远不应该处于当前帧是轨道的最后一帧，下一帧无效的情况。\n\n一旦知道了当前帧、下一帧以及它们之间的增量时间，就可以进行插值。调用`AdjustTimeToFitTrack`确定时间有效，从中减去第一帧的时间，将结果除以帧增量。这将产生插值值`t`。\n\n知道插值值后，调用`TrackHelpers::Interpolate`函数进行插值:\n\n```cpp\ntemplate<typename T, int N>\nT Track<T, N>::SampleLinear(float time, bool looping) {\n    int thisFrame = FrameIndex(time, looping);\n    if (thisFrame < 0 || thisFrame >= mFrames.size() - 1) {\n        return T();\n    }\n    int nextFrame = thisFrame + 1;\n    float trackTime = AdjustTimeToFitTrack(time, looping);\n    float thisTime = mFrames[thisFrame].mTime;\n    float frameDelta = mFrames[nextFrame].mTime – thisTime;\n    if (frameDelta <= 0.0f) {\n        return T();\n    }\n    float t = (trackTime - thisTime) / frameDelta;\n    T start = Cast(&mFrames[thisFrame].mValue[0]);\n    T end = Cast(&mFrames[nextFrame].mValue[0]);\n    return TrackHelpers::Interpolate(start, end, t);\n}\n```\n\n线性采样很常见，因为许多 3D 内容创建应用提供了一个选项，通过以设定的间隔对动画曲线进行采样来近似动画曲线。在下一节中，您将学习如何进行曲线的三次插值。三次插值存储的数据比线性插值少，但是计算起来更贵。\n\n### 立方径迹取样\n\n最后一种采样类型**立方采样**，以与线性采样相同的方式找到要采样的帧和插值时间。该函数调用`Hermite`辅助函数进行插值。\n\n如果你把`time`想象成赛道上的一个游戏头，它在第一个点的右边，第二个点的左边。因此，您需要第一点的外斜率(因为游戏头正在远离它)和第二点的内斜率(因为游戏头正在向它移动)。两个斜率都需要按照帧增量进行缩放:\n\n```cpp\ntemplate<typename T, int N>\nT Track<T, N>::SampleCubic(float time, bool looping) {\n    int thisFrame = FrameIndex(time, looping);\n    if (thisFrame < 0 || thisFrame >= mFrames.size() - 1) {\n        return T();\n    }\n    int nextFrame = thisFrame + 1;\n    float trackTime = AdjustTimeToFitTrack(time, looping);\n    float thisTime = mFrames[thisFrame].mTime;\n    float frameDelta = mFrames[nextFrame].mTime - thisTime;\n    if (frameDelta <= 0.0f) {\n        return T();\n    }\n    float t = (trackTime - thisTime) / frameDelta;\n    size_t fltSize = sizeof(float);\n    T point1 = Cast(&mFrames[thisFrame].mValue[0]);\n    T slope1;// = mFrames[thisFrame].mOut * frameDelta;\n    memcpy(&slope1, mFrames[thisFrame].mOut, N * fltSize);\n    slope1 = slope1 * frameDelta;\n    T point2 = Cast(&mFrames[nextFrame].mValue[0]);\n    T slope2;// = mFrames[nextFrame].mIn[0] * frameDelta;\n    memcpy(&slope2, mFrames[nextFrame].mIn, N * fltSize);\n    slope2 = slope2 * frameDelta;\n    return Hermite(t, point1, slope1, point2, slope2);\n}\n```\n\n为什么斜坡使用`memcpy`而不是`Cast`功能？这是因为`Cast`函数归一化四元数，这是不好，因为斜率不是四元数。使用`memcpy`代替`Cast`直接复制值，避免了归一化。\n\n在下一节中，您将学习如何将向量和四元数轨迹组合成`TransformTrack`。实际的动画框架将在`TransformTrack`类上工作，它不会被模板化。\n\n# 创建转换轨迹类\n\n对于任何动画的变换，您都不希望保持单独的向量和四元数轨迹；相反，您构建了一个更高级别的结构——转换轨道。变换轨迹封装了三个轨迹，一个用于位置，一个用于旋转，一个用于缩放。您可以在任何点对变换轨迹进行采样，并获得完整的变换，即使组件轨迹具有不同的持续时间或在不同的时间开始。\n\n需要考虑的一件事是，您希望如何存储这些与动画模型相关的变换轨迹。模型的骨架包含几块骨头。您可以存储变换轨迹的向量(每个骨骼一个向量)，也可以将骨骼标识添加为变换轨迹的成员，并且只存储所需的数量。\n\n这很重要，因为一个角色可以有很多骨骼，但不是所有的动画都会动画化所有这些骨骼。如果为每个骨骼存储一个变换轨迹，会浪费内存，但对动画进行采样会更快。如果您只存储所需数量的变换轨迹，采样会变得有点昂贵，但内存消耗会下降。\n\n实现选择总是以内存和速度为代价。在现代系统中，任一轴上的增量都应该是微不足道的。在本节中，您将向变换轨迹添加骨骼标识，并且只存储所需数量的轨迹。\n\n## 声明 TransformTrack 类\n\n`TransformTrack`类需要保持一个整数，该整数表示轨迹将影响到哪个骨骼(关节)。它还需要位置、旋转和缩放的实际轨迹。这四条信息加在一起应该足以为关节的位置、旋转和缩放制作动画。\n\n与`Track`类一样，`TransformTrack`类具有转换轨迹开始和结束时间的 getter 和 setter 函数。变换轨迹的开始和结束时间取决于其组成轨迹。组件轨迹是位置、旋转和缩放轨迹。\n\n在三个轨道中，最低的开始时间被用作变换轨道的开始时间。三个轨道中最大的结束时间用作变换轨道的结束时间。\n\n并非变换轨迹中的所有组件轨迹都需要有效。例如，如果只有变换的位置是动画的，则旋转和缩放组件轨迹可以保留为无效。只要变换轨迹的至少一个组成轨迹有效，变换轨迹就是有效的。\n\n因为不是所有的分量轨迹都保证有效，所以`TransformTrack`类的`Sample`函数需要进行参考变换。采取以下步骤申报`TransformTrack`类:\n\n1.  创建一个新文件`TransformTrack.h`，并通过定义成员变量\n\n    ```cpp\n    class TransformTrack {\n    protected:\n        unsigned int mId;\n        VectorTrack mPosition;\n        QuaternionTrack mRotation;\n        VectorTrack mScale;\n    ```\n\n    开始向其中添加`TransformTrack`定义\n2.  公共应用编程接口很简单。您需要默认构造函数为轨迹的关节标识指定默认值。您还需要 ID、组件跟踪、开始/结束时间以及持续时间和有效性的 getter 函数。只有 ID 需要一个 setter 函数；组件 getter 函数返回可变的引用:\n\n    ```cpp\n    public:\n        TransformTrack();\n        unsigned int GetId();\n        void SetId(unsigned int id);\n        VectorTrack& GetPositionTrack();\n        QuaternionTrack& GetRotationTrack();\n        VectorTrack& GetScaleTrack();\n        float GetStartTime();\n        float GetEndTime();\n        bool IsValid();\n        Transform Sample(const Transform& ref, float time, bool looping);\n    };\n    ```\n\n在下一节中，您将开始实现`TransfromTrack`的功能。\n\n## 实现 TransformTrack 类\n\n按照以下步骤实施`TransformTrack`类:\n\n1.  创建一个新文件`TransformTrack.cpp`，在中实现`TransformTrack`类。`TransformTrack`类的构造函数并不重要；为变换轨迹代表的关节指定默认值。轨道标识的获取器和设置器功能也很简单:\n\n    ```cpp\n    TransformTrack::TransformTrack() {\n        mId = 0;\n    }\n    unsigned int TransformTrack::GetId() {\n        return mId;\n    }\n    void TransformTrack::SetId(unsigned int id) {\n        mId = id;\n    }\n    ```\n\n2.  接下来，实现访问存储在变换轨道中的不同分量轨道的功能。这些函数需要返回一个引用，以便您可以变异返回的轨迹:\n\n    ```cpp\n    VectorTrack& TransformTrack::GetPositionTrack() {\n        return mPosition;\n    }\n    QuaternionTrack& TransformTrack::GetRotationTrack() {\n        return mRotation;\n    }\n    VectorTrack& TransformTrack::GetScaleTrack() {\n        return mScale;\n    }\n    ```\n\n3.  只有当存储在`TransformTrack`类中的至少一个组件轨道有效时，`IsValid`助手功能才应该返回`true`。要使一个轨道有效，它需要有两个或更多帧:\n\n    ```cpp\n    bool TransformTrack::IsValid() {\n        return mPosition.Size() > 1 || \n               mRotation.Size() > 1 || \n               mScale.Size() > 1;\n    }\n    ```\n\n4.  `GetStartTime`功能应返回三个分量轨道的最小开始时间。如果没有一个组件有效(即都有一个或没有框架)，则`TransformTrack`无效。在这种情况下，只需返回`0` :\n\n    ```cpp\n    float TransformTrack::GetStartTime() {\n        float result = 0.0f;\n        bool isSet = false;\n        if (mPosition.Size() > 1) {\n            result = mPosition.GetStartTime();\n            isSet = true;\n        }\n        if (mRotation.Size() > 1) {\n            float rotationStart = mRotation.GetStartTime();\n            if (rotationStart < result || !isSet) {\n                result = rotationStart;\n                isSet = true;\n            }\n        }\n        if (mScale.Size() > 1) {\n            float scaleStart = mScale.GetStartTime();\n            if (scaleStart < result || !isSet) {\n                result = scaleStart;\n                isSet = true;\n            }\n        }\n        return result;\n    }\n    ```\n\n5.  `GetEndTime`功能类似于`GetStartTime`功能。唯一不同的是，这个函数寻找最大的轨道结束时间:\n\n    ```cpp\n    float TransformTrack::GetEndTime() {\n        float result = 0.0f;\n        bool isSet = false;\n        if (mPosition.Size() > 1) {\n            result = mPosition.GetEndTime();\n            isSet = true;\n        }\n        if (mRotation.Size() > 1) {\n            float rotationEnd = mRotation.GetEndTime();\n            if (rotationEnd > result || !isSet) {\n                result = rotationEnd;\n                isSet = true;\n            }\n        }\n        if (mScale.Size() > 1) {\n            float scaleEnd = mScale.GetEndTime();\n            if (scaleEnd > result || !isSet) {\n                result = scaleEnd;\n                isSet = true;\n            }\n        }\n        return result;\n    }\n    ```\n\n6.  `Sample`功能仅在其组成轨道中有两帧或更多帧的情况下对该轨道进行采样。由于一个`TransformTrack`类只能动画一个组件，比如作为位置，这个函数需要以一个引用转换作为参数。如果其中一个变换组件没有被变换轨迹激活，则使用参考变换的值:\n\n    ```cpp\n    Transform TransformTrack::Sample(const Transform& ref,\n                                  float time, bool loop) {\n        Transform result = ref; // Assign default values\n        if (mPosition.Size() > 1) { // Only if valid\n           result.position = mPosition.Sample(time, loop);\n        }\n        if (mRotation.Size() > 1) { // Only if valid\n           result.rotation = mRotation.Sample(time, loop);\n        }\n        if (mScale.Size() > 1) { // Only if valid\n           result.scale = mScale.Sample(time, loop);\n        }\n        return result;\n    }\n    ```\n\n因为不是所有的动画都包含相同的轨迹，所以在动画切换时，重置正在采样的姿势非常重要。这确保了参考变换总是正确的。若要重置姿势，请将其指定为与其余姿势相同。\n\n# 总结\n\n在本章中，您学习了动画的构建块、一帧数据中的内容、几帧如何构成一个轨迹以及几个轨迹如何制作变换动画。您探索了用于插值动画轨迹的不同插值方法，使这些方法适用于标量、向量和四元数轨迹。\n\n本章中构建的类将在下一章中用作创建动画剪辑的构建模块。在下一章中，您将实现动画剪辑和姿势。动画剪辑将由`TransformTrack`对象组成。这些轨迹是现代动画系统的核心。\n\n本书可下载内容的`Chapter08`文件夹中有两个样本。`Sample00`包含书中至此使用的所有代码，`Sample01`创建了几个轨道，并在屏幕上绘制它们。直观地绘制轨迹是一个好主意，因为它有助于在早期防止调试问题。"
  },
  {
    "path": "docs/handson-cpp-game-ani-prog/09.md",
    "content": "# 九、实现动画剪辑\n\n动画剪辑是`TransformTrack`对象的集合。一个动画剪辑随着时间的推移动画化一组变换，动画化的一组变换称为一个姿势。把一个姿势想象成一个动画角色在特定时间点的骨架。姿势是变换的层次。每个转换的值都会影响其所有子转换。\n\n让我们来看看为游戏角色动画的一帧生成姿势需要什么。对动画剪辑进行采样时，结果是一个姿势。动画剪辑由动画轨迹组成，每个动画轨迹由一个或多个帧组成。这种关系看起来像这样:\n\n![Figure 9.1: The dependencies of generating a pose.](img/Figure_9.1_B16191.jpg)\n\n图 9.1:生成姿势的依赖关系\n\n到本章结束时，您应该能够从 glTF 文件中加载动画剪辑，并将这些剪辑采样成一个姿势。\n\n# 实施姿势\n\n要在变换之间存储父子层次结构，您需要维护两个并行的向量——一个用变换填充，一个用整数填充。整数数组包含每个关节的父变换的索引。不是所有的关节都有父母；如果关节没有父关节，则其父关节值为负。\n\n当考虑骨骼或姿势时，很容易想到有一个根节点和多个从其分支的节点的层次结构。实际上，有两三个根节点并不罕见。有时，文件格式存储模型的方式是骨架的第一个节点是根节点，但也有一个根节点，所有蒙皮网格都是它的子节点。这些层次结构往往如下所示:\n\n![Figure 9.2: Multiple root nodes in one file](img/Figure_9.2_B16191.jpg)\n\n图 9.2:一个文件中的多个根节点\n\n动画角色有三种常见的姿势——当前姿势、绑定姿势和 T2 姿势。其余姿势是所有骨骼的默认配置。动画描述了每块骨头随时间的变化。对动画进行时间采样会产生用于对角色进行蒙皮的当前姿势。绑定姿势将在下一章中介绍。\n\n并非所有动画都会影响角色的每个骨骼或关节；这意味着某些动画可能不会改变关节的值。请记住，在这种情况下，关节被表示为`Transform`对象。如果动画 **A** 在索引`1`处动画化关节，但动画 **B** 没有动画化关节，会发生什么？以下列表显示了结果:\n\n*   如果只玩 **A** 或者 **B** ，一切都好。\n*   如果先玩 **B** 再玩 **A** ，一切都好。\n*   如果你先玩 **A** 然后玩 **B** ，事情就有点不稳定了。\n\n在最后一个示例中，首先播放动画 **A** 其次播放动画 **B** ，索引`1`处的关节保持其从动画 **A** 最后修改的变换。因此，无论何时在动画之间切换，都必须重置当前姿势，使其与其余姿势相同。在下一节中，您将开始声明`Pose`类。\n\n## 声明姿势类\n\n`Pose`类需要跟踪您正在制作动画的角色骨架中每个关节的变换。它还需要跟踪每个关节的父关节。这些数据保存在两个平行的向量中。\n\n在对新的动画剪辑进行采样之前，需要将当前角色的姿势重置为静止姿势。`Pose`类实现了一个复制构造函数和赋值操作符，使复制姿势尽可能快。按照以下步骤申报`Pose`类:\n\n1.  创建一个新的头文件，`Pose.h`。将`Pose`类的定义添加到该文件中，从关节变换的平行向量及其父向量开始:\n\n    ```cpp\n    class Pose {\n    protected:\n        std::vector<Transform> mJoints;\n        std::vector<int> mParents;\n    ```\n\n2.  添加默认构造函数和复制构造函数，并重载赋值运算符。`Pose`类还有一个方便的构造器，它将姿势的关节数作为参数:\n\n    ```cpp\n    public:\n        Pose();\n        Pose(const Pose& p);\n        Pose& operator=(const Pose& p);\n        Pose(unsigned int numJoints);\n    ```\n\n3.  为姿势中的关节数量添加一个 getter 和 setter 函数。使用设置器功能时，`mJoints`和`mParents`向量都需要调整大小:\n\n    ```cpp\n        void Resize(unsigned int size);\n        unsigned int Size();\n    ```\n\n4.  为联合的父级添加 getter 和 setter 函数。这两个函数都需要以关节的指数为自变量:\n\n    ```cpp\n        int GetParent(unsigned int index);\n        void SetParent(unsigned int index, int parent);\n    ```\n\n5.  `Pose`类需要提供一种方法来获取和设置关节的局部变换，以及检索关节的全局变换。重载`[] operator`返回关节的全局变换:\n\n    ```cpp\n        Transform GetLocalTransform(unsigned int index);\n        void SetLocalTransform(unsigned int index, \n                               const Transform& transform);\n        Transform GetGlobalTransform(unsigned int index);\n        Transform operator[](unsigned int index);\n    ```\n\n6.  对于要传递给 OpenGL 的`Pose`类，需要将其转换为矩阵的线性数组。`GetMatrixPalette`功能执行该转换。函数引用一个矩阵向量，并用姿势中每个关节的全局变换矩阵来填充它:\n\n    ```cpp\n        void GetMatrixPalette(std::vector<mat4>& out);\n    ```\n\n7.  通过重载等式和不等式运算符完成`Pose`类的设置:\n\n    ```cpp\n        bool operator==(const Pose& other);\n        bool operator!=(const Pose& other);\n    };\n    ```\n\n`Pose`类用于保存动画层次中每个骨骼的变换。把它想象成动画中的一帧；`Pose`类表示动画在给定时间的状态。在下一节中，您将实现`Pose`类。\n\n## 实现姿势类\n\n创建新的文件，`Pose.cpp`。您将在该文件中实现`Pose`类。采取以下步骤实施`Pose`班:\n\n1.  默认构造函数不需要做任何事情。复制构造函数调用赋值运算符。便利构造器调用`Resize`方法:\n\n    ```cpp\n    Pose::Pose() { }\n    Pose::Pose(unsigned int numJoints) {\n        Resize(numJoints);\n    }\n    Pose::Pose(const Pose& p) {\n        *this = p;\n    }\n    ```\n\n2.  分配操作者需要尽可能快地复制姿势。您需要确保姿势没有分配给它自己。接下来，确保姿势有正确的关节和父母数量。然后，执行记忆复制，快速复制所有父数据和姿势数据:\n\n    ```cpp\n    Pose& Pose::operator=(const Pose& p) {\n        if (&p == this) {\n            return *this;\n        }\n        if (mParents.size() != p.mParents.size()) {\n            mParents.resize(p.mParents.size());\n        }\n        if (mJoints.size() != p.mJoints.size()) {\n            mJoints.resize(p.mJoints.size());\n        }\n        if (mParents.size() != 0) {\n            memcpy(&mParents[0], &p.mParents[0], \n                   sizeof(int) * mParents.size());\n        }\n        if (mJoints.size() != 0) {\n            memcpy(&mJoints[0], &p.mJoints[0], \n                   sizeof(Transform) * mJoints.size());\n        }\n        return *this;\n    }\n    ```\n\n3.  由于父向量和关节向量是平行的，`Resize`函数需要设置两者的大小。`size` getter 函数可以返回任一向量的大小:\n\n    ```cpp\n    void Pose::Resize(unsigned int size) {\n        mParents.resize(size);\n        mJoints.resize(size);\n    }\n    unsigned int Pose::Size() {\n        return mJoints.size();\n    }\n    ```\n\n4.  局部转换的 getter 和 setter 方法很简单:\n\n    ```cpp\n    Transform Pose::GetLocalTransform(unsigned int index) {\n        return mJoints[index];\n    }\n    void Pose::SetLocalTransform(unsigned int index, const Transform& transform) {\n        mJoints[index] = transform;\n    }\n    ```\n\n5.  从当前变换开始，`GetGlobalTransform`方法需要组合父链上的所有变换，直到它到达根骨骼。请记住，变换串联是从右向左进行的。过载的`[] operator`应该被当作`GetGlobalTransform`的别名:\n\n    ```cpp\n    Transform Pose::GetGlobalTransform(unsigned int i) {\n        Transform result = mJoints[i];\n        for (int p = mParents[i]; p >= 0; p = mParents[p]) {\n            result = combine(mJoints[p], result);\n        }\n        return result;\n    }\n    Transform Pose::operator[](unsigned int index) {\n        return GetGlobalTransform(index);\n    }\n    ```\n\n6.  要将一个`Pose`类转换成一个矩阵向量，循环遍历姿态中的每个变换。对于每个变换，找到全局变换，将其转换为矩阵，并将结果存储在矩阵向量中。该功能尚未优化；您将在后面的章节中对其进行优化:\n\n    ```cpp\n    void Pose::GetMatrixPalette(std::vector<mat4>& out) {\n        unsigned int size = Size();\n        if (out.size() != size) {\n            out.resize(size);\n        }\n        for (unsigned int i = 0; i < size; ++ i) {\n            Transform t = GetGlobalTransform(i);\n            out[i] = transformToMat4(t);\n        }\n    }\n    ```\n\n7.  父联合索引的 getter 和 setter 方法很简单:\n\n    ```cpp\n    int Pose::GetParent(unsigned int index) {\n        return mParents[index];\n    }\n    void Pose::SetParent(unsigned int index, int parent) {\n        mParents[index] = parent;\n    }\n    ```\n\n8.  比较两个姿势时，您需要确保两个姿势中的所有关节变换和父索引都相同:\n\n    ```cpp\n    bool Pose::operator==(const Pose& other) {\n        if (mJoints.size() != other.mJoints.size()) {\n            return false;\n        }\n        if (mParents.size() != other.mParents.size()) {\n            return false;\n        }\n        unsigned int size = (unsigned int)mJoints.size();\n        for (unsigned int i = 0; i < size; ++ i) {\n            Transform thisLocal = mJoints[i];\n            Transform otherLocal = other.mJoints[i];\n            int thisParent = mParents[i];\n            int otherParent = other.mParents[i];\n            if (thisParent != otherParent) { return false; }\n            if (thisLocal.position != otherLocal.position) {\n            return false; }\n            if (thisLocal.rotation != otherLocal.rotation {\n            return false; }\n            if (thisLocal.scale != otherLocal.scale { \n            return false; } \n        }\n        return true;\n    }\n    bool Pose::operator!=(const Pose& other) {\n        return !(*this == other);\n    }\n    ```\n\n一个动画角色有多个活动姿势并不罕见。考虑一个角色同时奔跑和开枪的情况。可能会播放两个动画——一个影响下半身，**运行**动画，一个影响上半身，**拍摄**动画。这些姿势混合在一起形成最终姿势，用于显示动画角色。这种类型的动画混合在 [*第 12 章*](12.html#_idTextAnchor204)*动画之间的混合*中有所介绍。\n\n在下一节中，您将实现动画剪辑。动画剪辑包含一个姿势中所有动画关节随时间变化的动画。`Clip`类用于采样动画和生成要显示的姿势。\n\n# 实现剪辑\n\n动画剪辑是动画轨迹的集合；每个轨迹描述一个关节随时间的运动，所有合并的轨迹描述动画模型随时间的运动。如果对动画剪辑进行采样，您将获得一个姿势，该姿势描述了动画剪辑中每个关节在指定时间的配置。\n\n对于一个基本剪辑类，你只需要一个**变换轨迹**的向量。因为变换轨迹包含它们影响的关节的标识，所以每个片段可以有最少数量的轨迹。`Clip`类还应该跟踪元数据，例如剪辑的名称、剪辑是否正在循环以及关于剪辑的时间或持续时间的信息。\n\n## 声明剪辑类\n\n`Clip`类需要来维护变换轨迹的向量。这是剪辑包含的最重要的数据。除了轨道，片段还有名称、开始时间和结束时间，片段应该知道它是否在循环。\n\n`Clip`类的循环属性可以被卸载到管道更下游的构造中(例如动画组件或类似的东西)。然而，当实现一个裸机动画系统时，这是一个放置循环属性的好地方:\n\n1.  新建一个文件`Clip.h`，开始`Clip`类的申报:\n\n    ```cpp\n    class Clip {\n    protected:\n        std::vector<TransformTrack> mTracks;\n        std::string mName;\n        float mStartTime;\n        float mEndTime;\n        bool mLooping;\n    ```\n\n2.  片段的采样方式与轨道的采样方式相同。提供的采样时间可能超出了剪辑的范围。为了解决这个问题，您需要实现一个助手函数来调整提供的采样时间，使其在当前动画剪辑的范围内:\n\n    ```cpp\n    protected:\n        float AdjustTimeToFitRange(float inTime);\n    ```\n\n3.  `Clip`类需要一个默认构造函数来为它的一些成员分配默认值。编译器生成的析构函数、复制构造函数和赋值运算符在这里应该没问题:\n\n    ```cpp\n    public:\n        Clip();\n    ```\n\n4.  `Clip`类应该提供一种方法来获取片段包含的关节数量，以及特定轨道索引的关节标识。您还需要一个基于片段中关节索引的关节标识设置器:\n\n    ```cpp\n        unsigned int GetIdAtIndex(unsigned int index);\n        void SetIdAtIndex(unsigned int idx, unsigned int id);\n        unsigned int Size();\n    ```\n\n5.  从剪辑中检索数据有两种方法。`[] operator`返回指定关节的变换轨迹。如果指定关节不存在轨迹，则会创建并返回一个轨迹。`Sample`函数采用一个`Pose`参考和一个时间，并返回一个也是一个时间的`float`值。该功能在提供的时间将动画剪辑采样到`Pose`参考:\n\n    ```cpp\n        float Sample(Pose& outPose, float inTime);\n        TransformTrack& operator[](unsigned int index);\n    ```\n\n6.  我们需要一个公共助手函数来计算动画剪辑的开始和结束时间。`RecalculateDuration`功能循环遍历所有`TransformTrack`对象，并根据构成剪辑的轨迹设置动画剪辑的开始/结束时间。该函数旨在由从文件格式加载动画剪辑的代码调用。\n\n    ```cpp\n        void RecalculateDuration();\n    ```\n\n7.  最后，`Clip`类接受简单的 getter 和 setter 函数:\n\n    ```cpp\n        std::string& GetName();\n        void SetName(const std::string& inNewName);\n        float GetDuration();\n        float GetStartTime();\n        float GetEndTime();\n        bool GetLooping();\n        void SetLooping(bool inLooping);\n    };\n    ```\n\n这里实现的`Clip`类可以用来动画任何东西；不要觉得自己局限于人类和人形动画。在下一节中，您将实现`Clip`类。\n\n## 实现剪辑类\n\n创建一个新文件，`Clip.cpp`。您将在这个新文件中实现`Clip`类。按照以下步骤实施`Clip`课程:\n\n1.  默认构造函数需要给`Clip`类的成员分配一些默认值:\n\n    ```cpp\n    Clip::Clip() {\n        mName = \"No name given\";\n        mStartTime = 0.0f;\n        mEndTime = 0.0f;\n        mLooping = true;\n    }\n    ```\n\n2.  要实现`Sample`功能，请确保剪辑有效，并且时间在剪辑范围内。然后，循环遍历所有的轨迹。获取轨迹的关节 ID，对轨迹进行采样，并将采样值赋回`Pose`参考。如果变换的某个组件没有设置动画，则参考组件将用于提供默认值。然后，该函数返回调整后的时间:\n\n    ```cpp\n    float Clip::Sample(Pose& outPose, float time) {\n        if (GetDuration() == 0.0f) {\n            return 0.0f;\n        }\n        time= AdjustTimeToFitRange(time);\n        unsigned int size = mTracks.size();\n        for (unsigned int i = 0; i < size; ++ i) {\n            unsigned int j = mTracks[i].GetId(); // Joint\n            Transform local = outPose.GetLocalTransform(j);\n            Transform animated = mTracks[i].Sample(\n                                 local, time, mLooping);\n            outPose.SetLocalTransform(j, animated);\n        }\n        return time;\n    }\n    ```\n\n3.  应该循环的`AdjustTimeToFitRange`函数与您为模板化的`Track`类实现的`AdjustTimeToFitTrack`函数具有相同的逻辑:\n\n    ```cpp\n    float Clip::AdjustTimeToFitRange(float inTime) {\n        if (mLooping) {\n            float duration = mEndTime - mStartTime;\n            if (duration <= 0) { 0.0f; }\n            inTime = fmodf(inTime - mStartTime, \n                           mEndTime - mStartTime);\n            if (inTime < 0.0f) {\n                inTime += mEndTime - mStartTime;\n            }\n            inTime = inTime + mStartTime;\n        }\n        else {\n            if (inTime < mStartTime) {\n                inTime = mStartTime;\n            }\n            if (inTime > mEndTime) {\n                inTime = mEndTime;\n            }\n        }\n        return inTime;\n    }\n    ```\n\n4.  `RecalculateDuration`功能将`mStartTime`和`mEndTime`设置为`0`的默认值。接下来，这些函数循环遍历动画剪辑中的每个`TransformTrack`对象。如果轨道有效，将检索轨道的开始和结束时间。存储最小开始时间和最大结束时间。剪辑的开始时间可能不是`0`；有可能在任意时间点开始剪辑:\n\n    ```cpp\n    void Clip::RecalculateDuration() {\n        mStartTime = 0.0f;\n        mEndTime = 0.0f;\n        bool startSet = false;\n        bool endSet = false;\n        unsigned int tracksSize = mTracks.size();\n        for (unsigned int i = 0; i < tracksSize; ++ i) {\n            if (mTracks[i].IsValid()) {\n                float startTime = mTracks[i].GetStartTime();\n                float endTime = mTracks[i].GetEndTime();\n                if (startTime < mStartTime || !startSet) {\n                    mStartTime = startTime;\n                    startSet = true;\n                }\n                if (endTime > mEndTime || !endSet) {\n                    mEndTime = endTime;\n                    endSet = true;\n                }\n            }\n        }\n    }\n    ```\n\n5.  `[] operator`用于检索片段中特定关节的`TransformTrack`对象。这个函数主要由从文件加载动画剪辑的任何代码使用。该函数对所有轨迹执行线性搜索，以查看是否有任何轨迹以指定关节为目标。如果找到一个合格的赛道，将返回对它的引用。如果没有找到合格的赛道，则创建并返回一个新的赛道:\n\n    ```cpp\n    TransformTrack& Clip::operator[](unsigned int joint) {\n        for (int i = 0, s = mTracks.size(); i < s; ++ i) {\n            if (mTracks[i].GetId() == joint) {\n                return mTracks[i];\n            }\n        }\n        mTracks.push_back(TransformTrack());\n        mTracks[mTracks.size() - 1].SetId(joint);\n        return mTracks[mTracks.size() - 1];\n    }\n    ```\n\n6.  `Clip`类剩下的 getter 函数很简单:\n\n    ```cpp\n    std::string& Clip::GetName() {\n        return mName;\n    }\n    unsigned int Clip::GetIdAtIndex(unsigned int index) {\n        return mTracks[index].GetId();\n    }\n    unsigned int Clip::Size() {\n        return (unsigned int)mTracks.size();\n    }\n    float Clip::GetDuration() {\n        return mEndTime - mStartTime;\n    }\n    float Clip::GetStartTime() {\n        return mStartTime;\n    }\n    float Clip::GetEndTime() {\n        return mEndTime;\n    }\n    bool Clip::GetLooping() {\n        return mLooping;\n    }\n    ```\n\n7.  同样的，`Clip`类剩下的 setter 函数也很简单:\n\n    ```cpp\n    void Clip::SetName(const std::string& inNewName) {\n        mName = inNewName;\n    }\n    void Clip::SetIdAtIndex(unsigned int index, unsigned int id) {\n        return mTracks[index].SetId(id);\n    }\n    void Clip::SetLooping(bool inLooping) {\n        mLooping = inLooping;\n    }\n    ```\n\n动画剪辑总是修改相同的关节。不需要重新设置采样到中的姿态，这样每一帧都是绑定姿态。但是，在切换动画时，不能保证两个剪辑将动画相同的轨道。重置采样到的姿势是个好主意，这样每当我们切换动画剪辑时，它就是绑定姿势！\n\n在下一节中，您将学习如何从 glTF 文件中加载角色的剩余姿势。剩下的姿势很重要；这是一个角色在没有动画时的姿势。\n\n# glTF–加载剩余姿势\n\n在本书中，我们将假设一个 glTF 文件只包含一个动画角色。可以安全地假设 glTF 文件的整个层次结构可以被视为模型的骨架。这使得加载静止姿势变得容易，因为静止姿势成为其初始配置中的层次。\n\n在加载休息姿势之前，您需要创建几个助手函数。这些函数是 glTF 加载程序内部的，不应该在头文件中公开。在`GLTFLoader.cpp`中创建新的命名空间，并将其称为`GLTFHelpers`。所有的辅助函数都是在这个命名空间中创建的。\n\n按照以下步骤实现从 glTF 文件加载静止姿势所需的辅助函数:\n\n1.  首先，实现一个辅助函数，得到`cgltf_node`的局部变换。节点可以将其变换存储为矩阵或单独的位置、旋转和缩放组件。如果节点将其变换存储为矩阵，则使用`mat4ToTransform`分解函数；否则，根据需要创建组件:\n\n    ```cpp\n    // Inside the GLTFHelpers namespace\n    Transform GLTFHelpers::GetLocalTransform(cgltf_node& n){\n        Transform result;\n        if (n.has_matrix) {\n            mat4 mat(&n.matrix[0]);\n            result = mat4ToTransform(mat);\n        }\n        if (n.has_translation) {\n            result.position = vec3(n.translation[0], \n                 n.translation[1], n.translation[2]);\n        }\n        if (n.has_rotation) {\n            result.rotation = quat(n.rotation[0], \n              n.rotation[1], n.rotation[2], n.rotation[3]);\n        }\n        if (n.has_scale) {\n            result.scale = vec3(n.scale[0], n.scale[1], \n                                n.scale[2]);\n        }\n        return result;\n    }\n    ```\n\n2.  接下来，实现一个辅助函数，从数组中获取`cgltf_node`的索引。`GLTFNodeIndex`函数可以通过循环遍历`.gltf`文件中的所有节点并返回您正在搜索的节点的索引来执行简单的线性查找。如果没有找到索引，返回`-1`表示无效索引:\n\n    ```cpp\n    // Inside the GLTFHelpers namespace\n    int GLTFHelpers::GetNodeIndex(cgltf_node* target, \n        cgltf_node* allNodes, unsigned int numNodes) {\n        if (target == 0) {\n            return -1;\n        }\n        for (unsigned int i = 0; i < numNodes; ++ i) {\n            if (target == &allNodes[i]) {\n                return (int)i;\n            }\n        }\n        return -1;\n    }\n    ```\n\n3.  有了这些辅助函数，加载休息姿势只需要很少的工作。遍历当前 glTF 文件中的所有节点。对于每个节点，将本地变换分配给将返回的姿势。您可以使用`GetNodeIndex`辅助函数找到节点的父节点，如果节点没有父节点，该函数将返回【T1:\n\n    ```cpp\n    Pose LoadRestPose(cgltf_data* data) {\n        unsigned int boneCount = data->nodes_count;\n        Pose result(boneCount);\n        for (unsigned int i = 0; i < boneCount; ++ i) {\n            cgltf_node* node = &(data->nodes[i]);\n            Transform transform = \n            GLTFHelpers::GetLocalTransform(data->nodes[i]);\n            result.SetLocalTransform(i, transform);\n            int parent = GLTFHelpers::GetNodeIndex(\n                         node->parent, data->nodes, \n                         boneCount);\n            result.SetParent(i, parent);\n        }\n        return result;\n    }\n    ```\n\n在下一节中，您将学习如何从 glTF 文件中加载关节名称。这些关节名称以与其余姿势关节相同的顺序出现。知道关节名称有助于调试骨骼的样子。联合名称也可以通过索引以外的方式来检索联合。您将在本书中构建的动画系统不支持按名称联合查找，仅支持索引。\n\n# GLTF–装载接头名称\n\n在某些情况下，您可能想要知道指定给加载的每个关节的名称。这有助于使调试或构建工具更加容易。要以加载其余姿势的关节的相同顺序加载每个关节的名称，请遍历关节并使用名称访问器。\n\n在`GLTFLoader.cpp`中实现`LoadJointNames`功能。别忘了给`GLTFLoader.h`添加功能声明:\n\n```cpp\nstd::vector<std::string> LoadJointNames(cgltf_data* data) {\n    unsigned int boneCount = (unsigned int)data->nodes_count;\n    std::vector<std::string> result(boneCount, \"Not Set\");\n    for (unsigned int i = 0; i < boneCount; ++ i) {\n        cgltf_node* node = &(data->nodes[i]);\n        if (node->name == 0) {\n            result[i] = \"EMPTY NODE\";\n        }\n        else {\n            result[i] = node->name;\n        }\n    }\n    return result;\n}\n```\n\n联合名称对于调试非常有用。他们让你把一个关节的索引和一个名字联系起来，所以你知道数据代表什么。在下一节中，您将学习如何从 glTF 文件加载动画剪辑。\n\n# GLTF–加载动画剪辑\n\n要在运行时生成姿势数据，您需要能够加载动画剪辑。和其他姿势一样，这个需要一些辅助功能。\n\n您需要实现的第一个助手函数`GetScalarValues`读取`gltf`访问器的浮点值。这可以通过`cgltf_accessor_read_float`助手功能来完成。\n\n下一个助手函数`TrackFromChannel`完成大部分繁重的工作。它将一个 glTF 动画频道转换成一个`VectorTrack`或一个`QuaternionTrack`。glTF 动画频道记录在[https://github . com/KhronosGroup/glTF-tutories/blob/master/glTF tutorial/glTF tutorial _ 007 _ animations . MD](https://github.com/KhronosGroup/glTF-Tutorials/blob/master/gltfTutorial/gltfTutorial_007_Animations.md)。\n\n`LoadAnimationClips`函数应该返回剪辑对象的向量。这不是最优的；这样做是为了让加载 API 更容易使用。如果性能是一个问题，考虑传递结果向量作为参考。\n\n按照以下步骤从 glTF 文件加载动画:\n\n1.  在`GLTFHelpers`命名空间\n\n    ```cpp\n    // Inside the GLTFHelpers namespace\n    void GLTFHelpers::GetScalarValues( vector<float>& out, \n                      unsigned int compCount, \n                      const cgltf_accessor& inAccessor) {\n        out.resize(inAccessor.count * compCount);\n        for (cgltf_size i = 0; i < inAccessor.count; ++ i) {\n            cgltf_accessor_read_float(&inAccessor, i, \n                                      &out[i * compCount], \n                                      compCount);\n        }\n    }\n    ```\n\n    的`GLTFLoader.cpp`中实现`GetScalarValues`助手函数\n2.  在`GLTFLoader.cpp`中实现`TrackFromChannel`助手功能。通过设置`Track`插值启动功能实现。为此，请确保轨道的`Interpolation`类型与采样器的`cgltf_interpolation_type`类型相匹配:\n\n    ```cpp\n    // Inside the GLTFHelpers namespace\n    template<typename T, int N>\n    void GLTFHelpers::TrackFromChannel(Track<T, N>& result,\n                  const cgltf_animation_channel& channel) {\n        cgltf_animation_sampler& sampler = *channel.sampler;\n        Interpolation interpolation = \n                      Interpolation::Constant;\n        if (sampler.interpolation ==\n            cgltf_interpolation_type_linear) {\n            interpolation = Interpolation::Linear;\n        }\n        else if (sampler.interpolation ==\n                 cgltf_interpolation_type_cubic_spline) {\n            interpolation = Interpolation::Cubic;\n        }\n        bool isSamplerCubic = interpolation == \n                              Interpolation::Cubic;\n        result.SetInterpolation(interpolation);\n    ```\n\n3.  采样器输入是动画时间线的访问器。采样器输出是动画值的访问器。使用`GetScalarValues`将这些访问器转换成浮点数的线性数组。采样输入中的帧数和元素数。每帧的组件数量(`vec3`或`quat`)是值元素的数量除以时间线元素的数量。调整轨道大小，以便有足够的空间存储所有帧:\n\n    ```cpp\n        std::vector<float> time; // times\n        GetScalarValues(time, 1, *sampler.input);\n        std::vector<float> val; // values\n        GetScalarValues(val, N, *sampler.output);\n        unsigned int numFrames = sampler.input->count; \n        unsigned int compCount = val.size() / time.size();\n        result.Resize(numFrames);\n    ```\n\n4.  要将`time`和`value`数组解析为帧结构，循环遍历采样器中的每一帧。对于每一帧，设置时间，然后读取输入正切值，然后读取输出正切值。输入和输出切线仅在采样器为立方时可用；如果不是，这些应该默认为`0`。需要使用局部`offset`变量来处理立方体轨迹，因为输入和输出切线与组件数量一样多:\n\n    ```cpp\n        for (unsigned int i = 0; i < numFrames; ++ i) {\n            int baseIndex = i * compCount;\n            Frame<N>& frame = result[i];\n            int offset = 0;\n            frame.mTime = time[i];\n            for (int comp = 0; comp < N; ++ comp) {\n                frame.mIn[comp] = isSamplerCubic ? \n                      val[baseIndex + offset++ ] : 0.0f;\n            }\n            for (int comp = 0; comp < N; ++ comp) {\n                frame.mValue[comp] = val[baseIndex + \n                                     offset++ ];\n            }\n            for (int comp = 0; comp < N; ++ comp) {\n                frame.mOut[comp] = isSamplerCubic ? \n                      val[baseIndex + offset++ ] : 0.0f;\n            }\n        }\n    } // End of TrackFromChannel function\n    ```\n\n5.  在`GLTFLoader.cpp`中实现`LoadAnimationClips`功能；不要忘记将函数的声明添加到`GLTFLoader.h`中。循环通过提供的`gltf_data`中的所有夹子。为每个片段设置其名称。循环播放片段中的所有通道，找到当前通道影响的节点的索引:\n\n    ```cpp\n    std::vector<Clip> LoadAnimationClips(cgltf_data* data) {\n        unsigned int numClips = data->animations_count;\n        unsigned int numNodes = data->nodes_count;\n        std::vector<Clip> result;\n        result.resize(numClips);\n        for (unsigned int i = 0; i < numClips; ++ i) {\n            result[i].SetName(data->animations[i].name);\n            unsigned int numChannels = \n                     data->animations[i].channels_count;\n            for (unsigned int j = 0; j < numChannels; ++ j){\n                cgltf_animation_channel& channel = \n                          data->animations[i].channels[j];\n                cgltf_node* target = channel.target_node;\n                int nodeId = GLTFHelpers::GetNodeIndex(\n                             target, data->nodes, numNodes);\n    ```\n\n6.  glTF 文件的每个通道都是一个动画轨道。一些节点可能只制作其位置的动画，而其他节点可能制作位置、旋转和缩放的动画。检查解析的通道类型，调用`TrackFromChannel`辅助函数将其转换为动画轨迹。`Track`类的`[] operator`要么检索当前轨道，要么创建一个新轨道。这意味着您正在解析的节点的`TransformTrack`函数始终有效:\n\n    ```cpp\n                if (channel.target_path == \n                     cgltf_animation_path_type_translation){\n                   VectorTrack& track = \n                     result[i][nodeId].GetPositionTrack();\n                   GLTFHelpers::TrackFromChannel<vec3, 3>\n                                (track, channel);\n                }\n                else if (channel.target_path == \n                         cgltf_animation_path_type_scale) {\n                    VectorTrack& track = \n                          result[i][nodeId].GetScaleTrack();\n                    GLTFHelpers::TrackFromChannel<vec3, 3>\n                                (track, channel);\n                }\n                else if (channel.target_path == \n                       cgltf_animation_path_type_rotation) {\n                    QuaternionTrack& track = \n                       result[i][nodeId].GetRotationTrack();\n                    GLTFHelpers::TrackFromChannel<quat, 4>\n                                 (track, channel);\n                }\n            } // End num channels loop\n    ```\n\n7.  剪辑中的所有轨道都已填充后，调用剪辑的`ReclaculateDuration`功能。这确保回放发生在适当的时间范围内:\n\n    ```cpp\n            result[i].RecalculateDuration();\n        } // End num clips loop\n        return result;\n    } // End of LoadAnimationClips function\n    ```\n\n能够加载动画片段并将其采样成姿势大约是动画编程所涉及工作的一半。您可以加载动画剪辑，在应用更新时对其进行采样，并使用调试线来绘制姿势。结果是一个动画骨架。在下一章中，您将学习如何使用这个动画骨架来变形网格。\n\n# 总结\n\n在本章中，您实现了`Pose`和`Clip`类。您学习了如何从 glTF 文件中加载其余的姿势，以及如何加载动画剪辑。您还学习了如何对动画剪辑进行采样以生成姿势。\n\n这本书的可下载内容可以在[网站上找到。`Chapter09/Sample01`中的示例加载一个 glTF 文件，并使用`DebugDraw`功能绘制静止姿势和当前动画姿势。要使用调试线绘制骨骼，请从关节位置到其父关节位置绘制一条线。](https://github.com/PacktPublishing/Game-Animation-Programming)\n\n请记住，并不是所有的剪辑都会为姿势的每个关节设置动画。只要您正在采样的动画剪辑发生变化，它被采样到的帖子就需要重置。重置一个姿势很容易——给它指定其余姿势的值。本章的代码示例演示了这一点。\n\n在下一章中，您将学习如何对动画网格进行蒙皮。一旦你知道如何给网格蒙皮，你就能显示一个动画模型。"
  },
  {
    "path": "docs/handson-cpp-game-ani-prog/10.md",
    "content": "# 十、网格蒙皮\n\n使网格变形以匹配动画姿势称为蒙皮。为了实现蒙皮，首先需要声明一个网格类。一旦声明了网格类，就可以使用着色器(GPU 蒙皮)或仅使用 C++ 代码(CPU 蒙皮)对其进行变形。这两种蒙皮方法都将在本章中介绍。到本章结束时，您应该能够执行以下操作:\n\n*   了解蒙皮网格与非蒙皮网格有何不同\n*   了解整个蒙皮管道\n*   实现一个框架类\n*   从 glTF 文件中加载骨骼的绑定姿势\n*   实现蒙皮网格类\n*   从 gLTF 文件加载蒙皮网格\n*   实现中央处理器蒙皮\n*   实现 GPU 蒙皮\n\n# 探索网格\n\n网格由几个顶点组成。通常，每个顶点至少有一个位置，一个法线，也许还有一个纹理坐标。这是简单静态网格顶点的定义。该定义具有以下顶点分量:\n\n*   位置(`vec3`)\n*   正常(`vec3`)\n*   The texture coordinate (`vec2`)\n\n    重要信息:\n\n    本章中用来演示蒙皮的模型是来自 GDQuest 的 Godot 人体模型。这是麻省理工学院授权的模型，你可以在 https://github.com/GDQuest/godot-3d-mannequ 的 GitHub 上找到。\n\n当一个网格被建模时，它是以某个姿势建模的。对于角色来说，这往往是一个 *T* 的姿势或者是一个 *A* 的姿势。建模的网格是静态的。下图显示了戈多人体模型的 *T* 姿势:\n\n![Figure 10.1: The Godot mannequin's T pose ](img/Figure_10.1_B16191.jpg)\n\n图 10.1:戈多人体模型的 T 型姿势\n\n网格建模后，将在网格中创建骨架。网格中的每个顶点都被分配给骨骼的一个或多个骨骼。这个过程叫做索具。骨架是以适合网格内部的姿势创建的；这是模型的**绑定姿势**。\n\n![Figure 10.2: Visualizing the bind pose of the mesh and skeleton ](img/Figure_10.2_B16191.jpg)\n\n图 10.2:可视化网格和骨架的绑定姿势\n\n绑定姿势和其余姿势通常是相同的，但并不总是这样。在本书中，我们将把这两种姿势作为单独的姿势来对待。上图显示了渲染在角色网格顶部的骨架的绑定姿势。在下一节中，您将探索如何对这样的网格进行蒙皮。\n\n# 了解蒙皮\n\n蒙皮是指定哪个顶点应该被哪个骨骼变形的过程。一个顶点可能会受到多个骨骼的影响。刚性蒙皮是指将每个顶点与一个骨骼相关联。平滑蒙皮将顶点与多个骨骼相关联。\n\n通常，顶点到骨骼的映射是按顶点进行的。这意味着每个顶点都知道自己属于哪个骨骼。有些文件格式以相反的方式存储这种关系，其中每个骨骼都包含一个它所影响的顶点列表。两种方法都有效；在本书的其余部分，映射是按顶点进行的。\n\n若要(刚性)蒙皮网格，请将每个顶点指定给骨骼。要在代码中将关节指定给顶点，请为每个顶点添加一个新属性。该属性只是一个整数，它保存使顶点变形的骨骼的索引。下图中，所有应该分配给左下臂骨的三角形颜色都比网格的其余部分深:\n\n![Figure 10.3: Isolating the lower arm ](img/Figure_10.3_B16191.jpg)\n\n图 10.3:隔离下臂\n\n让我们花一点时间更详细地回顾一下顶点变换管道。这里引入**空间**的概念。空间是指用矩阵变换一个顶点。例如，如果你有一个投影矩阵，它会把一个顶点转换成 NDC 空间。顶点变换管道如下:\n\n*   创建网格时，其所有顶点都在所谓的模型空间中。\n*   模型空间顶点乘以模型矩阵，将它放入世界空间。\n*   世界空间顶点乘以视图矩阵，将其放入相机空间。\n*   摄像机空间顶点乘以投影矩阵，将其移动到 NDC 空间。\n\n要对网格进行蒙皮，需要向顶点变换管道添加新的蒙皮步骤。蒙皮步骤将顶点从蒙皮空间移动到模型空间。这意味着新步骤先于转换管道中的任何其他步骤。\n\n如果蒙皮空间顶点乘以当前动画姿势，则可以将其移回模型空间。本章的*实现 CPU 蒙皮*部分详细介绍了这种转换。一旦顶点回到模型空间，它应该已经被动画化了。动画姿势矩阵变换进行实际动画。动画顶点变换管道是这样工作的:\n\n*   加载一个网格-它的所有顶点都在模型空间中。\n*   模型空间顶点乘以蒙皮矩阵，将其移入蒙皮空间。\n*   同族空间顶点乘以姿态矩阵，将其移回模型空间。\n*   模型空间顶点乘以模型矩阵，将它放入世界空间。\n*   世界空间顶点乘以视图矩阵，将其放入相机空间。\n*   摄像机空间顶点乘以投影矩阵，将其移动到 NDC 空间。\n\n要对网格进行蒙皮，需要将每个顶点转换为蒙皮空间。当皮肤空间中的顶点通过其所属关节的世界变换进行变换时，假设使用的姿势是绑定姿势，则该顶点应该在模型空间中结束。\n\n在下一节中，您将通过实际示例探索蒙皮管道。\n\n## 探索刚性蒙皮\n\n要对网格进行蒙皮，每个顶点都需要乘以其所属关节的反向绑定姿势变换。要找到关节的反向绑定姿势变换，请找到关节的世界变换，然后反转它。当矩阵(或变换)乘以它的逆时，结果总是恒等式。\n\n将皮肤空间网格的顶点乘以绑定姿势中关节的世界空间变换会撤销原始的反向绑定姿势乘法，`inverse bind pose * bind pose = identity`。但是，乘以不同的姿势会导致顶点从绑定姿势偏移两个姿势之间的增量。\n\n让我们探索一个顶点是如何在视觉上移动到皮肤空间的。例如，将 Godot 人体模型前臂中的所有顶点乘以前臂骨骼的反向绑定姿势，仅将前臂三角形放入皮肤空间。这使得网格看起来如下图所示:\n\n![Figure 10.4: The lower-arm mesh transformed by the inverse bind pose  ](img/Figure_10.4_B16191.jpg)\n\n图 10.4:由反向绑定姿势变换的下臂网格\n\n要将顶点从皮肤空间转换回模型空间，请依次应用姿势中每个骨骼的转换，直到到达目标骨骼。下图演示了从根骨到前臂骨需要走的六个步骤:\n\n![Figure 10.5: Visualizing the transform chain to the lower arm ](img/Figure_10.5_B16191.jpg)\n\n图 10.5:将变换链可视化到下臂\n\n在代码中，到达骨骼所需的所有转换都可以使用矩阵乘法来累加。或者，如果使用`Transform`结构，可以使用组合方法。使用累积矩阵或变换将顶点移回模型空间只需一次。\n\n将网格转换为皮肤空间是通过将每个顶点乘以其所属关节的反向绑定姿势来完成的。如何得到骨骼的反向绑定姿态矩阵？使用绑定姿势，找到骨骼的世界变换，将其转换为矩阵，并反转矩阵。\n\n下图为皮肤空间中的戈多人体模型。看到这样的网格表示蒙皮管道中有错误。看到这样的网格最常见的原因是反向绑定姿势和动画姿势的乘法顺序有错误:\n\n![Figure 10.6: The full mesh multiplied by the inverse bind pose ](img/Figure_10.6_B16191.jpg)\n\n图 10.6:全网格乘以反向绑定姿势\n\n到目前为止讨论的蒙皮实现称为刚性蒙皮。使用刚性蒙皮，每个顶点仅受一个骨骼的影响。在下一节中，您将开始探索平滑蒙皮，通过将多个骨骼影响指定给单个顶点，使蒙皮网格看起来更好。\n\n## 刚性蒙皮管道\n\n让我们探索一下每个顶点必须经过的管道。下图显示了静态网格相对于刚性蒙皮网格的变换管道。下图中的步骤顺序是从左到右，跟随箭头:\n\n![Figure 10.7: The vertex skinning pipelines ](img/Figure_10.7_B16191.jpg)\n\n图 10.7:顶点蒙皮管道\n\n上图中所示的**刚性蒙皮顶点管线**的工作原理如下:\n\n*   通过将顶点乘以指定给它的关节的反向绑定姿势矩阵，将顶点移动到皮肤空间中。\n*   将蒙皮顶点乘以动画关节的世界矩阵。这导致顶点再次位于局部空间，但它变形为动画姿态。\n*   一旦顶点处于动画局部位置，将其通过法线模型视图投影变换。\n*   探索平滑蒙皮\n\n刚性蒙皮的问题是弯曲关节。由于每个顶点都属于一个骨骼，所以位于肘关节等关节中的顶点不会自然弯曲。通过将三角形的不同顶点指定给不同的骨骼，可以避免网格中关节处(如肘部)的断裂。生成的网格不能很好地保持其体积，看起来很笨拙。\n\n刚性蒙皮不是免费的；它为每个顶点引入了额外的矩阵乘法。这可以优化到只增加一次乘法，这将在下一章中介绍。在下一节中，您将探索平滑蒙皮。\n\n## 探索平滑蒙皮\n\n刚性蒙皮的主要问题是它可以在网格中创建视觉断点，如下图所示。即使解决了这些问题，光滑蒙皮时可弯曲关节周围的变形看起来也不太好:\n\n![Figure 10.8: A visible artifact of rigid skinning ](img/Figure_10.8_B16191.jpg)\n\n图 10.8:刚性蒙皮的可见人工产物\n\n平滑蒙皮比刚性蒙皮具有更少的伪影并更好地保持其体积。平滑蒙皮背后的想法是，不止一个骨骼可以影响一个顶点。每种影响也有权重。权重用于将蒙皮顶点混合成一个组合的最终顶点。所有重量加起来必须是 1。\n\n将平滑蒙皮想象为多次蒙皮网格并混合结果。一根骨头能产生多少影响，在这里影响很大。一般四块骨头之后，每一块额外骨头的影响都不可见。这很方便，因为它允许您使用`ivec4`和`vec4`结构向顶点添加影响和权重。\n\n下图显示了一个蒙皮的网格，中间的顶点附着在左边的顶部骨骼和右边的底部骨骼上。这是需要混合的两个蒙皮位置。如果每个姿势的权重为`0.5`，最终插值的顶点位置将位于顶点之间的一半。如下图的中间图所示:\n\n![Figure 10.9: Assigning multiple joints to a vertex ](img/Figure_10.9_B16191.jpg)\n\n图 10.9:为一个顶点指定多个关节\n\n对顶点上的关节影响进行平均称为平滑蒙皮，或**线性混合蒙皮** ( **LBS** )。它有一些人工制品，但这是皮肤角色的标准方式。目前，LBS 是实现皮肤动画最流行的方式。\n\n添加对平滑蒙皮的支持后，最终的顶点结构现在如下所示:\n\n*   位置(`vec3`)\n*   正常(`vec3`)\n*   纹理坐标(`vec2`)\n*   联合影响(`ivec4`)\n*   The influence weights (`vec4`)\n\n    重要信息\n\n    glTF 支持将蒙皮网格附加到任意节点，并且这些节点可以被动画化。这为计算皮肤矩阵增加了一个额外的步骤。为了避免这个额外的步骤，我们将忽略网格轴，并假设所有网格节点全局变换都在原点。只要假设单个 glTF 文件只包含一个蒙皮网格，这是一个安全的假设。\n\n平滑蒙皮是目前游戏动画中使用的标准形态。大多数游戏每个顶点使用四个骨骼，其工作方式与本章中将要实现的类似。在下一节中，您将实现一个`Skeleton`类来帮助跟踪皮肤网格所需的一些不同数据。\n\n# 实现骨骼\n\n为模型设置动画时，有几件事需要跟踪，例如动画姿势或反向绑定姿势。骨架的概念是将动画模型之间共享的数据组合成单一结构。\n\n角色的所有实例都共享绑定姿势和反向绑定姿势。也就是说，如果屏幕上有 15 个角色，每个角色都有一个唯一的动画姿势，但它们都共享相同的静止姿势、绑定姿势、反向绑定姿势和关节名称。\n\n在接下来的部分中，您将实现一个新的类-`Skeleton`类。这个`Skeleton`类包含两个动画网格可能需要的所有共享数据。它还跟踪其余姿势、绑定姿势、反向绑定姿势和关节名称。一些发动机称骨架为电枢或钻机。\n\n## 骨骼类声明\n\n`Skeleton`类包含角色的静止姿势和绑定姿势，角色每个关节的名称，最重要的是，反向绑定姿势。由于反向绑定姿势涉及到矩阵的反向，因此只应计算一次。按照以下步骤申报新的`Skeleton`类:\n\n1.  创建新文件，`Skeleton.h`。在此文件中声明`Skeleton`类。将当前动画模型的静止姿势、绑定姿势、反向绑定姿势和关节名称添加到`Skeleton`类。反向绑定姿态应该实现为矩阵向量:\n\n    ```cpp\n    class Skeleton {\n    protected:\n        Pose mRestPose;\n        Pose mBindPose;\n        std::vector<mat4> mInvBindPose;\n        std::vector<std::string> mJointNames;\n    ```\n\n2.  添加助手功能，`UpdateInverseBindPose`。只要设置了绑定姿势，该函数就会更新反向绑定姿势矩阵:\n\n    ```cpp\n    protected:\n        void UpdateInverseBindPose();\n    ```\n\n3.  声明一个默认构造函数和一个便利构造函数。此外，声明方法来设置骨骼的静止姿势、绑定姿势和关节名称，并声明辅助函数来检索对骨骼所有变量的引用:\n\n    ```cpp\n    public:\n        Skeleton();\n        Skeleton(const Pose& rest, const Pose& bind, \n                 const std::vector<std::string>& names);\n        void Set(const Pose& rest, const Pose& bind, \n                 const std::vector<std::string>& names);\n        Pose& GetBindPose();\n        Pose& GetRestPose();\n        std::vector<mat4>& GetInvBindPose();\n        std::vector<std::string>& GetJointNames();\n        std::string& GetJointName(unsigned int index);\n    }; // End Skeleton class\n    ```\n\n将`Skeleton`类想象成一个辅助类——它将绑定姿势、反向绑定姿势、静止姿势和关节名称放入一个易于管理的对象中。骨架是共享的；你可以有许多角色，每个角色都有一个独特的动画姿势，但是他们可以共享同一个骨架。在下一节中，您将实现`Skeleton`类。\n\n## 骨架类实现\n\n反向绑定姿势是以矩阵数组的形式存储在骨架中的。每当骨骼的绑定姿势更新时，也应该重新计算反向绑定姿势。为了找到反向绑定姿势，找到骨架中每个关节的世界空间矩阵，然后反向世界空间关节矩阵。创建新文件，`Skeleton.cpp`。然后，实现骨架构造函数。为此，请采取以下步骤:\n\n1.  创建两个构造函数——默认构造函数不做任何事情。另一个便利构造器采用休息姿势、绑定姿势和关节名称。它调用`Set`方法:\n\n    ```cpp\n    Skeleton::Skeleton() { }\n    Skeleton::Skeleton(const Pose& rest, const Pose& bind,\n                    const std::vector<std::string>& names) {\n        Set(rest, bind, names);\n    }\n    ```\n\n2.  创建`Set`方法，该方法应该设置骨骼的内部姿势、绑定姿势和关节名称。设置绑定姿势后，调用`UpdateInverseBindPose`函数填充反向绑定姿势矩阵调色板:\n\n    ```cpp\n    void Skeleton::Set(const Pose& rest, const Pose& bind, \n                     const std::vector<std::string>& names) {\n        mRestPose = rest;\n        mBindPose = bind;\n        mJointNames = names;\n        UpdateInverseBindPose();\n    }\n    ```\n\n3.  接下来执行`UpdateInverseBindPose`功能。确保矩阵向量具有正确的大小，然后循环遍历绑定姿势中的所有关节。获取每个关节的世界空间变换，将其转换为矩阵，并对矩阵求逆。这个逆矩阵是关节的逆绑定姿态矩阵:\n\n    ```cpp\n    void Skeleton::UpdateInverseBindPose() {\n      unsigned int size = mBindPose.Size();\n      mInvBindPose.resize(size);\n      for (unsigned int i = 0; i < size; ++ i) {\n        Transform world = mBindPose.GetGlobalTransform(i);\n        mInvBindPose[i] = inverse(transformToMat4(world));\n      }\n    }\n    ```\n\n4.  在`Skeleton`类中实现简单的 getter 和 setter 函数:\n\n    ```cpp\n    Pose& Skeleton::GetBindPose() {\n        return mBindPose;\n    }\n    Pose& Skeleton::GetRestPose() {\n        return mRestPose;\n    }\n    std::vector<mat4>& Skeleton::GetInvBindPose() {\n        return mInvBindPose;\n    }\n    std::vector<std::string>& Skeleton::GetJointNames() {\n        return mJointNames;\n    }\n    std::string& Skeleton::GetJointName(unsigned int idx) {\n        return mJointNames[idx];\n    }\n    ```\n\n通过提供显式的 getter 函数，比如`Transform GetBindPoseTransform(unsigned int index)`，可以绕过返回引用。这在你完成下一章学习如何优化动画数据后更有意义。目前，更有价值的做法是访问这些引用，而不是修改它们。\n\n要生成逆绑定姿态矩阵，不需要将变换转换成矩阵再进行逆变换；你可以把变换反过来，然后把它转换成矩阵。两者之间的性能差异极小。\n\n`Skeleton`类跟踪动画模型的绑定姿势、反向绑定姿势和关节名称。该数据可以在模型的所有动画实例之间共享。在下一节中，您将从 glTF 文件中实现绑定姿势加载。glTF 格式不存储实际的绑定姿势。\n\n# glTF–加载绑定姿势\n\n你现在可以从一个 glTF 文件中加载绑定姿势了，但是有一个问题。glTF 文件不存储绑定姿势。相反，对于 glTF 文件包含的每个皮肤，它存储一个矩阵数组，该数组保存影响皮肤的每个关节的反向绑定姿势矩阵。\n\n像这样存储反向绑定姿势矩阵有利于优化，这将在下一章中更有意义，但目前，这是我们必须处理的事情。那么，你如何得到捆绑姿势？\n\n要获得绑定姿势，请加载静止姿势，并将静止姿势中的每个变换转换为世界空间变换。这确保了如果蒙皮没有为关节提供反向绑定姿势矩阵，一个好的默认值是可用的。\n\n接下来，循环通过`.gltf`文件中的每个蒙皮网格。对于每个蒙皮网格，反转每个关节的反向绑定姿势矩阵。反转反向绑定姿态矩阵会产生绑定姿态矩阵。将绑定姿势矩阵转换为可在绑定姿势中使用的变换。\n\n这是可行的，但是所有的关节变换都在世界空间中。您需要转换每个关节，使其位于关节的父关节的本地。采取以下步骤实现`GLTFLoader.cpp`中的`LoadBindPose`功能:\n\n1.  通过构建一个变换向量开始实现`LoadBindPose`函数。用静止姿势中每个关节的全局变换填充变换向量:\n\n    ```cpp\n    Pose LoadBindPose(cgltf_data* data) {\n        Pose restPose = LoadRestPose(data);\n        unsigned int numBones = restPose.Size();\n        std::vector<Transform> worldBindPose(numBones);\n        for (unsigned int i = 0; i < numBones; ++ i) {\n          worldBindPose[i] = restPose.GetGlobalTransform(i);\n        }\n    ```\n\n2.  循环通过 glTF 文件中的每个蒙皮网格。将`inverse_bind_matrices`访问器读入一个大的浮点值向量。向量需要包含`contain numJoints * 16`元素，因为每个矩阵都是 4x4 矩阵:\n\n    ```cpp\n        unsigned int numSkins = data->skins_count;\n        for (unsigned int i = 0; i < numSkins; ++ i) {\n            cgltf_skin* skin = &(data->skins[i]);\n            std::vector<float> invBindAccessor;\n            GLTFHelpers::GetScalarValues(invBindAccessor, \n                         16, *skin->inverse_bind_matrices);\n    ```\n\n3.  对于皮肤中的每个关节，获取反向绑定矩阵。逆绑定姿态矩阵求逆，得到绑定姿态矩阵。将绑定姿势矩阵转换为变换。将这个世界空间变换存储在`worldBindPose`向量中:\n\n    ```cpp\n            unsigned int numJoints = skin->joints_count;\n            for (int j = 0; j < numJoints; ++ j) { \n                // Read the ivnerse bind matrix of the joint\n                float* matrix = &(invBindAccessor[j * 16]);\n                mat4 invBindMatrix = mat4(matrix);\n                // invert, convert to transform\n                mat4 bindMatrix = inverse(invBindMatrix);\n                Transform bindTransform = \n                                mat4ToTransform(bindMatrix);\n                // Set that transform in the worldBindPose.\n                cgltf_node* jointNode = skin->joints[j];\n                int jointIndex = GLTFHelpers::GetNodeIndex(\n                           jointNode, data->nodes, numBones);\n                worldBindPose[jointIndex] = bindTransform;\n            } // end for each joint\n        } // end for each skin\n    ```\n\n4.  转换每个关节，使其相对于其父关节。要将关节移动到另一个关节的空间中，也就是说，使其相对于另一个关节，请将关节的世界变换与其父关节的逆世界变换相结合:\n\n    ```cpp\n        //Convert the world bind pose to a regular bind pose\n        Pose bindPose = restPose;\n        for (unsigned int i = 0; i < numBones; ++ i) {\n            Transform current = worldBindPose[i];\n            int p = bindPose.GetParent(i);\n            if (p >= 0) { // Bring into parent space\n                Transform parent = worldBindPose[p];\n                current = combine(inverse(parent), current);\n            }\n            bindPose.SetLocalTransform(i, current);\n        }\n        return bindPose;\n    } // End LoadBindPose function\n    ```\n\n重建绑定姿势并不理想，但这是你必须处理的 glTF 的一个怪癖。通过使用其余姿势作为默认关节值，任何没有反向绑定姿势矩阵的关节仍然具有有效的默认方向和大小。\n\n在本节中，您学习了如何从 glTF 文件加载动画网格的绑定姿势。在下一节中，您将创建一个便利函数来仅通过一次函数调用从 glTF 文件加载骨架。\n\n# glTF–加载骨骼\n\n我们需要再实现一个加载功能——即`LoadSkeleton`功能。这是一个便利函数，它加载一个骨架而不需要调用三个独立的函数。\n\n在`GLTFLoader.cpp`中实现`LoadSkeleton`功能。别忘了给`GLTFLoader.h`添加函数声明。该函数通过调用现有的`LoadPose`、`LoadBindPose`和`LoadJointNames`函数返回一个新的骨架:\n\n```cpp\nSkeleton LoadSkeleton(cgltf_data* data) {\n    return Skeleton(\n        LoadRestPose(data),\n        LoadBindPose(data),\n        LoadJointNames(data)\n    );\n}\n```\n\n`LoadSkeleton`函数只是一个助手函数，允许你用一个函数调用初始化一个骨架。在下一节中，您将实现一个`Mesh`类，它将让您显示动画网格。\n\n# 实现网格\n\n网格的定义取决于实现它的游戏(或引擎)。实现一个全面的网格类超出了本书的范围。相反，在本节中，您将声明一个简单版本的网格，它在中央处理器和图形处理器上存储一些数据，并提供一种将两者同步在一起的方法。\n\n## 网格类声明\n\n网格最基本的实现是什么？每个顶点都有一个位置、一个法线和一些纹理坐标。为了蒙皮网格，每个顶点也有四个可能影响它的骨骼和权重，以确定每个骨骼对顶点的影响程度。网格通常使用索引数组，但这是可选的。\n\n在本节中，您将实现 CPU 和 GPU 蒙皮。要在中央处理器上蒙皮网格，您需要保留姿势和法线数据的附加副本，以及用于蒙皮的矩阵调色板。\n\n创建一个新文件`Mesh.h`，在其中声明`Mesh`类。按照以下步骤申报新的`Mesh`类:\n\n1.  开始声明`Mesh`类。它应该在中央处理器和图形处理器上维护网格数据的副本。存储定义每个顶点的位置、法线、tex 坐标、权重和影响的向量。包括可选的索引向量:\n\n    ```cpp\n    class Mesh {\n    protected:\n        std::vector<vec3> mPosition;\n        std::vector<vec3> mNormal;\n        std::vector<vec2> mTexCoord;\n        std::vector<vec4> mWeights;\n        std::vector<ivec4> mInfluences;\n        std::vector<unsigned int> mIndices;\n    ```\n\n2.  前面代码中列出的每个向量也需要设置适当的属性。为每一个创建`Attribute`指针，以及一个索引缓冲区指针:\n\n    ```cpp\n    protected:\n        Attribute<vec3>* mPosAttrib;\n        Attribute<vec3>* mNormAttrib;\n        Attribute<vec2>* mUvAttrib;\n        Attribute<vec4>* mWeightAttrib;\n        Attribute<ivec4>* mInfluenceAttrib;\n        IndexBuffer* mIndexBuffer;\n    ```\n\n3.  添加姿势和正常数据的附加副本，以及用于 CPU 蒙皮的矩阵调色板:\n\n    ```cpp\n    protected:\n        std::vector<vec3> mSkinnedPosition;\n        std::vector<vec3> mSkinnedNormal;\n        std::vector<mat4> mPosePalette;\n    ```\n\n4.  添加构造函数、复制构造函数、赋值操作符以及析构函数的声明:\n\n    ```cpp\n    public:\n        Mesh();\n        Mesh(const Mesh&);\n        Mesh& operator=(const Mesh&);\n        ~Mesh();\n    ```\n\n5.  为网格包含的所有属性声明 getter 函数。这些函数返回向量引用。向量引用不是只读的；加载网格时使用这些来填充网格数据:\n\n    ```cpp\n        std::vector<vec3>& GetPosition();\n        std::vector<vec3>& GetNormal();\n        std::vector<vec2>& GetTexCoord();\n        std::vector<vec4>& GetWeights();\n        std::vector<ivec4>& GetInfluences();\n        std::vector<unsigned int>& GetIndices();\n    ```\n\n6.  声明`CPUSkin`功能，应用 CPU 网格蒙皮。要对网格进行蒙皮，您需要骨架和动画姿势。声明`UpdateOpenGLBuffers`函数，该函数将保存数据的向量同步到图形处理器:\n\n    ```cpp\n        void CPUSkin(Skeleton& skeleton, Pose& pose);\n        void UpdateOpenGLBuffers();\n        void Bind(int position, int normal, int texCoord, \n                  int weight, int influence);\n    ```\n\n7.  声明绑定、绘制和解除绑定网格的函数:\n\n    ```cpp\n        void Draw();\n        void DrawInstanced(unsigned int numInstances);\n        void UnBind(int position, int normal, int texCoord, \n                    int weight, int influence);\n    };\n    ```\n\n这个`Mesh`类不是制作就绪的，但是它很容易使用，并且将适用于本书的其余部分。在下一节中，您将开始实现`Mesh`类。\n\n## 网格类实现\n\n`Mesh`类包含两个相同数据的副本。它将中央处理器端的所有顶点数据保存在向量中，将图形处理器端的所有顶点数据保存在顶点缓冲对象中。这个类的预期用途是编辑中央处理器端的顶点，然后用`UpdateOpenGLBuffers`功能将更改同步到图形处理器。\n\n新建一个文件，`Mesh.cpp`；您将在这个文件中实现`Mesh`类。按照以下步骤实施`Mesh`课程:\n\n1.  实现默认构造函数，需要确保所有属性(和索引缓冲区)都已分配:\n\n    ```cpp\n    Mesh::Mesh() {\n        mPosAttrib = new Attribute<vec3>();\n        mNormAttrib = new Attribute<vec3>();\n        mUvAttrib = new Attribute<vec2>();\n        mWeightAttrib = new Attribute<vec4>();\n        mInfluenceAttrib = new Attribute<ivec4>();\n        mIndexBuffer = new IndexBuffer();\n    }\n    ```\n\n2.  实现复制构造函数。以与构造函数相同的方式创建缓冲区，然后调用赋值运算符:\n\n    ```cpp\n    Mesh::Mesh(const Mesh& other) {\n        mPosAttrib = new Attribute<vec3>();\n        mNormAttrib = new Attribute<vec3>();\n        mUvAttrib = new Attribute<vec2>();\n        mWeightAttrib = new Attribute<vec4>();\n        mInfluenceAttrib = new Attribute<ivec4>();\n        mIndexBuffer = new IndexBuffer();\n        *this = other;\n    }\n    ```\n\n3.  实现赋值操作符，复制出 CPU 端成员(所有向量)，然后调用`UpdateOpenGLBuffers`函数将属性数据上传到 GPU:\n\n    ```cpp\n    Mesh& Mesh::operator=(const Mesh& other) {\n        if (this == &other) {\n            return *this;\n        }\n        mPosition = other.mPosition;\n        mNormal = other.mNormal;\n        mTexCoord = other.mTexCoord;\n        mWeights = other.mWeights;\n        mInfluences = other.mInfluences;\n        mIndices = other.mIndices;\n        UpdateOpenGLBuffers();\n        return *this;\n    }\n    ```\n\n4.  实现析构函数，确保删除构造函数分配的所有数据:\n\n    ```cpp\n    Mesh::~Mesh() {\n        delete mPosAttrib;\n        delete mNormAttrib;\n        delete mUvAttrib;\n        delete mWeightAttrib;\n        delete mInfluenceAttrib;\n        delete mIndexBuffer;\n    }\n    ```\n\n5.  实现`Mesh` getter 函数。这些函数返回对向量的引用。参考文献返回后需要编辑:\n\n    ```cpp\n    std::vector<vec3>& Mesh::GetPosition() {\n        return mPosition;\n    }\n    std::vector<vec3>& Mesh::GetNormal() {\n        return mNormal;\n    }\n    std::vector<vec2>& Mesh::GetTexCoord() {\n        return mTexCoord;\n    }\n    std::vector<vec4>& Mesh::GetWeights() {\n        return mWeights;\n    }\n    std::vector<ivec4>& Mesh::GetInfluences() {\n        return mInfluences;\n    }\n    std::vector<unsigned int>& Mesh::GetIndices() {\n        return mIndices;\n    }\n    ```\n\n6.  通过在每个属性对象上调用`Set`来实现`UpdateOpenGLBuffers`功能。如果其中一个中央处理器侧向量的大小为`0`，则无需设置:\n\n    ```cpp\n    void Mesh::UpdateOpenGLBuffers() {\n        if (mPosition.size() > 0) {\n            mPosAttrib->Set(mPosition);\n        }\n        if (mNormal.size() > 0) {\n            mNormAttrib->Set(mNormal);\n        }\n        if (mTexCoord.size() > 0) {\n            mUvAttrib->Set(mTexCoord);\n        }\n        if (mWeights.size() > 0) {\n            mWeightAttrib->Set(mWeights);\n        }\n        if (mInfluences.size() > 0) {\n            mInfluenceAttrib->Set(mInfluences);\n        }\n        if (mIndices.size() > 0) {\n            mIndexBuffer->Set(mIndices);\n        }\n    }\n    ```\n\n7.  实现`Bind`功能。这将采用作为绑定槽索引的整数。如果绑定槽有效(即`0`或更大)，属性的`BindTo`功能为调用:\n\n    ```cpp\n    void Mesh::Bind(int position, int normal, int texCoord, \n                    int weight, int influcence) {\n        if (position >= 0) {\n            mPosAttrib->BindTo(position);\n        }\n        if (normal >= 0) {\n            mNormAttrib->BindTo(normal);\n        }\n        if (texCoord >= 0) {\n            mUvAttrib->BindTo(texCoord);\n        }\n        if (weight >= 0) {\n            mWeightAttrib->BindTo(weight);\n        }\n        if (influcence >= 0) {\n            mInfluenceAttrib->BindTo(influcence);\n        }\n    }\n    ```\n\n8.  实现`Draw`和`DrawInstanced`功能，调用相应的全局`::Draw`和`::DrawInstanced`功能:\n\n    ```cpp\n    void Mesh::Draw() {\n        if (mIndices.size() > 0) {\n            ::Draw(*mIndexBuffer, DrawMode::Triangles);\n        }\n        else {\n            ::Draw(mPosition.size(), DrawMode::Triangles);\n        }\n    }\n    void Mesh::DrawInstanced(unsigned int numInstances) {\n        if (mIndices.size() > 0) {\n            ::DrawInstanced(*mIndexBuffer, \n              DrawMode::Triangles, numInstances);\n        }\n        else {\n            ::DrawInstanced(mPosition.size(), \n              DrawMode::Triangles, numInstances);\n        }\n    }\n    ```\n\n9.  实现`UnBind`函数，该函数也采用整数绑定槽作为参数，但在属性对象上调用`UnBindFrom`:\n\n    ```cpp\n    void Mesh::UnBind(int position, int normal, int texCoord, \n                      int weight, int influence) {\n        if (position >= 0) {\n            mPosAttrib->UnBindFrom(position);\n        }\n        if (normal >= 0) {\n            mNormAttrib->UnBindFrom(normal);\n        }\n        if (texCoord >= 0) {\n            mUvAttrib->UnBindFrom(texCoord);\n        }\n        if (weight >= 0) {\n            mWeightAttrib->UnBindFrom(weight);\n        }\n        if (influcence >= 0) {\n            mInfluenceAttrib->UnBindFrom(influence);\n        }\n    }\n    ```\n\n`Mesh`类包含保存中央处理器数据的向量和将该数据复制到图形处理器的属性。它提供了一个简单的界面来渲染整个网格。在下一节中，您将学习如何实现 CPU 蒙皮来制作网格动画。\n\n### 实现中央处理器蒙皮\n\n先在 CPU 上实现，更容易理解蒙皮，不用担心着色器。在本节中，您将创建一个 CPU 蒙皮参考实现。本章稍后将介绍 GPU 蒙皮。\n\n重要信息:\n\n如果您正在开发的平台具有有限数量的统一寄存器或较小的统一缓冲区，那么 CPU 换肤非常有用。\n\n实现 CPU 蒙皮时，需要保留动画网格的两个副本。`mPosition`和`mNormal`向量不变。蒙皮位置和法线的结果存储在`mSkinnedPosition`和`mSkinnedNormal`中。这些向量然后被同步到要绘制的位置和法线属性。\n\n要对顶点进行蒙皮，需要计算蒙皮变换。皮肤变换需要通过反向绑定姿势，然后通过当前动画姿势来变换顶点。您可以通过在绑定姿势变换上调用反函数来实现这一点，然后将其与姿势变换相结合。\n\n对于每个顶点，`mInfluences`向量中的`ivec4`包含影响顶点的关节标识。您需要通过所有四个关节来变换顶点，这意味着您将网格蒙皮四次——一次蒙皮到影响顶点的每个骨骼。\n\n不是每个关节对最终顶点的贡献都是相同的。对于每个顶点，存储在`mWeights`中的`vec4`包含`0`到`1`的标量值。这些值用于将蒙皮顶点混合在一起。如果关节不影响顶点，则其权重为`0`，对最终蒙皮网格没有影响。\n\n权重的内容预计将标准化，如果所有权重加在一起，它们等于`1`。这样，权重可以用来混合，因为它们加起来会产生`1`的影响。例如:(`0.5`、`0.5`、`0`、`0`)有效，而(`0.6`、`0.5`、`0`、`0`)无效。\n\n按照以下步骤实现中央处理器蒙皮:\n\n1.  开始执行`CPUSkin`功能。确保蒙皮向量有足够的存储空间，并从骨架中获取绑定姿势。接下来，循环通过每个顶点:\n\n    ```cpp\n    void Mesh::CPUSkin(Skeleton& skeleton, Pose& pose) {\n        unsigned int numVerts = mPosition.size();\n        if (numVerts == 0) { return;  }\n        mSkinnedPosition.resize(numVerts);\n        mSkinnedNormal.resize(numVerts);\n        Pose& bindPose = skeleton.GetBindPose();\n        for (unsigned int i = 0; i < numVerts; ++ i) {\n            ivec4& joint = mInfluences[i];\n            vec4& weight = mWeights[i];\n    ```\n\n2.  计算皮肤变换。变换第一个顶点和法向影响:\n\n    ```cpp\n            Transform skin0 = combine(pose[joint.x], \n                              inverse(bindPose[joint.x]));\n            vec3 p0 = transformPoint(skin0, mPosition[i]);\n            vec3 n0 = transformVector(skin0, mNormal[i]);\n    ```\n\n3.  对可能影响当前顶点的其他三个关节重复此过程:\n\n    ```cpp\n            Transform skin1 = combine(pose[joint.y], \n                              inverse(bindPose[joint.y]));\n            vec3 p1 = transformPoint(skin1, mPosition[i]);\n            vec3 n1 = transformVector(skin1, mNormal[i]);\n\n            Transform skin2 = combine(pose[joint.z], \n                              inverse(bindPose[joint.z]));\n            vec3 p2 = transformPoint(skin2, mPosition[i]);\n            vec3 n2 = transformVector(skin2, mNormal[i]);\n\n            Transform skin3 = combine(pose[joint.w], \n                              inverse(bindPose[joint.w]));\n            vec3 p3 = transformPoint(skin3, mPosition[i]);\n            vec3 n3 = transformVector(skin3, mNormal[i]);\n    ```\n\n4.  至此，您已经对顶点进行了四次蒙皮——对影响它的每个骨骼进行一次蒙皮。接下来，您需要将这些合并到最终的顶点中。\n5.  使用`mWeights`混合蒙皮位置和正常位置。将位置和法线属性设置为新更新的蒙皮位置和法线:\n\n    ```cpp\n            mSkinnedPosition[i] = p0 * weight.x + \n                                  p1 * weight.y + \n                                  p2 * weight.z + \n                                  p3 * weight.w;\n            mSkinnedNormal[i] = n0 * weight.x + \n                                n1 * weight.y + \n                                n2 * weight.z + \n                                n3 * weight.w;\n        }\n        mPosAttrib->Set(mSkinnedPosition);\n        mNormAttrib->Set(mSkinnedNormal);\n    }\n    ```\n\n让我们解开这里发生的事情。这是基本的蒙皮算法。每个顶点都有一个称为权重的`vec4`值和一个称为影响的`ivec4`值。每个顶点有四个影响的关节和四个权重。如果关节对顶点没有影响，权重可以是`0`。\n\n`ivec4`影响的`x`、`y`、`z`和`w`分量是动画姿势和反向绑定姿势矩阵阵列中的索引。`vec4`权重的`x`、`y`、`z`和`w`分量是应用于`ivec4`影响的同一分量的标量权重。\n\n循环通过所有顶点。对于每个顶点，通过影响顶点的每个关节的蒙皮变换来变换顶点的位置和法线。皮肤变换是反向绑定姿势和姿势变换的组合。这意味着你最终会蒙皮顶点四次。根据属于关节的重量缩放每个变换位置或法线，并将所有四个值相加。结果总和是蒙皮位置或法线。\n\n这是蒙皮算法；无论如何表达，它都保持不变。有几种方法来表示联合变换，例如使用`Transform`对象、矩阵和对偶四元数。无论表示是什么，算法都保持不变。在下一节中，您将学习如何使用矩阵代替`Transform`对象来实现蒙皮算法。\n\n### 使用矩阵进行蒙皮\n\n对顶点进行蒙皮的常用方法是将矩阵线性混合成一个蒙皮矩阵，然后用这个蒙皮矩阵对顶点进行变换。为此，使用骨架中存储的反向绑定姿势，并从姿势中获取矩阵调色板。\n\n要构建皮肤矩阵，将姿势矩阵乘以反向绑定姿势。请记住，首先应该通过反向绑定姿势来转换顶点，然后是动画姿势。通过从右向左乘法，这将反向绑定姿势放在右侧。\n\n将影响当前顶点的每个关节的矩阵相乘，然后根据顶点的权重缩放生成的矩阵。一旦所有矩阵被缩放，将它们加在一起。生成的矩阵是皮肤矩阵，可用于变换顶点位置和法线。\n\n以下代码使用矩阵调色板蒙皮重新实现了`CPUSkin`功能。这段代码非常类似于在 GPU 上运行蒙皮时需要实现的着色器代码:\n\n```cpp\nvoid Mesh::CPUSkin(Skeleton& skeleton, Pose& pose) {\n    unsigned int numVerts = (unsigned int)mPosition.size();\n    if (numVerts == 0) { return; }\n    mSkinnedPosition.resize(numVerts);\n    mSkinnedNormal.resize(numVerts);\n    pose.GetMatrixPalette(mPosePalette);\n    vector<mat4> invPosePalette = skeleton.GetInvBindPose();\n    for (unsigned int i = 0; i < numVerts; ++ i) {\n        ivec4& j = mInfluences[i];\n        vec4& w = mWeights[i];\n        mat4 m0=(mPosePalette[j.x]*invPosePalette[j.x])*w.x;\n        mat4 m1=(mPosePalette[j.y]*invPosePalette[j.y])*w.y;\n        mat4 m2=(mPosePalette[j.z]*invPosePalette[j.z])*w.z;\n        mat4 m3=(mPosePalette[j.w]*invPosePalette[j.w])*w.w;\n        mat4 skin = m0 + m1 + m2 + m3;\n        mSkinnedPosition[i]=transformPoint(skin,mPosition[i]);\n        mSkinnedNormal[i] = transformVector(skin, mNormal[i]);\n    }\n    mPosAttrib->Set(mSkinnedPosition);\n    mNormAttrib->Set(mSkinnedNormal);\n}\n```\n\n用矩阵蒙皮的代码看起来有点不同，但它仍然是相同的蒙皮算法。矩阵被缩放并相加在一起，而不是变换每个顶点四次并缩放结果。结果是一个单一的皮肤基质。\n\n尽管顶点只变换一次，但引入了四种新的矩阵乘法。所需操作数量差不多，为什么要实现矩阵调色板蒙皮？当您实现图形处理器蒙皮时，很容易使用 GLSL 的内置矩阵。\n\n在本节中，您实现了一个`Mesh`类。网格类使用以下顶点格式:\n\n*   位置(`vec3`)\n*   正常(`vec3`)\n*   纹理坐标`(vec2`)\n*   影响(`ivec4`)\n*   重量(`vec4`)\n\n使用此定义，可以渲染蒙皮网格。在下一节中，您将学习如何从 glTF 文件加载网格。\n\n# GLTF–加载网格\n\n现在你有了一个功能性的`Mesh`类，理论上你可以在 CPU 上皮肤网格。然而，有一个问题——你还不能从一个 glTF 文件中加载一个网格。接下来让我们解决这个问题。\n\n首先创建一个新的助手功能，`MeshFromAttributes`。这只是一个助手函数，所以不需要将其公开给头文件。glTF 将网格存储为图元的集合，每个图元都是属性的集合。这些属性包含与我们的属性类相同的信息，例如位置、法线、权重等等。\n\n`MeshFromAttribute`助手函数接受一个网格和一个`cgltf_attribute`函数，以及一些解析所需的附加数据。该属性包含我们的网格组件之一，如位置、法线、紫外线坐标、权重或影响。该属性提供适当的网格数据。\n\n所有值都作为浮点数读入，但影响顶点的联合影响存储为整数。不要直接将浮点数转换为整数；由于精度问题，演员有可能会返回错误的号码。相反，通过添加 0.5 然后强制转换，将浮点数转换为整数。这样，整数截断总会使它成为正确的数字。\n\ngLTF 存储相对于正在解析的皮肤的关节数组影响关节的索引，而不是节点层次结构。`joints`数组又是一个指向节点的指针。您可以使用这个节点指针，并使用`GetNodeIndex`函数将其转换为节点层次结构中的索引。\n\n按照以下步骤从 glTF 文件中实现网格加载:\n\n1.  在`GLTFHelpers`命名空间中实现`MeshFromAttribute`函数。通过计算当前组件有多少属性开始实现:\n\n    ```cpp\n    // In the GLTFHelpers namespace\n    void GLTFHelpers::MeshFromAttribute(Mesh& outMesh, \n                      cgltf_attribute& attribute, \n                      cgltf_skin* skin, cgltf_node* nodes, \n                      unsigned int nodeCount) {\n        cgltf_attribute_type attribType = attribute.type;\n        cgltf_accessor& accessor = *attribute.data;\n        unsigned int componentCount = 0;\n        if (accessor.type == cgltf_type_vec2) {\n            componentCount = 2;\n        }\n        else if (accessor.type == cgltf_type_vec3) {\n            componentCount = 3;\n        }\n        else if (accessor.type == cgltf_type_vec4) {\n            componentCount = 4;\n        }\n    ```\n\n2.  使用`GetScalarValues`助手函数从提供的访问器中解析数据。创建参考网格的位置、法线、纹理坐标、影响和权重向量；`MeshFromAttribute`功能将写入这些参考:\n\n    ```cpp\n        std::vector<float> values;\n        GetScalarValues(values, componentCount, accessor);\n        unsigned int acessorCount = accessor.count;\n        std::vector<vec3>& positions = outMesh.GetPosition();\n        std::vector<vec3>& normals = outMesh.GetNormal();\n        std::vector<vec2>& texCoords = outMesh.GetTexCoord();\n        std::vector<ivec4>& influences = \n                                 outMesh.GetInfluences();\n        std::vector<vec4>& weights = outMesh.GetWeights();\n    ```\n\n3.  循环遍历当前访问器中的所有值，并根据访问器类型将它们分配给适当的向量。位置、纹理坐标和权重组件都可以通过从值向量中读取数据并将其直接分配给网格中的适当向量来找到:\n\n    ```cpp\n        for (unsigned int i = 0; i < acessorCount; ++ i) {\n            int index = i * componentCount;\n            switch (attribType) {\n            case cgltf_attribute_type_position:\n                positions.push_back(vec3(values[index + 0], \n                                        values[index + 1],\n                                        values[index + 2]));\n                break;\n            case cgltf_attribute_type_texcoord:\n                texCoords.push_back(vec2(values[index + 0], \n                                        values[index + 1]));\n                break;\n            case cgltf_attribute_type_weights:\n                weights.push_back(vec4(values[index + 0], \n                                       values[index + 1], \n                                       values[index + 2], \n                                       values[index + 3]));\n                break;\n    ```\n\n4.  正常读数后，检查其平方长度。如果法线无效，返回一个有效的向量，并考虑记录一个错误。如果法线有效，在将其推入法线向量\n\n    ```cpp\n            case cgltf_attribute_type_normal:\n            {\n                vec3 normal = vec3(values[index + 0], \n                                   values[index + 1], \n                                   values[index + 2]);\n                if (lenSq(normal) < 0.000001f) {\n                    normal = vec3(0, 1, 0);\n                }\n                normals.push_back(normalized(normal));\n            }\n            break;\n    ```\n\n    之前对其进行归一化\n5.  读入影响当前顶点的关节。这些关节存储为浮点数。将其转换为整数:\n\n    ```cpp\n            case cgltf_attribute_type_joints:\n            {\n                // These indices are skin relative.  This \n                // function has no information about the\n                // skin that is being parsed. Add +0.5f to \n                // round, since we can't read integers\n                ivec4 joints(\n                    (int)(values[index + 0] + 0.5f),\n                    (int)(values[index + 1] + 0.5f),\n                    (int)(values[index + 2] + 0.5f),\n                    (int)(values[index + 3] + 0.5f)\n                );\n    ```\n\n6.  使用`GetNodeIndex`辅助函数转换关节索引，使它们从相对于`joints`数组变为相对于骨架层次:\n\n    ```cpp\n                    joints.x = GetNodeIndex(\n                               skin->joints[joints.x], \n                               nodes, nodeCount);\n                    joints.y = GetNodeIndex(\n                               skin->joints[joints.y], \n                               nodes, nodeCount);\n                    joints.z = GetNodeIndex(\n                               skin->joints[joints.z], \n                               nodes, nodeCount);\n                    joints.w = GetNodeIndex(\n                               skin->joints[joints.w], \n                               nodes, nodeCount);\n    ```\n\n7.  确保即使无效节点的值也为`0`。任何负关节指数都会破坏蒙皮实现:\n\n    ```cpp\n                    joints.x = std::max(0, joints.x);\n                    joints.y = std::max(0, joints.y);\n                    joints.z = std::max(0, joints.z);\n                    joints.w = std::max(0, joints.w);\n                influences.push_back(joints);\n            }\n            break;\n            }\n        }\n    }// End of MeshFromAttribute function\n    ```\n\ngLTF 中的**网格**由**图元**组成。一个图元包含位置和法线等属性。glTF 中的每一个图元都被表示为您到目前为止创建的框架中的网格，因为它没有子网格的概念。\n\n现在完成`MeshFromAttribute`功能，接下来执行`LoadMeshes`功能。这是用来加载实际网格数据的功能；需要在`GLTFLoader.h`申报，在`GLTFLoader.cpp`执行。按照以下步骤实现`LoadMeshes`功能:\n\n1.  要实现`LoadMeshes`功能，首先，遍历 glTF 文件中的所有节点。仅处理既有网格又有蒙皮的节点；应跳过任何其他节点:\n\n    ```cpp\n    std::vector<Mesh> LoadMeshes(cgltf_data* data) {\n        std::vector<Mesh> result;\n        cgltf_node* nodes = data->nodes;\n        unsigned int nodeCount = data->nodes_count;\n        for (unsigned int i = 0; i < nodeCount; ++ i) {\n            cgltf_node* node = &nodes[i];\n            if (node->mesh == 0 || node->skin == 0) {\n                continue;\n            }\n    ```\n\n2.  遍历 glTF 文件中的所有原语。为每个图元创建一个新网格。循环遍历图元中的所有属性，并通过调用`MeshFromAttribute`辅助函数\n\n    ```cpp\n            int numPrims = node->mesh->primitives_count;\n            for (int j = 0; j < numPrims; ++ j) {\n                result.push_back(Mesh());\n                Mesh& mesh = result[result.size() - 1];\n                cgltf_primitive* primitive = \n                           &node->mesh->primitives[j];\n                unsigned int ac=primitive->attributes_count;\n                for (unsigned int k = 0; k < ac; ++ k) {\n                    cgltf_attribute* attribute = \n                             &primitive->attributes[k];\n                    GLTFHelpers::MeshFromAttribute(mesh,\n                               *attribute, node->skin, \n                               nodes, nodeCount);\n                }\n    ```\n\n    填充网格数据\n3.  检查原语是否包含索引。如果是，也需要填充网格的索引缓冲区:\n\n    ```cpp\n                if (primitive->indices != 0) {\n                    int ic = primitive->indices->count;\n                    std::vector<unsigned int>& indices = \n                                       mesh.GetIndices();\n                    indices.resize(ic);\n                    for (unsigned int k = 0; k < ic; ++ k) {\n                       indices[k]=cgltf_accessor_read_index(\n                                  primitive->indices, k);\n                    }\n                }\n    ```\n\n4.  网格完成了。调用`UpdateOpenGLBuffers`函数，确保网格可以渲染，并返回网格的结果向量:\n\n    ```cpp\n                mesh.UpdateOpenGLBuffers();\n            }\n        }\n        return result;\n    } // End of the LoadMeshes function\n    ```\n\n由于 glTF 存储了整个场景，而不仅仅是一个网格，因此它支持多个网格——每个网格都由图元组成，这些图元就是实际的三角形。glTF 中的图元可以认为是子网格。这里介绍的 glTF 加载器假设一个文件只包含一个模型。在下一节中，您将学习如何使用着色器将网格蒙皮从中央处理器移动到图形处理器。\n\n# 实现 GPU 蒙皮\n\n您在 [*第 6 章*](06.html#_idTextAnchor104) 、*构建抽象渲染器和 OpenGL* 中创建了一些基本着色器——即`static.vert`着色器和`lit.frag`着色器。`static.vert`着色器可用于显示静态、无网格的网格，该网格加载了和`LoadMeshes`功能。`static.vert`着色器甚至可以显示中央处理器蒙皮的网格。\n\n创建新文件，`skinned.vert`。按照以下步骤实现可以执行矩阵调色板蒙皮的顶点着色器。代码与`static.vert`使用的代码非常相似；突出显示了差异:\n\n1.  每个顶点获得两个新的分量，即影响顶点和每个关节权重的关节索引。这些新部件可以储存在`ivec4`和`vec4` :\n\n    ```cpp\n    #version 330 core\n    uniform mat4 model;\n    uniform mat4 view;\n    uniform mat4 projection;\n    in vec3 position;\n    in vec3 normal;\n    in vec2 texCoord;\n    in vec4 weights;\n    in ivec4 joints;\n    ```\n\n2.  接下来，向着色器添加两个矩阵数组——每个数组的长度为`120`。这个长度是任意的；着色器只需要与蒙皮网格有关节一样多的新统一矩阵。您可以通过在每次加载具有新骨骼数量的骨骼时在代码中生成新的着色器字符串来自动配置它:\n\n    ```cpp\n    uniform mat4 pose[120];\n    uniform mat4 invBindPose[120];\n    out vec3 norm;\n    out vec3 fragPos;\n    out vec2 uv;\n    ```\n\n3.  当着色器的主要功能运行时，计算皮肤矩阵。皮肤矩阵的生成方式与 CPU 皮肤相同-示例皮肤矩阵。它使用相同的逻辑，只是在 GPU 上执行的着色器中:\n\n    ```cpp\n    void main() {\n    mat4 skin =(pose[joints.x]* invBindPose[joints.x]) \n                      * weights.x;\n    skin+=(pose[joints.y] * invBindPose[joints.y]) \n                      * weights.y;\n             skin+=(pose[joints.z] * invBindPose[joints.z])\n                      * weights.z;\n    skin+=(pose[joints.w] * invBindPose[joints.w]) \n                      * weights.w;\n    ```\n\n4.  网格在放置到世界上之前应该会变形。在应用模型矩阵之前，将顶点位置和法线乘以蒙皮矩阵。所有相关代码在此高亮显示:\n\n    ```cpp\n        gl_Position= projection * view * model * \n                     skin * vec4(position,1.0);\n\n        fragPos = vec3(model * skin * vec4(position, 1.0));\n        norm = vec3(model * skin * vec4(normal, 0.0f));\n        uv = texCoord;\n    }\n    ```\n\n要向顶点着色器添加蒙皮支持，可以向每个顶点添加两个新属性，这两个属性最多代表四个可以影响顶点的关节。利用关节和权重属性，构造皮肤矩阵。若要蒙皮网格，请在应用其余顶点变换管道之前，将顶点或法线乘以蒙皮矩阵。\n\n# 总结\n\n在本章中，您学习了捆绑姿势和静止姿势之间的区别。您还创建了一个包含这两者的`Skeleton`类。您学习了蒙皮的一般概念，包括刚性蒙皮(每个顶点一个骨骼)和平滑蒙皮(每个顶点多个骨骼)。\n\n在本章中，我们实现了一个基本的网格类，并介绍了在中央处理器和图形处理器上蒙皮网格的过程，以及从不存储绑定姿势数据的 glTF 文件中加载绑定姿势的过程。\n\n你现在可以应用你学到的技能。完成蒙皮代码后，您可以显示完全动画的模型。模型可以从 glTF 文件中加载，这是一个开放的文件格式规范。\n\n在这本书的可下载示例中，`Chapter10/Sample01`包含一个绘制剩余姿势、绑定姿势和当前动画 ed 姿势的示例。`Chapter10/Sample02`演示如何同时使用 GPU 和 CPU 蒙皮。\n\n在下一章中，您将学习如何优化动画管道的各个方面。这包括姿势生成、蒙皮和缓存变换父查找步骤。"
  },
  {
    "path": "docs/handson-cpp-game-ani-prog/11.md",
    "content": "# 十一、优化动画流水线\n\n到目前为止，您已经编写了一个完整的动画系统，可以加载标准文件格式 gLTF，并在 CPU 或 GPU 上执行蒙皮。动画系统对于大多数简单的动画来说表现得足够好。\n\n在本章中，您将探索优化动画系统的方法，以使其更快、更少占用资源。这包括探索执行蒙皮的替代方法，提高示例动画剪辑的速度，以及重新研究矩阵调色板的生成方式。\n\n这些主题中的每一个都是单独探讨的，您可以选择实现尽可能少或尽可能多的优化。所有这些都很简单，可以用来轻松替换不太理想的管道版本。\n\n本章将涵盖以下主题:\n\n*   预生成皮肤矩阵\n*   将皮肤调色板存储在纹理中\n*   更快的采样\n*   姿势调色板生成\n*   探索`Pose::GetGlobalTransform`\n\n# 预生成皮肤矩阵\n\n**顶点着色器蒙皮**的一个更大的问题是系统占用的制服数量。一个`mat4`对象占据四个均匀的槽，蒙皮顶点着色器目前有两个矩阵数组，每个数组有 120 个元素。总共有 960 个统一插槽，太多了。\n\n顶点着色器中的这两个矩阵数组会发生什么？它们相乘，如下所示:\n\n```cpp\nmat4 skin=(pose[joints.x]*invBindPose[joints.x])*weights.x;\n  skin += (pose[joints.y]*invBindPose[joints.y])*weights.y;\n  skin += (pose[joints.z]*invBindPose[joints.z])*weights.z;\n  skin += (pose[joints.w]*invBindPose[joints.w])*weights.w;\n```\n\n这里一个简单的优化是组合`pose * invBindPose`乘法，这样着色器只需要一个数组。这确实意味着一些换肤过程被移回中央处理器，但这一变化清除了 480 个统一插槽。\n\n## 生成皮肤矩阵\n\n生成皮肤矩阵不需要调用应用编程接口——这很简单。使用`Pose`类的`GetMatrixPalette`功能从当前动画姿态生成矩阵调色板。然后，将调色板中的每个矩阵乘以同一索引的反向绑定姿势矩阵。\n\n显示网格的代码负责计算这些矩阵。例如，一个简单的更新循环可能如下所示:\n\n```cpp\nvoid Sample::Update(float deltaTime) {\n    mPlaybackTime = mAnimClip.Sample(mAnimatedPose, \n                         mPlaybackTime + deltaTime);\n    mAnimatedPose.GetMatrixPalette(mPosePalette);\n    vector<mat4>& invBindPose = mSkeleton.GetInvBindPose();\n    for (int i = 0; i < mPosePalette.size(); ++ i) {\n        mPosePalette[i] = mPosePalette[i] * invBindPose[i];\n    }\n    if (mDoCPUSkinning) {\n        mMesh.CPUSkin(mPosePalette);\n    }\n}\n```\n\n在前面的代码示例中，一个动画剪辑被采样成一个姿势。姿势被转换成矩阵向量。然后将该向量中的每个矩阵乘以相同索引的反向绑定姿态矩阵。矩阵的结果向量是组合的皮肤矩阵。\n\n如果网格是 CPU 蒙皮的，这是调用`CPUSkin`函数的好地方。该功能需要重新实现，以便与组合皮肤矩阵一起工作。如果网格是 GPU 蒙皮的，则需要对着色器进行编辑，使其仅使用一个矩阵数组，渲染代码需要更新为仅传递一个统一数组。\n\n在下面的部分，您将探索如何重新实现`CPUSkin`功能，使其与组合的皮肤矩阵一起工作。这样会稍微加快一点 CPU 结皮的过程。\n\n## CPU 蒙皮\n\n你需要一个新的蒙皮方法，尊重预先相乘的蒙皮矩阵。该函数引用一个矩阵向量。每个位置都由影响它的所有四个骨骼的组合皮肤矩阵转换。这四个结果然后被缩放并相加在一起。\n\n在`Mesh.cpp`增加以下 CPU 蒙皮功能。别忘了给`Mesh.h`添加函数声明:\n\n1.  确保网格有效，开始执行`CPUSkin`功能。一个有效的网格至少有一个顶点。确保`mSkinnedPosition`和`mSkinnedNormal`向量足够大，可以容纳所有顶点:\n\n    ```cpp\n    void Mesh::CPUSkin(std::vector<mat4>& animatedPose) {\n        unsigned int numVerts = mPosition.size();\n        if (numVerts == 0) { \n            return; \n        }\n        mSkinnedPosition.resize(numVerts);\n        mSkinnedNormal.resize(numVerts);\n    ```\n\n2.  接下来，循环遍历网格中的每个顶点:\n\n    ```cpp\n        for (unsigned int i = 0; i < numVerts; ++ i) {\n            ivec4& j = mInfluences[i];\n            vec4& w = mWeights[i];\n    ```\n\n3.  用动画姿势变换每个顶点四次，对影响顶点的每个关节变换一次。要找到蒙皮顶点，按适当的权重缩放每个变换的顶点，并将结果相加:\n\n    ```cpp\n            vec3 p0 = transformPoint(animatedPose[j.x], \n                                     mPosition[i]);\n            vec3 p1 = transformPoint(animatedPose[j.y], \n                                     mPosition[i]);\n            vec3 p2 = transformPoint(animatedPose[j.z], \n                                     mPosition[i]);\n            vec3 p3 = transformPoint(animatedPose[j.w],\n                                     mPosition[i]);\n            mSkinnedPosition[i] = p0 * w.x + p1 * w.y + \n                                  p2 * w.z + p3 * w.w;\n    ```\n\n4.  用同样的方法找到顶点的蒙皮法线:\n\n    ```cpp\n            vec3 n0 = transformVector(animatedPose[j.x], \n                                      mNormal[i]);\n            vec3 n1 = transformVector(animatedPose[j.y], \n                                      mNormal[i]);\n            vec3 n2 = transformVector(animatedPose[j.z], \n                                      mNormal[i]);\n            vec3 n3 = transformVector(animatedPose[j.w], \n                                      mNormal[i]);\n            mSkinnedNormal[i] = n0 * w.x + n1 * w.y + \n                                n2 * w.z + n3 * w.w;\n        }\n    ```\n\n5.  通过将蒙皮顶点位置和蒙皮顶点法线上传到位置和法线属性\n\n    ```cpp\n        mPosAttrib->Set(mSkinnedPosition);\n        mNormAttrib->Set(mSkinnedNormal);\n    }\n    ```\n\n    ，完成功能\n\n核心蒙皮算法保持不变；唯一改变的是变换后的位置是如何生成的。这个函数现在可以只使用已经组合好的矩阵，而不必组合动画姿势和反向绑定姿势。\n\n在下一节中，您将探索如何将此蒙皮函数移动到顶点着色器中。组合动画和反向绑定姿势仍然是在中央处理器上完成的，但是实际顶点的蒙皮可以在顶点着色器中实现。\n\n## GPU 蒙皮\n\n在顶点着色器中实现预乘皮肤矩阵蒙皮很简单。用新的预倍增皮肤姿势替换姿势和反向绑定姿势的输入制服。使用这个新的统一数组生成皮肤矩阵。这就是它的全部——皮肤管道的其余部分保持不变。\n\n创建一个新文件`preskinned.vert`，在中实现新的预蒙皮顶点着色器。将`skinned.vert`的内容复制到这个新文件中。按照以下步骤修改新着色器:\n\n1.  旧的蒙皮顶点着色器有统一的姿势和反向绑定姿势。两种制服都是矩阵阵列。脱下这些制服:\n\n    ```cpp\n    uniform mat4 pose[120];\n    uniform mat4 invBindPose[120];\n    ```\n\n2.  用新的`animated`制服代替制服。这是一个矩阵的单个数组，数组中的每个元素包含`animated`姿态和反向绑定姿态矩阵相乘:\n\n    ```cpp\n    uniform mat4 animated[120];\n    ```\n\n3.  接下来，找到皮肤矩阵的生成位置。生成皮肤矩阵的代码如下所示:\n\n    ```cpp\n    mat4 skin = (pose[joints.x] * invBindPose[joints.x]) *\n                 weights.x;\n        skin += (pose[joints.y] * invBindPose[joints.y]) * \n                 weights.y;\n        skin += (pose[joints.z] * invBindPose[joints.z]) * \n                 weights.z;\n        skin += (pose[joints.w] * invBindPose[joints.w]) * \n                 weights.w;\n    ```\n\n4.  换上新的`animated`制服。对于影响顶点的每个关节，按适当的权重缩放`animated`均匀矩阵，并将结果相加:\n\n    ```cpp\n    mat4 skin = animated[joints.x] * weights.x +\n                animated[joints.y] * weights.y +\n                animated[joints.z] * weights.z +\n                animated[joints.w] * weights.w;\n    ```\n\n着色器的其余部分保持不变。唯一需要更新的是着色器采用的制服以及`skin`矩阵是如何生成的。渲染时`animated`矩阵可以设置如下:\n\n```cpp\n// mPosePalette Generated in the Update method!\nint animated = mSkinnedShader->GetUniform(\"animated\")\nUniform<mat4>::Set(animated, mPosePalette);\n```\n\n您可能已经注意到，CPU 蒙皮实现和 GPU 蒙皮实现是不同的。CPU 实现将顶点变换四次，然后缩放并对结果求和。GPU 实现缩放和求和矩阵，并且只变换顶点一次。两种实现都是有效的，并且它们都产生相同的结果。\n\n在下一节中，您将探讨如何避免使用统一矩阵数组进行蒙皮。\n\n# 将皮肤调色板存储在纹理中\n\n预生成蒙皮矩阵会将蒙皮着色器所需的统一槽的数量减半，但是可以将所需的统一槽的数量减少到一个。这可以通过在纹理中编码预先生成的皮肤矩阵并在顶点着色器中读取该纹理来完成，而不是在统一的数组中。\n\n到目前为止，在这本书里，你只处理了`RGB24`和`RGBA32`纹理。在这些格式中，像素的三个或四个分量使用每个分量 8 位进行编码。这只能保存 256 个唯一值。这些纹理不能提供存储浮点数所需的精度。\n\n这里还有另一种有用的纹理格式——纹理。使用这种纹理格式，向量的每个分量都有一个完整的 32 位浮点数来支持它，给你完全的精度。这个纹理可以用一个特殊的采样函数来采样，这个函数不会对数据进行归一化。`FLOAT32`纹理可以被视为一个缓冲区，中央处理器可以写入，图形处理器可以读取。\n\n这种方法的好处是所需的均匀槽的数量变成了一个——所需的均匀槽是`FLOAT32`纹理的采样器。缺点是速度。必须为每个顶点采样纹理比快速统一数组查找更昂贵。请记住，这些示例查找中的每一个都需要返回几个 32 位浮点数。要传输的数据非常多。\n\n我们在这里不涉及纹理存储皮肤矩阵的实现，因为在*第 15 章*、*中有一大部分专门讨论这个主题，包括完整的代码实现。*\n\n# 更快的采样\n\n目前的动画剪辑采样代码表现不错，只要每个动画持续 1 秒以下。随着多分钟长的动画剪辑，如过场动画，动画系统的性能开始受到影响。为什么动画越长，性能越差？罪魁祸首是`Track::FrameIndex`函数中的以下代码:\n\n```cpp\n    for (int i = (int)size - 1; i >= 0; --i) {\n        if (time >= mFrames[i].mTime) {\n            return i;\n        }\n    }\n```\n\n呈现的循环会遍历轨道中的每一帧。如果一个动画有很多帧，性能就会开始变差。请记住，这段代码是为动画剪辑中每个动画骨骼的每个动画组件执行的。\n\n该功能目前做线性搜索，但可以用更高效的搜索进行优化。由于时间只会增加，在这里使用二分搜索法是一种自然的优化。然而，二分搜索法不是最好的优化。有可能把这个循环变成一个持续的查找。\n\n无论长度如何，采样动画的回放成本都是一样的。它们以已知的采样间隔对每一帧进行计时，找到正确的帧索引只需对提供的时间进行归一化，并将其移动到采样间隔范围内即可。不幸的是，对这样的动画进行采样会占用大量内存。\n\n如果您仍然以给定的时间间隔对动画轨迹进行采样，但是每个时间间隔都指向其左侧和右侧的关键帧，而不是包含完整的姿势，会怎么样？使用这种方法，额外的内存开销最小，并且找到正确的帧是恒定的。\n\n## 优化赛道等级\n\n有两种方法来处理优化`Track`类。您可以创建一个新的类，该类具有类的大部分功能并维护已知采样时间的查找表，或者扩展`Track`类。本节采用后一种方法——我们将扩展`Track`类。\n\n`FastTrack`子类包含一个无符号整数向量。`Track`类以统一的时间间隔采样。对于每个时间间隔，播放头左侧的帧(时间之前的帧)被记录到该向量中。\n\n所有新代码都被添加到现有的`Track.h`和`Track.cpp`文件中。按照以下步骤实施`FastTrack`课程:\n\n1.  找到`Track`类的`FrameIndex`成员函数，标记为`virtual`。这一变化允许新子类重新实现`FrameIndex`功能。更新后的申报应该是这样的:\n\n    ```cpp\n    template<typename T, int N>\n    class Track {\n    // ...\n            virtual int FrameIndex(float time, bool looping);\n    // ...\n    ```\n\n2.  创建一个继承自`Track`的新类`FastTrack`。`FastTrack`类包含一个无符号整数向量——重载的`FrameIndex`函数和一个填充无符号整数向量的函数:\n\n    ```cpp\n    template<typename T, int N>\n    class FastTrack : public Track<T, N> {\n    protected:\n        std::vector<unsigned int> mSampledFrames;\n        virtual int FrameIndex(float time, bool looping);\n    public:\n        void UpdateIndexLookupTable();\n    };\n    ```\n\n3.  为了使`FastTrack`类更容易使用，使用 typedef 为标量、向量和四元数类型创建别名:\n\n    ```cpp\n    typedef FastTrack<float, 1> FastScalarTrack;\n    typedef FastTrack<vec3, 3> FastVectorTrack;\n    typedef FastTrack<quat, 4> FastQuaternionTrack;\n    ```\n\n4.  在。`cpp`文件，为标量、向量和四元数快速轨迹添加模板声明:\n\n    ```cpp\n    template FastTrack<float, 1>;\n    template FastTrack<vec3, 3>;\n    template FastTrack<quat, 4>;\n    ```\n\n由于`FastTrack`类是`Track`的子类，所以现有的 API 都不变。当所讨论的动画具有更多帧时，通过这种方式实现轨迹采样的性能增益更大。在下一节中，您将学习如何构建索引查找表。\n\n### 实现 UpdateIndexLookupTable\n\n`UpdateIndexLookupTable`函数负责填充`mSampledFrames`向量。该功能需要以固定的时间间隔对动画进行采样，并记录每个时间间隔的动画时间之前的帧。\n\n`FastTrack`类应该包含多少样本？这个问题非常依赖于上下文，因为不同的游戏有不同的要求。对于本书的上下文，每秒 60 个样本应该足够了:\n\n1.  确保轨迹有效，开始执行`UpdateIndexLookupTable`功能。一个有效的轨道至少有两帧:\n\n    ```cpp\n    template<typename T, int N>\n    void FastTrack<T, N>::UpdateIndexLookupTable() {\n        int numFrames = (int)this->mFrames.size();\n        if (numFrames <= 1) {\n            return;\n        }\n    ```\n\n2.  接下来，找到需要的样本数量。由于该类每一秒都有`60`个动画样本，因此将持续时间乘以`60` :\n\n    ```cpp\n        float duration = this->GetEndTime() - \n                         this->GetStartTime();\n        unsigned int numSamples = duration * 60.0f;\n        mSampledFrames.resize(numSamples);\n    ```\n\n3.  对于每个样本，找出样本沿轨道的时间。要找到时间，将规范化的迭代器乘以动画持续时间，并将动画的开始时间加入其中:\n\n    ```cpp\n        for (unsigned int i = 0; i < numSamples; ++ i) {\n            float t = (float)i / (float)(numSamples - 1);\n            float time = t*duration+this->GetStartTime();\n    ```\n\n4.  最后，是时候找到每个给定时间的帧索引了。找到本次迭代采样时间之前的帧，并记录在`mSampledFrames`向量中。如果采样帧是最后一帧，则在最后一个索引之前返回索引。请记住，`FrameIndex`函数永远不会返回最后一帧:\n\n    ```cpp\n            unsigned int frameIndex = 0;\n            for (int j = numFrames - 1; j >= 0; --j) {\n                if (time >= this->mFrames[j].mTime) {\n                    frameIndex = (unsigned int)j;\n                    if ((int)frameIndex >= numFrames - 2) {\n                        frameIndex = numFrames - 2;\n                    }\n                    break;\n                }\n            }\n            mSampledFrames[i] = frameIndex;\n        }\n    }\n    ```\n\n`UpdateIndexLookupTable`函数旨在加载时调用。可以通过记住内部`j`循环的最后使用的索引来优化使其更快，因为在每次`i`迭代中，帧索引仅增加。在下一节中，您将学习如何实现`FrameIndex`来使用`mSampledFrames`向量。\n\n### 实现框架索引\n\n`FrameIndex`功能是负责在给定时间之前找到帧。优化的`FastTrack`类使用查找数组，而不是循环遍历轨迹的每一帧。所有输入时间都有非常相似的性能成本。按照以下步骤覆盖`FastTrack`类中的`FrameIndex`功能:\n\n1.  确保轨迹有效，开始执行`FrameIndex`功能。有效轨道必须至少有两帧或更多帧:\n\n    ```cpp\n    template<typename T, int N>\n    int FastTrack<T,N>::FrameIndex(float time,bool loop){\n        std::vector<Frame<N>>& frames = this->mFrames;\n        unsigned int size = (unsigned int)frames.size();\n        if (size <= 1) { \n            return -1; \n    }\n    ```\n\n2.  接下来，确保请求的采样时间在轨道的开始和结束时间之间。如果轨道正在循环，使用`fmodf`将其保持在有效范围内:\n\n    ```cpp\n        if (loop) {\n            float startTime = this->mFrames[0].mTime;\n            float endTime = this->mFrames[size - 1].mTime;\n            float duration = endTime - startTime;\n            time = fmodf(time - startTime, \n                         endTime - startTime);\n            if (time < 0.0f) {\n                time += endTime - startTime;\n            }\n            time = time + startTime;\n        }\n    ```\n\n3.  如果轨道没有循环，夹紧到第一帧或下一帧:\n\n    ```cpp\n        else {\n            if (time <= frames[0].mTime) {\n                return 0;\n            }\n            if (time >= frames[size - 2].mTime) {\n                return (int)size - 2;\n            }\n        }\n    ```\n\n4.  找到归一化样本时间和帧索引。帧索引是标准化的采样时间乘以采样数。如果索引无效，返回`-1`；否则，返回索引指向的帧:\n\n    ```cpp\n        float duration = this->GetEndTime() - \n                         this->GetStartTime();\n        float t = time / duration;\n        unsigned int numSamples = (duration * 60.0f);\n        unsigned int index = (t * (float)numSamples);\n        if (index >= mSampledFrames.size()) {\n            return -1;\n        }\n        return (int)mSampledFrames[index];\n    }\n    ```\n\n`FrameIndex`函数几乎总是在有效时间内被调用，因为它是一个受保护的辅助函数。这意味着找到帧索引所需的时间是一致的，与轨道中的帧数无关。在下一节中，您将学习如何将未优化的`Track`类转换为优化的`FastTrack`类。\n\n## 转换轨道\n\n既然`FastTrack`存在，你是如何创造的？您可以创建一个新的加载函数来加载一个`FastTrack`类，而不是`Track`。或者，您可以创建一个函数，将现有的`Track`类转换为`FastTrack`类。本章采用后一种方法。按照以下步骤创建功能，将`Track`对象转换为`FastTrack`对象:\n\n1.  在`FastTrack.h`中声明`OptimizeTrack`功能。该函数是模板化的。采用与`Track`相同的模板类型:\n\n    ```cpp\n    template<typename T, int N>\n    FastTrack<T, N> OptimizeTrack(Track<T, N>& input);\n    ```\n\n2.  为追踪到`FastTrack.cpp`的所有三种类型声明`OptimizeTrack`函数的模板专门化。这意味着声明使用标量、向量 3 和四元数轨迹的专门化:\n\n    ```cpp\n    template FastTrack<float, 1> \n    OptimizeTrack(Track<float, 1>& input);\n    template FastTrack<vec3, 3> \n    OptimizeTrack(Track<vec3, 3>& input);\n    template FastTrack<quat, 4> \n    OptimizeTrack(Track<quat, 4>& input);\n    ```\n\n3.  要实现`OptimizeTrack`功能，请调整结果轨迹的大小，使其与输入轨迹的大小相同，并与插值匹配。重载的`[]`运算符功能可用于复制每帧数据:\n\n    ```cpp\n    template<typename T, int N>\n    FastTrack<T, N> OptimizeTrack(Track<T, N>& input) {\n        FastTrack<T, N> result;\n        result.SetInterpolation(input.GetInterpolation());\n        unsigned int size = input.Size();\n        result.Resize(size);\n        for (unsigned int i = 0; i < size; ++ i) {\n            result[i] = input[i];\n        }\n        result.UpdateIndexLookupTable();\n        return result;\n    }\n    ```\n\n仅仅把`Track`类优化成`FastTrack`还不够。`TransformTrack`班也需要改变。它需要包含新的，优化的`FastTrack`类。在下一节中，您将更改`TransformTrack`类，使其模板化，并且可以包含`Track`或`FastTrack`。\n\n## 创建快速转换轨迹\n\n使用`Track`类的高级结构，如`TransformTrack`，需要适应新的`FastTrack`子类的。`FastTrack`班和`Track`班有相同的签名。因为类的签名是相同的，所以很容易模板化`TransformTrack`类，以便它可以使用这两个类中的任何一个。\n\n在本节中，您将把`TransformTrack`类重命名为`TTransformTrack`，并对该类进行模板化。然后，您将`typedef`模板专门化为`TransformTrack`和`FastTransformTrack`。这样，`TransformTrack`类保持不变，优化的变换轨迹使用相同的代码:\n\n1.  将`TransformTrack`类的名称更改为`TTransformTrack`，并模板化该类。该模板采用两个参数——要使用的向量轨迹类型和四元数轨迹类型。更新`mPosition`、`mRotation`和`mScale`轨道以使用新的模板化类型:\n\n    ```cpp\n    template <typename VTRACK, typename QTRACK>\n    class TTransformTrack {\n    protected:\n       unsigned int mId;\n       VTRACK mPosition;\n       QTRACK mRotation;\n       VTRACK mScale;\n    public:\n       TTransformTrack();\n       unsigned int GetId();\n       void SetId(unsigned int id);\n       VTRACK& GetPositionTrack();\n       QTRACK& GetRotationTrack();\n       VTRACK& GetScaleTrack();\n       float GetStartTime();\n       float GetEndTime();\n       bool IsValid();\n       Transform Sample(const Transform& r,float t,bool l);\n    };\n    ```\n\n2.  将此类定义为`TransformTrack`，参数为`VectorTrack`和`QuaternionTrack`。再次输入`FastTransformTrack`，以`FastVectorTrack`和`FastQuaternionTrack`为模板参数:\n\n    ```cpp\n    typedef TTransformTrack<VectorTrack, \n        QuaternionTrack> TransformTrack;\n    typedef TTransformTrack<FastVectorTrack, \n        FastQuaternionTrack> FastTransformTrack;\n    ```\n\n3.  声明将`TransformTrack`转换为`FastTransformTrack`的优化函数:\n\n    ```cpp\n    FastTransformTrack OptimizeTransformTrack(\n                       TransformTrack& input);\n    ```\n\n4.  在`TransformTrack.cpp`中添加两个`typedef`功能的模板规格:\n\n    ```cpp\n    template TTransformTrack<VectorTrack, QuaternionTrack>;\n    template TTransformTrack<FastVectorTrack, \n                             FastQuaternionTrack>;\n    ```\n\n5.  实现`OptimizeTransformTrack`功能。复制轨道标识，然后按值复制单个轨道:\n\n    ```cpp\n    FastTransformTrack OptimizeTransformTrack(\n                       TransformTrack& input) {\n        FastTransformTrack result;\n        result.SetId(input.GetId());\n        result.GetPositionTrack()= OptimizeTrack<vec3, 3> (\n                                 input.GetPositionTrack());\n        result.GetRotationTrack() = OptimizeTrack<quat, 4>(\n                                 input.GetRotationTrack());\n        result.GetScaleTrack()  =  OptimizeTrack<vec3, 3> (\n                                    input.GetScaleTrack());\n        return result;\n    }\n    ```\n\n因为`OptimizeTransformTrack`是按值复制实际的轨迹数据，所以可能会有点慢。该函数旨在初始化时被调用。在下一节中，您将对`Clip`类进行模板化，类似于您对`Transform`类的模板化，以创建`FastClip`。\n\n## 创建快速剪辑\n\n这个动画系统的用户与`Clip`对象交互。为了适应新的`FastTrack`类，`Clip`类同样被模板化，并分为`Clip`和`FastClip`。您将实现一个功能，将`Clip`对象转换为`FastClip`对象。按照这些步骤来模板化`Clip`类:\n\n1.  将`Clip`类的名称更改为`TClip`，并模板化该类。模板只采用一种类型，即`TClip`类包含的变换轨迹的类型。更改`mTracks`的类型和`[] operator`的返回类型，使其为模板类型:\n\n    ```cpp\n    template <typename TRACK>\n    class TClip {\n    protected:\n        std::vector<TRACK> mTracks;\n        std::string mName;\n        float mStartTime;\n        float mEndTime;\n        bool mLooping;\n    public:\n        TClip();\n        TRACK& operator[](unsigned int index);\n    // ...\n    ```\n\n2.  将`TransformTrack`类型定义为`Clip`。将`FastTransformTrack`型定义为`FastClip`。这样，`Clip`类不会改变，`FastClip`类可以重用所有已有的代码:\n\n    ```cpp\n    typedef TClip<TransformTrack> Clip;\n    typedef TClip<FastTransformTrack> FastClip;\n    ```\n\n3.  声明一个函数，将`Clip`对象转换为`FastClip`对象:\n\n    ```cpp\n    FastClip OptimizeClip(Clip& input);\n    ```\n\n4.  在`Clip.cpp` :\n\n    ```cpp\n    template TClip<TransformTrack>;\n    template TClip<FastTransformTrack>;\n    ```\n\n    中声明这些类型化类的模板专门化\n5.  要实现`OptimizeClip`功能，复制输入片段的名称和循环值。对于片段中的每个关节，调用其轨道上的`OptimizeTransformTrack`功能。在返回新的`FastClip`对象的副本之前，不要忘记计算它的持续时间:\n\n    ```cpp\n    FastClip OptimizeClip(Clip& input) {\n        FastClip result;\n        result.SetName(input.GetName());\n        result.SetLooping(input.GetLooping());\n        unsigned int size = input.Size();\n        for (unsigned int i = 0; i < size; ++ i) {\n            unsigned int joint = input.GetIdAtIndex(i);\n            result[joint] = \n                  OptimizeTransformTrack(input[joint]);\n        }\n        result.RecalculateDuration();\n        return result;\n    }\n    ```\n\n与其他转换函数一样，`OptimizeClip`仅用于在初始化时调用。在下一节中，您将探索如何优化`Pose`调色板的生成。\n\n# 姿势调色板生成\n\n你应该考虑的最后一个优化是从`Pose`生成矩阵调色板的过程。如果你看一下`Pose`类，下面的代码将一个姿势转换成矩阵的线性阵列:\n\n```cpp\nvoid Pose::GetMatrixPalette(std::vector<mat4>& out) {\n    unsigned int size = Size();\n    if (out.size() != size) {\n        out.resize(size);\n    }\n    for (unsigned int i = 0; i < size; ++ i) {\n        Transform t = GetGlobalTransform(i);\n        out[i] = transformToMat4(t);\n    }\n}\n```\n\n就其本身而言，这个函数还不算太坏，但是`GetGlobalTransform`函数循环遍历指定关节变换链上的每个关节，直到根关节。这意味着该函数浪费了大量的时间来寻找变换的矩阵，而它在之前的迭代中已经找到了变换的矩阵。\n\n要解决这个问题，您需要确保`Pose`类中关节的顺序是升序的。也就是说，在`mJoints`数组中，所有父关节的索引必须低于其子关节的索引。\n\n一旦设置了这个顺序，就可以遍历所有关节，知道当前索引处关节的父矩阵已经找到。这是因为所有父元素的索引都低于其子元素。要将此关节的局部矩阵与其父关节的全局矩阵相结合，只需将先前找到的世界矩阵和局部矩阵相乘即可。\n\n不能保证输入数据可以被信任以这种特定顺序列出关节。要解决这个问题，你需要写一些代码来重新安排一个`Pose`类的关节。在下一节中，您将学习如何改进`GetMatrixPalette`函数，以便它在可能的情况下使用优化的方法，而在不能的情况下返回到未优化的方法。\n\n## 更改 GetMatrixPalette 函数\n\n在本节中，如果当前关节的父索引低于该关节，您将修改`GetMatrixPalette`函数以预缓存全局矩阵。如果这个假设被打破，函数需要回到较慢的计算模式。\n\n`GetMatrixPalette`功能会有两个循环。第一个循环查找并存储变换的全局矩阵。如果关节父项的索引小于关节，则使用优化方法。如果关节的父关节不是更小，第一个循环就会爆发，给第二个循环一个运行的机会。\n\n在第二个循环中，每个关节返回调用缓慢的`GetWorldTransform`函数来找到它的世界变换。此循环是优化循环失败时使用的回退代码。如果优化循环一直执行，则第二个循环不会执行:\n\n```cpp\nvoid Pose::GetMatrixPalette(std::vector<mat4>& out) {\n    int size = (int)Size();\n    if ((int)out.size() != size) { out.resize(size); }\n    int i = 0;\n    for (; i < size; ++ i) {\n        int parent = mParents[i];\n        if (parent > i) { break; }\n        mat4 global = transformToMat4(mJoints[i]);\n        if (parent >= 0) {\n            global = out[parent] * global;\n        }\n        out[i] = global;\n    }\n    for (; i < size; ++ i) {\n        Transform t = GetGlobalTransform(i);\n        out[i] = transformToMat4(t);\n    }\n}\n```\n\n这一变化给`GetMatrixPalette`函数增加了非常少的开销，但很快就弥补了这一点。它使矩阵调色板计算运行得很快，如果可能的话，但如果不可能的话，仍然执行。在下一节中，您将学习如何重新排列加载模型的关节，以使`GetMatrixPalette`功能始终采用快速路径。\n\n## 重新排列关节\n\n不是所有的模型都会被很好地格式化；正因为如此，他们不会都能够利用的优化`GetMatrixPalette`功能。在本节中，您将学习如何重新排列模型的骨骼，以便它可以利用优化的`GetMatrixPalette`功能。\n\n创建新文件，`RearrangeBones.h`。使用键值对是骨骼索引的字典来重新映射骨骼索引。`RearrangeSkeleton`功能生成本词典并重新排列骨骼中的绑定、反向绑定和静止姿势。\n\n一旦`RearrangeSkeleton`功能生成了`BoneMap`，就可以使用它来处理任何影响当前骨骼的网格或动画剪辑。按照以下步骤对关节进行重新排序，以便骨骼可以始终利用优化的`GetMatrixPalette`路径:\n\n1.  将以下函数声明添加到`RearrangeBones.h`文件中:\n\n    ```cpp\n    typedef std::map<int, int> BoneMap;\n    BoneMap RearrangeSkeleton(Skeleton& skeleton);\n    void RearrangeMesh(Mesh& mesh, BoneMap& boneMap);\n    void RearrangeClip(Clip& clip, BoneMap& boneMap);\n    void RearrangeFastclip(FastClip& clip, BoneMap& boneMap);\n    ```\n\n2.  在新文件`ReearrangeBones.cpp`中开始执行`RearrangeSkeleton`功能。首先，创建对其余部分的引用并绑定姿势，然后确保您正在重新排列的骨架不是空的。如果是空的，就返回一个空字典:\n\n    ```cpp\n    BoneMap RearrangeSkeleton(Skeleton& skeleton) {\n        Pose& restPose = skeleton.GetRestPose();\n        Pose& bindPose = skeleton.GetBindPose();\n        unsigned int size = restPose.Size();\n        if (size == 0) { return BoneMap(); }\n    ```\n\n3.  接下来，创建一个二维整数数组(整数向量的向量)。外部向量的每个元素代表一个骨骼，该向量的索引和绑定或静止姿势中的`mJoints`数组是平行的。内向量表示外向量索引处的关节包含的所有子向量。循环通过静止姿势中的每个关节:\n\n    ```cpp\n        std::vector<std::vector<int>> hierarchy(size);\n        std::list<int> process;\n        for (unsigned int i = 0; i < size; ++ i) {\n            int parent = restPose.GetParent(i);\n    ```\n\n4.  如果关节有父节点，则将关节的索引添加到父节点的子节点向量中。如果节点是根节点(因此它没有父节点)，请将其直接添加到进程列表中。该列表稍后将用于遍历地图深度:\n\n    ```cpp\n            if (parent >= 0) {\n                hierarchy[parent].push_back((int)i);\n            }\n            else {\n                process.push_back((int)i);\n            }\n        }\n    ```\n\n5.  为了弄清楚如何对骨骼重新排序，您需要保留两个映射——一个从旧配置映射到新配置，另一个从新配置映射回旧配置:\n\n    ```cpp\n        BoneMap mapForward;\n        BoneMap mapBackward;\n    ```\n\n6.  对于每个元素，如果它包含子元素，则将这些子元素添加到进程列表中。这样，所有的关节都被处理，并且变换层次中更高的关节首先被处理:\n\n    ```cpp\n        int index = 0;\n        while (process.size() > 0) {\n            int current = *process.begin();\n            process.pop_front();\n            std::vector<int>& children = hierarchy[current];\n            unsigned int numChildren = children.size();\n            for (unsigned int i = 0; i < numChildren; ++ i) {\n                process.push_back(children[i]);\n            }\n    ```\n\n7.  将正向映射的当前索引设置为正在处理的关节的索引。正向映射的当前索引是一个原子计数器。对反向映射做同样的事情，但是切换键值对。不要忘记将空节点(`-1`)添加到两个映射:\n\n    ```cpp\n            mapForward[index] = current;\n            mapBackward[current] = index;\n            index += 1;\n        }\n        mapForward[-1] = -1;\n        mapBackward[-1] = -1;\n    ```\n\n8.  现在地图已经填好了，你需要建立新的休息和绑定姿势，这些姿势的骨骼顺序是正确的。循环遍历原始静止和绑定姿势中的每个关节，并将它们的局部变换复制到新姿势。对联名做同样的事情:\n\n    ```cpp\n        Pose newRestPose(size);\n        Pose newBindPose(size);\n        std::vector<std::string> newNames(size);\n        for (unsigned int i = 0; i < size; ++ i) {\n            int thisBone = mapForward[i];\n            newRestPose.SetLocalTransform(i, \n                    restPose.GetLocalTransform(thisBone));\n            newBindPose.SetLocalTransform(i, \n                    bindPose.GetLocalTransform(thisBone));\n            newNames[i] = skeleton.GetJointName(thisBone);\n    ```\n\n9.  为每个关节找到新的父关节标识需要两个映射步骤。首先，将当前索引映射到原始骨骼中的骨骼。这将返回原始骨架的父骨架。将该父索引映射回新骨架。这就是为什么有两个字典，使这个映射快速:\n\n    ```cpp\n            int parent = mapBackward[bindPose.GetParent(\n                                             thisBone)];\n            newRestPose.SetParent(i, parent);\n            newBindPose.SetParent(i, parent);\n        }\n    ```\n\n10.  一旦找到新的休息和绑定姿势，并且关节名称已经相应地重新排列，通过调用公共`Set`方法将这些数据写回到骨架。骨架的`Set`方法也计算反向绑定姿态矩阵调色板:\n\n    ```cpp\n        skeleton.Set(newRestPose, newBindPose, newNames);\n        return mapBackward;\n    } // End of RearrangeSkeleton function\n    ```\n\n`RearrangeSkeleton`功能重新排列骨骼中的骨骼，以便骨骼可以利用优化版本的`GetMatrixPalette`。重新排列骨架是不够的。由于关节索引已移动，任何引用此骨架的剪辑或网格现在都已断开。在下一节中，您将实现辅助函数来重新排列剪辑中的关节。\n\n## 重新排序剪辑\n\n要重新排列动画剪辑，循环播放剪辑中的所有轨道。对于每个轨迹，找到关节标识，然后使用`RearrangeSkeleton`函数返回的(向后)骨骼图转换该关节标识。将修改后的接头标识写回到大头钉中:\n\n```cpp\nvoid RearrangeClip(Clip& clip, BoneMap& boneMap) {\n    unsigned int size = clip.Size();\n    for (unsigned int i = 0; i < size; ++ i) {\n        int joint = (int)clip.GetIdAtIndex(i);\n        unsigned int newJoint = (unsigned int)boneMap[joint];\n        clip.SetIdAtIndex(i, newJoint);\n    }\n}\n```\n\n如果您已经实现了本章前面的`FastClip`优化，`RearrangeClip`功能应该仍然有效，因为它是`Clip`的子类。在下一节中，你将学习如何在网格中重新排列关节，这将是使用该优化所需的最后一步。\n\n## 重新排列网格\n\n要重新排列影响网格蒙皮的关节，循环遍历网格的每个顶点，并重新映射存储在该顶点的“影响”属性中的所有四个关节索引。关节的权重不需要编辑，因为关节本身没有变化；只有它在数组中的索引发生了变化。\n\n以这种方式更改网格只会编辑网格的中央处理器副本。调用`UpdateOpenGLBuffers`将新属性也上传到 GPU:\n\n```cpp\nvoid RearrangeMesh(Mesh& mesh, BoneMap& boneMap) {\n    std::vector<ivec4>& influences = mesh.GetInfluences();\n    unsigned int size = (unsigned int)influences.size();\n    for (unsigned int i = 0; i < size; ++ i) {\n        influences[i].x = boneMap[influences[i].x];\n        influences[i].y = boneMap[influences[i].y];\n        influences[i].z = boneMap[influences[i].z];\n        influences[i].w = boneMap[influences[i].w];\n    }\n    mesh.UpdateOpenGLBuffers();\n}\n```\n\n实现`RearrangeMesh`函数后，可以加载一个骨骼，然后调用`RearrangeSkeleton`函数并存储它返回的骨骼图。使用此骨骼贴图，您还可以使用`RearrangeClip`和`RearrangeMesh`功能修复任何引用骨骼的网格或动画剪辑。以这种方式处理资产后，`GetMatrixPalette`总是采用优化的路径。在下一节中，您将探索在层次结构中缓存转换。\n\n# 探索姿势::GetGlobalTransform\n\n使`Pose`类的`GetGlobalTransform`函数的一个特点是它总是计算世界变换。考虑这样一种情况，您请求节点的世界变换，然后紧接着请求其父节点的世界变换。原始请求计算并使用父节点的世界变换，但是一旦发出下一个请求，就会再次计算相同的变换。\n\n对此的解决方案是向`Pose`类添加两个新数组。一个是世界空间变换的向量，另一个包含脏标志。只要设置了关节的局部变换，关节的脏标志就需要设置为`true`。\n\n当一个世界变换被请求时，变换及其所有父变换的脏标志被检查。如果链中有脏变换，世界变换将被重新计算。如果未设置脏标志，则返回缓存的世界转换。\n\n在本章中，您不会实现这种优化。这种优化为`Pose`类的每个实例增加了大量内存。除了逆运动学的情况外，`GetGlobalTransform`功能很少使用。对于蒙皮，`GetMatrixPalette`函数用于检索世界空间矩阵，并且该函数已经过优化。\n\n# 总结\n\n在本章中，您探讨了如何针对几个场景优化动画系统。这些优化减少了顶点蒙皮着色器所需的制服数量，加快了具有许多关键帧的动画的采样速度，并更快地生成姿势的矩阵调色板。\n\n请记住，没有放之四海而皆准的解决方案。如果游戏中的所有动画都有几个关键帧，则使用查找表优化动画采样所增加的开销可能不值得额外的内存。然而，更改采样函数以使用二分搜索法可能是值得的。每个优化策略都有相似的优缺点；您必须选择对您的特定用例有意义的东西。\n\n查看本章的示例代码时，`Chapter11/Sample00`包含本章的全部代码。`Chapter11/Sample01`展示了如何使用预蒙皮网格，`Chapter11/Sample02`展示了如何使用`FastTrack`类来实现更快的采样，`Chapter11/Sample03`展示了如何重新排列骨骼来实现更快的调色板生成。\n\n在下一章中，您将探索如何混合动画以在两个动画之间平滑切换。本章还将探索通过添加混合来修改现有动画的混合技术。"
  },
  {
    "path": "docs/handson-cpp-game-ani-prog/12.md",
    "content": "# 十二、动画之间的融合\n\n从一个动画到另一个动画的过渡可能会不和谐。想象一下，如果一个角色正处于出拳过程中，而玩家决定要开始奔跑。如果动画只是从跳转剪辑切换到运行剪辑，过渡将是艰难和不自然的。\n\n动画混合可以通过生成两个动画的平均中间帧来解决这个问题。这种褪色通常很短——四分之一秒或更短。这种短暂的混合产生的平滑动画过渡提供了更好的外观体验。\n\n本章探讨如何实现动画混合和添加动画混合，以及如何设置交叉渐变控制器来管理混合队列。将涵盖以下主题:\n\n*   姿势混合\n*   交叉渐变动画\n*   添加剂混合\n\n# 姿势混合\n\n动画混合是每个关节的局部空间中两个姿势之间的线性混合。把它想象成一个`lerp`或`mix`函数，但应用于整个姿势。该技术不混合动画剪辑；相反，它混合了这些片段被采样到的姿势。\n\n混合两个姿势时，整个姿势不需要混合。假设有两个动画——一个运行周期和一个攻击。如果玩家按下攻击按钮，攻击姿势的上半部分会在短时间内混合，在整个动画中保持`1`的权重，然后在接近动画结束时混合出来。\n\n这是一个使用姿势混合来创建跑步攻击动画的例子，而不必制作攻击动画的腿部动画。攻击动画可以混合在行走动画的行走循环之上。动画混合可以用来在动画之间平滑过渡，或者将多个动画组合成一个新的动画。\n\n在下一节中，您将为`Pose`类声明一个`Blend`函数。这个`Blend`函数将在两个姿势之间线性插值，类似于向量`lerp`的工作方式。该功能需要两个姿势和一个插值，通常表示为`t`，其范围为`0`至`1`\n\n## 声明混合函数\n\n`Blend`函数采用两个姿势——一个混合值和一个根节点——作为参数。当混合值为`0`时，`Blend`功能返回第一个姿势，当混合值为`1`时，返回第二个姿势。对于`0`和`1`之间的任何值，姿势都是混合的。根节点决定第二个动画的哪个节点(及其子节点)应该混合到第一个动画中。\n\n为了适应指定开始混合的根骨骼，需要有一种方法来检查一个节点是否在另一个节点的层次结构中。`IsInHierarchy`函数取一个`Pose`类，一个节点是根节点，一个节点是搜索节点。如果搜索节点是根节点的后代，函数返回`true`:\n\n```cpp\nbool IsInHierarchy(Pose& pose, unsigned int root, \n                   unsigned int search);\nvoid Blend(Pose& output,Pose& a,Pose& b,float t,int root);\n```\n\n混合两个姿势时，假设姿势相似。相似的姿势具有相同数量的关节，并且每个关节在姿势之间具有相同的父索引。在下一节中，您将实现`Blend`功能。\n\n## 实现混合功能\n\n为了融合工作，必须发生在局部空间，便于两个姿势之间的融合。循环遍历输入姿势中的所有关节，并在混合的两个姿势中的关节的局部变换之间进行插值。对于位置和比例，使用向量`lerp`函数，对于旋转，使用四元数`nlerp`函数。\n\n要支持动画根，请检查当前变换是否是混合根的后代。如果是，进行混合。如果不是，跳过混合并保留第一个输入姿势的变换值。按照以下步骤实现层次检查和`Blend`功能:\n\n1.  要检查一个关节是否是另一个关节的后代，请沿着后代关节一直向上直到根节点。如果在此层次中遇到的任何节点是您要检查的节点，则返回`true` :\n\n    ```cpp\n    bool IsInHierarchy(Pose& pose, unsigned int parent, \n                       unsigned int search) {\n        if (search == parent) {\n            return true;\n        }\n        int p = pose.GetParent(search);\n        while (p >= 0) {\n            if (p == (int)parent) {\n                return true;\n            }\n            p = pose.GetParent(p);\n        }\n        return false;\n    }\n    ```\n\n2.  要将两个姿势混合在一起，循环每个姿势的关节。如果当前关节不在混合根的层次结构中，请不要混合它。否则，使用您在 [*第 5 章*](05.html#_idTextAnchor094)*实现变换*中编写的`mix`功能混合`Transform`对象。`mix`函数考虑了四元数邻域:\n\n    ```cpp\n    void Blend(Pose& output, Pose& a, Pose& b, \n               float t, int root) {\n        unsigned int numJoints = output.Size();\n        for (unsigned int i = 0; i < numJoints; ++ i) {\n            if (root >= 0) {\n                if (!IsInHierarchy(output, root, i)) {\n                    continue;\n                }\n            }\n            output.SetLocalTransform(i, mix(\n                  a.GetLocalTransform(i), \n                  b.GetLocalTransform(i), t)\n            );\n        }\n    }\n    ```\n\n如果使用整个层次混合两个动画，则`Blend`的根参数将为负。对于混合根的负关节，`Blend`功能跳过`IsInHierarchy`检查。在下面的部分，您将探索如何在两个动画之间渐变以实现平滑过渡。\n\n# 交叉渐变动画\n\n混合动画最常见的用例是两个动画之间的交叉渐变。一个**交叉渐变**是从一个动画到另一个动画的快速混合。交叉渐变的目标是隐藏两个动画之间的过渡。\n\n一旦完成交叉渐变，激活的动画需要被你正在渐变到的动画替换。如果您渐变到多个动画，它们都会被评估。最早结束的先被移除。请求的动画被添加到列表中，淡出的动画从列表中移除。\n\n在下一节中，您将构建一个`CrossFadeController`类来处理交叉渐变逻辑。这个类提供了一个简单直观的应用编程接口，只需一次函数调用就可以使动画之间的淡入淡出变得简单。\n\n## 创建助手类\n\n当将动画淡入已经采样的姿势时，你需要知道正在淡入的动画是什么，它的当前播放时间，淡入持续时间的长度，以及淡入的当前时间。这些值用于执行实际混合，并包含有关混合状态的数据。\n\n创建一个新文件并命名为`CrossFadeTarget.h`以实现中的`CrossFadeTarget`助手类。这个助手类包含前面描述的变量。默认构造函数应该将所有内容的值设置为`0`。还提供了一个方便的构造函数，用于获取剪辑指针、姿势引用和持续时间:\n\n```cpp\nstruct CrossFadeTarget {\n   Pose mPose;\n   Clip* mClip;\n   float mTime;\n   float mDuration;\n   float mElapsed;\n   inline CrossFadeTarget() \n          : mClip(0), mTime(0.0f), \n            mDuration(0.0f), mElapsed(0.0f) { }\n   inline CrossFadeTarget(Clip* target,Pose& pose,float dur) \n          : mClip(target), mTime(target->GetStartTime()), \n            mPose(pose), mDuration(dur), mElapsed(0.0f) { }\n};\n```\n\n`CrossFadeTarget`辅助类的`mPose`、`mClip`和`mTime`变量用于每一帧中，以对要淡入的动画进行采样。`mDuration`和`mElapsed`变量用于控制动画应该淡入多少。\n\n在下一节中，您将实现一个控制动画播放和淡入淡出的类。\n\n## 声明交叉衰落控制器\n\n跟踪当前播放的片段并管理渐变是一个新的`CrossFadeController`类的工作。创建一个新文件`CrossFadeController.h`，在其中声明新的类。这个类需要包含一个骨架、一个姿势、当前播放时间和一个动画剪辑。它还需要一个控制动画混合的`CrossFadeTarget`对象的向量。\n\n`CrossFadeController`和`CrossFadeTarget`类都包含指向动画剪辑的指针，但是它们没有这些指针。因为两个类都不拥有指针的内存，所以生成的构造函数、复制构造函数、赋值操作符和析构函数都可以使用。\n\n`CrossFadecontroller`类需要设置当前骨架、检索当前姿势、检索当前剪辑的函数。可以使用`Play`功能设置当前动画。使用`FadeTo`功能可以混合新的动画。由于`CrossFadeController`类管理动画播放，它需要一个`Update`函数来采样动画剪辑:\n\n```cpp\nclass CrossFadeController {\nprotected:\n    std::vector<CrossFadeTarget> mTargets;\n    Clip* mClip;\n    float mTime;\n    Pose mPose;\n    Skeleton mSkeleton;\n    bool mWasSkeletonSet;\npublic:\n    CrossFadeController();\n    CrossFadeController(Skeleton& skeleton);\n    void SetSkeleton(Skeleton& skeleton);\n    void Play(Clip* target);\n    void FadeTo(Clip* target, float fadeTime);\n    void Update(float dt);\n    Pose& GetCurrentPose();\n    Clip* GetcurrentClip();\n};\n```\n\n每一帧都会评估整个`mTargets`列表。每一个动画都会被评估并混合到当前正在播放的动画中。\n\n在下一节中，您将实现`CrossFadeController`类。\n\n## 实现交叉衰落控制器\n\n创建新文件，`CrossFadeController.cpp`。`CrossFadeController`在这个新文件中实现。按照这些步骤执行`CrossFadeController`:\n\n1.  在默认构造函数中，为当前剪辑和时间设置默认的值`0`，并将骨架标记为未设置。有一个方便的构造函数接受一个框架引用。便利构造器应该调用`SetSkeleton`函数:\n\n    ```cpp\n    CrossFadeController::CrossFadeController() {\n        mClip = 0;\n        mTime = 0.0f;\n        mWasSkeletonSet = false;\n    }\n    CrossFadeController::CrossFadeController(Skeleton& skeleton) {\n        mClip = 0;\n        mTime = 0.0f;\n        SetSkeleton(skeleton);\n    }\n    ```\n\n2.  实现`SetSkeleton`功能，将提供的骨架复制到`CrossFadeController`中。它将该类标记为设置了骨架，并将其余姿势复制到交叉渐变控制器的内部姿势中:\n\n    ```cpp\n    void CrossFadeController::SetSkeleton(\n                              Skeleton& skeleton) {\n        mSkeleton = skeleton;\n        mPose = mSkeleton.GetRestPose();\n        mWasSkeletonSet = true;\n    }\n    ```\n\n3.  实现`Play`功能。该功能应清除任何活跃的交叉渐变。它应该设置剪辑和回放时间，但它也需要将当前姿势重置为骨骼的其余姿势:\n\n    ```cpp\n    void CrossFadeController::Play(Clip* target) {\n        mTargets.clear();\n        mClip = target;\n        mPose = mSkeleton.GetRestPose();\n        mTime = target->GetStartTime();\n    }\n    ```\n\n4.  执行`FadeTo`功能，检查请求的淡入淡出目标是否有效。淡入淡出目标只有在不是淡入淡出列表中的第一项或最后一项时才有效。假设满足这些条件，`FadeTo`功能会将提供的动画剪辑和持续时间添加到渐变列表中:\n\n    ```cpp\n    void CrossFadeController::FadeTo(Clip* target, \n                                     float fadeTime) {\n        if (mClip == 0) {\n            Play(target);\n            return;\n        }\n        if (mTargets.size() >= 1) {\n            Clip* clip=mTargets[mTargets.size()-1].mClip;\n            if (clip == target) {\n                return;\n            }\n        }\n        else {\n            if (mClip == target) {\n                return;\n            }\n        }\n        mTargets.push_back(CrossFadeTarget(target, \n               mSkeleton.GetRestPose(), fadeTime));\n    }\n    ```\n\n5.  实现`Update`功能，播放激活的动画，并融入淡入淡出列表中的任何其他动画:\n\n    ```cpp\n    void CrossFadeController::Update(float dt) {\n        if (mClip == 0 || !mWasSkeletonSet) {\n            return;\n        }\n    ```\n\n6.  将当前动画设置为目标动画，如果动画已经完成淡入淡出，则移除淡入淡出对象。每帧只移除一个目标。如果您想要移除所有淡出的目标，请将循环改为向后:\n\n    ```cpp\n        unsigned int numTargets = mTargets.size();\n        for (unsigned int i = 0; i < numTargets; ++ i) {\n            float duration = mTargets[i].mDuration;\n            if (mTargets[i].mElapsed >= duration) {\n                mClip = mTargets[i].mClip;\n                mTime = mTargets[i].mTime;\n                mPose = mTargets[i].mPose;\n                mTargets.erase(mTargets.begin() + i);\n                break;\n            }\n        }\n    ```\n\n7.  将淡入淡出列表与当前动画混合。需要对当前动画和淡入淡出列表中的所有动画进行采样:\n\n    ```cpp\n        numTargets = mTargets.size();\n        mPose = mSkeleton.GetRestPose();\n        mTime = mClip->Sample(mPose, mTime + dt);\n        for (unsigned int i = 0; i < numTargets; ++ i) {\n            CrossFadeTarget& target = mTargets[i];\n            target.mTime = target.mClip->Sample(\n                         target.mPose, target.mTime + dt);\n            target.mElapsed += dt;\n            float t = target.mElapsed / target.mDuration;\n            if (t > 1.0f) { t = 1.0f; }\n            Blend(mPose, mPose, target.mPose, t, -1);\n        }\n    }\n    ```\n\n8.  用`GetCurrentPose`和`GetCurrentclip`助手函数完成`CrossFadeController`类的实现。这些是简单的 getter 函数:\n\n    ```cpp\n    Pose& CrossFadeController::GetCurrentPose() {\n        return mPose;\n    }\n    Clip* CrossFadeController::GetcurrentClip() {\n        return mClip;\n    }\n    ```\n\n您现在可以创建`CrossFadeController`的实例来控制动画播放，而不是手动控制播放什么动画。当你开始播放新动画时，`CrossFadeController`类会自动淡入新动画。在下一节中，您将探索添加动画混合。\n\n# 添加剂混合\n\n添加动画用于通过添加额外的关节运动来修改动画。一个常见的例子是左倾。如果有一个左倾动画只是弯曲角色的脊柱，它可以添加到行走动画中，以创建左倾行走动画、跑步动画或任何其他类型的动画。\n\n不是所有的动画都适合添加动画。附加动画通常是专门制作的。我在本章示例代码提供的`Woman.gltf`文件中添加了一个`Lean_Left`动画。这部动画是加性的。它只会弯曲一个脊椎关节。\n\n附加动画通常不会根据时间播放，而是根据其他输入播放。可以把向左倾斜作为一个例子——它应该由用户的操纵杆来控制。操纵杆越靠近左边的，在动画中倾斜应该走得越远。将附加动画的回放与时间以外的内容同步是很常见的。\n\n## 声明附加动画\n\n添加剂混合的功能在`Blending.h`中声明。第一个函数`MakeAditivePose`将时间点`0`的附加片段采样为输出姿势。这个输出姿势是用来将两个姿势加在一起的参考。\n\n`Add`功能执行两个姿势之间的加法混合过程。添加混合公式为*结果姿态* = *输入姿态* + ( *添加姿态–添加基础姿态*)。前两个参数，即输出姿势和输入姿势，可以指向同一个姿势。要应用附加姿势，需要附加姿势和附加姿势的参考:\n\n```cpp\nPose MakeAdditivePose(Skeleton& skeleton, Clip& clip);\nvoid Add(Pose& output, Pose& inPose, Pose& addPose, \n         Pose& additiveBasePose, int blendroot);\n```\n\n`MadeAdditivePose`辅助函数生成`Add`函数作为其第四个参数的附加基本姿势。该函数旨在初始化期间调用。在下一节中，您将实现这些功能。\n\n## 实现附加动画\n\n在`Blending.cpp`中实现`MakeAdditivePose`功能。该函数仅在加载期间调用。它应该在剪辑开始时对所提供的剪辑进行采样。样本结果是基于加性姿势:\n\n```cpp\nPose MakeAdditivePose(Skeleton& skeleton, Clip& clip) {\n    Pose result = skeleton.GetRestPose();\n    clip.Sample(result, clip.GetStartTime());\n    return result;\n}\n```\n\n添加混合的公式为*结果姿态* = *输入姿态* + ( *添加姿态–添加基础姿态*)。附加基本姿势的减法仅在动画的第一帧和当前帧之间应用附加动画的增量。正因为如此，你只能动画一个骨骼，比如说一个脊椎骨骼，并达到一个让角色向左倾斜的效果。\n\n要实现添加混合，循环通过姿势的每个关节。与常规动画混合一样，有一个`blendroot`参数需要考虑。使用每个关节的局部变换遵循提供的公式:\n\n```cpp\nvoid Add(Pose& output, Pose& inPose, Pose& addPose, \n         Pose& basePose, int blendroot) {\n   unsigned int numJoints = addPose.Size();\n   for (int i = 0; i < numJoints; ++ i) {\n      Transform input = inPose.GetLocalTransform(i);\n      Transform additive = addPose.GetLocalTransform(i);\n      Transform additiveBase=basePose.GetLocalTransform(i);\n      if (blendroot >= 0 && \n          !IsInHierarchy(addPose, blendroot, i)) {\n         continue;\n       }\n       // outPose = inPose + (addPose - basePose)\n       Transform result(input.position + \n           (additive.position - additiveBase.position),\n            normalized(input.rotation * \n           (inverse(additiveBase.rotation) * \n            additive.rotation)),\n            input.scale + (additive.scale - \n            additiveBase.scale)\n        );\n        output.SetLocalTransform(i, result);\n    }\n}\n```\n\n重要信息\n\n四元数没有减法运算符。要从四元数 *B* 中移除四元数 *A* 的旋转，将 *B* 乘以 *A* 的倒数。四元数的逆应用了旋转的逆，这就是为什么四元数乘以它的逆会得到恒等式。\n\n添加动画最常用于创建新的动画变体，例如，将行走动画与蹲伏姿势混合以创建蹲伏行走。所有动画都可以与蹲伏姿势相结合，以编程方式创建蹲伏版本的动画。\n\n# 总结\n\n在本章中，您学习了如何混合多个动画。混合动画可以混合整个层次或只是一个子集。您还构建了一个系统来管理新动画播放时动画之间的淡入淡出。我们还介绍了附加动画，当给定关节角度进行插值时，它可以用来创建新的运动。\n\n本章的可下载材料中包含四个示例。`Sample00`是书中至此的所有代码。`Sample01`演示如何通过在计时器上混合行走和跑步动画来使用`Blend`功能。`Sample02`演示了交叉渐变到随机动画的交叉渐变控制器的使用，`Sample03`演示了如何使用添加动画混合。\n\n在下一章，你将学习反向运动学。反向运动学可以让你根据角色末端的位置来计算角色的肢体应该如何弯曲。想想把一个角色的脚踩在不平的地面上。"
  },
  {
    "path": "docs/handson-cpp-game-ani-prog/13.md",
    "content": "# 十三、实现逆运动学\n\n**逆运动学** ( **IK** )是求解一组关节应该如何定向以到达世界空间中指定点的过程。例如，您可以为角色指定一个触摸点。通过使用 IK，您可以了解如何以角色的手指始终接触特定点的方式旋转角色的肩膀、肘部和手腕。\n\nIK 常用的算法有两种，即 CCD 和 FABRIK。这两者都将在本章中介绍。到本章结束时，您应该能够执行以下操作:\n\n*   了解 CCD IK 的工作原理\n*   实现一个电荷耦合器件求解器\n*   了解 FABRIK 的工作原理\n*   实现 FABRIK 求解器\n*   实现球窝约束\n*   实现铰链约束\n*   了解 IK 解算器适合动画管道的位置和方式\n\n# 创建电荷耦合器件解算器\n\n在这一节，你将学习并实现 CCD IK 算法。 **CCD** 代表**循环坐标下降**。该算法可用于以链上的最后一个关节尽可能接近接触目标的方式设置关节链。您将能够使用电荷耦合器件来创建肢体和其他 IK 系统，其中需要使用目标点来解决链。\n\nCCD 有三个重要的概念。首先是**目标**，你试图触及的空间点。接下来是 **IK 链**，这是一个需要旋转才能达到目标的所有关节的列表。最后是**末端执行器**，它是链条中的最后一个关节(需要接触目标的关节)。\n\n有了目标、链和末端效应器，伪代码中的 CCD 算法如下所示:\n\n```cpp\n// Loop through all joints in the chain in reverse, \n// starting with the joint before the end effecor\nforeach joint in ikchain.reverse() {\n    // Find a vector from current joint to end effector\n    jointToEffector = effector.position - joint.position\n    // Find a vector from the current joint to the goal\n    jointToGoal = goal.position - joint.position\n    // Rotate the joint so the joint to effector vector \n    // matches the orientation of the joint to goal vector\n    joint.rotation = fromToRotation(jointToEffector, \n                        jointToGoal) * joint.rotation\n}\n```\n\n电荷耦合器件算法看起来很简单，但它是如何工作的？从效应器前的关节开始。旋转效应器对链条没有影响。找到从效应器前的关节到目标的向量，然后找到从关节到效应器的向量。旋转有问题的关节，使两个向量对齐。对每个接头重复上述步骤，直到基础接头:\n\n![](img/Figure_13.1_B16191.jpg)\n\n图 13.1:可视化电荷耦合器件算法\n\n看*图 13.1* ，末端执行器不接触目标。为什么不呢？CCD 是一种迭代算法，前面的步骤描述了一次迭代。需要每帧多次迭代来实现收敛。在接下来的部分中，我们将学习如何声明 CCD 求解器，这将引导我们实现`CCDSolver`类。\n\n## 声明 CCD 求解器\n\n在这个部分，你将声明 CCD 解算器。这将使您有机会在深入实现之前熟悉应用编程接口，并了解该类在高层次上是如何工作的。\n\n创建一个新文件`CCDSolver.h`，在这个文件中声明`CCDSolver`类。`CCDSolver`类应该包含一个构成 IK 链的变换向量。假设 IK 链具有父子关系，其中每个索引都是它之前的索引的子级，使 0 成为我们的根节点。因此，IK 链中的每个变换都在局部空间中声明。按照以下步骤声明电荷耦合器件 IK 解算器:\n\n1.  首先用三个变量声明`CCDSolver`类:形成 IK 链的变换列表，要执行的迭代次数，以及一些小的增量，可以用来控制在链被认为解决之前目标必须离目标有多近。同时声明默认构造函数:\n\n    ```cpp\n    class CCDSolver {\n    protected:\n        std::vector<Transform> mIKChain;\n        unsigned int mNumSteps;\n        float mThreshold;\n    public:\n        CCDSolver();\n    ```\n\n2.  为 IK 链的大小、步数和阈值实现 getter 和 setter 函数。声明哪个`[] operator`用于获取和设置局部联合变换。声明`GetGlobalTransform`函数，返回关节的全局变换:\n\n    ```cpp\n        unsigned int Size();\n        void Resize(unsigned int newSize);\n        Transform& operator[](unsigned int index);\n        Transform GetGlobalTransform(unsigned int index);\n        unsigned int GetNumSteps();\n        void SetNumSteps(unsigned int numSteps);\n        float GetThreshold();\n        void SetThreshold(float value);\n    ```\n\n3.  声明`Solve`函数，将调用该函数求解 IK 链。提供了一个变换，但是只使用了变换的位置分量。如果解链，则`Solve`函数返回`true`，否则返回`false`:T4\n\n`mNumSteps`变量用于确保求解器不会陷入无限循环。不能保证末端执行器会达到目标。限制迭代次数有助于避免潜在的无限循环。在下一节中，您将开始实现电荷耦合器件求解器。\n\n## 实现 CCD 求解器\n\n创建一个新文件`CCDSolver.cpp`，在其中实现 CCD 求解器。按照以下步骤实现电荷耦合器件解算器:\n\n1.  定义默认构造函数，为步数和阈值赋值。使用小阈值，如`0.0001f`。使用`15`作为默认步数:\n\n    ```cpp\n    CCDSolver::CCDSolver() {\n        mNumSteps = 15;\n        mThreshold = 0.00001f;\n    }\n    ```\n\n2.  执行`Size`和`Resize`功能，控制 IK 链的大小，`[] operator`包含链中每个关节的值:\n\n    ```cpp\n    unsigned int CCDSolver::Size() {\n        return mIKChain.size();\n    }\n    void CCDSolver::Resize(unsigned int newSize) {\n        mIKChain.resize(newSize);\n    }\n    Transform& CCDSolver::operator[](unsigned int index) {\n        return mIKChain[index];\n    }\n    ```\n\n3.  为求解器包含的步数和阈值实现 getter 和 setter 函数:\n\n    ```cpp\n    unsigned int CCDSolver::GetNumSteps() {\n        return mNumSteps;\n    }\n    void CCDSolver::SetNumSteps(unsigned int numSteps) {\n        mNumSteps = numSteps;\n    }\n    float CCDSolver::GetThreshold() {\n        return mThreshold;\n    }\n    void CCDSolver::SetThreshold(float value) {\n        mThreshold = value;\n    }\n    ```\n\n4.  实现的`GetGlobalTransform`功能，可能看起来比较眼熟。它将指定关节的变换与其所有父关节的变换连接起来，并返回指定关节的全局变换:\n\n    ```cpp\n    Transform CCDSolver::GetGlobalTransform(unsigned int x) {\n        unsigned int size = (unsigned int)mIKChain.size();\n        Transform world = mIKChain[x];\n        for (int i = (int) x - 1; i >= 0; --i) {\n            world = combine(mIKChain[i], world);\n        }\n        return world;\n    }\n    ```\n\n5.  通过确保链的大小有效并存储最后一个元素的索引和目标位置的向量的局部变量来实现`Solve`功能:\n\n    ```cpp\n    bool CCDSolver::Solve(const Transform& target) {\n        unsigned int size = Size();\n        if (size == 0) { return false; }\n        unsigned int last = size - 1;\n        float thresholdSq = mThreshold * mThreshold;\n        vec3 goal = target.position;\n    ```\n\n6.  从`0`到`mNumSteps`循环执行正确的迭代次数。每次迭代时，获取末端执行器的位置，检查它是否足够接近目标。如果足够近，早点回来:\n\n    ```cpp\n        for (unsigned int i = 0; i < mNumSteps; ++ i) {\n            vec3 effector = GetGlobalTransform(last).position;\n            if (lenSq(goal - effector) < thresholdSq) {\n                return true;\n            }\n    ```\n\n7.  在每次迭代中，循环遍历整个 IK 链。在`size - 2`开始迭代；由于`size - 1`是最后一个元素，旋转最后一个元素对任何骨骼都没有影响:\n\n    ```cpp\n            for (int j = (int)size - 2; j >= 0; --j) {\n    ```\n\n8.  对于 IK 链中的每个关节，获取关节的世界变换。找到从关节位置到末端执行器位置的向量。找到从当前关节位置到目标位置的另一个向量:\n\n    ```cpp\n                effector=GetGlobalTransform(last).position;\n                Transform world = GetGlobalTransform(j);\n                vec3 position = world.position;\n                quat rotation = world.rotation;\n                vec3 toEffector = effector - position;\n                vec3 toGoal = goal - position;\n    ```\n\n9.  接下来，找到一个四元数，它从位置到效应器向量旋转到位置到目标向量。有一种边缘情况，其中指向效应器或目标的向量可以是零向量:\n\n    ```cpp\n                quat effectorToGoal;\n                if (lenSq(toGoal) > 0.00001f) {\n                    effectorToGoal = fromTo(toEffector, \n                                            toGoal);\n                }\n    ```\n\n10.  使用此向量将关节旋转到世界空间中的正确方向。通过关节先前世界旋转的逆旋转关节的世界空间方向，将四元数移回关节空间:\n\n    ```cpp\n                quat worldRotated =rotation * \n                                   effectorToGoal;\n                quat localRotate = worldRotated * \n                                   inverse(rotation);\n                mIKChain[j].rotation = localRotate * \n                                   mIKChain[j].rotation;\n    ```\n\n11.  当关节移动时，检查末端执行器在每次迭代中移动到目标的距离。如果足够接近，从函数中提前返回，值为`true` :\n\n    ```cpp\n                effector=GetGlobalTransform(last).position;\n                if (lenSq(goal - effector) < thresholdSq) {\n                    return true;\n                }\n             }\n        }\n    ```\n\n12.  如果没有达到目标，IK 链就无法解决，至少在指定的迭代次数内无法解决。只需返回`false`以表示该功能未能达到其目标:\n\n    ```cpp\n        return false;\n    } // End CCDSolver::Solve function\n    ```\n\n该电荷耦合器件解算器可用于求解具有一个原点和一个末端执行器的单个链。在单个链可以有多个末端效应器的情况下，有更高级的处理 IK 链的方法。然而，由于额外的实现复杂性，这些不太常见。在下一节，你将开始探索一种不同的 IK 算法，FABRIK。\n\n# 创建 FABRIK 解算器\n\n**FABRIK** ( **向前和向后到达反向运动学**)有一个更自然的，人形的看起来收敛。像电荷耦合器件一样，FABRIK 与具有基座、末端执行器和目标的 IK 链一起工作。与 CCD 不同的是，FABRIK 使用的是位置，而不是旋转。FABRIK 算法更容易理解，因为它可以只使用向量来实现。\n\n在许多方面，FABRIK 可以作为电荷耦合器件的替代产品。这两种算法都解决了同一个问题，但它们采用不同的方法来解决它。对于人形动画，FABRIK 往往收敛得更快，看起来更好，因此您可能会将其用作角色肢体的解算器。\n\n当涉及到人形钻机时，用位置而不是旋转来工作不会很好，人形钻机需要通过旋转关节来制作动画。这可以通过在算法中添加预处理和后处理步骤来解决。预处理步骤将把 IK 链中的所有变换转换成世界空间位置向量。后处理步骤将把这些向量转换成旋转数据。\n\nFABRIK 算法有两个部分。首先，从末端效应器向后迭代到底部。向后迭代时，将效应器移动到目标。接下来，移动每个骨骼，使它们相对于效应器；这将保持链条完好无损。接下来，将底座移回其原始位置，并相对于底座移动每个骨骼，使链条保持完整。\n\n在伪代码中，FABRIK 算法如下所示:\n\n```cpp\nvoid Iterate(const Transform& goal) {\n    startPosition = chain[0]\n    // Iterate backwards\n    chain[size - 1] = goal.position;\n    for (i = size - 2; i >= 0; --i) {\n        current = chain[i]\n        next = chain[i + 1]\n        direction = normalize(current - next)\n        offset = direction * length[i + 1]\n        chain[i] = next + offset\n    }\n    // Iterate forwards\n    chain[0] = startPosition\n    for (i  = 1; i < size; ++ i) {\n        current = chain[i]\n        prev = chain[i - 1]\n        direction = normalize(current - prev)\n        offset = direction * length[i]\n        chain[i] = prev + offset\n    }\n}\n```\n\n要可视化 FABRIK，请将末端效应器设置为目标所在的位置。找到从末端效应器到最后一个关节的向量。沿着该向量移动最后一个关节，保持其到末端执行器的距离。对每个关节重复上述步骤，直到到达底部。这将使底部接头移出位置。\n\n要进行正向迭代，请将基础放回原位。找到下一个关节的向量。将下一个关节放在这个向量上，保持它到底部的距离。一直重复这个步骤:\n\n![Figure 13.2 Visualizing the FABRIK algorithm ](img/Figure_13.2_B16191.jpg)\n\n图 13.2:可视化 FABRIK 算法\n\nFABRIK 和 CCD 都将尝试求解一个 IK 链，但它们在目标上的收敛方式不同。CCD 倾向于卷曲，而 FABRIK 倾向于拉伸。FABRIK 通常会为人形动画生成更自然的结果。在下一节中，您将开始声明`FABRIKSolver`类，然后是该类的实现。\n\n## 声明 FABRIK 求解器\n\nFABRIK 求解器将需要更多内存来运行，因为它必须将局部联合变换转换为全局位置。该算法可以分解成几个步骤，这些步骤都可以作为受保护的辅助函数来实现。\n\n创建新文件，`FABRIKSolver.h`。该文件将用于声明`FABRIKSolver`类。按照以下步骤申报`FABRIKSolver`类:\n\n1.  首先声明`FABRIKSolver`类，它需要跟踪 IK 链、最大步数和一些距离阈值。声明一个世界空间位置向量和一个关节长度向量。这些向量是需要的，因为 FABRIK 算法不考虑旋转:\n\n    ```cpp\n    class FABRIKSolver {\n    protected:\n        std::vector<Transform> mIKChain;\n        unsigned int mNumSteps;\n        float mThreshold;\n        std::vector<vec3> mWorldChain;\n        std::vector<float> mLengths;\n    ```\n\n2.  声明助手功能，将 IK 链复制到世界位置向量，向前迭代，向后迭代，并将最终世界位置复制回 IK 链:\n\n    ```cpp\n    protected:\n        void IKChainToWorld();\n        void IterateForward(const vec3& goal);\n        void IterateBackward(const vec3& base);\n        void WorldToIKChain();\n    ```\n\n3.  为链的大小、求解链的迭代次数以及末端关节需要离目标多远的ε值声明一个默认的构造函数、getter 和 setter 函数:\n\n    ```cpp\n    public:\n        FABRIKSolver();\n        unsigned int Size();\n        void Resize(unsigned int newSize);\n        unsigned int GetNumSteps();\n        void SetNumSteps(unsigned int numSteps);\n        float GetThreshold();\n        void SetThreshold(float value);\n    ```\n\n4.  为存储在 IK 链中的局部变换声明 getter 和 setter 函数。声明一个函数来检索关节的全局变换。最后，声明`Solve`函数，在给定目标时求解 IK 链:\n\n    ```cpp\n        Transform GetLocalTransform(unsigned int index);\n        void SetLocalTransform(unsigned int index, \n                               const Transform& t);\n        Transform GetGlobalTransform(unsigned int index);\n        bool Solve(const Transform& target);\n    };\n    ```\n\nFABRIK 算法实现起来比电荷耦合器件算法复杂一点，但是步骤更容易分解成函数。在下一节中，您将开始实现`FABRIKSolver`类的功能。\n\n## 实现 FABRIK 求解器\n\nFABRIK 算法适用于世界空间位置。这意味着，对于每次迭代，IK 链需要将局部关节变换转换为世界位置并存储结果。链求解后，世界位置向量需要转换回相对偏移，并存储回 IK 链中。\n\n新建一个文件，`FABRIKSolver.cpp`；`FABRIKSolver`类将在这个文件中实现。按照以下步骤实施`FABRIKSolver`课程:\n\n1.  实现`FABRIKSolver`类的构造函数。需要将步数和阈值设置为默认值:\n\n    ```cpp\n    FABRIKSolver::FABRIKSolver() {\n        mNumSteps = 15;\n        mThreshold = 0.00001f;\n    }\n    ```\n\n2.  为步数和阈值实现简单的 getter 和 setter 函数:\n\n    ```cpp\n    unsigned int FABRIKSolver::GetNumSteps() {\n        return mNumSteps;\n    }\n    void FABRIKSolver::SetNumSteps(unsigned int numSteps) {\n        mNumSteps = numSteps;\n    }\n    float FABRIKSolver::GetThreshold() {\n        return mThreshold;\n    }\n    void FABRIKSolver::SetThreshold(float value) {\n        mThreshold = value;\n    }\n    ```\n\n3.  为链的大小实现一个获取和设置函数。setter 函数需要设置链的大小、世界链和长度向量:\n\n    ```cpp\n    unsigned int FABRIKSolver::Size() {\n        return mIKChain.size();\n    }\n    void FABRIKSolver::Resize(unsigned int newSize) {\n        mIKChain.resize(newSize);\n        mWorldChain.resize(newSize);\n        mLengths.resize(newSize);\n    }\n    ```\n\n4.  实现获取和设置 IK 链中元素的局部变换的方法:\n\n    ```cpp\n    Transform FABRIKSolver::GetLocalTransform(\n                            unsigned int index) {\n        return mIKChain[index];\n    }\n    void FABRIKSolver::SetLocalTransform(unsigned int index,\n                                       const Transform& t) {\n        mIKChain[index] = t;\n    }\n    ```\n\n5.  实现 getter 函数来检索全局转换并将所有转换连接到根:\n\n    ```cpp\n    Transform FABRIKSolver::GetGlobalTransform(\n                            unsigned int index) {\n        unsigned int size = (unsigned int)mIKChain.size();\n        Transform world = mIKChain[index];\n        for (int i = (int)index - 1; i >= 0; --i) {\n            world = combine(mIKChain[i], world);\n        }\n        return world;\n    }\n    ```\n\n6.  执行`IKChainToWorld`功能，将 IK 链复制到世界变换向量中，并记录线段长度。长度数组存储关节与其父关节的距离。这意味着根关节将始终包含长度`0`。对于非根关节，`i`指数处的距离是关节`i`和`i–1`之间的距离:\n\n    ```cpp\n    void FABRIKSolver::IKChainToWorld() {\n        unsigned int size = Size();\n        for (unsigned int i = 0; i < size; ++ i) {\n            Transform world = GetGlobalTransform(i);\n            mWorldChain[i] = world.position;\n            if (i >= 1) {\n                vec3 prev = mWorldChain[i - 1];\n                mLengths[i] = len(world.position - prev);\n            }\n        }\n        if (size > 0) {\n            mLengths[0] = 0.0f;\n        }\n    }\n    ```\n\n7.  接下来执行`WorldToIKChain`功能，将世界位置 IK 链转换回局部空间变换。穿过所有关节。对于每个关节，找到当前关节和下一个关节的世界空间变换。缓存当前关节的世界空间位置和旋转:\n\n    ```cpp\n    void FABRIKSolver::WorldToIKChain() {\n        unsigned int size = Size();\n        if (size == 0) { return; }\n        for (unsigned int i = 0; i < size - 1; ++ i) {\n            Transform world = GetGlobalTransform(i);\n            Transform next = GetGlobalTransform(i + 1);\n            vec3 position = world.position;\n            quat rotation = world.rotation;\n    ```\n\n8.  创建一个从当前关节指向下一个关节的向量。这是当前节点和下一个节点之间的旋转:\n\n    ```cpp\n            vec3 toNext = next.position - position;\n            toNext = inverse(rotation) * toNext;\n    ```\n\n9.  构建一个从下一个关节的世界空间 IK 链指向当前位置的向量。这是当前节点和下一个节点的期望位置之间的旋转:\n\n    ```cpp\n            vec3 toDesired = mWorldChain[i + 1] - position;\n            toDesired = inverse(rotation) * toDesired;\n    ```\n\n10.  使用`fromTo`四元数函数对齐这两个向量。将最终增量旋转应用于当前关节的 IK 链旋转:\n\n    ```cpp\n            quat delta = fromTo(toNext, toDesired);\n            mIKChain[i].rotation = delta * \n                                   mIKChain[i].rotation;\n        }\n    }\n    ```\n\n11.  接下来，实现`IterateBackward`函数，该函数将链中的最后一个元素设置为目标。这将打破 IK 链。使用存储的距离调整所有其他关节，使链条保持完整。此功能执行后，末端效应器始终位于目标位置，初始关节可能不再位于底部:\n\n    ```cpp\n    void FABRIKSolver::IterateBackward(const vec3& goal) {\n        int size = (int)Size();\n        if (size > 0) {\n            mWorldChain[size - 1] = goal;\n        }\n        for (int i = size - 2; i >= 0; --i) {\n            vec3 direction = normalized(mWorldChain[i] - \n                                        mWorldChain[i + 1]);\n            vec3 offset = direction * mLengths[i + 1];\n            mWorldChain[i] = mWorldChain[i + 1] + offset;\n        }\n    }\n    ```\n\n12.  实现`IterateForward`功能。此函数重新排列 IK 链，使第一个链接从链的原点开始。该功能需要将初始关节设置为在底部，并迭代所有其他关节，调整它们以保持 IK 链完整。该函数执行后，如果链是可解的并且有足够的迭代次数，末端执行器可能在目标位置:\n\n    ```cpp\n    void FABRIKSolver::IterateForward(const vec3& base) {\n        unsigned int size = Size();\n        if (size > 0) {\n            mWorldChain[0] = base;\n        }\n        for (int i = 1; i < size; ++ i) {\n            vec3 direction = normalized(mWorldChain[i] - \n                                        mWorldChain[i - 1]);\n            vec3 offset = direction * mLengths[i];\n            mWorldChain[i] = mWorldChain[i - 1] + offset;\n        }\n    }\n    ```\n\n13.  通过将 IK 链复制到世界位置向量中并填写长度向量，开始执行`Solve`功能。这可以通过`IKChainToWorld`助手功能来完成。缓存基地和球门位置:\n\n    ```cpp\n    bool FABRIKSolver::Solve(const Transform& target) {\n        unsigned int size = Size();\n        if (size == 0) { return false; }\n        unsigned int last = size - 1;\n        float thresholdSq = mThreshold * mThreshold;\n\n        IKChainToWorld();\n        vec3 goal = target.position;\n        vec3 base = mWorldChain[0];\n    ```\n\n14.  从`0`迭代到`mNumSteps`。对于每次迭代，检查目标和末端效应器是否足够接近，以便求解链。如果是，使用`WorldToIKChain`助手功能将世界位置复制回链中，并提前返回。如果它们不够接近，通过调用`IterateBackward`和`IterateForward`方法进行迭代:\n\n    ```cpp\n        for (unsigned int i = 0; i < mNumSteps; ++ i) {\n            vec3 effector = mWorldChain[last];\n            if (lenSq(goal - effector) < thresholdSq) {\n                WorldToIKChain();\n                return true;\n            }\n            IterateBackward(goal);\n            IterateForward(base);\n        }\n    ```\n\n15.  迭代循环后，将世界位置向量复制回 IK 链，而不管求解器是否能够求解该链。最后检查一次末端效应器是否达到目标，并返回适当的布尔值:\n\n    ```cpp\n        WorldToIKChain();\n        vec3 effector = GetGlobalTransform(last).position;\n        if (lenSq(goal - effector) < thresholdSq) {\n            return true;\n        }\n        return false;\n    }\n    ```\n\nFABRIK 算法之所以受欢迎，是因为它倾向于快速收敛到最终目标，结果对于人形角色来说看起来很好，并且该算法易于实现。在下一节中，您将学习如何向 FABRIK 或 CCD 求解器添加约束。\n\n# 实施约束\n\nCCD 和 FABRIK 解算器都能产生好的结果，但都不能产生可预测的结果。在本节中，您将了解什么是约束，可以在哪里应用 IK 解算器约束，以及如何应用约束。这将让你建立更真实的 IK 解算器。\n\n考虑一个应该代表一条腿的 IK 链。你会想要确保每个关节的运动是可预测的，例如，膝盖可能不应该向前弯曲。\n\n这就是约束有用的地方。膝关节是一个铰链；如果应用了铰链约束，腿部 IK 链将看起来更真实。使用约束，可以为 IK 链中的每个关节设置规则。\n\n以下步骤将向您展示如何在 CCD 和 FABRIK 解算器中应用约束:\n\n1.  约束可以应用于 CCD 和 FABRIK 解算器，并且它们必须在每次迭代后应用。对于 CCD，这意味着在这里插入一位代码:\n\n    ```cpp\n    bool CCDSolver::Solve(const vec3& goal) {\n        // Local variables and size check\n        for (unsigned int i = 0; i < mNumSteps; ++ i) {\n            // Check if we've reached the goal\n            for (int j = (int)size - 2; j >= 0; --j) {\n               // Iteration logic\n     // -> APPLY CONSTRAINTS HERE!\n                effector = GetGlobalTransform(last).position;\n                if (lenSq(goal - effector) < thresholdSq) {\n                    return true;\n                }\n             }\n        }\n        // Last goal check\n    }\n    ```\n\n2.  将约束应用于 FABRIK 解算器更加复杂。约束应用于每次迭代，每次迭代都需要在世界位置链和 IK 链之间转换 IK 链。将数据复制到变换链后，每次迭代都应用约束:\n\n    ```cpp\n    bool FABRIKSolver::Solve(const vec3& goal) {\n        // Local variables and size check\n        IKChainToWorld();\n        vec3 base = mWorldChain[0];\n        for (unsigned int i = 0; i < mNumSteps; ++ i) {\n            // Check if we've reached the goal\n            IterateBackward(goal);\n            IterateForward(base);\n     WorldToIKChain();//NEW, NEEDED FOR CONSTRAINTS\n     // -> APPLY CONSTRAINTS HERE!\n     IKChainToWorld();//NEW, NEEDED FOR CONSTRAINTS\n        }\n        // Last goal check\n    }\n    ```\n\n`Solve`函数是虚函数的原因是，您可以将每个`IKChain`类扩展到特定类型的链中，如`LegIKChain`或`ArmIKChain`，并将约束代码直接添加到求解方法中。在以下各节中，您将探索常见的约束类型。\n\n## 球窝约束\n\n球窝关节像肩关节一样工作。关节可以在所有三个轴上旋转，但是有一个角度约束阻止它自由旋转。*图 13.3* 直观显示了球窝约束的外观:\n\n![Figure 13.3 A ball and socket constraint visualized ](img/Figure_13.3_B16191.jpg)\n\n图 13.3:球窝约束可视化\n\n要建立球窝约束，需要知道当前关节及其父关节的旋转。你可以从这些四元数中构造正向向量，并检查正向向量的角度。如果角度大于提供的极限，则需要调整旋转。\n\n要限制旋转，找到旋转轴。两个向前方向的叉积垂直于两个方向；这是旋转轴。创建一个四元数，将沿该轴的角度限制带入当前关节的局部空间，并将该四元数设置为关节的旋转:\n\n```cpp\nvoid ApplyBallSocketConstraint(int i, float limit) { \n    quat parentRot = i == 0 ? mOffset.rotation : \n                     GetWorldTransform(i - 1).rotation;\n    quat thisRot = GetWorldTransform(i).rotation;\n    vec3 parentDir = parentRot * vec3(0, 0, 1);\n    vec3 thisDir = thisRot * vec3(0, 0, 1);\n    float angle = ::angle(parentDir, thisDir);\n    if (angle > limit * QUAT_DEG2RAD) {\n        vec3 correction = cross(parentDir, thisDir);\n        quat worldSpaceRotation = parentRot * \n            angleAxis(limit * QUAT_DEG2RAD, correction);\n        mChain[i].rotation = worldSpaceRotation * \n                             inverse(parentRot);\n    }\n}\n```\n\n球窝约束通常应用于角色的髋关节或肩关节。这些也往往是肢体 IK 链的根关节。在下一节中，您将探索另一种类型的约束，即铰链约束。\n\n## 铰链约束\n\n铰链约束类似于肘或膝。它只允许在一个特定的轴上旋转。*图 13.4* 从视觉上展示了铰链接头的外观:\n\n![Figure 13.4 A hinge constraint visualized ](img/Figure_13.4_B16191.jpg)\n\n图 13.4:可视化的铰链约束\n\n要实现铰链约束，需要知道当前关节和父关节的世界空间旋转。将轴法线乘以两个旋转四元数，找到两者之间的四元数；这是将关节约束到轴上所需的旋转量。将此旋转带回关节空间并应用旋转:\n\n```cpp\nvoid ApplyHingeSocketConstraint(int i, vec3 axis) { \n    Transform joint = GetWorldTransform(i);\n    Transform parent = GetWorldTransform(i - 1);\n    vec3 currentHinge = joint.rotation * axis;\n    vec3 desiredHinge = parent.rotation * axis;\n    mChain[i].rotation = mChain[i].rotation * \n                         fromToRotation(currentHinge, \n                                        desiredHinge);\n}\n```\n\n铰链约束常用于肘关节或膝关节。在下一节中，您将探讨如何使用 IK 将角色的脚与地面对齐。\n\n# 使用 IK 将角色的脚与地面对齐\n\n在本节中，您将学习如何使用 IK 来修改动画，使其看起来更加正确。具体来说，您将学习如何使用 IK 在不平坦的表面上行走时阻止角色的脚穿过地面。\n\n现在，您可以使用 CCD 或 FABRIK 求解 IK 链，让我们探索如何使用这些解算器。IK 有两种常见的用途，即定位手或定位脚。在本节中，您将探索当角色行走时，如何将角色的脚夹在地上。\n\n要解决夹脚问题，您可以对照当前全局位置检查脚的最后一个全局位置。如果脚的运动碰到路上的任何东西，把脚钉在地上。即使是最琐碎的解决方案也有边缘情况:如果上升运动太远会发生什么？在动画循环的哪一点，我们可以在固定位置和非固定位置之间进行插值？\n\n为了便于实施，本章的接地箝位策略将保持简单。首先，检查脚是否与上面的任何东西碰撞，例如，穿过地形。为此，从角色的臀部到脚踝投射一条光线。\n\n如果射线击中任何东西，击中点将是腿部 IK 链的目标。如果光线没有击中任何东西，角色脚踝的当前位置将是腿部 IK 链的目标。接下来，做同样的光线投射，但不要停留在人物的脚踝处；继续走。\n\n如果这条射线击中任何东西，击中点就是未来的 IK 目标。如果光线没有击中任何东西，将未来的 IK 目标设置为当前的 IK 目标。现在有两个目标，一个是自由运动的，一个是固定在地上的。\n\n如果使用当前目标，角色的脚可能会突然落地。如果你用未来的目标，角色不会走路——它只会在地上拖着脚。相反，你必须在两个目标之间插入一些值。\n\n插值应该来自动画本身。当角色的脚向下时，应该使用当前目标；当它上升时，未来的目标应该被使用。当角色的脚被上下放置时，目标位置应该是`lerp`。\n\n知道了 IK 目标，IK 解算器就可以计算出如何弯曲角色的腿。一旦腿部关节在世界空间中，我们调整脚的头部，使其始终在地形上，遵循一些类似于解决腿部的步骤。\n\n在以下部分中，您将更详细地探索这里描述的每个步骤。然而，这里有一点蹊跷。所需的大多数值是特定于用于渲染的模型的；不同的角色需要不同的值。\n\n## 寻找脚下目标\n\n从角色臀部下方一点到脚踝下方一点直射一条光线。这个光线投射应该沿着脚踝的位置一直向下。然而，光线应该在击球下开始多远，以及应该在脚踝下走多远都是特定于模型的:\n\n![Figure 13.5 Ray cast to find the foot goal ](img/Figure_13.5_B16191.jpg)\n\n图 13.5:光线投射找足球门\n\n不管命中点有多远，都要记录下这个光线投射的结果。该点将被视为 IK 目标，始终被夹在地面上。检查光线是否击中了其原点和脚踝底部之间的任何东西。如果是的话，那将是脚踝的进球。如果没有，脚踝的目标将是脚踝的位置。\n\n重要的是要记住角色的脚踝是被定位的，而不是它的脚底。因此，球门点需要向上移动脚踝到地板的距离:\n\n![Figure 13.6 Offset to position the character's ankle ](img/Figure_13.6_B16191.jpg)\n\n图 13.6:角色脚踝位置的偏移\n\n这些脚部目标将控制 IK 系统如何覆盖动画。行走时，如果脚的运动没有受到阻碍，IK 系统应该不会被注意到。在下一节中，您将学习如何控制脚在动画和固定目标点之间的插值。\n\n## 内插脚进球\n\n要在当前和未来 IK 目标之间进行插值，您需要了解当前正在播放的动画剪辑。具体来说，你需要知道腿处于什么阶段；它是被停飞、被举起、被悬挂还是被放置？对这些信息进行编码的一种常见方法是使用标量曲线。\n\n这个想法是创建两条标量曲线，一条用于左腿，一条用于右腿。这些曲线对应于当前步骤的步幅。例如，当左脚离开地面时，左标量曲线的值需要为 0。如果左脚在地面上，左曲线的值需要为 1。曲线如下所示:\n\n![Figure 13.7 Walk cycle stride expressed as scalar curves ](img/Figure_13.7_B16191.jpg)\n\n图 13.7:步行周期步幅表示为标量曲线\n\n根据当前标准化回放时间对这些曲线进行采样。结果值将介于 0 和 1 之间。将非 IK 调整动画和 IK 调整动画混合在一起时，使用此 0 到 1 的值作为混合权重。该曲线通常由眼睛使用曲线编辑器创作。该曲线特定于当前正在播放的动画。\n\n在下一节中，您将探索如何调整 IK 角色的垂直放置，以避免肢体过度伸展。\n\n## 垂直字符放置\n\n接下来，角色需要垂直放置，这样看起来才好看。如果角色被放得太高，它会以腿处于过度伸展状态而结束。过低，IK 系统会过度弯曲腿部:\n\n![Figure 13.8 IK hyperextension compared to sampled animation ](img/Figure_13.8_B16191.jpg)\n\n图 13.8:与采样动画相比，IK 过度伸展\n\n角色的定位方式与它的建模方式有关。如果角色是在假设(0，0，0)是地面中心点的情况下建模的，您可以将它放在下面的表面上，并将其稍微沉入表面。\n\n角色需要稍微沉入表面，以允许 IK 系统做一些工作，并避免过度伸展。这就提出了一个问题:角色的脚需要对准的表面是什么？对准位置可以来自碰撞/物理系统，或者在更简单的例子中，只是从角色直接向下的光线投射。\n\n碰撞面和可视面不一样。考虑一个楼梯:碰撞几何体通常是一个斜坡。显示的几何形状看起来像一个真正的楼梯。在这种情况下，角色的位置应该相对于碰撞几何体，但是 IK 目标应该相对于视觉几何体定位。\n\n如果只有一种几何图形用于碰撞和视觉效果，会怎么样？在这种情况下，将角色放在夹住的任意一个 IK 目标上，以较低者为准。这将确保在没有过度伸展的情况下始终可以到达地面。\n\n## 传球\n\n是时候解决腿 IK 链了。在此之前，将关节从动画姿势复制到 IK 解算器中。对于每条腿，将髋关节的全局变换复制到 IK 解算器的根部。将膝盖的局部变换复制到关节 1，将脚踝的局部变换复制到关节 2。然后，运行 IK 解算器。解算器会将角色的脚放在固定在地面上的目标点上。\n\n## 脚部对正\n\n夹紧的脚动画在这一点上是平滑的，脚将不再夹在地面内部。但是只有角色的腿看起来是正确的，而不是脚。看一看非平坦表面上的角色的脚——仍然有相当多的剪裁发生:\n\n![Figure 13.9 The leg is clamped to the ground, but the foot is oriented wrong ](img/Figure_13.9_B16191.jpg)\n\n图 13.9:腿被夹在地上，但脚的方向不对\n\n要解决这个问题，创建一个脚趾射线。脚趾射线将位于角色的踝关节处，沿角色的前向轴有一段距离。这将确保脚趾目标总是向前看，即使在动画中，脚趾指向下方。调整脚趾射线的垂直位置，从膝盖上方拍摄到脚趾下方一点:\n\n![Figure 13.10 To cast the offset forward, even if the toe points down ](img/Figure_13.10_B16191.jpg)\n\n图 13.10:向前投射偏移，即使脚趾指向下方\n\n脚趾的位置类似于腿的位置。找到一个目标，即当前脚趾夹在地上的位置。通过动画的当前归一化时间，在地面夹紧目标和活动动画目标之间进行插值。\n\n这个脚趾目标将用于旋转脚。找到一个从脚踝到当前脚趾位置的向量。找到一个从脚踝到球门脚趾位置的向量。创建一个在这两个向量之间旋转的四元数。用这个四元数旋转脚踝。\n\n在本节中，您学习了如何找到脚目标，在它们之间进行插值，并使用这些目标和 IK 系统将角色的脚与地面对齐。地面对齐只是 IK 解算器的用例之一。类似的系统可以用手臂抓东西，也可以用全身来创造一个布娃娃系统。\n\n# 总结\n\n在本章中，您实现了 CCD 和 FABRIK IK 解算器。两个解算器都可以求解 IK 链，但它们的收敛方式不同。哪种算法效果更好在很大程度上取决于上下文。\n\n您还学习了如何使用约束来限制特定关节的运动范围。在适当的约束条件下，IK 系统会修改当前动画，使其与环境交互。您在本章的脚踏实地部分探讨了如何实现这一点。\n\n在本书的可下载内容中，本章有 4 个示例。`Sample00`包含至此的代码。`Sample01`演示如何使用 CCD 解算器，`Sample02`演示如何使用 FABRIK 解算器。`Sample03`演示角色沿着路径行走时的夹脚和地面对齐。\n\n在下一章中，您将学习对偶四元数如何用于蒙皮。当网格弯曲或旋转时，双四元数蒙皮比线性混合蒙皮更好地保持网格的体积。\n\n# 进一步阅读\n\n除了 FABRIK 和 CCD 之外，IK 链有时会通过解析或雅可比矩阵求解:\n\n*   有关分析 IK 解算器的更多信息，请访问。\n*   完整的雅可比解算器实现包含在*游戏编程宝石 4* 中。"
  },
  {
    "path": "docs/handson-cpp-game-ani-prog/14.md",
    "content": "# 十四、使用对偶四元数蒙皮\n\n当前的蒙皮实现在蒙皮权重之间线性混合，这被称为**线性混合蒙皮(LBS)** 或者有时是**线性蒙皮混合**。线性混合蒙皮不会保留模型的体积，这会引入蒙皮构件。可视化该工件的一个简单方法是将矩形的一端扭曲 180 度，如下图所示:\n\n![Figure 14.1: Comparing linear blended and dual quaternion skinning ](img/Figure_14.1_B16191.jpg)\n\n图 14.1:比较线性混合和对偶四元数蒙皮\n\n线性蒙皮混合的替代方法是**对偶四元数蒙皮混合**。当使用双四元数时，模型的体积保持不变。在本章中，您将实现对偶四元数网格蒙皮。到本章结束时，您应该能够使用双四元数来皮肤动画角色。本章涵盖以下主题:\n\n*   引入对偶四元数\n*   实现对偶四元数\n*   双四元数蒙皮\n*   了解如何使用对偶四元数蒙皮\n\n# 引入对偶四元数\n\n对偶四元数将线性和旋转变换结合成一个变量。这个变量可以被插值、转换和连接。对偶四元数可以用两个四元数或八个浮点数来表示。\n\n双数就像复数。复数有实部和虚部，对偶数有实部和对偶部。假设![](img/Formula_14_001.png)是双算符，一个双数可以表示为![](img/Formula_14_002.png)，其中![](img/Formula_14_003.png)和![](img/Formula_14_004.png)。\n\n对偶数的运算是以虚数的形式进行的，其中对偶分量和实分量必须分别作用。例如，对偶四元数加法可以用以下方式表示:\n\n![](img/Formula_14_005.png)\n\n请注意真实部分和双重部分是如何独立添加的。\n\n重要说明\n\n如果你对对偶四元数背后的更正式的数学感兴趣，请查看*本·肯赖特的《对偶四元数入门指南》*，网址为 https://cs . gmu . edu/~ JM lien/teaching/cs 451/uploads/Main/对偶四元数 .pdf\n\n对偶四元数只是对偶数的扩展。实部和对偶部分用四元数而不是标量值来表示，大多数数学运算只是起作用。在下一节中，您将开始在代码中实现双四元数。\n\n# 实现对偶四元数\n\n在本节中，您将在代码中实现对偶四元数。到本节结束时，您将实现一个对偶四元数结构，以及使用对偶四元数来蒙皮网格所需的所有数学函数。\n\n对偶四元数需要实现为结构，类似于变换或矩阵。创建两个新文件，`DualQuaternion.h`和`DualQuaternion.cpp`。您将在这些文件中实现与对偶四元数相关的数学。\n\n首先声明一个`DualQuaternion`结构。这种结构将允许您以两个四元数或八个数字的浮点数组的形式访问对偶四元数结构中的数据。构造函数应该将对偶四元数设置为恒等式。单位对偶四元数的实部有一个单位四元数，对偶部有一个零四元数，如下面的代码块所示:\n\n```cpp\nstruct DualQuaternion {\n    union {\n        struct {\n            quat real;\n            quat dual;\n        };\n        float v[8];\n    };\n    inline DualQuaternion() : real(0, 0, 0, 1), dual(0, 0, 0, 0) { }\n    inline DualQuaternion(const quat& r, const quat& d) :\n        real(r), dual(d) { }\n};\n```\n\n对偶四元数的实部保存旋转数据，对偶部保存位置数据。双四元数不处理缩放。在下一节中，您将声明并实现常见的对偶四元数运算，如加法和乘法。\n\n在*实现对偶四元数运算*小节中，您将实现简单的对偶四元数运算符，如加法、缩放、乘法和比较运算符。在*测量、归一化和反转对偶四元数*部分，您将学习如何实现对偶四元数的点积，如何测量对偶四元数，以及如何反转它。在*转换变换和对偶四元数*部分，您将学习如何在`DualQuaternion`和`Transform`结构之间转换。最后，在*变换向量和点*部分，您将学习如何用对偶四元数变换向量和点，就像变换或矩阵一样。\n\n## 实现对偶四元数运算\n\n您将需要定义一些数学运算符来处理对偶四元数。这些函数是加法、标量乘法、对偶四元数乘法和等式比较运算符。\n\n两个对偶四元数通过乘法结合。与矩阵和四元数不同，对偶四元数从左向右相乘。按照以下步骤实现对偶四元数运算:\n\n1.  在`DualQuaternion.h`中声明加法、标量乘法、对偶四元数乘法、等式比较运算符，如下:\n\n    ```cpp\n    DualQuaternion operator+(const DualQuaternion &l, \n                             const DualQuaternion &r);\n    DualQuaternion operator*(const DualQuaternion &dq, \n                             float f);\n    // Multiplication order is left to right\n    // This is the OPPOSITE of matrices and quaternions\n    DualQuaternion operator*(const DualQuaternion &l, \n                             const DualQuaternion &r);\n    bool operator==(const DualQuaternion &l, \n                    const DualQuaternion &r);\n    bool operator!=(const DualQuaternion &l, \n                    const DualQuaternion &r);\n    ```\n\n2.  实现加法、标量乘法和比较函数。它们都是组件式操作。对对偶四元数的实分量和对偶分量分别执行分量操作，如下所示:\n\n    ```cpp\n    DualQuaternion operator+(const DualQuaternion &l,\n                            const DualQuaternion &r) {\n       return DualQuaternion(l.real+r.real,l.dual+r.dual);\n    }\n    DualQuaternion operator*(const DualQuaternion &dq, \n                             float f) {\n        return DualQuaternion(dq.real * f, dq.dual * f);\n    }\n    bool operator==(const DualQuaternion &l, \n                    const DualQuaternion &r) {\n        return l.real == r.real && l.dual == r.dual;\n    }\n    bool operator!=(const DualQuaternion &l, \n                    const DualQuaternion &r) {\n        return l.real != r.real || l.dual != r.dual;\n    }\n    ```\n\n3.  首先确保两个对偶四元数都归一化，开始实现对偶四元数乘法，如下所示:\n\n    ```cpp\n    // Remember, multiplication order is left to right. \n    // This is the opposite of matrix and quaternion \n    // multiplication order\n    DualQuaternion operator*(const DualQuaternion &l, const DualQuaternion &r) {\n        DualQuaternion lhs = normalized(l);\n        DualQuaternion rhs = normalized(r);\n    ```\n\n4.  将两个规范化四元数的实部组合在一起。因为![](img/Formula_14_006.png)必须等于`0`，所以涉及的双重部分更多。通过将两个四元数的对偶和实边相乘并相加结果来满足这个要求，如下所示:\n\n    ```cpp\n        return DualQuaternion(lhs.real * rhs.real, \n                              lhs.real * rhs.dual + \n                              lhs.dual * rhs.real);\n    }\n    ```\n\n通常的对偶四元数算子在很大程度上是直观的，但是对偶四元数的乘法顺序违反惯例，使得它们有点难以处理。在下一节中，您将了解点积和对偶四元数的正常实现。\n\n## 对偶四元数的测量、归一化和反相\n\n点积衡量两个对偶四元数有多相似。对偶四元数点积的规则与向量和四元数点积相同。点积的结果是具有以下属性的标量值:\n\n*   如果对偶四元数指向同一个方向，则为正。\n*   如果对偶四元数指向相反的方向，则为负。\n*   如果对偶四元数是垂直的，则为零。\n\n非单位对偶四元数会在对偶四元数表示的变换中引入不必要的偏斜。为了规范化对偶四元数，实数部分和对偶部分都需要除以实数部分的长度。\n\n规范化对偶四元数就像规范化正则四元数一样，主要对实部进行操作。首先求出对偶四元数实部的长度，然后将实部和对偶部分都除以长度。这将实数部分和对偶部分归一化为实数部分的长度。\n\n由于点积只考虑方向，所以不使用对偶四元数的虚部。求两个对偶四元数实部的点积。对偶四元数`conjugate`运算是四元数共轭的扩展，分别求实部和对偶部的共轭。\n\n按照以下步骤实现`dot product`、`invert`和`normalize`功能:\n\n1.  在`DualQuaternion.h`中声明对偶四元数点积、共轭和归一化函数，如下:\n\n    ```cpp\n    float dot(const DualQuaternion& l, \n              const DualQuaternion& r);\n    DualQuaternion conjugate(const DualQuaternion& dq);\n    DualQuaternion normalized(const DualQuaternion& dq);\n    void normalize(DualQuaternion& dq);\n    ```\n\n2.  求两个对偶四元数实部的四元数点积并返回结果，实现点积，如下所示:\n\n    ```cpp\n    float dot(const DualQuaternion& l, \n              const DualQuaternion& r) {\n        return dot(l.real, r.real);\n    }\n    ```\n\n3.  分别取实部和对偶部的四元数共轭，实现`conjugate`函数，如下:\n\n    ```cpp\n    DualQuaternion conjugate(const DualQuaternion& dq) {\n        return DualQuaternion(conjugate(dq.real), \n                              conjugate(dq.dual));\n    }\n    ```\n\n4.  通过找到实部的长度并通过长度的倒数缩放对偶和实部来实现`normalized`功能，如下所示:\n\n    ```cpp\n    DualQuaternion normalized(const DualQuaternion& dq) {\n        float magSq = dot(dq.real, dq.real);\n        if (magSq  < 0.000001f) {\n            return DualQuaternion();\n        }\n        float invMag = 1.0f / sqrtf(magSq);\n        return DualQuaternion(dq.real * invMag, \n                              dq.dual * invMag);\n    }\n    ```\n\n5.  实现`normalize`功能。与`normalized`不同的是，`normalize`函数采用了一个对偶的四元数参考，并对其进行了适当的归一化，如下所示:\n\n    ```cpp\n    void normalize(DualQuaternion& dq) {\n        float magSq = dot(dq.real, dq.real);\n        if (magSq  < 0.000001f) {\n            return;\n        }\n        float invMag = 1.0f / sqrtf(magSq);\n        dq.real = dq.real * invMag;\n        dq.dual = dq.dual * invMag;\n    }\n    ```\n\n如果对偶四元数随时间变化，由于浮点误差，它可能会变得不正常。如果对偶四元数实部的长度不是`1`，则需要对对偶四元数进行归一化。你应该检查平方长度是否为`1`，而不是对照一检查长度，这将涉及平方根运算，这样，运算速度会快得多。在下一节中，您将学习如何在变换和对偶四元数之间进行转换。\n\n## 转换变换和对偶四元数\n\n双四元数保存类似于变换的数据，没有缩放组件。在两者之间转换是可能的，但是比例会丢失。\n\n将变换转换为对偶四元数时，对偶四元数的实部映射到变换的旋转。要计算对偶部分，请根据变换的平移向量创建一个纯四元数。然后，将这个纯四元数乘以变换的旋转。结果需要减半——除以 2 或乘以 0.5。\n\n将对偶四元数转换为变换时，变换旋转仍然映射对偶四元数的实部。要找到位置，将对偶部分乘以 2，并将结果与变换旋转的倒数相结合。这产生了一个纯四元数。这个纯四元数的向量部分是新的位置。\n\n按照以下步骤实现在`Transform`和`DualQuaternion`对象之间转换的代码:\n\n1.  在`DualQuaternion.h`中声明将对偶四元数转换为变换并将变换转换为对偶四元数的函数，如下所示:\n\n    ```cpp\n    DualQuaternion transformToDualQuat(const Transform& t);\n    Transform dualQuatToTransform(const DualQuaternion& dq);\n    ```\n\n2.  实现`transformToDualQuat`功能。得到的对偶四元数不需要归一化。这个的代码可以在下面的代码片段中看到:\n\n    ```cpp\n    DualQuaternion transformToDualQuat(const Transform& t) {\n        quat d(t.position.x, t.position.y, t.position.z, 0);\n        quat qr = t.rotation;\n        quat qd = qr * d * 0.5f;\n        return DualQuaternion(qr, qd);\n    }\n    ```\n\n3.  实现`dualQuatToTransform`功能。假设输入对偶四元数已经归一化。这个的代码可以在下面的代码片段中看到:\n\n    ```cpp\n    Transform dualQuatToTransform(const DualQuaternion& dq){\n        Transform result;\n        result.rotation = dq.real;\n        quat d = conjugate(dq.real) * (dq.dual * 2.0f);\n        result.position = vec3(d.x, d.y, d.z);\n        return result;\n    }\n    ```\n\n对偶四元数也可以转换成矩阵，也可以转换成矩阵；然而，通常不使用该操作。双四元数用于替换蒙皮管道中的矩阵，因此矩阵转换不是必需的。在下一节中，您将探索对偶四元数如何变换向量或点。\n\n## 变换向量和点\n\n对偶四元数包含刚性变换数据。这意味着对偶四元数可以用来变换向量和点。要用对偶四元数变换一个点，请将对偶四元数分解为旋转和位置分量，然后用与变换相同的方式变换向量，但不缩放。\n\n按照以下步骤，使用对偶四元数为向量和点声明并实现`transform`函数:\n\n1.  在`DualQuaternion.h`中声明`transformVector`和`transformPoint`功能，如下:\n\n    ```cpp\n    vec3 transformVector(const DualQuaternion& dq, \n                         const vec3& v);\n    vec3 transformPoint(const DualQuaternion& dq, \n                        const vec3& v);\n    ```\n\n2.  用对偶四元数旋转向量是微不足道的。由于对偶四元数的实部包含旋转，因此将向量乘以对偶四元数的实部，如下所示:\n\n    ```cpp\n    vec3 transformVector(const DualQuaternion& dq, \n                         const vec3& v) {\n        return dq.real * v;\n    }\n    ```\n\n3.  要用对偶四元数变换点，请将对偶四元数转换为旋转和平移分量。然后，对向量应用以下平移和旋转分量:`rotation * vector + translation`。该公式的工作方式与移动点的变换相同，但没有比例分量。这个的代码可以在下面的代码片段中看到:\n\n    ```cpp\n    vec3 transformPoint(const DualQuaternion& dq, \n                        const vec3& v) {\n        quat d = conjugate(dq.real) * (dq.dual * 2.0f);\n        vec3 t = vec3(d.x, d.y, d.z);\n        return dq.real * v + t;\n    }\n    ```\n\n对偶四元数类现在可以用来代替`Transform`类。对偶四元数可以按层次排列，并使用乘法进行组合，有了这些新功能，一个对偶四元数可以直接变换一个点或一个向量。\n\n在本节中，您在代码中实现了双四元数。您需要使用双四元数的所有功能也都实现了。在下一节中，您将学习如何使用双四元数进行网格蒙皮。\n\n# 双四元数蒙皮\n\n在本节中，您将学习如何修改蒙皮算法，使其与对偶四元数而不是矩阵一起工作。具体来说，您将使用变换顶点位置和法线位置的皮肤对偶四元数来替换皮肤矩阵。\n\n对偶四元数解决的问题是矩阵的线性混合，目前在顶点着色器中实现。具体来说，这是引入蒙皮工件的代码位:\n\n```cpp\nmat4 skin;\nskin  = (pose[joints.x] * invBindPose[joints.x]) * weights.x;\nskin += (pose[joints.y] * invBindPose[joints.y]) * weights.y;\nskin += (pose[joints.z] * invBindPose[joints.z]) * weights.z;\nskin += (pose[joints.w] * invBindPose[joints.w]) * weights.w;\n```\n\n在动画管道中有三个阶段，用对偶四元数替换矩阵是有意义的。每一个都会有相同的结果。这里列出了应该实施的三个地方，如下所示:\n\n1.  在顶点着色器中将矩阵转换为对偶四元数。\n2.  将当前姿态的矩阵转换为双四元数，然后将双四元数传递给顶点着色器。\n3.  将当前姿势的每个变换转换为对偶四元数，然后将世界变换累积为对偶四元数。\n\n在本章中，您将实现第三个选项，并在`Pose`类中添加一个`GetDualQuaternionPalette`函数。您还将为`Skeleton`类的`GetInvBindPose`功能添加一个重载。在下一节中，您将开始修改`Skeleton`类以支持双四元数蒙皮动画。\n\n## 修改姿势类\n\n`Pose`类需要两个新功能——一个是检索指定关节(即`GetGlobalDualQuaternion`)的世界对偶四元数，另一个是将姿态转换为对偶四元数调色板。按照以下步骤声明和实现这些函数:\n\n1.  将`GetDualQuaternionPalette`和`GetGlobalDualQuaternion`函数的声明添加到`Pose.h`中的`Pose`类，如下所示:\n\n    ```cpp\n    class Pose {\n    // Existing functions and interface\n    public: // NEW\n    void GetDualQuaternionPalette(vector<DualQuaternion>& o);\n    DualQuaternion GetGlobalDualQuaternion(unsigned int i); \n    };\n    ```\n\n2.  执行`GetGlobalDualQuaternion`功能返回关节的世界空间对偶四元数，如下所示:\n\n    ```cpp\n    DualQuaternion Pose::GetGlobalDualQuaternion(\n                            unsigned int index) {\n        DualQuaternion result = transformToDualQuat(\n                                mJoints[index]);\n        for (int p = mParents[index]; p >= 0; \n             p = mParents[p]) {\n            DualQuaternion parent = transformToDualQuat(\n                                    mJoints[p]);\n            // Remember, multiplication is in reverse!\n            result = result * parent;    \n        }\n        return result;\n    }\n    ```\n\n3.  实现`GetDualQuaternionPalette`功能，该功能应该循环遍历当前姿态中存储的所有关节，并将它们的世界空间对偶四元数存储在输出向量中，如下所示:\n\n    ```cpp\n    void Pose::GetDualQuaternionPalette(\n               vector<DualQuaternion>& out) {\n        unsigned int size = Size();\n        if (out.size() != size) {\n            out.resize(size);\n        }\n        for (unsigned int i = 0; i < size; ++ i) {\n            out[i] = GetGlobalDualQuaternion(i);\n        }\n    }\n    ```\n\n对偶四元数转换发生在关节局部空间，因此，您不需要向`Pose`类添加任何附加数据，而是能够添加两个新函数。在下一节中，您将修改`Skeleton`类，以提供作为双四元数的反向绑定姿势。\n\n## 修改骨架类\n\n为了使用双四元数对网格进行蒙皮，网格的反向绑定姿态也需要使用双四元数来表示。在本节中，您将向`GetInvBindPose`函数添加一个重载，该重载将填充对对偶四元数对象向量的引用。按照以下步骤实现新的`GetInvBindPose`功能:\n\n1.  在`Skeleton`类中声明一个额外的`GetInvBindPose`函数，该函数将引用对偶四元数的向量作为参数。当函数完成时，它将已经用反向绑定姿态对偶四元数填充了向量。这个的代码可以在下面的片段中看到:\n\n    ```cpp\n    class Skeleton {\n    // Existing functions and interface\n    public: // GetInvBindPose is new\n     void GetInvBindPose(vector<DualQuaternion>& pose);\n    };\n    ```\n\n2.  执行`Skeleton.cpp`中的`GetInvBindPose`功能超越。调整输入向量的大小，使其与绑定姿势一样大。对于每个关节，获取关节的全局对偶四元数表示。最后，在输出向量中存储每个世界空间对偶四元数的共轭。这个的代码可以在下面的片段中看到:\n\n    ```cpp\n    void Skeleton::GetInvBindPose(std::vector<DualQuaternion>& \n        outInvBndPose) {\n        unsigned int size = mBindPose.Size();\n        outInvBndPose.resize(size);\n        for (unsigned int i = 0; i < size; ++ i) {\n            DualQuaternion world = \n                 mBindPose.GetGlobalDualQuaternion(i);\n            outInvBndPose[i] = conjugate(world);\n        }\n    }\n    ```\n\n现在，您可以将骨骼的动画姿势和反向绑定姿势转换为双四元数数组。但是为了在着色器中使用这些双四元数，它们需要以某种方式传递给该着色器。在下一节中，您将实现一个新的对偶四元数统一类型来实现这一点。\n\n## 创建新的统一类型\n\n为了使用双四元数作为矩阵的替代，需要有一种方法将它们用作着色器制服。对偶四元数可以被视为 2×4 矩阵，可以用`glUniformMatrix2x4fv`函数设置。\n\n使用`DualQuaternion`为`Uniform`类声明模板专门化。`Set`功能需要实现。它应该使用`glUniformMatrix2x4fv`功能将对偶四元数数组上传为 2×4 矩阵。实现新的`Set`函数，如下面的代码片段所示:\n\n```cpp\ntemplate Uniform<DualQuaternion>;\ntemplate<>\nvoid Uniform<DualQuaternion>::Set(unsigned int slot, \n                                  DualQuaternion* inputArray, \n                                  unsigned int arrayLength) {\n    glUniformMatrix2x4fv(slot, arrayLength, \n                         false, inputArray[0].v);\n}\n```\n\n由于`Set`函数是模板化的，不需要在头文件中声明；它只是函数的一个专门实例。在下一节中，您将探索如何实现使用双四元数进行蒙皮的顶点着色器。\n\n## 创建对偶四元数着色器\n\n为了让支持双四元数蒙皮，唯一要做的就是实现一个顶点着色器。新的顶点着色器将类似于它的线性混合蒙皮对应物。这个着色器将有两个用于双四元数的`mat2x4`统一数组，而不是两个用于矩阵调色板的`mat4`统一数组。\n\n着色器必须将双四元数混合在一起。每当两个四元数(对偶四元数的实部)混合时，就有可能混合发生在错误的邻域中，四元数会进行长时间的插值。混合时需要记住“邻近”。\n\n按照以下步骤实现新的顶点着色器:\n\n1.  开始用 **OpenGL 着色语言** ( **GLSL** )版本和`model`、`view`和`projection`制服声明着色器，如下所示:\n\n    ```cpp\n    #version 330 core\n    uniform mat4 model;\n    uniform mat4 view;\n    uniform mat4 projection;\n    ```\n\n2.  声明顶点结构。顶点的输入值如下:`position`、`normal`、纹理坐标以及权重和关节影响。每个顶点应该有多达四个权重和影响。这个的代码可以在下面的片段中看到:\n\n    ```cpp\n    in vec3 position;\n    in vec3 normal;\n    in vec2 texCoord;\n    in vec4 weights;\n    in ivec4 joints;\n    ```\n\n3.  声明传递给片段着色器的输出值。这些是顶点法线、碎片在世界空间中的位置以及`uv`坐标，如下面的代码片段所示:\n\n    ```cpp\n    out vec3 norm;\n    out vec3 fragPos;\n    out vec2 uv;\n    ```\n\n4.  申报蒙皮制服。这些不再是`mat4`的数组；他们现在是`mat2x4`的阵列。一`mat2x4`有两列四行。认购 a `mat2x4`，指数`0`是我们对偶四元数的实部，指数`1`是对偶部分。代码可以在下面的片段中看到:\n\n    ```cpp\n    uniform mat2x4 pose[120];\n    uniform mat2x4 invBindPose[120];\n    ```\n\n5.  实现四元数乘法功能。其代码与 [*第四章*](04.html#_idTextAnchor069)*实现四元数*中创建的代码相同，可以在下面的代码片段中看到:\n\n    ```cpp\n    vec4 mulQ(vec4 Q1, vec4 Q2) {\n        return vec4(\n            Q2.x*Q1.w + Q2.y*Q1.z - Q2.z*Q1.y + Q2.w*Q1.x,\n           -Q2.x*Q1.z + Q2.y*Q1.w + Q2.z*Q1.x + Q2.w*Q1.y,\n            Q2.x*Q1.y - Q2.y*Q1.x + Q2.z*Q1.w + Q2.w*Q1.z,\n           -Q2.x*Q1.x - Q2.y*Q1.y - Q2.z*Q1.z + Q2.w*Q1.w\n        );\n    }\n    ```\n\n6.  实现`normalize`对偶四元数函数。对偶四元数通过将它的实部和对偶部分除以实部的大小来归一化。代码可以在下面的片段中看到:\n\n    ```cpp\n    mat2x4 normalizeDq(mat2x4 dq) {\n        float invMag = 1.0 / length(dq[0]);\n        dq[0] *= invMag;\n        dq[1] *= invMag;\n        return dq;\n    }\n    ```\n\n7.  实现对偶四元数乘法函数组合对偶四元数，如下:\n\n    ```cpp\n    mat2x4 combineDq(mat2x4 l, mat2x4 r) {\n        l = normalizeDq(l);\n        r = normalizeDq(r);\n        vec4 real = mulQ(l[0], r[0]);\n        vec4 dual = mulQ(l[0], r[1]) + mulQ(l[1], r[0]);\n        return mat2x4(real, dual);\n    }\n    ```\n\n8.  实现一个通过对偶四元数变换向量的函数，如下:\n\n    ```cpp\n    vec4 transformVector(mat2x4 dq, vec3 v) {\n      vec4 real = dq[0];\n      vec3 r_vector = real.xyz;\n      float r_scalar = real.w;\n\n      vec3 rotated = r_vector * 2.0f * dot(r_vector, v) +\n       v * (r_scalar * r_scalar - dot(r_vector, r_vector))+\n       cross(r_vector, v) * 2.0f * r_scalar;\n      return vec4(rotated, 0);\n    }\n    ```\n\n9.  通过对偶四元数对变换点实现一个函数，如下所示:\n\n    ```cpp\n    vec4 transformPoint(mat2x4 dq, vec3 v) {\n        vec4 real = dq[0];\n        vec4 dual = dq[1];\n        vec3 rotated = transformVector(dq, v).xyz;\n        vec4 conjugate = vec4(-real.xyz, real.w);\n        vec3 t = mulQ(conjugate, dual * 2.0).xyz;\n\n        return vec4(rotated + t, 1);\n    }\n    ```\n\n10.  实现顶点着色器的主要方法。通过将关节 1、2 和 3 ( `joints.y`、`joints.z`、`joints.w`)邻接到关节 0 ( `joints.x`)开始实施，如下所示:\n\n    ```cpp\n    void main() {\n        vec4 w = weights;\n        // Neighborhood all of the quaternions correctly\n        if (dot(pose[joints.x][0], pose[joints.y][0]) < 0.0)\n           { w.y *= -1.0; }\n        if (dot(pose[joints.x][0], pose[joints.z][0]) < 0.0)\n           { w.z *= -1.0; }\n        if (dot(pose[joints.x][0], pose[joints.w][0]) < 0.0)\n           { w.w *= -1.0; }\n    ```\n\n11.  将每个关节的世界空间对偶四元数与同一个关节的逆绑定姿态对偶四元数相结合。记住:对偶四元数乘法是从左向右的。将每次乘法的结果存储在新的变量中。代码可以在下面的片段中看到:\n\n    ```cpp\n        // Combine\n        mat2x4 dq0 = combineDq(invBindPose[joints.x], \n                               pose[joints.x]);\n        mat2x4 dq1 = combineDq(invBindPose[joints.y], \n                               pose[joints.y]);\n        mat2x4 dq2 = combineDq(invBindPose[joints.z], \n                               pose[joints.z]);\n        mat2x4 dq3 = combineDq(invBindPose[joints.w], \n                               pose[joints.w]);\n    ```\n\n12.  将四个蒙皮双四元数混合在一起。使用对偶四元数标量乘法和对偶四元数加法实现混合。别忘了归一化皮肤对偶四元数。代码可以在下面的片段中看到:\n\n    ```cpp\n        mat2x4 skinDq = w.x * dq0 + w.y * dq1 + \n                        w.z * dq2 + w.w * dq3;\n        skinDq = normalizeDq(skinDq);\n    ```\n\n13.  使用带有蒙皮对偶四元数的`transformPoint`函数蒙皮顶点。将生成的`vec4`通过正常的模型-视图-投影管道，如下所示:\n\n    ```cpp\n        vec4 v = transformPoint(skinDq, position);\n        gl_Position = projection * view * model * v;\n        fragPos = vec3(model * v);\n    ```\n\n14.  类似地变换法线。不要忘记将`uv`坐标传递给片段着色器。代码可以在下面的片段中看到:\n\n    ```cpp\n        vec4 n = transformVector(skinDq, normal);\n        norm = vec3(model * n);\n        uv = texCoord;\n    }\n    ```\n\n任何动画比例的动画都不会用这个方法。这种对偶四元数实现不支持缩放。在双四元数的基础上攻击缩放支持是可能的，但是所涉及的工作在性能方面超过了它的好处。\n\n在本节中，您学习了如何使用双四元数实现蒙皮。这包括修改姿势数据和`Skeleton`类，创建新的制服，以及构建新的着色器。在下一节中，您将探索如何使用至此编写的对偶四元数代码。\n\n# 了解如何使用对偶四元数蒙皮\n\n本节将探讨您如何获取到目前为止编写的对偶四元数蒙皮代码，并在现有应用中实现它。本代码仅供参考；你不必跟着它走。\n\n使用对偶四元数蒙皮着色器是微不足道的；在运行时很容易在蒙皮方法之间切换。以下步骤演示了如何使用对偶四元数着色器或线性蒙皮着色器来制作同一模型的动画。\n\n跟踪对偶四元数姿势调板和反向绑定姿势调板，以及线性混合姿势调板和反向绑定姿势调板。看看下面的代码:\n\n```cpp\n// For dual quaternion skinning\nstd::vector<DualQuaternion> mDqPosePalette;\nstd::vector<DualQuaternion> mDqInvBindPalette;\n// For linear blend skinning\nstd::vector<mat4> mLbPosePalette;\nstd::vector<mat4> mLbInvBindPalette;\n```\n\n当应用初始化时，将反向绑定姿态缓存为矩阵向量和对偶四元数向量，如下所示:\n\n```cpp\nmCurrentPose = mSkeleton.GetRestPose();\nmCurrentPose.GetDualQuaternionPalette(mDqPosePalette);\nmSkeleton.GetInvBindPose(mDqInvBindPalette);\nmCurrentPose.GetMatrixPalette(mLbPosePalette);\nmLbInvBindPalette = mSkeleton.GetInvBindPose();\n```\n\n对动画进行采样时，将生成的姿势选项板转换为双四元数和线性混合版本，如下所示:\n\n```cpp\nmPlayTime = mClips[mClip].Sample(mCurrentPose, \n                                 mPlayTime + dt);\nmCurrentPose.GetDualQuaternionPalette(mDqPosePalette);\nmCurrentPose.GetMatrixPalette(mLbPosePalette);\n```\n\n渲染动画时，请确保使用了正确的制服，如下所示:\n\n```cpp\nif (mSkinningMethod == SkinningMethod::DualQuaternion) {\n   Uniform<DualQuaternion>::Set(\n           shader->GetUniform(\"pose\"), mDqPosePalette);\n   Uniform<DualQuaternion>::Set(\n   shader->GetUniform(\"invBindPose\"), mDqInvBindPalette);\n}\nelse {\n   Uniform<mat4>::Set(shader->GetUniform(\"pose\"), \n                      mLbPosePalette);\n   Uniform<mat4>::Set(shader->GetUniform(\"invBindPose\"),\n                      mLbInvBindPalette);\n}\n```\n\n在此示例中，在线性混合蒙皮和双四元数蒙皮着色器之间切换就像更改`mSkinningMethod`变量的值一样简单。这是可行的，因为这两个着色器之间的唯一区别是姿势调色板制服。\n\n# 总结\n\n在本章中，您学习了对偶四元数背后的数学，并实现了一个对偶四元数类。您发现了一些由线性混合蒙皮产生的蒙皮工件，以及如何使用双四元数来避免这些工件。本章中实现的双四元数蒙皮着色器可用于替换线性混合蒙皮着色器。\n\n如果你在本书的可下载资料中`Chapter14`下查找，有两个例子。`Sample00`包含至此的所有代码。`Sample01`渲染同一个扭曲立方体模型两次。第一个立方体用线性混合蒙皮着色器渲染。第二个是用对偶四元数着色器渲染的。\n\n在下一章中，您将探索如何使用索引绘图来制作大量人群的动画。这很有趣，因为它涉及到将姿势生成移动到**图形处理单元** ( **GPU** )并在顶点着色器中执行整个蒙皮动画管道。"
  },
  {
    "path": "docs/handson-cpp-game-ani-prog/15.md",
    "content": "# 十五、使用实例渲染人群\n\n这最后一章探索了如何使用实例渲染大量人群。群组渲染是一个有趣的话题，因为它将姿势生成(采样)和混合移动到 GPU 上，使整个动画管道在顶点着色器中运行。\n\n要将姿势生成移动到顶点着色器，需要在纹理中编码动画信息。本章的重点将是将动画数据编码到纹理中，并使用该纹理创建动画姿势。\n\n如果没有实例，画一大群人就意味着要打很多抽奖电话，这会影响帧率。使用实例化，一个网格可以绘制多次。如果只有一个绘制调用，人群中每个角色的动画姿势将需要以不同的方式生成。\n\n在本章中，您将探索将动画采样移动到顶点着色器中，以便绘制大量人群。本章将涵盖以下主题:\n\n*   在纹理中存储任意数据\n*   从纹理中检索任意数据\n*   将动画烘焙成纹理\n*   在顶点着色器中采样动画纹理\n*   优化人群系统\n\n# 在纹理中存储数据\n\n采样动画不是一个微不足道的任务。有很多循环和函数，这使得 GPU 上的动画采样成为一个难题。解决这个问题的一个方法就是简化它。\n\n可以在设定的时间间隔内对动画进行采样，而不是实时采样。以设定的时间间隔对动画进行采样并将结果数据写入文件的过程称为烘焙。\n\n烘焙动画数据后，着色器不再需要对实际的动画片段进行采样。相反，它可以根据时间查找最近的采样姿势。那么，这些动画数据被烤到哪里去了呢？动画可以烘焙成纹理。纹理可以用作数据缓冲区，并且已经有一种在着色器中读取纹理数据的简单方法。\n\n通常，纹理中的存储类型和信息被着色器中的采样函数提取出来。例如，GLSL 的`texture2D`函数将归一化的`uv`坐标作为参数，并返回一个值范围从`0`到`1`的四分量向量。\n\n但这些信息都不在纹理里。当使用`glTexImage2D`创建纹理时，它采用内部纹理格式(`GL_RGBA`)、源格式(通常又是`GL_RGBA`)和数据类型(通常是`GL_UNSIGNED_BYTE`)。这些参数用于将基础数据类型转换为`texture2D`返回的标准化值。\n\n在纹理中存储任意数据时，这有两个问题。首先是数据的粒度。在`GL_RGBA`的情况下，每个采样的浮点分量只有 256 个唯一值。第二，如果需要存储的值没有归一化到`0`到`1`的范围，该怎么办？\n\n这就是浮点纹理的来源。您可以创建具有`GL_RGBA32F`格式的四分量浮点纹理。这个纹理将比其他纹理大得多，因为每个像素将存储四个完整的 32 位浮点数。\n\n浮点纹理可以存储任意数据。在下一节中，您将学习如何从浮点纹理中检索任意数据。之后，您将探索着色器如何从浮点纹理读取数据。\n\n# 从纹理读取数据\n\n本节探讨如何在着色器中检索存储在纹理中的动画数据。在本节中，您将学习如何对纹理进行采样以及在对纹理进行采样时应该使用什么样的采样器状态。\n\n一旦数据格式正确，采样就成了下一个挑战。`glTexImage2D`函数需要归一化的`uv`坐标，并返回一个归一化值。另一方面，`texelFetch`功能可用于使用像素坐标对纹理进行采样，并返回这些坐标下的原始数据。\n\n`texelFetch` glsl 有三个参数:一个采样器、`ivec2`和一个整数。`ivec2`是像素空间中被采样像素的 *x* 和 *y* 坐标。最后一个整数是要使用的 mip 级别，对于本章，它将始终是`0`。\n\nmipmap 是同一图像的一系列分辨率逐渐降低的版本。当 mip 级别降低时，数据会丢失。这种数据丢失会改变动画的内容。避免为动画纹理生成 MIP。\n\n因为数据需要以与完全相同的方式读取，任何插值都会破坏动画数据。确保使用最近邻采样对动画纹理进行采样。\n\n使用`texelFetch`而不是`glTexImage2D`对纹理进行采样应该会返回正确的数据。纹理可以在顶点着色器或片段着色器中进行采样。在下一节中，您将探索哪些动画数据应该存储在这些浮点纹理中。\n\n# 对动画数据进行编码\n\n现在你知道如何读写数据到一个纹理，下一个问题是，需要在纹理中写入什么数据？您将把动画数据编码成纹理。每一个动画片段都将按设定的时间间隔进行采样。所有这些样本产生的姿势将存储在一个纹理中。\n\n为了对该数据进行编码，纹理的 *x* 轴将代表时间。纹理的 *y* 轴将代表正在制作动画的骨骼中的骨骼。每个骨骼将占据三行:一行用于位置，一行用于旋转，一行用于刻度。\n\n动画剪辑将以设定的时间间隔进行采样，以确保纹理越宽采样越多。例如，对于一个 *256x256* 动画纹理，该动画剪辑将需要被采样 256 次。\n\n当对动画剪辑进行采样以将其编码为纹理时，对于每个采样，您将找到每个骨骼的世界空间变换并将其写入纹理。 *y* 坐标将为`joint_index * 3 + component`，其中有效成分为`position = 0`、`rotation = 1`和`scale = 3`。\n\n一旦这些值被写入纹理，将纹理上传到图形处理器并使用它。在下一节中，您将探索着色器如何评估此动画纹理。\n\n# 探索每个实例的数据\n\n在渲染一大群人时，人群中的每个演员都有一定的属性。在本节中，您将探索每实例数据是什么，以及如何将其传递给着色器。这将大大减少每帧作为统一数组上传到 GPU 的数据量。\n\n将蒙皮管道移动到顶点着色器并不能完全消除将人群相关的制服传递给着色器的需要。人群中的每个演员都需要一些数据上传到 GPU。每个实例的数据比使用姿势调色板矩阵时上传的数据要小得多。\n\n人群中的每个演员都需要一个位置、旋转和缩放来构建模型矩阵。演员需要知道要采样的当前帧以及要混合的当前帧和下一帧之间的时间。\n\n每个参与者实例数据的总大小为 11 个浮点数和 2 个整数。每个实例只有 52 字节。每实例数据将始终使用统一数组传递。每个数组的大小是人群包含的演员数量。数组的每个元素代表一个唯一的参与者。\n\n着色器将负责根据每个实例的数据和动画纹理构建适当的矩阵。当前帧和下一帧之间的混合是可选的；混合不会 100%正确，但看起来应该还是不错的。\n\n在下一节中，您将实现一个`AnimationTexture`类，它将允许您在代码中使用动画纹理。\n\n# 创建动画纹理\n\n在本节中，您将在`AnimTexture`类中实现处理浮点纹理所需的所有代码。每个`AnimTexture`对象将包含一个 32 位浮点 RGBA 纹理。这些数据将有两个副本:一个在中央处理器上，一个上传到图形处理器上。\n\nCPU 缓冲区保留在周围，以便在保存到磁盘或上传到 OpenGL 之前轻松批量修改纹理的内容。它以一些额外的内存为代价保持了 API 的简单性。\n\n没有标准的 32 位纹理格式，所以保存和写入磁盘时只会将`AnimTexture`类的二进制内容转储到磁盘。在下一节中，您将开始实现`AnimTexture`类。这个类将为实现 32 位浮点纹理提供一个易于使用的接口。\n\n## 声明动画纹理类\n\n动画纹理假设总是正方形；宽度和高度不需要单独跟踪。使用单个大小变量应该就足够了。`AnimTexture`类每次在内存中总是有两个纹理副本，一个在 CPU 上，一个在 GPU 上。\n\n创建一个名为`AnimTexture.h`的新文件，并在该文件中声明`AnimTexture`类。按照以下步骤申报`AnimTexture`类:\n\n1.  申报`AnimTexture`班。它有三个成员变量:一个浮点数组，一个表示纹理大小的整数，以及一个 OpenGL 纹理对象的句柄:\n\n    ```cpp\n    class AnimTexture {\n    protected:\n        float* mData;\n        unsigned int mSize;\n        unsigned int mHandle;\n    ```\n\n2.  用默认构造函数、复制构造函数、赋值操作符和析构函数声明【T0:\n\n    ```cpp\n    public:\n        AnimTexture();\n        AnimTexture(const AnimTexture&);\n        AnimTexture& operator=(const AnimTexture&);\n        ~AnimTexture();\n    ```\n\n3.  声明功能以便将`AnimTexture`保存到磁盘并再次加载:\n\n    ```cpp\n        void Load(const char* path);\n        void Save(const char* path);\n    ```\n\n4.  声明一个函数，将数据从`mData`变量上传到 OpenGL 纹理:\n\n    ```cpp\n        void UploadTextureDataToGPU();\n    ```\n\n5.  为`AnimTexture`包含的 CPU 端数据声明 getter 和 setter 函数:\n\n    ```cpp\n        unsigned int Size();\n        void Resize(unsigned int newSize);\n        float* GetData();\n    ```\n\n6.  声明`GetTexel`，取 *x* 和 *y* 坐标，返回一个`vec4`，以及一个`SetTexel`函数来设置`vec3`或`quat`对象。这些函数将写入纹理的数据:\n\n    ```cpp\n        void SetTexel(unsigned int x, unsigned int y, \n                      const vec3& v);\n        void SetTexel(unsigned int x, unsigned int y, \n                      const quat& q);\n        vec4 GetTexel(unsigned int x, unsigned int y);\n    ```\n\n7.  声明函数来绑定和取消绑定纹理以进行渲染。这与`Texture`类的`Set`和`Unset`功能相同:\n\n    ```cpp\n       void Set(unsigned int uniform, unsigned int texture);\n       void UnSet(unsigned int textureIndex);\n       unsigned int GetHandle();\n    };\n    ```\n\n类是处理浮点纹理的一种方便的方法。`get`和`SetTexel`方法可以使用直观的应用编程接口读写纹理。在下一节中，您将开始实现`AnimTexture`类。\n\n## 实现动画纹理类\n\n在本节中，您将实现`AnimTexture`类，该类包含用于处理浮点纹理的 OpenGL 代码，并提供了一个易于使用的 API。如果你想使用一个图形应用编程接口而不是 OpenGL，这个类将需要使用那个应用编程接口重写。\n\n当一个`AnimTexture`保存到磁盘时，整个`mData`数组作为一个大的二进制 blob 写入文件。这个大的纹理数据占用了相当多的内存；例如，一个 *512x512* 纹理占用大约 4 MB。纹理压缩并不适合，因为动画数据需要精确。\n\n`SetTexel`功能是我们将数据写入动画纹理的主要方式。这些函数采用 *x* 和 *y* 坐标，以及`vec3`或四元数值。该函数需要根据给定的 *x* 和 *y* 坐标计算出进入`mData`数组的正确索引，然后相应地设置像素值。\n\n创建一个名为`AnimTexture.cpp`的新文件。在这个新文件中实现`AnimTexture`类。现在，按照以下步骤实施`AnimTexture`课程:\n\n1.  实现默认构造函数。它应该将数据和大小设置为零，并生成一个新的 OpenGL 着色器句柄:\n\n    ```cpp\n    AnimTexture::AnimTexture() {\n        mData = 0;\n        mSize = 0;\n        glGenTextures(1, &mHandle);\n    }\n    ```\n\n2.  实现复制构造函数。它应该像默认构造函数一样，使用赋值操作符复制实际的纹理数据:\n\n    ```cpp\n    AnimTexture::AnimTexture(const AnimTexture& other) {\n        mData = 0;\n        mSize = 0;\n        glGenTextures(1, &mHandle);\n        *this = other;\n    }\n    ```\n\n3.  执行分配操作符。它只需要复制 CPU 端的数据；OpenGL 句柄可以单独使用:\n\n    ```cpp\n    AnimTexture& AnimTexture::operator=(\n                              const AnimTexture& other) {\n        if (this == &other) {\n            return *this;\n        }\n        mSize = other.mSize;\n        if (mData != 0) {\n            delete[] mData;\n        }\n        mData = 0;\n        if (mSize != 0) {\n            mData = new float[mSize * mSize * 4];\n            memcpy(mData, other.mData, \n                sizeof(float) * (mSize * mSize * 4));\n        }\n        return *this;\n    }\n    ```\n\n4.  实现`AnimTexture`类的析构函数。它应该删除内部浮点数组，并释放该类持有的 OpenGL 句柄:\n\n    ```cpp\n    AnimTexture::~AnimTexture() {\n        if (mData != 0) {\n            delete[] mData;\n        }\n        glDeleteTextures(1, &mHandle);\n    }\n    ```\n\n5.  实现`Save`功能。它应该将`AnimTexture`的大小写入文件，并将`mData`的内容写入一个大的二进制 blob:\n\n    ```cpp\n    void AnimTexture::Save(const char* path) {\n        std::ofstream file;\n        file.open(path, std::ios::out | std::ios::binary);\n        if (!file.is_open()) {\n            cout << \"Couldn't open \" << path << \"\\n\";\n        }\n        file << mSize;\n        if (mSize != 0) {\n            file.write((char*)mData, \n                 sizeof(float) * (mSize * mSize * 4));\n        }\n        file.close();\n    }\n    ```\n\n6.  实现的`Load`功能，将序列化的动画数据加载回内存:\n\n    ```cpp\n    void AnimTexture::Load(const char* path) {\n        std::ifstream file;\n        file.open(path, std::ios::in | std::ios::binary);\n        if (!file.is_open()) {\n            cout << \"Couldn't open \" << path << \"\\n\";\n        }\n        file >> mSize;\n        mData = new float[mSize * mSize * 4];\n        file.read((char*)mData, \n             sizeof(float) * (mSize * mSize * 4));\n        file.close();\n        UploadTextureDataToGPU();\n    }\n    ```\n\n7.  实现`UploadDataToGPU`功能。其实现与`Texture::Load`非常相似，但使用`GL_RGBA32F`代替`GL_FLOAT` :\n\n    ```cpp\n    void AnimTexture::UploadTextureDataToGPU() {\n        glBindTexture(GL_TEXTURE_2D, mHandle);\n        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, mSize, \n                      mSize, 0, GL_RGBA, GL_FLOAT, mData);\n        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, \n                        GL_CLAMP_TO_EDGE);\n        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, \n                        GL_CLAMP_TO_EDGE);\n        glTexParameteri(GL_TEXTURE_2D, \n                        GL_TEXTURE_MIN_FILTER, GL_LINEAR);\n        glTexParameteri(GL_TEXTURE_2D, \n                        GL_TEXTURE_MAG_FILTER, GL_LINEAR);\n        glBindTexture(GL_TEXTURE_2D, 0);\n    }\n    ```\n\n8.  实现大小、OpenGL 句柄和浮点数据获取函数:\n\n    ```cpp\n    unsigned int AnimTexture::Size() {\n        return mSize;\n    }\n    unsigned int AnimTexture::GetHandle() {\n        return mHandle;\n    }\n    float* AnimTexture::GetData() {\n        return mData;\n    }\n    ```\n\n9.  实现`resize`功能，应该设置`mData`数组的大小。这个函数接受的参数是动画纹理的宽度或高度:\n\n    ```cpp\n    void AnimTexture::Resize(unsigned int newSize) {\n        if (mData != 0) {\n            delete[] mData;\n        }\n        mSize = newSize;\n        mData = new float[mSize * mSize * 4];\n    }\n    ```\n\n10.  实现`Set`功能。工作原理类似`Texture::Set` :\n\n    ```cpp\n    void AnimTexture::Set(unsigned int uniformIndex, unsigned int textureIndex) {\n        glActiveTexture(GL_TEXTURE0 + textureIndex);\n        glBindTexture(GL_TEXTURE_2D, mHandle);\n        glUniform1i(uniformIndex, textureIndex);\n    }\n    ```\n\n11.  实现`UnSet`功能。工作原理类似`Texture::UnSet` :\n\n    ```cpp\n    void AnimTexture::UnSet(unsigned int textureIndex) {\n        glActiveTexture(GL_TEXTURE0 + textureIndex);\n        glBindTexture(GL_TEXTURE_2D, 0);\n        glActiveTexture(GL_TEXTURE0);\n    }\n    ```\n\n12.  实现`SetTexel`函数，该函数以向量`3`为参数。该功能应将像素未使用的 A 分量设置为`0` :\n\n    ```cpp\n    void AnimTexture::SetTexel(unsigned int x, \n                      unsigned int y, const vec3& v) {\n        unsigned int index = (y * mSize * 4) + (x * 4);\n        mData[index + 0] = v.x;\n        mData[index + 1] = v.y;\n        mData[index + 2] = v.z;\n        mData[index + 3] = 0.0f;\n    }\n    ```\n\n13.  实现`SetTexel`函数，以四元数为参数:\n\n    ```cpp\n    void AnimTexture::SetTexel(unsigned int x, \n                      unsigned int y, const quat& q) {\n        unsigned int index = (y * mSize * 4) + (x * 4);\n        mData[index + 0] = q.x;\n        mData[index + 1] = q.y;\n        mData[index + 2] = q.z;\n        mData[index + 3] = q.w;\n    }\n    ```\n\n14.  实现`GetTexel`功能。该函数将始终返回一个`vec4`，它包含像素的每个分量:\n\n    ```cpp\n    vec4 AnimTexture::GetTexel(unsigned int x, \n                               unsigned int y) {\n        unsigned int index = (y * mSize * 4) + (x * 4);\n        return vec4(\n            mData[index + 0],\n            mData[index + 1],\n            mData[index + 2],\n            mData[index + 3]\n        );\n    }\n    ```\n\n在本节中，您学习了如何创建 32 位浮点纹理并管理其中的数据。`AnimTexture`类应该让你使用直观的 API 来处理浮点纹理，而不必担心任何 OpenGL 函数。在下一节中，您将创建一个函数，该函数将对动画剪辑进行采样，并将生成的动画数据写入纹理。\n\n# 动画面包师\n\n在本节中，您将学习如何获取动画剪辑并将其编码为动画纹理。这个过程叫做烘焙。\n\n纹理烘焙是使用将动画烘焙成纹理的辅助函数来实现的。该`Bake`功能将以设定的间隔对动画进行采样，并将每个采样的骨架层次写入浮点纹理。\n\n对于参数，`Bake`函数需要一个骨架、一个动画剪辑和一个对要写入的`AnimTexture`的引用。骨架很重要，因为它提供了静止姿势，该姿势将用于动画剪辑中不存在的任何关节。骨架的每个关节都会被烤成纹理。让我们开始吧:\n\n1.  创建一个名为`AnimBaker.h`的新文件，并将`BakeAnimationToTexture`函数的声明添加到其中:\n\n    ```cpp\n    void BakeAnimationToTexture(Skeleton& skel, Clip& clip, \n                                AnimTexture& outTex);\n    ```\n\n2.  创建一个名为`AnimBaker.cpp`的新文件。开始执行本文件中的`BakeAnimationToTexture`功能:\n\n    ```cpp\n    void BakeAnimationToTexture(Skeleton& skel, Clip& clip, \n                                AnimTexture& tex) {\n        Pose& bindPose = skel.GetBindPose();\n    ```\n\n3.  要将动画烘焙成纹理，首先，创建一个动画将被采样的姿势。然后，循环通过纹理的 *x* 维度，也就是时间:\n\n    ```cpp\n        Pose pose = bindPose;\n        unsigned int texWidth = tex.Size();\n        for (unsigned int x = 0; x < texWidth; ++ x) {\n    ```\n\n4.  对于每次迭代，找到迭代器的归一化值(`iterator index / (size - 1)`)。将归一化时间乘以片段的持续时间，然后加上片段的开始时间。此时对当前像素的片段进行采样:\n\n    ```cpp\n            float t = (float)x / (float)(texWidth - 1);\n            float start = clip.GetStartTime();\n            float time = start + clip.GetDuration() * t;\n            clip.Sample(pose, time);\n    ```\n\n5.  一旦剪辑被采样，循环通过绑定姿势中的所有关节。找到当前关节的全局变换，使用`SetTexel` :\n\n    ```cpp\n            for (unsigned int y = 0;y<pose.Size()*3;y+=3) {\n               Transform node=pose.GetGlobalTransform(y/3);\n               tex.SetTexel(x, y + 0, node.position);\n               tex.SetTexel(x, y + 1, node.rotation);\n               tex.SetTexel(x, y + 2, node.scale);\n            }\n    ```\n\n    将数据写入纹理\n6.  在`Bake`函数返回之前，在提供的动画纹理上调用`UploadTextureDataToGPU`函数。这将使纹理在烘焙后立即可用:\n\n    ```cpp\n        } // End of x loop\n        tex.UploadTextureDataToGPU();\n    }\n    ```\n\n在高层次上，动画纹理用作时间轴，其中 *x* 轴是时间， *y* 轴是当时动画关节的变换。在下一节中，您将创建群组着色器。人群着色器使用由`BakeAnimationToTexture`烘焙成纹理的日期来采样动画的当前姿势。\n\n# 创建群组着色器\n\n要渲染人群，您将需要来创建一个新的着色器。群组着色器将具有投影和视图制服，但没有模型制服。这是因为所有演员都是用相同的投影和视图矩阵绘制的，但需要一个唯一的模型矩阵。代替模型矩阵，着色器将有三个统一的数组:一个用于位置，一个用于旋转，一个用于缩放。\n\n将被放入这些数组中的值将是一个实例索引——当前正在渲染的网格的索引。每个顶点通过内置的`glsl`变量`gl_InstanceID`获得其网格实例的副本。每个顶点将使用位置、旋转和缩放均匀数组来构建模型矩阵。\n\n反向绑定姿势就像一个有规则蒙皮的矩阵均匀阵列，但动画姿势不是。为了找到动画姿势，着色器必须对动画纹理进行采样。因为每个顶点被蒙皮为四个顶点，所以每个顶点的动画姿势必须被找到四次。\n\n创建一个名为`crowd.vert`的新文件。人群着色器将在此文件中实现。按照以下步骤实现群组着色器:\n\n1.  通过定义两个常数开始实现着色器:一个用于骨骼的最大数量，一个用于支持的实例的最大数量:\n\n    ```cpp\n    #version 330 core\n    #define MAX_BONES 60\n    #define MAX_INSTANCES 80\n    ```\n\n2.  宣布人群中所有演员共用的制服。这包括视图和投影矩阵、反向绑定姿势调色板和动画纹理:\n\n    ```cpp\n    uniform mat4 view;\n    uniform mat4 projection;\n    uniform mat4 invBindPose[MAX_BONES];\n    uniform sampler2D animTex;\n    ```\n\n3.  宣布人群中每个演员独有的制服。这包括演员的变换，当前和下一帧，以及混合时间:\n\n    ```cpp\n    uniform vec3 model_pos[MAX_INSTANCES];\n    uniform vec4 model_rot[MAX_INSTANCES];\n    uniform vec3 model_scl[MAX_INSTANCES];\n    uniform ivec2 frames[MAX_INSTANCES];\n    uniform float time[MAX_INSTANCES];\n    ```\n\n4.  声明顶点结构。每顶点数据与任何蒙皮网格相同:\n\n    ```cpp\n    in vec3 position;\n    in vec3 normal;\n    in vec2 texCoord;\n    in vec4 weights;\n    in ivec4 joints;\n    ```\n\n5.  声明群组着色器的输出值:\n\n    ```cpp\n    out vec3 norm;\n    out vec3 fragPos;\n    out vec2 uv;\n    ```\n\n6.  实现一个将向量和四元数相乘的函数。该函数将具有与您在 [*第 4 章*](04.html#_idTextAnchor069) *中构建的`transformVector`函数相同的实现，实现四元数*，除了它在着色器中运行:\n\n    ```cpp\n    vec3 QMulV(vec4 q, vec3 v) {\n        return q.xyz * 2.0f * dot(q.xyz, v) +\n               v * (q.w * q.w - dot(q.xyz, q.xyz)) +\n               cross(q.xyz, v) * 2.0f * q.w;\n    }\n    ```\n\n7.  实现`GetModel`功能。给定一个实例索引，这个函数应该采样动画纹理并返回一个 *4x4* 变换矩阵:\n\n    ```cpp\n    mat4 GetModel(int instance) {\n        vec3 position = model_pos[instance];\n        vec4 rotation = model_rot[instance];\n        vec3 scale = model_scl[instance];\n        vec3 xBasis = QMulV(rotation, vec3(scale.x, 0, 0));\n        vec3 yBasis = QMulV(rotation, vec3(0, scale.y, 0));\n        vec3 zBasis = QMulV(rotation, vec3(0, 0, scale.z));\n        return mat4(\n            xBasis.x, xBasis.y, xBasis.z, 0.0,\n            yBasis.x, yBasis.y, yBasis.z, 0.0,\n            zBasis.x, zBasis.y, zBasis.z, 0.0,\n            position.x, position.y, position.z, 1.0\n        );\n    }\n    ```\n\n8.  用一个关节和一个实例实现`GetPose`函数，其中该函数应该返回关节的动画世界矩阵。开始执行时，找到 x 和 y 位置，用\n\n    ```cpp\n    mat4 GetPose(int joint, int instance) {\n        int x_now = frames[instance].x;\n        int x_next = frames[instance].y;\n        int y_pos = joint * 3;\n    ```\n\n    对动画纹理进行采样\n9.  从动画纹理中采样当前帧的位置、旋转和缩放:\n\n    ```cpp\n        vec4 pos0 = texelFetch(animTex, ivec2(x_now, \n                              (y_pos + 0)), 0);\n        vec4 rot0 = texelFetch(animTex, ivec2(x_now, \n                              (y_pos + 1)), 0);\n        vec4 scl0 = texelFetch(animTex, ivec2(x_now, \n                              (y_pos + 2)), 0);\n    ```\n\n10.  从动画纹理中采样下一帧的位置、旋转和缩放:\n\n    ```cpp\n        vec4 pos1 = texelFetch(animTex, ivec2(x_next, \n                              (y_pos + 0)), 0);\n        vec4 rot1 = texelFetch(animTex, ivec2(x_next, \n                              (y_pos + 1)), 0);\n        vec4 scl1 = texelFetch(animTex, ivec2(x_next, \n                              (y_pos + 2)), 0);\n    ```\n\n11.  在两帧的变换之间进行插值:\n\n    ```cpp\n        if (dot(rot0, rot1) < 0.0) { rot1 *= -1.0; }\n        vec4 position = mix(pos0, pos1, time[instance]);\n        vec4 rotation = normalize(mix(rot0, \n                                  rot1, time[instance]));\n        vec4 scale = mix(scl0, scl1, time[instance]);\n    ```\n\n12.  使用插值的位置、旋转和缩放返回 4x4 矩阵:\n\n    ```cpp\n        vec3 xBasis = QMulV(rotation, vec3(scale.x, 0, 0));\n        vec3 yBasis = QMulV(rotation, vec3(0, scale.y, 0));\n        vec3 zBasis = QMulV(rotation, vec3(0, 0, scale.z));\n        return mat4(\n            xBasis.x, xBasis.y, xBasis.z, 0.0,\n            yBasis.x, yBasis.y, yBasis.z, 0.0,\n            zBasis.x, zBasis.y, zBasis.z, 0.0,\n            position.x, position.y, position.z, 1.0\n        );\n    }\n    ```\n\n13.  开始实现着色器的主要功能，找到所有四个动画姿势矩阵，以及人群中当前演员的模型矩阵。使用`gl_InstanceID`获取当前绘制演员的 ID:\n\n    ```cpp\n    void main() {\n        mat4 pose0 = GetPose(joints.x, gl_InstanceID);\n        mat4 pose1 = GetPose(joints.y, gl_InstanceID);\n        mat4 pose2 = GetPose(joints.z, gl_InstanceID);\n        mat4 pose3 = GetPose(joints.w, gl_InstanceID);\n        mat4 model = GetModel(gl_InstanceID);\n    ```\n\n14.  继续执行主要功能，找到顶点的`skin`矩阵:\n\n    ```cpp\n        mat4 skin = (pose0*invBindPose[joints.x])*weights.x;\n        skin += (pose1 * invBindPose[joints.y]) * weights.y;\n        skin += (pose2 * invBindPose[joints.z]) * weights.z;\n        skin += (pose3 * invBindPose[joints.w]) * weights.w;\n    ```\n\n15.  通过蒙皮顶点的变换管道\n\n    ```cpp\n        gl_Position = projection * view * model * \n                      skin * vec4(position, 1.0);\n        fragPos = vec3(model * skin * vec4(position, 1.0));\n        norm = vec3(model * skin * vec4(normal, 0.0f));\n        uv = texCoord;\n    }\n    ```\n\n    放置位置和法线，完成主功能的实现\n\n在本节中，您实现了群组着色器。这个顶点着色器使用动画纹理来构造每个渲染顶点的动画姿态。它将蒙皮管道的姿势生成部分移动到图形处理器。着色器旨在渲染实例化网格；它使用`gl_InstanceID`来确定当前正在渲染哪个实例。\n\n这个着色器是一个很好的起点，但总有改进的空间。着色器目前使用了许多统一的索引。一些低端机器可能无法提供足够的制服。本章末尾将介绍几种优化策略。在下一节中，您将实现一个`Crowd`类来帮助管理群组着色器所需的所有数据。\n\n# 创建群组实用程序类\n\n在本节中，您将构建`Crowd`类。这是一个实用程序类，将使用一个易于使用的应用编程接口渲染大量人群。`Crowd`类封装了人群的状态。\n\n`Crowd`类必须维护类中每个参与者的实例数据。为了适应这一点，您需要声明最大数量的参与者。然后，所有特定于行动者的信息可以存储在结构数组中，其中索引是行动者标识。\n\n演员特定数据包括演员的世界变换，以及与其动画回放相关的数据。动画数据是正在插值的帧、插值值以及当前帧和下一帧的关键时间。\n\n创建一个名为`Crowd.h`的新文件。`Crowd`类将在该文件中声明。按照以下步骤申报`Crowd`类:\n\n1.  将人群演员的最大数量定义为`80` :\n\n    ```cpp\n    #define CROWD_MAX_ACTORS 80\n    ```\n\n2.  通过为所有实例数据创建向量，开始声明`Crowd`类。这包括每个演员的变换、动画帧和时间的数据，以及帧插值信息:\n\n    ```cpp\n    struct Crowd {\n    protected:\n        std::vector<vec3> mPositions;\n        std::vector<quat> mRotations;\n        std::vector<vec3> mScales;\n        std::vector<ivec2> mFrames;\n        std::vector<float> mTimes;\n        std::vector<float> mCurrentPlayTimes;\n        std::vector<float> mNextPlayTimes;\n    ```\n\n3.  声明`AdjustTime`、`UpdatePlaybackTimes`、`UpdateFrameIndices`和`UpdateInterpolationTimes`功能。`AdjustTime`功能类似于`Clip::AdjustTimeToFitRange`；它确保给定时间有效:\n\n    ```cpp\n    protected:\n        float AdjustTime(float t, float start, \n                    float end, bool looping);\n        void UpdatePlaybackTimes(float dt, bool looping, \n                    float start, float end);\n        void UpdateFrameIndices(float start, \n                    float duration, unsigned int texWidth);\n        void UpdateInterpolationTimes(float start, \n                    float duration, unsigned int texWidth);\n    ```\n\n4.  为人群的大小和每个参与者的`Transform`属性声明 getter 和 setter 函数:\n\n    ```cpp\n    public:\n        unsigned int Size();\n        void Resize(unsigned int size);\n        Transform GetActor(unsigned int index);\n        void SetActor(unsigned int index, \n                      const Transform& t);\n    ```\n\n5.  最后，声明`Update`和`SetUniforms`功能。这些功能将推进当前动画并更新每个实例的着色器制服:\n\n    ```cpp\n        void Update(float deltaTime, Clip& mClip, \n                    unsigned int texWidth);\n        void SetUniforms(Shader* shader);\n    };\n    ```\n\n`Crowd`类提供了一个直观的界面，用于管理人群中每个参与者的每个实例信息。在下一节中，您将开始实现`Crowd`类。\n\n## 实施人群班\n\n`Crowd`类提供了一种方便的方式，让你管理人群中的所有演员。这个类的大部分复杂度是在计算正确的回放信息。这项工作在`Update`功能中完成。`Update`功能使用三个助手功能，即`UpdatePlaybackTimes`、`UpdateFrameIndices`和`UpdateInterpolateionTimes`来工作。\n\n人群中每个演员的当前动画播放时间将存储在`mCurrentPlayTimes`向量中。`mNextPlayTimes`向量是动画中估计的下一个时间，它允许两个采样帧进行插值。`UpdatePlaybackTimes`函数将更新这两个向量。\n\n猜测下一帧的播放时间很重要，因为动画纹理的采样率未知。例如，如果一个动画以 240 FPS 编码，并以 60 FPS 回放，那么下一帧将是四个样本。\n\n`mFrames`向量包含两个分量整数向量。第一个组件是当前动画帧的`u`纹理坐标。第二个组件是将在下一帧中显示的动画帧的`v`纹理坐标。`v`纹理坐标是关节索引。\n\n`UpdateFrameIndex`功能负责更新该向量。要找到当前帧的 *x* 坐标，将帧时间归一化，然后将归一化的帧时间乘以纹理的大小。您可以通过从帧时间中减去开始时间并将结果除以片段的持续时间来规范化帧时间。\n\n着色器将需要在当前动画姿态和下一个动画姿态之间进行插值。为此，它需要知道两个姿势的帧之间的当前归一化时间。这存储在`mTimes`变量中。\n\n`mTimes`变量由`UpdateInterpolationTimes`功能更新。该函数查找当前帧的持续时间，然后将相对于当前帧的播放时间标准化为该持续时间。\n\n要更新`Crowd`类，必须依次调用`UpdatePlaybackTimes`、`UpdateFrameIndices`和`UpdateInterpolateionTimes`函数。完成后，`Crowd`类可以使用`SetUniforms`功能设置其统一值。\n\n创建一个名为`Crowd.cpp`的新文件。`Crowd`类将在该文件中实现。按照以下步骤实施`Crowd`课程:\n\n1.  实现大小获取器和设置器函数。setter 函数需要设置包含在`Crowd`类中的所有向量的`size`:\n\n    ```cpp\n    unsigned int Crowd::Size() {\n        return mCurrentPlayTimes.size();\n    }\n    void Crowd::Resize(unsigned int size) {\n        if (size > CROWD_MAX_ACTORS) {\n            size = CROWD_MAX_ACTORS;\n        }\n        mPositions.resize(size);\n        mRotations.resize(size);\n        mScales.resize(size, vec3(1, 1, 1));\n        mFrames.resize(size);\n        mTimes.resize(size);\n        mCurrentPlayTimes.resize(size);\n        mNextPlayTimes.resize(size);\n    }\n    ```\n\n2.  实现参与者转换的 getter 和 setter 函数。位置、旋转和缩放保持在单独的向量中；actor getter 和 setter 函数隐藏了实现，支持使用`Transform`对象:\n\n    ```cpp\n    Transform Crowd::GetActor(unsigned int index) {\n        return Transform(\n            mPositions[index],\n            mRotations[index],\n            mScales[index] );\n    }\n    void Crowd::SetActor(unsigned int index, \n                         const Transform& t) {\n        mPositions[index] = t.position;\n        mRotations[index] = t.rotation;\n        mScales[index] = t.scale;\n    }\n    ```\n\n3.  执行`AdjustTime`功能；类似于`Clip::AdjustTimeToFitRange`功能:\n\n    ```cpp\n    float Crowd::AdjustTime(float time, float start, \n                            float end, bool looping) {\n        if (looping) {\n            time = fmodf(time - start, end - start);\n            if (time < 0.0f) {\n                time += end - start;\n            }\n            time = time + start;\n        }\n        else {\n            if (time < start) { time = start; }\n            if (time > end) { time = end; }\n        }\n        return time;\n    }\n    ```\n\n4.  实现`UpdatePlaybackTimes`助手功能。该功能将所有演员的播放时间提前δ时间:\n\n    ```cpp\n    void Crowd::UpdatePlaybackTimes(float deltaTime, \n                bool looping, float start, float end) {\n        unsigned int size = mCurrentPlayTimes.size();\n        for (unsigned int i = 0; i < size; ++ i) {\n            float time = mCurrentPlayTimes[i] + deltaTime;\n            mCurrentPlayTimes[i] = AdjustTime(time, start,\n                                            end, looping);\n            time = mCurrentPlayTimes[i] + deltaTime;\n            mNextPlayTimes[i] = AdjustTime(time, start, \n                                          end, looping);\n        }\n    }\n    ```\n\n5.  实现`UpdateFrameIndices`功能。该功能会将当前播放的时间转换为沿动画纹理的 *x* 轴的像素坐标:\n\n    ```cpp\n    void Crowd::UpdateFrameIndices(float start, float duration, unsigned int texWidth) {\n        unsigned int size = mCurrentPlayTimes.size();\n        for (unsigned int i = 0; i < size; ++ i) {\n            float thisNormalizedTime = \n                 (mCurrentPlayTimes[i] - start) / duration;\n            unsigned int thisFrame = \n                 thisNormalizedTime * (texWidth - 1);\n            float nextNormalizedTime = \n                 (mNextPlayTimes[i] - start) / duration;\n            unsigned int nextFrame = \n                 nextNormalizedTime * (texWidth - 1);\n            mFrames[i].x = thisFrame;\n            mFrames[i].y = nextFrame;\n        }\n    }\n    ```\n\n6.  实现`UpdateInterpolationTimes`功能。这个函数应该找到当前和下一个动画帧之间的插值时间:\n\n    ```cpp\n    void Crowd::UpdateInterpolationTimes(float start, \n              float duration, unsigned int texWidth) {\n        unsigned int size =  mCurrentPlayTimes.size();\n        for (unsigned int i = 0; i < size; ++ i) {\n            if (mFrames[i].x == mFrames[i].y) {\n                mTimes[i] = 1.0f;\n                continue;\n            }\n            float thisT = (float)mFrames[i].x / \n                          (float)(texWidth - 1);\n            float thisTime = start + duration * thisT;\n            float nextT = (float)mFrames[i].y / \n                          (float)(texWidth - 1);\n            float nextTime = start + duration * nextT;\n            if (nextTime < thisTime) {\n                nextTime += duration;\n            }\n            float frameDuration = nextTime - thisTime;\n            mTimes[i] = (mCurrentPlayTimes[i] - thisTime) /\n                        frameDuration;\n        }\n    }\n    ```\n\n7.  执行`Update`方法。该方法依赖于`UpdatePlaybackTimes`、`UpdateFrameIndices`和`UpdateInterpolationTimes`助手功能:\n\n    ```cpp\n    void Crowd::Update(float deltaTime, Clip& mClip, \n                            unsigned int texWidth) {\n       bool looping = mClip.GetLooping();\n       float start = mClip.GetStartTime();\n       float end = mClip.GetEndTime();\n       float duration = mClip.GetDuration();\n\n       UpdatePlaybackTimes(deltaTime, looping, start, end);\n       UpdateFrameIndices(start, duration, texWidth);\n       UpdateInterpolationTimes(start, duration, texWidth);\n    }\n    ```\n\n8.  实现`SetUniforms`函数，该函数将包含在`Crowd`类中的向量作为统一数组传递给人群着色器:\n\n    ```cpp\n    void Crowd::SetUniforms(Shader* shader) {\n        Uniform<vec3>::Set(shader->GetUniform(\"model_pos\"),\n                           mPositions);\n        Uniform<quat>::Set(shader->GetUniform(\"model_rot\"), \n                           mRotations);\n        Uniform<vec3>::Set(shader->GetUniform(\"model_scl\"), \n                           mScales);\n        Uniform<ivec2>::Set(shader->GetUniform(\"frames\"), \n                           mFrames);\n        Uniform<float>::Set(shader->GetUniform(\"time\"), \n                           mTimes);\n    }\n    ```\n\n使用`Crowd`类应该是直观的:创建一个人群，设置回放时间和其演员的模型变换，并绘制人群。在下一节中，您将探索如何使用`Crowd`类绘制大量人群的示例。\n\n## 使用人群类\n\n使用`Crowd`类应该是直观的，但是渲染代码可能不会立即显现出来。人群着色器的非实例制服，如视图或投影矩阵，仍需要手动设置。`Crowd`班级的`Set`功能设置的唯一制服是演员制服。\n\n使用`DrawInstanced`方法渲染，而不是使用`Mesh`类的`Draw`方法渲染。对于实例数量参数，传递人群的大小。下面的代码片段显示了如何绘制人群的最小示例:\n\n```cpp\nvoid Render(float aspect) {\n    mat4 projection = perspective(60.0f, aspect, 0.01f, 100);\n    mat4 view=lookAt(vec3(0,15,40), vec3(0,3,0), vec3(0,1,0));\n    mCrowdShader->Bind();\n    int viewUniform = mCrowdShader->GetUniform(\"view\")\n    Uniform<mat4>::Set(viewUniform, view);\n    int projUniform = mCrowdShader->GetUniform(\"projection\")\n    Uniform<mat4>::Set(projUniform, projection);\n    int lightUniform = mCrowdShader->GetUniform(\"light\");\n    Uniform<vec3>::Set(lightUniform, vec3(1, 1, 1));\n    int invBind = mCrowdShader->GetUniform(\"invBindPose\");\n    Uniform<mat4>::Set(invBind, mSkeleton.GetInvBindPose());\n    int texUniform = mCrowdShader->GetUniform(\"tex0\");\n    mDiffuseTexture->Set(texUniform, 0);\n    int animTexUniform = mCrowdShader->GetUniform(\"animTex\");\n    mCrowdTexture->Set(animTexUniform, 1);\n    mCrowd.SetUniforms(mCrowdShader);\n    int pAttrib = mCrowdShader->GetAttribute(\"position\");\n    int nAttrib = mCrowdShader->GetAttribute(\"normal\");\n    int tAttrib = mCrowdShader->GetAttribute(\"texCoord\");\n    int wAttrib = mCrowdShader->GetAttribute(\"weights\");\n    int jAttrib = mCrowdShader->GetAttribute(\"joints\");\n    mMesh.Bind(pAttrib, nAttrib, uAttrib, wAttrib, jAttrib);\n    mMesh.DrawInstanced(mCrowd.Size());\n    mMesh.UnBind(pAttrib, nAttrib, uAttrib, wAttrib, jAttrib);\n    mCrowdTexture->UnSet(1);\n    mDiffuseTexture->UnSet(0);\n    mCrowdShader->UnBind();\n}\n```\n\n在大多数情况下，代码看起来类似于一个规则的蒙皮网格。这是因为特定于实例的制服是由`Crowd`类的`SetUniforms`功能设置的。每隔一套制服都和以前一样。在下一节中，您将探索如何在顶点着色器中混合两个动画。\n\n在本节中，您创建了一个`Crowd`类，它提供了一个易于使用的界面，以便您可以设置`Crowd`着色器所需的制服。还演示了如何使用`Crowd`类来渲染大量人群。\n\n# 混合动画\n\n可以在顶点着色器中混合两个动画。有两个原因可以解释为什么想要避免顶点着色器中动画之间的混合。首先，这样做将使纹理元素提取量翻倍，这将使着色器更加昂贵。\n\n发生这种 texel 提取的爆炸是因为您必须检索姿态矩阵的两个副本——每个动画一个——然后在它们之间混合。这样做的着色器代码可能看起来像下面的代码片段:\n\n```cpp\n    mat4 pose0a = GetPose(animTexA, joints.x, instance);\n    mat4 pose1a = GetPose(animTexA, joints.y, instance);\n    mat4 pose2a = GetPose(animTexA, joints.z, instance);\n    mat4 pose3a = GetPose(animTexA, joints.w, instance);\n    mat4 pose0b = GetPose(animTexB, joints.x, instance);\n    mat4 pose1b = GetPose(animTexB, joints.y, instance);\n    mat4 pose2b = GetPose(animTexB, joints.z, instance);\n    mat4 pose3b = GetPose(animTexB, joints.w, instance);\n    mat4 pose0 = pose0a * (1.0 - fade) + pose0b * fade;\n    mat4 pose1 = pose1a * (1.0 - fade) + pose1b * fade;\n    mat4 pose2 = pose2a * (1.0 - fade) + pose2b * fade;\n    mat4 pose3 = pose3a * (1.0 - fade) + pose3b * fade;\n```\n\n另一个原因是这种混合在技术上不正确。着色器正在世界空间中进行线性混合。生成的混合骨架看起来不错，但与在局部空间内插值关节的效果不同。\n\n如果你在两个姿势之间交叉淡入淡出，混合是短暂的，只是为了隐藏过渡。在大多数情况下，过渡在技术上是否正确并不重要，重要的是过渡看起来是否平稳。在下一节中，您将探索使用替代纹理格式。\n\n# 探索纹理格式\n\n动画纹理目前以 32 位浮点纹理格式存储。这是一种存储动画纹理的简单格式，因为它与源数据的格式相同。这种方法在移动硬件上效果不好。从主存到内存的内存带宽是一种稀缺资源。\n\n针对移动平台，考虑从`GL_RGBA32F`改为`GL_RGBA`，采用`GL_UNSIGNED_BYTE`存储类型。切换到标准纹理格式确实意味着丢失一些数据。使用`GL_UNSIGNED_BYTE`存储类型，一种颜色的每个成分限于 256 个唯一值。这些值在采样时被标准化，并将在 0 到 1 的范围内返回。\n\n如果任何动画信息存储值不在 0 到 1 的范围内，则需要对数据进行规范化。规范化比例因子需要作为一个统一的传递给着色器。如果您的目标是移动硬件，您可能只想存储轮换信息，轮换信息已经在 0 到 1 的范围内。\n\n在下一节中，您将探索如何将多个动画纹理组合成一个纹理。这减少了需要为一群人绑定以播放多个动画的纹理数量。\n\n# 组合动画纹理\n\n将许多较小的纹理组合成一个较大的纹理的行为称为贴图。包含多个较小纹理的大纹理通常称为纹理图谱。贴图的好处是需要使用更少的贴图采样器。\n\n本章介绍的人群渲染系统有一个主要缺点:虽然人群可以在不同的时间偏移播放动画，但他们只能播放相同的动画。有一个简单的方法可以解决这个问题:将多个动画纹理映射到一个大纹理上。\n\n例如，一个 *1024x1024* 纹理可以包含 16 个较小的 *256x256* 纹理。这意味着人群中的任何成员都可以播放 16 个动画中的一个。必须为着色器的每个实例数据添加额外的“偏移”统一。这种偏移一致将是一个`MAX_INSTANCES`大小的数组。\n\n对于正在渲染的每个角色，`GetPose`函数必须在检索动画纹理元素之前应用偏移。在下一节中，您将探索通过最小化纹理元素提取来优化群组着色器的不同技术。\n\n# 优化纹理元素提取\n\n即使在游戏电脑上，渲染超过 200 个人群角色也需要超过 4 毫秒，这是一个相当长的时间，假设你有 16.6 毫秒的帧时间。那么，为什么人群渲染这么贵呢？\n\n每次调用`GetPose`辅助函数时，着色器都会执行 6 次纹理元素提取。因为每个顶点被蒙皮到四个影响，那就是每个顶点 24 个纹理元素提取！即使是低多边形模型，也需要大量的纹理元素提取。优化这个着色器可以归结为最小化纹理元素提取的次数。\n\n以下部分介绍了不同的策略，您可以使用这些策略来最小化每个顶点的纹理元素提取次数。\n\n## 限制影响\n\n优化纹理元素提取的一个简单方法是给着色器代码添加一个分支。毕竟，如果矩阵的权重是 0，为什么还要费心去弄姿势呢？这种优化可以如下实现:\n\n```cpp\n    mat4 pose0 = (weights.x < 0.0001)? \n        mat4(1.0) : GetPose(joints.x, instance);\n    mat4 pose1 = (weights.y < 0.0001)? \n        mat4(1.0) : GetPose(joints.y, instance);\n    mat4 pose2 = (weights.z < 0.0001)? \n        mat4(1.0) : GetPose(joints.z, instance);\n    mat4 pose3 = (weights.w < 0.0001)? \n        mat4(1.0) : GetPose(joints.w, instance);\n```\n\n在最好的情况下，这可能会节省一点时间。在最坏的情况下(每个骨骼正好有四个影响)，这实际上会给着色器增加额外的成本，因为现在，每个影响都有一个条件分支。\n\n限制纹理元素提取的更好方法是限制骨骼影响。诸如 Blender、3DS Max 或 Maya 等 3DCC 工具都有导出选项来限制每个顶点的骨骼影响的最大数量。您应该将骨骼影响的最大数量限制为 1 或 2。\n\n一般来说，在一大群人中，很难辨认出单个演员身上的细微细节。因此，将骨骼影响降低到 1，有效地刚性蒙皮人群，通常是可行的。在下一节中，您将探讨限制动画组件的数量如何有助于减少每个顶点的纹理元素提取次数。\n\n## 限制动画组件\n\n考虑一个动画人物。人体关节只旋转；他们从不翻译或缩放。如果你知道一个动画每个关节只动画一个或两个组件，那么`GetPose`功能可以被编辑以采样更少的数据。\n\n这里还有一个额外的好处:可以编码到动画纹理中的骨骼数量会增加。如果您正在编码位置、旋转和缩放，关节的最大数量是`texture size / 3`。如果只对一个组件进行编码，可以编码的关节数量就是纹理的大小。\n\n该优化将使 *256x256* 纹理能够编码 256 次旋转，而不是 85 次变换。在下一节中，您将探讨是否需要帧间插值。\n\n## 不插值\n\n考虑动画纹理。它以设定的增量对动画进行采样，以填充纹理的每一列。在 256 个样本的情况下，您可以以 60 FPS 编码 3.6 秒的动画。\n\n是否需要插值取决于动画纹理的大小和正在编码的动画的长度。对于大多数游戏中的角色动画，如跑步、行走、附着或死亡，插值不需要帧插值。\n\n通过这种优化，发送到图形处理器的数据量大大减少。统一的帧可以从`ivec2`变为`int`，将数据的大小减半。这意味着时间制服可以完全消失。\n\n在下一节中，您将探索刚刚了解到的三种优化的组合效果是什么。\n\n## 结合这些优化\n\n让我们探索这些优化可能产生的影响，假设实现了以下三个优化:\n\n*   将骨骼影响的数量限制为 2。\n*   仅动画显示变换的旋转组件。\n*   不要在帧间插值。\n\n这将减少纹理元素的提取次数，从每个顶点 24 次减少到每个顶点 2 次。可以编码到动画纹理中的关节数量将会增加，并且每帧传输到图形处理器的数据量将会大大减少。\n\n# 总结\n\n在本章中，您学习了如何将动画数据编码为纹理，以及如何在顶点着色器中解释数据。还介绍了通过改变动画数据的编码方式来提高性能的几种策略。这种将数据写入纹理的技术可用于烘焙任何种类的采样数据。\n\n要烘焙动画，您需要裁剪成纹理。该片段以设定的时间间隔进行采样。每块骨头的整体位置在每个间隔被记录下来，并被写入纹理。在这个动画纹理中，每个关节占用三行:一行用于位置，一行用于旋转，一行用于缩放。\n\n您使用实例化渲染了人群网格，并创建了一个可以从统一数组读取每个实例数据的着色器。每个实例-人群中演员的数据，如位置、旋转和缩放，作为统一数组传递给着色器，并使用实例标识作为这些数组的索引进行解释。\n\n最后，你创建了`Crowd`类。这个实用程序类为管理人群中的参与者提供了一个易于使用的界面。这个类将自动填充人群着色器的每个实例的统一。使用这个类，你可以轻松地创建大量有趣的人群。\n\n这本书的可下载内容中有两个关于本章的示例。`Sample00`是我们在这一章写的全部代码。`Sample01`另一方面，演示了如何在实践中使用这段代码来渲染大量人群。"
  },
  {
    "path": "docs/handson-cpp-game-ani-prog/README.md",
    "content": "# C++ 游戏动画编程实用指南\n\n> 原书：[Hands-On C++ Game Animation Programming](https://libgen.rs/book/index.php?md5=391AF0918E639A334904ED0F8E654D3E)\n> \n> 协议：[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)\n> \n> 阶段：机翻（1）\n> \n> 自豪地采用[谷歌翻译](https://translate.google.cn/)\n> \n> 让开源界提前感受入关。\n\n* [在线阅读](https://ccpp.apachecn.org)\n* [在线阅读（Gitee）](https://apachecn.gitee.io/apachecn-c-cpp-zh/)\n* [ApacheCN 学习资源](http://docs.apachecn.org/)\n\n## 贡献指南\n\n本项目需要校对，欢迎大家提交 Pull Request。\n\n> 请您勇敢地去翻译和改进翻译。虽然我们追求卓越，但我们并不要求您做到十全十美，因此请不要担心因为翻译上犯错——在大部分情况下，我们的服务器已经记录所有的翻译，因此您不必担心会因为您的失误遭到无法挽回的破坏。（改编自维基百科）\n\n## 联系方式\n\n### 负责人\n\n* [飞龙](https://github.com/wizardforcel): 562826179\n\n### 其他\n\n*   在我们的 [apachecn/apachecn-c-cpp-zh](https://github.com/apachecn/apachecn-c-cpp-zh) github 上提 issue.\n*   发邮件到 Email: `apachecn@163.com`.\n*   在我们的 [组织学习交流群](http://www.apachecn.org/organization/348.html) 中联系群主/管理员即可.\n\n## 赞助我们\n\n![](http://data.apachecn.org/img/about/donate.jpg)\n"
  },
  {
    "path": "docs/handson-cpp-game-ani-prog/SUMMARY.md",
    "content": "+   [C++ 游戏动画编程实用指南](README.md)\n+   [零、前言](00.md)\n+   [一、创建游戏窗口](01.md)\n+   [二、实现向量](02.md)\n+   [三、实现矩阵](03.md)\n+   [四、实现四元数](04.md)\n+   [五、实现转换](05.md)\n+   [六、构建抽象渲染器](06.md)\n+   [七、探索 glTF 文件格式](07.md)\n+   [八、创建曲线、帧和轨迹](08.md)\n+   [九、实现动画剪辑](09.md)\n+   [十、网格蒙皮](10.md)\n+   [十一、优化动画流水线](11.md)\n+   [十二、动画之间的融合](12.md)\n+   [十三、实现逆运动学](13.md)\n+   [十四、使用对偶四元数蒙皮](14.md)\n+   [十五、使用实例渲染人群](15.md)\n"
  },
  {
    "path": "docs/handson-func-prog-cpp/00.md",
    "content": "# 零、前言\n\n欢迎来到 C++ 函数式编程的实践之旅！这本书讲的是一个老思想，那就是函数式编程，还有一种经典的编程语言，那就是 C++，最后凝聚力量。\n\n函数式编程从 20 世纪 50 年代就出现了；然而，由于其数学基础，多年来主流软件开发对其兴趣有限。随着多核 CPU 和大数据的出现导致对并行化的需求，随着编程语言设计者对不变性和 lambdas 越来越感兴趣，函数式编程概念已经逐渐被引入到所有主要的编程语言中，包括 C#、Java、PHP、JavaScript、Python 和 Ruby。C++ 从未远离函数式编程，函数指针、函子和 STL 的算法等特性允许许多程序员利用某些构造。然而，从 C++ 11 开始，我们看到了 lambdas 的引入，以及更高阶的函数，如`all_of`、`any_of`和`none_of`。在 C++ 17 中，我们看到了更多的进步，引入了`map`(实现为`transform`)。此外，C++ 20 中的特性非常令人兴奋；例如，允许可组合、轻量级和延迟求值的转换的 ranges 库是对标准的一个很好的补充。\n\n这就引出了你将从这本书中学到的东西。无论您是经验丰富的程序员还是 C++ 初学者，您都将了解函数式编程概念，如何在 C++ 中使用它们，以及它们为什么对管理和改进现有代码库有用。每个想法都将展示清晰的代码样本，并通过单元测试进行验证；我们强烈建议您带着这些代码样本，自己玩一玩。\n\n已经作出特别努力，确保以明确的方式提出每一个想法，并遵循理解的流程；换句话说，我们一直在关注优化您的学习体验。为了做到这一点，我们决定夸大某些结构的使用。例如，示例代码使用了大量 lambdas，因为我们想展示如何使用它们。我们认为，学习函数式编程的最好方法是完全深入 lambdas 的世界和 lambdas 上的操作。我们期望读者将这种方法与生产方法分开；事实上，我建议您先自己试验这些概念，然后在生产代码的小部分上进行试验，然后才使用那些有希望充分发挥其潜力的概念。为了支持这个目标，我们已经记录了在函数上使用操作的多种方法，这样您将拥有足够的工具在各种环境中使用。\n\n值得注意的是，我们经过深思熟虑后决定在本书的大部分内容中介绍 C++ 17 标准。我们不使用外部库(除了单元测试库)，我们坚持语言的标准特性和**标准模板库** ( **STL** )。重点是函数式编程概念以及如何使用极简方法实现它们。唯一的例外是这本书的最后一部分，它着眼于 C++ 和 STL 的未来。我们这样做是因为我们相信，对您来说，理解概念并准备好用最少的工具应用它们比提供大量的实现选项更重要。这省去了本书大部分内容的范围库、对函数式编程的 Boost 库支持，以及很可能是其他可以扩展或简化代码的有用库。我将把它留给读者自己试用，让我们知道它们是如何工作的。\n\n# 这本书是给谁的\n\n这本书是为那些已经知道 C++(包括语言语法、STL 容器和模板元素)并想在工具包中添加更多工具的程序员准备的。读这本书不需要了解函数式编程；我们小心翼翼地用清晰实用的方式解释每一个想法。\n\n但是，您确实需要对来自函数式编程世界的工具集感到好奇。大量的实验将帮助你充分利用这本书，所以我鼓励你玩代码，让我们知道你的发现。\n\n# 这本书涵盖了什么\n\n[第一章](01.html)，*函数式编程入门*，为你介绍函数式编程的基本思想。\n\n[第二章](02.html)、*理解纯函数*，教你函数编程的基本构造块，关注不变性的函数，以及如何用 C++ 编写它们。\n\n[第三章](03.html)、*深入 Lambdas* ，重点介绍 Lambdas 以及如何用 C++ 编写。\n\n[第四章](04.html)*功能组合的思想*，着眼于如何用更高阶的操作组合功能。\n\n[第五章](05.html)、*部分应用和 Currying* ，教你如何在函数上使用两个基本操作——c++ 中的部分应用和 Currying。\n\n[第 6 章](06.html)、*功能思考——从数据输入到数据输出*，向您介绍了另一种组织代码的方式，支持以功能为中心的设计。\n\n[第 7 章](07.html)、*用功能操作*消除重复，是对**不重复自己** ( **DRY** )原理、代码重复的类型和代码的相似性，以及如何使用组合、部分应用、curry 等功能操作编写更多 DRY 代码的概述。\n\n[第 8 章](08.html)、*使用类*提高内聚性，演示了函数如何进化成类，以及类如何变成函数。\n\n[第 9 章](09.html)、*功能编程的测试驱动开发*，关注如何将**测试驱动开发** ( **TDD** )用于功能编程，以及不变性和纯函数如何简化测试。\n\n[第 10 章](10.html)、*性能优化*深入探讨了如何优化以功能为中心的设计性能的具体方法，包括记忆化、尾部递归优化和并行执行。\n\n[第 11 章](11.html)、*基于属性的测试*研究了函数式编程如何支持编写自动化测试的新范例，该范例通过数据生成增强基于示例的测试。\n\n[第 12 章](12.html)、*重构到纯函数并通过纯函数*，解释了如何将任何现有代码重构到纯函数，然后以最小的风险返回到类中。它还关注经典设计模式和一些功能设计模式。\n\n[第 13 章](13.html)、*不变性和架构-事件源*，解释了不变性可以在数据存储层面移动，看了如何使用事件源，并讨论了它的优缺点。\n\n[第 14 章](14.html)、*使用范围库*的延迟求值，深入到令人敬畏的范围库，并演示如何在 C++ 17 和 C++ 20 中使用它。\n\n[第 15 章](15.html)、 *STL 支持和建议*，关注 C++ 17 标准中的 STL 功能特性，以及 C++ 20 中一些有趣的补充。\n\n[第 16 章](16.html)、*标准语言支持和建议*，以函数式编程的基本构造块和在 C++ 17 标准中使用它们的各种选项的概述结束本书。\n\n# 充分利用这本书\n\n这本书假定对 C++ 语法和基本的 STL 容器有很好的了解。然而，它没有假设任何函数编程、函数构造、范畴理论或数学的知识。我们竭尽全力确保每个概念都得到清晰的解释，并且是从实用的、以程序员为中心的角度出发。\n\n我们强烈建议您在阅读完这些章节之后再去玩这些代码，或者在完成一章之后尝试从示例中复制这些代码。更好的是，选择一个编码卡塔(例如，来自[http://codingdojo.org/kata/](http://codingdojo.org/kata/))问题，并尝试使用本书中的技巧来解决它。把阅读和玩弄代码结合起来，你会学到比单纯地阅读理论本身更多的东西。\n\n这本书的大部分内容要求你对代码结构进行不同的思考，有时，这将与你习惯的相反。然而，我们将函数式编程视为您工具包中的另一个工具；它与您已经知道的并不矛盾，相反，它只是为您提供了额外的工具来与您的生产代码一起使用。何时以及如何使用它们是你的决定。\n\n要运行书中的代码示例，您将需要`g++ `和`make`命令。或者，您可以使用任何支持 C++ 17 的编译器运行示例，但是您需要手动运行每个文件。所有的代码示例都是用`make`或`make [specific example]`编译并自动运行的，并在控制台上提供了输出，下面还有一些警告。\n\n来自[第 10 章](10.html)、*性能优化*的内存优化样本，需要与`make allMemoryLogs`或特定目标一起运行，每次目标运行后需要按下键盘，并将在`out/`文件夹中创建日志文件，显示为进程分配的内存的演变。这只会在 Linux 系统上起作用。\n\n反应式编程示例来自[第 10 章](10.html)、*性能优化*，需要用户输入。只要输入数字，程序就会以反应的方式计算它们是否是质数。即使在计算时，程序也应该接收输入。来自[第 16 章](16.html)、*标准语言支持与建议*的代码样本，需要支持 C++ 20 的编译器；此时，使用`g++-8`。您需要单独安装`g++-8`。\n\n# 下载示例代码文件\n\n你可以从你在[www.packt.com](http://www.packt.com)的账户下载这本书的示例代码文件。如果您在其他地方购买了这本书，您可以访问[www.packt.com/support](http://www.packt.com/support)并注册将文件直接通过电子邮件发送给您。\n\n您可以按照以下步骤下载代码文件:\n\n1.  登录或注册[www.packt.com](http://www.packt.com)。\n2.  选择“支持”选项卡。\n3.  点击代码下载和勘误表。\n4.  在搜索框中输入图书的名称，并按照屏幕指示进行操作。\n\n下载文件后，请确保使用最新版本的解压缩文件夹:\n\n*   视窗系统的 WinRAR/7-Zip\n*   zipeg/izp/un ARX for MAC\n*   适用于 Linux 的 7-Zip/PeaZip\n\n这本书的代码包也托管在 GitHub 上，网址为[https://GitHub . com/packt publishing/hand-On-Functional-Programming-with-Cpp](https://github.com/PacktPublishing/Hands-On-Functional-Programming-with-Cpp)。如果代码有更新，它将在现有的 GitHub 存储库中更新。\n\n我们还有来自丰富的图书和视频目录的其他代码包，可在**[【https://github.com/PacktPublishing/】](https://github.com/PacktPublishing/)**获得。看看他们！\n\n# 行动中的代码\n\n请访问以下链接查看正在执行的代码:\n\n[http://bit.ly/2ZPw0KH](http://bit.ly/2ZPw0KH)\n\n# 使用的约定\n\n本书通篇使用了许多文本约定。\n\n`CodeInText`:表示文本中的码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟网址、用户输入和推特句柄。这里有一个例子:“在 STL 中，它是用`find_if`函数实现的。让我们看到它在行动。”\n\n代码块设置如下:\n\n```cpp\nclass Number{\n    public:\n        static int zero(){ return 0; }\n        static int increment(const int value){ return value + 1; }\n}\n```\n\n当我们希望将您的注意力吸引到代码块的特定部分时，相关的行或项目以粗体显示:\n\n```cpp\nFirst call: 1,367 ns < 16,281 ns\nSecond call: 58,045 ns < 890,056 ns Third call: 16,167 ns > 939 ns Fourth call: 1,334 ns > 798 ns\n```\n\nWarnings or important notes appear like this. Tips and tricks appear like this.\n\n# 取得联系\n\n我们随时欢迎读者的反馈。\n\n**一般反馈**:如果你对这本书的任何方面有疑问，在你的信息主题中提到书名，发邮件给我们`customercare@packtpub.com`。\n\n**勘误表**:虽然我们已经尽了最大的努力来保证内容的准确性，但是错误还是会发生。如果你在这本书里发现了一个错误，如果你能向我们报告，我们将不胜感激。请访问[www.packt.com/submit-errata](http://www.packt.com/submit-errata)，选择您的图书，点击勘误表提交链接，并输入详细信息。\n\n**盗版**:如果您在互联网上遇到任何形式的我们作品的非法拷贝，如果您能提供我们的位置地址或网站名称，我们将不胜感激。请通过`copyright@packt.com`联系我们，并提供材料链接。\n\n**如果你有兴趣成为一名作者**:如果有一个你有专长的话题，你有兴趣写或者投稿一本书，请访问[authors.packtpub.com](http://authors.packtpub.com/)。\n\n# 复习\n\n请留下评论。一旦你阅读并使用了这本书，为什么不在你购买它的网站上留下评论呢？然后，潜在的读者可以看到并使用您不带偏见的意见来做出购买决定，我们在 Packt 可以了解您对我们产品的看法，我们的作者可以看到您对他们的书的反馈。谢谢大家！\n\n更多关于 Packt 的信息，请访问[packt.com](http://www.packt.com/)。"
  },
  {
    "path": "docs/handson-func-prog-cpp/01.md",
    "content": "# 一、函数式编程导论\n\n为什么函数式编程有用？在过去的十年里，函数式编程结构在所有主要的编程语言中都出现了。程序员已经享受到了它们的好处——简化的循环、更具表现力的代码和简单的并行化。但它还有更多——与时间脱钩，提供消除重复、可组合性和更简单设计的机会。函数式编程的更高采用率(包括金融领域对 Scala 的大规模采用)意味着一旦你了解并理解了它，你就有了更多的机会。虽然我们将在本书中深入探讨函数式编程，以帮助您学习，但请记住，函数式编程是另一个添加到工具箱中的工具，当问题和上下文合适时，您可以选择使用它。\n\n本章将涵盖以下主题:\n\n*   函数式编程的介绍以及对您已经如何使用函数式构造的检查\n*   结构化循环与功能性循环\n*   不变\n*   面向对象编程 ( **面向对象程序设计**)与功能设计\n*   可组合性和消除重复\n\n# 技术要求\n\n代码使用 g++ 7.3.0 和 c++ 17；为了您的方便，它包括一个`makefile`。你可以在`Chapter01`目录下的 GitHub 资源库([https://GitHub . com/PacktPublishing/动手-函数-用-Cpp 编程](https://github.com/PacktPublishing/Hands-On-Functional-Programming-with-Cpp))中找到它。\n\n# 函数式编程导论\n\n我第一次接触函数式编程是在大学。我是一个对科幻、阅读和编程感兴趣的 20 岁极客；编程是我学术生活的亮点。与 C++、Java、MATLAB 和我们使用的其他一些编程语言相关的一切对我来说都很有趣。不幸的是，我不能对电气工程、电路或编译理论的学科说同样的话。我只想写代码！\n\n基于我的兴趣，函数式编程对我来说应该是一门非常有趣的课程。我们的老师非常热情。我们必须写代码。但是出了点问题——我没有听懂老师告诉我们的话。为什么列表如此有趣？为什么语法如此落后，而且充满了括号？用 C++ 写同样的代码要简单得多，我为什么要用这些东西呢？我最终尝试将所有我知道的编程结构从 BASIC 和 C++ 翻译成 Lisp 和 OCaml。它完全错过了函数式编程的要点，但我通过了这门课程，多年后就忘记了。\n\n我想你们中的许多人都可以理解这个故事，我对此有一个可能的原因。我现在相信我的老师，尽管非常热情，但使用了错误的方法。今天，我明白了函数式编程的核心有一定的优雅，因为它与数学的关系很强。但这种优雅需要一种我 20 岁时没有的洞察力，也就是说，这种感觉是我在多年的各种经历后幸运地建立起来的。现在对我来说很明显，学习函数式编程不应该与读者看到这种优雅的能力有关。\n\n那么，我们可以用什么方法来代替呢？想想过去的我，也就是那个只想写代码的极客，只有一条路可走——看看代码中常见的问题，探索函数式编程如何减少或完全消除它们。另外，从头开始；您已经看到了函数式编程，已经使用了一些概念和构造，甚至可能发现它们非常有用。让我们来看看为什么。\n\n# 函数式编程构造无处不在\n\n大约在我完成大学函数式编程课程 10 年后，我和我的朋友费利克斯聊了聊。作为任何两个极客，我们很少见面，但多年来，我们一直在即时通讯上讨论各种无聊的话题，当然还有编程。\n\n不知何故，函数式编程的话题出现了。费利克斯指出，我最喜欢和最喜欢的编程语言之一 LOGO，实际上是一种函数式编程语言。\n\n**LOGO** is an educational programming language whose main characteristic is utilization of so-called **turtle graphics**.\n\n回想起来很明显；下面是如何编写一个函数，在 KTurtle 版本的 LOGO 中绘制一个正方形:\n\n```cpp\nlearn square {\n    repeat 4 {forward 50 turnright 90}\n}\n```\n\n结果如下图所示:\n\n![](img/45f3ee41-d99c-4630-8595-8f4b1e2aadbb.png)\n\n你能看到我们是如何将两行代码传递给 repeat 函数的吗？那是函数式编程！函数式编程的一个基本原则是，代码只是另一种类型的数据，可以打包在一个函数中并传递给其他函数。我在 LOGO 中使用了这个构造数百次，但没有建立连接。\n\n这种认识让我思考:是否有其他函数式编程结构是我在不知情的情况下使用的？事实证明，是的，有。事实上，作为一名 C++ 程序员，你很可能也使用过它们；让我们看几个例子:\n\n```cpp\nint add(const int base, const int exponent){\n   return pow(base, exponent);\n}\n```\n\n这个函数是推荐的 C++ 代码的典型例子。我第一次从伯特兰·迈耶令人惊叹的书中了解到到处添加`const`的好处:*有效 c++**更有效 c++**有效 STL* 。这种结构运行良好有多种原因。首先，它保护不应该改变的数据成员和参数。其次，它允许程序员通过消除可能的副作用来更容易地推理函数中发生了什么。第三，它允许编译器优化函数。\n\n事实证明，这也是行动不变性的一个例子。正如我们将在以下章节中发现的，函数式编程将不变性置于程序的核心，将所有副作用移到程序的边缘。我们已经知道函数式编程的基本结构；说我们使用函数式编程只是意味着更广泛地使用它！\n\n下面是 STL 的另一个例子:\n\n```cpp\nstd::vector aCollection{5, 4, 3, 2, 1};\nsort (aCollection.begin(), aCollection.end());\n```\n\nSTL 算法有很大的威力；这种力量来自多态性。我在比 OOP 更基本的意义上使用这个术语——它仅仅意味着集合包含什么并不重要，因为只要实现了比较，算法仍然可以正常工作。我不得不承认，当我第一次理解它时，我对这个聪明、有效的解决方案印象深刻。\n\n`sort`函数有一个变体，即使没有实现比较，或者没有像我们希望的那样工作，也允许对元素进行排序；例如，当给我们一个`Name`结构时，如下所示:\n\n```cpp\nusing namespace std;\n\n// Parts of code omitted for clarity\nstruct Name{\n     string firstName;\n     string lastName;\n};\n```\n\n如果我们想按名字对`vector<Name>`容器进行排序，我们只需要一个`compare`函数:\n\n```cpp\nbool compareByFirstName(const Name& first, const Name& second){\n     return first.firstName < second.firstName;\n}\n```\n\n另外，我们需要将其传递给`sort`函数，如下面的代码所示:\n\n```cpp\nint main(){\n    vector<Name> names = {Name(\"John\", \"Smith\"), Name(\"Alex\",\n    \"Bolboaca\")};\n\n    sort(names.begin(), names.end(), compareByFirstName);\n}\n// The names vector now contains \"Alex Bolboaca\", \"John Smith\"\n```\n\n这就构成了一种*高阶函数*。高级函数是使用其他函数作为参数的函数，以便允许更高级别的多态性。祝贺您—您刚刚使用了第二个函数式编程构造！\n\n我甚至会说 STL 是函数式编程的一个很好的例子。一旦你对函数式编程结构有了更多的了解，你就会意识到它们在 STL 中无处不在。其中一些，如函数指针或函子，已经在 C++ 语言中存在了很长时间。事实上，STL 已经经受住了时间的考验，那么为什么不在我们的代码中也使用类似的范例呢？\n\n除了 STL 中存在的函数循环，没有更好的例子来支持这个语句。\n\n# 结构化循环与功能性循环\n\n作为程序员，我们学习的第一件事就是如何编写循环，这并不奇怪。我在 C++ 中的第一个循环是打印从`1`到`10`的数字:\n\n```cpp\nfor(int i = 0; i< 10; ++ i){\n    cout << i << endl;\n}\n```\n\n作为一个好奇的程序员，我认为这个语法是理所当然的，研究了它的特性和复杂性，然后就使用了它。回顾过去，我意识到这个结构有一些不寻常的地方。一、为什么从`0`开始？我被告知这是一个惯例，由于历史原因。然后，`for`循环有三个语句——初始化、条件和增量。对于我们试图实现的目标来说，这听起来有点太复杂了。最后，结束条件迫使我犯了比我愿意承认的更多的错误。\n\n此时，您将意识到 STL 允许您在遍历集合时使用迭代器:\n\n```cpp\nfor (list<int>::iterator it = aList.begin(); it != aList.end(); ++ it)\n      cout << *it << endl;\n```\n\n这肯定比使用光标的`for`循环要好。它避免了一个接一个的错误，也没有`0`常规的恶作剧。然而，手术仍然有很多仪式。更糟糕的是，随着程序复杂性的增加，循环趋于增长。\n\n有一种简单的方法可以显示这种症状。让我们回顾一下我使用循环解决的第一个问题。\n\n让我们考虑一个整数向量，并计算它们的和；简单的实现如下:\n\n```cpp\nint sumWithUsualLoop(const vector<int>& numbers){\n    int sum = 0;\n    for(auto iterator = numbers.begin(); iterator < numbers.end(); \n    ++ iterator){\n        sum += *iterator;\n    }\n    return sum;\n}\n```\n\n要是生产代码这么简单就好了！相反，当我们实现这段代码的时候，我们会得到一个新的需求。我们现在只需要对向量中的偶数求和。嗯，这很简单，对吧？让我们看看下面的代码:\n\n```cpp\nint sumOfEvenNumbersWithUsualLoop(const vector<int>& numbers){\n    int sum = 0;\n    for(auto iterator = numbers.begin(); iterator<numbers.end(); \n    ++ iterator){\n        int number = *iterator;\n        if (number % 2 == 0) sum+= number;\n    }\n    return sum;\n}\n```\n\n如果你认为这就是结局，那就不是。我们现在需要对同一个向量进行三次求和——一次是偶数，一次是奇数，一次是总数。现在让我们添加一些代码，如下所示:\n\n```cpp\nstruct Sums{\n    Sums(): evenSum(0),  oddSum(0), total(0){}\n    int evenSum;\n    int oddSum;\n    int total;\n};\n\nconst Sums sums(const vector<int>& numbers){\n    Sums theTotals;\n    for(auto iterator = numbers.begin(); iterator<numbers.end(); \n    ++ iterator){\n        int number = *iterator;\n        if(number % 2 == 0) theTotals.evenSum += number;\n        if(number %2 != 0) theTotals.oddSum += number;\n        theTotals.total += number;\n    }\n    return theTotals;\n}\n```\n\n我们最初相对简单的循环变得越来越复杂。当我第一次开始专业编程时，我们总是责怪用户和客户不能决定完美的特性，给我们最终的、冻结的需求。然而，这在现实中很少可能；我们的客户每天都从用户与我们编写的程序的互动中学习新的东西。这取决于我们如何清楚地解释这段代码，函数循环也是可能的。\n\n多年后，我学会了 Groovy。作为一种基于 Java 虚拟机的编程语言，Groovy 专注于通过帮助程序员编写更少的代码和避免常见错误来简化他们的工作。下面是如何用 Groovy 编写前面的代码:\n\n```cpp\ndef isEven(value){return value %2 == 0}\ndef isOdd(value){return value %2 == 1}\ndef sums(numbers){\n   return [\n      evenSum: numbers.filter(isEven).sum(),\n      oddSum: numbers.filter(isOdd).sum(),\n      total: numbers.sum()\n   ]\n}\n```\n\n让我们比较一下这两者。没有循环。代码极其清晰。不可能一个接一个地出错。没有计数器，所以，所以，从 `0`开始就没有*怪异。此外，它周围没有脚手架——我只写我想实现的，受过训练的读者很容易理解它。*\n\n虽然 C++ 版本更加冗长，但它允许我们实现相同的目标:\n\n```cpp\nconst Sums sumsWithFunctionalLoops(const vector<int>& numbers){\n    Sums theTotals;\n    vector<int> evenNumbers;\n    copy_if(numbers.begin(), numbers.end(), \n    back_inserter(evenNumbers), isEven);\n    theTotals.evenSum = accumulate(evenNumbers.begin(), \n    evenNumbers.end(), 0);\n\n    vector<int> oddNumbers;\n    copy_if(numbers.begin(), numbers.end(), back_inserter(oddNumbers), \n    isOdd);\n    theTotals.oddSum= accumulate(oddNumbers.begin(), oddNumbers.end(), \n    0);\n\n    theTotals.total = accumulate(numbers.begin(), numbers.end(), 0);\n\n    return theTotals;\n}\n```\n\n尽管如此，还是有很多仪式，以及太多的代码相似性。那么，让我们把它去掉，如下所示:\n\n```cpp\ntemplate<class UnaryPredicate>\nconst vector<int> filter(const vector<int>& input, UnaryPredicate filterFunction){\n    vector<int> filtered;\n    copy_if(input.begin(), input.end(), back_inserter(filtered), \n    filterFunction);\n    return filtered;\n}\n\nconst int sum(const vector<int>& input){\n    return accumulate(input.begin(), input.end(), 0);\n}\n\nconst Sums sumsWithFunctionalLoopsSimplified(const vector<int>& numbers){\n    Sums theTotals(\n        sum(filter(numbers, isEven)),\n        sum(filter(numbers, isOdd)),\n        sum(numbers)\n    ); \n    return theTotals;\n}\n```\n\n我们刚刚用一些更简单、更易读和可组合的函数替换了一个复杂的`for`循环。\n\n那么，这个代码更好吗？嗯，这取决于你对更好的 T2 的定义。我喜欢根据优点和缺点来考虑任何实现。函数循环的优点是简单、易读、减少代码重复和可组合。有什么缺点吗？嗯，我们最初的`for`循环只需要一次通过向量，而我们当前的实现需要三次通过。对于非常大的集合，或者当响应时间和内存使用非常重要时，这可能是一个负担。这绝对值得讨论，我们将在[第 10 章](10.html)、*性能优化、*中更详细地探讨这一点，这一章只关注函数式编程的性能优化。现在，我建议你把重点放在理解函数式编程的新工具上。\n\n为了做到这一点，我们需要重新审视不变性。\n\n# 不变\n\n我们已经明白，在 C++ 中，某种程度的不变性是首选的；常见的例子如下:\n\n```cpp\nclass ...{\n    int add(const int& first, const int& second) const{\n        return first + second;\n    }\n}\n```\n\n`const`关键字清楚地传达了代码的一些重要约束，例如:\n\n*   该函数在返回之前不会更改任何参数。\n*   该函数不会更改其所属类的任何数据成员。\n\n现在让我们想象一下`add`的另一个版本，如下所示\n\n```cpp\nint uglyAdd(int& first, int& second){\n    first = first + second;\n    aMember = 40;\n    return first;\n}\n```\n\n我称之为`uglyAdd`是有原因的——我在编程的时候不能容忍这样的代码！这个函数违反了最小惊喜原则，做了太多事情。阅读函数代码并不能揭示它的意图。想象一下调用者的惊讶，如果不小心，那么，仅仅通过调用一个`add`函数，两件事就发生了变化——一件是传递的参数，第二件是函数所在的类。\n\n虽然这是一个极端的例子，但它有助于论证不变性。不可变函数很无聊；它们接收数据，不改变接收数据中的任何内容，不改变包含它们的类中的任何内容，并返回一个值。然而，当涉及到长时间维护代码时，无聊是好的。\n\n不变性是函数编程中函数的核心属性。当然，你的程序中至少有一部分不能是不可变的——**输入/输出** ( **输入/输出**)。我们将接受输入/输出的本来面目，我们将专注于尽可能增加代码的不变性。\n\n现在，你可能想知道你是否必须彻底重新思考你写程序的方式。你应该忘记所有关于 OOP 的知识吗？不完全是，让我们看看为什么。\n\n# 面向对象与功能设计风格\n\n我工作的一个重要部分是与程序员一起工作，帮助他们改进编写代码的方式。为此，我尽力为复杂的想法想出简单的解释。我对软件设计有一个这样的解释。对我来说，软件设计就是我们构建代码的方式，这样我们就可以为商业目的优化它。\n\n我喜欢这个定义，因为它简单明了。但是有一件事困扰了我，在我开始尝试功能性结构之后；也就是说，函数式编程会产生如下代码:\n\n```cpp\nconst Sums sumsWithFunctionalLoopsSimplified(const vector<int>& numbers){\n    Sums theTotals(\n        sum(filter(numbers, isEven)),\n        sum(filter(numbers, isOdd)),\n        sum(numbers)\n    );\n    return theTotals;\n }\n```\n\n用面向对象的方式编写类似的代码很可能意味着创建类和使用继承。那么，哪种风格更好呢？另外，如果软件设计是关于代码结构的，那么这两种风格是否等价？\n\n首先，我们来看看这两种设计风格真正促进了什么。什么是 OOP？多年来，我相信所有列出面向对象语言以下三个属性的书:\n\n*   包装\n*   遗产\n*   多态性\n\nOOP 背后的思想家艾伦·凯并不真正同意这个列表。对他来说，OOP 是关于许多小对象之间的通信。作为一名生物学专业的学生，他看到了一个组织程序的机会，就像身体组织细胞一样，并允许物体像细胞一样进行交流。他更重视对象而不是类，以及通常列出的面向对象属性的通信。我最好把他的观点总结如下:系统中的动态关系比静态属性更重要。\n\n这改变了很多面向对象的范例。那么，类应该与现实世界相匹配吗？不完全是。它们应该针对真实世界的表现进行优化。我们是否应该专注于拥有清晰的、经过深思熟虑的类层次结构？不，因为这些不如物体之间的交流重要。我们能想到的最小的物体是什么？要么是数据的组合，要么是函数。\n\n最近在 Quora([https://www . Quora . com/not-摆脱邪恶状态-like-Haskells-approach-things-every-programming-they/answer/Alan-Kay-11](https://www.quora.com/Isnt-getting-rid-of-the-evil-state-like-Haskells-approach-something-every-programmer-should-follow/answer/Alan-Kay-11)上的一个回答中，Alan Kay 在回答一个关于函数式编程的问题时陈述了一个有趣的想法。函数式编程源于数学，源于为了实现人工智能而对现实世界进行建模的努力。这一努力解决了以下问题——*亚历克斯在布加勒斯特*和*亚历克斯在伦敦*可能都是真的，但时间点不同。这个建模问题的解决方案是不变性；也就是说，时间成为函数的参数，或者数据结构中的数据成员。在任何程序中，我们都可以将数据变化建模为数据的有时间限制的版本。没有什么能阻止我们将数据建模为小对象，将变化建模为函数。此外，正如我们将在后面看到的，我们可以轻松地将函数转换为对象，反之亦然。\n\n因此，总而言之，艾伦·凯所说的面向对象和函数式编程之间并没有真正的紧张关系。只要我们专注于增加代码的不变性，以及相互通信的小对象，我们就可以将它们结合在一起并互换使用。在接下来的章节中，我们将发现用函数替换类是多么容易，反之亦然。\n\n但是使用 OOP 的方式有很多，与艾伦·凯的设想不同。我在我的客户那里看到了很多 C++ 代码，我已经看到了所有这些——大函数、庞大的类和深度继承层次结构。大多数时候，我被叫的原因是因为设计太难改变，也因为添加新功能会减慢速度。继承是一种非常强的关系，过度使用它会导致强耦合，从而导致难以更改的代码。长方法和长课程更难理解，也更难改变。当然，有些情况下继承和长类是有意义的，但是，一般来说，使用松散耦合的小对象可以实现可变性。\n\n但是类是可以重用的，不是吗？我们能用函数做到吗？接下来我们来看看这个话题。\n\n# 可组合性和消除重复\n\n我们已经看到了一个存在大量重复的例子:\n\n```cpp\nconst Sums sumsWithFunctionalLoops(const vector<int>& numbers){\n    Sums theTotals;\n    vector<int> evenNumbers;\n    copy_if(numbers.begin(), numbers.end(), back_inserter(evenNumbers), \n    isEven);\n    theTotals.evenSum = accumulate(evenNumbers.begin(), \n    evenNumbers.end(), 0);\n\n    vector<int> oddNumbers;\n    copy_if(numbers.begin(), numbers.end(), back_inserter(oddNumbers), \n    isOdd);\n    theTotals.oddSum= accumulate(oddNumbers.begin(), oddNumbers.end(), \n    0);\n\n    theTotals.total = accumulate(numbers.begin(), numbers.end(), 0);\n\n    return theTotals;\n}\n```\n\n我们使用函数减少了它，如下面的代码所示:\n\n```cpp\ntemplate<class UnaryPredicate>\nconst vector<int> filter(const vector<int>& input, UnaryPredicate filterFunction){\n    vector<int> filtered;\n    copy_if(input.begin(), input.end(), back_inserter(filtered), \n    filterFunction);\n    return filtered;\n}\n\nconst int sum(const vector<int>& input){\n    return accumulate(input.begin(), input.end(), 0);\n}\n\nconst Sums sumsWithFunctionalLoopsSimplified(const vector<int>& numbers){\n    Sums theTotals(\n        sum(filter(numbers, isEven)),\n        sum(filter(numbers, isOdd)),\n        sum(numbers)\n    );\n\n    return theTotals;\n}\n```\n\n看到函数是如何以各种方式组成的很有趣；我们有`sum(filter())`叫了两次，`sum()`叫了一次。而且，`filter`可以和多个谓语连用。另外，通过一些工作，我们可以使`filter`和`sum`都具有多态功能:\n\n```cpp\ntemplate<class CollectionType, class UnaryPredicate>\nconst CollectionType filter(const CollectionType& input, UnaryPredicate filterFunction){\n    CollectionType filtered;\n    copy_if(input.begin(), input.end(), back_inserter(filtered), \n    filterFunction);\n    return filtered;\n}\ntemplate<typename T, template<class> class CollectionType>\nconst T sum(const CollectionType<T>& input, const T& init = 0){\n    return accumulate(input.begin(), input.end(), init);\n} \n```\n\n现在很容易用除了`vector<int>`类型的参数来调用`filter`和`sum`。实现并不完美，但它说明了我试图说明的一点，即小的、不可变的函数很容易变得多态和可组合。当我们可以将函数传递给其他函数时，这尤其有效。\n\n# 摘要\n\n我们已经讨论了很多有趣的话题！你刚刚意识到你知道函数式编程的基础。借助`const`关键字，可以用 C++ 编写不可变函数。您已经使用了 STL 中的高级函数。此外，你不必忘记关于 OOP 的任何事情，相反，只要从不同的角度来看待它。最后，我们发现了如何组合小的不可变函数来提供复杂的功能，以及它们如何在 C++ 模板的帮助下变得多态。\n\n现在是时候深入了解函数式编程的构建模块，并学习如何在 C++ 中使用它们了。这包括纯函数、lambdas 以及具有函数组合、currying 或部分函数应用等功能的操作。\n\n# 问题\n\n1.  什么是不可变函数？\n2.  如何编写不可变函数？\n3.  不可变函数如何支持代码简单性？\n4.  不可变函数如何支持简单设计？\n5.  什么是高级功能？\n6.  从 STL 可以举出什么高级函数的例子？\n7.  功能循环相对于结构化循环有什么优势？潜在的缺点是什么？\n8.  从艾伦·凯的角度来看，OOP 是什么？它与函数式编程有什么关系？"
  },
  {
    "path": "docs/handson-func-prog-cpp/02.md",
    "content": "# 二、理解纯函数\n\n纯函数是函数式编程的核心组件。它们是不可变的函数，这使得它们简单且可预测。用 C++ 编写纯函数很容易，但是有几件事你需要注意。由于 C++ 中的函数在默认情况下是可变的，我们需要学习告诉编译器如何防止突变的语法。我们还将探索如何将可变代码与不可变代码分开。\n\n本章将涵盖以下主题:\n\n*   理解什么是纯函数\n*   用 C++ 编写纯函数和使用元组返回多个参数的函数\n*   确保 C++ 纯函数的不变性\n*   理解为什么输入/输出是可变的，需要从纯函数中分离出来\n\n# 技术要求\n\n您将需要一个支持 C++ 17 的 C++ 编译器。我用的是 GCC 7 . 3 . 0 版本。代码示例位于`Chapter02`文件夹中的 GitHub([https://GitHub . com/PacktPublishing/hand-On-Functional-Programming-with-Cpp](https://github.com/PacktPublishing/Hands-On-Functional-Programming-with-Cpp))上，并有一个`makefile`文件，方便您使用。\n\n# 什么是纯函数？\n\n让我们花点时间想想一个简单的日常经历。当你打开电灯开关时，会发生两件事之一:\n\n*   如果灯亮着，它就会熄灭\n*   如果灯灭了，它就会亮\n\n灯开关的行为是高度可预测的。它是如此的可预测，以至于当灯不亮的时候，你会立刻认为出了问题——也就是说，灯泡、保险丝或开关本身出了问题。\n\n以下是一些你在打开或关闭开关时不会想到会发生的事情:\n\n*   你的冰箱不响了\n*   你邻居的灯不亮\n*   你浴室的水槽水开不了\n*   你的手机不会复位\n\n当你打开电灯开关时，为什么会发生这些事情？那将会非常混乱；我们不想生活混乱，对吧？\n\n然而，程序员经常在代码中遇到这种行为。调用函数通常会导致程序状态的改变；当这种情况发生时，我们说一个功能有**副作用**。\n\n函数编程试图通过扩展使用纯函数来减少由状态变化引起的混乱。纯函数是有两个约束的函数:\n\n*   对于相同的参数值，它们总是返回相同的输出值。\n*   它们没有副作用。\n\n让我们探索一下如何编写电灯开关的代码。我们将假设灯泡是一个我们可以称之为的外部实体；就当是我们程序的**输入/输出** ( **I/O** )的输出。结构化/面向对象程序员的自然代码如下所示:\n\n```cpp\nvoid switchLight(LightBulb bulb){\n    if(switchIsOn) bulb.turnOff();\n    else bulb.turnOn();\n}\n```\n\n这个函数发生了两件事。首先，它使用一个不属于参数列表的输入，即`switchIsOn`。其次，它直接对灯泡产生副作用。\n\n那么，纯函数是什么样子的呢？首先，它的所有参数都是可见的:\n\n```cpp\nvoid switchLight(boolean switchIsOn, LightBulb bulb){    if(switchIsOn) \n    bulb.turnOff();\n    else bulb.turnOn();\n}\n```\n\n第二，我们需要消除副作用。我们如何做到这一点？让我们将下一个状态的计算与打开或关闭灯泡的动作分开:\n\n```cpp\nLightBulbSignal signalForBulb(boolean switchIsOn){\n    if(switchIsOn) return LightBulbSignal.TurnOff;\n    else return LightBulbSignal.TurnOn;\n}\n// use the output like this: sendSignalToLightBulb(signalForBulb(switchIsOn))\n```\n\n该函数现在是纯函数，我们将在后面更详细地讨论它；但是，现在让我们将其简化如下:\n\n```cpp\nLightBulbSignal signalForBulb(boolean switchIsOn){\n    return switchIsOn ? LightBulbSignal.TurnOff :    \n    LightBulbSignal.TurnOn;\n}\n// use the output like this: sendSignalToLightBulb(signalForBulb(switchIsOn))\n```\n\n让我们把事情说得更清楚一些(我假设函数是类的一部分):\n\n```cpp\nstatic LightBulbSignal signalForBulb(const boolean switchIsOn){\n    return switchIsOn ? LightBulbSignal.TurnOff :  \n    LightBulbSignal.TurnOn;\n}\n// use the output like this: sendSignalToLightBulb(signalForBulb(switchIsOn))\n```\n\n这个函数非常无趣:它非常容易预测，容易阅读，而且没有副作用。这听起来完全像一个精心设计的电灯开关。此外，这听起来就像我们在几十年内维护大量代码行时想要的那样。\n\n我们现在明白了什么是纯函数，以及它为什么有用。我们还演示了一个将纯函数与副作用(通常是输入/输出)分开的例子。这是一个有趣的概念，但它能把我们带到哪里？我们真的能用这样简单的结构构建复杂的程序吗？我们将在接下来的章节中讨论如何构造纯函数。现在，让我们专注于理解如何用 C++ 编写纯函数。\n\n# C++ 中的纯函数\n\n在前面的例子中，您已经看到了我们在 C++ 中需要用于纯函数的基本语法。你只需要记住以下四个想法:\n\n*   纯功能没有副作用；如果它们是一个类的一部分，它们可以是`static`或`const`。\n*   纯函数不会改变参数，所以每个参数都必须是`const`、`const&`或`const* const`类型。\n*   纯函数总是返回值。从技术上讲，我们可以通过输出参数返回值，但通常只返回值更简单。这意味着纯函数通常没有 void 返回类型。\n*   以上几点都不能保证没有副作用或不变性，但它们让我们更加接近。例如，数据成员可以被标记为可变的，并且`const`方法可以改变它们。\n\n在接下来的部分中，我们将探索如何将纯函数编写为自由函数和类方法。当我们浏览示例时，请记住我们现在正在探索语法，重点是如何使用编译器尽可能接近纯函数。\n\n# 没有参数的纯函数\n\n让我们从简单开始。我们可以使用没有参数的纯函数吗？当然可以。一个例子是当我们需要一个默认值。让我们考虑以下示例:\n\n```cpp\nint zero(){return 0;}\n```\n\n这是一个独立的功能。让我们也理解如何在类中编写纯函数:\n\n```cpp\nclass Number{\n    public:\n        static int zero(){ return 0; }\n}\n```\n\n现在，`static`告诉我们函数不改变任何非静态数据成员。但是，这并不妨碍代码更改`static`数据成员的值:\n\n```cpp\nclass Number{\n    private:\n        static int accessCount;\n    public:\n        static int zero(){++ accessCount; return 0;}\n        static int getCount() { return accessCount; }\n};\nint Number::accessCount = 0;\nint main(){\nNumber::zero();\ncout << Number::getCount() << endl; // will print 1\n}\n```\n\n幸运的是，我们将看到，我们可以通过放置良好的`const`关键词来解决大多数可变状态问题。以下情况也不例外:\n\n```cpp\nstatic const int accessCount;\n```\n\n现在我们已经对如何编写没有参数的纯函数有了一些了解，是时候添加更多的参数了。\n\n# 具有一个或多个参数的纯函数\n\n让我们从一个带有一个参数的纯类方法开始，如下面的代码所示:\n\n```cpp\nclass Number{\n    public:\n        static int zero(){ return 0; }\n        static int increment(const int value){ return value + 1; }\n}\n```\n\n两个参数怎么样？当然，让我们考虑以下代码:\n\n```cpp\nclass Number{\n    public:\n        static int zero(){ return 0; }\n        static int increment(const int value){ return value + 1; }\n        static int add(const int first, const int second){ return first  \n        + second; }\n};\n```\n\n我们可以对引用类型执行同样的操作，如下所示:\n\n```cpp\nclass Number{\n    public:\n        static int zero(){ return 0; }\n        static int increment(const int& value){ return value + 1; }\n        static int add(const int& first, const int& second){ return \n        first + second; }\n};\n```\n\n此外，我们可以对指针类型做同样的事情，尽管语法上有点复杂:\n\n```cpp\nclass Number{\n    public:\n        static int incrementValueFromPointer(const int* const value )   \n        {return *value + 1;}\n};\n```\n\n恭喜——你现在知道如何用 C++ 编写纯函数了！\n\n嗯，算是吧；不幸的是，在 C++ 中实现不变性比我们目前看到的要复杂一点。我们需要更深入地看待各种情况。\n\n# 纯函数和不变性\n\n1995 年的电影《阿波罗 13 号》是我最喜欢的惊悚片之一。它涉及空间、真实故事和多个工程问题。在许多令人难忘的场景中，有一个特别的场景可以教会我们很多关于编程的知识。当宇航员团队正在准备一个复杂的程序时，由汤姆·汉克斯扮演的指挥官注意到，他的同事在一个命令开关上贴了一张贴纸，上面写着*不要翻转这个。*指挥官问他的同事为什么这么做，他的回答是类似于*的意思:我头脑不清楚，我害怕我会翻转这个，把你送上太空。所以，我写这个提醒自己不要犯这个错误。*\n\n如果这项技术适用于宇航员，那么它也应该适用于程序员。幸运的是，当我们做错事时，编译器会告诉我们。然而，我们需要告诉编译器我们想要它检查什么。\n\n毕竟，我们可以编写没有任何`const`或`static`的纯函数。函数纯度不是语法问题，而是一个概念。有合适的贴纸可以防止我们犯错。然而，我们将看到编译器只能做到这一步。\n\n让我们看一下实现前面讨论的增量函数的另一种方法:\n\n```cpp\nclass Number{\n    public:\n        int increment(int value){ return ++ value; }\n};\nint main(){\n    Number number;\n    int output = number.increment(Number::zero());\n    cout << output << endl;\n }\n```\n\n这不是一个纯粹的功能。你能看出为什么吗？答案在下面一行:\n\n```cpp\n int increment(int value){ return ++ value; }\n```\n\n`++ value`不仅增加`value`，还改变输入参数。虽然在这种情况下这不是问题(`value`参数是通过值传递的，所以只有它的副本被修改)，但这仍然是一个副作用。这表明用 C++ 或任何默认情况下不强制不变性的语言编写副作用是多么容易。幸运的是，编译器可以帮助我们，只要我们确切地告诉它我们想要什么。\n\n回想一下之前的实现，如下所示:\n\n```cpp\n static int increment(const int value){ return value + 1; }\n```\n\n如果你试图在这个函数的主体中写入`++ value`或`value++ `，编译器会立即告诉你你正在试图改变一个`const`输入参数。编译器真好，不是吗？\n\n那么通过引用传递的参数呢？\n\n# 不变性和引用传递\n\n问题本可以更糟。想象以下功能:\n\n```cpp\n static int increment(int& value){ return ++ value; }\n```\n\n我们避免了传递值，这需要更多的内存。但是价值会怎么样呢？让我们看看下面的代码:\n\n```cpp\n  int value = Number::zero(); //value is 0\n      cout << Number::increment(value) << endl;\n      cout << value << endl; // value is now 1\n```\n\n`value`参数从`0`开始，但是当我们调用函数时，它是递增的，所以现在它的`value`是`1`。就像每次你开灯，冰箱门就会打开。幸运的是，如果我们只添加一个小的`const`关键词，我们将看到以下内容:\n\n```cpp\nstatic int increment(const int& value) {return value + 1; }\n```\n\n然后，编译器再次很好地告诉我们，我们不能在它的主体中使用`++ value`或`value++ `。\n\n这很酷，但是指针参数呢？\n\n# 不变性和指针\n\n当使用指针作为输入参数时，防止不必要的更改变得更加复杂。让我们看看当我们尝试调用这个函数时会发生什么:\n\n```cpp\n  static int increment(int* pValue)\n```\n\n以下情况可能会改变:\n\n*   `pValue`指向的值可能会改变。\n*   指针可以改变它的地址。\n\n`pValue`所指的值可以在类似的条件下变化，正如我们之前发现的那样。例如，考虑以下代码:\n\n```cpp\n static int increment(int* pValue){ return ++*pValue; }\n```\n\n这将改变指向的值并返回它。为了使其不可能改变，我们需要使用一个位置合适的`const`关键词:\n\n```cpp\n static int increment(int* const pValue){ return *pValue + 1; }\n```\n\n指针地址的更改比您预期的要复杂。让我们看一个会以意想不到的方式表现的例子:\n\n```cpp\nclass Number {\n    static int* increment(int* pValue){ return ++ pValue; }\n}\n\nint main(){\n    int* pValue = new int(10);\n    cout << \"Address: \" << pValue << endl;\n    cout << \"Increment pointer address:\" <<   \n    Number::incrementPointerAddressImpure(pValue) << endl;\n    cout << \"Address after increment: \" << pValue << endl;\n    delete pValue;\n}\n```\n\n在我的笔记本电脑上运行此程序会得到以下结果:\n\n```cpp\nAddress: 0x55cd35098e80\nIncrement pointer address:0x55cd35098e80\nAddress after increment: 0x55cd35098e80\nIncrement pointer value:10\n```\n\n地址不会改变，即使我们使用`++ pValue`在函数中递增它。`pValue++ `也是这样，但为什么会这样呢？\n\n指针地址是一个值，它是通过值传递的，所以函数体中的任何变化都只适用于函数范围。要更改地址，您需要通过引用传递地址，如下所示:\n\n```cpp\n static int* increment(int*& pValue){ return ++ pValue; }\n```\n\n这告诉我们，幸运的是，编写改变指针地址的函数并不容易。我仍然觉得告诉编译器为我执行这条规则更安全:\n\n```cpp\n static int* increment(int* const& pValue){ return ++ pValue; }\n```\n\n当然，这并不妨碍您更改指向的值:\n\n```cpp\n  static int* incrementPointerAddressAndValue(int* const& pValue){\n      (*pValue)++ ;\n      return pValue + 1;\n  }\n```\n\n为了强制值和地址的不变性，您需要使用更多的`const`关键字，如以下代码所示:\n\n```cpp\n  static const int* incrementPointerAddressAndValuePure(const int* \n      const& pValue){\n          (*pValue)++ ;//Compilation error\n          return pValue + 1;\n  }\n```\n\n这涵盖了所有类型的类函数。然而，C++ 允许我们在类外编写函数。那么，`static`在这种情况下还管用吗？(剧透提醒:不完全如你所料)。\n\n# 不变性和非类函数\n\n到目前为止，所有的例子都假设函数是类的一部分。C++ 允许我们编写不属于任何类的函数。例如，我们可以编写以下代码:\n\n```cpp\nint zero(){ return 0; }\nint increment(int& value){ return ++ value; }\nconst int* incrementPointerAddressAndValuePure(const int* const& pValue){\n    return pValue + 1;\n}\n```\n\n你可能已经注意到我们不再使用`static`了。可以使用`static`，但需要注意的是，它与类中的函数有着完全不同的含义。`static`应用于独立功能意味着*不能从不同的翻译单元*使用；所以，如果你把函数写在一个 CPP 文件中，它将只在那个文件中可用，并且它将被链接器忽略。\n\n我们已经讨论了所有类型的类和非类函数。但是有输出参数的函数呢？事实证明，他们需要一些工作。\n\n# 不变性和输出参数\n\n有时候，我们想要一个函数来改变我们传递的数据。**标准模板库***(***)中有很多例子，最容易作为例子提供的是`sort`:**\n\n```cpp\nvector<int> values = {324, 454, 12, 45, 54564, 32};\n     sort(values.begin(), values.end());\n```\n\n然而，这不符合纯函数的思想；`sort`的纯等价物如下:\n\n```cpp\nvector<int> sortedValues = pureSort(values);\n```\n\nI can hear you thinking, *but the STL implementation works in place for optimization reasons, so are pure functions less optimized?* Well, as it turns out, pure functional programming languages, such as Haskell or Lisp, also optimize such operations; a `pureSort` implementation would just move the pointers around and only allocate more memory when one of the pointed values is changed. These are, however, two different contexts; C++ has to support multiple programming paradigms, while Haskell or Lisp optimize for immutability and functional style. We will discuss optimization further in [Chapter 10](10.html), *Performance Optimization*. For now, let's examine how to make these types of functions pure.\n\n我们已经发现了如何处理一个输出参数。但是如何才能写出有多个输出参数的纯函数呢？让我们考虑以下示例:\n\n```cpp\nvoid incrementAll(int& first, int& second){\n    ++ first;\n    ++ second;\n}\n```\n\n这个问题的一个简单解决方案是用`vector<int>`代替这两个参数。但是如果参数有不同的类型会发生什么呢？然后，我们可以使用一个结构。但如果这是我们唯一需要的时候呢？幸运的是，STL 为这个问题提供了一个解决方案，即通过元组:\n\n```cpp\nconst tuple<int, int> incrementAllPure(const int& first, const int&  \n    second){\n        return make_tuple(first + 1, second + 1);\n }\n int main(){\n     auto results = incrementAllPure(1, 2);\n     // Can also use a simplified version\n     // auto [first, second] = incrementAllPure(1, 2);\n     cout << \"Incremented pure: \" << get<0>(results) << endl;\n     cout << \"Incremented pure: \" << get<1>(results) << endl;\n }\n```\n\n元组有许多优点，如下所示:\n\n*   它们可以与多个值一起使用。\n*   这些值可以有不同的数据类型。\n*   它们很容易构建——只需一次函数调用。\n*   它们不需要额外的数据类型。\n\n根据我的经验，当您试图呈现一个具有多个纯输出参数的函数，或者一个返回值和一个输出参数的函数时，元组是一个很好的解决方案。然而，在我弄清楚如何设计它们之后，我经常尝试将它们重构为命名的*结构*或数据类。尽管如此，使用元组是一种非常有用的技术；节约使用。\n\n到现在为止，我们已经使用了很多`static`功能。但是他们不是不好的做法吗？嗯，这取决于很多事情；接下来我们将更详细地讨论这一点。\n\n# 静态函数不是不好的做法吗？\n\n到目前为止，你可能在想纯函数是否好，因为它们与**面向对象编程** ( **OOP** )或干净代码的规则相矛盾，也就是为了避免`static`。然而，直到现在，我们只编写了`static`函数。那么，它们是好是坏呢？\n\n反对使用`static`函数有两种说法。\n\n第一个反对`static`函数的理由是它们隐藏了全局状态。由于`static`函数只能访问`static`值，因此这些值成为全局状态。全局状态是不好的，因为很难理解是谁改变了它，当它的值出乎意料时也很难调试。\n\n但是请记住纯函数的规则——对于相同的输入值，纯函数应该返回相同的输出值。因此，当且仅当函数不依赖于全局状态时，它才是纯函数。即使程序有状态，所有必要的值都会作为输入参数发送给纯函数。不幸的是，我们无法使用编译器轻松地实现这一点；程序员的惯例是避免使用任何类型的全局变量，而是将其转换为参数。\n\n这种情况有一个优势，特别是在使用全局常量时。虽然常量是一种不可变的状态，但是考虑它们的演化也很重要。例如，考虑以下代码:\n\n```cpp\nstatic const string CURRENCY=\"EUR\";\n```\n\n在这里，你应该知道总有一天常数会变成变量，然后你必须改变一堆代码来实现新的需求。我的建议是，通常最好也传入常量。\n\n反对`static`函数的第二个理由是它们不应该是类的一部分。我们将在后面的章节中更详细地讨论这个论点；可以说，目前，类应该将内聚函数分组，有时，纯函数应该整齐地放在一个类中。还有一种方法可以替代将内聚的纯函数分组到一个类中——只需使用一个名称空间。\n\n幸运的是，我们不一定要在类中使用`static`函数。\n\n# 静态函数的替代\n\n我们在前一节中发现了如何使用`static`函数在`Number`类中编写纯函数:\n\n```cpp\nclass Number{\n    public:\n        static int zero(){ return 0; }\n        static int increment(const int& value){ return value + 1; }\n        static int add(const int& first, const int& second){ return  \n        first + second; }\n};\n```\n\n然而，还有另一种选择；C++ 允许我们避免`static`，但保持函数不变:\n\n```cpp\nclass Number{\n    public:\n        int zero() const{ return 0; }\n        int increment(const int& value) const{ return value + 1; }\n        int add(const int& first, const int& second) const{ return \n        first + second; }\n};\n```\n\n每个函数签名后的`const`关键字只是告诉我们函数可以访问`Number`类的数据成员，但永远不能更改。\n\n如果我们稍微修改一下这段代码，我们可以问一个关于类上下文中不变性的有趣问题。如果我们用一个值初始化这个数，并且总是加到初始值上，我们会得到下面的代码:\n\n```cpp\nclass Number{\n    private:\n        int initialValue;\n\n    public:\n        Number(int initialValue) : initialValue(initialValue){}\n        int initial() const{ return initialValue; }\n        int addToInitial(const int& first) const{ return first + \n        initialValue; }\n};\n\nint main(){\n    Number number(10);\n    cout << number.addToInitial(20) << endl;\n}\n```\n\n这里有一个有趣的问题:`addToInitial`函数是纯函数吗？让我们按如下方式检查标准:\n\n*   有副作用吗？不，不是的。\n*   对于相同的输入值，它是否返回相同的输出值？这是一个棘手的问题，因为函数有一个隐藏的参数，即`Number`类或其初始值。然而，没有人能从`Number`班之外改变`initialValue`。换句话说，`Number`类是不可变的。因此，该函数将为相同的`Number`实例和相同的参数返回相同的输出值。\n*   它会改变参数值吗？嗯，它只接收一个参数，它不改变它。\n\n结果是这个函数实际上是纯的。我们将在下一章中发现它也是*部分应用的功能*。\n\n我们之前提到，除了 I/O 之外，程序内部的一切都可以是纯的。那么，我们该如何处理执行 I/O 的代码呢？\n\n# 纯函数和输入输出\n\n看看下面，考虑一下这个函数是否是纯函数:\n\n```cpp\nvoid printResults(){\n    int* pValue = new int(10);\n    cout << \"Address: \" << pValue << endl;\n    cout << \"Increment pointer address and value pure:\" <<    \n    incrementPointerAddressAndValuePure(pValue) << endl;\n    cout << \"Address after increment: \" << pValue << endl;\n    cout << \"Value after increment: \" << *pValue << endl;\n    delete pValue;\n}\n```\n\n好吧，让我们看看——它没有参数，所以没有值被改变。但是与我们前面的例子相比，有些东西是不存在的，也就是说，它不返回值。相反，它调用几个函数，其中至少有一个是纯函数。\n\n那么，它有副作用吗？嗯，是的；几乎每一行代码都有一个:\n\n```cpp\ncout << ....\n```\n\n这一行代码在控制台上写了一行字符串，这是副作用！`cout`基于可变状态，所以不是纯函数。此外，由于其外部依赖性，`cout`可能会失败，导致异常。\n\n我们的程序需要输入输出，那么我们能做什么呢？嗯，这很简单——简单地将可变部分和不可变部分分开。把副作用和非副作用分开，尽量减少不纯的功能。\n\n那么，我们如何在这里实现这一点呢？好吧，有一个纯函数在等着摆脱这个不纯的函数。关键是从问题出发；所以，我们把`cout`分开如下:\n\n```cpp\nstring formatResults(){\n    stringstream output;\n    int* pValue = new int(500);\n    output << \"Address: \" << pValue << endl;\n    output << \"Increment pointer address and value pure:\" << \n    incrementPointerAddressAndValuePure(pValue) << endl;\n    output << \"Address after increment: \" << pValue << endl;\n    output << \"Value after increment: \" << *pValue << endl;\n    delete pValue;\n    return output.str();\n}\n\nvoid printSomething(const string& text){\n    cout << text;\n}\n\nprintSomething(formatResults());\n```\n\n我们已经将`cout`产生的副作用转移到了另一个函数中，并使初始函数的意图更加清晰——它正在格式化某些东西，而不是打印。似乎我们干净利落地把纯函数和不纯函数分开了。\n\n但我们有吗？让我们再次检查`formatResults`。它不像以前那样有副作用。我们正在使用`stringstream`，它可能不是纯的，并且正在分配内存，但是所有这些都是函数本地的。\n\nIs memory allocation a side effect? Can a function that allocates memory be pure? After all, memory allocation may fail. However, it's virtually impossible to avoid some kind of memory allocation in functions. We will accept, therefore, that a pure function may fail if there's some kind of memory failure.\n\n那么，它的产量呢？它会改变吗？嗯，它没有输入参数，但是它的输出可以根据`new`运算符分配的内存地址而变化。所以，它还不是一个纯粹的函数。我们如何使它纯净？这很简单，让我们输入一个参数`pValue`:\n\n```cpp\nstring formatResultsPure(const int* pValue){\n    stringstream output;\n    output << \"Address: \" << pValue << endl;\n    output << \"Increment pointer address and value pure:\" << \n    incrementPointerAddressAndValuePure(pValue) << endl;\n    output << \"Address after increment: \" << pValue << endl;\n    output << \"Value after increment: \" << *pValue << endl;\n    return output.str();\n}\n\nint main(){\n    int* pValue = new int(500);\n    printSomething(formatResultsPure(pValue));\n    delete pValue;\n}\n```\n\n在这里，我们将自己与副作用和易变状态隔离开来。代码不再依赖于输入/输出或`new`运算符。我们的功能是纯粹的，这带来了额外的好处——它只做一件事，更容易理解它做了什么，它是可预测的，我们可以非常容易地测试它。\n\n至于我们有副作用的函数，请考虑下面的代码:\n\n```cpp\nvoid printSomething(const string& text){\n    cout << text;\n}\n```\n\n我认为我们都可以同意，理解它的作用很容易，只要我们的其他功能都是纯粹的，我们就可以放心地忽略它。\n\n总之，为了获得更可预测的代码，我们应该将纯函数和不纯函数分开，并尽可能将不纯函数推到系统的边界。可能有些情况下，这种改变是昂贵的，在你的代码中有不纯的函数是非常好的。只要确保你知道哪些是哪些。\n\n# 摘要\n\n在这一章中，我们探讨了如何用 C++ 编写纯函数。由于您需要记住一些技巧，这里列出了推荐的语法:\n\n*   按值传递的类函数:\n    *   `static int increment(const int value)`\n\n    *   `int increment(const int value) const`\n\n*   通过引用传递的类函数:\n    *   `static int increment(const int& value)`\n\n    *   `int increment(const int&value) const`\n\n*   按值传递指针的类函数:\n    *   `static const int* increment(const int* const value)`\n\n    *   `const int* increment(const int* const value) const`\n\n*   通过引用传递指针的类函数:\n    *   `static const int* increment(const int* const& value)`\n\n    *   `const int* increment(const int* const& value) const`\n\n*   传递值的独立函数:``int increment(const int value)``\n*   通过引用传递的独立函数:`int increment(const int& value)`\n*   按值传递指针的独立函数:`const int* increment(const int* value)`\n*   通过引用传递指针的独立函数:`const int* increment(const int* const& value)`\n\n我们还发现，虽然编译器有助于减少副作用，但它并不总是告诉我们一个函数是否是纯函数。我们总是需要记住编写纯函数时要使用的标准，如下所示:\n\n*   对于相同的输入值，它总是返回相同的输出值。\n*   它没有副作用。\n*   它不改变输入参数的值。\n\n最后，我们看到了如何将副作用(通常与输入/输出相关)与我们的纯功能分开。这非常简单，通常需要传入值和提取函数。\n\n现在是向前迈进的时候了。当我们将功能视为设计中的一流公民时，我们可以在功能方面做得更多。要做到这一点，我们需要了解什么是 lambdas 以及它们是如何有用的。我们将在下一章这样做。\n\n# 问题\n\n1.  什么是纯函数？\n2.  不变性和纯函数有什么关系？\n3.  如何告诉编译器防止对通过值传递的变量进行更改？\n4.  如何告诉编译器防止对通过引用传递的变量进行更改？\n5.  如何告诉编译器防止对通过引用传递的指针地址的更改？\n6.  如何告诉编译器防止指针指向的值发生变化？**"
  },
  {
    "path": "docs/handson-func-prog-cpp/03.md",
    "content": "# 三、深入 lambdas\n\n恭喜你！你刚刚掌握了纯函数的力量！现在是时候进入下一个层次了——类固醇的纯功能，或者传奇的 lambdas。它们存在的时间比物体更长，它们周围有数学理论(如果你喜欢那种东西)，它们非常强大，我们将在本章和下一章中发现。\n\n本章将涵盖以下主题:\n\n*   理解 lambdas 的概念和历史\n*   如何用 C++ 写 lambdas\n*   纯函数与 lambdas 相比如何\n*   如何在类中使用 lambdas\n\n# 技术要求\n\n您将需要一个支持 C++ 17 的 C++ 编译器。代码可以在`Chapter03`文件夹中的 GitHub 存储库中找到。提供了一个`makefile`文件，使您更容易编译和运行代码。\n\n# 什么是λ？\n\n那一年是 1936 年。33 岁的数学家阿隆佐·邱奇发表了他对数学基础的研究。在这样做的时候，他创造了所谓的**λ演算**，这是最近创建的计算领域的模型。在艾伦·图灵的合作下，他将继续证明λ演算相当于图灵机。这一发现的相关性是编程的基础——它意味着我们可以通过使用 lambda 和利用 lambda 演算为现代计算机编写任何程序。这解释了为什么它被称为“T2λ”的原因——数学家们长期以来更喜欢每个符号都有一个希腊字母。但是到底是什么呢？\n\n如果忽略所有的数学符号，λ只是一个**纯函数**，可以应用于变量或值。我们来看一个例子。我们将学习如何用 C++ 编写 lambdas，但是，目前，我将使用 Groovy 语法，因为这是我所知道的最简单的语法:\n\n```cpp\ndef add = {first, second -> first + second}\nadd(1,2) //returns 3\n```\n\n`add`是一个λ。如您所见，这是一个函数，它有两个参数并返回它们的和。由于 Groovy 有可选类型，所以我不必指定参数的类型。另外，我不需要使用`return`语句来返回总和；它将自动返回最后一条语句的值。在 C++ 中，我们不能跳过类型或`return`语句，这将在下一节中发现。\n\n现在，让我们看看 lambda 的另一个属性，即从上下文中捕获值的能力:\n\n```cpp\ndef first = 5\ndef addToFirst = {second -> first + second}\naddToFirst(10) // returns 5 + 10 = 15\n```\n\n在本例中，`first`不是函数的参数，而是上下文中定义的变量。λ*捕捉*变量的值，并在其体内使用。我们可以使用 lambdas 的这个属性来简化代码，或者逐渐重构为不变性。\n\n我们将在以后的章节中探讨如何使用 lambdas 现在，让我们演示如何用 C++ 编写它们，如何确保它们是不可变的，以及如何从上下文中捕获值。\n\n# C++ 中的 Lambdas\n\n我们探索了如何用 Groovy 编写 lambdas。那么，我们可以在 C++ 中使用它们的力量吗？从 C++ 11 开始，引入了一种特定的语法。让我们看看我们的`add` lambda 在 C++ 中会是什么样子:\n\n```cpp\nint main(){\n    auto add = [](int first, int second){ return first + second;};\n    cout << add(1,2) << endl; // writes 3\n}\n```\n\n让我们将语法解包如下:\n\n*   我们的λ以`[]`开头。这个块指定了我们从上下文中捕获的变量，稍后我们将看到如何使用它。因为我们什么都没捕捉到，所以街区是空的。\n*   接下来，我们有了参数列表`(int first, int second)`，就像在任何其他 C++ 函数中一样。\n*   最后，我们编写 lambda 的主体，使用一个返回语句:`{ return first + second; }`。\n\n语法比 Groovy 中的更有仪式感，但感觉像 C++，这是一件好事；一致性帮助我们记住事情。\n\n或者，我们可以使用箭头语法，如以下代码所示:\n\n```cpp\n    auto add = [](int first, int second) -> int { return first +   \n        second;};\n```\n\n自从阿隆佐·邱奇在他的 lambda 演算中使用了这个符号，箭头语法就成了 lambda 的主要内容。除此之外，C++ 要求在 lambda 主体之前有返回类型规范，这可以在涉及类型转换的情况下提供清晰性。\n\n由于其历史，箭头语法以这样或那样的方式存在于所有函数式编程语言中。它在 C++ 中很少有用；然而，如果你想习惯于函数式编程，了解这一点是很有用的。\n\n现在是探索如何从上下文中获取变量的时候了。正如我们之前提到的，都在`[]`区块。\n\n# 捕获变量\n\n那么，如果我们想要捕捉变量呢？在 Groovy 中，我们只是使用了 lambda 范围内的变量。这在 C++ 中是行不通的，因为我们需要指定我们正在捕获哪些变量以及如何捕获它们。因此，如果我们只使用`add`λ中的`first`变量，我们将得到如下编译错误:\n\n```cpp\nint main(){\n    int first = 5;\n    auto addToFirst = [](int second){ return first + second;}; \n    // error: variable 'first' cannot be implicitly captured \n    cout << add(10) << endl;\n}\n```\n\n为了在 C++ 中捕获变量，我们需要在`[]`块内部使用一个捕获说明符。有多种方法可以做到这一点，这取决于你想要什么。最直观的方法是直接写下我们正在捕获的变量的名称。在我们的例子中，由于我们试图捕获第一个变量，我们只需要在 lambda 参数之前添加`[first]`:\n\n```cpp\nint main(){\n    int first = 5;\n    auto addToFirst = [first](int second){ return first + second;};\n    cout << addToFirst(10) << endl; // writes 15\n}\n```\n\n正如我们将看到的，这意味着`first`变量被一个值捕获。由于 C++ 给了程序员很多控制权，我们期望它提供特定的语法来通过引用捕获变量。现在，让我们更详细地探讨捕获语法。\n\n# 通过值和引用捕获变量\n\n我们知道按值捕获变量的说明符只是写变量的名字，也就是`[first]`。这意味着变量被复制了，所以我们浪费了几个字节的内存。解决方案是通过引用捕获变量。捕获说明符的语法非常直观——我们可以只使用变量的名称作为`[&first]`引用:\n\n```cpp\nint main(){\n    int first = 5;\n    auto addToFirstByReference = [&first](int second){ return first + \n        second;};\n    cout << addToFirstByReference(10) << endl; // writes 15\n}\n```\n\n我知道你在想什么:既然是通过引用传递的，lambda 现在能修改`first`变量的值吗？剧透警报——是的，可以。我们将在下一节重新讨论不变性、纯函数和 lambdas。目前，有更多的语法需要学习。例如，如果我们想从上下文中捕获多个变量，我们是否必须将它们都写在捕获说明符中？事实证明，有一些捷径可以帮助你避免这种情况。\n\n# 捕获多个值\n\n那么，如果我们想要捕捉多个值呢？让我们探索一下，如果我们添加五个捕获的值，我们的 lambda 会是什么样子:\n\n```cpp\n    int second = 6;\n    int third = 7;\n    int fourth = 8;\n    int fifth = 9;\n\n    auto addTheFive = [&first, &second, &third, &fourth, &fifth]()   \n    {return first + second + third + fourth + fifth;};\n    cout << addTheFive() << endl; // writes 35\n```\n\n我们现在的语法有点多余，不是吗？我们可以使用默认的捕获说明符来代替。幸运的是，语言设计师也是这么想的；注意λ参数前的`[&]`语法:\n\n```cpp\n    auto addTheFiveWithDefaultReferenceCapture = [&](){return first + second + third + fourth + fifth;};\n    cout << addTheFiveWithDefaultReferenceCapture() << endl; // writes 35\n```\n\n`[&]`语法告诉编译器通过引用从上下文中捕获所有指定的变量。这是*默认的引用捕获*说明符。\n\n如果我们想复制它们的值，我们需要使用*默认的按值捕获*说明符，你必须记住它，因为这是唯一这样使用的地方。请注意λ参数前的`[=]`语法:\n\n```cpp\nauto addTheFiveWithDefaultValueCapture = [=](){return first + \nsecond + third + fourth + fifth;};\ncout << addTheFiveWithDefaultValueCapture() << endl; // writes 35\n```\n\n`[=]`语法告诉编译器，所有变量都将通过复制它们的值来捕获。至少，这是默认的。如果出于某种原因，您希望除`first`之外的所有变量都按值传递，那么您只需将默认值与变量说明符结合起来:\n\n```cpp\nauto addTheFiveWithDefaultValueCaptureForAllButFirst = [=, &first](){return first + second + third + fourth + fifth;};\ncout << addTheFiveWithDefaultValueCaptureForAllButFirst() << endl; // writes 35\n```\n\n我们现在知道如何通过值和引用来捕获变量，以及如何使用默认说明符。这就给我们留下了一种重要的变量——指针。\n\n# 捕获指针值\n\n指针只是简单的值。如果我们想通过值捕获指针变量，我们可以只写它的名称，如下面的代码所示:\n\n```cpp\n    int* pFirst = new int(5);\n    auto addToThePointerValue = [pFirst](int second){return *pFirst + \n        second;};\n    cout << addToThePointerValue(10) << endl; // writes 15\n    delete pFirst;\n```\n\n如果我们想要通过引用捕获指针变量，捕获语法与捕获任何其他类型的变量相同:\n\n```cpp\nauto addToThePointerValue = [&pFirst](int second){return *pFirst + \n    second;};\n```\n\n默认说明符的工作原理与您预期的完全一样；也就是说，`[=]`通过值捕获指针变量:\n\n```cpp\n auto addToThePointerValue = [=](int second){return *pFirst + second;};\n```\n\n相比之下，`[&]`通过引用捕获指针变量，如下面的代码所示:\n\n```cpp\n    auto addToThePointerValue = [&](int second){return *pFirst + \n    second;};\n```\n\n我们将探索通过引用捕获变量会对不变性产生什么影响。但是首先，由于有多种方法来获取 lambda 的变量，我们需要检查我们更喜欢哪一种，以及何时使用它们。\n\n# 我们应该使用什么捕获？\n\n我们已经看到了一些捕获值的选项，如下所示:\n\n*   命名变量以按值捕获它；例如`[aVariable]`\n*   命名变量并在它前面加上引用说明符，以便通过引用捕获它；例如`[&aVariable]`\n*   使用默认值说明符按值捕获所有使用的变量；语法是`[=]`\n*   使用默认引用说明符通过引用捕获所有使用的变量；语法是`[&]`\n\n实际上，我发现使用默认值说明符是大多数情况下的最佳版本。这可能是受我喜欢不会改变捕获值的非常小的 lambdas 的影响。我相信简单很重要；当您有多个选项时，很容易使语法变得比必要的更复杂。仔细考虑每个上下文，使用最简单的语法；我的建议是从`[=]`开始，只有在需要的时候才能更改。\n\n我们已经探索了如何用 C++ 编写 lambdas。我们没有提到的是它们是如何实现的。当前的标准将 lambdas 实现为在堆栈上创建的未知类型的 C++ 对象。像任何 C++ 对象一样，它后面有一个类，该类有一个构造函数、一个析构函数和作为数据成员存储的捕获变量。我们可以将一个λ传递给一个`function<>`对象，在这种情况下`function<>`对象将存储一个λ的副本。而且，*小羊羔用懒人评价*，不像`function<>`对象。\n\nLambdas 似乎是编写纯函数的更简单的方法；那么，lambdas 和纯函数有什么关系呢？\n\n# Lambdas 和纯函数\n\n我们在[第二章](02.html)*了解纯函数*中了解到，纯函数有三个特点:\n\n*   对于相同的参数值，它们总是返回相同的值\n*   它们没有副作用\n*   它们不会改变参数的值\n\n我们还发现，在编写纯函数时，需要注意不变性。这很容易，只要我们记住`const`关键词放在哪里。\n\n那么，lambdas 如何处理不变性呢？我们必须做什么特别的事情还是他们只是工作？\n\n# Lambda 不变性和按值传递参数\n\n让我们从一个非常简单的λ开始，如下所示:\n\n```cpp\nauto increment = [](int value) { \n    return ++ value;\n};\n```\n\n这里，我们通过值传递参数，所以我们不期望在调用 lambda:\n\n```cpp\n    int valueToIncrement = 41;\n    cout << increment(valueToIncrement) << endl;// prints 42\n    cout << valueToIncrement << endl;// prints 41\n```\n\n由于我们复制了值，我们可能会使用一些额外的内存字节和一个额外的赋值。我们可以添加一个`const`关键词，让事情更清楚:\n\n```cpp\nauto incrementImmutable = [](const int value) { \n    return value + 1;\n};\n```\n\n由于`const`说明符，如果 lambda 试图改变`value`，编译器会给出一个错误。\n\n但是我们仍然在通过价值传递论点；通过参考怎么样？\n\n# Lambda 不变性和通过引用参数传递\n\n让我们探索一下当我们称之为 lambda 时对输入参数的影响:\n\n```cpp\nauto increment = [](int& value) { \n    return ++ value;\n};\n```\n\n事实证明，它与您的预期相对接近:\n\n```cpp\nint valueToIncrement = 41;\ncout << increment(valueToIncrement) << endl;// prints 42\ncout << valueToIncrement << endl;// prints 42\n```\n\n这里，lambda 改变了参数的值。这还不够好，所以让我们将其设为不可变，如以下代码所示:\n\n```cpp\nauto incrementImmutable = [](const int& value){\n    return value + 1;\n};\n```\n\n如果 lambda 试图改变`value`，编译器将再次帮助我们获得错误信息。\n\n嗯，这样更好；但是指针呢？\n\n# Lambda 不变性和指针参数\n\n就像我们在[第 2 章](02.html)、*理解纯函数*中看到的一样，关于指针参数有两个问题，如下所示:\n\n*   lambda 能改变指针地址吗？\n*   λ能改变定点值吗？\n\n同样，如果我们按值传入指针，地址没有变化:\n\n```cpp\nauto incrementAddress = [](int* value) { \n    return ++ value;\n};\n\nint main(){\n    int* pValue = new int(41);\n    cout << \"Address before:\" << pValue << endl;\n    cout << \"Address returned by increment address:\" <<   \n    incrementAddress(pValue) << endl;\n    cout << \"Address after increment address:\" << pValue << endl;\n}\n\nOutput:\nAddress before:0x55835628ae70\nAddress returned by increment address:0x55835628ae74\nAddress after increment address:0x55835628ae70\n```\n\n通过引用传递指针会改变这一点:\n\n```cpp\nauto incrementAddressByReference = [](int*& value) { \n    return ++ value;\n};\n\nvoid printResultsForIncrementAddressByReference(){\n    int* pValue = new int(41);\n    int* initialPointer = pValue;\n    cout << \"Address before:\" << pValue << endl;\n    cout << \"Address returned by increment address:\" <<    \n    incrementAddressByReference(pValue) << endl;\n    cout << \"Address after increment address:\" << pValue << endl;\n    delete initialPointer;\n}\n\nOutput:\nAddress before:0x55d0930a2e70\nAddress returned by increment address:0x55d0930a2e74\nAddress after increment address:0x55d0930a2e74\n```\n\n所以，我们再次需要用一个恰当的`const`关键词来保护我们自己不受这种变化的影响:\n\n```cpp\nauto incrementAddressByReferenceImmutable = [](int* const& value) { \n    return value + 1;\n};\n\nOutput:\nAddress before:0x557160931e80\nAddress returned by increment address:0x557160931e84\nAddress after increment address:0x557160931e80\n```\n\n让我们也使该值不变。不出所料，我们需要另一个`const`关键词:\n\n```cpp\nauto incrementPointedValueImmutable = [](const int* const& value) { \n    return *value + 1;\n};\n```\n\n虽然这是可行的，但我建议您支持一种更简单的传递`[](const int& value)`值的方法——也就是说，只需取消引用指针，并将一个实际值传递给 lambda，这将使参数语法更容易理解，并且更具可重用性。\n\n所以，没有惊喜！我们可以使用与纯函数相同的语法来确保不变性。\n\n但是 lambdas 可以调用可变函数吗，比如 I/O？\n\n# lambdas 和输入输出\n\n还有什么比`Hello, world`程序更好的测试 lambdas 和 I/O 的方法:\n\n```cpp\nauto hello = [](){cout << \"Hello, world!\" << endl;};\n\nint main(){\n    hello();\n}\n```\n\n显然，lambdas 没有被保护起来，不能调用可变函数。这并不奇怪，因为我们对纯函数也学到了同样的东西。这意味着，与纯函数类似，程序员需要格外注意将 I/O(从根本上来说是可变的)与代码的其余部分(可以是不可变的)分开。\n\n既然我们试图让编译器帮助我们实现不变性，我们能为捕获的值做到这一点吗？\n\n# Lambda 不变性和捕获值\n\n我们发现 lambdas 可以通过值和引用从上下文中捕获变量。那么，这是否意味着我们可以改变它们的价值？让我们来看看，如下所示:\n\n```cpp\nint value = 1;\nauto increment = [=](){return ++ value;};\n```\n\n这段代码会立即给你一个编译错误——*不能赋值给被复制*捕获的变量。这是对按值传递参数的改进；也就是说，没有必要使用`const`关键字——它只是按预期工作。\n\n# 引用捕获的值的不变性\n\n那么，引用捕获的值呢？嗯，我们可以只使用默认的引用说明符`[&]`，并在调用我们的`increment` lambda 之前和之后检查变量的值:\n\n```cpp\nvoid captureByReference(){\n    int value = 1;\n    auto increment = [&](){return ++ value;};\n\n    cout << \"Value before: \" << value << endl;\n    cout << \"Result of increment:\" << increment() << endl;\n    cout << \"Value after: \" << value << endl;\n}\n\nOutput:\nValue before: 1\nResult of increment:2\nValue after: 2\n```\n\n不出所料，`value`发生了变化。那么，我们如何防范这种突变呢？\n\n不幸的是，没有简单的方法可以做到这一点。C++ 假设如果你通过引用获取变量，你想修改它们。虽然这是可能的，但它需要更多的语法糖。具体来说，我们需要将其强制转换为`const`类型，而不是变量:\n\n```cpp\n#include <utility>\nusing namespace std;\n...\n\n    int value = 1;\n    auto increment = [&immutableValue = as_const(value)](){return  \n        immutableValue + 1;};\n\nOutput:\nValue before: 1\nResult of increment:2\nValue after: 1\n```\n\n如果可以选择，我更喜欢使用更简单的语法。因此，我宁愿使用按值捕获语法，除非我真的需要优化性能。\n\n我们已经探索了如何在捕获值类型时使 lambdas 不可变。但是在捕获指针类型时，我们能保证不变性吗？\n\n# 由值捕获的指针的不变性\n\n当我们使用指针时，事情变得有趣起来。如果我们按值捕获它们，我们就不能修改地址:\n\n```cpp\n    int* pValue = new int(1);\n    auto incrementAddress = [=](){return ++ pValue;}; // compilation \n    error\n```\n\n但是，我们仍然可以修改指向的值，如下面的代码所示:\n\n```cpp\n    int* pValue = new int(1);\n    auto increment= [=](){return ++(*pValue);};\n\nOutput:\nValue before: 1\nResult of increment:2\nValue after: 2\n```\n\n约束不变性需要`const int*`类型的变量:\n\n```cpp\n    const int* pValue = new int(1);\n    auto increment= [=](){return ++(*pValue);}; // compilation error\n```\n\n但是，有一个更简单的解决方案，那就是只捕获指针的值:\n\n```cpp\n int* pValue = new int(1);\n int value = *pValue;\n auto increment = [=](){return ++ value;}; // compilation error\n```\n\n# 引用捕获的指针的不变性\n\n通过引用捕获指针也允许您更改内存地址:\n\n```cpp\n auto increment = [&](){return ++ pValue;};\n```\n\n我们可以使用与之前相同的技巧来加强内存地址的恒定性质:\n\n```cpp\n auto increment = [&pImmutable = as_const(pValue)](){return pImmutable \n    + 1;};\n```\n\n然而，这变得相当复杂。这样做的唯一原因如下:\n\n*   我们希望避免最多复制 64 位\n*   编译器没有为我们优化它\n\n更简单的方法是坚持按值传递的值，也就是说，除非你想在你的 lambda 中做指针运算。\n\n你现在知道 lambdas 如何以不变性工作了。但是，在我们的 C++ 代码中，我们习惯了类。那么，lambdas 和类之间是什么关系呢？我们可以一起用吗？\n\n# Lambdas 和类\n\n到目前为止，我们已经学会了如何用 C++ 编写 lambdas。所有的例子都使用类外的 lambda 表达式，或者作为变量，或者作为`main()`函数的一部分。然而，我们大多数的 C++ 代码都存在于类中。这就引出了一个问题——我们如何在课堂上使用 lambdas？\n\n为了探究这个问题，我们需要一个简单类的例子。让我们使用一个表示基本虚数的类:\n\n```cpp\nclass ImaginaryNumber{\n    private:\n        int real;\n        int imaginary;\n\n    public:\n        ImaginaryNumber() : real(0), imaginary(0){};\n        ImaginaryNumber(int real, int imaginary) : real(real), \n        imaginary(imaginary){};\n};\n```\n\n我们想用我们新发现的 lambda 超能力写一个简单的`toString`函数，如下面的代码所示:\n\n```cpp\nstring toString(){\n    return to_string(real) + \" + \" + to_string(imaginary) + \"i\";\n}\n```\n\n那么，我们有什么选择呢？\n\n嗯，lambdas 是简单的变量，所以它们可以是数据成员。或者，它们可以是`static`变量。也许我们甚至可以将类函数转换成 lambdas。接下来让我们探索这些想法。\n\n# 作为数据成员的 Lambdas\n\n让我们首先尝试将其写成成员变量，如下所示:\n\n```cpp\nclass ImaginaryNumber{\n...\n    public:\n        auto toStringLambda = [](){\n            return to_string(real) + \" + \" + to_string(imaginary) +  \n             \"i\";\n        };\n...\n}\n```\n\n不幸的是，这会导致编译错误。如果我们想让 lambda 变量成为非静态数据成员，我们需要指定它的类型。为了做到这一点，让我们把λ包装成一个`function`类型，如下所示:\n\n```cpp\ninclude <functional>\n...\n    public:\n        function<string()> toStringLambda = [](){\n            return to_string(real) + \" + \" + to_string(imaginary) +    \n            \"i\";\n        };\n```\n\n函数类型有一个特殊的语法，允许我们定义 lambda 类型。`function<string()>`符号表示函数返回一个`string`值，并且不接收任何参数。\n\n然而，这仍然不起作用。我们收到另一个错误，因为我们没有捕获我们正在使用的变量。我们可以使用到目前为止了解到的任何捕获。或者，我们可以改为捕捉`this`:\n\n```cpp\n function<string()> toStringLambda = [this](){\n     return to_string(real) + \" + \" + to_string(imaginary) + \n     \"i\";\n };\n```\n\n因此，这就是我们如何编写一个 lambda 作为类的一部分，同时捕获类的数据成员。在重构现有代码时，捕捉`this`是一个有用的捷径。然而，我会在更持久的情况下避免它。最好直接捕获所需的变量，而不是整个指针。\n\n# 作为静态变量的 Lambdas\n\n我们也可以将λ定义为一个`static`变量。我们不能再捕获这些值，所以我们需要传入一个参数，但是我们仍然可以访问`real`和`imaginary`私有数据成员:\n\n```cpp\n    static function<string(const ImaginaryNumber&)>   \n         toStringLambdaStatic;\n...\n// after class declaration ends\nfunction<string(const ImaginaryNumber&)> ImaginaryNumber::toStringLambdaStatic = [](const ImaginaryNumber& number){\n    return to_string(number.real) + \" + \" + to_string(number.imaginary)  \n        + \"i\";\n};\n\n// Call it\ncout << ImaginaryNumber::toStringLambdaStatic(Imaginary(1,1)) << endl;\n// prints 1+1i\n```\n\n# 将静态函数转换为 lambda\n\n有时候，我们需要把一个`static`函数转换成一个 lambda 变量。这在 C++ 中非常容易，如下面的代码所示:\n\n```cpp\nstatic string toStringStatic(const ImaginaryNumber& number){\n    return to_string(number.real) + \" + \" + to_string(number.imaginary)  \n    + \"i\";\n }\nstring toStringUsingLambda(){\n    auto toStringLambdaLocal = ImaginaryNumber::toStringStatic;\n    return toStringLambdaLocal(*this);\n}\n```\n\n我们可以简单地将一个函数从一个类赋给一个变量，正如您在前面的代码中看到的:\n\n```cpp\n  auto toStringLambdaLocal = ImaginaryNumber::toStringStatic;\n```\n\n然后，我们可以像使用函数一样使用变量。正如我们将发现的，这是一个非常强大的概念，因为它允许我们编写函数，即使它们是在类中定义的。\n\n# Lambdas 和耦合\n\n当涉及到 lambdas 和类之间的交互时，我们有很多选择。它们会变得势不可挡，并且会使设计决策变得更加困难。\n\n虽然知道这些选项很好，因为它们在经历困难的重构时会有所帮助，但我通过实践发现，在涉及 lambdas 时，最好遵循一个简单的原则；也就是说，选择减少 lambda 和代码其余部分之间耦合面积的选项。\n\n例如，我们已经看到，我们可以将我们的 lambda 写成一个类中的`static`变量:\n\n```cpp\nfunction<string(const ImaginaryNumber&)> ImaginaryNumber::toStringLambdaStatic = [](const ImaginaryNumber& number){\n    return to_string(number.real) + \" + \" + to_string(number.imaginary)  \n        + \"i\";\n};\n```\n\n该λ具有与`ImaginaryNumber`类一样大的耦合面积。但是，它只需要两个值:实部和虚部。我们可以很容易地将其重写为纯函数，如下所示:\n\n```cpp\nauto toImaginaryString = [](auto real, auto imaginary){\n    return to_string(real) + \" + \" + to_string(imaginary) + \"i\";\n};\n```\n\n如果出于某种原因，您决定通过添加成员或方法、移除成员或方法、将其拆分为多个类或更改数据成员类型来更改虚数的表示，则不需要更改这个 lambda。当然，它需要两个参数而不是一个，但是参数类型不再重要，只要`to_string`对它们起作用。换句话说，这是一个多态函数，让您可以选择表示数据结构。\n\n但是我们将在接下来的章节中讨论如何使用 lambdas 进行设计。\n\n# 摘要\n\n你刚刚获得了λ超能力！不仅可以用 C++ 编写简单的 lambdas，还知道以下内容:\n\n*   如何从上下文中获取变量\n*   如何通过引用或值来指定默认捕获类型\n*   如何编写不可变的 lambdas，即使在捕获值时\n*   如何在课堂上使用 lambdas\n\n我们还谈到了低耦合的设计原则，以及 lambdas 如何对此有所帮助。我们将在接下来的章节中继续提到这个原则。\n\n如果我告诉你兰姆达斯比我们目前看到的还要强大，你会相信我吗？嗯，我们会发现我们可以通过功能组合从简单到复杂的 lambdas。\n\n# 问题\n\n1.  你能写的最简单的 lambda 是什么？\n2.  如何编写一个 lambda 来连接作为参数传递的两个字符串值？\n3.  如果其中一个值是被值捕获的变量，会发生什么？\n4.  如果其中一个值是被引用捕获的变量，会发生什么？\n5.  如果其中一个值是被值捕获的指针，会发生什么？\n6.  如果其中一个值是被引用捕获的指针，会发生什么？\n7.  如果使用默认捕获说明符按值捕获两个值，会发生什么？\n8.  如果使用默认捕获说明符通过引用捕获两个值，会发生什么？\n9.  在有两个字符串值作为数据成员的类中，如何将相同的 lambda 编写为数据成员？\n10.  怎么能把同一个 lambda 写成同一个类中的`static`变量？"
  },
  {
    "path": "docs/handson-func-prog-cpp/04.md",
    "content": "# 四、函数组合思想\n\n在过去的章节中，我们已经看到了如何编写纯函数和 lambdas。这些是函数式编程的基本组件。是时候让他们更上一层楼了。\n\n在本章中，我们将学习如何从现有的函数中获取更多的函数，从而从我们到目前为止看到的简单例子中构建复杂的行为。\n\n本章将涵盖以下主题:\n\n*   用 C++ 编写函数\n*   多参数函数的基本分解策略\n*   使用功能组合消除重复(或代码相似性)\n\n# 技术要求\n\n您将需要一个支持 C++ 17 的编译器。我用的是 GCC 7.3.0。\n\n代码在[网站上的`Chapter04`文件夹中。它包含并使用了`doctest`，这是一个单头开源单元测试库。你可以在它的 GitHub 储存库中找到它:https://github.com/onqtam/doctest](https://github.com/PacktPublishing/Hands-On-Functional-Programming-with-Cpp)。\n\n# 什么是功能成分？\n\n纯函数和 lambdas 是函数式编程的基本模块。但是到目前为止，我们看到的所有例子都使用非常简单的函数。显然，我们处理的是我们行业中复杂得多的问题。然而，正如我们所看到的，我们仍然希望我们的基本块非常简单，因为我们希望容易理解和维护它们。那么，我们如何从目前看到的简单 lambdas 和纯函数中创建复杂的程序呢？函数式编程有一个简单的答案——让我们通过组合现有的简单函数来创建更复杂的函数。函数编程中创建复杂函数的基本方法是函数组合。\n\n# 操作组合\n\n其核心，功能构成非常简单。我们将用一个基本的例子来说明它。我们将从我们的`increment`功能开始。此外，从现在开始，我将使用测试用例来展示代码是如何工作的。我用的是`doctest`，一个单头开源单元测试库([https://github.com/onqtam/doctest](https://github.com/onqtam/doctest))。\n\n让我们用一个测试用例来看看我们的`increment`函数:\n\n```cpp\nauto increment = [](const int value) { return value + 1; };\n\nTEST_CASE(\"Increments value\"){\n    CHECK_EQ(2, increment(1));\n}\n```\n\n我们还可以说，出于某种原因，我们需要将该值增加两次。因为我们在考虑函数，所以我们想重用我们的函数。因此，我们可以称之为两次:\n\n```cpp\nTEST_CASE(\"Increments twice\"){\n    CHECK_EQ(3, increment(increment(1)));\n}\n```\n\n如果我们在一个地方只需要双倍的增量，这很好。如果我们在代码的多个地方需要它，我们将需要一个函数。提取执行双增量的函数非常容易:\n\n```cpp\nauto incrementTwiceLambda = [](int value){return increment(increment(value));};\n\nTEST_CASE(\"Increments result of addition with lambda\"){\n    CHECK_EQ(3, incrementTwiceLambda(1));\n}\n```\n\n如果我们看一下`incrementTwiceLambda`，我们可以看到它是由`increment`的结果调用`increment`形成的。\n\n让我们暂时停下来，继续另一个案子。我们现在想计算一个数的平方，仍然使用函数。很容易写，再一次:\n\n```cpp\nauto square = [](int value){ return value * value; };\n\nTEST_CASE(\"Squares the number\"){\n    CHECK_EQ(4, square(2));\n}\n```\n\n我们的下一个要求是计算一个值的增量平方。再一次，我们可以根据需要提取结合`increment`和`square`的λ:\n\n```cpp\nauto incrementSquareLambda = [](int value) { return increment(square(value));};\n\nTEST_CASE(\"Increments the squared number\"){\n    CHECK_EQ(5, incrementSquareLambda(2));\n}\n\n```\n\n那很好。然而，我们在代码中有一个隐藏的相似之处。我们来看看`incrementTwiceLambda`和`incrementSquareLambda`功能:\n\n```cpp\nauto incrementTwiceLambda = [](int value){ return increment(increment(value)); };\nauto incrementSquareLambda = [](int value) { return increment(square(value)); };\n```\n\n它们都有相同的模式——我们创建了一个函数 *C* ，通过拥有一个函数， *f* 调用另一个函数的结果， *g* ，应用于传递给我们函数的值， *C* 。这是一种代码相似性，当我们使用小的纯函数时，我们可以期望看到很多。如果有一个名字，甚至有一种实现它的方法，而不用写那么多样板代码，那就太好了。\n\n事实证明，它确实有一个名字——这是功能组合。一般来说，对于任何单参数的 *f* 或 *g* 函数，我们可以得到一个函数， *C* ，如下所示:\n\n![](img/332f1ee6-da2c-45e7-8605-58a475e6b52f.png)表示对于 *x* ，![](img/fc102ff6-21a4-45e8-aa49-1b6bf0f9daf7.png)的每个值。\n\n![](img/14204b22-3e95-49e7-b99f-9726698eca8d.png)符号是函数合成的数学运算符。\n\n如您所见，我们实际上正在尝试通过对函数本身进行操作来从其他函数中获取函数！这是一种使用 lambdas 而不是数字的微积分，定义了对 lambdas 的运算。Lambda 微积分是个恰当的名字，你不觉得吗？\n\n这就是功能组合的概念。下一个问题是——我们能消除样板代码吗？\n\n# 用 C++ 实现函数式组合\n\n如果有一个允许我们执行功能合成的操作符就好了。事实上，其他编程语言提供了一种；例如，在 Groovy 中，我们可以使用`<<`运算符，如下所示:\n\n```cpp\ndef incrementTwiceLambda = increment << increment\ndef incrementSquareLambda = increment << square\n```\n\n不幸的是，C++ 还没有一个标准的函数组合运算符。然而，C++ 是一种强大的语言，所以应该可以编写我们自己的函数来执行功能组合，至少在有限的情况下是这样。\n\n首先，让我们明确定义问题。我们希望有一个`compose`函数接收两个 lambda，`f`和`g`，并返回一个调用`value -> f(g(value)`的新 lambda。C++ 中最简单的实现如下所示:\n\n```cpp\nauto compose(auto f, auto g){\n    return [f, g](auto x){ return f(g(x); };\n}\n\nTEST_CASE(\"Increments twice with composed lambda\"){\n    auto incrementTwice = compose(increment, increment);\n    CHECK_EQ(3, incrementTwice(1));\n}\n```\n\n不幸的是，这段代码无法编译，因为 C++ 不允许带有`auto`类型的参数。一种方法是指定函数类型:\n\n```cpp\nfunction<int(int)> compose(function<int(int)> f,  function<int(int)> g){\n    return [f, g](auto x){ return f(g(x); };\n}\n\nTEST_CASE(\"Increments twice with composed lambda\"){\n    auto incrementTwice = compose(increment, increment);\n    CHECK_EQ(3, incrementTwice(1));\n}\n```\n\n这很好，通过了测试。但是现在我们的`compose`功能取决于功能类型。那不是很有用，因为我们将不得不为我们需要的每种类型的功能重新实现`compose`。它比以前更少的样板，但仍然离理想很远。\n\n但这正是 C++ 模板解决的问题类型。也许他们可以帮忙:\n\n```cpp\ntemplate <class F, class G>\nauto compose(F f, G g){\n    return [=](auto value){return f(g(value));};\n}\n\nTEST_CASE(\"Increments twice with composed lambda\"){\n    auto incrementTwice = compose(increment, increment);\n    CHECK_EQ(3, incrementTwice(1));\n}\n\nTEST_CASE(\"Increments square with composed lambda\"){\n    auto incrementSquare = compose(increment, square);\n    CHECK_EQ(5, incrementSquare(2));\n}\n```\n\n的确，这个代码起作用了！所以，我们现在知道，虽然 C++ 中没有函数组合的运算符，但我们可以用一个优雅的函数来实现它。\n\n请注意 compose 是如何返回 lambda 的，它使用了惰性计算。因此，我们的函数组合函数也使用了惰性求值。这是一个优势，因为合成的 lambda 只会在我们使用它时被初始化。\n\n# 函数组合是不可交换的\n\n重要的是要认识到函数组合是不可交换的。确实，很容易理解为什么我们说话的时候——*一个值的增量平方*不同于*对一个值的增量平方*。但是，我们在代码中需要小心，因为两者的区别仅在于 compose 函数的参数顺序:\n\n```cpp\nauto incrementSquare = compose(increment, square);\nauto squareIncrement = compose(square, increment);\n```\n\n我们已经看到了什么是功能组合，如何在 C++ 中实现它，以及如何在简单的情况下使用它。我打赌你现在很想尝试一下更复杂的程序。我们会到达那里，但首先让我们看看更复杂的情况。多参数函数呢？\n\n# 复杂功能成分\n\n我们的复合函数有一个问题——它只适用于接收一个参数的 lambdas。那么，如果我们想用多个参数组合函数，我们该怎么做呢？\n\n让我们举下面的例子——给定两个λ，`multiply`和`increment`:\n\n```cpp\nauto increment = [](const int value) { return value + 1; };\nauto multiply = [](const int first, const int second){ return first * second; };\n```\n\n我们能得到一个增加乘法结果的λ吗？\n\n不幸的是，我们不能使用`compose`函数，因为它假设两个函数都有一个参数:\n\n```cpp\ntemplate <class F, class G>\nauto compose(F f, G g){\n    return [=](auto value){return f(g(value));};\n}\n```\n\n那么，我们有什么选择？\n\n# 实现更多的合成功能\n\n我们可以实现`compose`函数的一个变体，它接受一个函数`f`，这个函数接受一个参数，另一个函数`g`，这个函数接受两个参数:\n\n```cpp\ntemplate <class F1, class G2>\nauto compose12(F1 f, G2 g){\n    return [=](auto first, auto second){ return f(g(first, second)); };\n}\n\nTEST_CASE(\"Increment result of multiplication\"){\n    CHECK_EQ(5, compose12(increment, multiply)(2, 2));\n}\n```\n\n这个解决方案足够简单。然而，如果我们需要获得一个将其参数的递增值相乘的函数，我们还需要另一个`compose`变量:\n\n```cpp\ntemplate <class F2, class G1>\nauto compose21(F2 f, G1 g){\n    return [=](auto first, auto second){ return f(g(first), g(second)); };\n}\n\nTEST_CASE(\"Multiplies two incremented values\"){\n    CHECK_EQ(4, compose21(multiply, increment)(1, 1));\n}\n```\n\n如果我们只想增加其中一个参数呢？有很多可能的组合，虽然我们可以用 compose 的多种变体来覆盖它们，但也值得访问其他选项。\n\n# 用多个参数分解函数\n\n我们可以查看`multiply`函数本身，而不是实现更多的组合变体:\n\n```cpp\nauto multiply = [](const int first, const int second){ return first *  \n    second; };\n```\n\n我们可以用一个技巧把它分解成两个λ，每个λ取一个参数。关键思想是 lambda 只是一个值，所以它可以由函数返回。我们已经在`compose`函数中看到了这一点；它创建并返回一个新的 lambda:\n\n```cpp\ntemplate <class F, class G>\nauto compose(F f, G g){\n    return [=](auto value){return f(g(value));};\n}\n```\n\n因此，我们可以通过返回一个带有单个参数的新 lambda 来分解一个带有两个参数的函数，该参数从上下文中捕获`first`参数:\n\n```cpp\nauto multiplyDecomposed = [](const int first) { \n    return [=](const int second){ return first * second; }; \n};\n\nTEST_CASE(\"Adds using single parameter functions\"){\n    CHECK_EQ(4, multiplyDecomposed(2)(2));\n}\n```\n\n让我们解开这段代码，因为它非常复杂:\n\n*   `multiplyDecomposed`取一个参数`first`，返回一个λ。\n*   返回的 lambda 从上下文中捕获`first`。\n*   然后它接收一个参数`second`。\n*   返回`first`和`second`相加的结果。\n\n事实证明，任何有两个参数的函数都可以这样分解。因此，我们可以使用模板编写一个通用实现。我们只需要使用同样的技巧——将函数类型指定为模板类型，并在分解中继续使用它:\n\n```cpp\ntemplate<class F>\nauto decomposeToOneParameter(F f){\n    return [=](auto first){\n        return [=](auto second){\n            return f(first, second);\n        };\n    };\n}\n\nTEST_CASE(\"Multiplies using single parameter functions\"){\n    CHECK_EQ(4, decomposeToOneParameter(multiply)(2)(2));\n}\n```\n\n这种方法很有前途；这可能会简化我们的功能组合实现。让我们看看它是否有效。\n\n# 增加乘法的结果\n\n让我们朝着我们的目标前进。我们可以用`compose`得到一个增加乘法结果的函数吗？现在很容易了，因为`add`被分解成接收一个参数的 lambdas。我们希望用`increment`来创作`multiplyDecomposed`:\n\n```cpp\nTEST_CASE(\"Increment result of multiplication\"){\n    int first = 2;\n    int second = 2;\n    auto incrementResultOfMultiplication = compose(increment, \n        multiplyDecomposed);\n    CHECK_EQ(5, incrementResultOfMultiplication(first)(second));\n}\n```\n\n但是，这不会编译。我们的合成函数假设`multiplyDecomposed(first)`的结果可以传递给增量。但是`multiplyDecompose(first)`返回一个λ，`increment`取一个整数。\n\n因此，我们需要用`multipyDecomposed(first)`组成`increment`:\n\n```cpp\nTEST_CASE(\"Increment result of multiplication\"){\n    int first = 2;\n    int second = 2;\n    auto incrementResultOfMultiplication = compose(increment, \n        multiplyDecomposed(first));\n    CHECK_EQ(5, incrementResultOfMultiplication(second));\n}\n```\n\n这很有效，但是我们还没有达到我们的目标。我们没有得到接受这两个值的函数；相反，当用`increment`函数组合第一个值时，它被传递给`multiplyDecomposed`。\n\n幸运的是，这是使用 lambda 的最佳位置，如以下代码所示:\n\n```cpp\nTEST_CASE(\"Increment result of multiplication final\"){\n    auto incrementResultOfMultiplication = [](int first, int second) {\n        return compose(increment, multiplyDecomposed(first))(second);\n    };\n\n    CHECK_EQ(5, incrementResultOfMultiplication(2, 2));\n}\n```\n\n这绝对有效，我们已经达到了目标！`incrementResultOfMultiplication`λ取两个参数，返回乘法的增量。不过，如果我们不必重写`multiply`，那就更好了。幸运的是，我们有我们的`decomposeToOneParameter`功能来帮助我们:\n\n```cpp\nTEST_CASE(\"Increment result of multiplication\"){\n    auto incrementResultOfMultiplication = [](int first, int second) { \n        return compose(increment, decomposeToOneParameter(multiply) \n            (first)) (second);\n };\n    int result = incrementResultOfMultiplication(2, 2);\n    CHECK_EQ(5, result);\n}\n```\n\n是时候看看颠倒的构图了——如果我们想把两个论点的增量相乘呢？\n\n# 乘法增量\n\n我们希望通过使用我们的`compose`函数，获得一个将参数增量相乘的函数。不使用`compose`的最简单的代码如下:\n\n```cpp\nTEST_CASE(\"Multiply incremented values no compose\"){\n    auto multiplyIncrementedValues = [](int first, int second){\n        return multiply(increment(first), increment(second)); \n    };\n    int result = multiplyIncrementedValues(2, 2);\n    CHECK_EQ(9, result);\n}\n```\n\n正如我们已经看到的，如果我们想使用我们的 compose 版本，我们需要首先分解`multiply`λ:\n\n```cpp\nTEST_CASE(\"Multiply incremented values decompose\"){\n    auto multiplyIncrementedValues = [](int first, int second){\n        return multiplyDecomposed(increment(first))(increment(second)); \n    };\n    int result = multiplyIncrementedValues(2, 2);\n    CHECK_EQ(9, result);\n}\n```\n\n现在我们可以看到对`multiplyDecomposed(increment(first))`的调用，这是`multiplyDecomposed`和`increment`之间的合成。我们可以用我们的`compose`功能来代替，如下代码所示:\n\n```cpp\nTEST_CASE(\"Multiply incremented values compose simple\"){\n    auto multiplyIncrementedValues = [](int first, int second){\n        return compose(multiplyDecomposed, increment)(first)\n            (increment(second)); \n    };\n\n    int result = multiplyIncrementedValues(2, 2);\n    CHECK_EQ(9, result);\n}\n```\n\n同样，如果我们不必重写我们的`multiply`函数就好了。但是请记住，我们实现了一个有用的函数，它可以将任何具有两个参数的函数分解为两个具有一个参数的函数。我们不用重写`multiply`；我们只需要在上面调用我们的分解工具:\n\n```cpp\nTEST_CASE(\"Multiply incremented values decompose first\"){\n    auto multiplyIncrementedValues = [](int first, int second){\n        return compose(\n                decomposeToOneParameter(multiply), \n                increment\n               )(first)(increment(second)); \n    };\n    int result = multiplyIncrementedValues(2, 2);\n    CHECK_EQ(9, result);\n}\n```\n\n我们达到了目标！\n\n# 关于函数合成与分解的思考\n\n让我们花点时间看看结果和我们的工作方法。好消息是——我们在学习如何在函数中思考方面取得了很大进步。我们前面的例子仅仅通过作为代码的第一级公民操作函数来工作，如果我们想要使用函数范式设计应用，这正是我们所需要的心态。函数的分解和重组非常强大；掌握它，你将能够用很少的代码实现非常复杂的行为。\n\n至于生成的代码，它有一个有趣的特性——我们可以将其推广到许多函数组合中重用。\n\n但是我们还没完呢！我们可以使用这些函数从代码中删除某些类型的重复。让我们看看如何。\n\n# 使用功能组合消除重复\n\n到目前为止，我们已经看到了如何编写以各种方式组成 lambdas 的函数。但是代码倾向于重复本身，所以我们想让这个方法更通用。我们确实可以更进一步；我们来看几个例子。\n\n# 推广乘法的增量结果\n\n让我们再来看看我们的`incrementResultOfMultiplication`λ:\n\n```cpp\n auto incrementResultOfMultiplication = [](int first, int second) { \n     return compose(increment, decomposeToOneParameter(multiply) \n        (first))(second);\n  };\n```\n\n这里面有一些有趣的东西——它不是针对`increment`和`multiply`的。由于 lambdas 只是值，我们可以将它们作为参数传递，并获得一个通用的`composeWithTwoParameters`函数:\n\n```cpp\ntemplate <class F, class G>\nauto composeWithTwoParameters(F f, G g){\n    return [=](auto first, auto second) { \n        return compose(\n                f, \n                decomposeToOneParameter(g)(first)\n                )(second);\n   };\n};\n\nTEST_CASE(\"Increment result of multiplication\"){\n    auto incrementResultOfMultiplication =  \n    composeWithTwoParameters(increment, multiply);\n    int result = incrementResultOfMultiplication(2, 2);\n    CHECK_EQ(5, result);\n}\n```\n\n这个函数允许我们*组成另外任意两个函数，* `f` *和* `g` *，其中* `g` *取两个参数而* `f`只取一个参数。\n\n让我们再做一些。我们来概括一下`multiplyIncrementedValues`。\n\n# 推广多种货币价值\n\n同样，我们可以很容易地推广我们的`multiplyIncrementedValues`λ，如下面的代码所示:\n\n```cpp\n    auto multiplyIncrementedValues = [](int first, int second){\n        return compose(\n                 decomposeToOneParameter(multiply), \n                 increment\n                 )(first)(increment(second)); \n    };\n```\n\n同样，我们需要将`multiply`和`increment`λ作为参数传递:\n\n```cpp\ntemplate<class F, class G>\nauto composeWithFunctionCallAllParameters(F f, G g){\n    return [=](auto first, auto second){\n        return compose(\n                decomposeToOneParameter(f), \n                g \n                )(first)(g(second)); \n    };\n};\n\nTEST_CASE(\"Multiply incremented values generalized\"){\n    auto multiplyIncrementedValues = \n    composeWithFunctionCallAllParameters(multiply, increment);\n    int result = multiplyIncrementedValues(2, 2);\n    CHECK_EQ(9, result);\n}\n```\n\n我们可以用这个新函数创建一个函数， *C* ，实现`g(f(first), f(second))`，不管`g`和`f`是什么。\n\n我们在这里的工作暂时结束了。\n\n# 摘要\n\n如果你认为纯函数和 lambdas 是强大的，你现在会意识到你可以通过组合它们来做多少事情！在这一章中，您学习了什么是函数组合，以及如何在 C++ 中组合函数。\n\n我们还做了更重要的事情。在这一章中，我们真正开始思考函数。以下是我们学到的一些东西:\n\n*   lambda 只是一个值，所以我们可以有返回 lambda 的函数，或者返回 lambda 的函数。\n*   此外，我们可以让函数接收一个或多个 lambda 并返回一个新的 lambda。\n*   任何具有多个参数的函数都可以分解成多个具有单个参数和捕获值的 lambdas。\n*   带有函数的操作相当复杂。如果你觉得你的头在旋转，没关系——我们一直在玩非常强大和抽象的概念。\n*   当涉及到函数的各种组合方式时，很难立即想出解决方案。最好的方法是循序渐进，有明确的目标和清晰的头脑，并使用本章描述的技巧来改进。\n*   功能组合有助于消除某些类型的重复；例如，当您在具有相似签名的不同函数之间有多个组合时。\n*   然而，正如我们在本章中所做的那样，实现复合函数族是有代价的——更高层次的抽象。很难理解在 lambdas 上执行操作的函数是如何工作的；的确，相信我，我也很难理解结果。尽管如此，一旦你理解了它们的目标，它们还是很容易使用的。\n\n经过这么多努力，让我们花点时间考虑一下结果。想象一下，在你的代码库中，或者在你使用的库中，你已经拥有的任何两个函数，都可以通过一个函数调用组成，并表示为变量。而且，这些调用可以堆栈；您获得的功能可以组成更多。功能成分极其强大；通过非常简单的 lambdas 和一些带有函数的操作，我们可以非常快速地实现复杂的行为。\n\n我们已经看到了如何组合两个函数。我们还需要学习另一种函数操作——通过玩弄参数来获得新函数。\n\n# 问题\n\n1.  什么是功能成分？\n2.  函数组合有一个通常与数学运算相关的性质。这是什么？\n3.  如何把一个有两个参数的`add`函数变成一个参数的两个函数？\n4.  如何编写包含两个单参数函数的 C++ 函数？\n5.  功能性作文有哪些优势？\n6.  在函数上实现操作的潜在缺点是什么？"
  },
  {
    "path": "docs/handson-func-prog-cpp/05.md",
    "content": "# 五、局部应用与柯里化\n\n我们在寻求理解函数式编程方面已经走得很远了！我们学习了纯函数和 lambdas，并借助函数组合深入学习了 lambda 演算。我们现在知道如何从其他函数创建函数。\n\n关于 lambda 演算的基础，还有一件事需要学习。除了功能组合，我们还可以通过两种操作从其他功能创建功能——curry 和 partial application。这将完成我们关于功能构建模块的讨论，并允许您继续使用功能进行设计。\n\n本章将涵盖以下主题:\n\n*   什么是局部应用？\n*   如何在 C++ 中使用分部应用\n*   什么是拍马屁？\n*   如何在 C++ 中咖喱函数\n*   货币与局部应用的关系\n*   如何将讨好与功能组合相结合\n\n# 技术要求\n\n您将需要一个支持 C++ 17 的编译器。我用的是 GCC 7.3.0。\n\n代码在`Chapter05`文件夹中的[GitHub 上。它包括并使用`doctest`，这是一个单头开源单元测试库。你可以在它的 GitHub 存储库中找到它:https://github.com/onqtam/doctest](https://github.com/PacktPublishing/Hands-On-Functional-Programming-with-Cpp)。\n\n# 部分应用和修改\n\n如果您考虑 lambda 以及我们可以对其进行哪些操作来获得其他 lambda，您会想到两件事:\n\n*   一些关于组合两个 lambdas 的东西，我们在函数组合中看到过\n*   一些关于λ的参数，我们将在接下来访问\n\n我们能用λ的参数做什么？有两件事:\n\n*   将一个有多个参数的 lambda 分解成多个有一个参数的 lambda，这个操作叫做**curry**\n*   通过将带有 *N* 参数的λ的参数绑定到一个值，获得带有 *N-1* 参数的λ，这种操作称为**局部应用**\n\n由于即将变得明显的原因，这两个操作是相互关联的，所以我们将一起讨论它们。\n\n# 部分应用\n\n如果您有一个带有 *N* 参数的λ，部分应用意味着通过将一个参数绑定到一个值来获得另一个λ，从而获得一个带有 *N-1* 参数的新λ。例如，我们可以取一个`add`函数，做一个局部应用，将它的一个参数绑定到值`1`，得到一个`increment`函数。在伪 C++ 中，它看起来像这样:\n\n```cpp\nauto add = [](const int first, const int second){return first + second;};\nauto increment = partialApplication(add, /*first*/ 1); \n/* equivalent with \nauto increment = [](const int second){return 1 + second;}; \n*/\n```\n\n就这样！部分应用的想法相当简单。让我们看看 C++ 中的语法。\n\n# C++ 中的部分应用\n\n部分应用的基本实现可以手动完成。我们可以简单地创建一个名为`increment`的 lambda，它调用通用的`add`函数，传递`1`作为第二个参数:\n\n```cpp\nauto add = [](const int first, const int second) { return first + second; };\nTEST_CASE(\"Increments using manual partial application\"){\n    auto increment = [](const int value) { return add(value, 1); };\n\n    CHECK_EQ(43, increment(42));\n}\n```\n\n这不是我们要找的整洁的操作，但是当你因为某种原因不能使用泛型方法时，它会很有用。\n\n幸运的是，STL 在我们友好的头文件`functional`—函数`bind`中提供了一个更好的选择。它将函数、要绑定的值以及仅转发参数的占位符参数作为参数。为了通过调用`bind`获得`increment`函数，我们传入通用的`add`λ；第一个参数的参数值`1`；以及指定未绑定参数的占位符:\n\n```cpp\nusing namespace std::placeholders; // to allow _1, _2 etc.\n\nTEST_CASE(\"Increments using bind\"){\n    // bind the value 1 to the first parameter of add \n    // _1 is a placeholder for the first parameter of the increment    \n       lambda\n    auto increment = bind(add, 1, _1); \n\n    CHECK_EQ(43, increment(42));\n}\n```\n\n虽然方便，但你应该知道`bind`有很高的编译时开销。当这是一个问题时，您总是可以恢复到前面的选项——直接从另一个手动编写的 lambda 中调用更通用的 lambda。\n\n当然，没有什么能阻止我们绑定这两个参数。由于程序员喜欢数字`42`，我将把`add`λ的两个参数都绑定到值`1`和`41`，以便获得另一个λ，`number42` :\n\n```cpp\nTEST_CASE(\"Constant using bind\"){\n   auto number42 = bind(add, 1, 41); \n   CHECK_EQ(42, number42());\n}\n```\n\n`bind`语法有时可能有点棘手，所以让我们更详细地看看它。关键是要理解*参数占位符指的是结果λ的参数，而不是初始λ*的参数。\n\n为了更清楚地说明这一点，我们来看一个添加了三个参数的 lambda 示例:\n\n```cpp\nauto addThree = [](const int first, const int second, const int third){return first + second + third;};\n\nTEST_CASE(\"Adds three\"){\n    CHECK_EQ(42, addThree(10, 20, 12));\n}\n```\n\n如果我们想从我们的`addThree`λ中获得另一个λ，`addTwoNumbersTo10`，通过将其第一个参数绑定到值`10`，那么`bind`的语法是什么？嗯，我们得到的λ，`addTwoNumbersTo10`，将接收两个参数。它们的占位符将用`_1`和`_2`表示。因此，我们需要告诉 bind，我们的初始λ的第一个参数`addThree`是`10`。第二个参数将从`addTwoNumbersTo10`转发，所以是`_1`。第三个论点也是从`addNumbersTo10`的第二个论点转发过来的，所以是`_2`。我们最终得到了这个代码:\n\n```cpp\nTEST_CASE(\"Adds two numbers to 10\"){\n    auto addTwoNumbersTo10 = bind(addThree, 10, _1, _2);\n\n    CHECK_EQ(42, addTwoNumbersTo10(20, 12));\n}\n```\n\n让我们继续前进。我们想通过使用部分应用从我们的初始`addThree`λ中获得另一个λ`addTo10Plus20`。得到的函数只有一个参数，`_1`。其他需要绑定的参数是价值观`10`和`20`。我们以下面的代码结束:\n\n```cpp\nTEST_CASE(\"Adds one number to 10 + 20\"){\n    auto addTo10Plus20 = bind(addThree, 10, 20, _1);\n\n    CHECK_EQ(42, addTo10Plus20(12));\n}\n```\n\n如果我们想绑定第一个和第三个参数呢？现在应该清楚了，参数完全相同，但是它们的顺序在`bind`调用中发生了变化:\n\n```cpp\nTEST_CASE(\"Adds 10 to one number, and then to 20\"){\n    auto addTo10Plus20 = bind(addThree, 10, _1, 20);\n\n    CHECK_EQ(42, addTo10Plus20(12));\n}\n```\n\n如果我们想绑定第二个和第三个参数呢？占位符移动了，但它仍然是结果函数的唯一参数，所以`_1` :\n\n```cpp\nTEST_CASE(\"Adds one number to 10, and then to 20\"){\n    auto addTo10Plus20 = bind(addThree, _1, 10, 20);\n\n    CHECK_EQ(42, addTo10Plus20(12));\n}\n```\n\n如果我们想对一个类方法做部分应用呢？\n\n# 类方法的部分应用\n\n`bind`函数允许我们对类方法进行部分应用，但是有一个问题——第一个参数必须是类的实例。在这个例子中，我们将使用一个`AddOperation`类来实现两个数字之间的简单相加:\n\n```cpp\nclass AddOperation{\n    private:\n        int first;\n        int second;\n\n    public:\n        AddOperation(int first, int second): first(first), \n            second(second){}\n        int add(){ return first + second;}\n};\n```\n\n我们可以通过将`AddOperation`类的一个实例绑定到函数来创建一个新函数`add`:\n\n```cpp\nTEST_CASE(\"Bind member method\"){\n    AddOperation operation(41, 1);\n    auto add41And1 = bind(&AddOperation::add, operation); \n\n    CHECK_EQ(42, add41And1());\n}\n```\n\n更有趣的是，更接近部分应用的概念，我们可以从调用方转发实例参数:\n\n```cpp\nTEST_CASE(\"Partial bind member method no arguments\"){\n    auto add = bind(&AddOperation::add, _1); \n    AddOperation operation(41, 1);\n    CHECK_EQ(42, add(operation));\n}\n```\n\n如果方法接收参数，绑定也是可能的。例如，假设我们有另一个类实现`AddToOperation`:\n\n```cpp\nclass AddToOperation{\n    private:\n        int first;\n\n    public:\n        AddToOperation(int first): first(first) {}\n        int addTo(int second){ return first + second;}\n};\n```\n\n我们可以用类的一个实例来部分应用`addTo`，如下面的代码所示:\n\n```cpp\nTEST_CASE(\"Partial application member method\"){\n    AddToOperation operation(41);\n    auto addTo41 = bind(&AddToOperation::addTo, operation, _1); \n\n    CHECK_EQ(42, addTo41(1));\n}\n```\n\n类方法的部分应用表明，在功能和面向对象世界之间移动是非常容易的。我们将在接下来的章节中看到如何利用这一点。在此之前，让我们庆幸的是，我们现在知道了什么是部分应用，以及如何在 C++ 中使用它。是时候谈谈它的近亲柯林了。\n\n# 携带\n\n让我们试着举几个软件开发界的名人，不要搜索互联网。有艾伦·图灵、阿达·洛芙莱斯(她有一个迷人的故事)、格蕾丝·赫柏、唐纳德·克努特、比约恩·斯特罗斯图普、格雷迪·布奇，可能还有许多其他人。他们中有多少人给你在行业中经常听到的不是一个，而是两个东西起了名字？对艾伦·图灵来说是这样，对图灵机和图灵测试来说肯定是这样，但对许多其他人来说就不是这样了。\n\n因此，令人惊讶的是，哈斯克尔编程语言的名称和 currying 操作的名称来自同一个人——哈斯克尔·库里。哈斯克尔·库里是美国数学家和逻辑学家。他研究一种叫做**组合逻辑**的东西，这是部分函数式编程的基础。\n\n但是什么是讨好呢？它如何连接到部分应用？\n\n# 什么是拍马屁？\n\n**Currying** 是将带有 *N* 个参数的函数分解为带有一个参数的 *N* 个函数的过程。我们可以通过变量捕获或部分应用来实现这一点。\n\n让我们再来看看我们的`add`λ:\n\n```cpp\nauto add = [](const int first, const int second) { return first +  \n     second; };\n\nTEST_CASE(\"Adds values\"){\n    CHECK_EQ(42, add(25, 17));\n}\n```\n\n怎么分解？关键是 lambda 只是一个普通的值，这意味着我们可以从函数中返回它。因此，我们可以传入第一个参数，并返回一个 lambda，它捕获第一个参数并同时使用第一个和第二个参数。用代码比用文字更容易理解，所以这里是:\n\n```cpp\nauto curryAdd = [](const int first){ \n    return [first](const int second){\n        return first + second;\n    };\n};\n\nTEST_CASE(\"Adds values using captured curry\"){\n    CHECK_EQ(42, curryAdd(25)(17));\n}\n```\n\n让我们解开发生了什么:\n\n*   我们的`curryAdd`λ返回一个λ。\n*   返回的 lambda 捕获第一个参数，接受第二个参数，并返回它们的总和。\n\n这就是为什么，在调用它时，我们需要使用双圆括号。\n\n但这看起来很熟悉，好像和部分应用有关。\n\n# 柯里化和部分应用\n\n让我们再次看看我们之前是如何进行部分应用的。我们通过部分应用`add`函数创建了一个`increment`函数:\n\n```cpp\nTEST_CASE(\"Increments using bind\"){\n    auto increment = bind(add, 1, _1); \n\n    CHECK_EQ(43, increment(42));\n}\n```\n\n但是，让我们来讨好我们的`add`功能:\n\n```cpp\nauto curryAdd = [](const int first){ \n    return [first](const int second){\n        return first + second;\n    };\n};\n\nTEST_CASE(\"Adds values using captured curry\"){\n    CHECK_EQ(42, curryAdd(25)(17));\n}\n```\n\n那么，`increment`就很好写了。你能看出是怎么回事吗？\n\n`increment`λ正好是`curryAdd(1)`，如下代码所示:\n\n```cpp\nTEST_CASE(\"Increments value\"){\n    auto increment = curryAdd(1);\n\n    CHECK_EQ(43, increment(42));\n}\n```\n\n这向我们展示了函数式编程语言常用的一个技巧——默认情况下，函数可以被修改。在这样的语言中，编写以下内容意味着我们首先将`add`函数应用于`first`参数，然后将结果函数应用于`second`参数:\n\n```cpp\nadd first second\n```\n\n看起来我们好像在用参数列表调用函数；实际上，这是一个部分应用的课程功能。在这样的语言中，`increment`函数可以简单地通过编写以下内容从`add`中导出:\n\n```cpp\nincrement = add 1\n```\n\n反之亦然。由于 C++ 默认不做 curry，但是为局部应用提供了一个简单的方法，所以我们可以通过局部应用来实现 curry。不要使用值捕获返回复杂的 lambda，只需绑定到单个值并转发结果函数的单个参数:\n\n```cpp\nauto curryAddPartialApplication = [](const int first){ \n    return bind(add, first, _1);\n};\n\nTEST_CASE(\"Adds values using partial application curry\"){\n    CHECK_EQ(42, curryAddPartialApplication(25)(17));\n}\n```\n\n但是我们能走多远呢？用多个参数来讨好函数容易吗？\n\n# 具有多个参数的 Currying 函数\n\n在上一节中，我们已经看到了如何用两个参数来处理函数。当我们转到三个参数时，curried 函数也会增长。我们现在需要返回一个 lambda，它返回一个 lambda。同样，代码比任何解释都更容易理解，让我们看看:\n\n```cpp\nauto curriedAddThree = [](const int first){\n    return [first](const int second){ \n        return [first, second](const int third){\n            return first + second + third;\n        };\n    };\n}; \n\nTEST_CASE(\"Add three with curry\"){\n    CHECK_EQ(42, curriedAddThree(15)(10)(17));\n}\n```\n\n那里似乎有一个递归结构。也许通过使用`bind`我们可以理解它？\n\n原来没那么简单，但是有可能。我想写的是这样的:\n\n```cpp\nbind(bind(bind(addThree, _1),_1), _1)\n```\n\n然而，`addThree`有三个参数，所以我们需要将它们绑定到某个东西上。下一个`bind`产生一个带有两个参数的函数，同样，我们需要将它们绑定到某个东西上。所以，它实际上是这样的:\n\n```cpp\nbind(bind(bind(addThree, ?, ?, _1), ?,_1), _1)\n```\n\n问号应该用以前绑定的值替换，但这不适用于我们当前的语法。\n\n但是，有一个解决方法。让我们用 *N* 参数实现多个在函数上使用`bind`的`simpleCurryN`函数，并将其简化为 *N-1* 。对于有一个参数的函数，结果就是下面的函数:\n\n```cpp\nauto simpleCurry1 = [](auto f){\n     return f;\n };\n```\n\n对于两个参数，我们绑定第一个并转发下一个:\n\n```cpp\nauto simpleCurry2 = [](auto f){\n    return [f](auto x){ return bind(f, x, _1); };\n};\n```\n\n类似的操作适用于三个和四个参数:\n\n```cpp\nauto simpleCurry3 = [](auto f){\n     return [f](auto x, auto y){ return bind(f, x, y, _1); };\n};\nauto simpleCurry4 = [](auto f){\n    return [f](auto x, auto y, auto z){ return bind(f, x, y, z, _1);  \n};\n};\n```\n\n这组`simpleCurryN`函数允许我们编写我们的`curryN`函数，这些函数用 *N* 参数取一个函数，并返回它的 curried 形式:\n\n```cpp\nauto curry2 = [](auto f){\n    return simpleCurry2(f);\n };\n\nauto curry3 = [](auto f){\n    return curry2(simpleCurry3(f));\n };\n\nauto curry4 = [](auto f){\n    return curry3(simpleCurry4(f));\n};\n```\n\n让我们用两个、三个和四个参数在`add` lambdas 上测试它们，如下面的代码所示:\n\n```cpp\nTEST_CASE(\"Add three with partial application curry\"){\n    auto add = [](int a, int b) { return a+b; };\n    CHECK_EQ(3, curry2(add)(1)(2));\n\n    auto addThreeCurryThree = curry3(addThree);\n    CHECK_EQ(6, curry3(addThree)(1)(2)(3));\n\n    auto addFour = [](int a, int b, int c, int d){return a + b + c +  \n        d;};\n    CHECK_EQ(10, curry4(addFour)(1)(2)(3)(4));\n }\n```\n\n很可能我们可以用一些富有想象力的模板来重写这些函数。我将把这个练习留给读者。\n\n目前，重要的是要看到部分应用如何与货币挂钩。在默认情况下使用 curry 函数的编程语言中，部分应用非常容易——只需用更少的参数调用函数。对于其他编程语言，我们可以通过局部应用来实现 currying。\n\n这些概念非常有趣，但你可能想知道它们在实践中是否有用。让我们看看如何使用这些技术消除重复。\n\n# 使用部分应用和 currying 删除重复\n\n程序员长期以来一直在寻找解决方案，以编写更少的代码，做更多的事情。函数式编程提出了一种解决方案——通过从其他函数派生来构建函数。\n\n在前面的例子中，我们已经看到了这一点。由于`increment`是加法的特殊情况，我们可以从加法函数中推导出来:\n\n```cpp\nauto add = [](const auto first, const auto second) { return first + second; };\nauto increment = bind(add, _1, 1);\n\nTEST_CASE(\"Increments\"){\n    CHECK_EQ(43, increment(42));\n}\n```\n\n这对我们有什么帮助？好吧，想象一下，有一天你的客户进来告诉你*我们想使用另一种加法。*想象一下，必须在代码中到处搜索`+`和`++ `，并找出实现新行为的方法。\n\n相反，通过我们的`add`和`increment`功能，以及一点模板魔法，这就是我们能做的:\n\n```cpp\nauto add = [](const auto first, const auto second) { return first + \n    second; };\n\ntemplate<typename T, T one>\nauto increment = bind(add, _1, one);\n\nTEST_CASE(\"Increments\"){\n    CHECK_EQ(43, increment<int, 1>(42));\n}\n```\n\n我们的`add`方法不在乎得到什么类型，只要有一个加号运算符。我们的`increment`函数并不关心它使用什么类型以及`add`是如何工作的，只需要你为一个提供一个值。我们已经用三行代码完成了这项工作。我很少这样说代码，但是它不是很美吗？\n\n当然，你可能会说，但是我们的客户并不想改变我们添加东西的方式。你会惊讶于你能用几个简单的操作符做多少事。让我给你举个简单的例子。实现一个游戏，其中一个角色在一条换行线上移动，如下图所示:\n\n![](img/8e9654cf-b698-47a3-a965-8bc798779a06.png)\n\n这不就是加法的修改版吗？我们来看看:\n\n```cpp\n// Assume wrap at 20 for now\nauto addWrapped = [](const auto first, const auto second) { return \n    (first + second)%20; };\n\nTEST_CASE(\"Adds values\"){\n    CHECK_EQ(7, addWrapped(10, 17));\n}\n\ntemplate<typename T, T one>\nauto incrementWrapped = bind<T>(addWrapped, _1, one);\n\nTEST_CASE(\"Increments\"){\n    CHECK_EQ(1, incrementWrapped<int, 1>(20));\n}\n```\n\n嗯，这个代码看起来和`add`很像。也许我们可以用局部应用？让我们看看如何:\n\n```cpp\nauto addWrapped = [](const auto first, const auto second, const auto \n    wrapAt) { return (first + second) % wrapAt; };\n\nauto add = bind(addWrapped, _1, _2, 20);\n\ntemplate<typename T, T one>\n    auto increment = bind<T>(add, _1, one);\n\nTEST_CASE(\"Increments\"){\n    CHECK_EQ(1, increment<int, 1>(20));\n}\n```\n\n我们的`increment`功能和之前完全一样，而我们的`add`功能已经成为了`addWrapped`的部分应用。值得注意的是，为了使代码更清晰，我仍然会更改函数名，以使其非常清楚函数在做什么。然而，主要的一点是，部分应用和 curry 帮助我们从代码中删除某些类型的重复，使我们能够将代码打开到我们在设计初始解决方案时不一定知道的实现。虽然我们可以用面向对象程序或模板做到这一点，但功能解决方案通过消除副作用来限制复杂性，并且只需要几行代码。这使得它成为设计程序时的一个有价值的选择。\n\n# 摘要\n\n看看我们对函数式编程的理解已经走了多远！我们了解了所有的构建模块——纯函数和 lambdas，以及我们可以在它们上面使用的操作——curry、部分应用和函数组合。我们还看到了操作之间是如何相互关联的，以及如何使用 currying 来实现部分应用，反之亦然。我们也看到了用 C++ 实现 currying 的方法。\n\n但我们的探索才刚刚开始。下一站是——开始在更有趣的环境中使用这些结构。是时候解决这个难题了——我们到底该如何设计函数？\n\n# 问题\n\n1.  什么是部分函数应用？\n2.  什么是拍马屁？\n3.  currying 如何帮助我们实现部分应用？\n4.  如何在 C++ 中实现部分应用？"
  },
  {
    "path": "docs/handson-func-prog-cpp/06.md",
    "content": "# 六、函数思维——从数据输入到数据输出\n\n在我理解函数式编程的旅程中，我遇到了一个困难的障碍——我的思维被训练成一种完全不同的编程风格。让我们称之为命令式面向对象编程。那么，我该如何将思维模式从对象思维转变为功能思维呢？我怎样才能把这两者很好地结合起来呢？\n\n我首先研究了函数式编程资源。不幸的是，他们中的大多数人都专注于数学和概念的内在美——这对任何已经能用这些术语思考的人来说都是很棒的。但是如果你只是想学习它们呢？复习数学理论是学习的唯一途径吗？虽然我喜欢数学，但我对它已经生疏了，我宁愿找到更实用的方法。\n\n然后，我接触了各种通过 Coderetreats、Coding Dojos 等事件编写代码的方法，或者与来自欧洲各地的程序员进行结对编程。我逐渐意识到，有一种简单的方法可以解决这个问题——只关注输入和输出，而不是关注它们之间的模型。这是一种更具体、更实用的学习函数思维的方法，这也是我们接下来要探索的。\n\n本章将涵盖以下主题:\n\n*   功能思维的基础\n*   重新学习如何识别特征的数据输入和数据输出，并利用类型推断\n*   将数据转换定义为纯函数\n*   如何使用典型的数据转换，如映射、简化、过滤等\n*   如何用功能思维解决问题\n*   为围绕函数设计的代码设计错误管理\n\n# 技术要求\n\n您将需要一个支持 C++ 17 的编译器。我用的是 GCC 7.3.0。\n\n代码可以在[网站上的`Chapter06`文件夹中找到。它包括并使用`doctest`，这是一个单头开源单元测试库。你可以在 https://github.com/onqtam/doctest](https://github.com/PacktPublishing/Hands-On-Functional-Programming-with-Cpp)的 GitHub 储存库中找到它。\n\n# 通过函数从数据输入到数据输出\n\n我的计算机编程教育，以及我作为程序员的重点，主要是写代码，而不是深入理解输入和输出数据。当我学习**测试驱动开发** ( **TDD** )时，这个焦点改变了，因为这个实践迫使程序员从输入和输出开始。通过应用一种叫做 **TDD 的极端形式，我对程序的核心定义有了新的理解——它接受输入数据并返回输出数据。**\n\n不过，这并不容易。我的训练促使我重新思考构成这个项目的东西。但是后来，我意识到那些东西只能是纯粹的功能。毕竟，任何程序都可以写成如下形式:\n\n*   一组纯函数，如前所述\n*   一组与**输入/输出** ( **输入/输出**)交互的功能\n\n如果我们把程序减少到最小，把所有的输入输出分开，计算出程序其余部分的输入输出，并尽可能编写纯函数，我们就完成了用函数思考的第一步。\n\n下一个问题是——这些功能应该是什么？在本章中，我们将探讨使用函数进行设计的最简单方法:\n\n1.  从中的数据开始。\n2.  定义数据输出。\n3.  定义一系列转换(纯函数)，将数据一步一步地转换成数据。\n\n让我们看几个例子来对比两种编写程序的方法。\n\n# 命令式和功能式风格的一个范例\n\n为了显示方法之间的差异，我们需要使用一个问题。我喜欢用游戏中的问题来练习新的编程技巧。一方面，这是一个我不常接触的有趣领域。另一方面，游戏提供了许多普通商业应用所没有的挑战，从而让我们可以探索新的想法。\n\n在接下来的部分，我们将看一个让人们学会如何在函数中开始思考的问题——**井字游戏结果** **问题**。\n\n# 井字游戏结果\n\n井字游戏结果问题有以下要求——给定一个空的或者已经有招式的井字游戏板，打印出游戏结果，如果游戏已经结束，或者打印出还在进行中的游戏。\n\n看起来这个问题相当简单，但是它将向我们展示函数式和命令式**面向对象** ( **OO** )方法之间的根本区别。\n\n如果我们从面向对象的角度来处理这个问题，我们已经在考虑一些要定义的对象——一个游戏、一个玩家、一个棋盘，也许还有一些`X`和`O`(我称之为代币)的表示，等等。然后，我们可能会看到如何连接这些对象——一个游戏有两个玩家和一个棋盘，棋盘上有代币或空字段等等。正如你所看到的，这涉及到很多代表性。然后，我们需要在某个地方实现一个返回`GameState`的`computeResult`方法，要么是`XWon`、`OWon`、`draw`，要么是`InProgress`。乍一看`computeResult`好像很适合`Game`类。该方法可能需要在`Board`内部循环，使用一些条件语句，并返回相应的`GameState`。\n\n我们将使用一些严格的步骤来帮助我们对代码结构进行不同的思考，而不是使用面向对象的方法:\n\n1.  明确定义输入；举个例子。\n2.  明确定义输出；举个例子。\n3.  确定一系列可以应用于输入数据的功能转换，以将其转化为输出数据。\n\n在我们继续之前，请注意这种心态的改变需要一点知识和实践。我们将研究最常见的转换，为您提供一个良好的开端，但是您需要自己尝试这种方法。\n\n# 输入和输出\n\n作为程序员，我们学到的第一课是任何程序都有输入和输出。然后，我们继续关注代码本身中输入和输出之间发生的事情。\n\n然而，输入和输出应该得到程序员更多的关注，因为它们定义了我们软件的需求。我们知道，软件中最大的浪费是实现了一些完美的工作，但没有做它应该做的事情。\n\n我注意到程序员很难回到输入输出的角度去思考。给定特性的输入和输出应该是什么，这个看似简单的问题常常让他们困惑不解。所以，让我们详细看看问题的输入和输出数据。\n\n在这一点上，我们会做一些意想不到的事情。我从业务分析师那里学到了一个巧妙的技巧——在分析一个特性时，最好从输出开始，因为输出往往比输入数据更小、更清晰。所以，我们开始吧。\n\n# 输出数据是什么？\n\n我们期望产出是什么？考虑到董事会可以有任何东西，或者什么都没有，我们正在考虑以下可能性:\n\n*   *游戏未开始*\n*   *游戏进行中*\n*   `X`赢了\n*   `O`赢了\n*   画\n\n看，输出很简单！现在，我们可以看到输入数据是如何与这些可能性相关联的。\n\n# 输入数据是什么？\n\n在这种情况下，输入数据在问题陈述中——我们的输入是一个上面有移动的板。但是让我们看一些例子。最简单的例子是一块空板子:\n\n```cpp\n_ _ _ \n_ _ _ \n_ _ _\n```\n\n为了清楚起见，我们用`_`来表示棋盘上的一个空位。\n\n空棋盘当然对应于*游戏未开始*输出。\n\n这很简单。现在，让我们看一个有一些动作的:\n\n```cpp\nX _ _    \nO _ _ \n_ _ _\n```\n\n`X`和`O`都已经出手了，但是游戏还在进行中。我们可以提供许多正在进行的*游戏*的例子:\n\n```cpp\nX X _ \nO _ _ \n_ _ _\n```\n\n这里还有一个例子:\n\n```cpp\nX X O \nO _ _ \n_ _ _\n```\n\n有几个在井字游戏中永远不会发生的例子，比如这个:\n\n```cpp\nX X _ \nO X _ \nX _ _\n```\n\n在这种情况下，`X`走了四步，`O`只走了一步，这是井字游戏规则不允许的。我们暂时忽略这种情况，只返回一个正在进行的*游戏*。但是，一旦我们完成了代码的剩余部分，您就可以为此实现自己的算法。\n\n来看看`X`赢的一局:\n\n```cpp\nX X X \nO O _ \n_ _ _\n```\n\n`X`获胜是因为第一行被填满。`X`还有其他赢的方法吗？是的，在一个专栏上:\n\n```cpp\nX _ _ \nX O O \nX _ _\n```\n\n它也可能在主对角线上获胜:\n\n```cpp\nX O _ \nO X _ \n_ _ X\n```\n\n第二条对角线上`X`赢了:\n\n```cpp\n_ O X \nO X _ \nX _ _\n```\n\n类似地，我们有这样的例子`O`通过填充一行获胜:\n\n```cpp\nX X _ \nO O O \nX _ _\n```\n\n这里有一个填充栏的胜利:\n\n```cpp\nX O _ \nX O X \n_ O _\n```\n\n以下是`O`的主对角线获胜:\n\n```cpp\nO X _ \n_ O X \nX _ O\n```\n\n这是通过第二对角线获得的胜利:\n\n```cpp\nX X O \n_ O X \nO _ _\n```\n\n以平局告终的比赛怎么样？这很简单——所有的方块都填满了，但是没有赢家:\n\n```cpp\nX X O \nO X X \nX O O\n```\n\n我们已经查看了所有可能输出的示例。现在，是时候看看数据转换了。\n\n# 数据转换\n\n如何将输入转化为输出？为此，我们必须首先选择一个可能的输出来处理。目前最简单的是`X`赢的情况。那么，`X`怎么才能赢呢？\n\n根据游戏规则，如果棋盘上的一条线、一列或一条对角线被`X`填满，则`X`获胜。让我们写下所有可能的情况。`X`如果发生以下任一情况，则获胜:\n\n*   任何一行都用`X`或填充\n*   任何一列都用`X`或填充\n*   主对角线用`X` OR 填充\n*   次对角线填充`X`\n\n要实现这一点，我们需要一些东西:\n\n*   把黑板上所有的线都拿过来。\n*   从黑板上取下所有的柱子。\n*   从板子上取下主对角线和次对角线。\n*   如果其中任何一个填了`X`，`X`赢了！\n\n我们可以用另一种方式来写:\n\n```cpp\nboard -> collection(all lines, all columns, all diagonals) -> any(collection, filledWithX) -> X won\n```\n\n`filledWithX`是什么意思？我们举个例子；我们正在寻找这样的线路:\n\n```cpp\nX X X\n```\n\n我们不是在找`X O X`或`X _ X`这样的线。\n\n听起来我们在检查线、列或对角线上的所有标记是否都是`'X'`。让我们把这个检查想象成一个转换:\n\n```cpp\nline | column | diagonal -> all tokens equal X -> line | column | diagonal filled with X\n```\n\n所以，我们的一系列转换变成了这样:\n\n```cpp\nboard -> collection(all lines, all columns, all diagonals) -> if any(collection, filledWithX) -> X won \n\nfilledWithX(line|column|diagonal L) = all(token on L equals 'X')\n```\n\n还有一个问题——我们怎样才能得到线、柱和对角线？我们可以分开来看这个问题，就像我们看大问题一样。我们的投入绝对是板子。我们的输出是由第一行、第二行和第三行、第一列、第二列和第三列、主对角线和次对角线组成的列表。\n\n下一个问题是，什么定义了一条线？嗯，我们知道如何获得第一条线——我们使用`[0, 0]`、`[0, 1]`和`[0, 2]`坐标。第二行有`[1, 0]`、`[1, 1]`和`[1, 2]`坐标。专栏怎么样？嗯，第一列有`[1, 0]`、`[1, 1]`和`[2, 1]`坐标。正如我们将看到的，对角线也是由特定的坐标集定义的。\n\n那么，我们学到了什么？我们了解到，要获得线条、列和对角线，我们需要以下转换:\n\n```cpp\nboard -> collection of coordinates for lines, columns, diagonals -> apply coordinates to the board -> obtain list of elements for lines, columns, and diagonals\n```\n\n我们的分析到此结束。是时候开始实施了。前面所有的转换都可以通过函数构造用代码来表达。事实上，有些转换非常常见，已经在标准库中实现了。让我们看看如何使用它们！\n\n# 对 filledWithX 使用 all_of\n\n我们要看的第一个变换是`all_of`。给定一个集合和一个返回布尔值(也称为**逻辑谓词**)的函数，`all_of`将谓词应用于集合的每个元素，并返回结果之间的逻辑与。让我们看几个例子:\n\n```cpp\nauto trueForAll = [](auto x) { return true; };\nauto falseForAll = [](auto x) { return false; };\nauto equalsChara = [](auto x){ return x == 'a';};\nauto notChard = [](auto x){ return x != 'd';};\n\nTEST_CASE(\"all_of\"){\n    vector<char> abc{'a', 'b', 'c'};\n\n    CHECK(all_of(abc.begin(), abc.end(), trueForAll));\n    CHECK(!all_of(abc.begin(), abc.end(), falseForAll));\n    CHECK(!all_of(abc.begin(), abc.end(), equalsChara));\n    CHECK(all_of(abc.begin(), abc.end(), notChard));\n}\n```\n\n`all_of`函数使用两个迭代器定义一个范围的开始和结束以及一个谓词作为参数。当您想要将转换应用于集合的子集时，迭代器非常有用。因为我通常在全本上使用它，所以我觉得反复写`collection.begin()`和`collection.end()`很烦人。因此，我实现了自己的简化`all_of_collection`版本，该版本负责整个集合并处理其余部分:\n\n```cpp\nauto all_of_collection = [](const auto& collection, auto lambda){\n    return all_of(collection.begin(), collection.end(), lambda);\n};\n\nTEST_CASE(\"all_of_collection\"){\n    vector<char> abc{'a', 'b', 'c'};\n\n    CHECK(all_of_collection(abc, trueForAll));\n    CHECK(!all_of_collection(abc, falseForAll));\n    CHECK(!all_of_collection(abc, equalsChara));\n    CHECK(all_of_collection(abc, notChard));\n}\n```\n\n知道了这个转换，就很容易编写我们的`lineFilledWithX`函数——我们将令牌集合转换成布尔集合，指定令牌是否为`X`:\n\n```cpp\nauto lineFilledWithX = [](const auto& line){\n    return all_of_collection(line, [](const auto& token){ return token == 'X';});\n};\n\nTEST_CASE(\"Line filled with X\"){\n    vector<char> line{'X', 'X', 'X'};\n\n    CHECK(lineFilledWithX(line));\n}\n```\n\n就这样！我们可以确定我们的线路是否充满`X`。\n\n在继续之前，让我们做一些简单的调整。首先，让我们通过命名我们的`vector<char>`类型来使代码更加清晰:\n\n```cpp\nusing Line = vector<char>;\n```\n\n然后，让我们检查代码是否也适用于负面场景。如果`Line`没有装满`X`令牌，`lineFilledWithX`应该返回`false`:\n\n```cpp\nTEST_CASE(\"Line not filled with X\"){\n    CHECK(!lineFilledWithX(Line{'X', 'O', 'X'}));\n    CHECK(!lineFilledWithX(Line{'X', ' ', 'X'}));\n}\n```\n\n最后，精明的读者会注意到，对于`O`获胜条件，我们将需要相同的函数。我们现在知道如何做到这一点——记住参数绑定的力量。我们只需要提取一个`lineFilledWith`函数，通过将`tokenToCheck`参数分别绑定到`X`和`O`标记值来获取`lineFilledWithX`和`lineFilledWithO`函数:\n\n```cpp\nauto lineFilledWith = [](const auto line, const auto tokenToCheck){\n    return all_of_collection(line, [&tokenToCheck](const auto token){  \n        return token == tokenToCheck;});\n};\n\nauto lineFilledWithX = bind(lineFilledWith, _1, 'X'); \nauto lineFilledWithO = bind(lineFilledWith, _1, 'O');\n```\n\n让我们回顾一下——我们有一个`Line`数据结构，并且我们有一个可以检查该行是被`X`还是`O`填充的函数。我们用`all_of`功能为我们做了举重；我们只需要定义我们井字游戏的逻辑。\n\n是时候向前看了。我们需要把我们的棋盘变成一个线条集合，由三条线、三列和两条对角线组成。为此，我们需要访问另一个功能转换，`map`，它作为`transform`功能在**标准模板库** ( **STL** )中实现。\n\n# 使用地图/变换\n\n我们现在需要写一个函数，把板子变成一个行、列和对角线的列表；因此，我们可以使用一个转换，将一个集合转换成另一个集合。这种转换在一般函数式编程中称为`map`，在 STL 中实现为`transform`。为了理解它，我们将使用一个简单的例子；给定一个字符向量，让我们用`'a'`替换每个字符:\n\n```cpp\nTEST_CASE(\"transform\"){\n    vector<char> abc{'a', 'b', 'c'};\n\n// Not the best version, see below\nvector<char> aaa(3);\ntransform(abc.begin(), abc.end(), aaa.begin(), [](auto element){return \n    'a';});\nCHECK_EQ(vector<char>{'a', 'a', 'a'}, aaa);\n}\n```\n\n虽然它是有效的，但是前面的代码示例是幼稚的，因为它用随后被覆盖的值来初始化`aaa`向量。我们可以通过首先在`aaa`向量中保留`3`元素，然后使用`back_inserter`来避免这个问题，这样`transform`会自动调用`aaa`向量上的`push_back`:\n\n```cpp\nTEST_CASE(\"transform-fixed\") { \n    const auto abc = vector{'a', 'b', 'c'}; \n    vector<char> aaa; \n    aaa.reserve(abc.size()); \n    transform(abc.begin(), abc.end(), back_inserter(aaa), \n            [](const char elem) { return 'a'; }\n    ); \n    CHECK_EQ(vector{'a', 'a', 'a'}, aaa); \n}\n```\n\n如您所见，`transform`基于迭代器的工作方式与`all_of`相同。到现在，你会注意到我喜欢保持简单，专注于我们正在努力完成的事情。没有必要一直写这个；相反，我们可以实现我们自己的简化版本，在一个完整的集合上工作，并处理围绕这个功能的所有仪式。\n\n# 简化变换\n\n让我们尝试以最简单的方式实现`transform_all`功能:\n\n```cpp\nauto transform_all = [](auto const source, auto lambda){\n    auto destination; // Compilation error: the type is not defined\n    ...\n}\n```\n\n不幸的是，当我们试图以这种方式实现它时，我们遇到了一个障碍——我们需要一个目标集合的类型。这样做的自然方式是使用 C++ 模板并传入`Destination`类型参数:\n\n```cpp\ntemplate<typename Destination>\nauto transformAll = [](auto const source,  auto lambda){\n    Destination result;\n    result.reserve(source.size());\n    transform(source.begin(), source.end(), back_inserter(result), \n        lambda);\n    return result;\n};\n\n```\n\n这对于任何具有`push_back`功能的集合都很有效。一个不错的副作用是我们可以用它来连接`string`中的结果字符:\n\n```cpp\nauto turnAllToa = [](auto x) { return 'a';};\n\nTEST_CASE(\"transform all\"){\n    vector abc{'a', 'b', 'c'};\n\n    CHECK_EQ(vector<char>({'a', 'a', 'a'}), transform_all<vector<char>>\n        (abc, turnAllToa));\n    CHECK_EQ(\"aaa\", transform_all<string>(abc,turnAllToa));\n}\n```\n\n使用`transform_all`和`string`可以让我们做一些事情，比如把小写字符变成大写字符:\n\n```cpp\nauto makeCaps = [](auto x) { return toupper(x);};\n\nTEST_CASE(\"transform all\"){\n    vector<char> abc = {'a', 'b', 'c'};\n\n    CHECK_EQ(\"ABC\", transform_all<string>(abc, makeCaps));\n}\n```\n\n但这还不是全部——输出类型不必与输入相同:\n\n```cpp\nauto toNumber = [](auto x) { return (int)x - 'a' + 1;};\n\nTEST_CASE(\"transform all\"){\n    vector<char> abc = {'a', 'b', 'c'};\n    vector<int> expected = {1, 2, 3};\n\n    CHECK_EQ(expected, transform_all<vector<int>>(abc, toNumber));\n}\n```\n\n因此，每当我们需要将一个集合转换成另一个集合时,`transform`函数非常有用，无论是相同类型还是不同类型。在`back_inserter`的支持下，它还可以用于`string`输出，从而实现任何类型集合的字符串表示。\n\n我们现在知道如何使用转换。所以，让我们回到我们的问题。\n\n# 我们的坐标\n\n我们的转换从计算坐标开始。所以，让我们先定义它们。STL `pair`类型是坐标的简单表示:\n\n```cpp\nusing Coordinate = pair<int, int>;\n```\n\n# 从棋盘和坐标中得到一条线\n\n假设我们为一条线、一列或一条对角线建立了坐标列表，我们需要将代币集合转换为`Line`参数。这可以通过我们的`transformAll`功能轻松实现:\n\n```cpp\nauto accessAtCoordinates = [](const auto& board, const Coordinate&  \n    coordinate){\n        return board[coordinate.first][coordinate.second];\n};\n\nauto projectCoordinates = [](const auto& board, const auto&  \n    coordinates){\n        auto boardElementFromCoordinates = bind(accessAtCoordinates,  \n        board, _1);\n        return transform_all<Line>(coordinates,  \n            boardElementFromCoordinates);\n};\n```\n\n`projectCoordinates`λ取棋盘和一个坐标列表，并从棋盘返回对应于这些坐标的元素列表。我们在坐标列表中使用`transformAll`，以及一个包含两个参数的变换——一个`board`参数和一个`coordinate`参数。然而，`transformAll`需要一个带有单个参数的λ，`Coordinate`值。因此，我们要么获取电路板的价值，要么使用部分应用。\n\n我们现在只需要为线、柱和对角线建立我们的坐标列表！\n\n# 从黑板上得到一行字\n\n使用前面的函数`projectCoordinates`，我们可以很容易地从板子上得到一条线:\n\n```cpp\nauto line = [](auto board, int lineIndex){\n   return projectCoordinates(board, lineCoordinates(board, lineIndex));\n};\n```\n\n`line`λ取`board`和`lineIndex`，建立线坐标表，用`projectCoordinates`回线。\n\n那么，我们如何建立线坐标呢？好吧，既然我们有`lineIndex`和`Coordinate`一对，我们需要在`(lineIndex, 0)`、在`(lineIndex, 1)`和在`(lineIndex, 2)`上呼叫`make_pair`。这看起来也像是`transform`呼叫；输入为`{0, 1, 2}`集合，变换为`make_pair(lineIndex, index)`。让我们写下来:\n\n```cpp\nauto lineCoordinates = [](const auto board, auto lineIndex){\n    vector<int> range{0, 1, 2};\n    return transformAll<vector<Coordinate>>(range, [lineIndex](auto  \n        index){return make_pair(lineIndex, index);});\n};\n```\n\n# 范围\n\n但是`{0, 1, 2}`是什么呢？在其他编程语言中，我们可以使用范围的概念；例如，在 Groovy 中，我们可以编写以下内容:\n\n```cpp\ndef range = [0..board.size()]\n```\n\n范围非常有用，并且在 C++ 20 标准中采用了它们。我们将在[第 14 章](14.html)、*使用范围库*的延迟求值中讨论它们。在此之前，我们将编写自己的函数，`toRange`:\n\n```cpp\nauto toRange = [](auto const collection){\n    vector<int> range(collection.size());\n    iota(begin(range), end(range), 0);\n    return range;\n};\n```\n\n`toRange`以集合为输入，创建从`0`到`collection.size()`的`range`。所以，让我们在代码中使用它:\n\n```cpp\nusing Board = vector<Line>;\nusing Line = vector<char>;\n\nauto lineCoordinates = [](const auto board, auto lineIndex){\n    auto range = toRange(board);\n    return transform_all<vector<Coordinate>>(range, [lineIndex](auto  \n        index){return make_pair(lineIndex, index);});\n};\n\nTEST_CASE(\"lines\"){\n    Board board {\n        {'X', 'X', 'X'},\n        {' ', 'O', ' '},\n        {' ', ' ', 'O'}\n    };\n\n    Line expectedLine0 = {'X', 'X', 'X'};\n    CHECK_EQ(expectedLine0, line(board, 0));\n    Line expectedLine1 = {' ', 'O', ' '};\n    CHECK_EQ(expectedLine1, line(board, 1));\n    Line expectedLine2 = {' ', ' ', 'O'};\n    CHECK_EQ(expectedLine2, line(board, 2));\n}\n```\n\n我们已经准备好了所有的元素，所以是时候看看这些列了。\n\n# 获取列\n\n获取列的代码与获取行的代码非常相似，只是我们保留了`columnIndex`而不是`lineIndex`。我们只需要将它作为参数传递:\n\n```cpp\nauto columnCoordinates = [](const auto& board, const auto columnIndex){\n    auto range = toRange(board);\n    return transformAll<vector<Coordinate>>(range, [columnIndex](const  \n        auto index){return make_pair(index, columnIndex);});\n};\n\nauto column = [](auto board, auto columnIndex){\n    return projectCoordinates(board, columnCoordinates(board,  \n        columnIndex));\n};\n\nTEST_CASE(\"all columns\"){\n    Board board{\n        {'X', 'X', 'X'},\n        {' ', 'O', ' '},\n        {' ', ' ', 'O'}\n    };\n\n    Line expectedColumn0{'X', ' ', ' '};\n    CHECK_EQ(expectedColumn0, column(board, 0));\n    Line expectedColumn1{'X', 'O', ' '};\n    CHECK_EQ(expectedColumn1, column(board, 1));\n    Line expectedColumn2{'X', ' ', 'O'};\n    CHECK_EQ(expectedColumn2, column(board, 2));\n}\n```\n\n很酷吧？有了一些函数，在标准函数转换的帮助下，我们可以在代码中构建复杂的行为。对角线现在轻而易举。\n\n# 得到对角线\n\n主对角线由相等的线和列坐标定义。使用与以前相同的机制来阅读它相当容易；我们构建相等的指数对，并将它们传递给`projectCoordinates`函数:\n\n```cpp\nauto mainDiagonalCoordinates = [](const auto board){\n    auto range = toRange(board);\n    return transformAll<vector<Coordinate>>(range, [](auto index) \n       {return make_pair(index, index);});\n};\nauto mainDiagonal = [](const auto board){\n    return projectCoordinates(board, mainDiagonalCoordinates(board));\n};\n\nTEST_CASE(\"main diagonal\"){\n    Board board{\n        {'X', 'X', 'X'},\n        {' ', 'O', ' '},\n        {' ', ' ', 'O'}\n    };\n\n    Line expectedDiagonal = {'X', 'O', 'O'};\n\n    CHECK_EQ(expectedDiagonal, mainDiagonal(board));\n}\n```\n\n二次对角线呢？嗯，坐标之和总是等于`board`参数的大小。在 C++ 中，我们还需要考虑基于 0 的索引，所以在构建坐标列表时，我们需要通过`1`进行适当的调整:\n\n```cpp\nauto secondaryDiagonalCoordinates = [](const auto board){\n    auto range = toRange(board);\n    return transformAll<vector<Coordinate>>(range, [board](auto index) \n        {return make_pair(index, board.size() - index - 1);});\n};\n\nauto secondaryDiagonal = [](const auto board){\n    return projectCoordinates(board, \n        secondaryDiagonalCoordinates(board));\n};\n\nTEST_CASE(\"secondary diagonal\"){\n    Board board{\n        {'X', 'X', 'X'},\n        {' ', 'O', ' '},\n        {' ', ' ', 'O'}\n    };\n\n    Line expectedDiagonal{'X', 'O', ' '};\n\n    CHECK_EQ(expectedDiagonal, secondaryDiagonal(board));\n}\n```\n\n# 得到所有的线、所有的列和所有的对角线\n\n说到这里，我们现在可以建立一个所有线、列和对角线的集合。有多种方法可以做到这一点；既然我要找一个用函数式风格写的通用解决方案，我就再用一次`transform`。我们需要将`(0..board.size())`范围分别转换为行列表和列列表。然后，我们需要返回一个包含主对角线和次对角线的集合:\n\n```cpp\ntypedef vector<Line> Lines;\n\nauto allLines = [](auto board) {\n    auto range = toRange(board);\n    return transform_all<Lines>(range, [board](auto index) { return \n        line(board, index);});\n};\n\nauto allColumns = [](auto board) {\n    auto range = toRange(board);\n    return transform_all<Lines>(range, [board](auto index) { return \n        column(board, index);});\n};\n\nauto allDiagonals = [](auto board) -> Lines {\n    return {mainDiagonal(board), secondaryDiagonal(board)};\n};\n```\n\n我们只需要一件事——一种连接三个集合的方法。由于向量没有实现这一点，推荐的解决方案是使用`insert`和`move_iterator`，从而在第一个集合的末尾移动第二个集合中的项目:\n\n```cpp\nauto concatenate = [](auto first, const auto second){\n    auto result(first);\n    result.insert(result.end(), make_move_iterator(second.begin()), \n        make_move_iterator(second.end()));\n    return result;\n};\n\n```\n\n然后，我们将这三个集合合并为两个步骤:\n\n```cpp\nauto concatenate3 = [](auto first, auto const second, auto const third){\n    return concatenate(concatenate(first, second), third);\n};\n```\n\n我们现在可以从板上获取线、列和对角线的完整列表，如您在以下测试中所见:\n\n```cpp\nauto allLinesColumnsAndDiagonals = [](const auto board) {\n    return concatenate3(allLines(board), allColumns(board),  \n        allDiagonals(board));\n};\n\nTEST_CASE(\"all lines, columns and diagonals\"){\n    Board board {\n        {'X', 'X', 'X'},\n        {' ', 'O', ' '},\n        {' ', ' ', 'O'}\n    };\n\n    Lines expected {\n        {'X', 'X', 'X'},\n        {' ', 'O', ' '},\n        {' ', ' ', 'O'},\n        {'X', ' ', ' '},\n        {'X', 'O', ' '},\n        {'X', ' ', 'O'},\n        {'X', 'O', 'O'},\n        {'X', 'O', ' '}\n    };\n\n    auto all = allLinesColumnsAndDiagonals(board);\n    CHECK_EQ(expected, all);\n}\n```\n\n查明`X`是否获胜只剩下最后一步了。我们有所有线条、列和对角线的列表。我们知道如何检查一行是否充满`X`。我们只需要检查列表中是否有任何一行被`X`填充。\n\n# 使用任意 _ 检查 X 是否赢了\n\n类似于`all_of`，另一个函数构造帮助我们表达应用于集合的谓词之间的或条件。在 STL 中，这个构造在`any_of`函数中实现。让我们看看它的实际效果:\n\n```cpp\nTEST_CASE(\"any_of\"){\n    vector<char> abc = {'a', 'b', 'c'};\n\n    CHECK(any_of(abc.begin(), abc.end(), trueForAll));\n    CHECK(!any_of(abc.begin(), abc.end(), falseForAll));\n    CHECK(any_of(abc.begin(), abc.end(), equalsChara));\n    CHECK(any_of(abc.begin(), abc.end(), notChard));\n}\n```\n\n像我们在本章中看到的其他高级函数一样，它在集合的开头和结尾使用迭代器。和往常一样，我喜欢把事情简单化；因为我通常在完整集合上使用`any_of`，所以我喜欢实现我的助手函数:\n\n```cpp\nauto any_of_collection = [](const auto& collection, const auto& fn){\n return any_of(collection.begin(), collection.end(), fn);\n};\n\nTEST_CASE(\"any_of_collection\"){\n    vector<char> abc = {'a', 'b', 'c'};\n\n    CHECK(any_of_collection(abc, trueForAll));\n    CHECK(!any_of_collection(abc, falseForAll));\n    CHECK(any_of_collection(abc, equalsChara));\n    CHECK(any_of_collection(abc, notChard));\n}\n```\n\n我们只需要在我们的列表中使用它来检查`X`是否是赢家:\n\n```cpp\nauto xWins = [](const auto& board){\n    return any_of_collection(allLinesColumnsAndDiagonals(board), \n        lineFilledWithX);\n};\n\nTEST_CASE(\"X wins\"){\n    Board board{\n        {'X', 'X', 'X'},\n        {' ', 'O', ' '},\n        {' ', ' ', 'O'}\n    };\n\n    CHECK(xWins(board));\n}\n```\n\n这就是我们对`X`获胜条件的解决方案。在我们继续之前，最好能在控制台上显示该板。现在是时候使用`map`/`transform`——`reduce`的近亲了，或者，正如在 STL 中所知的那样，`accumulate`。\n\n# 使用减少/累积显示板\n\n我们想在控制台上显示该板。通常情况下，我们会使用一个可变函数，比如`cout`来实现；然而，请记住我们是如何讨论的，虽然我们需要保持程序的某些部分可变，比如那些调用`cout`的部分，但我们应该将它们限制在最低限度。那么，还有什么选择呢？嗯，我们需要再次考虑输入和输出——我们想写一个函数，它以`board`为输入，并返回一个`string`表示，我们可以通过使用一个可变函数(如`cout`)来显示它。让我们以测试的形式写下我们想要的:\n\n```cpp\nTEST_CASE(\"board to string\"){\n    Board board{\n        {'X', 'X', 'X'},\n        {' ', 'O', ' '},\n        {' ', ' ', 'O'}\n    };\n    string expected = \"XXX\\n O \\n  O\\n\";\n\n    CHECK_EQ(expected, boardToString(board));\n}\n```\n\n为了获得这个结果，我们首先需要将每一行从`board`转换成它的`string`表示。我们的线路是`vector<char>`，我们需要把它变成`string`；虽然有很多方法可以做到这一点，但请允许我使用带有`string`输出的`transformAll`功能:\n\n```cpp\nauto lineToString = [](const auto& line){\n    return transformAll<string>(line, [](const auto token) -> char { \n        return token;});\n};\n\nTEST_CASE(\"line to string\"){\n    Line line {\n        ' ', 'X', 'O'\n    };\n\n    CHECK_EQ(\" XO\", lineToString(line));\n}\n```\n\n写了这个函数，我们可以很容易的把一块板变成`vector<string>`:\n\n```cpp\nauto boardToLinesString = [](const auto board){\n    return transformAll<vector<string>>(board, lineToString);\n};\n\nTEST_CASE(\"board to lines string\"){\n    Board board{\n        {'X', 'X', 'X'},\n        {' ', 'O', ' '},\n        {' ', ' ', 'O'}\n    };\n    vector<string> expected{\n        \"XXX\",\n        \" O \",\n        \"  O\"\n    };\n\n    CHECK_EQ(expected, boardToLinesString(board));\n}\n```\n\n最后一步是将这些弦与它们之间的`\\n`结合起来。我们经常需要以各种方式组合一个集合的元素；这就是`reduce`发挥作用的地方。在函数式编程中，`reduce`是取一个集合、一个初始值(例如，空`strings`)和一个累加函数的运算。该函数接受两个参数，对它们执行运算，并返回一个新值。\n\n让我们看几个例子。首先，有一个添加数字向量的经典例子:\n\n```cpp\nTEST_CASE(\"accumulate\"){\n    vector<int> values = {1, 12, 23, 45};\n\n    auto add = [](int first, int second){return first + second;};\n    int result = accumulate(values.begin(), values.end(), 0, add);\n    CHECK_EQ(1 + 12 + 23 + 45, result);\n}\n```\n\n下面向我们展示了如果我们需要将向量与初始值相加，应该怎么做:\n\n```cpp\n    int resultWithInit100 = accumulate(values.begin(), values.end(),  \n        100, add);\n    CHECK_EQ(1oo + 1 + 12 + 23 + 45, resultWithInit100);\n```\n\n同样，我们可以串联`strings`:\n\n```cpp\n    vector<string> strings {\"Alex\", \"is\", \"here\"};\n    auto concatenate = [](const string& first, const string& second) ->  \n        string{\n        return first + second;\n    };\n    string concatenated = accumulate(strings.begin(), strings.end(),  \n        string(), concatenate);\n    CHECK_EQ(\"Alexishere\", concatenated);\n```\n\n或者，我们可以添加一个前缀:\n\n```cpp\n    string concatenatedWithPrefix = accumulate(strings.begin(),  \n        strings.end(), string(\"Pre_\"), concatenate);\n    CHECK_EQ(\"Pre_Alexishere\", concatenatedWithPrefix);\n```\n\n像往常一样，我更喜欢一个简化的实现，它可以在一个完整的集合上工作，并使用默认值作为初始值。有了一点`decltype`魔法，就很容易实现了:\n\n```cpp\nauto accumulateAll = [](auto source, auto lambda){\n    return accumulate(source.begin(), source.end(), typename  \n        decltype(source)::value_type(), lambda);\n};\n```\n\n这就给我们留下了一个任务——编写一个使用换行符组合`string`行的 concatenate 实现:\n\n```cpp\nauto boardToString = [](const auto board){\n    auto linesAsString = boardToLinesString(board);\n    return accumulateAll(linesAsString, \n        [](string current, string lineAsString) { return current + lineAsString + \"\\n\"; }\n    );\n};\nTEST_CASE(\"board to string\"){\n    Board board{\n        {'X', 'X', 'X'},\n        {' ', 'O', ' '},\n        {' ', ' ', 'O'}\n    };\n    string expected = \"XXX\\n O \\n  O\\n\";\n\n    CHECK_EQ(expected, boardToString(board));\n}\n```\n\n我们现在可以使用`cout << boardToString`来显示我们的棋盘。再一次，我们使用了一些函数转换和很少的定制代码来把所有的东西放在一起。真不错。\n\n`map` / `reduce`组合，或者说，如 STL 中所知的`transform` / `accumulate`组合，非常强大，在函数式编程中非常常见。我们经常需要从一个集合开始，多次将其转换为另一个集合，然后组合集合的元素。这是一个如此强大的概念，以至于它是大数据分析的核心，使用 Apache Hadoop 等工具，尽管是在机器级别上扩展的。这表明，通过掌握这些转换，你可能会在意想不到的情况下应用它们，使你成为一个不可或缺的问题解决者。很酷，不是吗？\n\n# 使用 find_if 显示特定的获胜详细信息\n\n我们很高兴我们已经解决了`X`的井字游戏结果问题。然而，一如既往，需求会改变；我们现在不仅要说`X`是否赢了，还要说怎么赢的——在哪条线上，还是列上，还是对角线上。\n\n幸运的是，我们已经具备了大部分要素。因为它们是非常小的功能，我们只需要以一种有助于我们的方式重组它们。让我们从数据的角度再考虑一下——我们的输入数据现在是线条、列和对角线的集合；我们的结果应该是像`X`在第一线赢了*这样的事情。我们只需要增强我们的数据结构，以包含每一行的信息；我们用`map`吧:*\n\n```cpp\n    map<string, Line> linesWithDescription{\n        {\"first line\", line(board, 0)},\n        {\"second line\", line(board, 1)},\n        {\"last line\", line(board, 2)},\n        {\"first column\", column(board, 0)},\n        {\"second column\", column(board, 1)},\n        {\"last column\", column(board, 2)},\n        {\"main diagonal\", mainDiagonal(board)},\n        {\"secondary diagonal\", secondaryDiagonal(board)},\n    };\n```\n\n我们知道如何找到`X`获胜的地方——通过我们的`lineFilledWithX`谓词函数。现在，我们只需要在地图中搜索符合`lineFilledWithX`谓词的行并返回相应的消息。\n\n同样，这是函数式编程中常见的操作。在 STL 中，是用`find_if`函数实现的。让我们看看它的实际效果:\n\n```cpp\nauto equals1 = [](auto value){ return value == 1; };\nauto greaterThan11 = [](auto value) { return value > 11; };\nauto greaterThan50 = [](auto value) { return value > 50; };\n\nTEST_CASE(\"find if\"){\n    vector<int> values{1, 12, 23, 45};\n\n    auto result1 = find_if(values.begin(), values.end(), equals1);\n    CHECK_EQ(*result1, 1);\n\n    auto result12 = find_if(values.begin(), values.end(), \n        greaterThan11);\n    CHECK_EQ(*result12, 12);\n\n    auto resultNotFound = find_if(values.begin(), values.end(), \n        greaterThan50);\n    CHECK_EQ(resultNotFound, values.end());\n}\n```\n\n`find_if`基于谓词查找集合，并返回一个指向结果的指针，如果没有找到，则返回一个指向`end()`迭代器的指针。\n\n像往常一样，让我们实现允许在整个集合中进行搜索的包装器实现。我们需要用一种方式来表示`not found`值；幸运的是，我们可以使用 STL 中的可选类型:\n\n```cpp\nauto findInCollection = [](const auto& collection, auto fn){\n    auto result = find_if(collection.begin(), collection.end(), fn);\n    return (result == collection.end()) ? nullopt : optional(*result);\n};\n\nTEST_CASE(\"find in collection\"){\n    vector<int> values {1, 12, 23, 45};\n\n    auto result1 = findInCollection(values, equals1);\n    CHECK_EQ(result1, 1);\n\n    auto result12 = findInCollection(values, greaterThan11);\n    CHECK_EQ(result12, 12);\n\n    auto resultNotFound = findInCollection(values, greaterThan50);\n    CHECK(!resultNotFound.has_value());\n}\n```\n\n现在，我们可以轻松实现新的要求。使用我们新实现的`findInCollection`函数，可以找到用`X`填充的行，并返回相应的描述。因此，我们可以告诉用户`X`是如何获胜的——在线上、柱上或对角线上:\n\n```cpp\nauto howDidXWin = [](const auto& board){\n    map<string, Line> linesWithDescription = {\n        {\"first line\", line(board, 0)},\n        {\"second line\", line(board, 1)},\n        {\"last line\", line(board, 2)},\n        {\"first column\", column(board, 0)},\n        {\"second column\", column(board, 1)},\n        {\"last column\", column(board, 2)},\n        {\"main diagonal\", mainDiagonal(board)},\n        {\"secondary diagonal\", secondaryDiagonal(board)},\n    };\n    auto found = findInCollection(linesWithDescription,[](auto value) \n        {return lineFilledWithX(value.second);}); \n    return found.has_value() ? found->first : \"X did not win\";\n};\n```\n\n当然，我们应该从板子上生成地图，而不是硬编码。我将把这个练习留给读者；只需再次使用我们最喜欢的`transform`功能。\n\n# 完成我们的解决方案\n\n虽然我们已经实现了`X`获胜的解决方案，但是我们现在需要研究其他可能的输出。先拿最简单的一个吧——`O`赢了。\n\n# 检查 O 是否赢了\n\n检查`O`是否获胜很容易——我们只需要对我们的功能进行一个小小的改变。我们需要一个新的函数`oWins`，它检查是否有任何一行、一列或一条对角线填充了`O`标记:\n\n```cpp\nauto oWins = [](auto const board){\n    return any_of_collection(allLinesColumnsAndDiagonals(board),  \n        lineFilledWithO);\n};\nTEST_CASE(\"O wins\"){\n    Board board = {\n        {'X', 'O', 'X'},\n        {' ', 'O', ' '},\n        {' ', 'O', 'X'}\n    };\n\n    CHECK(oWins(board));\n}\n```\n\n我们使用与`xWins`相同的实现，只是作为参数传递的λ略有变化。\n\n# 使用 none_of 检查绘图\n\n`draw`呢？嗯，当`board`参数满了，而`X`和`O`都没有赢的时候，就会出现平局:\n\n```cpp\nauto draw = [](const auto& board){\n    return full(board) && !xWins(board) && !oWins(board); \n};\n\nTEST_CASE(\"draw\"){\n    Board board {\n        {'X', 'O', 'X'},\n        {'O', 'O', 'X'},\n        {'X', 'X', 'O'}\n    };\n\n    CHECK(draw(board));\n}\n```\n\n全板是什么意思？意思是每一行都写满了:\n\n```cpp\nauto full = [](const auto& board){\n    return all_of_collection(board, fullLine);\n};\n```\n\n我们如何知道线路是否已满？嗯，我们知道，如果该行中没有一个令牌是空的(`' '`)令牌，则该行是满的。正如您现在可能已经预料到的那样，STL 中有一个名为`none_of`的函数可以为我们检查这一点:\n\n```cpp\nauto noneOf = [](const auto& collection, auto fn){\n    return none_of(collection.begin(), collection.end(), fn);\n};\n\nauto isEmpty = [](const auto token){return token == ' ';};\nauto fullLine = [](const auto& line){\n    return noneOf(line, isEmpty);\n};\n```\n\n# 检查正在进行的游戏\n\n最后一种情况是游戏还在进行的时候。最简单的方法就是检查游戏没有赢，棋盘还没有满:\n\n```cpp\nauto inProgress = [](const auto& board){\n    return !full(board) && !xWins(board) && !oWins(board); \n};\nTEST_CASE(\"in progress\"){\n    Board board {\n        {'X', 'O', 'X'},\n        {'O', ' ', 'X'},\n        {'X', 'X', 'O'}\n    };\n\n    CHECK(inProgress(board));\n}\n```\n\n恭喜，我们成功了！我们已经使用许多函数转换实现了井字游戏结果问题；一些我们自己的小羊羔。但是，更重要的是，我们已经学会了如何作为一个函数式程序员开始思考——清楚地定义输入数据，清楚地定义输出数据，并弄清楚可以将输入数据转化为所需输出数据的转换。\n\n# 使用可选类型的错误管理\n\n到目前为止，我们已经有了一个用函数风格编写的小程序。但是错误案例呢？我们如何应对他们？\n\n很明显，我们仍然可以使用 C++ 机制——返回值或异常。但是函数式编程也着眼于另一种方式——将错误视为数据。\n\n当我们实现我们的`find_if`包装器时，我们已经看到了这个技术的一个例子:\n\n```cpp\nauto findInCollection = [](const auto& collection, auto fn){\n    auto result = find_if(collection.begin(), collection.end(), fn);\n    return (result == collection.end()) ? nullopt : optional(*result);\n};\n```\n\n我们没有抛出异常或返回`collection.end()`，这是一个局部值，而是使用了`optional`类型。如其名称所述，可选类型表示一个变量，该变量可能有值，也可能没有值。可选值可以初始化，可以用基础类型支持的值，也可以用`nullopt`—可以说是默认的非值。\n\n当在我们的代码中遇到一个可选值时，我们需要考虑它，就像我们在检查`X`如何获胜的函数中所做的那样:\n\n```cpp\nreturn found.has_value() ? found->first : \"X did not win\";\n```\n\n因此，*没有发现*的情况不是错误；相反，它是我们代码和数据的正常部分。实际上，处理这种情况的另一种方法是增强`findInCollection`以在没有发现任何东西时返回指定的值:\n\n```cpp\nauto findInCollectionWithDefault = [](auto collection, auto \n    defaultResult, auto lambda){\n        auto result = findInCollection(collection, lambda);\n        return result.has_value() ? (*result) : defaultResult;\n}; \n```\n\n我们现在可以使用`findInCollectionWithDefault`在`X`未获胜的棋盘上呼叫`howDidXWin`时获取`X did not win`信息:\n\n```cpp\nauto howDidXWin = [](auto const board){\n    map<string, Line> linesWithDescription = {\n        {\"first line\", line(board, 0)},\n        {\"second line\", line(board, 1)},\n        {\"last line\", line(board, 2)},\n        {\"first column\", column(board, 0)},\n        {\"second column\", column(board, 1)},\n        {\"last column\", column(board, 2)},\n        {\"main diagonal\", mainDiagonal(board)},\n        {\"secondary diagonal\", secondaryDiagonal(board)},\n        {\"diagonal\", secondaryDiagonal(board)},\n    };\n    auto xDidNotWin = make_pair(\"X did not win\", Line());\n    auto xWon = [](auto value){\n        return lineFilledWithX(value.second);\n    };\n\n    return findInCollectionWithDefault(linesWithDescription, xDidNotWin, xWon).first; \n};\n\nTEST_CASE(\"X did not win\"){\n    Board board {\n        {'X', 'X', ' '},\n        {' ', 'O', ' '},\n        {' ', ' ', 'O'}\n    };\n\n    CHECK_EQ(\"X did not win\", howDidXWin(board));\n}\n```\n\n我最好的建议是这样的——对所有异常情况使用异常，并使其他一切都成为数据结构的一部分。使用可选类型或带有默认值的转换。你会惊讶于错误管理变得如此简单和自然。\n\n# 摘要\n\n我们在这一章已经讲了很多内容！我们经历了一个发现之旅——我们从列出问题的输出和相应的输入开始，分解它们，并找出如何将输入转换为必需的输出。我们看到了小功能和功能操作如何在需要新功能时给我们带来灵活性。我们看到了如何使用`any`、`all`、`none`、`find_if`、`map` / `transform`、`reduce` / `accumulate`以及如何使用可选类型或默认值来支持代码中所有可能的情况。\n\n既然我们已经知道了如何以函数式的方式编写代码，那么在下一章中就该看看这种方法是如何与 OO 编程相适应的了。"
  },
  {
    "path": "docs/handson-func-prog-cpp/07.md",
    "content": "# 七、通过函数操作消除重复\n\n软件设计的一个关键原则是减少代码重复。功能性结构为减少代码重复提供了额外的机会。\n\n本章将涵盖以下主题:\n\n*   如何以及为什么要避免重复代码\n*   如何识别代码相似性\n*   使用 currying 删除某些类型的代码相似性\n*   使用组合消除某些类型的代码相似性\n*   使用 lambdas 或 composition 移除某些类型的代码相似性\n\n# 技术要求\n\n您将需要一个支持 C++ 17 的编译器。我用的是 GCC 7.3.0。\n\n代码可以在[网站上的`Chapter07`文件夹中找到。它包括并使用`doctest`，这是一个单头开源单元测试库。你可以在 https://github.com/onqtam/doctest](https://github.com/PacktPublishing/Hands-On-Functional-Programming-with-Cpp)的 GitHub 储存库中找到它。\n\n# 通过功能操作消除重复\n\n当我们只需要在一个地方更改代码，并且可以重组现有的代码片段时，长时间维护代码要容易得多。实现这一理想的最有效的方法之一是识别并消除代码中的重复。来自函数式编程的操作——部分应用、currying 和函数式组合——提供了许多机会来使代码变得更干净，并且具有有限的重复。\n\n但是首先，让我们了解什么是重复，为什么我们需要减少重复。首先我们来看看**不重复自己** ( **DRY** )原理，再来看看重复和代码相似度的关系。最后，我们将研究消除代码相似性的方法。\n\n# 干燥原理\n\n软件开发核心书籍的数量出乎意料的低。当然，有很多关于细节和帮助人们更好地理解想法的书，但是关于核心想法的书非常少，而且很旧。在核心书籍的名单上是作者的荣誉，也暗示了这个主题是极其重要的。许多程序员会把安德鲁·亨特和戴维·托马斯的《实用程序员》这本书放在这样的名单上。这本书出版于 1999 年，详细介绍了一个对长期使用大型代码库的人来说非常有意义的原则——DRY。\n\nDRY 原则的核心是基于这样一种理解，即代码是存储知识的一种方式。每个函数和每个数据成员都代表关于一个问题的知识。理想情况下，我们希望避免知识在系统中重复。换句话说，无论你在寻找什么，都应该只在一个地方。不幸的是，大多数代码库是 **WET** (缩写为要么**把所有东西都写两遍**，**我们喜欢打字**，要么**浪费大家的时间**)，而不是 DRY。\n\n然而，消除重复的想法由来已久。肯特·贝克曾在 20 世纪 90 年代将它作为**极限编程** ( **XP** )实践的一部分提到过。肯特·贝克描述了简单设计的四个要素，简单设计是获得或改进软件设计的思维工具。\n\n简单的设计意味着它可以做到以下几点:\n\n*   通过测试\n*   揭示意图\n*   减少重复\n*   元素较少\n\n我从 J.B .兰斯伯格那里学到了这些规则，他也致力于简化这些规则。他告诉我，在大多数情况下，专注于三件事就足够了——测试代码、改进名称和减少重复。\n\n但这并不是唯一提到消除重复的地方。该原则以各种方式出现在 Unix 设计哲学中，出现在**领域驱动设计** ( **DDD** )技术中，作为对**测试驱动开发** ( **TDD** )实践的帮助，以及许多其他方面。可以肯定地说，这是优秀软件设计的一个普遍原则，每当我们谈论在一个模块中构建代码时，使用它都是有意义的。\n\n# 重复和相似\n\n后来在我学习好的软件设计的旅程中，我意识到术语**复制**对于表达我们试图实现的理念非常有用，但是很难理解如何将其付诸实践。当我试图改进设计时，我为我所寻找的东西找到了一个更好的名字——我寻找**代码相似性**。一旦我发现相似之处，我会问它们是否表现出更深层次的重复，或者它们只是一个意外。\n\n我也及时注意到我在寻找一些特定类型的相似之处。这里有几个例子:\n\n*   相似的名称，要么是全名，要么是嵌入在函数、参数、方法、变量、常数、类、模块、命名空间等更长名称中的名称\n*   类似的参数列表\n*   类似的函数调用\n*   不同的代码试图达到相似的结果\n\n一般来说，我遵循这两个步骤:\n\n1.  首先，注意相似之处。\n2.  其次，决定是否移除相似性。\n\n当不确定相似性是否说明了设计更深层次的东西时，最好保留它。一旦你见过三次相似之处，最好也开始消除它们；这样，你就可以确定它违反了 DRY 原则，而不仅仅是一个意外。\n\n接下来，我们将看看通过功能操作可以消除的几类相似之处。\n\n# 解决部分应用的参数相似性\n\n在我们前面的章节中，您已经看到了这样的情况:一个函数被多次调用，其中一个参数的值相同。例如，请看我们井字游戏结果问题中的代码；我们有一个函数负责检查一行是否填充了令牌:\n\n```cpp\nauto lineFilledWith = [](const auto& line, const auto tokenToCheck){\n    return all_of_collection(line, [&tokenToCheck](auto const token){   \n        return token == tokenToCheck;});\n};\n```\n\n由于井字游戏使用了两个标记`X`和`O`，很明显我们会重复调用这个函数，其中`tokenToCheck`要么是`X`要么是`O`。消除这种相似性的通常方法是实现两个新功能，`lineFilledWithX`和`lineFilledWithO`:\n\n```cpp\nauto lineFilledWithX = [](const auto& line){\n    return lineFilledWith(line, 'X');\n};\n```\n\n这是一个可行的解决方案，但它仍然需要我们编写一个单独的函数和三行代码。正如我们已经看到的，我们在函数式编程中有另一种选择；我们可以简单地使用部分应用来获得相同的结果:\n\n```cpp\nauto lineFilledWithX = bind(lineFilledWith, _1, 'X'); \nauto lineFilledWithO = bind(lineFilledWith, _1, 'O');\n```\n\n我更喜欢在可能的情况下使用部分应用，因为这种类型的代码只是管道，我需要编写的管道越少越好。但是，在团队中使用部分应用时需要小心。每个团队成员都应该熟悉部分应用，并精通理解这种类型的代码。否则，使用部分应用只会让开发团队更难理解代码。\n\n# 用函数组合替换另一个函数相似性输出上的调用函数\n\n您可能已经注意到了以下代码中显示的模式:\n\n```cpp\nint processA(){\n    a  = f1(....)\n    b = f2(a, ...)\n    c = f3(b, ...)\n}\n```\n\n通常，如果你足够努力，你会在你的代码库中找到另一个做类似事情的函数:\n\n```cpp\nint processB(){\n    a  = f1Prime(....)\n    b = f2(a, ...)\n    c = f3(b, ...)\n}\n```\n\n这种相似性似乎有更深层次的原因，因为应用的复杂性会随着时间的推移而增长。我们通常从实现一个经过多个步骤的简单流程开始。然后，我们实现相同流程的变体，其中一些步骤重复，而其他步骤发生变化。有时，流程的变化包括改变步骤的顺序，或者调整一些步骤。\n\n在我们的实现中，这些步骤被转换成以各种方式组合在其他函数中的函数。但是，如果我们使用上一步的输出，并将其输入到下一步，我们在代码中有一个相似性，它不依赖于每个步骤做什么。\n\n为了消除这种相似性，我们通常会提取代码的相似部分并传递结果，如以下代码所示:\n\n```cpp\nint processA(){\n    a  = f1(....)\n    return doSomething(a)\n}\n\nint processB(){\n    a = f1Prime(....)\n    return doSomething(a)\n}\n\nint doSomething(auto a){\n    b = f2(a, ...)\n    return f3(b, ...)\n}\n```\n\n然而，在提取函数时，代码往往变得更难理解，也更难更改，如前面的代码所示。提取函数的公共部分没有考虑到这样一个事实，即代码实际上是一个链式调用。\n\n为了使这一点可见，我倾向于将这种代码模式重新格式化为一条语句，如下面的代码所示:\n\n```cpp\nprocessA = f3(f2(f1(....), ...), ...)\nprocessB = f3(f2(f1Prime(....), ...), ...)\n```\n\n虽然不是每个人都喜欢这种格式，但这两种调用之间的相似性和差异更明显。同样显而易见的是，我们有一个使用功能组合的解决方案——我们只需要用`f2`组合`f3`，并用`f1`或`f1Prime`组合结果，就可以得到我们想要的结果:\n\n```cpp\nC = f3 ∘ f2\nprocessA = C ∘ f1\nprocessB  = C ∘ f1Prime\n```\n\n这是一个非常强大的机械师！我们可以通过函数组合，在几行代码中创建无数的链调用组合。我们可以用一些表达我们代码真实性质的组合语句来替换隐藏的管道伪装成函数中语句的顺序。\n\n然而，正如我们在[第 4 章](04.html)、*功能组合的思想*中所看到的，这在 C++ 中并不一定是一项容易的任务，因为我们需要编写自己的`compose`函数来满足我们的特定情况。在 C++ 为函数组合提供更好的支持之前，我们被迫将这种机制保持在最低限度，并且只在相似性不仅很明显，而且我们预计它会随着时间的推移而增加的地方使用它。\n\n# 用高级函数消除结构相似性\n\n到目前为止，在我们的讨论中一直有一种模式——函数式编程帮助我们从代码中移除管道，并表达代码的真实结构。命令式编程使用一系列语句作为基本结构；函数式编程减少了序列，专注于函数的有趣玩法。\n\n当我们讨论结构相似性时，这一点最为明显。作为一种普遍的模式，结构相似性是指代码结构重复的情况，尽管不一定是通过调用相同的函数或使用相同的参数。为了看到它的实际应用，让我们从井字游戏代码中一个非常有趣的相似之处开始。这是我们在[第 6 章](06.html)、*函数思维中编写的代码——从数据输入到数据输出*:\n\n```cpp\nauto lineFilledWith = [](const auto& line, const auto& tokenToCheck){\n    return allOfCollection(line, [&tokenToCheck](const auto& token){  \n        return token == tokenToCheck;});\n};\n\nauto lineFilledWithX = bind(lineFilledWith, _1, 'X'); \nauto lineFilledWithO = bind(lineFilledWith, _1, 'O');\n\nauto xWins = [](const auto& board){\n    return any_of_collection(allLinesColumnsAndDiagonals(board), \n        lineFilledWithX);\n};\n\nauto oWins = [](const auto& board){\n    return any_of_collection(allLinesColumnsAndDiagonals(board), \n        lineFilledWithO);\n};\n\n```\n\n`xWins`和`oWins`函数看起来非常相似，因为它们都调用同一个函数作为第一个参数，而`lineFilledWith`函数的变体作为它们的第二个参数。让我们消除它们的相似性。首先，让我们移除`lineFilledWithX`和`lineFilledWithO`，并用它们的等效物`lineFilledWith`替换它们:\n\n```cpp\nauto xWins = [](const auto& board){\n    return any_of_collection(allLinesColumnsAndDiagonals(board), []  \n        (const auto& line) { return lineFilledWith(line, 'X');});\n};\n\nauto oWins = [](const auto& board){\n    return any_of_collection(allLinesColumnsAndDiagonals(board), []\n        (const auto& line) { return lineFilledWith(line, 'O');});\n};\n```\n\n既然相似性很明显，我们可以很容易地提取一个共同的函数:\n\n```cpp\nauto tokenWins = [](const auto& board, const auto& token){\n    return any_of_collection(allLinesColumnsAndDiagonals(board),  \n        [token](auto line) { return lineFilledWith(line, token);});\n};\nauto xWins = [](auto const board){\n    return tokenWins(board, 'X');\n};\n\nauto oWins = [](auto const board){\n    return tokenWins(board, 'O');\n}\n```\n\n我们还注意到`xWins`和`oWins`只是`tokenWins`的部分应用，让我们明确一下:\n\n```cpp\nauto xWins = bind(tokenWins, _1, 'X');\nauto oWins = bind(tokenWins, _1, 'O');\n```\n\n现在，让我们关注`tokenWins`:\n\n```cpp\nauto tokenWins = [](const auto& board, const auto& token){\n    return any_of_collection(allLinesColumnsAndDiagonals(board),  \n        [token](auto line) { return lineFilledWith(line, token);});\n};\n```\n\n首先，我们注意到我们传递到`any_of_collection`中的 lambda 是一个带有固定令牌参数的部分应用，因此让我们替换它:\n\n```cpp\nauto tokenWins = [](const auto& board, const auto& token){\n    return any_of_collection(\n            allLinesColumnsAndDiagonals(board), \n            bind(lineFilledWith, _1, token)\n    );\n};\n```\n\n现在这是一个相当小的功能，由于我们的部分应用，封装了大量的功率。然而，我们已经可以提取一个更高级别的函数，这将允许我们在不编写任何代码的情况下创建更多类似的函数。还不知道叫什么，就叫`foo`:\n\n```cpp\ntemplate <typename F, typename G, typename H>\nauto foo(F f, G g, H h){\n    return [=](auto first, auto second){\n    return f(g(first), \n    bind(h, _1, second));\n    };\n}\nauto tokenWins = compose(any_of_collection, allLinesColumnsAndDiagonals, lineFilledWith);\n```\n\n我们的`foo`函数显示了代码的结构，但它相当不可读，所以让我们更好地命名一下:\n\n```cpp\ntemplate <typename CollectionBooleanOperation, typename CollectionProvider, typename Predicate>\nauto booleanOperationOnProvidedCollection(CollectionBooleanOperation collectionBooleanOperation, CollectionProvider collectionProvider, Predicate predicate){\n    return [=](auto collectionProviderSeed, auto predicateFirstParameter){\n      return collectionBooleanOperation(collectionProvider(collectionProviderSeed), \n              bind(predicate, _1, predicateFirstParameter));\n  };\n}\nauto tokenWins = booleanOperationOnProvidedCollection(any_of_collection, allLinesColumnsAndDiagonals, lineFilledWith);\n```\n\n我们引入了更高层次的抽象，这会使代码更难理解。另一方面，我们在一行代码中创建了`f(g(first), bind(h, _1, second))`表单的函数。\n\n代码更好吗？这取决于背景、你的判断以及你和你的同事对更高级功能的熟悉程度。然而，请记住——抽象虽然非常强大，但也是有代价的。抽象更难理解，但是如果你在抽象中说*，你可以用非常强大的方式组合它们。使用这些更高层次的功能就像从头开始构建一门语言——它使你能够在不同的层次上交流，但它也为其他人创造了进入的障碍。谨慎使用抽象概念！*\n\n *# 使用高级函数移除隐藏循环\n\n代码中经常会遇到结构重复的一个特殊例子，我最终将其称为**隐藏循环**。隐藏循环的思想是我们在一个序列中多次使用相同的代码结构。然而，诀窍是被调用的函数或参数不必相同；因为函数编程的基本思想是函数也是数据，所以我们可以将这些结构视为数据结构上的循环，这些数据结构也可能存储我们调用的函数。\n\n我通常在一系列`if`语句中看到这种模式。事实上，我是在使用井字游戏结果问题来促进实践的过程中开始看到它们的。这个问题的通常解决方案，在一个**面向对象编程** ( **OOP** )或命令式语言中，看起来像下面的代码所示:\n\n```cpp\nenum Result {\n    XWins,\n    OWins,\n    GameNotOverYet,\n    Draw\n};\n\nResult winner(const Board& board){ \n    if(board.anyLineFilledWith(Token::X) ||    \n        board.anyColumnFilledWith(Token::X) || \n        board.anyDiagonalFilledWith(Token::X)) \n    return XWins; \n\n    if(board.anyLineFilledWith(Token::O) ||  \n        board.anyColumnFilledWith(Token::O) ||  \n        board.anyDiagonalFilledWith(Token::O)) \n    return OWins; \n\n    if(board.notFilledYet()) \n    return GameNotOverYet; \n\nreturn Draw; \n}\n```\n\n在前面的示例中，`enum`标记包含三个值:\n\n```cpp\nenum Token {\n    X,\n    O,\n    Blank\n};\n\n```\n\n`Board`类看起来是这样的:\n\n```cpp\nusing Line = vector<Token>;\n\nclass Board{\n    private: \n        const vector<Line> _board;\n\n    public: \n        Board() : _board{Line(3, Token::Blank), Line(3, Token::Blank),  \n            Line(3, Token::Blank)}{}\n        Board(const vector<Line>& initial) : _board{initial}{}\n...\n}\n```\n\n`anyLineFilledWith`、`anyColumnFilledWith`、`anyDiagonalFilledWith`、`notFilledYet`的实现非常相似；`anyLineFilledWith`的一个非常简单的实现，假设一个 3×3 的板，如下所示:\n\n```cpp\n        bool anyLineFilledWith(const Token& token) const{\n            for(int i = 0; i < 3; ++ i){\n                if(_board[i][0] == token && _board[i][1] == token &&  \n                    _board[i][2] == token){\n                    return true;\n                }\n            }\n            return false;\n        };\n```\n\n然而，我们对底层实现不太感兴趣，而对前面 winner 函数的相似性更感兴趣。首先，`if`语句中的条件用不同的参数重复。但是，更有趣的是，有一个结构重复如下:\n\n```cpp\nif(condition) return value;\n```\n\n如果您看到这样一个结构，它使用数据而不是不同的函数，您会立即注意到它是一个隐藏的循环。当涉及到函数调用时，我们不会注意到这种类型的重复，因为我们没有受过将函数视为数据的训练。但事实就是如此。\n\n在我们消除这种相似性之前，让我们简化一下条件。我将使所有条件函数都不带参数，通过部分函数的神奇应用:\n\n```cpp\nauto tokenWins = [](const auto board, const auto& token){\n    return board.anyLineFilledWith(token) ||   \nboard.anyColumnFilledWith(token) || board.anyDiagonalFilledWith(token);\n};\n\nauto xWins = bind(tokenWins, _1, Token::X);\nauto oWins = bind(tokenWins, _1, Token::O);\n\nauto gameNotOverYet = [](auto board){\n    return board.notFilledYet();\n};\n\nResult winner(const Board& board){ \n    auto gameNotOverYetOnBoard = bind(gameNotOverYet, board);\n    auto xWinsOnBoard = bind(xWins, board);\n    auto oWinsOnBoard = bind(oWins, board);\n\n    if(xWins()) \n        return XWins; \n\n    if(oWins())\n        return OWins; \n\n    if(gameNotOverYetOnBoard()) \n        return GameNotOverYet; \n\n    return Draw; \n}\n```\n\n我们的下一步是移除四个不同条件之间的差异，并用循环替换相似性。我们只需要有一个 *(lambda，result)* 的对列表，并使用一个更高级的函数如`find_if`为我们做循环:\n\n```cpp\nauto True = [](){\n    return true;\n};\n\nResult winner(Board board){\n    auto gameNotOverYetOnBoard = bind(gameNotOverYet, board);\n    auto xWinsOnBoard = bind(xWins, board);\n    auto oWinsOnBoard = bind(oWins, board);\n\n    vector<pair<function<bool()>, Result>> rules = {\n        {xWins, XWins},\n        {oWins, OWins},\n        {gameNotOverYetOnBoard, GameNotOverYet},\n        {True, Draw}\n    };\n\n    auto theRule = find_if(rules.begin(), rules.end(), [](auto pair){\n            return pair.first();\n            });\n    // theRule will always be found, the {True, Draw} by default.\n    return theRule->second;\n}\n```\n\n拼图的最后一块是确保我们的代码返回`Draw`如果没有其他的工作。由于`find_if`返回符合规则的第一个元素，我们只需要在最后有`Draw`，关联一个总是返回`true`的函数。我把这个函数恰当地命名为`True`。\n\n这段代码对我们有什么用？嗯，它有几个优点。首先，我们可以很容易地添加一对新的条件和结果，例如，如果我们曾经得到在多维度或有更多玩家的情况下实现井字游戏变体的请求。第二，代码更短。第三，经过一些修改，我们获得了一个简单的规则引擎，尽管它非常通用:\n\n```cpp\nauto True = [](){\n    return true;\n};\n\nusing Rule = pair<function<bool()>, Result>;\n\nauto condition = [](auto rule){\n    return rule.first();\n};\n\nauto result = [](auto rule){\n    return rule.second;\n};\n\n// assumes that a rule is always found\nauto findTheRule = [](const auto& rules){\n    return *find_if(rules.begin(), rules.end(), [](auto rule){\n return condition(rule);\n });\n};\n\nauto resultForFirstRuleThatApplies = [](auto rules){\n    return result(findTheRule(rules));\n};\n\nResult winner(Board board){\n    auto gameNotOverYetOnBoard = bind(gameNotOverYet, board);\n    vector<Rule> rules {\n        {xWins, XWins},\n        {oWins, OWins},\n        {gameNotOverYetOnBoard, GameNotOverYet},\n        {True, Draw}\n    };\n\n    return resultForFirstRuleThatApplies(rules);\n}\n```\n\n上一个示例中唯一的特定代码是规则列表。其他的都很一般，可以在多个问题上重用。\n\n像往常一样，进入更高的抽象层次是要付出代价的。我们花时间尽可能清楚地命名事物，我相信这段代码非常容易阅读。然而，很多人可能并不熟悉它。\n\n另一个可能的问题是内存使用。代码的初始版本虽然重复相同的代码结构，但不需要为函数和结果对的列表分配内存；然而，衡量这些事情很重要，因为即使是初始代码也需要一些进程内存来存储额外的指令。\n\n这个例子向我们展示了如何通过一个非常简单的代码示例将重复的结构变成循环。这只是表面现象；这种模式非常普遍，我相信一旦你开始寻找，你会在代码中注意到它。\n\n# 摘要\n\n在本章中，我们研究了不同类型的代码相似性，以及如何通过各种函数式编程技术来减少它们。从可以用部分应用替换的重复参数，到可以转换成函数组合的链式调用，一直到可以通过更高级函数移除的结构相似性的奇妙复杂世界，您现在已经做好了充分的准备，可以注意和减少您使用的任何代码库中的相似性。\n\n正如你所注意到的，我们开始讨论代码结构和软件设计。这就引出了设计的另一个核心原则——高内聚、低耦合。我们如何使用函数来增加凝聚力？事实证明，这就是类非常有用的地方，这也是我们将在下一章讨论的内容。*"
  },
  {
    "path": "docs/handson-func-prog-cpp/08.md",
    "content": "# 八、使用类提高内聚性\n\n我们之前讨论过如何使用函数和函数上的操作来组织代码。然而，我们不能忽视过去几十年软件设计的流行范式——**面向对象编程** ( **OOP** )。OOP 能和函数式编程一起工作吗？两者有没有兼容性，还是完全脱节？\n\n事实证明，我们可以很容易地在类和函数之间进行转换。我通过我的朋友兼导师 J.B. Rainsberger 了解到，类只不过是一组部分应用的、内聚的纯函数。换句话说，我们可以使用类作为一个方便的位置来将内聚函数组合在一起。但是，为了做到这一点，我们需要理解高内聚原则，以及如何将函数转换成类，反之亦然。\n\n本章将涵盖以下主题:\n\n*   理解函数式编程和面向对象编程之间的联系\n*   理解类如何等同于内聚的、部分应用的纯函数集\n*   理解高凝聚力的必要性\n*   如何将纯函数分组到类中\n*   如何将一个类拆分成纯函数\n\n# 技术要求\n\n您将需要一个支持 C++ 17 的编译器。我用的是 GCC 7.3.0。\n\n代码可以在[的 GitHub 上找到。com/ PacktPublishing/动手-函数-用- Cpp 编程`Chapter08`文件夹中的](https://github.%E2%80%8Bcom/PacktPublishing/Hands-On-Functional-Programming-with-Cpp)。它包含并使用了`doctest`，这是一个单头开源单元测试库。你可以在它的 GitHub 资源库[中找到它。com/ onqtam/ doctest](https://github.%E2%80%8Bcom/onqtam/doctest) 。\n\n# 使用类提高凝聚力\n\n作为一名年轻的软件工程学生，我花了大量时间阅读面向对象程序设计。我试图理解面向对象是如何工作的，以及为什么它对现代软件开发如此重要。当时，大多数书籍都提到 OOP 是关于将代码组织成具有三个重要属性的类——封装、继承和多态。\n\n将近 20 年后，我意识到这种面向对象的愿景是相当有限的。面向对象程序主要是在施乐 PARC 开发的，该实验室以产生大量高质量的想法而闻名，例如图形用户界面、点击、鼠标和电子表格等。面向对象程序的创始人之一艾伦·凯(Alan Kay)从他的生物学专业知识中汲取了经验，同时面临着以支持新图形用户界面范式的方式组织大型代码库的问题。他提出了对象和类的概念，但是他在几年后声明这种代码组织风格的主要思想是消息传递。他对物体的看法是，它们应该以类似于细胞的方式交流，用代码模拟它们的化学信息。这就是为什么在他看来，OOP 语言中的方法调用应该是从一个单元或对象传递到另一个单元或对象的消息。\n\n一旦我们忘记了封装、继承和多态的思想，并且更加重视对象而不是类，那么功能范式和面向对象之间的摩擦就消失了。让我们看看这个面向对象的基本观点会把我们带到哪里。\n\n# 从功能角度看类\n\n有多种方法来看待类。在知识管理方面，我将一个*类*概念化为一个分类——这是一种将具有相似属性的实例(或对象)分组的方式。如果我们以这种方式来思考类，那么继承就成了一种自然属性——有些对象的类具有相似的属性，但它们也有各种不同的方式；说它们是相互继承的是解释它们的一个快速方法。\n\n然而，这种类的概念适用于我们的知识是准完整的领域。在软件开发领域，我们经常在有限的应用领域知识下工作，并且该领域随着时间的推移不断扩展。因此，我们需要关注概念之间存在薄弱环节的代码结构，允许我们在了解更多领域知识时更改或替换它们。那我们应该怎么上课呢？\n\n即使没有强大的关系，类在软件设计中也是一个强大的构造。它们提供了一种将方法分组，以及将方法与数据相结合的简洁方式。它们可以比函数更好地帮助我们导航更大的域，因为我们最终可以拥有成千上万个函数(如果不是更多的话)。那么，我们如何使用函数式编程的类呢？\n\n首先，您可能已经从我们前面的例子中注意到，函数式编程将复杂性放在了数据结构中。类通常是定义我们需要的数据结构的一种简洁方式，尤其是在 C++ 这样的语言中，它允许我们覆盖常见的运算符。常见的例子包括虚数、可测量单位(温度、长度、速度等)和货币数据结构。它们中的每一个都需要用特定的运算符和转换对数据进行分组。\n\n第二，我们编写的不可变函数倾向于自然地将自己分组到逻辑分类中。在我们的井字游戏示例中，我们有许多函数使用我们称之为**行**的数据结构；我们的自然倾向是将这些功能组合在一起。虽然没有什么能阻止我们在头文件中对它们进行分组，但是类提供了一个自然的地方来组合函数，以便我们以后可以找到它们。这导致了另一种类型的类——一个不可变的对象，它被初始化一次，并且它的每个操作都返回一个值，而不是改变它的状态。\n\n让我们更详细地看看面向对象设计和功能结构之间的等价性。\n\n# 面向对象的等价性——功能\n\n如果我们回到井字游戏结果解决方案，您会注意到有许多函数接收`board`作为参数:\n\n```cpp\nauto allLines = [](const auto& board) {\n...\n};\n\nauto allColumns = [](const auto& board) {\n...\n};\n\nauto mainDiagonal = [](const auto& board){\n...\n};\n\nauto secondaryDiagonal = [](const auto& board){\n ...\n};\n\nauto allDiagonals = [](const auto& board) -> Lines {\n...\n};\n\nauto allLinesColumnsAndDiagonals = [](const auto& board) {\n ...\n};\n```\n\n我们可以如下定义板，例如:\n\n```cpp\n    Board board {\n        {'X', 'X', 'X'},\n        {' ', 'O', ' '},\n        {' ', ' ', 'O'}\n    };\n```\n\n然后，当我们把它传递到函数中时，就像我们把板绑定到函数的参数上一样。现在，让我们为我们的`allLinesColumnsAndDiagonals`λ:\n\n```cpp\nauto bindAllToBoard = [](const auto& board){\n    return map<string, function<Lines  ()>>{\n        {\"allLinesColumnsAndDiagonals\",   \n            bind(allLinesColumnsAndDiagonals, board)},\n    };\n};\n```\n\nThe preceding lambda and many other examples we have looked at in earlier chapters call other lambdas, yet they don't capture them. For example, how does the `bindAllToBoard` lambda know about the `allLinesColumnsAndDiagonal` lambda? The only reason this works is because the lambdas are in a global scope. Moreover, with my compiler, when trying to capture `allLinesColumnsAndDiagonals`, I get the following error message: `<lambda>` *cannot be captured because it does not have automatic storage duration*, so it actually will not compile if I try to capture the lambda I use. I hope what I am about to say is self-explanatory, but I will say it anyway—for production code, avoid having lambdas (and anything else, really) in the global scope. This will also force you to capture the variables, which is a good thing because it makes dependencies explicit.\n\n现在，让我们看看我们如何称呼它:\n\n```cpp\nTEST_CASE(\"all lines, columns and diagonals with class-like structure\"){\n    Board board{\n        {'X', 'X', 'X'},\n        {' ', 'O', ' '},\n        {' ', ' ', 'O'}\n    };\n\n    Lines expected{\n        {'X', 'X', 'X'},\n        {' ', 'O', ' '},\n        {' ', ' ', 'O'},\n        {'X', ' ', ' '},\n        {'X', 'O', ' '},\n        {'X', ' ', 'O'},\n        {'X', 'O', 'O'},\n        {'X', 'O', ' '}\n    };\n\n    auto boardObject = bindAllToBoard(board);\n    auto all = boardObject[\"allLinesColumnsAndDiagonals\"]();\n    CHECK_EQ(expected, all);\n}\n```\n\n这让你想起什么了吗？让我们看看如何在课堂上写这个。我现在就给它取名`BoardResult`，因为我想不出更好的名字:\n\n```cpp\nclass BoardResult{\n    private:\n        const vector<Line> board;\n\n    public:\n        BoardResult(const vector<Line>& board) : board(board){\n        };\n\n         Lines allLinesColumnsAndDiagonals() const {\n             return concatenate3(allLines(board), allColumns(board),  \n                 allDiagonals(board));\n        }\n};\n\nTEST_CASE(\"all lines, columns and diagonals\"){\n BoardResult boardResult{{\n {'X', 'X', 'X'},\n {' ', 'O', ' '},\n {' ', ' ', 'O'}\n }};\n\n Lines expected {\n {'X', 'X', 'X'},\n {' ', 'O', ' '},\n {' ', ' ', 'O'},\n {'X', ' ', ' '},\n {'X', 'O', ' '},\n {'X', ' ', 'O'},\n {'X', 'O', 'O'},\n {'X', 'O', ' '}\n };\n\n auto all = boardResult.allLinesColumnsAndDiagonals();\n CHECK_EQ(expected, all);\n}\n```\n\n让我们回顾一下我们所做的事情:\n\n*   我们看到了更多以`board`为参数的函数。\n*   我们决定使用单独的函数将`board`参数绑定到一个值，从而获得表示函数名的字符串和绑定到该值的λ之间的映射。\n*   要调用它，我们需要首先调用初始化函数，然后我们可以调用部分应用的 lambda。\n*   *这看起来与类*极其相似——使用构造函数传入类方法之间共享的值，然后在不传入参数的情况下调用方法。\n\n因此，*类只是一组部分应用的 lambda*。但是我们如何对它们进行分组呢？\n\n# 高凝聚力原则\n\n在前面的例子中，我们根据函数采用相同参数`board`的事实，将函数分组到一个类中。我发现这是一个很好的经验法则。然而，我们可能会遇到更复杂的情况。\n\n为了理解原因，让我们来看看另一组函数(为了讨论的目的，实现被忽略了):\n\n```cpp\nusing Coordinate = pair<int, int>;\n\nauto accessAtCoordinates = [](const auto& board, const Coordinate& coordinate)\nauto mainDiagonalCoordinates = [](const auto& board)\nauto secondaryDiagonalCoordinates = [](const auto& board)\nauto columnCoordinates = [](const auto& board, const auto& columnIndex)\nauto lineCoordinates = [](const auto& board, const auto& lineIndex)\nauto projectCoordinates = [](const auto& board, const auto& coordinates)\n```\n\n这些函数应该是先前定义的`BoardResult`类的一部分吗？或者他们应该是另一个阶级的一部分，`Coordinate`？或者我们应该把他们分开，一部分去`BoardResult`班，另一部分去`Coordinate`班？\n\n我们以前的方法并不适用于所有的函数。如果我们只看它们的参数，所有前面的函数都取`board`。但也有一些是以`coordinate / coordinates`为参数的。`projectCoordinates`应该是`BoardResult`班的一部分，还是`Coordinate`班的一部分？\n\n更重要的是，我们可以遵循什么基本原则来将这些函数分组到类中？\n\n由于没有关于代码静态结构的明确答案，我们需要考虑代码的进化。我们需要问的问题如下:\n\n*   我们希望一起改变哪些功能？我们希望分别改变哪些功能？\n*   这条推理路线把我们引向了高衔接原则。但是，让我们先打开它。我们所说的凝聚力是什么意思？\n\n作为一名工程师和科学极客，我在物理世界中遇到了凝聚力。例如，当我们谈论水时，组成液体的分子倾向于粘在一起。我也遇到了凝聚力这种社会力量。作为一个与试图采用现代软件开发实践的客户一起工作的变更代理人，我经常不得不处理团队凝聚力——人们围绕一个观点聚集在一起的趋势。\n\n当我们谈论函数的内聚力时，没有物理力将它们推在一起，它们肯定不会坚持自己的观点。那么，我们在说什么？我们说的是一种神经力量。\n\n人类的大脑有巨大的能力去寻找模式和将相关的项目分类，结合一种不可思议的快速导航方式。将各种功能结合在一起的力量在我们的大脑中——这是从看似不相关的功能组合中找到的统一目标。\n\n高内聚是有用的，因为它允许我们理解和导航几个大的概念(比如板、线和记号)，而不是几十个或几百个小函数。此外，当(不是如果)我们需要添加一个新的行为或改变一个现有的行为时，高内聚性将允许我们快速找到一个新行为的位置，并以最小的改变添加到网络的剩余部分。\n\n内聚性是软件设计的一个度量标准，由拉里·康斯坦丁在 20 世纪 60 年代引入，作为他的*结构化设计*方法的一部分。通过经验，我们已经注意到，高凝聚力与低变革成本相关。\n\n让我们看看如何应用这个原则来将我们的函数分组到类中。\n\n# 将内聚函数分组到类中\n\n如前所述，我们可以从类的统一目的或概念的角度来看待内聚性。然而，我通常发现更彻底的方法是查看代码的演变，并根据未来可能发生的变化以及它可能触发的其他变化来决定函数组。\n\n你可能不会期望从我们的井字游戏结果问题中学到很多东西。它相当简单，而且似乎很有内涵。然而，在网上快速搜索，我们会发现很多井字游戏变体，包括以下几种:\n\n*   *m x n* 棋盘，胜者由连续的 *k* 项决定。一个有趣的变体是 Gomoku，在 *15 x 15* 棋盘上玩，赢家必须连续 5 局。\n*   3D 版本。\n*   使用数字作为标记，并将数字的总和作为获胜条件。\n*   使用单词作为代币，获胜者必须将 3 个单词与 1 个常用字母排成一行。\n*   使用*3×3*的 9 板进行游戏，其中胜者必须连续赢 3 板。\n\n这些甚至不是最奇怪的变体，如果你感兴趣，你可以在[https://en.wikipedia.org/wiki/Tic-tac-toe_variants](https://en.wikipedia.org/wiki/Tic-tac-toe_variants)查看维基百科关于这个主题的文章。\n\n那么，我们的实施会有什么变化呢？以下是一些建议:\n\n*   纸板尺寸\n*   玩家数量\n*   代币\n*   获胜规则(仍然连续，但条件不同)\n*   电路板拓扑—矩形、六边形、三角形或三维而非正方形\n\n幸运的是，如果我们只是改变板的大小，那么在我们的代码中应该没有什么真正的改变。事实上，我们可以通过一个更大的董事会，一切仍将工作。改变玩家数量需要非常小的改动；我们将假设它们有不同的令牌，我们只需要将`tokenWins`函数绑定到不同的令牌值。\n\n获胜规则怎么样？我们将假设规则仍然考虑线、列和对角线，因为这是井字游戏的基本要求，所有变体都使用它们。然而，我们可能没有考虑到一条完整的线、列或对角线；例如，在 Gomoku 中，我们需要在大小为 15 的行、列或对角线上连续查找 5 个令牌。看看我们的代码，这只是选择其他坐标组的问题；我们需要选择所有可能的五行坐标集，而不是搜索一整行来填充标记`X`。这意味着我们与坐标相关的功能发生了变化— `lineCoordinates`、`mainDiagonalCoordinates`、`columnCoordinates`和`secondaryDiagonalCoordinates`。它们将返回一个五行坐标的向量，这将导致`allLines`、`allColumns`和`allDiagonals`的变化，并且以我们连接它们的方式。\n\n如果令牌是一个单词，获胜条件是在单词之间找到一个公共字母，会怎么样？嗯，坐标是一样的，我们得到线、列和对角线的方式也是一样的。唯一的变化是在`fill`条件下，所以这个比较容易改变。\n\n这就引出了最后一个可能的变化——电路板拓扑。改变电路板拓扑将需要改变电路板数据结构，以及所有坐标和相应的功能。但是它需要改变线条、列和对角线规则吗？如果我们切换到 3D，那么我们会有更多的线，更多的列，以及不同的处理对角线的方式——所有这些都是坐标的变化。矩形板本身没有对角线*；我们将需要使用部分对角线，例如在 Gomoku 的情况下。至于六角板还是三角板，目前还没有明确的变体，暂时可以忽略。*\n\n *这向我们表明，如果我们想要为变革做准备，我们的职能应该围绕以下几条线进行分组:\n\n*   规则(也称为**填充条件**)\n*   坐标和投影-准备多组线、列和对角线的代码\n*   允许基于坐标访问的基本板结构\n\n就这么定了——我们需要把坐标和棋盘本身分开。虽然坐标数据类型将与棋盘数据类型同时改变，但提供线、列和对角线坐标的函数可能会因游戏规则而改变。因此，我们需要将电路板与其拓扑结构分开。\n\n在**面向对象设计** ( **OOD** )方面，我们需要在至少三个内聚类— `Rules`、`Topology`和`Board`之间分离程序的职责。`Rules`类包含游戏规则——基本上，当我们知道是平局或者游戏已经结束时，我们如何计算获胜条件。`Topology`课程是关于坐标和棋盘的结构。`Board`类应该是我们传递给算法的结构。\n\n那么，我们的功能应该如何构建呢？让我们列个清单:\n\n*   **规则** : `xWins`、`oWins`、`tokenWins`、`draw`、`inProgress`\n*   **拓扑** : `lineCoordinates`、`columnCoordinates`、`mainDiagonalCoordinates`和`secondaryDiagonalCoordinates`\n*   **板** : `accessAtCoordinates`和`allLinesColumnsAndDiagonals`\n*   **未定** : `allLines`、`allColumns`、`allDiagonals`、`mainDiagonal`和`secondaryDiagonal`\n\n总有一些函数可以成为更多结构的一部分。在我们的案例中，`allLines`应该是`Topology`类还是`Board`类的一部分？我能为两者找到同样好的论据。因此，解决方案留给编写代码的程序员的直觉。\n\n然而，这显示了您可以用来将这些函数分组到类中的方法——考虑什么可能会改变，并根据哪些函数将一起改变来对它们进行分组。\n\n然而，实践这种方法有一个警告——避免过度分析的陷阱。代码相对容易更改；当您对可能发生的变化知之甚少时，让它工作起来，直到在代码的相同区域出现新的需求。然后，您将对函数之间的关系有更好的了解。这种分析不会超过 15 分钟；任何额外的东西都很可能是过度设计。\n\n# 将一个类拆分成纯函数\n\n我们已经学会了如何将函数分组到一个类中。但是我们如何将代码从类转换成纯函数呢？很明显，这是相当简单的——我们只需要使函数变得纯粹，将它们移出类，然后添加一个初始化器，将它们绑定到它们需要的数据上。\n\n我们再举一个例子，一个用两个整数操作数执行数学运算的类:\n\n```cpp\nclass Calculator{\n    private:\n        int first;\n        int second;\n\n    public:\n        Calculator(int first, int second): first(first), second(second){}\n\n        int add() const {\n            return first + second;\n        }\n\n        int multiply() const {\n            return first * second;\n        }\n\n        int mod() const {\n            return first % second;\n        }\n\n};\n\nTEST_CASE(\"Adds\"){\n    Calculator calculator(1, 2);\n\n    int result = calculator.add();\n\n    CHECK_EQ(result, 3);\n}\n\nTEST_CASE(\"Multiplies\"){\n    Calculator calculator(3, 2);\n\n    int result = calculator.multiply();\n\n    CHECK_EQ(result, 6);\n}\n\nTEST_CASE(\"Modulo\"){\n    Calculator calculator(3, 2);\n\n    int result = calculator.mod();\n\n    CHECK_EQ(result, 1);\n}\n```\n\n为了使它更有趣，让我们添加另一个函数来恢复第一个参数:\n\n```cpp\nclass Calculator{\n...\n    int negateInt() const {\n        return -first;\n    }\n...\n}\n\nTEST_CASE(\"Revert\"){\n    Calculator calculator(3, 2);\n\n    int result = calculator.negateInt();\n\n    CHECK_EQ(result, -3);\n}\n```\n\n如何把这个类拆分成函数？幸运的是，功能已经很纯了。很明显，我们可以将函数提取为 lambdas:\n\n```cpp\nauto add = [](const auto first, const auto second){\n    return first + second;\n};\n\nauto multiply = [](const auto first, const auto second){\n    return first * second;\n};\n\nauto mod = [](const auto first, const auto second){\n    return first % second;\n};\n\nauto negateInt = [](const auto value){\n    return -value;\n};\n```\n\n如果您真的需要，让我们添加初始化器:\n\n```cpp\nauto initialize = [] (const auto first, const auto second) -> map<string, function<int()>>{\n    return  {\n        {\"add\", bind(add, first, second)},\n        {\"multiply\", bind(multiply, first, second)},\n        {\"mod\", bind(mod, first, second)},\n        {\"revert\", bind(revert, first)}\n    };\n};\n```\n\n然后，可以进行检查以确定一切正常:\n\n```cpp\nTEST_CASE(\"Adds\"){\n    auto calculator = initialize(1, 2);\n\n    int result = calculator[\"add\"]();\n\n    CHECK_EQ(result, 3);\n}\n\nTEST_CASE(\"Multiplies\"){\n    auto calculator = initialize(3, 2);\n\n    int result = calculator[\"multiply\"]();\n\n    CHECK_EQ(result, 6);\n}\n\nTEST_CASE(\"Modulo\"){\n    auto calculator = initialize(3, 2);\n\n    int result = calculator[\"mod\"]();\n\n    CHECK_EQ(result, 1);\n}\n\nTEST_CASE(\"Revert\"){\n    auto calculator = initialize(3, 2);\n\n    int result = calculator[\"revert\"]();\n\n    CHECK_EQ(result, -3);\n}\n\n```\n\n这留给我们的只有一个悬而未决的问题——如何才能把不纯的函数变成纯函数？我们将在[第 12 章](12.html)、*重构到纯函数*中详细讨论这个问题。现在，让我们记住本章的重要结论——*一个类只不过是一组内聚的、部分应用的函数。*\n\n# 摘要\n\n这一章我们经历了一段如此有趣的旅程！我们设法以一种非常优雅的方式将两种看似脱节的设计风格——面向对象和函数式编程——联系起来。纯函数可以根据内聚性的原则分组。我们只需要锻炼我们的想象力，考虑功能可能改变的场景，并决定将哪些功能组合在一起。相反，我们总是可以将函数从一个类转移到多个 lambdas 中，方法是使它们变纯并反转部分应用。\n\nOOP 和函数式编程没有摩擦；它们只是构造实现特性的代码的两种不同方式。\n\n我们使用功能进行软件设计的旅程还没有结束。在下一章中，我们将讨论如何使用**测试驱动开发** ( **TDD** )来设计功能。*"
  },
  {
    "path": "docs/handson-func-prog-cpp/09.md",
    "content": "# 九、面向函数式编程的测试驱动开发\n\n**测试驱动开发** ( **TDD** )是一种非常有用的软件设计方法。方法如下——我们首先编写一个失败的测试，然后实现最小的代码使测试通过，最后重构。我们以快速连续的小周期来做这件事。\n\n我们将研究纯函数如何简化测试，并提供一个将 TDD 应用于函数的例子。纯函数允许我们编写简单的测试，因为它们总是为相同的输入参数返回相同的值；因此，它们相当于大数据表。因此，我们可以编写测试来模拟输入和预期输出的数据表。\n\n本章将涵盖以下主题:\n\n*   如何使用数据驱动测试来利用纯函数\n*   了解 TDD 周期的基础知识\n*   如何用 TDD 设计一个纯函数\n\n# 技术要求\n\n你将需要一个支持 **C++ 17** 的编译器。我用的是 **GCC 7.3.0** 。\n\n代码可以在[的 GitHub 上找到。com/ PacktPublishing/动手-函数-用- Cpp 编程`Chapter09`文件夹中的](https://github.%E2%80%8Bcom/PacktPublishing/Hands-On-Functional-Programming-with-Cpp)。它包括并使用`doctest`，这是一个单头开源单元测试库。你可以在它的 GitHub 资源库[上找到它。com/ onqtam/ doctest](https://github.%E2%80%8Bcom/onqtam/doctest) 。\n\n# 功能编程的 TDD\n\n20 世纪 50 年代的编程与我们今天所知道的非常不同。我们现在所知道的程序员的工作被分成三个角色。程序员会编写要实现的算法。然后，一个专门的打字员会用一台特殊的机器把它打成穿孔卡片。然后程序员不得不手动验证穿孔卡是否正确——尽管有数百张。一旦高兴穿孔卡是正确的，程序员就会把它们带给主机操作员。因为现存的唯一的计算机都很大而且非常昂贵，所以花在计算机上的时间必须得到保护。主机操作员负责计算机，确保最重要的任务优先，因此一个新程序可能要等几天才能运行。一旦运行，程序将打印一个完整的堆栈跟踪。如果有错误，程序员必须看一张写满奇怪符号的很长的纸，找出可能是什么错误。这个过程缓慢，容易出错，而且不可预测。\n\n然而，一些工程师想出了一个主意。如果他们没有从一个失败的程序中得到复杂的读数，而是得到一个错误的明确指示呢？他们决定开始编写额外的代码来检查生产代码，并产生一个通过或失败的输出。他们会运行单元测试，而不是运行程序，或者除了运行程序之外。\n\n随着终端的发明，以及后来的个人电脑和强大的调试器的出现，一旦程序员的反馈循环变短，单元测试的实践就被遗忘了。然而，它从未完全消失，它突然以不同的形式回来了。\n\n在 20 世纪 90 年代，单元测试出人意料地再次出现。一群程序员，包括肯特·贝克、沃德·坎宁安和罗恩·杰弗里斯，尝试将开发实践发挥到极致。他们努力的结果叫做**极限编程** ( **XP** )。其中一个实践是单元测试，结果非常有趣。\n\n单元测试的常见做法是在编写代码后编写一些测试，作为测试周期的一部分。这些测试通常是由测试人员编写的——他们与实现这些特性的程序员是不同的群体。\n\n然而，最初的 XPers 以不同的方式尝试了单元测试。如果我们将测试和代码一起编写呢？更有趣的是，如果我们在实现之前编写测试*会怎么样？这导致了两种不同的技术——**测试优先编程** ( **TFP** )，包括先写几个测试，然后写一些代码让测试通过，还有 TDD，我们会更详细地讨论。*\n\n当我第一次听说这些技术时，我既困惑又着迷。你怎么能为不存在的东西编写测试呢？会有什么好处？幸运的是，在 J.B. Rainsberger 的支持下，我很快意识到了 TFP/TDD 的威力。我们的客户和利益相关者希望尽快在软件中实现工作特性。然而，他们往往无法解释自己想要什么样的功能。从测试开始意味着你完全理解要实现什么，并引导有用和有趣的对话来阐明需求。一旦需求明确，我们就可以专注于实现。此外，在 TDD 中，我们会尽快清理代码，这样我们就不会在久而久之制造混乱。这确实是一种令人惊讶的强大技术！\n\n但是让我们从头开始。我们如何编写单元测试？更重要的是，对于我们的目的来说，为纯函数编写单元测试更容易吗？\n\n# 纯函数的单元测试\n\n让我们首先看看单元测试是什么样子的。我已经在这本书里使用它们有一段时间了，我相信你已经理解了代码。但是现在是时候看一个特别的例子了:\n\n```cpp\nTEST_CASE(\"Greater Than\"){\n    int first = 3;\n    int second = 2;\n\n    bool result = greater<int>()(first, second);\n\n    CHECK(result);\n}\n```\n\n我们首先用特定的值初始化两个变量(单元测试的*排列*部分)。然后我们称之为生产代码(单元测试的*法案*部分)。最后，我们检查结果是否是我们期望的(单元测试的*断言*部分)。我们正在使用的名为`doctest`的库为允许我们编写单元测试的宏提供了实现。虽然 C++ 有更多的单元测试库，包括 GTest 和`Boost::unit_test`在内的著名例子，但是它们提供给程序员的工具非常相似。\n\n当谈论单元测试时，更重要的是找出使它们有用的特征。之前的测试规模小，重点突出，速度快，失败的原因只有一个。所有这些特性使测试变得有用，因为它易于编写、易于维护、清晰明了，并且如果引入了错误，还能提供有用且快速的反馈。\n\n就技术而言，前面的测试是基于示例的，因为它使用了一个非常具体的示例来检查代码的特定行为。我们将在[第 11 章](11.html)、*基于属性的测试*中看到一种不同的单元测试方法，称为**基于属性的测试**。由于这是一个基于示例的测试，一个有趣的问题出现了:如果我们要测试`greaterThan`函数，还有哪些示例会很有趣？\n\n好吧，我们想看看函数的所有可能的行为。那么，它可能的输出是什么？这里有一个列表:\n\n*   如果第一个值大于第二个值，则为 True\n*   如果第一个值小于第二个值，则为 False\n\n然而，这还不够。让我们添加边缘案例:\n\n*   如果第一个值等于第二个值，则为 False\n\n我们不要忘记可能的错误。传入值的域是什么？传递负值可以吗？浮点值？复数？这是与该职能部门的利益相关者进行的一次有趣的对话。\n\n现在让我们假设最简单的情况——函数将只接受有效的整数。这意味着我们还需要两个单元测试来检查第一个参数小于第二个参数以及两个参数相等的情况:\n\n```cpp\nTEST_CASE(\"Not Greater Than when first is less than second\"){\n    int first = 2;\n    int second = 3;\n\n    bool result = greater<int>()(first, second);\n\n    CHECK_FALSE(result);\n}\n\nTEST_CASE(\"Not Greater Than when first equals second\"){\n    int first = 2;\n\n    bool result = greater<int>()(first, first);\n\n    CHECK_FALSE(result);\n}\n```\n\n在[第 7 章](07.html)、*用功能操作消除重复*中，我们讨论了代码相似性以及如何消除它。这里，我们有一个测试之间相似的例子。去除它的一种方法是编写所谓的**数据驱动测试** ( **DDT** )。在滴滴涕中，我们写下输入和预期输出的清单，并在每一行数据上重复测试。不同的测试框架提供了不同的方法来编写这些测试；就目前而言，`doctest`对滴滴涕的支持有限，但我们仍然可以把它们写成如下:\n\n```cpp\nTEST_CASE(\"Greater than\") {\n    struct Data {\n        int first;\n        int second;\n        bool expected;\n } data;\n\n    SUBCASE(\"2 is greater than 1\") { data.first = 2; data.second = 1; \n        data.expected = true; }\n    SUBCASE(\"2 is not greater than 2\") { data.first = 2; data.second = \n         2; data.expected = false; }\n    SUBCASE(\"2 is not greater than 3\") { data.first = 2; data.second = \n         3; data.expected = false; }\n\n    CAPTURE(data);\n\n    CHECK_EQ(greaterThan(data.first, data.second), data.expected);\n}\n```\n\n如果我们忽略管道代码(定义`struct Data`和调用`CAPTURE`宏)，这显示了一种非常方便的编写测试的方式——尤其是对于纯函数。根据定义，如果纯函数在接收相同的输入时返回相同的输出，那么用输入/输出列表来测试它们是很自然的。\n\n滴滴涕的另一个便利之处是，我们只需在列表中添加一行新内容，就可以轻松添加新的测试。这尤其有助于我们在使用纯函数进行 TDD 时。\n\n# TDD 周期\n\nTDD 是一个开发周期，通常呈现如下:\n\n*   **红色**:写测试失败。\n*   **绿色**:通过对生产代码进行尽可能小的更改，使测试通过。\n*   **重构**:重新组织代码以包含新引入的行为。\n\n然而，TDD 从业者(比如我自己)会热衷于提及 TDD 周期从另一个步骤开始——思考。更准确地说，在编写第一个测试之前，让我们了解我们试图实现什么，并在现有代码中找到一个好的地方来添加行为。\n\n这个循环看似简单。然而，初学者经常纠结于第一个测试应该是什么，之后的测试应该是什么，以及编写过于复杂的代码。**重构**本身就是一门艺术，需要代码气味、设计原则和设计模式的知识。总的来说，最大的错误是想太多你想要获得的代码结构，并编写导致这种情况的测试。\n\n相反，TDD 需要心态的改变。我们从行为开始，一小步一小步地完善适合行为的代码结构。一个好的练习者的步数会小于 15 分钟。但这并不是 TDD 的唯一惊喜。\n\nTDD 最大的惊喜是，它可以让你探索同一问题的各种解决方案，从而教会你软件设计。你愿意探索的解决方案越多，你设计代码的能力就越强。当带着适当的好奇心练习时，TDD 是一种持续的学习体验。\n\n希望我让你对 TDD 产生了好奇。关于这个话题还有很多需要研究的，但是，对于我们的目标来说，尝试一个例子就足够了。既然我们谈论的是函数式编程，我们将使用 TDD 来设计一个纯函数。\n\n# 示例–使用 TDD 设计纯函数\n\n我们再次需要一个问题来展示 TDD 的实际应用。因为我喜欢用游戏练习开发实践，所以我浏览了编码道场武士刀([http://codingdojo.org/kata/PokerHands/](http://codingdojo.org/kata/))的列表，并为这个练习选择了扑克手问题。\n\n# 扑克牌的问题\n\n这个问题的描述如下——给定两手或更多手扑克，我们需要比较它们，并返回排名较高的一手以及它获胜的原因。\n\n每手牌有五张牌，这些牌是从一副普通的 52 张牌中挑选出来的。这副牌由四套花色组成——梅花、钻石、红心和黑桃。每一套以`2`开始，以一张王牌结束，表示如下——`2`、`3`、`4`、`5`、`6`、`7`、`8`、`9`、`T`、`J`、`Q`、`K`、`A` ( `T`表示 10)。\n\n扑克牌中的牌会形成队形。一手牌的价值由这些阵型决定，按以下降序排列:\n\n*   **同花顺**:同花色连续取值的五张牌。例如`2♠`、`3♠`、`4♠`、`5♠`、`6♠`。起始值越高，同花顺越有价值。\n*   **四张一类**:四张同值的牌。最高的一张是四张王牌— `A♣`、`A♠`、`A♦`和`A♥`。\n*   **满堂红**:三张同值的牌，另外两张同值(但不同)的牌。最高的一个如下——`A♣`、`A♠`、`A♦`、`K♥`、`K♠`。\n*   **同花**:同花色五张牌。例如— `2♠`、`3♠`、`5♠`、`6♠`、`9♠`。\n*   **直**:连续值五张牌。例如— `2♣`、`3♠`、`4♥`、`5♣`、`6♦`。\n*   **三张一类**:三张同值的卡。例如— `2♣`、`2♠`、`2♥`。\n*   **两对**:见对。例如— `2♣`、`2♠`、`3♥`、`3♣`。\n*   **配对**:两张相同价值的卡。例如— `2♣`和`2♠`。\n*   **高牌**:当没有其他阵型出现时，比较每手牌的最高牌，最高者获胜。如果最高的牌具有相同的值，则比较次高的牌，以此类推。\n\n# 要求\n\n我们的目标是实现一个程序，比较两个或更多的扑克手，并返回赢家和原因。例如，让我们使用以下输入:\n\n*   **玩家 1** : `*2♥ 4♦ 7♣ 9♠ K♦*`\n*   **玩家 2** : `*2♠ 4♥ 8♣ 9♠ A♥*`\n\n对于这个输入，我们应该得到以下输出:\n\n*   *玩家 2 以他们的高牌获胜——一张王牌*\n\n# 第一步——思考\n\n让我们更详细地看看这个问题。更准确地说，我们试图将问题分解成更小的部分，而不需要过多考虑实现。我发现查看输入和输出的可能例子是有用的，并且从一个简化的问题开始，它允许我实现一些尽可能快的工作，同时保留问题的本质。\n\n很明显，我们有大量的组合需要测试。那么，对于限制我们测试用例的问题，什么是有用的简化呢？\n\n一个显而易见的方法是从一只较短的手开始。我们可以从手中的一张牌开始，而不是五张牌。这将我们的规则限制在高牌。下一步是两张牌，介绍*对>高牌*，和*高牌对>低牌对*等等。\n\n另一种方法是从五张牌开始，但要限制规则。从一张高牌开始，然后实施一对，再实施两对，以此类推；或者，反过来，从同花顺一直往下到对子和高牌。\n\n关于 TDD 有趣的事情是，这些道路中的任何一条都将导致以相同方式工作的结果，尽管通常使用不同的代码结构。TDD 的功能之一是通过改变测试的顺序来帮助您访问同一问题的多个设计。\n\n不用说，这个问题我以前也做过，但一直都是从一张卡在手开始的。让我们找点乐子，尝试不同的方式，好吗？我选择跟五张牌，从同花开始。为了简单起见，我现在只支持两个玩家，因为我喜欢给他们命名，所以我将使用爱丽丝和鲍勃。\n\n# 例子\n\n对于这种情况，有哪些有趣的例子？让我们首先考虑可能的输出:\n\n*   爱丽丝以同花顺获胜。\n*   鲍勃以同花顺获胜。\n*   爱丽丝和鲍勃有同样好的同花顺。\n*   尚未决定(如尚未实施)。\n\n现在，让我们为这些输出写一些输入示例:\n\n```cpp\nCase 1: Alice wins\n\nInputs:\n Alice: 2♠, 3♠, 4♠, 5♠, 6♠\n Bob: 2♣, 4♦, 7♥, 9♠, A♥\n\nOutput:\n Alice wins with straight flush\n\nCase 2: Bob wins\n\nInputs:\n    Alice: 2♠, 3♠, 4♠, 5♠, 9♠\n    Bob: 2♣, 3♣, 4♣, 5♣, 6♣\n\nOutput:\n    Bob wins with straight flush\n\nCase 3: Alice wins with a higher straight flush\n\nInputs:\n    Alice: 3♠, 4♠, 5♠, 6♠, 7♠\n    Bob: 2♣, 3♣, 4♣, 5♣, 6♣\n\nOutput:\n    Alice wins with straight flush\n\nCase 4: Draw\n\nInputs:\n    Alice: 3♠, 4♠, 5♠, 6♠, 7♠\n    Bob: 3♣, 4♣, 5♣, 6♣, 7♣\n\nOutput:\n    Draw (equal straight flushes)\n\nCase 5: Undecided\n\nInputs:\n    Alice: 3♠, 3♣, 5♠, 6♠, 7♠\n    Bob: 3♣, 4♣, 6♣, 6♥, 7♣\n\nOutput:\n    Not implemented yet.\n\n```\n\n有了这些例子，我们就可以开始写我们的第一个测试了！\n\n# 第一次测试\n\n根据我们之前的分析，我们的第一个测试如下:\n\n```cpp\nCase 1: Alice wins\n\nInputs:\n Alice: 2♠, 3♠, 4♠, 5♠, 6♠\n Bob: 2♣, 4♦, 7♥, 9♠, A♥\n\nOutput:\n Alice wins with straight flush\n```\n\n我们来写吧！我们预计这次测试会失败，所以我们现在可以做任何我们想做的事情。我们需要用前面的卡片初始化两只手。现在，我们将使用`vector<string>`来表示每只手。然后，我们将调用一个函数(它还不存在)，我们想象它将在某个时候实现两只手之间的比较。最后，我们对照之前定义的预期输出消息检查结果:\n\n```cpp\nTEST_CASE(\"Alice wins with straight flush\"){\n    vector<string> aliceHand{\"2♠\", \"3♠\", \"4♠\", \"5♠\", \"6♠\"};\n    vector<string> bobHand{\"2♣\", \"4♦\", \"7♥\", \"9♠\", \"A♥\"};\n\n    auto result = comparePokerHands(aliceHand, bobHand);\n\n    CHECK_EQ(\"Alice wins with straight flush\", result);\n}\n```\n\n目前，这个测试没有编译，因为我们甚至还没有创建`comparePokerHands`函数。是时候向前看了。\n\n# 通过第一次测试\n\n我们先写函数。函数需要返回一些东西，所以我们现在只返回空字符串:\n\n```cpp\nauto comparePokerHands = [](const auto& aliceHand, const auto& bobHand){\n    return \"\";\n};\n```\n\n让测试通过的最简单的实现是什么？这就是 TDD 变得更奇怪的地方。通过测试的最简单实现是将预期结果作为硬编码值返回:\n\n```cpp\nauto comparePokerHands = [](const auto& aliceHand, const auto& bobHand){\n    return \"Alice wins with straight flush\";\n};\n```\n\n此时，我的编译器会抱怨，因为我打开了所有警告，并将所有警告报告为错误。编译器注意到我们没有使用这两个参数并发出抱怨。这是一个有效的投诉，但我计划很快开始使用这些论点。C++ 语言为我们提供了一个简单的解决方案——只需删除或注释掉参数名称，如以下代码所示:\n\n```cpp\nauto comparePokerHands = [](const auto& /*aliceHand*/, const auto&  \n    /*bobHand*/){\n        return \"Alice wins with straight flush\";\n};\n```\n\n我们进行测试，第一次测试通过了！酷，有东西起作用了！\n\n# 重构\n\n有什么需要重构的吗？我们有两个带注释的参数名，我通常会删除它们，因为带注释的代码很混乱。但是，我已经决定暂时保留它们，因为我知道我们很快就会用到它们。\n\n我们还有一个副本——相同的`Alice wins with straight flush`字符串出现在测试和实现中。将其作为常量或公共变量提取值得吗？如果这是我们实现的结束，那么当然。但我知道，字符串实际上是由多种东西构建的——获胜玩家的名字，以及基于哪手牌获胜的规则。我想保持这个原样一段时间。\n\n因此，没有什么需要重构的。所以，我们继续吧！\n\n# 再想想\n\n目前的执行情况令人失望。仅仅返回硬编码值并不能解决很多问题。还是真的？\n\n这是学习 TDD 时需要的心态转变。我知道是因为我经历过。我非常习惯于看最终结果，将这个解决方案与我试图实现的目标进行比较，感觉并不令人满意。然而，有一种不同的方式来看待它——我们有一些可行的东西，我们有最简单的可能实现。还有很长的路要走，但是我们已经可以向我们的利益相关者展示一些东西了。此外，正如我们将看到的，我们将始终建立在坚实的基础上，因为我们编写的代码已经过全面测试。这两件事是令人难以置信的解放；我只能希望你在试用 TDD 的时候也会有同样的感受。\n\n但是，我们下一步怎么办？我们有几个选择。\n\n首先，我们可以写另一个测试，爱丽丝以同花顺获胜。然而，这不会改变实现中的任何事情，测试将立即通过。虽然这似乎违背了 TDD 周期，但为我们的想法增加更多的测试并没有错。绝对是一个有效的选择。\n\n其次，我们可以进入下一个测试，在这个测试中，鲍勃以同花顺获胜。这肯定会改变一些事情。\n\n这两个选项都很好，你可以选择其中任何一个。但是既然我们想在实践中看到滴滴涕，让我们先写更多的测试。\n\n# 更多测试\n\n很容易把我们的测试变成滴滴涕，增加更多的病例。我们将改变爱丽丝手的值，同时保持鲍勃的手完好无损。结果如下:\n\n```cpp\nTEST_CASE(\"Alice wins with straight flush\"){\n    vector<string> aliceHand;\n    const vector<string> bobHand {\"2♣\", \"4♦\", \"7♥\", \"9♠\", \"A♥\"};\n\n    SUBCASE(\"2 based straight flush\"){\n        aliceHand = {\"2♠\", \"3♠\", \"4♠\", \"5♠\", \"6♠\"};\n    };\n    SUBCASE(\"3 based straight flush\"){\n        aliceHand = {\"3♠\", \"4♠\", \"5♠\", \"6♠\", \"7♠\"};\n    };\n    SUBCASE(\"4 based straight flush\"){\n        aliceHand = {\"4♠\", \"5♠\", \"6♠\", \"7♠\", \"8♠\"};\n    };\n    SUBCASE(\"10 based straight flush\"){\n        aliceHand = {\"T♠\", \"J♠\", \"Q♠\", \"K♠\", \"A♠\"};\n    };\n\n    CAPTURE(aliceHand);\n\n    auto result = comparePokerHands(aliceHand, bobHand);\n\n    CHECK_EQ(\"Alice wins with straight flush\", result);\n}\n```\n\n所有这些测试再次通过。是时候进行下一次测试了。\n\n# 次好的\n\n我们描述的第二个测试是当鲍勃同花顺获胜时:\n\n```cpp\nCase: Bob wins\n\nInputs:\n Alice: 2♠, 3♠, 4♠, 5♠, 9♠\n Bob: 2♣, 3♣, 4♣, 5♣, 6♣\n\nOutput:\n Bob wins with straight flush\n```\n\n我们来写吧！这一次，让我们从一开始就使用数据驱动格式:\n\n```cpp\nTEST_CASE(\"Bob wins with straight flush\"){\n    const vector<string> aliceHand{\"2♠\", \"3♠\", \"4♠\", \"5♠\", \"9♠\"};\n    vector<string> bobHand;\n\n    SUBCASE(\"2 based straight flush\"){\n        bobHand = {\"2♣\", \"3♣\", \"4♣\", \"5♣\", \"6♣\"};\n    };\n\n    CAPTURE(bobHand);\n\n    auto result = comparePokerHands(aliceHand, bobHand);\n\n    CHECK_EQ(\"Bob wins with straight flush\", result);\n}\n```\n\n当我们运行这个测试时，它失败了，原因很简单——我们有一个硬编码的实现，说爱丽丝赢了。现在怎么办？\n\n# 通过测试\n\n再一次，我们需要找到最简单的方法让这个测试通过。即使我们不喜欢这个实现，下一步也是清理混乱。那么，最简单的实现是什么？\n\n我们显然需要在实现中引入一个条件语句。问题是，我们应该检查什么？\n\n再说一次，我们有几个选择。一种选择是再次假装，使用一些简单的东西，比如将鲍勃的手与我们期望获胜的确切手进行比较:\n\n```cpp\nauto comparePokerHands = [](const vector<string>& /*aliceHand*/, const vector<string>& bobHand){\n    const vector<string> winningBobHand {\"2♣\", \"3♣\", \"4♣\", \"5♣\", \"6♣\"};\n    if(bobHand == winningBobHand){\n        return \"Bob wins with straight flush\";\n    }\n    return \"Alice wins with straight flush\";\n};\n```\n\n为了让它编译，我们还必须让`vector<string>`指针的类型到处出现。一旦做出这些改变，测试就通过了。\n\n我们的第二个选择是开始对同花顺进行实际检查。然而，这本身是一个小问题，要正确地做到这一点需要更多的测试。\n\n现在我将选择第一个选项，重构，然后开始更深入地研究检查的实现。\n\n# 重构\n\n有什么需要重构的吗？我们仍然有重复的字符串。此外，我们在包含鲍勃手的向量中添加了一个副本。但我们预计两者都将很快消失。\n\n然而，另一件事却困扰着我——`vector<string>`无处不在。让我们通过为其命名`vector<string>`类型来消除这种重复— `Hand`:\n\n```cpp\nusing Hand = vector<string>;\n\nauto comparePokerHands = [](const Hand& /*aliceHand*/, const Hand& bobHand){\n    Hand winningBobHand {\"2♣\", \"3♣\", \"4♣\", \"5♣\", \"6♣\"};\n    if(bobHand == winningBobHand){\n        return \"Bob wins with straight flush\";\n    }\n    return \"Alice wins with straight flush\";\n};\n\nTEST_CASE(\"Bob wins with straight flush\"){\n    Hand aliceHand{\"2♠\", \"3♠\", \"4♠\", \"5♠\", \"9♠\"};\n    Hand bobHand;\n\n    SUBCASE(\"2 based straight flush\"){\n        bobHand = {\"2♣\", \"3♣\", \"4♣\", \"5♣\", \"6♣\"};\n    };\n\n    CAPTURE(bobHand);\n\n    auto result = comparePokerHands(aliceHand, bobHand);\n\n    CHECK_EQ(\"Bob wins with straight flush\", result);\n}\n```\n\n# 想\n\n是时候再想想了。我们有两个用硬编码值实现的案例。对于爱丽丝同花顺获胜来说，这不是一个大问题，但是如果我们用不同的一组牌为鲍勃添加另一个测试用例，这就是一个问题。我们可以继续进行一些测试，但不可避免的是，我们需要实际检查同花顺。我认为现在是最好的时机。\n\n那么，什么是同花顺？这是一套五张牌，有相同的花色和连续的数值。我们需要一个函数，可以取一套五张牌，如果是同花顺就返回`true`，如果不是就返回`false`。让我们写下几个例子:\n\n*   输入:`2♣ 3♣ 4♣ 5♣ 6♣` = >输出:`true`\n*   输入:`2♠ 3♠ 4♠ 5♠ 6♠` = >输出:`true`\n*   输入:`T♠ J♠ Q♠ K♠ A♠` = >输出:`true`\n*   输入:`2♣ 3♣ 4♣ 5♣ 7♣` = >输出:`false`\n*   输入:`2♣ 3♣ 4♣ 5♣ 6♠` = >输出:`false`\n*   输入:`2♣ 3♣ 4♣ 5♣` = >输出:`false`(只有四张牌，正好需要五张)\n*   输入:`[empty vector]` = >输出:`false`(无牌，正好需要五张)\n*   输入:`2♣ 3♣ 4♣ 5♣ 6♣ 7♣` = >输出:`false`(六张牌，正好需要五张)\n\n你会注意到我们也考虑了边缘案例和奇怪的情况。我们有足够的信息继续，所以让我们写下一个测试。\n\n# 下一个测试–简单的同花\n\n我更喜欢从正面的案例开始，因为它们更倾向于推进实施。让我们看看最简单的一个:\n\n*   输入:`2♣ 3♣ 4♣ 5♣ 6♣` = >输出:`true`\n\n测试如下所示:\n\n```cpp\nTEST_CASE(\"Hand is straight flush\"){\n    Hand hand;\n\n    SUBCASE(\"2 based straight flush\"){\n        hand = {\"2♣\", \"3♣\", \"4♣\", \"5♣\", \"6♣\"};\n    };\n\n    CAPTURE(hand);\n\n    CHECK(isStraightFlush(hand));\n}\n```\n\n同样，测试没有编译，因为我们没有实现`isStraightFlush`函数。但是测试是对的，它失败了，所以是时候继续前进了。\n\n# 通过测试\n\n同样，第一步是编写函数体并返回预期的硬编码值:\n\n```cpp\nauto isStraightFlush = [](const Hand&){\n    return true;\n};\n```\n\n我们做了测试，他们通过了，所以我们现在结束了！\n\n# 走向\n\n你可以看到事情的发展。我们可以为正确的同花顺添加更多的输入，但是它们不会改变实现。迫使我们推进实现的第一个测试是我们第一个不是同花同花的一组牌的例子。\n\n对于本章的目标，我将快进。但是，我强烈建议你自己走完所有的小步骤，把你的结果和我的进行比较。学习 TDD 的唯一方法就是自己练习，反思自己的方法。\n\n# 实现 isStraightFlush\n\n让我们再来看看我们试图实现的目标——同花顺，这是通过拥有五张完全相同花色和连续值的牌来定义的。我们只需要用代码表达这三个条件:\n\n```cpp\nauto isStraightFlush = [](const Hand& hand){\n    return has5Cards(hand) && \n        isSameSuit(allSuits(hand)) && \n        areValuesConsecutive(allValuesInOrder(hand));\n};\n```\n\n许多不同的 lambdas 有助于实现。首先，为了检查地层的长度，我们使用`has5Cards`:\n\n```cpp\nauto has5Cards = [](const Hand& hand){\n    return hand.size() == 5;\n};\n```\n\n然后，为了检查它是否有相同的套装，我们使用`allSuits`从手上提取套装，`isSuitEqual`比较两个套装，`isSameSuit`检查一只手的所有套装是否相同:\n\n```cpp\nusing Card = string;\nauto suitOf = [](const Card& card){\n    return card.substr(1);\n};\n\nauto allSuits = [](Hand hand){\n    return transformAll<vector<string>>(hand, suitOf);\n};\n\nauto isSameSuit = [](const vector<string>& allSuits){\n    return std::equal(allSuits.begin() + 1, allSuits.end(),  \n        allSuits.begin());\n};\n```\n\n最后，为了验证数值是否连续，我们使用`valueOf`从牌中提取数值，`allValuesInOrder`从一手牌中获取所有数值并对其进行排序，`toRange`从初始值开始创建一系列连续的数值，`areValuesConsecutive`检查一手牌中的数值是否连续:\n\n```cpp\nauto valueOf = [](const Card& card){\n    return charsToCardValues.at(card.front());\n};\n\nauto allValuesInOrder = [](const Hand& hand){\n    auto theValues = transformAll<vector<int>>(hand, valueOf);\n    sort(theValues.begin(), theValues.end());\n    return theValues;\n};\n\nauto toRange = [](const auto& collection, const int startValue){\n    vector<int> range(collection.size());\n    iota(begin(range), end(range), startValue);\n    return range;\n};\n\nauto areValuesConsecutive = [](const vector<int>& allValuesInOrder){\n    vector<int> consecutiveValues = toRange(allValuesInOrder, \n        allValuesInOrder.front());\n\n    return consecutiveValues == allValuesInOrder;\n};\n```\n\n拼图的最后一块是从`char`到`int`的地图，帮助我们将所有的卡值，包括`T`、`J`、`Q`、`K`和`A`转换成数字:\n\n```cpp\nconst std::map<char, int> charsToCardValues = {\n    {'1', 1},\n    {'2', 2},\n    {'3', 3},\n    {'4', 4},\n    {'5', 5},\n    {'6', 6},\n    {'7', 7},\n    {'8', 8},\n    {'9', 9},\n    {'T', 10},\n    {'J', 11},\n    {'Q', 12},\n    {'K', 13},\n    {'A', 14},\n};\n```\n\n让我们也看看我们的测试(显然都通过了)。首先，有效的同花顺；我们将从`2`、`3`、`4`和`10`开始检查直冲，以及它们如何沿着数据间隔变化:\n\n```cpp\nTEST_CASE(\"Hand is straight flush\"){\n    Hand hand;\n\n    SUBCASE(\"2 based straight flush\"){\n        hand = {\"2♣\", \"3♣\", \"4♣\", \"5♣\", \"6♣\"};\n    };\n\n    SUBCASE(\"3 based straight flush\"){\n        hand = {\"3♣\", \"4♣\", \"5♣\", \"6♣\", \"7♣\"};\n    };\n\n    SUBCASE(\"4 based straight flush\"){\n        hand = {\"4♣\", \"5♣\", \"6♣\", \"7♣\", \"8♣\"};\n    };\n\n    SUBCASE(\"4 based straight flush on hearts\"){\n        hand = {\"4♥\", \"5♥\", \"6♥\", \"7♥\", \"8♥\"};\n    };\n\n    SUBCASE(\"10 based straight flush on hearts\"){\n        hand = {\"T♥\", \"J♥\", \"Q♥\", \"K♥\", \"A♥\"};\n    };\n\n    CAPTURE(hand);\n\n    CHECK(isStraightFlush(hand));\n}\n```\n\n最后，测试一组不是有效同花顺的牌。对于输入，我们将使用几乎同花的手牌，除了来自另一套花色、没有足够的牌或牌太多:\n\n```cpp\nTEST_CASE(\"Hand is not straight flush\"){\n    Hand hand;\n\n    SUBCASE(\"Would be straight flush except for one card from another \n        suit\"){\n            hand = {\"2♣\", \"3♣\", \"4♣\", \"5♣\", \"6♠\"};\n    };\n\n    SUBCASE(\"Would be straight flush except not enough cards\"){\n        hand = {\"2♣\", \"3♣\", \"4♣\", \"5♣\"};\n    };\n\n    SUBCASE(\"Would be straight flush except too many cards\"){\n        hand = {\"2♣\", \"3♣\", \"4♣\", \"5♣\", \"6♠\", \"7♠\"};\n    };\n\n    SUBCASE(\"Empty hand\"){\n        hand = {};\n    };\n\n    CAPTURE(hand);\n\n    CHECK(!isStraightFlush(hand));\n}\n```\n\n现在是时候回到我们的主要问题了——比较扑克牌。\n\n# 将止回阀直接插回比较器中\n\n尽管到目前为止我们已经实现了一切，但是我们对`comparePokerHands`的实现仍然是硬编码的。让我们记住它的当前状态:\n\n```cpp\nauto comparePokerHands = [](const Hand& /*aliceHand*/, const Hand& bobHand){\n    const Hand winningBobHand {\"2♣\", \"3♣\", \"4♣\", \"5♣\", \"6♣\"};\n    if(bobHand == winningBobHand){\n        return \"Bob wins with straight flush\";\n    }\n    return \"Alice wins with straight flush\";\n};\n```\n\n但是，我们现在有了一种检查同花顺的方法！所以，让我们插入我们的实现:\n\n```cpp\nauto comparePokerHands = [](Hand /*aliceHand*/, Hand bobHand){\n    if(isStraightFlush(bobHand)) {\n        return \"Bob wins with straight flush\";\n    }\n    return \"Alice wins with straight flush\";\n};\n```\n\n我们所有的测试都通过了，所以我们快完成了。是时候在我们的`Bob wins with straight flush`案例中增加一些测试，以确保我们没有遗漏任何东西。我们将为爱丽丝保留相同的手牌，几乎是同花，并将鲍勃的手牌从基于`2`、`3`和`10`的同花中改变:\n\n```cpp\nTEST_CASE(\"Bob wins with straight flush\"){\n    Hand aliceHand{\"2♠\", \"3♠\", \"4♠\", \"5♠\", \"9♠\"};\n    Hand bobHand;\n\n    SUBCASE(\"2 based straight flush\"){\n        bobHand = {\"2♣\", \"3♣\", \"4♣\", \"5♣\", \"6♣\"};\n    };\n\n    SUBCASE(\"3 based straight flush\"){\n        bobHand = {\"3♣\", \"4♣\", \"5♣\", \"6♣\", \"7♣\"};\n    };\n\n    SUBCASE(\"10 based straight flush\"){\n        bobHand = {\"T♣\", \"J♣\", \"Q♣\", \"K♣\", \"A♣\"};\n    };\n\n    CAPTURE(bobHand);\n\n    auto result = comparePokerHands(aliceHand, bobHand);\n\n    CHECK_EQ(\"Bob wins with straight flush\", result);\n}\n```\n\n之前的测试都通过了。所以，我们已经完成了两个案例——当爱丽丝或鲍勃同花顺，而他们的竞争对手没有同花顺。是时候进入下一个案子了。\n\n# 比较两次连续冲洗\n\n正如我们在本节开头所讨论的，还有另一种情况，爱丽丝和鲍勃都有同花顺，但爱丽丝赢了一个更高的同花顺:\n\n```cpp\nCase: Alice wins with a higher straight flush\n\nInputs:\n Alice: 3♠, 4♠, 5♠, 6♠, 7♠\n Bob: 2♣, 3♣, 4♣, 5♣, 6♣\n\nOutput:\n Alice wins with straight flush\n```\n\n让我们编写测试并运行它:\n\n```cpp\nTEST_CASE(\"Alice and Bob have straight flushes but Alice wins with higher straight flush\"){\n    Hand aliceHand;\n    Hand bobHand{\"2♣\", \"3♣\", \"4♣\", \"5♣\", \"6♣\"};\n\n    SUBCASE(\"3 based straight flush\"){\n        aliceHand = {\"3♠\", \"4♠\", \"5♠\", \"6♠\", \"7♠\"};\n    };\n\n    CAPTURE(aliceHand);\n\n    auto result = comparePokerHands(aliceHand, bobHand);\n\n    CHECK_EQ(\"Alice wins with straight flush\", result);\n}\n```\n\n测试失败，因为我们的`comparePokerHands`函数返回鲍勃赢了，而不是爱丽丝。让我们用最简单的实现来解决这个问题:\n\n```cpp\nauto comparePokerHands = [](const Hand& aliceHand, const Hand& bobHand){\n    if(isStraightFlush(bobHand) && isStraightFlush(aliceHand)){\n         return \"Alice wins with straight flush\";\n    }\n\n    if(isStraightFlush(bobHand)) {\n        return \"Bob wins with straight flush\";\n    }\n\n    return \"Alice wins with straight flush\";\n};\n```\n\n我们的实现决定了如果爱丽丝和鲍勃都同花顺，爱丽丝总是赢。这显然不是我们想要的，但测试通过了。那么，我们可以写什么测试来推动实现呢？\n\n# 想\n\n原来我们之前的分析漏掉了一个案例。我们观察了当爱丽丝和鲍勃都同花顺并且爱丽丝获胜时会发生什么；但是当鲍勃有更高的同花顺时呢？让我们写下一个例子:\n\n```cpp\nCase: Bob wins with a higher straight flush\n\nInputs:\n Alice: 3♠, 4♠, 5♠, 6♠, 7♠\n Bob: 4♣, 5♣, 6♣, 7♣, 8♣\n\nOutput:\n Bob wins with straight flush\n```\n\n是时候写另一个失败的测试了。\n\n# 比较两次直冲(续)\n\n到目前为止，这个测试很容易写出来:\n\n```cpp\nTEST_CASE(\"Alice and Bob have straight flushes but Bob wins with higher \n    straight flush\"){\n        Hand aliceHand = {\"3♠\", \"4♠\", \"5♠\", \"6♠\", \"7♠\"};\n        Hand bobHand;\n\n        SUBCASE(\"3 based straight flush\"){\n            bobHand = {\"4♣\", \"5♣\", \"6♣\", \"7♣\", \"8♣\"};\n    };\n\n    CAPTURE(bobHand);\n\n    auto result = comparePokerHands(aliceHand, bobHand);\n\n    CHECK_EQ(\"Bob wins with straight flush\", result);\n}\n```\n\n测试又失败了，因为我们的实现假设爱丽丝和鲍勃同花顺时爱丽丝总是赢。可能是时候检查一下哪一个是最高的同花顺了。\n\n为此，我们需要再次写下几个案例，并完成我们的 TDD 周期。我将再次快进到实施阶段。我们最终得到了下面的助手函数，它比较了两个直接的同花顺。如果第一手牌有较高的同花，则返回`1`，如果两者相等，则返回`0`，如果第二手牌有较高的同花，则返回`-1`:\n\n```cpp\nauto compareStraightFlushes = [](const Hand& first, const Hand& second){\n    int firstHandValue = allValuesInOrder(first).front();\n    int secondHandValue = allValuesInOrder(second).front();\n    if(firstHandValue > secondHandValue) return 1;\n    if(secondHandValue > firstHandValue) return -1;\n    return 0;\n};\n```\n\n通过改变我们的实现，我们可以通过测试:\n\n```cpp\nauto comparePokerHands = [](const Hand& aliceHand, const Hand& bobHand){\n    if(isStraightFlush(bobHand) && isStraightFlush(aliceHand)){\n        int whichIsHigher = compareStraightFlushes(aliceHand, bobHand);\n        if(whichIsHigher == 1) return \"Alice wins with straight flush\";\n        if(whichIsHigher == -1) return \"Bob wins with straight flush\";\n    }\n\n    if(isStraightFlush(bobHand)) {\n        return \"Bob wins with straight flush\";\n    }\n\n    return \"Alice wins with straight flush\";\n};\n```\n\n这给我们留下了最后一个案例——平局。考验再次变得十分明显:\n\n```cpp\nTEST_CASE(\"Draw due to equal straight flushes\"){\n    Hand aliceHand;\n    Hand bobHand;\n\n    SUBCASE(\"3 based straight flush\"){\n        aliceHand = {\"3♠\", \"4♠\", \"5♠\", \"6♠\", \"7♠\"};\n    };\n\n    CAPTURE(aliceHand);\n    bobHand = aliceHand;\n\n    auto result = comparePokerHands(aliceHand, bobHand);\n\n    CHECK_EQ(\"Draw\", result);\n}\n```\n\n实现上的变化相当简单:\n\n```cpp\nauto comparePokerHands = [](Hand aliceHand, Hand bobHand){\n    if(isStraightFlush(bobHand) && isStraightFlush(aliceHand)){\n        int whichIsHigher = compareStraightFlushes(aliceHand, bobHand);\n        if(whichIsHigher == 1) return \"Alice wins with straight flush\";\n        if(whichIsHigher == -1) return \"Bob wins with straight flush\";\n        return \"Draw\";\n    }\n\n    if(isStraightFlush(bobHand)) {\n        return \"Bob wins with straight flush\";\n    }\n\n    return \"Alice wins with straight flush\";\n};\n```\n\n这不是最漂亮的函数，但它通过了我们所有的同花顺比较测试。我们肯定可以将其重构为更小的函数，但是我将在这里停止，因为我们已经达到了我们的目标——我们已经使用 TDD 和 DDT 设计了不仅一个，而且多个纯函数。\n\n# 摘要\n\n在本章中，您已经学习了如何编写单元测试，如何编写数据驱动测试，以及如何使用数据驱动测试结合 TDD 来设计纯函数。\n\nTDD 是有效软件开发的核心实践之一。虽然它有时看起来奇怪和违反直觉，但它有一个强大的优势——每隔几分钟，你就有一些可以演示的工作。通过测试不仅是一个演示点，也是一个保存点。如果在尝试重构或实现下面的测试时发生了任何错误，您总是可以回到最后一个保存点。我发现这种做法在 C++ 中更有价值，因为在 c++ 中，很多事情都可能出错。事实上，从[第 3 章](03.html)、*深入 Lambdas* 开始，我就用 TDD 方法编写了所有的代码。这非常有帮助，因为我知道我的代码正在工作——如果没有这种方法，写一本技术书籍是很难做到的。我强烈建议你多看看 TDD，自己去实践；这是你变得精通的唯一方法。\n\n带有函数式编程的 TDD 是完美的搭配。当它与命令式面向对象代码一起使用时，我们经常需要考虑突变，这使得事情变得更加困难。有了纯函数和数据驱动的测试，添加更多测试的实践变得尽可能简单，并允许我们专注于实现。在功能操作的支持下，在许多情况下，通过测试变得更加容易。我个人觉得这种组合非常值得；我希望你会发现它同样有用。\n\n现在是时候向前迈进，重新审视软件设计的另一部分——设计模式了。它们会随着函数式编程而改变吗？(剧透警报——它们实际上变得简单多了。)这是我们下一章要讨论的。"
  },
  {
    "path": "docs/handson-func-prog-cpp/10.md",
    "content": "# 十、性能优化\n\n性能是选择 C++ 作为项目编程语言的关键驱动因素之一。现在是时候讨论当我们以功能性风格构建代码时，如何提高性能了。\n\n虽然性能是一个巨大的话题，我们显然不能在一章中完全涵盖，但我们将研究提高性能的关键思想，纯函数式语言如何优化性能，以及如何将这些优化转化为 C++。\n\n本章将涵盖以下主题:\n\n*   交付绩效的流程\n*   如何使用并行/异步来提高性能\n*   了解什么是尾部递归以及如何激活它\n*   使用函数构造时如何提高内存消耗\n*   功能异步代码\n\n# 技术要求\n\n您将需要一个支持 C++ 17 的编译器。我用的是 GCC 7.3.0。\n\n代码可以在[的 GitHub 上找到。com/ PacktPublishing/动手-函数-用- Cpp 编程`Chapter10`文件夹中的](https://github.%E2%80%8Bcom/PacktPublishing/Hands-On-Functional-Programming-with-Cpp)。它包含并使用了`doctest`，这是一个单头开源单元测试库。你可以在它的 GitHub 资源库[上找到它。com/ onqtam/ doctest](https://github.%E2%80%8Bcom/onqtam/doctest) 。\n\n# 性能优化\n\n谈论性能优化就像谈论披萨。有些人喜欢并搜索菠萝比萨。其他人只吃传统的意大利比萨饼(或来自特定地区)。有的只吃素食披萨，有的喜欢各种披萨。关键是，性能优化与您的代码库和产品息息相关。你在看什么样的表演？对用户来说，性能中最有价值的部分是什么？你需要考虑哪些限制因素？\n\n与我一起工作的客户通常有一些性能要求，具体取决于主题:\n\n*   *嵌入式产品*(例如汽车、能源或电信)往往需要在内存限制内工作。堆栈和堆通常很小，因此限制了长期变量的数量。增加内存的成本可能高得令人望而却步(一位客户告诉我们，他们需要 1000 多万欧元才能在所有设备上增加 1 MB 的内存)。因此，程序员需要通过尽可能避免不必要的内存分配来克服这些限制。这可以包括初始化、通过复制传递参数(尤其是较大的结构)、避免需要消耗内存的特定算法等等。\n*   *工程应用*(例如计算机辅助设计或 CAD)需要在非常大的数据集上使用从数学、物理和工程导出的特定算法，并尽快返回结果。处理通常是在现代个人电脑上完成的，所以内存不成问题；然而，中央处理器是。随着多核处理器、可以接管部分处理的专用 GPU 以及允许在多个强大或专用服务器之间分配工作负载的云技术的出现，开发人员的工作通常会变成在并行和异步环境中优化速度。\n*   *桌面游戏和游戏引擎*都有自己特别关注的地方。图形必须看起来尽可能好，以便在中低端机器上优雅地缩小比例，并避免滞后。游戏通常会接管它们运行的机器，所以它们只需要与操作系统和系统应用(如反病毒软件或防火墙)争夺资源。它们还可以采用特定级别的图形处理器、中央处理器和内存。优化变成了关于并行性(因为需要多个内核)和避免浪费，以便在整个游戏中保持流畅的体验。\n*   *游戏服务器*然而，是一个不同的野兽。像暴雪的 Battle.net 这样的服务(我经常作为一名*星际争霸 2*玩家使用的服务)需要快速响应，即使是在压力下。在云计算时代，使用的服务器数量和它们的能力并不重要；我们可以轻松地放大或缩小它们。主要关注的是尽可能快地响应主要是输入/输出的工作负载。\n*   *未来令人振奋*。游戏的趋势是将处理转移到服务器，从而允许玩家甚至在低端机器上玩。这将为未来的游戏打开惊人的机会。(用 10 个 GPU 代替一个能做什么？100 块怎么样？)但也会导致需要优化游戏引擎进行服务器端、多机、并行处理。为了远离游戏，物联网行业为嵌入式软件和可扩展的服务器端处理开辟了更多机会。\n\n考虑到所有这些可能性，我们能做些什么来在代码库中提供性能呢？\n\n# 交付绩效的流程\n\n如您所见，性能优化在很大程度上取决于您试图实现的目标。接下来的步骤可以很快总结如下:\n\n1.  定义一个明确的绩效目标，包括衡量标准以及如何衡量它们。\n2.  为性能定义一些编码准则。保持它们清晰，并针对代码的特定部分进行定制。\n3.  让代码发挥作用。\n4.  在需要的地方衡量和提高绩效。\n5.  监控和改进。\n\n在我们更详细地研究这些步骤之前，理解性能优化的一个重要注意事项是很重要的——优化有两种类型。第一个来自干净的设计和干净的代码。例如，通过从代码中移除某些类型的相似性，您可能最终会减小可执行文件的大小，从而为数据留出更多空间；数据在代码中的传播可能会减少，从而避免不必要的复制或间接访问；或者，它将允许编译器更好地理解代码，并为您优化代码。从我的经验来看，将代码重构为简单的设计通常也能提高性能。\n\n第二种提高性能的方法是使用点优化。这些都是非常具体的方法，我们可以重写一个函数或流程，让代码更快或更少地工作，通常是针对特定的编译器和平台。生成的代码通常看起来很聪明，但很难理解和更改。\n\n点优化与编写易于更改和维护的代码有着天然的冲突。这导致唐纳德·克努特(Donald Knuth)说*过早优化是所有邪恶的根源*。这并不意味着我们应该编写明显缓慢的代码，例如通过复制传递大型集合。然而，这确实意味着我们应该首先针对可变性优化设计，然后测量性能，然后优化它，并且只有在绝对必要的情况下才使用点优化。平台中的怪癖、特定的编译器版本或使用的库可能需要不时进行点优化；把它们分开，少用。\n\n现在让我们来看看优化性能的过程。\n\n# 为绩效定义一个明确的目标，包括指标以及如何衡量它们\n\n如果我们不知道我们要去哪里，我们去哪个方向并不重要——我是在转述《爱丽丝梦游仙境》。因此，我们应该知道我们要去哪里。我们需要一份符合产品需求的性能指标清单。此外，对于每个性能指标，我们需要一个范围来定义什么是指标的*良好*值，什么是*可接受的*值。我们来看几个例子。\n\n如果您正在为一个具有 4 MB 内存的设备构建一个*嵌入式产品*，您可以查看以下指标:\n\n*   内存消耗:\n    *   很好:1-3 MB\n    *   好:3-4 MB\n*   设备启动时间:\n    *   很好:< 1s\n    *   好:1-3 秒\n\n如果你正在构建一个*桌面计算机辅助设计应用*，通过一个建筑设计来模拟声波，其他的度量标准也很有趣。\n\n模拟声波的计算时间:\n\n*   对于小房间:\n    *   很好:< 1 分钟\n    *   良好:< 5 分钟\n*   对于中型房间:\n    *   很好:< 2 分钟\n    *   好:< 10 分钟\n\n这里的数字只是说明性的；您需要为您的产品找到自己的指标。\n\n有了这些指标和良好/良好的范围，我们就可以在添加新功能后衡量性能，并进行相应的优化。它还允许我们简单地向利益相关者或业务人员解释产品的性能。\n\n# 为性能定义一些编码准则——保持它们清晰，并针对代码的特定部分进行定制\n\n如果你问 50 个不同的 C++ 程序员优化性能的技巧，你很快就会被建议淹没。如果你开始调查这个建议，会发现有些是过时的，有些是非常具体的，有些是很棒的。\n\n因此，为性能制定编码准则是很重要的，但是有一个警告。C++ 代码库往往非常庞大，因为它们已经开发了很多年。如果你批判性地审视你的代码库，你会发现只有部分代码是性能的瓶颈。举个例子，只有当一个数学运算被多次调用时，计算速度快 1 毫秒才有意义；如果只调用一两次，或者很少调用，就没必要优化了。事实上，下一个版本的编译器或中央处理器在优化方面可能会比你做得更好。\n\n由于这个事实，您应该理解代码的哪些部分对于您定义的性能标准是至关重要的。找出什么样的设计最适合那段特定的代码；有明确的指导方针，并遵循它们。虽然`const&`在任何地方都很有用，但也许你可以避免浪费开发人员的时间来整理一个只做了一次的非常小的集合。\n\n# 让代码工作\n\n考虑到这些指导原则，并考虑到要实现的新特性，第一步应该始终是让代码工作。此外，对其进行结构化，以便在您的限制范围内进行更改。不要试图在这里优化性能；同样，编译器和 CPU 可能比你想象的更聪明，做的工作也比你预期的多。了解情况是否如此的唯一方法是衡量绩效。\n\n# 根据需要衡量和改进绩效\n\n您的代码按照您的指导方针工作和结构化，并针对变化进行了优化。是时候写下一些关于优化它的假设，然后测试它们了。\n\n因为您有明确的性能指标，所以验证它们相对容易。当然，这需要正确的基础设施和适当的测量过程。有了这些，您可以根据您的绩效指标来衡量自己的地位。\n\n这里应该欢迎更多的假设。类似于——*如果我们像这样重组这段代码，我预计指标 X* 会有所改进。然后，您可以继续并测试您的假设——创建一个分支，更改代码，构建产品，通过性能指标测量过程，并查看结果。当然，它比我说的要复杂——有时它可能需要用不同的编译器、不同的优化选项或统计数据来构建。如果你想做出明智的决定，所有这些都是必要的。最好在度量上投入一些时间，而不是改变代码并使其更难理解。否则，你最终会欠下一笔技术债务，并为此支付长期利息。\n\n然而，如果你必须做点优化，没有解决办法。只要确保尽可能详细地记录它们。既然你之前已经验证了你的假设，你会有很多东西要写，不是吗？\n\n# 监控和改进\n\n我们从定义性能指标开始循环。是时候结束它了——我们需要监控这些指标(可能还有其他指标)，并根据我们学到的知识调整我们的间隔和编码准则。性能优化是一个持续的过程，因为目标设备也在不断发展。\n\n我们已经研究了交付性能的过程，但是这与函数式编程有什么关系呢？哪些用例让功能代码结构闪闪发光，哪些没有那么好用？是时候深入研究我们的代码结构了。\n\n# 并行性——利用不变性\n\n编写并行运行的代码一直是软件开发中许多痛苦的根源。多线程、多进程或多服务器环境产生的问题似乎从根本上难以解决。死锁、饥饿、数据竞争、锁或调试多线程代码只是让我们这些见过它们的人害怕再次遇到它们的几个术语。然而，由于多核 CPU、GPU 和多服务器，我们不得不面对并行代码。函数式编程对此有帮助吗？\n\n每个人都同意这是函数式编程的优点之一，特别是源自不变性。如果您的数据从未改变，那么就没有锁，并且同步非常简单，可以一般化。如果你只使用纯函数和函数转换(当然除了输入输出)，你就可以免费获得并行化(几乎)。\n\n事实上，C++ 17 标准包含了 STL 高级函数的执行策略，允许我们仅用一个参数就可以将算法从顺序改为并行。让我们并行检查一个向量的所有数字是否都大于`5`。我们只需要将`execution::par`作为`all_of`的执行政策:\n\n```cpp\nauto aVector = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};\nauto all_of_parallel = [&aVector](){\n    return all_of(execution::par, aVector.begin(), aVector.end(),  \n        [](auto value){return value > 5;});\n};\n```\n\n然后，我们可以从`chrono`命名空间测量使用高分辨率计时器的算法的顺序和并行版本之间的差异，如下所示:\n\n```cpp\nauto measureExecutionTimeForF = [](auto f){\n    auto t1 = high_resolution_clock::now();\n    f();\n    auto t2 = high_resolution_clock::now();\n    chrono::nanoseconds duration = t2 - t1;\n    return duration;\n};\n```\n\n通常情况下，我现在会根据我的实验向您展示执行中的差异。不幸的是，在这种情况下，我不能这样做。在撰写本文时，实现执行策略的编译器只有 MSVC 和英特尔 C++，但两者都不符合标准。但是，如下面的代码片段所示，我在`parallelExecution.cpp`源文件中编写了代码，允许您在编译器支持该标准时通过取消注释一行来启用它，如下所示:\n\n```cpp\n// At the time when I created this file, only MSVC had implementation  \n    for execution policies.\n// Since you're seeing this in the future, you can enable the parallel \n    execution code by uncommenting the following line \n//#define PARALLEL_ENABLED\n```\n\n执行此操作时，您将运行的代码将显示顺序和并行运行`all_of`的相对持续时间，如下所示:\n\n```cpp\nTEST_CASE(\"all_of with sequential execution policy\"){\n    auto aVector = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};\n\n    auto all_of_sequential = [&aVector](){\n        return all_of(execution::seq, aVector.begin(), aVector.end(), \n            [](auto value){return value > 5;});\n    };\n\n    auto sequentialDuration = \n        measureExecutionTimeForF(all_of_sequential);\n        cout << \"Execution time for sequential policy:\" << \n            sequentialDuration.count() << \" ns\" << endl;\n\n    auto all_of_parallel = [&aVector](){\n        return all_of(execution::par, aVector.begin(), aVector.end(), \n            [](auto value){return value > 5;});\n    };\n\n    auto parallelDuration = measureExecutionTimeForF(all_of_parallel);\n    cout << \"Execution time for parallel policy:\" <<   \n        parallelDuration.count() << \" ns\" << endl;\n}\n```\n\n虽然我很想在这里分析一些执行数据，但这可能是最好的，因为本章最重要的信息是测量，测量，测量，然后优化。希望到时候你会做一些自我测量。\n\nC++ 17 标准支持很多 STL 函数的执行策略，包括`sort`、`find`、`copy`、`transform`和`reduce`。也就是说，如果你正在链接这些函数并使用纯函数，你只需要向所有调用(或`bind`更高级的函数)传递一个额外的参数来实现并行执行！我甚至可以说，对于任何尝试过自己管理线程或调试奇怪的同步问题的人来说，这就像变魔术一样。事实上，只要编译器支持完整的 C++ 17 标准，我们在前面几章中为井字游戏和扑克之手编写的所有代码都可以轻松切换到并行执行。\n\n但是这是如何工作的呢？`all_of`在多线程中运行相当容易；它们中的每一个都对集合中的特定元素执行谓词，返回一个布尔值，当第一个谓词返回`False`时，该过程停止。只有当谓词是纯函数时，这才是可能的；以任何方式修改结果或向量都会造成竞争条件。文档特别声明程序员负责保持谓词函数的纯粹性——不会有警告或编译错误。除了纯粹之外，您的谓词不能假定元素的处理顺序。\n\n如果并行执行策略无法启动(例如，由于缺乏资源)，执行将退回到顺序调用。在衡量性能时，这是一件需要记住的有用的事情:如果它比预期的低得多，首先检查程序是否可以并行执行。\n\n该选项对于使用多个 CPU 的计算密集型应用非常有用。如果你对它的内存命中感兴趣，你就必须测量它，因为它取决于你使用的编译器和标准库。\n\n# 记忆化\n\n纯函数有一个有趣的性质。对于相同的输入值，它们返回相同的输出。这使得它们相当于一个大的值表，其输出值对应于输入参数的每个值组合。有时候，记住这个表的某些部分比做计算更快。这种技术被称为**记忆**。\n\n纯函数式编程语言，以及 Python 和 Groovy 等语言，都有办法在特定的函数调用上实现记忆化，从而提供高级别的控制。不幸的是，C++ 没有这个功能，所以我们必须自己编写。\n\n# 实现记忆\n\n为了开始我们的实现，我们需要一个函数；理想情况下，计算量很大。让我们选择`power`功能。一个简单的实现只是标准`pow`函数的包装器，如下面的代码片段所示:\n\n```cpp\nfunction<long long(int, int)> power = [](auto base, auto exponent){\n    return pow(base, exponent);\n};\n```\n\n我们如何开始实施记忆化？嗯，记忆的核心是缓存。每当第一次调用函数时，它会正常运行，但也会将结果与输入值一起存储。在随后的调用中，该函数将在映射中进行搜索，以查看该值是否被缓存，如果被缓存，则返回该值。\n\n这意味着我们将需要一个缓存，它将参数作为关键字，并将计算结果作为值。要将参数组合在一起，我们可以简单地使用一对或一个元组:\n\n```cpp\ntuple<int, int> parameters\n```\n\n因此，缓存将是:\n\n```cpp\n    map<tuple<int, int>, long long> cache;\n```\n\n让我们改变我们的`power`函数来使用这个缓存。首先，我们需要在缓存中查找一个结果:\n\n```cpp\n    function<long long(int, int)> memoizedPower = [&cache](int base, \n        int exponent){\n            tuple<int, int> parameters(base, exponent);\n            auto valueIterator = cache.find(parameters);\n\n```\n\n如果什么都没有找到，我们计算结果并将其存储在缓存中。如果发现了什么，那就是我们返回的值:\n\n```cpp\n        if(valueIterator == cache.end()){\n            result = pow(base, exponent);\n            cache[parameters] = result;\n        } else{\n            result = valueIterator -> second;\n        }\n        return result; \n```\n\n为了检查这种方法是否工作正常，让我们运行一些测试:\n\n```cpp\n    CHECK_EQ(power(1, 1), memoizedPower(1, 1));\n    CHECK_EQ(power(3, 19), memoizedPower(3, 19));\n    CHECK_EQ(power(2, 25), memoizedPower(2, 25));\n```\n\n一切正常。现在让我们比较一下 power 的两个版本，在下面的代码片段中有记忆和没有记忆。下面的代码展示了我们如何提取一种更通用的方法来记忆函数:\n\n```cpp\n    function<long long(int, int)> power = [](int base, int exponent){\n        return pow(base, exponent);\n    };\n\n    map<tuple<int, int>, long long> cache;\n\n    function<long long(int, int)> memoizedPower = [&cache](int base, \n        int exponent){\n            tuple<int, int> parameters(base, exponent);\n            auto valueIterator = cache.find(parameters);\n            long long result;\n            if(valueIterator == cache.end()){\n result = pow(base, exponent);\n            cache[parameters] = result;\n        } else{\n            result = valueIterator -> second;\n        }\n        return result; \n    };\n```\n\n第一个观察结果是，我们可以用对原始幂函数的调用来替换粗线，所以让我们这样做:\n\n```cpp\n    function<long long(int, int)> memoizedPower = [&cache, &power](int \n        base, int exponent){\n            tuple<int, int> parameters(base, exponent);\n            auto valueIterator = cache.find(parameters);\n            long long result;\n            if(valueIterator == cache.end()){\n result = power(base, exponent);\n            cache[parameters] = result;\n        } else{\n            result = valueIterator -> second;\n        }\n        return result; \n    };\n```\n\n如果我们传入在记忆过程中需要调用的函数，我们会得到一个更一般的解:\n\n```cpp\n    auto memoize = [&cache](int base, int exponent, auto \n        functionToMemoize){\n            tuple<int, int> parameters(base, exponent);\n            auto valueIterator = cache.find(parameters);\n            long long result;\n            if(valueIterator == cache.end()){\n            result = functionToMemoize(base, exponent);\n            cache[parameters] = result;\n        } else{\n            result = valueIterator -> second;\n        }\n        return result; \n    };\n\n    CHECK_EQ(power(1, 1), memoize(1, 1, power));\n    CHECK_EQ(power(3, 19), memoize(3, 19, power));\n    CHECK_EQ(power(2, 25), memoize(2, 25, power));\n```\n\n但是返回一个记忆化的函数不是很好吗？我们可以修改我们的`memoize`函数来接收一个函数，并返回一个被记忆的函数，它接收与初始函数相同的参数:\n\n```cpp\n    auto memoize = [](auto functionToMemoize){\n        map<tuple<int, int>, long long> cache;\n return [&](int base, int exponent) {\n            tuple<int, int> parameters(base, exponent);\n            auto valueIterator = cache.find(parameters);\n            long long result;\n            if(valueIterator == cache.end()){\n                result = functionToMemoize(base, exponent);\n                cache[parameters] = result;\n            } else{\n                result = valueIterator -> second;\n            }\n            return result; \n            };\n    };\n    auto memoizedPower = memoize(power);\n```\n\n这种变化最初不起作用——我遇到了分割错误。原因是我们正在更改 lambda 内部的缓存。为了使它工作，我们需要使 lambda 可变并按值捕获:\n\n```cpp\n    auto memoize = [](auto functionToMemoize){\n        map<tuple<int, int>, long long> cache;\n return [=](int base, int exponent) mutable {\n            tuple<int, int> parameters(base, exponent);\n            auto valueIterator = cache.find(parameters);\n            long long result;\n            if(valueIterator == cache.end()){\n                result = functionToMemoize(base, exponent);\n                cache[parameters] = result;\n            } else{\n                result = valueIterator -> second;\n            }\n            return result; \n            };\n    };\n```\n\n我们现在有了一个函数，它可以记住任何带有两个整数参数的函数。借助几个类型参数，很容易使它更通用。我们需要返回值的类型、第一个参数的类型和第二个参数的类型:\n\n```cpp\ntemplate<typename ReturnType, typename FirstArgType, typename \n    SecondArgType>\nauto memoizeTwoParams = [](function<ReturnType(FirstArgType, SecondArgType)> functionToMemoize){\n    map<tuple<FirstArgType, SecondArgType>, ReturnType> cache;\n    return [=](FirstArgType firstArg, SecondArgType secondArg) mutable {\n        tuple<FirstArgType, SecondArgType> parameters(firstArg, \n    secondArg);\n        auto valueIterator = cache.find(parameters);\n        ReturnType result;\n        if(valueIterator == cache.end()){\n            result = functionToMemoize(firstArg, secondArg);\n            cache[parameters] = result;\n        } else{\n            result = valueIterator -> second;\n        }\n        return result; \n    };\n};\n```\n\n对于任何有两个参数的函数，我们都得到了一个记忆函数。我们可以做得更好。C++ 允许我们使用带有不确定数量的类型参数的模板——所谓的**变量模板**。利用它们的魔力，我们最终得到了一个记忆化的实现，它可以与具有任意数量参数的任何函数一起工作:\n\n```cpp\ntemplate<typename ReturnType, typename... Args>\nfunction<ReturnType(Args...)> memoize(function<ReturnType(Args...)> f){\n    map<tuple<Args...>, ReturnType> cache;\n    return ([=](Args... args) mutable  {\n            tuple<Args...> theArguments(args...);\n            auto cached = cache.find(theArguments);\n            if(cached != cache.end()) return cached -> second;\n            auto result = f(args...);\n            cache[theArguments] = result;\n            return result;\n    });\n};\n```\n\n该函数有助于缓存任何其他函数；然而，有一个问题。直到现在，我们一直在使用权力的包装实现。下面是一个例子，说明如果我们编写自己的代码会是什么样子:\n\n```cpp\nfunction<long long(int, int)> power = [&](auto base, auto exponent) \n{\n    return (exponent == 0) ? 1 : base * power(base, exponent - 1);\n};\n```\n\n记住这个函数只会缓存最终结果。然而，该函数是递归的，对我们的`memoize`函数的调用不会记住递归的中间结果。为此，我们需要告诉我们的记忆化幂函数不要调用幂函数，而要调用记忆化`power`函数。\n\n不幸的是，没有简单的方法可以做到这一点。我们可以将递归调用的函数作为参数传递，但是由于实现的原因，这将改变原始的函数签名。或者我们可以重写函数来利用记忆。\n\n尽管如此，我们最终还是找到了一个很好的解决方案。让我们来测试一下。\n\n# 使用记忆\n\n让我们使用`measureExecutionTimeForF`函数来测量对我们的`power`函数进行各种调用所需的时间。也是时候想想我们期待的结果了。我们确实缓存了重复调用的值，但是这需要在每次调用函数时都有自己的处理和内存。所以，也许会有帮助，也许不会。除非我们尝试，否则我们不会知道:\n\n```cpp\nTEST_CASE(\"Pow vs memoized pow\"){\n    function<int(int, int)> power = [](auto first, auto second){\n        return pow(first, second);\n    };\n\n    cout << \"Computing pow\" << endl;\n    printDuration(\"First call no memoization: \",  [&](){ return \n        power(5, 24);});\n    printDuration(\"Second call no memoization: \", [&](){return power(3, \n        1024);});\n    printDuration(\"Third call no memoization: \", [&](){return power(9, \n        176);});\n    printDuration(\"Fourth call no memoization (same as first call): \", \n        [&](){return power(5, 24);});\n\n    auto powerWithMemoization = memoize(power);\n    printDuration(\"First call with memoization: \",  [&](){ return \n        powerWithMemoization(5, 24);});\n    printDuration(\"Second call with memoization: \", [&](){return \n        powerWithMemoization(3, 1024);});\n    printDuration(\"Third call with memoization: \", [&](){return \n        powerWithMemoization(9, 176);});\n    printDuration(\"Fourth call with memoization (same as first call): \n        \", [&](){return powerWithMemoization(5, 24);});\n    cout << \"DONE computing pow\" << endl;\n\n    CHECK_EQ(power(5, 24),  powerWithMemoization(5, 24));\n    CHECK_EQ(power(3, 1024),  powerWithMemoization(3, 1024));\n    CHECK_EQ(power(9, 176),  powerWithMemoization(9, 176));\n}\n```\n\n这段代码用相同的值调用`power`函数，最后一次调用返回第一个值。然后它继续做同样的事情，但是在创建了`power`的记忆版本之后。最后，进行健全性检查——比较`power`函数和记忆化`power`函数的结果，以确保我们在`memoize`函数中没有错误。\n\n问题是——记忆是否改善了执行系列中最后一个调用的时间(与系列中第一个调用完全相同)？在我的配置中，结果是混合的，如下面的代码片段所示:\n\n```cpp\nComputing pow\nFirst call no memoization: 26421 ns\nSecond call no memoization: 5207 ns\nThird call no memoization: 2058 ns\nFourth call no memoization (same as first call): 179 ns\nFirst call with memoization: 2380 ns\nSecond call with memoization: 2207 ns\nThird call with memoization: 1539 ns\nFourth call with memoization (same as first call): 936 ns\nDONE computing pow\n\n```\n\n或者，为了更好地查看(首先是没有记忆的调用)，有以下内容:\n\n```cpp\nFirst call: 26421 ns > 2380 ns\nSecond call: 5207 ns > 2207 ns\nThird call: 2058 ns > 1539 ns\nFourth call: 179 ns < 936 ns\n```\n\n总的来说，记忆化的调用更好，除了我们重复第一次调用的时候。当然，当重复运行测试时，结果会有所不同，但这表明提高性能并不像仅仅使用缓存那么容易。幕后发生了什么？我认为最有可能的解释是另一种缓存机制在起作用——CPU 或其他。\n\n如果有什么不同的话，这证明了测量的重要性。毫不奇怪，CPU 和编译器已经做了相当多的优化，我们只能在代码中做这么多。\n\n如果我们尝试递归记忆呢？我重写了`power`函数来递归使用 memoization，它将缓存和递归调用混合在一起。下面是代码:\n\n```cpp\n    map<tuple<int, int>, long long> cache;\n    function<long long(int, int)> powerWithMemoization = [&](auto base, \n        auto exponent) -> long long{\n            if(exponent == 0) return 1;\n            long long value;\n\n            tuple<int, int> parameters(base, exponent);\n            auto valueIterator = cache.find(parameters);\n            if(valueIterator == cache.end()){\n            value = base * powerWithMemoization(base, exponent - 1);\n            cache[parameters] = value;\n            } else {\n            value = valueIterator->second;\n        };\n        return value;\n    };\n```\n\n当我们运行它时，结果如下:\n\n```cpp\nComputing pow\nFirst call no memoization: 1761 ns\nSecond call no memoization: 106994 ns\nThird call no memoization: 8718 ns\nFourth call no memoization (same as first call): 1395 ns\nFirst call with recursive memoization: 30921 ns\nSecond call with recursive memoization: 2427337 ns\nThird call with recursive memoization: 482062 ns\nFourth call with recursive memoization (same as first call): 1721 ns\nDONE computing pow\n```\n\n或者，在压缩视图中(首先是没有记忆的调用)，有以下内容:\n\n```cpp\nFirst call: 1761 ns < 30921 ns\nSecond call: 106994 ns < 2427337 ns\nThird call: 8718 ns < 482062 ns\nFourth call: 1395 ns < 1721 ns\n```\n\n如您所见，构建缓存的时间非常长。然而，它为重复调用提供了回报，但在这种情况下，它仍然无法击败中央处理器和编译器的优化。\n\n记忆有帮助吗？当我们使用一个更复杂的函数时，它就会发生。接下来让我们试着计算两个数的阶乘之差。我们将使用阶乘的简单实现，我们将尝试首先记住阶乘函数，然后计算差的函数。为了保持一致，我们将使用与之前相同的数字对。让我们看看下面代码片段中的代码:\n\n```cpp\nTEST_CASE(\"Factorial difference vs memoized\"){\n    function<int(int)> fact = [&fact](int n){\n        if(n == 0) return 1;\n        return n * fact(n-1);\n    };\n\n    function<int(int, int)> factorialDifference = [&fact](auto first, \n        auto second){\n            return fact(second) - fact(first);\n    };\n    cout << \"Computing factorial difference\" << endl;\n    printDuration(\"First call no memoization: \",  [&](){ return \n        factorialDifference(5, 24);});\n    printDuration(\"Second call no memoization: \", [&](){return \n        factorialDifference(3, 1024);});\n    printDuration(\"Third call no memoization: \", [&](){return \n        factorialDifference(9, 176);});\n    printDuration(\"Fourth call no memoization (same as first call): \", \n        [&](){return factorialDifference(5, 24);});\n\n    auto factWithMemoization = memoize(fact);\n    function<int(int, int)> factorialMemoizedDifference = \n        [&factWithMemoization](auto first, auto second){\n        return factWithMemoization(second) - \n            factWithMemoization(first);\n    };\n    printDuration(\"First call with memoized factorial: \",  [&](){ \n        return factorialMemoizedDifference(5, 24);});\n    printDuration(\"Second call with memoized factorial: \", [&](){return \n        factorialMemoizedDifference(3, 1024);});\n    printDuration(\"Third call with memoized factorial: \", [&](){return \n        factorialMemoizedDifference(9, 176);});\n    printDuration(\"Fourth call with memoized factorial (same as first \n        call): \", [&](){return factorialMemoizedDifference(5, 24);});\n\n    auto factorialDifferenceWithMemoization = \n        memoize(factorialDifference);\n    printDuration(\"First call with memoization: \",  [&](){ return \n        factorialDifferenceWithMemoization(5, 24);});\n    printDuration(\"Second call with memoization: \", [&](){return \n        factorialDifferenceWithMemoization(3, 1024);});\n    printDuration(\"Third call with memoization: \", [&](){return \n        factorialDifferenceWithMemoization(9, 176);});\n    printDuration(\"Fourth call with memoization (same as first call): \n        \", [&](){return factorialDifferenceWithMemoization(5, 24);});\n\n    cout << \"DONE computing factorial difference\" << endl;\n\n    CHECK_EQ(factorialDifference(5, 24),  \n        factorialMemoizedDifference(5, 24));\n    CHECK_EQ(factorialDifference(3, 1024),  \n        factorialMemoizedDifference(3, 1024));\n    CHECK_EQ(factorialDifference(9, 176),        \n        factorialMemoizedDifference(9, 176));\n\n    CHECK_EQ(factorialDifference(5, 24),  \n        factorialDifferenceWithMemoization(5, 24));\n    CHECK_EQ(factorialDifference(3, 1024),  \n        factorialDifferenceWithMemoization(3, 1024));\n    CHECK_EQ(factorialDifference(9, 176),  \n        factorialDifferenceWithMemoization(9, 176));\n}\n```\n\n结果如何？让我们首先看看普通函数和使用记忆化阶乘的函数之间的区别:\n\n```cpp\nComputing factorial difference\nFirst call no memoization: 1727 ns\nSecond call no memoization: 79908 ns\nThird call no memoization: 8037 ns\nFourth call no memoization (same as first call): 1539 ns\nFirst call with memoized factorial: 4672 ns\nSecond call with memoized factorial: 41183 ns\nThird call with memoized factrorial: 10029 ns\nFourth call with memoized factorial (same as first call): 1105 ns\n```\n\n让我们再一次并排比较它们:\n\n```cpp\nFirst call: 1727 ns < 4672 ns\nSecond call: 79908 ns > 41183 ns\nThird call: 8037 ns < 10029 ns\nFourth call: 1539 ns > 1105 ns\n```\n\n虽然其他调用的结果是混合的，但是当达到缓存值时，memoized 函数比非 memoized 函数有大约 20%的改进。这似乎是一个小小的改进，因为阶乘是递归的，所以，理论上，记忆应该有很大的帮助。然而，我们没有记住递归。相反，阶乘函数仍在递归调用非 memoized 版本。我们稍后再来讨论这个问题；现在，让我们来看看记忆`factorialDifference`功能时会发生什么:\n\n```cpp\nFirst call no memoization: 1727 ns\nSecond call no memoization: 79908 ns\nThird call no memoization: 8037 ns\nFourth call no memoization (same as first call): 1539 ns\nFirst call with memoization: 2363 ns\nSecond call with memoization: 39700 ns\nThird call with memoization: 8678 ns\nFourth call with memoization (same as first call): 704 ns\n```\n\n让我们一起看看结果:\n\n```cpp\nFirst call: 1727 ns < 2363 ns\nSecond call: 79908 ns > 39700 ns\nThird call: 8037 ns < 8678 ns\nFourth call: 1539 ns > 704 ns\n```\n\n在缓存值上，内存化版本的速度是非内存化版本的两倍！这是巨大的！然而，当我们没有缓存该值时，我们会用性能上的提升来买单。还有，第二次通话时发生了一些奇怪的事情；某种缓存可能会干扰我们的结果。\n\n我们能通过优化阶乘函数的所有递归来改善这一点吗？让我想想。我们需要更改我们的阶乘函数，以便缓存适用于每个调用。为此，我们需要递归调用 memoized 阶乘函数，而不是普通的阶乘函数，如下所示:\n\n```cpp\n    map<int, int> cache;\n    function<int(int)> recursiveMemoizedFactorial = \n        [&recursiveMemoizedFactorial, &cache](int n) mutable{\n        auto value = cache.find(n); \n        if(value != cache.end()) return value->second;\n        int result;\n\n        if(n == 0) \n            result = 1;\n        else \n            result = n * recursiveMemoizedFactorial(n-1);\n\n        cache[n] = result;\n        return result;\n    };\n```\n\n我们使用差分函数，它递归地记住对阶乘的两次调用:\n\n```cpp\n    function<int(int, int)> factorialMemoizedDifference =  \n        [&recursiveMemoizedFactorial](auto first, auto second){\n                return recursiveMemoizedFactorial(second) -  \n                    recursiveMemoizedFactorial(first);\n    };\n```\n\n通过并行运行没有记忆的初始函数和具有相同数据的前一个函数，我得到了以下输出:\n\n```cpp\nComputing factorial difference\nFirst call no memoization: 1367 ns\nSecond call no memoization: 58045 ns\nThird call no memoization: 16167 ns\nFourth call no memoization (same as first call): 1334 ns\nFirst call with recursive memoized factorial: 16281 ns\nSecond call with recursive memoized factorial: 890056 ns\nThird call with recursive memoized factorial: 939 ns\nFourth call with recursive memoized factorial (same as first call): 798 ns \n```\n\n我们可以一起看看这个:\n\n```cpp\nFirst call: 1,367 ns < 16,281 ns\nSecond call: 58,045 ns < 890,056 ns Third call: 16,167 ns > 939 ns Fourth call: 1,334 ns > 798 ns\n```\n\n正如我们所看到的，缓存正在积累，第一次大型计算就有大量的惩罚命中；第二个电话涉及 1024！但是，由于缓存命中，后续调用要快得多。\n\n总之，我们可以说，当有足够的可用内存时，记忆对于加速重复的复杂计算是有用的。这可能需要一些调整，因为缓存大小和缓存命中取决于对函数的调用次数和重复调用次数。所以，不要认为这是理所当然的——衡量，衡量，衡量。\n\n# 尾部递归优化\n\n递归算法在函数编程中非常常见。事实上，我们的许多命令循环可以用纯函数重写为递归算法。\n\n然而，递归在命令式编程中不是很流行，因为它有一些问题。首先，与命令式循环相比，开发人员倾向于较少练习递归算法。第二，可怕的堆栈溢出——默认情况下，递归调用放在堆栈上，如果迭代次数太多，堆栈就会溢出一个难看的错误。\n\n幸运的是，编译器很聪明，可以为我们解决这个问题，同时优化递归函数。进入尾部递归优化。\n\n让我们来看看一个简单的递归函数。我们将重用上一节中的阶乘，如下所示:\n\n```cpp\n    function<int(int)> fact = [&fact](int n){\n        if(n == 0) return 1;\n        return n * fact(n-1);\n    };\n```\n\n通常，每个调用都会放在堆栈上，因此您的堆栈会随着每个调用而增长。让我们想象一下:\n\n```cpp\nStack content fact(1024)\n1024 * fact(1023)\n1023 * fact(1022)\n...\n1 * fact(0)\nfact(0) = 1 => unwind the stack\n```\n\n我们可以通过重写代码来避免堆栈。我们注意到递归调用出现在最后；因此，我们可以重写函数，类似于下面的伪代码:\n\n```cpp\n    function<int(int)> fact = [&fact](int n){\n        if(n == 0) return 1;\n        return n * (n-1) * (n-1-1) * (n-1-1-1) * ... * fact(0);\n    };\n```\n\n简而言之，如果我们启用正确的优化标志，这就是编译器可以为我们做的。这种调用不仅占用更少的内存，避免堆栈溢出，而且速度更快。\n\n到现在，你应该知道不要相信任何人的主张——包括我的——而不去衡量它们。那么，让我们来验证这个假设。\n\n首先，我们需要一个测试来测量多次调用阶乘函数的时间。我选择了一些值来进行测试:\n\n```cpp\nTEST_CASE(\"Factorial\"){\n    function<int(int)> fact = [&fact](int n){\n        if(n == 0) return 1;\n        return n * fact(n-1);\n    };\n\n    printDuration(\"Duration for 0!: \", [&](){return fact(0);});\n    printDuration(\"Duration for 1!: \", [&](){return fact(1);});\n    printDuration(\"Duration for 10!: \", [&](){return fact(10);});\n    printDuration(\"Duration for 100!: \", [&](){return fact(100);});\n    printDuration(\"Duration for 1024!: \", [&](){return fact(1024);});\n}\n```\n\n然后，我们需要在禁用和启用优化的情况下编译这个函数。优化尾部递归的 **GNU 编译器集合** ( **GCC** )标志为`-foptimize-sibling-calls`；该名称指的是这样一个事实，即该标志优化了同级调用和尾部调用。我不会详细讨论兄弟调用优化的作用；这么说吧，这丝毫不影响我们的测试。\n\n是时候运行这两个程序了。首先，让我们看看原始输出:\n\n*   这是没有优化的程序:\n\n```cpp\nDuration for 0!: 210 ns\nDuration for 1!: 152 ns\nDuration for 10!: 463 ns\nDuration for 100!: 10946 ns\nDuration for 1024!: 82683 ns\n```\n\n*   这是带有优化的程序:\n\n```cpp\nDuration for 0!: 209 ns\nDuration for 1!: 152 ns\nDuration for 10!: 464 ns\nDuration for 100!: 6455 ns\nDuration for 1024!: 75602 ns\n```\n\n现在让我们一起看看结果；没有优化的持续时间在左边:\n\n```cpp\nDuration for 0!: 210 ns > 209 ns\nDuration for 1!: 152 ns  = 152 ns\nDuration for 10!: 463 ns < 464 ns\nDuration for 100!: 10946 ns > 6455 ns\nDuration for 1024!: 82683 ns > 75602 ns\n```\n\n看来优化真的在我的机器上实现了更大的价值。这再一次证明了无论什么时候性能都很重要。\n\n在接下来的部分中，我们将以各种方式对代码进行实验，并测量结果。\n\n# 完全优化的呼叫\n\n出于好奇，我决定在打开所有安全优化标志的情况下运行同一个程序。在 GCC 中，这个选项是`-O3`。至少可以说，结果是惊人的:\n\n```cpp\nDuration for 0!: 128 ns\nDuration for 1!: 96 ns\nDuration for 10!: 96 ns\nDuration for 100!: 405 ns\nDuration for 1024!: 17249 ns\n```\n\n让我们将启用所有优化标志(下一个片段中的第二个值)的结果与仅尾部递归优化的结果进行比较:\n\n```cpp\nDuration for 0!: 209 ns > 128 ns\nDuration for 1!: 152 ns > 96 ns\nDuration for 10!: 464 ns > 96 ns\nDuration for 100!: 6455 ns > 405 ns\nDuration for 1024!: 75602 ns > 17249 ns\n```\n\n正如你所看到的，差别是惊人的。结论是，虽然尾部递归优化是有用的，但更好的是让 CPU 缓存命中和所有好东西都由编译器启用。\n\n但是我们使用的是`if`语句；当我们使用`?:`运算符时，这是否会有所不同？\n\n# If vs？：\n\n出于好奇，我决定使用`?:`运算符而不是`if`语句重写代码，如下所示:\n\n```cpp\n    function<int(int)> fact = [&fact](int n){\n        return (n == 0) ? 1 : (n * fact(n-1));\n    };\n```\n\n我不知道会发生什么，结果很有趣。让我们看看原始输出:\n\n*   没有优化标志:\n\n```cpp\nDuration for 0!: 633 ns\nDuration for 1!: 561 ns\nDuration for 10!: 1441 ns\nDuration for 100!: 20407 ns\nDuration for 1024!: 215600 ns\n```\n\n*   尾部递归标志打开时:\n\n```cpp\nDuration for 0!: 277 ns\nDuration for 1!: 214 ns\nDuration for 10!: 578 ns\nDuration for 100!: 9573 ns\nDuration for 1024!: 81182 ns\n```\n\n让我们来看看结果的比较；没有优化的持续时间排在第一位:\n\n```cpp\nDuration for 0!: 633 ns > 277 ns\nDuration for 1!: 561 ns > 214 ns\nDuration for 10!: 1441 ns > 578 ns\nDuration for 100!: 20407 ns > 9573 ns\nDuration for 1024!: 75602 ns > 17249 ns\n```\n\n两个版本的差别非常大，这是我没有完全预料到的。和往常一样，这很可能是 GCC 编译器的结果，您应该自行测试。然而，这个版本似乎更适合我的编译器的尾部优化——至少可以说是一个有趣的结果。\n\n# 双递归\n\n尾部递归对双递归有效吗？我们需要想出一个例子，将递归从一个函数传递到另一个函数来检查这一点。我决定写两个函数，`f1`和`f2`，递归调用对方。`f1`将当前参数乘以`f2(n - 1 )`，而`f2`将`f1(n)`加到`f1(n-1)`。下面是代码:\n\n```cpp\n    function<int(int)> f2;\n    function<int(int)> f1 = [&f2](int n){\n        return (n == 0) ? 1 : (n * f2(n-1));\n    };\n\n    f2 = [&f1](int n){\n        return (n == 0) ? 2 : (f1(n) + f1(n-1));\n    };\n```\n\n让我们用从`0`到`8`的值来检查呼叫`f1`的时间:\n\n```cpp\n    printDuration(\"Duration for f1(0): \", [&](){return f1(0);});\n    printDuration(\"Duration for f1(1): \", [&](){return f1(1);});\n    printDuration(\"Duration for f1(2): \", [&](){return f1(2);});\n    printDuration(\"Duration for f1(3): \", [&](){return f1(3);});\n    printDuration(\"Duration for f1(4): \", [&](){return f1(4);});\n    printDuration(\"Duration for f1(5): \", [&](){return f1(5);});\n    printDuration(\"Duration for f1(6): \", [&](){return f1(6);});\n    printDuration(\"Duration for f1(7): \", [&](){return f1(7);});\n    printDuration(\"Duration for f1(8): \", [&](){return f1(8);});\n```\n\n以下是我们获得的信息:\n\n*   没有尾部调用优化:\n\n```cpp\nDuration for f1(0): 838 ns\nDuration for f1(1): 825 ns\nDuration for f1(2): 1218 ns\nDuration for f1(3): 1515 ns\nDuration for f1(4): 2477 ns\nDuration for f1(5): 3919 ns\nDuration for f1(6): 5809 ns\nDuration for f1(7): 9354 ns\nDuration for f1(8): 14884 ns\n```\n\n*   通过呼叫优化:\n\n```cpp\nDuration for f1(0): 206 ns\nDuration for f1(1): 327 ns\nDuration for f1(2): 467 ns\nDuration for f1(3): 642 ns\nDuration for f1(4): 760 ns\nDuration for f1(5): 1155 ns\nDuration for f1(6): 2023 ns\nDuration for f1(7): 3849 ns\nDuration for f1(8): 4986 ns\n```\n\n让我们一起来看看结果；没有尾部优化的呼叫持续时间在左边:\n\n```cpp\nf1(0): 838 ns > 206 ns\nf1(1): 825 ns > 327 ns\nf1(2): 1218 ns > 467 ns\nf1(3): 1515 ns > 642 ns\nf1(4): 2477 ns > 760 ns\nf1(5): 3919 ns > 1155 ns\nf1(6): 5809 ns > 2023 ns\nf1(7): 9354 ns > 3849 ns\nf1(8): 14884 ns > 4986 ns\n```\n\n差异确实非常大，表明代码得到了极大的优化。但是，请记住，对于 GCC，我们使用的是`-foptimize-sibling-calls`优化标志。此标志执行两种类型的优化:尾部调用和同级调用。同级调用是对具有相同大小的返回类型和相同总大小的参数列表的函数的调用，因此允许编译器用尾部调用类似地处理它们。很有可能，在我们的例子中，两种优化都被应用了。\n\n# 用异步代码优化执行时间\n\n当我们有多个线程时，我们可以使用两种关闭技术来优化执行时间:并行执行和异步执行。我们在前面的章节中已经看到了并行执行是如何工作的；异步调用呢？\n\n首先，让我们提醒自己什么是异步调用。我们想打一个电话，在主线程上正常继续，并在未来的某个时候得到结果。对我来说，这听起来是一份完美的职能工作。我们只需要调用函数，让它们执行，过一会儿再和它们对话。\n\n既然我们已经谈到了未来，那么我们就来谈谈 C++ 中的`future`构造。\n\n# 期货\n\n我们已经确定，避免管理程序中的线程是理想的，除非是在做非常专业的工作时，但是我们需要并行执行，并且经常需要同步来从另一个线程获得结果。一个典型的例子是一个长计算，它会阻塞主线程，除非我们在它自己的线程中运行它。我们如何知道计算何时完成，如何得到计算结果？\n\n在 1976-1977 年，计算机科学中提出了两个概念来简化这个问题的解决方案——未来和承诺。虽然这些概念在各种技术中经常互换使用，但在 C++ 中，它们有特定的含义:\n\n*   未来可以在处理同步的同时从提供程序中检索值\n*   承诺存储未来的价值，此外还提供同步点\n\n由于其性质，一个`future`对象在 C++ 中是有限制的。它不能复制，只能移动，并且只有在与共享状态相关联时才有效。这意味着我们只能通过调用`async`、`promise.get_future()`或`packaged_task.get_future()`来创建一个有效的未来对象。\n\n还值得一提的是，承诺和未来在实现中使用线程库；因此，您可能需要向另一个库添加依赖项。在我的系统(Ubuntu 18.04，64 位)上，用 g++ 编译的时候，不得不向`pthread`库添加链接依赖；如果您在 mingw 或 cygwin 配置上使用 g++ 的话，我想您也会需要同样的东西。\n\n我们先来看看如何串联使用`future`和`promise`。首先，我们将为一个秘密信息创建一个`promise`:\n\n```cpp\n    promise<string> secretMessagePromise;\n```\n\n然后，让我们创建一个`future`并使用它启动一个新线程。该线程将使用一个简单打印秘密消息的 lambda:\n\n```cpp\n    future<string> secretMessageFuture = \n        secretMessagePromise.get_future();\n    thread isPrimeThread(printSecretMessage, ref(secretMessageFuture));\n```\n\n注意我们需要避免复制`future`；在这种情况下，我们使用未来的引用包装器。\n\n我们暂时坚持这条线索；接下来的事情就是履行承诺，也就是设定一个值:\n\n```cpp\n    secretMessagePromise.set_value(\"It's a secret\");\n    isPrimeThread.join();\n```\n\n同时，另一个线程会做一些事情，然后会要求我们遵守诺言。嗯，不完全是；它会询问`promise`的值，这会阻止它，直到调用`join()`:\n\n```cpp\nauto printSecretMessage = [](future<string>& secretMessageFuture) {\n    string secretMessage = secretMessageFuture.get();\n    cout << \"The secret message: \" << secretMessage << '\\n';\n};\n```\n\n正如您可能注意到的，这个方法设置了在主线程中计算值的责任。如果我们想让它在第二线程上呢？我们只需要使用`async`。\n\n假设我们想检查一个数是否是质数。我们首先编写一个 lambda，它将以一种天真的方式检查从`2`到`x-1`的每个可能除数，并检查`x`是否能被它整除。如果它不能被任何值整除，它就是一个质数:\n\n```cpp\nauto is_prime = [](int x) {\n    auto xIsDivisibleBy = bind(isDivisibleBy, x, _1);\n    return none_of_collection(\n            rangeFrom2To(x - 1), \n            xIsDivisibleBy\n        );\n};\n```\n\n使用了几个辅助 lambdas。一个用于生成这样的范围:\n\n```cpp\nauto rangeFromTo = [](const int start, const int end){\n    vector<int> aVector(end);\n    iota(aVector.begin(), aVector.end(), start);\n    return aVector;\n};\n```\n\n然后，它专门用于生成以`2`开始的范围:\n\n```cpp\nauto rangeFrom2To = bind(rangeFromTo, 2, _1);\n```\n\n然后，检查两个数字是否可分的谓词:\n\n```cpp\nauto isDivisibleBy = [](auto value, auto factor){\n    return value % factor == 0;\n};\n```\n\n为了在独立于主线程的线程中运行这个函数，我们需要使用`async`声明一个`future`:\n\n```cpp\n    future<bool> futureIsPrime(async(is_prime, 2597));\n```\n\n`async`的第二个参数是我们函数的输入参数。允许多个参数。\n\n然后，我们可以做其他事情，最后，询问结果:\n\n```cpp\nTEST_CASE(\"Future with async\"){\n    future<bool> futureIsPrime(async(is_prime, 7757));\n    cout << \"doing stuff ...\" << endl;\n bool result = futureIsPrime.get();\n\n    CHECK(result);\n}\n```\n\n加粗的代码行标记了主线程停止等待辅助线程结果的时间点。\n\n如果需要多个`future`，可以使用。在下面的例子中，我们将在四个不同的线程中用四个不同的值运行`is_prime`，如下所示:\n\n```cpp\nTEST_CASE(\"more futures\"){\n    future<bool> future1(async(is_prime, 2));\n    future<bool> future2(async(is_prime, 27));\n    future<bool> future3(async(is_prime, 1977));\n    future<bool> future4(async(is_prime, 7757));\n\n    CHECK(future1.get());\n    CHECK(!future2.get());\n    CHECK(!future3.get());\n    CHECK(future4.get());\n}\n```\n\n# 功能异步代码\n\n我们已经看到线程最简单的实现是 lambda，但是我们可以做得更多。最后一个示例使用多个线程对不同的值异步运行相同的操作，它可以变成一个功能性的高阶函数。\n\n但是让我们从几个简单的循环开始。首先，我们将输入值和预期结果转换成向量:\n\n```cpp\n    vector<int> values{2, 27, 1977, 7757};\n    vector<bool> expectedResults{true, false, false, true};\n```\n\n然后，我们需要一个`for`循环来创建期货。重要的是不要调用`future()`构造函数，因为这将由于试图将新构造的`future`对象复制到容器中而失败。相反，将`async()`的结果直接添加到容器中:\n\n```cpp\n    vector<future<bool>> futures;\n    for(auto value : values){\n        futures.push_back(async(is_prime, value));\n    }\n```\n\n然后，我们需要从线程中获取结果。同样，我们需要避免复制`future`，所以我们将在迭代时使用引用:\n\n```cpp\n    vector<bool> results;\n    for(auto& future : futures){\n        results.push_back(future.get());\n    }\n```\n\n让我们看看整个测试:\n\n```cpp\nTEST_CASE(\"more futures with loops\"){\n    vector<int> values{2, 27, 1977, 7757};\n    vector<bool> expectedResults{true, false, false, true};\n\n    vector<future<bool>> futures;\n    for(auto value : values){\n        futures.push_back(async(is_prime, value));\n    }\n\n    vector<bool> results;\n    for(auto& future : futures){\n        results.push_back(future.get());\n    }\n\n    CHECK_EQ(results, expectedResults);\n}\n```\n\n很明显，我们可以把它变成一些转换调用。但是，我们需要特别注意避免期货的复制。首先，我创建了一个有助于创建`future`的 lambda:\n\n```cpp\n    auto makeFuture = [](auto value){\n        return async(is_prime, value);\n    };\n```\n\n第一个`for`循环然后变成一个`transformAll`调用:\n\n```cpp\n    vector<future<bool>> futures = transformAll<vector<future<bool>>>\n       (values, makeFuture);\n```\n\n第二部分比预期的要棘手。我们对`transformAll`的实现不起作用，所以我将把`transform`改为内联调用:\n\n```cpp\n    vector<bool> results(values.size());\n    transform(futures.begin(), futures.end(), results.begin(), []\n        (future<bool>& future){ return future.get();});\n```\n\n我们最终通过了以下测试:\n\n```cpp\nTEST_CASE(\"more futures functional\"){\n    vector<int> values{2, 27, 1977, 7757};\n\n    auto makeFuture = [](auto value){\n        return async(is_prime, value);\n    };\n\n    vector<future<bool>> futures = transformAll<vector<future<bool>>>\n        (values, makeFuture);\n    vector<bool> results(values.size());\n    transform(futures.begin(), futures.end(), results.begin(), []\n        (future<bool>& future){ return future.get();});\n\n    vector<bool> expectedResults{true, false, false, true};\n\n    CHECK_EQ(results, expectedResults);\n}\n```\n\n老实说，这是迄今为止最难正确实现的代码。在从事期货工作时，会有很多事情出错，原因并不明显。这些错误消息毫无帮助，至少对我的 g++ 版本来说是如此。正如我在本节中向您展示的那样，我成功完成这项工作的唯一方法是一步一步来。\n\n然而，这个代码示例显示了一个重要事实；通过对 futures 的深入思考和测试，我们可以并行化高阶函数。因此，如果您需要更好的性能，可以使用多个内核，并且不能等待标准中并行运行策略的实现，这是一个可能的解决方案。如果只是为了这个，我想我的努力是有用的！\n\n既然我们讨论的是异步调用，我们也可以快速浏览一下反应式编程的世界。\n\n# 反应式编程的味道\n\n**反应式编程**是一种专注于处理数据流的代码编写范式。想象一下，必须分析一系列温度值，来自安装在自动驾驶汽车上的传感器的值，或者分享特定公司的值。在反应式编程中，我们接收这种连续的数据流并运行分析它的函数。由于新数据可能不可预测地到达流中，编程模型必须是异步的；也就是说，主线程持续等待新数据，当它到达时，处理被委托给辅助流。结果通常也是异步收集的——要么被推送到用户界面，保存在数据存储中，要么被传递给其他数据流。\n\n我们已经看到函数式编程的主要焦点是数据。因此，函数式编程是处理实时数据流的一个很好的候选对象，这并不奇怪。`map`、`reduce`或`filter`等高阶函数的可组合性，加上并行处理的机会，使得函数式设计成为反应式编程的绝佳解决方案。\n\n我们不会详细讨论反应式编程。通常，使用特定的库或框架来促进这种数据流处理的实现，但是使用我们到目前为止拥有的元素，我们可以编写一个小规模的示例。\n\n我们需要一些东西。首先，数据流；第二，接收数据并立即将其传递到处理流水线的主线程；第三，获取输出的方法。\n\n对于这个例子的目标，我将简单地使用标准输入作为输入流。我们将从键盘输入数字，并以反应的方式检查它们是否是质数，从而保持主线程始终响应。这意味着我们将使用`async`功能为从键盘上读取的每个数字创建一个`future`。输出将被简单地写入输出流。\n\n我们将使用与之前相同的`is_prime`函数，但是添加另一个函数，无论值是否为质数，都会打印到标准输出中:\n\n```cpp\nauto printIsPrime = [](int value){\n    cout << value << (is_prime(value) ? \" is prime\" : \" is not prime\")  \n    << endl;\n};\n```\n\n`main`函数是一个无限循环，它从输入流中读取数据，并在每次有新值进入时启动一个`future`:\n\n```cpp\nint main(){\n    int number;\n\n    while(true){\n        cin >> number;\n        async(printIsPrime, number);\n    }\n}\n```\n\n用一些随机类型的值运行此代码会产生以下输出:\n\n```cpp\n23423\n23423 is not prime\n453576\n453576 is not prime\n53\n53 is prime\n2537\n2537 is not prime\n364544366\n5347\n54\n534532\n436\n364544366 is not prime\n5347 is prime\n54 is not prime\n534532 is not prime\n436 is not prime\n```\n\n如您所见，结果会尽快返回，但程序允许随时引入新数据。\n\n不得不提的是，为了避免每次编译本章代码时出现无限循环，反应式示例可以用`make reactive`编译运行。因为它是一个无限循环，所以你必须用一个中断来停止它。\n\n这是一个基本的反应式编程示例。随着数据量的增加、复杂的流水线以及每个流水线的并行化，它显然会变得更加复杂。然而，我们实现了本节的目标——让您体验反应式编程，以及我们如何使用函数构造和异步调用来使其工作。\n\n我们已经讨论了很多关于优化执行时间的问题，探讨了帮助我们实现更快性能的各种方法。现在是时候看看我们想要减少程序内存使用的情况了。\n\n# 优化内存使用\n\n到目前为止，我们讨论的以函数方式构造代码的方法包括多次传递被视为不可变的集合。因此，这可能会导致集合的副本。例如，让我们看一个简单的代码示例，它使用`transform`来增加向量的所有元素:\n\n```cpp\ntemplate<typename DestinationType>\nauto transformAll = [](const auto source, auto lambda){\n    DestinationType result;\n    transform(source.begin(), source.end(), back_inserter(result), \n        lambda);\n    return result;\n};\n\nTEST_CASE(\"Memory\"){\n    vector<long long> manyNumbers(size);\n    fill_n(manyNumbers.begin(), size, 1000L);\n\n    auto result = transformAll<vector<long long>>(manyNumbers, \n        increment);\n\n    CHECK_EQ(result[0], 1001);\n}\n```\n\n这种实现会导致大量内存分配。首先将`manyNumbers`向量复制到`transformAll`中。然后，`result.push_back()`被自动调用，可能导致内存分配。最后，`result`被返回，但是初始的`manyNumbers`向量仍然被分配。\n\n我们可以立即改善其中的一些问题，但也值得讨论它们与其他可能的优化相比如何。\n\n为了执行测试，我们需要处理大型集合和一种方法来测量进程的内存分配。第一部分很简单——只需分配大量 64 位值(我的编译器上的长整型)；足以分配 1 GB 内存:\n\n```cpp\nconst long size_1GB_64Bits = 125000000;\nTEST_CASE(\"Memory\"){\n    auto size = size_1GB_64Bits;\n    vector<long long> manyNumbers(size);\n    fill_n(manyNumbers.begin(), size, 1000L);\n\n    auto result = transformAll<vector<long long>>(manyNumbers, \n        increment);\n\n    CHECK_EQ(result[0], 1001);\n}\n```\n\n第二部分有点难。幸运的是，在我的 Ubuntu 18.04 系统上，我可以在`/proc/PID/status`中的一个文件中查看一个进程的内存，其中 PID 是进程标识符。有了一点 Bash 魔法，我可以创建一个`makefile`配方，将每 0.1 s 获取的内存值输出到一个文件中，如下所示:\n\n```cpp\nmemoryConsumptionNoMoveIterator: .outputFolder \n    g++ -DNO_MOVE_ITERATOR -std=c++ 17 memoryOptimization.cpp -Wall -\n        Wextra -Werror -o out/memoryOptimization\n    ./runWithMemoryConsumptionMonitoring memoryNoMoveIterator.log\n```\n\n你会注意到`-DNO_MOVE_ITERATOR`的论点；这是一个编译指令，允许我为不同的目标编译同一个文件，以便检查多个解决方案的内存占用。这意味着我们之前的测试是在`#if NO_MOVE_ITERATOR`指令中编写的。\n\n只有一个警告——由于我使用了 bash `watch`命令来生成输出，所以在运行`make memoryConsumptionNoMoveIterator`之后，您将需要按下一个键，对于每隔一个内存日志配方也是如此。\n\n有了这个设置，让我们改进`transformAll`使用更少的内存，看看输出。我们需要从一开始就使用引用类型并为结果分配内存，如下所示:\n\n```cpp\ntemplate<typename DestinationType>\nauto transformAll = [](const auto& source, auto lambda){\n    DestinationType result;\n    result.resize(source.size());\n    transform(source.begin(), source.end(), result.begin(), lambda);\n    return result;\n};\n```\n\n不出所料，改进的结果是，最大分配从 0.99 GB 开始，但跃升至 1.96 GB，大致翻了一番。\n\n我们需要把这个价值放在上下文中。让我们首先测量一个简单的`for`循环能做什么，并将结果与用`transform`实现的相同算法进行比较。\n\n# 测量简单 for 循环的内存\n\n带有`for`循环的解决方案非常简单:\n\n```cpp\nTEST_CASE(\"Memory\"){\n    auto size = size_1GB_64Bits;\n    vector<long long> manyNumbers(size);\n    fill_n(manyNumbers.begin(), size, 1000L);\n\n    for(auto iter = manyNumbers.begin(); iter != manyNumbers.end(); \n        ++ iter){\n            ++(*iter);\n    };\n\n    CHECK_EQ(manyNumbers[0], 1001);\n}\n```\n\n测量内存时，没有什么好惊讶的——整个过程中占用空间保持在 0.99 GB。我们也可以用`transform`达到这个结果吗？嗯，`transform`有一个版本可以就地修改集合。让我们来测试一下。\n\n# 测量就地转换的内存\n\n要使用`transform`，我们需要提供目标迭代器参数`source.begin()`，如下所示:\n\n```cpp\nauto increment = [](const auto value){\n    return value + 1;\n};\n\nauto transformAllInPlace = [](auto& source, auto lambda){\n    transform(source.begin(), source.end(), source.begin(), lambda);\n};\n\nTEST_CASE(\"Memory\"){\n    auto size = size_1GB_64Bits;\n    vector<long long> manyNumbers(size);\n    fill_n(manyNumbers.begin(), size, 1000L);\n\n    transformAllInPlace(manyNumbers, increment);\n\n    CHECK_EQ(manyNumbers[0], 1001);\n}\n```\n\n根据文档，这应该在同一个集合中改变；因此，它不应该分配更多的内存。不出所料，它具有与简单的`for`循环相同的行为，并且在整个程序期间内存占用保持在 0.99 GB。\n\n但是，您可能会注意到，我们现在不返回值以避免复制。不过，我喜欢从转换到返回值，我们还有另一个选择，使用移动语义:\n\n```cpp\ntemplate<typename SourceType>\nauto transformAllInPlace = [](auto& source, auto lambda) -> SourceType&& {\n    transform(source.begin(), source.end(), source.begin(), lambda);\n    return move(source);\n};\n```\n\n为了进行调用编译，我们需要在调用`transformAllInPlace`时传入源的类型，因此我们的测试更改为:\n\n```cpp\nTEST_CASE(\"Memory\"){\n    auto size = size_1GB_64Bits;\n    vector<long long> manyNumbers(size);\n    fill_n(manyNumbers.begin(), size, 1000L);\n\n    auto result = transformAllInPlace<vector<long long>>(manyNumbers, \n        increment);\n\n    CHECK_EQ(result[0], 1001);\n}\n```\n\n让我们衡量一下移动语义是否有任何帮助。结果果然不出所料；在整个运行期间，内存占用量保持在 0.99 GB。\n\n这引出了一个有趣的想法。如果我们在对`transform`的调用中使用移动语义呢？\n\n# 用移动迭代器转换\n\n我们可以重写我们的`transform`函数来使用移动迭代器，如下所示:\n\n```cpp\ntemplate<typename DestinationType>\nauto transformAllWithMoveIterator = [](auto& source, auto lambda){\n    DestinationType result(source.size());\n    transform(make_move_iterator(source.begin()), \n        make_move_iterator(source.end()), result.begin(), lambda);\n    source.clear();\n    return result;\n};\n```\n\n理论上，这应该做的是将值移动到目的地，而不是复制它们，从而保持低内存占用。为了进行测试，我们在记录内存的同时运行相同的测试:\n\n```cpp\nTEST_CASE(\"Memory\"){\n    auto size = size_1GB_64Bits;\n    vector<long long> manyNumbers(size);\n    fill_n(manyNumbers.begin(), size, 1000L);\n\n    auto result = transformAllWithMoveIterator<vector<long long>>\n        (manyNumbers, increment);\n\n    CHECK_EQ(result[0], 1001);\n}\n```\n\n结果出乎意料；内存从 0.99 GB 开始，上升到 1.96 GB(可能是在`transform`调用之后)，然后又回到 0.99 GB(很可能是`source.clear()`的结果)。我尝试了多种变体来避免这种行为，但找不到将内存占用保持在 0.99 GB 的解决方案。这似乎是移动迭代器实现的一个问题；我建议你在你的编译器上测试一下，看看它是否有效。\n\n# 比较解决方案\n\n使用就地或移动语义的解决方案在减少内存占用的同时，仅在额外计算不需要源数据时才起作用。如果您计划在其他计算中重用数据，就没有办法保留初始集合。此外，不清楚这些调用是否可以并行运行；由于 g++ 还没有实现并行执行策略，所以我无法测试它们，所以我将这个问题留给读者作为练习。\n\n但是函数式编程语言为了减少内存占用做了什么呢？答案很有意思。\n\n# 不可变的数据结构\n\n纯函数式编程语言使用不可变数据结构和垃圾收集的组合。每次修改数据结构的调用都会创建一个初始数据结构的副本，只改变一个元素。初始结构不受任何影响。但是，这是使用指针完成的；基本上，新的数据结构与初始数据结构相同，只是有一个指向已更改值的指针。丢弃初始集合时，旧值不再使用，垃圾收集器会自动将其从内存中移除。\n\n这种机制充分利用了不变性，允许 C++ 无法实现的优化。此外，实现通常是递归的，这也利用了尾部递归优化。\n\n然而，在 C++ 中实现这样的数据结构是可能的。一个例子是名为**音麦**的图书馆，你可以在[https://github.com/arximboldi/immer](https://github.com/arximboldi/immer)的 GitHub 上找到。Immer 实现了许多不可变的集合。我们来看看`immer::vector`；每次我们调用一个通常会修改向量的操作(比如`push_back` ), `immer::vector`都会返回一个新的集合。返回的每个值都可以是常量，因为它从不改变。我在章节代码中用 imme 0 . 5 . 0 写了一个小测试，展示了`immer::vector`的用法，可以在下面的代码中看到:\n\n```cpp\nTEST_CASE(\"Check immutable vector\"){\n    const auto empty = immer::vector<int>{};\n    const auto withOneElement = empty.push_back(42);\n\n    CHECK_EQ(0, empty.size());\n    CHECK_EQ(1, withOneElement.size());\n    CHECK_EQ(42, withOneElement[0]);\n}\n```\n\n关于不可变的数据结构，我不再赘述；不过，我强烈建议你看看*音麦*网站([https://sinusoid.es/immer/introduction.html](https://sinusoid.es/immer/introduction.html))上的文档，玩玩图书馆。\n\n# 摘要\n\n我们已经看到性能优化是一个复杂的话题。作为 C++ 程序员，我们已经准备好要求代码有更高的性能；我们在这一章中问的问题是:有没有可能优化以函数风格编写的代码？\n\n答案是——是的，如果你衡量，如果你有一个明确的目标。我们需要一个特定的计算来更快地完成吗？我们需要减少内存占用吗？应用的哪个领域最需要性能改进？我们想要做多少怪异的点优化，可能需要用下一个编译器、库或平台版本重写？在继续优化代码之前，这些都是您需要回答的问题。\n\n然而，我们已经看到，在使用计算机上的所有内核时，函数式编程有着巨大的好处。当我们等待高阶函数并行执行的标准实现时，我们可以通过编写自己的并行算法来利用不变性。递归是函数式编程的另一个主要部分，无论何时使用，我们都可以利用尾部递归优化。\n\n至于内存消耗，在第三方库中实现的不可变数据结构，以及根据目标仔细优化我们正在使用的高阶函数，可以帮助我们保持代码的简单性，而复杂性发生在代码的特定位置。当我们丢弃源集合时，可以使用移动语义，但是要记得检查它是否与并行执行一起工作。\n\n最重要的是，我希望您已经了解了测量是性能优化最重要的部分。毕竟，如果你不知道你在哪里，你需要去哪里，你怎么能去旅行？\n\n我们将利用数据生成器进行测试，继续我们的函数式编程之旅。是时候看看基于属性的测试了。"
  },
  {
    "path": "docs/handson-func-prog-cpp/11.md",
    "content": "# 十一、基于属性的测试\n\n我们已经看到，纯函数有一个重要的特性——对于相同的输入，它们返回相同的输出。我们还看到，这个属性允许我们轻松地为纯函数编写基于示例的单元测试。此外，我们可以编写数据驱动的测试，允许一个测试函数被多个输入和输出重用。\n\n事实证明，我们可以做得更好。我们可以利用纯函数的数学特性，而不是或者除此之外，编写许多行数据驱动的测试。这种技术是可能的，因为数据发生器是由函数编程实现的。这些测试被混乱地命名为**基于属性的测试**；你必须记住，这个名字来自纯函数的数学属性，而不是在类或对象中实现的属性。\n\n本章将涵盖以下主题:\n\n*   理解基于属性的测试的思想\n*   如何编写生成器并加以利用\n*   如何从基于示例的测试获得基于属性的测试\n*   如何写出好的属性\n\n# 技术要求\n\n您将需要一个支持 C++ 17 的编译器。我用的是 GCC 7.4.0。\n\n代码可以在[的 GitHub 上找到。com/ PacktPublishing/动手-函数-用- Cpp 编程`Chapter11`文件夹中的](https://github.%E2%80%8Bcom/PacktPublishing/Hands-On-Functional-Programming-with-Cpp)。它包括并使用`doctest`，这是一个单头开源单元测试库。你可以在它的 GitHub 资源库[上找到它。](https://github.%E2%80%8Bcom/onqtam/doctest)\n\n# 基于属性的测试\n\n单元测试是一种非常有用的软件开发技术。一套好的单元测试可以做到以下几点:\n\n*   通过自动化回归测试的枯燥部分来加快部署。\n*   使专业测试人员能够发现隐藏的问题，而不是一次又一次地运行相同的测试计划。\n*   在开发过程中尽早移除 bug，从而降低发现和修复 bug 的成本。\n*   通过作为代码结构的第一个客户提供反馈来改进软件设计(如果测试很复杂，很可能您的设计也很复杂)，只要开发人员知道如何查看和解释反馈。\n*   增加对代码的信任，从而允许更多的更改，从而促进重构，加快开发速度或消除代码中的风险。\n\n我喜欢写单元测试。我喜欢弄清楚有趣的测试用例，我喜欢用测试来驱动我的代码——正如你在[第 9 章](09.html)、*功能编程的测试驱动开发*中看到的那样。与此同时，我一直在寻找更好的方法来编写测试，因为如果我们能加快这个过程就太好了。\n\n我们已经在[第 9 章](09.html)、*功能编程的测试驱动开发*中看到，纯函数允许我们更容易地识别测试用例，因为根据定义，它们的输出是受约束的。事实证明，如果我们冒险进入与这些纯函数相关的数学性质领域，我们可以走得更远。\n\n如果你已经写单元测试有一段时间了，你可能会觉得有些测试有点多余。如果我们能编写这样的测试就好了——对于某个值区间内的输入，期望的输出必须具有某个属性。事实证明，在数据生成器和一些抽象思维的帮助下，我们可以做到这一点。\n\n让我们比较一下方法。\n\n# 基于示例的测试与基于属性的测试\n\n让我们举一个`power`函数的例子:\n\n```cpp\nfunction<int(int, int)> power = [](auto first, auto second){\n    return pow(first, second);\n};\n```\n\n您将如何使用基于示例的测试来测试它？我们需要为第一个和第二个找出一些有趣的值，并将它们组合起来。为了这个练习的目的，我们将把自己限制在正整数上。一般来说，整数的有趣值是— `0`、`1`、多和最大。这导致以下可能的情况:\n\n*   *0 <sup>0</sup> - >未定义*(* c++ 中的`pow`实现返回`1`，除非启用了特定错误)\n*   *0<sup>0 到最大值之间的任意整数</sup> - > 0*\n*   *1 <sup>任意整数</sup> - > 1*\n*   *(除 0 以外的任意整数) <sup>0</sup> - > 1*\n*   *2 <sup>2</sup> - > 4*\n*   *2 <sup>不溢出</sup> - >待计算值的最大整数*\n*   *10 <sup>5</sup> - > 100000*\n*   *10 <sup>不溢出的最大整数</sup> - >待计算值*\n\n这个列表并不完整，但它展示了对问题的有趣分析。所以，让我们写这些测试:\n\n```cpp\nTEST_CASE(\"Power\"){\n    int maxInt = numeric_limits<int>::max();\n    CHECK_EQ(1, power(0, 0));\n    CHECK_EQ(0, power(0, 1));\n    CHECK_EQ(0, power(0, maxInt));\n    CHECK_EQ(1, power(1, 1));\n    CHECK_EQ(1, power(1, 2));\n    CHECK_EQ(1, power(1, maxInt));\n    CHECK_EQ(1, power(2, 0));\n    CHECK_EQ(2, power(2, 1));\n    CHECK_EQ(4, power(2, 2));\n    CHECK_EQ(maxInt, power(2, 31) - 1);\n    CHECK_EQ(1, power(3, 0));\n    CHECK_EQ(3, power(3, 1));\n    CHECK_EQ(9, power(3, 2));\n    CHECK_EQ(1, power(maxInt, 0));\n    CHECK_EQ(maxInt, power(maxInt, 1));\n}\n```\n\n这显然不是我们需要检查以确定幂函数起作用的完整测试列表，但这是一个良好的开端。看着这个列表，我在想，你认为——你会写更多还是更少的测试？我肯定想写更多，但我在这个过程中失去了动力。当然，问题之一是我在代码之后编写了这些测试；我更有动力将它们和代码一起编写，就像在**测试驱动开发** ( **TDD** )中一样。但也许有更好的方法？\n\n让我们换个角度思考一下。我们是否可以测试某些或所有预期输出的属性？让我们写一个清单:\n\n*   *0 <sup>0</sup> - >未定义(C++ 中幂函数默认为 1)*\n*   *0<sup>【1..</sup> - > 0*\n*   *值:[1.. <sup>0</sup> - > 1*\n*   *值:[0.. <sup>1</sup> - >值*\n\n这些都是一些明显的性质。然而，它们只覆盖了一小部分值。我们还是需要涵盖*x*T5<sup>y</sup>的一般情况，其中 *x* 和 *y* 既不是`0`也不是`1`。我们能在这里找到任何财产吗？好吧，想想整数幂的数学定义——它是一个重复的乘法。因此，对于任何大于`1`的 *x* 和 *y* 值，我们可以推断如下:\n\n![](img/785b00f6-7df4-4d02-94ee-1e520377ed0c.png)\n\n我们这里确实有一个边界问题，因为计算可能会溢出。所以需要选取 *x* 和 *y* 的值，使得 *x <sup>y</sup>* 小于`maxInt`。处理这个问题的一种方法是先挑 *x* ，在 *y=2* 和`maxy=floor(log<sub>x</sub>maxInt)`之间挑 *y* 。为了使它尽可能接近边界，我们应该总是选择`maxy`作为一个值。为了检查溢出情况，我们只需要测试 *x* 到`maxy + 1`的幂溢出。\n\n当然，前面的方法意味着我们信任标准库中对数函数的结果。如果你的*测试偏执狂*比我的大，我建议对从`2`到`maxInt`的所有基数和数值`maxInt`使用经过验证的对数表。然而，我将使用 STL 对数函数。\n\n我们现在有了幂函数的数学性质的列表。但是我们希望像前面看到的那样，有间隔地实现它们。我们能做到吗？输入数据生成器。\n\n# 发电机\n\n生成器是函数式编程语言的主要特征。它们通常通过 lambdas 和 lazy 求值的组合来实现，允许如下代码:\n\n```cpp\n// pseudocode\nvector<int> values = generate(1, maxInt, [](){/*generatorCode*/}).pick(100)\n```\n\n生成器函数通常生成无限数量的值，但是因为它是惰性求值的，所以`100`值只有在调用`pick`时才会具体化。\n\nC++ 还没有对延迟求值和数据生成器的标准支持，所以我们必须实现自己的生成器。值得注意的是，C++ 20 已经在标准中采用了令人敬畏的范围库，它支持这两个特性。为了本章的目标，我们将坚持目前可用的标准，但是在本书的最后几章中，您将会发现 ranges library 的基本用法。\n\n首先，我们如何生成数据？STL 通过使用`uniform_int_distribution`类为我们提供了一个生成均匀分布的随机整数的好方法。我们先来看看代码；我添加了注释来解释发生了什么:\n\n```cpp\nauto generate_ints = [](const int min, const int max){\n    random_device rd; // use for generating the seed\n    mt19937 generator(rd()); // used for generating pseudo-random \n        numbers\n    uniform_int_distribution<int> distribution(min, max); // used to \n        generate uniformly distributed numbers between min and max\n    auto values = transformAll<vector<int>>(range(0, 98), // generates \n        the range [0..98]\n            [&distribution, &generator](auto){\n                return distribution(generator); // generate the random \n                    numbers\n            });\n    values.push_back(min); // ensure that min and max values are \n        included\n    values.push_back(max);\n    return values;\n};\n```\n\n该功能将生成从`min`到`max`的均匀分布的数字。我更喜欢总是包括区间的边缘，因为这些对于测试来说总是有趣的值。\n\n我们还使用了一个名为`range`的函数，你还没有看到。它的目标是用从`minValue`到`maxValue`的值填充向量，以允许更简单的转换。这是:\n\n```cpp\nauto range = [](const int minValue, const int maxValue){\n    vector<int> range(maxValue - minValue + 1);\n    iota(range.begin(), range.end(), minValue);\n    return range;\n};\n```\n\n值得注意的是，在函数式编程语言中，范围通常是延迟计算的，这大大减少了它们的内存占用。尽管对于我们例子的目标来说，这很好。\n\n前面的`generator`函数允许我们为测试创建输入数据，均匀分布在 1 和最大整数值之间。只需要一个简单的绑定:\n\n```cpp\nauto generate_ints_greater_than_1 = bind(generate_ints, 1, numeric_limits<int>::max());\n```\n\n让我们将它用于基于属性的测试。\n\n# 对属性进行测试\n\n让我们再次查看要检查的属性列表:\n\n*   *0 <sup>0</sup> - >未定义(C++ 中幂函数默认为 1)*\n*   *0<sup>【1..</sup> - > 0*\n*   *值:[1.. <sup>0</sup> - > 1*\n*   *值:[0.. <sup>1</sup> - >值*\n*   *x<sup>y</sup>= x<sup>y-1</sup>* x*\n\n我们现在将依次实现每个属性。对于每一个属性，我们将使用普通的基于示例的测试，或者由`generate_ints_greater_than_1`函数启发的数据生成器。让我们从最简单的属性开始——*0<sup>0</sup>*应该是未定义的——或者实际上是其标准实现中的`1`。\n\n# 属性:00 ->未定义\n\n第一个是非常简单的实现，使用一个普通的基于示例的测试。为了保持一致，我们将在函数中提取它:\n\n```cpp\nauto property_0_to_power_0_is_1 = [](){\n    return power(0, 0) == 1;\n};\n```\n\n在我们的测试中，我们还将编写属性的描述，以便获得信息输出:\n\n```cpp\nTEST_CASE(\"Properties\"){\n    cout << \"Property: 0 to power 0 is 1\" << endl;\n    CHECK(property_0_to_power_0_is_1);\n }\n```\n\n运行时，这将导致以下输出通过测试:\n\n```cpp\ng++ -std=c++ 17 propertyBasedTests.cpp -o out/propertyBasedTests\n./out/propertyBasedTests\n[doctest] doctest version is \"2.0.1\"\n[doctest] run with \"--help\" for options\nProperty: 0 to power 0 is 1\n===============================================================================\n[doctest] test cases:      1 |      1 passed |      0 failed |      0 skipped\n[doctest] assertions:      1 |      1 passed |      0 failed |\n[doctest] Status: SUCCESS!\n```\n\n这已经够简单的了！我们现在有了基于属性的测试的基本结构。下一个测试需要一个数据生成器，但是我们已经有了。让我们看看除了`0`等于`0`之外，它对任何力量的`0`属性如何起作用。\n\n# 属性:0[1..maxInt] -> 0\n\n我们需要从`1`到`maxInt`的数字生成器，我们已经实现了。然后我们需要一个属性函数来检查从`1`到`maxInt`的任何指数，`0`上升到指数等于`0`。代码很容易写:\n\n```cpp\nauto prop_0_to_any_nonzero_int_is_0= [](const int exponent){\n    CHECK(exponent > 0); // checking the contract just to be sure\n    return power(0, exponent) == 0;\n};\n```\n\n接下来，我们需要检查这个属性。由于我们有一个生成值的列表，我们可以使用`all_of`函数根据属性检查所有的值。为了提供更多信息，我决定显示我们正在使用的值列表:\n\n```cpp\nauto printGeneratedValues = [](const string& generatorName, const auto& \n    values){\n        cout << \"Check generator \" << generatorName << endl;\n        for_each(values.begin(), values.end(), [](auto value) { cout << \n            value << \", \";});\n        cout << endl;\n };\n\nauto check_property = [](const auto& generator, const auto& property, const string& generatorName){\n    auto values = generator();\n    printGeneratedValues(generatorName, values);\n    CHECK(all_of_collection(values, property));\n};\n```\n\n最后，我们可以写我们的测试。我们再次在测试前显示属性名:\n\n```cpp\nTEST_CASE(\"Properties\"){\n    cout << \"Property: 0 to power 0 is 1\" << endl;\n    CHECK(property_0_to_power_0_is_1);\n\n    cout << \"Property: 0 to [1..maxInt] is 0\" << endl;\n    check_property(generate_ints_greater_than_1,  \n        prop_0_to_any_nonzero_int_is_0, \"generate ints\");\n}\n```\n\n运行测试会产生以下输出:\n\n```cpp\nProperty: 0 to power 0 is 1\nProperty: 0 to [1..maxInt] is 0\nCheck generator generate ints\n1073496375, 263661517, 1090774655, 590994005, 168796979, 1988143371, 1411998804, 1276384966, 252406124, 111200955, 775255151, 1669887756, 1426286501, 1264685577, 1409478643, 944131269, 1688339800, 192256171, 1406363728, 1624573054, 2654328, 1025851283, 1113062216, 1099035394, 624703362, 1523770105, 1243308926, 104279226, 1330992269, 1964576789, 789398651, 453897783, 1041935696, 561917028, 1379973023, 643316376, 1983422999, 1559294692, 2097139875, 384327588, 867142643, 1394240860, 2137873266, 2103542389, 1385608621, 2058924659, 1092474161, 1071910908, 1041001035, 582615293, 1911217125, 1383545491, 410712068, 1161330888, 1939114509, 1395243657, 427165959, 28574042, 1391025789, 224683120, 1222884936, 523039771, 1539230457, 2114587312, 2069325876, 166181790, 1504124934, 1817094271, 328329837, 442231460, 2123558414, 411757963, 1883062671, 1529993763, 1645210705, 866071861, 305821973, 1015936684, 2081548159, 1216448456, 2032167679, 351064479, 1818390045, 858994762, 2073835547, 755252854, 2010595753, 1882881401, 741339006, 1080861523, 1845108795, 362033992, 680848942, 728181713, 1252227588, 125901168, 1212171311, 2110298117, 946911655, 1, 2147483647, \n===============================================================================\n[doctest] test cases:      1 |      1 passed |      0 failed |      0 skipped\n[doctest] assertions:    103 |    103 passed |      0 failed |\n[doctest] Status: SUCCESS!\n```\n\n可以看到，测试使用了一堆随机值，最后两个值是`1`和`maxInt`。\n\n是时候停下来反思一下了。这些测试不寻常。单元测试的关键思想之一是拥有可重复的测试，但是在这里，我们有一堆随机的值。这些算吗？当一种价值观导致失败时，我们该怎么办？\n\n这些都是很棒的问题！首先，使用基于属性的测试并不排除基于示例的测试。事实上，我们现在正在混合这两者——*0<sup>0</sup>*是一个例子而不是一个属性。所以，当有意义的时候，不要犹豫检查任何特定的值。\n\n其次，支持基于属性的测试的库允许收集特定的失败值，并自动对这些失败值进行重新测试。这很简单——每当出现故障时，将值保存在某个地方，并在测试运行时将它们包含在下一代中。这不仅可以让您更彻底地测试，还可以发现代码的行为。\n\n因此，我们必须将基于示例的测试和基于属性的测试视为互补技术。第一个帮助你使用**测试驱动开发** ( **TDD** )驱动代码，查看有趣的案例。第二个可以让你找到你没有考虑过的案例，重新测试同样的错误。两者都有用，只是方式不同。\n\n那么让我们继续写我们的属性。下一个大约是`0`等于`1`的幂的任意数。\n\n# 属性:值:[1..最大]0 -> 1\n\n我们已经准备好了一切，我们只需要写下来:\n\n```cpp\nauto prop_anyIntToPower0Is1 = [](const int base){\n    CHECK(base > 0);\n    return power(base, 0) == 1;\n};\n```\n\n测试结果如下:\n\n```cpp\nTEST_CASE(\"Properties\"){\n    cout << \"Property: 0 to power 0 is 1\" << endl;\n    CHECK(property_0_to_power_0_is_1);\n\n    cout << \"Property: 0 to [1..maxInt] is 0\" << endl;\n    check_property(generate_ints_greater_than_1, \n        prop_0_to_any_nonzero_int_is_0, \"generate ints\");\n\n    cout << \"Property: any int to power 0 is 1\" << endl;\n    check_property(generate_ints_greater_than_1, \n        prop_anyIntToPower0Is1, \"generate ints\");\n}\n```\n\n运行测试会产生以下输出(为简洁起见，省略了几行):\n\n```cpp\nProperty: 0 to power 0 is 1\nCheck generator generate ints\n1673741664, 1132665648, 342304077, 936735303, 917238554, 1081591838, 743969276, 1981329112, 127389617, \n...\n 1, 2147483647, \nProperty: any int to power 0 is 1\nCheck generator generate ints\n736268029, 1304281720, 416541658, 2060514167, 1695305196, 1479818034, 699224013, 1309218505, 302388654, 765083344, 430385474, 648548788, 1986457895, 794974983, 1797109305, 1131764785, 1221836230, 802640954,\n...\n1543181200, 1, 2147483647, \n===============================================================================\n[doctest] test cases:      1 |      1 passed |      0 failed |      0 skipped\n[doctest] assertions:    205 |    205 passed |      0 failed |\n[doctest] Status: SUCCESS!\n```\n\n从前面的例子可以看出，这些数字确实是随机的，同时总是包括`1`和`maxInt`。\n\n我们掌握了窍门！下一个性质是`1`的幂的任何值都是值。\n\n# 属性:值:[0..最大值]1 ->值\n\n我们需要另一个生成器方法，从`0`开始。我们只需要再次使用绑定魔法来获得所需的结果:\n\n```cpp\nauto generate_ints_greater_than_0 = bind(generate_ints, 0, numeric_limits<int>::max());\n```\n\n这个属性很容易写:\n\n```cpp\nauto prop_any_int_to_power_1_is_the_value = [](const int base){\n    return power(base, 1) == base;\n};\n```\n\n考验显而易见:\n\n```cpp\nTEST_CASE(\"Properties\"){\n    cout << \"Property: 0 to power 0 is 1\" << endl;\n    CHECK(property_0_to_power_0_is_1);\n\n    cout << \"Property: 0 to any non-zero power is 0\" << endl;\n    check_property(generate_ints_greater_than_1, \n        prop_0_to_any_nonzero_int_is_0, \"generate ints\");\n\n    cout << \"Property: any int to power 0 is 1\" << endl;\n    check_property(generate_ints_greater_than_1, \n        prop_anyIntToPower0Is1, \"generate ints\");\n\n    cout << \"Property: any int to power 1 is the value\" << endl;\n    check_property(generate_ints_greater_than_0, \n        prop_any_int_to_power_1_is_the_value, \"generate ints\");\n}\n```\n\n运行测试再次导致通过。\n\n让我们花点时间再思考一下:\n\n*   我们检查多少值？答案是`301`。\n*   有多少行测试代码？测试代码只有 23 行代码，而我们在测试中重用的*库*函数大约有 40 行代码。\n\n这不是很神奇吗？这难道不是你测试的一项有价值的投资吗？\n\n我们知道怎么做。现在是我们练习中最复杂的性质的时候了——任何升到幂的数 *y* 等于升到幂的数 *y-1* 乘以这个数。\n\n# 属性:xy = xy-1 * x\n\n这将需要我们生成两组值， *x* 和 *y* ，这样*x<sup>y</sup>T17】最大*。我花了一些时间摆弄数据发生器，但我发现任何大于![](img/26c14df0-ec8d-4ebe-96a3-b44950f3df7c.png)的 *x* 只能测试 *y=1* 。因此，我将使用两台发电机；第一个将生成介于`2`和![](img/11ab32f9-a191-4287-803b-b8c1f5457f18.png)之间的数字，而第二个将生成大于![](img/5406f9ba-b514-4cb4-9a41-d1d3813e3745.png)且小于`maxInt`的数字:\n\n```cpp\nauto generate_ints_greater_than_2_less_sqrt_maxInt = bind(generate_ints, 2, sqrt(numeric_limits<int>::max()));\n```\n\n属性的第一部分如下:\n\n```cpp\ncout << \"Property: next power of x is previous power of x multiplied by  \n    x\" << endl;\ncheck_property(generate_ints_greater_than_2_less_sqrt_maxInt, \n    prop_nextPowerOfXIsPreviousPowerOfXMultipliedByX, \"generate greater \n        than 2 and less than sqrt of maxInt\");\n```\n\n为了实现这个属性，我们还需要为`x`基数生成指数，这样我们就可以把这个属性写成如下:\n\n```cpp\nauto prop_nextPowerOfXIsPreviousPowerOfXMultipliedByX = [](const int x){\n    auto exponents = bind(generate_exponent_less_than_log_maxInt, x);\n    return check_property(exponents, [x](auto y){ return power(x, y) ==  \n      power(x, y - 1) * x;}, \"generate exponents for \" + to_string(x));\n};\n```\n\n从生成器函数的名称可以看出，我们需要生成`1`和 *log <sub>x</sub> maxInt* 之间的数字。计算 x <sup>y</sup> 时，任何高于该值的数字都会溢出。由于 STL 中没有通用的对数函数，我们需要实现一个。要计算 *log <sub>x</sub> maxInt* ，我们只需要使用一个数学等式:\n\n```cpp\nauto logMaxIntBaseX = [](const int x) -> int{\n    auto maxInt = numeric_limits<int>::max() ;\n    return floor(log(maxInt) / log(x));\n};\n```\n\n我们的生成器函数如下:\n\n```cpp\nauto generate_exponent_less_than_log_maxInt = [](const int x){\n    return generate_ints(1, logMaxIntBaseX(x));\n};\n```\n\n有了这些，我们就可以进行测试了。以下是输出的简短部分:\n\n```cpp\nCheck generator generate exponents for 43740\n1, 2, \nCheck generator generate exponents for 9320\n1, 2, \nCheck generator generate exponents for 2\n1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, \nCheck generator generate exponents for 46340\n1, 2,\n```\n\n测试的最后一部分是将![](img/11ab32f9-a191-4287-803b-b8c1f5457f18.png) + 1 到`maxInt`的间隔相加:\n\n```cpp\ncheck_property(generate_ints_greater_than_sqrt_maxInt,  \n    prop_nextPowerOfXIsPreviousPowerOfXMultipliedByX, \"generate greater    \n    than sqrt of maxInt\");\n```\n\n这也导致生成函数的更新，以支持一些边缘情况；有关以下代码中的说明，请参考注释:\n\n```cpp\nauto generate_ints = [](const int min, const int max){\n    if(min > max) { // when lower range is larger than upper range, \n        just return empty vector\n            return vector<int>();\n    }\n    if(min == max){ // if min and max are equal, just return {min}\n        return range(min, min);\n    }\n\n    if(max - min <= 100){ // if there not enough int values in the \n        range, just return it fully\n            return range(min, max);\n    }\n    ...\n}\n```\n\n就这样，我们实现了我们的最终财产！\n\n# 结论\n\n我们现在只需几行代码就可以检查以下所有内容:\n\n*   *0 <sup>0</sup> - >未定义(C++ 中幂函数默认为 1)*\n*   *0<sup>【1..</sup> - > 0*\n*   *值:[1.. <sup>0</sup> - > 1*\n*   *值:[0.. <sup>1</sup> - >值*\n*   *x<sup>y</sup>= x<sup>y-1</sup>* x*\n\n这与更常用的基于示例的测试方法相比如何？我们用更少的代码进行更多的测试。我们可以在代码中发现隐藏的问题。但是属性比示例更难识别。我们还发现，基于属性的测试与基于示例的测试配合得非常好。\n\n所以，现在让我们解决寻找属性的问题。这需要一些分析，我们将探索一种实用的方法，您可以通过数据驱动测试从示例中演化属性。\n\n# 从示例到数据驱动测试再到属性\n\n当我第一次听说基于属性的测试时，我有两个问题。首先，我认为它们是为了取代示例测试——我们现在知道它们不是；只是并排使用这两种技术。第二，我不知道如何想出好的属性。\n\n然而，我有一个好主意，如何提供好的例子，如何消除测试之间的重复。我们已经看到了一个关于如何给出幂函数的好例子的例子；让我们回顾一下:\n\n*   *0 <sup>0</sup> - >未定义(*除非启用特定错误，否则 C++ 中的 pow 实现返回 1)*\n*   *0<sup>0 到最大值之间的任意整数</sup> - > 0*\n*   *1 <sup>任意整数</sup> - > 1*\n*   *(除 0 以外的任意整数) <sup>0</sup> - > 1*\n*   *2 <sup>2</sup> - > 4*\n*   *2 <sup>不溢出的最大 int</sup>->待计算值*\n*   *10 <sup>5</sup> - > 100000*\n*   *10 <sup>不溢出的最大 int</sup>->待计算值*\n\n我们还看到，为这些情况编写基于示例的测试非常容易:\n\n```cpp\nTEST_CASE(\"Power\"){\n    int maxInt = numeric_limits<int>::max();\n    CHECK_EQ(1, power(0, 0));\n    CHECK_EQ(0, power(0, 1));\n    CHECK_EQ(0, power(0, maxInt));\n    CHECK_EQ(1, power(1, 1));\n    CHECK_EQ(1, power(1, 2));\n    CHECK_EQ(1, power(1, maxInt));\n    CHECK_EQ(1, power(2, 0));\n    CHECK_EQ(2, power(2, 1));\n    CHECK_EQ(4, power(2, 2));\n    CHECK_EQ(maxInt, power(2, 31) - 1);\n    CHECK_EQ(1, power(3, 0));\n    CHECK_EQ(3, power(3, 1));\n    CHECK_EQ(9, power(3, 2));\n    CHECK_EQ(1, power(maxInt, 0));\n    CHECK_EQ(maxInt, power(maxInt, 1));\n}\n```\n\n这些例子展示了代码的相似性。`0`、`1`、`2`和`3`碱基重复多次。我们在[第 9 章](09.html)、*功能编程的测试驱动开发*中看到，我们可以通过指定多个输入值来消除数据驱动测试的这种相似性:\n\n```cpp\nTEST_CASE(\"1 raised to a power is 1\"){\n    int exponent;\n\n    SUBCASE(\"0\"){\n        exponent = 0;\n    }\n    SUBCASE(\"1\"){\n        exponent = 1;\n    }\n    SUBCASE(\"2\"){\n        exponent = 1;\n    }\n    SUBCASE(\"maxInt\"){\n        exponent = maxInt;\n    }\n\n    CAPTURE(exponent);\n    CHECK_EQ(1, power(1, exponent));\n}\n```\n\n在我努力消除这些相似性一段时间后，我开始看到属性。很明显，在这种情况下，我们可以添加一个测试，通过使用随机输入而不是特定的例子来检查相同的数学属性。事实上，我们在上一节中写了它，它看起来是这样的:\n\n```cpp\ncout << \"Property: any int to power 1 is the value\" << endl;\ncheck_property(generate_ints_greater_than_0, \n    prop_any_int_to_power_1_is_the_value, \"generate ints\");\n```\n\n所以我的建议是——如果你对这个问题反思几分钟，找到要检查的数学属性，那就太好了！(编写基于属性的测试，并添加尽可能多的基于示例的测试，以确信您已经涵盖了这些情况。)看不到的话，不用担心；继续添加基于示例的测试，通过使用数据驱动的测试来消除测试之间的重复，最终您将揭示属性。然后，添加基于属性的测试，并决定如何处理现有的基于示例的测试。\n\n# 好的属性，坏的属性\n\n因为属性是比示例更高的抽象层次，所以很容易以混乱或不清楚的方式实现它们。您已经需要非常注意基于示例的测试；现在，您需要加强与基于属性的测试相关的工作。\n\n首先，好的属性就像好的单元测试。因此，我们希望拥有如下特性:\n\n*   小的\n*   恰当而明确地命名\n*   当他们失败时给出一个非常明确的信息\n*   快的\n*   可重复的\n\n但是基于属性的测试有一个警告——既然我们使用的是随机值，难道我们不应该期待随机失败吗？当基于属性的测试失败时，我们会学到一些关于代码的新东西，所以这是值得庆祝的。然而，我们应该期望到 2010 年久而久之的失败会更少，我们应该消除我们的缺陷。如果您的基于属性的测试每天都失败，那么肯定有问题——可能是属性太大，或者实现有很多漏洞。如果您的基于属性的测试不时失败，并且它们在代码中显示出一个可能的错误——这很好。\n\n基于属性的测试的难点之一是保持生成器和属性检查没有错误。这也是代码，任何代码都可能有 bug。在基于示例的测试中，我们通过将单元测试简化到几乎不可能出错的程度来处理这个问题。请注意，属性更复杂，因此可能需要更多的关注。旧的原则*保持简单，愚蠢的*在基于属性的测试中更有价值。因此，比起更大的属性，更喜欢更小的属性，进行您的分析，并与同事一起检查您的代码——包括名称和实现。\n\n# 关于执行的一些话\n\n在本章中，我们使用了一组自定义函数来实现数据生成器，以保持代码标准 C++ 17。然而，这些功能是为学习该技术而优化的，还没有做好生产准备。您可能已经看到，它们没有针对内存占用或性能进行优化。我们已经可以通过巧妙使用迭代器让它们变得更好，但是还有更好的方法。\n\n如果您可以使用范围库或者使用 C++ 20 编译您的测试，那么实现无限数据生成器是非常容易的(由于延迟求值)。我还建议您研究基于属性的测试库，或者生成器库，因为有些生成器已经被其他人编写过了，一旦您理解了这个概念，在代码中使用它们会快得多。\n\n# 摘要\n\n基于属性的测试是对我们已经知道并使用多年的基于示例的测试的一个受欢迎的补充。它们向我们展示了如何将数据生成和一些分析结合起来，以消除测试中的重复，并找到我们没有考虑到的案例。\n\n基于属性的测试是由数据生成器实现的，使用纯函数非常容易实现。随着 C++ 20 或范围库的出现，延迟求值将变得更加容易。\n\n但是基于属性的测试的核心技术是识别属性。我们已经看到了两种方法——第一种方法是分析示例，第二种方法是编写基于示例的测试，消除重复以将其转换为数据驱动的测试，然后用属性替换数据行。\n\n最后，请记住，基于属性的测试是代码，它们需要非常干净、易于更改和易于理解。尽可能偏爱小属性，并通过明确命名使它们非常容易理解。\n\n在下一章中，我们将研究如何使用纯函数来支持我们的重构工作，以及如何将设计模式实现为函数。"
  },
  {
    "path": "docs/handson-func-prog-cpp/12.md",
    "content": "# 十二、重构到纯函数和通过纯函数重构\n\n程序员经常会碰到他们害怕改变的代码。通过提取纯函数，使用 curry 和 composition，并利用编译器，您可以以更安全的方式重构现有代码。我们将看到一个通过纯函数进行重构的例子，然后我们将看看一些设计模式，它们是如何在函数式编程中实现的，以及如何在重构中使用它们。\n\n本章将涵盖以下主题:\n\n*   如何看待遗留代码\n*   如何使用编译器和纯函数来识别和分离依赖关系\n*   如何从任意一段代码中提取 lambdas\n*   如何使用 currying 和 composition 消除 lambdas 之间的重复，并将它们分组到类中\n*   如何使用函数实现一些设计模式(策略、命令和依赖注入)\n*   如何使用基于函数的设计模式来重构它们\n\n# 技术要求\n\n您将需要一个支持 C++ 17 的编译器。我用的是 GCC 7.4.0c。\n\n代码在[的 GitHub 上。com/ PacktPublishing/动手-函数-用- Cpp 编程`Chapter12`文件夹中的](https://github.%E2%80%8Bcom/PacktPublishing/Hands-On-Functional-Programming-with-Cpp)。它包括并使用`doctest`，这是一个单头、开源的单元测试库。你可以在它的 GitHub 资源库[上找到它。com/ onqtam/ doctest](https://github.%E2%80%8Bcom/onqtam/doctest) 。\n\n# 重构到纯函数和通过纯函数重构\n\n**重构**是软件开发中重要且持续的部分。主要原因是需求的不断变化，这是由我们构建的应用周围世界的变化所驱动的。我们的客户不断了解产品工作的生态系统，并需要我们使这些产品适应他们发现的新现实。因此，我们的代码，即使结构完美，也几乎总是落后于我们当前对正在解决的问题的理解。\n\n完美地构建我们的代码也不是一件容易的事。程序员是人，所以我们会犯错误，失去注意力，有时会找不到最好的解决方案。处理这种复杂情况的唯一方法是使用无情的重构；也就是说，在我们让事情运转起来之后，我们改进代码结构，直到代码在我们的约束下尽可能好。\n\n说起来容易，做起来也容易，只要我们很早就重构并编写测试。但是如果我们继承一个没有测试的代码库呢？那我们怎么办？我们将讨论这个问题，以及稍后使用纯函数重构遗留代码的一个有前途的想法。\n\n首先，让我们定义我们的术语。什么是重构？\n\n# 什么是重构？\n\n重构是业内普遍使用的术语之一，但并不为人所知。不幸的是，这个词经常被用来证明大的重新设计是合理的。考虑以下关于给定项目的常见故事:\n\n*   当项目开始时，功能会快速添加。\n*   很快(几个月，一年，甚至几周)，速度下降，但需求是一样的。\n*   多年后，添加新功能变得如此困难，以至于客户很恼火，给团队带来压力。\n*   最后，决定重写或改变代码的整个结构，希望它能加快速度。\n*   六个月后，重写或重新设计(通常)失败，管理层面临一个不可能的情况——我们应该尝试重新设计，重新启动项目，还是做其他事情？\n\n这个周期的**大重新设计**阶段经常被错误地称为重构，但这不是重构。\n\n相反，为了理解重构的真正含义，让我们从思考我们可以对代码库做什么改变开始。我们通常可以将这些变化分类如下:\n\n*   实施新要求\n*   修复错误\n*   以各种方式重新组织代码——重构、重新设计、重新设计和/或重新架构\n\n我们可以将这些变化大致分为以下两大类:\n\n*   影响代码行为的更改\n*   不影响代码行为的更改\n\n当我们谈论行为时，我们谈论的是输入和输出，比如“当我在一个**用户界面** ( **UI** )表单中引入这些值并点击这个按钮，然后我看到这个输出，这些东西就被保存了”。我们通常不会在行为中包含跨功能的关注点，例如性能、可伸缩性或安全性。\n\n有了这些清晰的术语，我们就可以定义重构——它只是对代码结构进行不影响程序外部行为的更改。大的重新设计或重写很少符合这个定义，因为通常情况下，进行大的重新设计的团队不会证明结果与原始的有相同的行为(包括已知的 bug，因为有人可能依赖它们)。\n\n任何改变程序行为的改变都不是重构。这包括修复错误或添加功能。然而，我们可以将这些变化分成两个阶段——首先，重构到*为变化腾出空间*，然后进行行为上的变化。\n\n这一定义提出了如下几个问题:\n\n*   我们如何证明我们没有改变行为？我们知道只有一种方法可以做到这一点:自动回归测试。如果我们有一套我们信任的自动化测试，并且足够快，我们可以很容易地做出改变，而不需要改变任何测试，看看它们是否通过。\n*   重构有多小？变化越大，越难证明什么都没有受到影响，因为程序员是人，也会犯错。我们更喜欢在重构中有非常小的步骤。以下是一些保持行为的小代码更改示例:重命名、向函数添加参数、更改函数的参数顺序以及将一组语句提取到函数中，等等。每一个微小的改变都可以很容易地实现，测试运行证明没有行为改变发生。每当我们需要进行更大的重构时，我们只需要进行一系列这些小的改变。\n*   当我们没有测试时，我们如何证明我们没有改变代码的行为？这是我们需要讨论遗留代码和遗留代码困境的时候。\n\n# 遗留代码困境\n\n编程可能是唯一一个单词 *legacy* 有负面含义的领域。在任何其他上下文中，遗产意味着某人留下的东西和某人通常引以为豪的东西。在编程中，遗留代码指的是我们继承的独占代码，维护起来很麻烦。\n\n程序员经常认为遗留代码是不可避免的，对此无能为力。然而，我们可以做很多事情。首先是澄清我们所说的遗留代码是什么意思。迈克尔·费哲在他的《遗留代码》一书中，将其定义为没有测试的代码。不过，我喜欢用一个更笼统的定义:*你害怕更改的代码*。你害怕改变的代码会让你慢下来，减少你的选择，并使任何新的发展成为一场磨难。但这绝不是不可避免的:我们可以改变它，我们将拭目以待。\n\n我们能做的第二件事是理解遗留代码的困境。为了不那么害怕变化，我们需要重构它，但是为了重构代码，我们需要编写测试。为了编写测试，我们需要调整代码，使其可测试；这看起来像一个圆圈——为了改变代码，我们需要改变代码！如果我们一开始就害怕修改代码，我们该怎么做呢？\n\n幸运的是，这个困境有了解决办法。如果我们可以对代码进行安全的修改——这些修改给我们留下了很少的出错机会，并允许我们测试代码——那么我们可以缓慢但肯定地改进代码。这些变化确实是重构，但它们甚至比重构步骤更小、更安全。他们的主要目标是打破代码中设计元素之间的依赖性，使我们能够编写测试，以便我们可以在之后继续重构。\n\n因为我们的重点是使用纯函数和函数构造来重构代码，所以我们不会查看完整的技术列表。我可以举一个简单的例子叫做**提取并覆盖**。假设您需要为一个非常大的函数编写测试。如果我们可以只为函数的一小部分编写测试，那将是非常理想的。我们可以这样做的方法是将我们想要测试的代码提取到另一个函数中。然而，新的函数依赖于旧的代码，所以我们很难弄清楚所有的依赖关系。为了解决这个问题，我们可以创建一个派生类，用伪函数覆盖函数的所有依赖关系。在单元测试中，这被称为*部分模拟*。这允许我们用测试覆盖提取函数的所有代码，同时假设类的所有其他部分都像预期的那样工作。一旦我们用测试覆盖了它，我们就可以开始重构；我们经常最终提取一个新的类，这个类在本练习结束时被完全嘲笑或存根化。\n\n这些技术是在我们的语言如此广泛地支持函数式编程之前编写的。我们现在可以利用纯函数来安全地重构我们编写的代码。但是，要做到这一点，我们需要了解依赖关系如何影响我们测试和更改代码的能力。\n\n# 依赖性和变化\n\n我们的用户和客户想要越来越多的功能，因为只要项目成功。然而，我们经常无法交付，因为随着时间的推移，代码往往会变得更加僵化。添加新功能变得越来越慢久而久之，当添加一个功能，新的 bug 弹出。\n\n这就引出了十亿个问题——是什么让代码变得难以更改？我们如何编写保持变化速度，甚至提高变化速度的代码？\n\n这是一个复杂的问题，有许多方面和各种解决方案。其中一个是业内基本认同的——依赖性往往会减缓开发速度。依赖较少的代码结构通常更容易更改，从而更容易添加特性。\n\n我们可以在很多层面上看依赖关系。在更高的层次上，我们可以谈论依赖于其他可执行文件的可执行文件；例如，直接调用另一个 web 服务的 web 服务。通过使用基于事件的系统而不是直接调用，可以减少这一级别的依赖。在较低的层次上，我们可以谈论对库或操作系统例程的依赖；例如，依赖于特定文件夹或特定库版本的 web 服务。\n\n虽然所有其他级别都很有趣，但对于我们的目标，我们将关注类/函数级别，特别是类和函数如何相互依赖。由于在任何不平凡的代码库中避免依赖是不可能的，我们将关注依赖的强度。\n\n我们将使用我编写的一小段代码作为示例，该代码基于员工列表和诸如角色、资历、组织中的连续性和奖金水平等参数来计算工资。它从 CSV 文件中读取员工列表，根据一些规则计算工资，并打印计算出的工资列表。代码的第一个版本是天真地编写的，只使用了`main`函数，并将所有内容放在同一个文件中，如下面的代码示例所示:\n\n```cpp\n#include <iostream>\n#include <fstream>\n#include <string>\n#include <cmath>\n\nusing namespace std;\n\nint main(){\n    string id;\n    string employee_id;\n    string first_name;\n    string last_name;\n    string seniority_level;\n    string position;\n    string years_worked_continuously;\n    string special_bonus_level;\n\n    ifstream employeesFile(\"./Employees.csv\");\n    while (getline(employeesFile, id, ',')) {\n        getline(employeesFile, employee_id, ',') ;\n        getline(employeesFile, first_name, ',') ;\n        getline(employeesFile, last_name, ',') ;\n        getline(employeesFile, seniority_level, ',') ;\n        getline(employeesFile, position, ',') ;\n        getline(employeesFile, years_worked_continuously, ',') ;\n        getline(employeesFile, special_bonus_level);\n        if(id == \"id\") continue;\n\n        int baseSalary;\n        if(position == \"Tester\") baseSalary= 1500;\n        if(position == \"Analyst\") baseSalary = 1600;\n        if(position == \"Developer\") baseSalary = 2000;\n        if(position == \"Team Leader\") baseSalary = 3000;\n        if(position == \"Manager\") baseSalary = 4000;\n\n        double factor;\n        if(seniority_level == \"Entry\") factor = 1;\n        if(seniority_level == \"Junior\") factor = 1.2;\n        if(seniority_level == \"Senior\") factor = 1.5;\n\n        double continuityFactor;\n        int continuity = stoi(years_worked_continuously);\n        if(continuity < 3) continuityFactor = 1;\n        if(continuity >= 3 && continuity < 5) continuityFactor = 1.2;\n        if(continuity >= 5 && continuity < 10) continuityFactor = 1.5;\n        if(continuity >=10 && continuity <= 20) continuityFactor = 1.7;\n        if(continuity > 20) continuityFactor = 2;\n\n        int specialBonusLevel = stoi(special_bonus_level);\n        double specialBonusFactor = specialBonusLevel * 0.03;\n\n        double currentSalary = baseSalary * factor * continuityFactor;\n        double salary = currentSalary + specialBonusFactor * \n            currentSalary;\n\n        int roundedSalary = ceil(salary);\n\n        cout  << seniority_level << position << \" \" << first_name << \" \n            \" << last_name << \" (\" << years_worked_continuously << \n            \"yrs)\" <<  \", \" << employee_id << \", has salary (bonus                 \n            level  \" << special_bonus_level << \") \" << roundedSalary << \n            endl;\n    }\n}\n```\n\n输入文件是使用专用工具用随机值生成的，如下所示:\n\n```cpp\nid,employee_id,First_name,Last_name,Seniority_level,Position,Years_worked_continuously,Special_bonus_level\n1,51ef10eb-8c3b-4129-b844-542afaba7eeb,Carmine,De Vuyst,Junior,Manager,4,3\n2,171338c8-2377-4c70-bb66-9ad669319831,Gasper,Feast,Entry,Team Leader,10,5\n3,807e1bc7-00db-494b-8f92-44acf141908b,Lin,Sunley,Medium,Manager,23,3\n4,c9f18741-cd6c-4dee-a243-00c1f55fde3e,Leeland,Geraghty,Medium,Team Leader,7,4\n5,5722a380-f869-400d-9a6a-918beb4acbe0,Wash,Van der Kruys,Junior,Developer,7,1\n6,f26e94c5-1ced-467b-ac83-a94544735e27,Marjie,True,Senior,Tester,28,1\n\n```\n\n当我们运行程序时，为每个员工计算`salary`，输出如下所示:\n\n```cpp\nJuniorManager Carmine De Vuyst (4yrs), 51ef10eb-8c3b-4129-b844-542afaba7eeb, has salary (bonus level  3) 6279\nEntryTeam Leader Gasper Feast (10yrs), 171338c8-2377-4c70-bb66-9ad669319831, has salary (bonus level  5) 5865\nMediumManager Lin Sunley (23yrs), 807e1bc7-00db-494b-8f92-44acf141908b, has salary (bonus level  3) 8720\nMediumTeam Leader Leeland Geraghty (7yrs), c9f18741-cd6c-4dee-a243-00c1f55fde3e, has salary (bonus level  4) 5040\nJuniorDeveloper Wash Van der Kruys (7yrs), 5722a380-f869-400d-9a6a-918beb4acbe0, has salary (bonus level  1) 3708\nSeniorTester Marjie True (28yrs), f26e94c5-1ced-467b-ac83-a94544735e27, has salary (bonus level  1) 4635\nEntryAnalyst Muriel Dorken (10yrs), f4934e00-9c01-45f9-bddc-2366e6ea070e, has salary (bonus level  8) 3373\nSeniorTester Harrison Mawditt (17yrs), 66da352a-100c-4209-a13e-00ec12aa167e, has salary (bonus level  10) 4973\n```\n\n那么，这段代码有依赖性吗？是的，它们藏在显眼的地方。\n\n查找依赖项的一种方法是查找构造函数调用或全局变量。在我们的例子中，我们有一个对`ifstream`的构造函数调用，以及一个对`cout`的使用，如下例所示:\n\n```cpp\nifstream employeesFile(\"./Employees.csv\")\ncout  << seniority_level << position << \" \" << first_name << \" \" << \n    last_name << \" (\" << years_worked_continuously << \"yrs)\" <<  \", \" \n    << employee_id << \", has salary (bonus level  \" << \n    special_bonus_level << \") \" << roundedSalary << endl;\n```\n\n另一种识别依赖性的方法是创建一个想象练习。想象一下什么需求会在代码中产生变化。有几个。如果我们决定切换到员工数据库，我们将需要改变我们读取数据的方式。如果我们想输出到一个文件，我们需要更改打印工资的代码行。如果计算工资的规则改变，我们将需要改变计算`salary`的线。\n\n两种方法都得出相同的结论；我们依赖于文件系统和标准输出。让我们关注标准输出，问一个问题；我们如何更改代码，以便将工资输出到标准输出和文件中？答案很简单，由于**标准模板库** ( **STL** )流的多态性，只需提取一个接收输出流并写入数据的函数。让我们看看这样的函数会是什么样子；为了简单起见，我们还引入了一个名为`Employee`的结构，它包含了我们需要的所有字段，如下例所示:\n\n```cpp\nvoid printEmployee(const Employee& employee, ostream& stream, int \n    roundedSalary){\n        stream << employee.seniority_level << employee.position << \n        \" \" << employee.first_name << \" \" << employee.last_name << \n        \" (\" << employee.years_worked_continuously << \"yrs)\" <<  \",             \n        \" << employee.employee_id << \", has salary (bonus level  \" << \n        employee.special_bonus_level << \") \" << roundedSalary << endl;\n    }\n```\n\n这个函数不再依赖于标准输出。依赖方面，可以说*打破了员工打印和标准输出之间的依赖*。我们是怎么做到的？我们从调用者那里传递了`cout`流作为函数的参数:\n\n```cpp\n        printEmployee(employee, cout, roundedSalary);\n```\n\n这个看似很小的改变使得函数多态。`printEmployee`的调用者现在控制函数的输出，而不改变函数内部的任何东西。\n\n此外，我们现在可以为`printEmployee`函数编写从不接触文件系统的测试。这一点很重要，因为文件系统访问很慢，并且在测试快乐路径时，由于缺少磁盘空间或部分损坏等原因，可能会出现错误。我们如何编写这样的测试？我们只需要使用内存流调用函数，然后将写入内存流的输出与我们期望的进行比较。\n\n因此，打破这种依赖会导致我们代码的可变性和可测试性的巨大改善。这个机制如此有用和广泛，以至于它获得了一个名字——**依赖注入** ( **DI** )。在我们的例子中，`printEmployee`函数的调用者(`main`函数、`test`函数或另一个未来的调用者)将对输出流的依赖注入到我们的函数中，从而控制它的行为。\n\n澄清关于 DI 的一件事很重要——它是一个设计模式，而不是一个库。许多现代库和 MVC 框架都支持 DI，但是您不需要任何外部的东西来注入依赖性。您只需要将依赖关系传递到构造函数、属性或函数参数中，您就万事大吉了。\n\n我们学习了如何识别依赖关系，以及如何使用 DI 来打破它们。是时候看看我们如何利用纯函数重构这段代码了。\n\n# 纯函数和程序结构\n\n几年前，我学到了一条关于计算机程序的基本定律，它让我开始研究如何在重构中使用纯函数:\n\n*任何计算机程序都可以由两种类型的类/函数构建而成——有些是做 I/O 的，有些是纯的。*\n\n后来搜索类似的想法，我找到了加里·伯恩哈特对这类结构的简洁命名:*功能核心，命令外壳*([https://www . destroyallsoftware . com/screencasts/catalog/functional-core-command-shell](https://www.destroyallsoftware.com/screencasts/catalog/functional-core-imperative-shell))。\n\n不管你怎么称呼它，这个想法对重构的影响是根本性的。如果任何程序都可以写成两种不同类型的类/函数，一些是不可变的，一些是输入输出的，那么我们可以利用这个属性重构遗留代码。高级流程如下所示:\n\n*   提取纯函数(我们将看到这些步骤识别依赖性)。\n*   测试并重构它们。\n*   根据高内聚原则将它们重新分组。\n\n我想给这条定律增加一条公理。我相信我们可以在代码的任何级别应用这一点，无论是函数、类、一组代码行、一组类还是整个模块，除了那些纯 I/O 的代码行。换句话说，这个定律是分形的；除了最基本的代码，它适用于任何级别的代码。\n\n这条公理的重要性是巨大的。它告诉我们的是，除了最基本的以外，我们可以在代码的任何级别应用我们之前描述的相同方法。换句话说，我们从哪里开始应用该方法并不重要，因为它将在任何地方工作。\n\n在接下来的部分中，我们将探索该方法的每一步。首先，让我们提取一些纯函数。\n\n# 使用编译器和纯函数来识别依赖关系\n\n试图更改我们不理解且没有测试的代码可能会有风险。任何错误都会导致丑陋的 bug，任何改变都会导致错误。\n\n幸运的是，编译器和纯函数可以帮助揭示依赖关系。记住什么是纯函数——对于相同的输入返回相同输出的函数。根据定义，这意味着纯函数的所有依赖都是可见的，要么作为参数、全局变量传递，要么通过变量捕获传递。\n\n这就给我们带来了一个识别代码中依赖关系的简单方法:挑选几行代码，将它们提取到一个函数中，使其纯净，然后让编译器告诉你依赖关系是什么。此外，依赖项将不得不被注入，从而导致我们得到一个可测试的函数。\n\n我们来看几个例子。简单的开始是下面几行代码，它们根据给定员工在公司中的职位计算基本工资:\n\n```cpp\n        int baseSalary;\n        if(position == \"Tester\") baseSalary = 1500;\n        if(position == \"Analyst\") baseSalary = 1600;\n        if(position == \"Developer\") baseSalary = 2000;\n        if(position == \"Team Leader\") baseSalary = 3000;\n        if(position == \"Manager\") baseSalary = 4000;\n```\n\n让我们把它提取为一个纯函数。名字暂时不重要，我们暂时称之为`doesSomething`，我只是将代码行复制粘贴到新函数中，不从旧函数中移除，如下例所示:\n\n```cpp\nauto doesSomething = [](){\n        int baseSalary;\n        if(position == \"Tester\") baseSalary = 1500;\n        if(position == \"Analyst\") baseSalary = 1600;\n        if(position == \"Developer\") baseSalary = 2000;\n        if(position == \"Team Leader\") baseSalary = 3000;\n        if(position == \"Manager\") baseSalary = 4000;\n};\n```\n\n我的编译器立即抱怨这个位置没有被定义，所以它帮我计算了依赖关系。让我们将其添加为一个参数，如下例所示:\n\n```cpp\nauto doesSomething = [](const string& position){\n        int baseSalary;\n        if(position == \"Tester\") baseSalary = 1500;\n        if(position == \"Analyst\") baseSalary = 1600;\n        if(position == \"Developer\") baseSalary = 2000;\n        if(position == \"Team Leader\") baseSalary = 3000;\n        if(position == \"Manager\") baseSalary = 4000;\n};\n```\n\n这个函数缺少一些东西；纯函数总是返回值，但这不是。让我们添加`return`语句，如下面的代码示例所示:\n\n```cpp\nauto doesSomething = [](const string& position){\n        int baseSalary;\n        if(position == \"Tester\") baseSalary = 1500;\n        if(position == \"Analyst\") baseSalary = 1600;\n        if(position == \"Developer\") baseSalary = 2000;\n        if(position == \"Team Leader\") baseSalary = 3000;\n        if(position == \"Manager\") baseSalary = 4000;\n        return baseSalary;\n};\n```\n\n该函数现在已经足够简单，可以单独测试。但是首先，我们需要把它提取到一个单独的`.h`文件中，并给它一个合适的名称。`baseSalaryForPosition`听起来不错；让我们在下面的代码中看看它的测试:\n\n```cpp\nTEST_CASE(\"Base salary\"){\n    CHECK_EQ(1500, baseSalaryForPosition(\"Tester\"));\n    CHECK_EQ(1600, baseSalaryForPosition(\"Analyst\"));\n    CHECK_EQ(2000, baseSalaryForPosition(\"Developer\"));\n    CHECK_EQ(3000, baseSalaryForPosition(\"Team Leader\"));\n    CHECK_EQ(4000, baseSalaryForPosition(\"Manager\"));\n    CHECK_EQ(0, baseSalaryForPosition(\"asdfasdfs\"));\n}\n```\n\n测试编写起来相当简单。他们还从函数中复制了许多东西，包括职位字符串和薪资值。有更好的方法来组织代码，但这是遗留代码的期望。目前，我们很高兴用测试覆盖了部分初始代码。我们也可以向领域专家展示这些测试，并检查它们是否正确，但是让我们继续我们的重构。我们需要从`main()`开始调用新函数，如下图所示:\n\n```cpp\n    while (getline(employeesFile, id, ',')) {\n        getline(employeesFile, employee_id, ',') ;\n        getline(employeesFile, first_name, ',') ;\n        getline(employeesFile, last_name, ',') ;\n        getline(employeesFile, seniority_level, ',') ;\n        getline(employeesFile, position, ',') ;\n        getline(employeesFile, years_worked_continuously, ',') ;\n        getline(employeesFile, special_bonus_level);\n        if(id == \"id\") continue;\n\n int baseSalary = baseSalaryForPosition(position);\n        double factor;\n        if(seniority_level == \"Entry\") factor = 1;\n        if(seniority_level == \"Junior\") factor = 1.2;\n        if(seniority_level == \"Senior\") factor = 1.5;\n        ...\n}\n\n```\n\n虽然这是一个简单的案例，但它显示了基本过程，如下所示:\n\n*   挑选几行代码。\n*   将它们提取到函数中。\n*   使函数纯净。\n*   注入所有依赖项。\n*   为新的纯函数编写测试。\n*   验证行为。\n*   重复，直到整个代码被测试覆盖。\n\n如果您遵循这个过程，引入 bug 的风险会变得非常小。从我的经验来看，你最需要小心的是使功能纯粹。记住——如果它在一个类中，用`const`参数使它静态，但是如果它在一个类之外，将所有参数作为`const`传递，使它成为一个λ。\n\n如果我们重复这个过程几次，我们最终会得到更纯的函数。首先，`factorForSeniority`根据资历级别计算因子，如下例所示:\n\n```cpp\nauto factorForSeniority = [](const string& seniority_level){\n    double factor;\n    if(seniority_level == \"Entry\") factor = 1;\n    if(seniority_level == \"Junior\") factor = 1.2;\n    if(seniority_level == \"Senior\") factor = 1.5;\n    return factor;\n};\n```\n\n然后，`factorForContinuity`根据——你猜对了——连续性计算因子:\n\n```cpp\nauto factorForContinuity = [](const string& years_worked_continuously){\n    double continuityFactor;\n    int continuity = stoi(years_worked_continuously);\n    if(continuity < 3) continuityFactor = 1;\n    if(continuity >= 3 && continuity < 5) continuityFactor = 1.2;\n    if(continuity >= 5 && continuity < 10) continuityFactor = 1.5;\n    if(continuity >=10 && continuity <= 20) continuityFactor = 1.7;\n    if(continuity > 20) continuityFactor = 2;\n    return continuityFactor;\n};\n\n```\n\n最后，`bonusLevel`功能读取奖励等级:\n\n```cpp\nauto bonusLevel = [](const string& special_bonus_level){\n    return stoi(special_bonus_level);\n};\n```\n\n这些函数中的每一个都可以通过基于示例、数据驱动或基于属性的测试来轻松测试。提取所有这些函数后，我们的主要方法看起来像下面的例子(为了简洁起见，省略了几行):\n\n```cpp\nint main(){\n...\n    ifstream employeesFile(\"./Employees.csv\");\n    while (getline(employeesFile, id, ',')) {\n        getline(employeesFile, employee_id, ',') ;\n...\n        getline(employeesFile, special_bonus_level);\n        if(id == \"id\") continue;\n\n int baseSalary = baseSalaryForPosition(position);\n double factor = factorForSeniority(seniority_level);\n\n double continuityFactor = \n            factorForContinuity(years_worked_continuously);\n\n int specialBonusLevel =  bonusLevel(special_bonus_level);\n        double specialBonusFactor = specialBonusLevel * 0.03;\n\n        double currentSalary = baseSalary * factor * continuityFactor;\n        double salary = currentSalary + specialBonusFactor * \n            currentSalary;\n\n        int roundedSalary = ceil(salary);\n\n        cout  << seniority_level << position << \" \" << first_name << \"           \n          \" << last_name << \" (\" << years_worked_continuously << \"yrs)\"     \n          <<  \", \" << employee_id << \", has salary (bonus level  \" << \n          special_bonus_level << \") \" << roundedSalary << endl;\n    }\n```\n\n这是一个有点干净，更好的测试覆盖。不过，Lambdas 可以用于更多用途；让我们看看如何做到这一点。\n\n# 从遗留代码到 lambdas\n\n除了纯度，lambdas 还提供了许多我们可以使用的操作:功能组合、部分应用、currying 和更高级的功能。我们可以在重构遗留代码时利用这些操作。\n\n最简单的方法是从`main`方法中提取整个`salary`计算。这些是计算`salary`的代码行:\n\n```cpp\n...        \n        int baseSalary = baseSalaryForPosition(position);\n        double factor = factorForSeniority(seniority_level);\n\n        double continuityFactor = \n            factorForContinuity(years_worked_continuously);\n\n        int specialBonusLevel =  bonusLevel(special_bonus_level);\n        double specialBonusFactor = specialBonusLevel * 0.03;\n\n        double currentSalary = baseSalary * factor * continuityFactor;\n        double salary = currentSalary + specialBonusFactor * \n            currentSalary;\n\n        int roundedSalary = ceil(salary);\n...\n```\n\n我们可以通过两种方式提取这个纯函数——一种是传入每个需要的值作为参数，结果如下所示:\n\n```cpp\nauto computeSalary = [](const string& position, const string seniority_level, const string& years_worked_continuously, const string& special_bonus_level){\n    int baseSalary = baseSalaryForPosition(position);\n    double factor = factorForSeniority(seniority_level);\n\n    double continuityFactor = \n        factorForContinuity(years_worked_continuously);\n\n    int specialBonusLevel =  bonusLevel(special_bonus_level);\n    double specialBonusFactor = specialBonusLevel * 0.03;\n\n    double currentSalary = baseSalary * factor * continuityFactor;\n    double salary = currentSalary + specialBonusFactor * currentSalary;\n\n    int roundedSalary = ceil(salary);\n    return roundedSalary;\n};\n```\n\n第二种选择要有趣得多。与其传递变量，不如我们传递函数，并事先将它们绑定到所需的变量上。\n\n这是一个有趣的想法。结果是一个函数接收多个函数作为参数，每个函数都没有任何参数:\n\n```cpp\nauto computeSalary = [](auto baseSalaryForPosition, auto factorForSeniority, auto factorForContinuity, auto bonusLevel){\n    int baseSalary = baseSalaryForPosition();\n    double factor = factorForSeniority();\n    double continuityFactor = factorForContinuity();\n    int specialBonusLevel =  bonusLevel();\n\n    double specialBonusFactor = specialBonusLevel * 0.03;\n\n    double currentSalary = baseSalary * factor * continuityFactor;\n    double salary = currentSalary + specialBonusFactor * currentSalary;\n\n    int roundedSalary = ceil(salary);\n    return roundedSalary;\n};\n```\n\n`main`方法需要先绑定函数，然后将它们注入到我们的方法中，如下所示:\n\n```cpp\n        auto roundedSalary = computeSalary(\n                bind(baseSalaryForPosition, position), \n                bind(factorForSeniority, seniority_level),\n        bind(factorForContinuity, years_worked_continuously),\n        bind(bonusLevel, special_bonus_level));\n\n        cout  << seniority_level << position << \" \" << first_name << \" \n          \" << last_name << \" (\" << years_worked_continuously << \"yrs)\"           \n          <<  \", \" << employee_id << \", has salary (bonus level  \" <<              \n          special_bonus_level << \") \" << roundedSalary << endl;\n```\n\n为什么这种方法很有趣？好吧，让我们从软件设计的角度来看。我们创建了小的纯函数，每个函数都有明确的职责。然后，我们将它们绑定到特定的值。之后，我们将它们作为参数传递给另一个 lambda，后者使用它们来计算我们需要的结果。\n\n这在面向对象编程风格中意味着什么？函数是类的一部分。将函数绑定到值相当于调用类的构造函数。将对象传递给另一个函数称为 DI。\n\n等一下！我们实际上在做的是分离责任和注入依赖，仅仅通过使用纯函数而不是对象！因为我们使用的是纯函数，编译器会使依赖关系变得明显。因此，我们有一种重构错误概率非常小的代码的方法，因为我们经常使用编译器。这是一个非常有用的重构过程。\n\n我不得不承认结果没有我想的那么好。让我们重构我们的 lambda。\n\n# 重构 lambdas\n\n我对我们提取的λ不满意。由于接收的参数多，责任多，所以相当复杂。让我们仔细看看，看看如何改进:\n\n```cpp\nauto computeSalary = [](auto baseSalaryForPosition, auto \n    factorForSeniority, auto factorForContinuity, auto bonusLevel){\n        int baseSalary = baseSalaryForPosition();\n        double factor = factorForSeniority();\n        double continuityFactor = factorForContinuity();\n        int specialBonusLevel =  bonusLevel();\n\n        double specialBonusFactor = specialBonusLevel * 0.03;\n\n        double currentSalary = baseSalary * factor * continuityFactor;\n        double salary = currentSalary + specialBonusFactor * \n            currentSalary;\n\n        int roundedSalary = ceil(salary);\n         return roundedSalary;\n};\n```\n\n所有迹象似乎都表明，该职能有多重责任。如果我们从中提取更多的函数呢？让我们从`specialBonusFactor`计算开始:\n\n```cpp\nauto specialBonusFactor = [](auto bonusLevel){\n    return bonusLevel() * 0.03;\n};\nauto computeSalary = [](auto baseSalaryForPosition, auto     \nfactorForSeniority, auto factorForContinuity, auto bonusLevel){\n    int baseSalary = baseSalaryForPosition();\n    double factor = factorForSeniority();\n    double continuityFactor = factorForContinuity();\n\n    double currentSalary = baseSalary * factor * continuityFactor;\n    double salary = currentSalary + specialBonusFactor() * \n        currentSalary;\n\n    int roundedSalary = ceil(salary);\n    return roundedSalary;\n};\n```\n\n我们现在可以注射`specialBonusFactor`。然而，请注意`specialBonusFactor`是唯一需要`bonusLevel`的λ。这意味着我们可以将`bonusLevel`λ替换为部分应用于`bonusLevel`的`specialBonusFactor`λ，如下例所示:\n\n```cpp\nint main(){\n        ...\n  auto bonusFactor = bind(specialBonusFactor, [&](){ return \n    bonusLevel(special_bonus_level); } );\n  auto roundedSalary = computeSalary(\n      bind(baseSalaryForPosition, position), \n      bind(factorForSeniority, seniority_level),\n      bind(factorForContinuity, years_worked_continuously),\n      bonusFactor\n     );\n ...\n}\n\nauto computeSalary = [](auto baseSalaryForPosition, auto factorForSeniority, auto factorForContinuity, auto bonusFactor){\n    int baseSalary = baseSalaryForPosition();\n    double factor = factorForSeniority();\n    double continuityFactor = factorForContinuity();\n\n    double currentSalary = baseSalary * factor * continuityFactor;\n    double salary = currentSalary + bonusFactor() * currentSalary;\n\n    int roundedSalary = ceil(salary);\n    return roundedSalary;\n};\n```\n\n我们的`computeSalary`λ现在变小了。通过内联临时变量，我们可以使它更小:\n\n```cpp\nauto computeSalary = [](auto baseSalaryForPosition, auto \n    factorForSeniority, auto factorForContinuity, auto bonusFactor){\n        double currentSalary = baseSalaryForPosition() * \n            factorForSeniority() * factorForContinuity();\n    double salary = currentSalary + bonusFactor() * currentSalary;\n    return ceil(salary);\n};\n```\n\n太好了！然而，我想让它更接近数学公式。首先，让我们重写行计算`salary`(在代码中以粗体突出显示):\n\n```cpp\nauto computeSalary = [](auto baseSalaryForPosition, auto \n    factorForSeniority, auto factorForContinuity, auto bonusFactor){\n        double currentSalary = baseSalaryForPosition() * \n            factorForSeniority() * factorForContinuity();\n double salary = (1 + bonusFactor()) * currentSalary;\n    return ceil(salary);\n};\n```\n\n然后，让我们用函数替换变量。然后，我们剩下下面的代码示例:\n\n```cpp\nauto computeSalary = [](auto baseSalaryForPosition, auto \n    factorForSeniority, auto factorForContinuity, auto bonusFactor){\n        return ceil (\n                (1 + bonusFactor()) * baseSalaryForPosition() *                             \n                    factorForSeniority() * factorForContinuity()\n    );\n};\n```\n\n因此，我们有一个 lambda，它接收多个 lambda 并使用它们来计算一个值。我们仍然可以对其他功能进行改进，但是我们已经达到了一个有趣的点。\n\n那我们从这里去哪里？我们已经注入了依赖项，代码更加模块化，更容易更改，也更容易测试。我们可以从返回我们想要的值的测试中注入 lambdas，这实际上是单元测试中的一个存根。虽然我们没有改进整个代码，但我们通过提取纯函数和使用函数操作来分离依赖关系和责任。如果我们愿意，我们可以这样留下代码。或者，我们可以采取另一个步骤，将函数重新组合成类。\n\n# 从 lambdas 到班级\n\n在本书中，我们已经多次指出，一个类只不过是一组内聚的部分应用的纯函数。以我们迄今为止的技术，我们已经创建了一堆部分应用的纯函数。把它们变成类现在是一个简单的任务。\n\n让我们看一个简单的`baseSalaryForPosition`函数的例子:\n\n```cpp\nauto baseSalaryForPosition = [](const string& position){\n    int baseSalary;\n    if(position == \"Tester\") baseSalary = 1500;\n    if(position == \"Analyst\") baseSalary = 1600;\n    if(position == \"Developer\") baseSalary = 2000;\n    if(position == \"Team Leader\") baseSalary = 3000;\n    if(position == \"Manager\") baseSalary = 4000;\n    return baseSalary;\n};\n```\n\n我们在`main()`中使用它，如下例所示:\n\n```cpp\n        auto roundedSalary = computeSalary(\n bind(baseSalaryForPosition, position), \n                bind(factorForSeniority, seniority_level),\n                bind(factorForContinuity, years_worked_continuously),\n                bonusFactor\n            );\n```\n\n要把它变成一个类，我们只需要创建一个将接收`position`参数的构造函数，然后把它变成一个类方法。让我们在下面的例子中看到它:\n\n```cpp\nclass BaseSalaryForPosition{\n    private:\n        const string& position;\n\n    public:\n        BaseSalaryForPosition(const string& position) : \n            position(position){};\n\n        int baseSalaryForPosition() const{\n            int baseSalary;\n            if(position == \"Tester\") baseSalary = 1500;\n            if(position == \"Analyst\") baseSalary = 1600;\n            if(position == \"Developer\") baseSalary = 2000;\n            if(position == \"Team Leader\") baseSalary = 3000;\n            if(position == \"Manager\") baseSalary = 4000;\n            return baseSalary;\n        }\n};\n```\n\n我们可以简单地初始化并传递对象，而不是将部分应用的函数传递给`computeSalary` lambda，如下面的代码所示:\n\n```cpp\n auto bonusFactor = bind(specialBonusFactor, [&](){ return \n            bonusLevel(special_bonus_level); } );\n            auto roundedSalary = computeSalary(\n                theBaseSalaryForPosition,\n                bind(factorForSeniority, seniority_level),\n                bind(factorForContinuity, years_worked_continuously),\n                bonusFactor\n            );\n```\n\n为此，我们还需要更改我们的`computeSalary`λ，如下所示:\n\n```cpp\nauto computeSalary = [](const BaseSalaryForPosition& \n    baseSalaryForPosition, auto factorForSeniority, auto     \n        factorForContinuity, auto bonusFactor){\n            return ceil (\n                (1 + bonusFactor()) * \n                    baseSalaryForPosition.baseSalaryForPosition() *                             \n                        factorForSeniority() * factorForContinuity()\n            );\n};\n```\n\n现在，为了允许注入不同的实现，我们实际上需要从`BaseSalaryForPosition`类中提取一个接口，并将其作为一个接口注入，而不是一个类。这对于从测试中注入双精度尤其有用，例如存根或模拟。\n\n从现在开始，没有什么能阻止你把函数重组成你认为合适的类。我将把这个留给读者作为一个练习，因为我相信我们已经展示了如何使用纯函数来重构代码，即使当我们想要在最后获得面向对象的代码时。\n\n# 概括重构方法\n\n到目前为止，我们学到了什么？嗯，我们经历了一个结构化的重构过程，它可以在代码的任何级别使用，降低了错误的概率，并支持可变性和测试。这个过程基于两个基本思想——任何程序都可以作为不可变函数和输入/输出函数的组合来编写，或者作为命令外壳中的功能核心来编写。此外，我们还证明了这个属性是分形的——我们可以将它应用于任何级别的代码，从几行代码到整个模块。\n\n因为不可变函数可以是我们程序的核心，所以我们可以一点一点地提取它们。我们编写新的函数名，复制并粘贴主体，并使用编译器传递任何依赖项作为参数。当代码被编译时，如果我们小心而缓慢地改变它，我们相当确定代码仍然正常工作。这种提取揭示了我们功能的依赖性，从而允许我们做出设计决策。\n\n接下来，我们将提取更多接收其他部分应用的纯函数作为参数的函数。这就导致了依赖关系和实际中断依赖关系之间的明显区别。\n\n最后，由于部分应用的函数相当于类，我们可以基于内聚性轻松封装其中的一个或多个。不管我们是从类还是函数开始，这个过程都是有效的，不管我们是想以函数还是类结束。然而，它允许我们使用函数构造来打破依赖，并在代码中分离责任。\n\n既然我们正在改进设计，是时候看看设计模式如何应用于函数式编程，以及如何面向它们进行重构了。我们将访问“四人帮”的一些模式，以及我们已经在代码中使用过的 DI。\n\n# 设计模式\n\n软件开发中的许多好东西来自那些注意到程序员如何工作并从中吸取某些教训的人；换句话说，着眼于实际的方法，并从中吸取共同的、有用的教训，而不是猜测解决方案。\n\n所谓的“四人帮”(埃里希·伽马、理查德·赫尔姆、拉尔夫·约翰逊和约翰·弗里西德斯)采用了这种精确的方法，他们用精确的语言记录了一系列设计模式。在注意到越来越多的程序员以相似的方式解决相同的问题后，他们决定将这些模式写下来，并将编程世界引入到清晰上下文中特定问题的可重用解决方案的思想中。\n\n由于当时的设计范式是面向对象的，他们出版的*设计模式*一书展示了这些使用面向对象方法的解决方案。此外，非常有趣的是，他们尽可能记录了至少两种类型的解决方案——一种基于谨慎的继承，另一种基于对象组合。我花了很多时间学习设计模式书，我可以告诉你，这是软件设计中非常有趣的一课。\n\n我们将在下一节探索一些设计模式以及如何使用函数来实现它们。\n\n# 战略模式、功能风格\n\n策略模式可以简单地描述为一种构建代码的方式，它允许在运行时选择算法。OOP 实现使用了 DI，您可能已经熟悉了 STL 的面向对象和功能设计。\n\n我们来看看 STL `sort`功能。它最复杂的形式需要一个 functor 对象，如下例所示:\n\n```cpp\nclass Comparator{\n    public: \n        bool operator() (int first, int second) { return (first < second);}\n};\n\nTEST_CASE(\"Strategy\"){\n    Comparator comparator;\n    vector<int> values {23, 1, 42, 83, 52, 5, 72, 11};\n    vector<int> expected {1, 5, 11, 23, 42, 52, 72, 83};\n\n    sort(values.begin(), values.end(), comparator);\n\n    CHECK_EQ(values, expected);\n}\n```\n\n`sort`函数使用`comparator`对象来比较向量中的元素，并对其进行适当排序。这是一种策略模式，因为我们可以用任何有相同界面的东西来交换`comparator`；其实只是需要`operator()`功能实现。例如，我们可以想象一个用户界面，其中用户选择比较函数，并使用它对值列表进行排序；我们只需要在运行时创建正确的`comparator`实例，并将其发送到`sort`函数。\n\n您已经可以看到功能解决方案的种子。事实上，`sort`函数允许一个简单得多的版本，如下例所示:\n\n```cpp\nauto compare = [](auto first, auto second) { return first < second;};\n\nTEST_CASE(\"Strategy\"){\n    vector<int> values {23, 1, 42, 83, 52, 5, 72, 11};\n    vector<int> expected {1, 5, 11, 23, 42, 52, 72, 83};\n\n    sort(values.begin(), values.end(), compare);\n\n    CHECK_EQ(values, expected);\n}\n```\n\n这一次，我们放下仪式，直接开始实现我们需要的东西——一个我们插入`sort`的比较函数。不再有类，不再有操作符——策略只是一个函数。\n\n让我们看看这在更复杂的环境中是如何工作的。我们将使用*策略模式*、[https://en.wikipedia.org/wiki/Strategy_pattern](https://en.wikipedia.org/wiki/Strategy_pattern)上的维基百科页面中的问题，并使用功能方法编写它。\n\n问题来了:我们需要为一家酒吧编写一个计费系统，可以为欢乐时光提供折扣。这个问题适用于策略模式的使用，因为我们有两个策略来计算账单的最终价格——一个返回完整价格，而第二个返回完整账单的快乐时光折扣(在我们的情况下，我们将使用 50%)。再一次，解决方案是简单地对这两种策略使用两个函数——只返回其收到的全部价格的`normalBilling`函数和返回其收到的一半价值的`happyHourBilling`函数。让我们在下面的代码中看到这一点(源自我的**测试驱动开发** ( **TDD** )方法):\n\n```cpp\nmap<string, double> drinkPrices = {\n    {\"Westmalle Tripel\", 15.50},\n    {\"Lagavulin 18y\", 25.20},\n};\n\nauto happyHourBilling = [](auto price){\n    return price / 2;\n};\n\nauto normalBilling = [](auto price){\n    return price;\n};\n\nauto computeBill = [](auto drinks, auto billingStrategy){\n    auto prices = transformAll<vector<double>>(drinks, [](auto drink){ \n    return drinkPrices[drink]; });\n    auto sum = accumulateAll(prices, 0.0, std::plus<double>());\n    return billingStrategy(sum);\n};\n\nTEST_CASE(\"Compute total bill from list of drinks, normal billing\"){\n   vector<string> drinks; \n   double expectedBill;\n\n   SUBCASE(\"no drinks\"){\n       drinks = {};\n       expectedBill = 0;\n   };\n\n   SUBCASE(\"one drink no discount\"){\n       drinks = {\"Westmalle Tripel\"};\n       expectedBill = 15.50;\n   };\n\n   SUBCASE(\"one another drink no discount\"){\n       drinks = {\"Lagavulin 18y\"};\n       expectedBill = 25.20;\n   };\n\n  double actualBill = computeBill(drinks, normalBilling);\n\n   CHECK_EQ(expectedBill, actualBill);\n}\n\nTEST_CASE(\"Compute total bill from list of drinks, happy hour\"){\n   vector<string> drinks; \n   double expectedBill;\n\n   SUBCASE(\"no drinks\"){\n       drinks = {};\n       expectedBill = 0;\n   };\n\n   SUBCASE(\"one drink happy hour\"){\n       drinks = {\"Lagavulin 18y\"};\n       expectedBill = 12.60;\n   };\n\n   double actualBill = computeBill(drinks, happyHourBilling);\n\n   CHECK_EQ(expectedBill, actualBill);\n}\n```\n\n我认为这表明策略最简单的实现是一个函数。我个人喜欢这种模式给战略模式带来的简单性；编写最少的有用代码让事情运转起来是一种解放。\n\n# 命令模式、功能样式\n\n命令模式是我在工作中广泛使用的一种模式。它非常适合 MVC 网络框架，允许将控制器分成多个功能块，同时允许与存储格式分离。它的目的是将请求与操作分开——这就是它如此通用的原因，因为任何调用都可以被视为请求。\n\n命令模式使用的一个简单例子是在支持多个控制器和改变键盘快捷键的游戏中。这些游戏无法将 *W* 按键事件直接链接到将你的角色上移的代码；取而代之的是，你将 *W* 键绑定到一个`MoveUpCommand`上，从而巧妙地将两者脱钩。我们可以轻松地更改与向上移动的命令或代码相关联的控制器事件，两者之间没有干扰。\n\n当我们研究命令如何在面向对象的代码中实现时，功能解决方案变得同样明显。一个`MoveUpCommand`类看起来像下面的例子:\n\n```cpp\nclass MoveUpCommand{\n    public:\n        MoveUpCommand(/*parameters*/){}\n        void execute(){ /* implementation of the command */}\n}\n```\n\n我说很明显！我们实际上试图完成的事情很容易通过命名函数来完成，如下例所示:\n\n```cpp\nauto moveUpCommand = [](/*parameters*/{\n/* implementation */\n};\n```\n\n最简单的命令模式是函数。谁会想到呢？\n\n# 带有函数的依赖注入\n\n如果不涉及到 DI，我们就不能谈论广泛传播的设计模式。虽然在“四人帮”的书中没有定义，但这种模式在现代代码中已经变得如此普遍，以至于许多程序员将其视为框架或库的一部分，而不是设计模式。\n\nDI 模式的目的是将类或函数的依赖创建与其行为分开。为了理解它所解决的问题，让我们看看这段代码:\n\n```cpp\nauto readFromFileAndAddTwoNumbers = [](){\n    int first;\n    int second;\n    ifstream numbersFile(\"numbers.txt\");\n    numbersFile >> first;\n    numbersFile >> second;\n    numbersFile.close();\n    return first + second;\n};\n\nTEST_CASE(\"Reads from file\"){\n    CHECK_EQ(30, readFromFileAndAddTwoNumbers());\n}\n```\n\n如果您只需要将从文件中读取的两个数字相加，这是一个相当公平的代码。不幸的是，在现实世界中，我们的客户很可能需要更多的来源来读取数字，例如控制台，如下所示:\n\n```cpp\nauto readFromConsoleAndAddTwoNumbers = [](){\n    int first;\n    int second;\n    cout << \"Input first number: \";\n    cin >> first;\n    cout << \"Input second number: \";\n    cin >> second;\n    return first + second;\n};\n\nTEST_CASE(\"Reads from console\"){\n    CHECK_EQ(30, readFromConsoleAndAddTwoNumbers());\n}\n```\n\n在继续之前，请注意，只有当您从控制台引入两个总和为`30`的数字时，该功能的测试才会通过。因为它们在每次运行时都需要输入，所以测试用例在我们的代码示例中被注释；请随意启用它并玩弄它。\n\n这两个函数看起来非常相似。为了解决这种相似性，DI 可以提供帮助，如下例所示:\n\n```cpp\nauto readAndAddTwoNumbers = [](auto firstNumberReader, auto \n    secondNumberReader){\n        int first = firstNumberReader();\n        int second = secondNumberReader();\n        return first + second;\n};\n```\n\n现在我们可以实现使用文件的读取器:\n\n```cpp\n\nauto readFirstFromFile = [](){\n    int number;\n    ifstream numbersFile(\"numbers.txt\");\n    numbersFile >> number;\n    numbersFile.close();\n    return number;\n};\n\nauto readSecondFromFile = [](){\n    int number;\n    ifstream numbersFile(\"numbers.txt\");\n    numbersFile >> number;\n    numbersFile >> number;\n    numbersFile.close();\n    return number;\n};\n```\n\n我们还可以实现使用控制台的阅读器:\n\n```cpp\n\nauto readFirstFromConsole = [](){\n    int number;\n    cout << \"Input first number: \";\n    cin >> number;\n    return number;\n};\n\nauto readSecondFromConsole = [](){\n    int number;\n    cout << \"Input second number: \";\n    cin >> number;\n    return number;\n};\n```\n\n像往常一样，我们可以测试它们在各种组合中是否正常工作，如下所示:\n\n```cpp\nTEST_CASE(\"Reads using dependency injection and adds two numbers\"){\n    CHECK_EQ(30, readAndAddTwoNumbers(readFirstFromFile, \n        readSecondFromFile));\n    CHECK_EQ(30, readAndAddTwoNumbers(readFirstFromConsole, \n        readSecondFromConsole));\n    CHECK_EQ(30, readAndAddTwoNumbers(readFirstFromFile, \n        readSecondFromConsole));\n}\n```\n\n我们正在注入通过 lambda 读取数字的代码。请注意，在测试代码中，使用这个方法允许我们混合和匹配我们认为合适的依赖关系——最后一个检查从文件中读取第一个数字，而第二个从控制台中读取。\n\n当然，我们通常在面向对象语言中实现 DI 的方式是使用接口和类。然而，正如我们所看到的，实现 DI 最简单的方法是使用函数。\n\n# 纯功能设计模式\n\n到目前为止，我们已经看到了一些经典的面向对象设计模式是如何变成功能变体的。但是我们能想象源于函数式编程的设计模式吗？\n\n实际上我们已经用过一些了。`map` / `reduce`(或 STL 中的`transform` / `accumulate`)就是一个例子。大多数高阶函数(如`filter`、`all_of`和`any_of`等)也是模式的例子。然而，我们可以走得更远，探索一种来自函数式编程的常见但不透明的设计模式。\n\n最好的理解方法是从具体问题入手。首先，我们将看到如何在不可变的上下文中维护状态。然后，我们将学习设计模式。最后，我们将在另一个环境中看到它的实际应用。\n\n# 维持状态\n\n如何在函数式编程中保持状态？考虑到函数式编程背后的思想之一是不变性，这似乎是一个奇怪的问题，反过来，这似乎阻止了状态的改变。\n\n然而，这种限制是一种错觉。为了理解它，让我们想一想时间是如何流逝的。如果我戴上帽子，我会把我的状态从摘下帽子变成戴上帽子。如果我能一秒一秒地回顾从我伸手拿帽子到戴上帽子的时间，我就能看到我的动作是如何一秒一秒地朝着这个目标前进的。但是我不能改变过去的任何一秒。过去是不可改变的，不管我们喜不喜欢(毕竟，也许我戴着帽子看起来很傻，但我不能还原它)。所以自然让时间以这样一种方式运转，过去是不可改变的，但我们可以改变状态。\n\n我们如何从概念上对此进行建模？嗯，这样想吧——首先，我们有一个初始状态，戴着帽子的亚历克斯，以及一个运动的定义，目的是到达帽子并戴上它。在编程术语中，我们用一个函数来模拟运动。函数接收手的位置和函数本身，并返回手的新位置加上函数。因此，通过复制自然，我们以下面例子中的状态序列结束:\n\n```cpp\nAlex wants to put the hat on\nInitial state: [InitialHandPosition, MovementFunction (HandPosition -> next HandPosition)]\nState1 = [MovementFunction(InitialHandPosition), MovementFunction]\nState2 = [MovementFunction(HandPosition at State1),MovementFunction]...\nStaten = [MovementFunction(HandPosition at Staten-1), MovementFunction]\nuntil Alex has hat on\n```\n\n通过反复应用`MovementFunction`，我们最终得到一系列状态。*每个状态都是不可变的，但是我们可以存储状态*。\n\n现在让我们看一个简单的 C++ 例子。我们可以使用的最简单的例子是自动增量索引。索引需要记住上次使用的值，并使用`increment`函数从索引中返回下一个值。通常，当我们试图使用不可变代码来实现它时，我们会遇到麻烦，但是我们可以用前面描述的方法来实现吗？\n\n我们来看看。首先，我们需要用第一个值初始化自动增量索引——假设它是`1`。像往常一样，我希望检查该值是否被初始化为我期望的值，如下所示:\n\n```cpp\nTEST_CASE(\"Id\"){\n    const auto autoIncrementIndex = initAutoIncrement(1);\n    CHECK_EQ(1, value(autoIncrementIndex)); \n}\n```\n\n注意，既然`autoIncrementIndex`不变，我们可以做成`const`。\n\n我们如何实现`initAutoIncrement`？正如我们所说的，我们需要初始化一个既保存当前值(在这种情况下为`1`)又保存增量函数的结构。我将从这样一对开始:\n\n```cpp\nauto initAutoIncrement = [](const int initialId){\n    function<int(const int)> nextId = [](const int lastId){\n        return lastId + 1;\n    };\n\n    return make_pair(initialId, nextId);\n};\n```\n\n至于之前的`value`函数，只是从对中返回值；它是这对元素中的第一个元素，如下面的代码片段所示:\n\n```cpp\nauto value = [](const auto previous){\n    return previous.first;\n};\n```\n\n现在让我们计算自动增量索引中的下一个元素。我们初始化它，然后计算下一个值，并检查下一个值是否为`2`:\n\n```cpp\nTEST_CASE(\"Compute next auto increment index\"){\n    const auto autoIncrementIndex = initAutoIncrement(1);\n\n    const auto nextAutoIncrementIndex = \n        computeNextAutoIncrement(autoIncrementIndex);\n\n    CHECK_EQ(2, value(nextAutoIncrementIndex)); \n}\n```\n\n请再次注意，两个`autoIncrementIndex`变量都是`const`，因为它们从不变异。我们已经有了价值函数，但是`computeNextAutoIncrement`函数是什么样子的呢？它必须从对中获取当前值和函数，将函数应用于当前值，并在新值和函数之间返回一对:\n\n```cpp\nauto computeNextAutoIncrement = [](pair<const int, function<int(const \n    int)>> current){\n        const auto currentValue = value(current);\n        const auto functionToApply = lambda(current);\n        const int newValue = functionToApply(currentValue);\n        return make_pair(newValue, functionToApply);\n};\n```\n\n我们正在使用一个实用函数`lambda`，它从这一对中返回λ:\n\n```cpp\nauto lambda = [](const auto previous){\n    return previous.second;\n};\n```\n\n这真的有用吗？让我们测试下一个值:\n\n```cpp\nTEST_CASE(\"Compute next auto increment index\"){\n    const auto autoIncrementIndex = initAutoIncrement(1);\n    const auto nextAutoIncrementIndex = \n        computeNextAutoIncrement(autoIncrementIndex);\n    CHECK_EQ(2, value(nextAutoIncrementIndex)); \n\n const auto newAutoIncrementIndex = \n        computeNextAutoIncrement(nextAutoIncrementIndex);\n CHECK_EQ(3, value(newAutoIncrementIndex));\n}\n```\n\n所有的测试都通过了，表明我们刚刚以不可变的方式存储了状态！\n\n既然这个解决方案看起来很简单，那么下一个问题就是——我们能概括它吗？让我们试试。\n\n首先，我们把`pair`换成`struct`。该结构需要有一个值和函数来计算下一个值作为数据成员。这将消除对我们的`value()`和`lambda()`功能的需求:\n\n```cpp\nstruct State{\n    const int value;\n    const function<int(const int)> computeNext;\n};\n```\n\n`int`类型会重复，但为什么要重复呢？一个状态可能比仅仅`int`更复杂，所以让我们把`struct`变成一个模板:\n\n```cpp\ntemplate<typename ValueType>\nstruct State{\n    const ValueType value;\n    const function<ValueType(const ValueType)> computeNext;\n};\n```\n\n这样，我们可以初始化一个自动增量索引并检查初始值:\n\n```cpp\nauto increment = [](const int current){\n    return current + 1;\n};\n\nTEST_CASE(\"Initialize auto increment\"){\n    const auto autoIncrementIndex = State<int>{1, increment};\n\n    CHECK_EQ(1, autoIncrementIndex.value); \n}\n```\n\n最后，我们需要一个函数来计算下一个`State`。函数需要返回一个`State<ValueType>`，所以最好将其封装到`State`结构中。此外，它可以使用当前值，因此无需向其中传递值:\n\n```cpp\ntemplate<typename ValueType>\nstruct State{\n    const ValueType value;\n    const function<ValueType(const ValueType)> computeNext;\n\n State<ValueType> nextState() const{\n return State<ValueType>{computeNext(value), computeNext};\n };\n};\n\n```\n\n有了这个实现，我们现在可以检查自动增量索引的下两个值:\n\n```cpp\nTEST_CASE(\"Compute next auto increment index\"){\n    const auto autoIncrementIndex = State<int>{1, increment};\n\n    const auto nextAutoIncrementIndex = autoIncrementIndex.nextState();\n\n    CHECK_EQ(2, nextAutoIncrementIndex.value); \n\n    const auto newAutoIncrementIndex = \n        nextAutoIncrementIndex.nextState();\n    CHECK_EQ(3, newAutoIncrementIndex.value);\n}\n```\n\n测试通过了，所以代码可以工作了！现在让我们再玩一会儿。\n\n假设我们正在实现一个简单的井字游戏。我们希望使用相同的模式来计算移动后棋盘的下一个状态。\n\n首先，我们需要一个可以容纳 TicTacToe 板的结构。为了简单起见，我将使用`vector<vector<Token>>`，其中`Token`是可以保存`Blank`、`X`或`O`值的`enum`:\n\n```cpp\nenum Token {Blank, X, O};\ntypedef vector<vector<Token>> TicTacToeBoard;\n```\n\n然后，我们需要一个`Move`结构。`Move`结构需要包含移动的棋盘坐标和用于移动的令牌:\n\n```cpp\nstruct Move{\n    const Token token;\n    const int xCoord;\n    const int yCoord;\n};\n```\n\n我们还需要一个函数，可以取一个`TicTacToeBoard`，应用一个移动，返回新板。为了简单起见，我将使用局部变异来实现它，如下所示:\n\n```cpp\nauto makeMove = [](const TicTacToeBoard board, const Move move) -> \n    TicTacToeBoard {\n        TicTacToeBoard nextBoard(board);\n        nextBoard[move.xCoord][move.yCoord] = move.token;\n         return nextBoard;\n};\n```\n\n我们还需要一块空板来初始化我们的`State`。我们就用手填充`Token::Blank`吧:\n\n```cpp\nconst TicTacToeBoard EmptyBoard{\n    {Token::Blank,Token::Blank, Token::Blank},\n    {Token::Blank,Token::Blank, Token::Blank},\n    {Token::Blank,Token::Blank, Token::Blank}\n};\n```\n\n我们想迈出第一步。但是，我们的`makeMove`函数没有`State`结构允许的签名；它需要一个额外的参数，`Move`。对于第一个测试，我们可以将`Move`参数绑定到硬编码值。假设`X`移动到左上角，坐标 *(0，0)* :\n\n```cpp\nTEST_CASE(\"TicTacToe compute next board after a move\"){\n    Move firstMove{Token::X, 0, 0};\n    const function<TicTacToeBoard(const TicTacToeBoard)> makeFirstMove \n        = bind(makeMove, _1, firstMove);\n    const auto emptyBoardState = State<TicTacToeBoard>{EmptyBoard, \n        makeFirstMove };\n    CHECK_EQ(Token::Blank, emptyBoardState.value[0][0]); \n\n    const auto boardStateAfterFirstMove = emptyBoardState.nextState();\n    CHECK_EQ(Token::X, boardStateAfterFirstMove.value[0][0]); \n}\n```\n\n如您所见，我们的`State`结构在这种情况下运行良好。然而，它有一个限制:它只允许一次移动。问题是计算下一阶段的函数不能改变。但是如果我们将它作为参数传递给`nextState()`函数呢？我们最终有了一个新的结构；姑且称之为`StateEvolved`。它保存一个值和一个`nextState()`函数，该函数接受计算下一个状态的函数，应用它，并返回下一个`StateEvolved`:\n\n```cpp\ntemplate<typename ValueType>\nstruct StateEvolved{\n    const ValueType value;\n    StateEvolved<ValueType> nextState(function<ValueType(ValueType)> \n        computeNext) const{\n            return StateEvolved<ValueType>{computeNext(value)};\n    };\n};\n```\n\n我们现在可以通过进入`nextState``makeMove`函数进行移动，其中`Move`参数绑定到实际移动:\n\n```cpp\nTEST_CASE(\"TicTacToe compute next board after a move with \n    StateEvolved\"){\n    const auto emptyBoardState = StateEvolved<TicTacToeBoard>\n        {EmptyBoard};\n    CHECK_EQ(Token::Blank, emptyBoardState.value[0][0]); \n    auto xMove = bind(makeMove, _1, Move{Token::X, 0, 0});\n    const auto boardStateAfterFirstMove = \n        emptyBoardState.nextState(xMove);\n    CHECK_EQ(Token::X, boardStateAfterFirstMove.value[0][0]); \n}\n```\n\n我们现在可以做第二步了。假设`O`在中心移动到坐标 *(1，1)* 。让我们检查前后状态:\n\n```cpp\n    auto oMove = bind(makeMove, _1, Move{Token::O, 1, 1});\n    const auto boardStateAfterSecondMove = \n        boardStateAfterFirstMove.nextState(oMove);\n    CHECK_EQ(Token::Blank, boardStateAfterFirstMove.value[1][1]); \n    CHECK_EQ(Token::O, boardStateAfterSecondMove.value[1][1]); \n```\n\n如您所见，使用这种模式，我们可以以不可变的方式存储任何状态。\n\n# 揭露\n\n我们之前讨论的设计模式对于函数式编程来说似乎非常有用，但是您可能已经意识到我已经避免给它命名了。\n\n事实上，我们到目前为止讨论的模式是一个单子的例子，特别是`State`单子。我一直避免告诉你它的名字，因为单子在软件开发中是一个特别不透明的话题。为了这本书，我在单子上看了几个小时的视频；我也看过博客和文章，出于某种原因，它们都不可理解。由于单子是范畴理论的数学对象，我提到的一些资源采用数学方法，并使用定义和运算符来解释它们。其他资源试图通过例子来解释，但是它们是用对 monad 模式有本地支持的编程语言编写的。它们都不符合我们本书的目标——一种处理复杂概念的实用方法。\n\n为了更好地理解单子，我们需要看更多的例子。最简单的可能是`Maybe`单子。\n\n# 可能\n\n考虑尝试在 C++ 中计算如下表达式:\n\n```cpp\n2  + (3/0) * 5\n```\n\n可能会发生什么？通常情况下，会抛出一个异常，因为我们试图除以`0`。但是，在某些情况下，我们希望看到一个值，如`None`或`NaN`，或某种信息。我们已经看到，我们可以使用`optional<int>`来存储可能是整数或值的数据；因此，我们可以实现一个返回`optional<int>`的除法函数，如下所示:\n\n```cpp\n    function<optional<int>(const int, const int)> divideEvenWith0 = []\n      (const int first, const int second) -> optional<int>{\n        return (second == 0) ? nullopt : make_optional(first / second);\n    };\n```\n\n然而，当我们试图在一个表达式中使用`divideEvenWith0`时，我们意识到我们也需要改变所有其他的运算符。例如，我们可以实现一个`plusOptional`函数，当任一参数为`nullopt`时返回`nullopt`，否则返回值，如下例所示:\n\n```cpp\n    auto plusOptional = [](optional<int> first, optional<int> second) -\n        > optional<int>{\n            return (first == nullopt || second == nullopt) ? \n                nullopt :\n            make_optional(first.value() + second.value());\n    };\n```\n\n虽然它可以工作，但这需要编写更多的函数和大量的重复。但是，嘿，我们能不能写一个函数，把一个`function<int(int, int)>`变成一个`function<optional<int>(optional<int>, optional<int>)>`？当然，让我们按如下方式编写函数:\n\n```cpp\n    auto makeOptional = [](const function<int(int, int)> operation){\n        return [operation](const optional<int> first, const \n            optional<int> second) -> optional<int>{\n            if(first == nullopt || second == nullopt) return nullopt;\n            return make_optional(operation(first.value(), \n                second.value()));\n        };\n    };\n```\n\n这很好，如以下通过的测试所示:\n\n```cpp\n    auto plusOptional = makeOptional(plus<int>());\n    auto divideOptional = makeOptional(divides<int>());\n\n    CHECK_EQ(optional{3}, plusOptional(optional{1}, optional{2}));\n    CHECK_EQ(nullopt, plusOptional(nullopt, optional{2}));\n\n    CHECK_EQ(optional{2}, divideOptional(optional{2}, optional{1}));\n    CHECK_EQ(nullopt, divideOptional(nullopt, optional{1}));\n```\n\n然而，这并不能解决一个问题——我们在除以`0`时仍然需要返回`nullopt`。因此，以下测试将失败，如下所示:\n\n```cpp\n//    CHECK_EQ(nullopt, divideOptional(optional{2}, optional{0}));\n//    cout << \"Result of 2 / 0 = \" << to_string(divideOptional\n        (optional{2}, optional{0})) << endl;\n```\n\n我们可以用自己的`divideEvenBy0`方法代替标准除法来解决这个问题:\n\n```cpp\n    function<optional<int>(const int, const int)> divideEvenWith0 = []\n      (const int first, const int second) -> optional<int>{\n        return (second == 0) ? nullopt : make_optional(first / second);\n    };\n\n```\n\n这一次，测试通过，如下所示:\n\n```cpp\n    auto divideOptional = makeOptional(divideEvenWith0);\n\n    CHECK_EQ(nullopt, divideOptional(optional{2}, optional{0}));\n    cout << \"Result of 2 / 0 = \" << to_string(divideOptional\n        (optional{2}, optional{0})) << endl;\n```\n\n此外，运行测试后的显示如下所示:\n\n```cpp\nResult of 2 / 0 = None\n```\n\n我不得不说，逃避除以`0`的暴政而得到结果，有一种奇怪的满足感。也许那只是我。\n\n不管怎样，这就引出了`Maybe`单子的定义。它存储一个值和一个名为`apply`的函数。`apply`函数接受一个操作(`plus<int>()`、`minus<int>()`、`divideEvenWith0`或`multiplies<int>()`)和我们应用该操作的第二个值，并返回结果:\n\n```cpp\ntemplate<typename ValueType>\nstruct Maybe{\n    typedef function<optional<ValueType>(const ValueType, const \n        ValueType)> OperationType;\n    const optional<ValueType> value;\n\n    optional<ValueType> apply(const OperationType operation, const \n        optional<ValueType> second){\n            if(value == nullopt || second == nullopt) return nullopt;\n            return operation(value.value(), second.value());\n    }\n};\n```\n\n我们可以使用`Maybe`单子进行如下计算:\n\n```cpp\nTEST_CASE(\"Compute with Maybe monad\"){\n    function<optional<int>(const int, const int)> divideEvenWith0 = []\n      (const int first, const int second) -> optional<int>{\n        return (second == 0) ? nullopt : make_optional(first / second);\n    };\n\n    CHECK_EQ(3, Maybe<int>{1}.apply(plus<int>(), 2));\n    CHECK_EQ(nullopt, Maybe<int>{nullopt}.apply(plus<int>(), 2));\n    CHECK_EQ(nullopt, Maybe<int>{1}.apply(plus<int>(), nullopt));\n\n    CHECK_EQ(2, Maybe<int>{2}.apply(divideEvenWith0, 1));\n    CHECK_EQ(nullopt, Maybe<int>{nullopt}.apply(divideEvenWith0, 1));\n    CHECK_EQ(nullopt, Maybe<int>{2}.apply(divideEvenWith0, nullopt));\n    CHECK_EQ(nullopt, Maybe<int>{2}.apply(divideEvenWith0, 0));\n    cout << \"Result of 2 / 0 = \" << to_string(Maybe<int>\n        {2}.apply(divideEvenWith0, 0)) << endl;\n}\n```\n\n再一次，我们可以计算表达式，即使使用`nullopt`。\n\n# 那么什么是单子呢？\n\nA **monad** 是一种模拟计算的功能设计模式。它来自数学；更准确地说，来自名为**范畴论**的领域。\n\n什么是计算？一个基本的计算是一个函数；然而，我们对在函数中添加更多的行为感兴趣。我们已经看到了两个用可选类型维护状态和允许操作的例子，但是单子在软件设计中非常普遍。\n\n一个单子基本上有一个值和一个高阶函数。为了理解它们的作用，让我们比较一下下面代码中显示的`State`单子:\n\n```cpp\ntemplate<typename ValueType>\nstruct StateEvolved{\n    const ValueType value;\n\n    StateEvolved<ValueType> nextState(function<ValueType(ValueType)> \n        computeNext) const{\n            return StateEvolved<ValueType>{computeNext(value)};\n    };\n};\n```\n\n这里显示的`Maybe`单子:\n\n```cpp\ntemplate<typename ValueType>\nstruct Maybe{\n    typedef function<optional<ValueType>(const ValueType, const \n        ValueType)> OperationType;\n    const optional<ValueType> value;\n\n    optional<ValueType> apply(const OperationType operation, const \n        optional<ValueType> second) const {\n            if(value == nullopt || second == nullopt) return nullopt;\n            return operation(value.value(), second.value());\n    }\n};\n```\n\n它们都有价值。该值封装在 monad 结构中。它们都持有一个对该值进行计算的函数。`apply` / `nextState`(文献中称为`bind`)函数本身接收一个封装计算的函数；然而，单子除了计算之外还做一些事情。\n\n单子不仅仅是这些简单的例子。然而，它们展示了如何封装某些计算以及如何移除某些类型的重复。\n\n值得注意的是，C++ 中的`optional<>`类型实际上是受到了`Maybe` monad 的启发，以及承诺，所以您可能已经在代码中使用了等待被发现的 monad。\n\n# 摘要\n\n我们在这一章学到了很多东西，都是围绕着改进设计。我们了解到重构意味着在不改变程序外部行为的情况下重构代码。我们看到，为了确保行为的保存，我们需要进行非常小的步骤和测试。我们了解到遗留代码是我们害怕更改的代码，为了编写测试，我们需要首先更改代码，这导致了一个困境。我们还了解到，幸运的是，我们可以在代码中进行一些小的更改，这些更改可以保证保留行为，但会破坏依赖性，从而允许我们用测试插入代码。我们看到，我们可以使用纯函数来识别和打破依赖关系，从而产生 lambdas，我们可以根据内聚性将 lamb das 重组为类。\n\n最后，我们了解到我们可以在函数式编程中使用设计模式，我们看到了几个例子。即使您不使用函数式编程中的任何其他东西，使用诸如策略、命令或注入的依赖项这样的函数将使您的代码更容易更改，而不会有太多麻烦。我们触及了一个非常抽象的设计模式，单子，我们看到了如何使用`Maybe`单子和`State`单子。这两者都有助于我们用更丰富的功能编写更少的代码。\n\n我们已经讨论了很多关于软件设计的问题。但是函数式编程适用于架构吗？这就是我们将在下一章中访问的内容——事件源。"
  },
  {
    "path": "docs/handson-func-prog-cpp/13.md",
    "content": "# 十三、不变性和架构——事件源\n\n事件源是一种利用存储不变性的架构模式。事件源的基本思想如下——与其存储数据的当前状态，不如存储修改数据的事件？这个想法看似激进，但并不新鲜；事实上，您已经在使用基于这一原则的工具——像 Git 这样的源代码控制系统遵循这一架构。我们将更详细地探讨这个想法，包括讨论它的优点和缺点。\n\n本章将涵盖以下主题:\n\n*   不变性的概念如何应用于数据存储\n*   事件源架构是什么样的\n*   在决定是否使用事件源时要考虑什么\n\n# 技术要求\n\n您将需要一个支持 C++ 17 的编译器。我用的是 GCC 7.4.0。\n\n代码可以在[的 GitHub 上找到。com/ PacktPublishing/动手-函数-用- Cpp 编程`Chapter13`文件夹中的](https://github.%E2%80%8Bcom/PacktPublishing/Hands-On-Functional-Programming-with-Cpp)。它包含并使用了`doctest`，这是一个单头开源单元测试库。你可以在它的 GitHub 资源库[上找到它。com/ onqtam/ doctest](https://github.%E2%80%8Bcom/onqtam/doctest) 。\n\n# 不变性和架构——事件源\n\n直到 2010 年左右，数据存储的选择还相当有限。无论您首选的是 Oracle、MySQL 还是 PostgreSQL，您都必须为数据使用关系模型。\n\n然后，突然，大量新的数据库引擎不知从哪里冒出来，对关系数据的支持部分甚至没有。它们是如此的不同，以至于它们挑战了积极的分类，所以世界最终根据它们没有做的事情来命名它们——NoSQL 数据库。事实上，它们唯一的共同点是对 SQL 的支持少之又少。引擎的列表很长，而且在不断变化，但是在撰写本文时，有几个很流行——Redis、MongoDB、DynamoDb、Cassandra 和 Couchbase 等等。这些引擎中的每一个都有自己的优缺点，它们出现的原因是针对各种场景进行优化，通常是在云计算的背景下。例如，Cassandra 是高度分布式的，而 MongoDB 允许轻松存储多种类型的数据。\n\n大约在我听说 NoSQL 的同时，我开始听说一种新的建筑模式，叫做事件源。与通常的用户界面-服务器-关系数据库管理系统模式相比，事件源采用了完全不同的数据存储方法。事件源模式没有存储系统的当前状态，而是说——为什么我们不将系统的增量变化存储为*域事件*？\n\n机敏的读者会注意到关于这个想法的两点:\n\n*   这听起来像是来自**领域驱动设计**(**【DDD】**)运动的东西，的确如此。领域事件可以是我们使用的另一种模式，作为我们的 DDD 架构方法的一部分，也是我们领域模型发展的一部分。\n*   将增量更改存储在数据存储中的想法虽然对业务应用来说很激进，但在软件架构中并不新鲜。事实上，在本书的整个写作过程中，我一直在使用基于这种模式的工具。您可能还用它来获取代码示例。虽然使用比我们将讨论的事件源更复杂的历史模型，但 Git 将增量更改存储在代码的当前状态旁边。\n\nGit 不是唯一使用这种模式的工具。多年来，我们一直在数据备份操作中使用此类工具。由于完整备份可能需要很长时间，因此一个好的策略是将频繁的增量备份与不频繁的完整备份混合在一起。但是，诀窍在于，当需要恢复时，我们可以一个接一个地应用增量备份，从而获得与完整备份相同的状态。一方面是用于备份的时间和存储，另一方面是恢复备份所需的时间，这是一个很好的权衡。\n\n此时，您可能会想知道事件源除了与存储相关之外，还与 NoSQL 数据库有什么关系？虽然我无法证明，但我相信这两个想法来自 2010 年代围绕编程的同一思潮——通过消除技术障碍来优化开发速度，并为各种基于网络和云的架构优化系统。\n\n让我们思考一下推特。在数据流方面，推特有两个主要功能——发布消息和查看其他人发布的消息。如果您没有立即看到另一个用户发布的消息，您甚至不会知道它，因此允许高延迟。然而，我们不想丢失数据，所以我们需要尽快存储用户消息。\n\n实现类似这样的事情的标准方法是根据请求将消息直接保存到数据库中，并在响应时返回更新的提要。这使我们可以立即看到消息，但它有几个缺点。首先，它使数据库成为瓶颈，因为每个发布的消息都执行一个`INSERT`和一个`SELECT`语句。其次，它需要服务器上更多的资源，从而增加了基于云的服务器的成本。\n\n如果我们有不同的想法呢？当您发布消息时，我们只需将事件保存到快速事件存储中并立即返回。将来请求更新提要时，会考虑该事件并返回更新的提要。数据存储不再是瓶颈，我们已经降低了服务器负载。然而，我们在系统中添加了一个新的元素，事件存储，这可能会花费更多一点，但事实证明，在高规模下，这可能比替代方案更便宜，响应速度也更快。这是事件源的一个例子。\n\n另一种选择是在数据引擎级别解决这个问题，并如前所述将写入和读取分开；然而，我们使用的数据存储是为编写而优化的。不利的一面是，数据可以以比以前更高的延迟读取，但这没关系。在未来的某个时候，它会变为可用，并且消息源会更新。这是一个使用 NoSQL 数据库而不是关系数据库管理系统的例子。\n\n2010 年代确实非常有趣，在将函数式编程引入主流编程语言的同时，在软件架构和设计方面产生了许多新想法。顺便说一句，他们还对从**漫威电影宇宙** ( **MCU** )发布的超级英雄系列电影感兴趣。两者没有联系，我就是喜欢 MCU！然而，我不得不停止 fanboying(关于软件设计和 MCU 的历史)，转到另一个奇怪的想法——将不变性带入数据存储。\n\n# 将不变性带到建筑中\n\n我们已经看到不变性对代码结构有着深远的影响，因此对软件设计也有着深远的影响。我们也多次讨论过，输入/输出从根本上来说是可变的。我们将展示数据存储不一定是可变的，并且不可变的数据存储对架构也有深远的影响。\n\n数据存储如何才能不变？毕竟，许多软件应用的全部原因是做 CRUD——创建、检索、更新和删除。唯一不改变数据的操作是检索，尽管在某些情况下，检索数据会有额外的副作用，如分析或记录。\n\n然而，请记住，我们在数据结构方面面临着同样的问题。可变数据结构在添加或删除元素时会改变其结构。然而，纯函数式语言支持不可变的数据结构。\n\n不可变数据结构具有以下属性—添加或删除项不会改变数据结构。相反，它返回初始数据结构的副本以及更改。为了优化内存，纯函数式编程语言实际上并不克隆数据，它们只是聪明地利用指针来重用现有的内存。然而，对于程序员来说，就好像数据结构已经被完全克隆了一样。\n\n考虑将同样的想法应用于存储。每次写入或删除都会创建一个应用了更改的数据新版本，而不是更改现有数据，同时保持以前的版本不变。想象可能性；我们获得了数据更改的整个历史，并且我们总是可以恢复它们，因为我们有数据的最新版本。\n\n不过，这并不容易。存储的数据往往很大，每次更改时复制数据会消耗大量存储空间，并且在此过程中会变得极其缓慢。与内存数据相同的优化技术效果不太好，因为存储的数据往往更复杂，而指针不是(还没有？)对于文件系统来说同样易于管理。\n\n幸运的是，还有一个替代方法——首先存储一个版本的数据，然后只存储对数据的一些更改。我们可以在关系数据库中实现这一点(毕竟更改只是实体)，但幸运的是，我们不必这样做。为了支持这种存储模式，已经实现了统称为**事件存储**的存储引擎。它们允许我们存储事件，并在需要时获取最新版本的数据。\n\n这样的系统将如何工作？我们需要对领域和领域事件进行建模。让我们以推特为例:\n\n![](img/48cff6f2-742f-4b2e-82f6-cf016f2a3624.png)\n\n如果我们使用传统的数据存储，我们会以某种方式保存实体，但我们希望存储事件，因此我们将拥有一长串增量更改，概念上看起来如下:\n\n```cpp\nCreateUser name:alexboly -> userid 1\nCreateUser name: johndoe -> userid 2\nPostMessage userid: 1, message: 'Hello, world!' -> messageid 1\nPostMessage userid: 2, message: 'Hi @alexboly' -> messageid 2\nCreateNotification userid: 1, notification: \"Message from johndoe\"\nPostMessage userid: 1, message: 'Hi @johndoe' -> messageid 3\nCreateNotification userid: 2, notification: \"Message from alexboly\"\nLikeMessage userid: 2, messageid: 3\n...\n```\n\n在我们继续看一个实现的例子之前，我们需要记住我们讨论的是软件架构，没有一个解决方案是完美的。因此，我们必须停下来考虑一下我们在使用事件源时所做的权衡。\n\n# 活动来源的优势\n\n如果没有优势，我们就不会谈论活动采购。\n\n在概念层面上，领域模型和领域事件可以很容易地在非常快速、轻量级的会话中从领域专家那里提取出来。事件风暴是一个简化的会议，通过技术和领域专家之间的合作，允许我们在几个小时内设计一个复杂的系统。本次活动创造的知识不容小觑；这样的共同理解是知识工作中任何跨领域合作的坚实基础。\n\n在软件设计层面，事件源比其他代码结构更能揭示意图。域操作倾向于隐藏在实体内部；有了事件源，对领域模型的改变是架构的首要和中心。我们实际上可以搜索数据可能经历的所有变化，并获得一个列表——这对于其他代码结构来说是很困难的。\n\n在编码层面，事件源简化了编程。虽然在事件中思考起初可能很困难，但它很快会成为第二天性。这个模型允许我们编写反映最重要业务特性的代码，从而使程序员和产品所有者或客户之间更容易理解。它还巧妙地封装了每种类型的变更，从而简化了我们的测试和代码。\n\n在数据存储层面，事件源允许我们查看对数据所做的更改列表，这对于其他数据存储模型来说是一个极端的壮举。增量备份更适合这种模式，因为它基本上是增量备份。恢复内置于数据存储中，允许我们从任何过去的物化存储开始，并应用所有事件。\n\n此外，事件源允许我们回到过去。如果每个事件都有一个相反的事件，这通常很容易做到，我们可以从结束到某个时间戳播放相反的事件，从而引导我们找到当时的确切数据。\n\n在性能级别上，事件源优化了数据写入，使其对大多数需要快速写入但可以处理读取延迟的应用非常有用(也称为**大多数基于网络的系统**)。\n\n但是没有什么是免费的，那么会出什么问题呢？\n\n# 事件来源的缺点和注意事项\n\n尽管事件源有很多优点，但它可能会成为构建复杂应用的一种流行方式，但它也有一些重要的缺点，你需要在加入之前考虑一下。\n\n# 更改事件模式\n\n第一个问题来自事件源的核心模型——如果我们已经有了一堆数据，还需要改变事件的结构，会怎么样？例如，如果我们需要为每个事件添加时间戳，会怎么样？或者，如果我们需要更改我们的`PostMessage`事件，以包括一个可见性字段，该字段可能只是接收者，只是追随者，或者是所有人呢？\n\n这个问题有解决办法，但每个办法都有自己的问题。一种解决方案是对事件模式进行版本化，并同时拥有多个模式，这种方法可行，但会使具体化变得复杂。另一个解决方案是使用数据迁移脚本来改变过去的事件，但是它打破了不变性的概念，必须正确完成。另一种选择是永远不要更改事件模式，只需添加一个新的事件类型，但这可能会由于多个不推荐使用的事件类型而导致混乱。\n\n# 删除过去的数据\n\n第二个问题是隐私。最近在**欧盟** ( **EU** )通过的**通用数据保护条例**(**【GDPR】**)影响了世界各地的许多软件系统，赋予用户要求从系统中完全删除私有数据的权利。当使用普通数据库时，这相对容易——只需删除与用户标识相关的记录——但是我们如何在事件存储中做到这一点呢？\n\n我们可以从删除与用户相关的所有事件开始。但是我们能做到吗？如果事件具有时间关系，我们可能会遇到问题。例如，设想以下协作编辑文档的场景:\n\n```cpp\nCreateAuthor alexboly => authorid 1\nCreateAuthor johndoe => authorid 2\n...\nAddText index: 2400, authorid:1, text: \"something interesting here.\"\nAddText index: 2427, authorid:2, text: \"yes, that's interesting\" => \n    \"something interesting here. yes that's interesting\"\nDeleteText index: 2400, length: 10, authorid: 1 =>\"interesting here. \n    yes that's interesting\"\n...\n```\n\n如果用户`alexboly`要求我们:\n\n```cpp\nCreateAuthor alexboly => authorid 1\nCreateAuthor johndoe => authorid 2\n...\nAddText index: 2400, authorid:1, text: \"something interesting here.\"\nAddText index: 2427, authorid:2, text: \"yes, that's interesting\" => \n    \"something interesting here. yes that's interesting\"\nDeleteText index: 2400, length: 10, authorid: 1 =>\"interesting here. \n    yes that's interesting\"\n...\n```\n\n你看到问题了吗？如果删除突出显示的事件，我们不仅会丢失文档中的数据，而且索引也不再匹配！因此，将事件应用于空白文档将导致错误或数据损坏。\n\n我们可以做几件事:\n\n*   一种解决方案是删除用户身份，但保留数据。虽然这可以在特定的环境中工作，但这种解决方案取决于删除请求的范围。有一种特殊情况，用户将个人数据(例如，地址、电子邮件地址或身份证号码)添加到文档中。如果我们删除了用户的身份，但还需要删除个人数据，我们将需要扫描所有事件中的个人数据，并删除或替换为相同数量的空白字符。\n*   另一个解决方案是物化数据库，删除数据，并从具有未来事件的新检查点开始。这打破了事件源的核心思想之一——从空存储中重建数据的能力——对于有许多事件或许多删除的系统来说，这可能很困难。不过，有了适当的规划和结构，这是可能的。\n*   第三种解决方案是利用架构优势，为`DeletePrivateData`使用特殊事件。然而，这个事件是不同的，因为它将不得不改变事件存储而不是数据。虽然它符合架构，但它有风险，需要大量测试，因为它会破坏一切。\n*   第四种解决方案是设计事件，使它们不在时间上耦合。理论上，这听起来不错，但我们不得不承认，在实践中，这可能并不总是可能的。在前面的例子中，我们需要文本的某种位置，我要求您找到一种方法来指定独立于现有文本的位置。还要考虑到，我们会在一种罕见的情况下进行这种设计工作，这可能会使所有事件不太容易理解。如果可能的话，改变很小，很好；但是如果没有，你需要自己做决定。\n\n# 一个实现的例子\n\n我们接下来将看一个使用事件源实现的简单例子。我们将从我们的 Twitter 示例开始，并开始编写一些测试。\n\n首先，让我们创建一个用户，用伪代码检查事件存储中的正确事件:\n\n```cpp\nTEST_CASE(\"Create User\"){\n    EventStore eventStore;\n    ...\n    auto alexId = createUser(\"alexboly\", eventStore);\n    ...\n    CHECK_EQ(lastEvent, expectedEvent);\n}\n```\n\n我们需要一些东西来编译这个测试。首先，一个可以存储事件的事件存储，但是我们如何表达一个可以存储的事件呢？我们需要某种能够保存属性名称和值的数据结构。最简单的是一个`map<string, string>`结构，它将属性的名称映射到它们的值。为了看到它的运行，让我们为`CreateUser`创建事件结构:\n\n```cpp\nauto makeCreateUserEvent = [](const string& handle, const int id){\n    return map<string, string>{\n            {\"type\", \"CreateUser\"}, \n            {\"handle\", handle}, \n            {\"id\", to_string(id)}\n    };\n};\n```\n\n`CreateUser`事件有一个类型，`CreateUser`，需要一个手柄，例如`alexboly`，用户需要一个`id`。让我们用`typedef`把它变得更好更明确:\n\n```cpp\ntypedef map<string, string> Event;\nauto makeCreateUserEvent = [](const string& handle, const int id){\n    return Event{\n            {\"type\", \"CreateUser\"}, \n            {\"handle\", handle}, \n            {\"id\", to_string(id)}\n    };\n};\n```\n\n我们现在可以创建我们的`EventStore`。因为它基本上是一个事件列表，让我们使用它:\n\n```cpp\nclass EventStore : public list<Event>{\n    public:\n        EventStore() : list<Event>(){\n        };\n};\n```\n\n因此，现在我们的测试可以使用`EventStore`和`makeCreateUserEvent`功能来检查，在调用`createUser`之后，正确的事件将在事件存储中:\n\n```cpp\nTEST_CASE(\"Create User\"){\n    auto handle = \"alexboly\";\n    EventStore eventStore;\n\n    auto alexId = createUser(handle, eventStore);\n\n    auto expectedEvent = makeCreateUserEvent(handle, alexId);\n    auto event = eventStore.back();\n    CHECK_EQ(event, expectedEvent);\n}\n```\n\n我们现在只需要执行`createUser`就可以让这个测试生效。很简单；调用`makeCreateUserEvent`并将结果添加到`EventStore`。我们需要一个`id`，但是因为我们只有一个元素，现在，让我们使用一个硬编码值`1`:\n\n```cpp\nint id = 1;\nauto createUser = [](string handle, EventStore& eventStore){\n    eventStore.push_back(makeCreateUserEvent(handle, id));\n    return id;\n};\n```\n\n测试通过；现在我们可以执行事件，它们将进入事件存储。\n\n现在让我们看看新用户如何发布消息。我们将需要第二种事件类型`PostMessage`，以及类似的代码基础结构。让我们写测试。首先，我们需要创建一个用户。其次，我们需要创建一个通过`userId`链接到用户的消息。测试如下:\n\n```cpp\nTEST_CASE(\"Post Message\"){\n    auto handle = \"alexboly\";\n    auto message = \"Hello, world!\";\n    EventStore eventStore;\n\n    auto alexId = createUser(handle, eventStore);\n    auto messageId = postMessage(alexId, message, eventStore);\n    auto expectedEvent = makePostMessageEvent(alexId, message, \n        messageId);\n    auto event = eventStore.back();\n    CHECK_EQ(event, expectedEvent);\n}\n```\n\n`makePostMessageEvent`功能将创建一个包含所有所需信息的`Event`结构。它还需要一个类型和`messageId`:\n\n```cpp\nauto makePostMessageEvent = [](const int userId, const string& message, int id){\n    return Event{\n            {\"type\", \"PostMessage\"}, \n            {\"userId\", to_string(userId)}, \n            {\"message\", message},\n            {\"id\", to_string(id)}\n    };\n};\n```\n\n最后，`postMessage`只是将`makePostMessageEvent`的结果加入到`EventStore`中。我们再次需要一个 ID，但是我们只有一条消息，所以我们可以使用相同的 ID，`1`:\n\n```cpp\nauto postMessage = [](const int userId, const string& message, \n    EventStore& eventStore){\n      eventStore.push_back(makePostMessageEvent(userId, message, id));\n      return id;\n};\n```\n\n所以，现在我们有了一个可以发布消息的用户，这一切都是通过事件实现的。这很简单，没有一开始看起来那么难。\n\n然而，这个实现提出了一些有趣的问题。\n\n# 如何检索数据？\n\n首先，如果我想通过用户的手柄或他们的`id`来搜索用户呢？这是推特上的真实使用场景。如果我在带有`@alexboly`的消息中提到另一个用户，应该用手柄`alexboly`向该用户发送通知。另外，我想在时间轴上显示与用户`@alexboly`相关的所有消息。\n\n对此我有两个选择。第一个选项是只存储事件，并在读取数据时运行所有事件。第二个选项是用当前值维护一个域存储，并像任何其他数据库一样查询它。需要注意的是，这些存储中的每一个或两个都可能在内存中，以便快速访问。\n\n不管当前值是缓存的还是计算的，我们都需要一种方法来执行事件并获取它们。我们该怎么做？\n\n让我们写一个测试来描述我们需要什么。在运行一个或多个事件后，我们需要执行这些事件并获取当前值，从而允许我们根据需要检索它们:\n\n```cpp\nTEST_CASE(\"Run events and get the user store\"){\n    auto handle = \"alexboly\";\n    EventStore eventStore;\n\n    auto alexId = createUser(handle, eventStore);\n    auto dataStore = eventStore.play();\n\n    CHECK_EQ(dataStore.users.back(), User(alexId, handle));\n}\n```\n\n为了通过测试，我们需要一些东西。首先，一个`User`域对象，我们将保持非常简单:\n\n```cpp\nclass User{\n    public:\n        int id;\n        string handle;\n        User(int id, string handle): id(id), handle(handle){};\n};\n```\n\n第二，有一个列表`users`的数据存储:\n\n```cpp\nclass DataStore{\n    public:\n        list<User> users;\n};\n```\n\n最后是`play`机制。现在让我们使用一个丑陋的实现:\n\n```cpp\n  class EventStore : public list<Event>{\n    public:\n       DataStore play(){\n            DataStore dataStore;\n            for(Event event :  *this){\n                if(event[\"type\"] == \"CreateUser\"){\n                    dataStore.users.push_back(User(stoi(event[\"id\"]), \n                        event[\"handle\"]));\n                }\n            };\n            return dataStore;\n        };\n}\n```\n\n知道了高阶函数，我们当然可以看到前面片段中的`for`语句可以转化为函数方法。事实上，我们可以通过`CreateUser`类型过滤所有事件，然后通过调用`transform`将每个事件转换为一个实体。首先，让我们提取一些较小的函数。我们需要一个将`CreateUser`事件转化为用户的功能:\n\n```cpp\nauto createUserEventToUser = [](Event event){\n    return User(stoi(event[\"id\"]), event[\"handle\"]);\n};\n```\n\n我们还需要一个按类型过滤事件列表的工具:\n\n```cpp\nauto createUserEventToUser = [](Event event){\n    return User(stoi(event[\"id\"]), event[\"handle\"]);\n};\n```\n\n我们现在可以提取一个`playEvents`函数，该函数获取一个事件列表，按类型过滤它，并运行转换，获得一个实体列表:\n\n```cpp\ntemplate<typename Entity>\nauto playEvents = [](const auto& events, const auto& eventType, \n    auto playEvent){\n      list<Event> allEventsOfType;\n      auto filterEventByThisEventType = bind(filterEventByEventType, \n        _1, eventType);\n      copy_if(events.begin(),events.end(),back_insert_iterator\n        (allEventsOfType), filterEventByThisEventType);\n      list<Entity> entities(allEventsOfType.size());\n      transform(allEventsOfType.begin(), allEventsOfType.end(),    \n        entities.begin(), playEvent); \n      return entities;\n};\n```\n\n我们现在可以在我们的`EventStore`中使用该功能来代替`CreateUser`的治疗，并将其推广到其他事件:\n\n```cpp\nclass EventStore : public list<Event>{\n    public:\n        EventStore() : list<Event>(){\n        };\n        DataStore play(){\n            DataStore dataStore;\n            dataStore.users = playEvents<User>(*this, \"CreateUser\", \n                createUserEventToUser);\n            return dataStore;\n        };\n};\n```\n\n我们现在有了一种基于事件从商店中检索数据的方法。是时候看下一个问题了。\n\n# 参照完整性呢？\n\n到目前为止，我们已经看到使用事件时实体之间的关系是基于 id 的，但是如果我们用错误的`id`来调用事件呢？请看下面代码片段中的例子:\n\n```cpp\nCreateUser handle:alexboly -> id 1\nDeleteUser id: 1\nPostMessage userId: 1, text: \"Hello, world!\" -> user with id 1 doesn't \n                                                exist anymore\n```\n\n我看到了一些解决这个问题的方法:\n\n*   第一个解决方案是运行事件。如果它不会在显示器上产生额外的问题，这将是可行的。在推特上，如果我看到一条消息，我可以导航到发布这条消息的用户。在这种情况下，导航会导致页面不存在。这有问题吗？我会说，对于像推特这样的东西来说，这不是一个大问题，只要它不经常发生，但是你必须在你自己的产品背景下进行判断。\n*   第二种解决方案是在没有任何检查的情况下运行事件，但是运行一个重复的作业来检查引用问题并清除它们(当然是通过事件)。这种方法允许您最终使用事件源清理数据，而不会减慢完整性检查的更新速度。再一次，你需要弄清楚这在你的环境中是否有效。\n*   第三种解决方案是在每次事件运行时运行完整性检查。虽然这可以确保参照完整性，但也会降低速度。\n\n检查可以通过两种方式运行——要么检查数据存储，要么检查事件存储。例如，您可以检查 ID 为`1`的`DeleteUser`从未发生，或者在`CreateUser`之后没有发生(但是您需要用户句柄)。\n\n在为您的应用选择事件源时，请记住这一点！\n\n# 摘要\n\n事件源是一种不可变的数据存储方法，从一个简单的想法开始——如果我们存储所有导致当前状态的事件，而不是存储世界的当前状态，会怎么样？这种方法有许多有趣的优点——能够在时间上向前和向后移动，内置增量备份，以及在时间线而不是状态中思考。它还附带了一些警告——删除过去的数据非常困难，事件模式很难更改，引用完整性往往会变得更加松散。您还需要注意可能的错误，并定义以结构化和可重复的方式处理它们的策略。\n\n我们还看到了如何在 lambdas as events 的帮助下实现简单的事件源架构。我们还可以查看用于存储 lambda 的事件源，因为存储的事件基本上是命令模式，命令模式的最简单实现是 lambda。好奇的读者可以尝试将事件序列化/反序列化为 lambdas，看看它如何改变设计。\n\n像任何架构模式一样，我的建议是仔细考虑权衡，并找到实现带来的最重要挑战的答案。如果你选择尝试活动采购，我也建议你尝试一个生产就绪的活动商店，而不是建立自己的。我们在这一章中所写的内容对于展示活动采购的核心原则和挑战非常有用，但它还远未准备好用于生产。\n\n现在是时候转向 C++ 函数式编程的未来了。在下一章中，我们将浏览 C++ 17 中现有的函数式编程特性，并查看关于 C++ 20 的新闻。"
  },
  {
    "path": "docs/handson-func-prog-cpp/14.md",
    "content": "# 十四、使用范围库的延迟求值\n\n我们在本书中详细讨论了如何从功能的角度思考，以及功能链接和组合如何帮助创建模块化和可组合的设计。然而，我们遇到了一个问题——在我们当前的方法中，大量数据需要从一个集合复制到另一个集合。\n\n幸运的是，埃里克·尼布勒自己开发了一个库，该库支持纯函数编程语言的解决方案——延迟求值。这个名为**范围**的库随后被正式接受为 C++ 20 标准。在本章中，我们将看到如何利用它。\n\n本章将涵盖以下主题:\n\n*   为什么以及什么时候延迟求值是有用的\n*   范围库简介\n*   如何使用范围库使用惰性计算\n\n# 技术要求\n\n您将需要一个支持 C++ 17 的编译器。我用的是 GCC 7.4.0。\n\n代码可以在[的 GitHub 上找到。com/ PacktPublishing/动手-函数-用- Cpp 编程`Chapter14`文件夹中的](https://github.%E2%80%8Bcom/PacktPublishing/Hands-On-Functional-Programming-with-Cpp)。它包括并使用`doctest`，这是一个单头开源单元测试库。你可以在它的 GitHub 资源库[上找到它。com/ onqtam/ doctest](https://github.%E2%80%8Bcom/onqtam/doctest) 。\n\n# 范围库概述\n\n范围库为 C++ 程序员提供了各种有用的新工具。所有这些都是有用的，但是许多对于我们的函数式编程需求来说尤其有用。\n\n但是首先，让我们看看如何设置它。要在 C++ 17 中使用范围库，您需要使用来自[https://ericniebler.github.io/range-v3/](https://ericniebler.github.io/range-v3/)的指令。然后，你只需要包含`all.hpp`头文件:\n\n```cpp\n#include <range/v3/all.hpp>\n```\n\n至于 C++ 20，您只需要包含`<ranges>`头，因为标准中包含了库:\n\n```cpp\n#include <ranges>\n```\n\n但是，如果您在尝试前一行代码时遇到编译错误，不要感到惊讶。在撰写本文时，g++ 的最新版本是 9.1，但是 ranges 库还没有包含在标准中。由于其规模，实现预计会很晚。在此之前，如果你想尝试，你仍然可以使用埃里克·尼布勒的版本。\n\n那么，靶场图书馆提供什么？嗯，这一切都是从范围的概念开始的。范围由开始迭代器和结束迭代器组成。首先，这允许我们在现有集合的基础上添加一个范围。然后，我们可以将一个范围传递给一个需要开始和结束迭代器的算法(如`transform`、`sort`或`accumulate`，从而消除对`begin()`和`end()`的不方便的调用。\n\n有了范围，我们可以构建视图。视图指定我们对通过两个迭代器的部分或全部集合感兴趣，但也允许延迟求值和可组合性。由于视图只是集合顶部的轻量级包装器，我们可以声明一个操作链，而无需实际执行它们，直到需要结果。我们将在下一节详细了解这是如何工作的，但这里有一个简单的例子，它由两个操作组成，这两个操作将首先过滤所有偶数的*，然后过滤 3* 的*倍数的数字，从而过滤集合中所有六的倍数:*\n\n```cpp\nnumbers | ranges::view::filter(isEven) | ranges::view::filter(isMultipleOf3)\n```\n\n在行动的帮助下，突变在范围上也是可能的。操作类似于视图，除了它们就地改变底层容器而不是创建副本。正如我们之前多次讨论过的，我们更喜欢在函数式编程中不变异数据；但是，有些情况下我们可以用这个解决方案优化性能，所以值得一提。这里有一个动作的例子...在行动中:\n\n```cpp\nnumbers |= action::sort | action::take(5);\n```\n\n`|`运算符对于函数程序员来说非常有趣，因为它是一种函数组合运算符。对于非常习惯于合成操作的 Unix/Linux 用户来说，使用也是很自然的。正如我们在[第四章](04.html)、*功能组合的思想*中所看到的，这样的操作符会非常有用。不幸的是，它还不支持任何两个功能的组合——只有视图和操作。\n\n最后，范围库支持自定义视图。这打开了诸如数据生成的可能性，这对于许多事情都是有用的，但是[第 11 章](11.html)、*基于属性的测试、*尤其有用。\n\n让我们通过示例更详细地了解范围库的特性。\n\n# 懒惰评价\n\n在过去的章节中，我们已经看到了如何通过利用数据结构上的小转换，以功能的方式来构造代码。让我们举一个简单的例子——计算列表中所有偶数的总和。结构化编程方法是编写一个循环，遍历整个结构并添加所有均匀的元素:\n\n```cpp\nint sumOfEvenNumbersStructured(const list<int>& numbers){\n    int sum = 0;\n    for(auto number : numbers){\n        if(number % 2 == 0) sum += number;\n    }\n    return sum;\n};\n```\n\n这个函数的测试在一个简单的例子中正确运行:\n\n```cpp\nTEST_CASE(\"Run events and get the user store\"){\n    list<int> numbers{1, 2, 5, 6, 10, 12, 17, 25};\n\n    CHECK_EQ(30, sumOfEvenNumbersStructured(numbers));\n}\n```\n\n当然，这种方法会变异数据，我们已经看到这并不总是一个好主意。它同时也做了太多的事情。我们宁愿合成更多的函数。需要的第一个函数决定一个数是否为偶数:\n\n```cpp\nauto isEven = [](const auto number){\n    return number % 2 == 0;\n};\n```\n\n第二个从集合中挑选满足谓词的数字:\n\n```cpp\nauto pickNumbers  = [](const auto& numbers, auto predicate){\n    list<int> pickedNumbers;\n    copy_if(numbers.begin(), numbers.end(), \n        back_inserter(pickedNumbers), predicate);\n    return pickedNumbers;\n};\n```\n\n第三种方法计算集合中所有元素的总和:\n\n```cpp\nauto sum = [](const auto& numbers){\n    return accumulate(numbers.begin(), numbers.end(), 0);\n};\n```\n\n这就引出了最终的实现，它包含了所有这些功能:\n\n```cpp\nauto sumOfEvenNumbersFunctional = [](const auto& numbers){\n    return sum(pickNumbers(numbers, isEven));\n};\n```\n\n然后它通过测试，就像结构化解决方案一样:\n\n```cpp\nTEST_CASE(\"Run events and get the user store\"){\n    list<int> numbers{1, 2, 5, 6, 10, 12, 17, 25};\n\n    CHECK_EQ(30, sumOfEvenNumbersStructured(numbers));\n    CHECK_EQ(30, sumOfEvenNumbersFunctional(numbers));\n}\n```\n\n函数式解决方案有明显的优势——它很简单，由可以重组的小函数组成，并且是不可变的，这也意味着它可以并行运行。然而，它确实有一个缺点——它复制数据。\n\n我们在[第 10 章、](10.html) *性能优化*中已经看到了如何处理这个问题，但事实是最简单的解决方法就是懒评价。想象一下，如果我们可以链接函数调用，但是代码直到我们需要它的结果时才会真正执行，这将意味着什么。这个解决方案提供了编写我们需要的代码的可能性，以及我们需要它的方式，编译器最大限度地优化了函数链。\n\n这就是范围库正在做的事情和其他事情。\n\n# 使用范围库的延迟求值\n\n靶场图书馆提供了一个名为“T2”的设施。视图允许从迭代器构建不可变且廉价的数据范围。他们不复制数据，只是参考数据。我们可以使用`view`来过滤我们集合中的所有偶数:\n\n```cpp\nranges::view::filter(numbers, isEven)\n```\n\n不需要任何复制，使用合成操作符`|`，就可以合成视图。例如，我们可以通过组成两个过滤器来获得可被`6`整除的数字列表:第一个过滤器是关于偶数的，第二个过滤器是关于可被`3`整除的数字的。给定一个检查一个数是否是`3`倍数的新谓词，我们使用以下内容:\n\n```cpp\nauto isMultipleOf3 = [](const auto number){\n    return number % 3 == 0;\n};\n```\n\n我们通过以下组合获得可被`6`整除的数字列表:\n\n```cpp\nnumbers | ranges::view::filter(isEven) | ranges::view::filter(isMultipleOf3)\n```\n\n需要注意的是，在编写这段代码时，实际上没有计算任何东西。视图已初始化，正在等待命令。因此，让我们计算视图中元素的总和:\n\n```cpp\nauto sumOfEvenNumbersLazy = [](const auto& numbers){\n    return ranges::accumulate(ranges::view::\n        filter(numbers, isEven), 0);\n};\nTEST_CASE(\"Run events and get the user store\"){\n    list<int> numbers{1, 2, 5, 6, 10, 12, 17, 25};\n\n    CHECK_EQ(30, sumOfEvenNumbersLazy(numbers));\n}\n```\n\n`ranges::accumulate`函数是一个特殊的累积实现，它知道如何处理视图。只有在调用`accumulate`时，视图才是代理的；此外，实际上没有复制任何数据——取而代之的是，范围使用智能迭代器来计算结果。\n\n让我们也看看组合视图的结果。不出所料，向量中所有可被`6`整除的数之和为`18`:\n\n```cpp\nauto sumOfMultiplesOf6 = [](const auto& numbers){\n    return ranges::accumulate(\n            numbers | ranges::view::filter(isEven) | \n                ranges::view::filter(isMultipleOf3), 0);\n};\nTEST_CASE(\"Run events and get the user store\"){\n    list<int> numbers{1, 2, 5, 6, 10, 12, 17, 25};\n\n    CHECK_EQ(18, sumOfMultiplesOf6(numbers));\n}\n```\n\n多好的编写代码的方法啊！它比前面的两个选项都容易得多，同时内存占用也很低。\n\n但这并不是所有的范围都能做到的。\n\n# 随着行动而变化\n\n除了视图，范围库还提供操作。行动允许急切的、易变的操作。例如，要对同一向量中的值进行排序，我们可以使用以下语法:\n\n```cpp\nTEST_CASE(\"Sort numbers\"){\n    vector<int> numbers{1, 12, 5, 20, 2, 10, 17, 25, 4};\n    vector<int> expected{1, 2, 4, 5, 10, 12, 17, 20, 25};\n\n    numbers |= ranges::action::sort;\n\n    CHECK_EQ(expected, numbers);\n}\n```\n\n`|=`运算符类似于`ranges::action::sort(numbers)`调用，将向量排序到位。动作也是可组合的，或者通过直接的方法调用，或者通过`|`操作符。这允许我们编写代码，通过组合`sort`和`unique`操作以及`|`操作，对容器中的唯一项目进行分类和保存:\n\n```cpp\nTEST_CASE(\"Sort numbers and pick unique\"){\n    vector<int> numbers{1, 1, 12, 5, 20, 2, 10, 17, 25, 4};\n    vector<int> expected{1, 2, 4, 5, 10, 12, 17, 20, 25};\n\n    numbers |= ranges::action::sort | ranges::action::unique;\n\n    CHECK_EQ(expected, numbers);\n}\n```\n\n然而，这并不是 ranges 所能做的一切。\n\n# 无穷级数和数据生成\n\n由于视图是延迟求值的，它们允许我们创建无限系列。例如，要生成一系列整数，我们可以使用`view::ints`函数。然后，我们需要限制系列，这样我们就可以使用`view::take`来保留系列的前五个元素:\n\n```cpp\nTEST_CASE(\"Infinite series\"){\n    vector<int> values = ranges::view::ints(1) | ranges::view::take(5);\n    vector<int> expected{1, 2, 3, 4, 5};\n\n    CHECK_EQ(expected, values);\n}\n```\n\n对于任何允许增量的类型，可以使用`view::iota`进行额外的数据生成，例如`chars`:\n\n```cpp\nTEST_CASE(\"Infinite series\"){\n    vector<char> values = ranges::view::iota('a') | \n        ranges::view::take(5);\n    vector<char> expected{'a', 'b', 'c', 'd', 'e'};\n\n    CHECK_EQ(expected, values);\n}\n```\n\n此外，您可以使用`linear_distribute`视图生成线性分布的值。给定一个值区间和要包括在线性分布中的多个项目，视图包括两个区间边界，以及来自区间内部的足够的值。例如，从[ `1`、`10` ]区间取 5 个线性分布的值，得出值`{1, 3, 5, 7, 10}`:\n\n```cpp\nTEST_CASE(\"Linear distributed\"){\n    vector<int> values = ranges::view::linear_distribute(1, 10, 5);\n    vector<int> expected{1, 3, 5, 7, 10};\n\n    CHECK_EQ(expected, values);\n}\n```\n\n如果我们需要更复杂的数据生成器呢？幸运的是，我们可以创建自定义范围。假设我们想从`1`开始创建`2`的每十分之一次方的列表(即*2<sup>1</sup>T7】*2<sup>11</sup>T11】*2<sup>21</sup>T15】等等)。我们可以通过转换调用来做到这一点；但是，我们也可以使用`yield_if`功能结合`for_each`视图来实现这一点。下面代码中的粗体行向您展示了如何将这两者结合使用:***\n\n```cpp\nTEST_CASE(\"Custom generation\"){\n    using namespace ranges;\n    vector<long> expected{ 2, 2048, 2097152, 2147483648 };\n\n auto everyTenthPowerOfTwo = view::ints(1) | view::for_each([](int \n        i){ return yield_if(i % 10 == 1, pow(2, i)); });\n    vector<long> values = everyTenthPowerOfTwo | view::take(4);\n\n    CHECK_EQ(expected, values);\n}\n```\n\n我们首先从`1`开始生成一个无限长的整数序列。然后，对于它们中的每一个，我们检查除以`10`的值是否有余数`1`。如果是这样，我们将`2`恢复到该功率。为了得到一个有限向量，我们把前面的无穷级数输入到`take`视图中，该视图只保留前四个元素。\n\n当然，这种类型的生成并不是最优的。每一个有用的数字，我们都需要访问`10`，最好从一个范围开始，到`1`、`11`、`21`等等。\n\n这里值得一提的是，编写这段代码的替代方法是使用 stride 视图。视图从一个系列中获取每 n 个<sup xmlns:epub=\"http://www.idpf.org/2007/ops\">元素，正如我们需要的那样。结合`transform`视图，我们可以获得完全相同的结果:</sup>\n\n```cpp\nTEST_CASE(\"Custom generation\"){\n    using namespace ranges;\n    vector<long> expected{ 2, 2048, 2097152, 2147483648 };\n\n auto everyTenthPowerOfTwo = view::ints(1) | view::stride(10) | \n        view::transform([](int i){ return pow(2, i); });\n    vector<long> values = everyTenthPowerOfTwo | view::take(4);\n\n    CHECK_EQ(expected, values);\n}\n```\n\n到目前为止，您可能已经意识到数据生成对于测试非常有趣，尤其是基于属性的测试(正如我们在[第 11 章](11.html)、*基于属性的测试*中所讨论的)。然而，为了测试，我们经常需要生成字符串。让我们看看如何。\n\n# 生成字符串\n\n要生成字符串，首先，我们需要生成字符。对于 ASCII 字符，我们可以从`32`到`126`的整数范围开始，也就是有趣的可打印字符的 ASCII 码。我们随机抽取一个样本，将代码转换成字符。我们如何随机抽取样本？嗯，有一种观点叫做`view::sample`，给定一些项目，从范围中随机抽取样本。最后，我们只需要把它变成一个字符串。这就是我们如何得到由 ASCII 字符组成的长度为`10`的随机字符串:\n\n```cpp\nTEST_CASE(\"Generate chars\"){\n    using namespace ranges;\n\n    vector<char> chars = view::ints(32, 126) | view::sample(10) | \n        view::transform([](int asciiCode){ return char(asciiCode); });\n    string aString(chars.begin(), chars.end()); \n\n    cout << aString << endl;\n\n    CHECK_EQ(10, aString.size());\n}\n```\n\n下面是运行这段代码的几个示例:\n\n```cpp\n%.0FL[cqrt\n#0bfgiluwy\n4PY]^_ahlr\n;DJLQ^bipy\n```\n\n如您所见，这些是在我们的测试中使用的有趣字符串。此外，我们可以通过改变`view::sample`的参数来改变字符串的大小。\n\n本示例仅限于 ASCII 字符。然而，随着对 UTF-8 的支持现在成为 C++ 标准的一部分，扩展到支持特殊字符应该很容易。\n\n# 摘要\n\nEric Niebler 的范围库在软件工程领域是一个罕见的壮举。它设法简化了现有 STL 高阶函数的使用，同时增加了延迟求值，并在数据生成方面占据了首要位置。它不仅是 C++ 20 标准的一部分，而且对旧版本的 C++ 也很有用。\n\n即使您没有使用函数式的方式来构造代码，无论您喜欢可变的还是不可变的代码，范围库都允许您使它变得优雅和可组合。因此，我建议您玩它，并自己尝试它如何改变您的代码。这绝对是值得的，也是一次愉快的锻炼。\n\n我们接近这本书的结尾了。现在是时候看看 STL 和支持函数式编程的语言标准，以及我们对 C++ 20 的期望了，这将是下一章的主题。"
  },
  {
    "path": "docs/handson-func-prog-cpp/15.md",
    "content": "# 十五、STL 支持和建议\n\n自 90 年代以来，**标准模板库** ( **STL** )一直是 C++ 程序员的有用伴侣。从泛型编程和值语义这样的概念开始，它已经成长为支持许多有用的场景。在本章中，我们将了解 STL 如何支持 C++ 17 中的函数式编程，并了解 C++ 20 中引入的一些新特性。\n\n本章将涵盖以下主题:\n\n*   使用`<functional>`标题中的功能特性\n*   使用`<numeric>`标题中的功能特性\n*   使用`<algorithm>`标题中的功能特性\n*   `std::optional`和`std::variant`\n*   C++ 20 和范围库\n\n# 技术要求\n\n您将需要一个支持 C++ 17 的编译器。我用的是 GCC 7.4.0c.\n\n代码在[的 GitHub 上。com/ PacktPublishing/动手-函数-用- Cpp 编程`Chapter15`文件夹中的](https://github.%E2%80%8Bcom/PacktPublishing/Hands-On-Functional-Programming-with-Cpp)。它包含并使用了`doctest`，这是一个单头开源单元测试库。你可以在它的 GitHub 存储库中找到它:这里: [https:/ /github。com/ onqtam/ doctest](https://github.%E2%80%8Bcom/onqtam/doctest) 。\n\n# <functional>表头</functional>\n\n我们需要从某个地方开始探索 STL 中的函数式编程支持，标题名为`<functional>`似乎是一个好的开始。该标题定义了基本的`function<>`类型，我们可以将其用于函数，并且在本书中已经多次用于 lambdas:\n\n```cpp\nTEST_CASE(\"Identity function\"){\n    function<int(int)> identity = [](int value) { return value;};\n\n    CHECK_EQ(1, identity(1));\n}\n```\n\n我们可以使用`function<>`类型来存储任何类型的函数，无论是自由函数、成员函数还是 lambda。让我们看一个自由函数的例子:\n\n```cpp\nTEST_CASE(\"Free function\"){\n    function<int()> f = freeFunctionReturns2;\n\n    CHECK_EQ(2, f());\n}\n```\n\n下面是一个成员函数的例子:\n\n```cpp\nclass JustAClass{\n    public:\n        int functionReturns2() const { return 2; };\n};\n\nTEST_CASE(\"Class method\"){\n    function<int(const JustAClass&)> f = &JustAClass::functionReturns2;\n    JustAClass justAClass;\n\n    CHECK_EQ(2, f(justAClass));\n}\n```\n\n如您所见，为了通过`function<>`类型调用成员函数，需要传入对对象的有效引用。把它想象成`*this`的例子。\n\n除了这种基本类型之外，`<functional>`头提供了一些已经定义的函数对象，当在集合上使用函数转换时，这些对象会派上用场。让我们看一个简单的例子，结合使用`sort`算法和定义的`greater`函数，以降序对向量进行排序:\n\n```cpp\nTEST_CASE(\"Sort with predefined function\"){\n    vector<int> values{3, 1, 2, 20, 7, 5, 14};\n    vector<int> expectedDescendingOrder{20, 14, 7, 5, 3,  2, 1};\n\n    sort(values.begin(), values.end(), greater<int>());\n\n    CHECK_EQ(expectedDescendingOrder, values);\n}\n```\n\n`<functional>`标题定义了以下有用的功能对象:\n\n*   **算术运算** : `plus`、`minus`、`multiplies`、`divides`、`modulus`和`negate`\n*   **对比** : `equal_to`、`not_equal_to`、`greater`、`less`、`greater_equal`和`less_equal`\n*   **逻辑运算** : `logical_and`、`logical_or`和`logical_not`\n*   **逐位操作** : `bit_and`、`bit_or`和`bit_xor`\n\n当我们需要使用高阶函数时，这些函数对象免去了我们将常见操作封装在函数中的麻烦。虽然这是一个很棒的集合，但我敢说身份函数也同样有用，尽管听起来很奇怪。幸运的是，实现一个很容易。\n\n然而，这并不是`<functional>`标题所能提供的全部。`bind`功能实现部分功能应用。我们在本书中已经多次看到它在行动中的运用，在[第五章](05.html)、*局部运用和 Currying* 中可以详细看到它的用法。它的基本功能是取一个函数，将一个或多个参数绑定到值，并获得一个新的函数:\n\n```cpp\nTEST_CASE(\"Partial application using bind\"){\n    auto add = [](int first, int second){\n        return first + second;\n    };\n\n    auto increment = bind(add, _1, 1);\n\n    CHECK_EQ(3, add(1, 2));\n    CHECK_EQ(3, increment(2));\n}\n```\n\n由于`function<>`类型允许我们编写 lambdas，预定义的函数对象减少了重复，而`bind`允许部分应用，我们有了以函数方式构造代码的基础。但是如果没有高阶函数，我们就无法做到这一点。\n\n# <algorithm>表头</algorithm>\n\n`<algorithm>`头文件包含算法，其中一些算法实现为高阶函数。在这本书里，我们已经看到了许多使用它们的例子。以下是一些有用的算法:\n\n*   `all_of`、`any_of`和`none_of`\n*   `find_if`和`find_if_not`\n*   `count_if`\n*   `copy_if`\n*   `generate_n`\n*   `sort`\n\n我们已经看到，关注数据并结合这些高阶函数，将输入数据转换为所需的输出，是您在小型、可组合的纯函数中思考的方式之一。我们还看到了这种方法的缺点——需要复制数据，或者对同一数据进行多次传递——我们还看到了新的范围库如何以优雅的方式解决这些问题。\n\n虽然所有这些函数都非常有用，但是`<algorithm>`命名空间中有一个函数值得特别一提——函数`map`操作的实现，`transform`。`transform`函数获取一个输入集合，并对集合中的每个元素应用一个λ，返回一个新的集合，该集合具有相同数量的元素，但其中存储了转换后的值。这为根据我们的需求调整数据结构打开了无限的可能性。我们来看几个例子。\n\n# 从集合中投影每个对象的一个属性\n\n我们经常需要从集合中的每个元素获取属性值。在下面的例子中，我们使用`transform`从一个向量中获得所有人名的列表:\n\n```cpp\nTEST_CASE(\"Project names from a vector of people\"){\n    vector<Person> people = {\n        Person(\"Alex\", 42),\n        Person(\"John\", 21),\n        Person(\"Jane\", 14)\n    };\n\n    vector<string> expectedNames{\"Alex\", \"John\", \"Jane\"};\n    vector<string> names = transformAll<vector<string>>(\n            people, \n            [](Person person) { return person.name; } \n    );\n\n    CHECK_EQ(expectedNames, names);\n}\n```\n\n我们再次在`transform`和`transformAll`上使用包装器，以避免编写样板代码:\n\n```cpp\ntemplate<typename DestinationType>\nauto transformAll = [](auto source, auto lambda){\n    DestinationType result;\n    transform(source.begin(), source.end(), back_inserter(result), \n        lambda);\n    return result;\n};\n```\n\n# 计算条件\n\n有时，我们需要计算一个条件是否适用于一组元素。在下面的例子中，我们将通过比较年龄和`18` :\n来计算人们是否是未成年人\n\n```cpp\nTEST_CASE(\"Minor or major\"){\n    vector<Person> people = {\n        Person(\"Alex\", 42),\n        Person(\"John\", 21),\n        Person(\"Jane\", 14)\n    };\n\n    vector<bool> expectedIsMinor{false, false, true};\n    vector<bool> isMinor = transformAll<vector<bool>>(\n            people, \n            [](Person person) { return person.age < 18; } \n    );\n\n    CHECK_EQ(expectedIsMinor, isMinor);\n}\n```\n\n# 将所有内容转换为可显示或可序列化的格式\n\n我们经常需要保存或显示一个列表。为此，我们需要将列表的每个元素转换为可显示或可序列化的格式。在下面的例子中，我们正在计算列表中`Person`对象的 JSON 表示:\n\n```cpp\nTEST_CASE(\"String representation\"){\n    vector<Person> people = {\n        Person(\"Alex\", 42),\n        Person(\"John\", 21),\n        Person(\"Jane\", 14)\n    };\n\n    vector<string> expectedJSON{\n        \"{'person': {'name': 'Alex', 'age': '42'}}\",\n        \"{'person': {'name': 'John', 'age': '21'}}\",\n        \"{'person': {'name': 'Jane', 'age': '14'}}\"\n    };\n    vector<string> peopleAsJson = transformAll<vector<string>>(\n            people, \n            [](Person person) { \n            return \n            \"{'person': {'name': '\" + person.name + \"', 'age': \n                '\" + to_string(person.age) + \"'}}\"; } \n    );\n\n    CHECK_EQ(expectedJSON, peopleAsJson);\n}\n```\n\n即使`transform`函数提供了无限的可能性，它在与`reduce`(【c++ 中的 T2】)高阶函数的结合中变得更加强大。\n\n# <numeric>表头–累计</numeric>\n\n有趣的是，形成函数式编程中最常见的模式之一`map` / `reduce`模式的两个高阶函数最终出现在 C++ 中的两个不同头文件中。`transform` / `accumulate`组合需要`<algorithm>`和`<numeric>`头文件，允许我们解决具有以下模式的许多问题:\n\n*   提供了一个集合。\n*   该系列需要转化为其他产品。\n*   需要计算聚合结果。\n\n我们来看几个例子。\n\n# 计算购物车的含税总价\n\n假设我们有一个`Product`结构，如下所示:\n\n```cpp\nstruct Product{\n    string name;\n    string category;\n    double price;\n    Product(string name, string category, double price): name(name), \n        category(category), price(price){}\n};\n```\n\n我们还假设我们根据产品类别有不同的税收水平:\n\n```cpp\nmap<string, int> taxLevelByCategory = {\n    {\"book\", 5},\n    {\"cosmetics\", 20},\n    {\"food\", 10},\n    {\"alcohol\", 40}\n};\n```\n\n假设我们得到了一个产品列表，如下所示:\n\n```cpp\n    vector<Product> products = {\n        Product(\"Lord of the Rings\", \"book\", 22.50),\n        Product(\"Nivea\", \"cosmetics\", 15.40),\n        Product(\"apple\", \"food\", 0.30),\n        Product(\"Lagavulin\", \"alcohol\", 75.35)\n    };\n\n```\n\n让我们计算一下含税和不含税的总价。我们还有一个助手包装器`accumulateAll`，供我们使用:\n\n```cpp\nauto accumulateAll = [](auto collection, auto initialValue,  auto \n    lambda){\n        return accumulate(collection.begin(), collection.end(), \n            initialValue, lambda);\n};\n```\n\n要计算不含税的价格，我们只需要把所有的产品价格加起来。这是典型的`map` / `reduce`场景:\n\n```cpp\n   auto totalWithoutTax = accumulateAll(transformAll<vector<double>>\n        (products, [](Product product) { return product.price; }), 0.0, \n            plus<double>());\n     CHECK_EQ(113.55, doctest::Approx(totalWithoutTax));\n```\n\n首先，我们将`Products`的列表`map` ( `transform`)转换成价格列表，然后将`reduce`(或`accumulate`)转换成单个值——它的总值。\n\n当我们需要含税的总价格时，类似的，尽管更复杂的过程也适用:\n\n```cpp\n    auto pricesWithTax = transformAll<vector<double>>(products, \n            [](Product product){\n                int taxPercentage = \n                    taxLevelByCategory[product.category];\n                return product.price + product.price * \n                    taxPercentage/100;\n            });\n    auto totalWithTax = accumulateAll(pricesWithTax, 0.0, \n        plus<double> ());\n    CHECK_EQ(147.925, doctest::Approx(totalWithTax));\n```\n\n首先我们`map` ( `transform`)把`Products`的清单跟含税的价格清单联系起来，然后`reduce`(或者`accumulate`)把所有的数值都跟含税的总额联系起来。\n\n如果你想知道的话，`doctest::Approx`函数允许在有小舍入误差的浮点数之间进行比较。\n\n# 将列表转换为 JSON\n\n在前一节中，我们看到了如何通过`transform`调用将列表中的每一项转换为 JSON。借助`accumulate`很容易将其转化为完整的 JSON 列表:\n\n```cpp\n    string expectedJSONList = \"{people: {'person': {'name': 'Alex', \n        'age': '42'}}, {'person': {'name': 'John', 'age': '21'}}, \n            {'person': {'name': 'Jane', 'age': '14'}}}\"; \n    string peopleAsJSONList = \"{people: \" + accumulateAll(peopleAsJson, \n        string(),\n            [](string first, string second){\n                return (first.empty()) ? second : (first + \", \" + \n                    second);\n            }) + \"}\";\n    CHECK_EQ(expectedJSONList, peopleAsJSONList);\n```\n\n我们使用`transform`将人员列表变成每个对象的 JSON 表示的列表，然后使用`accumulate`将它们连接起来，并使用一些额外的操作在 JSON 中添加列表表示的前面和后面。\n\n如你所见，`transform` / `accumulate`(或`map` / `reduce`)组合有很多不同的用途，这取决于我们传递给它的功能。\n\n# 返回<algorithm>–查找 _if 并复制 _if</algorithm>\n\n我们可以用`transform`、`accumulate`、`any_of` / `all_of` / `none_of`完成很多事情。然而，有时我们需要从集合中过滤掉一些数据。\n\n通常的做法是`find_if`。然而，如果我们需要从一个集合中找到符合特定条件的所有项目，那么`find_if`就很麻烦。因此，使用 C++ 17 标准以函数方式解决这个问题的最佳选择是`copy_if`。以下示例使用`copy_if`查找人员列表中的所有未成年人:\n\n```cpp\nTEST_CASE(\"Find all minors\"){\n    vector<Person> people = {\n        Person(\"Alex\", 42),\n        Person(\"John\", 21),\n        Person(\"Jane\", 14),\n        Person(\"Diana\", 9)\n    };\n\n    vector<Person> expectedMinors{Person(\"Jane\", 14), \n                                  Person(\"Diana\", 9)};\n\n    vector<Person> minors;\n    copy_if(people.begin(), people.end(), back_inserter(minors), []\n        (Person& person){ return person.age < 18; });\n\n    CHECK_EQ(minors, expectedMinors);\n}\n```\n\n# <optional>和<variant></variant></optional>\n\n我们已经讨论了很多 happy path 案例，即数据对我们的数据转换有效的时候。我们如何处理边缘情况和错误？当然，在例外情况下，我们可以抛出异常或返回错误情况，但是当我们需要返回错误消息时，情况会怎样呢？\n\n在这些情况下，函数方式是返回数据结构。毕竟，即使输入无效，我们也需要返回一个输出值。但是我们遇到了一个挑战——在错误的情况下，我们需要返回的类型是错误类型，而在有效数据的情况下，我们需要返回的类型是一些更有效的数据。\n\n幸运的是，在这些情况下，我们有两种结构支持我们——`std::optional`和`std::variant`。让我们举一个人员列表的例子，其中一些是有效的，另一些是无效的:\n\n```cpp\n    vector<Person> people = {\n        Person(\"Alex\", 42),\n        Person(\"John\", 21),\n        Person(\"Jane\", 14),\n        Person(\"Diana\", 0)\n    };\n```\n\n最后一个人的年龄无效。让我们试着用函数的方式编写代码，显示以下字符串:\n\n```cpp\nAlex, major\nJohn, major\nJane, minor\nInvalid person\n```\n\n为了有一个转换链，我们需要使用`optional`类型，如下所示:\n\n```cpp\nstruct MajorOrMinorPerson{\n    Person person;\n    optional<string> majorOrMinor;\n\n    MajorOrMinorPerson(Person person, string majorOrMinor) : \n        person(person), majorOrMinor(optional<string>(majorOrMinor)){};\n\n    MajorOrMinorPerson(Person person) : person(person), \n        majorOrMinor(nullopt){};\n};\n    auto majorMinorPersons = transformAll<vector<MajorOrMinorPerson>>\n        (people, [](Person& person){ \n            if(person.age <= 0) return MajorOrMinorPerson(person);\n            if(person.age > 0 && person.age < 18) return \n                MajorOrMinorPerson(person, \"minor\");\n            return MajorOrMinorPerson(person, \"major\");\n            });\n```\n\n通过这个调用，我们获得了这个人和一个值之间的配对列表，该值可以是`nullopt`、`minor`或`major`。我们可以在下面的`transform`调用中使用它，以便根据有效性条件获取字符串列表:\n\n```cpp\n    auto majorMinorPersonsAsString = transformAll<vector<string>>\n        (majorMinorPersons, [](MajorOrMinorPerson majorOrMinorPerson){\n            return majorOrMinorPerson.majorOrMinor ? \n            majorOrMinorPerson.person.name + \", \" + \n                majorOrMinorPerson.majorOrMinor.value() :\n                    \"Invalid person\";\n            });\n```\n\n最后，对累加的调用创建了预期的输出字符串:\n\n```cpp\n    auto completeString = accumulateAll(majorMinorPersonsAsString, \n        string(), [](string first, string second){\n            return first.empty() ? second : (first + \"\\n\" + second);\n            });\n```\n\n我们可以通过一个测试来检验这一点:\n\n```cpp\n    string expectedString(\"Alex, major\\nJohn, major\\nJane, \n                                    minor\\nInvalid person\");\n\n    CHECK_EQ(expectedString, completeString);\n```\n\n另一种方法是使用`variant`，如果我们需要，例如，返回一个错误代码结合人。\n\n# C++ 20 和范围库\n\n我们在[第 14 章](14.html)、*使用范围库*的延迟求值中详细讨论了范围库。如果您可以使用它，或者因为您使用 C++ 20，或者因为您可以将它用作第三方库，那么前面的函数将变得极其简单，而且速度更快:\n\n```cpp\nTEST_CASE(\"Ranges\"){\n    vector<Person> people = {\n        Person(\"Alex\", 42),\n        Person(\"John\", 21),\n        Person(\"Jane\", 14),\n        Person(\"Diana\", 0)\n    };\n    using namespace ranges;\n\n    string completeString = ranges::accumulate(\n            people |\n            view::transform(personToMajorMinor) | \n            view::transform(majorMinor),\n            string(),\n            combineWithNewline\n           ); \n    string expectedString(\"Alex, major\\nJohn, major\\nJane, \n                                    minor\\nInvalid person\");\n\n    CHECK_EQ(expectedString, completeString);\n}\n```\n\n同样，从人员列表中查找未成年人列表非常容易，范围为'`view::filter`:\n\n```cpp\nTEST_CASE(\"Find all minors with ranges\"){\n    using namespace ranges;\n\n    vector<Person> people = {\n        Person(\"Alex\", 42),\n        Person(\"John\", 21),\n        Person(\"Jane\", 14),\n        Person(\"Diana\", 9)\n    };\n    vector<Person> expectedMinors{Person(\"Jane\", 14),\n                                   Person(\"Diana\", 9)};\n\n    vector<Person> minors = people | view::filter(isMinor);\n\n    CHECK_EQ(minors, expectedMinors);\n}\n```\n\n一旦我们有了`isMinor`谓词，我们就可以将其传递给`view::filter`来从人员列表中找到未成年人。\n\n# 摘要\n\n在这一章中，我们介绍了 C++ 17 的 STL 中可用的函数式编程特性，以及 C++ 20 中的新特性。有了函数、算法、`variant`和`optional`在错误或边缘情况下提供的帮助，以及使用范围库可以实现的简化和优化的代码，我们对函数编程特性有了很好的支持。\n\n现在，是时候进入下一章，看看 C++ 17 语言对函数式编程的支持，以及 C++ 20 中函数式编程的有趣之处。"
  },
  {
    "path": "docs/handson-func-prog-cpp/16.md",
    "content": "# 十六、标准语言支持和建议\n\n我们在这本书里已经讨论了很多主题，所以现在是时候把它们集中在一个方便的章节里，你可以用它来帮助记住如何使用我们介绍的函数式编程技术。我们也将借此机会看看 C++ 20 标准，并提及我们如何在代码中使用这些新功能。\n\n本章将涵盖以下主题:\n\n*   支持用 C++ 编写纯函数的方法，以及未来的建议\n*   支持用 C++ 编写 lambdas 的方式，以及未来的建议\n*   支持的 C++ 方式，以及未来的建议\n*   C++ 中支持的函数组合方式以及未来的建议\n\n# 技术要求\n\n您将需要一个支持 C++ 17 的编译器；我用的是 GCC 7.4.0c.\n\n代码在[的 GitHub 上。com/ PacktPublishing/动手-函数-用- Cpp 编程`Chapter16`文件夹中的](https://github.%E2%80%8Bcom/PacktPublishing/Hands-On-Functional-Programming-with-Cpp)。它包含并使用了`doctest`，这是一个单头开源单元测试库。你可以在[的 GitHub 资源库中找到它。com/ onqtam/ doctest](https://github.%E2%80%8Bcom/onqtam/doctest) 。\n\n# 标准语言支持和建议\n\n到目前为止，我们已经探索了用 C++ 编写函数式代码的几种方法。现在，我们将看看 C++ 17 标准允许的一些附加选项，以及 C++ 20 允许的一些选项。所以，让我们从编写纯函数开始。\n\n# 纯函数\n\n**纯函数**是接收相同输入时返回相同输出的函数。它们的可预测性使它们有助于理解编写的代码如何与其运行时性能相关联。\n\n我们在[第二章](02.html)、*理解纯函数、*中发现，在 C++ 中编写纯函数需要结合`const`和`static`，这取决于函数是类的一部分还是自由函数，以及我们如何将参数传递给函数。为了方便起见，我将在这里重现我们对纯函数语法得出的结论:\n\n*   类函数，按值传递:\n*   `static int increment(const int value)`\n*   `int increment(const int value) const`\n*   类函数，通过引用传递:\n*   `static int increment(const int& value)`\n*   `int increment(const int&value) const`\n*   类函数，按值传递指针:\n*   `static const int* increment(const int* value)`\n*   `const int* increment(const int* value) const`\n*   类函数，通过引用传递指针:\n*   `static const int* increment(const int* const& value)`\n*   `const int* increment(const int* const& value) const`\n*   独立功能，通过值`int increment(const int value)`\n*   独立功能，通过引用传递`int increment(const int& value)`\n*   独立功能，通过数值传递指针`const int* increment(const int* value)`\n*   独立功能，通过引用传递指针`const int* increment(const int* const& value)`\n\n我们还发现，虽然编译器有助于减少副作用，但它并不总是告诉我们一个函数是否是纯函数。在编写纯函数时，我们总是需要记住使用这三个标准，并小心应用它们:\n\n*   对于相同的输入值，它总是返回相同的输出值。\n*   它没有副作用。\n*   它不会更改其参数值。\n\n# 希腊字母的第 11 个\n\nLambdas 是函数式编程的一个基本部分，允许我们用函数进行操作。C++ 从 C++ 11 开始就有了 lambdas，但是最近对语法进行了一些补充。此外，我们将探索一些 lambda 特性，这些特性在本书中到目前为止还没有使用过，但是对于您自己的代码来说可以派上用场。\n\n让我们从一个简单的λ开始— `increment`有一个输入，并返回递增的值:\n\n```cpp\nTEST_CASE(\"Increment\"){\n    auto increment =  [](auto value) { return value + 1;};\n\n    CHECK_EQ(2, increment(1));\n}\n```\n\n方括号(`[]`)指定了捕获值的列表，我们将在下面的代码中看到。我们可以像对任何函数一样指定参数的类型:\n\n```cpp\nTEST_CASE(\"Increment\"){\n    auto increment =  [](int value) { return value + 1;};\n\n    CHECK_EQ(2, increment(1));\n}\n```\n\n我们也可以在参数列表和一个`->`符号之后立即指定返回值:\n\n```cpp\nTEST_CASE(\"Increment\"){\n    auto increment =  [](int value) -> int { return value + 1;};\n\n    CHECK_EQ(2, increment(1));\n}\n```\n\n如果没有输入值，参数列表和圆括号`()`可以忽略:\n\n```cpp\nTEST_CASE(\"One\"){\n    auto one =  []{ return 1;};\n\n    CHECK_EQ(1, one());\n}\n```\n\n我们可以通过指定值的名称来捕获它，在这种情况下，它是通过复制来捕获的:\n\n```cpp\nTEST_CASE(\"Capture value\"){\n    int value = 5;\n    auto addToValue =  [value](int toAdd) { return value + toAdd;};\n\n    CHECK_EQ(6, addToValue(1));\n}\n```\n\n或者，我们可以通过引用捕获一个值，使用捕获规范中的`&`运算符:\n\n```cpp\nTEST_CASE(\"Capture value by reference\"){\n    int value = 5;\n    auto addToValue =  [&value](int toAdd) { return value + toAdd;};\n\n    CHECK_EQ(6, addToValue(1));\n}\n```\n\n如果我们捕获多个值，我们可以枚举它们，也可以只捕获所有的值。对于按值捕获，我们使用`=`说明符:\n\n```cpp\nTEST_CASE(\"Capture all values by value\"){\n    int first = 5;\n    int second = 10;\n    auto addToValues = [=](int toAdd) { return first + second + \n        toAdd;};\n    CHECK_EQ(16, addToValues(1));\n}\n```\n\n为了通过引用捕获所有值，我们使用没有任何变量名的`&`说明符:\n\n```cpp\nTEST_CASE(\"Capture all values by reference\"){\n    int first = 5;\n    int second = 10;\n    auto addToValues = [&](int toAdd) { return first + second + \n        toAdd;};\n    CHECK_EQ(16, addToValues(1));\n}\n```\n\n虽然不推荐，但我们可以在参数列表后使用`mutable`说明符使 lambda 调用可变:\n\n```cpp\nTEST_CASE(\"Increment mutable - NOT RECOMMENDED\"){\n    auto increment =  [](int& value) mutable { return ++ value;};\n\n    int value = 1;\n    CHECK_EQ(2, increment(value));\n    CHECK_EQ(2, value);\n}\n\n```\n\n另外，从 C++ 20 开始，我们可以指定函数调用为`consteval`，而不是默认的`constexpr`:\n\n```cpp\nTEST_CASE(\"Increment\"){\n    auto one = []() consteval { return 1;};\n\n    CHECK_EQ(1, one());\n}\n```\n\n不幸的是，g++ 8 还不支持这个用例。\n\n异常说明符也是可能的；也就是说，如果λ抛出没有异常，那么`noexcept`可能会派上用场:\n\n```cpp\nTEST_CASE(\"Increment\"){\n    auto increment =  [](int value) noexcept { return value + 1;};\n\n    CHECK_EQ(2, increment(1));\n}\n\n```\n\n如果 lambda 引发异常，可以将其指定为一般异常或特定异常:\n\n```cpp\nTEST_CASE(\"Increment\"){\n    auto increment =  [](int value) throw() { return value + 1;};\n\n    CHECK_EQ(2, increment(1));\n}\n```\n\n但是如果您想使用泛型类型呢？嗯，在 C++ 11 中，这个可以用`function<>`类型。从 C++ 20 开始，所有优秀的类型约束都可以用简洁的语法为您的 lambdas 提供:\n\n```cpp\nTEST_CASE(\"Increment\"){\n    auto increment =  [] <typename T>(T value) -> requires \n        NumericType<T> { return value + 1;};\n\n    CHECK_EQ(2, increment(1));\n}\n```\n\n不幸的是，这在 g++ 8 中也不被支持。\n\n# 部分应用和修改\n\n**部分应用**是指通过在`1`(或更多，但少于 *N* )参数上应用带有 *N* 参数的函数来获得新函数。\n\n我们可以通过实现传递参数的函数或 lambda 来手动实现部分应用。这里有一个部分应用的例子，它使用`std::plus`函数通过将其参数之一设置为`1`来获得`increment`函数:\n\n```cpp\nTEST_CASE(\"Increment\"){\n    auto increment =  [](const int value) { return plus<int>()(value, \n        1); };\n\n    CHECK_EQ(2, increment(1));\n}\n```\n\n在本书中，我们主要关注如何在这些情况下使用 lambdas 然而，值得一提的是，我们可以为同一个目标使用纯函数。例如，同一个增量函数可以写成一个普通的 C++ 函数:\n\n```cpp\nnamespace Increment{\n    int increment(const int value){\n        return plus<int>()(value, 1);\n    };\n}\n\nTEST_CASE(\"Increment\"){\n    CHECK_EQ(2, Increment::increment(1));\n}\n```\n\n部分应用可以借助`bind()`函数在 C++ 中完成。`bind()`函数允许我们将参数绑定到函数的值，允许我们从`plus`导出`increment`函数，如下所示:\n\n```cpp\nTEST_CASE(\"Increment\"){\n    auto increment = bind(plus<int>(), _1, 1);\n\n    CHECK_EQ(2, increment(1));\n}\n```\n\n`bind`取以下参数:\n\n*   我们要绑定的函数。\n*   要绑定到的参数；这些可以是一个值，也可以是一个占位符(如`_1`、`_2`等)。占位符允许将参数转发给最终函数。\n\n在纯函数式编程语言中，部分应用与 currying 相联系。 **Currying** 是将一个接受 *N* 个参数的函数分解成接受一个参数的 *N* 个函数。在 C++ 中没有标准的方法来咖喱一个函数，但是我们可以通过使用 lambdas 来做。让我们看一个实现`pow`功能的例子:\n\n```cpp\nauto curriedPower = [](const int base) {\n    return [base](const int exponent) {\n        return pow(base, exponent);\n    };\n};\n\nTEST_CASE(\"Power and curried power\"){\n    CHECK_EQ(16, pow(2, 4));\n    CHECK_EQ(16, curriedPower(2)(4));\n}\n```\n\n如您所见，在 currying 的帮助下，我们可以通过简单地调用 curried 函数来完成部分应用，该函数只有一个参数，而不是两个:\n\n```cpp\n    auto powerOf2 = curriedPower(2);\n    CHECK_EQ(16, powerOf2(4));\n```\n\n默认情况下，这种机制在许多纯函数式编程语言中都是启用的。然而，在 C++ 中更难做到。currying 没有标准支持，但是我们可以创建自己的`curry`函数，该函数采用现有函数并返回其 curried 形式。这里有一个带有两个参数的函数的广义`curry`函数的例子:\n\n```cpp\ntemplate<typename F>\nauto curry2(F f){\n    return [=](auto first){\n        return [=](auto second){\n            return f(first, second);\n        };\n    };\n}\n```\n\n此外，下面是我们如何使用它来咖喱和做部分应用:\n\n```cpp\nTEST_CASE(\"Power and curried power\"){\n    auto power = [](const int base, const int exponent){\n        return pow(base, exponent);\n    };\n    auto curriedPower = curry2(power);\n    auto powerOf2 = curriedPower(2);\n    CHECK_EQ(16, powerOf2(4));\n}\n```\n\n现在让我们看看实现功能组合的方法。\n\n# 操作组合\n\n功能组合是指取两个功能， *f* 和 *g* ，获得一个新的功能，*h；*对于任何值， *h(x) = f(g(x))* 。我们可以在 lambda 或普通函数中手动实现函数组合。例如，给定两个函数，`powerOf2`，计算`2`的幂，和`increment`，增加一个值，我们将看到以下内容:\n\n```cpp\nauto powerOf2 = [](const int exponent){\n    return pow(2, exponent);\n};\n\nauto increment = [](const int value){\n    return value + 1;\n};\n```\n\n我们可以通过简单地将调用封装到一个名为`incrementPowerOf2`的 lambda 中来编写它们:\n\n```cpp\nTEST_CASE(\"Composition\"){\n    auto incrementPowerOf2 = [](const int exponent){\n        return increment(powerOf2(exponent));\n    };\n\n    CHECK_EQ(9, incrementPowerOf2(3));\n}\n```\n\n或者，我们可以使用一个简单的函数，如下所示:\n\n```cpp\nnamespace Functions{\n    int incrementPowerOf2(const int exponent){\n        return increment(powerOf2(exponent));\n    };\n}\n\nTEST_CASE(\"Composition\"){\n    CHECK_EQ(9, Functions::incrementPowerOf2(3));\n}\n```\n\n然而，一个接受两个函数并返回组合函数的运算符很方便，它在许多编程语言中都有实现。C++ 中最接近函数组合运算符的是范围库中的`|`管道运算符，该运算符目前在 C++ 20 标准中。然而，虽然它实现了组合，但它不适用于一般函数或 lambdas。幸运的是，C++ 是一种强大的语言，我们可以编写自己的合成函数，正如我们在[第 4 章](04.html)、*函数合成的思想*中所发现的:\n\n```cpp\ntemplate <class F, class G>\nauto compose(F f, G g){\n    return [=](auto value){return f(g(value));};\n}\n\nTEST_CASE(\"Composition\"){\n    auto incrementPowerOf2 = compose(increment, powerOf2); \n\n    CHECK_EQ(9, incrementPowerOf2(3));\n}\n```\n\n回到范围库和管道操作器，我们可以在范围的上下文中使用这种形式的函数组合。我们已经在[第 14 章](14.html)、*中使用范围库*对这个主题进行了广泛的探讨，这里有一个使用管道运算符计算集合中所有既是`2`又是`3`的倍数的数字总和的例子:\n\n```cpp\nauto isEven = [](const auto number){\n    return number % 2 == 0;\n};\n\nauto isMultipleOf3 = [](const auto number){\n    return number % 3 == 0;\n};\n\nauto sumOfMultiplesOf6 = [](const auto& numbers){\n    return ranges::accumulate(\n            numbers | ranges::view::filter(isEven) | \n                ranges::view::filter(isMultipleOf3), 0);\n};\n\nTEST_CASE(\"Sum of even numbers and of multiples of 6\"){\n    list<int> numbers{1, 2, 5, 6, 10, 12, 17, 25};\n\n    CHECK_EQ(18, sumOfMultiplesOf6(numbers));\n}\n```\n\n如您所见，标准 C++ 中有多种函数编程选项，C++ 20 中也有一些令人兴奋的发展。\n\n# 摘要\n\n就是这里！我们已经快速浏览了函数式编程中最重要的操作，以及如何使用 C++ 17 和 C++ 20 实现它们。我相信您现在在工具包中拥有了更多的工具——包括纯函数、lambdas、部分应用、currying 和函数组合，仅举几个例子。\n\n从现在开始，如何使用它们是你的选择。挑选几个，或者将它们组合起来，或者根据可变状态慢慢地将代码移动到不变状态；掌握这些工具将使您在编写代码时有更多的选择和灵活性。\n\n无论你选择做什么，我都祝你项目和编程生涯好运。快乐编码！"
  },
  {
    "path": "docs/handson-func-prog-cpp/17.md",
    "content": "# 十七、答案\n\n# 第一章\n\n1.  **什么是不可变函数？**\n\n不可变函数是不改变其参数值或程序状态的函数。\n\n2.  **如何编写不可变函数？**\n\n如果你想让编译器帮你，做参数`const`。\n\n3.  **不可变函数如何支持代码简单性？**\n\n因为他们不改变他们的论点，他们从代码中移除了任何潜在的复杂性，从而允许程序员更好地理解它。\n\n4.  **不可变函数如何支持简单的设计？**\n\n不可变函数很无聊，因为它们只做计算。因此，它们便于长期维护。\n\n5.  **什么是高级功能？**\n\n高级函数是接收另一个函数作为参数的函数。\n\n6.  **从 STL 可以举出哪些高级函数的例子？**\n\nSTL 中有许多高级函数的例子，尤其是在算法中。`sort`是我们在本章中使用的例子；然而，如果你查看`<algorithm>`标题，你会发现很多其他标题，包括`find`、`find_if`、`count`、`search`等等。\n\n7.  **功能循环相对于结构化循环有哪些优势？它们的潜在缺点是什么？**\n\n函数循环避免了逐个错误，并且更清楚地表达了代码的意图。它们也是可组合的，因此允许通过链接多个循环进行复杂的操作。但是，在合成时，它们需要多次通过集合，否则可以通过使用简单的循环来避免。\n\n8.  **从艾伦·凯的角度来看，OOP 是什么？它与函数式编程有什么关系？**\n\n艾伦·凯把面向对象程序设计看作是一种根据细胞有机体原理来构造代码的方法。细胞是通过化学信号进行交流的独立实体。因此，小对象之间的通信是 OOP 最重要的部分。\n\n这意味着我们可以对表示为对象的数据结构使用函数算法，而没有任何冲突。\n\n# 第二章\n\n1.  **什么是纯函数？**\n\n纯函数是具有两个约束的函数，如下所示:\n\n2.  **不变性如何与纯函数相关？**\n\n纯函数是不可变的，因为它们不会改变程序状态中的任何东西。\n\n3.  **如何告诉编译器防止对通过值传递的变量进行更改？**\n\n简单定义参数为`const`，如下:\n\n```cpp\nint square(const int value)\n```\n\n4.  **如何告诉编译器防止对通过引用传递的变量进行更改？**\n\n简单定义参数为`const&`，如下:\n\n```cpp\nint square(const int& value)\n```\n\n5.  **如何告诉编译器阻止对引用传递的指针地址的更改？**\n\n如果指针是通过值传递的，则不需要任何东西，因为所有的更改都是函数本地的:\n\n```cpp\nint square(int* value)\n```\n\n如果指针是通过引用传递的，我们需要告诉编译器地址不能改变:\n\n```cpp\nint square(int*& const value)\n```\n\n6.  **如何告诉编译器防止指针指向的值发生变化？**\n\n如果指针是通过值传递的，我们将应用与通过值传递的简单值相同的规则:\n\n```cpp\nint square(const int* value)\n```\n\n为了防止通过引用传递指针时值和地址都发生变化，需要更多地使用`const`关键字:\n\n```cpp\nint square(const int&* const value)\n```\n\n# 第三章\n\n1.  **你能写的最简单的 lambda 是什么？**\n\n最简单的 lambda 不接收参数，返回一个常数；它可能如下所示:\n\n```cpp\nauto zero = [](){return 0;};\n```\n\n2.  **如何编写一个 lambda 来连接作为参数传递的两个字符串值？**\n\n这个答案有一些变化，这取决于您首选的连接字符串的方式。使用 STL 最简单的方法如下:\n\n```cpp\nauto concatenate = [](string first, string second){return first + second;};\n```\n\n3.  **如果其中一个值是一个被值捕获的变量，该怎么办？**\n\n答案类似于前面的解决方案，但使用了上下文中的值:\n\n```cpp\nauto concatenate = [first](string second){return first + second;};\n```\n\n当然，我们也可以使用默认的按值捕获符号，如下所示:\n\n```cpp\nauto concatenate = [=](string second){return first + second;};\n```\n\n4.  **如果其中一个值是被引用捕获的变量，该怎么办？**\n\n与之前的解决方案相比几乎没有变化，如下面的代码所示，除非您想要防止值的变化:\n\n```cpp\nauto concatenate = [&first](string second){return first + second;};\n```\n\n如果你想防止数值变化，我们需要转换到`const`:\n\n```cpp\nauto concatenate = [&firstValue = as_const(first)](string second){return firstValue + second;};\n```\n\n5.  **如果其中一个值是被值捕获的指针，该怎么办？**\n\n我们可以忽略不变性，如下所示:\n\n```cpp\nauto concatenate = [=](string second){return *pFirst + second;};\n```\n\n或者，我们可以使用指向`const`类型的指针:\n\n```cpp\nconst string* pFirst = new string(\"Alex\");\nauto concatenate = [=](string second){return *pFirst + second;};\n```\n\n或者，我们可以只使用该值，如下所示:\n\n```cpp\nstring* pFirst = new string(\"Alex\");\nfirst = *pFirst;\nauto concatenate = [=](string second){return first + second;}\n```\n\n6.  **如果其中一个值是被引用捕获的指针，该怎么办？**\n\n这允许我们改变指向的值和 lambda 内部的指针地址。\n\n最简单的方法是忽略不变性，如下所示:\n\n```cpp\nauto concatenate = [&](string second){return *pFirst + second;};\n```\n\n如果我们想要约束不变性，我们可以使用强制转换来`const`:\n\n```cpp\nauto concatenate = [&first = as_const(pFirst)](string second){return *first + second;};\n```\n\n但是，通常最好直接使用该值，如下所示:\n\n```cpp\nstring first = *pFirst;\nauto concatenate = [=](string second){return first + second;};\n```\n\n7.  **如果使用默认捕获说明符按值捕获两个值会怎么样？**\n\n该解决方案不需要参数，只需要从上下文中获取两个值:\n\n```cpp\nauto concatenate = [=](){return first + second;};\n```\n\n8.  **如果使用默认捕获说明符通过引用捕获两个值，会怎样？**\n\n如果我们不关心值的变化，我们可以做以下事情:\n\n```cpp\nauto concatenate = [&](){return first + second;};\n```\n\n为了保持不变性，我们需要对`const`进行强制转换:\n\n```cpp\nauto concatenate = [&firstValue = as_const(first), &secondValue = as_const(second)](){return firstValue + secondValue;}\n```\n\n仅仅使用默认的引用捕获说明符是无法确保不变性的。请改用按值捕获。\n\n9.  **在一个有两个字符串值作为数据成员的类中，如何将同一个 lambda 写成数据成员？**\n\n在一个类中，我们需要指定 lambda 变量的类型，以及我们是捕获两个数据成员还是这个。\n\n以下代码显示了如何使用`[=]`语法通过复制来捕获值:\n\n```cpp\nfunction<string()> concatenate = [=](){return first + second;};\n```\n\n下面的代码显示了如何捕获`this`:\n\n```cpp\nfunction<string()> concatenate = [this](){return first + second;};\n```\n\n10.  **怎么能把同一个 lambda 写成同一个类的静态变量？**\n\n我们需要接收数据成员作为参数，如下所示:\n\n```cpp\nstatic function<string()> concatenate;\n...\nfunction<string()> AClass::concatenate = [](string first, string second){return first + second;};\n```\n\n我们已经看到，这比将`AClass`的整个实例作为参数传递要好，因为它减少了函数和类之间的耦合面积。\n\n# 第四章\n\n1.  **什么是功能成分？**\n\n功能组合是对功能的操作。它接受两个函数， *f* 和 *g* ，并创建第三个函数， *C* ，任何参数都有以下属性: *x* ， *C(x) = f(g(x))* 。\n\n2.  **函数式组合有一个通常与数学运算相关的性质。这是什么？**\n\n函数组合是不可交换的。例如，对一个数的增量求平方与对一个数的平方求平方是不一样的。\n\n3.  **如何把两个参数的加法函数变成一个参数的两个函数？**\n\n考虑以下功能:\n\n```cpp\nauto add = [](const int first, const int second){ return first + second; };\n```\n\n我们可以将前面的函数转换为下面的函数:\n\n```cpp\nauto add = [](const int first){ \n    return [first](const int second){\n        return first + second;\n    };\n};\n```\n\n4.  **如何编写一个包含两个单参数函数的 C++ 函数？**\n\n在这一章中，我们看到借助模板和`auto`类型的魔力很容易做到这一点:\n\n```cpp\ntemplate <class F, class G>\nauto compose(F f, G g){\n  return [=](auto value){return f(g(value));};\n}\n```\n\n5.  **功能成分有哪些优势？**\n\n函数组合允许我们通过组合非常简单的函数来创建复杂的行为。此外，它允许我们删除某些类型的重复。它还允许以无限的方式重组小函数，从而提高了重用的可能性。\n\n6.  **在函数上实现操作有哪些潜在的缺点？**\n\n对函数的操作可能有非常复杂的实现，并且可能变得非常难以理解。抽象是有代价的，程序员必须始终平衡可组合性和小代码的好处与使用抽象操作的代价。\n\n# 第五章\n\n1.  **什么是偏函数应用？**\n\n部分函数应用是从函数中获取 *N-1* 参数的新函数的操作，该函数通过将其中一个参数绑定到一个值来获取 *N* 参数。\n\n2.  **什么是拍马屁？**\n\nCurrying 是将取 *N* 个参数的函数拆分成 *N* 个函数的操作，每个函数取一个参数。\n\n3.  **curry 如何帮助实现部分应用？**\n\n给定 curried 函数 *f(x)(y)* ，只需调用 *f* 就可以得到 *f* 在 *x =值*上的部分应用，取值如下: *g = f(值)*。\n\n4.  **如何在 C++ 中实现部分应用？**\n\n部分应用可以在 C++ 中手动实现，但是使用`functional`头中的`bind`函数更容易实现。"
  },
  {
    "path": "docs/handson-func-prog-cpp/README.md",
    "content": "# C++ 函数式编程实用指南\n\n> 原书：[Hands-On Functional Programming with C++](https://libgen.rs/book/index.php?md5=873BFE33DF74385C75906A2F129CA61F)\n> \n> 协议：[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)\n> \n> 阶段：机翻（1）\n> \n> 自豪地采用[谷歌翻译](https://translate.google.cn/)\n> \n> 让开源界提前感受入关。\n\n* [在线阅读](https://ccpp.apachecn.org)\n* [在线阅读（Gitee）](https://apachecn.gitee.io/apachecn-c-cpp-zh/)\n* [ApacheCN 学习资源](http://docs.apachecn.org/)\n\n## 贡献指南\n\n本项目需要校对，欢迎大家提交 Pull Request。\n\n> 请您勇敢地去翻译和改进翻译。虽然我们追求卓越，但我们并不要求您做到十全十美，因此请不要担心因为翻译上犯错——在大部分情况下，我们的服务器已经记录所有的翻译，因此您不必担心会因为您的失误遭到无法挽回的破坏。（改编自维基百科）\n\n## 联系方式\n\n### 负责人\n\n* [飞龙](https://github.com/wizardforcel): 562826179\n\n### 其他\n\n*   在我们的 [apachecn/apachecn-c-cpp-zh](https://github.com/apachecn/apachecn-c-cpp-zh) github 上提 issue.\n*   发邮件到 Email: `apachecn@163.com`.\n*   在我们的 [组织学习交流群](http://www.apachecn.org/organization/348.html) 中联系群主/管理员即可.\n\n## 赞助我们\n\n![](http://data.apachecn.org/img/about/donate.jpg)\n"
  },
  {
    "path": "docs/handson-func-prog-cpp/SUMMARY.md",
    "content": "+   [C++ 函数式编程实用指南](README.md)\n+   [零、前言](00.md)\n+   [第一部分：C++ 中的函数组件](sec1.md)\n\t+   [一、函数式编程导论](01.md)\n\t+   [二、理解纯函数](02.md)\n\t+   [三、深入 lambdas](03.md)\n\t+   [四、函数组合思想](04.md)\n\t+   [五、局部应用与柯里化](05.md)\n+   [第二部分：函数设计](sec2.md)\n\t+   [六、函数思维——从数据输入到数据输出](06.md)\n\t+   [七、通过函数操作消除重复](07.md)\n\t+   [八、使用类提高内聚性](08.md)\n\t+   [九、面向函数式编程的测试驱动开发](09.md)\n+   [第三部分：收获函数式编程的好处](sec3.md)\n\t+   [十、性能优化](10.md)\n\t+   [十一、基于属性的测试](11.md)\n\t+   [十二、重构到纯函数和通过纯函数重构](12.md)\n\t+   [十三、不变性和架构——事件源](13.md)\n+   [第四部分：C++ 函数式编程的现状和未来](sec4.md)\n\t+   [十四、使用范围库的延迟求值](14.md)\n\t+   [十五、STL 支持和建议](15.md)\n\t+   [十六、标准语言支持和建议](16.md)\n+   [十七、答案](17.md)\n"
  },
  {
    "path": "docs/handson-func-prog-cpp/sec1.md",
    "content": "# 第一部分：C++ 中的函数组件\n\n在本节中，我们将学习函数式编程的基本构造块以及如何在 C++ 中使用它们。首先，我们来看看什么是函数式编程，它与**面向对象编程** ( **OOP** )有何不同和相似之处。然后，我们将深入不变性的基本思想，学习如何用 C++ 编写纯函数——即不改变状态的函数。然后我们将学习如何使用 lambdas，以及如何使用它们编写纯函数。\n\n一旦我们掌握了这些构造块，我们就可以继续使用函数进行操作了。在函数式编程中，函数是数据，所以我们可以传递它们，并用它们进行操作。我们将学习局部应用和柯里化，两个基本的和密切相关的操作。我们还将看到如何组合函数。这些操作将把我们从简单的函数带到非常复杂的函数，只需要几行管道代码。\n\n本节将涵盖以下章节:\n\n*   [第 1 章](01.html)，*功能编程入门*\n*   [第二章](02.html)*理解纯函数*\n*   [第三章](03.html)*深入兰姆达斯*\n*   [第四章](04.html)*功能组合的理念*\n*   [第五章](05.html)、*局部应用和柯里化*"
  },
  {
    "path": "docs/handson-func-prog-cpp/sec2.md",
    "content": "# 第二部分：函数设计\n\n到目前为止，我们已经了解了函数式编程的基本构造块。是时候带他们兜兜风，参观一下专注于功能的软件设计世界了。\n\n首先，我们将研究一种改变我们思维模式的方法，从以命令方式编写的**面向对象编程** ( **OOP** )转变为以功能为中心的设计。为此，我们需要了解如何将输入数据转换为所需的输出数据，最好借助现有的高阶函数。然后，我们将看看**不要重复自己** ( **DRY** )原则，以及我们如何使用功能操作(部分应用、currying 和功能组合)从我们的代码中移除某些类型的重复。然后，我们将看看函数和类之间的关系，如果我们想将设计从以函数为中心切换到面向对象，我们如何将纯函数分组到类中，以及我们如何将一个类变成一组纯函数。\n\n有了所有这些技术，我们将学习测试驱动开发，以及如何通过使用纯函数来简化它。\n\n本节将涵盖以下章节:\n\n*   [第六章](06.html)*函数思维——从数据输入到数据输出*\n*   [第 7 章](07.html)，*通过功能操作消除重复*\n*   [第八章](08.html)、*利用班级*提高凝聚力\n*   [第 9 章](09.html)，*功能编程的测试驱动开发*"
  },
  {
    "path": "docs/handson-func-prog-cpp/sec3.md",
    "content": "# 第三部分：收获函数式编程的好处\n\n我们已经学习了很多关于函数式编程的构建块，如何用 C++ 编写它们，以及如何使用它们来构建以函数为中心的设计。是时候看看一些与函数式编程密切相关的专业主题了。\n\n首先，我们将深入探讨性能优化这个巨大的话题。我们将学习一些特别适合纯函数的优化技术(例如，记忆化和尾部递归优化)。我们将研究内存占用和执行时间优化，执行许多测量，并比较各种方法。\n\n然后，我们将研究函数式编程如何实现并行和异步执行。不变性导致共享状态的避免，因此导致更简单的并行执行模式。\n\n但是我们可以利用更多的函数式编程。数据生成器和纯函数实现了一个名为**基于属性测试**的自动化测试范例，它允许我们用很少的代码检查许多可能的场景。然后，如果我们需要重构复杂的现有代码，我们将看到我们可以首先将其重构为纯函数，快速为它们编写测试，然后决定是将它们重新分布到类中还是保留它们。\n\n最后，我们将更上一层楼，到一个基于不可变状态的架构范例，因此，与功能编程紧密相连的东西:事件源。\n\n本节将涵盖以下章节:\n\n*   [第十章](10.html)、*性能优化*\n*   [第 11 章](11.html)*物业测试*\n*   [第 12 章](12.html)、*重构到纯函数*\n*   [第 13 章](13.html)、*不变性和架构-事件源*"
  },
  {
    "path": "docs/handson-func-prog-cpp/sec4.md",
    "content": "# 第四部分：C++ 函数式编程的现状和未来\n\n我们已经访问了许多我们可以在函数式编程中使用的技术，从基本的构建块，通过以函数为中心的方式进行设计，到如何利用函数式编程实现各种目标。是时候看看标准 C++ 17 和 20 中函数式编程的现在和未来了。\n\n我们将首先使用惊人的范围库，它可以作为 C++ 17 的外部实现和 C++ 20 标准的一部分。我们将看到一个简单的想法，以一种轻量级的方式包装现有的容器，结合一个复合操作符和一个我们已经广泛使用的高阶函数的新形式，如何允许我们编写比标准 C++ 17 更简单、更快、更轻的代码。\n\n然后我们将访问 STL 支持，看看接下来会发生什么。最后，我们将看看函数式编程的主要构建模块，以及它们在 C++ 中是如何得到支持的。\n\n本节将涵盖以下章节:\n\n*   [第 14 章](14.html)、*使用范围库*的延迟求值\n*   [第 15 章](15.html)*STL 支持与建议*\n*   [第 16 章](16.html)*标准语言支持与提案*"
  },
  {
    "path": "docs/handson-game-dev-wasm/00.md",
    "content": "# 零、前言\n\nWebAssembly 是一项技术，它将在未来几年内改变我们所知的网络。WebAssembly 承诺实现一个基于 web 的应用以接近本机速度运行的世界。在这个世界里，你可以用你喜欢的任何语言为网络编写应用，并为本地平台和网络编译它。WebAssembly 现在还为时过早，但这项技术已经像火箭一样起飞了。如果你对网络的发展方向感兴趣，就像对今天一样，那就继续读下去吧！\n\n我写这本书是为了反映我喜欢学习新技能的方式。我将引导你使用 WebAssembly 及其所有相关技术开发一款游戏。我是一名长期的游戏和网页开发人员，我一直喜欢通过写游戏来学习新的编程语言。在本书中，我们将使用与 WebAssembly 齐头并进的网络和游戏开发工具，在许多主题上覆盖许多领域。我们将学习如何利用过多的编程语言和工具来编写以 WebAssembly 为目标的游戏，包括 Emscripten、C/C++、WebGL、OpenGL、JavaScript、HTML5 和 CSS。作为一家专门从事网络游戏开发的独立游戏开发工作室的长期所有者，我发现对网络和游戏技术有广泛的了解是至关重要的，我已经把这本书塞满了它们。您将学习一系列技能，重点是使用 WebAssembly 启动和运行您的应用。如果你想学习如何用 WebAssembly 开发游戏，或者如果你想创建速度极快的基于 web 的应用，这本书就是为你准备的。\n\n# 这本书是给谁的\n\n这本书不是编程入门。它是为那些知道如何用至少一种编程语言编码的人设计的。至少对一些基于网络的技术(如 HTML)有一个初步的了解是有帮助的，但不是绝对必要的。这本书包含了如何在 Windows 或 Ubuntu Linux 上安装所需工具的说明，在这两者中，我会推荐使用 Ubuntu，因为它的安装过程要简单得多。\n\n# 这本书涵盖了什么\n\n[第 1 章](01.html)*WebAssembly 和 Emscripten 简介*介绍了 web assembly，为什么 web 需要它，为什么它比 JavaScript 快这么多。我们将介绍 Emscripten，为什么我们需要它来进行 WebAssembly 开发，以及如何安装它。我们还将讨论与 WebAssembly 相关的技术，例如 asm.js、LLVM 和 WebAssembly Text。\n\n[第二章](02.html)、 *HTML5 和 WebAssembly* 讨论了 WebAssembly 模块如何使用 JavaScript“粘合代码”与 HTML 集成。我们将学习如何创建自己的 Emscripten HTML shell 文件，我们还将学习如何调用我们的 WebAssembly 模块，我们将用 c 语言编写该模块。最后，我们将学习如何编译和运行一个与我们的 WebAssembly 模块交互的 HTML 页面，我们还将学习如何使用 Emscripten 构建一个简单的 HTML5 Canvas 应用。\n\n[第 3 章](03.html)、*WebGL 介绍*，介绍 WebGL 以及支持它的新画布上下文。我们将了解着色器，它们是什么，以及 WebGL 如何使用它们将几何图形渲染到画布上。我们将学习如何使用 WebGL 和 JavaScript 在画布上绘制一个精灵。最后，我们将编写一个集成了 WebAssembly、JavaScript 和 WebGL 的应用，显示一个精灵并在画布上移动它。\n\n[第 4 章](04.html)、*与 SDL 的 WebAssembly 中的雪碧动画*，教你关于 SDL 库，以及我们如何使用它来简化从 WebAssembly 到网络 GL 的调用。我们将学习如何使用 SDL 渲染，动画和移动 HTML5 画布上的精灵。\n\n[第 5 章](05.html)、*键盘输入*，介绍如何从 JavaScript 中获取键盘输入并调用 WebAssembly 模块。我们还将学习如何在我们的 WebAssembly 模块中使用 SDL 接受键盘输入，并使用输入在 HTML5 画布上移动精灵。\n\n[第六章](06.html)、*游戏物件和游戏循环*，探索一些基本的游戏设计。我们将了解游戏循环，以及 WebAssembly 中的游戏循环与其他游戏有何不同。我们还将学习游戏对象以及如何在游戏中创建对象池。我们将以编写游戏开始的代码来结束这一章，两个宇宙飞船在画布上移动并互相发射炮弹。\n\n[第七章](07.html)*碰撞检测*，将碰撞检测引入我们的游戏。我们将探索 2D 碰撞检测的类型，实现一个基本的碰撞检测系统，并了解一些使其工作的三角学知识。我们将修改我们的游戏，这样当飞船相撞时，投射物就会摧毁它们。\n\n[第八章](08.html)*基础粒子系统*，介绍粒子系统，讨论它们如何在视觉上提升我们的游戏。我们将讨论虚拟文件系统，并学习如何通过网页向其中添加文件。我们将简要介绍 SVG 和 Vector graphics，以及如何将它们用于数据可视化。我们将进一步讨论三角学，以及如何在我们的粒子系统中使用它。我们将构建一个新的 HTML5 WebAssembly 应用，它将帮助我们配置和测试粒子系统，我们稍后会将其添加到我们的游戏中。\n\n[第 9 章](09.html)、*改进粒子系统*通过添加粒子缩放、旋转、动画和颜色过渡来改进我们的粒子系统配置工具。我们将修改工具以允许粒子系统循环，并添加一个爆发效果。然后我们将更新我们的游戏来支持粒子系统，并为我们的发动机排气和爆炸增加粒子系统效果。\n\n[第十章](10.html)、 *AI 和转向行为*，介绍了 AI 和游戏 AI 的概念，讨论了两者的区别。我们将讨论有限状态机、自主代理和转向行为的 AI 概念，我们将在敌人 AI 中实现这些行为，这将避免障碍并与玩家战斗。\n\n[第十一章](11.html)*设计一台 2D 相机*，引入了 2D 相机设计的概念。我们将首先在我们的游戏中添加一个渲染管理器，并创建一个锁定玩家飞船的摄像头，跟随它在一个扩大的游戏区域内移动。然后，我们将添加先进的 2D 相机功能的投影焦点和相机吸引。\n\n[第 12 章](12.html)、*音效 FX* ，涵盖了 SDL 音频在我们游戏中的使用。我们将讨论在哪里可以在线获得声音效果，以及如何将这些声音包含在我们的 WebAssembly 模块中。然后我们将在游戏中加入音效。\n\n[第十三章](13.html)*游戏物理*，介绍了电脑游戏中的物理概念。我们将在游戏对象之间增加弹性碰撞。我们将以宇宙飞船发射射弹时的后坐力形式，将牛顿第三定律加入到我们的游戏物理中。我们将在我们的恒星上增加一个引力场来吸引宇宙飞船。\n\n[第 14 章](14.html)、*用户界面和鼠标输入*，讨论了在我们的 WebAssembly 模块中添加一个要管理和呈现的用户界面。我们将收集需求，并将其转化为我们游戏的新屏幕。我们将添加一个新的按钮对象，并学习如何使用 SDL 在我们的 WebAssembly 模块中管理鼠标输入。\n\n[第 15 章](15.html)*着色器和 2D 照明*，深入探讨如何创建一个混合了 OpenGL 和 SDL 的新应用。我们将创建一个新的着色器，将多个纹理加载并渲染到一个四边形中。我们将学习普通地图，以及如何使用普通地图来近似 2D 的 Phong 照明模型，在我们的 WebAssembly 模块中使用 OpenGL。\n\n[第十六章](16.html)、*调试和优化*，介绍了调试和优化 WebAssembly 模块的基本方法。我们将从调试宏和 WebAssembly 的堆栈跟踪开始。我们将介绍源映射的概念以及网络浏览器如何使用它们来调试 WebAssembly 模块。我们将学习如何使用优化标志来优化 WebAssembly 代码。我们将讨论使用分析器来优化我们的 WebAssembly 代码。\n\n# 充分利用这本书\n\n你必须了解计算机编程的基础知识。\n\n对网页技术如 HTML 和 CSS 有一个基本的了解是有帮助的。\n\n# 下载示例代码文件\n\n你可以从这里下载这本书的代码包:[https://github . com/packt publishing/动手游戏-用 WebAssembly 开发](https://github.com/PacktPublishing/Hands-On-Game-Development-with-WebAssembly)。\n\n我们还有来自 https://github.com/PacktPublishing/丰富的书籍和视频目录的其他代码包。看看他们！\n\n# 下载彩色图像\n\n我们还提供了一个 PDF 文件，其中包含本书中使用的截图/图表的彩色图像。可以在这里下载:[https://www . packtpub . com/sites/default/files/downloads/9781838644659 _ color images . pdf](https://www.packtpub.com/sites/default/files/downloads/9781838644659_ColorImages.pdf)。\n\n# 使用的约定\n\n你可以从你在[www.packt.com](http://www.packt.com)的账户下载这本书的示例代码文件。如果您在其他地方购买了这本书，您可以访问[www.packt.com/support](http://www.packt.com/support)并注册将文件直接通过电子邮件发送给您。\n\n您可以按照以下步骤下载代码文件:\n\n1.  登录或注册[www.packt.com](http://www.packt.com)。\n2.  选择“支持”选项卡。\n\n3.  点击代码下载和勘误表。\n4.  在搜索框中输入图书的名称，并按照屏幕指示进行操作。\n\n下载文件后，请确保使用最新版本的解压缩文件夹:\n\n*   视窗系统的 WinRAR/7-Zip\n*   zipeg/izp/un ARX for MAC\n*   适用于 Linux 的 7-Zip/PeaZip\n\n这本书的代码包也在 GitHub 上的[**https://GitHub . com/packt publishing/hand-On-Game-Development-with-WebAssembly**](https://github.com/PacktPublishing/Hands-On-Game-Development-with-WebAssembly)托管。如果代码有更新，它将在现有的 GitHub 存储库中更新。\n\n我们还有来自丰富的图书和视频目录的其他代码包，可在**[【https://github.com/PacktPublishing/】](https://github.com/PacktPublishing/)**获得。看看他们！\n\n# 使用的约定\n\n本书通篇使用了许多文本约定。\n\n`CodeInText`:表示文本中的码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟网址、用户输入和推特句柄。这里有一个例子:“我们将把`basic_particle_shell.html`文件复制到一个新的 shell 文件中，我们称之为`advanced_particle_shell.html`\n\n代码块设置如下:\n\n```cpp\n<label class=\"ccontainer\"><span class=\"label\">loop:</span>\n<input type=\"checkbox\" id=\"loop\" checked=\"checked\">\n<span class=\"checkmark\"></span>\n</label>\n<br/>\n```\n\n当我们希望将您的注意力吸引到代码块的特定部分时，相关的行或项目以粗体显示:\n\n```cpp\n<label class=\"ccontainer\"><span class=\"label\">loop:</span>\n<input type=\"checkbox\" id=\"loop\" checked=\"checked\">\n<span class=\"checkmark\"></span>\n</label>\n<br/>\n```\n\n任何命令行输入或输出都编写如下:\n\n```cpp\nemrun --list_browsers\n```\n\n**粗体**:表示一个新的术语，一个重要的单词，或者你在屏幕上看到的单词。例如，菜单或对话框中的单词像这样出现在文本中。下面是一个示例:“从管理面板中选择系统信息。”\n\nWarnings or important notes appear like this. Tips and tricks appear like this.\n\n# 取得联系\n\n我们随时欢迎读者的反馈。\n\n**一般反馈**:如果你对这本书的任何方面有疑问，在你的信息主题中提到书名，发邮件给我们`customercare@packtpub.com`。\n\n**勘误表**:虽然我们已经尽了最大的努力来保证内容的准确性，但是错误还是会发生。如果你在这本书里发现了一个错误，如果你能向我们报告，我们将不胜感激。请访问[www.packt.com/submit-errata](http://www.packt.com/submit-errata)，选择您的图书，点击勘误表提交链接，并输入详细信息。\n\n**盗版**:如果您在互联网上遇到任何形式的我们作品的非法拷贝，如果您能提供我们的位置地址或网站名称，我们将不胜感激。请通过`copyright@packt.com`联系我们，并提供材料链接。\n\n**如果你有兴趣成为一名作者**:如果有一个你有专长的话题，你有兴趣写或者投稿一本书，请访问[authors.packtpub.com](http://authors.packtpub.com/)。\n\n# 复习\n\n请留下评论。一旦你阅读并使用了这本书，为什么不在你购买它的网站上留下评论呢？然后，潜在的读者可以看到并使用您不带偏见的意见来做出购买决定，我们在 Packt 可以了解您对我们产品的看法，我们的作者可以看到您对他们的书的反馈。谢谢大家！\n\n更多关于 Packt 的信息，请访问[packt.com](http://www.packt.com/)。"
  },
  {
    "path": "docs/handson-game-dev-wasm/01.md",
    "content": "# 一、WebAssembly 和电子脚本简介\n\n欢迎来到激动人心的 WebAssembly 新世界！对于 WebAssembly 来说，这些都是早期的事情，但是这项技术目前正在像火箭一样起飞，通过阅读这本书，你可以从底层进入。如果你对网络游戏开发感兴趣，或者你有兴趣尽可能多地了解这项新技术，以便在它成熟时为自己定位，那么你就来对地方了。尽管 WebAssembly 还处于起步阶段，但所有主要的浏览器供应商都采用了它。这些都是早期，用例有限，但幸运的是，游戏开发是其中之一。所以，如果你想早点参加下一代网络应用开发的聚会，继续读下去，冒险家！\n\n在这一章中，我将向您介绍 WebAssembly、Emscripten 以及一些关于 WebAssembly 的底层技术。我将教你 Emscripten 工具链的基础知识，以及如何使用 Emscripten 将 C++ 代码编译成 WebAssembly。我们将讨论什么是 LLVM，以及它如何适应 Emscripten 工具链。我们将讨论 WebAssembly 的**最小可行产品** ( **MVP** )、当前 MVP 形式的 WebAssembly 的最佳用例，以及 WebAssembly 即将推出的产品。我来介绍一下**网页组装文字** ( **)。wat** )，我们如何用它来理解 WebAssembly 字节码的设计，以及它与其他机器字节码的区别。我们还将简要讨论 **asm.js** ，及其在 WebAssembly 设计中的历史意义。最后，我将向您展示如何在 Windows 和 Linux 上安装和运行 Emscripten。\n\n在本章中，我们将涵盖以下主题:\n\n*   什么是 WebAssembly？\n*   为什么我们需要 WebAssembly？\n*   为什么 WebAssembly 比 JavaScript 快？\n*   WebAssembly 会取代 JavaScript 吗？\n*   什么是 asm.js？\n*   LLVM 简介\n*   WebAssembly 文本简介\n*   什么是 Emscripten，我们如何使用它？\n\n# 什么是 WebAssembly？\n\nWebAssembly 不是像 JavaScript 那样的高级编程语言，而是所有主要浏览器目前都能执行的编译二进制格式。WebAssembly 是一种机器字节码，其设计目的不是直接在任何真实的机器硬件上运行，而是在每个浏览器内置的 JavaScript 引擎中运行。在某些方面，它类似于旧的 **Java 虚拟机**(**JVM**)；例如，它是独立于平台的编译字节码。JavaScript 字节码的一个主要问题是，它要求在浏览器中下载并安装一个插件来运行字节码。**网络组件**不仅设计为在没有插件的浏览器中直接运行，还旨在产生一种紧凑的二进制格式，在网络浏览器中高效执行。该规范的 MVP 版本利用了浏览器制造商设计他们的 JavaScript **即时** ( **JIT** )编译器的现有工作。WebAssembly 目前是一项年轻的技术，计划进行许多改进。然而，使用当前版本 WebAssembly 的开发人员已经看到性能比 JavaScript 提高了 10–800%。\n\nAn MVP is the smallest set of features that can be given to a product to allow it to appeal to early adopters. Because the current version is an MVP, the feature set is small. For more information, see this excellent article discussing the \"post-MVP future\" of WebAssembly: [https://hacks.mozilla.org/2018/10/webassemblys-post-mvp-future/](https://hacks.mozilla.org/2018/10/webassemblys-post-mvp-future/).\n\n# 为什么我们需要 WebAssembly？\n\nJavaScript 已经存在很长时间了。它已经从一种允许在网页上添加花哨功能的小脚本语言发展成为一种具有庞大生态系统的扩展 JIT 编译语言，可以用来编写成熟的应用。如今，JavaScript 正在做许多可能是网景在 1995 年创建时从未想象过的事情。JavaScript 是一种解释语言，这意味着它必须被动态解析、编译和优化。JavaScript 也是一种动态类型语言，这给优化器带来了麻烦。\n\nFranziska Hinkelmann, a member of the Chrome V8 team, gave a great talk at the *Web Rebels 2017* conference where she discusses all the performance improvements made to JavaScript over the past 20 years, as well as the difficulties they had in squeezing every bit of performance imaginable out of the JavaScript V8 engine: [https://youtu.be/ihANrJ1Po0w](https://youtu.be/ihANrJ1Po0w).\n\nWebAssembly 解决了很多 JavaScript 及其在浏览器中的悠久历史所带来的问题。因为 JavaScript 引擎已经是字节码格式，所以它不需要运行解析器，这消除了我们应用执行中的一个重要瓶颈。这种设计还允许 JavaScript 引擎知道它一直在处理什么数据类型。字节码使优化变得更加容易。这种格式允许浏览器中的多个线程同时编译和优化代码的不同部分。\n\nFor a detailed explanation of what is happening when the Chrome V8 engine is parsing code, please refer to this video from the *JSConf EU 2017*, in which Marja Hölttä (who works on the Chrome V8 tool) goes into more detail than you ever imagined you wanted to learn about parsing JavaScript: [https://www.youtube.com/watch?v=Fg7niTmNNLg&t=123s](https://www.youtube.com/watch?v=Fg7niTmNNLg&t=123s).\n\nWebAssembly 不是一种高级编程语言，而是一个带有虚拟机操作码的二进制文件。目前被认为处于 MVP 发展阶段。这项技术仍处于起步阶段，但即使是现在，它也为许多用例(如游戏开发)提供了显著的性能和文件大小优势。由于 WebAssembly 目前的局限性，我们只有两种语言可供选择——C/c++ 或 Rust。WebAssembly 的长期计划是为其开发支持多种编程语言。如果我想在最底层的抽象层次上写，我可以在**Web Assembly Text**(**WAT**)中编写所有内容，但是 WAT 是作为一种支持调试和测试的语言开发的，并不打算被开发人员用于编写应用。\n\n# 为什么 WebAssembly 比 JavaScript 快？\n\n正如我提到的，WebAssembly 比 JavaScript 快 10-800%，这取决于应用。为了理解其中的原因，我需要简单介绍一下 JavaScript 引擎在运行 JavaScript 代码时会做什么，以及在运行 WebAssembly 时必须做什么。我将具体谈论 V8(Chrome JavaScript 引擎)，尽管据我所知，在 SpiderMonkey (Firefox)和 Chakra (IE & Edge) JavaScript 引擎中也存在相同的一般过程。\n\nJavaScript 引擎做的第一件事是将你的源代码解析成一个**抽象语法树** ( **AST** )。根据应用中的逻辑，源代码被分成分支和叶。此时，解释器开始处理您当前正在执行的语言。多年来，JavaScript 只是一种解释语言，因此，如果您在 JavaScript 中运行相同的代码 100 次，JavaScript 引擎必须将该代码转换为机器代码 100 次。可以想象，这是非常低效的。\n\nChrome 浏览器在 2008 年推出了第一个 JavaScript JIT 编译器。准时制编译器与超前时间的编译器形成对比，前者编译你的代码，而后者运行该代码。一个探查器坐在那里观察 JavaScript 的执行，寻找重复执行的代码。每当它看到执行了几次的代码时，它会将该代码标记为“温暖的”，以便进行 JIT 编译。然后，编译器编译该 JavaScript“存根”代码的字节码表示。这个字节码通常是一个**中间表示** ( **IR** )，从机器特定的汇编语言中去除了一步。对存根进行解码将比下次通过我们的解释器运行相同的代码行要快得多。\n\n以下是运行 JavaScript 代码所需的步骤:\n\n![](img/9dd90a0a-c663-46b1-9fa2-977cf4d76ee7.png)\n\nFigure 1.1: Steps required by a modern JavaScript engine\n\n当所有这些都在进行的时候，有一个**优化编译器**正在监视分析器的“热”代码分支。然后，优化编译器将这些代码分支，并将由 JIT 创建的字节码优化为高度优化的机器代码。至此，JavaScript 引擎已经创建了一些超级快速运行的代码，但是有一个陷阱(或者可能是几个陷阱)。\n\nJavaScript 引擎必须对数据类型做一些假设，以获得优化的机器代码。问题是，JavaScript 是一种动态类型的语言。动态类型使程序员更容易学习如何编写 JavaScript，但对于代码优化者来说，这是一个糟糕的选择。我经常看到的例子是当 JavaScript 看到表达式`c = a + b`时会发生什么(尽管我们几乎可以将这个例子用于任何表达式)。\n\n几乎所有执行此操作的机器代码都分三步完成:\n\n1.  将`a`值载入寄存器。\n\n2.  将`b`值添加到寄存器中。\n3.  然后将寄存器存入`c`。\n\n以下伪代码摘自 *ECMAScript 2018 语言规范*第 12.8.3 节，描述了在 JavaScript 中使用加法运算符(+)时必须运行的代码:\n\n```cpp\n1\\. Let lref be the result of evaluating AdditiveExpression.\n2\\. Let lval be ? GetValue(lref).\n3\\. Let rref be the result of evaluating MultiplicativeExpression.\n4\\. Let rval be ? GetValue(rref).\n5\\. Let lprim be ? ToPrimitive(lval).\n6\\. Let rprim be ? ToPrimitive(rval).\n7\\. If Type(lprim) is String or Type(rprim) is String, then\n   a. Let lstr be ? ToString(lprim).\n   b. Let rstr be ? ToString(rprim).\n   c. Return the string-concatenation of lstr and rstr.\n8\\. Let lnum be ? ToNumber(lprim).\n9\\. Let rnum be ? ToNumber(rprim).\n10.Return the result of applying the addition operation to lnum and      \n   rnum.\n```\n\nYou can find the *ECMAScript® 2018 Language Specification* on the web at [https://www.ecma-international.org/ecma-262/9.0/index.html](https://www.ecma-international.org/ecma-262/9.0/index.html).\n\n这个伪代码不是我们必须评估的全部。这些步骤中有几个是调用高级函数，而不是运行机器代码命令。`GetValue`例如，有 11 个自己的步骤，依次调用其他步骤。所有这些都可能导致数百个机器操作码。这里发生的绝大多数事情是类型检查。在 JavaScript 中，当您执行`a + b`时，这些变量中的每一个都可以是以下任何一种类型:\n\n*   整数\n*   浮动\n*   线\n*   目标\n*   这些的任意组合\n\n更糟糕的是，JavaScript 中的对象也是高度动态的。例如，也许您已经定义了一个名为`Point`的函数，并使用新的运算符用该函数创建了两个对象:\n\n```cpp\nfunction Point( x, y ) {\n    this.x = x;\n    this.y = y;\n}\n\nvar p1 = new Point(1, 100);\nvar p2 = new Point( 10, 20 );\n```\n\n现在我们有两个点共享同一个类。假设我们添加了这一行:\n\n```cpp\np2.z = 50;\n```\n\n这意味着这两点将不再共享同一个类。实际上，`p2`已经成为一个全新的类，这对于该对象在内存中的位置和可用的优化有影响。JavaScript 被设计成一种高度灵活的语言，但这一事实造成了许多死角，而死角使优化变得困难。\n\n由 JavaScript 的动态特性造成的优化的另一个问题是，没有任何优化是确定的。所有关于类型的优化都必须使用资源不断检查它们的类型假设是否仍然有效。此外，优化器必须保留未优化的代码，以防这些假设被证明是错误的。优化器可以确定最初做出的假设不是正确的假设。这将导致“紧急情况”，优化器将丢弃其优化的代码并进行去优化，从而导致性能不一致。\n\n最后，JavaScript 是一种带有**垃圾收集** ( **GC** )的语言，它允许 JavaScript 代码的作者在编写代码时承担更少的内存管理负担。虽然这对开发人员来说很方便，但它只是在运行时将内存管理的工作推给了机器。多年来，GC 在 JavaScript 中的效率已经提高了很多，但它仍然是 JavaScript 引擎在运行 JavaScript 时必须做的工作，而在运行 WebAssembly 时不需要做。\n\n执行 WebAssembly 模块删除了运行 JavaScript 代码所需的许多步骤。WebAssembly 消除了解析，因为 AOT 编译器完成了该功能。翻译是不必要的。我们的 JIT 编译器正在进行从字节码到机器代码的近乎一对一的翻译，速度非常快。由于 WebAssembly 中不存在的动态类型，JavaScript 需要进行大部分优化。硬件不可知的优化可以在网络程序集编译之前在 AOT 编译器中完成。JIT 优化器只需要执行特定于硬件的优化，而 WebAssembly AOT 编译器却不能。\n\n以下是 JavaScript 引擎运行网络程序集二进制文件时执行的步骤:\n\n![](img/1d7fb906-b411-416d-a1a2-b32a2812d1ac.png)\n\nFigure 1.2: The steps required to execute WebAssembly\n\n我想提到的最后一点不是当前 MVP 的一个特性，而是 WebAssembly 支持的潜在未来。所有让现代 JavaScript 变快的代码都会占用内存。保留未优化的救助代码的旧副本会占用内存。解析器、解释器和垃圾收集器都会占用内存。在我的桌面上，Chrome 经常占用大约 1 GB 的内存。通过使用[https://www.classicsolitaire.com](https://www.classicsolitaire.com)在我的网站上运行一些测试，我可以看到在 JavaScript 引擎打开的情况下，Chrome 浏览器占用了大约 654 MB 的内存。\n\n以下是任务管理器截图:\n\n![](img/8d052928-1b7a-4284-a3c1-f65bae8e02e6.png)\n\nFigure 1.3: Chrome Task Manager process screenshot with JavaScript\n\n关闭 JavaScript 后，Chrome 浏览器占用约 295MB。\n\n以下是任务管理器截图:\n\n![](img/2a92e10a-f57d-4275-92d0-09154ab9b9ea.png)\n\nFigure 1.4: Chrome Task Manager process screenshot without JavaScript\n\n因为这是我的一个网站，我知道那个网站上只有几百千字节的 JavaScript 代码。让我感到有点震惊的是，运行这么少量的 JavaScript 代码可以增加大约 350 MB 的浏览器占用空间。目前，WebAssembly 运行在现有的 JavaScript 引擎之上，仍然需要相当多的 JavaScript 粘合代码才能使一切正常工作，但从长远来看，WebAssembly 不仅可以让我们加快在 web 上的执行速度，还可以让我们以更小的内存占用来完成这项工作。\n\n# WebAssembly 会取代 JavaScript 吗？\n\n这个问题的简单答案是不会很快。目前，WebAssembly 还处于 MVP 阶段。在这一阶段，用例的数量仅限于 WebAssembly 与 JavaScript 和 **D** **文档对象模型** ( **DOM** )来回受限的应用。WebAssembly 目前还不能直接与 DOM 进行交互，Emscripten 使用 JavaScript“粘合代码”来实现这种交互。这种交互可能很快就会改变，可能在您阅读这篇文章的时候，但是在接下来的几年里，WebAssembly 将需要额外的功能来增加可能的用例数量。\n\nWebAssembly 不是一个“功能完整”的平台。目前，它不能用于任何需要 GC 的语言。这种情况将会改变，最终，几乎所有强类型语言都将以 WebAssembly 为目标。此外，WebAssembly 将很快与 JavaScript 紧密集成，允许 React、Vue 和 Angular 等框架开始用 WebAssembly 替换其大量 JavaScript 代码，而不会影响**应用编程接口** ( **API** )。React 团队目前正在努力改进 React 的性能。\n\n从长远来看，JavaScript 有可能编译成 WebAssembly。由于技术原因，这还有很长的路要走。JavaScript 不仅需要 GC(目前不支持)，而且由于其动态特性，JavaScript 还需要运行时分析器来优化。因此，JavaScript 将产生优化非常差的代码，或者需要进行重大修改来支持严格的类型。更有可能的是，一种语言，比如 TypeScript，会添加允许它编译成 WebAssembly 的特性。\n\nThe *AssemblyScript* project in development on GitHub is working on a TypeScript-to-WebAssembly compiler. This project creates JavaScript and uses Binaryen to compile that JavaScript into WebAssembly. How AssemblyScript handles the problem of garbage collection is unclear. For more information, refer to [https://github.com/AssemblyScript/assemblyscript](https://github.com/AssemblyScript/assemblyscript).\n\nJavaScript 目前在网络上无处不在；有大量用 JavaScript 开发的库和框架。即使有一大群开发人员渴望用 C++ 或 Rust 重写整个 web，WebAssembly 也还没有准备好取代这些 JavaScript 库和框架。浏览器制造商已经付出了巨大的努力来使 JavaScript 运行得(相对)快，所以 JavaScript 可能仍然是网络的标准脚本语言。网络将永远需要一种脚本语言，无数的开发人员已经投入工作来使 JavaScript 成为这种脚本语言，所以 JavaScript 似乎不太可能消失。\n\n然而，需要一种 WebAssembly 可能实现的 web 编译格式。编译代码目前可能是网络上的一个小众领域，但它几乎是其他任何地方的标准。随着 WebAssembly 接近功能完备的状态，它将提供比 JavaScript 更多的选择和更好的性能，业务、框架和库将逐渐向它迁移。\n\n# 什么是 asm.js？\n\n使用 JavaScript 在网络浏览器中实现类似本机速度的早期尝试是 asm.js。尽管这个目标已经实现，并且 asm.js 被所有主要的浏览器供应商采用，但它从未被开发人员广泛采用。asm.js 的美妙之处在于，它仍然可以在大多数浏览器中运行，即使是在那些没有进行优化的浏览器中。asm.js 背后的想法是，类型化数组可以在 JavaScript 中用来伪造 C++ 内存堆。浏览器模拟 C++ 中的指针和内存分配，以及类型。一个设计良好的 JavaScript 引擎可以避免动态类型检查。使用 asm.js，浏览器制造商可以通过假装这个版本的 JavaScript 不是动态类型的，来回避许多由 JavaScript 的动态特性所产生的优化问题。Emscripten 被设计为 C++ 到 JavaScript 的编译器，它很快采用了 asm.js 作为 JavaScript 的子集，因为它在大多数浏览器中的性能都有所提高。由 asm.js 驱动的性能改进引领了 WebAssembly 的发展。用于使 asm.js 表现良好的相同引擎修改可以用于引导 WebAssembly MVP。只需要添加一个字节码到字节码的编译器，就可以获取 WebAssembly 字节码，并直接将其转换为浏览器使用的 IR 字节码。\n\nAt the time of writing, Emscripten does not compile directly from LLVM to WebAssembly. Instead, it compiles to asm.js and uses a tool called Binaryen to convert the asm.js output from Emscripten into WebAssembly.\n\n# LLVM 简介\n\nEmscripten 是我们将用来把 C++ 编译成 WebAssembly 的工具。在讨论 Emscripten 之前，我需要解释一种称为 LLVM 的技术及其与 Emscripten 的关系。\n\n首先，花点时间想想航空公司(和我一起呆在这里)。航空公司想把乘客从一个机场送到另一个机场。但是提供从地球上每一个机场到每一个其他机场的直达航班是具有挑战性的。这意味着航空公司将不得不提供大量直飞航班，如俄亥俄州阿克伦至印度孟买的航班。让我们回到 20 世纪 90 年代——那是编译器世界的状态。如果你想从 C++ 编译到 ARM，你需要一个能够将 C++ 编译到 ARM 的编译器。如果你需要从 Pascal 编译到 x86，你需要一个能够从 Pascal 编译到 x86 的编译器。这就像任何两个城市之间都只有直达航班:语言和硬件的每一个组合的编译器。结果要么是你必须限制你为之编写编译器的语言的数量，限制你可以用那种语言支持的平台的数量，或者更可能的是，两者兼而有之。\n\n2003 年，伊利诺伊大学的一位名叫克里斯·拉特纳的学生想知道，“如果我们为编程语言创建一个中心辐射模型会怎么样？”他的想法导致了 LLVM，它最初代表“低级虚拟机”我们的想法是，不是为任何可能的发行版编译源代码，而是为 LLVM 编译它。中间语言和最终输出语言之间有编译器。理论上，这意味着如果您在下图的右侧开发一个新的目标平台，您将立即在左侧获得所有语言:\n\n![](img/e5aaec1e-100f-4f61-9bb7-d072053b93dc.png)\n\nFigure 1.5: LLVM as a hub between programming languages and the hardware To learn more about LLVM, visit the LLVM project home page at [https://llvm.org](https://llvm.org) or read the *LLVM Cookbook*, *Mayur Padney*, *and Suyog Sarda*, *Packt Publishing*: [https://www.packtpub.com/application-development/llvm-cookbook](https://www.packtpub.com/application-development/llvm-cookbook).\n\n# WebAssembly 文本简介\n\nWebAssembly 二进制不是一种语言，而是一种类似于为 ARM 或 x86 构建的构建目标。然而，字节码的结构不同于其他特定于硬件的构建目标。WebAssembly 字节码的设计者考虑的是网络。目的是创建一个紧凑且可流式传输的字节码。另一个目标是，用户应该能够在 WebAssembly 二进制文件上做一个“视图/源”，看看发生了什么。WebAssembly 文本是 WebAssembly 二进制文件的伴随代码，它允许用户以人类可读的形式查看字节码指令，类似于汇编语言让您看到哪些操作码以机器可读的形式执行的方式。\n\n对于习惯于为 ARM、x86 或 6502 等硬件编写汇编的人来说，WebAssembly 文本最初可能看起来并不熟悉(如果你是老派)。您用 S 表达式编写 WebAssembly 文本，它有一个括号很重的树结构。有些操作对于汇编语言来说也是非常高级的，比如 if/else 和循环操作码。如果你还记得 WebAssembly 不是为了直接在计算机硬件上运行而设计的，而是为了快速下载并翻译成机器代码，那就更有意义了。\n\n当您在处理 WebAssembly 文本时，另一件在一开始看起来有点陌生的事情是缺少注册器。WebAssembly 被设计成一个虚拟的*栈机*，它是*注册机*的替代品，比如你可能熟悉的 x86 和 ARM。堆栈机器比寄存器机器具有产生小得多的字节码的优势，这是为 WebAssembly 选择堆栈机器的一个很好的理由。堆栈机器中的每个操作码都将值推上或推下堆栈，而不是使用一系列寄存器来存储和操作数字(有时两者兼而有之)。例如，对 WebAssembly 中`i32.add`的调用从堆栈中取出两个 32 位整数，将它们相加，然后将它们的值推回到堆栈中。计算机硬件可以充分利用任何可用于执行该操作的寄存器。\n\n# 埃姆斯彭\n\n既然我们知道了 LLVM 是什么，我们就可以讨论 Emscripten 了。Emscripten 是为了将 LLVM IR 编译成 JavaScript 而开发的，但最近被更新为将 LLVM 编译成 WebAssembly。这个想法是，当你让 LLVM 编译器工作时，你可以受益于所有编译成 LLVM IR 的语言。实际上，WebAssembly 规范仍处于早期阶段，不支持 GC 等通用语言功能。因此，目前只支持 C/C++ 和 Rust 等非 GC 语言。WebAssembly 仍处于其开发的早期 MVP 阶段，但 GC 和其他通用语言功能的添加很快就会到来。当这种情况发生时，将编译成网络程序集的编程语言应该会激增。\n\n当 Emscripten 在 2012 年发布时，它的初衷是一个 LLVM 到 JavaScript 的编译器。2013 年，加入了对 asm.js 的支持，这是一种更快、更容易优化的 JavaScript 语言子集。2015 年，Emscripten 开始增加对 LLVM 到 WebAssembly 编译的支持。Emscripten 还为 C++ 和 JavaScript 提供了一个**软件开发工具包** ( **SDK** )，它提供了粘合代码，为用户提供了比目前仅由 WebAssembly MVP 提供的更好的 JavaScript 和 WebAssembly 之间的交互工具。Emscripten 还集成了一个名为 Clang 的 C/C++ 到 LLVM 编译器，这样你就可以把你的 C++ 编译成 WebAssembly 了。此外，Emscripten 将生成项目开始所需的 HTML 和 JavaScript 粘合代码。\n\nEmscripten is a very dynamic project and changes to the toolchain happen frequently. To stay up to date with the latest changes in Emscripten, visit the project home page at [https://emscripten.org](https://emscripten.org).\n\n# 在 Windows 上安装电子脚本\n\n我将保持这一部分的简短，因为这些说明可能会发生变化。您可以在电子脚本网站上找到官方的电子脚本下载和安装说明来补充这些说明:[https://emscripten.org/docs/getting_started/downloads.html](https://emscripten.org/docs/getting_started/downloads.html)。\n\n我们需要从 GitHub 上的 emsdk 源文件下载并构建 Emscripten。首先，我们将介绍在 Windows 上做什么。\n\nPython 2.7.12 或更高版本是先决条件。如果您没有安装高于 2.7.12 的 Python 版本，您需要从[python.org](http://python.org)获取 windows 安装程序，并首先安装该程序:[https://www.python.org/downloads/windows/](https://www.python.org/downloads/windows/)。\n\nIf you have installed Python and you are still getting errors telling you that Python is not found, you may need to add Python to your Windows PATH variable. For more information, refer to this tutorial: [https://www.pythoncentral.io/add-python-to-path-python-is-not-recognized-as-an-internal-or-external-command/](https://www.pythoncentral.io/add-python-to-path-python-is-not-recognized-as-an-internal-or-external-command/).\n\n如果您已经安装了 Git，克隆存储库相对简单:\n\n1.  运行以下命令克隆存储库:\n\n```cpp\ngit clone https://github.com/emscripten-core/emsdk.git\n```\n\n2.  无论你在哪里运行这个命令，它都会创建一个`emsdk`目录。使用以下命令输入该目录:\n\n```cpp\ncd emsdk\n```\n\n您可能没有安装 Git，在这种情况下，以下步骤将使您跟上进度:\n\n1.  在网络浏览器中访问以下网址:[https://github.com/emscripten-core/emsdk](https://github.com/juj/emsdk)。\n2.  您将在右侧看到一个绿色按钮，上面写着克隆或下载。下载压缩文件:\n\n![](img/bb90bfed-56b4-4e14-a43d-455d5d850245.png)\n\n3.  将下载的文件解压到`c:\\emsdk`目录。\n4.  在开始菜单中输入`cmd`并按*进入*，打开一个窗口命令提示符。\n5.  在那里，您可以通过键入以下内容来切换到`c:\\emsdk\\emsdk-master`目录:\n\n```cpp\n cd \\emsdk\\emsdk-master\n```\n\n此时，您是否安装了 Git 并不重要。让我们继续前进:\n\n1.  从运行以下命令的源代码中安装`emsdk`:\n\n```cpp\nemsdk install latest\n```\n\n2.  然后激活最新的`emsdk`:\n\n```cpp\nemsdk activate latest\n```\n\n3.  最后，设置我们的路径和环境变量:\n\n```cpp\nemsdk_env.bat\n```\n\nThis last step will need to be rerun from your install directory every time you open a new command-line window. Unfortunately, it does not permanently set the Windows environment variables. Hopefully, that will change in the future.\n\n# 在 Ubuntu 上安装 Emscripten\n\n如果您正在 Ubuntu 上安装，您应该能够使用`apt-get`包管理器和 git 来完成安装。让我们继续前进:\n\n1.  Python 是必需的，因此如果您没有安装 Python，请确保运行以下命令:\n\n```cpp\nsudo apt-get install python\n```\n\n2.  如果您尚未安装 Git，请运行以下命令:\n\n```cpp\nsudo apt-get install git\n```\n\n3.  现在您需要为`emsdk`克隆 Git 存储库:\n\n```cpp\ngit clone https://github.com/emscripten-core/emsdk.git\n```\n\n4.  更改您的目录，进入`emsdk`目录:\n\n```cpp\ncd emsdk\n```\n\n5.  从这里，您需要安装最新版本的 SDK 工具，激活它，并设置您的环境变量:\n\n```cpp\n./emsdk install latest\n./emsdk activate latest\nsource ./emsdk_env.sh\n```\n\n6.  要确保一切安装正确，请运行以下命令:\n\n```cpp\nemcc --version\n```\n\n# 使用 Emscripten\n\n我们从命令行运行 Emscripten 因此，您可以使用您选择的任何文本编辑器来编写您的 C/C++ 代码。个人比较偏爱 Visual Studio Code，可以在这里下载:[https://code.visualstudio.com/download](https://code.visualstudio.com/download)。\n\nVisual Studio Code 的一个优点是它有一个内置的命令行终端，可以让你在不切换窗口的情况下编译代码。它还有一个优秀的 C/C++ 扩展，你可以安装。只需从扩展菜单中搜索 C/C++ 并安装微软 C/C++ Intellisense 扩展即可。\n\n无论您为文本编辑器或集成开发环境选择什么，您都需要一段简单的 C 代码来测试 emcc 编译器。\n\n1.  创建一个新的文本文件并命名为`hello.c`。\n2.  将以下代码输入`hello.c`:\n\n```cpp\n#include <emscripten.h>\n#include <stdlib.h>\n#include <stdio.h>\n\nint main() {\n    printf(\"hello wasm\\n\");\n}\n```\n\n3.  现在我可以把`hello.c`文件编译成 WebAssembly，生成一个`hello.html`文件:\n\n```cpp\nemcc hello.c --emrun -o hello.html\n```\n\n4.  如果您想从`emrun`运行 HTML 页面，则`--emrun`标志是必需的。该标志添加将捕获`stdout`、`stderr`的代码，并在 C 代码中退出，`emrun`没有它将不起作用:\n\n```cpp\nemrun --browser firefox hello.html\n```\n\n用`--browser`标志运行`emrun`将选择您想要运行脚本的浏览器。`emrun`的行为似乎因浏览器而异。当 C 程序退出时，Chrome 会关闭窗口。这可能很烦人，因为我们只是想显示一个简单的打印信息。如果你有火狐，我建议用`--browser`旗运行`emrun`。\n\nI do not want to imply that Chrome cannot run WebAssembly. Chrome does have different behavior when a WebAssembly module exits. Because I was trying to keep our WebAssembly module as simple as possible, it exits when the main function completes. That is what is causing problems in Chrome. These problems will go away later when we learn about game loops.\n\n要了解哪些浏览器可供您使用，请运行以下命令:\n\n```cpp\nemrun --list_browsers\n```\n\n`emrun`应该在浏览器中打开一个 Emscripten 模板化的 HTML 文件。\n\n确保您有一个能够运行 WebAssembly 的浏览器。以下版本的主要浏览器应该可以使用网络组件:\n\n*   边缘 16\n*   火狐 52\n*   铬合金 57\n*   Safari 11\n*   歌剧 44\n\nIf you are familiar with setting up your own web server, you may want to consider using it rather than emrun. After using emrun for the first few chapters of this book, I returned to using my Node.js web server. I found it easier to have a Node-based web server up and running at all times, rather than restarting the emrun web server every time I wanted to test my code. If you know how to set up an alternative web server (such as one for Node, Apache, and IIS), you may use whatever web server you prefer. Although IIS requires some additional configuration to handle WebAssembly MIME types.\n\n# 其他安装资源\n\n为 Emscripten 创建安装指南会有些问题。WebAssembly 技术经常变化，当您阅读本文时，Emscripten 的安装过程可能会有所不同。如果您有任何问题，我建议您参考 Emscripten 网站上的下载和安装说明:[https://emscripten.org/docs/getting_started/downloads.html](https://emscripten.org/docs/getting_started/downloads.html)。\n\n你也可以参考 GitHub 上的 Emscripten 页面:[https://github.com/emscripten-core/emsdk](https://github.com/emscripten-core/emsdk)。\n\n谷歌集团有一个电子脚本论坛，如果你遇到安装问题，你可以在这里提问:[https://groups.google.com/forum/?nomobile=true#！论坛/电子脚本-讨论](https://groups.google.com/forum/?nomobile=true#!forum/emscripten-discuss)。\n\n你也可以在推特上联系我(`@battagline`)，我会尽力帮助你:[https://twitter.com/battagline](https://twitter.com/battagline)。\n\n# 摘要\n\n在这一章中，我们学习了什么是 WebAssembly，以及为什么它将是 web 应用开发的未来。我们了解了为什么我们需要 WebAssembly，尽管我们已经有了像 JavaScript 这样强大的语言。我们了解了为什么 WebAssembly 比 JavaScript 快得多，以及它如何有潜力提高性能领先优势。我们还讨论了 WebAssembly 取代 JavaScript 作为 web 上应用开发的事实标准的可能性。\n\n我们已经讨论了创建一个 WebAssembly 模块的实际方面，就像今天使用 Emscripten 和 LLVM 所做的那样。我们已经讨论了 WebAssembly 文本及其结构。我们还讨论了使用 Emscripten 来编译我们的第一个 WebAssembly 模块，以及使用它来创建运行该模块的 HTML 和 JavaScript 粘合代码。\n\n在下一章中，我们将进一步详细介绍如何使用 Emscripten 来创建我们的 WebAssembly 模块，以及用于驱动它的 HTML/CSS 和 JavaScript。"
  },
  {
    "path": "docs/handson-game-dev-wasm/02.md",
    "content": "# 二、HTML5 和 WebAssembly\n\n在这一章中，我们将向您展示我们为目标网络组件编写的 C 代码是如何与 HTML5、JavaScript 和 CSS 一起创建网页的。我们将教您如何创建一个新的 HTML 外壳文件，供 Emscripten 在创建我们的 WebAssembly 应用时使用。我们将讨论`Module`对象，以及 Emscripten 如何将其用作我们的 JavaScript 和 WebAssembly 模块之间的接口。我们将在我们的 HTML 页面上向您展示如何从 JavaScript 内部调用用 C 语言编写的 WebAssembly 函数。我们还将向您展示如何从我们的 C 代码中调用 JavaScript 函数。我们将讨论如何使用 CSS 来改善我们网页的外观。我们将向您介绍 HTML5 Canvas 元素，并展示如何从 JavaScript 中向 Canvas 显示图像。我们将简要讨论在 WebAssembly 模块的画布上移动这些图像。本章将让您了解一切是如何协同工作的，并为我们为 WebAssembly 应用开发的其他功能奠定基础。\n\nBeginning with this chapter and continuing through the remainder of the book, you will need image and font files from the GitHub project to compile the examples. For this chapter, you will need the `/Chapter02/spaceship.png` image file from the project directory. Please download the project from the following URL: [https://github.com/PacktPublishing/Hands-On-Game-Development-with-WebAssembly](https://github.com/PacktPublishing/Hands-On-Game-Development-with-WebAssembly). I highly recommend working along as you read each section of this chapter. You may use your favorite code editor and the command line to follow along. Even though we have provided links to download the code directly, it cannot be emphasized enough how much you will learn by actually following edits suggested in this chapter. You are going to make mistakes and learn a lot from them. If you decide to work along, another suggestion is the following: do not proceed to the next section unless your edit/steps in the current section are successful. If you need help, contact me on twitter (`@battagline`).\n\n在本章中，我们将涵盖以下主题:\n\n*   Emscripten 最小外壳文件\n*   创建一个新的 HTML 外壳和 C 文件\n*   定义我们的 CSS\n*   HTML5 与游戏开发\n*   向电子脚本模板添加画布\n\n# Emscripten 最小外壳文件\n\n我们用 Emscripten 创建的第一个构建使用了一个默认的 HTML 外壳文件。如果你有一个网站，这可能不是你希望你的网页看起来的样子。您可能更喜欢使用特定于您的设计或业务需求的 CSS 和 HTML5 来设计您的外观。例如，我用于网站的模板通常在游戏画布的左侧和右侧包含广告。这些网站的流量就是这样被货币化的。您可以选择在游戏画布上方添加网站徽标。还有一个文本区，Emscripten 记录来自`printf`或其他标准 IO 调用的输出。您可以选择完全删除这个`textarea`元素，也可以保留它，但将其隐藏起来，因为它对以后的调试很有用。\n\n要基于不是默认 Emscripten shell 的新 shell 文件构建 HTML 文件，我们必须使用`--shell-file`参数，将我们想要使用的新 HTML 模板文件传递给它，而不是 Emscripten 的默认值。新的`emcc`命令将如下所示:\n\n```cpp\nemcc hello.c --shell-file new_shell.html --emrun -o hello2.html\n```\n\n暂时不要执行这个命令。我们的项目目录中当前没有`new_shell.html`文件，因此在该文件存在之前运行命令将导致错误消息。我们需要创建`new_shell.html`文件，并将其用作 HTML 外壳，而不是 Emscripten 的默认 HTML 外壳。该 shell 文件必须遵循特定的格式。要构建它，我们必须从 Emscripten 的最小 HTML shell 文件开始，您可以在 GitHub 上找到它:\n\n[https://github . com/em script en-core/em script en/blob/master/src/shell _ minimum . html](https://github.com/emscripten-core/emscripten/blob/master/src/shell_minimal.html)\n\n我们将使用`shell_minimal.html`文件作为起点，编写自己的 HTML 外壳。最小外壳中的大部分内容是不需要的，因此我们将对其进行一些重要的编辑。我们将删除大部分代码来满足我们的目的。当您在文本编辑器中打开`shell_minimal.html`时，您会看到它以一个标准的 HTML 标题和一个`style`标签开始:\n\n```cpp\n<style>\n .emscripten { padding-right: 0; margin-left: auto; margin-right: auto;    \n               display: block; }\n textarea.emscripten { font-family: monospace; width: 80%; }\n div.emscripten { text-align: center; }\n div.emscripten_border { border: 1px solid black; }\n /* the canvas *must not* have any border or padding, or mouse coords \n    will be wrong */\n canvas.emscripten { border: 0px none; background-color: black; }\n .spinner {\n            height: 50px;\n            width: 50px;\n            margin: 0px auto;\n            -webkit-animation: rotation .8s linear infinite;\n            -moz-animation: rotation .8s linear infinite;\n            -o-animation: rotation .8s linear infinite;\n            animation: rotation 0.8s linear infinite;\n            border-left: 10px solid rgb(0,150,240);\n            border-right: 10px solid rgb(0,150,240);\n            border-bottom: 10px solid rgb(0,150,240);\n            border-top: 10px solid rgb(100,0,200);\n            border-radius: 100%;\n            background-color: rgb(200,100,250);\n          }\n @-webkit-keyframes rotation {\n         from {-webkit-transform: rotate(0deg);}\n         to {-webkit-transform: rotate(360deg);}\n  }\n @-moz-keyframes rotation {\n         from {-moz-transform: rotate(0deg);}\n         to {-moz-transform: rotate(360deg);}\n }\n @-o-keyframes rotation {\n         from {-o-transform: rotate(0deg);}\n         to {-o-transform: rotate(360deg);}\n }\n @keyframes rotation {\n         from {transform: rotate(0deg);}\n         to {transform: rotate(360deg);}\n }\n </style>\n```\n\nThis code is based on the version of `shell_minimal.html` available at the time of writing. No changes to this file are anticipated. However, WebAssembly is evolving quickly. Unfortunately, we cannot say with complete certainty that this file will remain unchanged by the time you read this. As mentioned earlier, if you run into problems, please feel free to contact me on Twitter (`@battagline`).\n\n我们删除了这个样式标签，这样你就可以用任何你喜欢的方式来设计你的代码。如果您喜欢他们的微调器加载图像并希望保留它，这是必要的，但是最好将所有这些都拉出来，并用从带有链接标签的 CSS 文件外部加载的 CSS 替换它，如下所示:\n\n```cpp\n<link href=\"shell.css\" rel=\"stylesheet\" type=\"text/css\">\n```\n\n再向下滚动一点，你会看到他们使用的负载指示器。我们最终会用自己的代码替换它，但目前，我们正在本地测试所有这些，我们的文件都很小，所以我们也会删除这些代码:\n\n```cpp\n<figure style=\"overflow:visible;\" id=\"spinner\">\n    <div class=\"spinner\"></div>\n    <center style=\"margin-top:0.5em\"><strong>emscripten</strong></center>\n</figure>\n<div class=\"emscripten\" id=\"status\">Downloading...</div>\n    <div class=\"emscripten\">\n        <progress value=\"0\" max=\"100\" id=\"progress\" hidden=1></progress>\n    </div>\n```\n\n之后还有一个 HTML5 `canvas`元素和一些其他与之相关的标签。我们最终将需要添加一个`canvas`元素，但是现在，我们将不使用`canvas`，所以部分代码也不是必需的:\n\n```cpp\n<div class=\"emscripten\">\n    <input type=\"checkbox\" id=\"resize\">Resize canvas\n    <input type=\"checkbox\" id=\"pointerLock\" checked>Lock/hide mouse \n     pointer&nbsp;&nbsp;&nbsp;\n    <input type=\"button\" value=\"Fullscreen\" onclick=\n    \"Module.requestFullscreen(document.getElementById\n    ('pointerLock').checked,\n            document.getElementById('resize').checked)\">\n </div>\n```\n\n在`canvas`之后，有一个`textarea`元素。这也不是必须的，但是使用它作为从我的 C 代码中执行的任何`printf`命令的打印位置是很好的。外壳周围有两个`<hr/>`标签，用于格式化，因此我们也可以删除这些标签:\n\n```cpp\n <hr/>\n <textarea class=\"emscripten\" id=\"output\" rows=\"8\"></textarea>\n <hr/>\n```\n\n下一件事是我们的 JavaScript。首先，我们删除了三个代表 HTML 元素的变量，因此我们也需要删除所有这些 JavaScript 变量:\n\n```cpp\nvar statusElement = document.getElementById('status');\nvar progressElement = document.getElementById('progress');\nvar spinnerElement = document.getElementById('spinner');\n```\n\nJavaScript 内部的`Module`对象是 Emscripten 生成的 JavaScript *粘合*代码用来与我们的 WebAssembly 模块交互的接口。它是 shell HTML 文件中最关键的部分，了解它在做什么至关重要。`Module`对象以两个数组开始，`preRun`和`postRun`。这些是将分别在模块加载之前和之后运行的函数数组。\n\n```cpp\nvar Module = {\n preRun: [],\n postRun: [],\n```\n\n出于演示目的，我们可以向这些数组添加如下函数:\n\n```cpp\npreRun: [function() {console.log(\"pre run 1\")},\n            function() {console.log(\"pre run 2\")}],\npostRun: [function() {console.log(\"post run 1\")},\n            function() {console.log(\"post run 2\")}],\n```\n\n这将从我们在[第 1 章](01.html)、*WebAssembly 和电子脚本简介*中创建的 hello WASM 应用中产生以下输出:\n\n```cpp\npre run 2\npre run 1\nstatus: Running...\nHello wasm\npost run 2\npost run 1\n```\n\nNotice that the `preRun` and `postRun` functions run in the reverse order in which they are placed in the array. We could use the `postRun` array to call a function that would initialize our WebAssembly wrappers, but, for demonstration purposes, we will instead call a JavaScript function from within our C `main()` function.\n\n`Module`对象中接下来的两个函数是`print`和`printErr`函数。`print`功能用于打印对控制台和我们命名为`output`的`textarea`的`printf`调用的输出。您可以将此`output`更改为打印出任何 HTML 标记，但是，如果您的输出是原始 HTML，则必须运行几个注释掉的文本替换调用。以下是`print`功能的样子:\n\n```cpp\nprint: (function() {\n    var element = document.getElementById('output');\n    if (element) element.value = ''; // clear browser cache\n    return function(text) {\n        if (arguments.length > 1) text = \n        Array.prototype.slice.call(arguments).join(' ');\n        // These replacements are necessary if you render to raw HTML\n        //text = text.replace(/&/g, \"&amp;\");\n        //text = text.replace(/</g, \"&lt;\");\n        //text = text.replace(/>/g, \"&gt;\");\n        //text = text.replace('\\n', '<br>', 'g');\n        console.log(text);\n        if (element) {\n            element.value += text + \"\\n\";\n            element.scrollTop = element.scrollHeight; // focus on \n            bottom\n        }\n    };\n})(),\n```\n\n当我们的网络组件模块或粘合代码本身出现错误或警告时，`printErr`功能由粘合代码运行。`printErr`的输出只是控制台，尽管原则上，如果您想添加可以写入 HTML 元素的代码，您也可以这样做。这里是`printErr`代码:\n\n```cpp\nprintErr: function(text) {\n     if (arguments.length > 1) text = \n     Array.prototype.slice.call(arguments).join(' ');\n     if (0) { // XXX disabled for safety typeof dump == 'function') {\n       dump(text + '\\n'); // fast, straight to the real console\n     } else {\n         console.error(text);\n     }\n },\n```\n\n在`print`功能之后，还有一个`canvas`功能。设置该功能是为了提醒用户丢失的 WebGL 上下文。我们现在不需要这些代码，因为我们已经移除了 HTML 画布。当我们重新加入`canvas`元素时，我们需要恢复这个功能。更新它以处理丢失的上下文事件也是有意义的，而不仅仅是提醒用户。\n\n```cpp\ncanvas: (function() {\n     var canvas = document.getElementById('canvas');\n     // As a default initial behavior, pop up an alert when webgl \n        context is lost. To make your\n     // application robust, you may want to override this behavior \n        before shipping!\n     // See http://www.khronos.org/registry/webgl/specs/latest/1.0/#5.15.2\n     canvas.addEventListener(\"webglcontextlost\", function(e) { \n        alert('WebGL context lost. You will need to reload the page.'); \n        e.preventDefault(); }, false);\n     return canvas;\n })(),\n```\n\nThere are several different situations when your web page could lose its WebGL context. The context is your portal into the GPU, and your app's access to the GPU is managed by both the browser and the operating system. Let's take a trip to *The Land of Metaphor*, where we imagine the GPU is a bus, the web browser is the bus driver, and the apps using their context are a bunch of rowdy middle school kids. If the bus driver (browser) feels that the kids (apps) are getting too rowdy, he can stop the bus (GPU), throw all the kids off the bus (make the apps lose their context), and let them come back one at a time if they promise to behave. \n\n之后，最小外壳有一些代码来跟踪模块的状态和依赖关系。在这段代码中，我们可以删除对`spinnerElement`、`progressElement`和`statusElement`的引用。稍后，如果我们选择，我们可以用元素替换这些元素来跟踪加载模块的状态，但是，目前不需要它们。下面是最小外壳中的状态和运行依赖关系监控代码:\n\n```cpp\nsetStatus: function(text) {\n    if (!Module.setStatus.last) Module.setStatus.last = { time: \n        Date.now(), text: '' };\n    if (text === Module.setStatus.last.text) return;\n    var m = text.match(/([^(]+)\\((\\d+(\\.\\d+)?)\\/(\\d+)\\)/);\n    var now = Date.now();\n\n    // if this is a progress update, skip it if too soon\n    if (m && now - Module.setStatus.last.time < 30) return; \n    Module.setStatus.last.time = now;\n    Module.setStatus.last.text = text;\n    if (m) {\n        text = m[1];\n    }\n    console.log(\"status: \" + text);\n},\ntotalDependencies: 0,\nmonitorRunDependencies: function(left) {\n  this.totalDependencies = Math.max(this.totalDependencies, left);\n    Module.setStatus(left ? 'Preparing... (' + (this.totalDependencies-\n                     left) + '/' + this.totalDependencies + ')' : 'All \n                     downloads complete.');\n}\n};\n Module.setStatus('Downloading...');\n```\n\n最小外壳文件中的最后一段 JavaScript 代码决定了在浏览器出错时 JavaScript 会做什么:\n\n```cpp\nwindow.onerror = function() {\n    Module.setStatus('Exception thrown, see JavaScript console');\n    Module.setStatus = function(text) {\n        if (text) Module.printErr('[post-exception status] ' + text);\n    };\n```\n\n在我们的 JavaScript 之后，还有一行更重要的内容:\n\n```cpp\n{{{ SCRIPT }}}\n```\n\n这个标签告诉 Emscripten 在这里放置到 JavaScript 粘合代码的链接。下面是一个编译成最终 HTML 文件的例子:\n\n```cpp\n<script async type=\"text/javascript\" src=\"shell-min.js\"></script>\n```\n\n`shell-min.js`是由 Emscripten 构建的 JavaScript 粘合代码。在下一节中，我们将学习如何创建自己的 HTML shell 文件。\n\n# 创建一个新的 HTML 外壳和 C 文件\n\n在本节中，我们将创建一个新的`shell.c`文件，该文件公开了从我们的 JavaScript 调用的几个函数。我们还将使用`EM_ASM`来调用`InitWrappers`函数，该函数将在我们将要创建的新的 HTML shell 文件中定义。这个函数将在 JavaScript 中创建包装器，可以调用在 WebAssembly 模块中定义的函数。在创建新的 HTML 外壳文件之前，我们需要创建将由 HTML 外壳内的 JavaScript 包装器调用的 C 代码:\n\n1.  如下创建新的`shell.c`文件:\n\n```cpp\n#include <emscripten.h>\n#include <stdlib.h>\n#include <stdio.h>\n\nint main() {\n    printf(\"Hello World\\n\");\n    EM_ASM( InitWrappers() );\n    printf(\"Initialization Complete\\n\");\n}\n\nvoid test() {\n    printf(\"button test\\n\");\n}\n\nvoid int_test( int num ) {\n    printf(\"int test=%d\\n\", num);\n}\n\nvoid float_test( float num ) {\n    printf(\"float test=%f\\n\", num);\n}\n\nvoid string_test( char* str ) {\n    printf(\"string test=%s\\n\", str);\n}\n```\n\n当加载网络组件模块时，`main`功能运行。此时，`Module`对象可以使用`cwrap`来创建该函数的 JavaScript 版本，我们可以将它与 HTML 元素上的`onclick`事件联系起来。在`main`函数内部，`EM_ASM( InitWrappers() );`代码调用一个`InitWrappers()`函数，这个函数是在 HTML shell 文件的 JavaScript 内部定义的。DOM 使用事件来调用接下来的四个函数。\n\nAnother way we could have initialized the wrappers is by calling the `InitWrappers()` function from the `Module` object `postRun: []` array.\n\n我们将把对`test()`函数的调用与 DOM 中的按钮点击联系起来。通过使用`printf`语句，`int_test`函数将作为 DOM 中输入字段的值传递，并将向控制台和包含该整数的`textarea`元素打印一条消息。`float_test`功能将一个数字作为浮点传递，打印到控制台和`textarea`元素。`string_test`函数将打印出一个从 JavaScript 传入的字符串。\n\n现在，我们将把下面的代码添加到一个 HTML shell 文件中，并将其称为`new_shell.html`。该代码基于由 Emscripten 团队创建的 *Emscripten 最小外壳文件*，并在上一节中进行了解释。我们将把整个网页分成四个部分。\n\n首先，是 HTML 文件的开头和`head`元素:\n\n```cpp\n<!doctype html>\n<html lang=\"en-us\">\n<head>\n    <meta charset=\"utf-8\">\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">\n    <title>New Emscripten Shell</title>\n    <link href=\"shell.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n```\n\n接下来，是`body`标记的开始。之后，我们有几个 HTML `input`元素以及`textarea`元素:\n\n```cpp\n<body>\n    <div class=\"input_box\">&nbsp;</div>\n    <div class=\"input_box\">\n        <button id=\"click_me\" class=\"em_button\">Click Me!</button>\n    </div>\n    <div class=\"input_box\">\n        <input type=\"number\" id=\"int_num\" max=\"9999\" min=\"0\" step=\"1\" \n         value=\"1\" class=\"em_input\">\n        <button id=\"int_button\" class=\"em_button\">Int Click!</button>\n    </div>\n    <div class=\"input_box\">\n        <input type=\"number\" id=\"float_num\" max=\"99\" min=\"0\" \n          step=\"0.01\" value=\"0.0\" class=\"em_input\">\n        <button id=\"float_button\" class=\"em_button\">Float Click!</button>\n    </div>\n    <div class=\"input_box\">&nbsp;</div>\n    <textarea class=\"em_textarea\" id=\"output\" rows=\"8\"></textarea>\n    <div id=\"string_box\">\n        <button id=\"string_button\" class=\"em_button\">String Click!</button>\n        <input id=\"string_input\">\n    </div>\n```\n\n在我们的 HTML 之后，我们有了我们的`script`标签的开始，以及一些我们添加到默认 shell 文件中的 JavaScript 代码:\n\n```cpp\n\n <script type='text/javascript'>\n    function InitWrappers() {\n        var test = Module.cwrap('test', 'undefined');\n        var int_test = Module.cwrap('int_test', 'undefined', ['int']);\n        var float_test = Module.cwrap('float_test', 'undefined', \n                                       ['float']);\n        var string_test = Module.cwrap('string_test', 'undefined', \n                                       ['string']);\n        document.getElementById(\"int_button\").onclick = function() {\n\n        if( int_test != null ) {\n            int_test(document.getElementById('int_num').value);\n        }\n    }\n\n    document.getElementById(\"string_button\").onclick = function() {\n        if( string_test != null ) {\n            string_test(document.getElementById('string_input').value);\n        }\n    }\n\n    document.getElementById(\"float_button\").onclick = function() {\n        if( float_test != null ) {\n            float_test(document.getElementById('float_num').value);\n        }\n    }\n\n    document.getElementById(\"click_me\").onclick = function() {\n        if( test != null ) {\n            test();\n        }\n    }\n }\n\nfunction runbefore() {\n    console.log(\"before module load\");\n}\n\nfunction runafter() {\n    console.log(\"after module load\");\n}\n```\n\n接下来，我们有了从默认 shell 文件中引入的`Module`对象。在`Module`对象之后，我们有了`script`标记的结尾，`{{{ SCRIPT }}}`标记在编译时被 Emscripten 替换，以及我们文件中的结尾标记:\n\n```cpp\nvar Module = {\n    preRun: [runbefore],\n    postRun: [runafter],\n    print: (function() {\n        var element = document.getElementById('output');\n        if (element) element.value = ''; // clear browser cache\n            return function(text) {\n                if (arguments.length > 1) text = \n                   Array.prototype.slice.call(arguments).join(' ');\n                /*\n                // The printf statement in C is currently writing to a \n                   textarea. If we want to write\n                // to an HTML tag, we would need to run these lines of \n                   codes to make our text HTML safe\n                text = text.replace(/&/g, \"&amp;\");\n                text = text.replace(/</g, \"&lt;\");\n                text = text.replace(/>/g, \"&gt;\");\n                text = text.replace('\\n', '<br>', 'g');\n                */\n                console.log(text);\n                if (element) {\n                    element.value += text + \"\\n\";\n                    element.scrollTop = element.scrollHeight; \n                     // focus on bottom\n                } \n            };\n        })(),\n        printErr: function(text) {\n            if (arguments.length > 1) text = \n                Array.prototype.slice.call(arguments).join(' ');\n            if (0) { // XXX disabled for safety typeof dump == \n                       'function') {\n                dump(text + '\\n'); // fast, straight to the real                     console\n            } else {\n                console.error(text);\n            }\n        },\n        setStatus: function(text) {\n            if (!Module.setStatus.last) Module.setStatus.last = { time: \n                Date.now(), text: '' };\n            if (text === Module.setStatus.last.text) return;\n            var m = text.match(/([^(]+)\\((\\d+(\\.\\d+)?)\\/(\\d+)\\)/);\n            var now = Date.now();\n\n            // if this is a progress update, skip it if too soon\n            if (m && now - Module.setStatus.last.time < 30) return;\n            Module.setStatus.last.time = now;\n            Module.setStatus.last.text = text;\n\n            if (m) {\n                text = m[1];\n            }\n            console.log(\"status: \" + text);\n        },\n        totalDependencies: 0,\n        monitorRunDependencies: function(left) {\n            this.totalDependencies = Math.max(this.totalDependencies,                                               \n                                              left);\n            Module.setStatus(left ? 'Preparing... (' + \n            (this.totalDependencies-left) + '/' +             \n            this.totalDependencies + ')' : 'All downloads complete.');\n        }\n    };\n    Module.setStatus('Downloading...');\n    window.onerror = function() {\n    Module.setStatus('Exception thrown, see JavaScript console');\n    Module.setStatus = function(text) {\n        if (text) Module.printErr('[post-exception status] ' + text);\n    };\n};\n</script>\n{{{ SCRIPT }}}\n</body>\n</html>\n```\n\n前面四个部分都组成了一个名为`new_shell.html`的外壳文件。您可以通过将最后四个部分键入一个名为`new_shell.html`的文件来创建该代码，或者您可以从我们的 GitHub 页面下载该文件，网址为[https://GitHub . com/packt publishing/hand-Game-Development-with-WebAssembly/blob/master/chapter 02/new _ shell . html](https://github.com/PacktPublishing/Hands-On-Game-Development-with-WebAssembly/blob/master/Chapter02/new_shell.html)。\n\n现在我们已经看到了大块的整个`new_shell.html`文件，我们可以花一点时间分解基本部分，并在粒度级别上对其进行检查。您会注意到，我们删除了所有的 CSS 样式代码，并创建了一个新的`shell.css`文件，包括以下行:\n\n```cpp\n<link href=\"shell.css\" rel=\"stylesheet\" type=\"text/css\">\n```\n\n接下来，我们修改了该文件中的 HTML 代码，以创建将与 WebAssembly 模块交互的元素。首先，我们将在 WebAssembly 模块中添加一个调用`test()`函数的按钮:\n\n```cpp\n<div class=\"input_box\">\n    <button id=\"click_me\" class=\"em_button\">Click Me!</button>\n</div>\n```\n\n我们将在已经创建的`shell.css`文件中设置按钮及其包含的`div`元素的样式。我们需要在稍后编写的 JavaScript 代码中定义这个`button`元素的`onclick`事件将调用的函数。我们将对 HTML 中定义的两个输入/按钮对进行类似的操作，如下面的代码块所示:\n\n```cpp\n<div class=\"input_box\">\n    <input type=\"number\" id=\"int_num\" max=\"9999\" min=\"0\" step=\"1\" \n     value=\"1\" class=\"em_input\">\n    <button id=\"int_button\" class=\"em_button\">Int Click!</button>\n</div>\n<div class=\"input_box\">\n    <input type=\"number\" id=\"float_num\" max=\"99\" min=\"0\" step=\"0.01\" \n     value=\"0.0\" class=\"em_input\">\n    <button id=\"float_button\" class=\"em_button\">Float Click!</button>\n</div>\n```\n\n就像我们对第一个`button`元素所做的那样，我们将把接下来的两个按钮与调用 WebAssembly 模块的函数联系起来。这些函数调用还将把`input`元素中定义的值传递给 WebAssembly 函数。我们将`textarea`元素作为发生在 WebAssembly 模块中的`printf`调用的输出。我们在 CSS 文件中对其进行了不同的样式设计，但是我们将保持功能不变:\n\n```cpp\n<textarea class=\"em_textarea\" id=\"output\" rows=\"8\"></textarea>\n<div id=\"string_box\">\n    <button id=\"string_button\" class=\"em_button\">String Click!</button>\n    <input id=\"string_input\">\n</div>\n```\n\n在`textarea`元素下面，我们又增加了一个`button`和一个`string` `input`元素。该按钮将调用 WebAssembly 模块内的`string_test`函数，将`string_input`元素内的值作为 C `char*`参数传递给它。\n\n现在我们已经定义了 HTML 中需要的所有元素，我们将遍历并添加一些 JavaScript 代码，将 JavaScript 和 WebAssembly 模块绑定在一起。我们首先需要做的是定义`InitWrappers`函数。`InitWrappers`将从 C 代码中的`main`函数内部调用:\n\n```cpp\nfunction InitWrappers() {\n    var test = Module.cwrap('test', 'undefined');\n    var int_test = Module.cwrap('int_test', 'undefined', ['int']);\n    var float_test = Module.cwrap('float_test', 'undefined', \n                                   ['float']);\n    var string_test = Module.cwrap('string_test', 'undefined',\n                                     ['string']);\n    document.getElementById(\"int_button\").onclick = function() {\n        if( int_test != null ) {\n            int_test(document.getElementById('int_num').value);\n        }\n    }\n\n    document.getElementById(\"string_button\").onclick = function() {\n        if( string_test != null ) {\n            string_test(document.getElementById('string_input').value);\n        }\n    }\n\n    document.getElementById(\"float_button\").onclick = function() {\n        if( float_test != null ) {\n            float_test(document.getElementById('float_num').value);\n        }\n    }\n\n    document.getElementById(\"click_me\").onclick = function() {\n        if( test != null ) {\n            test();\n        }\n    }\n}\n```\n\n该函数使用`Module.cwrap`在 WebAssembly 模块内部围绕导出的函数创建 JavaScript 函数包装器。我们传递给`cwrap`的第一个参数是我们正在包装的 C 函数的名称。所有这些 JavaScript 函数都将返回`undefined`。JavaScript 没有像 C 一样的`void`类型，所以当我们在 JavaScript 中声明`return`类型时，我们需要使用`undefined`类型来代替。如果函数返回一个`int`或一个`float,`，我们需要在这里输入`'number'`值。传递到`cwrap`的最后一个参数是一个字符串数组，表示传递到网络组件模块的参数的 C 类型。\n\n在我们定义了函数的 JavaScript 包装器之后，我们需要从按钮中调用它们。这些调用中的第一个是对 WebAssembly `int_test`函数的调用。以下是我们如何为`int_button`设置`onclick`事件:\n\n```cpp\ndocument.getElementById(\"int_button\").onclick = function() {\n    if( int_test != null ) {\n        int_test(document.getElementById('int_num').value);\n    }\n}\n```\n\n我们首先要做的是检查`int_test`是否定义。如果是这样，我们调用前面解释的`int_test`包装器，将来自`int_num`输入的值传递给它。然后，我们对所有其他按钮进行类似的操作。\n\n接下来我们要做的是创建一个`runbefore`和`runafter`函数，我们将它放在`Module`对象上的`preRun`和`postRun`数组中:\n\n```cpp\nfunction runbefore() {\n    console.log(\"before module load\");\n}\nfunction runafter() {\n    console.log(\"after module load\");\n}\nvar Module = {\n    preRun: [runbefore],\n    postRun: [runafter],\n```\n\n这将导致“模块加载前”在模块加载前打印到控制台，而“模块加载后”在模块加载后打印。这些功能不是必需的；它们旨在展示如何在加载 WebAssembly 模块之前和之后运行代码。如果您不想从 WebAssembly 模块中的`main`函数调用`InitWrappers`函数，您可以将该函数放在`postRun`数组中。\n\nJavaScript 代码的其余部分类似于您在由 Emscripten 创建的`shell_minimal.html`文件中找到的内容。我们删除了本演示中多余的代码，例如与`spinnerElement`、`progressElement`和`statusElement`相关的代码，以及与 HTML5 `canvas`相关的代码。并不是说将代码留在 JavaScript 中有什么问题，但是它对于我们的演示来说并不是真正必要的，所以我们删除了它，以将这个 shell 减少到所需的最低限度。\n\n# 定义 CSS\n\n现在我们已经有了一些基本的 HTML，我们需要创建一个新的`shell.css`文件。没有任何 CSS 样式，我们的页面看起来非常糟糕。\n\n没有样式的页面类似于如下所示的页面:\n\n![](img/f8b0d833-eab2-4125-9705-d44e21ddf664.png)\n\nFigure 2.1: The Hello WebAssembly app without a CSS style\n\n幸运的是，一点点 CSS 可以让我们的网页看起来更像样。以下是我们正在创建的新`shell.css`文件的外观:\n\n```cpp\nbody {\n    margin-top: 20px;\n}\n\n.input_box {\n    width: 20%;\n    display: inline-block;\n}\n.em_button {\n    width: 45%;\n    height: 40px;\n    background-color: orangered;\n    color: white;\n    border: 2px solid white;\n    font-size: 20px;\n    border-radius: 8px;\n    transition-duration: 0.5s;\n}\n\n.em_button:hover {\n    background-color: orange;\n    color: white;\n    border: 2px solid white;\n}\n\n.em_input {\n    width: 45%;\n    height: 20px;\n    font-size: 20px;\n    background-color: darkslategray;\n    color: white;\n    padding: 6px;\n}\n\n#output {\n    background-color: darkslategray;\n    color: white;\n    font-size: 16px;\n    padding: 10px;\n    padding-right: 0;\n    margin-left: auto;\n    margin-right: auto;\n    display: block;\n    width: 60%;\n}\n\n#string_box {\n    padding-top: 10px;\n    margin-left: auto;\n    margin-right: auto;\n    display: block;\n    width: 60%;\n}\n\n#string_input {\n    font-size: 20px;\n    background-color: darkslategray;\n    color: white;\n    padding: 6px;\n    margin-left: 5px;\n    width: 45%;\n    float: right;\n}\n```\n\n让我快速浏览一下我们需要做什么来设计这个页面。这本书不是关于 CSS 的书，但是粗略地涵盖一下主题并没有什么坏处。\n\n1.  我们要做的第一件事是在页面主体上留出一点 20 像素的空白，在浏览器工具栏和页面内容之间留出一点空间:\n\n```cpp\nbody {\n    margin-top: 20px;\n}\n```\n\n2.  我们创建了五个输入框，每个占据浏览器宽度的`20%`。左侧和右侧的框中没有任何内容，因此内容占据了浏览器宽度的 60%。它们显示为内嵌块，因此它们在屏幕上水平排列。以下是实现它的 CSS:\n\n```cpp\n.input_box {\n    width: 20%;\n    display: inline-block;\n}\n```\n\n3.  然后，我们有几个类来使用名为`em_button`的类来设计按钮的样式:\n\n```cpp\n.em_button {\n    width: 45%;\n    height: 40px;\n    background-color: orangered;\n    color: white;\n    border: 0px;\n    font-size: 20px;\n    border-radius: 8px;\n    transition-duration: 0.2s;\n}\n\n.em_button:hover {\n    background-color: orange;\n}\n```\n\n我们已经设置了按钮宽度来占据包含元素的`45%`。我们将按钮高度设置为 40 像素。我们已经将按钮的颜色设置为`orangered`，文本颜色设置为`white`。我们通过将边框宽度设置为 0 像素来移除边框。我们将字体大小设置为 20 像素，并赋予它 8 像素的边框半径，这为按钮提供了圆润的外观。最后一行设置当用户悬停在按钮上时转换到新颜色所需的时间。\n\n在我们完成`em_button`类的定义之后，我们定义`em_button:hover`类，当用户悬停在按钮上时，它会改变按钮的颜色。\n\nSome versions of Safari require the line `-webkit-transition-duration: 0.2s;` inside the `em_button` class definition to have a transition to the hover state. Without this line, the button would instantly change from `orangered` to `orange` in some versions of Safari, rather than transitioning over 200 milliseconds.\n\n我们定义的下一个类是`input`元素:\n\n```cpp\n.em_input {\n    width: 45%;\n    height: 20px;\n    font-size: 20px;\n    background-color: darkslategray;\n    color: white;\n    padding: 6px;\n}\n```\n\n我们一开始就设定了`height``width``font-size`。我们将背景颜色设置为带有`white`文字的`darkslategray`。我们添加了`6`像素的填充，这样字体和`input`元素的边缘之间就有了一个小空间。\n\nCSS 元素名称前面的`#`样式是一个 ID，而不是一个类。标识定义了一个特定的元素，其中一个类(在 CSS 中以`.`开头)可以被分配给你的 HTML 中的多个元素。下一个 CSS 样式是具有输出标识的`textarea`:\n\n```cpp\n#output {\n    background-color: darkslategray;\n    color: white;\n    font-size: 16px;\n    padding: 10px;\n    margin-left: auto;\n    margin-right: auto;\n    display: block;\n    width: 60%;\n}\n```\n\n前两行设置背景和文本颜色。我们将字体大小设置为`16`像素，并添加`10`像素的填充。接下来的两行使用左右边距来居中`textarea`:\n\n```cpp\nmargin-left: auto;\nmargin-right: auto;\n```\n\n设置`display: block;`将该元素单独放在一条线上。将宽度设置为`60%`会使元素占据包含元素的`60%`，在本例中，这是浏览器的`body`标签。\n\n最后，我们对`string_box`和`string_input`元素进行样式化:\n\n```cpp\n#string_box {\n    padding-top: 10px;\n    margin-left: auto;\n    margin-right: auto;\n    display: block;\n    width: 60%;\n}\n\n#string_input {\n    font-size: 20px;\n    background-color: darkslategray;\n    color: white;\n    padding: 6px;\n    margin-left: 5px;\n    width: 45%;\n    float: right;\n}\n```\n\n`string_box`是包含字符串按钮和字符串输入元素的框。我们垫起盒子的顶部，在`string_box`和上面的`textarea`之间增加一些空间。`margin-left: auto`和`margin-right: auto`居中。然后，我们用`display:block`和`width: 60%`让它占用网页浏览器的`60%`。\n\n对于`string_input`元素，我们设置字体大小和颜色，并填充 6 个像素。我们设置了一个 5 像素的左边距，在元素和它的按钮之间留出一些空间。我们将其设置为占据包含元素宽度的`45%`，而`float: right`样式将元素推到包含元素的右侧。\n\n要构建我们的应用，我们需要运行`emcc`:\n\n```cpp\n emcc shell.c -o shell-test.html --shell-file new_shell.html -s NO_EXIT_RUNTIME=1 -s EXPORTED_FUNCTIONS=\"['_test', '_string_test', '_int_test', '_float_test', '_main']\" -s EXTRA_EXPORTED_RUNTIME_METHODS=\"['cwrap', 'ccall']\"\n```\n\n`EXPORTED_FUNCTIONS`用于定义从 JavaScript 调用的所有函数。它们以前面的`_`字符列出。`EXTRA_EXPORTED_RUNTIME_METHODS`用于使`cwrap`和`ccall`方法对我们 shell 文件中的 JavaScript 可用。我们目前没有使用`ccall`，这是`cwrap`的替代品，我们可能会选择在未来使用。\n\nIt is important to remember that you must run WebAssembly apps using a web server, or with `emrun`. If you would like to run your WebAssembly app using `emrun`, you must compile it with the `--emrun` flag. The web browser requires a web server to stream the WebAssembly module. If you attempt to open an HTML page that uses WebAssembly in a browser directly from your hard drive, that WebAssembly module will not load.\n\n现在我们已经添加了一些 CSS 样式，我们有了一个外观更好的应用:\n\n![](img/e8381064-6edf-40d8-8e9d-2f1e4f4b678c.png)\n\nFigure 2.2: The Hello WebAssembly app with a CSS style\n\n在下一节中，我们将讨论 HTML5 网络游戏开发。\n\n# HTML5 与游戏开发\n\n大部分的 HTML 渲染都是通过 HTML **文档对象模型** ( **DOM** )完成的。DOM 是所谓的*保留模式*图形库。保留模式图形保留一棵称为**场景图**的树。这个场景图跟踪我们模型中的所有图形元素以及如何渲染它们。保留模式图形的好处是开发人员可以直接管理它们。图形库完成所有繁重的工作，并为我们跟踪我们的对象以及它们呈现的位置。缺点是保留模式系统占用更多的内存，给开发人员提供的控制少得多。当我们编写 HTML5 游戏时，我们可以使用`<IMG>` HTML 元素拍摄在 DOM 中渲染的图像，并使用 JavaScript 或 CSS 动画移动这些元素，以直接操纵这些图像在 DOM 中的位置。\n\n然而，在大多数情况下，这会让游戏慢得令人痛苦。每次我们在 DOM 中移动一个对象时，它都会强制我们的浏览器重新计算 DOM 中所有其他对象的位置。正因为如此，从我们的 DOM 中操纵对象来制作网络游戏通常是行不通的。\n\n# 即时模式与保留模式\n\n即时模式通常被认为是保留模式的对立面，但是，在实践中，当我们为即时模式系统编写代码时，我们可能会在 API 的基础上进行构建，该 API 为我们提供了一些保留模式库的功能。立即模式迫使开发人员完成保留模式库完成的所有或大部分繁重工作。作为开发人员，我们被迫管理我们的场景图，并了解我们需要渲染什么图形对象，以及这些对象必须如何和何时渲染。简而言之，这是一个非常多的工作，但是如果做得好，回报是一个比使用 DOM 渲染更快的游戏。\n\n你现在可能会问自己:*我如何开始使用这个即时模式的东西*？进入 HTML5 画布！2004 年，苹果公司开发了画布元素，作为苹果专有浏览器技术的即时模式显示标签。画布分割出网页的一部分，这允许我们使用即时模式渲染来渲染该区域。这将使我们能够渲染到 DOM 的一部分(画布)，而不需要浏览器重新计算 DOM 中所有元素的位置。这允许浏览器使用计算机的**图形处理单元** ( **图形处理器**)进一步优化画布的渲染。\n\n# 向电子脚本模板添加画布\n\n在本章的前一部分，我们讨论了从外壳模板调用 Emscripten WebAssembly 应用。现在您已经知道了如何使 JavaScript 和 WebAssembly 之间的交互工作，我们可以将一个`canvas`元素添加回模板中，并开始使用 WebAssembly 模块操作那个`canvas`。我们将创建一个新的`.c`文件，该文件将调用一个 JavaScript 函数，向它传递一个`x`和`y`坐标。JavaScript 函数将操纵一个宇宙飞船图像，使其在`canvas`周围移动。我们还将创建一个名为`canvas_shell.html`的全新 shell 文件。\n\n正如我们在 shell 的前一个版本中所做的那样，我们将从将这个文件分成四个部分开始，在更高的层次上讨论它。然后，我们将逐一讨论该文件的基本部分。\n\n1.  HTML 文件的开头以开始的`HTML`标签和`head`元素开始:\n\n```cpp\n<!doctype html>\n<html lang=\"en-us\">\n<head>\n    <meta charset=\"utf-8\">\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">\n    <title>Canvas Shell</title>\n    <link href=\"canvas.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n```\n\n2.  之后，我们有了开始的`body`标记，并且我们已经删除了这个文件的早期版本中的许多 HTML 元素:\n\n```cpp\n<body>\n    <canvas id=\"canvas\" width=\"800\" height=\"600\" oncontextmenu=\"event.preventDefault()\"></canvas>\n    <textarea class=\"em_textarea\" id=\"output\" rows=\"8\"></textarea>\n    <img src=\"spaceship.png\" id=\"spaceship\">\n```\n\n3.  接下来是开始的`script`标记，一些全局 JavaScript 变量，以及我们添加的一些新函数:\n\n```cpp\n    <script type='text/javascript'>\n        var img = null;\n        var canvas = null;\n        var ctx = null;\n        function ShipPosition( ship_x, ship_y ) {\n            if( img == null ) {\n                return;\n            }\n            ctx.fillStyle = \"black\";\n            ctx.fillRect(0, 0, 800, 600);\n            ctx.save();\n            ctx.translate(ship_x, ship_y);\n            ctx.drawImage(img, 0, 0, img.width, img.height);\n            ctx.restore();\n        }\n        function ModuleLoaded() {\n            img = document.getElementById('spaceship');\n            canvas = document.getElementById('canvas');\n            ctx = canvas.getContext(\"2d\");\n        }\n```\n\n4.  在新的 JavaScript 函数之后，我们有了`Module`对象的新定义:\n\n```cpp\n        var Module = {\n            preRun: [],\n            postRun: [ModuleLoaded],\n            print: (function() {\n                var element = document.getElementById('output');\n                if (element) element.value = ''; // clear browser cache\n                return function(text) {\n                    if (arguments.length > 1) text = \n                    Array.prototype.slice.call(arguments).join(' ');\n                        // uncomment block below if you want to write \n                           to an html element\n                        /*\n                        text = text.replace(/&/g, \"&amp;\");\n                        text = text.replace(/</g, \"&lt;\");\n                        text = text.replace(/>/g, \"&gt;\");\n                        text = text.replace('\\n', '<br>', 'g');\n                        */\n                        console.log(text);\n                        if (element) {\n                            element.value += text + \"\\n\";\n                            element.scrollTop = element.scrollHeight; \n      // focus on bottom\n                        }\n                    };\n                })(),\n                printErr: function(text) {\n                    if (arguments.length > 1) text = \n                       Array.prototype.slice.call(arguments).join(' ');\n                    console.error(text);\n                },\n                canvas: (function() {\n                    var canvas = document.getElementById('canvas');\n                    canvas.addEventListener(\"webglcontextlost\", \n                    function(e) { \n                        alert('WebGL context lost. You will need to \n                                reload the page.');\n                        e.preventDefault(); }, \n                        false);\n                    return canvas;\n                })(),\n                setStatus: function(text) {\n                    if (!Module.setStatus.last) Module.setStatus.last = \n                    { time: Date.now(), text: '' };\n                    if (text === Module.setStatus.last.text) return;\n                    var m = text.match(/([^(]+)\\((\\d+\n                    (\\.\\d+)?)\\/(\\d+)\\)/);\n                    var now = Date.now();\n\n                    // if this is a progress update, skip it if too        \n                       soon\n                    if (m && now - Module.setStatus.last.time < 30) \n            return; \n                    Module.setStatus.last.time = now;\n                    Module.setStatus.last.text = text;\n                    if (m) {\n                        text = m[1];\n                    }\n                    console.log(\"status: \" + text);\n                },\n                totalDependencies: 0,\n                monitorRunDependencies: function(left) {\n                    this.totalDependencies = \n                    Math.max(this.totalDependencies, left);\n                    Module.setStatus(left ? 'Preparing... (' + \n                    (this.totalDependencies-left) + \n                        '/' + this.totalDependencies + ')' : 'All \n                        downloads complete.');\n                }\n            };\n            Module.setStatus('Downloading...');\n            window.onerror = function() {\n                Module.setStatus('Exception thrown, see JavaScript \n                                    console');\n                Module.setStatus = function(text) {\n                    if (text) Module.printErr('[post-exception status] \n                    ' + text);\n                };\n            };\n```\n\n最后几行结束我们的标签，包括`{{{ SCRIPT }}}` Emscripten 标签:\n\n```cpp\n    </script>\n{{{ SCRIPT }}}\n</body>\n</html>\n```\n\n前面的四个代码块定义了我们新的`canvas_shell.html`文件。如果你想下载这个文件，你可以在 GitHub 上找到它，地址如下:[https://GitHub . com/packt publishing/hand-On-Game-Development-with-WebAssembly/blob/master/chapter 02/canvas . html](https://github.com/PacktPublishing/Hands-On-Game-Development-with-WebAssembly/blob/master/Chapter02/canvas.html)。\n\n既然我们已经在较高的层次上查看了代码，我们可以更详细地查看源代码。在 HTML 的`head`部分，我们正在更改链接的 CSS 文件的`title`和`name`。以下是 HTML `head`中的变化:\n\n```cpp\n<title>Canvas Shell</title>\n<link href=\"canvas.css\" rel=\"stylesheet\" type=\"text/css\">\n```\n\n我们不需要之前`<body>`标签中的大部分元素。我们需要一个`canvas`，我们已经从 Emscripten 提供的`shell_minimal.html`文件中删除了它，但是现在我们需要把它重新添加进来。我们保留了最初在最小外壳中的`textarea`，并且我们添加了一个新的`img`标签，该标签具有从位于[https://www.embed.com/typescript-games/draw-image.html](https://www.embed.com/typescript-games/draw-image.html)的[embed.com](https://www.embed.com)网站上的 TypeScript 画布教程中获取的飞船图像。以下是`body`元素中新的 HTML 标签:\n\n```cpp\n<canvas id=\"canvas\" width=\"800\" height=\"600\" oncontextmenu=\"event.preventDefault()\"></canvas>\n<textarea class=\"em_textarea\" id=\"output\" rows=\"8\"></textarea>\n<img src=\"spaceship.png\" id=\"spaceship\">\n```\n\n最后，我们需要更改 JavaScript 代码。我们要做的第一件事是在开头添加三个变量来保存对`canvas`元素、画布上下文和新的宇宙飞船`img`元素的引用:\n\n```cpp\nvar img = null;\nvar canvas = null;\nvar ctx = null;\n```\n\n我们添加到 JavaScript 中的下一件事是一个函数，该函数在给定的`x`和`y`坐标下将飞船图像渲染到画布上:\n\n```cpp\nfunction ShipPosition( ship_x, ship_y ) {\n    if( img == null ) {\n        return;\n    } \n    ctx.fillStyle = \"black\";\n    ctx.fillRect(0, 0, 800, 600); \n    ctx.save();\n    ctx.translate(ship_x, ship_y);\n    ctx.drawImage(img, 0, 0, img.width, img.height);\n    ctx.restore();\n}\n```\n\n该功能首先检查`img`变量是否为`null`以外的值。这将让我们知道模块是否已经加载，因为`img`变量开始设置为空。接下来，我们要做的是使用`ctx.fillStyle = “black”`线将背景填充样式设置为颜色`black`，然后调用`ctx.fillRect`绘制一个矩形，用黑色矩形填充整个画布。接下来的四行保存掉画布上下文，将上下文位置翻译到船的`x`和`y`坐标值，然后将船图像绘制到画布上。这四行中的最后一行执行上下文恢复，将我们的翻译设置回它开始的位置(0，0)。\n\n定义了这个函数之后，WebAssembly 模块就可以调用它了。我们需要设置一些初始化代码，以便在加载模块时初始化这三个变量。这是代码:\n\n```cpp\nfunction ModuleLoaded() {\n    img = document.getElementById('spaceship');\n    canvas = document.getElementById('canvas');\n    ctx = canvas.getContext(\"2d\");\n} \nvar Module = {\n    preRun: [],\n    postRun: [ModuleLoaded],\n```\n\n`ModuleLoaded`功能使用`getElementById`分别将`img`和`canvas`设置为飞船和`canvas` HTML 元素。然后我们将调用`canvas.getContext(”2d”)`来获取 2D 画布上下文，并将`ctx`变量设置到该上下文。当`Module`对象完成加载时，所有这些都会被调用，因为我们在`postRun`数组中添加了`ModuleLoaded`函数。\n\n我们还重新添加了最小外壳文件中`Module`对象上的`canvas`函数，在之前的教程中，我们已经将该函数和画布一起移除了。该代码监视画布上下文，并在上下文丢失时提醒用户。最终，我们希望这段代码能够解决这个问题，但是，就目前而言，知道它何时发生是件好事。这是代码:\n\n```cpp\ncanvas: (function() {\n    var canvas = document.getElementById('canvas');\n    // As a default initial behavior, pop up an alert when webgl \n       context is lost. To make your\n    // application robust, you may want to override this behavior \n       before shipping!\n    // See http://www.khronos.org/registry/webgl/specs/latest/1.0/#5.15.2\n    canvas.addEventListener(\"webglcontextlost\", function(e) { \n        alert('WebGL context lost. You will need to reload the page.'); \n        e.preventDefault(); }, false);\n    return canvas;\n})(),\n```\n\n为了配合这个新的 HTML shell 文件，我们创建了一个新的`canvas.c`文件来编译成一个 WebAssembly 模块。请注意，从长远来看，我们将在 JavaScript 中做得更少，而在 WebAssembly C/C++ 代码中做得更多。以下是新的`canvas.c`文件:\n\n```cpp\n#include <emscripten.h>\n#include <stdlib.h>\n#include <stdio.h>\n\nint ship_x = 0;\nint ship_y = 0;\n\nvoid MoveShip() {\n    ship_x += 2;\n    ship_y++ ;\n\n    if( ship_x >= 800 ) {\n        ship_x = -128;\n    }\n\n    if( ship_y >= 600 ) {\n        ship_y = -128;\n    }\n    EM_ASM( ShipPosition($0, $1), ship_x, ship_y );\n}\n\nint main() {\n    printf(\"Begin main\\n\");\n    emscripten_set_main_loop(MoveShip, 0, 0);\n    return 1;\n}\n```\n\n首先，我们创建一个`ship_x`和`ship_y`变量来跟踪船的 *x* 和 *y* 坐标。之后，我们创建一个`MoveShip`函数。该功能每次调用时将船的 *x* 位置增加`2`，将船的 *y* 位置增加`1`。它还会检查船的 x 坐标是否已经离开右侧的画布，如果已经离开，它会将其移回左侧，如果船已经离开底部的画布，它也会执行类似的操作。这个函数做的最后一件事就是调用我们的 JavaScript `ShipPosition`函数，传递给它船的 *x* 和 *y* 坐标。最后一步是将我们的飞船绘制到 HTML5 画布元素上的新坐标。\n\n在我们新版本的`main`功能中，我们有以下一行:\n\n```cpp\nemscripten_set_main_loop(MoveShip, 0, 0);\n```\n\n这一行将作为第一个参数传入的函数转换成一个游戏循环。我们将在后面的章节中详细介绍`emscripten_set_main_loop`的工作原理，但是目前，要知道这将导致每次在画布上渲染新帧时都会调用`MoveShip`函数。\n\n最后，我们将创建一个新的`canvas.css`文件，保存`body`和`#output` CSS 的代码，并添加一个新的`#canvas` CSS 类。以下是`canvas.css`文件的内容:\n\n```cpp\nbody {\n    margin-top: 20px;\n}\n\n#output {\n    background-color: darkslategray;\n    color: white;\n    font-size: 16px;\n    padding: 10px;\n    margin-left: auto;\n    margin-right: auto;\n    display: block;\n    width: 60%;\n}\n\n#canvas {\n    width: 800px;\n    height: 600px;\n    margin-left: auto;\n    margin-right: auto;\n    display: block;\n}\n```\n\n一切完成后，我们将使用`emcc`编译新的`canvas.html`文件以及`canvas.wasm`和`canvas.js`粘合代码。以下是对`emcc`的称呼:\n\n```cpp\nemcc canvas.c -o canvas.html --shell-file canvas_shell.html\n```\n\n紧接在`emcc`之后，我们传入`.c`文件的名称，`canvas.c`，它将用于编译我们的 WASM 模块。`-o`标志告诉我们的编译器下一个参数将是输出。使用扩展名为`.html`的输出文件告诉`emcc`编译 WASM、JavaScript 和 HTML 文件。传入的下一个标志是`--shell-file`，它告诉`emcc`接下来的参数是 HTML shell 文件的名称，它将用于创建我们最终输出的 HTML 文件。\n\nIt is important to remember that you must run WebAssembly apps using a web server, or with `emrun`. If you would like to run your WebAssembly app using `emrun`, you must compile it with the `--emrun` flag. The web browser requires a web server to stream the WebAssembly module. If you attempt to open an HTML page that uses WebAssembly in a browser directly from your hard drive, that WebAssembly module will not load.\n\n以下为`canvas.html`截图:\n\n![](img/d8f3036f-4633-49d4-85ac-8e6324bebfa5.png)\n\nFigure 2.3: Our first WebAssembly HTML5 canvas app\n\n# 摘要\n\n在本章中，我们讨论了 Emscripten 最小外壳 HTML 文件，它的各种组件是什么，以及它们是如何工作的。我们还写了如果不使用 shell 生成 canvas 代码，我们可以不用文件的哪些部分。您了解了`Module`对象，以及它是如何使用 JavaScript 粘合代码将我们的 HTML 和网络组件中的 JavaScript 绑定在一起的接口。然后，我们创建了一个新的 WebAssembly 模块，其中包含我们导出的函数，以允许 JavaScript 使用`Module.cwrap`来创建 JavaScript 函数，然后我们可以从执行 WebAssembly 函数的 DOM 中调用这些函数。\n\n我们创建了一个全新的 HTML shell 文件，它使用了 Emscripten 最小 shell 中的一些`Module`代码，但是几乎完全重写了原始 shell 的 HTML 和 CSS。然后，我们能够将新的 C 代码和 HTML 外壳文件编译成一个工作的 WebAssembly 应用，该应用能够从 JavaScript 调用 WebAssembly 函数，也能够从 WebAssembly 调用 JavaScript 函数。\n\n我们讨论了使用 HTML5 画布元素的好处，以及即时模式和保留模式图形之间的区别。我们还解释了为什么游戏和其他图形密集型任务使用即时模式而不是保留模式是有意义的。\n\n然后我们创建了一个 shell 文件来使用 HTML5 画布元素。我们添加了 JavaScript 代码来在画布上绘制图像，并编写了 C 代码，该代码使用 WebAssembly 来修改图像在画布上的位置，每一帧都在 HTML5 画布上创建一个移动的宇宙飞船的外观。\n\n在下一章中，我们将向您介绍 WebGL，它是什么，以及它如何改进网络上的图形渲染。"
  },
  {
    "path": "docs/handson-game-dev-wasm/03.md",
    "content": "# 三、WebGL 简介\n\n在苹果创建了 Canvas 元素之后，Mozilla 基金会在 2006 年开始致力于 Canvas 3D 原型的开发，到 2007 年，这个早期版本已经有了实现，最终将成为 WebGL。2009 年，一个名为克罗诺斯集团的财团成立了一个网络地理信息工作组。到 2011 年，这个团队已经生产了基于 OpenGL ES 2.0 应用编程接口的 WebGL 1.0 版本。\n\n正如我之前所说的，WebGL 被视为一个 3D 渲染应用编程接口，将与 HTML5 画布元素一起使用。它的实现消除了传统 2D 画布应用编程接口的一些渲染瓶颈，并提供了对计算机图形处理器的近乎直接的访问。因此，使用 WebGL 将 2D 图像渲染到 HTML5 画布通常比使用原始的 2D 画布实现更快。然而，由于三维渲染的复杂性增加，WebGL 的使用要复杂得多。因此，在 WebGL 的基础上构建了几个库。这允许用户使用 WebGL，但使用简化的 2D 应用编程接口。如果我们用传统的 JavaScript 编写游戏，为了简化代码，我们可能会在 WebGL 之上使用一个库，比如 Pixi.js 或者 Cocos2d-x 来进行 2d 渲染。目前，WebAssembly 使用的是**简单 direct media Layer**(**SDL**)的实现，是大多数开发者用来编写游戏的库。SDL 的这个 WebAssembly 版本建立在 WebGL 之上，提供高端性能，但是更容易使用。\n\n使用 SDL 并不妨碍您直接从编译成网络程序集的 C++ 代码中使用网络 GL。有时我们可能会对直接与 WebGL 交互感兴趣，因为我们感兴趣的功能在 SDL 内部无法直接获得。这些用例的一个例子是创建允许特殊 2D 照明效果的自定义着色器。\n\nIn this chapter, you will need an image file from the GitHub project to run the examples. The app requires the `/Chapter03/spaceship.png` image file from the project directory. Please download the project from the following URL: [https://github.com/PacktPublishing/Hands-On-Game-Development-with-WebAssembly](https://github.com/PacktPublishing/Hands-On-Game-Development-with-WebAssembly).\n\n在本章中，我们将涵盖以下主题:\n\n*   WebGL 和画布上下文\n*   WebGL 着色器简介\n*   WebGL 和 JavaScript\n\n# WebGL 和画布上下文\n\nWebGL 是一个绘制 HTML5 元素的渲染上下文，是 2D 渲染上下文的替代。通常，当有人提到画布时，他们指的是 2D 渲染上下文，通过调用`getContext`并传入字符串`2d`来访问该上下文。这两种上下文都是呈现 HTML5 画布元素的方法。上下文是一种用于即时模式呈现的应用编程接口。可以请求两个不同的 WebGL 上下文，这两个上下文都提供了对不同版本的 WebGL API 的访问。这些上下文是 *webgl* 和 *webgl2* 。在下面的例子中，我将使用 *webgl* 上下文，并将使用 WebGL 1.0 API。还有一个很少使用的将位图渲染到画布上的上下文，我们可以通过将`bitmaprenderer`作为字符串值传入来访问它。\n\nI want to point out that the term canvas is sometimes used to refer to the 2D canvas context and sometimes used to refer to the immediate mode rendering HTML5 canvas element. When I refer to canvas in this book without mentioning the 2D context, I am referring to the HTML5 canvas element.\n\n在下一节中，我将向您介绍着色器和 GLSL 着色器语言。\n\n# WebGL 着色器简介\n\n当 OpenGL 或 WebGL 与 GPU 交互时，它们会传入数据，告诉 GPU 它需要渲染的几何图形和纹理。在这一点上，图形处理器需要知道它必须如何将这些纹理和与其相关的几何图形渲染成一个单一的 2D 图像，该图像将显示在您的计算机显示器上。 **OpenGL 着色器语言** ( **GLSL** )是一种与 OpenGL 和 WebGL 一起使用的语言，用于指导 GPU 如何渲染 2D 图像。\n\nTechnically, WebGL uses the GLSL ES shader language (sometimes referred to as ELSL), which is a subset of the GLSL language. GLSL ES is the shader language that's used with OpenGL ES, a mobile-friendly subset of OpenGL (the ES is for Embedded Systems). Because WebGL is based on OpenGL ES, it inherited the GLSL ES shader language. Note that whenever I refer to GLSL within the context of WebGL or WebAssembly, I am referring to GLSL ES.\n\nWebGL 渲染管道要求我们编写两种类型的着色器来将图像渲染到屏幕上。它们是顶点着色器和片段着色器，顶点着色器基于每个顶点渲染几何图形，片段着色器渲染候选像素，称为片段。GLSL 看起来很像 C 语言，所以如果你用 C 或 C++ 工作，代码看起来会有些熟悉。\n\n对 GLSL 着色器的介绍不会涉及太多细节。在后面的章节中，我将更广泛地讨论 WebGL 着色器。现在，我只想介绍一下这个概念，并向您展示一个非常简单的 2D WebGL 着色器。我将在关于 2D 照明的一章中详细介绍。下面是一个简单的顶点着色器示例，用于为 2D WebGL 渲染引擎渲染四边形:\n\n```cpp\nprecision mediump float;\n\nattribute vec4 a_position;\nattribute vec2 a_texcoord;\n\nuniform vec4 u_translate;\n\nvarying vec2 v_texcoord;\n\nvoid main() {\n   gl_Position = u_translate + a_position;\n    v_texcoord = a_texcoord;\n}\n```\n\n这个非常简单的着色器接受顶点的位置，并根据通过 WebGL 传递到着色器中的位置统一值来移动它。这个着色器将在我们几何图形中的每个顶点上运行。在 2D 游戏中，所有的几何图形都会被渲染成四边形(即矩形)。以这种方式使用 WebGL 可以让我们更好地利用计算机的 GPU。让我简单讨论一下这个顶点着色器的代码中发生了什么。\n\nIf you are new to game development, the concept of vertex and pixel shaders may feel a little foreign. They are not as mysterious as they may first seem. You may want to quickly read over the Wikipedia *Shader* article if you want a better understanding of what shaders are ([https://en.wikipedia.org/wiki/Shader](https://en.wikipedia.org/wiki/Shader)). If you are still feeling lost, feel free to ask me questions on Twitter (`@battagline`).\n\n该着色器的第一行设置浮点精度:\n\n```cpp\nprecision mediump float;\n```\n\n计算机上的所有浮点运算都是实分数的近似值。我们可以用 0.333 逼近 1/3 的低精度，用 0.33333333333 逼近 1/3 的高精度。代码的精度行表示 GPU 上浮点值的精度。我们可以使用三种可能的精度之一:`highp`、`mediump`或`lowp`。浮点精度越高，GPU 执行代码越慢，但所有计算值的精度越高。总的来说，我一直将这个值保持在`mediump`，这对我来说很有效。如果您有一个应用要求性能高于精度，您可以将其更改为`lowp`。如果您需要高精度，请确保您知道目标图形处理器的功能。并非所有图形处理器都支持`highp`。\n\n属性变量是通过顶点数组传递到管道中的值。在我们的代码中，这些值包括与顶点关联的纹理坐标，以及与顶点关联的 2D 变换矩阵:\n\n```cpp\nattribute vec4 a_position;\nattribute vec2 a_texcoord;\n```\n\n统一变量类型是一种在所有顶点和片段上保持不变的变量类型。在这个顶点着色器中，我们通过一个统一的矢量`u_translate`。通常情况下，除非是针对相机，否则您不会希望将所有顶点平移相同的量，但是因为我们只编写了一个 WebGL 程序来绘制单个精灵，所以使用`translate`的`uniform`变量会很好:\n\n```cpp\nuniform vec4 u_translate;\n```\n\n`varying`变量(有时称为插值器)是从顶点着色器传递到片段着色器的值，片段着色器中的每个片段都获得该值的插值版本。在这段代码中，唯一的`varying`变量是顶点的纹理坐标:\n\n```cpp\nvarying vec2 v_texcoord;\n```\n\n在数学中，插值是一个计算的中间值。例如，如果我们在 0.2 和 1.2 之间插入中间点，我们将得到 0.7 的值。即起始值 0.2，加上(1.2 - 0.2) / 2 = 0.5 的平均值。所以，0.2 + 0.5 = 0.7。使用`varying`关键字从顶点着色器传递到片段着色器的值将根据片段相对于顶点的位置进行插值。\n最后，顶点着色器中执行的代码在`main`函数内部。该代码获取顶点的位置，并将其乘以平移矩阵，以获得顶点的世界坐标，以便将其放入`gl_Position`。然后，它将传递到顶点着色器的纹理坐标直接设置到可变变量中，这样它就可以将其传递到片段着色器中:\n\n```cpp\nvoid main() {\n    gl_Position = u_translate + a_position;\n    v_texcoord = a_texcoord;\n}\n```\n\n运行顶点着色器后，顶点着色器生成的所有片段都通过片段着色器运行，片段着色器为每个片段插值所有可变变量。\n\n下面是一个片段着色器的简单示例:\n\n```cpp\nprecision mediump float;\n\nvarying vec2 v_texcoord;\n\nuniform sampler2D u_texture;\n\nvoid main() {\n    gl_FragColor = texture2D(u_texture, v_texcoord);\n}\n```\n\n就像在我们的顶点着色器中一样，我们从将浮点精度设置为`mediump`开始。这些碎片有一个`uniform sample2D`纹理，它定义了在我们的游戏中用来生成 2D 精灵的纹理贴图:\n\n```cpp\nuniform sampler2D u_texture;\n```\n\n`uniform`有点像传递到管道中的全局变量，应用于使用它的着色器中的每个顶点或每个片段。在`main`函数中执行的代码也很简单。它从`v_texcoord`变化变量中获取插值纹理坐标，并从我们采样的纹理中检索颜色值，然后使用该值设置`gl_FragColor`片段的颜色:\n\n```cpp\nvoid main() {\n    gl_FragColor = texture2D(u_texture, v_texcoord);\n}\n```\n\n直接在 JavaScript 中使用 WebGL 在屏幕上绘制一个简单的 2D 图像需要更多的代码。在下一节中，我们将写出我能想到的最简单的 2D 雪碧渲染 WebGL 应用版本，它恰好是我们在上一章中编写的 2D 画布应用的新版本。我认为有必要看看将 2D 图像渲染到 HTML 画布上的两种方法之间的区别。当我们最终在 WebAssembly 中使用 SDL API 时，了解更多关于 WebGL 的信息也将有助于我们了解幕后发生了什么。在创建 WebGL JavaScript 应用时，我将尽可能地保持演示和代码的简单性。\n\nAs I mentioned previously, the point of this chapter is for you to get some hands-on experience with WebGL. For most of this book, we will not directly deal with WebGL, but rather use the simpler SDL API. If you are not interested in writing your own shaders, you can consider this chapter optional but beneficial information.\n\n在下一节中，我们将学习如何使用 WebGL 绘制到画布上。\n\n# WebGL 和 JavaScript\n\n正如我们在上一章中了解到的，使用 2D 画布非常简单。要绘制图像，只需要将上下文翻译成想要绘制图像的像素坐标，通过传入图像、图像的宽度和高度来调用`drawImage`上下文函数。如果你愿意的话，你可以把它变得更简单，忘记把 x 和 y 坐标直接传递给`drawImage`函数的转换。使用 2D 画布，您可以处理图像，但是使用 WebGL，您总是在处理 3D 几何图形，即使您正在编写 2D 游戏。使用 WebGL，您需要将纹理渲染到几何图形上。你需要使用顶点缓冲区和纹理坐标。我们之前编写的顶点着色器获取三维坐标数据和纹理坐标，并将这些值传递给片段着色器，片段着色器将在几何图形之间进行插值，并使用纹理采样函数来检索适当的纹理数据，以将像素渲染到画布上。\n\n# WebGL 坐标系与 2D 画布\n\n对于 WebGL，画布元素的中心是原点(0，0)。**正 Y** 向上，而**正 X** 向右。对于从未接触过 2D 图形的人来说，这有点更直观，因为它类似于坐标几何中的象限，这是我们在小学时了解到的。使用 2D 画布，您将始终使用像素，并且画布上不会出现负数:\n\n![](img/4eec22e0-cd90-4ea2-9e50-e58c4ba7f9b3.png)\n\n当你调用`drawImage`时，X 和 Y 坐标是图像左上角将要绘制的位置。WebGL 有点不同。一切都在使用几何图形，顶点和像素着色器都是必需的。我们将图像转换成纹理，然后在几何图形上拉伸它，以便显示出来。以下是 WebGL 坐标系的外观:\n\n![](img/9e188d87-56e7-4899-baff-52a2cd299f30.png)\n\n如果您想将图像放置在画布上的特定像素位置，您必须知道画布的宽度和高度。你画布的**中心点**是 **(0，0)** ，**左上角**是 **(-1，1)** ，**右下角**是 **(1，-1)** 。因此，如果要将图像放置在 x=150，y=160 的位置，则需要使用以下等式来找到 WebGL x 坐标:\n\n```cpp\n webgl_x = (pixel_x - canvas_width / 2) / (canvas_width / 2)\n```\n\n所以，对于 150 的`pixel_x`位置，我们必须从 150 减去 400 才能得到-250。然后，我们必须用-250 除以 400，我们会得到-0.625。我们必须做一些类似的事情来获得 WebGL 的 y 坐标，但是轴的符号被翻转了，所以我们需要做以下事情来代替我们对`pixel_x`值所做的事情:\n\n```cpp\n((canvas_height / 2) - pixel_y) / (canvas_height / 2)\n```\n\n通过插入这些值，我们得到((600 / 2) - 160) / (600 / 2)或(300 - 160) / 300 = 0.47。\n\nI am skipping a lot of information about WebGL to simplify this explanation. WebGL is not a 2D space, even though I am treating it as a 2D space in this example. Because it is a 3D space, the size of the canvas in units is based on a view area known as clip space. Mozilla has an excellent article on clip space if you would like to learn more: [https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/WebGL_model_view_projection](https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/WebGL_model_view_projection).\n\n# 顶点和紫外线数据\n\n在我们看一大块可怕的 WebGL JavaScript 代码之前，我想简单讨论一下数据缓冲区，以及我们如何将几何和纹理坐标数据传递到着色器中。我们将在一个大缓冲区中传递 32 位浮点数据，该缓冲区将包含顶点的 X 和 Y 坐标以及同一顶点的 UV 纹理坐标的组合。紫外线贴图是图形处理器将 2D 纹理坐标映射到三维几何图形的方法:\n\n![](img/8f2d81e2-1594-408e-85bc-6a01b00b9973.png)\n\nWebGL 和 OpenGL 通过为每个顶点分配一个 U 和 V 坐标来实现这一点。指定给顶点的 UV 坐标(0，0)意味着顶点将根据左上角纹理中的颜色进行着色。紫外坐标(1，1)意味着它将根据右下角纹理的颜色进行绘制。当我们在 3D 对象的点之间插值时，我们也在纹理内部的不同紫外线坐标之间插值。这些紫外线坐标可以在我们的片段着色器中使用`texture2D`内置函数通过传递纹理和当前紫外线坐标进行采样。\n\n让我们来看看我们在这个 WebGL 应用中使用的顶点和纹理数据数组:\n\n```cpp\nvar vertex_texture_data = new Float32Array([\n //  X,     Y,     U,   V\n     0.16,  0.213, 1.0, 1.0,\n    -0.16,  0.213, 0.0, 1.0,\n     0.16, -0.213, 1.0, 0.0,\n    -0.16, -0.213, 0.0, 0.0,\n    -0.16,  0.213, 0.0, 1.0,\n     0.16, -0.213, 1.0, 0.0\n ]);\n```\n\n这些数据已按行和列键入。尽管这是一个数据的线性数组，但是格式允许您看到我们有四个浮点值，将为每个顶点传递。数据上方有一个注释，显示每列代表什么。前两个数据值是几何图形的 X 和 Y 坐标。后两个值是将纹理映射到几何图形中的 X 和 Y 坐标的 U 和 V 坐标。这里有六行，尽管我们绘制的是一个矩形。我们需要六个点而不是四个点的原因是，WebGL 使用的几何图形通常由三角形组成。因此，我们需要重复两个顶点。\n\nYou may be wondering, *why triangles?* Well, there was a time when computer graphics used geometry that was not decomposed into triangles. But a problem arises when you have a quad, and not all the points are coplanar (in the same plane). This is the same problem I have whenever I go to a bar that uses four-legged stools. I am pretty sure the existence of the four-legged stool is some sort of Illuminati plot to keep me off balance, but I digress. Because three points define a plane, a triangle is, by definition, always coplanar, just like a three-legged stool will never wobble.\n\n# 2D 画布到 WebGL\n\n让我们从将画布代码从`Chapter02`目录复制到`Chapter03`目录开始。接下来，我们将把`canvas_shell.html`文件重命名为`webgl_shell.html`。我们将`canvas.css`更名为`webgl.css`。最后，我们将重命名`canvas.c`文件`webgl.c`。我们还需要确保我们复制了`spaceship.png`文件。我们根本不会更改`webgl.css`文件。我们将对`webgl_shell.html`文件进行最重要的更改。要从 2D 画布切换到 WebGL，必须添加大量代码；几乎所有这些都是额外的 JavaScript 代码。我们将需要对`webgl.c`进行一些小的调整，以便船舶在`MoveShip`功能中的位置反映 WebGL 坐标系，其原点在画布的中心。\n\n在我们开始之前，我想提一下，这个 WebGL 代码并不意味着可以生产。我们将要创建的游戏不会像我在这里演示的那样使用 WebGL。这不是最有效或可伸缩的代码。如果没有重大的改变，我们正在写的东西将不能一次呈现一个以上的精灵。我向您介绍使用 WebGL 渲染 2D 图像的方法，是为了让您了解在使用像 SDL 这样的库时，幕后发生了什么。如果你不关心幕后的工作，没有人会因为你跳过而责备你。就我个人而言，我总是更喜欢多了解一点。\n\n# 头部标签的小改动\n\n在我们的`head`标签内部，我们想要更改`title`，并且因为我们将`canvas.css`重命名为`webgl.css`，我们需要将我们的`link`标签指向新的样式表名称。以下是 HTML 开头必须更改的两个标签:\n\n```cpp\n<title>WebGL Shell</title>\n<link href=\"webgl.css\" rel=\"stylesheet\" type=\"text/css\">\n```\n\n稍后在 HTML 中，我们将移除`src`设置为`\"spaceship.png\"`的`img`标签。严格来说没有必要这样做。在画布版本中，我们使用这个标签将图像渲染到画布上。在这个 WebGL 版本中，我们将动态加载图像，因此没有必要保留它，但是如果您忘记删除它，将它放在那里不会对应用造成任何伤害。\n\n# 主要的 JavaScript 更改\n\n`webgl_shell.html`文件的 JavaScript 部分内部的`Module`代码将保持不变，因此您不必担心修改以下行之后的任何内容:\n\n```cpp\nvar Module = {\n```\n\n然而，`script`标签中代码的上半部分将需要一些重要的修改。您可能需要重新开始并删除整个模块。\n\n# WebGL 全局变量\n\n我们要做的第一件事是创建大量的 JavaScript 全局变量。如果这段代码不仅仅是为了演示，那么使用这么多全局变量通常会被认为是不好的做法。但是对于我们现在正在做的事情，它有助于简化事情:\n\n```cpp\n<script type='text/javascript'>\n var gl = null; // WebGLRenderingContext\n var program = null; // WebGLProgram\n var texture = null; // WebGLTexture\n var img = null; // HTMLImageElement\n var canvas = null;\n var image_width = 0;\n var image_height = 0;\n var vertex_texture_buffer = null; // WebGLBuffer\n var a_texcoord_location = null; // GLint\n var a_position_location = null; // GLint\n var u_translate_location = null; // WebGLUniformLocation\n var u_texture_location = null; // WebGLUniformLocation\n```\n\n第一个变量`gl`是渲染上下文的新版本。通常，如果您使用的是 2D 渲染上下文，您将其称为`ctx`，如果您使用的是 WebGL 渲染上下文，则将其命名为`gl`。第二行定义了程序变量。当我们编译顶点和片段着色器时，我们得到一个以`WebGLProgram`对象的形式存储在这个`program`变量中的编译版本。`texture`变量将保存一个我们将从`spaceship.png`图像文件加载的`WebGLTexture`。这是我们在上一章的 2D 画布教程中使用的图像。`img`变量将用于加载`spaceship.png`图像文件，该文件将用于加载纹理。画布变量将再次引用我们的 HTML 画布元素和`image_width`，一旦加载，`image_height`将保存`spaceship.png`图像的高度和宽度。\n\n`vertex_texture_buffer`属性是一个缓冲区，将用于将顶点几何和纹理数据传输到 GPU，以便我们在上一节中编写的着色器可以使用它。`a_texcoord_location`和`a_position_location`变量将用于保存对顶点着色器中`a_texcoord`和`a_position`属性变量的引用，最后，`u_translate_location`和`u_texture_location`用于引用着色器中的`u_translate`和`u_texture`统一变量。\n\n# 顶点和纹理数据的返回\n\n如果我告诉你我们还有一些变数要讨论，你会不高兴吗？下一个是我们之前讨论过的变量，但是我会再次提到它，因为它很重要。`vertex_texture_data`数组是存储用于渲染的所有顶点几何图形和紫外线纹理坐标数据的数组:\n\n```cpp\nvar vertex_texture_data = new Float32Array([\n     // x,  y,     u,   v\n     0.16,  0.213, 1.0, 1.0,\n    -0.16,  0.213, 0.0, 1.0,\n     0.16, -0.213, 1.0, 0.0,\n    -0.16, -0.213, 0.0, 0.0,\n    -0.16,  0.213, 0.0, 1.0,\n     0.16, -0.213, 1.0, 0.0\n ]);\n```\n\n有一点我之前没有提到，就是为什么 x 轴上的`x`和`y`数值范围是从`-0.16`到`0.16`，y 轴上的`-0.213`到`0.213`。因为我们渲染的是单个图像，所以不需要缩放几何图形来动态适应图像。我们使用的宇宙飞船图像是 128 x 128 像素。我们使用的画布大小是 800 x 600 像素。正如我们之前所讨论的，无论我们使用什么尺寸的画布，WebGL 都将两个轴放在-1 到+1 的范围内。这使得坐标(0，0)成为画布元素的中心。这也意味着无论画布元素有多宽或多高，画布宽度始终为 2，画布高度始终为 2。所以，如果我们想知道我们的几何图形有多宽，以使它与图像的宽度相匹配，我们必须做一些计算。首先，我们需要算出一个像素对应多少个单位的 WebGL 剪辑空间宽度。WebGL 剪辑空间的宽度为 2.0，实际画布的宽度为 800 像素，因此 WebGL 空间中单个像素的宽度为 2.0 / 800 = 0.0025。我们需要知道我们的图像在 WebGL 剪辑空间中有多宽，所以我们将 128 像素乘以 0.0025，得到 WebGL 剪辑空间宽度为 0.32。因为我们希望几何中心的 x 值为 0，所以我们的 x 几何范围为-0.16 到+0.16。\n\n既然我们已经做了宽度，让我们解决高度。画布的高度为 600 像素，但在 WebGL 剪辑空间中，画布的高度始终为 2.0 (-1.0 Y 到+1.0 Y)。那么，一个像素中有多少个 WebGL 单位呢？2.0/600 = 0.00333333…重复。显然，这是一个浮点精度无法匹配真实值的实例。我们将砍掉一些拖尾的 3，希望精度足够。回到计算图像在 WebGL 剪辑空间中的高度，它有 128 像素高，所以我们需要将 128 乘以 0.0033333…重复。结果是 0.4266666…重复，我们将截断为 0.426。所以，我们的 y 几何必须从`-0.213`到`+0.213`。\n\nI am doing my best to ignore the complexity of the WebGL clip space. This is a 3D volume and not a simple 2D drawing area like the 2D canvas context. For more information on this topic, please consult the Mozilla developer docs for clip space: [https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/WebGL_model_view_projection#Clip_space](https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/WebGL_model_view_projection#Clip_space).\n\n正如我之前所说的，当我们在游戏中工作时，很多事情将由 SDL 为我们管理，但是在未来，你可能希望在 WebAssembly 中使用 OpenGL。OpenGL ES 2.0 和 OpenGL ES 3.0 库已经移植到 WebAssembly，这些库或多或少都与 WebGL 有直接的相似之处。WebGL 1.0 是 OpenGL ES 2.0 的修改版本，它是 OpenGL 的一个版本，设计用于在移动硬件上运行。WebGL 2.0 是 OpenGL ES 3.0 的修改版。通过给 SDL 打电话了解 WebGL 在做什么，可以让我们成为更好的游戏开发者，即使 SDL 为我们做了很多繁重的工作。\n\n# 缓冲常数\n\n我选择使用一个`Float32Array`来保存这个应用的所有顶点数据。这包括 X 和 Y 坐标数据，以及 U 和 V 纹理坐标数据。正因为如此，当我们将这些数据加载到 GPU 的缓冲区时，我们需要告诉 WebGL 如何将这些数据分成不同的属性。我们将使用以下常量来告诉 WebGL】中的数据是如何分解的:\n\n```cpp\nconst FLOAT32_BYTE_SIZE = 4; // size of a 32-bit float\nconst STRIDE = FLOAT32_BYTE_SIZE * 4; // there are 4 elements for every vertex. x, y, u, v\nconst XY_OFFSET = FLOAT32_BYTE_SIZE * 0;\nconst UV_OFFSET = FLOAT32_BYTE_SIZE * 2;\n```\n\n`FLOAT32_BYTE_SIZE`常量是`Float32Array`中每个变量的大小。`STRIDE`常数将用于告诉 WebGL 单个顶点的数据使用了多少字节。我们在前面的代码中定义的四列分别代表 *x* 、 *y* 、 *u* 和 *v* 。由于这些变量中的每一个都使用四个字节的数据，我们将变量的数量乘以每个变量使用的字节数，以获得*步幅*，或者单个顶点使用的字节数。`XY_OFFSET`常数是每个步幅内的起始位置，我们将在这里找到 *x* 和 *y* 坐标数据。为了一致性，我将浮点字节大小乘以位置，但是由于它是`0`，我们可以只使用`const XY_OFFSET = 0`。现在，`UV_OFFSET`是从我们将找到紫外线纹理坐标数据的每个跨步开始的字节偏移量。因为它们在位置 2 和 3，偏移量是每个变量使用的字节数乘以`2`。\n\n# 定义着色器\n\n我浏览了上一节中着色器所做的一切。你可能想再复习一遍那一节。代码的下一部分定义了多行 JavaScript 字符串中的顶点着色器代码和片段着色器代码。以下是顶点着色器代码:\n\n```cpp\nvar vertex_shader_code = `\n    precision mediump float;\n    attribute vec4 a_position;\n    attribute vec2 a_texcoord;\n    varying vec2 v_texcoord;\n    uniform vec4 u_translate;\n\n    void main() {\n        gl_Position = u_translate + a_position;\n        v_texcoord = a_texcoord;\n    }\n`;\n```\n\n片段着色器代码如下:\n\n```cpp\nvar fragment_shader_code = `\n    precision mediump float;\n    varying vec2 v_texcoord;\n    uniform sampler2D u_texture;\n\n    void main() {\n        gl_FragColor = texture2D(u_texture, v_texcoord);\n    }\n`;\n```\n\n让我们看看顶点着色器代码中的属性:\n\n```cpp\nattribute vec4 a_position;\nattribute vec2 a_texcoord;\n```\n\n这两个属性将从`Float32Array`中的数据传入。WebGL 中一个巧妙的技巧是，如果你没有使用所有四个位置变量( *x* 、 *y* 、 *z* 、 *w* )，你可以传入你正在使用的两个( *x* 、 *y* )，GPU 将知道如何在另外两个位置使用适当的值。这些着色器需要传入两个属性:\n\n```cpp\nattribute vec4 a_position;\nattribute vec2 a_texcoord;\n```\n\n我们将再次使用缓冲区和`Float32Array`来实现这一点。我们还需要传入两个`uniform`变量。顶点着色器将使用`u_translate`变量来转换子画面的位置，`u_texture`是片段着色器将使用的纹理缓冲区。这些着色器几乎和它们得到的一样简单。许多教程开始时没有纹理，只是硬编码片段着色器的颜色输出，如下所示:\n\n```cpp\ngl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\n```\n\n进行此更改会导致片段着色器始终输出红色，因此请不要进行此更改。我能想到的唯一能让本教程变得更简单的事情是不加载纹理和渲染纯色，也不允许移动几何图形。\n\n# 模块加载功能\n\n在旧的 2D 画布代码中，我们在`ModuleLoaded`函数之前定义了`ShipPosition` JavaScript 函数，但是我们已经在 WebGL 演示中交换了这两个函数。我觉得最好在代码的渲染部分之前解释一下 WebGL 初始化。以下是全新版本的`ModuleLoaded`功能:\n\n```cpp\nfunction ModuleLoaded() {\n    canvas = document.getElementById('canvas');\n    gl = canvas.getContext(\"webgl\", { alpha: false }) ||\n                            canvas.getContext(\"experimental-webgl\", { \n                            alpha: false });\n\n    if (!gl) {\n        console.log(\"No WebGL support!\");\n        return;\n    }\n\n    gl.blendFunc( gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA );\n    gl.enable( gl.BLEND );\n\n    var vertex_shader = gl.createShader(gl.VERTEX_SHADER);\n    gl.shaderSource( vertex_shader, vertex_shader_code );\n    gl.compileShader( vertex_shader );\n\n    if( !gl.getShaderParameter(vertex_shader, gl.COMPILE_STATUS) ) {\n        console.log('Failed to compile vertex shader' + \n                     gl.getShaderInfoLog(vertex_shader));\n        gl.deleteShader(vertex_shader);\n        return;\n    }\n\n    var fragment_shader = gl.createShader(gl.FRAGMENT_SHADER);\n    gl.shaderSource( fragment_shader, fragment_shader_code );\n    gl.compileShader( fragment_shader );\n\n    if( !gl.getShaderParameter(fragment_shader, gl.COMPILE_STATUS) ) {\n        console.log('Failed to compile fragment shader' + \n                     gl.getShaderInfoLog(fragment_shader));\n        gl.deleteShader(fragment_shader);\n        return;\n    }\n\n    program = gl.createProgram();\n\n    gl.attachShader(program, vertex_shader);\n    gl.attachShader(program, fragment_shader);\n    gl.linkProgram(program);\n\n    if( !gl.getProgramParameter(program, gl.LINK_STATUS) ) {\n        console.log('Failed to link program');\n        gl.deleteProgram(program);\n        return;\n    }\n\n    gl.useProgram(program);\n\n    u_texture_location = gl.getUniformLocation(program, \"u_texture\");\n    u_translate_location = gl.getUniformLocation(program, \n    \"u_translate\");\n\n    a_position_location = gl.getAttribLocation(program, \"a_position\");\n    a_texcoord_location = gl.getAttribLocation(program, \"a_texcoord\");\n\n    vertex_texture_buffer = gl.createBuffer();\n\n    gl.bindBuffer(gl.ARRAY_BUFFER, vertex_texture_buffer);\n    gl.bufferData(gl.ARRAY_BUFFER, vertex_texture_data, \n    gl.STATIC_DRAW);\n\n    gl.enableVertexAttribArray(a_position_location);\n    gl.vertexAttribPointer(a_position_location, 2, gl.FLOAT, false, \n    STRIDE, XY_OFFSET);\n\n    gl.enableVertexAttribArray(a_texcoord_location);\n    gl.vertexAttribPointer(a_texcoord_location, 2, gl.FLOAT, false, \n    STRIDE, UV_OFFSET);\n\n    texture = gl.createTexture();\n\n    gl.bindTexture(gl.TEXTURE_2D, texture);\n    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);\n    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);\n\n    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);\n    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);\n\n    img = new Image();\n    img.addEventListener('load', function() {\n        image_width = img.width;\n        image_height = img.height;\n\n        gl.bindTexture(gl.TEXTURE_2D, texture);\n        gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA,\n        gl.UNSIGNED_BYTE, img );\n    });\n    img.src = \"spaceship.png\";\n\n    gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);\n}\n```\n\n前几行获得`canvas`元素，并使用它获得一个 WebGL 上下文。如果 JavaScript 无法获取 WebGL 上下文，我们会提醒用户，让他们知道他们的浏览器不支持 WebGL:\n\n```cpp\ncanvas = document.getElementById('canvas');\n\ngl = canvas.getContext(\"webgl\", { alpha: false }) ||\n                        canvas.getContext(\"experimental-webgl\", { \n                        alpha: false });\nif (!gl) {\n    console.log(\"No WebGL support!\");\n    return;\n}\n```\n\n之后的两行打开 alpha 混合:\n\n```cpp\ngl.blendFunc( gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA );\ngl.enable( gl.BLEND );\n```\n\n编译、加载和链接顶点和片段着色器是非常具有挑战性的代码。我不知道为什么 WebGL 库中没有一个函数可以一步完成所有这些。几乎每个为 2D 写 webgl 的人都这样做，他们要么把它放到一个单独的`.js`文件中，要么把它复制粘贴到每个项目的代码中。目前，您需要了解的关于下面一批代码的所有信息是，它采用了我们之前编写的顶点和片段着色器，并将其编译到程序变量中。从那时起，我们将使用程序变量与着色器进行交互。下面是代码:\n\n```cpp\nvar vertex_shader = gl.createShader(gl.VERTEX_SHADER);\ngl.shaderSource( vertex_shader, vertex_shader_code );\ngl.compileShader( vertex_shader );\n\nif( !gl.getShaderParameter(vertex_shader, gl.COMPILE_STATUS) ) {\n    console.log('Failed to compile vertex shader' + \n    gl.getShaderInfoLog(vertex_shader));\n    gl.deleteShader(vertex_shader);\n    return;\n}\n\nvar fragment_shader = gl.createShader(gl.FRAGMENT_SHADER);\ngl.shaderSource( fragment_shader, fragment_shader_code );\ngl.compileShader( fragment_shader );\n\nif( !gl.getShaderParameter(fragment_shader, gl.COMPILE_STATUS) ) {\n    console.log('Failed to compile fragment shader' + \n    gl.getShaderInfoLog(fragment_shader));\n    gl.deleteShader(fragment_shader);\n    return;\n}\n\nprogram = gl.createProgram();\ngl.attachShader(program, vertex_shader);\ngl.attachShader(program, fragment_shader);\ngl.linkProgram(program);\n\nif( !gl.getProgramParameter(program, gl.LINK_STATUS) ) {\n    console.log('Failed to link program');\n    gl.deleteProgram(program);\n    return;\n}\ngl.useProgram(program);\n```\n\n现在我们的`program`变量中有了`WebGLProgram`对象，我们可以使用该对象与着色器进行交互。\n\n1.  我们要做的第一件事是获取对着色器程序中`uniform`变量的引用:\n\n```cpp\nu_texture_location = gl.getUniformLocation(program, \"u_texture\");\nu_translate_location = gl.getUniformLocation(program, \"u_translate\");\n```\n\n2.  之后，我们将使用`program`对象来获取我们的顶点着色器使用的属性变量的引用:\n\n```cpp\na_position_location = gl.getAttribLocation(program, \"a_position\");\na_texcoord_location = gl.getAttribLocation(program, \"a_texcoord\");\n```\n\n3.  现在，是时候开始使用缓冲区了。你还记得当我们用所有的顶点数据创建那个`Float32Array`的时候吗？是时候使用缓冲区将数据发送到 GPU 了:\n\n```cpp\nvertex_texture_buffer = gl.createBuffer();\n\ngl.bindBuffer(gl.ARRAY_BUFFER, vertex_texture_buffer);\ngl.bufferData(gl.ARRAY_BUFFER, vertex_texture_data, \n              gl.STATIC_DRAW);\n\ngl.enableVertexAttribArray(a_position_location);\ngl.vertexAttribPointer(a_position_location, 2, gl.FLOAT, false, \n                        STRIDE, XY_OFFSET);\n\ngl.enableVertexAttribArray(a_texcoord_location);\ngl.vertexAttribPointer(a_texcoord_location, 2, gl.FLOAT, false, \n                        STRIDE, UV_OFFSET);\n```\n\n第一行创建一个名为`vertex_texture_buffer`的新缓冲区。以`gl.bindBuffer`开头的行将`vertex_texture_buffer`绑定到`ARRAY_BUFFER`，然后`bufferData`将我们在`vertex_texture_data`中的数据添加到`ARRAY_BUFFER`。之后，我们需要使用我们之前在`a_position_location`和`a_texcoord_location`变量中创建的对`a_position`和`a_texcoord`的引用来告诉 WebGL 在这个数组缓冲区中的什么地方可以找到`a_position`和`a_texcoord`属性的数据。它做的第一件事是调用`enableVertexAttribArray`来使用我们创建的位置变量启用该属性。接下来，`vertexAttribPointer`使用`STRIDE`和`XY_OFFSET`或`UV_OFFSET`来告诉 WebGL 属性数据在缓冲区数据中的位置。\n\n4.  之后，我们将创建并绑定一个纹理缓冲区:\n\n```cpp\ntexture = gl.createTexture();\ngl.bindTexture(gl.TEXTURE_2D, texture);\n```\n\n5.  现在我们有了一个绑定的纹理缓冲区，我们可以在缩放时为镜像环绕和最近邻插值配置该缓冲区:\n\n```cpp\ngl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);\ngl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);\n\ngl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);\ngl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);\n```\n\n我们用`gl.NEAREST`代替`gl.LINEAR`，因为我希望游戏有一个老派的像素化外观。在你的游戏中，你可能更喜欢不同的算法。\n\n6.  配置纹理缓冲区后，我们将下载`spaceship.png`图像并将该图像数据加载到纹理缓冲区:\n\n```cpp\nimg = new Image();\n\nimg.addEventListener('load', function() {\n    image_width = img.width;\n    image_height = img.height;\n\n    gl.bindTexture(gl.TEXTURE_2D, texture);\n    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA,\n                    gl.UNSIGNED_BYTE, img );\n});\n\nimg.src = \"spaceship.png\";\n```\n\n7.  我们要做的最后一件事是将视口从(0，0)设置为画布的宽度和高度。视口告诉 WebGL 画布元素中的空间将如何与我们的 WebGL 剪辑空间相关联:\n\n```cpp\ngl.viewport(0, 0, gl.canvas.width, gl.canvas.height);\n```\n\n# 装运位置功能\n\n如果这是生产质量代码，我会在这个渲染函数的初始化例程中做很多工作。在画布上独立移动精灵需要更新我们的数组缓冲区。我可能不会用我的方式定义我的几何，也就是用手计算尺寸。我目前没有对数组缓冲区或纹理缓冲区进行任何更改；我试图将这段代码保持在使用 WebGL 在画布上渲染精灵所需的最低限度。以下是我所拥有的:\n\n```cpp\nfunction ShipPosition( ship_x, ship_y ) {\n    if( image_width == 0 ) {\n        return;\n    }\n\n    gl.uniform4fv(u_translate_location, [ship_x, ship_y, 0.0, 0.0]);\n    gl.drawArrays(gl.TRIANGLES, 0, 6);\n}\n\n```\n\n1.  前几行检查图像下载是否完成。如果没有，我们将退出该功能:\n\n```cpp\nif( image_width == 0 ) {\n    return;\n}\n```\n\n2.  接下来，我们告诉 WebGL 用我们飞船的坐标加载统一`u_translate`统一变量:\n\n```cpp\ngl.uniform4fv(u_translate_location, [ship_x, ship_y, 0.0, 0.0]);\n```\n\n3.  最后，我们指示 WebGL 用数组缓冲区中的六个顶点绘制三角形:\n\n```cpp\ngl.drawArrays(gl.TRIANGLES, 0, 6);\n```\n\n# 移动功能\n\n我们需要跳回 WebAssembly C 模块。`webgl.c`文件是`canvas.c`的复制版本，我们只需要在`MoveShip`函数中进行更改。以下是`MoveShip`的新版本:\n\n```cpp\nvoid MoveShip() {\n    ship_x += 0.002;\n    ship_y += 0.001;\n\n    if( ship_x >= 1.16 ) {\n        ship_x = -1.16;\n    }\n\n    if( ship_y >= 1.21 ) {\n        ship_y = -1.21;\n    }\n\n    EM_ASM( ShipPosition($0, $1), ship_x, ship_y );\n}\n```\n\n这些变化都是从像素空间到 WebGL 剪辑空间的转换。在 2D 画布版本中，我们在每一帧中向船的`x`坐标添加两个像素，向船的`y`坐标添加一个像素。但是在 WebGL 中，将`x`坐标移动两个将会移动整个屏幕宽度。因此，相反，我们必须将这些值修改为小单位，以便与 WebGL 坐标系配合使用:\n\n```cpp\nship_x += 0.002;\nship_y += 0.001;\n```\n\n将`0.002`添加到`x`坐标会使船每帧移动画布宽度的 1/500。通过`0.001`移动`y`坐标，使船舶在 y 轴上移动每帧屏幕高度的千分之一。你可能会注意到，在这个应用的 2D 画布版本中，船向右下方移动。这是因为增加 2D 画布坐标系中的`y`坐标会使图像在屏幕上下移。在 WebGL 坐标系中，船向上移动。我们唯一要做的另一件事是将飞船包裹其`x`和`y`坐标的坐标更改为 WebGL 剪辑空间:\n\n```cpp\nif( ship_x >= 1.16 ) {\n    ship_x = -1.16;\n}\n\nif( ship_y >= 1.21 ) {\n    ship_y = -1.21;\n}\n```\n\n现在我们已经有了所有的源代码，继续运行`emcc`来编译我们新的`webgl.html`文件:\n\n```cpp\nemcc webgl.c -o webgl.html --shell-file webgl_shell.html\n```\n\n一旦你编译好`webgl.html`，把它加载到一个网页浏览器中。应该是这样的:\n\n![](img/bb758221-c27c-49c3-9aef-70052e0c0fff.png)\n\nFigure 3.1: Screenshot of our WebGL app\n\nIt is important to remember that the app must be run from a web server, or using `emrun`. If you do not run the app from a web server, or use `emrun`, you will receive a variety of errors when the JavaScript glue code attempts to download the WASM and data files. You should also know that IIS requires additional configuration in order to set the proper MIME types for the `.wasm` and `.data` file extensions.\n\n现在我们已经在 WebGL 中完成了所有这些工作，在下一章中，我将讨论如果我们首先使用 SDL 来完成这些工作，会变得多么容易。\n\n# 摘要\n\n在这一章中，我们已经讨论了 WebGL 以及它如何提高网络游戏的性能。我已经向您介绍了 GLSL 着色器的概念，并讨论了顶点着色器和片段着色器，这两种着色器之间的区别，以及它们如何用于将几何图形和图像的组合渲染到 HTML5 画布上。\n\n我们还使用 WebGL 重新创建了我们用 2D 画布创建的移动飞船。我们已经讨论了如何使用顶点几何将 2D 图像渲染到三维画布上。我们还讨论了基于像素的 2D 画布坐标系和 3D WebGL 坐标系之间的差异。\n\nWebGL 是一个涉及面很广的话题，所以单个章节最多只能给出非常粗略的介绍。WebGL 是一个 3D 渲染空间，在这一章中，我特意忽略了这一点，把它当成了一个 2D 空间。您可以利用我们在这里所做的工作并在此基础上进行构建，但是为了提高我们应用的性能，我们将在未来使用 WebAssembly SDL 应用编程接口与网络 GL 进行所有交互。如果你想了解更多关于网络教学语言的知识，帕克特在 https://search.packtpub.com/?query=webgl 有大量关于网络教学语言的书籍。\n\n在下一章中，我将教你 SDL 的基础知识，它是什么，以及它如何与 WebAssembly 一起工作。我们还将学习如何使用 SDL 将精灵渲染到 HTML5 画布上，制作动画，并在画布上移动它。"
  },
  {
    "path": "docs/handson-game-dev-wasm/04.md",
    "content": "# 四、WebAssembly 中使用 SDL 的的精灵动画\n\n在撰写本文时， **S** **实现 direct media Layer**(**SDL**)是唯一一个集成到 Emscripten 中用于 WebAssembly 的 2D 渲染库。但是，即使有更多的渲染库可用，SDL 也是一个高度受支持的渲染库，它已经被移植到大量的平台上，并且在可预见的未来，它对于 WebAssembly 和 C++ 开发仍然是相关和有用的。使用 SDL 渲染到 WebGL 节省了我们大量的时间，因为我们不需要编写代码来连接我们的 WebAssembly C++ 代码和 WebGL。大型社区也提供支持和文档。你可以在[libsdl.org](http://libsdl.org)找到更多的 SDL 在线资源。\n\nYou will need to include several images in your build to make this project work. Make sure you include the `/Chapter04/sprites/` and `/Chapter04/font/` folders from the project's GitHub. If you haven't yet downloaded the GitHub project, you can get it online from: [https://github.com/PacktPublishing/Hands-On-Game-Development-with-WebAssembly](https://github.com/PacktPublishing/Hands-On-Game-Development-with-WebAssembly).\n\n我们将在本章中讨论以下主题:\n\n*   在 WebAssembly 中使用 SDL\n*   将精灵渲染到画布上\n*   动画精灵\n*   移动精灵\n\n# 在 WebAssembly 中使用 SDL\n\n在这一点上，我可以滚动我自己的系统，以便在 WebAssembly 模块和 JavaScript 网络 GL 库之间进行交互。这将涉及使用一个函数表从 C++ 内部调用 JavaScript WebGL 函数。幸运的是，Emscripten 团队已经完成了大部分工作。他们为我们创建了一个流行的 2D C++ 图形库的端口。SDL 是一个 2D 图形**应用编程** **接口** ( **应用编程接口**)在大多数实现中建立在 OpenGL 之上。有一个 Emscripten 端口，用来帮助我们在 WebGL 上渲染我们的 2D 图形。如果您想知道 Emscripten 中还集成了哪些库，请使用以下`emcc`命令:\n\n```cpp\nemcc --show-ports\n```\n\n如果运行此命令，您会注意到显示了几个不同的 SDL 库。其中包括 SDL2、SDL2_image、SDL2_gfx、SDL2_ttf 和 SDL2_net。SDL 的创建采用了模块化设计，允许用户只包括他们需要的 SDL 部分，从而使核心的 SDL 图书馆保持较小。如果你的目标是创建一个下载量有限的网络游戏，这是非常有用的。\n\n我们要做的第一件事是通过创建一个简单的“Hello World”应用来熟悉 SDL，该应用向 HTML5 画布元素写入一些文本。为此，我们需要包含两个在运行`emcc --show-ports`命令时列出的 Emscripten 库。我们需要将核心 SDL 库添加到用`USE_SDL=2,`标志编译的 Emscripten 中，并且我们需要通过添加`USE_SDL_TTF=2`标志来添加 SDL TrueType 字体库。\n\n将在 HTML 画布中显示诸如`\"HELLO SDL!\"`等消息的`.c`源代码相对简单:\n\n```cpp\n#include <SDL2/SDL.h>\n#include <SDL2/SDL_ttf.h>\n#include <emscripten.h>\n#include <stdio.h>\n\n#define MESSAGE \"HELLO SDL!\"\n#define FONT_SIZE 16\n#define FONT_FILE \"font/Roboto-Black.ttf\"\n\nint main() {\n    SDL_Window *window;\n    SDL_Renderer *renderer;\n\n    SDL_Rect dest = {.x = 160, .y = 100, .w = 0, .h = 0 };\n\n    TTF_Font *font;\n    SDL_Texture* texture;\n\n    SDL_Init( SDL_INIT_VIDEO );\n    TTF_Init();\n\n    SDL_CreateWindowAndRenderer( 320, 200, 0, &window, &renderer );\n\n    SDL_SetRenderDrawColor( renderer, 0, 0, 0, 255 );\n    SDL_RenderClear( renderer );\n\n    font = TTF_OpenFont( FONT_FILE, FONT_SIZE );\n\n    SDL_Color font_color = {255, 255, 255, 255 }; // WHITE COLOR\n    SDL_Surface *temp_surface = TTF_RenderText_Blended( font, \n                                                        MESSAGE, \n                                                       font_color );\n\n    texture = SDL_CreateTextureFromSurface( renderer, temp_surface );\n\n    SDL_FreeSurface( temp_surface );\n    SDL_QueryTexture( texture,\n                        NULL, NULL,\n                        &dest.w, &dest.h ); // query the width and \n                                               height\n\n    dest.x -= dest.w / 2;\n    dest.y -= dest.h / 2;\n\n    SDL_RenderCopy( renderer, texture, NULL, &dest );\n    SDL_RenderPresent( renderer );\n\n    return EXIT_SUCCESS;\n}\n```\n\n让我带你看看到底发生了什么。前四行代码是 SDL 头文件和 Emscripten 头文件:\n\n```cpp\n#include <SDL2/SDL.h>\n#include <SDL2/SDL_ttf.h>\n#include <emscripten.h>\n#include <stdio.h>\n```\n\n接下来，有三个预处理器定义。如果我们想快速更改消息或字体大小，我们可以修改前两行。第三个定义不太清楚。我们有一个叫做`FONT_FILE`的东西，它是一个字符串，看起来是一个文件系统位置。这有点奇怪，因为网络程序集不能访问本地文件系统。为了让 WebAssembly 模块访问字体目录中的 TrueType 字体文件，我们将在编译`WASM`文件时使用`--preload-file`标志。这将根据字体目录的内容生成一个`.data`文件。网络浏览器将该数据文件加载到虚拟文件系统中，该文件系统由 WebAssembly 模块访问。这意味着我们正在编写的 C 代码可以访问这个文件，就像它在本地文件系统中访问它一样:\n\n```cpp\n#define MESSAGE \"HELLO SDL!\"\n#define FONT_SIZE 16\n#define FONT_FILE \"font/Roboto-Black.ttf\"\n```\n\n# 初始化 SDL\n\n与 C/C++ 的其他目标一样，代码从`main`函数内部开始执行。我们将通过声明一些变量来启动我们的`main`函数:\n\n```cpp\nint main() {\n    SDL_Window *window;\n    SDL_Renderer *renderer;\n\n    SDL_Rect dest = {.x = 160, .y = 100, .w = 0, .h = 0 };\n    TTF_Font *font;\n\n    SDL_Texture *texture;\n```\n\n前两个变量是`SDL_Window`和`SDL_Renderer`对象。`window`对象将定义应用窗口，如果我们为 Windows、Mac 或 Linux 系统编写代码，我们将把它渲染到其中。当我们为 WebAssembly 构建时，在我们的 HTML 中有一个画布，但是 SDL 仍然需要一个`window`对象指针来初始化和清理。所有对 SDL 的调用都使用`renderer`对象将图像渲染到画布上。\n\n`SDL_Rect dest`变量是一个矩形，表示我们将在画布上渲染的目的地。我们将渲染到 320x200 画布的中心，因此我们将从`x`和`y`的值`160`和`100`开始。我们还不知道要渲染的文本的宽度和高度，所以，在这一点上，我们将设置`w`和`h`为`0`。我们稍后将重置该值，因此，理论上，我们可以将其设置为任何值。\n\n`TTF_Font *font`变量是指向`SDL_TTF`库的`font`对象的指针。稍后，我们将使用该对象从虚拟文件系统加载一种字体，并将该字体呈现给`SDL_Texture *texture`指针变量。SDL 使用`SDL_Texture`变量将精灵渲染到画布上。\n\n接下来的几行用于在 SDL 进行一些初始化工作:\n\n```cpp\nSDL_Init( SDL_INIT_VIDEO );\nTTF_Init();\n\nSDL_CreateWindowAndRenderer( 320, 200, 0, &window, &renderer );\n```\n\n使用仅初始化视频子系统的单个标志调用`SDL_Init`功能。顺便说一下，我不知道 SDL 有任何不需要初始化视频子系统的用例。许多开发人员使用 SDL 作为 OpenGL/WebGL 图形渲染系统；所以，除非你设计了一款只有音频的游戏，否则你应该始终传入`SDL_INIT_VIDEO`标志。如果您想要初始化额外的 SDL 子系统，您可以使用布尔或`|`运算符传递这些子系统的标志，如下面的代码片段所示:\n\n```cpp\n SDL_Init( SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_HAPTIC );\n```\n\n如果我们使用前一行，SDL 也将初始化音频和触觉子系统，但我们现在不需要它们，所以我们不会做出改变。\n\n`TTF_Init();`函数初始化我们的 TrueType 字体，`SDL_CreateWindowAndRenderer`返回一个`window`和`renderer`对象给我们。我们通过`320`获得画布的宽度，通过`200`获得高度。第三个变量是`window`标志。我们为该参数传递`0`以表明我们不需要任何`window`标志。因为我们使用的是 SDL 脚本端口，所以我们无法控制窗口，所以这些标志不适用。\n\n# 清除 SDL 渲染器\n\n初始化完成后，我们需要清除渲染器。我们可以用我们选择的任何颜色来清除渲染器。为此，我们将调用`SDL_RenderDrawColor`函数:\n\n```cpp\nSDL_SetRenderDrawColor( renderer, 0, 0, 0, 255 );\nSDL_RenderClear( renderer );\n```\n\n它将渲染器的绘图颜色设置为黑色，具有完全不透明度。`0, 0, 0`为 RGB 颜色值，`255`为 alpha 不透明度。这些数字的范围都是从 0 到 255，其中 255 是色谱上的全部颜色。我们这样设置是为了当我们在下一行调用`SDL_RenderClear`函数时，它将清除带有黑色的渲染器。如果我们想让颜色变成透明的红色而不是黑色，我们必须按照以下方式修改调用:\n\n```cpp\nSDL_SetRenderDrawColor( renderer, 255, 0, 0, 255 );\n```\n\n这不是我们想要的，所以我们不会做出改变。我只想指出，我们可以用任何我们喜欢的颜色来清除渲染器。\n\n# 使用网络组件虚拟文件系统\n\n接下来的几行将在虚拟文件系统中打开 TrueType 字体文件，并将其渲染到`SDL_Texture`，可用于渲染到画布:\n\n```cpp\nfont = TTF_OpenFont( FONT_FILE, FONT_SIZE );\nSDL_Color font_color = {255, 255, 255, 255 }; // WHITE COLOR\nSDL_Surface *temp_surface = TTF_RenderText_Blended( font, MESSAGE,\n                                                    font_color );\ntexture = SDL_CreateTextureFromSurface( renderer, temp_surface );\nSDL_FreeSurface( temp_surface ); \n```\n\n在前面代码的第一行中，我们通过传入在程序顶部定义的 WebAssembly 虚拟文件系统中的文件位置来打开 TrueType 字体。我们还需要指定字体的磅值，它在程序的顶部也被定义为 16。接下来我们要做的是创建一个`SDL_Color`变量，我们将把它用于字体。这是一种 RGBA 颜色，我们将所有值设置为 255，因此它是一种完全不透明的白色。完成后，我们需要使用`TTF_RenderText_Blended`功能将文本渲染到一个表面。我们传递前面几行打开的 TrueType 字体，程序顶部附近定义为`\"HELLO SDL!\"`的`MESSAGE`，以及定义为白色的字体颜色。然后，我们将从我们的表面创建一个纹理，并释放我们刚刚分配的表面内存。在使用表面指针创建纹理之后，您应该总是立即从表面指针中释放内存，因为一旦您有了纹理，就不再需要表面了。\n\n# 向 HTML5 画布渲染纹理\n\n在我们从虚拟文件系统加载一个字体，然后将该字体渲染到纹理之后，我们需要将该纹理复制到渲染器对象中的一个位置。完成之后，我们将需要使用该渲染器，并将其内容呈现给 HTML5 画布元素。\n\n以下是将纹理渲染到画布上的源代码:\n\n```cpp\nSDL_QueryTexture( texture,\n                    NULL, NULL,\n                    &dest.w, &dest.h ); // query the width and height\n\ndest.x -= dest.w / 2;\ndest.y -= dest.h / 2;\n\nSDL_RenderCopy( renderer, texture, NULL, &dest );\nSDL_RenderPresent( renderer ); \n```\n\n对`SDL_QueryTexture`函数的调用用于检索纹理的宽度和高度。我们需要在目标矩形中使用这些值，以便在不改变画布尺寸的情况下将纹理渲染到画布上。在那次调用之后，程序知道纹理的宽度和高度，因此它可以使用这些值来修改目标矩形的 *x* 和 *y* 变量，以便它可以将我们的文本放在画布的中心。因为`dest`(目的地)矩形的 *x* 和 *y* 值指定了该矩形的左上角，所以我们需要减去矩形的一半宽度和一半高度，以确保它居中。`SDL_RenderCopy`函数随后将这个纹理渲染到我们的渲染缓冲区中，`SDL_RenderPresent`将整个缓冲区移动到 HTML5 画布中。\n\n此时，代码中剩下要做的就是`return`:\n\n```cpp\nreturn EXIT_SUCCESS;\n```\n\n返回一个值`EXIT_SUCCESS`告诉我们的 JavaScript 粘合代码，运行这个模块时一切都很顺利。\n\n# 清理 SDL\n\n您可能注意到这段代码中缺少了一些东西，这些东西可能在 SDL 应用的 Windows 或 Linux 版本中，是在程序结束时进行一些 SDL 清理的代码。例如，如果我们退出了 Windows 中的一个应用，并且没有进行清理工作，我们将在没有清除 SDL 分配的一些内存的情况下退出。如果这不是一个 WebAssembly 模块，那么在函数的末尾将包含以下几行:\n\n```cpp\nSDL_Delay(5000);\nSDL_DestroyWindow(window);\nSDL_Quit();\n```\n\n因为我们没有花时间进行游戏循环，所以我们希望通过调用`SDL_Delay(5000)``5000`将程序的清理和退出延迟 5 秒，T0 是在进行清理之前等待的毫秒数。我们想重申，因为我们正在编译到网络汇编，我们不想清理我们的 SDL。这样做在不同的浏览器上有不同的效果。\n\n在 Firefox 中测试这段代码时，使用延迟是不必要的，因为即使在 WebAssembly 模块停止执行后，网页浏览器选项卡也将保持打开状态。然而，一旦 SDL 破坏了`window`对象，Chrome 浏览器标签就会显示一个错误页面。\n\n如果这是一个 Windows 环境，`SDL_DestroyWindow`函数会破坏`window`对象。`SDL_Quit`功能终止 SDL 引擎，最后，`return EXIT_SUCCESS;`成功退出`main`功能。\n\n# 编译 hello_sdl.html\n\n最后，我们将使用 Emscripten `emcc`编译器编译和测试我们的 WebAssembly 模块:\n\n```cpp\nemcc hello_sdl.c --emrun --preload-file font -s USE_SDL=2 -s USE_SDL_TTF=2 -o hello_sdl.html\n```\n\nIt is important to remember that you must run WebAssembly apps using a web server, or with `emrun`. If you would like to run your WebAssembly app using `emrun`, you must compile it with the `--emrun` flag. The web browser requires a web server to stream the WebAssembly module. If you attempt to open an HTML page that uses WebAssembly in a browser directly from your hard drive, that WebAssembly module will not load.\n\n在这次对`emcc`的调用中，我们使用了一些新的标志，我们暂时省略了用于生成模板定制版本的`--shell-file new_shell.html`标志。如果您想继续使用`emrun`测试该应用，您必须包含`--emrun`标志，才能使用`emrun`命令运行。如果你正在使用一个网络服务器，比如 Node.js，来为应用服务，你可以从现在开始省略`--emrun`标志。如果你喜欢使用`emrun`，用那个标志继续编译。\n\n我们添加了`--preload-file`字体标志，允许我们创建包含在`hello_sdl.data`文件中的虚拟文件系统。这个文件保存了我们的 TrueType 字体。该应用使用核心的 SDL 库和附加的 SDL TrueType 字体模块，因此我们包含了以下标志:`-s USE_SDL=2 -s USE_SDL_TTF=2`，以允许调用`SDL`和`SDL_ttf`。如果您的编译一切顺利，当您在浏览器中调出新的`hello_sdl.html`文件时，它将是这样的:\n\n![](img/261996ad-2fb3-49af-9505-dead70bfb861.png)\n\nFigure 4.1: Hello SDL! app screenshot\n\n在下一节中，我们将学习如何使用 SDL 渲染一个精灵到 HTML5 画布。\n\n# 将精灵渲染到画布上\n\n现在，我们已经学习了如何使用 SDL 和 Emscripten 将文本渲染到我们的 HTML 画布元素，我们可以进行下一步，学习如何渲染精灵。用于将精灵渲染到画布上的代码与我们用于渲染 TrueType 字体的代码非常相似。我们将仍然使用虚拟文件系统来生成一个包含我们正在使用的精灵的数据文件，但是我们将需要一个新的 SDL 库来做到这一点。我们不再需要`SDL2_ttf`来加载一个 TrueType 字体并将其渲染成纹理。相反，我们需要`SDL2_image`。稍后，我们将向您展示如何更改我们对`emcc`的调用，以包括这个新的库。\n\n首先，让我们看看新版本的 SDL 代码，它将图像渲染到我们的 HTML 画布元素中，而不是我们在上一节中渲染的文本:\n\n```cpp\n#include <SDL2/SDL.h>\n#include <SDL2/SDL_image.h>\n#include <emscripten.h>\n#include <stdio.h>\n#define SPRITE_FILE \"sprites/Franchise1.png\"\n\nint main() {\n    SDL_Window *window;\n    SDL_Renderer *renderer;\n    SDL_Rect dest = {.x = 160, .y = 100, .w = 0, .h = 0 };\n    SDL_Texture *texture;\n    SDL_Init( SDL_INIT_VIDEO );\n    SDL_CreateWindowAndRenderer( 320, 200, 0, &window, &renderer );\n    SDL_SetRenderDrawColor( renderer, 0, 0, 0, 255 );\n    SDL_RenderClear( renderer );\n    SDL_Surface *temp_surface = IMG_Load( SPRITE_FILE );\n\n    if( !temp_surface ) {\n        printf(\"failed to load image: %s\\n\", IMG_GetError() );\n        return 0;\n    }\n\n    texture = SDL_CreateTextureFromSurface( renderer, temp_surface );\n\n    SDL_FreeSurface( temp_surface );\n\n    SDL_QueryTexture( texture,\n                        NULL, NULL,\n                        &dest.w, &dest.h ); // query the width and \n                        height\n\n    dest.x -= dest.w / 2;\n    dest.y -= dest.h / 2;\n\n    SDL_RenderCopy( renderer, texture, NULL, &dest );\n    SDL_RenderPresent( renderer );\n\n SDL_Delay(5000);\n SDL_DestroyWindow(window);\n SDL_Quit();\n    return 1;\n}\n```\n\n这段代码类似于我们在上一节 *HTML5 和 WebAssembly* 中为 *HELLO SDL 编写的代码！*应用。我们使用的不是`SDL2_ttf`模块，而是`SDL2_image`模块。因此，我们需要包含`SDL2/SDL_image.h`头文件。我们还需要从`sprites`目录加载一个精灵文件，我们将把它添加到 WebAssembly 虚拟文件系统中:\n\n```cpp\nSDL_Surface *temp_surface = IMG_Load( SPRITE_FILE );\n\nif( !temp_surface ) {\n    printf(\"failed to load image: %s\\n\", IMG_GetError() );\n    return 0;\n}\n```\n\n在对`IMG_Load`的调用下面，我们添加了一个错误检查，如果文件加载失败，它会让我们知道哪里出错了。除此之外，代码基本相同。如果我们成功了，画布将显示我们的星际飞船系列的 16x16 像素图像:\n\n![](img/e3515281-4ae7-471c-ba8c-b98e64540058.png)\n\nFigure 4.2: Franchise1.png\n\n在下一节中，我们将学习如何使用 SDL 在画布上制作精灵动画。\n\n# 动画精灵\n\n在这一节中，我们将学习如何在我们的 SDL 应用中制作一个快速而肮脏的小动画。这不会是我们在最终游戏中制作动画的方式，但它会让你知道我们如何通过随着时间的推移交换纹理来从 SDL 内部创建动画。我将展示将精灵分成两部分制作动画的代码。第一部分包括预处理器宏、全局变量和`show_animation`函数:\n\n```cpp\n#include <SDL2/SDL.h>\n#include <SDL2/SDL_image.h>\n\n#include <emscripten.h>\n#include <stdio.h>\n\n#define SPRITE_FILE \"sprites/Franchise1.png\"\n#define EXP_FILE \"sprites/FranchiseExplosion%d.png\"\n#define FRAME_COUNT 7\n\nint current_frame = 0;\nUint32 last_time;\nUint32 current_time;\nUint32 ms_per_frame = 100; // animate at 10 fps\n\nSDL_Window *window;\nSDL_Renderer *renderer;\nSDL_Rect dest = {.x = 160, .y = 100, .w = 0, .h = 0 };\nSDL_Texture *sprite_texture;\nSDL_Texture *temp_texture;\nSDL_Texture* anim[FRAME_COUNT];\n\nvoid show_animation() {\n    current_time = SDL_GetTicks();\n    int ms = current_time - last_time;\n\n    if( ms < ms_per_frame) {\n        return;\n    }\n\n    if( current_frame >= FRAME_COUNT ) {\n        SDL_RenderClear( renderer );\n        return;\n    }\n\n    last_time = current_time;\n    SDL_RenderClear( renderer );\n\n    temp_texture = anim[current_frame++ ];\n\n    SDL_QueryTexture( temp_texture,\n                        NULL, NULL,\n                        &dest.w, &dest.h ); // query the width and       \n                                               height\n\n    dest.x = 160 - dest.w / 2;\n    dest.y = 100 - dest.h / 2;\n\n    SDL_RenderCopy( renderer, temp_texture, NULL, &dest );\n    SDL_RenderPresent( renderer );\n}\n\n```\n\n在我们定义了我们的`show_animation`函数之后，我们将需要定义我们模块的`main`函数:\n\n```cpp\nint main() {\n    char explosion_file_string[40];\n    SDL_Init( SDL_INIT_VIDEO );\n    SDL_CreateWindowAndRenderer( 320, 200, 0, &window, &renderer );\n\n    SDL_SetRenderDrawColor( renderer, 0, 0, 0, 255 );\n    SDL_RenderClear( renderer );\n\n    SDL_Surface *temp_surface = IMG_Load( SPRITE_FILE );\n\n    if( !temp_surface ) {\n        printf(\"failed to load image: %s\\n\", IMG_GetError() );\n        return 0;\n    }\n\n    sprite_texture = SDL_CreateTextureFromSurface( renderer, \n    temp_surface );\n\n    SDL_FreeSurface( temp_surface );\n\n    for( int i = 1; i <= FRAME_COUNT; i++ ) {\n        sprintf( explosion_file_string, EXP_FILE, i );\n        SDL_Surface *temp_surface = IMG_Load( explosion_file_string );\n\n        if( !temp_surface ) {\n            printf(\"failed to load image: %s\\n\", IMG_GetError() );\n            return 0;\n        }\n\n        temp_texture = SDL_CreateTextureFromSurface( renderer, \n        temp_surface );\n        anim[i-1] = temp_texture;\n        SDL_FreeSurface( temp_surface );\n    }\n\n    SDL_QueryTexture( sprite_texture,\n                        NULL, NULL,\n                        &dest.w, &dest.h ); // query the width and \n                                               height\n\n    dest.x -= dest.w / 2;\n    dest.y -= dest.h / 2;\n\n    SDL_RenderCopy( renderer, sprite_texture, NULL, &dest );\n    SDL_RenderPresent( renderer );\n\n    last_time = SDL_GetTicks();\n    emscripten_set_main_loop(show_animation, 0, 0);\n    return 1;\n}\n```\n\n这里有很多东西要打开。有很多更有效的方法来制作这个动画，但是我们在这里所做的是将我们已经做的事情加以补充。在早期版本的代码中，我们在画布上呈现了一个框架，然后退出了 WebAssembly 模块。如果你的目标是把静态的东西渲染到画布上，并且永远不改变它，这就足够好了。然而，如果你正在写一个游戏，你需要能够激活你的精灵并在画布上移动它们。在这里，我们遇到了一个问题，如果我们为 WebAssembly 之外的任何目标编译 C++ 代码，我们就不会遇到这个问题。游戏通常以循环方式运行，并直接负责向屏幕呈现。WebAssembly 在您的 web 浏览器中的 JavaScript 引擎内部运行。WebAssembly 模块本身无法更新我们的画布。Emscripten 使用 JavaScript 粘合代码从 SDL API 间接更新 HTML 画布。但是，如果 WebAssembly 在一个循环中运行，并使用该循环通过 SDL 动画化我们的精灵，则 WebAssembly 模块永远不会放开它所在的线程，并且 JavaScript 永远不会有机会更新画布。正因为如此，我们不能把游戏循环放在`main`功能里面。相反，我们必须创建一个不同的函数，并使用 Emscripten 来设置每次浏览器呈现一个框架时调用该函数的 JavaScript 粘合代码。我们将使用的函数如下:\n\n```cpp\nemscripten_set_main_loop(show_animation, 0, 0);\n```\n\n我们要传递给`emscripten_set_main_loop`的第一个参数是`show_animation`。这是我们在代码顶部定义的一个函数的名称。稍后我将讨论`show_animation`功能的细节。现在，只要知道这是每次浏览器在画布上渲染新帧时调用的函数就足够了。\n\n`emscripten_set_main_loop`的第二个参数是**每秒帧数** ( **FPS** )。如果您想将游戏的 FPS 设置为固定速率，您可以通过将目标帧速率传递到此处的函数中来实现。如果你通过了`0`，这将告诉`emscripten_set_main_loop`以最高的帧速率运行。一般来说，你希望你的游戏以尽可能高的帧率运行，所以传入`0`通常是最好的做法。如果你传入一个高于计算机渲染能力的值，它只会尽可能快地渲染，所以这个值只会限制你的 FPS。\n\n我们传入的第三个参数是`simulate_infinite_loop`。传递`0`相当于传递一个`false`值。如果该参数的值为`true`，它会强制模块通过`main`功能重新进入每一帧。我不确定这是什么用例。我建议将它保持在`0`并将你的游戏循环分成另一个功能，就像我们在这里所做的那样。\n\n在调用`emscripten_set_main_loop`之前，我们将设置一组 SDL 纹理表面指针:\n\n```cpp\nfor( int i = 1; i <= FRAME_COUNT; i++ ) {\n sprintf( explosion_file_string, EXP_FILE, i );\n    SDL_Surface *temp_surface = IMG_Load( explosion_file_string );\n\n    if( !temp_surface ) {\n        printf(\"failed to load image: %s\\n\", IMG_GetError() );\n        return 0;\n    }\n\n    temp_texture = SDL_CreateTextureFromSurface( renderer, temp_surface );\n    anim[i-1] = temp_texture;\n    SDL_FreeSurface( temp_surface );\n}\n```\n\n这个循环将`FranchiseExplosion1.png`到`FranchiseExplosion7.png`加载到一个 SDL 纹理数组中，并将它们存储到一个不同的数组中，称为`anim`。这就是我们稍后将在`show_animation`函数中循环的数组。有更有效的方法来做到这一点，使用精灵表，并通过修改目标矩形。我们将在后面的章节中讨论渲染动画精灵的技术。\n\n在代码顶部附近，我们定义了`show_animation`函数，调用每个渲染帧:\n\n```cpp\nvoid show_animation() {\n    current_time = SDL_GetTicks();\n    int ms = current_time - last_time;\n\n    if( ms < ms_per_frame) {\n        return;\n    }\n\n    if( current_frame >= FRAME_COUNT ) {\n        SDL_RenderClear( renderer );\n        return;\n    }\n\n    last_time = current_time;\n    SDL_RenderClear( renderer );\n\n    temp_texture = anim[current_frame++ ];\n\n    SDL_QueryTexture( temp_texture,\n                        NULL, NULL,\n                        &dest.w, &dest.h ); // query the width and \n                                               height\n\n    dest.x = 160 - dest.w / 2;\n    dest.y = 100 - dest.h / 2;\n\n    SDL_RenderCopy( renderer, temp_texture, NULL, &dest );\n    SDL_RenderPresent( renderer );\n}\n```\n\n这个函数被设计成等待一定的毫秒数，然后更新我们正在渲染的纹理。我创作了一个七帧动画，在一个像素化的小爆炸中炸毁了星际飞船系列。我们在这个循环中需要短暂等待的原因是，我们的刷新率可能是 60+ FPS，如果我们在每次调用`show_animation`时渲染我们动画的新帧，整个动画将在大约 1/10 秒内运行。经典街机游戏经常以比游戏帧速率慢得多的速率翻转动画序列。许多经典的**任天堂娱乐系统** ( **NES** )游戏使用两阶段动画，动画每隔几百毫秒交替播放精灵，即使 NES 以 60 FPS 的帧速率运行。\n\n这个函数的核心类似于我们之前创建的单一纹理渲染。主要区别在于，我们等待固定的毫秒数，然后通过增加`current_frame`变量来改变动画的帧。这让我们在不到一秒钟的时间内完成了动画的所有七个阶段。\n\n# 移动精灵\n\n现在，我们已经学习了如何在逐帧动画中动画化我们的精灵，我们将学习如何在画布上移动精灵。我想让我们的飞船保持活力，但我更希望它不要以`explosion`循环运行。在我们的`sprites`文件夹中，我包含了一个简单的四阶段动画，它会导致我们船的引擎闪烁。源代码相当长，所以我将分三部分介绍:预处理器和全局变量部分、`show_animation`函数和`main`函数。\n\n下面是在我们的`cpp`文件开头定义预处理器指令和全局变量的代码:\n\n```cpp\n#include <SDL2/SDL.h>\n#include <SDL2/SDL_image.h>\n\n#include <emscripten.h>\n#include <stdio.h>\n\n#define SPRITE_FILE \"sprites/Franchise1.png\"\n#define EXP_FILE \"sprites/Franchise%d.png\"\n\n#define FRAME_COUNT 4\n\nint current_frame = 0;\nUint32 last_time;\nUint32 current_time;\nUint32 ms_per_frame = 100; // animate at 10 fps\n\nSDL_Window *window;\n\nSDL_Renderer *renderer;\nSDL_Rect dest = {.x = 160, .y = 100, .w = 0, .h = 0 };\nSDL_Texture *sprite_texture;\nSDL_Texture *temp_texture;\nSDL_Texture* anim[FRAME_COUNT];\n```\n\n遵循预处理器指令和全局变量，我们的`cpp`文件包含一个`show_animation`函数，它定义了我们的游戏循环。以下是我们的`show_animation`功能代码:\n\n```cpp\nvoid show_animation() {\n    current_time = SDL_GetTicks();\n    int ms = current_time - last_time;\n\n    if( ms >= ms_per_frame) {\n        ++ current_frame;\n        last_time = current_time;\n    }\n\n    if( current_frame >= FRAME_COUNT ) {\n        current_frame = 0;\n    }\n\n    SDL_RenderClear( renderer );\n    temp_texture = anim[current_frame];\n\n    dest.y--;\n\n    if( dest.y < -16 ) {\n        dest.y = 200;\n    }\n\n    SDL_RenderCopy( renderer, temp_texture, NULL, &dest );\n    SDL_RenderPresent( renderer );\n}\n\n```\n\n我们的`cpp`文件的最后一部分定义了`main`函数。这是我们的 WebAssembly 模块中的初始化代码:\n\n```cpp\nint main() {\n    char explosion_file_string[40];\n    SDL_Init( SDL_INIT_VIDEO );\n    SDL_CreateWindowAndRenderer( 320, 200, 0, &window, &renderer );\n    SDL_SetRenderDrawColor( renderer, 0, 0, 0, 255 );\n    SDL_RenderClear( renderer );\n    SDL_Surface *temp_surface = IMG_Load( SPRITE_FILE );\n\n    if( !temp_surface ) {\n        printf(\"failed to load image: %s\\n\", IMG_GetError() );\n        return 0;\n    }\n\n    sprite_texture = SDL_CreateTextureFromSurface( renderer, \n    temp_surface );\n    SDL_FreeSurface( temp_surface );\n\n    for( int i = 1; i <= FRAME_COUNT; i++ ) {\n        sprintf( explosion_file_string, EXP_FILE, i );\n        SDL_Surface *temp_surface = IMG_Load( explosion_file_string );\n\n        if( !temp_surface ) {\n            printf(\"failed to load image: %s\\n\", IMG_GetError() );\n            return 0;\n        }\n\n        temp_texture = SDL_CreateTextureFromSurface( renderer, \n        temp_surface );\n\n        anim[i-1] = temp_texture;\n        SDL_FreeSurface( temp_surface );\n    }\n\n    SDL_QueryTexture( sprite_texture,\n                        NULL, NULL,\n                        &dest.w, &dest.h ); // query the width and \n                                               height\n\n    dest.x -= dest.w / 2;\n    dest.y -= dest.h / 2;\n\n    SDL_RenderCopy( renderer, sprite_texture, NULL, &dest );\n    SDL_RenderPresent( renderer );\n\n    last_time = SDL_GetTicks();\n    emscripten_set_main_loop(show_animation, 0, 0);\n    return 1;\n}\n```\n\n这个代码类似于我们的`sprite_animation`代码。修改的地方很少，大部分都在`show_animation`功能内:\n\n```cpp\nvoid show_animation() {\n    current_time = SDL_GetTicks();\n\n    int ms = current_time - last_time;\n\n    if( ms >= ms_per_frame) {\n        ++ current_frame;\n        last_time = current_time;\n    }\n\n    if( current_frame >= FRAME_COUNT ) {\n        current_frame = 0;\n    }\n\n    SDL_RenderClear( renderer );\n    temp_texture = anim[current_frame];\n\n    dest.y--;\n\n    if( dest.y < -16 ) {\n        dest.y = 200;\n    }\n\n    SDL_RenderCopy( renderer, temp_texture, NULL, &dest );\n    SDL_RenderPresent( renderer );\n}\n```\n\n每当`ms`中的值(跟踪自上次帧更改以来的毫秒数)超过`ms_per_frame`时，我们就提前帧，我们将该值设置为`100`。因为飞船正在移动，我们仍然需要用新的飞船位置更新我们的画布每一帧。我们通过修改`dest.y`值来做到这一点，该值告诉 SDL 在 y 轴上何处渲染我们的飞船。我们每隔一帧从`dest.y`变量中减去一帧，将飞船向上移动。我们还会检查该值是否变得小于`-16`。因为精灵是 16 像素高，这将发生时，精灵已经完全离开屏幕顶部。如果是这种情况，我们需要通过将`y`值设置回`200`来将精灵移回游戏画面底部。在实际的游戏中，像这样将我们的运动直接与帧速率联系起来是一个坏主意，但是对于这个演示来说，它将是好的。\n\n# 编译 sprite.html\n\n我们现在可以使用`emcc`命令编译我们的精灵 WebAssembly 应用。您将需要 GitHub 上的`Chapter02`文件夹中的`sprites`文件夹。下载`sprites`文件夹并将其放入项目文件夹后，可以使用以下命令编译应用:\n\n```cpp\nemcc sprite_move.c --preload-file sprites -s USE_SDL=2 -s USE_SDL_IMAGE=2 -s SDL2_IMAGE_FORMATS=[\"png\"] -o sprite_move.html\n```\n\nIt is important to remember that the app must be run from a web server, or using `emrun`. If you do not run the app from a web server, or use `emrun`, you will receive a variety of errors when the JavaScript glue code attempts to download the WASM and data files. You should also know that IIS requires additional configuration in order to set the proper MIME types for the `.wasm` and `.data` file extensions.\n\n我们仍然使用`--preload-file`标志，但是，这次我们传递的是`sprites`文件夹，而不是`fonts`文件夹。我们将继续使用`-s USE_SDL=2`标志，并将添加`-s USE_SDL_IMAGE=2`标志，这将允许我们使用与 SDL 的图像作为`.bmp`文件格式的替代。\n\n为了告诉`SDL_IMAGE`使用哪种文件格式，我们使用下面的`-s SDL2_IMAGE_FORMATS=[\"png\"]`标志传递`png`格式:\n\n![](img/3bd87aeb-0bf2-46f2-95ba-58180ce3f054.png)\n\nFigure 4.3: Screenshot of sprite_move.html\n\n# 摘要\n\n在本章中，我向您介绍了 SDL 及其模块库，这些模块可在 WebAssembly 中使用。我们已经了解了 WebAssembly 虚拟文件系统，以及 Emscripten 如何创建`.data`文件，以便在 WebAssembly 虚拟文件系统中访问。我已经教过你如何使用 SDL 渲染图像和字体到 HTML 画布。最后，我们学习了如何使用 SDL 在我们的游戏中创建一个简单的动画。\n\n在下一章中，我们将学习如何使用键盘输入在画布上移动游戏对象。"
  },
  {
    "path": "docs/handson-game-dev-wasm/05.md",
    "content": "# 五、键盘输入\n\n现在我们有了精灵和动画，可以在画布上移动这些精灵，我们需要在游戏中增加一些互动。有几种方法可以让我们的游戏获得键盘输入。一种方法是通过 JavaScript，基于该输入调用我们的 WebAssembly 模块中的不同函数。我们代码的第一部分就是这么做的。我们将在 WebAssembly 模块中添加一些函数，以便用 JavaScript 包装器包装。我们还将设置一些 JavaScript 键盘事件处理程序，每当键盘事件被触发时，我们将使用这些程序来调用我们的 WebAssembly 模块。\n\n另一种方法是让 SDL 为我们做所有繁重的工作。这包括将 C 代码添加到我们捕获`SDL_KEYDOWN`和`SDL_KEYUP`事件的 WebAssembly 模块中。然后，该模块将查看事件键码，以确定是哪个键触发了该事件。使用这两种方法编写代码都有成本和收益。一般来说，让 SDL 管理我们的键盘输入会让我们失去在 JavaScript 中编写键盘输入管理器的一些灵活性，同时，我们还会受益于更直接的代码。\n\nYou will need to include several images in your build to make this project work. Make sure you include the `/Chapter05/sprites/` folder from the project's GitHub. If you haven't yet downloaded the GitHub project, you can get it online at: [https://github.com/PacktPublishing/Hands-On-Game-Development-with-WebAssembly](https://github.com/PacktPublishing/Hands-On-Game-Development-with-WebAssembly).\n\n在本章中，我们将执行以下操作:\n\n*   了解如何使用 JavaScript 键盘事件调用我们的 WebAssembly 模块\n*   了解如何使用 SDL 事件从我们的 WebAssembly 模块内部管理键盘输入\n*   通过使用键盘输入在画布上移动飞船精灵来演示我们所学的内容\n\n# JavaScript 键盘输入\n\n我们要做的第一件事是学习如何监听 JavaScript 键盘事件，并根据这些事件调用我们的 WebAssembly 模块。我们将重用很多我们为[第 2 章](02.html)、 *HTML5 和 WebAssembly* 编写的代码，所以我们应该做的第一件事是从`Chapter02`文件夹中抓取该代码，并将其复制到我们新的`Chapter05`文件夹中。将`new_shell.html`文件从`Chapter02`目录复制到`Chapter05`目录，然后重命名该文件`jskey_shell.html`。接下来，将`shell.c`从`Chapter02`目录复制到`Chapter05`目录，并重命名该文件`jskey.c`。最后，将`shell.css`文件从`Chapter02`目录复制到`Chapter05`目录，但不要重命名。这三个文件将为我们编写 JavaScript 键盘输入代码提供一个起点。\n\n首先，我们来看看刚刚从`shell.c`创建的`jskey.c`文件。我们可以在一开始就删除这个文件中的大部分代码。删除`main`功能结束后的所有代码。这意味着您将删除以下所有代码:\n\n```cpp\nvoid test() {\n    printf(\"button test\\n\");\n}\n\nvoid int_test( int num ) {\n    printf(\"int test=%d\\n\", num);\n}\n\nvoid float_test( float num ) {\n    printf(\"float test=%f\\n\", num);\n}\n\nvoid string_test( char* str ) {\n    printf(\"string test=%s\\n\", str);\n}\n```\n\n接下来，我们将修改`main`功能。我们不再想使用我们的`main`函数中的`EM_ASM`来调用我们的 JavaScript 包装器初始化函数，所以从`main`函数中删除下面两行代码:\n\n```cpp\nEM_ASM( InitWrappers() );\nprintf(\"Initialization Complete\\n\");\n```\n\n我们的`main`函数中唯一剩下的就是一个单一的`printf`语句。我们将更改该行，让我们知道`main`功能已经运行。您可以更改该代码以说出您喜欢的任何内容，或者完全删除`printf`语句。下面的代码显示了`main`函数的内容:\n\n```cpp\nint main() {\n    printf(\"main has run\\n\");\n}\n```\n\n现在我们已经修改了`main`函数，并删除了所有不再需要的函数，让我们放入一些在 JavaScript `keyboard`事件被触发时调用的函数。当用户按下键盘上的一个箭头键时，我们将为`keypress`事件添加一个功能。这些`keypress`事件将调用以下代码:\n\n```cpp\nvoid press_up() {\n    printf(\"PRESS UP\\n\");\n}\n\nvoid press_down() {\n    printf(\"PRESS DOWN\\n\");\n}\n\nvoid press_left() {\n    printf(\"PRESS LEFT\\n\");\n}\n\nvoid press_right() {\n    printf(\"PRESS RIGHT\\n\");\n}\n```\n\n我们还想知道用户何时释放密钥。为此，我们将在 C 模块中添加四个`release`函数，如下所示:\n\n```cpp\nvoid release_up() {\n    printf(\"RELEASE UP\\n\");\n}\n\nvoid release_down() {\n    printf(\"RELEASE DOWN\\n\");\n}\n\nvoid release_left() {\n    printf(\"RELEASE LEFT\\n\");\n}\n\nvoid release_right() {\n    printf(\"RELEASE RIGHT\\n\");\n}\n```\n\n现在我们有了新的 C 文件，我们可以更改我们的 shell 文件了。打开`jskey_shell.html`。我们不需要改变`head`标签中的任何东西，但是在`body`里面，我们会想要删除很多我们将不再使用的 HTML 元素。继续删除除`textarea`元素之外的所有元素。我们希望保留我们的`textarea`元素，这样我们就可以在我们的模块中看到`printf`语句的输出。我们需要从`textarea`元素之前的`jskey_shell.html`中删除以下 HTML:\n\n```cpp\n<div class=\"input_box\">&nbsp;</div>\n<div class=\"input_box\">\n    <button id=\"click_me\" class=\"em_button\">Click Me!</button>\n</div>\n\n<div class=\"input_box\">\n    <input type=\"number\" id=\"int_num\" max=\"9999\" min=\"0\" step=\"1\" \n     value=\"1\" class=\"em_input\">\n    <button id=\"int_button\" class=\"em_button\">Int Click!</button>\n</div>\n\n<div class=\"input_box\">\n    <input type=\"number\" id=\"float_num\" max=\"99\" min=\"0\" step=\"0.01\" \n     value=\"0.0\" class=\"em_input\">\n    <button id=\"float_button\" class=\"em_button\">Float Click!</button>\n</div>\n\n<div class=\"input_box\">&nbsp;</div>\n```\n\n然后，在`textarea`元素之后，我们需要删除下面的`div`及其内容:\n\n```cpp\n<div id=\"string_box\">\n    <button id=\"string_button\" class=\"em_button\">String Click!</button>\n    <input id=\"string_input\">\n</div>\n```\n\n之后，我们有了包含所有 JavaScript 代码的`script`标签。我们需要在`script`标签中添加一些全局变量。首先，让我们添加一些布尔变量，它会告诉我们玩家是否按下了我们的任何箭头键。将所有这些值初始化为`false`，如下例所示:\n\n```cpp\nvar left_key_press = false;\nvar right_key_press = false;\nvar up_key_press = false;\nvar down_key_press = false;\n```\n\n在我们的`key_press`标志之后，我们将拥有所有的`wrapper`变量，这些变量将用于保存`wrapper`函数，这些函数调用我们的 WebAssembly 模块中的函数。我们将所有这些包装器初始化为`null`。稍后，我们将只调用这些函数，如果它们不是`null`。下面的代码显示了我们的包装器:\n\n```cpp\nvar left_press_wrapper = null;\nvar left_release_wrapper = null;\n\nvar right_press_wrapper = null;\nvar right_release_wrapper = null;\n\nvar up_press_wrapper = null;\nvar up_release_wrapper = null;\n\nvar down_press_wrapper = null;\nvar down_release_wrapper = null;\n```\n\n既然我们已经定义了所有的全局变量，我们需要添加在`key_press`和`key_release`事件上触发的函数。第一个功能是`keyPress`。这个函数的代码如下:\n\n```cpp\nfunction keyPress() {\n    event.preventDefault();\n    if( event.repeat === true ) {\n        return;\n    }\n\n    // PRESS UP ARROW\n    if (event.keyCode === 38) {\n        up_key_press = true;\n        if( up_press_wrapper != null ) up_press_wrapper();\n    }\n\n    // PRESS LEFT ARROW\n    if (event.keyCode === 37) {\n        left_key_press = true;\n        if( left_press_wrapper != null ) left_press_wrapper();\n    }\n\n    // PRESS RIGHT ARROW\n    if (event.keyCode === 39) {\n        right_key_press = true;\n        if( right_press_wrapper != null ) right_press_wrapper();\n    }\n\n    // PRESS DOWN ARROW\n    if (event.keyCode === 40) {\n        down_key_press = true;\n        if( down_press_wrapper != null ) down_press_wrapper();\n    }\n}\n```\n\n这个功能的第一行是`event.preventDefault();`。这一行阻止 web 浏览器做它通常在用户按下有问题的键时会做的事情。例如，如果你正在玩游戏，你按下向下箭头键让你的飞船向下移动，你不会希望网页也向下滚动。在`keyPress`功能开始时调用`preventDefault`将禁用所有按键的默认行为。在其他项目中，这可能不是您想要的。如果您只想在按下向下箭头键时禁用默认行为，您可以将该调用放在管理向下箭头键按下的`if`块内。下面的代码块检查该事件是否是重复事件:\n\n```cpp\nif( event.repeat === true ) {\n    return;\n}\n```\n\n如果你按住其中一把钥匙，那就是真的。例如，如果您按住向上箭头键，您最初会得到一个向上箭头键按下事件，但经过一段时间后，您会开始得到一个向上箭头键的重复事件。如果你曾经按下一个键，比如说 *F* 键，你可能已经注意到了文字处理器内部的这种行为。你会从出现在你的文字处理器中的一个 F 开始，但是，大约一秒钟后，你会开始得到 ffffffffffff，只要你按住 *F* 键，你就会继续看到 F 重复出现在你的文字处理器中。一般来说，当你使用文字处理器时，这种行为可能会有帮助，但当你玩游戏时，这种行为是有害的。前面的`if`块使我们在接收重复按键事件时退出该功能。\n\n我们函数中接下来的几个`if`块检查各种 JavaScript 键码，并基于这些键码调用我们的 WebAssembly 模块。让我们快速了解一下当玩家按下向上箭头键时会发生什么，如下所示:\n\n```cpp\n// PRESS UP ARROW\nif (event.keyCode === 38) {\n    up_key_press = true;\n    if( up_press_wrapper != null ) up_press_wrapper();\n}\n```\n\n`if`语句正在对照值`38`检查事件的键码，该值是向上箭头的键码值。你可以在以下网址找到 HTML5 键码列表:[https://www.embed.com/typescript-games/html-keycodes.html](https://www.embed.com/typescript-games/html-keycodes.html)。如果触发事件是按下向上箭头键，我们将`up_key_press`变量设置为`true`。如果我们的`up_press_wrapper`被初始化，我们就调用它，这又会调用我们的 WebAssembly 模块中的`press_up`函数。在检查向上箭头键的`if`块之后，我们将需要更多的`if`块来检查其他箭头键，如下例所示:\n\n```cpp\n    // PRESS LEFT ARROW\n    if (event.keyCode === 37) {\n        left_key_press = true;\n        if( left_press_wrapper != null ) left_press_wrapper();\n    }\n\n    // PRESS RIGHT ARROW\n    if (event.keyCode === 39) {\n        right_key_press = true;\n        if( right_press_wrapper != null ) right_press_wrapper();\n    }\n\n    // PRESS DOWN ARROW\n    if (event.keyCode === 40) {\n        down_key_press = true;\n        if( down_press_wrapper != null ) down_press_wrapper();\n    }\n}\n```\n\n在`keyUp`函数之后，我们需要创建一个非常相似的函数:`keyRelease`。该功能与`keyUp`基本相同，只是它将调用 WebAssembly 模块中的关键发布功能。以下代码显示了`keyRelease()`功能的样子:\n\n```cpp\nfunction keyRelease() {\n    event.preventDefault();\n\n    // PRESS UP ARROW\n    if (event.keyCode === 38) {\n        up_key_press = false;\n        if( up_release_wrapper != null ) up_release_wrapper();\n    }\n\n    // PRESS LEFT ARROW\n    if (event.keyCode === 37) {\n        left_key_press = false;\n        if( left_release_wrapper != null ) left_release_wrapper();\n    }\n\n    // PRESS RIGHT ARROW\n    if (event.keyCode === 39) {\n        right_key_press = false;\n        if( right_release_wrapper != null ) right_release_wrapper();\n    }\n\n    // PRESS DOWN ARROW\n    if (event.keyCode === 40) {\n        down_key_press = false;\n        if( down_release_wrapper != null ) down_release_wrapper();\n    }\n}\n```\n\n在我们定义了这些函数之后，我们需要用下面两行 JavaScript 代码使它们成为事件侦听器:\n\n```cpp\ndocument.addEventListener('keydown', keyPress);\ndocument.addEventListener('keyup', keyRelease);\n```\n\n接下来我们需要做的是修改我们的`InitWrappers`函数来包装我们之前创建的函数。我们使用`Module.cwrap`功能来实现。我们新版本的`InitWrappers`功能如下:\n\n```cpp\nfunction InitWrappers() {\n    left_press_wrapper = Module.cwrap('press_left', 'undefined');\n    right_press_wrapper = Module.cwrap('press_right', 'undefined');\n    up_press_wrapper = Module.cwrap('press_up', 'undefined');\n    down_press_wrapper = Module.cwrap('press_down', 'undefined');\n\n    left_release_wrapper = Module.cwrap('release_left', 'undefined');\n    right_release_wrapper = Module.cwrap('release_right', 'undefined');\n    up_release_wrapper = Module.cwrap('release_up', 'undefined');\n    down_release_wrapper = Module.cwrap('release_down', 'undefined');\n}\n```\n\n我们有两个不再需要的功能可以删除。这些是`runbefore`和`runafter`功能。这些功能在第 2 章 *HTML5 和*的 shell 中被用来演示`preRun`和`postRun`模块的功能。他们所做的只是在控制台上记录一行代码，所以请从`jskey_shell.html`文件中删除以下代码:\n\n```cpp\nfunction runbefore() {\n    console.log(\"before module load\");\n}\n\nfunction runafter() {\n    console.log(\"after module load\");\n}\n```\n\n现在我们已经删除了这些行，我们可以从模块的`preRun`和`postRun`数组中删除对这些函数的调用。因为我们之前已经移除了对我们的 WebAssembly 模块的`main`函数中的`EM_ASM( InitWrappers() );`的调用，我们将需要从模块的`postRun`数组中运行`InitWrappers`。以下代码显示了这些更改后`Module`对象定义的开头是什么样子的:\n\n```cpp\npreRun: [],\npostRun: [InitWrappers],\n```\n\n现在我们应该构建并测试我们新的 JavaScript 键盘处理程序。运行以下`emcc`命令:\n\n```cpp\nemcc jskey.c -o jskey.html  -s NO_EXIT_RUNTIME=1 --shell-file jskey_shell.html -s EXPORTED_FUNCTIONS=\"['_main', '_press_up', '_press_down', '_press_left', '_press_right', '_release_up', '_release_down', '_release_left', '_release_right']\" -s EXTRA_EXPORTED_RUNTIME_METHODS=\"['cwrap', 'ccall']\"\n```\n\n您会注意到我们已经使用了`-s EXPORT_FUNCTIONS`标志来导出我们所有的按键和按键释放功能。因为我们没有使用默认外壳，所以我们使用了`--shell-file jskey_shell.html`标志。如果没有脚本主循环，则`-s NO_EXIT_RUNTIME=1`标志阻止浏览器退出网络组件模块。我们还出口了`cwrap`和`ccall``-s EXTRA_EXPORTED_RUNTIME_METHODS=\"['cwrap', 'ccall']\"`。\n\n以下是该应用的截图:\n\n![](img/e34de0b2-9525-4a1b-9cad-5e851c5e1368.png)\n\nFigure 5.1: Screenshot of jskey.html\n\nIt is important to remember that the app must be run from a web server, or using `emrun`. If you do not run the app from a web server, or use `emrun`, you will receive a variety of errors when the JavaScript glue code attempts to download the WASM and data files. You should also know that IIS requires additional configuration in order to set the proper MIME types for the `.wasm` and `.data` file extensions.\n\n在下一节中，我们将使用 SDL 事件处理程序和默认的 WebAssembly shell 来捕获和处理键盘事件。\n\n# 向网络组件添加 SDL 键盘输入\n\nSDL 允许我们轮询键盘输入。每当用户按下一个键，对`SDL_PollEvent( &event )`的呼叫将返回给我们一个`SDK_KEYDOWN SDL_Event`。当一个键被释放时，它将返回一个`SDK_KEYUP`事件。在这种情况下，我们可以查看值，找出哪个键被按下或释放。我们可以利用这些信息在我们的游戏中设置旗帜，让我们知道什么时候移动我们的飞船，向什么方向移动。稍后，我们可以添加代码来检测将发射我们飞船武器的空格键按压。\n\n现在，我们将回到使用默认的 Emscripten shell。在本节的剩余部分，我们将能够从 WebAssembly C 代码中完成所有的工作。我将带您从头开始创建一个新的`keyboard.c`文件，它将处理键盘事件并打印到我们默认外壳中的`textarea`中。\n\n首先创建一个新的`keyboard.c`文件，并在文件顶部添加以下`#include`指令:\n\n```cpp\n#include <SDL2/SDL.h>\n#include <emscripten.h>\n#include <stdio.h>\n#include <stdbool.h>\n```\n\n之后，我们需要添加我们的全局`SDL`对象。前两个，`SDL_Window`和`SDL_Renderer`，现在应该很熟悉了。第三个`SDL_Event`，是新的。我们将在后面的代码中使用对`SDL_PollEvent`的调用来填充这个事件对象:\n\n```cpp\nSDL_Window *window;\nSDL_Renderer *renderer;\nSDL_Event event;\n```\n\n像这段代码的 JavaScript 版本一样，我们将使用全局变量来跟踪我们当前正在按下的箭头键。这些都是布尔变量，如下面的代码所示:\n\n```cpp\nbool left_key_press = false;\nbool right_key_press = false;\nbool up_key_press = false;\nbool down_key_press = false;\n```\n\n我们要定义的第一个函数是`input_loop`，但是在定义该函数之前，我们需要声明`input_loop`将要调用的两个函数，如下所示:\n\n```cpp\nvoid key_press();\nvoid key_release();\n```\n\n这将允许我们在实际定义`input_loop`调用这些函数时会发生什么之前定义`input_loop`函数。`input_loop`函数将调用`SDL_PollEvent`来获取事件对象。然后我们可以查看事件的类型，如果是`SDL_KEYDOWN`或`SDL_KEYUP`事件，我们可以调用适当的函数来处理这些事件，如下所示:\n\n```cpp\nvoid input_loop() {\n    if( SDL_PollEvent( &event ) ){\n        if( event.type == SDL_KEYDOWN ){\n            key_press();\n        }\n        else if( event.type == SDL_KEYUP ) {\n            key_release();\n        }\n    }\n}\n```\n\n我们将定义的第一个函数是`key_press()`函数。在这个函数中，我们将查看开关中的键盘事件，并将该值与不同的箭头键 SDLK 事件进行比较。如果该键之前已被按下，它会打印出一条消息，让我们知道用户按下的键。那我们就应该把`keypress`旗设为`true`。以下示例完整显示了`key_press()`功能:\n\n```cpp\nvoid key_press() {\n    switch( event.key.keysym.sym ){\n        case SDLK_LEFT:\n            if( !left_key_press ) {\n                printf(\"left arrow key press\\n\");\n            }\n            left_key_press = true;\n            break;\n\n        case SDLK_RIGHT:\n            if( !right_key_press ) {\n                printf(\"right arrow key press\\n\");\n            }\n            right_key_press = true;\n            break;\n\n        case SDLK_UP:\n            if( !up_key_press ) {\n                printf(\"up arrow key press\\n\");\n            }\n            up_key_press = true;\n            break;\n\n        case SDLK_DOWN:\n            if( !down_key_press ) {\n                printf(\"down arrow key press\\n\");\n            }\n            down_key_press = true;\n            break;\n\n        default:\n            printf(\"unknown key press\\n\");\n            break;\n    }\n}\n```\n\n`key_press`函数内部的第一行是 switch 语句，`switch(event.key.keysym.sym)`。这些是结构中的结构。在`input_loop`函数中，我们调用`SDL_PollEvent`，传递对`SDL_Event`结构的引用。这个结构包含可能返回给我们的任何可能事件的事件数据，以及告诉我们这是什么类型的事件的类型。如果类型为`SDL_KEYDOWN`或`SDL_KEYUP`，则表示内部`key`结构被填充，该结构是类型为`SDL_KeyboardEvent`的结构。如果你想了解`SDL_Event`结构的完整定义，你可以在 SDL 网站上找到，网址是:[https://wiki.libsdl.org/SDL_Event](https://wiki.libsdl.org/SDL_Event)。查看`SDL_Event`内部的关键变量，会发现是一个`SDL_KeyboardEvent`类型的结构。这个结构中有很多我们还不会用到的数据。它包括时间戳、该键是否是重复按下，或者该键是否正在被按下或释放等信息；但是我们在开关中看到的是它们`keysym`变量，这是一个`SDL_Keysym`类型的结构。关于`SDL_KeyboardEvent`的更多信息，可以在 SDL 网站上找到它的定义，网址为:[https://wiki.libsdl.org/SDL_KeyboardEvent](https://wiki.libsdl.org/SDL_KeyboardEvent)。`SDL_KeyboardEvent`结构中的`keysym`变量就是你会在`sym`变量中找到`SDL_Keycode`的地方。这个键码是我们必须看的，以确定玩家按了哪个键。这就是我们围绕`switch( event.key.keysym.sym )`构建开关语句的原因。SDL 键码所有可能值的链接位于:[https://wiki.libsdl.org/SDL_Keycode](https://wiki.libsdl.org/SDL_Keycode)。\n\n我们的开关中的所有 case 语句看起来都非常相似:如果按下了给定的 SDLK 键码，我们会检查该键在前一个周期中是否被按下，如果没有，我们只打印出该值。然后我们将`keypress`标志设置为`true`。以下示例显示了我们检测到按下左箭头键的代码:\n\n```cpp\ncase SDLK_LEFT:\n    if( !left_key_press ) {\n        printf(\"left arrow key press\\n\");\n    }\n    left_key_press = true;\n    break;\n```\n\n当事件类型为`SDL_KEYUP`时，我们的应用调用`key_release`函数。这与`key_down`功能非常相似。主要区别在于，它会查看用户是否按下了键，并且仅在状态变为未按下时才打印出消息。以下示例显示了该函数的全部内容:\n\n```cpp\nvoid key_release() {\n    switch( event.key.keysym.sym ){\n\n        case SDLK_LEFT:\n            if( left_key_press ) {\n                printf(\"left arrow key release\\n\");\n            }\n            left_key_press = false;\n            break;\n\n        case SDLK_RIGHT:\n            if( right_key_press ) {\n                printf(\"right arrow key release\\n\");\n            }\n            right_key_press = false;\n            break;\n\n        case SDLK_UP:\n            if( up_key_press ) {\n                printf(\"up arrow key release\\n\");\n            }\n            up_key_press = false;\n            break;\n\n        case SDLK_DOWN:\n            if( down_key_press ) {\n                printf(\"down arrow key release\\n\");\n            }\n            down_key_press = false;\n            break;\n\n        default:\n            printf(\"unknown key release\\n\");\n            break;\n    }\n}\n```\n\n我们的最后一个函数是新版本的`main`函数，在我们的`Module`加载时调用。我们仍然需要使用`emscripten_set_main_loop`来防止我们的代码捆绑 JavaScript 引擎。我们已经创建了一个`input_loop`，这是我们之前定义的。它使用 SDL 来调查键盘事件。但是，在此之前，我们仍然需要进行 SDL 初始化。我们使用的是 Emscripten 默认外壳，因此对`SDL_CreateWindowAndRenderer`的调用将设置我们的`canvas`元素的宽度和高度。我们不会渲染到我们的`input_loop`中的`canvas`元素，但是我们仍然希望在这里对它进行初始化，因为在下一节中，我们将修改这段代码，将飞船图像渲染到画布上，并通过按键来移动它。下面的代码显示了我们新版本的`main`功能是什么样子的:\n\n```cpp\nint main() {\n    SDL_Init( SDL_INIT_VIDEO );\n\n    SDL_CreateWindowAndRenderer( 320, 200, 0, &window, &renderer );\n    SDL_SetRenderDrawColor( renderer, 0, 0, 0, 255 );\n\n    SDL_RenderClear( renderer );\n    SDL_RenderPresent( renderer );\n\n    emscripten_set_main_loop(input_loop, 0, 0);\n    return 1;\n}\n```\n\n现在我们已经有了`keyboard.c`文件中的所有代码，我们可以用下面的`emcc`命令编译我们的`keyboard.c`文件:\n\n```cpp\nemcc keyboard.c -o keyboard.html -s USE_SDL=2\n```\n\n当您在浏览器中运行`keyboard.html`时，您会注意到按下箭头键会导致一条消息被打印到 Emscripten 默认 shell 的文本区域。\n\n考虑以下截图:\n\n![](img/41a1b5c7-89b9-4b88-ae20-16283cad1c8e.png)\n\nFigure 5.2: Screenshot of keyboard.html\n\n在下一节中，我们将学习如何使用这个键盘输入在画布上移动精灵。\n\n# 使用键盘输入移动精灵\n\n既然我们知道了如何获得键盘输入并在我们的 WebAssembly 模块中使用它，那么让我们弄清楚如何获得键盘输入并使用它在 HTML 画布上移动我们的宇宙飞船精灵。让我们从将`Chapter04`目录复制到`Chapter05`目录开始。这将为我们提供一个良好的起点。现在我们可以开始修改代码了。我们需要在我们的`.c`文件的开头添加一个单独的`#include`。因为需要布尔变量，所以必须加上`#include <stdbool.h>`。我们的`.c`文件的新开始将如下所示:\n\n```cpp\n#include <SDL2/SDL.h>\n#include <SDL2/SDL_image.h>\n#include <emscripten.h>\n#include <stdio.h>\n#include <stdbool.h>\n```\n\n此后，所有`#define`指令将保持不变，与它们在`sprite_move.c`文件中的状态相同，如以下代码所示:\n\n```cpp\n#define SPRITE_FILE \"sprites/Franchise1.png\"\n#define ANIM_FILE \"sprites/Franchise%d.png\"\n#define FRAME_COUNT 4\n```\n\n`sprite_move.c`文件有几个全局变量，我们将在`keyboard_move.c`中继续使用。不要删除任何这些变量；我们只会增加:\n\n```cpp\nint current_frame = 0;\n\nUint32 last_time;\nUint32 current_time;\nUint32 ms_per_frame = 100; // animate at 10 fps\n\nSDL_Window *window;\nSDL_Renderer *renderer;\nSDL_Rect dest = {.x = 160, .y = 100, .w = 0, .h = 0 };\n\nSDL_Texture *sprite_texture;\nSDL_Texture *temp_texture;\nSDL_Texture* anim[FRAME_COUNT];\n```\n\n现在我们需要从上一节使用的`keyboard.c`文件中引入一些变量。我们需要`SDL_Event`全局变量，这样我们就有东西可以传递到我们对`SDL_PollEvent`的调用中，并且我们需要我们的布尔按键标志，如下所示:\n\n```cpp\nSDL_Event event;\n\nbool left_key_press = false;\nbool right_key_press = false;\nbool up_key_press = false;\nbool down_key_press = false;\n```\n\n然后我们有函数声明，允许我们在定义了`input_loop`函数之后定义`key_press`和`key_release`函数，如下例所示:\n\n```cpp\nvoid key_press();\nvoid key_release();\n```\n\n接下来，我们将从`keyboard.c`文件中引入`input_loop`功能。这是我们用来调用`SDL_PollEvent`的函数，根据返回的事件类型，调用`key_press`或`key_release`。该功能与我们在`keyboard.c`中的版本保持不变，如下例所示:\n\n```cpp\nvoid input_loop() {\n    if( SDL_PollEvent( &event ) ){\n        if( event.type == SDL_KEYDOWN ){\n            key_press();\n        }\n        else if( event.type == SDL_KEYUP ) {\n            key_release();\n        }\n    }\n}\n```\n\n`key_press`和`key_release`功能沿用`input_loop`功能，与`keyboard.c`版本保持不变。这些功能的主要目的是设置按键标志。`printf`声明现在没有必要了，但我们将把它们留在那里。这对于性能来说并不是一件好事，因为在每次按键和释放时继续为我们的`textarea`添加线条最终会降低我们的游戏速度，但是，在这一点上，我觉得最好将这些语句留在中，以供演示:\n\n```cpp\nvoid key_press() {\n    switch( event.key.keysym.sym ){\n\n        case SDLK_LEFT:\n            if( !left_key_press ) {\n                printf(\"left arrow key press\\n\");\n            }\n            left_key_press = true;\n            break;\n\n        case SDLK_RIGHT:\n            if( !right_key_press ) {\n                printf(\"right arrow key press\\n\");\n            }\n            right_key_press = true;\n            break;\n\n        case SDLK_UP:\n            if( !up_key_press ) {\n                printf(\"up arrow key press\\n\");\n            }\n            up_key_press = true;\n            break;\n\n        case SDLK_DOWN:\n            if( !down_key_press ) {\n                printf(\"down arrow key press\\n\");\n            }\n            down_key_press = true;\n            break;\n\n        default:\n            printf(\"unknown key press\\n\");\n            break;\n    }\n}\n\nvoid key_release() {\n    switch( event.key.keysym.sym ){\n\n        case SDLK_LEFT:\n            if( left_key_press ) {\n                printf(\"left arrow key release\\n\");\n            }\n            left_key_press = false;\n            break;\n\n        case SDLK_RIGHT:\n            if( right_key_press ) {\n                printf(\"right arrow key release\\n\");\n            }\n            right_key_press = false;\n            break;\n\n        case SDLK_UP:\n            if( up_key_press ) {\n                printf(\"up arrow key release\\n\");\n            }\n            up_key_press = false;\n            break;\n\n        case SDLK_DOWN:\n            if( down_key_press ) {\n                printf(\"down arrow key release\\n\");\n            }\n            down_key_press = false;\n            break;\n\n        default:\n            printf(\"unknown key release\\n\");\n            break;\n    }\n}\n```\n\n`keyboard_move.c`文件中的下一个功能将是`show_animation`。该功能将需要从出现在`sprite_move.c`的版本中进行重大改变，以允许玩家控制飞船并在画布上移动它。下面的例子向您展示了新函数的全部内容，然后我们一次遍历一部分:\n\n```cpp\nvoid show_animation() {\n    input_loop();\n\n    current_time = SDL_GetTicks();\n    int ms = current_time - last_time;\n\n    if( ms >= ms_per_frame) {\n        ++ current_frame;\n        last_time = current_time;\n    }\n\n    if( current_frame >= FRAME_COUNT ) {\n        current_frame = 0;\n    }\n\n    SDL_RenderClear( renderer );\n    temp_texture = anim[current_frame];\n\n    if( up_key_press ) {\n        dest.y--;\n\n        if( dest.y < -16 ) {\n            dest.y = 200;\n        }\n    }\n\n    if( down_key_press ) {\n        dest.y++ ;\n\n        if( dest.y > 200 ) {\n            dest.y = -16;\n        }\n    }\n\n    if( left_key_press ) {\n        dest.x--;\n\n        if( dest.x < -16 ) {\n            dest.x = 320;\n        }\n    }\n\n    if( right_key_press ) {\n        dest.x++ ;\n\n        if( dest.x > 320 ) {\n            dest.x = -16;\n        }\n    }\n\n    SDL_RenderCopy( renderer, temp_texture, NULL, &dest );\n    SDL_RenderPresent( renderer );\n}\n```\n\n我们在这个新版本的函数中添加了第一行`show_animation`。对`input_loop`的调用用于设置每帧的按键标志。调用`input_loop`后，有一大块代码我们没有从`sprite_move.c`文件中更改，如下例所示:\n\n```cpp\ncurrent_time = SDL_GetTicks();\nint ms = current_time - last_time;\n\nif( ms >= ms_per_frame) {\n    ++ current_frame;\n    last_time = current_time;\n}\n\nif( current_frame >= FRAME_COUNT ) {\n    current_frame = 0;\n}\n\nSDL_RenderClear( renderer );\ntemp_texture = anim[current_frame];\n```\n\n这段代码调用`SDL_GetTicks()`获取当前时间，然后从当前帧最后一次改变的时间中减去当前时间，得到我们上次帧改变后的毫秒数。如果自最后一帧改变以来的毫秒数大于我们希望停留在任何给定帧上的毫秒数，我们需要提前当前帧。一旦我们弄清楚是否推进了当前帧，我们需要确保当前帧不超过我们的帧数。如果是，我们需要将其重置为`0`。之后，我们需要清除我们的渲染器，并将我们正在使用的纹理设置为与当前帧相对应的动画数组中的纹理。\n\n在`sprite_move.c`中，我们用下面几行代码将飞船的`y`坐标每帧上移一个像素:\n\n```cpp\ndest.y--;\n\nif( dest.y < -16 ) {\n    dest.y = 200;\n}\n```\n\n在新的键盘 app 中，我们只想在玩家按下向上箭头键时改变我们的`y`坐标。为此，我们必须将更改`y`坐标的代码放在检查`up_key_press`标志的`if`块中。下面是该代码的新版本:\n\n```cpp\nif( up_key_press ) {\n    dest.y--;\n\n    if( dest.y < -16 ) {\n        dest.y = 200;\n    }\n}\n```\n\n我们还需要添加当玩家按下其他箭头键时移动飞船的代码。以下代码根据玩家当前按下的键向下、向左或向右移动飞船:\n\n```cpp\nif( down_key_press ) {\n    dest.y++ ;\n\n    if( dest.y > 200 ) {\n        dest.y = -16;\n    }\n}\n\nif( left_key_press ) {\n    dest.x--;\n\n    if( dest.x < -16 ) {\n        dest.x = 320;\n    }\n}\n\nif( right_key_press ) {\n    dest.x++ ;\n\n    if( dest.x > 320 ) {\n        dest.x = -16;\n    }\n}\n```\n\n最后，我们必须渲染纹理并呈现它，如下所示:\n\n```cpp\nSDL_RenderCopy( renderer, temp_texture, NULL, &dest );\nSDL_RenderPresent( renderer );\n```\n\n`main`功能不会从`sprite_move.c`内部的版本改变，因为初始化没有改变。以下代码显示了出现在`keyboard_move.c`中的`main`功能:\n\n```cpp\nint main() {\n    char explosion_file_string[40];\n\n    SDL_Init( SDL_INIT_VIDEO );\n    SDL_CreateWindowAndRenderer( 320, 200, 0, &window, &renderer );\n    SDL_SetRenderDrawColor( renderer, 0, 0, 0, 255 );\n    SDL_RenderClear( renderer );\n\n    SDL_Surface *temp_surface = IMG_Load( SPRITE_FILE );\n\n    if( !temp_surface ) {\n        printf(\"failed to load image: %s\\n\", IMG_GetError() );\n        return 0;\n    }\n\n    sprite_texture = SDL_CreateTextureFromSurface( renderer, temp_surface );\n\n    SDL_FreeSurface( temp_surface );\n\n    for( int i = 1; i <= FRAME_COUNT; i++ ) {\n        sprintf( explosion_file_string, ANIM_FILE, i );\n        SDL_Surface *temp_surface = IMG_Load( explosion_file_string );\n\n        if( !temp_surface ) {\n            printf(\"failed to load image: %s\\n\", IMG_GetError() );\n            return 0;\n        }\n\n        temp_texture = SDL_CreateTextureFromSurface( renderer, temp_surface );\n        anim[i-1] = temp_texture;\n        SDL_FreeSurface( temp_surface );\n    }\n\n    SDL_QueryTexture( sprite_texture,\n                        NULL, NULL,\n                        &dest.w, &dest.h ); // query the width and height\n\n    dest.x -= dest.w / 2;\n    dest.y -= dest.h / 2;\n\n    SDL_RenderCopy( renderer, sprite_texture, NULL, &dest );\n    SDL_RenderPresent( renderer );\n\n    last_time = SDL_GetTicks();\n    emscripten_set_main_loop(show_animation, 0, 0);\n    return 1;\n}\n```\n\n正如我之前所说的，这段代码是我们在[第 4 章](04.html)、*中用 SDL* 在 WebAssembly 中编写的最后一个应用和我们在*部分中编写的代码的组合，在*部分中，我们将 SDL 键盘输入添加到 WebAssembly* 中，我们从键盘获取输入，并用`printf`语句记录我们的按键。我们保留了我们的`input_loop`函数，并从我们的`show_animation`函数开始添加了对它的调用。在`show_animation`中，我们不再每帧向上移动一个像素，而是只在按下向上箭头键时向上移动船只。同样，当用户按下左箭头键时，我们向左移动船只，当按下右箭头键时，我们向右移动船只，当用户按下下箭头键时，我们向下移动船只。*\n\n现在我们有了新的`keyboard_move.c`文件，让我们编译它，并尝试我们新的移动飞船。运行以下`emcc`命令编译代码:\n\n```cpp\nemcc keyboard_move.c -o keyboard_move.html --preload-file sprites -s USE_SDL=2 -s USE_SDL_IMAGE=2 -s SDL2_IMAGE_FORMATS=[\"png\"]\n```\n\n我们需要添加`--preload-file sprites`标志来表明我们想要一个包含 sprites 文件夹的虚拟文件系统。我们还需要添加`-s USE_SDL=2`和`-s USE_SDL_IMAGE=2 -s SDL2_IMAGE_FORMATS=[\"png\"]`标志，以允许我们从虚拟文件系统加载`.png`文件。一旦你编译好了`keyboard_move.html`，把它加载到浏览器中，用箭头键在画布上移动飞船。请看下面的截图:\n\n![](img/cb33d02f-3e01-4378-a984-a4a408df6e4b.png)\n\nFigure 5.3: Screenshot of keyboard_move.html\n\n# 摘要\n\n在这一章中，我们学习了如何获取键盘输入以供 WebAssembly 使用。主要有两种方法。我们可以在 JavaScript 端接受键盘输入，并通过用`Module.cwrap`制作的包装器与 WebAssembly 通信，或者直接用`Module.ccall`调用 WebAssembly 函数。在 WebAssembly 中接受键盘输入的另一种方法是使用 SDL 键盘输入事件。当我们使用这个方法时，我们可以使用默认的 Emscripten shell。第二种方法，使用 SDL 事件，将是我们在本书其余部分的首选方法。\n\n在下一章中，我们将了解更多关于游戏循环的知识，以及我们将如何在游戏中使用它，以及一般的游戏。"
  },
  {
    "path": "docs/handson-game-dev-wasm/06.md",
    "content": "# 六、游戏对象和游戏循环\n\n在这一章，我们将开始把一个游戏的框架放到适当的位置。所有游戏都有**游戏对象**和一个**游戏循环**。每个游戏都有一个游戏循环。一些工具，比如 Unity，尽最大努力抽象出游戏循环，这样开发者不一定需要知道它在那里，但是即使在这些情况下，它仍然在那里。所有游戏都必须对运行它的操作系统或硬件的渲染能力有所控制，并在游戏运行时将图像绘制到屏幕上。游戏的所有工作都在一个**大循环**内完成。游戏对象可以是类的实例，在面向对象编程语言如 C++ 的情况下，或者在过程语言如 C 的情况下，它们可以是变量或结构的松散集合。在这一章中，我们将学习如何设计一个游戏循环，以及从 C++ 内部编译成**网络组件**的一些早期版本的游戏对象。\n\nYou will need to include several images in your build to make this project work. Make sure you include the `/Chapter06-game-object/sprites/` folder from the project's GitHub repository. If you haven't yet downloaded the GitHub project, you can get it online here: [https://github.com/PacktPublishing/Hands-On-Game-Development-with-WebAssembly](https://github.com/PacktPublishing/Hands-On-Game-Development-with-WebAssembly).\n\n在本章中，我们将涵盖以下主题:\n\n*   游戏循环\n*   对象池\n*   玩家游戏对象\n*   敌人游戏对象\n*   导弹\n\n# 理解游戏循环\n\n游戏设计中的一个关键概念是游戏循环。在任何游戏中，代码都必须一遍又一遍地运行，执行一系列任务，如输入、人工智能、物理和渲染。游戏循环可能如下所示:\n\n```cpp\nwhile(loop_forever) {\n    get_user_input();\n    move_game_objects();\n    collision_detection();\n    render_game_objects();\n    play_audio();\n}\n```\n\n一个 SDL/C++ 游戏，目标几乎是除了 WebAssembly 之外的任何平台，它会有一个`while`循环，可能位于 C++ 代码的`main`函数中，只有当玩家退出游戏时才会退出。WebAssembly 与您的 web 浏览器中的 JavaScript 引擎共享其运行时。JavaScript 引擎在一个线程上运行，Emscripten 使用 JavaScript **粘合代码**在 WebAssembly 中获取您在 SDL 内部所做的事情，并将其呈现到 HTML 画布元素中。因此，我们需要为我们的游戏循环使用一段特定于 Emscripten 的代码:\n\n```cpp\nemscripten_set_main_loop(game_loop, 0, 0);\n```\n\n在接下来的几章中，我们将在游戏中添加以下一些功能:\n\n*   游戏对象管理\n*   游戏对象之间的碰撞检测\n*   粒子系统\n*   敌方飞船 AI 使用**有限状态机** ( **有限状态机**)\n*   追踪我们玩家的游戏摄像机\n*   播放音频和声音效果\n*   游戏物理\n*   用户界面\n\n这些将是从游戏循环中调用的函数。\n\n# 写一个基本的游戏循环\n\n在某种程度上，我们已经有了一个简单的游戏循环，尽管我们没有显式地创建一个名为`game_loop`的函数。我们将修改我们的代码，使其有一个更明确的游戏循环，将`input`、`move`和`render`功能分开。此时，我们的`main`函数成为一个初始化函数，通过使用 Emscripten 设置游戏循环来完成。这个新应用的代码比以前的应用大。让我们首先从较高的层次浏览代码，介绍每个部分。然后，我们将详细介绍代码的各个部分。\n\n我们以`#include`和`#define`预处理器宏开始代码:\n\n```cpp\n#include <SDL2/SDL.h>\n#include <SDL2/SDL_image.h>\n#include <emscripten.h>\n#include <stdio.h>\n#include <stdbool.h>\n#include <math.h>\n\n#define SPRITE_FILE \"sprites/Franchise.png\"\n#define PI 3.14159\n#define TWO_PI 6.28318\n#define MAX_VELOCITY 2.0\n```\n\n在预处理器宏之后，我们有一些全局时间变量:\n\n```cpp\nUint32 last_time;\nUint32 last_frame_time;\nUint32 current_time;\n```\n\n然后，我们将定义几个与 SDL 相关的全局变量:\n\n```cpp\nSDL_Window *window;\nSDL_Renderer *renderer;\nSDL_Rect dest = {.x = 160, .y = 100, .w = 16, .h = 16 };\nSDL_Texture *sprite_texture;\nSDL_Event event;\n```\n\n在我们的 SDL 全局变量之后，我们有一组键盘标志:\n\n```cpp\nbool left_key_down = false;\nbool right_key_down = false;\nbool up_key_down = false;\nbool down_key_down = false;\n```\n\n最后一个全局变量跟踪玩家数据:\n\n```cpp\nfloat player_x = 160.0;\nfloat player_y = 100.0;\nfloat player_rotation = PI;\nfloat player_dx = 0.0;\nfloat player_dy = 1.0;\nfloat player_vx = 0.0;\nfloat player_vy = 0.0;\nfloat delta_time = 0.0;\n```\n\n现在我们已经定义了所有的全局变量，我们需要两个函数来左右旋转玩家的飞船:\n\n```cpp\n\nvoid rotate_left() {\n    player_rotation -= delta_time;\n    if( player_rotation < 0.0 ) {\n        player_rotation += TWO_PI;\n    }\n    player_dx = sin(player_rotation);\n    player_dy = -cos(player_rotation);\n}\n\nvoid rotate_right() {\n    player_rotation += delta_time;\n    if( player_rotation >= TWO_PI ) {\n        player_rotation -= TWO_PI;\n    }\n    player_dx = sin(player_rotation);\n    player_dy = -cos(player_rotation);\n}\n```\n\n然后，我们为玩家的飞船提供了三个与移动相关的功能。我们用它们来加速和减速我们的宇宙飞船，并控制我们宇宙飞船的速度:\n\n```cpp\n\nvoid accelerate() {\n    player_vx += player_dx * delta_time;\n    player_vy += player_dy * delta_time;\n}\n\nvoid decelerate() {\n    player_vx -= (player_dx * delta_time) / 2.0;\n    player_vy -= (player_dy * delta_time) / 2.0;\n}\n\nvoid cap_velocity() {\n    float vel = sqrt( player_vx * player_vx + player_vy * player_vy );\n    if( vel > MAX_VELOCITY ) {\n        player_vx /= vel;\n        player_vy /= vel;\n        player_vx *= MAX_VELOCITY;\n        player_vy *= MAX_VELOCITY;\n    }\n}\n```\n\n`move`功能执行游戏对象的高级移动:\n\n```cpp\n\nvoid move() {\n    current_time = SDL_GetTicks();\n    delta_time = (float)(current_time - last_time) / 1000.0;\n    last_time = current_time;\n\n    if( left_key_down ) {\n        rotate_left();\n    }\n    if( right_key_down ) {\n        rotate_right();\n    }\n    if( up_key_down ) {\n        accelerate();\n    }\n    if( down_key_down ) {\n        decelerate();\n    }\n    cap_velocity();\n\n    player_x += player_vx;\n\n    if( player_x > 320 ) {\n        player_x = -16;\n    }\n    else if( player_x < -16 ) {\n        player_x = 320;\n    }\n\n    player_y += player_vy;\n\n    if( player_y > 200 ) {\n        player_y = -16;\n    }\n    else if( player_y < -16 ) {\n        player_y = 200;\n    }\n} \n```\n\n`input`功能确定键盘输入状态并设置我们的全局键盘标志:\n\n```cpp\n\nvoid input() {\n    if( SDL_PollEvent( &event ) ){\n        switch( event.type ){\n            case SDL_KEYDOWN:\n                switch( event.key.keysym.sym ){\n                    case SDLK_LEFT:\n                        left_key_down = true;\n                        break;\n                    case SDLK_RIGHT:\n                        right_key_down = true;\n                        break;\n                    case SDLK_UP:\n                        up_key_down = true;\n                        break;\n                    case SDLK_DOWN:\n                        down_key_down = true;\n                        break;\n                    default:\n                        break;\n                }\n                break;\n            case SDL_KEYUP:\n                switch( event.key.keysym.sym ){\n                    case SDLK_LEFT:\n                        left_key_down = false;\n                        break;\n                    case SDLK_RIGHT:\n                        right_key_down = false;\n                        break;\n                    case SDLK_UP:\n                        up_key_down = false;\n                        break;\n                    case SDLK_DOWN:\n                        down_key_down = false;\n                        break;\n                    default:\n                        break;\n                }\n                break;\n\n            default:\n                break;\n        }\n    }\n}\n```\n\n`render`功能将玩家的精灵绘制到画布上:\n\n```cpp\nvoid render() {\n    SDL_RenderClear( renderer );\n\n    dest.x = player_x;\n    dest.y = player_y;\n\n    float degrees = (player_rotation / PI) * 180.0;\n    SDL_RenderCopyEx( renderer, sprite_texture,\n                        NULL, &dest,\n    degrees, NULL, SDL_FLIP_NONE );\n\n    SDL_RenderPresent( renderer );\n }\n```\n\n`game_loop`功能在每一帧中运行我们所有的高级游戏对象:\n\n```cpp\nvoid game_loop() {\n    input();\n    move();\n    render();\n}\n```\n\n像往常一样，`main`函数完成我们所有的初始化:\n\n```cpp\nint main() {\n    char explosion_file_string[40];\n    SDL_Init( SDL_INIT_VIDEO );\n    SDL_CreateWindowAndRenderer( 320, 200, 0, &window, &renderer );\n    SDL_SetRenderDrawColor( renderer, 0, 0, 0, 255 );\n    SDL_RenderClear( renderer );\n    SDL_Surface *temp_surface = IMG_Load( SPRITE_FILE );\n\n    if( !temp_surface ) {\n        printf(\"failed to load image: %s\\n\", IMG_GetError() );\n        return 0;\n    }\n\n    sprite_texture = SDL_CreateTextureFromSurface( renderer, \n                                                  temp_surface );\n    SDL_FreeSurface( temp_surface );\n    last_frame_time = last_time = SDL_GetTicks();\n\n    emscripten_set_main_loop(game_loop, 0, 0);\n    return 1;\n}\n```\n\n您可能已经注意到，在前面的代码中，我们添加了大量全局变量来定义玩家特定的值:\n\n```cpp\nfloat player_x = 160.0;\nfloat player_y = 100.0;\nfloat player_rotation = PI;\nfloat player_dx = 0.0;\nfloat player_dy = 1.0;\nfloat player_vx = 0.0;\nfloat player_vy = 0.0;\n```\n\n在*游戏对象*部分，我们将开始创建游戏对象，并将这些值从全局定义移动到对象中，但是，就目前而言，将它们作为全局变量是可行的。我们增加了移动玩家飞船的能力，类似于经典的街机游戏*小行星*。在我们游戏的最终版本中，我们将有两艘宇宙飞船在决斗中战斗。为此，我们需要跟踪我们的船的 *x* 和 *y* 坐标以及船的旋转；`player_dx`和`player_dy`组成了我们飞船的归一化方向向量。\n\n`player_vx`和`player_vy`变量分别是玩家当前的`x`和`y`速度。\n\n在按住左右键的同时，我们将让这些键将飞船向左或向右转动，而不是让左右键将飞船向左或向右移动。为此，我们将让我们的输入函数调用`rotate_left`和`rotate_right`函数:\n\n```cpp\nvoid rotate_left() {\n    player_rotation -= delta_time;\n    if( player_rotation < 0.0 ) {\n        player_rotation += TWO_PI;\n    }\n    player_dx = sin(player_rotation);\n    player_dy = -cos(player_rotation);\n}\n\nvoid rotate_right() {\n    player_rotation += delta_time;\n    if( player_rotation >= TWO_PI ) {\n         player_rotation -= TWO_PI;\n    }\n    player_dx = sin(player_rotation);\n    player_dy = -cos(player_rotation);\n}\n```\n\n如果玩家正在左转，我们从玩家旋转中减去`delta_time`变量，这是自最后一帧渲染以来的时间(以秒为单位)。`player_rotation`变量是玩家旋转的弧度，其中 180 度= π (3.14159…)。这意味着玩家可以通过按住左箭头或右箭头大约三秒钟来旋转 180 度。如果玩家的旋转低于 0°或者玩家的旋转高于 2π (360 度)，我们也必须修正我们的旋转。如果你不熟悉弧度，它是一种替代测量角度的系统，在这个系统中，一个圆有 360 度。使用弧度，你会想到你必须绕着一个单位圆的圆周走多远才能到达那个角度。半径为 1 的圆称为**单位圆**。\n\n单位圆在左边:\n\n![](img/8a524c84-989f-42ee-a3bb-07d3d95fa6ef.png)\n\nA unit circle and a circle with a radius of 2\n\n圆直径的公式是 2πr(在我们的代码`2 * PI * radius`中)。所以，弧度为 2π等于说 360 度。大多数游戏引擎和数学库使用弧度而不是度数，但是出于某种原因，SDL 在旋转精灵时使用度数，所以我们需要在渲染游戏对象时将弧度旋转回度数(讨厌！).\n\nJust to make sure everyone is following me, in our code the `PI` macro holds an approximate value for π that is defined as the ratio of a circle's diameter to its circumference. A typical approximation for π is 3.14, although we will approximate π as 3.14159 in our code. \n\n如果玩家按键盘上的向上或向下键，我们还需要加速或减速飞船。为此，我们将创建`accelerate`和`decelerate`函数，当玩家按住向上或向下键时调用这些函数:\n\n```cpp\nvoid accelerate() {\n    player_vx += player_dx * delta_time;\n    player_vy += player_dy * delta_time;\n}\n\nvoid decelerate() {\n    player_vx -= (player_dx * delta_time) / 2.0;\n    player_vy -= (player_dy * delta_time) / 2.0;\n}\n```\n\n这两个函数都采用在我们的旋转函数中使用`sin`和`-cos`计算的`player_dx`和`player_dy`变量，并使用这些值添加到存储在`player_vx`和`player_vy`变量中的玩家的 *x* 和 *y* 速度。我们将该值乘以`delta_time`，这将把我们的加速度设置为每秒 1 像素的平方。我们的减速函数将该值除以 2，这将我们的减速率设置为每秒 0.5 像素的平方。\n\n在我们定义了`accelerate`和`decelerate`函数之后，我们需要创建一个函数，将我们飞船的`x`和`y`速度限制在每秒 2.0 像素:\n\n```cpp\nvoid cap_velocity() {\n    float vel = sqrt( player_vx * player_vx + player_vy * player_vy );\n\n    if( vel > MAX_VELOCITY ) {\n        player_vx /= vel;\n        player_vy /= vel;\n        player_vx *= MAX_VELOCITY;\n        player_vy *= MAX_VELOCITY;\n     }\n}\n```\n\n这不是定义这个函数最有效的方法，但却是最容易理解的。第一条线决定了我们速度矢量的大小。如果你不知道那是什么意思，让我稍微解释一下。我们沿着 *x* 轴有一个速度。我们还有一个沿 *y* 轴的速度。我们想限制整体速度。如果我们分别设定`x`和`y`速度的上限，我们将能够通过对角行进而走得更快。计算我们的总速度，需要用到勾股定理(你还记得高中的三角学吗？).如果你不记得了，当你有一个直角三角形时，要计算它的斜边，你要取其他两边的平方和的平方根(还记得`A<sup>2</sup> + B<sup>2</sup> = C<sup>2</sup>`吗？):\n\n![](img/58d0adaf-31a8-45fa-85aa-2ef16b155019.png)\n\n<sub>Using the Pythagorean theorem to determine the magnitude of the velocity using the x and y velocities</sub>\n\n所以，为了计算我们的速度，我们需要对`x`速度进行平方，对`y`速度进行平方，将它们相加，然后求平方根。此时，我们对照`MAX_VELOCITY`值来检查我们的速度，我们已经将其定义为`2.0`。如果当前速度大于这个最大速度，我们需要调整我们的`x`和`y`速度，使我们处于`2`的值。我们通过将`x`和`y`速度除以总速度，然后乘以`MAX_VELOCITY`来实现。\n\n我们最终需要编写一个`move`函数来移动我们所有的游戏对象，但目前我们只移动玩家的飞船:\n\n```cpp\nvoid move() {\n    current_time = SDL_GetTicks();\n    delta_time = (float)(current_time - last_time) / 1000.0;\n    last_time = current_time;\n\n    if( left_key_down ) {\n        rotate_left();\n    }\n\n    if( right_key_down ) {\n        rotate_right();\n    }\n\n    if( up_key_down ) {\n        accelerate();\n    }\n\n    if( down_key_down ) {\n        decelerate();\n    }\n\n    cap_velocity();\n    player_x += player_vx;\n\n    if( player_x > 320 ) {\n         player_x = -16;\n     }\n    else if( player_x < -16 ) {\n        player_x = 320;\n    }\n    player_y += player_vy;\n\n    if( player_y > 200 ) {\n        player_y = -16;\n    }\n    else if( player_y < -16 ) {\n        player_y = 200;\n    }\n}\n```\n\n我们需要做的第一件事是获取该帧的当前时间，然后结合我们之前的帧时间来计算`delta_time`。`delta_time`变量是自上一帧时间以来的时间量，单位为秒。我们需要将大部分动作和动画与该值联系起来，以获得一致的游戏速度，该速度与任何给定计算机上的帧速率无关。之后，我们需要根据我们在`input`功能中设置的旗帜旋转和加速或减速我们的飞船。然后我们限制我们的速度，使用`x`和`y`值来修改玩家飞船的 *x* 和 *y* 坐标。\n\n我们在`move`功能中使用了一系列标志，告诉我们当前是否按下了键盘上的特定键。要设置这些标志，我们需要一个`input`功能，该功能使用`SDL_PollEvent`来查找键盘事件并相应地设置标志:\n\n```cpp\n\nvoid input() {\n    if( SDL_PollEvent( &event ) ){\n        switch( event.type ){\n            case SDL_KEYDOWN:\n                switch( event.key.keysym.sym ){\n                    case SDLK_LEFT:\n                        left_key_down = true;\n                        break;\n                    case SDLK_RIGHT:\n                        right_key_down = true;\n                        break;\n                    case SDLK_UP:\n                        up_key_down = true;\n                        break;\n                    case SDLK_DOWN:\n                        down_key_down = true;\n                        break;\n                    default:\n                        break;\n                }\n                break;\n            case SDL_KEYUP:\n                switch( event.key.keysym.sym ){\n                    case SDLK_LEFT:\n                        left_key_down = false;\n                        break;\n                    case SDLK_RIGHT:\n                        right_key_down = false;\n                        break;\n                    case SDLK_UP:\n                        up_key_down = false;\n                        break;\n                    case SDLK_DOWN:\n                        down_key_down = false;\n                        break;\n                    default:\n                        break;\n                }\n                break;\n            default:\n                break;\n        }\n    }\n}\n```\n\n该功能包括一些寻找箭头键按压和释放的`switch`语句。如果按下其中一个箭头键，我们将相应的标志设置为`true`；如果有一个被释放，我们就把它设为`false`。\n\n接下来，我们定义`render`函数。这个函数目前渲染我们的宇宙飞船精灵，最终将我们所有的精灵渲染到 HTML 画布上:\n\n```cpp\nvoid render() {\n    SDL_RenderClear( renderer );\n    dest.x = player_x;\n    dest.y = player_y;\n    float degrees = (player_rotation / PI) * 180.0;\n    SDL_RenderCopyEx( renderer, sprite_texture,\n                        NULL, &dest,\n                        degrees, NULL, SDL_FLIP_NONE );\n    SDL_RenderPresent( renderer );\n}\n```\n\n该函数清除 HTML 画布，将目的地`x`和`y`值设置为`player_x`和`player_y`，计算玩家的旋转度数，然后将该精灵渲染到画布上。我们将之前对`SDL_RenderCopy`的呼叫换成了对`SDL_RenderCopyEx`的呼叫。这个新函数允许我们传入一个旋转飞船精灵的值。\n\n在我们定义了`render`函数之后，我们有了新的`game_loop`函数:\n\n```cpp\nvoid game_loop() {\n    input();\n    move();\n    render();\n}\n```\n\n这个函数将由`emscripten_set_main_loop`从我们的`main`函数中调用。这个函数运行渲染的每一帧，并负责管理我们游戏中进行的所有活动。它目前调用我们之前在游戏代码中定义的`input`、`move`和`render`函数，未来还会调用我们的 AI 代码、音效、物理代码等等。\n\n# 编译 gameloop.html\n\n现在我们已经写好了代码，我们可以继续编译我们的游戏循环应用了。在运行这个命令之前，我想重申一下，你需要从 GitHub([https://GitHub . com/PacktPublishing/hand-On-Game-Development-with-WebAssembly](https://github.com/PacktPublishing/Hands-On-Game-Development-with-WebAssembly))下载这个项目，因为你需要位于`/Chapter06-game-loop/sprites`文件夹中的 PNG 文件来构建这个项目。\n\n正确设置文件夹后，使用以下命令编译应用:\n\n```cpp\nemcc game_loop.c -o gameloop.html  --preload-file sprites -s NO_EXIT_RUNTIME=1 -s USE_SDL_IMAGE=2 -s SDL2_IMAGE_FORMATS=[\"png\"] -s EXTRA_EXPORTED_RUNTIME_METHODS=\"['cwrap', 'ccall']\" -s USE_SDL=2\n```\n\n使用 web 服务器为编译它的目录提供服务，或者使用 emrun 构建并运行它，当加载到 web 浏览器中时，它应该是这样的:\n\n![](img/13918915-c0b6-448b-b3be-996e683d26a9.png)\n\nThe screenshot gameloop.html It is important to remember that you must run WebAssembly apps using a web server, or with `emrun`.  If you would like to run your WebAssembly app using `emrun`, you must compile it with the `--emrun` flag.  The web browser requires a web server to stream the WebAssembly module.  If you attempt to open an HTML page that uses WebAssembly in a browser directly from your hard drive, that WebAssembly module will not load.\n\n应用编译完成后，你应该可以使用箭头键在画布上移动飞船。现在我们有了一个基本的游戏循环，在下一部分，我们将在我们的应用中添加一些游戏对象，使它更像一个游戏。\n\n# 游戏对象\n\n到目前为止，我们的方法完全是程序性的，并且已经被编码，所以它可以用 C 而不是 C++ 编写。开发人员已经用 C 语言甚至汇编语言编写游戏很长时间了，所以拥有面向对象的游戏设计方法并不是严格必要的，但是从代码管理的角度来看，OOP 是设计和编写游戏的一个很好的方法。游戏对象可以帮助我们通过对象池管理分配的内存。在这一点上，开始将我们的程序分成多个文件也是有意义的。我的方法是有一个单独的`.hpp`文件来定义我们所有的游戏对象，每个对象有一个`.cpp`文件。\n\n# 玩家的宇宙飞船游戏对象\n\n到目前为止，我们一直在全球变量中保留所有跟踪玩家船的值。从组织的角度来看，这并不理想。我们将创建的第一个游戏对象将是玩家的飞船对象。我们将从一个基本类开始，并在以后的代码中添加更多面向对象的特性。\n\n下面是我们新头文件`game.hpp`的代码:\n\n```cpp\n#ifndef __GAME_H__\n#define __GAME_H__#include <SDL2/SDL.h>\n#include <SDL2/SDL_image.h>\n#include <emscripten.h>\n#include <stdio.h>\n#include <stdbool.h>\n#include <math.h>\n#include <string>\n#include <vector>\n\n#define SPRITE_FILE \"sprites/Franchise.png\"\n#define MAX_VELOCITY 2.0\n#define PI 3.14159\n#define TWO_PI 6.28318\n\nextern Uint32 last_time;\nextern Uint32 last_frame_time;\nextern Uint32 current_time;\nextern SDL_Window *window;\nextern SDL_Renderer *renderer;\nextern SDL_Rect dest;\nextern SDL_Texture *sprite_texture;\nextern SDL_Event event;\nextern bool left_key_down;\nextern bool right_key_down;\nextern bool up_key_down;\nextern bool down_key_down;\nextern bool space_key_down;\nextern float delta_time;\nextern int diff_time;\n\nclass PlayerShip {\n    public:\n        float m_X;\n        float m_Y;\n        float m_Rotation;\n        float m_DX;\n        float m_DY;\n        float m_VX;\n        float m_VY;\n\n        PlayerShip();\n        void RotateLeft();\n        void RotateRight();\n        void Accelerate();\n        void Decelerate();\n        void CapVelocity();\n        void Move();\n        void Render();\n};\n\nextern PlayerShip player;\n#endif\n```\n\n我们所有的 CPP 文件都会包含这个`game.hpp`头文件。该文件的前几行是为了确保我们不会多次包含该文件。然后我们定义了我们在旧的 C 文件中定义的所有全局变量:\n\n```cpp\nextern Uint32 last_time;\nextern Uint32 last_frame_time;\nextern Uint32 current_time;\nextern SDL_Window *window;\nextern SDL_Renderer *renderer;\nextern SDL_Rect dest;\nextern SDL_Texture *sprite_texture;\nextern SDL_Event event;\nextern bool left_key_down;\nextern bool right_key_down;\nextern bool up_key_down;\nextern bool down_key_down;\nextern float delta_time;\n```\n\n在头文件中，我们没有给堆分配空间。在我们的全局变量定义之前使用`extern`关键字告诉编译器，我们在其中一个`.cpp`文件中声明了全局变量。现在，我们仍然有很多全局变量。当我们在本章中修改代码时，我们将减少这些全局变量的数量。\n\n如果这是生产代码，将所有这些值移动到类中是有意义的，但是，目前，我们只创建了一个`PlayerShip`对象。我们对`PlayerShip`也有自己的类定义。开发人员通常在头文件中创建类定义。\n\n在我们定义了所有的全局变量之后，我们将需要我们的类定义。\n\n下面是我们`PlayerShip`类的定义:\n\n```cpp\nclass PlayerShip {\n    public:\n        float m_X;\n        float m_Y;\n        float m_Rotation;\n        float m_DX;\n        float m_DY;\n        float m_VX;\n        float m_VY;\n\n        PlayerShip();\n        void RotateLeft();\n        void RotateRight();\n        void Accelerate();\n        void Decelerate();\n        void CapVelocity();\n        void Move();\n        void Render();\n };\n\nextern PlayerShip player;\n```\n\n在本书中，我们将声明我们所有的属性`public`。这意味着我们的代码可以从任何地方访问它们，而不仅仅是从这个函数内部。如果你和不止一个开发人员在一个项目上工作，这通常不被认为是一个好的实践。如果您不想让另一个开发人员直接修改只有类中的函数才能修改的特定属性，那么防止其他类直接修改我们的某些属性(如`m_DX`和`m_DY`)是一个好主意。然而，出于演示的目的，将我们类中的所有东西定义为`public`将简化我们的设计。\n\n在我们定义了我们的属性之后，我们有了一系列的函数，这些函数一旦被定义就会与这个类相关联。第一个函数`PlayerShip()`与我们的类同名，这使得它成为构造函数，也就是我们的 app 创建`PlayerShip`类型的对象时默认调用的函数。如果我们愿意，我们可以定义一个析构函数，当对象被破坏时，通过调用它`~PlayerShip()`来运行它。我们目前不需要这个对象的析构函数，所以我们不会在这里定义它，这意味着我们将依赖 C++ 为这个类创建一个*默认析构函数*。\n\n我们在这个类中定义的所有其他函数都对应于我们在游戏的早期 C 版本中创建的函数。将所有这些函数移到一个类中可以让我们更好地组织代码。请注意，在我们的类定义之后，我们创建了另一个全局变量，即名为`player`的`PlayerShip`。编译器在所有包含我们的`game.hpp`文件的`.cpp`文件中共享这个播放器对象。\n\n# 对象池\n\n我们已经定义了我们的第一个游戏对象，它代表了我们玩家的飞船，但是我们所能做的就是在游戏屏幕上飞来飞去。我们需要允许我们的玩家发射炮弹。如果我们在每次玩家发射炮弹时都创建一个新的炮弹对象，我们将很快填满 WASM 模块的内存。我们需要做的是创建一个所谓的**对象池**。对象池用于创建具有固定寿命的对象。我们的射弹只需要存活足够长的时间，要么击中目标，要么在消失前移动一段固定的距离。如果我们一次在屏幕上创建的投射物数量比我们需要的多一点，我们就可以将池中的这些对象保持在活动或非活动状态。当我们需要发射一个新的射弹时，我们扫描我们的对象池寻找一个不活动的，然后激活它并把它放在发射点。这样，我们就不会不断地分配和取消分配内存来创建我们的射弹。\n\n让我们回到我们的`game.hpp`文件，在`#endif`宏之前添加一些类定义:\n\n```cpp\nclass Projectile {\n    public:\n        const char* c_SpriteFile = \"sprites/Projectile.png\";\n        const int c_Width = 8;\n        const int c_Height = 8;\n        SDL_Texture *m_SpriteTexture;\n        bool m_Active;\n        const float c_Velocity = 6.0;\n        const float c_AliveTime = 2000;\n        float m_TTL;\n        float m_X;\n        float m_Y;\n        float m_VX;\n        float m_VY;\n\n        Projectile();\n        void Move();\n        void Render();\n        void Launch(float x, float y, float dx, float dy);\n};\n\nclass ProjectilePool {\n    public:\n        std::vector<Projectile*> m_ProjectileList;\n        ProjectilePool();\n        ~ProjectilePool();\n        void MoveProjectiles();\n        void RenderProjectiles();\n        Projectile* GetFreeProjectile();\n};\n\nextern ProjectilePool* projectile_pool; \n```\n\n因此，我们已经在`game.hpp`文件中定义了所有的类。现在，我们有三个班级:`PlayerShip`、`Projectile`和`ProjectilePool`。\n\n`PlayerShip`类以前就存在了，但是我们正在给这个类增加一些额外的功能来允许我们发射炮弹。为了实现这一新功能，我们在类定义中添加了一些新的公共属性:\n\n```cpp\npublic:\n    const char* c_SpriteFile = \"sprites/Franchise.png\";\n    const Uint32 c_MinLaunchTime = 300;\n    const int c_Width = 16;\n    const int c_Height = 16;\n    Uint32 m_LastLaunchTime;\n    SDL_Texture *m_SpriteTexture;\n```\n\n我们将`#define`宏中的一些值直接移动到类中。`c_SpriteFile`常量是我们将加载的用来渲染玩家飞船精灵的 PNG 文件的名称。`c_MinLaunchTime`常数是两次发射射弹之间的最短时间，单位为毫秒。我们还用`c_Width`和`c_Height`常数定义了精灵的宽度和高度。这样，我们可以为不同的对象类型设置不同的值。`m_LastLaunchTime`属性以毫秒为单位跟踪最近的射弹发射时间。雪碧纹理，之前是一个全局变量，将移动到玩家的船级属性。\n\n在对`PlayerShip`类定义进行修改后，我们必须为两个新类添加一个类定义。这两个类中的第一个是`Projectile`类:\n\n```cpp\nclass Projectile {\n    public:\n        const char* c_SpriteFile = \"sprites/Projectile.png\";\n        const int c_Width = 8;\n        const int c_Height = 8;\n        const float c_Velocity = 6.0;\n        const float c_AliveTime = 2000;\n\n        SDL_Texture *m_SpriteTexture;\n        bool m_Active;\n        float m_TTL;\n        float m_X;\n        float m_Y;\n        float m_VX;\n        float m_VY;\n\n        Projectile();\n        void Move();\n        void Render();\n        void Launch(float x, float y, float dx, float dy);\n};\n```\n\n这个类代表玩家将要射击的抛射体游戏物体，然后是敌人的飞船。我们从几个常数开始，这些常数定义了我们在虚拟文件系统中放置精灵的位置，以及宽度和高度:\n\n```cpp\nclass Projectile {\n    public:\n        const char* c_SpriteFile = \"sprites/Projectile.png\";\n        const int c_Width = 8;\n        const int c_Height = 8;\n```\n\n下一个属性是`m_SpriteTexture`，这是一个指向 SDL 纹理的指针，用来渲染我们的射弹。我们需要一个变量来告诉我们的对象池这个游戏对象是活动的。我们称之为属性`m_Active`。接下来，我们有一个常数来定义我们的射弹每秒移动的速度，以像素为单位，称为`c_Velocity`，还有一个常数来指示射弹在自毁之前存活的时间，以毫秒为单位，称为`c_AliveTime`。\n\n`m_TTL`变量是一个**生存时间**变量，它跟踪剩余多少毫秒，直到该射弹将其`m_Active`变量更改为`false`，并将其自身回收到**射弹池**中。`m_X`、`m_Y`、`m_VX`和`m_VY`变量用于跟踪我们射弹的`x`和`y`位置以及`x`和`y`速度。\n\n然后，我们为射弹类声明四个函数:\n\n```cpp\nProjectile();\nvoid Move();\nvoid Render();\nvoid Launch(float x, float y, float dx, float dy);\n```\n\n`Projectile`函数是我们的类构造函数。如果我们的抛射体当前处于活动状态，每帧将调用一次`Move`和`Render`。`Move`功能将管理活动投射体的移动，`Render`将管理将投射体精灵绘制到我们的 HTML 画布元素中。`Launch`功能将从我们的`PlayerShip`类调用，使我们的船向船面对的方向发射一枚炮弹。\n\n我们必须添加到`game.hpp`文件中的最后一个类定义是`ProjectilePool`类:\n\n```cpp\nclass ProjectilePool {\n    public:\n        std::vector<Projectile*> m_ProjectileList;\n        ProjectilePool();\n        ~ProjectilePool();\n        void MoveProjectiles();\n        void RenderProjectiles();\n        Projectile* GetFreeProjectile();\n};\n```\n\n这个职业管理一个由 10 个射弹组成的**池，储存在一个矢量属性`m_ProjectileList`中。这个类的函数包括构造函数和析构函数、`MoveProjectiles`、`RenderProjectils`和`GetFreeProjectile`。**\n\n`MoveProjectiles()`函数在我们的射弹列表中循环调用任何活动射弹上的`move`函数。`RenderProjectiles()`功能在我们的投射物列表中循环并渲染以画布任何活动的投射物，并且`GetFreeProjectile`返回我们池中第一个不活动的投射物。\n\n# 汇集玩家的投射物\n\n现在我们已经查看了`Projectile`和`ProjectilePool`类的类定义，我们需要创建一个`projectile.cpp`文件和一个`projectile_pool.cpp`文件来存储这些类的函数代码。因为这是在 [第 6 章](06.html)*游戏对象和游戏循环*中，我建议创建一个名为`Chapter06`的新文件夹来保存这些文件。该代码将完成汇集我们的射弹、在我们需要时请求一个非活动射弹以及移动和渲染我们的活动射弹的工作。首先，我们来看看`projectile.cpp`中的代码:\n\n```cpp\n#include \"game.hpp\"\n\nProjectile::Projectile() {\n    m_Active = false;\n    m_X = 0.0;\n    m_Y = 0.0;\n    m_VX = 0.0;\n    m_VY = 0.0;\n\n    SDL_Surface *temp_surface = IMG_Load( c_SpriteFile );\n\n    if( !temp_surface ) {\n        printf(\"failed to load image: %s\\n\", IMG_GetError() );\n        return;\n    }\n\n    m_SpriteTexture = SDL_CreateTextureFromSurface( renderer, \n    temp_surface );\n\n    if( !m_SpriteTexture ) {\n        printf(\"failed to create texture: %s\\n\", IMG_GetError() );\n        return;\n    }\n\n    SDL_FreeSurface( temp_surface );\n}\n\nvoid Projectile::Move() {\n    m_X += m_VX;\n    m_Y += m_VY;\n    m_TTL -= diff_time;\n\n    if( m_TTL <= 0 ) {\n        m_Active = false;\n        m_TTL = 0;\n    }\n}\n\nvoid Projectile::Render() {\n    dest.x = m_X;\n    dest.y = m_Y;\n    dest.w = c_Width;\n    dest.h = c_Height;\n\n    int return_val = SDL_RenderCopy( renderer, m_SpriteTexture,\n                                     NULL, &dest );\n    if( return_val != 0 ) {\n        printf(\"SDL_Init failed: %s\\n\", SDL_GetError());\n    }\n}\n\nvoid Projectile::Launch(float x, float y, float dx, float dy) {\n    m_X = x;\n    m_Y = y;\n    m_VX = c_Velocity * dx;\n    m_VY = c_Velocity * dy;\n    m_TTL = c_AliveTime;\n    m_Active = true;\n}\n```\n\n这是处理移动、渲染和发射单个射弹的代码。这里声明的第一个函数是构造函数:\n\n```cpp\nProjectile::Projectile() {\n    m_Active = false;\n    m_X = 0.0;\n    m_Y = 0.0;\n    m_VX = 0.0;\n    m_VY = 0.0;\n\n    SDL_Surface *temp_surface = IMG_Load( c_SpriteFile );\n\n    if( !temp_surface ) {\n        printf(\"failed to load image: %s\\n\", IMG_GetError() );\n        return;\n    }\n\n    m_SpriteTexture = SDL_CreateTextureFromSurface( renderer, \n    temp_surface );\n\n    if( !m_SpriteTexture ) {\n        printf(\"failed to create texture: %s\\n\", IMG_GetError() );\n        return;\n    }\n    SDL_FreeSurface( temp_surface );\n}\n```\n\n这个构造器主要关心的是将投射体设置为非活动状态，并创建一个 SDL 纹理，我们稍后将使用它来将我们的精灵渲染到画布元素。在定义了我们的构造函数之后，我们定义了我们的`Move`函数:\n\n```cpp\nvoid Projectile::Move() {\n    m_X += m_VX;\n    m_Y += m_VY;\n    m_TTL -= diff_time;\n    if( m_TTL <= 0 ) {\n        m_Active = false;\n        m_TTL = 0;\n    }\n}\n```\n\n该功能根据速度改变我们射弹的 *x* 和 *y* 位置，并减少我们射弹的生存时间，如果生存时间小于或等于零，将其设置为非活动状态并将其回收到射弹池中。我们定义的下一个函数是我们的`Render`函数:\n\n```cpp\nvoid Projectile::Render() {\n    dest.x = m_X;\n    dest.y = m_Y;\n    dest.w = c_Width;\n    dest.h = c_Height;\n\n    int return_val = SDL_RenderCopy( renderer, m_SpriteTexture,\n                                    NULL, &dest );\n\n    if( return_val != 0 ) {\n        printf(\"SDL_Init failed: %s\\n\", SDL_GetError());\n    }\n}\n```\n\n这段代码类似于我们用来渲染飞船的代码，所以你应该很熟悉。我们最后的投射函数是`Launch`函数:\n\n```cpp\nvoid Projectile::Launch(float x, float y, float dx, float dy) {\n    m_X = x;\n    m_Y = y;\n    m_VX = c_Velocity * dx;\n    m_VY = c_Velocity * dy;\n    m_TTL = c_AliveTime;\n    m_Active = true;\n}\n```\n\n每当玩家按下键盘上的空格键时，从`PlayerShip`类调用该函数。`PlayerShip`对象将在玩家船只的 *x* 和 *y* 坐标中通过，同时在`dx`和`dy`参数中船只正对的方向也将通过。这些参数用于设定射弹的 *x* 和 *y* 坐标以及射弹的`x`和`y`速度。游戏将生存时间设置为默认生存时间，然后将对象设置为活动状态。\n\n现在我们已经完全定义了我们的`Projectile`类，让我们设置`ProjectilePool`类来管理那些射弹。以下代码将在我们的`projectile_pool.cpp`文件中:\n\n```cpp\n#include \"game.hpp\"\n\nProjectilePool::ProjectilePool() {\n    for( int i = 0; i < 10; i++ ) {\n        m_ProjectileList.push_back( new Projectile() );\n    }\n}\n\nProjectilePool::~ProjectilePool() {\n    m_ProjectileList.clear();\n}\n\nvoid ProjectilePool::MoveProjectiles() {\n    Projectile* projectile;\n    std::vector<Projectile*>::iterator it;\n\n    for( it = m_ProjectileList.begin(); it != m_ProjectileList.end(); it++ ) {\n        projectile = *it;\n        if( projectile->m_Active ) {\n            projectile->Move();\n        }\n    }\n}\n\nvoid ProjectilePool::RenderProjectiles() {\n    Projectile* projectile;\n    std::vector<Projectile*>::iterator it;\n\n    for( it = m_ProjectileList.begin(); it != m_ProjectileList.end(); it++ ) {\n        projectile = *it;\n        if( projectile->m_Active ) {\n            projectile->Render();\n         }\n    }\n}\n\nProjectile* ProjectilePool::GetFreeProjectile() {\n    Projectile* projectile;\n    std::vector<Projectile*>::iterator it;\n\n    for( it = m_ProjectileList.begin(); it != m_ProjectileList.end(); it++ ) {\n        projectile = *it;\n        if( projectile->m_Active == false ) {\n            return projectile;\n        }\n    }\n    return NULL;\n}\n```\n\n前两个函数是构造函数和析构函数。这些功能会产生并摧毁我们列表中的投射物。下一个函数是`MoveProjectiles`函数，它在我们的`m_ProjectileList`中循环寻找活动的抛射体并移动它们。之后，我们有了一个`RenderProjectiles`功能，和我们的`MoveProjectiles`功能非常相似。这个函数在我们的列表中循环，在所有活动的射弹上调用`Render`函数。最后一个功能是`GetFreeProjectile`功能，它通过`m_ProjectileList`寻找第一个未激活的射弹以将其返回。每当我们想要发射一个抛射体时，我们都需要调用这个函数来找到一个不活动的。\n\n# 制造敌人\n\n所以，现在我们有一艘玩家船正在射击，我们可以增加一艘敌人船。它将类似于`PlayerShip`类。稍后，我们将进入类继承，这样我们就不会得到相同代码的复制和粘贴版本，但是现在我们将向我们的`game.hpp`文件添加一个新的类定义，它几乎与我们的`PlayerShip`类相同:\n\n```cpp\nenum FSM_STUB {\n    SHOOT = 0,\n    TURN_LEFT = 1,\n    TURN_RIGHT = 2,\n    ACCELERATE = 3,\n    DECELERATE = 4\n};\n\nclass EnemyShip {\n    public:\n        const char* c_SpriteFile = \"sprites/BirdOfAnger.png\";\n        const Uint32 c_MinLaunchTime = 300;\n        const int c_Width = 16;\n        const int c_Height = 16;\n        const int c_AIStateTime = 2000;\n\n        Uint32 m_LastLaunchTime;\n        SDL_Texture *m_SpriteTexture;\n\n        FSM_STUB m_AIState;\n        int m_AIStateTTL;\n\n        float m_X;\n        float m_Y;\n        float m_Rotation;\n        float m_DX;\n        float m_DY;\n        float m_VX;\n        float m_VY;\n\n        EnemyShip();\n        void RotateLeft();\n        void RotateRight();\n        void Accelerate();\n        void Decelerate();\n        void CapVelocity();\n        void Move();\n        void Render();\n        void AIStub();\n};\n```\n\n您会注意到在`EnemyShip`类之前，我们定义了一个`FSM_STUB`枚举。枚举就像一种可以在 C 或 C++ 代码中定义的新数据类型。我们将在另一章中讨论**人工智能**和**有限状态机**，但是现在我们仍然希望我们的敌船做一些事情，即使这些事情不是很智能。我们创建了一个`FSM_STUB`枚举来定义我们的敌船当前可以做的事情。我们还在我们的`EnemyShip`类中创建了一个`AIStub`，它将作为未来人工智能逻辑的替身。`m_AIStateTTL`整数属性是人工智能状态变化的倒计时。还有一个名为`c_AIStateTime`的新常数，其值为`2000`。这是我们的人工智能状态在随机变化之前持续的毫秒数。\n\n我们将创建一个`enemy_ship.cpp`文件，并向其中添加九个函数。第一个函数是我们的构造函数，前面是我们的`game.hpp`文件的`#include`:\n\n```cpp\n#include \"game.hpp\"\nEnemyShip::EnemyShip() {\n m_X = 60.0;\n    m_Y = 50.0;\n    m_Rotation = PI;\n    m_DX = 0.0;\n    m_DY = 1.0;\n    m_VX = 0.0;\n    m_VY = 0.0;\n    m_LastLaunchTime = current_time;\n\n    SDL_Surface *temp_surface = IMG_Load( c_SpriteFile );\n\n    if( !temp_surface ) {\n        printf(\"failed to load image: %s\\n\", IMG_GetError() );\n        return;\n    }\n    else {\n        printf(\"success creating enemy ship surface\\n\");\n    }\n    m_SpriteTexture = SDL_CreateTextureFromSurface( renderer, \n    temp_surface );\n\n    if( !m_SpriteTexture ) {\n        printf(\"failed to create texture: %s\\n\", IMG_GetError() );\n        return;\n    }\n    else {\n        printf(\"success creating enemy ship texture\\n\");\n    }\n    SDL_FreeSurface( temp_surface );\n}\n```\n\n之后，我们有了`RotateLeft`和`RotateRight`这两个用来转动宇宙飞船的功能:\n\n```cpp\nvoid EnemyShip::RotateLeft() {\n    m_Rotation -= delta_time;\n\n    if( m_Rotation < 0.0 ) {\n        m_Rotation += TWO_PI;\n    }\n    m_DX = sin(m_Rotation);\n    m_DY = -cos(m_Rotation);\n}\nvoid EnemyShip::RotateRight() {\n    m_Rotation += delta_time;\n\n    if( m_Rotation >= TWO_PI ) {\n        m_Rotation -= TWO_PI;\n    }\n    m_DX = sin(m_Rotation);\n    m_DY = -cos(m_Rotation);\n}\n```\n\n功能`Accelerate`、`Decelerate`和`CapVelocity`都是用来修改敌方飞船的速度。：\n\n```cpp\nvoid EnemyShip::Accelerate() {\n    m_VX += m_DX * delta_time;\n    m_VY += m_DY * delta_time;\n}\n\nvoid EnemyShip::Decelerate() {\n    m_VX -= (m_DX * delta_time) / 2.0;\n    m_VY -= (m_DY * delta_time) / 2.0;\n}\n\nvoid EnemyShip::CapVelocity() {\n    float vel = sqrt( m_VX * m_VX + m_VY * m_VY );\n\n    if( vel > MAX_VELOCITY ) {\n        m_VX /= vel;\n        m_VY /= vel;\n\n        m_VX *= MAX_VELOCITY;\n        m_VY *= MAX_VELOCITY;\n    }\n}\n```\n\n我们添加到文件中的下一件事是`Render`函数:\n\n```cpp\nvoid EnemyShip::Render() {\n    dest.x = (int)m_X;\n    dest.y = (int)m_Y;\n    dest.w = c_Width;\n    dest.h = c_Height;\n\n    float degrees = (m_Rotation / PI) * 180.0;\n\n    int return_code = SDL_RenderCopyEx( renderer, m_SpriteTexture,\n                                        NULL, &dest,\n                                        degrees, NULL, SDL_FLIP_NONE );\n\n if( return_code != 0 ) {\n printf(\"failed to render image: %s\\n\", IMG_GetError() );\n }\n}\n\n```\n\n最后，我们添加`Move`和`AIStub`功能:\n\n```cpp\nvoid EnemyShip::Move() {\n     AIStub();\n\n if( m_AIState == TURN_LEFT ) {\n     RotateLeft();\n }\n\n if( m_AIState == TURN_RIGHT ) {\n     RotateRight();\n }\n\n if( m_AIState == ACCELERATE ) {\n     Accelerate();\n }\n\n if( m_AIState == DECELERATE ) {\n     Decelerate();\n }\n\n CapVelocity();\n m_X += m_VX;\n\n if( m_X > 320 ) {\n     m_X = -16;\n }\n else if( m_X < -16 ) {\n     m_X = 320;\n }\n\n m_Y += m_VY;\n\n if( m_Y > 200 ) {\n     m_Y = -16;\n }\n else if( m_Y < -16 ) {\n     m_Y = 200;\n }\n\n if( m_AIState == SHOOT ) {\n     Projectile* projectile;\n     if( current_time - m_LastLaunchTime >= c_MinLaunchTime ) {\n         m_LastLaunchTime = current_time;\n         projectile = projectile_pool->GetFreeProjectile();\n\n         if( projectile != NULL ) {\n             projectile->Launch( m_X, m_Y, m_DX, m_DY );\n             }\n         }\n     }\n}\n\nvoid EnemyShip::AIStub() {\n     m_AIStateTTL -= diff_time;\n     if( m_AIStateTTL <= 0 ) {\n         // for now get a random AI state.\n         m_AIState = (FSM_STUB)(rand() % 5);\n         m_AIStateTTL = c_AIStateTime;\n     }\n}\n```\n\n除了`Move`功能外，这些功能都与我们的`player_ship.cpp`文件中定义的功能相同。我们增加了一个新功能，`AIStub`。以下是`AIStub`功能中的代码:\n\n```cpp\nvoid EnemyShip::AIStub() {\n    m_AIStateTTL -= diff_time;\n\n    if( m_AIStateTTL <= 0 ) {\n        // for now get a random AI state.\n        m_AIState = (FSM_STUB)(rand() % 5);\n        m_AIStateTTL = c_AIStateTime;\n    }\n}\n```\n\n这个功能是暂时的。我们最终将为我们的敌人飞船定义一个真正的人工智能。现在，该功能使用`m_AIStateTTL`倒计时固定的毫秒数，直到达到或低于`0`。此时，它会根据我们之前定义的名为`FSM_STUB`的枚举中的一个值随机设置一个新的人工智能状态。我们还对为玩家船创建的`Move()`功能进行了一些修改:\n\n```cpp\nvoid EnemyShip::Move() {\n    AIStub();\n\n    if( m_AIState == TURN_LEFT ) {\n        RotateLeft();\n    }\n    if( m_AIState == TURN_RIGHT ) {\n        RotateRight();\n    }\n    if( m_AIState == ACCELERATE ) {\n        Accelerate();\n    }\n    if( m_AIState == DECELERATE ) {\n        Decelerate();\n    }\n    CapVelocity();\n     m_X += m_VX;\n\n    if( m_X > 320 ) {\n        m_X = -16;\n    }\n    else if( m_X < -16 ) {\n        m_X = 320;\n    }\n    m_Y += m_VY;\n\n    if( m_Y > 200 ) {\n        m_Y = -16;\n    }\n    else if( m_Y < -16 ) {\n        m_Y = 200;\n    }\n\n    if( m_AIState == SHOOT ) {\n        Projectile* projectile;\n        if( current_time - m_LastLaunchTime >= c_MinLaunchTime ) {\n            m_LastLaunchTime = current_time;\n            projectile = projectile_pool->GetFreeProjectile();\n\n            if( projectile != NULL ) {\n                projectile->Launch( m_X, m_Y, m_DX, m_DY );\n            }\n        }\n    }\n}\n```\n\n我已经从我们的`PlayerShip::Move`函数中获取了代码，并对其进行了一些修改。在这个新函数的开始，我们增加了对`AIStub`函数的调用。这个功能是我们未来 AI 的替身。而不是像我们对玩家船那样看我们的键盘输入，敌人船会看 AI 状态，选择向左旋转、向右旋转、加速、减速或射击。那不是真正的 AI，它只是船在做随机的事情，但是它让我们对船有了真正的 AI 之后会是什么样子有了一个大概的了解，它会让我们以后增加更多的功能，比如碰撞检测。\n\n# 编译游戏对象. html\n\n现在我们已经构建了所有这些游戏对象，我们不再拥有单个文件中的所有内容。我们需要包含几个 CPP 文件，并将它们全部编译成一个我们称之为`game_objects.html`的输出文件。因为我们已经从 C 的世界转移到 C++，我们将使用 em++ 来表示我们正在编译的文件是 C++ 文件，而不是 C 文件。严格来说，这并不是必须的，因为当 Emscripten 接收到带有`.cpp`扩展名的文件作为输入时，它会发现我们正在用 C++ 进行编译。当我们传入`-std=c++ 17`标志时，我们也明确地告诉编译器我们正在使用的 C++ 版本。使用以下 em++ 命令编译`game_objects.html`文件:\n\n```cpp\nem++ main.cpp enemy_ship.cpp player_ship.cpp projectile.cpp projectile_pool.cpp -std=c++ 17 --preload-file sprites -s USE_WEBGL2=1 -s USE_SDL=2 -s USE_SDL_IMAGE=2 -s SDL2_IMAGE_FORMATS=[\"png\"] -o game_objects.html\n```\n\n现在我们已经编译了我们的`game_objects.html`文件，使用网络服务器来提供文件并在浏览器中打开它，它应该如下所示:\n\n![](img/23123d7f-c321-4339-b623-e4be90bc4388.png)\n\nA screenshot of game_objects.html\n\nDo not forget that you must run WebAssembly apps using a web server, or with `emrun`.  If you would like to run your WebAssembly app using `emrun`, you must compile it with the `--emrun` flag.  The web browser requires a web server to stream the WebAssembly module.  If you attempt to open an HTML page that uses WebAssembly in a browser directly from your hard drive, that WebAssembly module will not load.\n\n你可以用箭头键在画布上移动你的飞船，用空格键发射炮弹。敌舰会在画布周围随意移动射击。\n\nIf you are having problems building this app, or any of the other apps in this book, please remember you can contact me on Twitter, [https://twitter.com/battagline/](https://twitter.com/battagline/), using the Twitter handle `@battagline` to ask questions. I am happy to help.\n\n# 摘要\n\n在这一章中，我们学习了如何创建一个基本的游戏框架。我们了解了什么是游戏循环，以及如何使用 Emscripten 为 WebAssembly 创建一个游戏循环。我们了解了游戏对象，并创建了一些类来定义玩家的飞船、敌人的飞船和投射物。我们了解了对象池，以及如何使用对象池来回收内存中的对象，这样我们就不需要不断地在内存中创建和销毁新对象。我们利用这些知识为我们的射弹创建了一个对象池。我们还为我们的敌人飞船创建了一个人工智能存根，给了那个物体随机行为，我们还创建了一些功能，让我们的玩家和敌人互相射击，同时我们的投射物无害地穿过飞船。\n\n在下一章结束时，我们将添加碰撞检测；这将允许我们的射弹摧毁他们击中的宇宙飞船，并添加一个动画序列，显示一艘船被其中一枚射弹击中时被摧毁。"
  },
  {
    "path": "docs/handson-game-dev-wasm/07.md",
    "content": "# 七、碰撞检测\n\n现在，我们的宇宙飞船可以飞来飞去，互相射击，但是什么都没有发生。\n\n**碰撞检测**在绝大多数电子游戏中用于判断游戏物体是否相交。有很多方法可以检测不同游戏对象之间的冲突。各种方法可以在不同的情况下更好地工作。在计算时间和碰撞检测的准确性之间也有权衡。\n\nYou will need to include several images in your build to make this project work. Make sure you include the `/Chapter07/sprites/` folder from the project's GitHub. If you haven't yet downloaded the GitHub project, you can get it online here: [https://github.com/PacktPublishing/Hands-On-Game-Develop](https://github.com/PacktPublishing/Hands-On-Game-Development-with-WebAssembly)[ment-with-WebAssembly](https://github.com/PacktPublishing/Hands-On-Game-Development-with-WebAssembly).\n\n在本章中，我们将讨论以下内容:\n\n*   冲突检出\n*   碰撞物体\n*   对撞机的类型\n*   给我们的游戏对象添加碰撞器\n\n# 2D 碰撞检测的类型\n\n我可以写一整本书，介绍我们可以使用的 2D 碰撞检测的种类，更不用说 3D 碰撞检测的数量了。我已经在[https://www . embed . com/TypeScript-games/basic-collection-detection . html](https://www.embed.com/typescript-games/basic-collision-detection.html)上写了几个关于如何使用不同检测技术的 TypeScript 教程，既有基本的也有复杂的，但是，在本书中，我们将坚持使用一些更基本的碰撞技术的组合。\n\n# 圆形碰撞检测\n\n最基本的一种碰撞检测是**圆**或**距离**碰撞检测。如果我们把我们所有的对撞机都当作有半径和位置的小圆，我们就可以计算出两个位置之间的距离，看看这个距离是否小于我们半径的总和。这种形式的碰撞检测速度很快，但精度有限。如果你看看我们游戏中的抛射体，这个方法效果相当不错。另一方面，我们的宇宙飞船并不整齐地排成一圈。我们可以在任何给定的船上调整我们的圆形对撞机的半径，以给出稍微不同的结果。当圆碰撞检测工作时，它可以非常有效:\n\n![](img/7d6fab86-e4bd-4a7c-9d6c-f8dcd049fe88.png)\n\n<sub>Circle collision hit test</sub>\n\n# 矩形碰撞检测\n\n**矩形**碰撞检测是另一种快速碰撞检测方法。在许多情况下，它可能比圆碰撞检测更快。矩形碰撞器由一个 *x* 和一个 *y* 坐标定义，这是我们矩形左上角的位置，以及宽度和高度。检测矩形碰撞非常简单。我们在两个矩形之间的 *x* 轴上寻找重叠。如果在 *x* 轴上有重叠，那么我们在 *y* 轴上寻找重叠。如果我们在两个轴上都有重叠，就会发生碰撞。这种技术在许多老派电子游戏中非常有效。任天堂娱乐系统上发布的几款经典游戏都使用了这种碰撞检测方法。在我们正在写的游戏中，我们正在旋转我们的精灵，所以使用传统的无导向碰撞检测对我们来说没有用。\n\n# 三角学的短期复习\n\n此时，我们的碰撞检测算法开始变得更加复杂。你可能还记得高中三角学课上的一些概念，但是一些基本的三角学对于很多碰撞检测算法来说非常重要。甚至我们前面讨论的圆碰撞检测都依赖于毕达哥拉斯定理，所以，在现实中，除非你在做简单的非定向矩形碰撞检测，否则至少需要极少量的三角测量。三角学是数学中对三角形的研究。大多数游戏使用所谓的笛卡尔坐标系。如果你不熟悉这个短语，*笛卡尔坐标系*意味着我们有一个带有 *x* 和 *y* 坐标的网格(对于 2D 游戏)。\n\nThe word *Cartesian* means Rene Descartes invented it—the \"*I think; therefore, I am\"* guy who had a lot of great ideas in mathematics and a lot of stupid ideas in philosophy (ghost in the machine…yuck!).\n\n高中三角学课上有几个关键概念我们要记住，都和直角三角形有关。直角三角形是一个 90 度角的三角形。当您使用笛卡尔坐标系时，这是一件很方便的事情，因为您的 *x* 和 *y* 轴恰好形成直角，因此两点之间不共享 *x* 或 *y* 坐标的任何直线都可以被视为直角三角形的斜边(长边)。有几个比率我们也需要记住；它们如下:\n\n*   sine-y/下丘脑\n*   *余弦- X /斜边*\n*   *切线- Y / X*\n\n你还记得 SOHCAHTOA 吗？(发音为“*袜子-啊-脚趾-啊*”)\n\n这是为了提醒你三角比率的以下版本:\n\n*   *正弦-相反/斜边*\n*   *余弦-相邻/斜边*\n*   *切线-相反/相邻*\n\n在这个公式中，三角形的*对面的*边是 *y* 轴，三角形的相邻边是 *x* 轴。如果你记得 SOHCAHTOA，你可能会更容易记住这些比率。如果没有，就打开这本书备份或使用谷歌:\n\n![](img/1b577127-7c70-4995-b1fc-94ff8c9025ff.png)\n\n<sub>SOHCAHTOA</sub> Some people have been taught the phrase \"*Some Old Horse Came A-Hoppin' Through Our Alley.\"* I'm not sure if that is helpful. I find it more difficult to remember than SOHCAHTOA, but that's a matter of opinion. So, if imagining a horse that hops like a rabbit around some city's back alley is your bag, then, by all means, use that instead.\n\n你可能还记得在这本书的前面，我们用`sin`和`cos`数学库函数来计算我们的船在 *x* 轴和 *y* 轴上移动的速度。这些函数返回给定角度的比值。\n\n我们需要知道的另一个概念是两个**单位向量**之间的**点积**。单位向量是长度为 1 的向量。两个单位向量之间的点积正好是这两个单位向量之间角度的余弦。点积越接近 1，两个向量之间的角度越接近 0 度。如果点积接近 0，则两个向量之间的角度接近 90 度，如果两个角度之间的点积接近-1，则两个向量之间的角度接近 180 度。不同矢量之间的点积在碰撞检测和游戏物理中都非常有用。参考下图:\n\n![](img/82871b59-db4d-4e5d-b1f9-62e36b1075f3.png)\n\n<sub>The dot product of two normalized vectors</sub>\n\n# 直线碰撞检测\n\n所以，我们需要做的第一件事是谈论直线和线段的区别。我们用两点定义一条线。那条线一直延伸到无穷远。线段在两点处终止，不会无限延续。两条不平行的线总会在某处相交。两条不平行的线段可以相交，也可以不相交。\n\n大多数情况下，在游戏中，我们有兴趣知道两条线段是否相交:\n\n![](img/b6e81faa-2b8e-49df-b571-e6f1037363e4.png)\n\n<sub>Line versus line segment</sub>\n\n确定直线是否与线段相交相对容易。你所要做的就是看线段的两个点是否在你的直线的相对两侧。因为一条线是无限的，这意味着你的线段必须与你的线相交。如果想找出两条线段是否相交，可以分两个阶段来做。首先，找出线段 A 是否与无限线段 B 相交，如果相交，则找出线段 B 是否与无限线段 A 相交，如果这两种情况都成立，则线段相交。\n\n那么，下一个问题是，我们如何从数学上知道两点是否在一条线的相对两侧？为此，我们将使用前面讨论的点积和一个叫做**向量法线**的东西。向量法线只是向量的 90 度旋转版本。请参见下图:\n\n![](img/4af90a66-08e0-48b0-9908-f3459639475b.png)\n\nA vector and that vector's normal\n\n我们还需要一个矢量，它的原点在同一点，但方向指向线段的点 1。如果这两个向量的点积是正值，这意味着该点与归一化向量在同一条直线上。如果点积是负值，那就意味着点在法向量的直线的另一边。如果线段相交，这意味着一个点有一个正点积，而另一边有一个负点积。因为将两个负数和两个正数相乘会得到正结果，将一个负数和一个正数相乘会得到负结果，所以将两个点积相乘，看看结果值是否为负。如果是，线段与直线相交:\n\n![](img/c0ad57a9-0d05-4ef3-a8bd-ffd8a4b47b63.png)\n\n<sub>Determining whether two points are on the opposite side of a line</sub>\n\n# 复合对撞机\n\n一个**复合碰撞器**是当一个游戏物体使用多个碰撞器来判断是否发生了碰撞。我们将在我们的船上使用复合圆碰撞器来提高我们的船碰撞检测的准确性，同时仍然提供使用圆碰撞器的增加的速度。我们将用三个圆圈覆盖玩家的船和敌人的船。我们的射弹是圆形的，所以用圆形射弹是完全自然的。没有理由需要限制复合对撞机只使用一种形状的对撞机。在内部，一个复合对撞机可以混合圆形对撞机和矩形对撞机或者任何你喜欢的类型。\n\n下图显示了由一个圆形和两个矩形对撞机组成的假想复合对撞机:\n\n![](img/8d0dbfbf-0bad-4e75-b06e-531557aea8f6.png)\n\n<sub>A compound collider composed of three basic colliders</sub>\n\n在下一节中，我们将学习如何实现一个基本的圆碰撞检测算法。\n\n# 实现圆碰撞检测\n\n我们将从实现圆形碰撞检测开始，因为这是最快的碰撞检测方法。它也非常适合我们的射弹，这将是我们游戏中最常见的碰撞器。它不会在我们的飞船上做得很好，但是后来，我们可以通过实现一个复合对撞机来改善这种情况，该对撞机将对每艘飞船使用多个圆形对撞机，而不是只有一个。因为我们只有两艘宇宙飞船，这将使我们在碰撞检测中两全其美:圆形碰撞检测的速度，以及我们一些更好的碰撞检测方法的准确性。\n\n让我们从在我们的`game.hpp`文件中添加一个`Collider`类定义开始，并创建一个新的`collider.cpp`文件，我们可以在其中定义我们的`Collider`类使用的函数。以下是我们新的`Collider`类在`game.hpp`文件中的样子:\n\n```cpp\nclass Collider {\n    public:\n        double m_X;\n        double m_Y;\n        double m_Radius;\n\n        Collider(double radius);\n\n        bool HitTest( Collider *collider );\n};\n```\n\n下面是我们放入`collider.cpp`文件的代码:\n\n```cpp\n#include \"game.hpp\"\nCollider::Collider(double radius) {\n    m_Radius = radius;\n}\n\nbool Collider::HitTest( Collider *collider ) {\n    double dist_x = m_X - collider->m_X;\n    double dist_y = m_Y - collider->m_Y;\n    double radius = m_Radius + collider->m_Radius;\n\n    if( dist_x * dist_x + dist_y * dist_y <= radius * radius ) {\n        return true;\n    }\n    return false;\n}\n```\n\n`Collider`类是一个相当简单的圆碰撞器。正如我们之前讨论的，圆形对撞机有一个 *x* 和一个 *y* 坐标和一个半径。`HitTest`功能做了一个非常简单的距离测试，看看两个圆是否足够近，可以相互接触。我们通过平方 *x* 距离和平方 *y* 两个对撞机之间的距离来实现，这就给出了两点之间的距离平方。我们可以用平方根来确定实际距离，但是平方根是一个执行起来相对较慢的函数，对半径之和求平方来进行比较要快得多。\n\n我们还需要简单谈谈类继承。如果你回顾我们之前的代码，我们有一个`PlayerShip`类和一个`EnemyShip`类。这些类共享它们的大部分属性。它们都有 *x* 和 *y* 坐标， *x* 和 *y* 速度，以及许多其他相同的属性。许多函数使用相同的代码复制和粘贴。让我们返回并创建一个`Ship`类，该类具有我们的`PlayerShip`和`EnemyShip`类所共有的所有特性，而不是将这段代码定义两次。然后，我们可以重构我们的`EnemyShip`和`PlayerShip`类来继承我们的`Ship`类。这是我们添加到`game.hpp`中的新的`Ship`类定义:\n\n```cpp\nclass Ship: public Collider {\n    public:\n        Uint32 m_LastLaunchTime;\n        const int c_Width = 16;\n        const int c_Height = 16;\n        SDL_Texture *m_SpriteTexture;\n        Ship();\n        float m_Rotation;\n        float m_DX;\n        float m_DY;\n        float m_VX;\n        float m_VY;\n\n        void RotateLeft();\n        void RotateRight();\n        void Accelerate();\n        void Decelerate();\n        void CapVelocity();\n\n        virtual void Move() = 0;\n        void Render();\n};\n```\n\n第一行`Ship class: public Collider`，告诉我们`Ship`将继承`Collider`类的所有公共和受保护成员。我们这样做是因为我们希望能够执行命中测试。`Collider`类现在还定义了`m_X`和`m_Y`属性变量，用于跟踪我们对象的 *x* 和 *y* 坐标。我们已经将`EnemyShip`和`PlayerShip`课程中常见的内容移到了`Ship`课程中。你会注意到我们有一个虚拟功能，`virtual void Move() = 0;`。这一行告诉我们，我们将在所有继承自`Ship`的类中有一个`Move`函数，但是我们需要在这些类中定义`Move`，而不是直接在`Ship`类中定义。这使得`Ship`成为一个**抽象类**，这意味着我们不能创建一个`Ship`的实例，相反，它是一个其他类将继承的类。\n\nClass inheritance, abstract classes, and virtual functions are all a part of a style of programming known as **Object-Oriented Programming** (**OOP**). C++ was created in 1979 by Bjarne Stroustrup to add OOP to the C programming language. If you're not familiar with OOP, there are hundreds of books that go into great detail on this topic. I will only be able to cover it in a cursory manner in this book.\n\n接下来，我们将修改`game.hpp`文件中的`PlayerShip`和`EnemyShip`类，以移除我们已经移动到父`Ship`类中的所有方法和属性。我们还将修改这些类，使它们继承自`Ship`。以下是新版本的类定义:\n\n```cpp\nclass PlayerShip: public Ship {\n    public:\n        const char* c_SpriteFile = \"sprites/Franchise.png\";\n        const Uint32 c_MinLaunchTime = 300;\n        PlayerShip();\n        void Move();\n};\n\nclass EnemyShip: public Ship {\n    public:\n        const char* c_SpriteFile = \"sprites/BirdOfAnger.png\";\n        const Uint32 c_MinLaunchTime = 300;\n        const int c_AIStateTime = 2000;\n        FSM_STUB m_AIState;\n        int m_AIStateTTL;\n\n        EnemyShip();\n        void AIStub();\n        void Move();\n};\n```\n\n现在，我们需要添加一个`ship.cpp`文件，并定义所有对`EnemyShip`和`PlayerShip`通用的方法。这些方法以前在`PlayerShip`和`EnemyShip`都有，但是现在我们可以把它们都放在一个地方。以下是`ship.cpp`文件的样子:\n\n```cpp\n#include \"game.hpp\"\n\nShip::Ship() : Collider(8.0) {\n    m_Rotation = PI;\n    m_DX = 0.0;\n    m_DY = 1.0;\n    m_VX = 0.0;\n    m_VY = 0.0;\n    m_LastLaunchTime = current_time;\n}\n\nvoid Ship::RotateLeft() {\n    m_Rotation -= delta_time;\n\n    if( m_Rotation < 0.0 ) {\n        m_Rotation += TWO_PI;\n    }\n    m_DX = sin(m_Rotation);\n    m_DY = -cos(m_Rotation);\n}\n\nvoid Ship::RotateRight() {\n    m_Rotation += delta_time;\n\n    if( m_Rotation >= TWO_PI ) {\n        m_Rotation -= TWO_PI;\n    }\n    m_DX = sin(m_Rotation);\n    m_DY = -cos(m_Rotation);\n}\n\nvoid Ship::Accelerate() {\n    m_VX += m_DX * delta_time;\n    m_VY += m_DY * delta_time;\n}\n\nvoid Ship::Decelerate() {\n    m_VX -= (m_DX * delta_time) / 2.0;\n    m_VY -= (m_DY * delta_time) / 2.0;\n}\nvoid Ship::CapVelocity() {\n    double vel = sqrt( m_VX * m_VX + m_VY * m_VY );\n\n    if( vel > MAX_VELOCITY ) {\n        m_VX /= vel;\n        m_VY /= vel;\n\n        m_VX *= MAX_VELOCITY;\n        m_VY *= MAX_VELOCITY;\n    }\n}\nvoid Ship::Render() {\n    dest.x = (int)m_X;\n    dest.y = (int)m_Y;\n    dest.w = c_Width;\n    dest.h = c_Height;\n\n    double degrees = (m_Rotation / PI) * 180.0;\n\n    int return_code = SDL_RenderCopyEx( renderer, m_SpriteTexture,\n                                        NULL, &dest,\n                                        degrees, NULL, SDL_FLIP_NONE );\n\n    if( return_code != 0 ) {\n        printf(\"failed to render image: %s\\n\", IMG_GetError() );\n    }\n}\n```\n\n这些类在`player_ship.cpp`和`enemy_ship.cpp`文件中的版本之间唯一真正的区别是，我们现在在函数定义前面有`Ship::`，而不是在每个函数定义前面有`PlayerShip::`或`EnemyShip::`。\n\n接下来，我们需要修改`player_ship.cpp`和`enemy_ship.cpp`，删除我们现在在`ship.cpp`文件中定义的所有函数。让我们来看看`enemy_ship.cpp`文件分成两部分是什么样子的。第一部分是我们的`game.hpp`文件的`#include`和`EnemyShip`构造函数:\n\n```cpp\n#include \"game.hpp\"\n\nEnemyShip::EnemyShip() {\n    m_X = 60.0;\n    m_Y = 50.0;\n    m_Rotation = PI;\n    m_DX = 0.0;\n    m_DY = 1.0;\n    m_VX = 0.0;\n    m_VY = 0.0;\n    m_LastLaunchTime = current_time;\n\n    SDL_Surface *temp_surface = IMG_Load( c_SpriteFile );\n\n    if( !temp_surface ) {\n        printf(\"failed to load image: %s\\n\", IMG_GetError() );\n        return;\n    }\n    else {\n        printf(\"success creating enemy ship surface\\n\");\n    }\n    m_SpriteTexture = SDL_CreateTextureFromSurface( renderer, \n    temp_surface );\n\n    if( !m_SpriteTexture ) {\n        printf(\"failed to create texture: %s\\n\", IMG_GetError() );\n        return;\n    }\n    else {\n        printf(\"success creating enemy ship texture\\n\");\n    }\n\n    SDL_FreeSurface( temp_surface );\n}\n\n```\n\n在我们的`enemy_ship.cpp`文件的第二部分，我们有`Move`和`AIStub`功能:\n\n```cpp\nvoid EnemyShip::Move() {\n    AIStub();\n\n    if( m_AIState == TURN_LEFT ) {\n        RotateLeft();\n    }\n\n    if( m_AIState == TURN_RIGHT ) {\n        RotateRight();\n    }\n\n    if( m_AIState == ACCELERATE ) {\n        Accelerate();\n    }\n\n    if( m_AIState == DECELERATE ) {\n        Decelerate();\n    }\n\n    CapVelocity();\n    m_X += m_VX;\n\n    if( m_X > 320 ) {\n        m_X = -16;\n    }\n    else if( m_X < -16 ) {\n        m_X = 320;\n    }\n\n    m_Y += m_VY;\n\n    if( m_Y > 200 ) {\n        m_Y = -16;\n    }\n    else if( m_Y < -16 ) {\n        m_Y = 200;\n    }\n\n    if( m_AIState == SHOOT ) {\n        Projectile* projectile;\n\n        if( current_time - m_LastLaunchTime >= c_MinLaunchTime ) {\n            m_LastLaunchTime = current_time;\n            projectile = projectile_pool->GetFreeProjectile();\n\n            if( projectile != NULL ) {\n                projectile->Launch( m_X, m_Y, m_DX, m_DY );\n            }\n        }\n    }\n}\n\nvoid EnemyShip::AIStub() {\n    m_AIStateTTL -= diff_time;\n\n    if( m_AIStateTTL <= 0 ) {\n        // for now get a random AI state.\n        m_AIState = (FSM_STUB)(rand() % 5);\n        m_AIStateTTL = c_AIStateTime;\n    }\n}\n```\n\n现在我们已经看到了`enemy_ship.cpp`文件中的内容，让我们来看看新的`player_ship.cpp`文件是什么样子的:\n\n```cpp\n#include \"game.hpp\"\nPlayerShip::PlayerShip() {\n    m_X = 160.0;\n    m_Y = 100.0;\n    SDL_Surface *temp_surface = IMG_Load( c_SpriteFile );\n\n    if( !temp_surface ) {\n        printf(\"failed to load image: %s\\n\", IMG_GetError() );\n        return;\n    }\n\n    m_SpriteTexture = SDL_CreateTextureFromSurface( renderer, \n    temp_surface );\n\n    if( !m_SpriteTexture ) {\n        printf(\"failed to create texture: %s\\n\", IMG_GetError() );\n        return;\n    }\n\n    SDL_FreeSurface( temp_surface );\n}\n\nvoid PlayerShip::Move() {\n    current_time = SDL_GetTicks();\n    diff_time = current_time - last_time;\n    delta_time = (double)diff_time / 1000.0;\n    last_time = current_time;\n\n    if( left_key_down ) {\n        RotateLeft();\n    }\n\n    if( right_key_down ) {\n        RotateRight();\n    }\n\n    if( up_key_down ) {\n        Accelerate();\n    }\n\n    if( down_key_down ) {\n        Decelerate();\n    }\n\n    CapVelocity();\n    m_X += m_VX;\n\n    if( m_X > 320 ) {\n        m_X = -16;\n    }\n    else if( m_X < -16 ) {\n        m_X = 320;\n    }\n\n    m_Y += m_VY;\n\n    if( m_Y > 200 ) {\n        m_Y = -16;\n    }\n    else if( m_Y < -16 ) {\n        m_Y = 200;\n    }\n\n    if( space_key_down ) {\n        Projectile* projectile;\n\n        if( current_time - m_LastLaunchTime >= c_MinLaunchTime ) {\n            m_LastLaunchTime = current_time;\n            projectile = projectile_pool->GetFreeProjectile();\n            if( projectile != NULL ) {\n                projectile->Launch( m_X, m_Y, m_DX, m_DY );\n            }\n        }\n    }\n}\n```\n\n接下来，让我们修改`ProjectilePool`类中的`Move`函数，这样每次它移动`Projectile`时，它也会测试它是否击中了我们的一艘船:\n\n```cpp\nvoid ProjectilePool::MoveProjectiles() {\n    Projectile* projectile;\n    std::vector<Projectile*>::iterator it;\n    for( it = m_ProjectileList.begin(); it != m_ProjectileList.end(); \n        it++ ) {\n        projectile = *it;\n        if( projectile->m_Active ) {\n            projectile->Move();\n            if( projectile->HitTest( player ) ) {\n                printf(\"hit player\\n\");\n            }\n            if( projectile->HitTest( enemy ) ) {\n                printf(\"hit enemy\\n\");\n            }\n        }\n    }\n}\n```\n\n目前，我们只打算在玩家或敌人与投射物相撞时打印到控制台上。这将告诉我们碰撞检测是否正常工作。在后面的部分中，我们将添加动画来摧毁我们的船只，当它们与抛射体碰撞时。\n\n我们需要对`Projectile`类的`Launch`功能做最后一个更改。当我们从我们的船上发射一枚炮弹时，我们给炮弹一个 x 和一个 y 位置以及一个 *x* 和 *y* 速度，这是基于船面对的方向。我们需要朝那个方向移动射弹的起点。也就是通过将射弹移出船只的碰撞检测圈来防止射弹击中发射它的船只:\n\n```cpp\nvoid Projectile::Launch(double x, double y, double dx, double dy) {\n    m_X = x + dx * 9;\n    m_Y = y + dy * 9;\n    m_VX = velocity * dx;\n    m_VY = velocity * dy;\n    m_TTL = alive_time;\n    m_Active = true;\n}\n```\n\n在下一节中，我们将检测我们的船何时与抛射体相撞，并运行爆炸动画。\n\n# 在碰撞中摧毁宇宙飞船\n\n现在我们正在探测射弹和宇宙飞船之间的碰撞，做一些比在控制台上打印一行更有趣的事情会很好。当我们的射弹和我们的飞船撞到什么东西时，有一个小小的爆炸动画会很好。当这些对象被销毁时，我们可以添加一个与它们相关的动画。\n\n我将介绍**精灵表**的概念，而不是像我们在前一章所做的那样，为动画的每一帧加载多个精灵。我们将为每艘宇宙飞船加载一个精灵表，而不是为每艘宇宙飞船加载一个单一的射弹框架和一个单一的飞船框架，这个精灵表不仅包括每艘飞船的未损坏版本，还包括一个销毁序列，当这些物体中的任何一个被销毁时，我们将制作这个序列的动画。\n\n在这个例子中有三个不同的精灵表只是为了方便。当你决定如何包装你的雪碧表生产，有几个考虑因素，你必须考虑。你很可能会想打破你的雪碧表根据什么时候你会需要它们。你可能有一系列你需要的精灵，这些精灵在游戏的所有级别都是通用的。你可以根据等级选择分解精灵。您还需要考虑到，出于性能原因，WebGL 需要 2 次方大小的精灵文件。这可能会影响你的决定，什么样的精灵包装到什么样的精灵表。你也可以考虑购买一个工具，比如纹理打包器，比手工打包更快。\n\n我们已经创建了三个精灵表来替换我们正在使用的三个精灵。这些`Sprites`分别是`FranchiseExp.png`代替`Franchise.png`、`BirdOfAngerExp.png`代替`BirdOfAnger.png`、`ProjectileExp.png`代替`Projectile.png`。我们需要对`Projectile`类、`Ship`类、`EnemyShip`类、`PlayerShip`类、`ProjectilePool`类以及`game_loop`功能进行一些调整。\n\n我们将从修改游戏循环开始，以跟踪游戏的计时数据。我们必须从`player_ship.cpp`文件内的`PlayerShip::Move`函数中删除一些代码。这段代码存在于[第 4 章](04.html)、*精灵动画与 SDL* 的 WebAssembly 中，我们讨论了通过动画`PlayerShip`来制作精灵动画的基础知识。我们必须从`PlayerShip::Move`的前几行中删除以下代码:\n\n```cpp\ncurrent_time = SDL_GetTicks();\ndiff_time = current_time - last_time;\ndelta_time = (double)diff_time / 1000.0;\nlast_time = current_time;\n```\n\n这段代码获取当前时间，并计算我们用于速度调整和动画计时的所有时间相关信息。我们可能应该在几章前将这段代码移到游戏循环中，但迟做总比不做好。以下是`main.cpp`中新增`game_loop`功能的代码:\n\n```cpp\nvoid game_loop() {\n    current_time = SDL_GetTicks();\n    diff_time = current_time - last_time;\n    delta_time = (double)diff_time / 1000.0;\n    last_time = current_time;\n    input();\n    move();\n    render();\n}\n```\n\n严格来说，我们没有必要进行这种更改，但是在游戏循环中使用游戏计时代码更有意义。现在我们已经改变了我们的游戏循环，我们将修改`Projectile`类。以下是我们必须在`game.hpp`文件中对类定义进行的更改:\n\n```cpp\nclass Projectile: public Collider {\n    public:\n        const char* c_SpriteFile = \"sprites/ProjectileExp.png\";\n        const int c_Width = 16;\n        const int c_Height = 16;\n        const double velocity = 6.0;\n        const double alive_time = 2000;\n        SDL_Texture *m_SpriteTexture;\n        SDL_Rect src = {.x = 0, .y = 0, .w = 16, .h = 16 };\n        Uint32 m_CurrentFrame = 0;\n        int m_NextFrameTime;\n        bool m_Active;\n\n        float m_TTL;\n        float m_VX;\n        float m_VY;\n\n        Projectile();\n        void Move();\n        void Render();\n        void Launch(float x, float y, float dx, float dy);\n};\n```\n\n我们需要修改`c_SpriteFile`变量指向新的精灵表 PNG 文件，而不是单个精灵文件。我们需要增加它的宽度和高度。为了给爆炸腾出空间，我们将把精灵表中的所有帧都做成 16 x 16，而不是 8 x 8。我们还需要一个源矩形。当每个精灵都使用了一个完整的文件时，我们可以将`null`传递给`SDL_RenderCopy`，该函数将呈现精灵文件的全部内容。现在我们只想渲染一帧，所以我们需要一个矩形，从 0，0 开始，渲染宽度和高度为 16。我们创建的精灵表是**水平条状精灵表**，意思是每一帧都按顺序排列，水平放置。要渲染动画的不同帧，我们只需要修改源矩形内的`.x`值。我们添加的最后一个属性是公共部分，是`m_CurrentFrame`属性。它跟踪我们当前正在播放的动画中的哪一帧。当我们不渲染爆炸动画时，我们将保持当前帧为 0。\n\n接下来，我们需要修改`Projectile`类上的几个函数。这些功能是`projectile.cpp`文件中的`Projectile::Move`功能和`Projectile::Render`功能。以下是新版本的`Projectile::Move`功能:\n\n```cpp\nvoid Projectile::Move() {\n    if( m_CurrentFrame > 0 ) {\n        m_NextFrameTime -= diff_time;\n        if( m_NextFrameTime <= 0 ) {\n            ++ m_CurrentFrame;\n            m_NextFrameTime = ms_per_frame;\n            if( m_CurrentFrame >= 4 ) {\n                m_Active = false;\n                m_CurrentFrame = 0;\n                return;\n            }\n        }\n        return;\n    }\n    m_X += m_VX;\n    m_Y += m_VY;\n    m_TTL -= diff_time;\n    if( m_TTL < 0 ) {\n        m_Active = false;\n        m_TTL = 0;\n    }\n}\n```\n\n`Move`功能的顶部是全新的。如果当前帧不是`0`，我们将运行动画直到它结束，然后停用我们的射弹，将其发送回射弹池。我们通过减去应用上次运行游戏循环以来的时间来实现这一点。这是存储在`diff_time`全局变量中的值。`m_NextFrameTime`属性变量存储我们切换到系列中的下一帧之前的毫秒数。一旦值低于 0，我们就增加当前帧，并将`m_NextFrameTime`重置为动画每一个新帧之间的毫秒数。现在我们已经增加了当前动画帧，我们可以检查它是否大于或等于该动画中最后一帧的帧数(在本例中为 4)。如果是这样的话，我们需要停止投射并将当前帧重置为 0。\n\n现在，我们已经对`Move()`功能进行了所需的更改，下面是我们必须对`Projectile::Render()`功能进行的更改:\n\n```cpp\nvoid Projectile::Render() {\n    dest.x = m_X + 8;\n    dest.y = m_Y + 8;\n    dest.w = c_Width;\n    dest.h = c_Height;\n    src.x = 16 * m_CurrentFrame;\n    int return_val = SDL_RenderCopy( renderer, m_SpriteTexture,\n                                    &src, &dest );\n    if( return_val != 0 ) {\n        printf(\"SDL_Init failed: %s\\n\", SDL_GetError());\n    }\n}\n```\n\n`Render`功能的第一个变化是将`src`矩形添加到`SDL_RenderCopy`调用中，并将其 *x* 值设置在该调用的正上方。我们的精灵表中的每一帧都是 16 像素宽，因此将 *x* 值设置为`16 * m_CurrentFrame`将从精灵表中选择不同的 16 x 16 精灵。该矩形的宽度和高度将始终为 16，并且 *y* 值将始终为 0，因为我们将子画面作为水平条放置在该子画面中。\n\n现在我们将对`game.hpp`文件中的`Ship`类定义进行一些修改:\n\n```cpp\nclass Ship: public Collider {\n    public:\n        Uint32 m_LastLaunchTime;\n        const int c_Width = 32;\n        const int c_Height = 32;\n\n        SDL_Texture *m_SpriteTexture;\n        SDL_Rect src = {.x = 0, .y = 0, .w = 32, .h = 32 };\n        bool m_Alive = true;\n        Uint32 m_CurrentFrame = 0;\n        int m_NextFrameTime;\n\n        float m_Rotation;\n        float m_DX;\n        float m_DY;\n        float m_VX;\n        float m_VY;\n\n        void RotateLeft();\n        void RotateRight();\n        void Accelerate();\n        void Decelerate();\n        void CapVelocity();\n\n        virtual void Move() = 0;\n        Ship();\n        void Render();\n};\n```\n\n我们修改了宽度和高度常数，以反映在我们的精灵表中出现的 32 x 32 像素的新精灵大小。我们还必须给`Projectile`类添加一个源矩形。在我们的公共属性部分，我们添加了一些变量来跟踪船只的生存或死亡状态，`(m_Alive)`；游戏正在渲染的当前帧，`(m_CurrentFrame)`；以及直到我们渲染下一帧的时间(毫秒)，`(m_NextFrameTime)`。接下来，我们将对`ship.cpp`文件进行必要的修改。我们需要修改`Ship::Render`功能:\n\n```cpp\nvoid Ship::Render() {\n    if( m_Alive == false ) {\n        return;\n    }\n    dest.x = (int)m_X;\n    dest.y = (int)m_Y;\n    dest.w = c_Width;\n    dest.h = c_Height;\n\n    src.x = 32 * m_CurrentFrame;\n    float degrees = (m_Rotation / PI) * 180.0;\n    int return_code = SDL_RenderCopyEx( renderer, m_SpriteTexture,\n                                    &src, &dest,\n                                    degrees, NULL, SDL_FLIP_NONE );\n    if( return_code != 0 ) {\n        printf(\"failed to render image: %s\\n\", IMG_GetError() );\n    }\n}\n```\n\n在函数的顶部，我们添加了代码来检查船当前是否还活着。如果不是，我们不想渲染船，所以我们返回。稍后，我们将源矩形 *x* 值设置为当前帧的 32 倍，线条为:`src.x = 32 * m_CurrentFrame;`。这改变了我们的渲染，根据我们想要渲染的帧，从我们的精灵表中渲染一个不同的 32 x 32 像素块。最后，我们必须将那个`src`矩形传递到对`SDL_RenderCopyEx`的调用中。\n\n现在我们已经修改了`Ship`类，我们将更改`EnemyShip`类定义和`PlayerShip`类定义，以使用我们的精灵表 PNG 文件，而不是旧的单一精灵文件。以下是对`game.hpp`文件中这两个类定义的修改:\n\n```cpp\nclass PlayerShip: public Ship {\n    public:\n        const char* c_SpriteFile = \"sprites/FranchiseExp.png\";\n        const Uint32 c_MinLaunchTime = 300;\n        PlayerShip();\n        void Move();\n};\n\nclass EnemyShip: public Ship {\n    public:\n        const char* c_SpriteFile = \"sprites/BirdOfAngerExp.png\";\n        const Uint32 c_MinLaunchTime = 300;\n        const int c_AIStateTime = 2000;\n\n        FSM_STUB m_AIState;\n        int m_AIStateTTL;\n\n        EnemyShip();\n        void AIStub();\n        void Move();\n};\n```\n\n对这些类定义所做的唯一更改是每个类中`c_SpriteFile`常量的值。`PlayerShip`类中的`c_SpriteFile`常量由`\"sprites/Franchise.png\"`修改为`\"sprites/FranchiseExp.png\"`，`EnemyShip`中的`c_SpriteFile`常量由`\"sprites/BirdOfAnger.png\"`修改为`\"sprites/BirdOfAngerExp.png\"`。现在我们已经做了那个改变，这些类将使用精灵表`.png`文件，而不是原始的精灵文件。\n\n现在我们已经修改了这些类的定义，我们必须为它们中的每一个改变`Move`函数。首先，我们将修改`enemy_ship.cpp`文件中的`EnemyShip::Move`功能:\n\n```cpp\nvoid EnemyShip::Move() {\n    if( m_Alive == false ) {\n        return;\n    }\n    AIStub();\n\n    if( m_AIState == TURN_LEFT ) {\n        RotateLeft();\n    }\n    if( m_AIState == TURN_RIGHT ) {\n        RotateRight();\n    }\n    if( m_AIState == ACCELERATE ) {\n        Accelerate();\n    }\n    if( m_AIState == DECELERATE ) {\n        Decelerate();\n    }\n\n    if( m_CurrentFrame > 0 ) {\n        m_NextFrameTime -= diff_time;\n\n        if( m_NextFrameTime <= 0 ) {\n            m_NextFrameTime = ms_per_frame;\n            if( ++ m_CurrentFrame >= 8 ) {\n                m_Alive = false;\n                return;\n            }\n        }\n    }\n    CapVelocity();\n\n    m_X += m_VX;\n\n    if( m_X > 320 ) {\n        m_X = -16;\n    }\n    else if( m_X < -16 ) {\n        m_X = 320;\n    }\n\n    m_Y += m_VY;\n\n    if( m_Y > 200 ) {\n        m_Y = -16;\n    }\n    else if( m_Y < -16 ) {\n        m_Y = 200;\n    }\n\n    if( m_AIState == SHOOT ) {\n        Projectile* projectile;\n        if( current_time - m_LastLaunchTime >= c_MinLaunchTime ) {\n            m_LastLaunchTime = current_time;\n            projectile = projectile_pool->GetFreeProjectile();\n\n            if( projectile != NULL ) {\n                projectile->Launch( m_X, m_Y, m_DX, m_DY );\n            }\n        }\n    }\n}\n```\n\n有两个地方必须更改代码。首先，如果敌方飞船不存在，我们不想做任何`Move`功能的工作，所以我们在该功能的开始添加了这个检查，以便在飞船不存在的情况下返回:\n\n```cpp\nif( m_Alive == false ) {\n    return;\n}\n```\n\n接下来，我们需要添加代码来检查是否需要运行死亡动画。如果当前帧大于 0，我们就这样做。本节中的代码类似于我们为弹丸运行死亡动画所做的。我们从下一帧时间`(m_NextFrameTime)`中减去帧之间的时间`(diff_time)`，以确定是否需要增加帧。当该值下降到 0 以下时，通过增加`m_CurrentFrame`帧准备改变，并且我们通过将`m_NextFrameTime`倒计时定时器设置为我们想要的每帧之间的毫秒数`(ms_per_frame)`来重置它。如果我们的当前帧击中了我们的帧精灵表的末尾，`(++ m_CurrentFrame >= 8)`，那么我们将敌人的船设置为不再活着，`(m_Alive = false)`。如下所示:\n\n```cpp\nif( m_CurrentFrame > 0 ) {\n    m_NextFrameTime -= diff_time;\n    if( m_NextFrameTime <= 0 ) {\n        m_NextFrameTime = ms_per_frame;\n        if( ++ m_CurrentFrame >= 8 ) {\n            m_Alive = false;\n            return;\n        }\n    }\n}\n```\n\n现在，我们将对`player_ship.cpp`文件中的`PlayerShip::Move`功能进行相同的更改:\n\n```cpp\nvoid PlayerShip::Move() {\n    if( m_Alive == false ) {\n        return;\n    }\n    if( left_key_down ) {\n        RotateLeft();\n    }\n    if( right_key_down ) {\n        RotateRight();\n    }\n    if( up_key_down ) {\n        Accelerate();\n    }\n    if( down_key_down ) {\n        Decelerate();\n    }\n    if( m_CurrentFrame > 0 ) {\n        m_NextFrameTime -= diff_time;\n        if( m_NextFrameTime <= 0 ) {\n            m_NextFrameTime = ms_per_frame;\n            if( ++ m_CurrentFrame >= 8 ) {\n                m_Alive = false;\n                return;\n            }\n        }\n    }\n    CapVelocity();\n    m_X += m_VX;\n\n    if( m_X > 320 ) {\n        m_X = -16;\n    }\n    else if( m_X < -16 ) {\n        m_X = 320;\n    }\n\n    m_Y += m_VY;\n\n    if( m_Y > 200 ) {\n        m_Y = -16;\n    }\n    else if( m_Y < -16 ) {\n        m_Y = 200;\n    }\n\n    if( space_key_down ) {\n        Projectile* projectile;\n        if( current_time - m_LastLaunchTime >= c_MinLaunchTime ) {\n            m_LastLaunchTime = current_time;\n            projectile = projectile_pool->GetFreeProjectile();\n            if( projectile != NULL ) {\n                projectile->Launch( m_X, m_Y, m_DX, m_DY );\n            }\n        }\n    }\n}\n```\n\n就像在我们的`EnemyShip::Move`函数中一样，我们用下面的代码添加一个检查来查看玩家是否还活着:\n\n```cpp\nif( m_Alive == false ) {\n    return;\n}\n```\n\n如果当前帧大于 0，我们还会添加一些代码来运行死亡动画:\n\n```cpp\nif( m_CurrentFrame > 0 ) {\n    m_NextFrameTime -= diff_time;\n    if( m_NextFrameTime <= 0 ) {\n        m_NextFrameTime = ms_per_frame;\n        if( ++ m_CurrentFrame >= 8 ) {\n            m_Alive = false;\n            return;\n        }\n    }\n}\n```\n\n我们需要做的最后一件事是修改我们之前添加到`ProjectilePool::MoveProjectiles`函数中的碰撞检测代码，以便在船只和抛射体碰撞时运行它们的死亡动画。这是新版本的`projectile_pool.cpp`文件里面的`ProjectilePool::MoveProjectiles`:\n\n```cpp\nvoid ProjectilePool::MoveProjectiles() {\n    Projectile* projectile;\n    std::vector<Projectile*>::iterator it;\n    for( it = m_ProjectileList.begin(); it != m_ProjectileList.end(); it++ ) {\n        projectile = *it;\n        if( projectile->m_Active ) {\n            projectile->Move();\n            if( projectile->m_CurrentFrame == 0 &&\n                player->m_CurrentFrame == 0 &&\n                projectile->HitTest( player ) ) {\n\n                player->m_CurrentFrame = 1;\n                player->m_NextFrameTime = ms_per_frame;\n                projectile->m_CurrentFrame = 1;\n                projectile->m_NextFrameTime = ms_per_frame;\n            }\n            if( projectile->m_CurrentFrame == 0 &&\n                enemy->m_CurrentFrame == 0 &&\n                projectile->HitTest( enemy ) ) {\n\n                enemy->m_CurrentFrame = 1;\n                enemy->m_NextFrameTime = ms_per_frame;\n                projectile->m_CurrentFrame = 1;\n                projectile->m_NextFrameTime = ms_per_frame;\n            }\n        }\n    }\n}\n```\n\n在这段代码中，每次我们移动一枚炮弹，我们都会对该炮弹和玩家进行一次命中测试，以及该炮弹和敌人之间的命中测试。如果飞船或抛射体正在运行其死亡动画(`m_CurrentFrame == 0`为假)，那么我们不需要运行命中测试，因为飞船或抛射体已经被摧毁。如果命中测试返回真，那么我们需要将射弹和飞船的当前帧都设置为 1 来开始破坏动画。我们还需要将下一帧时间设置为帧改变前的毫秒数。\n\n现在我们已经添加了所有这些新代码，飞船和敌方飞船将运行一个爆炸动画，在被击中时摧毁飞船。射弹也会爆炸，而不是消失。圆形对撞机速度很快，但不太精确。在*实现复合圆对撞机*部分，我们将学习在一艘船上使用多个圆对撞机需要做的修改。这将使我们的碰撞看起来比简单的圆更精确。\n\n# 内存中的指针\n\nWebAssembly 的内存模型搭载在 asm.js 内存模型上，该模型使用一个大型类型化的`ArrayBuffer`来保存模块要操作的所有原始字节。对`WebAssembly.Memory`的 JavaScript 调用将模块的内存缓冲区设置为 64 KB **页面**。\n\nA *page* is a block of linear data that is the smallest unit of data that can be allocated by an operating system, or, in the case of WebAssembly, a virtual machine. For more information on memory pages, see the Wikipedia Page: [https://en.wikipedia.org/wiki/Page_%28computer_memory%29](https://en.wikipedia.org/wiki/Page_%28computer_memory%29).\n\n网络组件模块只能从这个`ArrayBuffer`中访问数据。这可以防止来自 WebAssembly 的恶意攻击，这些攻击会创建指向浏览器沙箱之外的内存地址的指针。由于这种设计，WebAssembly 的内存模型和 JavaScript 一样安全。\n\n在下一节中，我们将在我们的`collider`对象中使用 C++ 指针。如果你是一个 JavaScript 开发人员，你可能不熟悉**指针**。指针是一个变量，它保存内存位置而不是直接保存值。让我们看一点代码:\n\n```cpp\nint VAR1 = 1;\nint* POINTER = &VAR1;\n```\n\n在这段代码中，我们创建了一个`VAR1`变量，并给了它一个值 1。在第二行，我们使用`int*`创建一个名为`POINTER`的指针。然后，我们使用`&`字符初始化指向`VAR1`地址的指针，在 C++ 中，这个字符被称为运算符的**地址。这个接线员给了我们之前申报的`VAR1`的地址。如果我们想改变`VAR1`，我们可以使用指针而不是直接改变，如下所示:**\n\n```cpp\n*POINTER = 2;\n printf(\"VAR1=%d\\n\", VAR1); // prints out \"VAR1=2\"\n```\n\n将`*`放在`POINTER`前面，告诉 C++ 在`POINTER`指向的内存地址中设置值；`*`以这种方式使用时称为**解引用运算符**。\n\nIf you would like to learn more about pointers in C++ and how they work, the following article goes into a good deal of detail on the subject: [http://www.cplusplus.com/doc/tutorial/pointers/](http://www.cplusplus.com/doc/tutorial/pointers/).\n\n在下一节中，我们将在我们的宇宙飞船上实现用于碰撞检测的复合圆碰撞器。\n\n# 实现复合圆碰撞器\n\n现在，我们的碰撞检测正在工作，并且我们的船只和射弹在碰撞中爆炸，让我们看看如何使我们的碰撞检测更好。我们选择圆碰撞检测有两个原因:碰撞算法快速，简单。然而，我们可以做得更好，只需在每艘船上增加更多的圆圈。这将使我们的碰撞检测时间增加一倍 *n* ，其中 *n* 是我们在每艘船上的平均圈数。这是因为我们所做的唯一碰撞检测是在射弹和船只之间。即便如此，我们也不想过分追求我们为每艘船选择的圈数。\n\n对于玩家船来说，飞船的正面被基本圈很好的覆盖。然而，我们可以通过在玩家飞船的每一侧增加一个圆圈来更好地覆盖飞船的背面:\n\n![](img/bb6abb5f-7d27-4e56-a72b-f41d7f92dde3.png)\n\n<sub>我们的玩家飞船复合对撞机</sub>\n\n敌舰则相反。飞船的背面被一个默认的圆圈覆盖得很好，但是前面可以使用一些更好的覆盖，所以，对于敌舰，我们将在前面添加一些额外的圆圈:\n\n![](img/45d21650-582a-4d43-9686-a132f6146e36.png)\n\n<sub>我方敌船复合对撞机</sub>\n\n我们需要做的第一件事是改变`Collider`类，以包含来自我们碰撞器的父级的信息。这是我们的`game.hpp`文件中的新版本的`Collider`类定义:\n\n```cpp\nclass Collider {\n    public:\n        float* m_ParentRotation;\n        float* m_ParentX;\n        float* m_ParentY;\n        float m_X;\n        float m_Y;\n        float m_Radius;\n\n        bool CCHitTest( Collider* collider );\n        void SetParentInformation( double* rotation, double* x, double* \n                                   y );\n        Collider(double radius);\n        bool HitTest( Collider *collider );\n};\n```\n\n我们已经为`Collider`类的父类的属性添加了三个指针。这些将指向 *x* 和 *y* 坐标，以及对撞机母体的`Rotation`，它将是敌方飞船、玩家飞船或`NULL`。我们将在构造函数中将这些值初始化为`NULL`，如果该值为空，我们将不会修改碰撞器的行为。但是，如果这些值被设置为其他值，我们将调用`CCHitTest`函数来确定是否存在冲突。这个版本的碰撞测试将在进行碰撞测试之前调整碰撞器的位置，使其相对于其父级的位置和旋转。现在我们已经更改了碰撞器的定义，我们将更改`collider.cpp`文件中的功能，以支持新的复合碰撞器。\n\n首先要做的是修改我们的构造函数来初始化指向`NULL`的新指针:\n\n```cpp\nCollider::Collider(double radius) {\n    m_ParentRotation = NULL;\n    m_ParentX = NULL;\n    m_ParentY = NULL;\n    m_Radius = radius;\n}\n```\n\n我们有一个新的函数要添加到我们的`collider.cpp`文件中，`CCHitTest`函数，这将是我们的复合对撞机命中测试。这个版本的撞击测试将调整我们对撞机的 *x* 和 *y* 坐标，使其相对于我们母船的位置和旋转:\n\n```cpp\nbool Collider::CCHitTest( Collider* collider ) {\n    float sine = sin(*m_ParentRotation);\n    float cosine = cos(*m_ParentRotation);\n    float rx = m_X * cosine - m_Y * sine;\n    float ry = m_X * sine + m_Y * cosine;\n    float dist_x = (*m_ParentX + rx) - collider->m_X;\n    float dist_y = (*m_ParentY + ry) - collider->m_Y;\n    float radius = m_Radius + collider->m_Radius;\n\n    if( dist_x * dist_x + dist_y * dist_y <= radius * radius ) {\n        return true;\n    }\n    return false;\n}\n```\n\n这个函数做的第一件事是获取父对象旋转的正弦和余弦，并使用该旋转来获得变量`rx`和`ry`中 *x* 和 *y* 的旋转版本。然后，在计算两个碰撞器 *x* 和 *y* 位置之间的距离之前，我们通过父母的 *x* 和 *y* 位置来调整旋转后的 *x* 和 *y* 位置。在我们添加了这个新的`CCHitTest`函数之后，如果父值被设置，我们需要修改`HitTest`函数来调用这个版本的命中测试。以下是最新版本的`HitTest`:\n\n```cpp\nbool Collider::HitTest( Collider *collider ) {\n    if( m_ParentRotation != NULL && m_ParentX != NULL && m_ParentY !=         NULL ) {\n        return CCHitTest( collider );\n    }\n\n    float dist_x = m_X - collider->m_X;\n    float dist_y = m_Y - collider->m_Y;\n    float radius = m_Radius + collider->m_Radius;\n\n    if( dist_x * dist_x + dist_y * dist_y <= radius * radius ) {\n        return true;\n    }\n    return false;\n}\n```\n\n我们创建了一个函数来设置所有这些值，称为`SetParentInformation`。下面是函数定义:\n\n```cpp\nvoid Collider::SetParentInformation( float* rotation, float* x, float* y ) {\n    m_ParentRotation = rotation;\n    m_ParentX = x;\n    m_ParentY = y;\n}\n```\n\n为了利用这些新型碰撞器，我们需要在`Ship`类中添加一个新的碰撞器向量。以下是`game.hpp`文件中`Ship`的新类定义:\n\n```cpp\nclass Ship : public Collider {\n    public:\n        Uint32 m_LastLaunchTime;\n        const int c_Width = 32;\n        const int c_Height = 32;\n\n        SDL_Texture *m_SpriteTexture;\n        SDL_Rect src = {.x = 0, .y = 0, .w = 32, .h = 32 };\n        std::vector<Collider*> m_Colliders;\n        bool m_Alive = true;\n        Uint32 m_CurrentFrame = 0;\n\n        int m_NextFrameTime;\n        float m_Rotation;\n        float m_DX;\n        float m_DY;\n        float m_VX;\n        float m_VY;\n\n        void RotateLeft();\n        void RotateRight();\n        void Accelerate();\n        void Decelerate();\n        void CapVelocity();\n        virtual void Move() = 0;\n        Ship();\n        void Render();\n        bool CompoundHitTest( Collider* collider );\n};\n```\n\n这个版本和之前版本的`Ship`类有两个区别。首先是`m_Colliders`向量属性的添加:\n\n```cpp\n std::vector<Collider*> m_Colliders;\n```\n\n第二个变化是在类的底部增加了新的`CompoundHitTest`函数:\n\n```cpp\nbool CompoundHitTest( Collider* collider );\n```\n\n对于我们类的更改，我们需要在我们的`ship.cpp`文件中添加一个新的函数:\n\n```cpp\nbool Ship::CompoundHitTest( Collider* collider ) {\n    Collider* col;\n    std::vector<Collider*>::iterator it;\n    for( it = m_Colliders.begin(); it != m_Colliders.end(); it++ ) {\n        col = *it;\n        if( col->HitTest(collider) ) {\n            return true;\n        }\n    }\n    return false;\n}\n```\n\n这个`CompoundHitTest`函数是一个非常简单的函数，它循环遍历我们所有的附加碰撞器，并对它们进行命中测试。这条线创建了一个碰撞指针向量。我们现在将修改我们的`EnemyShip`和`PlayerShip`构造器，在这个向量中添加一些碰撞器。首先，我们将向`enemy_ship.cpp`文件中的`EnemyShip`构造函数添加一些新行:\n\n```cpp\nEnemyShip::EnemyShip() {\n    m_X = 60.0;\n    m_Y = 50.0;\n    m_Rotation = PI;\n    m_DX = 0.0;\n    m_DY = 1.0;\n    m_VX = 0.0;\n    m_VY = 0.0;\n    m_AIStateTTL = c_AIStateTime;\n    m_Alive = true;\n    m_LastLaunchTime = current_time;\n\n    Collider* temp_collider = new Collider(2.0);\n    temp_collider->SetParentInformation( &(this->m_Rotation),\n                                         &(this->m_X), &(this->m_Y) );\n    temp_collider->m_X = -6.0;\n    temp_collider->m_Y = -6.0;\n    m_Colliders.push_back( temp_collider );\n    temp_collider = new Collider(2.0);\n    temp_collider->SetParentInformation( &(this->m_Rotation),\n                                         &(this->m_X), &(this->m_Y) );\n    temp_collider->m_X = 6.0;\n    temp_collider->m_Y = -6.0;\n    m_Colliders.push_back( temp_collider );\n\n    SDL_Surface *temp_surface = IMG_Load( c_SpriteFile );\n\n    if( !temp_surface ) {\n        printf(\"failed to load image: %s\\n\", IMG_GetError() );\n        return;\n    }\n    else {\n        printf(\"success creating enemy ship surface\\n\");\n    }\n    m_SpriteTexture = SDL_CreateTextureFromSurface( renderer, \n    temp_surface );\n    if( !m_SpriteTexture ) {\n        printf(\"failed to create texture: %s\\n\", IMG_GetError() );\n        return;\n    }\n    else {\n        printf(\"success creating enemy ship texture\\n\");\n    }\n    SDL_FreeSurface( temp_surface );\n}\n```\n\n我们添加的代码创建了新的碰撞器，并将这些碰撞器的父信息设置为指向 *x* 和 *y* 坐标的指针，以及指向该对象内部这些值的地址的半径。我们设置这个碰撞器相对于这个物体位置的`m_X`和`m_Y`值，然后我们将新的碰撞器推入`m_Colliders`矢量属性:\n\n```cpp\nCollider* temp_collider = new Collider(2.0);\ntemp_collider->SetParentInformation( &(this->m_Rotation),\n                                     &(this->m_X), &(this->m_Y) );\ntemp_collider->m_X = -6.0;\ntemp_collider->m_Y = -6.0;\nm_Colliders.push_back( temp_collider );\ntemp_collider = new Collider(2.0);\ntemp_collider->SetParentInformation( &(this->m_Rotation),\n                                     &(this->m_X), &(this->m_Y) );\ntemp_collider->m_X = 6.0;\ntemp_collider->m_Y = -6.0;\nm_Colliders.push_back( temp_collider );\n```\n\n我们现在将对`player_ship.cpp`文件中的`PlayerShip`构造函数进行类似的操作:\n\n```cpp\nPlayerShip::PlayerShip() {\n    m_X = 160.0;\n    m_Y = 100.0;\n    SDL_Surface *temp_surface = IMG_Load( c_SpriteFile );\n\n    Collider* temp_collider = new Collider(3.0);\n    temp_collider->SetParentInformation( &(this->m_Rotation),\n                                         &(this->m_X), &(this->m_Y) );\n    temp_collider->m_X = -6.0;\n    temp_collider->m_Y = 6.0;\n    m_Colliders.push_back( temp_collider );\n    temp_collider = new Collider(3.0);\n    temp_collider->SetParentInformation( &(this->m_Rotation),\n                                         &(this->m_X), &(this->m_Y) );\n    temp_collider->m_X = 6.0;\n    temp_collider->m_Y = 6.0;\n    m_Colliders.push_back( temp_collider );\n\n    if( !temp_surface ) {\n        printf(\"failed to load image: %s\\n\", IMG_GetError() );\n        return;\n    }\n    m_SpriteTexture = SDL_CreateTextureFromSurface( renderer, \n    temp_surface );\n\n    if( !m_SpriteTexture ) {\n        printf(\"failed to create texture: %s\\n\", IMG_GetError() );\n        return;\n    }\n    SDL_FreeSurface( temp_surface );\n}\n```\n\n现在，我们必须改变我们的射弹池，在我们的船上运行这些新的复合对撞机的碰撞检测。以下是`projectile_pool.cpp`文件中`MoveProjectiles`功能的修改版本:\n\n```cpp\nvoid ProjectilePool::MoveProjectiles() {\n    Projectile* projectile;\n    std::vector<Projectile*>::iterator it;\n\n    for( it = m_ProjectileList.begin(); it != m_ProjectileList.end(); \n         it++ ) {\n        projectile = *it;\n        if( projectile->m_Active ) {\n            projectile->Move();\n            if( projectile->m_CurrentFrame == 0 &&\n                player->m_CurrentFrame == 0 &&\n                ( projectile->HitTest( player ) ||\n                  player->CompoundHitTest( projectile ) ) ) {\n                player->m_CurrentFrame = 1;\n                player->m_NextFrameTime = ms_per_frame;\n                projectile->m_CurrentFrame = 1;\n                projectile->m_NextFrameTime = ms_per_frame;\n            }\n            if( projectile->m_CurrentFrame == 0 &&\n                enemy->m_CurrentFrame == 0 &&\n                ( projectile->HitTest( enemy ) ||\n                  enemy->CompoundHitTest( projectile ) ) ) {\n                enemy->m_CurrentFrame = 1;\n                enemy->m_NextFrameTime = ms_per_frame;\n                projectile->m_CurrentFrame = 1;\n                projectile->m_NextFrameTime = ms_per_frame;\n            }\n        }\n    }\n}\n```\n\n因为我们在`Ship`类中继续继承`Collider`，所以我们仍然会对玩家和敌舰进行常规命中测试。我们在`Ship`类中添加了一个对`CompoundHitTest`的调用，它循环遍历我们的`m_Colliders`向量，并对该向量中的每个碰撞器执行碰撞命中测试。\n\nOur compound collider solution is not generalized, and, for the most part, neither is our collision detection. We are only detecting collisions between our ships and our projectiles. We are not currently performing any collision detection between our ships. To have a generalized approach to collision detection, we would need to implement spacial segmenting. That would prevent the number of collision checks from growing exponentially with each additional collider added to our game.\n\n# 编译 collider.html\n\n我们用来编译`collider.html`文件的命令类似于上一章的编译命令。我们需要在命令行中添加一个新的`collider.cpp`文件，但除此之外，它应该是相同的。下面是你用来编译`collider.html`的命令:\n\n```cpp\nem++ main.cpp collider.cpp ship.cpp enemy_ship.cpp player_ship.cpp projectile.cpp projectile_pool.cpp -std=c++ 17 --preload-file sprites -s USE_WEBGL2=1 -s USE_SDL=2 -s USE_SDL_IMAGE=2 -s SDL2_IMAGE_FORMATS=[\"png\"] -o collider.html\n```\n\n现在我们已经编译了`collider.html`，我们可以从我们选择的网络服务器上提供它，或者用`emrun`运行它，并将其加载到网络浏览器中。这是它的样子:\n\n![](img/72206918-5f5b-4dac-8585-fb42d096df04.png)\n\n<sub>The enemy spaceship explodes when hit by a projectile</sub> Please remember that you must run WebAssembly apps using a web server, or with `emrun`.  If you would like to run your WebAssembly app using `emrun`, you must compile it with the `--emrun` flag.  The web browser requires a web server to stream the WebAssembly module.  If you attempt to open an HTML page that uses WebAssembly in a browser directly from your hard drive, that WebAssembly module will not load.\n\n我没有像之前的游戏截图那样拍摄整个浏览器的截图，因为我想放大玩家船摧毁敌人船的画面。正如你所看到的，我们现在有了对撞机，它可以探测到抛射体何时与宇宙飞船相撞，并可以通过运行爆炸动画在碰撞发生时摧毁宇宙飞船。\n\n# 摘要\n\n圆对撞机是我们现在需要的。它们速度快，效率高，对于这样一个简单的游戏，你也许可以不做任何复杂的事情。我们添加了一个复合碰撞器来演示这个简单的修改如何显著提高碰撞器的精度。我们将需要在本书后面添加更多的碰撞检测方法。未来，我们将在游戏中添加小行星和一颗恒星，我们将创建一个 **AI** ( **人工智能**)代理来导航我们的游戏并攻击我们的玩家。这个代理最终将需要知道它是否与玩家有视线，这样线碰撞检测将变得更加重要。我们的特工还想快速扫描它附近的区域，看看是否有它必须避开的小行星。对于这个特性，我们将使用矩形碰撞。\n\n2D 奥运会的碰撞检测技术有很多种，我们在这一章只触及了表面。我们学习了如何实现一些基本的圆形对撞机和复合对撞机，我们还添加了检测我们游戏中的射弹与玩家和敌方宇宙飞船之间碰撞的代码。这类对撞机速度快，相对容易实现，但也不是没有缺点。\n\n你可能会注意到像我们已经实现的简单碰撞器的一个缺点是，如果两个物体以足够高的相对速度相互通过，它们可能会相互通过而不会碰撞。这是因为我们的物体每一帧都有一个新的计算位置，它们不会从 A 点到 B 点连续移动，如果从 A 点到 B 点移动一帧，物体就有效地在两点之间进行了瞬间移动。如果在这两个点之间有第二个物体，但是我们在 A 点或 B 点都没有和那个物体碰撞，那么物体碰撞就错过了。这在我们的游戏中应该不成问题，因为我们将保持我们的最大物体速度相对较低。然而，在编写游戏时，这是需要记住的。\n\n在下一章中，我们将构建一个工具来帮助我们配置**粒子系统**。"
  },
  {
    "path": "docs/handson-game-dev-wasm/08.md",
    "content": "# 八、基本粒子系统\n\n粒子系统 *m* 是一种图形技术，我们从*发射器*发射大量精灵，并让这些精灵经历一个生命周期，在这个周期中它们以各种方式变化。我们在雪碧的生命周期中构建了一些随机性，以创建各种有趣的效果，如爆炸、火花、雪、灰尘、火灾、发动机排气等。一些粒子效应可以与它们的环境相互作用。在我们的游戏中，我们将使用粒子效果来创建好看的发动机排气和船只爆炸效果。\n\nFor this chapter, you will need to include several images in your build to make this project work. Make sure you include the `/Chapter08/sprites/` folder from the project's GitHub. If you haven't yet downloaded the GitHub project, you can get it online here: [https://github.com/PacktPublishing/Hands-On-Game-Develop](https://github.com/PacktPublishing/Hands-On-Game-Development-with-WebAssembly).\n\n这一章和下一章的开头一开始会让人觉得有点跑题。在接下来的两章中，我们会花很多时间在游戏以外的事情上。如果你对粒子系统感兴趣，我保证这是值得的。当你创建一个粒子系统时，你会花很多时间来调整它们，并玩它们来让它们看起来正确。在游戏中直接这样做将会导致大量的编译和测试。我们需要的是一个工具，我们可以在将粒子系统添加到游戏之前对其进行配置和测试。本章和下一章的一半致力于构建这个工具。如果你对学习如何构建这个工具不感兴趣，可以浏览一下本章的文字，从 GitHub 下载并编译这个工具。如果你对学习 JavaScript、HTML 和 WebAssembly 如何在应用中交互感兴趣，本章和第 9 章*改进粒子系统*的前半部分是学习如何编写应用的好教程，而不仅仅是一个使用 WebAssembly 的游戏。\n\n在本章中，我们将涵盖以下主题:\n\n*   SVG 简介\n*   又是三角学？\n*   添加 JavaScript\n*   简单的粒子发射器工具\n*   点类\n*   粒子类\n*   发射器类\n*   WebAssembly 接口函数\n*   编译和测试粒子发射器\n\n# 添加到虚拟文件系统\n\n这一部分将从*粒子系统*中简短地脱离出来，因为我想花时间创建一个*粒子系统设计工具*，它将要求我们向网络组件虚拟文件系统添加文件。我们将添加一个带有文件类型的输入元素，我们可以用它来将图像加载到虚拟文件系统中。我们将需要检查我们正在加载的文件，以验证它是一个`.png`文件，如果是，我们将使用 WebAssembly 和 SDL 在画布上绘制和移动图像。\n\nEmscripten does not create a virtual file system by default. Because we will need to use a virtual file system that will not initially have anything inside of it, we will need to pass the following flag to em++ to force Emscripten to build a virtual filesystem: `-s FORCE_FILESYSTEM=1`.\n\n我们首先要做的是从[第二章](02.html)、 *HTML5 和 WebAssembly* 中复制`canvas_shell.html`，用它创建一个新的 shell 文件，我们称之为`upload_shell.html`。我们需要在 JavaScript 中添加一些代码来处理文件加载，并将该文件插入到 WebAssembly 虚拟文件系统中。我们还需要添加一个`file`类型的 HTML `input`元素，直到`Module`对象加载完毕才会显示。在下面的代码中，我们有了新的 shell 文件:\n\n```cpp\n<!doctype html><html lang=\"en-us\">\n<head><meta charset=\"utf-8\"><meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">\n    <title>Upload Shell</title>\n    <link href=\"upload.css\" rel=\"stylesheet\" type=\"text/css\">\n</head>\n<body>\n    <canvas id=\"canvas\" width=\"800\" height=\"600\" \n     oncontextmenu=\"event.preventDefault()\"></canvas>\n    <textarea class=\"em_textarea\" id=\"output\" rows=\"8\"></textarea>\n    <script type='text/javascript'>\n        var canvas = null;\n        var ctx = null;\n        function ShowFileInput()         \n            {document.getElementById(\"file_input_label\")\n            .style.display=\"block\";}\n        var Module = {\n            preRun: [],\n            postRun: [ShowFileInput],\n            print: (function() {\n                var element = document.getElementById('output');\n                if (element) element.value = '';\n                return function(text) {\n                    if (arguments.length > 1)         \n                    text=Array.prototype.slice.call(arguments).join('                     \n                    ');\n                    console.log(text);\n                    if (element) {\n                        element.value += text + \"\\n\";\n                        element.scrollTop = element.scrollHeight;\n                } }; })(),\n    printErr: function(text) {\n        if (arguments.length > 1) \n        text=Array.prototype.slice.call(arguments).join(' ');\n        if (0) { dump(text + '\\n'); } \n        else { console.error(text); } },\n    canvas: (function() {\n        var canvas = document.getElementById('canvas');\n        canvas.addEventListener(\"webglcontextlost\", function(e) { \n        alert('WebGL context lost. You will need to reload the page.'); \n        e.preventDefault(); }, false);\n        return canvas; })(),\n    setStatus: function(text) {\n        if (!Module.setStatus.last) Module.setStatus.last = { time: \n            Date.now(), text: '' };\n        if (text === Module.setStatus.last.text) return;\n        var m = text.match(/([^(]+)\\((\\d+(\\.\\d+)?)\\/(\\d+)\\)/);\n        var now = Date.now();\n        if (m && now - Module.setStatus.last.time < 30) return;\n        Module.setStatus.last.time = now;\n        Module.setStatus.last.text = text;\n        if (m) { text = m[1]; }\n        console.log(\"status: \" + text);\n    },\n    totalDependencies: 0,\n    monitorRunDependencies: function(left) {\n        this.totalDependencies = Math.max(this.totalDependencies,left);\n        Module.setStatus(left ? 'Preparing... (' + \n        (this.totalDependencies-left) + '/' +\n         this.totalDependencies + ')' : 'All downloads complete.'); }\n};\nModule.setStatus('Downloading...');\nwindow.onerror = function() {\n    Module.setStatus('Exception thrown, see JavaScript console');\n    Module.setStatus = function(text) { if (text) Module.printErr('[post-exception status] ' + text); };\n};\nfunction handleFiles(files) {\n    var file_count = 0;\n    for (var i = 0; i < files.length; i++) {\n        if (files[i].type.match(/image.png/)) {\n            var file = files[i];\n            console.log(\"file name=\" + file.name);\n            var file_name = file.name;\n            var fr = new FileReader();\n            fr.onload = function (file) {\n                var data = new Uint8Array(fr.result);\n                Module.FS_createDataFile('/', file_name, data, true, \n                true, true);\n                Module.ccall('add_image', 'undefined', [\"string\"], \n                [file_name]);\n            };\n            fr.readAsArrayBuffer(files[i]);\n        }\n    }\n}\n</script>\n<input type=\"file\" id=\"file_input\" onchange=\"handleFiles(this.files)\" />\n<label for=\"file_input\" id=\"file_input_label\">Upload .png</label>\n{{{ SCRIPT }}}\n</body></html>\n```\n\n在标题中，我们只对标题和样式表进行了更改:\n\n```cpp\n<title>Upload Shell</title>\n<link href=\"upload.css\" rel=\"stylesheet\" type=\"text/css\">\n```\n\n在`body`标签中，我们只留下了`canvas`和`textarea`元素，但是 JavaScript 有很大的变化。我们将对 JavaScript 做的第一件事是添加一个`ShowFileInput`函数来显示`file_input_label`元素，它从我们的 CSS 隐藏开始。您可以在下面的代码片段中看到它:\n\n```cpp\nfunction ShowFileInput() {\n    document.getElementById(\"file_input_label\").style.display = \"block\";\n}\n\nvar Module = {\n    preRun: [],\n    postRun: [ShowFileInput],\n```\n\n请注意，我们在`postRun`数组中添加了对该函数的调用，以便它在模块加载后运行。这是为了确保在`Module`对象加载之前没有人加载图像文件，并且我们的页面可以处理它。除了在`postRun`阵中增加`ShowFileInput`之外，`Module`的目标没有改变。在我们的`Module`目标代码之后，我们添加了一个`handleFiles`函数，当用户选择一个新文件进行加载时，我们的文件输入元素会调用这个函数。下面是该函数的代码:\n\n```cpp\nfunction handleFiles(files) {\n    var file_count = 0;\n    for (var i = 0; i < files.length; i++) {\n        if (files[i].type.match(/image.png/)) {\n            var file = files[i];\n            var file_name = file.name;\n            var fr = new FileReader();\n\n            fr.onload = function (file) {\n                var data = new Uint8Array(fr.result);\n                Module.FS_createDataFile('/', file_name, data, true, \n                true, true);\n                Module.ccall('add_image', 'undefined', [\"string\"], \n                [file_name]);\n            };\n            fr.readAsArrayBuffer(files[i]);\n        }\n    }\n}\n```\n\n您会注意到，该函数旨在通过循环传递到`handleFiles`的`files`参数来同时处理多个文件。我们要做的第一件事是检查图像文件类型是否是 PNG。当我们编译网络程序集时，我们需要告诉它 SDL 将处理哪些图像文件类型。PNG 格式应该是您所需要的，但是在这里添加其他类型并不难。\n\n如果不想特别检查 PNG 文件，可以省略匹配字符串的`.png`部分，稍后在编译命令行参数中添加其他文件类型。如果文件是`img/png`类型，我们将文件名放入其变量`file_name`，并创建一个`FileReader`对象。然后我们定义`FileReader`加载文件时运行的函数:\n\n```cpp\nfr.onload = function (file) {\n    var data = new Uint8Array(fr.result);\n    Module.FS_createDataFile('/', file_name, data, true, true, true);\n    Module.ccall('add_image', 'undefined', [\"string\"], [file_name]);\n};\n```\n\n该函数将数据作为一个 8 位无符号整数数组，然后将其传递给`Module`函数`FS_createDataFile`。该函数将一个字符串作为其参数，该字符串是我们文件的父目录`'/'`、文件名`file_name`、我们从文件中读取的数据，后面是`canRead`、`canWrite`和`canOwn`，它们都应该设置为`true`，因为我们希望能够让我们的网络程序集读取、写入和拥有该文件。然后，我们使用`Module.ccall`调用我们的网络组件中定义的函数`add_image`，该函数将采用文件名，以便我们的网络组件可以使用 SDL 将该图像渲染到 HTML 画布上。当我们定义了告诉`FileReader`加载文件时该做什么的函数后，我们必须指示`FileReader`继续读取加载的文件作为`ArrayBuffer`:\n\n```cpp\nfr.readAsArrayBuffer(files[i]);\n```\n\n在 JavaScript 之后，我们添加了一个文件`input`元素和一个标签，如下所示:\n\n```cpp\n<input type=\"file\" id=\"file_input\" onchange=\"handleFiles(this.files)\" />\n<label for=\"file_input\" id=\"file_input_label\">Upload .png</label>\n```\n\n这个标签纯粹是为了造型。设定输入文件元素的样式在 CSS 中并不是一件简单的事情。我们稍后将讨论如何做到这一点。在讨论 CSS 之前，我想回顾一下我们将使用 SDL 加载和渲染这个图像的 WebAssembly C 代码。以下代码将进入我们命名为`upload.c`的文件:\n\n```cpp\n#include <emscripten.h>\n#include <stdlib.h>\n#include <SDL2/SDL.h>\n#include <SDL2/SDL_image.h>\n\nSDL_Window *window;\nSDL_Renderer *renderer;\nchar* fileName;\nSDL_Texture *sprite_texture = NULL;\nSDL_Rect dest = {.x = 160, .y = 100, .w = 16, .h = 16 };\n\nint sprite_x = 0;\nint sprite_y = 0;\n\nvoid add_image(char* file_name) {\n    SDL_Surface *temp_surface = IMG_Load( file_name );\n\n    if( !temp_surface ) {\n        printf(\"failed to load image: %s\\n\", IMG_GetError() );\n        return;\n    }\n    sprite_texture = SDL_CreateTextureFromSurface( renderer, \n    temp_surface );\n    SDL_FreeSurface( temp_surface );\n    SDL_QueryTexture( sprite_texture,\n                        NULL, NULL,\n                        &dest.w, &dest.h );\n}\n\nvoid show_animation() {\n    if( sprite_texture == NULL ) {\n        return;\n    }\n\n    SDL_SetRenderDrawColor( renderer, 0, 0, 0, 255 );\n    SDL_RenderClear( renderer );\n\n    sprite_x += 2;\n    sprite_y++ ;\n\n    if( sprite_x >= 800 ) {\n        sprite_x = -dest.w;\n    }\n\n    if( sprite_y >= 600 ) {\n        sprite_y = -dest.h;\n    }\n    dest.x = sprite_x;\n    dest.y = sprite_y;\n\n    SDL_RenderCopy( renderer, sprite_texture, NULL, &dest );\n    SDL_RenderPresent( renderer );\n}\n\nint main() {\n    printf(\"Enter Main\\n\");\n    SDL_Init( SDL_INIT_VIDEO );\n\n    int return_val = SDL_CreateWindowAndRenderer( 800, 600, 0, &window, \n    &renderer );\n\n    if( return_val != 0 ) {\n        printf(\"Error creating renderer %d: %s\\n\", return_val, \n        IMG_GetError() );\n         return 0;\n    }\n    emscripten_set_main_loop(show_animation, 0, 0);\n    printf(\"Exit Main\\n\");\n    return 1;\n}\n```\n\n我们在新的`upload.c`文件中定义了三个函数。第一个功能是`add_image`功能。这个函数接收一个`char*`字符串，该字符串代表我们刚刚加载到网络组件虚拟文件系统中的文件。我们使用 SDL 加载图像到一个表面，然后我们使用该表面创建一个纹理，我们将使用它来渲染我们加载的图像。第二个功能是`show_animation`，我们用它在画布上移动图像。第三个是`main`函数，它总是在模块加载时运行，所以我们用它来初始化我们的 SDL。\n\n让我们快速了解一下`add_image`功能:\n\n```cpp\nvoid add_image(char* file_name) {\n    SDL_Surface *temp_surface = IMG_Load( file_name );\n\n    if( !temp_surface ) {\n        printf(\"failed to load image: %s\\n\", IMG_GetError() );\n        return;\n    }\n    sprite_texture = SDL_CreateTextureFromSurface( renderer, \n    temp_surface );\n    SDL_FreeSurface( temp_surface );\n    SDL_QueryTexture( sprite_texture,\n                        NULL, NULL,\n                        &dest.w, &dest.h );\n}\n```\n\n我们在`add_image`函数中做的第一件事是使用我们传入的`file_name`参数，使用属于`SDL_image`库的`IMG_Load`函数将图像加载到`SDL_Surface`对象指针中:\n\n```cpp\nSDL_Surface *temp_surface = IMG_Load( file_name );\n```\n\n如果加载失败，我们将打印一条错误消息，并从函数返回:\n\n```cpp\nif( !temp_surface ) {\n    printf(\"failed to load image: %s\\n\", IMG_GetError() );\n    return;\n}\n```\n\n如果它没有失败，我们使用表面创建一个纹理，我们将能够在帧动画中渲染。然后，我们释放表面，因为我们不再需要它:\n\n```cpp\nsprite_texture = SDL_CreateTextureFromSurface( renderer, temp_surface );\nSDL_FreeSurface( temp_surface );\n```\n\n我们要做的最后一件事是使用`SDL_QueryTexture`函数获取图像的宽度和高度，并将这些值加载到`dest`矩形中:\n\n```cpp\nSDL_QueryTexture( sprite_texture,\n                  NULL, NULL,\n                  &dest.w, &dest.h );\n```\n\n`show_animation`功能类似于我们过去写过的其他游戏循环。它应该运行每一帧，只要加载了精灵纹理，它就应该清除画布，增加精灵的`x`和`y`值，然后将精灵渲染到画布上:\n\n```cpp\nvoid show_animation() {\n    if( sprite_texture == NULL ) {\n        return;\n    }\n\n    SDL_SetRenderDrawColor( renderer, 0, 0, 0, 255 );\n    SDL_RenderClear( renderer );\n    sprite_x += 2;\n    sprite_y++ ;\n\n    if( sprite_x >= 800 ) {\n        sprite_x = -dest.w;\n    }\n    if( sprite_y >= 600 ) {\n        sprite_y = -dest.h;\n    }\n\n    dest.x = sprite_x;\n    dest.y = sprite_y;\n    SDL_RenderCopy( renderer, sprite_texture, NULL, &dest );\n    SDL_RenderPresent( renderer );\n}\n```\n\n我们在`show_animation`做的第一件事就是检查`sprite_texture`是否还是`NULL`。如果是，用户还没有加载一个 PNG 文件，所以我们不能渲染任何东西:\n\n```cpp\nif( sprite_texture == NULL ) {\n    return;\n}\n```\n\n我们接下来要做的是用黑色清除画布:\n\n```cpp\nSDL_SetRenderDrawColor( renderer, 0, 0, 0, 255 );\nSDL_RenderClear( renderer );\n```\n\n然后，我们将增加精灵的`x`和`y`坐标，并使用这些值来设置`dest`(目的地)矩形:\n\n```cpp\nsprite_x += 2;\nsprite_y++ ;\nif( sprite_x >= 800 ) {\n    sprite_x = -dest.w;\n}\nif( sprite_y >= 600 ) {\n    sprite_y = -dest.h;\n}\ndest.x = sprite_x;\ndest.y = sprite_y;\n```\n\n最后，我们将精灵渲染到后缓冲区，然后将后缓冲区移动到画布:\n\n```cpp\nSDL_RenderCopy( renderer, sprite_texture, NULL, &dest );\nSDL_RenderPresent( renderer );\n```\n\n`upload.c`中的最后一个函数是`main`函数，该函数在模块加载时被调用。此函数用于初始化目的，如下所示:\n\n```cpp\nint main() {\n    printf(\"Enter Main\\n\");\n    SDL_Init( SDL_INIT_VIDEO );\n    int return_val = SDL_CreateWindowAndRenderer( 800, 600, 0, &window, \n    &renderer );\n\n    if( return_val != 0 ) {\n        printf(\"Error creating renderer %d: %s\\n\", return_val, \n        IMG_GetError() );\n        return 0;\n    }\n\n    emscripten_set_main_loop(show_animation, 0, 0);\n    printf(\"Exit Main\\n\");\n    return 1;\n}\n```\n\n它调用一些 SDL 函数来初始化我们的 SDL 渲染器:\n\n```cpp\nSDL_Init( SDL_INIT_VIDEO );\nint return_val = SDL_CreateWindowAndRenderer( 800, 600, 0, &window, &renderer );\n\nif( return_val != 0 ) {\n    printf(\"Error creating renderer %d: %s\\n\", return_val, \n    IMG_GetError() );\n    return 0;\n}\n```\n\n然后，它设置`show_animation`函数在我们每次渲染一帧时运行:\n\n```cpp\nemscripten_set_main_loop(show_animation, 0, 0);\n```\n\n我们要做的最后一件事是设置一个 CSS 文件，以便在我们的 shell 文件中正确显示 HTML。以下是新`upload.css`文件的内容:\n\n```cpp\nbody {\n    margin-top: 20px;\n}\n#output {\n    background-color: darkslategray;\n    color: white;\n    font-size: 16px;\n    padding: 10px;\n    margin-left: auto;\n    margin-right: auto;\n    display: block;\n    width: 780px;\n}\n#canvas {\n    width: 800px;\n    height: 600px;\n    margin-left: auto;\n    margin-right: auto;\n    display: block;\n    background-color: black;\n    margin-bottom: 20px;\n}\n[type=\"file\"] {\n    height: 0;\n    overflow: hidden;\n    width: 0;\n    display: none;\n}\n\n[type=\"file\"] + label {\n    background: orangered;\n    border-radius: 5px;\n    color: white;\n    display: none;\n    font-size: 20px;\n    font-family: Verdana, Geneva, Tahoma, sans-serif;\n    text-align: center;\n    margin-top: 10px;\n    margin-bottom: 10px;\n    margin-left: auto;\n    margin-right: auto;\n    width: 130px;\n    padding: 10px 50px;\n    transition: all 0.2s;\n    vertical-align: middle;\n}\n[type=\"file\"] + label:hover {\n    background-color: orange;\n}\n```\n\n前几个类`body`、`#output`和`#canvas`与我们在以前的 CSS 文件中拥有的那些类的版本没有太大的不同，所以我们不需要深入这些类的任何细节。在这些类之后是一个看起来有些不同的 CSS 类:\n\n```cpp\n[type=\"file\"] {\n height: 0;\n overflow: hidden;\n width: 0;\n display: none;\n }\n```\n\n它定义了一个类型为`file`的`input`元素的外观。出于某种原因，使用 CSS 来设置文件输入元素的样式不是很简单。我们将隐藏带有`display: none;`属性的元素，然后创建一个样式化的标签，而不是直接设置元素的样式，如下所示:\n\n```cpp\n[type=\"file\"] + label {\n    background: orangered;\n    border-radius: 5px;\n    color: white;\n    display: none;\n    font-size: 20px;\n    font-family: Verdana, Geneva, Tahoma, sans-serif;\n    text-align: center;\n    margin-top: 10px;\n    margin-bottom: 10px;\n    margin-left: auto;\n    margin-right: auto;\n    width: 130px;\n    padding: 10px 50px;\n    transition: all 0.2s;\n    vertical-align: middle;\n}\n[type=\"file\"] + label:hover {\n    background-color: orange;\n}\n```\n\n这就是为什么，在 HTML 中，我们在输入文件元素之后有一个标签元素。您可能会注意到，我们的标签也将`display`设置为`none`。也就是说，在加载`Module`对象之前，用户不能使用元素上传 PNG 文件。如果您回顾一下我们的 HTML shell 文件中的 JavaScript，我们在`postRun`上调用了以下代码，以便在加载我们的`Module`后标签变得可见:\n\n```cpp\nfunction ShowFileInput() {\n    document.getElementById(\"file_input_label\").style.display = \n    \"block\";\n}\n```\n\n现在，我们应该有一个可以将图像加载到 WebAssembly 虚拟文件系统中的应用。在接下来的几个部分中，我们将扩展这个应用来配置和测试一个简单的粒子发射器。\n\n# SVG 简介\n\nSVG 代表*可缩放矢量图形*，是发生在 HTML 画布中的即时模式光栅图形渲染的替代方案。SVG 是一种基于 XML 的图形渲染语言，对于熟悉 HTML 的人来说，至少应该看起来有些熟悉。一个 SVG 标签可以放在 HTML 的正内部，并像任何其他 DOM 节点一样被访问。因为我们正在编写一个配置粒子发射器数据的工具，我们将把 SVG 添加到我们的应用中，用于数据可视化目的。\n\n# 矢量图形与光栅图形\n\n作为游戏开发者，你可能不太熟悉*矢量图形*。当我们渲染计算机图形时，无论我们使用什么格式，在游戏将它们显示在计算机屏幕上之前，它们都需要被*光栅化为像素网格。使用光栅图形就是在像素级别上使用我们的图像。另一方面，矢量图形涉及到在不同的抽象层次上处理图形，这里我们使用的是线、点和曲线。最后，基于矢量的图形引擎仍然必须弄清楚它所处理的线、点和曲线是如何转换成像素的，但是使用矢量图形并不是没有好处。它们如下:*\n\n*   矢量图形可以干净地缩放\n*   矢量图形支持较小的下载量\n*   矢量图形很容易在运行时修改\n\n在网络上使用矢量图形的最佳地点之一是*数据可视化*。这本书不是关于 SVG 或数据可视化的，SVG 目前还不够快，不能用于大多数应用的游戏渲染。然而，当您想要在网站上呈现图形辅助以配合数据时，它是一个有用的工具。我们将在我们的粒子发射器配置工具中添加一点 SVG 作为视觉辅助，以帮助用户看到发射器被配置为发射粒子的方向。因为我们使用这个作为视觉辅助，所以严格来说没有必要把它放在我们的应用中。\n\n我们要做的第一件事是给我们的 HTML 添加一些标签。我们需要一个 SVG 标签来设置一个区域，我们可以用它来绘制我们的矢量圆图形。我们还需要几个输入值，允许我们输入两个角度的度数值。这两个输入场将采用最小和最大角度来发射粒子。当我们进行这项工作时，它会给我们的粒子发射提供一些方向。下面是我们需要添加到`body`标签中的 HTML 代码:\n\n```cpp\n<svg id=\"pie\" width=\"200\" height=\"200\" viewBox=\"-1 -1 2 2\"></svg>\n <br/>\n <div style=\"margin-left: auto; margin-right: auto\">\n <span class=\"label\">min angle:</span>\n <input type=\"number\" id=\"min_angle\" max=\"359\" min=\"-90\" step=\"1\" \n  value=\"-20\" class=\"em_input\"><br/>\n <span class=\"label\">max angle:</span>\n <input type=\"number\" id=\"max_angle\" max=\"360\" min=\"0\" step=\"1\" \n  value=\"20\" class=\"em_input\"><br/>\n </div>\n```\n\n我们已经在`svg`标签中将`id`设置为饼图。这将允许我们稍后用直线和圆弧修改这个标签内部的值。我们给了它`200`像素的高度和宽度。\n\n`viewbox`设置为`-1 -1 2 2`。这表示我们的 SVG 绘图区域的左上角坐标设置为坐标`-1, -1`。后两个数字`2 2`是 SVG 绘图区域绘图空间中的宽度和高度。这意味着我们的绘图空间将从左上角的坐标`-1, -1`到右下角的`1, 1`。当我们需要计算角度时，这将使处理正弦和余弦值变得容易。\n\n# 又是三角学？\n\nOMG 是的，还有更多*三角*。我已经在，[第七章](07.html)、*碰撞检测*中介绍过基本的三角学了，但信不信由你，三角学在游戏开发中确实有用。三角学碰巧对粒子系统非常有用，我们将使用 SVG 和一些 trig 来构建一个小饼图，我们可以用它来可视化我们的粒子发射器的方向。所以，让我们花一点时间再快速回顾一遍:\n\n*   *正弦=相反/斜边(SOH)*\n*   *余弦=相邻/斜边(CAH)*\n*   *正切=相反/相邻(TOA)*\n\n还记得 SOHCAHTOA 这个词吗？\n\n如果我们使用的是 2D 笛卡尔坐标系(剧透警报，我们是)我们场景中*对面的*边只是 *Y* 坐标，相邻的*边是 *X* 坐标。所以，在 2D 笛卡尔坐标系中，我们的比率是这样的:*\n\n*   *正弦= Y/圆半径*\n*   *余弦= X/圆半径*\n*   *切线= Y/X*\n\n如果您正在调用 JavaScript 数学库中的函数，如`cos`(表示余弦)或`sin`(表示正弦)，则通常会传入以弧度为单位的角度。你会得到这个比值，如果你处理的是一个*单位圆*(半径为 1 的圆)，它会给出余弦的 *X* 值和正弦的 *Y* 值。所以大多数时候，你需要记住的是:\n\n*   如果你想要 *Y* 坐标，使用正弦\n*   如果你想要 *X* 坐标，使用余弦\n\n我们之前用这个算出了我们船的方向和速度。我们稍后将使用它来获得给定角度下粒子的方向和速度。我们现在要用它来计算如何画出 SVG 图，显示我们将在什么角度发射粒子。\n\n我们采用两个不同的角度来获得发射粒子的角度范围。因为我们希望我们的角度与角度 0 度重叠，所以我们必须允许`min_angle`为负。我们的最小角度可以从-90 度到 359 度，最大角度可以从 0 度到 360 度。\n\n我更喜欢用角度而不是弧度来测量角度。数学函数通常使用弧度，因此，如果您在界面中更习惯使用弧度，就可以省去运行转换的麻烦。弧度是基于*单位圆*的角度测量。一个*单位圆*的周长为 *2π* 。如果你用弧度来测量角度，你就要根据你绕着*单位圆*走多远才能到达那个点来确定你的角度。所以，如果你从你的*单位圆*的一边走到对面，你就要走π的距离。因此 *π* (弧度)= 180 度。如果你想要一个四分之一圆的角度，你必须绕着你的圆走一段 *π / 2* 的距离，所以 *π / 2 = 90 度*。我仍然觉得 360 度的圆圈更直观，因为我在学校的时候，我们花了很多时间学习学位。弧度是后来才提到的。如果不是这样，我相信我会发现用*单位圆*来测量我的角度更有意义。\n\nThe idea of a 360-degree circle is only intuitive because they drilled it into us when we were in school. The only reason we have this model of a circle is that we inherited it from the ancient Babylonians who used a base 60 mathematical system, which is also the reason we have 60 seconds in a minute and 60 minutes in an hour.\n\n稍后，我们将使用 SVG 和一些 trig 绘制一个小饼图，表示粒子将从我们的粒子系统发射的方向。我们需要这种方向性来创建我们的发动机废气粒子发射器:\n\n![](img/47c00130-a4f0-451c-847c-bf530e50b17b.png)\n\nFigure 8.1: Our SVG pie chart\n\n在下一节中，我们将使用 JavaScript 实现我们的 SVG 饼图。\n\n# 添加 JavaScript\n\n现在，我们已经讨论了绘制 SVG 图表所需的一些三角函数，让我来逐步了解一下我们需要添加哪些 JavaScript 来使代码正常工作:\n\n```cpp\n\n<script>\n    document.getElementById(\"min_angle\").onchange = function() {\n        var min_angle = Number(this.value);\n        var max_angle = Number(document.getElementById         \n                        (\"max_angle\").value);\n\n        if( min_angle >= max_angle ) {\n            max_angle = min_angle + 1;\n            document.getElementById(\"max_angle\").value = max_angle;\n        }\n\n        if( min_angle < this.min ) {\n            min_angle = this.min;\n            this.value = min_angle;\n        }\n        SetPie( min_angle / 180 * Math.PI, max_angle / 180 * Math.PI );\n    }\n\n    document.getElementById(\"max_angle\").onchange = function() {\n        var min_angle = Number(document.getElementById         \n                        (\"min_angle\").value);\n        var max_angle = Number(this.value);\n\n        if( min_angle >= max_angle ) {\n            min_angle = max_angle - 1;\n            document.getElementById(\"min_angle\").value = min_angle;\n        }\n\n        if( max_angle > this.max ) {\n            max_angle = this.max;\n            this.value = max_angle;\n        }\n\n        SetPie( min_angle / 180 * Math.PI, max_angle / 180 * Math.PI );\n    }\n\n    function SetPie( start_angle, end_angle ) {\n        const svg = document.getElementById('pie');\n        const start_x = Math.cos( start_angle );\n        const start_y = Math.sin( start_angle );\n\n        const end_x = Math.cos( end_angle );\n        const end_y = Math.sin( end_angle );\n        var arc_flag_1 = 0;\n        var arc_flag_2 = 0;\n\n        if( end_angle - start_angle <= 3.14) {\n            arc_flag_1 = 0;\n            arc_flag_2 = 1;\n        }\n        else {\n            arc_flag_1 = 1;\n            arc_flag_2 = 0;\n        }\n\n        const path_data_1 = \n            `M 0 0 L ${start_x} ${start_y} A 1 1 0 ${arc_flag_1} 1 \n            ${end_x} ${end_y} L 0 0`;\n\n        const path_1 = document.createElementNS         \n        ('http://www.w3.org/2000/svg', 'path');\n        path_1.setAttribute('d', path_data_1);\n        path_1.setAttribute('fill', 'red');\n        svg.appendChild(path_1);\n\n        const path_data_2 = \n            `M 0 0 L ${end_x} ${end_y} A 1 1 0 ${arc_flag_2} 1 \n             ${start_x} ${start_y} L 0 0`;\n\n        const path_2 = \n        document.createElementNS('http://www.w3.org/2000/svg', 'path');\n        path_2.setAttribute('d', path_data_2);\n        path_2.setAttribute('fill', 'blue');\n        svg.appendChild(path_2);\n    }\n\n    SetPie( Number(document.getElementById(\"min_angle\").value) / 180 *             \n            Math.PI,\n    Number(document.getElementById(\"max_angle\").value) / 180 * Math.PI );\n</script>\n```\n\n即使这是本代码中的最后一个功能，我也想先解释一下`SetPie`功能，该功能用于设置 SVG 饼图，以红色显示用户输入的发射角度范围。早在我们设置 SVG 标签时，我们将`viewport`设置为从`-1`的`x`和`y`值到`1`值。那太好了，因为使用`Math.cos`和`Math.sin`会给我们*单位圆*的 *X* 和 *Y* 坐标的值，其半径为`1`，所以这些值也会从`-1`到`1`运行。\n\n我们使用`document.getElementById('pie')`从 DOM 中抓取`svg`元素，这样我们就可以根据角度值的变化来修改它。接下来，我们分别用`Math.cos`和`Math.sin`函数得到单位圆上的`x`和`y`坐标。然后我们用`end_angle`做同样的事情得到结束`x`和`y`坐标:\n\n```cpp\nconst end_x = Math.cos( end_angle );\nconst end_y = Math.sin( end_angle );\n```\n\n我们在 SVG 中需要做的是画两条路径。第一条路径将以红色绘制，代表粒子系统发射器发射粒子的角度。第二条路径将以蓝色绘制，将代表我们发射圆中不发射粒子的部分。当我们画一个 SVG 弧时，我们给弧两点，用旗帜告诉它我们需要绕圆走长路(钝角)还是走短路(锐角)。我们通过检查发射角度是否小于π来实现这一点，并基于此设置一个标志，该标志将进入我们的 SVG:\n\n```cpp\nif( end_angle - start_angle <= 3.14) {\n    arc_flag_1 = 0;\n    arc_flag_2 = 1;\n}\nelse {\n    arc_flag_1 = 1;\n    arc_flag_2 = 0;\n}\n```\n\n现在，我们需要定义路径数据，并将其放入 SVG 路径对象中。下面的代码为我们发射粒子的发射器部分设置路径数据:\n\n```cpp\nconst path_data_1 = `M 0 0 L ${start_x} ${start_y} A 1 1 0 ${arc_flag_1} 1 ${end_x} ${end_y} L 0 0`;\n\nconst path_1 = document.createElementNS('http://www.w3.org/2000/svg',                                         \n                                        'path');\npath_1.setAttribute('d', path_data_1);\npath_1.setAttribute('fill', 'red');\nsvg.appendChild(path_1);\n```\n\n一系列命令在 SVG 中定义路径数据。如果看`path_data_1`的定义，是从`M 0 0`开始的，它告诉 SVG 移动光标定位`0, 0`不用画图。下一个命令是`L ${start_x} ${start_y}`。因为我们使用的是字符串模板文字，`${start_x}`和`${start_y}`被替换为`start_x`和`start_y`变量中的值。该命令从我们在上一步`(0,0)`移动到的当前位置到坐标`start_x`和`start_y`之间画一条线。我们路径中的下一个命令是`Arc`命令，以`A` : `A 1 1 0 ${arc_flag_1} 1 ${end_x} ${end_y}`开始。\n\n前两个参数`1 1`是椭圆的`x`和`y`半径。因为我们想要一个单位圆，这两个值都是`1`。接下来的`0`是 SVG 绘制椭圆时使用的 *X* 轴旋转。因为我们在画圆，所以设置为`0`。之后的数值为`${arc_flag_1}`。用于设置*大圆弧标志*，告诉 SVG 我们画的是钝角圆弧(我们设置值为 1)还是锐角圆弧(我们设置值为 0)。之后的值是*扫描标志*。该标志确定我们是在顺时针(值为 1)还是逆时针(值为 0)方向绘制。我们总是想顺时针方向画，所以这个值将是 1。我们的*弧线*命令中的最后两个参数是`${end_x} ${end_y}`。这些值是我们弧线的终点位置，我们之前通过得到终点角度的余弦和正弦来确定。在我们完成我们的弧之后，我们通过使用`L 0 0`直线命令画一条线回到`0,0`坐标来完成我们的形状。\n\n在我们用红色画出发射角之后，我们用第二条路径覆盖蓝色圆圈的剩余部分，从结束位置画到开始位置。\n\n在下一节中，我们将构建一个简单的粒子发射器配置工具。\n\n# 简单的粒子发射器工具\n\n现在我们已经创建了一个简单的 web app，可以上传一个 PNG 图像文件到 WebAssembly *虚拟文件系统*，以及一个 SVG 图表来显示粒子的发射方向，接下来我们要添加一个简单的粒子系统配置工具。对于粒子系统配置工具的第一个版本，我们将保持可配置值的数量较少。稍后，我们将向粒子系统工具添加更多功能，但目前，这是我们可以用来配置粒子发射器的参数列表:\n\n*   图象档案\n*   最小发射角\n*   最大发射角\n*   最大粒子数\n*   粒子寿命(毫秒)\n*   粒子加速(或减速)\n*   Alpha 褪色(粒子会随着时间的推移而褪色吗？)\n*   发射率(每秒发射的粒子数)\n*   x 位置(发射器 x 坐标)\n*   y 位置(发射器 y 坐标)\n*   半径(离发射器的位置有多远我们可以创建一个粒子？)\n*   最小起动速度\n*   最大起动速度\n\n这将让我们创建一个非常基本的粒子发射器。我们将在下一部分改进这个发射器，但我们需要从某个地方开始。我不打算讨论我们添加的任何 CSS 来增强这个工具的外观。我想做的第一件事是覆盖将进入新外壳文件的 HTML，我们称之为`basic_particle_shell.html`。我们需要添加一些 HTML `input`字段来接受我们之前讨论过的所有可配置值。我们还需要一个按钮来更新发射器，一旦我们写了我们的变化。\n\n在我们新的 shell 文件的`<body>`标签中添加以下代码:\n\n```cpp\n<div class=\"container\">\n    <svg id=\"pie\" width=\"200\" height=\"200\" viewBox=\"-1 -1 2 2\"></svg>\n    <br/>\n    <div style=\"margin-left: auto; margin-right: auto\">\n        <span class=\"label\">min angle:</span>\n        <input type=\"number\" id=\"min_angle\" max=\"359\" min=\"-90\" \n         step=\"1\" value=\"-20\" class=\"em_input\">\n        <br/>\n        <span class=\"label\">max angle:</span>\n        <input type=\"number\" id=\"max_angle\" max=\"360\" min=\"0\" step=\"1\" \n         value=\"20\" class=\"em_input\">\n        <br/>\n    </div>\n    <span class=\"label\">max particles:</span>\n    <input type=\"number\" id=\"max_particles\" max=\"10000\" min=\"10\" \n            step=\"10\" value=\"100\" class=\"em_input\">    \n    <br/>\n    <span class=\"label\">life time:</span>\n    <input type=\"number\" id=\"lifetime\" max=\"10000\" min=\"10\"\n            step=\"10\" value=\"1000\" class=\"em_input\"><br/>\n    <span class=\"label\">acceleration:</span>\n\n    <input type=\"number\" id=\"acceleration\" max=\"2.0\" min=\"0.0\"\n                        step=\"0.1\" value=\"1.0\" class=\"em_input\"><br/>\n    <label class=\"ccontainer\"><span class=\"label\">alpha fade:</span>\n        <input type=\"checkbox\" checked=\"checked\">\n        <span class=\"checkmark\"></span>\n    </label>\n    <br/>\n    <span class=\"label\">emission rate:</span>\n    <input type=\"number\" id=\"emission_rate\" max=\"100\" min=\"1\" step=\"1\" \n     value=\"20\" class=\"em_input\">\n    <br/>\n\n    <span class=\"label\">x position:</span>\n    <input type=\"number\" id=\"x_pos\" max=\"800\" min=\"0\" step=\"1\" \n     value=\"400\" class=\"em_input\">\n    <br/>\n    <span class=\"label\">y position:</span>\n    <input type=\"number\" id=\"y_pos\" max=\"600\" min=\"0\" step=\"1\" \n     value=\"300\" class=\"em_input\">\n    <br/>\n    <span class=\"label\">radius:</span>\n    <input type=\"number\" id=\"radius\" max=\"500\" min=\"0\" step=\"1\" \n     value=\"20\" class=\"em_input\">\n    <br/>\n\n    <span class=\"label\">min start vel:</span>\n    <input type=\"number\" id=\"min_starting_vel\" max=\"9.9\" min=\"0.0\"\n                        step=\"0.1\" value=\"1.0\" class=\"em_input\"><br/>\n    <span class=\"label\">max start vel:</span>\n    <input type=\"number\" id=\"max_starting_vel\" max=\"10.0\" min=\"0.0\"\n                        step=\"0.1\" value=\"2.0\" class=\"em_input\"><br/>\n\n    <div class=\"input_box\">\n        <button id=\"update_btn\" class=\"em_button\" \n         onclick=\"UpdateClick()\">Update Emitter</button>\n    </div>\n </div>\n```\n\nCSS 文件使这个容器显示在网页的左侧。用户可以像以前一样将图像加载到虚拟文件系统中，但这次这些输入字段中的所有值都用于创建粒子发射器。用户可以修改这些设置，并单击“更新发射器”按钮来更新发射器使用的值。这将允许用户测试一些基本的发射器设置。\n\nThe code inside of the main function will need to be added to prevent the SDL Event handler from intercepting the keyboard events and preventing the default behavior inside of these input elements. We will cover that code a little later.\n\n现在，我已经向您展示了必须添加的 HTML 元素，以允许我们配置粒子系统，让我们逐步完成 JavaScript 代码，它将使我们能够将这些值传递到 WebAssembly 模块中。这是 JavaScript 代码的样子:\n\n```cpp\n<script type='text/javascript'>\n var canvas = null;\n var ctx = null;\n var ready = false;\n    var image_added = false;\n    function ShowFileInput() {\n        document.getElementById(\"file_input_label\").style.display = \n        \"block\";\n        ready = true;\n    }\n    function UpdateClick() {\n        if( ready == false || image_added == false ) { return; }\n        var max_particles = Number(document.getElementById         \n                             (\"max_particles\").value);\n        var min_angle = Number(document.getElementById         \n                            (\"min_angle\").value) / 180 * Math.PI;\n        var max_angle = Number(document.getElementById             \n                              (\"max_angle\").value) / 180 * Math.PI\n        var particle_lifetime = Number(document.getElementById         \n                                    (\"lifetime\").value);\n        var acceleration = Number(document.getElementById        \n                               (\"acceleration\").value);\n        var alpha_fade = Boolean(document.getElementById         \n                               (\"alpha_fade\").checked);\n        var emission_rate = Number(document.getElementById             \n                                (\"emission_rate\").value);\n        var x_pos = Number(document.getElementById(\"x_pos\").value);\n        var y_pos = Number(document.getElementById(\"y_pos\").value);\n        var radius = Number(document.getElementById(\"radius\").value);\n        var min_starting_velocity = Number(document.getElementById                                                                                                                                                         \n                                    (\"min_starting_vel\").value);\n        var max_starting_velocity = Number(document.getElementById                                                                                                                                                         \n                                    (\"max_starting_vel\").value);\n        Module.ccall('update_emitter', 'undefined',             \n        [\"number\",\"number\",\"number\",\"number\", \"number\",\"bool\", \n        \"number\",\"number\",\"number\",\"number\",\"number\",\"number\"],\n\n        [max_particles,min_angle,max_angle,particle_lifetime,\n         acceleration,alpha_fade,min_starting_velocity,\n         max_starting_velocity,emission_rate,x_pos ,y_pos,radius]);\n        }\n        var Module = {\n            preRun: [],\n            postRun: [ShowFileInput],\n            print: (function() {\n                var element = document.getElementById('output');\n                if (element) element.value = '';\n                return function(text) {\n                    if (arguments.length > 1) text =   \n                    Array.prototype.slice.call(arguments).join(' ');\n                    console.log(text);\n                    if (element) {\n                        element.value += text + \"\\n\";\n                        element.scrollTop = element.scrollHeight;\n                    }\n                }; })(),\n        printErr: function(text) {\n            if (arguments.length > 1) text = \n            Array.prototype.slice.call(arguments).join(' ');\n            if (0) { dump(text + '\\n'); } \n            else { console.error(text); }\n        },\n        canvas: (function() {\n            var canvas = document.getElementById('canvas');\n            canvas.addEventListener(\"webglcontextlost\", function(e) {\n                alert('WebGL context lost. You will need to reload the \n                       page.');\n                e.preventDefault();},false);\n            return canvas; })(),\n        setStatus: function(text) {\n            if (!Module.setStatus.last) Module.setStatus.last={ time: \n                Date.now(), text: '' };\n            if (text === Module.setStatus.last.text) return;\n            var m = text.match(/([^(]+)\\((\\d+(\\.\\d+)?)\\/(\\d+)\\)/);\n            var now = Date.now();\n            if (m && now - Module.setStatus.last.time < 30) return;\n            Module.setStatus.last.time = now;\n            Module.setStatus.last.text = text;\n            if(m) { text = m[1]; }\n            console.log(\"status: \" + text); },\n        totalDependencies: 0,\n        monitorRunDependencies: function(left) {\n            this.totalDependencies = Math.max(this.totalDependencies, \n                                              left);\n            Module.setStatus(left?'Preparing... (' + \n                            (this.totalDependencies-left) +\n                '/' + this.totalDependencies + ')' : \n                'All downloads complete.');\n        } };\n    Module.setStatus('Downloading...');\n    window.onerror = function() {\n        Module.setStatus('Exception thrown, see JavaScript console');\n        Module.setStatus = function(text) {\n            if (text) Module.printErr('[post-exception status] ' + \n                                        text);\n        }; };\n    function handleFiles(files) {\n      var file_count = 0;\n      for (var i = 0; i < files.length; i++) {\n          if (files[i].type.match(/image.png/)) {\n              var file = files[i];\n              var file_name = file.name;\n              var fr = new FileReader();\n              fr.onload = function(file) {\n                var data = new Uint8Array(fr.result);\n                Module.FS_createDataFile('/', file_name, data, \n                                          true, true, true);\n                var max_particles = Number(document.getElementById                                         \n                                    (\"max_particles\").value);\n                var min_angle = Number(document.getElementById                                       \n                                (\"min_angle\").value) / 180 * \n                                Math.PI;\n                var max_angle = Number(document.getElementById                                     \n                                (\"max_angle\").value) / 180 * \n                                 Math.PI\n                var particle_lifetime = Number(document.getElementById                                                \n                                        (\"lifetime\").value);\n                var acceleration = Number(document.getElementById \n                                    (\"acceleration\").value);\n                var alpha_fade = Boolean(document.getElementById \n                                 (\"alpha_fade\").checked);\n                var emission_rate = Number(document.getElementById \n                                    (\"emission_rate\").value);\n                var x_pos = Number(document.getElementById \n                            (\"x_pos\").value);\n                var y_pos = Number(document.getElementById \n                            (\"y_pos\").value);\n                var radius = Number(document.getElementById                                          \n                            (\"radius\").value);\n                var min_starting_velocity = Number(document.getElementById\n                                            (\"min_starting_vel\").value);\n                var max_starting_velocity = Number(document.getElementById                                             \n                                            (\"max_starting_vel\").value);\n                Module.ccall('add_emitter','undefined', \n                [\"string\",\"number\", \"number\", \"number\", \"number\", \n                 \"number\", \"bool\",  \"number\", \"number\",\"number\", \n                 \"number\", \"number\", \"number\"],\n                [file_name, max_particles, min_angle, max_angle, \n                particle_lifetime, acceleration, alpha_fade, \n                min_starting_velocity, max_starting_velocity, \n                emission_rate, x_pos, y_pos, radius]);\n                image_added = true; };\n              fr.readAsArrayBuffer(files[i]);\n} } }\n</script>\n```\n\n`Module`代码大部分没有修改，但是我们增加了几个函数和一些新的变量。我们添加了一个全局`ready`变量，初始化时设置为`false`。当`Module`装载时，该标志将被设置为`true`。与上一节一样，`ShowFileInput`在使用`postRun`数组加载`Module`后运行。我们已经调整了这个代码来设置我们前面提到的`ready`标志:\n\n```cpp\nfunction ShowFileInput() {\n    document.getElementById(\"file_input_label\").style.display = \"block\";\n    ready = true;\n}\n```\n\n在前面的部分中，我们创建了一个`handleFiles`函数，该函数将一个文件加载到我们的 WebAssembly 虚拟文件系统中。我们现在需要修改这个函数来调用一个函数`add_emitter`，我们需要在我们的 C++ 代码中定义这个函数。我们将调用这个函数，传入我们在 HTML 输入元素中定义的所有值。这个函数是这样的:\n\n```cpp\nfunction handleFiles(files) {\n    var file_count = 0;\n    for (var i = 0; i < files.length; i++) {\n        if (files[i].type.match(/image.png/)) {\n            var file = files[i];\n            var file_name = file.name;\n            var fr = new FileReader();\n            fr.onload = function (file) {\n                var data = new Uint8Array(fr.result);\n                Module.FS_createDataFile('/', file_name, data, true, \n                                          true, true);\n                var max_particles = Number(document.getElementById( \n                                    \"max_particles\").value);\n                var min_angle = Number(document.getElementById         \n                                (\"min_angle\").value) / 180 * Math.PI;\n                var max_angle = Number(document.getElementById         \n                                (\"max_angle\").value) / 180 * Math.PI\n                var particle_lifetime = Number(document.getElementById                                         \n                                        (\"lifetime\").value);\n                var acceleration = Number(document.getElementById \n                                   (\"acceleration\").value);\n                var alpha_fade = Boolean(document.getElementById \n                                 (\"alpha_fade\").checked);\n                var emission_rate = Number(document.getElementById \n                                    (\"emission_rate\").value);\n                var x_pos = Number(document.getElementById \n                            (\"x_pos\").value);\n                var y_pos = Number(document.getElementById    \n                            (\"y_pos\").value);\n                var radius = Number(document.getElementById \n                             (\"radius\").value);\n              var min_starting_velocity = Number(document.getElementById \n                                         (\"min_starting_vel\").value);\n              var max_starting_velocity = Number(document.getElementById                                                        \n                                          (\"max_starting_vel\").value);\n                Module.ccall('add_emitter', 'undefined', [\"string\", \n                \"number\", \"number\", \"number\",\n                \"number\", \"number\", \"bool\",\n                \"number\", \"number\",\n                \"number\", \"number\", \"number\", \"number\"],\n                [file_name, max_particles,\n                min_angle, max_angle,\n                particle_lifetime, acceleration, alpha_fade,                                                      \n                min_starting_velocity, max_starting_velocity,\n                emission_rate, x_pos, y_pos, radius]);\n                image_added = true;\n            };\n            fr.readAsArrayBuffer(files[i]);\n        }\n    }\n}\n```\n\n`FileReader`代码，以及这个函数上一次迭代对`Module.FS_createDataFile`的调用，还在这里。除此之外，我们使用`document.getElementById`抓取 HTML 元素并将这些元素的值存储到一组变量中:\n\n```cpp\nvar max_particles = Number(document.getElementById    \n                    (\"max_particles\").value);\nvar min_angle = Number(document.getElementById(\"min_angle\").value) / \n                180 * Math.PI;\nvar max_angle = Number(document.getElementById(\"max_angle\").value) / \n                180 * Math.PI\nvar particle_lifetime = Number(document.getElementById     \n                        (\"lifetime\").value);\nvar acceleration = Number(document.getElementById         \n                   (\"acceleration\").value);\nvar alpha_fade = Boolean(document.getElementById \n                 (\"alpha_fade\").checked);\nvar emission_rate = Number(document.getElementById \n                    (\"emission_rate\").value);\nvar x_pos = Number(document.getElementById(\"x_pos\").value);\nvar y_pos = Number(document.getElementById(\"y_pos\").value);\nvar radius = Number(document.getElementById(\"radius\").value);\nvar min_starting_velocity = Number(document.getElementById \n                            (\"min_starting_vel\").value);\nvar max_starting_velocity = Number(document.getElementById   \n                            (\"max_starting_vel\").value);\n```\n\n其中许多值需要使用`Number`强制函数显式强制为数字。必须将`alpha_fade`变量强制转换为`Boolean`值。现在我们已经在变量中有了所有这些值，我们可以使用`Module.ccall`调用 C++ 函数`add_emitter`，传入所有这些值:\n\n```cpp\nModule.ccall('add_emitter', 'undefined', [\"string\", \"number\", \"number\", \n             \"number\",\n             \"number\", \"number\", \"bool\",\n             \"number\", \"number\",\n             \"number\", \"number\", \"number\", \"number\"],\n             [file_name, max_particles, min_angle, max_angle,\n             particle_lifetime, acceleration, alpha_fade,\n             min_starting_velocity, max_starting_velocity,\n             emission_rate, x_pos, y_pos, radius]);\n```\n\n最后，我们将`image_added`标志设置为`true`。我们不允许用户更新发射器，除非对`add_emitter`的调用已经创建了它。我们还增加了一个新的函数`UpdateClick`，只要有人点击更新发射器按钮，我们就会调用这个函数，假设他们已经创建了一个发射器。下面是该函数中的代码:\n\n```cpp\nfunction UpdateClick() {\n    if( ready == false || image_added == false ) {\n        return;\n    }\n    var max_particles = Number(document.getElementById    \n                        (\"max_particles\").value);\n    var min_angle = Number(document.getElementById(\"min_angle\").value) \n                    / 180 * Math.PI;\n    var max_angle = Number(document.getElementById(\"max_angle\").value) \n                    / 180 * Math.PI\n    var particle_lifetime = Number(document.getElementById \n                            (\"lifetime\").value);\n    var acceleration = Number(document.getElementById     \n                       (\"acceleration\").value);\n    var alpha_fade = Boolean(document.getElementById \n                     (\"alpha_fade\").checked);\n    var emission_rate = Number(document.getElementById \n                        (\"emission_rate\").value);\n    var x_pos = Number(document.getElementById(\"x_pos\").value);\n    var y_pos = Number(document.getElementById(\"y_pos\").value);\n    var radius = Number(document.getElementById(\"radius\").value);\n    var min_starting_velocity = Number(document.getElementById     \n                                (\"min_starting_vel\").value);\n    var max_starting_velocity = Number(document.getElementById \n                                (\"max_starting_vel\").value);\n\n    Module.ccall('update_emitter', 'undefined', [\"number\", \"number\", \n                 \"number\",\n                 \"number\", \"number\", \"bool\",\n                 \"number\", \"number\",\n                 \"number\", \"number\", \"number\", \"number\"],\n                 [max_particles, min_angle, max_angle,\n                 particle_lifetime, acceleration, alpha_fade,\n                 min_starting_velocity, max_starting_velocity,\n                 emission_rate, x_pos, y_pos, radius]);\n}\n```\n\n我们要做的第一件事是确保`Module`对象被加载，并且我们创建了发射器。如果这两种情况都没有发生，我们就不想运行这段代码，所以我们必须返回:\n\n```cpp\nif( ready == false || image_added == false ) {\n    return;\n}\n```\n\n这段代码的其余部分类似于我们添加到`handleFiles`的代码。首先，我们抓取所有的 HTML 元素，并将其中的值强制转换为适当的数据类型，以传递给我们对 C++ 函数的调用:\n\n```cpp\nvar max_particles = Number(document.getElementById             \n                    (\"max_particles\").value);\nvar min_angle = Number(document.getElementById(\"min_angle\").value) / \n                180 * Math.PI;\nvar max_angle = Number(document.getElementById(\"max_angle\").value) / \n                180 * Math.PI\nvar particle_lifetime = Number(document.getElementById     \n                        (\"lifetime\").value);\nvar acceleration = Number(document.getElementById         \n                   (\"acceleration\").value); \nvar alpha_fade = Boolean(document.getElementById \n                 (\"alpha_fade\").checked);\nvar emission_rate = Number(document.getElementById     \n                    (\"emission_rate\").value);\nvar x_pos = Number(document.getElementById(\"x_pos\").value);\nvar y_pos = Number(document.getElementById(\"y_pos\").value);\nvar radius = Number(document.getElementById(\"radius\").value);\nvar min_starting_velocity = Number(document.getElementById \n                            (\"min_starting_vel\").value);\nvar max_starting_velocity = Number(document.getElementById \n                            (\"max_starting_vel\").value);\n```\n\n从输入元素中获取所有值后，我们使用这些值调用`update_emitter` C++ 函数，传递这些值:\n\n```cpp\nModule.ccall('update_emitter', 'undefined', [\"number\", \"number\", \n             \"number\",\n             \"number\", \"number\", \"bool\",\n             \"number\", \"number\",\n             \"number\", \"number\", \"number\", \"number\"],\n             [max_particles, min_angle, max_angle,\n             particle_lifetime, acceleration, alpha_fade,\n             min_starting_velocity, max_starting_velocity,\n             emission_rate, x_pos, y_pos, radius]);\n\n```\n\n在下一节中，我们将实现一个`Point`类来跟踪游戏对象的位置。\n\n# 点类\n\n在前面的章节中，我们已经在课堂上直接处理了 2D *X* 和 *Y* 坐标。我想增加一点功能，处理我们的 *X* 和 *Y* 坐标。为此，我们需要定义一个名为`Point`的新类。最终，`Point`将会比我们在这里使用它做的更多。但是现在，我希望能够创建一个`Point`对象，并且能够以一个角度`Rotate`指向那个点。以下是我们添加到`game.hpp`文件中的`Point`的类定义:\n\n```cpp\nclass Point {\n    public:\n        float x;\n        float y;\n        Point();\n        Point( float X, float Y );\n        Point operator=(const Point& p);\n        void Rotate( float radians );\n};\n```\n\n前几个函数和`operator=`非常简单。他们通过构造函数或使用一行代码(如`point_1 = point_2;`)来设置 x 和 y 属性。最后一个函数`Rotate`，是我们创建这个类的全部原因。它的工作是获取 *X* 和 *Y* 坐标，并围绕点`0,0`旋转它们。下面是实现这一点的代码:\n\n```cpp\nvoid Point::Rotate( float radians ) {\n    float sine = sin(radians);\n    float cosine = cos(radians);\n    float rx = x * cosine - y * sine;\n    float ry = x * sine + y * cosine;\n    x = rx;\n    y = ry;\n}\n```\n\n这个`Rotate`功能最终会在整个游戏中使用。现在，我们将根据发射角度用它来定义粒子的速度。\n\n# 粒子类\n\n`Particle`类是我们将用来表示由我们的粒子系统发射的单个粒子的类。需要使用构造函数创建`Particles`类，然后使用用于修改粒子定义属性的`Update`函数进行更新。将有一个`Spawn`功能用于激活`Particle`，一个`Move`功能用于在粒子的生命周期中移动粒子并最终将其停用，还有一个`Render`功能用于执行将粒子绘制到画布上所需的 SDL 渲染任务。以下是`Particle`类在我们的`game.hpp`文件中的样子:\n\n```cpp\nclass Particle {\n    public:\n        bool m_active;\n        bool m_alpha_fade;\n        SDL_Texture *m_sprite_texture;\n        int m_ttl;\n        Uint32 m_life_time;\n        float m_acceleration;\n        float m_alpha;\n        Point m_position;\n        Point m_velocity;\n        SDL_Rect m_dest = {.x = 0, .y = 0, .w = 0, .h = 0 };\n        Particle( SDL_Texture *sprite, Uint32 life_time, float \n        acceleration, bool alpha_fade, int width, int height );\n        void Update( Uint32 life_time, float acceleration,\n                    bool alpha_fade );\n        void Spawn( float x, float y, float velocity_x, float \n        velocity_y, float alpha );\n        void Move();\n        void Render();\n};\n```\n\n我们将在`particle.cpp`文件中定义与`Particle`类相关的函数。在这个文件的顶部，我们定义了一个构造函数和一个`Update`函数。每当用户点击网页上的更新发射器按钮时，我们就调用`Update`功能。这将更新所有粒子，使其使用寿命、加速度和阿尔法衰减的新值。以下是前两个函数的代码:\n\n```cpp\nParticle::Particle( SDL_Texture *sprite_texture, Uint32 life_time, \n                    float acceleration, bool alpha_fade, \n                    int width, int height ) {\n    m_sprite_texture = sprite_texture;\n    m_life_time = life_time;\n    m_acceleration = acceleration;\n    m_alpha_fade = alpha_fade;\n    m_dest.w = width;\n    m_dest.h = height;\n    m_active = false;\n}\nvoid Particle::Update( Uint32 life_time, float acceleration, bool \n                       alpha_fade ) {\n    m_life_time = life_time;\n    m_acceleration = acceleration;\n    m_alpha_fade = alpha_fade;\n    m_active = false;\n}\n```\n\n每当需要发射粒子时，`Spawn`函数被`Emitter`调用。`Emitter`检查它发射的粒子是否有设置为`false`的激活标志。传入`Spawn`的数值，如 *X* 和 *Y* 坐标、速度`x`和`y`值以及起始α值，都是在发射新粒子时由`Emitter`计算的。代码如下所示:\n\n```cpp\nvoid Particle::Spawn( float x, float y, float velocity_x, \n                      float velocity_y, float alpha ) {\n    m_position.x = x;\n    m_dest.x = (int)m_position.x;\n    m_position.y = y;\n    m_dest.y = (int)m_position.y;\n    m_velocity.x = velocity_x;\n    m_velocity.y = velocity_y;\n    m_alpha = alpha;\n    m_active = true;\n    m_ttl = m_life_time;\n}\n```\n\n发射器每帧调用一次每个活动粒子的`Move`函数，该函数是粒子计算其新位置α的地方，并根据它存活的时间来确定它是否仍处于活动状态。代码如下所示:\n\n```cpp\nvoid Particle::Move() { \n    float acc_adjusted = 1.0f;\n    if( m_acceleration < 1.0f ) {\n        acc_adjusted = 1.0f - m_acceleration;\n        acc_adjusted *= delta_time;\n        acc_adjusted = 1.0f - acc_adjusted;\n    }\n    else if( m_acceleration > 1.0f ) {\n        acc_adjusted = m_acceleration - 1.0f;\n        acc_adjusted *= delta_time;\n        acc_adjusted += 1.0f;\n    }\n    m_velocity.x *= acc_adjusted;\n    m_velocity.y *= acc_adjusted;\n    m_position.x += m_velocity.x;\n    m_position.y += m_velocity.y;\n    m_dest.x = (int)m_position.x;\n    m_dest.y = (int)m_position.y;\n\n    if( m_alpha_fade == true ) {\n        m_alpha = 255.0 * (float)m_ttl / (float)m_life_time;\n        if( m_alpha < 0 ) {\n            m_alpha = 0;\n        }\n    }\n    else {\n        m_alpha = 255.0;\n    }\n    m_ttl -= diff_time;\n    if( m_ttl <= 0 ) {\n        m_active = false;\n    }\n}\n```\n\n最后，`Render`函数调用设置粒子阿尔法值的 SDL 函数，然后将该粒子复制到渲染器:\n\n```cpp\nvoid Particle::Render() {\n    SDL_SetTextureAlphaMod(m_sprite_texture, (Uint8)m_alpha );\n    SDL_RenderCopy( renderer, m_sprite_texture, NULL, &m_dest );\n}\n```\n\n在下一节中，我们将讨论`Emitter`类以及使该类工作所需的代码。\n\n# 发射器类\n\n`Emitter`类管理一个粒子池，是粒子用来渲染自己的加载精灵纹理所在的地方。我们的发射器只会是圆形的。可以用许多不同的可能形状来定义发射器，但是对于我们的游戏来说，圆形发射器可以很好地工作。现在，我们的`Emitter`课将会非常基础。在后面的部分，我们将添加一些新的功能，但现在我想创建一个非常基本的粒子系统。以下是`game.hpp`文件中的类定义:\n\n```cpp\nclass Emitter {\n    public:\n        SDL_Texture *m_sprite_texture;\n        std::vector<Particle*> m_particle_pool;\n        int m_sprite_width;\n        int m_sprite_height;\n        Uint32 m_max_particles;\n        Uint32 m_emission_rate;\n        Uint32 m_emission_time_ms;\n        int m_next_emission;\n        float m_max_angle;\n        float m_min_angle;\n        float m_radius;\n        float m_min_starting_velocity;\n        float m_max_starting_velocity;\n        Point m_position;\n\n        Emitter(char* sprite_file, int max_particles, float min_angle, \n                float max_angle,\n                Uint32 particle_lifetime, float acceleration, bool \n                alpha_fade,\n                float min_starting_velocity, float \n                max_starting_velocity,\n                Uint32 emission_rate, int x_pos, int y_pos, float \n                radius );\n        void Update(int max_particles, float min_angle, float \n        max_angle,\n                    Uint32 particle_lifetime, float acceleration, bool \n                    alpha_fade,\n                    float min_starting_velocity, float \n                    max_starting_velocity,\n                    Uint32 emission_rate, int x_pos, int y_pos, float \n                    radius );\n        void Move();\n        Particle* GetFreeParticle();\n};\n```\n\n这个类中的属性反映了我们在本章前面创建的 HTML 输入元素。当使用构造函数创建`Emitter`时，或者当用户单击调用`Update`函数的更新按钮时，这些值被设置。`Move`功能将每帧调用一次，然后移动并渲染粒子池中所有活跃的粒子。它还将通过调用自由粒子上的`Spawn`函数来确定是否应该发射新粒子。\n\n我们将在`emitter.cpp`文件中定义所有这些函数。以下是`Emitter`构造函数和`Update`函数在`emitter.cpp`文件中的样子:\n\n```cpp\nEmitter::Emitter(char* sprite_file, int max_particles, float min_angle, \nfloat max_angle, Uint32 particle_lifetime, float acceleration, bool alpha_fade, float min_starting_velocity, float max_starting_velocity,\nUint32 emission_rate, int x_pos, int y_pos, float radius ) {\n\n    if( min_starting_velocity > max_starting_velocity ) {\n        m_min_starting_velocity = max_starting_velocity;\n        m_max_starting_velocity = min_starting_velocity;\n    }\n    else {\n        m_min_starting_velocity = min_starting_velocity;\n        m_max_starting_velocity = max_starting_velocity;\n    }\n    SDL_Surface *temp_surface = IMG_Load( sprite_file );\n\n    if( !temp_surface ) {\n        printf(\"failed to load image: %s\\n\", IMG_GetError() );\n        return;\n    }\n    m_sprite_texture = SDL_CreateTextureFromSurface( renderer, \n    temp_surface );\n    SDL_FreeSurface( temp_surface );\n    SDL_QueryTexture( m_sprite_texture,\n                     NULL, NULL, &m_sprite_width, &m_sprite_height );\n    m_max_particles = max_particles;\n\n    for( int i = 0; i < m_max_particles; i++ ) {\n        m_particle_pool.push_back(\n            new Particle( m_sprite_texture, particle_lifetime, \n            acceleration, alpha_fade, m_sprite_width, m_sprite_height )\n        );\n    }\n    m_max_angle = max_angle;\n    m_min_angle = min_angle;\n    m_radius = radius;\n    m_position.x = (float)x_pos;\n    m_position.y = (float)y_pos;\n    m_emission_rate = emission_rate;\n    m_emission_time_ms = 1000 / m_emission_rate;\n    m_next_emission = 0;\n}\n\nvoid Emitter::Update(int max_particles, float min_angle, float \n                     max_angle, Uint32 particle_lifetime, float \n                     acceleration, bool alpha_fade,\n                     float min_starting_velocity, float \n                     max_starting_velocity, Uint32 emission_rate, int \n                     x_pos, int y_pos, float radius ) {\n    if( min_starting_velocity > max_starting_velocity ) {\n        m_min_starting_velocity = max_starting_velocity;\n        m_max_starting_velocity = min_starting_velocity;\n    }\n    else {\n        m_min_starting_velocity = min_starting_velocity;\n        m_max_starting_velocity = max_starting_velocity;\n    }\n    m_max_particles = max_particles;\n    m_min_angle = min_angle;\n    m_max_angle = max_angle;\n    m_emission_rate = emission_rate;\n    m_position.x = (float)x_pos;\n    m_position.y = (float)y_pos;\n    m_radius = radius;\n\n    if( m_particle_pool.size() > m_max_particles ) {\n        m_particle_pool.resize( m_max_particles );\n    }\n    else if( m_max_particles > m_particle_pool.size() ) {\n        while( m_max_particles > m_particle_pool.size() ) {\n            m_particle_pool.push_back(\n                new Particle( m_sprite_texture, particle_lifetime, \n                acceleration, alpha_fade, m_sprite_width, \n                m_sprite_height )\n            );\n        }\n    }\n\n    Particle* particle;\n    std::vector<Particle*>::iterator it;\n\n    for( it = m_particle_pool.begin(); it != m_particle_pool.end(); \n         it++ ) {\n        particle = *it;\n        particle->Update( particle_lifetime, acceleration, alpha_fade );\n    }\n}\n```\n\n这两个函数都设置了`Emitter`类的属性，并基于传递给这些函数的`max_particles`值设置了粒子池。`GetFreeParticle`函数由`Move`函数调用，从当前未激活的粒子池中获取粒子。`Move`函数首先计算出它是否需要发射一个新粒子，如果需要，调用`GetFreeParticle`函数抓取一个不活动的粒子，然后使用`Emitter`的属性设置产生粒子时要使用的值。它将循环覆盖池中的所有粒子，如果粒子是活动的，它将`Move`然后`Render`该粒子:\n\n```cpp\nParticle* Emitter::GetFreeParticle() {\n    Particle* particle;\n    std::vector<Particle*>::iterator it;\n    for( it = m_particle_pool.begin(); it != m_particle_pool.end(); \n         it++ ) {\n        particle = *it;\n        if( particle->m_active == false ) {\n            return particle;\n        }\n    }\n    return NULL;\n}\n\nvoid Emitter::Move() {\n    Particle* particle;\n    std::vector<Particle*>::iterator it;\n    static int count = 0;\n    m_next_emission -= diff_time;\n    if( m_next_emission <= 0 ) {\n        m_next_emission = m_emission_time_ms;\n        particle = GetFreeParticle();\n        if( particle != NULL ) {\n            float rand_vel = (rand() %\n                (int)((m_max_starting_velocity - \n                       m_min_starting_velocity) * 1000)) / 1000.0f;\n            Point spawn_point;\n            spawn_point.x = (float)(rand() % (int)(m_radius * 1000)) / \n            1000.0;\n            Point velocity_point;\n            velocity_point.x = (float)(rand() %\n                (int)((m_max_starting_velocity + rand_vel) * 1000)) / \n                 1000.0;\n            int angle_int = (int)((m_max_angle - m_min_angle) * \n            1000.0);\n            float add_angle = (float)(rand() % angle_int) /1000.0f;\n            float angle = m_min_angle + add_angle;\n            velocity_point.Rotate(angle);\n            angle = (float)(rand() % 62832) / 10000.0;\n            spawn_point.Rotate( angle );\n            spawn_point.x += m_position.x;\n            spawn_point.y += m_position.y;\n            particle->Spawn(spawn_point.x, spawn_point.y, \n            velocity_point.x, velocity_point.y, 255.0f );\n        }\n    }\n    for( it = m_particle_pool.begin(); it != m_particle_pool.end(); \n         it++ ) {\n        particle = *it;\n        if( particle->m_active ) {\n            particle->Move();\n            particle->Render();\n        }\n    }\n}\n```\n\n我们将把这些类编译到我们的 WebAssembly 模块中，但是它们不会被用来直接与我们之前定义的 JavaScript 进行交互。为此，我们需要在一个新文件中定义一些函数，我们将在下一节中讨论。\n\n# WebAssembly 接口函数\n\n我们需要定义将与我们的 JavaScript 交互的函数。我们还需要定义一些将被我们的几个类使用的全局变量。以下是来自新`basic_particle.cpp`文件的代码:\n\n```cpp\n#include \"game.hpp\"\n#include <emscripten/bind.h>\nSDL_Window *window;\nSDL_Renderer *renderer;\nchar* fileName;\nEmitter* emitter = NULL;\nUint32 last_time = 0;\nUint32 current_time = 0;\nUint32 diff_time = 0;\nfloat delta_time = 0.0f;\nextern \"C\"\n    EMSCRIPTEN_KEEPALIVE\n    void add_emitter(char* file_name, int max_particles, float \n    min_angle, float max_angle, Uint32 particle_lifetime, float \n    acceleration, bool alpha_fade, float min_starting_velocity, float \n    kmax_starting_velocity, Uint32 emission_rate, float x_pos, float \n    y_pos, float radius) {\n        if( emitter != NULL ) {\n            delete emitter;\n        }\n        emitter = new Emitter(file_name, max_particles, min_angle, \n                              max_angle, particle_lifetime, \n                              acceleration, alpha_fade,\n                              min_starting_velocity, \n                              max_starting_velocity,\n                              emission_rate, x_pos, y_pos, radius );\n        }\nextern \"C\"\n    EMSCRIPTEN_KEEPALIVE\n    void update_emitter(int max_particles, float min_angle, float   \n    max_angle, Uint32 particle_lifetime, float acceleration, bool   \n    alpha_fade, float min_starting_velocity, float \n    max_starting_velocity, Uint32 emission_rate, float x_pos, float \n    y_pos, float radius ) {\n        if( emitter == NULL ) {\n            return;\n        }\n        emitter->Update(max_particles, min_angle, max_angle,\n                        particle_lifetime, acceleration, alpha_fade,\n                        min_starting_velocity, max_starting_velocity,\n                        emission_rate, x_pos, y_pos, radius );\n    }\n    void show_emission() {\n        current_time = SDL_GetTicks();\n        delta_time = (double)(current_time - last_time) / 1000.0;\n        diff_time = current_time - last_time;\n        last_time = current_time;\n        if( emitter == NULL ) {\n            return;\n        }\n        SDL_SetRenderDrawColor( renderer, 0, 0, 0, 255 );\n        SDL_RenderClear( renderer );\n        emitter->Move();\n        SDL_RenderPresent( renderer );\n    }\n    int main() {\n        printf(\"Enter Main\\n\");\n        SDL_Init( SDL_INIT_VIDEO );\n        int return_val = SDL_CreateWindowAndRenderer( 800, 600, 0, \n        &window, &renderer );\n        SDL_EventState(SDL_TEXTINPUT, SDL_DISABLE);\n        SDL_EventState(SDL_KEYDOWN, SDL_DISABLE);\n        SDL_EventState(SDL_KEYUP, SDL_DISABLE);\n        if( return_val != 0 ) {\n            printf(\"Error creating renderer %d: %s\\n\", return_val, \n            IMG_GetError() );\n            return 0;\n        }\n        last_time = SDL_GetTicks();\n        emscripten_set_main_loop(show_emission, 0, 0);\n        printf(\"Exit Main\\n\");\n        return 1;\n    }\n```\n\n前两个全局变量是`SDL_Window`和`SDL_Renderer`。我们需要这些作为全局对象(特别是渲染器)，以便它们可以用来将我们的纹理渲染到画布上:\n\n```cpp\nSDL_Window *window;\nSDL_Renderer *renderer;\n```\n\n之后，我们有了我们的发射器。现在，我们只支持一个发射器。在以后的版本中，我们希望有几个已经配置好的发射器:\n\n```cpp\nEmitter* emitter = NULL;\n```\n\n其余的全局变量都与跟踪毫秒(`diff_time`)帧间的时间和秒的分数项(`delta_time`)有关。`last_time`和`current_time`变量主要用于计算另外两个与时间相关的变量。下面是代码中的定义:\n\n```cpp\nUint32 last_time = 0;\nUint32 current_time = 0;\nUint32 diff_time = 0;\nfloat delta_time = 0.0f;\n```\n\n在我们定义了全局变量之后，是时候定义将与我们的 JavaScript 交互的函数了。第一个功能是`add_emitter`。这是一个简单的函数，查看是否定义了发射器，如果定义了，就删除它。然后，它创建一个新的发射器，其值是使用当时在 HTML 输入元素内部的值从 JavaScript 传递到这个函数的。函数如下所示:\n\n```cpp\nextern \"C\"\n    EMSCRIPTEN_KEEPALIVE\n    void add_emitter(char* file_name, int max_particles, float \n    min_angle, float max_angle, Uint32 particle_lifetime, float   \n    acceleration, bool alpha_fade, float min_starting_velocity, float \n    max_starting_velocity, Uint32 emission_rate, float x_pos, float \n    y_pos, float radius) {\n        if( emitter != NULL ) {\n            delete emitter;\n        }\n        emitter = new Emitter(file_name, max_particles, min_angle, \n        max_angle, particle_lifetime, acceleration, alpha_fade,\n        min_starting_velocity, max_starting_velocity,\n        emission_rate, x_pos, y_pos, radius );\n    }\n\n```\n\n您可能已经注意到`add_emitter`函数定义之前的这两行:\n\n```cpp\nextern \"C\"\n    EMSCRIPTEN_KEEPALIVE\n```\n\n我们需要这些线来防止*名称篡改*和*死码消除*。如果你以前从未听过这些术语，让我解释一下。\n\n# C++ 名称 mangling\n\n第一行`extern \"C\"`告诉编译器这是一个 C 函数，并指示它不要在该函数上使用 C++ *名称 mangling* 。如果你不熟悉 C++ 的名字 mangling，它的基础是这样的:C++ 支持函数重载。换句话说，可以有多个同名的函数具有不同的参数。C++ 将根据传递给该函数的参数调用正确的函数。由于这一功能，C++ 在编译时会对名称进行“T4”篡改，在编译过程中给每个函数一个不同的名称。因为我现在使用 C++ 并且不再使用 C，所以我希望从 JavaScript 调用的这些函数都服从这个名称 mangling 进程。`extern \"C\"`指令告诉 C++ 编译器，这些是 C 函数，请不要混淆名称，这样我就可以从我的 JavaScript 外部调用它们。\n\n# 死代码消除\n\n默认情况下，Emscripten 使用*死代码消除*从 C++ 代码中的某个地方移除您没有调用的任何函数。在大多数情况下，这是一件好事。您不希望未使用的代码占用您的 WebAssembly 模块的空间。当有一个函数需要从 JavaScript 调用，而不是从 C++ 代码内部调用时，这就产生了一个问题。Emscripten 编译器发现没有任何东西在调用这个函数，并消除了它。`EMSCRIPTEN_KEEPALIVE`告诉 Emscripten 编译器不要删除这段代码，因为您希望从外部源调用它。\n\n# 更新发射器\n\n在`add_emitter`代码之后，为外部呼叫设置的下一个功能是`update_emitter`。这个函数首先检查是否有定义的发射器，如果有，调用一个更新函数，将发射器上的所有属性更新为从 HTML 输入元素传入的值。代码如下所示:\n\n```cpp\nextern \"C\"\n    EMSCRIPTEN_KEEPALIVE\n    void update_emitter(int max_particles, float min_angle, float   \n    max_angle, Uint32 particle_lifetime, float acceleration, bool \n    alpha_fade, float min_starting_velocity, float \n    max_starting_velocity, Uint32 emission_rate, float x_pos, float \n    y_pos, float radius ) {\n        if( emitter == NULL ) {\n            return;\n        }\n        emitter->Update(max_particles, min_angle, max_angle,\n                        particle_lifetime, acceleration, alpha_fade,\n                        min_starting_velocity, max_starting_velocity,\n                        emission_rate, x_pos, y_pos, radius );\n    }\n```\n\n# 循环功能\n\n下一个功能`show_emission`，是如果这个应用是一个游戏，我们的游戏循环的功能。该函数为渲染的每一帧调用，负责设置定时器值，准备渲染我们的 SDL，并调用发射器`Move`函数，该函数将移动和渲染我们粒子系统中的所有粒子:\n\n```cpp\nvoid show_emission() {\n    current_time = SDL_GetTicks();\n    delta_time = (double)(current_time - last_time) / 1000.0;\n    diff_time = current_time - last_time;\n    last_time = current_time;\n\n    if( emitter == NULL ) {\n        return;\n    }\n    SDL_SetRenderDrawColor( renderer, 0, 0, 0, 255 );\n    SDL_RenderClear( renderer );\n    emitter->Move();\n    SDL_RenderPresent( renderer );\n}\n```\n\n前几行计算`delta_time`和`diff_time`全局变量，这些变量被粒子用来根据帧速率调整粒子的运动:\n\n```cpp\ncurrent_time = SDL_GetTicks();\ndelta_time = (double)(current_time - last_time) / 1000.0;\ndiff_time = current_time - last_time;\nlast_time = current_time;\n```\n\n如果发射器尚未设置，我们不想渲染任何东西，因此返回:\n\n```cpp\nif( emitter == NULL ) {\n    return;\n}\n```\n\n如果发射器存在，我们需要使用黑色清除渲染器:\n\n```cpp\nSDL_SetRenderDrawColor( renderer, 0, 0, 0, 255 );\nSDL_RenderClear( renderer );\n```\n\n之后，我们调用发射器`Move`函数，该函数既移动所有粒子，又将子画面纹理复制到渲染器中的适当位置。然后，我们调用`SDL_RenderPresent`函数，渲染到 HTML 画布元素:\n\n```cpp\nemitter->Move();\nSDL_RenderPresent( renderer );\n```\n\n# 初始化\n\n最后一个函数是`main`函数，在加载 WebAssembly 模块时自动调用:\n\n```cpp\nint main() {\n    SDL_Init( SDL_INIT_VIDEO );\n    int return_val = SDL_CreateWindowAndRenderer( 800, 600, 0, &window, \n                                                  &renderer );\n    if( return_val != 0 ) {\n        printf(\"Error creating renderer %d: %s\\n\", return_val, \n                IMG_GetError() );\n        return 0;\n    }\n    SDL_EventState(SDL_TEXTINPUT, SDL_DISABLE);\n    SDL_EventState(SDL_KEYDOWN, SDL_DISABLE);\n    SDL_EventState(SDL_KEYUP, SDL_DISABLE);\n    last_time = SDL_GetTicks();\n    emscripten_set_main_loop(show_emission, 0, 0);\n    return 1;\n}\n```\n\n前两行初始化了我们的 SDL:\n\n```cpp\nSDL_Init( SDL_INIT_VIDEO );\nint return_val = SDL_CreateWindowAndRenderer( 800, 600, 0, &window, \n                                              &renderer );\n```\n\n之后，接下来的几行用于禁用 SDL 文本输入和键盘事件。这些行阻止 SDL 捕获我们需要在 HTML 元素中设置输入值的键盘输入。在大多数游戏中，我们不想要这些线，因为我们更喜欢捕捉这些事件，这样我们就可以从我们的 WebAssembly 模块中管理我们的游戏输入。但是，如果我们希望我们的应用能够工作，并且希望我们的用户能够更改我们的 HTML 输入，那么我们的代码中必须有以下几行:\n\n```cpp\nSDL_EventState(SDL_TEXTINPUT, SDL_DISABLE);\nSDL_EventState(SDL_KEYDOWN, SDL_DISABLE);\nSDL_EventState(SDL_KEYUP, SDL_DISABLE);\n```\n\n下一行获取`last_time`全局变量的起始时钟值:\n\n```cpp\nlast_time = SDL_GetTicks();\n```\n\n这个函数在返回之前的最后一行用于设置我们的循环函数。我们的循环函数将在每次渲染帧时调用:\n\n```cpp\nemscripten_set_main_loop(show_emission, 0, 0);\n```\n\n在下一节中，我们将编译并测试发射器配置工具的早期版本。\n\n# 编译和测试粒子发射器\n\n哇，代码真多。好了，现在我们已经在粒子发射器配置工具中拥有了我们需要的一切，我们需要花时间来编译和测试它。在我们测试这个版本之后，我们可以使用对 em++ 的同样调用来测试我们将在下一节开始构建的高级版本。\n\n在命令行运行以下命令:\n\n```cpp\nem++ emitter.cpp particle.cpp point.cpp basic_particle.cpp -o particle.html -std=c++ 17 --shell-file basic_particle_shell.html -s NO_EXIT_RUNTIME=1 -s USE_WEBGL2=1 -s USE_SDL=2 -s USE_SDL_IMAGE=2 -s SDL2_IMAGE_FORMATS=[\"png\"] -s NO_EXIT_RUNTIME=1 -s EXPORTED_FUNCTIONS=\"['_add_emitter', '_update_emitter', '_main']\" -s EXTRA_EXPORTED_RUNTIME_METHODS=\"['cwrap', 'ccall']\" -s FORCE_FILESYSTEM=1\n```\n\n您的粒子发射器配置工具应该如下所示:\n\n![](img/c029c870-aef9-443f-8eb4-257c5e87a519.png)\n\nFigure 8.2: Screenshot of the particle system configuration tool Do not forget that you must run WebAssembly apps using a web server, or with `emrun`. If you would like to run your WebAssembly app using `emrun`, you must compile it with the `--emrun` flag. The web browser requires a web server to stream the We1bAssembly module.  If you attempt to open an HTML page that uses WebAssembly in a browser directly from your hard drive, that WebAssembly module will not load.\n\n用这个界面上传一个`.png`图像文件，在左边的字段里玩我们有的数字。我们还没有足够的值来制作一个优秀的粒子发射器，但是你可以用我们目前拥有的东西来感受一下基础知识。\n\n# 摘要\n\n在本章中，我们学习了如何创建一个基本的粒子发射器配置工具。我们介绍了在应用启动时没有文件加载到虚拟文件系统中时，如何强制 Emscripten 创建虚拟文件系统。我们学习了如何将图像从用户的计算机加载到浏览器的虚拟文件系统中，并添加了允许我们上传`.png`图像文件的功能。我们介绍了 SVG 的一些基础知识，讨论了矢量图形和光栅图形的区别，并学习了如何使用 SVG 为我们的配置工具绘制饼图。我们介绍了本章中有用的一些基本三角学，在后面的章节中会变得更加有用。我们创建了一个新的与我们的网络组件交互的 HTML 外壳文件，来帮助我们为我们的游戏配置一个新的粒子系统。我们在一个用于发射器的 WebAssembly 模块中创建了一个`Point`、`Particle`和`Emitter`类，我们最终将在游戏中使用它。最后，我们学习了 C++ 名称篡改、死代码消除，以及编写 Emscripten 代码时必须避免的情况。\n\n在下一章中，我们将改进我们的*粒子发射器配置工具*。在这一章的最后，我们将使用它来配置我们游戏中的效果，如爆炸、太阳耀斑和宇宙飞船排气羽流。该工具可以用来玩不同的效果，并在我们将该效果添加到游戏中之前了解它们的外观。最后，我们将使用我们在配置工具中使用的值，并将其用作在我们的游戏中配置粒子效果的起点。"
  },
  {
    "path": "docs/handson-game-dev-wasm/09.md",
    "content": "# 九、改进的粒子系统\n\n我们在上一章开发的*粒子系统*是一个很好的开始，但是你可以用它创造的效果相当平淡。我们的粒子不会旋转或缩放，也不会被动画化，并且随着时间的推移，它们的外观相对一致。\n\nFor this chapter, you will need to include several images in your build to make this project work. Make sure that you include the `/Chapter09/sprites/` folder from this project's GitHub repository. If you would like to build the particle system tool from GitHub, the source for the tool is located in the `/Chapter09/advanced-particle-tool/` folder. If you haven't downloaded the GitHub project yet, you can get it online here: [https://github.com/PacktPublishing/Hands-On-Game-Develop](https://github.com/PacktPublishing/Hands-On-Game-Development-with-WebAssembly).\n\n如果我们想从我们的粒子系统中得到最大的好处，我们需要给它增加更多的特性。在本章中，我们将添加以下附加功能:\n\n*   粒子在其寿命期间的尺度\n*   粒子旋转\n*   动画粒子\n*   颜色随时间变化\n*   支持粒子爆发\n*   支持环形和非环形发射器\n\n# 修改我们的 HTML 外壳文件\n\n我们需要做的第一件事是向 HTML shell 文件中添加一些新的输入。我们将把`basic_particle_shell.html`文件复制到一个新的 shell 文件中，我们称之为`advanced_particle_shell.html`。我们将在原始容器和`canvas`元素之间的外壳文件的 HTML 部分添加第二个容器类`div`元素和许多新的输入。以下是新容器元素的外观:\n\n```cpp\n<div class=\"container\">\n<div class=\"empty_box\">&nbsp;</div><br/>\n<span class=\"label\">min start scale:</span>\n<input type=\"number\" id=\"min_starting_scale\" max=\"9.9\" min=\"0.1\" step=\"0.1\" value=\"1.0\" class=\"em_input\"><br/>\n<span class=\"label\">max start scale:</span>\n<input type=\"number\" id=\"max_starting_scale\" max=\"10.0\" min=\"0.2\" step=\"0.1\" value=\"2.0\" class=\"em_input\"><br/>\n<span class=\"label\">min end scale:</span>\n<input type=\"number\" id=\"min_end_scale\" max=\"9.9\" min=\"0.1\" step=\"0.1\" value=\"1.0\" class=\"em_input\">\n<br/>\n<span class=\"label\">max end scale:</span>\n<input type=\"number\" id=\"max_end_scale\" max=\"10.0\" min=\"0.2\" step=\"0.1\" value=\"2.0\" class=\"em_input\">\n<br/>\n<span class=\"label\">start color:</span>\n<input type=\"color\" id=\"start_color\" value=\"#ffffff\" class=\"color_input\"><br/>\n<span class=\"label\">end color:</span>\n<input type=\"color\" id=\"end_color\" value=\"#ffffff\" class=\"color_input\"><br/>\n<span class=\"label\">burst time pct:</span>\n<input type=\"number\" id=\"burst_time\" max=\"1.0\" min=\"0.0\" step=\"0.05\" value=\"0.0\" class=\"em_input\">\n<br/>\n<span class=\"label\">burst particles:</span>\n<input type=\"number\" id=\"burst_particles\" max=\"100\" min=\"0\" step=\"1\" value=\"0\" class=\"em_input\">\n<br/>\n<label class=\"ccontainer\"><span class=\"label\">loop:</span>\n    <input type=\"checkbox\" id=\"loop\" checked=\"checked\">\n    <span class=\"checkmark\"></span>\n</label>\n<br/>\n<label class=\"ccontainer\"><span class=\"label\">align rotation:</span>\n    <input type=\"checkbox\" id=\"align_rotation\" checked=\"checked\">\n    <span class=\"checkmark\"></span>\n</label>\n<br/>\n<span class=\"label\">emit time ms:</span>\n<input type=\"number\" id=\"emit_time\" max=\"10000\" min=\"100\" step=\"100\" value=\"1000\" class=\"em_input\">\n<br/>\n<span class=\"label\">animation frames:</span>\n<input type=\"number\" id=\"animation_frames\" max=\"64\" min=\"1\" step=\"1\" value=\"1\" class=\"em_input\">\n<br/>\n<div class=\"input_box\">\n<button id=\"update_btn\" class=\"em_button\" onclick=\"UpdateClick()\">Update Emitter</button>\n</div>\n</div>\n```\n\n# 缩放值\n\n缩放精灵意味着将精灵的大小修改为其原始大小的倍数。例如，如果我们按缩放值`2.0`缩放 16 x 16 的子画面，子画面将作为 32 x 32 的图像渲染到画布上。这个新容器从四个输入元素及其标签开始，它们告诉粒子系统如何在粒子的生命周期内缩放粒子。`min_starting_scale`和`max_starting_scale`元素是粒子的起始范围刻度。如果您希望粒子总是以`1.0`的比例开始(1 比 1 的比例与`.png`图像大小)，您应该将`1.0`放在这两个字段中。实际的起始刻度值将是随机选择的值，介于您在这些字段中输入的两个值之间。我们没有在这个界面中添加任何检查来验证`max`是否大于`min`，所以请确保`max`与`min`值相同或更大，否则会损坏发射器。接下来的两个`input`元素是`min_end_scale`和`max_end_scale`。与起始刻度值一样，实际的结束刻度将是一个随机选择的值，介于我们在这些字段中输入的两个值之间。在粒子寿命的任何给定点，它都有一个标度，该标度是在分配给该粒子寿命开始的标度值和结束时的标度值之间的插值。所以，如果我以`1.0`的刻度值开始，以`3.0`的刻度值结束，当粒子寿命过半时，粒子的刻度值就是`2.0`。\n\n以下是这些元素在 HTML 文件中的样子:\n\n```cpp\n<span class=\"label\">min start scale:</span>\n<input type=\"number\" id=\"min_starting_scale\" max=\"9.9\" min=\"0.1\" step=\"0.1\" value=\"1.0\" class=\"em_input\"><br/>\n<span class=\"label\">max start scale:</span>\n<input type=\"number\" id=\"max_starting_scale\" max=\"10.0\" min=\"0.2\" step=\"0.1\" value=\"2.0\" class=\"em_input\"><br/>\n<span class=\"label\">min end scale:</span>\n<input type=\"number\" id=\"min_end_scale\" max=\"9.9\" min=\"0.1\" step=\"0.1\" value=\"1.0\" class=\"em_input\">\n<br/>\n<span class=\"label\">max end scale:</span>\n<input type=\"number\" id=\"max_end_scale\" max=\"10.0\" min=\"0.2\" step=\"0.1\" value=\"2.0\" class=\"em_input\">\n<br/>\n```\n\n# 混色值\n\nSDL 有一个名为`SDL_SetTextureColorMod`的功能，能够修改纹理的红色、绿色和蓝色通道。此功能只能减少颜色通道值，因此在灰度图像上使用这些值效果最佳。HTML 中接下来的两个输入是`start_color`和`end_color`。这些值将用于修改粒子在其寿命期间的颜色通道。每个颜色通道(红色、绿色和蓝色)都在粒子的生命周期内进行插值。\n\n以下是这些元素在 HTML 文件中的样子:\n\n```cpp\n<span class=\"label\">start color:</span>\n<input type=\"color\" id=\"start_color\" value=\"#ffffff\" class=\"color_input\"><br/>\n<span class=\"label\">end color:</span>\n<input type=\"color\" id=\"end_color\" value=\"#ffffff\" class=\"color_input\"><br/>\n```\n\n# 粒子爆发\n\n到目前为止，我们研究过的粒子系统已经发出了一致的粒子流。我们可能希望在我们的粒子系统的生命周期内有一个时间点，在这个时间点上，一束粒子同时被发射出去。接下来的两个输入元素是`burst_time`和`burst_particles`。`burst_time`元素允许从`0.0`到`1.0`的值。这个数字代表粒子发射器寿命中爆发发生的部分。`0.0`的值意味着爆发将发生在发射器生命周期的最开始，`1.0`将发生在最末端，`0.5`将发生在中间。在`burst_time`元素之后是`burst_particles`元素。该元素包含爆发中发射的粒子数。在调整它使其成为一个大数字之前，请确保您将`max_particles`输入元素设置为一个可以容纳突发的值。例如，如果您有一个每秒发射`20`粒子的粒子发射器，并且您的最大粒子数也是`20`粒子，则添加任何大小的爆发都不会引起注意，因为粒子池中没有足够的非活动粒子供爆发使用。\n\n以下是这些元素在 HTML 文件中的样子:\n\n```cpp\n<span class=\"label\">burst time pct:</span>\n<input type=\"number\" id=\"burst_time\" max=\"1.0\" min=\"0.0\" step=\"0.05\" value=\"0.0\" class=\"em_input\">\n<br/>\n<span class=\"label\">burst particles:</span>\n<input type=\"number\" id=\"burst_particles\" max=\"100\" min=\"0\" step=\"1\" value=\"0\" class=\"em_input\">\n<br/>\n```\n\n# 循环发射器\n\n一些发射器执行一段固定的时间，然后在该时间到期时停止。这种发射器的一个例子是爆炸。一旦爆炸效果完成，我们希望它结束。一种不同类型的发射器可能会循环，它将继续执行，直到其他代码停止发射器。这种发射器的一个例子是我们宇宙飞船的发动机废气。只要我们的宇宙飞船在加速，我们就希望看到一串粒子从它的后面发射出来。HTML 中的下一个元素是循环复选框元素。如果点击，发射器将继续发射，甚至在其寿命结束后。如果有一个脉冲与此发射器相关联，则每次发射器通过其回路的该部分时，该脉冲都会出现。\n\n以下是输入元素在 HTML 中的样子:\n\n```cpp\n<label class=\"ccontainer\"><span class=\"label\">loop:</span>\n<input type=\"checkbox\" id=\"loop\" checked=\"checked\">\n<span class=\"checkmark\"></span>\n</label>\n<br/>\n```\n\n# 对齐粒子旋转\n\n*旋转*可以提升很多粒子效果。我们被迫挑选我们希望在项目中用于粒子系统的值，因为坦率地说，我可以写一本关于粒子系统的书。我们将有一个标志，允许用户选择粒子系统是否将其旋转与发射速度矢量对齐，而不是像前面对粒子尺度所做的那样有旋转值范围。我发现这是一个令人愉快的效果。用户将通过`id=\"align_rotation\"`复选框做出该决定。\n\n以下是 HTML 代码的样子:\n\n```cpp\n<label class=\"ccontainer\"><span class=\"label\">align rotation:</span>\n <input type=\"checkbox\" id=\"align_rotation\" checked=\"checked\">\n <span class=\"checkmark\"></span>\n </label>\n <br/>\n```\n\n# 发射时间\n\n*发射时间*是我们的粒子发射器停止运行之前运行的时间(以毫秒为单位)，如果用户勾选了循环复选框，则为循环。如果粒子系统循环，该值仅在具有爆发的粒子系统中可见。这将导致粒子系统每次通过环路时都会发生爆发。\n\nHTML 代码如下:\n\n```cpp\n<span class=\"label\">emit time ms:</span>\n<input type=\"number\" id=\"emit_time\" max=\"10000\" min=\"100\" step=\"100\" value=\"1000\" class=\"em_input\"><br/>\n```\n\n# 动画帧\n\n如果我们想创建一个多帧动画的粒子，我们可以在这里添加帧数。该功能采用*水平条状精灵表*，将加载的图像文件均匀地分布在 *x* 轴上。当该值为`1`时，因为只有一帧，所以没有动画。动画的帧时间将平均分配给单个粒子的生存时间。换句话说，如果你有一个十帧动画，粒子寿命是 1000 毫秒，动画的每一帧将显示 100 毫秒(1000/10)。\n\n以下是 HTML 元素:\n\n```cpp\n<span class=\"label\">animation frames:</span>\n<input type=\"number\" id=\"animation_frames\" max=\"64\" min=\"1\" step=\"1\" value=\"1\" class=\"em_input\"><br/>\n```\n\n现在我们已经定义了我们的 HTML，让我们来看看代码的 JavaScript 部分。\n\n# 修改 JavaScript\n\n我们正在创建的工具在游戏之外运行，我们已经在几个章节中工作了。正因为如此，我们正在开发一个新的 HTML shell 文件，我们将编写大量的 JavaScript 来将我们的用户界面与我们稍后将放入游戏中的 WebAssembly 类集成在一起。让我们花点时间浏览一下我们需要添加到新的 HTML shell 文件中的所有 JavaScript 函数。\n\n# JavaScript 更新点击功能\n\n修改完 HTML 之后，接下来我们需要做的就是修改`UpdateClick()` JavaScript 函数，让它从 HTML 元素中抓取新的值，并将这些值传递给`update_emitter`的`Module.ccall`函数调用。\n\n以下是全新版本的`UpdateClick`功能的全部内容:\n\n```cpp\nfunction UpdateClick() {\n    if( ready == false || image_added == false ) {\n        return;\n    }\n    var max_particles = Number(document.getElementById\n                        (\"max_particles\").value);\n    var min_angle = Number(document.getElementById\n                    (\"min_angle\").value) / 180 * Math.PI;\n    var max_angle = Number(document.getElementById\n                    (\"max_angle\").value) / 180 * Math.PI\n    var particle_lifetime = Number(document.getElementById\n                            (\"lifetime\").value);\n    var acceleration = Number(document.getElementById\n                       (\"acceleration\").value);\n    var alpha_fade = Boolean(document.getElementById\n                     (\"alpha_fade\").checked);\n    var emission_rate = Number(document.getElementById\n                        (\"emission_rate\").value);\n    var x_pos = Number(document.getElementById\n                (\"x_pos\").value);\n    var y_pos = Number(document.getElementById\n                (\"y_pos\").value);\n    var radius = Number(document.getElementById\n                 (\"radius\").value);\n    var min_starting_velocity = Number(document.getElementById\n                                (\"min_starting_vel\").value);\n    var max_starting_velocity = Number(document.getElementById\n                                (\"max_starting_vel\").value);\n\n    /* NEW INPUT PARAMETERS */\n    var min_start_scale = Number(document.getElementById\n                          (\"min_starting_scale\").value);\n    var max_start_scale = Number(document.getElementById\n                          (\"max_starting_scale\").value);\n    var min_end_scale = Number(document.getElementById\n                        (\"min_end_scale\").value);\n    var max_end_scale = Number(document.getElementById\n                        (\"max_end_scale\").value);\n    var start_color_str = document.getElementById\n                          (\"start_color\").value.substr(1, 7);\n    var start_color = parseInt(start_color_str, 16);\n    var end_color_str = document.getElementById\n                        (\"end_color\").value.substr(1, 7);\n    var end_color = parseInt(end_color_str, 16);\n    var burst_time = Number(document.getElementById\n                     (\"burst_time\").value);\n    var burst_particles = Number(document.getElementById\n                          (\"burst_particles\").value);\n    var loop = Boolean(document.getElementById\n               (\"loop\").checked);\n    var align_rotation = Boolean(document.getElementById\n                         (\"align_rotation\").checked);\n    var emit_time = Number(document.getElementById\n                    (\"emit_time\").value);\n    var animation_frames = Number(document.getElementById\n                           (\"animation_frames\").value);\n\n    Module.ccall('update_emitter', 'undefined', [\"number\", \"number\", \n    \"number\", \"number\", \"number\", \"bool\", \"number\", \"number\",\n    \"number\", \"number\", \"number\", \"number\",\n    /* new parameters */\n    \"number\", \"number\", \"number\", \"number\", \"number\", \"number\", \n    \"number\", \"number\", \"bool\", \"bool\", \"number\"],\n    [max_particles, min_angle, max_angle, particle_lifetime, \n    acceleration, alpha_fade, min_starting_velocity, \n    max_starting_velocity, emission_rate, x_pos, y_pos, radius,\n    /* new parameters */\n    min_start_scale, max_start_scale, min_end_scale, max_end_scale,\n    start_color, end_color, burst_time, burst_particles,    \n    loop, align_rotation, emit_time, animation_frames]);\n    }\n```\n\n如您所见，我们在这个 JavaScript 函数中添加了新的局部变量，它将存储我们从新的 HTML 元素中获取的值。检索缩放值并强制将其转换为数字以传递到`update_emitter`现在应该很熟悉了。这是代码:\n\n```cpp\nvar min_start_scale = Number(document.getElementById\n                      (\"min_starting_scale\").value);\nvar max_start_scale = Number(document.getElementById\n                      (\"max_starting_scale\").value);\nvar min_end_scale = Number(document.getElementById\n                    (\"min_end_scale\").value);\nvar max_end_scale = Number(document.getElementById\n                    (\"max_end_scale\").value);\n```\n\n# 强制颜色值\n\n在 JavaScript 中，变量强制是将一种变量类型转换成另一种变量类型的过程。因为 JavaScript 是弱类型语言，强制与类型转换有点不同，后者类似于 C 和 C++ 等强类型语言中的变量强制。\n\n将我们的颜色值强制转换为`Integer`值的过程是一个两步走的过程。这些元素中的值是以`*#*`字符开始的字符串，后跟一个六位十六进制数。我们需要做的第一件事是删除那个开始的`#`字符，因为它会阻止我们将那个字符串解析成一个整数。我们通过一个简单的`substr`来获取元素内部值的子字符串(字符串的一部分)。\n\n以下是`start_color`的情况:\n\n```cpp\nvar start_color_str = document.getElementById\n                      (\"start_color\").value.substr(1, 7);\n```\n\n我们知道字符串总是七个字符长，但我们只想要最后六个字符。我们现在有了起始颜色的十六进制表示，但它仍然是一个字符串变量。现在，我们需要将此强制转换为一个`Integer`值，并且我们必须告诉`parseInt`函数使用基数 16(十六进制)，因此我们将把值`16`作为第二个参数传递到`parseInt`中:\n\n```cpp\nvar start_color = parseInt(start_color_str, 16);\n```\n\n现在我们已经将`start_color`强制转换为一个整数，我们将对`end_color`进行同样的操作:\n\n```cpp\nvar end_color_str = document.getElementById\n                    (\"end_color\").value.substr(1, 7);\nvar end_color = parseInt(end_color_str, 16);\n```\n\n# 附加可变胁迫\n\n在`start_color`和`end_color`胁迫之后，剩下的我们必须执行的胁迫应该会觉得熟悉。我们将`burst_time`、`burst_particles`、`emit_time`和`animation_frames`中的值强制转换为`Number`变量。我们将`loop`和`align_rotation`的校验值强制转换为布尔变量。\n\n以下是强制代码的剩余部分:\n\n```cpp\nvar burst_time = Number(document.getElementById\n                 (\"burst_time\").value);\nvar burst_particles = Number(document.getElementById\n                      (\"burst_particles\").value);\nvar loop = Boolean(document.getElementById\n           (\"loop\").checked);\nvar align_rotation = Boolean(document.getElementById\n                     (\"align_rotation\").checked);\nvar emit_time = Number(document.getElementById\n                (\"emit_time\").value);\nvar animation_frames = Number(document.getElementById\n                       (\"animation_frames\").value);\n```\n\n最后，我们需要将变量类型和新变量添加到我们的 WebAssembly 模块中对`update_emitter`的`Module.ccall`调用中:\n\n```cpp\nModule.ccall('update_emitter', 'undefined', [\"number\", \"number\",                                       \"number\", \"number\", \"number\", \"bool\",\n                                  \"number\", \"number\", \"number\",                                           \"number\", \"number\",\"number\",\n                                            /* new parameters */\n                                             \"number\", \"number\",\n                                             \"number\", \"number\",\n                                             \"number\", \"number\",\n                                             \"number\", \"number\",\n                                             \"bool\", \"bool\", \"number\"],\n                                            [max_particles, min_angle, \n                                             max_angle,\n                                             particle_lifetime,         \n                                             acceleration, alpha_fade,\n                                             min_starting_velocity, \n                                             max_starting_velocity,\n                                             emission_rate, x_pos, \n                                             y_pos, radius,\n                                            /* new parameters */\n                                             min_start_scale,   \n                                             max_start_scale,\n                                             min_end_scale, \n                                             max_end_scale,\n                                             start_color, end_color,\n                                             burst_time, \n                                             burst_particles,\n                                             loop, align_rotation, \n                                             emit_time,\n                                             animation_frames\n                                         ]);\n```\n\n# 修改句柄文件功能\n\n我们需要对 HTML shell 文件进行的最后一项更改是对`handleFiles`函数的修改。这些修改有效地反映了`UpdateClick`功能的变化。当您遍历代码时，您将看到相同的强制在`handleFiles`内部复制，并且`Module.ccall`到`add_emitter`将使用相同的新参数类型和参数进行更新。以下是最新版本`handleFiles`功能的代码:\n\n```cpp\nfunction handleFiles(files) {\n    var file_count = 0;\n    for (var i = 0; i < files.length; i++) {\n        if (files[i].type.match(/image.png/)) {\n            var file = files[i]; \n            var file_name = file.name;\n            var fr = new FileReader();\n            fr.onload = function (file) {\n                var data = new Uint8Array(fr.result);\n                Module.FS_createDataFile('/', file_name, data, true, true, \n                true);\n                var max_particles = Number(document.getElementById\n                                    (\"max_particles\").value);\n                var min_angle = Number(document.getElementById\n                                (\"min_angle\").value) / 180 * Math.PI;\n                var max_angle = Number(document.getElementById\n                                (\"max_angle\").value) / 180 * Math.PI\n                var particle_lifetime = Number(document.getElementById\n                                        (\"lifetime\").value);\n                var acceleration = Number(document.getElementById\n                                   (\"acceleration\").value);\n                var alpha_fade = Boolean(document.getElementById\n                                 (\"alpha_fade\").checked);\n                var emission_rate = Number(document.getElementById\n                                    (\"emission_rate\").value);\n                var x_pos = Number(document.getElementById\n                                  (\"x_pos\").value);\n                var y_pos = Number(document.getElementById\n                                  (\"y_pos\").value);\n                var radius = Number(document.getElementById\n                                   (\"radius\").value);\n                var min_starting_velocity = Number(document.getElementById\n                                            (\"min_starting_vel\").value);\n                var max_starting_velocity = Number(document.getElementById\n                                            (\"max_starting_vel\").value);\n\n                /* NEW INPUT PARAMETERS */\n                var min_start_scale = Number(document.getElementById\n                                      (\"min_starting_scale\").value);\n                var max_start_scale = Number(document.getElementById\n                                      (\"max_starting_scale\").value);\n                var min_end_scale = Number(document.getElementById\n                                    (\"min_end_scale\").value);\n                var max_end_scale = Number(document.getElementById\n                                    (\"max_end_scale\").value);\n                var start_color_str = document.getElementById\n                                     (\"start_color\").value.substr(1, 7);\n                var start_color = parseInt(start_color_str, 16);\n                var end_color_str = document.getElementById\n                                    (\"end_color\").value.substr(1, 7);\n                var end_color = parseInt(end_color_str, 16);\n                var burst_time = Number(document.getElementById\n                                 (\"burst_time\").value);\n                var burst_particles = Number(document.getElementById\n                                      (\"burst_particles\").value);\n                var loop = Boolean(document.getElementById\n                           (\"loop\").checked);\n                var align_rotation = Boolean(document.getElementById \n                                     (\"align_rotation\").checked);\n                var emit_time = Number(document.getElementById\n                                (\"emit_time\").value);\n                var animation_frames = Number(document.getElementById\n                                       (\"animation_frames\").value);\n\n                Module.ccall('add_emitter', 'undefined', \n                [\"string\",\"number\", \"number\", \"number\", \n                \"number\",\"number\",\"bool\",\"number\",\"number\",\n                \"number\", \"number\", \"number\",\"number\", \n                /* new parameters */ \n                \"number\", \"number\", \"number\",\n                \"number\", \"number\", \"number\", \"number\", \n                \"number\",\"bool\", \"bool\", \"number\"],\n                    file_name,max_particles,min_angle,max_angle,\n                    particle_lifetime,acceleration,alpha_fade,\n                    min_starting_velocity,max_starting_velocity,\n                    emission_rate, x_pos,y_pos,radius,\n                    /* new parameters */ \n                    min_start_scale,max_start_scale,min_end_scale, \n                    max_end_scale,start_color,end_color,\n                    burst_time,burst_particles,loop,\n                    align_rotation,emit_time,animation_frames ]);\n                image_added = true;\n            };\n            fr.readAsArrayBuffer(files[i]); }}}\n```\n\n现在我们已经有了 JavaScript 代码，我们可以开始对 WebAssembly 模块进行更改了。\n\n# 修改粒子类\n\n现在我们已经将更改添加到了我们的 HTML shell 文件中，我们需要对我们的 WebAssembly 模块进行一些更改，以支持这些新参数。我们将从底层开始，从`Particle`班开始。这个类不仅对我们正在构建的设计粒子系统的工具有用，而且它是少数几个类之一，一旦我们完成了它，我们将能够进入我们的游戏，允许我们添加一些漂亮的效果。\n\n以下是`game.hpp`文件中的粒子类定义:\n\n```cpp\nclass Particle {\n    public:\n        bool m_active;\n        bool m_alpha_fade;\n        bool m_color_mod;\n        bool m_align_rotation;\n        float m_rotation;\n\n        Uint8 m_start_red;\n        Uint8 m_start_green;\n        Uint8 m_start_blue;\n\n        Uint8 m_end_red;\n        Uint8 m_end_green;\n        Uint8 m_end_blue;\n\n        Uint8 m_current_red;\n        Uint8 m_current_green;\n        Uint8 m_current_blue;\n\n        SDL_Texture *m_sprite_texture;\n        int m_ttl;\n\n        Uint32 m_life_time;\n        Uint32 m_animation_frames;\n        Uint32 m_current_frame;\n        Uint32 m_next_frame_ms;\n\n        float m_acceleration;\n        float m_alpha;\n        float m_width;\n        float m_height;\n        float m_start_scale;\n        float m_end_scale;\n        float m_current_scale;\n\n        Point m_position;\n        Point m_velocity;\n\n        SDL_Rect m_dest = {.x = 0, .y = 0, .w = 0, .h = 0 };\n        SDL_Rect m_src = {.x = 0, .y = 0, .w = 0, .h = 0 };\n\n        Particle( SDL_Texture *sprite, Uint32 life_time, float \n        acceleration,\n                    bool alpha_fade, int width, int height, bool \n                    align_rotation,\n                    Uint32 start_color,\n                    Uint32 end_color,\n                    Uint32 animation_frames );\n        void Update( Uint32 life_time, float acceleration,\n                     bool alpha_fade, bool align_rotation,\n                     Uint32 start_color, Uint32 end_color,\n                     Uint32 animation_frames );\n\n        void Spawn( float x, float y, float velocity_x, float \n                velocity_y,\n                    float start_scale, float end_scale, float rotation );\n\n        void Move();\n        void Render();\n};\n```\n\n# 新属性\n\n我们将遍历添加到`Particle`类定义中的新属性，并简要讨论每个新属性的作用。我们添加的第一个属性是`bool m_color_mod`。在我们的 HTML 中，我们没有这个值的复选框，所以你可能想知道为什么这里有一个。原因是性能。如果用户不想修改颜色，打电话到`SDL_SetTextureColorMod`是浪费。如果我们有两个白色值传递到`Particle`对象中，则不需要插值或调用来修改该值。我们可以每次检查开始和结束颜色，看看它们的值是否是`0xffffff`，但我觉得添加这个标志会使检查更加清晰。\n\n# 对齐旋转属性\n\n接下来的`m_align_rotation`标志就是我们从复选框传入的标志。如果该值为`true`，粒子将自身旋转以指向其移动的方向。`m_rotation`浮点变量紧随其后。保存粒子角度的属性变量将根据粒子移动的方向进行旋转。下面是这些值在我们的代码中的样子:\n\n```cpp\nbool m_align_rotation;\nfloat m_rotation;\n```\n\n# 颜色属性\n\n我前面提到的颜色 mod 标志使下一组值的检查变得更加容易。我们的十六进制颜色值表示 HTML 中的红色、绿色和蓝色值，需要作为一个整数传入，这样它就可以分解成三个 8 位通道。下面是这些 8 位颜色变量在代码中的样子:\n\n```cpp\nUint8 m_start_red;\nUint8 m_start_green;\nUint8 m_start_blue;\n\nUint8 m_end_red;\nUint8 m_end_green;\nUint8 m_end_blue;\n\nUint8 m_current_red;\nUint8 m_current_green;\nUint8 m_current_blue;\n```\n\n你会注意到这些都是用`Uint8`声明的 8 位无符号整数变量。当 SDL 执行颜色修改时，它不会将 RGB 值作为单个变量；相反，它将值分解为三个 8 位变量，代表每个单独的通道。`m_start_(color)`变量和`m_end_(color)`变量将基于粒子寿命进行插值，以获得`m_current_(color)`变量，当我们进行颜色修改时，该变量将作为通道传递到 SDL。因为我们将这些值作为一个单一的颜色变量从 JavaScript 传入，`Particle`构造函数和`Update`函数将需要执行位操作来设置这些单独的通道变量。\n\n# 动画属性\n\n下一组新属性都与我们`Particle`中的新帧动画功能相关。下面是代码中的这些属性:\n\n```cpp\nUint32 m_animation_frames;\nUint32 m_current_frame;\nUint32 m_next_frame_ms;\n```\n\n第一个属性`m_animation_frames`是从 JavaScript 间接传递的值。它告诉`Particle`类，当它将纹理渲染到画布上时，精灵纹理中有多少帧。第二个属性`m_current_frame`，被`Particle`类用来跟踪当前应该渲染哪个帧。最后一个属性变量`m_next_frame_ms`，告诉粒子在必须增加当前帧以显示序列中的下一帧之前还剩下多少毫秒。\n\n# 大小和比例属性\n\n下一批属性与我们粒子的大小和尺度有关。在这个代码的前一个版本中，我们处理了`m_dest`矩形的宽度和高度。这不再实用，因为这个矩形的宽度和高度(`w`和`h`)属性需要修改，以适应我们当前的比例。下面是代码中出现的新变量:\n\n```cpp\nfloat m_width;\nfloat m_height;\n\nfloat m_start_scale;\nfloat m_end_scale;\nfloat m_current_scale;\n```\n\n现在需要`m_width`和`m_height`属性来跟踪粒子的原始宽度和高度，它们还没有被比例调整。\n\n`m_start_scale`和`m_end_scale`属性是在我们在 JavaScript 中定义的`max`和`min`值之间随机选择的值。\n\n`m_current_scale`属性是渲染粒子时计算`m_dest.w`和`m_dest.h`值时使用的当前比例。当前比例将是介于`m_start_scale`和`m_end_scale`属性之间的插值。\n\n# 源矩形属性\n\n在之前版本的代码中，我们没有帧动画粒子。因此，我们不需要声明一个源矩形。如果您想要将整个纹理渲染到画布上，您可以在调用`SDL_RenderCopy`时传入`NULL`来代替源矩形，这就是我们正在做的。现在我们有了帧动画，我们将把渲染到画布上的纹理部分的位置和尺寸传递进来。因此，我们需要定义一个源矩形属性:\n\n```cpp\nSDL_Rect m_src = {.x = 0, .y = 0, .w = 0, .h = 0 };\n```\n\n# 附加构造函数参数\n\n现在我们已经完成了所有新属性，我们将简要讨论函数签名所需的更改。`Particle`类构造器必须添加一些新的参数来支持我们的对齐旋转、颜色修改和帧动画功能。下面是构造函数的新签名:\n\n```cpp\nParticle( SDL_Texture *sprite, Uint32 life_time, float acceleration,\n             bool alpha_fade, int width, int height, bool align_rotation,\n             Uint32 start_color,\n             Uint32 end_color,\n             Uint32 animation_frames );\n```\n\n名为`align_rotation`的`boolean`值告诉构造函数将粒子的旋转与其移动方向对齐。如果我们使用粒子系统的新颜色修改功能，`start_color`和`end_color`参数是颜色修改值。最后一个参数`animation_frames`，告诉粒子系统它是否使用帧动画系统，如果是，它将使用多少帧。\n\n# 更新函数的参数\n\n对`Update`函数签名的修改反映了我们需要对构造函数进行的修改。共有四个新参数用于影响对齐旋转、颜色修改系统和帧动画系统。\n\n下面是新的`Update`函数签名的样子:\n\n```cpp\nvoid Update( Uint32 life_time, float acceleration,\n             bool alpha_fade, bool align_rotation,\n             Uint32 start_color, Uint32 end_color,\n             Uint32 m_animation_frames );\n```\n\n# 产卵函数的参数\n\n最后一个需要修改的函数签名是`Spawn`函数。当我们生成单个粒子时，需要新的值来允许`Emitter`设置比例和旋转值。当我们生成粒子时，`float start_scale`和`float end_scale`参数用于设置开始和结束比例乘数。最后一个添加的参数是`float rotation`，它代表粒子基于这个特定粒子的 *x* 和 *y* 速度移动的角度。以下是该功能的新版本:\n\n```cpp\nvoid Spawn( float x, float y, float velocity_x, float velocity_y,\n             float start_scale, float end_scale, float rotation );\n```\n\n# 换成\n\n我们需要对`Particle`类进行的下一组更改都是对我们在`particle.cpp`文件中定义的函数进行的更改。跟踪对这些功能所做的更改很有挑战性，因此，与其讨论这些更改，我将带您了解我们讨论的每个功能中正在发生的一切。\n\n# 粒子构造器逻辑\n\n新的`Particle`构造器中的逻辑增加了大量代码，为我们的新特性奠定了基础。以下是该函数的最新版本:\n\n```cpp\nParticle::Particle( SDL_Texture *sprite_texture, Uint32 life_time, \n                   float acceleration, bool alpha_fade, int width, \n                   int height, bool align_rotation,\n                   Uint32 start_color, Uint32 end_color, \n                   Uint32 animation_frames ) {\n\n    if( start_color != 0xffffff || end_color != 0xffffff ) {\n        m_color_mod = true;\n        m_start_red = (Uint8)(start_color >> 16);\n        m_start_green = (Uint8)(start_color >> 8);\n        m_start_blue = (Uint8)(start_color);\n\n        m_end_red = (Uint8)(end_color >> 16);\n        m_end_green = (Uint8)(end_color >> 8);\n        m_end_blue = (Uint8)(end_color);\n\n        m_current_red = m_start_red;\n        m_current_green = m_start_green;\n        m_current_blue = m_start_blue;\n    }\n    else {\n        m_color_mod = false;\n\n        m_start_red = (Uint8)255;\n        m_start_green = (Uint8)255;\n        m_start_blue = (Uint8)255;\n\n        m_end_red = (Uint8)255;\n        m_end_green = (Uint8)255;\n        m_end_blue = (Uint8)255;\n\n        m_current_red = m_start_red;\n        m_current_green = m_start_green;\n        m_current_blue = m_start_blue;\n    }\n    m_align_rotation = align_rotation;\n    m_animation_frames = animation_frames;\n    m_sprite_texture = sprite_texture;\n    m_life_time = life_time;\n    m_acceleration = acceleration;\n    m_alpha_fade = alpha_fade;\n    m_width = (float)width;\n    m_height = (float)height;\n\n    m_src.w = m_dest.w = (int)((float)width / (float)m_animation_frames);\n    m_src.h = m_dest.h = height;\n\n    m_next_frame_ms = m_life_time / m_animation_frames;\n    m_current_frame = 0;\n    m_active = false;\n}\n```\n\n该代码的第一个大批量用于在粒子寿命的开始和结束时设置 8 位颜色通道。如果开始颜色或结束颜色不是`0xffffff`(白色)，我们将使用`>>`操作符(移位)设置开始和结束颜色通道。下面是设置起始通道的代码:\n\n```cpp\nm_start_red = (Uint8)(start_color >> 16);\nm_start_green = (Uint8)(start_color >> 8);\nm_start_blue = (Uint8)(start_color);\n```\n\n如果不熟悉右移位运算符`>>`，它取运算符左侧的整数，移位运算符右侧的位数。例如，向右移动两位的二进制值 15 (0000 1111)将返回新值 3 (0000 0011)。当我们向右移动时，任何移动到右侧的位都会丢失，值为 0 的位会从左侧移入:\n\n![](img/67ed6961-92c2-4974-9df7-1b89a8846928.png)\n\nFigure 9.1: Example of a right bit shift\n\n如果我们有一个 RGB 整数，每个通道占用 1 字节或 8 位。所以，如果 R = **9** ，G = **8** ，B = **7** ，我们十六进制的整数值会是这样的:ff090807。如果我们想得到 R 值，我们需要去掉这个 4 字节整数右边的两个字节。每个字节是 8 位，所以我们将取我们的 RGB 并使用`>>`运算符将其移位 16 位。然后我们会有值`09`，我们可以用它来设置我们的 8 位红色通道。当我们使用绿色通道时，我们希望从右边数第二个字节，这样我们就可以移出 8 位。现在，在我们的 4 字节整数中，我们会有 00000908。因为我们将它转换成一个 8 位整数，所有不在最右边字节的数据都会在赋值中丢失，所以我们在绿色通道中以`08`结束。最后，蓝色通道值已经在最右边的字节中。我们需要做的就是将其转换为一个 8 位整数，这样我们就丢失了所有不在蓝色通道中的数据。以下是 32 位颜色的示意图:\n\n![](img/df4baba9-146f-4ce0-af07-995d2c162b92.png)\n\nFigure 9.2: Color bits in a 32-bit integer\n\n我们必须在最终颜色通道上执行同样的魔法:\n\n```cpp\nm_end_red = (Uint8)(end_color >> 16);\nm_end_green = (Uint8)(end_color >> 8);\nm_end_blue = (Uint8)(end_color);\n```\n\n我们要做的最后一件事是将当前颜色通道设置为起始颜色通道。我们这样做是为了用颜色的起始值创建粒子。\n\n如果开始和结束颜色都是白色，我们想将颜色 mod 标志设置为`false`，所以我们不会尝试修改这个粒子上的颜色。我们将所有颜色通道初始化为`255`。下面是这样做的代码:\n\n```cpp\nelse {\n    m_color_mod = false;\n    m_start_red = (Uint8)255;\n    m_start_green = (Uint8)255;\n    m_start_blue = (Uint8)255;\n\n    m_end_red = (Uint8)255;\n    m_end_green = (Uint8)255;\n    m_end_blue = (Uint8)255;\n\n    m_current_red = m_start_red;\n    m_current_green = m_start_green;\n    m_current_blue = m_start_blue;\n}\n```\n\n管理颜色修改的代码之后是一些初始化代码，它根据传递给构造函数的参数设置该对象中的属性变量:\n\n```cpp\nm_align_rotation = align_rotation;\nm_animation_frames = animation_frames;\nm_sprite_texture = sprite_texture;\nm_life_time = life_time;\nm_acceleration = acceleration;\nm_alpha_fade = alpha_fade;\n\nm_width = (float)width;\nm_height = (float)height;\n```\n\n然后，我们根据传入的高度和宽度以及粒子的动画帧数来设置源矩形和目标矩形:\n\n```cpp\nm_src.w = m_dest.w = (int)((float)width / (float)m_animation_frames);\nm_src.h = m_dest.h = height;\n```\n\n最后两行代码将当前帧初始化为`0`，并将我们的活动标志初始化为`false`。所有动画都从第`0`帧开始，新粒子在产生之前不会被激活。\n\n下面是最后几行代码:\n\n```cpp\nm_current_frame = 0;\nm_active = false;\n```\n\n# 粒子更新逻辑\n\n`Particle`类的`Update`功能在每个粒子上运行，这些粒子是由以前的巴布亚新几内亚文件上传创建的。此函数更新构造函数中设置的大多数值。唯一的例外是粒子的宽度和高度尺寸必须保持不变。这是因为构造函数根据上传的图像文件的尺寸设置这些值。我觉得没有必要一步一步地完成这个函数的每一部分，因为它与我们刚刚走过的构造函数非常相似。花点时间看看代码，看看它有多相似:\n\n```cpp\nvoid Particle::Update( Uint32 life_time, float acceleration, \n                       bool alpha_fade, bool align_rotation,\n                       Uint32 start_color, Uint32 end_color, \n                       Uint32 animation_frames ) {\n    if( start_color != 0xffffff || end_color != 0xffffff ) {\n        m_color_mod = true;\n\n        m_start_red = (Uint8)(start_color >> 16);\n        m_start_green = (Uint8)(start_color >> 8);\n        m_start_blue = (Uint8)(start_color);\n\n        m_end_red = (Uint8)(end_color >> 16);\n        m_end_green = (Uint8)(end_color >> 8);\n        m_end_blue = (Uint8)(end_color);\n\n        m_current_red = m_start_red;\n        m_current_green = m_start_green;\n        m_current_blue = m_start_blue;\n    }\n     else {\n        m_color_mod = false;\n\n        m_start_red = (Uint8)255;\n        m_start_green = (Uint8)255;\n        m_start_blue = (Uint8)255;\n\n        m_end_red = (Uint8)255;\n        m_end_green = (Uint8)255;\n        m_end_blue = (Uint8)255;\n\n        m_current_red = m_start_red;\n        m_current_green = m_start_green;\n        m_current_blue = m_start_blue;\n    }\n\n    m_align_rotation = align_rotation;\n    m_life_time = life_time;\n    m_acceleration = acceleration;\n    m_alpha_fade = alpha_fade;\n    m_active = false;\n\n    m_current_frame = 0;\n    m_animation_frames = animation_frames;\n    m_next_frame_ms = m_life_time / m_animation_frames;;\n\n    m_src.w = m_dest.w = (int)((float)m_width / (float)m_animation_frames);\n    m_src.h = m_dest.h = m_height;\n}\n```\n\n# 粒子产卵函数\n\n每当`Emitter`需要发射新粒子时，它就会运行`Particle`类的`Spawn`功能。当发射器到达下一个粒子发射时间时，它会搜索粒子池，寻找标记为未激活的粒子。如果它找到一个粒子，它调用该粒子上的`Spawn`函数，激活该粒子并设置几个特定于其运行的值。每次发射粒子时，传递到`Spawn`的所有值都会被`Emitter`改变。下面是这个函数的代码:\n\n```cpp\nvoid Particle::Spawn( float x, float y,\n                      float velocity_x, float velocity_y,\n                      float start_scale, float end_scale,\n                      float rotation ) {\n     m_position.x = x;\n     m_dest.x = (int)m_position.x;\n     m_position.y = y;\n     m_dest.y = (int)m_position.y;\n\n    m_velocity.x = velocity_x;\n    m_velocity.y = velocity_y;\n\n    m_alpha = 255.0;\n    m_active = true;\n    m_ttl = m_life_time;\n    m_rotation = rotation;\n\n    m_current_red = m_start_red;\n    m_current_green = m_start_green;\n    m_current_blue = m_start_blue;\n\n    m_current_scale = m_start_scale = start_scale;\n    m_end_scale = end_scale;\n\n    m_current_frame = 0;\n    m_next_frame_ms = m_life_time / m_animation_frames;\n}\n```\n\n这个函数中所做的几乎所有事情都是初始化，非常简单。前四行初始化位置属性(`m_position`，以及带有目标矩形(`m_dest`)的位置。然后，设定速度。阿尔法总是从`255`开始。粒子被激活，生存时间变量被激活，旋转被设置。颜色通道被重新初始化，刻度被初始化，当前帧和到下一帧的时间被设置。\n\n# 粒子移动功能\n\n`Particle`类的`Move`函数是一个不仅可以改变粒子渲染位置，还可以调整粒子生命开始和结束之间所有插值的函数。让我们逐步了解一下代码:\n\n```cpp\nvoid Particle::Move() {\n    float time_pct = 1.0 - (float)m_ttl / (float)m_life_time;\n    m_current_frame = (int)(time_pct * (float)m_animation_frames);\n    float acc_adjusted = 1.0f;\n\n    if( m_acceleration < 1.0f ) {\n        acc_adjusted = 1.0f - m_acceleration;\n        acc_adjusted *= delta_time;\n        acc_adjusted = 1.0f - acc_adjusted;\n    }\n    else if( m_acceleration > 1.0f ) {\n        acc_adjusted = m_acceleration - 1.0f;\n        acc_adjusted *= delta_time;\n        acc_adjusted += 1.0f;\n    }\n    m_velocity.x *= acc_adjusted;\n    m_velocity.y *= acc_adjusted;\n\n    m_position.x += m_velocity.x * delta_time;\n    m_position.y += m_velocity.y * delta_time;\n\n    m_dest.x = (int)m_position.x;\n    m_dest.y = (int)m_position.y;\n\n    if( m_alpha_fade == true ) {\n         m_alpha = 255.0 * (1.0 - time_pct);\n         if( m_alpha < 0 ) {\n            m_alpha = 0;\n        }\n    }\n    else {\n        m_alpha = 255.0;\n    }\n    if( m_color_mod == true ) {\n        m_current_red = m_start_red + (Uint8)(( m_end_red - m_start_red\n        ) * \n        time_pct);\n        m_current_green = m_start_green + (Uint8)(( m_end_green -\n        m_start_green ) * \n        time_pct);\n        m_current_blue = m_start_blue + (Uint8)(( m_end_blue -\n        m_start_blue ) * \n        time_pct);\n    }\n\n    m_current_scale = m_start_scale + (m_end_scale - m_start_scale) * \n    time_pct;\n    m_dest.w = (int)(m_src.w * m_current_scale);\n    m_dest.h = (int)(m_src.h * m_current_scale);    \n    m_ttl -= diff_time;\n\n    if( m_ttl <= 0 ) {\n        m_active = false;\n    }\n    else {\n        m_src.x = (int)(m_src.w * m_current_frame);\n    }\n}\n```\n\n`Move`函数的第一行计算`time_pct`。这是一个浮点值，范围从`0.0` - `1.0`。当粒子刚刚产生时，该变量以值`0.0`开始，当粒子准备去激活时，该变量达到`1.0`。它给我们一个浮点值，指示我们在这个粒子的生命周期中所处的位置:\n\n```cpp\nfloat time_pct = 1.0 - (float)m_ttl / (float)m_life_time;\n```\n\n`m_ttl`属性是该粒子的生存时间，单位为毫秒，`m_life_time`是该粒子的总寿命。这个值对于我们在这个`Move`函数中进行插值计算非常有用。\n\n下面一行根据`time_pct`中的值返回当前帧:\n\n```cpp\nm_current_frame = (int)(time_pct * (float)m_animation_frames);\n```\n\n之后，几条线根据加速度值调整粒子的 x 和 y 速度:\n\n```cpp\nfloat acc_adjusted = 1.0f;\n\nif( m_acceleration < 1.0f ) {\n    acc_adjusted = 1.0f - m_acceleration;\n    acc_adjusted *= delta_time;\n    acc_adjusted = 1.0f - acc_adjusted;\n}\nelse if( m_acceleration > 1.0f ) {\n    acc_adjusted = m_acceleration - 1.0f;\n    acc_adjusted *= delta_time;\n    acc_adjusted += 1.0f;\n}\n\nm_velocity.x *= acc_adjusted;\nm_velocity.y *= acc_adjusted;\n```\n\n我们需要根据已经过去的几分之一秒(`delta_time`)将`acc_adjusted`变量设置为`m_acceleration`变量的修改版本。更改`m_velocity`值后，我们需要使用这些速度值来修改粒子的位置:\n\n```cpp\nm_position.x += m_velocity.x * delta_time;\nm_position.y += m_velocity.y * delta_time;\n\nm_dest.x = (int)m_position.x;\nm_dest.y = (int)m_position.y;\n```\n\n如果`m_alpha_fade`变量是`true`，代码将修改阿尔法值，在`time_pct`值变为`1.0`时将其内插至`0`。如果未设置`m_alpha_fade`标志，阿尔法值将设置为`255`(完全不透明度)。下面是代码:\n\n```cpp\nif( m_alpha_fade == true ) {\n    m_alpha = 255.0 * (1.0 - time_pct);\n    if( m_alpha < 0 ) {\n        m_alpha = 0;\n    }\n}\nelse {\n    m_alpha = 255.0;\n}\n```\n\n如果`m_color_mod`标志为`true`，我们需要使用`time_pct`在起始通道颜色值和结束通道颜色值之间进行插值，以便找到当前通道颜色值:\n\n```cpp\nif( m_color_mod == true ) {\n    m_current_red = m_start_red + (Uint8)(( m_end_red - m_start_red ) *         \n    time_pct);\n    m_current_green = m_start_green + (Uint8)(( m_end_green -\n    m_start_green ) * time_pct);\n    m_current_blue = m_start_blue + (Uint8)(( m_end_blue - m_start_blue         \n    ) * time_pct);\n}\n```\n\n找到每个颜色通道的插值后，我们需要使用`time_pct`对当前比例进行插值。然后，我们根据当前比例值和源矩形的尺寸设置目标宽度和目标高度:\n\n```cpp\nm_current_scale = m_start_scale + (m_end_scale - m_start_scale) * time_pct;\nm_dest.w = (int)(m_src.w * m_current_scale);\nm_dest.h = (int)(m_src.h * m_current_scale);\n```\n\n我们要做的最后一件事是将`m_ttl`变量(生存时间)减少`diff_time`(自上一帧渲染以来的时间)。如果生存时间下降到或低于`0`，我们将停用粒子，使其在粒子池中可用，并停止渲染。如果还有一些时间，我们将`m_src.x`(源矩形 *x* 值)设置到要渲染的帧的适当位置:\n\n```cpp\nm_ttl -= diff_time;\nif( m_ttl <= 0 ) {\n    m_active = false;\n}\nelse {\n    m_src.x = (int)(m_src.w * m_current_frame);\n}\n```\n\n# 粒子渲染功能\n\n我们`Particle`类的最后一个函数是`Render`函数。`Emitter`类为粒子池中的每个活动粒子调用该函数。该函数设置粒子使用的子画面纹理的 alpha 和颜色通道值。然后检查`m_align_rotation`标志，看是否需要使用`SDL_RenderCopy`或`SDL_RederCopyEx`将纹理复制到后缓冲区。这两个渲染调用的区别在于`SDL_RenderCopyEx`允许旋转或翻转副本。这两个函数都使用`m_src`矩形来确定要复制的纹理内部的矩形。两者都使用`m_dest`矩形来确定后缓冲区中的目的地，在那里我们复制我们的纹理数据:\n\n```cpp\nvoid Particle::Render() {\n\n    SDL_SetTextureAlphaMod(m_sprite_texture,\n                            (Uint8)m_alpha );\n\n    if( m_color_mod == true ) {\n        SDL_SetTextureColorMod(m_sprite_texture,\n        m_current_red,\n        m_current_green,\n        m_current_blue );\n    }\n\n    if( m_align_rotation == true ) {\n        SDL_RenderCopyEx( renderer, m_sprite_texture, &m_src, &m_dest, \n                            m_rotation, NULL, SDL_FLIP_NONE );\n    }\n    else {\n        SDL_RenderCopy( renderer, m_sprite_texture, &m_src, &m_dest );\n    }\n}\n```\n\n在下一节中，我们将讨论如何修改我们的`Emitter`类来适应我们的改进。\n\n# 修改发射器类\n\n正如我前面提到的，当我们讨论`Emitter`类时，它管理和发射粒子。在典型的粒子系统中，您可能有许多发射器。在我们的游戏中，我们最终将允许多个发射器，但在这个工具中，为了简单起见，我们将保持单个发射器。我们在`Emitter`类中定义了四个函数，我们将改变其中的三个。唯一不需要改变的功能是`GetFreeParticle`功能。如果不记得了，`GetFreeParticle`循环通过`m_particle_pool`(粒子池属性)寻找未标记为活动的粒子(`particle->m_active == false`)。如果它找到一个，它就返回那个粒子。如果不是，则返回`null`。\n\n# 发射器构造函数\n\n`Emitter`构造器的代码将需要更改，以允许我们设置支持新粒子系统功能所需的属性。以下是新的`Emitter`构造函数的代码:\n\n```cpp\nEmitter::Emitter(char* sprite_file, int max_particles, float min_angle, \n         float max_angle, Uint32 particle_lifetime, \n         float acceleration, bool alpha_fade,\n         float min_starting_velocity, float max_starting_velocity,\n         Uint32 emission_rate, int x_pos, int y_pos, float radius,\n         float min_start_scale, float max_start_scale,\n         float min_end_scale, float max_end_scale,\n         Uint32 start_color, Uint32 end_color,\n         float burst_time_pct, Uint32 burst_particles,\n         bool loop, bool align_rotation, Uint32 emit_time_ms, \n         Uint32 animation_frames ) {\n    m_start_color = start_color;\n    m_end_color = end_color;\n    m_active = true;\n    if( min_starting_velocity > max_starting_velocity ) {\n        m_min_starting_velocity = max_starting_velocity;\n        m_max_starting_velocity = min_starting_velocity;\n    }\n    else {\n        m_min_starting_velocity = min_starting_velocity;\n        m_max_starting_velocity = max_starting_velocity;\n    }\n    SDL_Surface *temp_surface = IMG_Load( sprite_file );\n    if( !temp_surface ) {\n        printf(\"failed to load image: %s\\n\", IMG_GetError() );\n        return;\n    }\n    m_sprite_texture = SDL_CreateTextureFromSurface( renderer, temp_surface \n    );\n    SDL_FreeSurface( temp_surface );\n    SDL_QueryTexture( m_sprite_texture,\n                        NULL, NULL,\n                        &m_sprite_width, &m_sprite_height );\n    m_max_particles = max_particles;\n    for( int i = 0; i < m_max_particles; i++ ) {\n        m_particle_pool.push_back(\n            new Particle( m_sprite_texture, particle_lifetime, \n\n                          acceleration, alpha_fade, m_sprite_width, \n                          m_sprite_height, align_rotation,\n                          m_start_color, m_end_color, \n                          animation_frames )\n            );\n    }\n    m_max_angle = max_angle;\n    m_min_angle = min_angle;\n    m_radius = radius;\n    m_position.x = (float)x_pos;\n    m_position.y = (float)y_pos;\n    m_emission_rate = emission_rate;\n    m_emission_time_ms = 1000 / m_emission_rate;\n    m_next_emission = 0;\n    /* new values */\n    m_min_start_scale = min_start_scale;\n    m_max_start_scale = max_start_scale;\n    m_min_end_scale = min_end_scale;\n    m_max_end_scale = max_end_scale;\n\n    m_loop = loop;\n    m_align_rotation = align_rotation;\n    m_emit_loop_ms = emit_time_ms;\n    m_ttl = m_emit_loop_ms;\n    m_animation_frames = animation_frames;\n    m_burst_time_pct = burst_time_pct;\n    m_burst_particles = burst_particles;\n    m_has_burst = false;\n}\n```\n\n这段代码已经改变了很多，我觉得遍历整个函数是有意义的。前两行设置`color`属性，然后通过将`m_active`设置为`true`来激活发射器。当发射器被创建或更新时，我们将此激活标志设置为`true`。如果它是一个循环发射器，活动标志将无限期保持打开。如果`Emitter`不循环，发射器将在达到其发射时间结束时停止发射，如`emit_time_ms`参数所设置的。\n\n接下来我们要做的是设定最小和最大启动速度。我们在`Emitter`中有一个小代码，可以确保`max_starting_velocity`大于`min_starting_velocity`，但是当我们将这个代码移动到游戏中时，我们可能会选择将值设置为任何有效的值。下面是代码:\n\n```cpp\nif( min_starting_velocity > max_starting_velocity ) {\n    m_min_starting_velocity = max_starting_velocity;\n    m_max_starting_velocity = min_starting_velocity;\n}\nelse {\n    m_min_starting_velocity = min_starting_velocity;\n    m_max_starting_velocity = max_starting_velocity;\n}\n```\n\n在我们设置了速度之后，使用`sprite_file`字符串创建了一个 SDL 表面，这是我们已经加载到网络组件虚拟文件系统中的文件的位置。如果该文件不在虚拟文件系统中，我们将打印出一条错误消息并退出构造函数:\n\n```cpp\nSDL_Surface *temp_surface = IMG_Load( sprite_file );\n\nif( !temp_surface ) {\n    printf(\"failed to load image: %s\\n\", IMG_GetError() );\n    return;\n}\n```\n\n从图像文件创建表面后，我们使用该表面创建名为`m_sprite_texture`的 SDL 纹理，然后使用`SDL_FreeSurface`销毁表面使用的内存，因为现在我们有了纹理，不再需要它。然后，我们调用`SDL_QueryTexture`来检索雪碧纹理的宽度和高度，并使用它们来设置`Emitter`属性`m_sprite_width`和`m_sprite_height`。下面是代码:\n\n```cpp\nm_sprite_texture = SDL_CreateTextureFromSurface( renderer, temp_surface );\nSDL_FreeSurface( temp_surface );\nSDL_QueryTexture( m_sprite_texture,\n                  NULL, NULL,\n                  &m_sprite_width, &m_sprite_height );\n```\n\n接下来我们需要做的是设置`m_max_particles`属性，并使用该变量初始化粒子池。一个`for`循环用于将新粒子推到`std::vector`变量`m_particle_pool`的后面:\n\n```cpp\nm_max_particles = max_particles;\nfor( int i = 0; i < m_max_particles; i++ ) {\n    m_particle_pool.push_back(\n        new Particle( m_sprite_texture, particle_lifetime, acceleration,\n                        alpha_fade, m_sprite_width, m_sprite_height, \n                        align_rotation,\n                        m_start_color, m_end_color, animation_frames )\n    );\n}\n```\n\n设置粒子池后，我们使用参数来设置新旧粒子系统值的发射器属性:\n\n```cpp\nm_max_angle = max_angle;\nm_min_angle = min_angle;\nm_radius = radius;\nm_position.x = (float)x_pos;\nm_position.y = (float)y_pos;\nm_emission_rate = emission_rate;\nm_emission_time_ms = 1000 / m_emission_rate;\nm_next_emission = 0;\n\n/* new values */\nm_min_start_scale = min_start_scale;\nm_max_start_scale = max_start_scale;\nm_min_end_scale = min_end_scale;\nm_max_end_scale = max_end_scale;\n\nm_loop = loop;\nm_align_rotation = align_rotation;\nm_emit_loop_ms = emit_time_ms;\nm_ttl = m_emit_loop_ms;\nm_animation_frames = animation_frames;\nm_burst_time_pct = burst_time_pct;\nm_burst_particles = burst_particles;\nm_has_burst = false;\n```\n\n# 发射极更新逻辑\n\n`Emitter`的`Update`功能与构造函数类似，但在`Emitter`已经存在需要更新时运行。该功能从设置我们的`Emitter`上的所有属性变量开始:\n\n```cpp\nif( min_starting_velocity > max_starting_velocity ) {\n    m_min_starting_velocity = max_starting_velocity;\n    m_max_starting_velocity = min_starting_velocity;\n}\nelse {\n    m_min_starting_velocity = min_starting_velocity;\n    m_max_starting_velocity = max_starting_velocity;\n}\nm_active = true;\nm_has_burst = false;\nm_max_particles = max_particles;\nm_min_angle = min_angle;\nm_max_angle = max_angle;\nm_emission_rate = emission_rate;\nm_emission_time_ms = 1000 / m_emission_rate;\nm_position.x = (float)x_pos;\nm_position.y = (float)y_pos;\nm_radius = radius;\n/* new values */\nm_min_start_scale = min_start_scale;\nm_max_start_scale = max_start_scale;\nm_min_end_scale = min_end_scale;\nm_max_end_scale = max_end_scale;\nm_start_color = start_color;\nm_end_color = end_color;\nm_burst_time_pct = burst_time_pct;\nm_burst_particles = burst_particles;\nm_loop = loop;\nm_align_rotation = align_rotation;\nm_emit_loop_ms = emit_time_ms;\nm_ttl = m_emit_loop_ms;\nm_animation_frames = animation_frames;\n```\n\n设置属性变量后，我们可能需要增加或减少`m_particle_pool`向量(粒子池)的大小。如果我们池中的粒子数大于新的最大粒子数，我们可以通过简单的调整大小来缩小粒子池。如果粒子池太小，我们将需要循环创建新粒子的代码，并将这些粒子添加到池中。我们这样做，直到池的大小匹配新的最大粒子数。下面是这样做的代码:\n\n```cpp\nif( m_particle_pool.size() > m_max_particles ) {\n    m_particle_pool.resize( m_max_particles );\n}\nelse if( m_max_particles > m_particle_pool.size() ) {\n    while( m_max_particles > m_particle_pool.size() ) {\n        m_particle_pool.push_back(\n            new Particle( m_sprite_texture, particle_lifetime, \n                            acceleration, alpha_fade, m_sprite_width, \n                            m_sprite_height, m_align_rotation,\n                            m_start_color, m_end_color, \n                            m_animation_frames )\n        );\n    }\n}\n```\n\n现在我们已经调整了粒子池的大小，我们需要循环该池中的每个粒子，并对每个粒子运行`Update`函数，以确保每个粒子都用新的属性值更新。下面是代码:\n\n```cpp\nParticle* particle;\nstd::vector<Particle*>::iterator it;\nfor( it = m_particle_pool.begin(); it != m_particle_pool.end(); it++ ) {\n    particle = *it;\n    particle->Update( particle_lifetime, acceleration, alpha_fade, \n    m_align_rotation, m_start_color, m_end_color, m_animation_frames );\n}\n```\n\n# 发射器移动功能\n\n我们需要更新的最后一个发射器功能是`Emitter::Move`功能。这个函数决定了它在这个帧中是否发出任何新的粒子，如果是，有多少。它还使用随机化来挑选这些粒子的许多起始值，在从我们的 HTML 传入的范围内。生成任何新粒子后，该函数将在粒子池中循环，移动和渲染当前活动的任何粒子。以下是该函数的完整代码:\n\n```cpp\nvoid Emitter::Move() {\n    Particle* particle;\n    std::vector<Particle*>::iterator it;\n    if( m_active == true ) {\n        m_next_emission -= diff_time;\n        m_ttl -= diff_time;\n        if( m_ttl <= 0 ) {\n            if( m_loop ) {\n                m_ttl = m_emit_loop_ms;\n                m_has_burst = false;\n            }\n            else {\n                m_active = false;\n            }\n        }\n        if( m_burst_particles > 0 && m_has_burst == false ) {\n            if( (float)m_ttl / (float)m_emit_loop_ms <= 1.0 - \n            m_burst_time_pct ) {\n                m_has_burst = true;\n                m_next_emission -= m_burst_particles * m_emission_time_ms;\n            }\n        }\n        while( m_next_emission <= 0 ) {\n            m_next_emission += m_emission_time_ms;\n            particle = GetFreeParticle();\n            if( particle != NULL ) {\n                Point spawn_point;\n                spawn_point.x = get_random_float( 0.0, m_radius );\n                Point velocity_point;\n                velocity_point.x = get_random_float( \n                m_min_starting_velocity, m_max_starting_velocity );\n                float angle = get_random_float( m_min_angle, m_max_angle );\n                float start_scale = get_random_float( m_min_start_scale, \n                m_max_start_scale );\n                float end_scale = get_random_float( m_min_end_scale, \n                m_max_end_scale );\n                spawn_point.x += m_position.x;\n                spawn_point.y += m_position.y;\n                particle->Spawn(spawn_point.x, spawn_point.y, \n                velocity_point.x, velocity_point.y,\n                                start_scale, end_scale,\n                                (int)(angle / 3.14159 * 180.0 + 360.0) \n                                % 360 );\n            }\n            else {\n                m_next_emission = m_emission_time_ms;\n            }\n        }\n    }\n    for( it = m_particle_pool.begin(); it != m_particle_pool.end(); it++ ) {\n        particle = *it;\n        if( particle->m_active ) {\n            particle->Move();\n            particle->Render();\n        }\n    }\n}\n```\n\n我们将把这段代码分成两部分，以便更容易理解。`Move`功能的第一部分负责在必要时产生新的粒子。第二部分负责移动和渲染任何现有的活动粒子。该代码的粒子产生部分仅在`m_active`(活动标志)为`true`时运行。第二部分将是双向的。当发射器被停用时，我们不希望发射器产生的所有粒子突然消失。相反，我们希望所有粒子继续移动和渲染，直到它们都被停用。\n\n我们现在将分小块遍历代码来解释所有内容:\n\n```cpp\nif( m_active == true ) {\n    m_next_emission -= diff_time;\n    m_ttl -= diff_time;\n    if( m_ttl <= 0 ) {\n        if( m_loop ) {\n            m_ttl = m_emit_loop_ms;\n            m_has_burst = false;\n        }\n        else {\n            m_active = false;\n        }\n    }\n```\n\n第一段代码检查`m_active`属性变量，以确保发射器当前处于活动状态。如果不是，我们可以跳过这个函数产生新粒子的部分。接下来我们要做的是从`m_next_emission`属性中减去`diff_time`。当`m_next_emission`属性命中或低于`0`时，会产生另一个粒子。我们还从`m_ttl`中减去`diff_time`，这是生存时间属性。从`m_ttl`减去后，我们立即检查`m_ttl`中的值，看它是否小于或等于`0`。如果生存时间下降到`0`以下，我们需要通过查看`m_loop`属性来检查这是否是一个循环发射器。如果是循环发射器，我们将时间重置为活变量，并将`m_has_burst`标志设置为`false`。如果这不是循环发射器，我们通过将`m_active`设置为`false`来停用发射器。\n\n以下代码块与使用新的爆发特性发射粒子爆发有关:\n\n```cpp\nif( m_burst_particles > 0 && m_has_burst == false ) {\n    if( (float)m_ttl / (float)m_emit_loop_ms <= 1.0 - m_burst_time_pct ) {\n        m_has_burst = true;\n        m_next_emission -= m_burst_particles * m_emission_time_ms;\n    }\n}\n```\n\n爆发粒子功能对于我们的*高级粒子系统*来说是全新的。我们在这里使用嵌套的`if`语句。我们可以把`&&`放在第一个`if`的末尾，用一个`if`语句来完成，但是我想把条件分开，这样更容易理解。外部`if`语句首先检查`m_burst_particles`属性(爆发粒子数)是否大于`0`。如果是，那么这个发射器使用爆发系统，需要在适当的爆发时间产生粒子爆发。在这个外部`if`语句中的下一个检查是检查脉冲是否已经在这个发射器中运行。由于我们设计这种突发系统的方式，每个发射回路只能有一个突发。所以，如果`m_has_burst`属性是`true`，那么一个爆发就不会运行。\n\n转到内环，我们需要检查我们是否已经过了发射的爆发时间。`m_burst_time_pct`属性包含一个介于`0.0`和`1.0`之间的值，代表粒子爆发发生时发射时间的十进制百分比。`m_ttl`变量保存发射器的生存时间，单位为毫秒。如果我们将`m_ttl`除以`m_emit_loop_ms`(发射时间，单位为毫秒)，我们会得到从`1.0`到`0.0`的发射时间倒计时，其中`0.0`表示发射完成。`m_burst_time_pct`变量向另一个方向发展。`0.6`的值意味着爆发发生在我们发射过程的 60%。因为这个`if`语句的另一面是倒计时，突发时间也在计数，我们需要从`1.0`中减去`m_burst_time_pct`来做一个适当的比较。如果`(float)m_ttl / (float)m_emit_loop_ms`小于`1.0 - m_burst_time_pct`，那么我们就做好了爆发的准备。为了让爆发发生，我们首先设置`m_has_burst = true`。这将防止爆发在同一发射中多次发生。然后我们从`m_next_emission`中减去爆发粒子的数量，乘以发射时间(毫秒)。\n\n以下几行代码进入`while`循环，只要下一次发射时间小于`0`，就会发射粒子。在这段代码的前一个版本中，我们这里有一个`if`语句，而不是一个循环。这限制了我们的粒子系统每帧发射不超过一个粒子。这可能适用于一些没有爆发模式的简单粒子系统，但是一旦你添加了爆发，你需要能够在一个帧中发射许多粒子。让我们看看这个:\n\n```cpp\nwhile( m_next_emission <= 0 ) {\n    m_next_emission += m_emission_time_ms;\n    particle = GetFreeParticle();\n    if( particle != NULL ) {\n```\n\n`while`循环检查`m_next_emission`是否小于或等于`0`。紧接其后的线将`m_emission_time_ms`添加到下一次发射中。这样做的效果是，如果我们从`m_next_emission`中减去一个大的数字(就像我们在我们的爆发中所做的那样)，这个循环将允许我们在一次运行`Move`函数中发射多个粒子。这意味着我们可以在一个框架内发射大量粒子。添加到`m_next_emission`后，我们立即通过调用`GetFreeParticle`从粒子池中获取一个自由粒子。如果我们将最大粒子数设置得太小，`GetFreeParticle`可能会用完我们可以使用的粒子并返回`NULL`。如果是这种情况，我们需要跳过发出新粒子的所有步骤，这就是为什么有`if`语句，它检查`NULL`粒子。\n\n一旦我们知道我们可以产生一个粒子，我们需要在 HTML 文件中设置的范围内抓取一堆随机值。C/C++ `rand()`函数返回一个随机整数。我们需要的大部分数值都是浮点。我们需要编写一个名为`get_random_float`的简单函数。该函数获取一个随机浮点数，其三位小数精度介于传递给它的最小值和最大值之间。我们选择三位小数精度是基于我们对这个游戏的需求。如果以后有必要，可以修改函数以获得更高的精度。\n\n下面是获取随机值以用于新产生的粒子的代码:\n\n```cpp\nPoint spawn_point;\nspawn_point.x = get_random_float( 0.0, m_radius );\nPoint velocity_point;\nvelocity_point.x = get_random_float( m_min_starting_velocity, m_max_starting_velocity );\nfloat angle = get_random_float( m_min_angle, m_max_angle );\nfloat start_scale = get_random_float( m_min_start_scale, m_max_start_scale );\nfloat end_scale = get_random_float( m_min_end_scale, m_max_end_scale );\n```\n\n我们在这里得到的随机值是距离我们的发射器的距离，我们将在这里生成粒子，粒子的速度，粒子的方向角度，以及开始和结束的比例值。因为我们希望从发射器中心以给定角度产生的粒子也具有相同的方向速度，所以我们只给`spawn_point`和`velocity_point`的 *x* 值分配了一个随机数。我们将使用之前随机生成的相同角度来旋转这两个点。以下是这些点的旋转代码:\n\n```cpp\nvelocity_point.Rotate(angle);\nspawn_point.Rotate( angle );\n```\n\n我们生成相对于`0,0`原点的位置的种子点。因为我们的发射器可能不在`0,0`上，我们需要通过`m_position`点的值来调整产卵点的位置。下面是我们用来做这件事的代码:\n\n```cpp\nspawn_point.x += m_position.x;\nspawn_point.y += m_position.y;\n```\n\n我们做的最后一件事是用我们随机生成的值生成粒子:\n\n```cpp\nparticle->Spawn(spawn_point.x, spawn_point.y, velocity_point.x, \n                velocity_point.y,\n                start_scale, end_scale,\n                (int)(angle / 3.14159 * 180.0 + 360.0) % 360 );\n```\n\n现在该函数已经完成了当前帧的粒子生成，该函数将需要在粒子池中循环寻找要移动和渲染的活动粒子:\n\n```cpp\nfor( it = m_particle_pool.begin(); it != m_particle_pool.end(); it++ ) {\n    particle = *it;\n    if( particle->m_active ) {\n        particle->Move();\n        particle->Render();\n    }\n}\n```\n\n在下一节中，我们将更新我们从 JavaScript 调用的 C++/WebAssembly 函数。\n\n# 外部功能\n\n我们正在编写的*高级粒子系统*有两个外部函数，可以从我们应用中的 JavaScript 调用。调用这些函数`add_emitter`和`update_emitter`来插入或修改网络组件模块中的粒子系统。`advanced_particle.cpp`文件包含这些函数，以及加载`Module`时调用的`main`函数和每帧渲染调用一次的`show_emission`函数。我们不需要修改本章前面为基本粒子系统创建的`main`和`show_emission`函数。然而，我们需要将我们放入 JavaScript 代码中的附加参数添加到`add_emitter`和`update_emitter`中。此外，我们还创建了一个名为`get_random_float`的实用函数，我们在生成粒子时使用它。因为这个文件包含了我们所有其他的 C 风格函数，所以我觉得`advanced_particle.cpp`也是放这个函数最好的地方。\n\n# 随机浮点数\n\n让我们从讨论新的`get_random_float`功能开始。下面是代码:\n\n```cpp\nfloat get_random_float( float min, float max ) {\n    int int_min = (int)(min * 1000);\n    int int_max = (int)(max * 1000);\n    if( int_min > int_max ) {\n        int temp = int_max;\n        int_max = int_min;\n        int_min = temp;\n    }\n    int int_diff = int_max - int_min;\n    int int_rand = (int_diff == 0) ? 0 : rand() % int_diff;\n    int_rand += int_min;\n    return (float)int_rand / 1000.0;\n}\n```\n\n`%`(模运算符)用于使随机整数值介于 0 和您在`%`之后使用的任何值之间。模运算符是余数运算符。它返回除法运算的余数。例如，`13 % 10`会返回 3，`23 % 10`也会。取任意数的`% 10`总是得到一个 0 到 9 之间的数。模与`rand()`一起使用很有用，因为它会产生一个介于 0 和`%`之后的值之间的随机数。所以，`rand() % 10`会产生一个 0 到 9 之间的随机数。\n\n`get_random_float`函数接受最小和最大浮点值，并在该范围内生成一个随机数。前两行接受这些浮点值，将其乘以 1，000，并将其转换为整数。因为`rand()`只对整数起作用，所以我们需要模拟一个精度值。乘以 1000 可以得到三位小数的精度。例如，如果我们想要生成一个介于 1.125 和 1.725 之间的随机数，这两个值将乘以 1，000，我们将使用`rand()`生成一个介于 1，125 和 1，175 之间的随机值:\n\n```cpp\nint int_min = (int)(min * 1000);\nint int_max = (int)(max * 1000);\n```\n\n`rand()`再次只生成随机整数，使用`rand()`旁边的`%`(模运算符)会给你一个介于`0`和跟在`%`后面的数字之间的数字。正因为如此，我们想知道我们的`int_min`和`int_max`值之间的区别。如果我们从`int_max`中减去`int_min`，我们会得到一个就是这个差的数。如果调用代码不小心传入了一个小于`int_min`的 max 值，我们可能会被抛出，所以我们需要一点代码来检查`max`是否小于`min`，如果是，我们需要切换这两个值。下面是`if`语句代码:\n\n```cpp\nif( int_min > int_max ) {\n    int temp = int_max;\n    int_max = int_min;\n    int_min = temp;\n}\n```\n\n现在，我们可以继续了解两者之间的区别:\n\n```cpp\nint int_diff = int_max - int_min;\n```\n\n在下面一行代码中，我们得到一个介于 0 和`int_diff`中的值之间的随机值。在执行`rand() % int_diff`之前，我们使用`?:`(三元运算符)确保`int_diff`不是 0。这是因为`%`是一个除法余数运算符，所以像除以 0 一样，执行`% 0`会导致异常。如果我们的最小值和最大值之间没有差异，我们将返回最小值。所以，如果`int_diff`为 0，我们可以通过使用三元运算符将`int_rand`设置为 0。下面是代码:\n\n```cpp\nint int_rand = (int_diff == 0) ? 0 : rand() % int_diff;\n```\n\n然后，我们将`int_min`加到`int_rand`上，在`int_min`和`int_max`值之间有一个随机值:\n\n```cpp\nint_rand += int_min;\n```\n\n我们要做的最后一件事是将`int_rand`铸造成`float`并除以`1000.0`。这将返回一个介于传递给函数的`min`和`max`浮点值之间的浮点值:\n\n```cpp\nreturn (float)int_rand / 1000.0;\n```\n\n# 添加发射器\n\n`add_emitter`功能是一个传递，检查是否存在发射器，如果存在，则删除发射器。然后它创建一个新的`Emitter`对象，传入我们在 HTML 中设置并在 JavaScript 中传递的所有值。我们需要做的更改包括将新参数添加到`add_emitter`函数的签名中，并将这些相同的新参数添加到对`Emitter`构造函数的调用中。在函数签名和构造函数调用中，我们将添加一个`/* new parameters */`注释，显示旧参数的结束和新参数的开始。以下是新代码:\n\n```cpp\nextern \"C\"\n    EMSCRIPTEN_KEEPALIVE\n    void add_emitter(char* file_name, int max_particles, float min_angle, \n         float max_angle,\n         Uint32 particle_lifetime, float acceleration, bool alpha_fade,\n         float min_starting_velocity, float max_starting_velocity,\n         Uint32 emission_rate, float x_pos, float y_pos, float radius,\n         /* new parameters */\n         float min_start_scale, float max_start_scale,\n         float min_end_scale, float max_end_scale,\n         Uint32 start_color, Uint32 end_color,\n         float burst_time_pct, Uint32 burst_particles,\n         bool loop, bool align_rotation, Uint32 emit_time_ms,\n         Uint32 animation_frames ) {\n        if( emitter != NULL ) {\n            delete emitter;\n        }\n\n        emitter = new Emitter(file_name, max_particles, min_angle, \n                  max_angle,\n                  particle_lifetime, acceleration, alpha_fade,\n                  min_starting_velocity, max_starting_velocity,\n                  emission_rate, x_pos, y_pos, radius,\n                  /* new parameters */\n                  min_start_scale, max_start_scale,\n                  min_end_scale, max_end_scale,\n                  start_color, end_color,\n                  burst_time_pct, burst_particles,\n                  loop, align_rotation, emit_time_ms,\n                  animation_frames\n                  );\n    }\n```\n\n# 更新发射器\n\n我们对`update_emitter`功能所做的更改反映了在`add_emitter`功能中所做的更改。`add_emitter`和`update_emitter`的主要区别在于如果没有现有发射器`update_emitter`将不会运行，并且它不会调用`Emitter`构造函数来创建新的`Emitter`，而是调用现有发射器的`Update`函数。`Update`功能传入所有新值和大部分旧值(除了`char* file_name`)。就像我们对`add_emitter`函数所做的更改一样，我们在函数签名和对发射器`Update`函数的调用中放置了一个`/* new parameters */`注释，以显示新参数添加到了哪里。下面是代码:\n\n```cpp\nextern \"C\"\n    EMSCRIPTEN_KEEPALIVE\n    void update_emitter(int max_particles, float min_angle, \n         float max_angle,\n         Uint32 particle_lifetime, float acceleration, bool alpha_fade,\n         float min_starting_velocity, float max_starting_velocity,\n         Uint32 emission_rate, float x_pos, float y_pos, float radius,\n         /* new parameters */\n         float min_start_scale, float max_start_scale,\n         float min_end_scale, float max_end_scale,\n         Uint32 start_color, Uint32 end_color,\n         float burst_time_pct, Uint32 burst_particles,\n         bool loop, bool align_rotation, Uint32 emit_time_ms,\n         Uint32 animation_frames ) {\n         if( emitter == NULL ) {\n                        return;\n                    }\n                    emitter->Update(max_particles, min_angle, max_angle,\n                          particle_lifetime, acceleration, alpha_fade,\n                          min_starting_velocity, max_starting_velocity,\n                          emission_rate, x_pos, y_pos, radius,\n                          /* new parameters */\n                          min_start_scale, max_start_scale,\n                          min_end_scale, max_end_scale,\n                          start_color, end_color,\n                          burst_time_pct, burst_particles,\n                          loop, align_rotation, emit_time_ms,\n                          animation_frames\n                    );\n                }\n```\n\n在下一节中，我们将配置我们的*高级粒子系统工具*来创建一个新的*粒子发射器*。\n\n# 配置粒子发射器\n\n在这一点上，你可能想知道我们什么时候才能继续写游戏。我们构建这个*粒子发射器配置工具*有几个原因。首先，在编译代码中很难配置粒子系统。如果我们想测试一个发射器的配置，我们需要在每次测试中重新编译我们的值，或者我们需要编写一个数据加载器，并在进行配置更改后重新运行游戏。创建一个工具，允许我们测试不同的发射器配置，允许更快(和更有趣)的粒子系统创建。\n\n# HTML 外壳和 WebAssembly 模块交互\n\n我创建粒子系统配置工具也是别有用心的。可能你们中的一些人并不是为了学习游戏编程而阅读这本书。您可能已经购买了这本书，作为了解更多关于 WebAssembly 的有趣方式。编写这个工具是一种有趣的方式，可以了解更多关于 WebAssembly 模块和驱动该模块的 HTML 和 JavaScript 之间的交互。\n\n# 编译和运行新工具\n\n现在我们已经有了想要的所有参数，是时候重新编译配置工具的更新版本，并开始设计一些粒子系统了。\n\nIf you are building this from the GitHub project, you will need to run this compile command from the `/Chapter09/advanced-particle-tool/` directory.\n\n首先，在命令行上运行以下命令来编译新的配置工具:\n\n```cpp\nem++ emitter.cpp particle.cpp point.cpp advanced_particle.cpp -o particle.html -std=c++ 17 --shell-file advanced_particle_shell.html -s NO_EXIT_RUNTIME=1 -s USE_WEBGL2=1 -s USE_SDL=2 -s USE_SDL_IMAGE=2 -s SDL2_IMAGE_FORMATS=[\"png\"] -s NO_EXIT_RUNTIME=1 -s EXPORTED_FUNCTIONS=\"['_add_emitter', '_update_emitter', '_main']\" -s EXTRA_EXPORTED_RUNTIME_METHODS=\"['cwrap', 'ccall']\" -s FORCE_FILESYSTEM=1\n```\n\n在`emrun`或网络浏览器中打开网页(如果您运行的是网络服务器)。它看起来像这样:\n\n![](img/a987a969-a6ae-4290-98c9-c7285611d17f.png)\n\nFigure 9.3: Screenshot of our particle system configuration tool\n\n我们将从一个简单的废气排放器开始。对 HTML 值进行以下更改，然后单击上传。png 按钮:\n\n*   最小角度:-10°\n*   最大角度:10°\n*   最大粒子数:500\n*   排放率:50\n*   半径:0.5\n*   我的开局不错：100.0\n*   最大启动井数：150.0\n*   爆发时间:0.7\n*   爆裂颗粒:40\n*   动画帧数:6\n\n单击上传后。png 按钮，导航到图像目录中的`ProjectileExpOrange.png`文件并打开。\n\n下面是配置工具与我们的排气粒子发射器的截图:\n\n![](img/0cbb0c41-5fb0-470b-a355-bd404fc6df29.png)\n\nFigure 9.4: Engine exhaust configuration\n\n我鼓励你玩弄价值观，直到你得到你喜欢的东西。每当您更改页面左侧的值时，您都需要单击“更新发射器”按钮，以查看新值在网页右侧的粒子系统中的反映。\n\n# 创建粒子发射器\n\n现在我们有了一个排气粒子系统，我们将开始在游戏中添加粒子系统代码，以添加一些不错的粒子效果。我想要一个玩家和敌舰排气的粒子系统。我还想在动画爆炸的顶部添加一个粒子系统效果，我们必须让它脱颖而出。\n\n我们要做的第一件事是将`particle.cpp`和`emitter.cpp`文件复制到主`Chapter09`目录中。之后，我们需要将这些类定义添加到`game.hpp`文件以及`get_random_float`函数原型中。\n\n# 对 game.hpp 的更改\n\n我们需要做的第一组更改是对`game.hpp`文件进行更改。我们需要为`get_random_float`添加一个`Emitter`类定义、`Particle`类定义和一个外部函数原型。我们还需要给`Ship`类增加一些新的属性。以下是我们必须为`get_random_float`原型添加的线:\n\n```cpp\nextern float get_random_float( float min, float max );\n```\n\n# 添加粒子类定义\n\n我们必须添加到`game.hpp`中的`Particle`类的定义与我们的高级配置工具的定义相同。因为都是一样的，我们就不走班里什么都做了。如果你不记得了，请随时回到上一章作为参考。这是我们将要添加到`game.hpp`中的`Particle`的类定义代码:\n\n```cpp\nclass Particle {\n    public:\n        bool m_active;\n        bool m_alpha_fade;\n        bool m_color_mod;\n        bool m_align_rotation;\n\n        Uint8 m_start_red;\n        Uint8 m_start_green;\n        Uint8 m_start_blue;\n\n        Uint8 m_end_red;\n        Uint8 m_end_green;\n        Uint8 m_end_blue;\n\n        Uint8 m_current_red;\n        Uint8 m_current_green;\n        Uint8 m_current_blue;\n\n        SDL_Texture *m_sprite_texture;\n        int m_ttl;\n\n        Uint32 m_life_time;\n        Uint32 m_animation_frames;\n        Uint32 m_current_frame;\n\n        Uint32 m_next_frame_ms;\n        float m_rotation;\n        float m_acceleration;\n        float m_alpha;\n\n        float m_width;\n        float m_height;\n\n        float m_start_scale;\n        float m_end_scale;\n        float m_current_scale;\n\n        Point m_position;\n        Point m_velocity;\n        SDL_Rect m_dest = {.x = 0, .y = 0, .w = 0, .h = 0 };\n        SDL_Rect m_src = {.x = 0, .y = 0, .w = 0, .h = 0 };\n\n        Particle( SDL_Texture *sprite, Uint32 life_time, float \n                    acceleration,\n                    bool alpha_fade, int width, int height, bool \n                    align_rotation,\n                    Uint32 start_color,\n                    Uint32 end_color,\n                    Uint32 animation_frames );\n\n        void Update( Uint32 life_time, float acceleration,\n                    bool alpha_fade, bool align_rotation,\n                    Uint32 start_color, Uint32 end_color,\n                    Uint32 m_animation_frames );\n\n        void Spawn( float x, float y, float velocity_x, float velocity_y,\n                    float start_scale, float end_scale, float rotation );\n        void Move();\n        void Render();\n};\n```\n\n# 发射器类别定义\n\n`Emitter`类有一些我们已经添加的附加属性，帮助`Emitter`相对于游戏对象定位自己。在粒子发射器配置工具中有一个我们不需要的`Run`功能，但是我们会在游戏代码中需要它，这样我们就可以随时触发`Emitter`。`Emitter`和`Particle`里面的`Update`功能在游戏里面不是必须的，但是为了不使改动复杂化，我们将把它们留在里面。Emscripten 死代码消除逻辑应该在编译游戏时移除这些代码。下面是我们需要添加到`games.hpp`中的`Emitter`类定义的新代码:\n\n```cpp\nclass Emitter {\n    public:\n        bool m_loop;\n        bool m_align_rotation;\n        bool m_active;\n        bool m_has_burst;\n\n        SDL_Texture *m_sprite_texture;\n        std::vector<Particle*> m_particle_pool;\n        int m_sprite_width;\n        int m_sprite_height;\n        int m_ttl;\n\n        // added ----------------------------\n        int m_x_adjustment = 0;\n        int m_y_adjustment = 0;\n        // ----------------------------------\n\n        Uint32 m_max_particles;\n        Uint32 m_emission_rate;\n        Uint32 m_emission_time_ms;\n\n        Uint32 m_start_color;\n        Uint32 m_end_color;\n\n        Uint32 m_burst_particles;\n        Uint32 m_emit_loop_ms;\n        Uint32 m_animation_frames;\n\n        int m_next_emission;\n\n        float* m_parent_rotation;\n\n        float m_max_angle;\n        float m_min_angle;\n        float m_radius;\n        float m_min_starting_velocity;\n        float m_max_starting_velocity;\n\n        float m_min_start_scale;\n        float m_max_start_scale;\n        float m_min_end_scale;\n        float m_max_end_scale;\n        float m_min_start_rotation;\n        float m_max_start_rotation;\n        float m_burst_time_pct;\n\n        // added ----------------------------\n        float* m_parent_rotation_ptr;\n        float* m_parent_x_ptr;\n        float* m_parent_y_ptr;\n        // -----------------------------------\n\n        Point m_position;\n\n        Emitter(char* sprite_file, int max_particles, float min_angle, \n              float max_angle,\n              Uint32 particle_lifetime, float acceleration, \n              bool alpha_fade,\n              float min_starting_velocity, float max_starting_velocity,\n              Uint32 emission_rate, int x_pos, int y_pos, float radius,\n              float min_start_scale, float max_start_scale,\n              float min_end_scale, float max_end_scale,\n              Uint32 start_color, Uint32 end_color,\n              float burst_time_pct, Uint32 burst_particles,\n              bool loop, bool align_rotation,\n              Uint32 emit_time_ms, Uint32 animation_frames );\n\n        void Update(int max_particles, float min_angle, float max_angle,\n             Uint32 particle_lifetime, float acceleration, bool alpha_fade,\n             float min_starting_velocity, float max_starting_velocity,\n             Uint32 emission_rate, int x_pos, int y_pos, float radius,\n             float min_start_scale, float max_start_scale,\n             float min_end_scale, float max_end_scale,\n             Uint32 start_color, Uint32 end_color,\n             float burst_time_pct, Uint32 burst_particles,\n             bool loop, bool align_rotation, Uint32 emit_time_ms,\n             Uint32 animation_frames );\n\n        void Move();\n        Particle* GetFreeParticle();\n\n        void Run(); // added\n };\n```\n\n我们添加到粒子系统配置工具的代码被标注为`added`的注释包围。让我来介绍一下这些新属性和新函数的功能。以下是前两个添加的属性:\n\n```cpp\nint m_x_adjustment = 0;\nint m_y_adjustment = 0;\n```\n\n这两个值是调整值，用于修改发射器产生粒子的位置。这些变量对于粒子位置相对于发射器跟随的对象位置的小调整非常有用。以下是我们添加的三个属性:\n\n```cpp\nfloat* m_parent_rotation_ptr;\nfloat* m_parent_x_ptr;\nfloat* m_parent_y_ptr;\n```\n\n这些是指向父对象的 x、y 和旋转属性的指针。例如，如果我们设置`Emitter->m_parent_rotation_ptr = &m_Rotation`，指针将指向父对象的旋转，我们将能够访问`Emitter`内部的那个值来调整旋转。`m_parent_x_ptr`和`m_parent_y_ptr`也是如此。\n\n最后，我们增加了一个`Run`功能:\n\n```cpp\nvoid Run();\n```\n\n此功能允许不循环的粒子发射器重新启动。我们将把它用于我们添加到`Ship`类的`Explosion`发射器。\n\n# 更改发射器. cpp\n\n现在我们已经完成了我们需要对`game.hpp`进行的更改，我们将逐个功能地完成我们将对`emitter.cpp`文件进行的所有更改。\n\n# 对构造函数的更改\n\n要对构造函数进行两项更改。首先，我们将在顶部添加一些初始化，初始化所有指向`NULL`的新指针。我们不需要在每个发射器中使用这些指针，因此我们可以对照`NULL`检查它们何时被使用或未被使用。接下来，我们将把传递给构造函数的值从度数修改为弧度。函数如下所示:\n\n```cpp\nEmitter::Emitter(char* sprite_file, int max_particles, float min_angle, \n                float max_angle,\n                Uint32 particle_lifetime, float acceleration, bool \n                alpha_fade,\n                float min_starting_velocity, float max_starting_velocity,\n                Uint32 emission_rate, int x_pos, int y_pos, float radius,\n                float min_start_scale, float max_start_scale,\n                float min_end_scale, float max_end_scale,\n                Uint32 start_color, Uint32 end_color,\n                float burst_time_pct, Uint32 burst_particles,\n                bool loop, bool align_rotation, Uint32 emit_time_ms, Uint32 \n                animation_frames ) {\n    // added -----------------------------\n    m_parent_rotation_ptr = NULL;\n    m_parent_x_ptr = NULL;\n    m_parent_y_ptr = NULL;\n    // -----------------------------------\n    m_start_color = start_color;\n    m_end_color = end_color;\n    m_active = true;\n\n    if( min_starting_velocity > max_starting_velocity ) {\n        m_min_starting_velocity = max_starting_velocity;\n        m_max_starting_velocity = min_starting_velocity;\n    }\n    else {\n        m_min_starting_velocity = min_starting_velocity;\n        m_max_starting_velocity = max_starting_velocity;\n    }\n    SDL_Surface *temp_surface = IMG_Load( sprite_file );\n\n    if( !temp_surface ) {\n        printf(\"failed to load image: %s\\n\", IMG_GetError() );\n        printf(\"failed sprite file: %s\\n\", sprite_file );\n        return;\n    }\n    m_sprite_texture = SDL_CreateTextureFromSurface( renderer, temp_surface \n    );\n    SDL_FreeSurface( temp_surface );\n    SDL_QueryTexture( m_sprite_texture,\n                        NULL, NULL,\n                        &m_sprite_width, &m_sprite_height );\n                        m_max_particles = max_particles;\n\n    for( int i = 0; i < m_max_particles; i++ ) {\n        m_particle_pool.push_back(\n            new Particle( m_sprite_texture, particle_lifetime, \n            acceleration,\n                            alpha_fade, m_sprite_width, m_sprite_height, \n                            align_rotation,\n                            m_start_color, m_end_color, animation_frames )\n            );\n    }\n\n    // modified -----------------------------\n    m_min_angle = (min_angle+90) / 180 * 3.14159;\n    m_max_angle = (max_angle+90) / 180 * 3.14159;\n    // --------------------------------------\n\n    m_radius = radius;\n    m_position.x = (float)x_pos;\n    m_position.y = (float)y_pos;\n    m_emission_rate = emission_rate;\n    m_emission_time_ms = 1000 / m_emission_rate;\n    m_next_emission = 0;\n    m_min_start_scale = min_start_scale;\n    m_max_start_scale = max_start_scale;\n    m_min_end_scale = min_end_scale;\n    m_max_end_scale = max_end_scale;\n\n    m_loop = loop;\n    m_align_rotation = align_rotation;\n    m_emit_loop_ms = emit_time_ms;\n    m_ttl = m_emit_loop_ms;\n\n    m_animation_frames = animation_frames;\n    m_burst_time_pct = burst_time_pct;\n    m_burst_particles = burst_particles;\n    m_has_burst = false;\n}\n```\n\n第一个变化是在这个函数的最顶端，并将我们的新指针属性设置为`NULL`:\n\n```cpp\nm_parent_rotation_ptr = NULL;\nm_parent_x_ptr = NULL;\nm_parent_y_ptr = NULL;\n```\n\n稍后我们会检查这些指针是不是`NULL`，如果不是，我们会用`m_parent_rotation_ptr`来调整这个发射器的旋转角度。我们将使用`m_parent_x_ptr`改变发射器的 x 坐标，我们将使用`m_parent_y_ptr`调整该发射器的 y 坐标。之后，我们有代码修改传递的最小和最大角度，从度到弧度:\n\n```cpp\nm_min_angle = (min_angle+90) / 180 * 3.14159;\nm_max_angle = (max_angle+90) / 180 * 3.14159;\n```\n\n我们需要这样做的真正原因是，我们正在对传递给发射器的值进行硬编码。如果我们创建了一个数据加载器，我们可以在数据加载时完成这种转换。但是，因为我们直接从*粒子发射器配置工具*中获取这些值，并将这些值硬编码到对新发射器的调用中，所以我们要么必须记住每次更改这些值时都要自己进行转换，要么必须在构造函数和`Update`函数中进行转换。\n\n# 对更新功能的更改\n\n`Update`函数不太可能在我们的游戏中被调用。Emscripten 的死代码删除过程应该会消除它。然而，我们并没有将其从`Emitter`类中移除。如果你认为你可以这样称呼它，你可能想改变`m_min_angle`和`m_max_angle`的初始化，把度数转换成弧度，就像我们在构造函数中做的那样:\n\n```cpp\nm_min_angle = (min_angle+90) / 180 * 3.14159;\nm_max_angle = (max_angle+90) / 180 * 3.14159;\n```\n\n# 添加运行函数\n\n在粒子系统配置工具中，我们不需要`Run`函数，因为调用`Update`函数会运行`Emitter`。`Update`功能过于繁琐，无法在我们的游戏中使用。它使用了大量的配置变量，我们在调用函数时可能无法访问这些变量。我们所要做的就是将发射器设置为激活状态，重置生存时间和爆发标志。我们没有调用`Update`，而是创建了一个小的`Run`函数来做我们需要的事情:\n\n```cpp\nvoid Emitter::Run() {\n    m_active = true;\n    m_ttl = m_emit_loop_ms;\n    m_has_burst = false;\n}\n```\n\n将`m_active`设置为真将激活发射器，以便在调用`Move`功能时可以产生新粒子。将`m_ttl`重置为`m_emit_loop_ms`确保生存时间不会在下次调用`Move`功能时自动关闭发射器。设置`m_has_burst = false`确保，如果发射中的某个地方必须发生粒子爆发，它将运行。\n\n# 对移动功能的更改\n\n新版本的`Move`功能将需要能够基于父位置修改其位置，并基于父位置的旋转来旋转其定义的位置。它还需要能够使用`m_x_adjustment`和`m_y_adjustment`进行微调。以下是`Move`的完整新版本:\n\n```cpp\nvoid Emitter::Move() {\n Particle* particle;\n std::vector<Particle*>::iterator it;\n    if( m_active == true ) {\n        m_next_emission -= diff_time;\n        m_ttl -= diff_time;\n        if( m_ttl <= 0 ) {\n            if( m_loop ) {\n                m_ttl = m_emit_loop_ms;\n                m_has_burst = false;\n            }\n            else { m_active = false; }\n        }\n        if( m_burst_particles > 0 && m_has_burst == false ) {\n            if( (float)m_ttl / (float)m_emit_loop_ms <= 1.0 - \n                m_burst_time_pct ) {\n                m_has_burst = true;\n                m_next_emission -= m_burst_particles * m_emission_time_ms;\n            }\n        }\n        while( m_next_emission <= 0 ) {\n            m_next_emission += m_emission_time_ms;\n            particle = GetFreeParticle();\n            if( particle != NULL ) {\n                Point spawn_point, velocity_point, rotated_position;\n                spawn_point.x = get_random_float( 0.0, m_radius );\n                velocity_point.x = \n                get_random_float(m_min_starting_velocity, \n                m_max_starting_velocity);\n                float angle = get_random_float( m_min_angle,m_max_angle );\n                float start_scale = get_random_float(m_min_start_scale, \n                m_max_start_scale);\n                float end_scale = get_random_float( m_min_end_scale,\n                m_max_end_scale );\n                if( m_parent_rotation_ptr != NULL ) {\n                    angle += *m_parent_rotation_ptr;\n                    rotated_position = m_position;\n                    rotated_position.Rotate( *m_parent_rotation_ptr );\n                }\n                velocity_point.Rotate(angle);\n                spawn_point.Rotate( angle );\n\n                if( m_parent_rotation_ptr == NULL ) {\n                    spawn_point.x += m_position.x;\n                    spawn_point.y += m_position.y;\n                    if( m_parent_x_ptr != NULL ) { spawn_point.x += \n                    *m_parent_x_ptr; }\n                    if( m_parent_y_ptr != NULL ) { spawn_point.y += \n                    *m_parent_y_ptr; }\n                }\n                else {\n                    spawn_point.x += rotated_position.x;\n                    spawn_point.y += rotated_position.y;\n                    if( m_parent_x_ptr != NULL ) { spawn_point.x += \n                    *m_parent_x_ptr; }\n                    if( m_parent_y_ptr != NULL ) { spawn_point.y += \n                    *m_parent_y_ptr; }\n                }\n                spawn_point.x += m_x_adjustment;\n                spawn_point.y += m_y_adjustment;\n                particle->Spawn(spawn_point.x, \n                spawn_point.y,velocity_point.x, velocity_point.y,\n                    start_scale, end_scale, (int)(angle / 3.14159 * 180.0 + \n                    360.0) % 360 );\n            }\n            else {\n                m_next_emission = m_emission_time_ms;\n            }\n        }\n    }\n    for( it = m_particle_pool.begin(); it != m_particle_pool.end(); it++ ) \n    {\n        particle = *it;\n        if( particle->m_active ) {\n            particle->Move();\n            particle->Render();\n        }\n    }\n}\n```\n\n这些代码的大部分与早期版本相同。让我们来看看不同之处。首先，如果有旋转的父对象，我们需要旋转整个粒子系统。我们将把它用于我们将要添加到宇宙飞船物体中的排气粒子系统。这个排气口必须相对于宇宙飞船定位。要做到这一点，我们需要采取的立场，并旋转它。我们还需要将父对象的旋转添加到现有的发射角度。以下是新代码:\n\n```cpp\nPoint rotated_position;\n\nif( m_parent_rotation_ptr != NULL ) {\n    angle += *m_parent_rotation_ptr;\n    rotated_position = m_position;\n    rotated_position.Rotate( *m_parent_rotation_ptr );\n}\n```\n\n在顶部，我们创建了一个名为`rotated_position`的新`Point`对象。如果`m_parent_rotation_ptr`不是`NULL`，我们将该值添加到之前计算的发射角中。我们将通过父代的旋转将`m_position`的值复制到该位置的`rotated_position`和`Rotate`中。之后，我们会检查`m_parent_rotation_ptr`是否不是`NULL`，如果不是，我们会使用`rotated_position`相对于父对象的位置来计算发射器的位置。以下是`if`语句，检查`m_parent_rotation_ptr == NULL`是否。如果它为空，该`if`块的第一部分将执行之前会执行的操作。下面是代码:\n\n```cpp\nif( m_parent_rotation_ptr == NULL ) {\n    spawn_point.x += m_position.x;\n    spawn_point.y += m_position.y;\n}\n```\n\n因为`if`语句是检查`m_parent_rotation_ptr == NULL`是否存在，所以我们不想使用粒子系统位置的旋转版本。该块默认使用未修改的`m_position`属性。如果`m_parent_rotation_ptr`不是`NULL`，我们将运行以下`else`块:\n\n```cpp\nelse {\n    spawn_point.x += rotated_position.x;\n    spawn_point.y += rotated_position.y;\n}\n```\n\n该代码使用了`m_position`的旋转版本。接下来我们要看看`m_parent_x_ptr`和`m_parent_y_ptr`是不是`NULL`。如果不是，那么我们需要使用这些值将家长的位置添加到`spawn_point`中。下面是这段代码:\n\n```cpp\nif( m_parent_x_ptr != NULL ) {\n    spawn_point.x += *m_parent_x_ptr;\n}\nif( m_parent_y_ptr != NULL ) {\n    spawn_point.y += *m_parent_y_ptr;\n}\n```\n\n我们将添加到`Move`功能的最后一段代码是对产卵点的微调整。有时候，粒子系统在旋转之前需要稍微调整一下，让它们看起来恰到好处。因此，我们添加以下内容:\n\n```cpp\nspawn_point.x += m_x_adjustment;\nspawn_point.y += m_y_adjustment;\n```\n\n`m_x_adjustment`和`m_y_adjustment`的值默认为`0`，因此如果您想要使用这些值，需要在创建发射器后的某个时间进行设置。\n\n# 更改 ship.cpp\n\n接下来我们要做的是修改`ship.cpp`文件，以利用两个新的粒子发射器。我们想要一个用于飞船排气的粒子发射器，一个用于改善飞船爆炸的粒子发射器。我们需要更改`Ship`类的构造函数、`Ship`类的`Acceleration`函数和`Ship`类的`Render`函数。\n\n# 船舶类的构造函数\n\n`Ship`类的构造函数改变了`Ship`类内部的大部分函数。我们不仅要初始化新的属性，我们还需要设置发射器的父值和调整值。下面是构造函数的新代码:\n\n```cpp\nShip::Ship() : Collider(8.0) {\n    m_Rotation = PI;\n    m_DX = 0.0;\n    m_DY = 1.0;\n    m_VX = 0.0;\n    m_VY = 0.0;\n    m_LastLaunchTime = current_time;\n    m_Accelerating = false;\n    m_Exhaust = new Emitter((char*)\"/sprites/ProjectileExpOrange.png\", 200,\n                            -10, 10,\n                            400, 1.0, true,\n                            0.1, 0.1,\n                            30, 0, 12, 0.5,\n                            0.5, 1.0,\n                            0.5, 1.0,\n                            0xffffff, 0xffffff,\n                            0.7, 10,\n                            true, true,\n                            1000, 6 );\n\n    m_Exhaust->m_parent_rotation_ptr = &m_Rotation;\n    m_Exhaust->m_parent_x_ptr = &m_X;\n    m_Exhaust->m_parent_y_ptr = &m_Y;\n    m_Exhaust->m_x_adjustment = 10;\n    m_Exhaust->m_y_adjustment = 10;\n    m_Exhaust->m_active = false;\n    m_Explode = new Emitter((char*)\"/sprites/Explode.png\", 100,\n                             0, 360,\n                             1000, 0.3, false,\n                             20.0, 40.0,\n                             10, 0, 0, 5,\n                             1.0, 2.0,\n                             1.0, 2.0,\n                             0xffffff, 0xffffff,\n                             0.0, 10,\n                             false, false,\n                             800, 8 );\n    m_Explode->m_parent_rotation_ptr = &m_Rotation;\n    m_Explode->m_parent_x_ptr = &m_X;\n    m_Explode->m_parent_y_ptr = &m_Y;\n    m_Explode->m_active = false;\n}\n```\n\n前几行和老版本没什么变化。当我们将`m_Accelerating`初始化为`false`时，新的变化就开始了。之后，我们设置排气发射器，首先创建一个新的发射器，然后设置父值和调整值，最后将其设置为非活动状态:\n\n```cpp\nm_Exhaust = new Emitter((char*)\"/sprites/ProjectileExpOrange.png\", 200,\n                        -10, 10,\n                        400, 1.0, true,\n                        0.1, 0.1,\n                        30, 0, 12, 0.5,\n                        0.5, 1.0,\n                        0.5, 1.0,\n                        0xffffff, 0xffffff,\n                        0.7, 10,\n                        true, true,\n                        1000, 6 );\n\n m_Exhaust->m_parent_rotation_ptr = &m_Rotation;\n m_Exhaust->m_parent_x_ptr = &m_X;\n m_Exhaust->m_parent_y_ptr = &m_Y;\n m_Exhaust->m_x_adjustment = 10;\n m_Exhaust->m_y_adjustment = 10;\n m_Exhaust->m_active = false;\n```\n\n所有传递到`Emitter`函数的值都直接来自*粒子系统配置工具*。我们必须手动将它们添加到我们的函数调用中。如果我们在一个大项目上工作，这将不是非常可扩展的。我们可能会让配置工具创建某种数据文件(例如，JSON 或 XML)。但是为了方便起见，我们只是根据配置工具内部的内容对这些值进行了硬编码。不幸的是，这些值的顺序与它们在工具内部出现的顺序不同。您需要查看`Emitter`构造函数的签名，以确保将值放在正确的位置:\n\n```cpp\nEmitter(char* sprite_file, int max_particles, float min_angle, float max_angle,\n        Uint32 particle_lifetime, float acceleration, bool alpha_fade,\n        float min_starting_velocity, float max_starting_velocity,\n        Uint32 emission_rate, int x_pos, int y_pos, float radius,\n        float min_start_scale, float max_start_scale,\n        float min_end_scale, float max_end_scale,\n        Uint32 start_color, Uint32 end_color,\n        float burst_time_pct, Uint32 burst_particles,\n        bool loop, bool align_rotation, Uint32 emit_time_ms, Uint32 \n        animation_frames );\n```\n\n第一个参数`sprite_file`是文件在虚拟文件系统中的位置。该文件不会自动包含在您的项目中。您需要确保它位于正确的位置。我们将文件放在`sprites`目录中，并在运行 Emscripten 时使用以下标志:\n\n```cpp\n --preload-file sprites\n```\n\n在创建我们的`Exhaust`发射器之后，我们使用以下代码创建一个`Explosion`发射器:\n\n```cpp\nm_Explode = new Emitter((char*)\"/sprites/Explode.png\", 100,\n                         0, 360,\n                         1000, 0.3, false,\n                         20.0, 40.0,\n                         10, 0, 0, 5,\n                         1.0, 2.0,\n                         1.0, 2.0,\n                         0xffffff, 0xffffff,\n                         0.0, 10,\n                         false, false,\n                         800, 8 );\n\nm_Explode->m_parent_rotation_ptr = &m_Rotation;\nm_Explode->m_parent_x_ptr = &m_X;\nm_Explode->m_parent_y_ptr = &m_Y;\nm_Explode->m_active = false;\n```\n\n`m_Explode`发射器的创建类似于`m_Exhaust`发射器，但是我们根据在*粒子发射器配置工具*中创建的内容，将不同的值传递到发射器中:\n\n![](img/803f6bd8-9791-4cb1-aa82-2aaff1deb10f.png)\n\nFigure 9.5: Explosion configuration\n\n和`m_Exhaust`发射器一样，我们需要设置所有的父指针变量并关闭发射器。与`m_Exhaust`不同，我们不需要使用`m_x_adjustment`和`m_y_adjustment`属性进行微调。\n\n# 船舶级加速度函数\n\n我们只希望在船加速时运行废气排放器。为此，我们需要在我们飞船的`Accelerate`功能中设置一面旗帜。以下是加速功能的新版本:\n\n```cpp\nvoid Ship::Accelerate() {\n    m_Accelerating = true; // added line\n    m_VX += m_DX * delta_time;\n    m_VY += m_DY * delta_time;\n}\n```\n\n唯一的变化是在开头增加了一行，将`m_Accelerating`设置为`true`。当我们渲染船时，我们可以检查这个标志，并根据里面的值启动或停止发射器。\n\n# “船级”渲染功能\n\n对`Ship`级的最后修改是在飞船的`Render`功能中。在这个函数中，我们将需要添加移动和渲染两个新粒子系统的代码，以及如果船在加速时打开排气，如果没有加速时关闭排气的代码。以下是新版本的功能:\n\n```cpp\nvoid Ship::Render() {\n    if( m_Alive == false ) {\n        return;\n    }\n    m_Exhaust->Move();\n    m_Explode->Move();\n    dest.x = (int)m_X;\n    dest.y = (int)m_Y;\n    dest.w = c_Width;\n    dest.h = c_Height;\n    src.x = 32 * m_CurrentFrame;\n    float degrees = (m_Rotation / PI) * 180.0;\n    int return_code = SDL_RenderCopyEx( renderer, m_SpriteTexture,\n                                         &src, &dest,\n                                         degrees, NULL, SDL_FLIP_NONE );\n    if( return_code != 0 ) {\n        printf(\"failed to render image: %s\\n\", IMG_GetError() );\n    }\n\n    if( m_Accelerating == false ) {\n        m_Exhaust->m_active = false;\n    }\n    else {\n        m_Exhaust->m_active = true;\n    }\n    m_Accelerating = false;\n}\n```\n\n看看顶部附近添加的第一个代码块:\n\n```cpp\nm_Exhaust->Move();\nm_Explode->Move();\n```\n\n对发射器上的`Move`函数的调用会移动并渲染粒子系统内部的所有粒子。如果发射器需要的话，它也会产生新的粒子。在该功能的末尾，有处理废气排放器的代码:\n\n```cpp\nif( m_Accelerating == false ) {\n    m_Exhaust->m_active = false;\n}\nelse {\n    m_Exhaust->m_active = true;\n}\nm_Accelerating = false;\n```\n\n该代码检查`m_Accelerating`标志是否为`false`。如果是，我们就关闭废气排放器。如果船在加速，我们将`m_active`旗设置为`true`。我们不调用`Run`功能，因为我们每一帧都在这么做，而且我们不想在每次循环时都在那个发射器上启动*时间。最后一行将`m_Accelerating`设置为`false`。我们这样做是因为我们的代码中没有任何地方可以检测到船只何时停止加速。如果飞船正在加速，在我们到达代码中的这一点之前，该标志将被设置回`true`。如果没有，它将保持设置为`false`。*\n\n# 对弹丸池. cpp 的更改\n\n我们不需要在`ProjectilePool`类里面改动很多。事实上，我们只需要对一个函数进行两次修改。`ProjectilePool`级内部的`MoveProjectiles`功能执行所有射弹和我们两艘船之间的碰撞检测。如果一艘船被摧毁，我们在那艘船上运行`m_Explode`粒子发射器。这将需要在每艘船的命中测试条件中加入两行新代码。以下是新版本的`MoveProjectiles`功能:\n\n```cpp\nvoid ProjectilePool::MoveProjectiles() {\n    Projectile* projectile;\n    std::vector<Projectile*>::iterator it;\n    for( it = m_ProjectileList.begin(); it != m_ProjectileList.end(); it++ ) {\n        projectile = *it;\n        if( projectile->m_Active ) {\n            projectile->Move();\n            if( projectile->m_CurrentFrame == 0 &&\n                player->m_CurrentFrame == 0 &&\n                ( projectile->HitTest( player ) ||\n                    player->CompoundHitTest( projectile ) ) ) {\n                player->m_CurrentFrame = 1;\n                player->m_NextFrameTime = ms_per_frame;\n                player->m_Explode->Run(); // added\n                projectile->m_CurrentFrame = 1;\n                projectile->m_NextFrameTime = ms_per_frame;\n            }\n            if( projectile->m_CurrentFrame == 0 &&\n                enemy->m_CurrentFrame == 0 &&\n                ( projectile->HitTest( enemy ) ||\n                    enemy->CompoundHitTest( projectile ) ) ) {\n                enemy->m_CurrentFrame = 1;\n                enemy->m_NextFrameTime = ms_per_frame;\n                enemy->m_Explode->Run(); // added\n                projectile->m_CurrentFrame = 1;\n                projectile->m_NextFrameTime = ms_per_frame;\n            }\n        }\n    }\n}\n```\n\n我添加的两行代码用于调用`player->m_Explode->Run();`和`enemy->m_Explode->Run();`。当玩家的船或敌人的船与其中一个抛射体相撞并被摧毁时，这些线就会执行。\n\n# 更改 main.cpp\n\n为了添加排气和爆炸粒子系统，我们需要做的最后一个更改是`main.cpp`文件。这种变化需要增加一个单一的功能，`get_random_float`。我们之前讨论过这个函数。这是我们的粒子发射器获取介于最小值和最大值之间的随机浮点值的一种方式。下面是代码:\n\n```cpp\nfloat get_random_float( float min, float max ) {\n    int int_min = (int)(min * 1000);\n    int int_max = (int)(max * 1000);\n    if( int_min > int_max ) {\n        int temp = int_max;\n        int_max = int_min;\n        int_min = temp;\n    }\n    int int_diff = int_max - int_min;\n    int int_rand = (int_diff == 0) ? 0 : rand() % int_diff;\n    int_rand += int_min;\n    return (float)int_rand / 1000.0;\n}\n```\n\n# 编译新的粒子系统. html 文件\n\n现在我们已经对文件进行了所有必要的更改，我们可以继续使用 Emscripten 来编译和测试新版本的游戏。\n\nIf you are building this from the GitHub project, you will need to run this compile command from the `/Chapter09/` directory. The previous compile was done from inside the `/Chapter09/advanced-particle-tool/` directory, so make sure that you are in the right place when you run this command; otherwise, it won't have the files it needs to build the game.\n\n从命令行执行以下命令:\n\n```cpp\nem++ collider.cpp emitter.cpp enemy_ship.cpp particle.cpp player_ship.cpp point.cpp projectile_pool.cpp projectile.cpp ship.cpp main.cpp -o particle_system.html --preload-file sprites -std=c++ 17 -s USE_WEBGL2=1 -s USE_SDL=2 -s USE_SDL_IMAGE=2 -s SDL2_IMAGE_FORMATS=[\"png\"] -s USE_SDL_IMAGE=2 -s SDL2_IMAGE_FORMATS=[\"png\"]\n```\n\n# 更进一步\n\n我们不会为配置编写数据导出工具。这一章太长了。当你创建粒子系统的时候，你可以花几乎无限的时间来调整它们。粒子系统可以有大量的配置参数。您甚至可以使用贝塞尔曲线进行移动、旋转和缩放。一些先进的粒子系统具有发射其他粒子的粒子。我们可以给一个粒子系统增加的复杂性是没有限制的，但是我在这本书里可以拥有的页数是有限制的，所以我鼓励你拿着这个系统，加入它，直到你得到你想要的结果。\n\n# 摘要\n\n恭喜你！你已经读完了一个很长的、充满信息的章节。在最后两章中，我们讨论了什么是粒子系统以及为什么使用它们。我们学习了如何向网络程序集虚拟文件系统添加文件以及如何访问它。我们学习了如何在 HTML 外壳文件和 WebAssembly 模块之间创建更高级的交互。然后，我们构建了一个更高级的粒子发射器配置工具，具有更多的功能。在工具中构建了一些好看的粒子系统后，我们获取了数据和代码，并使用它在我们一直在构建的游戏中构建了两个新的粒子发射器。\n\n在下一章中，我们将讨论并为我们的敌人飞船构建人工智能。"
  },
  {
    "path": "docs/handson-game-dev-wasm/10.md",
    "content": "# 十、人工智能与驾驶行为\n\n我们一直在写的游戏，松散的基于电脑游戏 *Spacewar！*如果你不熟悉*太空战！*，这是有史以来写的第一款电脑游戏。它最初运行在麻省理工学院拥有的 PDP-1 上，由麻省理工学院的学生史蒂夫·拉塞尔于 1962 年编写。当时，仅仅让计算机显示图形输出已经足够困难了。*太空战！*以及许多其他早期的游戏系统，如 *Pong* ，都是设计成多人玩的。那是因为把计算机编程得像人一样是一件非常困难的事情。这在今天仍然有些道理，尽管更多的处理能力和数据允许现代**人工智能** ( **AI** )算法比过去表现得更加智能。\n\n因为我们的游戏是一个单人网络游戏，我们没有使用第二个人类智能来驱动我们的敌人飞船的好处。在这一章之前，我们使用了一个人工智能存根来允许我们的敌人飞船在我们的游戏区域内随意移动和射击。到目前为止，这可能对我们有用，但现在我们希望我们的玩家感受到敌人船只的威胁。它应该足够聪明，可以在一对一的战斗中战斗并杀死我们的玩家。\n\nYou will need to include several images in your build to make this project work. Make sure you include the `/Chapter10/sprites/` folder from the project's GitHub. If you haven't yet downloaded the GitHub project, you can get it online at: [https://github.com/PacktPublishing/Hands-On-Game-Development-with-WebAssembly](https://github.com/PacktPublishing/Hands-On-Game-Development-with-WebAssembly).\n\n在本章中，我们将执行以下操作:\n\n*   介绍人工智能和游戏人工智能的概念\n*   为游戏增加障碍以避免 AI(并增加画布大小)\n*   为视线添加新的碰撞检测\n*   引入**有限状态机** ( **有限状态机**)的概念\n*   引入**自主代理**的概念\n*   引入**转向行为**的概念\n*   给我们的游戏增加力场\n*   使用有限状态机和操纵行为来创建人工智能\n*   调整我们的人工智能，让敌人的宇宙飞船通过障碍物\n\n# 什么是游戏 AI？\n\n许多早期的视频游戏避免了人工智能，因为在当时可用的硬件中，这是一个非常具有挑战性的问题。例如*太空入侵者**加拉加*和*加拉西安*都有以特定的非智能模式移动的外星人。早期的雅达利游戏要么是双人游戏( *Pong* )，要么是让玩家与非智能环境交互(*breaking*)。一个早期成功的人工智能游戏尝试是*吃豆人*。 *PAC-MAN* 中的每个鬼魂都有不同的性格，在相同的情况下会表现得有些不同。 *PAC-MAN* 也使用了简单的**有限状态机** ( **FSM** )。这是一种人工智能，人工智能在不同的环境下表现不同。如果玩家在“吃豆人”中吃了一个“T21”能量球，鬼魂就会变成蓝色，然后在“猎人变成猎物”的“财富逆转”中突然变得可以吃。虽然鬼魂可以被吃掉，但对程序员来说，让这些鬼魂像以前一样继续追捕“吃豆人”会更容易。这将使鬼魂看起来要么愚蠢，要么有自杀倾向，这是我们在写人工智能时希望避免的行为。\n\n1950 年，数学和计算机天才艾伦·图灵为人工智能提出了一个基准，他称之为“模仿游戏”，但后来它被称为图灵测试。他提出了一种游戏，让人类玩家通过基于文本的界面与人类和计算机互动。如果一台计算机能让一个人相信他们正在与另一个人而不是一台计算机进行交互，那么这台计算机应该被认为是智能的。就我个人而言，我觉得我们好像很久以前就通过了这个门槛。但是当机器威胁到人类的智力时，人类喜欢移动目标。\n\n1964 年，麻省理工学院的约瑟夫·韦森鲍姆写了一个名叫伊莱扎的聊天机器人。伊莱扎在聊天系统的另一端假装是一名心理治疗师。伊莱扎设法愚弄了相当多的人，让他们相信这是一个真正的心理治疗师，这可能既是对心理治疗的评论，也是对人类智力的评论。任何寻找聊天机器人的人都可以很容易地分辨出 ELLIA 不是人类，但是 Joesph Weizenbaum 对愿意向 ELLIA 倾诉心声的人数感到非常不安，好像她是一个真实的人。\n\n罗布纳奖是一年一度的图灵测试比赛，一系列身为人工智能专家的评委还没有被一个聊天机器人愚弄。如今，许多程序经常愚弄人们，让他们以为自己是人类。我认为，需要一个人类专家来确定一个人工智能是否通过了图灵测试，这将大大偏离艾伦·图灵最初设定的目标。我相信，如果我们有大量被聊天机器人愚弄的非专家样本，我们应该认为聊天机器人是智能的，但我跑题了。\n\n我提出图灵测试的观点是，游戏人工智能需要通过图灵测试的修改形式。当你写一个游戏人工智能时，你的目标是让玩家相信他们不是在和一个完全失败的人玩游戏。所有的游戏人工智能或多或少都是蹩脚的。目前，我们将无法创建一个 IBM 沃森的游戏人工智能版本(在危险中击败肯·詹宁斯的人工智能)。就像电脑游戏中的一切一样，我们需要学会在系统的约束下工作。对于基于网络的游戏来说，这些限制可能很重要。\n\n记住，作弊可以，但不要被抓。许多游戏玩家作弊。一个 RTS 也许能看穿战争的迷雾。人工智能扑克玩家可能会在玩家的牌上达到顶峰。我们要作弊的一个方法是允许我们的敌人飞船向玩家不允许的方向加速。用游戏人工智能作弊的关键是确保行为或动作看起来不自然。很多年前，我写了一个在线版的纸牌游戏黑桃，可在[https://www.icardgames.com/spades.html](https://www.icardgames.com/spades.html)玩。玩家的伙伴 AI 被允许在每个人的牌上达到顶峰。我得到的一个常见抱怨是，玩家的搭档经常会胜过玩家的高牌。这是因为人工智能看的不是当前谁赢了这一招，而是如果跟随他的玩家没有打出比他领先的牌更高的牌，他是否能赢这一招。没有意识到这种行为正在帮助他们，我从玩家那里得到了许多沮丧的抱怨，关于合作伙伴胜过他们的牌。这是一个例子，玩家实际上因为人工智能做得更好，但给人的印象是人工智能正在做出愚蠢的选择。我的观点是，游戏 AI 就是印象。还记得 AI 主持人在 HBO 电视节目*西部世界*中说的话吗，其中一个角色问她是不是真的:“如果你分不清，真的有关系吗？”\n\n# 自主代理与自顶向下人工智能\n\n1986 年，克雷格·雷诺兹创建了一个备受关注的人工智能程序，名为 *Boids* (鸟和机器人的结合体)。这个程序创造了一种令人着迷的类似鸟的群集行为，小三角形在屏幕上移动，提醒观察者成群结队的鸟或鱼。当环境有障碍时，机器人会分开绕过障碍，然后重新加入。两群鸟之间的碰撞通常会以鸟群会合并继续前进而告终。Boids 算法是用于人工智能的自治代理的实现。每个独立的 boid 基于一些简单的规则和它的直接环境做出决定。这导致了所谓的**紧急行为**，这种行为看起来好像是自上而下设计的，但实际上并非如此。具有讽刺意味的是，自上而下实现的人工智能通常看起来不如允许单个代理做出自己的决定聪明。这有点像旧的苏联自上而下的命令和控制经济，而不是资本主义经济，在资本主义经济中，个人根据他们周围的环境做出决定。在游戏中，就像在经济学中一样，你也可以有一个混合系统，其中自上而下的人工智能可以向自主代理发送消息，给他们新的目标或指令。在我们正在编写的游戏中，我们有一个单一的敌人飞船，因此自上而下或通过自主代理来管理 AI 的决定实际上并没有太大关系，但因为您可能会选择在未来扩展游戏以支持多个敌人及其 AI，所以我们的代理将自主管理自己。\n\n# 什么是密克罗尼西亚联邦？\n\nFSM 在游戏中非常常见。正如我之前提到的，PAC-MAN 是一个早期的游戏，它有一个多状态的 AI。当吃豆人吃掉屏幕上的一个大点，也就是通常所说的“T4”能量球“T5”时，基于全局条件的翻转，鬼魂可能处于“T0”狩猎“T1”或“T2”逃跑“T3”状态。FSM 中的特定状态可以是全局条件，或者在**有限** **状态自动机**的情况下，可以是游戏中任何*自治代理*特定的状态。管理行为或状态转换可以像使用 switch 语句一样简单，也可以是在触发不同状态时加载和卸载 AI 模块的更复杂的系统。状态可以选择何时转换到不同的状态，或者状态转换可以由游戏自上而下地管理。\n\n我们将为这个游戏编写的有限状态机将是非常基本的。这将是一个简单的开关，根据当前状态执行不同的行为。敌舰相对于玩家的位置以及它们之间是否有通畅的视线将用于确定状态之间的转换。我们的密克罗尼西亚联邦将有四种基本状态:\n\n1.  `WANDER`\n2.  `APPROACH`\n3.  `ATTACK`\n4.  `FLEE`\n\n进入这些状态的条件如下:如果敌舰没有通往玩家舰的通畅路径，则进入`WANDER`状态，在该状态下，它会在游戏区域四处游荡，定期检查是否有通往玩家的视线路径。一旦有了玩家的视线路径，敌舰就会进入`APPROACH`状态，在该状态下它会尝试足够靠近玩家舰来攻击它。一旦玩家足够接近，它就会进入`ATTACK`状态，在玩家船上开火。如果玩家船离敌人船太近，敌人会`FLEE`，试图增加自己和玩家船之间的距离。\n\n# 引入转向行为\n\n转向行为是一种基于力的方法，用于在避开障碍物的同时朝着或远离特定点导航。它最初是由 Craig Reynolds(the*Boids*guy)在 1999 年**游戏开发者大会** ( **GDC** )上的演讲中讨论的，讨论转向行为的原始论文可以在网上[https://www.red3d.com/cwr/steer/gdc99/](https://www.red3d.com/cwr/steer/gdc99/)上找到。与寻路算法(如 A*或 Dijkstra 算法)不同，转向行为本质上是战术性的。它们包括一个目标位置，并迫使自主代理向其目标移动，同时将代理推离您希望它避开的障碍。在我们的游戏中，敌方飞船是我们的自主代理，它将使用转向行为。它将追逐玩家飞船，同时避开包括小行星、抛射体和游戏区域中心的恒星在内的障碍物。在接下来的几节中，我们将详细讨论几种转向行为。\n\n# 寻求行为\n\n**寻道转向行为**是一种将代理(敌船)指向所需目标并将代理向该目标方向移动的力。这种行为试图达到最大速度，并在最短时间内达到目标。寻道行为假设它所寻找的位置是静态的，不会随着时间而改变。此图显示了寻道行为的样子:\n\n![](img/be373985-1267-43f6-841e-9c1153c9ed8e.png)\n\nThe seek behavior\n\n# 逃跑行为\n\n**逃离**是一种转向行为，与寻求行为相反。这种行为占据一个位置或游戏对象，并试图尽可能远离它。\n\nFleeing is the behavior you demonstrate when chased by a bear. Your only goal is to put as much distance between you and the current location of that bear as you can. So, the next time a bear chases you, stop for a moment and think, \"*Wow, my brain is currently implementing a version of the autonomous agent steering behavior known as* flee.\" Or you could keep running. The choice is yours. Take a look at the next diagram:\n\n![](img/9d89bc0a-4783-4973-bc39-b34168a6135a.png)\n\nAn artist's rendering of a bear eating the reader\n\n你可以通过否定寻找行为的方向来编程逃离行为。换句话说，如果寻道行为产生的方向矢量力为 1，1，则逃离转向行为将产生的方向矢量力为-1，-1。此图描述了逃离行为:\n\n![](img/95bd7792-7a2a-4e73-933c-633246625c9a.png)\n\nThe flee behavior\n\n# 到达行为\n\n寻道转向行为的问题在于，在代理到达其目标位置之前，它不会得到满足。另一个问题是，因为它试图以最大速度到达那个位置，它几乎总是会超过它，导致围绕期望的目的地振荡。当到达目标的**到达范围**时，**到达转向行为**允许通过开始减速来优雅地结束搜寻行为。只要目标目的地在期望的范围内，到达行为将减少向搜寻位置的移动。下图描述了到达行为:\n\n![](img/9d63484e-dbbf-4987-bf91-7a9593292d5d.png)\n\nThe arrival behavior\n\n# 追求行为\n\n我们在寻求行为的基础上建立**追求行为**。当寻道行为希望到达一个静态点时，寻道行为假设目标在移动。因为我们的代理(敌船)希望追踪并摧毁玩家，玩家通常是移动的，所以我们将使用追击转向行为。追击行为看目标的速度。它不是直接向目标的当前位置前进，而是试图找到它预测的目标所在的拦截点。Seek 让我想起了一个儿童足球队。所有的孩子都跑向球在哪里，而不是球会在哪里。正因为如此，足球场上的每个人都像一个大单元一样在场上跑来跑去。总有一天，他们会长大，把追逐引导行为融入到他们的足球策略中。\n\n下图描述了追求行为:\n\n![](img/8f81489f-b09c-4751-8fec-6a85da83f056.png)\n\nThe pursuit behavior\n\n# 逃避行为\n\n**躲避**是为了*追击*如同*逃跑*是为了*寻找*。和追击一样，**躲避转向行为**是试图确定你正在躲避的障碍物会在哪里，并尽可能远离那个点。换句话说，它采取了我们在追求行为中发现的相同点，然后从那个点逃跑。下图描述了规避行为:\n\n![](img/5951f12c-2eef-48be-a168-5863dad8d48c.png)\n\nThe evade behavior\n\n# 排除故障\n\n**避障**与逃离和躲避行为的不同之处在于，当我们的代理试图寻找新的位置时，障碍物可能会潜在地出现在我们的代理的路径上。逃离和躲避使我们试图尽可能远离物体的位置或我们正在逃离的位置，而避障更多的是为了避免在到达目标的途中与障碍物发生碰撞。在我们的游戏中，需要避开的障碍物包括小行星、抛射体和游戏屏幕中央的星星。避障通常只涉及寻求避开最具威胁性(最近的)障碍物。我们的代理有一个给定的前视距离，可以看到它移动的方向。如果它的当前位置和它移动方向上的最大前视之间的一条线与一个物体相撞，避障要求我们调整方向。我们躲避的区域应该比障碍物的碰撞检测区域要大，这样才能给我们一个躲避的缓冲区，尤其是因为游戏中小行星和抛射体都在移动。\n\n下图描述了避障:\n\n![](img/558939ad-5d41-4b33-8c37-29b4547e82c9.png)\n\nObstacle avoidance\n\n# 流浪行为\n\n**游走**是代理在游戏画面中有些随机移动的状态。让敌人飞船的方向随机旋转每一帧会导致非常不稳定的行为。相反，应该有一个随机的毫秒数(200-2000)，飞船保持其当前方向。当船已经走了随机的毫秒数，它应该随机选择左转或右转，但应该有一个偏向性的机会转向与前一次相同的方向，这种偏向性在最初选择后每次选择相同的方向时都会减少。这将使流浪行为更加一致，显得不那么紧张。\n\n查看游走行为如何随机选择一个点并向其移动:\n\n![](img/93567b37-73f1-48e3-ade1-8b5e61fdb426.png)\n\nDemonstrating the wander behavior\n\n# 合力\n\n我们之前对读者利用逃跑行为逃离熊的讨论过于简单。它假设你正在一片开阔的田野里逃离那只熊。如果你在树林里躲避一只熊，你俩都需要避免跑进树林，尽可能远离那只熊。你必须将这两种活动无缝融合，否则就会被那只熊吃掉。如果我们想让敌船追击或逃离玩家船，同时避开障碍物，我们就需要联合转向力量。最优先考虑的总是避免障碍。如果你在逃离那只熊的时候撞上了一棵树，它最终还是会吃掉你。我们的转向行为将实施的一般策略是找到玩家船的视线向量。有几个机会，我们必须找到一个视线，因为我们的游戏水平缠绕在自己身上的方式。如果那条视线比选择的距离长，我们会徘徊，直到我们的距离足够短，我们可以在向玩家射击的同时追上他。当我们漫游时，我们会希望将任何漫游力量与帮助敌船避免撞击小行星或恒星的力量结合起来。一旦我们在追求，我们就会想要继续避开障碍。会有一个很大的到达区域，我们的船会慢慢停下来，向玩家的方向开火。一旦玩家接近特定范围，我们的船就会逃跑。\n\n# 修改 game.hpp\n\n在我们深入了解新代码之前，我想对`game.hpp`文件进行一些快速更改，以添加一些我们将在本章稍后使用的功能。我想在`game.hpp`文件顶部附近添加的第一件事是几个宏，它们可以让我们快速地从角度(以度为单位)转换为弧度，也可以从弧度转换为角度。我发现自己在使用 SDL 的时候经常这样做，因为 SDL，出于某种原因，想要以度为单位的旋转，而其他所有的库都使用弧度。因此，让我们继续在靠近`game.hpp`文件顶部的某处添加以下两行代码:\n\n```cpp\n#define DEG_TO_RAD(deg) ((float)deg/180.0)*3.14159\n#define RAD_TO_DEG(rad) ((float)rad*180.0)/3.14159\n```\n\n我们将把画布的尺寸从 320 x 200 改为 800 x 600。为了便于以后切换，我们先定义几个宏，用于画布的宽度和高度，并将它们放在靠近`game.hpp`文件顶部的某个地方:\n\n```cpp\n#define CANVAS_WIDTH 800\n#define CANVAS_HEIGHT 600\n```\n\n在 C 和 C++ 中用来获取随机数的`rand()`函数，只能用来返回一个整数。我将添加一个函数来获取介于最小和最大浮点值之间的随机数，因此我需要在我们的`game.hpp`文件中添加对该函数的外部引用:\n\n```cpp\nextern float get_random_float( float min, float max );\n```\n\n我们也开始需要循环引用。`FiniteStateMachine`类需要引用`EnemyShip`类，`EnemyShip`类需要引用`FiniteStateMachine`类。不幸的是，我们需要先定义其中一个类，再定义另一个类。在过去，我们已经能够以特定的顺序定义我们的类来避免这个问题，但是现在我们需要在任何类定义之前有一组类声明。这将允许编译器知道一个类将在定义之前被定义。将这个类声明块添加到`game.hpp`文件顶部附近的某个地方:\n\n```cpp\nclass Ship;\nclass Particle;\nclass Emitter;\nclass Collider;\nclass Asteroid;\nclass Star;\nclass PlayerShip;\nclass EnemyShip;\nclass Projectile;\nclass ProjectilePool;\nclass FiniteStateMachine;\n```\n\n我们将添加一个枚举来跟踪我们的 FSM 状态。正如我前面提到的，我们的密克罗尼西亚联邦有四个州:`APPROACH`、`ATTACK`、`FLEE`和`WANDER`。我们将在一个名为`FSM_STATE`的枚举中定义这些状态:\n\n```cpp\nenum FSM_STATE {\n    APPROACH = 0,\n    ATTACK = 1,\n    FLEE = 2,\n    WANDER = 3\n};\n```\n\n我们在`game.hpp`中定义的第一个类之一是`Point`类。这个职业有 *x* 和 *y* 属性和一些有用的功能，比如`Rotate`。我们需要极大地扩展这个类的用途以及它能做什么。以至于称之为*点*不再准确。我更愿意把这个类叫做*向量*，因为从现在开始我们将把它用于向量数学。我对这个名字唯一的问题是它可能会令人困惑，因为我们在代码中使用`std::vector`来处理类似数组的数据。正因为如此，我决定把这个班叫做`Vector2D`。我们将大大扩展这个类的功能，包括一个将向量归一化的函数(也就是说，将其大小改为 1)。我们需要两个函数来确定向量的大小和平方大小。我们需要一个函数将向量投影到另一个向量上(以帮助我们进行视线碰撞检测)。我们需要能够找到两个向量的点积。我们还需要能够找到给定向量的旋转。除了这些新函数之外，我们将在向量上重载运算符，以允许我们添加向量、减去向量以及用标量值乘和除向量。\n\n继续删除`Point`类定义，并用新的`Vector2D`类定义替换该代码:\n\n```cpp\nclass Vector2D {\n    public:\n        float x;\n        float y;\n\n        Vector2D();\n        Vector2D( float X, float Y );\n\n        void Rotate( float radians );\n        void Normalize();\n        float MagSQ();\n        float Magnitude();\n        Vector2D Project( Vector2D &onto );\n        float Dot(Vector2D &vec);\n        float FindRotation();\n\n        Vector2D operator=(const Vector2D &vec);\n        Vector2D operator*(const float &scalar);\n        void operator+=(const Vector2D &vec);\n        void operator-=(const Vector2D &vec);\n        void operator*=(const float &scalar);\n        void operator/=(const float &scalar);\n };\n```\n\n我们新的碰撞检测也需要一个`Range`类。范围表示最小值和最大值之间的值范围。我们可以把两个范围加在一起。我们可以找到这两个范围之间的重叠。我们可以通过给定的标量值来扩展一个范围，或者我们可以将一个值限制在给定的范围内。以下是新的`Range`类定义:\n\n```cpp\nclass Range {\n    public:\n        float min;\n        float max;\n\n        Range();\n        Range( float min_val, float max_val );\n\n        void operator+=(const Range& range);\n        Range operator+(const Range& range);\n        Range operator=(const Range& range);\n\n        bool Overlap( Range &other );\n        void Sort();\n        void Extend( float ex );\n        float Clamp( float value );\n };\n```\n\n如果您向下滚动到`Collider`类，我们将添加一些新的函数和一些新的属性。我想使用我们的`Collider`类来支持新的转向行为。因此，我们需要一些特定于转向的属性:\n\n```cpp\nfloat m_SteeringRadius;\nfloat m_SteeringRadiusSQ;\n```\n\n`m_SteeringRadius`是`m_Radius`的倍数的新属性。出于转向的目的，我们希望确保我们想要避免的物体的尺寸小于物体的碰撞面积。这为我们的转向行为创造了额外的空间，有助于我们避开这些物体。`m_SteeringRadiusSQ`属性是转向半径的平方。这将使我们不必为了一次又一次的碰撞检查而调整转向半径。\n\n我们还需要添加以下函数的声明:\n\n```cpp\nbool SteeringLineTest( Vector2D &p1, Vector2D &p2 );\nbool SteeringRectTest( Vector2D &start_point, Vector2D &end_point );\nvoid WrapPosition();\n```\n\n`SteeringLineTest`和`SteeringRecTest`功能将不同于实线和矩形碰撞测试。转向矩形测试(`SterringRectTest`)将用于限制我们为避免物体碰撞而必须测试的物体数量。我们只希望我们的人工智能担心敌人飞船周围 200 x 200 像素的盒子内的物体。如果我们有大量的对象要测试，这将非常有用。为了保持快速测试，我们将检查该框中的对象，就像它们是点一样，并且不会考虑对象的半径。`SteeringLineTest`功能将测试该对撞机的转向半径是否击中测试中两点定义的直线。\n\n在我们的游戏中，我们没有增加命中点系统。与小行星或抛射体的一次碰撞会导致瞬间死亡。这让比赛变得非常短。为了增加游戏时间，我们将在船上增加护盾。只要护盾有效，这些护盾将使玩家或敌人无坚不摧。当你使用护盾时，它们会慢慢从绿色变成红色，在某个时候，它们会停止工作。这将取决于你在给定游戏中使用护盾的时间，以鼓励玩家仅在需要时使用护盾。以下是`Shield`类的定义:\n\n```cpp\nclass Shield : public Collider {\n    public:\n        bool m_Active;\n        int m_ttl;\n        int m_NextFrame;\n        Uint32 m_CurrentFrame;\n        Ship* m_Ship;\n        SDL_Texture *m_SpriteTexture;\n\n        SDL_Rect m_src = {.x = 0, .y = 0, .w = 32, .h = 32 };\n        SDL_Rect m_dest = {.x = 0, .y = 0, .w = 32, .h = 32 };\n\n        Shield( Ship* ship, const char* sprite_file );\n\n        void Move();\n        void Render();\n        bool Activate();\n        void Deactivate();\n};\n```\n\n在`Shield`类定义之后，我们需要为我们的`Asteroid`类添加一个类定义。与雅达利游戏*小行星*不同，我们无法通过射击来摧毁这些小行星。它们本来就是障碍物，但是如果玩家在护盾激活的情况下撞上它们，我们会(暂时)允许小行星被摧毁。它们将在游戏屏幕周围缓慢移动，并在游戏过程中为玩家和敌人的人工智能导航提供障碍。下面是代码:\n\n```cpp\nclass Asteroid : public Collider {\n    public:\n        SDL_Texture *m_SpriteTexture;\n        SDL_Rect m_src = {.x = 0, .y = 0, .w = 16, .h = 16 };\n        SDL_Rect m_dest = {.x = 0, .y = 0, .w = 0, .h = 0 };\n\n        bool m_Alive;\n        Uint32 m_CurrentFrame = 0;\n        int m_NextFrameTime;\n        float m_Rotation;\n\n        Vector2D m_Direction;\n        Vector2D m_Velocity;\n\n        Emitter* m_Explode;\n        Emitter* m_Chunks;\n\n        Asteroid( float x, float y,\n                  float velocity,\n                  float rotation );\n\n        void Move();\n        void Render();\n        void Explode();\n};\n```\n\n我们还将在游戏区域的中心增加一颗大星星。这类似于游戏中心的黑洞*太空战！*，这是我们游戏的松散基础。这颗恒星最终将提供引力，使游戏更具挑战性。我们将制作一张恒星图像的动画，并使用粒子发射器添加一些太阳耀斑:\n\n```cpp\nclass Star : public Collider {\n    public:\n        SDL_Texture *m_SpriteTexture;\n        SDL_Rect m_src = {.x = 0, .y = 0, .w = 64, .h = 64 };\n        SDL_Rect m_dest = {.x = 0, .y = 0, .w = 64, .h = 64 };\n\n        std::vector<Emitter*> m_FlareList;\n        Uint32 m_CurrentFrame = 0;\n        int m_NextFrameTime;\n\n        Star();\n\n        void Move();\n        void Render();\n};\n```\n\n现在我们可以对我们的`Ship`类做一些修改。以下是我们完成后的样子:\n\n```cpp\nclass Ship : public Collider {\n    public:\n        const float c_Acceleration = 10.0f;\n        const float c_MaxVelocity = 50.0f;\n        const int c_AliveTime = 2000;\n        const Uint32 c_MinLaunchTime = 300;\n        const int c_Width = 32;\n        const int c_Height = 32;\n\n        bool m_Accelerating = false;\n        Uint32 m_LastLaunchTime;\n        SDL_Texture *m_SpriteTexture;\n        SDL_Rect src = {.x = 0, .y = 0, .w = 32, .h = 32 };\n\n        Emitter* m_Explode;\n        Emitter* m_Exhaust;\n        Shield* m_Shield;\n        std::vector<Collider*> m_Colliders;\n\n        bool m_Alive = true;\n        Uint32 m_CurrentFrame = 0;\n        int m_NextFrameTime;\n        float m_Rotation;\n\n        Vector2D m_Direction;\n        Vector2D m_Velocity;\n\n        void RotateLeft();\n        void RotateRight();\n        void Accelerate();\n        void Decelerate();\n        void CapVelocity();\n        void Shoot();\n        virtual void Move() = 0;\n        Ship();\n        void Render();\n        bool CompoundHitTest( Collider* collider );\n};\n```\n\n我们要做的第一件事是添加`m_Shield`属性，这是一个指向`Shield`对象的指针:\n\n```cpp\nShield* m_Shield;\n```\n\n之后，我们对 *x* 方向和 *y* 方向使用单独的变量，对 *x* 速度和 *y* 速度使用不同的变量，如下所示:\n\n```cpp\ndouble m_DX;  // x-direction variable\ndouble m_DY;  // y-direction variable\ndouble m_VX;  // x-velocity variable\ndouble m_VY;  // y-velocity variable\n```\n\n让我们去掉所有的代码，换成一些`Vector2D`对象，表示方向向量和速度向量，如下所示:\n\n```cpp\nVector2D m_Direction;\nVector2D m_Velocity;\n```\n\n最后，为了防止敌舰和玩家舰之间的代码重复，我们将增加一个`Shoot()`功能，从飞船发射一枚炮弹:\n\n```cpp\nvoid Shoot();\n```\n\n我们需要修改的下一个类是我们的`EnemyShip`类。我们需要添加一个带有`Shield`精灵文件名的字符串。我们还需要删除旧的`AIStub()`函数，并将其替换为指向我们的有限状态机的指针。以下是新版本的`EnemyShip`类的外观:\n\n```cpp\nclass EnemyShip: public Ship {\n    public:\n        const char* c_SpriteFile = \"/sprites/BirdOfAngerExp.png\";\n        const char* c_ShieldSpriteFile = \"/sprites/shield-bird.png\";\n        const int c_AIStateTime = 2000;\n\n        int m_AIStateTTL;\n        FiniteStateMachine* m_FSM;\n\n        EnemyShip();\n        void Move();\n};\n```\n\n我们将要添加的一个重要的新类是`FiniteStateMachine`类。这门课将承担人工智能的所有重任。以下是您必须添加到`game.hpp`中的类定义:\n\n```cpp\nclass FiniteStateMachine {\n    public:\n        const float c_AttackDistSq = 40000.0;\n        const float c_FleeDistSq = 2500.0;\n        const int c_MinRandomTurnMS = 100;\n        const int c_RandTurnMS = 3000;\n        const int c_ShieldDist = 20;\n        const int c_AvoidDist = 80;\n        const int c_StarAvoidDistSQ = 20000;\n        const int c_ObstacleAvoidForce = 150;\n        const int c_StarAvoidForce = 120;\n\n        FSM_STATE m_CurrentState;\n        EnemyShip* m_Ship;\n        bool m_HasLOS;\n        bool m_LastTurnLeft;\n        int m_SameTurnPct;\n        int m_NextTurnMS;\n        int m_CheckCycle;\n        float m_DesiredRotation;\n        float m_PlayerDistSQ;\n\n        FiniteStateMachine(EnemyShip* ship);\n\n        void SeekState(Vector2D &seek_point);\n        void FleeState(Vector2D &flee_point);\n        void WanderState();\n        void AttackState();\n        void AvoidForce();\n        bool ShieldCheck();\n        bool LOSCheck();\n        Vector2D PredictPosition();\n        float GetPlayerDistSq();\n        void Move();\n};\n```\n\n在这个类定义的顶部是九个常数:\n\n```cpp\n const float c_AttackDistSq = 40000.0;\n const float c_FleeDistSq = 2500.0;\n const int c_MinRandomTurnMS = 100;\n const int c_RandTurnMS = 3000;\n const int c_ShieldDist = 20;\n const int c_AvoidDist = 80;\n const int c_StarAvoidDistSQ = 20000;\n const int c_ObstacleAvoidForce = 150;\n const int c_StarAvoidForce = 120;\n```\n\n前两个常数`c_AttackDistSq`和`c_FleeDistSq`是密克罗尼西亚联邦用来确定是否将状态变为`ATTACK`或`FLEE`状态的值；`c_MinRandomTurnMS`和`c_RandTurnMS`都是`WANDER`状态用来确定 AI 下一次决定随机改变方向的常数。`c_ShieldDist`常数是障碍物将导致人工智能开启其护盾的距离。`c_AvoidDist`常数给出了人工智能进行校正调整以避开物体的范围。`c_StarAvoidDistSQ`功能是 AI 进行航向调整以避开游戏区域中心的星星的距离。`c_ObstacleAvoidForce`常数是一个加在物体速度上的转向力，帮助物体避开障碍物，`c_StarAvoidForce`是一个类似的力，用来避开恒星。\n\n在常量之后，我们有一组属性，由 FSM 用来做出基于状态的决策:\n\n```cpp\n FSM_STATE m_CurrentState;\n EnemyShip* m_Ship;\n bool m_HasLOS;\n bool m_LastTurnLeft;\n int m_SameTurnPct;\n int m_NextTurnMS;\n int m_CheckCycle;\n float m_DesiredRotation;\n float m_PlayerDistSQ;\n```\n\n`m_CurrentState`属性保存我们的 FSM 的当前状态。`m_Ship`属性包含一个指向船只的指针。现在，这总是我们游戏中的单个敌舰，但是在未来，你可能会想要添加多个敌舰。`m_HasLOS`属性是一个`boolean`，用来记录我们的飞船当前是否有玩家的无障碍视线。`m_LastTurnLeft`属性是一个`boolean`，它记录了船在`WANDER`状态下最后一次转向的方向。`m_SameTurnPct`属性是船在`WANDER`状态下继续朝同一个方向转弯的概率百分比。`m_NextTurnMS`属性是在改变方向航向之前，处于`WANDER`状态的船只将持续的毫秒数。`m_CheckCycle`变量用于将人工智能分解为在不同的帧渲染周期中执行不同的检查。如果你让你的人工智能在每次渲染的每一帧之间做所有的工作，你可能会让系统陷入困境。通常更好的做法是将人工智能分成多个部分，并在每个帧渲染时只执行部分逻辑。`m_DesiredRotation`属性是 AI 想要的航向，最后`m_PlayerDistSQ`是敌舰和玩家舰的平方距离。\n\n我们需要修改`Projectile`类，使用一个`Vector2D`来跟踪速度，而不是两个浮点变量，`m_VX`和`m_VY`。以下是修改后的新版`Projectile`类:\n\n```cpp\nclass Projectile: public Collider {\n    public:\n        const char* c_SpriteFile = \"sprites/ProjectileExp.png\";\n        const int c_Width = 16;\n        const int c_Height = 16;\n        const double velocity = 6.0;\n        const double alive_time = 2000;\n\n        SDL_Texture *m_SpriteTexture;\n        SDL_Rect src = {.x = 0, .y = 0, .w = 16, .h = 16 };\n\n        Uint32 m_CurrentFrame = 0;\n        int m_NextFrameTime;\n        bool m_Active;\n        float m_TTL;\n        float m_VX;\n        float m_VY;\n\n        Projectile();\n        void Move();\n        void Render();\n        void Launch(double x, double y, double dx, double dy);\n};\n```\n\n在`game.hpp`文件的末尾，我们应该给我们新的小行星列表添加一些外部参考，以及将在游戏区域中心出现的恒星:\n\n```cpp\nextern std::vector<Asteroid*> asteroid_list;\nextern Star* star;\n```\n\n现在我们已经处理了需要对`game.hpp`文件进行的修改，让我们进入正在添加的障碍。\n\n# 给我们的游戏增加障碍\n\n现在，我们的游戏中没有任何人工智能可以操控的东西。我们需要增加一些障碍物来阻挡我们的敌舰。我们希望我们的敌船在试图接近和攻击我们玩家的飞船时，尽可能避开这些障碍。我们将添加的第一件事是在我们的游戏区域的正中间有一颗大星星。我们可以制作这颗恒星的动画，并为恒星的日冕添加一些不错的粒子效果。在最后一节中，我们在`game.hpp`文件中创建了这颗星的类定义，它看起来像这样:\n\n```cpp\nclass Star : public Collider {\n    public:\n        SDL_Texture *m_SpriteTexture;\n        SDL_Rect m_src = {.x = 0, .y = 0, .w = 64, .h = 64 };\n        SDL_Rect m_dest = {.x = 0, .y = 0, .w = 64, .h = 64 };\n\n        std::vector<Emitter*> m_FlareList;\n\n        Uint32 m_CurrentFrame = 0;\n        int m_NextFrameTime;\n\n        Star();\n\n        void Move();\n        void Render();\n};\n```\n\n我们需要创建一个名为`star.cpp`的新文件来伴随这个类定义。在其中，我们应该定义我们的构造函数以及`Move`和`Render`函数。与我们所有的 CPP 文件一样，我们首先要做的是包含`game.hpp`文件:\n\n```cpp\n#include \"game.hpp\"\n```\n\n在那之后，我们有一些`#define`指令，用来定义我们将用来渲染我们的恒星和耀斑粒子系统的精灵文件:\n\n```cpp\n#define STAR_SPRITE_FILE \"/sprites/rotating-star.png\"\n#define FLARE_FILE (char*)\"/sprites/flare.png\"\n```\n\n构造函数相当长，但很多看起来应该很熟悉:\n\n```cpp\nStar::Star() : Collider(32.0) {\n    SDL_Surface *temp_surface = IMG_Load( STAR_SPRITE_FILE );\n\n    if( !temp_surface ) {\n        printf(\"failed to load image: %s\\n\", IMG_GetError() );\n        return;\n    }\n    else {\n        printf(\"success creating enemy ship surface\\n\");\n    }\n    m_SpriteTexture = SDL_CreateTextureFromSurface( renderer, \n    temp_surface );\n\n    if( !m_SpriteTexture ) {\n        printf(\"failed to create texture: %s\\n\", IMG_GetError() );\n        return;\n    }\n    else {\n        printf(\"success creating enemy ship texture\\n\");\n    }\n    SDL_FreeSurface( temp_surface );\n\n    m_Radius = 36;\n\n    m_Position.x = CANVAS_WIDTH / 2;\n    m_Position.y = CANVAS_HEIGHT / 2;\n\n    m_dest.x = m_Position.x - m_Radius / 2;\n    m_dest.y = m_Position.y - m_Radius / 2;\n\n    m_FlareList.push_back(new \n    Emitter(FLARE_FILE,100,160,220,1500,0.05,true,30,40, 1, \n    m_Position.x+8, m_Position.y+8, 10,0.1, 0.2,0.5, 1.0,0xffffff, \n    0xffffff, 0.1, 50,true, true, 4409, 1));\n\n    m_FlareList.push_back(new \n    Emitter(FLARE_FILE,100,220,280,1500,0.05,true,30,40, 1, m_Position.x+8, \n    m_Position.y+8,10,0.1,0.2,0.5,1.0,0xffffff, 0xffffff, 0.0, \n    50,true,true,3571, 1));\n\n    m_FlareList.push_back(new \n    Emitter(FLARE_FILE,100,280,360,1500,0.05,true,30,40, 1, \n    m_Position.x+8, m_Position.y+8, 10, 0.1, 0.2, 0.5, 1.0, 0xffffff, \n    0xffffff, 0.2, 50, true, true, 3989, 1));\n\n    m_FlareList.push_back(new \n    Emitter(FLARE_FILE,100,0,60,1500,0.05,true,30,40, 1, m_Position.x+8, \n    m_Position.y+8, 10, 0.1, 0.2, 0.5, 1.0, 0xffffff, 0xffffff, 0.1, 50, \n    true, true, 3371, 1));\n\n    m_FlareList.push_back(new \n    Emitter(FLARE_FILE,100,60,100,1500,0.05,true,30,40, 1, m_Position.x+8, \n    m_Position.y+8, 10, 0.1, 0.2, 0.5, 1.0, 0xffffff, 0xffffff, 0.3, 50, \n    true, true, 4637, 1));\n}\n```\n\n这个构造函数从继承`Collider`构造函数开始，传递给它一个半径`32`:\n\n```cpp\nStar::Star() : Collider(32.0) {\n```\n\n然后它会创建一个精灵纹理，在渲染星星时使用。这部分代码看起来应该很熟悉:\n\n```cpp\nSDL_Surface *temp_surface = IMG_Load( STAR_SPRITE_FILE );\n\nif( !temp_surface ) {\n    printf(\"failed to load image: %s\\n\", IMG_GetError() );\n    return;\n}\nelse {\n    printf(\"success creating enemy ship surface\\n\");\n}\nm_SpriteTexture = SDL_CreateTextureFromSurface( renderer, temp_surface );\nif( !m_SpriteTexture ) {\n    printf(\"failed to create texture: %s\\n\", IMG_GetError() );\n    return;\n}\nelse {\n    printf(\"success creating enemy ship texture\\n\");\n}\nSDL_FreeSurface( temp_surface );\n```\n\n设置完 sprite 纹理后，构造函数会设置一些属性，包括半径和位置:\n\n```cpp\nm_Radius = 36;\nm_Position.x = CANVAS_WIDTH / 2;\nm_Position.y = CANVAS_HEIGHT / 2;\nm_dest.x = m_Position.x - m_Radius / 2;\nm_dest.y = m_Position.y - m_Radius / 2;\n```\n\n最后，它将发射器添加到`m_FlareList`向量中。这些将是一些太阳耀斑粒子系统。我使用粒子系统配置工具得出了我们在这些发射器中创建的值。如果你愿意，你可以使用这些值，但是我觉得这些值创造了一个好看的闪光效果:\n\n```cpp\nm_FlareList.push_back(new Emitter(FLARE_FILE,100,160,220,1500,0.05,true,30,40, 1, m_Position.x+8, m_Position.y+8, 10,0.1, 0.2,0.5, 1.0,0xffffff, 0xffffff, 0.1, 50,true, true,4409, 1));\n\nm_FlareList.push_back(new Emitter(FLARE_FILE,100,220,280,1500,0.05,true,30,40, 1, m_Position.x+8, m_Position.y+8,10,0.1,0.2,0.5,1.0,0xffffff, 0xffffff, 0.0, 50,true,true,3571, 1));\n\nm_FlareList.push_back(new Emitter(FLARE_FILE,100,280,360,1500,0.05,true,30,40, 1, m_Position.x+8, m_Position.y+8, 10, 0.1, 0.2, 0.5, 1.0, 0xffffff, 0xffffff, 0.2, 50, true, true, 3989, 1));\n\nm_FlareList.push_back(new Emitter(FLARE_FILE,100,0,60,1500,0.05,true,30,40, 1, m_Position.x+8, m_Position.y+8, 10, 0.1, 0.2, 0.5, 1.0, 0xffffff, 0xffffff, 0.1, 50, true, true, 3371, 1));\n\nm_FlareList.push_back(new Emitter(FLARE_FILE,100,60,100,1500,0.05,true,30,40, 1, m_Position.x+8, m_Position.y+8, 10, 0.1, 0.2, 0.5, 1.0, 0xffffff, 0xffffff, 0.3, 50, true, true, 4637, 1));\n```\n\n恒星的`Move`功能相当简单。它循环播放明星动画序列的八帧:\n\n```cpp\nvoid Star::Move() {\n    m_NextFrameTime -= diff_time;\n    if( m_NextFrameTime <= 0 ) {\n        ++ m_CurrentFrame;\n        m_NextFrameTime = ms_per_frame;\n        if( m_CurrentFrame >= 8 ) {\n            m_CurrentFrame = 0;\n        }\n    }\n}\n```\n\n恒星的`Render`功能稍微复杂一点，因为它需要在耀斑发射器上循环，并在渲染恒星的精灵纹理之前移动它们:\n\n```cpp\nvoid Star::Render() {\n    Emitter* flare;\n    std::vector<Emitter*>::iterator it;\n\n    for( it = m_FlareList.begin(); it != m_FlareList.end(); it++ ) {\n        flare = *it;\n        flare->Move();\n    }\n    m_src.x = m_dest.w * m_CurrentFrame;\n\n    SDL_RenderCopy( renderer, m_SpriteTexture,\n                    &m_src, &m_dest );\n}\n```\n\n接下来，我们需要定义`asteroid.cpp`文件。这将保存我们的`Asteroid`类的函数定义。以下是我们在`games.hpp`文件中对`Asteroid`的类定义:\n\n```cpp\nclass Asteroid : public Collider {\n    public:\n        SDL_Texture *m_SpriteTexture;\n        SDL_Rect m_src = {.x = 0, .y = 0, .w = 16, .h = 16 };\n        SDL_Rect m_dest = {.x = 0, .y = 0, .w = 0, .h = 0 };\n\n        bool m_Alive;\n        Uint32 m_CurrentFrame = 0;\n        int m_NextFrameTime;\n        float m_Rotation;\n        Vector2D m_Direction;\n        Vector2D m_Velocity;\n\n        Emitter* m_Explode;\n        Emitter* m_Chunks;\n\n        Asteroid( float x, float y,\n                  float velocity,\n                  float rotation );\n\n        void Move();\n        void Render();\n        void Explode();\n};\n```\n\n在我们的`asteroid.cpp`文件中，我们需要定义`Asteroid`构造函数、`Move`函数、`Render`函数和`Explode`函数。在`asteroid.cpp`文件的顶部，我们需要`#include``game.hpp`文件，并定义我们的小行星精灵文件在虚拟文件系统中的位置。以下是前几行代码的样子:\n\n```cpp\n#include \"game.hpp\"\n#define ASTEROID_SPRITE_FILE (char*)\"/sprites/asteroid.png\"\n```\n\n我们将定义的第一个函数是我们的构造函数。以下是构造函数的全部内容:\n\n```cpp\nAsteroid::Asteroid( float x, float y,\n                    float velocity,\n                    float rotation ): Collider(8.0) {\n    SDL_Surface *temp_surface = IMG_Load( ADSTEROID_SPRITE_FILE );\n\n    if( !temp_surface ) {\n        printf(\"failed to load image: %s\\n\", IMG_GetError() );\n        return;\n    }\n    else {\n        printf(\"success creating asteroid surface\\n\");\n    }\n\n    m_SpriteTexture = SDL_CreateTextureFromSurface( renderer, temp_surface );\n\n    if( !m_SpriteTexture ) {\n        printf(\"failed to create texture: %s\\n\", IMG_GetError() );\n        return;\n    }\n    else {\n        printf(\"success creating asteroid texture\\n\");\n    }\n\n    SDL_FreeSurface( temp_surface );\n\n    m_Explode = new Emitter((char*)\"/sprites/Explode.png\",\n         100, 0, 360,     // int max_particles, float min_angle, float \n         max_angle,\n         1000, 0.3, false, // Uint32 particle_lifetime, float acceleration, \n         bool alpha_fade,\n         20.0, 40.0,     // float min_starting_velocity, float \n         max_starting_velocity,\n         10, 0, 0, 5,     // Uint32 emission_rate, int x_pos, int y_pos, \n         float radius,\n         1.0, 2.0,         // float min_start_scale, float max_start_scale,\n         1.0, 2.0,         // float min_end_scale, float max_end_scale,\n         0xffffff, 0xffffff,\n         0.01, 10,         // float burst_time_pct, Uint32 burst_particles,\n         false, false,     // bool loop, bool align_rotation,\n         800, 8 );         // Uint32 emit_time_ms, Uint32 animation_frames\n    m_Explode->m_parent_rotation_ptr = &m_Rotation;\n    m_Explode->m_parent_x_ptr = &(m_Position.x);\n    m_Explode->m_parent_y_ptr = &(m_Position.y);\n    m_Explode->m_active = false;\n\n    m_Chunks = new Emitter((char*)\"/sprites/small-asteroid.png\",\n         40, 0, 360, // int max_particles, float min_angle, float \n         max_angle,\n         1000, 0.05, false, // Uint32 particle_lifetime, float \n         acceleration, \n         bool alpha_fade,\n         80.0, 150.0, // float min_starting_velocity, float \n         max_starting_velocity,\n         5, 0, 0, 10, // Uint32 emission_rate, int x_pos, int y_pos, \n         float radius,\n         2.0, 2.0, // float min_start_scale, float max_start_scale,\n         0.25, 0.5, // float min_end_scale, float max_end_scale,\n         0xffffff, 0xffffff,\n         0.1, 10, // float burst_time_pct, Uint32 burst_particles,\n         false, true, // bool loop, bool align_rotation,\n         1000, 8 ); // Uint32 emit_time_ms, Uint32 animation_frames\n\n    m_Chunks->m_parent_rotation_ptr = &m_Rotation;\n    m_Chunks->m_parent_x_ptr = &m_Position.x;\n    m_Chunks->m_parent_y_ptr = &m_Position.    \n    m_Chunks->m_active = false;\n\n    m_Position.x = x;\n    m_Position.y = y;\n\n    Vector2D direction;\n    direction.x = 1;\n    direction.Rotate( rotation );\n\n    m_Direction = direction;\n    m_Velocity = m_Direction * velocity;\n\n    m_dest.h = m_src.h = m_dest.w = m_src.w = 16;\n\n    m_Rotation = rotation;\n    m_Alive = true;\n    m_CurrentFrame = 0;\n    m_NextFrameTime = ms_per_frame;\n}\n```\n\n构造函数的定义调用`Collider`类中的父构造函数，传递的半径为`8.0`的`Collider`:\n\n```cpp\nAsteroid::Asteroid( float x, float y,\n                    float velocity,\n                    float rotation ): Collider(8.0) {\n```\n\n之后，构造函数使用 SDL 加载并初始化 sprite 纹理，这个过程我们现在应该都很熟悉了:\n\n```cpp\nSDL_Surface *temp_surface = IMG_Load( ADSTEROID_SPRITE_FILE );\n\nif( !temp_surface ) {\n    printf(\"failed to load image: %s\\n\", IMG_GetError() );\n    return;\n}\nelse {\n    printf(\"success creating asteroid surface\\n\");\n}\n\nm_SpriteTexture = SDL_CreateTextureFromSurface( renderer, temp_surface );\n\nif( !m_SpriteTexture ) {\n    printf(\"failed to create texture: %s\\n\", IMG_GetError() );\n    return;\n}\nelse {\n    printf(\"success creating asteroid texture\\n\");\n}\n\nSDL_FreeSurface( temp_surface );\n```\n\n然后我们定义我们的爆炸发射器。如果我们的小行星被摧毁，这个发射器将被激活:\n\n```cpp\nm_Explode = new Emitter((char*)\"/sprites/Explode.png\",\n     100, 0, 360, // int max_particles, float min_angle, float max_angle,\n     1000, 0.3, false, // Uint32 particle_lifetime, float acceleration, \n     bool alpha_fade,\n     20.0, 40.0, // float min_starting_velocity, float \n     max_starting_velocity,\n     10, 0, 0, 5, // Uint32 emission_rate, int x_pos, int y_pos, \n     float radius,\n     1.0, 2.0, // float min_start_scale, float max_start_scale,\n     1.0, 2.0, // float min_end_scale, float max_end_scale,\n     0xffffff, 0xffffff,\n     0.01, 10, // float burst_time_pct, Uint32 burst_particles,\n     false, false, // bool loop, bool align_rotation,\n     800, 8 ); // Uint32 emit_time_ms, Uint32 animation_frames\n\nm_Explode->m_parent_rotation_ptr = &m_Rotation;\nm_Explode->m_parent_x_ptr = &(m_Position.x);\nm_Explode->m_parent_y_ptr = &(m_Position.y);\nm_Explode->m_active = false;\n```\n\n之后，我们创造了第二个发射器，当我们的小行星被摧毁时，它会射出小块的岩石。这是为了配合`m_Explosion`发射器，它会在小行星爆炸的同时运行:\n\n```cpp\nm_Chunks = new Emitter((char*)\"/sprites/small-asteroid.png\",\n     40, 0, 360, // int max_particles, float min_angle, float max_angle,\n     1000, 0.05, false, // Uint32 particle_lifetime, float acceleration, \n     bool alpha_fade,\n     80.0, 150.0, // float min_starting_velocity, float \n     max_starting_velocity,\n     5, 0, 0, 10, // Uint32 emission_rate, int x_pos, int y_pos, \n     float radius,\n     2.0, 2.0, // float min_start_scale, float max_start_scale,\n     0.25, 0.5, // float min_end_scale, float max_end_scale,\n     0xffffff, 0xffffff,\n     0.1, 10, // float burst_time_pct, Uint32 burst_particles,\n     false, true, // bool loop, bool align_rotation,\n     1000, 8 ); // Uint32 emit_time_ms, Uint32 animation_frames\n\nm_Chunks->m_parent_rotation_ptr = &m_Rotation;\nm_Chunks->m_parent_x_ptr = &m_Position.x;\nm_Chunks->m_parent_y_ptr = &m_Position.y;\nm_Chunks->m_active = false;\n```\n\n最后几行设定了小行星属性的起始值:\n\n```cpp\nm_Position.x = x;\nm_Position.y = y;\n\nVector2D direction;\ndirection.x = 1;\ndirection.Rotate( rotation );\n\nm_Direction = direction;\nm_Velocity = m_Direction * velocity;\nm_dest.h = m_src.h = m_dest.w = m_src.w = 16;\n\nm_Rotation = rotation;\nm_Alive = true;\nm_CurrentFrame = 0;\nm_NextFrameTime = ms_per_frame;\n```\n\n我们将定义的下一个函数是`Move`函数。这是它的样子:\n\n```cpp\nvoid Asteroid::Move() {\nm_NextFrameTime -= diff_time;\nif( m_NextFrameTime <= 0 ) {\n    m_NextFrameTime = ms_per_frame;\n    m_CurrentFrame++ ;\n    if( m_CurrentFrame >= 8 ) {\n        m_CurrentFrame = 0;\n    }\n}\nm_Position += m_Velocity * delta_time;\nWrapPosition();\n}\n```\n\n处理`m_NextFrameTime`和`m_CurrentFrame`的第一批代码只是根据过去的时间在子画面帧之间交替:\n\n```cpp\nm_NextFrameTime -= diff_time;\nif( m_NextFrameTime <= 0 ) {\n    m_NextFrameTime = ms_per_frame;\n    m_CurrentFrame++ ;\n\n    if( m_CurrentFrame >= 8 ) {\n        m_CurrentFrame = 0;\n    }\n}\n```\n\n之后，我们根据时间增量和流速更新位置:\n\n```cpp\nm_Position += m_Velocity * delta_time;\n```\n\n最后调用`WrapPosition`函数。如果我们的小行星离开了屏幕，这个功能会把它移回屏幕的右侧，如果它离开了屏幕的底部，这个功能会把它移到顶部。每当一颗小行星以给定的方向离开屏幕时，它的位置就会被绕到游戏区域的另一边。\n\n在`Move`函数之后，我们定义`Asteroid Render`函数。完整的功能如下所示:\n\n```cpp\nvoid Asteroid::Render() {\n    m_Explode->Move();\n    m_Chunks->Move();\n    if( m_Alive == false ) {\n        return;\n    }\n    m_src.x = m_dest.w * m_CurrentFrame;\n    m_dest.x = m_Position.x + m_Radius / 2;\n    m_dest.y = m_Position.y + m_Radius / 2;\n    SDL_RenderCopyEx( renderer, m_SpriteTexture,\n                        &m_src, &m_dest,\n                        RAD_TO_DEG(m_Rotation), NULL, SDL_FLIP_NONE );\n}\n```\n\n前两行移动爆炸发射器和块发射器。如果小行星没有被摧毁，这些功能将不起任何作用。如果小行星已经被摧毁，这些功能将运行粒子发射器。这些发射器不循环，因此当它们的发射时间到了，它们将停止:\n\n```cpp\nm_Explode->Move();\nm_Chunks->Move();\n```\n\n之后，我们检查小行星是否有生命，如果没有，我们就退出这个功能。我们在移动发射器后这样做的原因是，在小行星被摧毁后，我们必须继续运行发射器:\n\n```cpp\nif( m_Alive == false ) {\n    return;\n}\n```\n\n我们在这个函数中做的最后一件事是渲染我们的小行星精灵纹理，这个过程现在看起来应该很熟悉了:\n\n```cpp\nm_src.x = m_dest.w * m_CurrentFrame;\nm_dest.x = m_Position.x + m_Radius / 2;\nm_dest.y = m_Position.y + m_Radius / 2;\nSDL_RenderCopyEx( renderer, m_SpriteTexture,\n                  &m_src, &m_dest,\n                  RAD_TO_DEG(m_Rotation), NULL, SDL_FLIP_NONE );\n```\n\n我们的`asteroid.cpp`文件中的最后一个函数是`Explode`函数。当小行星被摧毁时，该功能将运行。该功能将运行我们的两个发射器，这两个发射器旨在创建爆炸效果。它还将把小行星的活旗设定为`false`。下面是代码:\n\n```cpp\nvoid Asteroid::Explode() {\n    m_Explode->Run();\n    m_Chunks->Run();\n    m_Alive = false;\n}\n```\n\n现在我们已经定义了我们的游戏障碍，让我们看看如何为我们的宇宙飞船创造一些护盾。\n\n# 添加力场\n\n目前，在我们的游戏中，我们的宇宙飞船被一次碰撞摧毁。这最终创造了一个游戏，很快就结束了。当碰撞即将发生时，有一个力场来防止船的毁灭会很好。这也将使我们的人工智能在它的技巧包中有所作为。当防护罩打开时，使用它的飞船周围会有一点力场动画。盾牌的使用是有时间限制的。这将阻止玩家或人工智能在整个游戏中保持盾。当护盾激活时，护盾的颜色将从绿色变为红色。颜色越接近红色，护盾就越接近耗尽能量。每次盾牌被击中，玩家或人工智能的盾牌都会有额外的时间被拿掉。我们已经在`game.hpp`文件中创建了类定义。这是它的样子:\n\n```cpp\nclass Shield : public Collider {\n    public:\n        bool m_Active;\n        int m_ttl;\n        int m_NextFrame;\n        Uint32 m_CurrentFrame;\n        Ship* m_Ship;\n        SDL_Texture *m_SpriteTexture;\n\n        SDL_Rect m_src = {.x = 0, .y = 0, .w = 32, .h = 32 };\n        SDL_Rect m_dest = {.x = 0, .y = 0, .w = 32, .h = 32 };\n\n        Shield( Ship* ship, const char* sprite_file );\n\n        void Move();\n        void Render();\n        bool Activate();\n        void Deactivate();\n};\n```\n\n为了配合这个类定义，我们需要一个`shield.cpp`文件，在这里我们可以定义这个类使用的所有函数。我们将在`shield.cpp`文件中定义的第一个函数是`Shield`构造函数:\n\n```cpp\nShield::Shield( Ship* ship, const char* sprite_string ) : Collider(12.0) {\n    m_Active = false;\n    m_ttl = 25500;\n    m_Ship = ship;\n    m_CurrentFrame = 0;\n    m_NextFrame = ms_per_frame;\n    SDL_Surface *temp_surface = IMG_Load( sprite_string );\n\n    if( !temp_surface ) {\n        printf(\"failed to load image: %s\\n\", IMG_GetError() );\n        return;\n    }\n\n    m_SpriteTexture = SDL_CreateTextureFromSurface( renderer, \n   temp_surface );\n\n    if( !m_SpriteTexture ) {\n        printf(\"failed to create texture: %s\\n\", IMG_GetError() );\n        return;\n    }\n    SDL_FreeSurface( temp_surface );\n}\n```\n\n`Shield`构造函数将调用`Collider`构造函数，半径为`12.0`。这是一个比船半径更大的半径。如果护盾有效，我们希望这个`Collider`而不是飞船被击中。此构造函数中的第一个代码块设置此类属性的起始值:\n\n```cpp\nm_Active = false;\nm_ttl = 25500;\nm_Ship = ship;\nm_CurrentFrame = 0;\nm_NextFrame = ms_per_frame;\n```\n\n请注意，我们将`m_ttl`设置为`25500`。这是你可以使用盾牌的时间，以毫秒为单位。总计 25.5 秒。我希望它是 255 的倍数，这样绿色将根据剩余时间从 255 过渡到 0。\n\n相反，红色将从 0 过渡到 255，也是基于剩余的时间。之后，我们以标准方式创建盾牌的精灵纹理:\n\n```cpp\nSDL_Surface *temp_surface = IMG_Load( sprite_string );\n\nif( !temp_surface ) {\n    printf(\"failed to load image: %s\\n\", IMG_GetError() );\n    return;\n}\n\nm_SpriteTexture = SDL_CreateTextureFromSurface( renderer, temp_surface );\n\nif( !m_SpriteTexture ) {\n    printf(\"failed to create texture: %s\\n\", IMG_GetError() );\nreturn;\n}\n\nSDL_FreeSurface( temp_surface );\n```\n\n构造函数之后，我们需要定义我们的`Move`函数:\n\n```cpp\nvoid Shield::Move() {\n    if( m_Active ) {\n        m_NextFrame -= diff_time;\n        m_ttl -= diff_time;\n\n        if( m_NextFrame <= 0 ) {\n            m_NextFrame = ms_per_frame;\n            m_CurrentFrame++ ;\n\n            if( m_CurrentFrame >= 6 ) {\n                m_CurrentFrame = 0;\n            }\n        }\n        if( m_ttl <= 0 ) {\n            m_Active = false;\n        }\n    }\n}\n```\n\n如果防护罩没有激活，这个功能什么也做不了。如果激活，`m_ttl`参数将根据自最后一帧以来经过的毫秒数递减。然后，如果经过了适当的毫秒数，我们就增加当前帧。如果护盾的剩余时间低于 0，护盾将被关闭。\n\n在我们定义了我们的`Move`函数之后，我们将定义我们的`Render`函数:\n\n```cpp\nvoid Shield::Render() {\n    if( m_Active ) {\n        int color_green = m_ttl / 100 + 1;\n        int color_red = 255 - color_green;\n        m_src.x = m_CurrentFrame * m_dest.w;\n        m_dest.x = m_Ship->m_Position.x;\n        m_dest.y = m_Ship->m_Position.y;\n\n        SDL_SetTextureColorMod(m_SpriteTexture,\n                             color_red,\n                             color_green,\n                             0 );\n\n        SDL_RenderCopyEx( renderer, m_SpriteTexture,\n                             &m_src, &m_dest,\n                             RAD_TO_DEG(m_Ship->m_Rotation),\n                             NULL, SDL_FLIP_NONE );\n    }\n}\n```\n\n与`Move`功能一样，`Render`功能在活动标志为假时不做任何事情。我们使用以下公式根据剩余时间计算颜色:\n\n```cpp\nint color_green = m_ttl / 100 + 1;\nint color_red = 255 - color_green;\n```\n\n这将使我们的护盾的颜色从绿色平稳过渡到红色。我们使用对`SDL_SetTextureColorMod`的调用来设置精灵纹理的颜色:\n\n```cpp\nSDL_SetTextureColorMod(m_SpriteTexture,\n                     color_red,\n                     color_green,\n                     0 );\n```\n\n`Shield::Render`功能中的其他一切都非常标准，现在看起来应该很熟悉了。\n\n# 更多碰撞检测\n\n让我们看看我们需要对我们的`Collider`类进行的修改。正如我们之前讨论的，我们的人工智能将实现转向行为。这些转向行为将在我们的`Collider`类中需要一些新的属性和功能。以下是新的`Collider`班级的样子:\n\n```cpp\nclass Collider {\n    public:\n        float* m_ParentRotation;\n        float* m_ParentX;\n        float* m_ParentY;\n        Vector2D m_TempPoint;\n\n        bool CCHitTest( Collider* collider );\n\n        Vector2D m_Position;\n        float m_Radius;\n        float m_SteeringRadius;\n        float m_SteeringRadiusSQ;\n\n        void SetParentInformation( float* rotation, float* x, float* y );\n        Collider(float radius);\n        bool HitTest( Collider *collider );\n        bool SteeringLineTest( Vector2D &p1, Vector2D &p2 );\n        bool SteeringRectTest( Vector2D &start_point, Vector2D &end_point \n        );\n        void WrapPosition();\n };\n```\n\n我们有三个新功能，其中两个用于转向。其中一个功能`WrapPosition()`将用于包装在一个方向上离开屏幕的物体，以便它们重新出现在游戏屏幕的另一侧。让我们打开`collider.cpp`看看。我们首先需要改变的是构造函数。下面是新版本的构造函数的外观:\n\n```cpp\nCollider::Collider(float radius) {\n    m_ParentRotation = NULL;\n    m_ParentX = NULL;\n    m_ParentY = NULL;\n\n    m_Radius = radius;\n    m_SteeringRadius = m_Radius * 1.5;\n    m_SteeringRadiusSQ = m_SteeringRadius * m_SteeringRadius;\n}\n```\n\n最后两行是唯一的修改。您会注意到我们将`m_SteeringRadius`属性设置为`1.5`乘以`m_Radius`值。这个额外的缓冲空间是为了防止我们的敌舰离小行星太近，尤其是当它们在移动的时候。这一因素有效地使转向行为更加警惕与小行星的碰撞。`1.5`的倍数选择有些随意，因为我测试的时候效果不错。如果你想让你的人工智能不那么关心小行星碰撞，更有可能通过将自己置于危险中来追求玩家，你可以降低这个值，也许是类似`1.1`的值。你也可以增加这个值，让人工智能更加警惕小行星。将该值设置得太高会导致 AI 过于胆小。将它设置得太低会让它在几乎任何情况下都追求玩家，模仿海军上将大卫·法拉格特在移动海湾战役中臭名昭著的话，“*该死的鱼雷——全速前进！*”\n\n接下来，我们需要将新功能`SteeringLineText`添加到`collider.cpp`中。这个新的功能将在连接我们的敌舰和我们的玩家的一条线之间进行圆-线碰撞检测，并检测我们的船可能沿着这条路径撞击的所有小行星和射弹。这是一个视线测试，以确定从我们的位置是否有一条通往玩家的清晰路径。与圆-圆或矩形-矩形碰撞检测相比，圆-线碰撞检测有些复杂。我大量借用了我在[embed.com](https://www.embed.com)上创建的一个解决方案，地址如下:[https://www . embed . com/typescript-games/multi-type-防撞检测. html](https://www.embed.com/typescript-games/multiple-type-collision-detection.html) 。\n\n# 圆-线碰撞检测\n\n确定圆和线是否碰撞的第一步是最简单的:检查线的任一端点是否落在圆的半径内。这是通过使用毕达哥拉斯定理的简单距离检查来完成的。如果其中一个点与我们的圆心之间的距离小于半径，则直线在圆内。这是一个落在圆半径内的点的示意图:\n\n![](img/c99d9002-ff23-456d-b5ab-d208f912ad0e.png)\n\nThe line's p2 point falls inside the circle radius\n\n如果任一点落在圆的半径内，我们就知道直线和圆碰撞了。如果两个点都不在圆的半径内，我们就完了。那么我们需要做的就是找到直线上离圆心最近的点。让我离题一会儿，讲得更专业一点。从技术上讲，所有的线都是无限的。当我们有两个点，并在这两个点之间画一条“线”时，它就是一条线段。为了找到直线和我们的圆之间最近的点，我们需要谈论一些叫做**向量投影**的东西。\n\n# 向量投影\n\n矢量投影有点复杂。如果你将一个给定的向量 b 投影到向量 *a* 上，你会得到向量 *a* 的标量倍数(我们将这个标量倍数称为 *c* )，在这里你可以添加一个垂直于向量 *ca* 的向量来得到向量 *b* 。\n\n下图是将向量 *b* 投影到向量 *a* 的示例:\n\n![](img/adfb214e-e771-4370-9e06-b044e38c8569.png)\n\nAn example of projecting vector b onto vector a\n\n另一种观点是，向量 b 在向量 a 上的投影给了我们距离向量 b 的端点最近的点，该点位于线段上，由向量 a 的任何标量倍数定义。你可能想知道这与检测圆和线之间的碰撞有什么关系。好吧，如果我们假设向量 b 代表我们圆的中心点的位置，我们就能算出我们线上离那个圆的中心点最近的点是什么。然后我们测试用投影找到的点和圆心之间的碰撞。请参见下图中如何使用矢量投影来确定直线上离圆最近的点:\n\n![](img/8f4d3c2b-0da5-4bb8-8dcc-486984f5aca3.png)\n\nNotice that projecting the vector onto our line gives us the closest point on the line to the circle\n\n还有一个潜在的问题你必须考虑。向量 a 的投影可能会给你一个大于 1 的 c 值(标量倍数)。如果是这种情况，那可能是我们的直线与我们终点以外的圆发生了碰撞。正因为如此，我们还需要做一些范围检查，看看我们是否超过了我们的底线:\n\n![](img/ee892c81-12aa-41a8-b496-7c1cc2cf8132.png)\n\nProjecting the circle's vector on to our line gives us the closest point that is passed the range of our line segment\n\n现在我已经解释了什么是向量投影，让我们看一下代码:\n\n```cpp\nbool Collider::SteeringLineTest( Vector2D &start, Vector2D &end ) {\n    if( m_Active == false ) {\n        return false;\n    }\n    Vector2D dist = start;\n    dist -= m_Position;\n\n    if( m_SteeringRadiusSQ > dist.MagSQ() ) {\n        return true;\n    }\n    dist = end;\n    dist -= m_Position;\n\n    if( m_SteeringRadiusSQ > dist.MagSQ() ) {\n        return true;\n    }\n    dist = end;\n    dist -= start;\n\n    Vector2D circle_vec = m_Position;\n    circle_vec -= start;\n\n    Vector2D near_point = circle_vec.Project( dist );\n    near_point += start;\n\n    Vector2D temp_vector = near_point;\n    circle_vec += start;\n    temp_vector -= circle_vec;\n\n    Range x_range;\n    x_range.min = start.x;\n    x_range.max = end.x;\n    x_range.Sort();\n    Range y_range;\n    y_range.min = start.y;\n    y_range.max = end.y;\n    y_range.Sort();\n\n    if ((x_range.min <= near_point.x && near_point.x <= x_range.max &&\n         y_range.min <= near_point.y && near_point.y <= y_range.max) == \n         false) {\n        return false;\n    }\n    if( temp_vector.MagSQ() < m_SteeringRadiusSQ ) {\n        return true;\n    }\n    return false;\n}\n```\n\n如前所述，我们首先要做的是测试起点和终点到这个`Collider`物体位置的距离。如果距离的平方小于任一点的转向半径的平方，我们知道直线与我们的转向半径相冲突:\n\n```cpp\nif( m_Active == false ) {\n    return false;\n}\n\nVector2D dist = start;\ndist -= m_Position;\n\nif( m_SteeringRadiusSQ > dist.MagSQ() ) {\n    return true;\n}\n\ndist = end;\ndist -= m_Position;\nif( m_SteeringRadiusSQ > dist.MagSQ() ) {\n    return true;\n}\n```\n\n如果两个点都不在圆内，我们需要对照投影进行测试。我们需要将线段转化为穿过原点的矢量。为此，我们需要从终点减去起点，并且还需要将圆的位置调整相同的量:\n\n```cpp\ndist = end;\ndist -= start;\n\nVector2D circle_vec = m_Position;\ncircle_vec -= start;\n\nVector2D near_point = circle_vec.Project( dist );\nnear_point += start;\n\nVector2D temp_vector = near_point;\ncircle_vec += start;\ntemp_vector -= circle_vec;\n```\n\n我们需要确保离对撞机最近的点仍然在线段上。这可以通过对开始和结束 *x* 和 *y* 值的简单范围测试来完成。如果 *x* 和 *y* 坐标都在我们的范围内，我们知道该点一定位于线段上的某个地方。如果没有，我们知道线不会与圆碰撞:\n\n```cpp\nRange x_range;\nx_range.min = start.x;\nx_range.max = end.x;\nx_range.Sort();\n\nRange y_range;\ny_range.min = start.y;\ny_range.max = end.y;\ny_range.Sort();\n\nif ((x_range.min <= near_point.x && near_point.x <= x_range.max &&\n     y_range.min <= near_point.y && near_point.y <= y_range.max) == false) {\n    return false;\n}\n```\n\n如果此时我们没有返回`false`值，我们就知道碰撞器最近的点在我们的线段上。现在我们可以测试从那个点到我们的对撞机的距离，看看它是否足够近，可以与我们的转向半径发生碰撞；如果是，我们返回`true`，如果不是，我们返回`false`:\n\n```cpp\nif( m_SteeringRadiusSQ > dist.MagSQ() ) {\n    return true;\n}\nreturn false;\n```\n\n# Vector2D 类\n\n我之前提到过，我们需要废弃我们的旧`Point`类，取而代之的是具有更多功能的东西。新的`Vector2D`类将为我们之前使用的`Point`类增加几个新功能。让我们再看看我们的`game.hpp`文件中的函数定义:\n\n```cpp\nclass Vector2D {\n    public:\n        float x;\n        float y;\n\n        Vector2D();\n        Vector2D( float X, float Y );\n\n        void Rotate( float radians );\n        void Normalize();\n        float MagSQ();\n        float Magnitude();\n\n        Vector2D Project( Vector2D &onto );\n        float Dot(Vector2D &vec);\n        float FindAngle();\n\n        Vector2D operator=(const Vector2D &vec);\n        Vector2D operator*(const float &scalar);\n        void operator+=(const Vector2D &vec);\n        void operator-=(const Vector2D &vec);\n        void operator*=(const float &scalar);\n        void operator/=(const float &scalar);\n};\n```\n\n与点不同，向量有大小。因为计算起来比较快，我们还会加一个平方量级，`MagSQ`，函数。矢量可以归一化，这意味着它们可以被修改为具有 1 的幅度。我们之前讨论过矢量投影，我们已经创建了一个`Project`函数来允许我们这样做。求两个向量的点积在游戏中是一个非常有用的操作。两个归一化向量的点积是一个标量值，范围在 1 和-1 之间，具体取决于这两个向量之间的角度。如果向量指向同一个方向，则值为 1；如果向量指向相反的方向，则值为-1；如果两个向量相互垂直，则值为 0。\n\nThe dot product of two normalized vectors is the same as the cosine of the angle between those two normalized vectors. Getting the dot product of any two vectors, *a* and *b*, gives you the (magnitude of *a*) * (magnitude of *b*) * cosine (angle between *a* and *b*). The reason we normalize these vectors first is to set the magnitude of *a* and the magnitude of *b* to 1, which causes our normalized dot product to return the cosine of the angle between vectors *a* and *b*.\n\n我们还会增加一个`FindAngle`函数，它会告诉我们这个函数的方向角。我们将重载许多操作符，以允许更容易的向量操作。\n\n让我们整体来看一下`vector.cpp`:\n\n```cpp\n#include \"game.hpp\"\n\nVector2D::Vector2D( float X, float Y ) {\n    x = X;\n    y = Y;\n}\nVector2D::Vector2D() {\n    y = x = 0.0;\n}\nVector2D Vector2D::operator=(const Vector2D& p) {\n    x = p.x;\n    y = p.y;\n    return *this;\n}\nvoid Vector2D::operator+=(const Vector2D& p) {\n    x += p.x;\n    y += p.y;\n}\nvoid Vector2D::operator-=(const Vector2D& p) {\n    x -= p.x;\n    y -= p.y;\n}\nvoid Vector2D::operator*=(const float& scalar) {\n    x *= scalar;\n    y *= scalar;\n}\nvoid Vector2D::operator/=(const float& scalar) {\n    x /= scalar;\n    y /= scalar;\n}\nVector2D Vector2D::operator*(const float& scalar) {\n    Vector2D vec = *this;\n    vec *= scalar;\n    return vec;\n}\nvoid Vector2D::Rotate( float radians ) {\n    float sine = sin(radians);\n    float cosine = cos(radians);\n    float rx = x * cosine - y * sine;\n    float ry = x * sine + y * cosine;\n    x = rx;\n    y = ry;\n}\nvoid Vector2D::Normalize() {\n    float mag = Magnitude();\n    x /= mag;\n    y /= mag;\n}\nVector2D Vector2D::Project(Vector2D &onto) {\n    Vector2D proj = *this;\n    float proj_dot_onto = proj.Dot(onto);\n    proj *= proj_dot_onto;\n    return proj;\n}\nfloat Vector2D::Dot(Vector2D &vec) {\n    Vector2D this_norm;\n    this_norm = *this;\n    this_norm.Normalize();\n    Vector2D vec_norm;\n    vec_norm = vec;\n    vec_norm.Normalize();\n\n    return this_norm.x * vec_norm.x + this_norm.y * vec_norm.y;\n}\nfloat Vector2D::FindAngle() {\n    if( x == 0.0 && y == 0.0 ) {\n        return 0.0;\n    }\n    Vector2D this_norm;\n    this_norm = *this;\n    this_norm.Normalize();\n    return atan2( this_norm.y, this_norm.x ) + PI / 2;\n}\nfloat Vector2D::MagSQ() {\n    return x * x + y * y;\n}\nfloat Vector2D::Magnitude() {\n    return sqrt( MagSQ() );\n}\n```\n\n前两个函数是构造函数，它们本质上与`Point`类中的构造函数相同:\n\n```cpp\nVector2D::Vector2D( float X, float Y ) {\n    x = X;\n    y = Y;\n}\nVector2D::Vector2D() {\n    y = x = 0.0;\n}\n```\n\n在那之后，我们有我们的重载操作符。这让我们可以轻松地进行矢量的加减乘除:\n\n```cpp\nVector2D Vector2D::operator=(const Vector2D& p) {\n    x = p.x;\n    y = p.y;\n    return *this;\n}\nvoid Vector2D::operator+=(const Vector2D& p) {\n    x += p.x;\n    y += p.y;\n}\nvoid Vector2D::operator-=(const Vector2D& p) {\n    x -= p.x;\n    y -= p.y;\n}\nvoid Vector2D::operator*=(const float& scalar) {\n    x *= scalar;\n    y *= scalar;\n}\nvoid Vector2D::operator/=(const float& scalar) {\n    x /= scalar;\n    y /= scalar;\n}\nVector2D Vector2D::operator*(const float& scalar) {\n    Vector2D vec = *this;\n    vec *= scalar;\n    return vec;\n}\n```\n\n`Rotate`函数是`Point`类中为数不多的函数之一。它与`Point`级版本没有变化:\n\n```cpp\nvoid Vector2D::Rotate( float radians ) {\n    float sine = sin(radians);\n    float cosine = cos(radians);\n    float rx = x * cosine - y * sine;\n    float ry = x * sine + y * cosine;\n    x = rx;\n    y = ry;\n}\n```\n\n`Normalize`功能将向量的大小更改为值 1。它通过确定矢量的大小并将 *x* 和 *y* 值除以该大小来实现:\n\n```cpp\nvoid Vector2D::Normalize() {\n    float mag = Magnitude();\n    x /= mag;\n    y /= mag;\n}\n```\n\n`Project`函数使用归一化角度的点积，并将标量值乘以向量，以确定新的投影向量:\n\n```cpp\nVector2D Vector2D::Project(Vector2D &onto) {\n    Vector2D proj = *this;\n    float proj_dot_onto = proj.Dot(onto);\n    proj *= proj_dot_onto;\n    return proj;\n}\n```\n\n我们的`Dot`积函数实际上是归一化向量的点积。这给了我们两个向量之间的角度信息。我们首先进行归一化，因为我们只在矢量投影中使用这个点积:\n\n```cpp\nfloat Vector2D::Dot(Vector2D &vec) {\n    Vector2D this_norm;\n    this_norm = *this;\n    this_norm.Normalize();\n\n    Vector2D vec_norm;\n    vec_norm = vec;\n    vec_norm.Normalize();\n\n    return this_norm.x * vec_norm.x + this_norm.y * vec_norm.y;\n}\n```\n\n`FindAngle`函数使用反正切来求两个矢量之间的角度，单位为弧度:\n\n```cpp\nfloat Vector2D::FindAngle() {\n    if( x == 0.0 && y == 0.0 ) {\n        return 0.0;\n    }\n    Vector2D this_norm;\n    this_norm = *this;\n    this_norm.Normalize();\n    return atan2( this_norm.y, this_norm.x ) + PI / 2;\n}\n```\n\n最后两个函数得到向量的大小和大小的平方:\n\n```cpp\nfloat Vector2D::MagSQ() {\n    return x * x + y * y;\n}\n\nfloat Vector2D::Magnitude() {\n    return sqrt( MagSQ() );\n}\n```\n\n# 写一个有限状态机\n\n现在我们在`Collider`和`Vector2D`类中有了我们需要的工具，我们可以构建我们的 FSM 了。`FiniteStateMachine`班将管理我们的人工智能。我们的密克罗尼西亚联邦将有四种状态:`SEEK`、`FLEE`、`ATTACK`和`WANDER`。它将实施转向行为，并在试图通过小行星等障碍物时增加一个躲避力。人工智能还需要检查敌舰是否应该升起或降下护盾。让我们再来看一下`FiniteStateMachine`类的定义，因为我们已经在`game.hpp`文件中定义了它:\n\n```cpp\nclass FiniteStateMachine {\n    public:\n        const float c_AttackDistSq = 40000.0;\n        const float c_FleeDistSq = 2500.0;\n        const int c_MinRandomTurnMS = 100;\n        const int c_RandTurnMS = 3000;\n        const int c_ShieldDist = 20;\n        const int c_AvoidDist = 80;\n        const int c_StarAvoidDistSQ = 20000;\n        const int c_ObstacleAvoidForce = 150;\n        const int c_StarAvoidForce = 120;\n\n        FSM_STATE m_CurrentState;\n        EnemyShip* m_Ship;\n\n        bool m_HasLOS;\n        bool m_LastTurnLeft;\n        int m_SameTurnPct;\n        int m_NextTurnMS;\n        int m_CheckCycle;\n        float m_DesiredRotation;\n        float m_PlayerDistSQ;\n\n        FiniteStateMachine(EnemyShip* ship);\n\n        void SeekState(Vector2D &seek_point);\n        void FleeState(Vector2D &flee_point);\n        void WanderState();\n        void AttackState();\n\n        void AvoidForce();\n        bool ShieldCheck();\n        bool LOSCheck();\n\n        Vector2D PredictPosition();\n\n        float GetPlayerDistSq();\n        void Move();\n};\n```\n\n现在让我们花一点时间浏览一下我们将在`finite_state_machine.cpp`文件中定义的所有函数。这个文件开头的构造函数没有做任何复杂的事情。它进行一些基本的初始化:\n\n```cpp\nFiniteStateMachine::FiniteStateMachine(EnemyShip* ship) {\n    m_Ship = ship;\n    m_CurrentState = APPROACH;\n    m_HasLOS = false;\n    m_DesiredRotation = 0.0;\n    m_CheckCycle = 0;\n    m_PlayerDistSQ = 0;\n}\n```\n\n在构造函数之后，我们定义了四个状态函数:`SeekState`、`FleeState`、`WanderState`和`AttackState`。这四种状态中的第一种会导致我们的敌舰在我们的游戏区域中寻找一个特定的点。该点将在我们的`Move`函数或`AttackState`函数中计算。代码如下所示:\n\n```cpp\nvoid FiniteStateMachine::SeekState(Vector2D &seek_point) {\n    Vector2D direction = seek_point;\n    direction -= m_Ship->m_Position;\n    m_DesiredRotation = direction.FindAngle();\n    float rotate_direction = m_Ship->m_Rotation - m_DesiredRotation;\n\n    if( rotate_direction > PI ) {\n        rotate_direction -= 2 * PI;\n    }\n    else if( rotate_direction < -PI ) {\n        rotate_direction += 2 * PI;\n    }\n\n    if( rotate_direction < -0.05 ) {\n        m_Ship->RotateRight();\n        m_Ship->RotateRight();\n    }\n    else if( rotate_direction > 0.05 ) {\n        m_Ship->RotateLeft();\n        m_Ship->RotateLeft();\n    }\n    m_Ship->Accelerate();\n    m_Ship->Accelerate();\n    m_Ship->Accelerate();\n    m_Ship->Accelerate();\n}\n```\n\n该函数做的第一件事是确定船只应该指向哪个角度来寻找目的地:\n\n```cpp\nVector2D direction = seek_point;\ndirection -= m_Ship->m_Position;\nm_DesiredRotation = direction.FindAngle();\nfloat rotate_direction = m_Ship->m_Rotation - m_DesiredRotation;\n\nif( rotate_direction > PI ) {\n    rotate_direction -= 2 * PI;\n}\nelse if( rotate_direction < -PI ) {\n    rotate_direction += 2 * PI;\n}\n```\n\n基于我们计算的`rotate_direction`值，人工智能决定向左或向右旋转船只:\n\n```cpp\nif( rotate_direction < -0.05 ) {\n    m_Ship->RotateRight();\n    m_Ship->RotateRight();\n}\nelse if( rotate_direction > 0.05 ) {\n    m_Ship->RotateLeft();\n    m_Ship->RotateLeft();\n}\n```\n\n你可能想知道为什么有两个电话打给`RotateRight()`和`RotateLeft()`。嗯，这有点人工智能作弊。我希望敌方飞船比玩家旋转和加速更快，所以我们两次调用`Rotate`功能，四次调用`Accelerate`功能。你作弊的数量取决于个人喜好，以及你作弊的明显程度。一般来说，你希望你的 AI 有挑战性，但不要太有挑战性。一个明显作弊的 AI 会让玩家不爽。最重要的是，如果你作弊，一定不要被抓！\n\n旋转之后，我们用对`Accelerate()`的四次调用来结束函数:\n\n```cpp\nm_Ship->Accelerate();\nm_Ship->Accelerate();\nm_Ship->Accelerate();\nm_Ship->Accelerate();\n```\n\n在我们的`SEEK`状态之后，我们需要定义我们在`FLEE`状态下运行的函数。`FLEE`状态与`SEEK`状态相反，因为人工智能试图尽可能远离逃离位置。在我们版本的`FLEE`状态中，我们少做了一点欺骗，但这可以根据个人喜好来改变:\n\n```cpp\nvoid FiniteStateMachine::FleeState(Vector2D& flee_point) {\n    Vector2D direction = flee_point;\n    direction -= m_Ship->m_Position;\n    m_DesiredRotation = direction.FindAngle();\n    float rotate_direction = m_DesiredRotation - m_Ship->m_Rotation;\n    rotate_direction -= PI;\n\n    if( rotate_direction > 0 ) {\n        m_Ship->RotateRight();\n    }\n    else {\n        m_Ship->RotateLeft();\n    }\n    m_Ship->Accelerate();\n    m_Ship->Accelerate();\n}\n```\n\n`WANDER`状态是 AI 在游戏区游荡的状态。如果敌人的船对玩家的船没有通畅的视线，这个状态就会运行。人工智能会在游戏区域四处游荡，寻找一条通往玩家的无障碍路径。在`WANDER`状态下，飞船更有可能继续朝着上次转向的方向转向，而不是选择新的方向。下面是代码:\n\n```cpp\nvoid FiniteStateMachine::WanderState() {\n    m_NextTurnMS -= delta_time;\n\n    if( m_NextTurnMS <= 0 ) {\n        bool same_turn = ( m_SameTurnPct >= rand() % 100 );\n        m_NextTurnMS = c_MinRandomTurnMS + rand() % c_RandTurnMS;\n\n        if( m_LastTurnLeft ) {\n            if( same_turn ) {\n                m_SameTurnPct -= 10;\n                m_Ship->RotateLeft();\n            }\n            else {\n                m_SameTurnPct = 80;\n                m_Ship->RotateRight();\n            }\n        }\n        else {\n            if( same_turn ) {\n                m_SameTurnPct -= 10;\n                m_Ship->RotateRight();\n            }\n            else {\n                m_SameTurnPct = 80;\n                m_Ship->RotateLeft();\n            }\n        }\n    }\n    m_Ship->Accelerate();\n}\n```\n\n向玩家射击时`Attack`状态称为`Seek`状态:\n\n```cpp\nvoid FiniteStateMachine::AttackState() {\n    Vector2D prediction = PredictPosition();\n    SeekState( prediction );\n    m_Ship->Shoot();\n}\n```\n\n为了知道当我们寻找和攻击时该去哪里，我们可以将我们的敌舰直接指向玩家当前的位置。如果我们能在到达时预测玩家的船会在哪里，那就更好了。我们有一个`PredictPosition`函数，它会利用玩家当前的速度来预测玩家会在哪里。以下是我们的`PredictPosition`功能:\n\n```cpp\nVector2D FiniteStateMachine::PredictPosition() {\n    Vector2D dist = player->m_Position;\n    dist -= m_Ship->m_Position;\n    float mag = dist.Magnitude();\n    Vector2D dir = player->m_Velocity;\n\n    if( dir.MagSQ() > 0 ) {\n        dir.Normalize();\n    }\n    dir *= (mag / 10);\n    Vector2D prediction = player->m_Position;\n    prediction += dir;\n    return prediction;\n}\n```\n\n这只是猜测，并不完美。我们用这个函数来预测我们将在哪里寻找和攻击。如果我们在寻找玩家，我们可能会想要预测玩家将移动的距离，这将与敌人船和玩家船之间的当前距离大致相同。然而，更重要的是，当我们发射炮弹时，我们要预测炮弹的位置。投射物的移动速度比我们的飞船快很多，所以我们用敌人飞船和玩家飞船之间的距离除以 10 来做我们的预测。投射物的移动速度实际上并不是 10 倍，但是，就像我们为人工智能选择的许多常量一样，试错和看起来正确的东西胜过实际数据。将倍数降低到 5 倍将使我们每次射击领先玩家飞船的距离增加一倍。将值设为 20 会将领先优势减半。当我在测试人工智能时，10 的值在我看来是正确的，但是你可以根据自己的口味调整这个数字。如果你愿意，你甚至可以添加一个随机因素。\n\n# 回避力函数\n\n`AvoidForce`功能也有点骗。转向行为使用避免力来防止自主代理与障碍物碰撞。如果躲避力值设置得太高，看起来敌人的船会被魔法从障碍物上击退。如果太低，它会直接撞上他们。我们的`AvoidForce`功能将寻找离我们的敌船最近的障碍物，并将提高敌船的速度，使其绕过任何障碍物。这个函数是这样的:\n\n```cpp\nvoid FiniteStateMachine::AvoidForce() {\n    Vector2D start_corner;\n    Vector2D end_corner;\n    Vector2D avoid_vec;\n    Vector2D dist;\n\n    float closest_square = 999999999999.0;\n    float msq;\n    Vector2D star_avoid;\n\n    star_avoid.x = CANVAS_WIDTH / 2;\n    star_avoid.y = CANVAS_HEIGHT / 2;\n    star_avoid -= m_Ship->m_Position;\n\n    msq = star_avoid.MagSQ();\n\n    if( msq >= c_StarAvoidDistSQ ) {\n        start_corner = m_Ship->m_Position;\n        start_corner.x -= c_AvoidDist;\n        start_corner.y -= c_AvoidDist;\n        end_corner = m_Ship->m_Position;\n        end_corner.x += c_AvoidDist;\n        end_corner.y += c_AvoidDist;\n        Asteroid* asteroid;\n        std::vector<Asteroid*>::iterator it;\n        int i = 0;\n\n        for( it = asteroid_list.begin(); it != asteroid_list.end(); \n             it++ ) {\n            asteroid = *it;\n            if( asteroid->m_Active == true &&\n                asteroid->SteeringRectTest( start_corner, end_corner ) ) {\n\n                dist = asteroid->m_Position;\n                dist -= m_Ship->m_Position;\n                msq = dist.MagSQ();\n\n                if( msq <= closest_square ) {\n                    closest_square = msq;\n                    avoid_vec = asteroid->m_Position;\n                }\n            }\n        }\n\n        // LOOP OVER PROJECTILES\n        Projectile* projectile;\n        std::vector<Projectile*>::iterator proj_it;\n\n        for( proj_it = projectile_pool->m_ProjectileList.begin(); \n             proj_it != projectile_pool->m_ProjectileList.end(); \n             proj_it++ ) {\n\n            projectile = *proj_it;\n\n            if( projectile->m_Active == true &&\n                projectile->SteeringRectTest( start_corner, end_corner ) \n                ) {\n\n                dist = projectile->m_Position;\n                dist -= m_Ship->m_Position;\n                msq = dist.MagSQ();\n\n                if( msq <= closest_square ) {\n                    closest_square = msq;\n                    avoid_vec = projectile->m_Position;\n                }\n            }\n        }\n        if( closest_square != 999999999999.0 ) {\n            avoid_vec -= m_Ship->m_Position;\n            avoid_vec.Normalize();\n            float rot_to_obj = avoid_vec.FindAngle();\n\n            if( std::abs( rot_to_obj - m_Ship->m_Rotation ) < 0.75 ) {\n                if( rot_to_obj >= m_Ship->m_Rotation ) {\n                    m_Ship->RotateLeft();\n                }\n                else {\n                    m_Ship->RotateRight();\n                }\n            }\n            m_Ship->m_Velocity -= avoid_vec * delta_time * \n            c_ObstacleAvoidForce;\n        }\n    }\n    else {\n        avoid_vec.x = CANVAS_WIDTH / 2;\n        avoid_vec.y = CANVAS_HEIGHT / 2;\n        avoid_vec -= m_Ship->m_Position;\n        avoid_vec.Normalize();\n        float rot_to_obj = avoid_vec.FindAngle();\n\n        if( std::abs( rot_to_obj - m_Ship->m_Rotation ) < 0.75 ) {\n            if( rot_to_obj >= m_Ship->m_Rotation ) {\n                m_Ship->RotateLeft();\n            }\n            else {\n                m_Ship->RotateRight();\n            }\n        }\n        m_Ship->m_Velocity -= avoid_vec * delta_time * c_StarAvoidForce;\n    }\n}\n```\n\n我们在这个功能中的第一个检查是我们离游戏区域中心的星星有多近。这个明星是我们最需要避免的。即使我们的护盾打开，它也是唯一会摧毁我们的物体，所以人工智能需要额外确保它不会击中恒星。这个检查包括找到游戏区域中心和敌方飞船之间的平方距离，并根据我们在类定义调用`c_StarAvoidDistSQ`中设置的常数来检查该值:\n\n```cpp\nif( msq >= c_StarAvoidDistSQ ) {\n```\n\n你可以调整`c_StarAvoidDistSQ`的值，让敌方飞船靠近或远离游戏画面的中心。如果我们的敌船不太靠近可视游戏区域，我们会查看飞船附近是否有障碍物:\n\n```cpp\nif( msq >= c_StarAvoidDistSQ ) {\n    start_corner = m_Ship->m_Position;\n    start_corner.x -= c_AvoidDist;\n    start_corner.y -= c_AvoidDist;\n\n    end_corner = m_Ship->m_Position;\n    end_corner.x += c_AvoidDist;\n    end_corner.y += c_AvoidDist;\n\n    Asteroid* asteroid;\n    std::vector<Asteroid*>::iterator it;\n    int i = 0;\n\n    for( it = asteroid_list.begin(); it != asteroid_list.end(); it++ ) {\n        asteroid = *it;\n        if( asteroid->m_Active == true &&\n            asteroid->SteeringRectTest( start_corner, end_corner ) ) {\n\n            dist = asteroid->m_Position;\n            dist -= m_Ship->m_Position;\n            msq = dist.MagSQ();\n\n            if( msq <= closest_square ) {\n                closest_square = msq;\n                avoid_vec = asteroid->m_Position;\n            }\n        }\n    }\n    // LOOP OVER PROJECTILES\n    Projectile* projectile;\n    std::vector<Projectile*>::iterator proj_it;\n\n    for( proj_it = projectile_pool->m_ProjectileList.begin(); \n         proj_it != projectile_pool->m_ProjectileList.end(); proj_it++ \n         ) {\n\n        projectile = *proj_it;\n\n        if( projectile->m_Active == true &&\n            projectile->SteeringRectTest( start_corner, end_corner \n            ) ) {\n            dist = projectile->m_Position;\n            dist -= m_Ship->m_Position;\n            msq = dist.MagSQ();\n\n            if( msq <= closest_square ) {\n                closest_square = msq;\n                avoid_vec = projectile->m_Position;\n            }\n        }\n    }\n    if( closest_square != 999999999999.0 ) {\n        avoid_vec -= m_Ship->m_Position;\n        avoid_vec.Normalize();\n        float rot_to_obj = avoid_vec.FindAngle();\n        if( std::abs( rot_to_obj - m_Ship->m_Rotation ) < 0.75 ) {\n            if( rot_to_obj >= m_Ship->m_Rotation ) {\n                m_Ship->RotateLeft();\n            }\n            else {\n                m_Ship->RotateRight();\n            }\n        }\n        m_Ship->m_Velocity -= avoid_vec * delta_time * \n        c_ObstacleAvoidForce;\n    }\n}\n```\n\n我们对游戏中的所有小行星和抛射体进行矩形测试。在`if`块的开始，我们设置矩形测试的角:\n\n```cpp\nstart_corner = m_Ship->m_Position;\nstart_corner.x -= c_AvoidDist;\nstart_corner.y -= c_AvoidDist;\n\nend_corner = m_Ship->m_Position;\nend_corner.x += c_AvoidDist;\nend_corner.y += c_AvoidDist;\n```\n\n`c_AvoidDist`常量在`FiniteStateMachine`类定义中设置，可以根据自己的口味进行更改。增加躲避距离使人工智能与所有射弹保持更大的距离。如果你把这个值设置得太高，你的 AI 会相当胆小。减少距离，人工智能将能够忍受飞得离障碍物更近。如果太低，它会频繁地撞上它们。在确定用于矩形测试的值后，我们在所有小行星上循环，寻找一颗既活跃又在矩形测试范围内的小行星:\n\n```cpp\nAsteroid* asteroid;\nstd::vector<Asteroid*>::iterator it;\nint i = 0;\n\nfor( it = asteroid_list.begin(); it != asteroid_list.end(); it++ ) \n{\n    asteroid = *it;\n    if( asteroid->m_Active == true &&\n        asteroid->SteeringRectTest( start_corner, end_corner ) ) {\n\n        dist = asteroid->m_Position;\n        dist -= m_Ship->m_Position;\n        msq = dist.MagSQ();\n\n        if( msq <= closest_square ) {\n             closest_square = msq;\n             avoid_vec = asteroid->m_Position;\n        }\n    }\n}\n```\n\n当增加躲避力时，我们只是在躲避最近的障碍物。您可以编写一个更复杂的版本，能够为我们的边界框内的几个对象添加一个躲避力，但是躲避最近的障碍物效果相当好。在检查了我们所有的小行星后，我们检查是否有一个抛射体比最近的小行星更活跃、更近:\n\n```cpp\n    // LOOP OVER PROJECTILES\n    Projectile* projectile;\n    std::vector<Projectile*>::iterator proj_it;\n    for( proj_it = projectile_pool->m_ProjectileList.begin(); \n         proj_it != projectile_pool->m_ProjectileList.end(); proj_it++ ) {\n        projectile = *proj_it;\n        if( projectile->m_Active == true &&\n            projectile->SteeringRectTest( start_corner, end_corner ) ) {\n            dist = projectile->m_Position;\n            dist -= m_Ship->m_Position;\n            msq = dist.MagSQ();\n\n            if( msq <= closest_square ) {\n                closest_square = msq;\n                avoid_vec = projectile->m_Position;\n            }\n        }\n    }\n```\n\n如果我们在我们的边界框中找到至少一个物体，我们希望两者都旋转我们的宇宙飞船，这样它就会像玩家一样自然地移动以避开它，我们还会添加一个回避力，这有点欺骗。根据我们在类定义中设置的常数`c_ObstacleAvoidForce`，躲避力将我们的敌人飞船推离物体。这个值可以上下调整。总的来说，我喜欢保持这个值很高，冒着玩家可能意识到这是作弊的风险。您可以根据自己的喜好修改`c_ObstacleAvoidForce`的值:\n\n```cpp\nif( closest_square != 999999999999.0 ) {\n    avoid_vec -= m_Ship->m_Position;\n    avoid_vec.Normalize();\n    float rot_to_obj = avoid_vec.FindAngle();\n    if( std::abs( rot_to_obj - m_Ship->m_Rotation ) < 0.75 ) {\n        if( rot_to_obj >= m_Ship->m_Rotation ) {\n            m_Ship->RotateLeft();\n        }\n        else {\n            m_Ship->RotateRight();\n        }\n    }\n    m_Ship->m_Velocity -= avoid_vec * delta_time * c_ObstacleAvoidForce;\n}\n```\n\n如果敌舰不太靠近恒星，障碍分支就会运行。如果物体离恒星太近，代码会跳到`else`块。该代码创建了一个避免力，推动和引导船只远离游戏区域的中心。它有自己的常数避免力，我们在类定义中设置:\n\n```cpp\nelse {\n    avoid_vec.x = CANVAS_WIDTH / 2;\n    avoid_vec.y = CANVAS_HEIGHT / 2;\n    avoid_vec -= m_Ship->m_Position;\n    avoid_vec.Normalize();\n    float rot_to_obj = avoid_vec.FindAngle();\n\n    if( std::abs( rot_to_obj - m_Ship->m_Rotation ) < 0.75 ) {\n        if( rot_to_obj >= m_Ship->m_Rotation ) {\n            m_Ship->RotateLeft();\n        }\n        else {\n            m_Ship->RotateRight();\n        }\n    }\n    m_Ship->m_Velocity -= avoid_vec * delta_time * c_StarAvoidForce;\n}\n```\n\n`ShieldCheck`功能类似于躲避力功能，它检查一个包围矩形，看我们的船附近是否有障碍物。然后，它确定船只是否不太可能避免碰撞。无论我们的转向力有多强，有时我们都无法避开小行星或抛射体。如果是这样的话，我们要举起护盾。我们不需要检查我们是否靠近恒星，因为无论我们的护盾是否打开，恒星都会杀死我们，所以没有必要在`ShieldCheck`功能中担心这个:\n\n```cpp\nbool FiniteStateMachine::ShieldCheck() {\n    Vector2D start_corner;\n    Vector2D end_corner;\n\n    start_corner = m_Ship->m_Position;\n    start_corner.x -= c_ShieldDist;\n    start_corner.y -= c_ShieldDist;\n\n    end_corner = m_Ship->m_Position;\n    end_corner.x += c_ShieldDist;\n    end_corner.y += c_ShieldDist;\n\n    Asteroid* asteroid;\n    std::vector<Asteroid*>::iterator it;\n    int i = 0;\n\n    for( it = asteroid_list.begin(); it != asteroid_list.end(); it++ ) {\n        asteroid = *it;\n        if( asteroid->m_Active &&\n            asteroid->SteeringRectTest( start_corner, end_corner ) ) {\n            return true;\n        }\n    }\n    // LOOP OVER PROJECTILES\n    Projectile* projectile;\n    std::vector<Projectile*>::iterator proj_it;\n\n    for( proj_it = projectile_pool->m_ProjectileList.begin(); \n         proj_it != projectile_pool->m_ProjectileList.end(); proj_it++ ) {\n        projectile = *proj_it;\n        if( projectile->m_Active &&\n            projectile->SteeringRectTest( start_corner, end_corner ) ) {\n            return true;\n        }\n    }\n    return false;\n}\n```\n\n像躲避力检查一样，我们在我们的船周围设置了一个边界矩形`c_ShieldDist`常数。该值应低于避免力。如果不是，当我们可以避开这个物体时，我们会不必要地举起我们的护盾。就像我们 AI 中的其他东西一样，如果`c_ShieldDist`的值设置得太高，我们在不需要的时候就会举起护盾。我们的护盾用途有限，所以这将浪费我们以后可以使用的护盾时间。如果我们把数值设得太低，在我们有机会升起护盾之前，我们就有可能碰到飞船正在加速前进的障碍物。\n\n下一个功能`LOSCheck`，是视线检查。这意味着它看起来是不是可以在敌人的船和玩家的船之间画一条直线，而不与任何障碍物相交。如果视线清晰，该功能返回`true`。如果有障碍物阻挡视线，该功能返回`false`:\n\n```cpp\nbool FiniteStateMachine::LOSCheck() { // LINE OF SIGHT CHECK\n    // LOOP OVER ASTEROIDS\n    Asteroid* asteroid;\n    std::vector<Asteroid*>::iterator it;\n    int i = 0;\n    for( it = asteroid_list.begin(); it != asteroid_list.end(); it++ ) {\n        asteroid = *it;\n        if( asteroid->SteeringLineTest( m_Ship->m_Position, \n        player->m_Position ) ) {\n            return false;\n        }\n    }\n\n    // LOOP OVER PROJECTILES\n    Projectile* projectile;\n    std::vector<Projectile*>::iterator proj_it;\n    for( proj_it = projectile_pool->m_ProjectileList.begin(); \n         proj_it != projectile_pool->m_ProjectileList.end(); proj_it++ ) {\n        projectile = *proj_it;\n        if( projectile->SteeringLineTest( m_Ship->m_Position, \n        player->m_Position ) ) {\n            return false;\n        }\n    }\n    return true;\n}\n```\n\n我们经常想要检查的一件事是玩家到敌舰的距离。因为平方根是一个耗时的操作，我们通过检查平方距离来消除它。我们使用`GetPlayerDistSq`函数获得敌方飞船和玩家飞船之间的平方距离:\n\n```cpp\nfloat FiniteStateMachine::GetPlayerDistSq() {\n    float x_diff = m_Ship->m_Position.x - player->m_Position.x;\n    float y_diff = m_Ship->m_Position.y - player->m_Position.y;\n    return x_diff * x_diff + y_diff * y_diff;\n}\n```\n\nFSM 的`Move`功能是每帧运行我们的 AI 的功能。它执行一系列检查来确定人工智能应该处于什么状态，并执行该状态的功能。它还检查人工智能是否应该升起或降下飞船的护盾。以下是该功能的全部内容:\n\n```cpp\nvoid FiniteStateMachine::Move() {\n    m_CheckCycle++ ;\n    if( m_CheckCycle == 0 ) {\n        m_HasLOS = LOSCheck();\n        if( !m_HasLOS ) {\n            m_CurrentState = WANDER;\n        }\n        float player_dist_sq = 0.0f;\n    }\n    else if( m_CheckCycle == 1 ) {\n        if( m_HasLOS ) {\n            m_PlayerDistSQ = GetPlayerDistSq();\n            if( m_PlayerDistSQ <= c_FleeDistSq ) {\n                m_CurrentState = FLEE;\n            }\n            else if( m_PlayerDistSQ <= c_AttackDistSq ) {\n                m_CurrentState = ATTACK;\n            }\n            else {\n                m_CurrentState = APPROACH;\n            }\n        }\n    }\n    else {\n        AvoidForce();\n        m_CheckCycle = -1;\n    }\n    if( ShieldCheck() ) {\n        m_Ship->m_Shield->Activate();\n    }\n    else {\n        m_Ship->m_Shield->Deactivate();\n    }\n    if( m_CurrentState == APPROACH ) {\n        Vector2D predict = PredictPosition();\n        SeekState(predict);\n    }\n    else if( m_CurrentState == ATTACK ) {\n        AttackState();\n    }\n    else if( m_CurrentState == FLEE ) {\n        Vector2D predict = PredictPosition();\n        FleeState(predict);\n    }\n    else if( m_CurrentState == WANDER ) {\n        WanderState();\n    }\n}\n```\n\n我们使用`m_CheckCycle`属性来循环执行不同的状态检查，以减轻 CPU 的负担。对于像这样简单的人工智能来说，这并不是真正必要的。在我们的游戏中只有一个代理在执行这个人工智能，但是如果我们扩展它来使用多个代理，我们可能会设置每个代理从一个不同的循环检查号开始来分散我们的计算。目前，此循环检查仅用于演示目的:\n\n```cpp\nm_CheckCycle++ ;\n\nif( m_CheckCycle == 0 ) {\n    m_HasLOS = LOSCheck();\n    if( !m_HasLOS ) {\n        m_CurrentState = WANDER;\n    }\n    float player_dist_sq = 0.0f;\n}\nelse if( m_CheckCycle == 1 ) {\n    if( m_HasLOS ) {\n        m_PlayerDistSQ = GetPlayerDistSq();\n        if( m_PlayerDistSQ <= c_FleeDistSq ) {\n            m_CurrentState = FLEE;\n        }\n        else if( m_PlayerDistSQ <= c_AttackDistSq ) {\n            m_CurrentState = ATTACK;\n        }\n        else {\n            m_CurrentState = APPROACH;\n        }\n    }\n}\nelse {\n    AvoidForce();\n    m_CheckCycle = -1;\n}\n```\n\n如你所见，如果我们在周期 0，我们运行视线检查，如果我们没有视线，我们将当前状态设置为`WANDER`。在第一个周期中，我们会查看我们在最后一帧是否有视线，如果有，我们会根据敌舰和玩家舰之间的距离来判断我们是想要靠近、逃跑还是攻击。在周期 2 中，我们添加任何避免力并重置我们的检查周期属性。\n\n然后我们每个周期都进行一次屏蔽检查。我最初每隔四个周期进行一次护盾检查，但是敌人的船被迎面射来的炮弹击中的次数太多了。正因为如此，我更改了代码，以便在每个周期执行屏蔽检查。这就是你最终在游戏人工智能中做的那种手动调整，以使其工作。有很多尝试和错误:\n\n```cpp\nif( ShieldCheck() ) {\n    m_Ship->m_Shield->Activate();\n}\nelse {\n    m_Ship->m_Shield->Deactivate();\n}\n```\n\n最后几个代码块只是一系列的`if`和`else if`语句，它们查看当前状态是什么，并基于该状态调用适当的函数:\n\n```cpp\nif( m_CurrentState == APPROACH ) {\n    Vector2D predict = PredictPosition();\n    SeekState(predict);\n}\nelse if( m_CurrentState == ATTACK ) {\n    AttackState();\n}\nelse if( m_CurrentState == FLEE ) {\n    Vector2D predict = PredictPosition();\n    FleeState(predict);\n}\nelse if( m_CurrentState == WANDER ) {\n    WanderState();\n}\n```\n\n# 正在编译 ai.html 文件\n\n我们现在准备编译和测试我们的`ai.html`文件。这个版本的游戏截图看起来和我们之前的版本有很大的不同:\n\n```cpp\nem++ asteroid.cpp collider.cpp emitter.cpp enemy_ship.cpp finite_state_machine.cpp main.cpp particle.cpp player_ship.cpp projectile_pool.cpp projectile.cpp range.cpp shield.cpp ship.cpp star.cpp vector.cpp -o ai.html --preload-file sprites -std=c++ 17 -s USE_WEBGL2=1 -s USE_SDL=2 -s USE_SDL_IMAGE=2 -s SDL2_IMAGE_FORMATS=[\"png\"] -s USE_SDL_IMAGE=2 -s SDL2_IMAGE_FORMATS=[\"png\"] \n```\n\n新版本的游戏将有一个更大的画布，小行星和一颗星星在中间。敌人的飞船会找到玩家并发动攻击。下面是截图:\n\n![](img/d777e80d-46b6-4267-9d9c-18343538ce48.png)\n\nA screenshot of ai.html Remember that you must run WebAssembly apps using a web server, or with `emrun`.  If you would like to run your WebAssembly app using `emrun`, you must compile it with the `--emrun` flag.  The web browser requires a web server to stream the WebAssembly module.  If you attempt to open an HTML page that uses WebAssembly in a browser directly from your hard drive, that WebAssembly module will not load.\n\n# 摘要\n\n在这一章中，我们讨论了游戏人工智能，它是什么，以及它与学术人工智能有什么不同。我们讨论了使用自主代理与自上而下的人工智能，以及每种人工智能风格的优势，以及我们如何将这两种风格混合在一起。\n\n我介绍了 FSM 的概念，并提到了 FSM 在游戏中的早期使用，如 *PAC-MAN* ，我们探索了转向行为，以及我们将在游戏中用来指导代理的转向行为的种类。我们增加了小行星和一颗恒星作为我们游戏的障碍，并增加了我们游戏区域的大小。我们增加了新的碰撞检测形式，使我们的人工智能能够确定它何时与我们的玩家有视线。我们还增加了矩形碰撞检测，以确定是否有障碍物足够近，我们的人工智能可以使用躲避力。我们将`Point`类扩展为`Vector2D`类，并增加了新的功能，包括投影、幅度和点积计算。我们编写了一个有限状态机，并使用它来确定我们将使用什么转向力，以及在什么情况下使用。\n\n在下一章中，我们将大大扩展我们关卡的尺寸，并增加一个摄像头，这样我们就可以在这个更大版本的游戏区域移动我们的飞船。"
  },
  {
    "path": "docs/handson-game-dev-wasm/11.md",
    "content": "# 十一、设计 2D 相机\n\n相机设计是游戏新手设计师经常忘记的事情之一。到目前为止，我们已经有了所谓的*固定位置摄像机*。只有一个屏幕，视角没有变化。20 世纪 70 年代，几乎所有早期的街机游戏都是这样设计的。我发现的用任何相机拍摄的最古老的游戏是雅达利的*月球着陆器*，它于 1979 年 8 月发布。*月球着陆器*是一个早期的基于矢量的游戏，当着陆器接近月球表面时，它会放大相机，然后当你的着陆器接近表面时，它会平移相机来跟随。\n\n20 世纪 80 年代初，更多的游戏开始尝试一个比单个游戏屏幕更大的游戏世界。*拉力赛 X* 是南科在 1980 年发布的一款 *Pac-Man-* 之类的迷宫游戏，迷宫比单个显示器还要大。*拉力赛 X* 使用了一个*位置抓拍摄像头*(有时被称为*锁定摄像头*)，无论发生什么情况，该摄像头始终将玩家的车保持在游戏屏幕的中央。这是你可以实现的最直接的 2D 滚动相机形式，许多游戏新手设计师会创建一个 *2D 位置抓拍相机*然后收工，但是你可能希望在你的游戏中实现一个更复杂的相机是有原因的。\n\n中途岛在 1981 年发布了游戏*防御者*。这是一个侧滚射击游戏，玩家可以向任何方向移动他们的飞船。意识到玩家需要在飞船面对的方向上看到更多的水平，*防御者*使用了第一个*双前焦摄像头*。这个摄像头会移动观看区域，让三分之二的屏幕在玩家飞船面对的方向前面，三分之一的屏幕在后面。这就把更多的焦点放在了玩家面前。相机不只是在两个位置之间来回切换。那会很不和谐。相反，当玩家切换方向时，相机位置会平稳地转换到新位置(对 1981 年来说相当酷)。\n\n20 世纪 80 年代，许多新的相机设计开始使用。Konami 开始在他们的许多射击游戏中使用自动滚动相机，包括 *Scramble* 、 *Gradius* 和 *1942* 。1985 年，雅达利发布了*战书*，这是一款早期的多人游戏，允许四名玩家同时参与游戏。*排管*中的摄像头定位在玩家所有位置的平均值。平台游戏，如*超级马里奥兄弟*，将允许用户向前推动相机的位置。\n\nYou will need to include several images in your build to make this project work. Make sure you include the `/Chapter11/sprites/` folder from the project's GitHub. If you haven't yet downloaded the GitHub project, you can get it online at [https://github.com/PacktPublishing/Hands-On-Game-Development-with-WebAssembly](https://github.com/PacktPublishing/Hands-On-Game-Development-with-WebAssembly).\n\n如果你花时间去看看，2D 相机有很多很好的例子。我们将集中(无意双关)一些对我们的游戏有帮助的 2D 相机功能。\n\n# 为我们的游戏制作一个摄像头\n\n我们将在几个不同的阶段制造我们的相机。我们将从裸机**锁定摄像头**实现开始。这将为我们添加新的相机功能提供一个良好的起点。稍后，我们将把这款相机修改为**投影对焦相机**。投射式对焦相机会观察玩家飞船的速度，并调整相机，以便在玩家面前显示更多的游戏区域。这种技术的工作原理是基于这样一种假设，即在这个游戏中，玩家通常更专注于玩家飞船移动方向上的游戏性。对于我们相机的最终版本，我们将在投射物中添加*相机* *吸引子*。这种修改背后的想法是，当游戏中有射击时，相机应该将注意力吸引到游戏的那个区域。\n\n# 用于跟踪玩家运动的摄像机\n\n我们相机的第一个实现将是一个锁定的相机，它将锁定我们的玩家，并跟随他们在关卡中的区域移动。现在，我们的关卡和那个关卡的*固定摄像头*一样大。我们不仅需要使我们的水平更大，而且我们还需要修改我们的对象包装，以便它与我们的相机一起工作。要实现我们的锁定相机，我们需要做的第一件事就是修改我们的`game.hpp`文件。我们将创建一个`Camera`类和一个`RenderManager`类，在那里我们将移动所有渲染特定的代码。我们还需要添加一些`#define`宏来定义我们级别的高度和宽度，因为这将不同于我们已经定义的画布高度和宽度。我们还将在我们的`Vector2D`类中添加一些额外的重载操作符。\n\n# 投影焦点和照相机吸引器\n\n锁定摄像头并不是一件可怕的事情，但是更好的摄像头可以显示玩家需要看到的更多内容。在我们的游戏中，玩家更有可能对他们前进的方向感兴趣。在运动方向上向前看的照相机有时被称为投射聚焦照相机。我们可以查看我们的船当前移动的速度，并相应地偏移我们的相机。\n\n我们将采用的另一种摄像技术叫做**摄像吸引器**。有时在游戏中，有一些感兴趣的对象可以用来拉/吸引相机的焦点。这些会产生一种吸引力，将我们的相机拉向那个方向。我们相机的一个吸引力是敌舰。另一个吸引力是射弹。敌人的船代表潜在的行动，投射物代表对我们玩家的潜在威胁。在本节中，我们将结合投影焦点和相机吸引器来改善我们的相机定位。\n\n最后我想补充的是一个箭头，它指向敌人的宇宙飞船。因为现在的游戏区域比画布还大，我们需要一个提示来帮助我们找到敌人。没有这一点，我们可能会发现自己漫无目的地闲逛，这不是很有趣。另一种方法是用迷你地图，但是，因为只有一个敌人，我觉得箭更容易实现。让我们浏览一下我们需要添加的代码，以改进我们的相机，并添加我们的定位箭头。\n\n# 修改我们的代码\n\n我们需要为这一章添加几个新的类。显然，如果我们在游戏中想要一个摄像头，我们将需要添加一个`Camera`类。在代码的早期版本中，渲染是通过直接调用 SDL 来完成的。因为 SDL 没有相机作为 API 的一部分，所以我们需要添加一个`RenderManager`类，作为渲染过程中的中间步骤。这个类将使用摄像机的位置来决定我们将在画布上的什么地方渲染我们的游戏对象。我们将增加我们的游戏区域到四个屏幕宽和四个屏幕高。这就产生了一个游戏性的问题，因为现在，我们在玩的时候需要能够找到敌人的飞船。为了解决这个问题，我们需要创建一个定位器**用户界面** ( **用户界面**)元素，该元素将箭头指向敌人飞船的方向。\n\n# 修改 game.hpp 文件\n\n让我们浏览一下我们将对`game.hpp`文件进行的更改。我们从添加几个`#define`宏开始:\n\n```cpp\n#define LEVEL_WIDTH CANVAS_WIDTH*4\n#define LEVEL_HEIGHT CANVAS_HEIGHT*4\n```\n\n这将定义我们关卡的宽度和高度是画布宽度和高度的四倍。在我们的类列表的末尾，我们应该添加一个`Camera`类、`Locator`类和`RenderManager`类，如下所示:\n\n```cpp\nclass Ship;\nclass Particle;\nclass Emitter;\nclass Collider;\nclass Asteroid;\nclass Star;\nclass PlayerShip;\nclass EnemyShip;\nclass Projectile;\nclass ProjectilePool;\nclass FiniteStateMachine;\nclass Camera;\nclass RenderManager;\nclass Locator;\n```\n\n您会注意到最后三行声明一个名为`Camera`的类、一个名为`Locator`的类和一个名为`RenderManager`的类将在代码的后面定义。\n\n# Vector2D 类定义\n\n我们将扩展我们的`Vector2D`类定义，为我们的`Vector2D`类中的`+`和`-`操作符添加一个`operator+`和`operator-`重载。\n\nIf you are not familiar with operator overloading, these are a convenient way to allow classes to use C++ operators instead of functions. There is a good tutorial that can help if you are looking for more information that is available at [https://www.tutorialspoint.com/cplusplus/cpp_overloading.htm](https://www.tutorialspoint.com/cplusplus/cpp_overloading.htm).\n\n以下是`Vector2D`类的新定义:\n\n```cpp\nclass Vector2D {\n    public:\n        float x;\n        float y;\n\n        Vector2D();\n        Vector2D( float X, float Y );\n\n        void Rotate( float radians );\n        void Normalize();\n        float MagSQ();\n        float Magnitude();\n        Vector2D Project( Vector2D &onto );\n        float Dot(Vector2D &vec);\n        float FindAngle();\n\n        Vector2D operator=(const Vector2D &vec);\n        Vector2D operator*(const float &scalar);\n        void operator+=(const Vector2D &vec);\n        void operator-=(const Vector2D &vec);\n        void operator*=(const float &scalar);\n        void operator/=(const float &scalar);\n Vector2D operator-(const Vector2D &vec);\n Vector2D operator+(const Vector2D &vec);\n};\n```\n\n您会注意到定义的最后两行是新的:\n\n```cpp\nVector2D operator-(const Vector2D &vec);\nVector2D operator+(const Vector2D &vec);\n```\n\n# 定位器类定义\n\n`Locator`类是一个 UI 元素的新类，它将是一个箭头，将我们的玩家指向敌人飞船的方向。我们需要一个 UI 元素来帮助玩家在敌人飞船没有出现在画布上的时候找到它。下面是类定义的样子:\n\n```cpp\nclass Locator {\n    public:\n        bool m_Active = false;\n        bool m_LastActive = false;\n        SDL_Texture *m_SpriteTexture;\n        SDL_Rect m_dest = {.x = 0, .y = 0, .w = 32, .h = 32 };\n        Vector2D m_Position;\n        int m_ColorFlux;\n        float m_Rotation;\n\n        Locator();\n        void SetActive();\n        void Move();\n        void Render();\n};\n```\n\n前两个属性是布尔标志，与定位器的活动状态有关。`m_Active`属性告诉我们定位器当前是否活动，是否应该渲染。`m_LastActive`属性是一个布尔标志，它告诉我们上次渲染帧时定位器是否处于活动状态。接下来的两行是 sprite 纹理和目标矩形，渲染管理器将使用它们来渲染这个游戏对象:\n\n```cpp\n        SDL_Texture *m_SpriteTexture;\n        SDL_Rect m_dest = {.x = 0, .y = 0, .w = 32, .h = 32 };\n```\n\n之后，我们在`m_Position`属性中有一个`x`和`y`位置值，`m_ColorFlux`中有一个代表 RGB 颜色值的整数，`m_Rotation`属性中有一个子画面的旋转值。我们将使用`m_ColorFlux`属性使箭头的颜色在敌人靠近时更红，在敌人更远时更白。\n\n这个类定义的最后四行是类函数。有一个构造函数，一个将定位器状态设置为激活的函数，`Move`和`Render`函数:\n\n```cpp\n        Locator();\n        void SetActive();\n        void Move();\n        void Render();\n```\n\n# 摄像机类别定义\n\n我们现在需要添加新的`Camera`类定义。这个类将用于定义我们的`viewport`和我们的摄像机的位置。每一帧都会调用`Move`功能。最初，`Move`会锁定我们玩家的位置，在关卡周围跟随。稍后，我们将更改此功能以创建更动态的相机。这就是`Camera`班的样子:\n\n```cpp\nclass Camera {\n    public:\n        Vector2D m_Position;\n        float m_HalfWidth;\n        float m_HalfHeight;\n\n        Camera( float width, float height );\n        void Move();\n};\n```\n\n# 渲染管理器类定义\n\n一直以来，我们都是在没有背景的情况下在自己的水平上移动。这在前面几章中很好，我们的关卡正好适合画布元素。然而，现在我们正在用相机滚动我们的水平。如果背景中没有任何东西在移动，很难判断你的飞船是否在移动。为了在我们的游戏中创建运动的错觉，我们需要添加一个背景渲染器。除此之外，我们希望游戏中的所有渲染都使用我们刚刚创建的相机作为偏移来完成。正因为如此，我们不再希望我们的游戏对象直接调用`SDL_RenderCopy`或者`SDL_RenderCopyEx`。相反，我们创建了一个`RenderManager`类，负责在我们的游戏中执行渲染。我们有一个`RenderBackground`功能，将渲染一个星空作为背景，我们创建了一个`Render`功能，将渲染我们的雪碧纹理使用相机作为偏移。这就是`RenderManager`类定义的样子:\n\n```cpp\nclass RenderManager {\n    public:\n        const int c_BackgroundWidth = 800;\n        const int c_BackgroundHeight = 600;\n        SDL_Texture *m_BackgroundTexture;\n        SDL_Rect m_BackgroundDest = {.x = 0, .y = 0, .w = \n        c_BackgroundWidth, .h = c_BackgroundHeight };\n\n        RenderManager();\n        void RenderBackground();\n        void Render( SDL_Texture *tex, SDL_Rect *src, SDL_Rect *dest, float \n        rad_rotation = 0.0, int alpha = 255, int red = 255, int green = \n        255, int blue = 255 );\n};\n```\n\n我们在`game.hpp`文件中需要做的最后一件事是创建一个到两个新的`Camera`和`RenderManager`类型的对象指针的外部链接。这些是我们将在这个版本的游戏引擎中使用的相机和渲染管理器对象，并且是我们将在`main.cpp`文件中定义的变量的外部引用:\n\n```cpp\nextern Camera* camera;\nextern RenderManager* render_manager;\nextern Locator* locator;\n```\n\n# camera.cpp 文件\n\n我们在`Camera`类中定义了两个函数；我们的`camera`对象和`Move`函数的构造函数，我们将使用它来跟踪我们的`player`对象。以下是我们在`camera.cpp`文件中的内容:\n\n```cpp\n#include \"game.hpp\"\nCamera::Camera( float width, float height ) {\n    m_HalfWidth = width / 2;\n    m_HalfHeight = height / 2;\n}\n\nvoid Camera::Move() {\n    m_Position = player->m_Position;\n    m_Position.x -= CANVAS_WIDTH / 2;\n    m_Position.y -= CANVAS_HEIGHT / 2;\n}\n```\n\n在这个实现中，`Camera`构造函数和`Move`函数是非常简单的。构造函数根据传入的宽度和高度设置摄像机的半宽半高。`Move`功能将摄像机的位置设置为玩家的位置，然后将摄像机的位置移动画布宽度和画布高度的一半，使玩家居中。我们刚刚构建了一个入门相机，并将在本章后面的内容中添加更多功能。\n\n# render_manager.cpp 文件\n\n我们将把所有我们用来渲染对象内部精灵的调用转移到`RenderManager`类。我们需要这样做，因为我们将使用我们的相机的位置来决定我们将在画布上的哪里渲染精灵。我们还需要一个功能来渲染我们的背景星域。我们的`render_manager.cpp`文件的前几行将包括`game.hpp`文件，并定义我们的背景图像的虚拟文件系统位置:\n\n```cpp\n#include \"game.hpp\"\n#define BACKGROUND_SPRITE_FILE (char*)\"/sprites/starfield.png\"\n```\n\n之后，我们将定义我们的构造函数。构造函数将用于加载我们的`starfield.png`文件作为`SDL_Surface`对象，然后将使用该表面创建一个`SDL_Texture`对象，我们将使用它来渲染我们的背景:\n\n```cpp\nRenderManager::RenderManager() {\n    SDL_Surface *temp_surface = IMG_Load( BACKGROUND_SPRITE_FILE );\n\n    if( !temp_surface ) {\n        printf(\"failed to load image: %s\\n\", IMG_GetError() );\n        return;\n    }\n\n    m_BackgroundTexture = SDL_CreateTextureFromSurface( renderer, \n    temp_surface );\n\n    if( !m_BackgroundTexture ) {\n        printf(\"failed to create texture: %s\\n\", IMG_GetError() );\n        return;\n    }\n    SDL_FreeSurface( temp_surface );\n}\n```\n\n`RenderBackground`函数需要在我们在`main`循环中定义的`render()`函数的开头调用。因此，`RenderBackground`的前两行将有两个函数，我们将使用这两个函数将先前从`main.cpp`中的`render()`函数调用的渲染器清除为黑色:\n\n```cpp\nSDL_SetRenderDrawColor( renderer, 0, 0, 0, 255 );\nSDL_RenderClear( renderer );\n```\n\n之后，我们将设置一个背景矩形作为渲染目的地。`starfield.png`的大小与我们的画布大小(800 x 600)匹配，所以我们需要根据相机的位置渲染四次。因为这是一个重复的纹理，所以我们可以在相机的位置上使用模运算符(`%`)来计算我们想要如何偏移 starfield。举个例子，如果我们把相机放在`*x* = 100`、`*y* = 200`上，我们会想要在`-100`、`-200`上渲染我们的星际背景的第一个副本。如果我们停在那里，右边会有 100 像素的黑色空间，画布底部会有 200 像素的黑色空间。因为我们想在这些领域的背景，我们将需要三个额外的渲染我们的背景。如果我们在`700`、`-200`第二次渲染我们的背景(将画布宽度添加到先前渲染的 *x* 值)，我们现在将在画布底部有一个 200 像素的黑色条带。然后，我们可以在`-100`、`400`处渲染我们的星域(将画布高度添加到原始渲染的 *y* 值中)。这将使我们在底部角落有一个 100 x 200 像素的黑色。第四个渲染需要将画布宽度和画布高度添加到原始渲染的 *x* 和 *y* 值中，以填充该角落。这就是在`RenderBackground`功能中正在发生的事情，我们使用该功能根据摄像机的位置将重复的背景渲染到画布上:\n\n```cpp\nvoid RenderManager::RenderBackground() {\n    SDL_SetRenderDrawColor( renderer, 0, 0, 0, 255 );\n    SDL_RenderClear( renderer );\n    SDL_Rect background_rect = {.x = 0, .y=0, .w=CANVAS_WIDTH, \n                                .h=CANVAS_HEIGHT};\n    int start_x = (int)(camera->m_Position.x) % CANVAS_WIDTH;\n    int start_y = (int)(camera->m_Position.y) % CANVAS_HEIGHT;\n    background_rect.x -= start_x;\n    background_rect.y -= start_y;\n    SDL_RenderCopy( renderer, m_BackgroundTexture, NULL, \n                    &background_rect );\n    background_rect.x += CANVAS_WIDTH;\n    SDL_RenderCopy( renderer, m_BackgroundTexture, NULL, \n                    &background_rect );\n    background_rect.x -= CANVAS_WIDTH;\n    background_rect.y += CANVAS_HEIGHT;\n    SDL_RenderCopy( renderer, m_BackgroundTexture, NULL, \n                    &background_rect );\n    background_rect.x += CANVAS_WIDTH;\n    SDL_RenderCopy( renderer, m_BackgroundTexture, NULL, \n                    &background_rect );\n }\n```\n\n我们在`render_manager.cpp`中定义的最后一个函数是我们的`Render`函数。在定义了这个函数之后，我们需要找到我们之前在代码中调用过`SDL_RenderCopy`和`SDL_RenderCopyEx`的每个地方，并用对渲染管理器的`Render`函数的调用来替换这些调用。这个功能不仅会根据我们相机的位置渲染我们的精灵，还会用来设置颜色和 alpha 通道的修改。以下是`Render`功能的全部代码:\n\n```cpp\n\nvoid RenderManager::Render( SDL_Texture *tex, SDL_Rect *src, SDL_Rect *dest, float rad_rotation,int alpha, int red, int green, int blue ) {\n\n    SDL_Rect camera_dest = *dest;\n    if( camera_dest.x <= CANVAS_WIDTH &&\n        camera->m_Position.x >= LEVEL_WIDTH - CANVAS_WIDTH ) {\n        camera_dest.x += (float)LEVEL_WIDTH;\n    }\n    else if( camera_dest.x >= LEVEL_WIDTH - CANVAS_WIDTH &&\n             camera->m_Position.x <= CANVAS_WIDTH ) {\n             camera_dest.x -= (float)LEVEL_WIDTH;\n    }\n    if( camera_dest.y <= CANVAS_HEIGHT &&\n        camera->m_Position.y >= LEVEL_HEIGHT - CANVAS_HEIGHT ) {\n        camera_dest.y += (float)LEVEL_HEIGHT;\n    }\n    else if( camera_dest.y >= LEVEL_HEIGHT - CANVAS_HEIGHT &&\n             camera->m_Position.y <= CANVAS_HEIGHT ) {\n             camera_dest.y -= (float)LEVEL_HEIGHT;\n    }\n    camera_dest.x -= (int)camera->m_Position.x;\n    camera_dest.y -= (int)camera->m_Position.y;\n\n    SDL_SetTextureAlphaMod(tex,\n                           (Uint8)alpha );\n\n    SDL_SetTextureColorMod(tex,\n                            (Uint8)red,\n                            (Uint8)green,\n                            (Uint8)blue );\n\n    if( rad_rotation != 0.0 ) {\n        float degree_rotation = RAD_TO_DEG(rad_rotation);\n        SDL_RenderCopyEx( renderer, tex, src, &camera_dest,\n                          degree_rotation, NULL, SDL_FLIP_NONE );\n    }\n    else {\n        SDL_RenderCopy( renderer, tex, src, &camera_dest );\n    }\n}\n```\n\n这个函数做的第一件事是创建一个新的`SDL_Rect`对象，我们将使用它来修改传递给`Render`函数的`dest`变量中的值。因为我们有一个包裹 *x* 和 *y* 坐标的关卡，如果我们在关卡的右边，我们会想要将关卡最左边的物体渲染到右边。同样，如果我们在我们级别的最左侧，我们将希望将位于我们级别最右侧的对象渲染到我们的右侧。这使得我们的宇宙飞船可以从我们水平的左侧循环回到我们水平的右侧，反之亦然。以下是调整相机位置以将对象环绕到关卡左侧和右侧的代码:\n\n```cpp\nif( camera_dest.x <= CANVAS_WIDTH &&\n    camera->m_Position.x >= LEVEL_WIDTH - CANVAS_WIDTH ) {\n    camera_dest.x += (float)LEVEL_WIDTH;\n}\nelse if( camera_dest.x >= LEVEL_WIDTH - CANVAS_WIDTH &&\n         camera->m_Position.x <= CANVAS_WIDTH ) {\n    camera_dest.x -= (float)LEVEL_WIDTH;\n}\n```\n\n完成此操作后，我们将做一些类似的事情，以允许在我们级别的顶部和底部包装对象的位置:\n\n```cpp\nif( camera_dest.y <= CANVAS_HEIGHT &&\n    camera->m_Position.y >= LEVEL_HEIGHT - CANVAS_HEIGHT ) {\n    camera_dest.y += (float)LEVEL_HEIGHT;\n}\nelse if( camera_dest.y >= LEVEL_HEIGHT - CANVAS_HEIGHT &&\n         camera->m_Position.y <= CANVAS_HEIGHT ) {\n    camera_dest.y -= (float)LEVEL_HEIGHT;\n}\n```\n\n接下来，我们需要从`camera_dest` *x* 和 *y* 坐标中减去摄像机的位置，并设置我们的`alpha`和`color` mod 的值:\n\n```cpp\ncamera_dest.x -= (int)camera->m_Position.x;\ncamera_dest.y -= (int)camera->m_Position.y;\nSDL_SetTextureAlphaMod(tex,\n                        (Uint8)alpha );\n\nSDL_SetTextureColorMod(tex,\n                       (Uint8)red,\n                       (Uint8)green,\n                       (Uint8)blue );\n```\n\n在函数的末尾，如果我们的精灵旋转了，我们将调用`SDL_RenderCopyEx`，如果没有旋转，我们将调用`SDL_RenderCopy`:\n\n```cpp\nif( rad_rotation != 0.0 ) {\n    float degree_rotation = RAD_TO_DEG(rad_rotation);\n    SDL_RenderCopyEx( renderer, tex, src, &camera_dest,\n                      degree_rotation, NULL, SDL_FLIP_NONE );\n}\nelse {\n    SDL_RenderCopy( renderer, tex, src, &camera_dest );\n}\n```\n\n# 修改 main.cpp\n\n为了实现我们的相机，我们需要对我们的`main.cpp`文件进行几次修改。我们需要为我们的相机、渲染管理器和定位器添加一些新的全局变量。我们将需要修改我们的`move`功能，以包括移动我们的相机和定位器的调用。我们将修改我们的`render`功能来渲染我们的背景和定位器。最后，我们需要给我们的`main`函数添加更多的初始化代码。\n\n# 新的全局变量\n\n我们需要在我们的`main.cpp`文件的开头附近创建三个新的全局变量。我们需要指向`RenderManager`、`Camera`和`Locator`的对象指针。这些声明是这样的:\n\n```cpp\nCamera* camera;\nRenderManager* render_manager;\nLocator* locator;\n```\n\n# 修改移动功能\n\n我们需要修改我们的`move`功能来移动我们的相机和定位器对象。我们需要在`move`函数的末尾添加以下两行:\n\n```cpp\n camera->Move();\n locator->Move();\n```\n\n以下是整个`move`功能:\n\n```cpp\nvoid move() {\n    player->Move();\n    enemy->Move();\n    projectile_pool->MoveProjectiles();\n    Asteroid* asteroid;\n    std::vector<Asteroid*>::iterator it;\n    int i = 0;\n\n    for( it = asteroid_list.begin(); it != asteroid_list.end(); it++ ) {\n        asteroid = *it;\n        if( asteroid->m_Active ) {\n            asteroid->Move();\n        }\n    }\n    star->Move();\n    camera->Move();\n    locator->Move();\n}\n```\n\n# 修改渲染功能\n\n我们将在`render`函数的最开始添加一行。这条线将渲染背景星空并根据摄像机位置移动它:\n\n```cpp\n render_manager->RenderBackground();\n```\n\n之后，我们需要在`render`函数的末尾添加一行。该行需要在`SDL_RenderPresent`调用之前立即出现，仍然需要是该功能中的最后一行:\n\n```cpp\n locator->Render();\n```\n\n这就是`render()`函数的整体外观:\n\n```cpp\nvoid render() {\n render_manager->RenderBackground();\n    player->Render();\n    enemy->Render();\n    projectile_pool->RenderProjectiles();\n\n    Asteroid* asteroid;\n    std::vector<Asteroid*>::iterator it;\n    for( it = asteroid_list.begin(); it != asteroid_list.end(); it++ ) {\n        asteroid = *it;\n        asteroid->Render();\n    }\n    star->Render();\n locator->Render();\n\n    SDL_RenderPresent( renderer );\n}\n```\n\n# 修改主要功能\n\n最后的修改将是在`main`功能中发生的初始化。我们需要为前面定义的`camera`、`render_manager`和`locator`指针创建新对象:\n\n```cpp\ncamera = new Camera(CANVAS_WIDTH, CANVAS_HEIGHT);\nrender_manager = new RenderManager();\nlocator = new Locator();\n```\n\n在之前的代码版本中，我们有 7 次调用`new Asteroid`并使用`asteroid_list.push_back`将这 7 个新的小行星推入我们的小行星列表。我们现在需要创建比七个小行星多得多的小行星，因此，我们将使用双`for`循环来创建小行星并将其分散到整个游戏区域，而不是单独调用它们。要做到这一点，我们首先需要删除所有早期创建和推送小行星的调用:\n\n```cpp\nasteroid_list.push_back( new Asteroid(\n                            200, 50, 0.05, \n                            DEG_TO_RAD(10) ) );\nasteroid_list.push_back( new Asteroid(\n                            600, 150, 0.03, \n                            DEG_TO_RAD(350) ) );\nasteroid_list.push_back( new Asteroid(\n                            150, 500, 0.05, \n                            DEG_TO_RAD(260) ) );\nasteroid_list.push_back( new Asteroid(\n                            450, 350, 0.01, \n                            DEG_TO_RAD(295) ) );\nasteroid_list.push_back( new Asteroid(\n                            350, 300, 0.08, \n                            DEG_TO_RAD(245) ) );\nasteroid_list.push_back( new Asteroid(\n                            700, 300, 0.09, \n                            DEG_TO_RAD(280) ) );\nasteroid_list.push_back( new Asteroid(\n                            200, 450, 0.03, \n                            DEG_TO_RAD(40) ) );\n```\n\n一旦您删除了前面的所有代码，我们将添加以下代码来创建我们的新小行星，并在整个游戏区域中半随机地分隔它们:\n\n```cpp\nint asteroid_x = 0;\nint asteroid_y = 0;\nint angle = 0;\n\n// SCREEN 1\nfor( int i_y = 0; i_y < 8; i_y++ ) {\n    asteroid_y += 100;\n    asteroid_y += rand() % 400;\n    asteroid_x = 0;\n\n    for( int i_x = 0; i_x < 12; i_x++ ) {\n        asteroid_x += 66;\n        asteroid_x += rand() % 400;\n        int y_save = asteroid_y;\n        asteroid_y += rand() % 400 - 200;\n        angle = rand() % 359;\n        asteroid_list.push_back( new Asteroid(\n                        asteroid_x, asteroid_y,\n                        get_random_float(0.5, 1.0),\n                        DEG_TO_RAD(angle) ) );\n        asteroid_y = y_save;\n    }\n}\n```\n\n# 修改小行星\n\n现在我们正在使用渲染管理器来渲染我们所有的游戏对象，我们将需要遍历我们的各种游戏对象，并修改它们以通过渲染管理器而不是直接渲染。我们要修改的第一个文件是`asteroid.cpp`。在`asteroid.cpp`里面，我们有`Asteroid::Render()`功能。在前几章中，这个函数将通过调用`SDL_RenderCopyEx`直接在 SDL 渲染小行星精灵。现在我们有了我们在`main.cpp`文件中定义的`render_manager`对象，我们将使用该渲染管理器来间接渲染我们的精灵。`RenderManager::Render`功能将使用相机调整画布上渲染精灵的位置。我们需要对`Asteroid::Render()`功能进行的第一个修改是删除以下几行:\n\n```cpp\n SDL_RenderCopyEx( renderer, m_SpriteTexture, \n                   &m_src, &m_dest, \n                   RAD_TO_DEG(m_Rotation), NULL, SDL_FLIP_NONE );\n```\n\n移除对`SDL_RenderCopyEX`的调用后，我们需要在`render_manager`对象中添加对`Render`函数的以下调用:\n\n```cpp\n render_manager->Render( m_SpriteTexture, &m_src, &m_dest, m_Rotation );\n```\n\n新版本的`Asteroid::Render`功能现在将如下所示:\n\n```cpp\nvoid Asteroid::Render() {\n    m_Explode->Move();\n    m_Chunks->Move();\n    if( m_Active == false ) {\n        return;\n    }\n    m_src.x = m_dest.w * m_CurrentFrame;\n    m_dest.x = m_Position.x + m_Radius / 2;\n    m_dest.y = m_Position.y + m_Radius / 2;\n    render_manager->Render( m_SpriteTexture, &m_src, &m_dest, m_Rotation );\n}\n```\n\n# 修改对撞机. cpp\n\n我们需要修改`collider.cpp`文件中的一个功能。先前版本的`WrapPosition`功能检查一个`Collider`物体是否从画布上移到一边或者另一边，如果是这样，该功能将把碰撞器移到另一边。这模仿了经典的雅达利街机游戏*小行星*的行为。在雅达利*小行星*中，如果一颗小行星或玩家的飞船在一侧移出屏幕，那颗小行星(或飞船)就会出现在游戏屏幕的另一侧。以下是我们的`wrap`代码的前一个版本:\n\n```cpp\nvoid Collider::WrapPosition() {\n    if( m_Position.x > CANVAS_WIDTH + m_Radius ) {\n        m_Position.x = -m_Radius;\n    }\n    else if( m_Position.x < -m_Radius ) {\n        m_Position.x = CANVAS_WIDTH;\n    }\n\n    if( m_Position.y > CANVAS_HEIGHT + m_Radius ) {\n        m_Position.y = -m_Radius;\n    }\n    else if( m_Position.y < -m_Radius ) {\n        m_Position.y = CANVAS_HEIGHT;\n    }\n}\n```\n\n因为我们的游戏现在扩展到了单个画布之外，所以如果一个对象离开了画布，我们就不再想要包装了。相反，如果对象超出了级别界限，我们希望将其环绕。以下是新版本的`WrapPosition`功能:\n\n```cpp\nvoid Collider::WrapPosition() {\n    if( m_Position.x > LEVEL_WIDTH ) {\n        m_Position.x -= LEVEL_WIDTH;\n    }\n    else if( m_Position.x < 0 ) {\n        m_Position.x += LEVEL_WIDTH;\n    }\n\n    if( m_Position.y > LEVEL_HEIGHT ) {\n        m_Position.y -= LEVEL_HEIGHT;\n    }\n    else if( m_Position.y < 0 ) {\n        m_Position.y += LEVEL_HEIGHT;\n    }\n}\n```\n\n# 修改敌人 _ship.cpp\n\n有必要对`enemy_ship.cpp`文件进行一个小的修改。`EnemyShip`构造函数现在将在`m_Position`属性上设置`x`和`y`值。我们需要将位置设置为`810`和`800`，因为现在级别比画布大小大很多。我们将在`EnemyShip`构造函数的最顶端设置`m_Position`属性。这是更改后构造函数的开头:\n\n```cpp\nEnemyShip::EnemyShip() {\n    m_Position.x = 810.0;\n    m_Position.y = 800.0;\n```\n\n# 修改有限状态机\n\n我们需要对`finite_state_machine.cpp`文件进行一个小的修改。在`FiniteStateMachine::AvoidForce()`功能中，有几个对画布尺寸的引用，现在我们的级别大小和画布大小不同，必须更改这些引用才能引用级别尺寸。之前，我们已经将`star_avoid`变量的`x`和`y`属性设置为以下基于画布的值:\n\n```cpp\nstar_avoid.x = CANVAS_WIDTH / 2;\nstar_avoid.y = CANVAS_HEIGHT / 2;\n```\n\n这些线必须改为参考`LEVEL_WIDTH`和`LEVEL_HEIGHT`:\n\n```cpp\nstar_avoid.x = LEVEL_WIDTH / 2;\nstar_avoid.y = LEVEL_HEIGHT / 2;\n```\n\n我们必须对`avoid_vec`变量做同样的事情。以下是我们之前的内容:\n\n```cpp\navoid_vec.x = CANVAS_WIDTH / 2;\navoid_vec.y = CANVAS_HEIGHT / 2;\n```\n\n也必须改为参考`LEVEL_WIDTH`和`LEVEL_HEIGHT`:\n\n```cpp\navoid_vec.x = LEVEL_WIDTH / 2;\navoid_vec.y = LEVEL_HEIGHT / 2;\n```\n\n`FiniteState::AvoidForce`功能的新版本整体如下:\n\n```cpp\nvoid FiniteStateMachine::AvoidForce() {\n    Vector2D start_corner;\n    Vector2D end_corner;\n    Vector2D avoid_vec;\n    Vector2D dist;\n    float closest_square = 999999999999.0;\n    float msq;\n    Vector2D star_avoid;\n star_avoid.x = LEVEL_WIDTH / 2;\n star_avoid.y = LEVEL_HEIGHT / 2;\n    star_avoid -= m_Ship->m_Position;\n    msq = star_avoid.MagSQ();\n\n    if( msq >= c_StarAvoidDistSQ ) {\n        start_corner = m_Ship->m_Position;\n        start_corner.x -= c_AvoidDist;\n        start_corner.y -= c_AvoidDist;\n        end_corner = m_Ship->m_Position;\n        end_corner.x += c_AvoidDist;\n        end_corner.y += c_AvoidDist;\n\n        Asteroid* asteroid;\n        std::vector<Asteroid*>::iterator it;\n\n        int i = 0;\n        for( it = asteroid_list.begin(); it != asteroid_list.end(); it++ ) {\n            asteroid = *it;\n            if( asteroid->m_Active == true &&\n                asteroid->SteeringRectTest( start_corner, end_corner ) ) {\n                dist = asteroid->m_Position;\n                dist -= m_Ship->m_Position;\n                msq = dist.MagSQ();\n\n                if( msq <= closest_square ) {\n                    closest_square = msq;\n                    avoid_vec = asteroid->m_Position;\n                }\n            }\n        }\n        // LOOP OVER PROJECTILES\n        Projectile* projectile;\n        std::vector<Projectile*>::iterator proj_it;\n\n        for( proj_it = projectile_pool->m_ProjectileList.begin(); \n             proj_it != projectile_pool->m_ProjectileList.end(); proj_it++ ) {\n            projectile = *proj_it;\n            if( projectile->m_Active == true &&\n                projectile->SteeringRectTest( start_corner, end_corner ) ) {\n                dist = projectile->m_Position;\n                dist -= m_Ship->m_Position;\n                msq = dist.MagSQ();\n\n                if( msq <= closest_square ) {\n                    closest_square = msq;\n                    avoid_vec = projectile->m_Position;\n                }\n            }\n        }\n        if( closest_square != 999999999999.0 ) {\n            avoid_vec -= m_Ship->m_Position;\n            avoid_vec.Normalize();\n            float rot_to_obj = avoid_vec.FindAngle();\n\n            if( std::abs( rot_to_obj - m_Ship->m_Rotation ) < 0.75 ) {\n                if( rot_to_obj >= m_Ship->m_Rotation ) {\n                    m_Ship->RotateLeft();\n                }\n                else {\n                    m_Ship->RotateRight();\n                }\n            }\n            m_Ship->m_Velocity -= avoid_vec * delta_time * \n            c_ObstacleAvoidForce;\n        }\n    }\n    else {\n        avoid_vec.x = LEVEL_WIDTH / 2;\n avoid_vec.y = LEVEL_HEIGHT / 2;\n        avoid_vec -= m_Ship->m_Position;\n        avoid_vec.Normalize();\n        float rot_to_obj = avoid_vec.FindAngle();\n        if( std::abs( rot_to_obj - m_Ship->m_Rotation ) < 0.75 ) {\n            if( rot_to_obj >= m_Ship->m_Rotation ) {\n                m_Ship->RotateLeft();\n            }\n            else {\n                m_Ship->RotateRight();\n            }\n        }\n        m_Ship->m_Velocity -= avoid_vec * delta_time * c_StarAvoidForce; \n    }\n}\n```\n\n# 修饰粒子\n\n我们需要修改`particle.cpp`文件中的`Render`函数，通过`render_manager`渲染粒子，而不是直接通过调用 SDL。`Particle::Render`功能的旧版本如下:\n\n```cpp\nvoid Particle::Render() {\n    SDL_SetTextureAlphaMod(m_sprite_texture,\n                            (Uint8)m_alpha );\n\n    if( m_color_mod == true ) {\n        SDL_SetTextureColorMod(m_sprite_texture,\n                                m_current_red,\n                                m_current_green,\n                                m_current_blue );\n    }\n\n    if( m_align_rotation == true ) {\n        SDL_RenderCopyEx( renderer, m_sprite_texture, &m_src, &m_dest, \n                            m_rotation, NULL, SDL_FLIP_NONE );\n    }\n    else {\n        SDL_RenderCopy( renderer, m_sprite_texture, &m_src, &m_dest );\n    }\n}\n```\n\n新的`Particle::Render`函数将通过`render_manager`对象对`Render`函数进行一次调用:\n\n```cpp\nvoid Particle::Render() {\n render_manager->Render( m_sprite_texture, &m_src, &m_dest, m_rotation,\n m_alpha, m_current_red, m_current_green, m_current_blue );\n}\n```\n\n# 正在修改 player_ship.cpp\n\n我们需要对`player_ship.cpp`文件进行一个小的修改。就像我们对`enemy_ship.cpp`文件所做的更改一样，我们需要添加两行来设置`m_Position`属性中的`x`和`y`值。\n\n我们需要删除`PlayerShip::PlayerShip()`构造函数的前两行:\n\n```cpp\nm_Position.x = CANVAS_WIDTH - 210.0;\nm_Position.y = CANVAS_HEIGHT - 200.0;\n```\n\n这些是我们需要对`PlayerShip::PlayerShip()`构造函数进行的更改:\n\n```cpp\nPlayerShip::PlayerShip() {\n m_Position.x = LEVEL_WIDTH - 810.0;\n m_Position.y = LEVEL_HEIGHT - 800.0;\n```\n\n# 修正抛射体\n\n我们需要对`projectile.cpp`文件进行一个小的修改。与其他游戏对象一样，`Render`函数先前直接调用 SDL 函数来渲染游戏对象。我们需要通过`render_manager`对象打电话，而不是打给 SDL。我们需要从`Projectile::Render()`功能中删除以下行:\n\n```cpp\nint return_val = SDL_RenderCopy( renderer, m_SpriteTexture, \n                                 &src, &dest );\nif( return_val != 0 ) {\n    printf(\"SDL_Init failed: %s\\n\", SDL_GetError());\n}\n```\n\n代替这些行，我们需要添加对`render_manager`对象上的`Render`函数的调用:\n\n```cpp\n render_manager->Render( m_SpriteTexture, &src, &dest );\n```\n\n这就是新版本的`Projectile::Render()`功能的样子:\n\n```cpp\nvoid Projectile::Render() {\n    dest.x = m_Position.x + 8;\n    dest.y = m_Position.y + 8;\n    dest.w = c_Width;\n    dest.h = c_Height;\n\n    src.x = 16 * m_CurrentFrame;\n\n render_manager->Render( m_SpriteTexture, &src, &dest );\n}\n```\n\n# 修改 shield.cpp\n\n与许多其他游戏对象一样，`Shield::Render()`函数将需要修改，以便它不再直接调用 SDL，而是从`render_manager`对象调用`Render`函数。在`Shield::Render()`功能中，我们需要删除对 SDL 的以下呼叫:\n\n```cpp\nSDL_SetTextureColorMod(m_SpriteTexture,\n                        color_red,\n                        color_green,\n                        0 );\n\nSDL_RenderCopyEx( renderer, m_SpriteTexture, \n                    &m_src, &m_dest, \n                    RAD_TO_DEG(m_Ship->m_Rotation), \n                    NULL, SDL_FLIP_NONE );\n```\n\n我们将用对`Render`的一次呼叫来替换这些线路:\n\n```cpp\nrender_manager->Render( m_SpriteTexture, &m_src, &m_dest, m_Ship->m_Rotation,\n                        255, color_red, color_green, 0 );\n```\n\n这就是新版本的`Shield::Render`功能的整体外观:\n\n```cpp\nvoid Shield::Render() {\n    if( m_Active ) {\n        int color_green = m_ttl / 100 + 1;\n        int color_red = 255 - color_green;\n\n        m_src.x = m_CurrentFrame * m_dest.w;\n\n        m_dest.x = m_Ship->m_Position.x;\n        m_dest.y = m_Ship->m_Position.y;\n render_manager->Render( m_SpriteTexture, &m_src, &m_dest, m_Ship->m_Rotation,\n 255, color_red, color_green, 0 );\n    }\n}\n```\n\n# 修改 ship.cpp\n\n在我们的游戏对象中修改`Render`功能变得非常常规。与我们修改了`Render`功能的其他对象一样，我们需要删除所有到 SDL 的直接呼叫。以下是我们需要从`Render`功能中删除的代码:\n\n```cpp\nfloat degrees = (m_Rotation / PI) * 180.0;\nint return_code = SDL_RenderCopyEx( renderer, m_SpriteTexture, \n                                    &src, &dest, \n                                    degrees, NULL, SDL_FLIP_NONE );\nif( return_code != 0 ) {\n    printf(\"failed to render image: %s\\n\", IMG_GetError() );\n}\n```\n\n删除这些行后，我们需要添加一行来调用`render_manager->Render`函数:\n\n```cpp\n render_manager->Render( m_SpriteTexture, &src, &dest, m_Rotation );\n```\n\n# 正在修改 star.cpp\n\n我们需要修改`star.cpp`文件中的两个函数。首先，我们需要在`Star::Star()`构造函数中修改星的位置。在上一章的`Star`构造函数版本中，我们将星星的位置设置在画布的中间。现在，它必须被设置到级别的中间。以下是构造函数原始版本中的行:\n\n```cpp\nm_Position.x = CANVAS_WIDTH / 2;\nm_Position.y = CANVAS_HEIGHT / 2;\n```\n\n我们现在将这些更改为相对于`LEVEL_WIDTH`和`LEVEL_HEIGHT`的位置，而不是相对于`CANVAS_WIDTH`和`CANVAS_HEIGHT`的位置:\n\n```cpp\nm_Position.x = LEVEL_WIDTH / 2;\nm_Position.y = LEVEL_HEIGHT / 2;\n```\n\n在对`Star::Star`构造函数进行上述更改后，我们需要对`Star::Render`函数进行更改。我们需要删除对`SDL_RenderCopy`的调用，并替换为对`render_manager`对象上的`Render`函数的调用。这就是之前版本的`Render`功能的样子:\n\n```cpp\nvoid Star::Render() {\n    Emitter* flare;\n    std::vector<Emitter*>::iterator it;\n    for( it = m_FlareList.begin(); it != m_FlareList.end(); it++ ) {\n        flare = *it;\n        flare->Move();\n    }\n    m_src.x = m_dest.w * m_CurrentFrame;\n    SDL_RenderCopy( renderer, m_SpriteTexture, \n                    &m_src, &m_dest );\n}\n```\n\n我们将修改如下:\n\n```cpp\nvoid Star::Render() {\n    Emitter* flare;\n    std::vector<Emitter*>::iterator it;\n    for( it = m_FlareList.begin(); it != m_FlareList.end(); it++ ) {\n        flare = *it;\n        flare->Move();\n    }\n    m_src.x = m_dest.w * m_CurrentFrame;\n    render_manager->Render( m_SpriteTexture, &m_src, &m_dest );\n}\n```\n\n# 修改 vector.cpp\n\n我们需要在`Vector2D`类中添加两个新的重载操作符。我们需要超越`operator-`和`operator+`。这段代码非常简单。它将使用已经超载的`operator-=`和`operator+=`来允许我们互相加减向量。下面是这些重载操作符的新代码:\n\n```cpp\nVector2D Vector2D::operator-(const Vector2D &vec) {\n Vector2D return_vec = *this;\n return_vec -= vec;\n return return_vec;\n}\n\nVector2D Vector2D::operator+(const Vector2D &vec) {\n Vector2D return_vec = *this;\n return_vec += vec;\n return return_vec;\n}\n```\n\n# 使用锁定的摄像头进行编辑和播放\n\n如果我们编译并测试我们现在拥有的东西，我们应该能够在我们的水平周围移动，并看到一个直接跟踪玩家位置的摄像机。我们应该有一个定位箭头来帮助我们找到敌人的飞船。下面是对 Emscripten 的命令行调用，我们可以用它来构建我们的项目:\n\n```cpp\nem++ asteroid.cpp camera.cpp collider.cpp emitter.cpp enemy_ship.cpp finite_state_machine.cpp locator.cpp main.cpp particle.cpp player_ship.cpp projectile_pool.cpp projectile.cpp range.cpp render_manager.cpp shield.cpp ship.cpp star.cpp vector.cpp -o index.html --preload-file sprites -std=c++ 17 -s USE_WEBGL2=1 -s USE_SDL=2 -s USE_SDL_IMAGE=2 -s SDL2_IMAGE_FORMATS=[\"png\"] -s USE_SDL_IMAGE=2 -s SDL2_IMAGE_FORMATS=[\"png\"] \n```\n\n在 Windows 或 Linux 命令提示符下运行前一行。运行此程序后，从网络服务器提供`index.html`文件，并在浏览器(如 Chrome 或 Firefox)中打开它。\n\n# 更先进的照相机\n\n我们现在的相机是功能性的，但是有点无聊。它只关注玩家，这没什么问题，但可以显著改进。首先，正如 *Defender* 的设计者所意识到的，更重要的是将摄像头的焦点放在玩家移动的方向，而不是直接对准玩家。为了实现这一点，我们将添加*投影焦点*到我们的相机。这将会看到玩家飞船的当前速度，并且会在这个速度的方向上向前移动相机。然而，有时你可能仍然希望你的相机的焦点在播放器后面。为了对此有所帮助，我们将添加一些相机吸引器。照相机吸引器是将照相机的注意力吸引到它们身上的物体。如果敌人出现在玩家身后，可能更重要的是稍微向后移动相机，以帮助将敌人保持在屏幕上。如果敌人正在向你射击，将摄像机对准向你飞来的射弹可能更重要。\n\n# games.hpp 的更改\n\n我们需要做的第一个改变是我们的`games.hpp`文件。让摄像机跟着我们的玩家很容易。相机没有任何啪嗒声或震动，因为玩家的船不会那样移动。如果我们打算使用更高级的功能，如吸引子和前焦点，我们将需要计算相机的期望位置，然后平滑地过渡到该位置。为了支持这一点，我们需要给我们的`Camera`类添加一个`m_DesiredPosition`属性。以下是我们必须添加的新行:\n\n```cpp\n Vector2D m_DesiredPosition;\n```\n\n这就是我们的`games.hpp`文件中的`Camera`类在添加之后的样子:\n\n```cpp\nclass Camera {\n    public:\n        Vector2D m_Position;\n Vector2D m_DesiredPosition;\n\n        float m_HalfWidth;\n        float m_HalfHeight;\n\n        Camera( float width, float height );\n        void Move();\n};\n```\n\n# 更改 camera.cpp\n\n现在我们已经在类定义中添加了一个期望的位置属性，我们需要更改我们的`camera.cpp`文件。我们需要修改构造函数，将摄像机的位置设置为玩家飞船的位置。以下是我们需要添加到构造函数中的行:\n\n```cpp\nm_Position = player->m_Position;\nm_Position.x -= CANVAS_WIDTH / 2;\nm_Position.y -= CANVAS_HEIGHT / 2;\n```\n\n下面是我们添加这些行后的构造函数:\n\n```cpp\nCamera::Camera( float width, float height ) {\n    m_HalfWidth = width / 2;\n    m_HalfHeight = height / 2;\n\n m_Position = player->m_Position;\n m_Position.x -= CANVAS_WIDTH / 2;\n m_Position.y -= CANVAS_HEIGHT / 2;\n}\n```\n\n我们的`Camera::Move`功能将完全不同。你不妨删除当前版本`Camera::Move`中的所有代码行，因为它们都不再有用了。我们新的期望位置属性将在`Move`功能开始时设置，就像之前设置位置一样。为此，在您通过删除该函数中的所有内容而创建的空版本`Camera::Move`中添加以下行:\n\n```cpp\nm_DesiredPosition = player->m_Position;\nm_DesiredPosition.x -= CANVAS_WIDTH / 2;\nm_DesiredPosition.y -= CANVAS_HEIGHT / 2;\n```\n\n如果玩家不在了，我们会希望我们的相机稳定在这个位置。玩家死了之后，我们就不希望任何吸引物影响相机的位置了。玩家死亡后过多移动玩家摄像头看起来有些奇怪，所以添加以下几行代码，检查玩家的飞船是否激活，如果没有，则将摄像头的位置移向想要的位置，然后从`Move`功能返回:\n\n```cpp\nif( player->m_Active == false ) {\n    m_Position.x = m_Position.x + (m_DesiredPosition.x - m_Position.x) \n    * delta_time;\n    m_Position.y = m_Position.y + (m_DesiredPosition.y - m_Position.y) \n    * delta_time;\n    return;\n}\n```\n\n我们将在我们的游戏吸引器中制造所有的主动射弹。如果有敌人向我们射击，这是对我们船只的威胁，因此应该引起摄像机的注意。如果我们发射射弹，这也表明了我们聚焦的方向。我们将使用一个`for`循环来循环我们游戏中的所有投射物，如果那个投射物是活动的，我们将使用它的位置来移动我们相机的期望位置。下面是代码:\n\n```cpp\nProjectile* projectile;\nstd::vector<Projectile*>::iterator it;\nVector2D attractor;\nfor( it = projectile_pool->m_ProjectileList.begin(); it != projectile_pool->m_ProjectileList.end(); it++ ) {\n    projectile = *it;\n    if( projectile->m_Active ) {\n        attractor = projectile->m_Position;\n        attractor -= player->m_Position;\n        attractor.Normalize();\n        attractor *= 5;\n        m_DesiredPosition += attractor;\n    }\n}\n```\n\n在使用我们的吸引器移动相机的期望位置后，我们将根据玩家船只的速度修改`m_DesiredPosition`变量，代码如下:\n\n```cpp\nm_DesiredPosition += player->m_Velocity * 2;\n```\n\n因为我们的关卡是环绕的，如果你从关卡的一边退出，你会出现在另一边，我们需要调整相机的位置来解决这个问题。如果没有以下几行代码，当玩家在一侧移出关卡边界并在另一侧重新出现时，摄像机会突然发出刺耳的声音:\n\n```cpp\nif( abs(m_DesiredPosition.x - m_Position.x) > CANVAS_WIDTH ) {\n    if( m_DesiredPosition.x > m_Position.x ) {\n        m_Position.x += LEVEL_WIDTH;\n    }\n    else {\n        m_Position.x -= LEVEL_WIDTH;\n    }\n}\nif( abs(m_DesiredPosition.y - m_Position.y) > CANVAS_HEIGHT ) {\n    if( m_DesiredPosition.y > m_Position.y ) {\n        m_Position.y += LEVEL_HEIGHT;\n    }\n    else {\n        m_Position.y -= LEVEL_HEIGHT;\n    }\n}\n```\n\n最后，我们将添加几行代码来平滑地将摄像机的当前位置转换到所需位置。我们使用`delta_time`来使这个转换花费大约一秒钟。直接设置我们的相机位置，而不是使用所需的位置和过渡，会导致新的吸引人进入游戏时动作不平稳。下面是过渡代码:\n\n```cpp\nm_Position.x = m_Position.x + (m_DesiredPosition.x - m_Position.x) * \ndelta_time;\nm_Position.y = m_Position.y + (m_DesiredPosition.y - m_Position.y) * \ndelta_time;\n```\n\n现在我们已经分别看到了我们的`Move`函数的所有行，让我们来看看这个函数的完整新版本:\n\n```cpp\nvoid Camera::Move() {\n    m_DesiredPosition = player->m_Position;\n    m_DesiredPosition.x -= CANVAS_WIDTH / 2;\n    m_DesiredPosition.y -= CANVAS_HEIGHT / 2;\n\n    if( player->m_Active == false ) {\n        m_Position.x = m_Position.x + (m_DesiredPosition.x - m_Position.x) \n        * delta_time;\n        m_Position.y = m_Position.y + (m_DesiredPosition.y - m_Position.y) \n        * delta_time;\n        return;\n    }\n\n    Projectile* projectile;\n    std::vector<Projectile*>::iterator it;\n    Vector2D attractor;\n\n    for( it = projectile_pool->m_ProjectileList.begin(); \n        it != projectile_pool->m_ProjectileList.end(); it++ ) {\n        projectile = *it;\n            if( projectile->m_Active ) {\n            attractor = projectile->m_Position;\n            attractor -= player->m_Position;\n            attractor.Normalize();\n            attractor *= 5;\n            m_DesiredPosition += attractor;\n        }\n    }\n    m_DesiredPosition += player->m_Velocity * 2;\n\n    if( abs(m_DesiredPosition.x - m_Position.x) > CANVAS_WIDTH ) {\n        if( m_DesiredPosition.x > m_Position.x ) {\n            m_Position.x += LEVEL_WIDTH;\n        }\n        else {\n            m_Position.x -= LEVEL_WIDTH;\n        }\n    }\n\n    if( abs(m_DesiredPosition.y - m_Position.y) > CANVAS_HEIGHT ) {\n        if( m_DesiredPosition.y > m_Position.y ) {\n            m_Position.y += LEVEL_HEIGHT;\n        }\n        else {\n            m_Position.y -= LEVEL_HEIGHT;\n        }\n    }\n\n    m_Position.x = m_Position.x + (m_DesiredPosition.x - m_Position.x) * \n    delta_time;\n    m_Position.y = m_Position.y + (m_DesiredPosition.y - m_Position.y) * \n    delta_time;\n}\n```\n\n# 使用高级相机编辑和播放\n\n当你建造了这个版本，你会注意到相机在你的船移动的方向前进。如果你开始拍摄，它会走得更远。当敌人的宇宙飞船靠近，并向你射击时，相机也应该向那些射弹的方向漂移。和以前一样，您可以通过在 Windows 或 Linux 命令提示符下输入以下代码来编译和测试代码:\n\n```cpp\nem++ asteroid.cpp camera.cpp collider.cpp emitter.cpp enemy_ship.cpp finite_state_machine.cpp locator.cpp main.cpp particle.cpp player_ship.cpp projectile_pool.cpp projectile.cpp range.cpp render_manager.cpp shield.cpp ship.cpp star.cpp vector.cpp -o camera.html --preload-file sprites -std=c++ 17 -s USE_WEBGL2=1 -s USE_SDL=2 -s USE_SDL_IMAGE=2 -s SDL2_IMAGE_FORMATS=[\"png\"] -s USE_SDL_IMAGE=2 -s SDL2_IMAGE_FORMATS=[\"png\"]\n```\n\n现在我们已经有了我们的应用的编译版本，我们应该运行它。新版本应该如下所示:\n\n![](img/4e7f8642-ae56-40d6-ad9d-4c43bdb3f11c.png)\n\nFigure 11.1: New camera version with lines added to divide the screen\n\n如你所见，相机没有对准玩家飞船的中心。相机的焦点主要投射在玩家船的速度方向，由于敌舰和抛射体的原因，会稍微向右上方拖动。\n\nDo not forget that you must run WebAssembly apps using a web server, or with `emrun`.  If you would like to run your WebAssembly app using `emrun`, you must compile it with the `--emrun` flag.  The web browser requires a web server to stream the WebAssembly module.  If you attempt to open an HTML page that uses WebAssembly in a browser directly from your hard drive, that WebAssembly module will not load.\n\n# 摘要\n\n我们从学习视频游戏中摄像头的历史开始这一章。我们讨论的第一台相机是最简单的相机，有时被称为锁定相机。那是一种能精确跟踪玩家位置的摄像机。之后，我们了解了 2D 太空中锁定摄像头的替代方案，包括引导玩家的摄像头。我们讨论了投影对焦相机，以及它们如何预测玩家的移动，并根据玩家移动的方向向前投影相机的位置。然后，我们讨论了相机吸引子，以及它们如何将相机的焦点吸引到感兴趣的对象上。在讨论了相机的类型后，我们创建了一个相机对象，并将其设计为实现投影焦点和相机吸引器。我们实现了一个渲染管理器，并修改了我们所有的游戏对象来通过`RenderManager`类进行渲染。然后，我们创建了一个`locator`对象，以帮助我们在敌人的宇宙飞船不再出现在画布上时找到它。\n\n在下一章中，我们将学习如何为我们的游戏添加音效。"
  },
  {
    "path": "docs/handson-game-dev-wasm/12.md",
    "content": "# 十二、声音 FX\n\n目前网络上的声音状态有点混乱，已经有一段时间了。很长一段时间以来，根据您使用的浏览器，加载 MP3 和 OGG 文件都存在问题。最近，浏览器在阻止自动播放的声音以防止烦人的音频垃圾方面出现了问题。Chrome 中的这项功能在我们的游戏中播放音频时，有时似乎会产生问题。我注意到，如果 Chrome 最初不播放音频，那么如果你重新加载页面，它通常会播放。我在火狐上没有这个问题。\n\nYou will need to include several images and audio files in your build to make this project work. Make sure that you include the `/Chapter12/sprites/` folder as well as the `/Chapter12/audio/` folder from the project's GitHub. If you haven't yet downloaded the GitHub project, you can get it online at [https://github.com/PacktPublishing/Hands-On-Game-Development-with-WebAssembly](https://github.com/PacktPublishing/Hands-On-Game-Development-with-WebAssembly).\n\nEmscripten 对音频播放的支持没有我希望的那么好。在留言板上，Emscripten 的支持者很快指责网络上的音频状态，而不是 Emscripten 本身，这种评价是有一定道理的。Emscripten 的 FAQ 声称 Emscripten 支持使用 SDL1 音频、SDL2 音频和 OpenAL，但是，根据我的经验，我发现使用非常有限的一组 SDL2 音频可以提供最好的结果。我将尽量少用 SDL2 音频，使用音频队列，而不是混合音效。您可能希望扩展或修改我在这里所做的工作。从理论上讲，OpenAL 应该与 Emscripten 合作，尽管我在这方面运气不太好。另外，您可能希望查看`SDL_MixAudio`()和`SDL_AudioStream`([【https://wiki.libsdl.org/Tutorials/AudioStream】](https://wiki.libsdl.org/Tutorials/AudioStream))来改进游戏中的音频系统，但请注意，网络上流式和混合音频的性能和支持可能还没有为黄金时段做好准备。\n\n我们将在本章中讨论以下主题:\n\n*   哪里可以获得音效\n*   带有 Emscripten 的简单音频\n*   给我们的游戏增加声音\n*   编译和运行\n\n# 哪里可以获得音效\n\n有很多很棒的地方可以在线获得音乐和音效。我用 SFXR([http://www.drpetter.se/project_sfxr.html](http://www.drpetter.se/project_sfxr.html))生成了我们在本章中使用的音效，这是一个用来生成老式 8 位音效的工具，听起来像你在 NES 游戏中听到的东西。这种音效可能不合你的口味。OpenGameArt.org 还有大量的音效([https://opengameart.org/art-search-advanced?keys=&field _ art _ type _ tid % 5B % 5D = 13&sort _ by = count&sort _ order = desc](https://opengameart.org/art-search-advanced?keys=&field_art_type_tid%5B%5D=13&sort_by=count&sort_order=DESC))和音乐([https://opengameart.org/art-search-advanced?keys=&field _ art _ type _ tid % 5B % 5D = 12&sort _ by = count&sort _ order = DESC](https://opengameart.org/art-search-advanced?keys=&field_art_type_tid%5B%5D=12&sort_by=count&sort_order=DESC))以及各种开放的许可证，因此请确保您在该网站上查看任何声音或艺术的许可证\n\n# 带有 Emscripten 的简单音频\n\n在我们给我们的主游戏添加音效之前，我将向您展示如何在`audio.c`文件中制作音频播放器，以演示如何使用 **SDL 音频**在 WebAssembly 应用中播放音效。这个应用将采用五种音效，我们将在我们的游戏中使用，并允许用户按数字键 1 到 5 来播放所有选择的音效。我将首先向您展示分成两个部分的代码，然后我将向您介绍每件事的作用。以下是`audio.c`中除`main`功能外的所有代码:\n\n```cpp\n#include <SDL2/SDL.h>\n#include <emscripten.h>\n#include <stdio.h>\n#include <stdbool.h>\n\n#define ENEMY_LASER \"/audio/enemy-laser.wav\"\n#define PLAYER_LASER \"/audio/player-laser.wav\"\n#define LARGE_EXPLOSION \"/audio/large-explosion.wav\"\n#define SMALL_EXPLOSION \"/audio/small-explosion.wav\"\n#define HIT \"/audio/hit.wav\"\n\nSDL_AudioDeviceID device_id;\nSDL_Window *window;\nSDL_Renderer *renderer;\nSDL_Event event;\n\nstruct audio_clip {\n    char file_name[100];\n    SDL_AudioSpec spec;\n    Uint32 len;\n    Uint8 *buf;\n} enemy_laser_snd, player_laser_snd, small_explosion_snd, large_explosion_snd, hit_snd;\n\nvoid play_audio( struct audio_clip* clip ) {\n    int success = SDL_QueueAudio(device_id, clip->buf, clip->len);\n    if( success < 0 ) {\n        printf(\"SDL_QueueAudio %s failed: %s\\n\", clip->file_name, \n        SDL_GetError());\n    }\n}\n\nvoid init_audio( char* file_name, struct audio_clip* clip ) {\n    strcpy( clip->file_name, file_name );\n\n    if( SDL_LoadWAV(file_name, &(clip->spec), &(clip->buf), &(clip->len)) \n    == NULL ) {\n        printf(\"Failed to load wave file: %s\\n\", SDL_GetError());\n    }\n}\n\nvoid input_loop() {\n    if( SDL_PollEvent( &event ) ){\n        if( event.type == SDL_KEYUP ) {\n            switch( event.key.keysym.sym ){\n                case SDLK_1:\n                    printf(\"one key release\\n\");\n                    play_audio(&enemy_laser_snd);\n                    break;\n                case SDLK_2:\n                    printf(\"two key release\\n\");\n                    play_audio(&player_laser_snd);\n                    break;\n                case SDLK_3:\n                    printf(\"three key release\\n\");\n                    play_audio(&small_explosion_snd);\n                    break;\n                case SDLK_4:\n                    printf(\"four key release\\n\");\n                    play_audio(&large_explosion_snd);\n                    break;\n                case SDLK_5:\n                    printf(\"five key release\\n\");\n                    play_audio(&hit_snd);\n                    break;\n                default:\n                    printf(\"unknown key release\\n\");\n                    break;\n            }\n        }\n    }\n}\n```\n\n在`audio.c`文件的末尾，我们有我们的`main`功能:\n\n```cpp\nint main() {\n    if((SDL_Init(SDL_INIT_VIDEO|SDL_INIT_AUDIO)==-1)) {\n        printf(\"Could not initialize SDL: %s.\\n\", SDL_GetError());\n        return 0;\n    }\n\n    SDL_CreateWindowAndRenderer( 320, 200, 0, &window, &renderer );\n\n    init_audio( ENEMY_LASER, &enemy_laser_snd );\n    init_audio( PLAYER_LASER, &player_laser_snd );\n    init_audio( SMALL_EXPLOSION, &small_explosion_snd );\n    init_audio( LARGE_EXPLOSION, &large_explosion_snd );\n    init_audio( HIT, &hit_snd );\n\n    device_id = SDL_OpenAudioDevice(NULL, 0, &(enemy_laser_snd.spec), \n                                    NULL, 0);\n\n    if (device_id == 0) {\n        printf(\"Failed to open audio: %s\\n\", SDL_GetError());\n    }\n\n    SDL_PauseAudioDevice(device_id, 0);\n\n    emscripten_set_main_loop(input_loop, 0, 0);\n\n    return 1;\n}\n```\n\n现在您已经看到了整个 audio.c 文件，让我们看一下它的所有部分。在这个文件的顶部，我们有我们的`#include`和`#define`宏:\n\n```cpp\n#include <SDL2/SDL.h>\n#include <emscripten.h>\n#include <stdio.h>\n#include <stdbool.h>\n\n#define ENEMY_LASER \"/audio/enemy-laser.wav\"\n#define PLAYER_LASER \"/audio/player-laser.wav\"\n#define LARGE_EXPLOSION \"/audio/large-explosion.wav\"\n#define SMALL_EXPLOSION \"/audio/small-explosion.wav\"\n#define HIT \"/audio/hit.wav\"\n```\n\n之后，我们就有了 SDL 特有的全局变量。我们的音频输出需要一个`SDL_AudioDeviceID`。`SDL_Window`、`SDL_Renderer`和`SDL_Event`在前面的大部分章节中已经使用过，现在应该很熟悉了:\n\n```cpp\nSDL_AudioDeviceID device_id;\nSDL_Window *window;\nSDL_Renderer *renderer;\nSDL_Event event;\n```\n\n我们正在开发一个 C 程序，而不是 C++ 程序，所以我们将使用一个结构来保存我们的音频数据，而不是一个类。我们将创建一个名为`audio_clip`的 C 结构，它将保存我们将在应用中播放的音频的所有信息。这些信息包括一个保存文件名的字符串。它包含一个保存音频规范的`SDL_AudioSpec`对象。它还包含音频片段的长度和指向 8 位数据缓冲区的指针，该缓冲区保存音频片段的波形数据。在`audio_clip`结构被定义之后，该结构的五个实例被创建，我们稍后将能够使用它们来播放这些声音:\n\n```cpp\nstruct audio_clip {\n    char file_name[100];\n    SDL_AudioSpec spec;\n    Uint32 len;\n    Uint8 *buf;\n} enemy_laser_snd, player_laser_snd, small_explosion_snd, large_explosion_snd, hit_snd;\n```\n\n在我们定义`audio_clip`结构之后，我们需要创建一个函数来播放该结构中的音频。这个函数调用`SDL_QueueAudio`传入全局`device_id`，一个指向波形缓冲区的指针，以及片段的长度。`device_id`是音频设备(声卡)的参考。`clip->buf`变量是一个指向缓冲区的指针，该缓冲区包含我们将要加载的`.wav`文件的波形数据。`clip->len`变量包含片段播放的时间长度:\n\n```cpp\nvoid play_audio( struct audio_clip* clip ) {\n    int success = SDL_QueueAudio(device_id, clip->buf, clip->len);\n    if( success < 0 ) {\n        printf(\"SDL_QueueAudio %s failed: %s\\n\", clip->file_name, \n        SDL_GetError());\n    }\n}\n```\n\n我们需要的下一个函数是初始化我们的`audio_clip`的函数，这样我们就可以把它传递给`play_audio`函数。该功能设置我们的`audio_clip`的文件名，并加载一个波形文件设置我们的`audio_clip`中的`spec`、`buf`和`len`值。如果对`SDL_LoadWAV`的调用失败，我们会打印出一条错误消息:\n\n```cpp\nvoid init_audio( char* file_name, struct audio_clip* clip ) {\n    strcpy( clip->file_name, file_name );\n\n    if( SDL_LoadWAV(file_name, &(clip->spec), &(clip->buf), &(clip-\n        >len)) \n    == NULL ) {\n        printf(\"Failed to load wave file: %s\\n\", SDL_GetError());\n    }\n}\n```\n\n现在`input_loop`应该很熟悉了。该函数调用`SDL_PollEvent`并使用它返回的事件来检查键盘键的释放。它检查哪个键被释放。如果该键是从 1 到 5 的数字键之一，则使用 switch 语句调用`play_audio`功能，传递特定的`audio_clip`。我们使用键释放而不是键按压的原因是为了防止用户按住键时重复按键。我们可以很容易地防止这种情况发生，但是我正在努力使这个应用的代码尽可能短。这里是`input_loop`代码:\n\n```cpp\nvoid input_loop() {\n    if( SDL_PollEvent( &event ) ){\n        if( event.type == SDL_KEYUP ) {\n            switch( event.key.keysym.sym ){\n                case SDLK_1:\n                    printf(\"one key release\\n\");\n                    play_audio(&enemy_laser_snd);\n                    break;\n                case SDLK_2:\n                    printf(\"two key release\\n\");\n                    play_audio(&player_laser_snd);\n                    break;\n                case SDLK_3:\n                    printf(\"three key release\\n\");\n                    play_audio(&small_explosion_snd);\n                    break;\n                case SDLK_4:\n                    printf(\"four key release\\n\");\n                    play_audio(&large_explosion_snd);\n                    break;\n                case SDLK_5:\n                    printf(\"five key release\\n\");\n                    play_audio(&hit_snd);\n                    break;\n                default:\n                    printf(\"unknown key release\\n\");\n                    break;\n            }\n        }\n    }\n}\n```\n\n像往常一样，`main`函数为我们的应用完成所有初始化。除了我们在以前的应用中执行的初始化之外，我们还需要一个新的音频初始化。这就是新版本的`main`功能的样子:\n\n```cpp\nint main() {\n    if((SDL_Init(SDL_INIT_VIDEO|SDL_INIT_AUDIO)==-1)) {\n        printf(\"Could not initialize SDL: %s.\\n\", SDL_GetError());\n        return 0;\n    }\n    SDL_CreateWindowAndRenderer( 320, 200, 0, &window, &renderer );\n    init_audio( ENEMY_LASER, &enemy_laser_snd );\n    init_audio( PLAYER_LASER, &player_laser_snd );\n    init_audio( SMALL_EXPLOSION, &small_explosion_snd );\n    init_audio( LARGE_EXPLOSION, &large_explosion_snd );\n    init_audio( HIT, &hit_snd );\n\n    device_id = SDL_OpenAudioDevice(NULL, 0, &(enemy_laser_snd.spec), NULL, \n    0);\n\n    if (device_id == 0) {\n        printf(\"Failed to open audio: %s\\n\", SDL_GetError());\n    }\n    SDL_PauseAudioDevice(device_id, 0);\n    emscripten_set_main_loop(input_loop, 0, 0);\n    return 1;\n}\n```\n\n我们改变的第一件事是我们对`SDL_Init`的呼唤。我们需要添加一个标志，告诉 SDL 初始化音频子系统。我们通过将`|SLD_INIT_AUDIO`添加到我们传入的参数中来实现这一点，该参数对带有`SDL_INIT_AUDIO`标志的参数执行按位运算。在`SDL_Init`的新版本之后，我们将创建窗口和渲染器，在这一点上我们已经做了很多次了。\n\n`init_audio`调用都是新的，并初始化我们的`audio_clip`结构:\n\n```cpp\ninit_audio( ENEMY_LASER, &enemy_laser_snd );\ninit_audio( PLAYER_LASER, &player_laser_snd );\ninit_audio( SMALL_EXPLOSION, &small_explosion_snd );\ninit_audio( LARGE_EXPLOSION, &large_explosion_snd );\ninit_audio( HIT, &hit_snd );\n```\n\n接下来，我们需要调用`SDL_OpenAudioDevice`并检索一个设备 ID。打开音频设备需要一个默认规范，它会通知音频设备您想要播放的声音剪辑的质量。确保你选择的声音文件的质量水平是你想在游戏中玩什么的好例子。在我们的代码中，我们选择了`enemy_laser_snd`。我们还需要称呼`SDL_PauseAudioDevice`。无论何时创建新的音频设备，默认情况下都会暂停。调用`SDL_PauseAudioDevice`并传入`0`作为第二个参数会打开我们刚刚创建的音频设备。一开始我觉得这有点混乱，但请记住，下面对`SDL_PauseAudioDevice`的调用实际上是对音频剪辑的解包:\n\n```cpp\ndevice_id = SDL_OpenAudioDevice(NULL, 0, &(enemy_laser_snd.spec), NULL, 0);\n\nif (device_id == 0) {\n    printf(\"Failed to open audio: %s\\n\", SDL_GetError());\n}\n\nSDL_PauseAudioDevice(device_id, 0);\n```\n\n在返回之前，我们要做的最后一件事是将我们的循环设置为我们之前创建的`input_loop`函数:\n\n```cpp\nemscripten_set_main_loop(input_loop, 0, 0);\n```\n\n既然我们已经有了代码，我们应该编译并测试我们的`audio.c`文件:\n\n```cpp\nemcc audio.c --preload-file audio -s USE_SDL=2 -o audio.html\n```\n\n我们需要预加载音频文件夹，以便能够访问虚拟文件系统中的`.wav`文件。然后，将`audio.html`加载到网络浏览器中，用 emrun 或其他网络服务器提供文件。在 Chrome 中加载应用时，可能会遇到一些小困难。新版本的 Chrome 增加了防止未请求音频播放的检查，以防止一些恼人的垃圾邮件。有时候，这个检查有点太敏感了，这可以阻止我们游戏中的音频运行。如果发生这种情况，请尝试在 Chrome 浏览器中重新加载页面。有时，这可以解决问题。防止这种情况发生的另一种方法是切换到火狐。\n\n# 给我们的游戏增加声音\n\n现在我们已经了解了如何让 SDL 音频在网络上工作，我们可以开始为我们的游戏添加音效了。我们不会在游戏中使用混音器，所以一次只会播放一种音效。正因为如此，我们需要将一些声音归类为**优先**音效。如果触发了优先音效，声音队列将被清除，该音效将运行。我们还想防止我们的声音队列变得太长，所以如果声音队列中有两个以上的项目，我们将清除它。不要害怕！当我们到达代码的那个部分时，我将重复所有这些。\n\n# 更新 game.hpp\n\n我们首先需要改变的是我们的`game.hpp`文件。我们需要添加一个新的`Audio`类，以及其他新的代码来支持我们游戏中的音频。在`game.hpp`文件的顶部附近，我们将添加一系列`#define`宏来定义我们的音效`.wav`文件的位置:\n\n```cpp\n#define ENEMY_LASER (char*)\"/audio/enemy-laser.wav\"\n#define PLAYER_LASER (char*)\"/audio/player-laser.wav\"\n#define LARGE_EXPLOSION (char*)\"/audio/large-explosion.wav\"\n#define SMALL_EXPLOSION (char*)\"/audio/small-explosion.wav\"\n#define HIT (char*)\"/audio/hit.wav\"\n```\n\n在类声明列表的顶部，我们应该添加一个名为`Audio`的类的新声明:\n\n```cpp\nclass Audio;\nclass Ship;\nclass Particle;\nclass Emitter;\nclass Collider;\nclass Asteroid;\nclass Star;\nclass PlayerShip;\nclass EnemyShip;\nclass Projectile;\nclass ProjectilePool;\nclass FiniteStateMachine;\nclass Camera;\nclass RenderManager;\nclass Locator;\n```\n\n然后我们将定义新的`Audio`类，它将非常类似于我们在`audio.c`文件中使用的`audio_clip`结构。这个类将有一个文件名、一个规范、一个长度(在运行时)和一个缓冲区。它还将有一个优先级标志，当设置时，它将优先于当前音频队列中的所有其他内容。最后，我们将在这个类中有两个函数；一个初始化声音的构造函数，一个实际播放声音的`Play`函数。这就是类定义的样子:\n\n```cpp\nclass Audio {\n    public:\n        char FileName[100];\n        SDL_AudioSpec spec;\n        Uint32 len;\n        Uint8 *buf;\n        bool priority = false;\n\n        Audio( char* file_name, bool priority_value );\n        void Play();\n};\n```\n\n最后，我们需要定义一些与全局变量相关的外部音频。这些全局变量将是出现在我们的`main.cpp`文件中的变量的引用。这些大部分是`Audio`类的实例，将在我们的游戏中用来播放音频文件。最后一个变量是对我们音频设备的引用:\n\n```cpp\nextern Audio* enemy_laser_snd;\nextern Audio* player_laser_snd;\nextern Audio* small_explosion_snd;\nextern Audio* large_explosion_snd;\nextern Audio* hit_snd;\nextern SDL_AudioDeviceID device_id;\n```\n\n# 正在更新 main.cpp\n\n我们在`main.cpp`文件中需要做的第一件事是定义音频相关的全局变量，我们在`game.hpp`文件的末尾将其定义为外部变量:\n\n```cpp\nSDL_AudioDeviceID device_id;\n\nAudio* enemy_laser_snd;\nAudio* player_laser_snd;\nAudio* small_explosion_snd;\nAudio* large_explosion_snd;\nAudio* hit_snd;\n```\n\n这些音效大多与我们游戏中发生碰撞时发生的爆炸有关。正因为如此，我们将在整个`collisions`功能中增加调用来播放这些音效。这就是我们新版本的`collisions`功能的样子:\n\n```cpp\nvoid collisions() {\n Asteroid* asteroid;\n std::vector<Asteroid*>::iterator ita;\n    if( player->m_CurrentFrame == 0 && player->CompoundHitTest( star ) ) {\n        player->m_CurrentFrame = 1;\n        player->m_NextFrameTime = ms_per_frame;\n        player->m_Explode->Run(); // added\n        large_explosion_snd->Play();\n    }\n    if( enemy->m_CurrentFrame == 0 && enemy->CompoundHitTest( star ) ) {\n        enemy->m_CurrentFrame = 1;\n        enemy->m_NextFrameTime = ms_per_frame;\n        enemy->m_Explode->Run(); // added\n        large_explosion_snd->Play();\n    }\n    Projectile* projectile;\n    std::vector<Projectile*>::iterator it;\n    for(it=projectile_pool->m_ProjectileList.begin(); \n        it!=projectile_pool->m_ProjectileList.end(); \n        it++){\n        projectile = *it;\n        if( projectile->m_CurrentFrame == 0 && projectile->m_Active ) {\n            for( ita = asteroid_list.begin(); ita != \n                asteroid_list.end(); \n                 ita++ ) {\n                asteroid = *ita;\n                if( asteroid->m_Active ) {\n                    if( asteroid->HitTest( projectile ) ) {\n                        projectile->m_CurrentFrame = 1;\n                        projectile->m_NextFrameTime = ms_per_frame;\n                        small_explosion_snd->Play();\n                    }\n                }\n            }\n            if( projectile->HitTest( star ) ){\n                projectile->m_CurrentFrame = 1;\n                projectile->m_NextFrameTime = ms_per_frame;\n                small_explosion_snd->Play();\n            }\n            else if( player->m_CurrentFrame == 0 && ( projectile-\n                     >HitTest( player ) ||\n                      player->CompoundHitTest( projectile ) ) ) {\n                if( player->m_Shield->m_Active == false ) {\n                    player->m_CurrentFrame = 1;\n                    player->m_NextFrameTime = ms_per_frame;\n                    player->m_Explode->Run();\n                    large_explosion_snd->Play();\n                }\n                else { hit_snd->Play(); }\n                projectile->m_CurrentFrame = 1;\n                projectile->m_NextFrameTime = ms_per_frame;\n            }\n            else if( enemy->m_CurrentFrame == 0 && ( projectile-\n                     >HitTest( enemy ) ||\n                      enemy->CompoundHitTest( projectile ) ) ) {\n                if( enemy->m_Shield->m_Active == false ) {\n                    enemy->m_CurrentFrame = 1;\n                    enemy->m_NextFrameTime = ms_per_frame;\n                    enemy->m_Explode->Run();\n                    large_explosion_snd->Play();\n                }\n                else { hit_snd->Play(); }\n                projectile->m_CurrentFrame = 1;\n                projectile->m_NextFrameTime = ms_per_frame;\n            }\n        }\n    }\n    for( ita = asteroid_list.begin(); ita != asteroid_list.end(); \n         ita++ ) {\n        asteroid = *ita;\n        if( asteroid->m_Active ) {\n            if( asteroid->HitTest( star ) ) {\n                asteroid->Explode();\n                small_explosion_snd->Play();\n            }\n        }\n        else { continue; }\n        if( player->m_CurrentFrame == 0 && asteroid->m_Active &&\n            ( asteroid->HitTest( player ) || player->CompoundHitTest( \n            asteroid ) ) ) {\n            if( player->m_Shield->m_Active == false ) {\n                player->m_CurrentFrame = 1;\n                player->m_NextFrameTime = ms_per_frame;\n                player->m_Explode->Run();\n                large_explosion_snd->Play();\n            }\n            else {\n                asteroid->Explode();\n                small_explosion_snd->Play();\n            }\n        }\n        if( enemy->m_CurrentFrame == 0 && asteroid->m_Active &&\n            ( asteroid->HitTest( enemy ) || enemy->CompoundHitTest( \n              asteroid ) ) ) {\n            if( enemy->m_Shield->m_Active == false ) {\n                enemy->m_CurrentFrame = 1;\n                enemy->m_NextFrameTime = ms_per_frame;\n                enemy->m_Explode->Run();\n                large_explosion_snd->Play();\n            }\n            else {\n                asteroid->Explode();\n                small_explosion_snd->Play();\n            }\n        }\n    }\n}\n```\n\n声音现在会在几次爆炸和碰撞后播放；例如，在玩家爆炸后:\n\n```cpp\nplayer->m_Explode->Run(); \nlarge_explosion_snd->Play();\n```\n\n敌舰爆炸时也会发出声音:\n\n```cpp\nenemy->m_Explode->Run();\nlarge_explosion_snd->Play();\n```\n\n小行星爆炸后，我们会想要同样的效果:\n\n```cpp\nasteroid->Explode();\nsmall_explosion_snd->Play();\n```\n\n如果敌人盾牌被击中，我们想播放`hit`声音:\n\n```cpp\nif( enemy->m_Shield->m_Active == false ) {\n    enemy->m_CurrentFrame = 1;\n    enemy->m_NextFrameTime = ms_per_frame;\n    enemy->m_Explode->Run();\n    large_explosion_snd->Play();\n}\nelse {\n    hit_snd->Play();\n}\n```\n\n同样，如果玩家的护盾被击中，我们将再次想要播放`hit`声音:\n\n```cpp\nif( player->m_Shield->m_Active == false ) {\n    player->m_CurrentFrame = 1;\n    player->m_NextFrameTime = ms_per_frame;\n\n    player->m_Explode->Run();\n    large_explosion_snd->Play();\n}\nelse {\n    hit_snd->Play();\n}\n```\n\n最后，我们需要改变`main`功能来初始化我们的音频。以下是整个`main`功能代码:\n\n```cpp\nint main() {\n    SDL_Init( SDL_INIT_VIDEO | SDL_INIT_AUDIO );\n    int return_val = SDL_CreateWindowAndRenderer( CANVAS_WIDTH, \n    CANVAS_HEIGHT, 0, &window, &renderer );\n\n    if( return_val != 0 ) {\n        printf(\"Error creating renderer %d: %s\\n\", return_val, \n        IMG_GetError() );\n        return 0;\n    }\n\n    SDL_SetRenderDrawColor( renderer, 0, 0, 0, 255 );\n    SDL_RenderClear( renderer );\n    last_frame_time = last_time = SDL_GetTicks();\n\n    player = new PlayerShip();\n    enemy = new EnemyShip();\n    star = new Star();\n    camera = new Camera(CANVAS_WIDTH, CANVAS_HEIGHT);\n    render_manager = new RenderManager();\n    locator = new Locator();\n    enemy_laser_snd = new Audio(ENEMY_LASER, false);\n player_laser_snd = new Audio(PLAYER_LASER, false);\n small_explosion_snd = new Audio(SMALL_EXPLOSION, true);\n large_explosion_snd = new Audio(LARGE_EXPLOSION, true);\n hit_snd = new Audio(HIT, false);\n device_id = SDL_OpenAudioDevice(NULL, 0, &(enemy_laser_snd->spec), \n    NULL, 0);\n\n if (device_id == 0) {\n printf(\"Failed to open audio: %s\\n\", SDL_GetError());\n }\n    int asteroid_x = 0;\n    int asteroid_y = 0;\n    int angle = 0;\n\n    // SCREEN 1\n    for( int i_y = 0; i_y < 8; i_y++ ) {\n        asteroid_y += 100;\n        asteroid_y += rand() % 400;\n        asteroid_x = 0;\n        for( int i_x = 0; i_x < 12; i_x++ ) {\n            asteroid_x += 66;\n            asteroid_x += rand() % 400;\n            int y_save = asteroid_y;\n            asteroid_y += rand() % 400 - 200;\n            angle = rand() % 359;\n            asteroid_list.push_back(\n                new Asteroid( asteroid_x, asteroid_y,\n                get_random_float(0.5, 1.0),\n                DEG_TO_RAD(angle) ) );\n            asteroid_y = y_save;\n        }\n    }\n    projectile_pool = new ProjectilePool();\n    emscripten_set_main_loop(game_loop, 0, 0);\n    return 1;\n}\n```\n\n我们需要对`main`函数进行的第一个更改是对`SDL_Init`调用进行更改，以包括音频子系统的初始化:\n\n```cpp\nSDL_Init( SDL_INIT_VIDEO | SDL_INIT_AUDIO );\n```\n\n我们需要做的另一个改变是增加新的`Audio`对象和对`SDL_OpenAudioDevice`的调用:\n\n```cpp\nenemy_laser_snd = new Audio(ENEMY_LASER, false);\nplayer_laser_snd = new Audio(PLAYER_LASER, false);\nsmall_explosion_snd = new Audio(SMALL_EXPLOSION, true);\nlarge_explosion_snd = new Audio(LARGE_EXPLOSION, true);\nhit_snd = new Audio(HIT, false);\n\ndevice_id = SDL_OpenAudioDevice(NULL, 0, &(enemy_laser_snd->spec), \nNULL, 0);\n\nif (device_id == 0) {\n    printf(\"Failed to open audio: %s\\n\", SDL_GetError());\n}\n```\n\n# 更新 ship.cpp\n\n`ship.cpp`文件有一处小改动。当飞船发射炮弹时，我们正在增加一个播放声音的呼叫。这发生在`Ship::Shoot()`功能中。您会注意到对`player_laser_snd->Play()`的呼叫发生在对`projectile->Launch`的呼叫之后:\n\n```cpp\nvoid Ship::Shoot() {\n     Projectile* projectile;\n     if( current_time - m_LastLaunchTime >= c_MinLaunchTime ) {\n         m_LastLaunchTime = current_time;\n         projectile = projectile_pool->GetFreeProjectile();\n         if( projectile != NULL ) {\n             projectile->Launch( m_Position, m_Direction );\n             player_laser_snd->Play();\n         }\n     }\n }\n```\n\n# 新的 audio.cpp 文件\n\n我们正在添加一个新的`audio.cpp`文件来实现`Audio`类构造函数和`Audio`类`Play`函数。以下是`audio.cpp`文件的全文:\n\n```cpp\n#include \"game.hpp\"\n\nAudio::Audio( char* file_name, bool priority_value ) {\n    strcpy( FileName, file_name );\n    priority = priority_value;\n\n    if( SDL_LoadWAV(FileName, &spec, &buf, &len) == NULL ) {\n        printf(\"Failed to load wave file: %s\\n\", SDL_GetError());\n    }\n}\n\nvoid Audio::Play() {\n    if( priority || SDL_GetQueuedAudioSize(device_id) > 2 ) {\n        SDL_ClearQueuedAudio(device_id);\n    }\n\n    int success = SDL_QueueAudio(device_id, buf, len);\n    if( success < 0 ) {\n        printf(\"SDL_QueueAudio %s failed: %s\\n\", FileName, SDL_GetError());\n    }\n}\n```\n\n这个文件中的第一个函数是`Audio`类的构造函数。该函数将`FileName`属性设置为传递的值，并设置`priority`值。它还从传入的文件名加载波形文件，并使用`SDL_LoadWAV`文件设置`spec`、`buf`和`len`属性。\n\n功能首先查看这是不是高优先级音频，或者音频队列的大小是否大于两个声音。如果出现这两种情况，我们会清除音频队列:\n\n```cpp\nif( priority || SDL_GetQueuedAudioSize(device_id) > 2 ) {\n    SDL_ClearQueuedAudio(device_id);\n}\n```\n\n我们这样做是因为我们不想混合音频。我们正在按顺序播放音频。如果我们有一个优先的音频剪辑，我们希望清除队列，以便音频立即播放。如果队列太长，我们也想这样做。然后我们会呼叫`SDL_QueueAudio`尽快排队播放这个声音:\n\n```cpp\nint success = SDL_QueueAudio(device_id, buf, len);\nif( success < 0 ) {\n printf(\"SDL_QueueAudio %s failed: %s\\n\", FileName, SDL_GetError());\n}\n```\n\n现在，我们应该准备好编译和运行我们的代码了。\n\n# 编译和运行\n\n现在，我们已经对代码进行了所有必要的更改，我们可以使用 Emscripten 编译并运行新代码:\n\n```cpp\nem++ asteroid.cpp audio.cpp camera.cpp collider.cpp emitter.cpp enemy_ship.cpp finite_state_machine.cpp locator.cpp main.cpp particle.cpp player_ship.cpp projectile_pool.cpp projectile.cpp range.cpp render_manager.cpp shield.cpp ship.cpp star.cpp vector.cpp -o sound_fx.html --preload-file audio --preload-file sprites -std=c++ 17 -s USE_WEBGL2=1 -s USE_SDL=2 -s USE_SDL_IMAGE=2 -s SDL2_IMAGE_FORMATS=[\"png\"] -s USE_SDL_IMAGE=2 -s SDL2_IMAGE_FORMATS=[\"png\"] \n```\n\n没有添加新的标志来允许我们使用 SDL 音频库。然而，我们需要添加一个新的`--preload-file audio`标志来将新的`audio`目录加载到我们的虚拟文件系统中。一旦你编译了游戏的新版本，你就可以使用 emrun 运行它了(假设你在编译的时候包含了必要的 emrun 标志)。如果您愿意，您可以选择不同的 web 服务器来提供这些文件。\n\n# 摘要\n\n我们已经讨论了网络上音频的当前(混乱的)状态，并查看了 Emscripten 可用的音频库。我提到了几个可以获得免费音效的地方。我们使用 C 和 Emscripten 创建了一个简单的音频应用，允许我们播放一系列音频文件。然后我们在游戏中加入了音效，包括爆炸和激光声。我们在`main()`函数中修改了我们的初始化代码来初始化 SDL 音频子系统。我们增加了一个新的`Shoot`功能，供我们的宇宙飞船发射炮弹时使用。我们还创建了一个新的`Audio`类来帮助我们播放音频文件。\n\n在下一章中，我们将学习如何在游戏中加入一些物理元素。"
  },
  {
    "path": "docs/handson-game-dev-wasm/13.md",
    "content": "# 十三、游戏物理\n\n我们的游戏中已经有了一些物理知识。我们的每艘船都有速度和加速度。它们也至少遵守一些牛顿定律并保持动量。所有这些都是在没有大张旗鼓的情况下提前添加的。电脑游戏中的物理可以追溯到最初的电脑游戏*太空战！*，也就是启发了我们目前正在写的这款游戏。*太空战原版中！，*宇宙飞船保持动量，就像我们目前在游戏中做的那样。一个黑洞在引力作用下将船只吸引到游戏区的中心。在创作经典游戏 *Pong* 之前，诺兰·布什内尔创作了一个*太空战的街机克隆！*，名为*电脑空间*。*计算机空间*并没有像 *Pong* 那样一炮而红，诺兰·布什内尔将游戏商业失败的部分原因归咎于牛顿定律和公众对基础物理的缺乏理解。\n\nAccording to The Ultimate History of Video Games: from Pong to Pokemon and Beyond, by Steven Kent, \"Computer Space obeys the first law—maintenance of momentum. (Bushnell is probably referring to Sir Isaac Newton's first law—objects maintain constant velocity unless acted upon by an external force.) And so that was really hard for people who didn't understand that.\"\n                                                                                                                    – Nolan Bushnell\n\n物理在游戏中很常见，但远非通用。游戏所需的物理种类高度依赖于游戏的种类。有一个名为*子弹物理*的 3D 物理库已经被移植，但是，因为它是 3D 的，子弹对于我们将在这个游戏中使用的物理种类来说是一个相当大的库。相反，我们将把一些简单的牛顿物理整合到我们的游戏中，以获得一些额外的味道。我们已经在游戏中简单实现了牛顿第一定律。当我们加速我们的宇宙飞船时，它向同一个方向移动，直到我们或者用向下的箭头使它减速，或者我们*翻转并燃烧*使我们的飞船转向并向与我们当前速度相反的方向加速。\n\nYou will need to include several images and audio files in your build to make this project work. Make sure that you include the `/Chapter13/sprites/` folder as well as the `/Chapter13/audio/` folder from the project's GitHub. If you haven't yet downloaded the GitHub project, you can get it online at [https://github.com/PacktPublishing/Hands-On-Game-Development-with-WebAssembly](https://github.com/PacktPublishing/Hands-On-Game-Development-with-WebAssembly).\n\n在本章中，我们将应用物理学的以下方面:\n\n*   小行星、射弹和宇宙飞船之间的弹性碰撞。\n*   当我们的宇宙飞船射击时，应该有后坐力(牛顿第三定律)。\n*   来自恒星的引力应该会吸引玩家的飞船。\n\n# 牛顿第三定律\n\n牛顿第三定律通常被表述为，*对于每一个动作，都有一个相等且相反的反作用力*。这意味着，当物体 *A* 对物体 *B* 施加力时，物体 *B* 对物体 *A* 施加同样的力。这方面的一个例子是用枪发射子弹。当一个拿着枪的人发射子弹时，枪的后座力与子弹离开枪的力相同。这听起来可能有悖常理，因为子弹可以杀死人，但枪的后坐力不会杀死开枪的人。那是因为枪明显比子弹大，牛顿第一定律说 *F = ma* ，或者说力等于质量乘以加速度。换句话说，如果枪比子弹大 50 倍，那么同样的力只会让它加速到 1/50 的速度。我们将修改我们的宇宙飞船，这样，每当它发射一枚射弹时，它就会根据宇宙飞船和射弹的相对质量，向与发射方向相反的方向加速。这会给我们船上的大炮一个后坐力。\n\n# 增加重力\n\n在我们给飞船的加农炮增加后坐力之后，我还想在我们的游戏中给宇宙飞船增加一个重力效应，当飞船在恒星的某个距离内时，它会把飞船拉向恒星。引力随着两个物体之间距离的平方而减小。这很方便，因为这意味着我们可以用`MagSQ`函数计算重力效应，它比`Magnitude`函数运行得快得多。出于个人喜好，我选择不在抛射体和小行星上添加引力效应。如果你选择这样做，就不难增加这种效果。\n\n# 改善碰撞\n\n我们将在游戏中改进我们的宇宙飞船与小行星和抛射体之间的碰撞。为了简化事情，我们将使用弹性碰撞。弹性碰撞是保留所有动能的碰撞。在现实中，碰撞总是会因为热量或摩擦而损失一些能量，即使是那些接近弹性碰撞的碰撞，比如台球。然而，使我们的碰撞完全有弹性简化了数学。在游戏中，更简单的数学通常意味着更快的算法。\n\n关于弹性碰撞的更多信息，维基百科有一篇优秀的文章([http](https://en.wikipedia.org/wiki/Elastic_collision)[s://en . Wikipedia . org/wiki/Elastic _ collection](https://en.wikipedia.org/wiki/Elastic_collision))讨论了我们将用来实现弹性碰撞功能的数学。\n\n# 修改代码\n\n在这一节中，我们将对我们的游戏对象进行一些更改。我们需要增加质量和弹性碰撞到我们的`collider`类。我们的恒星应该能够产生引力，并以基于距离的平方而减小的力吸引玩家和敌方飞船。我们将需要修改我们的碰撞功能，以增加我们的宇宙飞船、小行星和射弹之间的弹性碰撞。\n\n# 正在更改 game.hpp 文件\n\n为了让物理进入我们的游戏，我们需要修改几个类定义并添加新的`#define`宏。让我们从更新我们的`game.hpp`文件开始。我们首先需要添加的是`#define`，以便为我们的恒星质量设置一个恒定值。我希望恒星质量有一个大的常数值，我们将在`ElasticCollision`函数中对其进行检查。如果在我们的弹性碰撞中，任何一个物体的质量与`STAR_MASS`的质量相同，我们就不想加速那个物体。实际上，如果你把一块石头扔向太阳，你会在你扔石头的方向上，使太阳加速一点点。这个数量相对于太阳来说非常小，以至于无法探测到。我们将有一个恒星质量的固定值，在我们的游戏中，任何质量如此大的物体被任何物体击中时都不会加速。为此，我们需要添加以下`#define`:\n\n```cpp\n#define STAR_MASS 9999999\n```\n\n添加`#define`后，我们需要修改我们的`Collider`类，给它一个新的`ElasticCollision`功能。该功能将接收第二个`Collider`物体，并使用这两个物体的速度和质量来确定它们的新速度。我们还需要添加一个我们将命名为`m_Mass`的质量属性。最后，我们需要将两个属性移到我们的`Collider`类中，这个类以前在`Collider`的子类中。这些变量是 2D `m_Direction`和`m_Velocity`向量，因为我们的弹性碰撞函数需要这些数据来计算新的速度。这就是新版`Collider`类的样子:\n\n```cpp\nclass Collider {\n    public:\n        bool m_Active;\n        float* m_ParentRotation;\n        float* m_ParentX;\n        float* m_ParentY;\n        Vector2D m_TempPoint;\n\n        bool CCHitTest( Collider* collider );\n\n void ElasticCollision( Collider* collider );\n float m_Mass;\n Vector2D m_Direction;\n Vector2D m_Velocity;\n Vector2D m_Position;\n\n        float m_Radius;\n        float m_SteeringRadius;\n        float m_SteeringRadiusSQ;\n        void SetParentInformation( float* rotation, float* x, float* y );\n\n        Collider(float radius);\n        bool HitTest( Collider *collider );\n        bool SteeringLineTest( Vector2D &p1, Vector2D &p2 );\n        bool SteeringRectTest( Vector2D &start_point, Vector2D \n                               &end_point );\n        void WrapPosition();\n};\n```\n\n我们添加的四行靠近这个新版本类的中心:\n\n```cpp\nvoid ElasticCollision( Collider* collider );\nfloat m_Mass;\nVector2D m_Direction;\nVector2D m_Velocity;\n```\n\n在将`m_Direction`和`m_Velocity`添加到我们的`Collider`类之后，我们需要从三个子类中删除`m_Velocity`，在我们的游戏的先前版本中，这些子类中有这些代码。我们需要从`Asteroid`、`Ship`和`Projectile`类中移除这些属性。下面是我们需要删除的两行:\n\n```cpp\nVector2D m_Direction;\nVector2D m_Velocity;\n```\n\n在下面的代码片段中，在您删除了这两行之后，我们有了`Asteroid`类:\n\n```cpp\nclass Asteroid : public Collider {\n    public:\n        SDL_Texture *m_SpriteTexture;\n        SDL_Rect m_src = {.x = 0, .y = 0, .w = 16, .h = 16 };\n        SDL_Rect m_dest = {.x = 0, .y = 0, .w = 0, .h = 0 };\n\n        Uint32 m_CurrentFrame = 0;\n        int m_NextFrameTime;\n        float m_Rotation;\n\n        Emitter* m_Explode;\n        Emitter* m_Chunks;\n\n        Asteroid( float x, float y,\n                  float velocity,\n                  float rotation );\n\n        void Move();\n        void Render();\n        void Explode();\n};\n```\n\n这就是删除这两行后`Ship`类的样子:\n\n```cpp\nclass Ship : public Collider {\n    public:\n        const float c_Acceleration = 10.0f;\n        const float c_MaxVelocity = 100.0f;\n        const int c_AliveTime = 2000;\n        const Uint32 c_MinLaunchTime = 300;\n\n        bool m_Accelerating = false;\n        Uint32 m_LastLaunchTime;\n        const int c_Width = 32;\n        const int c_Height = 32;\n        SDL_Texture *m_SpriteTexture;\n        SDL_Rect src = {.x = 0, .y = 0, .w = 32, .h = 32 };\n\n        Emitter* m_Explode;\n        Emitter* m_Exhaust;\n        Shield* m_Shield;\n        std::vector<Collider*> m_Colliders;\n\n        Uint32 m_CurrentFrame = 0;\n        int m_NextFrameTime;\n        float m_Rotation;\n\n        void RotateLeft();\n        void RotateRight();\n        void Accelerate();\n        void Decelerate();\n        void CapVelocity();\n        void Shoot();\n        virtual void Move() = 0;\n        Ship();\n        void Render();\n        bool CompoundHitTest( Collider* collider );\n};\n```\n\n最后，下面是删除这两行后`Projectile`类的样子:\n\n```cpp\nclass Projectile: public Collider {\n    public:\n        const char* c_SpriteFile = \"sprites/ProjectileExp.png\";\n        const int c_Width = 16;\n        const int c_Height = 16;\n        SDL_Texture *m_SpriteTexture;\n        SDL_Rect src = {.x = 0, .y = 0, .w = 16, .h = 16 };\n\n        Uint32 m_CurrentFrame = 0;\n        int m_NextFrameTime;\n        const float c_Velocity = 300.0;\n        const float c_AliveTime = 2000;\n        float m_TTL;\n\n        Projectile();\n        void Move();\n        void Render();\n        void Launch(Vector2D &position, Vector2D &direction);\n};\n```\n\n我们必须改变的最后一个阶层是我们的`Star`阶层。`Star`级现在将能够通过引力吸引我们游戏中的宇宙飞船。为此，我们将添加一个常数属性，定义我们引力的最大范围。在现实中，重力会永远延伸下去，但是对于我们的游戏来说，我们不希望当恒星离屏幕很远(或者至少离屏幕很远)时，重力会影响我们的宇宙飞船。正因为如此，我们将把引力效应的距离限制在 500 像素。我们还将为我们的类添加一个名为`ShipGravity`的新函数。我们将传递一个`Ship`物体到这个函数中，这个函数将根据到`Star`物体的平方距离来修改船的速度。这就是新版`Star`类定义的样子:\n\n```cpp\nclass Star : public Collider {\n    public:\n        const float c_MaxGravityDistSQ = 250000.0; // 300 squared\n\n        SDL_Texture *m_SpriteTexture;\n        SDL_Rect m_src = {.x = 0, .y = 0, .w = 64, .h = 64 };\n        SDL_Rect m_dest = {.x = 0, .y = 0, .w = 64, .h = 64 };\n\n        std::vector<Emitter*> m_FlareList;\n\n        Uint32 m_CurrentFrame = 0;\n        int m_NextFrameTime;\n\n        Star();\n\n        void Move();\n        void Render();\n\n        void ShipGravity( Ship* s );\n};\n```\n\n# 更改对撞机. cpp\n\n我们将更改的下一个文件是`collider.cpp`文件，它保存了我们在`Collider`类定义中声明的函数。唯一的变化是增加了一个单一的功能，`ElasticCollision`。这个函数根据物体的质量和起始速度来修改我们两台对撞机的位置和速度。这就是`ElasticCollision`功能的样子:\n\n```cpp\nvoid Collider::ElasticCollision( Collider* collider ) {\n    if( collider->m_Mass == STAR_MASS || m_Mass == STAR_MASS ) {\n        return;\n    }\n\n    Vector2D separation_vec = collider->m_Position - m_Position;\n\n    separation_vec.Normalize();\n    separation_vec *= collider->m_Radius + m_Radius;\n\n    collider->m_Position = m_Position + separation_vec;\n\n    Vector2D old_v1 = m_Velocity;\n    Vector2D old_v2 = collider->m_Velocity;\n\n    m_Velocity = old_v1 * ((m_Mass - collider->m_Mass)/(m_Mass + \n    collider->m_Mass)) +\n    old_v2 * ((2 * collider->m_Mass) / (m_Mass + collider->m_Mass));\n\n    collider->m_Velocity = old_v1 * ((2 * collider->m_Mass)/(m_Mass + \n    collider->m_Mass)) +\n    old_v2 * ((collider->m_Mass - m_Mass)/(m_Mass + collider->m_Mass));\n}\n```\n\n这个函数做的第一件事是检查两个对撞机是否都有恒星的质量。如果其中一个是恒星，我们不会改变它们的速度。恒星的速度不会改变，因为它的质量太大而无法移动，与恒星碰撞的物体不会改变质量，因为它在碰撞中被摧毁了:\n\n```cpp\nif( collider->m_Mass == STAR_MASS || m_Mass == STAR_MASS ) {\n    return;\n}\n```\n\n质量检查后，我们需要调整对撞机的位置，使它们不重叠。重叠可能会发生，因为我们的对象的位置每帧都在变化，并且不是连续的。因此，我们需要移动其中一个物体的位置，使其几乎不接触另一个物体。更准确的方法是将两个对象的位置修改为我们修改一个对象的一半，但方向不同。为了简单起见，我们将只改变其中一个碰撞器的位置:\n\n```cpp\nseparation_vec.Normalize();\nseparation_vec *= collider->m_Radius + m_Radius;\n\ncollider->m_Position = m_Position + separation_vec;\n```\n\n之后，我们将使用这两个物体的质量和起始速度来修改两个对撞机物体的速度:\n\n```cpp\nVector2D old_v1 = m_Velocity;\nVector2D old_v2 = collider->m_Velocity;\n\nm_Velocity = old_v1 * ((m_Mass - collider->m_Mass)/(m_Mass + collider->m_Mass)) +\nold_v2 * ((2 * collider->m_Mass) / (m_Mass + collider->m_Mass));\n\ncollider->m_Velocity = old_v1 * ((2 * collider->m_Mass)/(m_Mass + collider->m_Mass)) +\nold_v2 * ((collider->m_Mass - m_Mass)/(m_Mass + collider->m_Mass));\n```\n\n如果你想了解更多关于我们用来计算新速度的公式，请查阅维基百科关于 https://en.wikipedia.org/wiki/Elastic_collision 弹性碰撞的文章。\n\n# 更改 star.cpp\n\n在我们的`star.cpp`文件中，我们将需要修改我们的`Star`类的构造函数，以及它的`Move`函数。我们还需要添加一个名为`ShipGravity`的新功能。我们要做的第一件事是在我们的`Star`类构造函数中的某个地方添加以下行:\n\n```cpp\nm_Mass = STAR_MASS;\n```\n\n之后，我们需要定义我们的`ShipGravity`函数。以下代码定义了该函数:\n\n```cpp\nvoid Star::ShipGravity( Ship* s ) {\n    Vector2D dist_vec = m_Position - s->m_Position;\n    float dist_sq = dist_vec.MagSQ();\n\n    if( dist_sq < c_MaxGravityDistSQ ) {\n        float force = (c_MaxGravityDistSQ / dist_sq) * delta_time;\n        dist_vec.Normalize();\n        dist_vec *= force;\n        s->m_Velocity += dist_vec;\n    }\n}\n```\n\n第一行创建一个`dist_vec`向量，这是一个表示恒星位置和船只位置之间距离的向量。第二条线得到恒星和飞船之间的平方距离。之后，我们有一个`if`块，看起来像这样:\n\n```cpp\nif( dist_sq < c_MaxGravityDistSQ ) {\n    float force = (c_MaxGravityDistSQ / dist_sq) * delta_time;\n    dist_vec.Normalize();\n    dist_vec *= force;\n    s->m_Velocity += dist_vec;\n}\n```\n\n这个`if`块正在对照重力影响船只的最大距离检查平方距离，我们在`c_MaxGravityDistSQ`常数中定义了这个距离。因为引力随着恒星和我们飞船之间距离的平方而减小，所以我们通过将最大引力距离除以到我们飞船的距离平方的 50 倍来计算标量力。50 的值是相当随意选择的，是我玩弄数字直到重力感觉适合我的结果。如果你希望你的重力不同，你可以选择不同的值。您也可以选择通过更改我们在`game.hpp`中定义的`c_MaxGravityDistSQ`的值来修改最大重力距离。以下几行用于将我们的标量力值转换为从我们的船指向我们的星的矢量力值:\n\n```cpp\ndist_vec.Normalize();\ndist_vec *= force;\n```\n\n现在我们已经将`dist_vec`转换为指向我们恒星方向的力矢量，我们可以将该力矢量添加到我们飞船的速度中，从而在我们的飞船上产生重力效应:\n\n```cpp\ns->m_Velocity += dist_vec;\n```\n\n我们需要做的最后一个改变是`Move`功能。我们需要向`ShipGravity`函数添加两个调用；一次召唤对玩家产生引力效果，第二次召唤对敌方飞船产生引力效果。以下是新版本的`Move`功能:\n\n```cpp\nvoid Star::Move() {\n    m_NextFrameTime -= diff_time;\n\n    if( m_NextFrameTime <= 0 ) {\n        ++ m_CurrentFrame;\n        m_NextFrameTime = ms_per_frame;\n        if( m_CurrentFrame >= 8 ) {\n            m_CurrentFrame = 0;\n        }\n    }\n\n ShipGravity( player );\n ShipGravity( enemy );\n}\n```\n\n最后两行是新的。确保将这两行添加到`Move`功能中:\n\n```cpp\nShipGravity( player );\nShipGravity( enemy );\n```\n\n# 更改 main.cpp 文件\n\n在更新我们的`star.cpp`文件之后，我们需要改变`main.cpp`文件来合并我们的弹性碰撞。我们需要对`collisions()`功能进行所有这些更改。以下是`collisions`的完整新版本:\n\n```cpp\nvoid collisions() {\n Asteroid* asteroid;\n std::vector<Asteroid*>::iterator ita;\n    if( player->m_CurrentFrame == 0 && player->CompoundHitTest( star ) ) {\n        player->m_CurrentFrame = 1;\n        player->m_NextFrameTime = ms_per_frame;\n        player->m_Explode->Run();\n        large_explosion_snd->Play();\n    }\n    if( enemy->m_CurrentFrame == 0 && enemy->CompoundHitTest( star ) ) {\n        enemy->m_CurrentFrame = 1;\n        enemy->m_NextFrameTime = ms_per_frame;\n        enemy->m_Explode->Run();\n        large_explosion_snd->Play();\n    }\n    Projectile* projectile;\n    std::vector<Projectile*>::iterator it;\n    for(it=projectile_pool->m_ProjectileList.begin(); \n    it!=projectile_pool->m_ProjectileList.end();\n    it++) {\n        projectile = *it;\n        if( projectile->m_CurrentFrame == 0 && projectile->m_Active ) {\n            for( ita = asteroid_list.begin(); ita != asteroid_list.end(); \n                 ita++ \n            ) {\n                asteroid = *ita;\n                if( asteroid->m_Active ) {\n                    if( asteroid->HitTest( projectile ) ) {\n asteroid->ElasticCollision( projectile );\n                        projectile->m_CurrentFrame = 1;\n                        projectile->m_NextFrameTime = ms_per_frame;\n                        small_explosion_snd->Play();\n                    }\n                }\n            }\n            if( projectile->HitTest( star ) ){\n                projectile->m_CurrentFrame = 1;\n                projectile->m_NextFrameTime = ms_per_frame;\n                small_explosion_snd->Play();\n            }\n            else if( player->m_CurrentFrame == 0 && ( projectile->HitTest( \n            player ) ||\n                      player->CompoundHitTest( projectile ) ) ) {\n                if( player->m_Shield->m_Active == false ) {\n                    player->m_CurrentFrame = 1;\n                    player->m_NextFrameTime = ms_per_frame;\n                    player->m_Explode->Run();\n                    large_explosion_snd->Play();\n                }\n                else {\n                    hit_snd->Play();\n player->ElasticCollision( projectile );\n                }\n                projectile->m_CurrentFrame = 1;\n                projectile->m_NextFrameTime = ms_per_frame;\n            }\n            else if( enemy->m_CurrentFrame == 0 && ( projectile-\n            >HitTest( enemy ) || enemy->CompoundHitTest( projectile ) ) \n             ) {\n                if( enemy->m_Shield->m_Active == false ) {\n                    enemy->m_CurrentFrame = 1;\n                    enemy->m_NextFrameTime = ms_per_frame;\n                    enemy->m_Explode->Run();\n                    large_explosion_snd->Play();\n                }\n                else {\n                    enemy->ElasticCollision( projectile );\n                    hit_snd->Play();\n                }\n                projectile->m_CurrentFrame = 1;\n                projectile->m_NextFrameTime = ms_per_frame;\n            }\n        }\n    }\n    for( ita = asteroid_list.begin(); ita != asteroid_list.end(); ita++ ) {\n        asteroid = *ita;\n        if( asteroid->m_Active ) {\n            if( asteroid->HitTest( star ) ) {\n                asteroid->Explode();\n                small_explosion_snd->Play();\n            }\n        }\n        else { continue; }\n        if( player->m_CurrentFrame == 0 && asteroid->m_Active &&\n            ( asteroid->HitTest( player ) || player->CompoundHitTest( \n            asteroid ) ) ) {\n            if( player->m_Shield->m_Active == false ) {\n                player->m_CurrentFrame = 1;\n                player->m_NextFrameTime = ms_per_frame;\n                player->m_Explode->Run();\n                large_explosion_snd->Play();\n            }\n            else {\n player->ElasticCollision( asteroid );\n                small_explosion_snd->Play();\n            }\n        }\n        if( enemy->m_CurrentFrame == 0 && asteroid->m_Active &&\n            ( asteroid->HitTest( enemy ) || enemy->CompoundHitTest( \n            asteroid ) ) ) {\n            if( enemy->m_Shield->m_Active == false ) {\n                enemy->m_CurrentFrame = 1;\n                enemy->m_NextFrameTime = ms_per_frame;\n                enemy->m_Explode->Run();\n                large_explosion_snd->Play();\n            }\n            else {\n enemy->ElasticCollision( asteroid );\n                small_explosion_snd->Play();\n            }\n        }\n    }\n    Asteroid* asteroid_1;\n    Asteroid* asteroid_2;\n    std::vector<Asteroid*>::iterator ita_1;\n    std::vector<Asteroid*>::iterator ita_2;\n    for( ita_1 = asteroid_list.begin(); ita_1 != asteroid_list.end(); \n         ita_1++ ) {\n        asteroid_1 = *ita_1;\n        if( !asteroid_1->m_Active ) { continue; }\n        for( ita_2 = ita_1+1; ita_2 != asteroid_list.end(); ita_2++ ) {\n            asteroid_2 = *ita_2;\n            if( !asteroid_2->m_Active ) { continue; }\n            if( asteroid_1->HitTest( asteroid_2 ) ) {\n asteroid_1->ElasticCollision( asteroid_2 );\n            }\n        }\n    }\n}\n```\n\n在这个功能的第一部分，我们在射弹上循环，检查它们是否击中了小行星或船只。如果炮弹击中一颗小行星或一艘船，当那艘船的防护罩打开时，我们想与炮弹产生弹性碰撞。抛射体仍将被摧毁，但飞船或小行星将根据碰撞具有修正的速度。以下是`projectile`循环的代码:\n\n```cpp\nfor( it = projectile_pool->m_ProjectileList.begin(); it != projectile_pool->m_ProjectileList.end(); it++ ) {\n    projectile = *it;\n    if( projectile->m_CurrentFrame == 0 && projectile->m_Active ) {\n        for( ita = asteroid_list.begin(); ita != asteroid_list.end(); \n        ita++ ) {\n            asteroid = *ita;\n            if( asteroid->m_Active ) {\n                if( asteroid->HitTest( projectile ) ) {\n asteroid->ElasticCollision( projectile );\n                    projectile->m_CurrentFrame = 1;\n                    projectile->m_NextFrameTime = ms_per_frame;\n                    small_explosion_snd->Play();\n                }\n            }\n        }\n        if( projectile->HitTest( star ) ){\n            projectile->m_CurrentFrame = 1;\n            projectile->m_NextFrameTime = ms_per_frame;\n            small_explosion_snd->Play();\n        }\n        else if( player->m_CurrentFrame == 0 &&\n                ( projectile->HitTest( player ) ||\n                  player->CompoundHitTest( projectile ) ) ) {\n            if( player->m_Shield->m_Active == false ) {\n                player->m_CurrentFrame = 1;\n                player->m_NextFrameTime = ms_per_frame;\n\n                player->m_Explode->Run();\n                large_explosion_snd->Play();\n            }\n            else {\n                hit_snd->Play();\n player->ElasticCollision( projectile );\n            }\n            projectile->m_CurrentFrame = 1;\n            projectile->m_NextFrameTime = ms_per_frame;\n        }\n        else if( enemy->m_CurrentFrame == 0 &&\n                ( projectile->HitTest( enemy ) ||\n                  enemy->CompoundHitTest( projectile ) ) ) {\n            if( enemy->m_Shield->m_Active == false ) {\n                enemy->m_CurrentFrame = 1;\n                enemy->m_NextFrameTime = ms_per_frame;\n                enemy->m_Explode->Run();\n                large_explosion_snd->Play();\n            }\n            else {\n enemy->ElasticCollision( projectile );\n                hit_snd->Play();\n            }\n            projectile->m_CurrentFrame = 1;\n            projectile->m_NextFrameTime = ms_per_frame;\n        }\n    }\n}\n```\n\n这个循环执行的第一系列检查是针对每一颗小行星。它寻找一颗正在碰撞的活跃的小行星。如果这些条件是真的，它做的第一件事就是调用小行星上的`ElasticCollision`函数，传入射弹:\n\n```cpp\nfor( ita = asteroid_list.begin(); ita != asteroid_list.end(); ita++ ) {\n    asteroid = *ita;\n    if( asteroid->m_Active ) {\n        if( asteroid->HitTest( projectile ) ) {\n asteroid->ElasticCollision( projectile );\n            projectile->m_CurrentFrame = 1;\n            projectile->m_NextFrameTime = ms_per_frame;\n            small_explosion_snd->Play();\n        }\n    }\n```\n\n该代码与早期版本相同，但增加了对`ElasticCollision`的调用:\n\n```cpp\nasteroid->ElasticCollision( projectile );\n```\n\n稍后，在我们遍历每个活动射弹的循环中，如果一个射弹击中了玩家的飞船，同时它的护盾打开，我们将添加对`ElasticCollision`功能的调用:\n\n```cpp\nelse if( player->m_CurrentFrame == 0 &&\n        ( projectile->HitTest( player ) ||\n          player->CompoundHitTest( projectile ) ) ) {\n    if( player->m_Shield->m_Active == false ) {\n        player->m_CurrentFrame = 1;\n        player->m_NextFrameTime = ms_per_frame;\n        player->m_Explode->Run();\n        large_explosion_snd->Play();\n    }\n    else {\n        hit_snd->Play();\n player->ElasticCollision( projectile );\n    }\n    projectile->m_CurrentFrame = 1;\n    projectile->m_NextFrameTime = ms_per_frame;\n}\n```\n\n我们将对一艘被炮弹击中的敌方宇宙飞船做同样的处理:\n\n```cpp\n    else if( enemy->m_CurrentFrame == 0 &&\n            ( projectile->HitTest( enemy ) ||\n              enemy->CompoundHitTest( projectile ) ) ) {\n        if( enemy->m_Shield->m_Active == false ) {\n            enemy->m_CurrentFrame = 1;\n            enemy->m_NextFrameTime = ms_per_frame;\n            enemy->m_Explode->Run();\n            large_explosion_snd->Play();\n        }\n        else {\n enemy->ElasticCollision( projectile );\n            hit_snd->Play();\n        }\n        projectile->m_CurrentFrame = 1;\n        projectile->m_NextFrameTime = ms_per_frame;\n    }\n}\n```\n\n在循环所有活动的射弹之后，`collisions`功能循环所有的小行星，寻找小行星和其中一艘飞船之间的碰撞。如果飞船没有启动护盾，飞船就会被摧毁。我们不对这部分代码进行任何修改。在我们代码的早期版本中，如果飞船确实有护盾，我们就摧毁了小行星。现在，我们将发生弹性碰撞，这将导致宇宙飞船和小行星相互反弹。这就是这个`asteroid`循环的样子:\n\n```cpp\nfor( ita = asteroid_list.begin(); ita != asteroid_list.end(); ita++ ) {\n    asteroid = *ita;\n    if( asteroid->m_Active ) {\n        if( asteroid->HitTest( star ) ) {\n            asteroid->Explode();\n            small_explosion_snd->Play();\n        }\n    }\n    else {\n        continue;\n    }\n\n    if( player->m_CurrentFrame == 0 &&\n        asteroid->m_Active &&\n        ( asteroid->HitTest( player ) ||\n          player->CompoundHitTest( asteroid ) ) ) {\n        if( player->m_Shield->m_Active == false ) {\n            player->m_CurrentFrame = 1;\n            player->m_NextFrameTime = ms_per_frame;\n\n            player->m_Explode->Run();\n            large_explosion_snd->Play();\n        }\n        else {\n player->ElasticCollision( asteroid );\n            small_explosion_snd->Play();\n        }\n    }\n    if( enemy->m_CurrentFrame == 0 &&\n        asteroid->m_Active &&\n        ( asteroid->HitTest( enemy ) ||\n          enemy->CompoundHitTest( asteroid ) ) ) {\n        if( enemy->m_Shield->m_Active == false ) {\n            enemy->m_CurrentFrame = 1;\n            enemy->m_NextFrameTime = ms_per_frame;\n\n            enemy->m_Explode->Run();\n            large_explosion_snd->Play();\n        }\n        else {\n            enemy->ElasticCollision( asteroid );\n            small_explosion_snd->Play();\n        }\n    }\n}\n```\n\n现在有两个电话打给`ElasticCollision`。其中一个召唤发生在玩家飞船与小行星相撞，玩家飞船的护盾升起的时候。另一种情况发生在敌舰与小行星相撞时，敌舰将护盾竖起。\n\n我们必须对`collisions()`函数做的最后一个改变是增加一个新的双`asteroid`环，它将环绕我们的所有小行星，寻找它们之间的碰撞。这样就产生了一种有趣的效果，小行星会像台球一样相互弹开。如果探测到两个小行星之间发生碰撞，我们称之为`ElasticCollision`:\n\n```cpp\nAsteroid* asteroid_1;\nAsteroid* asteroid_2;\n\nstd::vector<Asteroid*>::iterator ita_1;\nstd::vector<Asteroid*>::iterator ita_2;\n\nfor( ita_1 = asteroid_list.begin(); ita_1 != asteroid_list.end(); ita_1++ ) {\n    asteroid_1 = *ita_1;\n    if( !asteroid_1->m_Active ) {\n        continue;\n    }\n\n    for( ita_2 = ita_1+1; ita_2 != asteroid_list.end(); ita_2++ ) {\n        asteroid_2 = *ita_2;\n        if( !asteroid_2->m_Active ) {\n            continue;\n        }\n\n        if( asteroid_1->HitTest( asteroid_2 ) ) {\n asteroid_1->ElasticCollision( asteroid_2 );\n        }\n    }\n}\n```\n\n# 改为小行星. cpp 和抛射体. cpp\n\n我们必须对`asteroid.cpp`和`projectile.cpp`做一个小的补充。我们在`Collider`类中增加了一个名为`m_Mass`的新属性，所以所有从`Collider`派生的类都继承了这个属性。我们的`ElasticCollision`功能使用`m_Mass`属性来确定这些对象在弹性碰撞后将如何移动。宇宙飞船的质量和射弹的质量之比将被用来计算宇宙飞船发射射弹时产生的后坐力。首先修改的是`Projectile`类的构造函数。下面是该构造函数的新版本:\n\n```cpp\nProjectile::Projectile(): Collider(4.0) {\n    m_Active = false;\n\n    SDL_Surface *temp_surface = IMG_Load( c_SpriteFile );\n\n    if( !temp_surface ) {\n        printf(\"failed to load image: %s\\n\", IMG_GetError() );\n        return;\n    }\n\n    m_SpriteTexture = SDL_CreateTextureFromSurface( renderer, temp_surface \n    );\n\n    if( !m_SpriteTexture ) {\n        printf(\"failed to create texture: %s\\n\", IMG_GetError() );\n        return;\n    }\n\n    SDL_FreeSurface( temp_surface );\n\n m_Mass = 1.0;\n}\n```\n\n唯一的修改是最后一行，我们将`m_Mass`设置为`1.0`:\n\n```cpp\nm_Mass = 1.0;\n```\n\n下一个需要修改的构造函数在`asteroid.cpp`文件中。我们需要修改`Asteroid`类的构造函数。以下是新版本的`Asteroid`建造师:\n\n```cpp\nAsteroid::Asteroid( float x, float y, float velocity, float rotation ): Collider(8.0) {\n    SDL_Surface *temp_surface = IMG_Load( ADSTEROID_SPRITE_FILE );\n    if( !temp_surface ) {\n        printf(\"failed to load image: %s\\n\", IMG_GetError() );\n        return;\n    }\n    else { printf(\"success creating asteroid surface\\n\"); }\n    m_SpriteTexture = SDL_CreateTextureFromSurface( renderer, temp_surface \n    );\n    if( !m_SpriteTexture ) {\n        printf(\"failed to create texture: %s\\n\", IMG_GetError() );\n        return;\n    }\n    else { printf(\"success creating asteroid texture\\n\"); }\n    SDL_FreeSurface( temp_surface );\n    m_Explode = new Emitter((char*)\"/sprites/Explode.png\", 100, 0, 360, \n    1000, 0.3, false, 20.0, 40.0, 10, 0, 0, 5, 1.0, 2.0, 1.0, 2.0,\n    0xffffff, 0xffffff, 0.01, 10, false, false, 800, 8 ); \n    m_Explode->m_parent_rotation_ptr = &m_Rotation;\n    m_Explode->m_parent_x_ptr = &(m_Position.x);\n    m_Explode->m_parent_y_ptr = &(m_Position.y);\n    m_Explode->m_Active = false;\n    m_Chunks = new Emitter((char*)\"/sprites/small-asteroid.png\",40,0,360, \n    1000, 0.05, false, 80.0, 150.0, 5,0,0,10,2.0,2.0,0.25, 0.5, 0xffffff, \n    0xffffff, 0.1, 10, false, true, 1000, 8 ); \n    m_Chunks->m_parent_rotation_ptr = &m_Rotation;\n    m_Chunks->m_parent_x_ptr = &m_Position.x;\n    m_Chunks->m_parent_y_ptr = &m_Position.y;\n    m_Chunks->m_Active = false;\n    m_Position.x = x;\n    m_Position.y = y;\n    Vector2D direction;\n    direction.x = 1;\n    direction.Rotate( rotation );\n    m_Direction = direction;\n    m_Velocity = m_Direction * velocity;\n    m_dest.h = m_src.h = m_dest.w = m_src.w = 16;\n    m_Rotation = rotation;\n    m_Active = true;\n    m_CurrentFrame = 0;\n    m_NextFrameTime = ms_per_frame;\n\n    m_Mass = 100.0;\n}\n```\n\n同样，我们将添加的唯一一行是我们将`m_Mass`设置为`100.0`的最后一行:\n\n```cpp\nm_Mass = 100.0;\n```\n\n# 对 ship.cpp 文件的更改\n\n对`ship.cpp`文件的第一个更改将是对`Ship`构造函数的更改。这是我们需要对构造函数进行的一个简单的更改，在这里我们将把船的质量设置为`50.0`。以下是新版本的`Ship`类构造函数:\n\n```cpp\nShip::Ship() : Collider(8.0) {\n    m_Rotation = PI;\n\n    m_LastLaunchTime = current_time;\n\n    m_Accelerating = false;\n\n    m_Exhaust = new Emitter((char*)\"/sprites/ProjectileExpOrange.png\", 200,\n                             -10, 10,\n                             400, 1.0, true,\n                             0.1, 0.1,\n                             30, 0, 12, 0.5,\n                             0.5, 1.0,\n                             0.5, 1.0,\n                             0xffffff, 0xffffff,\n                             0.7, 10,\n                             true, true,\n                             1000, 6 );\n\n    m_Exhaust->m_parent_rotation_ptr = &m_Rotation;\n    m_Exhaust->m_parent_x_ptr = &(m_Position.x);\n    m_Exhaust->m_parent_y_ptr = &(m_Position.y);\n    m_Exhaust->m_x_adjustment = 10;\n    m_Exhaust->m_y_adjustment = 10;\n    m_Exhaust->m_Active = false;\n\n    m_Explode = new Emitter((char*)\"/sprites/Explode.png\", 100,\n                             0, 360,\n                             1000, 0.3, false,\n                             20.0, 40.0,\n                             10, 0, 0, 5,\n                             1.0, 2.0,\n                             1.0, 2.0,\n                             0xffffff, 0xffffff,\n                             0.0, 10,\n                             false, false,\n                             800, 8 );\n\n    m_Explode->m_parent_rotation_ptr = &m_Rotation;\n    m_Explode->m_parent_x_ptr = &(m_Position.x);\n    m_Explode->m_parent_y_ptr = &(m_Position.y);\n    m_Explode->m_Active = false;\n\n    m_Direction.y = 1.0;\n\n    m_Active = true;\n m_Mass = 50.0;\n}\n```\n\n唯一被更改的一行是最后一行:\n\n```cpp\nm_Mass = 50.0;\n```\n\n我们还需要改变`Shoot`功能来增加后坐力。将添加几条线，通过添加一个与船面对的方向相反的矢量来修改船的速度，该矢量的大小基于发射的射弹的速度和相对质量。以下是新的`Shoot`功能:\n\n```cpp\nvoid Ship::Shoot() {\n    Projectile* projectile;\n    if( current_time - m_LastLaunchTime >= c_MinLaunchTime ) {\n        m_LastLaunchTime = current_time;\n        projectile = projectile_pool->GetFreeProjectile();\n        if( projectile != NULL ) {\n            projectile->Launch( m_Position, m_Direction );\n            player_laser_snd->Play();\n            m_Velocity -= m_Direction * (projectile->c_Velocity * projectile->m_Mass / \n                                                                              m_Mass);\n            CapVelocity();\n        }\n    }\n}\n```\n\n这是我们添加到函数中的两行:\n\n```cpp\nm_Velocity -= m_Direction * (projectile->c_Velocity * projectile->m_Mass / m_Mass);\nCapVelocity();\n```\n\n# 正在编译 physics.html 文件\n\n现在我们已经添加了物理，是时候编译我们的代码了。我们可以使用以下`em++ `命令构建`physics.html`文件:\n\n```cpp\nem++ asteroid.cpp audio.cpp camera.cpp collider.cpp emitter.cpp enemy_ship.cpp finite_state_machine.cpp locator.cpp main.cpp particle.cpp player_ship.cpp projectile_pool.cpp projectile.cpp range.cpp render_manager.cpp shield.cpp ship.cpp star.cpp vector.cpp -o physics.html --preload-file audio --preload-file sprites -std=c++ 17 -s USE_WEBGL2=1 -s USE_SDL=2 -s USE_SDL_IMAGE=2 -s SDL2_IMAGE_FORMATS=[\"png\"] -s USE_SDL_IMAGE=2 -s SDL2_IMAGE_FORMATS=[\"png\"] \n```\n\n下面的截图看起来可能与早期版本相似，但是当你发射炮弹时，飞船会向后加速。如果你在护盾开启时与小行星相撞，你会像台球一样被它们弹开。离太阳太近，重力会开始吸引你的船:\n\n![](img/10269f25-3eed-46f3-9771-a8ed315e3005.png)\n\nFigure 13.1: physics.html screenshot\n\n# 摘要\n\n在这一章中，我们讨论了计算机游戏中的物理学史，以及这段历史是如何追溯到第一个计算机游戏 *SpaceWar 的！*。我们讨论了我们游戏中已经有的物理，包括动量守恒。我们简要讨论了牛顿第三定律及其如何应用于游戏，然后我们通过使用第三定律为我们的游戏添加了更多的牛顿物理学。我们给我们的恒星增加了一个引力场，让它在我们的游戏中用一个随着两个物体之间距离的平方而减小的力来吸引宇宙飞船。最后，我们增加了宇宙飞船、射弹和小行星之间的弹性碰撞。\n\n在下一章中，我们将为我们的游戏添加一个**用户界面** ( **UI** )。我们还将把游戏分成多个屏幕，并增加一个鼠标界面。"
  },
  {
    "path": "docs/handson-game-dev-wasm/14.md",
    "content": "# 十四、用户界面和鼠标输入\n\n一个**用户** **界面** ( **UI** )定义了计算机程序和用户之间的交互。在我们的游戏中，到目前为止，我们的交互仅限于控制玩家飞船的键盘界面。当我们编写粒子系统配置应用时，我们使用 HTML 定义了一个更健壮的用户界面，允许我们输入值来配置我们的粒子系统。从那个用户界面，我们的代码必须间接地与 WebAssembly 代码交互。如果你想利用 HTML 来定义你的用户界面，你可以在游戏中继续使用这种技术，但是它有一些缺点。首先，我们可能想要覆盖游戏内容的用户界面元素。对于这种效果，遍历 DOM 的效率不是很高。如果用户界面元素在游戏引擎中呈现，我们的用户界面和游戏中的对象之间的交互也更容易。此外，您可能正在开发 C/C++ 代码，以用于平台和网络发布。如果是这种情况，您可能不希望 HTML 在您的用户界面中扮演太多的角色。\n\n在本章中，我们将在游戏中实现一些用户界面功能。我们将需要实现一个`Button`类，这是最简单和最常见的 UI 元素之一。我们还需要实现一个单独的屏幕和游戏状态，这样我们就可以有一个开始和结束的游戏屏幕。\n\nYou will need to include several images and audio files in your build to make this project work. Make sure that you include the `/Chapter14/sprites/` and `/Chapter14/audio/` folders from this project's GitHub repository. If you haven't downloaded the GitHub project yet, you can get it online here: [https://github.com/PacktPublishing/Hands-On-Game-Development](https://github.com/PacktPublishing/Hands-On-Game-Development-with-WebAssembly).\n\n在本章中，我们将涵盖以下主题:\n\n*   用户界面要求\n*   获取鼠标输入\n*   创建按钮\n*   开始游戏屏幕\n*   屏幕上的游戏\n\n# 用户界面要求\n\n当实现我们的用户界面时，我们需要做的第一件事是决定一些需求。我们的用户界面到底需要什么？第一部分是决定我们的游戏需要什么游戏屏幕。这通常是你在游戏设计过程的早期所做的事情，但是因为我正在写一本关于 WebAssembly 的书，所以我把这一步留到了后面的章节。决定你的游戏需要什么样的屏幕通常需要一个故事板和一个过程，通过这个过程，你要么通过对话(如果不止一个人在玩游戏)，要么通过用户与你的网页以及网页上的游戏互动的方式来思考:\n\n![](img/44bf0fc4-4b90-44ee-bf06-3d7e6882c67e.png)\n\nFigure 14.1: Storyboard example for our user interface\n\n你不必画一个故事板，但是我发现它在思考我需要一个游戏的用户界面时很有用。当你需要将这些信息传递给另一个团队成员或艺术家时，这就更有用了。当思考我们在这个游戏中需要什么来制作前面的故事板时，我提出了以下需求列表:\n\n*   打开屏幕\n*   说明\n*   工作按钮\n*   游戏画面\n*   乐谱文本\n*   屏幕上的游戏\n*   你赢得了信息\n*   你失去了信息\n*   再次播放按钮\n\n# 打开屏幕\n\n出于几个原因，我们的游戏需要一个开放屏幕。首先，我们不希望用户一加载网页游戏就开始。用户可能会加载网页，但在网页完全加载后不会立即开始播放，原因有很多。如果他们的连接速度很慢，他们可能会在游戏加载时离开电脑，可能不会注意到第二次加载。如果他们通过点击链接到达这个页面，他们可能还没有准备好在游戏加载的瞬间开始玩。一般来说，让玩家在投入游戏之前必须做一些事情来确认他们已经准备好了，这也是一个很好的做法。开屏还应该包括一些基本玩法的说明。街机游戏有很长的历史，把简单的指令放在柜子上，告诉玩家玩游戏必须做什么。众所周知，游戏《乒乓》附带了印刷在柜子上的“高分避免漏球”的说明。不幸的是，我们没有一个街机柜来打印我们的说明，所以使用游戏开始屏幕是下一个最好的事情。我们还需要一个按钮，让用户点击后就可以开始玩游戏，如下所示:\n\n![](img/d514eacc-d959-427c-96d8-92bff06984f1.png)\n\nFigure 14.2: Opening screen image\n\n# 播放屏幕\n\n播放屏幕是我们一直拥有的屏幕。这是玩家移动飞船的屏幕，试图摧毁敌人的飞船。我们可能不需要改变这个屏幕的工作方式，但是我们需要根据游戏状态在这个屏幕上添加过渡。当玩家点击一个按钮时，游戏将需要从开始屏幕过渡到我们的播放屏幕。如果任何一艘船被摧毁，玩家还需要从屏幕上转移到游戏画面上。如下所示:\n\n![](img/2a9a73da-59ea-4d7f-9456-4c7faace707d.png)\n\nFigure 14.3: The original screen is now the play screen\n\n# 屏幕上的游戏\n\n如果其中一艘宇宙飞船被摧毁，游戏就结束了。如果玩家的船被摧毁，那么玩家就输了。如果敌舰被摧毁，那么玩家赢得游戏。*游戏结束画面*让我们知道游戏结束，并告诉我们玩家是赢了还是输了。它还需要提供一个按钮，允许我们的玩家再次玩游戏，如果他们愿意。屏幕上的游戏如下所示:\n\n![](img/d1ba8bf3-352c-4a1c-9774-b335856ee951.png)\n\nFigure 14.4: Game over screen\n\n# 鼠标输入\n\n在我们实现一个按钮之前，我们需要学习如何在 SDL 使用鼠标输入。我们用来获得键盘输入的代码在我们的`main.cpp`文件中。在`input`功能中，您会发现对`SDL_PollEvent`的调用，后面是一些不同的开关语句。第一个开关语句检查`SDL_KEYDOWN`的`event.type`。第二个开关检查`event.key.keysym.sym`看我们按了哪个键:\n\n```cpp\nif( SDL_PollEvent( &event ) ){\n    switch( event.type ){\n        case SDL_KEYDOWN:\n            switch( event.key.keysym.sym ){\n                case SDLK_LEFT:\n                    left_key_down = true;\n                    break;\n                case SDLK_RIGHT:\n                    right_key_down = true;\n                    break;\n                case SDLK_UP:\n                    up_key_down = true;\n                    break;\n                case SDLK_DOWN:\n                    down_key_down = true;\n                    break;\n                case SDLK_f:\n                    f_key_down = true;\n                    break;\n                case SDLK_SPACE:\n                    space_key_down = true;\n                    break;\n                default:\n                    break;\n            }\n            break;\n```\n\n当我们寻找鼠标输入时，我们需要使用相同的`SDL_PollEvent`函数来检索我们的鼠标事件。我们关注的三个鼠标事件分别是`SDL_MOUSEMOTION`、`SDL_MOUSEBUTTONDOWN`和`SDL_MOUSEBUTTONUP`。一旦我们知道了我们正在处理的鼠标事件的种类，我们就可以使用`SDL_GetMouseState`在事件发生时找到我们鼠标的`x`和`y`坐标:\n\n```cpp\nif(SDL_PollEvent( &event ) )\n{\n    switch (event.type)\n    {\n        case SDL_MOUSEMOTION:\n        {\n            int x_val = 0;\n            int y_val = 0;\n            SDL_GetMouseState( &x_val, &y_val );\n            printf(”mouse move x=%d y=%d\\n”, x_val, y_val);\n        }\n        case SDL_MOUSEBUTTONDOWN:\n        {\n            switch (event.button.button)\n            {\n                case SDL_BUTTON_LEFT:\n                {\n                    int x_val = 0;\n                    int y_val = 0;\n                    SDL_GetMouseState( &x_val, &y_val );\n                    printf(”mouse down x=%d y=%d\\n”, x_val, y_val);\n                    break;\n                }\n                default:\n                {\n                    break;\n                }\n            }\n            break;\n        }\n        case SDL_MOUSEBUTTONUP:\n        {\n            switch (event.button.button)\n            {\n                case SDL_BUTTON_LEFT:\n                {\n                    int x_val = 0;\n                    int y_val = 0;\n                    SDL_GetMouseState( &x_val, &y_val );\n                    printf(”mouse up x=%d y=%d\\n”, x_val, y_val);\n                    break;\n                }\n                default:\n                {\n                    break;\n                }\n            }\n            break;\n        }\n```\n\n现在我们可以接收鼠标输入，让我们创建一个简单的用户界面按钮。\n\n# 创建按钮\n\n现在我们知道了如何使用 SDL 在 WebAssembly 中捕获鼠标输入，我们可以使用这些知识来创建一个可以被鼠标点击的按钮。我们需要做的第一件事是在`game.hpp`文件中创建一个`UIButton`类定义。我们的按钮将有一个以上的雪碧纹理相关联。按钮通常有一个悬停状态和一个点击状态，因此如果用户将鼠标光标悬停在按钮上，或者点击了按钮，我们将希望显示另一个版本的精灵:\n\n![](img/647d63a3-ca68-451a-9781-0dd05a7f5597.png)\n\nFigure 14.5: Button states\n\n为了捕捉这些事件，我们需要函数来检测鼠标是点击了我们的按钮还是悬停在按钮上。下面是我们的类定义:\n\n```cpp\nclass UIButton {\n    public:\n        bool m_Hover;\n        bool m_Click;\n        bool m_Active;\n        void (*m_Callback)();\n\n        SDL_Rect m_dest = {.x = 0, .y = 0, .w = 128, .h = 32 };\n        SDL_Texture *m_SpriteTexture;\n        SDL_Texture *m_ClickTexture;\n        SDL_Texture *m_HoverTexture;\n\n        UIButton( int x, int y,\n        char* file_name, char* hover_file_name, char* click_file_name,\n        void (*callback)() );\n\n        void MouseClick(int x, int y);\n        void MouseUp(int x, int y);\n        void MouseMove( int x, int y );\n        void KeyDown( SDL_Keycode key );\n        void RenderUI();\n};\n```\n\n前三个属性是按钮状态属性，告诉我们的渲染函数绘制什么精灵，或者如果按钮不活动，不绘制任何东西。如果是`true`，则`m_Hover`属性将导致我们的渲染器绘制`m_HoverTexture`。如果是`true`，则`m_Click`属性将导致我们的渲染器绘制`m_ClickTexture`。最后，`m_Active`，如果设置为`false`，将导致我们的渲染器不绘制任何东西。\n\n下面一行是指向我们回调的函数指针:\n\n```cpp\nvoid (*m_Callback)();\n```\n\n这个函数指针是在我们的构造函数中设置的，当有人点击按钮时，我们就调用这个函数。在函数指针之后，我们有了目标矩形，在构造函数运行之后，它将有按钮图像文件的位置、宽度和高度:\n\n```cpp\nSDL_Rect m_dest = {.x = 0, .y = 0, .w = 128, .h = 32 };\n```\n\n然后，我们有三个纹理。这些纹理用于在画布上绘制图像，并在渲染过程中根据我们之前讨论的状态标志进行选择:\n\n```cpp\nSDL_Texture *m_SpriteTexture;\nSDL_Texture *m_ClickTexture;\nSDL_Texture *m_HoverTexture;\n```\n\n接下来，我们有构造函数。该功能接受我们按钮的`x`和`y`屏幕坐标。之后，有三个字符串，这是我们将用来加载我们的纹理的三个 PNG 文件的位置。最后一个参数是回调函数的指针:\n\n```cpp\nUIButton( int x, int y,\n         char* file_name, char* hover_file_name, char* click_file_name,\n         void (*callback)() );\n```\n\n然后，根据鼠标的当前状态，我们调用`SDL_PollEvent`后需要调用三个函数:\n\n```cpp\nvoid MouseClick(int x, int y);\nvoid MouseUp(int x, int y);\nvoid MouseMove( int x, int y );\n```\n\n`KeyDown`功能如果按下一个键会取一个键码，如果键码和我们的热键匹配，我们想用它来代替用鼠标点击按钮:\n\n```cpp\nvoid KeyDown( SDL_Keycode key );\n```\n\n`RenderUI`功能类似于我们为其他对象创建的`Render`功能。`RenderUI`和`Render`的区别在于`Render`功能在将精灵渲染到屏幕上时会考虑相机位置。`RenderUI`功能将始终在画布空间中渲染:\n\n```cpp\nvoid RenderUI();\n```\n\n在下一节中，我们将创建用户界面状态信息来跟踪当前屏幕。\n\n# 屏幕状态\n\n在我们开始给游戏添加新屏幕之前，我们需要创建一些屏幕状态。我们将从`main.cpp`文件中对这些状态进行大部分管理。不同的屏幕状态将需要不同的输入，将运行不同的逻辑，以及不同的渲染功能。我们将在代码的最高级别管理所有这些，作为我们的游戏循环调用的函数。我们将从`game.hpp`文件中定义一个可能状态的列表作为枚举:\n\n```cpp\nenum SCREEN_STATE {\n    START_SCREEN = 0,\n    PLAY_SCREEN = 1,\n    PLAY_TRANSITION = 2,\n    GAME_OVER_SCREEN = 3,\n    YOU_WIN_SCREEN = 4\n};\n```\n\n您可能会注意到，尽管只有三个不同的屏幕，但我们总共有五种不同的屏幕状态。`START_SCREEN`和`PLAY_SCREEN`分别是开始画面和播放画面。`PLAY_TRANSITION`状态在`START_SCREEN`和`PLAY_SCREEN`之间切换屏幕，在游戏中逐渐消失，而不是突然切换。我们将在屏幕上使用两种不同的游戏状态。这些状态是`GAME_OVER_SCREEN`和`YOU_WIN_SCREEN`。这两种状态的唯一区别是游戏结束时显示的信息。\n\n# games.hpp 的更改\n\n我们需要对`game.hpp`文件进行一些额外的更改。除了我们的`UIButton`类，我们还需要添加一个`UISprite`类定义文件。`UISprite`只是一个在画布空间中绘制的普通图像。除了作为用户界面元素呈现的精灵之外，它没有任何功能。定义如下:\n\n```cpp\nclass UISprite {\n    public:\n        bool m_Active;\n        SDL_Texture *m_SpriteTexture;\n        SDL_Rect m_dest = {.x = 0, .y = 0, .w = 128, .h = 32 };\n        UISprite( int x, int y, char* file_name );\n        void RenderUI();\n};\n```\n\n像按钮一样，它有一个由`m_Active`属性表示的活动状态。如果该值为假，精灵将不会呈现。它还有一个精灵纹理和一个目标属性，告诉渲染器绘制什么和在哪里绘制:\n\n```cpp\nSDL_Texture *m_SpriteTexture;\nSDL_Rect m_dest = {.x = 0, .y = 0, .w = 128, .h = 32 };\n```\n\n它有一个简单的构造器，接受我们将在画布上渲染精灵的`x`和`y`坐标，以及我们将从中加载精灵的虚拟文件系统中的图像文件名:\n\n```cpp\nUISprite( int x, int y, char* file_name );\n```\n\n最后，它有一个名为`RenderUI`的渲染函数，可以将精灵渲染到画布上:\n\n```cpp\nvoid RenderUI();\n```\n\n# 修改渲染管理器类\n\n`RenderManager`类将需要一个新的属性和一个新的函数。在我们游戏的早期版本中，我们有一种可以渲染的背景，那就是我们的滚动星域。当我们渲染我们的开始屏幕时，我想使用一个新的自定义背景，其中包括一些如何玩游戏的说明。\n\n以下是新版本的`RenderManager`类定义:\n\n```cpp\nclass RenderManager {\n    public:\n        const int c_BackgroundWidth = 800;\n        const int c_BackgroundHeight = 600;\n        SDL_Texture *m_BackgroundTexture;\n        SDL_Rect m_BackgroundDest = {.x = 0, .y = 0, .w = \n        c_BackgroundWidth, .h = c_BackgroundHeight };\n        SDL_Texture *m_StartBackgroundTexture;\n\n        RenderManager();\n        void RenderBackground();\n        void RenderStartBackground(int alpha = 255);\n        void Render( SDL_Texture *tex, SDL_Rect *src, SDL_Rect *dest, \n        float rad_rotation = 0.0,\n                     int alpha = 255, int red = 255, int green = 255, \n                     int blue = 255 );\n        void RenderUI( SDL_Texture *tex, SDL_Rect *src, SDL_Rect *dest, \n        float rad_rotation = 0.0,\n                       int alpha = 255, int red = 255, int green = 255, \n                       int blue = 255 );\n};\n```\n\n我们添加了一个新的`SDL_Texture`，我们将使用它来渲染开始屏幕中的背景图像:\n\n```cpp\nSDL_Texture *m_StartBackgroundTexture;\n```\n\n除了新属性之外，我们还添加了一个新功能，以便在启动屏幕激活时渲染该图像:\n\n```cpp\nvoid RenderStartBackground(int alpha = 255);\n```\n\n传递到该功能的α值将用于在`PLAY_TRANSITION`屏幕状态期间淡出开始屏幕。当玩家点击“播放”按钮时，过渡状态将开始，并持续大约一秒钟。\n\n# 新的外部变量\n\n我们需要添加三个新的`extern`变量定义，它们将引用我们在`main.cpp`文件中声明的变量。其中两个变量是指向`UISprite`对象的指针，其中一个变量是指向`UIButton`的指针。以下是三个`extern`的定义:\n\n```cpp\nextern UISprite *you_win_sprite;\nextern UISprite *game_over_sprite;\nextern UIButton* play_btn;\n```\n\n我们在屏幕上方的游戏中使用这两个`UISprite`指针。第一个，`you_win_sprite`，是玩家赢得游戏后会显示的精灵。第二个精灵`game_over_sprite`，是玩家输了会显示的精灵。最后一个变量`play_btn`是将显示在开始屏幕上的播放按钮。\n\n# 更改 main.cpp\n\n我们在游戏循环中管理新的屏幕状态。正因为如此，我们将对`main.cpp`文件进行大部分修改。我们需要将`input`功能分成三个新功能，每个游戏屏幕一个。我们需要将`render`功能分解为`start_render`和`play_render`功能。我们不需要`end_render`功能，因为当显示结束屏幕时，我们将继续使用`play_render`功能。\n\n我们还需要一个功能来显示开始屏幕和播放屏幕之间的转换。在游戏循环内部，我们需要根据当前屏幕添加逻辑来执行不同的循环逻辑。\n\n# 添加全局变量\n\n我们需要对`main.cpp`文件进行的第一个更改是添加新的全局变量。我们的用户界面精灵和按钮需要新的全局变量。我们将需要一个新的全局变量来表示当前的屏幕状态，状态之间的转换时间，以及一个标志来告诉我们玩家是否赢得了游戏。以下是我们在`main.cpp`文件中需要的新的全局变量:\n\n```cpp\nUIButton* play_btn;\nUIButton* play_again_btn;\nUISprite *you_win_sprite;\nUISprite *game_over_sprite;\nSCREEN_STATE current_screen = START_SCREEN;\nint transition_time = 0;\nbool you_win = false;\n```\n\n前两个变量是`UIButton`对象指针。第一个是`play_btn`，是用户点击开始玩游戏的开始画面按钮。第二个是`play_again_btn`，这是游戏结束画面上的一个按钮，玩家可以点击重新开始游戏。在 UIButtons 之后，我们有两个`UISprite`对象:\n\n```cpp\nUISprite *you_win_sprite;\nUISprite *game_over_sprite;\n```\n\n这些是显示在最终游戏屏幕上的精灵。显示哪个精灵取决于玩家是否摧毁了敌舰，反之亦然。在那些精灵之后，我们有一个`SCREEN_STATE`变量，用来跟踪当前的屏幕状态:\n\n```cpp\nSCREEN_STATE current_screen = START_SCREEN;\n```\n\n`transition_time`变量用于记录开始屏幕和播放屏幕之间过渡状态的剩余时间。`you_win`标志在游戏结束时设置，用于记录谁赢得了游戏。\n\n# 输入功能\n\n我们游戏的前一个版本有一个单一的`input`功能，使用`SDL_PollEvent`来轮询按键。在这个版本中，我们希望三种屏幕状态都有一个输入功能。我们首先要做的就是将原来的`input`功能`play_input`重新命名。这将不再是通用输入功能，它将只执行播放屏幕的输入功能。现在我们已经重命名了我们原来的输入函数，让我们为我们的开始屏幕定义输入函数，并将其称为`start_input`:\n\n```cpp\nvoid start_input() {\n    if(SDL_PollEvent( &event ) )\n    {\n        switch (event.type)\n        {\n            case SDL_MOUSEMOTION:\n            {\n                int x_val = 0;\n                int y_val = 0;\n                SDL_GetMouseState( &x_val, &y_val );\n                play_btn->MouseMove(x_val, y_val);\n            }\n            case SDL_MOUSEBUTTONDOWN:\n            {\n                switch (event.button.button)\n                {\n                    case SDL_BUTTON_LEFT:\n                    {\n                        int x_val = 0;\n                        int y_val = 0;\n                        SDL_GetMouseState( &x_val, &y_val );\n                        play_btn->MouseClick(x_val, y_val);\n                        break;\n                    }\n                    default:\n                    {\n                        break;\n                    }\n                }\n                break;\n            }\n            case SDL_MOUSEBUTTONUP:\n            {\n                switch (event.button.button)\n                {\n                    case SDL_BUTTON_LEFT:\n                    {\n                        int x_val = 0;\n                        int y_val = 0;\n                        SDL_GetMouseState( &x_val, &y_val );\n                        play_btn->MouseUp(x_val, y_val);\n                        break;\n                    }\n                    default:\n                    {\n                        break;\n                    }\n                }\n                break;\n            }\n            case SDL_KEYDOWN:\n            {\n                play_btn->KeyDown( event.key.keysym.sym );\n            }\n        }\n    }\n}\n```\n\n像我们的`play_input`功能一样，`start_input`功能将会调用`SDL_PollEvent`。除了检查`SDL_KEYDOWN`以确定某个键是否被按下，我们还将检查三个鼠标事件:`SDL_MOUSEMOTION`、`SDL_MOUSEBUTTONDOWN`和`SDL_MOUSEBUTTONUP`。当检查这些鼠标事件时，我们将根据检索到的`SDL_GetMouseState`值调用`play_btn`函数。鼠标事件将触发以下代码:\n\n```cpp\ncase SDL_MOUSEMOTION:\n{\n    int x_val = 0;\n    int y_val = 0;\n    SDL_GetMouseState( &x_val, &y_val );\n    play_btn->MouseMove(x_val, y_val);\n}\n```\n\n如果`event.type`是`SDL_MOUSEMOTION`，我们创建`x_val`和`y_val`整数变量，并使用对`SDL_GetMouseState`的调用来检索鼠标光标的`x`和`y`坐标。然后我们称之为`play_btn->MouseMove(x_val, y_val)`。这将鼠标 x 和 y 坐标传递给播放按钮，播放按钮使用这些值来确定按钮是否处于悬停状态。如果`event.type`是`SDL_MOUSEBUTTONDOWN`，我们会做类似的事情:\n\n```cpp\ncase SDL_MOUSEBUTTONDOWN:\n{\n    switch (event.button.button)\n    {\n        case SDL_BUTTON_LEFT:\n        {\n            int x_val = 0;\n            int y_val = 0;\n\n            SDL_GetMouseState( &x_val, &y_val );\n            play_btn->MouseClick(x_val, y_val);\n            break;\n        }\n        default:\n        {\n            break;\n        }\n    }\n    break;\n}\n```\n\n如果按下鼠标按钮，我们看一下`event.button.button`的内部，看看被点击的按钮是否是鼠标左键。如果是，我们用`x_val`、`y_val`结合`SDL_GetMouseState`找到鼠标光标位置。我们用这些价值观来称呼`play_btn->MouseClick(x_val, y_val)`。`MouseClick`功能将确定按钮点击是否落在按钮内，如果是，它将调用按钮的回调功能。\n\n事件为`SDL_MOUSEBUTTONUP`时执行的代码与`SDL_MOUSEBUTTONDOWN`非常相似，不同的是它调用的是`play_btn->MouseUp`而不是`play_btn->MouseClick`:\n\n```cpp\ncase SDL_MOUSEBUTTONUP:\n{\n    switch (event.button.button)\n    {\n        case SDL_BUTTON_LEFT:\n        {\n            int x_val = 0;\n            int y_val = 0;\n\n            SDL_GetMouseState( &x_val, &y_val );\n            play_btn->MouseUp(x_val, y_val);\n            break;\n        }\n        default:\n        {\n            break;\n        }\n    }\n    break;\n}\n```\n\n除了鼠标事件，我们还将键盘事件传递给我们的按钮。这样做是为了让我们可以创建一个热键来触发回调:\n\n```cpp\ncase SDL_KEYDOWN:\n{\n    play_btn->KeyDown( event.key.keysym.sym );\n}\n```\n\n# 结束输入函数\n\n在`start_input`功能之后，我们将定义`end_input`功能。`end_input`功能与`start_input`功能非常相似。唯一显著的区别是`play_btn`对象被`play_again_btn`对象替换，这将有不同的回调和与之关联的 SDL 纹理:\n\n```cpp\nvoid end_input() {\n    if(SDL_PollEvent( &event ) )\n    {\n        switch(event.type)\n        {\n            case SDL_MOUSEMOTION:\n            {\n                int x_val = 0;\n                int y_val = 0;\n                SDL_GetMouseState( &x_val, &y_val );\n                play_again_btn->MouseMove(x_val, y_val);\n            }\n            case SDL_MOUSEBUTTONDOWN:\n            {\n                switch(event.button.button)\n                {\n                    case SDL_BUTTON_LEFT:\n                    {\n                        int x_val = 0;\n                        int y_val = 0;\n                        SDL_GetMouseState( &x_val, &y_val );\n                        play_again_btn->MouseClick(x_val, y_val);\n                        break;\n                    }\n                    default:\n                    {\n                        break;\n                    }\n                }\n                break;\n            }\n            case SDL_MOUSEBUTTONUP:\n            {\n                switch(event.button.button)\n                {\n                    case SDL_BUTTON_LEFT:\n                    {\n                        int x_val = 0;\n                        int y_val = 0;\n                        SDL_GetMouseState( &x_val, &y_val );\n                        play_again_btn->MouseUp(x_val, y_val);\n                        break;\n                    }\n                    default:\n                    {\n                        break;\n                    }\n                }\n                break;\n            }\n            case SDL_KEYDOWN:\n            {\n                printf(\"SDL_KEYDOWN\\n\");\n                play_again_btn->KeyDown( event.key.keysym.sym );\n            }\n        }\n    }\n}\n```\n\n# 渲染功能\n\n在我们游戏的早期版本中，我们只有一个渲染功能。现在，我们必须有一个渲染功能，我们的开始屏幕和我们的播放屏幕。现有的渲染器将成为我们新的播放屏幕渲染器，所以我们必须重命名`render`功能`play_render`。我们还需要为我们的启动屏幕添加一个名为`start_render`的渲染功能。该功能将渲染我们的新背景和`play_btn`。以下是`start_render`的代码:\n\n```cpp\nvoid start_render() {\n    render_manager->RenderStartBackground();\n    play_btn->RenderUI();\n}\n```\n\n# 碰撞函数\n\n需要对`collisions()`功能进行一些小的修改。当一艘玩家船或一艘敌人船被摧毁时，我们需要将当前屏幕改为游戏屏幕。根据哪艘船被摧毁，我们要么需要把它换成胜利画面，要么换成失败画面。这是我们碰撞功能的新版本:\n\n```cpp\nvoid collisions() {\n Asteroid* asteroid;\n std::vector<Asteroid*>::iterator ita;\n    if( player->m_CurrentFrame == 0 && player->CompoundHitTest( star ) ) {\n        player->m_CurrentFrame = 1;\n        player->m_NextFrameTime = ms_per_frame;\n        player->m_Explode->Run();\n        current_screen = GAME_OVER_SCREEN;\n        large_explosion_snd->Play();\n    }\n    if( enemy->m_CurrentFrame == 0 && enemy->CompoundHitTest( star ) ) {\n        enemy->m_CurrentFrame = 1;\n        enemy->m_NextFrameTime = ms_per_frame;\n        current_screen = YOU_WIN_SCREEN;\n        enemy->m_Explode->Run();\n        large_explosion_snd->Play();\n    }\n    Projectile* projectile;\n    std::vector<Projectile*>::iterator it;\n    for(it=projectile_pool->m_ProjectileList.begin(); \n    it!=projectile_pool->m_ProjectileList.end();it++){\n        projectile = *it;\n        if( projectile->m_CurrentFrame == 0 && projectile->m_Active ) {\n            for( ita = asteroid_list.begin(); ita!=asteroid_list.end(); \n            ita++ ) {\n                asteroid = *ita;\n                if( asteroid->m_Active ) {\n                    if( asteroid->HitTest( projectile ) ) {\n                        asteroid->ElasticCollision( projectile );\n                        projectile->m_CurrentFrame = 1;\n                        projectile->m_NextFrameTime = ms_per_frame;\n                        small_explosion_snd->Play();\n                    }\n                }\n            }\n            if( projectile->HitTest( star ) ){\n                projectile->m_CurrentFrame = 1;\n                projectile->m_NextFrameTime = ms_per_frame;\n                small_explosion_snd->Play();\n            }\n            else if( player->m_CurrentFrame == 0 &&\n                ( projectile->HitTest( player ) || player->CompoundHitTest( \n                 projectile ) ) ) {\n                if( player->m_Shield->m_Active == false ) {\n                    player->m_CurrentFrame = 1;\n                    player->m_NextFrameTime = ms_per_frame;\n                    current_screen = GAME_OVER_SCREEN;\n                    player->m_Explode->Run();\n                    large_explosion_snd->Play();\n                }\n                else {\n                    hit_snd->Play();\n                    player->ElasticCollision( projectile );\n                }\n                projectile->m_CurrentFrame = 1;\n                projectile->m_NextFrameTime = ms_per_frame;\n            }\n            else if( enemy->m_CurrentFrame == 0 &&\n                ( projectile->HitTest( enemy ) || enemy->CompoundHitTest( \n                 projectile ) ) ) {\n                if( enemy->m_Shield->m_Active == false ) {\n                    enemy->m_CurrentFrame = 1;\n                    enemy->m_NextFrameTime = ms_per_frame;\n                    current_screen = YOU_WIN_SCREEN;\n                    enemy->m_Explode->Run();\n                    large_explosion_snd->Play();\n                    enemy->m_Shield->m_ttl -= 1000;\n                }\n                else {\n                    enemy->ElasticCollision( projectile );\n                    hit_snd->Play();\n                }\n                projectile->m_CurrentFrame = 1;\n                projectile->m_NextFrameTime = ms_per_frame;\n            }\n        }\n    }\n    for( ita = asteroid_list.begin(); ita != asteroid_list.end(); ita++ ) {\n        asteroid = *ita;\n        if( asteroid->m_Active ) {\n            if( asteroid->HitTest( star ) ) {\n                asteroid->Explode();\n                small_explosion_snd->Play();\n            }\n        }\n        else { continue; }\n        if( player->m_CurrentFrame == 0 && asteroid->m_Active &&\n          ( asteroid->HitTest( player ) || player->CompoundHitTest( \n           asteroid ) ) ) {\n            if( player->m_Shield->m_Active == false ) {\n                player->m_CurrentFrame = 1;\n                player->m_NextFrameTime = ms_per_frame;\n\n                player->m_Explode->Run();\n                current_screen = GAME_OVER_SCREEN;\n                large_explosion_snd->Play();\n            }\n            else {\n                player->ElasticCollision( asteroid );\n                small_explosion_snd->Play();\n            }\n        }\n        if( enemy->m_CurrentFrame == 0 && asteroid->m_Active &&\n          ( asteroid->HitTest( enemy ) || enemy->CompoundHitTest( asteroid \n           ) ) ) {\n            if( enemy->m_Shield->m_Active == false ) {\n                enemy->m_CurrentFrame = 1;\n                enemy->m_NextFrameTime = ms_per_frame;\n\n                enemy->m_Explode->Run();\n                current_screen = YOU_WIN_SCREEN;\n                large_explosion_snd->Play();\n            }\n            else {\n                enemy->ElasticCollision( asteroid );\n                small_explosion_snd->Play();\n            }\n        }\n    }\n    Asteroid* asteroid_1;\n    Asteroid* asteroid_2;\n    std::vector<Asteroid*>::iterator ita_1;\n    std::vector<Asteroid*>::iterator ita_2;\n    for( ita_1 = asteroid_list.begin(); ita_1 != asteroid_list.end(); \n    ita_1++ ) {\n        asteroid_1 = *ita_1;\n        if( !asteroid_1->m_Active ) { continue; }\n        for( ita_2 = ita_1+1; ita_2 != asteroid_list.end(); ita_2++ ) {\n            asteroid_2 = *ita_2;\n            if( !asteroid_2->m_Active ) { continue; }\n            if(asteroid_1->HitTest(asteroid_2)) { \n            asteroid_1->ElasticCollision( asteroid_2 ); }\n        }\n    }\n}\n```\n\n你会注意到玩家被消灭的每一行，都有一个`player->m_Explode->Run()`的召唤。我们现在通过调用`current_screen = GAME_OVER_SCREEN`将屏幕设置为玩家丢失的屏幕。另一种方法是在`Ship`类中添加一个函数，该函数运行爆炸动画并设置游戏屏幕，但我选择通过在`main`函数中进行更改来修改更少的文件。如果我们使用这个项目不仅仅是为了演示，我可能会用另一种方式。\n\n我们对碰撞所做的其他改变是相似的。每当通过运行`enemy->m_Explode->Run()`功能消灭了一个敌人，我们就用一行将当前屏幕设置为“你赢了”屏幕，如下所示:\n\n```cpp\ncurrent_screen = YOU_WIN_SCREEN;\n```\n\n# 过渡状态\n\n从开始屏幕到游戏的突然转变可能会有点不和谐。为了使过渡更加平滑，我们将创建一个名为`draw_play_transition`的过渡功能，它将使用 alpha 渐变将我们的屏幕从开始屏幕过渡到游戏屏幕。这个函数是这样的:\n\n```cpp\nvoid draw_play_transition() {\n    transition_time -= diff_time;\n    if( transition_time <= 0 ) {\n        current_screen = PLAY_SCREEN;\n        return;\n    }\n    render_manager->RenderStartBackground(transition_time/4);\n}\n```\n\n该函数使用我们之前创建的`transition_time`全局变量，并减去自上一帧以来的时间(以毫秒为单位)。当绘制开始屏幕背景时，它使用该值除以 4 作为 alpha 值，以便在过渡到游戏时淡出。当过渡时间降至 0 以下时，我们将当前屏幕设置为播放屏幕。过渡开始时，我们将`transition_time`设置为 1020 毫秒，比一秒多一点。将该值除以 4 得到一个从 255(完全不透明度)过渡到 0(完全透明度)的值。\n\n# 游戏循环\n\n需要修改`game_loop`功能，为每个屏幕执行不同的逻辑。以下是游戏循环的新版本:\n\n```cpp\nvoid game_loop() {\n    current_time = SDL_GetTicks();\n    diff_time = current_time - last_time;\n    delta_time = diff_time / 1000.0;\n    last_time = current_time;\n    if( current_screen == START_SCREEN ) {\n        start_input();\n        start_render();\n    }\n    else if( current_screen == PLAY_SCREEN || current_screen == \n             PLAY_TRANSITION ) {\n        play_input();\n        move();\n        collisions();\n        play_render();\n        if( current_screen == PLAY_TRANSITION ) {\n            draw_play_transition();\n        }\n    }\n    else if( current_screen == YOU_WIN_SCREEN || current_screen == \n             GAME_OVER_SCREEN ) {\n        end_input();\n        move();\n        collisions();\n        play_render();\n        play_again_btn->RenderUI();\n        if( current_screen == YOU_WIN_SCREEN ) {\n            you_win_sprite->RenderUI();\n        }\n        else {\n            game_over_sprite->RenderUI();\n        }\n    }\n}\n```\n\n我们有新的分支逻辑，基于当前屏幕进行分支。如果当前屏幕是开始屏幕，则运行第一个`if`块。它运行`start_input`和`start_render`功能:\n\n```cpp\nif( current_screen == START_SCREEN ) {\n    start_input();\n    start_render();\n}\n```\n\n除了这段代码末尾的`PLAY_TRANSITION`周围的`if`块外，播放屏幕和播放过渡与原始游戏循环具有相同的逻辑。这通过调用我们前面定义的`draw_play_transition()`函数来绘制游戏过渡:\n\n```cpp\nelse if( current_screen == PLAY_SCREEN || current_screen == PLAY_TRANSITION ) {\n    play_input();\n    move();\n    collisions();\n    play_render();\n    if( current_screen == PLAY_TRANSITION ) {\n        draw_play_transition();\n    }\n}\n```\n\n这个函数的最后一段代码是为屏幕上的游戏准备的。如果当前屏幕为`YOU_WIN_SCREEN`，将渲染`you_win_sprite`；如果当前屏幕为`GAME_OVER_SCREEN`，将渲染`game_over_sprite`；\n\n```cpp\nelse if( current_screen == YOU_WIN_SCREEN || current_screen == \n         GAME_OVER_SCREEN ) {\n    end_input();\n    move();\n    collisions();\n    play_render();\n    play_again_btn->RenderUI();\n    if( current_screen == YOU_WIN_SCREEN ) {\n        you_win_sprite->RenderUI();\n    }\n    else {\n        game_over_sprite->RenderUI();\n    }\n}\n```\n\n# 播放并再次播放回调\n\n在我们对游戏循环进行更改后，我们需要为按钮添加一些回调函数。这些功能中的第一个是`play_click`功能。这是当玩家点击开始屏幕上的播放按钮时运行的回调。该功能将当前屏幕设置为播放过渡，并将过渡时间设置为 1，020 毫秒:\n\n```cpp\nvoid play_click() {\n    current_screen = PLAY_TRANSITION;\n    transition_time = 1020;\n}\n```\n\n之后，我们将定义`play_again_click`回调。当玩家点击游戏结束屏幕上的再次播放按钮时，该功能运行。因为这是一个网络游戏，我们将使用一个小技巧来简化这个逻辑。在为几乎任何其他平台编写的游戏中，您需要创建一些重新初始化逻辑，这些逻辑必须返回到您的游戏中，并重置所有内容的状态。我们将通过简单地使用 JavaScript 重新加载网页来欺骗:\n\n```cpp\nvoid play_again_click() {\n    EM_ASM(\n        location.reload();\n    );\n}\n```\n\n这种欺骗并不适用于所有游戏。重新加载一些游戏会导致不可接受的延迟。对于一些游戏来说，可能有太多的状态信息需要我们保存。然而，对于这个游戏来说，重新加载页面是一个快速简单的完成任务的方法。\n\n# 对主要功能的更改\n\n我们在应用中使用`main`函数来执行所有的游戏初始化。这是我们需要添加一些代码来初始化我们将在我们的游戏屏幕上使用的精灵和我们的新按钮的地方。\n\n在下面的代码片段中，我们有了新的 sprite 初始化行:\n\n```cpp\ngame_over_sprite = new UISprite( 400, 300, (char*)\"/sprites/GameOver.png\" );\ngame_over_sprite->m_Active = true;\nyou_win_sprite = new UISprite( 400, 300, (char*)\"/sprites/YouWin.png\" );\nyou_win_sprite->m_Active = true;\n```\n\n您可以看到我们正在将`game_over_sprite`坐标和`you_win_sprite`坐标设置为`400, 300`。这将把这些精灵放在屏幕的中央。我们将这两个精灵都设置为活动的，因为它们无论如何都只会在最终游戏屏幕上呈现。在后面的代码中，我们将调用`UIButton`对象的构造函数:\n\n```cpp\nplay_btn = new UIButton(400, 500,\n                     (char*)\"/sprites/play_button.png\",\n                     (char*)\"/sprites/play_button_hover.png\",\n                     (char*)\"/sprites/play_button_click.png\",\n                     play_click );\n\nplay_again_btn = new UIButton(400, 500,\n                     (char*)\"/sprites/play_again_button.png\",\n                     (char*)\"/sprites/play_again_button_hover.png\",\n                     (char*)\"/sprites/play_again_button_click.png\",\n                     play_again_click );\n```\n\n这将这两个按钮放在`400, 500`上，以 x 轴为中心，但靠近 y 轴上的游戏屏幕底部。回调被设置为`play_click`和`play_again_click`，这是我们之前定义的。以下是整个`main`功能的外观:\n\n```cpp\nint main() {\n    SDL_Init( SDL_INIT_VIDEO | SDL_INIT_AUDIO );\n    int return_val = SDL_CreateWindowAndRenderer( CANVAS_WIDTH, \n    CANVAS_HEIGHT, 0, &window, &renderer );\n    if( return_val != 0 ) {\n        printf(\"Error creating renderer %d: %s\\n\", return_val, \n        IMG_GetError() );\n        return 0;\n    }\n    SDL_SetRenderDrawColor( renderer, 0, 0, 0, 255 );\n    SDL_RenderClear( renderer );\n    game_over_sprite = new UISprite( 400, 300, \n    (char*)\"/sprites/GameOver.png\" );\n    game_over_sprite->m_Active = true;\n    you_win_sprite = new UISprite( 400, 300, \n    (char*)\"/sprites/YouWin.png\" );\n    you_win_sprite->m_Active = true;\n    last_frame_time = last_time = SDL_GetTicks();\n    player = new PlayerShip();\n    enemy = new EnemyShip();\n    star = new Star();\n    camera = new Camera(CANVAS_WIDTH, CANVAS_HEIGHT);\n    render_manager = new RenderManager();\n    locator = new Locator();\n    enemy_laser_snd = new Audio(ENEMY_LASER, false);\n    player_laser_snd = new Audio(PLAYER_LASER, false);\n    small_explosion_snd = new Audio(SMALL_EXPLOSION, true);\n    large_explosion_snd = new Audio(LARGE_EXPLOSION, true);\n    hit_snd = new Audio(HIT, false);\n    device_id = SDL_OpenAudioDevice(NULL, 0, &(enemy_laser_snd->spec), \n    NULL, 0);\n    if (device_id == 0) {\n        printf(\"Failed to open audio: %s\\n\", SDL_GetError());\n    }\n    SDL_PauseAudioDevice(device_id, 0);\n    int asteroid_x = 0;\n    int asteroid_y = 0;\n    int angle = 0;\n    // SCREEN 1\n    for( int i_y = 0; i_y < 8; i_y++ ) {\n        asteroid_y += 100;\n        asteroid_y += rand() % 400;\n        asteroid_x = 0;\n        for( int i_x = 0; i_x < 12; i_x++ ) {\n            asteroid_x += 66;\n            asteroid_x += rand() % 400;\n            int y_save = asteroid_y;\n            asteroid_y += rand() % 400 - 200;\n            angle = rand() % 359;\n            asteroid_list.push_back(\n            new Asteroid( asteroid_x, asteroid_y,\n                          get_random_float(0.5, 1.0),\n                          DEG_TO_RAD(angle) ) );\n            asteroid_y = y_save;\n        }\n    }\n    projectile_pool = new ProjectilePool();\n    play_btn = new UIButton(400, 500,\n                     (char*)\"/sprites/play_button.png\",\n                     (char*)\"/sprites/play_button_hover.png\",\n                     (char*)\"/sprites/play_button_click.png\",\n                     play_click );\n    play_again_btn = new UIButton(400, 500,\n                     (char*)\"/sprites/play_again_button.png\",\n                     (char*)\"/sprites/play_again_button_hover.png\",\n                     (char*)\"/sprites/play_again_button_click.png\",\n                     play_again_click );\n    emscripten_set_main_loop(game_loop, 0, 0);\n    return 1;\n}\n```\n\n在下一节中，我们将在我们的`ui_button.cpp`文件中定义函数。\n\n# ui_button.cpp\n\n`UIButton`对象有几个必须定义的功能。我们已经创建了一个新的`ui_button.cpp`文件来保存所有这些新功能。我们需要定义一个构造函数，以及`MouseMove`、`MouseClick`、`MouseUp`、`KeyDown`和`RenderUI`。\n\n首先，我们将包括我们的`game.hpp`文件:\n\n```cpp\n#include \"game.hpp\"\n```\n\n现在，我们将定义我们的构造函数:\n\n```cpp\nUIButton::UIButton( int x, int y, char* file_name, char* hover_file_name, char* click_file_name, void (*callback)() ) {\n    m_Callback = callback;\n    m_dest.x = x;\n    m_dest.y = y;\n    SDL_Surface *temp_surface = IMG_Load( file_name );\n\n    if( !temp_surface ) {\n        printf(\"failed to load image: %s\\n\", IMG_GetError() );\n        return;\n    }\n    else {\n        printf(\"success creating ui button surface\\n\");\n    }\n    m_SpriteTexture = SDL_CreateTextureFromSurface( renderer, \n    temp_surface );\n    if( !m_SpriteTexture ) {\n        return;\n    }\n    SDL_QueryTexture( m_SpriteTexture,\n                        NULL, NULL,\n                        &m_dest.w, &m_dest.h );\n    SDL_FreeSurface( temp_surface );\n\n     temp_surface = IMG_Load( click_file_name );\n    if( !temp_surface ) {\n        printf(\"failed to load image: %s\\n\", IMG_GetError() );\n        return;\n    }\n    else {\n        printf(\"success creating ui button click surface\\n\");\n    }\n    m_ClickTexture = SDL_CreateTextureFromSurface( renderer, \n    temp_surface );\n\n    if( !m_ClickTexture ) {\n        return;\n    }\n    SDL_FreeSurface( temp_surface );\n\n    temp_surface = IMG_Load( hover_file_name );\n    if( !temp_surface ) {\n        printf(\"failed to load image: %s\\n\", IMG_GetError() );\n        return;\n    }\n    else {\n        printf(\"success creating ui button hover surface\\n\");\n    }\n    m_HoverTexture = SDL_CreateTextureFromSurface( renderer, \n    temp_surface );\n\n    if( !m_HoverTexture ) {\n        return;\n    }\n    SDL_FreeSurface( temp_surface );\n\n    m_dest.x -= m_dest.w / 2;\n    m_dest.y -= m_dest.h / 2;\n\n    m_Hover = false;\n    m_Click = false;\n    m_Active = true;\n}\n```\n\n构造函数从设置传入参数的回调函数开始:\n\n```cpp\nm_Callback = callback;\n```\n\n然后，它根据我们传入的参数设置`m_dest`矩形的`x`和`y`坐标:\n\n```cpp\nm_dest.x = x;\nm_dest.y = y;\n```\n\n之后，它将三个不同的图像文件加载到按钮的三个不同纹理中，即按钮的悬停状态和按钮的单击状态:\n\n```cpp\nSDL_Surface *temp_surface = IMG_Load( file_name );\n\nif( !temp_surface ) {\n    printf(\"failed to load image: %s\\n\", IMG_GetError() );\n    return;\n}\nelse {\n    printf(\"success creating ui button surface\\n\");\n}\nm_SpriteTexture = SDL_CreateTextureFromSurface( renderer, temp_surface );\n\nif( !m_SpriteTexture ) {\n    return;\n}\nSDL_QueryTexture( m_SpriteTexture,\n                  NULL, NULL,\n                  &m_dest.w, &m_dest.h );\nSDL_FreeSurface( temp_surface );\n\ntemp_surface = IMG_Load( click_file_name );\n\nif( !temp_surface ) {\n    printf(\"failed to load image: %s\\n\", IMG_GetError() );\n    return;\n}\nelse {\n    printf(\"success creating ui button click surface\\n\");\n}\nm_ClickTexture = SDL_CreateTextureFromSurface( renderer, temp_surface );\n\nif( !m_ClickTexture ) {\n    return;\n}\nSDL_FreeSurface( temp_surface );\n\ntemp_surface = IMG_Load( hover_file_name );\nif( !temp_surface ) {\n    printf(\"failed to load image: %s\\n\", IMG_GetError() );\n    return;\n}\nelse {\n    printf(\"success creating ui button hover surface\\n\");\n}\nm_HoverTexture = SDL_CreateTextureFromSurface( renderer, temp_surface );\n\nif( !m_HoverTexture ) {\n    return;\n}\nSDL_FreeSurface( temp_surface );\n```\n\n前面的代码看起来应该很熟悉，因为加载一个图像文件到一个`SDL_Texture`对象中是我们已经做了很多的事情。之后，我们使用前面查询的宽度和高度值来居中目标矩形:\n\n```cpp\nm_dest.x -= m_dest.w / 2;\nm_dest.y -= m_dest.h / 2;\n```\n\n然后，我们设置悬停、点击和活动状态标志:\n\n```cpp\nm_Hover = false;\nm_Click = false;\nm_Active = true;\n```\n\n# 鼠标移动功能\n\n我们需要一个函数来确定鼠标光标是否已经移动到我们的按钮上。我们从输入函数中调用`MouseMove`函数，然后传入当前鼠标光标`x`和`y`坐标。我们对照我们的`m_dest`矩形检查这些坐标，看它们是否重叠。如果是这样，我们将悬停旗设置为`true`。如果没有，我们将悬停标志设置为`false`:\n\n```cpp\nvoid UIButton::MouseMove(int x, int y) {\n    if( x >= m_dest.x && x <= m_dest.x + m_dest.w &&\n        y >= m_dest.y && y <= m_dest.y + m_dest.h ) {\n        m_Hover = true;\n    }\n    else {\n        m_Hover = false;\n    }\n}\n```\n\n# 鼠标点击功能\n\n`MouseClick`功能与`MouseMove`功能非常相似。当用户按下鼠标左键时，我们的输入函数也会调用它。鼠标光标的`x`和`y`坐标被传入，该功能使用`m_dest`矩形查看鼠标光标在点击按钮时是否在按钮上方。如果是，我们将点击标志设置为`true`。如果没有，我们将点击标志设置为`false`:\n\n```cpp\nvoid UIButton::MouseClick(int x, int y) {\n    if( x >= m_dest.x && x <= m_dest.x + m_dest.w &&\n        y >= m_dest.y && y <= m_dest.y + m_dest.h ) {\n        m_Click = true;\n    }\n    else {\n        m_Click = false;\n    }\n}\n```\n\n# MouseUp 函数\n\n当鼠标左键被释放时，我们调用这个函数。无论鼠标光标坐标是什么，我们都要将点击标志设置为`false`。如果释放按钮时鼠标在按钮上，并且按钮被点击，我们需要调用回调函数:\n\n```cpp\nvoid UIButton::MouseUp(int x, int y) {\n    if( m_Click == true &&\n        x >= m_dest.x && x <= m_dest.x + m_dest.w &&\n        y >= m_dest.y && y <= m_dest.y + m_dest.h ) {\n        if( m_Callback != NULL ) {\n            m_Callback();\n        }\n    }\n    m_Click = false;\n}\n```\n\n# 按键功能\n\n我本可以让按键功能更灵活一点。最好将热键设置为在对象中设置的值。支持屏幕上不止一个按钮。事实上，如果有人点击*进入*键，屏幕上的所有按钮都会被点击。这对于我们的游戏来说不是问题，因为我们不会在一个屏幕上有一个以上的按钮，但是如果你想提高热键功能，这应该不会太难。就功能而言，它会将正在检查的键硬编码到`SDLK_RETURN`。以下是我们拥有的功能版本:\n\n```cpp\nvoid UIButton::KeyDown( SDL_Keycode key ) {\n    if( key == SDLK_RETURN) {\n        if( m_Callback != NULL ) {\n            m_Callback();\n        }\n    }\n}\n```\n\n# RenderUI 函数\n\n`RenderUI`功能检查按钮中的各种状态标志，并根据这些值呈现正确的子画面。如果`m_Active`标志为`false`，则该功能不渲染任何内容。以下是功能:\n\n```cpp\nvoid UIButton::RenderUI() {\n    if( m_Active == false ) {\n        return;\n    }\n    if( m_Click == true ) {\n        render_manager->RenderUI( m_ClickTexture, NULL, &m_dest, 0.0,\n                                    0xff, 0xff, 0xff, 0xff );\n    }\n    else if( m_Hover == true ) {\n        render_manager->RenderUI( m_HoverTexture, NULL, &m_dest, 0.0,\n                                    0xff, 0xff, 0xff, 0xff );\n    }\n    else {\n        render_manager->RenderUI( m_SpriteTexture, NULL, &m_dest, 0.0,\n                                    0xff, 0xff, 0xff, 0xff );\n    }\n}\n```\n\n在下一节中，我们将在我们的`ui_sprite.cpp`文件中定义函数。\n\n# ui_sprite.cpp\n\n`UISprite`类相当简单。它只有两个功能:构造函数和呈现函数。与我们项目中的其他所有 CPP 文件一样，我们必须做的第一件事是包含`game.hpp`文件:\n\n```cpp\n#include \"game.hpp\"\n```\n\n# 定义构造函数\n\n构造函数非常熟悉。它将`m_dest`矩形的`x`和`y`值设置为传递给构造函数的值。它使用我们作为参数传入的`file_name`变量从虚拟文件系统加载纹理。最后，它使用使用`SDL_QueryTexture`函数检索的宽度和高度值使`m_dest`矩形居中。下面是构造函数的代码:\n\n```cpp\nUISprite::UISprite( int x, int y, char* file_name ) {\n    m_dest.x = x;\n    m_dest.y = y;\n    SDL_Surface *temp_surface = IMG_Load( file_name );\n\n    if( !temp_surface ) {\n        printf(\"failed to load image: %s\\n\", IMG_GetError() );\n        return;\n    }\n    else {\n        printf(\"success creating ui button surface\\n\");\n    }\n\n    m_SpriteTexture = SDL_CreateTextureFromSurface( renderer, \n    temp_surface );\n\n    if( !m_SpriteTexture ) {\n        return;\n    }\n    SDL_QueryTexture( m_SpriteTexture,\n                      NULL, NULL,\n                      &m_dest.w, &m_dest.h );\n    SDL_FreeSurface( temp_surface );\n    m_dest.x -= m_dest.w / 2;\n    m_dest.y -= m_dest.h / 2;\n}\n```\n\n# RenderUI 函数\n\n我们雪碧的`RenderUI`功能也很简单。它检查精灵是否是活动的，如果是，调用渲染管理器的`RenderUI`功能。下面是代码:\n\n```cpp\nvoid UISprite::RenderUI() {\n    if( m_Active == false ) {\n        return;\n    }\n    render_manager->RenderUI( m_SpriteTexture, NULL, &m_dest, 0.0,\n                              0xff, 0xff, 0xff, 0xff );\n}\n```\n\n# 编译 ui.html\n\n现在我们已经给我们的游戏添加了一个用户界面，让我们编译它，从我们的 web 服务器或 emrun 提供它，并在 web 浏览器中打开它。下面是我们编译`ui.html`文件所需的`em++ `命令:\n\n```cpp\nem++ asteroid.cpp audio.cpp camera.cpp collider.cpp emitter.cpp enemy_ship.cpp finite_state_machine.cpp locator.cpp main.cpp particle.cpp player_ship.cpp projectile_pool.cpp projectile.cpp range.cpp render_manager.cpp shield.cpp ship.cpp star.cpp ui_button.cpp ui_sprite.cpp vector.cpp -o ui.html --preload-file audio --preload-file sprites -std=c++ 17 -s USE_WEBGL2=1 -s USE_SDL=2 -s USE_SDL_IMAGE=2 -s SDL2_IMAGE_FORMATS=[\"png\"] -s USE_SDL_IMAGE=2 -s SDL2_IMAGE_FORMATS=[\"png\"] \n```\n\n新版本将打开我们的开始屏幕。如果你想玩游戏，你现在需要点击*播放*按钮。下面是截图:\n\n![](img/af0aed53-ba7c-4980-82e7-52d72d32ed5a.png)\n\nFigure 14.6: Opening screen\n\n你会注意到*开屏*有如何玩游戏的说明。在面向动作的网络游戏中有一个打开的屏幕通常是好的，因为当页面加载时，玩家并不总是准备好玩。并不是所有的网络游戏都需要打开屏幕。我的网站，[classicsolitaire.com](https://www.classicsolitaire.com/)，一个都没有。这是因为接龙是一个基于回合的游戏，玩家不会马上投入到游戏中。您的游戏的用户界面需求可能与我们为这本书编写的游戏不同。所以，画一个故事板，花时间收集需求。你会很高兴你做到了。\n\n# 摘要\n\n在本章中，我们花了一些时间收集用户界面的需求。我们创建了一个故事板来帮助我们思考我们的游戏需要什么屏幕，以及它们可能是什么样子。我们讨论了我们的开屏布局，以及我们为什么需要它。然后，我们将原本是我们整个游戏的屏幕变成了游戏屏幕。然后，我们讨论了游戏在屏幕上的布局以及需要哪些用户界面元素，并学习了如何使用 SDL 来检索鼠标输入。我们还创建了一个按钮类作为用户界面的一部分，以及一个屏幕状态的枚举，并讨论了这些状态之间的转换。然后，我们添加了一个 sprite 用户界面对象，然后修改我们的渲染管理器，以允许我们渲染我们的开始屏幕的背景图像。最后，我们对代码进行了修改，以支持多个游戏屏幕。\n\n在下一章中，我们将学习如何编写新的着色器，并使用网络组件的 OpenGL 应用编程接口来实现它们。"
  },
  {
    "path": "docs/handson-game-dev-wasm/15.md",
    "content": "# 十五、着色器和 2D 照明\n\n我们已经在 [第 3 章](03.html)*中谈到了着色器，介绍了 WebGL* 。不幸的是，SDL 不允许用户定制它的着色器，除非深入库的源代码并在那里修改它们。这些修改超出了\n\n这本书的范围。结合 OpenGL 使用 SDL 并不少见。SDL 可以用来渲染游戏的用户界面，而 OpenGL 渲染游戏对象。这一章将与前面的许多章节有所不同，因为我们不会在已经编写的游戏中直接混合 SDL 和 OpenGL。更新游戏以支持 OpenGL 2D 渲染引擎将需要对游戏进行彻底的重新设计。然而，我想为那些对创建更高级的 2D 渲染引擎感兴趣的人提供一章，让他们通过结合 OpenGL 和 SDL 并为该引擎编写着色器来涉猎。\n\nYou will need to include several images in your build to make this project work. Make sure that you include the `/Chapter15/sprites/` folder from this project's GitHub repository. If you haven't downloaded the GitHub project yet, you can get it online here: [https://github.com/PacktPublishing/Hands-On-Game-Development-with-WebAssembly](https://github.com/PacktPublishing/Hands-On-Game-Development-with-WebAssembly).\n\n在本章中，我们将执行以下操作:\n\n*   重新创建我们在[第 3 章](https://cdp.packtpub.com/hands_on_game_development_with_webassembly/wp-admin/post.php?post=38&action=edit#post_26)*中制作的应用，使用 SDL 和 OpenGL 的组合进行 WebAssembly*\n*   了解如何创建一个新的着色器，将多个纹理加载并渲染到一个四边形中\n*   了解普通地图，以及如何在 2D 游戏对象上创建深度错觉\n*   了解如何在 OpenGL 和 WebAssembly 中使用普通地图来近似 2D 的 Phong 照明模型\n\n# 将 OpenGL 与网络组件结合使用\n\nEmscripten 能够通过将这些调用分别映射到 WebGL 或 WebGL 2 调用来编译使用 OpenGL ES 2.0 或 OpenGL ES 3.0 的 C/C++ 代码。因此，Emscripten 只支持 OpenGL ES 命令的一个子集，该子集对应于您使用的 WebGL 库中可用的命令。例如，如果您想使用 OpenGL ES 3.0，您需要在编译时通过将`-s USE_WEBGL2=1`参数传递给 Emscripten 编译器来包含 WebGL 2。在本章中，我们将使用 OpenGL ES 2.0 结合 SDL 使用着色器渲染子画面，稍后我们将使用 SDL 渲染一个图标，该图标代表我们应用中光源的位置。SDL 提供了许多 OpenGL 没有的功能，比如音频库、图像加载库以及鼠标和键盘输入库。在许多方面，SDL 更适合渲染游戏的用户界面，因为它将对象渲染到屏幕坐标，而不是 OpenGL 剪辑空间。在幕后，SDL 的 WebAssembly 版本也在使用依赖于 WebGL 的 Emscripten OpenGL ES 实现。因此，更好地理解 WebAssembly 的 OpenGL 实现可以帮助我们将我们的游戏开发技能提升到一个新的水平，即使我们不会在我们为这本书开发的游戏中使用这些技能。\n\n# 关于着色器的更多信息\n\n我们在[第 2 章](02.html)、 *HTML5 和*中简单介绍了着色器的概念。着色器是现代三维图形渲染的关键部分。回到计算机和视频游戏的早期，图形都是 2D 的，图形渲染的速度取决于系统将像素从一个数据缓冲区移动到另一个数据缓冲区的速度。这个过程叫做*无忧无虑*。早期的一个重大进步是任天堂在其任天堂娱乐系统中增加了一个**图像处理单元** ( **PPU** )。这是一个早期的硬件，旨在通过移动像素来加速图形处理，而不使用游戏系统的中央处理器。阿米加准将也是这些早期 2D 图形协处理器的先驱，到 20 世纪 90 年代中期，用于传输的硬件成为计算机行业的标准。1996 年，像 Quake 这样的游戏开始创造消费者对 3D 图形处理的需求，早期的显卡开始提供具有固定功能流水线的 GPU。这允许应用加载几何数据，并在该几何上执行不可编程的纹理和照明功能。21 世纪初，英伟达推出了 GeForce 3。这是第一个支持可编程流水线的图形处理器。最终，这些可编程流水线图形处理器开始围绕*统一着色器模型*进行标准化，该模型允许程序员为所有支持着色语言的显卡编写 GLSL 等着色语言。\n\n# GLSL ES 1.0 和 3.0\n\n我们将用来编写着色器的语言是 GLSL 着色器语言的一个子集，称为 GLSL ES。这种着色器语言恰好可以与 WebGL 一起工作，因此被移植到 WebAssembly 的 OpenGL ES 版本所支持。我们正在编写的代码将在 GLSL ES 1.0 和 3.0 上运行，这是 WebAssembly 支持的 GLSL ES 的两个版本。\n\nIf you are wondering why there is no support for GLSL ES 2.0, it's because it doesn't exist. OpenGL ES 1.0 used a fixed function pipeline and so it had no shader language associated with it. When the Khronos Group created OpenGL ES 2.0, they created GLSL ES 1.0 as the shader language to go with it. When they released OpenGL ES 3.0, they decided that they wanted the version number of the shader language to be the same number as the API. Therefore, all the new versions of OpenGL ES will come with a version of GLSL that bears the same version number.\n\nGLSL 是一种非常类似于 c 语言的语言。每个着色器都有一个`main`函数作为其入口点。GLSL ES 2.0 仅支持两种着色器类型:*顶点着色器*和*片段着色器*。这些着色器的执行是高度并行的。如果你习惯于单线程思维，你需要重新安排你的大脑。着色器经常同时处理数千个顶点和像素。\n\nI briefly discussed the definition of a vertex and a fragment in [Chapter 3](03.html), *Introduction to WebGL*. A vertex is a point in space, and a collection of vertices define the geometry that our graphics card uses to render to the screen. A fragment is a pixel candidate. Multiple fragments usually go into determining the pixel output.\n\n传递给顶点着色器的几何图形的每个顶点都由该着色器处理。然后使用*变量*将值传递给大量通过片段着色器处理单个像素的线程。片段着色器接收在多个顶点着色器的输出之间插值的值。片段着色器的输出是一个*片段*，它是一个候选像素。不是所有的碎片都会变成像素。有些片段被丢弃，这意味着它们根本不会渲染。其他片段混合在一起，形成完全不同的像素颜色。我们在[第 3 章](03.html)*中为我们的 WebGL 应用创建了一个顶点和一个片段着色器。让我们逐步将该应用转换为 OpenGL/WebAssembly 应用。一旦我们有了一个可用的应用，我们就可以进一步讨论着色器以及编写这些着色器以改进我们的 2D WebAssembly 游戏的新方法。*\n\n# WebGL 应用 redux\n\n现在，我们将使用 SDL 和 OpenGL，演示如何重写我们在[第三章](03.html)、*中制作的 WebGL 应用。如果你不记得了，这是一个非常简单的应用，它在我们的画布上画了一艘飞船，并向左移动了 2 个像素，每帧向上移动一个像素。我们制作这个应用的原因是，这是我认为在 WebGL 中可以做的最简单的事情，比画三角形更有趣。出于同样的原因，这将是我们使用 OpenGL 进行 WebAssembly 的第一件事。继续创建一个名为`webgl-redux.c`的新文件并打开它。现在，让我们开始添加一些代码。我们需要的第一个代码块是我们的`#include`命令，用于引入这个应用所需的所有库:*\n\n```cpp\n#include <SDL2/SDL.h>\n#include <SDL2/SDL_image.h>\n#include <SDL_opengl.h>\n#include <GLES2/gl2.h>\n#include <stdlib.h>\n#include <emscripten.h>\n```\n\n第一行包括标准的 SDL2 库。第二个库`SDL_image.h`，是我们用来加载图像文件的库。这个文件的第三行包括`SDL_opengl.h`，是允许我们混合 SDL 和 OpenGL 调用的库。包括`GLES2/gl2.h`让我们可以访问所有的 OpenGL 命令，我们可以使用 OpenGL ES 2.0。像往常一样，我们包含`stdlib.h`让我们使用`printf`命令，`emscripten.h`为我们提供了使用 Emscripten 编译器编译到目标网络程序集所需的功能。\n\n在我们的`#include`命令之后，我们有一系列的`#define`宏来定义我们的游戏需要的常量:\n\n```cpp\n#define CANVAS_WIDTH 800\n#define CANVAS_HEIGHT 600\n#define FLOAT32_BYTE_SIZE 4\n#define STRIDE FLOAT32_BYTE_SIZE*4\n```\n\n前两个定义了我们的画布宽度和画布高度。剩余的`#define`调用用于设置我们在定义顶点缓冲区时将使用的值。在这些`#define`宏之后，我们为着色器定义代码。\n\n# 着色器代码\n\n下面我将要展示的几个代码块将定义我们创建 2D 照明效果所需的着色器。以下是顶点着色器代码:\n\n```cpp\nconst GLchar* vertex_shader_code[] = {\n    \"precision mediump float; \\n\"\n    \"attribute vec4 a_position; \\n\"\n    \"attribute vec2 a_texcoord; \\n\"\n\n    \"uniform vec4 u_translate; \\n\"\n\n    \"varying vec2 v_texcoord; \\n\"\n\n    \"void main() { \\n\"\n        \"gl_Position = u_translate + a_position; \\n\"\n        \"v_texcoord = a_texcoord; \\n\"\n    \"} \\n\"\n};\n```\n\n这是我们在创建这个应用的 WebGL 版本时使用的同一着色器代码。在 C 语言中看起来有些不同，因为 JavaScript 可以使用多行字符串，这使得代码的阅读更加清晰。像在 WebGL 版本中一样，我们使用精度调用将浮点精度设置为中等。我们设置属性来接收位置和紫外线纹理坐标数据作为向量。我们将使用顶点缓冲对象来传递这些向量。我们定义了一个统一的翻译变量，该变量将与所有顶点使用的值相同，这通常不是我们在游戏中使用的方式，但对于这个应用来说会很好。最后，我们定义一个可变的`v_texcoord`变量。这个变量将代表我们从顶点着色器传递到片段着色器的纹理坐标值。这个顶点着色器中的`main()`函数非常简单。它将传递到顶点着色器的`u_translate`统一变量平移值添加到通过`a_position`传递的顶点的属性位置，以获得我们使用`gl_Position`变量设置的最终顶点位置。之后，我们通过将`v_texcoord`变量设置为`a_texcoord`将顶点的纹理坐标传递给片段着色器。\n\n定义顶点着色器后，我们创建定义片段着色器的字符串。片段着色器接收`v_texcoord`的插值版本，这是从顶点着色器传递出来的可变变量。你需要戴上你的并行处理帽子一会儿来理解这是如何工作的。当 GPU 处理我们的顶点着色器和片段着色器时，它不是一次处理一个，而是可能一次处理数千个顶点和片段。片段着色器也不是从这些线程中的单个线程接收输出，而是从当前正在处理的多个顶点混合的值。\n\n例如，如果您的顶点着色器有一个名为 X 的可变变量作为输出，并且您的片段位于 X 为 0 的顶点和 X 为 10 的顶点之间，那么进入片段的可变变量中的值将是 5。这是因为 5 位于 0 和 10 这两个顶点值的中间。同样，如果片段位于两点之间的 30%，X 中的值将是 3。\n\n以下是片段着色器代码的定义:\n\n```cpp\nconst GLchar* fragment_shader_code[] = {\n    \"precision mediump float; \\n\"\n    \"varying vec2 v_texcoord; \\n\"\n\n    \"uniform sampler2D u_texture; \\n\"\n\n    \"void main() { \\n\"\n        \"gl_FragColor = texture2D(u_texture, v_texcoord); \\n\"\n    \"} \\n\"\n };\n```\n\n与顶点材质球一样，我们从设置精度开始。之后，我们有一个变化的变量，这是一个插值为我们的纹理坐标。该值存储在`v_texcoord`中，并将用于将我们的纹理映射到像素颜色。最后一个变量是类型为`sampler2D`的均匀变量。这是一块内存，我们已经加载了我们的纹理。这个片段明暗器的主要功能唯一做的就是使用内置的`texture2D`函数，使用我们传递到片段明暗器的纹理坐标从我们的纹理中抓取一个像素颜色。\n\n# OpenGL 全局变量\n\n在定义我们的着色器之后，我们需要在 C 中定义几个变量，我们将使用这些变量与它们进行交互:\n\n```cpp\nGLuint program = 0;\nGLuint texture;\n\nGLint a_texcoord_location = -1;\nGLint a_position_location = -1;\n\nGLint u_texture_location = -1;\nGLint u_translate_location = -1;\n\nGLuint vertex_texture_buffer;\n```\n\nOpenGL 使用引用变量与 GPU 进行交互。这些变量中的前两个属于`GLuint`类型。A `GLuint`是无符号整数，使用`GLuint`类型只是一个 OpenGL 类型。看到`GLuint`而不是`unsigned int`是给阅读你代码的人一个暗示，你正在使用这个变量与 OpenGL 交互的好方法。程序变量最终将保存对由着色器定义的程序的引用，纹理变量将保存对已加载到图形处理器中的纹理的引用。在引用程序和纹理之后，我们有两个变量将用于引用着色器程序属性。`a_texcoord_location`变量将引用`a_texcoord`着色器属性，`a_position_location`变量将引用`a_position`着色器属性值。属性引用后面是两个统一的变量引用。如果您想知道统一变量和属性变量之间的区别，统一变量对所有顶点都保持相同的值，而属性变量是特定于顶点的。最后，我们在`vertex_texture_buffer`变量中引用了我们的顶点纹理缓冲区。\n\n在我们定义了这些价值之后，我们需要定义我们的四元。你可能还记得，我们的四边形是由六个顶点组成的。这是因为它是由两个三角形组成的。我在[第三章](03.html)、*WebGL 简介*中谈到了为什么要这样设置顶点数据。如果你觉得这令人困惑，你可能想回到那一章做一点回顾。以下是`vertex_texture_data`数组的定义:\n\n```cpp\nfloat vertex_texture_data[] = {\n    // x,   y,        u,   v\n    0.167,  0.213,    1.0, 1.0,\n   -0.167,  0.213,    0.0, 1.0,\n    0.167, -0.213,    1.0, 0.0,\n   -0.167, -0.213,    0.0, 0.0,\n   -0.167,  0.213,    0.0, 1.0,\n    0.167, -0.213,    1.0, 0.0\n};\n```\n\n# SDL 全局变量\n\n我们仍将使用 SDL 来初始化我们的画布，以便进行 OpenGL 渲染。我们还将使用 SDL 从虚拟文件系统加载我们的图像数据。因此，我们需要定义以下与 SDL 相关的全局变量:\n\n```cpp\nSDL_Window *window;\nSDL_Renderer *renderer;\nSDL_Texture* sprite_texture;\nSDL_Surface* sprite_surface;\n```\n\n之后，当我们使用 SDL 加载图像时，我们需要变量来保存我们的精灵宽度和高度值:\n\n```cpp\nint sprite_width;\nint sprite_height;\n```\n\n当我们将船绘制到画布上时，我们将需要该船的`x`和`y`坐标，因此我们将创建一些全局变量来保存这些值:\n\n```cpp\nfloat ship_x = 0.0;\nfloat ship_y = 0.0;\n```\n\n最后，我们将为我们的游戏循环创建一个函数原型。我想在定义我们的主要功能后定义我们的游戏循环，因为我想先完成我们的初始化。这是我们游戏循环的函数原型:\n\n```cpp\nvoid game_loop();\n```\n\n# 主要功能\n\n现在，我们来到了我们的`main`功能。我们需要做大量的初始化工作。我们不仅初始化了 SDL，就像我们创建游戏时做的那样。我们还需要为 OpenGL 做几个初始化步骤。以下是`main`功能的全部内容:\n\n```cpp\nint main() {\n SDL_Init( SDL_INIT_VIDEO );\n SDL_CreateWindowAndRenderer( CANVAS_WIDTH, CANVAS_HEIGHT, 0, &window, &renderer );\n    SDL_SetRenderDrawColor( renderer, 0, 0, 0, 255 );\n    SDL_RenderClear( renderer );\n    GLuint vertex_shader = glCreateShader(GL_VERTEX_SHADER);\n    glShaderSource( vertex_shader,1,vertex_shader_code,0);\n    glCompileShader(vertex_shader);\n    GLint compile_success = 0;\n    glGetShaderiv(vertex_shader, GL_COMPILE_STATUS, &compile_success);\n    if(compile_success == GL_FALSE)\n    {\n        printf(\"failed to compile vertex shader\\n\");\n        glDeleteShader(vertex_shader);\n        return 0;\n    }\n    GLuint fragment_shader = glCreateShader(GL_FRAGMENT_SHADER);\n    glShaderSource( fragment_shader,1,fragment_shader_code,0);\n    glCompileShader(fragment_shader);\n    glGetShaderiv(fragment_shader, GL_COMPILE_STATUS,&compile_success);\n    if(compile_success == GL_FALSE)\n    {\n        printf(\"failed to compile fragment shader\\n\");\n        glDeleteShader(fragment_shader);\n        return 0;\n    }\n    program = glCreateProgram();\n    glAttachShader( program,vertex_shader);\n    glAttachShader( program,fragment_shader);\n    glLinkProgram(program);\n    GLint link_success = 0;\n    glGetProgramiv(program, GL_LINK_STATUS, &link_success);\n    if (link_success == GL_FALSE)\n    {\n        printf(\"failed to link program\\n\");\n        glDeleteProgram(program);\n        return 0;\n    }\n    glUseProgram(program);\n    u_texture_location = glGetUniformLocation(program, \"u_texture\");\n    u_translate_location = glGetUniformLocation(program,\"u_translate\");\n    a_position_location = glGetAttribLocation(program, \"a_position\");\n    a_texcoord_location = glGetAttribLocation(program, \"a_texcoord\");\n    glGenBuffers(1, &vertex_texture_buffer);\n    glBindBuffer( GL_ARRAY_BUFFER, vertex_texture_buffer );\n    glBufferData(GL_ARRAY_BUFFER, \n    sizeof(vertex_texture_data),vertex_texture_data, GL_STATIC_DRAW);\n    sprite_surface = IMG_Load( \"/sprites/spaceship.png\" );\n    if( !sprite_surface ) {\n        printf(\"failed to load image: %s\\n\", IMG_GetError() );\n        return 0;\n    }\n    sprite_texture = SDL_CreateTextureFromSurface( renderer, \n    sprite_surface );\n    if( !sprite_texture ) {\n        printf(\"failed to create texture: %s\\n\", IMG_GetError() );\n        return 0;\n    }\n    SDL_QueryTexture( sprite_texture,NULL, NULL,&sprite_width, &sprite_height );\n    glTexImage2D( GL_TEXTURE_2D,0,GL_RGBA,sprite_width,sprite_height,\n                  0,GL_RGBA,GL_UNSIGNED_BYTE,sprite_surface );\n    SDL_FreeSurface( sprite_surface );\n    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);\n    glEnable(GL_BLEND);\n    glEnableVertexAttribArray(a_position_location);\n    glEnableVertexAttribArray(a_texcoord_location);\n    glVertexAttribPointer(a_position_location,2,GL_FLOAT,GL_FALSE,4 * \n    sizeof(float),(void*)0 );\n    glVertexAttribPointer(a_texcoord_location,2,GL_FLOAT,GL_FALSE,\n                          4 * sizeof(float),(void*)(2 * sizeof(float)));\n    emscripten_set_main_loop(game_loop, 0, 0);\n}\n```\n\n让我把它分成一些更容易消化的部分。我们在`main`函数中需要做的第一件事是标准的 SDL 初始化。我们需要初始化视频模块，创建一个渲染器，并设置绘制和清晰的颜色。到目前为止，您应该对这段代码非常熟悉:\n\n```cpp\nSDL_Init( SDL_INIT_VIDEO );\nSDL_CreateWindowAndRenderer( CANVAS_WIDTH, CANVAS_HEIGHT, 0, &window, &renderer );\nSDL_SetRenderDrawColor( renderer, 0, 0, 0, 255 );\nSDL_RenderClear( renderer );\n```\n\n接下来，我们需要创建和编译我们的顶点着色器。这需要几个步骤。我们需要创建我们的着色器，将源代码加载到着色器中，编译着色器，然后检查以确保编译时没有任何错误。基本上，这些步骤会获取您的代码，编译它，然后将编译后的代码加载到视频卡中以供以后执行。以下是编译顶点着色器所需执行的所有步骤:\n\n```cpp\nGLuint vertex_shader = glCreateShader(GL_VERTEX_SHADER);\nglShaderSource( vertex_shader,\n                1,\n                vertex_shader_code,\n                0);\n\nglCompileShader(vertex_shader);\n\nGLint compile_success = 0;1\nglGetShaderiv(vertex_shader, GL_COMPILE_STATUS, &compile_success);\nif(compile_success == GL_FALSE)\n{\n    printf(\"failed to compile vertex shader\\n\");\n    glDeleteShader(vertex_shader);\n    return 0;\n}\n```\n\n在编译顶点着色器之后，我们需要编译片段着色器。这是同样的过程。我们首先调用`glCreateShader`来创建一个片段着色器。然后我们使用`glShaderSource`加载我们的片段着色器源代码。之后，我们调用`glCompileShader`来编译我们的片段着色器。最后，我们调用`glGetShaderiv`来查看当我们试图编译我们的片段着色器时是否发生了编译器错误:\n\n```cpp\nGLuint fragment_shader = glCreateShader(GL_FRAGMENT_SHADER);\nglShaderSource( fragment_shader,\n                1,\n                fragment_shader_code,\n                0);\n\nglCompileShader(fragment_shader);\nglGetShaderiv(fragment_shader, GL_COMPILE_STATUS, &compile_success);\n\nif(compile_success == GL_FALSE)\n{\n    printf(\"failed to compile fragment shader\\n\");\n    glDeleteShader(fragment_shader);\n    return 0;\n}\n```\n\n为了简单起见，我对着色器何时无法编译的错误消息保持模糊。它只告诉你哪个着色器编译失败。在本章的后面，我将向您展示如何从着色器编译器获得更详细的错误消息。\n\n现在我们已经编译了我们的着色器，我们需要将我们的着色器链接到一个程序中，然后告诉 OpenGL 这是我们想要使用的程序。如果你正在使用 OpenGL 编写一个游戏，很有可能你会使用不止一个程序。例如，您可能希望在游戏中的某些对象上有灯光效果，而不是其他对象。一些游戏对象可能需要旋转和缩放，而另一些可能不需要。\n\nAs you will learn in the next chapter, using multiple programs with WebGL has a significantly higher CPU hit than it does in a native OpenGL app. This has to do with the web browser's security checks.\n\n对于这个应用，我们将使用一个单独的程序，我们将使用以下代码来附加我们的着色器并将它们链接到程序:\n\n```cpp\nprogram = glCreateProgram();\nglAttachShader( program,\n                vertex_shader);\n\nglAttachShader( program,\n                fragment_shader);\n\nglLinkProgram(program);\n\nGLint link_success = 0;\n\nglGetProgramiv(program, GL_LINK_STATUS, &link_success);\n\nif (link_success == GL_FALSE)\n{\n    printf(\"failed to link program\\n\");\n    glDeleteProgram(program);\n    return 0;\n}\nglUseProgram(program);\n```\n\n`glCreateProgram`函数创建一个新程序，并为其返回一个引用标识。我们将在程序变量中存储引用标识。我们对`glAttachShader`进行两次调用，将我们的顶点和片段着色器附加到我们刚刚创建的程序中。然后我们调用`glLinkProgram`将程序着色器链接在一起。我们调用`glGetProgramiv`验证程序链接成功。最后，我们打电话给`glUseProgram`告诉 OpenGL，这是我们想要使用的程序。\n\n现在我们正在使用一个特定的程序，我们可以用下面几行代码检索对该程序内部的属性和统一变量的引用:\n\n```cpp\nu_texture_location = glGetUniformLocation(program, \"u_texture\");\nu_translate_location = glGetUniformLocation(program, \"u_translate\");\n\na_position_location = glGetAttribLocation(program, \"a_position\");\na_texcoord_location = glGetAttribLocation(program, \"a_texcoord\");\n```\n\n第一行检索对`u_texture`统一变量的引用，第二行检索对`u_translate`统一变量的引用。我们可以稍后使用这些引用在着色器内部设置这些值。其后的两行用于检索对着色器内部的`a_position`位置属性和`a_texcoord`纹理坐标属性的引用。像统一变量一样，我们稍后将使用这些引用来设置着色器中的值。\n\n现在，我们需要创建数据并将其加载到顶点缓冲区中。顶点缓冲区保存我们将要渲染的每个顶点的所有属性数据。如果我们要渲染一个三维模型，我们需要加载我们从外部获取的模型数据。幸运的是，我们只需要渲染一些二维四边形。四边形非常简单，我们可以在前面的数组中定义它们。\n\n在我们将数据加载到缓冲区之前，我们需要通过调用`glGenBuffers`来生成该缓冲区。然后我们需要*使用`glBindBuffer`绑定*缓冲区。绑定一个缓冲区只是你告诉 OpenGL 你当前正在处理的缓冲区的方式。下面是生成并绑定顶点缓冲区的代码:\n\n```cpp\nglGenBuffers(1, &vertex_texture_buffer);\nglBindBuffer( GL_ARRAY_BUFFER, vertex_texture_buffer );\n```\n\n现在我们已经选择了一个缓冲区，我们可以通过调用`glBufferData`将数据放入该缓冲区。我们将传入前面定义的`vertex_texture_data`。它定义了四边形顶点的`x`和`y`坐标以及这些顶点的紫外线映射数据:\n\n```cpp\nglBufferData(GL_ARRAY_BUFFER, sizeof(vertex_texture_data),\n                vertex_texture_data, GL_STATIC_DRAW);\n```\n\n在缓冲我们的数据后，我们将使用 SDL 加载一个精灵表面。然后，我们将从那个表面创建一个纹理，我们可以用它来找到我们刚刚加载的图像的宽度和高度。之后，我们调用`glTexImage2D`从 SDL 表面创建一个 OpenGL 纹理。下面是代码:\n\n```cpp\nsprite_surface = IMG_Load( \"/sprites/spaceship.png\" );\n\nif( !sprite_surface ) {\n    printf(\"failed to load image: %s\\n\", IMG_GetError() );\n    return 0;\n}\n\nsprite_texture = SDL_CreateTextureFromSurface( renderer, sprite_surface );\n\nif( !sprite_texture ) {\n    printf(\"failed to create texture: %s\\n\", IMG_GetError() );\n    return 0;\n}\n\nSDL_QueryTexture( sprite_texture,\n                    NULL, NULL,\n                    &sprite_width, &sprite_height );\n\nglTexImage2D( GL_TEXTURE_2D,\n                0,\n                GL_RGBA,\n                sprite_width,\n                sprite_height,\n                0,\n                GL_RGBA,\n                GL_UNSIGNED_BYTE,\n                sprite_surface );\n\nSDL_FreeSurface( sprite_surface );\n```\n\n前面的大部分代码看起来应该很熟悉。我们已经使用`IMG_Load`从虚拟文件系统加载 SDL 曲面有一段时间了。然后我们使用`SDL_CreateTextureFromSurface`来创建一个 SDL 纹理。一旦我们有了纹理，我们就使用`SDL_QueryTexture`来计算图像的宽度和高度，并将这些值存储在`sprite_width`和`sprite_height`中。下一个函数调用是新的。`GlTexImage2D`功能用于创建新的 OpenGL 纹理图像。我们传入`sprite_surface`作为我们的图像数据，我们之前已经加载了几行。最后一行使用`SDL_FreeSurface`释放曲面。\n\n然后，我们在游戏中添加了两条启用 alpha 混合的线:\n\n```cpp\nglBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);\nglEnable(GL_BLEND);\n```\n\n启用 alpha 混合后，我们有几条线在着色器中设置属性:\n\n```cpp\nglEnableVertexAttribArray(a_position_location);\nglEnableVertexAttribArray(a_texcoord_location);\n\nglVertexAttribPointer(\n        a_position_location,     // set up the a_position attribute\n        2,                       // how many attributes in the position\n        GL_FLOAT,                // data type of float\n        GL_FALSE,                // the data is not normalized\n        4 * sizeof(float),       // stride (how many array items until \n                                 //the next position)\n        (void*)0                 // starting point for attribute\n);\n\nglVertexAttribPointer(\n        a_texcoord_location,         // set up the a_texcoord attribute\n        2,                           // how many attributes in the \n                                     //texture coordinates\n        GL_FLOAT,                    // data type of float\n        GL_FALSE,                    // the data is not normalized\n        4 * sizeof(float),           // stride (how many array items \n                                     //until the next position)\n        (void*)(2 * sizeof(float))   // starting point for attribute\n);\n```\n\n前两行在我们的着色器中启用`a_position`和`a_texcoord`属性。之后，我们有两个电话打给`glVertexAttribPointer`。对`glVertexAttribPointer`的调用用于告诉我们的着色器，分配给每个特定属性的数据位于我们的顶点缓冲区中的什么位置。我们用 32 位浮点变量填充了顶点缓冲区。对`glVertexAttribPointer`的第一次调用使用我们在`a_position_location`中创建的参考变量来设置分配给`a_position`属性的值的位置。然后，我们传入用于该属性的值的数量。在位置的情况下，我们传入一个`x`和一个`y`坐标，所以这个值是 2。我们传入缓冲区数组的数据类型，这是一种浮点数据类型。我们告诉函数，我们没有标准化数据。`stride`值是倒数第二个参数。这是此缓冲区中用于顶点的字节数。因为缓冲区中的每个顶点都使用四个浮点值，所以我们传入`4 * sizeof( float )`来计算步幅。最后，我们传入的最后一个值是我们用来填充该属性的数据的偏移量(以字节为单位)。对于`a_position`属性，该值为`0`，因为位置在开头。对于`a_texcoord`属性，该值为`2 * sizeof(float)`，因为我们在`a_texcoord`数据之前有两个用于`a_position`的浮点值。\n\n`main`功能中的最后一行设置游戏循环回调:\n\n```cpp\nemscripten_set_main_loop(game_loop, 0, 0);\n```\n\n# 游戏循环\n\n我们的游戏循环非常简单。在我们的游戏循环中，我们将使用 OpenGL 来清除画布，移动我们的船，并将我们的船渲染到画布上。下面是代码:\n\n```cpp\nvoid game_loop() {\n    glClearColor( 0, 0, 0, 1 );\n    glClear( GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT );\n\n    ship_x += 0.002;\n    ship_y += 0.001;\n\n    if( ship_x >= 1.16 ) {\n        ship_x = -1.16;\n    }\n\n    if( ship_y >= 1.21 ) {\n        ship_y = -1.21;\n    }\n\n    glUniform4f(u_translate_location,\n                ship_x, ship_y, 0, 0 );\n\n    glDrawArrays(GL_TRIANGLES, 0, 6);\n}\n```\n\n游戏循环的前两行清空画布:\n\n```cpp\nglClearColor( 0, 0, 0, 1 );\nglClear( GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT );\n```\n\n之后，我们有几条线更新飞船的`x`和`y`坐标，然后在着色器中设置新坐标:\n\n```cpp\nship_x += 0.002;\nship_y += 0.001;\n\nif( ship_x >= 1.16 ) {\n    ship_x = -1.16;\n}\n\nif( ship_y >= 1.21 ) {\n    ship_y = -1.21;\n}\n\nglUniform4f(u_translate_location,\n            ship_x, ship_y, 0, 0 );\n```\n\n最后，游戏循环使用`glDrawArrays`将我们的飞船画到画布上:\n\n```cpp\nglDrawArrays(GL_TRIANGLES, 0, 6);\n```\n\n# 编译和运行我们的代码\n\n您将希望从 GitHub 项目中下载 sprites 文件夹，以便包含我们编译和运行该项目所需的图像文件。一旦你有了这些图像，并把我们刚刚写的代码保存到`webgl-redux.c`文件中，我们就可以编译和测试这个新的应用了。如果成功了，应该就像[第三章](03.html)、*WebGL 简介*，WebGL 版本。运行以下`emcc`命令编译应用:\n\n```cpp\nemcc webgl-redux.c -o redux.html --preload-file sprites -s USE_WEBGL2=1 -s USE_SDL=2 -s USE_SDL_IMAGE=2 -s SDL2_IMAGE_FORMATS=[\"png\"]\n```\n\n如果应用运行成功，你应该有一个飞船，从左向右移动，并在 HTML 画布上。以下是该应用工作版本的截图:\n\n![](img/57930775-44b4-4d64-a0ff-01e41f2688e0.png)\n\nFigure 15.1: Screenshot of the OpenGL and SDL app\n\n在下一节中，我们将学习如何在着色器中混合纹理。\n\n# 混合纹理以获得发光效果\n\n现在，我们将花一些时间学习如何将多个纹理加载到我们的程序中。我们将添加这两种纹理的颜色来创建脉冲发光效果。为此，我们需要修改我们的片段着色器，以接收第二个纹理和时间一致的变量。我们将把这个变量转换成正弦波函数，用它来计算发光引擎的强度。我们将需要添加一些代码来跟踪已经过去的时间，以及一些新的初始化代码来加载第二个纹理。我们可以从将`webgl-redux.c`复制到一个名为`glow.c`的新文件开始。现在我们有了新的`glow.c`文件，我们可以浏览我们需要的改变，使我们的发光引擎效果。第一个代码变化是增加了一个新的`#define`宏来定义`2π`的值。\n\n我们将使用一个从`0`循环到`2π`的值，并将其输入正弦波函数，以在我们的发动机辉光上产生脉冲效果。这里是我们应该在我们的`glow.c`文件的开头附近添加的`#define`:\n\n```cpp\n#define TWOPI 6.2831853 // 2π\n```\n\n# 片段着色器更改\n\n在新的宏之后，我们需要对片段着色器代码进行一些更改。我们的顶点着色器代码将保持不变，因为确定我们顶点位置的过程不会与应用的上一版本有任何不同。以下是片段着色器的更新版本:\n\n```cpp\nconst GLchar* fragment_shader_code[] = {\n    \"precision mediump float; \\n\"\n    \"varying vec2 v_texcoord; \\n\"\n\n    \"uniform float u_time; \\n\"\n    \"uniform sampler2D u_texture; \\n\"\n    \"uniform sampler2D u_glow; \\n\"\n\n    \"void main() { \\n\"\n        \"float cycle = (sin(u_time) + 1.0) / 2.0; \\n\"\n        \"vec4 tex = texture2D(u_texture, v_texcoord); \\n\"\n        \"vec4 glow = texture2D(u_glow, v_texcoord); \\n\"\n        \"glow.rgb *= glow.aaa; \\n\"\n        \"glow *= cycle; \\n\"\n        \"gl_FragColor = tex + glow; \\n\"\n    \"} \\n\"\n};\n```\n\n我们添加了一个新的统一变量`u_time`，它将用于传递一个基于时间的变量，该变量将在`0`和`2π`之间循环。我们还增加了第二个`sampler2D`统一变量`u_glow`，它将保存我们新的发光纹理。我们的`main`函数的第一行根据`u_time`中的值计算`0.0`和`1.0`之间的值。我们使用内置的`texture2D`功能从`u_texture`和`u_glow`中检索采样值。这次，我们将这两个值保存到名为`tex`和`glow`的`vec4`变量中，而不是将纹理中的一个值直接存储到`gl_FragColor`中。我们将把这两个值加在一起，所以为了防止任何地方都变得太亮，我们将`glow`样本颜色中的`rgb`(红色、绿色和蓝色)值乘以 alpha 通道。之后，我们将`glow`颜色中的所有值乘以之前计算的`cycle`值。\n\n`cycle`中的值将遵循在值`0.0`和`1.0`之间振荡的正弦波。这将导致我们的`glow`值随时间上下循环。然后，我们通过将`tex`颜色添加到`glow`颜色来计算片段颜色。然后，我们将输出值存储在`gl_FragColor`中。\n\n# OpenGL 全局变量更改\n\n接下来，我们需要更新与 OpenGL 相关的变量，这样我们就可以添加三个新的全局变量。我们将需要一个名为`glow_tex`的新变量，我们将使用它来存储对发光纹理的引用。我们还需要两个新的参考变量，用于着色器中的两个新的统一变量，称为`u_time_location`和`u_glow_location`。一旦我们添加了这三个新行，新的 OpenGL 变量块将会是什么样子:\n\n```cpp\nGLuint program = 0;\nGLuint texture;\nGLuint glow_tex;\n\nGLint a_texcoord_location = -1;\nGLint a_position_location = -1;\nGLint u_texture_location = -1;\nGLint u_glow_location = -1;\nGLint u_time_location = -1;\n\nGLint u_translate_location = -1;\nGLuint vertex_texture_buffer;\n```\n\n# 其他全局变量变化\n\n在我们的 OpenGL 全局变量之后，我们将需要添加一个新的与时间相关的全局变量块。我们需要他们让我们的着色器循环通过引擎发光的值。这些与时间相关的变量看起来应该很熟悉。我们已经使用了与我们正在开发的游戏中将要使用的技术相似的技术。以下是这些全局时间变量:\n\n```cpp\nfloat time_cycle = 0;\nfloat delta_time = 0.0;\nint diff_time = 0;\n\nUint32 last_time;\nUint32 last_frame_time;\nUint32 current_time;\n```\n\n我们需要再添加一个与 SDL 相关的全局表面变量，我们将使用它来加载我们的发光纹理。在`main`函数之前的全局变量块附近添加以下行:\n\n```cpp\nSDL_Surface* glow_surface;\n```\n\n# 对 main()的更改\n\n我们将对我们在`main`函数中进行的初始化进行一些重大修改。让我首先向您展示整个功能。然后，我们将逐一介绍所有的变化:\n\n```cpp\nint main() {\n    last_frame_time = last_time = SDL_GetTicks();\n\n    SDL_Init( SDL_INIT_VIDEO );\n\n    SDL_CreateWindowAndRenderer( CANVAS_WIDTH, CANVAS_HEIGHT, 0, \n    &window, &renderer );\n\n    SDL_SetRenderDrawColor( renderer, 0, 0, 0, 255 );\n    SDL_RenderClear( renderer );\n\n    GLuint vertex_shader = glCreateShader(GL_VERTEX_SHADER);\n\n    glShaderSource( vertex_shader,\n                    1,\n                    vertex_shader_code,\n                    0);\n\n    glCompileShader(vertex_shader);\n\n    GLint compile_success = 0;\n    glGetShaderiv(vertex_shader, GL_COMPILE_STATUS, &compile_success);\n\n    if(compile_success == GL_FALSE)\n    {\n        printf(\"failed to compile vertex shader\\n\");\n        glDeleteShader(vertex_shader);\n        return 0;\n    }\n\n    GLuint fragment_shader = glCreateShader(GL_FRAGMENT_SHADER);\n\n    glShaderSource( fragment_shader,\n                    1,\n                    fragment_shader_code,\n                    0);\n\n    glCompileShader(fragment_shader);\n    glGetShaderiv(fragment_shader, GL_COMPILE_STATUS, \n    &compile_success);\n\n    if(compile_success == GL_FALSE)\n    {\n        printf(\"failed to compile fragment shader\\n\");\n        glDeleteShader(fragment_shader);\n        return 0;\n    }\n\n    program = glCreateProgram();\n    glAttachShader( program,\n                    vertex_shader);\n\n    glAttachShader( program,\n                    fragment_shader);\n\n    glLinkProgram(program);\n\n    GLint link_success = 0;\n\n    glGetProgramiv(program, GL_LINK_STATUS, &link_success);\n\n    if (link_success == GL_FALSE)\n    {\n        printf(\"failed to link program\\n\");\n        glDeleteProgram(program);\n        return 0;\n    }\n\n    glUseProgram(program);\n\n    u_glow_location = glGetUniformLocation(program, \"u_glow\");\n    u_time_location = glGetUniformLocation(program, \"u_time\");\n\n    u_texture_location = glGetUniformLocation(program, \"u_texture\");\n    u_translate_location = glGetUniformLocation(program, \n    \"u_translate\");\n\n    a_position_location = glGetAttribLocation(program, \"a_position\");\n    a_texcoord_location = glGetAttribLocation(program, \"a_texcoord\");\n\n    glGenBuffers(1, &vertex_texture_buffer);\n\nglBindBuffer( GL_ARRAY_BUFFER, vertex_texture_buffer );\n glBufferData(GL_ARRAY_BUFFER, sizeof(vertex_texture_data),\n vertex_texture_data, GL_STATIC_DRAW);\n\nsprite_surface = IMG_Load( \"/sprites/spaceship.png\" );\n\n    if( !sprite_surface ) {\n        printf(\"failed to load image: %s\\n\", IMG_GetError() );\n        return 0;\n    }\n\n    sprite_texture = SDL_CreateTextureFromSurface( renderer, \n    sprite_surface );\n\n    if( !sprite_texture ) {\n        printf(\"failed to create texture: %s\\n\", IMG_GetError() );\n        return 0;\n    }\n\n    SDL_QueryTexture( sprite_texture,\n                        NULL, NULL,\n                        &sprite_width, &sprite_height );\n\n    glTexImage2D( GL_TEXTURE_2D,\n                    0,\n                    GL_RGBA,\n                    sprite_width,\n                    sprite_height,\n                    0,\n                    GL_RGBA,\n                    GL_UNSIGNED_BYTE,\n                    sprite_surface );\n\n    SDL_FreeSurface( sprite_surface );\n\n    glGenTextures( 1,\n                    &glow_tex);\n\n    glActiveTexture(GL_TEXTURE1);\n    glEnable(GL_TEXTURE_2D);\n    glBindTexture(GL_TEXTURE_2D, glow_tex);\n\n    glow_surface = IMG_Load( \"/sprites/glow.png\" );\n\n    if( !glow_surface ) {\n        printf(\"failed to load image: %s\\n\", IMG_GetError() );\n        return 0;\n    }\n\n    glTexImage2D( GL_TEXTURE_2D,\n                    0,\n                    GL_RGBA,\n                    sprite_width,\n                    sprite_height,\n                    0,\n                    GL_RGBA,\n                    GL_UNSIGNED_BYTE,\n                    glow_surface );\n\n    glGenerateMipmap(GL_TEXTURE_2D);\n\n    SDL_FreeSurface( glow_surface );\n\n    glUniform1i(u_texture_location, 0);\n    glUniform1i(u_glow_location, 1);\n\n    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);\n    glEnable(GL_BLEND);\n\n    glEnableVertexAttribArray(a_position_location);\n    glEnableVertexAttribArray(a_texcoord_location);\n\n    glVertexAttribPointer(\n        a_position_location,     // set up the a_position attribute\n        2,                       // how many attributes in the position\n        GL_FLOAT,                // data type of float\n        GL_FALSE,                // the data is not normalized\n        4 * sizeof(float),       // stride (how many array items until \n                                 //the next position)\n        (void*)0                 // starting point for attribute\n    );\n\n    glVertexAttribPointer(\n        a_texcoord_location,       // set up the a_texcoord attribute\n        2,                         // how many attributes in the \n                                   //texture coordinates\n        GL_FLOAT,                  // data type of float\n        GL_FALSE,                  // the data is not normalized\n        4 * sizeof(float),         // stride (how many array items \n                                   //until the next position)\n        (void*)(2 * sizeof(float)) // starting point for attribute\n    );\n\n    emscripten_set_main_loop(game_loop, 0, 0);\n}\n```\n\n我们`main`功能的第一行是新的。我们使用该行将`last_frame_time`和`last_time`设置为系统时间，我们使用`SDL_GetTicks()`检索系统时间:\n\n```cpp\nlast_frame_time = last_time = SDL_GetTicks();\n```\n\n在那之后，我们将不会做任何改变，直到我们到达我们检索我们的统一位置的代码部分。我们将需要从我们的程序中检索两个更统一的位置，因此在我们调用`glUseProgram`的情况下，我们应该进行以下调用来获取`u_glow`和`u_time`的统一位置:\n\n```cpp\nu_glow_location = glGetUniformLocation(program, \"u_glow\");\nu_time_location = glGetUniformLocation(program, \"u_time\");\n```\n\n在我们调用`SDL_FreeSurface`来释放`sprite_surface`变量之后，必须有以下代码块。该代码块将生成一个新的纹理，激活它，绑定它，并将`glow.png`图像加载到该纹理中。然后，它将释放 SDL 表面，并为我们的纹理生成 mipmaps。最后，我们使用`glUniform1i`设置纹理的统一位置。下面是我们用来加载新纹理的代码:\n\n```cpp\nglGenTextures( 1,\n                &glow_tex);\n\nglActiveTexture(GL_TEXTURE1);\nglEnable(GL_TEXTURE_2D);\nglBindTexture(GL_TEXTURE_2D, glow_tex);\n\nglow_surface = IMG_Load( \"/sprites/glow.png\" );\n\nif( !glow_surface ) {\n    printf(\"failed to load image: %s\\n\", IMG_GetError() );\n    return 0;\n}\n\nglTexImage2D( GL_TEXTURE_2D,\n                0,\n                GL_RGBA,\n                sprite_width,\n                sprite_height,\n                0,\n                GL_RGBA,\n                GL_UNSIGNED_BYTE,\n                glow_surface );\n\nSDL_FreeSurface( glow_surface );\n\nglGenerateMipmap(GL_TEXTURE_2D);\n\nglUniform1i(u_texture_location, 0);\nglUniform1i(u_glow_location, 1);\n```\n\nIf you are not familiar with Mipmaps, you may be wondering what the `glGenerateMipmap(GL_TEXTURE_2D);` line does. When you scale textures using OpenGL, those textures take time to generate. Mipmaps are a way to speed up scaling by performing some power of two scaled versions of your images while the game is initializing. This will reduce the amount of time it will take to scale these images at runtime.\n\n# 更新 game_loop()\n\n为了在我们飞船的引擎上循环发光效果，我们需要在我们的游戏循环中添加一些代码，从`0.0`循环到`2π`。然后我们将这个值作为`u_time`统一变量传递到着色器中。我们需要在游戏循环函数的开头添加这段新代码:\n\n```cpp\ncurrent_time = SDL_GetTicks();\n\ndiff_time = current_time - last_time;\n\ndelta_time = diff_time / 1000.0;\nlast_time = current_time;\n\ntime_cycle += delta_time * 4;\n\nif( time_cycle >= TWOPI ) {\n    time_cycle -= TWOPI;\n}\n\nglUniform1f( u_time_location, time_cycle );\n```\n\n第一行使用`SDL_GetTicks()`检索当前时钟时间。然后我们从当前时间中减去最后一次，得到`diff_time`变量的值。这将告诉我们此帧与生成的前一帧之间的毫秒数。之后，我们计算`delta_time`，这将是这一帧和前一帧之间一秒的分数。计算完`diff_time`和`delta_time`后，我们将`last_time`变量设置为`current_time`。\n\n我们这样做是为了下一次我们通过游戏循环时，我们会有这个帧运行的时间。所有这些行都在我们代码的先前迭代中。现在，让我们为`time_cycle`获取一个值，我们将它传递到片段着色器中的`u_time`统一变量中。首先，用以下行将`delta-time * 4`添加到时间周期中:\n\n```cpp\ntime_cycle += delta_time * 4;\n```\n\n你可能想知道为什么我要乘以`4`。最初，我没有添加倍数，这意味着发动机发光大约每 6 秒钟循环一次。这感觉好像周期太长了。玩这个数字，4 的倍数对我来说刚刚好，但是如果你希望你的引擎循环更快或更慢，你没有理由坚持这个特定的倍数。\n\n因为我们使用正弦函数来循环我们的发光级别，所以我们需要确保当我们的时间循环达到`TWOPI`时，我们从`time_cycle`变量中减去`TWOPI`:\n\n```cpp\nif( time_cycle >= TWOPI ) {\n    time_cycle -= TWOPI;\n}\n```\n\n现在我们已经计算了我们周期的值，我们通过调用`glUniform1f`使用`u_time_location`参考变量设置该值:\n\n```cpp\nglUniform1f( u_time_location, time_cycle );\n```\n\n# 编译和运行我们的代码\n\n现在我们已经完成了所有需要的代码更改，我们可以继续编译和运行新版本的应用了。通过运行以下`emcc`命令编译`glow.c`文件:\n\n```cpp\nemcc glow.c -o glow.html --preload-file sprites -s USE_WEBGL2=1 -s USE_SDL=2 -s USE_SDL_IMAGE=2 -s SDL2_IMAGE_FORMATS=[\"png\"]\n```\n\n如果建造成功，在你的网络浏览器中运行`glow.html`应该会显示飞船像以前一样移动。但是，现在，发动机上会有发光效果。当发动机处于最大发光状态时，该发光将上下循环，如下所示:\n\n![](img/b7146f36-42c8-4a69-860d-7d08556afe4f.png)\n\nFigure 15.2: Screenshot of the glow shader app\n\n在下一节中，我们将讨论 Phong 3D 照明模型。\n\n# 三维照明\n\n我想简单地讨论一下三维照明，因为我们会用 2D 照明效果来近似它。Phong 照明模型是计算机图形学中三维照明模型的标准。这是 1975 年犹他大学的裴祥风创造的照明模型，但直到 20 世纪 90 年代末，台式电脑才变得足够快，可以在游戏中实现这一模型。此后，灯光模型成为 3D 游戏开发的标准。它结合了环境光、漫射光和镜面光来渲染几何体。我们将无法实现一个合适版本的照明模型，因为我们没有写一个 3D 游戏。然而，我们可以通过使用 2D 精灵和法线贴图来实现模型的近似。\n\n# 背景光\n\n在现实世界中，有一定量的光从周围表面随机反射。这创造了照明，将照亮一切均匀。如果没有环境照明，一个物体在另一个物体的阴影下会完全变黑。环境光照量因环境而异。在游戏中，环境照明的数量通常取决于游戏设计者试图达到的情绪和外观。对于 2D 奥运会来说，环境照明可能是我们唯一有效的照明方式。在 3D 游戏中，完全依赖环境光会产生看起来扁平的模型:\n\n![](img/312e7693-39ef-4252-9d53-dc284d0a7f31.png)\n\nFigure 15.3: A sphere with only ambient lighting\n\n# 漫射光\n\n漫射光是来自特定方向的光。如果你在现实世界中观察一个三维物体，面对光源的一面看起来会比远离光源的一面更亮。这为 3D 环境中的对象提供了真实的 3D 外观。在许多 2D 游戏中，漫射照明不是用着色器创建的，而是由创建它的艺术家包含在精灵中。例如，在平台游戏中，艺术家可能假设有一个光源来自游戏物体的上方。艺术家将通过改变艺术品中像素的颜色来设计游戏对象，使其具有一种漫射照明。对于许多 2D 奥运会来说，这将非常有效。但是，如果您希望在游戏中有一个手电筒，在游戏对象移动时改变它们的外观，那么您需要设计能够完成这项工作的着色器:\n\n![](img/26581e91-5ca6-408c-9fb6-319303979dee.png)\n\nFigure 15.4: Sphere with diffuse lighting\n\n# 镜面光\n\n有些物体是有光泽的，有反光的斑点，可以产生明亮的高光。当光线照射到一个表面上时，它有一个基于光线照射到表面的角度的反射矢量，相对于它照射到的表面的法线。镜面高光的强度基于表面的反射率，结合视角，相对于反射光角度。游戏对象上的镜面高光可以使其看起来平滑或抛光。并非所有游戏对象都需要这种照明，但它在您想要发光的对象上看起来很棒:\n\n![](img/2e8a12b9-f654-49cb-b1bf-ce46a269680d.png)\n\nFigure 15.5: Sphere with specular lighting\n\n在下一节中，我们将讨论普通地图以及它们如何在现代游戏中使用。\n\n# 普通地图\n\n法线贴图是一种用于在 3D 游戏中使用相对较低的多边形数量创建非常详细的模型的方法。这个想法是，游戏引擎可以使用低多边形模型，而不是创建一个有大量多边形的表面，该模型有一个法线贴图，其中法线贴图中的每个像素将包含使用图像的红色、绿色和蓝色的法线的 x、y 和 z 值。在着色器内部，我们可以像采样其他纹理贴图一样采样普通贴图纹理。然而，我们可以使用正常数据来帮助我们计算对我们的精灵的照明效果。如果，在我们的游戏中，我们希望我们的宇宙飞船总是相对于游戏区域中心的星星被照亮，我们可以为我们的宇宙飞船创建一个普通的地图，并在我们的游戏中心创建一个光源。我们现在将创建一个应用来演示 2D 照明正常地图的使用。\n\n# 创建 2D 照明演示应用\n\n我们可以通过创建一个名为`lighting.c`的新 C 文件来启动我们的照明应用。`lighting.c`开头的宏与我们在`glow.c`中使用的宏相同，但是我们可以删除`#define TWOPI`宏，因为它不再需要了。以下是我们的`lighting.c`文件中的宏:\n\n```cpp\n#include <SDL2/SDL.h>\n#include <SDL2/SDL_image.h>\n#include <SDL_opengl.h>\n\n#include <GLES3/gl3.h>\n#include <stdlib.h>\n#include <emscripten.h>\n\n#define CANVAS_WIDTH 800\n#define CANVAS_HEIGHT 600\n#define FLOAT32_BYTE_SIZE 4\n#define STRIDE FLOAT32_BYTE_SIZE*4\n```\n\n这个文件中的顶点着色器代码将非常类似于我们在`glow.c`文件中的顶点着色器代码。我们要做的一个改变是去掉`u_translate`统一变量。我们这样做是因为我们将把我们的阴影精灵图像放在中心，我们将允许用户在画布上移动光线。以下是顶点着色器的新版本:\n\n```cpp\nconst GLchar* vertex_shader_code[] = {\n    \"precision mediump float; \\n\"\n    \"attribute vec4 a_position; \\n\"\n    \"attribute vec2 a_texcoord; \\n\"\n    \"varying vec2 v_texcoord; \\n\"\n\n    \"void main() { \\n\"\n        \"gl_Position = a_position; \\n\"\n        \"v_texcoord = a_texcoord; \\n\"\n    \"} \\n\"\n};\n```\n\n# 片段着色器更新\n\n现在，我们需要创建一个新版本的片段着色器。除了加载的原始纹理外，该着色器还将加载法线贴图。这个法线贴图将被用来计算我们游戏对象的光照法线。这个版本的着色器将使用 Phong 光照模型的 2D 形式，因为我们将为正在渲染的精灵计算环境、漫射和正常光照。下面是我们新的片段着色器的代码:\n\n```cpp\nconst GLchar* fragment_shader_code[] = {\n    \"precision mediump float; \\n\"\n\n    \"varying vec2 v_texcoord; \\n\"\n\n    \"uniform sampler2D u_texture; \\n\"\n    \"uniform sampler2D u_normal; \\n\"\n    \"uniform vec3 u_light_pos; \\n\"\n\n    \"const float ambient = 0.6; \\n\"\n    \"const float specular = 32.0; \\n\"\n    \"const vec3 view_pos = vec3(400, 300,-100); \\n\"\n    \"const vec4 light_color = vec4( 0.6, 0.6, 0.6, 0.0); \\n\"\n\n    \"void main() { \\n\"\n        \"vec4 tex = texture2D(u_texture, v_texcoord); \\n\"\n\n        \"vec4 ambient_frag = tex * ambient; \\n\"\n        \"ambient_frag.rgb *= light_color.rgb; \\n\"\n\n        \"vec3 norm = vec3(texture2D(u_normal, v_texcoord)); \\n\"\n        \"norm.xyz *= 2.0; \\n\"\n        \"norm.xyz -= 1.0; \\n\"\n\n        \"vec3 light_dir = normalize(gl_FragCoord.xyz - u_light_pos); \\n\"\n\n        \"vec3 view_dir = normalize(view_pos - gl_FragCoord.xyz); \\n\"\n        \"vec3 reflect_dir = reflect(light_dir, norm); \\n\"\n\n        \"float reflect_dot = max( dot(view_dir, reflect_dir), 0.0 ); \\n\"\n        \"float spec = pow(reflect_dot, specular); \\n\"\n        \"vec4 specular_frag = spec * light_color; \\n\"\n\n        \"float diffuse = max(dot(norm, light_dir), 0.0); \\n\"\n        \"vec4 diffuse_frag = vec4( diffuse*light_color.r, \n         diffuse*light_color.g, \"\n                                    \"diffuse*light_color.b,  0.0);    \\n\"\n        \"gl_FragColor = ambient_frag + diffuse_frag + specular_frag; \\n\"\n    \"} \\n\"\n};\n```\n\n让我们来分析一下新版本的片段着色器内部发生了什么。首先你会注意到我们有两个`sampler2D`均匀变量；第二个称为`u_normal`，用于对我们图像的法线贴图进行采样:\n\n```cpp\n\"uniform sampler2D u_texture; \\n\"\n\"uniform sampler2D u_normal; \\n\"\n```\n\n在我们的采样器之后，我们需要一个`uniform vec3`变量来保存我们的光的位置。我们我们称之为`u_light_pos`:\n\n```cpp\n\"uniform vec3 u_light_pos; \\n\"\n```\n\n我们将在新的片段着色器中使用几个常量。我们将需要环境和镜面照明的因素，以及视图位置和灯光颜色。我们将在以下四行代码中定义这些常数:\n\n```cpp\n\"const float ambient = 0.6; \\n\"\n\"const float specular = 0.8; \\n\"\n\"const vec3 view_pos = vec3(400, 300,-100); \\n\"\n\"const vec4 light_color = vec4( 0.6, 0.6, 0.6, 0.0); \\n\"\n```\n\n在我们的`main`函数内部，我们首先需要做的是获取环境碎片颜色。确定环境颜色非常容易。你所需要做的就是将纹理颜色乘以环境因子，然后再乘以浅色。下面是计算片段的环境分量值的代码:\n\n```cpp\n\"vec4 tex = texture2D(u_texture, v_texcoord); \\n\"\n\"vec4 ambient_frag = tex * ambient; \\n\"\n\n\"ambient_frag.rgb *= light_color.rgb; \\n\"\n```\n\n在计算我们的环境颜色分量之后，我们需要根据传递到着色器的法线贴图纹理来计算片段的法线。纹理使用红色来表示法线的`x`值。绿色代表`y`值。最后，蓝色代表`z`价值。颜色都是从`0.0`到`1.0`的浮点，所以我们需要修改普通的`x`、`y`、`z`组件才能从`-1.0`到`+1.0`。下面是我们用来定义法线的代码:\n\n```cpp\n\"vec3 norm = vec3(texture2D(u_normal, v_texcoord)); \\n\"\n\"norm.xyz *= 2.0; \\n\"\n\"norm.xyz -= 1.0; \\n\"\n```\n\n要将`0.0`的`norm`向量中的值转换为`1.0`、`-1.0`和`+1.0`，我们需要将法向量中的值乘以 2，然后减去 1。计算法线的值后，我们需要找到光源的方向:\n\n```cpp\n\"vec3 light_dir = normalize(gl_FragCoord.xyz - u_light_pos); \\n\"\n```\n\n我们正在使用归一化 GLSL 函数对该值进行归一化，因为我们在这个应用中不会有任何光衰减。如果你有一个带手电筒的游戏，你可能想要一个基于光源距离的平方的急剧衰减。对于这个应用，我们假设光源有无限的范围。对于我们的镜面照明，我们需要计算我们的视图方向:\n\n```cpp\n\"vec3 view_dir = normalize(view_pos - gl_FragCoord.xyz); \\n\"\n```\n\n我们将`view_pos`向量设置到画布的中心，所以当我们的光源也在画布的中心时，我们的镜面照明应该是最大的。当你编译这个应用的时候，你可以测试一下。计算视图方向后，我们将需要计算反射向量，我们也将在镜面照明计算中使用该向量:\n\n```cpp\n\"vec3 reflect_dir = reflect(light_dir, norm); \\n\"\n```\n\n然后，我们可以计算这两个向量的点积，并将它们提升到我们的镜面反射因子(前面定义为 32)的幂，以计算这个片段所需的镜面反射光量:\n\n```cpp\n\"float reflect_dot = max( dot(view_dir, reflect_dir), 0.0 ); \\n\"\n\"float spec = pow(reflect_dot, specular); \\n\"\n\"vec4 specular_frag = spec * light_color; \\n\"\n```\n\n之后，我们使用法线和光线方向的点积计算碎片的漫射分量。我们将其与浅色相结合，以获得漫射分量值:\n\n```cpp\n\"float diffuse = max(dot(norm, light_dir), 0.0); \\n\"\n\"vec4 diffuse_frag = vec4(diffuse*light_color.r, diffuse*light_color.g, diffuse*light_color.b, 0.0); \\n\"\n```\n\n最后，我们将所有这些值加在一起，找到我们的片段值:\n\n```cpp\n\"gl_FragColor = ambient_frag + diffuse_frag + specular_frag; \\n\"\n```\n\n# OpenGL 全局变量\n\n在定义了我们的片段着色器之后，我们需要定义一系列与 OpenGL 相关的全局变量。从这个应用的前两个版本来看，这些变量应该是你所熟悉的。我们应该注意一些新的变量。我们将不再只有一个程序标识。SDL 使用自己的程序，我们也需要该程序的 ID。我们将这个变量称为`sdl_program`。我们还需要新的纹理参考。此外，我们将需要传递到着色器中的统一变量的新引用。下面是新版本的 OpenGL 全局变量代码:\n\n```cpp\nGLuint program = 0;\nGLint sdl_program = 0;\nGLuint circle_tex, normal_tex, light_tex;\nGLuint normal_map;\n\nGLint a_texcoord_location = -1;\nGLint a_position_location = -1;\nGLint u_texture_location = -1;\nGLint u_normal_location = -1;\nGLint u_light_pos_location = -1;\n\nGLint u_translate_location = -1;\nGLuint vertex_texture_buffer;\n\nfloat vertex_texture_data[] = {\n    // x,    y,         u,   v\n     0.167,  0.213,     1.0, 1.0,\n    -0.167,  0.213,     0.0, 1.0,\n     0.167, -0.213,     1.0, 0.0,\n    -0.167, -0.213,     0.0, 0.0,\n    -0.167,  0.213,     0.0, 1.0,\n     0.167, -0.213,     1.0, 0.0\n};\n```\n\n# SDL 全局变量\n\n一些 SDL 变量与我们在本章之前创建的应用中使用的变量相同。照明和法线的其他变量在这个部分是新的。以下是本应用需要的与 SDL 相关的全球变量:\n\n```cpp\nSDL_Window *window;\nSDL_Renderer *renderer;\n\nSDL_Texture* light_texture;\n\nSDL_Surface* surface;\n\nint light_width;\nint light_height;\n\nint light_x = 600;\nint light_y = 200;\nint light_z = -300;\n```\n\n我们需要声明一个名为`light_texture`的`SDL_Texture`变量，我们将使用它来保存灯光图标的 SDL 纹理。我们将使用 SDL 绘制我们的光图标，而不是使用 OpenGL 绘制它。我们将使用一个表面指针变量来加载我们所有的纹理，在我们创建纹理后立即释放该表面。我们需要宽度和高度值来跟踪灯光图标的宽度和高度。我们还需要数值来跟踪光源的`x`、`y`和`z`坐标。\n\n# 功能原型\n\n因为我想把`main`函数的代码放在我们其他函数的代码之前，所以我们需要一些函数原型。在这个应用中，我们将有一个游戏循环功能，一个通过 SDL 检索鼠标输入的功能，以及一个使用 SDL 绘制我们的灯光图标的功能。下面是这些函数原型的样子:\n\n```cpp\nvoid game_loop();\nvoid input();\nvoid draw_light_icon();\n```\n\n# 主要功能\n\n与我们在本章中创建的其他应用一样，我们的`main`函数将需要初始化 SDL 和 OpenGL 变量。`main`功能的开始与我们的发光应用开始时相同。它初始化 SDL，然后编译并链接 OpenGL 着色器，并创建一个新的 OpenGL 程序:\n\n```cpp\nint main() {\n    SDL_Init( SDL_INIT_VIDEO );\n    SDL_CreateWindowAndRenderer( CANVAS_WIDTH, CANVAS_HEIGHT, 0, \n    &window, &renderer );\n    SDL_SetRenderDrawColor( renderer, 0, 0, 0, 255 );\n    SDL_RenderClear( renderer );\n\n    GLuint vertex_shader = glCreateShader(GL_VERTEX_SHADER);\n\n    glShaderSource( vertex_shader,\n                    1,\n                    vertex_shader_code,\n                    0);\n\n    glCompileShader(vertex_shader);\n\n    GLint compile_success = 0;\n    glGetShaderiv(vertex_shader, GL_COMPILE_STATUS, &compile_success);\n\n    if(compile_success == GL_FALSE)\n    {\n        printf(\"failed to compile vertex shader\\n\");\n        glDeleteShader(vertex_shader);\n        return 0;\n    }\n\n    GLuint fragment_shader = glCreateShader(GL_FRAGMENT_SHADER);\n\n    glShaderSource( fragment_shader,\n                    1,\n                    fragment_shader_code,\n                    0);\n\n    glCompileShader(fragment_shader);\n    glGetShaderiv(fragment_shader, GL_COMPILE_STATUS, \n    &compile_success);\n\n    if(compile_success == GL_FALSE)\n    {\n        printf(\"failed to compile fragment shader\\n\");\n\n        GLint maxLength = 0;\n        glGetShaderiv(fragment_shader, GL_INFO_LOG_LENGTH, &maxLength);\n\n        GLchar* errorLog = malloc(maxLength);\n        glGetShaderInfoLog(fragment_shader, maxLength, &maxLength, \n        &errorLog[0]);\n        printf(\"error: %s\\n\", errorLog);\n\n        glDeleteShader(fragment_shader);\n        return 0;\n    }\n\n    program = glCreateProgram();\n    glAttachShader( program,\n                    vertex_shader);\n\n    glAttachShader( program,\n                    fragment_shader);\n\n    glLinkProgram(program);\n\n    GLint link_success = 0;\n\n    glGetProgramiv(program, GL_LINK_STATUS, &link_success);\n\n    if (link_success == GL_FALSE)\n    {\n        printf(\"failed to link program\\n\");\n        glDeleteProgram(program);\n        return 0;\n    }\n\n    glDeleteShader(vertex_shader);\n    glDeleteShader(fragment_shader);\n    glUseProgram(program);\n```\n\n在初始化 SDL 和创建 OpenGL 着色器程序后，我们需要为我们的 OpenGL 着色器程序获取统一的变量引用。其中两个引用对于这个版本的程序来说是新的。`u_normal_location`变量将是对`u_normal`采样器均匀变量的引用，`u_light_pos_location`变量将是对`u_light_pos`均匀变量的引用。以下是我们参考资料的新版本:\n\n```cpp\nu_texture_location = glGetUniformLocation(program, \"u_texture\");\nu_normal_location = glGetUniformLocation(program, \"u_normal\");\nu_light_pos_location = glGetUniformLocation(program, \"u_light_pos\");\nu_translate_location = glGetUniformLocation(program, \"u_translate\");\n```\n\n在获取对我们的统一变量的引用之后，我们需要对我们的属性做同样的事情:\n\n```cpp\na_position_location = glGetAttribLocation(program, \"a_position\");\na_texcoord_location = glGetAttribLocation(program, \"a_texcoord\");\n```\n\n然后，我们需要生成顶点缓冲区，绑定它，并缓冲我们之前创建的数组中的数据。这应该与我们在`glow.c`文件中的代码相同:\n\n```cpp\nglGenBuffers(1, &vertex_texture_buffer);\n\nglBindBuffer( GL_ARRAY_BUFFER, vertex_texture_buffer );\nglBufferData( GL_ARRAY_BUFFER, sizeof(vertex_texture_data),\n              vertex_texture_data, GL_STATIC_DRAW);\n```\n\n接下来，我们需要设置所有的纹理。其中两个将使用 OpenGL 渲染，而另一个将使用 SDL 渲染。以下是所有三种纹理的初始化代码:\n\n```cpp\nglGenTextures( 1,\n                &circle_tex);\n\nglActiveTexture(GL_TEXTURE0);\nglBindTexture(GL_TEXTURE_2D, circle_tex);\n\nsurface = IMG_Load( \"/sprites/circle.png\" );\nif( !surface ) {\n    printf(\"failed to load image: %s\\n\", IMG_GetError() );\n    return 0;\n}\n\nglTexImage2D( GL_TEXTURE_2D,\n                0,\n                GL_RGBA,\n                128, // sprite width\n                128, // sprite height\n                0,\n                GL_RGBA,\n                GL_UNSIGNED_BYTE,\n                surface );\n\nglUniform1i(u_texture_location, 1);\nglGenerateMipmap(GL_TEXTURE_2D);\n\nSDL_FreeSurface( surface );\n\nglGenTextures( 1,\n                &normal_tex);\n\nglActiveTexture(GL_TEXTURE1);\nglBindTexture(GL_TEXTURE_2D, normal_tex);\n\nsurface = IMG_Load( \"/sprites/ball-normal.png\" );\n\nif( !surface ) {\n    printf(\"failed to load image: %s\\n\", IMG_GetError() );\n    return 0;\n}\n\nglTexImage2D( GL_TEXTURE_2D,\n                0,\n                GL_RGBA,\n                128, // sprite width\n                128, // sprite height\n                0,\n                GL_RGBA,\n                GL_UNSIGNED_BYTE,\n                surface );\n\nglUniform1i(u_normal_location, 1);\nglGenerateMipmap(GL_TEXTURE_2D);\n\nSDL_FreeSurface( surface );\n\nsurface = IMG_Load( \"/sprites/light.png\" );\n\nif( !surface ) {\n    printf(\"failed to load image: %s\\n\", IMG_GetError() );\n    return 0;\n}\n\nlight_texture = SDL_CreateTextureFromSurface( renderer, surface );\n\nif( !light_texture ) {\n    printf(\"failed to create light texture: %s\\n\", IMG_GetError() );\n    return 0;\n}\n\nSDL_QueryTexture( light_texture,\n                    NULL, NULL,\n                    &light_width, &light_height );\n\nSDL_FreeSurface( surface );\n```\n\n这是一个相当大的代码块，所以让我一次浏览一段。前三行生成、激活和绑定圆形纹理，以便我们可以开始更新它:\n\n```cpp\nglGenTextures( 1,\n                &circle_tex);\n\nglActiveTexture(GL_TEXTURE0);\nglBindTexture(GL_TEXTURE_2D, circle_tex);\n```\n\n现在我们已经准备好更新圆形纹理，我们可以使用 SDL 加载图像文件:\n\n```cpp\nsurface = IMG_Load( \"/sprites/circle.png\" );\n\nif( !surface ) {\n    printf(\"failed to load image: %s\\n\", IMG_GetError() );\n    return 0;\n}\n```\n\n接下来，我们需要将该数据加载到绑定纹理中:\n\n```cpp\nglTexImage2D( GL_TEXTURE_2D,\n                0,\n                GL_RGBA,\n                128, // sprite width\n                128, // sprite height\n                0,\n                GL_RGBA,\n                GL_UNSIGNED_BYTE,\n                surface );\n```\n\n然后，我们可以激活纹理，生成纹理贴图，并释放表面:\n\n```cpp\nglUniform1i(u_texture_location, 1);\nglGenerateMipmap(GL_TEXTURE_2D);\n\nSDL_FreeSurface( surface );\n```\n\n在为我们的圆形纹理做了这些之后，我们需要为我们的普通贴图做同样的一系列步骤:\n\n```cpp\nglGenTextures( 1,\n                &normal_tex);\n\nglActiveTexture(GL_TEXTURE1);\nglBindTexture(GL_TEXTURE_2D, normal_tex);\nsurface = IMG_Load( \"/sprites/ball-normal.png\" );\n\nif( !surface ) {\n    printf(\"failed to load image: %s\\n\", IMG_GetError() );\n    return 0;\n}\n\nglTexImage2D( GL_TEXTURE_2D,\n    0,\n    GL_RGBA,\n    128, // sprite width\n    128, // sprite height\n    0,\n    GL_RGBA,\n    GL_UNSIGNED_BYTE,\n    surface );\n\nglUniform1i(u_normal_location, 1);\nglGenerateMipmap(GL_TEXTURE_2D);\n\nSDL_FreeSurface( surface );\n```\n\n我们将以不同的方式处理最终纹理，因为它将只使用 SDL 渲染。你现在应该很熟悉了。我们需要从图像文件加载表面，从表面创建纹理，查询该纹理的大小，然后释放原始表面:\n\n```cpp\nsurface = IMG_Load( \"/sprites/light.png\" );\n\nif( !surface ) {\n    printf(\"failed to load image: %s\\n\", IMG_GetError() );\n    return 0;\n}\n\nlight_texture = SDL_CreateTextureFromSurface( renderer, surface );\n\nif( !light_texture ) {\n    printf(\"failed to create light texture: %s\\n\", IMG_GetError() );\n    return 0;\n}\n\nSDL_QueryTexture( light_texture,\n                    NULL, NULL,\n                    &light_width, &light_height );\n\nSDL_FreeSurface( surface );\n```\n\n既然我们已经创建了纹理，我们应该设置我们的 alpha 混合:\n\n```cpp\nglBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);\nglEnable(GL_BLEND);\n```\n\n我们的`main`函数的最后一行使用 Emscripten 调用游戏循环:\n\n```cpp\nemscripten_set_main_loop(game_loop, 0, 0);\n```\n\n# 游戏循环功能\n\n既然我们已经定义了`main`函数，我们需要定义我们的`game_loop`。因为`game_loop`函数使用 SDL 和 OpenGL 进行渲染，所以每次在 OpenGL 渲染之前，我们都需要通过循环设置顶点属性指针。我们还需要在多个 OpenGL 程序之间切换，因为 SDL 使用的着色程序与我们使用的 OpenGL 程序不同。让我首先向您展示整个功能，然后我们可以一次一个地浏览:\n\n```cpp\nvoid game_loop() {\n    input();\n\n    glGetIntegerv(GL_CURRENT_PROGRAM,&sdl_program);\n    glUseProgram(program);\n\n    glClearColor( 0, 0, 0, 1 );\n    glClear( GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT );\n\n    glBindBuffer(GL_ARRAY_BUFFER, vertex_texture_buffer);\n    glVertexAttribPointer(\n        a_position_location,       // set up the a_position attribute\n        2,                         // how many attributes in the \n                                   //position\n        GL_FLOAT,                  // data type of float\n        GL_FALSE,                  // the data is not normalized\n        4 * sizeof(float),         // stride (how many array items \n                                   //until the next position)\n        (void*)0                   // starting point for attribute\n     );\n\n    glEnableVertexAttribArray(a_texcoord_location);\n    glBindBuffer(GL_ARRAY_BUFFER, vertex_texture_buffer);\n    glVertexAttribPointer(\n        a_texcoord_location,     // set up the a_texcoord attribute\n        2,                       // how many attributes in the texture \n                                 //coordinates\n        GL_FLOAT,                // data type of float\n        GL_FALSE,                // the data is not normalized\n        4 * sizeof(float),       // stride (how many array items until \n                                 //the next position)\n        (void*)(2 * sizeof(float)) // starting point for attribute\n    );\n\n    glUniform3f( u_light_pos_location,\n                (float)(light_x), (float)(600-light_y), (float)(light_z) );\n\n    glDrawArrays(GL_TRIANGLES, 0, 6);\n\n    glUseProgram(sdl_program);\n    draw_light_icon();\n}\n\n```\n\n游戏循环的第一行调用`input`函数。该功能将使用鼠标输入来设置灯光位置。第二行和第三行检索 SDL 着色器程序，并将其保存到`sdl_program`变量。然后，调用`glUseProgram`，切换到自定义的 OpenGL 着色器。以下是我们调用来保存当前程序并设置新程序的两行代码:\n\n```cpp\nglGetIntegerv(GL_CURRENT_PROGRAM,&sdl_program);\nglUseProgram(program);\n```\n\n之后，我们调用 OpenGL 来清除画布:\n\n```cpp\nglClearColor( 0, 0, 0, 1 );\nglClear( GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT );\n```\n\n接下来，我们需要设置我们的几何图形:\n\n```cpp\nglBindBuffer(GL_ARRAY_BUFFER, vertex_texture_buffer);\nglVertexAttribPointer(\n            a_position_location,   // set up the a_position attribute\n            2,                     // how many attributes in the \n                                   //position\n            GL_FLOAT,              // data type of float\n            GL_FALSE,              // the data is not normalized\n            4 * sizeof(float),     // stride (how many array items \n                                   //until the next position)\n            (void*)0               // starting point for attribute\n);\n\nglEnableVertexAttribArray(a_texcoord_location);\nglBindBuffer(GL_ARRAY_BUFFER, vertex_texture_buffer);\nglVertexAttribPointer(\n    a_texcoord_location,          // set up the a_texcoord attribute\n    2,                            // how many attributes in the texture \n                                  //coordinates\n    GL_FLOAT,                     // data type of float\n    GL_FALSE,                     // the data is not normalized\n    4 * sizeof(float),            // stride (how many array items until \n                                  //the next position)\n    (void*)(2 * sizeof(float))    // starting point for attribute\n);\n```\n\n然后，我们使用对`glUniform3f`的调用将`vec3 uniform u_light_pos`变量设置为我们之前定义的`light_x`、`light_y`和`light_z`全局变量。这些灯光位置可以用鼠标移动。允许用户移动灯光的代码将在后面我们编写`input`功能时定义。在我们设置了光线位置的值后，我们可以使用 OpenGL 绘制我们的三角形:\n\n```cpp\nglDrawArrays(GL_TRIANGLES, 0, 6);\n```\n\n最后，我们需要切换回我们的 SDL 程序并调用`draw_light_icon`函数，该函数将使用 SDL 绘制我们的灯光图标:\n\n```cpp\nglUseProgram(sdl_program);\ndraw_light_icon();\n```\n\n# 输入功能\n\n既然我们已经定义了我们的游戏循环，我们将需要编写一个函数来捕获我们的鼠标输入。我希望能够点击我们的画布，让灯光图标和光源移动到我刚刚点击的位置。我还希望能够按住鼠标按钮，并在画布上拖动灯光图标，以查看当灯光位于画布上不同位置时，阴影是如何工作的。这些代码的大部分看起来都很熟悉。我们使用`SDL_PollEvent`来检索事件，并查看鼠标左键是否按下，或者用户是否移动了滚轮。如果用户转动了滚轮，`light_z`变量会改变，这又会改变我们光源的`z`位置。我们使用`static int mouse_down`变量来跟踪用户是否按下了鼠标按钮。如果用户按下鼠标按钮，我们将调用`SDL_GetMouseState`来检索`light_x`和`light_y`变量，这将修改我们的光源的 x 和 y 位置。以下是输入函数的完整代码:\n\n```cpp\nvoid input() {\n    SDL_Event event;\n    static int mouse_down = 0;\n\n    if(SDL_PollEvent( &event ) )\n    {\n        if(event.type == SDL_MOUSEWHEEL )\n        {\n            if( event.wheel.y > 0 ) {\n                light_z+= 100;\n            }\n            else {\n                light_z-=100;\n            }\n\n            if( light_z > 10000 ) {\n                light_z = 10000;\n            }\n            else if( light_z < -10000 ) {\n                light_z = -10000;\n            }\n        }\n        else if(event.type == SDL_MOUSEMOTION )\n        {\n            if( mouse_down == 1 ) {\n                SDL_GetMouseState( &light_x, &light_y );\n            }\n        }\n        else if(event.type == SDL_MOUSEBUTTONDOWN )\n        {\n            if(event.button.button == SDL_BUTTON_LEFT)\n            {\n                SDL_GetMouseState( &light_x, &light_y );\n                mouse_down = 1;\n            }\n        }\n        else if(event.type == SDL_MOUSEBUTTONUP )\n        {\n            if(event.button.button == SDL_BUTTON_LEFT)\n            {\n                mouse_down = 0;\n            }\n        }\n    }\n}\n```\n\n# 绘图灯图标功能\n\n我们在`lighting.c`文件中需要定义的最后一个函数是`draw_light_icon`函数。该函数将使用 SDL 根据`light_x`和`light_y`变量中的值绘制我们的灯光图标。我们创建一个名为`dest`的`SDL_Rect`变量，并设置该结构的`x`、`y`、`w`和`h`属性。然后我们调用`SDL_RenderCopy`在适当的位置渲染我们的灯光图标。下面是该函数的代码:\n\n```cpp\nvoid draw_light_icon() {\n    SDL_Rect dest;\n    dest.x = light_x - light_width / 2 - 32;\n    dest.y = light_y - light_height / 2;\n    dest.w = light_width;\n    dest.h = light_height;\n\n    SDL_RenderCopy( renderer, light_texture, NULL, &dest );\n}\n```\n\n# 编译和运行我们的照明应用\n\n当我们编译和运行我们的照明应用时，我们应该能够在画布上点击和拖动我们的灯光。我们有一个与普通地图相关联的小圆。加上我们的阴影和照明，它应该会让那个圆圈看起来更像一个闪亮的按钮。在命令行执行以下命令编译`lighting.html`文件:\n\n```cpp\nemcc lighting.c -o lighting.html --preload-file sprites -s USE_SDL=2 -s USE_SDL_IMAGE=2 -s SDL2_IMAGE_FORMATS=[\"png\"]\n```\n\n现在，您应该能够从网络服务器或 emrun 提供`lighting.html`文件。如果一切顺利，这款应用应该是什么样子:\n\n![](img/aade1966-f9f3-46d9-a525-75605d4672ca.png)\n\nFigure 15.6: Screenshot of the 2D lighting app\n\n# 摘要\n\n在本章中，我们在[第 3 章](03.html)、*WebGL*的简介中介绍了概念之后，进一步了解了着色器，当时我们构建了一个 WebGL 应用。当您使用 OpenGL 进行 WebAssembly 时，了解 WebGL 是有帮助的，因为从 WebAssembly 对 OpenGL 的每次调用都是在内部调用相应的 WebGL 函数。我们从用 C++ 中的 OpenGL ES 和 SDL 的组合重建那个 WebGL 应用开始，并将其编译成 WebAssembly。然后，我们学习了如何使用 OpenGL 和着色器以有趣的方式混合不同的纹理。我们利用这些知识在宇宙飞船的引擎周围创造了一个脉冲辉光。最后，我们讨论了三维照明和普通地图，然后开发了一个 2D 照明模型，并创建了一个应用，允许我们用该照明模型照亮一个简单的圆圈。这个应用展示了 2D 照明的可能性，它允许我们用一张普通的地图围绕 2D 圆移动我们的光线，这张地图用来给 2D 表面以深度的外观。\n\n在下一章中，我们将讨论调试我们的 WebAssembly 应用以及可以用于性能测试的工具。"
  },
  {
    "path": "docs/handson-game-dev-wasm/16.md",
    "content": "# 十六、调试和优化\n\n在最后一章中，我们将讨论两个主题，这两个主题将有助于您继续使用 Emscripten 创建游戏和在 WebAssembly 中构建。我们将讨论调试和优化的主题。我们将在优化之前进行调试，因为构建代码以输出更多调试信息会妨碍优化。我们将从使用一些基本的调试技术开始，例如打印堆栈跟踪和定义调试宏，我们可以通过更改编译标志来删除这些宏。然后，我们将转向一些更高级的调试技术，例如使用 Emscripten 标志进行编译，这允许我们在 Firefox 和 Chrome 中跟踪代码。我们还将讨论使用火狐和 Chrome 开发工具进行调试之间的一些差异。\n\nYou will need to include several images in your build to make this project work. Make sure that you include the `/Chapter16/sprites/` folder from this project's GitHub repository. If you haven't downloaded the GitHub project yet, you can get it online here: [https://github.com/PacktPublishing/Hands-On-Game-Development-with-WebAssembly](https://github.com/PacktPublishing/Hands-On-Game-Development-with-WebAssembly).\n\n讨论完调试后，我们将继续优化。我们将讨论您可以与 Emscripten 一起使用的优化标志，以及使用剖析器来确定您的游戏或应用可能存在性能问题的地方。我们将讨论优化 WebAssembly 部署代码的一般技术。最后，我们将讨论与网络游戏和 WebAssembly 模块进行的网络 GL 调用相关的优化。\n\n# 调试宏和堆栈跟踪\n\n您可以开始调试代码的一种方法是使用`#define`创建一个调试宏，我们可以通过向 Emscripten 编译器传递一个标志来激活它。然而，如果我们不通过那面旗帜，这将化为乌有。宏很容易添加，如果我们带着调试标志运行，我们可以创建一个打印行的调用，但是如果我们不这样做，就不会降低性能。如果您不熟悉预处理器命令，它们是在编译代码时而不是在运行时向编译器发出的评估命令。例如，如果我使用了`#ifdef PRINT_ME`命令，那么只有当`PRINT_ME`宏在代码的前面一行用`#define PRINT_ME`宏定义时，或者当我们运行编译器时用传递给编译器的`-DPRINT_ME`标志编译源代码时，这一行代码才会被编译成我们的源代码。假设我们的`main`函数中有以下代码块:\n\n```cpp\n#ifdef PRINT_ME\n    printf(\"PRINT_ME was defined\\n\");\n#else\n    printf(\"nothing was defined\\n\");\n#endif\n```\n\n如果我们这样做了，我们就会编译并运行这些代码。网络浏览器的控制台打印以下内容:\n\n```cpp\n\"nothing was defined\"\n```\n\n如果我们用`-DPRINT_ME`标志编译它，然后在命令行运行代码，我们会看到下面的输出:\n\n```cpp\n\"PRINT_ME was defined\"\n```\n\n如果您将代码分解成 WebAssembly 文本，那么您将看不到任何打印“没有定义任何东西”的原始`printf`语句的提示。在编译时，代码被移除。这使得预处理器宏在创建我们希望在开发阶段包含的代码时非常有用。\n\nIf you are using the `-D` flag to include debug macros in your code, make sure that you don't include that flag when you are compiling for release, as that will continue to include all of your debug macros when you don't want them. You may want to consider having a `-DRELEASE` flag that overrides your `-DDEBUG` flag when you compile your code for general release.\n\n将所有`printf`调用限制在一个宏中是一个很好的方法，可以确保您删除了所有对`printf`的调用，这将在您发布应用时降低其速度。让我们以`webgl-redux.c`文件作为基线来尝试一下。根据我们在上一章中创建的代码，将`webgl-redux.c`复制并粘贴到名为`debug.cpp`的文件中。我们将在这个文件的开头添加调试宏。紧接在包含`emscripten.h`的行之后，但在定义画布宽度的代码行之前，添加以下代码块:\n\n```cpp\n#ifdef DEBUG\n    void run_debug(const char* str) {\n        EM_ASM (\n            console.log(new Error().stack);\n        );\n        printf(\"%s\\n\", str);\n    }\n\n    #define DBG(str) run_debug(str)\n#else\n    #define DBG(str)\n#endif\n```\n\n如果我们将`-DDEBUG`标志传递给编译器，这段代码只会编译`run_debug`函数。用户不应该直接运行`run_debug`功能，因为不使用`-DDEBUG`标志就不存在。相反，我们应该使用`DBG`宏观功能。不管我们是否使用`-DDEBUG`标志，这个宏都存在。如果我们使用这个标志，函数调用`run_debug`函数。如果我们不使用这个标志，对`DBG`的调用就会神奇地消失。`run_debug`函数不仅使用`printf`打印出一个字符串，还使用`EM_ASM`将堆栈跟踪转储到 JavaScript 控制台。堆栈跟踪会注销当前在 JavaScript 堆栈上的每个函数。让我们添加几个最终会调用我们的`DBG`宏的函数调用。这些应在`main`功能之前添加:\n\n```cpp\nextern \"C\" {\n    void inner_call_1() {\n        DBG(\"check console log for stack trace\");\n    }\n    void inner_call_2() {\n        inner_call_1();\n    }\n    void inner_call_3() {\n        inner_call_2();\n    }\n}\n```\n\n在我们的`main`函数中，我们应该添加对`inner_call_3()`的调用，如下所示:\n\n```cpp\nint main() {\n    inner_call_3();\n```\n\n现在，让我们用以下命令编译我们的`debug.cpp`文件:\n\n```cpp\nemcc debug.cpp -o index.html -DDEBUG --preload-file sprites -s USE_SDL=2 -s USE_SDL_IMAGE=2 -s SDL2_IMAGE_FORMATS=[\"png\"]\n```\n\n这会将`debug.cpp`文件编译成`index.html`文件。如果我们从 web 服务器提供该文件，并在浏览器中打开它，我们将在 JavaScript 控制台中看到以下内容:\n\n```cpp\nError\n at Array.ASM_CONSTS (index.js:1901)\n at _emscripten_asm_const_i (index.js:1920)\n at :8080/wasm-function[737]:36\n at :8080/wasm-function[738]:11\n at :8080/wasm-function[739]:7\n at :8080/wasm-function[740]:7\n at :8080/wasm-function[741]:102\n at Object.Module._main (index.js:11708)\n at Object.callMain (index.js:12096)\n at doRun (index.js:12154)\n\n(index):1237 check console log for stack trace\n```\n\n您会注意到我们有一个堆栈跟踪，后面是我们的消息`check console log for stack trace`，这是我们传递到`DBG`宏中的字符串。如果你仔细观察，你可能会注意到一件事，那就是这个堆栈跟踪没有太大帮助。堆栈跟踪中的大多数函数都被标记为`wasm-function`，从调试的角度来看，这有点没用。这是因为我们在编译过程中丢失了函数名。为了保留这些名称，我们需要在编译时将`-g4`标志传递给 Emscripten。`-g`标志后面跟一个数字，告诉编译器在编译过程中要保留多少调试信息，其中`-g0`是最少的信息量，`-g4`是最多的。如果我们想创建源映射，将我们的 WebAssembly 映射到创建它的 C/C++ 源代码，我们需要传入`-g4`命令，如果我们想知道堆栈跟踪调用的函数，我们也需要`-g4`命令。让我们尝试用我们的`-g4`标志重新编译。以下是新版本的`emcc`命令:\n\n```cpp\nemcc debug.cpp -o index.html -g4 -DDEBUG --preload-file sprites -s USE_SDL=2 -s USE_SDL_IMAGE=2 -s SDL2_IMAGE_FORMATS=[\"png\"]\n```\n\n现在，重新加载页面并检查控制台。在下面的代码片段中，我们有了新的堆栈跟踪:\n\n```cpp\nError\n at Array.ASM_CONSTS (index.js:1901)\n at _emscripten_asm_const_i (index.js:1920)\n at __Z9run_debugPKc (:8080/wasm-function[737]:36)\n at _inner_call_1 (:8080/wasm-function[738]:11)\n at _inner_call_2 (:8080/wasm-function[739]:7)\n at _inner_call_3 (:8080/wasm-function[740]:7)\n at _main (:8080/wasm-function[741]:102)\n at Object.Module._main (index.js:11708)\n at Object.callMain (index.js:12096)\n at doRun (index.js:12154)\n (index):1237 check console log for stack trace\n```\n\n这可读性更强。您可以看到我们定义的所有内部调用函数，以及`main`函数。但是`run_debug`怎么了？结果是这样的:\n\n```cpp\n __Z9run_debugPKc\n```\n\n这里发生的事情被称为 C++ 名称 mangling，我们在前面的章节中对此进行了简要讨论。因为 C++ 允许函数重载，编译器*会修改*函数的名称，这样每个版本的函数都有不同的名称。我们能够通过将它们放在一个标有`extern \"C\"`的块中来防止这种情况发生。这告诉编译器不要篡改这些函数的名称。这对于调试来说并不是绝对必要的，但是我想演示如何向这个块中添加函数，以便在堆栈跟踪中更容易识别我们的函数。如果我移除`extern \"C\"`块，同样的堆栈跟踪看起来如下:\n\n```cpp\nError\n at Array.ASM_CONSTS (index.js:1901)\n at _emscripten_asm_const_i (index.js:1920)\n at __Z9run_debugPKc (:8080/wasm-function[737]:36)\n at __Z12inner_call_1v (:8080/wasm-function[738]:11)\n at __Z12inner_call_2v (:8080/wasm-function[739]:7)\n at __Z12inner_call_3v (:8080/wasm-function[740]:7)\n at _main (:8080/wasm-function[741]:102)\n at Object.Module._main (index.js:11708)\n at Object.callMain (index.js:12096)\n at doRun (index.js:12154)\n (index):1237 check console log for stack trace\n```\n\n如您所见，我们所有的内部调用函数都被破坏了。在下一节中，我们将讨论源地图。\n\n# 源地图\n\n现在，让我们简单讨论一下源图。回到网络的早期，人们决定用户应该能够查看每个网页上的所有源代码。早期，这一直是 HTML，但后来，JavaScript 被添加进来，成为用户可以查看的东西，试图理解给定网页的工作原理。今天，这在大多数情况下是不可能的。今天的一些代码，比如 TypeScript，被从另一种语言翻译成了 JavaScript。如果您正在编写 JavaScript，您可以使用 Babel 来转换最新的 JavaScript，以便在旧的网络浏览器上运行。Uglify 或 Minify 可用于删除空格和缩短变量名。如果您需要调试原始源代码，源代码映射是一种工具，您可以使用它将浏览器中运行的 JavaScript 映射回原始源代码。\n\n源映射是一个 JSON 文件，它包含机器生成的 JavaScript 输出代码的数据映射，并将其指向手写的 JavaScript 或其他语言，如 TypeScript 或 CoffeeScript。应用可以通过两种方式告诉 web 浏览器有一个与给定代码段相关联的源映射文件。我们可以在代码中包含带有`sourceMappingURL`指令的注释，或者我们可以在该文件的 HTTP 头中包含一个`SourceMap`。如果我们使用的是`sourceMappingURL`注释方法，请在输出 JavaScript 文件的末尾添加以下一行:\n\n```cpp\n//# sourceMappingURL=http://localhost:8080/debug.wasm.map\n```\n\n这通常是在构建过程中以编程方式完成的。另一种方法是在 HTTP 头中添加以下一行:\n\n```cpp\nSourceMap: http://localhost:8080/debug.wasm.map\n```\n\n在下一节中，我们将讨论基于浏览器的 WebAssembly 调试工具。\n\n# 浏览器调试\n\n在 web 浏览器中调试 WebAssembly 仍然相当粗糙。例如，在编写时，仍然不可能使用调试器直接*观察*一个变量。在 Firefox 和 Chrome 中，您必须偶尔刷新浏览器才能看到 CPP 源文件。与调试 JavaScript 不同，WebAssembly 调试器感觉(讽刺的是)有问题。在 Chrome 中，您经常需要多次单击“单步执行”按钮来推进代码行。在两种浏览器中，断点有时都无法工作。\n\n我经常不得不删除，然后重新添加一个断点，让他们再次工作。WebAssembly 源代码图和浏览器内调试还为时过早，所以希望情况很快会有所改善。在此之前，请尝试将浏览器中的调试与添加调试语句结合起来，就像我之前建议的那样。\n\n# 编译代码进行调试\n\n正如我之前提到的，我们将需要编译我们的应用来支持源地图，我们可以在火狐和 Chrome 中使用它进行浏览器内调试。目前，唯一支持浏览器内调试的浏览器是火狐、Chrome 和 Safari。我只会在这本书里介绍火狐和 Chrome。您可以使用以下`emcc`命令编译`debug.cpp`文件，以便与网络程序集调试器一起使用:\n\n```cpp\nemcc -g4 debug.cpp -o debug.html --source-map-base http://localhost:8080/ --preload-file sprites -s USE_SDL=2 -s USE_SDL_IMAGE=2 -s SDL2_IMAGE_FORMATS=[\"png\"] -s MAIN_MODULE=1 -s WASM=1\n```\n\n第一个新标志是`-g4`，它指示编译器拥有最高数量的调试数据，并为我们的 WebAssembly 创建源映射文件。之后是`--source-map-base http://localhost:8080/`标志，它告诉编译器将`sourceMappingURL$http://localhost:8080/debug.wasm.map`字符串添加到`debug.wasm`文件的末尾。这允许浏览器找到与`debug.wasm`文件相关联的源地图文件。最后两个新标志是`-s MAIN_MODULE=1`和`-s WASM=1`。我不确定为什么需要这两个标志来使源映射工作。这两个标志都明确告诉编译器运行默认行为。但是，在编写时，如果不包含这些标志，浏览器调试将不起作用。这在我看来是一个 bug，所以有可能在你阅读这篇文章的时候，`emcc`不需要最后的两个标志。使用前面的命令进行编译将允许您在 Chrome 和 Firefox 上使用 WebAssembly 调试器进行测试。如果您真的想在 Opera、Edge 或其他不支持 WebAssembly 调试的调试器上进行调试，您确实有一个替代方案。\n\n# 使用 asm.js 作为调试的替代方案\n\n无论出于什么原因，您可能会觉得使用 Edge 或 Opera 进行调试是必要的。如果您觉得必须在没有 WebAssembly 调试器的浏览器中进行调试，您可以选择为 asm.js 进行编译。如果是，将`-s WASM=1`标志改为`-s WASM=0`，你就被设置了。这将创建一个 JavaScript 文件，而不是一个 WASM 文件，但是这两个文件(理论上)应该表现相同。\n\n# 使用 Chrome 调试\n\nChrome 有一些很好的调试 JavaScript 的工具，但是在调试 WebAssembly 的时候仍然很粗糙。构建应用后，在 Chrome 中打开它，然后打开 Chrome 开发工具:\n\n![](img/1a11a945-2feb-426c-8ab0-79ce61d0d3ca.png)\n\nFigure 16.1: Screenshot of opening Chrome Developer Tools using the menu\n\n您可以使用浏览器左上角的菜单打开它，如前面的截图所示，或者您可以通过按键盘上的*Ctrl*+*Shift*+*I*打开开发人员工具。当你在 Chrome 中加载你的`debug.html`文件时，你需要点击开发者窗口中的来源标签。如果您在“来源”选项卡上，应该是这样的:\n\n![](img/3d5b7fd9-415c-4edb-a9f4-c2e387054c2f.png)\n\nFigure 16.2: Screenshot using the sources tab in Chrome Developer Tools\n\n如果在“来源”标签中没有看到`debug.cpp`，可能需要点击顶部 URL 旁边的浏览器的重新加载按钮来重新加载页面。正如我前面所说的，界面感觉有点问题，有时 CPP 文件在第一次尝试时没有加载。希望当你读到这篇文章的时候，这已经改变了。一旦您选择了 CPP 文件，您应该能够在开发人员工具窗口中心的代码窗口中看到我们的`debug.cpp`文件中的 C++ 代码。通过单击代码行旁边需要断点的行号，可以在 C++ 代码中设置断点。然后，您可以使用`Watch`变量上方的按钮单步执行代码。虽然手表变量在编写时不起作用，但您可能还是想尝试一下。WebAssembly 几乎每天都在改进，错误修复也在不断发生，所以当你读到这篇文章的时候，事情可能已经发生了变化。如果没有，您可以使用`Local`变量来了解哪些值在变化。\n\n您可以在逐步浏览源代码时看到这些变量被填充，并且您可以通过观察这些值的变化来频繁地确定哪些变量被更新。请看下面的截图:\n\n![](img/2194af80-6559-4c96-85cf-a4aa50496525.png)\n\nFigure 16.3: Screenshot of the debug tools in the Chrome browser\n\n在编写时，您需要多次单击“单步执行”按钮，以使该行在 C++ 代码窗口中前进。在 Chrome 中，单步执行按钮是每次点击推进一条 WebAssembly 指令，而不是一条 C++ 指令。当您阅读本文时，这可能已经发生了变化，但是如果您需要多次单击“单步执行”来浏览代码，请不要感到惊讶。\n\n# 使用 Firefox 进行调试\n\n与 Chrome 相比，Firefox 有很多优点和缺点。从好的方面来说，你可以在 C++ 代码中的每一行点击一次火狐中的单步执行按钮。不利的一面是，这使得知道哪些局部变量在响应您正在执行的行而改变变得更加困难。这些`Local`变量有点像基于寄存器的汇编语言中的寄存器，因此同一个变量可以进出其中的几个。如果您必须在每个汇编指令中单击一次按钮，那么遵循这些值可能会更容易一些。但是，如果您对跟踪代码流比了解每个 WebAssembly 指令的值变化更感兴趣，那么火狐在这方面要好得多。\n\n要打开火狐开发者工具，请单击浏览器窗口右上角的菜单按钮，然后选择网络开发者:\n\n![](img/571431e1-1c13-455c-9300-6d5f00ab92f2.png)\n\nFigure 16.4: Web Developer tools in the Firefox browser\n\n在“网站开发人员”菜单上，单击“调试器”菜单项打开“调试器”窗口:\n\n![](img/ee20cca5-4719-4b79-8de6-4dc4ef3b5887.png)\n\nFigure 16.5: Screenshot of opening Debugger in Firefox\n\n不用通过菜单系统选择调试器，可以使用快捷键*Ctrl*+*Shift*+*C*打开检查器，然后从 Web Developer 窗口选择调试器选项卡。以下是您在火狐调试器中的样子:\n\n![](img/a40bc212-12a6-460a-b385-3262e7b75831.png)\n\nFigure 16.6: Screenshot of using Debugger in the Firefox browser\n\n现在，调试需要将调试宏的使用(如前一节所述)与浏览器完全理解正在发生的事情的能力结合起来。\n\n# 火狐开发者版\n\n我将简要提及火狐开发者版。如果您更喜欢使用火狐作为您的主要网络组件开发浏览器，您可能需要考虑使用火狐开发者版。开发者版比标准版火狐更快地推进网络开发者工具的更新。因为 WebAssembly 是如此新，所以改进开发体验的更新很可能会比标准版本早几周或几个月出现在开发人员版中。在撰写本文时，这两个版本之间没有显著差异，但如果您有兴趣试用，可在以下网址获得:[https://www.mozilla.org/en-US/firefox/developer/](https://www.mozilla.org/en-US/firefox/developer/)。\n\n# WebAssembly 的优化\n\n优化您的 WebAssembly 代码部分是关于决策和实验。它是关于发现什么适合你的特定游戏或应用。例如，当设计 WebAssembly 时，决定让 WebAssembly 字节码在虚拟堆栈机器上运行。WebAssembly 的设计者之所以做出这个选择，是因为他们觉得可以用小得多的字节码下载量来弥补性能的小损失。每段代码都有一个瓶颈。在 OpenGL 应用中，瓶颈将是与图形处理器的接口。应用的瓶颈可能是内存，也可能是受 CPU 限制的。总的来说，优化代码是关于确定什么是延迟，以及决定你想要做什么样的权衡来改进事情。如果优化下载大小，可能会损失一些运行时性能。如果优化运行时性能，可能需要增加内存占用。\n\n# 优化标志\n\nEmscripten 为我们提供了大量的标志选择，以针对不同的潜在瓶颈进行优化。所有的优化标志都会导致不同程度的更长编译时间，所以使用这些标志应该会出现在开发周期的后期。\n\n# 优化性能\n\n我们可以使用`-O`标志进行常规优化。`-O0`、`-O1`、`-O2`和`-O3`在编译时和代码性能之间提供了不同程度的权衡。`-O0`和`-O1`标志提供最小的优化。`-O2`标志提供了从`-O3`标志获得的大部分优化，但是编译时间明显缩短。最后，`-O3`提供了最高级别的优化，但编译时间比任何其他标志都长，所以最好等到开发接近尾声时再开始使用。除了`-O`标志之外，`-s AGGRESSIVE_VARIABLE_ELIMINATION=1`可以用来提高性能，但是可能会导致更大的字节码下载量。\n\n# 优化尺寸\n\n还有另外两个`-O`标志，我在前面部分没有提到。这些标志用于优化字节码下载大小，而不是纯粹的性能优化。`-Os`标志的时间大约与`-O3`一样长，并且提供了尽可能多的性能优化，但是牺牲了一些`-O3`优化以支持更小的下载量。`-Oz`就像`-Os`一样，但是通过牺牲更多的性能优化来进一步优先考虑更小的下载量，这导致了更小的字节码。另一种优化尺寸的方法是加入`-s ENVIRONMENT='web'`标志。只有在为网站编译时，才应该使用此标志。它删除了任何用于支持其他环境的源代码，例如 Node.js\n\n# 不安全标志\n\n除了到目前为止我们一直在使用的安全优化标志之外，Emscripten 还允许两个*不安全*标志，它们可以提高性能，但有可能破坏您的代码。这些标志是高风险/高回报的优化，您应该只在大量测试完成之前使用。使用`--closure 1`标志运行 Closure JavaScript 编译器，它对我们应用中的 JavaScript 执行非常激进的优化。但是，您不应该使用`--closure 1`标志，除非您已经熟悉使用闭包编译器以及编译器可能对 JavaScript 产生的影响。第二个*不安全*标志是`--llvm-lto 1`标志，它在 LLVM 编译步骤中启用*链接时间优化*。这个过程可能会破坏您的代码，所以在使用这个标志时要特别小心。\n\n# 压型\n\n分析是确定源代码中存在哪些瓶颈的最佳方法。当您分析 WebAssembly 模块时，我建议您在编译时使用`--profiling`标志。没有它你也可以配置，但是你调用的所有模块功能都会被贴上`wasm-function`的标签，这会让你的生活变得比需要的更艰难。用`--profile`标志编译完代码后，在 Chrome 中打开一个新的*隐姓埋名*窗口。\n\n您可以通过按下 *CTRL + SHIFT + N* 键，或者通过浏览器右上角的菜单来实现:\n\n![](img/1b6fc82e-f25b-4487-87b5-2632a5b32714.png)\n\nFigure 16.7: Opening an Incognito window in the Chrome browser\n\n打开一个匿名窗口将会阻止任何 Chrome 扩展在分析你的应用时运行。这将防止您不得不费力地通过这些扩展中的代码来获取应用中的代码。打开微服窗口后，按*Ctrl*+*Shift*+*I*查看页面。这将在浏览器窗口的底部打开 Chrome 开发工具。在 Chrome 开发工具中，选择性能选项卡，如下图所示:\n\n![](img/ed201a26-d861-4f07-91b7-29d544bc9195.png)\n\nFigure 16.8: The Performance tab in the Chrome browser\n\n现在，点击记录按钮，让它运行几秒钟。记录五六秒钟后，单击停止按钮停止分析:\n\n![](img/f360963f-d7da-48ee-b60c-4e69981c71d9.png)\n\nFigure 16.9: Screenshot of recording performance metrics in the Chrome browser\n\n停止分析后，您将在性能窗口中看到数据。这称为“摘要”选项卡，并以饼图的形式显示数据，该饼图按毫秒数细分应用在各种任务上花费的时间。\n\n如您所见，绝大多数时间，我们的应用都处于闲置状态:\n\n![](img/d8fea306-0daa-4c78-a71c-a1b49c4397cf.png)\n\nFigure 16.10: Performance overview in the Chrome browser\n\n总结很有意思。它可以在很高的层次上告诉您瓶颈在哪里，但是要评估我们的网络组件，我们需要查看调用树选项卡。单击呼叫树选项卡，您将看到下面的 window:￼\n\n![](img/e3aa8cd8-4542-4c9e-bc91-70ecf0f28220.png)\n\nFigure 16.11: Screenshot of the Call Tree in the Chrome browser\n\n因为我们的`game_loop`函数每一帧都被调用，所以我们可以在`Animation Frame Fired`树中找到调用。往下钻，寻找`game_loop`。当我们找到这个函数时，它被破坏了，因为它是一个 C++ 函数。所以，我们看到的不是`_game_loop`，而是`_Z9game_loopv`，尽管你可能会看到不同的东西。如果您想防止这种混乱，您可以将此功能包装在`extern \"C\"`块中。\n\n可以看到，这个函数的执行总共占用了浏览器 3.2%的 CPU 时间。您也可以从这个函数中查看每个 OpenGL 调用。如果你看看我们的游戏循环，超过一半的 CPU 时间都花在`_glClear`上。这对于这个应用来说不是问题，因为绝大多数浏览器的 CPU 时间都是闲置的。然而，如果我们的游戏循环函数占用了很大一部分 CPU 时间，我们就需要看看我们在这个函数中花了多少时间。\n\n# 尝试/捕捉块的问题\n\n在编写本文时，众所周知，try/catch 块会在 WebAssembly 模块中导致严重的性能问题，因此只有在绝对必要时才使用它们。您可能希望在开发阶段使用它们，并在构建版本时删除它们。一些`-O`优化标志将删除 try/catch 块，如果您计划在生产中使用它们，您需要注意这些块。如果您想在生产构建中使用 try/catch 块，您需要使用`-s DISABLE_EXCEPTION_CATCHING=0`标志进行编译。这将告诉编译器不要从字节码的优化版本中移除 try/catch 块。如果您想从未优化的开发代码中删除您的 try/catch 块，您可以使用`-s DISABLE_EXCEPTION_CATCHING=1`标志来完成。\n\n# 面向 WebAssembly 的 OpenGL 优化\n\n重要的是要记住，从 WebAssembly 对 OpenGL 的任何调用都是使用函数表调用 WebGL。这一点很重要的部分原因是，每当您使用 OpenGL ES 和 OpenGL 功能(在 WebGL 中不可用)时，Emscripten 必须对这些功能执行一些非常慢的软件仿真。同样重要的是要记住，在原生平台上，WebGL 调用比 OpenGL 调用更昂贵，因为 WebGL 是沙箱化的，浏览器在调用 WebGL 时会执行各种安全检查。Emscripten 为您提供了几个标志，允许您模拟在 WebGL 中不可用的 OpenGL 和 OpenGL ES 调用。但是，出于性能原因，除非绝对必要，否则不要使用这些函数。\n\n# 如果可能的话，使用 WebGL 2.0\n\nWebGL 2.0 比 WebGL 1.0 更快，但是，在撰写本文时，支持它的浏览器要少得多。只需将您的 WebGL 1.0 代码编译为 WebGL 2.0，就可以获得大约 7%的性能提升。但是，在您选择这样做之前，您可能想咨询一下[https://caniuse.com/#search=webgl2](https://caniuse.com/#search=webgl2)，看看您所瞄准的浏览器是否支持 WebGL 2.0。\n\n# 最小化 OpenGL 调用的次数\n\n从 WebAssembly 调用 OpenGL 的速度不如从本机编译的应用调用 OpenGL 的速度快。从网络组件对 OpenGL 的调用就是对网络组件模拟的调用。WebGL 被构建为在 web 浏览器内部执行，并执行一些安全检查，以验证我们没有要求 WebGL 做任何恶意的事情。这意味着我们在编写针对 WebAssembly 的 OpenGL 时，必须考虑到额外的开销。在某些情况下，对一个本地应用调用两到三次 OpenGL 会比将这些调用组合成一次 OpenGL 调用更快。但是，如果您将 WebAssembly 中的相同代码压缩为对 OpenGL 的一次调用，它可能会运行得更快。为 WebAssembly 进行优化时，尝试尽可能减少 OpenGL 调用的数量，并使用您的探查器来验证新代码是否更快。\n\n# Emscripten OpenGL flags\n\n几个 Emscripten 链接器标志会对性能产生显著影响。一些标志的创建是为了方便将代码移植到网络组件，但是可能会产生性能问题。其他人可以在合适的条件下提高绩效。\n\n`-s FULL_ES2=1`和`-s FULL_ES3=1`链接器标志模拟整个 OpenGL ES 2.0/3.0 API。正如我前面提到的，默认情况下，WebAssembly 中的 OpenGL ES 2/3 实现只支持与 WebGL 兼容的 OpenGL ES 2/3 的子集。这是因为 WebGL 是在 WebAssembly 中进行渲染的。你绝对需要 OpenGL ES 2/3 的一个默认不可用的特性，这可能是有原因的。如果是这样，您可以使用`-s FULL_ES2=1`或`-s FULL_ES3=1`标志在软件中模拟该功能。在性能方面，这是有代价的，所以如果你决定使用它，就要考虑到这一点。\n\n`-s LEGACY_GL_EMULATION=1`标志用于模拟使用固定函数管道的 OpenGL 旧版本。也不建议您使用此标志，因为这会导致性能下降。对于希望将旧代码移植到 WebAssembly 的人来说，此标志是存在的。\n\n如果您想使用 WebGL 2 获得与其相关的性能提升，请使用`-s USE_WEBGL2=1`链接器标志。如果您有为 WebGL 1.0 编写的代码，但希望获得 WebGL 2.0 的性能提升，您可以尝试编译到 WebGL 2.0，看看您是否使用了任何在 WebGL 2.0 中不向后兼容的代码。如果它没有用这个标志编译，你可以尝试`-s WEBGL2_BACKWARDS_COMPATIBILITY_EMULATION=1`链接器标志，这将允许你编译你的 WebGL 1.0 代码，这样你就可以在 WebGL 2.0 中使用它。\n\n# 摘要\n\n在本章中，我们讨论了可以用来调试和优化我们的 WebAssembly 代码的不同策略。我们讨论了编写 C 宏，当我们从开发阶段进入生产阶段时，它允许我们轻松地删除打印到控制台的调用。我们讨论了源映射，它们是什么，以及它们如何帮助我们从浏览器中调试我们的 WebAssembly 代码。我们讨论了在 Chrome 和 Firefox 中使用调试器来逐步查看 WebAssembly 的源代码。最后，我们讨论了 WebAssembly 中的优化，Emscripten 中有哪些编译器选项，以及如何着手提高我们的 WebGL 性能。\n\n# 这就是结局\n\n恭喜你！你应该可以在 WebAssembly 中开发自己的游戏或应用了。我希望您喜欢学习我们如何使用 WebGL 为网络构建游戏。如果您有任何问题、评论或只想打个招呼，您可以在以下平台找到我:\n\n*   **推特**:[https://twitter.com/battagline](https://twitter.com/battagline)\n*   **领英**:[https://www.linkedin.com/in/battagline/](https://www.linkedin.com/in/battagline/)\n*   **YouTube**:[https://www . YouTube . com/channel/ucajytbkp 0 VM 1 rlt 82 pcxwkq](https://www.youtube.com/channel/UCaJYTBKp0vM1rLT82PcXwKQ)"
  },
  {
    "path": "docs/handson-game-dev-wasm/README.md",
    "content": "# WebAssembly 游戏编程实用指南\n\n> 原书：[THANDS-ON GAME DEVELOPMENT WITH WEBASSEMBLY](https://libgen.rs/book/index.php?md5=32DB6CCB52B094CA3C72416C43DC7A6D)\n> \n> 协议：[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)\n> \n> 阶段：机翻（1）\n> \n> 自豪地采用[谷歌翻译](https://translate.google.cn/)\n> \n> 让开源界提前感受入关。\n\n* [在线阅读](https://ccpp.apachecn.org)\n* [在线阅读（Gitee）](https://apachecn.gitee.io/apachecn-c-cpp-zh/)\n* [ApacheCN 学习资源](http://docs.apachecn.org/)\n\n## 贡献指南\n\n本项目需要校对，欢迎大家提交 Pull Request。\n\n> 请您勇敢地去翻译和改进翻译。虽然我们追求卓越，但我们并不要求您做到十全十美，因此请不要担心因为翻译上犯错——在大部分情况下，我们的服务器已经记录所有的翻译，因此您不必担心会因为您的失误遭到无法挽回的破坏。（改编自维基百科）\n\n## 联系方式\n\n### 负责人\n\n* [飞龙](https://github.com/wizardforcel): 562826179\n\n### 其他\n\n*   在我们的 [apachecn/apachecn-c-cpp-zh](https://github.com/apachecn/apachecn-c-cpp-zh) github 上提 issue.\n*   发邮件到 Email: `apachecn@163.com`.\n*   在我们的 [组织学习交流群](http://www.apachecn.org/organization/348.html) 中联系群主/管理员即可.\n\n## 赞助我们\n\n![](http://data.apachecn.org/img/about/donate.jpg)\n"
  },
  {
    "path": "docs/handson-game-dev-wasm/SUMMARY.md",
    "content": "+   [WebAssembly 游戏编程实用指南](README.md)\n+   [零、前言](00.md)\n+   [一、WebAssembly 和电子脚本简介](01.md)\n+   [二、HTML5 和 WebAssembly](02.md)\n+   [三、WebGL 简介](03.md)\n+   [四、WebAssembly 中使用 SDL 的的精灵动画](04.md)\n+   [五、键盘输入](05.md)\n+   [六、游戏对象和游戏循环](06.md)\n+   [七、碰撞检测](07.md)\n+   [八、基本粒子系统](08.md)\n+   [九、改进的粒子系统](09.md)\n+   [十、人工智能与驾驶行为](10.md)\n+   [十一、设计 2D 相机](11.md)\n+   [十二、声音 FX](12.md)\n+   [十三、游戏物理](13.md)\n+   [十四、用户界面和鼠标输入](14.md)\n+   [十五、着色器和 2D 照明](15.md)\n+   [十六、调试和优化](16.md)\n"
  },
  {
    "path": "docs/lcthw-zh/README.md",
    "content": "# 笨办法学C 中文版\n\n来源：[Learn C The Hard Way](http://c.learncodethehardway.org/book/)\n\n作者：[Zed A. Shaw](https://twitter.com/lzsthw)\n\n译者：[飞龙](https://github.com/wizardforcel)\n\n阶段：精细校对（4）\n\n自豪地采用[谷歌翻译](https://translate.google.cn/)\n\n> **一句 MMP 送给在座的各位程序正义垃圾。**\n\n+ [在线阅读](https://lcthw.apachecn.org)\n+ [在线阅读（Gitee）](https://apachecn.gitee.io/lcthw-zh/)\n+ [PDF格式](https://www.gitbook.com/download/pdf/book/wizardforcel/lcthw)\n+ [EPUB格式](https://www.gitbook.com/download/epub/book/wizardforcel/lcthw)\n+ [MOBI格式](https://www.gitbook.com/download/mobi/book/wizardforcel/lcthw)\n+ [Github](https://github.com/wizardforcel/lcthw-zh)\n\n## 下载\n\n### Docker\n\n```\ndocker pull apachecn0/lcthw-zh\ndocker run -tid -p <port>:80 apachecn0/lcthw-zh\n# 访问 http://localhost:{port} 查看文档\n```\n\n### PYPI\n\n```\npip install lcthw-zh\nlcthw-zh <port>\n# 访问 http://localhost:{port} 查看文档\n```\n\n### NPM\n\n```\nnpm install -g lcthw-zh\nlcthw-zh <port>\n# 访问 http://localhost:{port} 查看文档\n```\n\n## 赞助我\n\n![](img/qr_alipay.png)\n\n## 协议\n\n此版本遵循[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)协议，原版无此约束。\n"
  },
  {
    "path": "docs/lcthw-zh/SUMMARY.md",
    "content": "+ [笨办法学C 中文版](README.md)\n+ [前言](preface.md)\n+ [导言：C的笛卡尔之梦](introduction.md)\n+ [练习0：准备](ex0.md)\n+ [练习1：启用编译器](ex1.md)\n+ [练习2：用Make来代替Python](ex2.md)\n+ [练习3：格式化输出](ex3.md)\n+ [练习4：Valgrind 介绍](ex4.md)\n+ [练习5：一个C程序的结构](ex5.md)\n+ [练习6：变量类型](ex6.md)\n+ [练习7：更多变量和一些算术](ex7.md)\n+ [练习8：大小和数组](ex8.md)\n+ [练习9：数组和字符串](ex9.md)\n+ [练习10：字符串数组和循环](ex10.md)\n+ [练习11：While循环和布尔表达式](ex11.md)\n+ [练习12：If，Else If，Else](ex12.md)\n+ [练习13：Switch语句](ex13.md)\n+ [练习14：编写并使用函数](ex14.md)\n+ [练习15：指针，可怕的指针](ex15.md)\n+ [练习16：结构体和指向它们的指针](ex16.md)\n+ [练习17：堆和栈的内存分配](ex17.md)\n+ [练习18：函数指针](ex18.md)\n+ [练习19：一个简单的对象系统](ex19.md)\n+ [练习20：Zed的强大的调试宏](ex20.md)\n+ [练习21：高级数据类型和控制结构](ex21.md)\n+ [练习22：栈、作用域和全局](ex22.md)\n+ [练习23：认识达夫设备](ex23.md)\n+ [练习24：输入输出和文件](ex24.md)\n+ [练习25：变参函数](ex25.md)\n+ [练习26：编写第一个真正的程序](ex26.md)\n+ [练习27：创造性和防御性编程](ex27.md)\n+ [练习28：Makefile 进阶](ex28.md)\n+ [练习29：库和链接](ex29.md)\n+ [练习30：自动化测试](ex30.md)\n+ [练习31：代码调试](ex31.md)\n+ [练习32：双向链表](ex32.md)\n+ [练习33：链表算法](ex33.md)\n+ [练习34：动态数组](ex34.md)\n+ [练习35：排序和搜索](ex35.md)\n+ [练习36：更安全的字符串](ex36.md)\n+ [练习37：哈希表](ex37.md)\n+ [练习38：哈希算法](ex38.md)\n+ [练习39：字符串算法](ex39.md)\n+ [练习40：二叉搜索树](ex40.md)\n+ [练习41：将 Cachegrind 和 Callgrind 用于性能调优](ex41.md)\n+ [练习42：栈和队列](ex42.md)\n+ [练习43：一个简单的统计引擎](ex43.md)\n+ [练习44：环形缓冲区](ex44.md)\n+ [练习45：一个简单的TCP/IP客户端](ex45.md)\n+ [练习46：三叉搜索树](ex46.md)\n+ [练习47：一个快速的URL路由](ex47.md)\n+ [后记：“解构 K&R C” 已死](postscript.md)\n+ [捐赠名单](donors.md)"
  },
  {
    "path": "docs/lcthw-zh/donors.md",
    "content": "# 捐赠名单\n\n感谢以下童鞋的捐助，你们的慷慨是我继续的动力：\n\n| donor | value |\n| --- | --- |\n| jxdwinter | 6.00 |\n| 贾**@悠云.com | 20.00 |\n| Mr.Moon | 2.00 |"
  },
  {
    "path": "docs/lcthw-zh/ex0.md",
    "content": "# 练习0：准备\n\n> 原文：[Exercise 0: The Setup](http://c.learncodethehardway.org/book/ex0.html)\n\n> 译者：[飞龙](https://github.com/wizardforcel)\n\n在这一章中，你将为C语言编程配置好你的系统。一个好消息是对于所有使用Linux或者Mac的人，你的系统是为C语言编程而设计的。C语言的创造者也对Unix操作系统的创造做出了贡献，并且Linux和OSX都是基于Unix的。事实上，安装工作会非常简单。\n\n对于Windows上的用户，我有一个坏消息：在Windows上学习C非常痛苦。你可以在Windows上编写C代码，这并不是问题。问题是所有的库、函数和工具都和其它的C语言环境有些差异。C来自于Unix，并且和Unix平台配合得比较好。恐怕这是一个你并不能接受的事实。\n\n然而你并不需要为此恐慌。我并不是说要完全避免Windows。然而我说的是，如果你打算以最短的时间来学习C，你需要接触Unix并适应它。这同时也对你有帮助，因为懂得一些Unix的知识，也会让你懂得一些C编程的习惯，以及扩充你的技能。\n\n这也意味着每个人都需要使用命令行。嗯，就是这样。你将会进入命令行并且键入一些命令。不要为此感到害怕，因为我会告诉你要键入什么，以及结果应该是什么样子，所以你实际上会学到很多东西，同时扩充自己的技能。\n\n## Linux\n\n在多数Linux系统上你都需要安装一些包。对于基于Debian的系统，例如Ubuntu你需要使用下列命令来安装一些东西：\n\n```sh\n$ sudo apt-get install build-essential\n```\n\n上面是命令行提示符的一个示例。你需要接触到能输入它的地方，找到你的“终端”程序并且运行它。接着，你会看到一个类似于`$`的Shell提示符，并且你可以在里面键入命令。不要键入`$`，而是它后面的东西。\n\n下面是在基于RPM的Linux系统，例如Fedora中执行相同安装工作的方法：\n\n```sh\n$ su -c \"yum groupinstall development-tools\"\n```\n\n一旦你运行了它，它会正常工作，你应该能够做本书的第一个练习。如果不能请告诉我。\n\n## Mac OSX\n\n在 Mac OSX上，安装工作会更简单。首先，你需要从苹果官网下载最新的`XCode`，或者找到你的安装DVD并从中安装。需要下载的文件很大，要花费很长时间，所以我推荐你从DVD安装。同时，上网搜索“安装xcode”来指导你来安装它。\n\n一旦你安装完XCode，可能需要重启你的电脑。你可以找到你的终端程序并且将它放到快捷启动栏中。在本书中你会经常用到终端，所以最好将它放到顺手的区域。\n\n## Windows\n\n对于Windows用户，你需要在虚拟机中安装并运行一个基本的Ubuntu Linux系统，来做本书的练习，并且避免任何Windows中安装的问题。\n\n> 译者注：如果你的Windows版本是Win10 14316及之后的版本，可以开启Ubuntu子系统来获取Linux环境。\n\n## 文本编辑器\n\n对于程序员来说，文本编辑器的选择有些困难。对于初学者我推荐他们使用[`Gedit`](http://projects.gnome.org/gedit/)，因为它很简单，并且可以用于编写代码。然而，它在特定的国际化环境中并不能正常工作。如果你已经是老司机的话，你可以选用你最喜欢的编辑器。\n\n出于这种考虑，我打算让你尝试一些你所在平台上的标准的用于编程的文本编辑器，并且长期使用其中你最喜欢的一个。如果你已经用了Gedit并且很喜欢他，那么就一致用下去。如果你打算尝试一些不同的编辑器，则赶快尝试并选择一个。\n\n最重要的事情是，不要纠结于寻找最完美的编辑器。文本编辑器几乎都很奇怪，你只需要选择一个并熟悉它，如果你发现喜欢别的编辑器可以切换到它。不要在挑选它和把它变得更好上面花很多时间。\n\n这是亦可以尝试的一些编辑器：\n\n+ Linux和OSX上的[`Gedit`](http://projects.gnome.org/gedit/)。\n+ OSX上的[`TextWrangler`](http://www.barebones.com/products/textwrangler/)。\n+ 可以在终端中运行并几乎在任何地方工作的[`Nano`](http://www.nano-editor.org/)。\n+ [`Emacs`](http://www.gnu.org/software/emacs/)和[`Emacs OSX`](http://emacsformacosx.com/)。需要学习一些东西。\n+ [`Vim`](http://www.vim.org/)和[`Mac Vim`](http://code.google.com/p/macvim/)。\n\n每个人都可能选择一款不同的编辑器，这些只是一部分人所选择的开源编辑器。在找到你最喜欢的那个之前，尝试其中的一些，甚至是一些商业编辑器。\n\n## 警告：不要使用IDE\n\nIDE，或者“集成开发工具”，会使你变笨。如果你想要成为一个好的程序员，它会是最糟糕的工具，因为它隐藏了背后的细节，你的工作是弄清楚背后发生了什么。如果你试着完成一些事情，并且所在平台根据特定的IDE而设计，它们非常有用，但是对于学习C编程（以及许多其它语言），它们没有意义。\n\n> 注\n\n> 如果你玩过吉他，你应该知道TAB是什么。但是对于其它人，让我对其做个解释。在音乐中有一种乐谱叫做“五线谱”。它是通用、非常古老的乐谱，以一种通用的方法来记下其它人应该在乐器上弹奏的音符。如果你弹过钢琴，这种乐谱非常易于使用，因为它几乎就是为钢琴和交响乐发明的。\n\n> 然而吉他是一种奇怪的乐器，它并不能很好地适用这种乐谱。所以吉他手通常使用一种叫做TAB（tablature）的乐谱。它所做的不是告诉你该弹奏哪个音符，而是在当时应该拨哪根弦。你完全可以在不知道所弹奏的单个音符的情况下学习整首乐曲，许多人也都是这么做的，但是如果你想知道你弹的是什么，TAB是毫无意义的。\n\n> 传统的乐谱可能比TAB更难一些，但是会告诉你如何演奏音乐，而不是如果玩吉他。通过传统的乐谱我可以在钢琴上，或者在贝斯上弹奏相同的曲子。我也可以将它放到电脑中，为它设计全部的曲谱。但是通过TAB我只能在吉他上弹奏。\n\n> IDE就像是TAB，你可以用它非常快速地编程，但是你只能够用一种语言在一个平台上编程。这就是公司喜欢将它卖给你的原因。它们知道你比较懒，并且由于它只适用于它们自己的平台，他们就将你锁定在了那个平台上。\n\n> 打破这一循环的办法就是不用IDE学习编程。一个普通的文本编辑器，或者一个程序员使用的文本编辑器，例如Vim或者Emacs，能让你更熟悉代码。这有一点点困难，但是终结果是你将会熟悉任何代码，在任何计算机上，以任何语言，并且懂得背后的原理。\n"
  },
  {
    "path": "docs/lcthw-zh/ex1.md",
    "content": "# 练习1：启用编译器\n\n> 原文：[Exercise 1: Dust Off That Compiler](http://c.learncodethehardway.org/book/ex1.html)\n\n> 译者：[飞龙](https://github.com/wizardforcel)\n\n这是你用C写的第一个简单的程序：\n\n```c\nint main(int argc, char *argv[])\n{\n    puts(\"Hello world.\");\n\n    return 0;\n}\n```\n\n把它写进 `ex1.c` 并输入：\n\n```sh\n$ make ex1\ncc     ex1.c   -o ex1\n```\n\n你的编译器可能会使用一个有些不同的命令，但是最后应该会产生一个名为`ex1`的文件，并且你可以运行它。\n\n## 你会看到什么\n\n现在你可以运行程序并看到输出。\n\n```c\n$ ./ex1\nHello world.\n```\n\n如果没有，则需要返回去修复它。\n\n## 如何使它崩溃\n\n在这本书中我会添加一个小节，关于如何使程序崩溃。我会让你对程序做一些奇怪的事情，以奇怪的方式运行，或者修改代码，以便让你看到崩溃和编译器错误。\n\n对于这个程序，打开所有编译警告重新构建它：\n\n```sh\n$ rm ex1\n$ CFLAGS=\"-Wall\" make ex1\ncc -Wall    ex1.c   -o ex1\nex1.c: In function 'main':\nex1.c:3: warning: implicit declaration of function 'puts'\n$ ./ex1\nHello world.\n$\n```\n\n现在你会得到一个警告，说`puts`函数是隐式声明的。C语言的编译器很智能，它能够理解你想要什么。但是如果可以的话，你应该去除所有编译器警告。把下面一行添加到`ex1.c`文件的最上面，之后重新编译来去除它：\n\n```c\n#include <stdio.h>\n```\n\n现在像刚才一样重新执行make命令，你会看到所有警告都消失了。\n\n## 附加题\n\n+ 在你的文本编辑器中打开`ex1`文件，随机修改或删除一部分，之后运行它看看发生了什么。\n+ 再多打印5行文本或者其它比`\"Hello world.\"`更复杂的东西。\n+ 执行`man 3 puts`来阅读这个函数和其它函数的文档。\n"
  },
  {
    "path": "docs/lcthw-zh/ex10.md",
    "content": "# 练习10：字符串数组和循环\n\n> 原文：[Exercise 10: Arrays Of Strings, Looping](http://c.learncodethehardway.org/book/ex10.html)\n\n> 译者：[飞龙](https://github.com/wizardforcel)\n\n你现在可以创建不同类型的数组，并且也知道了“字符串”和“字节数组”是相同的东西。接下来，我们要更进一步，创建一个包含字符串的数组。我也会介绍第一个循环结构，`for`循环来帮我们打印出这一新的数据结构。\n\n这一章的有趣之处就是你的程序中已经有一个现成的字符串数组，`main`函数参数中的`char *argv[]`。下面这段代码打印出了所有你传入的命令行参数：\n\n```c\n#include <stdio.h>\n\nint main(int argc, char *argv[])\n{\n    int i = 0;\n\n    // go through each string in argv\n    // why am I skipping argv[0]?\n    for(i = 1; i < argc; i++) {\n        printf(\"arg %d: %s\\n\", i, argv[i]);\n    }\n\n    // let's make our own array of strings\n    char *states[] = {\n        \"California\", \"Oregon\",\n        \"Washington\", \"Texas\"\n    };\n    int num_states = 4;\n\n    for(i = 0; i < num_states; i++) {\n        printf(\"state %d: %s\\n\", i, states[i]);\n    }\n\n    return 0;\n}\n```\n\n`for`循环的格式是这样的：\n\n```c\nfor(INITIALIZER; TEST; INCREMENTER) {\n    CODE;\n}\n```\n\n下面是`for`循环的工作机制：\n\n+ `INITIALIZER`中是用来初始化循环的代码，这个例子中它是`i = 0`。\n+ 接下来会检查`TEST`布尔表达式，如果为`false`（0）则跳过`CODE`，不做任何事情。\n+ 执行`CODE`，做它要做的任何事情。\n+ 在`CODE`执行之后会执行`INCREMENTER`部分，通常情况会增加一些东西，比如这个例子是`i++`。\n+ 然后跳到第二步继续执行，直到`TEST`为`false`（0）为止。\n\n例子中的`for`循环使用`argc`和`argv`，遍历了命令行参数，像这样：\n\n+ OS将每个命令行参数作为字符串传入`argv`数组，程序名称`./ex10`在下标为0的位置，剩余的参数紧随其后。\n+ OS将`argc`置为`argv`数组中参数的数量，所以你可以遍历它们而不会越界。要记住如果你提供了一个参数，程序名称是第一个，参数应该在第二个。\n+ 接下来程序使用`i < argc`测试`i`是否使用`argc`，由于最开始`1 < 2`，测试通过。\n+ 之后它会执行代码，输出`i`，并且将`i`用做`argv`的下标。\n+ 然后使用`i++`来运行自增语句，它是`i = i + 1`的便捷形式。\n+ 程序一直重复上面的步骤，直到`i < argc`值为`false`（0），这时退出循环但程序仍然继续执行。\n\n## 你会看到什么\n\n你需要用两种方法运行它来玩转这个程序。第一种方法是向命令行参数传递一些东西来设置`argc`和`argv`。第二种是不传入任何参数，于是你可以看到第一次的`for`循环没有被执行，由于`i < argc`值为`false`。\n\n## 理解字符串数组\n\n你应该可以从这个练习中弄明白，你在C语言中通过混合`char *str = \"blah\"`和`char str[] = {'b','l','a','h'}`语法构建二维数组来构建字符串数组。第十四行的`char *states[] = {...}`语法就是这样的二维混合结构，其中每个字符串都是数组的一个元素，字符串的每个字符又是字符串的一个元素。\n\n\n感到困惑吗？多维的概念是很多人从来都不会去想的，所以你应该在纸上构建这一字符串数组：\n\n+ 在纸的左边为每个字符串画一个小方格，带有它们的下标。\n+ 然后在方格上方写上每个字符的下标。\n+ 接着将字符串中的字符填充到方格内。\n+ 画完之后，在纸上模拟代码的执行过程。\n\n理解它的另一种方法是在你熟悉的语言，比如Python或Ruby中构建相同的结构。\n\n## 如何使它崩溃\n\n+ 使用你喜欢的另一种语言，来写这个程序。传入尽可能多的命令行参数，看看是否能通过传入过多参数使其崩溃。\n+ 将`i`初始化为0看看会发生什么。是否也需要改动`argc`，不改动的话它能正常工作吗？为什么下标从0开始可以正常工作？\n+ 将`num_states`改为错误的值使它变大，来看看会发生什么。\n\n## 附加题\n\n+ 弄清楚在`for`循环的每一部分你都可以放置什么样的代码。\n+ 查询如何使用`','`（逗号）字符来在`for`循环的每一部分中，`';'`（分号）之间分隔多条语句。\n+ 查询`NULL`是什么东西，尝试将它用做`states`的一个元素，看看它会打印出什么。\n+ 看看你是否能在打印之前将`states`的一个元素赋值给`argv`中的元素，再试试相反的操作。\n"
  },
  {
    "path": "docs/lcthw-zh/ex11.md",
    "content": "# 练习11：While循环和布尔表达式\n\n> 原文：[Exercise 11: While-Loop And Boolean Expressions](http://c.learncodethehardway.org/book/ex11.html)\n\n> 译者：[飞龙](https://github.com/wizardforcel)\n\n你已经初步了解C语言如何处理循环，但是你可能不是很清楚布尔表达式`i < argc`是什么。在学习`while`循环之前，让我先来对布尔表达式做一些解释。\n\n在C语言中，实际上没有真正的“布尔”类型，而是用一个整数来代替，0代表`false`，其它值代表`true`。上一个练习中表达式`i < argc`实际上值为1或者0，并不像Python是显式的`Ture`或者`False`。这是C语言更接近计算机工作方式的另一个例子，因为计算机只把值当成数字。\n\n现在用`while`循环来实现和上一个练习相同的函数。这会让你使用两种循环，来观察两种循环是什么关系。\n\n```c\n#include <stdio.h>\n\nint main(int argc, char *argv[])\n{\n    // go through each string in argv\n\n    int i = 0;\n    while(i < argc) {\n        printf(\"arg %d: %s\\n\", i, argv[i]);\n        i++;\n    }\n\n    // let's make our own array of strings\n    char *states[] = {\n        \"California\", \"Oregon\",\n        \"Washington\", \"Texas\"\n    };\n\n    int num_states = 4;\n    i = 0;  // watch for this\n    while(i < num_states) {\n        printf(\"state %d: %s\\n\", i, states[i]);\n        i++;\n    }\n\n    return 0;\n}\n```\n\n你可以看到`while`循环的语法更加简单：\n\n```c\nwhile(TEST) {\n    CODE;\n}\n```\n\n只要`TEST`为`true`（非0），就会一直运行`CODE`中的代码。这意味着如果要达到和`for`循环同样的效果，我们需要自己写初始化语句，以及自己来使`i`增加。\n\n## 你会看到什么\n\n输出基本相同，所以我做了一点修改，使你可以看到它运行的另一种方式。\n\n```sh\n$ make ex11\ncc -Wall -g    ex11.c   -o ex11\n$ ./ex11\narg 0: ./ex11\nstate 0: California\nstate 1: Oregon\nstate 2: Washington\nstate 3: Texas\n$\n$ ./ex11 test it\narg 0: ./ex11\narg 1: test\narg 2: it\nstate 0: California\nstate 1: Oregon\nstate 2: Washington\nstate 3: Texas\n$\n```\n\n## 如何使它崩溃\n\n在你自己的代码中，应优先选择`for`循环而不是`while`循环，因为`for`循环不容易崩溃。下面是几点普遍的原因：\n\n+ 忘记初始化`int i`，使循环发生错误。\n+ 忘记初始化第二个循环的`i`，于是`i`还保留着第一个循环结束时的值。你的第二个循环可能执行也可能不会执行。\n+ 忘记在最后执行`i++`自增，你会得到一个“死循环”，它是在你开始编程的第一个或前两个十年中，最可怕的问题之一。\n\n## 附加题\n\n+ 让这些循环倒序执行，通过使用`i--`从`argc`开始递减直到0。你可能需要做一些算数操作让数组的下标正常工作。\n+ 使用`while`循环将`argv`中的值复制到`states`。\n+ 让这个复制循环不会执行失败，即使`argv`之中有很多元素也不会全部放进`states`。\n+ 研究你是否真正复制了这些字符串。答案可能会让你感到意外和困惑。\n"
  },
  {
    "path": "docs/lcthw-zh/ex12.md",
    "content": "# 练习12：If，Else If，Else\n\n> 原文：[Exercise 12: If, Else-If, Else](http://c.learncodethehardway.org/book/ex12.html)\n\n> 译者：[飞龙](https://github.com/wizardforcel)\n\n`if`语句是每个编程语言中共有的特性，包括C语言。下面是一段代码，使用了`if`语句来确保只传入了一个或两个命令行参数：\n\n```c\n#include <stdio.h>\n\nint main(int argc, char *argv[])\n{\n    int i = 0;\n\n    if(argc == 1) {\n        printf(\"You only have one argument. You suck.\\n\");\n    } else if(argc > 1 && argc < 4) {\n        printf(\"Here's your arguments:\\n\");\n\n        for(i = 0; i < argc; i++) {\n            printf(\"%s \", argv[i]);\n        }\n        printf(\"\\n\");\n    } else {\n        printf(\"You have too many arguments. You suck.\\n\");\n    }\n\n    return 0;\n}\n```\n\n`if`语句的格式为：\n\n```c\nif(TEST) {\n    CODE;\n} else if(TEST) {\n    CODE;\n} else {\n    CODE;\n}\n```\n\n下面是其它语言和C的差异：\n\n+ 像之前提到的那样，`TEST`表达式值为0时为`false`，其它情况为`true`。\n+ 你需要在`TEST`周围写上圆括号，其它语言可能不用。\n+ （只有单条语句时）你并不需要使用花括号`{}`来闭合代码，但是这是一种非常不好的格式，不要这么写。花括号让一个分支的代码的开始和结束变得清晰。如果你不把代码写在里面会出现错误。\n\n除了上面那些，就和其它语言一样了。`else if`或者`else`的部分并不必须出现。\n\n## 你会看到什么\n\n这段代码非常易于运行和尝试：\n\n```sh\n$ make ex12\ncc -Wall -g    ex12.c   -o ex12\n$ ./ex12\nYou only have one argument. You suck.\n$ ./ex12 one\nHere's your arguments:\n./ex12 one\n$ ./ex12 one two\nHere's your arguments:\n./ex12 one two\n$ ./ex12 one two three\nYou have too many arguments. You suck.\n$\n```\n\n## 如何使它崩溃\n\n使这段代码崩溃并不容易，因为它太简单了。尝试把`if`语句的测试表达式搞乱：\n\n+ 移除`else`部分，使它不能处理边界情况。\n+ 将`&&`改为`||`，于是你会把“与”操作变成“或”操作，并且看看会发生什么。\n\n## 附加题\n\n+ 我已经向你简短地介绍了`&&`，它执行“与”操作。上网搜索与之不同的“布尔运算符”。\n+ 为这个程序编写更多的测试用例，看看你会写出什么。\n+ 回到练习10和11，使用`if`语句使循环提前退出。你需要`break`语句来实现它，搜索它的有关资料。\n+ 第一个判断所输出的话真的正确吗？由于你的“第一个参数”不是用户输入的第一个参数，把它改正。\n"
  },
  {
    "path": "docs/lcthw-zh/ex13.md",
    "content": "# 练习13：Switch语句\n\n> 原文：[Exercise 13: Switch Statement](http://c.learncodethehardway.org/book/ex13.html)\n\n> 译者：[飞龙](https://github.com/wizardforcel)\n\n在其它类似Ruby的语言中，`switch`语句可以处理任意类型的表达式。一些语言比如Python没有`switch`语句，因为带有布尔表达式的`if`语句可以做相同的事情。对于这些语言，`switch`语句比`if`语句更加灵活，然而内部的机制是一样的。\n\nC中的`switch`语句与它们不同，实际上是一个“跳转表”。你只能够放置结果为整数的表达式，而不是一些随机的布尔表达式，这些整数用于计算从`swicth`顶部到匹配部分的跳转。下面有一段代码，我要分解它来让你理解“跳转表”的概念：\n\n```c\n#include <stdio.h>\n\nint main(int argc, char *argv[])\n{\n    if(argc != 2) {\n        printf(\"ERROR: You need one argument.\\n\");\n        // this is how you abort a program\n        return 1;\n    }\n\n    int i = 0;\n    for(i = 0; argv[1][i] != '\\0'; i++) {\n        char letter = argv[1][i];\n\n        switch(letter) {\n            case 'a':\n            case 'A':\n                printf(\"%d: 'A'\\n\", i);\n                break;\n\n            case 'e':\n            case 'E':\n                printf(\"%d: 'E'\\n\", i);\n                break;\n\n            case 'i':\n            case 'I':\n                printf(\"%d: 'I'\\n\", i);\n                break;\n\n            case 'o':\n            case 'O':\n                printf(\"%d: 'O'\\n\", i);\n                break;\n\n            case 'u':\n            case 'U':\n                printf(\"%d: 'U'\\n\", i);\n                break;\n\n            case 'y':\n            case 'Y':\n                if(i > 2) {\n                    // it's only sometimes Y\n                    printf(\"%d: 'Y'\\n\", i);\n                }\n                break;\n\n            default:\n                printf(\"%d: %c is not a vowel\\n\", i, letter);\n        }\n    }\n\n    return 0;\n}\n```\n\n在这个程序中我们接受了单一的命令行参数，并且用一种极其复杂的方式打印出所有原因，来向你演示`switch`语句。下面是`swicth`语句的工作原理：\n\n+ 编译器会标记`swicth`语句的顶端，我们先把它记为地址Y。\n+ 接着对`switch`中的表达式求值，产生一个数字。在上面的例子中，数字为`argv[1]`中字母的原始的ASCLL码。\n+ 编译器也会把每个类似`case 'A'`的`case`代码块翻译成这个程序中距离语句顶端的地址，所以`case 'A'`就在`Y + 'A'`处。\n+ 接着计算是否`Y+letter`位于`switch`语句中，如果距离太远则会将其调整为`Y+Default`。\n+ 一旦计算出了地址，程序就会“跳”到代码的那个位置并继续执行。这就是一些`case`代码块中有`break`而另外一些没有的原因。\n+ 如果输出了`'a'`，那它就会跳到`case 'a'`，它里面没有`break`语句，所以它会贯穿执行底下带有代码和`break`的`case 'A'`。\n+ 最后它执行这段代码，执行`break`完全跳出`switch`语句块。\n\n> 译者注：更常见的情况是，gcc会在空白处单独构建一张跳转表，各个偏移处存放对应的`case`语句的地址。Y不是`switch`语句的起始地址，而是这张表的起始地址。程序会跳转到`*(Y + 'A')`而不是`Y + 'A'`处。\n\n这是对`swicth`语句工作原理的一个深究，然而实际操作中你只需要记住下面几条简单的原则：\n\n+ 总是要包含一个`default:`分支，可以让你接住被忽略的输入。\n+ 不要允许“贯穿”执行，除非你真的想这么做，这种情况下最好添加一个`//fallthrough`的注释。\n+ 一定要先编写`case`和`break`，再编写其中的代码。\n+ 如果能够简化的话，用`if`语句代替。\n\n## 你会看到什么\n\n下面是我运行它的一个例子，也演示了传入命令行参数的不同方法：\n\n```sh\n$ make ex13\ncc -Wall -g    ex13.c   -o ex13\n$ ./ex13\nERROR: You need one argument.\n$\n$ ./ex13 Zed\n0: Z is not a vowel\n1: 'E'\n2: d is not a vowel\n$\n$ ./ex13 Zed Shaw\nERROR: You need one argument.\n$\n$ ./ex13 \"Zed Shaw\"\n0: Z is not a vowel\n1: 'E'\n2: d is not a vowel\n3:   is not a vowel\n4: S is not a vowel\n5: h is not a vowel\n6: 'A'\n7: w is not a vowel\n$\n```\n\n记住在代码的开始有个`if`语句，当没有提供足够的参数时使用`return 1`返回。返回非0是你提示操作系统程序出错的办法。任何大于0的值都可以在脚本中测试，其它程序会由此知道发生了什么。\n\n## 如何使它崩溃\n\n破坏一个`switch`语句块太容易了。下面是一些方法，你可以挑一个来用：\n\n+ 忘记写`break`，程序就会运行两个或多个代码块，这些都是你不想运行的。\n+ 忘记写`default`，程序会在静默中忽略你所忘记的值。\n+ 无意中将一些带有预料之外的值的变量放入`switch`中，比如带有奇怪的值的`int`。\n+ 在`switch`中是否未初始化的值。\n\n你也可以使用一些别的方法使这个程序崩溃。试着看你能不能自己做到它。\n\n## 附加题\n\n+ 编写另一个程序，在字母上做算术运算将它们转换为小写，并且在`switch`中移除所有额外的大写字母。\n+ 使用`','`（逗号）在`for`循环中初始化`letter`。\n+ 使用另一个`for`循环来让它处理你传入的所有命令行参数。\n+ 将这个`switch`语句转为`if`语句，你更喜欢哪个呢？\n+ 在“Y”的例子中，我在`if`代码块外面写了个`break`。这样会产生什么效果？如果把它移进`if`代码块，会发生什么？自己试着解答它，并证明你是正确的。\n"
  },
  {
    "path": "docs/lcthw-zh/ex14.md",
    "content": "# 练习14：编写并使用函数\n\n> 原文：[Exercise 14: Writing And Using Functions](http://c.learncodethehardway.org/book/ex14.html)\n\n> 译者：[飞龙](https://github.com/wizardforcel)\n\n\n到现在为止，你只使用了作为`stdio.h`头文件一部分的函数。在这个练习中你将要编写并使用自己的函数。\n\n```c\n#include <stdio.h>\n#include <ctype.h>\n\n// forward declarations\nint can_print_it(char ch);\nvoid print_letters(char arg[]);\n\nvoid print_arguments(int argc, char *argv[])\n{\n    int i = 0;\n\n    for(i = 0; i < argc; i++) {\n        print_letters(argv[i]);\n    }\n}\n\nvoid print_letters(char arg[])\n{\n    int i = 0;\n\n    for(i = 0; arg[i] != '\\0'; i++) {\n        char ch = arg[i];\n\n        if(can_print_it(ch)) {\n            printf(\"'%c' == %d \", ch, ch);\n        }\n    }\n\n    printf(\"\\n\");\n}\n\nint can_print_it(char ch)\n{\n    return isalpha(ch) || isblank(ch);\n}\n\n\nint main(int argc, char *argv[])\n{\n    print_arguments(argc, argv);\n    return 0;\n}\n```\n\n在这个例子中你创建了函数来打印任何属于“字母”和“空白”的字符。下面是一个分解：\n\nex14.c:2\n\n包含了新的头文件，所以你可以访问`isalpha`和`isblank`。\n\nex14.c:5-6\n\n告诉C语言你稍后会在你的程序中使用一些函数，它们实际上并没有被定义。这叫做“前向声明”，它解决了要想使用函数先要定义的鸡和蛋的问题。\n\nex14.c:8-15\n\n定义`print_arguments`，它知道如何打印通常由`main`函数获得的相同字符串数组。\n\nex14.c:17-30\n\n定义了`can_print_it`，它只是简单地将`isalpha(ch) || isblank(ch)`的真值（0或1）返回给它的调用者`print_letters`。\n\nex14.c:38-42\n\n最后`main`函数简单地调用`print_arguments`，来启动整个函数链。\n\n我不应该描述每个函数里都有什么，因为这些都是你之前遇到过的东西。你应该看到的是，我只是像你定义`main`函数一样来定义其它函数。唯一的不同就是如果你打算使用当前文件中没有碰到过的函数，你应该事先告诉C。这就是代码顶部的“前向声明”的作用。\n\n## 你会看到什么\n\n向这个程序传入不同的命令行参数来玩转它，这样会遍历你函数中的所有路径。这里演示了我和它的交互：\n\n```sh\n$ make ex14\ncc -Wall -g    ex14.c   -o ex14\n\n$ ./ex14\n'e' == 101 'x' == 120\n\n$ ./ex14 hi this is cool\n'e' == 101 'x' == 120\n'h' == 104 'i' == 105\n't' == 116 'h' == 104 'i' == 105 's' == 115\n'i' == 105 's' == 115\n'c' == 99 'o' == 111 'o' == 111 'l' == 108\n\n$ ./ex14 \"I go 3 spaces\"\n'e' == 101 'x' == 120\n'I' == 73 ' ' == 32 'g' == 103 'o' == 111 ' ' == 32 ' ' == 32 's' == 115 'p' == 112 'a' == 97 'c' == 99 'e' == 101 's' == 115\n$\n```\n\n`isalpha`和`isblank`做了检查提供的字符是否是字母或者空白字符的所有工作。当我最后一次运行时，它打印出除了`'3'`之外的任何东西，因为它是一个数字。\n\n## 如何使它崩溃\n\n下面是使它崩溃的两种不同的方法：\n\n+ 通过移除前向声明来把编译器搞晕。它会报告`can_print_it` 和 `print_letters`的错误。\n+ 当你在`main`中调用`print_arguments`时，试着使`argc`加1，于是它会越过`argv`数组的最后一个元素。\n\n## 附加题\n\n+ 重新编写这些函数，使它们的数量减少。比如，你真的需要`can_print_it`吗？\n+ 使用`strlen`函数，让`print_arguments`知道每个字符串参数都有多长，之后将长度传入`print_letters`。然后重写`print_letters`，让它只处理固定的长度，不按照`'\\0'`终止符。你需要`#include <string.h>`来实现它。\n+ 使用`man`来查询`isalpha`和`isblank`的信息。使用其它相似的函数来只打印出数字或者其它字符。\n+ 上网浏览不同的人喜欢什么样的函数格式。永远不要使用“K&R”语法，因为它过时了，而且容易使人混乱，但是当你碰到一些人使用这种格式时，要理解代码做了什么。\n"
  },
  {
    "path": "docs/lcthw-zh/ex15.md",
    "content": "# 练习15：指针，可怕的指针\n\n> 原文：[Exercise 15: Pointers Dreaded Pointers](http://c.learncodethehardway.org/book/ex15.html)\n\n> 译者：[飞龙](https://github.com/wizardforcel)\n\n指针是C中的一个著名的谜之特性，我会试着通过教授你一些用于处理它们的词汇，使之去神秘化。指针实际上并不复杂，只不过它们经常以一些奇怪的方式被滥用，这样使它们变得难以使用。如果你避免这些愚蠢的方法来使用指针，你会发现它们难以置信的简单。\n\n要想以一种我们可以谈论的方式来讲解指针，我会编写一个无意义的程序，它以三种方式打印了一组人的年龄：\n\n```c\n#include <stdio.h>\n\nint main(int argc, char *argv[])\n{\n    // create two arrays we care about\n    int ages[] = {23, 43, 12, 89, 2};\n    char *names[] = {\n        \"Alan\", \"Frank\",\n        \"Mary\", \"John\", \"Lisa\"\n    };\n\n    // safely get the size of ages\n    int count = sizeof(ages) / sizeof(int);\n    int i = 0;\n\n    // first way using indexing\n    for(i = 0; i < count; i++) {\n        printf(\"%s has %d years alive.\\n\",\n                names[i], ages[i]);\n    }\n\n    printf(\"---\\n\");\n\n    // setup the pointers to the start of the arrays\n    int *cur_age = ages;\n    char **cur_name = names;\n\n    // second way using pointers\n    for(i = 0; i < count; i++) {\n        printf(\"%s is %d years old.\\n\",\n                *(cur_name+i), *(cur_age+i));\n    }\n\n    printf(\"---\\n\");\n\n    // third way, pointers are just arrays\n    for(i = 0; i < count; i++) {\n        printf(\"%s is %d years old again.\\n\",\n                cur_name[i], cur_age[i]);\n    }\n\n    printf(\"---\\n\");\n\n    // fourth way with pointers in a stupid complex way\n    for(cur_name = names, cur_age = ages;\n            (cur_age - ages) < count;\n            cur_name++, cur_age++)\n    {\n        printf(\"%s lived %d years so far.\\n\",\n                *cur_name, *cur_age);\n    }\n\n    return 0;\n}\n```\n\n在解释指针如何工作之前，让我们逐行分解这个程序，这样你可以对发生了什么有所了解。当你浏览这个详细说明时，试着自己在纸上回答问题，之后看看你猜测的结果符合我对指针的描述。\n\nex15.c:6-10\n\n创建了两个数组，`ages`储存了一些`int`数据，`names`储存了一个字符串数组。\n\nex15.c:12-13\n\n为之后的`for`循环创建了一些变量。\n\nex15.c:16-19\n\n你知道这只是遍历了两个数组，并且打印出每个人的年龄。它使用了`i`来对数组索引。\n\nex15.c:24\n\n创建了一个指向`ages`的指针。注意`int *`创建“指向整数的指针”的指针类型的用法。它很像`char *`，意义是“指向字符的指针”，而且字符串是字符的数组。是不是很相似呢？\n\nex15.c:25\n\n创建了指向`names`的指针。`char *`已经是“指向`char`的指针”了，所以它只是个字符串。你需要两个层级，因为`names`是二维的，也就是说你需要`char **`作为“指向‘指向字符的指针’的指针”。把它学会，并且自己解释它。\n\nex15.c:28-31\n\n遍历`ages`和`names`，但是使用“指针加偏移`i`”。`*(cur_name+i)`和`name[i]`是一样的，你应该把它读作“‘`cur_name`指针加`i`’的值”。\n\nex15.c:35-39\n\n这里展示了访问数组元素的语法和指针是相同的。\n\nex15.c:44-50\n\n另一个十分愚蠢的循环和其它两个循环做着相同的事情，但是它用了各种指针算术运算来代替：\n\nex15.c:44\n\n通过将`cur_name`和`cur_age`置为`names`和`age`数组的起始位置来初始化`for`循环。\n\nex15.c:45\n\n`for`循环的测试部分比较`cur_age`指针和`ages`起始位置的距离，为什么可以这样写呢？\n\nex15.c:46\n\n`for`循环的增加部分增加了`cur_name`和`cur_age`的值，这样它们可以只想`names`和`ages`的下一个元素。\n\nex15.c:48-49\n\n`cur_name`和`cur_age`的值现在指向了相应数组中的一个元素，我们我可以通过`*cur_name`和`*cur_age`来打印它们，这里的意思是“`cur_name`和`cur_age`指向的值”。\n\n这个看似简单的程序却包含了大量的信息，其目的是在我向你讲解之前尝试让你自己弄清楚指针。直到你写下你认为指针做了什么之前，不要往下阅读。\n\n## 你会看到什么\n\n在你运行这个程序之后，尝试根据打印出的每一行追溯到代码中产生它们的那一行。在必要情况下，修改`printf`调用来确认你得到了正确的行号：\n\n```sh\n$ make ex15\ncc -Wall -g    ex15.c   -o ex15\n$ ./ex15\nAlan has 23 years alive.\nFrank has 43 years alive.\nMary has 12 years alive.\nJohn has 89 years alive.\nLisa has 2 years alive.\n---\nAlan is 23 years old.\nFrank is 43 years old.\nMary is 12 years old.\nJohn is 89 years old.\nLisa is 2 years old.\n---\nAlan is 23 years old again.\nFrank is 43 years old again.\nMary is 12 years old again.\nJohn is 89 years old again.\nLisa is 2 years old again.\n---\nAlan lived 23 years so far.\nFrank lived 43 years so far.\nMary lived 12 years so far.\nJohn lived 89 years so far.\nLisa lived 2 years so far.\n$\n```\n\n## 解释指针\n\n当你写下一些类似`ages[i]`的东西时，你实际上在用`i`中的数字来索引`ages`。如果`i`的值为0，那么就等同于写下`ages[0]`。我们把`i`叫做下标，因为它是`ages`中的一个位置。它也能称为地址，这是“我想要`ages`位于地址`i`处的整数”中的说法。\n\n如果`i`是个下标，那么`ages`又是什么？对C来说`ages`是在计算机中那些整数的起始位置。当然它也是个地址，C编译器会把任何你键入`ages`的地方替换为数组中第一个整数的地址。另一个理解它的办法就是把`ages`当作“数组内部第一个整数的地址”，但是它是整个计算机中的地址，而不是像`i`一样的`ages`中的地址。`ages`数组的名字在计算机中实际上是个地址。\n\n这就产生了一种特定的实现：C把你的计算机看成一个庞大的字节数组。显然这样不会有什么用处，于是C就在它的基础上构建出类型和大小的概念。你已经在前面的练习中看到了它是如何工作的，但现在你可以开始了解C对你的数组做了下面一些事情：\n\n+ 在你的计算机中开辟一块内存。\n+ 将`ages`这个名字“指向”它的起始位置。\n+ 通过选取`ages`作为基址，并且获取位置为`i`的元素，来对内存块进行索引。\n+ 将`ages+i`处的元素转换成大小正确的有效的`int`，这样就返回了你想要的结果：下标`i`处的`int`。\n\n如果你可以选取`ages`作为基址，之后加上比如`i`的另一个地址，你是否就能随时构造出指向这一地址的指针呢？是的，这种东西就叫做指针。这也是`cur_age`和`cur_name`所做的事情，它们是指向计算机中这一位置的变量，`ages`和`names`就处于这一位置。之后，示例程序移动它们，或者做了一些算数运算，来从内存中获取值。在其中一个实例中，只是简单地将`cur_age`加上`i`，这样等同于`array[i]`。在最后一个`for`循环中，这两个指针在没有`i`辅助的情况下自己移动，被当做数组基址和整数偏移合并到一起的组合。\n\n指针仅仅是指向计算机中的某个地址，并带有类型限定符，所以你可以通过它得到正确大小的数据。它类似于将`ages`和`i`组合为一个数据类型的东西。C了解指针指向什么地方，所指向的数据类型，这些类型的大小，以及如何为你获取数据。你可以像`i`一样增加它们，减少它们，对他们做加减运算。然而它们也像是`ages`，你可以通过它获取值，放入新的值，或执行全部的数组操作。\n\n指针的用途就是让你手动对内存块进行索引，一些情况下数组并不能做到。绝大多数情况中，你可能打算使用数组，但是一些处理原始内存块的情况，是指针的用武之地。指针向你提供了原始的、直接的内存块访问途径，让你能够处理它们。\n\n在这一阶段需要掌握的最后一件事，就是你可以对数组和指针操作混用它们绝大多数的语法。你可以对一个指针使用数组的语法来访问指向的东西，也可以对数组的名字做指针的算数运算。\n\n## 实用的指针用法\n\n你可以用指针做下面四个最基本的操作：\n\n+ 向OS申请一块内存，并且用指针处理它。这包括字符串，和一些你从来没见过的东西，比如结构体。\n+ 通过指针向函数传递大块的内存（比如很大的结构体），这样不必把全部数据都传递进去。\n+ 获取函数的地址用于动态调用。\n+ 对一块内存做复杂的搜索，比如，转换网络套接字中的字节，或者解析文件。\n\n对于你看到的其它所有情况，实际上应当使用数组。在早期，由于编译器不擅长优化数组，人们使用指针来加速它们的程序。然而，现在访问数组和指针的语法都会翻译成相同的机器码，并且表现一致。由此，你应该每次尽可能使用数组，并且按需将指针用作提升性能的手段。\n\n## 指针词库\n\n现在我打算向你提供一个词库，用于读写指针。当你遇到复杂的指针语句时，试着参考它并且逐字拆分语句（或者不要使用这个语句，因为有可能并不好）：\n\n`type *ptr`\n\n`type`类型的指针，名为`ptr`。\n\n`*ptr`\n\n`ptr`所指向位置的值。\n\n`*(ptr + i)`\n\n（`ptr`所指向位置加上`i`）的值。\n\n> 译者注：以字节为单位的话，应该是`ptr`所指向的位置再加上`sizeof(type) * i`。\n\n`&thing`\n\n`thing`的地址。\n\n`type *ptr = &thing`\n\n名为`ptr`，`type`类型的指针，值设置为`thing`的地址。\n\n`ptr++`\n\n自增`ptr`指向的位置。\n\n我们将会使用这份简单的词库来拆解这本书中所有的指针用例。\n\n## 指针并不是数组\n\n无论怎么样，你都不应该把指针和数组混为一谈。它们并不是相同的东西，即使C让你以一些相同的方法来使用它们。例如，如果你访问上面代码中的`sizeof(cur_age)`，你会得到指针的大小，而不是它指向数组的大小。如果你想得到整个数组的大小，你应该使用数组的名称`age`，就像第12行那样。\n\n> 译者注，除了`sizeof`、`&`操作和声明之外，数组名称都会被编译器推导为指向其首个元素的指针。对于这些情况，不要用“是”这个词，而是要用“推导”。\n\n## 如何使它崩溃\n\n你可以通过将指针指向错误的位置来使程序崩溃：\n\n+ 试着将`cur_age`指向`names`。可以需要C风格转换来强制执行，试着查阅相关资料把它弄明白。\n+ 在最后的`for`循环中，用一些古怪的方式使计算发生错误。\n+ 试着重写循环，让它们从数组的最后一个元素开始遍历到首个元素。这比看上去要困难。\n\n## 附加题\n\n+ 使用访问指针的方式重写所有使用数组的地方。\n+ 使用访问数组的方式重写所有使用指针的地方。\n+ 在其它程序中使用指针来代替数组访问。\n+ 使用指针来处理命令行参数，就像处理`names`那样。\n+ 将获取值和获取地址组合到一起。\n+ 在程序末尾添加一个`for`循环，打印出这些指针所指向的地址。你需要在`printf`中使用`%p`。\n+ 对于每一种打印数组的方法，使用函数来重写程序。试着向函数传递指针来处理数据。记住你可以声明接受指针的函数，但是可以像数组那样用它。\n+ 将`for`循环改为`while`循环，并且观察对于每种指针用法哪种循环更方便。\n"
  },
  {
    "path": "docs/lcthw-zh/ex16.md",
    "content": "# 练习16：结构体和指向它们的指针\n\n> 原文：[Exercise 16: Structs And Pointers To Them](http://c.learncodethehardway.org/book/ex16.html)\n\n> 译者：[飞龙](https://github.com/wizardforcel)\n\n在这个练习中你将会学到如何创建`struct`，将一个指针指向它们，以及使用它们来理解内存的内部结构。我也会借助上一节课中的指针知识，并且让你使用`malloc`从原始内存中构造这些结构体。\n\n像往常一样，下面是我们将要讨论的程序，你应该把它打下来并且使它正常工作：\n\n```c\n#include <stdio.h>\n#include <assert.h>\n#include <stdlib.h>\n#include <string.h>\n\nstruct Person {\n    char *name;\n    int age;\n    int height;\n    int weight;\n};\n\nstruct Person *Person_create(char *name, int age, int height, int weight)\n{\n    struct Person *who = malloc(sizeof(struct Person));\n    assert(who != NULL);\n\n    who->name = strdup(name);\n    who->age = age;\n    who->height = height;\n    who->weight = weight;\n\n    return who;\n}\n\nvoid Person_destroy(struct Person *who)\n{\n    assert(who != NULL);\n\n    free(who->name);\n    free(who);\n}\n\nvoid Person_print(struct Person *who)\n{\n    printf(\"Name: %s\\n\", who->name);\n    printf(\"\\tAge: %d\\n\", who->age);\n    printf(\"\\tHeight: %d\\n\", who->height);\n    printf(\"\\tWeight: %d\\n\", who->weight);\n}\n\nint main(int argc, char *argv[])\n{\n    // make two people structures\n    struct Person *joe = Person_create(\n            \"Joe Alex\", 32, 64, 140);\n\n    struct Person *frank = Person_create(\n            \"Frank Blank\", 20, 72, 180);\n\n    // print them out and where they are in memory\n    printf(\"Joe is at memory location %p:\\n\", joe);\n    Person_print(joe);\n\n    printf(\"Frank is at memory location %p:\\n\", frank);\n    Person_print(frank);\n\n    // make everyone age 20 years and print them again\n    joe->age += 20;\n    joe->height -= 2;\n    joe->weight += 40;\n    Person_print(joe);\n\n    frank->age += 20;\n    frank->weight += 20;\n    Person_print(frank);\n\n    // destroy them both so we clean up\n    Person_destroy(joe);\n    Person_destroy(frank);\n\n    return 0;\n}\n```\n\n我打算使用一种和之前不一样的方法来描述这段程序。我并不会对程序做逐行的拆分，而是由你自己写出来。我会基于程序所包含的部分来给你提示，你的任务就是写出每行是干什么的。\n\n包含（`include`）\n\n我包含了一些新的头文件，来访问一些新的函数。每个头文件都提供了什么东西？\n\n`struct Person`\n\n这就是我创建结构体的地方了，结构体含有四个成员来描述一个人。最后我们得到了一个复合类型，让我们通过一个名字来整体引用这些成员，或它们的每一个。这就像数据库表中的一行或者OOP语言中的一个类那样。\n\n`Pearson_create` 函数\n\n我需要一个方法来创建这些结构体，于是我定义了一个函数来实现。下面是这个函数做的几件重要的事情：\n\n+ 使用用于内存分配的`malloc`来向OS申请一块原始的内存。\n+ 向`malloc`传递`sizeof(struct Person)`参数，它计算结构体的大小，包含其中的所有成员。\n+ 使用了`assert`来确保从`malloc`得到一块有效的内存。有一个特殊的常量叫做`NULL`，表示“未设置或无效的指针”。这个`assert`大致检查了`malloc`是否会返回`NULL`。\n+ 使用`x->y`语法来初始化`struct Person`的每个成员，它指明了所初始化的成员。\n+ 使用`strdup`来复制字符串`name`，是为了确保结构体真正拥有它。`strdup`的行为实际上类似`malloc`但是它同时会将原来的字符串复制到新创建的内存。\n\n> 译者注：`x->y`是`(*x).y`的简写。\n\n`Person_destroy` 函数\n\n如果定义了创建函数，那么一定需要一个销毁函数，它会销毁`Person`结构体。我再一次使用了`assert`来确保不会得到错误的输入。接着我使用了`free`函数来交还通过`malloc`和`strdup`得到的内存。如果你不这么做则会出现“内存泄露”。\n\n> 译者注：不想显式释放内存又能避免内存泄露的办法是引入`libGC`库。你需要把所有的`malloc`换成`GC_malloc`，然后把所有的`free`删掉。\n\n`Person_print` 函数\n\n接下来我需要一个方法来打印出人们的信息，这就是这个函数所做的事情。它用了相同的`x->y`语法从结构体中获取成员来打印。\n\n`main` 函数\n\n我在`main`函数中使用了所有前面的函数和`struct Person`来执行下面的事情：\n\n+ 创建了两个人：`joe`和`frank`。\n+ 把它们打印出来，注意我用了`%p`占位符，所以你可以看到程序实际上把结构体放到了哪里。\n+ 把它们的年龄增加20岁，同时增加它们的体重。\n+ 之后打印出每个人。\n+ 最后销毁结构体，以正确的方式清理它们。\n\n请仔细阅读上面的描述，然后做下面的事情：\n\n+ 查询每个你不了解的函数或头文件。记住你通常可以使用`man 2 function`或者`man 3 function`来让它告诉你。你也可以上网搜索资料。\n+ 在每一行上方编写注释，写下这一行代码做了什么。\n+ 跟踪每一个函数调用和变量，你会知道它在程序中是在哪里出现的。\n+ 同时也查询你不清楚的任何符号。\n\n## 你会看到什么\n\n在你使用描述性注释扩展程序之后，要确保它实际上能够运行，并且产生下面的输出：\n\n```sh\n$ make ex16\ncc -Wall -g    ex16.c   -o ex16\n\n$ ./ex16\nJoe is at memory location 0xeba010:\nName: Joe Alex\n    Age: 32\n    Height: 64\n    Weight: 140\nFrank is at memory location 0xeba050:\nName: Frank Blank\n   Age: 20\n   Height: 72\n   Weight: 180\nName: Joe Alex\n   Age: 52\n   Height: 62\n   Weight: 180\nName: Frank Blank\n   Age: 40\n   Height: 72\n   Weight: 200\n```\n\n## 解释结构体\n\n如果你完成了我要求的任务，你应该理解了结构体。不过让我来做一个明确的解释，确保你真正理解了它。\n\nC中的结构体是其它数据类型（变量）的一个集合，它们储存在一块内存中，然而你可以通过独立的名字来访问每个变量。它们就类似于数据库表中的一行记录，或者面向对象语言中的一个非常简单的类。让我们以这种方式来理解它：\n\n\n+ 在上面的代码中，你创建了一个结构体，它们的成员用于描述一个人：名称、年龄、体重、身高。\n+ 每个成员都有一个类型，比如是`int`。\n+ C会将它们打包到一起，于是它们可以用单个的结构体来存放。\n+ `struct Person`是一个复合类型，这意味着你可以在同种表达式中将其引用为其它的数据类型。\n+ 你可以将这一紧密的组合传递给其它函数，就像`Person_print`那样。\n+ 如果结构体是指针的形式，接着你可以使用`x->y`通过它们的名字来访问结构体中独立的部分。\n+ 还有一种创建结构体的方法，不需要指针，通过`x.y`来访问。你将会在附加题里面见到它。\n\n如果你不使用结构体，则需要自己计算出大小、打包以及定位出指定内容的内存片位置。实际上，在大多数早期（甚至现在的一些）的汇编代码中，这就是唯一的方式。在C中你就可以让C来处理这些复合数据类型的内存构造，并且专注于和它们交互。\n\n## 如何使它崩溃\n\n使这个程序崩溃的办法涉及到使用指针和`malloc`系统的方法：\n\n+ 试着传递`NULL`给`Person_destroy`来看看会发生什么。如果它没有崩溃，你必须移除Makefile的`CFLAGS`中的`-g`选项。\n+ 在结尾处忘记调用`Person_destroy`，在`Valgrind`下运行程序，你会看到它报告出你忘记释放内存。弄清楚你应该向`valgrind`传递什么参数来让它向你报告内存如何泄露。\n+ 忘记在`Person_destroy`中释放`who->name`，并且对比两次的输出。同时，使用正确的选项来让`Valgrind`告诉你哪里错了。\n+ 这一次，向`Person_print`传递`NULL`，并且观察`Valgrind`会输出什么。\n+ 你应该明白了`NULL`是个使程序崩溃的快速方法。\n\n## 附加题\n\n在这个练习的附加题中我想让你尝试一些有难度的东西：将这个程序改为不用指针和`malloc`的版本。这可能很困难，所以你需要研究下面这些东西：\n\n+ 如何在栈上创建结构体，就像你创建任何其它变量那样。\n+ 如何使用`x.y`而不是`x->y`来初始化结构体。\n+ 如何不使用指针来将结构体传给其它函数。\n"
  },
  {
    "path": "docs/lcthw-zh/ex17.md",
    "content": "# 练习17：堆和栈的内存分配\n\n> 原文：[Exercise 17: Heap And Stack Memory Allocation](http://c.learncodethehardway.org/book/ex17.html)\n\n> 译者：[飞龙](https://github.com/wizardforcel)\n\n在这个练习中，你会在难度上做一个大的跳跃，并且创建出用于管理数据库的完整的小型系统。这个数据库并不实用也存储不了太多东西，然而它展示了大多数到目前为止你学到的东西。它也以更加正规的方法介绍了内存分配，以及带领你熟悉文件处理。我们使用了一些文件IO函数，但是我并不想过多解释它们，你可以先试着自己理解。\n\n像通常一样，输入下面整个程序，并且使之正常工作，之后我们会进行讨论：\n\n```c\n#include <stdio.h>\n#include <assert.h>\n#include <stdlib.h>\n#include <errno.h>\n#include <string.h>\n\n#define MAX_DATA 512\n#define MAX_ROWS 100\n\nstruct Address {\n    int id;\n    int set;\n    char name[MAX_DATA];\n    char email[MAX_DATA];\n};\n\nstruct Database {\n    struct Address rows[MAX_ROWS];\n};\n\nstruct Connection {\n    FILE *file;\n    struct Database *db;\n};\n\nvoid die(const char *message)\n{\n    if(errno) {\n        perror(message);\n    } else {\n        printf(\"ERROR: %s\\n\", message);\n    }\n\n    exit(1);\n}\n\nvoid Address_print(struct Address *addr)\n{\n    printf(\"%d %s %s\\n\",\n            addr->id, addr->name, addr->email);\n}\n\nvoid Database_load(struct Connection *conn)\n{\n    int rc = fread(conn->db, sizeof(struct Database), 1, conn->file);\n    if(rc != 1) die(\"Failed to load database.\");\n}\n\nstruct Connection *Database_open(const char *filename, char mode)\n{\n    struct Connection *conn = malloc(sizeof(struct Connection));\n    if(!conn) die(\"Memory error\");\n\n    conn->db = malloc(sizeof(struct Database));\n    if(!conn->db) die(\"Memory error\");\n\n    if(mode == 'c') {\n        conn->file = fopen(filename, \"w\");\n    } else {\n        conn->file = fopen(filename, \"r+\");\n\n        if(conn->file) {\n            Database_load(conn);\n        }\n    }\n\n    if(!conn->file) die(\"Failed to open the file\");\n\n    return conn;\n}\n\nvoid Database_close(struct Connection *conn)\n{\n    if(conn) {\n        if(conn->file) fclose(conn->file);\n        if(conn->db) free(conn->db);\n        free(conn);\n    }\n}\n\nvoid Database_write(struct Connection *conn)\n{\n    rewind(conn->file);\n\n    int rc = fwrite(conn->db, sizeof(struct Database), 1, conn->file);\n    if(rc != 1) die(\"Failed to write database.\");\n\n    rc = fflush(conn->file);\n    if(rc == -1) die(\"Cannot flush database.\");\n}\n\nvoid Database_create(struct Connection *conn)\n{\n    int i = 0;\n\n    for(i = 0; i < MAX_ROWS; i++) {\n        // make a prototype to initialize it\n        struct Address addr = {.id = i, .set = 0};\n        // then just assign it\n        conn->db->rows[i] = addr;\n    }\n}\n\nvoid Database_set(struct Connection *conn, int id, const char *name, const char *email)\n{\n    struct Address *addr = &conn->db->rows[id];\n    if(addr->set) die(\"Already set, delete it first\");\n\n    addr->set = 1;\n    // WARNING: bug, read the \"How To Break It\" and fix this\n    char *res = strncpy(addr->name, name, MAX_DATA);\n    // demonstrate the strncpy bug\n    if(!res) die(\"Name copy failed\");\n\n    res = strncpy(addr->email, email, MAX_DATA);\n    if(!res) die(\"Email copy failed\");\n}\n\nvoid Database_get(struct Connection *conn, int id)\n{\n    struct Address *addr = &conn->db->rows[id];\n\n    if(addr->set) {\n        Address_print(addr);\n    } else {\n        die(\"ID is not set\");\n    }\n}\n\nvoid Database_delete(struct Connection *conn, int id)\n{\n    struct Address addr = {.id = id, .set = 0};\n    conn->db->rows[id] = addr;\n}\n\nvoid Database_list(struct Connection *conn)\n{\n    int i = 0;\n    struct Database *db = conn->db;\n\n    for(i = 0; i < MAX_ROWS; i++) {\n        struct Address *cur = &db->rows[i];\n\n        if(cur->set) {\n            Address_print(cur);\n        }\n    }\n}\n\nint main(int argc, char *argv[])\n{\n    if(argc < 3) die(\"USAGE: ex17 <dbfile> <action> [action params]\");\n\n    char *filename = argv[1];\n    char action = argv[2][0];\n    struct Connection *conn = Database_open(filename, action);\n    int id = 0;\n\n    if(argc > 3) id = atoi(argv[3]);\n    if(id >= MAX_ROWS) die(\"There's not that many records.\");\n\n    switch(action) {\n        case 'c':\n            Database_create(conn);\n            Database_write(conn);\n            break;\n\n        case 'g':\n            if(argc != 4) die(\"Need an id to get\");\n\n            Database_get(conn, id);\n            break;\n\n        case 's':\n            if(argc != 6) die(\"Need id, name, email to set\");\n\n            Database_set(conn, id, argv[4], argv[5]);\n            Database_write(conn);\n            break;\n\n        case 'd':\n            if(argc != 4) die(\"Need id to delete\");\n\n            Database_delete(conn, id);\n            Database_write(conn);\n            break;\n\n        case 'l':\n            Database_list(conn);\n            break;\n        default:\n            die(\"Invalid action, only: c=create, g=get, s=set, d=del, l=list\");\n    }\n\n    Database_close(conn);\n\n    return 0;\n}\n```\n\n在这个程序中我使用了一系列的结构来创建用于地址薄的小型数据库。其中，我是用了一些你从来没见过的东西，所以你应该逐行浏览这段代码，解释每一行做了什么，并且查询你不认识的任何函数。下面是你需要注意的几个关键部分：\n\n`#define` 常量\n\n我使用了“C预处理器”的另外一部分，来创建`MAX_DATA`和`MAX_ROWS`的设置常量。我之后会更多地讲解预处理器的功能，不过这是一个创建可靠的常量的简易方法。除此之外还有另一种方法，但是在特定场景下并不适用。\n\n定长结构体\n\n`Address`结构体接着使用这些常量来创建数据，这些数据是定长的，它们并不高效，但是便于存储和读取。`Database`结构体也是定长的，因为它有一个定长的`Address`结构体数组。这样你就可以稍后把整个数据一步写到磁盘。\n\n出现错误时终止的`die`函数\n\n在像这样的小型程序中，你可以编写一个单个函数在出现错误时杀掉程序。我把它叫做`die`。而且在任何失败的函数调用，或错误输出之后，它会调用`exit`带着错误退出程序。\n\n用于错误报告的 `errno`和`perror`\n\n当函数返回了一个错误时，它通常设置一个叫做`errno`的“外部”变量，来描述发生了什么错误。它们只是数字，所以你可以使用`perror`来“打印出错误信息”。\n\n\n文件函数\n\n我使用了一些新的函数，比如`fopen`，`fread`，`fclose`，和`rewind`来处理文件。这些函数中每个都作用于`FILE`结构体上，就像你的结构体似的，但是它由C标准库定义。\n\n嵌套结构体指针\n\n你应该学习这里的嵌套结构器和获取数组元素地址的用法，它读作“读取`db`中的`conn`中的`rows`的第`i`个元素，并返回地址（`&`）”。\n\n> 译者注：这里有个更简便的写法是`db->conn->row + i`。\n\n结构体原型的复制\n\n它在`Database_delete`中体现得最清楚，你可以看到我是用了临时的局部`Address`变量，初始化了它的`id`和`set`字段，接着通过把它赋值给`rows`数组中的元素，简单地复制到数组中。这个小技巧确保了所有除了`set`和`id`的字段都初始化为0，而且很容易编写。顺便说一句，你不应该在这种数组复制操作中使用`memcpy`。现代C语言中你可以只是将一个赋值给另一个，它会自动帮你处理复制。\n\n处理复杂参数\n\n我执行了一些更复杂的参数解析，但是这不是处理它们的最好方法。在这本书的后面我们将会了解一些用于解析的更好方法。\n\n将字符串转换为整数\n\n我使用了`atoi`函数在命令行中接受作为id的字符串并把它转换为`int id`变量。去查询这个函数以及相似的函数。\n\n在堆上分配大块数据\n\n这个程序的要点就是在我创建`Database`的时候，我使用了`malloc`来向OS请求一块大容量的内存。稍后我会讲得更细致一些。\n\n`NULL`就是0，所以可转成布尔值\n\n在许多检查中，我简单地通过`if(!ptr) die(\"fail!\")`检测了一个指针是不是`NULL`。这是有效的，因为`NULL`会被计算成假。在一些少见的系统中，`NULL`会储存在计算机中，并且表示为一些不是0的东西。但在C标准中，你可以把它当成0来编写代码。到目前为止，当我说“`NULL`就是0”的时候，我都是对一些迂腐的人说的。\n\n## 你会看到什么\n\n你应该为此花费大量时间，知道你可以测试它能正常工作了。并且你应当用`Valgrind`来确保你在所有地方都正确使用内存。下面是我的测试记录，并且随后使用了`Valgrind`来检查操作：\n\n```sh\n$ make ex17\ncc -Wall -g    ex17.c   -o ex17\n$ ./ex17 db.dat c\n$ ./ex17 db.dat s 1 zed zed@zedshaw.com\n$ ./ex17 db.dat s 2 frank frank@zedshaw.com\n$ ./ex17 db.dat s 3 joe joe@zedshaw.com\n$\n$ ./ex17 db.dat l\n1 zed zed@zedshaw.com\n2 frank frank@zedshaw.com\n3 joe joe@zedshaw.com\n$ ./ex17 db.dat d 3\n$ ./ex17 db.dat l\n1 zed zed@zedshaw.com\n2 frank frank@zedshaw.com\n$ ./ex17 db.dat g 2\n2 frank frank@zedshaw.com\n$\n$ valgrind --leak-check=yes ./ex17 db.dat g 2\n# cut valgrind output...\n$\n```\n\n`Valgrind`实际的输出没有显式，因为你应该能够发现它。\n\n> 注\n\n> `Vagrind`可以报告出你泄露的小块内存，但是它有时会过度报告OSX内部的API。如果你发现它显示了不属于你代码中的泄露，可以忽略它们。\n\n## 堆和栈的内存分配\n\n对于现在你们这些年轻人来说，编程简直太容易了。如果你玩玩Ruby或者Python的话，只要创建对象或变量就好了，不用管它们存放在哪里。你并不关心它们是否存放在栈上或堆上。你的编程语言甚至完全不会把变量放在栈上，它们都在堆上，并且你也不知道是否是这样。\n\n然而C完全不一样，因为它使用了CPU真实的机制来完成工作，这涉及到RAM中的一块叫做栈的区域，以及另外一块叫做堆的区域。它们的差异取决于取得储存空间的位置。\n\n堆更容易解释，因为它就是你电脑中的剩余内存，你可以通过`malloc`访问它来获取更多内存，OS会使用内部函数为你注册一块内存区域，并且返回指向它的指针。当你使用完这片区域时，你应该使用`free`把它交还给OS，使之能被其它程序复用。如果你不这样做就会导致程序“泄露”内存，但是`Valgrind`会帮你监测这些内存泄露。\n\n栈是一个特殊的内存区域，它储存了每个函数的创建的临时变量，它们对于该函数为局部变量。它的工作机制是，函数的每个函数都会“压入”栈中，并且可在函数内部使用。它是一个真正的栈数据结构，所以是后进先出的。这对于`main`中所有类似`char section`和`int id`的局部变量也是相同的。使用栈的优点是，当函数退出时C编译器会从栈中“弹出”所有变量来清理。这非常简单，也防止了栈上变量的内存泄露。\n\n理清内存的最简单的方式是遵守这条原则：如果你的变量并不是从`malloc`中获取的，也不是从一个从`malloc`获取的函数中获取的，那么它在栈上。\n\n下面是三个值得关注的关于栈和堆的主要问题：\n\n+ 如果你从`malloc`获取了一块内存，并且把指针放在了栈上，那么当函数退出时，指针会被弹出而丢失。\n+ 如果你在栈上存放了大量数据（比如大结构体和数组），那么会产生“栈溢出”并且程序会中止。这种情况下应该通过`malloc`放在堆上。\n+ 如果你获取了指向栈上变量的指针，并且将它用于传参或从函数返回，接收它的函数会产生“段错误”。因为实际的数据被弹出而消失，指针也会指向被释放的内存。\n\n这就是我在程序中使用`Database_open`来分配内存或退出的原因，相应的`Database_close`用于释放内存。如果你创建了一个“创建”函数，它创建了一些东西，那么一个“销毁”函数可以安全地清理这些东西。这样会更容易理清内存。\n\n最后，当一个程序退出时，OS会为你清理所有的资源，但是有时不会立即执行。一个惯用法（也是本次练习中用到的）是立即终止并且让OS清理错误。\n\n## 如何使它崩溃\n\n这个程序有很多可以使之崩溃的地方，尝试下面这些东西，同时也想出自己的办法。\n\n+ 最经典的方法是移除一些安全检查，你就可以传入任意数据。例如，第160行的检查防止你传入任何记录序号。\n+ 你也可以尝试弄乱数据文件。使用任何编辑器打开它并且随机修改几个字节并关闭。\n+ 你也可以寻找在运行中向程序传递非法参数的办法。例如将文件参数放到动作后面，就会创建一个以动作命名的文件，并且按照文件名的第一个字符执行动作。\n+ 这个程序中有个bug，因为`strncpy`有设计缺陷。查询`strncpy`的相关资料，然后试着弄清楚如果`name`或者`address`超过512个字节会发生什么。可以通过简单把最后一个字符设置成`'\\0'`来修复它，你应该无论如何都这样做（这也是函数原本应该做的）。\n+ 在附加题中我会让你传递参数来创建任意大小的数据库。在你造成程序退出或`malloc`的内存不足之前，尝试找出最大的数据库尺寸是多少。\n\n## 附加题\n\n+ `die`函数需要接收`conn`变量作为参数，以便执行清理并关闭它。\n+ 修改代码，使其接收参数作为`MAX_DATA`和`MAX_ROWS`，将它们储存在`Database`结构体中，并且将它们写到文件。这样就可以创建任意大小的数据库。\n+ 向数据库添加更多操作，比如`find`。\n+ 查询C如何打包结构体，并且试着弄清楚为什么你的文件是相应的大小。看看你是否可以计算出结构体添加一些字段之后的新大小。\n+ 向`Address`添加一些字段，使它们可被搜索。\n+ 编写一个shell脚本来通过以正确顺序运行命令执行自动化测试。提示：在`bash`顶端使用使用`set -e`，使之在任何命令发生错误时退出。\n  > 译者注：使用Python编写多行脚本或许更方便一些。\n+ 尝试重构程序，使用单一的全局变量来储存数据库连接。这个新版本和旧版本比起来如何？\n+ 搜索“栈数据结构”，并且在你最喜欢的语言中实现它，然后尝试在C中实现。\n"
  },
  {
    "path": "docs/lcthw-zh/ex18.md",
    "content": "# 练习18：函数指针\n\n> 原文：[Exercise 18: Pointers To Functions](http://c.learncodethehardway.org/book/ex18.html)\n\n> 译者：[飞龙](https://github.com/wizardforcel)\n\n函数在C中实际上只是指向程序中某一个代码存在位置的指针。就像你创建过的结构体指针、字符串和数组那样，你也可以创建指向函数的指针。函数指针的主要用途是向其他函数传递“回调”，或者模拟类和对象。在这个练习中我们会创建一些回调，并且下一节我们会制作一个简单的对象系统。\n\n函数指针的格式类似这样：\n\n```c\nint (*POINTER_NAME)(int a, int b)\n```\n\n记住如何编写它的一个方法是：\n\n+ 编写一个普通的函数声明：`int callme(int a, int b)`\n+ 将函数用指针语法包装：`int (*callme)(int a, int b)`\n+ 将名称改成指针名称：`int (*compare_cb)(int a, int b)`\n\n这个方法的关键是，当你完成这些之后，指针的变量名称为`compare_cb`，而你可以将它用作函数。这类似于指向数组的指针可以表示所指向的数组。指向函数的指针也可以用作表示所指向的函数，只不过是不同的名字。\n\n```c\nint (*tester)(int a, int b) = sorted_order;\nprintf(\"TEST: %d is same as %d\\n\", tester(2, 3), sorted_order(2, 3));\n```\n\n即使是对于返回指针的函数指针，上述方法依然有效：\n\n+ 编写：`char *make_coolness(int awesome_levels)`\n+ 包装：`char *(*make_coolness)(int awesome_levels)`\n+ 重命名：`char *(*coolness_cb)(int awesome_levels)`\n\n需要解决的下一个问题是使用函数指针向其它函数提供参数比较困难，比如当你打算向其它函数传递回调函数的时候。解决方法是使用`typedef`，它是C的一个关键字，可以给其它更复杂的类型起个新的名字。你需要记住的事情是，将`typedef`添加到相同的指针语法之前，然后你就可以将那个名字用作类型了。我使用下面的代码来演示这一特性：\n\n```c\n#include <stdio.h>\n#include <stdlib.h>\n#include <errno.h>\n#include <string.h>\n\n/** Our old friend die from ex17. */\nvoid die(const char *message)\n{\n    if(errno) {\n        perror(message);\n    } else {\n        printf(\"ERROR: %s\\n\", message);\n    }\n\n    exit(1);\n}\n\n// a typedef creates a fake type, in this\n// case for a function pointer\ntypedef int (*compare_cb)(int a, int b);\n\n/**\n * A classic bubble sort function that uses the\n * compare_cb to do the sorting.\n */\nint *bubble_sort(int *numbers, int count, compare_cb cmp)\n{\n    int temp = 0;\n    int i = 0;\n    int j = 0;\n    int *target = malloc(count * sizeof(int));\n\n    if(!target) die(\"Memory error.\");\n\n    memcpy(target, numbers, count * sizeof(int));\n\n    for(i = 0; i < count; i++) {\n        for(j = 0; j < count - 1; j++) {\n            if(cmp(target[j], target[j+1]) > 0) {\n                temp = target[j+1];\n                target[j+1] = target[j];\n                target[j] = temp;\n            }\n        }\n    }\n\n    return target;\n}\n\nint sorted_order(int a, int b)\n{\n    return a - b;\n}\n\nint reverse_order(int a, int b)\n{\n    return b - a;\n}\n\nint strange_order(int a, int b)\n{\n    if(a == 0 || b == 0) {\n        return 0;\n    } else {\n        return a % b;\n    }\n}\n\n/**\n * Used to test that we are sorting things correctly\n * by doing the sort and printing it out.\n */\nvoid test_sorting(int *numbers, int count, compare_cb cmp)\n{\n    int i = 0;\n    int *sorted = bubble_sort(numbers, count, cmp);\n\n    if(!sorted) die(\"Failed to sort as requested.\");\n\n    for(i = 0; i < count; i++) {\n        printf(\"%d \", sorted[i]);\n    }\n    printf(\"\\n\");\n\n    free(sorted);\n}\n\n\nint main(int argc, char *argv[])\n{\n    if(argc < 2) die(\"USAGE: ex18 4 3 1 5 6\");\n\n    int count = argc - 1;\n    int i = 0;\n    char **inputs = argv + 1;\n\n    int *numbers = malloc(count * sizeof(int));\n    if(!numbers) die(\"Memory error.\");\n\n    for(i = 0; i < count; i++) {\n        numbers[i] = atoi(inputs[i]);\n    }\n\n    test_sorting(numbers, count, sorted_order);\n    test_sorting(numbers, count, reverse_order);\n    test_sorting(numbers, count, strange_order);\n\n    free(numbers);\n\n    return 0;\n}\n```\n\n在这段程序中，你将创建动态排序的算法，它会使用比较回调对整数数组排序。下面是这个程序的分解，你应该能够清晰地理解它。\n\nex18.c:1~6\n\n通常的包含，用于所调用的所有函数。\n\nex18.c:7~17\n\n这就是之前练习的`die`函数，我将它用于错误检查。\n\nex18.c:21\n\n这是使用`typedef`的地方，在后面我像`int`或`char`类型那样，在`bubble_sort`和`test_sorting`中使用了`compare_cb`。\n\nex18.c:27~49\n\n一个冒泡排序的实现，它是整数排序的一种不高效的方法。这个函数包含了：\n\nex18.c:27\n\n这里是将`typedef`用于` compare_cb`作为`cmp`最后一个参数的地方。现在它是一个会返回两个整数比较结果用于排序的函数。\n\nex18.c:29~34\n\n栈上变量的通常创建语句，前面是使用`malloc`创建的堆上整数数组。确保你理解了`count * sizeof(int)`做了什么。\n\nex18.c:38\n\n冒泡排序的外循环。\n\nex18.c:39\n\n冒泡排序的内循环。\n\nex18.c:40\n\n现在我调用了`cmp`回调，就像一个普通函数那样，但是不通过预先定义好的函数名，而是一个指向它的指针。调用者可以像它传递任何参数，只要这些参数符合`compare_cb` `typedef`的签名。\n\nex18.c:41-43\n\n冒泡排序所需的实际交换操作。\n\nex18.c:48\n\n最后返回新创建和排序过的结果数据`target`。\n\nex18.c:51-68\n\n`compare_cb`函数类型三个不同版本，它们需要和我们所创建的`typedef`具有相同的定义。否则C编辑器会报错说类型不匹配。\n\nex18.c:74-87\n\n这是`bubble_sort`函数的测试。你可以看到我同时将`compare_cb`传给了`bubble_sort`来演示它是如何像其它指针一样传递的。\n\nex18.c:90-103\n\n一个简单的主函数，基于你通过命令行传递进来的整数，创建了一个数组。然后调用了`test_sorting`函数。\n\nex18.c:105-107\n\n最后，你会看到`compare_cb`函数指针的`typedef`是如何使用的。我仅仅传递了`sorted_order`、`reverse_order`和`strange_order`的名字作为函数来调用`test_sorting`。C编译器会找到这些函数的地址，并且生成指针用于`test_sorting`。如果你看一眼`test_sorting`你会发现它把这些函数传给了`bubble_sort`，并不关心它们是做了什么。只要符合`compare_cb`原型的东西都有效。\n\nex18.c:109\n\n我们在最后释放了我们创建的整数数组。\n\n## 你会看到什么\n\n运行这个程序非常简单，但是你要尝试不同的数字组合，甚至要尝试输入非数字来看看它做了什么：\n\n```sh\n$ make ex18\ncc -Wall -g    ex18.c   -o ex18\n$ ./ex18 4 1 7 3 2 0 8\n0 1 2 3 4 7 8\n8 7 4 3 2 1 0\n3 4 2 7 1 0 8\n$\n```\n\n## 如何使它崩溃\n\n我打算让你做一些奇怪的事情来使它崩溃，这些函数指针都是类似于其它指针的指针，他们都指向内存的一块区域。C中可以将一种指针的指针转换为另一种，以便以不同方式处理数据。这些通常是不必要的，但是为了想你展示如何侵入你的电脑，我希望你把这段代码添加在`test_sorting`下面：\n\n```c\nunsigned char *data = (unsigned char *)cmp;\n\nfor(i = 0; i < 25; i++) {\n    printf(\"%02x:\", data[i]);\n}\n\nprintf(\"\\n\");\n```\n\n这个循环将你的函数转换成字符串，并且打印出来它的内容。这并不会中断你的程序，除非CPU和OS在执行过程中遇到了问题。在它打印排序过的数组之后，你所看到的是一个十六进制数字的字符串：\n\n```\n55:48:89:e5:89:7d:fc:89:75:f8:8b:55:fc:8b:45:f8:29:d0:c9:c3:55:48:89:e5:89:\n```\n\n这就应该是函数的原始的汇编字节码了，你应该能看到它们有相同的起始和不同的结尾。也有可能这个循环并没有获得函数的全部，或者获得了过多的代码而跑到程序的另外一片空间。这些不通过更多分析是不可能知道的。\n\n## 附加题\n\n+ 用十六进制编辑器打开`ex18`，接着找到函数起始处的十六进制代码序列，看看是否能在原始程序中找到函数。\n+ 在你的十六进制编辑器中找到更多随机出现的东西并修改它们。重新运行你的程序看看发生了什么。字符串是你最容易修改的东西。\n+ 将错误的函数传给`compare_cb`，并看看C编辑器会报告什么错误。\n+ 将`NULL`传给它，看看程序中会发生什么。然后运行`Valgrind`来看看它会报告什么。\n+ 编写另一个排序算法，修改`test_sorting`使它接收任意的排序函数和排序函数的比较回调。并使用它来测试两种排序算法。\n"
  },
  {
    "path": "docs/lcthw-zh/ex19.md",
    "content": "# 练习19：一个简单的对象系统\n\n> 原文：[Exercise 19: A Simple Object System](http://c.learncodethehardway.org/book/ex19.html)\n\n> 译者：[飞龙](https://github.com/wizardforcel)\n\n我在学习面向对象编程之前学了C，所以它有助于我在C中构建面向对象系统，来理解OOP的基本含义。你可能在学习C之前就学了OOP语言，所以这章也可能会起到一种衔接作用。这个联系中，你将会构建一个简单的对象系统，但是也会了解更多关于C预处理器的事情。\n\n这个练习会构建一个简单的游戏，在游戏中你会在一个小型的城堡中杀死弥诺陶洛斯，并没有任何神奇之处，只是四个房间和一个坏家伙。这个练习同时是一个多文件的项目，并且比起之前的一些程序看起来更像一个真正的C程序。我在这里介绍C预处理器的原因，是你需要它来在你自己的程序中创建多个文件。\n\n## C预处理器如何工作\n\nC预处理器是个模板处理系统，它主要的用途是让C代码的编程更加容易，但是它通过一个语法感知的模板机制来实现。以前人们主要使用C预处理器来储存常量，以及创建“宏”来简化复杂的代码。在现代C语言中你会实际上使用它作为代码生成器来创建模板化的代码片段。\n\nC预处理器的工作原理是，如果你给它一个文件，比如`.c`文件，它会处理以`#`（井号）字符开头的各种文本。当它遇到一个这样的文本时，它会对输入文件中的文本做特定的替换。C预处理器的主要优点是他可以包含其他文件，并且基于该文件的内容对它的宏列表进行扩展。\n\n一个快速查看预处理器所做事情的方法，是对上个练习中的代码执行下列命令：\n\n```sh\ncpp ex18.c | less\n```\n\n这会产生大量输出，但是如果你滚动它，会看到你使用`#include`包含的其他文件的内容。在原始的代码中向下滚动，你可以看到`cpp`如何基于头文件中不同的`#define`宏来转换代码。\n\nC编译器与`cpp`的集成十分紧密，这个例子只是向你展示它是如何在背后工作的。在现代C语言中，`cpp`系统也集成到C的函数中，你或许可以将它当做C语言的一部分。\n\n在剩余的章节中，我们会使用更多预处理器的语法，并且像往常一样解释它们。\n\n## 原型对象系统\n\n我们所创建的OOP系统是一个简单的“原型”风格的对象系统，很像JavaScript。你将以设置为字段的原型来开始，而不是类，接着将他们用作创建其它对象实例的基础。这个“没有类”的设计比起传统的基于类的对象系统更加易于实现和使用。\n\n## Object头文件\n\n我打算将数据类型和函数声明放在一个单独的头文件中，叫做`object.h`。这个是一个标准的C技巧，可以让你集成二进制库，但其它程序员任然需要编译。在这个文件中，我使用了多个高级的C预处理器技巧，我接下来准备简略地描述它们，并且你会在后续的步骤中看到。\n\n```c\n#ifndef _object_h\n#define _object_h\n\ntypedef enum {\n    NORTH, SOUTH, EAST, WEST\n} Direction;\n\ntypedef struct {\n    char *description;\n    int (*init)(void *self);\n    void (*describe)(void *self);\n    void (*destroy)(void *self);\n    void *(*move)(void *self, Direction direction);\n    int (*attack)(void *self, int damage);\n} Object;\n\nint Object_init(void *self);\nvoid Object_destroy(void *self);\nvoid Object_describe(void *self);\nvoid *Object_move(void *self, Direction direction);\nint Object_attack(void *self, int damage);\nvoid *Object_new(size_t size, Object proto, char *description);\n\n#define NEW(T, N) Object_new(sizeof(T), T##Proto, N)\n#define _(N) proto.N\n\n#endif\n```\n\n看一看这个文件，你会发现我使用了几个新的语法片段，你之前从来没见过它们：\n\n`#ifndef`\n\n你已经见过了用于创建简单常量的`#define`，但是C预处理器可以根据条件判断来忽略一部分代码。这里的`#ifndef`是“如果没有被定义”的意思，它会检查是否已经出现过`#define _object_h`，如果已出现，就跳过这段代码。我之所以这样写，是因为我们可以将这个文件包含任意次，而无需担心多次定义里面的东西。\n\n`#define`\n\n有了上面保护该文件的`#ifndef`，我们接着添加`_object_h`的定义，因此之后任何试图包含此文件的行为，都会由于上面的语句而跳过这段代码。\n\n`#define NEW(T,N)`\n\n这条语句创建了一个宏，就像模板函数一样，无论你在哪里编写左边的代码，都会展开成右边的代码。这条语句仅仅是对我们通常调用的`Object_new`制作了一个快捷方式，并且避免了潜在的调用错误。在宏这种工作方式下，`T`、`N`还有`New`都被“注入”进了右边的代码中。`T##Proto`语法表示“将Proto连接到T的末尾”，所以如果你写下`NEW(Room, \"Hello.\")`，就会在这里变成`RoomProto`。\n\n`#define _(N)`\n\n这个宏是一种为对象系统设计的“语法糖”，将`obj->proto.blah`简写为`obj->_(blah)`。它不是必需的，但是它是一个接下来会用到的有趣的小技巧。\n\n## Object源文件\n\n`object.h`是声明函数和数据类型的地方，它们在`object.c`中被定义（创建），所以接下来：\n\n```c\n#include <stdio.h>\n#include <string.h>\n#include <stdlib.h>\n#include \"object.h\"\n#include <assert.h>\n\nvoid Object_destroy(void *self)\n{\n    Object *obj = self;\n\n    if(obj) {\n        if(obj->description) free(obj->description);\n        free(obj);\n    }\n}\n\nvoid Object_describe(void *self)\n{\n    Object *obj = self;\n    printf(\"%s.\\n\", obj->description);\n}\n\nint Object_init(void *self)\n{\n    // do nothing really\n    return 1;\n}\n\nvoid *Object_move(void *self, Direction direction)\n{\n    printf(\"You can't go that direction.\\n\");\n    return NULL;\n}\n\nint Object_attack(void *self, int damage)\n{\n    printf(\"You can't attack that.\\n\");\n    return 0;\n}\n\nvoid *Object_new(size_t size, Object proto, char *description)\n{\n    // setup the default functions in case they aren't set\n    if(!proto.init) proto.init = Object_init;\n    if(!proto.describe) proto.describe = Object_describe;\n    if(!proto.destroy) proto.destroy = Object_destroy;\n    if(!proto.attack) proto.attack = Object_attack;\n    if(!proto.move) proto.move = Object_move;\n\n    // this seems weird, but we can make a struct of one size,\n    // then point a different pointer at it to \"cast\" it\n    Object *el = calloc(1, size);\n    *el = proto;\n\n    // copy the description over\n    el->description = strdup(description);\n\n    // initialize it with whatever init we were given\n    if(!el->init(el)) {\n        // looks like it didn't initialize properly\n        el->destroy(el);\n        return NULL;\n    } else {\n        // all done, we made an object of any type\n        return el;\n    }\n}\n```\n\n\n这个文件中并没有什么新东西，除了一个小技巧之外。`Object_new`函数通过把原型放到结构体的开头，利用了`structs`工作机制的一个方面。当你在之后看到`ex19.h`头文件时，你会明白为什么我将`Object`作为结构体的第一个字段。由于C按顺序将字段放入结构体，并且由于指针可以指向一块内存，我就可以将指针转换为任何我想要的东西。在这种情况下，即使我通过`calloc`获取了一大块内存，我仍然可以使用`Object`指针来指向它。\n\n当我开始编写`ex19.h`文件时，我会把它解释得更详细一些，因为当你看到它怎么用的时候才能更容易去理解它。\n\n上面的代码创建了基本的对象系统，但是你需要编译它和将它链接到`ex19.c`文件，来创建出完整的程序。`object.c`文件本身并没有`main`函数，所以它不可能被编译为完整的程序。下面是一个`Makefile`文件，它基于已经完成的事情来构建程序：\n\n```make\nCFLAGS=-Wall -g\n\nall: ex19\n\nex19: object.o\n\nclean:\n  rm -f ex19\n```\n\n这个`Makefile`所做的事情仅仅是让`ex19`依赖于`object.o`。还记得`make`可以根据扩展名构建不同的文件吗？这相当于告诉`make`执行下列事情：\n\n+ 当我运行`make`时，默认的`all`会构建`ex19`。\n+ 当它构建`ex19`时，也需要构建`object.o`，并且将它包含在其中。\n+ `make`并不能找到`object.o`，但是它能发现`object.c`文件，并且知道如何把`.c`文件变成`.o`文件，所以它就这么做了。\n+ 一旦`object.o`文件构建完成，它就会运行正确的编译命令，从`ex19.c`和`object.o`中构建`ex19`。\n\n## 游戏实现\n\n一旦你编写完成了那些文件，你需要使用对象系统来实现实际的游戏，第一步就是把所有数据类型和函数声明放在`ex19.h`文件中：\n\n```c\n#ifndef _ex19_h\n#define _ex19_h\n\n#include \"object.h\"\n\nstruct Monster {\n    Object proto;\n    int hit_points;\n};\n\ntypedef struct Monster Monster;\n\nint Monster_attack(void *self, int damage);\nint Monster_init(void *self);\n\nstruct Room {\n    Object proto;\n\n    Monster *bad_guy;\n\n    struct Room *north;\n    struct Room *south;\n    struct Room *east;\n    struct Room *west;\n};\n\ntypedef struct Room Room;\n\nvoid *Room_move(void *self, Direction direction);\nint Room_attack(void *self, int damage);\nint Room_init(void *self);\n\n\nstruct Map {\n    Object proto;\n    Room *start;\n    Room *location;\n};\n\ntypedef struct Map Map;\n\nvoid *Map_move(void *self, Direction direction);\nint Map_attack(void *self, int damage);\nint Map_init(void *self);\n\n#endif\n```\n\n它创建了三个你将会用到的新对象：`Monster`，`Room`，和`Map`。\n\n看一眼`object.c:52`，你可以看到这是我使用`Object *el = calloc(1, size)`的地方。回去看`object.h`的`NEW`宏，你可以发现它获得了另一个结构体的`sizeof`，比如`Room`，并且分配了这么多的空间。然而，由于我像一个`Object`指针指向了这块内存，并且我在`Room`的开头放置了`Object proto`，所以就可以将`Room`当成`Object`来用。\n\n详细分解请见下面：\n\n+ 我调用了`NEW(Room, \"Hello.\")`，C预处理器会将其展开为`Object_new(sizeof(Room), RoomProto, \"Hello.\")`。\n+ 执行过程中，在`Object_new`的内部我分配了`Room`大小的一块内存，但是用`Object *el`来指向它。\n+ 由于C将`Room.proto`字段放在开头，这意味着`el`指针实际上指向了能访问到完整`Object`结构体的，足够大小的一块内存。它不知道这块内存叫做`proto`。\n+ 接下来它使用`Object *el`指针，通过`*el = proto`来设置这块内存的内容。要记住你可以复制结构体，而且`*el`的意思是“`el`所指向对象的值”，所以整条语句意思是“将`el`所指向对象的值赋为`proto`”。\n+ 由于这个谜之结构体被填充为来自`proto`的正确数据，这个函数接下来可以在`Object`上调用`init`，或者`destroy`。但是最神奇的一部分是无论谁调用这个函数都可以将它们改为想要的东西。\n\n结合上面这些东西，我就可以使用这一个函数来创建新的类型，并且向它们提供新的函数来修改它们的行为。这看起来像是“黑魔法”，但它是完全有效的C代码。实际上，有少数标准的系统函数也以这种方式工作，我们将会用到一些这样的函数在网络程序中转换地址。\n\n编写完函数定义和数据结构之后，我现在就可以实现带有四个房间和一个牛头人的游戏了。\n\n```c\n#include <stdio.h>\n#include <errno.h>\n#include <stdlib.h>\n#include <string.h>\n#include <time.h>\n#include \"ex19.h\"\n\n\nint Monster_attack(void *self, int damage)\n{\n    Monster *monster = self;\n\n    printf(\"You attack %s!\\n\", monster->_(description));\n\n    monster->hit_points -= damage;\n\n    if(monster->hit_points > 0) {\n        printf(\"It is still alive.\\n\");\n        return 0;\n    } else {\n        printf(\"It is dead!\\n\");\n        return 1;\n    }\n}\n\nint Monster_init(void *self)\n{\n    Monster *monster = self;\n    monster->hit_points = 10;\n    return 1;\n}\n\nObject MonsterProto = {\n    .init = Monster_init,\n    .attack = Monster_attack\n};\n\n\nvoid *Room_move(void *self, Direction direction)\n{\n    Room *room = self;\n    Room *next = NULL;\n\n    if(direction == NORTH && room->north) {\n        printf(\"You go north, into:\\n\");\n        next = room->north;\n    } else if(direction == SOUTH && room->south) {\n        printf(\"You go south, into:\\n\");\n        next = room->south;\n    } else if(direction == EAST && room->east) {\n        printf(\"You go east, into:\\n\");\n        next = room->east;\n    } else if(direction == WEST && room->west) {\n        printf(\"You go west, into:\\n\");\n        next = room->west;\n    } else {\n        printf(\"You can't go that direction.\");\n        next = NULL;\n    }\n\n    if(next) {\n        next->_(describe)(next);\n    }\n\n    return next;\n}\n\n\nint Room_attack(void *self, int damage)\n{\n    Room *room = self;\n    Monster *monster = room->bad_guy;\n\n    if(monster) {\n        monster->_(attack)(monster, damage);\n        return 1;\n    } else {\n        printf(\"You flail in the air at nothing. Idiot.\\n\");\n        return 0;\n    }\n}\n\n\nObject RoomProto = {\n    .move = Room_move,\n    .attack = Room_attack\n};\n\n\nvoid *Map_move(void *self, Direction direction)\n{\n    Map *map = self;\n    Room *location = map->location;\n    Room *next = NULL;\n\n    next = location->_(move)(location, direction);\n\n    if(next) {\n        map->location = next;\n    }\n\n    return next;\n}\n\nint Map_attack(void *self, int damage)\n{\n    Map* map = self;\n    Room *location = map->location;\n\n    return location->_(attack)(location, damage);\n}\n\n\nint Map_init(void *self)\n{\n    Map *map = self;\n\n    // make some rooms for a small map\n    Room *hall = NEW(Room, \"The great Hall\");\n    Room *throne = NEW(Room, \"The throne room\");\n    Room *arena = NEW(Room, \"The arena, with the minotaur\");\n    Room *kitchen = NEW(Room, \"Kitchen, you have the knife now\");\n\n    // put the bad guy in the arena\n    arena->bad_guy = NEW(Monster, \"The evil minotaur\");\n\n    // setup the map rooms\n    hall->north = throne;\n\n    throne->west = arena;\n    throne->east = kitchen;\n    throne->south = hall;\n\n    arena->east = throne;\n    kitchen->west = throne;\n\n    // start the map and the character off in the hall\n    map->start = hall;\n    map->location = hall;\n\n    return 1;\n}\n\nObject MapProto = {\n    .init = Map_init,\n    .move = Map_move,\n    .attack = Map_attack\n};\n\nint process_input(Map *game)\n{\n    printf(\"\\n> \");\n\n    char ch = getchar();\n    getchar(); // eat ENTER\n\n    int damage = rand() % 4;\n\n    switch(ch) {\n        case -1:\n            printf(\"Giving up? You suck.\\n\");\n            return 0;\n            break;\n\n        case 'n':\n            game->_(move)(game, NORTH);\n            break;\n\n        case 's':\n            game->_(move)(game, SOUTH);\n            break;\n\n        case 'e':\n            game->_(move)(game, EAST);\n            break;\n\n        case 'w':\n            game->_(move)(game, WEST);\n            break;\n\n        case 'a':\n\n            game->_(attack)(game, damage);\n            break;\n        case 'l':\n            printf(\"You can go:\\n\");\n            if(game->location->north) printf(\"NORTH\\n\");\n            if(game->location->south) printf(\"SOUTH\\n\");\n            if(game->location->east) printf(\"EAST\\n\");\n            if(game->location->west) printf(\"WEST\\n\");\n            break;\n\n        default:\n            printf(\"What?: %d\\n\", ch);\n    }\n\n    return 1;\n}\n\nint main(int argc, char *argv[])\n{\n    // simple way to setup the randomness\n    srand(time(NULL));\n\n    // make our map to work with\n    Map *game = NEW(Map, \"The Hall of the Minotaur.\");\n\n    printf(\"You enter the \");\n    game->location->_(describe)(game->location);\n\n    while(process_input(game)) {\n    }\n\n    return 0;\n}\n```\n\n说实话这里面并没有很多你没有见过的东西，并且你只需要理解我使用头文件中宏的方法。下面是需要学习和理解的一些重要的核心知识：\n\n+ 实现一个原型涉及到创建它的函数版本，以及随后创建一个以“Proto”结尾的单一结构体。请参照`MonsterProto`，`RoomProto`和`MapProto`。\n+ 由于`Object_new`的实现方式，如果你没有在你的原型中设置一个函数，它会获得在`object.c`中创建的默认实现。\n+ 在`Map_init`中我创建了一个微型世界，然而更重要的是我使用了`object.h`中的`NEW`宏来创建全部对象。要把这一概念记在脑子里，可以试着把使用`NEW`的地方替换成`Object_new`的直接调用，来观察它如何被替换。\n+ 使用这些对象涉及到在它们上面调用函数，`_(N)`为我做了这些事情。如果你观察代码`monster->_(attack)(monster, damage)`，你会看到我使用了宏将其替换成`monster->proto.attack(monster, damage)`。通过重新将这些调用写成原始形式来再次学习这个转换。另外，如果你被卡住了，手动运行`cpp`来查看究竟发生了什么。\n+ 我使用了两个新的函数`srand`和`rand`，它们可以设置一个简单的随机数生成器，对于游戏已经够用了。我也使用了`time`来初始化随机数生成器。试着研究它们。\n+ 我使用了一个新的函数`getchar`来从标准输入中读取单个字符。试着研究它。\n\n## 你会看到什么\n\n下面是我自己的游戏的输出：\n\n```sh\n$ make ex19\ncc -Wall -g   -c -o object.o object.c\ncc -Wall -g    ex19.c object.o   -o ex19\n$ ./ex19\nYou enter the The great Hall.\n\n> l\nYou can go:\nNORTH\n\n> n\nYou go north, into:\nThe throne room.\n\n> l\nYou can go:\nSOUTH\nEAST\nWEST\n\n> e\nYou go east, into:\nKitchen, you have the knife now.\n\n> w\nYou go west, into:\nThe throne room.\n\n> s\nYou go south, into:\nThe great Hall.\n\n> n\nYou go north, into:\nThe throne room.\n\n> w\nYou go west, into:\nThe arena, with the minotaur.\n\n> a\nYou attack The evil minotaur!\nIt is still alive.\n\n> a\nYou attack The evil minotaur!\nIt is dead!\n\n> ^D\nGiving up? You suck.\n$\n```\n\n## 审计该游戏\n\n我把所有`assert`检查留给你作为练习，我通常把它们作为软件的一部分。你已经看到了我如何使用`assert`来保证程序正确运行。然而现在我希望你返回去并完成下列事情：\n\n+ 查看你定义的每个函数，一次一个文件。\n+ 在每个函数的最上面，添加`assert`来保证参数正确。例如在`Object_new`中要添加`assert(description != NULL)`。\n+ 浏览函数的每一行，找到所调用的任何函数。阅读它们的文档（或手册页），确认它们在错误下返回什么。添加另一个断言来检查错误是否发生。例如，`Object_new`在调用`calloc`之后应该进行`assert(el != NULL)`的检查。\n+ 如果函数应该返回一个值，也确保它返回了一个错误值（比如`NULL`），或者添加一个断言来确保返回值是有效的。例如，`Object_new`中，你需要在最后的返回之前添加`assert(el != NULL)`，由于它不应该为`NULL`。\n+ 对于每个你编写的`if`语句，确保都有对应的`else`语句，除非它用于错误检查并退出。\n+ 对于每个你编写的`switch`语句，确保都有一个`default`分支，来处理非预期的任何情况。\n\n花费一些时间浏览函数的每一行，并且找到你犯下的任何错误。记住这个练习的要点是从“码农”转变为“黑客”。试着找到使它崩溃的办法，然后尽可能编写代码来防止崩溃或者过早退出。\n\n## 附加题\n\n+ 修改`Makefile`文件，使之在执行`make clean`时能够同时清理`object.o`。\n+ 编写一个测试脚本，能够以多种方式来调用该游戏，并且扩展`Makefile`使之能够通过运行`make test`来测试该游戏。\n+ 在游戏中添加更多房间和怪物。\n+ 把游戏的逻辑放在其它文件中，并把它编译为`.o`。然后，使用它来编写另一个小游戏。如果你正确编写的话，你会在新游戏中创建新的`Map`和`main`函数。\n"
  },
  {
    "path": "docs/lcthw-zh/ex2.md",
    "content": "# 练习2：用Make来代替Python\n\n> 原文：[Exercise 2: Make Is Your Python Now](http://c.learncodethehardway.org/book/ex2.html)\n\n> 译者：[飞龙](https://github.com/wizardforcel)\n\n在Python中，你仅仅需要输入`python`，就可以运行你想要运行的代码。Python的解释器会运行它们，并且在运行中导入它所需的库和其它东西。C是完全不同的东西，你需要事先编译你的源文件，并且手动将它们整合为一个可以自己运行的二进制文件。手动来做这些事情很痛苦，在上一个练习中只需要运行`make`就能完成。\n\n这个练习是GNU make 的速成课，由于你在学C语言，所以你就必须掌握它。Make 将贯穿剩下的课程，等效于Python（命令）。它会构建源码，执行测试，设置一些选项以及为你做所有Python通常会做的事情。\n\n有所不同的是，我会向你展示一些更智能化的Makefile魔法，你不需要指出你的C程序的每一个愚蠢的细节来构建它。我不会在练习中那样做，但是你需要先用一段时间的“低级 make”，我才能向你演示“大师级的make”。\n\n## 使用 Make\n\n使用make的第一阶段就是用它已知的方式来构建程序。Make预置了一些知识，来从其它文件构建多种文件。上一个练习中，你已经使用像下面的命令来这样做了：\n\n```sh\n$ make ex1\n# or this one too\n$ CFLAGS=\"-Wall\" make ex1\n```\n\n第一个命令中你告诉make，“我想创建名为ex1的文件”。于是Make执行下面的动作：\n\n+ 文件`ex1`存在吗？\n+ 没有。好的，有没有其他文件以`ex1`开头？\n+ 有，叫做`ex1.c`。我知道如何构建`.c`文件吗？\n+ 是的，我会运行命令`cc ex1.c -o ex1`来构建它。\n+ 我将使用`cc`从`ex1.c`文件来为你构建`ex1`。\n\n上面列出的第二条命令是一种向make命令传递“修改器”的途径。如果你不熟悉Unix shell如何工作，你可以创建这些“环境变量”，它们会在程序运行时生效。有时你会用一条类似于`export CFLAGS=\"-Wall\"`的命令来执行相同的事情，取决于你所用的shell。然而你可以仅仅把它们放到你想执行的命令前面，于是环境变量只会在程序运行时有效。\n\n在这个例子中我执行了`CFLAGS=\"-Wall\" make ex1`，所以它会给make通常使用的`cc`命令添加`-Wall`选项。这行命令告诉`cc`编译器要报告所有的警告（然而实际上不可能报告所有警告）。\n\n实际上你可以深入探索使用make的上述方法，但是先让我们来看看`Makefile`，以便让你对make了解得更多一点。首先，创建文件并写入以下内容：\n\n```make\nCFLAGS=-Wall -g\n\nclean:\n    rm -f ex1\n```\n\n\n将文件在你的当前文件夹上保存为`Makefile`。Make会自动假设当前文件夹中有一个叫做`Makefile`的文件，并且会执行它。此外，一定要注意：确保你只输入了 TAB 字符，而不是空格和 TAB 的混合。\n\n> 译者注：上述代码中第四行`rm`前面是一个 TAB ，而不是多个等量的空格。\n\n`Makefile`向你展示了make的一些新功能。首先我们在文件中设置`CFLAGS`，所以之后就不用再设置了。并且，我们添加了`-g`标识来获取调试信息。接着我们写了一个叫做`clean`的部分，它告诉make如何清理我们的小项目。\n\n确保它和你的`ex1.c`文件在相同的目录中，之后运行以下命令：\n\n```sh\n$ make clean\n$ make ex1\n```\n\n## 你会看到什么\n\n如果代码能正常工作，你应该看到这些：\n\n```sh\n$ make clean\nrm -f ex1\n$ make ex1\ncc -Wall -g    ex1.c   -o ex1\nex1.c: In function 'main':\nex1.c:3: warning: implicit declaration of function 'puts'\n$\n```\n\n你可以看出来我执行了`make clean`，它告诉make执行我们的`clean`目标。再去看一眼Makefile，之后你会看到在它的下面，我缩进并且输入了一些想要make为我运行的shell命令。你可以在此处输入任意多的命令，所以它是一个非常棒的自动化工具。\n\n> 注\n\n> 如果你修改了`ex1.c`，添加了`#include<stdio>`，输出中的关于`puts`的警告就会消失（这其实应该算作一个错误）。我这里有警告是因为我并没有去掉它。\n\n同时也要注意，即使我们在`Makefile`中并没有提到`ex1`，`make`仍然会知道如何构建它，以及使用我们指定的设置。\n\n## 如何使它崩溃\n\n上面那些已经足够让你起步了，但是让我们以一种特定的方式来破坏make文件，以便你可以看到发生了什么。找到`rm -f ex1`的那一行并去掉缩进（让它左移），之后你可以看到发生了什么。再次运行`make clean`，你就会得到下面的信息：\n\n```sh\n$ make clean\nMakefile:4: *** missing separator.  Stop.\n```\n\n永远记住要缩进，以及如果你得到了像这种奇奇怪怪的错误，应该复查你是否都使用了 TAB 字符，由于一些make的变种十分挑剔。\n\n## 附加题\n\n+ 创建目标`all:ex1`，可以以单个命令`make`构建`ex1`。\n+ 阅读`man make`来了解关于如何执行它的更多信息。\n+ 阅读`man cc`来了解关于`-Wall`和`-g`行为的更多信息。\n+ 在互联网上搜索Makefile文件，看看你是否能改进你的文件。\n+ 在另一个C语言项目中找到`Makefile`文件，并且尝试理解它做了什么。\n"
  },
  {
    "path": "docs/lcthw-zh/ex20.md",
    "content": "# 练习20：Zed的强大的调试宏\n\n> 原文：[Exercise 20: Zed's Awesome Debug Macros](http://c.learncodethehardway.org/book/ex20.html)\n\n> 译者：[飞龙](https://github.com/wizardforcel)\n\n在C中有一个永恒的问题，它伴随了你很长时间，然而在这个练习我打算使用一系列我开发的宏来解决它。到现在为止你都不知道它们的强大之处，所以你必须使用它们，总有一天你会来找我说，“Zed，这些调试宏真是太伟大了，我应该把我的第一个孩子的出生归功于你，因为你治好了我十年的心脏病，并且打消了我数次想要自杀的念头。真是要谢谢你这样一个好人，这里有一百万美元，和Leo Fender设计的Snakehead Telecaster电吉他的原型。”\n\n是的，它们的确很强大。\n\n## C的错误处理问题\n\n几乎每个编程语言中，错误处理都非常难。有些语言尽可能试图避免错误这个概念，而另一些语言发明了复杂了控制结构，比如异常来传递错误状态。当然的错误大多是因为程序员假定错误不会发生，并且这一乐观的思想影响了他们所用和所创造的语言。\n\nC通过返回错误码或设置全局的`errno`值来解决这些问题，并且你需要检查这些值。这种机制可以检查现存的复杂代码中，你执行的东西是否发生错误。当你编写更多的C代码时，你应该按照下列模式：\n\n+ 调用函数。\n+ 如果返回值出现错误（每次都必须检查）。\n+ 清理创建的所有资源。\n+ 打印出所有可能有帮助的错误信息。\n\n这意味着对于每一个函数调用（是的，每个函数）你都可能需要多编写3~4行代码来确保它正常功能。这些还不包括清理你到目前创建的所有垃圾。如果你有10个不同的结构体，3个方式。和一个数据库链接，当你发现错误时你应该写额外的14行。\n\n之前这并不是个问题，因为发生错误时，C程序会像你以前做的那样直接退出。你不需要清理任何东西，因为OS会为你自动去做。然而现在很多C程序需要持续运行数周、数月或者数年，并且需要优雅地处理来自于多种资源的错误。你并不能仅仅让你的服务器在首次运行就退出，你也不能让你写的库使使用它的程序退出。这非常糟糕。\n\n其它语言通过异常来解决这个问题，但是这些问题也会在C中出现（其它语言也一样）。在C中你只能够返回一个值，但是异常是基于栈的返回系统，可以返回任意值。C语言中，尝试在栈上模拟异常非常困难，并且其它库也不会兼容。\n\n## 调试宏\n\n我使用的解决方案是，使用一系列“调试宏”，它们在C中实现了基本的调试和错误处理系统。这个系统非常易于理解，兼容于每个库，并且使C代码更加健壮和简洁。\n\n它通过实现一系列转换来处理错误，任何时候发生了错误，你的函数都会跳到执行清理和返回错误代码的“error:”区域。你可以使用`check`宏来检查错误代码，打印错误信息，然后跳到清理区域。你也可以使用一系列日志函数来打印出有用的调试信息。\n\n我现在会向你展示你目前所见过的，最强大且卓越的代码的全部内容。\n\n```c\n#ifndef __dbg_h__\n#define __dbg_h__\n\n#include <stdio.h>\n#include <errno.h>\n#include <string.h>\n\n#ifdef NDEBUG\n#define debug(M, ...)\n#else\n#define debug(M, ...) fprintf(stderr, \"DEBUG %s:%d: \" M \"\\n\", __FILE__, __LINE__, ##__VA_ARGS__)\n#endif\n\n#define clean_errno() (errno == 0 ? \"None\" : strerror(errno))\n\n#define log_err(M, ...) fprintf(stderr, \"[ERROR] (%s:%d: errno: %s) \" M \"\\n\", __FILE__, __LINE__, clean_errno(), ##__VA_ARGS__)\n\n#define log_warn(M, ...) fprintf(stderr, \"[WARN] (%s:%d: errno: %s) \" M \"\\n\", __FILE__, __LINE__, clean_errno(), ##__VA_ARGS__)\n\n#define log_info(M, ...) fprintf(stderr, \"[INFO] (%s:%d) \" M \"\\n\", __FILE__, __LINE__, ##__VA_ARGS__)\n\n#define check(A, M, ...) if(!(A)) { log_err(M, ##__VA_ARGS__); errno=0; goto error; }\n\n#define sentinel(M, ...)  { log_err(M, ##__VA_ARGS__); errno=0; goto error; }\n\n#define check_mem(A) check((A), \"Out of memory.\")\n\n#define check_debug(A, M, ...) if(!(A)) { debug(M, ##__VA_ARGS__); errno=0; goto error; }\n\n#endif\n```\n\n是的，这就是全部代码了，下面是它每一行所做的事情。\n\ndbg.h:1-2\n\n防止意外包含多次的保护措施，你已经在上一个练习中见过了。\n\ndbg.h:4-6\n\n包含这些宏所需的函数。\n\ndbg.h:8\n\n`#ifdef`的起始，它可以让你重新编译程序来移除所有调试日志信息。\n\ndbg.h:9\n\n如果你定义了`NDEBUG`之后编译，没有任何调试信息会输出。你可以看到`#define debug()`被替换为空（右边没有任何东西）。\n\ndbg.h:10\n\n上面的`#ifdef`所匹配的`#else`。\n\ndbg.h:11\n\n用于替代的`#define debug`，它将任何使用`debug(\"format\", arg1, arg2)`的地方替换成`fprintf`对`stderr`的调用。许多程序员并不知道，但是你的确可以创建与`printf`类似的可变参数宏。许多C编译器（实际上是C预处理器）并不支持它，但是gcc可以做到。这里的魔法是使用`##__VA_ARGS__`，意思是将剩余的所有额外参数放到这里。同时也要注意，使用了`__FILE__`和`__LINE__`来获取当前`fine:line`用于调试信息。这会非常有帮助。\n\ndbg.h:12\n\n`#ifdef`的结尾。\n\ndbg.h:14\n\n`clean_errno`宏用于获取`errno`的安全可读的版本。中间奇怪的语法是“三元运算符”，你会在后面学到它。\n\ndbg.h:16-20\n\n`log_err`，`log_warn`和`log_info`宏用于为最终用户记录信息。它们类似于`debug`但不能被编译。\n\ndbg.h:22\n\n到目前为止最棒的宏。`check`会保证条件`A`为真，否则会记录错误`M`（带着`log_err`的可变参数），之后跳到函数的`error:`区域来执行清理。\n\ndbg.h:24\n\n第二个最棒的宏，`sentinel`可以放在函数的任何不应该执行的地方，它会打印错误信息并且跳到`error:`标签。你可以将它放到`if-statements`或者`switch-statements`的不该被执行的分支中，比如`default`。\n\ndbg.h:26\n\n简写的`check_mem`宏，用于确保指针有效，否则会报告“内存耗尽”的错误。\n\ndbg.h:28\n\n用于替代的`check_debug`宏，它仍然会检查并处理错误，尤其是你并不想报告的普遍错误。它里面使用了`debug`代替`log_err`来报告错误，所以当你定义了`NDEBUG`，它仍然会检查并且发生错误时跳出，但是不会打印消息了。\n\n## 使用dbg.h\n\n下面是一个例子，在一个小的程序中使用了`dbg.h`的所有函数。这实际上并没有做什么事情，只是向你演示了如何使用每个宏。我们将在接下来的所有程序中使用这些宏，所有要确保理解了如何使用它们。\n\n```c\n#include \"dbg.h\"\n#include <stdlib.h>\n#include <stdio.h>\n\n\nvoid test_debug()\n{\n    // notice you don't need the \\n\n    debug(\"I have Brown Hair.\");\n\n    // passing in arguments like printf\n    debug(\"I am %d years old.\", 37);\n}\n\nvoid test_log_err()\n{\n    log_err(\"I believe everything is broken.\");\n    log_err(\"There are %d problems in %s.\", 0, \"space\");\n}\n\nvoid test_log_warn()\n{\n    log_warn(\"You can safely ignore this.\");\n    log_warn(\"Maybe consider looking at: %s.\", \"/etc/passwd\");\n}\n\nvoid test_log_info()\n{\n    log_info(\"Well I did something mundane.\");\n    log_info(\"It happened %f times today.\", 1.3f);\n}\n\nint test_check(char *file_name)\n{\n    FILE *input = NULL;\n    char *block = NULL;\n\n    block = malloc(100);\n    check_mem(block); // should work\n\n    input = fopen(file_name,\"r\");\n    check(input, \"Failed to open %s.\", file_name);\n\n    free(block);\n    fclose(input);\n    return 0;\n\nerror:\n    if(block) free(block);\n    if(input) fclose(input);\n    return -1;\n}\n\nint test_sentinel(int code)\n{\n    char *temp = malloc(100);\n    check_mem(temp);\n\n    switch(code) {\n        case 1:\n            log_info(\"It worked.\");\n            break;\n        default:\n            sentinel(\"I shouldn't run.\");\n    }\n\n    free(temp);\n    return 0;\n\nerror:\n    if(temp) free(temp);\n    return -1;\n}\n\nint test_check_mem()\n{\n    char *test = NULL;\n    check_mem(test);\n\n    free(test);\n    return 1;\n\nerror:\n    return -1;\n}\n\nint test_check_debug()\n{\n    int i = 0;\n    check_debug(i != 0, \"Oops, I was 0.\");\n\n    return 0;\nerror:\n    return -1;\n}\n\nint main(int argc, char *argv[])\n{\n    check(argc == 2, \"Need an argument.\");\n\n    test_debug();\n    test_log_err();\n    test_log_warn();\n    test_log_info();\n\n    check(test_check(\"ex20.c\") == 0, \"failed with ex20.c\");\n    check(test_check(argv[1]) == -1, \"failed with argv\");\n    check(test_sentinel(1) == 0, \"test_sentinel failed.\");\n    check(test_sentinel(100) == -1, \"test_sentinel failed.\");\n    check(test_check_mem() == -1, \"test_check_mem failed.\");\n    check(test_check_debug() == -1, \"test_check_debug failed.\");\n\n    return 0;\n\nerror:\n    return 1;\n}\n```\n\n要注意`check`是如何使用的，并且当它为`false`时会跳到`error:`标签来执行清理。这一行读作“检查A是否为真，不为真就打印M并跳出”。\n\n## 你会看到什么\n\n当你执行这段代码并且向第一个参数提供一些东西，你会看到：\n\n```sh\n$ make ex20\ncc -Wall -g -DNDEBUG    ex20.c   -o ex20\n$ ./ex20 test\n[ERROR] (ex20.c:16: errno: None) I believe everything is broken.\n[ERROR] (ex20.c:17: errno: None) There are 0 problems in space.\n[WARN] (ex20.c:22: errno: None) You can safely ignore this.\n[WARN] (ex20.c:23: errno: None) Maybe consider looking at: /etc/passwd.\n[INFO] (ex20.c:28) Well I did something mundane.\n[INFO] (ex20.c:29) It happened 1.300000 times today.\n[ERROR] (ex20.c:38: errno: No such file or directory) Failed to open test.\n[INFO] (ex20.c:57) It worked.\n[ERROR] (ex20.c:60: errno: None) I shouldn't run.\n[ERROR] (ex20.c:74: errno: None) Out of memory.\n```\n\n看到`check`失败之后，它是如何打印具体的行号了吗？这会为接下来的调试工作节省时间。同时也观察`errno`被设置时它如何打印错误信息。同样，这也可以节省你调试的时间。\n\n## C预处理器如何扩展宏\n\n现在我会向你简单介绍一些预处理器的工作原理，让你知道这些宏是如何工作的。我会拆分`dbg.h`中阿最复杂的宏并且让你运行`cpp`来让你观察它实际上是如何工作的。\n\n假设我有一个函数叫做`dosomething()`，执行成功是返回0，发生错误时返回-1。每次我调用`dosomething`的时候，我都要检查错误码，所以我将代码写成这样：\n\n```c\nint rc = dosomething();\n\nif(rc != 0) {\n    fprintf(stderr, \"There was an error: %s\\n\", strerror());\n    goto error;\n}\n```\n\n我想使用预处理器做的是，将这个`if`语句封装为更可读并且便于记忆的一行代码。于是可以使用这个`check`来执行`dbg.h`中的宏所做的事情：\n\n```c\nint rc = dosomething();\ncheck(rc == 0, \"There was an error.\");\n```\n\n这样更加简洁，并且恰好解释了所做的事情：检查函数是否正常工作，如果没有就报告错误。我们需要一些特别的预处理器“技巧”来完成它，这些技巧使预处理器作为代码生成工具更加易用。再次看看`check`和`log_err`宏：\n\n```c\n#define log_err(M, ...) fprintf(stderr, \"[ERROR] (%s:%d: errno: %s) \" M \"\\n\", __FILE__, __LINE__, clean_errno(), ##__VA_ARGS__)\n#define check(A, M, ...) if(!(A)) { log_err(M, ##__VA_ARGS__); errno=0; goto error; }\n```\n\n第一个宏，`log_err`更简单一些，只是将它自己替换为`fprintf`对`stderr`的调用。这个宏唯一的技巧性部分就是在`log_err(M, ...)`的定义中使用`...`。它所做的是让你向宏传入可变参数，从而传入`fprintf`需要接收的参数。它们是如何注入`fprintf`的呢？观察末尾的`##__VA_ARGS__`，它告诉预处理器将`...`所在位置的参数注入到`fprintf`调用的相应位置。于是你可以像这样调用了：\n\n```c\nlog_err(\"Age: %d, name: %s\", age, name);\n```\n\n`age, name`参数就是`...`所定义的部分，这些参数会被注入到`fprintf`中，输出会变成：\n\n```c\nfprintf(stderr, \"[ERROR] (%s:%d: errno: %s) Age %d: name %d\\n\",\n    __FILE__, __LINE__, clean_errno(), age, name);\n```\n\n看到末尾的`age, name`了吗？这就是`...`和`##__VA_ARGS__`的工作机制，在调用其它变参宏（或者函数）的时候它会起作用。观察`check`宏调用`log_err`的方式，它也是用了`...`和`##__VA_ARGS__`。这就是传递整个`printf`风格的格式字符串给`check`的途径，它之后会传给`log_err`，二者的机制都像`printf`一样。\n\n下一步是学习`check`如何为错误检查构造`if`语句，如果我们剖析`log_err`的用法，我们会得到：\n\n```c\nif(!(A)) { errno=0; goto error; }\n```\n\n它的意思是，如果`A`为假，则重置`errno`并且调用`error`标签。`check`宏会被上述`if`语句·替换，所以如果我们手动扩展`check(rc == 0, \"There was an error.\")`，我们会得到：\n\n```c\nif(!(rc == 0)) {\n    log_err(\"There was an error.\");\n    errno=0;\n    goto error;\n}\n```\n\n在这两个宏的展开过程中，你应该了解了预处理器会将宏替换为它的定义的扩展版本，并且递归地来执行这个步骤，扩展宏定义中的宏。预处理器是个递归的模板系统，就像我之前提到的那样。它的强大来源于使用参数化的代码来生成整个代码块，这使它成为便利的代码生成工具。\n\n下面只剩一个问题了：为什么不像`die`一样使用函数呢？原因是需要在错误处理时使用`file:line`的数值和`goto`操作。如果你在函数在内部执行这些，你不会得到错误真正出现位置的行号，并且`goto`的实现也相当麻烦。\n\n另一个原因是，如果你编写原始的`if`语句，它看起来就像是你代码中的其它的`if`语句，所以它看起来并不像一个错误检查。通过将`if`语句包装成`check`宏，就会使这一错误检查的逻辑更清晰，而不是主控制流的一部分。\n\n最后，C预处理器提供了条件编译部分代码的功能，所以你可以编写只在构建程序的开发或调试版本时需要的代码。你可以看到这在`dbg.h`中已经用到了，`debug`宏的主体部分只被编译器用到。如果没有这个功能，你需要多出一个`if`语句来检查是否为“调试模式”，也浪费了CPU资源来进行没有必要的检查。\n\n## 附加题\n\n+ 将`#define NDEBUG`放在文件顶端来消除所有调试信息。\n+ 撤销上面添加的一行，并在`MakeFile`顶端将`-D NDEBUG`添加到`CFLAGS`，之后重新编译来达到同样效果。\n+ 修改日志宏，使之包含函数名称和`file:line`。\n"
  },
  {
    "path": "docs/lcthw-zh/ex21.md",
    "content": "# 练习21：高级数据类型和控制结构\n\n> 原文：[Exercise 21: Advanced Data Types And Flow Control](http://c.learncodethehardway.org/book/ex21.html)\n\n> 译者：[飞龙](https://github.com/wizardforcel)\n\n这个练习是C语言中所有可用的数据类型和控制结构的摘要。它也可以作为一份参考在补完你的知识，并且不含有任何代码。我会通过创建教学卡片的方式，让你记住一些信息，所以你会在脑子里记住所有重要的概念。\n\n这个练习非常有用，你应该花至少一周的时间来巩固内容并且补全这里所没有的元素。你应学出每个元素是什么意思，以及编写程序来验证你得出的结论。\n\n## 可用的数据类型\n\n`int`\n\n储存普通的整数，默认为32位大小。\n\n> 译者注：`int`在32或64位环境下为32位，但它不应该被看作平台无关的。如果需要用到平台无关的定长整数，请使用`int(n)_t`。\n\n`double`\n\n储存稍大的浮点数。\n\n`float`\n\n储存稍小的浮点数。\n\n`char`\n\n储存单字节字符。\n\n`void`\n\n表示“无类型”，用于声明不返回任何东西的函数，或者所指类型不明的指针，例如`void *thing`。\n\n`enum`\n\n枚举类型，类似于整数，也可转换为整数，但是通过符号化的名称访问或设置。当`switch`语句中没有覆盖到所有枚举的元素时，一些编译器会发出警告。\n\n## 类型修饰符\n\n`unsigned`\n\n修改类型，使它不包含任何负数，同时上界变高。\n\n`signed`\n\n可以储存正数和负数，但是上界会变为（大约）一半，下界变为和上界（大约）等长。\n\n> 译者注：符号修饰符只对`char`和`*** int`有效。`*** int`默认为`signed`，而`char`根据具体实现，可以默认为`signed`，也可以为`unsigned`。\n\n`long`\n\n对该类型使用较大的空间，使它能存下更大的数，通常使当前大小加倍。\n\n`short`\n\n对该类型使用较小的空间，使它储存能力变小，但是占据空间也变成一半。\n\n## 类型限定符\n\n`const`\n\n表示变量在初始化后不能改变。\n\n`volatile`\n\n表示会做最坏的打算，编译器不会对它做任何优化。通常仅在对变量做一些奇怪的事情时，才会用到它。\n\n`register`\n\n强制让编译器将这个变量保存在寄存器中，并且也可以无视它。目前的编译器更善于处理在哪里存放变量，所以应该只在确定这样会提升性能时使用它。\n\n## 类型转换\n\nC使用了一种“阶梯形类型提升”的机制，它会观察运算符两边的变量，并且在运算之前将较小边的变量转换为较大边。这个过程按照如下顺序：\n\n+ long double\n+ double\n+ float\n+ long long\n+ long\n+ int (short, char)\n\n> 译者注：`short`和`char`会在运算之前转换成`int`。同种类型的`unsigned`和`signed`运算，`signed`保持字节不变转换成`unsigned`。\n\n## 类型大小\n\n\n`stdint.h`为定长的整数类型定义了一些`typedef`，同时也有一些用于这些类型的宏。这比老的`limits.h`更加易于使用，因为它是不变的。这些类型如下：\n\n`int8_t`\n\n8位符号整数。\n\n`uint8_t`\n\n8位无符号整数。\n\n`int16_t`\n\n16位符号整数。\n\n`uint16_t`\n\n16位无符号整数。\n\n`int32_t`\n\n32位符号整数。\n\n`uint32_t`\n\n32位无符号整数。\n\n`int64_t`\n\n64位符号整数。\n\n`uint64_t`\n\n64位无符号整数。\n\n> 译者注：当用于对类型大小有要求的特定平台时，可以使用这些类型。如果你怕麻烦，不想处理平台相关类型的今后潜在的扩展的话，也可以使用这些类型。\n\n下面的模式串为`(u)int(BITS)_t`，其中前面的`u`代表`unsigned`，`BITS`是所占位数的大小。这些模式串返回了这些类型的最大（或最小）值。\n\n`INT(N)_MAX`\n\n`N`位符号整数的最大正值，例如`INT16_MAX`。\n\n`INT(N)_MIN`\n\n`N`位符号整数的最小负值。\n\n`UINT(N)_MAX`\n\n`N`位无符号整数的最大正值。为什么不定义其最小值，是因为最小值是0，不可能出现负值。\n\n> 警告\n\n> 要注意，不要从字面上在任何头文件中去找`INT(N)_MAX`的定义。这里的`N`应该为特定整数，比如8、16、32、64，甚至可能是128。我在这个练习中使用了这个记法，就不需要显式写出每一个不同的组合了。\n\n在`stdint.h`中，对于`size_t`类型和足够存放指针的整数也有一些宏定义，以及其它便捷类型的宏定义。编译器至少要保证它们为某一大小，并允许它们为更大的大小。\n\n`int_least(N)_t`\n\n至少`N`位的整数。\n\n`uint_least(N)_t`\n\n至少`N`位的无符号整数。\n\n`INT_LEAST(N)_MAX`\n\n`int_least(N)_t`类型的最大值。\n\n`INT_LEAST(N)_MIN`\n\n`int_least(N)_t`类型的最小值。\n\n`UINT_LEAST(N)_MAX`\n\n`uint_least(N)_t`的最大值。\n\n`int_fast(N)_t`\n\n与`int_least(N)_t`相似，但是是至少`N`位的“最快”整数。\n\n`uint_fast(N)_t`\n\n至少`N`位的“最快”无符号整数。\n\n`INT_FAST(N)_MAX`\n\n`int_fast(N)_t`的最大值。\n\n`INT_FAST(N)_MIN`\n\n`int_fast(N)_t`的最小值。\n\n`UINT_FAST(N)_MAX`\n\n`uint_fast(N)_t`的最大值。\n\n`intptr_t`\n\n足够存放指针的符号整数。\n\n`uintptr_t`\n\n足够存放指针的无符号整数。\n\n`INTPTR_MAX`\n\n`intptr_t`的最大值。\n\n`INTPTR_MIN`\n\n`intptr_t`的最小值。\n\n`UINTPTR_MAX`\n\n`uintptr_t`的最大值。\n\n`intmax_t`\n\n系统中可能的最大尺寸的整数类型。\n\n`uintmax_t`\n\n系统中可能的最大尺寸的无符号整数类型。\n\n`INTMAX_MAX`\n\n`intmax_t`的最大值。\n\n`INTMAX_MIN`\n\n`intmax_t`的最小值。\n\n`UINTMAX_MAX`\n\n`uintmax_t`的最大值。\n\n`PTRDIFF_MIN`\n\n`ptrdiff_t`的最小值。\n\n`PTRDIFF_MAX`\n\n`ptrdiff_t`的最大值。\n\n`SIZE_MAX`\n\n`size_t`的最大值。\n\n## 可用的运算符\n\n这是一个全面的列表，关于你可以在C中使用的全部运算符。这个列表中我会标明一些东西：\n\n二元\n\n该运算符有左右两个操作数：`X + Y`。\n\n一元\n\n该运算符作用于操作数本身`-X`。\n\n前缀\n\n该运算符出现在操作数之前：`++X`。\n\n后缀\n\n通常和前缀版本相似，但是出现在操作数之后，并且意义不同：`X++`。\n\n三元\n\n只有一个三元运算符，意思是“三个操作数”：`X ? Y : Z`。\n\n## 算数运算符\n\n下面是基本的算数运算符，我将函数调用`()`放入其中因为它更接近“算数”运算。\n\n`()`\n\n函数调用。\n\n二元 `*`\n\n乘法。\n\n`/`\n\n除法。\n\n二元 `+`\n\n加法。\n\n一元 `+`\n\n无变化。\n\n后缀 `++`\n\n读取变量然后自增。\n\n前缀 `++`\n\n自增变量然后读取。\n\n后缀 `--`\n\n读取变量然后自减。\n\n前缀 `--`\n\n自减变量然后读取。\n\n二元 `-`\n\n减法。\n\n一元 `-`\n\n取反，可用于表示负数。\n\n## 数据运算\n\n它们用于以不同方式和形式访问数据。\n\n`->`\n\n结构体指针的成员访问。一元`*`和`.`运算符的复合。\n\n`.`\n\n结构体值的成员访问。\n\n`[]`\n\n取数组下标。二元`+`和一元`*`运算符的复合。\n\n`sizeof`\n\n取类型或变量大小。\n\n一元 `&`\n\n取地址。\n\n一元 `*`\n\n取值（提领地址）。\n\n## 逻辑运算符\n\n它们用于测试变量的等性和不等性。\n\n`!=`\n\n不等于。\n\n`<`\n\n小于。\n\n`<=`\n\n小于等于。\n\n`==`\n\n等于（并不是赋值）。\n\n`>`\n\n大于。\n\n`>=`\n\n大于等于。\n\n## 位运算符\n\n它们更加高级，用于修改整数的原始位。\n\n二元 `&`\n\n位与。\n\n`<<`\n\n左移。\n\n`>>`\n\n右移。\n\n`^`\n\n位异或。\n\n`|`\n\n位或。\n\n`~`\n\n取补（翻转所有位）。\n\n## 布尔运算符。\n\n用于真值测试，仔细学习三元运算符，它非常有用。\n\n`!`\n\n取非。\n\n`&&`\n\n与。\n\n`||`\n\n或。\n\n`?:`\n\n三元真值测试，`X ? Y : Z`读作“若X则Y否则Z”。\n\n## 赋值运算符\n\n复合赋值运算符在赋值同时执行运算。大多数上面的运算符都可以组成复合赋值运算符。\n\n`=`\n\n赋值。\n\n`%=`\n\n取余赋值。\n\n`&=`\n\n位与赋值。\n\n`*=`\n\n乘法赋值。\n\n\n`+=`\n\n加法赋值。\n\n`-=`\n\n减法赋值。\n\n`/=`\n\n除法赋值。\n\n`<<=`\n\n左移赋值。\n\n`>>=`\n\n右移赋值。\n\n`^=`\n\n位异或赋值。\n\n`|=`\n\n位或赋值。\n\n## 可用的控制结构\n\n\n下面是一些你没有接触过的控制结构：\n\n`do-while`\n\n`do { ... } while(X);`首先执行花括号中的代码，之后再跳出前测试`X`表达式。\n\n`break`\n\n放在循环中用于跳出循环。\n\n`continue`\n\n跳到循环尾。\n\n`goto`\n\n跳到你已经放置`label`的位置，你已经在`dbg.h`中看到它了，用于跳到`error`标签。\n\n## 附加题\n\n+ 阅读`stdint.h`或它的描述，写出所有可能出现的大小定义。\n+ 查询本练习的每一项，写出它在代码中的作用。上网浏览资料来研究它如何正确使用。\n+ 将这些信息做成教学卡片，每天看上15分钟来记住它们。\n+ 创建一个程序，打印出每个类型的示例，并验证你的研究结果是否正确。\n"
  },
  {
    "path": "docs/lcthw-zh/ex22.md",
    "content": "# 练习22：栈、作用域和全局\n\n> 原文：[Exercise 22: The Stack, Scope, And Globals](http://c.learncodethehardway.org/book/ex22.html)\n\n> 译者：[飞龙](https://github.com/wizardforcel)\n\n许多人在开始编程时，对“作用域”这个概念都不是很清楚。起初它来源于系统栈的使用方式（在之前提到过一些），以及它用于临时变量储存的方式。这个练习中，我们会通过学习栈数据结构如何工作来了解作用域，然后再来看看现代C语言处理作用域的方式。\n\n这个练习的真正目的是了解一些比较麻烦的东西在C中如何存储。当一个人没有掌握作用域的概念时，它几乎也不能理解变量在哪里被创建，存在以及销毁。一旦你知道了这些，作用域的概念会变得易于理解。\n\n这个练习需要如下三个文件：\n\n`ex22.h`\n\n用于创建一些外部变量和一些函数的头文件。\n\n`ex22.c`\n\n它并不像通常一样，是包含`main`的源文件，而是含有一些`ex22.h`中声明的函数和变量，并且会变成`ex22.o`。\n\n`ex22_main.c`\n\n`main`函数实际所在的文件，它会包含另外两个文件，并演示了它们包含的东西以及其它作用域概念。\n\n## ex22.h 和 ex22.c\n\n你的第一步是创建你自己的`ex22.h`头文件，其中定义了所需的函数和“导出”变量。\n\n```c\n#ifndef _ex22_h\n#define _ex22_h\n\n// makes THE_SIZE in ex22.c available to other .c files\nextern int THE_SIZE;\n\n// gets and sets an internal static variable in ex22.c\nint get_age();\nvoid set_age(int age);\n\n// updates a static variable that's inside update_ratio\ndouble update_ratio(double ratio);\n\nvoid print_size();\n\n#endif\n```\n\n最重要的事情是`extern int THE_SIZE`的用法，我将会在你创建完`ex22.c`之后解释它：\n\n```c\n#include <stdio.h>\n#include \"ex22.h\"\n#include \"dbg.h\"\n\nint THE_SIZE = 1000;\n\nstatic int THE_AGE = 37;\n\nint get_age()\n{\n    return THE_AGE;\n}\n\nvoid set_age(int age)\n{\n    THE_AGE = age;\n}\n\n\ndouble update_ratio(double new_ratio)\n{\n    static double ratio = 1.0;\n\n    double old_ratio = ratio;\n    ratio = new_ratio;\n\n    return old_ratio;\n}\n\nvoid print_size()\n{\n    log_info(\"I think size is: %d\", THE_SIZE);\n}\n```\n\n这两个文件引入了一些新的变量储存方式：\n\n`extern`\n\n这个关键词告诉编译器“这个变量已存在，但是他在别的‘外部区域’里”。通常它的意思是一个`.c`文件要用到另一个`.c`文件中定义的变量。这种情况下，我们可以说`ex22.c`中的`THE_SIZE`变量能被`ex22_main.c`访问到。\n\n`static`（文件）\n\n这个关键词某种意义上是`extern`的反义词，意思是这个变量只能在当前的`.c`文件中使用，程序的其它部分不可访问。要记住文件级别的`static`（比如这里的`THE_AGE`）和其它位置不同。\n\n`static`（函数）\n\n如果你使用`static`在函数中声明变量，它和文件中的`static`定义类似，但是只能够在该函数中访问。它是一种创建某个函数的持续状态的方法，但事实上它很少用于现代的C语言，因为它们很难和线程一起使用。\n\n在上面的两个文件中，你需要理解如下几个变量和函数：\n\n`THE_SIZE`\n\n这个你使用`extern`声明的变量将会在`ex22_main.c`中用到。\n\n`get_age`和`set_age`\n\n它们用于操作静态变量`THE_AGE`，并通过函数将其暴露给程序的其它部分。你不能够直接访问到`THE_AGE`，但是这些函数可以。\n\n`update_ratio`\n\n它生成新的`ratio`值并返回旧的值。它使用了函数级的静态变量`ratio`来跟踪`ratio`当前的值。\n\n`print_size`\n\n打印出`ex22.c`所认为的`THE_SIZE`的当前值。\n\n## ex22_main.c\n\n一旦你写完了上面那些文件，你可以接着编程`main`函数，它会使用所有上面的文件并且演示了一些更多的作用域转换：\n\n```c\n#include \"ex22.h\"\n#include \"dbg.h\"\n\nconst char *MY_NAME = \"Zed A. Shaw\";\n\nvoid scope_demo(int count)\n{\n    log_info(\"count is: %d\", count);\n\n    if(count > 10) {\n        int count = 100;  // BAD! BUGS!\n\n        log_info(\"count in this scope is %d\", count);\n    }\n\n    log_info(\"count is at exit: %d\", count);\n\n    count = 3000;\n\n    log_info(\"count after assign: %d\", count);\n}\n\nint main(int argc, char *argv[])\n{\n    // test out THE_AGE accessors\n    log_info(\"My name: %s, age: %d\", MY_NAME, get_age());\n\n    set_age(100);\n\n    log_info(\"My age is now: %d\", get_age());\n\n    // test out THE_SIZE extern\n    log_info(\"THE_SIZE is: %d\", THE_SIZE);\n    print_size();\n\n    THE_SIZE = 9;\n\n    log_info(\"THE SIZE is now: %d\", THE_SIZE);\n    print_size();\n\n    // test the ratio function static\n    log_info(\"Ratio at first: %f\", update_ratio(2.0));\n    log_info(\"Ratio again: %f\", update_ratio(10.0));\n    log_info(\"Ratio once more: %f\", update_ratio(300.0));\n\n    // test the scope demo\n    int count = 4;\n    scope_demo(count);\n    scope_demo(count * 20);\n\n    log_info(\"count after calling scope_demo: %d\", count);\n\n    return 0;\n}\n```\n\n我会把这个文件逐行拆分，你应该能够找到我提到的每个变量在哪里定义。\n\nex22_main.c:4\n\n使用了`const`来创建常量，它可用于替代`define`来创建常量。\n\nex22_main.c:6\n\n一个简单的函数，演示了函数中更多的作用域问题。\n\nex22_main.c:8\n\n在函数顶端打印出`count`的值。\n\nex22_main.c:10\n\n`if`语句会开启一个新的作用域区块，并且在其中创建了另一个`count`变量。这个版本的`count`变量是一个全新的变量。`if`语句就好像开启了一个新的“迷你函数”。\n\nex22_main.c:11\n\n`count`对于当前区块是局部变量，实际上不同于函数参数列表中的参数。\n\nex22_main.c:13\n\n将它打印出来，所以你可以在这里看到100，并不是传给`scope_demo`的参数。\n\nex22_main.c:16\n\n这里是最难懂得部分。你在两部分都有`count`变量，一个数函数参数，另一个是`if`语句中。`if`语句创建了新的代码块，所以11行的`count`并不影响同名的参数。这一行将其打印出来，你会看到它打印了参数的值而不是100。\n\nex22_main.c:18-20\n\n之后我将`count`参数设为3000并且打印出来，这里演示了你也可以修改函数参数的值，但并不会影响变量的调用者版本。\n\n确保你浏览了整个函数，但是不要认为你已经十分了解作用娱乐。如果你在一个代码块中（比如`if`或`while`语句）创建了一些变量，这些变量是全新的变量，并且只在这个代码块中存在。这是至关重要的东西，也是许多bug的来源。我要强调你应该在这里花一些时间。\n\n`ex22_main.c`的剩余部分通过操作和打印变量演示了它们的全部。\n\nex22_main.c:26\n\n打印出`MY_NAME`的当前值，并且使用`get_age`读写器从`ex22.c`获取`THE_AGE`。\n\n\nex22_main.c:27-30\n\n使用了`ex22.c`中的`set_age`来修改并打印`THE_AGE`。\n\nex22_main.c:33-39\n\n接下来我对`ex22.c`中的`THE_SIZE`做了相同的事情，但这一次我直接访问了它，并且同时演示了它实际上在那个文件中已经修改了，还使用`print_size`打印了它。\n\nex22_main.c:42-44\n\n展示了`update_ratio`中的`ratio`在两次函数调用中如何保持了它的值。\n\nex22_main.c:46-51\n\n最后运行`scope_demo`，你可以在实例中观察到作用域。要注意到的关键点是，`count`局部变量在调用后保持不变。你将它像一个变量一样传入函数，它一定不会发生改变。要想达到目的你需要我们的老朋友指针。如果你将指向`count`的指针传入函数，那么函数就会持有它的地址并且能够改变它。\n\n上面解释了这些文件中所发生的事情，但是你应该跟踪它们，并且确保在你学习的过程中明白了每个变量都在什么位置。\n\n## 你会看到什么\n\n这次我想让你手动构建这两个文件，而不是使用你的`Makefile`。于是你可以看到它们实际上如何被编译器放到一起。这是你应该做的事情，并且你应该看到如下输出：\n\n```sh\n$ cc -Wall -g -DNDEBUG   -c -o ex22.o ex22.c\n$ cc -Wall -g -DNDEBUG    ex22_main.c ex22.o   -o ex22_main\n$ ./ex22_main\n[INFO] (ex22_main.c:26) My name: Zed A. Shaw, age: 37\n[INFO] (ex22_main.c:30) My age is now: 100\n[INFO] (ex22_main.c:33) THE_SIZE is: 1000\n[INFO] (ex22.c:32) I think size is: 1000\n[INFO] (ex22_main.c:38) THE SIZE is now: 9\n[INFO] (ex22.c:32) I think size is: 9\n[INFO] (ex22_main.c:42) Ratio at first: 1.000000\n[INFO] (ex22_main.c:43) Ratio again: 2.000000\n[INFO] (ex22_main.c:44) Ratio once more: 10.000000\n[INFO] (ex22_main.c:8) count is: 4\n[INFO] (ex22_main.c:16) count is at exit: 4\n[INFO] (ex22_main.c:20) count after assign: 3000\n[INFO] (ex22_main.c:8) count is: 80\n[INFO] (ex22_main.c:13) count in this scope is 100\n[INFO] (ex22_main.c:16) count is at exit: 80\n[INFO] (ex22_main.c:20) count after assign: 3000\n[INFO] (ex22_main.c:51) count after calling scope_demo: 4\n```\n\n确保你跟踪了每个变量是如何改变的，并且将其匹配到所输出的那一行。我使用了`dbg.h`的`log_info`来让你获得每个变量打印的具体行号，并且在文件中找到它用于跟踪。\n\n## 作用域、栈和Bug\n\n如果你正确完成了这个练习，你会看到有很多不同方式在C代码中放置变量。你可以使用`extern`或者访问类似`get_age`的函数来创建全局。你也可以在任何代码块中创建新的变量，它们在退出代码块之前会拥有自己的值，并且屏蔽掉外部的变量。你也可以响函数传递一个值并且修改它，但是调用者的变量版本不会发生改变。\n\n需要理解的最重要的事情是，这些都可以造成bug。C中在你机器中许多位置放置和访问变量的能力会让你对它们所在的位置感到困扰。如果你不知道它们的位置，你就可能不能适当地管理它们。\n\n下面是一些编程C代码时需要遵循的规则，可以让你避免与栈相关的bug：\n\n+ 不要隐藏某个变量，就像上面`scope_demo`中对`count`所做的一样。这可能会产生一些隐蔽的bug，你认为你改变了某个变量但实际上没有。\n+ 避免过多的全局变量，尤其是跨越多个文件。如果必须的话，要使用读写器函数，就像`get_age`。这并不适用于常量，因为它们是只读的。我是说对于`THE_SIZE`这种变量，如果你希望别人能够修改它，就应该使用读写器函数。\n+ 在你不清楚的情况下，应该把它放在堆上。不要依赖于栈的语义，或者指定区域，而是要直接使用`malloc`创建它。\n+ 不要使用函数级的静态变量，就像`update_ratio`。它们并不有用，而且当你想要使你的代码运行在多线程环境时，会有很大的隐患。对于良好的全局变量，它们也非常难于寻找。\n+ 避免复用函数参数，因为你搞不清楚仅仅想要复用它还是希望修改它的调用者版本。\n\n## 如何使它崩溃\n\n对于这个练习，崩溃这个程序涉及到尝试访问或修改你不能访问的东西。\n\n+ 试着从`ex22_main.c`直接访问`ex22.c`中的你不能访问变量。例如，你能不能获取`update_ratio`中的`ratio`？如果你用一个指针指向它会发生什么？\n+ 移除`ex22.h`的`extern`声明，来观察会得到什么错误或警告。\n+ 对不同变量添加`static`或者`const`限定符，之后尝试修改它们。\n\n## 附加题\n\n+ 研究“值传递”和“引用传递”的差异，并且为二者编写示例。（译者注：C中没有引用传递，你可以搜索“指针传递”。）\n+ 使用指针来访问原本不能访问的变量。\n+ 使用`Valgrind`来观察错误的访问是什么样子。\n+ 编写一个递归调用并导致栈溢出的函数。如果不知道递归函数是什么的话，试着在`scope_demo`底部调用`scope_demo`本身，会形成一种循环。\n+ 重新编写`Makefile`使之能够构建这些文件。\n"
  },
  {
    "path": "docs/lcthw-zh/ex23.md",
    "content": "# 练习23：认识达夫设备\n\n> 原文：[Exercise 23: Meet Duff's Device](http://c.learncodethehardway.org/book/ex23.html)\n\n> 译者：[飞龙](https://github.com/wizardforcel)\n\n这个练习是一个脑筋急转弯，我会向你介绍最著名的C语言黑魔法之一，叫做“达夫设备”，以“发明者”汤姆·达夫的名字命名。这一强大（或邪恶？）的代码中，几乎你学过的任何东西都被包装在一个小的结构中。弄清它的工作机制也是一个好玩的谜题。\n\n> 注\n\n> C的一部分乐趣来源于这种神奇的黑魔法，但这也是使C难以使用的地方。你最好能够了解这些技巧，因为他会带给你关于C语言和你计算机的深入理解。但是，你应该永远都不要使用它们，并总是追求简单易读的代码。\n\n达夫设备由汤姆·达夫“发现”（或创造），它是一个C编译器的小技巧，本来不应该能够正常工作。我并不想告诉你做了什么，因为这是一个谜题，等着你来思考并尝试解决。你需要运行这段代码，之后尝试弄清它做了什么，以及为什么可以这样做。\n\n```c\n#include <stdio.h>\n#include <string.h>\n#include \"dbg.h\"\n\n\nint normal_copy(char *from, char *to, int count)\n{\n    int i = 0;\n\n    for(i = 0; i < count; i++) {\n        to[i] = from[i];\n    }\n\n    return i;\n}\n\nint duffs_device(char *from, char *to, int count)\n{\n    {\n        int n = (count + 7) / 8;\n\n        switch(count % 8) {\n            case 0: do { *to++ = *from++;\n                        case 7: *to++ = *from++;\n                        case 6: *to++ = *from++;\n                        case 5: *to++ = *from++;\n                        case 4: *to++ = *from++;\n                        case 3: *to++ = *from++;\n                        case 2: *to++ = *from++;\n                        case 1: *to++ = *from++;\n                    } while(--n > 0);\n        }\n    }\n\n    return count;\n}\n\nint zeds_device(char *from, char *to, int count)\n{\n    {\n        int n = (count + 7) / 8;\n\n        switch(count % 8) {\n            case 0:\n            again: *to++ = *from++;\n\n            case 7: *to++ = *from++;\n            case 6: *to++ = *from++;\n            case 5: *to++ = *from++;\n            case 4: *to++ = *from++;\n            case 3: *to++ = *from++;\n            case 2: *to++ = *from++;\n            case 1: *to++ = *from++;\n                    if(--n > 0) goto again;\n        }\n    }\n\n    return count;\n}\n\nint valid_copy(char *data, int count, char expects)\n{\n    int i = 0;\n    for(i = 0; i < count; i++) {\n        if(data[i] != expects) {\n            log_err(\"[%d] %c != %c\", i, data[i], expects);\n            return 0;\n        }\n    }\n\n    return 1;\n}\n\n\nint main(int argc, char *argv[])\n{\n    char from[1000] = {'a'};\n    char to[1000] = {'c'};\n    int rc = 0;\n\n    // setup the from to have some stuff\n    memset(from, 'x', 1000);\n    // set it to a failure mode\n    memset(to, 'y', 1000);\n    check(valid_copy(to, 1000, 'y'), \"Not initialized right.\");\n\n    // use normal copy to\n    rc = normal_copy(from, to, 1000);\n    check(rc == 1000, \"Normal copy failed: %d\", rc);\n    check(valid_copy(to, 1000, 'x'), \"Normal copy failed.\");\n\n    // reset\n    memset(to, 'y', 1000);\n\n    // duffs version\n    rc = duffs_device(from, to, 1000);\n    check(rc == 1000, \"Duff's device failed: %d\", rc);\n    check(valid_copy(to, 1000, 'x'), \"Duff's device failed copy.\");\n\n    // reset\n    memset(to, 'y', 1000);\n\n    // my version\n    rc = zeds_device(from, to, 1000);\n    check(rc == 1000, \"Zed's device failed: %d\", rc);\n    check(valid_copy(to, 1000, 'x'), \"Zed's device failed copy.\");\n\n    return 0;\nerror:\n    return 1;\n}\n```\n\n这段代码中我编写了三个版本的复制函数：\n\n`normal_copy`\n\n使用普通的`for`循环来将字符从一个数组复制到另一个。\n\n`duffs_device`\n\n这个就是称为“达夫设备”的脑筋急转弯，以汤姆·达夫的名字命名。这段有趣的邪恶代码应归咎于他。\n\n`zeds_device`\n\n“达夫设备”的另一个版本，其中使用了`goto`来让你发现一些线索，关于`duffs_device`中奇怪的`do-while`做了什么。\n\n在往下学习之前仔细了解这三个函数，并试着自己解释代码都做了什么。\n\n## 你会看到什么\n\n这个程序没有任何输出，它只会执行并退出。你应当在`Valgrind`下运行它并确保没有任何错误。\n\n## 解决谜题\n\n首先需要了解的一件事，就是C对于它的一些语法是弱检查的。这就是你可以将`do-while`的一部分放入`switch`语句的一部分的原因，并且在其它地方的另一部分还可以正常工作。如果你观察带有`goto again`的我的版本，它实际上更清晰地解释了工作原理，但要确保你理解了这一部分是如何工作的。\n\n第二件事是`switch`语句的默认贯穿机制可以让你跳到指定的`case`，并且继续运行直到`switch`结束。\n\n最后的线索是`count % 8`以及顶端对`n`的计算。\n\n现在，要理解这些函数的工作原理，需要完成下列事情：\n\n+ 将代码抄写在一张纸上。\n+ 当每个变量在`switch`之前初始化时，在纸的空白区域，把每个变量列在表中。\n+ 按照`switch`的逻辑模拟执行代码，之后再正确的`case`处跳出。\n+ 更新变量表，包括`to`、`from`和它们所指向的数组。\n+ 当你到达`while`或者我的`goto`时，检查你的变量，之后按照逻辑返回`do-while`顶端，或者`again`标签所在的地方。\n+ 继续这一手动的执行过程，更新变量，直到确定明白了代码如何运作。\n\n## 为什么写成这样？\n\n当你弄明白它的实际工作原理时，最终的问题是：为什么要把代码写成这样？这个小技巧的目的是手动编写“循环展开”。大而长的循环会非常慢，所以提升速度的一个方法就是找到循环中某个固定的部分，之后在循环中复制代码，序列化地展开。例如，如果你知道一个循环会执行至少20次，你就可以将这20次的内容直接写在源代码中。\n\n达夫设备通过将循环展开为8个迭代块，来完成这件事情。这是个聪明的办法，并且可以正常工作。但是目前一个好的编译器也会为你完成这些。你不应该这样做，除非少数情况下你证明了它的确可以提升速度。\n\n## 附加题\n\n+ 不要再这样写代码了。\n+ 查询维基百科的“达夫设备”词条，并且看看你能不能找到错误。将它与这里的版本对比，并且阅读文章来试着理解，为什么维基百科上的代码在你这里不能正常工作，但是对于汤姆·达夫可以。\n+ 创建一些宏，来自动完成任意长度的这种设备。例如，你想创建32个`case`语句，并且不想手动把它们都写出来时，你会怎么办？你可以编写一次展开8个的宏吗？\n+ 修改`main`函数，执行一些速度检测，来看看哪个实际上更快。\n+ 查询`memcpy`、`memmove`和`memset`，并且也比较一下它们的速度。\n+ 不要再这样写代码了！\n"
  },
  {
    "path": "docs/lcthw-zh/ex24.md",
    "content": "# 练习24：输入输出和文件\n\n> 原文：[Exercise 24: Input, Output, Files](http://c.learncodethehardway.org/book/ex24.html)\n\n> 译者：[飞龙](https://github.com/wizardforcel)\n\n你已经学会了使用`printf`来打印变量，这非常不错，但是还需要学习更多。这个练习中你会用到`fscanf`和`fgets`在结构体中构建关于一个人的信息。在这个关于读取输入的简介之后，你会得到C语言IO函数的完整列表。其中一些你已经见过并且使用过了，所以这个练习也是一个记忆练习。\n\n```c\n#include <stdio.h>\n#include \"dbg.h\"\n\n#define MAX_DATA 100\n\ntypedef enum EyeColor {\n    BLUE_EYES, GREEN_EYES, BROWN_EYES,\n    BLACK_EYES, OTHER_EYES\n} EyeColor;\n\nconst char *EYE_COLOR_NAMES[] = {\n    \"Blue\", \"Green\", \"Brown\", \"Black\", \"Other\"\n};\n\ntypedef struct Person {\n    int age;\n    char first_name[MAX_DATA];\n    char last_name[MAX_DATA];\n    EyeColor eyes;\n    float income;\n} Person;\n\n\nint main(int argc, char *argv[])\n{\n    Person you = {.age = 0};\n    int i = 0;\n    char *in = NULL;\n\n    printf(\"What's your First Name? \");\n    in = fgets(you.first_name, MAX_DATA-1, stdin);\n    check(in != NULL, \"Failed to read first name.\");\n\n    printf(\"What's your Last Name? \");\n    in = fgets(you.last_name, MAX_DATA-1, stdin);\n    check(in != NULL, \"Failed to read last name.\");\n\n    printf(\"How old are you? \");\n    int rc = fscanf(stdin, \"%d\", &you.age);\n    check(rc > 0, \"You have to enter a number.\");\n\n    printf(\"What color are your eyes:\\n\");\n    for(i = 0; i <= OTHER_EYES; i++) {\n        printf(\"%d) %s\\n\", i+1, EYE_COLOR_NAMES[i]);\n    }\n    printf(\"> \");\n\n    int eyes = -1;\n    rc = fscanf(stdin, \"%d\", &eyes);\n    check(rc > 0, \"You have to enter a number.\");\n\n    you.eyes = eyes - 1;\n    check(you.eyes <= OTHER_EYES && you.eyes >= 0, \"Do it right, that's not an option.\");\n\n    printf(\"How much do you make an hour? \");\n    rc = fscanf(stdin, \"%f\", &you.income);\n    check(rc > 0, \"Enter a floating point number.\");\n\n    printf(\"----- RESULTS -----\\n\");\n\n    printf(\"First Name: %s\", you.first_name);\n    printf(\"Last Name: %s\", you.last_name);\n    printf(\"Age: %d\\n\", you.age);\n    printf(\"Eyes: %s\\n\", EYE_COLOR_NAMES[you.eyes]);\n    printf(\"Income: %f\\n\", you.income);\n\n    return 0;\nerror:\n\n    return -1;\n}\n```\n\n这个程序非常简单，并且引入了叫做`fscanf`的函数，意思是“文件的格式化输入”。`scanf`家族的函数是`printf`的反转版本。`printf`用于以某种格式打印数据，然而`scanf`以某种格式读取（或者扫描）输入。\n\n文件开头没有什么新的东西，所以下面只列出`main`所做的事情：\n\nex24.c:24-28\n\n创建所需的变量。\n\nex24.c:30-32\n\n使用`fgets`函数获取名字，它从输入读取字符串（这个例子中是`stdin`），但是确保它不会造成缓冲区溢出。\n\nex24.c:34-36\n\n对` you.last_name`执行相同操作，同样使用了`fgets`。\n\nex24.c:38-39\n\n使用`fscanf`来从`stdin`读取整数，并且将其放到`you.age`中。你可以看到，其中使用了和`printf`相同格式的格式化字符串。你也应该看到传入了`you.age`的地址，便于`fscnaf`获得它的指针来修改它。这是一个很好的例子，解释了使用指向数据的指针作为“输出参数”。\n\nex24.c:41-45\n\n打印出用于眼睛颜色的所有可选项，并且带有`EyeColor`枚举所匹配的数值。\n\nex24.c:47-50\n\n再次使用了`fscanf`，从`you.eyes`中获取数值，但是保证了输入是有效的。这非常重要，因为用户可以输入一个超出`EYE_COLOR_NAMES`数组范围的值，并且会导致段错误。\n\nex24.c:52-53\n\n获取`you.income`的值。\n\nex24.c:55-61\n\n将所有数据打印出来，便于你看到它们是否正确。要注意`EYE_COLOR_NAMES`用于打印`EyeColor`枚举值实际上的名字。\n\n## 你会看到什么\n\n当你运行这个程序时，你应该看到你的输入被适当地转换。你应该尝试给它非预期的输入，看看程序是怎么预防它的。\n\n```sh\n$ make ex24\ncc -Wall -g -DNDEBUG    ex24.c   -o ex24\n$ ./ex24\nWhat's your First Name? Zed\nWhat's your Last Name? Shaw\nHow old are you? 37\nWhat color are your eyes:\n1) Blue\n2) Green\n3) Brown\n4) Black\n5) Other\n> 1\nHow much do you make an hour? 1.2345\n----- RESULTS -----\nFirst Name: Zed\nLast Name: Shaw\nAge: 37\nEyes: Blue\nIncome: 1.234500\n```\n\n## 如何使它崩溃\n\n这个程序非常不错，但是这个练习中真正重要的部分是，`scanf`如何发生错误。对于简单的数值转换没有问题，但是对于字符串会出现问题，因为`scanf`在你读取之前并不知道缓冲区有多大。类似于`gets`的函数（并不是`fgets`，不带`f`的版本）也有一个我们已经避免的问题。它并不是道输入缓冲区有多大，并且可能会使你的程序崩溃。\n\n要演示`fscanf`和字符串的这一问题，需要修改使用`fgets`的那一行，使它变成`fscanf(stdin, \"%50s\", you.first_name)`，并且尝试再次运行。你会注意到，它读取了过多的内容，并且吃掉了你的回车键。这并不是你期望它所做的，你应该使用`fgets`而不是去解决古怪的`scanf`问题。\n\n接下来，将`fgets`改为`gets`，接着使用`valgrind`来执行`valgrind ./ex24 < /dev/urandom`，往你的程序中输入一些垃圾字符串。这叫做对你的程序进行“模糊测试”，它是一种不错的方法来发现输入错误。这个例子中，你需要从`/dev/urandom`文件来输入一些垃圾，并且观察它如何崩溃。在一些平台上你需要执行数次，或者修改`MAX_DATA`来使其变小。\n\n`gets`函数非常糟糕，以至于一些平台在程序运行时会警告你使用了`gets`。你应该永远避免使用这个函数。\n\n最后，找到`you.eyes`输入的地方，并移除对其是否在正确范围内的检查。然后，为它输入一个错误的数值，比如-1或者1000。在`Valgrind`执行这些操作，来观察会发生什么。\n\n> 译者注：根据最新的C11标准，对于输入函数，你应该总是使用`_s`后缀的安全版本。对于向字符串的输出函数，应该总是使用C99中新增的带`n`的版本，例如`snprintf`。如果你的编译器支持新版本，就不应该使用旧版本的不安全函数。\n\n## IO函数\n\n这是一个各种IO函数的简单列表。你应该查询每个函数并为其创建速记卡，包含函数名称，功能和它的任何变体。\n\n+ `fscanf`\n+ `fgets`\n+ `fopen`\n+ `freopen`\n+ `fdopen`\n+ `fclose`\n+ `fcloseall`\n+ `fgetpos`\n+ `fseek`\n+ `ftell`\n+ `rewind`\n+ `fprintf`\n+ `fwrite`\n+ `fread`\n\n过一遍这些函数，并且记住它们的不同变体和它们的功能。例如，对于`fscanf`的卡片，上面应该有`scanf`、`sscanf`、`vscanf`，以及其它。并且在背面写下每个函数所做的事情。\n\n最后，为了获得这些卡片所需的信息，使用`man`来阅读它的帮助。例如，`fscanf`帮助页由`man fscanf`得到。\n\n## 附加题\n\n+ 将这个程序重写为不需要`fscanf`的版本。你需要使用类似于`atoi`的函数来将输入的字符串转换为数值。\n+ 修改这个程序，使用`scanf`来代替`fscanf`，并观察有什么不同。\n+ 修改程序，是输入的名字不包含任何换行符和空白字符。\n+ 使用`scanf`编写函数，按照文件名读取文件内容，每次读取单个字符，但是不要越过（文件和缓冲区的）末尾。使这个函数接受字符串大小来更加通用，并且确保无论什么情况下字符串都以`'\\0'`结尾。\n"
  },
  {
    "path": "docs/lcthw-zh/ex25.md",
    "content": "# 练习25：变参函数\n\n> 原文：[Exercise 25: Variable Argument Functions](http://c.learncodethehardway.org/book/ex25.html)\n\n> 译者：[飞龙](https://github.com/wizardforcel)\n\n在C语言中，你可以通过创建“变参函数”来创建你自己的`printf`或者`scanf`版本。这些函数使用`stdarg.h`头，它们可以让你为你的库创建更加便利的接口。它们对于创建特定类型的“构建”函数、格式化函数和任何用到可变参数的函数都非常实用。\n\n理解“变参函数”对于C语言编程并不必要，我在编程生涯中也只有大约20次用到它。但是，理解变参函数如何工作有助于你对它的调试，并且让你更加了解计算机。\n\n```c\n/** WARNING: This code is fresh and potentially isn't correct yet. */\n\n#include <stdlib.h>\n#include <stdio.h>\n#include <stdarg.h>\n#include \"dbg.h\"\n\n#define MAX_DATA 100\n\nint read_string(char **out_string, int max_buffer)\n{\n    *out_string = calloc(1, max_buffer + 1);\n    check_mem(*out_string);\n\n    char *result = fgets(*out_string, max_buffer, stdin);\n    check(result != NULL, \"Input error.\");\n\n    return 0;\n\nerror:\n    if(*out_string) free(*out_string);\n    *out_string = NULL;\n    return -1;\n}\n\nint read_int(int *out_int)\n{\n    char *input = NULL;\n    int rc = read_string(&input, MAX_DATA);\n    check(rc == 0, \"Failed to read number.\");\n\n    *out_int = atoi(input);\n\n    free(input);\n    return 0;\n\nerror:\n    if(input) free(input);\n    return -1;\n}\n\nint read_scan(const char *fmt, ...)\n{\n    int i = 0;\n    int rc = 0;\n    int *out_int = NULL;\n    char *out_char = NULL;\n    char **out_string = NULL;\n    int max_buffer = 0;\n\n    va_list argp;\n    va_start(argp, fmt);\n\n    for(i = 0; fmt[i] != '\\0'; i++) {\n        if(fmt[i] == '%') {\n            i++;\n            switch(fmt[i]) {\n                case '\\0':\n                    sentinel(\"Invalid format, you ended with %%.\");\n                    break;\n\n                case 'd':\n                    out_int = va_arg(argp, int *);\n                    rc = read_int(out_int);\n                    check(rc == 0, \"Failed to read int.\");\n                    break;\n\n                case 'c':\n                    out_char = va_arg(argp, char *);\n                    *out_char = fgetc(stdin);\n                    break;\n\n                case 's':\n                    max_buffer = va_arg(argp, int);\n                    out_string = va_arg(argp, char **);\n                    rc = read_string(out_string, max_buffer);\n                    check(rc == 0, \"Failed to read string.\");\n                    break;\n\n                default:\n                    sentinel(\"Invalid format.\");\n            }\n        } else {\n            fgetc(stdin);\n        }\n\n        check(!feof(stdin) && !ferror(stdin), \"Input error.\");\n    }\n\n    va_end(argp);\n    return 0;\n\nerror:\n    va_end(argp);\n    return -1;\n}\n\n\n\nint main(int argc, char *argv[])\n{\n    char *first_name = NULL;\n    char initial = ' ';\n    char *last_name = NULL;\n    int age = 0;\n\n    printf(\"What's your first name? \");\n    int rc = read_scan(\"%s\", MAX_DATA, &first_name);\n    check(rc == 0, \"Failed first name.\");\n\n    printf(\"What's your initial? \");\n    rc = read_scan(\"%c\\n\", &initial);\n    check(rc == 0, \"Failed initial.\");\n\n    printf(\"What's your last name? \");\n    rc = read_scan(\"%s\", MAX_DATA, &last_name);\n    check(rc == 0, \"Failed last name.\");\n\n    printf(\"How old are you? \");\n    rc = read_scan(\"%d\", &age);\n\n    printf(\"---- RESULTS ----\\n\");\n    printf(\"First Name: %s\", first_name);\n    printf(\"Initial: '%c'\\n\", initial);\n    printf(\"Last Name: %s\", last_name);\n    printf(\"Age: %d\\n\", age);\n\n    free(first_name);\n    free(last_name);\n    return 0;\nerror:\n    return -1;\n}\n```\n\n这个程序和上一个练习很像，除了我编写了自己的`scanf`风格函数，它以我自己的方式处理字符串。你应该对`main`函数很清楚了，以及`read_string`和`read_int`两个函数，因为它们并没有做什么新的东西。\n\n这里的变参函数叫做`read_scan`，它使用了`va_list`数据结构执行和`scanf`相同的工作，并支持宏和函数。下面是它的工作原理：\n\n+ 我将函数的最后一个参数设置为`...`，它向C表示这个函数在`fmt`参数之后接受任何数量的参数。我可以在它前面设置许多其它的参数，但是在它后面不能放置任何参数。\n+ 在设置完一些参数时，我创建了`va_list`类型的变量，并且使用`va_list`来为其初始化。这配置了`stdarg.h`中的这一可以处理可变参数的组件。\n+ 接着我使用了`for`循环，遍历`fmt`格式化字符串，并且处理了类似`scanf`的格式，但比它略简单。它里面只带有整数、字符和字符串。\n+ 当我碰到占位符时，我使用了`switch`语句来确定需要做什么。\n+ 现在，为了从`va_list argp`中获得遍历，我需要使用`va_arg(argp, TYPE)`宏，其中`TYPE`是我将要向参数传递的准确类型。这一设计的后果是你会非常盲目，所以如果你没有足够的变量传入，程序就会崩溃。\n+ 和`scanf`的有趣的不同点是，当它碰到`'s'`占位符时，我使用`read_string`来创建字符串。`va_list argp`栈需要接受两个函数：需要读取的最大尺寸，以及用于输出的字符串指针。`read_string`使用这些信息来执行实际工作。\n+ 这使`read_scan`比`scan`更加一致，因为你总是使用`&`提供变量的地址，并且合理地设置它们。\n+ 最后，如果它碰到了不在格式中的字符，它仅仅会读取并跳过，而并不关心字符是什么，因为它只需要跳过。\n\n## 你会看到什么\n\n当你运行程序时，会得到与下面详细的结果：\n\n```sh\n$ make ex25\ncc -Wall -g -DNDEBUG    ex25.c   -o ex25\n$ ./ex25\nWhat's your first name? Zed\nWhat's your initial? A\nWhat's your last name? Shaw\nHow old are you? 37\n---- RESULTS ----\nFirst Name: Zed\nInitial: 'A'\nLast Name: Shaw\nAge: 37\n```\n\n## 如何使它崩溃\n\n这个程序对缓冲区溢出更加健壮，但是和`scanf`一样，它不能够处理输入的格式错误。为了使它崩溃，试着修改代码，把首先传入用于`'%s'`格式的尺寸去掉。同时试着传入多于`MAX_DATA`的数据，之后找到在`read_string`中不使用`calloc`的方法，并且修改它的工作方式。最后还有个问题是`fgets`会吃掉换行符，所以试着使用`fgetc`修复它，要注意字符串结尾应为`'\\0'`。\n\n## 附加题\n\n+ 再三检查确保你明白了每个`out_`变量的作用。最重要的是`out_string`，并且它是指针的指针。所以，理清当你设置时获取到的是指针还是内容尤为重要。\n+ 使用变参系统编写一个和`printf`相似的函数，重新编写`main`来使用它。\n+ 像往常一样，阅读这些函数/宏的手册页，确保知道了它在你的平台做了什么，一些平台会使用宏而其它平台会使用函数，还有一些平台会让它们不起作用。这完全取决于你所用的编译器和平台。\n"
  },
  {
    "path": "docs/lcthw-zh/ex26.md",
    "content": "# 练习26：编写第一个真正的程序\n\n> 原文：[Exercise 26: Write A First Real Program](http://c.learncodethehardway.org/book/ex26.html)\n\n> 译者：[飞龙](https://github.com/wizardforcel)\n\n这本书你已经完成一半了，所以你需要做一个期中检测。期中检测中你需要重新构建一个我特地为本书编写的软件，叫做`devpkg`。随后你需要以一些方式扩展它，并且通过编写一些单元测试来改进代码。\n\n> 注\n\n> 我在一些你需要完成的练习之前编写了这个练习。如果你现在尝试这个练习，记住软件可能会含有一些bug，你可能由于我的错误会产生一些问题，也可能不知道需要什么来完成它。如果这样的话，通过[help@learncodethehardway.org](mailto:help@learncodethehardway.org)来告诉我，之后等待我写完其它练习。\n\n## 什么是`devpkg`？\n\n`devpkg`是一个简单的C程序，可以用于安装其它软件。我特地为本书编写了它，作为一种方式来教你真正的软件是如何构建的，以及如何复用他人的库。它使用了一个叫做[Apache可移植运行时（APR）](http://apr.apache.org/)的库，其中含有许多工作跨平台的便利的C函数，包括Windows。此外，它只是从互联网（或本地文件）抓取代码，并且执行通常的`./configure ; make ; make install`命令，每个程序员都用到过。\n\n这个练习中，你的目标是从源码构建`devpkg`，完成我提供的每个挑战，并且使用源码来理解`devpkg`做了什么和为什么这样做。\n\n## 我们打算创建什么\n\n我们打算创建一个具有三个命令的工具：\n\ndevpkg -S\n\n在电脑上安装新的软件。\n\ndevpkg -I\n\n从URL安装软件。\n\ndevpkg -L\n\n列出安装的所有软件。\n\ndevpkg -F\n\n为手动构建抓取源代码。\n\ndevpkg -B\n\n构建所抓取的源码代码并且安装它，即使它已经安装了。\n\n我们想让`devpkg`能够接受几乎任何URL，判断项目的类型，下载，安装，以及注册已经安装的软件。我们也希望它能够处理一个简单的依赖列表，以便它能够安装项目所需的所有软件。\n\n## 设计\n\n为了完成这一目标，`devpkg`具有非常简单的设计：\n\n使用外部命令\n\n大多数工作都是通过类似于`curl`、`git`和`tar`的外部命令完成的。这样减少了`devpkg`所需的代码量。\n\n简单的文件数据库\n\n你可以轻易使它变得很复杂，但是一开始你需要完成一个简单的文件数据库，位于`/usr/local/.devpkg/db`，来跟踪已安装的软件。\n\n`/usr/local`\n\n同样你可以使它更高级，但是对于初学者来说，假设项目始终位于`/usr/local`中，它是大多数Unix软件的标准安装目录。\n\n`configure; make; make install`\n\n假设大多数软件可以通过`configure; make; make install`来安装，也许`configure`是可选的。如果软件不能通过这种方式安装，要么提供某种方式来修改命令，要么`devpkg`就可以无视它。\n\n用户可以root\n\n我们假设用于可以使用`sudo`来提升至root权限，除非他们直到最后才想root。\n\n这会使我们的程序像当初设想的一样简单，并且对于它的功能来说已经足够了。之后你可以进一步修改它。\n\n## Apache 可移植运行时\n\n你需要做的另外一件事情就是使用[Apache可移植运行时（APR）](http://apr.apache.org/)来未完成这个练习获得一个可移植的工具集。APR并不是必要的，你也可以不用它，但是你需要写的代码就会非常多。我现在强制你使用APR，使你能够熟悉链接和使用其他的库。最后，APR也能在Windows上工作，所以你可以把它迁移到许多其它平台上。\n\n你应该获取`apr-1.4.5`和`apr-util-1.3`的库，以及浏览在[apr.apache.org主站](http://apr.apache.org/)上的文档。\n\n下面是一个ShellScript，用于安装所需的所有库。你应该手动将它写到一个文件中，之后运行它直到APR安装好并且没有任何错误。\n\n```sh\nset -e\n\n# go somewhere safe\ncd /tmp\n\n# get the source to base APR 1.4.6\ncurl -L -O http://archive.apache.org/dist/apr/apr-1.4.6.tar.gz\n\n# extract it and go into the source\ntar -xzvf apr-1.4.6.tar.gz\ncd apr-1.4.6\n\n# configure, make, make install\n./configure\nmake\nsudo make install\n\n# reset and cleanup\ncd /tmp\nrm -rf apr-1.4.6 apr-1.4.6.tar.gz\n\n# do the same with apr-util\ncurl -L -O http://archive.apache.org/dist/apr/apr-util-1.4.1.tar.gz\n\n# extract\ntar -xzvf apr-util-1.4.1.tar.gz\ncd apr-util-1.4.1\n\n# configure, make, make install\n./configure --with-apr=/usr/local/apr\n# you need that extra parameter to configure because\n# apr-util can't really find it because...who knows.\n\nmake\nsudo make install\n\n#cleanup\ncd /tmp\nrm -rf apr-util-1.4.1* apr-1.4.6*\n```\n\n我希望你输入这个脚本，因为这就是`devpkg`基本上所做的事情，只是带有了一些选项和检查项。实际上，你可以使用Shell以更少的代码来完成它，但是这对于一本C语言的书不是一个很好的程序。\n\n简单运行这个脚本，修复它直到正常工作，就完成的所有库的安装，之后你需要完成项目的剩下部分。\n\n## 项目布局\n\n你需要创建一些简单的项目文件来起步。下面是我通常创建一个新项目的方法：\n\n```sh\nmkdir devpkg\ncd devpkg\ntouch README Makefile\n```\n\n## 其它依赖\n\n你应该已经安装了APR和APR-util，所以你需要一些更多的文件作为基本的依赖：\n\n+ 练习20中的`dbg.h`。\n+ 从[http://bstring.sourceforge.net/](http://bstring.sourceforge.net/)下载的`bstrlib.h`和`bstrlib.c`。下载`.zip`文件，解压并且将这个两个文件拷贝到项目中。\n+ 运行`make bstrlib.o`，如果这不能正常工作，阅读下面的“修复`bstring`”指南。\n\n> 注\n\n> 在一些平台上`bstring.c`文件会出现下列错误：\n\n> ```sh\n> bstrlib.c:2762: error: expected declaration specifiers or '...' before numeric constant\n> ```\n\n> 这是由于作者使用了一个不好的定义，它在一些平台上不能工作。你需要修改第2759行的`#ifdef __GNUC__`，并把它改成：\n\n> ```c\n> #if defined(__GNUC__) && !defined(__APPLE__)\n> ```\n\n之后在Mac OSX平台上就应该能够正常工作了。\n\n做完上面这些后，你应该有了`Makefile`，`README`，`dbg.h`，`bstrlib.h`和`bstrlib.c`，并做好了准备。\n\n## Makefile\n\n我们最好从`Makefile`开始，因为它列出了项目如何构建，以及你会创建哪些源文件。\n\n```make\nPREFIX?=/usr/local\nCFLAGS=-g -Wall -I${PREFIX}/apr/include/apr-1  -I${PREFIX}/apr/include/apr-util-1\nLDFLAGS=-L${PREFIX}/apr/lib -lapr-1 -pthread -laprutil-1\n\nall: devpkg\n\ndevpkg: bstrlib.o db.o shell.o commands.o\n\ninstall: all\n     install -d $(DESTDIR)/$(PREFIX)/bin/\n     install devpkg $(DESTDIR)/$(PREFIX)/bin/\n\nclean:\n     rm -f *.o\n     rm -f devpkg\n     rm -rf *.dSYM\n```\n\n比起之前看到过的，这并没有什么新东西，除了可能有些奇怪的`?=`语法，它表示“如果之前没有定义，就将`PREFIX`设置为该值”。\n\n> 注\n\n> 如果你使用了最近版本的Ubuntu，你会得到`apr_off_t` 或 `off64_t`的错误，之后需要向`CFLAGS`添加`-D_LARGEFILE64_SOURCE=1`。\n\n> 所需的另一件事是，你需要向`/etc/ld.conf.so.d/`添加`/usr/local/apr/lib`，之后运行`ldconfig`使它能够选择正常的库。\n\n## 源文件\n\n我们可以从`makefile`中看到，`devpkg`有四个依赖项，它们是：\n\n`bstrlib.o`\n\n由`bstrlib.c`和`bstrlib.o`产生，你已经将它们引入了。\n\n`db.o`\n\n由`db.c`和`db.h`产生，它包含了一个小型“数据库”程序集的代码。\n\n`shell.o`\n\n由`shell.c`和`shell.h`产生，包含一些函数，是类似`curl`的一些命令运行起来更容易。\n\n`commands.o`\n\n由`commands.c`和`commands.h`产生，包含了`devpkg`所需的所有命令并使它更易用。\n\n`devpkg`\n\n它不会显式提到，但是它是`Makefile`在这一部分的目标。它由`devpkg.c`产生，包含用于整个程序的`main`函数。\n\n你的任务就是创建这些文件，并且输入代码并保证正确。\n\n> 注\n\n> 你读完这个描述可能会想，“Zed为什么那么聪明，坐着就能设计出来这些文件？！”我并不是用我强大的代码功力魔术般地把`devpkg`设计成这样。而是我做了这些：\n\n> + 我编写了简单的`README`来获得如何构建项目的灵感。\n> + 我创建了一个简单的bash脚本（就像你编写的那样）来理清所需的所有组件。\n> + 我创建了一个`.c`文件，并且在它上面花了几天，酝酿并想出点子。\n> + 接着我编写并调试程序，之后我将这一个大文件分成四个文件。\n> + 做完这些之后，我重命名和优化了函数和数据结构，使它们在逻辑上更“美观”。\n> + 最后，使新程序成功并以相同方式工作之后，我添加了一些新的特性，比如`-F`和`-B`选项。\n\n> 你读到的这份列表是我打算教给你的，但不要认为这是我构建软件的通用方法。有时候我会事先知道主题，并且会做更多的规划。也有时我会编写一份规划并将它扔掉，之后再规划更好的版本。它完全取决于我的经验告诉我哪个比较好，或者我的灵感将我带到何处。\n\n> 如果你碰到一个“专家”，它告诉你只有一个方法可以解决编程问题，那么它在骗你。要么它们实际使用了很多策略，要么他们并不足够好。\n\n## DB函数\n\n程序中必须有个方法来记录已经安装的URL，列出这些URL，并且检查一些程序是否已安装以便跳过。我会使用一个简单、扁平化的文件数据库，以及`bstrlib.h`。\n\n首先，创建`db.h`头文件，以便让你知道需要实现什么。\n\n```c\n#ifndef _db_h\n#define _db_h\n\n#define DB_FILE \"/usr/local/.devpkg/db\"\n#define DB_DIR \"/usr/local/.devpkg\"\n\n\nint DB_init();\nint DB_list();\nint DB_update(const char *url);\nint DB_find(const char *url);\n\n#endif\n```\n\n之后实现`db.c`中的这些函数，在你编写它的时候，像之前一样使用`make`。\n\n```c\n#include <unistd.h>\n#include <apr_errno.h>\n#include <apr_file_io.h>\n\n#include \"db.h\"\n#include \"bstrlib.h\"\n#include \"dbg.h\"\n\nstatic FILE *DB_open(const char *path, const char *mode)\n{\n    return fopen(path, mode);\n}\n\n\nstatic void DB_close(FILE *db)\n{\n    fclose(db);\n}\n\n\nstatic bstring DB_load()\n{\n    FILE *db = NULL;\n    bstring data = NULL;\n\n    db = DB_open(DB_FILE, \"r\");\n    check(db, \"Failed to open database: %s\", DB_FILE);\n\n    data = bread((bNread)fread, db);\n    check(data, \"Failed to read from db file: %s\", DB_FILE);\n\n    DB_close(db);\n    return data;\n\nerror:\n    if(db) DB_close(db);\n    if(data) bdestroy(data);\n    return NULL;\n}\n\n\nint DB_update(const char *url)\n{\n    if(DB_find(url)) {\n        log_info(\"Already recorded as installed: %s\", url);\n    }\n\n    FILE *db = DB_open(DB_FILE, \"a+\");\n    check(db, \"Failed to open DB file: %s\", DB_FILE);\n\n    bstring line = bfromcstr(url);\n    bconchar(line, '\\n');\n    int rc = fwrite(line->data, blength(line), 1, db);\n    check(rc == 1, \"Failed to append to the db.\");\n\n    return 0;\nerror:\n    if(db) DB_close(db);\n    return -1;\n}\n\n\nint DB_find(const char *url)\n{\n    bstring data = NULL;\n    bstring line = bfromcstr(url);\n    int res = -1;\n\n    data = DB_load();\n    check(data, \"Failed to load: %s\", DB_FILE);\n\n    if(binstr(data, 0, line) == BSTR_ERR) {\n        res = 0;\n    } else {\n        res = 1;\n    }\n\nerror: // fallthrough\n    if(data) bdestroy(data);\n    if(line) bdestroy(line);\n\n    return res;\n}\n\n\nint DB_init()\n{\n    apr_pool_t *p = NULL;\n    apr_pool_initialize();\n    apr_pool_create(&p, NULL);\n\n    if(access(DB_DIR, W_OK | X_OK) == -1) {\n        apr_status_t rc = apr_dir_make_recursive(DB_DIR,\n                APR_UREAD | APR_UWRITE | APR_UEXECUTE |\n                APR_GREAD | APR_GWRITE | APR_GEXECUTE, p);\n        check(rc == APR_SUCCESS, \"Failed to make database dir: %s\", DB_DIR);\n    }\n\n    if(access(DB_FILE, W_OK) == -1) {\n        FILE *db = DB_open(DB_FILE, \"w\");\n        check(db, \"Cannot open database: %s\", DB_FILE);\n        DB_close(db);\n    }\n\n    apr_pool_destroy(p);\n    return 0;\n\nerror:\n    apr_pool_destroy(p);\n    return -1;\n}\n\n\nint DB_list()\n{\n    bstring data = DB_load();\n    check(data, \"Failed to read load: %s\", DB_FILE);\n\n    printf(\"%s\", bdata(data));\n    bdestroy(data);\n    return 0;\n\nerror:\n    return -1;\n}\n```\n\n### 挑战1：代码复查\n\n在继续之前，仔细阅读这些文件的每一行，并且确保你以准确地输入了它们。通过逐行阅读代码来实践它。同时，跟踪每个函数调用，并且确保你使用了`check`来校验返回值。最后，在APR网站上的文档，或者bstrlib.h 或 bstrlib.c的源码中，查阅每个你不认识的函数。\n\n## Shell 函数\n\n`devkpg`的一个关键设计是，使用类似于`curl`、`tar`和`git`的外部工具来完成大部分的工作。我们可以找到在程序内部完成这些工作的库，但是如果我们只是需要这些程序的基本功能，这样就毫无意义。在Unix运行其它命令并不丢人。\n\n为了完成这些，我打算使用`apr_thread_proc.h`函数来运行程序，但是我也希望创建一个简单的类“模板”系统。我会使用`struct Shell`，它持有所有运行程序所需的信息，但是在参数中有一些“空位”，我可以将它们替换成实际值。\n\n观察`shell.h`文件来了解我会用到的结构和命令。你可以看到我使用`extern`来表明其他的`.c`文件也能访问到`shell.c`中定义的变量。\n\n```c\n#ifndef _shell_h\n#define _shell_h\n\n#define MAX_COMMAND_ARGS 100\n\n#include <apr_thread_proc.h>\n\ntypedef struct Shell {\n    const char *dir;\n    const char *exe;\n\n    apr_procattr_t *attr;\n    apr_proc_t proc;\n    apr_exit_why_e exit_why;\n    int exit_code;\n\n    const char *args[MAX_COMMAND_ARGS];\n} Shell;\n\nint Shell_run(apr_pool_t *p, Shell *cmd);\nint Shell_exec(Shell cmd, ...);\n\nextern Shell CLEANUP_SH;\nextern Shell GIT_SH;\nextern Shell TAR_SH;\nextern Shell CURL_SH;\nextern Shell CONFIGURE_SH;\nextern Shell MAKE_SH;\nextern Shell INSTALL_SH;\n\n#endif\n```\n\n确保你已经创建了`shell.h`，并且`extern Shell`变量的名字和数量相同。它们被`Shell_run`和`Shell_exec`函数用于运行命令。我定义了这两个函数，并且在`shell.c`中创建实际变量。\n\n```c\n#include \"shell.h\"\n#include \"dbg.h\"\n#include <stdarg.h>\n\nint Shell_exec(Shell template, ...)\n{\n    apr_pool_t *p = NULL;\n    int rc = -1;\n    apr_status_t rv = APR_SUCCESS;\n    va_list argp;\n    const char *key = NULL;\n    const char *arg = NULL;\n    int i = 0;\n\n    rv = apr_pool_create(&p, NULL);\n    check(rv == APR_SUCCESS, \"Failed to create pool.\");\n\n    va_start(argp, template);\n\n    for(key = va_arg(argp, const char *);\n        key != NULL;\n        key = va_arg(argp, const char *))\n    {\n        arg = va_arg(argp, const char *);\n\n        for(i = 0; template.args[i] != NULL; i++) {\n            if(strcmp(template.args[i], key) == 0) {\n                template.args[i] = arg;\n                break; // found it\n            }\n        }\n    }\n\n    rc = Shell_run(p, &template);\n    apr_pool_destroy(p);\n    va_end(argp);\n    return rc;\n\nerror:\n    if(p) {\n        apr_pool_destroy(p);\n    }\n    return rc;\n}\n\nint Shell_run(apr_pool_t *p, Shell *cmd)\n{\n    apr_procattr_t *attr;\n    apr_status_t rv;\n    apr_proc_t newproc;\n\n    rv = apr_procattr_create(&attr, p);\n    check(rv == APR_SUCCESS, \"Failed to create proc attr.\");\n\n    rv = apr_procattr_io_set(attr, APR_NO_PIPE, APR_NO_PIPE,\n            APR_NO_PIPE);\n    check(rv == APR_SUCCESS, \"Failed to set IO of command.\");\n\n    rv = apr_procattr_dir_set(attr, cmd->dir);\n    check(rv == APR_SUCCESS, \"Failed to set root to %s\", cmd->dir);\n\n    rv = apr_procattr_cmdtype_set(attr, APR_PROGRAM_PATH);\n    check(rv == APR_SUCCESS, \"Failed to set cmd type.\");\n\n    rv = apr_proc_create(&newproc, cmd->exe, cmd->args, NULL, attr, p);\n    check(rv == APR_SUCCESS, \"Failed to run command.\");\n\n    rv = apr_proc_wait(&newproc, &cmd->exit_code, &cmd->exit_why, APR_WAIT);\n    check(rv == APR_CHILD_DONE, \"Failed to wait.\");\n\n    check(cmd->exit_code == 0, \"%s exited badly.\", cmd->exe);\n    check(cmd->exit_why == APR_PROC_EXIT, \"%s was killed or crashed\", cmd->exe);\n\n    return 0;\n\nerror:\n    return -1;\n}\n\nShell CLEANUP_SH = {\n    .exe = \"rm\",\n    .dir = \"/tmp\",\n    .args = {\"rm\", \"-rf\", \"/tmp/pkg-build\", \"/tmp/pkg-src.tar.gz\",\n        \"/tmp/pkg-src.tar.bz2\", \"/tmp/DEPENDS\", NULL}\n};\n\nShell GIT_SH = {\n    .dir = \"/tmp\",\n    .exe = \"git\",\n    .args = {\"git\", \"clone\", \"URL\", \"pkg-build\", NULL}\n};\n\nShell TAR_SH = {\n    .dir = \"/tmp/pkg-build\",\n    .exe = \"tar\",\n    .args = {\"tar\", \"-xzf\", \"FILE\", \"--strip-components\", \"1\", NULL}\n};\n\nShell CURL_SH = {\n    .dir = \"/tmp\",\n    .exe = \"curl\",\n    .args = {\"curl\", \"-L\", \"-o\", \"TARGET\", \"URL\", NULL}\n};\n\nShell CONFIGURE_SH = {\n    .exe = \"./configure\",\n    .dir = \"/tmp/pkg-build\",\n    .args = {\"configure\", \"OPTS\", NULL},\n};\n\nShell MAKE_SH = {\n    .exe = \"make\",\n    .dir = \"/tmp/pkg-build\",\n    .args = {\"make\", \"OPTS\", NULL}\n};\n\nShell INSTALL_SH = {\n    .exe = \"sudo\",\n    .dir = \"/tmp/pkg-build\",\n    .args = {\"sudo\", \"make\", \"TARGET\", NULL}\n};\n```\n\n自底向上阅读`shell.c`的代码（这也是常见的C源码布局），你会看到我创建了实际的`Shell`变量，它在`shell.h`中以`extern`修饰。它们虽然在这里，但是也被程序的其它部分使用。这就是创建全局变量的方式，它们可以存在于一个`.c`文件中，但是可在任何地方使用。你不应该创建很多这类变量，但是它们的确很方便。\n\n继续阅读代码，我们读到了`Shell_run`，它是一个“基”函数，只是基于`Shell`中的东西执行命令。它使用了许多在`apr_thread_proc.h`中定义的函数，你需要查阅它们的每一个来了解工作原理。这就像是一些使用`system`函数调用的代码一样，但是它可以让你控制其他程序的执行。例如，在我们的`Shell`结构中，存在`.dir`属性在运行之前强制程序必须在指定目录中。\n\n最后，我创建了`Shell_exec`函数，它是个变参函数。你在之前已经看到过了，但是确保你理解了`stdarg.h`函数以及如何编写它们。在下个挑战中你需要分析这一函数。\n\n### 挑战2：分析`Shell_exec`\n\n为这些文件（以及向挑战1那样的完整的代码复查）设置的挑战是完整分析`Shell_exec`，并且拆分代码来了解工作原理。你应该能够理解每一行代码，`for`循环如何工作，以及参数如何被替换。\n\n一旦你分析完成，向`struct Shell`添加一个字段，提供需要替代的`args`变量的数量。更新所有命令来接受参数的正确数量，随后增加一个错误检查，来确认参数被正确替换，以及在错误时退出。\n\n## 命令行函数\n\n现在你需要构造正确的命令来完成功能。这些命令会用到APR的函数、`db.h`和`shell.h`来执行下载和构建软件的真正工作。这些文件最为复杂，所以要小心编写它们。你需要首先编写`commands.h`文件，接着在`commands.c`文件中实现它的函数。\n\n```c\n#ifndef _commands_h\n#define _commands_h\n\n#include <apr_pools.h>\n\n#define DEPENDS_PATH \"/tmp/DEPENDS\"\n#define TAR_GZ_SRC \"/tmp/pkg-src.tar.gz\"\n#define TAR_BZ2_SRC \"/tmp/pkg-src.tar.bz2\"\n#define BUILD_DIR \"/tmp/pkg-build\"\n#define GIT_PAT \"*.git\"\n#define DEPEND_PAT \"*DEPENDS\"\n#define TAR_GZ_PAT \"*.tar.gz\"\n#define TAR_BZ2_PAT \"*.tar.bz2\"\n#define CONFIG_SCRIPT \"/tmp/pkg-build/configure\"\n\nenum CommandType {\n    COMMAND_NONE, COMMAND_INSTALL, COMMAND_LIST, COMMAND_FETCH,\n    COMMAND_INIT, COMMAND_BUILD\n};\n\n\nint Command_fetch(apr_pool_t *p, const char *url, int fetch_only);\n\nint Command_install(apr_pool_t *p, const char *url, const char *configure_opts,\n        const char *make_opts, const char *install_opts);\n\nint Command_depends(apr_pool_t *p, const char *path);\n\nint Command_build(apr_pool_t *p, const char *url, const char *configure_opts,\n        const char *make_opts, const char *install_opts);\n\n#endif\n```\n\n`commands.h`中并没有很多之前没见过的东西。你应该看到了一些字符串的定义，它们在任何地方都会用到。真正的代码在`commands.c`中。\n\n```c\n#include <apr_uri.h>\n#include <apr_fnmatch.h>\n#include <unistd.h>\n\n#include \"commands.h\"\n#include \"dbg.h\"\n#include \"bstrlib.h\"\n#include \"db.h\"\n#include \"shell.h\"\n\n\nint Command_depends(apr_pool_t *p, const char *path)\n{\n    FILE *in = NULL;\n    bstring line = NULL;\n\n    in = fopen(path, \"r\");\n    check(in != NULL, \"Failed to open downloaded depends: %s\", path);\n\n    for(line = bgets((bNgetc)fgetc, in, '\\n'); line != NULL;\n            line = bgets((bNgetc)fgetc, in, '\\n'))\n    {\n        btrimws(line);\n        log_info(\"Processing depends: %s\", bdata(line));\n        int rc = Command_install(p, bdata(line), NULL, NULL, NULL);\n        check(rc == 0, \"Failed to install: %s\", bdata(line));\n        bdestroy(line);\n    }\n\n    fclose(in);\n    return 0;\n\nerror:\n    if(line) bdestroy(line);\n    if(in) fclose(in);\n    return -1;\n}\n\nint Command_fetch(apr_pool_t *p, const char *url, int fetch_only)\n{\n    apr_uri_t info = {.port = 0};\n    int rc = 0;\n    const char *depends_file = NULL;\n    apr_status_t rv = apr_uri_parse(p, url, &info);\n\n    check(rv == APR_SUCCESS, \"Failed to parse URL: %s\", url);\n\n    if(apr_fnmatch(GIT_PAT, info.path, 0) == APR_SUCCESS) {\n        rc = Shell_exec(GIT_SH, \"URL\", url, NULL);\n        check(rc == 0, \"git failed.\");\n    } else if(apr_fnmatch(DEPEND_PAT, info.path, 0) == APR_SUCCESS) {\n        check(!fetch_only, \"No point in fetching a DEPENDS file.\");\n\n        if(info.scheme) {\n            depends_file = DEPENDS_PATH;\n            rc = Shell_exec(CURL_SH, \"URL\", url, \"TARGET\", depends_file, NULL);\n            check(rc == 0, \"Curl failed.\");\n        } else {\n            depends_file = info.path;\n        }\n\n        // recursively process the devpkg list\n        log_info(\"Building according to DEPENDS: %s\", url);\n        rv = Command_depends(p, depends_file);\n        check(rv == 0, \"Failed to process the DEPENDS: %s\", url);\n\n        // this indicates that nothing needs to be done\n        return 0;\n\n    } else if(apr_fnmatch(TAR_GZ_PAT, info.path, 0) == APR_SUCCESS) {\n        if(info.scheme) {\n            rc = Shell_exec(CURL_SH,\n                    \"URL\", url,\n                    \"TARGET\", TAR_GZ_SRC, NULL);\n            check(rc == 0, \"Failed to curl source: %s\", url);\n        }\n\n        rv = apr_dir_make_recursive(BUILD_DIR,\n                APR_UREAD | APR_UWRITE | APR_UEXECUTE, p);\n        check(rv == APR_SUCCESS, \"Failed to make directory %s\", BUILD_DIR);\n\n        rc = Shell_exec(TAR_SH, \"FILE\", TAR_GZ_SRC, NULL);\n        check(rc == 0, \"Failed to untar %s\", TAR_GZ_SRC);\n    } else if(apr_fnmatch(TAR_BZ2_PAT, info.path, 0) == APR_SUCCESS) {\n        if(info.scheme) {\n            rc = Shell_exec(CURL_SH, \"URL\", url, \"TARGET\", TAR_BZ2_SRC, NULL);\n            check(rc == 0, \"Curl failed.\");\n        }\n\n        apr_status_t rc = apr_dir_make_recursive(BUILD_DIR,\n                APR_UREAD | APR_UWRITE | APR_UEXECUTE, p);\n\n        check(rc == 0, \"Failed to make directory %s\", BUILD_DIR);\n        rc = Shell_exec(TAR_SH, \"FILE\", TAR_BZ2_SRC, NULL);\n        check(rc == 0, \"Failed to untar %s\", TAR_BZ2_SRC);\n    } else {\n        sentinel(\"Don't now how to handle %s\", url);\n    }\n\n    // indicates that an install needs to actually run\n    return 1;\nerror:\n    return -1;\n}\n\nint Command_build(apr_pool_t *p, const char *url, const char *configure_opts,\n        const char *make_opts, const char *install_opts)\n{\n    int rc = 0;\n\n    check(access(BUILD_DIR, X_OK | R_OK | W_OK) == 0,\n            \"Build directory doesn't exist: %s\", BUILD_DIR);\n\n    // actually do an install\n    if(access(CONFIG_SCRIPT, X_OK) == 0) {\n        log_info(\"Has a configure script, running it.\");\n        rc = Shell_exec(CONFIGURE_SH, \"OPTS\", configure_opts, NULL);\n        check(rc == 0, \"Failed to configure.\");\n    }\n\n    rc = Shell_exec(MAKE_SH, \"OPTS\", make_opts, NULL);\n    check(rc == 0, \"Failed to build.\");\n\n    rc = Shell_exec(INSTALL_SH,\n            \"TARGET\", install_opts ? install_opts : \"install\",\n            NULL);\n    check(rc == 0, \"Failed to install.\");\n\n    rc = Shell_exec(CLEANUP_SH, NULL);\n    check(rc == 0, \"Failed to cleanup after build.\");\n\n    rc = DB_update(url);\n    check(rc == 0, \"Failed to add this package to the database.\");\n\n    return 0;\n\nerror:\n    return -1;\n}\n\nint Command_install(apr_pool_t *p, const char *url, const char *configure_opts,\n        const char *make_opts, const char *install_opts)\n{\n    int rc = 0;\n    check(Shell_exec(CLEANUP_SH, NULL) == 0, \"Failed to cleanup before building.\");\n\n    rc = DB_find(url);\n    check(rc != -1, \"Error checking the install database.\");\n\n    if(rc == 1) {\n        log_info(\"Package %s already installed.\", url);\n        return 0;\n    }\n\n    rc = Command_fetch(p, url, 0);\n\n    if(rc == 1) {\n        rc = Command_build(p, url, configure_opts, make_opts, install_opts);\n        check(rc == 0, \"Failed to build: %s\", url);\n    } else if(rc == 0) {\n        // no install needed\n        log_info(\"Depends successfully installed: %s\", url);\n    } else {\n        // had an error\n        sentinel(\"Install failed: %s\", url);\n    }\n\n    Shell_exec(CLEANUP_SH, NULL);\n    return 0;\n\nerror:\n    Shell_exec(CLEANUP_SH, NULL);\n    return -1;\n}\n```\n\n在你输入并编译它之后，就可以开始分析了。如果到目前为止你完成了前面的挑战，你会理解如何使用`shell.c`函数来运行shell命令，以及参数如何被替换。如果没有则需要回退到前面的挑战，确保你真正理解了`Shell_exec`的工作原理。\n\n### 挑战3：评判我的设计\n\n像之前一样，完整地复查一遍代码来保证一模一样。接着浏览每个函数并且确保你知道他如何工作。你也应该跟踪这个文件或其它文件中，每个函数对其它函数的调用。最后，确认你理解了这里的所有调用APR的函数。\n\n一旦你正确编写并分析了这个文件，把我当成一个傻瓜一样来评判我的设计，我需要看看你是否可以改进它。不要真正修改代码，只是创建一个`notes.txt`并且写下你的想法和你需要修改的地方。\n\n## `devpkg`的`main`函数\n\n`devpkg.c`是最后且最重要的，但是也可能是最简单的文件，其中创建了`main`函数。没有与之配套的`.h`文件，因为这个文件包含其他所有文件。这个文件用于创建`devpkg`可执行程序，同时组装了来自`Makefile`的其它`.o`文件。在文件中输入代码并保证正确。\n\n```c\n#include <stdio.h>\n#include <apr_general.h>\n#include <apr_getopt.h>\n#include <apr_strings.h>\n#include <apr_lib.h>\n\n#include \"dbg.h\"\n#include \"db.h\"\n#include \"commands.h\"\n\nint main(int argc, const char const *argv[])\n{\n    apr_pool_t *p = NULL;\n    apr_pool_initialize();\n    apr_pool_create(&p, NULL);\n\n    apr_getopt_t *opt;\n    apr_status_t rv;\n\n    char ch = '\\0';\n    const char *optarg = NULL;\n    const char *config_opts = NULL;\n    const char *install_opts = NULL;\n    const char *make_opts = NULL;\n    const char *url = NULL;\n    enum CommandType request = COMMAND_NONE;\n\n\n    rv = apr_getopt_init(&opt, p, argc, argv);\n\n    while(apr_getopt(opt, \"I:Lc:m:i:d:SF:B:\", &ch, &optarg) == APR_SUCCESS) {\n        switch (ch) {\n            case 'I':\n                request = COMMAND_INSTALL;\n                url = optarg;\n                break;\n\n            case 'L':\n                request = COMMAND_LIST;\n                break;\n\n            case 'c':\n                config_opts = optarg;\n                break;\n\n            case 'm':\n                make_opts = optarg;\n                break;\n\n            case 'i':\n                install_opts = optarg;\n                break;\n\n            case 'S':\n                request = COMMAND_INIT;\n                break;\n\n            case 'F':\n                request = COMMAND_FETCH;\n                url = optarg;\n                break;\n\n            case 'B':\n                request = COMMAND_BUILD;\n                url = optarg;\n                break;\n        }\n    }\n\n    switch(request) {\n        case COMMAND_INSTALL:\n            check(url, \"You must at least give a URL.\");\n            Command_install(p, url, config_opts, make_opts, install_opts);\n            break;\n\n        case COMMAND_LIST:\n            DB_list();\n            break;\n\n        case COMMAND_FETCH:\n            check(url != NULL, \"You must give a URL.\");\n            Command_fetch(p, url, 1);\n            log_info(\"Downloaded to %s and in /tmp/\", BUILD_DIR);\n            break;\n\n        case COMMAND_BUILD:\n            check(url, \"You must at least give a URL.\");\n            Command_build(p, url, config_opts, make_opts, install_opts);\n            break;\n\n        case COMMAND_INIT:\n            rv = DB_init();\n            check(rv == 0, \"Failed to make the database.\");\n            break;\n\n        default:\n            sentinel(\"Invalid command given.\");\n    }\n\n\n    return 0;\n\nerror:\n    return 1;\n}\n```\n\n### 挑战4：README 和测试文件\n\n为这个文件设置的挑战是理解参数如何处理，以及参数是什么，之后创建含有使用指南的`README`文件。在编写`README`的同时，也编写一个简单的`simple.sh`，它运行`./devpkg`来检查每个命令都在实际环境下工作。在你的脚本顶端使用`set -e`，使它跳过第一个错误。\n\n最后，在`Valgrind`下运行程序，确保在进行下一步之前，所有东西都能正常运行。\n\n## 期中检测\n\n最后的挑战就是这个期中检测，它包含三件事情：\n\n+ 将你的代码与我的在线代码对比，以100%的分数开始，每错一行减去1%。\n+ 在你的`notes.txt`中记录你是如何改进代码和`devpkg`的功能，并且实现你的改进。\n+ 编写一个`devpkg`的替代版本，使用其他你喜欢的语言，或者你觉得最适合编写它的语言。对比二者，之后基于你的结果改进你的`devpkg`的C版本。\n\n你可以执行下列命令来将你的代码与我的对比：\n\n```sh\ncd ..  # get one directory above your current one\ngit clone git://gitorious.org/devpkg/devpkg.git devpkgzed\ndiff -r devpkg devpkgzed\n```\n\n这将会克隆我的`devpkg`版本到`devpkgzed`目录中。之后使用工具`diff`来对比你的和我的代码。书中你所使用的这些文件直接来自于这个项目，所以如果出现了不同的行，肯定就有错误。\n\n要记住这个练习没有真正的及格或不及格，它只是一个方式来让你挑战自己，并尽可能变得精确和谨慎。\n"
  },
  {
    "path": "docs/lcthw-zh/ex27.md",
    "content": "# 练习27：创造性和防御性编程\n\n> 原文：[Exercise 27: Creative And Defensive Programming](http://c.learncodethehardway.org/book/ex27.html)\n\n> 译者：[飞龙](https://github.com/wizardforcel)\n\n你已经学到了大多数C语言的基础，并且准备好开始成为一个更严谨的程序员了。这里就是从初学者走向专家的地方，不仅仅对于C，更对于核心的计算机科学概念。我将会教给你一些核心的数据结构和算法，它们是每个程序员都要懂的，还有一些我在真实程序中所使用的一些非常有趣的东西。\n\n在我开始之前，我需要教给你一些基本的技巧和观念，它们能帮助你编写更好的软件。练习27到31会教给你高级的概念和特性，而不是谈论编程，但是这些之后你将会应用它们来编写核心库或有用的数据结构。\n\n编写更好的C代码（实际上是所有语言）的第一步是，学习一种新的观念叫做“防御性编程”。防御性编程假设你可能会制造出很多错误，之后尝试在每一步尽可能预防它们。这个练习中我打算教给你如何以防御性的思维来思考编程。\n\n## 创造性编程思维\n\n在这个简单的练习中要告诉你如何做到创造性是不可能的，但是我会告诉你一些涉及到任务风险和开放思维的创造力。恐惧会快速地扼杀创造力，所以我采用，并且许多程序员也采用的这种思维方式使我不会惧怕风险，并且看上去像个傻瓜。\n\n+ 我不会犯错误。\n+ 人们所想的并不重要。\n+ 我脑子里面诞生的想法才是最好的。\n\n我只是暂时接受了这种思维，并且在应用中用了一些小技巧。为了这样做我会提出一些想法，寻找创造性的解决方案，开一些奇奇怪怪的脑洞，并且不会害怕发明一些古怪的东西。在这种思维方式下，我通常会编写出第一个版本的糟糕代码，用于将想法描述出来。\n\n然而，当我完成我的创造性原型时，我会将它扔掉，并且将它变得严谨和可考。其它人在这里常犯的一个错误就是将创造性思维引入它们的实现阶段。这样会产生一种非常不同的破坏性思维，它是创造性思维的阴暗面：\n\n+ 编写完美的软件是可行的。\n+ 我的大脑告诉我了真相，它不会发现任何错误，所以我写了完美的软件。\n+ 我的代码就是我自己，批判它的人也在批判我。\n\n这些都是错误的。你经常会碰到一些程序员，它们对自己创造的软件具有强烈的荣誉感。这很正常，但是这种荣誉感会成为客观上改进作品的阻力。由于这种荣誉感和它们对作品的依恋，它们会一直相信它们编写的东西是完美的。只要它们忽视其它人的对这些代码的观点，它们就可以保护它们的玻璃心，并且永远不会改进。\n\n同时具有创造性思维和编写可靠软件的技巧是，采用防御性编程的思维。\n\n## 防御性编程思维\n\n在你做出创造性原型，并且对你的想法感觉良好之后，就应该切换到防御性思维了。防御性思维的程序员大致上会否定你的代码，并且相信下面这些事情：\n\n+ 软件中存在错误。\n+ 你并不是你的软件，但你需要为错误负责。\n+ 你永远不可能消除所有错误，只能降低它们的可能性。\n\n这种思维方式让你诚实地对待你的代码，并且为改进批判地分析它。注意上面并没有说**你**充满了错误，只是说你的**代码**充满错误。这是一个需要理解的关键，因为它给了你编写下一个实现的客观力量。\n\n就像创造性思维，防御性编程思维也有阴暗面。防御性程序员是一个惧怕任何事情的偏执狂，这种恐惧使他们远离可能的错误或避免犯错误。当你尝试做到严格一致或正确时这很好，但是它是创造力和专注的杀手。\n\n## 八个防御性编程策略\n\n一旦你接受了这一思维，你可以重新编写你的原型，并且遵循下面的八个策略，它们被我用于尽可能把代码变得可靠。当我编写代码的“实际”版本，我会严格按照下面的策略，并且尝试消除尽可能多的错误，以一些会破坏我软件的人的方式思考。\n\n永远不要信任输入\n\n永远不要提供的输入，并总是校验它。\n\n避免错误\n\n如果错误可能发生，不管可能性多低都要避免它。\n\n过早暴露错误\n\n过早暴露错误，并且评估发生了什么、在哪里发生以及如何修复。\n\n记录假设\n\n清楚地记录所有先决条件，后置条件以及不变量。\n\n防止过多的文档\n\n不要在实现阶段就编写文档，它们可以在代码完成时编写。\n\n使一切自动化\n\n使一切自动化，尤其是测试。\n\n简单化和清晰化\n\n永远简化你的代码，在没有牺牲安全性的同时变得最小和最整洁。\n\n质疑权威\n\n不要盲目遵循或拒绝规则。\n\n这些并不是全部，仅仅是一些核心的东西，我认为程序员应该在编程可靠的代码时专注于它们。要注意我并没有真正说明如何具体做到这些，我接下来会更细致地讲解每一条，并且会布置一些覆盖它们的练习。\n\n## 应用这八条策略\n\n这些观点都是一些流行心理学的陈词滥调，但是你如何把它们应用到实际编程中呢？我现在打算向你展示这本书中的一些代码所做的事情，这些代码用具体的例子展示每一条策略。这八条策略并不止于这些例子，你应该使用它们作为指导，使你的代码更可靠。\n\n### 永远不要信任输入\n\n让我们来看一个坏设计和“更好”的设计的例子。我并不想称之为好设计，因为它可以做得更好。看一看这两个函数，它们都复制字符串，`main`函数用于测试哪个更好。\n\n```c\nundef NDEBUG\n#include \"dbg.h\"\n#include <stdio.h>\n#include <assert.h>\n\n/*\n * Naive copy that assumes all inputs are always valid\n * taken from K&R C and cleaned up a bit.\n */\nvoid copy(char to[], char from[])\n{\n    int i = 0;\n\n    // while loop will not end if from isn't '\\0' terminated\n    while((to[i] = from[i]) != '\\0') {\n        ++i;\n    }\n}\n\n/*\n * A safer version that checks for many common errors using the\n * length of each string to control the loops and termination.\n */\nint safercopy(int from_len, char *from, int to_len, char *to)\n{\n    assert(from != NULL && to != NULL && \"from and to can't be NULL\");\n    int i = 0;\n    int max = from_len > to_len - 1 ? to_len - 1 : from_len;\n\n    // to_len must have at least 1 byte\n    if(from_len < 0 || to_len <= 0) return -1;\n\n    for(i = 0; i < max; i++) {\n        to[i] = from[i];\n    }\n\n    to[to_len - 1] = '\\0';\n\n    return i;\n}\n\n\nint main(int argc, char *argv[])\n{\n    // careful to understand why we can get these sizes\n    char from[] = \"0123456789\";\n    int from_len = sizeof(from);\n\n    // notice that it's 7 chars + \\0\n    char to[] = \"0123456\";\n    int to_len = sizeof(to);\n\n    debug(\"Copying '%s':%d to '%s':%d\", from, from_len, to, to_len);\n\n    int rc = safercopy(from_len, from, to_len, to);\n    check(rc > 0, \"Failed to safercopy.\");\n    check(to[to_len - 1] == '\\0', \"String not terminated.\");\n\n    debug(\"Result is: '%s':%d\", to, to_len);\n\n    // now try to break it\n    rc = safercopy(from_len * -1, from, to_len, to);\n    check(rc == -1, \"safercopy should fail #1\");\n    check(to[to_len - 1] == '\\0', \"String not terminated.\");\n\n    rc = safercopy(from_len, from, 0, to);\n    check(rc == -1, \"safercopy should fail #2\");\n    check(to[to_len - 1] == '\\0', \"String not terminated.\");\n\n    return 0;\n\nerror:\n    return 1;\n}\n```\n\n`copy`函数是典型的C代码，而且它是大量缓冲区溢出的来源。它有缺陷，因为它总是假设接受到的是合法的C字符串（带有`'\\0'`），并且只是用一个`while`循环来处理。问题是，确保这些是十分困难的，并且如果没有处理好，它会使`while`循环无限执行。编写可靠代码的一个要点就是，不要编写可能不会终止的循环。\n\n`safecopy`函数尝试通过要求调用者提供两个字符串的长度来解决问题。它可以执行有关这些字符串的、`copy`函数不具备的特定检查。他可以保证长度正确，`to`字符串具有足够的容量，以及它总是可终止。这个函数不像`copy`函数那样可能会永远执行下去。\n\n这个就是永远不信任输入的实例。如果你假设你的函数要接受一个没有终止标识的字符串（通常是这样），你需要设计你的函数，不要依赖字符串本身。如果你想让参数不为`NULL`，你应该对此做检查。如果大小应该在正常范围内，也要对它做检查。你只需要简单假设调用你代码的人会把它弄错，并且使他们更难破坏你的函数。\n\n这个可以扩展到从外部环境获取输入的的软件。程序员著名的临终遗言是，“没人会这样做。”我看到他们说了这句话后，第二天有人就这样做，黑掉或崩溃它们的应用。如果你说没有人会这样做，那就加固代码来保证他们不会简单地黑掉你的应用。你会因所做的事情而感到高兴。\n\n这种行为会出现收益递减。下面是一个清单，我会尝试对我用C写的每个函数做如下工作：\n\n+ 对于每一个参数定义它的先决条件，以及这个条件是否导致失效或返回错误值。如果你在编写一个库，比起失效要更倾向于错误。\n+ 对于每个先决条件，使用`assert(test && \"message\");`在最开始添加`assert`检查。这句代码会执行检查，失败时OS通常会打印断言行，通常它包括信息。当你尝试弄清`assert`为什么在这里时，这会非常有用。\n+ 对于其它先决条件，返回错误代码或者使用我的`check`宏来执行它并且提供错误信息。我在这个例子中没有使用`check`，因为它会混淆比较。\n+ 记录为什么存在这些先决条件，当一个程序员碰到错误时，他可以弄清楚这些是否是真正必要的。\n+ 如果你修改了输入，确保当函数退出或中止时它们也会正确产生。\n+ 总是要检查所使用的函数的错误代码。例如，人们有时会忘记检查`fopen`或`fread`的返回代码，这会导致他们在错误下仍然使用这个资源。这会导致你的程序崩溃或者易受攻击。\n+ 你也需要返回一致的错误代码，以便对你的每个函数添加相同的机制。一旦你熟悉了这一习惯，你就会明白为什么我的`check`宏这样工作。\n\n只是这些微小的事情就会改进你的资源处理方式，并且避免一大堆错误。\n\n### 避免错误\n\n上一个例子中你可能会听到别人说，“程序员不会经常错误地使用`copy`。”尽管大量攻击都针对这类函数，他们仍旧相信这种错误的概率非常低。概率是个很有趣的事情，因为人们不擅长猜测所有事情的概率，这非常难以置信。然而人们对于判断一个事情是否可能，是很擅长的。他们可能会说`copy`中的错误不常见，但是无法否认它可能发生。\n\n关键的原因是对于一些常见的事情，它首先是可能的。判断可能性非常简单，因为我们都知道事情如何发生。但是随后判断出概率就不是那么容易了。人们错误使用`copy`的情况会占到20%、10%，或1%？没有人知道。为了弄清楚你需要收集证据，统计许多软件包中的错误率，并且可能需要调查真实的程序员如何使用这个函数。\n\n这意味着，如果你打算避免错误，你不需要尝试避免可能发生的事情，而是要首先集中解决概率最大的事情。解决软件所有可能崩溃的方式并不可行，但是你可以尝试一下。同时，如果你不以最少的努力解决最可能发生的事件，你就是在不相关的风险上浪费时间。\n\n下面是一个决定避免什么的处理过程：\n\n+ 列出所有可能发生的错误，无论概率大小，并带着它们的原因。不要列出外星人可能会监听内存来偷走密码这样的事情。\n+ 评估每个的概率，使用危险行为的百分比来表示。如果你处理来自互联网的情况，那么则为可能出现错误的请求的百分比。如果是函数调用，那么它是出现错误的函数调用百分比。\n+ 评估每个的工作量，使用避免它所需的代码量或工作时长来表示。你也可以简单给它一个“容易”或者“难”的度量。当需要修复的简单错误仍在列表上时，任何这种度量都可以让你避免做无谓的工作。\n+ 按照工作量（低到高）和概率（高到低）排序，这就是你的任务列表。\n+ 之后避免你在列表中列出的任何错误，如果你不能消除它的可能性，要降低它的概率。\n+ 如果存在你不能修复的错误，记录下来并提供给可以修复的人。\n\n这一微小的过程会产生一份不错的待办列表。更重要的是，当有其它重要的事情需要解决时，它让你远离劳而无功。你也可以更正式或更不正式地处理这一过程。如果你要完成整个安全审计，你最好和团队一起做，并且有个更详细的电子表格。如果你只是编写一个函数，简单地复查代码之后划掉它们就够了。最重要的是你要停止假设错误不会发生，并且着力于消除它们，这样就不会浪费时间。\n\n### 过早暴露错误\n\n如果你遇到C中的错误，你有两个选择：\n\n+ 返回错误代码。\n+ 中止进程。\n\n这就是处理方法，你需要执行它来确保错误尽快发生，记录清楚，提供错误信息，并且易于程序员来避免它。这就是我提供的`check`宏这样工作的原因。对于每一个错误，你都要让它你打印信息、文件名和行号，并且强制返回错误代码。如果你使用了我的宏，你会以正确的方式做任何事情。\n\n我倾向于返回错误代码而不是终止程序。如果出现了大错误我会中止程序，但是实际上我很少碰到大错误。一个需要中止程序的很好例子是，我获取到了一个无效的指针，就像`safecopy`中那样。我没有让程序在某个地方产生“段错误”，而是立即捕获并中止。但是，如果传入`NULL`十分普遍，我可能会改变方式而使用`check`来检查，以保证调用者可以继续运行。\n\n然而在库中，我尽我最大努力永不中止。使用我的库的软件可以决定是否应该中止。如果这个库使用非常不当，我才会中止程序。\n\n最后，关于“暴露”的一大部分内容是，不要对多于一个错误使用相同的信息或错误代码。你通常会在外部资源的错误中见到这种情况。比如一个库捕获了套接字上的错误，之后简单报告“套接字错误”。它应该做的是返回具体的信息，比如套接字上发生了什么错误，使它可以被合理地调试和修复。当你设计错误报告时，确保对于不同的错误你提供了不同的错误消息。\n\n### 记录假设\n\n如果你遵循并执行了这个建议，你就构建了一份“契约”，关于函数期望这个世界是什么样子。你已经为每个参数预设了条件，处理潜在的错误，并且优雅地产生失败。下一步是完善这一契约，并且添加“不变量”和“后置条件”。\n\n不变量就是在函数运行时，一些场合下必须恒为真的条件。这对于简单的函数并不常见，但是当你处理复杂的结构时，它会变得很必要。一个关于不变量的很好的例子是，结构体在使用时都会合理地初始化。另一个是有序的数据结构在处理时总是排好序的。\n\n后置条件就是退出值或者函数运行结果的保证。这可以和不变了混在一起，但是也可以是一些很简单的事情，比如“函数应总是返回0，或者错误时返回-1”。通常这些都有文档记录，但是如果你的函数返回一个分配的资源，你应该添加一个后置条件，做检查来确保它返回了一个不为`NULL`的东西。或者，你可以使用`NULL`来表示错误，这种情况下，你的后置条件就是资源在任何错误时都会被释放。\n\n在C编程中，不变量和后置条件都通常比实际的代码和断言更加文档化。处理它们的最好当时就是尽可能添加`assert`调用，之后记录剩下的部分。如果你这么做了，当其它人碰到错误时，他们可以看到你在编写函数时做了什么假设。\n\n### 避免过多文档\n\n程序员编写代码时的一个普遍问题，就是他们会记录一个普遍的bug，而不是简单地修复它。我最喜欢的方式是，Ruby on Rails系统只是简单地假设所有月份都有30天。日历太麻烦了，所以与其修复它，不如在一些地方放置一个小的注释，说这是故意的，并且几年内都不会改正。每次一些人试图抱怨它时，他们都会说，“文档里面都有！”\n\n如果你能够实际修复问题，文档并不重要，并且，如果函数具有严重的缺陷，你在修复它之前可以不记录它。在Ruby on Rails的例子中，不包含日期函数会更好一些，而不是包含一个没人会用的错误的函数。\n\n当你为防御性编程执行清理时，尽可能尝试修复任何事情。如果你发现你记录了越来越多的，你不能修复的事情，需要考虑重新设计特性，或简单地移除它。如果你真的需要保留这一可怕的错误的特性，那么我建议你编写它、记录它，并且在你受责备之前找一份新的工作。\n\n### 使一切自动化\n\n你是个程序员，这意味着你的工作是通过自动化消灭其它人的工作。它的终极目标是使用自动化来使你自己也失业。很显然你不应该完全消除你做的东西，但如果你花了一整天在终端上重复运行手动测试，你的工作就不是编程。你只是在做QA，并且你应该使自己自动化，消除这个你可能并不是真的想干的QA工作。\n\n实现它的最简单方式就是编写自动化测试，或者单元测试。这本书里我打算讲解如何使它更简单，并且我会避免多数编写测试的信条。我只会专注于如何编写它们，测试什么，以及如何使测试更高效。\n\n下面是程序员没有但是应该自动化的一些事情：\n\n+ 测试和校验。\n+ 构建过程。\n+ 软件部署。\n+ 系统管理。\n+ 错误报告。\n\n尝试花一些时间在自动化上面，你会有更多的时间用来处理一些有趣的事情。或者，如果这对你来说很有趣，也许你应该编写自动化完成这些事情的软件。\n\n### 简单化和清晰化\n\n“简单性”的概念对许多人来说比较微妙，尤其是一些聪明人。它们通常将“内涵”与“简单性”混淆起来。如果他们很好地理解了它，很显然非常简单。简单性的测试是通过将一个东西与比它更简单的东西比较。但是，你会看到编写代码的人会使用最复杂的、匪夷所思的数据结构，因为它们认为做同样事情的简单版本非常“恶心”。对复杂性的爱好是程序员的弱点。\n\n你可以首先通过告诉自己，“简单和清晰并不恶心，无论谁在干什么事情”来战胜这一弱点。如果其它人编写了愚蠢的观察者模式涉及到19个类，12个接口，而你只用了两个字符串操作就可以实现它，那么你赢了。他们就是错了，无论他们认为自己的复杂设计有多么高大上。\n\n对于要使用哪个函数的最简单测试是：\n\n+ 确保所有函数都没有问题。如果它有错误，它有多快或多简单就不重要了。\n+ 如果你不能修复问题，就选择另外一个。\n+ 它们会产生相同结果嘛？如果不是就挑选具有所需结果的函数。\n+ 如果它们会产生相同结果，挑选包含更少特性，更少分支的那个，或者挑选你认为最简单的那个。\n+ 确保你没有只是挑选最具有表现力的那个。无论怎么样，简单和清晰，都会战胜复杂和恶心。\n\n你会注意到，最后我一般会放弃并告诉你根据你的判断。简单性非常讽刺地是一件复杂的事情，所以使用你的品位作为指引是最好的方式。只需要确保在你获取更多经验之后，你会调整你对于什么是“好”的看法。\n\n### 质疑权威\n\n最后一个策略是最重要的，因为它让你突破防御性编程思维，并且让你转换为创造性思维。防御性编程是权威性的，并且比较无情。这一思维方式的任务是让你遵循规则，因为否则你会错失一些东西或心烦意乱。\n\n这一权威性的观点的坏处是扼杀了独立的创造性思维。规则对于完成事情是必要的，但是做它们的奴隶会扼杀你的创造力。\n\n这条最后的策略的意思是你应该周期性地质疑你遵循的规则，并且假设它们都是错误的，就像你之前复查的软件那样。在一段防御性编程的时间之后，我通常会这样做，我会拥有一个不编程的休息并让这些规则消失。之后我会准备好去做一些创造性的工作，或按需做更多的防御型编程。\n\n## 顺序并不重要\n\n在这一哲学上我想说的最后一件事，就是我并不是告诉你要按照一个严格的规则，比如“创造！防御！创造！防御！”去做这件事。最开始你可能想这样做，但是我实际上会做不等量的这些事情，取决于我想做什么，并且我可能会将二者融合到一起，没有明确的边界。\n\n我也不认为其中一种思维会优于另一种，或者它们之间有严格的界限。你需要在编程上既有创造力也要严格，所以如果想要提升的话，需要同时做到它们。\n\n## 附加题\n\n+ 到现在为止（以及以后）书中的代码都可能违反这些规则。回退并挑选一个练习，将你学到的应用在它上面，来看看你能不能改进它或发现bug。\n+ 寻找一个开源项目，对其中一些文件进行类似的代码复查。如果你发现了bug，提交一个补丁来修复它。\n"
  },
  {
    "path": "docs/lcthw-zh/ex28.md",
    "content": "# 练习28：Makefile 进阶\n\n> 原文：[Exercise 28: Intermediate Makefiles](http://c.learncodethehardway.org/book/ex28.html)\n\n> 译者：[飞龙](https://github.com/wizardforcel)\n\n在下面的三个练习中你会创建一个项目的目录框架，用于构建之后的C程序。这个目录框架会在这本书中剩余的章节中使用，并且这个练习中我会涉及到`Makefile`便于你理解它。\n\n这个结构的目的是，在不凭借配置工具的情况下，使构建中等规模的程序变得容易。如果完成了它，你会学到很多GNU make和一些小型shell脚本方面的东西。\n\n## 基本的项目结构\n\n首先要做的事情是创建一个C的目录框架，并且放置一些多续项目都拥有的，基本的文件和目录。这是我的目录：\n\n```sh\n$ mkdir c-skeleton\n$ cd c-skeleton/\n$ touch LICENSE README.md Makefile\n$ mkdir bin src tests\n$ cp dbg.h src/   # this is from Ex20\n$ ls -l\ntotal 8\n-rw-r--r--  1 zedshaw  staff     0 Mar 31 16:38 LICENSE\n-rw-r--r--  1 zedshaw  staff  1168 Apr  1 17:00 Makefile\n-rw-r--r--  1 zedshaw  staff     0 Mar 31 16:38 README.md\ndrwxr-xr-x  2 zedshaw  staff    68 Mar 31 16:38 bin\ndrwxr-xr-x  2 zedshaw  staff    68 Apr  1 10:07 build\ndrwxr-xr-x  3 zedshaw  staff   102 Apr  3 16:28 src\ndrwxr-xr-x  2 zedshaw  staff    68 Mar 31 16:38 tests\n$ ls -l src\ntotal 8\n-rw-r--r--  1 zedshaw  staff  982 Apr  3 16:28 dbg.h\n$\n```\n\n之后你会看到我执行了`ls -l`，所以你会看到最终结果。\n\n下面是每个文件所做的事情：\n\n`LICENSE`\n\n如果你在项目中发布源码，你会希望包含一份协议。如果你不这么多，虽然你有代码的版权，但是通常没有人有权使用。\n\n`README.md`\n\n对你项目的简要说明。它以`.md`结尾，所以应该作为Markdown来解析。\n\n`Makefile`\n\n这个项目的主要构建文件。\n\n`bin/`\n\n放置可运行程序的地方。这里通常是空的，Makefile会在这里生成程序。\n\n`build/`\n\n当值库和其它构建组件的地方。通常也是空的，Makefile会在这里生成这些东西。\n\n`src/`\n\n放置源码的地方，通常是`.c`和`.h`文件。\n\n`tests/`\n\n放置自动化测试的地方。\n\n`src/dbg.h`\n\n我将练习20的`dbg.h`复制到了这里。\n\n我刚才分解了这个项目框架的每个组件，所以你应该明白它们怎么工作。\n\n## Makefile\n\n\n我要讲到的第一件事情就是Makefile，因为你可以从中了解其它东西的情况。这个练习的Makeile比之前更加详细，所以我会在你输入它之后做详细的分解。\n\n```make\nCFLAGS=-g -O2 -Wall -Wextra -Isrc -rdynamic -DNDEBUG $(OPTFLAGS)\nLIBS=-ldl $(OPTLIBS)\nPREFIX?=/usr/local\n\nSOURCES=$(wildcard src/**/*.c src/*.c)\nOBJECTS=$(patsubst %.c,%.o,$(SOURCES))\n\nTEST_SRC=$(wildcard tests/*_tests.c)\nTESTS=$(patsubst %.c,%,$(TEST_SRC))\n\nTARGET=build/libYOUR_LIBRARY.a\nSO_TARGET=$(patsubst %.a,%.so,$(TARGET))\n\n# The Target Build\nall: $(TARGET) $(SO_TARGET) tests\n\ndev: CFLAGS=-g -Wall -Isrc -Wall -Wextra $(OPTFLAGS)\ndev: all\n\n$(TARGET): CFLAGS += -fPIC\n$(TARGET): build $(OBJECTS)\n       ar rcs $@ $(OBJECTS)\n       ranlib $@\n\n$(SO_TARGET): $(TARGET) $(OBJECTS)\n       $(CC) -shared -o $@ $(OBJECTS)\n\nbuild:\n       @mkdir -p build\n       @mkdir -p bin\n\n# The Unit Tests\n.PHONY: tests\ntests: CFLAGS += $(TARGET)\ntests: $(TESTS)\n       sh ./tests/runtests.sh\n\nvalgrind:\n       VALGRIND=\"valgrind --log-file=/tmp/valgrind-%p.log\" $(MAKE)\n\n# The Cleaner\nclean:\n       rm -rf build $(OBJECTS) $(TESTS)\n       rm -f tests/tests.log\n       find . -name \"*.gc*\" -exec rm {} \\;\n       rm -rf `find . -name \"*.dSYM\" -print`\n\n# The Install\ninstall: all\n       install -d $(DESTDIR)/$(PREFIX)/lib/\n       install $(TARGET) $(DESTDIR)/$(PREFIX)/lib/\n\n# The Checker\nBADFUNCS='[^_.>a-zA-Z0-9](str(n?cpy|n?cat|xfrm|n?dup|str|pbrk|tok|_)|stpn?cpy|a?sn?printf|byte_)'\ncheck:\n       @echo Files with potentially dangerous functions.\n       @egrep $(BADFUNCS) $(SOURCES) || true\n\n```\n\n要记住你应该使用一致的Tab字符来缩进Makefile。你的编辑器应该知道怎么做，但是如果不是这样你可以换个编辑器。没有程序员会使用一个连如此简单的事情都做不好的编辑器。\n\n## 头部\n\n这个Makefile设计用于构建一个库，我们之后会用到它，并且通过使用`GNU make`的特殊特性使它在任何平台上都可用。我会在这一节拆分它的每一部分，先从头部开始。\n\nMakefile:1\n\n这是通常的`CFLAGS`，几乎每个项目都会设置，但是带有用于构建库的其它东西。你可能需要为不同平台调整它。要注意最后的`OPTFLAGS`变量可以让使用者按需扩展构建选项。\n\nMakefile:2\n\n用于链接库的选项，同样也允许其它人使用`OPTFLAGS`变量扩展链接选项。\n\nMakefile:3\n\n设置一个叫做`PREFIX`的可选变量，它只在没有`PREFIX`设置的平台上运行Makefile时有效。这就是`?=`的作用。\n\nMakefile:5\n\n这神奇的一行通过执行`wildcard`搜索在`src/`中所有`*.c`文件来动态创建`SOURCES`变量。你需要提供`src/**/*.c`和`src/*.c`以便GNU make能够包含`src`目录及其子目录的所有此类文件。\n\nMakefile:6\n\n一旦你创建了源文件列表，你可以使用`patsubst`命令获取`*.c`文件的`SOURCES`来创建目标文件的新列表。你可以告诉`patsubst`把所有`%.c`扩展为`%.o`，并将它们赋给`OBJECTS`。\n\nMakefile:8\n\n再次使用`wildcard`来寻找所有用于单元测试的测试源文件。它们存放在不同的目录中。\n\nMakefile:9\n\n之后使用相同的`patsubst`技巧来动态获得所有`TEST`目标。其中我去掉了`.c`后缀，使整个程序使用相同的名字创建。之前我将`.c`替换为`.o`来创建目标文件。\n\nMakefile:11\n\n最后，我将最终目标设置为`build/libYOUR_LIBRARY.a`，你可以为你实际构建的任何库来修改它。\n\n这就是Makefile的头部了，但是我应该对“让其他人扩展构建”做个解释。你在运行它的时候可以这样做：\n\n```sh\n# WARNING! Just a demonstration, won't really work right now.\n# this installs the library into /tmp\n$ make PREFIX=/tmp install\n# this tells it to add pthreads\n$ make OPTFLAGS=-pthread\n```\n\n如果你传入匹配`Makefile`中相同名称的变量，它们会在构建中生效。你可以利用它来修改`Makefile`的运行方式。第一条命令改变了`PREFIX`，使它安装到`/tmp`。第二条设置了`OPTFLAGS`，为之添加了`pthread`选项。\n\n## 构建目标\n\n我会继续`Makefile`的分解，这一部分用于构建目标文件（object file）和目标（target）：\n\nMakefile:14\n\n要记住在没有提供目标时`make`会默认运行第一个目标。这里它叫做`all:`，并且它提供了`$(TARGET) tests`作为构建目标。查看`TARGET`变量，你会发现这就是库文件，所以`all:`首先会构建出库文件。之后，`tests`目标会构建单元测试。\n\nMakefile:16\n\n另一个用于执行“开发者构建”的目标，它介绍了一种为单一目标修改选项的技巧，如果我执行“开发构建”，我希望`CFLAGS`包含类似`Wextra`这样用于发现bug的选项。如果你将它们放到目标的那行中，并再编写一行来指向原始目标（这里是`all`），那么它就会将改为你设置的选项。我通常将它用于在不同的平台上设置所需的不同选项。\n\nMakefile:19\n\n构建`TARGET`库，然而它同样使用了15行的技巧，向一个目标提供选项来为当前目标修改它们。这里我通过适用`+=`语法为库的构建添加了`-fPIC`。\n\nMakefile:20\n\n现在这一真实目标首先创建`build`目录，之后编译所有`OBJECTS`。\n\nMakefile:21\n\n运行实际创建`TARGET`的`ar`的命令。`$@ $(OBJECTS)`语法的意思是，将当前目标的名称放在这里，并把`OBJECTS`的内容放在后面。这里`$@`的值为19行的`$(TARGET)`，它实际上为`build/libYOUR_LIBRARY.a`。看起来在这一重定向中它做了很多跟踪工作，它也有这个功能，并且你可以通过修改顶部的`TARGET`，来构建一个全新的库。\n\nMakefile:22\n\n最后，在`TARGET`上运行`ranlib`来构建这个库。\n\nMakefile:24-24\n\n用于在`build/`和`bin/`目录不存在的条件下创建它们。之后它被19行引用，那里提供了`build`目标来确保`build/`目录已创建。\n\n你现在拥有了用于构建软件的所需的所有东西。之后我们会创建用于构建和运行单元测试的东西，来执行自动化测试。\n\n## 单元测试\n\nC不同于其他语言，因为它更易于为每个需要测试的东西创建小型程序。一些测试框架试图模拟其他语言中的模块概念，并且执行动态加载，但是它在C中并不适用。这也不是必要的，因为你可以仅仅编写一个程序用于每个测试。\n\n我接下来会涉及到Makefile的这一部分，并且你会看到`test/`目录中真正起作用的内容。\n\nMakefile:29\n\n如果你拥有一个不是“真实”的目标，只有有个目录或者文件叫这个名字，你需要使用g`.PHONY:`标签来标记它，以便`make`忽略该文件。\n\nMakefile:30\n\n我使用了与修改`CFLAGS`变量相同的技巧，并且将`TARGET`添加到构建中，于是每个测试程序都会链接`TARGET`库。这里它会添加`build/libYOUR_LIBRARY.a`用于链接。\n\nMakefile:31\n\n之后我创建了实际的`test:`目录，它依赖于所有在`TESTS`变量中列出的程序。这一行实际上说，“Make，请使用你已知的程序构建方法，以及当前`CFLAGS`设置的内容来构建`TESTS`中的每个程序。”\n\nMakefile:32\n\n最后，所有`TESTS`构建完之后，会运行一个我稍后创建的简单shell脚本，它知道如何全部运行他们并报告它们的输出、这一行实际上运行它来让你看到测试结果。\n\nMakefile:34-35\n\n为了能够动态使用`Valgrind`重复运行测试，我创建了`valgrind:`标签，它设置了正确的变量并且再次运行它。它会将`Valgrind`的日志放到`/tmp/valgrind-*.log`，你可以查看并了解发生了什么。之后`tests/runtests.sh`看到`VALGRIND`变量时，它会明白要在`Valgrind`下运行测试程序。\n\n你需要为单元测试创建一个小型的shell脚本，它知道如何运行程序。我们开始创建这个`tests/runtests.sh`脚本：\n\n```sh\necho \"Running unit tests:\"\n\nfor i in tests/*_tests\ndo\n    if test -f $i\n    then\n        if $VALGRIND ./$i 2>> tests/tests.log\n        then\n            echo $i PASS\n        else\n            echo \"ERROR in test $i: here's tests/tests.log\"\n            echo \"------\"\n            tail tests/tests.log\n            exit 1\n        fi\n    fi\ndone\n\necho \"\"\n```\n\n当我提到单元测试如何工作时，我会在之后用到它。\n\n## 清理工具\n\n我已经有了用于单元测试的工具，所以下一步就是创建需要重置时的清理工具。\n\nMakefile:38\n\n`clean:`目标在我需要清理这个项目的任何时候都会执行清理。\n\nMakefile:39-42\n\n这会清理不同编译器和工具留下的多数垃圾。它也会移除`build/`目录并且使用了一个技巧来清理XCode为调试目的而留下的`*.dSYM`。\n\n如果你碰到了想要执行清理的垃圾，你只需要简单地扩展需要删除的文件列表。\n\n## 安装\n\n然后，我会需要一种安装项目的方法，对`Makefile`来说就是把构建出来的库放到通常的`PREFIX`目录下，它通常是`/usr/local/lib`。\n\nMakefile:45\n\n它会使`install:`依赖于`all:`目录，所以当你运行`make install`之后也会先确保一切都已构建。\n\nMakefile:46\n\n接下来我使用`install`程序来创建`lib`目标的目录。其中我通过使用两个为安装者提供便利的变量，尝试让安装尽可能灵活。`DESTDIR`交给安装者，便于在安全或者特定的目录里执行自己的构建。`PREFIX`在别人想要将项目安装到其它目录而不是`/user/local`时会被使用。\n\nMakefile:47\n\n在此之后我使用`insyall`来实际安装这个库，到它需要安装的地方。\n\n`install`程序的目的是确保这些事情都设置了正确的权限。当你运行`make install`时你通常使用root权限来执行，所以通常的构建过程应为`make && sudo make install`。\n\n## 检查工具\n\n`Makefile`的最后一部分是个额外的部分，我把它包含在我的C项目中用于发现任何使用C中“危险”函数的情况。这些函数是字符串函数和另一些“不保护栈”的函数。\n\nMakefile:50\n\n设置变量，它是个稍大的正则表达式，用于检索类似`strcpy`的危险函数。\n\nMakefile:51\n\n这是`check:`目标，使你能够随时执行检查。\n\nMakefile:52\n\n它只是一个打印信息的方式，使用了`@echo`来告诉`make`不要打印命令，只需打印输出。\n\nMakefile:53\n\n对源文件运行`egrep`命令来寻找任何危险的字符串。最后的`|| true`是一种方法，用于防止`make`认为`egrep`没有找到任何东西是执行失败。\n\n当你执行它之后，它会表现得十分奇怪，如果没有任何危险的函数，你会得到一个错误。\n\n## 你会看到什么\n\n我在完成这个项目框架目录的构建之前，还设置了两个额外的练习。下面这是我对`Makefile`特性的测试结果：\n\n```sh\n$ make clean\nrm -rf build  \nrm -f tests/tests.log\nfind . -name \"*.gc*\" -exec rm {} \\;\nrm -rf `find . -name \"*.dSYM\" -print`\n$ make check\nFiles with potentially dangerous functions.\n^Cmake: *** [check] Interrupt: 2\n\n$ make\nar rcs build/libYOUR_LIBRARY.a\nar: no archive members specified\nusage:  ar -d [-TLsv] archive file ...\n      ar -m [-TLsv] archive file ...\n      ar -m [-abiTLsv] position archive file ...\n      ar -p [-TLsv] archive [file ...]\n      ar -q [-cTLsv] archive file ...\n      ar -r [-cuTLsv] archive file ...\n      ar -r [-abciuTLsv] position archive file ...\n      ar -t [-TLsv] archive [file ...]\n      ar -x [-ouTLsv] archive [file ...]\nmake: *** [build/libYOUR_LIBRARY.a] Error 1\n$ make valgrind\nVALGRIND=\"valgrind --log-file=/tmp/valgrind-%p.log\" make\nar rcs build/libYOUR_LIBRARY.a\nar: no archive members specified\nusage:  ar -d [-TLsv] archive file ...\n      ar -m [-TLsv] archive file ...\n      ar -m [-abiTLsv] position archive file ...\n      ar -p [-TLsv] archive [file ...]\n      ar -q [-cTLsv] archive file ...\n      ar -r [-cuTLsv] archive file ...\n      ar -r [-abciuTLsv] position archive file ...\n      ar -t [-TLsv] archive [file ...]\n      ar -x [-ouTLsv] archive [file ...]\nmake[1]: *** [build/libYOUR_LIBRARY.a] Error 1\nmake: *** [valgrind] Error 2\n$\n```\n\n当我运行`clean:`目标时它会生效，但是由于我在`src/`目录中并没有任何源文件，其它命令并没有真正起作用。我会在下个练习中补完它。\n\n## 附加题\n\n+ 尝试通过将源文件和头文件添加进`src/`，来使`Makefile`真正起作用，并且构建出库文件。在源文件中不应该需要`main`函数。\n+ 研究`check:`目标会使用`BADFUNCS`的正则表达式来寻找什么函数。\n+ 如果你没有做过自动化测试，查询有关资料为以后做准备。\n"
  },
  {
    "path": "docs/lcthw-zh/ex29.md",
    "content": "# 练习29：库和链接\n\n> 原文：[Exercise 29: Libraries And Linking](http://c.learncodethehardway.org/book/ex29.html)\n\n> 译者：[飞龙](https://github.com/wizardforcel)\n\nC语言编程的核心能力之一就是链接OS所提供的库。链接是一种为你的程序添加额外特性的方法，这些特性由其它人在系统中创建并打包。你已经使用了一些自动包含的标准库，但是我打算对库的不同类型和它们的作用做个解释。\n\n首先，库在每个语言中都没有良好的设计。我不知道为什么，但是似乎语言的设计者都将链接视为不是特别重要的东西。它们通常令人混乱，难以使用，不能正确进行版本控制，并以不同的方式链接到各种地方。\n\nC没有什么不同，但是C中的库和链接是Unix操作系统的组件，并且可执行的格式在很多年前就设计好了。学习C如何链接库有助于理解OS如何工作，以及它如何运行你的程序。\n\nC中的库有两种基本类型：\n\n静态\n\n你可以使用`ar`和`ranlib`来构建它，就像上个练习中的`libYOUR_LIBRARY.a`那样（Windows下后缀为`.lib`）。这种库可以当做一系列`.o`对象文件和函数的容器，以及当你构建程序时，可以当做是一个大型的`.o`文件。\n\n动态\n\n它们通常以`.so`（Linux）或`.dll`（Windows）结尾。在OSX中，差不多有一百万种后缀，取决于版本和编写它的人。严格来讲，OSX中的`.dylib`，`.bundle`和`framework`这三个之间没什么不同。这些文件都被构建好并且放置到指定的地方。当你运行程序时，OS会动态加载这些文件并且“凭空”链接到你的程序中。\n\n我倾向于对小型或中型项目使用静态的库，因为它们易于使用，并且工作在在更多操作系统上。我也喜欢将所有代码放入静态库中，之后链接它来执行单元测试，或者链接到所需的程序中。\n\n动态库适用于大型系统，它的空间十分有限，或者其中大量程序都使用相同的功能。这种情况下不应该为每个程序的共同特性静态链接所有代码，而是应该将它放到动态库中，这样它仅仅会为所有程序加载一份。\n\n在上一个练习中，我讲解了如何构建静态库（`.a`），我会在本书的剩余部分用到它。这个练习中我打算向你展示如何构建一个简单的`.so`库，并且如何使用Unix系统的`dlopen`动态加载它。我会手动执行它，以便你可以理解每件实际发生的事情。之后，附加题这部分会使用c项目框架来创建它。\n\n## 动态加载动态库\n\n我创建了两个源文件来完成它。一个用于构建`libex29.so`库，另一个是个叫做`ex29`的程序，它可以加载这个库并运行其中的程序：\n\n```c\n#include <stdio.h>\n#include <ctype.h>\n#include \"dbg.h\"\n\n\nint print_a_message(const char *msg)\n{\n    printf(\"A STRING: %s\\n\", msg);\n\n    return 0;\n}\n\n\nint uppercase(const char *msg)\n{\n    int i = 0;\n\n    // BUG: \\0 termination problems\n    for(i = 0; msg[i] != '\\0'; i++) {\n        printf(\"%c\", toupper(msg[i]));\n    }\n\n    printf(\"\\n\");\n\n    return 0;\n}\n\nint lowercase(const char *msg)\n{\n    int i = 0;\n\n    // BUG: \\0 termination problems\n    for(i = 0; msg[i] != '\\0'; i++) {\n        printf(\"%c\", tolower(msg[i]));\n    }\n\n    printf(\"\\n\");\n\n    return 0;\n}\n\nint fail_on_purpose(const char *msg)\n{\n    return 1;\n}\n```\n\n这里面没什么神奇之处。其中故意留了一些bug，看你是否注意到了。你需要在随后修复它们。\n\n我们将要使用`dlopen`，`dlsym`，和`dlclose`函数来处理上面的函数。\n\n```c\n#include <stdio.h>\n#include \"dbg.h\"\n#include <dlfcn.h>\n\ntypedef int (*lib_function)(const char *data);\n\n\nint main(int argc, char *argv[])\n{\n    int rc = 0;\n    check(argc == 4, \"USAGE: ex29 libex29.so function data\");\n\n    char *lib_file = argv[1];\n    char *func_to_run = argv[2];\n    char *data = argv[3];\n\n    void *lib = dlopen(lib_file, RTLD_NOW);\n    check(lib != NULL, \"Failed to open the library %s: %s\", lib_file, dlerror());\n\n    lib_function func = dlsym(lib, func_to_run);\n    check(func != NULL, \"Did not find %s function in the library %s: %s\", func_to_run, lib_file, dlerror());\n\n    rc = func(data);\n    check(rc == 0, \"Function %s return %d for data: %s\", func_to_run, rc, data);\n\n    rc = dlclose(lib);\n    check(rc == 0, \"Failed to close %s\", lib_file);\n\n    return 0;\n\nerror:\n    return 1;\n}\n```\n\n我现在会拆分这个程序，便于你理解这一小段代码其中的原理。\n\nex29.c:5\n\n我随后会使用这个函数指针定义，来调用库中的函数。这没什么新东西，确保你理解了它的作用。\n\nex29.c:17\n\n在为一个小型程序做必要的初始化后，我使用了`dlopen`函数来加载由`lib_file`表示的库。这个函数返回一个句柄，我们随后会用到它，就像来打开文件那样。\n\nex29.c:18\n\n如果出现错误，我执行了通常的检查并退出，但是要注意最后我使用了`dlerror`来查明发生了什么错误。\n\nex29.c:20\n\n我使用了`dlsym`来获取`lib`中的函数，通过它的字面名称`func_to_run`。这是最强大的部分，因为我动态获取了一个函数指针，基于我从命令行`argv`获得的字符串。\n\nex29.c:23\n\n接着我调用`func`函数，获得返回值并进行检查。\n\nex29.c:26\n\n最后，我像关闭文件那样关闭了库。通常你需要在程序的整个运行期间保证它们打开，所以关闭操作并不非常实用，我只是在这里演示它。\n\n> 译者注：由于能够使用系统调用加载，动态库可以被多种语言的程序调用，而静态库只能被C及兼容C的程序调用。\n\n## 你会看到什么\n\n既然你已经知道这些文件做什么了，下面是我的shell会话，用于构建`libex29.so`和`ex29`并随后运行它。下面的代码中你可以学到如何手动构建：\n\n```sh\n# compile the lib file and make the .so\n# you may need -fPIC here on some platforms. add that if you get an error\n$ cc -c libex29.c -o libex29.o\n$ cc -shared -o libex29.so libex29.o\n\n# make the loader program\n$ cc -Wall -g -DNDEBUG ex29.c -ldl -o ex29\n\n# try it out with some things that work\n$ ex29 ./libex29.so print_a_message \"hello there\"\n-bash: ex29: command not found\n$ ./ex29 ./libex29.so print_a_message \"hello there\"\nA STRING: hello there\n$ ./ex29 ./libex29.so uppercase \"hello there\"\nHELLO THERE\n$ ./ex29 ./libex29.so lowercase \"HELLO tHeRe\"\nhello there\n$ ./ex29 ./libex29.so fail_on_purpose \"i fail\"\n[ERROR] (ex29.c:23: errno: None) Function fail_on_purpose return 1 for data: i fail\n\n# try to give it bad args\n$ ./ex29 ./libex29.so fail_on_purpose\n[ERROR] (ex29.c:11: errno: None) USAGE: ex29 libex29.so function data\n\n# try calling a function that is not there\n$ ./ex29 ./libex29.so adfasfasdf asdfadff\n[ERROR] (ex29.c:20: errno: None) Did not find adfasfasdf\n  function in the library libex29.so: dlsym(0x1076009b0, adfasfasdf): symbol not found\n\n# try loading a .so that is not there\n$ ./ex29 ./libex.so adfasfasdf asdfadfas\n[ERROR] (ex29.c:17: errno: No such file or directory) Failed to open\n    the library libex.so: dlopen(libex.so, 2): image not found\n$\n```\n\n需要注意，你可能需要在不同OS、不同OS的不同版本，以及不同OS的不同版本的不同编译器上执行构建，则需要修改构建共享库的方式。如果我构建`libex29.so`的方式在你的平台上不起作用，请告诉我，我会为其它平台添加一些注解。\n\n> 译者注：到处编写、到处调试、到处编译、到处发布。--vczh\n\n&zwj;\n\n> 注\n\n> 有时候你像往常一样运行`cc -Wall -g -DNDEBUG -ldl ex29.c -o ex29`，并且认为它能够正常工作，但是没有。在一些平台上，参数的顺序会影响到它是否生效，这也没什么理由。在Debian或者Ubuntu中你需要执行`cc -Wall -g -DNDEBUG ex29.c -ldl -o ex29`，这是唯一的方式。所以虽然我在这里使用了OSX，但是以后如果你链接动态库的时候它找不到某个函数，要试着自己解决问题。\n\n> 这里面比较麻烦的事情是，实际平台的不同会影响到命令参数的顺序。将`-ldl`放到某个位置没有理由与其它位置不同。它只是一个选项，还需要了解这些简直是太气人了。\n\n## 如何使它崩溃\n\n打开`lbex29.so`，并且使用能够处理二进制的编辑器编辑它。修改一些字节，然后关闭。看看你是否能使用`dlopen`函数来打开它，即使你修改了它。\n\n## 附加题\n\n+ 你注意到我在`libex29.c`中写的不良代码了吗？我使用了一个`for`循环来检查`'\\0'`的结尾，修改它们使这些函数总是接收字符串长度，并在函数内部使用。\n+ 使用项目框架目录，来为这个练习创建新的项目。将`libex29.c`放入`src/`目录，修改`Makefile`使它能够构建`build/libex29.so`。\n+ 将`ex29.c`改为`tests/ex29_tests.c`，使它做为单元测试执行。使它能够正常工作，意思是你需要修改它让它加载`build/libex29.so`文件，并且运行上面我手写的测试。\n+ 阅读`man dlopen`文档，并且查询所有有关函数。尝试`dlopen`的其它选项，比如`RTLD_NOW`。\n"
  },
  {
    "path": "docs/lcthw-zh/ex3.md",
    "content": "# 练习3：格式化输出\n\n> 原文：[Exercise 3: Formatted Printing](http://c.learncodethehardway.org/book/ex3.html)\n\n> 译者：[飞龙](https://github.com/wizardforcel)\n\n不要删除Makefile，因为它可以帮你指出错误，以及当我们需要自动化处理一些事情时，可以向它添加新的东西。\n\n许多编程语言都使用了C风格的格式化输出，所以让我们尝试一下：\n\n```c\n#include <stdio.h>\n\nint main()\n{\n    int age = 10;\n    int height = 72;\n\n    printf(\"I am %d years old.\\n\", age);\n    printf(\"I am %d inches tall.\\n\", height);\n\n    return 0;\n}\n```\n\n写完之后，执行通常的`make ex3`命令来构建并运行它。一定要确保你处理了所有的警告。\n\n这个练习的代码量很小，但是信息量很大，所以让我们逐行分析一下：\n\n+ 首先你包含了另一个头文件叫做`stdio.h`。这告诉了编译器你要使用“标准的输入/输出函数”。它们之一就是`printf`。\n+ 然后你使用了一个叫`age`的变量并且将它设置为10。\n+ 接着你使用了一个叫`height`的变量并且设置为72。\n+ 再然后你使用`printf`函数来打印这个星球上最高的十岁的人的年龄和高度。\n+ 在`printf`中你会注意到你传入了一个字符串，这就是格式字符串，和其它语言中一样。\n+ 在格式字符串之后，你传入了一些变量，它们应该被`printf`“替换”进格式字符串中。\n\n这些语句的结果就是你用`printf`处理了一些变量，并且它会构造出一个新的字符串，之后将它打印在终端上。\n\n## 你会看到什么\n\n当你做完上面的整个步骤，你应该看到这些东西：\n\n```sh\n$ make ex3\ncc -Wall -g    ex3.c   -o ex3\n$ ./ex3\nI am 10 years old.\nI am 72 inches tall.\n$\n```\n\n不久之后我会停下来让你运行`make`，并且告诉你构建过程是什么样子的。所以请确保你正确得到了这些信息并且能正常执行。\n\n## 外部研究\n\n在附加题一节我可能会让你自己查找一些资料，并且弄明白它们。这对于一个自我学习的程序员来说相当重要。如果你一直在自己尝试了解问题之前去问其它人，你永远都不会学到独立解决问题。这会让你永远都不会在自己的技能上建立信心，并且总是依赖别人去完成你的工作。\n\n打破你这一习惯的方法就是强迫你自己先试着自己回答问题，并且确认你的回答是正确的。你可以通过打破一些事情，用实验验证可能的答案，以及自己进行研究来完成它。\n\n对于这个练习，我想让你上网搜索`printf`的所有格式化占位符和转义序列。转义序列类似`\\n`或者`\\r`，可以让你分别打印新的一行或者 tab 。格式化占位符类似`%s`或者`%d`，可以让你打印字符串或整数。找到所有的这些东西，以及如何修改它们，和可设置的“精度”和宽度的种类。\n\n从现在开始，这些任务会放到附加题里面，你应该去完成它们。\n\n## 如何使它崩溃\n\n尝试下面的一些东西来使你的程序崩溃，在你的电脑上它们可能会崩溃，也可能不会。\n\n+ 从第一个`printf`中去掉`age`并重新编译，你应该会得到一大串的警告。\n+ 运行新的程序，它会崩溃，或者打印出奇怪的年龄。\n+ 将`printf`恢复原样，并且去掉`age`的初值，将那一行改为`int age;`，之后重新构建并运行。\n\n```sh\n# edit ex3.c to break printf\n$ make ex3\ncc -Wall -g    ex3.c   -o ex3\nex3.c: In function 'main':\nex3.c:8: warning: too few arguments for format\nex3.c:5: warning: unused variable 'age'\n$ ./ex3\nI am -919092456 years old.\nI am 72 inches tall.\n# edit ex3.c again to fix printf, but don't init age\n$ make ex3\ncc -Wall -g    ex3.c   -o ex3\nex3.c: In function 'main':\nex3.c:8: warning: 'age' is used uninitialized in this function\n$ ./ex3\nI am 0 years old.\nI am 72 inches tall.\n$\n```\n\n## 附加题\n\n+ 找到尽可能多的方法使`ex3`崩溃。\n+ 执行`man 3 printf`来阅读其它可用的'%'格式化占位符。如果你在其它语言中使用过它们，应该看着非常熟悉（它们来源于`printf`）。\n+ 将`ex3`添加到你的`Makefile`的`all`列表中。到目前为止，可以使用`make clean all`来构建你所有的练习。\n+ 将`ex3`添加到你的`Makefile`的`clean`列表中。当你需要的时候使用`make clean`可以删除它。\n"
  },
  {
    "path": "docs/lcthw-zh/ex30.md",
    "content": "# 练习30：自动化测试\n\n> 原文：[Exercise 30: Automated Testing](http://c.learncodethehardway.org/book/ex30.html)\n\n> 译者：[飞龙](https://github.com/wizardforcel)\n\n自动化测试经常用于例如Python和Ruby的其它语言，但是很少用于C。一部分原因是自动化加载和测试C的代码片段具有较高的难度。这一章中，我们会创建一个非常小型的测试“框架”，并且使用你的框架目录构建测试用例的示例。\n\n我接下来打算使用，并且你会包含进框架目录的框架，叫做“minunit”，它以[Jera Design](http://www.jera.com/techinfo/jtns/jtn002.html)所编写的一小段代码作为开始，之后我扩展了它，就像这样：\n\n```c\n#undef NDEBUG\n#ifndef _minunit_h\n#define _minunit_h\n\n#include <stdio.h>\n#include <dbg.h>\n#include <stdlib.h>\n\n#define mu_suite_start() char *message = NULL\n\n#define mu_assert(test, message) if (!(test)) { log_err(message); return message; }\n#define mu_run_test(test) debug(\"\\n-----%s\", \" \" #test); \\\n    message = test(); tests_run++; if (message) return message;\n\n#define RUN_TESTS(name) int main(int argc, char *argv[]) {\\\n    argc = 1; \\\n    debug(\"----- RUNNING: %s\", argv[0]);\\\n        printf(\"----\\nRUNNING: %s\\n\", argv[0]);\\\n        char *result = name();\\\n        if (result != 0) {\\\n            printf(\"FAILED: %s\\n\", result);\\\n        }\\\n        else {\\\n            printf(\"ALL TESTS PASSED\\n\");\\\n        }\\\n    printf(\"Tests run: %d\\n\", tests_run);\\\n        exit(result != 0);\\\n}\n\n\nint tests_run;\n\n#endif\n```\n\n原始的内容所剩不多了，现在我使用`dbg.h`宏，并且在模板测试运行器的末尾创建了大量的宏。在这小段代码中我们创建了整套函数单元测试系统，一旦它结合上shell脚本来运行测试，你可以将其用于你的C代码。\n\n## 完成测试框架\n\n为了基础这个练习，你应该让你的`src/libex29.c`正常工作，并且完成练习29的附加题，是`ex29.c`加载程序并合理运行。练习29中我这事了一个附加题来使它像单元测试一样工作，但是现在我打算重新想你展示如何使用`minunit.h`来做这件事。\n\n首先我们需要创建一个简单的空单元测试，命名为`tests/libex29_tests.c`，在里面输入：\n\n```c\n#include \"minunit.h\"\n\nchar *test_dlopen()\n{\n\n    return NULL;\n}\n\nchar *test_functions()\n{\n\n    return NULL;\n}\n\nchar *test_failures()\n{\n\n    return NULL;\n}\n\nchar *test_dlclose()\n{\n\n    return NULL;\n}\n\nchar *all_tests() {\n    mu_suite_start();\n\n    mu_run_test(test_dlopen);\n    mu_run_test(test_functions);\n    mu_run_test(test_failures);\n    mu_run_test(test_dlclose);\n\n    return NULL;\n}\n\nRUN_TESTS(all_tests);\n```\n\n这份代码展示了`tests/minunit.h`中的`RUN_TESTS`宏，以及如何使用其他的测试运行器宏。我没有编写实际的测试函数，所以你只能看到单元测试的结构。我首先会分解这个文件：\n\nlibex29_tests.c:1\n\n包含`minunit.h`框架。\n\nlibex29_tests.c:3-7\n\n第一个测试。测试函数具有固定的结构，它们不带任何参数并且返回`char *`，成功时为`NULL`。这非常重要，因为其他宏用于向测试运行器返回错误信息。\n\nlibex29_tests.c:9-25\n\n与第一个测试相似的更多测试。\n\nlibex29_tests.c:27\n\n控制其他测试的运行器函数。它和其它测试用例格式一致，但是使用额外的东西来配置。\n\nlibex29_tests.c:28\n\n为`mu_suite_start`测试设置一些通用的东西。\n\nlibex29_tests.c:30\n\n这就是使用`mu_run_test`返回结果的地方。\n\nlibex29_tests.c:35\n\n在你运行所有测试之后，你应该返回`NULL`，就像普通的测试函数一样。\n\nlibex29_tests.c:38\n\n最后需要使用`RUN_TESTS`宏来启动`main`函数，让它运行`all_tests`启动器。\n\n这就是用于运行测试所有代码了，现在你需要尝试使它运行在项目框架中。下面是我的执行结果：\n\n```sh\nnot printable\n```\n\n我首先执行`make clean`，之后我运行了构建，它将模板改造为`libYOUR_LIBRARY.a`和`libYOUR_LIBRARY.so`文件。要记住你需要在练习29的附加题中完成它。但如果你没有完成的话，下面是我所使用的`Makefile`的文件差异：\n\n```diff\ndiff --git a/code/c-skeleton/Makefile b/code/c-skeleton/Makefile\nindex 135d538..21b92bf 100644\n--- a/code/c-skeleton/Makefile\n+++ b/code/c-skeleton/Makefile\n@@ -9,9 +9,10 @@ TEST_SRC=$(wildcard tests/*_tests.c)\n TESTS=$(patsubst %.c,%,$(TEST_SRC))\n\n TARGET=build/libYOUR_LIBRARY.a\n+SO_TARGET=$(patsubst %.a,%.so,$(TARGET))\n\n # The Target Build\n-all: $(TARGET) tests\n+all: $(TARGET) $(SO_TARGET) tests\n\n dev: CFLAGS=-g -Wall -Isrc -Wall -Wextra $(OPTFLAGS)\n dev: all\n@@ -21,6 +22,9 @@ $(TARGET): build $(OBJECTS)\n         ar rcs $@ $(OBJECTS)\n         ranlib $@\n\n+$(SO_TARGET): $(TARGET) $(OBJECTS)\n+       $(CC) -shared -o $@ $(OBJECTS)\n+\n build:\n         @mkdir -p build\n         @mkdir -p bin\n```\n\n完成这些改变后，你现在应该能够构建任何东西，并且你可以最后补完剩余的单元测试函数：\n\n```c\n#include \"minunit.h\"\n#include <dlfcn.h>\n\ntypedef int (*lib_function)(const char *data);\nchar *lib_file = \"build/libYOUR_LIBRARY.so\";\nvoid *lib = NULL;\n\nint check_function(const char *func_to_run, const char *data, int expected)\n{\n    lib_function func = dlsym(lib, func_to_run);\n    check(func != NULL, \"Did not find %s function in the library %s: %s\", func_to_run, lib_file, dlerror());\n\n    int rc = func(data);\n    check(rc == expected, \"Function %s return %d for data: %s\", func_to_run, rc, data);\n\n    return 1;\nerror:\n    return 0;\n}\n\nchar *test_dlopen()\n{\n    lib = dlopen(lib_file, RTLD_NOW);\n    mu_assert(lib != NULL, \"Failed to open the library to test.\");\n\n    return NULL;\n}\n\nchar *test_functions()\n{\n    mu_assert(check_function(\"print_a_message\", \"Hello\", 0), \"print_a_message failed.\");\n    mu_assert(check_function(\"uppercase\", \"Hello\", 0), \"uppercase failed.\");\n    mu_assert(check_function(\"lowercase\", \"Hello\", 0), \"lowercase failed.\");\n\n    return NULL;\n}\n\nchar *test_failures()\n{\n    mu_assert(check_function(\"fail_on_purpose\", \"Hello\", 1), \"fail_on_purpose should fail.\");\n\n    return NULL;\n}\n\nchar *test_dlclose()\n{\n    int rc = dlclose(lib);\n    mu_assert(rc == 0, \"Failed to close lib.\");\n\n    return NULL;\n}\n\nchar *all_tests() {\n    mu_suite_start();\n\n    mu_run_test(test_dlopen);\n    mu_run_test(test_functions);\n    mu_run_test(test_failures);\n    mu_run_test(test_dlclose);\n\n    return NULL;\n}\n\nRUN_TESTS(all_tests);\n```\n\n我希望你可以弄清楚它都干了什么，因为这里没有什么新的东西，除了`check_function`函数。这是一个通用的模式，其中我需要重复执行一段代码，然后通过为之创建宏或函数来使它自动化。这里我打算运行`.so`中所加载的函数，所以我创建了一个小型函数来完成它。\n\n## 附加题\n\n+ 这段代码能起作用，但是可能有点乱。清理框架目录，是它包含所有这些文件，但是移除任何和练习29有关的代码。你应该能够复制这个目录并且无需很多编辑操作就能开始新的项目。\n+ 研究`runtests.sh`，并且查询有关`bash`语法的资料，来弄懂它的作用。你能够编写这个脚本的C版本吗？\n"
  },
  {
    "path": "docs/lcthw-zh/ex31.md",
    "content": "# 练习31：代码调试\n\n> 原文：[Exercise 31: Debugging Code](http://c.learncodethehardway.org/book/ex31.html)\n\n> 译者：[飞龙](https://github.com/wizardforcel)\n\n我已经教给你一些关于我的强大的调试宏的技巧，并且你已经开始用它们了。当我调试代码时，我使用`debug()`宏，分析发生了什么以及跟踪问题。在这个练习中我打算教给你一些使用gdb的技巧，用于监视一个不会退出的简单程序。你会学到如何使用gdb附加到运行中的进程，并挂起它来观察发生了什么。在此之后我会给你一些用于gdb的小提示和小技巧。\n\n## 调试输出、GDB或Valgrind\n\n我主要按照一种“科学方法”的方式来调试，我会提出可能的所有原因，之后排除它们或证明它们导致了缺陷。许多程序员拥有的问题是它们对解决bug的恐慌和急躁使他们觉得这种方法会“拖慢”他们。它们并没有注意到，它们已经失败了，并且在收集无用的信息。我发现日志（调试输出）会强迫我科学地解决bug，并且在更多情况下易于收集信息。\n\n此外，使用调试输出来作为我的首要调试工具的理由如下：\n\n+ 你可以使用变量的调试输出，来看到程序执行的整个轨迹，它让你跟踪变量是如何产生错误的。使用gdb的话，你必须为每个变量放置查看和调试语句，并且难以获得执行的实际轨迹。\n+ 调试输出存在于代码中，当你需要它们是你可以重新编译使它们回来。使用gdb的话，你每次调试都需要重新配置相同的信息。\n+ 当服务器工作不正常时，它的调试日志功能易于打开，并且在它运行中可以监视日志来查看哪里不对。系统管理员知道如何处理日志，他们不知道如何使用gdb。\n+ 打印信息更加容易。调试器通常由于它奇特的UI和前后矛盾显得难用且古怪。`debug(\"Yo, dis right? %d\", my_stuff);`就没有那么麻烦。\n+ 编写调试输出来发现缺陷，强迫你实际分析代码，并且使用科学方法。你可以认为它是，“我假设这里的代码是错误的”，你可以运行它来验证你的假设，如果这里没有错误那么你可以移动到其它地方。这看起来需要更长时间，但是实际上更快，因为你经历了“鉴别诊断”的过程，并排除所有可能的原因，直到你找到它。\n+ 调试输入更适于和单元测试一起运行。你可以实际上总是编译调试语句，单元测试时可以随时查看日志。如果你用gdb，你需要在gdb中重复运行单元测试，并跟踪他来查看发生了什么。\n+ 使用Valgrind可以得到和调试输出等价的内存相关的错误，所以你并不需要使用类似gdb的东西来寻找缺陷。\n\n尽管所有原因显示我更倾向于`debug`而不是`gdb`，我还是在少数情况下回用到`gdb`，并且我认为你应该选择有助于你完成工作的工具。有时，你只能够连接到一个崩溃的程序并且四处转悠。或者，你得到了一个会崩溃的服务器，你只能够获得一些核心文件来一探究竟。这些货少数其它情况中，gdb是很好的办法。你最好准备尽可能多的工具来解决问题。\n\n接下来我会通过对比gdb、调试输出和Valgrind来详细分析，像这样：\n\n+ Valgrind用于捕获所有内存错误。如果Valgrind中含有错误或Valgrind会严重拖慢程序，我会使用gdb。\n+ 调试输出用于诊断或修复有关逻辑或使用上的缺陷。在你使用Valgrind之前，这些共计90%的缺陷。\n+ 使用gdb解决剩下的“谜之bug”，或如要收集信息的紧急情况。如果Valgrind不起作用，并且我不能打印出所需信息，我就会使用gdb开始四处搜索。这里我仅仅使用gdb来收集信息。一旦我弄清发生了什么，我会回来编程单元测试来引发缺陷，之后编程打印语句来查找原因。\n\n## 调试策略\n\n这一过程适用于你打算使用任何调试技巧，无论是Valgrind、调试输出，或者使用调试器。我打算以使用`gdb`的形式来描述他，因为似乎人们在使用调试器是会跳过它。但是应当对每个bug使用它，直到你只需要在非常困难的bug上用到。\n\n+ 创建一个小型文本文件叫做`notes.txt`，并且将它用作记录想法、bug和问题的“实验记录”。\n+ 在你使用`gdb`之前，写下你打算修复的bug，以及可能的产生原因。\n+ 对于每个原因，写下你所认为的，问题来源的函数或文件，或者仅仅写下你不知道。\n+ 现在启动`gdb`并且使用`file:function`挑选最可能的因素，之后在那里设置断点。\n+ 使用`gdb`运行程序，并且确认它是否是真正原因。查明它的最好方式就是看看你是否可以使用`set`命令，简单修复问题或者重现错误。\n+ 如果它不是真正原因，则在`notes.txt`中标记它不是，以及理由。移到下一个可能的原因，并且使最易于调试的，之后记录你收集到的信息。\n\n这里你并没有注意到，它是最基本的科学方法。你写下一些假设，之后调试来证明或证伪它们。这让你洞察到更多可能的因素，最终使你找到他。这个过程有助于你避免重复步入同一个可能的因素，即使你发现它们并不可能。\n\n你也可以使用调试输出来执行这个过程。唯一的不同就是你实际在源码中编写假设来推测问题所在，而不是`notes.txt`中。某种程度上，调试输出强制你科学地解决bug，因为你需要将假写为打印语句。\n\n## 使用 GDB\n\n我将在这个练习中调试下面这个程序，它只有一个不会正常终止的`while`循环。我在里面放置了一个`usleep`调用，使它循环起来更加有趣。\n\n```c\n#include <unistd.h>\n\nint main(int argc, char *argv[])\n{\n    int i = 0;\n\n    while(i < 100) {\n        usleep(3000);\n    }\n\n    return 0;\n}\n```\n\n像往常一样编译，并且在`gdb`下启动它，例如：`gdb ./ex31`。\n\n一旦它运行之后，我打算让你使用这些`gdb`命令和它交互，并且观察它们的作用以及如何使用它们。\n\nhelp COMMAND\n\n获得`COMMAND`的简单帮助。\n\nbreak file.c:(line|function)\n\n在你希望暂停之星的地方设置断点。你可以提供行号或者函数名称，来在文件中的那个地方暂停。\n\nrun ARGS\n\n运行程序，使用`ARGS`作为命令行参数。\n\ncont\n\n继续执行程序，直到断点或错误。\n\nstep\n\n单步执行代码，但是会进入函数内部。使用它来跟踪函数内部，来观察它做了什么。\n\nnext\n\n就像是`step`，但是他会运行函数并步过它们。\n\nbacktrace (or bt)\n\n执行“跟踪回溯”，它会转储函数到当前执行点的执行轨迹。对于查明如何执行到这里非常有用，因为它也打印出传给每个函数的参数。它和Valgrind报告内存错误的方式很接近。\n\nset var X = Y\n\n将变量`X`设置为`Y`。\n\nprint X\n\n打印出`X`的值，你通常可以使用C的语法来访问指针的值或者结构体的内容。\n\nENTER\n\n重复上一条命令。\n\nquit\n\n退出`gdb`。\n\n这些都是我使用`gdb`时的主要命令。你现在的任务是玩转它们和`ex31`，你会对它的输出更加熟悉。\n\n一旦你熟悉了`gdb`之后，你会希望多加使用它。尝试在更复杂的程序，例如`devpkg`上使用它，来观察你是否能够改函数的执行或分析出程序在做什么。\n\n## 附加到进程\n\n`gdb`最实用的功能就是附加到运行中的程序，并且就地调试它的能力。当你拥有一个崩溃的服务器或GUI程序，你通常不需要像之前那样在`gdb`下运行它。而是可以直接启动它，希望它不要马上崩溃，之后附加到它并设置断点。练习的这一部分中我会向你展示怎么做。\n\n当你退出`gdb`之后，如果你停止了`ex31`我希望你重启它，之后开启另一个中断窗口以便于启动`gdb`并附加。进程附加就是你让`gdb`连接到已经运行的程序，以便于你实时监测它。它会挂起程序来让你单步执行，当你执行完之后程序会像往常一样恢复运行。\n\n下面是一段会话，我对`ex31`做了上述事情，单步执行它，之后修改`while`循环并使它退出。\n\n```sh\n$ ps ax | grep ex31\n10026 s000  S+     0:00.11 ./ex31\n10036 s001  R+     0:00.00 grep ex31\n\n$ gdb ./ex31 10026\nGNU gdb 6.3.50-20050815 (Apple version gdb-1705) (Fri Jul  1 10:50:06 UTC 2011)\nCopyright 2004 Free Software Foundation, Inc.\nGDB is free software, covered by the GNU General Public License, and you are\nwelcome to change it and/or distribute copies of it under certain conditions.\nType \"show copying\" to see the conditions.\nThere is absolutely no warranty for GDB.  Type \"show warranty\" for details.\nThis GDB was configured as \"x86_64-apple-darwin\"...Reading symbols for shared libraries .. done\n\n/Users/zedshaw/projects/books/learn-c-the-hard-way/code/10026: No such file or directory\nAttaching to program: `/Users/zedshaw/projects/books/learn-c-the-hard-way/code/ex31', process 10026.\nReading symbols for shared libraries + done\nReading symbols for shared libraries ++........................ done\nReading symbols for shared libraries + done\n0x00007fff862c9e42 in __semwait_signal ()\n\n(gdb) break 8\nBreakpoint 1 at 0x107babf14: file ex31.c, line 8.\n\n(gdb) break ex31.c:11\nBreakpoint 2 at 0x107babf1c: file ex31.c, line 12.\n\n(gdb) cont\nContinuing.\n\nBreakpoint 1, main (argc=1, argv=0x7fff677aabd8) at ex31.c:8\n8      while(i < 100) {\n\n(gdb) p i\n$1 = 0\n\n(gdb) cont\nContinuing.\n\nBreakpoint 1, main (argc=1, argv=0x7fff677aabd8) at ex31.c:8\n8      while(i < 100) {\n\n(gdb) p i\n$2 = 0\n\n(gdb) list\n3  \n4  int main(int argc, char *argv[])\n5  {\n6      int i = 0;\n7  \n8      while(i < 100) {\n9          usleep(3000);\n10     }\n11\n12     return 0;\n\n(gdb) set var i = 200\n\n(gdb) p i\n$3 = 200\n\n(gdb) next\n\nBreakpoint 2, main (argc=1, argv=0x7fff677aabd8) at ex31.c:12\n12     return 0;\n\n(gdb) cont\nContinuing.\n\nProgram exited normally.\n(gdb) quit\n$\n```\n\n> 注\n\n> 在OSX上你可能会看到输入root密码的GUI输入框，并且即使你输入了密码还是会得到来自`gdb`的“Unable to access task for process-id XXX: (os/kern) failure.”的错误。这种情况下，你需要停止`gdb`和`ex31`程序，并重新启动程序使它工作，只要你成功输入了root密码。\n\n我会遍历整个会话，并且解释我做了什么：\n\ngdb:1\n\n使用`ps`来寻找我想要附加的`ex31`的进程ID。\n\ngdb:5\n\n我使用`gdb ./ex31 PID`来附加到进程，其中`PID`替换为我所拥有的进程ID。\n\ngdb:6-19\n\n`gdb`打印出了一堆关于协议的信息，接着它读取了所有东西。\n\ngdb:21\n\n程序被附加，并且在当前执行点上停止。所以现在我在文件中的第8行使用`break`设置了断点。我假设我这么做的时候，已经在这个我想中断的文件中了。\n\ngdb:24\n\n执行`break`的更好方式，是提供`file.c line`的格式，便于你确保定位到了正确的地方。我在这个`break`中这样做。\n\ngdb:27\n\n我使用`cont`来继续运行，直到我命中了断点。\n\ngdb:30-31\n\n我已到达断点，于是`gdb`打印出我需要了解的变量（`argc`和`argv`），以及停下来的位置，之后打印出断点的行号。\n\ngdb:33-34\n\n我使用`print`的缩写`p`来打印出`i`变量的值，它是0。\n\ngdb:36\n\n继续运行来查看`i`是否改变。\n\ngdb:42\n\n再次打印出`i`，显然它没有变化。\n\ngdb:45-55\n\n使用`list`来查看代码是什么，之后我意识到它不可能退出，因为我没有自增`i`。\n\ngdb:57\n\n确认我的假设是正确的，即`i`需要使用`set`命令来修改为`i = 200`。这是`gdb`最优秀的特性之一，让你“修改”程序来让你快速知道你是否正确。\n\ngdb:59\n\n打印`i`来确保它已改变。\n\ngdb:62\n\n使用`next`来移到下一段代码，并且我发现命中了`ex31.c:12`的断点，所以这意味着`while`循环已退出。我的假设正确，我需要修改`i`。\n\ngdb:67\n\n使用`cont`来继续运行，程序像往常一样退出。\n\ngdb:71\n\n最后我使用`quit`来退出`gdb`。\n\n## GDB 技巧\n\n下面是你可以用于GDB的一些小技巧：\n\ngdb --args\n\n通常`gdb`获得你提供的变量并假设它们用于它自己。使用`--args`来向程序传递它们。\n\nthread apply all bt\n\n转储所有线程的执行轨迹，非常有用。\n\ngdb --batch --ex r --ex bt --ex q --args\n\n运行程序，当它崩溃时你会得到执行轨迹。\n\n?\n\n如果你有其它技巧，在评论中写下它吧。\n\n## 附加题\n\n+ 找到一个图形化的调试器，将它与原始的`gdb`相比。它们在本地调试程序时非常有用，但是对于在服务器上调试没有任何意义。\n+ 你可以开启OS上的“核心转储”，当程序崩溃时你会得到一个核心文件。这个核心文件就像是对程序的解剖，便于你了解崩溃时发生了什么，以及由什么原因导致。修改`ex31.c`使它在几个迭代之后崩溃，之后尝试得到它的核心转储并分析。\n"
  },
  {
    "path": "docs/lcthw-zh/ex32.md",
    "content": "# 练习32：双向链表\n\n> 原文：[Exercise 32: Double Linked Lists](http://c.learncodethehardway.org/book/ex32.html)\n\n> 译者：[飞龙](https://github.com/wizardforcel)\n\n这本书的目的是教给你计算机实际上如何工作，这也包括多种数据结构和算法函数。计算机自己其实并没有太大用处。为了让它们做一些有用的事情，你需要构建数据，之后在这些结构上组织处理。其它编程语言带有实现所有这些结构的库，或者带有直接的语法来创建它们。C需要你手动实现所有数据结构，这使它成为最“完美”的语言，让你知道它们的工作原理。\n\n我的目标是交给你这些数据结构，以及相关算法的知识，来帮助你完成下面这三件事：\n\n+ 理解Python、Ruby或JavaScript的`data = {\"name\": \"Zed\"}`到底做了什么。\n+ 使用数据结构来解决问题，使你成为更好的C程序员。\n+ 学习数据结构和算法的核心部分，让你知道在特定条件下哪个最好。\n\n## 数据结构是什么。\n\n“数据结构”这个名称自己就能够解释。它是具有特性模型的数据组织方法。这一模型可能设计用于以新的方法处理数据，也可能只是用于将它们更高效地储存在磁盘上。这本书中我会遵循一些简单的模式来构建可用的数据结构：\n\n+ 定义一个结构的主要“外部结构”。\n+ 定义一个结构的内容，通常是带有链接的节点。\n+ 创建函数操作它们的函数。\n\nC中还有其它样式的数据结构，但是这个模式效果很好，并且对于你创建的大部分数据结构都适用。\n\n## 构建库\n\n对于这本书的剩余部分，当你完成这本书之后，你将会创建一个可用的库。这个库会包含下列元素：\n\n+ 为每个数据结构编写的头文件`.h`。\n+ 为算法编写的实现文件`.c`。\n+ 用于测试它们确保有效的单元测试。\n+ 从头文件自动生成的文档。\n\n你已经实现了`c-skeleton`（项目框架目录），使用它来创建一个`liblcthw`项目：\n\n```sh\n$ cp -r c-skeleton liblcthw\n$ cd liblcthw/\n$ ls\nLICENSE             Makefile        README.md       bin             build           src             tests\n$ vim Makefile\n$ ls src/\ndbg.h               libex29.c       libex29.o\n$ mkdir src/lcthw\n$ mv src/dbg.h src/lcthw\n$ vim tests/minunit.h\n$ rm src/libex29.* tests/libex29*\n$ make clean\nrm -rf build  tests/libex29_tests\nrm -f tests/tests.log\nfind . -name \"*.gc*\" -exec rm {} \\;\nrm -rf `find . -name \"*.dSYM\" -print`\n$ ls tests/\nminunit.h  runtests.sh\n$\n```\n\n这个会话中我执行了下列事情：\n\n+ 复制了`c-skeleton`。\n+ 编辑Makefile，将`libYOUR_LIBRARY.a`改为`liblcthw.a`作为新的`TARGET`。\n+ 创建`src/lcthw`目录，我们会在里面放入代码。\n+ 移动`src/dbg.h`文件到新的目录中。\n+ 编辑` tests/minunit.h`，使它使用所包含的`#include <lcthw/dbg.h>`。\n+ 移除`libex29.*`中我们不需要的源文件和测试文件。\n+ 清理所有遗留的东西。\n\n执行完之后你就准备好开始构建库了，我打算构建第一个数据结构是双向链表。\n\n## 双向链表\n\n我们将要向`liblcthw`添加的第一个数据结构是双向链表。这是你能够构建的最简单的数据结构，并且它拥有针对特定操作的实用属性。单向链表通过指向下一个或上一个元素的节点来工作。“双向”链表持有全部这两个指针，而“单向”链表只持有下一个元素的指针。\n\n由于每个节点都有下一个和上一个元素的指针，并且你可以跟踪联保的第一个和最后的元素，你就可以快速地执行一些操作。任何涉及到插入和删除元素的操作会非常快。它对大多数人来说也易于实现。\n\n链表的主要缺点是，遍历它涉及到处理沿途每个单个的指针。这意味着搜索、多数排序以及迭代元素会表较慢。这也意味着你不能直接跳过链表的随机一部分。如果换成数组，你就可以直接索引到它的中央，但是链表不行。也就是说如果你想要访问第十个元素，你必须经过1~9。\n\n### 定义\n\n正如在这个练习的介绍部分所说，整个过程的第一步，是编程一个头文件，带有正确的C结构定义。\n\n```c\n#ifndef lcthw_List_h\n#define lcthw_List_h\n\n#include <stdlib.h>\n\nstruct ListNode;\n\ntypedef struct ListNode {\n    struct ListNode *next;\n    struct ListNode *prev;\n    void *value;\n} ListNode;\n\ntypedef struct List {\n    int count;\n    ListNode *first;\n    ListNode *last;\n} List;\n\nList *List_create();\nvoid List_destroy(List *list);\nvoid List_clear(List *list);\nvoid List_clear_destroy(List *list);\n\n#define List_count(A) ((A)->count)\n#define List_first(A) ((A)->first != NULL ? (A)->first->value : NULL)\n#define List_last(A) ((A)->last != NULL ? (A)->last->value : NULL)\n\nvoid List_push(List *list, void *value);\nvoid *List_pop(List *list);\n\nvoid List_unshift(List *list, void *value);\nvoid *List_shift(List *list);\n\nvoid *List_remove(List *list, ListNode *node);\n\n#define LIST_FOREACH(L, S, M, V) ListNode *_node = NULL;\\\n    ListNode *V = NULL;\\\n    for(V = _node = L->S; _node != NULL; V = _node = _node->M)\n\n#endif\n```\n\n我所做的第一件事就是创建两个结构，`ListNode`和包含这些节点的`List`。这创建了是将在函数中使用的数据结构，以及随后定义的宏。如果你浏览这些函数，它们看起来非常简单。当我讲到实现时，我会解释他们，但我更希望你能猜出它们的作用。\n\n这些数据结构的工作方式，就是每个`ListNode`都有三个成员。\n\n+ 值，它是无类型的指针，存储我们想在链表中放置的东西。\n+ `ListNode *next`指针，它指向另一个储存下一个元素的`ListNode `。\n+ `ListNode *prev`指针，它指向另一个储存上一个元素的`ListNode `。\n\n`List`结构只是这些`ListNode`结构的容器，它们互联链接组成链型。它跟踪链表的`count`，`first`和`last`元素。\n\n最后，看一看`src/lcthw/list.h:37`，其中我定义了`LIST_FOREACH`宏。这是个常见的习语，你可以创建一个宏来生成迭代代码，使用者就不会弄乱了。正确使用这类执行过程来处理数据结构十分困难，所以可以编写宏来帮助使用者。当我讲到实现时，你可以看到我如何使用它。\n\n### 实现\n\n一旦你理解了它们之后，你很可能理解了双向链表如何工作。它只是带有两个指针的节点，指向链表中前一个和后一个元素。接下来你可以编写`src/lcthw/list.c`中的代码，来理解每个操作如何实现。\n\n```c\n#include <lcthw/list.h>\n#include <lcthw/dbg.h>\n\nList *List_create()\n{\n    return calloc(1, sizeof(List));\n}\n\nvoid List_destroy(List *list)\n{\n    LIST_FOREACH(list, first, next, cur) {\n        if(cur->prev) {\n            free(cur->prev);\n        }\n    }\n\n    free(list->last);\n    free(list);\n}\n\n\nvoid List_clear(List *list)\n{\n    LIST_FOREACH(list, first, next, cur) {\n        free(cur->value);\n    }\n}\n\n\nvoid List_clear_destroy(List *list)\n{\n    List_clear(list);\n    List_destroy(list);\n}\n\n\nvoid List_push(List *list, void *value)\n{\n    ListNode *node = calloc(1, sizeof(ListNode));\n    check_mem(node);\n\n    node->value = value;\n\n    if(list->last == NULL) {\n        list->first = node;\n        list->last = node;\n    } else {\n        list->last->next = node;\n        node->prev = list->last;\n        list->last = node;\n    }\n\n    list->count++;\n\nerror:\n    return;\n}\n\nvoid *List_pop(List *list)\n{\n    ListNode *node = list->last;\n    return node != NULL ? List_remove(list, node) : NULL;\n}\n\nvoid List_unshift(List *list, void *value)\n{\n    ListNode *node = calloc(1, sizeof(ListNode));\n    check_mem(node);\n\n    node->value = value;\n\n    if(list->first == NULL) {\n        list->first = node;\n        list->last = node;\n    } else {\n        node->next = list->first;\n        list->first->prev = node;\n        list->first = node;\n    }\n\n    list->count++;\n\nerror:\n    return;\n}\n\nvoid *List_shift(List *list)\n{\n    ListNode *node = list->first;\n    return node != NULL ? List_remove(list, node) : NULL;\n}\n\nvoid *List_remove(List *list, ListNode *node)\n{\n    void *result = NULL;\n\n    check(list->first && list->last, \"List is empty.\");\n    check(node, \"node can't be NULL\");\n\n    if(node == list->first && node == list->last) {\n        list->first = NULL;\n        list->last = NULL;\n    } else if(node == list->first) {\n        list->first = node->next;\n        check(list->first != NULL, \"Invalid list, somehow got a first that is NULL.\");\n        list->first->prev = NULL;\n    } else if (node == list->last) {\n        list->last = node->prev;\n        check(list->last != NULL, \"Invalid list, somehow got a next that is NULL.\");\n        list->last->next = NULL;\n    } else {\n        ListNode *after = node->next;\n        ListNode *before = node->prev;\n        after->prev = before;\n        before->next = after;\n    }\n\n    list->count--;\n    result = node->value;\n    free(node);\n\nerror:\n    return result;\n}\n```\n\n我实现了双向链表上的所有操作，它们不能用简单的宏来完成。比起覆盖文件中的每一行，我打算为`list.h`和`list.c`中的每个操作提供一个高阶的概览。你需要自己阅读代码。\n\nlist.h:List_count\n\n返回链表中元素数量，它在元素添加或移除时维护。\n\nlist.h:List_first\n\n返回链表的首个元素，但是并不移除它。\n\nlist.h:List_last\n\n返回链表的最后一个元素，但是不移除它。\n\nlist.h:LIST_FOREACH\n\n遍历链表中的元素。\n\nlist.c:List_create\n\n简单地创建主要的`List`结构。\n\nlist.c:List_destroy\n\n销毁`List`以及其中含有的所有元素。\n\nlist.c:List_clear\n\n为释放每个节点中的值（而不是节点本身）创建的辅助函数。\n\nlist.c:List_clear_destroy\n\n清理并销毁链表。它并不十分搞笑因为它对每个元素遍历两次。\n\nlist.c:List_push\n\n第一个操作演示了链表的有点。它向链表尾添加新的元素，由于只是一些指针赋值，所以非常快。\n\nlist.c:List_pop\n\n`List_push`的反向版本，它去除最后一个元素并返回它。\n\nlist.c:List_unshift\n\n亦可以轻易对链表执行的另一件事，就是快速地向链表头部添加元素。由于找不到合适的词，这里我把它称为`unshift`。\n\nlist.c:List_shift\n\n类似`List_pop`，但是它移除链表的首个元素并返回。\n\nlist.c:List_remove\n\n当你执行`List_pop`或`List_shift`时，它执行实际的移除操作。在数据结构中移除数据总是看似比较困难，这个函数也不例外。它需要处理一些条件，取决于被移除的位置，在开头、在结尾、开头并且结尾，或者在中间。\n\n这些函数大多数都没什么特别的，你应该能够轻易描述出来，并且根据代码来理解它。你应该完全专注于`List_destroy`中的`LIST_FOREACH`如何使用来理解它如何简化通常的操作。\n\n## 测试\n\n在你编译它们之前，需要创建测试来确保它们正确执行。\n\n```c\n#include \"minunit.h\"\n#include <lcthw/list.h>\n#include <assert.h>\n\nstatic List *list = NULL;\nchar *test1 = \"test1 data\";\nchar *test2 = \"test2 data\";\nchar *test3 = \"test3 data\";\n\n\nchar *test_create()\n{\n    list = List_create();\n    mu_assert(list != NULL, \"Failed to create list.\");\n\n    return NULL;\n}\n\n\nchar *test_destroy()\n{\n    List_clear_destroy(list);\n\n    return NULL;\n\n}\n\n\nchar *test_push_pop()\n{\n    List_push(list, test1);\n    mu_assert(List_last(list) == test1, \"Wrong last value.\");\n\n    List_push(list, test2);\n    mu_assert(List_last(list) == test2, \"Wrong last value\");\n\n    List_push(list, test3);\n    mu_assert(List_last(list) == test3, \"Wrong last value.\");\n    mu_assert(List_count(list) == 3, \"Wrong count on push.\");\n\n    char *val = List_pop(list);\n    mu_assert(val == test3, \"Wrong value on pop.\");\n\n    val = List_pop(list);\n    mu_assert(val == test2, \"Wrong value on pop.\");\n\n    val = List_pop(list);\n    mu_assert(val == test1, \"Wrong value on pop.\");\n    mu_assert(List_count(list) == 0, \"Wrong count after pop.\");\n\n    return NULL;\n}\n\nchar *test_unshift()\n{\n    List_unshift(list, test1);\n    mu_assert(List_first(list) == test1, \"Wrong first value.\");\n\n    List_unshift(list, test2);\n    mu_assert(List_first(list) == test2, \"Wrong first value\");\n\n    List_unshift(list, test3);\n    mu_assert(List_first(list) == test3, \"Wrong last value.\");\n    mu_assert(List_count(list) == 3, \"Wrong count on unshift.\");\n\n    return NULL;\n}\n\nchar *test_remove()\n{\n    // we only need to test the middle remove case since push/shift\n    // already tests the other cases\n\n    char *val = List_remove(list, list->first->next);\n    mu_assert(val == test2, \"Wrong removed element.\");\n    mu_assert(List_count(list) == 2, \"Wrong count after remove.\");\n    mu_assert(List_first(list) == test3, \"Wrong first after remove.\");\n    mu_assert(List_last(list) == test1, \"Wrong last after remove.\");\n\n    return NULL;\n}\n\n\nchar *test_shift()\n{\n    mu_assert(List_count(list) != 0, \"Wrong count before shift.\");\n\n    char *val = List_shift(list);\n    mu_assert(val == test3, \"Wrong value on shift.\");\n\n    val = List_shift(list);\n    mu_assert(val == test1, \"Wrong value on shift.\");\n    mu_assert(List_count(list) == 0, \"Wrong count after shift.\");\n\n    return NULL;\n}\n\n\n\nchar *all_tests() {\n    mu_suite_start();\n\n    mu_run_test(test_create);\n    mu_run_test(test_push_pop);\n    mu_run_test(test_unshift);\n    mu_run_test(test_remove);\n    mu_run_test(test_shift);\n    mu_run_test(test_destroy);\n\n    return NULL;\n}\n\nRUN_TESTS(all_tests);\n```\n\n它简单地遍历了每个操作，并且确保它们有效。我在测试中做了简化，对于整个程序我只创建了一个`List *list`，这解决了为每个测试构建一个`List`的麻烦，但它同时意味着一些测试会受到之前测试的影响。这里我试着是每个测试不改变链表，或实际使用上一个测试的结果。\n\n## 你会看到什么\n\n如果你正确完成了每件事，当你执行构建并且运行单元测试是，你会看到：\n\n```sh\n$ make\ncc -g -O2 -Wall -Wextra -Isrc -rdynamic -DNDEBUG  -fPIC   -c -o src/lcthw/list.o src/lcthw/list.c\nar rcs build/liblcthw.a src/lcthw/list.o\nranlib build/liblcthw.a\ncc -shared -o build/liblcthw.so src/lcthw/list.o\ncc -g -O2 -Wall -Wextra -Isrc -rdynamic -DNDEBUG  build/liblcthw.a    tests/list_tests.c   -o tests/list_tests\nsh ./tests/runtests.sh\nRunning unit tests:\n----\nRUNNING: ./tests/list_tests\nALL TESTS PASSED\nTests run: 6\ntests/list_tests PASS\n$\n```\n\n确保6个测试运行完毕，以及构建时没有警告或错误，并且成功构建了`build/liblcthw.a`和`build/liblcthw.so`文件。\n\n## 如何改进\n\n我打算告诉你如何改进代码，而不是使它崩溃。\n\n+ 你可以使用`LIST_FOREACH`并在循环中调用`free`来使`List_clear_destroy`更高效。\n+ 你可以为一些先决条件添加断言，使其部结构`NULL`值作为`List *list`的参数。\n+ 你可以添加不变了，来检查列表的内容始终正确，例如`count`永远不会`< 0`，如果`count > 0`，`first`不为`NULL`。\n+ 你可以向头文件添加文档，在每个结构、函数和宏之前添加描述其作用的注释。\n\n这些改进执行了防御性编程实践，并且“加固”了代码来避免错误或使用不当。马上去做这些事情，之后找到尽可能多的办法来改进代码。\n\n## 附加题\n\n+ 研究双向和单向链表，以及什么情况下其中一种优于另一种。\n+ 研究双向链表的限制。例如，虽然它们对于插入和删除元素很高效，但是对于变量元素比较慢。\n+ 还缺少什么你能想到的操作？比如复制、连接、分割等等。实现这些操作，并且为它们编写单元测试。\n"
  },
  {
    "path": "docs/lcthw-zh/ex33.md",
    "content": "# 练习33：链表算法\n\n> 原文：[Exercise 33: Linked List Algorithms](http://c.learncodethehardway.org/book/ex33.html)\n\n> 译者：[飞龙](https://github.com/wizardforcel)\n\n我将想你介绍涉及到排序的两个算法，你可以用它们操作链表。我首先要警告你，如果你打算对数据排序，不要使用链表，它们对于排序十分麻烦，并且有更好的数据结构作为替代。我向你介绍这两种算法只是因为它们难以在链表上完成，并且让你思考如何高效操作它们。\n\n为了编写这本书，我打算将算法放在两个不同的文件中，`list_algos.h`和`list_algos.c`，之后在`list_algos_test.c`中编写测试。现在你要按照我的结构，因为它足以把事情做好，但是如果你使用其它的库要记住这并不是通用的结构。\n\n这个练习中我打算给你一些额外的挑战，并且希望你不要作弊。我打算先给你单元测试，并且让你打下来。之后让你基于它们在维基百科中的描述，尝试实现这个两个算法，之后看看你的代码是否和我的类似。\n\n## 冒泡排序和归并排序\n\n互联网的强大之处，就是我可以仅仅给你[冒泡排序](http://en.wikipedia.org/wiki/Bubble_sort)和[归并排序](http://en.wikipedia.org/wiki/Merge_sort)的链接，来让你学习它们。是的，这省了我很多字。现在我要告诉你如何使用它们的伪代码来实现它们。你可以像这样来实现算法：\n\n+ 阅读描述，并且观察任何可视化的图表。\n+ 使用方框和线条在纸上画出算法，或者使用一些带有数字的卡片（比如扑克牌），尝试手动执行算法。这会向你形象地展示算法的执行过程。\n+ 在`list_algos.c`文案总创建函数的主干，并且创建`list_algos.h`文件，之后创建测试代码。\n+ 编写第一个测试并且编译所有东西。\n+ 回到维基百科页面，复制粘贴伪代码到你创建的函数中（不是C代码）。\n+ 将伪代码翻译成良好的C代码，就像我教你的那样，使用你的单元测试来保证它有效。\n+ 为边界情况补充一些测试，例如空链表，排序号的链表，以及其它。\n+ 对下一个算法重复这些过程并测试。\n\n我只是告诉你理解大多数算法的秘密，直到你碰到一些更加麻烦的算法。这里你只是按照维基百科来实现冒泡排序和归并排序，它们是一个好的起始。\n\n## 单元测试\n\n下面是你应该通过的单元测试：\n\n```c\n#include \"minunit.h\"\n#include <lcthw/list_algos.h>\n#include <assert.h>\n#include <string.h>\n\nchar *values[] = {\"XXXX\", \"1234\", \"abcd\", \"xjvef\", \"NDSS\"};\n#define NUM_VALUES 5\n\nList *create_words()\n{\n    int i = 0;\n    List *words = List_create();\n\n    for(i = 0; i < NUM_VALUES; i++) {\n        List_push(words, values[i]);\n    }\n\n    return words;\n}\n\nint is_sorted(List *words)\n{\n    LIST_FOREACH(words, first, next, cur) {\n        if(cur->next && strcmp(cur->value, cur->next->value) > 0) {\n            debug(\"%s %s\", (char *)cur->value, (char *)cur->next->value);\n            return 0;\n        }\n    }\n\n    return 1;\n}\n\nchar *test_bubble_sort()\n{\n    List *words = create_words();\n\n    // should work on a list that needs sorting\n    int rc = List_bubble_sort(words, (List_compare)strcmp);\n    mu_assert(rc == 0, \"Bubble sort failed.\");\n    mu_assert(is_sorted(words), \"Words are not sorted after bubble sort.\");\n\n    // should work on an already sorted list\n    rc = List_bubble_sort(words, (List_compare)strcmp);\n    mu_assert(rc == 0, \"Bubble sort of already sorted failed.\");\n    mu_assert(is_sorted(words), \"Words should be sort if already bubble sorted.\");\n\n    List_destroy(words);\n\n    // should work on an empty list\n    words = List_create(words);\n    rc = List_bubble_sort(words, (List_compare)strcmp);\n    mu_assert(rc == 0, \"Bubble sort failed on empty list.\");\n    mu_assert(is_sorted(words), \"Words should be sorted if empty.\");\n\n    List_destroy(words);\n\n    return NULL;\n}\n\nchar *test_merge_sort()\n{\n    List *words = create_words();\n\n    // should work on a list that needs sorting\n    List *res = List_merge_sort(words, (List_compare)strcmp);\n    mu_assert(is_sorted(res), \"Words are not sorted after merge sort.\");\n\n    List *res2 = List_merge_sort(res, (List_compare)strcmp);\n    mu_assert(is_sorted(res), \"Should still be sorted after merge sort.\");\n    List_destroy(res2);\n    List_destroy(res);\n\n    List_destroy(words);\n    return NULL;\n}\n\n\nchar *all_tests()\n{\n    mu_suite_start();\n\n    mu_run_test(test_bubble_sort);\n    mu_run_test(test_merge_sort);\n\n    return NULL;\n}\n\nRUN_TESTS(all_tests);\n```\n\n建议你从冒泡排序开始，使它正确，之后再测试归并。我所做的就是编写函数原型和主干，让这三个文件能够编译，但不能通过测试。之后你将实现填充进入之后才能够工作。\n\n## 实现\n\n你作弊了吗？之后的练习中，我只会给你单元测试，并且让自己实现它。对于你来说，不看这段代码知道你自己实现它是一种很好的练习。下面是`list_algos.c`和`list_algos.h`的代码：\n\n```c\n#ifndef lcthw_List_algos_h\n#define lcthw_List_algos_h\n\n#include <lcthw/list.h>\n\ntypedef int (*List_compare)(const void *a, const void *b);\n\nint List_bubble_sort(List *list, List_compare cmp);\n\nList *List_merge_sort(List *list, List_compare cmp);\n\n#endif\n```\n\n```c\n#include <lcthw/list_algos.h>\n#include <lcthw/dbg.h>\n\ninline void ListNode_swap(ListNode *a, ListNode *b)\n{\n    void *temp = a->value;\n    a->value = b->value;\n    b->value = temp;\n}\n\nint List_bubble_sort(List *list, List_compare cmp)\n{\n    int sorted = 1;\n\n    if(List_count(list) <= 1) {\n        return 0;  // already sorted\n    }\n\n    do {\n        sorted = 1;\n        LIST_FOREACH(list, first, next, cur) {\n            if(cur->next) {\n                if(cmp(cur->value, cur->next->value) > 0) {\n                    ListNode_swap(cur, cur->next);\n                    sorted = 0;\n                }\n            }\n        }\n    } while(!sorted);\n\n    return 0;\n}\n\ninline List *List_merge(List *left, List *right, List_compare cmp)\n{\n    List *result = List_create();\n    void *val = NULL;\n\n    while(List_count(left) > 0 || List_count(right) > 0) {\n        if(List_count(left) > 0 && List_count(right) > 0) {\n            if(cmp(List_first(left), List_first(right)) <= 0) {\n                val = List_shift(left);\n            } else {\n                val = List_shift(right);\n            }\n\n            List_push(result, val);\n        } else if(List_count(left) > 0) {\n            val = List_shift(left);\n            List_push(result, val);\n        } else if(List_count(right) > 0) {\n            val = List_shift(right);\n            List_push(result, val);\n        }\n    }\n\n    return result;\n}\n\nList *List_merge_sort(List *list, List_compare cmp)\n{\n    if(List_count(list) <= 1) {\n        return list;\n    }\n\n    List *left = List_create();\n    List *right = List_create();\n    int middle = List_count(list) / 2;\n\n    LIST_FOREACH(list, first, next, cur) {\n        if(middle > 0) {\n            List_push(left, cur->value);\n        } else {\n            List_push(right, cur->value);\n        }\n\n        middle--;\n    }\n\n    List *sort_left = List_merge_sort(left, cmp);\n    List *sort_right = List_merge_sort(right, cmp);\n\n    if(sort_left != left) List_destroy(left);\n    if(sort_right != right) List_destroy(right);\n\n    return List_merge(sort_left, sort_right, cmp);\n}\n```\n\n冒泡排序并不难以理解，虽然它非常慢。归并排序更为复杂，实话讲如果我想要牺牲可读性的话，我会花一点时间来优化代码。\n\n归并排序有另一种“自底向上”的实现方式，但是它太难了，我就没有选择它。就像我刚才说的那样，在链表上编写排序算法没有什么意思。你可以把时间都花在使它更快，它比起其他可排序的数据结构会相当版。链表的本质决定了如果你需要对数据进行排序，你就不要使用它们（尤其是单向的）。\n\n## 你会看到什么\n\n如果一切都正常工作，你会看到这些：\n\n```sh\n$ make clean all\nrm -rf build src/lcthw/list.o src/lcthw/list_algos.o tests/list_algos_tests tests/list_tests\nrm -f tests/tests.log\nfind . -name \"*.gc*\" -exec rm {} \\;\nrm -rf `find . -name \"*.dSYM\" -print`\ncc -g -O2 -Wall -Wextra -Isrc -rdynamic -DNDEBUG  -fPIC   -c -o src/lcthw/list.o src/lcthw/list.c\ncc -g -O2 -Wall -Wextra -Isrc -rdynamic -DNDEBUG  -fPIC   -c -o src/lcthw/list_algos.o src/lcthw/list_algos.c\nar rcs build/liblcthw.a src/lcthw/list.o src/lcthw/list_algos.o\nranlib build/liblcthw.a\ncc -shared -o build/liblcthw.so src/lcthw/list.o src/lcthw/list_algos.o\ncc -g -O2 -Wall -Wextra -Isrc -rdynamic -DNDEBUG  build/liblcthw.a    tests/list_algos_tests.c   -o tests/list_algos_tests\ncc -g -O2 -Wall -Wextra -Isrc -rdynamic -DNDEBUG  build/liblcthw.a    tests/list_tests.c   -o tests/list_tests\nsh ./tests/runtests.sh\nRunning unit tests:\n----\nRUNNING: ./tests/list_algos_tests\nALL TESTS PASSED\nTests run: 2\ntests/list_algos_tests PASS\n----\nRUNNING: ./tests/list_tests\nALL TESTS PASSED\nTests run: 6\ntests/list_tests PASS\n$\n```\n\n这个练习之后我就不会向你展示这样的输出了，除非有必要向你展示它的工作原理。你应该能知道我运行了测试，并且通过了所有测试。\n\n## 如何改进\n\n退回去查看算法描述，有一些方法可用于改进这些实现，其中一些是很显然的：\n\n+ 归并排序做了大量的链表复制和创建操作，寻找减少它们的办法。\n+ 归并排序的维基百科描述提到了一些优化，实现它们。\n+ 你能使用`List_split`和`List_join`（如果你实现了的话）来改进归并排序嘛？\n+ 浏览所有防御性编程原则，检查并提升这一实现的健壮性，避免`NULL`指针，并且创建一个可选的调试级别的不变量，在排序后实现`is_sorted`的功能。\n\n## 附加题\n\n+ 创建单元测试来比较这两个算法的性能。你需要`man 3 time`来查询基本的时间函数，并且需要运行足够的迭代次数，至少以几秒钟作为样本。\n+ 改变需要排序的链表中的数据总量，看看耗时如何变化。\n+ 寻找方法来创建不同长度的随机链表，并且测量需要多少时间，之后将它可视化并与算法的描述对比。\n+ 尝试解释为什么对链表排序十分麻烦。\n+ 实现`List_insert_sorted`（有序链表），它使用`List_compare`，接收一个值，将其插入到正确的位置，使链表有序。它与创建链表后再进行排序相比怎么样？\n+ 尝试实现维基百科上“自底向上”的归并排序。上面的代码已经是C写的了，所以很容易重新创建，但是要试着理解它的工作原理，并与这里的低效版本对比。\n"
  },
  {
    "path": "docs/lcthw-zh/ex34.md",
    "content": "# 练习34：动态数组\n\n> 原文：[Exercise 34: Dynamic Array](http://c.learncodethehardway.org/book/ex34.html)\n\n> 译者：[飞龙](https://github.com/wizardforcel)\n\n动态数组是自增长的数组，它与链表有很多相同的特性。它通常占据更少的空间，跑得更快，还有一些其它的优势属性。这个练习会涉及到它的一些缺点，比如从开头移除元素会很慢，并给出解决方案（只从末尾移除）。\n\n动态数组简单地实现为`void **`指针的数组，它是预分配内存的，并且指向数据。在链表中你创建了完整的结构体来储存`void *value`指针，但是动态数组中你只需要一个储存它们的单个数组。也就是说，你并不需要创建任何其它的指针储存上一个或下一个元素。它们可以直接索引。\n\n我会给你头文件作为起始，你需要为实现打下它们：\n\n```c\n#ifndef _DArray_h\n#define _DArray_h\n#include <stdlib.h>\n#include <assert.h>\n#include <lcthw/dbg.h>\n\ntypedef struct DArray {\n    int end;\n    int max;\n    size_t element_size;\n    size_t expand_rate;\n    void **contents;\n} DArray;\n\nDArray *DArray_create(size_t element_size, size_t initial_max);\n\nvoid DArray_destroy(DArray *array);\n\nvoid DArray_clear(DArray *array);\n\nint DArray_expand(DArray *array);\n\nint DArray_contract(DArray *array);\n\nint DArray_push(DArray *array, void *el);\n\nvoid *DArray_pop(DArray *array);\n\nvoid DArray_clear_destroy(DArray *array);\n\n#define DArray_last(A) ((A)->contents[(A)->end - 1])\n#define DArray_first(A) ((A)->contents[0])\n#define DArray_end(A) ((A)->end)\n#define DArray_count(A) DArray_end(A)\n#define DArray_max(A) ((A)->max)\n\n#define DEFAULT_EXPAND_RATE 300\n\n\nstatic inline void DArray_set(DArray *array, int i, void *el)\n{\n    check(i < array->max, \"darray attempt to set past max\");\n    if(i > array->end) array->end = i;\n    array->contents[i] = el;\nerror:\n    return;\n}\n\nstatic inline void *DArray_get(DArray *array, int i)\n{\n    check(i < array->max, \"darray attempt to get past max\");\n    return array->contents[i];\nerror:\n    return NULL;\n}\n\nstatic inline void *DArray_remove(DArray *array, int i)\n{\n    void *el = array->contents[i];\n\n    array->contents[i] = NULL;\n\n    return el;\n}\n\nstatic inline void *DArray_new(DArray *array)\n{\n    check(array->element_size > 0, \"Can't use DArray_new on 0 size darrays.\");\n\n    return calloc(1, array->element_size);\n\nerror:\n    return NULL;\n}\n\n#define DArray_free(E) free((E))\n\n#endif\n```\n\n这个头文件向你展示了`static inline`的新技巧，它就类似`#define`宏的工作方式，但是它们更清楚，并且易于编写。如果你需要创建一块代码作为宏，并且不需要代码生成，可以使用`static inline`函数。\n\n为链表生成`for`循环的`LIST_FOREACH`不可能写为`static inline`函数，因为它需要生成循环的内部代码块。实现它的唯一方式是灰调函数，但是这不够块，并且难以使用。\n\n之后我会修改代码，并且让你创建`DArray`的单元测试。\n\n```c\n#include \"minunit.h\"\n#include <lcthw/darray.h>\n\nstatic DArray *array = NULL;\nstatic int *val1 = NULL;\nstatic int *val2 = NULL;\n\nchar *test_create()\n{\n    array = DArray_create(sizeof(int), 100);\n    mu_assert(array != NULL, \"DArray_create failed.\");\n    mu_assert(array->contents != NULL, \"contents are wrong in darray\");\n    mu_assert(array->end == 0, \"end isn't at the right spot\");\n    mu_assert(array->element_size == sizeof(int), \"element size is wrong.\");\n    mu_assert(array->max == 100, \"wrong max length on initial size\");\n\n    return NULL;\n}\n\nchar *test_destroy()\n{\n    DArray_destroy(array);\n\n    return NULL;\n}\n\nchar *test_new()\n{\n    val1 = DArray_new(array);\n    mu_assert(val1 != NULL, \"failed to make a new element\");\n\n    val2 = DArray_new(array);\n    mu_assert(val2 != NULL, \"failed to make a new element\");\n\n    return NULL;\n}\n\nchar *test_set()\n{\n    DArray_set(array, 0, val1);\n    DArray_set(array, 1, val2);\n\n    return NULL;\n}\n\nchar *test_get()\n{\n    mu_assert(DArray_get(array, 0) == val1, \"Wrong first value.\");\n    mu_assert(DArray_get(array, 1) == val2, \"Wrong second value.\");\n\n    return NULL;\n}\n\nchar *test_remove()\n{\n    int *val_check = DArray_remove(array, 0);\n    mu_assert(val_check != NULL, \"Should not get NULL.\");\n    mu_assert(*val_check == *val1, \"Should get the first value.\");\n    mu_assert(DArray_get(array, 0) == NULL, \"Should be gone.\");\n    DArray_free(val_check);\n\n    val_check = DArray_remove(array, 1);\n    mu_assert(val_check != NULL, \"Should not get NULL.\");\n    mu_assert(*val_check == *val2, \"Should get the first value.\");\n    mu_assert(DArray_get(array, 1) == NULL, \"Should be gone.\");\n    DArray_free(val_check);\n\n    return NULL;\n}\n\nchar *test_expand_contract()\n{\n    int old_max = array->max;\n    DArray_expand(array);\n    mu_assert((unsigned int)array->max == old_max + array->expand_rate, \"Wrong size after expand.\");\n\n    DArray_contract(array);\n    mu_assert((unsigned int)array->max == array->expand_rate + 1, \"Should stay at the expand_rate at least.\");\n\n    DArray_contract(array);\n    mu_assert((unsigned int)array->max == array->expand_rate + 1, \"Should stay at the expand_rate at least.\");\n\n    return NULL;\n}\n\nchar *test_push_pop()\n{\n    int i = 0;\n    for(i = 0; i < 1000; i++) {\n        int *val = DArray_new(array);\n        *val = i * 333;\n        DArray_push(array, val);\n    }\n\n    mu_assert(array->max == 1201, \"Wrong max size.\");\n\n    for(i = 999; i >= 0; i--) {\n        int *val = DArray_pop(array);\n        mu_assert(val != NULL, \"Shouldn't get a NULL.\");\n        mu_assert(*val == i * 333, \"Wrong value.\");\n        DArray_free(val);\n    }\n\n    return NULL;\n}\n\n\nchar * all_tests() {\n    mu_suite_start();\n\n    mu_run_test(test_create);\n    mu_run_test(test_new);\n    mu_run_test(test_set);\n    mu_run_test(test_get);\n    mu_run_test(test_remove);\n    mu_run_test(test_expand_contract);\n    mu_run_test(test_push_pop);\n    mu_run_test(test_destroy);\n\n    return NULL;\n}\n\nRUN_TESTS(all_tests);\n```\n\n这向你展示了所有操作都如何使用，它会使`DArray`的实现变得容易：\n\n```c\n#include <lcthw/darray.h>\n#include <assert.h>\n\n\nDArray *DArray_create(size_t element_size, size_t initial_max)\n{\n    DArray *array = malloc(sizeof(DArray));\n    check_mem(array);\n    array->max = initial_max;\n    check(array->max > 0, \"You must set an initial_max > 0.\");\n\n    array->contents = calloc(initial_max, sizeof(void *));\n    check_mem(array->contents);\n\n    array->end = 0;\n    array->element_size = element_size;\n    array->expand_rate = DEFAULT_EXPAND_RATE;\n\n    return array;\n\nerror:\n    if(array) free(array);\n    return NULL;\n}\n\nvoid DArray_clear(DArray *array)\n{\n    int i = 0;\n    if(array->element_size > 0) {\n        for(i = 0; i < array->max; i++) {\n            if(array->contents[i] != NULL) {\n                free(array->contents[i]);\n            }\n        }\n    }\n}\n\nstatic inline int DArray_resize(DArray *array, size_t newsize)\n{\n    array->max = newsize;\n    check(array->max > 0, \"The newsize must be > 0.\");\n\n    void *contents = realloc(array->contents, array->max * sizeof(void *));\n    // check contents and assume realloc doesn't harm the original on error\n\n    check_mem(contents);\n\n    array->contents = contents;\n\n    return 0;\nerror:\n    return -1;\n}\n\nint DArray_expand(DArray *array)\n{\n    size_t old_max = array->max;\n    check(DArray_resize(array, array->max + array->expand_rate) == 0,\n            \"Failed to expand array to new size: %d\",\n            array->max + (int)array->expand_rate);\n\n    memset(array->contents + old_max, 0, array->expand_rate + 1);\n    return 0;\n\nerror:\n    return -1;\n}\n\nint DArray_contract(DArray *array)\n{\n    int new_size = array->end < (int)array->expand_rate ? (int)array->expand_rate : array->end;\n\n    return DArray_resize(array, new_size + 1);\n}\n\n\nvoid DArray_destroy(DArray *array)\n{\n    if(array) {\n        if(array->contents) free(array->contents);\n        free(array);\n    }\n}\n\nvoid DArray_clear_destroy(DArray *array)\n{\n    DArray_clear(array);\n    DArray_destroy(array);\n}\n\nint DArray_push(DArray *array, void *el)\n{\n    array->contents[array->end] = el;\n    array->end++;\n\n    if(DArray_end(array) >= DArray_max(array)) {\n        return DArray_expand(array);\n    } else {\n        return 0;\n    }\n}\n\nvoid *DArray_pop(DArray *array)\n{\n    check(array->end - 1 >= 0, \"Attempt to pop from empty array.\");\n\n    void *el = DArray_remove(array, array->end - 1);\n    array->end--;\n\n    if(DArray_end(array) > (int)array->expand_rate && DArray_end(array) % array->expand_rate) {\n        DArray_contract(array);\n    }\n\n    return el;\nerror:\n    return NULL;\n}\n```\n\n这占你展示了另一种处理复杂代码的方法，观察头文件并阅读单元测试，而不是一头扎进`.c`实现中。这种“具体的抽象”让你理解代码如何一起工作，并且更容易记住。\n\n## 优点和缺点\n\n`DArray`在你需要这些操作时占优势。\n\n+ 迭代。你可以仅仅使用基本的`for`循环，使用`DArray_count`和`DArray_get`来完成任务。不需要任何特殊的宏。并且由于不处理指针，它非常快。\n+ 索引。你可以使用`DArray_get`和`DArray_set`来随机访问任何元素，但是`List`上你就必须经过第N个元素来访问第N+1个元素。\n+ 销毁。你只需要以两个操作销毁结构体和`content`。但是`List`需要一些列的`free`调用同时遍历每个元素。\n+ 克隆。你只需要复制结构体和`content`，用两步复制整个结构。`List`需要遍历所有元素并且复制每个`ListNode`和值。\n+ 排序。你已经见过了，如果你需要对数据排序，`List`非常麻烦。`DArray`上可以实现所有高效的排序算法，因为你可以随机访问任何元素。\n+ 大量数据。如果你需要储存大量数据，`DArray`由于基于`content`，比起相同数量的`ListNode`占用更少空间而占优。\n\n然而`List`在这些操作上占优势。\n\n+ 在开头插入和移除元素。`DArray`需要特殊的优化来高效地完成它，并且通常还需要一些复制操作。\n+ 分割和连接。`List`只需要复制一些指针就能完成，但是`DArray`需要复制涉及到的所有数组。\n+ 少量数据。如果你只需要存储几个元素，通常使用`List`所需的空间要少于`DArray`，因为`DArray`需要考虑到日后的添加而扩展背后的空间，但是`List`只需要元素所需的空间。\n\n考虑到这些，我更倾向使用`DArray`来完成其它人使用`List`所做的大部分事情。对于任何需要少量节点并且在两端插入删除的，我会使用`List`。我会想你展示两个相似的数据结构，叫做`Stack`和`Queue`，它们也很重要。\n\n## 如何改进\n\n像往常一样，浏览每个函数和操作，并且执行防御性编程检查，以及添加先决条件、不变量等任何可以使实现更健壮的东西。\n\n## 附加题\n\n+ 改进单元测试来覆盖耕作操作，并使用`for`循环来测试迭代。\n+ 研究`DArray`上如何实现冒泡排序和归并排序，但是不要马上实现它们。我会在下一张实现`DArray`的算法，之后你可以完成它。\n+ 为一些常用的操作编写一些性能测试，并与`List`中的相同操作比较。你已经做过很多次了，但是这次需要编写重复执行所涉及操作的单元测试，之后在主运行器中计时。\n+ 观察`DArray_expand`如何使用固定增长（`size + 300`）来实现。通常动态数组都以倍数增长（`size * 2`）的方式实现，但是我发现它会花费无用的内存并且没有真正取得性能收益。测试我的断言，并且看看什么情况下需要倍数增长而不是固定增长。\n"
  },
  {
    "path": "docs/lcthw-zh/ex35.md",
    "content": "# 练习35：排序和搜索\n\n> 原文：[Exercise 35: Sorting And Searching](http://c.learncodethehardway.org/book/ex35.html)\n\n> 译者：[飞龙](https://github.com/wizardforcel)\n\n这个练习中我打算涉及到四个排序算法和一个搜索算法。排序算法是快速排序、堆排序、归并排序和基数排序。之后在你完成基数排序之后，我打算想你展示二分搜索。\n\n然而，我是一个懒人，大多数C标准库都实现了堆排序、快速排序和归并排序算法，你可以直接使用它们：\n\n```c\n#include <lcthw/darray_algos.h>\n#include <stdlib.h>\n\nint DArray_qsort(DArray *array, DArray_compare cmp)\n{\n    qsort(array->contents, DArray_count(array), sizeof(void *), cmp);\n    return 0;\n}\n\nint DArray_heapsort(DArray *array, DArray_compare cmp)\n{\n    return heapsort(array->contents, DArray_count(array), sizeof(void *), cmp);\n}\n\nint DArray_mergesort(DArray *array, DArray_compare cmp)\n{\n    return mergesort(array->contents, DArray_count(array), sizeof(void *), cmp);\n}\n```\n\n这就是`darray_algos.c`文件的整个实现，它在大多数现代Unix系统上都能运行。它们的每一个都使用`DArray_compare`对`contents`中储存的无类型指针进行排序。我也要向你展示这个头文件：\n\n```c\n#ifndef darray_algos_h\n#define darray_algos_h\n\n#include <lcthw/darray.h>\n\ntypedef int (*DArray_compare)(const void *a, const void *b);\n\nint DArray_qsort(DArray *array, DArray_compare cmp);\n\nint DArray_heapsort(DArray *array, DArray_compare cmp);\n\nint DArray_mergesort(DArray *array, DArray_compare cmp);\n\n#endif\n```\n\n大小几乎一样，你也应该能预料到。接下来你可以了解单元测试中这三个函数如何使用：\n\n```c\n#include \"minunit.h\"\n#include <lcthw/darray_algos.h>\n\nint testcmp(char **a, char **b)\n{\n    return strcmp(*a, *b);\n}\n\nDArray *create_words()\n{\n    DArray *result = DArray_create(0, 5);\n    char *words[] = {\"asdfasfd\", \"werwar\", \"13234\", \"asdfasfd\", \"oioj\"};\n    int i = 0;\n\n    for(i = 0; i < 5; i++) {\n        DArray_push(result, words[i]);\n    }\n\n    return result;\n}\n\nint is_sorted(DArray *array)\n{\n    int i = 0;\n\n    for(i = 0; i < DArray_count(array) - 1; i++) {\n        if(strcmp(DArray_get(array, i), DArray_get(array, i+1)) > 0) {\n            return 0;\n        }\n    }\n\n    return 1;\n}\n\nchar *run_sort_test(int (*func)(DArray *, DArray_compare), const char *name)\n{\n    DArray *words = create_words();\n    mu_assert(!is_sorted(words), \"Words should start not sorted.\");\n\n    debug(\"--- Testing %s sorting algorithm\", name);\n    int rc = func(words, (DArray_compare)testcmp);\n    mu_assert(rc == 0, \"sort failed\");\n    mu_assert(is_sorted(words), \"didn't sort it\");\n\n    DArray_destroy(words);\n\n    return NULL;\n}\n\nchar *test_qsort()\n{\n    return run_sort_test(DArray_qsort, \"qsort\");\n}\n\nchar *test_heapsort()\n{\n    return run_sort_test(DArray_heapsort, \"heapsort\");\n}\n\nchar *test_mergesort()\n{\n    return run_sort_test(DArray_mergesort, \"mergesort\");\n}\n\n\nchar * all_tests()\n{\n    mu_suite_start();\n\n    mu_run_test(test_qsort);\n    mu_run_test(test_heapsort);\n    mu_run_test(test_mergesort);\n\n    return NULL;\n}\n\nRUN_TESTS(all_tests);\n```\n\n你需要注意的事情是第四行`testcmp`的定义，它困扰了我一整天。你必须使用`char **`而不是`char *`，因为`qsort`会向你提供指向`content`数组中指针的指针。原因是`qsort`会打扫数组，使用你的比较函数来处理数组中每个元素的指针。因为我在`contents`中存储指针，所以你需要使用指针的指针。\n\n有了这些之后，你只需要实现三个困难的搜索算法，每个大约20行。你应该在这里停下来，不过这本书的一部分就是学习这些算法的原理，附加题会涉及到实现这些算法。\n\n## 基数排序和二分搜索\n\n既然你打算自己实现快速排序、堆排序和归并排序，我打算向你展示一个流行的算法叫做基数排序。它的实用性很小，只能用于整数数组，并且看上去像魔法一样。这里我打算常见一个特殊的数据结构，叫做`RadixMap`，用于将一个整数映射为另一个。\n\n下面是为新算法创建的头文件，其中也含有数据结构：\n\n```c\n#ifndef _radixmap_h\n#include <stdint.h>\n\ntypedef union RMElement {\n    uint64_t raw;\n    struct {\n        uint32_t key;\n        uint32_t value;\n    } data;\n} RMElement;\n\ntypedef struct RadixMap {\n    size_t max;\n    size_t end;\n    uint32_t counter;\n    RMElement *contents;\n    RMElement *temp;\n} RadixMap;\n\n\nRadixMap *RadixMap_create(size_t max);\n\nvoid RadixMap_destroy(RadixMap *map);\n\nvoid RadixMap_sort(RadixMap *map);\n\nRMElement *RadixMap_find(RadixMap *map, uint32_t key);\n\nint RadixMap_add(RadixMap *map, uint32_t key, uint32_t value);\n\nint RadixMap_delete(RadixMap *map, RMElement *el);\n\n#endif\n```\n\n你看到了其中有许多和`Dynamic Array`或`List`数据结构相同的操作，不同就在于我只处理固定32位大小的`uint32_t`正忽视。我也会想你介绍C语言的一个新概念，叫做`union`。\n\n## C联合体\n\n联合体是使用不同方式引用内存中同一块区域的方法。它们的工作方式，就像你把它定义为`sturct`，然而，每个元素共享同一片内存区域。你可以认为，联合体是内存中的一幅画，所有颜色不同的元素都重叠在它上面。\n\n它可以用于节约内存，或在不同格式之间转换内存块。它的第一个用途就是实现“可变类型”，你可以创建一个带有类型“标签”的结构体，之后在其中创建含有多种类型的联合体。用于在内存的不同格式之间转换时，只需要定义两个结构体，访问正确的那个类型。\n\n首先让我向你展示如何使用C联合体构造可变类型：\n\n```c\n#include <stdio.h>\n\ntypedef enum {\n    TYPE_INT,\n    TYPE_FLOAT,\n    TYPE_STRING,\n} VariantType;\n\nstruct Variant {\n    VariantType type;\n    union {\n        int as_integer;\n        float as_float;\n        char *as_string;\n    } data;\n};\n\ntypedef struct Variant Variant;\n\nvoid Variant_print(Variant *var)\n{\n    switch(var->type) {\n        case TYPE_INT:\n           printf(\"INT: %d\\n\", var->data.as_integer);\n           break;\n        case TYPE_FLOAT:\n           printf(\"FLOAT: %f\\n\", var->data.as_float);\n           break;\n        case TYPE_STRING:\n           printf(\"STRING: %s\\n\", var->data.as_string);\n           break;\n        default:\n           printf(\"UNKNOWN TYPE: %d\", var->type);\n    }\n}\n\nint main(int argc, char *argv[])\n{\n    Variant a_int = {.type = TYPE_INT, .data.as_integer = 100};\n    Variant a_float = {.type = TYPE_FLOAT, .data.as_float = 100.34};\n    Variant a_string = {.type = TYPE_STRING, .data.as_string = \"YO DUDE!\"};\n\n    Variant_print(&a_int);\n    Variant_print(&a_float);\n    Variant_print(&a_string);\n\n    // here's how you access them\n    a_int.data.as_integer = 200;\n    a_float.data.as_float = 2.345;\n    a_string.data.as_string = \"Hi there.\";\n\n    Variant_print(&a_int);\n    Variant_print(&a_float);\n    Variant_print(&a_string);\n\n    return 0;\n}\n```\n\n你可以在许多动态语言实现中发现它。对于为语言中所有基本类型，代码中首先定义了一些带有变迁的可变类型，之后通常给你所创建的类型打上`object`标签。这样的好处就是`Variant`通常只需要`VariantType type`标签的空间，加上联合体最大成员的空间，因为C将`Variant.data`的每个元素堆起来，它们是重叠的，只保证有足够的空间放下最大的元素。\n\n`radixmap.h`文件中我创建了`RMElement`联合体，用于在类型之间转换内存块。这里，我希望存储`uint64_t`定长整数用于排序目录，但是我也希望使用两个`uint32_t`用于表示数据的`key`和`value`对。通过使用联合体我就能够使用所需的两种不同方法来访问内存。\n\n## 实现\n\n接下来是实际的`RadixMap`对于这些操作的实现：\n\n```c\n/*\n* Based on code by Andre Reinald then heavily modified by Zed A. Shaw.\n*/\n\n#include <stdio.h>\n#include <stdlib.h>\n#include <assert.h>\n#include <lcthw/radixmap.h>\n#include <lcthw/dbg.h>\n\nRadixMap *RadixMap_create(size_t max)\n{\n    RadixMap *map = calloc(sizeof(RadixMap), 1);\n    check_mem(map);\n\n    map->contents = calloc(sizeof(RMElement), max + 1);\n    check_mem(map->contents);\n\n    map->temp = calloc(sizeof(RMElement), max + 1);\n    check_mem(map->temp);\n\n    map->max = max;\n    map->end = 0;\n\n    return map;\nerror:\n    return NULL;\n}\n\nvoid RadixMap_destroy(RadixMap *map)\n{\n    if(map) {\n        free(map->contents);\n        free(map->temp);\n        free(map);\n    }\n}\n\n\n#define ByteOf(x,y) (((uint8_t *)x)[(y)])\n\nstatic inline void radix_sort(short offset, uint64_t max, uint64_t *source, uint64_t *dest)\n{\n    uint64_t count[256] = {0};\n    uint64_t *cp = NULL;\n    uint64_t *sp = NULL;\n    uint64_t *end = NULL;\n    uint64_t s = 0;\n    uint64_t c = 0;\n\n    // count occurences of every byte value\n    for (sp = source, end = source + max; sp < end; sp++) {\n        count[ByteOf(sp, offset)]++;\n    }\n\n    // transform count into index by summing elements and storing into same array\n    for (s = 0, cp = count, end = count + 256; cp < end; cp++) {\n        c = *cp;\n        *cp = s;\n        s += c;\n    }\n\n    // fill dest with the right values in the right place\n    for (sp = source, end = source + max; sp < end; sp++) {\n        cp = count + ByteOf(sp, offset);\n        dest[*cp] = *sp;\n        ++(*cp);\n    }\n}\n\nvoid RadixMap_sort(RadixMap *map)\n{\n    uint64_t *source = &map->contents[0].raw;\n    uint64_t *temp = &map->temp[0].raw;\n\n    radix_sort(0, map->end, source, temp);\n    radix_sort(1, map->end, temp, source);\n    radix_sort(2, map->end, source, temp);\n    radix_sort(3, map->end, temp, source);\n}\n\nRMElement *RadixMap_find(RadixMap *map, uint32_t to_find)\n{\n    int low = 0;\n    int high = map->end - 1;\n    RMElement *data = map->contents;\n\n    while (low <= high) {\n        int middle = low + (high - low)/2;\n        uint32_t key = data[middle].data.key;\n\n        if (to_find < key) {\n            high = middle - 1;\n        } else if (to_find > key) {\n            low = middle + 1;\n        } else {\n            return &data[middle];\n        }\n    }\n\n    return NULL;\n}\n\nint RadixMap_add(RadixMap *map, uint32_t key, uint32_t value)\n{\n    check(key < UINT32_MAX, \"Key can't be equal to UINT32_MAX.\");\n\n    RMElement element = {.data = {.key = key, .value = value}};\n    check(map->end + 1 < map->max, \"RadixMap is full.\");\n\n    map->contents[map->end++] = element;\n\n    RadixMap_sort(map);\n\n    return 0;\n\nerror:\n    return -1;\n}\n\nint RadixMap_delete(RadixMap *map, RMElement *el)\n{\n    check(map->end > 0, \"There is nothing to delete.\");\n    check(el != NULL, \"Can't delete a NULL element.\");\n\n    el->data.key = UINT32_MAX;\n\n    if(map->end > 1) {\n        // don't bother resorting a map of 1 length\n        RadixMap_sort(map);\n    }\n\n    map->end--;\n\n    return 0;\nerror:\n    return -1;\n}\n```\n\n像往常一样键入它并使它通过单元测试，之后我会解释它。尤其要注意`radix_sort`函数，我实现它的方法非常特别。\n\n```c\n#include \"minunit.h\"\n#include <lcthw/radixmap.h>\n#include <time.h>\n\nstatic int make_random(RadixMap *map)\n{\n    size_t i = 0;\n\n    for (i = 0; i < map->max - 1; i++) {\n        uint32_t key = (uint32_t)(rand() | (rand() << 16));\n        check(RadixMap_add(map, key, i) == 0, \"Failed to add key %u.\", key);\n    }\n\n    return i;\n\nerror:\n    return 0;\n}\n\nstatic int check_order(RadixMap *map)\n{\n    RMElement d1, d2;\n    unsigned int i = 0;\n\n    // only signal errors if any (should not be)\n    for (i = 0; map->end > 0 && i < map->end-1; i++) {\n        d1 = map->contents[i];\n        d2 = map->contents[i+1];\n\n        if(d1.data.key > d2.data.key) {\n            debug(\"FAIL:i=%u, key: %u, value: %u, equals max? %d\\n\", i, d1.data.key, d1.data.value,\n                    d2.data.key == UINT32_MAX);\n            return 0;\n        }\n    }\n\n    return 1;\n}\n\nstatic int test_search(RadixMap *map)\n{\n    unsigned i = 0;\n    RMElement *d = NULL;\n    RMElement *found = NULL;\n\n    for(i = map->end / 2; i < map->end; i++) {\n        d = &map->contents[i];\n        found = RadixMap_find(map, d->data.key);\n        check(found != NULL, \"Didn't find %u at %u.\", d->data.key, i);\n        check(found->data.key == d->data.key, \"Got the wrong result: %p:%u looking for %u at %u\",\n                found, found->data.key, d->data.key, i);\n    }\n\n    return 1;\nerror:\n    return 0;\n}\n\n// test for big number of elements\nstatic char *test_operations()\n{\n    size_t N = 200;\n\n    RadixMap *map = RadixMap_create(N);\n    mu_assert(map != NULL, \"Failed to make the map.\");\n    mu_assert(make_random(map), \"Didn't make a random fake radix map.\");\n\n    RadixMap_sort(map);\n    mu_assert(check_order(map), \"Failed to properly sort the RadixMap.\");\n\n    mu_assert(test_search(map), \"Failed the search test.\");\n    mu_assert(check_order(map), \"RadixMap didn't stay sorted after search.\");\n\n    while(map->end > 0) {\n        RMElement *el = RadixMap_find(map, map->contents[map->end / 2].data.key);\n        mu_assert(el != NULL, \"Should get a result.\");\n\n        size_t old_end = map->end;\n\n        mu_assert(RadixMap_delete(map, el) == 0, \"Didn't delete it.\");\n        mu_assert(old_end - 1 == map->end, \"Wrong size after delete.\");\n\n        // test that the end is now the old value, but uint32 max so it trails off\n        mu_assert(check_order(map), \"RadixMap didn't stay sorted after delete.\");\n    }\n\n    RadixMap_destroy(map);\n\n    return NULL;\n}\n\n\nchar *all_tests()\n{\n    mu_suite_start();\n    srand(time(NULL));\n\n    mu_run_test(test_operations);\n\n    return NULL;\n}\n\nRUN_TESTS(all_tests);\n```\n\n我不应该向你解释关于测试的过多东西，它只是模拟将随机正是放入`RadixMap`，确保你可以可靠地将其取出。也不是非常有趣。\n\n在`radixmap.c`中的大多数操作都易于理解，如果你阅读代码的话。下面是每个基本函数作用及其工作原理的描述：\n\nRadixMap_create\n\n像往常一样，我分配了结构体所需的内存，结构体在`radixmap.h`中定义。当后面涉及到`radix_sort`时我会使用`temp`和`contents`。\n\nRadixMap_destroy\n\n同样，销毁我所创建的东西。\n\nradix_sort\n\n这个数据结构的灵魂，我会在下一节中解释其作用。\n\nRadixMap_sort\n\n它使用了`radix_sort`函数来实际对`contents`进行排序。\n\nRadixMap_find\n\n使用二分搜索算法来寻找提供的`key`，我之后会解释它的原理。\n\nRadixMap_add\n\n使用`RadixMap_sort`函数，它会在末尾添加`key`和`value`，然后简单地重新排序使一切元素都有序。一旦排序完，`RadixMap_find`会正确工作，因为它是二分搜索。\n\nRadixMap_delete\n\n工作方式类似`RadixMap_add`，除了“删除”结构中的元素，通过将它们的值设为无符号的32为整数的最大值，也就是`UINT32_MAX`。这意味着你不能使用这个值作为合法的键，但是它是元素删除变得容易。简单设置它之后排序，它会被移动到末尾，这就算删除了。\n\n学习我所描述的代码，接下来还剩`RadixMap_sort`，`radix_sort`和`RadixMap_find`需要了解。\n\n## RadixMap_find 和二分搜索\n\n我首先以二分搜索如何实现开始。二分搜索是一种简单算法，大多数人都可以直观地理解。实际上，你可以取一叠游戏卡片（或带有数字的卡片）来手动操作。下面是该函数的工作方式，也是二分搜索的原理：\n\n+ 基于数组大小设置上界和下界。\n+ 获取上下界之间的中间元素。\n+ 如果键小于这个元素的值，就一定在它前面，所以上界设置为中间元素。\n+ 如果键大于这个元素的值，就一定在它后面，所以下界设置为中间元素。\n+ 继续循环直到上界和下界越过了彼此。如果退出了循环则没有找到。\n\n你实际上所做的事情是，通过挑选中间的值来比较，猜出`key`可能的位置。由于数据是有序的，你知道`key`一定会在它前面或者后面，这样就能把搜索区域分成两半。之后你继续搜索知道找到他，或者越过了边界并穷尽了搜索空间。\n\n## RadixMap_sort 和 radix_sort\n\n如果你事先手动模拟基数排序，它就很易于理解。这个算法利用了一个现象，数字都以十进制字符的序列来表示，按照“不重要”到“重要”的顺序排列。之后它通过十进制字符来选取数字并且将它们储存在桶中，当它处理完所有字符时，数字就排好序了。一开始它看上去像是魔法，浏览代码也的确如此，但是你要尝试手动执行它。\n\n为了解释这个算法，需要先写下一组三位的十进制数，以随机的顺序，假设就是223、912、275、100、633、120 和 380。\n\n+ 按照它们的个位，将数字放入桶中：`[380, 100, 120], [912], [633, 223], [275]`。\n+ 现在遍历每个桶中的数字，接着按十位排序：`[100], [912], [120, 223], [633], [275], [380]`。\n+ 现在每个桶都包含了按照个位和十位排序后的数字。接着我需要按照这个顺序遍历，并把它们放入最后百位的桶中：`[100, 120], [223, 275], [380], [633], [912]`。\n+ 到现在为止，每个数字都按照百位、十位和个位排序，并且如果我按照顺序遍历每个桶，我会得到最终排序的结果：`100, 120, 223, 275, 380, 633, 912`。\n\n确保你多次重复了这个过程，便于你理解它如何工作。这实在是一种机智的算法，并且最重要的是它对于任何大小的数字都有效。所以你可以用它来排序比较大的数字，因为你一次只是处理一位。\n\n在我的环境下，“字符”是独立的8位字节，所以我需要256个桶来储存这些数字按照字节的分布结果。我需要一种方法来储存它，并且不需要花费太多的空间。如果你查看`radix_sort`，首先我会构建`count`直方图，便于我了解对于给定的`offset`，每个字节的频率。\n\n一旦我知道了每一种字节的数量（共有256种），我就可以将目标数组用于存储这些值的分布。比如，如果0x00的数量为10个，我就可以将它们放在目标数组的前10个位置中。这可以让我索引到它们在目标数组中的位置，这就是`radix_sort`中的第二个`for`循环。\n\n最后，当我知道它们在目标数组中储存在哪里，我只是遍历`source`数组对于当前`offset`的所有字节，并且将数值按顺序放入它们的位置中。`ByteOf`宏的使用有助于保持代码整洁，因为它需要一些指针的黑魔法，但是最后当`for`循环结束之后，所有整数都会按照它们的字节放入桶中。\n\n我在`RadixMap_sort`中对这些64位的整数按照它们的前32位进行排序，这非常有意思。还记得我是如何将键和值放入`RMElement`类型的联合体了吗？这意味着如果要按照键来对这个数组排序，我只需要对每个整数前4个字节（32位/8位每字节）进行排序。\n\n如果你观察`RadixMap_sort`，你会看到我获取了`contents`和`temp`的便利指针，用于源数组和目标数组，之后我四次调用`radix_sort`。每次调用我将源数组和目标数组替换为下一字节的情况。当我完成时，`radix_sort`就完成了任务，并且`contents`中也有了最后的结果。\n\n## 如何改进\n\n这个实现有个很大的缺点，就是它遍历了整个数组四次。它执行地很快，但是如果你通过需要排序的数值大小来限制排序的总量，会更好一些。\n\n有两个方法可以用于改进这个实现：\n\n+ 使用二分搜索来寻找新元素的最小位置，只对这个位置到微末之间进行排序。你需要找到它，将新元素放到末尾，之后对它们之间进行排序。大多数情况下这会显著地缩减排序范围。\n+ 跟踪当前所使用的最大的键，之后只对足够的位数进行排序，来处理这个键。你也可以跟踪最小的数值，之后只对范围中必要的字节进行排序。为了这样做，你需要关心CPU的整数存储顺序（大小端序）。\n\n## 附加题\n\n+ 实现快速排序、堆排序和归并排序，并且提供一个`#define`让其他人在二者（标准库和你的实现）当中进行选择，或者创建另一套不同名称的函数。使用我教给你的技巧，阅读维基百科的算法页面，之后参照伪代码来实现它。\n+ 对比你的实现和标准库实现的性能。\n+ 使用这些排序函数创建`DArray_sort_add`，它可以向`DArray`添加元素，但是随后对数组排序。\n+ 编写`DArray_find`，使用`RadixMap_find`中的二分搜索算法和`DArray_compare`，来在有序的`DArray`中寻找元素。\n"
  },
  {
    "path": "docs/lcthw-zh/ex36.md",
    "content": "# 练习36：更安全的字符串\n\n> 原文：[Exercise 36: Safer Strings](http://c.learncodethehardway.org/book/ex36.html)\n\n> 译者：[飞龙](https://github.com/wizardforcel)\n\n我已经在练习26中，构建`devpkg`的时候介绍了[Better String](http://bstring.sourceforge.net/)库。这个练习让你从现在开始熟悉`bstring`库，并且明白C风格字符串为什么十分糟糕。之后你需要修改`liblcthw`的代码来使用`bstring`。\n\n## 为什么C风格字符串十分糟糕\n\n当人们谈论C的问题时，“字符串”的概念永远是首要缺陷之一。你已经用过它们，并且我也谈论过它们的种种缺陷，但是对为什么C字符串拥有缺陷，以及为什么一直是这样没有明确的解释。我会试着现在做出解释，部分原因是C风格字符串经过数十年的使用，有足够的证据表明它们是个非常糟糕的东西。\n\n对于给定的任何C风格字符串，都不可能验证它是否有效。\n\n+ 以`'\\0'`结尾的C字符串是有效的。\n+ 任何处理无效C字符串的循环都是无限的（或者造成缓冲区溢出）。\n+ C字符串没有确定的长度，所以检查它们的唯一方法就是遍历它来观察循环是否正确终止。\n+ 所以，不通过有限的循环就不可能验证C字符串。\n\n这个逻辑非常简单。你不能编写一个循环来验证C字符串是否有效，因为无效的字符串导致循环永远不会停止。就是这样，唯一的解决方案就是包含大小。一旦你知道了大小，你可以避免无限循环问题。如果你观察练习27中我向你展示的两个函数：\n\n> 译者注：检验C风格字符串是否有效等价于“停机问题”，这是一个非常著名的不可解问题。\n\n```c\nvoid copy(char to[], char from[])\n{\n    int i = 0;\n\n    // while loop will not end if from isn't '\\0' terminated\n    while((to[i] = from[i]) != '\\0') {\n        ++i;\n    }\n}\n\nint safercopy(int from_len, char *from, int to_len, char *to)\n{\n    int i = 0;\n    int max = from_len > to_len - 1 ? to_len - 1 : from_len;\n\n    // to_len must have at least 1 byte\n    if(from_len < 0 || to_len <= 0) return -1;\n\n    for(i = 0; i < max; i++) {\n        to[i] = from[i];\n    }\n\n    to[to_len - 1] = '\\0';\n\n    return i;\n}\n```\n\n想象你想要向`copy`函数添加检查来确保`from`字符串有效。你该怎么做呢？你编写了一个循环来检查字符串是否已`'\\0'`结尾。哦，等一下，如果字符串不以`'\\0'`结尾，那它怎么让循环停下？不可能停下，所以无解。\n\n无论你怎么做，你都不能在不知道字符串长度的情况下检查C字符串的有效性，这里`safercopy`包含了程度。这个函数没有相同的问题，因为他的循环一定会中止，即使你传入了错误的大小，大小也是有限的。\n\n> 译者注：但是问题来了，对于一个C字符串，你怎么获取其大小？你需要在这个函数之前调用`strlen`，又是一个无限循环问题。\n\n于是，`bstring`库所做的事情就是创建一个结构体，它总是包含字符串长度。由于这个长度对于`bstring`来说总是可访问的，它上面的所有操作都会更安全。循环是有限的，内容也是有效的，并且这个主要的缺陷也不存在了。BString库也带有大量所需的字串操作，比如分割、格式化、搜索，并且大多数都会正确并安全地执行。\n\n`bstring`中也可能有缺陷，但是经过这么长时间，可能性已经很低了。`glibc`中也有缺陷，所以你让程序员怎么做才好呢？\n\n## 使用 bstrlib\n\n有很多改进后的字符串库，但是我最喜欢`bstrlib`，因为它只有一个程序集，并且具有大多数所需的字符串功能。你已经在使用它了，所以这个练习中你需要从[Better String](http://bstring.sourceforge.net/)获取两个文件，`bstrlib.c`和`bstrlib.h`。\n\n下面是我在`liblcthw`项目目录里所做的事情：\n\n```sh\n$ mkdir bstrlib\n$ cd bstrlib/\n$ unzip ~/Downloads/bstrlib-05122010.zip\nArchive:  /Users/zedshaw/Downloads/bstrlib-05122010.zip\n...\n$ ls\nbsafe.c             bstraux.c       bstrlib.h       bstrwrap.h      license.txt     test.cpp\nbsafe.h             bstraux.h       bstrlib.txt     cpptest.cpp     porting.txt     testaux.c\nbstest.c    bstrlib.c       bstrwrap.cpp    gpl.txt         security.txt\n$ mv bstrlib.h bstrlib.c ../src/lcthw/\n$ cd ../\n$ rm -rf bstrlib\n# make the edits\n$ vim src/lcthw/bstrlib.c\n$ make clean all\n...\n$\n```\n在第14行你可以看到，我编辑了`bstrlib.c`文件，来将它移动到新的位置，并且修复OSX上的bug。下面是差异：\n\n```diff\n25c25\n< #include \"bstrlib.h\"\n---\n> #include <lcthw/bstrlib.h>\n2759c2759\n< #ifdef __GNUC__\n---\n> #if defined(__GNUC__) && !defined(__APPLE__)\n```\n\n我把包含修改为`<lcthw/bstrlib.h>`，然后修复2759行`ifdef`的问题。\n\n## 学习使用该库\n\n这个练习很短，只是让你准备好剩余的练习，它们会用到这个库。接下来两个联系中，我会使用`bstrlib.c`来创建Hashmap`数据结构。\n\n你现在应该阅读头文件和实现，之后编写`tests/bstr_tests.c`来测试下列函数，来熟悉这个库：\n\n`bfromcstr`\n\n从C风格字符串中创建一个`bstring`。\n\n`blk2bstr`\n\n与上面相同，但是可以提供缓冲区长度。\n\n`bstrcpy`\n\n复制`bstring`。\n\n`bassign`\n\n将一个`bstring`赋值为另一个。\n\n`bassigncstr`\n\n将`bsting`的内容设置为C字符串的内容。\n\n`bassignblk`\n\n将`bsting`的内容设置为C字符串的内容，但是可以提供长度。\n\n`bdestroy`\n\n销毁`bstring`。\n\n`bconcat`\n\n在一个`bstring`末尾连接另一个。\n\n`bstricmp`\n\n比较两个`bstring`，返回值与`strcmp`相同。\n\n`biseq`\n\n检查两个`bstring`是否相等。\n\n`binstr`\n\n判断一个`bstring`是否被包含于另一个。\n\n`bfindreplace`\n\n在一个`bstring`中寻找另一个，并且将其替换为别的。\n\n`bsplit`\n\n将`bstring`分割为`bstrList`。\n\n`bformat`\n\n执行字符串格式化，十分便利。\n\n`blength`\n\n获取`bstring`的长度。\n\n`bdata`\n\n获取`bstring`的数据。\n\n`bchar`\n\n获得`bstring`中的字符。\n\n你的测试应该覆盖到所有这些操作，以及你从头文件中发现的更多有趣的东西。在`valgrind`下运行测试，确保内存使用正确。\n"
  },
  {
    "path": "docs/lcthw-zh/ex37.md",
    "content": "# 练习37：哈希表\n\n> 原文：[Exercise 37: Hashmaps](http://c.learncodethehardway.org/book/ex37.html)\n\n> 译者：[飞龙](https://github.com/wizardforcel)\n\n哈希表（`HashMap`、`HashTable`以及`Dictionary`）广泛用于许多动态编程语言来储存键值对的数据。哈希表通过在键上执行“哈希”运算产生整数，之后使用它来寻找相应的桶来获取或储存值。它是非常快速的使用数据结构，因为它适用于任何数据并且易于实现。\n\n下面是哈希表（也叫作字典）的一个使用示例：\n\n```py\nfruit_weights = {'Apples': 10, 'Oranges': 100, 'Grapes': 1.0}\n\nfor key, value in fruit_weights.items():\n    print key, \"=\", value\n```\n\n几乎所有现代语言都具备这种特性，所以许多人写完代码都不知道它实际上如何工作。通过在C中创建`Hashmap`数据结构，我会向你展示它的工作原理。我会从头文件开始，来谈论整个数据结构。\n\n```c\n#ifndef _lcthw_Hashmap_h\n#define _lcthw_Hashmap_h\n\n#include <stdint.h>\n#include <lcthw/darray.h>\n\n#define DEFAULT_NUMBER_OF_BUCKETS 100\n\ntypedef int (*Hashmap_compare)(void *a, void *b);\ntypedef uint32_t (*Hashmap_hash)(void *key);\n\ntypedef struct Hashmap {\n    DArray *buckets;\n    Hashmap_compare compare;\n    Hashmap_hash hash;\n} Hashmap;\n\ntypedef struct HashmapNode {\n    void *key;\n    void *data;\n    uint32_t hash;\n} HashmapNode;\n\ntypedef int (*Hashmap_traverse_cb)(HashmapNode *node);\n\nHashmap *Hashmap_create(Hashmap_compare compare, Hashmap_hash);\nvoid Hashmap_destroy(Hashmap *map);\n\nint Hashmap_set(Hashmap *map, void *key, void *data);\nvoid *Hashmap_get(Hashmap *map, void *key);\n\nint Hashmap_traverse(Hashmap *map, Hashmap_traverse_cb traverse_cb);\n\nvoid *Hashmap_delete(Hashmap *map, void *key);\n\n#endif\n```\n\n这个结构就是`Hashmap`，含有许多`HashmapNode`节点。观察`Hashmap`你会看到它类似这样：\n\n`DArray *buckets`\n\n一个动态数组，设置为100个桶的固定大小。每个桶会含有一个`DArray`，来实际存档`HashmapNode`对。\n\n`Hashmap_compare compare`\n\n这是一个比较函数，被`Hashmap`用于实际用过键寻找元素。它应该和其它的比较函数类似，并且默认设置为`bstrcmp`来比较字符串。\n\n`Hashmap_hash`\n\n这是哈希函数，它用于接收键，处理它的内容，之后产生一个`uint32_t`索引数值。之后你会看到默认的实现。\n\n这些告诉了你数据如何存储，但是用作`buckets`的`DArray`还没有创建。要记住它具有二层结构；\n\n+ 第一层有100个桶，数据基于它们的哈希值储存在桶中。\n+ 每个桶都是一个`DArray`，其中含有`HashmapNode`，添加时只是简单地附加到末尾。\n\n`HashMapNode`由下面三个元素组成：\n\n`void *key`\n\n键值对的键。\n\n`void *value`\n\n键值对的值。\n\n`uint32_t hash`\n\n计算出的哈希值，它用于使查找该节点更加迅速，只要判断键是否相等。\n\n有文件的剩余部分没有新的东西，所以我现在可以向你展示`hashmap.c`的实现了：\n\n```c\n#undef NDEBUG\n#include <stdint.h>\n#include <lcthw/hashmap.h>\n#include <lcthw/dbg.h>\n#include <lcthw/bstrlib.h>\n\nstatic int default_compare(void *a, void *b)\n{\n    return bstrcmp((bstring)a, (bstring)b);\n}\n\n/**\n * Simple Bob Jenkins's hash algorithm taken from the\n * wikipedia description.\n */\nstatic uint32_t default_hash(void *a)\n{\n    size_t len = blength((bstring)a);\n    char *key = bdata((bstring)a);\n    uint32_t hash = 0;\n    uint32_t i = 0;\n\n    for(hash = i = 0; i < len; ++i)\n    {\n        hash += key[i];\n        hash += (hash << 10);\n        hash ^= (hash >> 6);\n    }\n\n    hash += (hash << 3);\n    hash ^= (hash >> 11);\n    hash += (hash << 15);\n\n    return hash;\n}\n\n\nHashmap *Hashmap_create(Hashmap_compare compare, Hashmap_hash hash)\n{\n    Hashmap *map = calloc(1, sizeof(Hashmap));\n    check_mem(map);\n\n    map->compare = compare == NULL ? default_compare : compare;\n    map->hash = hash == NULL ? default_hash : hash;\n    map->buckets = DArray_create(sizeof(DArray *), DEFAULT_NUMBER_OF_BUCKETS);\n    map->buckets->end = map->buckets->max; // fake out expanding it\n    check_mem(map->buckets);\n\n    return map;\n\nerror:\n    if(map) {\n        Hashmap_destroy(map);\n    }\n\n    return NULL;\n}\n\n\nvoid Hashmap_destroy(Hashmap *map)\n{\n    int i = 0;\n    int j = 0;\n\n    if(map) {\n        if(map->buckets) {\n            for(i = 0; i < DArray_count(map->buckets); i++) {\n                DArray *bucket = DArray_get(map->buckets, i);\n                if(bucket) {\n                    for(j = 0; j < DArray_count(bucket); j++) {\n                        free(DArray_get(bucket, j));\n                    }\n                    DArray_destroy(bucket);\n                }\n            }\n            DArray_destroy(map->buckets);\n        }\n\n        free(map);\n    }\n}\n\nstatic inline HashmapNode *Hashmap_node_create(int hash, void *key, void *data)\n{\n    HashmapNode *node = calloc(1, sizeof(HashmapNode));\n    check_mem(node);\n\n    node->key = key;\n    node->data = data;\n    node->hash = hash;\n\n    return node;\n\nerror:\n    return NULL;\n}\n\n\nstatic inline DArray *Hashmap_find_bucket(Hashmap *map, void *key,\n        int create, uint32_t *hash_out)\n{\n    uint32_t hash = map->hash(key);\n    int bucket_n = hash % DEFAULT_NUMBER_OF_BUCKETS;\n    check(bucket_n >= 0, \"Invalid bucket found: %d\", bucket_n);\n    *hash_out = hash; // store it for the return so the caller can use it\n\n\n    DArray *bucket = DArray_get(map->buckets, bucket_n);\n\n    if(!bucket && create) {\n        // new bucket, set it up\n        bucket = DArray_create(sizeof(void *), DEFAULT_NUMBER_OF_BUCKETS);\n        check_mem(bucket);\n        DArray_set(map->buckets, bucket_n, bucket);\n    }\n\n    return bucket;\n\nerror:\n    return NULL;\n}\n\n\nint Hashmap_set(Hashmap *map, void *key, void *data)\n{\n    uint32_t hash = 0;\n    DArray *bucket = Hashmap_find_bucket(map, key, 1, &hash);\n    check(bucket, \"Error can't create bucket.\");\n\n    HashmapNode *node = Hashmap_node_create(hash, key, data);\n    check_mem(node);\n\n    DArray_push(bucket, node);\n\n    return 0;\n\nerror:\n    return -1;\n}\n\nstatic inline int Hashmap_get_node(Hashmap *map, uint32_t hash, DArray *bucket, void *key)\n{\n    int i = 0;\n\n    for(i = 0; i < DArray_end(bucket); i++) {\n        debug(\"TRY: %d\", i);\n        HashmapNode *node = DArray_get(bucket, i);\n        if(node->hash == hash && map->compare(node->key, key) == 0) {\n            return i;\n        }\n    }\n\n    return -1;\n}\n\nvoid *Hashmap_get(Hashmap *map, void *key)\n{\n    uint32_t hash = 0;\n    DArray *bucket = Hashmap_find_bucket(map, key, 0, &hash);\n    if(!bucket) return NULL;\n\n    int i = Hashmap_get_node(map, hash, bucket, key);\n    if(i == -1) return NULL;\n\n    HashmapNode *node = DArray_get(bucket, i);\n    check(node != NULL, \"Failed to get node from bucket when it should exist.\");\n\n    return node->data;\n\nerror: // fallthrough\n    return NULL;\n}\n\n\nint Hashmap_traverse(Hashmap *map, Hashmap_traverse_cb traverse_cb)\n{\n    int i = 0;\n    int j = 0;\n    int rc = 0;\n\n    for(i = 0; i < DArray_count(map->buckets); i++) {\n        DArray *bucket = DArray_get(map->buckets, i);\n        if(bucket) {\n            for(j = 0; j < DArray_count(bucket); j++) {\n                HashmapNode *node = DArray_get(bucket, j);\n                rc = traverse_cb(node);\n                if(rc != 0) return rc;\n            }\n        }\n    }\n\n    return 0;\n}\n\nvoid *Hashmap_delete(Hashmap *map, void *key)\n{\n    uint32_t hash = 0;\n    DArray *bucket = Hashmap_find_bucket(map, key, 0, &hash);\n    if(!bucket) return NULL;\n\n    int i = Hashmap_get_node(map, hash, bucket, key);\n    if(i == -1) return NULL;\n\n    HashmapNode *node = DArray_get(bucket, i);\n    void *data = node->data;\n    free(node);\n\n    HashmapNode *ending = DArray_pop(bucket);\n\n    if(ending != node) {\n        // alright looks like it's not the last one, swap it\n        DArray_set(bucket, i, ending);\n    }\n\n    return data;\n}\n```\n\n这个实现中并没有什么复杂的东西，但是`default_hash`和`Hashmap_find_bucket`需要一些解释。当你使用`Hashmap_create`时，你可以传入任何定制的比较和哈希函数。但是如果你没有则会使用`default_compare`和`default_hash`函数。\n\n需要观察的第一件事，是`default_hash`的行为。这是一个简单的哈希函数，叫做“Jenkins hash”，以Bob Jenkins的名字命名。我从[维基百科](http://en.wikipedia.org/wiki/Jenkins_hash_function)上获得了这个算法。它仅仅遍历键（`bstring`）的每个字节来计算哈希，以便得出`uint32_t`的结果。它使用一些加法和异或运算来实现。\n\n哈希函数有很多中，它们具有不同的特性，然而一旦你选择了一种，就需要一种方法来使用它找到正确的桶。`Hashmap_find_bucket`像这样实现它：\n\n+ 首先调用` map->hash(key)`来获得键的哈希值。\n+ 之后使用`hash % DEFAULT_NUMBER_OF_BUCKETS`，这样无论哈希值有多大，都能找到匹配的桶。\n+ 找到桶之后，它是个`DArray`，可能还没有创建，这取决与`create`变量的内容。\n+ 一旦找到了正确的`DArray`桶，就会将它返回，并且`hash_out`变量用于向调用者提供所找到的哈希值。\n\n其它函数都使用`Hashmap_find_bucket`来完成工作：\n\n+ 设置键值对涉及到找到正确的桶，之后创建`HashmapNode`，将它添加到桶中。\n+ 获取键值涉及到找到正确的桶，之后找到匹配`hash`和`key`的`HashmapNode`。\n+ 删除元素也需要找到正确的桶，找到所需的节点，之后通过与末尾的节点交换位置来删除。\n\n你需要学习的唯一一个其他函数是`Hashmap_travers`，它仅仅遍历每个桶，对于任何含有值的桶，在每个值上调用`traverse_cb`。这就是扫描整个`Hashmap`的办法。\n\n## 单元测试\n\n最后你需要编写单元测试，对于所有这些操作：\n\n```c\n#include \"minunit.h\"\n#include <lcthw/hashmap.h>\n#include <assert.h>\n#include <lcthw/bstrlib.h>\n\nHashmap *map = NULL;\nstatic int traverse_called = 0;\nstruct tagbstring test1 = bsStatic(\"test data 1\");\nstruct tagbstring test2 = bsStatic(\"test data 2\");\nstruct tagbstring test3 = bsStatic(\"xest data 3\");\nstruct tagbstring expect1 = bsStatic(\"THE VALUE 1\");\nstruct tagbstring expect2 = bsStatic(\"THE VALUE 2\");\nstruct tagbstring expect3 = bsStatic(\"THE VALUE 3\");\n\nstatic int traverse_good_cb(HashmapNode *node)\n{\n    debug(\"KEY: %s\", bdata((bstring)node->key));\n    traverse_called++;\n    return 0;\n}\n\n\nstatic int traverse_fail_cb(HashmapNode *node)\n{\n    debug(\"KEY: %s\", bdata((bstring)node->key));\n    traverse_called++;\n\n    if(traverse_called == 2) {\n        return 1;\n    } else {\n        return 0;\n    }\n}\n\n\nchar *test_create()\n{\n    map = Hashmap_create(NULL, NULL);\n    mu_assert(map != NULL, \"Failed to create map.\");\n\n    return NULL;\n}\n\nchar *test_destroy()\n{\n    Hashmap_destroy(map);\n\n    return NULL;\n}\n\n\nchar *test_get_set()\n{\n    int rc = Hashmap_set(map, &test1, &expect1);\n    mu_assert(rc == 0, \"Failed to set &test1\");\n    bstring result = Hashmap_get(map, &test1);\n    mu_assert(result == &expect1, \"Wrong value for test1.\");\n\n    rc = Hashmap_set(map, &test2, &expect2);\n    mu_assert(rc == 0, \"Failed to set test2\");\n    result = Hashmap_get(map, &test2);\n    mu_assert(result == &expect2, \"Wrong value for test2.\");\n\n    rc = Hashmap_set(map, &test3, &expect3);\n    mu_assert(rc == 0, \"Failed to set test3\");\n    result = Hashmap_get(map, &test3);\n    mu_assert(result == &expect3, \"Wrong value for test3.\");\n\n    return NULL;\n}\n\nchar *test_traverse()\n{\n    int rc = Hashmap_traverse(map, traverse_good_cb);\n    mu_assert(rc == 0, \"Failed to traverse.\");\n    mu_assert(traverse_called == 3, \"Wrong count traverse.\");\n\n    traverse_called = 0;\n    rc = Hashmap_traverse(map, traverse_fail_cb);\n    mu_assert(rc == 1, \"Failed to traverse.\");\n    mu_assert(traverse_called == 2, \"Wrong count traverse for fail.\");\n\n    return NULL;\n}\n\nchar *test_delete()\n{\n    bstring deleted = (bstring)Hashmap_delete(map, &test1);\n    mu_assert(deleted != NULL, \"Got NULL on delete.\");\n    mu_assert(deleted == &expect1, \"Should get test1\");\n    bstring result = Hashmap_get(map, &test1);\n    mu_assert(result == NULL, \"Should delete.\");\n\n    deleted = (bstring)Hashmap_delete(map, &test2);\n    mu_assert(deleted != NULL, \"Got NULL on delete.\");\n    mu_assert(deleted == &expect2, \"Should get test2\");\n    result = Hashmap_get(map, &test2);\n    mu_assert(result == NULL, \"Should delete.\");\n\n    deleted = (bstring)Hashmap_delete(map, &test3);\n    mu_assert(deleted != NULL, \"Got NULL on delete.\");\n    mu_assert(deleted == &expect3, \"Should get test3\");\n    result = Hashmap_get(map, &test3);\n    mu_assert(result == NULL, \"Should delete.\");\n\n    return NULL;\n}\n\nchar *all_tests()\n{\n    mu_suite_start();\n\n    mu_run_test(test_create);\n    mu_run_test(test_get_set);\n    mu_run_test(test_traverse);\n    mu_run_test(test_delete);\n    mu_run_test(test_destroy);\n\n    return NULL;\n}\n\nRUN_TESTS(all_tests);\n```\n\n需要学习的唯一一件事情就是我在单元测试的顶端使用了`bstring`的特性来创建静态字符串用于测试。我使用`tagbstring`和`bsStatic`在7~13行创建他们。\n\n## 如何改进\n\n这是一个非常简单的`Hashmap`实现，就像书中的大多数其他数据结构那样。我的目标不是让你以非常快的速度来掌握数据结构。通常这些讨论起来非常复杂，并且会让你偏离真正的基础和实用的数据结构。我的目标是提供一个易于理解的起始点，然后再改进或理解它们如何实现。\n\n对于这和练习，下面是你能够用于改进这个实现的方法：\n\n+ 你可以对每个桶进行排序，使它们有序。这会增加你的插入时间，但是减少寻找时间，因为你可以使用二分搜索来寻找每个节点。到现在为止它遍历桶中的所有节点来寻找元素。\n+ 你可以动态设定桶的数量，或者让调用者指定每个`Hashmap`中的该值。\n+ 你可以使用更好的`default_hash`函数，有许多这样的函数。\n+ 这个实现以及几乎所有实现都有将一些特定的键存到一个桶中的风险。这会使你的程序运行速度变慢，因为它使`Hashmap`的处理过程变成了处理单个的`DArray`。如果你对桶中的数组排序会有帮助，但是你可以仅仅使用更好的哈希函数来避免，并且对于真正的偏执狂，你可以添加一个随机的盐，让键不可预测。\n+ 你可以删掉不歪有任何节点的桶来节约空间，或者将空的桶当如缓存中，便于节约创建和销毁它们的开销。\n+ 现在为止它可以添加已存在的元素，编写一个替代的实现，使它只能够添加不存在的元素。\n\n像往常一样，你需要浏览每个函数，并且使之健壮。`Hashmap`也可以使用一些调试设置，来执行不变量检查。\n\n## 附加题\n\n+ 研究你最喜欢的编程语言的`Hashmap`实现，了解它们具有什么特性。\n+ 找到`Hashmap`的主要缺点，以及如何避免它们。例如，它们不做特定的修改就不能保存顺序，并且当你基于键的一部分来查找元素时，它们就不能生效。\n+ 编写单元测试来展示将键都填充到`Hashmap`的一个桶中所带来的缺陷，之后测试这样如何影响性能。一个实现它的好方法，就是把桶的数量减少到一个愚蠢的数值，比如1。\n"
  },
  {
    "path": "docs/lcthw-zh/ex38.md",
    "content": "# 练习38：哈希算法\n\n> 原文：[Exercise 38: Hashmap Algorithms](http://c.learncodethehardway.org/book/ex38.html)\n\n> 译者：[飞龙](https://github.com/wizardforcel)\n\n你需要在这个练习中实现下面这三个哈希函数：\n\nFNV-1a\n\n以创造者Glenn Fowler、Phong Vo 和 Landon Curt Noll的名字命名。这个算法产生合理的数值并且相当快。\n\nAdler-32\n\n以Mark Adler命名。一个比较糟糕的算法，但是由来已久并且适于学习。\n\nDJB Hash\n\n由Dan J. Bernstein (DJB)发明的哈希算法，但是难以找到这个算法的讨论。它非常快，但是结果不是很好。\n\n你应该看到我使用了Jenkins hash作为`Hashmap`数据结构的默认哈希函数，所以这个练习的重点会放在这三个新的函数上。它们的代码通常来说不多，并且没有任何优化。像往常一样我会放慢速度来让你理解。\n\n头文件非常简单，所以我以它开始：\n\n```c\n#ifndef hashmap_algos_h\n#define hashmap_algos_h\n\n#include <stdint.h>\n\nuint32_t Hashmap_fnv1a_hash(void *data);\n\nuint32_t Hashmap_adler32_hash(void *data);\n\nuint32_t Hashmap_djb_hash(void *data);\n\n#endif\n```\n\n我只是声明了三个函数，我会在`hashmap_algos.c`文件中实现它们：\n\n```c\n#include <lcthw/hashmap_algos.h>\n#include <lcthw/bstrlib.h>\n\n// settings taken from\n// http://www.isthe.com/chongo/tech/comp/fnv/index.html#FNV-param\nconst uint32_t FNV_PRIME = 16777619;\nconst uint32_t FNV_OFFSET_BASIS = 2166136261;\n\nuint32_t Hashmap_fnv1a_hash(void *data)\n{\n    bstring s = (bstring)data;\n    uint32_t hash = FNV_OFFSET_BASIS;\n    int i = 0;\n\n    for(i = 0; i < blength(s); i++) {\n        hash ^= bchare(s, i, 0);\n        hash *= FNV_PRIME;\n    }\n\n    return hash;\n}\n\nconst int MOD_ADLER = 65521;\n\nuint32_t Hashmap_adler32_hash(void *data)\n{\n    bstring s = (bstring)data;\n    uint32_t a = 1, b = 0;\n    int i = 0;\n\n    for (i = 0; i < blength(s); i++)\n    {\n        a = (a + bchare(s, i, 0)) % MOD_ADLER;\n        b = (b + a) % MOD_ADLER;\n    }\n\n    return (b << 16) | a;\n}\n\nuint32_t Hashmap_djb_hash(void *data)\n{\n    bstring s = (bstring)data;\n    uint32_t hash = 5381;\n    int i = 0;\n\n    for(i = 0; i < blength(s); i++) {\n        hash = ((hash << 5) + hash) + bchare(s, i, 0); /* hash * 33 + c */\n    }\n\n    return hash;\n}\n```\n\n这个文件中有三个哈希函数。你应该注意到我默认使用`bstring`作为键，并且使用了`bchare`函数从字符串获取字符，然而如果字符超出了字符串的长度会返回0。\n\n这些算法中每个都可以在网上搜索到，所以你需要搜索它们并阅读相关内容。同时我主要使用维基百科上的结果，之后参照了其它来源。\n\n接着我为每个算法编写了单元测试，同时也测试了它们在多个桶中的分布情况。\n\n```c\n#include <lcthw/bstrlib.h>\n#include <lcthw/hashmap.h>\n#include <lcthw/hashmap_algos.h>\n#include <lcthw/darray.h>\n#include \"minunit.h\"\n\nstruct tagbstring test1 = bsStatic(\"test data 1\");\nstruct tagbstring test2 = bsStatic(\"test data 2\");\nstruct tagbstring test3 = bsStatic(\"xest data 3\");\n\nchar *test_fnv1a()\n{\n    uint32_t hash = Hashmap_fnv1a_hash(&test1);\n    mu_assert(hash != 0, \"Bad hash.\");\n\n    hash = Hashmap_fnv1a_hash(&test2);\n    mu_assert(hash != 0, \"Bad hash.\");\n\n    hash = Hashmap_fnv1a_hash(&test3);\n    mu_assert(hash != 0, \"Bad hash.\");\n\n    return NULL;\n}\n\nchar *test_adler32()\n{\n    uint32_t hash = Hashmap_adler32_hash(&test1);\n    mu_assert(hash != 0, \"Bad hash.\");\n\n    hash = Hashmap_adler32_hash(&test2);\n    mu_assert(hash != 0, \"Bad hash.\");\n\n    hash = Hashmap_adler32_hash(&test3);\n    mu_assert(hash != 0, \"Bad hash.\");\n\n    return NULL;\n}\n\nchar *test_djb()\n{\n    uint32_t hash = Hashmap_djb_hash(&test1);\n    mu_assert(hash != 0, \"Bad hash.\");\n\n    hash = Hashmap_djb_hash(&test2);\n    mu_assert(hash != 0, \"Bad hash.\");\n\n    hash = Hashmap_djb_hash(&test3);\n    mu_assert(hash != 0, \"Bad hash.\");\n\n    return NULL;\n}\n\n#define BUCKETS 100\n#define BUFFER_LEN 20\n#define NUM_KEYS BUCKETS * 1000\nenum { ALGO_FNV1A, ALGO_ADLER32, ALGO_DJB};\n\nint gen_keys(DArray *keys, int num_keys)\n{\n    int i = 0;\n    FILE *urand = fopen(\"/dev/urandom\", \"r\");\n    check(urand != NULL, \"Failed to open /dev/urandom\");\n\n    struct bStream *stream = bsopen((bNread)fread, urand);\n    check(stream != NULL, \"Failed to open /dev/urandom\");\n\n    bstring key = bfromcstr(\"\");\n    int rc = 0;\n\n    // FNV1a histogram\n    for(i = 0; i < num_keys; i++) {\n        rc = bsread(key, stream, BUFFER_LEN);\n        check(rc >= 0, \"Failed to read from /dev/urandom.\");\n\n        DArray_push(keys, bstrcpy(key));\n    }\n\n    bsclose(stream);\n    fclose(urand);\n    return 0;\n\nerror:\n    return -1;\n}\n\nvoid destroy_keys(DArray *keys)\n{\n    int i = 0;\n    for(i = 0; i < NUM_KEYS; i++) {\n        bdestroy(DArray_get(keys, i));\n    }\n\n    DArray_destroy(keys);\n}\n\nvoid fill_distribution(int *stats, DArray *keys, Hashmap_hash hash_func)\n{\n    int i = 0;\n    uint32_t hash = 0;\n\n    for(i = 0; i < DArray_count(keys); i++) {\n        hash = hash_func(DArray_get(keys, i));\n        stats[hash % BUCKETS] += 1;\n    }\n\n}\n\nchar *test_distribution()\n{\n    int i = 0;\n    int stats[3][BUCKETS] = {{0}};\n    DArray *keys = DArray_create(0, NUM_KEYS);\n\n    mu_assert(gen_keys(keys, NUM_KEYS) == 0, \"Failed to generate random keys.\");\n\n    fill_distribution(stats[ALGO_FNV1A], keys, Hashmap_fnv1a_hash);\n    fill_distribution(stats[ALGO_ADLER32], keys, Hashmap_adler32_hash);\n    fill_distribution(stats[ALGO_DJB], keys, Hashmap_djb_hash);\n\n    fprintf(stderr, \"FNV\\tA32\\tDJB\\n\");\n\n    for(i = 0; i < BUCKETS; i++) {\n        fprintf(stderr, \"%d\\t%d\\t%d\\n\",\n                stats[ALGO_FNV1A][i],\n                stats[ALGO_ADLER32][i],\n                stats[ALGO_DJB][i]);\n    }\n\n    destroy_keys(keys);\n\n    return NULL;\n}\n\nchar *all_tests()\n{\n    mu_suite_start();\n\n    mu_run_test(test_fnv1a);\n    mu_run_test(test_adler32);\n    mu_run_test(test_djb);\n    mu_run_test(test_distribution);\n\n    return NULL;\n}\n\nRUN_TESTS(all_tests);\n```\n\n我在代码中将`BUCKETS`的值设置得非常高，因为我的电脑足够快。如果你将它和`NUM_KEYS`调低，就会比较慢了。这个测试运行之后，对于每个哈希函数，通过使用R语言做统计分析，可以观察键的分布情况。\n\n我实现它的方式是使用`gen_keys`函数生成键的大型列表。这些键从`/dev/urandom`设备中获得，它们是一些随机的字节。之后我使用了这些键来调用`fill_distribution`，填充了`stats `数组，这些键计算哈希值后会被放入理论上的一些桶中。所有这类函数会遍历所有键，计算哈希，之后执行类似`Hashmap`所做的事情来寻找正确的桶。\n\n最后我只是简单打印出一个三列的表格，包含每个桶的最终数量，展示了每个桶中随机储存了多少个键。之后可以观察这些数值，来判断这些哈希函数是否合理对键进行分配。\n\n## 你会看到什么\n\n教授R是这本书范围之外的内容，但是如果你想试试它，可以访问[r-project.org](http://www.r-project.org/)。\n\n下面是一个简略的shell会话，向你展示了我如何运行`1tests/hashmap_algos_test`来获取`test_distribution`产生的表（这里没有展示），之后使用R来观察统计结果：\n\n```sh\n$ tests/hashmap_algos_tests\n# copy-paste the table it prints out\n$ vim hash.txt\n$ R\n> hash <- read.table(\"hash.txt\", header=T)\n> summary(hash)\n      FNV            A32              DJB      \n Min.   : 945   Min.   : 908.0   Min.   : 927  \n 1st Qu.: 980   1st Qu.: 980.8   1st Qu.: 979  \n Median : 998   Median :1000.0   Median : 998  \n Mean   :1000   Mean   :1000.0   Mean   :1000  \n 3rd Qu.:1016   3rd Qu.:1019.2   3rd Qu.:1021  \n Max.   :1072   Max.   :1075.0   Max.   :1082  \n```\n\n首先我只是运行测试，它会在屏幕上打印表格。之后我将它复制粘贴到下来并使用`vim hash.txt`来储存数据。如果你观察数据，它会带有显示这三个算法的`FNV A32 DJB`表头。\n\n接着，我运行R来使用`read.table`命令加载数据集。它是个非常智能的函数，适用于这种tab分隔的数据，我只要告诉它`header=T`，它就知道数据集中带有表头。\n\n最后，我家在了数据并且可以使用`summary`来打印出它每行的统计结果。这里你可以看到每个函数处理随机数据实际上都没有问题。我会解释每个行的意义：\n\nMin.\n\n它是列出数据的最小值。FNV似乎在这方面是最优的，因为它有最大的结果，也就是说它的下界最严格。\n\n1st Qu.\n\n数据的第一个四分位点。\n\nMedian\n\n如果你对它们排序，这个数值就是最重点的那个数。中位数比起均值来讲更有用一些。\n\nMean\n\n均值对大多数人意味着“平均”，它是数据的总数比数量。如果你观察它们，所有均值都是1000，这非常棒。如果你将它去中位数对比，你会发现，这三个中位数都很接近均值。这就意味着这些数据都没有“偏向”一端，所以均值是可信的。\n\n3rd Qu.\n\n数据后四分之一的起始点，代表了尾部的数值。\n\nMax.\n\n这是数据中的最大值，代表了它们的上界。\n\n观察这些数据，你会发现这些哈希算法似乎都适用于随机的键，并且均值与我设置的`NUM_KEYS`匹配。我所要找的就是如果我为每个桶中生成了1000个键，那么平均每个桶中就应该有100个键。如果哈希函数工作不正常，你会发现统计结果中均值不是1000，并且第一个和第三个四分位点非常高。一个好的哈希算法应该使平均值为1000，并且具有严格的范围。\n\n同时，你应该明白即使在这个单元测试的不同运行之间，你的数据的大多数应该和我不同。\n\n## 如何使它崩溃\n\n这个练习的最后，我打算向你介绍使它崩溃的方法。我需要让你变写你能编写的最烂的哈希函数，并且我会使用数据来证明它确实很烂。你可以使用R来进行统计，就像我上面一样，但也可能你知道其他可以使用的工具来进行相同的统计操作。\n\n这里的目标是让一个哈希函数，它表面看起来是正常的，但实际运行就得到一个糟糕的均值，并且分布广泛。这意味着你不能只让你返回1，而是需要返回一些看似正常的数值，但是分布广泛并且都填充到相同的桶中。\n\n如果你对这四个函数之一做了一些小修改来完成任务，我会给你额外的分数。\n\n这个练习的目的是，想像一下一些“友好”的程序员见到你并且打算改进你的哈希函数，但是实际上只是留了个把你的`Hashmap`搞砸的后门。\n\n## 附加题\n\n+ 将`hashmap.c`中的`default_hash`换成`hashmap_algos.c`中的算法之一，并且再次通过所有测试。\n+ 向`hashmap_algos_tests.c`添加`default_hash`，并将它与其它三个哈希函数比较。\n+ 寻找一些更多的哈希函数并添加进来，你永远都不可能找到太多的哈希函数！\n"
  },
  {
    "path": "docs/lcthw-zh/ex39.md",
    "content": "# 练习39：字符串算法\n\n> 原文：[Exercise 39: String Algorithms](http://c.learncodethehardway.org/book/ex39.html)\n\n> 译者：[飞龙](https://github.com/wizardforcel)\n\n这个练习中，我会向你展示可能是最快的字符串搜索算法之一，并且将它与`bstrlib.c`中现有的`binstr`比较。`binstr`的文档说它仅仅使用了“暴力搜索”的字符串算法来寻找第一个实例。我所实现的函数使用Boyer-Moore-Horspool（BMH）算法，如果你分析理论时间的话，一般认为它会更快。你也会看到，如果我的实现没有任何缺陷，BMH的实际时间会比`binstr`简单的暴力搜索更糟。\n\n这个练习的要点并不是真正解释算法本身，因为你可以直接去[Boyer-Moore-Horspool 的维基百科页面](http://en.wikipedia.org/wiki/Boyer%E2%80%93Moore%E2%80%93Horspool_algorithm)去阅读它。这个算法的要点就是它会计算出“跳跃字符列表”作为第一步操作，之后它使用这个列表来快速扫描整个字符串。它应当比暴力搜索更快，所以让我们在文件里写出代码来看看吧。\n\n首先，创建头文件：\n\n```c\n#ifndef string_algos_h\n#define string_algos_h\n\n#include <lcthw/bstrlib.h>\n#include <lcthw/darray.h>\n\ntypedef struct StringScanner {\n    bstring in;\n    const unsigned char *haystack;\n    ssize_t hlen;\n    const unsigned char *needle;\n    ssize_t nlen;\n    size_t skip_chars[UCHAR_MAX + 1];\n} StringScanner;\n\nint String_find(bstring in, bstring what);\n\nStringScanner *StringScanner_create(bstring in);\n\nint StringScanner_scan(StringScanner *scan, bstring tofind);\n\nvoid StringScanner_destroy(StringScanner *scan);\n\n#endif\n```\n\n为了观察“跳跃字符列表”的效果，我打算创建这个算法的两种版本：\n\nString_find\n\n只是在一个字符串中，寻找另一个字符串的首个实例，以一个动作执行整个算法。\n\nStringScanner_scan\n\n使用`StringScanner`状态结构，将跳跃列表的构建和实际的查找操作分开。这让我能看到什么影响了性能。这个模型有另一个优点，就是我可以在一个字符串中逐步搜索，并且快速地找到所有实例。\n\n一旦你完成了头文件，下面就是实现了：\n\n```c\n#include <lcthw/string_algos.h>\n#include <limits.h>\n\nstatic inline void String_setup_skip_chars(\n        size_t *skip_chars,\n        const unsigned char *needle, ssize_t nlen)\n{\n    size_t i = 0;\n    size_t last = nlen - 1;\n\n    for(i = 0; i < UCHAR_MAX + 1; i++) {\n        skip_chars[i] = nlen;\n    }\n\n    for (i = 0; i < last; i++) {\n        skip_chars[needle[i]] = last - i;\n    }\n}\n\n\nstatic inline const unsigned char *String_base_search(\n        const unsigned char *haystack, ssize_t hlen,\n        const unsigned char *needle, ssize_t nlen,\n        size_t *skip_chars)\n{\n    size_t i = 0;\n    size_t last = nlen - 1;\n\n    assert(haystack != NULL && \"Given bad haystack to search.\");\n    assert(needle != NULL && \"Given bad needle to search for.\");\n\n    check(nlen > 0, \"nlen can't be <= 0\");\n    check(hlen > 0, \"hlen can't be <= 0\");\n\n    while (hlen >= nlen)\n    {\n        for (i = last; haystack[i] == needle[i]; i--) {\n            if (i == 0) {\n                return haystack;\n            }\n        }\n\n        hlen -= skip_chars[haystack[last]];\n        haystack += skip_chars[haystack[last]];\n    }\n\nerror: // fallthrough\n    return NULL;\n}\n\nint String_find(bstring in, bstring what)\n{\n    const unsigned char *found = NULL;\n\n    const unsigned char *haystack = (const unsigned char *)bdata(in);\n    ssize_t hlen = blength(in);\n    const unsigned char *needle = (const unsigned char *)bdata(what);\n    ssize_t nlen = blength(what);\n    size_t skip_chars[UCHAR_MAX + 1] = {0};\n\n    String_setup_skip_chars(skip_chars, needle, nlen);\n\n    found = String_base_search(haystack, hlen, needle, nlen, skip_chars);\n\n    return found != NULL ? found - haystack : -1;\n}\n\nStringScanner *StringScanner_create(bstring in)\n{\n    StringScanner *scan = calloc(1, sizeof(StringScanner));\n    check_mem(scan);\n\n    scan->in = in;\n    scan->haystack = (const unsigned char *)bdata(in);\n    scan->hlen = blength(in);\n\n    assert(scan != NULL && \"fuck\");\n    return scan;\n\nerror:\n    free(scan);\n    return NULL;\n}\n\nstatic inline void StringScanner_set_needle(StringScanner *scan, bstring tofind)\n{\n    scan->needle = (const unsigned char *)bdata(tofind);\n    scan->nlen = blength(tofind);\n\n    String_setup_skip_chars(scan->skip_chars, scan->needle, scan->nlen);\n}\n\nstatic inline void StringScanner_reset(StringScanner *scan)\n{\n    scan->haystack = (const unsigned char *)bdata(scan->in);\n    scan->hlen = blength(scan->in);\n}\n\nint StringScanner_scan(StringScanner *scan, bstring tofind)\n{\n    const unsigned char *found = NULL;\n    ssize_t found_at = 0;\n\n    if(scan->hlen <= 0) {\n        StringScanner_reset(scan);\n        return -1;\n    }\n\n    if((const unsigned char *)bdata(tofind) != scan->needle) {\n        StringScanner_set_needle(scan, tofind);\n    }\n\n    found = String_base_search(\n            scan->haystack, scan->hlen,\n            scan->needle, scan->nlen,\n            scan->skip_chars);\n\n    if(found) {\n        found_at = found - (const unsigned char *)bdata(scan->in);\n        scan->haystack = found + scan->nlen;\n        scan->hlen -= found_at - scan->nlen;\n    } else {\n        // done, reset the setup\n        StringScanner_reset(scan);\n        found_at = -1;\n    }\n\n    return found_at;\n}\n\n\nvoid StringScanner_destroy(StringScanner *scan)\n{\n    if(scan) {\n        free(scan);\n    }\n}\n```\n\n整个算法都在两个`static inline`的函数中，叫做`String_setup_skip_chars` 和 `String_base_search`。它们在别的函数中使用，用于实现我想要的的搜索形式。研究这两个函数，并且与维基百科的描述对比，你就可以知道它的工作原理。\n\n之后`String_find`使用这两个函数来寻找并返回所发现的位置。它非常简单并且我使用它来查看“跳跃字符列表”的构建如何影响到真实性能。要注意，你或许可以使它更快，但是我要教给你在你实现算法之后如何验证理论速度。\n\n`StringScanner_scan`函数随后按照“创建、扫描、销毁”的常用模式，并且用于在一个字符串中逐步搜索另一个字符串。当我向你展示单元测试的时候，你会看到它如何使用。\n\n最后，我编写了单元测试来确保算法有效，之后在它的注释部分，我为三个搜索函数运行了简单的性能测试：\n\n```c\n#include \"minunit.h\"\n#include <lcthw/string_algos.h>\n#include <lcthw/bstrlib.h>\n#include <time.h>\n\nstruct tagbstring IN_STR = bsStatic(\"I have ALPHA beta ALPHA and oranges ALPHA\");\nstruct tagbstring ALPHA = bsStatic(\"ALPHA\");\nconst int TEST_TIME = 1;\n\nchar *test_find_and_scan()\n{\n    StringScanner *scan = StringScanner_create(&IN_STR);\n    mu_assert(scan != NULL, \"Failed to make the scanner.\");\n\n    int find_i = String_find(&IN_STR, &ALPHA);\n    mu_assert(find_i > 0, \"Failed to find 'ALPHA' in test string.\");\n\n    int scan_i = StringScanner_scan(scan, &ALPHA);\n    mu_assert(scan_i > 0, \"Failed to find 'ALPHA' with scan.\");\n    mu_assert(scan_i == find_i, \"find and scan don't match\");\n\n    scan_i = StringScanner_scan(scan, &ALPHA);\n    mu_assert(scan_i > find_i, \"should find another ALPHA after the first\");\n\n    scan_i = StringScanner_scan(scan, &ALPHA);\n    mu_assert(scan_i > find_i, \"should find another ALPHA after the first\");\n\n    mu_assert(StringScanner_scan(scan, &ALPHA) == -1, \"shouldn't find it\");\n\n    StringScanner_destroy(scan);\n\n    return NULL;\n}\n\nchar *test_binstr_performance()\n{\n    int i = 0;\n    int found_at = 0;\n    unsigned long find_count = 0;\n    time_t elapsed = 0;\n    time_t start = time(NULL);\n\n    do {\n        for(i = 0; i < 1000; i++) {\n            found_at = binstr(&IN_STR, 0, &ALPHA);\n            mu_assert(found_at != BSTR_ERR, \"Failed to find!\");\n            find_count++;\n        }\n\n        elapsed = time(NULL) - start;\n    } while(elapsed <= TEST_TIME);\n\n    debug(\"BINSTR COUNT: %lu, END TIME: %d, OPS: %f\",\n            find_count, (int)elapsed, (double)find_count / elapsed);\n    return NULL;\n}\n\nchar *test_find_performance()\n{\n    int i = 0;\n    int found_at = 0;\n    unsigned long find_count = 0;\n    time_t elapsed = 0;\n    time_t start = time(NULL);\n\n    do {\n        for(i = 0; i < 1000; i++) {\n            found_at = String_find(&IN_STR, &ALPHA);\n            find_count++;\n        }\n\n        elapsed = time(NULL) - start;\n    } while(elapsed <= TEST_TIME);\n\n    debug(\"FIND COUNT: %lu, END TIME: %d, OPS: %f\",\n            find_count, (int)elapsed, (double)find_count / elapsed);\n\n    return NULL;\n}\n\nchar *test_scan_performance()\n{\n    int i = 0;\n    int found_at = 0;\n    unsigned long find_count = 0;\n    time_t elapsed = 0;\n    StringScanner *scan = StringScanner_create(&IN_STR);\n\n    time_t start = time(NULL);\n\n    do {\n        for(i = 0; i < 1000; i++) {\n            found_at = 0;\n\n            do {\n                found_at = StringScanner_scan(scan, &ALPHA);\n                find_count++;\n            } while(found_at != -1);\n        }\n\n        elapsed = time(NULL) - start;\n    } while(elapsed <= TEST_TIME);\n\n    debug(\"SCAN COUNT: %lu, END TIME: %d, OPS: %f\",\n            find_count, (int)elapsed, (double)find_count / elapsed);\n\n    StringScanner_destroy(scan);\n\n    return NULL;\n}\n\n\nchar *all_tests()\n{\n    mu_suite_start();\n\n    mu_run_test(test_find_and_scan);\n\n    // this is an idiom for commenting out sections of code\n#if 0\n    mu_run_test(test_scan_performance);\n    mu_run_test(test_find_performance);\n    mu_run_test(test_binstr_performance);\n#endif\n\n    return NULL;\n}\n\nRUN_TESTS(all_tests);\n```\n\n我把它们写在`#if 0`中间，它是使用C预处理器来注释一段代码的方法。像这样输入，并且把它和`#endif`移除，你就可以运行性能测试。当你继续这本书时，需要简单地把它们再次注释，以防它们浪费你的开发时间。\n\n这个单元测试没有什么神奇之处，它只是在尊换种调用每个不同的函数，循环需要持续足够长的时间来得到一个几秒的样本。第一个测试（`test_find_and_scan`）只是确保我所编写的代码正常工作，因为测试无效的代码没有意义。之后，下面的三个函数使用三个函数中的每一个来执行大量的搜索。\n\n需要注意的一个技巧是，我在`start`中存储了起始时间，之后一直循环到至少过了`TEST_TIME`秒。这确保了我能或得到足够好的样本用于比较三者。我之后会使用不同的`TEST_TIME`设置来运行测试，并且分析结果。\n\n## 你会看到什么\n\n当我在我的笔记本上运行测试时，我得到的数据是这样的：\n\n```sh\n$ ./tests/string_algos_tests\nDEBUG tests/string_algos_tests.c:124: ----- RUNNING: ./tests/string_algos_tests\n----\nRUNNING: ./tests/string_algos_tests\nDEBUG tests/string_algos_tests.c:116:\n----- test_find_and_scan\nDEBUG tests/string_algos_tests.c:117:\n----- test_scan_performance\nDEBUG tests/string_algos_tests.c:105: SCAN COUNT: 110272000, END TIME: 2, OPS: 55136000.000000\nDEBUG tests/string_algos_tests.c:118:\n----- test_find_performance\nDEBUG tests/string_algos_tests.c:76: FIND COUNT: 12710000, END TIME: 2, OPS: 6355000.000000\nDEBUG tests/string_algos_tests.c:119:\n----- test_binstr_performance\nDEBUG tests/string_algos_tests.c:54: BINSTR COUNT: 72736000, END TIME: 2, OPS: 36368000.000000\nALL TESTS PASSED\nTests run: 4\n$\n```\n\n我看到了它，觉得每轮运行应该超过两秒。并且，我打算多次运行它，并且像之前一样使用R来验证。下面是我获得的10个样例，每个基本上是10秒：\n\n```\nscan find binstr\n71195200 6353700 37110200\n75098000 6358400 37420800\n74910000 6351300 37263600\n74859600 6586100 37133200\n73345600 6365200 37549700\n74754400 6358000 37162400\n75343600 6630400 37075000\n73804800 6439900 36858700\n74995200 6384300 36811700\n74781200 6449500 37383000\n```\n\n我在shell的一点点帮助下获取数据，之后编辑输出：\n\n```sh\n$ for i in 1 2 3 4 5 6 7 8 9 10; do echo \"RUN --- $i\" >> times.log; ./tests/string_algos_tests 2>&1 | grep COUNT >> times.log ; done\n$ less times.log\n$ vim times.log\n```\n\n现在你可以看到`scan`系统要优于另外两个，但是我会在R中打开它并且验证结果：\n\n```r\n> times <- read.table(\"times.log\", header=T)\n> summary(times)\n      scan               find             binstr        \n Min.   :71195200   Min.   :6351300   Min.   :36811700  \n 1st Qu.:74042200   1st Qu.:6358100   1st Qu.:37083800  \n Median :74820400   Median :6374750   Median :37147800  \n Mean   :74308760   Mean   :6427680   Mean   :37176830  \n 3rd Qu.:74973900   3rd Qu.:6447100   3rd Qu.:37353150  \n Max.   :75343600   Max.   :6630400   Max.   :37549700  \n>\n```\n\n为了理解我为什么要生成这份概要统计，我必须对你解释一些统计学概念。我在这些数字中寻找的东西能够简单地告诉我，“这三个函数（`scan`、`find`、`binstr`）实际上不同吗？”我知道每次我运行测试函数的时候，我都会得到有些不同的数值，并且那些数值始终处理一个固定的范围。你可以看到两个四分位数反映了这一点。\n\n我首先会去看均值，并且我会观察每个样例的均值是否不同于其它的。我可以清楚地看到`scan`优于`binstr`，同时后者优于`find`。然而问题来了，如果我只使用均值，就可以出现每个样例的范围会重叠的可能性。\n\n如果均值不同，但是两个四分位点重叠会怎么用？这种情况下我只能说有这种可能性，并且如果我再次运行测试，均值就可能不同了。很可能出现的范围上的重叠是，我的两个样例（以及两个函数）并非实际上不同。任何我看到的差异都是随机产生的结果。\n\n统计学拥有大量工具来解决这一问题，但是在我们的例子中我可以仅仅观察两个四分位值，以及所有样例的均值。如果均值不同，并且四分位值不可能重叠，就可以说它们完全不同。\n\n在我的三个样例中，我可以说`scan`、`find`和`binstr`都是不同的，范围上没有重叠，并且（最重要的是）我可以相信数据。\n\n## 分析结果\n\n从结果中可以看出`String_find`比其它两个更慢。实际上，我认为慢的原因是我实现的方式有些问题。然而当我将它与`StringScanner_scan`比较时，我发现正是构造跳跃列表的那一部分最消耗时间。并且它的功能比`scan`要少，因为它仅仅找到了第一个位置，而`scan`找到了全部。\n\n我也可以发现`scan`以很大优势优于`binstr`。同时我可以说`scan`的功能比其他两个要多，速度也更快。\n\n下面是这个分析的一些注解：\n\n+ 我可能将实现或测试弄乱了。现在我打算研究所有实现BMH的可能方式来改进它。我也会确保我所做的事情正确。\n+ 如果你修改了测试运行的时间，你会得到不同的结果。这就是我没有考虑的”热身“环节。\n+ `test_scan_performance`单元测试和其它两个并不相同，但是它比其它测试做得更多（并且也是按照时间和操作数量计算的），所以他可能是合理的。\n+ 我只通过在一个字符串内搜索另一个来执行测试。我应该使所查找的字符串随机化，来移除它们的位置和长度，作为干扰因素。\n+ `binstr`的实现可能比“暴力搜索”要好。（所以应该自己编写暴力搜索作为对照。）\n+ 我可能以不幸的顺序来执行这些函数，并且随机化首先运行的测试可能会得到更好的结果。\n\n可以从中学到的是，你需要确保知己的性能，即使你“正确”实现了一个算法。在这里BMH算法应该优于`binstr`算法，但是一个简单的测试证明了它是错误。如果我没有这些测试，我可能就使用了一个劣等的算法实现而不自知。参照这些度量，我可以开始调优我的实现，或者只是抛弃它并寻找新的算法。\n\n## 附加题\n\n+ 看看你能不能使`Scan_find`更快。为什么我的实现这么慢？\n+ 尝试一些不同的搜索时长，看看你是否能得到不同的数值。当你改变`scan`的测试时间时，时间的长度会有什么影响？对于这些结果你能得出什么结论？\n+ 修改单元测试，使它最开始执行每个函数一小段时间，来消除任何“热身”缓解。这样会修改所运行时长的依赖性吗？每秒可能出现多少次操作？\n+ 使单元测试中的所查找字符串随机化，之后测量你的得到的性能。一种实现它的方式就是使用`bstrlib.h`中的`bsplit`函数在空格处分割`IN_STR`。之后使用你得到的`strList`结构访问它返回的每个字符串。这也教给你如何使用`bstrList`操作进行字符串处理。\n+ 尝试一些不同顺序的测试，看看能否得到不同的结果。\n"
  },
  {
    "path": "docs/lcthw-zh/ex4.md",
    "content": "# 练习4：Valgrind 介绍\n\n> 原文：[Exercise 4: Introducing Valgrind](http://c.learncodethehardway.org/book/ex4.html)\n\n> 译者：[飞龙](https://github.com/wizardforcel)\n\n现在是介绍另一个工具的时间了，在你学习C的过程中，你会时时刻刻用到它，它就是 `Valgrind`。我现在就向你介绍 `Valgrind`，是因为从现在开始你将会在“如何使它崩溃”一节中用到它。`Valgrind`是一个运行你的程序的程序，并且随后会报告所有你犯下的可怕错误。它是一款相当棒的自由软件，我在编写C代码时一直使用它。\n\n回忆一下在上一章中，我让你移除`printf`的一个参数，来使你的代码崩溃。它打印出了一些奇怪的结果，但我并没有告诉你为什么它会这样打印。这个练习中我们要使用`Valgrind`来搞清楚为什么。\n\n> 注\n\n> 这本书的前几章讲解了一小段代码，同时掺杂了一些必要的工具，它们在本书的剩余章节会用到。这样做的原因是，阅读这本书的大多数人都不熟悉编译语言，也必然不熟悉自动化的辅助工具。通过先让你懂得如何使用`make`和`Valgrind`，我可以在后面使用它们更快地教你C语言，以及帮助你尽早找出所有的bug。\n\n> 这一章之后我就不再介绍更多的工具了，每章的内容大部分是代码，以及少量的语法。然而，我也会提及少量工具，我们可以用它来真正了解发生了什么，以及更好地了解常见的错误和问题。\n\n## 安装 Valgrind\n\n你可以用OS上的包管理器来安装`Valgrind`，但是我想让你学习如何从源码安装程序。这涉及到下面几个步骤：\n\n+ 下载源码的归档文件来获得源码\n+ 解压归档文件，将文件提取到你的电脑上\n+ 运行`./configure`来建立构建所需的配置\n+ 运行`make`来构建源码，就像之前所做的那样\n+ 运行`sudo make install`来将它安装到你的电脑\n\n下面是执行以上步骤的脚本，我想让你复制它：\n\n```sh\n# 1) Download it (use wget if you don't have curl)\ncurl -O http://valgrind.org/downloads/valgrind-3.6.1.tar.bz2\n\n# use md5sum to make sure it matches the one on the site\nmd5sum valgrind-3.6.1.tar.bz2\n\n# 2) Unpack it.\ntar -xjvf valgrind-3.6.1.tar.bz2\n\n# cd into the newly created directory\ncd valgrind-3.6.1\n\n# 3) configure it\n./configure\n\n# 4) make it\nmake\n\n# 5) install it (need root)\nsudo make install\n```\n\n按照这份脚本，但是如果 `Valgrind` 有新的版本请更新它。如果它不能正常执行，也请试着深入研究原因。\n\n## 使用 Valgrind\n\n使用 `Valgrind` 十分简单，只要执行`valgrind theprogram`，它就会运行你的程序，随后打印出你的程序运行时出现的所有错误。在这个练习中，我们会崩溃在一个错误输出上，然后会修复它。\n\n首先，这里有一个`ex3.c`的故意出错的版本，叫做`ex4.c`。出于练习目的，将它再次输入到文件中：\n\n```c\n#include <stdio.h>\n\n/* Warning: This program is wrong on purpose. */\n\nint main()\n{\n    int age = 10;\n    int height;\n\n    printf(\"I am %d years old.\\n\");\n    printf(\"I am %d inches tall.\\n\", height);\n\n    return 0;\n}\n```\n\n你会发现，除了两个经典的错误外，其余部分都相同：\n\n+ 没有初始化`height`变量\n+ 没有将`age`变量传入第一个`printf`函数\n\n## 你会看到什么\n\n现在我们像通常一样构建它，但是不要直接运行，而是使用`Valgrind`来运行它（见源码：\"使用Valgrind构建并运行 ex4.c\"）：\n\n```sh\n$ make ex4\ncc -Wall -g    ex4.c   -o ex4\nex4.c: In function 'main':\nex4.c:10: warning: too few arguments for format\nex4.c:7: warning: unused variable 'age'\nex4.c:11: warning: 'height' is used uninitialized in this function\n$ valgrind ./ex4\n==3082== Memcheck, a memory error detector\n==3082== Copyright (C) 2002-2010, and GNU GPL'd, by Julian Seward et al.\n==3082== Using Valgrind-3.6.0.SVN-Debian and LibVEX; rerun with -h for copyright info\n==3082== Command: ./ex4\n==3082==\nI am -16775432 years old.\n==3082== Use of uninitialised value of size 8\n==3082==    at 0x4E730EB: _itoa_word (_itoa.c:195)\n==3082==    by 0x4E743D8: vfprintf (vfprintf.c:1613)\n==3082==    by 0x4E7E6F9: printf (printf.c:35)\n==3082==    by 0x40052B: main (ex4.c:11)\n==3082==\n==3082== Conditional jump or move depends on uninitialised value(s)\n==3082==    at 0x4E730F5: _itoa_word (_itoa.c:195)\n==3082==    by 0x4E743D8: vfprintf (vfprintf.c:1613)\n==3082==    by 0x4E7E6F9: printf (printf.c:35)\n==3082==    by 0x40052B: main (ex4.c:11)\n==3082==\n==3082== Conditional jump or move depends on uninitialised value(s)\n==3082==    at 0x4E7633B: vfprintf (vfprintf.c:1613)\n==3082==    by 0x4E7E6F9: printf (printf.c:35)\n==3082==    by 0x40052B: main (ex4.c:11)\n==3082==\n==3082== Conditional jump or move depends on uninitialised value(s)\n==3082==    at 0x4E744C6: vfprintf (vfprintf.c:1613)\n==3082==    by 0x4E7E6F9: printf (printf.c:35)\n==3082==    by 0x40052B: main (ex4.c:11)\n==3082==\nI am 0 inches tall.\n==3082==\n==3082== HEAP SUMMARY:\n==3082==     in use at exit: 0 bytes in 0 blocks\n==3082==   total heap usage: 0 allocs, 0 frees, 0 bytes allocated\n==3082==\n==3082== All heap blocks were freed -- no leaks are possible\n==3082==\n==3082== For counts of detected and suppressed errors, rerun with: -v\n==3082== Use --track-origins=yes to see where uninitialised values come from\n==3082== ERROR SUMMARY: 4 errors from 4 contexts (suppressed: 4 from 4)\n$\n```\n\n> 注\n\n> 如果你运行了`Valgrind`，它显示一些类似于`by 0x4052112: (below main) (libc-start.c:226)`的东西，而不是`main.c`中的行号，你需要使用`valgrind --track-origins=yes ./ex4`命令来运行你的`Valgrind`。由于某些原因，`valgrind`的Debian和Ubuntu上的版本会这样，但是其它的不会。\n\n上面那段输出非常长，因为`Valgrind`在明确地告诉你程序中的每个错误都在哪儿。让我们从开头逐行分析一下（行号在左边，你可以参照）：\n\n1\n\n你执行了通常的`make ex4`来构建它。确保你看到的`cc`命令和它一样，并且带有`-g`选项，否则`Valgrind`的输出不会带上行号。\n\n2~6\n\n要注意编译器也会向你报告源码的错误，它警告你“向格式化函数传入了过少的变量”，因为你忘记包含`age`变量。\n\n7\n\n然后使用`valgrind ./ex4`来运行程序。\n\n8\n\n之后`Valgrind`变得十分奇怪，并向你报错：\n\n　　14~18\n\n　　在`main (ex4.c:11)`（意思是文件`ex4.c`的`main`函数的第11行）的那行中，有“大小为8的未初始化的值”。你通过查看错误找到了它，并且在它下面看到了“栈踪迹”。最开始看到的那行`(ex4.c:11)`在最下面，如果你不明白哪里出错了，你可以向上看，比如`printf.c:35`。通常最下面的一行最重要（这个例子中是第18行）。\n\n　　20~24\n\n　　下一个错误位于 `main` 函数中的 `ex4.c:11`。`Valgrind`不喜欢这一行，它说的是一些 if 语句或者 while 循环基于一个未初始化的值，在这个例子中是`height`。\n\n　　25~35\n\n　　剩下的错误都大同小异，因为这个值还在继续使用。\n\n37~46\n\n最后程序退出了，`Valgrind`显示出一份摘要，告诉你程序有多烂。\n\n这段信息读起来会相当多，下面是你的处理方法：\n\n+ 无论什么时候你运行C程序并且使它工作，都应该使用`Valgrind`重新运行它来检查。\n+ 对于得到的每个错误，找到“源码:行数”提示的位置，然后修复它。你可以上网搜索错误信息，来弄清楚它的意思。\n+ 一旦你的程序在`Valgrind`下不出现任何错误信息，应该就好了。你可能学会了如何编写代码的一些技巧。\n\n在这个练习中我并不期待你马上完全掌握`Valgrind`，但是你应该安装并且学会如何快速使用它，以便我们将它用于后面的练习。\n\n## 附加题\n\n+ 按照上面的指导，使用`Valgrind`和编译器修复这个程序。\n+ 在互联网上查询`Valgrind`相关的资料。\n+ 下载另一个程序并手动构建它。尝试一些你已经使用，但从来没有手动构建的程序。\n+ 看看`Valgrind`的源码是如何在目录下组织的，并且阅读它的Makefile文件。不要担心，这对我来说没有任何意义。\n"
  },
  {
    "path": "docs/lcthw-zh/ex40.md",
    "content": "# 练习40：二叉搜索树\n\n> 原文：[Exercise 40: Binary Search Trees](http://c.learncodethehardway.org/book/ex40.html)\n\n> 译者：[飞龙](https://github.com/wizardforcel)\n\n二叉树是最简单的树形数据结构，虽然它在许多语言中被哈希表取代，但仍旧对于一些应用很实用。二叉树的各种变体可用于一些非常实用东西，比如数据库的索引、搜索算法结构、以及图像处理。\n\n我把我的二叉树叫做`BSTree`，描述它的最佳方法就是它是另一种`Hashmap`形式的键值对储存容器。它们的差异在于，哈希表为键计算哈希值来寻找位置，而二叉树将键与树中的节点进行对比，之后深入树中找到储存它的最佳位置，基于它与其它节点的关系。\n\n在我真正解释它的工作原理之前，让我向你展示`bstree.h`头文件，便于你看到数据结构，之后我会用它来解释如何构建。\n\n```c\n#ifndef _lcthw_BSTree_h\n#define _lcthw_BSTree_h\n\n\ntypedef int (*BSTree_compare)(void *a, void *b);\n\ntypedef struct BSTreeNode {\n    void *key;\n    void *data;\n\n    struct BSTreeNode *left;\n    struct BSTreeNode *right;\n    struct BSTreeNode *parent;\n} BSTreeNode;\n\ntypedef struct BSTree {\n    int count;\n    BSTree_compare compare;\n    BSTreeNode *root;\n} BSTree;\n\ntypedef int (*BSTree_traverse_cb)(BSTreeNode *node);\n\nBSTree *BSTree_create(BSTree_compare compare);\nvoid BSTree_destroy(BSTree *map);\n\nint BSTree_set(BSTree *map, void *key, void *data);\nvoid *BSTree_get(BSTree *map, void *key);\n\nint BSTree_traverse(BSTree *map, BSTree_traverse_cb traverse_cb);\n\nvoid *BSTree_delete(BSTree *map, void *key);\n\n#endif\n```\n\n这遵循了我之前用过的相同模式，我创建了一个基容器叫做`BSTree`，它含有叫做`BSTreeNode`的节点，组成实际内容。厌倦了吗？是的，这种结构也没有什么高明之处。\n\n最重要的部分是，`BSTreeNode`如何配置，以及它如何用于进行每个操作：设置、获取和删除。我会首先讲解`get`，因为它是最简单的操作，并且我会在数据结构上手动操作：\n\n+ 我获得你要找的键，并且用根节点开始遍历，首先我将你的键与这个节点的键进行对比。\n+ 如果你的键小于`node.key`，我使用`left`指针来详细遍历。\n+ 如果你的键大于`node.key`，我使用`right`指针来详细遍历。\n+ 重复第二步和第三部，知道我找到了匹配`node.key`的节点，或者我遍历到了没有左子树或右子树的节点。这种情况我会返回`node.data`，其它情况会返回`NULL`。\n\n这就是`get`的全部操作，现在是`set`，它几乎执行相同的操作，除了你在寻找防止新节点的位置。\n\n+ 如果`BSTree.root`为空，就算是执行完成了。它就是第一个节点。\n+ 之后我会将你的键与`node.key`进行比对，从根节点开始。\n+ 如果你的键小于或等于`node.key`，我会遍历左子树，否则是右子树。\n+ 重复第三步，直到我到达了没有左子树或右子树的节点，但是这是我需要选择的方向。\n+ 我选择了这个方向（左或者右）来放置新的节点，并且将这个新节点的父节点设为我来时的上一个节点。当我删除它时，我会使用它的父节点。\n\n这也解释了它如何工作。如果寻找一个节点涉及到按照键的对比来遍历左子树或右子树，那么设置一个节点涉及到相同的事情，直到我找到了一个位置，可以在其左子树或右子树上放置新的节点。\n\n花一些时间在纸上画出一些树并且遍历一些节点来进行查找或设置，你就可以理解它如何工作。之后你要准备好来看一看实现，我在其中解释了删除操作。删除一个节点非常麻烦，因此它最适合逐行的代码分解。\n\n```c\n#include <lcthw/dbg.h>\n#include <lcthw/bstree.h>\n#include <stdlib.h>\n#include <lcthw/bstrlib.h>\n\nstatic int default_compare(void *a, void *b)\n{\n    return bstrcmp((bstring)a, (bstring)b);\n}\n\n\nBSTree *BSTree_create(BSTree_compare compare)\n{\n    BSTree *map = calloc(1, sizeof(BSTree));\n    check_mem(map);\n\n    map->compare = compare == NULL ? default_compare : compare;\n\n    return map;\n\nerror:\n    if(map) {\n        BSTree_destroy(map);\n    }\n    return NULL;\n}\n\nstatic int BSTree_destroy_cb(BSTreeNode *node)\n{\n    free(node);\n    return 0;\n}\n\nvoid BSTree_destroy(BSTree *map)\n{\n    if(map) {\n        BSTree_traverse(map, BSTree_destroy_cb);\n        free(map);\n    }\n}\n\n\nstatic inline BSTreeNode *BSTreeNode_create(BSTreeNode *parent, void *key, void *data)\n{\n    BSTreeNode *node = calloc(1, sizeof(BSTreeNode));\n    check_mem(node);\n\n    node->key = key;\n    node->data = data;\n    node->parent = parent;\n    return node;\n\nerror:\n    return NULL;\n}\n\n\nstatic inline void BSTree_setnode(BSTree *map, BSTreeNode *node, void *key, void *data)\n{\n    int cmp = map->compare(node->key, key);\n\n    if(cmp <= 0) {\n        if(node->left) {\n            BSTree_setnode(map, node->left, key, data);\n        } else {\n            node->left = BSTreeNode_create(node, key, data);\n        }\n    } else {\n        if(node->right) {\n            BSTree_setnode(map, node->right, key, data);\n        } else {\n            node->right = BSTreeNode_create(node, key, data);\n        }\n    }\n}\n\n\nint BSTree_set(BSTree *map, void *key, void *data)\n{\n    if(map->root == NULL) {\n        // first so just make it and get out\n        map->root = BSTreeNode_create(NULL, key, data);\n        check_mem(map->root);\n    } else {\n        BSTree_setnode(map, map->root, key, data);\n    }\n\n    return 0;\nerror:\n    return -1;\n}\n\nstatic inline BSTreeNode *BSTree_getnode(BSTree *map, BSTreeNode *node, void *key)\n{\n    int cmp = map->compare(node->key, key);\n\n    if(cmp == 0) {\n        return node;\n    } else if(cmp < 0) {\n        if(node->left) {\n            return BSTree_getnode(map, node->left, key);\n        } else {\n            return NULL;\n        }\n    } else {\n        if(node->right) {\n            return BSTree_getnode(map, node->right, key);\n        } else {\n            return NULL;\n        }\n    }\n}\n\nvoid *BSTree_get(BSTree *map, void *key)\n{\n    if(map->root == NULL) {\n        return NULL;\n    } else {\n        BSTreeNode *node = BSTree_getnode(map, map->root, key);\n        return node == NULL ? NULL : node->data;\n    }\n}\n\n\nstatic inline int BSTree_traverse_nodes(BSTreeNode *node, BSTree_traverse_cb traverse_cb)\n{\n    int rc = 0;\n\n    if(node->left) {\n        rc = BSTree_traverse_nodes(node->left, traverse_cb);\n        if(rc != 0) return rc;\n    }\n\n    if(node->right) {\n        rc = BSTree_traverse_nodes(node->right, traverse_cb);\n        if(rc != 0) return rc;\n    }\n\n    return traverse_cb(node);\n}\n\nint BSTree_traverse(BSTree *map, BSTree_traverse_cb traverse_cb)\n{\n    if(map->root) {\n        return BSTree_traverse_nodes(map->root, traverse_cb);\n    }\n\n    return 0;\n}\n\nstatic inline BSTreeNode *BSTree_find_min(BSTreeNode *node)\n{\n    while(node->left) {\n        node = node->left;\n    }\n\n    return node;\n}\n\nstatic inline void BSTree_replace_node_in_parent(BSTree *map, BSTreeNode *node, BSTreeNode *new_value)\n{\n    if(node->parent) {\n        if(node == node->parent->left) {\n            node->parent->left = new_value;\n        } else {\n            node->parent->right = new_value;\n        }\n    } else {\n        // this is the root so gotta change it\n        map->root = new_value;\n    }\n\n    if(new_value) {\n        new_value->parent = node->parent;\n    }\n}\n\nstatic inline void BSTree_swap(BSTreeNode *a, BSTreeNode *b)\n{\n    void *temp = NULL;\n    temp = b->key; b->key = a->key; a->key = temp;\n    temp = b->data; b->data = a->data; a->data = temp;\n}\n\nstatic inline BSTreeNode *BSTree_node_delete(BSTree *map, BSTreeNode *node, void *key)\n{\n    int cmp = map->compare(node->key, key);\n\n    if(cmp < 0) {\n        if(node->left) {\n            return BSTree_node_delete(map, node->left, key);\n        } else {\n            // not found\n            return NULL;\n        }\n    } else if(cmp > 0) {\n        if(node->right) {\n            return BSTree_node_delete(map, node->right, key);\n        } else {\n            // not found\n            return NULL;\n        }\n    } else {\n        if(node->left && node->right) {\n            // swap this node for the smallest node that is bigger than us\n            BSTreeNode *successor = BSTree_find_min(node->right);\n            BSTree_swap(successor, node);\n\n            // this leaves the old successor with possibly a right child\n            // so replace it with that right child\n            BSTree_replace_node_in_parent(map, successor, successor->right);\n\n            // finally it's swapped, so return successor instead of node\n            return successor;\n        } else if(node->left) {\n            BSTree_replace_node_in_parent(map, node, node->left);\n        } else if(node->right) {\n            BSTree_replace_node_in_parent(map, node, node->right);\n        } else {\n            BSTree_replace_node_in_parent(map, node, NULL);\n        }\n\n        return node;\n    }\n}\n\nvoid *BSTree_delete(BSTree *map, void *key)\n{\n    void *data = NULL;\n\n    if(map->root) {\n        BSTreeNode *node = BSTree_node_delete(map, map->root, key);\n\n        if(node) {\n            data = node->data;\n            free(node);\n        }\n    }\n\n    return data;\n}\n```\n\n在讲解`BSTree_delete`如何工作之前，我打算解释一下我用于执行递归函数的模式。你会发现许多树形数据结构都易于使用递归来编写，而写成单个函数的形式相当困难。一部分原因在于你需要为第一次操作建立一些初始的数据，之后在数据结构中递归，这难以写成一个函数。\n\n解决办法就是使用两个函数。一个函数“建立”数据结构和首次递归的条件使第二层函数能够执行真正的逻辑。首先看一看`BSTree_get`来理解我所说的。\n\n+ 我设置了初始条件来处理递归，如果`map->NULL`是`NULL`，那么就返回`NULL`并且不需要递归。\n+ 之后我执行了真正的递归调用，它就是`BSTree_getnode`。我设置了根节点的初始条件、`key`和`map`。\n+ 之后在`BSTree_getnode`中，我执行了真正的递归逻辑，我将是用`map->compare(node->key, key)`来进行键的比对，并且根据结果遍历左子树或右子树，或者相等。\n+ 由于这个函数时“自相似”的，并且不用处理任何初始条件（因为`BSTree_get`处理了），我就可以使它非常简单。当它完成时会返回给调用者，最后把结构返回给`BSTree_get`。\n+ 最后，在结果不为`NULL`的情况下，`BSTree_get`处理获得的`node.data`元素。\n\n这种构造递归算法的方法，与我构造递归数据结构的方法一致。我创建了一个起始的“基函数”，它处理初始条件和一些边界情况，之后它调用了一个简洁的递归函数来执行任务。与之相比，我在`BStree`中创建了“基结构”，它持有递归的`BSTreeNode`结构，每个节点都引用树中的其它节点。使用这种模式让我更容易处理递归并保持简洁。\n\n接下来，浏览`BSTree_set` 和 `BSTree_setnode`，来观察相同的模式。我使用`BSTree_set`来确保初始条件和便捷情况。常见的边界情况就是树中没有根节点，于是我需要创建一个函数来初始化它们。\n\n这个模式适用于几乎任何递归的算法。我按照这种模式来编写它们：\n\n+ 理解初始变量，它们如何改变，以及递归每一步的终止条件。\n+ 编写调用自身的递归函数，带有参数作为终止条件和初始变量。\n+ 编程一个启动函数来设置算法的初始条件，并且处理边界情况，之后调用递归函数。\n+ 最后，启动函数返回最后的结果，并且如果递归函数不能处理最终的边界情况可能还要做调整。\n\n这引导了我完成`BSTree_delete`和`BSTree_node_delete`。首先你可以看一下`BSTree_delete`和它的启动函数，它获取结果节点的数据，并且释放找到的节点。在`BSTree_node_delete`中事情就变得复杂了，因为要在树中任意位置删除一个节点，我需要将子节点翻转上来。我会逐行拆分这个函数：\n\nbstree.c:190\n\n我执行比较函数来找出应该选择的方向。\n\nbstree.c:192-198\n\n这是“小于”的分支，我应该移到左子树。这里左子树并不存在并且返回了`NULL`来表示“未找到”。这处理了一些不在`BSTree`中元素的删除操作。\n\nbstree.c:199-205\n\n和上面相同，但是是对于树的右侧分支。这就像其它函数一样只是在树中向下遍历，并且在不存在时返回`NULL`。\n\nbstree.c:206\n\n这里是发现目标节点的地方，因为键是相等的（`compare`返回了0）。\n\nbstree.c:207\n\n这个节点同时具有`left`和`right`分支，所以它深深嵌入在树中。\n\nbstree.c:209\n\n要移除这个节点，我首先要找到大于这个节点的最小节点，这里我在右子树上调用了`BSTree_find_min`。\n\nbstree.c:210\n\n一旦我获得了这个几点，我将它的`key`和`data`与当前节点互换。这样就高效地将当前节点移动到树的最底端，并且不同通过它的指针来调整节点。\n\nbstree.c:214\n\n现在`successor`是一个无效的分支，储存了当前节点的值。然而它可能还带有右子树，也就是说我必须做一个旋转使它的右节点上来代替它。\n\nbstree.c:217\n\n到此为止，`successor`已经从树中移出了，它的值被当前节点的值代替，它的任何子树都合并进了它的父节点。我可以像`node`一样返回它。\n\nbstree.c:218\n\n这个分支中，我了解到这个节点没有右子树只有左子树，所以我可以简单地用左节点来替代它。\n\nbstree.c:219\n\n我再次使用`BSTree_replace_node_in_parent`来执行替换，把左节点旋转上去。\n\nbstree.c:220\n\n这是只有右子树而没有左子树的情况，所以需要将右节点旋转上去。\n\nbstree.c:221\n\n再次使用相同的函数，这次是针对右节点。\n\nbstree.c:222\n\n最后，对于我发现的节点只剩下一种情况，就是它没有任何子树（没有做子树也没有右子树）。这种情况，我只需要使用相同函数以`NULL`来执行替换。\n\nbstree.c:210\n\n在此之后，我已经将当前节点从书中移除，并且以某个合适的子节点的元素来替换。我只需要把它返回给调用者，使它能够被释放或管理。\n\n这个操作非常复杂，实话说，在一些树形数据结构中，我并不需要执行删除，而是把它当做软件中的常亮数据。如果我需要做繁杂的插入和删除工作，我会使用`Hashmap`。\n\n最后，你可以查看它的单元测试以及测试方法：\n\n```c\n#include \"minunit.h\"\n#include <lcthw/bstree.h>\n#include <assert.h>\n#include <lcthw/bstrlib.h>\n#include <stdlib.h>\n#include <time.h>\n\nBSTree *map = NULL;\nstatic int traverse_called = 0;\nstruct tagbstring test1 = bsStatic(\"test data 1\");\nstruct tagbstring test2 = bsStatic(\"test data 2\");\nstruct tagbstring test3 = bsStatic(\"xest data 3\");\nstruct tagbstring expect1 = bsStatic(\"THE VALUE 1\");\nstruct tagbstring expect2 = bsStatic(\"THE VALUE 2\");\nstruct tagbstring expect3 = bsStatic(\"THE VALUE 3\");\n\nstatic int traverse_good_cb(BSTreeNode *node)\n{\n    debug(\"KEY: %s\", bdata((bstring)node->key));\n    traverse_called++;\n    return 0;\n}\n\n\nstatic int traverse_fail_cb(BSTreeNode *node)\n{\n    debug(\"KEY: %s\", bdata((bstring)node->key));\n    traverse_called++;\n\n    if(traverse_called == 2) {\n        return 1;\n    } else {\n        return 0;\n    }\n}\n\n\nchar *test_create()\n{\n    map = BSTree_create(NULL);\n    mu_assert(map != NULL, \"Failed to create map.\");\n\n    return NULL;\n}\n\nchar *test_destroy()\n{\n    BSTree_destroy(map);\n\n    return NULL;\n}\n\n\nchar *test_get_set()\n{\n    int rc = BSTree_set(map, &test1, &expect1);\n    mu_assert(rc == 0, \"Failed to set &test1\");\n    bstring result = BSTree_get(map, &test1);\n    mu_assert(result == &expect1, \"Wrong value for test1.\");\n\n    rc = BSTree_set(map, &test2, &expect2);\n    mu_assert(rc == 0, \"Failed to set test2\");\n    result = BSTree_get(map, &test2);\n    mu_assert(result == &expect2, \"Wrong value for test2.\");\n\n    rc = BSTree_set(map, &test3, &expect3);\n    mu_assert(rc == 0, \"Failed to set test3\");\n    result = BSTree_get(map, &test3);\n    mu_assert(result == &expect3, \"Wrong value for test3.\");\n\n    return NULL;\n}\n\nchar *test_traverse()\n{\n    int rc = BSTree_traverse(map, traverse_good_cb);\n    mu_assert(rc == 0, \"Failed to traverse.\");\n    mu_assert(traverse_called == 3, \"Wrong count traverse.\");\n\n    traverse_called = 0;\n    rc = BSTree_traverse(map, traverse_fail_cb);\n    mu_assert(rc == 1, \"Failed to traverse.\");\n    mu_assert(traverse_called == 2, \"Wrong count traverse for fail.\");\n\n    return NULL;\n}\n\nchar *test_delete()\n{\n    bstring deleted = (bstring)BSTree_delete(map, &test1);\n    mu_assert(deleted != NULL, \"Got NULL on delete.\");\n    mu_assert(deleted == &expect1, \"Should get test1\");\n    bstring result = BSTree_get(map, &test1);\n    mu_assert(result == NULL, \"Should delete.\");\n\n    deleted = (bstring)BSTree_delete(map, &test1);\n    mu_assert(deleted == NULL, \"Should get NULL on delete\");\n\n    deleted = (bstring)BSTree_delete(map, &test2);\n    mu_assert(deleted != NULL, \"Got NULL on delete.\");\n    mu_assert(deleted == &expect2, \"Should get test2\");\n    result = BSTree_get(map, &test2);\n    mu_assert(result == NULL, \"Should delete.\");\n\n    deleted = (bstring)BSTree_delete(map, &test3);\n    mu_assert(deleted != NULL, \"Got NULL on delete.\");\n    mu_assert(deleted == &expect3, \"Should get test3\");\n    result = BSTree_get(map, &test3);\n    mu_assert(result == NULL, \"Should delete.\");\n\n    // test deleting non-existent stuff\n    deleted = (bstring)BSTree_delete(map, &test3);\n    mu_assert(deleted == NULL, \"Should get NULL\");\n\n    return NULL;\n}\n\nchar *test_fuzzing()\n{\n    BSTree *store = BSTree_create(NULL);\n    int i = 0;\n    int j = 0;\n    bstring numbers[100] = {NULL};\n    bstring data[100] = {NULL};\n    srand((unsigned int)time(NULL));\n\n    for(i = 0; i < 100; i++) {\n        int num = rand();\n        numbers[i] = bformat(\"%d\", num);\n        data[i] = bformat(\"data %d\", num);\n        BSTree_set(store, numbers[i], data[i]);\n    }\n\n    for(i = 0; i < 100; i++) {\n        bstring value = BSTree_delete(store, numbers[i]);\n        mu_assert(value == data[i], \"Failed to delete the right number.\");\n\n        mu_assert(BSTree_delete(store, numbers[i]) == NULL, \"Should get nothing.\");\n\n        for(j = i+1; j < 99 - i; j++) {\n            bstring value = BSTree_get(store, numbers[j]);\n            mu_assert(value == data[j], \"Failed to get the right number.\");\n        }\n\n        bdestroy(value);\n        bdestroy(numbers[i]);\n    }\n\n    BSTree_destroy(store);\n\n    return NULL;\n}\n\nchar *all_tests()\n{\n    mu_suite_start();\n\n    mu_run_test(test_create);\n    mu_run_test(test_get_set);\n    mu_run_test(test_traverse);\n    mu_run_test(test_delete);\n    mu_run_test(test_destroy);\n    mu_run_test(test_fuzzing);\n\n    return NULL;\n}\n\nRUN_TESTS(all_tests);\n```\n\n我要重点讲解`test_fuzzing`函数，它是针对复杂数据结构的一种有趣的测试技巧。创建一些键来覆盖`BSTree_node_delete`的所有分支相当困难，而且有可能我会错过一些边界情况。更好的方法就是创建一个“模糊测试”的函数来执行所有操作，并尽可能以一种可怕且随机的方式执行它们。这里我插入了一系列随机字符串的键，之后我删除了它们并试着在删除之后获取它们的值。\n\n这种测试可以避免只测试到你知道能正常工作的部分，这意味着你不会遗漏不知道的事情。通过想你的数据结构插入一些随机的垃圾数据，你可以碰到意料之外的事情，并检测出任何bug。\n\n## 如何改进\n\n不要完成下列任何习题，因为在下个练习中我会使用这里的单元测试，来教你使用一些性能调优的技巧。在你完成练习41之后，你需要返回来完成这些习题。\n\n+ 像之前一样，你应该执行所有防御性编程检查，并且为不应发生的情况添加`assert`。例如，你不应该在递归函数中获取到`NULL`，为此添加断言。\n+ 遍历函数按照左子树、右子树和当前节点的顺组进行遍历。你可以创建相反顺序的遍历函数。\n+ 每个节点上都会执行完整的字符串比较，但是我可以使用`Hashmap`的哈希函数来提升速度。我可以计算键的哈希值，在`BSTreeNode`中储存它。之后在每个创建的函数中，我可以实现计算出键的哈希值，然后在递归中向下传递。我可以使用哈希来很快地比较每个节点，就像`Hashmap`那样。\n\n## 附加题\n\n同样，现在先不要完成它们，直到完成练习41，那时你就可以使用`Valgrind`的性能调优技巧来完成它们了。\n\n+ 有一种不使用递归的替代的方法，也可以操作这个数据结构。维基百科上介绍了不使用递归来完成相同事情的替代方法。这样做会更好还是更糟？\n+ 查询你能找到的所有不同的树的相关资料。比如AVL树、红黑树、以及一些非树形结构例如跳转表。\n"
  },
  {
    "path": "docs/lcthw-zh/ex41.md",
    "content": "# 练习41：将 Cachegrind 和 Callgrind 用于性能调优\n\n> 原文：[Exercise 41: Using Cachegrind And Callgrind For Performance Tuning](http://c.learncodethehardway.org/book/ex41.html)\n\n> 译者：[飞龙](https://github.com/wizardforcel)\n\n这个练习中，我打算上一节速成课，内容是使用`Valgrind`的两个工具`callgrind`和`cachegrind`。这两个工具会分析你程序的执行，并且告诉你哪一部分运行缓慢。这些结果非常精确，因为`Valgrind`的工作方式有助于你解决一些问题，比如执行过多的代码行，热点，内容访问问题，甚至是CPU的缓存未命中。\n\n为了做这个练习，我打算使用`bstree_tests`单元测试，你之前用于寻找能提升算法的地方。你需要确保你这些程序的版本没有任何`valgrind`错误，并且和我的代码非常相似，因为我会使用我的代码的转储来谈论`cachegrind`和`callgrind`如何工作。\n\n## 运行 Callgrind\n\n为了运行Callgrind，你需要向`valgrind`传入`--tool=callgrind`选项，之后它会产生`callgrind.out.PID`文件（其中PID为所运行程序的进程PID）。一旦你这样运行了，你就可以使用一个叫做`callgrind_annotate`的工具分析`callgrind.out`文件，它会告诉你哪个函数运行中使用了最多的指令。下面是个例子，我在`bstree_tests`上运行了`callgrind`，之后得到了这个信息：\n\n```sh\n$ valgrind --dsymutil=yes --tool=callgrind tests/bstree_tests\n...\n$ callgrind_annotate callgrind.out.1232\n--------------------------------------------------------------------------------\nProfile data file 'callgrind.out.1232' (creator: callgrind-3.7.0.SVN)\n--------------------------------------------------------------------------------\nI1 cache:\nD1 cache:\nLL cache:\nTimerange: Basic block 0 - 1098689\nTrigger: Program termination\nProfiled target:  tests/bstree_tests (PID 1232, part 1)\nEvents recorded:  Ir\nEvents shown:     Ir\nEvent sort order: Ir\nThresholds:       99\nInclude dirs:     \nUser annotated:   \nAuto-annotation:  off\n\n--------------------------------------------------------------------------------\n       Ir\n--------------------------------------------------------------------------------\n4,605,808  PROGRAM TOTALS\n\n--------------------------------------------------------------------------------\n       Ir  file:function\n--------------------------------------------------------------------------------\n  670,486  src/lcthw/bstrlib.c:bstrcmp [tests/bstree_tests]\n  194,377  src/lcthw/bstree.c:BSTree_get [tests/bstree_tests]\n   65,580  src/lcthw/bstree.c:default_compare [tests/bstree_tests]\n   16,338  src/lcthw/bstree.c:BSTree_delete [tests/bstree_tests]\n   13,000  src/lcthw/bstrlib.c:bformat [tests/bstree_tests]\n   11,000  src/lcthw/bstrlib.c:bfromcstralloc [tests/bstree_tests]\n    7,774  src/lcthw/bstree.c:BSTree_set [tests/bstree_tests]\n    5,800  src/lcthw/bstrlib.c:bdestroy [tests/bstree_tests]\n    2,323  src/lcthw/bstree.c:BSTreeNode_create [tests/bstree_tests]\n    1,183  /private/tmp/pkg-build/coregrind//vg_preloaded.c:vg_cleanup_env [/usr/local/lib/valgrind/vgpreload_core-amd64-darwin.so]\n\n$\n```\n\n我已经移除了单元测试和`valgrind`输出，因为它们对这个练习没有用。你应该看到了`callgrind_anotate`输出，它向你展示了每个函数所运行的指令数量（`valgrind`中叫做`Ir`），由高到低排序。你通常可以忽略头文件的数据，直接跳到函数列表。\n\n> 注\n\n> 如果你获取到一堆“???:Image”的行，并且它们不是你程序中的东西，那么你读到的是OS的垃圾。只需要在末尾添加`| grep -v \"???\"`来过滤掉它们。\n\n我现在可以对这个输出做个简短的分解，来找出下一步观察什么：\n\n+ 每一行都列出了`Ir`序号和执行它们的`file:function `。`Ir`是指令数量，并且如果它越少就越快。这里有些复杂，但是首先要着眼于`Ir`。\n+ 解决这个程序的方式是观察最上面的函数，之后看看你首先可以改进哪一个。这里，我可以改进`bstrcmp`或者`BStree_get`。可能以`BStree_get`开始更容易些。\n+ 这些函数的一部分由单元测试调用，所以我可以忽略它们。类似`bformat`，`bfromcstralloc`和 `bdestroy`就是这样的函数。\n+ 我也可以找到我可以简单地避免调用的函数。例如，或许我可以假设`BSTree`仅仅处理`bstring`键，之后我可以不使用回调系统，并且完全移除`default_compare`。\n\n到目前为止，我只知道我打算改进`BSTree_get`，并且不是因为`BSTree_get`执行慢。这是分析的第二阶段。\n\n## Callgrind 注解源文件\n\n下一步我使用`callgrind_annotate`输出`bstree.c`文件，并且使用所带有的`Ir`对每一行做注解。你可以通过运行下面的命令来得到注解后的源文件：\n\n```sh\n$ callgrind_annotate callgrind.out.1232 src/lcthw/bstree.c\n...\n```\n\n你的输出会是这个源文件的一个较大的转储，但是我会将它们剪切成包含`BSTree_get`和`BSTree_getnode`的部分：\n\n```c\n--------------------------------------------------------------------------------\n-- User-annotated source: src/lcthw/bstree.c\n--------------------------------------------------------------------------------\n    Ir\n\n\n 2,453  static inline BSTreeNode *BSTree_getnode(BSTree *map, BSTreeNode *node, void *key)\n     .  {\n61,853      int cmp = map->compare(node->key, key);\n663,908  => src/lcthw/bstree.c:default_compare (14850x)\n     .\n14,850      if(cmp == 0) {\n     .          return node;\n24,794      } else if(cmp < 0) {\n30,623          if(node->left) {\n     .              return BSTree_getnode(map, node->left, key);\n     .          } else {\n     .              return NULL;\n     .          }\n     .      } else {\n13,146          if(node->right) {\n     .              return BSTree_getnode(map, node->right, key);\n     .          } else {\n     .              return NULL;\n     .          }\n     .      }\n     .  }\n     .\n     .  void *BSTree_get(BSTree *map, void *key)\n 4,912  {\n24,557      if(map->root == NULL) {\n14,736          return NULL;\n     .      } else {\n     .          BSTreeNode *node = BSTree_getnode(map, map->root, key);\n 2,453          return node == NULL ? NULL : node->data;\n     .      }\n     .  }\n```\n\n每一行都显示它的`Ir`（指令）数量，或者一个点（`.`）来表示它并不重要。我所要找的就是一些热点，或者带有巨大数值的`Ir`的行，它能够被优化掉。这里，第十行的输出表明，`BSTree_getnode`开销非常大的原因是它调用了`default_comapre`，它又调用了`bstrcmp`。我已经知道了`bstrcmp`是性能最差的函数，所以如果我想要改进`BSTree_getnode`的速度，我应该首先解决掉它。\n\n之后我以相同方式查看`bstrcmp`：\n\n```c\n 98,370  int bstrcmp (const_bstring b0, const_bstring b1) {\n      .  int i, v, n;\n      .\n196,740     if (b0 == NULL || b1 == NULL || b0->data == NULL || b1->data == NULL ||\n 32,790             b0->slen < 0 || b1->slen < 0) return SHRT_MIN;\n 65,580     n = b0->slen; if (n > b1->slen) n = b1->slen;\n 89,449     if (b0->slen == b1->slen && (b0->data == b1->data || b0->slen == 0))\n      .             return BSTR_OK;\n      .\n 23,915     for (i = 0; i < n; i ++) {\n163,642             v = ((char) b0->data[i]) - ((char) b1->data[i]);\n      .             if (v != 0) return v;\n      .             if (b0->data[i] == (unsigned char) '\\0') return BSTR_OK;\n      .     }\n      .\n      .     if (b0->slen > n) return 1;\n      .     if (b1->slen > n) return -1;\n      .     return BSTR_OK;\n      .  }\n```\n\n输出中让我预料之外的事情就是`bstrcmp`最糟糕的一行并不是我想象中的字符比较。对于内存访问，顶部的防御性`if`语句将所有可能的无效变量都检查了一遍。与第十七行比较字符的语句相比，这个`if`语句进行了多于两倍的内存访问。如果我要优化`bstcmp`，我会完全把它去掉，或者在其它一些地方来执行它。\n\n另一种选择是将这个检查改为`assert`，它只在开发时的运行中存在，之后在发布时把它去掉。我没有足够的证明来表明这行代码不适于这个数据结构，所以我可以证明移除它是可行的。\n\n然而，我并不想弱化这个函数的防御性，来得到一些性能。在真实的性能优化环境，我会简单地把它放到列表中，之后挖掘程序中能得到的其它收益。\n\n## 调优之道\n\n> 我们应该忽略微小的效率，对于97%的情况：过早优化是万恶之源。\n\n> -- 高德纳\n\n在我看来，这个引述似乎忽略了一个关于性能调优的重点。在高德纳的话中，当你做性能调优时，如果你过早去做它，可能会导致各种问题。根据他的话，优化应该执行于“稍晚的某个时间”，或者这只是我的猜测。谁知道呢。\n\n我打算澄清这个引述并不是完全错误，而是忽略了某些东西，并且我打算给出我的引述。你可以引用我的这段话：\n\n> 使用证据来寻找最大的优化并花费最少的精力。\n\n> -- 泽德 A. 肖\n\n你什么时候优化并不重要，但是你需要弄清楚你的优化是否真正能改进软件，以及需要投入多少精力来实现它。通过证据你就可以找到代码中的位置，用一点点精力就能取得最大的提升。通常这些地方都是一些愚蠢的决定，就像`bstrcmp`试图检查任何东西不为`NULL`一样。\n\n在某个特定时间点上，代码中需要调优的地方只剩下极其微小的优化，比如重新组织`if`语句，或者类似达夫设备这样的特殊循环。这时候，你应该停止优化，因为这是一个好机会，你可以通过重新设计软件并且避免这些事情来获得更多收益。\n\n这是一些只想做优化的程序员没有看到的事情。许多时候，把一件事情做快的最好方法就是寻找避免它们的办法。在上面的分析中，我不打算优化`bstrcmp`，我会寻找一个不使用它的方法。也许我可以使用一种哈希算法来执行可排序的哈希计算而不是始终使用`bstrcmp`。也许我可以通过首先尝试第一个字符，如果它们不匹配就没必要调用`bstrcmp`。\n\n如果在此之后你根本不能重新设计，那么就开始寻找微小的优化，但是要始终确保它们能够提升速度。要记住目标是使用最少的精力尽可能得到最大的效果。\n\n## 使用 KCachegrind\n\n这个练习最后一部分就是向你介绍一个叫做[KCachegrind](http://kcachegrind.sourceforge.net/html/Home.html)的神奇的GUI工具，用于分析`callgrind` 和 `cachegrind`的输出。我使用Linux或BSD电脑上工作时几乎都会使用它，并且我实际上为了使用`KCachegrind`而切换到Linux来编写代码。\n\n教会你如何使用是这个练习之外的内容，你需要在这个练习之后自己学习如何用它。输出几乎是相同的，除了`KCachegrind`可以让你做这些：\n\n+ 图形化地浏览源码和执行次数，并使用各种排序来搜索可优化的东西。\n+ 分析不同的图表，来可视化地观察什么占据了大多数时间，以及它调用了什么。\n+ 查看真实的汇编机器码输出，使你能够看到实际的指令，给你更多的线索。\n+ 可视化地显示源码中的循环和分支的跳跃方式，便于你更容易地找到优化代码的方法。\n\n你应该在获取、安装和玩转`KCachegrind`上花一些时间。\n\n## 附加题\n\n+ 阅读[ callgrind 手册页](http://valgrind.org/docs/manual/cl-manual.html)并且尝试一些高级选项。\n+ 阅读[ cachegrind 手册页](http://valgrind.org/docs/manual/cg-manual.html)并且也尝试一些高级选项。\n+ 在所有单元测试上使用`callgrind` 和 `cachegrind`，看看你能否找到可优化的地方。你找到一些预料之外的事情了吗？如果没有，你可能观察地不够仔细。\n+ 使用 KCachegrind 并且观察它和我这里的输出有什么不同。\n+ 现在使用这些工具来完成练习40的附加题和改进部分。\n"
  },
  {
    "path": "docs/lcthw-zh/ex42.md",
    "content": "# 练习42：栈和队列\n\n> 原文：[Exercise 42: Stacks and Queues](http://c.learncodethehardway.org/book/ex42.html)\n\n> 译者：[飞龙](https://github.com/wizardforcel)\n\n到现在为止，你已经知道了大多数用于构建其它数据结构的数据结构。如果你拥有一些`List`、`DArray`、`Hashmap` 和 `Tree`，你就能用他们构造出大多数其它的任何结构。你碰到的其它任何结构要么可以用它们实现，要么是它们的变体。如果不是的话，它可能是外来的数据结构，你可能不需要它。\n\n`Stack`和`Queue`是非常简单的数据结构，它们是`List`的变体。它们是`List`的弱化或者转换形式，因为你只需要在`List`的一端放置元素。对于`Stack`，你只能能够在一段压入和弹出元素。而对于`Queue`，你只能够在开头压入元素，并在末尾弹出（或者反过来）。\n\n我能够只通过C预处理器和两个头文件来实现这两个数据结构。我的头文件只有21行的长度，并且实现了所有`Stack`和`Queue`的操作，不带有任何神奇的定义。\n\n我将会向你展示单元测试，你需要实现头文件来让它们正常工作。你不能创建`stack.c` 或 `queue.c`实现文件来通过测试，只能使用`stack.h` 和 `queue.h`来使测试运行。\n\n```c\n#include \"minunit.h\"\n#include <lcthw/stack.h>\n#include <assert.h>\n\nstatic Stack *stack = NULL;\nchar *tests[] = {\"test1 data\", \"test2 data\", \"test3 data\"};\n#define NUM_TESTS 3\n\n\nchar *test_create()\n{\n    stack = Stack_create();\n    mu_assert(stack != NULL, \"Failed to create stack.\");\n\n    return NULL;\n}\n\nchar *test_destroy()\n{\n    mu_assert(stack != NULL, \"Failed to make stack #2\");\n    Stack_destroy(stack);\n\n    return NULL;\n}\n\nchar *test_push_pop()\n{\n    int i = 0;\n    for(i = 0; i < NUM_TESTS; i++) {\n        Stack_push(stack, tests[i]);\n        mu_assert(Stack_peek(stack) == tests[i], \"Wrong next value.\");\n    }\n\n    mu_assert(Stack_count(stack) == NUM_TESTS, \"Wrong count on push.\");\n\n    STACK_FOREACH(stack, cur) {\n        debug(\"VAL: %s\", (char *)cur->value);\n    }\n\n    for(i = NUM_TESTS - 1; i >= 0; i--) {\n        char *val = Stack_pop(stack);\n        mu_assert(val == tests[i], \"Wrong value on pop.\");\n    }\n\n    mu_assert(Stack_count(stack) == 0, \"Wrong count after pop.\");\n\n    return NULL;\n}\n\nchar *all_tests() {\n    mu_suite_start();\n\n    mu_run_test(test_create);\n    mu_run_test(test_push_pop);\n    mu_run_test(test_destroy);\n\n    return NULL;\n}\n\nRUN_TESTS(all_tests);\n```\n\n之后是`queue_tests.c`，几乎以相同的方式来使用`Queue`：\n\n```c\n#include \"minunit.h\"\n#include <lcthw/queue.h>\n#include <assert.h>\n\nstatic Queue *queue = NULL;\nchar *tests[] = {\"test1 data\", \"test2 data\", \"test3 data\"};\n#define NUM_TESTS 3\n\n\nchar *test_create()\n{\n    queue = Queue_create();\n    mu_assert(queue != NULL, \"Failed to create queue.\");\n\n    return NULL;\n}\n\nchar *test_destroy()\n{\n    mu_assert(queue != NULL, \"Failed to make queue #2\");\n    Queue_destroy(queue);\n\n    return NULL;\n}\n\nchar *test_send_recv()\n{\n    int i = 0;\n    for(i = 0; i < NUM_TESTS; i++) {\n        Queue_send(queue, tests[i]);\n        mu_assert(Queue_peek(queue) == tests[0], \"Wrong next value.\");\n    }\n\n    mu_assert(Queue_count(queue) == NUM_TESTS, \"Wrong count on send.\");\n\n    QUEUE_FOREACH(queue, cur) {\n        debug(\"VAL: %s\", (char *)cur->value);\n    }\n\n    for(i = 0; i < NUM_TESTS; i++) {\n        char *val = Queue_recv(queue);\n        mu_assert(val == tests[i], \"Wrong value on recv.\");\n    }\n\n    mu_assert(Queue_count(queue) == 0, \"Wrong count after recv.\");\n\n    return NULL;\n}\n\nchar *all_tests() {\n    mu_suite_start();\n\n    mu_run_test(test_create);\n    mu_run_test(test_send_recv);\n    mu_run_test(test_destroy);\n\n    return NULL;\n}\n\nRUN_TESTS(all_tests);\n```\n\n你应该在不修改测试文件的条件下，使单元测试能够运行，并且它应该能够通过`valgrind`而没有任何内存错误。下面是当我直接运行`stack_tests`时它的样子：\n\n```sh\n$ ./tests/stack_tests\nDEBUG tests/stack_tests.c:60: ----- RUNNING: ./tests/stack_tests\n----\nRUNNING: ./tests/stack_tests\nDEBUG tests/stack_tests.c:53:\n----- test_create\nDEBUG tests/stack_tests.c:54:\n----- test_push_pop\nDEBUG tests/stack_tests.c:37: VAL: test3 data\nDEBUG tests/stack_tests.c:37: VAL: test2 data\nDEBUG tests/stack_tests.c:37: VAL: test1 data\nDEBUG tests/stack_tests.c:55:\n----- test_destroy\nALL TESTS PASSED\nTests run: 3\n$\n```\n\n`queue_test`的输出基本一样，所以我在这里就不展示了。\n\n## 如何改进\n\n你可以做到的唯一真正的改进，就是把所用的`List`换成`DArray`。`Queue`数据结构难以用`DArray`实现，因为它要同时处理两端的节点。\n\n完全在头文件中来实现它们的缺点，是你并不能够轻易地对它做性能调优。你需要使用这种技巧，建立一种以特定的方式使用`List`的“协议”。做性能调优时，如果你优化了`List`，这两种数据结构都会有所改进。\n\n## 附加题\n\n+ 使用`DArray`代替`List`实现`Stack`，并保持单元测试不变。这意味着你需要创建你自己的`STACK_FOREACH`。\n"
  },
  {
    "path": "docs/lcthw-zh/ex43.md",
    "content": "# 练习43：一个简单的统计引擎\n\n> 原文：[Exercise 43: A Simple Statistics Engine](http://c.learncodethehardway.org/book/ex43.html)\n\n> 译者：[飞龙](https://github.com/wizardforcel)\n\n这是一个简单的算法，我将其用于“联机”（不储存任何样本）收集概要统计。我在任何需要执行一些统计，比如均值、标准差和求和中使用它，但是其中我并不会储存所需的全部样本。我只需要储存计算出的结果，它们仅仅含有5个数值。\n\n## 计算标准差和均值\n\n首先你需要一系列样本。它可以使任何事情，比如完成一个任务所需的时间，某人访问某个东西的次数，或者甚至是网站的评分。是什么并不重要，只要你能得到一些数字，并且你想要知道它们的下列概要统计值：\n\n`sum`\n\n对所有数字求和。\n\n`sumsq`（平方和）\n\n对所有数字求平方和。\n\n`count(n)`\n\n求出样本数量。\n\n`min`\n\n求出样本最小值。\n\n`max`\n\n求出样本最大值。\n\n`mean`\n\n求出样本的均值。它类似于但又不是中位数，但可作为中位数的估计。\n\n`stddev`\n\n使用`$sqrt(sumsq - (sum * mean) / (n - 1) )`来计算标准差，其中`sqrt`为`math.h`头文件中的平方根。\n\n我将会使用R来验证这些计算，因为我知道R能够计算正确。\n\n```r\n> s <- runif(n=10, max=10)\n> s\n [1] 6.1061334 9.6783204 1.2747090 8.2395131 0.3333483 6.9755066 1.0626275\n [8] 7.6587523 4.9382973 9.5788115\n> summary(s)\n   Min. 1st Qu.  Median    Mean 3rd Qu.    Max.\n 0.3333  2.1910  6.5410  5.5850  8.0940  9.6780\n> sd(s)\n[1] 3.547868\n> sum(s)\n[1] 55.84602\n> sum(s * s)\n[1] 425.1641\n> sum(s) * mean(s)\n[1] 311.8778\n> sum(s * s) - sum(s) * mean(s)\n[1] 113.2863\n> (sum(s * s) - sum(s) * mean(s)) / (length(s) - 1)\n[1] 12.58737\n> sqrt((sum(s * s) - sum(s) * mean(s)) / (length(s) - 1))\n[1] 3.547868\n>\n```\n\n你并不需要懂得R，只需要看着我拆分代码来解释如何检查这些运算：\n\nlines 1-4\n\n我使用`runit`函数来获得“随机形式”的数字分布，之后将它们打印出来。我会在接下来的单元测试中用到它。\n\nlines 5-7\n\n这个就是概要，便于你看到R如何计算它们。\n\nlines 8-9\n\n这是使用`sd`函数计算的`stddev`。\n\nlines 10-11\n\n现在我开始手动进行这一计算，首先计算`sum`。\n\nlines 12-13\n\n`stddev`公式中的下一部分是`sumsq`，我可以通过`sum(s * s)`来得到，它告诉R将整个`s`列表乘以其自身，之后计算它们的`sum`。R的可以在整个数据结构上做运算，就像这样。\n\nlines 14-15\n\n观察那个公式，我之后需要`sum`乘上`mean`，所以我执行了`sum(s) * mean(s)`。\n\nlines 16-17\n\n我接着将`sumsq`参与运算，得到`sum(s * s) - sum(s) * mean(s)`。\n\nlines 18-19\n\n还需要除以`n - 1`，所以我执行了`(sum(s * s) - sum(s) * mean(s)) / (length(s) - 1)`。\n\nlines 20-21\n\n随后，我使用`sqrt`算出平方根，并得到3.547868，它符合R通过`sd`的运算结果。\n\n## 实现\n\n这就是计算`stddev`的方法，现在我可以编写一些简单的代码来实现这一计算。\n\n```c\n#ifndef lcthw_stats_h\n#define lctwh_stats_h\n\ntypedef struct Stats {\n    double sum;\n    double sumsq;\n    unsigned long n;\n    double min;\n    double max;\n} Stats;\n\nStats *Stats_recreate(double sum, double sumsq, unsigned long n, double min, double max);\n\nStats *Stats_create();\n\ndouble Stats_mean(Stats *st);\n\ndouble Stats_stddev(Stats *st);\n\nvoid Stats_sample(Stats *st, double s);\n\nvoid Stats_dump(Stats *st);\n\n#endif\n```\n\n这里你可以看到我将所需的统计量放入一个struct，并且创建了用于处理样本和获得数值的函数。实现它只是转换数字的一个练习：\n\n```c\n#include <math.h>\n#include <lcthw/stats.h>\n#include <stdlib.h>\n#include <lcthw/dbg.h>\n\nStats *Stats_recreate(double sum, double sumsq, unsigned long n, double min, double max)\n{\n    Stats *st = malloc(sizeof(Stats));\n    check_mem(st);\n\n    st->sum = sum;\n    st->sumsq = sumsq;\n    st->n = n;\n    st->min = min;\n    st->max = max;\n\n    return st;\n\nerror:\n    return NULL;\n}\n\nStats *Stats_create()\n{\n    return Stats_recreate(0.0, 0.0, 0L, 0.0, 0.0);\n}\n\ndouble Stats_mean(Stats *st)\n{\n    return st->sum / st->n;\n}\n\ndouble Stats_stddev(Stats *st)\n{\n   return sqrt( (st->sumsq - ( st->sum * st->sum / st->n)) / (st->n - 1) );\n}\n\nvoid Stats_sample(Stats *st, double s)\n{\n    st->sum += s;\n    st->sumsq += s * s;\n\n    if(st->n == 0) {\n        st->min = s;\n        st->max = s;\n    } else {\n        if(st->min > s) st->min = s;\n        if(st->max < s) st->max = s;\n    }\n\n    st->n += 1;\n}\n\nvoid Stats_dump(Stats *st)\n{\n    fprintf(stderr, \"sum: %f, sumsq: %f, n: %ld, min: %f, max: %f, mean: %f, stddev: %f\",\n            st->sum, st->sumsq, st->n, st->min, st->max,\n            Stats_mean(st), Stats_stddev(st));\n}\n```\n\n下面是` stats.c`中每个函数的作用：\n\nStats_recreate\n\n我希望从一些数据中加载这些数据，这和函数让我重新创建`Stats`结构体。\n\nStats_create\n\n只是以全0的值调用`Stats_recreate`。\n\nStats_mean\n\n使用`sum`和`n`计算均值。\n\nStats_stddev\n\n实现我之前的公式，唯一的不同就是我使用`t->sum / st->n`来计算均值，而不是调用`Stats_mean`。\n\nStats_sample\n\n它用于在`Stats`结构体中储存数值。当你向它提供数值时，它看到`n`是0，并且相应地设置`min`和`max`。之后的每次调用都会使`sum`、`sumsq`和`n`增加，并且计算出这一新的样本的`min`和`max`值。\n\nStats_dump\n\n简单的调试函数，用于转储统计量，便于你看到它们。\n\n我需要干的最后一件事，就是确保这些运算正确。我打算使用我的样本，以及来自于R会话中的计算结果创建单元测试，来确保我会得到正确的结果。\n\n```c\n#include \"minunit.h\"\n#include <lcthw/stats.h>\n#include <math.h>\n\nconst int NUM_SAMPLES = 10;\ndouble samples[] = {\n    6.1061334, 9.6783204, 1.2747090, 8.2395131, 0.3333483,\n    6.9755066, 1.0626275, 7.6587523, 4.9382973, 9.5788115\n};\n\nStats expect = {\n    .sumsq = 425.1641,\n    .sum = 55.84602,\n    .min = 0.333,\n    .max = 9.678,\n    .n = 10,\n};\ndouble expect_mean = 5.584602;\ndouble expect_stddev = 3.547868;\n\n#define EQ(X,Y,N) (round((X) * pow(10, N)) == round((Y) * pow(10, N)))\n\nchar *test_operations()\n{\n    int i = 0;\n    Stats *st = Stats_create();\n    mu_assert(st != NULL, \"Failed to create stats.\");\n\n    for(i = 0; i < NUM_SAMPLES; i++) {\n        Stats_sample(st, samples[i]);\n    }\n\n    Stats_dump(st);\n\n    mu_assert(EQ(st->sumsq, expect.sumsq, 3), \"sumsq not valid\");\n    mu_assert(EQ(st->sum, expect.sum, 3), \"sum not valid\");\n    mu_assert(EQ(st->min, expect.min, 3), \"min not valid\");\n    mu_assert(EQ(st->max, expect.max, 3), \"max not valid\");\n    mu_assert(EQ(st->n, expect.n, 3), \"max not valid\");\n    mu_assert(EQ(expect_mean, Stats_mean(st), 3), \"mean not valid\");\n    mu_assert(EQ(expect_stddev, Stats_stddev(st), 3), \"stddev not valid\");\n\n    return NULL;\n}\n\nchar *test_recreate()\n{\n    Stats *st = Stats_recreate(expect.sum, expect.sumsq, expect.n, expect.min, expect.max);\n\n    mu_assert(st->sum == expect.sum, \"sum not equal\");\n    mu_assert(st->sumsq == expect.sumsq, \"sumsq not equal\");\n    mu_assert(st->n == expect.n, \"n not equal\");\n    mu_assert(st->min == expect.min, \"min not equal\");\n    mu_assert(st->max == expect.max, \"max not equal\");\n    mu_assert(EQ(expect_mean, Stats_mean(st), 3), \"mean not valid\");\n    mu_assert(EQ(expect_stddev, Stats_stddev(st), 3), \"stddev not valid\");\n\n    return NULL;\n}\n\nchar *all_tests()\n{\n    mu_suite_start();\n\n    mu_run_test(test_operations);\n    mu_run_test(test_recreate);\n\n    return NULL;\n}\n\nRUN_TESTS(all_tests);\n```\n\n这个单元测试中没什么新东西，除了`EQ`宏。我比较懒，并且不想查询比较两个`double`值的标准方法，所以我使用了这个宏。`double`的问题是等性不是完全相等，因为我使用了两个不同的系统，并带有不同的四舍五入的位数。解决方案就是判断两个数“乘以10的X次方是否相等”。\n\n我使用`EQ`来计算数字的10的幂，之后使用`round`函数来获得证书。这是个简单的方法来四舍五入N位小数，并以整数比较结果。我确定有数以亿计的其它方法能做相同的事情，但是现在我就用这种。\n\n预期结果储存在`Stats` `struct`中，之后我只是确保我得到的数值接近R给我的数值。\n\n## 如何使用\n\n你可以使用标准差和均值来决定一个新的样本是否是“有趣”的，或者你可以使用它们计算统计量的统计量。前者对于人们来说更容易理解，所以我用登录的例子来做个简短的解释。\n\n假设你在跟踪人们花费多长时间在一台服务器上，并且你打算用统计来分析它。每次有人登录进来，你都对它们在这里的时长保持跟踪，之后调用`Stats_sample`函数。我会寻找停留“过长”时间的人，以及“过短”的人。\n\n比起设定特殊的级别，我更倾向于将一个人的停留时间与`mean (plus or minus) 2 * stddev`这个范围进行比较。我计算出`mean`和`2 * stddev`，并且如果它们在这个范围之外，我就认为是“有趣”的。由于我使用了联机算法来维护这些统计量，所以它非常快，并且我可以使软件标记在这个范围外的用户。\n\n这不仅仅用于找出行为异常的用户，更有助于标记一些潜在的问题，你可以查看它们来观察发生了什么。它基于所有用户的行为来计算，这也避免了你任意挑出一个数值而并不基于实际情况的问题。\n\n你可以从中学到的通用规则是，`mean (plus or minus) 2 * stddev`是90%的值预期所属的范围预测值，任何在它之外的值都是有趣的。\n\n第二种利用这些统计量的方式就是继续将其用于其它的`Stats`计算。基本上像通常一样使用`Stats_sample`，但是之后在`min`、`max`、`n`、`mean`和`stddev`上执行`Stats_sample`。这会提供二级的度量，并且让你对比样本的样本。\n\n被搞晕了吗？我会以上面的例子基础，并且假设你拥有100台服务器，每台都运行一个应用。你已经在每个应用服务器上跟踪了用户的登录时长，但是你想要比较所有的这100和应用，并且标记它们当中任何登录时间过长的用户。最简单的方式就是每次有人登录进来时，计算新的登录统计量，之后将`Stats structs`的元素添加到第二个`Stats`中。\n\n你最后应该会得到一些统计量，它们可以这样命名：\n\n均值的均值\n\n这是一个`Stats struct`，它向你提供所有服务器的均值的`mean`和`stddev`。你可以用全局视角来观察任何在此之外的用户或服务器。\n\n标准差的均值\n\n另一个`Stats struct`，计算这些服务器的分布的统计量。你之后可以分析每个服务器并且观察是否它们中的任何服务器具有异常分散的分布，通过将它们的`stddev`和这个`mean of stddevs`统计量进行对比。\n\n你可以计算出全部统计量，但是这两个是最有用的。如果你打算监视服务器上的移除登录时间，你可以这样做：\n\n+ 用户John登录并登出服务器A。获取服务器A的统计量，并更新它们。\n+ 获取`mean of means`统计量，计算出A的均值并且将其加入样本。我叫它`m_of_m`。\n+ 获取`mean of stddev`统计量，将A的标准差添加到样本中。我叫它` m_of_s`。\n+ 如果A的`mean`在`m_of_m.mean + 2 * m_of_m.stddev`范围外，标记它可能存在问题。\n+ 如果A的`stddev`在`m_of_s.mean + 2 * m_of_s.stddev`范围外，标记它可能存在行为异常。\n+ 最后，如果John的登录时长在A的范围之外，或A的`m_of_m`范围之外，标记为有趣的。\n\n通过计算“均值的均值”，或者“标准差的均值”，你可以以最小的执行和储存总量，有效地跟踪许多度量。\n\n## 附加题\n\n+ 将`Stats_stddev` 和 `Stats_mean`转换为`static inline`函数，放到`stats.h`文件中，而不是`stats.c`文件。\n+ 使用这份代码来编写`string_algos_test.c`的性能测试。使它为可选的，并且运行基准测试作为一系列样本，之后报告结果。\n+ 编写它的另一个语言的版本。确保这个版本基于我的数据正确执行。\n+ 编写一个小型程序，它能从文件读取所有数字，并执行这些统计。\n+ 使程序接收一个数据表，其中第一行是表头，剩下的行含有任意数量空格分隔的数值。你的程序应该按照表头中的名称，打印出每一列的统计值。\n"
  },
  {
    "path": "docs/lcthw-zh/ex44.md",
    "content": "# 练习44：环形缓冲区\n\n> 原文：[Exercise 44: Ring Buffer](http://c.learncodethehardway.org/book/ex44.html)\n\n> 译者：[飞龙](https://github.com/wizardforcel)\n\n环形缓冲区在处理异步IO时非常实用。它们可以在一端接收随机长度和区间的数据，在另一端以相同长度和区间提供密致的数据块。它们是`Queue`数据结构的变体，但是它针对于字节块而不是一系列指针。这个练习中我打算向你展示`RingBuffer`的代码，并且之后你需要对它执行完整的单元测试。\n\n```c\n#ifndef _lcthw_RingBuffer_h\n#define _lcthw_RingBuffer_h\n\n#include <lcthw/bstrlib.h>\n\ntypedef struct {\n    char *buffer;\n    int length;\n    int start;\n    int end;\n} RingBuffer;\n\nRingBuffer *RingBuffer_create(int length);\n\nvoid RingBuffer_destroy(RingBuffer *buffer);\n\nint RingBuffer_read(RingBuffer *buffer, char *target, int amount);\n\nint RingBuffer_write(RingBuffer *buffer, char *data, int length);\n\nint RingBuffer_empty(RingBuffer *buffer);\n\nint RingBuffer_full(RingBuffer *buffer);\n\nint RingBuffer_available_data(RingBuffer *buffer);\n\nint RingBuffer_available_space(RingBuffer *buffer);\n\nbstring RingBuffer_gets(RingBuffer *buffer, int amount);\n\n#define RingBuffer_available_data(B) (((B)->end + 1) % (B)->length - (B)->start - 1)\n\n#define RingBuffer_available_space(B) ((B)->length - (B)->end - 1)\n\n#define RingBuffer_full(B) (RingBuffer_available_data((B)) - (B)->length == 0)\n\n#define RingBuffer_empty(B) (RingBuffer_available_data((B)) == 0)\n\n#define RingBuffer_puts(B, D) RingBuffer_write((B), bdata((D)), blength((D)))\n\n#define RingBuffer_get_all(B) RingBuffer_gets((B), RingBuffer_available_data((B)))\n\n#define RingBuffer_starts_at(B) ((B)->buffer + (B)->start)\n\n#define RingBuffer_ends_at(B) ((B)->buffer + (B)->end)\n\n#define RingBuffer_commit_read(B, A) ((B)->start = ((B)->start + (A)) % (B)->length)\n\n#define RingBuffer_commit_write(B, A) ((B)->end = ((B)->end + (A)) % (B)->length)\n\n#endif\n```\n\n观察这个数据结构，你会看到它含有`buffer`、`start` 和 `end`。`RingBuffer`的所做的事情只是在`buffer`中移动`start`和`end`，所以当数据到达缓冲区末尾时还可以继续“循环”。这样就会给人一种在固定空间内无限读取的“幻觉”。接下来我创建了一些宏来基于它执行各种计算。\n\n下面是它的实现，它是对工作原理更好的解释：\n\n```c\n#undef NDEBUG\n#include <assert.h>\n#include <stdio.h>\n#include <stdlib.h>\n#include <string.h>\n#include <lcthw/dbg.h>\n#include <lcthw/ringbuffer.h>\n\nRingBuffer *RingBuffer_create(int length)\n{\n    RingBuffer *buffer = calloc(1, sizeof(RingBuffer));\n    buffer->length  = length + 1;\n    buffer->start = 0;\n    buffer->end = 0;\n    buffer->buffer = calloc(buffer->length, 1);\n\n    return buffer;\n}\n\nvoid RingBuffer_destroy(RingBuffer *buffer)\n{\n    if(buffer) {\n        free(buffer->buffer);\n        free(buffer);\n    }\n}\n\nint RingBuffer_write(RingBuffer *buffer, char *data, int length)\n{\n    if(RingBuffer_available_data(buffer) == 0) {\n        buffer->start = buffer->end = 0;\n    }\n\n    check(length <= RingBuffer_available_space(buffer),\n            \"Not enough space: %d request, %d available\",\n            RingBuffer_available_data(buffer), length);\n\n    void *result = memcpy(RingBuffer_ends_at(buffer), data, length);\n    check(result != NULL, \"Failed to write data into buffer.\");\n\n    RingBuffer_commit_write(buffer, length);\n\n    return length;\nerror:\n    return -1;\n}\n\nint RingBuffer_read(RingBuffer *buffer, char *target, int amount)\n{\n    check_debug(amount <= RingBuffer_available_data(buffer),\n            \"Not enough in the buffer: has %d, needs %d\",\n            RingBuffer_available_data(buffer), amount);\n\n    void *result = memcpy(target, RingBuffer_starts_at(buffer), amount);\n    check(result != NULL, \"Failed to write buffer into data.\");\n\n    RingBuffer_commit_read(buffer, amount);\n\n    if(buffer->end == buffer->start) {\n        buffer->start = buffer->end = 0;\n    }\n\n    return amount;\nerror:\n    return -1;\n}\n\nbstring RingBuffer_gets(RingBuffer *buffer, int amount)\n{\n    check(amount > 0, \"Need more than 0 for gets, you gave: %d \", amount);\n    check_debug(amount <= RingBuffer_available_data(buffer),\n            \"Not enough in the buffer.\");\n\n    bstring result = blk2bstr(RingBuffer_starts_at(buffer), amount);\n    check(result != NULL, \"Failed to create gets result.\");\n    check(blength(result) == amount, \"Wrong result length.\");\n\n    RingBuffer_commit_read(buffer, amount);\n    assert(RingBuffer_available_data(buffer) >= 0 && \"Error in read commit.\");\n\n    return result;\nerror:\n    return NULL;\n}\n```\n\n这些就是一个基本的`RingBuffer`实现的全部了。你可以从中读取和写入数据，获得它的大小和容量。也有一些缓冲区使用OS中的技巧来创建虚拟的无限存储，但它们不可移植。\n\n由于我的`RingBuffer`处理读取和写入内存块，我要保证任何`end == start`出现的时候我都要将它们重置为0，使它们从退回缓冲区头部。在维基百科上的版本中，它并不可以写入数据块，所以只能移动`end`和`start`来转圈。为了更好地处理数据块，你需要在数据为空时移动到内部缓冲区的开头。\n\n## 单元测试\n\n对于你的单元测试，你需要测试尽可能多的情况。最简单的方法就是预构造不同的`RingBuffer`结构，之后手动检查函数和算数是否有效。例如，你可以构造`end`在缓冲区末尾的右边，而`start`在缓冲区范围内的`RingBuffer`，来看看它是否执行成功。\n\n## 你会看到什么\n\n下面是我的`ringbuffer_tests`运行结果：\n\n```sh\n$ ./tests/ringbuffer_tests\nDEBUG tests/ringbuffer_tests.c:60: ----- RUNNING: ./tests/ringbuffer_tests\n----\nRUNNING: ./tests/ringbuffer_tests\nDEBUG tests/ringbuffer_tests.c:53:\n----- test_create\nDEBUG tests/ringbuffer_tests.c:54:\n----- test_read_write\nDEBUG tests/ringbuffer_tests.c:55:\n----- test_destroy\nALL TESTS PASSED\nTests run: 3\n$\n```\n\n你应该测试至少三次来确保所有基本操作有效，并且看看在我完成之前你能测试到额外的多少东西。\n\n## 如何改进\n\n像往常一样，你应该为这个练习做防御性编程检查。我希望你这样做，是因为` liblcthw`的代码基本上没有做我教给你的防御型编程检查。我将它们留给你，便于你熟悉使用这些额外的检查来改进代码。\n\n例如，这个环形缓冲区并没有过多检查每次访问是否实际上都在缓冲区内。\n\n如果你阅读[环形缓冲区的维基百科页面](http://en.wikipedia.org/wiki/Ring_buffer)，你会看到“优化的POSIX实现”，它使用POSIX特定的调用来创建一块无限的区域。研究并且在附加题中尝试实现它。\n\n## 附加题\n\n+ 创建`RingBuffer`的替代版本，使用POSIX的技巧并为其执行单元测试。\n+ 为二者添加一个性能对比测试，通过带有随机数据和随机读写操作的模糊测试来比较两个版本。确保你你对每个版本进行了相同的操作，便于你在操作之间比较二者。\n+ 使用`callgrind` 和 `cachegrind`比较二者的性能。\n"
  },
  {
    "path": "docs/lcthw-zh/ex45.md",
    "content": "# 练习45：一个简单的TCP/IP客户端\n\n> 原文：[Exercise 45: A Simple TCP/IP Client](http://c.learncodethehardway.org/book/ex45.html)\n\n> 译者：[飞龙](https://github.com/wizardforcel)\n\n我打算使用`RingBuffer`来创建一个非常简单的小型网络测试工具，叫做`netclient`。为此我需要向`Makefile`添加一些工具，来处理`bin/`目录下的小程序。\n\n## 扩展Makefile\n\n首先，为程序添加一些变量，就像单元测试的`TESTS`和`TEST_SRC`变量：\n\n```make\nPROGRAMS_SRC=$(wildcard bin/*.c)\nPROGRAMS=$(patsubst %.c,%,$(PROGRAMS_SRC))\n```\n\n之后你可能想要添加`PROGRAMS`到所有目标中：\n\n```make\nall: $(TARGET) $(SO_TARGET) tests $(PROGRAMS)\n```\n\n之后在`clean`目标中向`rm`那一行添加`PROGRAMS`：\n\n```make\nrm -rf build $(OBJECTS) $(TESTS) $(PROGRAMS)\n```\n\n最后你还需要在最后添加一个目标来构建它们：\n\n```make\n$(PROGRAMS): CFLAGS += $(TARGET)\n```\n\n做了这些修改你就能够将`.c`文件扔到`bin`中，并且编译它们以及为其链接库文件，就像测试那样。\n\n## netclient 代码\n\nnetclient的代码是这样的：\n\n```c\n#undef NDEBUG\n#include <stdlib.h>\n#include <sys/select.h>\n#include <stdio.h>\n#include <lcthw/ringbuffer.h>\n#include <lcthw/dbg.h>\n#include <sys/socket.h>\n#include <sys/types.h>\n#include <sys/uio.h>\n#include <arpa/inet.h>\n#include <netdb.h>\n#include <unistd.h>\n#include <fcntl.h>\n\nstruct tagbstring NL = bsStatic(\"\\n\");\nstruct tagbstring CRLF = bsStatic(\"\\r\\n\");\n\nint nonblock(int fd) {\n    int flags = fcntl(fd, F_GETFL, 0);\n    check(flags >= 0, \"Invalid flags on nonblock.\");\n\n    int rc = fcntl(fd, F_SETFL, flags | O_NONBLOCK);\n    check(rc == 0, \"Can't set nonblocking.\");\n\n    return 0;\nerror:\n    return -1;\n}\n\nint client_connect(char *host, char *port)\n{\n    int rc = 0;\n    struct addrinfo *addr = NULL;\n\n    rc = getaddrinfo(host, port, NULL, &addr);\n    check(rc == 0, \"Failed to lookup %s:%s\", host, port);\n\n    int sock = socket(AF_INET, SOCK_STREAM, 0);\n    check(sock >= 0, \"Cannot create a socket.\");\n\n    rc = connect(sock, addr->ai_addr, addr->ai_addrlen);\n    check(rc == 0, \"Connect failed.\");\n\n    rc = nonblock(sock);\n    check(rc == 0, \"Can't set nonblocking.\");\n\n    freeaddrinfo(addr);\n    return sock;\n\nerror:\n    freeaddrinfo(addr);\n    return -1;\n}\n\nint read_some(RingBuffer *buffer, int fd, int is_socket)\n{\n    int rc = 0;\n\n    if(RingBuffer_available_data(buffer) == 0) {\n        buffer->start = buffer->end = 0;\n    }\n\n    if(is_socket) {\n        rc = recv(fd, RingBuffer_starts_at(buffer), RingBuffer_available_space(buffer), 0);\n    } else {\n        rc = read(fd, RingBuffer_starts_at(buffer), RingBuffer_available_space(buffer));\n    }\n\n    check(rc >= 0, \"Failed to read from fd: %d\", fd);\n\n    RingBuffer_commit_write(buffer, rc);\n\n    return rc;\n\nerror:\n    return -1;\n}\n\n\nint write_some(RingBuffer *buffer, int fd, int is_socket)\n{\n    int rc = 0;\n    bstring data = RingBuffer_get_all(buffer);\n\n    check(data != NULL, \"Failed to get from the buffer.\");\n    check(bfindreplace(data, &NL, &CRLF, 0) == BSTR_OK, \"Failed to replace NL.\");\n\n    if(is_socket) {\n        rc = send(fd, bdata(data), blength(data), 0);\n    } else {\n        rc = write(fd, bdata(data), blength(data));\n    }\n\n    check(rc == blength(data), \"Failed to write everything to fd: %d.\", fd);\n    bdestroy(data);\n\n    return rc;\n\nerror:\n    return -1;\n}\n\n\nint main(int argc, char *argv[])\n{\n    fd_set allreads;\n    fd_set readmask;\n\n    int socket = 0;\n    int rc = 0;\n    RingBuffer *in_rb = RingBuffer_create(1024 * 10);\n    RingBuffer *sock_rb = RingBuffer_create(1024 * 10);\n\n    check(argc == 3, \"USAGE: netclient host port\");\n\n    socket = client_connect(argv[1], argv[2]);\n    check(socket >= 0, \"connect to %s:%s failed.\", argv[1], argv[2]);\n\n    FD_ZERO(&allreads);\n    FD_SET(socket, &allreads);\n    FD_SET(0, &allreads);\n\n    while(1) {\n        readmask = allreads;\n        rc = select(socket + 1, &readmask, NULL, NULL, NULL);\n        check(rc >= 0, \"select failed.\");\n\n        if(FD_ISSET(0, &readmask)) {\n            rc = read_some(in_rb, 0, 0);\n            check_debug(rc != -1, \"Failed to read from stdin.\");\n        }\n\n        if(FD_ISSET(socket, &readmask)) {\n            rc = read_some(sock_rb, socket, 0);\n            check_debug(rc != -1, \"Failed to read from socket.\");\n        }\n\n        while(!RingBuffer_empty(sock_rb)) {\n            rc = write_some(sock_rb, 1, 0);\n            check_debug(rc != -1, \"Failed to write to stdout.\");\n        }\n\n        while(!RingBuffer_empty(in_rb)) {\n            rc = write_some(in_rb, socket, 1);\n            check_debug(rc != -1, \"Failed to write to socket.\");\n        }\n    }\n\n    return 0;\n\nerror:\n    return -1;\n}\n```\n\n代码中使用了`select`来处理`stdin`（文件描述符0）和用于和服务器交互的`socket`中的事件。它使用了`RingBuffer`来储存和复制数据，并且你可以认为`read_some`和`write_some`函数都是`RingBuffer`中相似函数的原型。\n\n在这一小段代码中，可能有一些你并不知道的网络函数。当你碰到不知道的函数时，在手册页上查询它来确保你理解了它。这一小段代码可能需要让你研究用于小型服务器编程的所有C语言API。\n\n## 你会看到什么\n\n如果你完成了所有构建，测试的最快方式就是看看你能否从learncodethehardway.org上得到一个特殊的文件：\n\n```sh\n$\n$ ./bin/netclient learncodethehardway.org 80\nGET /ex45.txt HTTP/1.1\nHost: learncodethehardway.org\n\nHTTP/1.1 200 OK\nDate: Fri, 27 Apr 2012 00:41:25 GMT\nContent-Type: text/plain\nContent-Length: 41\nLast-Modified: Fri, 27 Apr 2012 00:42:11 GMT\nETag: 4f99eb63-29\nServer: Mongrel2/1.7.5\n\nLearn C The Hard Way, Exercise 45 works.\n^C\n$\n```\n\n这里我所做的事情是键入创建`/ex45.txt`的HTTP请求所需的语法，在`Host:`请求航之后，按下ENTER键来输入空行。接着我获取相应，包括响应头和内容。最后我按下CTRL-C来退出。\n\n## 如何使它崩溃\n\n这段代码肯定含有bug，但是当前在本书的草稿中，我会继续完成它。与此同时，尝试分析代码，并且用其它服务器来击溃它。一种叫做`netcat`的工具可以用于建立这种服务器。另一种方法就是使用`Python`或`Ruby`之类的语言创建一个简单的“垃圾服务器”，来产生垃圾数据，随机关闭连接，或者其它异常行为。\n\n如果你找到了bug，在评论中报告它们，我会修复它。\n\n## 附加题\n\n+ 像我提到的那样，这里面有一些你不知道的函数，去查询他们。实际上，即使你知道它们也要查询。\n+ 在`valgrind`下运行它来寻找错误。\n+ 为函数添加各种防御性编程检查，来改进它们。\n+ 使用`getopt`函数，运行用户提供选项来防止将`\\n`转换为`\\r\\n`。这仅仅用于需要处理行尾的协议例如HTTP。有时你可能不想执行转换，所以要给用户一个选择。\n"
  },
  {
    "path": "docs/lcthw-zh/ex46.md",
    "content": "# 练习46：三叉搜索树\n\n> 原文：[Exercise 46: Ternary Search Tree](http://c.learncodethehardway.org/book/ex46.html)\n\n> 译者：[飞龙](https://github.com/wizardforcel)\n\n我打算向你介绍的最后一种数据结构就是三叉搜索树（`TSTree`），它和`BSTree`很像，除了它有三个分支，`low`、`equal`和`high`。它的用法和`BStree`以及`Hashmap`基本相同，用于储存键值对的数据，但是它通过键中的独立字符来控制。这使得`TSTree`具有一些`BStree`和`Hashmap`不具备的功能。\n\n`TSTree`的工作方式是，每个键都是字符串，根据字符串中字符的等性，通过构建或者遍历一棵树来进行插入。首先由根节点开始，观察每个节点的字符，如果小于、等于或大于则去往相应的方向。你可以参考这个头文件：\n\n```c\n#ifndef _lcthw_TSTree_h\n#define _lctwh_TSTree_h\n\n#include <stdlib.h>\n#include <lcthw/darray.h>\n\ntypedef struct TSTree {\n    char splitchar;\n    struct TSTree *low;\n    struct TSTree *equal;\n    struct TSTree *high;\n    void *value;\n} TSTree;\n\nvoid *TSTree_search(TSTree *root, const char *key, size_t len);\n\nvoid *TSTree_search_prefix(TSTree *root, const char *key, size_t len);\n\ntypedef void (*TSTree_traverse_cb)(void *value, void *data);\n\nTSTree *TSTree_insert(TSTree *node, const char *key, size_t len, void *value);\n\nvoid TSTree_traverse(TSTree *node, TSTree_traverse_cb cb, void *data);\n\nvoid TSTree_destroy(TSTree *root);\n\n#endif\n```\n\n`TSTree`拥有下列成员：\n\nsplitchar\n\n树中该节点的字符。\n\nlow\n\n小于`splitchar`的分支。\n\nequal\n\n等于`splitchar`的分支。\n\nhigh\n\n大于`splitchar`的分支。\n\nvalue\n\n这个节点上符合当前`splitchar`的值的集合。\n\n你可以看到这个实现中含有下列操作：\n\nsearch\n\n为特定`key`寻找值的典型操作。\n\nsearch_prefix\n\n寻找第一个以`key`为前缀的值，这是你不能轻易使用`BSTree` 或 `Hashmap` 完成的操作。\n\ninsert\n\n将`key`根据每个字符拆分，并把它插入到树中。\n\ntraverse\n\n遍历整颗树，使你能够收集或分析所包含的所有键和值。\n\n唯一缺少的操作就是`TSTree_delete`，这是因为它是一个开销很大的操作，比`BSTree_delete`大得多。当我使用`TSTree`结构时，我将它们视为常量数据，我打算遍历许多次，但是永远不会移除任何东西。它们对于这样的操作会很快，但是不适于需要快速插入或删除的情况。为此我会使用`Hashmap`因为它优于`BSTree`和`TSTree`。\n\n`TSTree`的实现非常简单，但是第一次可能难以理解。我会在你读完之后拆分它。\n\n```c\n#include <stdlib.h>\n#include <stdio.h>\n#include <assert.h>\n#include <lcthw/dbg.h>\n#include <lcthw/tstree.h>\n\nstatic inline TSTree *TSTree_insert_base(TSTree *root, TSTree *node,\n        const char *key, size_t len, void *value)\n{\n    if(node == NULL) {\n        node = (TSTree *) calloc(1, sizeof(TSTree));\n\n        if(root == NULL) {\n            root = node;\n        }\n\n        node->splitchar = *key;\n    }\n\n    if(*key < node->splitchar) {\n        node->low = TSTree_insert_base(root, node->low, key, len, value);\n    } else if(*key == node->splitchar) {\n        if(len > 1) {\n            node->equal = TSTree_insert_base(root, node->equal, key+1, len - 1, value);\n        } else {\n            assert(node->value == NULL && \"Duplicate insert into tst.\");\n            node->value = value;\n        }\n    } else {\n        node->high = TSTree_insert_base(root, node->high, key, len, value);\n    }\n\n    return node;\n}\n\nTSTree *TSTree_insert(TSTree *node, const char *key, size_t len, void *value)\n{\n    return TSTree_insert_base(node, node, key, len, value);\n}\n\nvoid *TSTree_search(TSTree *root, const char *key, size_t len)\n{\n    TSTree *node = root;\n    size_t i = 0;\n\n    while(i < len && node) {\n        if(key[i] < node->splitchar) {\n            node = node->low;\n        } else if(key[i] == node->splitchar) {\n            i++;\n            if(i < len) node = node->equal;\n        } else {\n            node = node->high;\n        }\n    }\n\n    if(node) {\n        return node->value;\n    } else {\n        return NULL;\n    }\n}\n\nvoid *TSTree_search_prefix(TSTree *root, const char *key, size_t len)\n{\n    if(len == 0) return NULL;\n\n    TSTree *node = root;\n    TSTree *last = NULL;\n    size_t i = 0;\n\n    while(i < len && node) {\n        if(key[i] < node->splitchar) {\n            node = node->low;\n        } else if(key[i] == node->splitchar) {\n            i++;\n            if(i < len) {\n                if(node->value) last = node;\n                node = node->equal;\n            }\n        } else {\n            node = node->high;\n        }\n    }\n\n    node = node ? node : last;\n\n    // traverse until we find the first value in the equal chain\n    // this is then the first node with this prefix\n    while(node && !node->value) {\n        node = node->equal;\n    }\n\n    return node ? node->value : NULL;\n}\n\nvoid TSTree_traverse(TSTree *node, TSTree_traverse_cb cb, void *data)\n{\n    if(!node) return;\n\n    if(node->low) TSTree_traverse(node->low, cb, data);\n\n    if(node->equal) {\n        TSTree_traverse(node->equal, cb, data);\n    }\n\n    if(node->high) TSTree_traverse(node->high, cb, data);\n\n    if(node->value) cb(node->value, data);\n}\n\nvoid TSTree_destroy(TSTree *node)\n{\n    if(node == NULL) return;\n\n    if(node->low) TSTree_destroy(node->low);\n\n    if(node->equal) {\n        TSTree_destroy(node->equal);\n    }\n\n    if(node->high) TSTree_destroy(node->high);\n\n    free(node);\n}\n```\n\n对于`TSTree_insert`，我使用了相同模式的递归结构，其中我创建了一个小型函数，它调用真正的递归函数。我对此并不做任何检查，但是你应该为之添加通常的防御性编程策略。要记住的一件事，就是它使用了一些不同的设计，这里并没有单独的`TSTree_create`函数，如果你将`node`传入为`NULL`，它会新建一个，然后返回最终的值。\n\n这意味着我需要为你分解`TSTree_insert_base`，使你理解插入操作。\n\ntstree.c:10-18\n\n像我提到的那样，如果函数接收到`NULL`，我需要创建节点，并且将`*key`（当前字符）赋值给它。这用于当我插入键时来构建树。\n\ntstree.c:20-21\n\n当`*key`小于`splitchar`时，选择`low`分支。\n\ntstree.c:22\n\n如果`splitchar`相等，我就要进一步确定等性。这会在我刚刚创建这个节点时发生，所以这里我会构建这棵树。\n\ntstree.c:23-24\n\n仍然有字符串需要处理，所以向下递归`equal`分支，并且移动到下一个`*key`字符。\n\ntstree.c:26-27\n\n这是最后一个字符的情况，所以我将值设置好。我编写了一个`assert`来避免重复。\n\ntstree.c:29-30\n\n最后的情况是`*key`大于`splitchar`，所以我需要向下递归`high`分支。\n\n这个数据结构的`key`实际上带有一些特性，我只会在`splitchar`相等时递增所要分析的字符。其它两种情况我只会继续遍历整个树，直到碰到了相等的字符，我才会递归处理下一个字符。这一操作使它对于找不到键的情况是非常快的。我可以传入一个不存在的键，简单地遍历一些`high`和`low`节点，直到我碰到了末尾并且知道这个键不存在。我并不需要处理键的每个字符，或者树的每个节点。\n\n一旦你理解了这些，之后来分析`TSTree_search`如何工作：\n\ntstree.c:46\n\n我并不需要递归处理整棵树，只需要使用`while`循环和当前的`node`节点。\n\ntstree.c:47-48\n\n如果当前字符小于节点中的`splitchar`，则选择`low`分支。\n\ntstree.c:49-51\n\n如果相等，自增`i`并且选择`equal`分支，只要不是最后一个字符。这就是`if(i < len)`所做的，使我不会越过最后的`value`。\n\ntstree.c:52-53\n\n否则我会选择`high`分支，由于当前字符更大。\n\ntstree.c:57-61\n\n循环结束后如果`node`不为空，那么返回它的`value`，否则返回`NULL`。\n\n这并不难以理解，并且你可以看到`TSTree_search_prefix`函数用了几乎相同的算法。唯一的不同就是我并不试着寻找精确的匹配，而是可找到的最长前缀。我在相等时跟踪`last`节点来实现它，并且在搜索循环结束之后，遍历这个节点直到发现`value`。\n\n观察`TSTree_search_prefix`，你就会开始明白`TSTree`相对`BSTree` 和 `Hashmap`在查找操作上的另一个优点。给定一个长度为X的键，你可以在X步内找到任何键，但是也可以在X步加上额外的N步内找到第一个前缀，取决于匹配的键有多长。如果树中最长的键是十个字符，那么你就可以在10步之内找到任意的前缀。更重要的是，你可以通过对键的每个字符只比较一次来实现。\n\n相比之下，使用`BSTree`执行相同操作，你需要在`BSTree`的每一个可能匹配的节点中检查两个字符串是否有共同的前缀。这对于寻找键，或者检查键是否存在（`TSTree_search`）是相同的。你需要将每个字符与`BSTree`中的大多数字符对比，来确认是否匹配。\n\n`Hashamp`对于寻找前缀更加糟糕，因为你不能够仅仅计算前缀的哈希值。你基本上不能高效在`Hashmap`中实现它，除非数据类似URL可以被解析。即使这样你还是需要遍历`Hashmap`的所有节点。\n\n> 译者注：二叉树和三叉树在搜索时都是走其中的一支，但由于二叉树中每个节点储存字符串，而三叉树储存的是字符。所以三叉树的整个搜索过程相当于一次字符串比较，而二叉树的每个节点都需要一次字符串比较。三叉树堆叠储存字符串使搜索起来更方便。\n\n> 至于哈希表，由于字符串整体和前缀计算出来的哈希值差别很大，所以按前缀搜索时，哈希的优势完全失效，所以只能改为暴力搜索，效果比二叉树还要差。\n\n最后的两个函数应该易于分析，因为它们是典型的遍历和销毁操作，你已经在其它数据结构中看到过了。\n\n最后，我编写了简单的单元测试，来确保我所做的全部东西正确。\n\n```c\n#include \"minunit.h\"\n#include <lcthw/tstree.h>\n#include <string.h>\n#include <assert.h>\n#include <lcthw/bstrlib.h>\n\n\nTSTree *node = NULL;\nchar *valueA = \"VALUEA\";\nchar *valueB = \"VALUEB\";\nchar *value2 = \"VALUE2\";\nchar *value4 = \"VALUE4\";\nchar *reverse = \"VALUER\";\nint traverse_count = 0;\n\nstruct tagbstring test1 = bsStatic(\"TEST\");\nstruct tagbstring test2 = bsStatic(\"TEST2\");\nstruct tagbstring test3 = bsStatic(\"TSET\");\nstruct tagbstring test4 = bsStatic(\"T\");\n\nchar *test_insert()\n{\n    node = TSTree_insert(node, bdata(&test1), blength(&test1), valueA);\n    mu_assert(node != NULL, \"Failed to insert into tst.\");\n\n    node = TSTree_insert(node, bdata(&test2), blength(&test2), value2);\n    mu_assert(node != NULL, \"Failed to insert into tst with second name.\");\n\n    node = TSTree_insert(node, bdata(&test3), blength(&test3), reverse);\n    mu_assert(node != NULL, \"Failed to insert into tst with reverse name.\");\n\n    node = TSTree_insert(node, bdata(&test4), blength(&test4), value4);\n    mu_assert(node != NULL, \"Failed to insert into tst with second name.\");\n\n    return NULL;\n}\n\nchar *test_search_exact()\n{\n    // tst returns the last one inserted\n    void *res = TSTree_search(node, bdata(&test1), blength(&test1));\n    mu_assert(res == valueA, \"Got the wrong value back, should get A not B.\");\n\n    // tst does not find if not exact\n    res = TSTree_search(node, \"TESTNO\", strlen(\"TESTNO\"));\n    mu_assert(res == NULL, \"Should not find anything.\");\n\n    return NULL;\n}\n\nchar *test_search_prefix()\n{\n    void *res = TSTree_search_prefix(node, bdata(&test1), blength(&test1));\n    debug(\"result: %p, expected: %p\", res, valueA);\n    mu_assert(res == valueA, \"Got wrong valueA by prefix.\");\n\n    res = TSTree_search_prefix(node, bdata(&test1), 1);\n    debug(\"result: %p, expected: %p\", res, valueA);\n    mu_assert(res == value4, \"Got wrong value4 for prefix of 1.\");\n\n    res = TSTree_search_prefix(node, \"TE\", strlen(\"TE\"));\n    mu_assert(res != NULL, \"Should find for short prefix.\");\n\n    res = TSTree_search_prefix(node, \"TE--\", strlen(\"TE--\"));\n    mu_assert(res != NULL, \"Should find for partial prefix.\");\n\n\n    return NULL;\n}\n\nvoid TSTree_traverse_test_cb(void *value, void *data)\n{\n    assert(value != NULL && \"Should not get NULL value.\");\n    assert(data == valueA && \"Expecting valueA as the data.\");\n    traverse_count++;\n}\n\nchar *test_traverse()\n{\n    traverse_count = 0;\n    TSTree_traverse(node, TSTree_traverse_test_cb, valueA);\n    debug(\"traverse count is: %d\", traverse_count);\n    mu_assert(traverse_count == 4, \"Didn't find 4 keys.\");\n\n    return NULL;\n}\n\nchar *test_destroy()\n{\n    TSTree_destroy(node);\n\n    return NULL;\n}\n\nchar * all_tests() {\n    mu_suite_start();\n\n    mu_run_test(test_insert);\n    mu_run_test(test_search_exact);\n    mu_run_test(test_search_prefix);\n    mu_run_test(test_traverse);\n    mu_run_test(test_destroy);\n\n    return NULL;\n}\n\nRUN_TESTS(all_tests);\n```\n\n## 优点和缺点\n\n`TSTree`可以用于实现一些其它实用的事情：\n\n+ 除了寻找前缀，你可以反转插入的所有键，之后通过后缀来寻找。我使用它来寻找主机名称，因为我想要找到`*.learncodethehardway.com`，所以如果我反向来寻找，会更快匹配到它们。\n+ 你可以执行“模糊”搜索，其中你可以收集所有与键的大多数字符相似的节点，或者使用其它算法用于搜索近似的匹配。\n+ 你可以寻找所有中间带有特定部分的键。\n\n我已经谈论了`TSTree`能做的一些事情，但是它们并不总是最好的数据结构。`TSTree`的缺点在于：\n\n+ 像我提到过的那样，删除操作非常麻烦。它们适用于需要快速检索并且从不移除的操作。如果你需要删除，可以简单地将`value`置空，之后当树过大时周期性重构它。\n+ 与`BSTree`和`Hashmap`相比，它在相同的键上使用了大量的空间。它对于键中的每个字符都使用了完整的节点。它对于短的键效果更好，但如果你在`TSTree`中放入一大堆东西，它会变得很大。\n+ 它们也不适合处理非常长的键，然而“长”是主观的词，所以应当像通常一样先进行测试。如果你尝试储存一万个字符的键，那么应当使用`Hashmap`。\n\n## 如何改进\n\n像通常一样，浏览代码，使用防御性的先决条件、断言，并且检查每个函数来改进。下面是一些其他的改进方案，但是你并不需要全部实现它们：\n\n+ 你可以使用`DArray`来允许重复的`value`值。\n+ 因为我提到删除非常困难，但是你可以通过将值设为`NULL`来模拟，使值能够高效被删除。\n+ 目前还不能获取到所有匹配指定前缀的值，我会让你在附加题中实现它。\n+ 有一些其他得更复杂的算法会比它要好。查询前缀数组、前缀树和基数树的资料。\n\n## 附加题\n\n+ 实现`TSTree_collect`返回`DArray`包含所有匹配指定前缀的键。\n+ 实现`TSTree_search_suffix`和`TSTree_insert_suffix`，实现后缀搜索和插入。\n+ 使用`valgrind`来查看与`BSTree` 和 `Hashmap`相比，这个结构使用了多少内存来储存数据。\n"
  },
  {
    "path": "docs/lcthw-zh/ex47.md",
    "content": "# 练习47：一个快速的URL路由\n\n> 原文：[Exercise 47: A Fast URL Router](http://c.learncodethehardway.org/book/ex47.html)\n\n> 译者：[飞龙](https://github.com/wizardforcel)\n\n我现在打算向你展示使用`TSTree`来创建服务器中的快速URL路由。它适用于应用中的简单的URL匹配，而不是在许多Web应用框架中的更复杂（一些情况下也不必要）的路由发现功能。\n\n我打算编程一个小型命令行工具和路由交互，他叫做`urlor`，读取简单的路由文件，之后提示用户输入要检索的URL。\n\n```c\n#include <lcthw/tstree.h>\n#include <lcthw/bstrlib.h>\n\nTSTree *add_route_data(TSTree *routes, bstring line)\n{\n    struct bstrList *data = bsplit(line, ' ');\n    check(data->qty == 2, \"Line '%s' does not have 2 columns\",\n            bdata(line));\n\n    routes = TSTree_insert(routes,\n            bdata(data->entry[0]), blength(data->entry[0]),\n            bstrcpy(data->entry[1]));\n\n    bstrListDestroy(data);\n\n    return routes;\n\nerror:\n    return NULL;\n}\n\nTSTree *load_routes(const char *file)\n{\n    TSTree *routes = NULL;\n    bstring line = NULL;\n    FILE *routes_map = NULL;\n\n    routes_map = fopen(file, \"r\");\n    check(routes_map != NULL, \"Failed to open routes: %s\", file);\n\n    while((line = bgets((bNgetc)fgetc, routes_map, '\\n')) != NULL) {\n        check(btrimws(line) == BSTR_OK, \"Failed to trim line.\");\n        routes = add_route_data(routes, line);\n        check(routes != NULL, \"Failed to add route.\");\n        bdestroy(line);\n    }\n\n    fclose(routes_map);\n    return routes;\n\nerror:\n    if(routes_map) fclose(routes_map);\n    if(line) bdestroy(line);\n\n    return NULL;\n}\n\nbstring match_url(TSTree *routes, bstring url)\n{\n    bstring route = TSTree_search(routes, bdata(url), blength(url));\n\n    if(route == NULL) {\n        printf(\"No exact match found, trying prefix.\\n\");\n        route = TSTree_search_prefix(routes, bdata(url), blength(url));\n    }\n\n    return route;\n}\n\nbstring read_line(const char *prompt)\n{\n    printf(\"%s\", prompt);\n\n    bstring result = bgets((bNgetc)fgetc, stdin, '\\n');\n    check_debug(result != NULL, \"stdin closed.\");\n\n    check(btrimws(result) == BSTR_OK, \"Failed to trim.\");\n\n    return result;\n\nerror:\n    return NULL;\n}\n\nvoid bdestroy_cb(void *value, void *ignored)\n{\n    (void)ignored;\n    bdestroy((bstring)value);\n}\n\nvoid destroy_routes(TSTree *routes)\n{\n    TSTree_traverse(routes, bdestroy_cb, NULL);\n    TSTree_destroy(routes);\n}\n\nint main(int argc, char *argv[])\n{\n    bstring url = NULL;\n    bstring route = NULL;\n    check(argc == 2, \"USAGE: urlor <urlfile>\");\n\n    TSTree *routes = load_routes(argv[1]);\n    check(routes != NULL, \"Your route file has an error.\");\n\n    while(1) {\n        url = read_line(\"URL> \");\n        check_debug(url != NULL, \"goodbye.\");\n\n        route = match_url(routes, url);\n\n        if(route) {\n            printf(\"MATCH: %s == %s\\n\", bdata(url), bdata(route));\n        } else {\n            printf(\"FAIL: %s\\n\", bdata(url));\n        }\n\n        bdestroy(url);\n    }\n\n    destroy_routes(routes);\n    return 0;\n\nerror:\n    destroy_routes(routes);\n    return 1;\n}\n```\n\n之后我创建了一个简单的文件，含有一些用于交互的伪造的路由：\n\n```\n/ MainApp /hello Hello /hello/ Hello /signup Signup /logout Logout /album/ Album\n```\n\n## 你会看到什么\n\n一旦你使`urlor`工作，并且创建了路由文件，你可以尝试这样：\n\n```sh\n$ ./bin/urlor urls.txt\nURL> /\nMATCH: / == MainApp\nURL> /hello\nMATCH: /hello == Hello\nURL> /hello/zed  \nNo exact match found, trying prefix.\nMATCH: /hello/zed == Hello\nURL> /album\nNo exact match found, trying prefix.\nMATCH: /album == Album\nURL> /album/12345\nNo exact match found, trying prefix.\nMATCH: /album/12345 == Album\nURL> asdfasfdasfd\nNo exact match found, trying prefix.\nFAIL: asdfasfdasfd\nURL> /asdfasdfasf\nNo exact match found, trying prefix.\nMATCH: /asdfasdfasf == MainApp\nURL>\n$\n```\n\n你可以看到路由系统首先尝试精确匹配，之后如果找不到的话则会尝试前缀匹配。这主要是尝试这二者的不同。根据你的URL的语义，你可能想要之中精确匹配，始终前缀匹配，或者执行二者并选出“最好”的那个。\n\n## 如何改进\n\nURL非常古怪。因为人们想让它们神奇地处理它们的web应用所具有的，所有疯狂的事情，即使不是很合逻辑。在这个对如何将`TSTree`用作路由的简单演示中，它具有一些人们不想要的缺陷。比如，它会把`/al`匹配到`Album`，它是人们通常不想要的。它们想要`/album/*`匹配到`Album`以及`/al`匹配到404错误。\n\n这并不难以实现，因为你可以修改前缀算法来以你想要的任何方式匹配。如果你修改了匹配算法，来寻找所有匹配的前缀，之后选出“最好”的那个，你就可以轻易做到它。这种情况下，`/al`回匹配`MainApp`或者`Album`。获得这些结果之后，就可以执行一些逻辑来决定哪个“最好”。\n\n另一件你能在真正的路由系统里做的事情，就是使用`TSTree`来寻找所有可能的匹配，但是这些匹配是需要检查的一些模式串。在许多web应用中，有一个正则表达式的列表，用于和每个请求的URL进行匹配。匹配所有这些正则表达式非常花时间，所以你可以使用`TSTree`来通过它们的前缀寻找所有可能的结果。于是你就可以缩小模式串的范围，更快速地做尝试。\n\n使用这种方式，你的URL会精确匹配，因为你实际上运行了正则表达式，它们匹配起来更快，因为你通过可能的前缀来查找它们。\n\n这种算法也可用于所有需要用户可视化的灵活路由机制。域名、IP地址、包注册器和目录，文件或者URL。\n\n## 附加题\n\n+ 创建一个实际的引擎，使用`Handler`结构储存应用，而不是仅仅储存应用的字符串。这个结构储存它所绑定的URL，名称和任何需要构建实际路由系统的东西。\n+ 将URL映射到`.so`文件而不是任意的名字，并且使用`dlopen`系统动态加载处理器，并执行它们所包含的回调。将这些回调放进你的`Handler`结构体中，之后你就用C编写了动态回调处理器系统的全部。\n"
  },
  {
    "path": "docs/lcthw-zh/ex5.md",
    "content": "# 练习5：一个C程序的结构\n\n> 原文：[Exercise 5: The Structure Of A C Program](http://c.learncodethehardway.org/book/ex5.html)\n\n> 译者：[飞龙](https://github.com/wizardforcel)\n\n你已经知道了如何使用`printf`，也有了可以随意使用的一些工具，现在让我们逐行分析一个简单的C程序，以便你了解它是如何组织的。在这个程序里你会编写一些不是很熟悉的东西，我会轻松地把它们拆开。之后在后面的几章我们将会处理这些概念。\n\n```c\n#include <stdio.h>\n\n/* This is a comment. */\nint main(int argc, char *argv[])\n{\n    int distance = 100;\n\n    // this is also a comment\n    printf(\"You are %d miles away.\\n\", distance);\n\n    return 0;\n}\n```\n\n手动输入这段代码并运行它，之后确保在`Valgrind`下不出现任何错误。你可能不会这样做，但你得习惯它。\n\n## 你会看到什么\n\n这真是一段无聊的输出，但是这个练习的目的是让你分析代码：\n\n```sh\n$ make ex5\ncc -Wall -g    ex5.c   -o ex5\n$ ./ex5\nYou are 100 miles away.\n$\n```\n\n## 分解代码\n\n当你输出这段代码时，可能你只弄清楚了这段代码中的一小部分C语言特性。让我们快速地逐行分解它，之后我们可以做一些练习来更好地了解每一部分：\n\n　　ex5.c:1\n\n　　这是一个`include`，它是将一个文件的内容导入到这个文件的方式。C具有使用`.h`扩展名作为头文件的惯例。头文件中拥有一些函数的列表，这些都是你想在程序中使用的函数。\n\n　　ex5.c:3\n\n　　这是多行注释，你可以在`/*`和`*/`之间放置任意多行。\n\n　　ex5.c:4\n\n　　这是一个你遇到的更复杂的 `main` 函数。操作系统加载完你的程序，之后会运行叫做`main`的函数，这是C程序的工作方式。这个函数只需要返回`int`，并接受两个参数，一个是`int`作为命令行参数的数量，另一个是`char*`字符串的数组作为命令行参数。这是不是让人难以理解？不用担心，我们稍后会讲解它。\n\n　　ex5.c:5\n\n　　任何函数都以`{`字符开始，它表示“程序块”的开始。在Python中用一个`:`来表示。在其它语言中，可能需要用`begin`或者`do`来表示。\n\n　　ex5.c:6\n\n　　一个变量的声明和同时的赋值。你可以使用语法`type name = value;`来创建变量。在C的语句中，除了逻辑语句，都以一个`;`（分号）来结尾。\n\n　　ex5.c:8\n\n　　注释的另一种形式，它就像Python或Ruby的注释。它以`//`开头，直到行末结束。\n\n　　ex5.c:9\n\n　　调用了我们的老朋友`printf`。就像许多语言中的函数调用，使用语法`name(arg1, arg2);`。函数可以不带任何参数，也可以拥有任何数量的参数。`printf`函数是一类特别的函数，可以带可变数量的参数。我们会在之后说明。\n\n　　ex5.c:11\n\n　　一个`main`函数的返回语句，它会向OS提供退出值。你可能不熟悉Unix软件的返回代码，所以这个也放到后面去讲。\n\n　　ex5.c:12\n\n　　最后，我们以一个闭合的`}`花括号来结束了`main`函数。它就是整个程序的结尾了。\n\n在这次分解中有大量的信息，所以你应该逐行来学习，并且确保至少掌握了背后发生了什么。你不一定了解所有东西，但是在我们继续之前，你可以猜猜它们的意思。\n\n## 附加题\n\n+ 对于每一行，写出你不理解的符号，并且看看是否能猜出它们的意思。在纸上写下你的猜测，你可以在以后检查它，看看是否正确。\n+ 回头去看之前几个练习的源代码，并且像这样分解代码，来看看你是否了解它们。写下你不了解和不能自己解释的东西。\n"
  },
  {
    "path": "docs/lcthw-zh/ex6.md",
    "content": "# 练习6：变量类型\n\n> 原文：[Exercise 6: Types Of Variables](http://c.learncodethehardway.org/book/ex6.html)\n\n> 译者：[飞龙](https://github.com/wizardforcel)\n\n你应该掌握了一个简单的C程序的结构，所以让我们执行下一步简单的操作，声明不同类型的变量。\n\n```c\ninclude <stdio.h>\n\nint main(int argc, char *argv[])\n{\n    int distance = 100;\n    float power = 2.345f;\n    double super_power = 56789.4532;\n    char initial = 'A';\n    char first_name[] = \"Zed\";\n    char last_name[] = \"Shaw\";\n\n    printf(\"You are %d miles away.\\n\", distance);\n    printf(\"You have %f levels of power.\\n\", power);\n    printf(\"You have %f awesome super powers.\\n\", super_power);\n    printf(\"I have an initial %c.\\n\", initial);\n    printf(\"I have a first name %s.\\n\", first_name);\n    printf(\"I have a last name %s.\\n\", last_name);\n    printf(\"My whole name is %s %c. %s.\\n\",\n            first_name, initial, last_name);\n\n    return 0;\n}\n```\n\n在这个程序中我们声明了不同类型的变量，并且使用了不同的`printf`格式化字符串来打印它们。\n\n## 你会看到什么\n\n你的输出应该和我的类似，你可以看到C的格式化字符串相似于Python或其它语言，很长一段时间中都是这样。\n\n```sh\n$ make ex6\ncc -Wall -g    ex6.c   -o ex6\n$ ./ex6\nYou are 100 miles away.\nYou have 2.345000 levels of power.\nYou have 56789.453200 awesome super powers.\nI have an initial A.\nI have a first name Zed.\nI have a last name Shaw.\nMy whole name is Zed A. Shaw.\n$\n```\n\n你可以看到我们拥有一系列的“类型”，它们告诉编译器变量应该表示成什么，之后格式化字符串会匹配不同的类型。下面解释了它们如何匹配：\n\n整数\n\n　　使用`int`声明，使用`%d`来打印。\n\n浮点\n\n　　使用`float`或`double`声明，使用`%f`来打印。\n\n字符\n\n　　使用`char`来声明，以周围带有`'`（单引号）的单个字符来表示，使用`%c`来打印。\n\n字符串（字符数组）\n\n　　使用`char name[]`来声明，以周围带有`\"`的一些字符来表示，使用`%s`来打印。\n\n你会注意到C语言中区分单引号的`char`和双引号的`char[]`或字符串。\n\n> 注\n\n> 当我提及C语言类型时，我通常会使用`char[]`来代替整个的`char SOMENAME[]`。这不是有效的C语言代码，只是一个用于讨论类型的一个简化表达方式。\n\n## 如何使它崩溃\n\n你可以通过向`printf`传递错误的参数来轻易使这个程序崩溃。例如，如果你找到打印我的名字的那行，把`initial`放到`first_name`前面，你就制造了一个bug。执行上述修改编译器就会向你报错，之后运行的时候你可能会得到一个“段错误”，就像这样：\n\n```sh\n$ make ex6\ncc -Wall -g    ex6.c   -o ex6\nex6.c: In function 'main':\nex6.c:19: warning: format '%s' expects type 'char *', but argument 2 has type 'int'\nex6.c:19: warning: format '%c' expects type 'int', but argument 3 has type 'char *'\n$ ./ex6\nYou are 100 miles away.\nYou have 2.345000 levels of power.\nYou have 56789.453125 awesome super powers.\nI have an initial A.\nI have a first name Zed.\nI have a last name Shaw.\nSegmentation fault\n$\n```\n\n在`Valgrind`下运行修改后的程序，来观察它会告诉你什么关于错误“Invalid read of size 1”的事情。\n\n## 附加题\n\n+ 寻找其他通过修改`printf`使这段C代码崩溃的方法。\n+ 搜索“`printf`格式化”，试着使用一些高级的占位符。\n+ 研究可以用几种方法打印数字。尝试以八进制或十六进制打印，或者其它你找到的方法。\n+ 试着打印空字符串，即`\"\"`。\n"
  },
  {
    "path": "docs/lcthw-zh/ex7.md",
    "content": "# 练习7：更多变量和一些算术\n\n> 原文：[Exercise 7: More Variables, Some Math](http://c.learncodethehardway.org/book/ex7.html)\n\n> 译者：[飞龙](https://github.com/wizardforcel)\n\n你可以通过声明`int`，`float`，`char`和`double`类型的变量，来对它们做更多的事情，让我们来熟悉它们吧。接下来我们会在各种数学表达式中使用它们，所以我会向你介绍C的基本算术操作。\n\n```c\nint main(int argc, char *argv[])\n{\n    int bugs = 100;\n    double bug_rate = 1.2;\n\n    printf(\"You have %d bugs at the imaginary rate of %f.\\n\",\n            bugs, bug_rate);\n\n    long universe_of_defects = 1L * 1024L * 1024L * 1024L;\n    printf(\"The entire universe has %ld bugs.\\n\",\n            universe_of_defects);\n\n    double expected_bugs = bugs * bug_rate;\n    printf(\"You are expected to have %f bugs.\\n\",\n            expected_bugs);\n\n    double part_of_universe = expected_bugs / universe_of_defects;\n    printf(\"That is only a %e portion of the universe.\\n\",\n            part_of_universe);\n\n    // this makes no sense, just a demo of something weird\n    char nul_byte = '\\0';\n    int care_percentage = bugs * nul_byte;\n    printf(\"Which means you should care %d%%.\\n\",\n            care_percentage);\n\n    return 0;\n}\n```\n\n下面是这一小段无意义代码背后发生的事情：\n\n　　ex7.c:1-4\n\n　　C程序的通常开始。\n\n　　ex7.c:5-6\n\n　　为一些伪造的bug数据声明了一个`int`和一个`double`变量。\n\n　　ex7.c:8-9\n\n　　打印这两个变量，没有什么新东西。\n\n　　ex7.c:11\n\n　　使用了一个新的类型`long`来声明一个大的数值，它可以储存比较大的数。\n\n　　ex7.c:12-13\n\n　　使用`%ld`打印出这个变量，我们添加了个修饰符到`%d`上面。添加的\"l\"表示将它当作长整形打印。\n\n　　ex7.c:15-17\n\n　　只是更多的算术运算和打印。\n\n　　ex7.c:19-21\n\n　　编撰了一段你的bug率的描述，这里的计算非常不精确。结果非常小，所以我们要使用`%e`以科学记数法的形式打印它。\n\n　　ex7.c:24\n\n　　以特殊的语法`'\\0'`声明了一个字符。这样创建了一个“空字节”字符，实际上是数字0。\n\n　　ex7.c:25\n\n　　使用这个字符乘上bug的数量，它产生了0，作为“有多少是你需要关心的”的结果。这条语句展示了你有时会碰到的丑陋做法。\n\n　　ex7.c:26-27\n\n　　将它打印出来，注意我使用了`%%`（两个百分号）来打印一个`%`字符。\n\n　　ex7.c:28-30\n\n　　`main`函数的结尾。\n\n这一段代码只是个练习，它演示了许多算术运算。在最后，它也展示了许多你能在C中看到，但是其它语言中没有的技巧。对于C来说，一个“字符”同时也是一个整数，虽然它很小，但的确如此。这意味着你可以对它做算术运算，无论是好是坏，许多软件中也是这样做的。\n\n在最后一部分中，你第一次见到C语言是如何直接访问机器的。我们会在后面的章节中深入。\n\n## 你会看到什么\n\n通常，你应该看到如下输出：\n\n```sh\n$ make ex7\ncc -Wall -g    ex7.c   -o ex7\n$ ./ex7\nYou have 100 bugs at the imaginary rate of 1.200000.\nThe entire universe has 1073741824 bugs.\nYou are expected to have 120.000000 bugs.\nThat is only a 1.117587e-07 portion of the universe.\nWhich means you should care 0%.\n$\n```\n\n## 如何使它崩溃\n\n像之前一样，向`printf`传入错误的参数来使它崩溃。对比`%c`，看看当你使用`%s`来打印`nul_byte`变量时会发生什么。做完这些之后，在`Valgrind`下运行它看看关于你的这次尝试会输出什么。\n\n## 附加题\n\n+ 把为`universe_of_defects`赋值的数改为不同的大小，观察编译器的警告。\n+ 这些巨大的数字实际上打印成了什么？\n+ 将`long`改为`unsigned long`，并试着找到对它来说太大的数字。\n+ 上网搜索`unsigned`做了什么。\n+ 试着自己解释（在下个练习之前）为什么`char`可以和`int`相乘。\n"
  },
  {
    "path": "docs/lcthw-zh/ex8.md",
    "content": "# 练习8：大小和数组\n\n> 原文：[Exercise 8: Sizes And Arrays](http://c.learncodethehardway.org/book/ex8.html)\n\n> 译者：[飞龙](https://github.com/wizardforcel)\n\n在上一个练习中你做了一些算术运算，并且使用了`'\\0'`（空）字符。这对于其它语言来说非常奇怪，因为它们把“字符串”和“字节数组”看做不同的东西。但是C中的字符串就是字节数组，并且只有不同的打印函数才知道它们的不同。\n\n在我真正解释其重要性之前，我先要介绍一些概念：`sizeof`和数组。下面是我们将要讨论的一段代码：\n\n```c\n#include <stdio.h>\n\nint main(int argc, char *argv[])\n{\n    int areas[] = {10, 12, 13, 14, 20};\n    char name[] = \"Zed\";\n    char full_name[] = {\n        'Z', 'e', 'd',\n         ' ', 'A', '.', ' ',\n         'S', 'h', 'a', 'w', '\\0'\n    };\n\n    // WARNING: On some systems you may have to change the\n    // %ld in this code to a %u since it will use unsigned ints\n    printf(\"The size of an int: %ld\\n\", sizeof(int));\n    printf(\"The size of areas (int[]): %ld\\n\",\n            sizeof(areas));\n    printf(\"The number of ints in areas: %ld\\n\",\n            sizeof(areas) / sizeof(int));\n    printf(\"The first area is %d, the 2nd %d.\\n\",\n            areas[0], areas[1]);\n\n    printf(\"The size of a char: %ld\\n\", sizeof(char));\n    printf(\"The size of name (char[]): %ld\\n\",\n            sizeof(name));\n    printf(\"The number of chars: %ld\\n\",\n            sizeof(name) / sizeof(char));\n\n    printf(\"The size of full_name (char[]): %ld\\n\",\n            sizeof(full_name));\n    printf(\"The number of chars: %ld\\n\",\n            sizeof(full_name) / sizeof(char));\n\n    printf(\"name=\\\"%s\\\" and full_name=\\\"%s\\\"\\n\",\n            name, full_name);\n\n    return 0;\n}\n```\n\n这段代码中我们创建了一些不同数据类型的数组。由于数组是C语言工作机制的核心，有大量的方法可以用来创建数组。我们暂且使用`type name[] = {initializer};`语法，之后我们会深入研究。这个语法的意思是，“我想要那个类型的数组并且初始化为{..}”。C语言看到它时，会做这些事情：\n\n+ 查看它的类型，以第一个数组为例，它是`int`。\n+ 查看`[]`，看到了没有提供长度。\n+ 查看初始化表达式`{10, 12, 13, 14, 20}`，并且了解你想在数组中存放这5个整数。\n+ 在电脑中开辟出一块空间，可以依次存放这5个整数。\n+ 将数组命名为`areas`，也就是你想要的名字，并且在当前位置给元素赋值。\n\n在`areas`的例子中，我们创建了一个含有5个整数的数组来存放那些数字。当它看到`char name[] = \"Zed\";`时，它会执行相同的步骤。我们先假设它创建了一个含有3个字符的数组，并且把字符赋值给`name`。我们创建的最后一个数组是`full_name`，但是我们用了一个比较麻烦的语法，每次用一个字符将其拼写出来。对C来说，`name`和`full_name`的方法都可以创建字符数组。\n\n在文件的剩余部分，我们使用了`sizeof`关键字来问C语言这些东西占多少个字节。C语言无非是内存块的大小和地址以及在上面执行的操作。它向你提供了`sizeof`便于你理解它们，所以你在使用一个东西之前可以先询问它占多少空间。\n\n这是比较麻烦的地方，所以我们先运行它，之后再解释。\n\n## 你会看到什么\n\n```sh\n$ make ex8\ncc -Wall -g    ex8.c   -o ex8\n$ ./ex8\nThe size of an int: 4\nThe size of areas (int[]): 20\nThe number of ints in areas: 5\nThe first area is 10, the 2nd 12.\nThe size of a char: 1\nThe size of name (char[]): 4\nThe number of chars: 4\nThe size of full_name (char[]): 12\nThe number of chars: 12\nname=\"Zed\" and full_name=\"Zed A. Shaw\"\n$\n```\n\n现在你可以看到这些不同`printf`调用的输出，并且瞥见C语言是如何工作的。你的输出实际上可能会跟我的完全不同，因为你电脑上的整数大小可能会不一样。下面我会过一遍我的输出：\n\n> 译者注：16位机器上的`int`是16位的，不过现在16位机很少见了吧。\n\n　　5\n\n　　我的电脑认为`int`的大小是4个字节。你的电脑上根据位数不同可能会使用不同的大小。\n\n　　6\n\n　　`areas`中含有5个整数，所以我的电脑自然就需要20个字节来储存它。\n\n　　7\n\n　　如果我们把`areas`的大小与`int`的大小相除，我们就会得到元素数量为5。这也符合我们在初始化语句中所写的东西。\n\n　　8\n\n　　接着我们访问了数组，读出`areas[0]`和`areas[1]`，这也意味着C语言的数组下标是0开头的，像Python和Ruby一样。\n\n　　9~11\n\n　　我们对`name`数组执行同样的操作，但是注意到数组的大小有些奇怪，它占4个字节，但是我们用了三个字符来打出\"Zed\"。那么第四个字符是哪儿来的呢？\n\n　　12~13\n\n　　我们对`full_name`数组执行了相同的操作，但它是正常的。\n\n　　13\n\n　　最后我们打印出`name`和`full_name`，根据`printf`证明它们实际上就是“字符串”。\n\n确保你理解了上面这些东西，并且知道这些输出对应哪些创建的变量。后面我们会在它的基础上探索更多关于数组和存储空间的事情。\n\n## 如何使它崩溃\n\n使这个程序崩溃非常容易，只需要尝试下面这些事情：\n\n+ 将`full_name`最后的`'\\0'`去掉，并重新运行它，在`valgrind`下再运行一遍。现在将`full_name`的定义从`main`函数中移到它的上面，尝试在`Valgrind`下运行它来看看是否能得到一些新的错误。有些情况下，你会足够幸运，不会得到任何错误。\n+ 将`areas[0]`改为`areas[10]`并打印，来看看`Valgrind`会输出什么。\n+ 尝试上述操作的不同变式，也对`name`和`full_name`执行一遍。\n\n## 附加题\n\n+ 尝试使用`areas[0] = 100;`以及相似的操作对`areas`的元素赋值。\n+ 尝试对`name`和`full_name`的元素赋值。\n+ 尝试将`areas`的一个元素赋值为`name`中的字符。\n+ 上网搜索在不同的CPU上整数所占的不同大小。\n"
  },
  {
    "path": "docs/lcthw-zh/ex9.md",
    "content": "# 练习9：数组和字符串\n\n> 原文：[Exercise 9: Arrays And Strings](http://c.learncodethehardway.org/book/ex9.html)\n\n> 译者：[飞龙](https://github.com/wizardforcel)\n\n上一个练习中，我们学习了如何创建基本的数组，以及数组如何映射为字符串。这个练习中我们会更加全面地展示数组和字符串的相似之处，并且深入了解更多内存布局的知识。\n\n这个练习向你展示了C只是简单地将字符串储存为字符数组，并且在结尾加上`'\\0'`（空字符）。你可能在上个练习中得到了暗示，因为我们手动这样做了。下面我会通过将它与数字数组比较，用另一种方法更清楚地实现它。\n\n```c\n#include <stdio.h>\n\nint main(int argc, char *argv[])\n{\n    int numbers[4] = {0};\n    char name[4] = {'a'};\n\n    // first, print them out raw\n    printf(\"numbers: %d %d %d %d\\n\",\n            numbers[0], numbers[1],\n            numbers[2], numbers[3]);\n\n    printf(\"name each: %c %c %c %c\\n\",\n            name[0], name[1],\n            name[2], name[3]);\n\n    printf(\"name: %s\\n\", name);\n\n    // setup the numbers\n    numbers[0] = 1;\n    numbers[1] = 2;\n    numbers[2] = 3;\n    numbers[3] = 4;\n\n    // setup the name\n    name[0] = 'Z';\n    name[1] = 'e';\n    name[2] = 'd';\n    name[3] = '\\0';\n\n    // then print them out initialized\n    printf(\"numbers: %d %d %d %d\\n\",\n            numbers[0], numbers[1],\n            numbers[2], numbers[3]);\n\n    printf(\"name each: %c %c %c %c\\n\",\n            name[0], name[1],\n            name[2], name[3]);\n\n    // print the name like a string\n    printf(\"name: %s\\n\", name);\n\n    // another way to use name\n    char *another = \"Zed\";\n\n    printf(\"another: %s\\n\", another);\n\n    printf(\"another each: %c %c %c %c\\n\",\n            another[0], another[1],\n            another[2], another[3]);\n\n    return 0;\n}\n```\n\n在这段代码中，我们创建了一些数组，并对数组元素赋值。在`numbers`中我们设置了一些数字，然而在`names`中我们实际上手动构造了一个字符串。\n\n## 你会看到什么\n\n当你运行这段代码的时候，你应该首先看到所打印的数组的内容初始化为0值，之后打印初始化后的内容：\n\n```sh\n$ make ex9\ncc -Wall -g    ex9.c   -o ex9\n$ ./ex9\nnumbers: 0 0 0 0\nname each: a   \nname: a\nnumbers: 1 2 3 4\nname each: Z e d\nname: Zed\nanother: Zed\nanother each: Z e d\n$\n```\n\n你会注意到这个程序中有一些很有趣的事情：\n\n+ 我并没有提供全部的4个参数来初始化它。这是C的一个简写，如果你只提供了一个元素，剩下的都会为0。\n+ `numbers`的每个元素被打印时，它们都输出0。\n+ `names`的每个元素被打印时，只显示了第一个元素`'a'`，因为`'\\0'`是特殊字符而不会显示。\n+ 然后我们首次打印`names`，打印出了`\"a\"`，因为在初始化表达式中，`'a'`字符之后的空间都用`'\\0'`填充，是以`'\\0'`结尾的有效字符串。\n+ 我们接着通过手动为每个元素赋值来建立数组，并且再次把它打印出来。看看他们发生了什么改变。现在`numbers`已经设置好了，看看`names`字符串如何正确打印出我的名字。\n+ 创建一个字符串也有两种语法：第六行的`char name[4] = {'a'}`，或者第44行的`char *another = \"name\"`。前者不怎么常用，你应该将后者用于字符串字面值。\n\n注意我使用了相同的语法和代码风格来和整数数组和字符数组交互，但是`printf`认为`name`是个字符串。再次强调，这是因为对C语言来说，字符数组和字符串没有什么不同。\n\n最后，当你使用字符串字面值时你应该用`char *another = \"Literal\"`语法，它会产生相同的东西，但是更加符合语言习惯，也更省事。\n\n## 如何使它崩溃\n\nC中所有bug的大多数来源都是忘了预留出足够的空间，或者忘了在字符串末尾加上一个`'\\0'`。事实上，这些bug是非常普遍并且难以改正的，大部分优秀的C代码都不会使用C风格字符串。下一个练习中我们会学到如何彻底避免C风格字符串。\n\n使这个程序崩溃的的关键就是拿掉字符串结尾的`'\\0'`。下面是实现它的一些途径：\n\n+ 删掉`name`的初始化表达式。\n+ 无意中设置`name[3] = 'A'`，于是它就没有终止字符了。\n+ 将初始化表达式设置为`{'a','a','a','a'}`，于是就有过多的`'a'`字符，没有办法给`'\\0'`留出位置。\n\n试着想出一些其它的办法让它崩溃，并且在`Valgrind`下像往常一样运行这个程序，你可以看到具体发生了什么，以及错误叫什么名字。有时`Valgrind`并不能发现你犯的错误，则需要移动声明这些变量的地方看看是否能找出错误。这是C的黑魔法的一部分，有时变量的位置会改变bug。\n\n## 附加题\n\n+ 将一些字符赋给`numbers`的元素，之后用`printf`一次打印一个字符，你会得到什么编译器警告？\n+ 对`names`执行上述的相反操作，把`names`当成`int`数组，并一次打印一个`int`，`Valgrind`会提示什么？\n+ 有多少种其它的方式可以用来打印它？\n+ 如果一个字符数组占四个字节，一个整数也占4个字节，你可以像整数一样使用整个`name`吗？你如何用黑魔法实现它？\n+ 拿出一张纸，将每个数组画成一排方框，之后在纸上画出代码中的操作，看看是否正确。\n+ 将`name`转换成`another`的形式，看看代码是否能正常工作。\n"
  },
  {
    "path": "docs/lcthw-zh/introduction.md",
    "content": "# 导言：C的笛卡尔之梦\n\n> 原文：[Introduction: The Cartesian Dream Of C](http://c.learncodethehardway.org/book/introduction.html)\n\n> 译者：[飞龙](https://github.com/wizardforcel)\n\n&zwj;\n\n> 直到现在，凡是我当作最真实、最可靠而接受的东西，都是从感官或通过感官得来的。不过，我有时觉得这些感官是骗人的，并且为了小心谨慎起见，对于一经骗过我们的东西就决不完全加以信任。\n\n> 勒内·笛卡尔，《第一哲学沉思录》\n\n如果有一段引述用来描述C语言编程的话，那就是它了。对于大多数程序员，C是极其可怕而且邪恶的。他就像是恶魔、撒旦，或者一个使用指针的花言巧语和对机器的直接访问来破坏你生产力的骗子洛基。于是，一旦这位计算界的路西法将你缠住，他就会使用邪恶的“段错误”来毁掉你的世界，并且揭露出与你交易中的骗局而嘲笑你。\n\n然而，C并不应由于这些事实而受到责备。你的电脑和控制它的操作系统才是真正的骗子，而不是朋友。它们通过密谋来向你隐藏它们的真实执行逻辑，使你永远都不真正知道背后发生了什么。C编程语言的失败之处只是向你提供接触背后真正工作原理的途径，并且告诉了你一些难以接受的事实。C会向你展示痛苦的真像（红色药丸），它将幕布拉开来向你展示一些神奇的原理。C即是真理。\n\n既然C如此危险，为什么还要使用它？因为C给了你力量来穿越抽象的假象，并且将你从愚昧中解放出来。\n\n## 你会学到什么\n\n这本书的目的是让你足够熟悉C语言，并能够使用它编写自己的软件，或者修改其他人的代码。这本书的最后，我们会从一本叫做“K&R C”的名著中选取实际的代码，并且用你学过的知识来做代码审查。你需要学习下面这些东西来达到这一阶段：\n\n+ C的基本语法和编写习惯。\n+ 编译，`make`文件和链接。\n+ 寻找和预防bug。\n+ 防御性编程实践。\n+ 使C的代码崩溃。\n+ 编写基本的Unix系统软件。\n\n截至最后一章，你将会有足够的工具来解决基本的系统软件、库和其它小项目。\n\n## 如何阅读本书\n\n这本书为那些已经掌握至少一门编程语言的人而设计。如果你还没有接触过编程，我推荐你先学习[笨办法学Python](http://learnpythonthehardway.org/)，这本书适用于真正的新手并且适合作为第一本编程书。一旦你学会了Python，你可以返回来开始学习这本书。\n\n对于那些已经学会编程的人，这本书的开头可能有些奇怪。它不像其它书一样，那些书中你会阅读一段段的文字然后编写一些代码。相反，这本书中我会让你立即开始编程，之后我会解释你做了什么。这样更有效果，因为你已经经历过的事情解释起来更加容易。\n\n由于采用了这样的结构，下面是本书中你必须遵守的规则：\n\n+ 手动输入所有代码。不要复制粘贴！\n+ 正确地输入所有代码，也包括注释。\n+ 运行代码并保证产生相同的输出。\n+ 如果出现了bug则修正它。\n+ 做附加题时，如果你做不出某道题，马上跳过。\n+ 在寻求帮助之前首先试着自己弄懂。\n\n如果你遵守了这些规则，完成了本书的每一件事，并且还不会编程C代码的话，你至少尝试过了。它并不适用于每个人，但是尝试的过程会让你成为一个更好的程序员。\n\n## 核心能力\n\n我假设你之前使用为“弱者”设计的语言。这些“易用的”语言之一是Python或者Ruby，它们带给了你草率的思维和半吊子的黑魔法。或者，你可能使用类似Lisp的语言，它假设计算机是纯函数式的奇幻大陆，带有一些为婴儿准备的充气墙。再或者你可能学过Prolog，于是你认为整个世界都是一个数据库，你可以从中寻找线索。甚至更糟糕的是，我假设你一直都在用IDE，所以你的大脑布满了内存漏洞，并且你每打三个字符都要按CTRL+空格来打出函数的整个名字。\n\n无论你的背景如何，你都可能不擅长下面四个技能：\n\n阅读和编写\n\n如果你使用IDE这会尤其正确。但是总体上我发现程序员做了很多“略读”，并且在理解上存在问题。它们会略读需要详细理解的代码，并且觉得他们已经理解了但事实上没有。其它语言提供了可以让他们避免实际编写任何代码的工具，所以面对一种类似C的语言时，他们就玩完了。你需要知道每个人都有这个问题，并且你可以通过强迫自己慢下来并且仔细对待阅读和编写代码来改正它。一开始你可能感到痛苦和无聊，但是这样的次数多了它也就变得容易了。\n\n专注细节\n\n每个人都不擅长这方面，它也是劣质软件的罪魁祸首。其它语言让你不会集中注意力，但是C要求你集中全部注意力，因为它直接在机器上运行，并且机器比较挑剔。C中没有“相似的类型”或者“足够接近”，所以你需要注意，再三检查你的代码，并假设你写的任何代码都是错的，直到你能证明它是对的。\n\n定位差异\n\n其它语言程序员的一个关键问题就是他们的大脑被训练来指出那个语言的差异，而不是C。当你对比你的代码和我练习中的代码时，你的眼睛会跳过你认为不重要或者不熟悉的字符。我会给你一些策略来强制你观察你的错误，但是要记住如果你的代码并不完全像书中的代码，它就是错的。\n\n规划和调试\n\n我喜欢其它较简单的语言，因为我可以想怎么写就怎么写。我将已有的想法输入进解释器，然后可以立即看到结果。你可以把你的想法试验出来，但是要注意，如果你仍然打算“试验代码使其能够工作”，它就行不通了。C对于你来说稍困难，因为你需要规划好首先创建什么。的确，你也可以进行试验，但是比起其他语言，你必须在C中更早地严肃对待代码。我会教给你在编程之前规划程序核心部分的方法，这对于使你成为更好的程序员十分有帮助。即使一个很小的规划，都会使接下来的事情变得顺利。\n\n学习C语言会使你变成更好的程序员，因为会强制你更早、更频繁地解决这些问题。你不会再草率地编写半吊子的代码，代码也会能够正常工作。C的优势是，它是一个简单的语言，你可以自己来弄清楚，这使得它成为用于学习机器，以及提升程序员核心技能的最佳语言。\n\nC比其它语言都要难，而这是由于C并不对你隐藏细节，它们在其它语言中都试图并且未能被掩盖。\n\n## 协议\n\n原书在完稿之后可以自由分发，并且能在[亚马逊](http://www.amazon.com/Learn-Hard-Way-Practical-Computational/dp/0321884922/)上购买。该中译版本遵循[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)协议，你可以在保留署名和出处的前提下以非商业目的自由转载。\n"
  },
  {
    "path": "docs/lcthw-zh/postscript.md",
    "content": "# “解构 K&R C” 已死\n\n> 原文：[Deconstructing K&RC Is Dead](http://c.learncodethehardway.org/book/krcritique.html)\n\n> 译者：[飞龙](https://github.com/wizardforcel)\n\n我彻底失败了。我放弃了多年以来尝试理清C语言如何编写的想法，因为它的发明是有缺陷的。起初，我的书中有一章叫做“解构 K&R C”。这一章的目的是告诉人们永远不要假设它们的代码是正确的，或者对于任何人的代码，不管它有多出名，也不能避免缺陷。这看起来似乎并不是革命性的想法，并且对我来说它只是分析代码缺陷和编写更好更可靠代码的一部分。\n\n多年以来，我在写这本书的这一块时收到重挫，并且收到了比任何其它事情更多的批评和侮辱。不仅如此，而且书中这部分的批评以这些话而结束，“你是对的，但是你认为他们的代码很烂这件事是错的。”我不能理解，有一群被认为很聪明的人，他们的大脑中充满理性，却坚持“我可以是错的，但是同时也可以是对的”的观点。我不得不与这些学究在C IRC channels、邮件列表、评论上斗争，这包括每一个它们提出一些怪异的、迂腐的刻薄意见的情况，需要我对我的文章进行更多的逻辑性修改来说服他们。\n\n有趣的一点是，在我写这部分之前，我收到了本书许多正面的评论。当时本书还在写作中，所以我觉得确实需要改进。我甚至设置了一些奖金让人们帮助改进。但可悲的是，一旦他们被自己的英雄蒙蔽，所崇拜的基调就发生了翻天覆地的变化。我变得十分令人讨厌，只不过是尝试教人们如何安全使用一个极易出错的垃圾语言，比如C语言。这是我很擅长的东西。\n\n这些批评者向我承认，他们不写C代码也不教授它，他们只是死记硬背标准库来“帮助”其它人，这对我来说并不重要。我以一个开放的心态试图解决问题，甚至设置奖金给那些有助于修复它的人，这也不重要。这可以使更多的人爱上C语言，并且使其它人入门编程，这更不重要。重要的是我“侮辱”了他们的英雄，这意味着我所说的话永远地完蛋了，没有人会再次相信我。\n\n坦率地说，这是编程文化极为的黑暗、丑陋、邪恶的一面。他们整天在说，“我与你们同在”，但是如果你不屈服于大师们海量的学识，以及乞求他们准许你质疑他们所信奉的东西，你突然就会变成敌人。程序员费尽心机地把自己放在权力的宝座上，来要求别人赞许他们高超的记忆能力，或者对一些微不足道的琐事的熟知，并且会尽全力消灭那些胆敢质疑的人。\n\n这非常恶心，我对此也没什么能做的。我对老程序员无能为力。但他们注定会失败。它们通过标准化记忆所积累的学识，也会在咸鱼的下一次翻身中蒸发掉。它们对考虑如何事物的运作方式，以及如何改进它们，或者将它们的手艺传授给他人毫无兴趣，除非这里面涉及到大量的阿谀奉承并让他们觉得很爽。老程序员总会完蛋的。\n\n他们向现在的年轻程序员施压，我对此并不能做任何事情。我不能阻止无能程序员的诽谤，他们甚至根本不像专业的C程序员那样。然而，我宁愿使本书有助于那些想要学习C语言以及如何编写可靠的软件的人，而不是和那些思维闭锁的保守派做斗争。它们贪图安逸的行为给人一种感觉，就是他们知道更多迂腐的、可怜的小话题，就比如未定义行为。\n\n因此，我删除了书中的K&R C部分，并且找到了新的主题。我打算重写这本书，但是并不知道如何去做。我犹如在地狱中，因为我自己非常执着于我觉得很重要的一些事情，但我不知道如何推进。我现在算是明白了这是错的，因为它阻碍我将一些与C不相关的重要技巧教给许多新的程序员，包括编程规范、代码分析、缺陷和安全漏洞的检测，以及学习其它编程语言的方法。\n\n现在我明白了，我将为这本书制作一些课程，关于编写最安全的C代码，以及将C语言代码打破为一种学习C和编程规范的方式。我会卑微地说我的书只是一个桥梁，所有人应该去读K&R C来迎合这些学究，并且在这些黄金法则的脚下顶礼膜拜。我要澄清我的C版本限制于一个固定的目的之中，因为这让我的代码更安全。我一定会提到所有迂腐的东西，比如每个书呆子式的，关于20世纪60年代的PDP-11电脑上空指针的要求。\n\n之后，我会告诉人们不要再去写别的C程序。这不会很明显，完全不会，但我的目标是将人们从C带到能更好地编程的其它语言中。Go、Rust或者Swift，是我能想到的能处理C语言主要任务新型语言，所以我推荐人们学习它们。我会告诉他们，他们的技能在于发现缺陷，并且对C代码的严格分析将会对所有语言都有巨大的好处，以及使其它语言更易于学习。\n\n但是C呢？C已经死了，它是为想要争论A.6.2章第四段的指针未定义行为的老程序员准备的。谢天谢地，我打算去学习Go（或者Rust，或者Swift，或者其它任何东西）了。\n"
  },
  {
    "path": "docs/lcthw-zh/preface.md",
    "content": "# 前言\n\n> 原文：[Preface](http://c.learncodethehardway.org/book/preface.html)\n\n> 译者：[飞龙](https://github.com/wizardforcel)\n\n这是本书创作中的转储版本，所用的措辞可能不是很好，也可能缺失了一些章节，但是你可以看到我编写这本书的过程，以及我的做事风格。\n\n你也可以随时发送邮件到[help@learncodethehardway.org](mailto:help@learncodethehardway.org)来向我寻求帮助，我通常会在1~2天之内答复。\n\n这个列表是一个讨论列表，并不只允许发布公告，它用于讨论本书和询问问题。\n\n最后，不要忘了我之前写过[笨办法学Python](http://learnpythonthehardway.org/)，如果你还不会编程，你应该先读完它。LCTHW并不面向初学者，而是面向至少读完LPTHW或者已经懂得一门其它编程语言的人。\n\n## 常见问题\n\n这门课程需要多少时间？\n\n你应该花一些时间直到你掌握它，并且每天都要坚持编写代码。一些人花了大约三个月，其它人花了六个月，还有一些人只用了一个星期。\n\n我需要准备什么样的电脑？\n\n你需要OSX或者Linux来完成这本书。\n"
  },
  {
    "path": "docs/learn-cpp-build-game-ue4/00.md",
    "content": "# 零、前言\n\n所以，你想用**虚幻引擎 4** ( **UE4** )来编程自己的游戏。你有很多理由这样做:UE4 很强大——UE4 提供了一些最先进、最美丽、最逼真的照明和物理效果，这是 AAA 工作室使用的那种。\n\nUE4 是设备无关的:为 UE4 编写的代码将在 Windows 桌面机、Mac 桌面机、所有主要的游戏控制台(如果你是官方开发者)、Android 设备和 iOS 设备上工作(在撰写本书时——未来可能会支持更多设备！).所以，你可以用 UE4 写一遍你游戏的主要部分，然后毫无问题地部署到 iOS 和安卓市场。(当然，会有一些小问题:iOS 和安卓的应用内购买和通知必须分开编程，可能还有其他不同。)\n\n# 这本书是给谁的\n\n这本书是给任何想学游戏编程的人看的。我们将通过并创建一个简单的游戏，所以你会对整个过程有一个很好的了解。\n\n这本书也适合任何想学习 C++，尤其是 C++ 17 的人。我们将学习 C++ 的基础知识以及如何在其中编程，并介绍最新 C++ 版本中的一些新特性。\n\n最后，这本书是给任何想学 UE4 的人看的。我们将用它来创造我们的游戏。我们将主要关注 C++ 方面，但我们将关注一些基本的蓝图开发。\n\n# 这本书涵盖了什么\n\n[第 1 章](01.html)、*C++ 17 入门*涵盖了在 Visual Studio Community 2017 或 Xcode 中创建您的第一个 c++ 项目。我们将创建第一个简单的 C++ 程序。\n\n[第 2 章](02.html)、*变量和内存*，涵盖了不同类型的变量，C++ 中存储数据的基本方法，以及控制台应用中的指针、名称空间和基本输入输出。\n\n[第 3 章](03.html)、 *If、Else 和 Switch* 涵盖了 C++ 中的基本逻辑语句，允许您根据变量中的值在代码中进行选择。\n\n[第 4 章](04.html)、*循环*，讲述了如何将一段代码运行一定次数，或者直到某个条件为真。它还涵盖了逻辑运算符，我们将在 UE4 中看到我们的第一个代码示例。\n\n[第 5 章](05.html)、*函数和宏*讲述了我们如何设置可以从代码的其他部分调用的代码部分。我们还介绍了如何传入值或获取返回值，以及一些与变量相关的更高级的主题。\n\n[第 6 章](06.html)、*对象、类和继承*涵盖了 C++ 中的对象，这些对象是将数据成员和成员函数绑定在一起的代码片段，这些代码被称为类或结构。我们将了解封装，以及如何更容易和更有效地对对象进行编程，使它们保持自己的内部状态。\n\n[第 7 章](07.html)、*动态内存分配*，关注动态内存分配以及如何为对象组创建内存空间。本章向您介绍 C 和 C++ 风格的数组和向量。在大多数 UE4 代码中，您将使用 UE4 编辑器内置的集合类。\n\n[第八章](08.html)、*演员和棋子*，深入探讨了如何创建角色并在屏幕上显示，用轴绑定控制你的角色，以及创建和显示可以向 HUD 发布消息的 NPC。\n\n[第 9 章](09.html)、*模板和常用容器*，讲述了如何在 C++ 中使用模板，并讨论了 UE4 和 C++ 标准模板库中都提供的基于模板的数据结构。\n\n[第 10 章](10.html)、*库存系统和取货物品*，是我们为玩家编码设计一个背包存放物品的地方。当用户按下 *I* 键时，我们将显示玩家在背包中携带的物品。我们将学习如何为玩家设置多个拾取项目。\n\n[第十一章](11.html)、*怪物*，看如何添加一个景观。玩家将沿着为他们雕刻的道路行走，然后他们将遇到一支军队。你将学习如何在屏幕上实例化追赶玩家并攻击他们的怪物。\n\n[第 12 章](12.html)*用高级 AI* 打造更聪明的怪物，涵盖了 AI 的基础知识。我们将学习如何使用导航网格、行为树和其他人工智能技术来让你的怪物看起来更聪明。\n\n[第十三章](13.html)、*法术书*，看游戏中如何创造法术来防御自己，以及粒子系统来可视化显示法术。\n\n[第十四章](14.html)、*用 UMG 和音频*改善 UI 反馈，是关于用新的 UMG UI 系统向用户显示游戏信息。我们将使用 UMG 更新您的库存窗口，使其更简单、更美观，我将为您提供创建自己的用户界面的技巧。它还涵盖了如何添加基本音频来增强您的游戏。\n\n[第 15 章](15.html)、*虚拟现实与超越*，概述了 UE4 在 VR、AR、程序编程、加载项以及与不同平台协同工作方面的能力。\n\n# 充分利用这本书\n\n在这本书里，我们不假设你有任何编程背景，所以如果你是一个完全的初学者，那很好！然而，这将有助于你熟悉电脑，以及了解一些基本的游戏概念。当然，如果你想编程游戏，很有可能你至少玩过几个！\n\n我们将运行虚幻引擎 4 和 Visual Studio 2017(如果您在 Mac 上，则为 Xcode)，因此您可能希望确保您运行的是一台最新的、功能相当强大的计算机(如果您想进行 VR，请确保您的计算机已为 VR 做好准备)。\n\n还有，做好工作准备！UE4 使用 C++，你可以很快学会它的基础知识(这里也将会)，但是真正掌握这门语言可能需要很长时间。如果你正在寻找一种快速简单的方法来创建一个游戏，还有其他工具，但如果你真的想学习技能，可能会导致职业编程游戏，这是一个很好的开始！\n\n# 下载示例代码文件\n\n你可以从你在[www.packt.com](http://www.packt.com)的账户下载这本书的示例代码文件。如果您在其他地方购买了这本书，您可以访问[www.packt.com/support](http://www.packt.com/support)并注册将文件直接通过电子邮件发送给您。\n\n您可以按照以下步骤下载代码文件:\n\n1.  登录或注册[www.packt.com](http://www.packt.com)。\n2.  选择“支持”选项卡。\n3.  点击代码下载和勘误表。\n4.  在搜索框中输入图书的名称，并按照屏幕指示进行操作。\n\n下载文件后，请确保使用最新版本的解压缩文件夹:\n\n*   视窗系统的 WinRAR/7-Zip\n*   zipeg/izp/un ARX for MAC\n*   适用于 Linux 的 7-Zip/PeaZip\n\n这本书的代码包也在 GitHub 上托管在[https://GitHub . com/packt publishing/Learning-Cpp-by-Building-Games-with-虚幻-Engine-4-Second Edition](https://github.com/PacktPublishing/Learning-Cpp-by-Building-Games-with-Unreal-Engine-4-Second-Edition)。如果代码有更新，它将在现有的 GitHub 存储库中更新。\n\n我们还有来自丰富的图书和视频目录的其他代码包，可在**[【https://github.com/PacktPublishing/】](https://github.com/PacktPublishing/)**获得。看看他们！\n\n# 下载彩色图像\n\n我们还提供了一个 PDF 文件，其中包含本书中使用的截图/图表的彩色图像。可以在这里下载:[https://www . packtpub . com/sites/default/files/downloads/9781788476249 _ color images . pdf](https://www.packtpub.com/sites/default/files/downloads/9781788476249_ColorImages.pdf)。\n\n# 使用的约定\n\n本书通篇使用了许多文本约定。\n\n`CodeInText`:表示文本中的码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟网址、用户输入和推特句柄。这里有一个例子:“我们首先看到的是一个`#include`语句。我们要求 C++ 复制并粘贴另一个名为`<iostream>`的 C++ 源文件的内容\n\n代码块设置如下:\n\n```cpp\n#include <iostream>\nusing namespace std;  \nint main() \n{ \n  cout << \"Hello, world\" << endl; \n  cout << \"I am now a C++ programmer.\" << endl; \n  return 0;\n} \n```\n\n当我们希望将您的注意力吸引到代码块的特定部分时，相关的行或项目以粗体显示:\n\n```cpp\nstring name; \nint goldPieces; \nfloat hp; \n```\n\n**粗体**:表示一个新的术语、一个重要的单词或者你在屏幕上看到的单词。例如，菜单或对话框中的单词像这样出现在文本中。这里有一个例子:“打开史诗游戏启动器应用。选择启动虚幻引擎 4.20.X。”\n\nWarnings or important notes appear like this. Tips and tricks appear like this.\n\n# 取得联系\n\n我们随时欢迎读者的反馈。\n\n**一般反馈**:如果你对这本书的任何方面有疑问，在你的信息主题中提到书名，发邮件给我们`customercare@packtpub.com`。\n\n**勘误表**:虽然我们已经尽了最大的努力来保证内容的准确性，但是错误还是会发生。如果你在这本书里发现了一个错误，如果你能向我们报告，我们将不胜感激。请访问[www.packt.com/submit-errata](http://www.packt.com/submit-errata)，选择您的图书，点击勘误表提交链接，并输入详细信息。\n\n**盗版**:如果您在互联网上遇到任何形式的我们作品的非法拷贝，如果您能提供我们的位置地址或网站名称，我们将不胜感激。请通过`copyright@packt.com`联系我们，并提供材料链接。\n\n**如果你有兴趣成为一名作者**:如果有一个你有专长的话题，你有兴趣写或者投稿一本书，请访问[authors.packtpub.com](http://authors.packtpub.com/)。\n\n# 复习\n\n请留下评论。一旦你阅读并使用了这本书，为什么不在你购买它的网站上留下评论呢？然后，潜在的读者可以看到并使用您不带偏见的意见来做出购买决定，我们在 Packt 可以了解您对我们产品的看法，我们的作者可以看到您对他们的书的反馈。谢谢大家！\n\n更多关于 Packt 的信息，请访问[packt.com](http://www.packt.com/)。"
  },
  {
    "path": "docs/learn-cpp-build-game-ue4/01.md",
    "content": "# 一、C++ 17 入门\n\n学者们经常在理论上描述编程概念，但喜欢把实现留给其他人，最好是业内人士。在这本书里，我们涵盖了所有内容:我们将描述 C++ 概念背后的理论，并实现我们自己的游戏。如果你是第一次做程序员，你有很多东西要学！\n\n我建议的第一件事是你做练习。你不能仅仅通过阅读来学习编程。你必须在练习中应用理论来吸收它，并能够在将来使用它。\n\n我们将从用 C++ 编写非常简单的程序开始。我知道你想现在就开始玩你的游戏。然而，你必须从头开始才能到达那个终点(如果你真的想，跳到[第 13 章](13.html)**法术书*，或者打开一些样本来感受一下我们要去的地方)。*\n\n *在本章中，我们将涵盖以下主题:\n\n*   设置新项目(在 Visual Studio 或 Xcode 中)\n*   你的第一个 C++ 项目\n*   如何处理错误\n*   什么是构建和编译？\n\n# 建立我们的项目\n\n我们的第一个 C++ 程序将在 UE4 之外编写。首先，我将提供 Xcode 和 Visual Studio 2017 的步骤，但是在这一章之后，我将尝试只讨论 C++ 代码，而不涉及您是使用微软 Windows 还是 macOS。\n\n# 在视窗系统上使用微软视觉工作室\n\n在这一部分，我们将安装一个**集成开发环境** ( **IDE** )，允许你为微软的 Visual Studio Windows 编辑代码。如果您使用的是苹果电脑，请跳到下一部分。\n\n# 下载和安装 Visual Studio\n\n要启动，请下载并安装 Microsoft Visual Studio Community 2017。\n\nThe Community edition of Visual Studio is the free version of Visual Studio that Microsoft provides on their website. Go to [https://www.visualstudio.com/downloads/](http://www.visualstudio.com/en-us/products/visual-studio-express-vs.aspx) to download and then start the installation process.\n\n你可以在这里找到完整的安装说明:[https://docs . Microsoft . com/en-us/visualstudio/install/install-visual-studio？view=vs-2017](https://docs.microsoft.com/en-us/visualstudio/install/install-visual-studio?view=vs-2017) 。当您进入工作负载部分时，您会想要选择 C++ 桌面开发。\n\n一旦有了 Visual Studio Community 2017，请打开它。这是软件图标的外观:\n\n![](img/265876f5-f04d-466c-95f3-f8a59af9665b.png)\n\n# 在 Visual Studio 中启动新项目\n\n通过以下步骤，您可以真正键入代码:\n\n1.  从“文件”菜单中，选择“新建|项目”...，如下图所示:\n\n![](img/669cecd1-75e3-4245-8948-c4b972d1887e.png)\n\n2.  您将看到以下对话框:\n\n![](img/c3b51d0a-eedc-44d2-95c5-322e25267b91.png)\n\nNote that there is a small box at the bottom with the text Solution name. In general, Visual Studio Solutions might contain many projects. However, this book only works with a single project, but at times you might find it useful to integrate many projects into the same solution.\n\n3.  现在有五件事需要处理，如下所示:\n    1.  从左侧面板中选择联机|模板| Visual C++\n    2.  从右侧面板中选择控制台应用(通用)项目模板\n    3.  说出你的应用(我用了`MyFirstApp`)\n    4.  选择保存代码的文件夹\n    5.  点击确定按钮\n4.  如果您以前从未使用过此模板，它将打开 VSIX 安装程序并显示此对话框:\n\n![](img/0b03f1e7-944b-4a00-ba2f-f5116cbb77dc.png)\n\n5.  单击修改。它将安装并关闭 Visual Studio。如果出现此对话框，您可能需要单击结束任务:\n\n![](img/e023a532-033d-4c40-bd80-49bb437d461c.png)\n\n6.  然后，它会为您安装项目模板。这需要很长时间，但你应该只需要做一次。完成后，单击关闭并重新启动 Visual Studio。\n7.  您需要从文件|新建|项目中重新开始前面的步骤....这一次，Visual C++ 将显示在“已安装:\n\n![](img/54cb942c-bc59-40da-85f6-81f31104ea0a.png)\n\n8.  选择空项目，您可以将项目 1 的名称更改为您想要的任何名称，在我的例子中是 MyFirstApp。\n\n现在，您处于 Visual Studio 2017 环境中。这是你将完成所有工作和代码的地方。\n\n然而，我们需要一个文件来编写代码。因此，我们将通过在解决方案资源管理器中右键单击项目名称并选择添加| **新项**，向我们的项目添加一个 C++ 代码文件，如下图所示:\n\n![](img/92b4196f-85b6-4a5f-baa0-5786a1603754.png)\n\n添加新的 C++ ( `.cpp`)源代码文件，如下图截图所示:\n\n![](img/526bf3b2-f0ca-4ac1-a65f-66e8ab1f2fe3.png)\n\n`Source.cpp`现在已经打开，可以添加代码了。跳到*创建你的第一个 C++ 程序*部分开始。\n\n# 在苹果电脑上使用 Xcode\n\n在本节中，我们将讨论如何在 Mac 上安装 Xcode。如果您使用的是 Windows，请跳到下一部分。\n\n# 下载并安装 Xcode\n\nXcode 可用(免费！)在苹果应用商店的所有苹果电脑上。\n\n如果可能的话，你应该得到最新版本。截至本文撰写之时，它是 Xcode 10，但它至少需要 macOS Sierra 或(最好)High Sierra。如果你的苹果电脑比较旧，运行的是旧的操作系统，你可以免费下载操作系统更新，只要你使用的机器足够新，可以支持它。\n\n只需在苹果应用商店搜索 Xcode，如下所示:\n\n![](img/bd16b214-46c1-44d2-9d00-c308094aa746.png)\n\n只需点击获取按钮，等待它下载并安装。\n\n# 在 Xcode 中启动新项目\n\n1.  一旦你安装了 Xcode，打开它。然后，从打开的闪屏中选择创建新的 Xcode 项目，或者导航到文件|新建|项目...从屏幕顶部的系统菜单栏中，如下图所示:\n\n![](img/b711f1db-82e5-4bad-9263-468d15c9d89d.png)\n\n2.  在“新建项目”对话框中，在屏幕顶部 macOS 下的“应用”部分，选择“命令行工具”。然后，点击下一步:\n\n![](img/6719fad2-0b15-4df3-9733-92d228725f81.png)\n\n3.  在下一个对话框中，命名您的项目。请务必填写所有字段，否则 Xcode 不会让您继续。确保项目的类型设置为 C++，然后单击“下一步”按钮，如下所示:\n\n![](img/96c9388b-1d41-450e-a571-77acbf282617.png)\n\n4.  下一个弹出窗口将要求您选择一个位置来保存您的项目。在你的硬盘上选一个位置，保存在那里。默认情况下，Xcode 为您创建的每个项目创建一个 Git 存储库。您可以取消选中创建 git 存储库，因为我们不会在本章中介绍 Git，如下图所示:\n\n![](img/99a90ba0-9ea9-4dd6-a96b-d250c142808d.png)\n\nGit is a **version control system**. This basically means that Git takes and keeps snapshots of all the code in your project every so often (every time you *commit* to the repository). Other popular **source control management** (**SCM**) tools are Mercurial, Perforce, and Subversion. When multiple people are collaborating on the same project, the scm tool has the ability to automatically merge and copy other people's changes from the repository to your local code base.\n\n好的。你们都准备好了。点击 Xcode 左侧面板中的`main.cpp`文件。如果文件没有出现，请确保首先选择左侧面板顶部的文件夹图标，如下图所示:\n\n![](img/2d168111-aab2-4b3b-a1c9-61328c4b675f.png)\n\n# 创建您的第一个 C++ 程序\n\n我们现在要写一些 C++ 源代码。我们称之为源代码有一个很好的理由:它是构建二进制可执行代码的来源。相同的 C++ 源代码可以构建在不同的平台上，如 Mac、Windows 和移动平台，理论上，在每个平台上做完全相同的事情的可执行代码应该会产生结果。\n\n在不远的过去，在引入 C 和 C++ 之前，程序员为他们分别瞄准的每台特定机器编写代码。他们用一种叫做汇编语言的语言编写代码。但是现在，有了 C 和 C++ 可用，程序员只需编写一次代码，只需使用不同的编译器构建相同的源代码，就可以将其部署到许多不同的机器上。\n\nIn practice, there are some differences between Visual Studio's flavor of C++ and Xcode's flavor of C++, but these differences mostly appear when working with advanced C++ concepts, such as templates. UE4 is very helpful when working with multiple platforms.\n\nEpic Games 投入了大量的工作，以便让相同的代码在 Windows 和 Mac 上工作，以及许多其他平台，如移动和游戏控制台。\n\nA real-world tip\nIt is important for the code to run in the same way on all machines, especially for networked games or games that allow things such as shareable replays. This can be achieved using standards. For example, the IEEE floating-point standard is used to implement decimal math on all C++ compilers. This means that the result of computations such as 200 * 3.14159 should be the same on all machines. Without standards, different compilers might (for example) round numbers differently, and where there are many calculations and the code needs to be precise, this could cause unacceptable differences.\n\n在 Microsoft Visual Studio 或 Xcode 中编写以下代码:\n\n```cpp\n#include <iostream>\nusing namespace std;  \nint main() \n{ \n  cout << \"Hello, world\" << endl; \n  cout << \"I am now a C++ programmer.\" << endl; \n  return 0;\n} \n```\n\n为了解释发生了什么，这里是相同的代码，但添加了注释(同一行中`//`之后的任何内容都将被编译器忽略，但可以帮助解释发生了什么)。\n\n```cpp\n#include <iostream>  // Import the input-output library \nusing namespace std; // allows us to write cout \n                     // instead of std::cout \nint main() \n{ \n  cout << \"Hello, world\" << endl; \n  cout << \"I am now a C++ programmer.\" << endl; \n  return 0;      // \"return\" to the operating sys \n} \n```\n\n在 Visual Studio 中按 *Ctrl* + *F5* (或使用调试|开始不调试菜单)运行前面的代码，或按*命令* + *R* (产品|运行)在 Xcode 中运行。第一次在 Visual Studio 中按 *Ctrl* + *F5* ，会看到这个对话框:\n\n![](img/fa80783c-d71f-4fb8-8804-cf43911cff55.png)\n\n如果您不想在每次运行程序时看到此对话框，请选择“是”并不再显示此对话框。\n\n以下是您应该在窗口中看到的内容:\n\n![](img/0c3809e7-173f-4e56-b212-7e3688928c01.png)\n\n这是在苹果电脑上:\n\n![](img/1442d579-e93d-420a-80a0-be030bfef8ad.png)\n\nIf you're on Windows, you will probably notice that the window closes automatically when you run it so you can't see the results. There are various ways around this, including changing the settings to pause and make you press a key to continue. You can get more information here: [https://stackoverflow.com/questions/454681/how-to-keep-the-console-window-open-in-visual-c/1152873#1152873](https://stackoverflow.com/questions/454681/how-to-keep-the-console-window-open-in-visual-c/1152873#1152873)\n\n你可能想到的第一件事是“我的！一大堆废话！”\n\n事实上，在正常的英语文本中，你很少看到散列(#)符号(除非你使用推特)和大括号对`{` `}`的使用。然而，在 C++ 代码中，这些奇怪的符号比比皆是。你只需要习惯它们。\n\n那么，我们来解读一下这个节目，从第一行开始。\n\n这是节目的第一行:\n\n```cpp\n#include <iostream>  // Import the input-output library \n```\n\n这条线有两点需要注意:\n\n1.  我们首先看到的是一个`#include`语句。我们要求 C++ 将另一个名为`<iostream>`的 C++ 源文件的内容直接复制粘贴到我们的代码文件中。`<iostream>`是一个标准的 C++ 库，处理所有让我们在屏幕上打印文本的代码。\n2.  我们注意到的第二件事是`//`评论。如前所述，C++ 会忽略双斜线(`//`)之后的任何文本，直到该行结束。注释对于在纯文本解释中添加一些代码的功能非常有用。您可能还会在源代码中看到`/* */`多行 C 风格的注释。用斜杠星`/*`和斜杠星`*/`包围 C 或 C++ 中的任何文本(甚至是多行文本)会给出一条指令，要求编译器删除该代码。\n\n这是下一行代码:\n\n```cpp\nusing namespace std; // allows us to write cout \n                     // instead of std::cout \n```\n\n这一行旁边的注释解释了`using`语句的作用:它只是让你使用一个简写(例如，`cout`)来代替我们的很多 C++ 代码命令的完全限定名(在这种情况下，应该是`std::cout`)。有些人不喜欢`using namespace std;`的说法；每当他们想使用`cout`的时候，他们更喜欢用长字书写`std::cout`。像这样的事情你会陷入长时间的争论。在本文的这一部分，我们更喜欢`using namespace` `std;`语句的简洁。\n\n此外，请注意，本节第二行的注释与上一行的注释是一致的。这是很好的编程实践，因为它直观地显示了它是前面注释的延续。\n\n这是下一行:\n\n```cpp\nint main() \n```\n\n这是应用的起点。你可以把`main`想象成一场比赛的起跑线。`int main()`语句是你的 C++ 程序如何知道从哪里开始。\n\n如果你没有`int main()`程序标记或者`main`拼写错误，那么你的程序就不会运行，因为程序不知道从哪里开始。\n\n下一行是您不常看到的字符:\n\n```cpp\n{ \n```\n\n这个`{`人物不是侧身小胡子。它被称为大括号，表示程序的起点。\n\n下面两行将文本打印到屏幕上:\n\n```cpp\ncout << \"Hello, world\" << endl; \ncout << \"I am now a C++ programmer.\" << endl; \n```\n\n`cout`语句代表控制台输出。双引号之间的文本将像出现在引号之间一样输出到控制台。除了双引号之外，您可以在双引号之间写任何您想要的内容，它仍然是有效的代码。另外，注意`endl`告诉`cout`添加一个结束行(回车)字符，这对格式化非常有用。\n\nTo enter a double quote between double quotes, you need to stick a backslash () in front of the double quote character that you want inside the string, as shown here:\n\n```cpp\ncout << \"John shouted into the cave \\\"Hello!\\\" The cave echoed.\"  \n```\n\n`\\\"`符号是转义序列的一个例子。您可以使用其他转义序列；你会发现最常见的转义序列是`\\n`，用于将文本输出跳转到下一行。\n\n程序的最后一行是`return`语句:\n\n```cpp\nreturn 0; \n```\n\n这一行代码表明 C++ 程序正在退出。你可以把`return`语句想象成返回操作系统。\n\n最后，您的程序的结尾由右花括号表示，它是一个面向相反方向的侧髭:\n\n```cpp\n} \n```\n\n# 分号\n\n分号(；)在 C++ 编程中很重要。请注意，在前面的代码示例中，大多数代码行都以分号结尾。如果你没有用分号结束每一行，你的代码将不会编译，如果发生这种情况，你的雇主不会很高兴(当然，一旦你这样做了一段时间，你会发现并解决这些问题，甚至在他们发现之前)。\n\n# 处理错误\n\n如果您在输入代码时出错，那么您将会遇到语法错误。面对语法错误，C++ 会尖叫血腥谋杀，你的程序甚至不会编译；而且，它不会运行。\n\n让我们试着在之前的 C++ 代码中插入几个错误:\n\n![](img/3fca0dff-5690-4455-8896-87da9e078a6e.png)\n\nWarning! This code listing contains errors. It is a good exercise to find all the errors and fix them!\n\n作为练习，试着找出并修复这个程序中的所有错误。\n\nNote that if you are extremely new to C++, this might be a hard exercise. However, this will show you how careful you need to be when writing C++ code.\n\n修复编译错误可能是一件令人讨厌的事情。但是，如果您将此程序的文本输入到代码编辑器中并尝试编译它，它将导致编译器向您报告所有错误。一次修复一个错误，然后尝试重新编译(从列表中的第一个开始，因为它可能会导致后面的一些错误)。将弹出一个新的错误，或者程序将正常工作，如下图所示:\n\n![](img/5e745745-442b-422b-8382-56d4f5f364e3.png)\n\n当您尝试编译代码时，编译器会向您显示代码中的错误(尽管如果您使用的是 Visual Studio，它会询问您是否要先运行先前成功的构建)。\n\n我向您展示这个示例程序的原因是，只要您是 C++ 新手，就应该鼓励以下工作流:\n\n1.  总是从一个有效的 C++ 代码示例开始。你可以从你的第一个 C++ 程序部分*创建* *中分叉出一堆新的 C++ 程序。*\n2.  分几步修改代码。当你是新用户时，在写完每一行新代码后进行编译。不要花一到两个小时编写代码，然后一次编译所有新代码。\n3.  你可以期待几个月之后，你才能写出第一次编写时就能达到预期性能的代码。不要气馁。学习编码很有趣。\n\n# C++ 中的警告\n\n编译器会标记它认为可能是错误的东西。这些是另一类被称为警告的编译器通知。警告是代码中的问题，您不必修复这些问题就可以运行代码，只需建议编译器修复即可。警告通常表明代码并不完美，在代码中修复警告通常被认为是一种好的做法。\n\n然而，并不是所有的警告都会在您的代码中引起问题。一些程序员更喜欢禁用他们不认为是问题的警告(例如，警告 4018 警告不要出现指定/无符号不匹配，这很可能会在后面看到)。\n\n# 什么是构建和编译？\n\n你可能听说过一个叫做编译的计算机过程术语。编译是将 C++ 程序转换成可以在 CPU 上运行的代码的过程。构建你的源代码意味着编译它。\n\n看，你的源`code.cpp`文件实际上不会在电脑上运行。必须先编译它才能运行。\n\n这就是使用微软 Visual Studio 社区或 Xcode 的全部意义。Visual Studio 和 Xcode 都是编译器。您可以在任何文本编辑程序中编写 C++ 源代码，甚至可以在记事本中编写。但是你需要一个编译器在你的机器上运行它。\n\n每个操作系统通常都有一个或多个 C++ 编译器，它们可以编译 C++ 代码以在该平台上运行。在 Windows 上，您有 Visual Studio 和英特尔 C++ Studio 编译器。在 Mac 上，有 Xcode，在所有的 Windows、Mac 和 Linux 上，都有 **GNU 编译器集合** ( **GCC** )。\n\n我们编写的相同 C++ 代码(源代码)可以使用不同的编译器针对不同的操作系统进行编译，理论上它们应该会产生相同的结果。在不同平台上编译相同代码的能力称为可移植性。总的来说，便携性是一件好事。\n\n# 示例输出\n\n这是你第一个 C++ 程序的截图:\n\n![](img/65ae7f80-3457-4436-acbc-94b35c03c77f.png)\n\n下面的截图是它的输出，你的第一次胜利:\n\n![](img/921550c8-8ed5-4817-a15b-fbe91ac1bd6e.png)\n\nThere is another class of programming languages called scripting languages. These include languages such as PHP, Python, and `ActionScript.` Scripted languages are not compiled; for JavaScript, PHP, and ActionScript, there is no compilation step. Rather, they are interpreted from the source as the program is run. The good thing about scripting languages is that they are usually platform-independent from the word go, because interpreters are very carefully designed to be platform-independent.\n\n# 练习- ASCII 艺术\n\n游戏程序员喜欢 ASCII 艺术，你可以只用字符画一幅画。下面是一个 ASCII 艺术迷宫的例子:\n\n```cpp\ncout << \"****************\" << endl; \ncout << \"*............*.*\" << endl; \ncout << \"*.*.*******..*.*\" << endl; \ncout << \"*.*.*..........*\" << endl; \ncout << \"*.*.*.**********\" << endl; \ncout << \"***.***........*\" << endl; \n```\n\n用 C++ 代码构建自己的迷宫或者用字符画一幅画。\n\n# 摘要\n\n总而言之，我们学习了如何在集成开发环境(IDE、Visual Studio 或 Xcode)中用 C++ 编程语言编写我们的第一个程序。这是一个简单的程序，但是你应该把编译和运行你的第一个程序算作你的第一次胜利。在接下来的章节中，我们将把更复杂的程序放在一起，并开始为我们的游戏使用虚幻引擎。*"
  },
  {
    "path": "docs/learn-cpp-build-game-ue4/02.md",
    "content": "# 二、变量和内存\n\n要编写你的 C++ 游戏程序，你将需要你的电脑记住很多东西，比如玩家在世界的什么地方，他们有多少命中点，他们还剩多少弹药，物品在世界的什么地方，他们提供什么加电，以及组成玩家屏幕名称的字母。\n\n你的电脑里面其实有一种叫做**内存***或者内存的电子画板。从物理上讲，计算机内存是由硅制成的，它看起来类似于下图所示:*\n\n *![](img/50811780-25ce-481b-a8d4-3f90d20df7fc.png)\n\n这个内存看起来像停车场吗？因为这是我们要用的比喻。\n\n随机存取存储器是随机存取存储器的简称。它被称为随机访问，因为你可以随时访问它的任何部分。如果你身边还有一些光盘，它们是非随机存取的一个例子。光盘应该按顺序阅读和播放。我还记得在迈克尔·杰克逊的*危险*专辑中跳跃曲目的那段时间，当时在 CD 上切换曲目要花很多时间！然而，跳来跳去并访问不同的内存单元根本不需要太多时间。随机存取存储器是一种被称为闪存的快速存储器。\n\n内存被称为易失性闪存，因为当计算机关闭时，内存的内容会被清除，内存的旧内容会丢失，除非它们先保存到硬盘上。\n\n为了永久存储，您必须将数据保存到硬盘上。硬盘主要有两种类型:\n\n*   基于盘片的**硬盘驱动器** ( **硬盘**)\n*   **固态硬盘** ( **固态硬盘**)\n\n固态硬盘比基于盘片的硬盘更现代，因为它们使用随机存取存储器的快速存取(闪存)原理。然而，与内存不同，固态硬盘上的数据在计算机关闭后仍然存在。如果你能得到一个固态硬盘，我强烈建议你使用它！基于盘片的驱动器已经过时。\n\n当程序运行时，访问存储在内存中的数据比从硬盘或固态硬盘访问要快得多，因此我们需要一种方法在内存中保留空间并从中读写。幸运的是，C++ 使这变得容易。\n\n# 变量\n\n计算机内存中我们可以读取或写入的保存位置称为**变量**。\n\n变量是其值可以变化的组件。在计算机程序中，你可以把变量想象成一个容器，你可以在里面存储一些数据。在 C++ 中，这些数据容器(变量)有类型，以及可以用来引用它们的名称。您必须使用正确类型的数据容器将数据保存在程序中。\n\n如果要保存一个整数，如 1、0 或 20，将使用`int`类型的容器。您可以使用浮点型容器来携带浮点(十进制)值，例如 38.87，也可以使用字符串变量来携带字符串(可以将其想象为一串*珍珠*，其中每个字母都是一颗珍珠)。\n\n你可以把你在内存中预留的位置想象成在停车场里预留一个停车位:一旦我们声明了我们的变量并为它获得了一个位置，操作系统就不会给任何其他人(甚至是在同一台机器上运行的其他程序)这块内存。变量旁边的内存可能没有使用，或者可能被其他程序使用。\n\nThe operating system exists to keep programs from stepping on each other's toes and accessing the same bits of computer hardware at the same time. In general, civil computer programs should not read or write to each other's memory. However, some types of cheat programs (for example, maphacks) secretly access your program's memory. Programs such as PunkBuster were introduced to prevent cheating in online games.\n\n# 声明变量–触摸硅片\n\n使用 C++ 在计算机内存中保留一个位置很容易。我们想用一个好的、描述性的名字来命名我们将要存储数据的内存块。\n\n例如，假设我们知道玩家**的命中点** ( **hp** )将是一个整数(整数)，例如 1、2、3 或 100。要获得一块硅来将玩家的`hp`存储在内存中，我们将声明下面一行代码:\n\n```cpp\nint hp;     // declare variable to store the player's hp \n```\n\n这一行代码保留了一小块内存来存储一个名为`hp`的整数(`int`是整数的缩写)。以下是我们用来存储玩家的`hp`的内存块的例子。这在内存中为我们预留了一个停车位(在所有其他停车位中)，我们可以通过它的标签(`hp`)来引用内存中的这个空间:\n\n![](img/43575241-0eb0-41bd-b8e3-0e5d9ce0182b.png)\n\n在内存中的所有其他空间中，我们有一个位置来存储 hp 数据。\n\nWhen you name a variable, there are a few rules. Variable names can't start with a number, and there are certain \"reserved words\" the compiler won't let you use (usually because they are used by C++ itself). You will learn these as you learn more C++, or you can look for lists of reserved words online.\n\n请注意，如果变量空间是一个双精度或不同类型变量的空间，那么在该图中变量空间是如何被类型标记为`int`的。C++ 记住你在内存中为你的程序保留的空间，不仅仅是名字，还有变量的类型。\n\n请注意，我们还没有在惠普的盒子里放任何东西！我们稍后会这样做——现在`hp`变量的值没有设置，所以它将具有前一个占用者留在那个停车位上的值(可能是另一个程序留下的值)。告诉 C++ 变量的类型很重要！稍后，我们将声明一个变量来存储十进制值，例如 3.75。\n\n# 对你记忆中保留的位置进行读写\n\n将值写入内存很容易！一旦你有了一个`hp`变量，你只需要用`=`符号来写:\n\n```cpp\nhp = 500; \n```\n\n瞧啊。玩家有 500 点生命值。\n\n读取变量同样简单。要打印出变量值，只需输入以下内容:\n\n```cpp\ncout << hp << endl; \n```\n\n这将打印存储在`hp`变量中的值。`cout`对象足够聪明，可以算出它是什么类型的变量，并打印内容。如果您更改`hp`的值，然后再次使用`cout`，将打印最新的值，如下所示:\n\n```cpp\nhp = 1200; \ncout << hp << endl; // now shows 1200 \n```\n\n# 数字和数学\n\n标题说明了一切；在这一节中，我们将深入探讨 C++ 中数字和数学的重要性。\n\n# 数字就是一切\n\n当你开始计算机编程时，你需要习惯的是，惊人数量的东西可以作为数字存储在计算机内存中。玩家的血量？正如我们在上一节看到的，hp 可以是一个整数。如果玩家受伤，我们减少这个数字。如果玩家获得生命值，我们增加数量。\n\n颜色也可以存储为数字！如果您使用了标准的图像编辑程序，可能会有一些滑块指示颜色，如使用了多少红色、绿色和蓝色，如 Pixelmator 的颜色滑块。Photoshop 没有滑块，但会向您显示数字，并允许您直接编辑它们来更改颜色。一种颜色用三个数字来表示。下图截图中显示的紫色为(R: `127`、G: `34`、B: `203`):\n\n![](img/31daffe0-b152-444a-a39a-c7b14167ab65.png)\n\n如您所见，Photoshop 允许您使用其他数字来表示颜色，例如 HSB(色调、饱和度、亮度)，这是一种表示颜色的替代方式，或者用于打印的 CMYK(青色、品红色、黄色、黑色)，因为专业印刷机在打印时使用这些颜色的油墨。在计算机显示器上观看时，您通常会坚持使用 RGB 颜色表示，因为这是显示器使用的颜色。\n\n世界几何呢？这些也只是数字；我们所要做的就是存储一个 3D 空间点的列表( *x* 、 *y* 和 *z* 坐标)，然后存储另一个点的列表，解释这些点如何连接形成三角形。在下面的截图中，我们可以看到 3D 空间点是如何用来表示世界几何的:\n\n![](img/ed292664-eb2e-4e40-94eb-3b3187163583.png)\n\n颜色的数字和三维空间点的数字的组合将让你在你的游戏世界中画出大的彩色风景。\n\n前面例子的诀窍是我们如何解释存储的数字，这样我们就可以让它们表达我们想要表达的意思。\n\n# 更多变量\n\n你可以把变量想象成宠物携带者。猫背带可以用来背猫，但不能背狗。同样，您应该使用浮点型变量来携带十进制值的数字。如果您将十进制值存储在`int`变量中，它将不适合:\n\n```cpp\nint x = 38.87f; \ncout << x << endl; // prints 38, not 38.87 \n```\n\n这里真正发生的是 C++ 在`38.87`、*上进行自动类型转换，将*转换成整数以适合`int`携带盒。它会将小数`38.87`转换为整数值`38`。\n\n因此，例如，我们可以修改代码以包含三种类型变量的使用，如以下代码所示:\n\n```cpp\n#include <iostream> \n#include <string>  // need this to use string variables! \nusing namespace std; \nint main() \n{ \n  string name; \n  int goldPieces; \n  float hp; \n  name = \"William\"; // That's my name \n  goldPieces = 322; // start with this much gold  \n  hp = 75.5f;       // hit points are decimal valued \n  cout << \"Character \" << name << \" has \"  \n           << hp << \" hp and \"  \n           << goldPieces << \" gold.\"; \n} \n```\n\n在前三行中，我们声明了三个存储数据部分的框，如下所示:\n\n```cpp\nstring name; int goldPieces; float hp; \n```\n\n这三条线在内存中预留了三个位置(比如停车位)。接下来的三行用我们需要的值填充变量，如下所示:\n\n```cpp\nname = \"William\"; \ngoldPieces = 322; \nhp = 75.5f; \n```\n\n在计算机内存中，如下图所示:\n\n![](img/91199601-5d19-43f5-98e6-a7dea05d3d5d.png)\n\n您可以随时更改变量的内容。您可以使用`=`赋值运算符编写一个变量，如下所示:\n\n```cpp\ngoldPieces = 522;// = is called the \"assignment operator\" \n```\n\n您也可以随时读取变量的内容。这就是接下来三行代码的作用，如下所示:\n\n```cpp\ncout << \"Character \" << name << \" has \"  \n     << hp << \" hp and \"  \n     << goldPieces << \" gold.\"; \n```\n\n请看下面一行:\n\n```cpp\ncout << \"I have \" << hp << \" hp.\" << endl; \n```\n\n`hp`这个词在这行有两种用法。一个在双引号之间，另一个不在。双引号之间的单词总是完全按照您键入的内容输出。当不使用双引号时(例如，`<< hp <<`)，将执行变量查找。如果变量不存在，那么您将得到一个编译器错误(未声明的标识符)。\n\n内存中有一个空间分配给名字，一个空间分配给玩家有多少`goldPieces`，一个空间分配给玩家的血量。\n\n这是您运行程序时应该看到的内容:\n\n![](img/aa24ff21-3d49-4d67-9c0c-ddd78d56a222.png)\n\nIn general, you should always try to store the right type of data inside the right type of variable. If you happen to store the wrong type of data, your code may misbehave. For example, accidentally storing a float into an `int` variable will make you lose the decimal points, and storing the value of a char in an `int` will give you the ASCII value, but will no longer treat it as a letter. Sometimes, it even doesn't have any type of automatic type conversion so it won't know how to handle the value at all.\n\n# C++ 中的数学\n\nC++ 的数学很好做；`+`(正)、`-`(负)、`*`(次)、`/`(除)都是常见的 C++ 运算，会遵循适当的**括号**、**指数**、*、* **除法**、**乘法**、**加法**、**减法** ( **BEDMAS** )顺序。例如，我们可以按照下面的代码进行操作:\n\n```cpp\nint answer = 277 + 5 * 4 / 2 + 20; \n```\n\n当然，如果你想绝对确定顺序，使用括号总是一个好主意。另一个您可能还不熟悉的运算符是%(模数)。模数(例如 10 % 3)求`x` (10)除以`y` (3)的余数。有关示例，请参见下表:\n\n| 操作员(姓名) | 例子 | 回答 |\n| +(加号) | 7 + 3 | Ten |\n| -(减) | 8 - 5 | three |\n| *(次) | 5*6 | Thirty |\n| /(分部) | 12/6 | Two |\n| %(模量) | 10 % 3 | 1(因为 10/3 是 3，余数= 1)。 |\n\n然而，我们经常不想以这种方式做数学。相反，我们通常希望将变量值改变一定的计算量。这是一个比较难理解的概念。假设玩家遇到一个小鬼，受到 15 点伤害。\n\n下面一行代码将用来把玩家的`hp`减少`15`(信不信由你):\n\n```cpp\nhp = hp - 15;                  // probably confusing :) \n```\n\n你可能会问为什么。因为在右边，我们正在计算 hp 的新值(`hp-15`)。找到 hp 的新值后(比之前少 15)，新值被写入`hp`变量。\n\n把`hp`想象成墙上某个特定位置的一幅画。`-15`告诉你在画上画个小胡子，但留在原地。这幅新的留胡子的画现在是`hp`。\n\nPitfall\nAn uninitialized variable has the bit pattern that was held in memory for it before. Declaring a variable does not clear the memory.\n\n假设我们使用了下面一行代码:\n\n```cpp\nint hp;   \nhp = hp - 15;   \n```\n\nThe second line of code reduces the hp by 15 from its previous value. What was its previous value if we never set `hp = 100` or so? It could be 0, but not always.\nOne of the most common errors is to proceed with using a variable without initializing it first.\n\n下面是这样做的简写语法:\n\n```cpp\nhp -= 15; \n```\n\n除`-=`外，可以使用`+=`给变量加一个量，`*=`给变量乘以一个量，`/=`给变量除以一个量。\n\n如果您使用的是`int`并想将其增加(或减少)1，您可以缩短语法。您不需要编写以下内容:\n\n```cpp\nhp = hp + 1;\nhp = hp - 1;\n```\n\n相反，您可以执行以下任一操作:\n\n```cpp\nhp++ ;\n++ hp;\nhp--;\n--hp;\n```\n\n将它放在变量之前，在使用值之前递增或递减它(如果您在更大的语句中使用它)。放在后面会在变量使用后更新变量。\n\n# 练习\n\n执行以下操作后，记下`x`的值，然后与您的编译器核对:\n\n| 练习 | 解决方法 |\n| --- | --- |\n| `int x = 4; x += 4;` | `8` |\n| `int x = 9; x-=2;` | `7` |\n| `int x = 900; x/=2;` | `450` |\n| `int x = 50; x*=2;` | `100` |\n| `int x = 1; x += 1;` | `2` |\n| `int x = 2; x -= 200;` | `-198` |\n| `int x = 5; x*=5;` | `25` |\n\n# 广义变量语法\n\n在前一节中，您了解到用 C++ 保存的每一条数据都有一个类型。所有变量都是以同样的方式创建的；在 C++ 中，变量声明的形式如下:\n\n```cpp\nvariableType variableName; \n```\n\n`variableType`对象告诉你我们要在变量中存储什么类型的数据。`variableName`对象是我们用来读取或写入内存的符号。\n\n# 原始类型\n\n我们之前讨论过计算机内部的所有数据在某个时候将会是一个数字。您的计算机代码负责正确解释该数字。\n\n据说 C++ 只定义了几个基本的数据类型，如下表所示:\n\n| `Char` | 单个字母，如 *a* 、 *b* 或 *+* 。使用 ASCII 将它存储为-127 到 127 之间的数值，ASCII 是一种为每个字符分配特定数值的标准。 |\n| `Short` | 从`-32,767`到`+32,768`的整数。 |\n| `Int` | 从`-2,147,483,647`到`+2,147,483,648`的整数。 |\n| `Long` | 从`-2,147,483,647`到`+2,147,483,648`的整数。 |\n| `Float` | 从大约。`-1x10<sup>38</sup>`至`1x10<sup>38</sup>`。 |\n| `Double` | 从大约。`-1x10<sup>308</sup>`至`1x10<sup>308</sup>`。 |\n| `Bool` | 是真是假。 |\n\n上表中提到的每种变量类型都有无符号版本(当然 Bool 除外，它不会真正有意义)。无符号变量可以包含自然数，包括 0 (x >= 0)。例如，无符号的`short`可能具有介于`0`和`65535`之间的值。如果有必要，您也可以使用`long long`或`long long int`获得更大的整数。\n\nThe size of variables can sometimes be different for different compilers, or depending on whether you are compiling for a 32-bit or 64-bit operating system. Keep that in mind if you find yourself working on something different in the future.\n\n在这种情况下，我们关注的是 Visual Studio 或 Xcode 和(最有可能的)64 位。\n\nIf you're interested in the difference between float and double, please feel free to look it up on the internet. I will keep my explanations only for the most important C++ concepts used for games. If you are curious about something that's not covered by this text, feel free to look it up.\n\n# 高级可变主题\n\n较新版本的 C++ 增加了一些与变量相关的新特性，还有一些还没有提到。这里有几件事你应该记住。\n\n# 自动检测类型\n\n从 C++ 11 开始，有一个新的变量*类型*，你可以用在你可能不确定你期望得到什么类型的情况下。这种新型被称为`auto`。它的意思是，它会检测你首先赋予它的任何值的类型，然后使用它。假设您键入以下内容:\n\n```cpp\nauto x = 1.5;\nauto y = true;\n```\n\n如果这样做，`x`将自动成为浮点数，`y`将成为布尔值。一般来说，如果你知道实际的变量类型，你会想使用它(大多数时候你会)，作为初学者，最好避免使用它。然而，当你看到它的时候，你应该能够认出它，如果你最终遇到了你需要它的情况，你应该知道它。\n\n# 枚举数\n\n枚举已经存在很长时间了，但是从 C++ 11 开始，您可以更好地控制它们。枚举背后的想法是，有时你想在游戏中跟踪不同类型的东西，你只是想要一个简单的方法来给每个东西一个值，告诉你它是什么，你可以稍后检查。枚举如下所示:\n\n```cpp\nenum weapon {\n    sword = 0;\n    knife,\n    axe,\n    mace,\n    numberOfWeaponTypes,\n    defaultWeapon = mace\n}; // Note the semicolon at the end\n```\n\n这将创建这些武器类型中的每一种，并通过给每一种添加 1 来为每一种分配唯一的值，因此刀将等于 1，斧等于 2，依此类推。请注意，您不需要将第一个数字设置为 0(它会自动设置)，但是如果您想从不同的数字开始，您可以这样做(并且它不仅仅是第一个可以设置为特定值的数字)。您也可以将任何`enum`成员分配给不同的成员，它将具有相同的值(在本例中，`defaultWeapon`具有与`mace` : 3 相同的值)。每当您在枚举列表中的任何位置分配一个特定值时，您在该列表中添加的任何类型都将从该值开始增加 1。\n\n枚举总是包含一个 int 值，但是从 C++ 11 开始，您可以指定一个变量类型。例如，您可能想做类似以下的事情:\n\n```cpp\nenum isAlive : bool {\n    alive = true,\n    dead = false\n}\n```\n\n虽然您可以使用 0 和 1 来实现这一点，但在某些情况下，您可能会发现这更方便。\n\n# 常量变量\n\n有时候你会有一个你不想在游戏中改变的价值。你不希望生命值、最大生命值、达到特定等级所需的经验值或移动速度发生变化(除非你的角色确实达到了那个等级，在这种情况下，你可能会切换到不同的常量值)。\n\n在某些情况下，一个`enum`可以实现这一点，但是对于单个值，更容易创建一个新的变量并将其声明为`const`。这里有一个例子:\n\n```cpp\nconst int kNumLives = 5;\n```\n\n将`const`放在变量类型的前面，告诉程序永远不允许改变那个值，如果你尝试，会给你一个错误。将`k`放在变量名前面是`const`变量常用的命名约定。许多公司会坚持让你遵循这个标准。\n\n# 构建更复杂的类型\n\n事实证明，这些简单的数据类型可以单独用来构建任意复杂的程序。*如何？*你问。只用浮点数和整数构建一个 3D 游戏不是很难吗？\n\n从`float`和`int`构建一个游戏其实并不难，但是更复杂的数据类型会有帮助。如果我们为玩家的位置使用松散的浮动，那么编程将是乏味和混乱的。\n\n# 对象类型–结构\n\nC++ 为你提供了将变量组合在一起的结构，这将使你的生活变得更加容易。以下面的代码块为例:\n\n```cpp\n#include <iostream> \nusing namespace std; \nstruct Vector        // BEGIN Vector OBJECT DEFINITION \n{ \n  float x, y, z;     // x, y and z positions all floats \n};                   // END Vector OBJECT DEFINITION. \n// The computer now knows what a Vector is \n// So we can create one. \nint main() \n{ \n  Vector v; // Create a Vector instance called v \n  v.x=20, v.y=30, v.z=40; // assign some values \n  cout << \"A 3-space vector at \" << v.x << \", \" << v.y << \", \" <<  \n   v.z << endl; \n} \n```\n\n这在记忆中的样子很直观；一个**向量**只是一个有三个浮点的内存块，如下图所示:\n\n![](img/f0a7f2fa-edae-43ba-8254-665d7b7eec5a.png)\n\nDon't confuse the `struct Vector` in the preceding screenshot with the `std::vector` of the **Standard Template Library** (**STL**)—we'll get into that later. The preceding `Vector` object is meant to represent a three-space vector, while the STL's `std::vector` type represents a collection of values.\n\n下面是关于前面代码清单的一些回顾性注释。\n\n首先，甚至在我们使用我们的`Vector`对象类型之前，我们就必须定义它。C++ 没有数学向量的内置类型(它只支持标量数字，他们认为这就足够了！).因此，C++ 让您可以构建自己的对象结构，让您的生活更轻松。我们首先有了以下定义:\n\n```cpp\nstruct Vector        // BEGIN Vector STRUCT DEFINITION \n{ \n  float x, y, z;     // x, y, and z positions all floats \n};                   // END Vector STRUCT DEFINITION. \n```\n\n这告诉计算机什么是`Vector`(它是三个浮动，在内存中它们都被声明为彼此相邻)。上图显示了`Vector`在内存中的样子。\n\n接下来，我们使用`Vector`对象定义来创建一个名为`v`的矢量实例:\n\n```cpp\nVector v; // Create a Vector instance called v \n```\n\n一旦你有了一个`Vector`的实例，你就可以使用我们所说的**点语法**访问其中的变量。您可以使用`v.x`访问矢量`v`上的变量`x`。`struct`矢量定义实际上并没有创建矢量对象，它只是定义了对象类型。你做不到`Vector.x = 1`。你说的是哪个对象实例？C++ 编译器会问。需要先创建一个 Vector 实例，比如 Vector `v`。这会创建一个向量的实例，并将其命名为`v`。然后，你可以在`v`实例上做作业，比如`v.x = 0`。\n\n然后，我们使用这个实例将值写入`v`:\n\n```cpp\nv.x=20, v.y=30, v.z=40; // assign some values \n```\n\nWe used commas in the preceding code to initialize a bunch of variables on the same line. This is okay in C++. Although you can do each variable on its own line, the approach shown here is okay too.\n\n这使得`v`看起来像前面的图像。然后，我们把它们打印出来:\n\n```cpp\ncout << \"A 3-space vector at \" << v.x << \", \" << v.y << \", \" <<  \n   v.z << endl;\n```\n\n在这两行代码中，我们通过简单地使用一个点(`.`)来访问对象内部的各个数据成员；`v.x`指物体内部的`x`成员`v`。每个矢量对象内部正好有三个浮动:一个叫做`x`，一个叫做`y`，一个叫做`z`。\n\n# 锻炼-玩家\n\n为`Player`对象定义一个 C++ 数据结构。然后，创建一个`Player`结构的实例，并用值填充每个数据成员。\n\n# 解决办法\n\n让我们声明我们的`Player`对象。我们想把和玩家有关的一切都归入`Player`对象。我们这样做是为了代码整洁。您在虚幻引擎中阅读的代码将在任何地方使用这样的对象，因此请注意:\n\n```cpp\nstruct Player \n{ \n  string name; \n  int hp; \n  Vector position; \n}; // Don't forget this semicolon at the end! \nint main() \n{ \n  // create an object of type Player, \n  Player me; // instance named 'me' \n  me.name = \"William\"; \n  me.hp = 100; \n  me.position.x = me.position.y = me.position.z=0; \n} \n```\n\nThe line `me.position.x = me.position.y = me.position.z=0;` means `me.position.z` is set to `0`, and then that value is passed on to set `me.position.y` to 0, and then it is passed along and sets `me.position.x` to `0`.\n\n`struct Player`定义告诉计算机一个`Player`对象是如何在内存中布局的。\n\nI hope you noticed the mandatory semicolon at the end of the struct declaration. Struct object declarations need to have a semicolon at the end, but functions do not (we'll go over functions later). This is just a C++ rule that one must remember.\n\n在一个`Player`对象中，我们声明了一个字符串代表玩家的名字，一个浮点数代表他们的血量，一个`Vector`对象代表他们完整的`x`、`y`和`z`位置。\n\n我说的对象，是指一个 C++ 结构(后面我们会介绍术语*类*)。\n\n等等！我们把一个矢量对象放在一个玩家对象里面！是的，你能做到。只要确保向量是在同一个文件中定义的。\n\n在定义了`Player`对象内部的内容之后，我们实际上创建了一个名为`me`的`Player`对象实例，并为其分配了一些值。\n\n# 两颗北极指极星\n\n一个特别难理解的概念是指针的概念。指针并不难理解，但需要一段时间才能掌握。指针基本上包含内存中存储对象的地址，因此它们“指向”内存中的对象。\n\n假设我们像以前一样，在内存中声明了一个类型为`Player`的变量:\n\n```cpp\nPlayer me; \nme.name = \"William\"; \nme.hp = 100; \n```\n\n我们现在声明一个指向`Player`的指针:\n\n```cpp\nPlayer* ptrMe;               // Declaring a pointer to \n                             // a Player object\n```\n\n`*`改变变量类型的含义。正是`*`使`ptrMe`成为指向`Player`对象的指针，而不是常规的`Player`对象。\n\n我们现在要将`ptrMe`链接到`me`:\n\n```cpp\nptrMe = &me;                  // LINKAGE \n```\n\nThis linkage step is very important. If you don't link the pointer to an object before you use the pointer, you will get a memory access violation—an error that you are trying to access memory that you didn't set, so it could contain random data or even part of another program!\n\n`ptrMe`指针现在指向与`me`相同的对象。更改对象`ptrMe`指向的变量值将在`me`中更改它们，如下图所示:\n\n![](img/39c61393-1da4-452f-a007-f0252648101b.png)\n\n# 指针能做什么？\n\n当我们在指针变量和它所指向的对象之间建立链接时，我们可以通过指针来操作它所指向的变量。\n\n指针的一个用途是从代码中的几个不同位置引用同一个对象。如果您经常尝试访问它，您可能希望在本地存储一个指向它的指针，以便于访问。`Player`对象是被指向的一个很好的候选对象，因为代码中的许多地方可能会不断地访问它。\n\n您可以创建任意多的指向同一个对象的指针，但是您需要跟踪所有的指针(除非您使用智能指针，我们将在后面讨论)。被指向的对象不一定知道它们被指向，但是可以通过指针对对象进行更改。\n\n比如说，玩家被攻击了。结果将是他们的血量减少，这种减少将使用指针来完成，如下面的代码所示:\n\n```cpp\nptrMe->hp -= 33;      // reduced the player's hp by 33 \nptrMe->name = \"John\";// changed his name to John \n```\n\n使用指针时，需要使用`->`而不是`.`来访问指向对象中的变量。\n\n以下是`Player`对象现在的样子:\n\n![](img/aa4316de-1a4a-43bc-a029-729cf5c3125e.png)\n\n所以，我们通过改变`ptrMe->name`来改变`me.name`。因为`ptrMe`指向`me`，通过`ptrMe`的变化直接影响`me`。\n\n# 操作员地址(&)\n\n请注意前面代码示例中`&`符号的使用。`&`运算符获取存储变量的内存地址。变量的内存地址是计算机内存空间中为存储变量的值而保留的位置。C++ 能够获取程序内存中任何对象的内存地址。变量的地址是唯一的，也是随机的。\n\n假设我们打印一个整数变量的地址`x`，如下所示:\n\n```cpp\nint x = 22; \ncout << &x << endl; // print the address of x \n```\n\n在程序第一次运行时，我的计算机会打印以下内容:\n\n```cpp\n0023F744 \n```\n\n这个数字(`&x`的值)只是存储`x`变量的存储单元。这意味着在程序的这个特定启动中，`x`变量位于存储单元号`0023F744`，如下图所示:\n\n![](img/6bdb0610-ff05-47f6-a361-5b6026e67f61.png)\n\nYou may wonder why the preceding number contains an `F`. Addresses are in hexadecimal (base 16) so since you run out of numerical digits after 9, but you can't really fit two digits in 1, you set the values that would be 10-15 to A-F instead. So A = 10, B = 11, and in this case F = 15.\n\n现在，创建一个指针变量并分配给`x`的地址:\n\n```cpp\nint *px; \npx = &x; \n```\n\n我们在这里做的是将`x`的内存地址存储在`px`变量中。所以，我们用另一个不同的变量`px`来比喻`x`变量。这可能类似于下图所示:\n\n![](img/6e560752-06c4-46e1-b673-7e7486f96405.png)\n\n这里`px`变量里面有`x`变量的地址。换句话说，`px`变量是对另一个变量的引用。去引用`px`意味着访问`px`正在引用的变量。使用`*`符号进行去参考:\n\n```cpp\ncout << *px << endl; \n```\n\n# 使用 nullptr\n\n`nullptr`变量是一个指针变量，值为`0`。一般来说，大多数程序员喜欢在创建新的指针变量时初始化指向`nullptr` ( `0`)的指针。一般来说，计算机程序不能访问内存地址`0`(它是保留的)，所以如果你试图引用一个空指针，你的程序就会崩溃。\n\n*Pointer Fun with Binky* is a fun video about pointers. Take a look at [http://www.youtube.com/watch?v=i49_SNt4yfk](http://www.youtube.com/watch?v=i49_SNt4yfk).\n\n# 智能指针\n\n指针可能很难管理。一旦我们在本书后面开始创建和删除新对象，我们可能不知道指向特定对象的所有指针在哪里。删除另一个指针仍在使用的对象(导致崩溃)或者停止从指向某个对象的唯一指针指向该对象，并让它在内存中浮动而没有任何引用它的对象(这称为内存泄漏，会降低您的计算机速度)可能太容易了。\n\n智能指针跟踪特定对象的引用数量，并随着代码的变化自动增加或减少这个数量。这使得控制正在发生的事情变得容易得多，在现实编程中，如果可能的话，最好使用常规指针。\n\n人们过去必须编写自己的智能指针，但从 C++ 11 开始就没有了。现在有一个`shared_ptr`模板可用(我们稍后将讨论模板和 STL)。这将自动跟踪指向某个对象的指针，如果没有其他对象引用该对象，将自动删除该对象，从而防止内存泄漏。这就是为什么使用智能指针比指针更好的原因，因为常规指针可能指向代码中其他地方已经删除的对象。\n\n# 输入和输出\n\n在编程中，您必须不断地向用户传递信息，或者从用户那里获取信息。对于简单的情况，比如我们将要开始的情况(以及以后发现错误的许多情况)，您需要输入和输出标准文本和数字。C++ 使这变得容易。\n\n# cin 和 cout 物件\n\n我们已经在前面的例子中看到了`cout`是如何工作的。`cin`对象是 C++ 传统上将用户输入输入程序的方式。`cin`对象很容易使用，因为它查看它将放入值的变量的类型，并使用它来确定放入其中的类型。例如，假设我们想询问用户的年龄，并将其存储在`int`变量中。我们可以这样做:\n\n```cpp\ncout << \"What is your age?\" << endl; \nint age; \ncin >> age; \n```\n\n当你运行这个时，它会打印`What is your age?`并等待你的响应。输入答案，点击*进入*输入。除了`int`变量之外，你可能想试着输入其他东西，看看会发生什么！\n\n# printf()函数\n\n虽然到目前为止我们已经使用`cout`打印出变量，但是您应该还知道另一个用于打印到控制台的常用函数。这个函数被称为`printf`函数，它最初来自于 c .`printf`函数包含在`<iostream>`库中，所以您不必额外使用`#include`任何东西。游戏行业有些人更喜欢`printf`而不是`cout`，下面就来介绍一下。\n\n让我们继续讨论`printf()`是如何工作的，如下面的代码所示:\n\n```cpp\n#include <iostream> \n#include <string> \nusing namespace std; \nint main() \n{ \n  char character = 'A'; \n  int integer = 1; \n  printf( \"integer %d, character %c\\n\", integer, character ); \n} \n```\n\nDownloading the example code\n\nYou can download the example code files from your account at [http://www.packtpub.com](http://www.packtpub.com) for all the Packt books you have purchased. If you purchased this book elsewhere, you can visit [http://www.packtpub.com/support](http://www.packtpub.com/support) and register to have the files emailed directly to you.\n\n我们从格式字符串开始。格式字符串就像一个图片框，变量会插在格式字符串中`%`的位置。然后，整个事情被转到控制台。在上例中，整数变量将插入第一个`%` ( `%d`)的位置，字符将插入第二个`%` ( `%c`)的位置，如下截图所示:\n\n![](img/0f75934f-d8a3-4f1f-92c3-8e724477863f.png)\n\n您必须使用正确的格式代码才能使输出正确格式化；请看下表:\n\n| 数据类型 | 格式代码 |\n| `Int` | `%d` |\n| `Char` | `%c` |\n| `String` | `%s` |\n\n要打印 C++ 字符串，必须使用`string.c_str()`函数:\n\n```cpp\nstring s = \"Hello\"; printf( \"string %s\\n\", s.c_str() ); \n```\n\n`s.c_str()`函数访问字符串的 C 指针，`printf`需要。\n\n如果您使用了错误的格式代码，输出将不会正确显示，或者程序可能会崩溃。\n\n您可能还会发现需要使用这种格式来设置字符串的情况，因此了解这一点很有帮助。但是如果你想避免记住这些不同的格式代码，就使用`cout`。它会帮你找出类型。只要确保你使用你最终工作的公司喜欢的任何标准。在编程中的大多数事情上这样做通常是一个好主意。\n\n# 锻炼\n\n询问用户的姓名和年龄，并使用`cin`将他们带入。然后，在控制台使用`printf()`(不是`cout`)为他们发出问候。\n\n# 解决办法\n\n这是程序的外观:\n\n```cpp\n#include <iostream> \n#include <string> \nusing namespace std; \nint main() \n{ \n  cout << \"Name?\" << endl; \n  string name; \n  cin >> name; \n  cout << \"Age?\" << endl;  \n  int age; \n  cin >> age; \n  //Change to printf:\n  cout << \"Hello \" << name << \" I see you have attained \" << age  \n   << \" years. Congratulations.\" << endl; \n} \n```\n\nA string is actually an object type. Inside, it is just a bunch of chars!\n\n# 命名空间\n\n到目前为止，我们已经在`std`的例子中看到了名称空间，我们通过将以下内容放在文件的顶部，基本上避免了这个问题:\n\n```cpp\nusing namespace std;\n```\n\n但是，你应该知道这对未来意味着什么。\n\n名称空间是将相关代码组合在一起的方法，它允许您在不同的名称空间中使用相同的变量名，而不会有任何命名冲突(当然，除非您将两者的`using namespace`放在顶部，这就是为什么许多人不喜欢使用它)。\n\n您可以在 C++ 文件中创建自己的命名空间，如下所示:\n\n```cpp\nnamespace physics {\n    float gravity = 9.80665;\n    //Add the rest of your your physics related code here...\n}\n```\n\n一旦创建了命名空间，就可以像这样访问代码:\n\n```cpp\nfloat g = physics::gravity;\n```\n\n或者，您可以在顶部放一个 using 语句(只需确保该名称没有用于其他用途)。但是，一般来说，您不希望将此用于更复杂的程序，因为一个命名空间允许您在不同的命名空间中重用相同的变量名，所以如果您将它与一个命名空间一起使用，该命名空间中的变量与当前命名空间中的变量同名，并试图访问它，编译器将不会知道您指的是哪个变量，这将导致冲突。\n\n# 摘要\n\n在这一章中，我们谈到了变量和记忆。我们讨论了变量的数学运算，以及它们在 C++ 中有多简单。\n\n我们还讨论了如何使用这些简单数据类型的组合来构建任意复杂的数据类型，例如浮点数、整数和字符。像这样的结构称为对象。在下一章中，我们将开始谈论我们可以用这些对象做什么！*"
  },
  {
    "path": "docs/learn-cpp-build-game-ue4/03.md",
    "content": "# 三、`if...else`和`switch`\n\n在前一章中，我们讨论了内存的重要性，以及如何在计算机中存储数据。我们谈到了如何使用变量为程序保留内存，以及如何在变量中包含不同类型的信息。\n\n在本章中，我们将讨论如何控制程序的流程，以及如何通过使用控制流语句来分支代码来更改执行的代码。在这里，我们将讨论不同类型的控制流，如下所示:\n\n*   `If`陈述\n*   如何使用`==`运算符检查事物是否相等\n*   `else`陈述\n*   如何测试不等式(即如何使用`>`、`>=`、`<`、`<=`和`!=`运算符检查一个数是大于还是小于另一个数)\n*   使用逻辑运算符(如 not ( `!`)和(`&&`)或(`||`)\n*   分支方式不止两种:\n    *   `else if`声明\n    *   `switch`声明\n*   我们的第一个虚幻引擎示例项目\n\n# 分支\n\n我们在[第二章](02.html)、*变量和记忆*中写的计算机代码朝着一个方向:一直向下。有时，我们可能希望能够跳过部分代码。我们可能希望代码能够分支到多个方向。示意性地，我们可以用以下方式表示:\n\n![](img/c1178346-f65b-44e1-84a2-c7030d6be946.png)\n\n换句话说，我们希望选项在某些条件下不运行某些代码行。前面的图表称为流程图。根据这个流程图，如果，也只有当，我们饿了，我们会去准备一个三明治，吃了它，然后去沙发上休息。如果我们不饿，那么就没有必要做三明治，所以我们会简单地在沙发上休息。\n\n我们有时会在这本书里只使用流程图，但是在 UE4 中，你甚至可以使用流程图来编程你的游戏(使用一些叫做蓝图的东西)。\n\nThis book is about C++ code, so we will always transform our flowcharts into actual C++ code in this book.\n\n# 控制程序的流程\n\n最终，我们想要的是代码在特定条件下以一种方式分支。改变接下来执行哪一行代码的代码命令称为控制流语句。最基本的控制流语句是`if`语句。为了能够对`if`语句进行编码，我们首先需要一种检查变量值的方法。\n\n所以，首先，我们来介绍一下`==`符号，它用来检查一个变量的值。\n\n# ==运算符\n\n为了在 C++ 中检查两件事是否相等，我们需要一个接一个地使用两个等号(`==`)而不是一个，如下所示:\n\n```cpp\nint x = 5; // as you know, we use one equals sign  \nint y = 4; // for assignment.. \n// but we need to use two equals signs  \n// to check if variables are equal to each other \ncout << \"Is x equal to y? C++ says: \" << (x == y) << endl; \n```\n\n如果运行前面的代码，您会注意到输出如下:\n\n```cpp\nIs x equal to y? C++ says: 0  \n```\n\n在 C++ 中，`1`表示真，`0`表示假。如果您希望出现单词`true`或`false`而不是`1`和`0`，您可以在`cout`行代码中使用`boolalpha`流操纵器，如下所示:\n\n```cpp\ncout << \"Is x equal to y? C++ says: \" << boolalpha <<  \n        (x == y) << endl; \n```\n\n`==`运算符是一种比较运算符。C++ 之所以使用`==`检查等式而不仅仅是`=`是因为我们已经用完了赋值运算符的`=`符号！(详见[第二章](02.html)、*变量和记忆*中的*变量*部分)。如果我们使用单个`=`符号，C++ 将假设我们想要用`y`覆盖`x`，而不是比较它们。\n\n# if 语句编码\n\n现在我们已经有了双等号，让我们来编码流程图。上述流程图的代码如下:\n\n```cpp\nbool isHungry = true;  // can set this to false if not \n                       // hungry! \nif( isHungry == true ) // only go inside { when isHungry is true \n{ \n  cout << \"Preparing snack..\" << endl; \n  cout << \"Eating .. \" << endl; \n} \ncout << \"Sitting on the couch..\" << endl; \n```\n\nThis is the first time we are using a `bool` variable! A `bool` variable either holds the value `true` or the value `false`.\n\n首先，我们从一个名为`isHungry`的`bool`变量开始，并将其设置为`true`。\n\n然后，我们使用一个`if`语句，如下所示:\n\n```cpp\nif( isHungry == true )\n```\n\n`if`语句就像是对它下面的代码块的守卫(记住，一个代码块是一组封装在`{`和`}`中的代码):\n\n![](img/31a956f9-de7e-4848-b059-20dade64db0d.png)\n\n只有在`isHungry==true`的情况下，才能读取`{`和`}`之间的代码。\n\n只有当`isHungry == true`时，才能得到花括号内的代码。否则，您将被拒绝访问并被迫跳过整个代码块。\n\nBasically, anything that can be evaluated as a boolean can go inside `if (boolean)`. So, we can achieve the same effect by simply writing the following line of code:\n`if( isHungry ) // only go here if isHungry is true` This can be used as an alternative for the following:\n`if( isHungry == true )`\n\n人们可能使用`if( isHungry )`形式的原因是为了避免出错的可能性。每次`if`语句被击中时，偶然写`if( isHungry = true )`会将`isHungry`设置为真！为了避免这种可能性，我们可以只写`if( isHungry )`来代替。或者，一些(明智的)人使用所谓的尤达条件来检查`if`语句:`if( true == isHungry )`。我们这样写`if`语句的原因是，如果我们不小心写了`if( true = isHungry )`，这会产生一个编译器错误，抓住错误。\n\n试着运行这段代码，看看我的意思:\n\n```cpp\nint x = 4, y = 5; \ncout << \"Is x equal to y? C++ says: \" << (x = y) << endl; //bad! \n// above line overwrote value in x with what was in y, \n// since the above line contains the assignment x = y \n// we should have used (x == y) instead. \ncout << \"x = \" << x << \", y = \" << y << endl; \n```\n\n下面几行显示了前面几行代码的输出:\n\n```cpp\nIs x equal to y? C++ says: 5 \nx = 5, y = 5 \n```\n\n有`(x = y)`的代码行用`y`的值(5)覆盖`x`的前一个值(4)。虽然我们试图检查`x`是否等于`y`，但是在前面的陈述中发生的是`x`被赋予了`y`的值。\n\n# 编码 else 语句\n\n`else`语句用于让我们的代码在代码的`if`部分没有运行的情况下做一些事情。\n\n例如，假设我们有其他事情要做，以防不饿，如下面的代码片段所示:\n\n```cpp\nbool isHungry = true; \nif( isHungry )      // notice == true is implied! \n{ \n  cout << \"Preparing snack..\" << endl; \n  cout << \"Eating .. \" << endl; \n} \nelse                // we go here if isHungry is FALSE \n{ \n  cout << \"I'm not hungry\" << endl; \n} \ncout << \"Sitting on the couch..\" << endl; \n```\n\n关于`else`关键词，有几件重要的事情你需要记住，如下:\n\n*   `else`语句必须紧跟在`if`语句之后。在`if`块的末尾和相应的`else`块之间不能有任何额外的代码行。\n*   一个程序永远不能同时执行`if`和相应的`else`块。总是这样或那样:\n\n![](img/3676e502-fcb4-404c-b163-9fc5bb354f97.png)\n\nelse 语句是`isHungry`不等于真时你要走的路。\n\n你可以把`if` / `else`语句想象成一个守卫，让人们转向左边或右边。每个人要么走向食物(当`isHungry==true`)，要么远离食物(当`isHungry==false`)。\n\n# 使用其他比较运算符(>，> =，\n\n其他逻辑比较可以很容易地在 C++ 中完成。`>`和`<`符号的意思就是它们在数学中的作用。它们是大于(`>`)和小于(`<`)的符号。`>=`和数学中的`≥`符号意思相同。`<=`是`≤`的 C++ 代码。由于键盘上没有`≤`符号，我们不得不用 C++ 中的两个字符来写。`!=`就是我们在 C++ 中怎么说*“不等于”*。例如，假设我们有以下几行代码:\n\n```cpp\nint x = 9; \nint y = 7; \n```\n\n我们可以问电脑是`x > y`还是`x < y`，如下图:\n\n```cpp\ncout << \"Is x greater than y? \" << (x > y) << endl; \ncout << \"Is x greater than OR EQUAL to y? \" << (x >= y) << endl; \ncout << \"Is x less than y? \" << (x < y) << endl; \ncout << \"Is x less than OR EQUAL to y? \" << (x <= y) << endl; \ncout << \"Is x not equal to y? \" << (x != y) << endl; \n```\n\nWe need the brackets around the comparisons of `x` and `y` because of something known as operator precedence. If we don't have the brackets, C++ will get confused between the `<<` and `<` operators. It's weird and you will better understand this later, but you need C++ to evaluate the `(x < y)` comparison before you output the result (<<). There is an excellent table available for reference at [http://en.cppreference.com/w/cpp/language/operator_precedence](http://en.cppreference.com/w/cpp/language/operator_precedence).\n\n# 使用逻辑运算符\n\n逻辑运算符允许您进行更复杂的检查，而不是检查简单的等式或不等式。比方说，进入一个特殊房间的条件要求玩家同时拥有红色和绿色的钥匙卡。我们想检查两个条件是否同时成立。为了进行这种复杂的逻辑语句检查，我们需要学习另外三个构造:not ( `!`)、and ( `&&`)和 or ( `||`)运算符。\n\n# The not(！)操作员\n\n`!`运算符可以方便地反转`boolean`变量的值。以下面的代码为例:\n\n```cpp\nbool wearingSocks = true; \nif( !wearingSocks ) // same as if( false == wearingSocks ) \n{\n         cout << \"Get some socks on!\" << endl;\n } \nelse \n{ \n        cout << \"You already have socks\" << endl; \n} \n```\n\n这里的`if`语句检查你是否穿了袜子。然后，你会接到一个命令，让你穿上袜子。`!`运算符将`boolean`变量中的任何值反转为相反的值。\n\n我们使用一个叫做真值表的东西来显示在一个`boolean`变量上使用`!`运算符的所有可能结果，如下所示:\n\n| `wearingSocks` | `!wearingSocks` |\n| `true` | `false` |\n| `false` | `true` |\n\n所以，当`wearingSocks`有值`true`时，`!wearingSocks`有值`false`，反之亦然。\n\n# 练习\n\n1.  当`wearingSocks`的值为`true`时，你认为`!!wearingSocks`的值会是多少？\n2.  运行以下代码后`isVisible`的值是多少？\n\n```cpp\nbool hidden = true; \nbool isVisible = !hidden; \n```\n\n# 解决方法\n\n1.  如果`wearingSocks`是`true`，那么`!wearingSocks`就是`false`。于是，`!!wearingSocks`又变成了`true`。就像在说“我不饿。”不是不是是双重否定，所以这句话的意思是我其实是饿了。\n2.  第二个问题的答案是`false`。`hidden`的价值是`true`，所以`!hidden`是`false`。然后`false`值被保存到`isVisible`变量中。但是`hidden`本身的价值依然是`true`。\n\nThe `!` operator is sometimes colloquially known as a bang. The preceding bang-bang operation (`!!`) is a double negative and a double logical inversion. If you bang-bang a `bool` variable, there is no net change to the variable.\n\nOf course, you can use these on an `int` and in that case, if the `int` is set to zero, `! int` will be `true`, and if it is greater than zero, `! int` will be `false`. Therefore, if you bang-bang that `int` variable, and the `int` value is greater than zero, it is reduced to a simple `true`. If the `int` value is 0 already, it is reduced to a simple `false`.\n\n# and (&&)运算符\n\n假设两个条件为`true`，我们只想运行一段代码。例如，我们只在穿袜子和衣服的情况下才穿衣服。您可以使用以下代码来检查这一点:\n\n```cpp\nbool wearingSocks = true; \nbool wearingClothes = false; \nif( wearingSocks && wearingClothes )// && requires BOTH to be true \n{ \n        cout << \"You are dressed!\" << endl; \n} \nelse \n{ \n        cout << \"You are not dressed yet\" << endl; \n} \n```\n\n# or (||)运算符\n\n如果其中一个变量是`true`，我们有时想要运行一段代码。\n\n例如，假设玩家在关卡中找到一个特殊的星星，或者完成关卡的时间少于 60 秒，那么他就赢得了一定的奖励。在这种情况下，您可以使用以下代码:\n\n```cpp\nbool foundStar = false; \nfloat levelCompleteTime = 25.f; \nfloat maxTimeForBonus = 60.f; \n// || requires EITHER to be true to get in the { below \nif( foundStar || (levelCompleteTime < maxTimeForBonus) ) \n{ \n        cout << \"Bonus awarded!\" << endl; \n} \nelse \n{ \n        cout << \"No bonus.\" << endl; \n} \n```\n\nYou may notice that I added parentheses around `levelCompleteTime < maxTimeForBonus`. While precedence rules may let you add longer statements without them, I've found it can be better to just add them if you have any doubt. It's better safe than sorry (and may be a little clearer to someone else looking at it later).\n\n# 锻炼\n\n到现在为止，你应该已经注意到了，提高编程水平的最好方法就是去做。你必须经常练习编程，才能在这方面做得更好。\n\n创建两个整数变量，称为`x`和`y`，并从用户处读入。编写一个`if` / `else`语句对，打印大值变量的名称。\n\n# 解决办法\n\n前面练习的解决方案显示在下面的代码块中:\n\n```cpp\nint x, y; \ncout << \"Enter two numbers (integers), separated by a space \" << endl; \ncin >> x >> y; \nif( x < y )  \n{ \n  cout << \"x is less than y\" << endl; \n} \nelse \n{ \n  cout << \"x is greater than y\" << endl; \n} \n```\n\nDon't type a letter when `cin` expects a number. If that happens, `cin` can fail and give a bad value to your variable.\n\n# 以两种以上的方式分支代码\n\n在前面几节中，我们只能用两种方法之一来创建代码分支。在伪代码中，我们有以下代码:\n\n```cpp\nif( some condition is true ) \n{ \n  execute this; \n} \nelse // otherwise \n{ \n  execute that; \n} \n```\n\nPseudocode is *fake code*. Writing pseudocode is a great way to brainstorm and plan out your code, especially if you are not quite used to C++.\n\n这段代码有点像一个隐喻性的岔路口，只有两个方向可以选择。\n\n有时，我们可能想在两个方向上分支代码。我们可能希望代码以三种方式分支，甚至更多。例如，假设代码的方向取决于玩家当前持有的物品。玩家可以拿着三种不同的物品中的一种:硬币、钥匙或沙币。而 C++ 允许这样！事实上，在 C++ 中，你可以向任何你希望的方向分支。\n\n# else if 语句\n\n`else if`语句是一种不止在两个可能的分支方向上编码的方式。在下面的代码示例中，代码将以三种不同的方式之一运行，具体取决于玩家是拿着`Coin`、`Key`还是`Sanddollar`对象:\n\n```cpp\n#include <iostream> \nusing namespace std; \nint main() \n{ \n  enum Item  // This is how enums come in handy!\n  { \n    Coin, Key, Sanddollar // variables of type Item can have  \n    // any one of these 3 values \n  };\n  Item itemInHand = Key;  // Try changing this value to Coin,  \n                          // Sanddollar \n  if( itemInHand == Key ) \n  { \n    cout << \"The key has a lionshead on the handle.\" << endl; \n    cout << \"You got into a secret room using the Key!\" << endl; \n  } \n  else if( itemInHand == Coin ) \n  { \n    cout << \"The coin is a rusted brassy color. It has a picture  \n     of a lady with a skirt.\" << endl; \n    cout << \"Using this coin you could buy a few things\" << endl; \n  } \n  else if( itemInHand == Sanddollar ) \n  { \n    cout << \"The sanddollar has a little star on it.\" << endl; \n    cout << \"You might be able to trade it for something.\" <<  \n     endl; \n  } \n  return 0;  \n} \n```\n\nNote that the preceding code only goes in one of the three separate ways! In an `if`, `else`, and `else if` series of checks, we will only ever go into one of the blocks of code.\n\n![](img/ee886ad0-ece1-421b-a109-35e91255f97f.png)\n\n# 锻炼\n\n使用 C++ 程序回答代码后面的问题。请务必尝试这些练习，以便熟练使用这些等式运算符:\n\n```cpp\n#include <iostream> \nusing namespace std; \nint main() \n{ \n  int x; \n  int y; \n  cout << \"Enter an integer value for x:\" << endl; \n  cin >> x; // This will read in a value from the console \n  // The read in value will be stored in the integer  \n  // variable x, so the typed value better be an integer! \n  cout << \"Enter an integer value for y:\" << endl; \n  cin >> y; \n  cout << \"x = \" << x << \", y = \" << y << endl; \n  // *** Write new lines of code here \n} \n```\n\n在写着(`// *** Write new...`)的地方写一些新的代码行:\n\n1.  检查`x`和`y`是否相等。如果它们相等，打印`x and y are equal`。否则，打印`x and y are not equal`。\n2.  不等式练习:检查`x`是否大于`y`。如果是，打印`x is greater than y`。否则，打印`y is greater than x`。\n\n# 解决办法\n\n要计算相等性，请插入以下代码:\n\n```cpp\nif( x == y ) \n{ \n  cout << \"x and y are equal\" << endl; \n} \nelse \n{ \n  cout << \"x and y are not equal\" << endl; \n} \n```\n\n要检查哪个值更大，请插入以下代码:\n\n```cpp\nif( x > y ) \n{ \n  cout << \"x is greater than y\" << endl; \n} \nelse if( x < y ) \n{ \n  cout << \"y is greater than x\" << endl; \n} \nelse // in this case neither x > y nor y > x \n{ \n  cout << \"x and y are equal\" << endl; \n} \n```\n\n# switch 语句\n\n`switch`语句允许您的代码以多种方式分支。`switch`语句要做的是看一个变量的值，根据它的值，代码会往不同的方向走。\n\n我们还将在这里看到`enum`结构:\n\n```cpp\n#include <iostream> \nusing namespace std; \nenum Food  // enums are very useful with switch! \n{ \n  // a variable of type Food can have any of these values \n  Fish, \n  Bread, \n  Apple, \n  Orange \n}; \nint main() \n{ \n  Food food = Bread; // Change the food here \n  switch( food ) \n  { \n    case Fish: \n      cout << \"Here fishy fishy fishy\" << endl; \n      break; \n    case Bread: \n      cout << \"Chomp! Delicious bread!\" << endl; \n      break; \n    case Apple: \n      cout << \"Mm fruits are good for you\" << endl; \n      break; \n    case Orange: \n      cout << \"Orange you glad I didn't say banana\" << endl; \n      break; \n    default:  // This is where you go in case none \n              // of the cases above caught \n      cout << \"Invalid food\" << endl; \n      break; \n  } \n  return 0; \n} \n```\n\n开关就像硬币分拣机。当你把一枚 25 美分的硬币投入硬币分类机时，它会进入 25 美分的硬币堆。类似地，一个`switch`语句将简单地允许代码跳到适当的部分。下图显示了硬币分类的示例:\n\n![](img/d32684e2-0719-4159-af5b-c8cf73d1773d.png)\n\n`switch`语句中的代码将继续运行(逐行运行)，直到命中`break;`语句。`break`声明让你跳出了`switch`声明。如果您省略了`break;`语句，它将继续运行下一个案例语句中的代码，并且不会停止，直到它到达`break;`或`switch`的末尾。如果你想实验，试着拿出所有的`break;`语句，看看会发生什么！请看下图，了解`switch`的工作原理:\n\n![](img/313c6aed-604d-4a6d-a39f-ab1684cd5561.png)\n\n1.  首先，检查`Food`变量。它有什么价值？在这种情况下，它内部有`Fish`。\n2.  `switch`命令跳至正确的案例标签。(如果没有匹配的案例标签，`switch`将被跳过)。\n3.  运行`cout`语句，控制台上出现`Here fishy fishy fishy`。\n4.  检查变量并打印用户响应后，点击`break`语句。这使得我们停止在`switch`中运行代码行，并退出`switch`。正在运行的下一行代码正是如果`switch`根本不存在(在`switch`语句的右大括号之后)程序中的下一行代码。退出程序的是`return 0`。\n\n# switch 语句与 if 语句\n\n开关就像早期的`if` / `else if` / `else`链。但是，交换机生成代码的速度比`if` / `else if` / `else if` / `else`链更快。直观地说，开关只跳转到要执行的代码的适当部分。一个`if` / `else if` / `else`链可能涉及更复杂的比较(包括逻辑比较)，这可能需要更多的 CPU 时间。您将使用`if`语句的主要原因是，如果您试图检查比仅仅比较一组特定值中的某些内容更复杂的内容。\n\nAn instance of an `enum` is really an `int`. To verify this, print the following code:\n`cout << \"Fish=\" << Fish <<\n   \" Bread=\" << Bread <<\n   \" Apple=\" << Apple <<` \n` \"Orange=\" << Orange << endl;`\n\n你会看到`enum`的整数值——正如你所知。\n\n有时候，程序员想在同一个开关`case`标签下分组多个值。假设我们有一个`enum`对象，如下所示:\n\n```cpp\nenum Vegetables { Potato, Cabbage, Broccoli, Zucchini }; \n```\n\n一个程序员想把所有的绿色组织在一起，所以他们写了一个`switch`语句如下:\n\n```cpp\nVegetable veg = Zucchini;\n\nswitch( veg ) \n{ \ncase Zucchini:             // zucchini falls through because no break \ncase Broccoli:             // was written here \n  cout << \"Greens!\" << endl; \n  break; \ndefault: \n  cout << \"Not greens!\" << endl; \n  break; \n} \n```\n\n在这种情况下，`Zucchini`会失败并执行与`Broccoli`相同的代码。\n非绿色蔬菜在`default`箱标签中。为了防止失败，你必须记得在每个`case`标签后插入一个明确的`break`语句。\n\n通过在开关中明确使用`break`关键字，我们可以编写另一个版本的不会让西葫芦掉下去的开关:\n\n```cpp\nswitch( veg ) \n{ \ncase Zucchini:              // zucchini no longer falls due to break \n  cout << \"Zucchini is a green\" << endl; \n  break;// stops case zucchini from falling through \ncase Broccoli:               // was written here \n  cout << \"Broccoli is a green\" << endl; \n  break; \ndefault: \n  cout << \"Not greens!\" << endl; \n  break; \n} \n```\n\n请注意，对`break``default`情况也是很好的编程实践，即使它是列出的最后一种情况。\n\n# 锻炼\n\n完成以下程序，其中有一个`enum`对象，有一系列挂载可供选择。编写一个`switch`语句，打印所选装载的以下消息:\n\n| `Horse` | 这匹骏马骁勇善战。 |\n| `Mare` | 这匹母马又白又漂亮。 |\n| `Mule` | 你有一头骡子可以骑。你讨厌这样。 |\n| `Sheep` | 咩！羊几乎支撑不了你的体重。 |\n| `Chocobo` | 巧克力棒！ |\n\n记住，一个`enum`对象实际上是一个`int`语句。默认情况下，`enum`对象中的第一个条目是`0`，但是您可以使用`=`运算符给`enum`对象任何您想要的起始值。`enum`对象中的后续值按顺序排列。\n\n# 解决办法\n\n下面的代码显示了前面练习的解决方案:\n\n```cpp\n#include <iostream> \nusing namespace std; \nenum Mount \n{ \n  Horse=1, Mare, Mule, Sheep, Chocobo \n  // Since Horse=1, Mare=2, Mule=3, Sheep=4, and Chocobo=5\\. \n}; \nint main() \n{ \n  int mount;  // We'll use an int variable for mount \n              // so cin works \n  cout << \"Choose your mount:\" << endl; \n  cout << Horse << \" Horse\" << endl; \n  cout << Mare << \" Mare\" << endl; \n  cout << Mule << \" Mule\" << endl; \n  cout << Sheep << \" Sheep\" << endl; \n  cout << Chocobo << \" Chocobo\" << endl; \n  cout << \"Enter a number from 1 to 5 to choose a mount\" << endl; \n  cin >> mount; \n    // Describe what happens \n    // when you mount each animal in the switch below \n  switch( mount ) \n  { \n    default: \n      cout << \"Invalid mount\" << endl; \n      break; \n  } \nreturn 0; \n} \n```\n\n# 移位枚举\n\n在`enum`对象中，一个常见的做法是为每个条目分配一个移位值:\n\n```cpp\nenum   WindowProperties   \n{   \n    Bordered    = 1 << 0, // binary 001   \n    Transparent = 1 << 1, // binary 010   \n    Modal       = 1 << 2  // binary 100   \n};   \n```\n\n移位值应该能够组合窗口属性。作业是这样的:\n\n```cpp\n//   bitwise OR combines properties   \nWindowProperties   wp = Bordered | Modal;   \n```\n\n检查哪些`WindowProperties`已经被设置涉及使用`bitwise AND`的检查:\n\n```cpp\n//   bitwise AND checks to see if wp is Modal   \nif( wp   & Modal )   \n{   \n    cout << \"You are looking at a modal window\" << endl;\n}   \n```\n\nBit-shifting is a technique that is slightly beyond the scope of this book, but I've included this tip just so you know about it.\n\n# 我们关于虚幻引擎的第一个例子\n\n我们需要开始使用虚幻引擎。\n\nA word of warning: when you open your first Unreal project, you will find that the code looks very complicated. Don't get discouraged. Simply focus on the highlighted parts. Throughout your career as a programmer, you will often have to deal with very large code bases containing sections that you do not understand. However, focusing on the parts that you do understand will make this section productive.\n\n首先，你需要下载启动器来安装引擎。转到[https://www.unrealengine.com/en-US/what-is-unreal-engine-4](https://www.unrealengine.com/en-US/what-is-unreal-engine-4)，当你点击立即开始或下载时，你必须创建一个免费账户，然后才能下载启动器。\n\n下载启动器后，打开史诗游戏启动器应用。选择启动虚幻引擎 4.20.X(当你读到这个的时候可能会有一个新版本)，如下图截图所示:\n\n![](img/04daefd6-113d-4bca-b666-9978e61e8f50.png)\n\nIf you don't have the engine installed, you need to go to the Unreal Engine tab and download an engine (~7 GB).\n\n引擎启动后(可能需要几秒钟)，您将进入虚幻项目浏览器屏幕，如下图所示:\n\n![](img/2f95b850-6c0b-4430-8406-180d2d8c046c.png)\n\n现在，在 UE4 项目浏览器中选择“新建项目”选项卡。选择 C++ 选项卡并选择拼图项目。这是一个简单的项目，没有太多的代码，所以它是一个很好的开始。我们稍后将继续进行 3D 项目。\n\n在此屏幕中，需要注意以下几点:\n\n*   确保您在“新项目”选项卡中。\n*   当你点击拼图时，确保它是在 C++ 选项卡中的拼图，而不是蓝图选项卡。\n\n*   在“名称”框中输入您的项目名称`Puzzle`(这对于我稍后将提供给您的示例代码很重要)。\n*   如果您想更改存储文件夹(例如，到不同的驱动器)，请单击...按钮，以便出现浏览窗口。然后，找到要存储项目的目录。\n\n完成所有这些后，选择创建项目。\n\nNote: if it tells you it can't create the project because you do not have the Windows 8.1 SDK installed, you can download it from [https://developer.microsoft.com/en-us/windows/downloads/sdk-archive](https://developer.microsoft.com/en-us/windows/downloads/sdk-archive).\n\nVisual Studio 2017 将使用您项目的代码以及虚幻编辑器打开，如下图所示:\n\n![](img/a1f96d5d-3788-4084-af56-7b2642074bb6.png)\n\n看起来很复杂？哦，天啊，的确是！我们将在后面探讨工具栏中的一些功能。现在，只需选择播放，如前面的截图所示。\n\n这就启动了游戏。应该是这样的:\n\n![](img/a8243275-3064-4ce4-a989-5047adb97709.png)\n\n现在，试着点击方块。只要你点击一个块，它就会变成橙色，这增加了你的分数。您可以通过单击停止或按键盘上的 *Esc* 来结束您的播放会话。\n\n我们要做的是找到这样做的部分，稍微改变一下行为。\n\n找到并打开`PuzzleBlock.cpp`文件。在 C++ 类|拼图下寻找 PuzzleBlock，双击它在 IDE 中打开它。\n\nIn Visual Studio, the list of files in the project is located inside the Solution Explorer. If your Solution Explorer is hidden, simply click on View/Solution Explorer from the menu at the top.\n\n在这个文件中，向下滚动到底部，您会发现一个以下列单词开头的部分:\n\n```cpp\nvoid APuzzleBlock::BlockClicked(UPrimitiveComponent* ClickedComp, FKey ButtonClicked)\n```\n\n`APuzzleBlock`是类名(我们后面会讲到类)，而`BlockClicked`是函数名。每当点击一个拼图块，从开始的`{`到结束的`}`的代码段就会运行。希望这到底是如何发生的，以后会更有意义。\n\n这在某种程度上有点像`if`语句。如果点击了一个拼图块，那么这组代码将为该拼图块运行。\n\n我们将逐步使块在被点击时翻转颜色(因此，第二次点击会将块的颜色从橙色改回蓝色)。\n\n极其小心地执行以下步骤:\n\n1.  打开`PuzzleBlock.h`文件。在具有此代码的行之后:\n\n```cpp\n/** Pointer to blue material used on inactive blocks */\n  UPROPERTY()\n  class UMaterialInstance* BlueMaterial;\n\n  /** Pointer to orange material used on active blocks */\n  UPROPERTY()\n  class UMaterialInstance* OrangeMaterial;\n```\n\n2.  现在，打开`PuzzleBlock.cpp`文件。查找以下代码:\n\n```cpp\nBlueMaterial = ConstructorStatics.BlueMaterial.Get();\nOrangeMaterial = ConstructorStatics.OrangeMaterial.Get()\n```\n\n3.  在`PuzzleBlock.cpp`中，用以下代码替换无效的`APuzzleBlock::BlockClicked`部分代码的内容:\n\n```cpp\nvoid APuzzleBlock::BlockClicked(UPrimitiveComponent* ClickedComp, FKey ButtonClicked) \n{ \n  // --REPLACE FROM HERE-- \n  bIsActive = !bIsActive; // flip the value of bIsActive \n  // (if it was true, it becomes false, or vice versa) \n  if ( bIsActive ) \n  { \n    BlockMesh->SetMaterial(0, OrangeMaterial); \n  } \n  else \n  { \n    BlockMesh->SetMaterial(0, BlueMaterial); \n  } \n  // Tell the Grid \n  if(OwningGrid != NULL) \n  { \n    OwningGrid->AddScore(); \n  } \n  // --TO HERE-- \n}\n```\n\nOnly replace inside the `void APuzzleBlock::BlockClicked(UPrimitiveComponent* ClickedComp, FKey ButtonClicked)`statement.\nDo not replace the line that starts with `void APuzzleBlock::BlockClicked`. You might get an error (if you haven't named your project `Puzzle`). If so, you can start over by creating a new project with the correct name.\n\n按“播放”观看您的动作变化！让我们来分析一下。这是第一行代码:\n\n```cpp\nbIsActive = !bIsActive; // flip the value of bIsActive \n```\n\n这一行代码只是翻转`bIsActive`的值。`bIsActive`变量是一个`bool`变量(它是在`APuzzleBlock.h`中创建的)，它跟踪块是否处于活动状态，并且应该以橙色显示。就像扳动开关一样。如果`bIsActive`是`true`，`!bIsActive`就是`false`。因此，每当命中这一行代码时(点击任何一个块都会发生这种情况)，`bIsActive`值就会反转(从`true`到`false`或从`false`到`true`)。\n\n让我们考虑下一段代码:\n\n```cpp\nif ( bIsActive ) \n  { \n    BlockMesh->SetMaterial(0, OrangeMaterial); \n  } \n  else \n  { \n    BlockMesh->SetMaterial(0, BlueMaterial); \n  } \n```\n\n我们只是改变块的颜色。如果`bIsActive`为`true`，则该区块变为橙色。否则，区块会变成蓝色。\n\n# 摘要\n\n在本章中，您学习了如何分支代码。分支使得代码可以朝着不同的方向前进，而不是直接向下。\n\n在下一章中，我们将继续讨论一种不同的控制流语句，它允许您返回并重复一行代码一定的次数。重复的代码段将被称为循环。"
  },
  {
    "path": "docs/learn-cpp-build-game-ue4/04.md",
    "content": "# 四、循环\n\n在前一章中，我们讨论了`if`语句。`if`语句使您能够对代码块的执行设置条件。\n\n在本章中，我们将探讨循环，这是一种代码结构，使您能够在特定条件下重复一个代码块。一旦条件变为假，我们就停止重复这段代码。\n\n在本章中，我们将探讨以下主题:\n\n*   while 循环\n*   do/while 循环\n*   for 循环\n*   虚幻引擎中一个实际循环的简单例子\n\n# while 循环\n\n`while`循环用于重复运行一段代码。如果您有一组必须重复执行才能完成某个目标的操作，这将非常有用。例如，以下代码中的`while`循环重复打印变量`x`的值，因为它从`1`增加到 5:\n\n```cpp\nint x = 1; \nwhile( x <= 5 ) // may only enter the body of the while when x<=5 \n{ \n  cout << \"x is \" << x << endl; \n  x++ ; \n} \ncout << \"Finished\" << endl; \n```\n\n这是前面程序的输出:\n\n```cpp\nx is 1 \nx is 2 \nx is 3 \nx is 4 \nx is 5 \nFinished \n```\n\n在第一行代码中，创建了一个整数变量`x`，并将其设置为`1`。然后，我们去`while`境。`while`条件表示当`x`小于或等于`5`时，您必须留在后面的代码块中。\n\n循环的每次迭代(一次迭代意味着执行一次`{`和`}`之间的所有事情)从任务(打印数字`1`到`5`)中完成得更多一点。一旦任务完成(当`x <= 5`不再正确时)，我们将循环编程为自动退出。\n\n与前一章的`if`语句类似，只有当您满足`while`循环(在前面的示例中为`x <= 5`)括号内的条件时，`while`循环才允许进入后面的块。你可以试着用一个`if`循环代替`while`循环，如下面的代码所示:\n\n```cpp\nint x = 1; \nif( x <= 5 ) // you may only enter the block below when x<=5 \n{ \n  cout << \"x is \" << x << endl; \n  x = x + 1; \n} \ncout << \"End of program\" << endl; \n```\n\n前面的代码示例只打印`x is 1`。因此，`while`循环完全像一个`if`语句，只是它有自动重复自身的特殊属性，直到`while`循环括号之间的条件变为假。\n\nI'd like to explain the repetition of the `while` loop using a video game. If you don't know Valve's *Portal*, you should play it, if only to understand loops. Check out [https://www.youtube.com/watch?v=TluRVBhmf8w](https://www.youtube.com/watch?v=TluRVBhmf8w) for a demo video.\n\n`while`循环在底部有一种魔法入口，这导致循环重复。下面的截图说明了我的意思:\n\n![](img/f688cd95-3a2c-4e18-91c1-a54c37fe99dd.png)\n\nThere is a portal at the end of the while loop that takes you back to the beginning\n\n在前面的截图中，我们从橙色入口(标记为`O`)返回到蓝色入口(标记为`B`)。这是我们第一次能够回到代码中。这就像时间旅行，只是为了代码。多刺激啊！\n\n通过`while`循环块的唯一方法是不满足进入条件。在前面的例子中，一旦`x`的值变为 6(因此`x <= 5`变为假)，我们将不会再次进入`while`循环。因为橙色的入口在循环内，一旦`x`变成 6，我们就能退出循环。\n\n# 无限循环\n\n你会永远困在同一个循环里。考虑以下代码块中的修改程序(您认为输出会是什么？):\n\n```cpp\nint x = 1; \nwhile( x <= 5 ) // may only enter the body of the while when x<=5 \n{ \n  cout << \"x is \" << x << endl; \n} \ncout << \"End of program\" << endl; \n```\n\n输出将是这样的:\n\n```cpp\nx is 1 \nx is 1 \nx is 1 \n. \n. \n. \n(repeats forever) \n```\n\n循环永远重复，因为我们删除了改变`x`值的代码行。如果`x`的值保持不变，不允许增加，我们就会卡在`while`回路的体内。这是因为如果`x`在环体内没有变化，则不能满足环的退出条件(`x`的值变为 6)。\n\nJust click the x button on the window to close the program.\n\n以下练习将使用前几章的所有概念，如`+=`和减量操作。如果你忘记了什么，回去重读前面几节。\n\n# 练习\n\n让我们来看几个练习:\n\n1.  写一个`while`循环，打印从`1`到`10`的数字\n2.  写一个`while`循环，打印从 10 到 1(向后)的数字\n3.  编写一个`while`循环，打印数字 2 到 20，递增 2(例如 2、4、6 和 8)\n4.  写一个`while`循环，打印数字 1 到 16 以及它们旁边的方块\n\n以下是练习 4 的示例程序输出:\n\n| `1` | `1` |\n| `2` | `4` |\n| `3` | `9` |\n| `4` | `16` |\n| `5` | `25` |\n\n# 解决方法\n\n前面练习的代码解决方案如下:\n\n1.  打印从`1`到`10`的数字的`while`循环的解决方案如下:\n\n```cpp\nint x = 1; \nwhile( x <= 10 ) \n{ \n  cout << x << endl; \n  x++ ; \n}\n```\n\n2.  将数字从`10`向后打印到`1`的`while`循环的解决方案如下:\n\n```cpp\nint x = 10; // start x high \nwhile( x >= 1 ) // go until x becomes 0 or less \n{ \n  cout << x << endl; \n  x--; // take x down by 1 \n} \n```\n\n3.  将从`2`到`20`的数字递增`2`的`while`循环的解决方案如下:\n\n```cpp\nint x = 2; \nwhile( x <= 20 ) \n{ \n  cout << x << endl; \n  x+=2; // increase x by 2's \n} \n```\n\n4.  打印从`1`到`16`的数字及其方块的`while`循环的解决方案如下:\n\n```cpp\nint x = 1; \nwhile( x <= 16 ) \n{ \n  cout << x << \"   \" << x*x << endl; // print x and it's  \n   square \n  x++ ; \n} \n```\n\n# do/while 循环\n\n`do` / `while`循环几乎与`while`循环相同。这里有一个`do` / `while`循环的例子，它相当于我们检查的第一个`while`循环:\n\n```cpp\nint x = 1; \ndo \n{ \n  cout << \"x is \" << x << endl; \n  x++ ; \n} while( x <= 5 ); // may only loop back when x<=5 \ncout << \"End of program\" << endl; \n```\n\n这里唯一的区别是，我们不必在第一次进入循环时检查`while`条件。这意味着`do` / `while`循环的主体总是至少被执行一次(其中`while`循环可以被完全跳过，如果当你第一次击中它时进入 while `loop`的条件为假)。\n\n这里有一个例子:\n\n```cpp\nint val = 5;\nwhile (val < 5)\n{\n    cout << \"This will not print.\" << endl;\n}\ndo {\n    cout << \"This will print once.\" << endl;\n} while (val < 5);\n```\n\n# for 循环\n\n`for`环的解剖结构与`while`环略有不同，但两者非常相似。\n\n让我们来研究一下`for`环与等效`while`环的解剖结构。以下面的代码片段为例:\n\n| `for`循环 | 等效的`while`循环 |\n| for(int x = 1；x < = 5；x++){层< < x <} | int x = 1;而(x <= 5){层< < x <x++；} |\n\n`for`循环的括号内有三个语句。让我们按顺序检查它们。\n\n`for`循环(`int x = 1;`)的第一个语句只执行一次，当我们第一次进入`for`循环的主体时。它通常用于初始化循环的计数器变量的值(在本例中是变量`x`)。`for`循环(`x <= 5;`)中的第二个语句是循环的重复条件。只要`x <= 5`，我们就必须继续留在`for`循环的体内。`for`循环(`x++ ;`)括号内的最后一条语句在我们每次完成`for`循环的主体后执行。\n\n下图说明了`for`循环的进程:\n\n![](img/de84ff9a-68b1-49b7-b067-c112b9c42efd.png)\n\n# 练习\n\n让我们看看这里的一些练习:\n\n1.  写一个`for`循环，收集从`1`到`10`的数字总和\n2.  写一个`for`循环，打印`6`的倍数，从`6`到`30` (6，12，18，24，30)\n3.  写一个`for`循环，以`2`的倍数打印数字 2 到 100(例如 2、4、6、8 等等)\n4.  写一个`for`循环，将数字`1`打印到`16`及其旁边的方块\n\n# 解决方法\n\n以下是前面练习的解决方案:\n\n1.  打印从`1`到`10`的数字总和的`for`循环的解决方案如下:\n\n```cpp\nint sum = 0; \nfor( int x = 1; x <= 10; x++ ) \n{ \n  sum += x; \n} \ncout << sum << endl; \n```\n\n2.  从`6`到`30`打印`6`倍数的`for`循环的解决方案如下:\n\n```cpp\nfor( int x = 6; x <= 30; x += 6 ) \n{ \n  cout << x << endl; \n} \n```\n\n3.  以`2`的倍数从`2`到`100`打印数字的`for`循环的解决方案如下:\n\n```cpp\nfor( int x = 2; x <= 100; x += 2 ) \n{ \n  cout << x << endl; \n}\n```\n\n4.  打印从`1`到`16`的数字及其方块的`for`循环的解决方案如下:\n\n```cpp\nfor( int x = 1; x <= 16; x++ ) \n{ \n  cout << x << \" \" << x*x << endl; \n} \n```\n\n# 用虚幻引擎循环\n\n在你的代码编辑器中，从[第三章](03.html)、 *If、Else 打开你的虚幻`Puzzle`项目，然后切换*。\n\n有几种方法可以打开您的虚幻项目。在 Windows 上，最简单的方法可能是导航到`Unreal Projects`文件夹(默认情况下位于用户的`Documents`文件夹中)，然后在 Windows 资源管理器中双击`.sln`文件，如下图所示:\n\n![](img/4f035cee-9c4b-43d3-806e-b5ea1e5394fa.png)\n\n在 Windows 中，打开`.sln`文件编辑项目代码。您也可以只打开 Visual Studio，它会记住您最近处理的项目并显示它们，以便您可以从那里单击它来打开它。您还需要从史诗游戏启动器在虚幻编辑器中打开项目来测试它。\n\n现在，打开`PuzzleBlockGrid.cpp`文件。在该文件中，向下滚动到以以下语句开头的部分:\n\n```cpp\nvoid APuzzleBlockGrid::BeginPlay() \n```\n\n请注意，这里有一个`for`循环来产生最初的九个块，如以下代码所示:\n\n```cpp\n// Loop to spawn each block \nfor( int32 BlockIndex=0; BlockIndex < NumBlocks; BlockIndex++ ) \n{ \n  // ... \n} \n```\n\n由于`NumBlocks`(用于确定何时停止循环)被计算为`Size*Size`，我们可以通过改变`Size`变量的值来轻松改变产生的块数。转到`PuzzleBlockGrid.cpp`的第 24 行，将`Size`变量的值更改为`4`或`5`。然后，再次运行代码(确保在虚幻编辑器中按下编译按钮，使其使用更新的代码)。\n\n您应该会看到屏幕上的块数增加(尽管您可能需要滚动才能全部看到)，如下图所示:\n\n![](img/9c230a33-a53b-4439-a60b-7f5fb96d89c2.png)\n\n将大小设置为`14`会创建更多的块。\n\n# 摘要\n\n在本章中，您学习了如何通过循环代码来重复代码行，这允许您多次运行它。这可以用来重复使用同一行代码来完成任务。想象一下从`1`到`10`打印数字(或者 10000！)而不使用循环。\n\n在下一章中，我们将探索函数，函数是可重用代码的基本单元。"
  },
  {
    "path": "docs/learn-cpp-build-game-ue4/05.md",
    "content": "# 五、函数和宏\n\n编写代码时，您会发现自己需要多次运行相同的代码。你最不想做的事情就是在一堆不同的地方复制和粘贴相同的代码(毕竟，如果你需要做一个改变会发生什么？).写一次叫多次不是更容易吗？这就是我们在这一章要讲的内容。我们将讨论的主题包括:\n\n*   功能\n*   带参数的函数\n*   返回值的函数\n*   初始化列表\n*   更多变量\n*   宏指令\n*   康斯特布尔\n\n# 功能\n\n有些事情需要重复。代码不是其中之一。一个函数是一堆代码，可以被调用任意多次，就像你经常希望的那样。\n\n类比是好的。让我们探索一个涉及服务员、厨师、比萨饼和功能的类比。在英语中，当我们说一个人有功能时，我们的意思是这个人执行一些非常具体(通常非常重要)的任务。每当他们被要求这样做时，他们可以一次又一次地完成这项任务。\n\n下面的连环画展示了服务员(呼叫者)和厨师(被呼叫者)之间的互动。服务员想为他的桌子准备食物，所以他叫厨师准备等候的桌子需要的食物。厨师准备食物，然后将结果返回给服务员:\n\n![](img/fc74aa10-910d-4458-9ff7-f6a46c30b1c7.png)\n\n在这里，厨师履行烹饪食物的职责。厨师接受了关于烹饪哪种食物的参数(三个意大利辣香肠馅饼)。然后厨师走了，做了一些工作，带着三个比萨饼回来了。请注意，服务员不知道也不关心厨师如何烹饪比萨饼。厨师为服务员抽象出做披萨的过程，所以做披萨对服务员来说只是一个简单的单行命令。服务员只是想完成他的要求，把比萨饼还给他。\n\n当一个函数(厨师)被调用了一些参数(要准备的比萨饼的类型)时，这个函数会执行一些操作(准备比萨饼)并可选地返回一个结果(实际完成的比萨饼)。\n\n# 库函数示例–sqrt()\n\n现在，让我们谈一个更实际的例子，并把它与比萨饼的例子联系起来。\n\n`<cmath>`库中有一个函数叫做`sqrt()`函数。让我快速说明它的用法，如下面的代码所示:\n\n```cpp\n#include <iostream> \n#include <cmath> \nusing namespace std; \nint main() \n{ \n  double rootOf5 = sqrt( 5 ); // function call to the sqrt  \n   function \n  cout << rootOf5  << endl; \n} \n```\n\n函数调用在`=`字符后:`sqrt( 5 )`。所以，`sqrt()`可以求出给它的任何数的数学平方根。\n\n你知道如何求一个棘手数字的平方根吗，比如 5？这并不简单。一个聪明的灵魂坐下来写了一个函数，可以找到所有类型的数字的平方根。使用`sqrt(5)`函数调用如何求 5 的平方根，你一定要理解背后的数学吗？见鬼，不！所以，就像服务员不一定要了解如何烹饪比萨饼才能得到比萨饼一样，C++ 库函数的调用者也不一定要完全了解库函数内部是如何工作的，才能有效地使用它。\n\n以下是使用函数的优点:\n\n*   函数将复杂的任务抽象成简单的、可调用的例程。这使得*做披萨*所需的代码，例如，只是调用者的一个单行命令(调用者通常是你的程序)。\n*   函数避免了不必要的代码重复。假设我们有 20 行左右的代码可以找到一个双精度值的平方根。我们将这些代码行包装成一个可调用的函数；我们不需要重复复制粘贴这 20 行代码，只需要在需要根的时候调用`sqrt`函数(数字为 root)。\n\n下图显示了求平方根的过程:\n\n![](img/59897c3a-4d94-4e10-b59b-a99f766f8a8f.png)\n\n# 编写我们自己的函数\n\n假设我们想编写一些代码，打印出一条道路，如下所示:\n\n```cpp\ncout << \"*   *\" << endl; \ncout << \"* | *\" << endl; \ncout << \"* | *\" << endl; \ncout << \"*   *\" << endl; \n```\n\n现在，假设我们想连续打印两条路，或者三条路。或者，假设我们想要打印任意数量的道路带。我们将不得不对我们试图打印的每一条道路重复生成第一条道路的四行代码。\n\n如果我们引入自己的 C++ 命令，允许我们在调用命令时打印一条道路，会怎么样？看起来是这样的:\n\n```cpp\nvoid printRoad() \n{ \n  cout << \"*   *\" << endl; \n  cout << \"* | *\" << endl; \n  cout << \"* | *\" << endl; \n  cout << \"*   *\" << endl; \n} \n```\n\n这是函数的定义。C++ 函数有以下结构:\n\n![](img/0bbc34fb-1b86-45cd-9566-5d90babc62e7.png)\n\n`void`表示它不返回任何值，并且由于括号内没有任何内容，因此它不接受任何参数。稍后我们将讨论参数和返回值。使用函数很简单:我们只需通过名称调用我们想要执行的函数，后跟两个圆括号，`()`。例如，调用`printRoad()`函数将导致`printRoad()`函数运行。让我们追踪一个示例程序来完全理解这意味着什么。\n\n# 示例程序跟踪\n\n下面是函数调用工作原理的完整示例:\n\n```cpp\n#include <iostream> \nusing namespace std; \nvoid printRoad() \n{ \n  cout << \"*   *\" << endl; \n  cout << \"* | *\" << endl; \n  cout << \"* | *\" << endl; \n  cout << \"*   *\" << endl; \n} \nint main() \n{ \n  cout << \"Program begin!\" << endl; \n  printRoad(); \n  cout << \"Program end\" << endl; \n  return 0; \n} \n```\n\n让我们从头到尾跟踪程序的执行。请记住，对于所有 C++ 程序，执行从`main()`的第一行开始。\n\n`main()` is also a function. It oversees the execution of the whole program. Once `main()` executes the `return` statement, your program ends.\n\n前面程序执行的逐行跟踪如下所示:\n\n```cpp\nvoid printRoad() \n{ \n  cout << \"*   *\" << endl;          // 3: then we jump up here \n  cout << \"* | *\" << endl;          // 4: run this \n  cout << \"* | *\" << endl;          // 5: and this \n  cout << \"*   *\" << endl;          // 6: and this \n} \nint main() \n{ \n  cout << \"Program begin!\" << endl; // 1: first line to execute \n  printRoad();                      // 2: second line.. \n  cout << \"Program end\" << endl;    // 7: finally, last line \n  return 0;                         // 8: and return to o/s \n} \n```\n\n这个程序的输出是这样的:\n\n```cpp\nProgram begin! \n*   * \n* | * \n* | * \n*   * \nProgram end \n```\n\n下面是对前面代码的逐行解释:\n\n1.  程序的执行从`main()`的第一行开始，输出`program begin!`。\n2.  运行的下一行代码是对`printRoad()`的调用。这是把程序计数器跳到`printRoad()`的第一行。`printRoad()`的所有行按顺序执行(第 3-6 行)。\n3.  对`printRoad()`的函数调用完成后，控制返回到`main()`语句。我们接着看到`Program end`印出来了。\n\nDon't forget the brackets after the function call to `printRoad()`. A function call must always be followed by round brackets, `()`, otherwise the function call will not work and you will get a compiler error.\n\n以下代码用于打印四条道路:\n\n```cpp\nint main() \n{ \n        printRoad(); \n        printRoad(); \n        printRoad(); \n        printRoad(); \n} \n```\n\n或者，您也可以使用以下代码:\n\n```cpp\nfor( int i = 0; i < 4; i++ ) \n{\n    printRoad();\n}\n```\n\n所以，我们不是每次打印一个盒子都重复`cout`的四行，而是简单的调用`printRoad()`函数让它打印出来。此外，如果我们想改变印刷道路的外观，我们必须简单地修改`printRoad()`功能的实现。\n\n调用一个函数需要一行一行地运行该函数的整个主体。函数调用完成后，程序的控制将在函数调用点恢复。\n\n# 锻炼\n\n作为练习，找出以下代码有什么问题:\n\n```cpp\n#include <iostream> \nusing namespace std; \nvoid myFunction() \n{ \n   cout << \"You called?\" << endl; \n} \nint main() \n{ \n   cout << \"I'm going to call myFunction now.\" << endl; \n   myFunction; \n} \n```\n\n# 解决办法\n\n这个问题的正确答案是对`myFunction`(在`main()`的最后一行)的调用后面没有圆括号。所有函数调用都必须后跟圆括号。`main()`的最后一行应该是`myFunction();`，而不仅仅是`myFunction`。\n\n# 带参数的函数\n\n如何扩展`printRoad()`功能，打印出有一定段数的道路？答案很简单。我们可以让`printRoad()`函数接受一个名为`numSegments`的参数，来打印一定数量的路段。\n\n下面的代码片段显示了这将如何出现:\n\n```cpp\nvoid printRoad(int numSegments) \n{ \n  // use a for loop to print numSegments road segments \n  for( int i = 0; i < numSegments; i++) \n  { \n    cout << \"*   *\" << endl; \n    cout << \"* | *\" << endl; \n    cout << \"* | *\" << endl; \n    cout << \"*   *\" << endl; \n  } \n} \n```\n\n下面的截图显示了接受参数的函数的结构:\n\n![](img/696b374e-dfb2-4f95-b0c2-ce4c02b8ed63.png)\n\n调用这个新版本的`printRoad()`，要求它打印四段，如下:\n\n```cpp\nprintRoad( 4 );    // function call \n```\n\n前面语句中`function call`括号之间的值`4`被分配给`printRoad(int numSegments)`函数的`numSegments`变量。这就是值`4`传递到`numSegments`的方式:\n\n![](img/6b5ac394-8767-4232-9b36-27f662031484.png)\n\nAn illustration of how printRoad(4) will assign the value 4 to the numSegments variable\n\n因此，`numSegments`将调用\n中括号之间传递的值赋给`printRoad()`。\n\n# 返回值的函数\n\n一个返回值的函数的例子是`sqrt()`函数。`sqrt()`函数接受括号中的单个参数(要求根的数字)，并返回该数字的实际根。\n\n这里有一个使用`sqrt`函数的例子:\n\n```cpp\ncout << sqrt( 4 ) << endl; \n```\n\n`sqrt()`功能的作用类似于厨师在准备比萨饼时所做的。\n\n作为函数的调用者，你不关心`sqrt()`函数体内发生了什么；这个信息是不相关的，因为你想要的只是你传递的数字的平方根的结果。\n\n让我们声明自己的简单的返回值函数，如下面的代码所示:\n\n```cpp\nint sum(int a, int b) \n{ \n  return a + b; \n} \n```\n\n下面的屏幕截图显示了带有参数和返回值的函数的结构:\n\n![](img/ac3d5a85-c21c-45c1-b467-d1a6eb6f54e6.png)\n\n`sum`功能非常基础。它所做的只是取两个`int`数字，`a`和`b`，将它们相加，并返回一个结果。你可能会说，我们甚至不需要一个完整的函数来加两个数字。你说得对，但请稍等。我们将使用这个简单的函数来解释返回值的概念。\n\n您将以这种方式使用`sum`功能(从`main()`开始):\n\n```cpp\nint sum( int a, int b ) \n{ \n  return a + b; \n} \nint main() \n{ \n  cout << \"The sum of 5 and 6 is \" << sum( 5,6 ) << endl;  \n} \n```\n\n要完成`cout`命令，必须评估`sum( 5,6 )`函数调用。在发生`sum( 5,6 )`函数调用的地方，从`sum( 5,6 )`返回的值就放在那里。\n\n换句话说，这是`cout`在评估`sum( 5,6 )`函数调用后实际看到的代码行:\n\n```cpp\ncout << \"The sum of 5 and 6 is \" << 11 << endl;     \n```\n\n从`sum( 5,6 )`返回的值被有效地剪切并粘贴到函数调用点。一个值必须总是由一个承诺这样做的函数返回(如果该函数的返回类型不是`void`)。\n\n# 练习\n\n1.  写一个`isPositive`函数，当传递给它的双参数确实为正时，返回`true`。\n2.  完成以下功能定义:\n\n```cpp\n// function returns true when the magnitude of 'a' \n// is equal to the magnitude of 'b' (absolute value) \nbool absEqual(int a, int b)\n { \n    // to complete this exercise, try to not use \n    // cmath library functions \n}\n```\n\n3.  编写一个接受整数值(满分 100 分)并返回等级的`getGrade()`函数(可以是`A`、`B`、`C`、`D`或`F`)。\n4.  数学函数的形式是`f(x) = 3x + 4`。编写一个为`f(x)`返回值的 C++ 函数。\n\n# 解决方法\n\n1.  `isPositive`函数接受一个双参数并返回一个布尔值:\n\n```cpp\nbool isPositive( double value ) \n{ \n  return value > 0; \n} \n```\n\n2.  以下是已完成的`absEqual`功能:\n\n```cpp\nbool absEqual( int a, int b ) \n{ \n  // Make a and b positive \n  if( a < 0 ) \n  {\n    a = -a;\n  } \n  if( b < 0 ) \n  {\n    b = -b; \n  }\n  // now since they're both +ve, \n  // we just have to compare equality of a and b together \n  return a == b; \n} \n```\n\n3.  `getGrade()`功能由以下代码给出:\n\n```cpp\nchar getGrade( int grade ) \n{ \n  if( grade >= 90 )\n  { \n    return 'A'; \n  }\n  else if( grade >= 80 ) \n  {\n    return 'B'; \n  }\n  else if( grade >= 70 ) \n  {\n    return 'C'; \n  }\n  else if( grade >= 60 ) \n  {\n    return 'D'; \n  }\n  else \n  {\n    return 'F'; \n  }\n} \n```\n\n4.  这个节目很简单，应该能让你开心。C++ 中名字函数的起源实际上来自数学世界，如下面的代码所示:\n\n```cpp\ndouble f( double x ) \n{ \n  return 3*x + 4; \n} \n```\n\n# 初始化列表\n\n有时，您可能不知道要向数组传递多少项。较新版本的 C++ 增加了一种简单的方法，初始化列表。这允许您在花括号内传递任意数量的项目，并用逗号分隔，如下所示:\n\n```cpp\n{ 1, 2, 3, 4 }\n```\n\n要进行设置，您需要使用`initializer_list`作为类型:\n\n```cpp\n#include <initializer_list>\nusing namespace std;\n\nint sum(initializer_list<int> list) {\n    int total = 0;\n    for (int e : list) { // Iterate through the list\n        total += e;\n    }\n\n    return total;\n}\n```\n\n这是一个模板，我们将在后面讨论，但是现在你需要知道的是你放在列表中的对象的类型是在尖括号内，像这样:`<int>`。这很可能是另一种类型，如`float`或`char`。\n\n要调用该函数，可以传入如下值:\n\n```cpp\nsum({ 1, 2, 3, 4 });\n```\n\n对于这种情况，结果将是`10`。\n\n# 重新审视变量\n\n既然您对 C++ 编码有了更深入的理解，那么重温您以前讨论过的主题总是很好的。\n\n# 全局变量\n\n既然我们已经引入了函数的概念，那么就可以引入全局变量的概念了。\n\n什么是全局变量？全局变量是程序的所有函数都可以访问的任何变量。我们如何使程序的所有函数都可以访问一个变量？我们只需在代码文件的顶部声明全局变量，通常在`#include`语句之后或附近。\n\n下面是一个带有一些全局变量的示例程序:\n\n```cpp\n#include <iostream> \n#include <string> \nusing namespace std; \n\nstring g_string;             // global string variable, \n// accessible to all functions within the program \n// (because it is declared before any of the functions \n// below!) \n\nvoid addA(){ g_string += \"A\"; } \nvoid addB(){ g_string += \"B\"; } \nvoid addC(){ g_string += \"C\"; } \n\nint main() \n{ \n  addA(); \n  addB(); \n  cout << g_string << endl; \n  addC(); \n  cout << g_string << endl; \n} \n```\n\n在这里，程序中的所有四个函数都可以访问同一个`g_string`全局变量(`addA()`、`addB()`、`addC()`和`main()`)。全局变量在程序期间一直存在。\n\nPeople sometimes prefer to prefix global variables with `g_`, but prefixing the variable name with `g_` is not a requirement for a variable to be global.\n\n# 局部变量\n\n局部变量是在代码块中定义的变量。局部变量在声明它们的块的末尾超出范围。一些例子将在下一节中跟随**范围的一个变量*。*\n\n *# 变量的范围\n\n变量的范围是可以使用该变量的代码区域。任何变量的范围基本上都是定义它的块。我们可以用一个例子演示变量的作用域，如下面的代码所示:\n\n```cpp\nint g_int; // global int, has scope until end of file \nvoid func( int arg ) \n{ \n  int fx; \n} // </fx> dies, </arg> dies \n\nint main() \n{ \n  int x = 0; // variable <x> has scope starting here.. \n         // until the end of main() \n  if( x == 0 ) \n  { \n    int y;  // variable <y> has scope starting here, \n            // until closing brace below \n  } // </y> dies \n  if( int x2 = x ) // variable <x2> created and set equal to <x> \n  { \n    // enter here if x2 was nonzero \n  } // </x2> dies \n\n  for( int c = 0; c < 5; c++ ) // c is created and has \n  { // scope inside the curly braces of the for loop \n    cout << c << endl; \n  } // </c> dies only when we exit the loop \n} // </x> dies \n```\n\n定义变量范围的主要东西是块。让我们讨论一下前面代码示例中定义的几个变量的范围:\n\n*   `g_int`:这是一个全局整数，它的作用域范围是从它被声明的那一点一直到代码文件的末尾。也就是说，`g_int`可以在`func()`和`main()`内部使用，但不能在其他代码文件中使用。要有一个跨多个代码文件使用的全局变量，您需要一个外部变量。\n*   `arg`(第`func()`的论点):这个可以从第一行的`func()`(左花括号后，`{`)到最后一行的`func()`(直到右花括号，`}`)使用。\n*   `fx`:这个可以在`func()`里面的任何地方使用，直到`func()`的右花括号(`}`)。\n*   `main()`(在`main()`内部的变量):这可以在评论中标记使用。\n\n请注意，在函数参数列表的括号中声明的变量只能在该函数声明下面的块中使用，例如，传递给`func()`的`arg`变量:\n\n```cpp\nvoid func( int arg ) \n{ \n  int fx; \n} // </fx> dies, </arg> dies \n```\n\n`arg`变量将在`func()`函数的右花括号(`}`)后消失。这是违背直觉的，因为圆括号在技术上不在定义`{`区块`}`的花括号内。\n\n在`for`循环的圆括号内声明的变量也是如此。以下面的`for`循环为例:\n\n```cpp\nfor( int c = 0; c < 5; c++ ) \n{ \n  cout << c << endl; \n} // c dies here \n```\n\n`int c`变量可以用在`for`循环声明的圆括号内，也可以用在`for`循环声明下面的块内。`c`变量将在其声明所在的`for`循环的大括号结束后消失。如果希望`c`变量在`for`循环的括号后继续存在，需要在`for`循环之前声明`c`变量，如下图所示:\n\n```cpp\nint c; \nfor( c = 0; c < 5; c++ ) \n{ \n  cout << c << endl; \n} // c does not die here \n```\n\n# 静态局部变量\n\n`static`局部变量有局部作用域，但退出函数时不会消失，而是记住调用之间的值，如下面的代码所示:\n\n```cpp\nvoid testFunc() \n{ \n  static int runCount = 0; // this only runs ONCE, even on \n  // subsequent calls to testFunc()! \n  cout << \"Ran this function \" << ++ runCount << \" times\" << endl; \n} // runCount stops being in scope, but does not die here \n\nint main() \n{ \n  testFunc();  // says 1 time \n  testFunc();  // says 2 times! \n} \n```\n\n通过使用`testFunc()`中的`static`关键字，`runCount`变量会在`testFunc()`的两次调用之间记住它的值。因此，`testFunc()`前面两个独立运行的输出如下:\n\n```cpp\nRan this function 1 times \nRan this function 2 times \n```\n\n这是因为静态变量只创建和初始化一次(第一次在函数中声明它们时)，之后，静态变量保留其旧值。假设我们将`runCount`声明为一个正则的、局部的、非静态的变量:\n\n```cpp\nint runCount = 0; // if declared this way, runCount is local \n```\n\n然后，输出会是这样的:\n\n```cpp\nRan this function 1 times \nRan this function 1 times \n```\n\n在这里，我们看到`testFunc`两次都说`Ran this function 1 time`。作为局部变量，`runCount`的值不会在函数调用之间保留。\n\n您不应该过度使用静态局部变量。一般来说，只有在绝对必要时，才应该使用静态局部变量。\n\n# 常量变量\n\n`const`变量是一个变量，它的值你承诺编译器在第一次初始化后不会改变。我们可以简单地声明一个，例如`pi`的值:\n\n```cpp\nconst double pi = 3.14159; \n```\n\n由于`pi`是一个普适常数(你可以依赖的为数不多的一样东西之一)，所以初始化后应该没有必要改变`pi`。事实上，对`pi`的修改应该被编译器禁止。例如，尝试为`pi`分配一个新值:\n\n```cpp\npi *= 2; \n```\n\n我们将得到以下编译器错误:\n\n```cpp\nerror C3892: 'pi' : you cannot assign to a variable that is const \n```\n\n这个错误非常有意义，因为除了初始化之外，我们不应该能够改变`pi`的值——一个常量变量。\n\n# 常量和函数\n\n`const`可以有多种用法，其中有些涉及函数。有时，您将变量传递给函数，但不希望函数对值进行任何更改。你可能会认为，我可以确保我不会改变它，不是吗？在你自己的项目中可能是这样，但是如果你在一个有多个程序员的大团队中呢？你可以只放一个注释，但是通常最好确保参数被标记为`const`。为此，您可以这样编写函数:\n\n```cpp\nint sum(const int x, const int y)\n{\n    return x + y;\n}\n```\n\n现在，如果您试图更改这些值中的任何一个，都会导致错误。例如，这是行不通的:\n\n```cpp\nint sum(const int x, const int y)\n{\n    x = x + y; //ERROR!\n    return x;\n}\n```\n\n您也可以通过将其更改为类似以下内容来返回常数值:\n\n```cpp\nconst int returnConst()\n```\n\n只要确保将函数返回的值保存在一个也标记为`const`的变量中，否则会出现错误。\n\n# 功能原型\n\n函数原型是没有主体的函数的签名。例如，让我们从以下练习中原型化`isPositive`、`absEqual`和`getGrade`功能:\n\n```cpp\nbool isPositive( double value ); \nbool absEqual( int a, int b ); \nchar getGrade( int grade ); \n```\n\n注意函数原型只是函数需要的返回类型、函数名和参数列表。功能原型没有身体。函数的主体通常放在`.cpp`文件中。\n\n# 。h 和。cpp 文件\n\n典型的做法是将你的函数原型放在`.h`文件中，将函数的主体放在`.cpp`文件中。这是因为您可以将您的`.h`文件包含在一堆`.cpp`文件中，而不会出现多个定义错误。\n\n下面的截图清晰地展示了`.h`和`.cpp`文件，显示了主代码和函数的`.cpp`文件，以及保存函数原型的`.h`文件:\n\n![](img/95086156-bec4-4b0d-bb04-96ccf2f21185.jpg)\n\n这里，我们在这个 Visual C++ 项目中有三个文件:\n\n![](img/fe37fba4-9f16-44bf-a7a6-42836edb896e.png)\n\n# 原型\n\n`prototypes.h`文件包含功能原型。我们稍后将解释`extern`关键字的作用:\n\n```cpp\n// Make sure these prototypes are \n// only included in compilation ONCE \n#pragma once \nextern int superglobal; // extern: variable \"prototype\" \n// function prototypes \nbool isPositive( double value ); \nbool absEqual( int a, int b ); \nchar getGrade( int grade ); \n```\n\n# 功能.cpp\n\n以下是`funcs.cpp`的内容:\n\n```cpp\n#include \"prototypes.h\" // every file that uses isPositive, \n// absEqual or getGrade must #include \"prototypes.h\" \nint superglobal; // variable \"implementation\" \n// The actual function definitions are here, in the .cpp file \nbool isPositive( double value ) \n{ \n  return value > 0; \n} \nbool absEqual( int a, int b ) \n{ \n  // Make a and b positive \n  if( a < 0 ) \n  {\n    a = -a; \n  }\n  if( b < 0 ) \n  {\n    b = -b; \n  }\n  // now since they're both +ve, \n  // we just have to compare equality of a and b together \n  return a == b; \n} \nchar getGrade( int grade ) \n{ \n  if( grade >= 90 ) \n  {\n    return 'A'; \n  }\n  else if( grade >= 80 ) \n  {\n    return 'B'; \n  }\n  else if( grade >= 70 ) \n  {\n    return 'C'; \n  }\n  else if( grade >= 60 ) \n  {\n    return 'D'; \n  }\n  else \n  {\n    return 'F'; \n  }\n} \n```\n\n# main.cpp\n\n以下是`main.cpp`的内容:\n\n```cpp\n #include <iostream> \nusing namespace std; \n#include \"prototypes.h\" // for use of isPositive, absEqual  \n// functions \nint main() \n{ \n  cout << boolalpha << isPositive( 4 ) << endl; \n  cout << absEqual( 4, -4 ) << endl; \n} \n```\n\n当你把代码拆分成`.h`和`.cpp`文件时，`.h`文件(头文件)称为接口，`.cpp`文件(包含实际功能的文件)称为实现。\n\n对于一些程序员来说，一开始令人困惑的是，如果我们只有原型，C++ 怎么知道`isPositive`和`getGrade`函数体在哪里？难道我们不应该把`funcs.cpp`文件也放入`main.cpp`吗？\n\n答案是*魔法*。您只需要在`main.cpp`和`funcs.cpp`中`#include`到`prototypes.h`头文件。只要这两个`.cpp`文件都包含在您的 C++ **集成开发环境** ( **IDE** )项目中(即它们出现在左侧的解决方案资源管理器树视图中)，原型到函数体的链接就由编译器自动完成。\n\n# 外部变量\n\n一个`extern`声明类似于一个函数原型，只是它用在一个变量上。您可以将`extern`全局变量声明放在一个`.h`文件中，并将这个`.h`文件包含在一大堆其他文件中。这样，您可以拥有一个跨多个源文件共享的全局变量，而不会出现多重定义符号发现链接器错误。你应该把实际的变量声明放在一个`.cpp`文件中，这样变量只被声明一次。在上例的`prototypes.h`文件中有一个`extern`变量。\n\n# 宏指令\n\nC++ 宏来自一类称为预处理器指令的 C++ 命令。预处理器指令在编译发生之前执行。宏以`#define`开始。例如，假设我们有以下宏:\n\n```cpp\n#define PI 3.14159 \n```\n\n在最底层，宏只是在编译前发生的复制和粘贴操作。在前面的宏语句中，`3.14159`文字将被复制并粘贴到程序中出现符号`PI`的任何地方。\n\n以下面的代码为例:\n\n```cpp\n#include <iostream> \nusing namespace std; \n#define PI 3.14159 \nint main() \n{ \n  double r = 4; \n  cout << \"Circumference is \" << 2*PI*r << endl; \n} \n```\n\nC++ 预处理器要做的是首先检查代码，寻找`PI`符号的任何用法。它将在这条线上找到这样一个用途:\n\n```cpp\ncout << \"Circumference is \" << 2*PI*r << endl; \n```\n\n前一行将在编译前转换为下面的行:\n\n```cpp\ncout << \"Circumference is \" << 2*3.14159*r << endl; \n```\n\n因此，`#define`语句所发生的是，所使用的符号(例如，`PI`)的所有出现都被文字数字`3.14159`代替，甚至在编译发生之前。以这种方式使用宏的目的是避免将数字硬编码到代码中。符号通常比又大又长的数字更容易阅读。\n\nAdvice: try to use `const` variables where possible.\n\n您可以使用宏来定义常量变量。也可以用`const`变量表达式代替。假设我们有下面一行代码:\n\n```cpp\n#define PI 3.14159 \n```\n\n我们将被鼓励使用以下内容:\n\n```cpp\nconst double PI = 3.14159; \n```\n\n使用`const`变量将受到鼓励，因为它将您的值存储在实际变量中。变量是类型化的，类型化数据是一件好事。\n\n# 带参数的宏\n\n我们也可以编写接受参数的宏。下面是一个带有参数的宏示例:\n\n```cpp\n#define println(X) cout << X << endl; \n```\n\n这个宏会做的是每次在代码中遇到`println(\"Some value\")`，右侧的代码(`cout << \"Some value\" << endl`)就会被复制粘贴到控制台中。注意括号之间的参数是如何在`X`处被复制的。假设我们有下面一行代码:\n\n```cpp\nprintln( \"Hello there\" ) \n```\n\n这将由以下语句代替:\n\n```cpp\ncout << \"Hello there\" << endl; \n```\n\n带有参数的宏就像非常短的函数。宏不能包含任何换行符。\n\nAdvice: use inline functions instead of macros with arguments.\n\n你必须知道带有参数的宏是如何工作的，因为你会在 C++ 代码中经常遇到它们。然而，只要有可能，许多 C++ 程序员更喜欢使用内联函数，而不是带有参数的宏。\n\n正常的函数调用执行包括对函数的`jump`指令，然后是函数的执行。内联函数的代码行被复制到函数调用点，并且不发出跳转。对于没有很多代码行的非常小的简单函数，使用内联函数通常是有意义的。例如，我们可以内联一个简单的函数`max`，它可以找到两个值中较大的一个:\n\n```cpp\ninline int max( int a, int b ) \n{ \n  if( a > b ) return a; \n  else return b; \n} \n```\n\n在使用这个`max`函数的任何地方，函数体的代码都会被复制粘贴到函数调用点。不必对函数进行`jump`节省了执行时间，使内联函数有效地类似于宏。\n\n使用内联函数有一个缺点。内联函数的主体必须完全包含在`.h`头文件中。这是为了让编译器可以进行优化，并在使用函数的任何地方进行内联。函数被内联通常是为了速度(因为你不必跳到另一段代码来执行函数)，但代价是代码膨胀。\n\n以下是内联函数优于宏的原因:\n\n*   宏容易出错:没有键入宏的参数。\n*   宏必须写在一行中，否则您会看到它们使用转义:\n\n```cpp\n \\\nnewline characters \\\nlike this \\\nwhich is hard to read \\ \n```\n\n*   如果宏编写得不仔细，将导致难以修复的编译器错误。例如，如果您没有正确地将参数括起来，那么您的代码就是错误的。\n*   大型宏很难调试。\n\n应该说，宏确实允许你执行一些预处理器编译魔法。UE4 大量使用带有参数的宏，您将在后面看到。\n\n# 康斯特布尔\n\n还有一种新的方法，你也可以在编译时而不是运行时做事情，那就是使用`constexpr`。与宏一样，您可以创建变量和函数，这些变量和函数将被编译器自动复制到使用它们的地方。所以，你可以做这样的变量:\n\n```cpp\nconstexpr float pi = 3.14129f;\n```\n\n您也可以将`constexpr`添加到您想要在编译时运行的函数中，如下所示:\n\n```cpp\nconstexpr int increment(int i)\n{\n    return i + 1;\n}\n```\n\n还有一件事你可以用`constexpr`来做，那就是在编译的时候用它和`if`语句一起计算一些东西。因此，如果您想在编译游戏时为演示版本做一些不同的事情，您可以这样做:\n\n```cpp\nif constexpr (kIsDemoVersion) {\n    //use demo version code here\n} else {\n   //use regular version code here\n}\n```\n\n当我们谈论模板时，您会发现这些的更多用途。\n\n# 摘要\n\n函数调用允许您重用基本代码。代码重用很重要，原因有很多，主要是因为编程很难，应该尽可能避免重复劳动。编写`sqrt()`函数的程序员的努力，不需要其他想解决同样问题的程序员重复。*"
  },
  {
    "path": "docs/learn-cpp-build-game-ue4/06.md",
    "content": "# 六、对象、类和继承\n\n在前一章中，我们讨论了函数作为捆绑一堆相关代码行的方法。我们讨论了函数是如何抽象出实现细节的，以及`sqrt()`函数如何不需要你理解它在内部是如何工作的，就可以用它来寻找根。这是一件好事，主要是因为它节省了程序员的时间和精力，同时使寻找平方根的实际工作更容易。当我们讨论对象时，这个抽象原则会再次出现。\n\n在本章中，我们将介绍:\n\n*   什么是物体？\n*   结构\n*   类与结构\n*   吸气剂和沉降剂\n*   构造函数和析构函数\n*   类继承\n*   多重继承\n*   将您的类放入标题中\n*   面向对象编程设计模式\n*   可调用对象和调用\n\nThis chapter contains a lot of keywords that might be difficult to grasp at first, including `virtual` and `abstract`.\n\nDon't let the more difficult sections of this chapter bog you down. I included descriptions of many advanced concepts for completeness. However, bear in mind that you don't need to completely understand everything in this chapter to write working C++ code in UE4\\. It helps to understand everything, but if something doesn't make sense, don't get stuck. Give it a read and then move on. Probably what will happen is you will not get it at first, but remember a reference to the concept in question when you're coding. Then, when you open this book up again, voilà! It will make sense.\n\n# 什么是物体？\n\n简而言之，对象将方法(函数的另一个词)和它们相关的数据绑定到一个单一的结构中。这种结构称为类。使用对象背后的主要思想是为游戏中的所有东西创建一个代码表示。代码中表示的每个对象都有数据和对该数据进行操作的相关函数。因此，您将有一个对象来表示您的`Player`和相关函数，这些函数构成了`Player``jump()``shoot()`和`pickupItem()`。您还可以有一个对象来表示每个怪物实例和相关功能，例如`growl()`、`attack()`，可能还有`follow()`。\n\n然而，对象是变量的类型，只要你把它们保存在内存中，对象就会一直存在。一旦在你的游戏中它所代表的东西被创建，你就创建一个实例，或者一个具有它自己的一组值的对象的特定表示，并且当它所代表的东西在你的游戏中死亡时，你就销毁该对象实例。\n\n对象可以用来表示游戏中的事物，但也可以用来表示任何其他类型的事物。例如，您可以将图像存储为对象。数据字段将是图像的宽度、高度和其中的像素集合。C++ 字符串也是对象。\n\n# 结构对象\n\nC++ 中的对象基本上是由简单类型的集合组成的任何变量类型。C++ 中最基本的对象是`struct`。我们使用`struct`关键字将一堆较小的变量粘合成一个大变量。如果你还记得的话，我们在[第二章](02.html)*变量和记忆*中确实简单介绍过`struct`。让我们修改这个简单的例子:\n\n```cpp\nstruct Player \n{ \n  string name; \n  int hp; \n}; \n```\n\n这是构成`Player`对象的结构定义。`Player`的`name`值有一个`string`，其`hp`值有一个整数。\n\n如果你回忆一下[第二章](02.html)、*变量和记忆*，我们对`Player`对象进行实例化的方式是这样的:\n\n```cpp\nPlayer me;    // create an instance of Player, called me \n```\n\n从这里，我们可以访问`me`对象的字段，如下所示:\n\n```cpp\nme.name = \"Tom\"; \nme.hp = 100; \n```\n\n# 成员职能\n\n现在，激动人心的部分来了。我们可以将成员函数附加到`struct`定义中，只需将这些函数写入`struct Player`定义中:\n\n```cpp\nstruct Player \n{ \n  string name; \n  int hp; \n  // A member function that reduces player hp by some amount \n  void damage( int amount )      \n  { \n    hp -= amount; \n  } \n  void recover( int amount ) \n  { \n    hp += amount; \n  } \n}; \n```\n\n成员函数只是在`struct`或`class`定义中声明的 C++ 函数。\n\n这里有点搞笑的想法，我就出来说一下。`struct Player`的变量对`struct Player`内的所有功能都是可访问的。在`struct Player`的每个成员函数中，我们实际上可以访问`name`和`hp`变量，就好像它们是函数的本地变量一样。换句话说，`struct Player`的`name`和`hp`变量在`struct Player`的所有成员函数之间共享。\n\n# 这个关键字\n\n在一些 C++ 代码中(在后面的章节中)，你会看到更多对`this`关键字的引用。`this`关键字是指向当前对象的指针。例如，在`Player::damage()`函数内部，我们可以显式地编写对`this`的引用:\n\n```cpp\nvoid damage( int amount ) \n{ \n  this->hp -= amount; \n} \n```\n\n`this`关键字只在成员函数中有意义。我们可以在成员函数中明确包含`this`关键字的使用，但是不写`this`，这意味着我们在谈论当前对象的`hp`。因此，虽然这在大多数情况下并不是绝对必要的，但这可能是个人或公司的偏好，并可能使代码更易读。\n\n# 字符串是对象吗？\n\n是的，字符串是对象！过去每次使用`string`变量时，都是在使用一个对象。让我们尝试一下`string`类的一些成员函数。\n\n```cpp\n#include <iostream> \n#include <string> \nusing namespace std; \nint main() \n{ \n  string s = \"strings are objects\"; \n  s.append( \"!!\" ); // add on \"!!\" to end of the string! \n  cout << s << endl; \n} \n```\n\n我们在这里所做的是使用`append()`成员函数在字符串(`!!`)的末尾添加两个额外的字符。成员函数始终应用于调用成员函数的对象(点左边的对象)。\n\n要查看对象上可用的成员和成员函数列表，请执行以下步骤:\n\n1.  在 Visual Studio 中键入对象的变量名\n2.  然后输入一个点(`.`)\n3.  然后按下 *Ctrl* 和空格键\n\n将弹出如下成员列表:\n\n![](img/5b34de60-8c71-4702-9af6-e80b967e0264.png)\n\nPressing Ctrl and the spacebar will make the member listing appear\n\n# 调用成员函数\n\n可以使用以下语法调用成员函数:\n\n```cpp\nobjectName.memberFunction(); \n```\n\n调用成员函数的对象在点的左边。要调用的成员函数在点的右边。成员函数调用总是跟在圆括号`()`后面，即使没有参数传递给括号。\n\n所以，在程序中怪物攻击的部分，我们可以将`player`的`hp`值降低如下:\n\n```cpp\nplayer.damage( 15 );  // player takes 15 damage \n```\n\n这难道不比下面的更易读吗？\n\n```cpp\nplayer.hp -= 15;      // player takes 15 damage \n```\n\nWhen member functions and objects are used effectively, your code will read more like prose or poetry than a bunch of operator symbols slammed together.\n\n除了美观和可读性，写成员函数还有什么意义？在`Player`对象之外，我们现在可以用一行代码做更多的事情，而不仅仅是通过`15`减少`hp`成员。我们还可以在减少`player`的`hp`的同时做其他的事情，比如考虑`player`的护甲，检查玩家是否刀枪不入，或者在`Player`被破坏的时候有其他的效果出现。玩家受损时发生的事情应该通过`damage()`功能抽象掉。\n\n现在，想象一下`Player`有一个`armorClass`。让我们为`armorClass`在`struct Player`中添加一个字段:\n\n```cpp\nstruct Player \n{ \n  string name; \n  int hp; \n  int armorClass; \n}; \n```\n\n我们需要将`Player`受到的伤害减少`Player`的护甲等级。所以，我们会输入一个公式来减少`hp`。我们可以通过非面向对象的方式直接访问`Player`对象的数据字段:\n\n```cpp\nplayer.hp -= 15 - player.armorClass; // non OOP \n```\n\n否则，我们可以通过编写一个成员函数，根据需要改变`Player`对象的数据成员，以面向对象的方式来实现。在`Player`对象内部，我们可以编写一个`damage()`成员函数:\n\n```cpp\nstruct Player \n{ \n  string name; \n  int hp; \n  int armorClass;  \n  void damage( int dmgAmount )                \n  { \n    hp -= dmgAmount - armorClass; \n  } \n}; \n```\n\n# 练习\n\n1.  在前面的代码中`Player`的`damage`函数有一个细微的错误。你能找到并修复它吗？提示:如果造成的伤害低于`Player`的`armorClass`会怎么样？\n2.  只有一个装甲等级的数字并不能给出足够的关于装甲的信息！盔甲的名字是什么？它看起来像什么？为`Player`的装甲设计一个`struct`功能，包括`name`、`armorClass`和`durability`等级的字段。\n\n# 解决方法\n\n第一个练习的解决方案是在下一节*私有和封装*中列出的`struct Player`代码。\n\n对于第二个，使用下面的代码怎么样？\n\n```cpp\nstruct Armor \n{ \n  string name; \n  int armorClass; \n  double durability; \n}; \n```\n\n`Armor`的一个实例将被放入`struct Player`中:\n\n```cpp\nstruct Player \n{ \n  string name; \n  int hp; \n  Armor armor; // Player has-an Armor \n}; \n```\n\n这意味着`Player`有盔甲。记住这一点——我们稍后将探讨`has-a`和`is-a`的关系。\n\nAll variable names thus far start with a lowercase character. This is a good convention with C++ code. You may find some cases where specific teams or other languages prefer to use uppercase characters to start variable names, in which case it's better to just do what people at your company expect.\n\n# 私处和封装\n\n所以现在我们定义了几个成员函数，其目的是修改和维护我们的`Player`对象的数据成员，但是有些人提出了一个论点。\n\n论据如下:\n\n*   一个对象的数据成员应该只能通过它的成员函数来访问，而不能直接访问。\n\n这意味着您永远不要直接从对象外部访问对象的数据成员，换句话说，直接修改`player`的`hp`:\n\n```cpp\nplayer.hp -= 15 - player.armorClass; // bad: direct member access \n```\n\n这是应该禁止的，应该强制该类的用户使用适当的成员函数来更改数据成员的值:\n\n```cpp\nplayer.damage( 15 );  // right: access through member function \n```\n\n这个原理叫做*封装*。封装是一个概念，即每个对象只能通过其成员函数进行交互。封装说原始数据成员永远不应该被直接访问。\n\n封装背后的原因如下:\n\n*   **使类自包含**:封装背后的主要思想是，当对象被编程为管理和维护自己的内部状态变量，而不需要类外的代码来检查该类的私有数据时，对象工作得最好。当对象以这种方式编码时，它使对象更容易处理，也就是说，更容易阅读和维护。要让`Player`对象跳跃，你只需要调用`player.jump()`；让`Player`对象管理其`y-height`位置的状态变化(使`Player`跳跃！).当一个对象的内部成员没有被公开时，与该对象的交互更加容易和有效。仅与对象的公共成员函数交互；让对象管理它的内部状态(我们稍后会解释关键词`private`和`public`)。\n*   **避免破坏代码**:当类外的代码只与该类的公共成员函数(类的公共接口)交互时，对象的内部状态管理可以自由改变，而不会破坏任何调用代码。这样，如果一个对象的内部数据成员因为任何原因发生变化，只要成员函数的签名(名称、返回类型和任何参数)保持不变，所有使用该对象的代码仍然有效。\n\n那么，如何才能防止一个程序员做错事情，直接访问数据成员呢？C++ 引入了*访问修饰符*的概念，以防止访问对象的内部数据。\n\n下面是我们如何使用访问修饰符来禁止从`struct Player`外部访问`struct Player`的某些部分。\n\n你要做的第一件事是决定`struct`定义的哪些部分你想在类外被访问。这些部分将被标记为`public`。在`struct`之外无法进入的所有其他区域将被标为`private`、\n，如下所示:\n\n```cpp\nstruct Player \n{ \nprivate:        // begins private section.. cannot be accessed  \n                // outside the class until \n  string name; \n  int hp;  \n  int armorClass; \npublic:         //  until HERE. This begins the public section \n  // This member function is accessible outside the struct \n  // because it is in the section marked public: \n  void damage( int amount ) \n  { \n    int reduction = amount - armorClass; \n    if( reduction < 0 ) // make sure non-negative! \n      reduction = 0; \n    hp -= reduction; \n  } \n}; \n```\n\n# 有些人喜欢公开\n\n有些人不加掩饰地使用`public`数据成员，不封装他们的对象。这是一个偏好问题，尽管被认为是糟糕的面向对象编程实践。\n\n但是，UE4 中的类有时确实会使用`public`成员。这是一个判断的电话；数据成员应该是`public`还是`private`真的要看程序员了。\n\n有了经验，你会发现，有时候，当你创建一个本该是`private`的数据成员`public`时，你会遇到一个需要大量重构(修改代码)的情况。\n\n# 类关键字与结构\n\n您可能已经看到了一种不同的声明对象的方式，使用`class`关键字，而不是`struct`，如下面的代码所示:\n\n```cpp\nclass Player // we used class here instead of struct! \n{ \n  string name; \n  // \n}; \n```\n\nC++ 中的`class`和`struct`关键词几乎相同。`class`和`struct`只有一个区别，那就是一个`struct`关键字里面的数据成员默认会被声明为`public`，而一个`class`关键字里面的数据成员默认会被声明为`private`。(这就是为什么我用`struct`介绍对象；我不想莫名其妙地把`public`作为`class`的第一行。)\n\n一般来说，`struct`对于不使用封装、成员函数不多、必须向后兼容 c 的简单类型是首选，其他地方几乎都在使用类。\n\n从现在开始，让我们用`class`关键字代替`struct`。\n\n# 吸气剂和沉降剂\n\n你可能已经注意到，一旦我们把`private`放到`Player`类定义上，我们就不能再从`Player`类之外读取或写入`Player`的名字。\n\n假设我们试着用下面的代码读这个名字:\n\n```cpp\nPlayer me; \ncout << me.name << endl; \n```\n\n或者写到名字，如下:\n\n```cpp\nme.name = \"William\"; \n```\n\n使用带有`private`成员的`struct Player`定义，我们将得到以下错误:\n\n```cpp\n    main.cpp(24) : error C2248: 'Player::name' : cannot access private \n    member declared in class 'Player'\n```\n\n这正是我们在标注`name`字段`private`时所要求的。我们让它在`Player`班之外完全无法进入。\n\n# 吸气剂\n\ngetter(也称为访问器函数)用于将内部数据成员的副本传递回调用者。为了读取`Player`的名称，我们将使用成员函数来包装`Player`类，具体来说就是检索该`private`数据成员的副本:\n\n```cpp\nclass Player \n{ \nprivate: \n  string name;  // inaccessible outside this class! \n                //  rest of class as before \npublic: \n  // A getter function retrieves a copy of a variable for you \n  string getName() \n  { \n    return name; \n  } \n}; \n```\n\n所以，现在可以读取`player`的`name`信息了。我们可以通过使用下面的代码语句来做到这一点:\n\n```cpp\ncout << player.getName() << endl; \n```\n\nGetters 用于检索`private`成员，否则您将无法从类外访问这些成员。\n\nReal world tip - the const keyword\n\nInside a class, you can add the `const` keyword to a member function declaration. What the `const` keyword does is promise to the compiler that the internal state of the object will not change as a result of running this function. Attaching the `const` keyword will look something like this:\n\n`string getName() const`\n`{`\n  `return name;`\n`}`\n\n标记为`const`的成员函数中不能分配数据成员。由于对象的内部状态保证不会因为运行`const`函数而改变，编译器可以对`const`成员函数的函数调用进行一些优化。\n\n# 安装员\n\nsetter(也称为修饰符函数或 mutator 函数)是一个成员函数，其唯一目的是更改类内部变量的值，如以下代码所示:\n\n```cpp\nclass Player \n{ \nprivate: \n  string name;  // inaccessible outside this class! \n                //  rest of class as before \npublic: \n  // A getter function retrieves a copy of a variable for you \n  string getName() \n  { \n    return name; \n  } \n  void setName( string newName ) \n  { \n    name = newName; \n  } \n}; \n```\n\n因此，我们仍然可以从`class`函数之外的`class`中更改`private`变量，但前提是我们通过 setter 函数进行更改。\n\n# 但是 get/set 操作有什么意义呢？\n\n所以，当新手程序员第一次在`private`成员上遇到 get / set 操作时，脑海中浮现的第一个问题是，get/set 不是弄巧成拙吗？我的意思是，当我们打算以另一种方式再次公开相同的数据时，隐藏对数据成员的访问有什么意义呢？这就像说，*“你不能吃任何巧克力，因为它们是私人的，除非你说请* `getMeTheChocolate()`”。*然后，你可以吃巧克力了。”*\n\n一些专业程序员甚至将 get/set 函数缩短为一行，如下所示:\n\n```cpp\nstring getName(){ return name; } \nvoid setName( string newName ){ name = newName; } \n```\n\n让我们来回答这个问题。get/set 对不会因为完全公开数据而破坏封装吗？\n\n答案是双重的。首先，get 成员函数通常只返回被访问的数据成员的副本。这意味着原始数据成员的值仍然受到保护，并且不能通过`get()`操作进行修改。\n\n但是`set()`(变异方法)操作有点反直觉。如果二传手是`passthru`操作，比如`void setName( string newName ) { name=newName; }`，那么拥有二传手似乎毫无意义。使用 mutator 方法而不是直接覆盖变量有什么好处？\n\n使用 mutator 方法的理由是在赋值变量之前编写额外的代码，以防止变量接受不正确的值。\n比方说，我们有一个`hp`数据成员的设置器，如下所示:\n\n```cpp\nvoid setHp( int newHp ) \n{ \n  // guard the hp variable from taking on negative values \n  if( newHp < 0 ) \n  { \n    cout << \"Error, player hp cannot be less than 0\" << endl; \n    newHp = 0; \n  } \n  hp = newHp; \n} \n```\n\nmutator 方法应该防止内部`hp`数据成员取负值。您可能会认为 mutator 方法有点追溯性。在调用`setHp( -2 )`之前，是否应该由调用代码检查它正在设置的值，而不是让它只被变异器方法捕获？你不能使用一个`public`成员变量，并在调用代码中而不是在 setter 中负责确保该变量不接受无效值吗？你可以。\n\n这就是使用 mutator 方法背后的核心原因。mutator 方法背后的思想是，调用代码可以将它想要的任何值传递给`setHp`函数(例如，`setHp( -2 )`)，而不必担心它传递给函数的值是否有效。`setHp`函数负责确保该值对`hp`变量有效。\n\n一些程序员认为像`getHp()` / `setHp()`这样的直接变异函数是一种代码味道。总的来说，代码味道是一种糟糕的编程实践，人们不会公开注意到，除了一种令人担忧的感觉，即某些事情正在以次优的方式进行。他们认为可以编写更高级别的成员函数来代替变异函数。例如，我们应该有`heal()`、`damage()`等`public`成员函数来代替`setHp()`成员函数。关于这个话题的文章可以在[http://c2.com/cgi/wiki?AccessorsAreEvil](http://c2.com/cgi/wiki?AccessorsAreEvil)上找到。\n\n# 构造函数和析构函数\n\nC++ 代码中的构造函数是一个简单的小函数，在第一次创建 C++ 对象实例时运行一次。当 C++ 对象实例被销毁时，析构函数运行一次。假设我们有以下程序:\n\n```cpp\n#include <iostream> \n#include <string> \nusing namespace std; \nclass Player \n{ \nprivate: \n  string name;  // inaccessible outside this class! \npublic: \n  string getName(){ return name; } \n// The constructor! \n  Player() \n  { \n    cout << \"Player object constructed\" << endl; \n    name = \"Diplo\"; \n  } \n  // ~Destructor (~ is not a typo!) \n  ~Player() \n  { \n    cout << \"Player object destroyed\" << endl; \n  } \n}; \n\nint main() \n  { \n    Player player; \n    cout << \"Player named '\" << player.getName() << \"'\" << endl; \n  } \n  // player object destroyed here \n```\n\n在这里，我们创建了一个`Player`对象。该代码的输出如下:\n\n```cpp\nPlayer object constructed \nPlayer named 'Diplo' \nPlayer object destroyed \n```\n\n对象构造期间发生的第一件事是构造函数实际运行。这会打印线条`Player object constructed`。随后，印有`Player`名字的一行被打印出来:`Player named 'Diplo'`。`Player`为什么叫迪普？因为这是`Player()`建造者的名字。\n\n最后，在程序的最后，调用`Player`析构函数，我们看到`Player object destroyed`。`Player`物体在`main()`末端(在`main`的`}`处)超出范围时被摧毁。\n\n那么，构造函数和析构函数有什么用呢？确切地说，它们看起来是为了什么:建立和拆除一个对象。构造函数可以用于数据字段的初始化，析构函数可以对任何动态分配的资源调用`delete`(我们还没有涉及到动态分配的资源，所以不用担心最后这一点)。\n\n# 类继承\n\n当您想要基于现有的代码类创建一个新的、功能更强的代码类时，可以使用继承。继承是一个棘手的话题。让我们从派生类(或子类)的概念开始。\n\n# 派生类\n\n最自然的考虑继承的方式是类比动物王国。生物的分类如下图所示:\n\n![](img/b3b310e9-d3c3-4849-bf72-f2a09006fd81.png)\n\n这个图的意思是**狗**、**猫**、**马**、**人**都是哺乳动物。这意味着它们都有一些共同的特征，比如有共同的器官(女性的大脑有新皮层、肺、肝和子宫)，而在其他方面完全不同。每个人走路的方式都不一样。每个人交流的方式也不同。\n\n如果你是编码生物，那意味着什么？您只需要对公共功能编程一次。然后，您将为每个`Dog`、`Cat`、`Horse`和`Human`类实现不同部分的代码。\n\n上图的一个具体例子如下:\n\n```cpp\n#include <iostream> \nusing namespace std; \nclass Mammal \n{ \nprotected: \n  // protected variables are like privates: they are \n  // accessible in this class but not outside the class. \n  // the difference between protected and private is \n  // protected means accessible in derived subclasses also \nint hp; \n  double speed; \n\npublic: \n  // Mammal constructor - runs FIRST before derived class ctors! \n  Mammal() \n  { \n    hp = 100; \n    speed = 1.0; \n    cout << \"A mammal is created!\" << endl; \n  } \n  ~Mammal() \n  { \n    cout << \"A mammal has fallen!\" << endl; \n  } \n  // Common function to all Mammals and derivatives \n  void breathe() \n  { \n    cout << \"Breathe in.. breathe out\" << endl; \n  } \n  virtual void talk() \n  { \n    cout << \"Mammal talk.. override this function!\" << endl; \n  } \n  // pure virtual function, (explained below) \n  virtual void walk() = 0; \n}; \n\n// This next line says \"class Dog inherits from class Mammal\" \nclass Dog : public Mammal // : is used for inheritance \n{ \npublic: \n  Dog() \n  { \n    cout << \"A dog is born!\" << endl; \n  } \n  ~Dog() \n  { \n    cout << \"The dog died\" << endl; \n  } \n  virtual void talk() override \n  { \n    cout << \"Woof!\" << endl; // dogs only say woof! \n  } \n  // implements walking for a dog \n  virtual void walk() override \n  { \n    cout << \"Left front paw & back right paw, right front paw &  \n     back left paw.. at the speed of \" << speed << endl; \n  } \n}; \n\nclass Cat : public Mammal \n{ \npublic: \n  Cat() \n  { \n    cout << \"A cat is born\" << endl; \n  } \n  ~Cat() \n  { \n    cout << \"The cat has died\" << endl; \n  } \n  virtual void talk() override \n  { \n    cout << \"Meow!\" << endl; \n  } \n  // implements walking for a cat.. same as dog! \n  virtual void walk() override \n  { \n    cout << \"Left front paw & back right paw, right front paw &  \n     back left paw.. at the speed of \" << speed << endl; \n  } \n}; \n\nclass Human : public Mammal \n{ \n  // Data member unique to Human (not found in other Mammals) \n  bool civilized; \npublic: \n  Human() \n  { \n    cout << \"A new human is born\" << endl; \n    speed = 2.0; // change speed. Since derived class ctor \n    // (ctor is short for constructor!) runs after base  \n    // class ctor, initialization sticks initialize member  \n    // variables specific to this class \n    civilized = true; \n  } \n  ~Human() \n  { \n    cout << \"The human has died\" << endl; \n  } \n  virtual void talk() override \n  { \n    cout << \"I'm good looking for a .. human\" << endl; \n  } \n  // implements walking for a human.. \n  virtual void walk() override \n  { \n    cout << \"Left, right, left, right at the speed of \" << speed  \n     << endl; \n  } \n  // member function unique to human derivative \n  void attack( Human & other ) \n  { \n    // Human refuses to attack if civilized \n    if( civilized ) \n      cout << \"Why would a human attack another? I refuse\" <<  \n       endl; \n    else \n      cout << \"A human attacks another!\" << endl; \n  } \n}; \n\nint main() \n{ \n  Human human; \n  human.breathe(); // breathe using Mammal base class  \n   functionality \n  human.talk(); \n  human.walk(); \n\n  Cat cat; \n  cat.breathe(); // breathe using Mammal base class functionality \n  cat.talk(); \n  cat.walk(); \n\n  Dog dog; \n  dog.breathe(); \n  dog.talk(); \n  dog.walk(); \n} \n```\n\n`Dog`、`Cat`、`Human`都是从`class Mammal`继承的。这意味着`dog`、`cat`和`human`是哺乳动物，还有更多。\n\n# 继承语法\n\n继承的语法很简单。我们以`Human`类定义为例。下面的截图是典型的继承语句:\n\n![](img/06c5e550-28b0-4346-8bdb-8aaae6895259.png)\n\n冒号左边的类( **:** )是新的派生类，冒号右边的类是基类。\n\n# 继承是做什么的？\n\n继承的目的是让派生类承担基类的所有特性(数据成员和成员函数)，然后用更多的功能扩展它。例如，所有哺乳动物都有`breathe()`功能。通过继承`Mammal`类，`Dog`、`Cat`和`Human`类都自动获得`breathe()`的能力。\n\n继承减少了代码的复制，因为我们不必为`Dog`、`Cat`和`Human`重新实现公共功能(如`.breathe()`)。相反，这些派生类中的每一个都可以重用在`class Mammal`中定义的`breathe()`函数。\n\n但是只有`Human`类有`attack()`成员功能。这意味着，在我们的代码中，只有`Human`类攻击。`cat.attack()`函数会引入编译器错误，除非你在`class Cat`里面(或者在`class Mammal`里面)写了一个`attack()`成员函数。\n\n# 这是一种关系\n\n继承常被说成是一种`is-a`关系。当一个`Human`类继承了`Mammal`类，那么我们说人类*就是——一个*哺乳动物:\n\n![](img/994a96f0-b90d-4752-bbb0-7e736de6bbed.png)\n\n人类继承了哺乳动物的所有特征。\n\n但是如果一个`Human`对象内部包含一个`Mammal`对象，会怎么样呢，如下所示？\n\n```cpp\nclass Human \n{ \n  Mammal mammal; \n}; \n```\n\n在这个例子中，我们会说人类在某个地方有一个`Mammal`(如果人类怀孕了，或者以某种方式携带了哺乳动物，这就有意义了):\n\n![](img/8ec61aa4-5a39-4f71-a182-48a5f68439c7.png)\n\n这个`Human`类实例附着了某种哺乳动物\n\n还记得我们之前在里面给了`Player`一个`Armor`物体吗？`Player`对象从`Armor`类继承是没有意义的，因为说`Player` *是-装甲*是没有意义的。在代码设计中决定一个类是否从另一个类继承时(例如`Human`类从`Mammal`类继承)，你必须总是能够舒服地说一些类似`Human`类 *is-a* `Mammal`的话。如果*是-a* 语句听起来不对，那么继承很可能是那对对象的错误关系。\n\n在前面的例子中，我们引入了一些新的 C++ 关键字。首先是`protected`。\n\n# 受保护变量\n\n`protected`成员变量不同于`public`或`private`变量。所有三类变量都可以在定义它们的类中访问。它们之间的区别在于类外的可访问性。一个`public`变量可以在类内和类外的任何地方访问。一个`private`变量可以在类内访问，但不能在类外访问。一个`protected`变量可以在类内部和派生子类内部访问，但不能在类外部访问。因此，`class Mammal`的`hp`和`speed`成员在派生类`Dog`、`Cat`、`Horse`和`Human`中是可访问的，但不能在这些类之外访问(例如在`main()`中)。\n\n# 虚函数\n\n虚函数是一个成员函数，它的实现可以在派生类中重写。在本例中，`talk()`成员函数(在`class Mammal`中定义)被标记为`virtual`。这意味着派生类可能会也可能不会选择实现他们自己版本的`talk()`成员函数。\n\n# 纯虚函数\n\n纯`virtual`函数(和抽象类)是一个需要在派生类中重写其实现的函数。`class Mammal`中的`walk()`功能纯粹是虚拟的；它是这样宣布的:\n\n```cpp\nvirtual void walk() = 0; \n```\n\n前面代码末尾的`= 0`部分是使函数纯粹虚拟的部分。\n\n`class Mammal`中的`walk()`函数是纯虚函数，这使得`Mammal`类变得抽象。C++ 中的抽象类是至少有一个纯虚函数的任何类。\n\n如果一个类包含一个纯虚函数并且是抽象的，那么这个类不能被直接实例化。也就是说，由于纯虚函数`walk()`，现在不能创建`Mammal`对象。如果您尝试执行以下代码，将会得到一个错误:\n\n```cpp\nint main() \n{ \n  Mammal mammal; \n} \n```\n\n如果您试图创建一个`Mammal`对象，您将得到以下错误:\n\n```cpp\nerror C2259: 'Mammal' : cannot instantiate abstract class \n```\n\n但是，您可以创建`class Mammal`的派生实例，只要派生类实现了所有的纯虚拟成员函数。\n\n你可能会想为什么要用这个。嗯，你真的认为你会想要在游戏中创建一个`Mammal`对象吗？不，您需要创建一个从`Mammal`派生的类型的对象，例如`Cat`或`Dog`。这样的话，你就不会不小心创造出一个`Mammal`，这对于`Player`来说是非常混乱的！\n\n# 多重继承\n\n并非所有倍数都像听起来那么好。多重继承是指派生类从多个基类继承。通常，如果我们继承的多个基类是完全不相关的，那么这种方法就不会有问题。\n\n例如，我们可以有一个继承自`SoundManager`和`GraphicsManager`基类的类`Window`。如果`SoundManager`提供了成员功能`playSound()`，`GraphicsManager`提供了成员功能`drawSprite()`，那么`Window`类将能够顺利使用这些附加功能:\n\n![](img/d58d2f1d-0fea-4c2e-84d1-d5e23b7151c4.png)\n\nGame Window inheriting from Sound Man and Graphics Man means Game Window will have both sets of capabilities\n\n然而，多重继承可能会产生负面后果。假设我们想要创建一个从`Donkey`和`Horse`类派生的`Mule`类。然而`Donkey`和`Horse`类都继承自`Mammal`基类。我们马上就有问题了！如果我们要调用`mule.talk()`，但是`mule`没有覆盖`talk()`函数，应该调用哪个成员函数，是`Horse`还是`Donkey`？很暧昧。\n\n# 私人继承\n\nC++ 一个较少被提及的特性是`private`继承。每当一个类从另一个类公开继承时，它所属的父类的所有代码都知道它，例如:\n\n```cpp\nclass Cat : public Mammal \n```\n\n这意味着所有代码都知道`Cat`是`Mammal`的对象，并且可以使用基类`Mammal*`指针指向`Cat*`实例。例如，以下代码将是有效的:\n\n```cpp\nCat cat; \nMammal* mammalPtr = &cat; // Point to the Cat as if it were a  \n                          // Mammal \n```\n\n将一个类的对象放入父类类型的变量中称为转换。如果`Cat`从`Mammal`公开继承，前面的代码没问题。私有继承是指`Cat`类之外的代码不允许知道父类:\n\n```cpp\nclass Cat : private Mammal \n```\n\n这里，外部调用的代码不会“知道”`Cat`类派生自`Mammal`类。当继承为`private`时，编译器不允许将`Cat`实例强制转换为`Mammal`基类。当需要隐藏某个类派生自某个父类的事实时，使用`private`继承。\n\n但是`private`继承在实践中很少使用。大多数类只是使用`public`继承。如果你想了解更多关于`private`继承的信息，请访问。\n\n# 将您的类放入标题中\n\n到目前为止，我们的课只是在`main()`之前贴的。如果你继续用这种方式编程，你的代码将全部在一个文件中，并且看起来像一个混乱的大文件。\n\n因此，将您的类组织到单独的文件中是一种很好的编程实践。当项目中有多个类时，这使得单独编辑每个类的代码变得更加容易。\n\n取`class Mammal`及其之前的派生类。我们将把这个例子组织成单独的文件。让我们分步骤进行:\n\n1.  在你的 C++ 项目中创建一个名为`Mammal.h`的新文件。将整个`Mammal`类剪切并粘贴到该文件中。请注意，由于`Mammal`类包含了`cout`的使用，我们也在该文件中编写了一个`#include <iostream>`语句。\n2.  在你的`Source.cpp`文件顶端写一个`\"#include` `Mammal.h\"`语句。\n\n下面的截图显示了这种情况的一个示例:\n\n![](img/dbddea3d-8b13-4b3d-8753-a7deb036398e.png)\n\n代码编译时这里发生的是整个`Mammal`类被复制粘贴(`#include`)到`Source.cpp`文件中，这个文件包含`main()`函数，其余的类都是从`Mammal`派生出来的。由于`#include`是复制粘贴功能，所以代码的功能会和之前完全一样；唯一不同的是，它会更好地组织和更容易看。在此步骤中编译并运行您的代码，以确保它仍然有效。\n\nCheck that your code compiles and runs often, especially when refactoring. When you don't know the rules, you're bound to make a lot of mistakes. This is why you should do your refactoring only in small steps. Refactoring is the name for the activity we are doing now - we are reorganizing the source to make more sense to other readers of our code base. Refactoring usually does not involve rewriting too much of it.\n\n接下来需要做的是将`Dog`、`Cat`和`Human`类隔离到自己的文件中。为此，创建`Dog.h`、`Cat.h`和`Human.h`文件并将其添加到您的项目中。\n\n先从`Dog`类开始，如下图截图所示。\n\n如果您完全使用这种设置并尝试编译和运行您的项目，您将看到“哺乳动物”:“类”类型重定义错误，如以下屏幕截图所示:\n\n![](img/300eff3f-4a48-41eb-9547-e9c95e432595.png)\n\n这个错误意味着`Mammal.h`在你的项目中被包含了两次，一次是在`Source.cpp`中，另一次是在`Dog.h`中。这实际上意味着两个版本的`Mammal`类被添加到编译代码中，C++ 不确定使用哪个版本。\n\n有几种方法可以解决这个问题，但最简单的(也是虚幻引擎使用的)是`#pragma once`宏，如下图截图所示:\n\n![](img/e4a0f4c4-3aee-4358-be65-a55683d8e697.png)\n\n我们在每个头文件的顶部写`#pragma once`。这样，第二次包含`Mammal.h`时，编译器就不会再复制粘贴它的内容了，因为之前已经包含过了，它的内容实际上已经在文件的编译组中了。\n\n对`Cat.h`和`Human.h`做同样的事情，然后在您的`main()`功能所在的`Source.cpp`文件中对它们进行`include`:\n\n![](img/b28030e8-77f6-4bdd-84c2-be4d67edb365.png)\n\nScreenshot with all classes included\n\n现在我们已经将所有的类都包含到了您的项目中，代码应该可以编译和运行了。\n\n# 使用。h 和。cpp 文件\n\n下一级组织是将类声明留在头文件(`.h`)中，将实际的函数实现体放在一些新的`.cpp`文件中。此外，将现有成员保留在`class Mammal`声明中。\n\n对于每个类，执行以下操作:\n\n1.  删除所有函数体(在`{`和`}`之间的代码)并用分号替换。对于`Mammal`类，如下所示:\n\n```cpp\n// Mammal.h \n#pragma once \nclass Mammal \n{ \nprotected: \n  int hp; \n  double speed; \n\npublic: \n  Mammal(); \n  ~Mammal(); \n  void breathe(); \n  virtual void talk(); \n  // pure virtual function,  \n  virtual void walk() = 0; \n}; \n```\n\n2.  创建一个名为`Mammal.cpp`的新`.cpp`文件。然后，简单地将成员函数体放在这个文件中:\n\n```cpp\n// Mammal.cpp \n#include <iostream> \nusing namespace std; \n\n#include \"Mammal.h\" \nMammal::Mammal() // Notice use of :: (scope resolution operator) \n{ \n  hp = 100; \n  speed = 1.0; \n  cout << \"A mammal is created!\" << endl; \n} \nMammal::~Mammal() \n{ \n  cout << \"A mammal has fallen!\" << endl; \n} \nvoid Mammal::breathe() \n{ \n  cout << \"Breathe in.. breathe out\" << endl; \n} \nvoid Mammal::talk() \n{ \n  cout << \"Mammal talk.. override this function!\" << endl; \n} \n```\n\n声明成员函数体时，务必注意类名和范围解析运算符(双冒号)的使用。我们在所有属于`Mammal`类的成员函数前面加上`Mammal::`。这表明它们属于该类(这使它们不同于用于该类类型的特定对象实例的`.`)。\n\n注意纯虚函数是如何没有身体的；不应该的！纯虚函数只是在基类中声明(并初始化为`0`)，但稍后在派生类中实现。\n\n# 锻炼\n\n完成以上不同生物类到类头(`.h`)和类定义文件(`.cpp`)的分离。\n\n# 面向对象编程设计模式\n\n如果你一直在研究编程，你可能会遇到术语*设计模式*。设计模式很重要，因为它们是标准的做事方式，可以应用于许多编程项目。想要深入了解设计模式的话，一本经典的书就是*设计模式*([https://www.goodreads.com/book/show/85009.Design_Patterns](https://www.goodreads.com/book/show/85009.Design_Patterns))。一旦你熟悉了它们，你会发现它们在你的职业生涯中有很多用途。并不是所有的都与对象相关，但这里有几个例子。\n\n# 一个\n\n有时，您只想拥有一个对象实例。假设你在做一个王国模拟器。你只想有一个国王。否则，你将面临一个到处都是阴谋和红色婚礼的*权力游戏-* 类型的局面，这不是你想要的游戏类型，对吗？(当然，对于不同的游戏，您可能会记住这一点。)但是对于这个特殊的游戏，你需要一个国王来管理事情。\n\n那么，你如何确保其他国王不会到处出现呢？你使用单例。singleton 是一个保存对象实例的类，您可以在任何地方使用它，而不是创建一个新的对象，您可以调用一个函数来访问一个对象的实例，然后您可以在这个对象上调用函数。为了确保您只创建一个对象实例，它会在类内部的静态变量中保存一个自身的副本(注意:我们将在下一节中详细讨论静态类成员)，当您调用`GetInstance()`时，它会检查您是否已经创建了对象实例。如果有，它会使用现有的。如果没有，它会创建一个新的。这里有一个例子:\n\n```cpp\n//King.h\n\n#pragma once\n#include <string>\n\nusing namespace std;\n\nclass King\n{\npublic:\n    ~King();\n\n    static King* getInstance();\n\n    void setName(string n) { name = n; };\n    string getName() const { return name; };\n    //Add more functions for King\nprivate:\n    King();\n\n    static King* instance;\n    string name;\n};\n```\n\n以下是`cpp`的代码:\n\n```cpp\n//King.cpp\n\n#include \"King.h\"\n\nKing* King::instance = nullptr;\n\nKing::King()\n{\n}\n\nKing::~King()\n{\n}\n\nKing* King::getInstance()\n{\n    if (instance == nullptr)\n    {\n        instance = new King();\n    }\n    return instance;\n}\n```\n\nThe constructor is listed in the `private:` section of the code. This is important. If you do this, the constructor will not be accessible from outside the class, meaning that no other programmers, who may not realize that this is a singleton, can start creating new `King` objects and wreak havoc on the game. If they try, they will get an error. So, this enforces that this class can only be accessed through the `getInstance()` function.\n\n要使用这个新的 singleton 类，您应该这样做:\n\n```cpp\n    King::getInstance()->setName(\"Arthur\");\n    cout << \"I am King \" << King::getInstance()->getName();\n```\n\n一旦你设置了名称，它就会输出`I am King Arthur`，不管你从代码的哪个地方调用它(只要确保在文件的顶部加上`#include \"King.h\"`)。\n\n# 工厂\n\n当你想到*工厂*这个术语时，你会想到什么？可能是他们大量生产物品的地方，比如汽车、鞋子或电脑。在代码中，一个`Factory`的工作原理是一样的。工厂是一个可以创建其他类型对象的类。但是它更加灵活，因为它可以创建不同类型的对象。\n\n我们之前看到哺乳动物可以是狗、猫、马或人。因为这四种类型都是从`Mammal`派生出来的，一个`Factory`对象可以有一个函数，你告诉它你想要哪种类型的`Mammal`，它会创建一个那种类型的对象，做任何必要的设置，然后返回。因为一个叫做多态的原理，你可以得到一个类型为`Mammal`的对象，但是当你调用任何虚函数时，它知道使用那些用于`Cat`、`Dog`或`Human`的虚函数，这取决于所创建的对象的类型。您的 C++ 编译器知道这一点，因为它在幕后维护一个虚拟函数表，该表保存指向您真正想要使用的每个虚拟函数版本的指针，并将这些指针存储在每个对象中。\n\n# 对象池\n\n假设您正在创建许多对象，例如用于显示烟花的粒子系统，并且您必须在整个屏幕上不断创建新的烟花动画。过一会儿，你会注意到事情变得缓慢，你甚至可能耗尽内存并崩溃。幸运的是，有办法解决这个问题。\n\n您可以创建一个对象池，它基本上是一组足够大的对象，可以在任何给定的时间包含屏幕上的每个对象。当一个完成了它的动画并消失后，你不是创建一个新的，而是把它扔回池中，当你需要另一个的时候，你可以把那个拉回来重新使用它(你可能想先改变颜色或其他设置)。重用池中的对象比不断创建新对象要快得多，处理时间也更少。它还有助于避免内存泄漏。\n\n# 静态成员\n\n正如我们在单例中看到的，类可以有静态成员。一个类的静态成员对该类的所有实例都存在一次，而不是对每个实例都不同。您通常可以像我们访问 singleton 一样访问它们:\n\n```cpp\nKing::getInstance()->setName(\"Arthur\");\n```\n\n静态变量也常用于与类相关的常数。但是它们也可以用来跟踪一些事情，比如你有多少个对象实例，方法是在构造函数中增加静态变量，然后在析构函数中减少它。这类似于智能指针如何跟踪仍存在多少对对象的引用。\n\n# 可调用对象和调用\n\n另一个新的 C++ 特性是可调用对象。这是一个高级话题，所以在这一点上不要太担心理解它，但我会给你一个简短的概述。但是要解释它，首先我需要提到另一个话题——运算符重载。\n\n你可能认为不能改变`+`、`-`、`*`、`/`等运算符的含义。其实在 C++ 里，可以。您可以添加一个名为`operator(symbol)`的功能。因此，如果你有一个字符串类，你可以创建一个`operator+`函数，使字符串连接起来，而不是试图找出如何添加两个实际上不是数字的对象。\n\n通过用`operator()`覆盖`()`，可调用对象走得更远。因此，您可以拥有一个可以作为对象调用的类。C++ 17 增加了一个新的函数，`invoke()`，它可以让你调用一个带参数的可调用对象。\n\n# 摘要\n\n在这一章中，你学习了 C++ 中的对象；它们是将数据成员和成员函数联系在一起的代码片段，这些代码被称为`class`或`struct`。面向对象编程意味着你的代码将充满东西，而不仅仅是`int`、`float`和`char`变量。你会有一个代表`Barrel`的变量，另一个代表`Player`的变量，以此类推，也就是一个代表你游戏中每一个实体的变量。您将能够通过使用继承来重用代码；如果你必须编码`Cat`和`Dog`的实现，你可以在基类`Mammal`中编码一个公共功能。我们还讨论了封装，以及如何更容易和更有效地对对象进行编程，使它们保持自己的内部状态。我们还介绍了一些对象的设计模式(你会发现还有很多)。\n\n在下一章中，我们将讨论如何动态分配内存，以及数组和向量。"
  },
  {
    "path": "docs/learn-cpp-build-game-ue4/07.md",
    "content": "# 七、动态存储分配\n\n在前一章中，我们讨论了类定义以及如何设计自己的定制类。我们讨论了如何通过设计您自己的定制类，来构造代表游戏或程序中实体的变量。\n\n在本章中，我们将讨论动态内存分配以及如何在内存中为对象组创建空间。让我们来看看本章涵盖的主题:\n\n*   重新审视构造函数和析构函数\n*   动态存储分配\n*   常规数组\n*   C++ 风格的动态大小数组(新建[]和删除[])\n*   动态 C 风格数组\n*   向量\n\n# 重新审视构造函数和析构函数\n\n假设我们有一个简化版本的`class Player`，和以前一样，只有一个构造函数和一个析构函数:\n\n```cpp\nclass Player \n{ \n  string name; \n  int hp; \npublic: \n  Player(){ cout << \"Player born\" << endl; } \n  ~Player(){ cout << \"Player died\" << endl; } \n}; \n```\n\n我们之前谈到了 C++ 中变量的*作用域*；概括地说，变量的作用域是程序中可以使用该变量的部分。变量的范围通常在声明它的块内。块只是包含在`{`和`}`之间的任何代码段。下面是一个示例程序，演示了变量范围:\n\n![](img/089f6225-04df-4f5c-993d-fcb0765a7f5a.png)\n\nIn this sample program, the x variable has scope through all of main(). The y variable's scope is only inside the if block.\n\n我们之前提到，一般来说，变量超出范围就会被销毁。让我们用`class Player`的例子来验证这个想法:\n\n```cpp\nint main() \n{ \n  Player player; // \"Player born\" \n}                // \"Player died\" - player object destroyed here \n```\n\n该程序的输出如下:\n\n```cpp\nPlayer born \nPlayer died \n```\n\n`Player`对象的析构函数在玩家对象的作用域末端被调用。由于变量的范围是在三行代码中定义的块，当对象超出范围时，`Player`对象将在`main()`的末尾立即被销毁。\n\n# 动态存储分配\n\n现在，让我们尝试动态分配一个`Player`对象。这是什么意思？\n\n我们使用`new`关键字来分配它:\n\n```cpp\nint main() \n{ \n  // \"dynamic allocation\" - using keyword new! \n  // this style of allocation means that the player object will \n  // NOT be deleted automatically at the end of the block where \n  // it was declared! Note: new always returns a pointer\nPlayer *player = new Player(); \n} // NO automatic deletion! \n```\n\n该程序的输出如下:\n\n```cpp\nPlayer born \n```\n\n玩家不会死！我们如何杀死玩家？我们必须明确调用`player`指针上的`delete`。\n\n# 删除关键字\n\n`delete`运算符调用被删除对象上的析构函数，如以下代码所示:\n\n```cpp\nint main() \n{ \n  // \"dynamic allocation\" - using keyword new! \n  Player *player = new Player(); \n  delete player; // deletion invokes dtor \n} \n```\n\n程序的输出如下:\n\n```cpp\nPlayer born \nPlayer died \n```\n\n因此，只有普通(或自动，也称为非指针类型)变量类型会在声明它们的块的末尾被销毁。指针类型(用`*`和`new`声明的变量)不会自动销毁，即使它们超出了范围。\n\n这有什么用？动态分配允许您控制对象的创建和销毁时间。这个以后会派上用场的。\n\n# 内存泄漏\n\n因此，用`new`创建的动态分配对象不会被自动删除，除非您在它们上面显式调用`delete`。这里有风险！这被称为“T4”内存泄漏。当分配有`new`的对象从未被删除时，就会发生内存泄漏。可能发生的情况是，如果你的程序中很多对象都用`new`分配了，然后你停止使用它们，你的电脑最终会因为内存泄漏而耗尽内存。\n\n下面是一个可笑的示例程序来说明这个问题:\n\n```cpp\n#include <iostream> \n#include <string> \nusing namespace std; \nclass Player \n{ \n  string name; \n  int hp; \npublic: \n  Player(){ cout << \"Player born\" << endl; } \n  ~Player(){ cout << \"Player died\" << endl; } \n}; \n\nint main() \n{ \n  while( true ) // keep going forever, \n  { \n    // alloc.. \n    Player *player = new Player(); \n    // without delete == Memory Leak! \n  } \n} \n```\n\n如果让这个程序运行足够长的时间，它最终会吞噬计算机的内存，如下图所示:\n\n![](img/f33d9c48-4e20-4276-9e66-97d6a9c19fe3.png)\n\n2 GB of RAM used for Player objects.\n\n请注意，从来没有人打算写一个有这类问题的程序！内存泄漏问题是偶然发生的。你必须注意你的内存分配和不再使用的`delete`对象。\n\n# 常规数组\n\nC++ 中的数组可以声明如下:\n\n```cpp\n#include <iostream> \nusing namespace std; \nint main() \n{ \n  int array[ 5 ];  // declare an \"array\" of 5 integers \n                   // fill slots 0-4 with values \narray[ 0 ] = 1; \narray[ 1 ] = 2; \narray[ 2 ] = 3; \narray[ 3 ] = 4; \narray[ 4 ] = 5; \n  // print out the contents \n  for( int index = 0; index < 5; index++ ) \n    cout << array[ index ] << endl; \n} \n```\n\n这在记忆中的样子是这样的:\n\n![](img/110fedec-6e9f-4833-82f1-e9bd06c15964.png)\n\n也就是说，`array`变量内部是五个槽或元素。每个槽内都有一个常规的`int`变量。您也可以通过传入值来声明数组，如下所示:\n\n```cpp\nint array[ ] = {6, 0, 5, 19};\n```\n\n您也可以传入`int`变量来使用存储在那里的值。\n\n# 数组语法\n\n那么，如何访问数组中的一个`int`值呢？要访问数组的各个元素，我们使用方括号，如下面一行代码所示:\n\n```cpp\narray[ 0 ] = 10; \n```\n\n这与最初创建数组的语法非常相似。前一行代码会将数组槽`0`处的元素更改为`10`:\n\n![](img/da693da7-325f-4d27-b8eb-1f38a9f0786f.png)\n\n通常，要到达阵列的特定插槽，您将编写以下内容:\n\n```cpp\narray[ slotNumber ] = value to put into array; \n```\n\n请记住，数组槽总是从`0`开始索引(有些语言可能从`1`开始，但这很不寻常，可能会引起混淆)。要进入阵列的第一个插槽，请使用`array[0]`。阵的第二个槽是`array[1]`(不是`array[2]`)。前一个数组的最后一个槽是`array[4]`(不是`array[5]`)。`array[5]`数据类型超出数组界限！(在上图中没有索引为 5 的插槽。最高指数为 4。)\n\n不要超出数组的界限！它有时可能会起作用，但其他时候你的程序会因**内存访问冲突**而崩溃(访问不属于你的程序的内存)。一般来说，访问不属于你的程序的内存会导致你的应用崩溃，如果它不立即这样做，你的程序中就会有一个隐藏的 bug，只会偶尔引起问题。对数组进行索引时，必须始终小心。\n\n数组内置于 C++ 中，也就是说，您不需要包含任何特殊的东西就可以立即使用数组。您可以拥有任何类型的数据数组，例如`int`、`double`、`string`数组，甚至您自己的自定义对象类型(`Player`)。\n\n# 锻炼\n\n1.  创建一个由五个字符串组成的数组，并在其中放入一些名称(虚构的或随机的，这并不重要)。\n2.  用三个元素创建一个名为`temps`的双精度数组，并在其中存储最近三天的温度。\n\n# 解决办法\n\n1.  下面是一个包含五个字符串的示例程序:\n\n```cpp\n#include <iostream> \n#include <string> \nusing namespace std; \nint main() \n{ \n  string array[ 5 ];  // declare an \"array\" of 5 strings \n                      // fill slots 0-4 with values \narray[ 0 ] = \"Mariam McGonical\"; \narray[ 1 ] = \"Wesley Snice\"; \narray[ 2 ] = \"Kate Winslett\"; \narray[ 3 ] = \"Erika Badu\"; \narray[ 4 ] = \"Mohammad\"; \n  // print out the contents \n  for( int index = 0; index < 5; index++ ) \n    cout << array[ index ] << endl; \n} \n```\n\n2.  以下只是数组:\n\n```cpp\ndouble temps[ 3 ]; \n// fill slots 0-2 with values \ntemps[ 0 ] = 0; \ntemps[ 1 ] = 4.5; \ntemps[ 2 ] = 11; \n```\n\n# C++ 风格的动态大小数组(新建[]和删除[])\n\n您可能会想到，在程序开始时，我们并不总是知道数组的大小。我们需要动态分配阵列的大小。\n\n然而，如果你尝试过，你可能已经注意到这是行不通的！\n\n让我们尝试使用`cin`命令从用户那里获取数组大小。让我们询问用户他想要多大的阵列，并尝试为他创建一个这样大小的阵列:\n\n```cpp\n#include <iostream> \nusing namespace std; \nint main() \n{ \n  cout << \"How big?\" << endl; \n  int size;       // try and use a variable for size.. \n  cin >> size;    // get size from user \n  int array[ size ];  // get error\n} \n```\n\n我们得到一个错误。问题是编译器想要分配数组的大小。但是，除非变量大小标记为`const`，否则编译器在编译时无法确定其值。C++ 编译器无法在编译时调整数组的大小，因此会生成编译时错误。\n\n要解决这个问题，我们必须动态分配数组(在“堆”上):\n\n```cpp\n#include <iostream> \nusing namespace std; \nint main() \n{ \n  cout << \"How big?\" << endl; \n  int size;       // try and use a variable for size.. \n  cin >> size; \n  int *array = new int[ size ];  // this works \n  // fill the array and print \nfor( int index = 0; index < size; index++ ) \n{ \n  array[ index ] = index * 2; \n  cout << array[ index ] << endl; \n} \ndelete[] array; // must call delete[] on array allocated with  \n                // new[]! \n} \n```\n\n所以，这里的教训如下:\n\n*   要动态分配某种类型的数组(例如`int`)，必须使用新的`int[numberOfElementsInArray]`。\n*   分配有`new[]`的数组必须在后面用`delete[]`删除，否则会出现内存泄漏(那是带方括号的`delete[]`；不定期删除)！\n\n# 动态 C 风格数组\n\nc 风格的数组是一个遗留的话题，但是它们仍然值得讨论，因为即使它们很老了，你可能仍然会看到它们有时被使用。\n\n我们声明 C 风格数组的方式如下:\n\n```cpp\n#include <iostream> \nusing namespace std; \nint main() \n{ \n  cout << \"How big?\" << endl; \n  int size;       // try and use a variable for size.. \n  cin >> size; \n  // the next line will look weird.. \n  int *array = (int*)malloc( size*sizeof(int) ); // C-style \n  // fill the array and print \nfor( int index = 0; index < size; index++ ) \n  { \n    //At this point the syntax is the same as with regular arrays.\n    array[ index ] = index * 2; \n    cout << array[ index ] << endl; \n  } \nfree( array ); // must call free() on array allocated with  \n               // malloc() (not delete[]!) \n} \n```\n\n这里强调了不同之处。\n\n使用`malloc()`函数创建一个 C 风格的数组。malloc 这个词代表内存分配。该函数要求您传入要创建的数组的大小(以字节为单位)，而不仅仅是数组中想要的元素数量。为此，我们将请求的元素数量(`size`)乘以数组内部类型的`sizeof`。下表列出了几种典型 C++ 类型的字节大小:\n\n| C++ 基元类型 | `sizeof`(字节大小) |\n| `int` | `4` |\n| `float` | `4` |\n| `double` | `8` |\n| `long long` | `8` |\n\n使用`malloc()`功能分配的内存必须稍后使用`free()`释放。\n\n# 向量\n\n还有一种创建数组的方法，这种方法最容易使用，也是许多程序员的首选——使用向量。想象一下，在前面的任何一个例子中，你正在向一个数组中添加新的项目，在程序运行时突然用完了空间。你会怎么做？您可以创建一个全新的数组并复制所有内容，但正如您可能猜测的那样，这需要大量的额外工作和处理。那么，如果你有一种类型的阵列，在幕后为你处理类似的案件，而你甚至没有问呢？\n\n这就是向量的作用。一个向量是标准模板库的一个成员(我们将在几章中讨论模板，所以耐心一点)，和前面的例子一样，你可以在尖括号(`<>`)中设置类型。你创建一个像这样的向量:\n\n```cpp\nvector<string> names; // make sure to add #include <vector> at the top\n```\n\n这基本上是说你正在创建一个字符串向量，称为名称。要向向量添加新项目，可以使用`push_back()`功能，如下所示:\n\n```cpp\nnames.push_back(\"John Smith\");\n```\n\n这会将您传入的项目添加到向量的末尾。你想叫多少次`push_back()`就叫多少次，只要向量用完空间，它就会自动增加大小，你什么都不用做！因此，您可以继续添加您想要的项目(在合理范围内，因为您最终可能会耗尽内存)，而不用担心内存是如何管理的。\n\n向量还添加了其他有用的函数，例如`size()`，它告诉你一个向量包含多少项(在标准数组中，你必须自己记录这些)。\n\n一旦你创建了一个向量，你可以把它当作一个数组来使用标准的`[]`语法访问它:\n\n```cpp\n//Make it unsigned int to avoid a signed/unsigned mismatch error\nfor (unsigned int i = 0; i < names.size(); i++)\n{\n    //If you get an error about << add #include <string> at the top\n    cout << names[i] << endl; //endl tells it to go to the next line\n}\n```\n\n# 摘要\n\n本章向您介绍了 C 和 C++ 风格的数组和向量。在大多数 UE4 代码中，你会使用 UE4 编辑器内置的集合类(`TArray<T>`)，类似于向量。然而，要成为一名非常优秀的 C++ 程序员，你需要熟悉基本的 C 和 C++ 风格的数组。\n\n我们现在已经介绍了足够多的基础 C++ 来继续下一步的 UE4，包括演员和棋子。"
  },
  {
    "path": "docs/learn-cpp-build-game-ue4/08.md",
    "content": "# 八、演员和棋子\n\n现在，我们将真正深入研究 UE4 代码。起初，这看起来让人望而生畏。UE4 类框架很庞大，但不用担心:框架很庞大，所以你的代码不必如此。你会发现，你可以用更少的代码完成很多事情，在屏幕上显示很多东西。这是因为 UE4 引擎代码如此广泛且编程良好，以至于它们可以轻松完成几乎任何与游戏相关的任务。只要调用正确的功能，瞧，你想看的就会出现在屏幕上。框架的整个概念是，它旨在让你获得你想要的游戏性，而不必花很多时间去解决细节问题。\n\n本章的学习成果如下:\n\n*   演员对棋子\n*   创造一个让你的演员参与的世界\n*   UE4 编辑器\n*   从零开始\n*   给场景增加一个演员\n*   创建玩家实体\n*   编写控制游戏角色的 C++ 代码\n*   创建非玩家角色实体\n*   显示每个 NPC 对话框中的报价\n\n# 演员对棋子\n\n在这一章中，我们将讨论演员和棋子。虽然听起来典当会是一个比演员更基础的阶层，但实际上恰恰相反。一个 UE4 演员(`Actor`类)对象是 UE4 游戏世界中可以放置的东西的基本类型。为了在 UE4 世界放置任何东西，你必须从`Actor`类派生。\n\nA `Pawn`是一个代表你或者电脑的**人工智能** ( **AI** )可以在屏幕上控制的东西的物体。`Pawn`职业来源于`Actor`职业，拥有由玩家直接控制或者通过 AI 脚本控制的额外能力。当一个棋子或演员被一个控制器或人工智能控制时，它被称为被那个控制器或人工智能拥有。\n\n把`Actor`类想象成一个戏剧中的角色(虽然它也可能是一个戏剧中的道具)。你的游戏世界将由一群*演员*组成，所有人一起行动，让游戏运行起来。游戏角色，**非玩家角色** ( **NPC** s)，甚至宝箱都会是演员。\n\n# 创造一个让你的演员参与的世界\n\n在这里，我们将从头开始，创建一个基本的水平，我们可以把我们的游戏角色。UE4 团队已经很好地展示了如何使用世界编辑器在 UE4 中创建一个世界。我希望您花点时间通过执行以下步骤来创建自己的世界:\n\n1.  创建一个新的空白 UE4 项目来开始。为此，在虚幻启动器中，单击最近安装的引擎旁边的启动按钮，如下图所示:\n\n![](img/3d0f9dc1-a80e-4e54-9c15-0c8881dad25a.png)\n\n这将启动虚幻编辑器。虚幻编辑器用于可视化编辑你的游戏世界。您将在虚幻编辑器中花费大量时间，因此请花一些时间进行实验并玩它。\n\n我将只介绍如何使用 UE4 编辑器的基本知识。然而，你需要让你的创造力流动起来，并投入一些时间来熟悉编辑。\n\nTo learn more about the UE4 editor, take a look at the *Getting Started: Introduction to the UE4 Editor* playlist, which is available at [https://www.youtube.com/playlist?list=PLZlv_N0_O1gasd4IcOe9Cx9wHoBB7rxFl](https://www.youtube.com/playlist?list=PLZlv_N0_O1gasd4IcOe9Cx9wHoBB7rxFl).\n\n2.  您将看到“项目”对话框。下面的屏幕截图显示了要执行的步骤，数字对应于它们需要执行的顺序:\n\n![](img/a610e410-863d-4628-888b-504b78722746.png)\n\n3.  执行以下步骤创建项目:\n    1.  选择屏幕顶部的“新建项目”选项卡。\n    2.  单击 C++ 选项卡(第二子选项卡)。\n    3.  从可用的项目列表中选择“基本代码”。\n    4.  设置你的项目所在的目录(我的是 Y:虚幻项目)。选择一个有大量空间的硬盘位置(最终项目将在 1.5 GB 左右)。\n    5.  说出你的项目。我叫我的 GoldenEgg。\n    6.  单击创建项目以完成项目创建。\n\n一旦你做到了这一点，UE4 启动器将启动 Visual Studio(或 Xcode)。这可能需要一段时间，进度条可能会出现在其他窗口后面。只有几个源文件可用，但我们现在不打算接触这些。\n\n4.  确保从屏幕顶部的配置管理器下拉列表中选择了开发编辑器，如下图所示:\n\n![](img/6107dc53-907d-420b-bfdb-9b37e848dcdf.png)\n\n虚幻编辑器也已经启动，如下图所示:\n\n![](img/0c5ccf1d-cd4b-4fb6-8595-b3376e98bdab.png)\n\n# UE4 编辑器\n\n我们将在这里探索 UE4 编辑器。我们将从控件开始，因为知道如何在虚幻中导航很重要。\n\n# 编辑器控件\n\n如果你以前从未使用过 3D 编辑器，这些控件可能会很难学习。这些是编辑模式下的基本导航控件:\n\n*   使用箭头键在场景中移动\n*   按下*向上翻页*或*向下翻页*垂直上下移动\n*   鼠标左键单击并向左或向右拖动，以更改您面对的方向\n*   鼠标左键点击+上下拖动至*移动*(前后移动相机，与按上下箭头键相同)\n*   鼠标右键单击+拖动可改变您面临的反应\n*   鼠标中键单击+拖动可平移视图\n*   鼠标右键点击 *W* 、 *A* 、 *S* 和 *D* 键在场景中移动\n\n# 播放模式控制\n\n点击顶部栏中的播放按钮，如下图所示。这将启动播放模式:\n\n![](img/9110d2aa-8a04-46d7-b1b4-f6520d6fdf75.png)\n\n一旦你点击播放按钮，控制就会改变。在播放模式下，控制如下:\n\n*   移动的 *W* 、 *A* 、 *S* 和 *D* 键\n*   向左或向右箭头键分别向左或向右看\n*   鼠标移动来改变你看的方向\n*   按*键退出播放模式并返回编辑模式*\n\n在这一点上，我建议你尝试在场景中添加一堆形状和对象，并尝试用不同的*材质*给它们上色。\n\n# 向场景添加对象\n\n将对象添加到场景中就像从内容浏览器选项卡中拖放对象一样简单，如下所示:\n\n1.  默认情况下，“内容浏览器”选项卡停靠在窗口底部。如果看不到，只需选择“窗口”并导航到“内容浏览器”即可显示:\n\n![](img/807f76e7-3dcc-47e5-8257-83ad5a5ef5e2.png)\n\n确保内容浏览器可见，以便向您的级别添加对象\n\n2.  双击`StarterContent`文件夹打开。\n3.  双击`Props`文件夹，找到可以拖到场景中的对象。\n\n4.  将内容浏览器中的内容拖放到游戏世界中:\n\n![](img/836a4d4e-0f9a-4ba5-99cf-4934c60f92ed.png)\n\n5.  要调整对象的大小，请按键盘上的 *R* (点击 *W* 再次移动，或点击 *E* 旋转对象)。对象周围的操纵器将显示为方框，表示调整大小模式:\n\n![](img/a2034959-5072-4e20-ba52-d6b7d91e2461.png)\n\n6.  要更改用于绘制对象的材质，只需从“材质”文件夹内的“内容浏览器”窗口中拖放新材质:\n\n![](img/7eb693d2-dcc1-4c9c-a3ba-6236a1ae129f.png)\n\n材料就像颜料。只需将所需的材料拖放到要在其上绘画的对象上，就可以在对象上涂上所需的任何材料。材料只是皮囊；它们不会改变对象的其他属性(如重量)。\n\n# 开始一个新的水平\n\n如果要从头开始创建级别，请执行以下步骤:\n\n1.  单击文件并导航至新级别...，如下所示:\n\n![](img/13852d6b-750d-4e72-a365-ac1071e21140.png)\n\n2.  然后，您可以在默认、虚拟现实-基本和空级别之间进行选择。我认为选择空级别是个好主意:\n\n![](img/b38499e3-8b1e-46f2-a2d1-92f421a87646.png)\n\n3.  新的关卡开始时将完全是黑色的。再次尝试从内容浏览器选项卡拖放一些对象。\n    这一次，我为地平面添加了一个调整了大小的 shapes/shape_plane(模式下不要使用常规平面，否则一旦添加玩家就会掉进去)，并用 T_ground_Moss_D、几个道具/ SM_Rocks 和粒子/ P_Fire 对其进行了纹理化。\n    一定要保存好你的地图。这是我的地图的快照(你的看起来怎么样？):\n\n![](img/f1d1b823-1c5e-422e-9501-3c2afa46821f.png)\n\n4.  如果要更改启动编辑器时打开的默认级别，请转到编辑|项目设置|地图和模式；然后，您将看到一个游戏默认地图和编辑器启动地图设置，如下图所示:\n\n![](img/f5069206-46dc-4fd0-af8c-7e63d548efa0.png)\n\n只要确保先保存当前场景！\n\n# 添加光源\n\n请注意，当您尝试运行场景时，它可能会完全(或大部分)呈现黑色。这是因为你还没有在里面放光源！\n\n在前面的场景中，P_Fire 粒子发射器充当光源，但它只发出少量的光。为了确保场景中的一切看起来都很亮，您应该添加一个光源，如下所示:\n\n1.  转到窗口，然后单击模式以确保显示光源面板:\n\n![](img/536b5a94-2f3d-4d35-b2b3-4c7399e8ad5e.png)\n\n2.  从“模式”面板中，将其中一个灯光对象拖到场景中:\n\n![](img/9e64a618-4720-4acc-a369-83b053f2b03c.png)\n\n3.  选择灯泡和盒子图标(它看起来像蘑菇，但不是)。\n4.  单击左侧面板中的“灯光”。\n5.  选择你想要的光的类型，然后把它拉进你的场景。\n\n如果您没有光源，当您尝试运行它时(或者如果场景中没有对象)，您的场景将显示为完全黑色。\n\n# 碰撞体积\n\n你可能已经注意到，到目前为止，相机只是通过至少一些场景的几何图形，即使在播放模式。这可不好。让我们把它做好，这样玩家就不能在我们的场景中穿过岩石。\n\n有几种不同类型的碰撞体积。一般来说，完美的网格-网格碰撞在运行时代价太高。相反，我们使用近似(包围体)来猜测碰撞体。\n\nA mesh is the actual geometry of an object.\n\n# 添加碰撞体积\n\n我们要做的第一件事是将碰撞体积与场景中的每个岩石相关联。\n\n我们可以从 UE4 编辑器中这样做，如下所示:\n\n1.  单击场景中要为其添加碰撞体积的对象。\n2.  在“世界大纲视图”选项卡中右键单击该对象(默认显示在屏幕右侧)，然后选择编辑，如下图所示:\n\n![](img/cfd4d03d-80a9-4853-9685-7727f7d73ab6.png)\n\nYou will find yourself in the mesh editor.\n\n3.  转到碰撞菜单，然后单击添加胶囊简化碰撞:\n\n![](img/934c27c4-7f97-421e-8b25-4cbdb7f064d6.png)\n\n4.  添加成功后，碰撞体积将显示为围绕对象的一串线条，如下图所示:\n\n![](img/ddf3cd4b-6b70-4b18-8664-c32c781dae5b.png)\n\nThe default collision capsule (left) and manually resized versions (right)\n\n5.  您可以根据需要调整大小(R)、旋转(E)、移动(W)和更改碰撞体积，就像在 UE4 编辑器中操纵对象一样。\n6.  添加完碰撞网格后，保存并返回编辑器主窗口，点击播放；你会注意到你不能再通过你的可碰撞物体。\n\n# 将玩家添加到场景中\n\n现在我们已经有一个场景开始运行，我们需要添加一个演员到场景中。让我们首先为玩家添加一个头像，完成一个碰撞体。为此，我们必须从 UE4 `GameFramework`中继承一个类，如`Actor`或`Character`。\n\n为了创建玩家的屏幕表现，我们需要从虚幻中的`ACharacter`类派生。\n\n# 继承自 UE4 游戏框架类\n\nUE4 使得从基础框架类继承变得容易。您所要做的就是执行以下步骤:\n\n1.  在 UE4 编辑器中打开您的项目。\n2.  转到文件，然后选择新建 C++ 类...：\n\n![](img/88ecd03a-96b4-48a8-a5b5-fc45d6c067d7.png)\n\n导航到文件|新的 C++ 类...将允许你从任何 UE4 游戏框架类派生\n\n3.  选择要从中派生的基类。你有角色、棋子、演员等等，但现在，我们将从角色中得到:\n\n![](img/b2bf7b68-86ba-4a8a-a4ad-7c0c2cb7fee1.png)\n\n4.  选择要从中派生的 UE4 类。\n5.  单击“下一步”以显示该对话框，您可以在其中命名该类。我给我的玩家等级命名为`Avatar`:\n\n![](img/72788a9d-a226-4aed-aac5-033641857c8d.png)\n\n6.  点击创建类，用代码创建类，如前面的截图所示。\n\n让 UE4 刷新你的 Visual Studio 或 Xcode 项目，如果它问你。从解决方案资源管理器中打开新的`Avatar.h`文件。\n\nUE4 生成的代码看起来会有点奇怪。还记得我在[第五章](05.html)、*功能和宏*中建议大家避开的宏吗？UE4 代码广泛使用宏。这些宏用于复制和粘贴样板启动代码，让您的代码与 UE4 编辑器集成。\n\n`Avatar.h`文件的内容如下代码所示:\n\n```cpp\n#pragma once\n\n#include \"CoreMinimal.h\"\n#include \"GameFramework/Character.h\"\n#include \"Avatar.generated.h\"\n\nUCLASS()\nclass GOLDENEGG_API AAvatar : public ACharacter\n{\n    GENERATED_BODY()\n\npublic:\n    // Sets default values for this character's properties\n    AAvatar();\n\nprotected:\n    // Called when the game starts or when spawned\n    virtual void BeginPlay() override;\n\npublic:    \n    // Called every frame\n    virtual void Tick(float DeltaTime) override;\n\n    // Called to bind functionality to input\n    virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;\n\n};\n```\n\n我们先来谈谈宏。\n\n`UCLASS()`宏基本上使你的 C++ 代码类在 UE4 编辑器中可用。`GENERATED_BODY()`宏复制并粘贴 UE4 需要的代码，以使您的类作为 UE4 类正常运行。\n\nFor `UCLASS()` and `GENERATED_BODY()`, you don't truly need to understand how UE4 works its magic. You just need to make sure that they are present at the right spot (where they were when you generated the class).\n\n# 将模型与化身类相关联\n\n现在，我们需要将模型与角色对象相关联。为此，我们需要一个可以玩的模型。幸运的是，UE4 市场上有一整套免费的样车。\n\n# 下载免费模型\n\n要创建播放器对象，请执行以下步骤:\n\n1.  从市场选项卡下载动画初学者工具包文件(免费)。找到它最简单的方法是搜索它:\n\n![](img/f1073a42-b50b-4964-8013-2a20e849d90f.png)\n\n2.  从虚幻启动器，点击市场和搜索动画初学者包，这是免费的时候写这本书。\n\n3.  下载动画初学者工具包文件后，您可以将其添加到之前创建的任何项目中，如下图所示:\n\n![](img/113956d6-bf7c-4cb6-95dd-efc546c88354.png)\n\n4.  当您单击动画初学者工具包下的添加到项目时，您会看到一个弹出窗口，询问要将工具包添加到哪个项目:\n\n![](img/66f17733-1221-4546-92b5-0ed1c9e04730.png)\n\n5.  只需选择您的项目，新的图稿就会出现在您的内容浏览器中。\n\n# 加载网格\n\n一般来说，将你的资产(或游戏中使用的对象)硬编码到游戏中被认为是一种不好的做法。硬编码意味着您编写指定要加载的资产的 C++ 代码。然而，硬编码意味着加载的资产是最终可执行文件的一部分，这意味着更改加载的资产在运行时是不可修改的。这是一种不好的做法。能够在运行时更改加载的资产要好得多。\n\n为此，我们将使用 UE4 蓝图功能来设置我们`Avatar`类的模型网格和碰撞胶囊。\n\n# 从我们的 C++ 类创建蓝图\n\n让我们继续创建一个蓝图—这非常简单:\n\n1.  导航到窗口|开发人员工具，然后单击类查看器，打开类查看器选项卡，如下所示:\n\n![](img/40fa53a9-8873-4867-b9c1-e5646dffd0d8.png)\n\n2.  在“类查看器”对话框中，开始键入 C++ 类的名称。如果您已经正确地从 C++ 代码中创建并导出了该类，它将会出现，如下面的屏幕截图所示:\n\n![](img/b5a0efe8-efa9-4f15-b014-92c290522a84.png)\n\nIf your `Avatar` class does not show up, close the editor and compile/run the C++ project in Visual Studio or Xcode again.\n\n3.  右键单击要创建蓝图的类(在我的例子中，\n    是我的头像类)，然后选择创建蓝图类....\n4.  给你的蓝图起一个独特的名字。我把我的蓝图叫做 BP_Avatar。BP_ 将其标识为蓝图，便于以后搜索。\n\n5.  新蓝图应该会自动打开进行编辑。如果没有，双击 BP_Avatar 打开(添加后会出现在类查看器选项卡中，就在 Avatar 下)，如下图截图所示:\n\n![](img/3354a828-7007-4665-9e70-bfa050b3bdd7.png)\n\n6.  您将看到新 BP_Avatar 对象的蓝图窗口，如下所示(确保选择事件图选项卡):\n\n![](img/ba5ad46c-4b2b-4838-a0d3-5e2a09809db5.png)\n\nFrom this window, you can attach a model to the `Avatar` class visually. Again, this is the recommended pattern since artists will typically be the ones setting up their assets for game designers to play with.\n\n7.  您的蓝图将已经继承了默认的骨骼网格。要查看其选项，请单击左侧封装组件下的网格(继承的):\n\n![](img/82e63e31-ade0-4cf5-9b21-8e1a16aad62b.png)\n\n8.  点击下拉菜单，为你的网格选择“人体模型”:\n\n![](img/5ee928d3-2f02-432c-b73f-8d9336092d2a.png)\n\n9.  如果 SK_Mannequin 没有出现在下拉列表中，请确保下载动画初学者工具包并将其添加到项目中。\n10.  碰撞体积呢？您已经有一个名为封装组件的。如果您的胶囊没有封装您的模型，请调整模型使其适合。\n\nIf your model ended up like mine, the capsule is off the mark! We need to adjust it.\n\n![](img/ccef727b-a32d-4679-aa96-364d28b76c57.png)\n\n11.  点击头像模型，然后点击并按住指向上方的蓝色箭头，如前面的截图所示。把他放下来，直到他能放进胶囊里。如果胶囊不够大，您可以在胶囊半高和胶囊半径下的详细信息选项卡中调整其大小:\n\n![](img/6e2931d1-ffbc-460d-9b34-4cdfc41d572a.png)\n\n您可以通过调整胶囊半高属性来拉伸胶囊\n\n12.  让我们把这个头像加入游戏世界。单击并将您的 BP_Avatar 模型从“类查看器”选项卡拖到 UE4 编辑器中的场景中:\n\n![](img/47722d4b-ab9b-4455-9466-c5c85c77e8c4.png)\n\nOur Avatar class added to the scene\n\n头像的姿势是默认姿势。你想让他充满活力，你说！很简单，只需执行以下步骤:\n\n1.  在蓝图编辑器中点击你的网格，你会在右边的细节下看到动画。注意:如果您出于任何原因关闭了蓝图并重新打开它，您将看不到完整的蓝图。如果发生这种情况，请单击链接打开完整的蓝图编辑器。\n2.  现在，您可以使用动画的蓝图。这样，艺术家可以根据角色正在做的事情来适当地设置动画。如果从`AnimClass`下拉菜单中选择 UE4ASP _ HeroTPP _ animal bluetooth，随着角色的移动，动画将根据蓝图(由艺术家完成)进行调整:\n\n![](img/0904ac8d-0a09-4db4-aa81-d78268b3ed59.png)\n\n如果你保存并编译好蓝图，在游戏主窗口点击播放，你会看到闲置的动画。\n\nWe can't cover everything here. Animation blueprints are covered in [Chapter 11](11.html), *Monsters*. If you're really interested in animation, it wouldn't be a bad idea to sit through a couple of Gnomon Workshop tutorials on IK, animation, and rigging, which can be found at [gnomonworkshop.com/tutorials](http://gnomonworkshop.com/tutorials).\n\n还有一件事:让《阿凡达》的镜头出现在它的背后。这会给你一个第三人称的视角，让你看到整个角色，如下图截图所示，有相应的步骤:\n\n1.  在 BP_Avatar 蓝图编辑器中，选择 BP_Avatar(自身)，然后单击添加组件。\n2.  向下滚动以选择添加摄像机。\n\n视口中将出现一个摄像机。你可以点击摄像头并移动它。把相机放在播放器后面的某个地方。确保播放器上的蓝色箭头与相机朝向相同的方向。如果不是，请旋转化身模型网格，使其面向与蓝色箭头相同的方向:\n\n![](img/98e53d3b-5ffa-429f-842b-32dbd53351ec.png)\n\n模型网格上的蓝色箭头指示模型网格的前进方向。确保摄像机的开口与角色的前向矢量朝向相同的方向。\n\n# 编写控制游戏角色的 C++ 代码\n\n当你启动你的 UE4 游戏时，你可能会注意到相机没有改变。我们现在要做的是使起始字符成为我们的`Avatar`类的一个实例，并使用键盘控制我们的字符。\n\n# 让玩家成为化身类的一个实例\n\n让我们看看我们是怎么做的。在虚幻编辑器中，执行以下步骤:\n\n1.  通过导航到文件|新的 C++ 类来创建游戏模式的子类...以及选择游戏模式库。我给我的起名`GameModeGoldenEgg`:\n\n![](img/23a322b6-c022-43ea-97fb-9807874a5637.png)\n\nUE4 游戏模式包含游戏规则，并描述了游戏如何在引擎上进行。稍后我们将更多地与我们的`GameMode`班合作。目前，我们需要将其子类化。\n\n它应该会在你创建类后自动编译你的 C++ 代码，这样你就可以创建一个`GameModeGoldenEgg`蓝图。\n\n2.  创建游戏模式蓝图，方法是转到顶部菜单栏中的蓝图图标，单击游戏模式新建，然后选择+创建|游戏模式 GoldenEgg(或您在步骤 1 中命名的游戏模式子类):\n\n![](img/2f3f5cc3-d6a3-453a-831f-420ff6bfba75.png)\n\n3.  说出你的蓝图；我称我的为`BP_GameModeGoldenEgg`:\n\n![](img/7a38f985-9ad1-419a-98be-5946eb81ea29.png)\n\n4.  您新创建的蓝图将在蓝图编辑器中打开。如果没有，您可以从“类查看器”选项卡中打开 BP _ GameModeGoldenEgg 类。\n5.  从默认棋子类面板中选择你的 BP_Avatar 类，如下图所示。“默认棋子类别”面板是将用于玩家的对象类型:\n\n![](img/23295e1f-cf29-4e40-bc83-a00c8eef7b91.png)\n\n6.  启动你的游戏。当摄像机放在播放器后面时，您可以看到背面视图:\n\n![](img/09b890d9-96b0-43e7-bec8-5e0ef4b8b9a2.png)\n\n你会注意到你不能动。为什么会这样？答案是因为我们还没有设置控制器输入。下一节将教你如何去做。\n\n# 设置控制器输入\n\n以下是设置输入的步骤:\n\n1.  要设置控制器输入，请转到设置|项目设置...：\n\n![](img/a408251e-921d-4c48-b3aa-f05828af46dc.png)\n\n2.  在左侧面板中，向下滚动，直到在“引擎:\n\n![](img/513f595b-2dbe-4f82-88dc-f9881ae1a587.png)\n\n3.  在右侧，您可以设置一些绑定。单击+添加新绑定，然后单击轴映射旁边的小箭头将其展开。只需添加两个轴映射即可开始，一个名为 Forward(连接到键盘字母 *W* )，一个名为钢鞭(连接到键盘字母 *D* )。记住你设定的名字；我们将在稍后用 C++ 代码查找它们。\n4.  关闭“项目设置”对话框。打开你的 C++ 代码。在`Avatar.h`构造函数中，需要添加两个成员函数声明，如下图所示:\n\n```cpp\nUCLASS()\nclass GOLDENEGG_API AAvatar : public ACharacter\n{\n    GENERATED_BODY()\n\npublic:\n    // Sets default values for this character's properties\n    AAvatar();\n\nprotected:\n    // Called when the game starts or when spawned\n    virtual void BeginPlay() override;\n\npublic:    \n    // Called every frame\n    virtual void Tick(float DeltaTime) override;\n\n    // Called to bind functionality to input\n    virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;\n\n    // New! These 2 new member function declarations \n    // they will be used to move our player around! \n    void MoveForward(float amount);\n    void MoveRight(float amount);\n\n}; \n```\n\n请注意现有函数`SetupPlayerInputComponent`和`Tick`是如何覆盖虚拟函数的。`SetupPlayerInputComponent`是`APawn`基类中的一个虚函数。我们还将向这个函数添加代码。\n\n5.  在`Avatar.cpp`文件中，需要添加功能体。在`Super::SetupPlayerInputComponent(PlayerInputComponent);`下的`SetupPlayerInputComponent`中，添加以下行:\n\n```cpp\n  check(PlayerInputComponent);\n    PlayerInputComponent->BindAxis(\"Forward\", this,\n        &AAvatar::MoveForward);\n    PlayerInputComponent->BindAxis(\"Strafe\", this, &AAvatar::MoveRight);\n```\n\n这个成员函数查找我们刚刚在虚幻编辑器中创建的前向和钢鞭轴绑定，并将它们连接到`this`类中的成员函数。我们应该连接到哪些成员函数？为什么，我们应该连接到`AAvatar::MoveForward`和`AAvatar::MoveRight`。以下是这两个函数的成员函数定义:\n\n```cpp\nvoid AAvatar::MoveForward( float amount ) \n{ \n  // Don't enter the body of this function if Controller is \n  // not set up yet, or if the amount to move is equal to 0 \n  if( Controller && amount ) \n  { \n    FVector fwd = GetActorForwardVector(); \n    // we call AddMovementInput to actually move the \n    // player by `amount` in the `fwd` direction \n    AddMovementInput(fwd, amount); \n  } \n} \n\nvoid AAvatar::MoveRight( float amount ) \n{ \n  if( Controller && amount ) \n  { \n    FVector right = GetActorRightVector(); \n    AddMovementInput(right, amount); \n  } \n} \n```\n\nThe `Controller` object and the `AddMovementInput` function are defined in the `APawn` base class. Since the `Avatar` class derives from `ACharacter`, which in turn derives from `APawn`, we get free use of all the member functions in the `APawn` base class. Now, do you see the beauty of inheritance and code reuse?    If you test this out, make sure you click inside the game window, because otherwise the game won't receive keyboard events.\n\n# 锻炼\n\n添加轴绑定和 C++ 函数，将播放器向左后移动。\n\nHere's a hint: you only need to add axis bindings if you realize going backward is simply the negative of going forward.\n\n# 解决办法\n\n导航到设置|项目设置，输入两个额外的轴绑定...|输入，如下图所示:\n\n![](img/90c0f3a9-20ee-4edf-a119-0de485d653cc.png)\n\n将 S 和 A 输入缩放-1.0。这将使轴反转，因此在游戏中按下 *S* 键将使玩家向前移动。试试看！\n\n或者，您可以在`AAvatar`类中定义两个完全独立的成员函数，如下所示，并将 *A* 和 *S* 键分别绑定到`AAvatar::MoveLeft`和`AAvatar::MoveBack`(并确保将这些键的绑定添加到`AAvatar::SetupPlayerInputComponent`):\n\n```cpp\nvoid AAvatar::MoveLeft( float amount ) \n{ \n  if( Controller && amount ) \n  { \n    FVector left = -GetActorRightVector(); \n    AddMovementInput(left, amount); \n  } \n} \nvoid AAvatar::MoveBack( float amount ) \n{ \n  if( Controller && amount ) \n  { \n    FVector back = -GetActorForwardVector(); \n    AddMovementInput(back, amount); \n  } \n} \n```\n\n# 偏航和俯仰\n\n我们可以通过设置控制器的偏航和俯仰来改变玩家看的方向。检查以下步骤:\n\n1.  为鼠标添加新的轴绑定，如下图所示:\n\n![](img/92087a57-27cd-4228-ade9-ae57c6b78825.png)\n\n2.  从 C++ 中，给`AAvatar.h`增加两个新的成员函数声明:\n\n```cpp\nvoid Yaw( float amount ); \nvoid Pitch( float amount ); \n```\n\n这些成员函数的主体将进入`AAvatar.cpp`文件:\n\n```cpp\nvoid AAvatar::Yaw(float amount)\n{\n    AddControllerYawInput(200.f * amount * GetWorld()->GetDeltaSeconds());\n}\nvoid AAvatar::Pitch(float amount)\n{\n    AddControllerPitchInput(200.f * amount * GetWorld()->GetDeltaSeconds());\n}\n```\n\n3.  `SetupPlayerInputComponent`增加两行:\n\n```cpp\nvoid AAvatar::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)\n{ \n  // .. as before, plus: \n  PlayerInputComponent->BindAxis(\"Yaw\", this, &AAvatar::Yaw);\n  PlayerInputComponent->BindAxis(\"Pitch\", this, &AAvatar::Pitch); \n} \n```\n\n这里，请注意我是如何将`Yaw`和`Pitch`函数中的`amount`值乘以 200 的。这个数字代表鼠标的灵敏度。您可以(应该)在`AAvatar`类中添加一个`float`成员，以避免硬编码这个敏感号。\n\n`GetWorld()->GetDeltaSeconds()`给出最后一帧和这一帧之间经过的时间。不是很多；`GetDeltaSeconds()`大部分时间应该在 16 毫秒(0.016 s)左右(如果你的游戏运行速度是 60 fps)。\n\n注意:你可能会注意到，现在推销实际上并不奏效。这是因为你用的是第三人称相机。虽然它可能对这个相机没有意义，但您可以通过进入 BP_Avatar，选择相机，并选中相机选项下的使用棋子控制旋转来让它工作:\n\n![](img/18534c33-1564-4a99-aab9-28422d145e3c.png)\n\n所以，现在我们有了玩家输入和控制。要为您的头像添加新功能，您只需完成以下工作:\n\n1.  通过转到设置|项目设置|输入来绑定您的按键或鼠标操作。\n2.  添加按下该键时运行的成员函数。\n3.  在`SetupPlayerInputComponent`处加一行，将绑定输入的名称连接到我们要在该键被按下时运行的成员函数。\n\n# 创建非玩家角色实体\n\n所以，我们需要创建几个 **NPC** ( **不可玩角色**)。NPC 是游戏中帮助玩家的角色。有些提供特殊物品，有些是商店小贩，有些有信息给玩家。在这个游戏中，当玩家靠近时，他们会做出反应。让我们对一些行为进行编程:\n\n1.  创建另一个字符子类。在 UE4 编辑器中，转到文件|新建 C++ 类...并选择可以创建子类的字符类。说出你的子类`NPC`。\n2.  在 Visual Studio 中编辑您的代码。每个 NPC 都会有一个消息告诉玩家，所以我们在`NPC`类中增加了一个`UPROPERTY() FString`属性。\n\n`FString` is UE4's version of C++'s `<string>` type. When programming in UE4, you should use `FString` objects over C++ STL's `string` objects. In general, you should use UE4's built-in types, as they guarantee cross-platform compatibility.\n\n3.  下面是如何将`UPROPERTY() FString`属性添加到`NPC`类:\n\n```cpp\nUCLASS()\nclass GOLDENEGG_API ANPC : public ACharacter\n{\n    GENERATED_BODY()\n\n    // This is the NPC's message that he has to tell us. \n    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category =\n        NPCMessage)\n        FString NpcMessage;\n    // When you create a blueprint from this class, you want to be  \n    // able to edit that message in blueprints, \n    // that's why we have the EditAnywhere and BlueprintReadWrite  \n    // properties. \npublic:\n    // Sets default values for this character's properties\n    ANPC();\n\nprotected:\n    // Called when the game starts or when spawned\n    virtual void BeginPlay() override;\n\npublic:    \n    // Called every frame\n    virtual void Tick(float DeltaTime) override;\n\n    // Called to bind functionality to input\n    virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;\n\n};\n```\n\n请注意，我们将`EditAnywhere`和`BlueprintReadWrite`属性放入了`UPROPERTY`宏。这将使`NpcMessage`在蓝图中可编辑。\n\nFull descriptions of all the UE4 property specifiers are available at [https://docs.unrealengine.com/latest/INT/Programming/UnrealArchitecture/Reference/Properties/index.html](https://docs.unrealengine.com/latest/INT/Programming/UnrealArchitecture/Reference/Properties/index.html).\n\n4.  重新编译你的项目(就像我们对`Avatar`类所做的那样)。然后，转到类查看器，右键单击您的`NPC`类，并从中创建一个蓝图类。\n5.  你想要创造的每个 NPC 角色都可以是基于`NPC`类的蓝图。为每个蓝图命名一些独特的东西，因为我们将为出现的每个 NPC 选择不同的模型网格和消息，如下图所示:\n\n![](img/4e56443b-06bf-4682-ba59-640982763c4a.png)\n\n6.  打开蓝图并选择网格(继承的)。然后，您可以在骨骼网格下拉列表中更改新角色的材质，使其看起来与玩家不同:\n\n![](img/6539c287-1585-4a05-a469-3b448b3fa947.png)\n\n通过从每个可用元素的下拉列表中进行选择，更改网格属性中角色的材质\n\n7.  在组件选项卡中选择蓝图名称(自身)的详细信息选项卡中，查找`NpcMessage`属性。这是我们在 C++ 代码和蓝图之间的联系；因为我们在`FString NpcMessage`变量上输入了一个`UPROPERTY()`函数，该属性在 UE4 中显示为可编辑，如下图所示:\n\n![](img/a1b93490-aa67-4b8b-bc38-7d39feb6f7e1.png)\n\n8.  将 BP _ NPC _ 欧文拖到场景中。您也可以创建第二个或第三个角色，并确保给它们唯一的名称、外观和消息:\n\n![](img/369f6e1e-e458-4bdd-b7ff-dcdc3605201d.png)\n\n我根据 NPC 的基本类创建了两个 NPC 蓝图:BP _ NPC _ 乔纳森和 BP _ NPC _ 欧文。它们对玩家来说有不同的外观和不同的信息:\n\n![](img/fc0b7f35-74e7-42fe-b77a-6cb51e93f894.png)\n\nJonathan and Owen in the scene\n\n# 显示每个 NPC 对话框中的报价\n\n要显示一个对话框，我们需要一个自定义的**平视显示器** ( **平视显示器**)。在 UE4 编辑器中，转到文件|新建 C++ 类...并选择创建子类的`HUD`类(你需要向下滚动找到它)。根据您的意愿命名您的子类；我已经命名我的`MyHUD`。\n\n创建`MyHUD`类后，让 Visual Studio 重新加载。我们将进行一些代码编辑。\n\n# 在抬头显示器上显示信息\n\n在`AMyHUD`类中，我们需要实现`DrawHUD()`功能，以便将我们的消息绘制到平视显示器上，并初始化一个字体绘制到平视显示器上，如`MyHUD.h`中的以下代码所示:\n\n```cpp\nUCLASS()\nclass GOLDENEGG_API AMyHUD : public AHUD\n{\n    GENERATED_BODY()\npublic:\n    // The font used to render the text in the HUD. \n    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = HUDFont)\n    UFont* hudFont;\n    // Add this function to be able to draw to the HUD! \n    virtual void DrawHUD() override;\n};\n```\n\n抬头显示器字体将在`AMyHUD`类的蓝色打印版本中设置。`DrawHUD()`功能每帧运行一次。为了在框架内绘图，向`AMyHUD.cpp`文件添加一个函数:\n\n```cpp\nvoid AMyHUD::DrawHUD()\n{\n    // call superclass DrawHUD() function first \n    Super::DrawHUD();\n    // then proceed to draw your stuff. \n    // we can draw lines.. \n    DrawLine(200, 300, 400, 500, FLinearColor::Blue);\n    // and we can draw text! \n    const FVector2D ViewportSize = FVector2D(GEngine->GameViewport->Viewport->GetSizeXY());\n    DrawText(\"Greetings from Unreal!\", FLinearColor::White, ViewportSize.X/2, ViewportSize.Y/2, hudFont);\n}\n```\n\n等等！我们还没有初始化字体。我们现在就开始吧:\n\n1.  在蓝图中设置。在编辑器中编译您的 Visual Studio 项目，然后转到顶部的“蓝图”菜单，导航到游戏模式| HUD | +创建|我的 HUD:\n\n![](img/e7338fbe-3349-4835-9170-23c3c8b968d2.png)\n\nCreating a blueprint of the MyHUD class\n\n2.  我称我的为`BP_MyHUD`。找到`Hud Font`，选择下拉菜单，创建一个新的字体资产。我给我的起名`MyHUDFont`:\n\n![](img/7da1c1a0-5e0a-4be6-a077-10aed0bd1e75.png)\n\n3.  在内容浏览器中找到我的字体，双击它进行编辑:\n\n![](img/032d326f-aa66-45f5-bc40-e85bd4610b06.png)\n\n在接下来的窗口中，您可以点击显示`+ Add Font`的位置来创建新的默认字体系列。你可以给它起一个你喜欢的名字，然后点击文件夹图标从你的硬盘中选择一种字体(你可以找到。TTF 或 TrueType 字体在线在许多网站免费-我使用了我发现的闪耀字体)；当您导入字体时，它会要求您保存字体。您还需要将我的主字体中的传统字体大小更改为更大的大小(我使用了 36)。\n\n4.  编辑您的游戏模式蓝图(BP _ GameModeGoldenEgg)，并为抬头显示器类别面板选择您的新`BP_MyHUD`(不是`MyHUD`)类别:\n\n![](img/cf43dd26-ad50-423c-be6f-7bff4f073942.png)\n\n通过运行来编译和测试你的程序！您应该会看到屏幕上打印的文本:\n\n![](img/f46ba1db-4910-4069-a458-cde7dad072cd.png)\n\n# 锻炼\n\n您可以看到文本没有完全居中。这是因为位置是基于文本的左上角，而不是中间。\n\nSee whether you can fix that. Here's a hint: get the width and height of the text and subtract half of that from the viewport width and height/2 you're already using. You'll want to use something similar to the following:\n\n```cpp\n    const FVector2D ViewportSize = FVector2D(GEngine->GameViewport->Viewport->GetSizeXY());\n    const FString message(\"Greetings from Unreal!\");\n    float messageWidth = 0;\n    float messageHeight = 0;\n    GetTextSize(message, messageWidth, messageHeight, hudFont);\n    DrawText(message, FLinearColor::White, (ViewportSize.X - messageWidth) / 2, (ViewportSize.Y - messageHeight) / 2, hudFont);\n```\n\n# 使用 TArray<message></message>\n\n我们要为玩家显示的每个消息都有几个属性:\n\n*   消息的`FString`变量\n*   显示时间的`float`变量\n*   消息颜色的`FColor`变量\n\n所以，我们写一个小小的`struct`函数来包含所有这些信息是有意义的。\n\n在`MyHUD.h`顶部，插入以下`struct`声明:\n\n```cpp\nstruct Message \n{ \n  FString message; \n  float time; \n  FColor color; \n  Message() \n  { \n    // Set the default time. \n    time = 5.f; \n    color = FColor::White; \n  } \n  Message( FString iMessage, float iTime, FColor iColor ) \n  { \n    message = iMessage; \n    time = iTime; \n    color = iColor; \n  } \n}; \n```\n\n现在，在`AMyHUD`类中，我们想要添加这些消息的一个`TArray`。`TArray`是 UE4 定义的一种特殊类型的可动态增长的 C++ 数组。我们将在[第 9 章](09.html)、*模板和常用容器*中介绍`TArray`的详细使用，但是`TArray`的这种简单使用应该是一个很好的介绍，可以让你对数组在游戏中的用途感兴趣。这将被声明为`TArray<Message>`:\n\n```cpp\nUCLASS()\nclass GOLDENEGG_API AMyHUD : public AHUD\n{\n    GENERATED_BODY()\npublic:\n    // The font used to render the text in the HUD. \n    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = HUDFont)\n        UFont* hudFont;\n    // New! An array of messages for display \n    TArray<Message> messages;\n    virtual void DrawHUD() override;\n    // New! A function to be able to add a message to display \n    void addMessage(Message msg);\n};\n```\n\n还将 **`#include \"CoreMinimal.h\"`** *添加到文件的顶部*。\n\n现在，每当 NPC 有消息要显示时，我们只需要用我们的消息呼叫`AMyHud::addMessage()`。该消息将被添加到要显示的消息的`TArray`中。当消息过期时(经过一定时间)，它将从抬头显示器中删除。\n\n在`AMyHUD.cpp`文件中，添加以下代码:\n\n```cpp\nvoid AMyHUD::DrawHUD()\n{\n    Super::DrawHUD();\n    // iterate from back to front thru the list, so if we remove \n    // an item while iterating, there won't be any problems \n    for (int c = messages.Num() - 1; c >= 0; c--)\n    {\n        // draw the background box the right size \n        // for the message \n        float outputWidth, outputHeight, pad = 10.f;\n        GetTextSize(messages[c].message, outputWidth, outputHeight,\n            hudFont, 1.f);\n\n        float messageH = outputHeight + 2.f*pad;\n        float x = 0.f, y = c * messageH;\n\n        // black backing \n        DrawRect(FLinearColor::Black, x, y, Canvas->SizeX, messageH\n        );\n        // draw our message using the hudFont \n        DrawText(messages[c].message, messages[c].color, x + pad, y +\n            pad, hudFont);\n\n        // reduce lifetime by the time that passed since last  \n        // frame. \n        messages[c].time -= GetWorld()->GetDeltaSeconds();\n\n        // if the message's time is up, remove it \n        if (messages[c].time < 0)\n        {\n            messages.RemoveAt(c);\n        }\n    }\n}\n\nvoid AMyHUD::addMessage(Message msg)\n{\n    messages.Add(msg);\n}\n```\n\n`AMyHUD::DrawHUD()`函数现在绘制`messages`数组中的所有消息，并按照自上一帧以来经过的时间量排列`messages`数组中的每个消息。一旦过期消息的`time`值降至 0 以下，它们将从`messages`集合中删除。\n\n# 锻炼\n\n重构`DrawHUD()`函数，使得将消息绘制到屏幕上的代码在一个单独的函数中，称为`DrawMessages()`。您可能想要创建至少一个示例消息对象，并使用它调用`addMessage`，以便您可以看到它。\n\n`Canvas`变量只在`DrawHUD()`中可用，所以你必须在类级变量中保存`Canvas->SizeX`和`Canvas->SizeY`。\n\nRefactoring means changing the way code works internally so that it is more organized or easier to read but still has the same apparent result to the user running the program. Refactoring often is a good practice. The reason why refactoring occurs is because nobody knows exactly what the final code should look like when they start writing it.\n\n# 当玩家在 NPC 附近时触发事件\n\n要触发 NPC 附近的事件，我们需要设置一个比默认胶囊形状稍宽的额外碰撞检测体积。额外的碰撞检测体积将是围绕每个 NPC 的球体。当玩家进入 NPC 球体时，NPC(如下所示)会做出反应并显示一条信息:\n\n![](img/492972ff-87a2-4db4-a813-5aa37fd55b3f.png)\n\n我们将把暗红色的球体添加到 NPC，这样它就可以知道玩家何时在附近。\n\n在您的`NPC.h`类文件中，在顶部添加`#include \"Components/SphereComponent.h\"`和以下代码:\n\n```cpp\nUCLASS() class GOLDENEGG_API ANPC : public ACharacter {\n    GENERATED_BODY()\n\npublic:\n    // The sphere that the player can collide with tob\n    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category =\n        Collision)\n        USphereComponent* ProxSphere;\n    // This is the NPC's message that he has to tell us. \n    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category =\n        NPCMessage)\n        FString NpcMessage; // The corresponding body of this function is \n                            // ANPC::Prox_Implementation, __not__ ANPC::Prox()! \n                            // This is a bit weird and not what you'd expect, \n                            // but it happens because this is a BlueprintNativeEvent \n    UFUNCTION(BlueprintNativeEvent, Category = \"Collision\")\n        void Prox(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp,\n            int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);\n    // You shouldn't need this unless you get a compiler error that it can't find this function.\n    virtual int Prox_Implementation(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp,\n        int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);\n\n    // Sets default values for this character's properties\n    ANPC(const FObjectInitializer& ObjectInitializer);\n\nprotected:\n    // Called when the game starts or when spawned\n    virtual void BeginPlay() override;\n\npublic:\n    // Called every frame\n    virtual void Tick(float DeltaTime) override;\n\n    // Called to bind functionality to input\n    virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;\n};\n```\n\n这看起来有点乱，但实际上没那么复杂。在这里，我们声明了一个额外的被称为`ProxSphere`的包围球体积，它可以检测玩家何时在 NPC 附近。\n\n在`NPC.cpp`文件中，我们需要添加以下代码来完成接近检测:\n\n```cpp\nANPC::ANPC(const FObjectInitializer& ObjectInitializer)\n : Super(ObjectInitializer)\n{\n ProxSphere = ObjectInitializer.CreateDefaultSubobject<USphereComponent>(this,\n TEXT(\"Proximity Sphere\"));\n ProxSphere->AttachToComponent(RootComponent, FAttachmentTransformRules::KeepWorldTransform);\n ProxSphere->SetSphereRadius(32.0f);\n // Code to make ANPC::Prox() run when this proximity sphere \n // overlaps another actor. \n ProxSphere->OnComponentBeginOverlap.AddDynamic(this, &ANPC::Prox);\n NpcMessage = \"Hi, I'm Owen\";//default message, can be edited \n // in blueprints \n}\n\n// Note! Although this was declared ANPC::Prox() in the header, \n// it is now ANPC::Prox_Implementation here. \nint ANPC::Prox_Implementation(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp,\n int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult) \n{ \n    // This is where our code will go for what happens \n    // when there is an intersection \n    return 0;\n} \n```\n\n# 当玩家在附近时，让 NPC 在平视显示器上显示一些东西\n\n当玩家靠近 NPC 球体碰撞体积时，向平视显示器显示一条消息，提醒玩家 NPC 在说什么。\n\n这是`ANPC::Prox_Implementation`的完整实现:\n\n```cpp\nint ANPC::Prox_Implementation(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp,\n    int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)\n{ \n    // if the overlapped actor is not the player, \n    // you should just simply return from the function \n    if( Cast<AAvatar>( OtherActor ) == nullptr ) { \n        return -1; \n    } \n    APlayerController* PController = GetWorld()->GetFirstPlayerController(); \n    if( PController ) \n    { \n        AMyHUD * hud = Cast<AMyHUD>( PController->GetHUD() ); \n        hud->addMessage( Message( NpcMessage, 5.f, FColor::White ) ); \n    } \n    return 0;\n} \n```\n\n此外，请确保在文件顶部添加以下内容:\n\n```cpp\n#include \"Avatar.h\"\n#include \"MyHud.h\"\n```\n\n我们在这个函数中做的第一件事是将`OtherActor`(靠近 NPC 的东西)铸造成`AAvatar`。当`OtherActor`是`AAvatar`对象时，演员成功了(不是`nullptr`)。我们得到了平视显示器对象(碰巧附在播放器控制器上)，并将 NPC 的信息传递给平视显示器。只要玩家在 NPC 周围的红色边界球内，就会显示该消息:\n\n![](img/92ceff6f-a598-4b30-8c21-855ab81441b3.png)\n\nJonathan's greeting\n\n# 练习\n\n试试这些，进行更多练习:\n\n1.  为 NPC 的名字添加一个`UPROPERTY`函数名，这样 NPC 的名字就可以在蓝图中编辑，类似于 NPC 给玩家的信息。在输出中显示 NPC 的名字。\n2.  为 NPC 的脸部纹理添加一个`UPROPERTY`功能(键入`UTexture2D*`)。在输出的信息旁边画出 NPC 的脸。\n3.  将玩家的血量渲染为条形(实心矩形)。\n\n# 解决方法\n\n将以下属性添加到`ANPC`类中:\n\n```cpp\n// This is the NPC's name \nUPROPERTY(EditAnywhere, BlueprintReadWrite, Category = NPCMessage) \nFString name; \n```\n\n然后，在`ANPC::Prox_Implementation`中，将传递给抬头显示器的字符串改为:\n\n```cpp\nname + FString(\": \") + NpcMessage\n```\n\n这样，NPC 的名字就会附在信息上。\n\n将`this`属性添加到`ANPC`类中:\n\n```cpp\nUPROPERTY(EditAnywhere, BlueprintReadWrite, Category = NPCMessage) \nUTexture2D* Face; \n```\n\n然后，你可以在蓝图中选择要贴在 NPC 脸上的脸图标。\n\n给你的`struct Message`加上一个纹理:\n\n```cpp\nUTexture2D* tex; \n```\n\n要渲染这些图标，您需要添加对`DrawTexture()`的调用，并向其传递正确的纹理:\n\n```cpp\nDrawTexture( messages[c].tex, x, y, messageH, messageH, 0, 0, 1, 1  \n   );\n```\n\n在渲染纹理之前，一定要检查它是否有效。图标应该类似于屏幕顶部显示的内容:\n\n![](img/945c32ed-8d00-47bf-84c0-c8ed6a4f0b24.png)\n\n这是一个在条形图中绘制玩家剩余生命值的函数的外观:\n\n```cpp\nvoid AMyHUD::DrawHealthbar()\n{\n    // Draw the healthbar. \n    AAvatar *avatar = Cast<AAvatar>(\nb        UGameplayStatics::GetPlayerPawn(GetWorld(), 0));\n    float barWidth = 200, barHeight = 50, barPad = 12, barMargin = 50;\n    float percHp = avatar->Hp / avatar->MaxHp;\n    const FVector2D ViewportSize = FVector2D(GEngine->GameViewport->Viewport->GetSizeXY());\n    DrawRect(FLinearColor(0, 0, 0, 1), ViewportSize.X - barWidth -\n        barPad - barMargin, ViewportSize.Y - barHeight - barPad -\n        barMargin, barWidth + 2 * barPad, barHeight + 2 * barPad);  DrawRect(FLinearColor(1 - percHp, percHp, 0, 1), ViewportSize.X\n            - barWidth - barMargin, ViewportSize.Y - barHeight - barMargin,\n            barWidth*percHp, barHeight);\n}\n```\n\n还需要在 Avatar 类中添加`Hp`和`MaxHp`(测试时可以只设置现在的默认值)，并在文件顶部添加以下内容:\n\n```cpp\n#include \"Kismet/GameplayStatics.h\"\n#include \"Avatar.h\"\n```\n\n# 摘要\n\n在这一章里，我们看了很多材料。我们向您展示了如何创建一个角色并将其显示在屏幕上，如何使用轴绑定来控制您的角色，以及如何创建和显示可以向平视显示器发布消息的 NPC。现在看起来可能让人望而生畏，但一旦你多加练习，这就有意义了。\n\n在接下来的章节中，我们将进一步开发我们的游戏，增加库存系统和拾取物品，以及代码和概念来说明玩家携带的东西。然而，在此之前，在下一章中，我们将对一些 UE4 容器类型进行深入的探索。"
  },
  {
    "path": "docs/learn-cpp-build-game-ue4/09.md",
    "content": "# 九、模板和常用容器\n\n在[第 7 章](07.html)*动态内存分配*中，我们谈到了如果您想要创建一个在编译时大小未知的新数组，您将如何使用动态内存分配。动态内存分配的形式为`int * array = new int[ number_of_elements ]`。\n\n您还看到，使用`new[]`关键字的动态分配要求您稍后在数组上调用`delete[]`，否则您会有内存泄漏。必须以这种方式管理内存是一项艰巨的工作。\n\n有没有办法创建一个动态大小的数组，让 C++ 自动为你管理内存？答案是肯定的。有一些 C++ 对象类型(通常称为容器)可以自动处理动态内存分配和释放。UE4 提供了两种容器类型来将数据存储在可动态调整大小的集合中。\n\n有两组不同的模板容器。有 UE4 系列容器(从`T*`开始)和 C++ **标准模板库** ( **STL** )系列容器。UE4 容器和 C++ STL 容器之间有一些区别，但是区别不是很大。UE4 容器集的编写考虑了游戏性能。C++ STL 容器也表现良好，它们的接口更加一致(API 中的一致性是您更喜欢的)。你用哪个容器组由你决定。但是，建议您使用 UE4 容器集，因为它保证您在尝试编译代码时不会有跨平台问题。\n\n我们将在本章中讨论以下主题:\n\n*   在 UE4 中调试输出\n*   模板和容器\n*   欧盟四国杯\n*   测试和\n*   常用容器的 C++ STL 版本\n\n# 在 UE4 中调试输出\n\n本章(以及后面章节)中的所有代码都要求您在 UE4 项目中工作。为了测试`TArray`，我创建了一个名为`TArrays`的基本代码项目。在`ATArraysGameMode::ATArraysGameMode`构造器中，我使用调试输出功能将文本打印到控制台。\n\n以下是`TArraysGameMode.cpp`中的代码外观:\n\n```cpp\n#include \"TArraysGameMode.h\"\n#include \"Engine/Engine.h\"\n\nATArraysGameMode::ATArraysGameMode(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer)\n{\n    if (GEngine)\n    {\n        GEngine->AddOnScreenDebugMessage(-1, 30.f, FColor::Red, \n        TEXT(\"Hello!\"));\n    }\n}\n```\n\n确保您也将该功能添加到`.h`文件中。如果您编译并运行这个项目，当您开始游戏时，您会在游戏窗口的左上角看到调试文本。您可以随时使用调试输出来查看程序的内部。只需确保在调试输出时`GEngine`对象存在。前面代码的输出显示在下面的截图中(请注意，您可能需要将其作为独立游戏运行才能看到它):\n\n![](img/e8c01c25-6466-44de-bba4-a0db06c1f05d.png)\n\n# 模板和容器\n\n模板是一种特殊类型的对象。模板对象允许您指定它应该预期的数据类型。例如，正如您将很快看到的，您可以运行一个`TArray<T>`变量。这是一个模板的例子。\n\n要理解`TArray<T>`变量是什么，首先你必须知道尖括号之间的`<T>`选项代表什么。`<T>`选项意味着存储在数组中的数据类型是一个变量。要不要`int`的数组？然后创建一个`TArray<int>`变量。`double`的一个`TArray`变量？创建一个`TArray<double>`变量。\n\n所以一般来说，无论`<T>`出现在哪里，都可以插入自己选择的 C++ 数据类型。\n\n容器是用于存储对象的不同结构。模板对于这些特别有用，因为它们可以用来存储许多不同类型的对象。您可能希望用 int 或 float、字符串或不同类型的游戏对象来存储数字。想象一下，如果你必须为你想要存储的每种类型的对象编写一个新的类。幸运的是，你不必。模板让一个类足够灵活，可以处理您想要存储在其中的任何对象。\n\n# 你的第一个模板\n\n创建模板是一个高级主题，您可以几年都不用创建自己的模板(尽管您会一直使用标准模板)。但是看到一个人的样子可能会有所帮助，这只是为了帮助你理解幕后发生的事情。\n\n假设您想创建一个数字模板，让您使用 int、float 或其他类型。你可以这样做:\n\n```cpp\ntemplate <class T>\nclass Number {\n    T value;\npublic:\n    Number(T val)\n    {\n        value = val;\n    }\n\n    T getSumWith(T val2);\n};\n\ntemplate <class T>\nT Number<T>::getSumWith(T val2)\n{\n    T retval;\n    retval = value + val2;\n    return retval;\n}\n```\n\n第一部分是类本身。如您所见，您想要在模板中的任何地方使用该类型，您创建了该类，并将使用`T`而不是指定特定的类型。您也可以使用模板来指定发送给函数的值。在这种情况下，最后一部分让您添加另一个数字并返回总和。\n\n您甚至可以通过重载+运算符使事情变得更简单，这样您就可以像添加任何标准类型一样添加这些数字。这是通过一种叫做运算符重载的方法实现的。\n\n# UE4 '塔雷〔t0〕\n\ntarray 是 UE4 的动态数组版本，使用模板构建。像我们讨论的其他动态阵列一样，您不必担心自己管理阵列大小。让我们继续，用一个例子来看看这个。\n\n# 一个使用 TArray <t>的例子</t>\n\n一个`TArray<int>`变量只是一个`int`的数组。一个`TArray<Player*>`变量将是一个`Player*`指针数组。数组是可动态调整大小的，并且可以在创建后在数组末尾添加元素。\n\n要创建一个`TArray<int>`变量，你所要做的就是使用正常的变量分配语法:\n\n```cpp\nTArray<int> array; \n```\n\n使用成员函数完成对`TArray`变量的更改。有几个成员函数可以在`TArray`变量上使用:\n\n您需要了解的第一个成员函数是如何向数组中添加值，如以下代码所示:\n\n```cpp\narray.Add( 1 ); \narray.Add( 10 ); \narray.Add( 5 ); \narray.Add( 20 ); \n```\n\n这四行代码将在内存中产生数组值，如下图所示:\n\n![](img/8f29a440-73b4-4596-9a09-342c3fad4a2e.png)\n\n当你呼叫`array.Add( number )`时，新号码会到达数组的末尾。既然我们把数字 **1** 、 **10** 、 **5** 、 **20** 加到了数组中，按照这个顺序，那就是它们进入数组的顺序。\n\n如果您想在数组的前面或中间插入一个数字，这也是可能的。你所要做的就是使用`array.Insert(value, index)`函数，如下一行代码所示:\n\n```cpp\narray.Insert( 9, 0 ); \n```\n\n该功能将把数字`9`推到阵列的位置`0`(在前面)。这意味着剩余的数组元素将向右偏移，如下图所示:\n\n![](img/53088c53-3664-4a96-9f42-6fa30b9713f2.png)\n\n我们可以使用下面一行代码将另一个元素插入数组的位置`2`:\n\n```cpp\narray.Insert( 30, 2 ); \n```\n\n该函数将重新排列数组，如下图所示:\n\n![](img/3b989010-02d1-4356-8a5e-e5bca807c402.png)\n\nIf you insert a number into a position in the array that is out of bounds (it doesn't exist), UE4 will crash. So, be careful not to do that. You can use `Add` to add a new item instead.\n\n# 重复一天\n\n您可以通过两种方式迭代(遍历)`TArray`变量的元素:使用基于整数的索引或使用迭代器。我将在这里向你展示两种方法。\n\n# 普通的循环加方括号符号\n\n使用整数来索引数组的元素有时被称为普通的`for`循环。可以使用`array[ index ]`访问数组的元素，其中`index`是元素在数组中的数字位置:\n\n```cpp\nfor( int index = 0; index < array.Num(); index++ ) \n{ \n  // print the array element to the screen using debug message \n  GEngine->AddOnScreenDebugMessage( -1, 30.f, FColor::Red,  \n   FString::FromInt( array[ index ] ) ); \n} \n```\n\n# 迭代程序\n\n您也可以使用迭代器逐个遍历数组的元素，如以下代码所示:\n\n```cpp\nfor (TArray<int>::TIterator it = array.CreateIterator(); it; ++ it)\n{\n    GEngine->AddOnScreenDebugMessage(-1, 30.f, FColor::Green, FString::FromInt(*it));\n}\n```\n\n迭代器是指向数组的指针。迭代器可以用来检查或改变数组中的值。下图显示了一个迭代器示例:\n\n![](img/47aabe5d-01fc-4220-9f22-726d1d5595db.png)\n\n迭代器是一个外部对象，可以查看和检查数组的值。做`++ it`移动迭代器检查下一个元素。\n\n迭代器必须适合它所遍历的集合。要遍历一个`TArray<int>`变量，需要一个`TArray<int>::TIterator`类型的迭代器。\n\n我们使用`*`来查看迭代器后面的值。在前面的代码中，我们使用`(*it)`从迭代器中获取整数值。这叫做取消引用。取消引用迭代器意味着查看它的值。\n\n在`for`循环的每次迭代结束时发生的`++ it`操作递增迭代器，使其继续指向列表中的下一个元素。\n\n把代码插入程序，现在就试一试。以下是我们使用`TArray`(都在`ATArraysGameMode::ATArraysGameMode()`构造函数中)创建的示例程序:\n\n```cpp\nATArraysGameMode::ATArraysGameMode(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer)\n{\n    if (GEngine)\n    {\n        TArray<int> array;\n        array.Add(1);\n        array.Add(10);\n        array.Add(5);\n        array.Add(20);\n        array.Insert(9, 0);// put a 9 in the front \n        array.Insert(30, 2);// put a 30 at index 2 \n        if (GEngine)\n        {\n            for (int index = 0; index < array.Num(); index++)\n            {\n                GEngine->AddOnScreenDebugMessage(index, 30.f, FColor::Red,\n                    FString::FromInt(array[index]));\n            }\n        }\n    }\n}\n```\n\n下面的屏幕截图显示了前面代码的输出:\n\n![](img/657541c8-f2fd-4aa0-ba33-285189680049.png)\n\n# 确定元素是否在数组中\n\n搜索我们的 UE4 容器很容易。通常使用`Find`成员函数来完成。使用我们之前创建的数组，我们可以通过键入以下代码行找到`10`值的索引:\n\n```cpp\nint index = array.Find( 10 ); // would be index 3 in image above \n```\n\n一个`TSet<int>`变量存储一组整数。一个`TSet<FString>`变量存储一组字符串。`TSet`和`TArray`的主要区别在于`TSet`不允许重复；`TSet`内部的所有元素保证是唯一的。一个`TArray`变量不介意相同元素的重复。\n\n要给`TSet`添加号码，只需拨打`Add`。这里有一个例子:\n\n```cpp\nTSet<int> set; \nset.Add( 1 ); \nset.Add( 2 ); \nset.Add( 3 ); \nset.Add( 1 );// duplicate! won't be added \nset.Add( 1 );// duplicate! won't be added \n```\n\n这就是`TSet`的样子:\n\n![](img/740b0648-f23d-4b33-887c-c11c86a96683.png)\n\n不允许在`TSet`中出现相同值的重复条目。注意`TSet`中的条目没有编号，就像它们在`TArray`中一样；您不能使用方括号来访问`TSet`数组中的条目。\n\n# 迭代一个 TSet\n\n为了查看`TSet`数组，必须使用迭代器。您不能使用方括号符号来访问`TSet`的元素:\n\n```cpp\nfor( TSet<int>::TIterator it = set.CreateIterator(); it; ++ it ) \n{ \n  GEngine->AddOnScreenDebugMessage( -1, 30.f, FColor::Red,  \n   FString::FromInt( *it ) ); \n} \n```\n\n# 交叉 TSet 阵列\n\n`TSet`数组有两个特殊功能，`TArray`变量没有。两个`TSet`数组的交集基本上是它们共有的元素。如果我们有两个`TSet`数组，如`X`和`Y`，我们将它们相交，结果将是第三个新的`TSet`数组，它只包含它们之间共有的元素。请看下面的例子:\n\n```cpp\nTSet<int> X; \nX.Add( 1 ); \nX.Add( 2 ); \nX.Add( 3 ); \nTSet<int> Y; \nY.Add( 2 ); \nY.Add( 4 ); \nY.Add( 8 ); \nTSet<int> common = X.Intersect(Y); // 2 \n```\n\n`X`和`Y`之间的共同元素就是`2`元素。\n\n# 正在联合 TSet 数组\n\n从数学上讲，两个集合的并集是指基本上将所有元素插入同一个集合。既然我们在这里谈论的是布景，就不会有任何重复。\n\n如果我们取前面例子中的`X`和`Y`集合并创建一个并集，我们将得到一个新集合，如下所示:\n\n```cpp\nTSet<int> uni = X.Union(Y); // 1, 2, 3, 4, 8 \n```\n\n# 在 TSet 数组中查找\n\n您可以使用集合上的`Find()`成员函数来确定元素是否在`TSet`内。`TSet`将返回一个指向`TSet`中与您的查询相匹配的条目的指针，如果该元素存在于`TSet`中，或者如果您所请求的元素不存在于`TSet`中，它将返回`NULL`。\n\n# tmap〔t0〕\n\n`TMap<T,S>`在内存中创建一个分类表。`TMap`表示左侧的键到右侧值的映射。您可以将`TMap`可视化为一个两列表格，左列为键，右列为值。\n\n# 玩家物品清单\n\n例如，假设我们想要创建一个 C++ 数据结构，以便为玩家的库存存储一个项目列表。在桌子的左手边(钥匙)，我们会有`FString`作为物品的名称。在右侧(数值)，我们有一个`int`表示该项目的数量，如下表所示:\n\n| 项目(键) | 数量(价值) |\n| `apples` | `4` |\n| `donuts` | `12` |\n| `swords` | `1` |\n| `shields` | `2` |\n\n要在代码中做到这一点，我们只需使用以下内容:\n\n```cpp\nTMap<FString, int> items; \nitems.Add( \"apples\", 4 ); \nitems.Add( \"donuts\", 12 ); \nitems.Add( \"swords\", 1 ); \nitems.Add( \"shields\", 2 ); \n```\n\n一旦创建了`TMap`，就可以使用方括号并通过在方括号之间传递一个键来访问`TMap`内的值。例如，在前面代码中的`items`地图中，`items[ \"apples\" ]`是`4`。\n\nUE4 will crash if you use square brackets to access a key that doesn't exist in the map yet, so be careful! The C++ STL does not crash if you do this.\n\n# 迭代一个 TMap\n\n为了迭代一个`TMap`，你也可以使用一个迭代器:\n\n```cpp\nfor( TMap<FString, int>::TIterator it = items.CreateIterator(); it; ++ it ) \n{ \n  GEngine->AddOnScreenDebugMessage( -1, 30.f, FColor::Red, \n  it->Key + FString(\": \") + FString::FromInt( it->Value ) ); \n} \n```\n\n`TMap`迭代器与`TArray`或`TSet`迭代器略有不同。一个`TMap`迭代器包含一个`Key`和一个`Value`。我们可以使用`it->Key`访问密钥，使用`it->Value`访问`TMap`中的值。\n\n这里有一个例子:\n\n![](img/1f66537a-711e-4291-aae6-4fa91483e851.png)\n\n# TLinkedList/t 双链接列表\n\n当您使用 TArray 时，每个项目都有一个按数字顺序排列的索引，数组数据通常以相同的方式存储，因此每个条目也紧挨着内存中它之前的条目。但是，如果您需要在中间的某个地方放置一个新项目(例如，如果数组中按字母顺序填充了字符串)，该怎么办呢？\n\n由于这些物品是一个挨着一个的，所以旁边的那一个必须挪过去腾出空间。但是要做到这一点，旁边的那一个也必须被移走。这将一直持续到数组的末尾，当它最终到达它可以使用的内存时，不需要移动其他东西。正如你可能想象的，这可能会变得非常慢，尤其是如果你经常这样做的话。\n\n这就是链表出现的地方。链表没有任何索引。链接列表具有包含项目的节点，并允许您访问列表中的第一个节点。该节点有一个指向列表中下一个节点的指针，可以通过调用`Next()`获得。然后，你可以在那个上面调用`Next()`来获得它后面的那个。它看起来像这样:\n\n![](img/d1ab445b-147b-4ff9-8f86-bbf2bdcc9859.png)\n\n正如你可能猜到的，如果你在列表的末尾寻找一个项目，这可能会变得很慢。但与此同时，您可能不会经常搜索列表，而是会在中间的某个地方添加新项目。在中间添加一个项目要快得多。假设您试图在**节点 1** 和**节点 2** 之间插入一个新节点，如下所示:\n\n![](img/edb6efd2-6025-4a81-b93c-aaf3f7fecc5a.png)\n\n这次没有必要在内存中移动东西来腾出空间。相反，要一个接一个地插入一个项目，请从**节点 1** ( **节点 2** )获取`Next()`指向的节点。设置新节点指向那个节点(**节点 2** )。然后，将节点 1 设置为指向新节点。现在应该是这样的:\n\n![](img/7286df8d-4f4e-498c-be18-19b9bb7fac08.png)\n\n你完蛋了！\n\n那么，如果你打算花更多的时间在清单的末尾寻找物品呢？这就是`TDoubleLinkedList`派上用场的地方。双向链表可以给你列表中的第一个节点，也可以给你列表中的最后一个节点。每个节点也有指向下一个节点和上一个节点的指针。您可以使用`GetNextLink()`和`GetPrevLink()`访问这些。所以，你可以选择前进或后退，甚至两者兼而有之，在中间相遇。\n\n现在，你可能会问自己，*“为什么我可以只用 TArray，不用担心它在幕后做什么，这有什么关系？”*首先，专业游戏程序员总要担心速度。计算机和游戏机的每一次进步都伴随着越来越多更好的图形，以及其他可以让事情再次减速的进步。所以，优化速度总是很重要的。\n\n此外，还有另一个实际原因:根据经验，我可以告诉你，如果你不使用链表，这个行业中有人会在求职面试中拒绝你。程序员都有自己喜欢的做事方式，所以你应该随时熟悉可能出现的任何事情。\n\n# 常用容器的 C++ STL 版本\n\n现在，我们将介绍几个容器的 C++ STL 版本。STL 是大多数 C++ 编译器附带的标准模板库。我想介绍这些 STL 版本的原因是，它们的行为与相同容器的 UE4 版本有些不同。在某些方面，他们的行为非常好，但是游戏程序员经常抱怨 STL 有性能问题。特别是我想涵盖 STL 的`set`和`map`容器，但我也会涵盖常用的`vector`。\n\nIf you like STL's interface but want better performance, there is a well-known reimplementation of the STL library by Electronic Arts called EASTL, which you can use. It provides the same functionality as STL but is implemented with better performance (basically by doing things such as eliminating bounds checking). It is available on GitHub at [https://github.com/paulhodge/EASTL](https://github.com/paulhodge/EASTL).\n\n# C++ STL 集\n\nC++ 集合是一堆唯一且经过排序的项目。STL `set`的好特性是它保持集合元素有序。对一堆值进行分类的一种快速而肮脏的方法实际上是把它们塞进同一个`set`中。`set`会帮你整理的。\n\n我们可以返回一个简单的 C++ 控制台应用来使用集合。要使用 C++ STL 集，需要包含`<set>`，如下图:\n\n```cpp\n#include <iostream> \n#include <set> \nusing namespace std; \n\nint main() \n{ \n  set<int> intSet; \n  intSet.insert( 7 ); \n  intSet.insert( 7 ); \n  intSet.insert( 8 ); \n  intSet.insert( 1 ); \n\n  for( set<int>::iterator it = intSet.begin(); it != intSet.end();  \n   ++ it ) \n  { \n    cout << *it << endl; \n  } \n} \n```\n\n以下是前面代码的输出:\n\n```cpp\n1 \n7 \n8 \n```\n\n重复的`7`被过滤掉，元素在`set`内保持递增的顺序。我们迭代 STL 容器元素的方式类似于 UE4 的`TSet`数组。`intSet.begin()`函数返回一个指向`intSet`头部的迭代器。\n\n停止迭代的条件是当它变成`intSet.end()`时。`intSet.end()`实际上是经过`set`末端的一个位置，如下图所示:\n\n![](img/2143e060-1cc8-4ded-a163-8a2aca5fb9be.png)\n\n# 在<set>中寻找元素</set>\n\n要在 STL `set`中找到一个元素，我们可以使用`find()`成员函数。如果我们要找的项目出现在`set`中，我们会得到一个迭代器，指向我们要搜索的元素。如果我们要找的物品不在`set`中，我们将返回`set.end()`，如下图所示:\n\n```cpp\nset<int>::iterator it = intSet.find( 7 ); \nif( it != intSet.end() ) \n{ \n  //  7  was inside intSet, and *it has its value \n  cout << \"Found \" << *it << endl; \n} \n```\n\n# 锻炼\n\n要求用户提供一组三个唯一的名称。逐个输入每个名字，然后按排序顺序打印出来。如果用户重复一个名字，请他们再叫一个，直到你数到三。\n\n# 解决办法\n\n可以使用以下代码找到前面练习的解决方案:\n\n```cpp\n#include <iostream> \n#include <string> \n#include <set> \nusing namespace std; \nint main() \n{ \n  set<string> names; \n  // so long as we don't have 3 names yet, keep looping, \n  while( names.size() < 3 ) \n  { \n    cout << names.size() << \" names so far. Enter a name\" << endl; \n    string name; \n    cin >> name; \n    names.insert( name ); // won't insert if already there, \n  } \n  // now print the names. the set will have kept order \n  for( set<string>::iterator it = names.begin(); it !=  \n   names.end(); ++ it ) \n  { \n    cout << *it << endl; \n  } \n} \n```\n\n# C++ STL 映射\n\nC++ STL `map`对象很像 UE4 的`TMap`对象。`TMap`没有做的一件事是在地图中保持有序。排序引入了额外的成本，但是如果你想要你的地图被排序，选择 STL 版本可能是一个不错的选择。\n\n为了使用 C++ STL `map`对象，我们包括`<map>`。在下面的示例程序中，我们用一些键值对填充一个项目映射:\n\n```cpp\n#include <iostream> \n#include <string> \n#include <map> \nusing namespace std; \nint main() \n{ \n  map<string, int> items; \n  items.insert( make_pair( \"apple\", 12 ) ); \n  items.insert( make_pair( \"orange\", 1 ) ); \n  items.insert( make_pair( \"banana\", 3 ) ); \n  // can also use square brackets to insert into an STL map \n  items[ \"kiwis\" ] = 44; \n\n  for( map<string, int>::iterator it = items.begin(); it !=  \n   items.end(); ++ it ) \n  { \n    cout << \"items[ \" << it->first << \" ] = \" << it->second <<  \n     endl; \n  } \n} \n```\n\n这是前面程序的输出:\n\n```cpp\nitems[ apple ] = 12 \nitems[ banana ] = 3 \nitems[ kiwis ] = 44 \nitems[ orange ] = 1 \n```\n\n请注意迭代器对 STL 映射的语法与`TMap`略有不同；我们使用`it->first`访问密钥，使用`it->second`访问值。\n\n注意 C++ STL 如何比`TMap`还提供一点语法糖；可以用方括号插入到 C++ STL `map`中。你不能用方括号插入`TMap`。\n\n# 在<map>中寻找元素</map>\n\n您可以使用 STL 地图的`find`成员功能在地图中搜索<`key`、`value`、>对。你通常通过`key`搜索，它会给你这个`key`的价值。\n\n# 锻炼\n\n要求用户在一个空的`map`中输入五个项目及其数量。按排序顺序打印结果(即，按字母顺序或从低到高，如果是数字)。\n\n# 解决办法\n\n前面练习的解决方案使用了以下代码:\n\n```cpp\n#include <iostream> \n#include <string> \n#include <map> \nusing namespace std; \nint main() \n{ \n  map<string, int> items; \n  cout << \"Enter 5 items, and their quantities\" << endl; \n  while( items.size() < 5 ) \n  { \n    cout << \"Enter item\" << endl; \n    string item; \n    cin >> item; \n    cout << \"Enter quantity\" << endl; \n    int qty; \n    cin >> qty; \n    items[ item ] = qty; // save in map, square brackets \n    // notation \n  } \n\n  for( map<string, int>::iterator it = items.begin(); it !=  \n   items.end(); ++ it ) \n  { \n    cout << \"items[ \" << it->first << \" ] = \" << it->second <<  \n     endl; \n  } \n} \n```\n\n在这个解决方案代码中，我们首先创建`map<string, int> items`来存储我们将要接收的所有项目。向用户询问项目和数量；然后，我们使用方括号符号将`item`保存在`items`地图中。\n\n# C++ STL 向量\n\n`Vector`是`TArray`的 STL 等价物。它基本上是一个管理幕后一切的数组，就像`TArray`一样。在 UE4 工作的时候可能不需要用到，但是知道万一别人在项目中用到就好了。\n\n# 摘要\n\nUE4 的容器和 C++ STL 系列容器都非常适合存储游戏数据。通常，通过选择正确的数据容器类型，可以大大简化编程问题。\n\n在下一章中，我们将通过跟踪玩家携带的信息并将这些信息存储在一个`TMap`对象中，开始对我们游戏的开始进行编程。"
  },
  {
    "path": "docs/learn-cpp-build-game-ue4/10.md",
    "content": "# 十、库存系统和提取项目\n\n我们希望我们的玩家能够从游戏世界中挑选物品。在这一章中，我们将为我们的玩家编码和设计一个背包来存放物品。当用户按下 *I* 键时，我们将显示玩家在背包中携带的物品。\n\n作为数据表示，我们可以使用上一章中介绍的`TMap<FString, int>`项来存储我们的项。当玩家拿起一个物品时，我们把它添加到地图上。如果该物品已经在地图中，我们只需根据新获得的物品数量增加其价值。\n\n我们将在本章中讨论以下主题:\n\n*   申报背包\n*   PickupItem 基类\n*   绘制玩家清单\n\n# 申报背包\n\n我们可以将玩家的背包表示为一个简单的`TMap<FString, int>`物品。为了让我们的玩家从世界上收集物品，打开`Avatar.h`文件并添加以下`TMap`声明:\n\n```cpp\nclass APickupItem; //  forward declare the APickupItem class, \n                   // since it will be \"mentioned\" in a member  \n                    function decl below \nUCLASS() \nclass GOLDENEGG_API AAvatar : public ACharacter \n{ \n  GENERATED_BODY() \npublic: \n  // A map for the player's backpack \n  TMap<FString, int> Backpack; \n\n  // The icons for the items in the backpack, lookup by string \n  TMap<FString, UTexture2D*> Icons; \n\n  // A flag alerting us the UI is showing \n  bool inventoryShowing; \n  // member function for letting the avatar have an item \n  void Pickup( APickupItem *item ); \n  // ... rest of Avatar.h same as before \n}; \n```\n\n# 远期申报\n\n在`AAvatar`课之前，注意我们有一个`class APickupItem`正向申报。当提到一个类(例如`APickupItem::Pickup( APickupItem *item );`函数原型)时，代码文件中需要正向声明，但是文件中没有实际使用文件中该类型对象的代码。由于`Avatar.h`头文件不包含使用类型为`APickupItem`的对象的可执行代码，因此我们需要一个正向声明。虽然包含. h 文件似乎更容易。有时最好避免这种情况，否则你可能会得到循环依赖(两个类，每个都试图包含另一个会导致问题)。\n\n没有正向声明会导致编译器错误，因为编译器在`class AAvatar`中编译代码之前不会听说过`class APickupItem`。编译器错误将出现在`APickupItem::Pickup( APickupItem *item );`函数原型声明的声明中。\n\n我们在`AAvatar`类中声明了两个`TMap`对象。这就是对象的外观，如下表所示:\n\n| `FString`(名称) | `int`(数量) | `UTexture2D*` (im) |\n| `GoldenEgg` | `2` | ![](img/c3918bbc-1d30-4f56-aea2-4df86976f902.png) |\n| `MetalDonut` | `1` | ![](img/9b920363-321a-41c9-a215-8f472aad5bc9.png) |\n| `Cow` | `2` | ![](img/771da95a-5e07-4846-94f1-346279693904.png) |\n\n在`TMap`背包中，我们存储了玩家所持物品的`FString`变量。在`Icons`地图中，我们存储了玩家所持物品图像的单一参考。\n\n在渲染时，我们可以使用两个地图一起工作来查找玩家拥有的物品数量(在他的`Backpack`地图中)和该物品的纹理资产参考(在`Icons`地图中)。以下截图显示了抬头显示器的渲染效果:\n\n![](img/ead175d8-8699-48a9-88de-6c8fa1c2086b.png)\n\n请注意，我们也可以使用带有`FString`变量和`UTexture2D*`的`struct`数组，而不是使用两个地图。\n\n例如，我们可以用一个`struct`变量来保存`TArray<Item> Backpack;`，如下面的代码所示:\n\n```cpp\nstruct Item   \n{   \n  FString name;   \n  int qty;   \n  UTexture2D*   tex;   \n};   \n```\n\n然后，当我们拾取项目时，它们将被添加到线性阵列中。然而，计算我们背包中的每件物品的数量需要不断地重新评估，每次我们想要查看数量时都要遍历物品的数组。例如，要想知道你有多少发刷，你需要遍历整个数组。这不如使用地图有效。\n\n# 导入资产\n\n您可能已经注意到了前面截图中的 Cow 资产，它不是 UE4 在新项目中提供的标准资产集的一部分。为了使用 cow 资产，您需要从内容示例项目中导入 Cow。UE4 使用了一个标准的导入程序。\n\n在下面的截图中，我已经概述了导入 Cow 资产的过程。其他资产将使用相同的方法从 UE4 中的其他项目导入。\n\n执行以下步骤导入奶牛资产:\n\n1.  下载并打开 UE4 的内容示例项目。在史诗游戏启动器的“学习”下找到它，如下所示:\n\n![](img/d6b3e574-9bbc-455d-a1aa-44ad05f4822e.png)\n\n2.  下载内容示例后，打开并点击\n    创建项目:\n\n![](img/d191f3be-af11-46e1-b608-8255fd89edc7.png)\n\n3.  接下来，命名您将放置`ContentExamples`的文件夹，并点击创建。\n4.  从库中打开你的`ContentExamples`项目。浏览项目中的可用资源，直到找到您喜欢的资源。搜索`SM_`会有所帮助，因为按照惯例，所有静态网格通常以`SM_`开头:\n\n![](img/c401500f-fb64-4339-ad53-cb369c27eed2.png)\n\nAssets available in the project\n\n5.  找到您喜欢的资产后，右键单击该资产，然后单击资产操作>迁移，将其导入到项目中...：\n\n![](img/aaf26cd5-cdb2-4f0c-9784-bd815f8f43c4.png)\n\n6.  在资产报告对话框中单击确定:\n\n![](img/335e3f20-4f45-4d42-b3b0-2f87a74e648b.png)\n\n7.  从项目中选择要添加 SM_Toy_Cow 文件的内容文件夹。我们将其添加到`/Documents/Unreal Projects/GoldenEgg/Content`，如下图截图所示:\n\n![](img/be9114fd-5243-4752-b5a0-eaa04fed5e27.png)\n\n8.  如果导入成功完成，您将看到以下消息:\n\n![](img/fd127d7a-4618-45e8-9a57-66f9bca8328e.png)\n\n9.  导入资产后，您将看到它显示在项目内部的资产浏览器中:\n\n![](img/2eea6f29-6e2e-4ef0-a5a5-7fc312a12e4e.png)\n\n然后，您可以正常使用项目中的资产。\n\n# 将动作映射附加到键\n\n我们需要附上一把钥匙来激活玩家清单的显示。在 UE4 编辑器中，按照以下步骤操作:\n\n1.  添加一个名为`Inventory`的动作映射+\n2.  分配到键盘键 *I* ，如图:\n\n![](img/6b70bffd-3536-4c2b-b521-1103b6ba0183.png)\n\n3.  接下来，在`Avatar.h`文件中，添加一个需要显示玩家库存时要运行的会员功能:\n\n```cpp\nvoid ToggleInventory(); \n```\n\n4.  在`Avatar.cpp`文件中，实现`ToggleInventory()`功能，如下代码所示:\n\n```cpp\nvoid AAvatar::ToggleInventory() \n{ \n  if( GEngine ) \n  { \n    GEngine->AddOnScreenDebugMessage( -1, 5.f, FColor::Red,  \n     \"Showing inventory...\" ); \n  } \n} \n```\n\n5.  然后，将`\"Inventory\"`动作连接到`SetupPlayerInputComponent()`中的`AAvatar::ToggleInventory()`:\n\n```cpp\nvoid AAvatar::SetupPlayerInputComponent(class UInputComponent*  \n   InputComponent) \n{ \n Super::SetupPlayerInputComponent(PlayerInputComponent);\n\n    check(PlayerInputComponent);\n    PlayerInputComponent->BindAction(\"Inventory\", IE_Pressed, this,\n        &AAvatar::ToggleInventory);\n  // rest of SetupPlayerInputComponent same as before \n} \n```\n\n# PickupItem 基类\n\n我们需要在代码中定义提货项目的外观。每个拾取项目都将从一个公共基类派生。现在让我们为一个`PickupItem`类构建基类。\n\n`PickupItem`基类应该继承自`AActor`类。类似于我们如何从 NPC 基类创建多个 NPC 蓝图，我们可以从单个`PickupItem`基类创建多个`PickupItem`蓝图，如下图所示:\n\n![](img/01cb7674-3e98-4a88-b927-5a8a3b556723.png)\n\nThe text in this screenshot is not important. this image gives you an idea of how to create multiple `PickupItem` blueprints from a single `PickupItem` base class\n\n创建`PickupItem`类后，在 Visual Studio 中打开它的代码。\n\n`APickupItem`类将需要相当多的成员，如下所示:\n\n*   一个`FString`变量，用于被拾取项目的名称\n*   一个`int32`变量，表示被拾取物品的数量\n*   一个`USphereComponent`变量，用于要拾取的物品将与之碰撞的球体\n*   一个`UStaticMeshComponent`变量来保存实际的`Mesh`\n*   代表项目的图标的`UTexture2D`变量\n*   平视显示器的指针(我们稍后会初始化)\n\n`PickupItem.h`中的代码是这样的:\n\n```cpp\n// Fill out your copyright notice in the Description page of Project Settings.\n\n#pragma once\n\n#include \"CoreMinimal.h\"\n#include \"GameFramework/Actor.h\"\n#include \"Components/SphereComponent.h\"\n#include \"Components/StaticMeshComponent.h\"\n#include \"PickupItem.generated.h\"\n\nUCLASS()\nclass GOLDENEGG_API APickupItem : public AActor\n{\n    GENERATED_BODY()\n\npublic:    \n    // Sets default values for this actor's properties\n    APickupItem(const FObjectInitializer& ObjectInitializer);\n\n    // The name of the item you are getting \n    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Item)\n        FString Name;\n\n    // How much you are getting \n    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Item)\n        int32 Quantity;\n\n    // the sphere you collide with to pick item up \n    UPROPERTY(VisibleDefaultsOnly, BlueprintReadOnly, Category = Item)\n        USphereComponent* ProxSphere;\n\n    // The mesh of the item \n    UPROPERTY(VisibleDefaultsOnly, BlueprintReadOnly, Category = Item)\n        UStaticMeshComponent* Mesh;\n    // The icon that represents the object in UI/canvas \n    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Item) \n        UTexture2D* Icon; \n    // When something comes inside ProxSphere, this function runs \n    UFUNCTION(BlueprintNativeEvent, Category = Collision) \n        void Prox(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp,\n            int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);\n        virtual int Prox_Implementation(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp,\n        int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);\n\nprotected:\n    // Called when the game starts or when spawned\n    virtual void BeginPlay() override;\n\npublic:    \n    // Called every frame\n    virtual void Tick(float DeltaTime) override;\n};\n```\n\n所有这些`UPROPERTY()`声明的目的是使`APickupItem`完全可由蓝图配置。例如，“拾取”类别中的项目将在蓝图编辑器中显示如下:\n\n![](img/d5a2836e-5c31-47f8-8303-68ad756e0f6d.png)\n\n在`PickupItem.cpp`文件中，我们完成了`APickupItem`类的构造函数，如下代码所示:\n\n```cpp\nAPickupItem::APickupItem(const FObjectInitializer& ObjectInitializer)\n    : Super(ObjectInitializer)\n{\n    Name = \"UNKNOWN ITEM\";\n    Quantity = 0;\n\n    // initialize the unreal objects \n    ProxSphere = ObjectInitializer.CreateDefaultSubobject<USphereComponent>(this,\n        TEXT(\"ProxSphere\"));  \n    Mesh = ObjectInitializer.CreateDefaultSubobject<UStaticMeshComponent>(this,\n            TEXT(\"Mesh\"));\n\n    // make the root object the Mesh \n    RootComponent = Mesh;\n    Mesh->SetSimulatePhysics(true);\n\n    // Code to make APickupItem::Prox() run when this \n    // object's proximity sphere overlaps another actor. \n    ProxSphere->OnComponentBeginOverlap.AddDynamic(this, &APickupItem::Prox);\n    ProxSphere->AttachToComponent(Mesh, FAttachmentTransformRules::KeepWorldTransform); // very important!              \n}\n```\n\n在前两行中，我们将`Name`和`Quantity`初始化为游戏设计者认为未初始化的值。我们使用了大写的块，这样设计者可以清楚地看到变量以前从未被初始化过。\n\n然后我们使用`ObjectInitializer.CreateDefaultSubobject`初始化`ProxSphere`和`Mesh`组件。新初始化的对象可能会初始化它们的一些默认值，但是`Mesh`将从空开始。稍后，您将不得不在蓝图中加载实际的网格。\n\n对于网格，我们将其设置为模拟真实的物理，以便拾取的项目在掉落或移动时会反弹和滚动。特别注意线路`ProxSphere->AttachToComponent(Mesh, FAttachmentTransformRules::KeepWorldTransform);`。该行告诉您确保拾取项目的`ProxSphere`组件连接到`Mesh`根组件。这意味着当网格在层中移动时，`ProxSphere`跟随。如果你忘记了这一步(或者你反过来做了)，那么`ProxSphere`将不会跟随网格反弹。\n\n# 根组件\n\n在前面的代码中，我们将`APickupItem`的`RootComponent`分配给了`Mesh`对象。`RootComponent`成员是`AActor`基类的一部分，所以每个`AActor`及其派生都有一个根组件。根组件基本上意味着是对象的核心，并且还定义了如何与对象碰撞。`RootComponent`对象在`Actor.h`文件中定义，如下代码所示:\n\n```cpp\n/** Collision primitive that defines the transform (location, rotation, scale) of this Actor. */\n    UPROPERTY(BlueprintGetter=K2_GetRootComponent, Category=\"Utilities|Transformation\")\n    USceneComponent* RootComponent;\n```\n\n所以，UE4 的创造者们希望`RootComponent`永远是碰撞原语的参考。有时碰撞图元可以是胶囊形的，有时也可以是球形的，甚至是盒形的，或者是任意形状的，就像我们的例子中的网格一样。然而，一个角色很少会有盒状的根部件，因为盒子的角会被墙壁卡住。圆形通常是首选。`RootComponent`属性显示在蓝图中，您可以在其中看到并操作它:\n\n![](img/3283ccec-e589-4b8e-a8b9-140c368d86d8.png)\n\nYou can edit the ProxSphere root component from its blueprints once you create a blueprint based on the PickupItem class\n\n最后，`Prox_Implementation`函数实现如下:\n\n```cpp\nint APickupItem::Prox_Implementation(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp,\n    int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)\n{\n    // if the overlapped actor is NOT the player, \n    // you simply should return \n    if (Cast<AAvatar>(OtherActor) == nullptr)\n    {\n        return -1;\n    }\n\n    // Get a reference to the player avatar, to give him \n    // the item \n    AAvatar *avatar = Cast<AAvatar>(UGameplayStatics::GetPlayerPawn(GetWorld(), 0));\n\n    // Let the player pick up item \n    // Notice use of keyword this! \n    // That is how _this_ Pickup can refer to itself. \n    avatar->Pickup(this);\n\n    // Get a reference to the controller \n    APlayerController* PController = GetWorld()->GetFirstPlayerController();\n\n    // Get a reference to the HUD from the controller \n    AMyHUD* hud = Cast<AMyHUD>(PController->GetHUD());\n    hud->addMessage(Message(Icon, FString(\"Picked up \") + FString::FromInt(Quantity) + FString(\" \") + Name, 5.f, FColor::White)\n);\n\n    Destroy();\n\n    return 0;\n}\n```\n\n此外，请确保在文件顶部添加以下内容:\n\n```cpp\n#include \"Avatar.h\"\n#include \"MyHUD.h\"\n#include \"Kismet/GameplayStatics.h\"\n```\n\n这里有几个非常重要的提示:首先，我们必须访问几个*全局*来获取我们需要的对象。我们将通过这些操纵抬头显示器的功能访问三个主要对象:\n\n*   控制器(`APlayerController`)\n*   抬头显示器(`AMyHUD`)\n*   玩家本人(`AAvatar`)\n\n在游戏实例中，这三种类型的对象中只有一种。UE4 让找到它们变得很容易。\n\n同样，为了编译这个，你还需要在`MyHud.h`的`Message`结构中添加另一个构造函数。你需要一个这样的图像:\n\n```cpp\nMessage(UTexture2D* img, FString iMessage, float iTime, FColor iColor)\n    {\n        tex = img;\n        message = iMessage;\n        time = iTime;\n        color = iColor;\n    }\n```\n\n要编译，还需要向结构中添加另一个变量`UTexture2D* tex;`。你还需要在头像中实现拾取功能。\n\n# 获得头像\n\n只需调用以下代码，就可以随时从代码中的任何位置找到`player`类对象:\n\n```cpp\nAAvatar *avatar = Cast<AAvatar>( \n  UGameplayStatics::GetPlayerPawn( GetWorld(), 0 ) ); \n```\n\n然后我们通过调用前面定义的`AAvatar::Pickup()`函数将物品传递给玩家。\n\n因为`PlayerPawn`对象实际上是一个`AAvatar`实例，所以我们使用`Cast<AAvatar>`命令将结果投射到`AAvatar`类。`UGameplayStatics`函数系列可以在代码中的任何地方访问，因为它们是全局函数。\n\n# 获取播放器控制器\n\n也可以从全局函数中检索播放器控制器:\n\n```cpp\nAPlayerController* PController = \n  GetWorld()->GetFirstPlayerController(); \n```\n\n`GetWorld()`函数实际上是在`UObject`基类中定义的。由于所有 UE4 对象都来自`UObject`，游戏中的任何对象实际上都可以访问`world`对象。\n\n# 获取抬头显示器\n\n虽然这个组织一开始看起来很奇怪，但 HUD 实际上是附着在玩家的控制器上的。您可以按如下方式检索抬头显示器:\n\n```cpp\nAMyHUD* hud = Cast<AMyHUD>( PController->GetHUD() ); \n```\n\n我们投射平视显示器对象，因为我们之前在蓝图中将平视显示器设置为`AMyHUD`实例。由于我们将经常使用平视显示器，我们实际上可以在我们的`APickupItem`类中存储一个指向平视显示器的永久指针。这一点我们以后再讨论。\n\n接下来，我们实现`AAvatar::Pickup`，它将类型为`APickupItem`的对象添加到头像的背包中:\n\n```cpp\nvoid AAvatar::Pickup(APickupItem *item)\n{\n    if (Backpack.Find(item->Name))\n    {\n        // the item was already in the pack.. increase qty of it \n        Backpack[item->Name] += item->Quantity;\n    }\n    else\n    {\n        // the item wasn't in the pack before, add it in now \n        Backpack.Add(item->Name, item->Quantity);\n        // record ref to the tex the first time it is picked up \n        Icons.Add(item->Name, item->Icon);\n    }\n}\n```\n\n另外，确保在文件顶部添加`#include \"PickupItem.h\"`。\n\n在前面的代码中，我们检查玩家刚刚获得的拾取物品是否已经在他的包中。如果是，我们增加它的数量。如果它不在他的包中，我们将其添加到他的包和`Icons`映射中。\n\n要将提货物品添加到包装中，请使用以下代码行:\n\n```cpp\navatar->Pickup( this ); \n```\n\n`APickupItem::Prox_Implementation`是这个成员函数被调用的方式。\n\n现在，当玩家按下 *I* 时，我们需要在 HUD 中显示我们背包的内容。\n\n# 绘制玩家清单\n\n游戏中的库存屏幕，如*暗黑破坏神*有一个弹出窗口，你过去拿过的物品的图标排列在一个网格中。我们可以在 UE4 中实现这种类型的行为。\n\n在 UE4 中有许多绘制用户界面的方法。最基本的方法就是简单使用`HUD::DrawTexture()`调用。另一种方法是使用 Slate。还有一种方法就是使用最新的 UE4 UI 功能:**虚幻运动图形** ( **UMG** )设计师。\n\nSlate 使用声明性语法来布局 C++ 中的用户界面元素。Slate 最适合菜单之类的。自 UE 4.5 以来，UMG 一直在使用大量基于蓝图的工作流程。由于我们这里的重点是使用 C++ 代码的练习，我们将坚持使用`HUD::DrawTexture()`实现，但是我们将在后面的章节中讨论 UMG。这意味着我们必须管理代码中处理库存的所有数据。\n\n# 使用 HUD::DrawTexture()\n\n`HUD::DrawTexture()`是此时我们将用于将库存绘制到屏幕上的内容。我们将分两步实现这一目标:\n\n1.  当用户按下 *I* 键时，我们将库存的内容推送到平视显示器。\n2.  然后，我们以类似网格的方式将图标渲染到平视显示器中。\n\n    为了保存关于如何渲染小部件的所有信息，我们声明了一个简单的结构来保存关于它使用什么图标、它的当前位置和当前大小的信息。\n\n    这就是`Icon`和`Widget`结构的样子:\n\n```cpp\nstruct Icon \n{ \n  FString name; \n  UTexture2D* tex; \n  Icon(){ name = \"UNKNOWN ICON\"; tex = 0; } \n  Icon( FString& iName, UTexture2D* iTex ) \n  { \n    name = iName; \n    tex = iTex; \n  } \n}; \n\nstruct Widget \n{ \n  Icon icon; \n  FVector2D pos, size; \n  Widget(Icon iicon) \n  { \n    icon = iicon; \n  } \n  float left(){ return pos.X; } \n  float right(){ return pos.X + size.X; } \n  float top(){ return pos.Y; } \n  float bottom(){ return pos.Y + size.Y; } \n}; \n```\n\n您可以将这些结构声明添加到`MyHUD.h`的顶部，或者您可以将它们添加到一个单独的文件中，并在使用这些结构的任何地方包含该文件。\n\n注意`Widget`结构上的四个成员函数，到达小部件的`left()`、`right()`、`top()`和`bottom()`函数。我们稍后将使用这些来确定点击点是否在框内。\n\n3.  接下来，我们在`AMyHUD`类中声明将在屏幕上呈现小部件的函数。首先，在`MyHud.h`中，添加一个数组来保存小部件，添加一个向量来保存屏幕尺寸:\n\n```cpp\n    // New! An array of widgets for display \n    TArray<Widget> widgets;\n    //Hold screen dimensions\n    FVector2D dims;\n```\n\n4.  另外，增加一行`void DrawWidgets();`。然后，将其添加到`MyHud.cpp`:\n\n```cpp\nvoid AMyHUD::DrawWidgets()\n{\n    for (int c = 0; c < widgets.Num(); c++)\n    {\n        DrawTexture(widgets[c].icon.tex, widgets[c].pos.X,\n            widgets[c].pos.Y, widgets[c].size.X, widgets[c].size.Y, 0, 0,\n            1, 1);    DrawText(widgets[c].icon.name, FLinearColor::Yellow,\n                widgets[c].pos.X, widgets[c].pos.Y, hudFont, .6f, false);\n    }\n}\n```\n\n5.  对`DrawWidgets()`函数的调用应该被添加到`DrawHUD()`函数中，您可能想要将当前的消息处理代码移动到一个单独的`DrawMessages`函数中，这样您就可以得到这个(或者将原始代码留在那里):\n\n```cpp\nvoid AMyHUD::DrawHUD()\n{\n    Super::DrawHUD();\n    // dims only exist here in stock variable Canvas \n    // Update them so use in addWidget() \n    const FVector2D ViewportSize = FVector2D(GEngine->GameViewport->Viewport->GetSizeXY());\n    dims.X = ViewportSize.X;\n    dims.Y = ViewportSize.Y;\n    DrawMessages();\n    DrawWidgets();\n}\n```\n\n6.  接下来，我们将填充`ToggleInventory()`功能。这是用户按下 *I* 时运行的功能:\n\n```cpp\nvoid AAvatar::ToggleInventory()\n{\n    // Get the controller & hud \n    APlayerController* PController = GetWorld()->GetFirstPlayerController();\n    AMyHUD* hud = Cast<AMyHUD>(PController->GetHUD());\n\n    // If inventory is displayed, undisplay it. \n    if (inventoryShowing)\n    {\n        hud->clearWidgets();\n        inventoryShowing = false;\n        PController->bShowMouseCursor = false;\n        return;\n    }\n\n    // Otherwise, display the player's inventory \n    inventoryShowing = true;\n    PController->bShowMouseCursor = true;\n    for (TMap<FString, int>::TIterator it =\n        Backpack.CreateIterator(); it; ++ it)\n    {\n        // Combine string name of the item, with qty eg Cow x 5 \n        FString fs = it->Key + FString::Printf(TEXT(\" x %d\"), it->Value);\n        UTexture2D* tex;\n        if (Icons.Find(it->Key))\n        {\n            tex = Icons[it->Key];\n            hud->addWidget(Widget(Icon(fs, tex)));\n        }    \n    }\n}\n```\n\n7.  对于前面要编译的代码，我们需要给`AMyHUD`增加两个函数:\n\n```cpp\nvoid AMyHUD::addWidget( Widget widget ) \n{ \n  // find the pos of the widget based on the grid. \n  // draw the icons.. \n  FVector2D start( 200, 200 ), pad( 12, 12 ); \n  widget.size = FVector2D( 100, 100 ); \n  widget.pos = start; \n  // compute the position here \n  for( int c = 0; c < widgets.Num(); c++ ) \n  { \n    // Move the position to the right a bit. \n    widget.pos.X += widget.size.X + pad.X; \n    // If there is no more room to the right then \n    // jump to the next line \n    if( widget.pos.X + widget.size.X > dims.X ) \n    { \n      widget.pos.X = start.X; \n      widget.pos.Y += widget.size.Y + pad.Y; \n    } \n  } \n  widgets.Add( widget ); \n} \n\nvoid AMyHUD::clearWidgets()\n{\n    widgets.Empty();\n}\n```\n\n确保将以下内容也添加到`.h`文件中:\n\n```cpp\n    void clearWidgets();\n    void addWidget(Widget widget);\n```\n\n8.  我们一直使用`inventoryShowing`中的`Boolean`变量来告诉我们库存当前是否显示。当显示清单时，我们还会显示鼠标，以便用户知道他在点击什么。此外，当清单被显示时，玩家的自由运动被禁止。禁用玩家自由移动的最简单方法是在实际移动之前简单地从移动功能返回。以下代码是一个示例:\n\n```cpp\nvoid AAvatar::Yaw( float amount ) \n{ \n  if( inventoryShowing ) \n  { \n    return; // when my inventory is showing, \n    // player can't move \n  } \n  AddControllerYawInput(200.f*amount * GetWorld()- \n   >GetDeltaSeconds()); \n} \n```\n\n# 锻炼\n\n将`if( inventoryShowing ) { return; }`添加到每个移动功能中，这样当库存显示时，它将阻止所有移动。\n\n# 检测库存项目点击\n\n我们可以通过做一个简单的测试来检测是否有人在点击我们的库存物品，看看该点是否在物体的`rect`(矩形)内。该测试通过对照包含您想要测试的区域的`rect`的内容检查点击点来完成。\n\n为了对照`rect`进行检查，在`struct Widget`中增加以下成员功能:\n\n```cpp\nstruct Widget \n{ \n  // .. rest of struct same as before .. \n  bool hit( FVector2D p ) \n  { \n    // +---+ top (0) \n    // |   | \n    // +---+ bottom (2) (bottom > top) \n    // L   R \n    return p.X > left() && p.X < right() && p.Y > top() && p.Y <  \n     bottom(); \n  } \n}; \n```\n\n对`rect`的测试如下:\n\n![](img/d31e3ea5-d9e9-4708-b3a0-6e15ee9f845c.png)\n\n所以，如果`p.X`全是:\n\n*   `left() (p.X > left())`右侧\n*   `right() (p.X < right())`左侧\n*   下方`top() (p.Y > top())`\n*   以上`bottom() (p.Y < bottom())`\n\n请记住，在 UE4 中(以及一般的 UI 渲染中)， *y* 轴是倒置的。换句话说，y 在 UE4 中下降。这意味着`top()`小于`bottom()`，因为原点(`(0, 0)`点)在屏幕的左上角。\n\n# 拖动元素\n\n我们可以轻松拖动元素:\n\n1.  启用拖动的第一步是响应鼠标左键单击。首先，我们将编写单击鼠标左键时要执行的函数。在`Avatar.h`文件中，将以下原型添加到类声明中:\n\n```cpp\nvoid MouseClicked();\n```\n\n2.  在`Avatar.cpp`文件中，我们可以添加一个在鼠标点击时执行的功能，并将点击请求传递给 HUD，如下所示:\n\n```cpp\nvoid AAvatar::MouseClicked() \n{ \n  APlayerController* PController = GetWorld()- \n   >GetFirstPlayerController(); \n  AMyHUD* hud = Cast<AMyHUD>( PController->GetHUD() ); \n  hud->MouseClicked(); \n} \n```\n\n3.  然后，在`AAvatar::SetupPlayerInputComponent`中，我们必须附上我们的应答者:\n\n```cpp\nPlayerInputComponent->BindAction( \"MouseClickedLMB\", IE_Pressed, this, &AAvatar::MouseClicked );\n```\n\n下面的截图显示了如何设置绑定:\n\n![](img/4fcda832-ff9a-491d-9c16-031a2ca91188.png)\n\n4.  向`AMyHUD`类添加一个成员，加上两个新的函数定义:\n\n```cpp\n    Widget* heldWidget;  // hold the last touched Widget in memory \n\n    void MouseClicked();\n    void MouseMoved();\n```\n\n5.  接下来，在`AMyHUD::MouseClicked()`中，我们开始搜索`Widget`命中:\n\n```cpp\nvoid AMyHUD::MouseClicked()\n{\n    FVector2D mouse;\n    APlayerController* PController = GetWorld()->GetFirstPlayerController();\n    PController->GetMousePosition(mouse.X, mouse.Y);\n    heldWidget = NULL; // clear handle on last held widget \n                       // go and see if mouse xy click pos hits any widgets \n    for (int c = 0; c < widgets.Num(); c++)\n    {\n        if (widgets[c].hit(mouse))\n        {\n            heldWidget = &widgets[c];// save widget \n            return;                  // stop checking \n        }\n    }\n}\n```\n\n6.  在`AMyHUD::MouseClicked`功能中，我们遍历屏幕上的所有小部件，并检查当前鼠标位置是否命中。只需查阅`PController->GetMousePosition()`，即可随时从控制器获取当前鼠标位置。\n7.  每个小部件都根据当前的鼠标位置进行检查，一旦鼠标被拖动，被鼠标点击的小部件就会被移动。一旦我们确定了哪个小部件被击中，我们就可以停止检查了，因此我们有了来自`MouseClicked()`函数的`return`值。\n8.  然而，点击小部件是不够的。我们需要拖动鼠标移动时被击中的小部件。为此，我们需要在`AMyHUD`中实现一个`MouseMoved()`功能:\n\n```cpp\nvoid AMyHUD::MouseMoved()\n{\n    static FVector2D lastMouse;\n    FVector2D thisMouse, dMouse;\n    APlayerController* PController = GetWorld()->GetFirstPlayerController();\n    PController->GetMousePosition(thisMouse.X, thisMouse.Y);\n    dMouse = thisMouse - lastMouse;\n    // See if the left mouse has been held down for \n    // more than 0 seconds. if it has been held down, \n    // then the drag can commence. \n    float time = PController->GetInputKeyTimeDown(\n        EKeys::LeftMouseButton);\n    if (time > 0.f && heldWidget)\n    {\n        // the mouse is being held down. \n        // move the widget by displacement amt \n        heldWidget->pos.X += dMouse.X;\n        heldWidget->pos.Y += dMouse.Y; // y inverted \n    }\n    lastMouse = thisMouse;\n}\n```\n\n拖动功能查看最后一帧和此帧之间的鼠标位置差异，并将选定的小部件移动该量。一个`static`变量(具有局部范围的全局变量)用于记住`MouseMoved()`函数调用之间的`lastMouse`位置。\n\n如何将鼠标的运动与运行`AMyHUD`中的`MouseMoved()`功能联系起来？如果你记得的话，我们已经在`Avatar`课上把鼠标运动联系起来了。我们使用的两个函数是:\n\n*   `AAvatar::Pitch()`(y 轴)\n*   `AAvatar::Yaw()`(x 轴)\n\n扩展这些功能将使您能够将鼠标输入传递到抬头显示器。我现在将向您展示`Yaw`功能，您可以从那里推断`Pitch`将如何工作:\n\n```cpp\nvoid AAvatar::Yaw( float amount ) \n{ \n  //x axis \n  if( inventoryShowing ) \n  { \n    // When the inventory is showing, \n    // pass the input to the HUD \n    APlayerController* PController = GetWorld()- \n     >GetFirstPlayerController(); \n    AMyHUD* hud = Cast<AMyHUD>( PController->GetHUD() ); \n    hud->MouseMoved(); \n    return; \n  } \n  else \n  { \n    AddControllerYawInput(200.f*amount * GetWorld()- \n     >GetDeltaSeconds()); \n  } \n} \n```\n\n`AAvatar::Yaw()`功能首先检查库存是否显示。如果显示，输入将直接发送到抬头显示器，不会影响`Avatar`。如果抬头显示器没有显示，输入直接进入`Avatar`。\n\n确保您在文件顶部添加了`#include \"MyHUD.h\"`以使其生效。\n\n# 练习\n\n1.  完成`AAvatar::Pitch()`功能(y 轴)，将输入发送到抬头显示器，而不是`Avatar`。\n2.  取[第八章](08.html)*演员和棋子*中的 NPC 角色，在玩家靠近时给其一个物品(如`GoldenEgg`)。\n\n# 把东西放在一起\n\n现在您已经有了所有这些代码，您将希望把它们放在一起，并看到它工作。使用您复制的网格来创建新的蓝图，方法是在类查看器中右键单击`PickupItem`类，然后选择创建蓝图类，就像我们之前所做的那样。设置值(包括网格)，然后将对象拖到游戏中。当你走进它们时，你会得到一个信息，它被捡起来了。届时，您可以点击 *I* 查看您的库存。\n\n# 摘要\n\n在这一章中，我们讲述了如何设置多个拾取项目，让玩家看到显示在关卡中，并且也拾取。我们还在屏幕上显示它们，并添加了拖动小部件的功能。在[第 11 章](11.html)*怪物*中，我们将介绍怪物以及如何让它们跟随并攻击玩家。"
  },
  {
    "path": "docs/learn-cpp-build-game-ue4/11.md",
    "content": "# 十一、怪物\n\n在本章中，我们将为玩家添加对手。我们将创建一个新的场景来漫游，当怪物足够接近玩家时，它们会开始向玩家走来。一旦他们进入玩家的范围内，他们也会攻击，给你一些基本的游戏玩法。\n\n![](img/099dee92-144b-4d95-8154-49031935ac34.png)\n\nLet's take a look at the topics covered in this chapter:\n\n*   风景\n*   创造怪物\n*   怪物攻击玩家\n\n# 风景\n\n我们还没有在这本书里讨论如何雕刻风景，所以我们将在这里讨论。首先，你必须有一个工作环境。为此，请遵循以下步骤:\n\n1.  导航到文件|新级别，开始一个新文件....可以选择空的关卡或者有天空的关卡。在这个例子中，我选择了没有天空的那个。\n2.  要创建一个景观，我们必须从模式面板工作。通过导航到窗口|模式，确保显示模式面板:\n\n![](img/8a3fa3b5-85bb-480e-a634-504b05e0fe5e.png)\n\n3.  风景可以分三步创建，如下图所示:\n\n![](img/94d517c5-c8a8-4b22-af61-ae788248c780.png)\n\n这三个步骤如下:\n\n4.  你现在应该有一个可以工作的环境了。它将在主窗口中显示为灰色平铺区域:\n\n![](img/7a6f7cb0-afd0-4851-ac58-07423d5fd68e.png)\n\n你要做的第一件事就是给你的风景添加一些颜色。没有颜色的风景是什么？\n\n5.  单击灰色平铺景观对象上的任意位置。在右侧的“详细信息”面板中，您将看到其中填充了信息，如下图所示:\n\n![](img/d385810c-148a-4e9e-8c31-ac63d7f20a8b.png)\n\n6.  向下滚动，直到看到“风景材质”属性。您可以为逼真的地面选择“地面草”材质。\n7.  为场景添加灯光。你可能应该使用一个方向灯，这样所有的地面上都有一些光。我们在[第 8 章](08.html)、*演员和棋子*中讨论了如何做到这一点。\n\n# 雕刻风景\n\n平坦的风景可能很无聊。我们至少应该给这个地方增加一些曲线和山丘。为此，请执行以下步骤:\n\n1.  单击“模式”面板中的“雕刻”按钮:\n\n![](img/08ac4743-6ac4-4870-951b-963bf391d7e9.png)\n\n笔刷的强度和大小由“模式”窗口中的“笔刷大小”和“工具强度”参数决定。\n\n2.  点击你的风景，拖动鼠标改变草坪的高度。\n3.  一旦你对你所拥有的感到满意，点击“播放”按钮进行尝试。结果输出可以在下面的截图中看到:\n\n![](img/1930fd27-8065-41f2-ba2c-671e3ad97354.png)\n\n4.  玩转你的风景，创造一个场景。我所做的是降低一个平坦地面周围的景观，这样玩家就有一个清晰的平坦区域可以行走，如下面的截图所示:\n\n![](img/cdb0d0a7-010b-4ffc-93f4-7aaeeed000db.png)\n\n随意对你的风景做任何你喜欢的事情。如果你愿意，你可以用我在这里做的事情作为灵感。\n\nI recommend that you import assets from ContentExamples or from StrategyGame so that you can use them inside your game. To do this, refer to the *Importing assets* section in [Chapter 10](10.html), *Inventory System and Pickup Items*. When you're done importing assets, we can proceed to bringing monsters into our world.\n\n# 创造怪物\n\n我们将像编程 NPC 和`PickupItem`一样开始编程怪物。我们将编写一个基类(通过从字符派生)来表示`Monster`类，然后为每个怪物类型派生一堆蓝图。每个怪物都有几个共同的属性来决定它的行为。以下是常见属性:\n\n*   它将有一个`float`速度变量。\n*   它将有一个`HitPoints`值的`float`变量(我通常使用浮动来表示惠普，所以我们可以很容易地模拟惠普的背风效果，比如穿过熔岩池)。\n*   它会有一个`int32`变量来表示击败怪物的经验。\n*   它会对怪物掉落的战利品有`UClass`功能。\n*   对于每次攻击完成的`BaseAttackDamage`，它将有一个`float`变量。\n*   它将有一个`AttackTimeout`的`float`变量，这是怪物在攻击之间休息的时间\n*   它会有两个`USphereComponents`物体:其中一个是`SightSphere`——怪物能看多远。另一个是`AttackRangeSphere`，这就是它的攻击范围。`AttackRangeSphere`物体总是比`SightSphere`小。\n\n请遵循以下步骤:\n\n1.  从`Character`类派生，为`Monster`创建你的类。您可以在 UE4 中通过转到文件|新的 C++ 类来做到这一点...然后从基类的菜单中选择字符选项。\n2.  用基本属性填写`Monster`类。\n3.  一定要声明`UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = MonsterProperties)`，这样怪物的属性就可以在蓝图中改变了。这是你在`Monster.h`应该有的:\n\n```cpp\n#pragma once\n\n#include \"CoreMinimal.h\"\n#include \"GameFramework/Character.h\"\n#include \"Components/SphereComponent.h\"\n#include \"Monster.generated.h\"\n\nUCLASS()\nclass GOLDENEGG_API AMonster : public ACharacter\n{\n    GENERATED_BODY()\npublic:\n    AMonster(const FObjectInitializer& ObjectInitializer);\n\n        // How fast he is \n        UPROPERTY(EditAnywhere, BlueprintReadWrite, Category =\n            MonsterProperties)\n        float Speed;\n\n    // The hitpoints the monster has \n    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category =\n        MonsterProperties)\n        float HitPoints;\n\n    // Experience gained for defeating \n    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category =\n        MonsterProperties)\n        int32 Experience;\n\n    // Blueprint of the type of item dropped by the monster \n    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category =\n        MonsterProperties)\n        UClass* BPLoot;\n\n    // The amount of damage attacks do \n    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category =\n        MonsterProperties)\n        float BaseAttackDamage;\n\n    // Amount of time the monster needs to rest in seconds \n    // between attacking \n    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category =\n        MonsterProperties)\n        float AttackTimeout;\n\n    // Time since monster's last strike, readable in blueprints \n    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category =\n        MonsterProperties)\n        float TimeSinceLastStrike;\n\n    // Range for his sight \n    UPROPERTY(VisibleDefaultsOnly, BlueprintReadOnly, Category =\n        Collision)\n        USph.ereComponent* SightSphere;\n\n    // Range for his attack. Visualizes as a sphere in editor, \n    UPROPERTY(VisibleDefaultsOnly, BlueprintReadOnly, Category =\n        Collision)\n        USphereComponent* AttackRangeSphere;\n};\n```\n\n4.  在你的`Monster`构造函数中，你需要一些最少的代码来初始化怪物的属性。在`Monster.cpp`文件中使用以下代码(这将替换默认构造函数):\n\n```cpp\nAMonster::AMonster(const FObjectInitializer& ObjectInitializer)\n : Super(ObjectInitializer)\n{\n Speed = 20;\n HitPoints = 20;\n Experience = 0;\n BPLoot = NULL;\n BaseAttackDamage = 1;\n AttackTimeout = 1.5f;\n TimeSinceLastStrike = 0;\n\n SightSphere = ObjectInitializer.CreateDefaultSubobject<USphereComponent>\n (this, TEXT(\"SightSphere\"));\n SightSphere->AttachToComponent(RootComponent, FAttachmentTransformRules::KeepWorldTransform);\n\n AttackRangeSphere = ObjectInitializer.CreateDefaultSubobject\n <USphereComponent>(this, TEXT(\"AttackRangeSphere\"));\n AttackRangeSphere->AttachToComponent(RootComponent, FAttachmentTransformRules::KeepWorldTransform);\n}\n```\n\n5.  编译并运行代码。\n6.  打开虚幻编辑器，根据你的`Monster`类导出一个蓝图(称之为`BP_Monster`)。\n7.  现在，我们可以开始配置我们怪物的`Monster`属性了。对于骨骼网格，我们不会对怪物使用相同的模型，因为我们需要怪物能够进行近战攻击，并且相同的模型不会附带近战攻击。然而，Mixamo 动画包文件中的一些模型有近战攻击动画。\n\n8.  所以，从 UE4 市场下载 Mixamo 动画包文件(免费):\n\n![](img/362f9aaa-ae15-478b-b496-a3508975a84b.png)\n\n包里面有一些我会避免的很恶心的模型，但是其他的都很好。\n\n9.  您应该将 Mixamo 动画包文件添加到您的项目中。它有一段时间没有更新了，但是您可以通过选中“显示所有项目”并从下拉列表中选择 4.10 版本来添加它，如下图所示:\n\n![](img/0e166f02-471a-4ebc-a3ef-148acd63bdc0.png)\n\n10.  编辑`BP_Monster`蓝图的类属性，选择 Mixamo_Adam(在当前一期的包中实际上是键入 Maximo_Adam)作为骨架网格。确保将其与胶囊组件对齐。另外，选择 MixamoAnimBP_Adam 作为动画蓝图:\n\n![](img/ca4ed3fc-b5ba-466d-9847-8cf08e780dae.png)\n\n稍后我们将修改动画蓝图，以正确合并近战攻击动画。\n\nWhile you're editing your `BP_Monster` blueprint, change the sizes of the `SightSphere` and `AttackRangeSphere` objects to values that make sense to you. I made my monster's `AttackRangeSphere` object just big enough to be about an arm's reach (60 units) and his `SightSphere` object to be 25 times bigger than that (about 1,500 units).\n\n请记住，怪物一旦进入玩家的`SightSphere`就会开始向玩家移动，一旦进入怪物的`AttackRangeSphere`物体内部，怪物就会开始攻击玩家:\n\n![](img/ebed74f3-72a6-4d98-80f4-72c22542739a.png)\n\n在你的游戏中放置一些你的`BP_Monster`实例；编译并运行。没有任何代码驱动`Monster`角色移动，你的怪物应该只是站在那里无所事事。\n\n# 基本怪物智能\n\n在我们的游戏中，我们将只给`Monster`角色添加基本智力。怪物会知道如何做两件基本的事情:\n\n*   跟踪玩家，跟着他\n*   攻击玩家\n\n怪物不会做其他任何事情。当玩家第一次被看到的时候，你可以让怪物嘲讽玩家，但是我们会留给你一个练习。\n\n# 移动怪物-转向行为\n\n非常基础的游戏中的怪物通常不会有复杂的动作行为。通常，他们只是走向目标并攻击它。我们将在这个游戏中编程这种类型的怪物，但是你可以得到更有趣的游戏，怪物可以在地形上有利地定位自己来执行远程攻击等等。我们不会在这里编程，但这是需要考虑的事情。\n\n为了让`Monster`角色向玩家移动，我们需要在每一帧动态更新`Monster`角色移动的方向。为了更新怪物面对的方向，我们用`Monster::Tick()`方法编写代码。\n\n`Tick`功能运行在游戏的每一帧。`Tick`功能的签名如下:\n\n```cpp\nvirtual void Tick(float DeltaSeconds) override; \n```\n\n您需要将这个函数的原型添加到您的`Monster.h`文件中的`AMonster`类中。如果我们覆盖`Tick`，我们可以在每一帧中放置`Monster`角色应该做的自定义行为。以下是一些基本代码，可以在每一帧中将怪物移向玩家:\n\n```cpp\nvoid AMonster::Tick(float DeltaSeconds) {\n    Super::Tick(DeltaSeconds); \n\n    //basic intel : move the monster towards the player \n    AAvatar *avatar = Cast<AAvatar>(\n            UGameplayStatics::GetPlayerPawn(GetWorld(), 0)); \n    if (!avatar) return;\n    FVector toPlayer = avatar->GetActorLocation() - GetActorLocation(); \n    toPlayer.Normalize(); // reduce to unit vector \n                        // Actually move the monster towards the player a bit\n    AddMovementInput(toPlayer, Speed*DeltaSeconds); // At least face the target\n    // Gets you the rotator to turn something // that looks in the `toPlayer`direction \n    FRotator toPlayerRotation = toPlayer.Rotation();\n    toPlayerRotation.Pitch = 0; // 0 off the pitch\n    RootComponent->SetWorldRotation(toPlayerRotation);\n}\n```\n\n您还必须在文件顶部添加以下内容:\n\n```cpp\n#include \"Avatar.h\"\n\n#include \"Kismet/GameplayStatics.h\"\n```\n\n要使`AddMovementInput`工作，您必须在蓝图中的“控制器类”面板下选择一个控制器，如下图所示:\n\n![](img/65466d23-e455-432a-b7e6-7cf73d2f5dd2.png)\n\n如果选择了`None`，对`AddMovementInput`的调用不会有任何影响。为防止这种情况，请选择`AIController`类或`PlayerController`类作为您的管理员类。确保你对地图上的每个怪物都进行了检查。\n\n前面的代码非常简单。它包含了敌人情报的最基本形式——在每一帧中向玩家移动一点点:\n\n![](img/b7ef6c0c-c7db-4051-a3c5-9dd6eb8a64c8.png)\n\nIf your monsters are facing away from the player, try changing the rotation of the mesh -90 degrees in the Z direction.\n\n结果，在一系列的帧之后，怪物会在关卡周围跟踪玩家。要理解这是如何工作的，你必须记住`Tick`函数平均每秒被调用大约 60 次。这意味着，在每一帧中，怪物都会向玩家靠近一点。由于怪物的移动步伐非常小，所以它的动作看起来流畅而连续(而在现实中，它在每一帧中都在进行小的跳跃和跳跃):\n\n![](img/90504cda-d36b-4541-b6f1-7d83149666b3.png)\n\nDiscrete nature of tracking—a monster's motion over three superimposed frames The reason why the monster moves about 60 times a second is because of a hardware constraint. The refresh rate of a typical monitor is 60 Hz, so it acts as a practical limiter on how many updates per second are useful. Updating at a frame rate faster than the refresh rate is possible, but it is not necessarily useful for games since you will only see a new picture once every 1/60 of a second on most hardware. Some advanced physics modeling simulations do almost 1,000 updates a second, but arguably, you don't need that kind of resolution for a game and you should reserve the extra CPU time for something that the player will enjoy instead, such as better AI algorithms. Some newer hardware boasts of a refresh rate of up to 120 Hz (look up gaming monitors, but don't tell your parents I asked you to blow all your money on one).\n\n# 怪物运动的离散性\n\n电脑游戏本质上是离散的。在前面的叠加帧序列截图中，玩家被视为以微小的步伐在屏幕上直线移动。怪物的动作也是在小步前进。在每一帧中，怪物向玩家迈出一小步。怪物正沿着一条明显弯曲的路径，直接朝着玩家在每一帧中的位置移动。\n\n要将怪物移向玩家，请执行以下步骤:\n\n1.  我们必须得到玩家的位置。由于玩家可以在全局函数`UGameplayStatics::GetPlayerPawn`中访问，我们只需使用该函数检索指向玩家的指针。\n2.  我们从指向玩家(`avatar->GetActorLocation()`)的`Monster`函数(`GetActorLocation()`)中找到指向向量。\n3.  我们需要找到从怪物指向化身的向量。为此，您必须从头像的位置减去怪物的位置，如下图所示:\n\n![](img/ba693371-ca92-4022-a37f-db4bafa740c2.png)\n\n这是一个简单的数学规则要记住，但往往容易出错。为了得到正确的向量，总是从目标(终点)向量中减去源(起点)向量。在我们的系统中，我们必须从`Avatar`向量中减去`Monster`向量。这是因为从系统中减去`Monster`矢量会将`Monster`矢量移动到原点，而`Avatar`矢量会移动到`Monster`矢量的左下角:\n\n![](img/28a26f5a-99e2-4765-b5d9-d592214bbbec.png)\n\n请务必试用您的代码。此时，怪物会向你的玩家跑来，并挤在他周围。有了前面概述的代码，它们就不会攻击了；他们会一直跟着他，如下图所示:\n\n![](img/57fafe09-5b16-444e-82e9-518cebc00290.png)\n\n# 怪物视界\n\n现在，怪物们没有注意`SightSphere`组件。也就是说，玩家在世界上的任何地方，怪物都会在当前设置中向他移动。我们现在想改变这种状况。\n\n为此，我们所要做的就是让`Monster`尊重`SightSphere`的限制。如果玩家在怪物的`SightSphere`对象里面，怪物就会追击。否则，怪物会对玩家的位置浑然不觉，不会追击玩家。\n\n检查一个物体是否在球体内部很简单。在下面的截图中，如果 **p** 和中心 **c** 之间的距离 **d** 小于球体半径 **r** ，则点 **p** 在球体内部:\n\n![](img/9cc450f6-dc51-49ea-aa6c-e85521cf4c98.png)\n\nP is inside the sphere when d is less than r\n\n因此，在我们的代码中，前面的截图可以翻译为以下内容:\n\n```cpp\nvoid AMonster::Tick(float DeltaSeconds) \n{ \n  Super::Tick( DeltaSeconds ); \n  AAvatar *avatar = Cast<AAvatar>(  \n   UGameplayStatics::GetPlayerPawn(GetWorld(), 0) ); \n  if( !avatar ) return; \n    FVector toPlayer = avatar->GetActorLocation() -  \n     GetActorLocation(); \n  float distanceToPlayer = toPlayer.Size(); \n  // If the player is not in the SightSphere of the monster, \n  // go back \n  if( distanceToPlayer > SightSphere->GetScaledSphereRadius() ) \n  { \n    // If the player is out of sight, \n    // then the enemy cannot chase \n    return; \n  } \n\n  toPlayer /= distanceToPlayer;  // normalizes the vector \n  // Actually move the monster towards the player a bit \n  AddMovementInput(toPlayer, Speed*DeltaSeconds); \n  // (rest of function same as before (rotation)) \n} \n```\n\n前面的代码为`Monster`字符增加了额外的智能。如果玩家在怪物的`SightSphere`对象之外，那么`Monster`角色现在可以停止追逐玩家。结果是这样的:\n\n![](img/5cf683b3-17bb-4e80-b390-fe36c15f53e8.png)\n\n这里要做的一件好事是将距离比较打包成一个简单的内联函数。我们可以在`Monster`头中提供这两个内联成员函数，如下所示:\n\n```cpp\ninline bool isInSightRange( float d ) \n{ return d < SightSphere->GetScaledSphereRadius(); } \ninline bool isInAttackRange( float d ) \n{ return d < AttackRangeSphere->GetScaledSphereRadius(); } \n```\n\n当传递的参数`d`在所讨论的球体内部时，这些函数返回值`true`。\n\nAn inline function means that the function is more like a macro than a function. Macros are copied and pasted to the calling location, while functions are jumped to by C++ and executed at their location. Inline functions are good because they give good performance while keeping the code easy to read. They are reusable.\n\n# 怪物攻击玩家\n\n怪物可以进行几种不同类型的攻击。根据`Monster`角色的类型，怪物的攻击可能是近战(近距离)或远程(投射武器)。\n\n每当玩家处于其`AttackRangeSphere`对象中时，`Monster`角色就会攻击玩家。如果玩家不在怪物的`AttackRangeSphere`对象的范围内，但是玩家在怪物的`SightSphere`对象中，那么怪物会向玩家靠近，直到玩家在怪物的`AttackRangeSphere`对象中。\n\n# 近战攻击\n\n*混战*的字典定义是一大群迷茫的人。近战攻击是在近距离进行的攻击。想象一群*小狗*与一群*雷兽*搏斗(如果你是星际争霸玩家，你会知道小狗和雷兽都是近战单位)。近战攻击基本都是近距离、肉搏战。要进行近战攻击，你需要一个近战攻击动画，在怪物开始近战攻击时开启。为此，您需要在 UE4 的动画编辑器中编辑动画蓝图。\n\nZak Parrish's series is an excellent place to get started with programming animations in blueprints: [https://www.youtube.com/watch?v=AqYmC2wn7Cg&list=PL6VDVOqa_mdNW6JEu9UAS_s40OCD_u6yp&index=8](https://www.youtube.com/watch?v=AqYmC2wn7Cg&list=PL6VDVOqa_mdNW6JEu9UAS_s40OCD_u6yp&index=8).\n\n现在，我们将只对近战攻击进行编程，然后担心以后会修改蓝图中的动画。\n\n# 定义近战武器\n\n将有三个部分来定义我们的近战武器。它们如下:\n\n*   表示它的 C++ 代码\n*   模型\n*   将代码和模型连接在一起的 UE4 蓝图\n\n# C++ 中近战武器的编码\n\n我们将定义一个新的类`AMeleeWeapon`(源自`AActor`)，来表示手持战斗武器(你现在可能已经想通了，A 会自动添加到你使用的名称中)。我将为`AMeleeWeapon`类附加几个蓝图可编辑的属性，并且`AMeleeWeapon`类将如下代码所示:\n\n```cpp\n#include \"CoreMinimal.h\"\n#include \"GameFramework/Actor.h\"\n#include \"Components/BoxComponent.h\"\n#include \"MeleeWeapon.generated.h\"\n\nclass AMonster;\n\nUCLASS()\nclass GOLDENEGG_API AMeleeWeapon : public AActor\n{\n    GENERATED_BODY()\n\npublic:\n    AMeleeWeapon(const FObjectInitializer& ObjectInitializer);\n\n    // The amount of damage attacks by this weapon do \n    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category =\n        MeleeWeapon)\n        float AttackDamage;\n\n    // A list of things the melee weapon already hit this swing \n    // Ensures each thing sword passes thru only gets hit once \n    TArray<AActor*> ThingsHit;\n\n    // prevents damage from occurring in frames where \n    // the sword is not swinging \n    bool Swinging;\n\n    // \"Stop hitting yourself\" - used to check if the  \n    // actor holding the weapon is hitting himself \n    AMonster *WeaponHolder;\n\n    // bounding box that determines when melee weapon hit \n    UPROPERTY(VisibleDefaultsOnly, BlueprintReadOnly, Category =\n        MeleeWeapon)\n        UBoxComponent* ProxBox;\n\n    UPROPERTY(VisibleDefaultsOnly, BlueprintReadOnly, Category =\n        MeleeWeapon)\n        UStaticMeshComponent* Mesh;\n\n    UFUNCTION(BlueprintNativeEvent, Category = Collision)\n        void Prox(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp,\n            int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);\n\n    // You shouldn't need this unless you get a compiler error that it can't find this function.\n    virtual int Prox_Implementation(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp,\n        int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);\n\n    void Swing();\n    void Rest();\n};\n```\n\n请注意我是如何为`ProxBox`使用边界框而不是边界球的。这是因为剑和斧更接近于盒子而不是球体。有两个成员功能，`Rest()`和`Swing()`，让`MeleeWeapon`知道演员处于什么状态(休息还是摇摆)。这个职业中还有一个`TArray<AActor*> ThingsHit`属性，可以记录每次挥杆时被近战武器击中的演员。我们正在对它进行编程，这样武器每次挥杆只能击中一个物体。\n\n`AMeleeWeapon.cpp`文件将只包含一个基本的构造函数和一些简单的代码，当我们的剑击中它时，会向`OtherActor`发送伤害。我们还将实现`Rest()`和`Swing()`功能来清除命中的事物列表。`MeleeWeapon.cpp`文件有以下代码:\n\n```cpp\n#include \"MeleeWeapon.h\"\n#include \"Monster.h\"\n\nAMeleeWeapon::AMeleeWeapon(const FObjectInitializer& ObjectInitializer)\n    : Super(ObjectInitializer)\n{\n    AttackDamage = 1;\n    Swinging = false;\n    WeaponHolder = NULL;\n\n    Mesh = ObjectInitializer.CreateDefaultSubobject<UStaticMeshComponent>(this,\n        TEXT(\"Mesh\"));\n    RootComponent = Mesh;\n\n    ProxBox = ObjectInitializer.CreateDefaultSubobject<UBoxComponent>(this,\n        TEXT(\"ProxBox\"));  \n    ProxBox->OnComponentBeginOverlap.AddDynamic(this,\n            &AMeleeWeapon::Prox);\n    ProxBox->AttachToComponent(RootComponent, FAttachmentTransformRules::KeepWorldTransform);\n}\n\nint AMeleeWeapon::Prox_Implementation(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp,\n    int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)\n{\n    // don't hit non root components \n    if (OtherComp != OtherActor->GetRootComponent())\n    {\n        return -1;\n    }\n\n    // avoid hitting things while sword isn't swinging, \n    // avoid hitting yourself, and \n    // avoid hitting the same OtherActor twice \n    if (Swinging && OtherActor != (AActor *) WeaponHolder &&\n        !ThingsHit.Contains(OtherActor))\n    {\n        OtherActor->TakeDamage(AttackDamage + WeaponHolder->BaseAttackDamage, FDamageEvent(), NULL, this);\n        ThingsHit.Add(OtherActor);\n    }\n\n    return 0;\n}\n\nvoid AMeleeWeapon::Swing()\n{\n    ThingsHit.Empty();  // empty the list \n    Swinging = true;\n}\n\nvoid AMeleeWeapon::Rest()\n{\n    ThingsHit.Empty();\n    Swinging = false;\n}\n```\n\n# 下载剑\n\n为了完成这个练习，我们需要一把剑放入模型的手中。我在卡根·居尔汉的[http://tf3dm.com/3d-model/sword-95782.html](http://tf3dm.com/3d-model/sword-95782.html)的项目中加入了一把名为*基里奇*的剑。以下是您将获得免费模型的其他地方的列表:\n\n*   [http://www.turbosquid.com/](http://www.turbosquid.com/)\n*   [http://tf3dm.com/](http://tf3dm.com/)\n*   [http://archive3d.net/](http://archive3d.net/)\n*   [http://www.3dtotal.com/\n    T2】](http://www.3dtotal.com/)\n\nSecret tip\n\nIt might appear at first on [TurboSquid.com](http://TurboSquid.com) that there are no free models. In fact, the secret is that you have to select free under Price:\n\n![](img/a490b27f-7f26-432b-b5aa-42cc31a5c2f0.png)\n\n我不得不稍微编辑一下 kilic 剑网来修正初始尺寸和旋转。您可以将 **Filmbox** ( **FBX** )格式的任何网格导入到游戏中。kilic 剑模型在本章的示例代码包中。要将您的剑导入 UE4 编辑器，请执行以下步骤:\n\n1.  右键单击要添加模型的任何文件夹\n2.  导航到新资产|导入到(路径)...\n3.  从弹出的文件资源管理器中，选择要导入的新资产。\n4.  如果“模型”文件夹不存在，只需右键单击左侧的树视图，然后在“内容浏览器”选项卡左侧的窗格中选择“新建文件夹”，即可创建一个文件夹\n\n我从桌面上选择了`kilic.fbx`资产:\n\n![](img/45784dd7-4728-40b3-9c87-5ac514e17acb.png)\n\n# 为你的近战武器创建一个蓝图\n\n创建近战武器蓝图的步骤如下:\n\n1.  在 UE4 编辑器中，基于`AMeleeWeapon`创建一个名为`BP_MeleeSword`的蓝图。\n2.  配置`BP_MeleeSword`使用 kilic 刀片型号(或您选择的任何刀片型号)，如下图截图所示:\n\n![](img/cca72232-e787-4af8-b8df-e39c6920a0e5.png)\n\n3.  `ProxBox`类将决定是否有东西被武器击中，所以我们将修改`ProxBox`类，使其刚好围住剑之刃，如下图截图所示:\n\n![](img/557119be-e6aa-4e96-98b5-2f2afe4b1341.png)\n\n4.  在“碰撞预设”面板下，为网格选择“无碰撞”选项(而不是“全部阻止”)非常重要。下面的截图说明了这一点:\n\n![](img/bd4c4809-0985-44e7-91bc-afe6a6e32a7d.png)\n\n5.  如果你选择了 BlockAll，那么游戏引擎会通过推开剑每次挥动时接触到的东西，自动解决剑和角色之间的所有穿插。结果是你的角色看起来会在挥剑的时候飞起来。\n\n# 套接字\n\nUE4 中的插座是一个骨架网上另一个的插座`Actor`。您可以将插座放置在骨骼网格体的任何位置。正确放置插座后，可以在 UE4 代码中将另一个`Actor`附加到该插座上。\n\n例如，如果我们想把一把剑放在我们的怪物手里，我们只需要在我们的怪物手里创建一个插座。我们可以通过在玩家的头上创建一个插座来为他安装头盔。\n\n# 在怪物的手上创建一个骨骼网格插座\n\n要将插座连接到怪物的手上，我们必须编辑怪物正在使用的骨骼网格。由于我们对怪物使用了 Mixamo_Adam 骨骼网格，我们必须打开并编辑这个骨骼网格。为此，请执行以下步骤:\n\n1.  双击内容浏览器选项卡中的 Mixamo_Adam 骨架网格(这将显示为 T 姿势)以打开骨架网格编辑器。\n2.  如果您在内容浏览器选项卡中没有看到 Mixamo Adam，请确保您已经从虚幻启动器应用中将 Mixamo 动画包文件导入到项目中:\n\n![](img/38214ed6-eb3e-41ec-9713-af15d7a21f2f.png)\n\n3.  点击屏幕右上角的骨架。\n4.  向下滚动左侧面板中的骨骼树，直到找到右侧骨骼。\n\n5.  我们会在这块骨头上安一个插座。右键单击右侧骨骼，选择添加插座，如下图所示:\n\n![](img/8804beca-cfb9-4430-b55d-a345d5a9b4fc.png)\n\n6.  您可以保留默认名称(RightHandSocket)或根据需要重命名套接字，如下图所示:\n\n![](img/ef650814-b14d-40e9-9416-df4685a222ff.png)\n\n接下来，我们需要在演员的手上加一把剑。\n\n# 将剑附在模型上\n\n附剑步骤如下:\n\n1.  打开 Adam 骨骼网格，在树视图中找到 RightHandSocket 选项。既然亚当用右手挥杆，你就应该把剑挂在他的右手上。\n\n2.  右键单击右侧锁定选项，选择添加预览资源，并在出现的窗口中找到剑的骨架网格:\n\n![](img/4fef8179-57ae-4873-926e-6151ac4dd213.png)\n\n3.  您应该会在模型的图像中看到亚当握剑，在下面截图的右侧:\n\n![](img/4f3f2584-be87-4dc2-9467-423e25a01453.png)\n\n4.  现在，点击右手锁定，放大亚当的手。我们需要在预览中调整插座的位置，以便剑正确地放入其中。\n5.  使用移动和旋转操纵器或手动更改“详细信息”窗口中的插座参数，将剑排成一行，使其正确放在他的手中:\n\n![](img/35148bf0-82e2-46c0-9c75-6e6e48946a84.png)\n\nA real-world tip\n\nIf you have several sword models that you want to switch in and out of the same `RightHandSocket`, you will need to ensure quite a bit of uniformity (lack of anomalies) between the different swords that are supposed to go in that same socket.\n\n6.  您可以通过转到屏幕右上角的“动画”选项卡来预览手握剑的动画:\n\n![](img/d85e7871-26ca-400d-900a-882e7f37f0df.png)\n\n然而，如果你启动你的游戏，亚当不会拿着剑。这是因为在 Persona 中将剑添加到插座只是为了预览。\n\n# 给玩家装备剑的代码\n\n要从代码中为玩家装备一把剑，并将其永久绑定到角色，请实例化一个`AMeleeWeapon`实例，并在怪物实例初始化后将其附加到`RightHandSocket`上。我们在`PostInitializeComponents()`中这样做，因为在这个函数中，`Mesh`对象已经被完全初始化了。\n\n在`Monster.h`文件中，添加一个钩子来选择要使用的近战武器的`Blueprint`类名(`UClass`)。此外，使用以下代码为变量添加一个钩子来实际存储`MeleeWeapon`实例:\n\n```cpp\n// The MeleeWeapon class the monster uses \n// If this is not set, he uses a melee attack \nUPROPERTY(EditAnywhere, BlueprintReadWrite, Category =  \n   MonsterProperties) \nUClass* BPMeleeWeapon; \n\n// The MeleeWeapon instance (set if the character is using \n// a melee weapon) \nAMeleeWeapon* MeleeWeapon; \n```\n\n另外，确保在文件顶部添加`#include \"MeleeWeapon.h\"`。现在，在你的怪物蓝图职业中选择`BP_MeleeSword`蓝图。\n\n在 C++ 代码中，您需要实例化武器。为此，我们需要为`Monster`类声明并实现一个`PostInitializeComponents`函数。在`Monster.h`中，添加一个原型声明:\n\n```cpp\nvirtual void PostInitializeComponents() override; \n```\n\n`PostInitializeComponents`在怪物对象的构造器完成并且对象的所有组件都被初始化(包括蓝图构造)之后运行。因此，这是检查怪物是否附有`MeleeWeapon`蓝图的最佳时机，如果有，则实例化该武器。在`AMonster::PostInitializeComponents()`的`Monster.cpp`实现中添加了以下代码来实例化武器:\n\n```cpp\nvoid AMonster::PostInitializeComponents()\n{\n    Super::PostInitializeComponents();\n\n    // instantiate the melee weapon if a bp was selected \n    if (BPMeleeWeapon)\n    {\n        MeleeWeapon = GetWorld()->SpawnActor<AMeleeWeapon>(\n            BPMeleeWeapon, FVector(), FRotator());\n\n        if (MeleeWeapon)\n        {\n            const USkeletalMeshSocket *socket = GetMesh()->GetSocketByName(\n                FName(\"RightHandSocket\")); // be sure to use correct \n                                    // socket name! \n            socket->AttachActor(MeleeWeapon, GetMesh());\n            MeleeWeapon->WeaponHolder = this;\n        }\n    }\n}\n```\n\n另外，确保将`#include \"Engine/SkeletalMeshSocket.h\"`放在文件的顶部。如果选择`BPMeleeWeapon`作为怪物的蓝图，怪物现在将开始手握剑:\n\n![](img/c8ca07fb-67ea-4ec5-bc93-e6994e0512ef.png)\n\n# 触发攻击动画\n\n默认情况下，我们的 C++ `Monster`类与触发攻击动画之间没有联系；换句话说，`MixamoAnimBP_Adam`类没有办法知道怪物什么时候处于攻击状态。\n\n因此，我们需要更新亚当骨架(`MixamoAnimBP_Adam`)的动画蓝图，在`Monster`类变量列表中加入一个查询，检查怪物是否处于攻击状态。我们以前没有在这本书里使用过动画蓝图(或者一般的蓝图)，但是按照这些说明一步一步来，你应该会看到它走到一起。\n\nI'll introduce blueprints terminology gently here, but I'll encourage you to have a look at Zak Parrish's tutorial series at [https://www.youtube.com/playlist?list=PLZlv_N0_O1gbYMYfhhdzfW1tUV4jU0YxH](https://www.youtube.com/playlist?list=PLZlv_N0_O1gbYMYfhhdzfW1tUV4jU0YxH) for your first introduction to blueprints.\n\n# 蓝图基础\n\nUE4 蓝图是代码的可视化实现(不要与有时人们说 C++ 类是类实例的隐喻蓝图相混淆)。在 UE4 蓝图中，您不是实际编写代码，而是将元素拖放到图形上，并将它们连接起来以实现所需的播放。通过将正确的节点连接到正确的元素，您可以在游戏中编写任何您想要的程序。\n\nThis book does not encourage the use of blueprints since we are trying to encourage you to write your own code instead. Animations, however, are best worked with blueprints, because that is what artists and designers will know.\n\n让我们开始编写一个示例蓝图，了解它们是如何工作的:\n\n1.  点击顶部的蓝图菜单栏，选择开放级蓝图，如下图所示:\n\n![](img/30c64367-3938-4bd3-9bff-a4ab5a4d36ea.png)\n\n当您开始层级时，层级蓝图选项会自动执行。一旦你打开这个窗口，你应该会看到一个空白的石板来创建你的游戏，如下所示:\n\n![](img/69ea554d-fcca-457e-9d6c-c9918fbb4acd.png)\n\n2.  右键单击图表纸上的任意位置。\n3.  开始输入`begin`并从出现的下拉列表中点击事件开始播放选项。\n\nEnsure that the Context Sensitive checkbox is checked, as shown in the following screenshot:\n\n![](img/09a8efff-e8c0-4d7b-a7e2-66fe5ebd1646.png)\n\n4.  点击事件开始播放选项后，屏幕上会立即出现一个红色框。它的右手边有一个白色别针。这称为执行引脚，如下所示:\n\n![](img/856e15bb-4a78-448d-a1a5-2f62f37d9da2.png)\n\n关于动画蓝图，您首先需要知道的是白色 pin 执行路径(白线)。如果您以前看过蓝图图，您一定注意到一条白线穿过该图，如下图所示:\n\n![](img/8afd6b4b-eab7-435c-837a-957a1427358a.png)\n\n白色引脚执行路径相当于让代码行一行接一行地排列和运行。白线决定了哪些节点将被执行以及执行的顺序。如果一个节点没有一个白色的执行管脚，那么这个节点根本不会被执行。\n\n5.  将白色执行针拖离事件开始播放。\n6.  首先在可执行动作对话框中输入`draw debug box`。\n7.  选择第一个弹出的东西(fDraw 调试框)，如下所示:\n\n![](img/44282d51-4c34-4c01-bd77-187c8efa5ab9.png)\n\n8.  填写一些您希望盒子看起来如何的细节。在这里，我选择了框的蓝色，框的中心在(0，0，100)，框的大小为(200，200，200)，持续时间为 180 秒(一定要输入足够长的持续时间，以便您可以看到结果)，如下图所示:\n\n![](img/83031d3b-4f34-4633-9969-fbc4a60e86fe.png)\n\n9.  现在，点击播放按钮实现图形。请记住，您必须找到世界的原点才能看到调试框。\n10.  通过在(`0, 0`，(某个`z`值))放置一个金蛋来查找世界原点，如下图截图所示，或者尝试增加线条粗细使其更加可见:\n\n![](img/0847a7b9-141e-4370-828b-fffb37162f92.png)\n\n这是盒子在关卡中的外观:\n\n![](img/ec1eee36-0d8a-4f42-a3ad-ab2455b7b498.png)\n\n# 修改 Mixamo Adam 的动画蓝图\n\n为了整合我们的攻击动画，我们必须修改蓝图。在内容浏览器下，打开`MixamoAnimBP_Adam`。\n\n您将注意到的第一件事是，该图在事件通知部分上方有两个部分:\n\n*   顶部标记为基本角色移动....\n*   底部说 Mixamo 示例角色动画....\n\n基本角色动作负责模型的行走和跑步动作。我们将在 Mixamo 示例角色动画的攻击和跳跃部分工作，该部分负责攻击动画。我们将在图的后半部分工作，如下面的截图所示:\n\n![](img/ebebd7b4-e25e-4b79-859c-395edd3b50d9.png)\n\n当您第一次打开图表时，它是从靠近底部的部分放大开始的。要向上滚动，右键单击鼠标并向上拖动。您也可以使用鼠标滚轮或按住 *Alt* 键和鼠标右键，同时向上移动鼠标来缩小。\n\n在继续之前，您可能希望复制 MixamoAnimBP_Adam 资源，这样就不会损坏原始资源，以防以后需要返回并更改某些内容。这样，如果您发现自己在一次修改中犯了错误，就可以轻松地返回并纠正错误，而不必将整个动画包的新副本重新安装到项目中:\n\n![](img/b1e64964-84fe-4a34-8d19-cac80acc6293.png)\n\nWhen assets are added to a project from the Unreal Launcher, a copy of the original asset is made, so you can modify MixamoAnimBP_Adam in your project now and get a fresh copy of the original assets in a new project later.\n\n我们只做几件事，让亚当在进攻时挥剑。让我们按照这个顺序来做:\n\n1.  删除写着攻击的节点？：\n\n![](img/a854e233-ab67-4e0f-9f14-5235f6478623.png)\n\n2.  重新排列节点，如下所示，使“启用攻击”节点位于底部:\n\n![](img/c60e121a-d732-400c-8326-59e9c536c342.png)\n\n3.  我们将处理这个动画制作的怪物。向上滚动图形一点，并在“尝试获取典当物主”对话框中拖动标记为“返回值”的蓝点。将它放入图形中，当弹出菜单出现时，选择“转换为怪物”(确保选中了“上下文相关”，否则“转换为怪物”选项将不会出现)。“尝试获取棋子所有者”选项获取拥有动画的`Monster`实例，它只是`AMonster`类对象，如下图所示:\n\n![](img/af2cf342-af78-4cff-b678-654390089ac3.png)\n\n4.  单击序列对话框中的+并将另一个执行引脚从序列组拖到“转换为怪物”节点实例，如下图所示。这确保了“强制转换为怪物”实例实际得到执行:\n\n![](img/bbee93c5-cb1f-435a-bbc1-cd9b4d36e413.png)\n\n5.  下一步是从“施放到怪物”节点的“作为怪物”终端拔出大头针，并查找“在攻击范围内”属性:\n\nFor this to show up, you need to go back to `Monster.h` and add the following line before the is in Attack Range function and compile the project (this will be explained a little later):\n`UFUNCTION(BlueprintCallable, Category = Collision)`\n\n![](img/17dac8e2-421b-4e46-933a-76910aa82edc.png)\n\n6.  从左侧的“施法到怪物”节点到右侧的“在攻击范围内”节点之间，应该会自动出现一条线。接下来，从“作为怪物”中拖动另一条线，这次查找“获取距离到”:\n\n![](img/d6c28419-9dd2-4087-964b-a534901f7447.png)\n\n7.  您需要添加一个节点，让玩家角色发送到“获取距离”的“其他参与者”节点。只需右键单击任意位置并查找获取玩家角色:\n\n![](img/b4197cef-65b9-4ab1-a66b-2b2223366a1b.png)\n\n8.  将“获取玩家角色”的“返回值”节点连接到“其他角色”，并将“获取距离”的“返回值”连接到“在攻击范围内”:\n\n![](img/a346fb6f-3da4-4e9c-baf1-ad7ef4f63973.png)\n\n9.  将白色和红色引脚拉到 SET 节点，如下所示:\n\n![](img/35f5c1f6-8cbe-45e6-a86e-324565b2024b.png)\n\nThe equivalent pseudocode of the preceding blueprint is something similar to the following:\n\n```cpp\nif(   Monster.isInAttackRangeOfPlayer() )   \n{   \n    Monster.Animation = The Attack Animation;   \n}   \n```\n\n测试你的动画。怪物应该只在玩家的范围内摆动。如果不起作用，并且您创建了一个副本，请确保您将`animBP`切换到副本。还有，默认动画是射击，不是挥剑。我们稍后会解决这个问题。\n\n# 挥剑密码\n\n我们想在挥剑时添加一个动画通知事件:\n\n1.  声明一个蓝图可调用 C++ 函数并将其添加到您的`Monster`类中:\n\n```cpp\n// in Monster.h: \nUFUNCTION( BlueprintCallable, Category = Collision ) \nvoid SwordSwung(); \n```\n\n`BlueprintCallable`语句意味着可以从蓝图中调用该函数。换句话说，`SwordSwung()`将是一个我们可以从蓝图节点调用的 C++ 函数，如下所示:\n\n```cpp\n// in Monster.cpp \nvoid AMonster::SwordSwung() \n{ \n  if( MeleeWeapon ) \n  { \n    MeleeWeapon->Swing(); \n  } \n} \n```\n\n2.  通过在内容浏览器中双击打开 Mixamo_Adam_Sword_Slash 动画(它应该在 mixamoanimspack/Mixamo _ Adam/Anims/Mixamo _ Adam _ Sword _ Slash 中)。\n3.  找到亚当开始挥剑的地方。\n\n4.  右键单击通知栏上的那个点，并在添加通知下选择新建通知...，如下图所示:\n\n![](img/b8e6713c-cf27-4651-b2d0-95376742a1e6.png)\n\n5.  通知名称`SwordSwung`:\n\n![](img/40f7087a-a2a0-49da-99d7-7ac61a68c4d9.png)\n\n通知名称应该出现在动画的时间线中，如下所示:\n\n![](img/0f5f2318-f089-4e65-abb2-28e50f1f3ce4.png)\n\n6.  保存动画，然后再次打开你的 MixamoAnimBP_Adam 版本。\n7.  在 SET 节点组下面，创建以下图形:\n\n![](img/e70c5fcf-6cba-47f1-bacd-2174d596cd80.png)\n\n8.  当您在图形中右键单击(打开上下文相关)并开始键入`SwordSwung`时，会出现动漫通知 _ 剑挥节点。“投射到怪物”节点再次从“尝试获得棋子所有者”节点输入，如*修改“米夏莫·亚当”*动画蓝图部分的第 2 步。\n9.  剑挥是我们的蓝图-在`AMonster`类中可调用的 C++ 函数(你需要为此编译项目才能显示出来)。\n10.  你还需要进入 MaximoAnimBP_Adam 的动画选项卡。\n11.  双击状态机打开该图。\n12.  双击攻击状态打开它。\n13.  选择左边写着“播放 Mixamo_Adam 拍摄”的那个。\n14.  拍摄是默认动画，但这显然不是我们想要发生的。所以，删除它，右键单击并寻找“玩 Mixamo_Adam_Sword_Slash”。然后，从一个人的小图标上单击并拖动它最终动画姿势的结果:\n\n![](img/a5be2291-79fe-4c79-946c-cbebc67ae619.png)\n\n如果你现在开始游戏，你的怪物会在实际攻击时执行他们的攻击动画。如果你也在`AAvatar`类中覆盖`TakeDamage`来降低血量，当剑的包围盒接触到你的时候，你会看到你的血量条下降了一点(回想一下血量条是在[第八章](08.html)、*演员和棋子*的末尾增加的，作为练习):\n\n![](img/e4885b2d-6c9b-4228-9cec-1e4a0e84cf1f.png)\n\n# 投射或远程攻击\n\n远程攻击通常包括某种投射物。射弹是子弹之类的东西，但也可以包括闪电魔法攻击或火球攻击之类的东西。要对投射攻击进行编程，你应该生成一个新的物体，并且只在投射物到达玩家时对玩家造成伤害。\n\n为了在 UE4 中实现一个基本项目符号，我们应该派生一个新的对象类型。我从`AActor`类派生了一个`ABullet`类，如下面的代码所示:\n\n```cpp\n#pragma once\n\n#include \"CoreMinimal.h\"\n#include \"GameFramework/Actor.h\"\n#include \"Components/SphereComponent.h\"\n#include \"Bullet.generated.h\"\n\nUCLASS()\nclass GOLDENEGG_API ABullet : public AActor\n{\n GENERATED_BODY()\n\npublic:\n // Sets default values for this actor's properties\n ABullet(const FObjectInitializer& ObjectInitializer);\n\n // How much damage the bullet does. \n UPROPERTY(EditAnywhere, BlueprintReadWrite, Category =\n Properties)\n float Damage;\n\n // The visible Mesh for the component, so we can see \n // the shooting object \n UPROPERTY(VisibleDefaultsOnly, BlueprintReadOnly, Category =\n Collision)\n UStaticMeshComponent* Mesh;\n\n // the sphere you collide with to do impact damage \n UPROPERTY(VisibleDefaultsOnly, BlueprintReadOnly, Category =\n Collision)\n USphereComponent* ProxSphere;\n\n UFUNCTION(BlueprintNativeEvent, Category = Collision)\n void Prox(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp,\n int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult);\n\n // You shouldn't need this unless you get a compiler error that it can't find this function.\n virtual int Prox_Implementation(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp,\n int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult); };\n```\n\n`ABullet`类中有几个重要成员，如下所示:\n\n*   子弹接触时造成的伤害的变量\n*   一个`Mesh`变量为子弹的主体\n*   一个`ProxSphere`变量，用于检测子弹何时最终击中某物\n*   在物体附近检测到`Prox`时运行的功能\n\n`ABullet`类的构造函数应该有`Mesh`和`ProxSphere`变量的初始化。在构造器中，我们将`RootComponent`设置为`Mesh`变量，然后将`ProxSphere`变量附加到`Mesh`变量。`ProxSphere`变量将用于碰撞检查。`Mesh`变量的碰撞检查应该关闭，如下代码所示:\n\n```cpp\nABullet::ABullet(const FObjectInitializer& ObjectInitializer)\n    : Super(ObjectInitializer)\n{\n    Mesh = ObjectInitializer.CreateDefaultSubobject<UStaticMeshComponent>(this,\n        TEXT(\"Mesh\"));\n    RootComponent = Mesh;\n\n    ProxSphere = ObjectInitializer.CreateDefaultSubobject<USphereComponent>(this,\n        TEXT(\"ProxSphere\"));\n    ProxSphere->AttachToComponent(RootComponent, FAttachmentTransformRules::KeepWorldTransform);\n\n    ProxSphere->OnComponentBeginOverlap.AddDynamic(this,\n        &ABullet::Prox);\n    Damage = 1;\n}\n```\n\n我们在构造函数中将`Damage`变量初始化为`1`，但是一旦我们从`ABullet`类中创建了一个蓝图，就可以在 UE4 编辑器中进行更改。接下来，`ABullet::Prox_Implementation()`函数应该会对演员造成伤害，如果我们与其他演员的`RootComponent.`发生碰撞，我们可以通过代码实现:\n\n```cpp\nint ABullet::Prox_Implementation(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp,\n    int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)\n{\n    if (OtherComp != OtherActor->GetRootComponent())\n    {\n        // don't collide w/ anything other than \n        // the actor's root component \n        return -1;\n    }\n\n    OtherActor->TakeDamage(Damage, FDamageEvent(), NULL, this);\n    Destroy();\n    return 0;\n}\n```\n\n# 子弹物理学\n\n要让子弹飞过关卡，可以使用 UE4 的物理引擎。\n\n基于`ABullet`类创建蓝图。我为网格选择了 Shape_Sphere，并将其缩小到更合适的大小。子弹的网格应该启用碰撞物理，但是子弹的边界球将用于计算伤害。\n\n将项目符号配置为适当的行为有点棘手，因此我们将分四个步骤进行介绍，如下所示:\n\n1.  在“组件”选项卡中选择“网格(继承)”。`ProxSphere`变量应该在网格下。\n2.  在详细信息选项卡中，选中模拟物理和模拟生成命中事件。\n3.  从碰撞预设下拉列表中，选择自定义....\n4.  从启用冲突下拉列表中选择启用冲突(查询和物理)。此外，检查碰撞响应框，如图所示；选中阻止大多数类型(世界静态、世界动态等)并选中重叠，但仅适用于典当:\n\n![](img/d6763116-289f-4124-b346-e15ac551212a.png)\n\n“模拟物理”复选框使`ProxSphere`属性经历重力和施加在其上的脉冲力。冲动是一种瞬间的力量，我们将用它来推动子弹的射出。如果您没有选中“模拟生成击球事件”复选框，则球会掉到地板上。阻挡所有碰撞的作用是确保球不能穿过任何东西。\n\n如果你现在将这些`BP_Bullet`对象从内容浏览器标签直接拖放到世界上，它们将会简单地掉落到地板上。一旦它们掉在地上，你就可以踢它们。下面的截图显示了地板上的球对象:\n\n![](img/28db3eff-f0db-4e40-af94-d77fa0d03978.png)\n\n然而，我们不希望我们的子弹落在地板上。我们希望他们被枪毙。所以，让我们把子弹放在`Monster`班。\n\n# 给怪物类添加子弹\n\n让我们来看一个循序渐进的方法:\n\n1.  向接收蓝图实例引用的`Monster`类添加成员。这就是`UClass`对象类型的用途。另外，添加一个蓝图可配置`float`属性来调整射出子弹的力，如以下代码所示:\n\n```cpp\n// The blueprint of the bullet class the monster uses \nUPROPERTY(EditAnywhere, BlueprintReadWrite, Category =  \n   MonsterProperties) \nUClass* BPBullet; \n// Thrust behind bullet launches \nUPROPERTY(EditAnywhere, BlueprintReadWrite, Category =  \n   MonsterProperties) \nfloat BulletLaunchImpulse; \n```\n\n2.  编译运行 C++ 项目，打开你的`BP_Monster`蓝图。\n3.  现在可以在`BPBullet`下选择一个蓝图类，如下图截图所示:\n\n![](img/7bcb73ca-41ea-41f4-8b58-b8002a16b82c.png)\n\n4.  一旦你选择了一个蓝图类类型来实例化怪物射击时，你必须编程怪物射击时，玩家在其范围内。\n\n怪物从哪里射来？实际上，它应该是从骨头里射出来的。如果您不熟悉这个术语，骨骼只是模型网格中的参考点。模型网格通常由许多“骨骼”组成\n\n5.  要查看一些骨骼，通过双击内容浏览器选项卡中的资源打开 Mixamo_Adam 网格，如下图所示:\n\n![](img/4bab18f9-4d0a-41ae-9488-852cb695ec28.png)\n\n6.  转到骨骼选项卡，您将在左侧的树视图列表中看到所有怪物的骨骼。我们要做的是选择一根骨头，子弹将从骨头中射出。这里，我选择了`LeftHand`选项。\n\nAn artist will normally insert an additional bone into the model mesh to emit the particle, which is likely to be on the tip of the nozzle of a gun.\n\n从基础模型网格开始，我们可以获得`Mesh`骨骼的位置，并让怪物从代码中的该骨骼发出`Bullet`实例。\n\n使用以下代码可以获得完整的怪物`Tick`和`Attack`功能:\n\n```cpp\nvoid AMonster::Tick(float DeltaSeconds) \n{ \n  Super::Tick( DeltaSeconds ); \n\n  // move the monster towards the player \n  AAvatar *avatar = Cast<AAvatar>(  \n   UGameplayStatics::GetPlayerPawn(GetWorld(), 0) ); \n  if( !avatar ) return; \n\n  FVector playerPos = avatar->GetActorLocation(); \n  FVector toPlayer = playerPos - GetActorLocation(); \n  float distanceToPlayer = toPlayer.Size(); \n\n  // If the player is not the SightSphere of the monster, \n  // go back \n  if( distanceToPlayer > SightSphere->GetScaledSphereRadius() ) \n  { \n    // If the player is OS, then the enemy cannot chase \n    return; \n  } \n\n  toPlayer /= distanceToPlayer;  // normalizes the vector \n\n  // At least face the target \n  // Gets you the rotator to turn something \n  // that looks in the `toPlayer` direction \n  FRotator toPlayerRotation = toPlayer.Rotation(); \n  toPlayerRotation.Pitch = 0; // 0 off the pitch \n  RootComponent->SetWorldRotation( toPlayerRotation ); \n\n  if( isInAttackRange(distanceToPlayer) ) \n  { \n    // Perform the attack \n    if( !TimeSinceLastStrike ) \n    { \n      Attack(avatar); \n    } \n\n    TimeSinceLastStrike += DeltaSeconds; \n    if( TimeSinceLastStrike > AttackTimeout ) \n    { \n      TimeSinceLastStrike = 0; \n    } \n\n    return;  // nothing else to do \n  } \n  else \n  { \n    // not in attack range, so walk towards player \n    AddMovementInput(toPlayer, Speed*DeltaSeconds); \n  } \n} \n```\n\n`AMonster::Attack`功能比较简单。当然，我们首先需要在`Monster.h`文件中添加一个原型声明，以便在`.cpp`文件中编写我们的函数:\n\n```cpp\nvoid Attack(AActor* thing); \n```\n\n在`Monster.cpp`中，我们实现`Attack`功能，如下所示:\n\n```cpp\nvoid AMonster::Attack(AActor* thing) \n{ \n  if( MeleeWeapon ) \n  { \n    // code for the melee weapon swing, if  \n    // a melee weapon is used \n    MeleeWeapon->Swing(); \n  } \n  else if( BPBullet ) \n  { \n    // If a blueprint for a bullet to use was assigned, \n    // then use that. Note we wouldn't execute this code \n    // bullet firing code if a MeleeWeapon was equipped \n    FVector fwd = GetActorForwardVector(); \n    FVector nozzle = GetMesh()->GetBoneLocation( \"RightHand\" ); \n    nozzle += fwd * 155;// move it fwd of the monster so it  \n     doesn't \n    // collide with the monster model \n    FVector toOpponent = thing->GetActorLocation() - nozzle; \n    toOpponent.Normalize(); \n    ABullet *bullet = GetWorld()->SpawnActor<ABullet>(  \n     BPBullet, nozzle, RootComponent->GetComponentRotation()); \n\n    if( bullet ) \n    { \n      bullet->Firer = this; \n      bullet->ProxSphere->AddImpulse(  \n        toOpponent*BulletLaunchImpulse ); \n    } \n    else \n    { \n      GEngine->AddOnScreenDebugMessage( 0, 5.f,  \n      FColor::Yellow, \"monster: no bullet actor could be spawned.  \n       is the bullet overlapping something?\" ); \n    } \n  } \n} \n```\n\n另外，确保在文件顶部添加`#include \"Bullet.h\"`。我们让实现近战攻击的代码保持原样。假设怪物没有手持近战武器，那么我们就检查一下`BPBullet`成员是否设置好了。如果设置了`BPBullet`成员，这意味着怪物将创建并触发`BPBullet`蓝印类的一个实例。\n\n请特别注意以下几行:\n\n```cpp\nABullet *bullet = GetWorld()->SpawnActor<ABullet>(BPBullet,  \n   nozzle, RootComponent->GetComponentRotation() );\n```\n\n这就是我们如何给这个世界增加一个新的演员。`SpawnActor()`函数将您传入的`UCLASS`实例放入`spawnLoc`，并带有一些初始方向。\n\n在我们生成子弹后，我们调用其`ProxSphere`变量上的`AddImpulse()`函数来推进它。\n\n另外，在 Bullet.h 中添加以下一行:\n\n```cpp\nAMonster *Firer;\n```\n\n# 玩家反击\n\n为了给玩家添加一个震退，我给`Avatar`类添加了一个名为`knockback`的成员变量。每当化身受到伤害时，就会发生反击:\n\n```cpp\nFVector knockback; // in class AAvatar\n```\n\n要想知道玩家被击中后要往哪个方向回击，我们需要给`AAvatar::TakeDamage`添加一些代码。这将覆盖`AActor`类中的版本，所以首先，将它添加到头像中。\n\n```cpp\nvirtual float TakeDamage(float DamageAmount, struct FDamageEvent const& DamageEvent, class AController* EventInstigator, AActor* DamageCauser) override;\n```\n\n计算从攻击者到玩家的方向向量，并将该向量存储在`knockback`变量中:\n\n```cpp\nfloat AAvatar::TakeDamage(float DamageAmount, struct FDamageEvent const& DamageEvent, class AController* EventInstigator, AActor* DamageCauser)\n{\n    // add some knockback that gets applied over a few frames \n    knockback = GetActorLocation() - DamageCauser->GetActorLocation();\n    knockback.Normalize();\n    knockback *= DamageAmount * 500; // knockback proportional to damage \n    return AActor::TakeDamage(DamageAmount, DamageEvent, EventInstigator, DamageCauser);\n}\n```\n\n在`AAvatar::Tick`中，我们将震退应用到头像的位置:\n\n```cpp\nvoid AAvatar::Tick( float DeltaSeconds ) \n{ \n  Super::Tick( DeltaSeconds ); \n\n  // apply knockback vector \n  AddMovementInput( -1*knockback, 1.f ); \n\n  // half the size of the knockback each frame \n  knockback *= 0.5f; \n} \n```\n\n由于回退向量随着每一帧而减小，所以它会随着时间的推移而变弱，除非回退向量随着另一次命中而更新。\n\nFor the bullets to work, you need to set BPMelee Weapon to None. You should also increase the size of AttackRangeSphere and adjust Bullet Launch Impulse to a value that works.\n\n# 摘要\n\n在这一章中，我们探索了如何在屏幕上实例化追赶玩家并攻击他的怪物。我们使用了不同的球体来检测怪物是否在视野或攻击范围内，并根据怪物是否有近战武器增加了近战或射击攻击的能力。如果你想进一步实验，你可以尝试改变射击动画，或者增加一个额外的球体，让怪物在移动的同时开火，在攻击范围内切换到近战。在下一章中，我们将通过研究先进的人工智能技术来进一步扩展怪物的能力。"
  },
  {
    "path": "docs/learn-cpp-build-game-ue4/12.md",
    "content": "# 十二、使用高级人工智能构建更聪明的怪物\n\n到目前为止，我们遇到的怪物并不多。他们站在一个地方，直到他们进入射程看到你，然后他们走向你，要么进行近战攻击，要么进行射击攻击，这取决于你设置了什么。在真实的游戏中，你希望你的角色做得更多，这样他们看起来更真实。这就是**人工智能** ( **AI** )的用武之地。\n\n人工智能是一个巨大的话题，有整本书都在讨论它，但是我们将讨论 UE4 支持人工智能编程的一些方式，这样你就可以轻松地创建更真实的怪物。我们将快速概述以下主题:\n\n*   导航-寻路和导航网格\n*   行为树\n*   环境查询系统\n*   棉屑\n*   机器学习和神经网络\n*   遗传算法\n\n如果你对学习更多感兴趣，那么你可以看看很多伟大的书籍，更深入地了解你还可以用人工智能做什么。\n\n# 导航-寻路和导航网格\n\n现在，我们创造的怪物只向一个方向移动——直线直接向你的位置移动。但是如果路上有山、建筑物、树木、河流或其他物体呢？在许多情况下，直线是不可能的。现在，如果怪物撞到墙上，它只会呆在那里，这不太现实。这就是寻路的好处。\n\n# 什么是寻路？\n\n寻路是一种找出到达目的地的路径(通常是最短和/或最简单的路径)的方法。将整个环境想象成一个网格，每个单元格中的数字表示导航有多困难。所以一个有墙挡住路的细胞会有很高的值，一条陡峭的路径会比一条容易的路径有更高的值。寻路的目标是当你把路径上的所有单元格相加时，找到整体值最低的路径。\n\n有不同的算法或方法来处理可用的寻路。最著名的一颗叫做 A*(发音为 *A 星*)。\n\n# 什么是 A*？\n\n这里我们不会用 A*了，但是如果你打算以后做 AI 编程的话，你至少应该熟悉一下，所以我做一个简单的概述。A*基本上搜索角色周围的单元格，优先选择成本最低的单元格。它计算到目前为止的路径成本(通过将成本加起来直到那个点)加上一个启发式算法，一个从那个点到目标的成本猜测。\n\n计算启发式方法有很多方法。它可以是一些简单的东西，比如直接到目标的距离(你可能会说，就像乌鸦飞一样)。如果启发式算法实际上比实际成本低，对结果会更好，所以效果很好。\n\n一旦你找到了成本最低的细胞，再往前走一步，看看细胞周围的细胞。你继续，直到你达到目标。如果你发现自己在一个你以前去过的牢房里，这种方式的总路径成本较低，你可以用成本较低的路径代替它。这有助于您获得更短的路径。一旦你到达目标，你就可以沿着这条路往回走，你就会有一条通往目标的完整路径。\n\n你可以在网上或人工智能书籍中找到更多关于 A*和其他寻路算法的信息。如果你在更复杂的项目中这样做，你将需要了解它们，但是对于这一点，UE4 有一个更简单更容易的方法:使用`NavMesh`。\n\n# 使用导航网格\n\nA `NavMesh`是 UE4 中的一个对象，你可以把它放在你的世界中，告诉它你希望角色能够导航到环境的哪个部分。为此，请执行以下步骤:\n\n1.  增加一些障碍。您可以添加立方体、圆柱体或任何其他想要添加的东西来阻止移动，如下所示:\n\n![](img/4ba19b29-326b-4b73-ac0b-5da49506bf4b.png)\n\n2.  一旦你按照你想要的方式设置了关卡，在“模式”窗口中，转到“体积”，找到“导航网格边界体积”，将它拖到关卡上，并缩放它以覆盖你想要怪物能够导航的整个区域。\n\nIf you try it now, you'll still see the monsters walk into walls and just stop. That's because we need to change the way movement is handled. We'll do this by creating our own `AIController` class.\n\n# 创建一个控件类\n\n让我们按照一步一步的步骤来完成:\n\n1.  创建一个新的 C++ 类。在这种情况下，您需要选中“显示所有课程”复选框并搜索以找到`AIController`:\n\n![](img/8be786b7-f5e5-466a-90c5-b5bef7eb9eb5.png)\n\n2.  命名类`MonsterAIController`。你的`MonsterAIController.h`应该是这样的:\n\n```cpp\nUCLASS()\nclass GOLDENEGG_API AMonsterAIController : public AAIController\n{\n    GENERATED_BODY()\n\npublic:\n    //Start following the player\n    void StartFollowingPlayer();\n};\n```\n\n`MonsterAIController.cpp`应实现如下功能:\n\n```cpp\nvoid AMonsterAIController::StartFollowingPlayer()\n{\n    AActor *player = Cast<AActor>(\n        UGameplayStatics::GetPlayerPawn(GetWorld(), 0));\n    FVector playerPos = player->GetActorLocation();\n    MoveToLocation(playerPos);\n}\n```\n\n还要确保在文件顶部添加`#include \"Kismet/GameplayStatics.h\"`。\n\n3.  回到`Monster.cpp`中的`Tick()`功能。在`else`子句中找到以下行:\n\n```cpp\nAddMovementInput(toPlayer, Speed*DeltaSeconds);\n```\n\n删除这一行，代之以:\n\n```cpp\n\n        if (GetController() != nullptr)\n        {\n            Cast<AMonsterAIController>(GetController())-\n            >StartFollowingPlayer();\n        }\n```\n\n同样在文件顶部添加`#include \"MonsterAIController.h\"`，进入`BP_Monster`，将 Ai 控制器类改为`MonsterAIController`。现在怪物可以绕着墙找到你了。如果他们不动，检查以确保`NavMesh`覆盖该区域，并且足够高以覆盖字符。\n\n# 行为树\n\n现在，所有控制怪物的逻辑都在`Monster.cpp`的`Tick()`功能中。但是到目前为止你所做的很简单。在大型复杂的游戏中，怪物会有更多的行为。他们可以在一个区域巡逻，直到他们看到你，或者甚至与你交流，只有在对话不顺利的情况下才会攻击。所有这一切的逻辑将变得过于复杂，以至于无法将所有东西都保存在一个函数中，甚至无法保存在`AMonster`类中。\n\n幸运的是，UE4 还有另一种管理复杂任务的方式，那就是行为树。行为树允许您直观地设置一系列任务，使它们更容易管理。由于我们这里关注的是 C++，我们将通过这种方式创建任务本身，但是在蓝图中整体树似乎更容易管理。\n\n行为树主要由两种不同类型的节点控制:\n\n*   **选择器**:选择器将从左到右遍历其子节点，直到一个成功，然后返回树上。把它想象成一个`or`陈述——一旦它找到一个真实的论点，`or`本身就是真实的，所以它就完成了。\n*   **序列**:相反，序列从左到右遍历孩子，直到一个失败。这更像是一个`and`语句，它一直持续下去，直到某个东西出现错误，从而使整个语句变成错误。\n\n因此，如果您想运行多个步骤，您将使用序列，而如果您只想成功运行一个步骤并停止，您将使用选择器。\n\n# 设置行为树\n\n首先，您需要进入您的库(将它放在一个有意义的文件夹名称中，这样您就可以记住在哪里可以找到它，或者蓝图可以工作)，然后从添加新项中选择人工智能|行为树:\n\n![](img/45cc7752-e2b6-4b38-932b-a2cbbc892b6a.png)\n\n我把我的命名为`MonsterBT`。您还需要创建一个黑板。这将存储您将在行为树中使用的数据，并使您可以轻松地在人工智能控制器和行为树之间传输数据。您可以通过转到添加新项来创建它，这次选择人工智能|黑板。我把这个命名为`MonsterBlackboard`:\n\n![](img/aed7c42b-29df-495d-9c15-63837eff7415.png)\n\n# 设置黑板值\n\n接下来，您需要在刚刚创建的黑板中设置值。您可以通过选择“新密钥”，然后选择一种类型(在本例中为 Bool)来实现。为此，我添加了其中的两个，IsInAttackRange 和 IsInFollowRange:\n\n![](img/bd5fe05d-6f94-4222-a2f7-ebe86f1eefe7.png)\n\n你也可以给每个人描述它的用途。\n\n# 设置任务\n\n我们将创建一个 C++ 任务来处理玩家跟踪。为此，请执行以下步骤:\n\n1.  添加一个新的 C++ 类，并基于 BTTaskNode(您需要查看所有类并搜索它):\n\n![](img/fb9cd044-3520-4c1a-b69a-dbefc762d5e9.png)\n\n我给新班级取名`BTTask_FollowPlayer`\n\n2.  在`BTTaskFollowPlayer.h`中，增加以下内容:\n\n```cpp\nUCLASS()\nclass GOLDENEGG_API UBTTask_FollowPlayer : public UBTTaskNode\n{\n    GENERATED_BODY()\n\n    virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;\n    virtual void OnGameplayTaskActivated(UGameplayTask& Task) override {}\n};\n```\n\n我们不会使用`OnGameplayTaskActivated`，但是，如果不声明它，您的代码可能不会编译(如果您抱怨它不在那里，这就是原因)\n\n3.  在`BTTaskFollowPlayer.cpp`中，增加以下内容:\n\n```cpp\n#include \"BTTask_FollowPlayer.h\"\n#include \"MonsterAIController.h\"\n\nEBTNodeResult::Type UBTTask_FollowPlayer::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)\n{\n    AMonsterAIController* Controller = Cast<AMonsterAIController>(OwnerComp.GetAIOwner());\n    if (Controller == nullptr)\n    {\n        return EBTNodeResult::Failed;\n    }\n\n    Controller->StartFollowingPlayer();\n\n    return EBTNodeResult::Succeeded;\n}\n```\n\nOnce you have this working, you can go back and create another `BTTask` to handle attacking too, as well as any other behaviors you might want.\n\n# 设置行为树本身\n\n一旦设置了任务，就该设置树本身了:\n\n1.  双击它打开蓝图:\n\n![](img/4985b890-0951-4a29-b528-09c21619402f.png)\n\n2.  单击根节点底部的黄色区域，并将其拖出以创建一个新节点(它是黑色的，但当鼠标滑过它时会变成黄色)。\n3.  从出现的菜单中选择类型(我们将使用选择器):\n\n![](img/f26c5c77-9cbf-4f45-a37d-4cdfc6d342bb.png)\n\nThe selector icon in the center tab\n\n4.  您应该具备以下条件:\n\n![](img/0e7d45c8-0303-4cd9-ac6a-6c502be69764.png)\n\n如前所述，选择器将按从左到右的顺序遍历节点，直到一个成功，然后停止。在这种情况下，我们有三种可能的状态:攻击范围内、视距内和两者都没有(忽略玩家)。首先，你要检查你是否足够接近攻击，这意味着你要检查你的黑板中的 IsInAttackRange。\n\n不要先进行跟随，因为攻击范围在技术上仍然在跟随范围内，但是你不想使用跟随功能，所以选择器会在检查跟随范围后停止，因为这是它进行的第一次检查，所以它永远不会检查攻击范围(这是它真正应该检查的)。\n\n要检查它需要处于哪种状态，您需要检查 Blackboard 值，这是通过使用装饰器来完成的。为此，请单击选择器的底部，并像创建节点时那样向左拖动一个新节点，这次选择一个复合选择器节点。此节点允许您右键单击；选择添加装饰器...，并确保选择了 Blackboard 类型。添加后，您可以选择顶部的蓝色装饰器。您应该能够检查键查询 IsSet 并选择您想要检查的值，在这种情况下是 IsInAttackRange(如果它没有显示，请确保在细节中设置 MonsterBlackboard 作为黑板；正常情况下应自动设置):\n\n![](img/b101c01b-ea3c-4dfc-8234-78df2ae18134.png)\n\n攻击节点最终将转到一个攻击任务，但目前，我只是放入一个等待作为占位符(一个内置任务，允许您指定等待时间，单位为秒)。\n\n在它的右边，您还想添加另一个带有检查 IsInFollowRange 的装饰器的复合。这将使用您创建的新任务(如果它没有出现，请确保您已经编译了代码并且没有任何错误)。\n\n在右侧，我添加了一个等待任务，以防两种情况都失败。完成后，你应该有这样的东西:\n\n![](img/195cf009-e901-4bed-a965-f8228644e407.png)\n\n现在，您已经准备好返回并修改您现有的代码来使用所有这些。\n\n# 更新 MonsterAIController\n\n现在，您将为您的`AIController`类添加更多功能，以支持行为树:\n\n1.  你的新`MonsterAIController.h`应该是这样的:\n\n```cpp\nUCLASS()\nclass GOLDENEGG_API AMonsterAIController : public AAIController\n{\n    GENERATED_BODY()\n\npublic:\n    AMonsterAIController(const FObjectInitializer& ObjectInitializer);\n\n    virtual void Possess(class APawn* InPawn) override;\n\n    virtual void UnPossess() override;\n\n    UBehaviorTreeComponent* BehaviorTreeCmp;\n\n    UBlackboardComponent* BlackboardCmp;\n\n    //Start following the player\n    void StartFollowingPlayer();\n    void SetFollowRange(bool val);\n    void SetAttackRange(bool val);\n};\n```\n\n还要确保在文件顶部添加`#include \"BehaviorTree/BehaviorTreeComponent.h\"`。在这里，您将覆盖构造函数以及`Possess`和`UnPossess`类。`SetFollowRange`和`SetAttackRange`功能是新的，允许您设置黑板值。\n\n2.  在`MonsterAIController.cpp`增加以下功能:\n\n```cpp\nAMonsterAIController::AMonsterAIController(const class FObjectInitializer& ObjectInitializer)\n    : Super(ObjectInitializer)\n{\n    BehaviorTreeCmp = ObjectInitializer.CreateDefaultSubobject<UBehaviorTreeComponent>(this, TEXT(\"MonsterBT\"));\n    BlackboardCmp = ObjectInitializer.CreateDefaultSubobject<UBlackboardComponent>(this, TEXT(\"MonsterBlackboard\"));\n}\n\nvoid AMonsterAIController::Possess(class APawn* InPawn)\n{\n    Super::Possess(InPawn);\n\n    AMonster* Monster = Cast<AMonster>(InPawn);\n    if (Monster)\n    {\n        if (Monster->BehaviorTree->BlackboardAsset)\n        {\n            BlackboardCmp->InitializeBlackboard(*Monster->BehaviorTree->BlackboardAsset);\n        }\n\n        BehaviorTreeCmp->StartTree(*Monster->BehaviorTree);\n    }\n}\n\nvoid AMonsterAIController::UnPossess()\n{\n    Super::UnPossess();\n\n    BehaviorTreeCmp->StopTree();\n}\n\nvoid AMonsterAIController::SetFollowRange(bool val)\n{\n    BlackboardCmp->SetValueAsBool(\"IsInFollowRange\", val);\n}\n\nvoid AMonsterAIController::SetAttackRange(bool val)\n{\n    BlackboardCmp->SetValueAsBool(\"IsInAttackRange\", val);\n}\n```\n\n另外，在文件顶部添加以下几行:\n\n```cpp\n#include \"Monster.h\"\n#include \"BehaviorTree/BehaviorTree.h\"\n#include \"BehaviorTree/BlackboardComponent.h\"\n```\n\n`StartFollowingPlayer`保持不变，所以这里没有列出，但一定要把它留在那里！现在是时候更新你的`Monster`类了(在你这样做之前你无法编译)。\n\n# 更新怪物类\n\n我们将在`Monster`课程中进行以下更新:\n\n*   在`Monster.h`中，您将进行的唯一更改是添加以下代码行:\n\n```cpp\n    UPROPERTY(EditDefaultsOnly, Category = \"AI\")\n        class UBehaviorTree* BehaviorTree;\n```\n\n*   在`Monster.cpp`中，您将对`Tick()`功能进行一些重大更改，因此这里是完整版本:\n\n```cpp\n// Called every frame\nvoid AMonster::Tick(float DeltaSeconds)\n{\n    Super::Tick(DeltaSeconds);\n\n    // move the monster towards the player \n    AAvatar *avatar = Cast<AAvatar>(\n        UGameplayStatics::GetPlayerPawn(GetWorld(), 0));\n    if (!avatar) return;\n\n    FVector playerPos = avatar->GetActorLocation();\n    FVector toPlayer = playerPos - GetActorLocation();\n    float distanceToPlayer = toPlayer.Size();\n    AMonsterAIController* controller = Cast<AMonsterAIController>(GetController());\n\n    // If the player is not the SightSphere of the monster, \n    // go back \n    if (distanceToPlayer > SightSphere->GetScaledSphereRadius())\n    {\n        // If the player is OS, then the enemy cannot chase \n        if (controller != nullptr)\n        {\n            controller->SetAttackRange(false);\n            controller->SetFollowRange(false);\n        }\n        return;\n    }\n\n    toPlayer /= distanceToPlayer;  // normalizes the vector \n\n                                   // At least face the target \n                                   // Gets you the rotator to turn something \n                                   // that looks in the `toPlayer` direction \n    FRotator toPlayerRotation = toPlayer.Rotation();\n    toPlayerRotation.Pitch = 0; // 0 off the pitch \n    RootComponent->SetWorldRotation(toPlayerRotation);\n\n    if (isInAttackRange(distanceToPlayer))\n    {\n        if (controller != nullptr)\n        {\n            controller->SetAttackRange(true);\n        }\n\n        // Perform the attack \n        if (!TimeSinceLastStrike)\n        {\n            Attack(avatar);\n        }\n\n        TimeSinceLastStrike += DeltaSeconds;\n        if (TimeSinceLastStrike > AttackTimeout)\n        {\n            TimeSinceLastStrike = 0;\n        }\n\n        return;  // nothing else to do \n    }\n    else\n    {\n        // not in attack range, so walk towards player \n        //AddMovementInput(toPlayer, Speed*DeltaSeconds);\n\n        if (controller != nullptr)\n        {\n            controller->SetAttackRange(false);\n            controller->SetFollowRange(true);\n        }\n    }\n}\n```\n\nThe changes are to set the values for both the attack and follow ranges. The code for attacking is still in there, but if you move TimeSinceLastStrike and AttackTimeout into the Blackboard, you can use that to move all that functionality into a `BTTask`. Now make sure everything compiles.\n\n*   一旦编译完成，你需要打开`BP_Monster`蓝图，这样设置行为树(如果你想让单个怪物不一样，也可以在单个怪物上设置):\n\n![](img/c3c7768e-704d-47e1-a96d-31ae6e04bce5.png)\n\n还要确保人工智能控制器设置为 MonsterAIController。如果你在这一点上运行游戏，功能应该是相同的，但是行为树将控制玩家的跟随。\n\n如果你想了解更多，考虑将`Attack`代码移入`BTTask`类，并研究当你在范围之外的时候怪物能做什么(阅读下一节中可能有帮助的内容)。\n\n# 环境查询系统\n\n**环境查询系统** ( **EQS** )是新的，仍处于试验阶段。它们允许您在行为树中创建一个查询，以便在一个级别中搜索项目，并找到最符合您设置的条件的项目。也许你想让你的怪物在你设定的路标之间游荡，而不是在玩家不在范围内时站着不动。您可以设置一个查询来查找最接近的查询，或者使用一些其他条件。EQS 允许你这么做。\n\n您需要在设置中启用此功能才能使用它们。为此，请执行以下步骤:\n\n1.  转到编辑|编辑器首选项:\n\n![](img/d6395ceb-67fc-4d78-84d0-f29c8e2ee2c5.png)\n\n2.  在实验|人工智能下，勾选环境查询系统:\n\n![](img/e30ed859-293d-4372-8471-864a91f53b5e.png)\n\n3.  通过转到添加新项|人工智能来添加新查询。环境查询现在将出现在行为树和黑板下:\n\n![](img/18186d59-76d5-4cf2-bf62-995a0863e37a.png)\n\n你还需要在蓝图中创建一个`Context`和一个`Generator`(T2 将获得特定类型的所有物品，例如航路点)。要实际运行查询，您需要在行为树中创建一个“运行 EQS 查询”任务节点。有关环境查询系统如何工作的更多信息，请参见的虚幻文档。\n\n# 棉屑\n\n如果你有很多怪物在屏幕上同时移动，你会希望他们以一种看起来真实的方式移动。你不希望他们走到一起或者朝着不同的方向走。\n\n人工智能研究人员对此进行了研究，并提出了一些算法来现实地处理这一问题。它们被称为群集算法，因为它们基于一群鸟的行为。\n\n当一起行动时，怪物们必须考虑的不仅仅是到达同一个目标。他们还必须考虑到和他们一起移动的怪物。他们必须确保不要离周围的怪物太近，也不要离得太远，否则它们会散开。\n\n在许多情况下，有一个怪物被选为领导者。那个怪物朝目标前进，其他人则专注于跟随那个首领。\n\n网上有很多关于蜂拥的好参考。它没有内置在 UE4 中，但你可以购买扩展或编程自己的植绒系统。\n\n# 机器学习和神经网络导论\n\n机器学习和神经网络是一个巨大的话题，所以我在这里只做一个简单的介绍。机器学习是你如何教一个程序弄清楚如何对某件事做出反应，而不仅仅是给它制定规则。有许多不同的算法可以做到这一点，但它们都需要大量的样本数据。\n\n基本上，你给学习程序大量的示例案例(越多越好)，*和*每个案例的结果最好。你可以用不同的方式给他们打分。通过查看这么多案例，它可以根据过去看到的结果对类似案例做出最佳猜测。有了足够的训练数据，结果可能会非常好，尽管你仍然会遇到它不能很好地工作的情况。\n\n由于这需要如此多的数据(更不用说处理能力)，除了极少数情况，这是由游戏公司在游戏发货前完成的(如果真的完成了——这种事情往往会被削减以利于截止日期)。培训是离线进行的，程序已经学会了怎么做。\n\n神经网络是一种特殊类型的机器学习，用来模拟大脑处理数据的方式。有些节点像神经元一样工作。可以有多层节点，每层处理前一层的结果。\n\n数据跨多个节点发送，每个节点根据某个阈值量调整数据。只有数据可以传递回(或转发到)节点，然后节点调整这些阈值，以获得更准确的训练数据结果。一旦训练好了，这些阈值就可以用于未来的决策。\n\n虽然我们离制造真正的人工智能还有很长的路要走，但神经网络已经被用于产生有趣的结果。神经网络已经在特定类型的音乐上进行了训练，然后生成了非常令人印象深刻(和原创)的音乐，听起来与训练它的类型相似。我也听说过有人写神经网络试图写书。不过，我认为我们距离能够编写 UE4 程序的神经网络还有很长的路要走！\n\n# 遗传算法\n\n回忆你的高中生物；你可能学过遗传学。来自两个不同父母的染色体结合在一起，创造了一个结合了父母双方 DNA 的孩子，随机的基因突变也可以带来改变。遗传算法基于同样的原理。\n\n就像达尔文的适者生存一样，你可以在代码中做一些非常相似的事情。遗传算法有三个基本原则:\n\n*   **选择**:你挑成绩最好的例子，那些是下一代的基础。\n*   **交叉**:然后将这两个选定的例子结合起来，创造一个孩子，这是两者的产物，就像在生物学中一样。\n*   **引入随机基因突变**:可能有一些老的没有的好的性状，或者因为那些性状被其他性状压倒而被丢弃。这意味着你不会因为一些潜在的伟大特征不在原始群体中而错过它们。\n\n# 摘要\n\n正如你所看到的，人工智能是一个巨大的话题，我们这里只涉及到基础知识。我们已经复习了寻路的基础知识(使用导航网格)、行为树、环境查询系统、群集、机器学习和神经网络以及遗传算法。如果你想了解更多，这里有整本书，还有很多网站，比如[http://aigamedev.com/](http://aigamedev.com/)，还有关于[https://www.gamasutra.com](https://www.gamasutra.com)的文章。\n\n在下一节中，我们将学习施法来保护你的玩家免受怪物的伤害。"
  },
  {
    "path": "docs/learn-cpp-build-game-ue4/13.md",
    "content": "# 十三、咒语书\n\n这名球员还没有办法为自己辩护。我们现在将为玩家提供一种非常有用和有趣的方法，叫做魔法咒语。魔法法术将被玩家用来影响附近的怪物，所以你现在可以伤害他们。\n\n我们将从描述如何创建我们自己的粒子系统开始这一章。然后，我们将继续把粒子发射器包装成一个`Spell`类，并为化身编写一个`CastSpell`函数，以便能够真正地`CastSpells`。\n\n本章将涵盖以下主题:\n\n*   什么是咒语？\n*   粒子系统\n*   法术类演员\n*   将鼠标右键单击附加到 CastSpell\n*   创造其他法术\n\n# 什么是咒语？\n\n实际上，法术将是一个粒子系统和一个由包围体表示的效果区域的组合。检查每个帧中包含的演员的边界体积。当一个演员在一个法术的范围内，那么这个演员就会受到这个法术的影响。\n\n下面是暴雪法术的截图，包围体用橙色突出显示:\n\n![](img/d502f3b7-aa44-4c5a-a35c-e7b0580c894d.png)\n\n暴风雪法术有一个长长的、盒状的包围体。在每一帧中，检查包围体中包含的演员。包含在该法术的包围体中的任何演员将只在该帧中受到该法术的影响。如果演员移动到该法术的包围体之外，演员将不再受到该法术的影响。记住，法术的粒子系统只是一个可视化；粒子本身并不是影响游戏演员的因素。\n\n我们在[第 8 章](08.html)、*演员和棋子*中创建的`PickupItem`职业可以让玩家拾取代表法术的物品。我们将扩展`PickupItem`职业，并附上一个法术蓝图来施放每个`PickupItem`。从平视显示器点击一个法术的小部件将会施放它。界面如下所示:\n\n![](img/9ac617a8-92ec-4798-b090-495b3fcb6524.png)\n\n# 设置粒子系统\n\n首先，我们需要一个地方来放置我们所有的时髦效果。为此，我们将遵循以下步骤:\n\n1.  在内容浏览器选项卡中，右键单击内容根目录，并创建一个名为`ParticleSystems`的新文件夹。\n2.  右键单击该新文件夹，并选择“新资产|粒子系统”，如下图所示:\n\n![](img/8dda3c34-0393-4b68-b568-3a3b14626a26.png)\n\nSee this Unreal Engine 4 particle systems guide for information on how Unreal particle emitters work: [https://www.youtube.com/watch?v=OXK2Xbd7D9w&amp;index=1&amp;list=PLZlv_N0_O1gYDLyB3LVfjYIcbBe8NqR8t](https://www.youtube.com/watch?v=OXK2Xbd7D9w&index=1&list=PLZlv_N0_O1gYDLyB3LVfjYIcbBe8NqR8t).\n\n3.  双击出现的新粒子系统图标，如下图所示:\n\n![](img/17c681be-20b8-4b94-8296-407dd2f2fda4.png)\n\n完成上述步骤后，您将进入粒子编辑器“级联”。环境如下图所示:\n\n![](img/173d7371-cbb2-419b-8ed1-863f44e04ebb.png)\n\n这里有几个不同的窗格，每个窗格显示不同的信息。它们如下:\n\n*   左上角是“视口”窗格。这显示了当前发射器工作时的动画。\n*   右侧是“发射器”面板。在其中，您可以看到一个名为“粒子发射器”的单个对象(您的粒子系统中可以有多个发射器，但我们现在不想要它)。“粒子发射器”的模块列表显示在它下面。从前面的截图中，我们有了必需、产卵、生命周期、初始大小、初始速度和颜色生命周期模块。\n\n# 改变粒子属性\n\n默认粒子发射器发射类似十字准线的形状。我们想把它变成更有趣的东西。为此，请遵循以下步骤:\n\n1.  单击发射器面板下的黄色“必需”框，然后在“详细信息”面板中打开“材质”下拉列表。\n\n将弹出所有可用粒子材料的列表(您可以在顶部键入`particles`，以便更容易找到您想要的材料)。\n\n2.  选择 m_flare_01 选项来创建我们的第一个粒子系统，如下图所示:\n\n![](img/ac976fd3-6e9a-4358-8b5a-142353c6dba7.png)\n\n3.  现在，让我们改变粒子系统的行为。单击发射器窗格下的“终生颜色”条目。底部的详细信息窗格显示了不同参数的信息，如下图所示:\n\n![](img/4c64ae04-cd1d-4801-a952-deee7b5ae84d.png)\n\n4.  在“生命色彩”条目的“细节”面板中，我增加了 R，但没有增加 G，也没有增加 b。这给了粒子系统一种红色的光。(R 为红色，G 为绿色，B 为蓝色)。你可以看到吧台上的颜色。\n\n然而，您实际上可以更直观地更改粒子颜色，而不是编辑原始数字。如果您单击发射器下“寿命期内颜色”条目旁边的绿色之字形按钮，您将看到“曲线编辑器”选项卡中显示的寿命期内颜色图表，如下图所示:\n\n![](img/cf931d8b-84fa-44fb-80fa-e72bc65ea783.png)\n\n我们现在可以更改色彩寿命参数。曲线编辑器选项卡中的图形显示发射的颜色与粒子存活时间的关系。您可以通过拖动周围的点来调整值。按下 *Ctrl* +鼠标左键为一条线添加一个新的点(如果不起作用，请在黄色框中单击取消选择 AlphaOverLife，并确保仅选择 ColorOverLife):\n\n![](img/6c416672-9b92-4de2-b9ef-882ea4895335.png)\n\n您可以使用粒子发射器设置来创建自己的法术可视化效果。\n\n# 暴雪咒语的设置\n\n在这一点上，我们应该将我们的粒子系统从新粒子系统重命名为更具描述性的系统。我们把它重新命名为`P_Blizzard`。\n\n![](img/422079ca-06d0-49ff-b57f-59b93b200da7.png)\n\n只需点击粒子系统并按下 *F2，即可重命名粒子系统，如下图*:\n\n![](img/adcfef39-8761-4855-be26-c1884276844b.png)\n\n我们将调整一些设置来获得暴雪粒子效果法术。请执行以下步骤:\n\n1.  回到 P _ 暴雪粒子系统进行编辑。\n2.  在产卵模块下，将产卵率更改为`200.0`。这增加了可视化的密度，如下所示:\n\n![](img/d29f563b-55a5-4525-8a58-bad64b23d627.png)\n\n3.  在寿命模块下，将 Max 属性从`1.0`增加到`2.0`，如下图截图所示。这给粒子的寿命带来了一些变化，一些发射粒子的寿命比其他粒子长:\n\n![](img/3fa2b655-864f-4888-b124-d7d2bebba71b.png)\n\n4.  在“初始大小”模块下，将 X、Y 和 Z 中的“最小”属性大小更改为`12.5`，如下图所示:\n\n![](img/67d82c41-cf10-43b4-b50f-1e7931a51416.png)\n\n5.  在初始速度模块下，将最小值/最大值更改为此处显示的值:\n\n![](img/5b14c94b-a895-4f16-a7c6-f1e890175a0f.png)\n\n6.  我们让暴雪吹进+X 的原因是因为玩家的前进方向是从+X 开始的，因为法术会来自玩家的手，所以我们希望法术指向和玩家相同的方向。\n7.  在“色彩寿命”菜单下，将蓝色(B)值更改为`100.0`。也把 R 改回`1.0`。您将看到蓝色辉光的瞬间变化:\n\n![](img/278ad147-cb00-4705-890f-4c0866c07900.png)\n\n现在开始看起来神奇了！\n\n8.  右键单击生命周期颜色模块下方的黑色区域。选择位置|初始位置，如屏幕截图所示:\n\n![](img/bcf639ba-60da-462f-b93c-b2bb17afbeeb.png)\n\n9.  在起始位置|分布下输入值，如下图所示:\n\n![](img/be1b38a4-0f7d-436f-8ca9-39f1822e792d.png)\n\n10.  你应该有一场像这样的暴风雪:\n\n![](img/3b25a958-cb24-40c4-b82b-98475a219166.png)\n\n11.  将相机移动到您喜欢的位置，然后单击顶部菜单栏中的缩略图选项。这将在内容浏览器选项卡中为粒子系统生成缩略图图标，如下图所示:\n\n![](img/a422433a-ec14-4068-af06-32445aa0ad80.png)\n\n# 法术类演员\n\n`Spell`职业最终会对所有怪物造成伤害。为此，我们需要在`Spell`类参与者中包含粒子系统和边界框。当化身施放一个`Spell`类时，`Spell`对象将被实例化到该级别并开始`Tick()`功能。在`Spell`对象的每个`Tick()`上，包含在该法术的包围体中的任何怪物都将受到该`Spell`的影响。\n\n`Spell`类应该类似于下面的代码:\n\n```cpp\n#include \"CoreMinimal.h\"\n#include \"GameFramework/Actor.h\"\n#include \"Components/BoxComponent.h\"\n#include \"Runtime/Engine/Classes/Particles/ParticleSystemComponent.h\"\n#include \"Spell.generated.h\"\n\nUCLASS()\nclass GOLDENEGG_API ASpell : public AActor\n{\n    GENERATED_BODY()\n\npublic:    \n    ASpell(const FObjectInitializer&amp; ObjectInitializer);\n\n    // box defining volume of damage \n    UPROPERTY(VisibleDefaultsOnly, BlueprintReadOnly, Category =\n        Spell)\n        UBoxComponent* ProxBox;\n\n    // the particle visualization of the spell \n    UPROPERTY(VisibleDefaultsOnly, BlueprintReadOnly, Category =\n        Spell)\n        UParticleSystemComponent* Particles;\n\n    // How much damage the spell does per second \n    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Spell)\n        float DamagePerSecond;\n\n    // How long the spell lasts \n    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Spell)\n        float Duration;\n\n    // Length of time the spell has been alive in the level \n    float TimeAlive;\n\n    // The original caster of the spell (so player doesn't \n    // hit self) \n    AActor* Caster;\n\n    // Parents this spell to a caster actor \n    void SetCaster(AActor* caster);\n\n    // Runs each frame. override the Tick function to deal damage  \n    // to anything in ProxBox each frame. \n    virtual void Tick(float DeltaSeconds) override;\n};\n```\n\n我们只需要担心实现三个函数，即`ASpell::ASpell()`构造函数、`ASpell::SetCaster()`函数和`ASpell::Tick()`函数。\n\n打开`Spell.cpp`文件。在`Spell.h`的 include 行下面，添加一行来包含`Monster.h`文件，这样我们就可以访问`Spell.cpp`文件中`Monster`对象的定义(以及其他几个 include)，如下面一行代码所示:\n\n```cpp\n#include \"Monster.h\" \n#include \"Kismet/GameplayStatics.h\"\n#include \"Components/CapsuleComponent.h\"\n```\n\n首先，下面的代码显示了构造函数，它设置了拼写并初始化了所有组件:\n\n```cpp\nASpell::ASpell(const FObjectInitializer&amp; ObjectInitializer)\n : Super(ObjectInitializer)\n{\n ProxBox = ObjectInitializer.CreateDefaultSubobject<UBoxComponent>(this,\n TEXT(\"ProxBox\")); \n Particles = ObjectInitializer.CreateDefaultSubobject<UParticleSystemComponent>(this,\n TEXT(\"ParticleSystem\"));\n\n // The Particles are the root component, and the ProxBox \n // is a child of the Particle system. \n // If it were the other way around, scaling the ProxBox \n // would also scale the Particles, which we don't want \n RootComponent = Particles;\n ProxBox->AttachToComponent(RootComponent, FAttachmentTransformRules::KeepWorldTransform);\n\n Duration = 3;\n DamagePerSecond = 1;\n TimeAlive = 0;\n\n PrimaryActorTick.bCanEverTick = true;//required for spells to \n // tick! \n}\n```\n\n特别重要的是这里的最后一行`PrimaryActorTick.bCanEverTick = true`。如果不设置，您的`Spell`对象将永远不会有`Tick()`调用。\n\n接下来，我们有`SetCaster()`方法。这样叫是为了让`Spell`对象知道施咒的人。我们可以通过使用以下代码来确保施法者不会用自己的法术伤害自己:\n\n```cpp\nvoid ASpell::SetCaster(AActor *caster)\n{\n Caster = caster;\n RootComponent->AttachToComponent(caster->GetRootComponent(), FAttachmentTransformRules::KeepRelativeTransform);\n}\n```\n\n最后，我们有`ASpell::Tick()`方法，它实际上对所有包含的参与者造成伤害，如下面的代码所示:\n\n```cpp\nvoid ASpell::Tick(float DeltaSeconds)\n{\n    Super::Tick(DeltaSeconds);\n\n    // search the proxbox for all actors in the volume. \n    TArray<AActor*> actors;\n    ProxBox->GetOverlappingActors(actors);\n\n    // damage each actor the box overlaps \n    for (int c = 0; c < actors.Num(); c++)\n    {\n        // don't damage the spell caster \n        if (actors[c] != Caster)\n        {\n            // Only apply the damage if the box is overlapping \n            // the actors ROOT component. \n            // This way damage doesn't get applied for simply  \n            // overlapping the SightSphere of a monster \n            AMonster *monster = Cast<AMonster>(actors[c]);\n\n            if (monster &amp;&amp; ProxBox->IsOverlappingComponent(Cast<UPrimitiveComponent>(monster->GetCapsuleComponent())))\n            {\n                monster->TakeDamage(DamagePerSecond*DeltaSeconds,\n                    FDamageEvent(), 0, this);\n            }\n\n            // to damage other class types, try a checked cast  \n            // here.. \n        }\n    }\n\n    TimeAlive += DeltaSeconds;\n    if (TimeAlive > Duration)\n    {\n        Destroy();\n    }\n}\n```\n\n`ASpell::Tick()`功能有很多功能，如下所示:\n\n*   它让所有演员重叠`ProxBox`。如果重叠的组件是该对象的根组件，任何不是施法者的角色都会受到伤害。我们必须检查与根组件重叠的原因是，如果我们不这样做，法术可能会与怪物的`SightSphere`重叠，这意味着我们将从很远的地方获得命中，这是我们不想要的。\n*   请注意，如果我们有另一类应该被损坏的东西，我们将不得不尝试对每种对象类型进行强制转换。每个类类型可能有一个不同类型的边界体积应该被碰撞；其他类型甚至可能没有`CapsuleComponent`(他们可能有`ProxBox`或`ProxSphere`)。\n*   它增加了法术存活的时间。如果该法术超过了分配的施法持续时间，它将从该等级中移除。\n\n现在，让我们专注于玩家如何获得法术，为玩家可以拾取的每个法术对象创建一个单独的`PickupItem`。\n\n# 蓝印我们的咒语\n\n用我们刚刚添加的`Spell`类编译并运行你的 C++ 项目。我们需要为我们想要施展的每一个法术创建蓝图。为此，请遵循以下步骤:\n\n1.  在类查看器标签中，开始输入`Spell`，你会看到你的法术类出现\n2.  右击法术，创建一个名为 BP _ 法术 _ 暴雪的蓝图，如下图截图所示:\n\n![](img/d2c44941-90e5-47c0-bad6-063fd84cab08.png)\n\n3.  如果它没有自动打开，请双击打开它。\n4.  在法术属性中，为粒子发射器选择 P _ 暴雪法术，如下图所示:\n\n![](img/f1ce427a-570e-4bc1-98b5-2ee9c87c8efa.png)\n\nIf you can't find it, try selecting Particles (Inherited) under Components.\n\n选择 BP_SpellBlizzard(自)后，向下滚动，直到到达“法术”类别，然后将“每秒伤害”和“持续时间”参数更新为您喜欢的值，如下图所示。这里，暴雪法术持续`3.0`秒，每秒造成`16.0`伤害。三秒钟后，暴风雪会消失:\n\n![](img/592a8226-92ac-4099-a193-662622fa24c8.png)\n\n配置默认属性后，切换到组件选项卡进行进一步修改。点击并改变`ProxBox`的形状，使其形状有意义。盒子应该包裹粒子系统最强烈的部分，但不要忘了扩大它的尺寸。`ProxBox`物体不应该太大，因为那样你的暴雪法术会影响到那些甚至没有被暴雪接触到的东西。如下图所示，一些异常值是可以的:\n\n![](img/cbf2312a-6a5d-4de4-8ac3-ead61614357c.png)\n\n你的暴雪法术现在是蓝印的，可以被玩家使用了。\n\n# 拾起咒语\n\n回想一下，我们之前对库存进行了编程，当用户按下 *I* 时，会显示玩家的领取物品数量。然而，我们想做的不止这些:\n\n![](img/0d443ba0-5008-4ed8-a000-1cfbb0a04eae.png)\n\nItems displayed when the user presses I\n\n为了让玩家获得法术，我们将修改`PickupItem`类，通过使用以下代码为玩家施放的法术蓝图加入一个槽:\n\n```cpp\n// inside class APickupItem: \n// If this item casts a spell when used, set it here \nUPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Item) \nUClass* Spell;\n```\n\n一旦将`UClass* Spell`属性添加到`APickupItem`类中，重新编译并重新运行您的 C++ 项目。现在，您可以继续为您的`Spell`对象制作`PickupItem`实例的蓝图。\n\n# 为施法的拾取物品创建蓝图\n\n创建一个名为 BP _ pick _ 法术 _ 暴雪的 PickupItem 蓝图，如下图截图所示:\n\n![](img/c858650a-7e6b-496c-9855-2027dc4dd31f.png)\n\n它应该会自动打开，以便您可以编辑其属性。我将暴雪物品的拾取属性设置如下:\n\n物品名称为暴雪法术，`5`在每个包裹中。我拍摄了一张暴雪粒子系统的截图，并将其导入到项目中，因此图标被选为该图像。在法术下，我选择了 BP _ 法术 _ 暴雪作为要施放的法术名称(不是 BP _ 皮卡 _ 法术 _ 暴雪)，如下图截图所示:\n\n![](img/0ef88141-1364-4dea-9d2c-904abd726f13.png)\n\n我为`PickupItem`类的`Mesh`类选择了一个蓝色的球体(使用 M_Water_Lake 材质也可以得到一个有趣的)。对于 Icon，我在粒子查看器预览中拍摄了暴雪法术的截图，保存到磁盘，并将该图像导入到项目中，如下图所示(参见示例项目的内容浏览器选项卡中的`images`文件夹):\n\n![](img/612d25a5-142b-45fb-ba60-f7cbfd0f501f.png)\n\n在你的关卡中放置一些这样的`PickupItem`。如果我们拿起它们，我们的库存中会有一些暴雪法术(如果你不能拿起它们，确保你把 ProxSphere 做得足够大):\n\n![](img/badddff5-2324-4dc4-9320-1957be5bf452.png)\n\n现在，我们需要激活暴雪。既然我们已经在[第 10 章](10.html)、*库存系统和拾取物品*中附加了鼠标左键，为了拖动图标，让我们附加鼠标右键来施法。\n\n# 将鼠标右键单击附加到 CastSpell\n\n鼠标右键在调用头像的`CastSpell`方法之前，需要经历相当多的函数调用。调用图看起来像下面的截图:\n\n![](img/3946b2e1-8cbb-40fe-b7aa-556f61174f31.png)\n\n右击和施法之间会发生一些事情。它们如下:\n\n*   正如我们之前看到的，所有用户鼠标和键盘的交互都是通过`Avatar`对象进行的。当`Avatar`对象检测到右键点击时，会通过`AAvatar::MouseRightClicked()`将点击事件传递给`HUD`。\n*   在[第 10 章](10.html)、*库存系统和拾取物品*中，我们使用了`struct Widget`类来记录玩家拾取的物品。`struct Widget`只有三个成员:\n\n```cpp\nstruct Widget \n{ \n  Icon icon; \n  FVector2D pos, size; \n  ///.. and some member functions \n}; \n```\n\n*   我们现在需要为`struct Widget`类增加一个额外的属性来记住它所施放的法术。\n*   `HUD`将确定点击事件是否在`AMyHUD::MouseRightClicked()`的`Widget`内。\n\n*   如果点击的是施放法术的`Widget`，那么`HUD`通过召唤`AAvatar::CastSpell()`来召唤化身并请求施放该法术。\n\n# 写头像的施法功能\n\n我们将反向实现前面的调用图。我们将从编写游戏中实际施法的函数`AAvatar::CastSpell()`开始，如下面的代码所示:\n\n```cpp\nvoid AAvatar::CastSpell( UClass* bpSpell ) \n{ \n  // instantiate the spell and attach to character \n  ASpell *spell = GetWorld()->SpawnActor<ASpell>(bpSpell,  \n   FVector(0), FRotator(0) ); \n\n  if( spell ) \n  { \n    spell->SetCaster( this ); \n  } \n  else \n  { \n    GEngine->AddOnScreenDebugMessage( 1, 5.f, FColor::Yellow,  \n    FString(\"can't cast \") + bpSpell->GetName() ); } \n} \n```\n\n还要确保将功能添加到`Avatar.h`并将`#include \"Spell.h\"`添加到该文件的顶部。\n\n你可能会发现，实际上召唤一个咒语非常简单。施法有两个基本步骤:\n\n1.  使用世界对象的`SpawnActor`功能实例化法术对象\n2.  将其附加到头像上\n\n一旦`Spell`对象被实例化，当该法术处于该等级时，其`Tick()`功能将运行每一帧。在每一个`Tick()`上，`Spell`物体会自动感应出关卡中的怪物并对其造成伤害。前面提到的每一行代码都会发生很多事情，所以让我们分别讨论每一行。\n\n# 实例化拼写–GetWorld()-> SpawnActor()\n\n要从蓝图创建`Spell`对象，我们需要从`World`对象调用`SpawnActor()`函数。`SpawnActor()`函数可以采用任何蓝图，并在级别内实例化。幸运的是，`Avatar`对象(实际上是任何`Actor`对象)只需调用`GetWorld()`成员函数，就可以随时获得`World`对象的句柄。\n\n将`Spell`对象带入该级别的代码行如下:\n\n```cpp\nASpell *spell = GetWorld()->SpawnActor<ASpell>( bpSpell,  \n   FVector(0), FRotator(0) );\n```\n\n关于前一行代码，有几点需要注意:\n\n*   `bpSpell`必须是一个`Spell`对象要创建的蓝图。尖括号中的`<ASpell>`表示期望。\n*   新的`Spell`对象从原点(`0`、`0`、`0`)开始，并且没有对其应用额外的旋转。这是因为我们将把`Spell`对象附加到`Avatar`对象，这将为`Spell`对象提供平移和方向组件。\n\n# if(拼写)\n\n我们总是通过检查`if( spell )`来测试对`SpawnActor<ASpell>()`的调用是否成功。如果传递给`CastSpell`对象的蓝图实际上不是基于`ASpell`类的蓝图，那么`SpawnActor()`函数返回一个`NULL`指针，而不是一个`Spell`对象。如果发生这种情况，我们会在屏幕上打印一条错误消息，指出在施法过程中出现了问题。\n\n# 法术->设定施法者(这个)\n\n实例化时，如果法术成功，我们通过调用`spell->SetCaster( this )`将法术附加到`Avatar`对象上。请记住，在`Avatar`类的编程上下文中，`this`方法是对`Avatar`对象的引用。\n\n现在，我们实际上如何从 UI 输入中连接施法，首先调用`AAvatar::CastSpell()`函数？我们需要再做一些`HUD`编程。\n\n# 正在编写 AMyHUD::MouseRightClicked()\n\n施法命令最终将来自平视显示器。我们需要编写一个 C++ 函数，该函数将遍历所有的 HUD 小部件并进行测试，看看是否有任何一个部件被点击。如果点击是在一个`widget`对象上，那么该`widget`对象应该通过施法来回应，如果它已经被分配了咒语的话。\n\n我们必须扩展我们的`Widget`对象，使其有一个变量来保存要施放的法术的蓝图。使用以下代码向您的`struct Widget`对象添加成员:\n\n```cpp\nstruct Widget\n{\n    Icon icon;\n    // bpSpell is the blueprint of the spell this widget casts \n    UClass *bpSpell;\n    FVector2D pos, size;\n    //...\n};\n```\n\n现在，回想一下，我们的`PickupItem`之前已经附上了它施放的法术蓝图。但是当`PickupItem`类被玩家从关卡中拾取时，那么`PickupItem`类就被破坏了，如下代码所示:\n\n```cpp\n// From APickupItem::Prox_Implementation(): \navatar->Pickup( this ); // give this item to the avatar \n// delete the pickup item from the level once it is picked up \nDestroy(); \n```\n\n所以，我们需要保留每个`PickupItem`施放什么法术的信息。当第一次拿起`PickupItem`时，我们可以这样做。\n\n在`AAvatar`类中，添加一个额外的地图来记住物品施放的法术蓝图，按照物品名称，用下面一行代码:\n\n```cpp\n// Put this in Avatar.h \nTMap<FString, UClass*> Spells; \n```\n\n现在，在`AAvatar::Pickup()`中，记住`PickupItem`类用下面一行代码实例化的拼写类:\n\n```cpp\n// the spell associated with the item \nSpells.Add(item->Name, item->Spell); \n```\n\n现在，在`AAvatar::ToggleInventory()`中，我们可以拥有在屏幕上显示的`Widget`对象。通过查看`Spells`地图，记住它应该使用什么法术。\n\n找到我们创建小部件的那一行，并对其进行修改，以添加`Widget`强制转换的`bpSpell`对象的赋值，如以下代码所示:\n\n```cpp\n// In AAvatar::ToggleInventory() \nWidget w(Icon(fs, tex));\nw.bpSpell = Spells[it->Key];\nhud->addWidget(w);\n```\n\n在`AMyHUD`中增加以下功能，我们将设置为每当鼠标右键点击图标时运行:\n\n```cpp\nvoid AMyHUD::MouseRightClicked()\n{\n    FVector2D mouse;\n    APlayerController *PController = GetWorld()->GetFirstPlayerController();\n    PController->GetMousePosition(mouse.X, mouse.Y);\n    for (int c = 0; c < widgets.Num(); c++)\n    {\n        if (widgets[c].hit(mouse))\n        {\n            AAvatar *avatar = Cast<AAvatar>(\n                UGameplayStatics::GetPlayerPawn(GetWorld(), 0));\n            if (widgets[c].bpSpell)\n                avatar->CastSpell(widgets[c].bpSpell);\n        }\n    }\n}\n```\n\n这与我们的鼠标左键点击功能非常相似。我们只需对照所有小部件检查点击位置。如果任何`Widget`被右键点击，并且该`Widget`有一个`Spell`对象与之相关联，那么将通过调用头像的`CastSpell()`方法来施法。\n\n# 激活鼠标右键单击\n\n要连接这个 HUD 功能运行，我们需要在鼠标右键上附加一个事件处理程序。我们可以通过执行以下步骤来做到这一点:\n\n1.  转到设置|项目设置；弹出对话框\n\n2.  在引擎-输入下，为鼠标右键添加一个动作映射，如下图所示:\n\n![](img/222a2cf3-be13-4d8d-a73c-df854a0550b4.png)\n\n3.  在`Avatar.h` / `Avatar.cpp`中声明一个名为`MouseRightClicked()`的函数，代码如下:\n\n```cpp\nvoid AAvatar::MouseRightClicked() \n{ \n  if( inventoryShowing ) \n  { \n    APlayerController* PController = GetWorld()- \n     >GetFirstPlayerController(); \n    AMyHUD* hud = Cast<AMyHUD>( PController->GetHUD() ); \n    hud->MouseRightClicked(); \n  } \n}\n```\n\n4.  然后，在`AAvatar::SetupPlayerInputComponent()`中，我们应该将`MouseClickedRMB`事件附加到那个`MouseRightClicked()`函数:\n\n```cpp\n// In AAvatar::SetupPlayerInputComponent(): \nPlayerInputComponent->BindAction(\"MouseClickedRMB\", IE_Pressed, this,\n        &amp;AAvatar::MouseRightClicked);\n```\n\n我们终于接上了施法。试试看；游戏的玩法非常酷，如下图所示:\n\n![](img/28b4e241-832c-405a-85d7-b66336ffac8d.png)\n\n# 创造其他法术\n\n通过玩粒子系统，你可以创造各种不同的法术，产生不同的效果。你可以创造火、闪电或者把敌人从你身边推开的法术。你可能在玩其他游戏的时候遇到了很多其他可能的咒语。\n\n# 火焰咒语\n\n通过将粒子系统的颜色改为红色，你可以很容易地创造出暴雪法术的火焰变体。这就是我们暴雪法术的火变体将会出现的方式:\n\n![](img/1b86ad1b-7a5e-4675-b4b1-233d26ed5059.png)\n\nThe out val of the color changed to red\n\n# 练习\n\n尝试以下练习:\n\n*   **闪电法术**:使用光束粒子制造闪电法术。跟随扎克的教程，在[https://www.youtube.com/watch?v=ywd3lFOuMV8&amp；list = PLZlv _ N0 _ o1gydlyb3lvfjyibbe8nqr8t&amp；指数=7](https://www.youtube.com/watch?v=ywd3lFOuMV8&list=PLZlv_N0_O1gYDLyB3LVfjYIcbBe8NqR8t&index=7) 。\n*   **力场法术**:力场会转移攻击。这对任何球员来说都是必不可少的。建议实现:派生一个名为`ASpellForceField`的`ASpell`子类。给类添加一个包围球，并在`ASpellForceField::Tick()`函数中使用它来驱逐怪物。\n\n# 摘要\n\n你现在知道如何在游戏中创造法术来保护自己了。我们已经使用粒子系统创造了一个可见的法术效果，以及一个可以对里面的任何敌人造成伤害的区域。你可以扩展你所学的知识，创造更多。\n\n在下一章中，我们将研究一种更新、更简单的方法来构建用户界面。"
  },
  {
    "path": "docs/learn-cpp-build-game-ue4/14.md",
    "content": "# 十四、利用 UMG 和音频改善用户界面反馈\n\n用户反馈在游戏中非常重要，因为用户需要游戏中正在发生什么的信息(分数、hp、显示库存等)。在前面的章节中，我们已经创建了一个非常简单的 HUD 来显示你库存中的文本和物品，但是如果你想要一个看起来专业的游戏，你会想要一个比这个更好的**用户界面** ( **UI** )！\n\n幸运的是，现在有更简单的方法来使用虚幻运动图形用户界面设计器(UMG)来构建用户界面，这是 UE4 包含的一个系统，就是为了这个目的。本章将向您展示如何使用它来继承我们之前所做的工作，并制作看起来更好、功能更多的东西。我们将开始更新清单窗口，我将建议您如何继续该过程并更新 UI 的其余部分。\n\n另一种提供反馈的方式是通过音频，或者在游戏本身中，或者在您与它交互时通过 UI，因此我们也将介绍如何播放声音。\n\n我们将讨论的主题如下:\n\n*   什么是 UMG？\n*   更新库存窗口\n*   布局您的用户界面\n*   更新您的平视显示器并添加健康栏\n*   播放音频\n\n# 什么是 UMG？\n\n您可能已经注意到，我们在屏幕上绘制的代码非常复杂。每个元素都需要手动放在屏幕上。你可能会问自己有没有更简单的方法。还有！这是虚幻运动图形用户界面设计师，或 UMG。\n\nUMG 通过使用特殊的蓝图简化了创建用户界面的过程，让你可以直观地布局界面。这也可以让你有一个精通技术的艺术家为你做布局，而你连接一切。我们将使用这个，但是因为这是一本 C++ 的书，我们将处理 C++ 中的大部分幕后功能。\n\n为了使用 UMG，首先你需要在你的 Visual Studio 项目中找到`GoldenEgg.Build.cs`文件。`.cs`文件通常是 C#，而不是 C++，但是您不必担心这一点，因为我们只会对该文件进行微小的更改。找到这一行:\n\n```cpp\nPublicDependencyModuleNames.AddRange(new string[] { \"Core\", \"CoreUObject\", \"Engine\", \"InputCore\" });\n```\n\n并将以下内容添加到该列表中:\n\n```cpp\n, \"UMG\", \"Slate\", \"SlateCore\"\n```\n\n一旦这样做，您可能需要重新启动发动机。然后你就可以在 UMG 编码了！\n\n# 更新库存窗口\n\n我们将从更新库存窗口开始。我们现在拥有的不是一个真实的窗口，只是屏幕上绘制的图像和文本，但是现在您将看到如何轻松创建看起来更像真实窗口的东西——带有背景和关闭按钮，代码将会简单得多。\n\n# WidgetBase 类\n\n要为 UMG 小部件创建一个 C++ 类，需要基于`UserWidget`创建一个新类。要在添加新的 C++ 类时找到它，您需要选中显示所有类并搜索它:\n\n![](img/bf64bda7-1e63-4817-b4d7-48e14dd54e41.png)\n\n说出你的班级`WidgetBase`。这将是您创建的任何其他小部件类的基类。这允许您将功能放在这个类中，该类将在许多不同的小部件中重用。在这种情况下，我把`CloseButton`的功能放在那里。不是所有的小部件都需要一个，但是如果你正在尝试一个标准的窗口，这通常是一个好主意。\n\n以下是`WidgetBase.h`的代码:\n\n```cpp\n#include \"CoreMinimal.h\"\n#include \"Blueprint/UserWidget.h\"\n#include \"UMG/Public/Components/Button.h\"\n#include \"WidgetBase.generated.h\"\n\n/**\n * WidgetBase.h\n */\nUCLASS()\nclass GOLDENEGG_API UWidgetBase : public UUserWidget\n{\n    GENERATED_BODY()\n\npublic:\n    UPROPERTY(meta = (BindWidgetOptional))\n    UButton* CloseButton;\n\n    bool isOpen;\n\n    bool Initialize();\n    void NativeConstruct();\n\n    UFUNCTION(BlueprintCallable)\n    void CloseWindow();\n};\n```\n\n这将设置允许您使用按钮关闭窗口的所有代码。`CloseButton`将是我们在设计蓝图中创建的按钮的名称。\n\n线`UPROPERTY(meta = (BindWidgetOptional))`应该会自动将`CloseWindow`变量链接到我们稍后将创建的蓝图中同名的`Button`对象。如果您知道小部件将一直在那里，您可以使用`UPROPERTY(meta = (BindWidget))`来代替，但是在这种情况下，可能没有关闭窗口所需的按钮。\n\n这里是`WidgetBase.cpp`:\n\n```cpp\n#include \"WidgetBase.h\"\n#include \"Avatar.h\"\n#include \"Kismet/GameplayStatics.h\"\n\nbool UWidgetBase::Initialize()\n{\n    bool success = Super::Initialize();\n    if (!success)  return false;\n\n    if (CloseButton != NULL)\n    {\n        CloseButton->OnClicked.AddDynamic(this, &UWidgetBase::CloseWindow);\n    }\n\n    return true;\n}\n\nvoid UWidgetBase::NativeConstruct()\n{\n    isOpen = true;\n}\n\nvoid UWidgetBase::CloseWindow()\n{\n    if (isOpen)\n    {\n        AAvatar *avatar = Cast<AAvatar>(\n            UGameplayStatics::GetPlayerPawn(GetWorld(), 0));\n\n        avatar->ToggleInventory();\n        isOpen = false;\n    }\n}\n```\n\nIf the UMG includes in this chapter don't work for you you might need to add `Runtime/` to the front of the path. But they should work like this (and do work in my project).\n\n下面一行是设置`OnClicked`事件调用特定函数的内容:\n\n```cpp\nCloseButton->OnClicked.AddDynamic(this, &UWidgetBase::CloseWindow);\n```\n\n我们不再需要像以前那样设置输入设置中的所有内容，因为 UMG 按钮已经设置为处理`OnClicked`，您只需要告诉它调用什么功能。如果由于某种原因不起作用，我将在稍后的蓝图中通过设置`OnClicked`向您展示如何解决它。由于`CloseButton`是可选的，您确实需要检查它，以确保它没有设置为`NULL`以避免错误。\n\n`isOpen`变量用于处理常见的用户界面问题，有时点击(或按键)注册多次，导致函数被调用多次，这可能会导致错误。通过在第一次调用`OnClicked`函数时将`isOpen`设置为真，您可以确保它不会运行多次，因为它只会在值为假时运行。当然，如果你重新打开窗口，你也需要确保该值被重置，这就是`NativeConstruct()`功能的作用。\n\n# 清单获取类\n\n现在，您将希望创建专门的类来处理库存小部件，从`WidgetBase`派生。如果由于某种原因，你不能找到`WidgetBase`来用通常的方式创建类，取消选中过滤器下的仅参与者。称这个为`InventoryWidget`。\n\n一旦创建了这个类，就可以开始添加代码了。首先是`InventoryWidget.h`:\n\n```cpp\n#include \"CoreMinimal.h\"\n#include \"WidgetBase.h\"\n#include \"UMG/Public/Components/Image.h\"\n#include \"UMG/Public/Components/TextBlock.h\"\n#include \"UMG/Public/Components/Button.h\"\n#include \"InventoryWidget.generated.h\"\n\n/**\n * \n */\nUCLASS()\nclass GOLDENEGG_API UInventoryWidget : public UWidgetBase\n{\n    GENERATED_BODY()\n\npublic:\n    const int kNumWidgets = 2;\n    //image widgets\n    UPROPERTY(meta = (BindWidget))\n        UImage* InventoryImage1;\n\n    UPROPERTY(meta = (BindWidget))\n        UImage* InventoryImage2;\n\n    //text widgets\n    UPROPERTY(meta = (BindWidget))\n        UTextBlock* InventoryText1;\n\n    UPROPERTY(meta = (BindWidget))\n        UTextBlock* InventoryText2;\n\n    //Invisible Buttons\n    UPROPERTY(meta = (BindWidget))\n        UButton* InventoryButton1;\n\n    UPROPERTY(meta = (BindWidget))\n        UButton* InventoryButton2;\n\n    bool Initialize();\n\n    void HideWidgets();\n    void AddWidget(int idx, FString name, UTexture2D* img);\n\n    UFUNCTION(BlueprintCallable)\n    void MouseClicked1();\n    UFUNCTION(BlueprintCallable)\n    void MouseClicked2();\n};\n```\n\n这个文件要复杂得多。我们再次使用`BindWidget`在蓝图中设置对象。虽然您可以像我们以前那样在代码中布局小部件(但是您应该能够创建一个包含图像、文本和按钮的子 idget)，为了使事情更简单，我只是在屏幕上布局了两个，并分别引用它们。你可以以后自己多加练习。\n\n因此，在这个特殊的例子中，我们为两个图像、两个文本块和两个按钮设置了小部件。有一个`Initialize`功能来设置这些，还有添加一个小部件、隐藏所有小部件以及每个按钮的鼠标点击处理程序的功能。\n\n那我们需要写`InventoryWidget.cpp`。首先，在文件顶部添加 includes:\n\n```cpp\n#include \"InventoryWidget.h\"\n#include \"MyHUD.h\"\n#include \"Runtime/UMG/Public/Components/SlateWrapperTypes.h\"\n```\n\n然后设置`Initialize`功能:\n\n```cpp\nbool UInventoryWidget::Initialize()\n{\n    bool success = Super::Initialize();\n    if (!success)  return false;\n\n    if (InventoryButton1 != NULL)\n    {\n        InventoryButton1->OnClicked.AddDynamic(this, &UInventoryWidget::MouseClicked1);\n    }\n    if (InventoryButton2 != NULL)\n    {\n        InventoryButton2->OnClicked.AddDynamic(this, &UInventoryWidget::MouseClicked2);\n    }\n\n    return true;\n}\n```\n\n该功能设置按钮的`OnClicked`功能。然后添加处理小部件的功能:\n\n```cpp\nvoid UInventoryWidget::HideWidgets()\n{\n    InventoryImage1->SetVisibility(ESlateVisibility::Hidden);\n    InventoryText1->SetVisibility(ESlateVisibility::Hidden);\n    InventoryImage2->SetVisibility(ESlateVisibility::Hidden);\n    InventoryText2->SetVisibility(ESlateVisibility::Hidden);\n}\n\nvoid UInventoryWidget::AddWidget(int idx, FString name, UTexture2D* img)\n{\n    if (idx < kNumWidgets)\n    {\n        switch (idx)\n        {\n        case 0:\n            InventoryImage1->SetBrushFromTexture(img);\n            InventoryText1->SetText(FText::FromString(name));\n            InventoryImage1->SetVisibility(ESlateVisibility::Visible);\n            InventoryText1->SetVisibility(ESlateVisibility::Visible);\n            break;\n        case 1:\n            InventoryImage2->SetBrushFromTexture(img);\n            InventoryText2->SetText(FText::FromString(name));\n            InventoryImage2->SetVisibility(ESlateVisibility::Visible);\n            InventoryText2->SetVisibility(ESlateVisibility::Visible);\n            break;\n        }\n\n    }\n}\n```\n\n`HideWidgets`隐藏窗口中的所有小部件，这样如果窗口中什么都没有，它们就不会出现。`AddWidget`获取图像本身的索引、名称和纹理，然后为该索引设置小部件。文本小部件有一个`SetText`功能，可以让你进入`FText` ( `FText::FromString`将其从`FString`转换为`FText`)。图像小部件具有设置图像的`SetBrushFromTexture`。\n\n最后，您需要设置`MouseClicked`功能:\n\n```cpp\nvoid UInventoryWidget::MouseClicked1()\n{\n    // Get the controller & hud \n    APlayerController* PController = GetWorld()->GetFirstPlayerController();\n    AMyHUD* hud = Cast<AMyHUD>(PController->GetHUD());\n    hud->MouseClicked(0);\n}\n\nvoid UInventoryWidget::MouseClicked2()\n{\n    // Get the controller & hud \n    APlayerController* PController = GetWorld()->GetFirstPlayerController();\n    AMyHUD* hud = Cast<AMyHUD>(PController->GetHUD());\n    hud->MouseClicked(1);\n}\n```\n\n这些只是用按钮的索引调用抬头显示器的`MouseClicked`功能(提示:这不会编译，直到那些抬头显示器功能被更新以获取索引)。如果你想进一步试验，以后你可以根据点击的按钮寻找另一种方法来获得索引，这样你就可以对所有的按钮使用相同的功能。\n\n# 设置小部件蓝图\n\n接下来，您需要设置蓝图。由于这是一种特殊的蓝图，建立一个有自己的类有点棘手。你不能仅仅创建一个类的蓝图，否则你将没有一个设计蓝图。相反，您必须先创建设计蓝图，然后更改父级。\n\n为此，请进入内容浏览器并选择要放入的目录，然后选择添加新项|用户界面|小部件蓝图:\n\n![](img/21cb9202-5a8a-4914-b4d2-865824109afb.png)\n\n重命名`BP_InventoryWidget`然后双击打开。你应该看到这样的东西:\n\n![](img/e677d439-c58d-4973-a2ef-31d64e6b4f4d.png)\n\n在中间，你将在视觉上布局屏幕，方框代表你要瞄准的理论屏幕的边缘。在左侧，调色板向您展示了可以添加到屏幕上的基本用户界面对象。您将看到许多常见对象，如图像、文本栏、进度条、按钮、复选框和滑块。这是很多你基本上免费得到的功能。一旦你到了为你的游戏设置一个设置窗口的时候，很多设置都会派上用场。\n\n但是首先，我们需要在这上面更改父类，您将在这里这样做。选择右上角的图形和顶部工具栏上的类设置，然后在类选项的详细信息下查看，并选择按父类的下拉列表。选择清单获取:\n\n![](img/b60f8599-1131-4736-8ba9-13aa326477a4.png)\n\n现在我们要回到设计器，开始布置屏幕！\n\n屏幕上应该已经有一个画布面板了。你可以点击右下角并拖动使其成为你想要的大小。画布通常应该是全屏大小。所有其他用户界面小部件将进入画布。当你拖动它时，它会在屏幕上显示你想要的各种分辨率。你会想要选择一个类似于你的目标分辨率。\n\n然后选择调色板下的边框，并将其拖到屏幕上。这将是窗口的背景。您可以单击角并将其拖动到您想要的大小。您也可以在右侧找到颜色栏(在“详细信息”下的“外观”>“画笔颜色”旁边)，然后单击它打开颜色选择器来选择背景颜色:\n\n![](img/0ec86d68-7381-4e7b-8928-d64953fd59c6.png)\n\n您也可以在“详细信息”下重命名对象。完成后，点按并拖移屏幕上的按钮，并将其放在背景的右上角。如果它试图填充整个边框对象，请确保在层次结构中选择“画布面板”，或者将其拖到边框对象之外，然后将其拖到顶部。一定要把这个命名为`CloseButton`。如果你想让它看起来更像一个关闭按钮，你也可以放一个带有字母 X 的文本对象。您应该取消选中“详细信息”中“行为”下的“启用”，这样它就不会阻止鼠标单击。\n\n接下来，您将需要定位两个图像对象和两个文本对象(您可以稍后添加更多)。请确保名称与您在代码中使用的名称完全匹配，否则它们将不起作用。在文本字段中，您会发现设置字体要容易得多。在“详细信息|外观”下，您会发现字体选项就像您在任何文字处理器中习惯的一样，并且您可以使用已经在您的计算机上的字体(尽管，如果您仍然想要下载字体，没有什么能阻止您)。您也可以使用之前添加的字体。\n\n另外，对于`OnClicked`，您会想要添加一个按钮。您可以在下面添加一个，但是我使用了一个通用的用户界面方法:不可见的按钮。将一个按钮拖出来，让它覆盖其中一个按钮的图像和文本。然后进入背景色，将 alpha (A)设置为`0`。阿尔法是衡量一种颜色透明度的标准，`0`意味着你根本看不到它。\n\nIf you later have trouble clicking the buttons other objects might be in the way. Try dragging them so they are behind the button or look into ways of disabling clicks on those objects.\n\n最后，你应该有这样的东西:\n\n![](img/3c7d7aed-0bfc-4ded-88be-e5c08bb2d320.png)\n\n此外，当您选择了边框时，请仔细注意右侧内容下的选项。这里是您可以设置水平和垂直对齐的地方。始终尝试设置这些，因此，如果您希望某些内容始终位于屏幕的左上角，对齐方式将设置为水平向左对齐和垂直向上对齐。如果您没有为每个对象设置对齐方式，不同屏幕分辨率下的结果可能是不可预测的。我以后会更深入地探讨这个问题。\n\n但现在，这将是你的库存窗口。它不一定要看起来和我的一模一样，所以尽情享受视觉布局吧！但是，请记住，您可能不希望它占据整个屏幕，这样您就可以在单击后看到正在施放的法术(尽管您可以在稍后单击某个法术时查看关闭窗口)。\n\n# AMyHUD 更改\n\n但这还不是全部！我们仍然需要修改我们现有的类来支持这个新的 Widget，从`AMyHud`类开始。为了使事情更简单，我们不会在这里复制所有以前的功能。相反，我们将只是设置`OnClicked`功能来施法，因为这将比在屏幕上拖动物品更有用。UMG 不会自动处理右键单击，但是如果您以后想要添加它，您可以自己查看更多内容，还可以查看以前的单击和拖动功能，因此如果您认为以后可能需要，您可能想要注释掉旧代码，而不是删除它。\n\n现在，`MouseMoved`和`MouseRightClicked`功能都没有了，`MouseClicked`功能现在取一个`int`索引。我们还有`OpenInventory`和`CloseInventory`的新功能，所以`MyHUD.h`现在应该有这个:\n\n```cpp\n    void MouseClicked(int idx);\n\n    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = \"Widgets\") \n        TSubclassOf<class UUserWidget> wInventory;\n\n    UInventoryWidget* Inventory;\n\n    void OpenInventory();\n    void CloseInventory();\n```\n\n还在文件顶部添加# include“inventorywidget . h”。其他一些功能也将被修改。所以，现在我们来看看`AMyHUD.cpp`，你会看到新版本的函数有多简单。以下是处理小部件的新功能:\n\n```cpp\nvoid AMyHUD::DrawWidgets()\n{\n    for (int c = 0; c < widgets.Num(); c++)\n    {\n        Inventory->AddWidget(c, widgets[c].icon.name, widgets[c].icon.tex);\n    }\n}\n\nvoid AMyHUD::addWidget(Widget widget)\n{\n    widgets.Add(widget);\n}\n\nvoid AMyHUD::clearWidgets()\n{\n    widgets.Empty();\n}\n```\n\n我们还需要将`MouseClicked`功能更新为:\n\n```cpp\nvoid AMyHUD::MouseClicked(int idx)\n{\n    AAvatar *avatar = Cast<AAvatar>(\n        UGameplayStatics::GetPlayerPawn(GetWorld(), 0));\n    if (widgets[idx].bpSpell)\n    {\n        avatar->CastSpell(widgets[idx].bpSpell);\n    }\n\n}\n```\n\n这将根据传入的索引施放咒语。然后是打开和关闭库存的新功能:\n\n```cpp\nvoid AMyHUD::OpenInventory()\n{\n    if (!Inventory)\n    {\n        Inventory = CreateWidget<UInventoryWidget>(GetOwningPlayerController(), wInventory);\n    }\n    Inventory->AddToViewport();\n    Inventory->HideWidgets();\n}\n\nvoid AMyHUD::CloseInventory()\n{\n    clearWidgets();\n    if (Inventory)\n    {\n        Inventory->HideWidgets();\n        Inventory->RemoveFromViewport();\n    }\n}\n```\n\n主要部分是在`Viewport`中添加或删除新的 Widget。我们还希望可视化地隐藏小部件，以防止显示空的小部件，并在窗口关闭时清除所有小部件。\n\n我们还更改了`struct Widget`删除所有定位信息。对它的任何引用都应该被删除，但是如果你以后遇到任何错误(除非你对头像类进行了更改，否则你将无法编译)，请确保`MouseMoved`和`MouseRightClicked`已经消失或被注释掉，并且没有其他任何东西引用它们。更新、更简单的小部件应该如下所示:\n\n```cpp\nstruct Widget\n{\n    Icon icon;\n    // bpSpell is the blueprint of the spell this widget casts \n    UClass *bpSpell;\n    Widget(Icon iicon)\n    {\n        icon = iicon;\n    }\n};\n```\n\n# AAvatar 更改\n\n在`AAvatar`中，我们将主要修改`ToggleInventory`功能。较新的函数如下所示:\n\n```cpp\n\nvoid AAvatar::ToggleInventory()\n{\n    // Get the controller & hud \n    APlayerController* PController = GetWorld()->GetFirstPlayerController();\n    AMyHUD* hud = Cast<AMyHUD>(PController->GetHUD());\n\n    // If inventory is displayed, undisplay it. \n    if (inventoryShowing)\n    {\n        hud->CloseInventory();\n        inventoryShowing = false;\n        PController->bShowMouseCursor = false;\n        return;\n    }\n\n    // Otherwise, display the player's inventory \n    inventoryShowing = true;\n    PController->bShowMouseCursor = true;\n    hud->OpenInventory();\n    for (TMap<FString, int>::TIterator it =\n        Backpack.CreateIterator(); it; ++ it)\n    {\n        // Combine string name of the item, with qty eg Cow x 5 \n        FString fs = it->Key + FString::Printf(TEXT(\" x %d\"), it->Value);\n        UTexture2D* tex;\n        if (Icons.Find(it->Key))\n        {\n            tex = Icons[it->Key];\n            Widget w(Icon(fs, tex));\n            w.bpSpell = Spells[it->Key];\n            hud->addWidget(w);\n        }    \n    }\n    hud->DrawWidgets();\n}\n```\n\n如您所见，许多相同的 HUD 功能被重用，但是`OpenInventory`和`CloseInventory`的新功能现在从这里调用，因此 HUD 可以在添加小部件之前显示窗口，并移除窗口以关闭它。\n\n另外，从`Yaw`和`Pitch`功能中删除以下行:\n\n```cpp\n        AMyHUD* hud = Cast<AMyHUD>(PController->GetHUD());\n        hud->MouseMoved();\n```\n\n同时从`MouseRightClicked`中删除以下几行(或者删除该功能，但如果删除，请确保也从`SetupPlayerInputComponent`中删除):\n\n```cpp\n        AMyHUD* hud = Cast<AMyHUD>(PController->GetHUD());\n        hud->MouseRightClicked();\n```\n\n最后，从`MouseClicked`中移除这些线(因为你不想在点击不属于清单的某个地方时意外触发法术):\n\n```cpp\n    AMyHUD* hud = Cast<AMyHUD>(PController->GetHUD());\n    hud->MouseClicked();\n```\n\n现在你应该可以编译了。完成后，进入业务伙伴 _ 我的 HUD，将类别默认值>小部件>工作清单下拉列表更改为业务伙伴 _ 清单。\n\n# 关于舔的一个注意事项\n\n您的`OnClicked`功能可能无法正常工作(我自己也遇到了这个问题)。如果你找不到解决方案，你可以用蓝图绕过，这就是为什么我让所有的鼠标点击功能蓝图可调用。\n\n如果发生这种情况，请进入小部件蓝图的设计器，单击每个按钮，在详细信息下找到事件，然后单击单击时旁边的绿色+按钮。这会将该按钮的`OnClicked`添加到图表中并切换到该按钮。您需要返回添加其他 2 个按钮。然后，从节点中拖出并添加所需的函数。应该是这样的:\n\n![](img/168d8986-d61f-4867-82fa-656e1ce75423.png)\n\n# 布局您的用户界面\n\n当你设计一个用户界面时，有一些重要的事情要记住，UMG 有工具让你更容易做到这一点。要记住的最重要的事情之一是，你的游戏不会总是以相同的分辨率运行。如果你在玩手机游戏，可能会有很多不同分辨率的不同设备，你希望你的游戏在所有这些设备上看起来基本相同。即使是游戏机也不再摆脱这个问题，因为 Xbox One 和 PS4 现在都有 4K 选项。所以，你的游戏需要以一种让这成为可能的方式来设置。\n\n如果你把所有的小部件都做成特定的像素大小，然后以更大的分辨率运行，它可能会非常小，看起来不可读，按钮也很难点击。在较小的分辨率下，它可能太大而无法显示在屏幕上。所以，记住这一点。\n\n您之前设置的画布面板将直观地向您展示它在您想要的尺寸下的外观。但是对于不同的尺寸，你需要记住几件事。\n\n首先，一定要用主播。在详细信息下，您将看到锚的下拉列表。当您打开它时，您应该会看到如下内容:\n\n![](img/fc965460-7745-4f64-b872-a92b8a533d53.png)\n\n蓝线左上角的九个选项是对齐对象。行与屏幕的顶部、中间和底部对齐，而列与左侧、中间和右侧对齐。因此，如果你有一些你总是想出现在屏幕左上角的东西(比如分数，或健康条)，你会选择左上角的选项。如果你想让其他东西水平和垂直居中，选择第二行，第二列。白色小方块基本上给你展示了定位。\n\n剩下的选项给了你在整个屏幕上拉伸东西的方法(不管它是什么尺寸)。所以，如果你想在顶部、中部或底部水平拉伸一些东西，看看右边的栏。垂直的话，看下排。如果你想让一个窗口在整个屏幕上延伸，右下角的那个就很好。\n\n如果您想要缩放调色板中的所有内容以适应屏幕大小，也可以从调色板中添加缩放框。尽管如果您有想要保持固定大小的内容，例如图像，您可以选中“内容大小”来防止它自动调整大小。\n\n如果你想变得更高级，你可以添加代码来检查屏幕大小，并交换出部分或整个用户界面，但这超出了本书的范围，所以如果你想以后自己尝试，请记住这一点！\n\nAnother important things to keep in mind with your UI is localization. If you want to release your game anywhere outside your own country you will need to localize. This means you will have to get used to not just hardcoding text but use the built-in localization system to add string ids you've set up instead of hardcoding the text. The code will look for specific ids and swap them for the appropriate localized text. You can look into the built-in localization system here: [https://docs.unrealengine.com/en-us/Gameplay/Localization](https://docs.unrealengine.com/en-us/Gameplay/Localization).\n\nThis will also affect how you lay out your UI. The first time you localize your game in German, you'll find out that everything is twice as long! While you may be able to get your translators to come up with shorter ways to say the same thing, you will probably want to make text blocks longer than you think they need to be, or consider finding ways to make the text shrink to fit or scroll.\n\n# 更新您的平视显示器并添加健康栏\n\n我不会在这里给出完整的说明，但这里有一些关于更新你的平视显示器的提示。一旦你这样做了，它将进一步简化你的代码！\n\n# 创建抬头显示器类\n\n您需要为新的抬头显示器创建一个从 WidgetBase 派生的新类。在这种情况下，您将需要画布面板，但没有背景。确保所有的东西都能在整个屏幕上延伸。\n\n您会希望将大部分用户界面保留在角落，因此您可以在屏幕的左上角添加一个进度条小部件来显示运行状况。此外，考虑添加一个文本小部件来告诉它是什么和/或将实际数字放在屏幕上。\n\n对于消息，您可以将文本小部件与屏幕的中上对齐，并使用它们来显示文本。\n\n# 添加健康栏\n\n如果您添加了推荐的进度条小部件，您会发现现在绘制健康条要容易得多。您需要像使用其他小部件一样获得对它的引用。然后，你所需要做的就是调用`SetPercent`来显示当前的健康状况(并在它改变时重置它)。\n\n你不再需要自己画整个东西，但是你可以使用`SetFillColorAndOpacity`自定义它的外观！\n\n# 播放音频\n\n我们将回到你的代码中，做最后一件真正有助于你的游戏反馈的事情，然而在某种程度上，它往往是任何人在创建游戏时考虑的最后一件事:音频。\n\n音频确实可以增强你的游戏，从点击按钮时播放声音到添加音效、对话框、背景音乐和氛围。如果你晚上一个人在树林里散步，蟋蟀的鸣叫声，你自己的脚步声，不祥的音乐真的可以设定心情。或者，你可以用鸟叫声和快乐的音乐来营造完全不同的心情。一切都取决于你！\n\n我们只是在你施放暴风雪法术时增加一个声音。所以寻找一种自由风的声音。有很多网站提供免版税的声音文件。如果你使用它们，他们中的一些人希望你在你的学分中提到它们。为此，我在一个名为[SoundBible.com](http://www.soundbible.com)的网站上找到了一个公共域声音，这意味着任何人都可以使用它。但是找一个你喜欢的。\n\n有些网站可能会让你注册下载声音。如果你有野心，你甚至可以自己录一张！\n\nI used a .wav file, a standard format, although other formats will probably work. But for small sounds, you may want to stick to .wav because MP3s use compression, which might slow down your game slightly because it needs to de-compress it.\n\n有了喜欢的文件后，为声音创建一个文件夹，并将声音文件从文件管理器拖到其中。然后右键单击同一个文件夹，选择声音|声音提示:\n\n![](img/2b52b6f2-364a-4485-8585-a519d7ae5a5e.png)\n\n将其重命名为 WindCue，并双击它以在蓝图编辑器中打开它。应该是这样的:\n\n![](img/131d3ae3-3a8b-4994-a0da-109a867a4541.png)\n\nSoundCue 是我们设置声音的地方。首先，右键单击任意位置并选择波形播放器添加一个:\n\n![](img/500545b1-f0ba-4555-8f2f-fdea977a5965.png)\n\n然后，选择波形播放器。在详细信息中，您将看到声波选项。选择下拉列表，搜索您添加的`.wav`文件进行选择:\n\n![](img/279db068-abc1-4cde-8025-315c2bb495c0.png)\n\n然后，从波形播放器的输出中单击并拖动到输出中(带有小扬声器图像)。这会把它连接起来。要测试它，您可以选择播放提示，当声音传输到输出时，您应该会听到并看到线路亮起橙色:\n\n![](img/9be39afa-c610-4f29-aa53-d6ea16ec7fc1.png)\n\n如果你不喜欢它听起来的样子，试试细节下面的选项。我用的那个太安静了，不符合我的要求，所以我增加了音量倍增器，让它大得多。\n\n现在我们已经设置好了声音，是时候将它添加到代码中了。在这种情况下，我们将更新`AMyHUD`类。首先，在`MyHUD.h`的顶部增加以下一行:\n\n```cpp\n#include \"Sound/SoundCue.h\"\n```\n\n另外，在同一文件中添加以下内容:\n\n```cpp\nUPROPERTY(EditAnywhere, BlueprintReadWrite, Category = \"Sound\")\n    USoundCue* audioCue;\n```\n\n您会希望将`SoundCue`引用存储在蓝图中，以便于更新。\n\n现在，转到`MyHUD.cpp`并在调用`CastSpell`后将以下行添加到您的`MouseClicked`功能中:\n\n```cpp\n        UGameplayStatics::PlaySound2D(this, audioCue);\n```\n\n这实际上会播放声音。确保该文件中有`#include \"Kismet/GameplayStatics.h\"`以使其工作。在这种情况下，由于它是正确的球员无论何时你投它，一个 2D 的声音将是好的。如果你想让你环境中的东西(比如怪物)发出自己的声音，你会想研究 3D 声音。UE4 会让你这么做的！\n\n现在，回到编辑器并编译所有内容，然后回到 HUD 蓝图。您需要将您创建的`SoundCue`添加到蓝图中。\n\n您可以从下拉列表中选择它，并像这样搜索它:\n\n![](img/0e38e675-aeb7-418a-b880-9cc392463579.png)\n\n现在，保存、编译并运行游戏。跑来跑去，直到你获得一个暴雪法术，然后点击 *I* 打开库存。点击暴雪咒语。你不仅应该看到施法，还应该听到它！\n\n# 摘要\n\n您现在已经很好地了解了如何使用 UMG 创建用户界面，以及如何添加音频来进一步增强您的体验！还有很多工作要做，但是考虑一下那种做法！\n\n我们已经完成了代码，但还没有完成书。接下来，我们将看看如何获取我们所拥有的，并在虚拟现实中查看它！我会给你一些提示，然后我们会以 UE4 中一些其他高级特性的概述来结束。"
  },
  {
    "path": "docs/learn-cpp-build-game-ue4/15.md",
    "content": "# 十五、虚拟现实及其他\n\n除非你一直住在山洞里，否则你可能听说过**虚拟现实** ( **VR** )。VR 和**增强现实** ( **AR** )是目前游戏领域最热门的趋势之一，这将在本章稍后介绍。由于超便宜的谷歌纸板和类似设备等创新，让你可以在最近的智能手机上查看基本的虚拟现实，很容易获得虚拟现实技术。\n\n无论你只有一个谷歌纸板，还是你有一个更高端的设备，如 Oculus Rift 或 HTC VIVE，UE4 都可以很容易地为虚拟现实编程。当然，如果你有 PlayStation VR，你需要成为索尼官方开发人员来为其编程(就像你为 PlayStation 编程其他东西一样)，所以你可能无法做到这一点，除非你为一家正在做 PSVR 冠军的公司工作。\n\n在这里，您将获得虚拟现实和 UE4 的概述，这将有助于您开始。以下是我们将要讲述的内容:\n\n*   为虚拟现实做准备\n*   使用虚拟现实预览和虚拟现实模式\n*   虚拟现实中的控件\n*   虚拟现实开发技巧\n\n我还将介绍 UE4 的一些更高级的功能。我们将从现在关注的另一个大的热门技术 AR 开始，然后转向其他技术。以下是我们将介绍的内容:\n\n*   阿肯色州\n*   程序设计\n*   用插件和附加组件扩展功能\n*   移动、控制台和其他平台\n\n# 为虚拟现实做准备\n\n这是一个进入虚拟现实开发的激动人心的时刻。也许你是想接触最新的热门技术。或者，也许你和我一样，几十年来一直在阅读威廉·吉布森、尼尔·斯蒂芬森、威廉敏娜·贝尔德和布鲁斯·贝克等作家的赛博朋克书籍中关于虚拟现实的内容，并为它终于出现而感到兴奋。无论是哪种情况，以下是你如何为你的虚拟现实编程之旅做准备。\n\n要开始使用 Oculus Rift 或 HTC Vive 进行虚拟现实，首先您需要一台准备好虚拟现实的计算机。Oculus 有一个免费程序，你可以在他们的网站[https://ocul.us/compat-tool](https://ocul.us/compat-tool)下载，或者去他们的支持页面，它会告诉你你的显卡是否有问题。\n\n即使你有一台最新的电脑，除非你有一台特别标记为虚拟现实就绪的电脑，否则你很可能需要一个新的显卡。虚拟现实是非常图形密集型的，所以它需要一个相当高端(通常相当昂贵)的显卡。\n\n当然，如果你想做的只是手机上的 VR，没有它你可能也能过得去，但你将不得不在手机上做所有的测试，并且无法访问 UE4 的很多很酷的功能，比如 VR 编辑。\n\n一旦你有了一台可以处理的电脑，你可能会想要一台 Oculus Rift 或 HTC Vive(或者两者都要，如果你真的很认真，并且有足够的钱投入其中，因为两者都不便宜)。作为安装过程的一部分，您得到的任何设备都将安装您需要的所有驱动程序。\n\n然后，进入 UE4，转到编辑|插件，并确保您拥有任何设备的插件(您可以搜索它们)。它应该看起来像这样，这取决于您的虚拟现实硬件:\n\n![](img/566d5501-f7d1-476d-94bd-bd88596fd6df.png)\n\n此外，请确保您的虚拟现实软件正在运行(当您打开 UE4 时，它可能会自动启动，具体取决于您的虚拟现实硬件)。\n\n# 使用虚拟现实预览和虚拟现实模式\n\n如果你想在虚拟现实中查看一些东西，好消息是你不需要写任何新的东西！只需进入现有项目，单击播放按钮旁边的箭头，然后选择虚拟现实预览:\n\n![](img/4c5602ad-8138-4d8f-bdfd-3f4299a861c1.png)\n\n现在，只要戴上你的虚拟现实耳机，你就应该可以在虚拟现实中看到游戏了！\n\n一旦你运行了游戏，你就能看到游戏世界。你将无法四处移动(当你在虚拟现实中时，你看不到你的键盘或鼠标)，但你将能够转过头来环顾四周。\n\nBe very careful if you're prone to motion sickness. This is a serious problem in VR, although there are ways to minimize the effects in your game, which we will talk about later. You might not want to be in  VR mode for too long until you get used to it and know how it affects you.\n\nUE4 还有另一个真正能帮到你的工具，VR 模式。这允许你在虚拟现实中实际查看和编辑游戏，所以你可以看到当你做出改变时，它们会是什么样子。这可能非常有帮助，因为许多东西在虚拟现实中看起来不像在非虚拟现实游戏中一样。\n\n要激活虚拟现实模式，请单击工具栏中的虚拟现实模式或点击 *Alt* + *V* :\n\n![](img/35549f17-a3f4-4eab-9206-20c89a971d69.png)\n\n你可以四处看看，在虚拟现实模式下，你可以在游戏中使用你的运动控制器\n。在第一次进入虚拟现实模式之前，您可能想要查找您需要的控件。在虚幻网站上有关于虚拟现实模式及其控件的详细说明，网址为:[https://docs.unrealengine.com/en-us/Engine/Editor/VR](https://docs.unrealengine.com/en-us/Engine/Editor/VR)。\n\nIf you want to go further, by programming for specific VR systems, such as the Oculus Rift, Vive, Steam VR, or others, there are detailed instructions on working with many different VR systems on the Unreal website. You can find them here: [https://docs.unrealengine.com/en-us/Platforms/VR](https://docs.unrealengine.com/en-us/Platforms/VR).\n\n# 虚拟现实中的控件\n\n你可能会注意到，在虚拟现实模式下，通常的控制不会起作用。你甚至看不到带着虚拟现实耳机的键盘和鼠标，这使得使用它们变得极其困难。幸运的是，高端设备有自己的控制器，UE4 有一个运动控制器组件，你可以把它添加到你的玩家棋子上，这样你就可以用它而不是鼠标来指向东西。\n\nIf you know from the beginning that you are aiming for VR, UE4 has VR-specific classes and templates available that will add some of the functionality you need automatically. There is also a VR expansion plugin that is extremely helpful, and if you're not on a big team of developers, you should really look into it. You can find it here: [https://forums.unrealengine.com/development-discussion/vr-ar-development/89050-vr-expansion-plugin](https://forums.unrealengine.com/development-discussion/vr-ar-development/89050-vr-expansion-plugin)\n\nUI 在 VR 中非常棘手，很多人还在努力想出最好的方法。你最好的选择可能是玩很多现有的游戏，看看你认为什么最适合你。确保你尽可能多地进行实验，因为这是观察什么有效的最好方法！\n\n# 虚拟现实开发技巧\n\n虚拟现实是一项新的令人兴奋的技术。人们仍在寻找可行的方法，因此有足够的实验空间，而且还有很多这样的实验正在进行。但是你仍然需要记住一些最佳实践，因为你不想让玩你游戏的人有不好的体验，甚至在玩你的游戏时生病。如果他们这样做了，他们可能不会再玩它，也不太可能买你的下一个游戏。所以，你希望体验对每个人都好。\n\nVR 最大的问题是模拟病(或晕动病)。有些人比其他人更容易受到这种影响，但是如果你不小心，即使是通常不容易晕车的人也会有问题。所以，小心很重要。确保你有其他人测试你的游戏，因为虽然你可能会习惯，但这并不意味着其他人不会有麻烦。\n\n最重要的考虑之一是保持非常高的帧速率。不同的设备对最低帧速率有不同的建议，如果你低于这些，人们很可能会开始有问题。\n\n总的来说，保持尽可能高的质量很重要。任何看起来假的或坏的东西都可能把人扔出去，导致晕车。所以，如果你试图达到的任何效果看起来不像你预期的那样，试着做些别的事情。\n\n你可能会注意到，许多虚拟现实游戏没有让玩家在游戏中走动，或者让他们坐在移动的车辆上。这是避免模拟疾病的另一种方法。最大的问题是运动，尤其是垂直运动，比如跳跃，或者通过控制器旋转，而不仅仅是转动你的头。基本上，你的大脑认为你在运动，但是你的身体得到了冲突的信息，因为它没有感觉到运动。如果你认为你坐在车里，你的身体并不期望感觉到运动，所以这就是为什么它似乎工作得更好。虽然，如果玩家在玩的时候站着，他们的问题可能会少一些。\n\n网上有很多关于虚拟现实的信息和最佳实践。虚幻网站有一个关于最佳实践的页面，包含一些非常好的特定于 UE4 的信息。我建议在你开始你的项目之前仔细检查一下，因为从一开始就记住最佳实践比在项目结束时发现有些东西不工作或者不会很好地工作要好。\n\n正如我之前所说，让人们去测试它是非常重要的。虚拟现实技术是如此新，以至于你会想确保它能为尽可能多的人服务。\n\n# 阿肯色州\n\nAR 类似于 VR，只是，不是完全被虚拟世界包围，在这种情况下，你看到的是放置在现实世界中的虚拟物体(通过摄像头观看)。这可能是通过耳机，如微软的全息镜头，或魔术飞跃。但是由于这些都是新的，目前只能作为面向开发人员的昂贵设备提供，您将主要通过移动设备看到增强现实。\n\n移动设备上流行的 ar 游戏包括口袋妖怪 Go，在这里你可以抓住口袋妖怪，在你周围的世界面前观看它们。在 AR 模式下，你必须环顾四周，直到你找到一只口袋妖怪(它显示要转向的方向)并抓住它。你甚至可以拍照，这使得一些有趣的图像。它的前身，入口，让你去游戏中的真实世界的位置，但口袋妖怪围棋真的在这一点上有所扩展。\n\n由于那款游戏的成功，手机 AR 游戏现在非常受欢迎。因为你在处理你无法控制的现实世界的物体，这可能会涉及一些复杂的计算机视觉，但幸运的是，UE4 有内置的功能来帮助你。\n\nUE4 支持的两个主要移动 AR 系统是 iOS 的 ARKit 和安卓的 ARCore。你可以在[https://docs.unrealengine.com/en-us/Platforms/AR](https://docs.unrealengine.com/en-us/Platforms/AR)的虚幻网站上找到更多关于 AR 编程和每种类型的先决条件的详细信息。要启动其中任何一个，您需要使用手持增强现实模板创建一个新项目:\n\n![](img/18e0180d-0637-4b27-b536-badb415b48eb.png)\n\n如前一张截图所示，您的设置应该是移动/平板电脑、可缩放 3D 或 2D 以及无入门内容。创建项目后，您可以将手机连接到电脑，如果手机已完全设置好(取决于您的手机，您可能需要在电脑上安装软件才能看到它)，当您单击“启动”旁边的箭头时，应该会在“设备”下看到它。否则，您仍然可以在“播放”下使用移动预览 ES2 (PIE)。\n\nWhile you're not likely to be programming for Magic Leap anytime soon, there is early access documentation on it available on the Unreal site at: [https://docs.unrealengine.com/en-us/Platforms/AR/MagicLeap](https://docs.unrealengine.com/en-us/Platforms/AR/MagicLeap).\n\n# 程序设计\n\n游戏中的程序设计最近非常流行。如果你玩过《《我的世界》》、《无人区的天空》或《孢子》等游戏，你就玩过一个程序性游戏。程序游戏的历史可以追溯到几十年前，可以追溯到老的基于文本的游戏，如莫莉亚、安格班德和网络黑客。Rogue like 游戏(以原作命名，Rogue)仍然是一种流行的游戏类型，它使用程序技术来生成随机等级，因此每次你玩的时候，你都会得到一个完全不同的游戏。因此，程序编程增加了可重放性，当必须手工构建关卡时，这是很难得到的。\n\n程序化编程让你通过代码中的规则和算法来创建游戏的各个部分，无论是环境、关卡，甚至是音频。基本上，代码不是让一个人设置每个细节，而是为你设置。\n\n结果可能是不可预测的，尤其是在 3D 中，这比用 2D 文本字符绘制房间和路径要复杂得多。正因为如此，有时候会提前创建程序级别，所以设计师可以在添加到游戏之前选择自己喜欢的级别。\n\n有许多不同的技术有助于过程编程。一种是使用**体积像素** ( **体素**)，它允许您根据点与其他体素的关系，以简单的方式引用 3D 空间中的点。体素已经被用在很多项目中，包括现在已经停止运行的游戏《地标》(我曾经参与过)，并且应该被用在现在已经取消的 EverQuest Next 中。UE4 通过插件支持体素，比如体素插件([https://voxelplugin.com/](https://voxelplugin.com/))。\n\n程序编程也可以用于音乐。有些项目已经对特定类型的音乐进行了神经网络训练，并推出了类似风格的令人印象深刻的音乐。您还可以根据游戏中发生的事情修改播放的音乐。孢子用这个做了一些令人印象深刻的事情。\n\n如果你对学习更多感兴趣，可以查阅大卫·科普，他是一位研究人员，已经就这个主题写了几本书。或者，你可以在这里看到虚幻的开发人员一直在用程序音频做什么:[http://proceduralaudionow . com/aaron-mcleran-and-Dan-Reynolds-procedural-audio-in-new-Unreal-audio-engine/](http://proceduralaudionow.com/aaron-mcleran-and-dan-reynolds-procedural-audio-in-the-new-unreal-audio-engine/)。你也可以找到 UE4 插件，比如我以前用过的一个程序性 MIDI 插件。\n\n# 用插件和附加组件扩展功能\n\n我们已经看到了一些插件和其他插件的例子，以及它们如何扩展 UE4，从为您的特定虚拟现实耳机添加虚拟现实功能到添加支持体素或程序音乐的功能。但是还有很多。\n\n对于插件，您可以转到编辑|插件，并按类别查看所有可用的内容:\n\n![](img/e98ff7d3-2450-4583-bf19-640225a53e22.png)\n\n这些是内置插件。\n\n但是如果你想了解更多，你可以在史诗游戏的启动器中查看市场:\n\n![](img/a651bb48-ffdc-4cd3-8032-fab9709bb77e.png)\n\n虽然您会看到很多图形和模型，但您可以添加大量可用的功能。有些是免费的，有些你需要付费。例如，以下是对“程序性”的搜索:\n\n![](img/ca744b78-3900-4962-947e-fcff46050c11.png)\n\nUE4 是一个非常受欢迎的游戏引擎，所以如果你有什么需要，很有可能别人已经为它开发了一个插件。你也可以在互联网上的其他地方找到许多项目，其中许多是开源的，开发者很乐意帮助你实现它们。但是这些可能需要额外的工作来实现，您需要小心，并确切知道您正在下载和安装什么。\n\n# 移动、控制台和其他平台\n\n正如您在我们提到 AR 时所看到的，您可以在 UE4 中为移动设备开发，并在您的计算机或手机上预览您的游戏。UE4 的一大优点就是支持很多不同的平台。\n\n很多 AAA 游戏工作室都使用 UE4，所以它肯定支持所有主要的游戏主机(Xbox One、PS4、Switch，甚至是 3DS 和 Vita 等移动主机)。其中的诀窍是，你通常不能只为他们开发——你需要成为一名授权的开发人员，通常需要花很多钱购买一个开发工具包(一个专门用于开发的控制台版本，允许你在控制台上调试)。\n\n幸运的是，随着游戏机上独立游戏市场的发展，现在获得开发者访问权限的门槛比过去低了很多。但是在你开始研究这个之前，你可能仍然想要更多的经验和出版的游戏标题。\n\n同时，你还有很多不同的游戏选择和平台。一旦你为一个平台构建了一个游戏，将这个游戏移植到另一个平台就容易多了(UE4 让这变得非常容易！).\n\n主要区别在于控制，因为你可能使用触摸屏、控制器、运动控制器(在虚拟现实中)或键盘和鼠标。每一个都会有不同的要求，并且会稍微改变游戏的玩法。但是，只要你从一开始就记住你的目标是哪个平台，你就能以一种适合所有平台的方式来规划你的游戏。\n\n# 摘要\n\n这本书我们已经讲了很多，但我们已经到了结尾。我们已经学习了 C++ 的基础知识，并在 UE4 中创建了一个非常简单的游戏，其中包含一些基本的 AI，一个包含库存的部分 UI，以及使用粒子系统施法的能力。我们还了解了虚拟现实、增强现实以及 UE4 可以帮助您解决的其他新兴新技术。\n\n你现在已经学得足够多，可以开始自己的游戏了。如果你需要更多关于特定主题的信息，还有很多其他的高级书籍和网站可以看，但是你应该对你现在正在看的东西有一个更好的想法。\n\n我希望你旅途愉快。祝你未来的项目好运！"
  },
  {
    "path": "docs/learn-cpp-build-game-ue4/README.md",
    "content": "# 通过使用 UE4 构建游戏学习 C++\n\n> 原书：[Learning C++ by Building Games with Unreal Engine 4](https://libgen.rs/book/index.php?md5=1C4190D0F9858DF324374DCAE7B4DD27)\n> \n> 协议：[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)\n> \n> 阶段：机翻（1）\n> \n> 自豪地采用[谷歌翻译](https://translate.google.cn/)\n> \n> 让开源界提前感受入关。\n\n* [在线阅读](https://ccpp.apachecn.org)\n* [在线阅读（Gitee）](https://apachecn.gitee.io/apachecn-c-cpp-zh/)\n* [ApacheCN 学习资源](http://docs.apachecn.org/)\n\n## 贡献指南\n\n本项目需要校对，欢迎大家提交 Pull Request。\n\n> 请您勇敢地去翻译和改进翻译。虽然我们追求卓越，但我们并不要求您做到十全十美，因此请不要担心因为翻译上犯错——在大部分情况下，我们的服务器已经记录所有的翻译，因此您不必担心会因为您的失误遭到无法挽回的破坏。（改编自维基百科）\n\n## 联系方式\n\n### 负责人\n\n* [飞龙](https://github.com/wizardforcel): 562826179\n\n### 其他\n\n*   在我们的 [apachecn/apachecn-c-cpp-zh](https://github.com/apachecn/apachecn-c-cpp-zh) github 上提 issue.\n*   发邮件到 Email: `apachecn@163.com`.\n*   在我们的 [组织学习交流群](http://www.apachecn.org/organization/348.html) 中联系群主/管理员即可.\n\n## 赞助我们\n\n![](http://data.apachecn.org/img/about/donate.jpg)\n"
  },
  {
    "path": "docs/learn-cpp-build-game-ue4/SUMMARY.md",
    "content": "+   [通过使用 UE4 构建游戏学习 C++](README.md)\n+   [零、前言](00.md)\n+   [一、C++ 17 入门](01.md)\n+   [二、变量和内存](02.md)\n+   [三、`if...else`和`switch`](03.md)\n+   [四、循环](04.md)\n+   [五、函数和宏](05.md)\n+   [六、对象、类和继承](06.md)\n+   [七、动态存储分配](07.md)\n+   [八、演员和棋子](08.md)\n+   [九、模板和常用容器](09.md)\n+   [十、库存系统和提取项目](10.md)\n+   [十一、怪物](11.md)\n+   [十二、使用高级人工智能构建更聪明的怪物](12.md)\n+   [十三、咒语书](13.md)\n+   [十四、利用 UMG 和音频改善用户界面反馈](14.md)\n+   [十五、虚拟现实及其他](15.md)\n"
  },
  {
    "path": "docs/learn-cpp-func-prog/0.md",
    "content": "# 零、前言\n\n函数式编程是一种通过组合纯函数来构建计算机程序元素和结构的方式，避免共享状态、可变数据和副作用，就像我们通常在数学中看到的那样。代码函数中的变量表示函数参数的值，它类似于数学函数。其思想是程序员定义包含表达式、定义和参数的函数，这些函数可以通过变量来表达以解决问题。\n函数式编程是声明性的，而不是命令性的，这意味着编程是用表达式或声明而不是语句来完成的。函数式编程的应用状态流过纯函数，避免了副作用。与命令式编程相反，应用状态通常与对象中的方法共享和搭配。在命令式编程中，表达式被求值，结果值被赋给变量。例如，当我们将一系列表达式组合成一个函数时，结果值取决于变量在该时间点的状态。由于状态的不断变化，求值的顺序至关重要。在函数式编程中，破坏性赋值是被禁止的，每次赋值发生时，都会引入一个新的变量。最棒的是，函数式代码往往比命令式或面向对象的代码更简洁、更可预测、更容易测试。\n虽然有一些专门为函数式编程设计的语言，比如 Haskell 和 Scala，但是我们也可以使用 C++ 来完成函数式编程的设计，这一点我们将在本书通篇讨论。\n\n# 这本书涵盖了什么\n\n[第 1 章](1.html)、*潜入现代 C++* ，概述现代 C++，包括现代 C++ 中几个新特性的实现，如 auto 关键字、decltype 关键字、null 指针、基于范围的 for 循环、标准模板库、Lambda 表达式、智能指针和元组。\n\n[第 2 章](2.html)、*在函数式编程中操纵函数*，涵盖了函数式编程中操纵函数的基本技术；它们是一流的函数技巧、纯函数和 curry 技巧。通过应用第一类函数，我们可以将我们的函数视为数据，这意味着它可以被分配给任何变量，而不仅仅是作为函数调用。我们还将应用纯函数技术，这样函数就不会再产生副作用。此外，为了简化函数，我们可以应用 currying 技术，这将通过求值每个函数中具有单个参数的函数序列来减少多参数函数。\n\n[第 3 章](3.html)、*将不可变状态应用于函数*，解释了我们如何为可变对象实现不可变对象。我们还将深入研究第一类函数和纯函数(我们在上一章中讨论过)，以生成一个不可变的对象。\n\n[第 4 章](4.html)、*使用递归算法重复方法调用*，讨论了迭代和递归的区别以及为什么递归技术更适合函数式编程。我们还将列举三种递归:函数递归、过程递归和回溯递归。\n\n[第 5 章](5.html)、*使用延迟求值*来拖延执行过程，解释了如何延迟执行过程以获得更高效的代码。我们还将实现缓存和记忆技术，以使我们的代码运行得更快。\n\n[第六章](6.html)、*用元编程优化代码*讲的是用元编程优化代码，用编译时执行运行代码。我们还将讨论如何将流控制重构为模板元编程。\n\n[第 7 章](7.html)、*使用并发*运行并行执行，引导我们在 C++ 编程中运行多个线程，以及同步线程以避免死锁。我们还将在 Windows 操作系统中应用线程处理。\n\n[第 8 章](8.html)、*创建和调试函数式方法中的应用*，阐述了我们在前面章节中讨论的设计函数式编程的所有技术。此外，如果出现意外结果或程序在执行过程中崩溃，我们将尝试调试代码以找到解决方案。\n\n# 这本书你需要什么\n\n要浏览本书并成功编译所有源代码示例，您需要一台运行微软视窗 8.1(或更高版本)并包含以下软件的个人计算机:\n\n*   GCC 的最新版本，支持 C++ 11、C++ 14 和 C++ 17(在撰写本书期间，最新版本是 GCC v7.1.0)\n*   微软 Visual Studio 2017 中提供的微软 C++ 编译器，用于支持 C++ 11、C++ 14 和 C++ 17(针对[第 7 章](7.html)、*使用并发*运行并行执行)\n*   代码::Blocks v16.01(所有示例代码都是使用代码::Blocks IDE 编写的；但是，使用这个 IDE 是可选的)\n\n# 这本书是给谁的\n\n这本书是为熟悉面向对象的 C++ 开发人员编写的，他们有兴趣学习如何应用功能范例来创建健壮且可测试的应用。\n\n# 约定\n\n在这本书里，你会发现许多区分不同种类信息的文本样式。以下是这些风格的一些例子和对它们的意义的解释。\n\n文本中的码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、伪 URL 和用户输入如下所示:“还可以将`auto`关键字应用于函数，以自动推导函数的返回类型。”\n\n代码块设置如下:\n\n```cpp\n    int add(int i, int j)\n    {\n      return i + j;\n    }\n\n```\n\n当我们希望将您的注意力吸引到代码块的特定部分时，相关的行或项目以粗体显示:\n\n```cpp\n    // Initializing a string variable\n Name n = {\"Frankie Kaur\"};\n       cout << \"Initial name = \" << n.str;\n       cout << endl; \n\n```\n\n**新名词**和**重要词语**以粗体显示。\n\nWarnings or important notes appear like this. Tips and tricks appear like this.\n\n# 读者反馈\n\n我们随时欢迎读者的反馈。让我们知道你对这本书的看法——你喜欢或不喜欢什么。读者反馈对我们来说很重要，因为它有助于我们开发出你真正能从中获益的标题。\n\n要向我们发送一般反馈，只需发送电子邮件[feedback@packtpub.com](mailto:feedback@packtpub.com)，并在您的邮件主题中提及书名。\n\n如果你对某个主题有专业知识，并且对写作或投稿感兴趣，请参见我们位于[www.packtpub.com/authors](http://www.packtpub.com/authors)的作者指南。\n\n# 客户支持\n\n现在，您已经自豪地拥有了一本书，我们有许多东西可以帮助您从购买中获得最大收益。\n\n# 下载示例代码\n\n你可以从你在[http://www.packtpub.com](http://www.packtpub.com)的账户下载这本书的示例代码文件。如果您在其他地方购买了这本书，您可以访问[http://www.packtpub.com/support](http://www.packtpub.com/support)并注册，以便将文件直接通过电子邮件发送给您。\n\n您可以按照以下步骤下载代码文件:\n\n1.  使用您的电子邮件地址和密码登录或注册我们的网站。\n2.  将鼠标指针悬停在顶部的“支持”选项卡上。\n3.  点击代码下载和勘误表。\n4.  在搜索框中输入图书的名称。\n5.  选择要下载代码文件的书籍。\n6.  从您购买这本书的下拉菜单中选择。\n7.  点击代码下载。\n\n下载文件后，请确保使用最新版本的解压缩文件夹:\n\n*   视窗系统的 WinRAR / 7-Zip\n*   zipeg/izp/un ARX for MAC\n*   适用于 Linux 的 7-Zip / PeaZip\n\n这本书的代码包也托管在 GitHub 上的[https://GitHub . com/packt publishing/learning cppffunctionalprograming](https://github.com/PacktPublishing/LearningCPPFunctionalProgramming)。我们还有来自丰富的图书和视频目录的其他代码包，可在[https://github.com/PacktPublishing/](https://github.com/PacktPublishing/)获得。看看他们！\n\n# 下载这本书的彩色图片\n\n我们还为您提供了一个 PDF 文件，其中包含本书中使用的截图/图表的彩色图像。彩色图像将帮助您更好地理解输出中的变化。您可以从[https://www . packtpub . com/sites/default/files/downloads/learning cppffunctionalprogramming _ color images . pdf](https://www.packtpub.com/sites/default/files/downloads/LearningCPPFunctionalProgramming_ColorImages.pdf)下载此文件。\n\n# 正误表\n\n尽管我们尽了最大努力来确保我们内容的准确性，但错误还是会发生。如果你在我们的某本书里发现一个错误，也许是文本或代码中的错误，如果你能向我们报告，我们将不胜感激。通过这样做，你可以让其他读者免受挫折，并帮助我们改进这本书的后续版本。如果您发现任何勘误表，请访问[http://www.packtpub.com/submit-errata](http://www.packtpub.com/submit-errata)，选择您的书籍，点击勘误表提交表格链接，并输入您的勘误表的详细信息。一旦您的勘误表得到验证，您的提交将被接受，勘误表将上传到我们的网站或添加到该标题勘误表部分下的任何现有勘误表列表中。\n\n要查看之前提交的勘误表，请前往[https://www.packtpub.com/books/content/support](https://www.packtpub.com/books/content/support)并在搜索栏中输入图书名称。所需信息将出现在勘误表部分。\n\n# 海盗行为\n\n互联网上版权材料的盗版是所有媒体的一个持续问题。在 Packt，我们非常重视版权和许可证的保护。如果您在互联网上遇到任何形式的我们作品的非法拷贝，请立即向我们提供位置地址或网站名称，以便我们寻求补救。\n\n请通过[copyright@packtpub.com](mailto:copyright@packtpub.com)联系我们，获取疑似盗版材料的链接。\n\n我们感谢您在保护我们的作者方面的帮助，以及我们为您带来有价值内容的能力。\n\n# 问题\n\n如果您对本书的任何方面有问题，可以联系我们[questions@packtpub.com](mailto:questions@packtpub.com)，我们将尽最大努力解决问题。"
  },
  {
    "path": "docs/learn-cpp-func-prog/1.md",
    "content": "# 一、深入现代 C++\n\n自 1979 年发明以来，C++ 编程语言发生了巨大的变化。这个时代的一些人可能有点害怕使用 C++ 语言进行编码，因为它对用户不友好。我们要处理的内存管理有时会让人不愿意使用这种语言。幸运的是，自从 **C++ 11** -也被称为**现代 C++** ，以及 **C++ 14** 和 **C++ 17** -发布以来，已经引入了许多特性来简化我们在 C++ 语言中的代码。此外，它最好的部分是 C++ 编程语言是任何项目的伟大语言，从低级编程到 web 编程，以及函数式编程。\n\n这一章是我们在这本书里开始旅程的最好的地方，因为它是针对 C++ 程序员的，以刷新他们的知识，并将讨论以下主题:\n\n*   理解现代 C++ 的几个新特性\n*   在现代 C++ 中实现 C++ 标准库\n*   Lambda 表达式的使用以及 C++ Lambda 中包含的所有特性\n*   使用智能指针避免手动内存管理\n*   使用元组处理许多返回值\n\n# 更接近现代 C++ 的几个新特性\n\n那么，与旧的 C++ 相比，现代 c++ 有什么新的地方呢？现代 C++ 与旧的相比有如此多的变化，如果我们讨论所有的变化，书的页数将会大大增加。然而，我们将讨论现代 C++ 中的新特性，我们应该了解这些特性，以使我们在编码活动中更有效率。我们将讨论几个新的关键词，如`auto`、`decltype`、`nullptr`。我们还将讨论`begin()`和`end()`功能的增强，该功能现已成为非成员类功能。我们还将讨论对`for-each`技术的增强支持，以使用`range-based for loop`技术迭代集合。\n\n本章接下来的几个小节还将讨论现代 C++ 的新特性，即 Lambda 表达式、智能指针和元组，它们刚刚在 C++ 11 版本中添加。\n\n# 使用 auto 关键字自动定义数据类型\n\n在现代 C++ 之前，C++ 语言有一个名为`auto`的关键字，用来明确指定变量应该有**自动持续时间**。附着在变量上的自动持续时间将在定义点(和初始化，如果相关的话)创建变量，并在定义它们的块退出时销毁变量。例如，局部变量将在函数开始定义时被创建，当程序退出局部变量所在的函数时被销毁。\n\n从 C++ 11 开始，`auto`关键字被用来告诉编译器从其初始化器中推导出正在声明的变量的实际类型。并且由于 C++ 14，关键字也可以应用于函数，以指定作为尾随返回类型的函数的返回类型。现在，在现代 C++ 中，使用`auto`关键字来指定自动持续时间被取消，因为默认情况下所有变量都被设置为自动持续时间。\n\n下面是一个`auto.cpp`代码，演示了`auto`关键字在变量中的使用。我们将使用`auto`关键字定义四个变量，然后使用`typeid()`函数找出每个变量的数据类型。让我们来看看:\n\n```cpp\n    /* auto.cpp */\n\n    #include <iostream>\n    #include <typeinfo>\n\n    int main()\n    {\n      std::cout << \"[auto.cpp]\" << std::endl;\n\n      // Creating several auto-type variables\n      auto a = 1;\n      auto b = 1.0;\n      auto c = a + b;\n      auto d = {b, c};\n\n      // Displaying the preceding variables' type\n      std::cout << \"type of a: \" << typeid(a).name() << std::endl;\n      std::cout << \"type of b: \" << typeid(b).name() << std::endl;\n      std::cout << \"type of c: \" << typeid(c).name() << std::endl;\n      std::cout << \"type of d: \" << typeid(d).name() << std::endl;\n      return 0;\n    }\n\n```\n\n正如我们在前面的代码中看到的，我们有一个存储`integer`值的`a`变量和一个存储`double`值的`b`变量。我们计算`a`和`b`的相加，并将结果存储在变量`c`中。在这里，我们预计`c`将存储`double`对象，因为我们添加了`integer`和`double`对象。最后一个是`d`变量，它将存储`initializer_list<double>`数据类型。当我们运行前面的代码时，我们将在控制台上看到以下输出:\n\n![](img/5d5e1164-6f13-4afb-9011-c6444f0c3589.png)\n\n从前面的快照中可以看到，我们只是得到了数据类型的第一个字符，比如`i`代表`integer`、`d`代表`double`、`St16initializer_listIdE`代表`initializer_list<double>`，也就是代表`double`的最后一个小写`d`字符。\n\nWe may have to enable the **Run-Time Type Information** (**RTTI**) feature in our compiler options to retrieve the data type object. However, GCC has enabled the feature by default. Also, the output of the use of the `typeid()` function depends on the compiler. We may get the raw type name or just a symbol as we did in the preceding example.\n\n此外，对于变量，如前所述，`auto`关键字也可以应用于函数，自动推导函数的返回类型。假设我们有以下名为`add()`的平凡函数来计算两个参数的相加:\n\n```cpp\n    int add(int i, int j)\n    {\n      return i + j;\n    }\n\n```\n\n我们可以重构前面的方法来使用`auto`关键字，正如我们在下面几行代码中看到的:\n\n```cpp\n    auto add(int i, int j)\n    {\n      return i + j;\n    }\n\n```\n\n类似于自动类型变量，编译器可以根据函数的返回值决定正确的返回类型。并且，如前面的代码所示，由于我们只添加了两个整数值，因此该函数确实会返回整数值。\n\n在现代 C++ 中使用`auto`关键字的另一个特性是尾随返回类型语法。通过使用这个特性，我们可以指定返回类型、函数原型的其余部分或函数签名。从前面的代码中，我们可以重构它以使用如下特性:\n\n```cpp\n    auto add(int i, int j) -> int\n    {\n      return i + j;\n    }\n\n```\n\n你可能会问我，为什么我们要在箭头符号(`->`)之后再次指定数据类型，尽管我们已经使用了`auto`关键字。我们将在下一节讨论`decltype`关键词时找到答案。此外，通过使用这个特性，我们现在可以通过修改`main()`方法的语法来重构前面的`auto.cpp`代码，而不是下面的`main()`函数签名的语法:\n\n```cpp\n    int main()\n    {\n      // The body of the function\n    }\n\n```\n\n我们可以将签名语法更改为以下代码行:\n\n```cpp\n    auto main -> int\n    {\n      // The body of the function\n    }\n\n```\n\n现在，我们将在本书中看到我们所有的代码都使用这个尾随返回类型特性来应用现代 C++ 语法。\n\n# 使用 decltype 关键字查询表达式的类型\n\n我们在上一节中讨论了`auto`关键字可以根据变量存储的值的类型自动推断变量的类型。关键字还可以根据返回值的类型推断函数的返回类型。现在，让我们结合`auto`关键词和`decltype`关键词，获得现代 C++ 的力量。\n\n在我们结合这两个关键词之前，我们会发现`decltype`关键词是用来做什么的——它是用来询问一个对象或者一个表达式的类型。让我们看看下面几行琐碎的变量声明:\n\n```cpp\n    const int func1();\n    const int& func2();\n    int i;\n\n    struct X { double d; };\n    const X* x = new X();\n\n```\n\n现在，基于前面的代码，我们可以使用`decltype`关键字声明其他变量，如下所示:\n\n```cpp\n    // Declaring const int variable\n    // using func1() type\n    decltype(func1()) f1;\n\n    // Declaring const int& variable\n    // using func2() type\n    decltype(func2()) f2;\n\n    // Declaring int variable\n    // using i type\n    decltype(i) i1;\n\n    // Declaring double variable\n    // using struct X type\n    decltype(x->d) d1; // type is double\n    decltype((x->d)) d2; // type is const double&\n\n```\n\n正如我们在前面的代码中看到的，我们可以基于另一个对象的类型来指定一个对象的类型。现在，让我们假设我们需要重构前面的`add()`方法成为一个模板。没有`auto`和`decltype`关键字，我们将有以下模板实现:\n\n```cpp\n    template<typename I, typename J, typename K>\n    K add(I i, J j)\n    {\n      return i + j;\n    }\n\n```\n\n幸运的是，由于`auto`关键字可以指定函数的返回类型，这是一个尾随返回类型，而`decltype`关键字可以根据表达式推导出类型，所以我们可以如下重构前面的模板:\n\n```cpp\n    template<typename I, typename J>\n    auto add(I i, J j) -> decltype(i + j)\n    {\n      return i + j;\n    }\n\n```\n\n为了证明，让我们编译并运行以下`decltype.cpp`代码。我们将使用以下模板来计算两个不同值类型的加法- `integer`和`double`:\n\n```cpp\n    /* decltype.cpp */\n    #include <iostream>\n\n    // Creating template\n    template<typename I, typename J>\n    auto add(I i, J j) -> decltype(i + j)\n    {\n      return i + j;\n    }\n\n    auto main() -> int\n    {\n      std::cout << \"[decltype.cpp]\" << std::endl;\n\n      // Consuming the template\n      auto d = add<int, double>(2, 2.5);\n\n      // Displaying the preceding variables' type\n      std::cout << \"result of 2 + 2.5: \" << d << std::endl;\n\n      return 0;\n    }\n\n```\n\n编译过程应该平稳运行，没有错误。如果运行前面的代码，我们将在屏幕上看到以下输出:\n\n![](img/9b573b77-6cc6-41a6-94cb-8fc04dd314c8.png)\n\n正如我们所看到的，我们已经成功地组合了`auto`和`decltype`关键字，创建了一个比现代 C++ 宣布之前我们通常做的更简单的模板。\n\n# 指向空指针\n\n现代 C++ 的另一个新特性是一个名为`nullptr`的关键字，它取代了`NULL`宏来表示空指针。现在，使用`NULL`宏来表示零数字或空指针没有任何歧义。假设我们的声明中有以下两个方法的签名:\n\n```cpp\n    void funct(const char *);\n    void funct(int)\n\n```\n\n前者将传递一个指针作为参数，后者将传递整数作为参数。然后调用`funct()`方法，传递`NULL`宏作为参数，如下图:\n\n```cpp\n    funct(NULL);\n\n```\n\n我们打算称之为前一个函数。但是由于我们传递的是`NULL`参数，基本定义为`0`，后面的函数将被调用。在现代 C++ 中，我们可以使用`nullptr`关键字来确保我们将传递一个空指针给参数。`funct()`方法的调用如下:\n\n```cpp\n    funct(nullptr);\n\n```\n\n现在编译器将调用前一个函数，因为它向参数传递了一个空指针，这就是我们所期望的。不会再有歧义了，也避免了以后不必要的问题。\n\n# 使用非成员 begin()和 end()函数返回迭代器\n\n在现代 C++ 之前，为了迭代一个序列，我们调用每个容器的`begin()`和`end()`成员方法。对于数组，我们可以通过迭代索引来迭代它的元素。从 C++ 11 开始，该语言有一个非成员函数- `begin()`和`end()` -来检索序列的迭代器。假设我们有一个由以下元素组成的数组:\n\n```cpp\n    int arr[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };\n\n```\n\n当语言没有`begin()`和`end()`函数时，我们需要使用我们可以在下面几行代码中看到的索引来迭代数组的元素:\n\n```cpp\n    for (unsigned int i = 0; i < sizeof(arr)/sizeof(arr[0]); ++ i)\n    // Do something to the array\n\n```\n\n幸运的是，使用`begin()`和`end()`函数，我们可以将前面的`for`循环重构如下:\n\n```cpp\n    for (auto i = std::begin(arr); i != std::end(arr); ++ i)\n    // Do something to the array\n\n```\n\n我们可以看到，`begin()`和`end()`函数的使用创建了一个紧凑的代码，因为我们不需要担心数组的长度，因为`begin()`和`end()`的迭代器指针会为我们做这件事。为了比较，我们来看看下面的`begin_end.cpp`代码:\n\n```cpp\n    /* begin_end.cpp */\n    #include <iostream>\n\n    auto main() -> int\n    {\n      std::cout << \"[begin_end.cpp]\" << std::endl;\n\n      // Declaring an array\n      int arr[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };\n\n      // Displaying the array elements\n      // using conventional for-loop\n      std::cout << \"Displaying array element using conventional for-\n       loop\";\n      std::cout << std::endl;\n      for (unsigned int i = 0; i < sizeof(arr)/sizeof(arr[0]); ++ i)\n      std::cout << arr[i] << \" \";\n      std::cout << std::endl;\n\n      // Displaying the array elements\n      // using non-member begin() and end()\n      std::cout << \"Displaying array element using non-member begin()\n       and end()\";\n      std::cout << std::endl;\n      for (auto i = std::begin(arr); i != std::end(arr); ++ i)\n       std::cout << *i << \" \";\n      std::cout << std::endl;\n\n      return 0;\n    }\n\n```\n\n为了证明前面的代码，我们可以编译代码，当我们运行它时，控制台屏幕上将显示以下输出:\n\n![](img/3219ad51-9855-43ce-b596-536273d9c047.png)\n\n正如我们在截图中看到的，当我们使用传统的`for-loop`或`begin()`和`end()`函数时，我们得到了完全相同的输出。\n\n# 使用基于范围的 for 循环迭代集合\n\n在现代 C++ 中，有一个新的特性被扩充以支持迭代集合的`for-each`技术。如果您想对集合或数组的元素做些什么，而不考虑元素的数量或索引，那么这个特性非常有用。该功能的语法也很简单。假设我们有一个名为`arr`的数组，我们希望使用`range-based for loop`技术迭代每个元素；我们可以使用以下语法:\n\n```cpp\n    for (auto a : arr)\n    // Do something with a\n\n```\n\n因此，我们可以重构前面的`begin_end.cpp`代码来使用`range-based for loop`，如下面的代码所示:\n\n```cpp\n    /* range_based_for_loop.cpp */\n    #include <iostream>\n\n    auto main() -> int\n    {\n      std::cout << \"[range_based_for_loop.cpp]\" << std::endl;\n\n      // Declaring an array\n      int arr[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};\n\n      // Displaying the array elements\n      // using non-member begin() and end()\n      std::cout << \"Displaying array element using range-based for\n        loop\";\n      std::cout << std::endl;\n      for (auto a : arr) std::cout << a << \" \";\n      std::cout << std::endl;\n\n      return 0;\n    }\n\n```\n\n我们在前面代码中看到的语法现在更简单了。如果我们编译前面的代码，应该不会发现错误，如果我们运行代码，应该会在控制台屏幕上看到以下输出:\n\n![](img/3f3f2d37-a65c-491d-86e6-c0cc8523e391.png)\n\n我们现在有了一种新的技术来迭代集合，而不用考虑集合的索引。我们将在这本书里继续使用它。\n\n# 利用 C++ 标准库使用 C++ 语言\n\nC++ 标准库是一组强大的类和函数，具有创建应用所需的许多功能。它们由 C++ ISO 标准委员会控制，并受**标准模板库** ( **STL** )的影响，这是 C++ 11 推出之前的通用库。标准库中的所有特性都在`std namespace`中声明，不再有以`.h`结尾的头(除了并入 C++ 标准库中的 ISO C90 C 标准库的 18 个头)。\n\n有几个头文件包含 C++ 标准库的声明。然而，几乎不可能在这些小章节中讨论所有的头文件。因此，我们将讨论一些在日常编码活动中最常用的特性。\n\n# 在容器中放置任何物体\n\n**容器**是一个对象，用于存储其他对象并管理其包含的对象所使用的内存。数组是 C++ 11 中添加的一项新功能，用于存储特定数据类型的集合。它是一个序列容器，因为它存储相同的数据类型对象并线性排列它们。让我们看看下面的代码片段:\n\n```cpp\n    /* array.cpp */\n    #include <array>\n    #include <iostream>\n\n    auto main() -> int\n    {\n      std::cout << \"[array.cpp]\" << std::endl;\n\n      // Initializing an array containing five integer elements\n      std::array<int, 10> arr = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };\n\n      // Displaying the original elements of the array\n      std::cout << \"Original Data : \";\n      for(auto a : arr) std::cout << a << \" \";\n      std::cout << std::endl;\n\n      // Modifying the content of\n      // the 1st and 3rd element of the array\n      arr[1] = 9;\n      arr[3] = 7;\n\n      // Displaying the altered array elements\n      std::cout << \"Manipulated Data: \";\n      for(auto a : arr) std::cout << a << \" \";\n      std::cout << std::endl;\n\n      return 0;\n     }\n\n```\n\n正如我们在前面的代码中看到的，我们实例化了一个名为`arr`的新数组，将其长度设置为`10`，并且只批准了`int`元素。我们可以猜测，代码的输出是一行数字`0`到`9`，显示在原始数据中，另一行将显示修改后的数据，如下图截图所示:\n\n![](img/9bf41be3-c581-4cf9-87d9-be35bc06f403.png)\n\nThere is no performance issue if we declare an array using `std::array`; we use in the `array.cpp` code and compare it with a usual array as we use in the `begin_end.cpp` code. However, in modern C++, we are given a new array declaration that has a friendly value semantic, so that it can be passed to or returned from functions by value. Also, the interface of this new array declaration makes it more convenient to find the size, and use it with **Standard Template Library** (**STL**)-style iterator-based algorithms.\n\n使用数组作为容器是很好的，因为我们可以存储数据并操作它们。如果我们愿意，我们也可以分类并找到特定的元素。然而，由于数组是一个编译时不可调整大小的对象，我们必须在一开始就决定我们打算使用的数组的大小，因为我们不能在以后更改大小。换句话说，我们不能在现有数组中插入或移除元素。作为这个问题的解决方案，也是使用容器的最佳实践，我们现在可以使用`vector`来存储我们的收藏。让我们看看下面的代码:\n\n```cpp\n    /* vector.cpp */\n    #include <vector>\n    #include <iostream>\n\n    auto main() -> int\n    {\n      std::cout << \"[vector.cpp]\" << std::endl;\n\n      // Initializing a vector containing three integer elements\n      std::vector<int> vect = { 0, 1, 2 };\n\n      // Displaying the original elements of the vector\n      std::cout << \"Original Data : \";\n      for (auto v : vect) std::cout << v << \" \";\n      std::cout << std::endl;\n\n      // Adding two new data\n      vect.push_back(3);\n      vect.push_back(4);\n\n      // Displaying the elements of the new vector\n      // and reverse the order\n      std::cout << \"New Data Added : \";\n      for (auto v : vect) std::cout << v << \" \";\n      std::cout << std::endl;\n\n      // Modifying the content of\n      // the 2nd and 4th element of the vector\n      vect.at(2) = 5;\n      vect.at(4) = 6;\n\n      // Displaying the altered array elements\n      std::cout << \"Manipulate Data: \";\n      for (auto v : vect) std::cout << v << \" \";\n      std::cout << std::endl;\n\n      return 0;\n    }\n\n```\n\n现在，我们在前面的代码中有一个`vector`实例，而不是`array`实例。如我们所见，我们使用`push_back()`方法为`vector`实例赋予了一个附加值。我们可以随时增加价值。每个元素的操作也更容易，因为`vector`有一个`at()`方法，返回对特定索引元素的引用。下面的截图是我们在运行代码时将看到的输出:\n\n![](img/d6c12da0-75ee-450a-b324-a86e4bf59dad.png)\n\nIt is better to always use the `at()` method instead of the `[]` operator when we want to access the specific element by its index in a `vector` instance. It's because, when we accidentally access the out of range position, the `at()` method will throw an `out_of_range` exception. Otherwise, the `[]` operator will give undefined behavior.\n\n# 使用算法\n\n我们可以在`array`或`vector`中对集合的元素进行排序，也可以找到元素的具体内容。出于这些目的，我们必须使用 C++ 标准库提供的算法特性。让我们看看下面的代码，演示算法特性中的排序元素功能:\n\n```cpp\n    /* sort.cpp */\n    #include <vector>\n    #include <algorithm>\n    #include <iostream>\n\n    bool comparer(int a, int b)\n    {\n      return (a > b);\n    }\n\n    auto main() -> int\n    {\n      std::cout << \"[sort.cpp]\" << std::endl;\n\n      // Initializing a vector containing several integer elements\n      std::vector<int> vect = { 20, 43, 11, 78, 5, 96 };\n\n      // Displaying the original elements of the vector\n      std::cout << \"Original Data : \";\n      for (auto v : vect)\n      std::cout << v << \" \";\n      std::cout << std::endl;\n\n      // Sorting the vector element ascending\n      std::sort(std::begin(vect), std::end(vect));\n\n      // Displaying the ascending sorted elements\n      // of the vector\n      std::cout << \"Ascending Sorted : \";\n      for (auto v : vect)\n      std::cout << v << \" \";\n      std::cout << std::endl;\n\n      // Sorting the vector element descending\n      // using comparer\n      std::sort(std::begin(vect), std::end(vect), comparer);\n\n      // Displaying the descending sorted elements\n      // of the vector\n      std::cout << \"Descending Sorted: \";\n      for (auto v : vect)\n      std::cout << v << \" \";\n      std::cout << std::endl;\n\n      return 0;\n   }\n\n```\n\n正如我们在前面的代码中看到的，我们调用了`sort()`方法两次。首先，我们只是提供了我们想要排序的元素的范围。然后我们添加了比较函数`comparer()`，提供给`sort()`方法，以获得该方法更大的灵活性。我们将在控制台上看到前面代码的输出如下:\n\n![](img/a39ab3f7-55b8-4dda-8f2f-4d253d92f0d9.png)\n\n从前面的截图中，我们可以看到我们在开始的一个`vector`中有六个元素。然后，我们使用简单的`sort()`方法对向量的元素进行排序。然后，我们再次调用`sort()`方法，但是我们现在向`sort()`方法提供`comparer()`而不是简单的`sort()`方法。因此，向量元素将向下排序，因为`comparer()`函数从两个输入中寻找更大的值。\n\n现在，让我们转到算法特性的另一个功能，即查找特定元素。假设我们的代码中有`Vehicle`类。它有两个名为`m_vehicleType`和`m_totalOfWheel`的私有字段，我们可以分别从名为`GetType()`和`GetNumOfWheel()`的 getter 方法中检索值。它还有两个构造函数，即默认构造函数和用户定义的构造函数。类的声明应该如下:\n\n```cpp\n    /* vehicle.h */\n    #ifndef __VEHICLE_H__\n    #define __VEHICLE_H__\n\n    #include <string>\n\n    class Vehicle\n    {\n      private:\n        std::string vehicleType;\n        int totalOfWheel;\n\n      public:\n        Vehicle(\n          const std::string &type,\n          int _wheel);\n        Vehicle();\n        ~Vehicle();\n        std::string GetType() const {return vehicleType;}\n        int GetNumOfWheel() const {return totalOfWheel;}\n    };\n\n    #endif // End of __VEHICLE_H__\n\n```\n\n`Vehicle`类的实现如下:\n\n```cpp\n    /* vehicle.cpp */\n    #include \"vehicle.h\"\n\n    using namespace std;\n\n    // Constructor with default value for\n    // m_vehicleType and m_totalOfWheel\n    Vehicle::Vehicle() : m_totalOfWheel(0)\n    {\n    }\n\n    // Constructor with user-defined value for\n    // m_vehicleType and m_totalOfWheel\n    Vehicle::Vehicle( const string &type, int wheel) :\n     m_vehicleType(type),\n     m_totalOfWheel(wheel)\n    {\n    }\n\n    // Destructor\n    Vehicle::~Vehicle()\n    {\n    }\n\n```\n\n我们将在`vector`容器中存储`Vehicle`的集合，然后我们将根据其属性搜索一些元素。代码如下:\n\n```cpp\n    /* find.cpp */\n    #include <vector>\n    #include <algorithm>\n    #include <iostream>\n    #include \"../vehicle/vehicle.h\"\n\n    using namespace std;\n\n    bool TwoWheeled(const Vehicle &vehicle)\n    {\n      return _vehicle.GetNumOfWheel() == 2 ? \n        true : false;\n     }\n\n    auto main() -> int\n    {\n      cout << \"[find.cpp]\" << endl;\n\n      // Initializing several Vehicle instances\n      Vehicle car(\"car\", 4);\n      Vehicle motorcycle(\"motorcycle\", 2);\n      Vehicle bicycle(\"bicycle\", 2);\n      Vehicle bus(\"bus\", 6);\n\n      // Assigning the preceding Vehicle instances to a vector\n      vector<Vehicle> vehicles = { car, motorcycle, bicycle, bus };\n\n      // Displaying the elements of the vector\n      cout << \"All vehicles:\" << endl;;\n      for (auto v : vehicles)\n        std::cout << v.GetType() << endl;\n      cout << endl;\n\n      // Displaying the elements of the vector\n      // which are the two-wheeled vehicles\n      cout << \"Two-wheeled vehicle(s):\" << endl;;\n      auto tw = find_if(\n                      begin(vehicles),\n                      end(vehicles),\n                      TwoWheeled);\n      while (tw != end(vehicles))\n      {\n        cout << tw->GetType() << endl ;\n        tw = find_if(++ tw, end(vehicles), TwoWheeled);\n      }\n      cout << endl;\n\n      // Displaying the elements of the vector\n      // which are not the two-wheeled vehicles\n      cout << \"Not the two-wheeled vehicle(s):\" << endl;;\n      auto ntw = find_if_not(begin(vehicles),\n                           end(vehicles),\n                           TwoWheeled);\n      while (ntw != end(vehicles))\n      {\n        cout << ntw->GetType() << endl ;\n        ntw = find_if_not(++ ntw, end(vehicles), TwoWheeled);\n      }\n\n      return 0;\n     }\n\n```\n\n如我们所见，我们实例化四个`Vehicle`对象，然后将它们存储在`vector`中。在那里，我们试图找到有两个轮子的车辆。`find_if()`功能用于此目的。我们还有`TwoWheeled()`方法提供比较值。由于我们正在寻找两轮车，我们将通过调用`GetNumOfWheel()`方法来检查`Vehicle`类中的`totalOfWheel`变量。相比之下，如果我们想找到不符合比较值的元素，可以使用 C++ 11 中添加的`find_if_not()`函数。我们得到的输出应该如下所示:\n\n![](img/7c5af549-86c7-4414-896e-e9fe96a9cb20.png)\n\nAs we can see in the `vehicle.cpp` code and `find.cpp` code, we now add the `using namespace std;` line in the `*.cpp` files. We do this to make our coding activity become more productive since we don't have to type many words. In contrast, in `vehicle.h`, we still using `std::` followed by the methods or properties name rather than use the std namespace at the beginning. It's best practice to not declare `using namespace` in header files since the header files are the files we will deliver if we create some libraries for instances. The user of our library may have another method with the same name as the function our library has. It will definitely create conflict between these two functions.\n\n我们最常用的另一个算法特性是`for_each`循环。使用`for_each`循环将使我们的代码在许多情况下更加简洁，而不是使用`for`循环。它也比`for`循环更简单，更不容易出错，因为我们可以为`for_each`循环定义一个特定的函数。现在让我们重构之前的代码，使用`for_each`循环。代码编写如下:\n\n```cpp\n    /* for_each.cpp */\n    #include <vector>\n    #include <algorithm>\n    #include <iostream>\n    #include \"vehicle.h\"\n\n    using namespace std;\n\n    void PrintOut(const Vehicle &vehicle)\n    {\n      cout << vehicle.GetType() << endl;\n    }\n\n    auto main() -> int\n   {\n      cout << \"[for_each.cpp]\" << endl;\n\n      // Initializing several Vehicle instances\n      Vehicle car(\"car\", 4);\n      Vehicle motorcycle(\"motorcycle\", 2);\n      Vehicle bicycle(\"bicycle\", 2);\n      Vehicle bus(\"bus\", 6);\n\n      // Assigning the preceding Vehicle instances to a vector\n      vector<Vehicle> vehicles = { car, motorcycle, bicycle, bus };\n\n      // Displaying the elements of the vector\n      cout << \"All vehicles:\" << endl;\n      for_each(begin(vehicles), end(vehicles), PrintOut);\n\n      return 0;\n    }\n\n```\n\n现在，通过`for_each`循环，我们有了更清晰的代码。我们只需要提供第一个和最后一个迭代器，然后传递一个函数——在这种情况下是`PrintOut()`函数——它将在范围内的每个元素中被调用。\n\n# 使用 Lambda 表达式简化函数符号\n\nLambda 表达式是一个匿名符号，表示执行操作或计算的东西。在函数式编程中，Lambda 表达式对于生成第一类和纯函数非常有用，我们将在本书的单独章节中讨论。现在，让我们通过研究 Lambda 表达式的三个基本部分来熟悉 C++ 11 中引入的这个新特性:\n\n*   捕获列表:[]\n*   参数列表: ()\n*   正文:{}\n\n这三个基本部分的顺序如下:\n\n```cpp\n    [](){} \n\n```\n\n捕获列表部分也用作标识 Lambda 表达式的标记。它是表达式中涉及的值的占位符。唯一的捕获默认值是&符号(`&`)，它将通过引用隐式捕获自动变量，以及等号(`=`)，它将通过复制隐式捕获自动变量(我们将在下一节中进一步讨论)。参数列表类似于每个函数中的捕获列表，我们可以将值传递给它。身体是功能本身的实现。\n\n# 对一个小函数使用 Lambda 表达式\n\n假设我们有一个只调用一次的微小的单行函数。如果我们在需要的时候直接写那个函数的操作就更好了。在前面的例子中，当讨论 C++ 标准库时，我们实际上有这个函数。回到`for_each.cpp`文件，我们会发现`for_each()`只调用了一次的`PrintOut()`函数。如果我们使用 Lambda，我们可以使这个`for_each`循环更易读。让我们看看下面的代码片段，看看我们如何重构`for_each.cpp`文件:\n\n```cpp\n    /* lambda_tiny_func.cpp */\n    #include <vector>\n    #include <algorithm>\n    #include <iostream>\n    #include \"../vehicle/vehicle.h\"\n\n    using namespace std;\n\n    auto main() -> int\n    {\n      cout << \"[lambda_tiny_func.cpp]\" << endl;\n\n      // Initializing several Vehicle instances\n      Vehicle car(\"car\", 4);\n      Vehicle motorcycle(\"motorcycle\", 2);\n      Vehicle bicycle(\"bicycle\", 2);\n      Vehicle bus(\"bus\", 6);\n\n      // Assigning the preceding Vehicle instances to a vector\n      vector<Vehicle> vehicles = { car, motorcycle, bicycle, bus };\n\n      // Displaying the elements of the vector\n      // using Lambda expression\n      cout << \"All vehicles:\" << endl;\n      for_each(\n             begin(vehicles),\n             end(vehicles),\n             [](const Vehicle &vehicle){\n                 cout << vehicle.GetType() << endl;\n            });\n\n      return 0;\n    }\n\n```\n\n正如我们所看到的，我们已经将在`for_each.cpp`文件中使用的`PrintOut()`函数转换成了一个 Lambda 表达式，并将其传递给了`for_each`循环。它确实会给出与`for_each.cpp`文件相同的输出。然而，现在我们的代码变得更加简洁易读。\n\n# 将 Lambda 表达式用于多行函数\n\nLambda 表达式也可以用于多行函数，所以我们可以将函数体放在上面。这也将使我们的代码更易读。让我们制定一个新的代码。在这段代码中，我们将有一个整数集合，并打算检查所选元素是否是质数。我们可以创建一个单独的函数，例如`PrintPrime()`，然后调用它。然而，由于质数检查操作只被调用一次，如果我们将其转换成一个 Lambda 表达式，它会更易读。代码应该如下所示:\n\n```cpp\n    /* lambda_multiline_func.cpp */\n    #include <vector>\n    #include <algorithm>\n    #include <iostream>\n\n    using namespace std;\n\n    auto main() -> int\n    {\n      cout << \"[lambda_multiline_func.cpp]\" << endl;\n\n      // Initializing a vector containing integer element\n      vector<int> vect;\n      for (int i = 0; i < 10; ++ i)\n        vect.push_back(i);\n\n      // Displaying whether or not the element is prime number\n      for_each(\n             begin(vect),\n             end(vect),\n             [](int n) {\n                cout << n << \" is\";\n                if(n < 2)\n                {\n                  if(n == 0)\n                  cout << \" not\";\n                }\n                else\n                {\n                  for (int j = 2; j < n; ++ j)\n                    {\n                       if (n % j == 0)\n                       {\n                         cout << \" not\";\n                         break;\n                       }\n                   }\n                 }\n\n                cout << \" prime number\" << endl;\n            });\n\n        return 0;\n     }\n\n```\n\n我们应该在屏幕上看到的输出如下:\n\n![](img/04d24940-d788-4a9d-a8cd-006f15cfc228.png)\n\n正如我们在前面的截图中看到的，我们已经通过使用 Lambda 表达式成功地识别了质数。\n\n# 从 Lambda 表达式中返回一个值\n\n我们前面的两个 Lambda 表达式示例只是为了在控制台上打印。这意味着函数不需要返回值。但是，如果我们在函数内部进行计算并返回计算结果，我们可以要求 Lambda 表达式为实例返回值。让我们看一下下面的代码来检查这个 Lambda 的使用:\n\n```cpp\n    /* lambda_returning_value.cpp */\n    #include <vector>\n    #include <algorithm>\n    #include <iostream>\n\n    using namespace std;\n\n    auto main() -> int\n    {\n      cout << \"[lambda_returning_value.cpp]\" << endl;\n\n      // Initializing a vector containing integer element\n      vector<int> vect;\n      for (int i = 0; i < 10; ++ i)\n        vect.push_back(i);\n\n      // Displaying the elements of vect\n      cout << \"Original Data:\" << endl;\n      for_each(\n             begin(vect),\n             end(vect),\n             [](int n){\n                cout << n << \" \";\n            });\n      cout << endl;\n\n      // Creating another vect2 vector\n      vector<int> vect2;\n      // Resize the size of vect2 exactly same with vect\n      vect2.resize(vect.size());\n      // Doubling the elements of vect and store to vect2\n      transform(\n              begin(vect),\n              end(vect),\n              begin(vect2),\n              [](int n) {\n                return n * n;\n            });\n\n      // Displaying the elements of vect2\n      cout << \"Squared Data:\" << endl;\n      for_each(\n             begin(vect2),\n             end(vect2),\n             [](int n) {\n                cout << n << \" \";\n            });\n      cout << endl;\n\n      // Creating another vect3 vector\n      vector<double> vect3;\n      // Resize the size of vect3 exactly same with vect\n      vect3.resize(vect.size());\n      // Finding the average of the elements of vect\n      // and store to vect2\n      transform(\n              begin(vect2),\n              end(vect2),\n              begin(vect3),\n              [](int n) -> double {\n                return n / 2.0;\n            });\n\n      // Displaying the elements of vect3\n      cout << \"Average Data:\" << endl;\n      for_each(\n             begin(vect3),\n             end(vect3),\n             [](double d) {\n                cout << d << \" \";\n            });\n      cout << endl;\n\n      return 0;\n     }\n\n```\n\n当我们在前面的代码中使用`transform()`方法时，我们有一个 Lambda 表达式，它从`n * n`的计算中返回一个值。但是，表达式中没有声明返回类型。这是因为我们可以省略返回类型的语句，因为编译器已经理解表达式将返回一个`integer`值。所以，在我们有了另一个向量`vect2`之后，它的大小和`vect`一样，我们可以和 Lambda 表达式一起调用`transform()`方法，而`vect`的值将会加倍并存储在`vect2`中。\n\n如果我们愿意，我们可以指定 Lambda 表达式的返回类型。正如我们在前面的代码中看到的，我们基于`vect`向量的所有值来转换`vect3`向量，但是现在我们使用箭头符号(`->`)将返回类型指定给`double`。前面代码的结果应该如下图所示:\n\n![](img/e395ef78-2ab2-418d-a186-6fe8b1884f70.png)\n\n从前面的截图中我们可以看到，我们已经成功地使用 Lambda 表达式找到了翻倍的平均结果。\n\n# 捕获 Lambda 表达式的值\n\n在我们之前的 Lambda 表达式示例中，我们保持捕获部分和方括号(`[]`)为空，因为 Lambda 不捕获任何内容，并且在编译器生成的匿名对象中没有任何额外的成员变量。我们也可以通过在这个方括号中指定对象来指定我们想要在 Lambda 表达式中捕获的对象。让我们看看下面这段代码来完成讨论:\n\n```cpp\n    /* lambda_capturing_by_value.cpp */\n    #include <vector>\n    #include <algorithm>\n    #include <iostream>\n\n    using namespace std;\n\n    auto main() -> int\n    {\n      cout << \"[lambda_capturing_by_value.cpp]\" << endl;\n\n      // Initializing a vector containing integer element\n      vector<int> vect;\n      for (int i = 0; i < 10; ++ i)\n      vect.push_back(i);\n\n      // Displaying the elements of vect\n      cout << \"Original Data:\" << endl;\n      for_each(\n             begin(vect),\n             end(vect),\n             [](int n){\n                cout << n << \" \";\n             });\n      cout << endl;\n\n      // Initializing two variables\n      int a = 2;\n      int b = 8;\n\n      // Capturing value explicitly from the two variables\n      cout << \"Printing elements between \" << a;\n      cout << \" and \" << b << \" explicitly [a,b]:\" << endl;\n      for_each(\n             begin(vect),\n             end(vect),\n             [a,b](int n){\n                if (n >= a && n <= b)\n                cout << n << \" \";\n             });\n      cout << endl;\n\n      // Modifying variable a and b\n      a = 3;\n      b = 7;\n\n      // Capturing value implicitly from the two variables\n      cout << \"printing elements between \" << a;\n      cout << \" and \" << b << \" implicitly[=]:\" << endl;\n      for_each(\n             begin(vect),\n             end(vect),\n             [=](int n){\n                if (n >= a && n <= b)\n                cout << n << \" \";\n            });\n      cout << endl;\n\n      return 0;\n    }\n\n```\n\n在前面的代码中，我们将尝试显式和隐式地捕获 Lambda 表达式中的值。假设我们有两个变量，`a`和`b`，我们想要显式捕获这些值，我们可以使用`[a,b]`语句在 Lambda 表达式中指定它们，然后使用函数体内部的值。此外，如果我们希望隐式捕获该值，只需将`[=]`用于捕获部分，然后当我们在函数体中指定它们时，表达式将知道我们打算使用哪个变量。如果我们运行前面的代码，我们将在屏幕上获得以下输出:\n\n![](img/f55d1c5a-b38f-4bed-a8e2-b2e2945878c8.png)\n\n我们还可以变异我们捕获的值的状态，而无需修改 Lambda 表达式函数体之外的值。为此，我们可以使用与之前相同的技术，并添加`mutable`关键字，如下面的代码块所示:\n\n```cpp\n    /* lambda_capturing_by_value_mutable.cpp */\n    #include <vector>\n    #include <algorithm>\n    #include <iostream>\n\n    using namespace std;\n\n    auto main() -> int\n    {\n      cout << \"[lambda_capturing_by_value_mutable.cpp]\" << endl;\n\n      // Initializing a vector containing integer element\n      vector<int> vect;\n      for (int i = 0; i < 10; ++ i)\n        vect.push_back(i);\n\n      // Displaying the elements of vect\n      cout << \"Original Data:\" << endl;\n      for_each(\n             begin(vect),\n             end(vect),\n             [](int n){\n                 cout << n << \" \";\n            });\n      cout << endl;\n\n      // Initializing two variables\n      int a = 1;\n      int b = 1;\n\n      // Capturing value from the two variables\n      // without mutate them\n      for_each(\n             begin(vect),\n             end(vect),\n             [=](int& x) mutable {\n                 const int old = x;\n                 x *= 2;\n                 a = b;\n                 b = old;\n             });\n\n      // Displaying the elements of vect\n      cout << \"Squared Data:\" << endl;\n      for_each(\n             begin(vect),\n             end(vect),\n             [](int n) {\n                  cout << n << \" \";\n            });\n      cout << endl << endl;\n\n      // Displaying value of variable a and b\n      cout << \"a = \" << a << endl;\n      cout << \"b = \" << b << endl;\n\n      return 0;\n    }\n\n```\n\n前面的代码将使`vect`向量的元素加倍。它在 Lambda 表达式中使用了按值捕获，还使用了`mutable`关键字。如我们所见，我们通过引用`(int& x)`传递了向量元素，并将其乘以 2，然后更改了`a`和`b`的值。但是，由于我们使用了`mutable`关键字，所以`a`和`b`的最终结果将保持不变，尽管我们已经通过引用传递了向量。控制台上的输出如下图所示:\n\n![](img/a1a58756-c71c-4554-afd8-05f6e71dff45.png)\n\n如果我们想改变`a`和`b`变量的值，我们必须使用 Lambda 表达式来引用捕获。我们可以通过将引用传递给 Lambda 表达式中的尖括号来实现，例如，`[&a, &b]`。有关更多详细信息，让我们看看下面这段代码:\n\n```cpp\n    /* lambda_capturing_by_reference.cpp */\n    #include <vector>\n    #include <algorithm>\n    #include <iostream>\n\n    using namespace std;\n\n    auto main() -> int\n    {\n      cout << \"[lambda_capturing_by_reference.cpp]\" << endl;\n\n      // Initializing a vector containing integer element\n      vector<int> vect;\n      for (int i = 0; i < 10; ++ i)\n        vect.push_back(i);\n\n      // Displaying the elements of vect\n      cout << \"Original Data:\" << endl;\n      for_each(\n             begin(vect),\n             end(vect),\n             [](int n){\n                 cout << n << \" \";\n            });\n      cout << endl;\n\n      // Initializing two variables\n      int a = 1;\n      int b = 1;\n\n      // Capturing value from the two variables\n      // and mutate them\n      for_each(\n             begin(vect),\n             end(vect),\n             [&a, &b](int& x){\n                 const int old = x;\n                 x *= 2;\n                 a = b;\n                 b = old;\n            });\n\n      // Displaying the elements of vect\n      cout << \"Squared Data:\" << endl;\n      for_each(\n             begin(vect),\n             end(vect),\n             [](int n) {\n                 cout << n << \" \";\n            });\n      cout << endl << endl;\n\n      // Displaying value of variable a and b\n      cout << \"a = \" << a << endl;\n      cout << \"b = \" << b << endl;\n\n      return 0;\n     }\n\n```\n\n前面的代码与将使`vect`向量的元素加倍的`lambda_capturing_by_value_mutable.cpp`文件具有相同的行为。然而，通过引用捕获，当在`for_each`循环中处理`a`和`b`时，它现在也修改它们的值。`a`和`b`值将在代码末尾更改，如下图所示:\n\n![](img/7d62a43b-5efe-443e-899c-c80c88f26608.png)\n\n# 使用初始化捕获准备值\n\nC++ 14 中出现的 Lambda 表达式的另一个很好的特性是它的初始化捕获。表达式可以捕获变量值并将其分配给表达式的变量。让我们看看下面这段实现初始化捕获的代码:\n\n```cpp\n    /* lambda_initialization_captures.cpp */\n    #include <iostream>\n\n    using namespace std;\n\n    auto main() -> int\n    {\n      cout << \"[lambda_initialization_captures.cpp]\" << endl;\n\n      // Initializing a variable\n      int a = 5;\n      cout << \"Initial a = \" << a << endl;\n\n      // Initializing value to lambda using the variable\n      auto myLambda = [&x = a]() { x += 2; };\n\n      // Executing the Lambda\n      myLambda();\n\n      // Displaying a new value of the variable\n      cout << \"New a = \" << a << endl;\n\n      return 0;\n     }\n\n```\n\n正如我们在前面的代码中看到的，我们有一个名为`a`的 int 变量，值为`5`。λ表达式`myLambda`，然后捕获`a`值并在代码中执行。结果就是现在的`a`值会是`7`，因为是`2`加的。当我们运行前面的代码时，下面的输出截图应该会出现在我们的控制台窗口中:\n\n![](img/66131890-ed47-4d35-93cc-c79b68cbd840.png)\n\n从前面的快照中，我们看到我们可以准备要包含在 Lambda 表达式内部计算中的值。\n\n# 编写一个通用的 Lambda 表达式，用于许多不同的数据类型\n\n在 C++ 14 之前，我们必须明确说明参数列表的类型。幸运的是，现在在 C++ 14 中，Lambda 表达式接受`auto`作为有效的参数类型。因此，我们现在可以构建一个通用的 Lambda 表达式，如下面的代码所示。在这段代码中，我们只有一个 Lambda 表达式来找出传递给该表达式的两个数字之间的最大值。我们将在参数声明中使用`auto`关键字，以便它可以由任何数据类型传递。因此，`findMax()`功能参数可以通过`int`和`float`数据类型传递。代码应如下所示:\n\n```cpp\n    /* lambda_expression_generic.cpp */\n    #include <iostream>\n\n    using namespace std;\n\n    auto main() -> int\n    {\n      cout << \"[lambda_expression_generic.cpp]\" << endl;\n\n      // Creating a generic lambda expression\n      auto findMax = [](auto &x, auto &y){\n        return x > y ? x : y; };\n\n      // Initializing various variables\n      int i1 = 5, i2 = 3;\n      float f1 = 2.5f, f2 = 2.05f;\n\n      // Consuming generic lambda expression\n      // using integer data type\n      cout << \"i1 = 5, i2 = 3\" << endl;\n      cout << \"Max: \" << findMax(i1, i2) << endl << endl;\n\n      // Consuming generic lambda expression\n      // using double data type\n      cout << \"f1 = 2.5f, f2 = 2.05f\" << endl;\n      cout << \"Max: \" << findMax(f1, f2) << endl << endl;\n\n      return 0;\n     }\n\n```\n\n我们将在控制台上看到如下输出:\n\n![](img/f2d0eb85-dba1-4aa5-bd8e-445522a4e8a2.png)\n\nThe C++ 17 language plans to introduce two new features for the Lambda expression--they are capturing `*this`, which allows the expression to capture the enclosing object by copy, and the `constexpr` Lambda expressions, which allows us to use the result of the Lambda expressions and generate `constexpr` objects at compile time. However, since C++ 17 has not been released yet, we cannot try it for now.\n\n# 使用智能指针避免手动内存管理\n\n智能指针非常有用，并且具有有效使用 C++ 的基本知识。C++ 11 为智能指针增加了许多新的能力，我们可以在`memory`头文件中找到。很长一段时间，在 C++ 11 之前，我们使用`auto_ptr`作为智能指针。但是，它非常不安全，因为它具有不兼容的复制语义。现在也不推荐使用了，不应该再用了。幸运的是，C++ 已经展示了`unique_ptr`，它有类似的功能，但是有额外的特性，比如增加`deleters`和对数组的支持。我们可以用`auto_pt`做的任何事情，我们都可以也应该用`unique_ptr`来代替。我们将深入讨论`unique_ptr`以及 C++ 11 - `shared_ptr`和`weak_ptr`中的其他新智能指针。\n\n# 使用 unique_ptr 替换原始指针\n\n我们将看到的下一个指针是`unique_ptr`指针。它快速、高效，几乎可以替代原始指针或裸指针。它提供独占所有权语义，独占它所指向的对象。由于它的排他性，如果它有一个非空指针，当它的析构函数被调用时，它可以销毁对象。由于它的排他性，它也不能被复制。它没有复制构造函数和复制赋值。虽然不能复制，但可以移动，因为它提供了移动构造函数和移动赋值。\n\n这些是我们可以用来构建`unique_ptr`的方法:\n\n```cpp\n    auto up1 = unique_ptr<int>{};\n    auto up2 = unique_ptr<int>{ nullptr };\n    auto up3 = unique_ptr<int>{ new int { 1234 } };\n\n```\n\n基于前面的代码，`up1`和`up2`将构造两个新的`unique_ptr`，它们什么都不指向(空)，而`up3`将指向保存`1234`值的地址。但是 C++ 14 增加了一个新的库函数来构造`unique_ptr`，也就是`make_unique`。因此，我们可以如下构造一个新的`unique_ptr`指针:\n\n```cpp\n    auto up4 = make_unique<int>(1234);\n\n```\n\n`up4`变量也将指向保存`1234`值的地址。\n\n现在，让我们看看下面的代码块:\n\n```cpp\n    /* unique_ptr_1.cpp */\n    #include <memory>\n    #include <iostream>\n\n    using namespace std;\n\n    struct BodyMass\n    {\n      int Id;\n      float Weight;\n\n      BodyMass(int id, float weight) :\n        Id(id),\n        Weight(weight)\n        {\n          cout << \"BodyMass is constructed!\" << endl;\n          cout << \"Id = \" << Id << endl;\n          cout << \"Weight = \" << Weight << endl;\n        }\n\n       ~BodyMass()\n       {\n         cout << \"BodyMass is destructed!\" << endl;\n       }\n     };\n\n     auto main() -> int\n     {\n       cout << \"[unique_ptr_1.cpp]\" << endl;\n       auto myWeight = make_unique<BodyMass>(1, 165.3f);\n       cout << endl << \"Doing something!!!\" << endl << endl;\n       return 0;\n     }\n\n```\n\n我们试图构造一个新的`unique_ptr`指针，指向保存`BodyMass`数据类型的地址。在`BodyMass`中，我们有一个构造函数和一个析构函数。现在，让我们通过运行前面的代码来看看`unique_ptr`指针是如何工作的。我们在屏幕上得到的输出应该如下图所示:\n\n![](img/1f91c15d-0216-4193-9396-a4d0aba1463d.png)\n\n正如我们在前面的截图中看到的，构造`unique_ptr`时会调用构造函数。此外，与传统的 C++ 语言不同，在传统 C++ 语言中，当我们使用指针时，我们必须释放内存，而在现代 c++ 中，当内存超出范围时，它将自动释放。我们可以看到`BodyMass`的析构函数在程序退出时被调用，这意味着`myWeight`超出了范围。\n\n现在，让我们通过分析下面的代码片段来测试`unique_ptr`的排他性:\n\n```cpp\n    /* unique_ptr_2.cpp */\n    #include <memory>\n    #include <iostream>\n\n    using namespace std;\n\n    struct BodyMass\n    {\n      int Id;\n      float Weight;\n\n      BodyMass(int id, float weight) :\n        Id(id), \n        Weight(weight)\n        {\n          cout << \"BodyMass is constructed!\" << endl;\n          cout << \"Id = \" << Id << endl;\n          cout << \"Weight = \" << Weight << endl;\n        }\n\n BodyMass(const BodyMass &other) :\n Id(other.Id),\n Weight(other.Weight)\n {\n cout << \"BodyMass is copy constructed!\" << endl;\n cout << \"Id = \" << Id << endl;\n cout << \"Weight = \" << Weight << endl;\n }\n\n      ~BodyMass()\n       {\n          cout << \"BodyMass is destructed!\" << endl;\n       }\n    };\n\n    auto main() -> int\n    {\n      cout << \"[unique_ptr_2.cpp]\" << endl;\n\n      auto myWeight = make_unique<BodyMass>(1, 165.3f);\n\n      // The compiler will forbid to create another pointer\n      // that points to the same allocated memory/object\n      // since it's unique pointer\n      //auto myWeight2 = myWeight;\n\n      // However, we can do the following expression\n      // since it actually copies the object that has been allocated\n      // (not the unique_pointer)\n      auto copyWeight = *myWeight;\n\n      return 0;\n    }\n\n```\n\n正如我们在前面的代码中看到的，我们看到我们不能将`unique_ptr`实例分配给另一个指针，因为它将打破`unique_ptr`的排他性。如果我们生成以下表达式，编译器将抛出一个错误:\n\n```cpp\n    auto myWeight2 = myWeight;\n\n```\n\n但是，我们可以将`unique_ptr`的值分配给另一个对象，因为它已经被分配了。为了证明这一点，我们在执行以下表达式时向日志中添加了一个复制构造函数:\n\n```cpp\n    auto copyWeight = *myWeight;\n\n```\n\n如果我们运行前面的`unique_ptr_2.cpp`代码，我们将在屏幕上看到以下输出:\n\n![](img/f33ba25b-3a90-47b9-a234-0d796c2c263e.png)\n\n正如我们在前面的截图中看到的，在执行复制分配时，会调用复制构造函数。证明我们可以复制`unique_ptr`对象的值，但不能复制对象本身。\n\n正如我们前面讨论的，`unique_ptr`已经移动了构造函数，尽管它没有复制构造函数。这种结构的使用可以在下面的代码中找到:\n\n```cpp\n    /* unique_ptr_3.cpp */\n    #include <memory>\n    #include <iostream>\n\n    using namespace std;\n\n    struct BodyMass\n    {\n      int Id;\n      float Weight;\n\n      BodyMass(int id, float weight) :\n        Id(id), \n        Weight(weight)\n        {\n          cout << \"BodyMass is constructed!\" << endl;\n          cout << \"Id = \" << Id << endl;\n          cout << \"Weight = \" << Weight << endl;\n        }\n\n      ~BodyMass()\n       {\n         cout << \"BodyMass is destructed!\" << endl;\n       }\n    };\n\n    unique_ptr<BodyMass> GetBodyMass()\n    {\n      return make_unique<BodyMass>(1, 165.3f);\n    }\n\n    unique_ptr<BodyMass> UpdateBodyMass(\n      unique_ptr<BodyMass> bodyMass)\n      {\n        bodyMass->Weight += 1.0f;\n        return bodyMass;\n      }\n\n     auto main() -> int\n     {\n       cout << \"[unique_ptr_3.cpp]\" << endl;\n\n       auto myWeight = GetBodyMass();\n\n       cout << \"Current weight = \" << myWeight->Weight << endl;\n\n       myWeight = UpdateBodyMass(move(myWeight));\n\n       cout << \"Updated weight = \" << myWeight->Weight << endl;\n\n       return 0;\n     }\n\n```\n\n在前面的代码中，我们有两个新的函数- `GetBodyMass()`和`UpdateBodyMass()`。我们从`GetBodyMass()`函数构造一个新的`unique_ptr`对象，然后使用`UpdateBodyMass()`函数更新其*权重*的值。我们可以看到，当我们向`UpdateBodyMass()`函数传递参数时，我们使用了`move`函数。这是因为`unique_ptr`没有复制构造函数，为了更新其属性的值，必须移动它。前面代码的屏幕输出如下:\n\n![](img/5188478c-d944-4612-b5cf-2b73c89a204d.png)\n\n# 使用 shared_ptr 共享对象\n\n与`unique_ptr`相比，`shared_ptr`实现了共享所有权语义，因此提供了复制构造函数和复制赋值的能力。虽然在实现上有区别，`shared_ptr`实际上是`unique_ptr`的计数版。我们可以调用`use_count()`方法找出`shared_ptr`参考的计数器值。`shared_ptr`有效对象的每个实例都计为一个。我们可以将`shared_ptr`实例复制到其他`shared_ptr`变量，引用计数将会增加。当`shared_ptr`对象被销毁时，析构函数减少引用计数。只有当计数达到零时，对象才会被删除。现在让我们检查以下`shared_ptr`代码:\n\n```cpp\n    /* shared_ptr_1.cpp */\n    #include <memory>\n    #include <iostream>\n\n    using namespace std;\n\n    auto main() -> int\n    {\n      cout << \"[shared_ptr_1.cpp]\" << endl;\n\n      auto sp1 = shared_ptr<int>{};\n\n      if(sp1)\n         cout << \"sp1 is initialized\" << endl;\n      else\n         cout << \"sp1 is not initialized\" << endl;\n      cout << \"sp1 pointing counter = \" << sp1.use_count() << endl;\n      if(sp1.unique())\n         cout << \"sp1 is unique\" << endl;\n      else\n        cout << \"sp1 is not unique\" << endl;\n      cout << endl;\n\n      sp1 = make_shared<int>(1234);\n\n      if(sp1)\n        cout << \"sp1 is initialized\" << endl;\n      else\n        cout << \"sp1 is not initialized\" << endl;\n      cout << \"sp1 pointing counter = \" << sp1.use_count() << endl;\n      if(sp1.unique())\n        cout << \"sp1 is unique\" << endl;\n      else\n        cout << \"sp1 is not unique\" << endl;\n      cout << endl;\n\n      auto sp2 = sp1;\n\n      cout << \"sp1 pointing counter = \" << sp1.use_count() << endl;\n      if(sp1.unique())\n        cout << \"sp1 is unique\" << endl;\n      else\n        cout << \"sp1 is not unique\" << endl;\n      cout << endl;\n\n      cout << \"sp2 pointing counter = \" << sp2.use_count() << endl;\n      if(sp2.unique())\n        cout << \"sp2 is unique\" << endl;\n      else\n        cout << \"sp2 is not unique\" << endl;\n      cout << endl;\n\n      sp2.reset();\n\n      cout << \"sp1 pointing counter = \" << sp1.use_count() << endl;\n      if(sp1.unique())\n        cout << \"sp1 is unique\" << endl;\n      else\n        cout << \"sp1 is not unique\" << endl;\n      cout << endl;\n\n      return 0;\n    }\n\n```\n\n在我们检查前面代码的每一行之前，让我们看一下应该出现在控制台窗口上的以下输出:\n\n![](img/88c9cd88-ace1-4cb8-a3bf-bd423b32376a.png)\n\n首先，我们创建一个名为`sp1`的`shared_ptr`对象，但不实例化它。从控制台上，我们看到`sp1`没有初始化，计数器仍然是`0`。它也不是唯一的，因为指针没有指向任何东西。然后我们使用`make_shared`方法构建`sp1`。现在`sp1`被初始化，计数器变成`1`。它也变得独一无二，因为它只是`shared_ptr`对象之一(由计数器的值`1`证明)。接下来，我们创建另一个名为`sp2`的变量，并将`sp1`复制到其中。因此，`sp1`和`sp2`现在共享由计数器和唯一性值证明的同一对象。然后，调用`sp2`中的`reset()`方法会破坏`sp2`的对象。现在`sp1`的计数器变成了`1`，又是独一无二的。\n\nIn the `shared_ptr_1.cpp` code, we declare the `unique_ptr` object using `shared_ptr<int>`, then invoke `make_shared<int>` to instance the pointer. It's because we just need to analyze the `shared_ptr` behavior. However, we should use `make_shared<>` for shared pointers since it has to keep the reference counter somewhere in memory and allocates the counter and memory for objects together instead of two separate allocations.\n\n# 使用弱指针跟踪对象\n\n我们已经在前一节讨论了`shared_ptr`。指针实际上是一个有点胖的指针。它在逻辑上指向两个对象，被管理的对象和使用`use_count()`方法的指向计数器。每个`shared_ptr`基本上都有一个防止对象被删除的强引用计数和一个不防止对象被删除的弱引用计数，如果`shared_ptr`对象的使用计数达到 0，虽然我们甚至没有使用弱引用计数。由于这个原因，我们只能使用一个引用计数，因此我们可以使用`weak_ptr`指针。`weak_ptr`指针指向由`shared_ptr`管理的对象。`weak_ptr`的优点是可以用来引用一个对象，但是我们只能在对象仍然存在的情况下访问它，而不能在强引用计数达到零时阻止该对象被其他引用持有者删除。当我们处理数据结构时，它很有用。让我们来看看下面这段代码，分析一下`weak_ptr`的用法:\n\n```cpp\n    /* weak_ptr_1.cpp */\n    #include <memory>\n    #include <iostream>\n\n    using namespace std;\n\n    auto main() -> int\n    {\n      cout << \"[weak_ptr_1.cpp]\" << endl;\n\n      auto sp = make_shared<int>(1234);\n\n      auto wp = weak_ptr<int>{ sp };\n\n      if(wp.expired())\n       cout << \"wp is expired\" << endl;\n      else\n       cout << \"wp is not expired\" << endl;\n      cout << \"wp pointing counter = \" << wp.use_count() << endl;\n      if(auto locked = wp.lock())\n       cout << \"wp is locked. Value = \" << *locked << endl;\n      else\n      {\n        cout << \"wp is unlocked\" << endl;\n        wp.reset();\n      }\n      cout << endl;\n\n      sp = nullptr;\n\n      if(wp.expired())\n       cout << \"wp is expired\" << endl;\n      else\n       cout << \"wp is not expired\" << endl;\n      cout << \"wp pointing counter = \" << wp.use_count() << endl;\n      if(auto locked = wp.lock())\n       cout << \"wp is locked. Value = \" << *locked << endl;\n      else\n      {\n        cout << \"wp is unlocked\" << endl;\n        wp.reset();\n      }\n      cout << endl;\n\n      return 0;\n     }\n\n```\n\n在我们分析前面的代码之前，让我们从输出控制台看一下下面的截图，如果我们运行代码的话:\n\n![](img/ed5b0412-e22b-4ff3-86b2-2cb32d2480b7.png)\n\n首先，我们实例化`shared_ptr`，正如我们之前讨论的那样，`weak_ptr`指向由`shared_ptr`管理的对象。然后我们将`wp`分配给`shared_ptr`变量`sp`。有了`weak_ptr`指针后，我们就可以检查它的行为了。通过调用`expired()`方法，我们可以知道被引用的对象是否已经被删除。而且`wp`变量是刚构造的，还没有过期。`weak_ptr`指针还通过调用`use_count()`方法保存对象计数的值，就像我们在`shared_ptr`中使用的那样。然后我们调用`locked()`方法创建一个`shared_ptr`，管理被引用的对象并找到`weak_ptr`所指向的值。我们现在有一个`shared_ptr`变量指向保存`1234`值的地址。\n\n之后我们将`sp`重置为`nullptr`。虽然我们没有碰`weak_ptr`指针，但它也变了。从控制台截图可以看到，现在`wp`已经过期，因为对象已经被删除了。计数器也变了，变成了`0`，因为它什么都不指向。而且由于`shared_ptr`对象被删除，所以是解锁的。\n\n# 使用元组存储许多不同的数据类型\n\n我们将熟悉元组，一个能够保存元素集合的对象，每个元素可以是不同的类型。这是 C++ 11 中的一个新特性，为函数式编程提供了动力。元组在创建返回值的函数时最有用。此外，由于函数不会改变函数编程中的全局状态，因此我们可以返回需要改变的所有值的元组。现在，让我们检查下面这段代码:\n\n```cpp\n    /* tuples_1.cpp */\n    #include <tuple>\n    #include <iostream>\n\n    using namespace std;\n\n    auto main() -> int\n    {\n      cout << \"[tuples_1.cpp]\" << endl;\n\n      // Initializing two Tuples\n      tuple<int, string, bool> t1(1, \"Robert\", true);\n      auto t2 = make_tuple(2, \"Anna\", false);\n\n      // Displaying t1 Tuple elements\n      cout << \"t1 elements:\" << endl;\n      cout << get<0>(t1) << endl;\n      cout << get<1>(t1) << endl;\n      cout << (get<2>(t1) == true ? \"Male\" : \"Female\") << endl;\n      cout << endl;\n\n      // Displaying t2 Tuple elements\n      cout << \"t2 elements:\" << endl;\n      cout << get<0>(t2) << endl;\n      cout << get<1>(t2) << endl;\n      cout << (get<2>(t2) == true ? \"Male\" : \"Female\") << endl;\n      cout << endl;\n\n      return 0;\n    }\n\n```\n\n在前面的代码中，我们创建了两个元组`t1`和`t2`，使用`tuple<int, string, bool>`和`make_tuple`使用不同的构建技术。然而，这两种不同的技术会给出相同的结果。显然，在代码中，我们使用`get<x>(y)`访问元组中的每个元素，其中`x`是索引，`y`是元组对象。并且，满怀信心，我们将在控制台上获得以下结果:\n\n![](img/d1853ef6-56aa-4020-949d-d1e527e4f8ff.png)\n\n# 解包元组值\n\n在元组类中起作用的另一个有用的成员是`tie()`，它用于将元组解包为单个对象或创建一个引用的元组`lvalue`。此外，我们在元组中有`ignore`助手类，当使用`tie()`解包元组时，它是一个跳过元素的占位符。让我们看看下面这段代码中`tie()`和`ignore`的用法:\n\n```cpp\n    /* tuples_2.cpp */\n    #include <tuple>\n    #include <iostream>\n\n    using namespace std;\n\n    auto main() -> int\n   {\n      cout << \"[tuples_2.cpp]\" << endl;\n\n      // Initializing two Tuples\n      tuple<int, string, bool> t1(1, \"Robert\", true);\n      auto t2 = make_tuple(2, \"Anna\", false);\n\n      int i;\n      string s;\n      bool b;\n\n      // Unpacking t1 Tuples\n      tie(i, s, b) = t1;\n      cout << \"tie(i, s, b) = t1\" << endl;\n      cout << \"i = \" << i << endl;\n      cout << \"s = \" << s << endl;\n      cout << \"b = \" << boolalpha << b << endl;\n      cout << endl;\n\n      // Unpacking t2 Tuples\n      tie(ignore, s, ignore) = t2;\n      cout << \"tie(ignore, s, ignore) = t2\" << endl;\n      cout << \"new i = \" << i << endl;\n      cout << \"new s = \" << s << endl;\n      cout << \"new b = \" << boolalpha << b << endl;\n      cout << endl;\n\n      return 0;\n    }\n\n```\n\n在前面的代码中，我们有相同的两个元组`tuples_1.cpp`。我们希望使用`tie()`方法将`t1`分别解包为变量`i`、`s`和`b`。然后，我们只将`t2`解包到`s`变量，忽略`t2`中的`int`和`bool`数据。如果我们运行代码，输出应该如下所示:\n\n![](img/cb5b261a-d591-4942-8062-caadadba0aac.png)\n\n# 返回元组值类型\n\n正如我们前面讨论的，当我们想要编写一个返回多个数据的函数时，我们可以在函数编程中最大限度地使用元组。让我们看看下面的代码块，了解如何返回元组和访问返回值:\n\n```cpp\n    /* tuples_3.cpp */\n    #include <tuple>\n    #include <iostream>\n\n    using namespace std;\n\n    tuple<int, string, bool> GetData(int DataId)\n    {\n      if (DataId == 1) \n        return std::make_tuple(0, \"Chloe\", false);\n      else if (DataId == 2) \n        return std::make_tuple(1, \"Bryan\", true);\n      else \n        return std::make_tuple(2, \"Zoey\", false);\n     }\n\n    auto main() -> int\n    {\n      cout << \"[tuples_3.cpp]\" << endl;\n\n      auto name = GetData(1);\n      cout << \"Details of Id 1\" << endl;\n      cout << \"ID = \" << get<0>(name) << endl;\n      cout << \"Name = \" << get<1>(name) << endl;\n      cout << \"Gender = \" << (get<2>(name) == true ? \n        \"Male\" : \"Female\");\n      cout << endl << endl;\n\n      int i;\n      string s;\n      bool b;\n      tie(i, s, b) = GetData(2);\n      cout << \"Details of Id 2\" << endl;\n      cout << \"ID = \" << i << endl;\n      cout << \"Name = \" << s << endl;\n      cout << \"Gender = \" << (b == true ? \"Male\" : \"Female\");\n      cout << endl;\n\n      return 0;\n    }\n\n```\n\n正如我们在前面的代码中看到的，我们有一个名为`GetData()`的新函数，返回一个`Tuple`值。从该函数中，我们将使用从中返回的数据。我们从创建名称变量开始，从`GetData()`函数中获取值。我们也可以使用`tie()`方法来解包来自`GetData()`函数的元组，正如我们在代码中看到的，当我们访问数据时，ID = `2`。当我们运行代码时，控制台上的输出应该如下图所示:\n\n![](img/f46900eb-e1ba-4d02-9f88-af7b6a43b879.png)\n\n# 摘要\n\n通过完成这一章，我们更新了我们在 C++ 语言方面的经验。现在我们知道 C++ 更现代，它有许多功能可以帮助我们创建更好的程序。我们可以使用标准库来提高代码的效率，因为我们不需要编写太多多余的函数。我们可以使用 Lambda 表达式来使我们的代码整洁、易读和易于维护。我们还可以使用智能指针，这样我们就不再需要担心内存管理了。此外，由于我们关心函数式编程中的不变性，我们将在下一章更深入地讨论这个问题；元组的使用可以帮助我们确保代码中不涉及全局状态。\n\n在下一章中，我们将讨论 First-Class 和 Pure Function，它用于净化我们的类，并确保当前函数中不涉及任何外部状态。因此，它将避免我们的函数代码中的副作用。"
  },
  {
    "path": "docs/learn-cpp-func-prog/2.md",
    "content": "# 二、函数式编程中的函数操作\n\n在前一章中，我们深入讨论了现代 C++ 语言，特别是 C++ 11 中的新特性 Lambda 表达式。正如我们前面所讨论的，Lambda 表达式在简化函数符号时非常有用。因此，在本章中，我们将再次应用 Lambda 表达式的能力，这将在函数代码中使用，尤其是当我们谈论 curry-拆分和减少当前函数的技术时。\n\n在本章中，我们将讨论以下主题:\n\n*   应用一级函数和高阶函数，这样我们的函数不仅可以作为函数调用，还可以赋给任何变量、传递函数和返回函数\n*   纯功能，以避免我们的功能的副作用，因为它不再接触外部状态\n*   正如本章开头提到的，Currying 是为了减少多参数函数，这样我们就可以计算一系列函数，每个函数中只有一个参数\n\n# 在所有函数中应用第一类函数\n\n一级函数只是一个普通类。我们可以像对待任何其他数据类型一样对待第一类函数。但是，在支持一级函数的语言中，我们可以在不递归调用编译器的情况下完成以下任务:\n\n*   将一个函数作为另一个函数的参数传递\n*   将函数分配给变量\n*   在集合中存储函数\n*   运行时从现有函数创建新函数\n\n幸运的是，C++ 可以用来解决前面的任务。我们将在以下主题中深入讨论它。\n\n# 将一个函数作为另一个函数的参数传递\n\n让我们开始传递一个函数作为函数参数。我们将从四个函数中选择一个，并从其主函数中调用该函数。代码如下所示:\n\n```cpp\n    /* first_class_1.cpp */\n    #include <functional>\n    #include <iostream>\n\n    using namespace std;\n\n    // Defining a type of function named FuncType\n    // representing a function\n    // that pass two int arguments\n    // and return an int value\n    typedef function<int(int, int)> FuncType;\n\n    int addition(int x, int y)\n    {\n      return x + y;\n    }\n\n    int subtraction(int x, int y)\n    {\n      return x - y;\n    }\n\n    int multiplication(int x, int y)\n    {\n      return x * y;\n    }\n\n    int division(int x, int y)\n    {\n      return x / y;\n    }\n\n    void PassingFunc(FuncType fn, int x, int y)\n    {\n      cout << \"Result = \" << fn(x, y) << endl;\n    }\n\n    auto main() -> int\n    {\n      cout << \"[first_class_1.cpp]\" << endl;\n      int i, a, b;\n      FuncType func;\n\n      // Displaying menu for user\n      cout << \"Select mode:\" << endl;\n      cout << \"1\\. Addition\" << endl;\n      cout << \"2\\. Subtraction\" << endl;\n      cout << \"3\\. Multiplication\" << endl;\n      cout << \"4\\. Division\" << endl;\n      cout << \"Choice: \";\n      cin >> i;\n\n      // Preventing user to select\n      // unavailable modes\n      if(i < 1 || i > 4)\n      {\n         cout << \"Please select available mode!\";\n         return 1;\n      }\n\n      // Getting input from user for variable a\n      cout << \"a -> \";\n      cin >> a;\n\n      // Input validation for variable a\n      while (cin.fail())\n      {\n        // Clearing input buffer to restore cin to a usable state\n        cin.clear();\n\n        // Ignoring last input\n        cin.ignore(INT_MAX, '\\n');\n\n        cout << \"You can only enter numbers.\\n\";\n        cout << \"Enter a number for variable a -> \";\n        cin >> a;\n      }\n\n      // Getting input from user for variable b\n      cout << \"b -> \";\n      cin >> b;\n\n      // Input validation for variable b\n      while (cin.fail())\n      {\n        // Clearing input buffer to restore cin to a usable state\n        cin.clear();\n\n        // Ignoring last input\n        cin.ignore(INT_MAX, '\\n');\n\n        cout << \"You can only enter numbers.\\n\";\n        cout << \"Enter a number for variable b -> \";\n        cin >> b;\n      }\n      switch(i)\n      {\n        case 1: PassingFunc(addition, a, b); break;\n        case 2: PassingFunc(subtraction, a, b); break;\n        case 3: PassingFunc(multiplication, a, b); break;\n        case 4: PassingFunc(division, a, b); break;\n      }\n\n      return 0;\n    }\n\n```\n\n从前面的代码中，我们可以看到我们有四个函数，我们希望用户选择一个，然后运行它。在 switch 语句中，我们将根据用户的选择调用四个函数之一。我们将把选择的函数传递给`PassingFunc()`，如下面的代码片段所示:\n\n```cpp\n    case 1: PassingFunc(addition, a, b); break;\n    case 2: PassingFunc(subtraction, a, b); break;\n    case 3: PassingFunc(multiplication, a, b); break;\n    case 4: PassingFunc(division, a, b); break;\n\n```\n\n我们还有输入验证，以防止用户选择不可用的模式以及输入变量`a`和`b`的非整数值。我们将在屏幕上看到的输出应该如下所示:\n\n![](img/e7a276f3-b116-4d3e-80fd-12b4fe9e197f.png)\n\n前面的截图显示我们从可用模式中选择`Multiplication`模式。然后，我们尝试为变量`a`输入`r`和`e`变量。幸运的是，程序拒绝了它，因为我们已经进行了输入验证。然后，我们将`4`给变量`a`，将`2`给变量`b`。正如我们所料，程序给我们的结果是`8`。\n\nAs we can see in the `first_class_1.cpp` program, we use the `std::function` class and the `typedef` keyword to simplify the code. The `std::function` class is used to store, copy, and invoke any callable functions, Lambda expressions, or other function objects, as well as pointers to member functions and pointers to data members. However, the `typedef` keyword is used as an alias name for another type or function.\n\n# 将函数赋给变量\n\n我们还可以给变量分配一个函数，这样我们就可以通过调用变量来调用函数。我们将重构`first_class_1.cpp`，如下所示:\n\n```cpp\n    /* first_class_2.cpp */\n    #include <functional>\n    #include <iostream>\n\n    using namespace std;\n\n    // Defining a type of function named FuncType\n    // representing a function\n    // that pass two int arguments\n    // and return an int value\n    typedef function<int(int, int)> FuncType;\n\n    int addition(int x, int y)\n    {\n      return x + y;\n    }\n\n    int subtraction(int x, int y)\n    {\n      return x - y;\n    }\n\n    int multiplication(int x, int y)\n    {\n      return x * y;\n    }\n\n    int division(int x, int y)\n    {\n      return x / y;\n    }\n\n    auto main() -> int\n    {\n      cout << \"[first_class_2.cpp]\" << endl;\n\n      int i, a, b;\n      FuncType func;\n\n      // Displaying menu for user\n      cout << \"Select mode:\" << endl;\n      cout << \"1\\. Addition\" << endl;\n      cout << \"2\\. Subtraction\" << endl;\n      cout << \"3\\. Multiplication\" << endl;\n      cout << \"4\\. Division\" << endl;\n      cout << \"Choice: \";\n      cin >> i;\n\n      // Preventing user to select\n      // unavailable modes\n      if(i < 1 || i > 4)\n      {\n        cout << \"Please select available mode!\";\n        return 1;\n      }\n\n      // Getting input from user for variable a\n      cout << \"a -> \";\n      cin >> a;\n\n      // Input validation for variable a\n      while (cin.fail())\n      {\n        // Clearing input buffer to restore cin to a usable state\n        cin.clear();\n\n        // Ignoring last input\n        cin.ignore(INT_MAX, '\\n');\n\n        cout << \"You can only enter numbers.\\n\";\n        cout << \"Enter a number for variable a -> \";\n        cin >> a;\n      }\n\n      // Getting input from user for variable b\n      cout << \"b -> \";\n      cin >> b;\n\n      // Input validation for variable b\n      while (cin.fail())\n      {\n        // Clearing input buffer to restore cin to a usable state\n        cin.clear();\n\n        // Ignoring last input\n        cin.ignore(INT_MAX, '\\n');\n\n        cout << \"You can only enter numbers.\\n\";\n        cout << \"Enter a number for variable b -> \";\n        cin >> b;\n      }\n\n      switch(i)\n      {\n        case 1: func = addition; break;\n        case 2: func = subtraction; break;\n        case 3: func = multiplication; break;\n        case 4: func = division; break;\n      }\n\n      cout << \"Result = \" << func(a, b) << endl;\n\n      return 0;\n    }\n\n```\n\n我们现在将根据用户的选择分配四个函数，并将选择的函数存储在 switch 语句内部的`func`变量中，如下所示:\n\n```cpp\n    case 1: func = addition; break;\n    case 2: func = subtraction; break;\n    case 3: func = multiplication; break;\n    case 4: func = division; break;\n\n```\n\n当`func`变量被赋予用户的选择后，代码将像调用函数一样调用该变量，如下一行代码所示:\n\n```cpp\n    cout << \"Result = \" << func(a, b) << endl;\n\n```\n\n如果我们运行代码，我们将在控制台上获得相同的输出。\n\n# 在容器中存储函数\n\n现在，让我们将函数保存到容器中。这里，我们将使用**向量**作为容器。代码编写如下:\n\n```cpp\n    /* first_class_3.cpp */\n    #include <vector>\n    #include <functional>\n    #include <iostream>\n\n    using namespace std;\n\n    // Defining a type of function named FuncType\n    // representing a function\n    // that pass two int arguments\n    // and return an int value\n    typedef function<int(int, int)> FuncType;\n\n    int addition(int x, int y)\n    {\n      return x + y;\n    }\n\n    int subtraction(int x, int y)\n    {\n      return x - y;\n    }\n\n    int multiplication(int x, int y)\n    {\n      return x * y;\n    }\n\n    int division(int x, int y)\n    {\n      return x / y;\n    }\n\n    auto main() -> int\n    {\n      cout << \"[first_class_3.cpp]\" << endl;\n\n      // Declaring a vector containing FuncType element\n      vector<FuncType> functions;\n\n      // Assigning several FuncType elements to the vector\n      functions.push_back(addition);\n      functions.push_back(subtraction);\n      functions.push_back(multiplication);\n      functions.push_back(division);\n\n      int i, a, b;\n      function<int(int, int)> func;\n\n      // Displaying menu for user\n      cout << \"Select mode:\" << endl;\n      cout << \"1\\. Addition\" << endl;\n      cout << \"2\\. Subtraction\" << endl;\n      cout << \"3\\. Multiplication\" << endl;\n      cout << \"4\\. Division\" << endl;\n      cout << \"Choice: \";\n      cin >> i;\n\n      // Preventing user to select\n      // unavailable modes\n      if(i < 1 || i > 4)\n      {\n        cout << \"Please select available mode!\";\n        return 1;\n      }\n\n      // Getting input from user for variable a\n      cout << \"a -> \";\n      cin >> a;\n\n      // Input validation for variable a\n      while (cin.fail())\n      {\n        // Clearing input buffer to restore cin to a usable state\n        cin.clear();\n\n        // Ignoring last input\n        cin.ignore(INT_MAX, '\\n');\n\n        cout << \"You can only enter numbers.\\n\";\n        cout << \"Enter a number for variable a -> \";\n        cin >> a;\n      }\n\n      // Getting input from user for variable b\n      cout << \"b -> \";\n      cin >> b;\n\n      // Input validation for variable b\n      while (cin.fail())\n      {\n        // Clearing input buffer to restore cin to a usable state\n        cin.clear();\n\n        // Ignoring last input\n        cin.ignore(INT_MAX, '\\n');\n\n        cout << \"You can only enter numbers.\\n\";\n        cout << \"Enter a number for variable b -> \";\n        cin >> b;\n      }\n\n      // Invoking the function inside the vector\n      cout << \"Result = \" << functions.at(i - 1)(a, b) << endl;\n\n      return 0;\n    }\n\n```\n\n从前面的代码中，我们可以看到我们创建了一个名为 functions 的新向量，然后向它存储了四个不同的函数。就像前面两个代码示例一样，我们也要求用户选择模式。但是，现在代码变得更简单了，因为我们不需要添加 switch 语句；我们可以通过选择向量索引直接选择函数，如下面的代码片段所示:\n\n```cpp\n    cout << \"Result = \" << functions.at(i - 1)(a, b) << endl;\n\n```\n\n但是，由于向量是一个**零基**索引，我们必须用菜单选项来调整索引。结果将与我们前面的两个代码示例相同。\n\n# 运行时从现有函数创建新函数\n\n现在，让我们在运行时从先前存在的函数中创建一个新函数。假设我们有两个函数集合，第一个是双曲函数，第二个是第一个的反函数。除了这些内置函数，我们还添加了一个用户定义的函数来计算第一个集合中的平方数和第二个集合中平方数的倒数。然后，我们将实现函数组合，并从两个现有函数中构建一个新函数。\n\n**Function composition** is a process to combine two or more simple functions to create a more complex one. The result of each function is passed as the argument to the next function. The final result is obtained from the last function result. In a mathematical approach, we usually use the following notation to function composition: `compose(f, g) (x) = f(g(x))`. Let's suppose we have the following code:\n\n`double x, y, z; // ... y = g(x); z = f(y);`\n\n因此，为了简化符号，我们可以使用函数组合，并对 *z* 有以下符号:\n\n`z = f(g(x));`\n\n如果我们运行双曲函数，然后将结果传递给反函数，我们会看到我们确实得到了传递给双曲函数的初始值。现在，让我们看看下面的代码:\n\n```cpp\n    /* first_class_4.cpp */\n    #include <vector>\n    #include <cmath>\n    #include <algorithm>\n    #include <functional>\n    #include <iostream>\n\n    using std::vector;\n    using std::function;\n    using std::transform;\n    using std::back_inserter;\n    using std::cout;\n    using std::endl;\n\n    // Defining a type of function named HyperbolicFunc\n    // representing a function\n    // that pass a double argument\n    // and return an double value\n    typedef function<double(double)> HyperbolicFunc;\n\n    // Initializing a vector containing four functions\n    vector<HyperbolicFunc> funcs = {\n      sinh,\n      cosh,\n      tanh,\n      [](double x) {\n        return x*x; }\n    };\n\n    // Initializing a vector containing four functions\n    vector<HyperbolicFunc> inverseFuncs = {\n      asinh,\n      acosh,\n      atanh,\n      [](double x) {\n        return exp(log(x)/2); }\n    };\n\n    // Declaring a template to be able to be reused\n    template <typename A, typename B, typename C>\n    function<C(A)> compose(\n      function<C(B)> f,\n      function<B(A)> g) {\n        return [f,g](A x) {\n            return f(g(x));\n      };\n    }\n\n    auto main() -> int\n    {\n      cout << \"[first_class_4.cpp]\" << endl;\n\n      // Declaring a template to be able to be reused\n      vector<HyperbolicFunc> composedFuncs;\n\n      // Initializing a vector containing several double elements\n      vector<double> nums;\n      for (int i = 1; i <= 5; ++ i)\n        nums.push_back(i * 0.2);\n\n      // Transforming the element inside the vector\n      transform(\n        begin(inverseFuncs),\n        end(inverseFuncs),\n        begin(funcs),\n        back_inserter(composedFuncs),\n        compose<double, double, double>);\n\n      for (auto num: nums)\n      {\n        for (auto func: composedFuncs)\n            cout << \"f(g(\" << num << \")) = \" << func(num) << endl;\n\n        cout << \"---------------\" << endl;\n      }\n\n      return 0;\n    }\n\n```\n\n正如我们在前面的代码中看到的，我们有两个函数集合- `funcs`和`inverseFuncs`。此外，正如我们之前讨论的那样，`inverseFuncs`函数是`funcs`函数的逆函数。`funcs`函数包含三个内置双曲函数，以及一个用于计算平方数的用户定义函数，而`inverseFuncs`包含三个内置反双曲函数以及一个用于计算平方数倒数的用户定义函数。\n\nAs we can see in the preceding `first_class_4.cpp` code, we will use individual classes/functions when calling the `using` keyword. Compared to the other code samples in this chapter, the use of the `using` keyword in individual classes/functions is inconsistent, since we use `using namespace std`. It's because there's a clashing function name in the `std` namespace, so we have to call them individually.\n\n通过使用这两个函数集合，我们将从它们构造一个新的函数。为了达到这个目的，我们将使用`transform()`函数来组合来自两个不同集合的两个函数。代码片段如下:\n\n```cpp\n transform(\n begin(inverseFuncs), \n inverseFuncs.end(inverseFuncs), \n begin(funcs), \n back_inserter(composedFuncs), \n compose<double, double, double>);\n\n```\n\n现在，我们在`composedFuncs`向量中存储了一个新的函数集合。我们可以迭代集合，并将我们在`nums`变量中提供的值传递给这个新函数。如果运行代码，我们应该在控制台上获得以下输出:\n\n![](img/020e5ce8-760c-4446-82c2-7aeca8fddc1a.png)\n\n从前面的输出可以看出，无论我们传递给转换函数什么，我们都会得到与输入相同的输出。在这里，我们可以证明 C++ 编程可以用来从两个或多个现有函数组成一个函数。\n\nOn the preceding `first_class_4.cpp` code, we use `template<>` in the code. If you need a more detailed explanation about `template<>`, refer to [Chapter 7](7.html), *Running Parallel Execution Using Concurrency*.\n\n# 熟悉高阶函数中的三种函数技巧\n\n我们讨论过，在第一类函数中，C++ 语言将函数视为值，这意味着我们可以将它们传递给其他函数，分配给变量，等等。然而，我们在函数式编程中有另一个术语，即高阶函数，它是对其他函数起作用的函数。这意味着高阶函数可以传递函数作为参数，也可以返回函数。\n\n高阶函数概念可以应用于一般函数，就像数学函数一样，而不是只能应用于函数编程语言的一级函数概念。现在，让我们来看看函数编程中最有用的三个高阶函数- **映射**、**过滤**和**折叠**。\n\n# 使用映射执行每个元素列表\n\n我们不会在 C++ 语言中讨论作为容器的 map，而是高阶函数中的一个特性。此功能用于对列表中的每个元素应用给定的函数，并以相同的顺序返回结果列表。我们可以使用`transform()`功能来达到这个目的。如您所知，我们之前已经讨论过这个函数。但是，我们可以看一下下面这段代码来查看`transform()`功能的使用情况:\n\n```cpp\n    /* transform_1.cpp */\n    #include <vector>\n    #include <algorithm>\n    #include <iostream>\n\n    using namespace std;\n\n    auto main() -> int\n    {\n      cout << \"[transform_1.cpp]\" << endl;\n\n      // Initializing a vector containing integer element\n      vector<int> v1;\n      for (int i = 0; i < 5; ++ i)\n        v1.push_back(i);\n\n      // Creating another v2 vector\n      vector<int> v2;\n      // Resizing the size of v2 exactly same with v1\n      v2.resize(v1.size());\n\n      // Transforming the element inside the vector\n      transform (\n        begin(v1),\n        end(v1),\n        begin(v2),\n        [](int i){\n            return i * i;});\n\n      // Displaying the elements of v1\n      std::cout << \"v1 contains:\";\n      for (auto v : v1)\n        std::cout << \" \" << v;\n      std::cout << endl;\n\n      // Displaying the elements of v2\n      std::cout << \"v2 contains:\";\n      for (auto v : v2)\n        std::cout << \" \" << v;\n      std::cout << endl;\n\n      return 0;\n    }\n\n```\n\n如我们前面在高阶函数中对 map 的定义所示，它会将给定的函数应用于列表的每个元素。在前面的代码中，我们尝试使用 Lambda 表达式中的给定函数将`v1`向量映射到`v2`向量，如下所示:\n\n```cpp\n transform (\n      begin(v1), \n      end(v1), \n      begin(v2), \n      [](int i){\n        return i * i;});\n\n```\n\n如果我们运行代码，我们应该在控制台屏幕上获得以下输出:\n\n![](img/30abb408-577c-4cfd-82c7-8b1030998fc0.png)\n\n正如我们在输出显示中看到的，我们使用 Lambda 表达式中的给定函数 notating 将`v1`转换为`v2`，该函数是输入值的两倍。\n\n# 使用过滤器提取数据\n\n在高阶函数中，Filter 是一种从现有数据结构生成新数据结构的函数，该数据结构将新数据结构中的每个元素与返回布尔值的给定谓词完全匹配。在 C++ 语言中，我们可以应用 C++ 11 中添加的`copy_if()`函数来获取过滤过程。让我们看看下面这段代码，用`copy_if()`函数分析过滤过程:\n\n```cpp\n    /* filter_1.cpp */\n    #include <vector>\n    #include <algorithm>\n    #include <iterator>\n    #include <iostream>\n\n    using namespace std;\n\n    auto main() -> int\n    {\n      cout << \"[filter_1.cpp]\" << endl;\n\n      // Initializing a vector containing integer elements\n      vector<int> numbers;\n      for (int i = 0; i < 20; ++ i)\n        numbers.push_back(i);\n\n       // Displaying the elements of numbers\n       cout << \"The original numbers: \" << endl;\n       copy(\n        begin(numbers),\n        end(numbers),\n        ostream_iterator<int>(cout, \" \"));\n       cout << endl;\n\n       // Declaring a vector containing int elements\n       vector<int> primes;\n\n      // Filtering the vector\n      copy_if(\n        begin(numbers),\n        end(numbers),\n        back_inserter(primes),\n        [](int n) {\n            if(n < 2) {\n                return (n != 0) ? true : false;}\n            else {\n                for (int j = 2; j < n; ++ j) {\n                    if (n % j == 0){\n                        return false;}\n            }\n\n            return true;\n         }});\n\n        // Displaying the elements of primes\n        // using copy() function\n        cout << \"The primes numbers: \" << endl;\n        copy(\n         begin(primes),\n         end(primes),\n         ostream_iterator<int>(cout, \" \"));\n         cout << endl;\n\n         return 0;\n    }\n\n```\n\n正如我们在前面的代码中看到的，我们使用`copy_if()`函数将`numbers`向量过滤成`0`素数向量。我们将通过 Lambda 表达式来决定所选元素是否是质数，就像我们在第 1 章*中使用的`lambda_multiline_func.cpp`代码一样，进入现代 C++* 。我们还将使用`copy()`功能复制要打印的选定矢量中的所有元素。当我们运行前面的代码时，结果应该如下:\n\n![](img/84a91b01-5fb5-4111-afaa-c2ca77c868ca.png)\n\n除了`copy_if()`功能，我们还可以使用`remove_copy_if()`功能对数据结构进行过滤。使用`remove_copy_if()`函数将省略匹配谓词元素，选择不匹配的元素，并将其存储在新的数据结构中，而不是从现有数据结构中选择匹配谓词元素。让我们重构我们的`filter_1.cpp`代码，创建一个不是素数的新向量。代码如下:\n\n```cpp\n    /* filter_2.cpp */\n    #include <vector>\n    #include <algorithm>\n    #include <iterator>\n    #include <iostream>\n\n    using namespace std;\n\n    int main()\n   {\n      cout << \"[filter_2.cpp]\" << endl;\n\n      // Initializing a vector containing integer elements\n      vector<int> numbers;\n      for (int i = 0; i < 20; ++ i)\n        numbers.push_back(i);\n\n      // Displaying the elements of numbers\n      cout << \"The original numbers: \" << endl;\n      copy(\n        begin(numbers),\n        end(numbers),\n        ostream_iterator<int>(cout, \" \"));\n      cout << endl;\n\n      // Declaring a vector containing int elements\n      vector<int> nonPrimes;\n\n      // Filtering the vector\n      remove_copy_if(\n        numbers.begin(),\n        numbers.end(),\n        back_inserter(nonPrimes),\n        [](int n) {\n            if(n < 2){\n                return (n != 0) ? true : false;}\n            else {\n                for (int j = 2; j < n; ++ j){\n                    if (n % j == 0) {\n                        return false;}\n            }\n\n            return true;\n        }});\n\n      // Displaying the elements of nonPrimes\n      // using copy() function\n      cout << \"The non-primes numbers: \" << endl;\n      copy(\n        begin(nonPrimes),\n        end(nonPrimes),\n        ostream_iterator<int>(cout, \" \"));\n      cout << endl;\n\n      return 0;\n    }\n\n```\n\n从前面突出显示的代码中我们可以看到，我们重构了前面的代码，并使用`remove_copy_if()`函数来选择非质数。如我们所料，控制台窗口将显示以下输出:\n\n![](img/c0b50b7a-1cfb-48ed-b552-c4bfb806a152.png)\n\n我们现在有了非质数，而不是质数，就像我们在`filter_1.cpp`代码中一样。\n\n# 使用 fold 组合列表的所有元素\n\n在函数式编程中，折叠是一种将数据结构简化为单个值的技术。褶皱有两种类型——左褶皱(`foldl`)和右褶皱(`foldr`)。假设我们有一个包含 0、1、2、3 和 4 的列表。让我们使用折叠技术添加列表的所有内容，首先使用`foldl`，然后使用`foldr`。然而，这两者之间有一个显著的区别- `foldl`是左关联的，这意味着我们组合最左边的元素，然后向最右边的元素移动。例如，根据我们的列表，我们将得到以下括号:\n\n```cpp\n    ((((0 + 1) + 2) + 3) + 4)\n\n```\n\n而`foldr`是右边的关联，这意味着我们将组合最右边的元素，然后向最左边的元素移动。括号将类似于下面的代码行:\n\n```cpp\n    (0 + (1 + (2 + (3 + 4))))\n\n```\n\n现在，让我们看看下面的代码:\n\n```cpp\n    /* fold_1.cpp */\n    #include <vector>\n    #include <numeric>\n    #include <functional>\n    #include <iostream>\n\n    using namespace std;\n\n    auto main() -> int\n    {\n      cout << \"[fold_1.cpp]\" << endl;\n\n      // Initializing a vector containing integer elements\n      vector<int> numbers = {0, 1, 2, 3, 4};\n\n      // Calculating the sum of the value\n      // in the vector\n      auto foldl = accumulate(\n        begin(numbers),\n        end(numbers),\n        0,\n        std::plus<int>());\n\n      // Calculating the sum of the value\n      // in the vector\n      auto foldr = accumulate(\n        rbegin(numbers),\n        rend(numbers),\n        0,\n        std::plus<int>());\n\n      // Displaying the calculating result\n      cout << \"foldl result = \" << foldl << endl;\n      cout << \"foldr result = \" << foldr << endl;\n\n      return 0;\n    }\n\n```\n\n在 C++ 编程中，我们可以使用`accumulate()`函数应用`fold`技术。正如我们在前面的代码中看到的，我们在`foldl`中使用正向迭代器，而在`foldr`中使用反向迭代器。控制台上的输出应该如下图所示:\n\n![](img/9cb482af-59c6-4f00-b396-d4fbabd686c9.png)\n\n正如我们在前面的输出截图中看到的，我们得到了相同的结果，即`foldl`和`foldr`技术。对于那些好奇总和顺序的人，我们可以将前面的代码重构为下面的代码:\n\n```cpp\n    /* fold_2.cpp */\n    #include <vector>\n    #include <numeric>\n    #include <functional>\n    #include <iostream>\n\n    using namespace std;\n\n    // Function for logging the flow\n    int addition(const int& x, const int& y)\n    {\n      cout << x << \" + \" << y << endl;\n      return x + y;\n    }\n\n    int main()\n    {\n      cout << \"[fold_2.cpp]\" << endl;\n\n      // Initializing a vector containing integer elements\n      vector<int> numbers = {0, 1, 2, 3, 4};\n\n      // Calculating the sum of the value\n      // in the vector\n      // from left to right\n      cout << \"foldl\" << endl;\n      auto foldl = accumulate(\n          begin(numbers),\n          end(numbers),\n          0,\n          addition);\n\n      // Calculating the sum of the value\n      // in the vector\n      // from right to left\n      cout << endl << \"foldr\" << endl;\n      auto foldr = accumulate(\n          rbegin(numbers),\n          rend(numbers),\n          0,\n          addition);\n\n      cout << endl;\n\n      // Displaying the calculating result\n      cout << \"foldl result = \" << foldl << endl;\n      cout << \"foldr result = \" << foldr << endl;\n\n      return 0;\n    }\n\n```\n\n在前面的代码中，我们传递了一个新的`addition()`函数，并将其传递给`accumulate()`函数。从`addition()`功能，我们将跟踪每个元素的操作。现在，让我们运行前面的代码，其输出如下:\n\n![](img/5b662182-a4b8-4a3f-a969-29983904dcbb.png)\n\n从前面的输出截图中，我们可以看到，即使`foldl`和`foldr`给出了完全相同的结果，但它们做出了不同的操作顺序。由于我们将初始值设置为`0`，加法操作开始于将`0`添加到`foldl`技术中的第一个元素和`foldr`技术中的最后一个元素。\n\nWe will set the initial value to `0` because `0` is the additive identity that won't impact the addition result. However, in multiplication, we have to consider changing the initial value to `1` since `1` is the identity element for multiplication.\n\n# 避免纯功能的副作用\n\nA **纯函数**是一个每次给它相同的输入时总是返回相同结果的函数。结果不依赖于任何信息或状态，不会产生**副作用**，或功能外的系统状态变化。让我们看看下面这段代码:\n\n```cpp\n    /* pure_function_1.cpp */\n    #include <iostream>\n\n    using namespace std;\n\n    float circleArea(float r)\n    {\n      return 3.14 * r * r;\n    }\n\n    auto main() -> int\n    {\n      cout << \"[pure_function_1.cpp]\" << endl;\n\n      // Initializing a float variable\n      float f = 2.5f;\n\n      // Invoking the circleArea() function\n      // passing the f variable five times\n      for(int i = 1; i <= 5; ++ i)\n      {\n        cout << \"Invocation \" << i << \" -> \";\n        cout << \"Result of circleArea(\" << f << \") = \";\n        cout << circleArea(f) << endl;\n      }\n\n      return 0;\n    }\n\n```\n\n从前面的代码可以看出，我们有一个名为`circleArea()`的函数，根据给定的半径计算圆的面积。然后我们调用该函数五次，并传递相同的半径值。控制台上的输出应该如下图所示:\n\n![](img/363316b8-632e-4346-a2f6-50003c5a8c14.png)\n\n正如我们所看到的，在传递相同输入的五次调用中，函数也返回相同的输出。这样我们就可以说`circleArea()`是一个纯函数。现在，让我们看看下面这段代码中不纯函数的样子:\n\n```cpp\n    /* impure_function_1.cpp */\n    #include <iostream>\n\n    using namespace std;\n\n    // Initializing a global variable\n    int currentState = 0;\n\n    int increment(int i)\n    {\n      currentState += i;\n      return currentState;\n    }\n\n    auto main() -> int\n    {\n      cout << \"[impure_function_1.cpp]\" << endl;\n\n      // Initializing a local variable\n      int fix = 5;\n\n      // Involving the global variable\n      // in the calculation\n      for(int i = 1; i <= 5; ++ i)\n      {\n        cout << \"Invocation \" << i << \" -> \";\n        cout << \"Result of increment(\" << fix << \") = \";\n        cout << increment(fix) << endl;\n      }\n\n       return 0;\n    }\n\n```\n\n在前面的代码中，我们看到一个名为`increment()`的函数增加了`currentState`变量的值。我们可以看到，`increment()`函数依赖于`currentState`变量的值，所以它不是一个纯函数。让我们通过运行前面的代码来证明它。控制台窗口应显示以下屏幕截图:\n\n![](img/d0f1d544-925c-447a-9780-1ae4db9ca6f1.png)\n\n我们看到`increment()`函数给出了不同的结果，即使我们传递了相同的输入。这是不纯函数依赖于外部状态或改变外部状态值时的副作用。\n\n我们已经能够区分纯函数和不纯函数。但是，请考虑以下代码:\n\n```cpp\n    /* im_pure_function_1.cpp */\n    #include <iostream>\n\n    using namespace std;\n\n    // Initializing a global variable\n    float phi = 3.14f;\n\n    float circleArea(float r)\n    {\n      return phi * r * r;\n    }\n\n    auto main() -> int\n    {\n      cout << \"[im_pure_function_1.cpp]\" << endl;\n\n      // Initializing a float variable\n      float f = 2.5f;\n\n      // Involving the global variable\n      // in the calculation\n      for(int i = 1; i <= 5; ++ i)\n      {\n        cout << \"Invocation \" << i << \" -> \";\n        cout << \"Result of circleArea(\" << f << \") = \";\n        cout << circleArea(f) << endl;\n      }\n\n      return 0;\n    }\n\n```\n\n前面的代码来自`pure_function_1.cpp`，但是我们添加了一个全局状态`phi`。如果我们运行前面的代码，我们肯定会得到与`pure_function_1.cpp`相同的结果。虽然函数在五次调用中返回相同的结果，但是`im_pure_function_1.cpp`中的`circleArea()`不是一个纯函数，因为它依赖于`phi`变量。\n\nThe side effect is not only the change of global state that is done by the function. Printing to the screen is also the side effect. However, since we need to show the result of every code we create, we cannot avoid the existence of printing to screen in our codes. In the next chapter, we will also discuss the immutable state, which is the way we can turn an impure function into a pure function.\n\n# 用 currying 简化多参数函数\n\nCurrying 是一种拆分函数的技术，该技术将多个参数用于计算一系列函数，每个函数都有一个参数。换句话说，我们通过减少当前函数来创建基于当前函数的其他函数。假设我们有一个名为`areaOfRectangle()`的函数，它接受两个参数，`length`和`width`。代码是这样的:\n\n```cpp\n    /* curry_1.cpp */\n\n    #include <functional>\n    #include <iostream>\n\n    using namespace std;\n\n    // Variadic template for currying\n    template<typename Func, typename... Args>\n    auto curry(Func func, Args... args)\n    {\n      return [=](auto... lastParam)\n      {\n        return func(args..., lastParam...);\n      };\n    }\n\n    int areaOfRectangle(int length, int width)\n    {\n      return length * width;\n    }\n\n    auto main() -> int\n    {\n      cout << \"[curry_1.cpp]\" << endl;\n\n      // Currying the areaOfRectangle() function\n      auto length5 = curry(areaOfRectangle, 5);\n\n      // Invoking the curried function\n      cout << \"Curried with spesific length = 5\" << endl;\n      for(int i = 0; i <= 5; ++ i)\n      {\n        cout << \"length5(\" << i << \") = \";\n        cout << length5(i) << endl;\n      }\n\n      return 0;\n    }\n\n```\n\n正如我们在前面的代码中看到的，我们有一个名为`curry`的变量模板和函数。我们将使用这个模板来构造一个 currying function。在正常的函数调用中，我们可以如下调用`areaOfRectangle()`函数:\n\n```cpp\n    int i = areaOfRectangle(5, 2);\n\n```\n\n正如我们在前面的代码片段中看到的，我们将`5`和`2`作为参数传递给`areaOfRectangle()`函数。然而，使用 curried 函数，我们可以减少`areaOfRectangle()`函数，所以我们只有一个参数。我们要做的就是调用 curry 函数模板，如下所示:\n\n```cpp\n auto length5 = curry(areaOfRectangle, 5);\n\n```\n\n现在，我们有了`areaOfRectangle()`函数，该函数具有名为`length5`的`length`参数的值。我们更容易调用函数，只添加`width`参数，如以下代码片段所示:\n\n```cpp\n length5(i) // where i is the width parameter we want to pass\n\n```\n\n让我们看看运行前面的代码时在控制台上看到的输出:\n\n![](img/1097f10c-3b57-448c-8d71-fc69bf9f4918.png)\n\n变量模板和函数帮助我们将`areaOfRectangle()`函数简化为`length5()`函数。但是，它也可以帮助我们减少具有两个以上参数的函数。假设我们有一个名为`volumeOfRectanglular()`的函数，它传递三个参数。我们也将减少函数，如下面的代码所示:\n\n```cpp\n    /* curry_2.cpp */\n\n    #include <functional>\n    #include <iostream>\n\n    using namespace std;\n\n    // Variadic template for currying\n    template<typename Func, typename... Args>\n    auto curry(Func func, Args... args)\n    {\n      return [=](auto... lastParam)\n      {\n        return func(args..., lastParam...);\n      };\n    }\n\n    int volumeOfRectanglular(\n      int length,\n      int width,\n      int height)\n     {\n        return length * width * height;\n     }\n\n    auto main() -> int\n    {\n      cout << \"[curry_2.cpp]\" << endl;\n\n      // Currying the volumeOfRectanglular() function\n      auto length5width4 = curry(volumeOfRectanglular, 5, 4);\n\n      // Invoking the curried function\n      cout << \"Curried with spesific data:\" << endl;\n      cout << \"length = 5, width 4\" << endl;\n      for(int i = 0; i <= 5; ++ i)\n      {\n        cout << \"length5width4(\" << i << \") = \";\n        cout << length5width4(i) << endl;\n      }\n\n      return 0;\n    }\n\n```\n\n正如我们在前面的代码中看到的，我们已经成功地将`length`和`width`参数传递给了`volumeOfRectanglular()`函数，然后将其简化为`length5width4()`。我们可以调用`length5width4()`函数，并将剩下的参数`height`传递给它。如果运行前面的代码，下面是我们将在控制台屏幕上看到的输出:\n\n![](img/dbbc74e9-74e1-4a84-af5e-59a4b8494f0c.png)\n\n通过使用 currying 技术，我们可以通过减少一个函数来部分求值一个多参数函数，这样它将只传递一个参数。\n\n# 摘要\n\n我们已经讨论了一些操作函数的技术。我们将从中获得许多好处。因为我们可以用 C++ 语言实现第一类函数，所以我们可以将一个函数作为另一个函数的参数传递。我们可以将函数视为数据对象，这样我们就可以将其分配给变量并将其存储在容器中。此外，我们可以从现有的函数组成一个新的函数。此外，通过使用映射、过滤和折叠，我们可以在创建的每个函数中实现高阶函数。\n\n为了获得更好的函数代码，我们必须实现的另一种技术是纯函数，以避免副作用。我们可以重构我们拥有的所有函数，这样它就不会与外部变量或状态对话，也不会从外部状态改变和检索值。此外，为了减少多参数函数以便我们可以求值它的序列，我们可以对我们的函数实现 currying 技术。\n\n在下一章中，我们将讨论另一种避免副作用的技术。我们将使代码中的所有状态都不可变，这样就不会有每次调用函数时都会发生变化的状态。"
  },
  {
    "path": "docs/learn-cpp-func-prog/3.md",
    "content": "# 三、将不可变状态应用于函数\n\n在前一章讨论了一级函数和纯函数之后，现在我们来讨论一个可变的和不可变的对象。如您所知，我们必须能够将一个函数传递给一级函数中的另一个函数，并确保如果我们也传递相同的参数，该函数将返回相同的值。我们将讨论的不可变对象可以帮助我们在代码中使用这两个函数式编程概念。我们将在本章中讨论的主题如下:\n\n*   在函数式编程方法中修改变量\n*   演示如何使用`const`关键字来避免修改值\n*   将一流的纯函数应用于不可变对象\n*   将可变对象重构为不可变对象\n*   不可变对象优于可变对象的好处\n\n# 从不可变对象理解本质部分\n\n在面向对象编程中，我们通常多次操作变量对象，甚至在类本身内部，我们通常将其描述为属性。此外，我们有时会从特定函数更改全局变量。然而，要在函数式编程中获得不变性，我们必须遵守两个规则。首先，我们不允许改变局部变量。其次，我们必须避免函数中涉及全局变量，因为它会影响函数结果。\n\n# 修改局部变量\n\n当我们谈论变量时，我们谈论的是存储数据的容器。在日常编程中，我们通常会重用自己创建的变量。说清楚一点，我们来看看`mutable_1.cpp`代码。我们有`mutableVar`变量并将`100`存储到其中。然后我们为`i`变量迭代操作它的值。代码编写如下:\n\n```cpp\n    /* mutable_1.cpp */\n    #include <iostream>\n\n    using namespace std;\n\n    auto main() -> int\n    {\n      cout << \"[mutable_1.cpp]\" << endl;\n\n      // Initializing an int variable\n      int mutableVar = 100;\n      cout << \"Initial mutableVar = \" << mutableVar;\n      cout << endl;\n\n      // Manipulating mutableVar\n      for(int i = 0; i <= 10; ++ i)\n        mutableVar = mutableVar + i;\n\n      // Displaying mutableVar value\n      cout << \"After manipulating mutableVar = \" << mutableVar;\n      cout << endl;\n\n      return 0;\n    }\n\n```\n\n我们应该在屏幕上看到的结果将如下截图所示:\n\n![](img/7afd04cb-5f89-48eb-b264-e8687a17bda1.png)\n\n如我们所见，我们已经成功操纵了`mutableVar`变量。然而，我们将`mutableVar`变量视为可变对象。这是因为我们多次重复使用`mutableVar`变量。换句话说，我们打破了前面讨论的不可改变的规则。如果我们愿意，我们可以将`mutable_1.cpp`代码重构为不可变的代码。我们来分析一下`immutable_1.cpp`代码。在这里，我们将在每次打算更改之前的变量时创建一个新的局部变量。代码编写如下:\n\n```cpp\n    /* immutable_1.cpp */\n    #include <iostream>\n\n    using namespace std;\n\n    auto main() -> int\n    {\n      cout << \"[immutable_1.cpp]\" << endl;\n\n      // Initializing an int variable\n      int mutableVar = 100;\n      cout << \"Initial mutableVar = \" << mutableVar;\n      cout << endl;\n\n      // Manipulating mutableVar using immutable approach\n      int mutableVar0 = mutableVar + 0;\n int mutableVar1 = mutableVar0 + 1;\n int mutableVar2 = mutableVar1 + 2;\n int mutableVar3 = mutableVar2 + 3;\n int mutableVar4 = mutableVar3 + 4;\n int mutableVar5 = mutableVar4 + 5;\n int mutableVar6 = mutableVar5 + 6;\n int mutableVar7 = mutableVar6 + 7;\n int mutableVar8 = mutableVar7 + 8;\n int mutableVar9 = mutableVar8 + 9;\n int mutableVar10 = mutableVar9 + 10;\n\n      // Displaying mutableVar value in mutable variable\n      cout << \"After manipulating mutableVar = \" << mutableVar10;\n      cout << endl;\n\n      return 0;\n    }\n\n```\n\n可以看到，为了避免改变局部变量`mutableVar`，我们创建了另外十个局部变量。结果存储在`mutableVar10`变量中。然后我们向控制台显示结果。的确，这在我们的编程活动习惯中并不常见。然而，这是我们获得不可变对象的方法。通过这种不变的方法，我们永远不会错过之前的状态，因为我们拥有所有的状态。此外，我们通过运行`immutable_1.cpp`获得的输出与`mutable_1.cpp`代码的输出完全相同，如下图所示:\n\n![](img/7fd867be-dd5e-4fa2-9b1f-e652f6ca741e.png)\n\n但是，由于`immutable_1.cpp`中的代码行比`mutable_1.cpp`中的代码多，因此`immutable_1.cpp`代码的性能会比`mutable_1.cpp`代码慢。此外，`mutable_1.cpp`代码当然比`immutable_1.cpp`代码更高效。\n\n# 修改传递给函数的变量\n\n现在，我们将讨论当变量被传递给函数时如何修改它。假设我们有一个名为`n`的变量，它包含一个字符串数据。然后，我们将其作为参数传递给名为`Modify()`的函数。在函数内部，我们操纵名称变量。我们来看看下面的`immutable_2.cpp`代码，分析一下:\n\n```cpp\n    /* immutable_2.cpp */\n    #include <iostream>\n\n    using namespace std;\n\n    void Modify(string name)\n    {\n      name = \"Alexis Andrews\";\n    }\n\n    auto main() -> int\n    {\n      cout << \"[immutable_2.cpp]\" << endl;\n\n      // Initializing a string variable\n      string n = \"Frankie Kaur\";\n      cout << \"Initial name = \" << n;\n      cout << endl;\n\n      // Invoking Modify() function\n      // to modify the n variable\n      Modify(n);\n\n      // Displaying n value\n      cout << \"After manipulating = \" << n;\n      cout << endl;\n\n      return 0;\n    }\n\n```\n\n从前面的代码中，我们看到我们将`Frankie Kaur`存储为`n`变量的初始值，然后在`Modify()`函数中修改为`Alexis Andrews`。现在，让我们看看运行前面代码时屏幕上的输出:\n\n![](img/c35acf8e-d920-473a-9b15-c35a7f4b7f59.png)\n\n从前面的截图中我们可以看到，名称变量仍然包含`Frankie Kaur`作为其值，尽管我们已经在`Modify()`函数内部对其进行了修改。这是因为我们在`main()`函数中传递`n`变量，`Modify()`函数接收存储在`name`变量中的值的副本，因此名称变量保持不变并包含原始值。如果我们将`n`变量作为引用传递，我们可以对其进行变异，如下面的`mutable_2.cpp`代码所示:\n\n```cpp\n    /* mutable_2.cpp */\n    #include <iostream>\n\n    using namespace std;\n\n    void Modify(string &name)\n    {\n      name = \"Alexis Andrews\";\n    }\n\n    auto main() -> int\n    {\n      cout << \"[mutable_2.cpp]\" << endl;\n\n      // Initializing a string variable\n      string n = \"Frankie Kaur\";\n      cout << \"Initial name = \" << n;\n      cout << endl;\n\n      // Invoking Modify() function\n      // to modify the n variable\n      Modify(n);\n\n      // Displaying n value\n      cout << \"After manipulating = \" << n;\n      cout << endl;\n\n      return 0;\n    }\n\n```\n\n只需在`Modify()`函数的参数中添加&符号(`&`)即可将参数作为引用传递。屏幕上的输出将如下图所示:\n\n![](img/e2ffb9ca-57f6-4a70-83f2-ed28d0965f01.png)\n\n根据前面的截图，由于我们通过了`n`变量的引用，而不是值本身，因此`Modify()`函数中的`n`变量现在已经成功更改。还有另一种使用结构或类类型变异变量的最佳方法，正如我们在下面的`mutable_2a.cpp`代码中看到的:\n\n```cpp\n     /* mutable_2a.cpp */\n     #include <iostream>\n\n     using namespace std;\n\n class Name\n {\n       public:\n string str;\n };\n\n     void Modify(Name &name)\n     {\n       name.str = \"Alexis Andrews\";\n     }\n\n     auto main() -> int\n     {\n       cout << \"[mutable_2a.cpp]\" << endl;\n\n       // Initializing a string variable\n       Name n = {\"Frankie Kaur\"};\n       cout << \"Initial name = \" << n.str;\n       cout << endl;\n\n       // Invoking Modify() function\n       // to modify the n variable\n       Modify(n);\n\n       // Displaying n value\n       cout << \"After manipulating = \" << n.str;\n       cout << endl;\n\n       return 0;\n    }\n\n```\n\n正如我们在前面的代码中看到的，我们有一个名为`Name`的类，它包含一个字符串变量。开始时，我们用初始值实例化`Name`类。然后我们修改类内的`str`值。如果我们运行代码，我们将获得与`mutable_2.cpp`代码完全相同的输出。然而，我们看到虽然`n`变量没有改变，`name.str`却改变了。\n\n# 防止修改值\n\n不变性的本质是防止值的修改。在 C++ 编程语言中，有一个关键字可以防止代码修改值。关键字是`const`，我们将在`const.cpp`代码中使用它。我们有一个名为`MyAge`的类，它包含一个名为`age`的公共字段，我们将其设置为`const`。我们将使用这个`const`字段，代码如下所示:\n\n```cpp\n    /* const.cpp */\n    #include <iostream>\n\n    using namespace std;\n\n    // My Age class will store an age value\n    class MyAge\n    {\n       public:\n         const int age;\n         MyAge(const int initAge = 20) :\n          age(initAge)\n         {\n         }\n     };\n\n    auto main() -> int\n    {\n      cout << \"[const.cpp]\" << endl;\n\n      // Initializing several MyAge variables\n      MyAge AgeNow, AgeLater(8);\n\n      // Displaying age property in AgeNow instance\n      cout << \"My current age is \";\n      cout << AgeNow.age << endl;\n\n      // Displaying age property in AgeLater instance\n      cout << \"My age in eight years later is \";\n      cout << AgeLater.age << endl;\n\n      return 0;\n    }\n\n```\n\n正如我们在前面的代码中看到的，我们实例化了两个`MyAge`类；他们是`AgeNow`和`AgeLater`。对于`AgeNow`，我们使用年龄的初始值，而对于`AgeLater`，我们将`8`给`age`字段。控制台上的输出如下:\n\n![](img/ea608d37-968f-4063-8110-67ff692584e3.png)\n\n但是，无法将分配插入年龄字段。以下`const_error.cpp`代码将不会运行，因为编译器会拒绝它:\n\n```cpp\n    /* const_error.cpp */\n    #include <iostream>\n\n    using namespace std;\n\n    // My Age class will store an age value\n    class MyAge\n    {\n       public:\n         const int age;\n         MyAge(const int initAge = 20) :\n          age(initAge)\n        {\n        }\n    };\n\n    auto main() -> int\n    {\n      cout << \"[const_error.cpp]\" << endl;\n\n      // Initializing several MyAge variables\n      MyAge AgeNow, AgeLater(8);\n\n      // Displaying age property in AgeNow instance\n      cout << \"My current age is \";\n      cout << AgeNow.age << endl;\n\n      // Displaying age property in AgeLater instance\n      cout << \"My age in eight years later is \";\n      cout << AgeLater.age << endl;\n\n      // Trying to assign age property\n      // in AgeLater instance\n      // However, the compiler will refuse it\n      AgeLater.age = 10;\n\n      return 0;\n    }\n\n```\n\n如我们所见，我们将`age`值修改为`10`。由于`age`被设置为`const`，编译器将拒绝运行，并将显示以下错误:\n\n![](img/ce1c13f3-673b-4574-9687-2f511443d3cc.png)\n\n因此，我们通过添加`const`关键字成功地创建了一个不可变的对象。\n\n# 将第一类函数和纯函数应用于不可变对象\n\n我们从前面的讨论中获得了对不可变对象的介绍。正如您在上一章中所学习的，我们可以利用一流的函数和纯函数来创建一种不可变的编程方法。借用[章*2*](2.html)*在功能编程*中操作功能的代码，即`first_class_1.cpp`。我们将在下面的`first_class_pure_immutable.cpp`代码中使用`addition()`、`subtraction()`、`multiplication()`和`division()`方法。然后，我们将调用类的纯函数，并将结果赋给变量。代码编写如下:\n\n```cpp\n    /* first_class_pure_immutable.cpp */\n    #include <iostream>\n\n    using namespace std;\n\n    // MyValue class stores the value\n    class MyValue\n    {\n      public:\n        const int value;\n        MyValue(int v) : value(v)\n       {\n       }\n    };\n\n    // MyFunction class stores the methods\n    class MyFunction\n    {\n      public:\n        const int x, y;\n\n        MyFunction(int _x, int _y) :\n        x(_x), y(_y)\n       {\n       }\n\n      MyValue addition() const\n      {\n        return MyValue(x + y);\n      }\n\n      MyValue subtraction() const\n     {\n        return MyValue(x - y);\n      }\n\n     MyValue multiplication() const\n     {\n        return MyValue(x * y);\n     }\n\n     MyValue division() const\n     {\n        return MyValue(x / y);\n     }\n   };\n\n    auto main() -> int\n    {\n      cout << \"[first_class_pure_immutable.cpp]\" << endl;\n\n      // Setting the initial value\n      // for MyFunction class constructor\n      int a = 100;\n      int b = 10;\n\n      // Displaying initial value\n      cout << \"Initial value\" << endl;\n      cout << \"a = \" << a << endl;\n      cout << \"b = \" << b << endl;\n      cout << endl;\n\n      // Constructing the MyFunction class\n      MyFunction func(a, b);\n\n      // Generating wrapper for each function\n      // in the MyFunction class\n      // so it will be the first-class function\n      auto callableAdd = mem_fn(&MyFunction::addition);\n      auto callableSub = mem_fn(&MyFunction::subtraction);\n      auto callableMul = mem_fn(&MyFunction::multiplication);\n      auto callableDiv = mem_fn(&MyFunction::division);\n\n      // Invoking the functions\n      auto value1 = callableAdd(func);\n      auto value2 = callableSub(func);\n      auto value3 = callableMul(func);\n      auto value4 = callableDiv(func);\n\n      // Displaying result\n      cout << \"The result\" << endl;\n      cout << \"addition = \" << value1.value << endl;\n      cout << \"subtraction = \" << value2.value << endl;\n      cout << \"multiplication = \" << value3.value << endl;\n      cout << \"division = \" << value4.value << endl;\n\n      return 0;\n    }\n\n```\n\n正如我们在前面的代码中看到的那样，`addition()`、`subtraction()`、`multiplication()`和`division()`方法是一个纯函数，因为只要它们接收到相同的输入，它们就会产生相同的输出。我们还制作了一个名为`MyValue`的类，并将其设置为`const`以使其不变。然后，为了使我们的函数成为一流的函数，我们使用`mem_fn()`函数将每个方法包装在`MyFunction`类中。之后，我们用我们得到的函数包装器分配四个变量。屏幕上的输出应该如下图所示:\n\n![](img/e8a3cb17-011e-4742-ac95-dd0c36683a35.png)\n\n# 开发不可变对象\n\n在我们讨论了不变的概念之后，现在让我们开发不变的对象。我们将首先从可变对象开始，然后将其重构为不可变对象。\n\n# 从可变对象开始\n\n现在，让我们走得更远。我们将创建另一个类来设计一个不可变的对象。首先，我们将创建一个名为`MutableEmployee`的可变类。我们在那个类中有一些字段和方法。该类的标题类似于下面这段代码:\n\n```cpp\n    /* mutableemployee.h */\n    #ifndef __MUTABLEEMPLOYEE_H__\n    #define __MUTABLEEMPLOYEE_H__\n\n    #include <string>\n\n    class MutableEmployee\n    {\n      private:\n        int m_id;\n        std::string m_firstName;\n        std::string m_lastName;\n        double m_salary;\n\n     public:\n       MutableEmployee(\n         int id,\n         const std::string& firstName,\n         const std::string& lastName,\n         const double& salary);\n       MutableEmployee();\n\n       void SetId(const int id);\n       void SetFirstName(\n        const std::string& FirstName);\n       void SetLastName(\n        const std::string& LastName);\n       void SetSalary(\n        const double& Salary);\n\n       int Id() const {return m_id;}\n       std::string FirstName() const {return m_firstName;}\n       std::string LastName() const {return m_lastName;}\n       double Salary() const {return m_salary;}\n     };\n\n    #endif // End of __MUTABLEEMPLOYEE_H__\n\n```\n\n如我们所见，我们有四个字段- `m_id`、`m_firstName`、`m_lastName`和`m_salary`。我们还定义了四种方法来存储这些字段的任何值。这些方法的实现如下:\n\n```cpp\n    /* mutableemployee.cpp */\n    #include \"mutableemployee.h\"\n\n    using namespace std;\n\n    MutableEmployee::MutableEmployee() :\n      m_id(0),\n      m_salary(0.0)\n    {\n    }\n\n    MutableEmployee::MutableEmployee(\n      int id,\n      const string& firstName,\n      const string& lastName,\n      const double& salary) :\n        m_id(id),\n        m_firstName(firstName),\n        m_lastName(lastName),\n        m_salary(salary)\n    {\n    }\n\n    void MutableEmployee::SetId(const int id)\n    {\n      m_id = id;\n    }\n\n    void MutableEmployee::SetFirstName(\n      const std::string& FirstName) {\n        m_firstName = FirstName;\n      }\n\n    void MutableEmployee::SetLastName(\n      const std::string& LastName) {\n        m_lastName = LastName;\n      }\n\n   void MutableEmployee::SetSalary(\n      const double& Salary) {\n        m_salary = Salary;\n      }\n\n```\n\n正如我们在前面的代码中看到的，我们有一个很好的 OOP 代码，其中的成员是私有的；然而，我们可以通过 setters 和 getters 访问它们。换句话说，任何代码都可以改变任何值，使其可变。现在，让我们使用这个即将到来的`mutable_3.cpp`代码来消费前面的类。我们将使用初始值实例化该类，并尝试对其进行变异。代码如下所示:\n\n```cpp\n    /* mutable_3.cpp */\n    #include <iostream>\n    #include \"../mutableemployee/mutableemployee.h\"\n\n    using namespace std;\n\n    auto main() -> int\n    {\n      cout << \"[mutable_3.cpp]\" << endl;\n\n      // Initializing several variables\n      string first = \"Frankie\";\n      string last = \"Kaur\";\n      double d = 1500.0;\n\n      // Creating an instance of MutableEmployee\n      MutableEmployee me(0, first, last, d);\n\n      // Displaying initial value\n      cout << \"Content of MutableEmployee instance\" << endl;\n      cout << \"ID : \" << me.Id() << endl;\n      cout << \"Name : \" << me.FirstName();\n      cout << \" \" << me.LastName() << endl;\n      cout << \"Salary : \" << me.Salary() << endl << endl;\n\n      // Mutating the instance of MutableEmployee\n      me.SetId(1);\n      me.SetFirstName(\"Alexis\");\n      me.SetLastName(\"Andrews\");\n      me.SetSalary(2100.0);\n\n      // Displaying mutate value\n      cout << \"Content of MutableEmployee after mutating\" << endl;\n      cout << \"ID : \" << me.Id() << endl;\n      cout << \"Name : \" << me.FirstName();\n      cout << \" \" << me.LastName() << endl;\n      cout << \"Salary : \" << me.Salary() << endl;\n\n      return 0;\n    }\n\n```\n\n正如我们在前面的代码中看到的，初始值存储在三个变量中- `first`、`last`和`d`。然后，我们将使用 setter 成功地变异实例。输出应如下所示:\n\n![](img/50a47674-e3af-4527-b437-7c12d7e567d3.png)\n\n前面的截图给我们展示了`MutableEmployee`类的变异结果。因为我们需要通过避免突变状态来避免副作用，所以我们必须将类重构为不可变的类。\n\n# 将可变对象重构为不可变对象\n\n正如我们之前讨论的，为了避免副作用，我们必须将我们的类设计成一个不可变的对象。我们将重构之前的`MutableEmployee`类。让我们看看下面的头类:\n\n```cpp\n    /* immutableemployee.h */\n    #ifndef __IMMUTABLEEMPLOYEE_H__\n    #define __IMMUTABLEEMPLOYEE_H__\n\n    #include <string>\n\n    class ImmutableEmployee\n    {\n      private:\n        int m_id;\n        std::string m_firstName;\n        std::string m_lastName;\n        double m_salary;\n\n     public:\n       ImmutableEmployee(\n         const int id,\n         const std::string& firstName,\n         const std::string& lastName,\n         const double& _salary);\n       ImmutableEmployee();\n\n       const int Id() const {\n          return m_id;\n       }\n\n       const std::string& FirstName() const {\n         return m_firstName;\n       }\n\n       const std::string& LastName() const {\n         return m_lastName;\n       }\n\n       const double Salary() const {\n        return m_salary;\n       }\n    };\n\n    #endif // End of __IMMUTABLEEMPLOYEE_H__\n\n```\n\n正如我们在前面的头代码中看到的，我们从前面的`MutableEmployee`类中移除了设置器。我们这样做是为了使`ImmutableEmployee`类不变。标题的实现可以在下面的代码中找到:\n\n```cpp\n    /* immutableemployee.cpp */\n    #include \"immutableemployee.h\"\n\n    using namespace std;\n\n    ImmutableEmployee::ImmutableEmployee() :\n      m_id(0),\n      m_salary(0.0)\n      {\n      }\n\n    ImmutableEmployee::ImmutableEmployee(\n      const int id,\n      const string& firstName,\n      const string& lastName,\n      const double& salary) :\n        m_id(id),\n        m_firstName(firstName),\n        m_lastName(lastName),\n        m_salary(salary)\n      {\n      }\n\n```\n\n现在，让我们分析一下`ImmutableEmployee`类，并将其与`MutableEmployee`类进行比较。以下是我们应该获得的:\n\n*   我们现在将所有成员变量设置为`const`，这意味着变量只能在构造函数中初始化。这将是创建不可变对象的最佳方法。但是`const`成员阻止对其他成员进行移动操作，这是一个整洁的 C++ 11 优化。\n*   getter 方法现在返回`const`引用，而不是值。由于不可变对象不能修改值，最好返回对它们的引用。\n*   getters 现在返回`const`值，以避免结果被其他语句修改。它还防止了一些常见的错误，比如使用`=`而不是`==`进行比较。它声明了我们使用不可变类型的事实。\n\n例如，如果我们想要更改`m_firstName`或`m_salary`字段，就会出现问题。为了解决这个问题，我们可以将二传手添加到`ImmutableEmployee`类中。但是，它现在返回`ImmutableEmployee`实例，而不是突变场目标。`immutableemployee.h`代码如下:\n\n```cpp\n    /* immutableemployee.h */\n    #ifndef __IMMUTABLEEMPLOYEE_H__\n    #define __IMMUTABLEEMPLOYEE_H__\n\n    #include <string>\n\n    class ImmutableEmployee\n    {\n      private:\n       int m_id;\n       std::string m_firstName;\n       std::string m_lastName;\n       double m_salary;\n\n      public:\n        ImmutableEmployee(\n          const int id,\n          const std::string& firstName,\n          const std::string& lastName,\n          const double& _salary);\n        ImmutableEmployee();\n        ~ImmutableEmployee();\n\n        const int Id() const {\n          return m_id;\n        }\n\n        const std::string& FirstName() const {\n          return m_firstName;\n        }\n\n        const std::string& LastName() const {\n          return m_lastName;\n         }\n\n        const double Salary() const {\n          return m_salary;\n         }\n\n        const ImmutableEmployee SetId(\n          const int id) const {\n            return ImmutableEmployee(\n              id, m_firstName, m_lastName, m_salary);\n          }\n\n       const ImmutableEmployee SetFirstName(\n          const std::string& firstName) const {\n            return ImmutableEmployee(\n              m_id, firstName, m_lastName, m_salary);\n          }\n\n       const ImmutableEmployee SetLastName(\n          const std::string& lastName) const {\n            return ImmutableEmployee(\n              m_id, m_firstName, lastName, m_salary);\n          }\n\n       const ImmutableEmployee SetSalary(\n          const double& salary) const {\n            return ImmutableEmployee(\n              m_id, m_firstName, m_lastName, salary);\n          }\n      };\n\n    #endif // End of __IMMUTABLEEMPLOYEE_H__\n\n```\n\n如我们所见，现在，在`immutableemployee.h`文件中，我们有四个设置器。分别是`SetId`、`SetFirstName`、`SetLastName`和`SetSalary`。虽然`ImmutableEmployee`类中 setter 的名称与`MutableEmployee`类完全相同，但是在`ImmutableEmployee`类中，setter 返回类的实例，正如我们前面讨论的那样。通过使用这个`ImmutableEmployee`类，我们必须采用函数方法，因为这个类是不可变的对象。下面的代码是`immutable_3.cpp`，我们从`mutable_3.cpp`文件中重构出来的:\n\n```cpp\n    /* immutable_3.cpp */\n    #include <iostream>\n    #include \"../immutableemployee/immutableemployee.h\"\n\n    using namespace std;\n\n    auto main() -> int\n    {\n      cout << \"[immutable_3.cpp]\" << endl;\n\n      // Initializing several variables\n      string first = \"Frankie\";\n      string last = \"Kaur\";\n      double d = 1500.0;\n\n      // Creating the instance of ImmutableEmployee\n      ImmutableEmployee me(0, first, last, d);\n\n      // Displaying initial value\n      cout << \"Content of ImmutableEmployee instance\" << endl;\n      cout << \"ID : \" << me.Id() << endl;\n      cout << \"Name : \" << me.FirstName()\n      << \" \" << me.LastName() << endl;\n      cout << \"Salary : \" << me.Salary() << endl << endl;\n\n      // Modifying the initial value\n      ImmutableEmployee me2 = me.SetId(1);\n      ImmutableEmployee me3 = me2.SetFirstName(\"Alexis\");\n      ImmutableEmployee me4 = me3.SetLastName(\"Andrews\");\n      ImmutableEmployee me5 = me4.SetSalary(2100.0);\n\n      // Displaying the new value\n      cout << \"Content of ImmutableEmployee after modifying\" << endl;\n      cout << \"ID : \" << me5.Id() << endl;\n      cout << \"Name : \" << me5.FirstName()\n      << \" \" << me5.LastName() << endl;\n      cout << \"Salary : \" << me5.Salary() << endl;\n\n      return 0;\n    }\n\n```\n\n正如我们在前面的代码中看到的，我们通过实例化其他四个`ImmutableEmployee`类来修改内容- `me2`、`me3`、`me4`和`me5`。这类似于我们在`immutable_1.cpp`中所做的。然而，我们现在处理一个类。前面代码的输出应该如下图所示:\n\n![](img/a09d9280-17e5-4508-bcc2-af9cafd1a3eb.png)\n\n通过获得前面的输出，我们可以说我们已经成功地修改了`ImmutableEmployee`类的实例，而没有对其进行变异。\n\n# 列举不可变的好处\n\n经过我们的讨论，我们现在知道不可变对象是函数式编程的重要部分。以下是我们可以从不可变对象中获得的好处:\n\n*   我们不会处理副作用。这是因为我们已经确保没有外部状态被修改。每次我们打算更改对象内部的值时，也会创建一个新对象。\n*   没有无效对象的状态。因为我们会一直处于不一致的状态。如果我们忘记调用特定的方法，我们肯定会得到正确的状态，因为方法之间没有联系。\n*   它将是线程安全的，因为我们可以一起运行许多方法，而不需要锁定在池中运行的第一个方法。换句话说，我们永远不会面临任何同步问题。\n\n# 摘要\n\n首先，在本章中，我们试图用函数的方式修改局部变量。我们不能重用我们创建的变量；相反，当我们需要修改它时，我们必须创建另一个。我们还讨论了修改传递给另一个函数的变量的技术。我们必须通过引用传递参数来改变它，而不是通过值传递参数。\n\n然后，我们挖掘`const`关键字的使用，为函数提供不可变的行为。通过使用这个关键字，我们可以确保类内部的变量不能被修改。另一个讨论是关于应用一流的纯函数——你在上一章学到的东西——来获得不变性的力量。\n\n我们还创建了可变类，然后将其重构为不可变类。我们现在能够区分可变和不可变的对象，并且可以在我们的函数代码中应用它。最后，在本章中，我们列举了不可变对象的好处，因此我们有信心在日常代码中使用它。\n\n另一个问题现在可能会出现在我们的脑海中。如果我们必须处理不可变的对象，我们如何运行递归？我们甚至不能修改方法中的一个变量。在下一章中，我们将通过讨论函数式编程中的递归来解决这个问题。"
  },
  {
    "path": "docs/learn-cpp-func-prog/4.md",
    "content": "# 四、使用递归算法重复方法调用\n\n在上一章中，您学习了不可变状态，这些状态使我们无法处理副作用。在本章中，让我们来看看递归的概念。作为面向对象编程的程序员，我们通常使用迭代来重复这个过程，而不是递归。然而，递归比迭代带来更多的好处。例如，一些问题(尤其是数学)很容易用递归解决，幸运的是，所有的算法都可以递归定义。这使得可视化和证明变得非常非常容易。为了进一步了解递归，本章将讨论以下主题:\n\n*   区分迭代和递归调用\n*   重复不可变函数\n*   用尾部递归寻找更好的递归方法\n*   列举了三种递归——函数递归、过程递归和回溯递归\n\n# 递归重复函数调用\n\n作为一名程序员，尤其是在面向对象编程中，我们通常使用迭代技术来重复我们的过程。现在，我们将讨论递归方法来重复我们的过程，并在函数方法中使用它。基本上，递归和迭代执行相同的任务，也就是一件一件地解决复杂的任务，然后组合结果。然而，它们有所不同。迭代过程强调我们应该不断重复这个过程，直到任务完成，而递归强调需要将任务分解成更小的部分，直到我们能够解决任务，然后组合结果。当我们需要运行某个进程直到达到极限时，我们可以使用迭代过程，或者读取一个流直到它达到`eof()`。此外，当我们使用递归时，递归可以给出最佳值，例如，在计算阶乘时。\n\n# 执行迭代过程以重复该过程\n\n我们将从迭代过程开始。正如我们之前讨论的，如果使用递归方法设计阶乘，阶乘的计算会更好。然而，也可以用迭代的方法来设计它。这里，我们将有一个`factorial_iteration_do_while.cpp`代码，我们可以用来计算阶乘。我们将有一个名为`factorial ()`的函数，它传递一个参数来计算我们在参数中传递的阶乘值。代码应该如下所示:\n\n```cpp\n    /* factorial_iteration_do_while.cpp */\n    #include <iostream>\n\n    using namespace std;\n\n    // Function containing\n    // do-while loop iteration\n\n    int factorial (int n)\n    {\n      int result = 1;\n      int i = 1;\n\n      // Running iteration using do-while loop\n      do\n       {\n         result *= i;\n       }\n       while(++ i <= n);\n\n       return result;\n    }\n\n    auto main() -> int\n    {\n      cout << \"[factorial_iteration_do_while.cpp]\" << endl;\n\n      // Invoking factorial() function nine times\n      for(int i = 1; i < 10; ++ i)\n      {\n        cout << i << \"! = \" << factorial(i) << endl;\n      }\n\n      return 0;\n    } \n\n```\n\n正如我们在前面的代码中所看到的，我们依赖于`n`的值，我们将其传递给`factorial()`函数，以确定将发生多少次迭代。每次迭代执行时，`result`变量将乘以计数器`i`。最后，`result`变量将通过组合迭代的结果值来保存最后的结果。我们应该在屏幕上获得的输出如下:\n\n![](img/444549f8-8a38-48bb-b1e5-2be38e9f7bee.png)\n\n迭代中的另一种技术是使用另一个迭代过程。我们可以重构前面的代码来使用`factorial()`函数中的`for`循环。以下是从我们之前的`factorial_iteration_do_while.cpp`代码重构而来的`factorial_iteration_for.cpp`代码:\n\n```cpp\n    /* factorial_iteration_do_while.cpp */\n    #include <iostream>\n\n    using namespace std;\n\n    // Function containing\n    // for loop iteration\n    int factorial (int n)\n    {\n      int result = 1;\n\n      // Running iteration using for loop\n for(int i = 1; i <= n; ++ i)\n {\n result *= i;\n }\n\n      return result;\n     }\n\n     auto main() -> int\n     {\n      cout << \"[factorial_iteration_for.cpp]\" << endl;\n\n      // Invoking factorial() function nine times\n      for(int i = 1; i < 10; ++ i)\n       {\n         cout << i << \"! = \" << factorial(i) << endl;\n       }\n\n      return 0;\n    }\n\n```\n\n如我们所见，我们将`do-while`循环替换为`for`循环。但是，程序的行为将完全相同，因为每次迭代执行时，它还会将当前结果与`i`计数器相乘。在这个迭代的最后，我们将从这个乘法过程中获得最终结果。屏幕应显示以下输出:\n\n![](img/c604f1cb-6277-4b29-a074-4bbf92bd02bc.png)\n\n既然我们已经成功地执行了迭代来获得阶乘目的，要么使用`do-while`要么使用`for`循环。\n\nIt looks too trivial when we try to refactor the `do-while` loop into the `for` loop. As we may know, `for` loops allow us to run through the loop when we know how many times we'd like it to run through the problem, while the `do-while` loops give us more flexibility in what we put in it and when it will stop, for instance `while(i > 0)` or use a Boolean value such as `while(true)`. However, based on the preceding example, we now can say that we can switch the `for` loop or the `do-while` loop into recursion.\n\n# 执行递归过程以重复该过程\n\n我们之前讨论过递归在函数式编程中有更好的性能。我们还在迭代方法中开发了`factorial()`函数。现在，让我们将之前的代码重构为`factorial_recursion.cpp`，它将使用递归方法而不是迭代方法。与我们之前的代码相比，该代码将执行相同的任务。但是，我们将修改`factorial()`函数，使其在函数结束时调用自己。代码编写如下:\n\n```cpp\n    /* factorial_recursion.cpp */\n    #include <iostream>\n\n    using namespace std;\n\n    int factorial(int n)\n    {\n      // Running recursion here\n      if (n == 0)\n        return 1;\n      else\n        return n * factorial (n - 1);\n    }\n\n    auto main() -> int\n    {\n       cout << \"[factorial_recursion.cpp]\" << endl;\n\n      for(int i = 1; i < 10; ++ i)\n      {\n        cout << i << \"! = \" << factorial(i) << endl;\n      }\n\n      return 0;\n    }\n\n```\n\n正如我们所看到的，前面代码中的`factorial()`函数调用自己直到`n`是`0`。每次函数调用自己时，都会递减`n`参数。传递的参数为`0`后，该功能将很快返回`1`。与前面的两个代码块相比，我们也将获得相同的输出，如下图所示:\n\n![](img/b9c6c1c3-250a-4a02-961f-1ae358de0e60.png)\n\nAlthough recursion gives us the simplicity required to easily maintain code, we have to be aware of the parameter we pass to the recursion function. For instance, in the `factorial()` function in the `factorial_recursion.cpp` code, if we pass the negative number to the `n < 0` function, we will get the infinity loop, and it can crash our device.\n\n# 重复不可变函数\n\n正如我们在上一章中讨论的，我们需要递归地循环不可变函数。假设我们有不可变的`fibonacci()`函数。然后，我们需要将其重构为递归函数。`fibonacci_iteration.cpp`代码以迭代的方式实现`fibonacci()`函数。代码编写如下:\n\n```cpp\n    /* fibonacci_iteration.cpp */\n    #include <iostream>\n\n    using namespace std;\n\n    // Function for generating\n    // Fibonacci sequence using iteration\n    int fibonacci(int n)\n    {\n      if (n == 0)\n        return 0;\n\n      int previous = 0;\n      int current = 1;\n\n      for (int i = 1; i < n; ++ i)\n      {\n        int next = previous + current;\n        previous = current;\n        current = next;\n      }\n\n      return current;\n    }\n\n    auto main() -> int\n    {\n      cout << \"[fibonacci_iteration.cpp]\" << endl;\n\n      // Invoking fibonacci() function ten times\n      for(int i = 0; i < 10; ++ i)\n       {\n         cout << fibonacci(i) << \" \";\n       }\n      cout << endl;\n\n      return 0;\n    }\n\n```\n\n正如我们在前面的代码中看到的那样，`fibonacci()`函数是不可变的，因为每次它获得完全相同的`n`输入时，它都会返回相同的值。输出应该如下图所示:\n\n![](img/bb62a358-6e52-44fb-9628-d55839169d48.png)\n\n如果需要将其重构为递归函数，可以使用以下`fibonacci_recursion.cpp`代码:\n\n```cpp\n    /* fibonacci_recursion.cpp */\n    #include <iostream>\n\n    using namespace std;\n\n    // Function for generating\n    // Fibonacci sequence using recursion\n    int fibonacci(int n)\n    {\n      if(n <= 1)\n        return n;\n\n      return fibonacci(n-1) + fibonacci(n-2);\n    }\n\n    auto main() -> int\n    {\n      cout << \"[fibonacci_recursion.cpp]\" << endl;\n\n      // Invoking fibonacci() function ten times\n      for(int i = 0; i < 10; ++ i)\n      {\n        cout << fibonacci(i) << \" \";\n      }\n      cout << endl;\n\n      return 0;\n    }\n\n```\n\n我们可以看到，前面的代码采用了递归方法，因为它在函数的末尾调用函数本身。现在我们有了递归`fibonacci()`函数，它将在控制台上给出如下输出:\n\n![](img/cb853f00-201a-493a-b561-5eed14833299.png)\n\n现在，与`fibonacci_iteration.cpp`代码相比，`fibonacci_recursion.cpp`代码显示完全相同的输出。\n\n# 更接近尾部递归\n\n当函数在最后执行递归调用时，会发生尾部递归。它被认为比我们之前开发的非尾部递归代码更好，因为编译器可以更好地优化代码。由于递归调用是该函数执行的最后一条语句，因此在该函数中没有其他事情可做。结果是编译器不需要保存当前函数的堆栈帧。让我们看看下面实现尾部递归的`tail_recursion.cpp`代码:\n\n```cpp\n    /* tail_recursion.cpp */\n    #include <iostream>\n\n    using namespace std;\n\n    void displayNumber(long long n)\n    {\n      // Displaying the current n value\n      cout << n << endl;\n\n      // The last executed statement \n      // is the recursive call\n      displayNumber(n + 1);\n    }\n\n    auto main() -> int\n    {\n      cout << \"[tail_recursion.cpp]\" << endl;\n\n      // Invoking the displayNumber() function\n      // containing tail recursion\n      displayNumber(0);\n\n      return 0;\n    }\n\n```\n\n正如我们在前面的代码中看到的那样，`displayNumber()`函数是一个尾部递归调用函数，因为它在进程结束时调用自己。事实上，如果我们运行前面的`tail_recursion.cpp`代码，程序不会结束，因为它会增加`displayNumber()`函数中`n`的值。当`n`的值达到`long long`数据类型的最大值时，程序可能会崩溃。但是，程序不会发出堆栈(堆栈溢出)，因为尾部递归不会在堆栈中存储值。\n\n此外，我们可以重构`tail_recursion.cpp`代码中前面的`displayNumber()`函数，使用`goto`关键字，而不是反复调用该函数。重构的代码可以在下面的`tail_recursion_goto.cpp`代码中看到:\n\n```cpp\n    /* tail_recursion_goto.cpp */\n    #include <iostream>\n\n    using namespace std;\n\n    void displayNumber(long long n)\n    {\n loop:\n        // Displaying the current n value\n        cout << n << endl;\n\n       // Update parameters of recursive call\n // and replace recursive call with goto\n n++ ;\n goto loop;\n    }\n\n    auto main() -> int\n    {\n      cout << \"[tail_recursion_goto.cpp]\" << endl;\n\n      // Invoking the displayNumber() function\n      // containing tail recursion\n      displayNumber(0);\n\n      return 0;\n    }\n\n```\n\n正如我们在前面的代码中看到的，我们可以用`goto`关键字删除`displayNumber()`函数中的最后一个调用。这就是编译器如何通过执行尾调用消除来优化尾递归，该消除将最后一次调用替换为`goto`关键字。我们还会看到`displayNumber()`功能中不需要堆栈。\n\nDon't forget to compile the code containing a tail recursion with the optimization option provided by the compiler. Since we use GCC, always enable optimization level 2 (`-O2`) to gain the optimized code. The effect of not compiling with optimizations enabled, is that our two preceding programs (`tail_recursion.cpp` and `tail_recursion_goto.cpp`) will crash with the stack overflowed issue. For more information about the optimizations option in GCC, check out [https://gcc.gnu.org/onlinedocs/gcc-7.1.0/gcc/Optimize-Options.html](https://gcc.gnu.org/onlinedocs/gcc-7.1.0/gcc/Optimize-Options.html).\n\n现在，让我们创建一个有用的尾部递归调用。在前一节中，我们已经成功地将迭代函数重构为递归函数。`factorial()`函数现在变成了递归函数，并在函数结束时调用自己。然而，它不是尾部递归，尽管函数在函数的末尾调用自己。如果我们仔细看的话，`factorial(n-1)`返回的值是在`factorial(n)`中使用的，所以对`factorial(n-1)`的调用并不是`factorial(n)`做的最后一件事。\n\n我们可以创建我们的`factorial_recursion.cpp`代码成为尾部递归函数。我们将开发以下`factorial_recursion_tail.cpp`代码，修改`factorial()`功能，并添加一个名为`factorialTail()`的新功能。代码编写如下:\n\n```cpp\n    /* factorial_recursion_tail.cpp */\n    #include <iostream>\n\n    using namespace std;\n\n // Function for calculating factorial\n // tail recursion\n int factorialTail(int n, int i)\n {\n if (n == 0)\n return i;\n\n return factorialTail(n - 1, n * i);\n } \n // The caller of tail recursion function\n int factorial(int n)\n {\n return factorialTail(n, 1);\n }\n\n    auto main() -> int\n    {\n      cout << \"[factorial_recursion_tail.cpp]\" << endl;\n\n      // Invoking fibonacci() function ten times\n      for(int i = 1; i < 10; ++ i)\n      {\n        cout << i << \"! = \" << factorial(i) << endl;\n      }\n\n     return 0;\n    }\n\n```\n\n如我们所见，我们已经将`factorial_recursion.cpp`代码中的`factorial()`函数移动到了`factorial_recursion_tail.cpp`代码中需要两个参数的`factorialTail()`函数。因此，在我们调用`factorial(i)`之后，它将调用`factorialTail()`函数。在该功能结束时，`factorialTail()`功能是唯一被调用的功能。下图是`factorial_recursion_tail.cpp`代码的输出，和`factorial_recursion.cpp`代码完全一样。这也证明了我们已经成功地将`factorial_recursion.cpp`代码重构为尾部递归。\n\n![](img/de6f4d4b-d59c-40cc-8ed7-f041a58a3d1b.png)\n\n# 熟悉函数、过程和回溯递归\n\n现在我们已经了解了一些关于递归的知识，递归函数将从它的体内调用它自己。递归只有在达到某个值时才会停止。我们马上要讨论的递归有三种:**函数递归**、**过程递归**、**回溯递归**；然而，这三种类型的递归可能不是标准术语。函数递归是返回一些值的递归过程。过程递归是一个递归过程，它不返回值，但在每次递归中执行它所采取的动作。回溯递归是一个递归过程，将任务分解成一小组子任务，如果这些子任务不起作用，就可以取消。让我们在下面的讨论中考虑这些递归类型。\n\n# 期待函数递归的结果\n\n在函数递归中，过程试图通过递归地组合子问题的结果来解决问题。我们结合的结果来自子问题的返回值。假设我们有一个计算一个数的幂的问题，例如，`2`幂`2`是`4` ( `2<sup>2</sup> = 4`)。通过使用迭代，我们可以构建像下面的`exponential_iteration.cpp`代码一样的代码。我们有一个名为`power()`的函数，它将通过两个参数传递- `base`和`exp`。符号为`base<sup>exp</sup>`，代码如下:\n\n```cpp\n    /* exponential_iteration.cpp */\n    #include <iostream>\n\n    using namespace std;\n\n    // Calculating the power of number\n    // using iteration\n    int power(int base, int exp)\n    {\n      int result = 1;\n\n      for(int i = 0; i < exp; ++ i)\n       {\n         result *= base;\n       }\n\n       return(result);\n    } \n\n    auto main() -> int\n    {\n      cout << \"[exponential_iteration.cpp]\" << endl;\n\n      // Invoking power() function six times\n      for(int i = 0; i <= 5; ++ i)\n      {\n        cout << \"power (2, \" << i << \") = \";\n        cout << power(2, i) << endl;\n      }\n\n      return 0;\n    }\n\n```\n\n正如我们在前面的代码中看到的，在我们进入递归版本之前，我们首先使用迭代版本，因为我们通常每天使用迭代最多。我们在每次迭代中将`result`值与`base`值相乘。如果我们运行前面的代码，我们将在控制台上获得以下输出:\n\n![](img/84a5e00c-542f-40cc-b398-d5124ddb94e3.png)\n\n现在，让我们将前面的代码重构为递归版本。我们将拥有`exponential_recursion.cpp`代码，该代码将具有相同的`power()`函数签名。然而，我们不会使用`for`循环来代替函数在函数末尾调用自己的递归。代码应编写如下:\n\n```cpp\n    /* exponential_recursion.cpp */\n    #include <iostream>\n\n    using namespace std;\n\n    // Calculating the power of number\n    // using recursion\n    int power(int base, int exp)\n    {\n      if(exp == 0)\n        return 1;\n      else\n        return base * power(base, exp - 1);\n    }\n\n    auto main() -> int\n    {\n      cout << \"[exponential_recursion.cpp]\" << endl;\n\n      // Invoking power() function six times\n      for(int i = 0; i <= 5; ++ i)\n      {\n        cout << \"power (2, \" << i << \") = \";\n        cout << power(2, i) << endl;\n      }\n\n      return 0;\n    }\n\n```\n\n正如我们前面讨论的函数递归返回值一样，`power()`函数是函数递归，因为它返回`int`值。我们将从每个子函数返回的值中得到最终结果。因此，我们将在控制台上获得以下输出:\n\n![](img/2488e227-b4d4-4275-98c4-a499df6d85de.png)\n\n# 在过程递归中递归运行任务\n\n因此，我们有一个函数递归，它期待函数的返回值。有时，我们不需要返回值，因为我们从函数内部运行任务。为了达到这个目的，我们可以使用过程递归。假设我们想要置换一个短字符串，以找到它的所有可能的排列。我们只需要在每次执行递归时打印结果，而不是返回值。\n\n我们有以下`permutation.cpp`代码来演示这个任务。它有`permute()`函数，调用一次，然后递归调用`doPermute()`函数。代码应编写如下:\n\n```cpp\n    /* permutation.cpp */\n    #include <iostream>\n\n    using namespace std;\n\n    // Calculation the permutation\n    // of the given string\n    void doPermute(\n      const string &chosen,\n      const string &remaining)\n      {\n       if(remaining == \"\")\n       {\n          cout << chosen << endl;\n       }\n       else\n       {\n         for(uint32_t u = 0; u < remaining.length(); ++ u)\n         {\n            doPermute(\n              chosen + remaining[u],\n              remaining.substr(0, u)\n              + remaining.substr(u + 1));\n         }\n       }\n    }     \n\n    // The caller of doPermute() function\n    void permute(\n      const string &s)\n    {\n      doPermute(\"\", s);\n    }\n\n    auto main() -> int\n    {\n      cout << \"[permutation.cpp]\" << endl;\n\n      // Initializing str variable\n      // then ask user to fill in\n      string str;\n      cout << \"Permutation of a string\" << endl;\n      cout << \"Enter a string: \";\n      getline(cin, str);\n\n      // Finding the possibility of the permutation\n      // by calling permute() function\n      cout << endl << \"The possibility permutation of \";\n      cout << str << endl;\n      permute(str);\n\n      return 0;\n    }\n\n```\n\n正如我们在前面的代码中看到的，我们要求用户输入一个字符串，然后代码将使用`permute()`函数找到这种排列的可能性。它将以`doPermute()`中的空字符串开始，因为用户给出的字符串也是可能的。控制台上的输出应该如下所示:\n\n![](img/7b9a4176-1af8-412e-a860-110982c72225.png)\n\n# 回溯递归\n\n正如我们之前讨论的，如果子任务不起作用，我们可以撤销这个过程。让我们试试迷宫，在那里我们必须找到从起点到终点的路。假设我们必须找到从`S`到`F`的路，如下图迷宫所示:\n\n```cpp\n    # # # # # # # #\n    # S           #\n    # # #   # # # #\n    #   #   # # # #\n    #             #\n    #   # # # # # #\n    #           F #\n    # # # # # # # #\n\n```\n\n要解决这个问题，我们必须决定我们需要的路线，找到终点。然而，我们会假设每个选择都是好的，直到我们证明它不是。递归将返回一个布尔值来标记它是否正确。如果我们选择了错误的方式，调用堆栈会展开，它会撤销选择。首先，我们将在代码中绘制`labyrinth`。在下面的代码中，将有`createLabyrinth()`和`displayLabyrinth()`功能。代码如下所示:\n\n```cpp\n    /* labyrinth.cpp */\n    #include <iostream>\n    #include <vector>\n\n    using namespace std;\n\n    vector<vector<char>> createLabyrinth()\n    {\n      // Initializing the multidimensional vector\n      // labyrinth \n      // # is a wall\n      // S is the starting point\n      // E is the finishing point\n      vector<vector<char>> labyrinth = \n      {\n        {'#', '#', '#', '#', '#', '#', '#', '#'},\n        {'#', 'S', ' ', ' ', ' ', ' ', ' ', '#'},\n        {'#', '#', '#', ' ', '#', '#', '#', '#'},\n        {'#', ' ', '#', ' ', '#', '#', '#', '#'},\n        {'#', ' ', ' ', ' ', ' ', ' ', ' ', '#'},\n        {'#', ' ', '#', '#', '#', '#', '#', '#'},\n        {'#', ' ', ' ', ' ', ' ', ' ', 'F', '#'},\n        {'#', '#', '#', '#', '#', '#', '#', '#'}\n     };\n\n     return labyrinth;\n    }\n\n    void displayLabyrinth(vector<vector<char>> labyrinth)\n    {\n      cout << endl;\n      cout << \"====================\" << endl;\n      cout << \"The Labyrinth\" << endl;\n      cout << \"====================\" << endl;\n\n      // Displaying all characters in labyrinth vector\n      for (int i = 0; i < rows; i++)\n      {\n        for (int j = 0; j < cols; j++)\n        {\n            cout << labyrinth[i][j] << \" \";\n        }\n        cout << endl;\n      }\n      cout << \"====================\" << endl << endl;\n    }\n\n    auto main() -> int\n    {\n      vector<vector<char>> labyrinth = createLabyrinth();\n      displayLabyrinth(labyrinth);\n\n      string line;\n      cout << endl << \"Press enter to continue...\" << endl;\n      getline(cin, line);\n\n      return 0;\n    }\n\n```\n\n正如我们所看到的，在前面的代码中没有递归。`createLabyrinth()`函数只是创建一个包含`labyrinth`模式的二维数组，而`displayLabyrinth()`只是将数组显示给控制台。如果运行前面的代码，我们将在控制台上看到以下输出:\n\n![](img/9e5caa72-94f1-4458-9bf4-e723d64e3fa7.png)\n\n从前面的截图中，我们可以看到那里有两点- `S`是起点，`F`是终点。代码必须找到从`S`到达`F`的方法。预期路线如下:\n\n![](img/bb949478-e271-4a23-96c1-fdc57ad1d0cf.png)\n\n前面截图上的白色箭头是我们期望从`S`到达`F`的路径。现在，让我们开发代码来解决这个迷宫问题。我们将创建一个名为`navigate`的函数，通过计算这三种状态来找到可能的路线:\n\n*   如果我们发现`F`在[ *x* ， *y* ]位置，比如`labyrinth[2][4]`，我们已经解决了问题，那么就返回`true`作为返回值。\n*   如果【 *x* 、 *y* 】位置为`#`，则意味着我们面对墙壁，不得不重新访问其他【 *x* 、 *y* 】位置。\n*   否则，我们在那个位置打印`*`来标记我们已经访问过了。\n\n分析完这三种状态后，我们将从递归情况开始，如下所示:\n\n*   路径搜索者如果能导航到`row - 1`，并且大于或等于`0` ( `row - 1 >= 0 && navigate(labyrinth, row - 1, col)`)就会向上走\n*   路径搜索者如果能导航到`row + 1`会向下走，而且比`8` ( `row + 1 < 8 && navigate(labyrinth, row + 1, col)`)小\n*   路径搜索者如果能导航到`col - 1`就向左走，大于等于`0` ( `col - 1 >= 0 && navigate(labyrinth, row, col - 1)`)\n*   路径搜索者如果能导航到`col + 1`就会向右走，而且比`8` ( `col + 1 < 8 && navigate(labyrinth, row, col + 1)`)小\n\n我们将具有如下`navigate()`功能:\n\n```cpp\n    bool navigate(\n      vector<vector<char>> labyrinth,\n      int row,\n      int col)\n    {\n      // Displaying labyrinth\n      displayLabyrinth(labyrinth);\n\n      cout << \"Checking cell (\";\n      cout << row << \",\" << col << \")\" << endl;\n\n      // Pause 1 millisecond\n      // before navigating\n      sleep(1);\n\n      if (labyrinth[row][col] == 'F')\n      {\n        cout << \"Yeayy.. \";\n        cout << \"Found the finish flag \";\n        cout << \"at point (\" << row << \",\";\n        cout << col << \")\" << endl;\n        return (true);\n      }\n      else if (\n        labyrinth[row][col] == '#' ||\n        labyrinth[row][col] == '*')\n      {\n        return (false);\n      }\n      else if (labyrinth[row][col] == ' ')\n      {\n        labyrinth[row][col] = '*';\n      }\n\n      if ((row + 1 < rows) &&\n        navigate(labyrinth, row + 1, col))\n        return (true);\n\n      if ((col + 1 < cols) &&\n        navigate(labyrinth, row, col + 1))\n        return (true);\n\n      if ((row - 1 >= 0) &&\n        navigate(labyrinth, row - 1, col))\n        return (true);\n\n      if ((col - 1 >= 0) &&\n        navigate(labyrinth, row, col - 1))\n        return (true);\n\n        return (false);\n    }\n\n```\n\n我们现在有`navigate()`功能来找出找到`F`的正确路径。但是，在运行`navigate()`功能之前，我们必须确保`S`在那里。然后我们必须开发名为`isLabyrinthSolvable()`的助手函数。它将在迷宫阵列中循环，并通知`S`是否存在。下面的代码片段是`isLabyrinthSolvable()`函数的实现:\n\n```cpp\n    bool isLabyrinthSolvable(\n      vector<vector<char>> labyrinth)\n    {\n      int start_row = -1;\n      int start_col = -1;\n      for (int i = 0; i < rows; i++)\n      {\n        for (int j = 0; j < cols; j++)\n        {\n            if (labyrinth[i][j] == 'S')\n            {\n                start_row = i;\n                start_col = j;\n                break;\n            }\n        }\n      }\n\n      if (start_row == -1 || start_col == -1)\n      {\n        cout << \"No valid starting point found!\" << endl;\n        return (false);\n      }\n\n      cout << \"Starting at point (\" << start_row << \",\";\n      cout << start_col << \")\" << endl;\n\n      return navigate(labyrinth, start_row, start_col);\n    }\n\n```\n\n正如我们在前面的代码片段中看到的，我们提到了`rows`和`cols`变量。我们将把它们初始化为全局变量，如下面的代码片段所示:\n\n```cpp\n    const int rows = 8;\n    const int cols = 8;\n\n```\n\n现在，让我们看看下面的代码，如果我们将`navigate()`和`isLabyrinthSolvable()`功能插入到`labyrinth.cpp`代码中:\n\n```cpp\n    /* labyrinth.cpp */\n    #include <iostream>\n    #include <vector>\n #include <unistd.h>\n\n    using namespace std;\n\n const int rows = 8;\n const int cols = 8;\n\n    vector<vector<char>> createLabyrinth()\n    {\n      // Initializing the multidimensional vector\n      // labyrinth\n      // # is a wall\n      // S is the starting point\n      // E is the finishing point\n      vector<vector<char>> labyrinth =\n      {\n        {'#', '#', '#', '#', '#', '#', '#', '#'},\n        {'#', 'S', ' ', ' ', ' ', ' ', ' ', '#'},\n        {'#', '#', '#', ' ', '#', '#', '#', '#'},\n        {'#', ' ', '#', ' ', '#', '#', '#', '#'},\n        {'#', ' ', ' ', ' ', ' ', ' ', ' ', '#'},\n        {'#', ' ', '#', '#', '#', '#', '#', '#'},\n        {'#', ' ', ' ', ' ', ' ', ' ', 'F', '#'},\n        {'#', '#', '#', '#', '#', '#', '#', '#'}\n       };\n\n     return labyrinth;\n    }\n\n    void displayLabyrinth(\n      vector<vector<char>> labyrinth)\n    {\n      cout << endl;\n      cout << \"====================\" << endl;\n      cout << \"The Labyrinth\" << endl;\n      cout << \"====================\" << endl;\n      // Displaying all characters in labyrinth vector\n      for (int i = 0; i < rows; i++)\n      {\n        for (int j = 0; j < cols; j++)\n        {\n            cout << labyrinth[i][j] << \" \";\n        }\n        cout << endl;\n       }\n      cout << \"====================\" << endl << endl;\n    }\n\n bool navigate(\n vector<vector<char>> labyrinth,\n int row,\n int col)\n {\n // Displaying labyrinth\n displayLabyrinth(labyrinth);\n\n cout << \"Checking cell (\";\n cout << row << \",\" << col << \")\" << endl;\n\n // Pause 1 millisecond\n // before navigating\n sleep(1);\n\n if (labyrinth[row][col] == 'F')\n {\n cout << \"Yeayy.. \";\n cout << \"Found the finish flag \";\n        cout << \"at point (\" << row << \",\";\n cout << col << \")\" << endl;\n return (true);\n }\n else if (\n labyrinth[row][col] == '#' ||\n labyrinth[row][col] == '*')\n {\n return (false);\n }\n else if (labyrinth[row][col] == ' ')\n {\n labyrinth[row][col] = '*';\n }\n\n if ((row + 1 < rows) &&\n navigate(labyrinth, row + 1, col))\n return (true); \n if ((col + 1 < cols) &&\n navigate(labyrinth, row, col + 1))\n return (true); \n if ((row - 1 >= 0) &&\n navigate(labyrinth, row - 1, col))\n return (true); \n if ((col - 1 >= 0) &&\n navigate(labyrinth, row, col - 1))\n return (true); \n return (false);\n } \n bool isLabyrinthSolvable(\n vector<vector<char>> labyrinth)\n {\n int start_row = -1;\n int start_col = -1;\n for (int i = 0; i < rows; i++)\n {\n for (int j = 0; j < cols; j++)\n {\n if (labyrinth[i][j] == 'S')\n {\n start_row = i;\n start_col = j;\n break;\n }\n }\n }\n\n if (start_row == -1 || start_col == -1)\n {\n cerr << \"No valid starting point found!\" << endl;\n return (false);\n }\n\n cout << \"Starting at point (\" << start_row << \",\";\n cout << start_col << \")\" << endl;\n\n return navigate(labyrinth, start_row, start_col);\n }\n\n    auto main() -> int\n    {\n      vector<vector<char>> labyrinth = createLabyrinth();\n      displayLabyrinth(labyrinth);\n\n      string line;\n      cout << endl << \"Press enter to continue...\" << endl;\n      getline(cin, line);\n\n if (isLabyrinthSolvable(labyrinth))\n cout << \"Labyrinth solved!\" << endl;\n else\n cout << \"Labyrinth could not be solved!\" << endl;\n\n     return 0;\n    }\n\n```\n\n正如我们在前面的引用中看到的，在`main()`函数中，我们首先运行`isLabyrinthSolvable()`函数，该函数又调用`navigate()`函数。`navigate()`功能将通过迷宫找到正确的路径。下面是代码的输出:\n\n![](img/529db26b-9492-4c16-bfba-ea14a2a6061d.png)\n\n但是，如果我们跟踪程序如何解决迷宫，当它找到完成标志时，它会面临错误的路线，如下面的截图所示:\n\n![](img/be30241d-2365-4325-907c-8761c1460abd.png)\n\n我们可以看到，在前面的截图中有一个白色的方块。当它在寻找正确的道路时，这是错误的选择。一旦遇到障碍，它就会返回并找到其他方法。这也将撤销它所做的选择。让我们看看下面的截图，它向我们展示了当递归找到另一条路径并撤销之前的选择时的情形:\n\n![](img/b6bd29c3-e866-4819-8eaf-eb23403b3dc4.png)\n\n在前面的截图中，我们可以看到递归尝试了另一条路由，并且之前失败的路由已经消失，因为回溯递归撤销了该路由。递归现在有了正确的路径，它可以继续，直到找到完成标志。因此，我们现在已经成功地开发了回溯递归。\n\n# 摘要\n\n本章向我们介绍了使用迭代和递归重复函数调用的技术。然而，由于递归比迭代更具功能性，我们着重讨论递归而不是迭代。我们从迭代和递归的区别开始。然后，我们继续讨论将不可变函数重构为递归不可变函数。\n\n在我们了解了递归之后，我们发现了其他更好的递归技术。我们还讨论了尾部递归来获得这种改进的技术。最后，我们列举了三种递归——函数递归、过程递归和回溯递归。当我们期望递归的返回值时，我们通常使用函数递归。否则，我们使用过程递归。而且，如果我们需要分解问题，并在递归不起作用时撤销递归性能，我们可以使用回溯递归来解决问题。\n\n在下一章中，我们将讨论延迟求值，以使代码运行得更快。这将使代码变得高效，因为它将确保不必要的代码不会被执行。"
  },
  {
    "path": "docs/learn-cpp-func-prog/5.md",
    "content": "# 五、使用延迟求值拖延执行过程\n\n在前一章中，我们讨论了在函数方法中重复函数调用的递归。现在，我们将讨论可以使我们的代码变得更高效的延迟求值，因为它只会在我们需要的时候运行。我们还将应用递归(我们在上一章中讨论过的主题)来生成惰性代码。\n\n在本章中，我们讨论**延迟求值**以使代码运行得更快。这将使代码变得高效，因为它将确保不必要的代码不会被执行。以下是我们将讨论的深入到延迟求值的主题:\n\n*   区分渴望和懒惰评价的区别\n*   使用缓存技术优化代码\n*   将急切求值重构为延迟求值\n*   设计可以在其他函数代码中重用的有用类\n\n# 求值表达式\n\n每种编程语言都有自己的策略来确定何时求值函数调用的参数，以及必须传递给参数的值的类型。一种编程语言中使用最多的策略求值有两种- **严格**(急切)求值和**非严格**(懒散)求值。\n\n# 严格求值后立即运行表达式\n\n严格求值用于最命令式的编程语言。它会立即执行我们的代码。假设我们有以下等式:\n\n```cpp\n    int i = (x + (y * z));\n\n```\n\n在严格的求值中，将首先计算最里面的括号，然后向外计算前面的等式。这意味着我们将计算`y * z`，然后将结果添加到`x`。为了更清楚，我们来看看下面的`strict.cpp`代码:\n\n```cpp\n    /* strict.cpp */\n    #include <iostream>\n\n    using namespace std;\n\n    int OuterFormula(int x, int yz)\n    {\n      // For logging purpose only\n      cout << \"Calculate \" << x << \" + \";\n      cout << \"InnerFormula(\" << yz << \")\";\n      cout << endl;\n\n      // Returning the calculation result\n      return x * yz;\n    }\n\n    int InnerFormula(int y, int z)\n    {\n      // For logging purpose only\n      cout << \"Calculate \" << y << \" * \";\n      cout << z << endl;\n\n      // Returning the calculation result\n      return y * z;\n    }\n\n    auto main() -> int\n    {\n      cout << \"[strict.cpp]\" << endl;\n\n      // Initializing three int variables\n      // for the calculation\n      int x = 4;\n      int y = 3;\n      int z = 2;\n\n      // Calculating the expression\n      cout << \"Calculate \" << x <<\" + \";\n      cout << \"(\" << y << \" * \" << z << \")\";\n      cout << endl;\n      int result = OuterFormula(x, InnerFormula(y, z));\n\n      // For logging purpose only\n      cout << x << \" + \";\n      cout << \"(\" << y << \" * \" << z << \")\";\n      cout << \" = \" << result << endl;\n\n      return 0;\n    }\n\n```\n\n正如我们之前讨论的，前面代码的执行将首先是`y * z`，然后我们将结果添加到`x`，正如我们在下面的输出中看到的:\n\n![](img/5df7ea63-3801-47d1-9fbd-e50d30c1f8ec.png)\n\n前面的执行顺序是我们通常所期待的。但是，在非严格求值中，我们将对这个执行过程重新排序。\n\n# 用非严格求值延迟表达式\n\n在一个非严格的评价中，首先对`+`算子进行约简，然后我们对内部公式进行约简，就是`(y * z)`。我们会看到求值会从外到内开始。我们将重构我们以前的`strict.cpp`代码，使其成为一个非严格的求值。代码应该如下`non_strict.cpp`代码:\n\n```cpp\n    /* non_strict.cpp */\n    #include <functional>\n    #include <iostream>\n\n    using namespace std;\n\n int OuterFormulaNonStrict(\n int x,\n int y,\n int z,\n function<int(int, int)> yzFunc)\n {\n // For logging purpose only\n cout << \"Calculate \" << x << \" + \";\n cout << \"InnerFormula(\" << y << \", \";\n cout << z << \")\" << endl;\n\n // Returning the calculation result\n return x * yzFunc(y, z);\n }\n\n     int InnerFormula(int y, int z)\n     {\n       // For logging purpose only\n       cout << \"Calculate \" << y << \" * \";\n       cout << z << endl;\n\n       // Returning the calculation result\n       return y * z;\n     }\n\n     auto main() -> int\n     {\n       cout << \"[non_strict.cpp]\" << endl;\n\n       // Initializing three int variables\n       // for the calculation\n       int x = 4;\n       int y = 3;\n       int z = 2;\n\n       // Calculating the expression\n       cout << \"Calculate \" << x <<\" + \";\n       cout << \"(\" << y << \" * \" << z << \")\";\n       cout << endl;\n       int result = OuterFormulaNonStrict(x, y, z, InnerFormula);\n\n       // For logging purpose only\n       cout << x << \" + \";\n       cout << \"(\" << y << \" * \" << z << \")\";\n       cout << \" = \" << result << endl;\n\n       return 0;\n    }\n\n```\n\n如我们所见，我们将`strict.cpp`代码中的`OuterFormula()`函数修改为`non_strict.cpp`代码中的`OuterFormulaNonStrict()`函数。在`OuterFormulaNonStrict()`函数中，除了三个变量- `x`、`y`和`z`之外，我们还传递一个函数作为参数。结果，前面表达式的执行顺序被改变。以下是我们运行`non_strict.cpp`代码时应该在控制台屏幕上看到的内容:\n\n![](img/ce639e69-1325-47bc-b524-6894d77af3b5.png)\n\n从前面的输出中，我们已经证明我们的代码正在执行非严格求值，因为它现在首先计算加法运算符(`+`)而不是乘法(`*`)。然而，结果仍然是正确的，尽管顺序已经改变。\n\n# 懒惰评价的基本概念\n\n在我们创建懒惰代码之前，让我们讨论一下延迟求值的基本概念。我们将使用延迟过程来使我们的代码变得懒惰，使用缓存技术来通过避免不必要的计算来提高代码的性能，使用优化技术来通过存储昂贵的函数调用的结果并在相同的输入再次出现时返回缓存的结果来加速代码。在我们研究了这些技术之后，我们将尝试开发真正的惰性代码。\n\n# 推迟进程\n\n懒惰的基本概念是拖延一个过程。在本节中，我们将讨论如何延迟特定流程的执行。我们将创建一个名为`Delay`的新类。我们将在构造类时向其中传递一个函数。除非我们调用`Fetch()`方法，否则该函数不会运行。该功能的实现如下:\n\n```cpp\n    template<class T> class Delay\n    {\n      private:\n        function<T()> m_func;\n\n      public:\n        Delay(\n          function<T()> func)\n          : m_func(func)\n          {\n          }\n\n        T Fetch()\n        {\n          return m_func();\n        }\n    };\n\n```\n\n现在，让我们消耗`Delay`类来延迟执行。我们将创建一个名为`delaying.cpp`的文件，它将运行两个功能- `multiply`和`division`。但是，这两个函数只有在我们调用`Fetch()`方法后才会运行。文件的内容如下:\n\n```cpp\n    /* delaying.cpp */\n    #include <iostream>\n    #include <functional>\n\n    using namespace std;\n\n    template<class T> class Delay\n    {\n      private:\n        function<T()> m_func;\n\n      public:\n        Delay(function<T()> func) : m_func(func)\n        {\n        }\n\n        T Fetch()\n        {\n          return m_func();\n        }\n    };\n\n    auto main() -> int\n    {\n      cout << \"[delaying.cpp]\" << endl;\n\n      // Initializing several int variables\n      int a = 10;\n      int b = 5;\n\n      cout << \"Constructing Delay<> named multiply\";\n      cout << endl;\n      Delay<int> multiply([a, b]()\n      {\n        cout << \"Delay<> named multiply\";\n        cout << \" is constructed.\" << endl;\n        return a * b;\n      });\n\n     cout << \"Constructing Delay<> named division\";\n     cout << endl;\n     Delay<int> division([a, b]()\n     {\n       cout << \"Delay<> named division \";\n       cout << \"is constructed.\" << endl;\n       return a / b; \n     });\n\n     cout << \"Invoking Fetch() method in \";\n     cout << \"multiply instance.\" << endl;\n     int c = multiply.Fetch();\n\n     cout << \"Invoking Fetch() method in \";\n     cout << \"division instance.\" << endl;\n     int d = division.Fetch();\n\n     // Displaying the result\n     cout << \"The result of a * b = \" << c << endl;\n     cout << \"The result of a / b = \" << d << endl;\n\n     return 0;\n    }\n\n```\n\n正如我们在[第 1 章](1.html)、*深入到现代 C++* 中所讨论的，我们可以使用一个 Lambda 表达式来构建`multiply`和`division`函数。然后我们把它们传递给每个`Delay`建造者。在这个阶段，函数还没有运行。将在调用`Fetch()`方法- `multiply.Fetch()`和`division.Fetch()`后运行。我们将在屏幕上看到的输出应该如下图所示:\n\n![](img/5ade918f-d163-4d2b-9d84-d41ddc962d86.png)\n\n正如我们在前面的输出截图中看到的那样，`multiply`和`division`实例是在调用`Fetch()`方法时构造的(见两个白色箭头)，而不是在调用`Delay`类的构造函数时。现在，我们已经成功延迟了执行，可以说流程只有在需要的时候才会执行。\n\n# 使用记忆技术缓存值\n\n我们现在已经通过使用`Delay`类成功地延迟了函数的执行。但是，由于每次调用`Fetch()`方法时都会运行`Delay`类实例的函数，如果函数不纯或有副作用，可能会出现意外结果。让我们通过修改`multiply`函数来重构之前的`delaying.cpp`代码。这个函数现在变成了一个非纯函数，因为它依赖于一个外部变量。代码应该如下所示:\n\n```cpp\n    /* delaying_non_pure.cpp */\n    #include <iostream>\n    #include <functional>\n\n    using namespace std;\n\n    template<class T> class Delay\n    {\n      private:\n        function<T()> m_func;\n\n      public:\n        Delay(function<T()> func) : m_func(func)\n        {\n        }\n\n        T Fetch()\n        {\n          return m_func();\n        }\n    };\n\n    auto main() -> int\n    {\n      cout << \"[delaying_non_pure.cpp]\" << endl;\n\n      // Initializing several int variables\n      int a = 10;\n      int b = 5;\n      int multiplexer = 0;\n\n      // Constructing Delay<> named multiply_impure\n      Delay<int> multiply_impure([&]()\n      {\n        return multiplexer * a * b;\n      });\n\n      // Invoking Fetch() method in multiply_impure instance\n      // multiple times\n      for (int i = 0; i < 5; ++ i)\n      {\n        ++ multiplexer;\n        cout << \"Multiplexer = \" << multiplexer << endl;\n        cout << \"a * b = \" << multiply_impure.Fetch();\n        cout << endl;\n      }\n\n      return 0;\n    }\n\n```\n\n正如我们在前面的代码中看到的，我们现在有了一个名为`multiply_impure`的新 Lambda 表达式，它是我们在`delaying.cpp`代码中创建的`multiply`函数的重构版本。`multiply_impure`函数依赖于`multiplexer`变量，每次调用`Fetch()`方法之前，该变量的值都会增加。以下是我们应该在屏幕上看到的屏幕截图输出:\n\n![](img/f1e15207-1b93-4d22-b534-54ce6b0bba97.png)\n\n如我们所见，`Fetch()`方法每次被调用时都会给出不同的结果。我们现在必须重构`Delay`类，以确保每次`Fetch()`方法使用相同的传递参数运行函数时，它都会返回完全相同的结果。为了实现这一点，我们将使用记忆技术来存储函数调用的结果，并在相同的输入再次出现时返回缓存的结果。\n\n我们将`Delay`类重命名为`Memoization`类。这不仅会延迟函数调用，还会记录带有特定传递参数的函数。因此，下一次带有这些参数的函数出现时，函数本身将不会运行，而是返回缓存的结果。为了便于讨论，我们来看看下面的`Memoization`类实现:\n\n```cpp\n    template<class T> class Memoization\n    {\n      private:\n        T const & (*m_subRoutine)(Memoization *);\n        mutable T m_recordedFunc;\n        function<T()> m_func;\n\n        static T const & ForceSubroutine(Memoization * d)\n        {\n          return d->DoRecording();\n        }\n\n        static T const & FetchSubroutine(Memoization * d)\n        {\n          return d->FetchRecording();\n        }\n\n        T const & FetchRecording()\n        {\n          return m_recordedFunc;\n        }\n\n        T const & DoRecording()\n        {\n          m_recordedFunc = m_func();\n          m_subRoutine = &FetchSubroutine;\n          return FetchRecording();\n        }\n\n     public:\n        Memoization(function<T()> func) : m_func(func),\n         m_subRoutine(&ForceSubroutine),\n         m_recordedFunc(T())\n        {\n        }\n\n       T Fetch()\n       {\n         return m_subRoutine(this);\n       }\n    };\n\n```\n\n正如我们在前面的代码片段中看到的，我们现在有`FetchRecording()`和`DoRecording()`来获取和设置我们已经存储的函数。而且，当构造类时，它会记录传递的函数并保存到`m_subRoutine`。当调用`Fetch()`方法时，该类将检查`m_subRoutine`，并使用当前传递的参数查找它是否具有来自函数的值。如果是，它只是从`m_subRoutine`返回值，而不是运行函数。现在，让我们看看下面消耗`Memoization`类的`delaying_non_pure_memoization.cpp`代码:\n\n```cpp\n    /* delaying_non_pure_memoization.cpp */\n    #include <iostream>\n    #include <functional>\n\n    using namespace std;\n\n    template<class T> class Memoization\n    {\n      private:\n        T const & (*m_subRoutine)(Memoization *);\n        mutable T m_recordedFunc;\n        function<T()> m_func;\n\n        static T const & ForceSubroutine(Memoization * d)\n        {\n          return d->DoRecording();\n        }\n\n       static T const & FetchSubroutine(Memoization * d)\n       {\n          return d->FetchRecording();\n       }\n\n       T const & FetchRecording()\n       {\n          return m_recordedFunc;\n       }\n\n       T const & DoRecording()\n       {\n          m_recordedFunc = m_func();\n          m_subRoutine = &FetchSubroutine;\n          return FetchRecording();\n       }\n\n     public:\n       Memoization(function<T()> func) : m_func(func),\n        m_subRoutine(&ForceSubroutine),\n        m_recordedFunc(T())\n       {\n       }\n\n      T Fetch()\n      {\n        return m_subRoutine(this);\n      }\n    };\n\n    auto main() -> int\n    {\n      cout << \"[delaying_non_pure_memoization.cpp]\" << endl;\n\n      // Initializing several int variables\n      int a = 10;\n      int b = 5;\n      int multiplexer = 0;\n\n // Constructing Memoization<> named multiply_impure\n Memoization<int> multiply_impure([&]()\n {\n return multiplexer * a * b;\n });\n\n      // Invoking Fetch() method in multiply_impure instance\n      // multiple times\n      for (int i = 0; i < 5; ++ i)\n      {\n        ++ multiplexer;\n        cout << \"Multiplexer = \" << multiplexer << endl;\n        cout << \"a * b = \" << multiply_impure.Fetch();\n        cout << endl;\n      }\n\n      return 0;\n    }\n\n```\n\n从前面的代码片段中，我们看到我们在`main()`函数中没有太多修改。我们修改的只是用于`multiply_impure`变量的类类型，从`Delay`到`Memoization`。然而，结果现在已经改变了，因为我们将从第五次调用`multiply_impure()`函数中获得完全相同的返回值。我们来看看下面的截图来证明一下:\n\n![](img/eb597eec-cfe2-4c06-b0eb-a546e3a26fb5.png)\n\n从前面的截图中，我们可以看到，即使`Multiplexer`的值增加了，计算的回报也总是一样的。这是因为记录了第一次函数调用的返回值，所以不需要为剩下的调用再次运行函数。\n\nAs we discussed in [Chapter 2](2.html), *Manipulating Functions in Functional Programming*, having an impure function seems wrong in functional programming. Hiding an impure function behind memoization might also cause a bug if the code really needs a different result (non-cached result). Use the preceding technique for caching the impure function wisely.\n\n# 使用记忆技术优化代码\n\n记忆对于应用于非纯函数或有副作用的函数非常有用。但是，它也可以用来优化代码。通过使用内存化，我们开发的代码将运行得更快。假设我们需要多次运行具有完全相同的传递参数的完全相同的函数。如果代码从我们记录值的地方获取值，而不是运行函数，速度会更快。对于昂贵的函数调用也更好，因为通过使用 memoization，我们不需要一遍又一遍地执行不必要的昂贵的函数调用。\n让我们创建一个代码来讨论进一步的优化。我们将使用`Delay`类来证明与`Memoization`类相比，它不是一个优化的代码。我们将拥有消耗`Delay`类的`not_optimize_code.cpp`代码。在这段未优化的代码中，我们将调用我们在[第 4 章](4.html)、*中创建的`fibonacci()`函数，使用递归算法*重复方法调用。我们将把`40`作为参数传递给`fibonacci()`函数，并从`fib40`类实例中调用`Fetch()`方法五次。我们还将使用位于`chrono`头中的`high_resolution_clock`类计算方法每次调用的经过时间，以记录**开始**和**结束**时间，通过用开始值减去结束值来检索经过时间。除了每个`Fetch()`方法调用的经过时间，我们还计算整个代码的经过时间。`not_optimize_code.cpp`代码的实现如下:\n\n```cpp\n    /* not_optimize_code.cpp */\n    #include <iostream>\n    #include <functional>\n    #include <chrono>\n\n    using namespace std;\n\n    template<class T> class Delay\n    {\n      private:\n        function<T()> m_func;\n\n      public:\n        Delay(function<T()> func): m_func(func)\n        {\n        }\n\n        T Fetch()\n        {\n          return m_func();\n        }\n    };\n\n    // Function for calculating Fibonacci sequence\n    int fibonacci(int n)\n    {\n      if(n <= 1)\n         return n;\n      return fibonacci(n-1) + fibonacci(n-2);\n    }\n\n    auto main() -> int\n    {\n      cout << \"[not_optimize_code.cpp]\" << endl;\n\n      // Recording start time for the program\n      auto start = chrono::high_resolution_clock::now();\n\n      // Initializing int variable to store the result\n      // from Fibonacci calculation\n      int fib40Result = 0;\n\n      // Constructing Delay<> named fib40\n      Delay<int> fib40([]()\n      {\n        return fibonacci(40);\n      });\n\n      for (int i = 1; i <= 5; ++ i)\n      {\n        cout << \"Invocation \" << i << \". \";\n\n        // Recording start time\n        auto start = chrono::high_resolution_clock::now();\n\n        // Invoking the Fetch() method\n        // in fib40 instance\n        fib40Result = fib40.Fetch();\n\n        // Recording end time\n        auto finish = chrono::high_resolution_clock::now();\n\n        // Calculating the elapsed time\n        chrono::duration<double, milli> elapsed = finish - start;\n\n        // Displaying the result\n        cout << \"Result = \" << fib40Result << \". \";\n\n        // Displaying elapsed time\n        // for each fib40.Fetch() invocation\n        cout << \"Consuming time = \" << elapsed.count();\n        cout << \" milliseconds\" << endl;\n      }\n\n       // Recording end time for the program\n       auto finish = chrono::high_resolution_clock::now();\n\n       // Calculating the elapsed time for the program\n       chrono::duration<double, milli> elapsed = finish - start;\n\n       // Displaying elapsed time for the program\n       cout << \"Total consuming time = \";\n       cout << elapsed.count() << \" milliseconds\" << endl;\n\n       return 0;\n    }\n\n```\n\n现在，让我们运行代码来获得前面代码过程的运行时间。以下截图是我们将在屏幕上看到的内容:\n\n![](img/d2ef7cc8-601b-47af-ae9f-19797be02341.png)\n\n从前面的截图中，我们可以看到我们需要大约`2357.79`毫秒来处理代码。每次调用`fib40.Fetch()`方法，平均需要大约`470`毫秒，尽管我们将完全相同的参数传递给`fibonacci()`函数，也就是`40`。现在，让我们看看如果我们在前面的代码中使用记忆化技术会发生什么。我们不会修改太多代码，只是重构`fib40`的实例化。它不是从`Delay`类实例化，而是从`Memoization`类实例化。代码应如下所示:\n\n```cpp\n    /* optimizing_memoization.cpp */\n    #include <iostream>\n    #include <functional>\n    #include <chrono>\n\n    using namespace std;\n\n    template<class T> class Memoization\n    {\n      private:\n        T const & (*m_subRoutine)(Memoization *);\n        mutable T m_recordedFunc;\n        function<T()> m_func;\n\n        static T const & ForceSubroutine(Memoization * d)\n        {\n          return d->DoRecording();\n        }\n\n        static T const & FetchSubroutine(Memoization * d)\n        {\n          return d->FetchRecording();\n        }\n\n        T const & FetchRecording()\n        {\n          return m_recordedFunc;\n        }\n\n        T const & DoRecording()\n        {\n          m_recordedFunc = m_func();\n          m_subRoutine = &FetchSubroutine;\n          return FetchRecording();\n        }\n\n      public:\n        Memoization(function<T()> func): m_func(func),\n          m_subRoutine(&ForceSubroutine),\n          m_recordedFunc(T())\n          {\n          }\n\n        T Fetch()\n        {\n          return m_subRoutine(this);\n        }\n     };\n\n       // Function for calculating Fibonacci sequence\n       int fibonacci(int n)\n       {\n         if(n <= 1)\n           return n;\n           return fibonacci(n-1) + fibonacci(n-2);\n       }\n\n       auto main() -> int\n       {\n         cout << \"[optimizing_memoization.cpp]\" << endl;\n\n         // Recording start time for the program\n         auto start = chrono::high_resolution_clock::now();\n\n         // Initializing int variable to store the result\n         // from Fibonacci calculation\n         int fib40Result = 0;\n\n         // Constructing Memoization<> named fib40\n Memoization<int> fib40([]()\n {\n return fibonacci(40);\n });\n\n         for (int i = 1; i <= 5; ++ i)\n         {\n           cout << \"Invocation \" << i << \". \";\n\n           // Recording start time\n           auto start = chrono::high_resolution_clock::now();\n\n           // Invoking the Fetch() method\n           // in fib40 instance\n           fib40Result = fib40.Fetch();\n\n           // Recording end time\n           auto finish = chrono::high_resolution_clock::now();\n\n           // Calculating the elapsed time\n           chrono::duration<double, milli> elapsed = finish - start;\n\n           // Displaying the result\n           cout << \"Result = \" << fib40Result << \". \";\n\n           // Displaying elapsed time\n           // for each fib40.Fetch() invocation\n           cout << \"Consuming time = \" << elapsed.count();\n           cout << \" milliseconds\" << endl;\n       }\n\n          // Recording end time for the program\n          auto finish = chrono::high_resolution_clock::now();\n\n          // Calculating the elapsed time for the program\n          chrono::duration<double, milli> elapsed = finish - start;\n\n          // Displaying elapsed time for the program\n          cout << \"Total consuming time = \";\n          cout << elapsed.count() << \" milliseconds\" << endl;\n\n          return 0;\n     }\n\n```\n\n以下是我们运行`optimizing_memoization.cpp`代码时在控制台屏幕上看到的内容:\n\n![](img/0c030b41-e024-44bc-b1fc-986e0504eae2.png)\n\n令人惊讶的是，我们只需要`494.681`毫秒来执行`optimizing_memoization.cpp`代码。与`not_optimize_code.cpp`码相比，该码的速度大约快`4.7`倍。发生这种情况是因为代码在将`40`传递给其参数时成功地缓存了`fibonacci()`函数的结果。每次我们再次调用`fib40.Fetch()`方法，它都会再次调用`fibonacci()`函数，输入完全相同。代码将只返回缓存的结果，这样就可以避免运行不必要的昂贵的函数调用。\n\n# 行动中的懒惰评价\n\n讨论了延迟求值的基本概念后，让我们通过用惰性方法设计代码来深入研究延迟求值。在本节中，我们将首先开发一个急切的求值代码，然后将该代码重构为懒惰的求值代码。我们开发的代码将生成一系列素数。首先，我们将使用`for`循环迭代整数，以获得热切求值中的素数。以下`prime.cpp`代码就是我们正在谈论的:\n\n```cpp\n    /* prime.cpp */\n    #include <iostream>\n    #include <cmath>\n\n    using namespace std;\n\n    bool PrimeCheck(int i)\n    {\n      // All even numbers are not prime number\n      // except 2\n      if ((i % 2) == 0)\n      {\n        return i == 2;\n      }\n\n      // Calculating the square root of i\n      // and store in int data type variable\n      // if the argument i is not even number,\n      int sqr = sqrt(i);\n\n      // For numbers 9 and below,\n      // the prime numbers is simply the odd numbers\n      // For number above 9\n      // the prime numbers is all of odd numbers\n      // except the square number\n      for (int t = 3; t <= sqr; t += 2)\n      {\n        if (i % t == 0)\n        {\n            return false;\n        }\n      }\n\n       // The number 1 is not prime number\n       // but still passing the preceding test\n       return i != 1;\n    }\n\n    auto main() -> int\n    {\n      cout << \"[delaying.cpp]\" << endl;\n\n      // Initializing a counting variable\n      int n = 0;\n\n      // Displaying the first 100 prime numbers\n      cout << \"List of the first 100 prime numbers:\" << endl;\n      for (int i = 0; ; ++ i)\n      {\n        if (PrimeCheck(i))\n        {\n            cout << i << \"\\t\";\n\n            if (++ n == 100)\n                return 0;\n        }\n      }\n\n      return 0;\n    }\n\n```\n\n正如我们在前面的代码中看到的，我们有一个简单的`PrimeCheck()`函数来分析整数是否是素数。之后，代码使用`for`循环迭代无穷整数，然后检查它是否是质数。如果我们有一百个质数，循环就结束了。下面的截图是我们应该看到的控制台上的输出:\n\n![](img/3263a17d-40a8-4065-82ad-48cfa4601359.png)\n\n我们现在有了一个使用热切求值生成素数的代码。正如我们在前面的截图中看到的，我们使用`for`循环生成了一百个质数。接下来，我们将把它重构为惰性代码。\n\n# 设计块和行类\n\n在`prime.cpp`代码中，我们使用`for`循环生成一行整数。在这一行中，有几个数字被称为**组块**。现在，在我们重构代码之前，我们将准备一个名为`Row`和`Chunk`的类供我们进一步讨论。根据我们前面的类比，`Row`类将保存整数序列，`Chunk`类将保存单个数字。我们将从数据中最小的部分开始，也就是块。这里是`Chunk`类的实现:\n\n```cpp\n    template<class T> class Chunk\n    {\n      private:\n        T m_value;\n        Row<T> m_lastRow;\n\n      public:\n        Chunk()\n         {\n         }\n\n        Chunk(T value, Row<T> lastRow): m_value(value),\n         m_lastRow(std::move(lastRow))\n        {\n        }\n\n        explicit Chunk(T value) : m_value(value)\n        {\n        }\n\n        T Value() const\n        {\n          return m_value;\n        }\n\n        Row<T> ShiftLastToFirst() const\n        {\n          return m_lastRow;\n        }\n    };\n\n```\n\n由于`Row`类是由几个`Chunk`类构成的，除了`Chunk`本身的值之外，`Chunk`类还有当前`Row`中的下一个值`Chunk`，由`m_lastRow`成员变量标注。我们也可以通过调用`ShiftLastToFirst()`方法得到`m_lastRow`值。现在，让我们转到`Row`课。该类的实现如下:\n\n```cpp\n    template<class T> class Row\n    {\n      private:\n        std::shared_ptr <Memoization<Chunk<T>>>\n        m_lazyChunk;\n\n      public:\n         Row()\n         {\n         }\n\n         explicit Row(T value)\n         {\n           auto chunk = ChunkPreparation<T>(value);\n           m_lazyChunk = std::make_shared<Memoization<Chunk<T>>> \n           (chunk);\n         }\n\n         Row(T value, Row row)\n         {\n           auto chunk = ChunkPreparation<T>( value, std::move(row));\n\n           m_lazyChunk = std::make_shared<Memoization<Chunk<T>>>(\n           chunk);\n         }\n\n         Row(std::function<Chunk<T>()> func): m_lazyChunk(\n         std::make_shared<Memoization<Chunk<T>>>(func))\n         {\n         }\n\n         bool IsEmpty() const\n         {\n           return !m_lazyChunk;\n         }\n\n         T Fetch() const\n         {\n           return m_lazyChunk->Fetch().Value();\n         }\n\n         Row<T> ShiftLastToFirst() const\n         {\n          return m_lazyChunk->Fetch().ShiftLastToFirst();\n         }\n\n         Row Pick(int n) const\n         {\n           if (n == 0 || IsEmpty())\n            return Row();\n\n          auto chunk = m_lazyChunk;\n          return Row([chunk, n]()\n          {\n            auto val = chunk->Fetch().Value();\n            auto row = chunk->Fetch().ShiftLastToFirst();\n            return Chunk<T>(val, row.Pick(n - 1));\n          });\n         }\n    };\n\n```\n\n正如我们在前面的代码片段中看到的那样，`Row`类只有一个私有成员来存储`Chunk`数据的记忆。`Row`类有四个构造函数，我们将在下一个代码中使用它们。它还有`Fetch()`方法，我们在前一节设计`Memoization`类时得到的，用来得到`m_lazyChunk`值。其他方法对我们接下来的惰性代码也很有用。`IsEmpty()`方法会检查`m_lazyChunk`值是否为空，`ShiftLastToFirst()`方法会取`m_lazyChunk`的最后一行，`Pick(int n)`方法会取出第一个`n`行的元素，如果以后需要取出一百个整数质数，我们会用到。\n\n我们还可以看到其中一个`Row`构造函数正在调用`ChunkPreparation`类构造函数。`ChunkPreparation`类将使用给定值和最后一行值初始化一个新的`Chunk`类构造函数。该类的实现如下:\n\n```cpp\n    template<class T> class ChunkPreparation\n    {\n      public:\n        T m_value;\n        Row<T> m_row;\n\n        ChunkPreparation(T value, Row<T> row) :\n          m_value(value),\n          m_row(std::move(row))\n          {\n          }\n\n        explicit ChunkPreparation(T value) :\n          m_value(value)\n          {\n          }\n\n        Chunk<T> operator()()\n        {\n          return Chunk<T>(\n            m_value,\n            m_row);\n        }\n    };\n\n```\n\n我们可以看到，通过调用`operator ()`，新的`Chunk`将以给定的`m_value`和`m_row`值生成。\n\n# 连接几行\n\n当我们计划生成一行素数时，我们必须能够将当前行与代码生成的新行连接起来。为了满足这一需求，下面是连接两行的`ConcatenateRows()`函数的实现:\n\n```cpp\n    template<class T> Row<T> ConcatenateRows(\n      Row<T> leftRow,\n      Row<T> rightRow)\n      {\n        if (leftRow.IsEmpty())\n          return rightRow;\n\n        return Row<T>([=]()\n        {\n          return Chunk<T>(\n            leftRow.Fetch(),\n            ConcatenateRows<T>(\n             leftRow.ShiftLastToFirst(),\n             rightRow));\n         });\n       }\n\n```\n\n当我们查看前面的代码片段时，很清楚`ConcatenateRows()`函数是做什么的。如果`leftRow`还是空的，就退回第二排，也就是`rightRow`。如果`leftRow`和`rightRow`可用，我们可以返回已经形成一行的给定行的块。\n\n# 迭代每个行类的元素\n\n构造素数行后，我们需要迭代每行的元素来操作它，例如，将值打印到控制台。为此，我们必须开发以下`ForEach()`方法:\n\n```cpp\n    template<class T, class U> void ForEach( Row<T> row, U func)\n     {\n        while (!row.IsEmpty())\n        {\n          func(row.Fetch());\n          row = row.ShiftLastToFirst();\n         }\n     }\n\n```\n\n我们将把行本身和一个函数传递给`ForEach()`方法。我们传递给它的函数将对该行的每个元素运行。\n\nFor our convenience in developing the lazy code in this chapter, I will bundle our previous discussion `template` class into a single header file named `lazyevaluation.h`; we can also reuse it for other projects. The header will contain the `Memoization`, `Row`, `Chunk`, `ChunkPreparation`, `ConcatenateRows`, and `ForEach` template class. You can create the header file yourself or download it from the code repository on the Packt website ([https://github.com/PacktPublishing/LearningCPPFunctionalProgramming](https://github.com/PacktPublishing/LearningCPPFunctionalProgramming)).\n\n# 生成无限整数行\n\n现在是时候生成无限整数行了，就像我们在前面的`prime.cpp`代码中使用`for`循环一样。然而，我们现在将创建一个名为`GenerateInfiniteIntRow()`的新函数，从几个整数块中生成一个整数行。下面的代码片段是函数的实现:\n\n```cpp\n    Row<int> GenerateInfiniteIntRow( int initialNumber)\n    {\n      return Row<int>([initialNumber]()\n      {\n        return Chunk<int>(\n            initialNumber,\n            GenerateInfinityIntRow(\n             initialNumber + 1));\n      });\n    }\n\n```\n\n正如我们所看到的，首先，我们从`initialNumber`到无限创建`Chunk`。这些块将在最后转换成`Row`数据类型。为了停止这个递归函数，我们可以在`Row`类中调用`Pick()`方法。\n\n# 生成无限素数行\n\n在成功生成无限个数字之后，我们现在必须将行限制为只生成质数。我们将从`prime.cpp`代码修改`CheckPrime()`功能。我们将改变函数的返回值，如果不是质数`Row<void*>(nullptr)`或者相反的话`Row<void*>()`。该功能的实现应如下所示:\n\n```cpp\n    Row<void*> PrimeCheck(int i)\n    {\n      if ((i % 2) == 0)\n      {\n        if (i == 2)\n            return Row<void*>(nullptr);\n        else\n            return Row<void*>();\n      }\n\n      int sqr = sqrt(i);\n\n      for (int t = 3; t <= sqr; t = t + 2)\n      {\n        if (i % t == 0)\n        {\n            return Row<void*>();\n        }\n      }\n\n      if (i == 1)\n        return Row<void*>();\n      else\n        return Row<void*>(nullptr);\n    }\n\n```\n\n为什么我们需要改变函数的返回值？因为我们想将返回值传递给`JoiningPrimeNumber()`函数，该函数将把生成的 Chunk 与以下实现连接起来:\n\n```cpp\n    template<class T, class U> \n    auto JoiningPrimeNumber(\n      Row<T> row, U func) -> decltype(func())\n      {\n         return JoiningAllRows(\n           MappingRowByValue(row, func));\n      }\n\n```\n\n此外，`MappingRowByValue()`函数会将给定的行映射到给定的函数。该功能的实现如下:\n\n```cpp\n    template<class T, class U> \n    auto MappingRowByValue(\n      Row<T> row, U func) -> Row<decltype(func())>\n    {\n      using V = decltype(func());\n\n      if (row.IsEmpty())\n        return Row<V>();\n\n      return Row<V>([row, func]()\n      {\n        return Chunk<V>(\n          func(),\n          MappingRowByValue(\n            row.ShiftLastToFirst(),\n            func));\n      });\n    }\n\n```\n\n使用`JoiningPrimeNumber()`函数成功连接所有素数后，我们必须使用`Binding()`函数将其绑定到现有行，实现如下:\n\n```cpp\n    template<class T, class U> Row<T> \n    Binding( Row<T> row, U func)\n    {\n       return JoiningAllRows( MappingRow( row, func));\n    }\n\n```\n\n从前面的代码片段中，`MappingRow()`函数将给定的行映射到给定的函数，然后`JoiningAllRows()`将连接来自`MappingRow()`返回值的所有行。`MappingRow()`和`JoiningAllRows()`功能的实现如下:\n\n```cpp\n    template<class T, class U>\n    auto MappingRow(\n      Row<T> row, U func) -> Row<decltype(\n        func(row.Fetch()))>\n      {\n        using V = decltype(func(row.Fetch()));\n\n        if (row.IsEmpty())\n          return Row<V>();\n\n        return Row<V>([row, func]()\n        {\n          return Chunk<V>(func(\n            row.Fetch()),\n            MappingRow(\n              row.ShiftLastToFirst(),\n              func));\n       });\n    }\n\n    template<class T> Row<T> \n    JoiningAllRows(\n      Row<Row<T>> rowOfRows)\n    {\n      while (!rowOfRows.IsEmpty() && \n        rowOfRows.Fetch().IsEmpty())\n      {\n        rowOfRows = rowOfRows.ShiftLastToFirst();\n      }\n\n     if (rowOfRows.IsEmpty()) \n        return Row<T>();\n\n     return Row<T>([rowOfRows]()\n     {\n        Row<T> row = rowOfRows.Fetch();\n\n        return Chunk<T>(\n          row.Fetch(), \n          ConcatenateRows(\n            row.ShiftLastToFirst(), \n            JoiningAllRows(\n              rowOfRows.ShiftLastToFirst())));\n     });\n    }\n\n```\n\n现在，我们可以创建一个函数来限制无限整数行，实现如下:\n\n```cpp\n    Row<int> GenerateInfinitePrimeRow()\n    {\n      return Binding(\n        GenerateInfiniteIntRow(1),\n        [](int i)\n        {\n          return JoiningPrimeNumber(\n            PrimeCheck(i),\n            [i]()\n            {\n              return ConvertChunkToRow(i);\n            });\n        });\n     }\n\n```\n\n由于`JoiningPrimeNumber()`函数的第二个参数需要一行作为数据类型，我们需要使用`ConvertChunkToRow()`函数将`Chunk`转换为`Row`，实现如下:\n\n```cpp\n    template<class T> Row<T> \n    ConvertChunkToRow(\n      T value)\n      {\n        return Row<T>([value]()\n        {\n          return Chunk<T>(value);\n        });\n      }\n\n```\n\n现在我们可以使用所有前面的类和函数来重构我们的`prime.cpp`代码。\n\n# 将急切求值重构为延迟求值\n\n我们拥有将`prime.cpp`代码重构为惰性代码所需的所有功能。我们将创建一个`prime_lazy.cpp`代码，该代码将首先生成无限整数，并选择其元素的前一百个。之后，我们迭代 100 个元素，并将它们交给函数，函数将在控制台上打印值。代码应该如下所示:\n\n```cpp\n    /* prime_lazy.cpp */\n    #include <iostream>\n    #include <cmath>\n    #include \"../lazyevaluation/lazyevaluation.h\"\n\n    using namespace std;\n\n    Row<void*> PrimeCheck(int i)\n    {\n      // Use preceding implementation\n    }\n\n    Row<int> GenerateInfiniteIntRow(\n      int initialNumber)\n    {\n      // Use preceding implementation\n    }\n\n    template<class T, class U>\n    auto MappingRow(\n      Row<T> row, U func) -> Row<decltype(\n        func(row.Fetch()))>\n      {     \n        // Use preceding implementation\n      }\n\n    template<class T, class U>\n    auto MappingRowByValue(\n      Row<T> row, U func) -> Row<decltype(func())>\n      {\n        // Use preceding implementation\n      }\n\n    template<class T> Row<T>\n    ConvertChunkToRow(\n      T value)\n    {\n      // Use preceding implementation\n    }\n\n    template<class T> Row<T>\n    JoiningAllRows(\n      Row<Row<T>> rowOfRows)\n    {\n      // Use preceding implementation\n    }\n\n    template<class T, class U> Row<T>\n    Binding(\n      Row<T> row, U func)\n      {\n        // Use preceding implementation\n      }\n\n    template<class T, class U>\n    auto JoiningPrimeNumber(\n      Row<T> row, U func) -> decltype(func())\n      {\n        // Use preceding implementation\n      }\n\n    Row<int> GenerateInfinitePrimeRow()\n    {\n      // Use preceding implementation\n    }\n\n    auto main() -> int\n    {\n      cout << \"[prime_lazy.cpp]\" << endl;\n\n      // Generating infinite prime numbers list\n      Row<int> r = GenerateInfinitePrimeRow();\n\n      // Picking the first 100 elements from preceding list\n      Row<int> firstAHundredPrimeNumbers = r.Pick(100);\n\n      // Displaying the first 100 prime numbers\n      cout << \"List of the first 100 prime numbers:\" << endl;\n      ForEach(\n        move(firstAHundredPrimeNumbers),\n        [](int const & i)\n        {\n            cout << i << \"\\t\";\n        });\n\n      return 0;\n    }\n\n```\n\n从前面的代码中我们可以看到，我们有`r`保存无限个数字，然后我们挑选前一百个质数并存储到`firstAHundredPrimeNumbers`中。要将元素的值打印到控制台，我们使用`ForEach()`函数并将 Lambda 表达式传递给它。如果我们运行代码，结果与`prime.cpp`代码完全相同，只是使用的标题是一个区别点。如果我们运行`prime_lazy.cpp`代码，下面的输出是我们应该在控制台上看到的:\n\n![](img/c60d9bc0-433e-4a8a-ad78-6b0691382add.png)\n\n通过使用`template`类，我们在本章中已经揭示了我们可以开发其他的懒惰代码来获得懒惰的好处。\n\nIn the preceding `prime_lazy.cpp` code, I omitted several lines of code that were written in the previous section to avoid the code redundancy. If you find any difficulty following the code because it's not complete, go to [https://github.com/PacktPublishing/LearningCPPFunctionalProgramming](https://github.com/PacktPublishing/LearningCPPFunctionalProgramming).\n\n# 摘要\n\n延迟求值不仅对函数式编程有用，而且对命令式编程也有好处。使用延迟求值，我们可以通过实现缓存和优化技术来获得更高效、更快速的代码。\n\n在下一章中，我们将讨论我们可以在函数式方法中使用的元编程。我们将讨论如何使用元编程来获得它的所有好处，包括代码优化。"
  },
  {
    "path": "docs/learn-cpp-func-prog/6.md",
    "content": "# 六、使用元编程优化代码\n\n在前一章中，我们讨论了使用延迟求值的优化技术，并使用延迟过程、缓存技术和内存化来使我们的代码快速运行。在本章中，我们将使用**元编程**来优化代码，在这里我们将创建一个将创建更多代码的代码。我们将在本章中讨论的主题如下:\n\n*   元编程介绍\n*   构建模板元编程的部分\n*   将流控制重构为模板元编程\n*   在编译时执行中运行代码\n*   模板元编程的优缺点\n\n# 元编程介绍\n\n最简单的说法是元编程是一种通过使用代码来创建代码的技术。在实现元编程时，我们编写一个计算机程序来操纵其他程序，并将它们视为自己的数据。此外，模板是 C++ 中的编译时机制，即**图灵完成**，这意味着任何可由计算机程序表达的计算都可以在运行前由模板元程序以某种形式进行计算。它也经常使用递归，并且有不可变的变量。因此，在元编程中，我们创建在编译代码时运行的代码。\n\n# 使用宏预处理代码\n\n为了开始我们对元编程的讨论，让我们回到 ANSI C 编程语言是一种流行语言的时代。为了简单起见，我们通过创建一个宏来使用 C 预处理器。C 参数化宏也被称为**元函数**，是元编程的例子之一。考虑以下参数化宏:\n\n```cpp\n    #define MAX(a,b) (((a) > (b)) ? (a) : (b))\n\n```\n\n由于 C++ 编程语言与 C 语言的兼容性有缺陷，我们可以使用 C++ 编译器编译前面的宏。让我们创建代码来使用前面的宏，如下所示:\n\n```cpp\n    /* macro.cpp */\n    #include <iostream>\n\n    using namespace std;\n\n    // Defining macro\n    #define MAX(a,b) (((a) > (b)) ? (a) : (b))\n\n    auto main() -> int\n    {\n      cout << \"[macro.cpp]\" << endl;\n\n      // Initializing two int variables\n      int x = 10;\n      int y = 20;\n\n      // Consuming the MAX macro\n      // and assign the result to z variable\n      int z = MAX(x,y);\n\n      // Displaying the result\n      cout << \"Max number of \" << x << \" and \" << y;\n      cout << \" is \" << z << endl;\n\n      return 0;\n    }\n\n```\n\n正如我们在前面的`macro.cpp`代码中所看到的，我们向`MAX`宏传递了两个参数，因为它是一个参数化的宏，这意味着可以从用户那里获得参数。如果我们运行前面的代码，我们应该会在控制台上看到以下输出:\n\n![](img/c3415c09-ba2c-4b3c-9377-afd18f2d9d30.png)\n\n正如我们在本章开头所讨论的，元编程是一种将在编译时运行的代码。通过使用前面代码中的一个宏，我们可以演示从`MAX`宏生成了一个新代码。预处理器将在编译时解析宏并带来新的代码。在编译时，编译器修改代码如下:\n\n```cpp\n    auto main() -> int\n    {\n      // same code\n      // ...\n\n      int z = (((a) > (b)) ? (a) : (b)); // <-- Notice this section\n\n      // same code\n      // ...\n\n      return 0;\n    }\n\n```\n\n除了一行宏预处理器，我们还可以生成一个多行宏元函数。为此，我们可以在行尾使用反斜杠字符。假设我们需要交换这两个值。我们可以创建一个名为`SWAP`的参数化宏，并像下面的代码一样使用它:\n\n```cpp\n    /* macroswap.cpp */\n    #include <iostream>\n\n    using namespace std;\n\n    // Defining multi line macro\n    #define SWAP(a,b) { \\\n      (a) ^= (b); \\\n      (b) ^= (a); \\\n      (a) ^= (b); \\\n    }\n\n    auto main() -> int\n    {\n      cout << \"[macroswap.cpp]\" << endl;\n\n      // Initializing two int variables\n      int x = 10;\n      int y = 20;\n\n      // Displaying original variable value\n      cout << \"before swapping\" << endl;\n      cout << \"x = \" << x << \", y = \" << y ;\n      cout << endl << endl;\n\n      // Consuming the SWAP macro\n      SWAP(x,y);\n\n      // Displaying swapped variable value\n      cout << \"after swapping\" << endl;\n      cout << \"x = \" << x << \", y = \" << y;\n      cout << endl;\n\n      return 0;\n    }\n\n```\n\n正如我们在前面的代码中看到的，我们将创建一个多行预处理器宏，并在每行的末尾使用反斜杠字符。每次我们调用`SWAP`参数化宏时，它都会被宏的实现所取代。如果运行前面的代码，我们将在控制台上看到以下输出:\n\n![](img/502d5c3f-23b1-4998-b4c8-04ea9ef089bf.png)\n\n现在我们已经对元编程有了基本的了解，尤其是在元功能方面，我们可以在下一个主题中更进一步。\n\nWe use parenthesis for each variable in every implementation of the macro preprocessor because the preprocessor is simply replacing our code with the implementation of the macro. Let's suppose we have the following macro:\n`MULTIPLY(a,b) (a * b)` It won't be a problem if we pass the number as the parameters. However, if we pass an operation as the argument, a problem will occur. For instance, if we use the `MULTIPLY` macro as follows:\n`MULTIPLY(x+2,y+5);`\nThen the compiler will replace it as `(x+2*y+5)`. This happens because the macro just replaces the `a` variable with the `x + 2` expression and the `b` variable with the `y + 5` expression, with any additional parentheses. And because the order of multiplication is higher than addition, we will have got the result as follows:\n`(x+2y+5)`\nAnd that is not what we expect. As a result, the best approach is to use parenthesis in each variable of the parameter.\n\n# 在标准库中剖析模板元编程\n\n我们在[第 1 章](1.html)、*潜入现代 C++* 中讨论了标准库，在前一章中也讨论过。C++ 语言提供的标准库大部分是包含不完整函数的模板。但是，它将用于生成完整的函数。模板元编程是在编译时生成 C++ 类型和代码的 C++ 模板。\n\n让我们从标准图书馆中选一门课——T0 课。在`Array`类中，我们可以为其定义一个数据类型。当我们实例化数组时，编译器实际上为我们定义的数据类型的数组生成代码。现在，让我们尝试构建一个简单的`Array`模板实现如下:\n\n```cpp\n    template<typename T>\n    class Array\n    {\n      T element;\n    };\n\n```\n\n然后，我们将`char`和`int`数组实例化如下:\n\n```cpp\n    Array<char> arrChar;\n    Array<int> arrInt;\n\n```\n\n编译器根据我们定义的数据类型创建模板的这两个实现。虽然我们不会在代码中看到这一点，但编译器实际上会创建以下代码:\n\n```cpp\n    class ArrayChar\n    {\n      char element;\n    };\n\n    class ArrayInt\n    {\n      int element;\n    };\n\n    ArrayChar arrChar;\n    ArrayInt arrInt;\n\n```\n\n正如我们在前面的代码片段中看到的，模板元编程是一个在编译时创建另一个代码的代码。\n\n# 构建模板元编程\n\n在我们进一步讨论模板元编程之前，最好先讨论一下构建模板元编程的框架。形成模板元编程的因素有四个- **类型**、**值**、**分支**和**递归**。在本主题中，我们将深入探讨形成模板的因素。\n\n# 向模板中的变量添加值\n\n在本章的开始，我们讨论宏预处理器时讨论了元功能的概念。在宏预处理器中，我们显式地操作源代码；在这种情况下，宏(元功能)操纵源代码。相比之下，我们在 C++ 模板元编程中使用类型。这意味着元函数是一个处理类型的函数。因此，使用模板元编程的更好方法是只在可能的情况下使用类型参数。当我们谈论模板元编程中的变量时，它实际上不是一个变量，因为它的值不能被修改。我们需要变量的名称，这样我们就可以访问它。因为我们将使用类型进行编码，所以命名的值是`typedef`，正如我们在下面的代码片段中所看到的:\n\n```cpp\n    struct ValueDataType\n    {\n      typedef int valueDataType;\n    };\n\n```\n\n通过使用前面的代码，我们将`int`类型存储到`valueDataType`别名中，这样我们就可以使用`valueDataType`变量访问数据类型。如果我们需要存储一个值而不是变量的数据类型，我们可以使用`enum`，所以它将是`enum`本身的数据成员。如果我们想要存储该值，让我们看一下下面的代码片段:\n\n```cpp\n    struct ValuePlaceHolder\n    {\n      enum \n       { \n        value = 1 \n       };\n    };\n\n```\n\n基于前面的代码片段，我们现在可以访问`value`变量来获取它的值。\n\n# 将函数映射到输入参数\n\n我们可以将该变量添加到模板元编程中。现在，我们接下来要做的是检索用户参数，并将它们映射到一个函数。假设我们想开发一个将两个值相乘的`Multiplexer`函数，我们必须使用模板元编程。下面的代码片段可以用来解决这个问题:\n\n```cpp\n    template<int A, int B>\n    struct Multiplexer\n    {\n      enum \n      {\n        result = A * B \n      };\n    };\n\n```\n\n正如我们在前面的代码片段中看到的，模板需要来自用户的两个参数`A`和`B`，它将使用它们通过乘以这两个参数来获得`result`变量的值。我们可以使用以下代码访问结果变量:\n\n```cpp\n    int i = Multiplexer<2, 3>::result;\n\n```\n\n如果我们运行前面的代码片段，`i`变量将存储`6`，因为它将计算`2`时间`3`。\n\n# 根据条件选择正确的流程\n\n当我们有一个以上的功能时，我们必须根据特定的条件选择一个。我们可以通过提供`template`类的两个可选专门化来构建条件分支，如下所示:\n\n```cpp\n    template<typename A, typename B>\n    struct CheckingType\n    {\n      enum \n      { \n        result = 0 \n      };\n    };\n\n    template<typename X>\n    struct CheckingType<X, X>\n    {\n      enum \n      { \n        result = 1 \n      };\n    };\n\n```\n\n正如我们在前面的`template`代码中看到的，我们有两个模板，它们的类型分别是`X`和`A` / `B`。当模板只有一个类型，即`typename X`时，意味着我们比较的两个类型(`CheckingType <X, X>`)完全相同。否则，这两种数据类型是不同的。下面的代码片段可以用来使用前面的两个模板:\n\n```cpp\n    if (CheckingType<UnknownType, int>::result)\n    {\n      // run the function if the UnknownType is int\n    } \n    else \n    { \n      // otherwise run any function \n    }\n\n```\n\n正如我们在前面的代码片段中看到的，我们尝试将`UnknownType`数据类型与`int`类型进行比较。`UnknownType`数据类型可能来自另一个过程。然后，我们可以通过使用模板比较这两种类型来决定下一个要运行的进程。\n\nUp to here, you might wonder how template multiprogramming will help us make code optimization. Soon we will use the template metaprogramming to optimize code. However, we need to discuss other things that will solidify our knowledge in template multiprogramming. For now, please be patient and keep reading.\n\n# 递归重复该过程\n\n我们已经成功地向模板添加了值和数据类型，然后创建了一个分支来根据当前条件决定下一个流程。在基本模板中，我们必须考虑的另一件事是重复这个过程。然而，由于模板中的变量是不可变的，我们不能迭代序列。相反，我们必须重复我们在[第 4 章](4.html)、*中讨论的过程，使用递归算法*重复方法调用。\n假设我们正在开发一个计算阶乘值的模板。我们要做的第一件事是开发一个通用模板，将`I`值传递给函数，如下所示:\n\n```cpp\n    template <int I>\n    struct Factorial\n    {\n      enum \n      { \n        value = I * Factorial<I-1>::value \n      };\n    };\n\n```\n\n正如我们在前面的代码中看到的，我们可以通过运行下面的代码获得阶乘的值:\n\n```cpp\n    Factorial<I>::value;\n\n```\n\n在前面的代码中，`I`是一个整数。\n接下来，我们必须开发一个模板，以确保它不会以无限循环结束。我们可以创建以下模板，将零(`0`)作为参数传递给它:\n\n```cpp\n    template <>\n    struct Factorial<0>\n    {\n      enum \n      { \n        value = 1 \n      };\n    };\n\n```\n\n现在我们有了一对模板，可以在编译时生成阶乘的值。以下是在编译时获取`Factorial(10)`值的示例代码:\n\n```cpp\n    int main()\n    {\n      int fact10 = Factorial<10>::value;\n    }\n\n```\n\n如果我们运行前面的代码，我们将得到`10`的阶乘结果`3628800`。\n\n# 在编译时选择类型\n\n正如我们在前面的主题中所讨论的，`type`是模板的基本部分。但是，我们可以根据用户的输入选择某种类型。让我们创建一个模板来决定变量中应该使用什么类型。以下`types.cpp`代码将展示模板的实现:\n\n```cpp\n    /* types.cpp */\n    #include <iostream>\n\n    using namespace std;\n\n // Defining a data type\n // in template\n template<typename T>\n struct datatype\n {\n using type = T;\n };\n\n    auto main() -> int\n    {\n      cout << \"[types.cpp]\" << endl;\n\n      // Selecting a data type in compile time\n      using t = typename datatype<int>::type;\n\n      // Using the selected data type\n      t myVar = 123;\n\n      // Displaying the selected data type\n      cout << \"myVar = \" << myVar;\n\n      return 0;\n    }\n\n```\n\n正如我们在前面的代码中看到的，我们有一个名为`datatype`的模板。这个模板可以用来选择我们传递给它的`type`。我们可以使用`using`关键字给`type`分配一个变量。从前面的`types.cpp`代码中，我们将从`datatype`模板中为`type`分配一个`t`变量。由于我们将`int`数据类型传递给模板，现在的`t`变量将是`int`。\n我们还可以创建一个代码，根据当前条件选择正确的数据类型。我们将有一个`IfElseDataType`模板，它接受三个参数，即`predicate`、`predicate`参数为真时的数据类型和`predicate`参数为假时的数据类型。代码如下所示:\n\n```cpp\n    /* selectingtype.cpp */\n    #include <iostream>\n\n    using namespace std;\n\n    // Defining IfElseDataType template\n    template<\n      bool predicate,\n      typename TrueType,\n      typename FalseType>\n      struct IfElseDataType\n      {\n      };\n\n    // Defining template for TRUE condition\n    // passed to 'predicate' parameter\n    template<\n      typename TrueType,\n      typename FalseType>\n      struct IfElseDataType<\n       true,\n       TrueType,\n       FalseType>\n       {\n         typedef TrueType type;\n       };\n\n    // Defining template for FALSE condition\n    // passed to 'predicate' parameter\n    template<\n      typename TrueType,\n      typename FalseType>\n      struct IfElseDataType<\n      false,\n      TrueType,\n      FalseType>\n      {\n         typedef FalseType type;\n      };\n\n    auto main() -> int\n    {\n      cout << \"[types.cpp]\" << endl;\n\n      // Consuming template and passing\n      // 'SHRT_MAX == 2147483647'\n      // It will be FALSE\n      // since the maximum value of short\n      // is 32767\n      // so the data type for myVar\n      // will be 'int'\n      IfElseDataType<\n        SHRT_MAX == 2147483647,\n        short,\n        int>::type myVar;\n\n      // Assigning myVar to maximum value\n      // of 'short' type\n      myVar = 2147483647;\n\n      // Displaying the data type of myVar\n      cout << \"myVar has type \";\n      cout << typeid(myVar).name() << endl;\n\n      return 0;\n    }\n\n```\n\n现在，通过拥有`IfElseDataType`模板，我们可以根据我们拥有的条件为变量选择正确的类型。假设我们想将`2147483647`赋给一个变量，这样我们就可以检查它是否是一个短数字。如果是，则`myVar`为`short`类型，否则为`int`。此外，由于`short`类型的最大值是`32767`，因此将谓词赋予为`SHRT_MAX == 2147483647`将导致`FALSE`。因此，`myVar`的类型将是一个`int`类型，我们可以在控制台上出现的以下输出中看到:\n\n![](img/c9f65a57-9612-429f-82b7-55741f74cc50.png)\n\n# 使用模板元编程的流控制\n\n代码流是编写程序的一个重要方面。在许多编程语言中，它们有一个`if-else`、`switch`和`do-while`语句来安排代码的流程。现在，让我们重构通常的代码流，使之成为基于模板的流。我们将从使用`if-else`语句开始，然后是`switch`语句，最后以`do-while`语句结束，所有这些都在模板中。\n\n# 根据当前条件决定下一个进程\n\n现在是时候使用我们之前讨论过的模板了。假设我们有两个函数，我们必须根据某个条件来选择。我们通常使用`if-else`语句，如下所示:\n\n```cpp\n    /* condition.cpp */\n    #include <iostream>\n\n    using namespace std;\n\n    // Function that will run\n    // if the condition is TRUE\n    void TrueStatement()\n    {\n      cout << \"True Statement is run.\" << endl;\n    }\n\n    // Function that will run\n    // if the condition is FALSE\n    void FalseStatement()\n    {\n      cout << \"False Statement is run.\" << endl;\n    }\n\n    auto main() -> int\n    {\n      cout << \"[condition.cpp]\" << endl;\n\n      // Choosing the function\n      // based on the condition\n      if (2 + 3 == 5)\n        TrueStatement();\n      else\n        FalseStatement();\n\n      return 0;\n    }\n\n```\n\n正如我们在前面的代码中看到的，我们有两个函数- `TrueStatement()`和`FalseStatement()`。我们在代码中还有一个条件——`2 + 3 == 5`。由于条件是`TRUE`，那么`TrueStatement()`功能将运行，如下图所示:\n\n![](img/9e5c6760-bbad-42e6-9208-48d2213aca67.png)\n\n现在，让我们重构前面的`condition.cpp`代码。我们将在这里创建三个模板。首先，输入条件的模板初始化如下:\n\n```cpp\n    template<bool predicate> class IfElse\n\n```\n\n然后，我们为每个条件创建两个模板- `TRUE`或`FALSE`。名称如下:\n\n```cpp\n    template<> class IfElse<true>\n    template<> class IfElse<false> \n\n```\n\n前面代码片段中的每个模板都将运行我们之前创建的函数-`TrueStatement()`和`FalseStatement()`函数。我们将得到完整的代码如下`conditionmeta.cpp`代码:\n\n```cpp\n    /* conditionmeta.cpp */\n    #include <iostream>\n\n    using namespace std;\n\n    // Function that will run\n    // if the condition is TRUE\n    void TrueStatement()\n    {\n      cout << \"True Statement is run.\" << endl;\n    }\n\n    // Function that will run\n    // if the condition is FALSE\n    void FalseStatement()\n    {\n      cout << \"False Statement is run.\" << endl;\n    }\n\n    // Defining IfElse template\n    template<bool predicate>\n    class IfElse\n    {\n    };\n\n    // Defining template for TRUE condition\n    // passed to 'predicate' parameter\n    template<>\n    class IfElse<true>\n    {\n      public:\n        static inline void func()\n        {\n          TrueStatement();\n        }\n    };\n\n    // Defining template for FALSE condition\n    // passed to 'predicate' parameter\n    template<>\n    class IfElse<false>\n    {\n      public:\n        static inline void func()\n        {\n          FalseStatement();\n        }\n    };\n\n    auto main() -> int\n    {\n      cout << \"[conditionmeta.cpp]\" << endl;\n\n      // Consuming IfElse template\n      IfElse<(2 + 3 == 5)>::func();\n\n      return 0;\n    }\n\n```\n\n可以看到，我们把条件放在`IfElse`模板的括号上，然后在模板里面调用`func()`方法。如果我们运行`conditionmeta.cpp`代码，我们将获得与`condition.cpp`代码完全相同的输出，如下所示:\n\n![](img/52dfab6d-8e80-4036-9e19-95f40d013725.png)\n\n我们现在有了`if-else`语句来在模板元编程中流动我们的代码。\n\n# 选择正确的语句\n\n在 C++ 编程中，以及其他编程语言中，我们使用`switch`语句根据我们给`switch`语句的值来选择某个进程。如果该值与开关情况匹配，它将在该情况下运行进程。让我们看看下面实现`switch`语句的`switch.cpp`代码:\n\n```cpp\n    /* switch.cpp */\n    #include <iostream>\n\n    using namespace std;\n\n    // Function to find out\n    // the square of an int\n    int Square(int a)\n    {\n      return a * a;\n    }\n\n    auto main() -> int\n    {\n      cout << \"[switch.cpp]\" << endl;\n\n      // Initializing two int variables\n      int input = 2;\n      int output = 0;\n\n      // Passing the correct argument\n      // to the function\n      switch (input)\n      {\n        case 1:\n            output = Square(1);\n            break;\n        case 2:\n            output = Square(2);\n            break;\n        default:\n            output = Square(0);\n            break;\n      }\n\n      // Displaying the result\n      cout << \"The result is \" << output << endl;\n\n      return 0;\n    }\n\n```\n\n正如我们在前面的代码中看到的，我们有一个名为`Square()`的函数，它接受一个参数。我们传递给它的参数基于我们给 switch 语句的值。由于我们传递给开关的值是`2`，因此将运行`Square(2)`方法。以下屏幕截图是我们将在控制台屏幕上看到的内容:\n\n![](img/bb934a6e-293e-47b9-b771-cb7d9da4832f.png)\n\n为了将`switch.cpp`代码重构为模板元编程，我们必须创建三个模板，这三个模板由我们计划运行的函数组成。首先，我们将创建初始化模板，从用户处检索值，如下:\n\n```cpp\n    template<int val> class SwitchTemplate \n\n```\n\n前面的初始化模板也将用作默认值。接下来，我们将为每个可能的值添加两个模板，如下所示:\n\n```cpp\n    template<> class SwitchTemplate<1>\n    template<> class SwitchTemplate<2> \n\n```\n\n每个前面的模板将运行`Square()`函数，并根据模板的值传递参数。完整的代码编写如下:\n\n```cpp\n    /* switchmeta.cpp */\n    #include <iostream>\n\n    using namespace std;\n\n    // Function to find out\n    // the square of an int\n    int Square(int a)\n    {\n      return a * a;\n    }\n\n    // Defining template for\n    // default output\n    // for any input value\n    template<int val>\n    class SwitchTemplate\n    {\n      public:\n        static inline int func()\n        {\n          return Square(0);\n        }\n    };\n\n    // Defining template for\n    // specific input value\n    // 'val' = 1\n    template<>\n    class SwitchTemplate<1>\n    {\n       public:\n         static inline int func()\n         {\n           return Square(1);\n         }\n    };\n\n    // Defining template for\n    // specific input value\n    // 'val' = 2\n    template<>\n    class SwitchTemplate<2>\n    {\n       public:\n         static inline int func()\n         {\n            return Square(2);\n         }\n    };\n\n    auto main() -> int\n    {\n      cout << \"[switchmeta.cpp]\" << endl;\n\n      // Defining a constant variable\n      const int i = 2;\n\n      // Consuming the SwitchTemplate template\n      int output = SwitchTemplate<i>::func();\n\n      // Displaying the result\n      cout << \"The result is \" << output << endl;\n\n      return 0;\n    }\n\n```\n\n我们可以看到，我们做的和`conditionmeta.cpp`一样——我们在模板里面调用`func()`方法来运行选中的函数。这个`switch-case`条件的值是我们放在尖括号中的模板。如果我们运行前面的`switchmeta.cpp`代码，我们将在控制台上看到以下输出:\n\n![](img/eeb896aa-2598-4995-b3b0-bbb61386f762.png)\n\n正如我们在前面的截图中看到的，与`switch.cpp`代码相比，`switchmeta.cpp`代码的输出完全相同。因此，我们已经成功地将`switch.cpp`代码重构为模板元编程。\n\n# 循环流程\n\n当我们迭代一些东西时，我们通常使用`do-while`循环。假设我们需要打印某些数字，直到它达到零(`0`)。代码如下:\n\n```cpp\n    /* loop.cpp */\n    #include <iostream>\n\n    using namespace std;\n\n    // Function for printing\n    // given number\n    void PrintNumber(int i)\n    {\n      cout << i << \"\\t\";\n    }\n\n    auto main() -> int\n    {\n      cout << \"[loop.cpp]\" << endl;\n\n      // Initializing an int variable\n      // marking as maximum number\n      int i = 100;\n\n      // Looping to print out\n      // the numbers below i variable\n      cout << \"List of numbers between 100 and 1\";\n      cout << endl;\n      do\n      {\n        PrintNumber(i);\n      }\n      while (--i > 0);\n      cout << endl;\n\n      return 0;\n    }\n\n```\n\n正如我们在前面的代码中看到的，我们将打印数字`100`，减少它的值，然后再次打印。它将一直运行，直到数字达到零(`0`)。控制台上的输出应该如下所示:\n\n![](img/1a84c23b-dd08-4dfc-99a6-273c36918e06.png)\n\n现在，让我们将其重构为模板元编程。在这里，我们只需要两个模板就可以实现模板元编程中的`do-while`循环。首先，我们将创建以下模板:\n\n```cpp\n    template<int limit> class DoWhile\n\n```\n\n前面代码中的限制是传递给`do-while`循环的值。并且，为了不使循环成为无限循环，我们必须在到达零点(`0`)时设计`DoWhile`模板，如下图所示:\n\n```cpp\n    template<> class DoWhile<0>\n\n```\n\n前面的模板不会做任何事情，因为它只是用来打破循环。`do-while`循环的完整重构类似于下面的`loopmeta.cpp`代码:\n\n```cpp\n    /* loopmeta.cpp */\n    #include <iostream>\n\n    using namespace std;\n\n    // Function for printing\n    // given number\n    void PrintNumber(int i)\n    {\n      cout << i << \"\\t\";\n    }\n\n    // Defining template for printing number\n    // passing to its 'limit' parameter\n    // It's only run\n    // if the 'limit' has not been reached\n    template<int limit>\n    class DoWhile\n    {\n       private:\n         enum\n         {\n           run = (limit-1) != 0\n         };\n\n       public:\n         static inline void func()\n         {\n           PrintNumber(limit);\n           DoWhile<run == true ? (limit-1) : 0>\n            ::func();\n         }\n    };\n\n    // Defining template for doing nothing\n    // when the 'limit' reaches 0\n    template<>\n    class DoWhile<0>\n    {\n      public:\n        static inline void func()\n        {\n        }\n    };\n\n    auto main() -> int\n    {\n      cout << \"[loopmeta.cpp]\" << endl;\n\n      // Defining a constant variable\n      const int i = 100;\n\n      // Looping to print out\n      // the numbers below i variable\n      // by consuming the DoWhile\n      cout << \"List of numbers between 100 and 1\";\n      cout << endl;\n      DoWhile<i>::func();\n      cout << endl;\n\n      return 0;\n    }\n\n```\n\n然后我们调用模板中的`func()`方法来运行我们想要的函数。如果我们运行代码，我们将在屏幕上看到以下输出:\n\n![](img/b5fd496c-ddac-483e-b0db-523307b3c91e.png)\n\n同样，我们已经成功地将`loop.cpp`代码重构为`loopmeta.cpp`代码，因为两者具有完全相同的输出。\n\n# 在编译时执行代码\n\n正如我们前面讨论的，模板元编程将通过创建新代码在编译时运行代码。现在，让我们看看如何在这一节中获取编译时常数并生成编译时类。\n\n# 获取编译时常数\n\n为了检索编译时常数，让我们创建一个包含斐波那契算法模板的代码。我们将使用模板，这样编译器将在编译时提供值。代码应如下所示:\n\n```cpp\n    /* fibonaccimeta.cpp */\n    #include <iostream>\n\n    using namespace std;\n\n    // Defining Fibonacci template\n    // to calculate the Fibonacci sequence\n    template <int number>\n    struct Fibonacci\n    {\n      enum\n      {\n        value =\n            Fibonacci<number - 1>::value +\n            Fibonacci<number - 2>::value\n      };\n    };\n\n    // Defining template for\n    // specific input value\n    // 'number' = 1\n    template <>\n    struct Fibonacci<1>\n    {\n      enum\n      {\n        value = 1\n      };\n    };\n\n    // Defining template for\n    // specific input value\n    // 'number' = 0\n    template <>\n    struct Fibonacci<0>\n    {\n      enum\n      {\n        value = 0\n      };\n    };\n\n    auto main() -> int\n    {\n      cout << \"[fibonaccimeta.cpp]\" << endl;\n\n      // Displaying the compile-time constant\n      cout << \"Getting compile-time constant:\";\n      cout << endl;\n      cout << \"Fibonacci(25) = \";\n      cout << Fibonacci<25>::value;\n      cout << endl;\n\n      return 0;\n    }\n\n```\n\n正如我们在前面的代码中看到的，斐波那契模板中的值变量将提供一个编译时常数。如果我们运行前面的代码，我们将在控制台屏幕上看到以下输出:\n\n![](img/015e83df-4905-4a3c-aa49-c8f233a2282c.png)\n\n现在，我们有了由编译器生成的编译时常数`75025`。\n\n# 使用编译时类生成来生成类\n\n除了生成编译时常数，我们还将在编译时生成类。假设我们有一个模板来找出`0`到`X`范围内的素数。以下`isprimemeta.cpp`代码将解释寻找质数的模板元编程的实现:\n\n```cpp\n    /* isprimemeta.cpp */\n    #include <iostream>\n\n    using namespace std;\n\n    // Defining template that decide\n    // whether or not the passed argument\n    // is a prime number\n    template <\n      int lastNumber,\n      int secondLastNumber>\n    class IsPrime\n    {\n      public:\n        enum\n        {\n          primeNumber = (\n            (lastNumber % secondLastNumber) &&\n            IsPrime<lastNumber, secondLastNumber - 1>\n                ::primeNumber)\n        };\n     };\n\n    // Defining template for checking\n    // the number passed to the 'number' parameter\n    // is a prime number\n    template <int number>\n    class IsPrime<number, 1>\n    {\n      public:\n        enum\n        {\n          primeNumber = 1\n        };\n    };\n\n    // Defining template to print out\n    // the passed argument is it's a prime number\n    template <int number>\n    class PrimeNumberPrinter\n    {\n      public:\n        PrimeNumberPrinter<number - 1> printer;\n\n      enum\n      {\n        primeNumber = IsPrime<number, number - 1>\n            ::primeNumber\n      };\n\n      void func()\n      {\n        printer.func();\n\n        if (primeNumber)\n        {\n            cout << number << \"\\t\";\n        }\n      }\n    };\n\n    // Defining template to just ignoring the number\n    // we pass 1 as argument to the parameter\n    // since 1 is not prime number\n    template<>\n    class PrimeNumberPrinter<1>\n    {\n      public:\n        enum\n        {\n          primeNumber = 0\n        };\n\n        void func()\n        {\n        }\n    };\n\n    int main()\n    {\n      cout << \"[isprimemeta.cpp]\" << endl;\n\n      // Displaying the prime numbers between 1 and 500\n      cout << \"Filtering the numbers between 1 and 500 \";\n      cout << \"for of the prime numbers:\" << endl;\n\n      // Consuming PrimeNumberPrinter template\n      PrimeNumberPrinter<500> printer;\n\n      // invoking func() method from the template\n      printer.func();\n\n      cout << endl;\n      return 0;\n    }\n\n```\n\n有两种不同角色的模板-**质检员**，确保传递的数字是质数，以及**打印机**，向控制台显示质数。然后，当代码访问`PrimeNumberPrinter<500> printer`和`printer.func()`时，编译器在编译时生成该类。当我们运行前面的`isprimemeta.cpp`代码时，我们将在控制台屏幕上看到以下输出:\n\n![](img/1377d14f-9287-4e54-93b8-39bf5906fe43.png)\n\n由于我们将`500`传递给模板，我们将得到从`0`到`500`的质数。前面的输出已经证明编译器已经成功地生成了一个编译时类，因此我们可以获得正确的值。\n\n# 元编程的优点和缺点\n\n在我们讨论了模板元编程之后，我们得到了以下优势:\n\n*   模板元编程没有副作用，因为它是不可变的，所以我们不能修改现有的类型\n*   与不实现元编程的代码相比，代码可读性更好\n*   它减少了代码的重复\n\n虽然我们可以从模板元编程中获益，但也有以下几个缺点:\n\n*   语法相当复杂。\n*   编译时间更长，因为我们现在在编译时执行代码。\n*   编译器可以更好地优化生成的代码并执行内联，例如 C `qsort()`函数和 C++ `sort`模板。在 C 语言中，`qsort()`函数接受一个指向比较函数的指针，因此将会有一个没有内联的`qsort`代码副本。它将通过指向比较例程的指针进行调用。在 C++ 中，`std::sort`是一个模板，它可以拿一个`functor`对象作为比较器。对于用作比较器的每种不同类型，都有不同的`std::sort`副本。如果我们使用带有重载`operator()`函数的`functor`类，对比较器的调用可以很容易地内联到这个`std::sort`副本中。\n\n# 摘要\n\n元编程，尤其是模板元编程，会自动为我们创建新代码，因此我们不需要在源代码中编写大量代码。通过使用模板元编程，我们可以重构代码的流控制，并在编译时执行代码。\n在下一章中，我们将讨论将为我们构建的应用带来响应性增强的并发技术。我们可以使用并行技术同时运行代码中的进程。"
  },
  {
    "path": "docs/learn-cpp-func-prog/7.md",
    "content": "# 七、使用并发运行并行执行\n\n在前一章中，我们讨论了将在编译时执行代码的模板元编程。它还将改进我们代码的流控制，因为我们可以使用模板重构流。现在，在本章中，我们将讨论 C++ 中的并发性，当我们同时运行两个或多个进程时，我们必须再次控制流。在本章中，我们将讨论以下主题:\n\n*   在 C++ 编程中运行单线程和多线程\n*   同步线程以避免死锁\n*   使用 Windows 中的**句柄**资源创建一个线程\n\n# C++ 中的并发性\n\n如今，许多编程语言都支持并发。在并发编程中，代码的计算不是按顺序进行的，而是在重叠的时间段内执行。这将使我们的程序响应，因为代码不需要等到所有的计算完成。假设我们想开发一个可以同时播放视频和下载巨大视频文件的程序。如果没有并发技术，我们必须等待视频下载成功后才能播放另一个视频文件。通过使用这种技术，我们可以分割这两个任务，播放和下载一个视频，然后同时运行它们。\n\n在 C++ 11 宣布之前，C++ 程序员依靠`Boost::thread`使用多线程技术创建并发程序。在多线程中，我们将进程分成最小的序列，并发运行这些小进程。现在，在 C++ 11 库中，我们得到了`thread`类来解决我们使用多线程技术的并发需求。\n\n# 处理单线程代码\n\n要使用`thread`类，我们只需要创建一个`std::thread`的实例，并传递函数名作为参数。然后我们调用`std::join()`暂停进程，直到所选线程完成其进程。我们来看看下面的`singlethread.cpp`代码:\n\n```cpp\n    /* singlethread.cpp */\n    #include <thread>\n    #include <iostream>\n\n    using namespace std;\n\n    void threadProc()\n    {\n      cout << \"Thread ID: \";\n      cout << this_thread::get_id() << endl;\n    }\n\n    auto main() -> int\n    {\n      cout << \"[singlethread.cpp]\" << endl;\n\n      thread thread1(threadProc);\n      thread1.join();\n\n      return 0;\n    }\n\n```\n\n正如我们在前面的代码中看到的，我们有一个名为`threadProc()`的函数，我们将其传递到`main()`函数中的`thread1`初始化中。初始化之后，我们调用`join()`方法来执行`thread1`对象。我们将在控制台上看到如下输出:\n\n![](img/86d5d8c0-8176-4e09-8737-4e31bd48ba43.png)\n\n我们已经成功地在代码中运行了一个线程。现在，让我们在`main()`函数中添加一段代码，它将迭代一行代码。我们将同时运行它们。`singlethread2.cpp`的代码如下:\n\n```cpp\n    /* singlethread2.cpp */\n    #include <thread>\n    #include <chrono>\n    #include <iostream>\n\n    using namespace std;\n\n    void threadProc()\n    {\n      for (int i = 0; i < 5; i++)\n      {\n        cout << \"thread: current i = \";\n        cout << i << endl;\n      }\n    }\n\n    auto main() -> int\n    {\n      cout << \"[singlethread2.cpp]\" << endl;\n\n      thread thread1(threadProc);\n\n      for (int i = 0; i < 5; i++)\n {\n cout << \"main : current i = \" << i << endl;\n\n        this_thread::sleep_for(\n            chrono::milliseconds(5)); }\n\n      thread1.join();\n\n      return 0;\n    }\n\n```\n\n正如我们在前面的代码中看到的，我们添加了一个`for`循环来迭代一些代码，并与`thread1`同时运行。为了理解它，我们还在`threadProc()`函数中添加了一个`for`循环。让我们看看下面的截图，以了解我们将获得什么样的输出:\n\n![](img/8aa03435-bafd-4bef-a608-028751070653.png)\n\n我们看到`threadProc()`函数和`main()`函数中的代码同时运行。你们中的一些人可能会得到不同的结果，但这没关系，因为结果无法预测，因为它取决于设备本身。然而，目前，我们已经能够同时运行两个进程。\n\nI ran the preceding code multiple times to get the output we see in the preceding screenshot. You might see different order in between the `threadProc()` and `main()` function or get a messy output since the flow of the thread is unpredictable.\n\n# 处理多线程代码\n\n在多线程技术中，我们同时运行两个或多个线程。假设我们同时运行五个线程。我们可以使用下面的`multithread.cpp`代码将这五个线程存储在一个数组中:\n\n```cpp\n    /* multithread.cpp */\n    #include <thread>\n    #include <iostream>\n\n    using namespace std;\n\n    void threadProc()\n    {\n      cout << \"Thread ID: \";\n      cout << this_thread::get_id() << endl;\n    }\n\n    auto main() -> int\n    {\n      cout << \"[multithread.cpp]\" << endl;\n\n      thread threads[5];\n\n      for (int i = 0; i < 5; ++ i)\n      {\n        threads[i] = thread(threadProc);\n      }\n\n      for (auto& thread : threads)\n      {\n        thread.join();\n      }\n\n      return 0;\n    }\n\n```\n\n在我们基于前面的代码初始化这五个线程之后，我们将为所有线程运行`join()`方法来执行它们。通过使用`join()`方法，程序将等待调用线程中的所有进程完成，然后继续下一个进程(如果有)。我们在控制台中看到的结果如下:\n\n![](img/808eb931-b775-4816-b4c6-e756f8d99112.png)\n\n在前面的截图中，我们看到所有五个线程都已成功执行。我们也可以使用 Lambda 表达式初始化线程。下面的`lambdathread.cpp`代码是从前面的代码重构而来的，它使用 Lambda 而不是创建一个单独的函数:\n\n```cpp\n    /* lambdathread.cpp */\n    #include <thread>\n    #include <iostream>\n\n    using namespace std;\n\n    auto main() -> int\n    {\n      cout << \"[lambdathread.cpp]\" << endl;\n\n      thread threads[5];\n\n      for (int i = 0; i < 5; ++ i)\n      {\n threads[i] = thread([]()\n {\n cout << \"Thread ID: \";\n cout << this_thread::get_id() << endl;\n });\n       }\n\n      for (auto& thread : threads)\n      {\n        thread.join();\n      }\n\n      return 0;\n    }\n\n```\n\n如果我们看到`lambdathread.cpp`代码与`multithread.cpp`代码相比没有显著变化。但是，由于该函数只会被调用一次，所以最好使用 Lambda，这样更容易维护它。我们将在控制台上看到的输出如下截图所示，与`multithread.cpp`代码输出相比没有太大区别:\n\n![](img/533c3423-6bf0-450e-99a6-6988f42d3d61.png)\n\n虽然我们在运行`lambdathread.cpp`时检索到了与`multithread.cpp`代码相同的输出，但是当我们使用 Lambda 表达式初始化线程时，我们有一个清晰的代码。我们不需要创建另一个传递给`Thread`的方法，例如`threadProc()`，因为这个方法实际上只使用一次。\n\nAgain, note that the result you see on your screen might be different from the screenshot I gave.\n\n# 使用互斥体同步线程\n\n到目前为止，我们已经成功执行了一个多线程代码。但是，如果我们使用一个共享对象并在线程中操作它，就会出现问题。叫做**同步**。在本节中，我们将尝试通过应用`mutex`技术来避免这个问题。\n\n# 避免同步问题\n\n正如我们前面讨论的，在这一节中，我们必须确保在线程中运行的共享对象在执行时给出正确的值。假设我们有一个名为`counter`的全局变量，我们计划在所有五个线程中增加它的值。每个线程将执行`10000`次增量迭代，因此我们期望得到所有五个线程的`50000`结果。代码如下:\n\n```cpp\n    /* notsync.cpp */\n    #include <thread>\n    #include <iostream>\n\n    using namespace std;\n\n    auto main() -> int\n    {\n      cout << \"[notsync.cpp]\" << endl;\n\n      int counter = 0;\n\n      thread threads[5];\n\n      for (int i = 0; i < 5; ++ i)\n      {\n        threads[i] = thread([&counter]()\n        {\n for (int i = 0; i < 10000; ++ i)\n {\n ++ counter;\n cout << \"Thread ID: \";\n cout << this_thread::get_id();\n cout << \"\\tCurrent Counter = \";\n cout << counter << endl;\n }\n        });\n      }\n\n      for (auto& thread : threads)\n      {\n        thread.join();\n      }\n\n      cout << \"Final result = \" << counter << endl;\n\n      return 0;\n    }\n\n```\n\n现在，让我们看看下面的截图，当我们运行前面的代码时，我们可能会在控制台上看到它:\n\n![](img/e0d31bea-b6e2-49db-acff-172784871ce3.png)\n\n不幸的是，根据前面的截图，我们没有得到我们所期望的。这是因为增量进程不是原子操作，因为原子操作将保证并发进程的隔离。\n\nIf you get a different output, don't worry, we are still on the right track as this program demonstrates synchronization issues, as you will see next.\n\n如果我们更深入地跟踪输出，我们会看到有两个线程为`counter`变量执行完全相同的值，如下面的截图所示:\n\n![](img/8f7a5eff-9464-4f99-b4e6-3bb1f2553f25.png)\n\n我们看到 ID 为`2504`和`5524`的线程在计数器变量的值为`44143`时访问计数器变量。这就是为什么我们在运行前面的代码时会检索到一个意外的结果。现在，我们需要使增量操作成为原子操作，该操作将在没有任何其他进程能够读取或更改操作期间读取或更改的状态的情况下执行。\n\n为了解决这个问题，我们可以使用`mutex`类来使我们的计数器变量`thread-safe`。这意味着在线程访问计数器变量之前，它必须确保该变量不会被其他线程访问。我们可以使用`mutex`类中的`lock()`和`unlock()`方法来锁定和解锁目标变量。让我们看看下面的`mutex.cpp`代码来演示`mutex`的实现:\n\n```cpp\n    /* mutex.cpp */\n    #include <thread>\n    #include <mutex>\n    #include <iostream>\n\n    using namespace std;\n\n    auto main() -> int\n    {\n      cout << \"[mutex.cpp]\" << endl;\n\n      mutex mtx;\n      int counter = 0;\n\n      thread threads[5];\n\n      for (int i = 0; i < 5; ++ i)\n      {\n        threads[i] = thread([&counter, &mtx]()\n        {\n           for (int i = 0; i < 10000; ++ i)\n           {\n             mtx.lock();\n             ++ counter;\n             mtx.unlock();\n\n             cout << \"Thread ID: \";\n             cout << this_thread::get_id();\n             cout << \"\\tCurrent Counter = \";\n             cout << counter << endl;\n           }\n        });\n      }\n\n      for (auto& thread : threads)\n      {\n        thread.join();\n      }\n\n      cout << \"Final result = \" << counter << endl;\n\n      return 0;\n    }\n\n```\n\n正如我们在前面的代码中看到的，在代码递增`counter`变量之前，它调用`lock()`方法。之后，它调用`unlock()`方法通知其他线程`counter`变量现在可以自由操作了。如果我们运行前面的代码，我们应该会在控制台上看到以下输出:\n\n![](img/0e60f03b-94fb-43c7-aa0e-2e9ede5261b2.png)\n\n通过使用`mutex`类，现在我们检索我们期望的结果，正如我们在前面的截图中看到的。\n\n# 自动解锁变量\n\n我们现在知道如何锁定变量，以确保没有两个处理相同值的线程同时从中检索到正确的值。但是，如果在线程调用`unlock()`方法之前抛出异常，就会出现问题。如果变量的状态保持锁定，程序将被完全锁定。为了解决这个问题，我们可以使用`lock_guard<mutex>`来锁定变量，并确保无论发生什么情况，它都将在范围的末尾解锁。下面这段代码是通过添加`lock_guard<mutex>`功能从前面的代码重构而来的:\n\n```cpp\n    /* automutex.cpp */\n    #include <thread>\n    #include <mutex>\n    #include <iostream>\n\n    using namespace std;\n\n    auto main() -> int\n    {\n      cout << \"[automutex.cpp]\" << endl;\n\n      mutex mtx;\n      int counter = 0;\n\n      thread threads[5];\n\n      for (int i = 0; i < 5; ++ i)\n      {\n        threads[i] = thread([&counter, &mtx]()\n        {\n          for (int i = 0; i < 10000; ++ i)\n          {\n            {\n              lock_guard <mutex> guard(mtx);\n              ++ counter;\n             }\n\n             cout << \"Thread ID: \";\n             cout << this_thread::get_id();\n             cout << \"\\tCurrent Counter = \";\n             cout << counter << endl;\n          }\n         });\n       }\n\n       for (auto& thread : threads)\n       {\n          thread.join();\n       }\n\n      cout << \"Final result = \" << counter << endl;\n\n      return 0;\n    }\n\n```\n\n从前面的`automutex.cpp`代码中我们可以看到，它在递增`counter`变量之前调用`lock_guard <mutex> guard(mtx)`。如果我们运行代码，我们将获得与`mutex.cpp`代码完全相同的输出。然而，现在我们有了一个不会被不可预测地锁定的程序。\n\n# 使用递归互斥避免死锁\n\n在前一节中，我们使用`lock_guard`来确保变量不会被多个线程访问。但是如果有多个`lock_guard`获得锁，我们还是会面临一个问题。在下面这段代码中，我们有两个函数将调用`lock_guard` - `Multiplexer()`和`Divisor()`。除此之外，我们还有一个函数会调用这两个函数- `RunAll()`在调用这两个函数之前会先调用`lock_guard`。代码应该如下所示:\n\n```cpp\n    /* deadlock.cpp */\n    #include <thread>\n    #include <mutex>\n    #include <iostream>\n\n    using namespace std;\n\n    struct Math\n    {\n      mutex mtx;\n      int m_content;\n\n      Math() : m_content(0)\n      {\n      }\n\n      // This method will lock the mutex\n      void Multiplexer(int i)\n      {\n        lock_guard<mutex> lock(mtx);\n        m_content *= i;\n        cout << \"Multiplexer() is called. m_content = \";\n        cout << m_content << endl;\n      }\n\n      // This method will lock the mutex also\n      void Divisor(int i)\n      {\n        lock_guard<mutex> lock(mtx);\n        m_content /= i;\n        cout << \"Divisor() is called. m_content = \";\n        cout << m_content << endl;\n      }\n\n      // This method will invoke \n      // the two preceding methods\n      // which each method locks the mutex\n      void RunAll(int a)\n      {\n        lock_guard<mutex> lock(mtx);\n        Multiplexer(a);\n        Divisor(a);\n      }\n    };\n\n    auto main() -> int\n    {\n      cout << \"[deadlock.cpp]\" << endl;\n\n      // Instantiating Math struct\n      // and invoking the RunAll() method \n      Math math;\n      math.RunAll(10);\n\n      return 0;\n    }\n\n```\n\n我们将成功编译以下代码。但是，如果我们运行前面的代码，由于**死锁**，程序不会退出，因此会出现错误。这是因为同一个互斥体不能被多个线程获取两次。调用`RunAll()`功能时，获取`lock`对象。`RunAll()`功能中的`Multiplexer()`功能也想获得`lock`。但是`lock`已经被`RunAll()`功能锁定。为了解决这个问题，我们可以将`lock_guard<mutex>`替换为`lock_guard<recursive_mutex>`，如下面这段代码所示:\n\n```cpp\n    /* recursivemutex.cpp */\n    #include <thread>\n    #include <mutex>\n    #include <iostream>\n\n    using namespace std;\n\n    struct Math\n    {\n recursive_mutex mtx;\n      int m_content;\n\n      Math() : m_content(1)\n      {\n      }\n\n      // This method will lock the mutex\n      void Multiplexer(int i)\n      {\n        lock_guard<recursive_mutex> lock(mtx);\n        m_content *= i;\n        cout << \"Multiplexer() is called. m_content = \";\n        cout << m_content << endl;\n      }\n\n      // This method will lock the mutex also\n      void Divisor(int i)\n      {\n        lock_guard<recursive_mutex> lock(mtx);\n        m_content /= i;\n        cout << \"Divisor() is called. m_content = \";\n        cout << m_content << endl;\n      }\n\n      // This method will invoke \n      // the two preceding methods\n      // which each method locks the mutex\n      void RunAll(int a)\n      {\n        lock_guard<recursive_mutex> lock(mtx);\n        Multiplexer(a);\n        Divisor(a);\n      }\n    };\n\n    auto main() -> int\n    {\n      cout << \"[recursivemutex.cpp]\" << endl;\n\n      // Instantiating Math struct\n      // and invoking the RunAll() method \n      Math math;\n      math.RunAll(10);\n\n      return 0;\n    }\n\n```\n\n现在，我们可以成功编译并运行前面的代码。我们可以使用`lock_guard<recursive_mutex>`类，它将允许互斥锁被锁定不止一次，而不会进入死锁。当我们运行前面的代码时，将在控制台上看到下面的屏幕截图:\n\n![](img/7faafdd2-15bc-4f57-82ca-d50df7296692.png)\n\n现在，我们知道如果我们想递归地调用锁定同一个`mutex`的函数，我们需要使用一个递归`mutex`。\n\n# 了解 Windows 操作系统中的线程处理\n\n让我们转到一个被许多用户计算机广泛使用的特定操作系统，那就是 Windows。我们的代码必须在领先的操作系统供应商(如微软)的商业平台上运行。因此，我们现在将在 Windows 操作系统中运行该线程。在这个操作系统中，线程是一个内核资源，这意味着它是一个由操作系统内核创建和拥有的对象，并且驻留在内核中。内核本身是一个核心程序，可以完全控制系统中的一切。在这一部分，我们将在视窗操作系统中开发一个线程，这样我们的程序就可以在这个操作系统中很好地工作。\n\n# 使用手柄\n\n在 Windows 操作系统中，句柄是对资源的抽象引用值。在本讨论中，我们将使用抽象引用来保持线程。让我们假设我们有一个`threadProc()`函数，它将在`hnd`变量中的线程内被调用。代码如下:\n\n```cpp\n    /* threadhandle.cpp */\n    #include <iostream>\n    #include <windows.h>\n\n    using namespace std;\n\n    auto threadProc(void*) -> unsigned long\n    {\n      cout << \"threadProc() is run.\" << endl;\n      return 100;\n    }\n\n    auto main() -> int\n    {\n      cout << \"[threadhandle.cpp]\" << endl;\n\n      auto hnd = HANDLE\n      {\n        CreateThread(\n            nullptr,\n            0,\n            threadProc,\n            nullptr,\n            0,\n            nullptr)\n      };\n\n      if (hnd)\n      {\n        WaitForSingleObject(hnd, INFINITE);\n\n        unsigned long exitCode;\n        GetExitCodeThread(hnd, &exitCode);\n\n        cout << \"The result = \" << exitCode << endl;\n\n        CloseHandle(hnd);\n      }\n\n      return 0;\n    }\n\n```\n\n正如我们在前面的代码中看到的，我们使用`windows.h`头提供的`CreateThread()`函数来生成一个线程。现在，我们只传递`nullptr`值作为默认参数，除了`threadProc`作为我们将从线程调用的函数。\n\n我们初始化线程的句柄后，可以保证`hnd`变量包含线程的句柄，然后调用`WaitForSingleObject()`函数。它类似于我们在前一节中使用的`join()`方法，将运行线程并等待线程完成。既然线程句柄是我们使用的资源，别忘了使用`CloseHandle()`功能释放它。如果我们运行前面的代码，我们将在控制台屏幕上看到以下输出:\n\n![](img/b74afe83-9bdb-431b-b75f-7a7da30e77ba.png)\n\n正如我们所看到的，我们已经成功地运行了线程，因为我们已经从`threadProc()`函数获得了预期的进程。\n\n# 重构到唯一句柄\n\n现在，为了简化我们的编程过程，我们将创建一个名为`NullHandle`的类，每当我们不再需要该资源时，它将自动释放该资源。它将从`UniqueHandle`类构建，我们也将开发它。这些课程可以在`uniquehandle.h`档案中找到。`UniqueHandle`实施如下:\n\n```cpp\n    template <typename C>\n    class UniqueHandle\n    {\n      private:\n        HANDLE m_val;\n\n        void Close()\n        {\n          if (*this)\n          {\n            C::Exit(m_val);\n          }\n        }\n\n      public:\n        // Copy assignment operator \n        UniqueHandle(UniqueHandle const &) = delete;\n        auto operator=(UniqueHandle const &)->UniqueHandle & = delete;\n\n        // UniqueHandle constructor\n        explicit UniqueHandle(HANDLE value = C::Invalid()) :\n        m_val{ value }\n        {\n        }\n\n        // Move assignment operator\n        UniqueHandle(UniqueHandle && other) :\n        m_val{ other.Release() }\n        {\n        }\n\n        // Move assignment operator\n        auto operator=(UniqueHandle && other) -> UniqueHandle &\n        {\n          if (this != &other)\n          {\n            Reset(other.Release());\n          }\n\n          return *this;\n        }\n\n        // Destructor of UniqueHandle class\n        ~UniqueHandle()\n        {\n          Close();\n        }\n\n        // bool operator for equality\n        explicit operator bool() const \n        {\n          return m_val != C::Invalid();\n        }\n\n        // Method for retrieving the HANDLE value\n        HANDLE Get() const\n        {\n          return m_val;\n        }\n\n       // Method for releasing the HANDLE value\n       HANDLE Release()\n       {\n         auto value = m_val;\n         m_val = C::Invalid();\n         return value;\n       }\n\n       // Method for reseting the HANDLE\n       bool Reset(HANDLE value = C::Invalid())\n       {\n        if (m_val != value)\n        {\n           Close();\n           m_val = value;\n        }\n\n         return static_cast<bool>(*this);\n       }\n    };\n\n```\n\n正如我们所看到的，我们有一个完整的`UniqueHandle`类的实现，它可以被实例化，并将自动从其析构函数中关闭句柄。要使用`NullHandle`对象，我们将使用以下代码:\n\n```cpp\n    using NullHandle = UniqueHandle<NullHandleCharacteristics>;\n\n```\n\n`NullHandleCharacteristics`结构的实现如下:\n\n```cpp\n    struct NullHandleCharacteristics\n    {\n      // Returning nullptr when the HANDLE is invalid\n      static HANDLE Invalid()\n      {\n         return nullptr;\n      }\n\n      // Exit the HANDLE by closing it\n      static void Exit(HANDLE val)\n      {\n         CloseHandle(val);\n      }\n    };\n\n```\n\n现在，让我们重构前面的`threadhandle.cpp`代码。我们将`HANDLE`替换为`NullHandle`，如下:\n\n```cpp\n    auto hnd = NullHandle\n    {\n      CreateThread(\n        nullptr,\n        0,\n        threadProc,\n        nullptr,\n        0,\n        nullptr)\n    };\n\n```\n\n然后，我们将创建一个名为`WaitOneThread()`的新函数来调用线程本身，并等待它完成。实施应如下:\n\n```cpp\n    auto WaitOneThread(\n      HANDLE const h,\n      DWORD const ms = INFINITE) -> bool\n      {\n        auto const r = WaitForSingleObject(\n        h,\n        ms);\n\n        // Inform that thread is not idle\n        if (r == WAIT_OBJECT_0)\n          return true;\n\n        // Inform that thread is not idle\n        if (r == WAIT_TIMEOUT)\n          return false;\n\n        throw WinException();\n      }\n\n```\n\n通过使用`WaitOneThread()`功能，我们可以知道线程是否已经运行。`WinException`结构可以如下实现:\n\n```cpp\n    struct WinException\n    {\n      unsigned long error;\n\n      explicit WinException(\n        unsigned long value = GetLastError()) :\n        error{ value }\n       {\n       }\n    };\n\n```\n\n现在，我们可以在初始化`hnd` HANDLE 后，将以下代码添加到`main()`函数中:\n\n```cpp\n    if (hnd)\n    {\n      if (WaitOneThread(hnd.Get(), 0))\n        cout << \"Before running thread\" << endl;\n\n      WaitOneThread(hnd.Get());\n\n      if (WaitOneThread(hnd.Get(), 0))\n        cout << \"After running thread\" << endl;\n\n      unsigned long exitCode;\n      GetExitCodeThread(hnd.Get(), &exitCode);\n\n      cout << \"The result = \" << exitCode << endl;\n    }\n\n```\n\n从前面的代码中我们可以看到，我们调用`WaitOneThread()`函数，并将`0`作为`ms`参数传递，以了解`WaitForSingleObject()`函数调用的状态。我们可以将`INFINITE`值传递给它来调用线程，并等待它完成。以下是从`threadhandle.cpp`代码重构并使用了`UniqueHandle`类的`threaduniquehandle.cpp`代码:\n\n```cpp\n    /* threaduniquehandle.cpp */\n    #include <iostream>\n    #include <windows.h>\n    #include \"../uniquehandle_h/uniquehandle.h\"\n\n    using namespace std;\n\n    unsigned long threadProc(void*)\n    {\n      cout << \"threadProc() is run.\" << endl;\n      return 100;\n    }\n\n    struct WinException\n    {\n      unsigned long error;\n      explicit WinException(\n        unsigned long value = GetLastError()) :\n        error{ value }\n        {\n        }\n    };\n\n    auto WaitOneThread(\n      HANDLE const h,\n      DWORD const ms = INFINITE) -> bool\n      {\n        auto const r = WaitForSingleObject(\n        h,\n        ms);\n\n       // Inform that thread is not idle\n       if (r == WAIT_OBJECT_0)\n         return true;\n\n       // Inform that thread is not idle\n       if (r == WAIT_TIMEOUT)\n         return false;\n\n       throw WinException();\n      }\n\n    auto main() -> int\n    {\n      cout << \"[threaduniquehandle.cpp]\" << endl;\n\n      auto hnd = NullHandle\n      {\n        CreateThread(\n            nullptr,\n            0,\n            threadProc,\n            nullptr,\n            0,\n            nullptr)\n      };\n\n      if (hnd)\n      {\n        if (WaitOneThread(hnd.Get(), 0))\n          cout << \"Before running thread\" << endl;\n\n        WaitOneThread(hnd.Get());\n\n        if (WaitOneThread(hnd.Get(), 0))\n          cout << \"After running thread\" << endl;\n\n        unsigned long exitCode;\n        GetExitCodeThread(hnd.Get(), &exitCode);\n\n        cout << \"The result = \" << exitCode << endl;\n      }\n\n     return 0;\n    }\n\n```\n\n下面的截图是我们应该在控制台屏幕上看到的输出:\n\n![](img/70059c6e-2e41-4913-86c8-d05ada792004.png)\n\n从前面的截图可以看出，上面没有`Before running thread`线。因为我们每次不调用线程都会得到`WAIT_TIMEOUT`输出。尽管如此，我们还是成功地执行了`threadProc()`函数中的代码。\n\n# 触发事件\n\n在 Windows 下玩转线程后，我们来试试另一种并发类型——`Event`。这是一个可以由系统触发的动作。为了进一步了解它，让我们看看下面的代码片段，其中我们创建了一个名为`Event`的新类，它也实现了`UniqueHandle`:\n\n```cpp\n    class Event\n    {\n      private:\n        NullHandle hnd;\n\n      public:\n        Event(Event const &) = delete;\n        auto operator=(Event const &)->Event & = delete;\n        ~Event() = default;\n\n        explicit Event(bool manual) :\n         hnd\n         {\n           CreateEvent(nullptr,\n            manual, false, nullptr)\n         }\n         {\n           if (!hnd)\n            throw WinException();\n         }\n\n        explicit Event(EventType evType) :\n         hnd\n         {\n           CreateEvent(\n            nullptr,\n            static_cast<BOOL>(evType),\n            false,\n            nullptr)\n         }\n         {\n           if (!hnd)\n            throw WinException();\n         }\n\n         Event(Event && other) throw() :\n           hnd\n           {\n             other.hnd.Release()\n           }\n           {\n           }\n\n         auto operator=(Event && other) throw()->Event &\n         {\n           hnd = move(other.hnd);\n         }\n\n         void Set()\n         {\n           cout << \"The event is set\" << endl;\n           SetEvent(hnd.Get());\n         }\n\n         void Clear()\n         {\n           cout << \"The event is cleared\" << endl;\n           ResetEvent(hnd.Get());\n         }\n\n         auto Wait(\n           DWORD const ms = INFINITE) -> bool\n           {\n             auto const result = WaitForSingleObject(\n             hnd.Get(), ms);\n\n            return result == WAIT_OBJECT_0;\n           }\n     };\n\n```\n\n正如我们在前面的`Event`类实现中看到的，我们有`Set()`、`Clear()`和`Wait()`方法分别设置事件、清除事件和等待事件完成。我们有两种事件类型，即自动重置和手动重置，声明如下:\n\n```cpp\n    enum class EventType\n    {\n      AutoReset,\n      ManualReset\n    };\n\n```\n\n现在，我们将在`main()`功能中创建内容。我们将首先实例化`Event`类，然后检查事件信号。如果没有信号，我们将设置事件。相反，我们将清除事件。该代码将是以下`event.cpp`代码:\n\n```cpp\n    /* event.cpp */\n    #include <iostream>\n    #include <windows.h>\n    #include \"../uniquehandle_h/uniquehandle.h\"\n\n    using namespace std;\n\n    struct WinException\n    {\n      unsigned long error;\n\n      explicit WinException(\n        unsigned long value = GetLastError()) :\n        error{ value }\n        {\n        }\n    };\n\n    enum class EventType\n    {\n      AutoReset,\n      ManualReset\n    };\n\n    class Event\n    {\n      private:\n        NullHandle hnd;\n\n      public:\n        Event(Event const &) = delete;\n        auto operator=(Event const &)->Event & = delete;\n        ~Event() = default;\n\n        explicit Event(bool manual) :\n         hnd\n         {\n           CreateEvent(nullptr,\n           manual, false, nullptr)\n         }\n         {\n           if (!hnd)\n            throw WinException();\n         }\n\n         explicit Event(EventType evType) :\n          hnd\n          {\n            CreateEvent(\n            nullptr,\n            static_cast<BOOL>(evType),\n            false,\n            nullptr)\n          }\n          {\n            if (!hnd)\n             throw WinException();\n          }\n\n          Event(Event && other) throw() :\n            hnd\n            {\n              other.hnd.Release()\n            }\n            {\n            }\n\n          auto operator=(Event && other) throw() -> Event &\n          {\n              hnd = move(other.hnd);\n          }\n\n          void Set()\n          {\n              cout << \"The event is set\" << endl;\n              SetEvent(hnd.Get());\n          }\n\n          void Clear()\n          {\n               cout << \"The event is cleared\" << endl;\n               ResetEvent(hnd.Get());\n          }\n\n          auto Wait(\n            DWORD const ms = INFINITE) -> bool\n              {\n                auto const result = WaitForSingleObject(\n                  hnd.Get(), ms);\n\n                return result == WAIT_OBJECT_0;\n             }\n          };\n\n          void CheckEventSignaling( bool b)\n          {\n            if (b)\n            {\n              cout << \"The event is signaled\" << endl;\n            }\n            else\n            {\n             cout << \"The event is not signaled\" << endl;\n            }\n         }\n\n         auto main() -> int\n         {\n           cout << \"[event.cpp]\" << endl;\n\n           auto ev = Event{\n             EventType::ManualReset };\n\n             CheckEventSignaling(ev.Wait(0));\n\n             ev.Set();\n\n             CheckEventSignaling(ev.Wait(0));\n\n             ev.Clear();\n\n             CheckEventSignaling(ev.Wait(0));\n\n             return 0;\n          }\n\n```\n\n正如我们在前面的代码中看到的，下面是代码的作用:\n\n1.  它在`main()`函数中创建`Event`类的实例，并手动重置事件。\n2.  它调用`CheckEventSignaling()`函数，通过将`Wait()`函数传递给`CheckEventSignaling()`函数来找出事件的状态，该函数又调用`WaitForSingleObject()`函数。\n3.  它调用`Set()`和`Reset()`函数。\n4.  现在运行前面的`event.cpp`代码。您将在控制台上看到以下输出:\n\n![](img/72bfd423-0b5b-4915-9711-490b1b27b46f.png)\n\n如果我们看一下前面的截图，首先`Event`类的初始化是没有信号的。然后我们设置事件，现在它作为状态信号从`CheckEventSignaling()`方法发出。这里，我们可以说，我们可以通过调用`WaitForSingleObject()`函数来检查发出信号的事件的状态。\n\n# 从线程调用事件\n\n现在，让我们用线程调用`Event`类。然而，在此之前，我们必须能够包装多个线程，将它们调用在一起，并等待它们的进程完成。以下代码块是将打包线程的`Wrap()`函数:\n\n```cpp\n    void Wrap(HANDLE *)\n    {\n    }\n\n    template <typename T, typename... Args>\n    void Wrap(\n      HANDLE * left,\n      T const & right,\n      Args const & ... args)\n      {\n        *left = right.Get();\n        Wrap(++ left, args...);\n      }\n\n```\n\n当我们连接所有线程时，我们将调用前面的`Wrap()`函数。因此，我们需要另一个名为`WaitAllThreads()`的函数，正如我们在下面这段代码中看到的:\n\n```cpp\n    template <typename... Args>\n    void WaitAllThreads(Args const & ... args)\n    {\n      HANDLE handles[sizeof...(Args)];\n\n      Wrap(handles, args...);\n\n      WaitForMultipleObjects(\n        sizeof...(Args),\n        handles,\n        true,\n        INFINITE);\n    }\n\n```\n\n现在，我们可以使用下面的`eventthread.cpp`代码创建运行两个线程的完整代码:\n\n```cpp\n    /* eventthread.cpp */\n    #include <iostream>\n    #include <windows.h>\n    #include \"../uniquehandle_h/uniquehandle.h\"\n\n    using namespace std;\n\n    void Wrap(HANDLE *)\n    {\n    }\n\n    template <typename T, typename... Args>\n    void Wrap(\n      HANDLE * left,\n      T const & right,\n      Args const & ... args)\n      {\n        *left = right.Get();\n        Wrap(++ left, args...);\n      }\n\n    template <typename... Args>\n    void WaitAllThreads(Args const & ... args)\n    {\n      HANDLE handles[sizeof...(Args)];\n\n      Wrap(handles, args...);\n\n      WaitForMultipleObjects(\n        sizeof...(Args),\n        handles,\n        true,\n        INFINITE);\n    }\n\n    auto threadProc(void*) -> unsigned long\n    {\n      cout << \"Thread ID: \";\n      cout << GetCurrentThreadId() << endl;\n      return 120;\n    }\n\n    auto main() -> int\n    {\n      cout << \"[eventthread.cpp]\" << endl;\n\n      auto thread1 = NullHandle\n      {\n        CreateThread(\n          nullptr,\n          0,\n          threadProc,\n          nullptr,\n          CREATE_SUSPENDED,\n          nullptr)\n      };\n\n      auto thread2 = NullHandle\n      {\n        CreateThread(\n          nullptr,\n          0,\n          threadProc,\n          nullptr,\n          CREATE_SUSPENDED,\n          nullptr)\n     };\n\n ResumeThread(thread1.Get());\n ResumeThread(thread2.Get());\n\n     WaitAllThreads(thread1, thread2);\n\n     return 0;\n    }\n\n```\n\n此外，如果我们运行前面的`eventthread.cpp`代码，我们将在控制台屏幕上看到以下输出:\n\n![](img/0a60a3d6-c786-4791-b43d-797e4d01e1e6.png)\n\n我们已经成功触发了一个`Event`，因此它可以被设置为已发出信号，并且可以在`event.cpp`代码中被清除为未发出信号。我们还成功地包装了一个以上的线程，然后在`eventthread.cpp`代码中一起调用它们。现在，让我们连接这两个代码，这样我们就可以从线程中访问事件。代码应该如下`eventthread2.cpp`代码:\n\n```cpp\n    /* eventthread2.cpp */\n    #include <iostream>\n    #include <windows.h>\n    #include \"../uniquehandle_h/uniquehandle.h\"\n\n    using namespace std;\n\n    struct WinException\n    {\n      unsigned long error;\n\n      explicit WinException(\n        unsigned long value = GetLastError()) :\n        error{ value }\n        {\n        }\n    };\n\n    enum class EventType\n    {\n      AutoReset,\n      ManualReset\n     };\n\n    class Event\n    {\n      private:\n        NullHandle hnd;\n\n      public:\n        Event(Event const &) = delete;\n        auto operator=(Event const &)->Event & = delete;\n        ~Event() = default;\n\n        explicit Event(bool manual) :\n          hnd\n          {\n            CreateEvent(nullptr,\n            manual, false, nullptr)\n          }\n          {\n            if (!hnd)\n             throw WinException();\n          }\n\n        explicit Event(EventType evType) :\n          hnd\n          {\n            CreateEvent(\n              nullptr,\n              static_cast<BOOL>(evType),\n              false,\n              nullptr)\n           }\n           {\n             if (!hnd)\n              throw WinException();\n           }\n\n        Event(Event && other) throw() :\n          hnd\n          {\n            other.hnd.Release()\n          }\n          {\n          }\n\n        auto operator=(Event && other) throw() -> Event &\n        {\n          hnd = move(other.hnd);\n        }\n\n        void Set()\n        {\n          cout << \"The event is set\" << endl;\n          SetEvent(hnd.Get());\n        }\n\n        void Clear()\n        {\n          cout << \"The event is cleared\" << endl;\n          ResetEvent(hnd.Get());\n        }\n\n        auto Wait( DWORD const ms = INFINITE) -> bool\n        {\n           auto const result = WaitForSingleObject(\n            hnd.Get(), ms);\n\n           return result == WAIT_OBJECT_0;\n        }\n     };\n\n        void Wrap(HANDLE *)\n        {\n        }\n\n        template <typename T, typename... Args>\n        void Wrap(\n        HANDLE * left,\n        T const & right,\n        Args const & ... args)\n        {\n          *left = right.Get();\n           Wrap(++ left, args...);\n        }\n\n        template <typename... Args>\n        void WaitAllThreads(Args const & ... args)\n        {\n        HANDLE handles[sizeof...(Args)];\n\n        Wrap(handles, args...);\n\n        WaitForMultipleObjects(\n          sizeof...(Args),\n          handles,\n          true,\n          INFINITE);\n        }\n\n        static auto ev = Event{\n        EventType::ManualReset };\n\n        auto threadProc(void*) -> unsigned long\n        {\n          cout << \"Thread ID: \";\n          cout << GetCurrentThreadId() << endl;\n\n          ev.Wait();\n\n          cout << \"Run Thread ID: \";\n          cout << GetCurrentThreadId() << endl;\n\n          return 120;\n        }\n\n        auto main() -> int\n        {\n          cout << \"[eventthread2.cpp]\" << endl;\n\n          auto thread1 = NullHandle\n          {\n            CreateThread(\n              nullptr,\n              0,\n              threadProc,\n              nullptr,\n              0,\n              nullptr)\n          };\n\n          auto thread2 = NullHandle\n          {\n            CreateThread(\n              nullptr,\n              0,\n              threadProc,\n              nullptr,\n              0,\n              nullptr)\n         };\n\n Sleep(100);\n ev.Set();\n Sleep(100);\n\n         WaitAllThreads(thread1, thread2);\n\n         return 0;\n    }\n\n```\n\n在前面的`eventthread2.cpp`代码中，我们尝试使用线程触发事件。我们首先初始化两个`NullHandle`对象线程。然后，我们设置事件并调用`Sleep()`功能使事件激活。`WaitAllThreads()`函数然后调用`threadProc()`函数并运行每个线程。这将通过调用`ev.Wait()`功能触发事件。线程将会运行。下面的截图是我们将在控制台屏幕上看到的输出:\n\n![](img/94bc604a-76c4-49b3-9325-383479e5379f.png)\n\n前面的代码是我们手动设置来重置事件的事件。这意味着我们必须在清除事件时声明。现在，我们将`AutoReset`传递给事件实例。我们也会稍微修改一下`threadProc()`功能。下面这段代码是我们正在谈论的`eventthread3.cpp`:\n\n```cpp\n    /* eventthread3.cpp */\n    #include <iostream>\n    #include <windows.h>\n    #include \"../uniquehandle_h/uniquehandle.h\"\n\n    using namespace std;\n\n    struct WinException\n    {\n      unsigned long error;\n\n      explicit WinException(\n        unsigned long value = GetLastError()) :\n        error{ value }\n        {\n        }\n    };\n\n    enum class EventType\n    {\n      AutoReset,\n      ManualReset\n    };\n\n    class Event\n    {\n       private:\n         NullHandle hnd;\n\n       public:\n         Event(Event const &) = delete;\n         auto operator=(Event const &)->Event & = delete;\n         ~Event() = default;\n\n         explicit Event(bool manual) :\n           hnd\n           {\n             CreateEvent(nullptr,\n             manual, false, nullptr)\n           }\n           {\n             if (!hnd)\n              throw WinException();\n           }\n\n          explicit Event(EventType evType) :\n             hnd\n             {\n               CreateEvent(\n                 nullptr,\n                 static_cast<BOOL>(evType),\n                 false,\n                 nullptr)\n             }\n             {\n               if (!hnd)\n                throw WinException();\n             }\n\n         Event(Event && other) throw() :\n           hnd\n           {\n             other.hnd.Release()\n           }\n           {\n           }\n\n         auto operator=(Event && other) throw() -> Event &\n           {\n              hnd = move(other.hnd);\n           }\n\n          void Set()\n          {\n             cout << \"The event is set\" << endl;\n             SetEvent(hnd.Get());\n          }\n\n          void Clear()\n          {\n              cout << \"The event is cleared\" << endl;\n              ResetEvent(hnd.Get());\n          }\n\n          auto Wait(\n            DWORD const ms = INFINITE) -> bool\n            {\n              auto const result = WaitForSingleObject(\n                hnd.Get(), ms);\n\n             return result == WAIT_OBJECT_0;\n            }\n       };\n\n         void Wrap(HANDLE *)\n         {\n         }\n\n         template <typename T, typename... Args>\n         void Wrap(\n           HANDLE * left,\n           T const & right,\n           Args const & ... args)\n           {\n             *left = right.Get();\n             Wrap(++ left, args...);\n           }\n\n           template <typename... Args>\n           void WaitAllThreads(Args const & ... args)\n           {\n              HANDLE handles[sizeof...(Args)];\n\n              Wrap(handles, args...);\n\n              WaitForMultipleObjects(\n                sizeof...(Args),\n                handles,\n                true,\n                INFINITE);\n           }\n\n static auto ev = Event{\n EventType::AutoReset };\n\n           auto threadProc(void*) -> unsigned long\n           {\n             cout << \"Thread ID: \";\n             cout << GetCurrentThreadId() << endl;\n\n             ev.Wait();\n\n             cout << \"Run Thread ID: \";\n             cout << GetCurrentThreadId() << endl;\n\n             Sleep(1000);\n ev.Set();\n\n             return 120;\n           }\n\n           auto main() -> int\n           {\n             cout << \"[eventthread3.cpp]\" << endl;\n\n             auto thread1 = NullHandle\n             {\n               CreateThread(\n                 nullptr,\n                 0,\n                 threadProc,\n                 nullptr,\n                 0,\n                 nullptr)\n             };\n\n             auto thread2 = NullHandle\n             {\n                CreateThread(\n                  nullptr,\n                  0,\n                  threadProc,\n                  nullptr,\n                  0,\n                  nullptr)\n             };\n\n             Sleep(100);\n             ev.Set();\n             Sleep(100);\n\n             WaitAllThreads(thread1, thread2);\n\n             return 0;\n       }\n\n```\n\n正如我们在前面的代码中看到的，我们将事件的`Set()`方法从`main()`功能移动到`threadProc()`功能。现在，每次调用`threadProc()`功能，都会自动设置事件。下面的截图是我们应该在控制台屏幕上看到的输出:\n\n![](img/c3e50f9d-5157-4271-a317-35d79fff8152.png)\n\n# 摘要\n\n在本章中，我们已经学习了 C++ 并发的概念。我们现在可以处理单线程和多线程。我们还可以同步多线程，这样它就可以平稳运行；因此，我们可以避免同步问题和死锁。最后，我们可以使用 Windows 中的句柄资源来创建一个线程，并使用该事件触发事件。\n\n在下一章中，我们将应用前几章中所学的知识，以功能性的方式生成一个应用。它还将解释如何测试使用 C++ 语言构建的应用。"
  },
  {
    "path": "docs/learn-cpp-func-prog/8.md",
    "content": "# 八、使用函数方法创建和调试应用\n\n在前几章中，我们讨论了开发函数式编程的一些基本技术，包括一个一级函数、一个纯函数和一个不可变对象。在本章中，我们将使用在前面章节中学习的所有技术，以功能性的方式生成应用。它还将解释如何调试使用 C++ 语言构建的应用。\n\n在本章中，我们将涵盖以下主题:\n\n*   准备一个命令代码作为要转换成功能代码的基础代码\n*   对基础代码实现纯函数\n*   将模板元编程实现到基础代码\n*   使用 Lambda 表达式对基础代码实现过滤技术\n*   对基础代码实现递归技术\n*   对基础代码实现记忆技术\n*   调试代码来解决，如果我们得到一个意想不到的结果\n\n# 准备命令式课程\n\n我们现在将开发函数类，这样我们就可以将它消费到我们的函数程序中。在此之前，让我们准备一个新的命令类`Customer`。该类将有一个名为`id`的`int`属性作为唯一的客户标识号。它还有四个字符串属性来存储我们客户的信息- `name`、`address`、`phoneNumber`和`email`。该类还有一个标志- `isActive` -指示我们的客户是否活跃。如果客户与我们签订了合同，他们就被视为活跃客户。另一个属性是`registeredCustomers`，保存我们所有的注册客户，不考虑活跃客户。我们将使`registeredCustomers`成员成为`static`，这样我们就可以从类外填充它，并且可以保留`Customer`类的列表。\n\n除了这些属性，我们的类还将有四个方法来访问我们的属性列表。它们将是以下方法:\n\n*   `GetActiveCustomerNames()`:可以用来获取活跃客户名称列表\n*   `GetActiveCustomerAddresses()`:可以用来获取活动客户地址列表\n*   `GetActiveCustomerPhoneNumbers()`:可以用来获取活跃客户电话号码列表\n*   `GetActiveCustomerEmails()`:可以用来获取活跃客户邮件列表\n\n现在，让我们看看下面的`Customer.h`代码，我们可以在`Step01`文件夹中找到它来适应我们前面的场景:\n\n```cpp\n    /* Customer.h - Step01 */\n    #ifndef __CUSTOMER_H__\n    #define __CUSTOMER_H__\n\n    #include <string>\n    #include <vector>\n\n    class Customer\n    {\n      public:\n        static std::vector<Customer> registeredCustomers;\n        int id = 0;\n        std::string name;\n        std::string address;\n        std::string phoneNumber;\n        std::string email;\n        bool isActive = true;\n\n        std::vector<std::string> GetActiveCustomerNames();\n        std::vector<std::string> GetActiveCustomerAddresses();\n        std::vector<std::string> GetActiveCustomerPhoneNumbers();\n        std::vector<std::string> GetActiveCustomerEmails();\n    };\n    #endif // __CUSTOMER_H__\n\n```\n\n从前面的代码中，我们有四个尚未定义的公共方法。现在，让我们定义它们，如我们在下面的`Customer.cpp`代码中所见:\n\n```cpp\n    /* Customer.cpp - Step01 */\n    #include \"Customer.h\"\n\n    using namespace std;\n\n    vector<Customer> Customer::registeredCustomers;\n\n    vector<string> Customer::GetActiveCustomerNames()\n    {\n      vector<string> returnList;\n      for (auto &customer : Customer::registeredCustomers)\n      {\n        if (customer.isActive)\n        {\n            returnList.push_back(customer.name);\n        }\n      }\n       return returnList;\n    }\n\n    vector<string> Customer::GetActiveCustomerAddresses()\n    {\n      vector<string> returnList;\n      for (auto &customer : Customer::registeredCustomers)\n      {\n        if (customer.isActive)\n        {\n            returnList.push_back(customer.address);\n        }\n      }\n      return returnList;\n    }\n\n    vector<string> Customer::GetActiveCustomerPhoneNumbers()\n    {\n      vector<string> returnList;\n      for (auto &customer : Customer::registeredCustomers)\n      {\n        if (customer.isActive)\n        {\n            returnList.push_back(customer.phoneNumber);\n        }\n      }\n      return returnList;\n    }\n\n    vector<string> Customer::GetActiveCustomerEmails()\n    {\n      vector<string> returnList;\n      for (auto &customer : Customer::registeredCustomers)\n      {\n        if (customer.isActive)\n        {\n            returnList.push_back(customer.email);\n        }\n      }\n      return returnList;\n    } \n\n```\n\n从前面的代码中，我们可以看到`Customer`类中四个方法的定义。例如，在`GetActiveCustomerNames()`方法中，代码循环`registeredCustomers`向量中的每个元素来找出活跃的客户。如果找到他们，代码将提取每个客户的名字并存储到`returnList`向量中。完成方法过程后，方法会将`returnList`结果反馈给方法用户。\n\n现在，让我们使用下面的`main.cpp`代码来使用前面的类:\n\n```cpp\n    /* Main.cpp - Step01 */\n    #include <iostream>\n    #include <algorithm>\n    #include \"Customer.h\"\n\n    using namespace std;\n\n    void RegisterCustomers()\n    {\n      int i = 0;\n      bool b = false;\n\n      // Initialize name\n      vector<string> nameList =\n      {\n        \"William\",\n        \"Aiden\",\n        \"Rowan\",\n        \"Jamie\",\n        \"Quinn\",\n        \"Haiden\",\n        \"Logan\",\n        \"Emerson\",\n        \"Sherlyn\",\n        \"Molly\"\n       };\n\n       // Clear the registeredCustomers vector array\n       Customer::registeredCustomers.clear();\n\n       for (auto name : nameList)\n       {\n         // Create Customer object\n         // and fill all properties\n         Customer c;\n         c.id = i++ ;\n         c.name = name;\n         c.address = \"somewhere\";\n         c.phoneNumber = \"0123\";\n         c.email = name + \"@xyz.com\";\n         c.isActive = b;\n\n         // Flip the b value\n         b = !b;\n\n         // Send data to the registeredCustomers\n         Customer::registeredCustomers.push_back(c);\n      }\n    }\n\n    auto main() -> int\n    {\n      cout << \"[Step01]\" << endl;\n      cout << \"--------\" << endl;\n\n      // Fill the Customer::registeredCustomers\n      // with the content\n      RegisterCustomers();\n\n      // Instance Customer object\n      Customer customer;\n\n      // Get the active customer names\n      cout << \"List of active customer names:\" << endl;\n      vector<string> activeCustomerNames =\n        customer.GetActiveCustomerNames();\n      for (auto &name : activeCustomerNames)\n      {\n        cout << name << endl;\n      }\n\n      return 0;\n    }\n\n```\n\n从前面的代码中，在`main()`方法中，我们可以看到我们首先从`RegisterCustomers()`方法注册我们的客户。在那里，我们用一堆客户信息填充`Customer`类`registeredCustomers`的静态公共属性。之后，代码实例化`Customer`类，并调用名为`GetActiveCustomerNames()`的类的方法。如我们所见，该方法返回一个字符串向量，该向量包含我们将存储在`activeCustomerNames`向量中的活动客户名称列表。现在，我们可以迭代向量来提取活动客户名称的列表。下面是我们应该在控制台中看到的输出:\n\n![](img/0c74fd8b-73c6-4039-b261-75edddd462dd.png)\n\n我们可以在`RegisterCustomer()`方法中看到，十个客户中只有五个是活跃的，所以在前面的输出中不会列出所有的名字。我们可以尝试其余三种方法来获取活跃客户的信息，具体来说，他们的地址、电话号码和电子邮件地址。我们在本章中的目标是使用我们在前面章节中学习的概念，并使用功能方法制作一个应用。那么，让我们看看如何实现这一点。\n\n# 重构命令类成为一个函数类\n\n的确，前面的`Customer`类可以很好地工作，我们已经成功地调用了它的方法。然而，这个类仍然可以通过转换成一个函数类来调整。正如我们在前面的代码中看到的，我们可以实现一个纯函数、一级函数、高阶函数，并对其进行记忆，使其成为函数。因此，在本节中，我们将重构`Customer`类，使其成为一个功能类，并使用我们从前面章节中获得的知识。在接下来的部分中，我们将实现我们在上一章中讨论过的函数方法，这是一级函数。\n\n# 将函数作为参数传递\n\n正如我们在[第 2 章](2.html)、*在函数编程*中讨论的，我们可以将函数重写为一级函数，这意味着我们可以将一个函数传递给另一个函数。我们将简化我们在`Step01`代码中所有四个方法的定义，然后我们将通过将其传递给另一个名为`GetActiveCustomerByFunctionField()`的方法来调用该函数。我们还将创建一个名为`GetActiveCustomerByField()`的新方法来选择我们应该运行的正确方法。`Customer`类的定义现在类似于下面的`Customer.h`代码:\n\n```cpp\n    /* Customer.h - Step02 */\n    #ifndef __CUSTOMER_H__\n    #define __CUSTOMER_H__\n\n    #include <string>\n    #include <vector>\n    #include <functional>\n\n    class Customer\n    {\n      private:\n        std::string GetActiveCustomerNames(\n          Customer customer) const;\n        std::string GetActiveCustomerAddresses(\n          Customer customer) const;\n        std::string GetActiveCustomerPhoneNumbers(\n          Customer customer) const;\n        std::string GetActiveCustomerEmails(\n          Customer customer) const;\n\n      public:\n        static std::vector<Customer> registeredCustomers;\n        int id = 0;\n        std::string name;\n        std::string address;\n        std::string phoneNumber;\n        std::string email;\n        bool isActive = true;\n\n std::vector<std::string> GetActiveCustomerByField(\n const std::string &field);\n\n std::vector<std::string> GetActiveCustomerByFunctionField(\n std::function<std::string(const Customer&, Customer)> \n funcField);\n     };\n     #endif //#ifndef __CUSTOMER_H__\n\n```\n\n正如我们在前面的头文件中看到的，除了四个私有方法之外，我们还添加了一个名为`GetActiveCustomerByFunctionField()`的新公共方法，当我们需要其中一个属性的列表时，我们将调用该方法。现在，让我们定义在前面的头文件中创建的四种方法。代码应如下`Customer.cpp`文件:\n\n```cpp\n    /* Customer.cpp - Step02 */\n    #include <stdexcept>\n    #include \"Customer.h\"\n\n    using namespace std;\n\n    vector<Customer> Customer::registeredCustomers;\n\n    string Customer::GetActiveCustomerNames(\n      Customer customer) const\n      {\n        return customer.name;\n      }\n\n    string Customer::GetActiveCustomerAddresses(\n      Customer customer) const\n      {\n        return customer.address;\n      }\n\n    string Customer::GetActiveCustomerPhoneNumbers(\n      Customer customer) const\n      {\n        return customer.phoneNumber;\n      }\n\n    string Customer::GetActiveCustomerEmails(\n      Customer customer) const\n      {\n return customer.email;\n      }\n\n vector<string> Customer::GetActiveCustomerByFunctionField(\n function<string(const Customer&, Customer)> funcField)\n {\n vector<string> returnList;\n\n Customer c;\n\n for (auto customer : Customer::registeredCustomers)\n {\n if (customer.isActive)\n {\n returnList.push_back(\n funcField(c, customer));\n }\n }\n return returnList;\n }\n\n vector<string> Customer::GetActiveCustomerByField(\n const string &field)\n {\n function<string(const Customer&, Customer)> funct;\n\n if (field == \"name\")\n {\n funct = &Customer::GetActiveCustomerNames;\n }\n else if (field == \"address\")\n {\n funct = &Customer::GetActiveCustomerAddresses;\n }\n else if (field == \"phoneNumber\")\n {\n funct = &Customer::GetActiveCustomerPhoneNumbers;\n }\n else if (field == \"email\")\n {\n funct = &Customer::GetActiveCustomerEmails;\n }\n else\n {\n throw invalid_argument(\"Unknown field\");\n }\n\n return GetActiveCustomerByFunctionField(funct);\n }\n\n```\n\n与`Step01`代码相比，`GetActiveCustomerNames()`、`GetActiveCustomerAddresses()`、`GetActiveCustomerPhoneNumbers()`、`GetActiveCustomerEmails()`方法的实现现在更加简洁。它们只包含一行代码。然而，我们需要一种新的方法来适应这个过程，以获得类的私有属性列表，这就是`GetActiveCustomerByField()`方法。方法被传递给函数，使其成为一级函数，正如我们在前面的代码中看到的那样。在这个`Step02`文件夹中，`main.cpp`代码应该如下:\n\n```cpp\n    /* Main.cpp - Step02 */\n    #include <iostream>\n    #include \"Customer.h\"\n\n    using namespace std;\n\n    void RegisterCustomers()\n    {\n      int i = 0;\n      bool b = false;\n\n      // Initialize name\n      vector<string> nameList =\n      {\n        \"William\",\n        \"Aiden\",\n        \"Rowan\",\n        \"Jamie\",\n        \"Quinn\",\n        \"Haiden\",\n        \"Logan\",\n        \"Emerson\",\n        \"Sherlyn\",\n        \"Molly\"\n       };\n\n      // Clear the registeredCustomers vector array\n      Customer::registeredCustomers.clear();\n      for (auto name : nameList)\n      {\n        // Create Customer object\n        // and fill all properties\n        Customer c;\n        c.id = i++ ;\n        c.name = name;\n        c.address = \"somewhere\";\n        c.phoneNumber = \"0123\";\n        c.email = name + \"@xyz.com\";\n        c.isActive = b;\n\n        // Flip the b value\n        b = !b;\n\n        // Send data to the registeredCustomers\n        Customer::registeredCustomers.push_back(c);\n       }\n    }\n\n    auto main() -> int\n    {\n      cout << \"[Step02]\" << endl;\n      cout << \"--------\" << endl;\n\n      // Fill the Customer::registeredCustomers\n      // with the content\n      RegisterCustomers();\n\n      // Instance Customer object\n      Customer customer;\n\n      // Get the active customer names\n      cout << \"List of active customer names:\" << endl;\n      vector<string> activeCustomerNames =\n customer.GetActiveCustomerByField(\"name\");\n      for (auto &name : activeCustomerNames)\n      {\n        cout << name << endl;\n      }\n\n      return 0;\n    }\n\n```\n\n正如我们在前面的`main.cpp`代码中所看到的，我们现在将调用`GetActiveCustomerByField()`方法而不是`GetActiveCustomerNames()`，就像我们在`Step01`中所做的那样。我们只需要将字符串数据类型中的字段名传递给`GetActiveCustomerNames()`方法，它将调用适当的方法来检索属性值。例如，我们将检索`name`属性值，因为我们在`GetActiveCustomerByField()`方法中通过了`name`。而且，如果我们运行前面的`Step02`代码，应该会看到下面的截图，和我们在`Step01`代码中看到的完全一样:\n\n![](img/793236ef-020b-465c-b392-8edceb8b9408.png)\n\n虽然我们已经让代码正常运行，但是如果我们想要向类中添加更多的字段或属性，然后需要收集新字段的列表，我们将会面临一个问题。通过使用前面的代码，我们必须在`GetActiveCustomerByFunctionField()`方法中添加一个新的`else`部分。接下来，我们会找到应对的办法。\n\n# 添加基类\n\n如果我们想在类中添加更多的字段，并想在每次添加新字段时轻松访问它的列表，我们必须创建一个从包含虚拟函数的基类派生的新类。通过这样做，我们可以派生基类虚拟方法，并为其实现正确的代码。我们还将在这里获得模板元编程的能力，因为我们将把基类设计为模板。基类的声明如下:\n\n```cpp\n    template<typename T, typename U>\n    class BaseClass\n    {\n      public:\n        virtual U InvokeFunction(\n          const std::shared_ptr<T>&) = 0;\n    };\n\n```\n\n现在，我们可以为类中的四个方法声明从基类派生的四个新类。类的声明应该如下:\n\n```cpp\n    class CustomerName :\n      public BaseClass<Customer, std::string>\n      {\n        public:\n          virtual std::string InvokeFunction(\n            const std::shared_ptr<Customer> &customer)\n          {\n             return customer->name;\n          }\n      };\n\n    class CustomerAddress :\n      public BaseClass<Customer, std::string>\n      {\n        public:\n          virtual std::string InvokeFunction(\n            const std::shared_ptr<Customer> &customer)\n            {\n              return customer->address;\n            }\n      };\n\n    class CustomerPhoneNumber :\n      public BaseClass<Customer, std::string>\n      {\n         public:\n           virtual std::string InvokeFunction(\n             const std::shared_ptr<Customer> &customer)\n             {\n               return customer->phoneNumber;\n             }\n      };\n\n    class CustomerEmail :\n      public BaseClass<Customer, std::string>\n      {\n        public:\n          virtual std::string InvokeFunction(\n            const std::shared_ptr<Customer> &customer)\n            {\n              return customer->email;\n            }\n    };\n\n```\n\n我们还需要修改`GetActiveCustomerByFunctionField()`方法的参数类型，所以方法的签名应该如下:\n\n```cpp\n    template<typename T>\n    static std::vector<T> GetActiveCustomerByFunctionField(\n      const std::shared_ptr<BaseClass<Customer, T>>\n        &classField);\n\n```\n\n此外，实现上述代码的这个`Step03`代码的完整头文件应该如下:\n\n```cpp\n    /* Customer.h - Step03 */\n    #ifndef __CUSTOMER_H__\n    #define __CUSTOMER_H__\n\n    #include <string>\n    #include <vector>\n    #include <memory>\n\n    class Customer\n    {\n      private:\n        template<typename T, typename U>\n        class BaseClass\n        {\n          public:\n            virtual U InvokeFunction(\n            const std::shared_ptr<T>&) = 0;\n         };\n\n        class CustomerName :\n          public BaseClass<Customer, std::string>\n          {\n            public:\n              virtual std::string InvokeFunction(\n                const std::shared_ptr<Customer> &customer)\n                {\n                  return customer->name;\n                }\n          };\n\n        class CustomerAddress :\n          public BaseClass<Customer, std::string>\n          {\n            public:\n              virtual std::string InvokeFunction(\n                const std::shared_ptr<Customer> &customer)\n                {\n                  return customer->address;\n                }\n          };\n\n        class CustomerPhoneNumber :\n          public BaseClass<Customer, std::string>\n          {\n            public:\n            virtual std::string InvokeFunction(\n              const std::shared_ptr<Customer> &customer)\n              {\n                return customer->phoneNumber;\n               }\n          };\n\n        class CustomerEmail :\n          public BaseClass<Customer, std::string>\n          {\n            public:\n              virtual std::string InvokeFunction(\n                const std::shared_ptr<Customer> &customer)\n                {\n                  return customer->email;\n                }\n           };\n\n         public:\n           static std::vector<Customer> registeredCustomers;\n           int id = 0;\n           std::string name;\n           std::string address;\n           std::string phoneNumber;\n           std::string email;\n           bool isActive = true;\n\n           static std::vector<std::string> GetActiveCustomerNames();\n           static std::vector<std::string> \n             GetActiveCustomerAddresses();\n           static std::vector<std::string> \n             GetActiveCustomerPhoneNumbers();\n           static std::vector<std::string> GetActiveCustomerEmails();\n\n           template<typename T>\n           static std::vector<T> GetActiveCustomerByFunctionField(\n             const std::shared_ptr<BaseClass<Customer, T>>\n             &classField);\n      };\n     #endif // __CUSTOMER_H__\n\n```\n\n现在，前面每个类中的每个方法都有不同的任务，并且可以通过类的名称来识别。我们还将修改`GetActiveCustomerByFunctionField()`方法实现，因为它现在传递了一个新的参数类型，即类名。通过传递一个类，现在更容易传递我们期望的位于类方法中的任务。`GetActiveCustomerByFunctionField()`方法的实施应如下:\n\n```cpp\n    template<typename T>\n    vector<T> Customer::GetActiveCustomerByFunctionField(\n      const shared_ptr<BaseClass<Customer, T>> &classField)\n      {\n        vector<T> returnList;\n        for (auto customer : Customer::registeredCustomers)\n        {\n          if (customer.isActive)\n           {\n            returnList.push_back(\n              classField->InvokeFunction(\n                make_shared<Customer>(customer)));\n           }\n         }\n         return returnList;\n       }\n\n```\n\n我们可以看到，前面的方法可以运行我们已经通过的类的方法，也就是`classField`。此外，由于我们拥有的类是从`BaseClass`类派生的，我们可以通知方法接收输入的参数`BaseClass`。\n\n现在我们可以实现头文件中已经声明的公共方法——方法`GetActiveCustomerNames()`、`GetActiveCustomerAddresses()`、`GetActiveCustomerPhoneNumbers()`和`GetActiveCustomerEmails()`。这四个方法将调用`GetActiveCustomerByFunctionField()`方法并传递`InvokeFunction()`方法的定义。代码应如下所示:\n\n```cpp\n    vector<string> Customer::GetActiveCustomerNames()\n    {\n      return Customer::GetActiveCustomerByFunctionField<string>(\n        make_shared<CustomerName>());\n    }\n\n    vector<string> Customer::GetActiveCustomerAddresses()\n    {\n      return Customer::GetActiveCustomerByFunctionField<string>(\n        make_shared<CustomerAddress>());\n    }\n\n    vector<string> Customer::GetActiveCustomerPhoneNumbers()\n    {\n      return Customer::GetActiveCustomerByFunctionField<string>(\n        make_shared<CustomerPhoneNumber>());\n    }\n\n    vector<string> Customer::GetActiveCustomerEmails()\n    {\n      return Customer::GetActiveCustomerByFunctionField<string>(\n        make_shared<CustomerEmail>());\n    }\n\n```\n\n然后，我们将有一个完整的`Customer.cpp`文件如下:\n\n```cpp\n    /* Customer.cpp - Step03 */\n    #include \"Customer.h\"\n\n    using namespace std;\n\n    vector<Customer> Customer::registeredCustomers;\n\n    vector<string> Customer::GetActiveCustomerNames()\n    {\n      return Customer::GetActiveCustomerByFunctionField<string>(\n        make_shared<CustomerName>());\n    }\n\n    vector<string> Customer::GetActiveCustomerAddresses()\n    {\n      return Customer::GetActiveCustomerByFunctionField<string>(\n        make_shared<CustomerAddress>());\n    }\n\n    vector<string> Customer::GetActiveCustomerPhoneNumbers()\n    {\n      return Customer::GetActiveCustomerByFunctionField<string>(\n        make_shared<CustomerPhoneNumber>());\n    }\n\n    vector<string> Customer::GetActiveCustomerEmails()\n    {\n      return Customer::GetActiveCustomerByFunctionField<string>(\n        make_shared<CustomerEmail>());\n    }\n\n    template<typename T>\n    vector<T> Customer::GetActiveCustomerByFunctionField(\n    const shared_ptr<BaseClass<Customer, T>> &classField)\n    {\n      vector<T> returnList;\n      for (auto &customer : Customer::registeredCustomers)\n      {\n        if (customer.isActive)\n        {\n          returnList.push_back(\n            classField->InvokeFunction(\n              make_shared<Customer>(customer)));\n         }\n       }\n       return returnList;\n    }\n\n```\n\n通过在这个`Step03`文件夹中拥有`Customer.h`和`Customer.cpp`代码，我们现在更容易获取我们在`Customer`类中拥有的属性列表。例如，如果我们想要检索活跃客户的列表，我们可以直接调用`GetActiveCustomerNames()`方法，如下面的`main.cpp`代码所示:\n\n```cpp\n    /* Main.cpp - Step03 */\n    #include <iostream>\n    #include \"Customer.h\"\n\n    using namespace std;\n\n    void RegisterCustomers()\n    {\n      int i = 0;\n      bool b = false;\n\n      // Initialize name\n      vector<string> nameList =\n      {\n        \"William\",\n        \"Aiden\",\n        \"Rowan\",\n        \"Jamie\",\n        \"Quinn\",\n        \"Haiden\",\n        \"Logan\",\n        \"Emerson\",\n        \"Sherlyn\",\n        \"Molly\"\n      };\n\n      // Clear the registeredCustomers vector array\n      Customer::registeredCustomers.clear();\n\n      for (auto name : nameList)\n      {\n        // Create Customer object\n        // and fill all properties\n        Customer c;\n        c.id = i++ ;\n        c.name = name;\n        c.address = \"somewhere\";\n        c.phoneNumber = \"0123\";\n        c.email = name + \"@xyz.com\";\n        c.isActive = b;\n\n        // Flip the b value\n        b = !b;\n\n        // Send data to the registeredCustomers\n        Customer::registeredCustomers.push_back(c);\n      }\n    }\n\n    auto main() -> int\n    {\n      cout << \"[Step03]\" << endl;\n      cout << \"--------\" << endl;\n\n      // Fill the Customer::registeredCustomers\n      // with the content\n      RegisterCustomers();\n\n      // Instance Customer object\n      Customer customer;\n\n      // Get the active customer names\n      cout << \"List of active customer names:\" << endl;\n vector<string> activeCustomerNames =\n customer.GetActiveCustomerNames();\n      for (auto &name : activeCustomerNames)\n      {\n        cout << name << endl;\n      }\n\n       return 0;\n    }\n\n```\n\n现在，让我们运行`Step03`文件夹中的程序。我们应该会在控制台上看到下面的截图:\n\n![](img/5bc3935e-3059-4f30-b414-8e04a2a1086a.png)\n\n同样，与上一步相比，我们得到了完全相同的输出。我们将在下一节让`Customer`类变得纯粹。所以，继续走！\n\n# 把班级变得纯粹\n\n正如我们在[第 2 章](2.html)、*在函数式编程中操纵函数*中所讨论的，我们必须在函数式编程中创建一个纯函数来避免副作用。如果我们回到前面的`GetActiveCustomerByFunctionField()`方法定义，它迭代一个`registeredCustomers`静态成员，它是一个全局变量。这将是一个问题，因为`GetActiveCustomerByFunctionField()`方法将提供不同的输出，尽管作为参数传递的是完全相同的输出。\n\n为了解决这个问题，我们必须废除这个全局变量。然后，我们必须修改方法定义如下:\n\n```cpp\n    template<typename T>\n    vector<T> Customer::GetActiveCustomerByFunctionField(\n      vector<Customer> customers,\n      const shared_ptr<BaseClass<Customer, T>>\n        &classField)\n        {\n          vector<T> returnList;\n          for (auto &customer : customers)\n          {\n            if (customer.isActive)\n            {\n              returnList.push_back(\n                classField->InvokeFunction(\n                make_shared<Customer>(customer)));\n            }\n          }\n          return returnList;\n        }\n\n```\n\n既然我们已经没有`registeredCustomers`属性了，我们还必须通过`GetActiveCustomerByFunctionField()`方法传递一个注册客户列表。然后，该方法将迭代我们传递的客户列表，以找到活动客户。此外，因为我们已经修改了方法签名，我们还必须修改`Customer.h`文件中的方法声明，如下所示:\n\n```cpp\n    template<typename T>\n    static std::vector<T> GetActiveCustomerByFunctionField(\n      std::vector<Customer> customers,\n      const std::shared_ptr<BaseClass<Customer, T>>\n        &classField);\n\n```\n\n我们讨论了`Customer`类中的其他方法调用`GetActiveCustomerByFunctionField()`方法。因此，我们还必须修改方法实现，如下面的代码片段所示:\n\n```cpp\n    vector<string> Customer::GetActiveCustomerNames(\n      vector<Customer> customers)\n      {\n        return Customer::GetActiveCustomerByFunctionField<string>(\n        customers,\n        make_shared<CustomerName>());\n      }\n\n    vector<string> Customer::GetActiveCustomerAddresses(\n      vector<Customer> customer)\n      {\n        return Customer::GetActiveCustomerByFunctionField<string>(\n        customer,\n        make_shared<CustomerAddress>());\n      }\n\n    vector<string> Customer::GetActiveCustomerPhoneNumbers(\n      vector<Customer> customer)\n      {\n        return Customer::GetActiveCustomerByFunctionField<string>(\n        customer,\n        make_shared<CustomerPhoneNumber>());\n      }\n\n   vector<string> Customer::GetActiveCustomerEmails(\n     vector<Customer> customer)\n     {\n        return Customer::GetActiveCustomerByFunctionField<string>(\n        customer,\n        make_shared<CustomerEmail>());\n     }\n\n```\n\n我们还需要修改`Customer.h`文件中的方法声明，如下面的代码片段所示:\n\n```cpp\n    static std::vector<std::string> GetActiveCustomerNames(\n      std::vector<Customer> customer);\n    static std::vector<std::string> GetActiveCustomerAddresses(\n      std::vector<Customer> customer);\n    static std::vector<std::string> GetActiveCustomerPhoneNumbers(\n      std::vector<Customer> customer);\n    static std::vector<std::string> GetActiveCustomerEmails(\n      std::vector<Customer> customer);\n\n```\n\n现在，`Customer.h`文件将包含以下完整的代码块:\n\n```cpp\n    /* Customer.h - Step04 */\n    #ifndef __CUSTOMER_H__\n    #define __CUSTOMER_H__\n\n    #include <string>\n    #include <vector>\n    #include <memory>\n\n    class Customer\n    {\n      private:\n        template<typename T, typename U>\n        class BaseClass\n        {\n          public:\n            virtual U InvokeFunction(\n            const std::shared_ptr<T>&) = 0;\n        };\n\n        class CustomerName :\n          public BaseClass<Customer, std::string>\n          {\n            public:\n              virtual std::string InvokeFunction(\n                const std::shared_ptr<Customer> &customer)\n                {\n                  return customer->name;\n                }\n          };\n\n       class CustomerAddress :\n         public BaseClass<Customer, std::string>\n         {\n           public:\n             virtual std::string InvokeFunction(\n               const std::shared_ptr<Customer> &customer)\n              {\n            return customer->address;\n          }\n    };\n\n      class CustomerPhoneNumber :\n        public BaseClass<Customer, std::string>\n        {\n          public:\n            virtual std::string InvokeFunction(\n              const std::shared_ptr<Customer> &customer)\n              {\n                return customer->phoneNumber;\n              }\n       };\n\n     class CustomerEmail :\n        public BaseClass<Customer, std::string>\n        {\n          public:\n            virtual std::string InvokeFunction(\n            const std::shared_ptr<Customer> &customer)\n            {\n              return customer->email;\n            }\n        };\n\n      public:\n        int id = 0;\n        std::string name;\n        std::string address;\n        std::string phoneNumber;\n        std::string email;\n        bool isActive = true;\n\n        static std::vector<std::string> GetActiveCustomerNames(\n          std::vector<Customer> customer);\n       static std::vector<std::string> GetActiveCustomerAddresses(\n          std::vector<Customer> customer);\n       static std::vector<std::string> GetActiveCustomerPhoneNumbers(\n          std::vector<Customer> customer);\n       static std::vector<std::string> GetActiveCustomerEmails(\n          std::vector<Customer> customer);\n\n       template<typename T>\n       static std::vector<T> GetActiveCustomerByFunctionField(\n        std::vector<Customer> customers,\n        const std::shared_ptr<BaseClass<Customer, T>>\n          &classField);\n    };\n    #endif // __CUSTOMER_H__\n\n```\n\n并且，`Customer.cpp`文件将如下所示:\n\n```cpp\n    /* Customer.cpp - Step04 */\n    #include \"Customer.h\"\n\n    using namespace std;\n\n    vector<string> Customer::GetActiveCustomerNames(\n      vector<Customer> customers)\n      {\n        return Customer::GetActiveCustomerByFunctionField<string>(\n        customers,\n        make_shared<CustomerName>());\n      }\n\n    vector<string> Customer::GetActiveCustomerAddresses(\n      vector<Customer> customer)\n      {\n        return Customer::GetActiveCustomerByFunctionField<string>(\n          customer,\n        make_shared<CustomerAddress>());\n       }\n\n    vector<string> Customer::GetActiveCustomerPhoneNumbers(\n      vector<Customer> customer)\n      {\n        return Customer::GetActiveCustomerByFunctionField<string>(\n         customer,\n        make_shared<CustomerPhoneNumber>());\n      }\n\n    vector<string> Customer::GetActiveCustomerEmails(\n      vector<Customer> customer)\n      {\n        return Customer::GetActiveCustomerByFunctionField<string>(\n        customer,\n        make_shared<CustomerEmail>());\n       }\n\n    template<typename T>\n    vector<T> Customer::GetActiveCustomerByFunctionField(\n      vector<Customer> customers,\n      const shared_ptr<BaseClass<Customer, T>>\n        &classField)\n        {\n          vector<T> returnList;\n          for (auto &customer : customers)\n          {\n            if (customer.isActive)\n            {\n              returnList.push_back(\n                classField->InvokeFunction(\n                make_shared<Customer>(customer)));\n             }\n           }\n           return returnList;\n         }\n\n```\n\n由于`Customer`类已经更改，不再有`registeredCustomer`变量，我们还需要修改`main.cpp`文件中的`RegisterCustomers()`方法。该方法的早期版本不返回任何内容。现在，我们将使代码返回客户列表。我们还需要修改`main()`方法，因为我们必须使用`Main.cpp`文件中的新`RegisterCustomers()`方法。该文件将包含以下代码块:\n\n```cpp\n    /* Main.cpp - Step04 */\n    #include <iostream>\n    #include \"Customer.h\"\n\n    using namespace std;\n\n vector<Customer> RegisterCustomers()\n    {\n      int i = 0;\n      bool b = false;\n\n      vector<Customer> returnValue;\n\n      // Initialize name\n      vector<string> nameList =\n      {\n        \"William\",\n        \"Aiden\",\n        \"Rowan\",\n        \"Jamie\",\n        \"Quinn\",\n        \"Haiden\",\n        \"Logan\",\n        \"Emerson\",\n        \"Sherlyn\",\n        \"Molly\"\n       };\n\n      for (auto name : nameList)\n      {\n        // Create Customer object\n        // and fill all properties\n        Customer c;\n        c.id = i++ ;\n        c.name = name;\n        c.address = \"somewhere\";\n        c.phoneNumber = \"0123\";\n        c.email = name + \"@xyz.com\";\n        c.isActive = b;\n        // Flip the b value\n        b = !b;\n        // Send data to the registeredCustomers\n        returnValue.push_back(c);\n      }\n\n      return returnValue;\n    }\n\n    auto main() -> int\n    {\n      cout << \"[Step04]\" << endl;\n      cout << \"--------\" << endl;\n\n      // Instance Customer object\n      Customer customer;\n\n      // Get the active customer names\n      cout << \"List of active customer names:\" << endl;\n      vector<string> activeCustomerNames =\n        customer.GetActiveCustomerNames(\n            RegisterCustomers());\n      for (auto name : activeCustomerNames)\n      {\n        cout << name << endl;\n       }\n\n       return 0;\n    }\n\n```\n\n正如我们在前面的`main()`方法中看到的，我们调用`GetActiveCustomerNames()`方法并传递`RegisterCustomers()`方法的结果。现在，让我们通过在`Step06`文件夹中运行程序来尝试代码。运行程序时，我们应该在控制台上获得以下输出:\n\n![](img/83af4fa6-23aa-4db0-a7a2-860c55bfc133.png)\n\n同样，我们得到了与上一步完全相同的输出，但是采用了函数式编程的新方法。接下来，我们将重构代码，使用 Lambda 表达式来简化过滤任务。\n\n# 过滤条件并实现一个 Lambda 表达式\n\n我们来关注一下`GetActiveCustomerByFunctionField()`法。在那里，我们可以找到一个`if`结构来过滤活跃的客户。正如我们在前面章节中所讨论的，我们可以使用`copy_if()`方法来过滤条件。下面的代码片段实现了`copy_if()`方法来过滤活跃客户:\n\n```cpp\n    template<typename T>\n    vector<T> Customer::GetActiveCustomerByFunctionField(\n      vector<Customer> customers,\n      const shared_ptr<BaseClass<Customer, T>>\n        &classField)\n        {\n vector<Customer> activeCustomers;\n          vector<T> returnList;\n\n copy_if(\n customers.begin(),\n customers.end(),\n back_inserter(activeCustomers),\n [](Customer customer)\n {\n if (customer.isActive)\n return true;\n else\n return false;\n });\n\n            for (auto &customer : customers)\n             {\n                if (customer.isActive)\n                {\n                  returnList.push_back(\n                  classField->InvokeFunction(\n                  make_shared<Customer>(customer)));\n                 }\n             }\n\n          return returnList;\n      }\n\n```\n\n正如我们在前面的代码片段中看到的，我们创建了一个匿名方法，如果我们传递的客户实例是活动的，该方法返回 true。此外，我们可以重构前面的`GetActiveCustomerByFunctionField()`方法，这样它将再次使用匿名方法，正如我们在下面的代码片段中看到的:\n\n```cpp\n    template<typename T>\n    vector<T> Customer::GetActiveCustomerByFunctionField(\n      vector<Customer> customers,\n      const shared_ptr<BaseClass<Customer, T>> \n        &classField)\n        {\n          vector<Customer> activeCustomers;\n          vector<T> returnList;\n\n          copy_if(\n            customers.begin(),\n            customers.end(),\n            back_inserter(activeCustomers),\n            [](Customer customer)\n            {\n              if (customer.isActive)\n                return true;\n              else\n                return false;\n             });\n\n for_each(\n activeCustomers.begin(),\n activeCustomers.end(),\n [&returnList, &classField](Customer customer)\n {\n returnList.push_back(\n classField->InvokeFunction(\n make_shared<Customer>(customer))\n );\n });\n\n         return returnList;\n    }\n\n```\n\n除了使用 Lambda 表达式实现过滤技术之外，我们还将向名为`CountActiveCustomers()`的`Customer`类添加一个方法。该方法将计算活跃客户。该方法的定义如下:\n\n```cpp\n    int Customer::CountActiveCustomers(\n      vector<Customer> customer)\n      {\n        int add = 0;\n\n        for (auto cust : customer)\n        {\n          // Adding 1 if the customer is active\n          if(cust.isActive)\n            ++ add;\n        }\n\n        return add;\n    }\n\n```\n\n现在，我们将在这个`Step05`代码块中有如下的`Customer.cpp`代码:\n\n```cpp\n    /* Customer.cpp - Step05 */\n    #include <algorithm>\n    #include \"Customer.h\"\n\n    using namespace std;\n\n    vector<string> Customer::GetActiveCustomerNames(\n      vector<Customer> customers)\n      {\n        return Customer::GetActiveCustomerByFunctionField<string>(\n        customers,\n        make_shared<CustomerName>());\n      }\n\n    vector<string> Customer::GetActiveCustomerAddresses(\n      vector<Customer> customer)\n      {\n        return Customer::GetActiveCustomerByFunctionField<string>(\n          customer,\n        make_shared<CustomerAddress>());\n      }\n\n    vector<string> Customer::GetActiveCustomerPhoneNumbers(\n      vector<Customer> customer)\n      {\n        return Customer::GetActiveCustomerByFunctionField<string>(\n          customer,\n        make_shared<CustomerPhoneNumber>());\n      }\n\n    vector<string> Customer::GetActiveCustomerEmails(\n      vector<Customer> customer)\n      {\n        return Customer::GetActiveCustomerByFunctionField<string>(\n        customer,\n        make_shared<CustomerEmail>());\n      }\n\n int Customer::CountActiveCustomers(\n vector<Customer> customer)\n {\n int add = 0;\n\n for (auto cust : customer)\n {\n // Adding 1 if the customer is active\n if(cust.isActive)\n ++ add;\n }\n\n return add;\n }\n\n    template<typename T>\n    vector<T> Customer::GetActiveCustomerByFunctionField(\n vector<Customer> customers,\n      const shared_ptr<BaseClass<Customer, T>>\n        &classField)\n        {\n vector<Customer> activeCustomers;\n          vector<T> returnList;\n\n copy_if(\n customers.begin(),\n customers.end(),\n back_inserter(activeCustomers),\n [](Customer customer)\n {\n if (customer.isActive)\n return true;\n else\n return false;\n });\n\n for_each(\n activeCustomers.begin(),\n activeCustomers.end(),\n [&returnList, &classField](Customer customer)\n {\n returnList.push_back(\n classField->InvokeFunction(\n make_shared<Customer>(customer))\n );\n });\n\n return returnList;\n     }\n\n```\n\n不要忘记修改`Customer.h`文件，因为我们已经给类添加了一个新方法。该文件应包含以下代码:\n\n```cpp\n    /* Customer.h - Step05 */\n    #ifndef __CUSTOMER_H__\n    #define __CUSTOMER_H__\n\n    #include <string>\n    #include <vector>\n    #include <memory>\n\n    class Customer\n    {\n      private:\n        template<typename T, typename U>\n        class BaseClass\n        {\n          public:\n            virtual U InvokeFunction(\n            const std::shared_ptr<T>&) = 0;\n        };\n\n        class CustomerName :\n          public BaseClass<Customer, std::string>\n          {\n            public:\n              virtual std::string InvokeFunction(\n                const std::shared_ptr<Customer> &customer)\n                {\n                  return customer->name;\n                 }\n          };\n\n        class CustomerAddress :\n          public BaseClass<Customer, std::string>\n          {\n             public:\n               virtual std::string InvokeFunction(\n                 const std::shared_ptr<Customer> &customer)\n                 {\n                   return customer->address;\n                 }\n          };\n\n        class CustomerPhoneNumber :\n          public BaseClass<Customer, std::string>\n          {\n            public:\n              virtual std::string InvokeFunction(\n                const std::shared_ptr<Customer> &customer)\n                {\n                 return customer->phoneNumber;\n                }\n           };\n\n    class CustomerEmail :\n        public BaseClass<Customer, std::string>\n    {\n    public:\n        virtual std::string InvokeFunction(\n            const std::shared_ptr<Customer> &customer)\n        {\n            return customer->email;\n        }\n    };\n\n    public:\n      int id = 0;\n      std::string name;\n      std::string address;\n      std::string phoneNumber;\n      std::string email;\n      bool isActive = true;\n\n      static std::vector<std::string> GetActiveCustomerNames(\n        std::vector<Customer> customer);\n      static std::vector<std::string> GetActiveCustomerAddresses(\n        std::vector<Customer> customer);\n      static std::vector<std::string> GetActiveCustomerPhoneNumbers(\n        std::vector<Customer> customer);\n      static std::vector<std::string> GetActiveCustomerEmails(\n        std::vector<Customer> customer);\n\n static int CountActiveCustomers(\n std::vector<Customer> customer);\n\n      template<typename T>\n      static std::vector<T> GetActiveCustomerByFunctionField(\n        std::vector<Customer> customers,\n        const std::shared_ptr<BaseClass<Customer, T>>\n            &classField);\n    };\n    #endif // __CUSTOMER_H__\n\n```\n\n现在，我们将在我们的`main()`函数中调用`CountActiveCustomers()`方法。我们将通过检查以下`Main.cpp`代码块来了解如何做到这一点:\n\n```cpp\n    /* Main.cpp - Step05 */\n    #include <iostream>\n    #include <chrono>\n    #include \"Customer.h\"\n\n    using namespace std;\n\n    vector<Customer> RegisterCustomers()\n    {\n      int i = 0;\n      bool b = false;\n\n      vector<Customer> returnValue;\n\n      // Initialize name\n      vector<string> nameList =\n      {\n        \"William\",\n        \"Aiden\",\n        \"Rowan\",\n        \"Jamie\",\n        \"Quinn\",\n        \"Haiden\",\n        \"Logan\",\n        \"Emerson\",\n        \"Sherlyn\",\n        \"Molly\"\n      };\n\n      for (auto name : nameList)\n      {\n        // Create Customer object\n        // and fill all properties\n        Customer c;\n        c.id = i++ ;\n        c.name = name;\n        c.address = \"somewhere\";\n        c.phoneNumber = \"0123\";\n        c.email = name + \"@xyz.com\";\n        c.isActive = b;\n\n        // Flip the b value\n        b = !b;\n\n        // Send data to the registeredCustomers\n        returnValue.push_back(c);\n      }\n\n     return returnValue;\n    }\n\n    auto main() -> int\n    {\n      cout << \"[Step05]\" << endl;\n      cout << \"--------\" << endl;\n\n // Recording start time for the program\n auto start = chrono::high_resolution_clock::now();\n\n      // Instance Customer object\n      Customer customer;\n\n // Counting active customers\n cout << \"Total active customers: \" << endl;\n cout << customer.CountActiveCustomers(\n RegisterCustomers());\n cout << endl << \"--------\" << endl;\n\n      // Get the active customer names\n      cout << \"List of active customer names:\" << endl;\n      vector<string> activeCustomerNames =\n        customer.GetActiveCustomerNames(\n            RegisterCustomers());\n      for (auto name : activeCustomerNames)\n      {\n        cout << name << endl;\n      }\n\n // Recording end time for the program\n auto finish = chrono::high_resolution_clock::now();\n\n // Calculating the elapsed time for the program\n chrono::duration<double, milli> elapsed = finish - start;\n\n // Displaying elapsed time for the program\n cout << \"--------\" << endl;\n cout << \"Total consuming time = \";\n cout << elapsed.count() << \" milliseconds\" << endl;\n\n      return 0;\n    }\n\n```\n\n正如我们在前面的代码中看到的，我们调用`CountActiveCustomers()`方法，并将`RegisterCustomers()`方法的输出作为参数传递。我们还添加了一个简单的秒表来计算代码需要运行程序多长时间。前面代码的输出应该如下:\n\n![](img/b6c381e2-85f3-4717-9990-031a1a9fb7f3.png)\n\n如我们所见，我们需要`0.997`毫秒来运行这一步的代码。然而，我们可以通过实现递归和记忆化来优化前面的代码，使其运行得更快，这将在下一节中讨论。\n\nIndeed, we can find out the total of the active customers by running the method of `activeCustomerNames.size()` to get the number of elements in the vector after we run the following code line:\n\n`vector<string> activeCustomerNames =\ncustomer.GetActiveCustomerNames(RegisterCustomers())`\n\nHowever, the preceding code example wants to show us how the `for` loop can be transformed into recursion, to optimize the speed of execution. We will discuss this in the upcoming\nsection.\n\n# 在客户类中实现递归和记忆技术\n\n如果我们看一下`Step05`中的`CountActiveCustomers()`方法定义，我们使用`for`循环来计算活跃客户。然而，我们可以重写方法来使用递归技术。让我们看看下面的代码，这是`CountActiveCustomers()`方法的新定义:\n\n```cpp\n    int Customer::CountActiveCustomers(\n      vector<Customer> customer)\n      {\n        if(customer.empty())\n          return 0;\n        else\n        {\n          // Adding 1 if the customer is active\n          int add = customer.front().isActive ? 1 : 0;\n\n          // Removing the first element of vector\n          // It's similar with removing head\n          // and pass the tail\n          customer.erase(customer.begin());\n\n          // Running the recursion\n          return add + CountActiveCustomers(\n            customer);\n         }\n       }\n\n```\n\n正如我们在前面的代码片段中看到的，我们对`CountActiveCustomers()`方法使用尾部递归。我们只需要每次在`customer`向量中找到活跃客户时增加`add`变量。然后，代码移除`customer`向量的第一个元素，并再次将其传递给`CountActiveCustomers()`方法。我们重复这个过程，直到`customer`向量的元素为空。\n\n此外，我们使用我们在[第 5 章](5.html)、*中讨论的`Memoization`类，使用延迟求值*来延迟执行过程，以优化我们的代码。我们将修改`Main.cpp`文件中的`main()`函数，因此`main()`函数包含以下代码片段:\n\n```cpp\n    auto main() -> int\n    {\n      cout << \"[Step06]\" << endl;\n      cout << \"--------\" << endl;\n\n      // Recording start time for the program\n      auto start = chrono::high_resolution_clock::now();\n\n      // Instance Customer object\n      Customer customer;\n\n      // Counting active customers\n      cout << \"Total active customers: \" << endl;\n      cout << customer.CountActiveCustomers(\n        RegisterCustomers());\n      cout << endl << \"--------\" << endl;\n\n // Initializing memoization instance\n Memoization<vector<string>> custMemo(\n [customer]()\n {\n return customer.GetActiveCustomerNames(\n RegisterCustomers());\n });\n\n      // Get the active customer names\n      cout << \"List of active customer names:\" << endl;\n vector<string> activeCustomerNames =\n custMemo.Fetch();\n      for (auto name : activeCustomerNames)\n      {\n        cout << name << endl;\n      }\n\n      // Recording end time for the program\n      auto finish = chrono::high_resolution_clock::now();\n\n      // Calculating the elapsed time for the program\n      chrono::duration<double, milli> elapsed = finish - start;\n\n      // Displaying elapsed time for the program\n      cout << \"--------\" << endl;\n      cout << \"Total consuming time = \";\n      cout << elapsed.count() << \" milliseconds\" << endl;\n\n      return 0;\n    }\n\n```\n\n正如我们在前面的代码片段中看到的，我们现在通过调用`Fetch()`方法从`Memoization`实例运行`GetActiveCustomerNames()`方法。如果我们运行`Step06`代码，我们应该会在控制台上看到以下输出:\n\n![](img/75cf002b-d6a6-4118-a72b-89ccb548a3d4.png)\n\n代码现在只需要`0.502`毫秒运行。相比`Step05`代码，代码执行的速度几乎快了一倍。实践证明，使用函数式方法，不仅可以获得更好的代码结构，而且可以加快优化速度。\n\n# 调试代码\n\n有时，在编码过程中，当我们运行代码时，我们会从一个或多个变量中得到意想不到的结果。可能发生在行刑过程中。为了避免陷入这种情况，我们可以通过逐步运行程序来分析我们的程序。我们可以使用 GCC 编译器中包含的调试器工具-**GDB**(**GNU 项目调试器**)。这个工具允许我们找出目标程序执行时内部发生了什么，或者它崩溃时在做什么。在这一节中，我们将应用 GDB 来减轻我们的编程任务，并找到问题的解决方案并处理它。\n\n# 启动调试工具\n\n现在，让我们准备将要分析的可执行文件。我们将使用`Step01`文件夹中的代码，因为它是一个简单的代码，我们可以很容易地从中学习。我们必须使用`-g`选项重新编译代码，并将可执行文件命名为`customer.exe`。下面是编译代码以便调试的三个命令:\n\n```cpp\ng++ -Wall -g -c Main.cpp -o Main.o\ng++ -Wall -g -c Customer.cpp -o Customer.o\ng++ Main.o Customer.o -o Customer.exe\n\n```\n\nGDB can only analyze the executable file that contains the debugging information and symbols that are important in the debugging process. We can insert the `-g` option when we compile the source so the debugging information and symbol will be added to the executable file.\n\n在控制台上键入`gdb customer`将打开调试器工具，并从`customer.exe`文件加载调试器信息和符号。然后，我们将在控制台上看到以下屏幕截图:\n\n![](img/075cb3bb-6396-46a8-aa8d-6cb3bd8b24b6.png)\n\n正如我们在前面的截图中看到的，它已经成功地从`customer.exe`文件中读取了符号。然后，在 GDB 控制台中键入`start`开始分析过程。调试器将在`main()`方法的第一行创建一个临时断点。启动 GDB 后，我们将在控制台上看到以下屏幕截图:\n\n![](img/c1fb49c2-5b72-40fa-9555-4cb2a43d7635.png)\n\n现在，程序正在调试过程中。我们可以继续这个过程来分析程序正在进行什么。在下一节中，我们可以选择是一步一步地继续，还是运行程序直到找到下一个断点。\n\nTo start the debugging process, we can either call the `run` or `start` command. The former will start our program under GDB, while the latter will behave similarly but will execute the code line by line. The difference is, if we don't have the breakpoint yet, the program will run as usual, just like it does when we call the `run` command, while the debugger will automatically set the breakpoint in the main block of code and the program will stop when it reaches that breakpoint, if we start with the `start` command.\n\n# 继续并逐步进行调试过程\n\n有三个命令可以继续上一节中的步骤。它们如下:\n\n*   `continue`:这将恢复程序的执行，直到我们的程序正常完成。如果找到断点，执行将在设置断点的行停止。\n*   `step`:这只是执行我们程序的最后一步。该步骤可能意味着一行源代码或一条机器指令。如果它发现一个函数的调用，它将进入该函数，并在该函数中再运行一步。\n*   `next`:继续到当前堆栈帧的下一行。换句话说，如果下一个命令找到了函数的调用，它将不会进入函数。\n\n因为我们还没有设置断点，所以让我们键入`next`命令，这样调试指针就指向代码的下一行。我们将多次运行`next`命令，直到代码结束(或者直到我们可以看到进程正常退出)。当我们多次应用`next`命令时，应该会看到下面的截图:\n\n![](img/99f37d0c-0b09-4a03-8fe5-dca276cb92e6.png)\n\n正如我们在前面的截图中看到的，我们可以通过一步一步地运行程序来分析我们的程序。接下来，如果我们有一个要分析的可疑对象，我们将设置断点。\n\nWe just need to press the `Enter` key to run the previous command in GDB. Pressing the *Q* key will make the debugging console exit to the window console.\n\n# 设置和删除断点\n\n让我们通过键入 *Q* 键退出调试控制台。我们需要重新开始调试，所以我们需要在窗口控制台上再次键入`gdb customer`。之后，让我们在继续该过程之前设置断点，而不是键入`start`命令。我们在 GDB 控制台分别输入`break 68`和`break Customer.cpp:15`。输出如下所示:\n\n![](img/e49ce2b0-4515-4fe4-bb5a-9b6ad9bd43dc.png)\n\n现在，我们在单独的文件中有两个断点- `Main.cpp`和`Customer.cpp`。我们现在可以通过在 GDB 控制台中键入`run`来启动调试器，如下图所示:\n\n![](img/a98825ab-5bc8-4cdb-8ee1-23f8afd0f400.png)\n\n由于调试器首先命中`GetActiveCustomerNames()`方法，所以它在我们将断点放入该方法的那一行停止，这就是`Customer.cpp`文件中的第`15`行。只需键入`continue`命令并多次按*进入*，直到碰到`Main.cpp`文件第`69`行的断点。\n\n# 打印对象值\n\n让我们通过在`Main.cpp`文件的第`68`行设置断点来重新运行调试器，然后启动调试器，直到它遇到断点。命中断点后，键入`print name`查看名称变量的值。下面的截图显示了该过程的步骤:\n\n![](img/7a03fa03-bbf0-4f84-bb7c-b253f53ac888.png)\n\n正如我们在前面的截图中看到的，`name`变量的值是`Aiden`。我们可以通过键入`continue`命令继续调试，以便调试器在`for`循环中再次命中断点，然后键入`print name`找出下一个名称值。\n\nThere are so many commands in the GDB that, will be overloaded if they are written in this book. If you need to find more commands in the GDB, refer to the following link:\n\n[https://www.gnu.org/software/gdb/documentation/](https://www.gnu.org/software/gdb/documentation/)\n\n# 摘要\n\n在这本书的最后一章中，我们通过重构命令类成功地开发了函数类，我们可以用它来创建一个更复杂的程序。我们实现了我们在前面章节中学到的东西。我们还讨论了调试技术，当我们在程序中遇到意外结果或崩溃时，这是一个有用的武器。"
  },
  {
    "path": "docs/learn-cpp-func-prog/README.md",
    "content": "# C++ 函数式编程学习手册\n\n> 原书：[Learning C++ Functional Programming](https://libgen.rs/book/index.php?md5=F9CD8E202DD0EE4D8D894152D55560F4)\n> \n> 协议：[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)\n> \n> 阶段：机翻（1）\n> \n> 自豪地采用[谷歌翻译](https://translate.google.cn/)\n> \n> 让开源界提前感受入关。\n\n* [在线阅读](https://ccpp.apachecn.org)\n* [在线阅读（Gitee）](https://apachecn.gitee.io/apachecn-c-cpp-zh/)\n* [ApacheCN 学习资源](http://docs.apachecn.org/)\n\n## 贡献指南\n\n本项目需要校对，欢迎大家提交 Pull Request。\n\n> 请您勇敢地去翻译和改进翻译。虽然我们追求卓越，但我们并不要求您做到十全十美，因此请不要担心因为翻译上犯错——在大部分情况下，我们的服务器已经记录所有的翻译，因此您不必担心会因为您的失误遭到无法挽回的破坏。（改编自维基百科）\n\n## 联系方式\n\n### 负责人\n\n* [飞龙](https://github.com/wizardforcel): 562826179\n\n### 其他\n\n*   在我们的 [apachecn/apachecn-c-cpp-zh](https://github.com/apachecn/apachecn-c-cpp-zh) github 上提 issue.\n*   发邮件到 Email: `apachecn@163.com`.\n*   在我们的 [组织学习交流群](http://www.apachecn.org/organization/348.html) 中联系群主/管理员即可.\n\n## 赞助我们\n\n![](http://data.apachecn.org/img/about/donate.jpg)\n"
  },
  {
    "path": "docs/learn-cpp-func-prog/SUMMARY.md",
    "content": "+   [C++ 函数式编程学习手册](README.md)\n+   [零、前言](0.md)\n+   [一、深入现代 C++](1.md)\n+   [二、函数式编程中的函数操作](2.md)\n+   [三、将不可变状态应用于函数](3.md)\n+   [四、使用递归算法重复方法调用](4.md)\n+   [五、使用延迟求值拖延执行过程](5.md)\n+   [六、使用元编程优化代码](6.md)\n+   [七、使用并发运行并行执行](7.md)\n+   [八、使用函数方法创建和调试应用](8.md)\n"
  },
  {
    "path": "docs/learn-cuda-prog/00.md",
    "content": "# 零、前言\n\nDon't take rest after your first victory. Because if you fail in second, more lips are waiting to say that your first victory was just luck. - A. P. J. Abdul Kalam\n\n传统上，计算需求与**中央处理器** ( **中央处理器**相关联，中央处理器已经从单核发展到现在的多核。每一代新的中央处理器都提供了更高的性能，但科学和**高性能计算** ( **高性能计算**)社区对性能的要求逐年提高，这在应用的需求和硬件/软件堆栈的能力之间造成了计算差距。与此同时，传统上用于视频图形的新架构进入了科学领域。**图形处理单元**(**GPU**)——本质上是用于加速计算机图形的并行计算处理器——在 2007 年**计算统一设备架构** ( **CUDA** )推出时就在 HPC 领域崭露头角。在通用计算中使用图形处理器时，CUDA 逐渐成为事实上的标准；也就是非图形应用。\n\nCUDA 从一开始就有很多版本，现在 CUDA 已经到了 10.x 版本，每个版本都提供了支持新硬件架构的新特性。这本书旨在帮助你学习 GPU 并行编程，并指导你在它的现代应用。在它的帮助下，您将能够发现现代图形处理器架构的 CUDA 编程方法。这本书不仅将指导您了解 GPU 特性、工具和 API，还将帮助您了解如何使用示例并行编程算法分析性能。这本书将确保你获得大量的优化经验和对具有各种库、开放加速器(OpenACC)和其他语言的 CUDA 编程平台的见解。随着您的进步，您将发现如何利用一个盒子或多个盒子中的多个图形处理器产生额外的计算能力。最后，您将探索 CUDA 如何加速深度学习算法，包括**卷积神经网络** ( **CNNs** )和**递归神经网络** ( **RNNs** )。\n\n这本书旨在成为任何新来者或新手开发者的切入点。但是到最后，您将能够为不同的领域编写优化的 CUDA 代码，包括人工智能。\n\n如果以下任何一项适用于您，这本书将是一个有用的资源:\n\n*   您不熟悉高性能计算或并行计算\n*   你有代码，并希望通过将并行计算应用于 GPU 来提高它的性能\n*   您是深度学习专家，希望利用 GPU 来提高深度学习算法(如 CNNs 和 RNNs)的性能\n*   您想学习优化代码和分析 GPU 应用性能以及发现优化策略的技巧和诀窍\n*   您想了解最新的 GPU 功能，以及高效的分布式多 GPU 编程\n\n如果你觉得自己属于其中任何一类，请加入我们的旅程。\n\n# 这本书是给谁的\n\n这本书是为那些想深入研究并行计算，成为高性能计算社区的一部分并构建现代应用的程序员编写的。假设有基本的 C 和 C++ 编程经验。对于深度学习爱好者来说，这本书涵盖了 Python InterOps、DL 库以及性能评估的实际例子。\n\n# 充分利用这本书\n\n这本书是为完全初学者和刚刚开始学习并行计算的人设计的。除了计算机体系结构的基础知识之外，它不需要任何特定的知识，并且假设有 C/C++ 编程的经验。对于深度学习爱好者来说，在[第 10 章](10.html)、*用 CUDA* 进行深度学习加速时，也提供了基于 Python 的示例代码，因此预计该章会有一些 Python 知识。\n\n这本书的代码主要是在 Linux 环境中开发和测试的。因此，熟悉 Linux 环境是有帮助的。任何最新的 Linux 风格，比如 CentOS 或者 Ubuntu，都可以。可以使用 makefile 或命令行编译代码。这本书主要使用自由软件栈，所以不需要购买任何软件许可证。将贯穿始终的两个关键软件是 CUDA 工具包和 PGI 社区版。\n\n由于本书主要涵盖了利用 CUDA 10.x 的最新 GPU 特性，为了充分利用所有的培训材料，最新的 GPU 架构(Pascal onward)将是有益的。虽然并非所有章节都需要最新的 GPU，但拥有最新的 GPU 将有助于您重现书中取得的成果。每一章在*技术要求*部分都有关于首选或必备 GPU 架构的章节。\n\n# 下载示例代码文件\n\n你可以从你在[www.packt.com](http://www.packt.com)的账户下载这本书的示例代码文件。如果您在其他地方购买了这本书，您可以访问[www.packtpub.com/support](https://www.packtpub.com/support)并注册将文件直接通过电子邮件发送给您。\n\n您可以按照以下步骤下载代码文件:\n\n1.  登录或注册[www.packt.com](http://www.packt.com)。\n2.  选择“支持”选项卡。\n3.  点击代码下载。\n4.  在搜索框中输入图书的名称，并按照屏幕指示进行操作。\n\n下载文件后，请确保使用最新版本的解压缩文件夹:\n\n*   视窗系统的 WinRAR/7-Zip\n*   zipeg/izp/un ARX for MAC\n*   适用于 Linux 的 7-Zip/PeaZip\n\n这本书的代码包也托管在 https://github.com/PacktPublishing/Learn-CUDA-Programming 的 GitHub 上。如果代码有更新，它将在现有的 GitHub 存储库中更新。\n\n我们还有来自丰富的图书和视频目录的其他代码包，可在**[【https://github.com/PacktPublishing/】](https://github.com/PacktPublishing/)**获得。看看他们！\n\n# 下载彩色图像\n\n我们还提供了一个 PDF 文件，其中包含本书中使用的截图/图表的彩色图像。可以在这里下载:[https://static . packt-cdn . com/downloads/9781788996242 _ color images . pdf](_ColorImages.pdf)。\n\n# 使用的约定\n\n本书通篇使用了许多文本约定。\n\n`CodeInText`:表示文本中的码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟网址、用户输入和推特句柄。这里有一个例子:“注意`cudaMemcpy`有一个异步替代方案。”\n\n代码块设置如下:\n\n```cpp\n#include<stdio.h>\n#include<stdlib.h>\n\n__global__ void print_from_gpu(void) {\n    printf(\"Hello World! from thread [%d,%d] \\\n        From device\\n\", threadIdx.x,blockIdx.x);\n}\n```\n\n当我们希望将您的注意力吸引到代码块的特定部分时，相关的行或项目以粗体显示:\n\n```cpp\nint main(void) {\n    printf(\"Hello World from host!\\n\");\n    print_from_gpu<<<1,1>>>();\n    cudaDeviceSynchronize();\n    return 0;\n}\n```\n\n任何命令行输入或输出都编写如下:\n\n```cpp\n$ nvcc -o hello_world hello_world.cu\n```\n\n**粗体**:表示一个新的术语、一个重要的单词或者你在屏幕上看到的单词。例如，菜单或对话框中的单词像这样出现在文本中。下面是一个例子:“对于 Windows 用户，在 VS 项目属性对话框中，您可以在 CUDA C/C++ |设备|代码生成时指定您的 GPU 的计算能力。”\n\nWarnings or important notes appear like this. Tips and tricks appear like this.\n\n# 取得联系\n\n我们随时欢迎读者的反馈。\n\n**一般反馈**:如果你对这本书的任何方面有疑问，在你的信息主题中提到书名，发邮件给我们`customercare@packtpub.com`。\n\n**勘误表**:虽然我们已经尽了最大的努力来保证内容的准确性，但是错误还是会发生。如果你在这本书里发现了一个错误，如果你能向我们报告，我们将不胜感激。请访问[www.packtpub.com/support/errata](https://www.packtpub.com/support/errata)，选择您的图书，点击勘误表提交链接，并输入详细信息。\n\n**盗版**:如果您在互联网上遇到任何形式的我们作品的非法拷贝，如果您能提供我们的位置地址或网站名称，我们将不胜感激。请通过`copyright@packt.com`联系我们，并提供材料链接。\n\n**如果你有兴趣成为一名作者**:如果有一个你有专长的话题，你有兴趣写或者投稿一本书，请访问[authors.packtpub.com](http://authors.packtpub.com/)。\n\n# 复习\n\n请留下评论。一旦你阅读并使用了这本书，为什么不在你购买它的网站上留下评论呢？然后，潜在的读者可以看到并使用您不带偏见的意见来做出购买决定，我们在 Packt 可以了解您对我们产品的看法，我们的作者可以看到您对他们的书的反馈。谢谢大家！\n\n更多关于 Packt 的信息，请访问[packt.com](http://www.packt.com/)。"
  },
  {
    "path": "docs/learn-cuda-prog/01.md",
    "content": "# 一、CUDA 编程入门\n\n自 2007 年首次发布以来，**计算统一设备架构** ( **CUDA** )已经成长为使用**图形计算单元**(**GPU**)进行通用计算(即非图形应用)的事实标准。那么，到底什么是 CUDA 呢？有人可能会问以下问题:\n\n*   是编程语言吗？\n*   是编译器吗？\n*   它是一种新的计算范式吗？\n\n在这一章中，我们将揭开关于 GPU 和 CUDA 的一些神话。本章通过提供**高性能计算** ( **高性能计算**)历史的简化视图，并通过摩尔定律和丹尼德标度等定律来证明这一点，从而为异构计算奠定基础，摩尔定律和丹尼德标度过去和现在都在推动半导体行业，进而推动处理器架构本身。还将向您介绍 CUDA 编程模型，并了解 CPU 和 GPU 架构之间的根本区别。到本章结束时，您将能够使用 C 语言中的 CUDA 编程构造来编写和理解`Hello World!`程序。\n\nWhile this chapter primarily uses C to demonstrate CUDA constructs, we will be covering other programming languages such as Python, Fortran, and OpenACC in other chapters.\n\n本章将涵盖以下主题:\n\n*   高性能计算的历史\n*   来自 CUDA 的你好世界\n*   使用 CUDA 的向量加法\n*   用 CUDA 报告错误\n*   CUDA 中的数据类型支持\n\n# 高性能计算的历史\n\n为了实现科学发现，高性能计算一直在挑战极限。处理器架构和设计的根本转变有助于跨越 FLOP 障碍，从**超级浮点运算** ( **微操作**)到现在能够在一秒钟内完成 PetaFLOP 计算。\n\n**Floating-Point Operations** (**FLOPs**) per second is the fundamental unit for measuring the theoretical peak of any compute processor. MegaFLOP stands for 10 to the 6<sup>th</sup> power of FLOPS. PetaFLOP stands for 10 to the 15<sup>th</sup> power of FLOPS.\n**Instruction-Level Parallelism** (**ILP**) is a concept wherein code-independent instructions can execute at the same time. For the instructions to execute in parallel, they need to be independent of each other. All modern CPU architecture (even GPU architecture) provides five to 15+ stages to allow for faster clock rates:\n\n`Instr 1: add = inp1 + inp2`\n`Instr 2: mult = inp1 * inp2`\n`Instr 3: final_result = mult / add`\n\nOperations for calculating the `mult` and `add` variables do not depend on each other, so they can be calculated simultaneously while calculating `final_result`, which depends on the results of the `Instr 1` and `Instr 2` operations. Therefore, it cannot be calculated until `add` and `mult` have been calculated.\n\n当我们从技术变革的角度来看高性能计算的历史时，技术变革导致了新处理器设计的根本转变及其对科学界的影响，其中有三个突出的主要变化，可以被称为时代:\n\n*   **纪元 1** :超级计算机的历史可以追溯到 CRAY-1，它基本上是一个提供峰值 160 兆触发器/MFLOP 计算能力的单矢量 CPU 架构。\n*   **纪元 2**:CRAY-2 从单核设计转向多核设计，跨越了 MegaFLOP 的壁垒，CRAY-2 是一款 4 核 Vector CPU，提供了 2gb 的峰值性能。\n*   **纪元 3** :跨越千兆的计算性能是一个根本性的转变，需要计算节点相互协作并通过网络进行通信，以提供更高的性能。克雷·T3D 是首批提供 1tb 计算性能的机器之一。该网络是 3D 圆环，提供 300 兆字节/秒的带宽。这是围绕标准微处理器的丰富的*外壳*的第一次重大实现。\n\n在这之后的近 20 年里，没有根本性的创新。技术创新主要集中在三个架构创新上:\n\n*   从 8 位到 16 位再到 32 位，现在是 64 位指令集\n*   增加 ILP\n*   增加内核数量\n\n这是通过提高时钟速率来实现的，目前时钟速率为 4 千兆赫。之所以有可能实现这一目标，是因为推动半导体产业发展的基本规律。\n\nMoore's Law: This law observes the number of transistors in a dense integrated circuit double every two years.\n\n几十年来，摩尔的预测被证明是准确的，现在仍然如此。摩尔定律是对历史趋势的观察和投射。\n\n**Dennard 标度:**这是一个保持摩尔定律生命力的标度定律。Dennard 对晶体管尺寸和功率密度之间的关系进行了观察，并将其总结为以下公式:\n\n*P = QfCV <sup>2</sup> + V I <sub>漏</sub>T5】*\n\n在这个方程中， *Q* 为晶体管数量， *f* 为工作频率， *C* 为电容， *V* 为工作电压， *I <sub>泄漏</sub>* 为泄漏电流。\n\nDennard scaling 和 Moore 定律是相互关联的，因为可以推断，从成本效益的角度来看，减小晶体管的尺寸可以导致每个芯片上的晶体管越来越多。\n\n使用 Dennard 缩放规则，给定尺寸的总芯片功率在许多代处理器中保持不变。晶体管数量增加了一倍，而尺寸保持收缩( *1/S* 速率)，频率每两年增加 40%。这在特征尺寸达到 65 纳米以下后停止，因为这些规则由于泄漏电流指数增长而不再持续。为了降低漏电流的影响，开关过程进行了新的创新。然而，这些突破仍然不足以恢复电压的比例。对于许多处理器设计，电压保持恒定在 1 伏。不再可能保持功率包络恒定。这也被普遍称为 Powerwall。\n\n从 1977 年到 1997 年，Dennard scaling 保持了自己的风格，然后开始衰落。因此，从 2007 年到 2017 年，处理器从 45 纳米发展到 16 纳米，但导致能量/芯片尺寸增加了三倍。\n\n与此同时，在最新的架构中，流水线阶段从五个阶段发展到 15+阶段。为了保持指令流水线满，使用了诸如推测等高级技术。推测单元包括预测程序的行为，例如预测分支和内存地址。如果一个预测是准确的，它可以继续进行；否则，它会撤消所做的工作并重新启动。深度流水线阶段和传统软件的写入方式导致了未使用的晶体管和浪费的时钟周期，这意味着应用的性能没有改善。\n\n然后是 GPU，主要用于图形处理。一位名叫马克·哈里斯的研究人员第一次将图形处理器用于非图形任务，并创造了新的术语**使用图形处理器的通用计算** ( **图形处理器**)。当涉及到某些属于数据并行的任务时，GPU 被证明是高效的。不出所料，许多高性能计算应用中的大多数计算密集型任务本质上都是数据并行的。它们主要是矩阵到矩阵乘法，这是**基本线性代数规范** ( **BLAS** 中的一个例程，并且被广泛使用。\n\n用户在适应和使用 GPU 时遇到的唯一问题是，他们必须了解图形管道才能使用 GPU。为 GPU 上的任何计算工作提供的唯一界面以着色器执行为中心。需要提供一个更通用的界面，该界面是 HPC 社区中的开发人员所知道的。2007 年引入 CUDA 解决了这个问题。\n\n虽然 GPU 架构也受到相同定律的约束(摩尔定律和 Dennard 缩放)，但处理器的设计采用了不同的方法，并为不同的用途提供晶体管，从而获得比传统同构架构更高的性能。\n\n下图显示了计算机体系结构从顺序处理到分布式内存的演变及其对编程模型的影响:\n\n![](img/88656735-ceac-44e5-87e3-3a376bd123a1.png)\n\n随着 GPU 被添加到现有服务器，应用运行在两种类型的处理器(中央处理器和图形处理器)上，这带来了异构性的概念。这是我们将在下一节中介绍的内容。\n\n# 异构计算\n\n围绕 GPU 的普遍误解是，它是 CPU 的替代品。GPU 用于加速代码中本质上并行的部分。 **Accelerator** 是 GPU 的常用术语，因为它们通过更快地运行代码的并行部分来加速应用，而 CPU 运行代码的另一部分，这是延迟限制。因此，高效的中央处理器和高吞吐量的图形处理器可以提高应用的性能。\n\n下图显示了在多种处理器类型上运行的应用:\n\n![](img/3d540d44-63ee-44d7-b988-654c173377a4.png)\n\n借助阿姆达尔定律可以很好地定义这个概念。Amdahl 定律用于定义当只有一小部分应用被并行化时可以实现的最大加速。为了演示这一点，上图显示了代码的两个部分。一部分是延迟限制，而另一部分是吞吐量限制。我们将在下一节讨论这两个术语的含义，这是中央处理器和图形处理器架构的区别。\n\n关键点在于，对于一定分数的受延迟限制的代码，CPU 是好的，而 GPU 则擅长并行运行代码的**单指令多数据** ( **SIMD** )部分。如果只有其中一个，即 CPU 代码或 GPU 代码，在优化后运行得更快，这不一定会给整个应用带来良好的加速。要求两个处理器在最佳使用时，在性能方面都有最大的优势。这种本质上是“T4”将某些类型的操作从处理器卸载到图形处理器上的方法被称为“T6”异构计算“T7”。\n\n下图描述了所有应用都具有的两种类型的部分，即延迟限制和吞吐量限制:\n\n![](img/9762f9d6-53f0-4e5e-9faf-ad4bde50a008.png)\n\n在这里，使用阿姆达尔定律证明了改进这两个部分的重要性。\n\n# 编程范例\n\n计算机体系结构的分类是使用弗林分类法完成的，该分类法描述了四类体系结构。弗林的分类 SIMDs 之一是用来描述 GPU 架构。然而，这两者之间有一个微妙的区别。SIMD 用于描述一种架构，其中相同的指令并行应用于多个数据点。此描述适用于具有矢量化能力的处理器。相比之下，在**单指令多线程** ( **SIMTs** )中，不是单个线程发出指令，而是多个线程向不同的数据发出相同的指令。与 SIMD 相比，GPU 架构更适合 SIMT 类别。\n\n让我们看一个添加两个数组并将数据存储在第三个数组中的例子。该操作的数据集由数组 *A* 、 *B* 和 *C* 组成。用于加法的相同操作用于数组的每个元素:\n\nCX = ax+bx\n\n很明显，每个任务都是相互独立的，但是所有线程都在应用相同的操作。\n\n下面的截图显示了向量加法，描述了这个范例的一个例子:\n\n![](img/135a15fb-cafc-4775-89b5-25054ea73460.png)\n\n# 低延迟与高吞吐量\n\n正如我们在上一节中提到的，CPU 架构针对低延迟访问进行了优化，而 GPU 架构针对数据并行吞吐量计算进行了优化。如下图截图所示，CPU 架构相比 GPU 缓存量大，类型多。我们走得越高，也就是说，L3 到 L1 的距离越长，缓存的数量越少，但延迟越短。中央处理器架构旨在实现对缓存数据集的低延迟访问。大量晶体管用于实现推测执行和无序执行。由于中央处理器以非常高的时钟速度运行，因此有必要通过将频繁使用的数据存储在缓存中并预测下一条要执行的指令来隐藏提取数据的延迟。能够探索这种时间局部性的应用可以最佳地利用 CPU 缓存。此外，容易填充指令流水线的应用，例如代码中没有`if`和`else`语句的应用，可以通过隐藏提取指令的延迟而从中受益。因此，中央处理器架构是一个减少延迟的架构。\n\n下面的截图显示了 **CPU** 和 **GPU** 架构如何为不同的内存和计算单元分配芯片芯片区域。虽然 **GPU** 使用大量晶体管来计算 **ALUs** ， **CPU** 使用它来减少延迟:\n\n![](img/e651f520-b274-4564-b578-eec763952aa8.png)\n\n另一方面，GPU 架构被称为**延迟降低**或**高吞吐量架构**。GPU 架构隐藏了其他线程的计算延迟。当一个线程等待数据可供计算时，其他线程可以开始执行，因此不会浪费任何时钟周期。如果你熟悉 CUDA，那么你可能知道扭曲的概念。我们将在接下来的章节中介绍扭曲的概念。(在 CUDA 中，执行单元是一条经线，而不是一条线。因此，上下文切换发生在经线和纬线之间)。\n\n你们中的一些人可能已经在想，为什么我们不能在中央处理器中创建这些线程，并做同样的事情来隐藏延迟。这是因为 GPU 有很多寄存器，并且所有的线程上下文切换信息已经存在于其中。这是目前最快的内存。然而，在中央处理器中，寄存器组有限，因此线程相关信息通常存储在较低的内存层次结构中，如缓存。例如，Volta 包含 20 MB 的寄存器存储。因此，与 GPU 相比，CPU 中线程之间的上下文切换时间要长得多。\n\n现在，让我们看看在 GPU 上编程的不同方法。\n\n# 图形处理器的编程方法\n\n让我们回到最初的问题，那就是什么是 CUDA？CUDA 是由 NVIDIA 开发的并行计算平台和编程模型架构，它将 GPU 上的通用计算作为一流的能力公开。像任何其他处理器一样，GPU 架构可以使用各种方法进行编码。提供嵌入式加速的最简单的方法是利用现有的库。或者，开发人员可以选择使用 **OpenACC** 指令来获得快速加速结果和可移植性。另一种选择是通过使用 C、C++、Fortran、Python 等语言构造来深入 CUDA，以获得最高的性能和灵活性。我们将在后续章节中详细介绍所有这些方法。\n\n下面的截图代表了我们可以执行 GPU 编程的各种方式:\n\n![](img/f38685df-fd3b-4df8-a3ab-bbe34b0ea63b.png)\n\n在本节中，我们为您提供了处理器和高性能计算如何随着时间的推移而发展的视角。我们向您概述了为什么异构编程模型是从应用中获得最佳性能的关键，然后介绍了 GPU 编程的方法。在下一节中，我们将开始在 GPU 上编写 Hello World 程序。\n\n# 技术要求\n\n本章需要一台带有现代 NVIDIA GPU(帕斯卡架构以上)的 Linux/Windows 电脑，以及所有必要的 GPU 驱动程序和安装的 CUDA 工具包(10.0 以上)。如果您不确定您的 GPU 架构，请访问英伟达的 GPU 网站([https://developer.nvidia.com/cuda-gpus](https://developer.nvidia.com/cuda-gpus))并确认您的 GPU 架构。本章的代码也可以在 https://github.com/PacktPublishing/Learn-CUDA-Programming 的 GitHub 上找到。\n\n本章中的代码示例已经用 CUDA 工具包的 10.1 版本进行了开发和测试，但是如果可能的话，建议使用最新的 CUDA 版本。\n\n# 来自 CUDA 的你好世界\n\nCUDA 是一种异构编程模型，包括对 CPU 和 GPU 的规定。CUDA C/C++ 编程接口由 C 语言扩展组成，因此您可以将部分源代码作为目标，以便在设备(GPU)上并行执行。它基于行业标准 C/C++ 并提供了一个可以在主机(CPU)上执行的 C 函数库，以便它可以与设备交互。\n\n在 CUDA 中，有两个处理器相互协作。主机通常被称为中央处理器，而设备通常被称为图形处理器。主机负责调用设备功能。正如我们已经提到的，在 GPU 上运行的部分代码称为**设备代码**，而在 CPU 上运行的串行代码称为**主机代码**。\n\n让我们从用 c 语言编写我们的第一个 CUDA 代码开始，目的是采取系统的分步方法，从一些顺序代码开始，并通过添加一些额外的关键字将其转换为 CUDA 感知代码。正如我们前面提到的，没有必要学习一门新的语言——我们只需要在现有的语言中添加一些关键词，这样我们就可以在具有 CPU 和 GPU 的异构环境中运行它。\n\n让我们看看第一段代码。这些代码所做的就是打印 Hello World！从主机和设备:\n\n```cpp\n#include<stdio.h>\n#include<stdlib.h>\n\n__global__ void print_from_gpu(void) {\n    printf(\"Hello World! from thread [%d,%d] \\\n        From device\\n\", threadIdx.x,blockIdx.x);\n}\n\nint main(void) {\n    printf(\"Hello World from host!\\n\");\n    print_from_gpu<<<1,1>>>();\n    cudaDeviceSynchronize();\n    return 0;\n}\n\n```\n\n让我们试着编译并运行前面的代码片段:\n\n1.  **编译代码**:将前面的代码放入名为`hello_world.cu`的文件中，使用 **NVIDIA C 编译器** ( **nvcc** )进行编译。注意文件的扩展名是`.cu`，它告诉编译器这个文件里面有 GPU 代码:\n\n```cpp\n$ nvcc -o hello_world hello_world.cu\n```\n\n2.  **执行 GPU 代码**:执行 GPU 代码后，我们应该会收到如下输出:\n\n![](img/2cfcd6a2-b158-4c73-8cc4-55e05d6df128.png)\n\n到目前为止，您可能已经观察到 CUDA C 代码的使用并没有很大的不同，只需要我们学习一些额外的构造来告诉编译器哪个函数是 GPU 代码以及如何调用 GPU 函数。我们不需要完全学习一门新语言。\n\n在前面的代码中，我们添加了一些构造和关键字，如下所示:\n\n*   `__global__`:这个关键字加在函数之前，告诉编译器这是一个会在设备上运行的函数，不会在主机上运行。但是，请注意它是由主机调用的。这里需要注意的另一件重要的事情是，设备函数的返回类型总是“void”。算法的数据并行部分作为内核在设备上执行。\n*   `<<<,>>>`:这个关键字告诉编译器，这是对设备函数的调用，而不是对主机函数的调用。此外，`1,1`参数基本上决定了内核中要启动的线程数量。稍后我们将讨论尖括号内的参数。就目前而言，`1,1`参数基本上意味着我们只用一个线程启动内核，也就是说，用一个线程启动顺序代码，因为除了打印之外，我们在代码中没有做任何重要的事情。\n*   `threadIdx.x`*`blockIdx.x`:这是一个唯一的 ID，给所有线程。我们将在下一节中更多地讨论这个主题。*\n**   `cudaDeviceSynchronize()`:CUDA 中所有的内核调用本质上都是异步的。主机在调用内核后变得空闲，然后开始执行下一条指令。这应该不足为奇，因为这是一个异构环境，因此主机和设备可以并行运行，以利用可用的处理器类型。在主机需要等待设备完成的情况下，作为 CUDA 编程的一部分，已经提供了 API，使得主机代码等待设备功能完成。一个这样的应用编程接口是`cudaDeviceSynchronize`，它会一直等到对设备的所有调用都完成。*\n\n*Try removing the `cudaDeviceSynchronize()` call and see whether the device output is visible or not. Alternatively, try putting this call before printing it on the host code.\n\n# 线程层次结构\n\n现在，让我们开始玩弄这两个参数，即`threadIdx.x`和`blockIdx.x`。\n\n**实验 1** :首先将参数从`<<<1,1>>>`改为`<<<2,1>>`，查看输出。运行多个单线程 Hello World 代码块的输出应该如下所示:\n\n![](img/71a3108d-a11b-4cf8-8df6-2ef02f1c53c5.png)\n\n如我们所见，我们现在有两个线程打印该值，而不是一个线程。请注意，它们的唯一标识是不同的。\n\n**实验二**:现在我们不改变第一个参数，而是改变第二个，即把`<<<1,1>>>`改为`<<<1,2>>>`，观察运行 Hello World 代码多个单线程块的输出，如下:\n\n![](img/451492da-208c-4cc7-98ff-9d943decd7f0.png)\n\n如您所见，像以前一样，被启动到内核中的线程总数是两个——唯一的区别是它们的标识不同。那么，这些线程和块概念是什么？为了解决这个问题，让我们深入研究一下 GPU 架构。\n\n# 图形处理器体系结构\n\nCUDA 如此受欢迎的一个关键原因是，硬件和软件都经过了设计，并紧密结合，以获得应用的最佳性能。因此，有必要展示软件 CUDA 编程概念和硬件设计本身之间的关系。\n\n下面的截图展示了 CUDA 的两面:\n\n![](img/dfdc4e4b-dce4-4f73-a781-8e21a84db518.png)\n\n我们可以看到 CUDA 软件已经映射到了 GPU 硬件。\n\n根据前面的截图，下表根据 CUDA 编程模型解释了软件和硬件映射:\n\n| **软件** | **按**执行 | **硬件** |\n| CUDA 线程 | 核心/SIMD 代码 |\n| CUDA 块 | 流式多处理器 |\n| 网格/内核 | 图形处理器设备 |\n\n让我们详细看一下上表的组件:\n\n*   **CUDA 线程** : CUDA 线程在一个 CUDA 核上执行。CUDA 线程不同于 CPU 线程。CUDA 线程极其轻量级，并提供快速的上下文切换。快速上下文切换的原因是由于图形处理器和基于硬件的调度器中有大的寄存器大小。与中央处理器相比，线程上下文存在于寄存器中，其中线程句柄位于较低的内存层次结构中，例如缓存。因此，当一个线程空闲/等待时，另一个准备好的线程可以几乎没有延迟地开始执行。每个 CUDA 线程必须执行相同的内核，并独立处理不同的数据(SIMT)。\n*   **CUDA 块** : CUDA 线程被组合成一个逻辑实体，称为 CUDA 块。CUDA 块在单个**流式多处理器** ( **SM** )上执行。一个块在单个 SM 上运行，也就是说，一个块内的所有线程只能在一个 SM 的内核上执行，而不能在其他 SM 的内核上执行。每个图形处理器可以有一个或多个微处理器，从而有效地利用整个图形处理器；用户需要将并行计算分成块和线程。\n*   **网格/内核** : CUDA 块被组合成一个逻辑实体，称为 CUDA 网格。然后在设备上执行 CUDA 网格。\n\n乍一看，这听起来有些复杂。在下一节中，我们将看一个向量加法的例子来解释这一点。希望事情会变得更清楚。\n\n# 使用 CUDA 的向量加法\n\n我们试图解决的问题是向量加法。我们知道，**向量**加法是一个数据并行运算。我们的数据集由三个数组组成: *A* 、 *B* 和 *C* 。对每个元素执行相同的操作:\n\nCX = ax+bx\n\n每次添加都是相互独立的，但是相同的操作由所有 CUDA 线程应用。要开始，请按照以下步骤配置您的环境:\n\n1.  准备好你的 GPU 应用。该代码将被置于`01_cuda_introduction/01_vector_addition`中。\n2.  使用以下命令，使用`nvcc`编译器编译您的应用:\n\n```cpp\n$nvcc -o vector_addition vector_addition.cu\n```\n\n前面的代码是顺序代码。我们将转换此代码，以便它可以使用分步方法在 GPU 上运行，如下所示:\n\n```cpp\n#include<stdio.h>\n#include<stdlib.h>\n\n#define N 512\n\nvoid host_add(int *a, int *b, int *c) {\n    for(int idx=0;idx<N;idx++)\n        c[idx] = a[idx] + b[idx];\n}\n\n//basically just fills the array with index.\nvoid fill_array(int *data) {\n    for(int idx=0;idx<N;idx++)\n        data[idx] = idx;\n}\n\nvoid print_output(int *a, int *b, int*c) {\n    for(int idx=0;idx<N;idx++)\n        printf(\"\\n %d + %d = %d\", a[idx] , b[idx], c[idx]);\n}\n\nint main(void) {\n    int *a, *b, *c;\n    int size = N * sizeof(int);\n   // Alloc space for host copies of a, b, c and setup input values\n    a = (int *)malloc(size); fill_array(a);\n    b = (int *)malloc(size); fill_array(b);\n    c = (int *)malloc(size);\n    host_add(a,b,c);\n    print_output(a,b,c);\n    free(a); free(b); free(c);\n    return 0;\n}\n```\n\n在转换顺序代码之前，让我们看一下 CUDA 和顺序代码之间的基本变化或步骤:\n\n| **顺序码** | **CUDA 代码** |\n| 第一步 | 在 CPU 上分配内存，即`malloc new`。 | 第一步 | 在 CPU 上分配内存，即`malloc new`。 |\n| 第二步 | 填充/初始化中央处理器数据。 | 第二步 | 在 GPU 上分配内存，即`cudaMalloc`。 |\n| 第三步 | 调用处理数据的中央处理器函数。在这种情况下，实际的算法是向量加法。 | 第三步 | 填充/初始化中央处理器数据。 |\n| 第四步 | 使用经过处理的数据，在这种情况下会打印出来。 | 第四步 | 用`cudaMemcpy`将数据从主机传输到设备。 |\n| 第五步 | 用`<<<,>>>`括号调用 GPU 函数。 |\n| 第六步 | 用`cudaDeviceSynchronize`同步设备和主机。 |\n| 第七步 | 用`cudaMemcpy`将数据从设备传输到主机。 |\n| 第八步 | 使用经过处理的数据，在这种情况下会打印出来。 |\n\nThis book is not a replacement for the CUDA API guide and does not cover all CUDA APIs. For extensive use of the API, please refer to the CUDA API guide.\n\n正如我们所看到的，CUDA 处理流程有一些额外的步骤需要添加到顺序代码中。这些措施如下:\n\n1.  **GPU 上的内存分配:** CPU 内存和 GPU 内存是物理上分开的内存。`malloc`在 CPU 的 RAM 上分配内存。图形处理器内核/设备功能只能访问分配/指向设备内存的内存。要在 GPU 上分配内存，我们需要使用`cudaMalloc` API。与`malloc`命令不同，`cudaMalloc`不返回分配内存的指针；相反，它将指针引用作为参数，并使用分配的内存更新该引用。\n\n2.  **将数据从主机内存传输到设备内存:**然后将主机数据复制到设备内存，该内存是使用上一步中使用的`cudaMalloc`命令分配的。用于在主机和设备之间以及主机和设备之间复制数据的应用编程接口是`cudaMemcpy`。像其他`memcopy`命令一样，这个应用编程接口需要目标指针、源指针和大小。它采用的另一个参数是拷贝方向，即我们是从主机拷贝到设备还是从设备拷贝到主机。在最新版本的 CUDA 中，这是可选的，因为驱动程序能够理解指针是指向主机内存还是设备内存。请注意`cudaMemcpy`有一个异步替代方案。这将在其他章节中详细介绍。\n3.  **调用并执行一个 CUDA 函数:**如 Hello World CUDA 程序所示，我们通过使用`<<<,>>>`括号调用一个内核，括号中分别提供了块和线程大小的参数。所有步骤完成后，我们将对此进行更详细的介绍。\n4.  **Synchronize:** 正如我们在 Hello World 程序中提到的，内核调用本质上是异步的。为了让主机确定内核执行已经完成，主机调用`cudaDeviceSynchronize`函数。这可以确保所有先前启动的设备调用都已完成。\n5.  **将数据从主机内存传输到设备内存:**使用相同的`cudaMemcpy`应用编程接口将数据从设备复制回主机，用于后处理或验证任务，如打印。与第一步相比，这里唯一的变化是我们反转了拷贝的方向，即目标指针指向主机，而源指针指向内存中分配的设备。\n6.  **释放分配的 GPU 内存:**最后，使用`cudaFree` API 释放分配的 GPU 内存。\n\n更改顺序向量加法代码的`main`功能，以反映这些新步骤。`main`功能将如下所示:\n\n```cpp\nint main(void) {\n    int *a, *b, *c;\n    int *d_a, *d_b, *d_c; // device copies of a, b, c\n    int size = N * sizeof(int);\n\n    // Alloc space for host copies of a, b, c and setup input values\n    a = (int *)malloc(size); fill_array(a);\n    b = (int *)malloc(size); fill_array(b);\n    c = (int *)malloc(size);\n\n    // Alloc space for device copies of vector (a, b, c)\n    cudaMalloc((void **)&d_a, N* * sizeof(int));\n    cudaMalloc((void **)&d_b, N* *sizeof(int));\n    cudaMalloc((void **)&d_c, N* * sizeof(int));\n\n    // Copy from host to device\n    cudaMemcpy(d_a, a, N * sizeof(int), cudaMemcpyHostToDevice);\n    cudaMemcpy(d_b, b, N* sizeof(int), cudaMemcpyHostToDevice);\n\n    device_add<<<1,1>>>(d_a,d_b,d_c);\n\n    // Copy result back to host\n    cudaMemcpy(c, d_c, N * sizeof(int), cudaMemcpyDeviceToHost);\n\n    print_output(a,b,c);\n    free(a); free(b); free(c);\n\n    //free gpu memory\n    cudaFree(d_a); cudaFree(d_b); cudaFree(d_c);\n\n    return 0;\n} \n```\n\n现在，让我们看看内核代码是如何编写的，以及如何管理线程和块大小。为此，我们将进行多次实验。\n\n# 实验 1–创建多个块\n\n在本节中，我们将利用 CUDA 块在 GPU 上并行运行矢量加法代码。额外的关键字将被暴露，这些关键字与我们如何索引 CUDA 块有关。更改对`device_add`功能的调用，如下所示:\n\n```cpp\n//changing from device_add<<<1,1>>> to\ndevice_add<<<N,1>>>\n```\n\n这将并行执行`device_add`功能`N`次，而不是一次。`device_add`函数的每次并行调用被称为一个块。现在，让我们添加一个`__global__`设备功能，如下所示:\n\n```cpp\n__global__ void device_add(int *a, int *b, int *c) {\n c[blockIdx.x] = a[blockIdx.x] + b[blockIdx.x];\n}\n```\n\n通过使用`blockIdx.x`来索引数组，每个块处理数组的不同元素。在设备上，每个块可以并行执行。让我们看看下面的截图:\n\n![](img/ad588e52-b0fc-4c3a-9ab8-d3035f49ccdc.png)\n\n前面的截图表示矢量加法图形处理器代码，其中每个块显示多个单线程块的索引。\n\n# 实验 2–创建多个线程\n\n在本节中，我们将利用 CUDA 线程在 GPU 上并行运行向量加法代码。额外的关键字将被暴露，这些关键字与我们如何索引 CUDA 线程有关。\n\n一个块可以分成多个线程。更改对`device_add`功能的调用，如下所示:\n\n```cpp\n//changing from device_add<<<1,1>>> to\ndevice_add<<<1,N>>>\n```\n\n这将并行执行`device_add`功能`N`次，而不是一次。`device_add`函数的每次并行调用被称为一个线程。更改设备例程以反映内核，如下所示:\n\n```cpp\n__global__ void device_add(int *a, int *b, int *c) {\n     c[threadIdx.x] = a[threadIdx.x] + b[threadIdx.x];\n}\n```\n\n一个值得注意的区别是，我们使用的不是`blockIdx.x`，而是`threadIdx.x`，如下图截图所示:\n\n![](img/5534f36e-92c5-434e-a5c6-a4a5d2ac36d9.png)\n\n前面的截图表示矢量加法图形处理器代码，其中每个块显示单个块的索引——多线程。\n\n# 实验 3–组合块和线程\n\n到目前为止，我们已经在*实验 1–创建多个块*部分和*实验 2–创建多个线程*部分研究了通过使用多个具有一个线程的块进行并行向量加法。在这个实验中，我们将使用多个块以及包含多个线程的独立块。这在如何找到索引方面变得更具挑战性，因为我们需要组合`threadIdx`和`blockIdx`来生成唯一的 ID。\n\n让我们看一下两个场景，它们描述了开发人员可以选择的不同组合:\n\n*   **场景一:**我们来考虑一下向量元素的总数是 32。每个块包含八个线程，总共四个块。\n*   **场景二:**我们来考虑一下向量元素的总数是 32。每个块包含四个线程，总共八个块。\n\n在这两种情况下，并行执行的数量都是 32，其中所有 32 个元素并行填充。开发人员根据问题的大小和每个硬件的限制，在块内的线程和块的数量之间做出选择。我们将在另一章中详细介绍基于体系结构的正确规模选择。\n\n下面的截图显示了不同块和线程配置的矢量加法 GPU 索引代码:\n\n![](img/a4c34dbe-c938-4624-83d5-a3ed095c03f6.jpg)\n\n现在，让我们看看如何更改内核代码来组合线程和块来计算全局索引:\n\n```cpp\n__global__ void device_add(int *a, int *b, int *c) {\n     int index = threadIdx.x + blockIdx.x * blockDim.x;\n     c[index] = a[index] + b[index];\n}\n```\n\n当从`main()`函数调用内核时，开发人员为我们前面提到的两种情况选择块和线程配置，如下面的代码所示:\n\n*   **场景 1:** 以下是针对每个块八个线程的矢量加法 GPU 网格和块大小计算的代码:\n\n```cpp\nthreads_per_block = 8;\nno_of_blocks = N/threads_per_block;\ndevice_add<<<no_of_blocks,threads_per_block>>>(d_a,d_b,d_c);\n```\n\n*   **场景 2:** 以下是矢量加法 GPU 网格和每个块四个线程的块大小计算的代码:\n\n```cpp\nthreads_per_block = 4;\nno_of_blocks = N/threads_per_block;\ndevice_add<<<no_of_blocks,threads_per_block>>>(d_a,d_b,d_c);\n```\n\n通过线程和块的组合，可以计算线程的唯一标识。如前面的代码所示，另一个变量被赋予所有线程。这叫`blockDim`。该变量由块的尺寸组成，即每个块的线程数。让我们看看下面的截图:\n\n![](img/ffe81d23-3db4-4439-847b-4733a7386a23.png)\n\n在这里，我们可以看到场景 1 的向量加法 GPU 索引计算。\n\n# 为什么要为线程和块烦恼？\n\n为什么我们需要这种额外的线程和块层次结构，这可能并不明显。它们增加了开发人员需要找到正确的块和网格大小的复杂性。此外，全局索引成为一项挑战。这是因为 CUDA 编程模型对其设置了限制。\n\n与并行块不同，线程具有高效通信和同步的机制。现实世界的应用需要线程相互通信，并且可能希望在继续之前等待某些数据被交换。这种操作需要线程进行通信，CUDA 编程模型允许同一块内的线程进行这种通信。在内核执行期间，属于不同块的线程不能相互通信/同步。这种限制允许调度程序相互独立地调度 SM 上的块。这样做的结果是，如果新硬件发布了更多的 SMs，并且代码具有足够的并行性，则代码可以线性扩展。换句话说，这允许硬件根据图形处理器的能力来扩展并行运行的块的数量。\n\n线程使用一种称为共享内存的特殊内存相互通信。我们将在[第 2 章](02.html)、 *CUDA 内存管理*中详细介绍共享内存，其中我们将展示 GPU 中的其他内存层次及其最佳使用。下面的截图演示了跨不同 GPU 的缩放块，这些 GPU 由不同数量的 SMs 组成:\n\n![](img/8aefa6e3-b6e7-433c-8115-040a7f7a55d6.png)\n\n现在，让我们了解更多关于多维启动内核的信息。\n\n# 多维启动内核\n\n到目前为止，我们一直在一维空间中启动线程和块。这意味着我们只对一个维度使用索引；例如，我们一直在使用`threadIdx.x`，其中`x`表示我们只使用了一个`x`维度线程索引。同样，我们一直在使用`blockIdx.x`，其中`x`表示我们只使用了一个`x`维度块索引。我们可以在一个、两个或三个维度上启动线程和块。二维启动线程和块的一个例子是，当我们对图像使用并行操作时，例如，使用滤镜模糊图像。开发人员可以选择启动二维的线程和块，这是一个更自然的选择，因为图像本质上是二维的。\n\n重要的是要理解，每一个 GPU 架构也对线程和块的尺寸进行了限制。例如，NVIDIA Pascal 卡在`x`和`y`维度中每个线程块最多允许 1，024 个线程，而在`z`维度中，您只能启动 64 个线程。类似地，在帕斯卡架构的`y`和`z`维度以及`x`维度中，网格中的最大块数被限制为 65，535。如果开发人员启动一个不支持维度的内核，应用将抛出一个运行时错误。\n\n到目前为止，我们一直假设我们编写的代码没有错误。但是在现实世界中，每个程序员写的代码都有 bug，有必要捕捉这些错误。在下一节中，我们将看看 CUDA 中的错误报告是如何工作的。\n\n# CUDA 中的错误报告\n\n在 CUDA 中，主机代码管理错误。大多数 CUDA 函数调用`cudaError_t`，基本上是枚举类型。`cudaSuccess`(值 0)表示`0`错误。用户也可以使用`cudaGetErrorString()`功能，该功能返回描述错误情况的字符串，如下所示:\n\n```cpp\n cudaError_t e;\n e = cudaMemcpy(...);\n if(e)\n     printf(\"Error: %sn\", cudaGetErrorString(err));\n```\n\n内核启动没有返回值。我们可以在这里使用`cudaGetLastError()`这样的函数，它返回最后一个 CUDA 函数的错误代码(包括内核启动)。在多个错误的情况下，只报告最后一个错误:\n\n```cpp\nMyKernel<<< ... >>> (...);\ncudaDeviceSynchronize();\ne = cudaGetLastError();\n```\n\n当涉及到生产代码时，建议在逻辑检查点使用错误检查代码，因为即使 GPU 内核崩溃，CPU 代码也会继续正常执行，从而导致不正确的结果。\n\n在下一节中，我们将向您介绍 CUDA 编程模型中支持的数据类型。\n\n# CUDA 中的数据类型支持\n\n像任何处理器架构一样，图形处理器也有不同类型的存储器，每种存储器都有不同的用途。我们将在[第 2 章](02.html)、 *CUDA 内存管理*中更详细地介绍它们。但是，了解支持的不同数据类型及其对性能和准确性的影响非常重要。CUDA 编程支持开发人员在各自语言方面熟悉的所有标准数据类型。除了不同大小的标准数据类型(`char`为 1 字节、`float`为 4 字节、`double`为 8 字节等等)，它还支持`float2`、`float4`等向量类型。\n\n建议数据类型自然对齐，因为大小为 1、2、4、8 或 16 字节的数据类型的对齐数据访问可确保 GPU 调用单个内存指令。如果它们没有对齐，编译器会生成多个指令，这些指令是交错的，导致内存和指令总线的利用率低下。因此，建议对驻留在 GPU 内存中的数据使用自然对齐的类型。内置类型`char`、`short`、`int`、`long`、`long long`、`float`和`double`如`float2`、`float4`自动满足对中要求。\n\n此外，CUDA 编程支持复杂的数据结构，如结构和类(在 C 和 C++ 的上下文中)。对于复杂的数据结构，开发人员可以利用编译器的对齐说明符来实施对齐要求，如以下代码所示:\n\n```cpp\nstruct __align__(16) {\n    float r;\n    float g;\n    float b;\n};\n```\n\n每个图形处理器都有一组有限的内核，因此 FLOPS 是不同的。例如，采用 Volta 架构的特斯拉 V100 卡有 2，560 个 FP64 内核(双精度)，而它的 32 位单精度内核数量是前者的两倍。很明显，根据算法的精度要求使用正确的数据类型是至关重要的。现在正在开发混合精度算法，以利用不同类型的内核，其中算法的某些部分以较高的精度运行，而某些部分以较低的精度运行。在接下来的章节中，我们还将讨论这个主题。目前，重要的是要理解 GPU 内存层次结构是不同的，因此，使用正确的数据类型很重要。\n\nWhile this was a general introduction to the data types that are supported in GPU, more details about all of the supported data types can be found at [https://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html#built-in-vector-types](https://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html#built-in-vector-types).\n\n# 摘要\n\n在本章中，我们借助历史和高性能计算为您提供了异构计算的视角。我们详细讨论了两个处理器，即中央处理器和图形处理器，是如何不同的。我们还在 GPU 上编写了 Hello World 和矢量加法 CUDA 程序。最后，我们研究了如何检测 CUDA 中的错误，因为对 CUDA API 的大多数调用本质上都是异步的。\n\n在下一章中，我们将看看可用的不同类型的图形处理器内存，以及如何最佳地利用它们。*"
  },
  {
    "path": "docs/learn-cuda-prog/02.md",
    "content": "# 二、内存管理\n\n正如我们在[第 1 章](01.html)、*引言 t* *o CUDA 编程*中所描述的，CPU 和 GPU 架构有着根本的不同，它们的内存层次也是如此。它们不仅尺寸和类型不同，而且用途和设计也不同。到目前为止，我们已经研究了每个线程如何借助索引(`blockIdx`和`threadIdx`)访问自己的数据。我们还利用了像`cudaMalloc`这样的应用编程接口在设备上分配内存。图形处理器中有许多内存路径，每个路径都有不同的性能特征。启动 CUDA 内核可以帮助我们实现最大的性能，但前提是以最佳方式使用正确类型的内存层次结构。开发人员有责任将数据集映射到正确的内存类型。\n\n从经验来看，如果我们绘制一个图表，概述图形处理器上的顶级应用性能限制，它看起来像下图:\n\n![](img/ce9fca55-7032-4add-8f56-5a8d1af24a01.png)\n\n前面的饼图显示了大多数基于 CUDA 的应用中出现的性能问题的大致分类。很明显，在大多数情况下，应用的性能会受到与内存相关的限制。根据应用和采用的内存路径，进一步划分内存相关的约束。\n\n让我们从不同的角度来看待这种方法，并理解有效使用正确内存类型的重要性。采用 Volta 架构的最新 NVIDIA GPU 提供了 7，000 GFLOP 的峰值性能，其设备内存带宽为 900 GB/s。您将首先看到的是 FLOP 与内存带宽的比率，约为 7:1。这是假设所有线程都在访问 4 字节(浮点)的数据来执行操作。一次性执行此操作所需的总带宽为 *4*7，000 = 28，000* GB/s，即达到峰值性能。900 GB/s 将执行限制在 225 GFLOP。这将执行速率限制在设备浮点执行速率峰值的 3.2% ( 225 GFLOP 是峰值的 3.2%，即 7,000 GFLOP)。如您所知，GPU 是一种延迟隐藏架构，有许多线程可供执行，这意味着理论上它可以容忍长时间的内存访问延迟。尽管如此，对内存的多余调用可以防止极少数线程停止或等待，并将导致一些 SMs 空闲。CUDA 架构提供了其他几种我们可以用来访问内存的方法来解决内存瓶颈的问题。\n\n下图演示了数据从中央处理器内存穿越到被内存管理器用于处理的路径。在这里，我们可以看到数据元素在到达 SM 核心进行计算之前的行程。每个内存带宽的数量级不同，访问它们的延迟也不同:\n\n![](img/39dc4102-fded-4f2f-a522-3b7dbc01cb19.png)\n\n在上图中，我们可以看到从中央处理器到寄存器的数据路径，最终计算由算术逻辑单元/内核完成。\n\n下图显示了最新图形处理器体系结构中存在的不同类型的内存层次结构。对于应用开发人员来说，每个内存可能有不同的大小、延迟、吞吐量和可见性:\n\n![](img/77cc060d-afe2-41f5-b4ea-842fe41e58fe.png)\n\n上图显示了最新图形处理器架构中存在的不同类型的内存及其在硬件中的位置。\n\n在本章中，您将学习如何优化利用不同类型的图形处理器内存。我们还将关注类似 GPU 的统一内存的最新功能，它使程序员的生活变得简单得多。本章将详细介绍以下内存主题:\n\n*   全局内存/设备内存\n*   共用存储器\n*   只读数据/缓存\n*   固定存储器\n*   统一内存\n\n但是在我们查看内存层次结构之前，我们将遵循优化周期，如下所示:\n\n*   第一步:分析\n*   步骤 2:并行化\n*   第三步:优化\n\n对应用的分析要求我们不仅要了解应用的特性，还要了解它在 GPU 上的运行效率。为此，在进入 GPU 内存之前，我们将首先向您介绍 Visual Profiler。由于我们在这里使用了 CUDA 的一些最新功能，请在继续本章之前阅读以下部分。\n\n# 技术要求\n\n本章需要一台带有现代 NVIDIA GPU(帕斯卡架构以上)的 Linux 电脑，以及所有必要的 GPU 驱动程序和安装的 CUDA 工具包(10.0 以上)。如果您不确定您的图形处理器的架构，请访问位于[https://developer.nvidia.com/cuda-gpus](https://developer.nvidia.com/cuda-gpus)的英伟达图形处理器网站并确认。本章的代码也可以在 https://github.com/PacktPublishing/Learn-CUDA-Programming 的 GitHub 上找到。\n\n本章的示例代码已经用 CUDA 工具包的 10.1 版本开发和测试。但是，建议使用最新的 CUDA 版本或更高版本。\n\n在下一节中，我们将向您介绍 Visual Profiler，它将帮助我们分析应用。我们还将看看它在 GPU 上的运行情况。\n\n# 英伟达视觉轮廓仪\n\n为了理解不同内存层次的有效利用，在运行时分析应用的特性是很重要的。Profilers 是非常方便的工具，可以测量和显示不同的指标，帮助我们分析内存、SM、内核和其他资源的使用方式。NVIDIA 决定提供一个 API，探查器工具的开发人员可以使用它来挂钩到 CUDA 应用中，随着时间的推移，许多探查器工具也发生了变化，例如 TAU Performance 系统、Vampir Trace 和 HPC Toolkit。这些都利用 **CUDA Profiler 工具接口** ( **CUPTI** )为 CUDA 应用提供概要信息。\n\n英伟达自己开发和维护作为 CUDA 工具包一部分的分析工具。本章利用这两个分析工具(NVPROF 和 NVVP)来演示不同内存类型的有效使用，并不是分析工具的指南。\n\n我们将使用 NVPROF 或 NVVP 演示 CUDA 应用的特性。NVPROF 是命令行工具，`nvvp`有可视化界面。`nvvp`有两种格式，一种是独立版本，另一种是 Nsight Eclipse 内部的集成版本。\n\n我们将广泛使用的 NVVP 探查器窗口如下所示:\n\n![](img/56803b05-bc2d-41d1-aba1-66bb38d85c30.png)\n\n这是在 macOS 上拍摄的 9.0 版窗口快照。\n\n窗口中有四个视图:时间线、指南、分析结果和摘要。顾名思义，时间轴视图显示了跨时间发生的中央处理器和图形处理器活动。可视化探查器显示了 CUDA 编程模型的内存层次的概要视图。“分析”视图显示分析结果。可视化探查器提供两种分析模式:\n\n*   **引导式分析:**顾名思义，它通过采取循序渐进的方法来理解关键的性能限制因素，从而指导开发人员。一旦初学者成为理解不同指标的专家，我们会建议他们在进入无指导模式之前使用这种模式。\n*   **无指导分析:**开发人员必须在此模式下手动查看结果，才能了解性能限制器。\n\nCUDA 工具包提供了两个 GPU 应用配置文件工具，**NVIDIA Profiler**(**NVPROF**)和 **NVIDIA 可视化 Profiler** ( **NVVP** )。为了获得性能限制信息，我们需要进行不同类型的分析:时间线分析和度量分析。该代码可在`02_memory_overview/04_sgemm`处访问。分析命令可以按如下方式执行:\n\n```cpp\n$ nvcc -o sgemm sgemm.cu\n$ nvprof -o sgemm.nvvp ./sgemm\n$ nvprof --analysis-metrics -o sgemm-analysis.nvvp ./sgemm\n```\n\n让我们打开可视化探查器。如果你用的是 Linux 或者 OSX，可以在终端执行`nvvp`。或者，可以从 CUDA 工具包安装的二进制中找到`nvvp`二进制。如果您使用的是 Windows，您可以使用带有`nvvp`命令的 Windows 搜索框执行该工具。\n\n要打开双概要数据，我们将使用文件|导入...菜单，如下所示:\n\n![](img/93fc3b11-ff9a-4c01-ab35-c4b52977a905.png)\n\n然后，我们将继续单击底部的“下一步”按钮:\n\n![](img/fef3b906-3e8d-43ea-9945-8bee75de1527.png)。\n\n我们的 CUDA 应用使用一个进程。因此，让我们继续点击底部的“下一步”按钮:\n\n![](img/3e64070c-e869-4d9a-aa83-e4915355f965.png)\n\n现在，让我们将收集的概要数据放入可视化概要分析器。下面的截图显示了一个例子。使用浏览将时间线数据放在第二个文本框中...右边的按钮。然后，以同样的方式将度量分析数据放入下一个文本框:\n\n![](img/2ccb86bb-9e2a-4062-b862-eedf5473da49.png)\n\nFor detailed usage of profiling tools, please refer to the CUDA Profiling guide, which comes as part of the CUDA Toolkit (the respective web link is [https://docs.nvidia.com/cuda/profiler-users-guide/index.html](https://docs.nvidia.com/cuda/profiler-users-guide/index.html)).\n\n在基于 Windows 的系统中，安装 CUDA 工具包后，您可以从“开始”菜单启动可视化探查器。在具有 X11 转发的 Linux 系统上，您可以通过运行`nvvp`命令来启动视觉探查器，该命令代表 NVIDIA 视觉探查器:\n\n```cpp\n$ ./nvvp\n```\n\n既然我们现在已经对将要使用的分析工具有了相当的了解，让我们跳到第一个也是最关键的 GPU 内存——全局内存/设备内存。\n\n# 全局内存/设备内存\n\n本节将详细介绍如何使用全局内存，也称为设备内存。在本节中，我们还将讨论如何高效地将数据从全局内存加载/存储到缓存中。由于全局内存是一个暂存区，所有数据都从中央处理器内存中复制，因此充分利用该内存至关重要。全局内存或设备内存对内核中的所有线程都是可见的。该内存对 CPU 也是可见的。\n\n程序员分别用`cudaMalloc`和`cudaFree`明确管理分配和解除分配。数据分配`cudaMalloc`并声明为`__device__`。全局内存是使用`cudaMemcpy`应用编程接口从中央处理器传输的所有内存的默认暂存区。\n\n# 全局内存上的向量加法\n\n我们在第一章中使用的向量加法示例演示了全局内存的使用。让我们再次查看代码片段，并尝试了解如何使用全局内存:\n\n```cpp\n__global__ void device_add(int *a, int *b, int *c) {\n     int index = threadIdx.x + blockIdx.x * blockDim.x;\n     c[index] = a[index] + b[index];\n}\nint main (void) {\n...\n    // Alloc space for device copies of a, b, c\n    cudaMalloc((void **)&d_a, size);\n    cudaMalloc((void **)&d_b, size);\n    cudaMalloc((void **)&d_c, size);\n...\n\n   // Free space allocated for device copies\n   cudaFree(d_a); cudaFree(d_b); cudaFree(d_c);\n...\n\n}\n```\n\n`cudaMalloc`在设备内存上分配数据。内核中参数的指针(`a`、`b`和`c`)指向这个设备内存。我们使用`cudaFree`应用编程接口释放内存。如您所见，块中的所有线程都可以访问内核中的内存。\n\n该代码可在`02_memory_overview/01_vector_addition`处访问。为了编译这段代码，您可以使用以下命令:\n\n```cpp\n$ nvcc -o vec_addition ./vector_addition_gpu_thread_block.cu\n```\n\n这是一个利用全局内存的简单例子。在下一节中，我们将了解如何以最佳方式访问数据。\n\n# 合并与未合并的全局内存访问\n\n为了有效地使用全局内存，理解 CUDA 编程模型中的翘曲概念非常重要，到目前为止，我们一直忽略了这一点。翘曲是 SMs 中线程调度/执行的一个单元。一旦一个块被分配给一个 SM，它就被分成一个称为**经线**的 32 线单元。这是 CUDA 编程中的基本执行单元。\n\n为了演示扭曲的概念，让我们看一个例子。如果两个块被分配给一个 SM，并且每个块有 128 根线，那么一个块内的经纱数为 *128/32 = 4 根*经纱，SM 上的经纱总数为 *4 * 2 = 8 根*经纱。下图显示了 CUDA 块如何在 GPU SM 上被划分和调度:\n\n![](img/b1b16bf3-50bc-4e16-a5be-1bbc72b7e395.png)\n\n区块和扭曲在 SM 上是如何安排的，它的核心更多的是特定于架构的，对于开普勒、帕斯卡和最新的架构沃尔特来说会有所不同。目前，我们可以忽略调度的完整性。在所有可用的扭曲中，操作数准备好用于下一条指令的扭曲有资格执行。基于 CUDA 程序运行的 GPU 的调度策略，选择扭曲来执行。选中时，扭曲中的所有线程都执行相同的指令。CUDA 遵循**单指令，多线程** ( **SIMT** )模型，即一个 warp 中的所有线程在一个时间实例中获取并执行相同的指令。为了最佳地利用来自全局内存的访问，访问应该合并。聚结和未聚结的区别如下:\n\n*   **合并全局内存访问:**顺序内存访问相邻。\n*   **未完成的全局内存访问:**顺序内存访问不相邻。\n\n下图更详细地显示了这种访问模式的一个示例。图的左侧显示了合并访问，其中来自 warp 的线程访问相邻数据，因此导致一个 32 位宽的操作和一个缓存未命中。图的右侧显示了一个场景，其中来自 warp 中线程的访问是随机的，可能导致调用 32 个一宽操作，因此可能有 32 个缓存未命中，这是最坏的情况:\n\n![](img/b125e1a6-614e-48f2-b4d6-dd00e95b1cfe.jpg)\n\n为了进一步理解这个概念，我们需要了解数据是如何通过缓存线从全局内存到达的。\n\n**场景 1:** 扭曲请求 32 对齐，4 个连续字节\n\n该地址属于一个高速缓存行和一个 32 位宽的操作。总线利用率是 100%，也就是说，我们正在利用从全局内存中提取到缓存中的所有数据，完全没有浪费任何带宽。如下图所示:\n\n![](img/1f19c553-c555-425a-a0c6-fe683f87a986.png)\n\n上图显示了合并访问，从而优化了总线利用率。\n\n**场景 2:** 扭曲请求 32 个分散的 4 字节字\n\n虽然 warp 需要 128 字节，但有 32 个一位宽的读取正在执行，导致 *32 * 128* 字节在总线上未命中。总线利用率实际上低于 1%，如下图所示:\n\n![](img/bd6f035c-56e5-4aa9-8062-310f4bbe5908.png)\n\n上图显示了未完成的访问，导致了总线带宽的浪费**。**\n\n正如我们在上图中看到的，warp 中的线程如何从全局内存中访问数据非常重要。为了优化利用全局内存，改进合并非常重要。可以使用多种策略。一个这样的策略是改变数据布局以提高局部性。我们来看一个例子。将滤镜应用于图像或将遮罩应用于图像的计算机视觉算法要求将图像存储到数据结构中。在声明图像类型时，开发人员有两种选择。\n\n下面的代码片段利用`Coefficients_SOA`数据结构以数组格式存储数据。`Coefficients_SOA`结构存储图像相关数据，如 RGB、色调和饱和度值:\n\n```cpp\n//Data structure representing an image stored in Structure of Array Format\nstruct Coefficients_SOA {\n int r;\n int b;\n int g;\n int hue;\n int saturation;\n int maxVal;\n int minVal;\n int finalVal;\n};\n```\n\n下图显示了关于如何为`Coefficients_SOA`存储数据并由内核中的不同线程访问的数据布局:\n\n![](img/3742c385-2a7c-4ed3-be87-6f5c1bd9ee67.jpg)\n\n通过这样做，我们可以看到 AOS 数据结构的使用如何导致未完成的全局内存访问。\n\n相同的图像可以以数组结构格式存储，如下面的代码片段所示:\n\n```cpp\n//Data structure representing an image stored in Array of Structure Format\nstruct Coefficients_AOS {\n int* r;\n int* b;\n int* g;\n int* hue;\n int* saturation;\n int* maxVal;\n int* minVal;\n int* finalVal;\n};\n```\n\n下图显示了关于如何为`Coefficients_AOS`存储数据并由内核中的不同线程访问的数据布局:\n\n![](img/6cba8cd2-87a2-4b90-8145-0f2ad79ace78.jpg)\n\n通过这样做，我们可以看到使用 SOA 数据结构如何导致非高级全局内存访问。\n\nCPU 上的顺序代码更倾向于 AOS 的缓存效率，而 SOA 在**单指令多线程** ( **SIMT** ) 等模型中更倾向于 CUDA 的执行和内存效率。\n\n让我们试着利用剖析器来分析这个方面。根据以下步骤配置您的环境:\n\n1.  准备好你的 GPU 应用。作为一个例子，我们将使用两段代码来演示全局内存的有效使用。虽然`aos_soa.cu`文件包含使用 AOS 数据结构的简单实现，但是`aos_soa_solved.cu`使用了有效利用全局内存的 SOA 数据结构。这个代码可以在`02_memory_overview/02_aos_soa`找到。\n2.  用`nvcc`编译器编译你的应用，然后用`nvprof`编译器分析它。以下命令是这方面的`nvcc`命令的一个例子。然后，我们使用`nvprof`命令来分析应用。`--analysis-metrics`标志也被传递，这样我们就可以得到内核的度量。\n3.  生成的配置文件，即`aos_soa.prof`和`aos_soa_solved.prof`，然后被加载到 NVIDIA 可视化配置文件中。用户需要从文件|打开菜单加载分析输出。另外，不要忘记选择“所有文件”作为文件名选项的一部分:\n\n```cpp\n$ nvcc -o aos_soa ./aos_soa.cu\n$ nvcc -o aos_soa_solved ./aos_soa_solved.cu\n$ nvprof --analysis-metrics --export-profile aos_soa.prof ./aos_soa\n$ nvprof --analysis-metrics --export-profile aos_soa_solved.prof ./aos_soa_solved\n```\n\n下面的截图显示了配置文件输出。这是一个使用 AOS 数据结构的幼稚实现:\n\n![](img/3d6e2a1c-0926-460e-bd5d-d9b72094e20b.png)\n\nThe preceding diagram shows the output of the profiler in guided analysis mode.\n\n您将看到的第一件事是探查器清楚地声明应用是受内存限制的。正如您所看到的，概要分析器不仅仅显示指标，还显示对这些指标含义的分析。在这个例子中，由于我们使用的是 AOS，分析器清楚地表明访问模式效率不高。但是编译器是怎么得出这个结论的呢？让我们看一下下面的截图，它给出了更多关于它的细节:\n\n![](img/90e14975-2add-439a-9772-65d594fed082.png)\n\n正如我们所看到的，它清楚地表明访问数据的理想事务数是 4，而运行是 32 个事务/访问。\n\n当我们将数据结构从 AOS 改变为 SOA 时，瓶颈就解决了。当您运行`aos_soa_solved`可执行文件时，您会看到内核时间减少了，这是对我们计时的改进。在 V100 16 GB 卡上，时间从 104 μs 减少到 47 μs，这是`2.2x`的加速因子。探查器输出`aos_soa_solved.prof`将显示内核仍然受内存限制，这一点非常明显，因为与执行计算相比，我们正在读写更多的内存数据。\n\n# 内存吞吐量分析\n\n对于应用开发人员来说，了解应用的内存吞吐量变得非常重要。这可以通过两种方式来定义:\n\n*   **从应用的角度来看:**计算应用请求的字节数\n*   **从硬件角度来看:**计算硬件移动的字节数\n\n这两个数字完全不同。造成这种情况的原因有很多，包括未完成的访问导致不是所有的事务字节都被利用，共享内存库冲突等等。我们应该从内存角度分析应用的两个方面如下:\n\n*   **地址模式:**确定真实代码中的访问模式相当困难，因此使用诸如 profilers 之类的工具变得非常重要。需要仔细查看探查器显示的指标，例如全局内存效率和每次访问的 L1/L2 事务。\n*   **正在进行的并发访问数量:**由于 GPU 是一种延迟隐藏架构，因此使内存带宽饱和变得非常重要。但是确定并发访问的数量通常是不够的。此外，从硬件角度来看，吞吐量与理论值相差甚远。\n\n下图表明，对于 Volta 架构，每个 SM 大约 6 KB 的飞行数据可以达到峰值带宽的 90%。同样的实验，在上一代架构上完成时，将产生不同的图形。一般来说，建议了解特定架构的 GPU 内存特性，以便从该硬件获得最佳性能:\n\n![](img/86177233-314d-430b-88d6-e81fedb8d916.png)\n\n本节为我们提供了全局内存的示例用法，以及我们如何以最佳方式利用它。有时，从全局内存访问合并数据很困难(例如，在计算流体力学领域，在非结构化网格的情况下，相邻单元的数据可能不会在内存中彼此相邻)。为了解决这样的问题或减少对性能的影响，我们需要使用另一种形式的内存，称为共享内存。\n\n# 共用存储器\n\n共享内存在被称为**用户管理缓存**的 CUDA 内存层次结构中一直扮演着至关重要的角色。这为用户提供了一种机制，以便他们可以从全局内存中以合并的方式读取/写入数据，并将其存储在内存中，这就像缓存一样，但可以由用户控制。在本节中，我们不仅将介绍利用共享内存可以采取的步骤，还将讨论如何有效地从共享内存加载/存储数据，以及如何在内存库内部安排数据。共享内存仅对同一块中的线程可见。一个块中的所有线程都看到同一个版本的共享变量。\n\n共享内存与中央处理器缓存有相似的好处；但是，虽然不能显式管理 CPU 缓存，但共享内存可以。共享内存的延迟比全局内存低一个数量级，带宽比全局内存高一个数量级。但是共享内存的关键用途来自于一个块中的线程可以共享内存访问。CUDA 程序员可以使用共享变量来保存在内核执行阶段多次重用的数据。此外，由于同一块中的线程可以共享结果，这有助于避免冗余计算。直到 9.0 版本，CUDA 工具包都没有在不同块中的线程之间提供可靠的通信机制。我们将在后续章节中更详细地介绍 CUDA 9.0 通信机制。现在，我们将假设线程之间的通信只可能在 CUDA 中通过使用共享内存来实现。\n\n# 共享内存上的矩阵转置\n\n用于演示共享内存的最原始的例子之一是矩阵转置。矩阵转置是一种受内存限制的操作。下面的代码片段使用`matrix_transpose_naive`内核，显示了矩阵转置内核的示例实现:\n\n```cpp\n__global__ void matrix_transpose_naive(int *input, int *output) {\n     int indexX = threadIdx.x + blockIdx.x * blockDim.x;\n     int indexY = threadIdx.y + blockIdx.y * blockDim.y;\n     int index = indexY * N + indexX;\n     int transposedIndex = indexX * N + indexY;\n     output[index] = input[transposedIndex];\n}\n```\n\n前面的代码展示了使用全局内存进行矩阵转置的简单实现。如果以简单的方式实现，这将导致在读取矩阵或写入矩阵时的非高级访问。内核在 V100 PCIe 16 GB 卡上的执行时间约为 60 μs。\n\n根据以下步骤配置您的环境:\n\n1.  准备好你的 GPU 应用。该代码可在`02_memory_overview/02_matrix_transpose`中找到。\n2.  用`nvcc`编译器编译你的应用，然后用`nvprof`编译器分析它。以下命令是这方面的`nvcc`命令的一个例子。然后，我们使用`nvprof`命令来分析应用。`--analysis-metrics`标志也被传递来获取内核的度量。\n\n3.  生成的配置文件，即`matrix_transpose.prof`，然后被加载到 NVIDIA 可视化配置文件中。用户需要从文件|打开菜单加载分析输出。另外，不要忘记选择“所有文件”作为文件名选项的一部分:\n\n```cpp\n$ nvcc -o matrix_transpose ./matrix_transpose.cu\n$ nvcc -o conflict_solved ./conflict_solved.cu\n$ nvprof --analysis-metrics --export-profile matrix_transpose.prof ./matrix_transpose\n$ nvprof --analysis-metrics --export-profile conflict_solved.prof ./conflict_solved\n```\n\n下面的截图显示了分析的输出。该输出明确指出，存在对全局内存的非高级访问，这是一个需要努力的关键指标，以便我们能够提高性能:\n\n![](img/44f9e83a-c02c-424b-b703-4f58cdb5f3a8.png)\n\n解决这个问题的一种方法是利用高带宽和低延迟的内存，例如共享内存。这里的技巧是以合并的方式从全局内存中读写。在这里，对共享内存的读或写可以是一种非高级模式。共享内存的使用带来了更好的性能，时间减少到 21 微秒，是时间加速的 3 倍:\n\n```cpp\n__global__ void matrix_transpose_shared(int *input, int *output) {\n\n    __shared__ int sharedMemory [BLOCK_SIZE] [BLOCK_SIZE];\n\n    //global index\n     int indexX = threadIdx.x + blockIdx.x * blockDim.x;\n     int indexY = threadIdx.y + blockIdx.y * blockDim.y;\n\n    //transposed global memory index\n     int tindexX = threadIdx.x + blockIdx.y * blockDim.x;\n     int tindexY = threadIdx.y + blockIdx.x * blockDim.y;\n\n    //local index\n     int localIndexX = threadIdx.x;\n     int localIndexY = threadIdx.y;\n     int index = indexY * N + indexX;\n     int transposedIndex = tindexY * N + tindexX;\n\n    //transposed the matrix in shared memory. \n    // Global memory is read in coalesced fashion\n     sharedMemory[localIndexX][localIndexY] = input[index];\n     __syncthreads();\n\n    //output written in global memory in coalesed fashion.\n     output[transposedIndex] = sharedMemory[localIndexY][localIndexX];\n}\n```\n\n前面的代码片段显示了使用共享内存实现矩阵转置。全局内存读/写合并，而转置发生在共享内存中。\n\n# 银行冲突及其对共享记忆的影响\n\n与使用全局内存相比，良好的加速并不一定意味着我们在有效地使用共享内存。如果我们查看探查器指标，这一点会变得更加清晰。如果我们将探查器输出的引导分析转换为非引导分析，即`matrix_transpose.prof`，我们将看到共享内存访问模式显示对齐问题，如下图所示:\n\n![](img/436d4870-5d3b-4251-8d60-cdf8e0fc4ad4.png)\n\n我们可以看到探查器如何显示共享内存的非最佳使用，这是存储体冲突的迹象。\n\n为了有效地理解这个对齐问题，理解*银行*的概念是很重要的。共享内存被组织成组，以实现更高的带宽。每家银行每个周期可以服务一个地址。内存可以提供与存储体一样多的同时访问。沃尔特图形处理器有 32 个存储体，每个存储体有 4 字节宽。当一个数组存储在共享内存中时，相邻的 4 字节字会进入连续的存储体，如下图所示:\n\n![](img/367a30e8-819e-4b7a-bda1-a1aa3212e9c4.png)\n\n上图中的逻辑视图显示了数据是如何存储在共享内存中的。\n\n翘曲中的线程对存储体的多次同时访问会导致存储体冲突。换句话说，当两个或更多线程访问同一存储体中不同的 4 字节字时，就会发生存储体冲突。逻辑上，这是当两个或多个线程访问同一存储体中不同的*行*时。下图显示了不同的 *n* 路银行冲突的示例。最坏的情况是 32 路冲突| 31 次重放–每次重放都会增加几个周期的延迟:\n\n![](img/7826d460-f142-4d1e-86bb-fdd80b74bcaa.png)\n\n前面的场景显示了来自同一经线的线程访问位于不同存储体中的相邻 4 字节元素，不会导致存储体冲突。请看下图:\n\n![](img/6b606f97-4c2f-46f1-8cdb-3f9f1b43d8f7.png)\n\n这是另一个无存储体冲突的场景，其中来自同一扭曲的线程访问驻留在不同存储体中的随机 4 字节元素，从而不会导致存储体冲突。由于共享内存中的双向存储体冲突而导致的顺序访问如下图所示:\n\n![](img/8b840222-c509-44e5-94df-5340ecdc9d65.png)\n\n上图显示了一个场景，其中来自同一扭曲的线程 **T0** 和 **T1** 访问驻留在同一存储体中的 4 字节元素，从而导致双向存储体冲突。\n\n在前面的矩阵转置的例子中，我们使用了共享内存来获得更好的性能。然而，我们可以看到一个 32 路的银行冲突。为了解决这个问题，可以使用一种称为填充的简单技术。所有这些都是在共享内存中填充一个虚拟内存，即一个额外的列，这将导致线程访问不同的内存库，从而获得更好的性能:\n\n```cpp\n__global__ void matrix_transpose_shared(int *input, int *output) {\n\n     __shared__ int sharedMemory [BLOCK_SIZE] [BLOCK_SIZE + 1];\n\n    //global index\n     int indexX = threadIdx.x + blockIdx.x * blockDim.x;\n     int indexY = threadIdx.y + blockIdx.y * blockDim.y;\n\n    //transposed index\n     int tindexX = threadIdx.x + blockIdx.y * blockDim.x;\n     int tindexY = threadIdx.y + blockIdx.x * blockDim.y;\n     int localIndexX = threadIdx.x;\n     int localIndexY = threadIdx.y;\n     int index = indexY * N + indexX;\n     int transposedIndex = tindexY * N + tindexX;\n\n    //reading from global memory in coalesed manner \n    // and performing tanspose in shared memory\n     sharedMemory[localIndexX][localIndexY] = input[index];\n\n    __syncthreads();\n\n    //writing into global memory in coalesed fashion \n    // via transposed data in shared memory\n     output[transposedIndex] = sharedMemory[localIndexY][localIndexX];\n}\n```\n\n前面的代码片段中，我们使用了`matrix_transpose_shared`内核，展示了填充的概念，这将消除存储体冲突，从而更好地利用共享内存带宽。像往常一样，运行代码并在可视化探查器的帮助下验证此行为。有了这些变化，您应该会看到内核的时间减少到 13 微秒，这是 60%的进一步加速。\n\n在这一节中，我们看到了如何最佳地利用共享内存，共享内存作为暂存区提供读写访问。但有时，数据只是只读输入，不需要写访问。在这种情况下，图形处理器提供了一个称为**纹理**内存的最佳内存。我们将在下一章中研究这一点，以及它为开发人员提供的其他优势。我们将在下一节讨论只读数据。\n\n# 只读数据/缓存\n\n根据内存名称，您可能已经猜到，只读缓存适合存储只读数据，并且在内核执行过程中不会改变。高速缓存为此目的进行了优化，并且基于 GPU 架构，释放并减少了另一个高速缓存上的负载，从而获得更好的性能。在这一节中，我们将详细介绍如何在图像处理代码示例的帮助下利用只读缓存来调整图像大小。\n\n只读数据对图形处理器网格中的所有线程都是可见的。该数据被标记为 GPU 的只读，这意味着对该数据的任何更改都将导致内核中未指定的行为。另一方面，中央处理器对这些数据既有读写权限。\n\n传统上，这个缓存也被称为纹理缓存。虽然用户可以显式调用纹理 API 来利用只读缓存，但使用最新的 GPU 架构，开发人员可以利用该缓存，而无需显式利用 CUDA 纹理 API。有了最新的 CUDA 版本和 Volta 等 GPU，标记为`const __restrict__`的内核指针参数就有资格成为遍历只读缓存数据路径的只读数据。开发人员也可以通过`__ldg`固有的缓存强制加载。\n\n当算法要求整个 warp 读取相同的地址/数据时，最好使用只读数据，这主要导致每个时钟周期向所有请求数据的线程广播。纹理缓存针对 2D 和 3D 局部性进行了优化。由于线程是同一扭曲的一部分，从具有 2D 和 3D 局部性的纹理地址读取数据往往会获得更好的性能。纹理已被证明在需要随机存储器访问的应用中非常有用，尤其是在 Volta 架构卡之前。\n\n纹理支持双线性和三线性插值，这对于图像处理算法(如缩放图像)特别有用。\n\n下图显示了位于 2D 空间的经线访问元素中的线的示例。纹理适合以下几种工作负载:\n\n![](img/b5bd2c1c-54a6-44ad-9bf1-fd533f1da8bc.png)\n\n现在，让我们来看一个关于缩放的小的真实世界算法，以演示纹理内存的使用。\n\n# 计算机视觉-使用纹理存储器的图像缩放\n\n我们将以图像缩放为例来演示纹理记忆的使用。下图显示了图像缩放的示例:\n\n![](img/fea97149-f4e0-4cad-b8a1-dfec2573fabc.png)\n\n图像缩放需要二维图像像素的插值。纹理提供了这两种功能(插值和对 2D 局部性的有效访问)，如果由全局内存直接访问，将导致不加掩饰的内存访问。\n\n根据以下步骤配置您的环境:\n\n1.  准备好你的 GPU 应用。这个代码可以在`02_memory_overview/03_image_scaling`找到。\n2.  使用以下命令，使用`nvcc`编译器编译您的应用:\n\n```cpp\n$nvcc -c scrImagePgmPpmPackage.cpp \n$nvcc -c image_scaling.cu\n$nvcc -o image_scaling image_scaling.o scrImagePgmPpmPackage.o\n```\n\n`scrImagePgmPpmPackage.cpp`文件包含读写扩展名为`.pgm`的图像的源代码。纹理代码存在于`image_scaling.cu`中。\n\nFor viewing the `pgm` files users can make use of viewers like IrfanView ([https://www.irfanview.com/main_download_engl.htm](https://www.irfanview.com/main_download_engl.htm)) which are free to use.\n\n首先，我们需要四个步骤来利用纹理内存:\n\n1.  声明纹理内存。\n2.  将纹理内存绑定到纹理引用。\n3.  使用 CUDA 内核中的纹理引用读取纹理内存。\n4.  从你的纹理参考中解除纹理记忆。\n\n下面的代码片段显示了我们可以用来利用纹理内存的四个步骤。从开普勒图形处理器架构和 CUDA 5.0 开始，引入了一个新特性，称为无绑定纹理。这暴露了纹理对象，它基本上是一个 C++ 对象，可以传递给 CUDA 内核。它们被称为无绑定，因为它们不需要手动绑定/解除绑定，早期的 GPU 和 CUDA 版本就是这种情况。纹理对象是使用`cudaTextureObject_t`类 API 声明的。让我们现在来看看这些步骤:\n\n1.  首先，声明纹理内存:\n\n```cpp\ntexture<unsigned char, 2, cudaReadModeElementType> tex;\n```\n\n创建一个通道描述，当我们链接到纹理时将使用它:\n\n```cpp\ncudaArray* cu_array;\ncudaChannelFormatKind kind = cudaChannelFormatKindUnsigned;\ncudaChannelFormatDesc channelDesc = cudaCreateChannelDesc(8, 0, 0, 0, kind);\n```\n\n2.  然后，指定纹理对象参数:\n\n```cpp\nstruct cudaTextureDesc texDesc;\nmemset(&texDesc, 0, sizeof(texDesc)); \n//set the memory to zero\ntexDesc.addressMode[0] = cudaAddressModeClamp; \n// setting the x dimension addressmode to Clamp\ntexDesc.addressMode[1] = cudaAddressModeClamp; \n//Setting y dimension addressmode to Clamp\ntexDesc.filterMode = cudaFilterModePoint; \n// Filter mode set to Point\ntexDesc.readMode = cudaReadModeElementType; \n// Reading element type and not interpolated\ntexDesc.normalizedCoords = 0;\n```\n\n3.  接下来，从 CUDA 内核中的纹理引用中读取纹理内存:\n\n```cpp\nimageScaledData[index] = tex2D<unsigned char>(texObj,(float)(tidX*scale_factor),(float)(tidY*scale_factor));\n```\n\n4.  最后，破坏纹理对象:\n\n```cpp\ncudaDestroyTextureObject(texObj);\n```\n\n纹理内存的重要方面类似于配置，由开发人员设置，如下所示:\n\n*   **纹理维度:**这定义了纹理是作为 1D、2D 还是三维阵列来处理的。纹理中的元素也被称为纹理元素。深度、宽度和高度也设置为定义每个尺寸。请注意，每个 GPU 架构都为每个维度定义了可接受的最大大小。\n*   **纹理类型:**这根据它是基本整数还是浮点纹理元素来定义大小。\n*   **纹理读取模式:**纹理的读取模式定义了元素的读取方式。它们可以以`NormalizedFloat`或`ModeElement`格式读取。对于无符号整数和有符号整数类型，规范化浮点模式要求索引在[0.0 1.0]和[-1.0 1.0]范围内。\n*   **纹理寻址模式:**纹理的一个独特的特性是它如何处理超出范围的访问。这听起来可能不寻常，但事实上，在许多成像算法中非常常见。例如，如果您通过平均相邻像素来应用插值，边界像素的行为应该是什么？纹理为开发人员提供了一个选项，以便他们可以选择是将超出范围的部分视为夹紧、包裹还是镜像。在调整大小的例子中，我们将其设置为箝位模式，这基本上意味着超出范围的访问被箝位到边界。\n*   **纹理过滤模式:**设置该模式定义了获取纹理时返回值的计算方式。支持两种过滤模式:`cudaFilterModePoint`和`cudaFilterModeLinear`。当设置为线性模式时，插值是可能的(1D 为简单线性，2D 为双线性，三维为三线性)。线性模式仅在返回类型为浮点类型时有效。`ModePoint`则不执行插值，而是返回最近坐标的纹理元素。\n\nThe key intention of introducing texture memory in this section is to provide you with an example of its usage and to show you where texture memory is useful. It provides a good overview of the different configuration parameters. Please refer to the CUDA API guide ([https://docs.nvidia.com/cuda/cuda-runtime-api/index.html](https://docs.nvidia.com/cuda/cuda-runtime-api/index.html)) for more information.\n\n在本节中，我们通过一个例子描述了使用纹理内存的目的。在下一节中，我们将了解可用的最快(最低延迟)GPU 内存(寄存器)。与中央处理器相比，这在图形处理器中大量存在。\n\n# 图形处理器中的寄存器\n\n中央处理器和图形处理器架构之间的一个根本区别是，与中央处理器相比，图形处理器中有大量的寄存器。这有助于线程将大部分数据保存在寄存器中，从而减少上下文切换的延迟。因此，优化这种记忆也很重要。\n\n寄存器有一个单线程的范围。为网格中所有启动的线程创建变量的私有副本。每个线程都可以访问其私有的变量副本，而其他线程的私有变量不能被访问。例如，如果一个内核用 1000 个线程启动，那么一个作用域为线程的变量会得到它自己的变量副本。\n\n声明为内核一部分的局部变量存储在寄存器中。中间值也存储在寄存器中。每个 SM 都有一组固定的寄存器。在编译期间，编译器(`nvcc`)试图找到每个线程的最佳寄存器数量。在寄存器数量不足的情况下(这通常发生在 CUDA 内核较大且具有大量局部变量和中间计算的情况下)，数据被推送到本地内存，该内存可能位于 L1/L2 缓存中，或者甚至位于内存层次结构的较低位置，例如全局内存。这也称为寄存器溢出。每个线程的寄存器数量对于 SM 上有多少块和线程是活动的起着重要作用。下一章将详细介绍这一概念，其中有一节专门讨论占用问题。一般来说，建议不要声明大量不必要的局部变量。如果寄存器限制了 SM 上可以调度的线程数量，那么开发人员应该考虑通过将内核分成两个——或者更多，如果可能的话——来重组代码。\n\n声明为`vecAdd`内核一部分的变量存储在寄存器存储器中。传递给内核的参数，即`A`、`B`和`C`，指向全局内存，但变量本身存储在基于 GPU 架构的寄存器共享内存中。下图显示了 UDA 内存层次结构和不同变量类型的默认位置:\n\n![](img/4063603e-0e98-42d7-9a61-5353c9dfb82d.png)\n\n到目前为止，我们已经看到了关键内存层次结构(全局、纹理、共享和寄存器)的用途和最佳用法。在下一节中，我们将了解 GPU 内存的一些优化和功能，这些优化和功能可以提高应用的性能，并提高开发人员编写 CUDA 程序时的工作效率。\n\n# 固定存储器\n\n现在是时候回忆一下数据所采取的路径了，也就是说，从中央处理器内存到图形处理器寄存器，这些寄存器最终被图形处理器内核用于计算。即使图形处理器具有更高的计算性能和更高的内存带宽，由于中央处理器内存和图形处理器内存之间的传输，应用获得的加速的整体优势也可以正常化。这种数据传输是通过总线/链路/协议进行的，例如 PCIe(对于英特尔和 AMD 的 CPU 架构)或 NVLink(对于 OpenPower Foundation 的`power`等 CPU 架构)。\n\n为了克服这些瓶颈，建议采用以下技巧/指南:\n\n*   首先，建议尽可能减少主机和设备之间传输的数据量。这甚至可能意味着将一部分顺序代码作为内核在 GPU 上运行，从而与在主机 CPU 上顺序运行它们相比，几乎没有加速。\n*   其次，利用固定内存在主机和设备之间实现更高的带宽非常重要。\n*   建议将小批量转移到一个大批量转移中。这有助于减少调用数据传输 CUDA 应用编程接口所涉及的延迟，根据系统的配置，延迟可能从几微秒到几毫秒不等。\n*   最后，应用可以利用异步数据传输将内核执行与数据传输重叠。\n\n在这一节中，我们将更详细地介绍固定内存传输。异步传输将在[第 4 章](04.html)、*内核执行模型和优化策略*中有更详细的介绍，我们将利用一个叫做 CUDA 流的概念。\n\n# 带宽测试–固定与可分页\n\n默认情况下，称为`malloc()`的内存分配应用编程接口分配可分页的内存类型。这意味着，如果需要，映射为页面的内存可以被其他应用或操作系统本身换出。因此，大多数设备，包括图形处理器和其他设备，如同样位于 PCIe 总线上的 InfiniBand，都希望在传输之前锁定内存。默认情况下，图形处理器不会访问可分页内存。因此，当调用内存传输时，CUDA 驱动程序分配临时固定内存，将数据从默认可分页内存复制到该临时固定内存，然后通过**设备内存控制器** ( **DMA** )将其传输到设备。\n\n这个额外的步骤不仅增加了延迟，而且有机会将请求的页面传输到 GPU 内存，该页面已被交换，需要带回 GPU 内存。\n\n为了理解使用固定内存的影响，让我们试着编译并运行一段示例代码。这是作为统一数据自动化系统样本的一部分提供的。根据以下步骤配置您的环境:\n\n1.  准备好你的 GPU 应用。该代码出现在`<CUDA_SAMPLES_DIR>/1_Utilities/bandwidthTest`中。\n2.  使用`make`命令编译您的应用。\n3.  以两种模式运行可执行文件，即`pageable`和`pinned`，如下所示:\n\n```cpp\n$make\n$./bandwidthTest --mode=shmoo --csv --memory=pageable > pageable.csv\n$./bandwidthTest --mode=shmoo --csv --memory=pinned >  pinned.csv\n```\n\nNote that `CUDA_SAMPLES_DIR` is the path to the directory where the CUDA installation has been placed.\n\n正如我们所看到的，与前面的代码相比，关键的变化是我们到目前为止编写的是一个数据分配 API。下面的代码片段显示了使用`cudaMallocHost`应用编程接口而不是`malloc`分配内存:\n\n```cpp\ncudaError_t status = cudaMallocHost((void**)&h_aPinned, bytes);\nif (status != cudaSuccess)\n printf(\"Error allocating pinned host memory\\n\");\n```\n\n`cudaMallocHost` API 使内存成为固定内存，而不是可分页内存。虽然分配 API 发生了变化，但我们仍然可以使用相同的数据传输 API，即`cudaMemcpy()` *。*现在，重要的问题是，*这是什么固定内存，为什么它能提供更好的带宽？*我们将在下一节介绍这一点。\n\n从带宽测试的输出可以看出对性能的影响。我们已经把结果绘制成图表，这样你就可以很容易地理解影响。 *x* 轴显示以千字节为单位传输的数据，而 *y* 轴显示以兆字节/秒为单位实现的带宽。\n\n第一个图是**主机到设备**的传输，而第二个图是**设备到主机**的传输。首先你会看到，可以达到的最大带宽是~ 12gb/秒。PCIe 第三代的理论带宽为 16gb/秒，但可实现的带宽在 12gb/秒的范围内。可实现的带宽在很大程度上取决于系统(主板、中央处理器、PCIe 拓扑等):\n\n![](img/e309df71-3726-487e-a095-dcdda648b79b.jpg)\n\n如您所见，对于固定内存，带宽对于较低的传输大小总是较高的，而可分页内存带宽在较高的数据大小传输时变得相等，因为驱动程序和 DMA 引擎开始通过应用重叠等概念来优化传输。尽管建议利用固定记忆，但过度使用也有不利的一面。为应用分配固定的整个系统内存会降低整体系统性能。发生这种情况是因为它会占用可用于其他应用和操作系统任务的页面。应该固定的正确大小非常依赖于应用和系统，对此没有现成的公式。我们能做的最好的事情是在可用的系统上测试应用，并选择最佳的性能参数。\n\n此外，重要的是要理解，新的互连，如 NVLink，为受这些数据传输约束的应用提供了更高的带宽和更低的延迟。目前，中央处理器和图形处理器之间的非易失性链路仅由电源中央处理器提供。\n\n在本节中，我们研究了如何提高中央处理器和图形处理器之间的数据传输速度。我们现在将继续利用 CUDA 的一个新特性，称为统一内存，这有助于提高开发人员编写 CUDA 程序的生产率。\n\n# 统一内存\n\n随着每一个新的 CUDA 和 GPU 架构的发布，新的功能被添加进来。这些新特性提供了更高的性能和更容易的编程，或者允许开发人员实现新的算法，否则就不可能使用 CUDA 移植到图形处理器上。从 CUDA 6.0 开始发布并在开普勒 GPU 架构中实现的一个重要特性是统一内存。在本章中，我们将统一内存称为 UM。\n\n简单来说，UM 为用户提供了一个单一内存空间的视图，系统中的所有图形处理器和中央处理器都可以访问这个空间。下图说明了这一点:\n\n![](img/768c31b3-add3-4a60-a38e-34966eb35f1b.jpg)\n\n在这一节中，我们将介绍如何利用 UM，优化它，并强调利用它的主要优势。与全局内存访问一样，如果以非高级方式进行，会导致性能下降，而 UM 功能如果使用不当，则会导致应用的整体性能下降。我们将采取循序渐进的方法，从一个简单的程序开始，并在此基础上进行构建，以便我们能够理解 UM 及其对性能的影响。\n\n让我们试着编译并运行一些示例代码。根据以下步骤配置您的环境:\n\n1.  准备好你的 GPU 应用。这个代码可以在`02_memory_overview/unified_memory`找到。\n2.  使用以下`nvcc`命令编译您的应用:\n\n```cpp\n$nvcc -o unified_simple.out unified_memory.cu\n$nvcc -o unified_initialized.out unified_memory_initialized.cu\n$nvcc -o unified_prefetch.out unified_memory_prefetch.cu\n$nvcc -o unified_64align.out unified_memory_64align.cu\n```\n\n请注意，本部分显示的结果是针对特斯拉 P100 卡的。同样的代码，当在开普勒等其他架构上运行时，预计会给出不同的结果。本节的重点是最新的架构，如帕斯卡和沃尔特。\n\n# 了解统一内存页面分配和传输\n\n让我们从 UM 的天真实现开始。第一段代码`unified_memory.cu`演示了这个概念的基本用法。代码的主要变化是使用了`cudaMallocManaged()`应用编程接口，而不是使用`malloc`分配内存，如下面的代码片段所示:\n\n```cpp\nfloat *x, *y;\nint size = N * sizeof(float);\n...\ncudaMallocManaged(&x, size);\ncudaMallocManaged(&y, size);\n...\n\n for (int ix = 0; ix < N; ix++) {\n    x[ix] = rand()%10;\n    y[ix] = rand()%20;\n  }\n...\n\n add<<<numBlocks, blockSize>>>(x, y, N);\n```\n\n仔细看源代码，会发现`x`和`y`变量只分配一次，指向统一内存。相同的指针被发送到图形处理器`add<<<>>>()`内核，并使用`for`循环在中央处理器中进行初始化。这对程序员来说非常简单，因为他们不需要跟踪指针指向的是中央处理器内存还是图形处理器内存。但这是否一定意味着我们从中获得了良好的性能或传输速度？不一定，所以让我们试着通过分析这段代码来深入挖掘，如下面的截图所示:\n\n![](img/d9df1195-6123-4c69-bc00-fd5ef9fc642e.jpg)\n\n我们使用以下命令来获取分析输出:\n\n```cpp\n$ nvprof ./unified_simple.out\n```\n\n不出所料，大部分时间都在`add<<<>>>`内核中度过。让我们尝试从理论上计算带宽。我们将使用以下公式计算带宽:\n\n*带宽=字节/秒= (3 * 4，194，304 字节* 1e-9 字节/GB) / 2.6205e-3s = 5 GB/s*\n\n如您所见，P100 提供的理论带宽为 720 GB/s，而我们只能达到 5 GB/s，这确实很差。你可能想知道为什么我们只计算内存带宽。这样做的原因是，应用是受内存限制的，因为它完成了三次内存操作，并且只有一次添加。因此，只专注于这方面是有意义的。\n\n从帕斯卡卡开始，`cudaMallocManaged()`不分配物理内存，而是基于第一次触摸分配内存。如果 GPU 第一次接触变量，页面将在 GPU 页面表中进行分配和映射；否则，如果 CPU 第一次接触变量，它将被分配并映射到 CPU。在我们的代码中，`x`和`y`变量在中央处理器中用于初始化。因此，页面被分配给中央处理器。在`add<<<>>>`内核中，当访问这些变量时，会出现页面错误，页面迁移的时间会被添加到内核时间中。这是内核时间高的根本原因。现在，让我们深入探讨页面迁移的步骤。\n\n页面迁移中完成的操作顺序如下:\n\n1.  首先，我们需要在 GPU 和 CPU 上分配新页面(第一次接触的基础上)。如果页面不存在并映射到另一个页面，则会出现设备页面表页面错误。当位于**第 2 页**的 ***x** 在当前映射到 CPU 内存的 GPU 中被访问时，会出现页面错误。请看下图:\n\n![](img/1870160e-d6f8-4d3a-92e4-ff861290d337.png)\n\n2.  在下一步中，将取消 CPU 上旧页面的映射，如下图所示:\n\n![](img/8535b1f3-68ac-46a9-bc94-a6b16b9c50c1.jpg)\n\n3.  接下来，数据从中央处理器复制到图形处理器，如下图所示:\n\n![](img/016530df-0a4d-4be0-a8b8-3fd2437925be.jpg)\n\n4.  最后，新页面映射到 GPU 上，而旧页面在 CPU 上释放，如下图所示:\n\n![](img/c0655a9b-0339-4ca4-a6a2-f39c32608523.jpg)\n\nGPU 中的**转换后备缓冲器** ( **TLB** )与 CPU 非常相似，执行从物理地址到虚拟地址的地址转换。当出现页面错误时，相应 SM 的 TLB 被锁定。这基本上意味着新指令将被停止，直到执行前面的步骤并最终解锁 TLB。这对于在 SM 中保持一致性和保持一致的内存视图状态是必要的。驱动程序负责删除这些重复项、更新映射和传输页面数据。正如我们前面提到的，所有这些时间都被添加到总内核时间中。\n\n所以，我们现在知道问题了。不过，解决办法是什么？为了解决这个问题，我们将使用两种方法:\n\n*   首先，我们将在 GPU 上创建一个初始化内核，以便在`add<<<>>>`内核运行期间没有页面错误。然后，我们将利用每页扭曲的概念来优化页面错误。\n*   我们将预取数据。\n\n我们将在接下来的章节中介绍这些方法。\n\n# 通过每页扭曲优化统一内存\n\n让我们从第一种方法开始，这就是初始化内核。如果您看一下`unified_memory_initialized.cu`文件中的源代码，我们在那里添加了一个名为`init<<<>>>`的新内核，如下代码所示:\n\n```cpp\n__global__ void init(int n, float *x, float *y) {\n int index = threadIdx.x + blockIdx.x * blockDim.x;\n int stride = blockDim.x * gridDim.x;\n for (int i = index; i < n; i += stride) {\n   x[i] = 1.0f;\n   y[i] = 2.0f;\n  }\n}\n```\n\n通过添加一个内核来初始化 GPU 本身中的数组，页面被分配并映射到 GPU 内存，因为它们首先在`init<<<>>>`内核中被触摸。让我们看看这段代码的分析结果输出，其中显示了使用初始化内核分析输出:\n\n![](img/29bcc42e-cf74-4f62-98c5-f5c47a533c79.jpg)\n\n我们使用了以下命令来获取分析输出\n\n```cpp\nnvprof ./unified_initialized.out\n```\n\n如您所见，`add<<<>>>`内核的时间减少到了 18 μs。这有效地为我们提供了以下内核带宽:\n\n*带宽=字节/秒= (3 * 4，194，304 字节* 1e-9 字节/GB) / 18.84e-6s = 670 GB/s*\n\n这种带宽是您在非统一内存场景中所期望的。从前面截图中的简单实现可以看出，在分析输出中没有主机到设备行。但是，您可能已经看到，即使`add<<<>>>`内核时间减少了，但是`init<<<>>>`内核并没有成为占用最大时间的热点。这是因为我们第一次触摸到了记忆中的`init<<<>>>`内核。此外，您可能想知道这些 GPU 故障组是什么。正如我们之前所讨论的，单个页面错误可以分组在一起，以根据试探法以及访问模式来提高带宽。为了更深入地了解这一点，让我们用`--print-gpu-trace`重新编写代码，这样我们就可以看到单个页面错误。正如您可以看到下面的截图，GPU 跟踪显示了故障的整体跟踪和发生故障的虚拟地址:\n\n![](img/8c5e2708-82dc-44ee-a680-74bbd6a305e1.jpg)\n\n我们使用以下命令来获取分析输出:\n\n```cpp\n$ nvprof --print-gpu-trace ./unified_initialized.out\n```\n\n第二行显示了同一页面的 11 个页面错误。正如我们前面所讨论的，驱动程序的作用是过滤这些重复的错误，并且只传输每个页面一次。在复杂的访问模式中，通常驱动程序没有足够的信息来说明哪些数据可以迁移到 GPU。为了改善这种情况，我们将进一步实现每页扭曲的概念，这基本上意味着每个扭曲将访问相同页面中的元素。这需要开发人员付出额外的努力。让我们重新实现`init<<<>>>`内核。您可以在我们之前编译的`unified_memory_64align.cu`文件中看到这个实现。下面的代码片段显示了内核的快照:\n\n```cpp\n#define STRIDE_64K 65536\n__global__ void init(int n, float *x, float *y) {\n  int lane_id = threadIdx.x & 31;\n  size_t warp_id = (threadIdx.x + blockIdx.x * blockDim.x) >> 5;\n  size_t warps_per_grid = (blockDim.x * gridDim.x) >> 5;\n  size_t warp_total = ((sizeof(float)*n) + STRIDE_64K-1) / STRIDE_64K;\n  for(; warp_id < warp_total; warp_id += warps_per_grid) {\n    #pragma unroll\n    for(int rep = 0; rep < STRIDE_64K/sizeof(float)/32; rep++) {\n      size_t ind = warp_id * STRIDE_64K/sizeof(float) + rep * 32 + lane_id;\n      if (ind < n) {\n        x[ind] = 1.0f;\n        y[ind] = 2.0f;\n      }\n    }\n  }\n}\n```\n\n内核显示索引基于`warp_id`。GPU 中的 warp 大小为 32，负责在 64 KB 范围的索引内填充`x`和`y`变量，也就是说，warp 1 负责前 64 KB，而 warp 2 负责后 64 KB 的元素。经纱循环(最里面的`for`循环)中的每个纱线在相同的 64 KB 内填充索引。让我们看看这段代码的分析结果。从下面截图的剖析输出中我们可以看到，`init<<<>>>`内核的时间减少了，GPU 故障组也大大减少了:\n\n![](img/3566af1c-8117-4300-a9ca-f6881d8e16f8.jpg)\n\n我们可以通过运行`--print-gpu-trace`分析器来再次确认这一点:\n\n```cpp\n$ nvprof --print-gpu-trace ./unified_64align.out\n```\n\n下面的截图清楚地显示了每页的 GPU 页面错误已经减少:\n\n![](img/4a798b4b-3c10-4012-917a-edf54eab358a.jpg)\n\n# 使用数据预取优化统一内存\n\n现在，让我们看看一种更简单的方法，称为数据预取。CUDA 的一个关键是它为开发者提供了不同的方法，从最简单的方法开始，到需要忍者编程技能的方法。**数据预取**基本上是提示驱动程序在设备使用之前预取我们认为将在设备中使用的数据。CUDA 为此提供了一个名为`cudaMemPrefetchAsync()`的预取 API。要看它的实现，我们先来看看我们之前编译的`unified_memory_prefetch.cu`文件。下面的代码片段显示了这段代码的快照:\n\n```cpp\n// Allocate Unified Memory -- accessible from CPU or GPU\n cudaMallocManaged(&x, N*sizeof(float));  cudaMallocManaged(&y, N*sizeof(float));\n// initialize x and y arrays on the host\n for (int i = 0; i < N; i++) {  x[i] = 1.0f;  y[i] = 2.0f;  } \n//prefetch the memory to GPU\ncudaGetDevice(&device);\ncudaMemPrefetchAsync(x, N*sizeof(float), device, NULL);\ncudaMemPrefetchAsync(y, N*sizeof(float), device, NULL); \n...\n add<<<numBlocks, blockSize>>>(N, x, y);\n//prefetch the memory to CPU\n cudaMemPrefetchAsync(y, N*sizeof(float), cudaCpuDeviceId, NULL);\n // Wait for GPU to finish before accessing on host\n cudaDeviceSynchronize();\n...\nfor (int i = 0; i < N; i++)\n maxError = fmax(maxError, fabs(y[i]-3.0f));\n\n```\n\n代码非常简单，并且可以自己解释。这个概念相当简单:在已知特定设备将使用什么内存的情况下，可以预取内存。让我们看一下分析结果，如下面的截图所示。\n\n我们可以看到，`add<<<>>>`内核提供了我们期望它提供的带宽:\n\n![](img/5a07082c-4e58-4946-9ba6-764b41041553.jpg)\n\n统一内存是一个不断发展的特性，随着每个 CUDA 版本和 GPU 架构的发布而变化。希望大家通过访问最新的 CUDA 编程指南([https://docs . NVIDIA . com/CUDA/CUDA-c-programming-guide/index . html # um-unified-memory-programming-HD](https://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html#um-unified-memory-programming-hd))随时了解情况。\n\n到目前为止，我们已经看到了 UM 概念的有用性，它不仅提供了编程的便利性(没有使用 CUDA API 显式管理内存)，而且在移植应用时更加强大和有帮助，否则这些应用要么无法在 GPU 上移植，要么太难移植。使用 UM 的一个主要优势是超额预订。与 CPU 内存相比，GPU 内存相当有限。最新的图形处理器(Volta 卡 V100)提供每个图形处理器最大 32 GB。在 UM 的帮助下，多块 GPU 内存和 CPU 内存可以看作一个大内存。比如 NVIDIA DGX2 机器，它的 16 Volta GPU 为 323 GB，可以看作是 GPU 内存的集合，最大容量为 512 GB。对于计算流体力学**(**计算流体力学**)和分析等应用来说，这些技术的优势是巨大的。以前，在图形处理器内存中很难容纳问题大小的地方，现在可以了。手工移动碎片容易出错，需要调整内存大小。**\n\n **此外，高速互连(如 NVLink 和 NVSwitch)的出现允许在具有高带宽和低延迟的 GPU 之间进行快速传输。使用统一内存，您实际上可以获得高性能！\n\n数据预取与指定数据实际驻留位置的提示相结合，有助于需要同时访问相同数据的多个处理器。在这种情况下使用的应用编程接口名称是`cudaMemAdvice()`。因此，通过彻底了解您的应用，您可以利用这些提示来优化访问。如果您希望覆盖一些驱动程序试探法，这些也很有用。API 目前采纳的一些建议如下:\n\n*   `cudaMemAdviseSetReadMostly`:顾名思义，这意味着数据大部分是只读的。驱动程序创建数据的只读副本，从而减少页面错误。需要注意的是，数据仍然可以写入。在这种情况下，除了写入内存的设备之外，页面副本将失效:\n\n```cpp\n// Sets the data readonly for the GPU\ncudaMemAdvise(data, N, ..SetReadMostly, processorId); \nmykernel<<<..., s>>>(data, N); \n```\n\n*   `cudaMemAdviseSetPreferredLocation`:此建议将数据的首选位置设置为属于设备的内存。设置首选位置不会导致数据立即迁移到该位置。像下面的代码一样，`mykernel<<<>>>`将页面出错，并生成到 CPU 上数据的直接映射。驱动程序试图使用`cudaMemAdvise`阻止将数据从设置的首选位置迁移出去:\n\n```cpp\ncudaMemAdvise(input, N, ..PreferredLocation, processorId); \nmykernel<<<..., s>>>(input, N); \n```\n\n*   `cudaMemAdviseSetAccessedBy`:此建议暗示数据将被设备访问。该设备将在 CPU 内存中创建输入的直接映射，并且不会产生页面错误:\n\n```cpp\ncudaMemAdvise(input, N, ..SetAccessedBy, processorId); \nmykernel<<<..., s>>>(input, N); \n```\n\n在下一节中，我们将从整体的角度来看 GPU 中不同的内存是如何随着更新的架构而发展的。\n\n# 图形处理器内存进化\n\nGPU 架构随着时间的推移而发展，内存架构也发生了很大变化。如果我们看一下过去四代人，就会发现一些常见的模式，其中一些如下:\n\n*   总的来说，记忆容量在增加。\n*   新一代架构增加了内存带宽和容量。\n\n下表显示了过去四代的属性:\n\n| **记忆类型** | **属性** | **转 V100** | **帕斯卡 P100** | **麦克斯韦 M60** | **开普勒 K80** |\n| **注册** | 每个 SM 的大小 | 256 千字节 | 256 千字节 | 256 千字节 | 256 千字节 |\n| **L1** | 大小 | 32...128 KiB | 24 KiB | 24 KiB | 16...48 KiB |\n| 管道尺寸 | Thirty-two | 32 B | 32 B | 128 B |\n| **L2** | 大小 | 6144 KiB | 4，096 KiB | 2，048 KiB | 1，536 Kib |\n| 管道尺寸 | 64 B | 32B | 32B | 32B |\n| **共享内存** | 每个 SMX 的尺寸 | 高达 96 KiB | 64 KiB | 64 KiB | 48 KiB |\n| 每个图形处理器的大小 | 高达 7，689 KiB | 3，584 KiB | 1，536 KiB | 624 KiB |\n| 理论带宽 | 每秒 13，800 次 | 每秒 9.519 次 | 每秒 2，410 次 | 每秒 2，912 次 |\n| **全局内存** | 存储总线 | HBM2 | HBM2 | GDDR5 | GDDR5 |\n| 大小 | 32，152 MiB | 16，276 MiB | 8，155 MiB | 12，237 MiB |\n| 理论带宽 | 900 fps | 732 fps | 160 fps | 240 fps |\n\n总的来说，前面的观察有助于 CUDA 应用在较新的架构下运行得更快。但与此同时，CUDA 编程模型以及内存架构也发生了一些根本性的变化，让 CUDA 程序员的生活变得更加轻松。我们观察到的一个变化是纹理内存，在 CUDA 5.0 之前，开发人员必须手动绑定和解除绑定纹理，并且必须全局声明。对于 CUDA 5.0，没有必要这样做。它还取消了开发人员在应用中可以拥有的纹理引用数量的限制。\n\n我们还研究了 Volta 架构以及为简化开发人员的编程而进行的一些基本更改。Volta 中的总容量为 128 KB/SM，是其上一代卡 Pascal P100 的 7 倍，这使得开发人员可以使用更大的缓存。此外，由于 Volta 体系结构中的 L1 缓存因统一而具有更少的延迟，这使得它可以高带宽、低延迟地访问频繁重用的数据。这样做的主要原因是允许 L1 缓存操作获得共享内存性能的好处。共享内存的关键问题是它需要由开发人员明确控制。当与 Volta 等较新的架构一起工作时，这就变得不那么必要了。然而，这并不意味着共享内存变得多余。想要提取每一寸性能的忍者程序员仍然更喜欢使用共享内存，但许多其他应用不再需要这种专业知识。Pascal 和 Volta L1 缓存和共享内存之间的区别如下图所示:\n\n![](img/675c799b-6de1-445d-affd-9ddd781c90f8.jpg)\n\n上图显示了与 Pascal 相比共享内存和 L1 缓存的统一。重要的是要理解，CUDA 编程模型从一开始就几乎保持不变。即使内存的容量、带宽或延迟随着每个架构而变化，相同的 CUDA 代码也将在所有架构上运行。然而，肯定会改变的是这些架构变化对性能的影响。例如，由于 L1 和共享内存的统一，在 Volta 之前使用共享内存的应用在 Volta 中可能看不到如此高的性能提升。\n\n# 为什么 GPU 有缓存？\n\n在这个演变过程中，同样重要的是要明白，CPU 和 GPU 缓存是非常不同的，服务于不同的目的。作为 CUDA 架构的一部分，我们通常为每个 SM 启动数百到数千个线程。数万个线程共享 L2 缓存。因此，L1 和 L2 的人均收入很低。例如，在 2048 个线程/80sm 的情况下，每个线程在 L1 只能获得 64 个字节，在 L2 只能获得 38 个字节。GPU 中的缓存缓存由许多线程访问的公共数据。这有时被称为空间局部性。一个典型的例子是当线程的访问不一致和不规则时。由于中央处理器高速缓存主要用于时间局部性，因此图形处理器高速缓存有助于减少寄存器溢出和本地内存的影响。\n\n# 摘要\n\n本章首先介绍了不同类型的 GPU 内存。我们详细讨论了全局、纹理、共享内存以及寄存器。我们还研究了图形处理器的内存进化提供了哪些新功能，例如统一内存，这有助于提高程序员的工作效率。我们看到了这些特性是如何在最新的 GPU 架构中实现的，例如 Pascal 和 Volta。\n\n在下一章中，我们将深入探讨 CUDA 线程编程的细节，以及如何以最佳方式启动不同的线程配置，以获得 GPU 硬件的最佳性能。我们还将引入新的 CUDA 工具包功能，例如用于灵活线程编程和 GPU 多精度编程的协作组。**"
  },
  {
    "path": "docs/learn-cuda-prog/03.md",
    "content": "# 三、线程编程\n\nCUDA 有一个分层的线程架构，这样我们可以分组控制 CUDA 线程。了解它们如何在 GPU 上并行工作有助于您编写并行编程代码并获得更好的性能。在本章中，我们将介绍 CUDA 线程操作及其与 GPU 资源的关系。作为实践经验，我们将研究并行约简算法，看看如何通过使用优化策略来优化 CUDA 代码。\n\n在本章中，您将了解 CUDA 线程如何在 GPU 中运行:并行和并发线程执行、扭曲执行、内存带宽问题、控制开销、SIMD 操作等。\n\n本章将涵盖以下主题:\n\n*   分层 CUDA 线程操作\n*   了解 CUDA 占用情况\n*   跨多个 CUDA 线程的数据共享\n*   识别应用的性能限制\n*   最小化 CUDA 翘曲发散效应\n*   提高内存利用率和网格跨步循环\n*   灵活线程处理的协作组\n*   翘曲同步编程\n*   低/混合精度操作\n\n# 技术要求\n\n本章建议使用比帕斯卡架构更晚的 NVIDIA GPU 卡。换句话说，你的图形处理器的计算能力应该等于或大于 60。如果您不确定自己的 GPU 架构，请访问位于[https://developer.nvidia.com/cuda-gpus](https://developer.nvidia.com/cuda-gpus)的英伟达 GPU 网站，确认您的 GPU 计算能力。\n\n当我们写这本书时，示例代码是用 10.1 开发和测试的。一般来说，如果适用，建议使用最新的 CUDA 版本。\n\n在本章中，我们将通过分析代码来执行 CUDA 编程。如果你的 GPU 架构是图灵，建议安装 Nsight Compute 来剖析代码。免费，可以从[https://developer.nvidia.com/nsight-compute](https://developer.nvidia.com/nsight-compute)下载。当我们写这本书时，这是侧写员的一个过渡时刻。您可以在[第 5 章](05.html)、 *CUDA 应用分析和调试*中的*用 Nsight 计算分析内核*部分了解它的基本用法。\n\n# CUDA 线程、块和图形处理器\n\nCUDA 编程中的基本工作单元是 CUDA 线程。基本的 CUDA 线程执行模型是**单指令多线程** ( **SIMT** )。换句话说，内核函数的主体是对单个 CUDA 线程的工作描述。但是，CUDA 架构执行多个具有相同动作的 CUDA 线程。\n\n从概念上讲，多个 CUDA 线程在一个组中并行工作。CUDA 线程块是多个 CUDA 线程的集合。多个线程块相互并发操作。我们称一组线程块为网格。下图显示了它们之间的关系:\n\n![](img/60928263-1c45-4083-8d5a-0b549796024d.png)\n\n这些分级 CUDA 线程操作与分级 CUDA 架构相匹配。当我们启动一个 CUDA 内核时，一个或多个 CUDA 线程块在 GPU 中的每个流式多处理器上执行。此外，流式多处理器可以根据资源可用性运行多个线程块。线程块中的线程数量不同，网格中的块数量也不同:\n\n![](img/54d3c5aa-a4c2-4418-83e2-1ae060c8df0f.png)\n\n流式多处理器可以任意和并发地执行线程块，执行尽可能多的图形处理器资源。因此，可并行执行的线程块的数量取决于该块需要多少 GPU 资源以及可用的 GPU 资源量。我们将在下一节讨论这个问题。流式多处理器的数量因 GPU 规格而异。例如，特斯拉 V100 是 80，RTX 2080 (Ti)是 48。\n\nCUDA 流式多处理器控制 32 个一组的 CUDA 线程。一个组被称为**扭曲**。以这种方式，一个或多个经线配置一个 CUDA 线程块。下图显示了这种关系:\n\n![](img/4d49ee70-c8f0-4ce4-b663-d6f2b49cd288.png)\n\n绿色的小盒子是 CUDA 线程，它们由一个经线分组。翘曲是 GPU 架构的基本控制单元。因此，它的大小会隐式或显式地影响 CUDA 编程。例如，最佳线程块大小是在能够充分利用块的扭曲调度和操作的多个扭曲大小中确定的。我们称之为占用，这将在下一节中详细介绍。此外，翘曲中的 CUDA 线程并行工作，并且本质上具有同步操作。我们将在本章的*扭曲级原语编程*部分讨论这一点。\n\n# 利用 CUDA 块和翘曲\n\n现在，我们将使用 CUDA 的`printf`来看看 CUDA 线程调度及其隐式同步。并行 CUDA 线程和块操作的执行是并发的。另一方面，从设备打印出来是一个连续的任务。因此，我们可以很容易地看到它们的执行顺序，因为并发任务的输出是任意的，而并行任务的输出是一致的。\n\n我们将开始编写打印全局线程索引、线程块索引、扭曲索引和通道索引的内核代码。为此，代码可以编写如下:\n\n```cpp\n__global__ void index_print_kernel() {\n    int idx = blockIdx.x * blockDim.x + threadIdx.x;\n    int warp_idx = threadIdx.x / warpSize;\n    int lane_idx = threadIdx.x & (warpSize - 1);\n\n    if ((lane_idx & (warpSize/2 - 1)) == 0)\n        //thread, block, warp, lane\n        printf(\" %5d\\t%5d\\t %2d\\t%2d\\n\", idx, blockIdx.x, \n               warp_idx, lane_idx);\n}\n```\n\n这段代码将帮助我们理解 warp 和 CUDA 线程调度的并发性。让我们的代码从 shell 中获取参数，以便轻松测试各种网格和线程块配置。\n\n然后，我们将编写调用内核函数的宿主代码:\n\n```cpp\nint main() {\n    int gridDim = 4, blockDim = 128;\n    puts(\"thread, block, warp, lane\");\n    index_print_kernel<<< gridDim, blockDim >>>();\n    cudaDeviceSynchronize();\n}\n```\n\n最后，让我们编译代码，执行它，并看到结果:\n\n```cpp\nnvcc -m64 -o cuda_thread_block cuda_thread_block.cu\n```\n\n以下结果是输出结果的示例。实际输出可能不同:\n\n```cpp\n$ ./cuda_thread_block.cu 4 128\nthread, block, warp, lane\n 64     0     2     0\n 80     0     2    16\n 96     0     3     0\n 112     0     3    16\n 0     0     0     0\n 16     0     0    16\n ...\n 352     2     3     0\n 368     2     3    16\n 288     2     1     0\n 304     2     1    16\n```\n\n从结果中，您将看到 CUDA 线程是以经纱大小启动的，并且顺序没有确定。另一方面，通道输出是有序的。从给定的结果，我们可以证实以下事实:\n\n*   **乱序块执行:**第二列显示线程块的索引。结果表明，它不能保证按照块索引的顺序执行。\n*   **带有线块的无序经纱索引:**第三列显示了线块中经纱的索引。不同区块的经纱顺序不同。所以，我们可以推断，曲速执行顺序没有保证。\n*   **经纱中执行的成组纱线:**第四列显示经纱中的通道。为了减少输出数量，应用将其限制为只打印两个索引。从每个扭曲内的顺序输出，我们可以类比`printf`函数的输出顺序是固定的，因此没有反转。\n\n总结一下，CUDA 线程被分成 32 个线程，它们的输出和 warp 的执行没有顺序。因此，程序员在进行 CUDA 内核开发时，必须牢记这一点。\n\n# 了解 CUDA 占用情况\n\nCUDA 占用率是活动 CUDA 扭曲与每个流式多处理器可以并发执行的最大扭曲的比率。一般来说，更高的占用率会导致更有效的图形处理器利用率，因为有更多的扭曲可以用来隐藏停滞扭曲的延迟。但是，由于 CUDA 线程之间的资源争用增加，它也可能降低性能。因此，开发人员理解这种权衡是至关重要的。\n\n寻找最佳 CUDA 占用率的目的是使 GPU 应用有效地利用 GPU 资源发出扭曲指令。GPU 在流式多处理器上使用多个扭曲调度器来调度多个扭曲。当有效调度多个扭曲时，图形处理器可以隐藏图形处理器指令之间的延迟或内存延迟。然后，CUDA 内核可以执行从多个扭曲连续发出的指令，而未调度的扭曲必须等待，直到它们可以发出下一个指令。\n\n开发人员可以使用两种方法来确定 CUDA 占用率:\n\n*   **理论占用率**由 CUDA 占用率计算器确定:该计算器是 CUDA 工具包提供的 Excel 表。理论上，我们可以从内核资源使用情况和 GPU 的流式多处理器来确定每个内核的占用率。\n*   **实现的占用率**由 GPU 确定:实现的占用率反映了流式多处理器上并发执行的扭曲的真实数量和最大可用扭曲。这种占用率可以通过英伟达性能分析器的度量分析来衡量。\n\n理论占用率可视为最大上限占用率，因为占用率不考虑指令相关性或内存带宽限制。\n\n现在，让我们看看这个占用和 CUDA C/C++ 是如何关联的。\n\n# 设置 NVCC 报告图形处理器资源使用情况\n\n首先，我们将使用**简单矩阵乘法** ( **SGEMM** )内核代码，如下所示:\n\n```cpp\n__global__ void sgemm_gpu_kernel(const float *A, const float *B, \n        float *C, int N, int M, int K, alpha, float beta) {\n    int col = blockIdx.x * blockDim.x + threadIdx.x;\n    int row = blockIdx.y * blockDim.y + threadIdx.y;\n\n    float sum = 0.f;\n    for (int i = 0; i < K; ++ i) {\n        sum += A[row * K + i] * B[i * K + col];\n    }\n    C[row * M + col] = alpha * sum + beta * C[row * M + col];\n}\n```\n\n并且，我们将使用以下内核代码调用内核函数:\n\n```cpp\nvoid sgemm_gpu(const float *A, const float *B, float *C,\n            int N, int M, int K, float alpha, float beta) {\n    dim3 dimBlock(BLOCK_DIM, BLOCK_DIM);\n    dim3 dimGrid(M / dimBlock.x, N / dimBlock.y);\n    sgemm_gpu_kernel<<< dimGrid, dimBlock >>>(A, B, C, N, M, K, alpha, beta);\n}\n```\n\n您可能需要提供适当的 GPU 内存及其大小信息。我们将使用 2048 作为`N`、`M`和`K`。内存大小是这个数字的平方。我们将`BLOCK_DIM`设为`16`。\n\n现在，让我们看看如何让`nvcc`编译器报告内核函数的 GPU 资源使用情况。\n\n# Linux 的设置\n\n在 Linux 环境中，我们应该提供两个编译器选项，如下所示:\n\n*   `--resource-usage` ( `--res-usage`):设置 GPU 资源使用的详细选项\n*   `-gencode`:指定要编译和生成操作码的目标架构，如下所示:\n    *   图灵:`compute_75,sm_75`\n    *   回:`compute_70,sm_70`\n    *   帕斯卡:`compute_61,sm_61`、`compute_61,sm_61`\n\n如果你不确定自己使用的是哪种架构，可以从 CUDA GPU 网站([https://developer.nvidia.com/cuda-gpus](https://developer.nvidia.com/cuda-gpus))找到。例如，`nvcc`编译命令可以有如下编译选项:\n\n```cpp\n$ nvcc -m 64 --resource-usage \\\n -gencode arch=compute_70,code=sm_70 \\\n -I/usr/local/cuda/samples/common/inc \\\n -o sgemm ./sgemm.cu \n```\n\n我们还可以将代码编译为针对多个 GPU 架构，如下所示:\n\n```cpp\n$ nvcc -m64 --resource-usage \\\n      -gencode arch=compute_70,code=sm_70 \\\n      -gencode arch=compute_75,code=sm_75 \\\n      -I/usr/local/cuda/samples/common/inc \\\n      -o sgemm ./sgemm.cu\n```\n\n如果您想使您的代码与新的 GPU 架构(图灵)兼容，您需要提供一个额外的选项，如下所示:\n\n```cpp\n$ nvcc -m64 --resource-usage \\\n      -gencode arch=compute_70,code=sm_70 \\\n      -gencode arch=compute_75,code=sm_75 \\\n      -gencode arch=compute_75,code=compute_75 \\\n      -I/usr/local/cuda/samples/common/inc \\\n      -o sgemm ./sgemm.cu\n```\n\n如果您想了解这些选项更多信息，可以在本文档中找到相关信息:[https://docs . NVIDIA . com/cuda/turing-compatibility-guide/index . html # building-turing-compatible-apps-use-cuda-10-0](https://docs.nvidia.com/cuda/turing-compatibility-guide/index.html#building-turing-compatible-apps-using-cuda-10-0)。\n\n现在，让我们编译源代码。我们可以从 NVCC 的输出中找到资源使用报告。使用前面的命令生成以下结果:\n\n![](img/48ba5f5b-a5b2-4fae-a022-e2eaeba65b62.png)\n\nNVCC 报告了每个计算能力的 CUDA 内核资源使用信息。在前面的输出截图中，我们可以看到每个线程的寄存器数量和恒定的内存使用量。\n\n# 窗口设置\n\n当我们开发一个 Windows 应用时，我们可以在 Visual Studio 的项目属性对话框中设置这些设置。下面的截图显示了该对话框:\n\n![](img/056e4da9-6936-49c0-957b-ef911ecb8bd9.png)\n\n要打开这个对话框，我们应该打开 debug_vs 属性页，然后遍历左侧面板上的 CUDA C/C++ | Device 选项卡。然后，我们应该设置如下选项:\n\n*   详细 PTXAS 输出:否|是\n*   代码生成:更新选项以指定您的目标体系结构，如下所示:\n    *   图灵:`compute_75,sm_75`\n    *   回:`compute_70,sm_70`\n    *   帕斯卡:t0]\n\n我们可以为每个目标使用分号(`;`)指定多个目标架构。\n\n现在，让我们构建源代码，我们将在 Visual Studio 的输出面板上看到 NVCC 的报告。然后，您将看到类似如下的输出:\n\n![](img/9f073fc9-3503-4296-a33e-60416c95573e.png)\n\n它与 Linux 中的 NVCC 输出相同。\n\n现在，让我们使用资源使用报告来分析内核的占用率。\n\n# 使用占用率计算器分析最佳占用率\n\n实际上，我们可以使用 CUDA 占用计算器，它是由 CUDA 工具包提供的。利用这一点，我们可以通过提供一些内核信息来获得理论占用率。计算器是一个 Excel 文件，根据您使用的操作系统，您可以在下面找到它:\n\n*   **窗户:** `C:\\Program Files\\NVIDIA GPU Computing Toolkit\\CUDA\\<cuda-version>\\tools`\n*   **Linux:** `/usr/local/cuda/tools`\n*   **柔软:** `/Developer/NVIDIA/<cuda-version>/tools`\n\n以下是计算器的截图:\n\n![](img/7845ff21-4805-4c93-beb1-9e788919b60e.png)\n\nCUDA Occupancy Calculator\n\n这个计算器有两个部分:内核信息输入和占用信息输出。作为输入，它需要两种信息，如下所示:\n\n*   图形处理器的计算能力(绿色)\n*   线程块资源信息(黄色):\n    *   每个 CUDA 线程块的线程数\n    *   每个 CUDA 线程的寄存器\n    *   每个块的共享内存\n\n计算器在此显示图形处理器的占用信息:\n\n*   图形处理器占用数据(蓝色)\n*   图形处理器对图形处理器计算能力的物理限制(灰色)\n*   每个块分配的资源(黄色)\n*   每个流多处理器的最大线程块(黄色、橙色和红色)\n*   占用限制图遵循三个关键的占用资源，即线程、寄存器和每个块的共享内存\n*   图表上的红色三角形，显示当前占用数据\n\n现在，让我们把获得的信息输入计算器。我们可以在 Excel 工作表中编辑绿色和橙色区域:\n\n![](img/d8220238-38e8-4301-a0af-6e667621ff39.png)\n\n输入您获得的内核资源信息，并查看工作表的变化。\n\n根据计算能力和输入数据，占用率会发生变化，如下图所示:\n\n![](img/d2ddc32f-0596-4d41-8b1b-0f8c4fe7fde8.png)\n\nChanges in occupancy depending on compute capability and input data\n\n蓝色区域显示了内核函数实现的占用率。在这张截图中，它显示了 100%的占用率成就。该表的右侧显示了图形处理器资源的占用率图:CUDA 线程、共享内存和寄存器。\n\n一般来说，由于许多原因，内核代码不可能有 100%的理论占用率。然而，设置 pick 占用率是高效利用 GPU 资源的开始。\n\n# 占用调节–边界寄存器使用\n\n当内核算法复杂，或者处理数据类型是双精度时，CUDA 寄存器的使用会增加。在这种情况下，由于活动经纱尺寸有限，占用率会下降。在这种情况下，我们可以通过限制寄存器的使用来提高理论占用率，并看看性能是否得到提高。\n\n资源调优 GPU 资源使用的一种方法是将`__launch_bound__`限定符与内核函数一起使用。这通知 NVCC 以最大块大小保证每个流的最小线程块被多处理。然后，NVCC 找到达到给定条件的最佳寄存器大小。如果您知道可以让您的算法在编译时高效运行的大小，您可以使用它。标识符可以如下使用:\n\n```cpp\nint maxThreadPerBlock = 256;\nint minBlocksPerMultiprocessor = 2;\n__global__ void\n__launch_bound__ (maxThreadPerBlock, minBlocksPerMultiprocessor) foo_kernel() {\n    ...\n}\n```\n\n然后，编译器检查上限资源，并减少每个块的有限资源使用。如果它的资源使用没有超过上限，如果 CUDA 可以为每个多处理器调度一个额外的线程块，如果第二个参数没有给出，编译器会调整寄存器的使用。或者，编译器增加寄存器使用来隐藏单线程指令延迟。\n\n此外，我们可以简单地在应用级别限制占用寄存器的数量。`--maxrregcount`标志到`NVCC`将指定数字，编译器将对寄存器使用重新排序。以下编译命令显示了如何在 Linux 终端中使用该标志:\n\n```cpp\n$ nvcc -m64 -I/usr/local/cuda/samples/common/inc -gencode arch=compute_70,code=sm_70 --resource-usage --maxrregcount 24 -o sgemm ./sgemm.cu\n```\n\n但是，请记住，以这种方式限制寄存器的使用会引入由寄存器限制带来的线程性能。即使是编译器，如果不能将寄存器设置在极限之下，也可以将寄存器拆分到局部内存中，局部变量放在全局内存中。\n\n# 从分析器获取已实现的占用率\n\n现在，我们可以使用可视化探查器从已分析的度量数据中获得已实现的占用率。单击目标内核时间线栏。然后，我们可以在“属性”面板中看到理论占用和实际占用。我们还可以从内核延迟菜单获得更多细节。下面的截图显示了我们使用的示例代码的性能:\n\n![](img/aea2d647-f88b-4f6f-9a00-922010fb039f.png)\n\nPerformance showing achieved and theoretical occupancy\n\n通过这种占用率调整，我们可以设计 CUDA 块大小，以充分利用流式多处理器中的翘曲调度。但是，这并不能解决 54.75%的内存限制问题，这是我们在上一节中发现的。这意味着，由于内存请求受阻，多处理器可能会停滞，并且无法隐藏内存访问延迟。我们将在本章中讨论如何对此进行优化，在[第 7 章](07.html)、*CUDA*中的并行编程模式中，我们将讨论矩阵-矩阵乘法优化。\n\n# 理解平行约简\n\n约简是一种简单但有用的算法，可以在许多参数中获得一个公共参数。这项任务可以按顺序完成，也可以并行完成。当涉及到并行架构的并行处理时，并行约简是获得直方图、平均值或任何其他统计值的最快方法。\n\n下图显示了顺序约简和并行约简之间的区别:\n\n![](img/cff3f592-ee81-4d90-b567-de34f12202ea.png)\n\n通过并行执行约简任务，并行约简算法可以以对数规模减少总步骤。现在，让我们开始在 GPU 上实现这个并行约简算法。首先，我们将通过使用全局内存的简单设计来实现这一点。然后，我们将使用共享内存实现另一个缩减版本。通过比较这两种实现，我们将讨论什么会带来性能差异。\n\n# 使用全局内存的朴素并行约简\n\n缩减的第一个基本方法是使用并行 CUDA 线程，并使用全局内存共享缩减输出。对于每一次迭代，CUDA 内核通过将其大小减少 2 来从全局内存中获取累积值。缩减的工作方式如下图所示，该图显示了使用全局内存数据共享的简单并行缩减:\n\n![](img/873af97a-c719-4f57-94b1-2732193a2873.png)\n\n这种方法在 CUDA 中很慢，因为它浪费了全局内存的带宽，并且没有利用任何更快的片内内存。为了获得更好的性能，建议使用共享内存来节省全局内存带宽并减少内存获取延迟。稍后我们将讨论这种方法如何浪费带宽。\n\n现在，让我们实现这种减少。首先，我们将编写约简核函数，如下所示:\n\n```cpp\n__global__ void naive_reduction_kernel\n     (float *data_out, float *data_in, int stride, int size) {\n     int idx_x = blockIdx.x * blockDim.x + threadIdx.x;\n     if (idx_x + stride < size)\n         data_out[idx_x] += data_in[idx_x + stride];\n}\n```\n\n我们将调用内核函数，同时迭代地将步长减少一半，直到`stride`大小为 1，如下所示:\n\n```cpp\nvoid naive_reduction(float *d_out, float *d_in, int n_threads, int size) {\n    int n_blocks = (size + n_threads - 1) / n_threads;\n    for (int stride = 1; stride < size; stride *= 2)\n        naive_reduction_kernel<<<n_blocks, n_threads>>>(d_out, d_in, stride, size);\n}\n```\n\n在这个实现中，内核代码以跨越式寻址获取设备内存，并输出一个缩减结果。宿主代码为每一步触发缩减内核，参数大小减少一半。我们不能有一个内部内核循环，因为 CUDA 不能保证跨线程块和流多处理器的同步操作。\n\n# 使用共享内存减少内核\n\n在这种减少中，每个 CUDA 线程块减少输入值，并且 CUDA 线程使用共享内存共享数据。为了进行正确的数据更新，它们使用块级固有同步功能`__syncthreads()`。然后，下一次迭代对先前的约简结果进行操作。它的设计如下图所示，显示了使用共享内存的并行缩减:\n\n![](img/606f158b-fc9c-4723-8a8c-a9c8b42bdacf.png)\n\n黄色虚线框表示 CUDA 线程块的操作覆盖范围。在这个设计中，每个 CUDA 线程块输出一个约简结果。\n\n块级约简允许每个 CUDA 线程块进行约简，并输出单个约简输出。由于不需要我们将中间结果保存在全局内存中，所以 CUDA 内核可以将过渡值存储在共享内存中。这种设计有助于节省全局内存带宽并减少内存延迟。\n\n正如我们为全球减排所做的那样，我们将实施这项行动。首先，我们将编写内核函数，如下所示:\n\n```cpp\n__global__ void reduction_kernel(float* d_out, float* d_in, \n                                 unsigned int size) {\n    unsigned int idx_x = blockIdx.x * blockDim.x + threadIdx.x;\n\n    extern __shared__ float s_data[];\n    s_data[threadIdx.x] = (idx_x < size) ? d_in[idx_x] : 0.f;\n\n    __syncthreads();\n\n    // do reduction\n    for (unsigned int stride = 1; stride < blockDim.x; stride *= 2) {\n        // thread synchronous reduction\n        if ( (idx_x % (stride * 2)) == 0 )\n            s_data[threadIdx.x] += s_data[threadIdx.x + stride];\n\n        __syncthreads();\n    }\n\n    if (threadIdx.x == 0)\n        d_out[blockIdx.x] = s_data[0];\n}\n```\n\n然后，我们将调用内核函数，如下所示:\n\n```cpp\nvoid reduction(float *d_out, float *d_in, int n_threads, int size)\n{\n    cudaMemcpy(d_out, d_in, size * sizeof(float), cudaMemcpyDeviceToDevice);\n    while(size > 1) {\n        int n_blocks = (size + n_threads - 1) / n_threads;\n        reduction_kernel\n            <<< n_blocks, n_threads, n_threads * sizeof(float), 0 >>>\n            (d_out, d_out, size);\n        size = n_blocks;\n    }\n}\n```\n\n在这段代码中，我们提供了`n_threads * sizeof (float)`字节，因为每个 CUDA 线程将为每个字节共享一个变量。\n\n# 编写性能测量代码\n\n为了衡量每个版本的性能，我们将使用 CUDA 示例`timer`助手函数:\n\n```cpp\n// Initialize timer\nStopWatchInterface *timer;\nsdkCreateTimer(&timer);\nsdkStartTimer(&timer);\n\n... Execution code ...\n\n// Getting elapsed time\ncudaDeviceSynchronize(); // Blocks the host until GPU finishes the work\nsdkStopTimer(&timer);\n\n// Getting execution time in micro-secondes\nfloat execution_time_ms = sdkGetTimerValue(&timer)\n\n// Termination of timer\nsdkDeleteTimer(&timer);\n```\n\n该功能集有助于在微秒级测量执行时间。此外，建议在性能测量之前调用内核函数，以消除设备初始化开销。有关更详细的实现，请访问`global_reduction.cu`和`reduction.cu`文件中的实现代码。这些代码集在本章中与概要分析器一起用于评估优化效果。\n\n# 两种缩减的性能比较—全局内存和共享内存\n\n现在，我们可以比较两个并行约简操作的执行时间。性能可能因图形处理器和实现环境而异。分别运行以下命令进行全局缩减和使用共享内存进行缩减:\n\n```cpp\n# Reduction with global memory\n$ nvcc -run -m64 -gencode arch=compute_70,code=sm_70 -I/usr/local/cuda/samples/common/inc -o reduction_global ./reduction_global.cpp reduction_global_kernel.cu\n\n# Reduction using shared memory\n$ nvcc -run -m64 -gencode arch=compute_70,code=sm_70 -I/usr/local/cuda/samples/common/inc -o reduction_shared ./reduction_shared.cpp reduction_shared_kernel.cu\n```\n\n使用我的特斯拉 V100 PCIe 卡，两种降低的估计性能如下。元素数量为*2<sup>24</sup>T3】项:*\n\n| \n\n**操作**\n\n | \n\n**预计时间(毫秒)**\n\n | \n\n**加速**\n\n |\n| 原始方法(使用全局内存进行缩减) | \n\nFour point six zero nine\n\n | \n\n1.0x\n\n |\n| 使用共享内存进行还原 | \n\nZero point six two four\n\n | \n\n7.4 倍\n\n |\n\n从这个结果中，我们可以看到使用共享内存减少共享数据如何快速返回输出。第一个实现版本在`global_reduction.cu`中，第二个版本在`shared_reduction.cu`中，大家可以自己比较一下实现。\n\n通过将缩减与共享内存分开，我们可以显著提高性能。然而，我们无法确定这是我们能获得的最大性能，也不知道我们的应用有什么瓶颈。为了分析这一点，我们将在下一节讨论性能限制器。\n\n# 识别应用的性能限制\n\n之前，我们看到了节省全局内存如何提高 CUDA 内核的性能。总的来说，使用片内缓存比使用片外内存要好。但是，通过这个简单的类比，我们无法确定是否还有很多优化空间。\n\n性能限制器显示了限制因素，它最大程度地限制了应用的性能。基于其分析信息，它分析了计算和内存带宽之间的性能限制因素。根据这些资源的利用率，应用可以分为四种类型:**计算界限**、**带宽界限**、**延迟界限**和**计算和延迟界限**。下图显示了与计算和内存利用率相关的类别:\n\n![](img/2689638e-eff4-4f99-bb64-c3664f8d08c2.png)\n\n在我们确定限制器之后，我们可以使用下一个优化策略。如果两种资源的利用率都很高，我们可以集中精力优化资源。如果两者都没有得到充分利用，我们可以从系统的输入/输出方面进行延迟优化。如果两者都很高，我们可以调查是否存在内存操作停滞问题和计算相关问题。\n\n现在让我们看看如何获取利用率信息。\n\n# 寻找性能限制和优化\n\n现在，让我们将这种分析应用于两种简化实现。我们将对它们进行比较，并讨论共享内存如何有助于性能限制器分析，从而提高性能。首先，让我们使用以下命令通过度量分析来分析基于内存的全局缩减应用:\n\n```cpp\n$ nvprof -o reduction_global.nvvp ./reduction_global \n$ nvprof --analysis-metrics -o reduction_global_metric.nvvp ./reduction_global\n```\n\n然后，我们将从 NVIDIA profiler 获得以下图表，其中显示了第一个基于内存的全局缩减的性能限制器:\n\n![](img/2238b4cf-3f68-4583-a824-bf5c47467f6b.png)\n\n在这个图表中，我们需要通过检查内核延迟分析来查看性能执行比率是否平衡。因为从上图中可以看出，**计算**和**内存**之间的利用率差距很大，这可能意味着由于内存瓶颈，计算会有很大的延迟。下图显示了基于采样的分析结果，我们可以确定 CUDA 内核由于内存依赖而处于饥饿状态:\n\n![](img/cca26c03-d198-426d-ad68-fee55109fcec.png)\n\n如您所见，内核执行由于内存等待而延迟。现在，让我们描述一下基于共享内存的缩减。我们可以使用以下命令来实现这一点:\n\n```cpp\n$ nvprof -o reduction_shared.nvvp ./reduction_shared \n$ nvprof --analysis-metrics -o reduction_shared_metric.nvvp ./reduction_shared\n```\n\n然后，我们将获得下面的图表，该图表显示了第二个基于共享内存的缩减的性能限制器:\n\n![](img/ed221ef5-237f-4cb7-aa13-38e164a8d15f.png)\n\n我们可以确定它是计算受限的，并且内存不会饿死 CUDA 内核。\n\n现在让我们回顾一下优化计算操作的内核操作。以下代码显示了内核函数中的并行约简部分:\n\n```cpp\nfor (unsigned int stride = 1; stride < blockDim.x; stride *= 2) {\n     if ( (idx_x % (stride * 2)) == 0 )\n         s_data[threadIdx.x] += s_data[threadIdx.x + stride];\n     __syncthreads();\n }\n```\n\n作为一种算术运算，模运算是重运算。由于`stride`变量是`2`的指数，因此可以用按位运算代替，如下所示:\n\n```cpp\nfor (unsigned int stride = 1; stride < blockDim.x; stride *= 2) {\n     if ( (idx_x & (stride * 2 - 1)) == 0 )\n         s_data[threadIdx.x] += s_data[threadIdx.x + stride];\n     __syncthreads();\n }\n```\n\n运行以下命令查看优化的输出:\n\n```cpp\n$ nvcc -run -m64 -gencode arch=compute_70,code=sm_70 -I/usr/local/cuda/samples/common/inc -o reduction_shared ./reduction_shared.cpp reduction_shared_kernel.cu\n```\n\n那么，新的估计时间为 **0.399 ms** ，我们可以实现更优化的性能，如下表所示:\n\n| **操作** | **预计时间(毫秒)** | **加速** |\n| 原始方法(使用全局内存进行缩减) | Four point six zero nine | 1.0x |\n| 使用共享内存进行还原 | Zero point six two four | 7.4 倍 |\n| 将条件操作从`%`更改为`&` | Zero point three nine nine | 11.55 倍 |\n\n下图显示了更新后的性能限制器:\n\n![](img/15e9f92a-af6d-4b94-ab56-1cd041292870.png)\n\n我们可以识别出它的操作是**计算和延时有界**。因此，我们可以确定我们可以通过优化计算效率来提高内存利用率。\n\n# 最小化 CUDA 翘曲发散效应\n\n在**单指令、多线程** ( **SIMT** )执行模型中，线程被分组为 32 个线程的集合，每一个集合被称为一个 **warp** 。如果一个 warp 遇到一个条件语句或分支，它的线程可以被分叉和序列化来执行每个条件。这被称为**分支发散**，对性能影响很大。\n\nCUDA 经线发散指的是这种 CUDA 线程在经线中的发散操作。如果条件分支具有`if` - `else`结构，并且扭曲具有这种扭曲发散，则所有 CUDA 线程对于分支的代码块都具有活动和非活动的操作部分。\n\n下图显示了 CUDA 扭曲中的扭曲发散效果。不处于空闲状态并降低 GPU 线程使用效率的 CUDA 线程:\n\n![](img/e0513741-a1a7-41e0-9598-534517e18798.png)\n\n随着更多分支部分变得重要，GPU 调度吞吐量变得低效。因此，我们需要避免或最小化这种翘曲发散效应。有几个选项可以选择:\n\n*   通过处理不同的经线来执行分支部分，从而避免发散\n*   合并分枝部分以减少经线中的分枝\n*   缩短分枝部分；只有关键部分需要分支\n*   重新排列数据(即，调换、合并等)\n*   在协作组中使用`tiled_partition`划分组\n\n# 将发散确定为性能瓶颈\n\n从前面的简化优化中，您可能会发现一个关于低效内核的警告，这是由于计算分析中的分歧分支，如下所示:\n\n![](img/1e6569d1-6b97-4720-a49a-2b3883487a3a.png)\n\n73.4 %的背离意味着我们有一条低效的运行路径。我们可以确定缩减解决方案是问题所在，接下来重点介绍:\n\n```cpp\n__global__ void reduction_kernel(float* d_out, float* d_in, unsigned int size) {\n    unsigned int idx_x = blockIdx.x * blockDim.x + threadIdx.x;\n\n    extern __shared__ float s_data[];\n    s_data[threadIdx.x] = (idx_x < size) ? d_in[idx_x] : 0.f;\n\n    __syncthreads();\n\n    // do reduction\n    for (unsigned int stride = 1; stride < blockDim.x; stride *= 2) {\n        // thread synchronous reduction\n        if ( (idx_x % (stride * 2 - 1)) == 0 )\n            s_data[threadIdx.x] += s_data[threadIdx.x + stride];\n\n        __syncthreads();\n    }\n\n    if (threadIdx.x == 0)\n        d_out[blockIdx.x] = s_data[0];\n}\n```\n\n说到精简寻址，我们可以选择以下 CUDA 线程索引策略之一:\n\n*   交错寻址\n*   顺序寻址\n\n让我们回顾一下它们是什么，并通过实施这些策略来比较它们的性能。因为我们将只修改简化内核，所以我们可以在接下来的两个实现中重用宿主代码。\n\n# 交错寻址\n\n在这种策略中，连续的 CUDA 线程使用交错寻址策略获取输入数据。与以前的版本相比，CUDA 线程通过增加步幅值来访问输入数据。下图显示了 CUDA 线程如何与缩减项交错:\n\n![](img/f4d697d2-a75f-4b7c-8bd0-712132017416.png)\n\n这种交错寻址可以如下实现:\n\n```cpp\n__global__ void\n interleaved_reduction_kernel(float* g_out, float* g_in, unsigned int size) {\n    unsigned int idx_x = blockIdx.x * blockDim.x + threadIdx.x;\n\n    extern __shared__ float s_data[];\n    s_data[threadIdx.x] = (idx_x < size) ? g_in[idx_x] : 0.f;\n    __syncthreads();\n\n    // do reduction\n    // interleaved addressing\n    for (unsigned int stride = 1; stride < blockDim.x; stride *= 2) {\n        int index = 2 * stride * threadIdx.x;\n        if (index < blockDim.x)\n            s_data[index] += s_data[index + stride];\n        __syncthreads();\n    }\n    if (threadIdx.x == 0)\n        g_out[blockIdx.x] = s_data[0];\n}\n```\n\n运行以下命令来编译前面的代码:\n\n```cpp\n$ nvcc -run -m64 -gencode arch=compute_70,code=sm_70 -I/usr/local/cuda/samples/common/inc -o reduction ./reduction.cpp ./reduction_kernel_interleaving.cu\n```\n\n在特斯拉 V100 上测得的内核执行时间为 **0.446 ms** 。它比以前的版本慢，因为这种方法没有充分利用每个线程块。我们将能够通过分析其指标获得更多细节。\n\n现在，我们将尝试另一种寻址方法，这种方法被设计为每个线程块计算更多的数据。\n\n# 顺序寻址\n\n与以前的版本相比，这具有高度整合的索引和寻址。这种设计效率更高，因为当步幅大于弯曲尺寸时，没有发散。下图显示了合并线程操作:\n\n![](img/316964ae-b4ed-460f-9285-5043c3782407.png)\n\n现在，让我们编写一个内核函数，对缩减项使用顺序寻址:\n\n```cpp\n__global__ void\n sequantial_reduction_kernel(float *g_out, float *g_in, \n                             unsigned int size)\n{\n    unsigned int idx_x = blockIdx.x * blockDim.x + threadIdx.x;\n\n    extern __shared__ float s_data[];\n\n    s_data[threadIdx.x] = (idx_x < size) ? g_in[idx_x] : 0.f;\n\n    __syncthreads();\n\n    // do reduction\n    // sequential addressing\n    for (unsigned int stride = blockDim.x / 2; stride > 0; \n         stride >>= 1)\n    {\n        if (threadIdx.x < stride)\n            s_data[threadIdx.x] += s_data[threadIdx.x + stride];\n\n        __syncthreads();\n    }\n\n    if (threadIdx.x == 0)\n        g_out[blockIdx.x] = s_data[0];\n}\n```\n\n运行以下命令来编译前面的代码:\n\n```cpp\n$ nvcc -run -m64 -gencode arch=compute_70,code=sm_70 -I/usr/local/cuda/samples/common/inc -o reduction ./reduction.cpp ./reduction_kernel_sequential.cu\n```\n\n它在特斯拉 V100 GPU 上的实测执行时间为 **0.378 ms** ，比之前的策略(0.399 ms)稍快。\n\n由于避免了扭曲发散，我们可以在原始计算上获得 12.2 倍的性能增益。下图显示了更新后的性能限制器分析:\n\n![](img/1598561f-8e61-41ea-a891-d6744ba53a19.png)\n\n与之前的性能限制器相比，我们可以看到控制流操作的减少和内存利用率的提高。\n\n# 限制器的性能建模和平衡\n\n在性能限制器分析之后，我们当前的降低性能受到内存带宽导致的计算延迟的限制，尽管限制器分析显示了每个资源的充分利用。让我们解释一下为什么这是一个问题，以及我们如何通过遵循屋顶线性能模型来解决这个问题。\n\n# 屋顶模型\n\n**屋顶线模型**是一个直观的可视化性能分析模型，用于为并行处理单元上的给定计算内核提供估计性能。基于这个模型，并行编程中的开发人员可以识别算法应该绑定到什么，并确定应该优化哪个。\n\n下图显示了屋顶线模型的示例:\n\n![](img/8d377d5f-92e2-43bd-84f5-bf446cea1edb.png)\n\n倾斜部分表示受记忆限制，平坦部分表示受算术限制。每个并行算法和实现都有自己的屋顶线模型，因为它们具有不同的计算能力和内存带宽。有了这个模型，算法可以根据它们的操作强度(触发器/字节)来放置。如果一个实现没有达到这个模型的预期性能，我们可以确定这个版本是受延迟限制的。\n\n考虑到我们的并行约简的复杂性，它必须是受内存限制的。换句话说，它的操作强度低，所以我们的策略应该尽可能地最大化内存带宽。\n\n因此，我们需要使用分析器中的性能分析来确认我们的缩减内核函数是如何消耗内存带宽的。下图显示了全局内存的带宽使用情况:\n\n![](img/9b17a6cf-98a6-49ac-9f48-4392a4459f5a.png)\n\n如图所示，我们没有充分利用内存带宽。在特斯拉 V100 GPU 上，总带宽为 343.376 GB/s，由于该 GPU 具有 900 GB/s 带宽的 HBM2 内存，因此它利用了大约三分之一的带宽。因此，下一步是通过让每个 CUDA 线程消化更多的数据来增加带宽使用。这将解决延迟受限的情况，并使我们的应用受限于内存带宽。\n\n现在，让我们讨论如何增加内存带宽。\n\n# 利用网格环最大化内存带宽\n\n我们可以用一个简单的想法实现这一点。约简问题允许我们用 CUDA 线程积累输入数据并开始约简操作。以前，我们的缩减实现从输入数据大小开始。但是现在，我们将使用一组 CUDA 线程迭代输入数据，这个大小将是我们内核函数的网格大小。这种迭代风格被称为网格纹循环。这种技术对于控制多个 CUDA 内核有很多好处，在本文中介绍:[https://dev blogs . NVIDIA . com/CUDA-pro-tip-write-flexible-kernel-grid-stride-loops](https://devblogs.nvidia.com/cuda-pro-tip-write-flexible-kernels-grid-stride-loops)。\n\n下面的代码显示了更新后的约简内核函数:\n\n```cpp\n__global__ void reduction_kernel(float *g_out, float *g_in, \n                                 unsigned int size) {\n    unsigned int idx_x = blockIdx.x * blockDim.x + threadIdx.x;\n    extern __shared__ float s_data[];\n\n    // cumulates input with grid-stride loop \n // and save to the shared memory\n float input = 0.f;\n for (int i = idx_x; i < size; i += blockDim.x * gridDim.x)\n input += g_in[i];\n s_data[threadIdx.x] = input;\n __syncthreads();\n\n    // do reduction\n    for (unsigned int stride = blockDim.x / 2; stride > 0; \n         stride >>= 1) {\n        if (threadIdx.x < stride)\n            s_data[threadIdx.x] += s_data[threadIdx.x + stride];\n        __syncthreads();\n    }\n    if (threadIdx.x == 0)\n        g_out[blockIdx.x] = s_data[0];\n}\n\n```\n\n你会发现这个内核函数首先专注于积累输入数据，然后减少加载的数据。\n\n现在，我们需要确定网格大小。为了让我们的 GPU 代码在各种 GPU 目标上运行，我们必须在运行时确定它们的大小。此外，我们需要利用 GPU 中的所有多处理器。CUDA C 提供相关功能。我们可以使用`cudaOccpancyMaxActiveBlocksPerMultiprocessor()`函数获得每个多处理器的占用感知最大活动块。此外，我们可以使用`cudaDeviceGetAttribte()`功能获得目标图形处理器上的多处理器号。下面的代码展示了我们如何使用这些函数并调用内核函数:\n\n```cpp\nint reduction(float *g_outPtr, float *g_inPtr, int size, int n_threads)\n{\n    int num_sms;\n    int num_blocks_per_sm;\n    cudaDeviceGetAttribute(&num_sms, \n                           cudaDevAttrMultiProcessorCount, 0);\n    cudaOccupancyMaxActiveBlocksPerMultiprocessor(&num_blocks_per_sm, \n           reduction_kernel, n_threads, n_threads*sizeof(float));\n    int n_blocks = min(num_blocks_per_sm * num_sms, (size \n                       + n_threads - 1) / n_threads);\n\n    reduction_kernel<<<n_blocks, n_threads, n_threads * \n                       sizeof(float), 0>>>(g_outPtr, g_inPtr, size);\n    reduction_kernel<<<1, n_threads, n_threads * sizeof(float), \n                       0>>>(g_outPtr, g_inPtr, n_blocks);\n    return 1;\n}\n```\n\n这个函数还有一个额外的修改。为了节省占用率计算开销，它用单个块再次启动`reduction_kernel()`功能。运行以下命令:\n\n```cpp\n$ nvcc -run -m64 -gencode arch=compute_70,code=sm_70 -I/usr/local/cuda/samples/common/inc -o reduction ./reduction.cpp ./reduction_kernel.cu\n```\n\n在一台特斯拉 V100 上更新后的还原性能为 **0.278** **ms** ，比之前的方法快了 100 ms 左右。\n\n现在，让我们回顾一下如何利用内存带宽。下图显示了 Visual Profiler 中的内存利用率分析，并显示了我们如何将内存带宽增加了两倍:\n\n![](img/44e2f316-82e4-494e-a957-3e859fdcba39.png)\n\n虽然它显示带宽增加，但我们仍有进一步增加的空间。让我们讨论如何获得更多带宽。\n\n# 平衡输入/输出吞吐量\n\n从我们从分析器得到的结果来看，局部变量输入有大量的加载/存储请求。由于操作依赖性，这种大规模的输入/输出会影响线程块的调度。当前数据积累中最糟糕的是它依赖于设备内存。因此，我们将使用额外的寄存器发布更多的加载指令来缓解依赖性。下面的代码展示了我们如何做到这一点:\n\n```cpp\n#define NUM_LOAD 4\n__global__ void\n reduction_kernel(float *g_out, float *g_in, unsigned int size)\n{\n    unsigned int idx_x = blockIdx.x * blockDim.x + threadIdx.x;\n\n    extern __shared__ float s_data[];\n\n    // cumulates input with grid-stride loop \n    // and save to the shared memory\n    float input[NUM_LOAD] = {0.f};\n    for (int i = idx_x; i < size; i += blockDim.x * \n         gridDim.x * NUM_LOAD)\n    {\n        for (int step = 0; step < NUM_LOAD; step++)\n            input[step] += (i + step * blockDim.x * gridDim.x < size) ? \n                g_in[i + step * blockDim.x * gridDim.x] : 0.f;\n    }\n    for (int i = 1; i < NUM_LOAD; i++)\n        input[0] += input[i];\n    s_data[threadIdx.x] = input[0];\n\n    __syncthreads();\n\n    // do reduction\n    for (unsigned int stride = blockDim.x / 2; stride > 0; \n         stride >>= 1)\n    {\n        if (threadIdx.x < stride)\n            s_data[threadIdx.x] += s_data[threadIdx.x + stride];\n\n        __syncthreads();\n    }\n\n    if (threadIdx.x == 0) {\n        g_out[blockIdx.x] = s_data[0];\n    }\n}\n```\n\n这段代码使用另外三个寄存器来收集全局内存数据。`NUM_LOAD`的值会因 GPU 而异，因为它会受到 GPU 内存带宽和 GPU 中 CUDA 内核数量的影响:\n\n![](img/3e7d7521-757d-4041-8999-c04e3cdfc52c.png)\n\n运行以下命令时，使用特斯拉 V100 卡获得的性能为 **0.264** 毫秒:\n\n```cpp\n$ nvcc -run -m64 -gencode arch=compute_70,code=sm_70 -I/usr/local/cuda/samples/common/inc -o reduction ./reduction.cpp ./reduction_kernel_opt.cu\n```\n\n# 翘曲级原语编程\n\nCUDA 9.0 引入了新的 warp 同步编程。这一重大变化旨在避免 CUDA 编程依赖隐式扭曲同步操作和显式处理同步目标。这有助于防止扭曲同步操作中的疏忽竞争条件和死锁。\n\n历史上，CUDA 只为线程块中的 CUDA 线程提供了一个显式同步应用编程接口`__syncthreads()`，它依赖于扭曲的隐式同步。下图显示了 CUDA 线程块操作的两个同步级别:\n\n![](img/e9d730cb-7dbe-4aa1-a5ba-f727ac229f21.png)\n\n然而，最新的图形处理器架构(沃尔特和图灵)有一个增强的线程控制模型，其中每个线程可以执行不同的指令，而他们保持其 SIMT 编程模型。下图显示了它的变化情况:\n\n![](img/19f1bde5-e031-4aac-bb90-b3162e1d8069.png)\n\n在帕斯卡架构(左)之前，线程是在扭曲级别调度的，它们在扭曲中隐式同步。因此，弯曲中的 CUDA 线程隐式同步。然而，这有意外的死锁可能性。\n\nVolta 架构对此进行了翻新，引入了**独立线程调度**。这种控制模型使每个 CUDA 线程都有自己的程序计数器，并允许一组线程参与一个扭曲。在这个模型中，我们必须使用一个显式的同步应用编程接口来指定每个 CUDA 线程的操作。\n\n因此，CUDA 9 引入了显式的翘曲级原语函数:\n\n|  | \n\n**扭曲级基本函数**\n\n |\n| \n\n**识别活动线程**\n\n | `__activemask()` |\n| \n\n**屏蔽活动线程**\n\n | `__all_sync()`、`__any_sync()`、`__uni_sync()`、`__ballot_sync()``__match_any_sync()`、`__match_all_sync()` |\n| \n\n**同步数据交换**\n\n | `__shfl_sync()`、`__shfl_up_sync()`、`__shfl_down_sync()`、`__shfl_xor_sync()` |\n| \n\n**线程同步**\n\n | `__syncwarp()` |\n\n有三类扭曲方向的基本函数，它们是扭曲识别、扭曲操作和同步。所有这些函数都隐式指定同步目标，以避免意外的竞争情况。\n\n# 使用扭曲图元的并行缩减\n\n让我们看看这对我们的并行缩减实现有什么好处。该配方将在协作组中使用`shfl_down()`功能，在扭曲图元功能中使用`shfl_down_sync()`。下图显示了`shfl_down_sync()`如何进行降档操作:\n\n![](img/f62b8889-8529-494e-8b4f-61ec7ec2ca8e.png)\n\n在这个集合操作中，一个经线中的 CUDA 线程可以将指定的寄存器值转移到同一经线中的另一个线程，并与之同步。具体来说，集合操作有两个步骤(第三个是可选的):\n\n1.  识别、屏蔽或投票在一个将要进行操作的扭曲中寻找 CUDA 线程。\n2.  让 CUDA 线程转移数据。\n3.  翘曲中的所有 CUDA 线程都是同步的(可选)。\n\n对于并行约简问题，我们可以使用`__shfl_down_sync()`进行翘曲级约简。现在，我们可以通过下图来增强线程块级别的减少:\n\n![](img/10831534-803f-4d34-ba5f-b944331803d3.png)\n\n每个扭曲的缩减结果都存储到共享内存中，以便与其他扭曲共享。然后，通过再次执行扭曲收集，可以获得最终的块级缩减。\n\nWe use `__shfl_down_sync()` since we need only one thread to have warp-level reduction. If you need to make all the CUDA threads have warp-level reduction, you can use `__shfl_xor_sync()` instead.\n\n第一次块级缩减的数量是网格的维数，输出存储在全局内存中。通过再次调用，我们可以使用扭曲级同步函数构建一个并行约简内核。\n\n现在，让我们使用扭曲级基本函数来实现扭曲级缩减。首先，我们将编写一个函数，使用扭曲移动函数来减少扭曲级别。下面的代码显示了如何实现这一点:\n\n```cpp\n__inline__ __device__ float warp_reduce_sum(float val) {\n    for (int offset = warpSize / 2; offset > 0; offset >>= 1) {\n        unsigned int mask = __activemask();\n        val += __shfl_down_sync(mask, val, offset);\n    }\n    return val;\n}\n```\n\n对于翘曲移位，我们需要让 CUDA 调度器识别活动线程，让翘曲移位函数进行缩减。\n\n第二步是使用之前的扭曲级缩减编写一个块级缩减函数。我们将在共享内存中收集之前的结果，并根据结果进行第二次缩减。下面的代码显示了如何实现这一点:\n\n```cpp\n__inline__ __device__ float block_reduce_sum(float val) {\n    // Shared mem for 32 partial sums\n    static __shared__ float shared[32]; \n    int lane = threadIdx.x % warpSize;\n    int wid = threadIdx.x / warpSize;\n\n    val = warp_reduce_sum(val); // Warp-level partial reduction\n    if (lane == 0)\n        shared[wid] = val; // Write reduced value to shared memory\n    __syncthreads(); // Wait for all partial reductions\n\n    //read from shared memory only if that warp existed\n    if (wid == 0) {\n        val = (threadIdx.x < blockDim.x / warpSize) ? shared[lane] : 0;\n        val = warp_reduce_sum(val); //Final reduce within first warp\n    }\n    return val;\n}\n```\n\n现在，我们将实现累积输入数据的约简核函数，并根据我们已经实现的块级约简进行约简。由于我们只是专注于优化翘曲级优化，所以整体设计与之前的版本相同。下面的代码显示了内核函数:\n\n```cpp\n__global__ void\nreduction_kernel(float *g_out, float *g_in, unsigned int size) {\n    unsigned int idx_x = blockIdx.x * blockDim.x + threadIdx.x;\n    // cumulates input with grid-stride loop and save to share memory\n    float sum[NUM_LOAD] = { 0.f };\n    for (int i = idx_x; i < size; i += blockDim.x * gridDim.x * NUM_LOAD) {\n        for (int step = 0; step < NUM_LOAD; step++)\n            sum[step] += (i + step * blockDim.x * gridDim.x < size) ? g_in[i + step * blockDim.x * gridDim.x] : 0.f;\n    }\n    for (int i = 1; i < NUM_LOAD; i++)\n        sum[0] += sum[i];\n    // warp synchronous reduction\n    sum[0] = block_reduce_sum(sum[0]);\n\n    if (threadIdx.x == 0)\n        g_out[blockIdx.x] = sum[0];\n}\n```\n\n然后，让我们使用以下命令编译代码:\n\n```cpp\n$ nvcc -run -m64 -gencode arch=compute_70,code=sm_70 -I/usr/local/cuda/samples/common/inc -o reduction ./reduction.cpp ./reduction_wp_kernel.cu\n```\n\n下面的截图显示了执行时间的减少:\n\n![](img/71029119-ad15-4383-9eb8-ccc6b3850aa7.png)\n\n没有主机代码修改可用于从扭曲原语切换到协作组。因此，我们可以对两个简化实现使用相同的宿主代码。\n\n我们已经介绍了 CUDA 中的翘曲同步编程。它的应用不仅仅限于约简，还可以用于其他并行算法:扫描、二进制排序和转置。如果您需要了解更多信息，可以查看以下文章:\n\n*   [http://on-demand . gputechconf . com/GTC/2017/presentation/s 7622-Kyrylo-perelygin-robust-and-scalable-cuda . pdf](http://on-demand.gputechconf.com/gtc/2017/presentation/s7622-Kyrylo-perelygin-robust-and-scalable-cuda.pdf)\n*   [https://devblogs . NVIDIA . com/using-cuda-warp-level-primitive/](https://devblogs.nvidia.com/using-cuda-warp-level-primitives/)\n*   [https://dev blogs . NVIDIA . com/fast-parallel-reductions-Kepler/](https://devblogs.nvidia.com/faster-parallel-reductions-kepler/)\n*   [http://on-demand . gputechconf . com/GTC/2013/presentations/S3174-Kepler-Shuffle-Tips-ticks . pdf](http://on-demand.gputechconf.com/gtc/2013/presentations/S3174-Kepler-Shuffle-Tips-Tricks.pdf)\n\n# 灵活线程处理的协作组\n\nCUDA 9.0 引入了名为**协作组**的新 CUDA 编程特性。这引入了一种新的 CUDA 编程设计模式，通过指定分组操作来实现 CUDA 集合操作。利用这一点，程序员可以编写 CUDA 代码，明确地控制 CUDA 线程。\n\n首先，让我们看看什么是协作组及其编程优势。\n\n# CUDA 线程块中的协作组\n\n协作组提供了显式的 CUDA 线程分组对象，这有助于程序员更清楚、更方便地编写集体操作。例如，我们需要获得一个掩码来控制扭曲中的活动 CUDA 线程，以进行扭曲移动操作。另一方面，协作组对象将可用的线程绑定为一个瓦片，我们将它们作为一个对象来控制。这给 CUDA C 编程带来了 C++ 语言的好处。\n\n合作小组的基本类型是`thread_group`。这将启用一个 C++ 类风格的类型`thread_group`，它可以通过`is_valid()`、`size()`和`thread_rank()`功能提供其配置信息。此外，这提供了可以应用于组中所有 CUDA 线程的集合函数。这些功能如下:\n\n|  | \n\nthread_group 集合函数\n\n |\n| \n\n**识别活动线程**\n\n | `tiled_partition()`、`coalesced_threads()` |\n| \n\n**屏蔽活动线程**\n\n | `any()`、`all()`、`ballot()``match_any()`、`match_all()` |\n| \n\n**同步数据交换**\n\n | `shfl()`、`shfl_up()`、`shfl_down()`、`shfl_xor()` |\n| \n\n**线程同步**\n\n | `sync()` |\n\n这些函数列表类似于扭曲级基本函数。因此，扭曲级别的基本操作可以用协作组来代替。`thread_group`可以被更小的`thread_group`、`thread_block_tile`或`coalesced_group`分割。\n\n协作组还提供了线程块编程的灵活性。使用下面一行代码，我们可以处理一个线程块:\n\n```cpp\nthread_block block = this_thread_block();\n```\n\n`thread_block`提供 CUDA 内置的关键字换行功能，我们用它来获取一个块索引和线程索引:\n\n```cpp\ndim3 group_index();  // 3-dimensional block index within the grid\ndim3 thread_index(); // 3-dimensional thread index within the block\n```\n\n我们可以使用`this_thread_block()`获得一个线程块对象，如下所示:\n\n```cpp\nthread_block block = this_thread_block();\n```\n\n现在，让我们看看与传统的 CUDA 内置变量相比，合作组的优势是什么。\n\n# 合作团体的好处\n\n使用协作组提供了更多的 C++ 可编程性，而不是使用传统的 CUDA 内置变量。使用`thread_block`组，您可以将内核代码从使用内置变量切换到合作组的索引。但是，合作团体的真正力量不止于此。让我们在以下几节中介绍它的优点。\n\n# 模块性\n\n通过协作组，程序员可以模块化他们对应于障碍目标的集体操作内核代码。这有助于避免疏忽，假设所有线程都在并发运行，从而导致死锁和争用情况。下面是一个死锁和 CUDA 线程同步正常操作的例子:\n\n![](img/50c46410-4362-4702-8911-d95b431f6817.png)\n\n对于左边的例子，内核代码打算同步 CUDA 线程块中的一部分线程。这段代码通过指定屏障目标来最小化同步开销。但是，它引入了死锁情况，因为`__syncthreads()`调用了一个屏障，等待所有 CUDA 线程到达该屏障。但是`__synchthroead()`不能满足其他人，等待。右手边的例子显示了合理的操作，因为它没有任何死锁点，因为线程块中的所有线程都可以满足`__syncthreads()`。\n\n另一方面，在协作组应用编程接口中，CUDA 程序员指定要同步的线程组。协作组启用显式同步目标，以便程序员可以让 CUDA 线程显式同步。这个项目也可以被视为一个实例，这样我们就可以将这个实例传递给设备函数。\n\n下面的代码显示了协作组如何提供显式同步对象，并让它们作为实例来处理:\n\n```cpp\n__device__ bar(thread_group block, float *x) {\n    ...\n    block.sync();\n    ...\n}\n\n__global__ foo() {\n    bar(this_thread_block(), float *x);\n}\n```\n\n正如您在前面的示例代码中看到的，内核代码可以指定同步组，并将它们作为参数作为`thread_group`传递。这有助于我们在子程序中指定同步目标。因此，程序员可以通过使用协作组来防止无意中的死锁。此外，我们可以将不同类型的组设置为`thread_group`类型，并重用同步代码。\n\n# 显式分组线程操作和竞争条件避免\n\n协作组通过在扭曲中平铺线程来支持扭曲级别的协作操作。如果图块大小与扭曲大小匹配，CUDA 可以省略扭曲的隐式同步，确保正确的内存操作以避免争用情况。通过消除隐式同步，可以提高图形处理器的性能。历史上，有经验的 CUDA 程序员使用分离的扭曲进行扭曲级同步。这意味着扭曲中的协同操作不必与其他扭曲操作同步。这释放了 GPU 性能。然而，这是有风险的，因为它在合作操作之间引入了竞争条件。\n\n# 动态活动线程选择\n\nCUDA 协作组的另一个好处是程序员可以在扭曲中选择活动线程，以避免分支发散效应。由于 CUDA 是一个 SIMT 体系结构，一个指令单元发出一组线程，如果它们遇到一个分支，就没有办法禁用发散。但是，从 CUDA 9.0 开始，程序员可以使用`coalesced_threads()`选择将在分支块中活动的活动线程。这将通过禁用不采用分支的线程来返回合并的线程。然后，SM 的指令单元发出活动线程组中的下一个活动线程。\n\n# 应用于平行归约\n\n我们将更新之前的简化内核代码，以使用协作组。从前面的内核代码中，您可以很容易地应用协作组“`thread_block`”，如下所示:\n\n```cpp\n__global__ void\n reduction_kernel(float* g_out, float* g_in, unsigned int size)\n{\n    unsigned int idx_x = blockIdx.x * blockDim.x + threadIdx.x;\n\n    thread_block block = this_thread_block();\n\n    extern __shared__ float s_data[];\n```\n\n我们不必更新数据输入累积部分，所以让我们更新每个线程块的缩减部分。以下代码显示了块大小缩减的示例:\n\n```cpp\n    // do reduction\n    for (unsigned int stride = block.group_dim().x / 2; stride > 0; \n         stride >>= 1) {\n        if (block.thread_index().x < stride) {\n            s_data[block.thread_index().x] += \n                s_data[block.thread_index().x + stride];\n            block.sync(); // threads synchronization in a branch\n        }\n    }\n}\n```\n\n使用以下命令，估计操作性能为 0.264 毫秒:\n\n```cpp\n$ nvcc -run -m64 -gencode arch=compute_70,code=sm_70 -I/usr/local/cuda/samples/common/inc -o reduction_cg -rdc=true ./reduction.cpp ./reduction_cg_kernel.cu \n```\n\n前面的命令显示了与以前版本相同的性能。\n\n# 避免僵局的合作小组\n\n协作组可以支持独立的 CUDA 线程调度。因此，我们可以用一个组单独控制 CUDA 线程，并显式同步它们。目标组可以是预定义的图块，但也可以根据条件分支确定，如下所示:\n\n```cpp\n// do reduction\nfor (unsigned int stride = block.group_dim().x / 2; stride > 0; \n     stride >>= 1) {\n    // scheduled threads reduce for every iteration\n    // and will be smaller than a warp size (32) eventually.\n    if (block.thread_index().x < stride) { \n        s_data[block.thread_index().x] += s_data[\n                       block.thread_index().x + stride];\n\n        // __syncthreads(); // (3) Error. Deadlock.\n        // block.sync();    // (4) Okay. Benefit of Cooperative Group\n    }\n    // __syncthreads();     // (1) Okay\n    block.sync();           // (2) Okay\n}\n```\n\n这段代码有四个线程块同步选项。选项`(1)`和`(2)`是具有不同 API 的等价操作。另一方面，选项`(3)`和`(4)`则没有。选项`(3)`引入了 CUDA 线程的死锁，主机不能有 CUDA 内核的返回，因为活动的 CUDA 线程不能与未激活的 CUDA 线程同步。另一方面，由于协作组的自动活动线程识别，选项`(4)`起作用。这有助于我们避免意外错误，并轻松开发复杂的算法。\n\n英伟达在以下文档中提供了合作小组的详细描述:\n\n*   [https://devblogs.nvidia.com/cuda-9-features-revealed](https://devblogs.nvidia.com/cuda-9-features-revealed)\n*   [http://on-demand . gputechconf . com/GTC/2017/presentation/s 7622-Kyrylo-perelygin-robust-and-scalable-cuda . pdf](http://on-demand.gputechconf.com/gtc/2017/presentation/s7622-Kyrylo-perelygin-robust-and-scalable-cuda.pdf)\n\n你也可以从`cooperative_groups.h`本身了解它的架构和完整的 API 列表。\n\n# CUDA 内核中的循环展开\n\nCUDA 也可以像其他编程语言一样获得循环展开的好处。通过这种技术，CUDA 线程可以减少或消除循环控制开销，例如每次迭代的循环结束测试、分支惩罚等等。\n\n如果 CUDA 编译器能够识别循环的迭代次数，它会自动展开小循环。程序员也可以放置`#pragma unroll`指令给编译器一个提示，或者只是将循环代码重写为一组独立的语句。应用循环展开很简单，因此您可以很容易地将它应用到当前的工作代码中。\n\n让我们将此应用到我们的并行约简实现中。就像 C/C++ 中的普通循环展开指令一样，我们可以将`#pragma`循环展开指令放在`for`循环之上。NVCC 编译器可以展开循环，因为编译器可以自己获得`group.size()`的精确大小:\n\n```cpp\ntemplate <typename group_t>\n__inline__ __device__ float\n warp_reduce_sum(group_t group, float val)\n{\n    #pragma unroll\n    for (int offset = group.size() / 2; offset > 0; offset >>= 1)\n        val += group.shfl_down(val, offset);\n    return val;\n}\n```\n\n使用以下命令，估计操作性能为 0.263 毫秒:\n\n```cpp\n$ nvcc -run -m64 -gencode arch=compute_70,code=sm_70 -I/usr/local/cuda/samples/common/inc -o reduction_cg -rdc=true ./reduction.cpp ./reduction_cg_kernel.cu\n```\n\n如果你更喜欢使用翘曲图元功能，可以像下面这样写`warp_reduce_sum`。通过将`group.size()`替换为`warpSize`，可以重用循环代码，但是在这种情况下，这稍微快了一点:\n\n```cpp\n#define FULL_MASK 0xFFFFFFFF\n__inline__ __device__ float\nwarp_reduce_sum(float val) {\n#pragma unroll 5\n    for (int offset = 1; offset < 6; offset++)\n        val += __shfl_down_sync(FULL_MASK, val, warpSize >> offset);\n    return val;\n}\n```\n\n运行以下命令来编译前面的代码:\n\n```cpp\nnvcc -run -m64 -gencode arch=compute_70,code=sm_70 -I/usr/local/cuda/samples/common/inc -o reduction_wp -rdc=true ./reduction.cpp ./reduction_wp_kernel.cu\n```\n\n其结果为 0.263 ms，与之前的结果相同。\n\n使用循环展开有一个陷阱。展开的代码执行可能会因寄存器使用增加而导致占用率降低。此外，由于代码执行大小增加，指令高速缓存未命中损失可能更大。\n\n# 原子操作\n\n在 CUDA 编程中，程序员可以使用原子 API 来更新来自多个 CUDA 线程的共享资源。这些原子应用编程接口保证消除共享资源的竞争条件，因此我们可以期待并行执行的一致输出。该操作对于获取统计参数(如直方图、平均值、总和等)特别有用。我们还可以简化代码实现。例如，可以使用以下代码中的`atomicAdd()`函数编写归约操作:\n\n```cpp\n__global__ void\n atomic_reduction_kernel(float *data_out, float *data_in, int size)\n {\n     int idx_x = blockIdx.x * blockDim.x + threadIdx.x;\n     atomicAdd(&data_out[0], data_in[idx_x]);\n }\n```\n\n如您所见，原子函数简化了所需的操作。但是，它的性能很慢，因为原子操作将所有请求序列化到共享资源。运行以下命令查看执行时间:\n\n```cpp\n$ nvcc -run -m64 -gencode arch=compute_70,code=sm_70 -I/usr/local/cuda/samples/common/inc -o mixed_precision_single ./mixed_precision.cu\n```\n\n在我的特斯拉 V100 上显示的这个内核功能花费了 39 毫秒，这比原始版本(4.609 毫秒)慢得多。因此，推荐的原子操作用法是仅在必要时限制请求。例如，对于并行约简问题，我们可以在某个级别上并行约简项目，并使用原子操作来输出最终结果。\n\n下图显示了另一种可能的方法。这取代了块式缩减为`atomicAdd`:\n\n![](img/654607c4-860e-4c99-a464-97de231fd946.png)\n\n在上图中，我们可以看到有两个约简点:一个**扭曲**和一个**线程块**，分块约简结果由单个全局内存变量原子性地累加。因此，我们可以消除第二次约简迭代。下面的截图显示了第二次缩减迭代的内核优化优先级(左边)和性能限制分析(右边):\n\n![](img/3c35e7bf-577c-4903-be51-7f79d9a57267.png)\n\nKernel Optimization Priorities with Performance Limiter Analysis (2nd iteration)\n\n换句话说，第二次迭代的性能受到延迟的限制，因为它的网格很小。因此，我们可以通过删除它来减少执行时间。\n\n现在，让我们实现这个设计，看看如何改变性能。我们只需要更新缩减内核函数的最后一部分:\n\n```cpp\n__global__ void\n reduction_kernel(float* g_out, float* g_in, unsigned int size)\n{\n    unsigned int idx_x = blockIdx.x * (2 * blockDim.x) + threadIdx.x;\n\n    thread_block block = this_thread_block();\n\n    // cumulates input with grid-stride loop and save to share memory\n    float sum[NUM_LOAD] = { 0.f };\n    for (int i = idx_x; i < size; i += blockDim.x \n         * gridDim.x * NUM_LOAD)\n    {\n        for (int step = 0; step < NUM_LOAD; step++)\n            sum[step] += (i + step * blockDim.x * gridDim.x < size) ? \n                         g_in[i + step * blockDim.x * gridDim.x] : 0.f;\n    }\n    for (int i = 1; i < NUM_LOAD; i++)\n        sum[0] += sum[i];\n    // warp synchronous reduction\n    sum[0] = block_reduce_sum(block, sum[0]);\n\n    sum[0] = block_reduce_sum(sum[0]);\n\n    // Performing Atomic Add per block\n    if (block.thread_rank() == 0) {\n        atomicAdd(&g_out[0], sum);\n    }\n}\n```\n\n然后，我们将删除第二次迭代函数调用。因此，如果原子操作的延迟比这更短，我们可以消除内核调用延迟并获得更好的性能。运行以下命令:\n\n```cpp\n$ nvcc -run -m64 -gencode arch=compute_70,code=sm_70 -I/usr/local/cuda/samples/common/inc -o reduction_atomic_block ./reduction.cpp ./reduction_blk_atmc_kernel.cu\n```\n\n幸运的是，在特斯拉 V100 上，估计的执行时间为 0.259 毫秒，因此我们可以获得稍微增强的结果。\n\n如果你想了解更多 CUDA C 中的原子操作，请点击此链接查看编程指南:[https://docs . NVIDIA . com/CUDA/CUDA-C-编程-指南/index . html #原子-函数](https://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html#atomic-functions)。\n\n# 低/混合精度操作\n\n混合精度是一种探索低精度的技术，可以获得高精度的结果。这种技术以低精度计算核心运算，并以高精度运算生成输出。与高精度计算相比，低精度运算计算具有减少内存带宽和提高计算吞吐量的优点。如果低精度足以从高精度应用中获得目标精度，这种技术可以在这种折衷的情况下提高性能。NVIDIA 开发者博客介绍这种可编程性:[https://devblogs . NVIDIA . com/mixed-precision-programming-cuda-8](https://devblogs.nvidia.com/mixed-precision-programming-cuda-8)。\n\n在这种情况下，CUDA 将其支持扩展到低于 32 位数据类型的低精度工具，如 8/16 位整数(INT8/INT16)和 16 位浮点(FP16)。对于那些低精度的数据类型，一个 GPU 可以使用**单指令，多数据** ( **SIMD** )操作，带有一些特定的 API。在本节中，我们将研究这两种针对混合精度目的的低精度操作的指令。\n\n为了从中获益，您需要确认您的 GPU 能够支持低混合精度操作和支持数据类型。在特定的图形处理器中支持低精度计算是可能的，精度因图形处理器芯片组而异。具体来说，GP102(特斯拉 P40 和泰坦 X)、GP104(特斯拉 P4)、GP106 支持 INT8 而 GP100(特斯拉 P100)和 GV100(特斯拉 V100)支持 FP16(半精密)操作。特斯拉 GV100 兼容 INT8 操作，性能没有下降。\n\nCUDA 具有一些特殊的内在功能，支持低精度数据类型的 SIMD 运算。\n\n# 半精密操作\n\nCUDA 为半尺寸浮点数据类型(FP16)提供了内在函数，开发人员可以选择 CUDA 是为每条指令计算一个值还是两个值。CUDA 还提供单精度和半精度之间的类型转换功能。由于 FP16 的精度限制，您必须使用固有转换来处理单精度值。\n\n现在，让我们实现并测试 GPU 的 FP16 操作。图形处理器可以支持比计算能力 5.3 更高的本机计算。但是有些 GPU 不支持这个，所以请仔细检查你的 GPU 是否支持这个半精度操作。\n\nCUDA C 中的半精度数据类型是`half`，但是也可以使用`__half`类型。对于应用编程接口，CUDA 用这个数据类型提供相关的内部函数，如`__hfma()`、`__hmul()`和`__hadd()`。这些内在函数还使用`__hfma2()`、`__hmul2()`和`__hadd2()`同时为本机操作提供两个数据。有了这些函数，我们可以编写混合精度的操作内核代码:\n\n```cpp\n__global__ void hfma_kernel(half *d_x, half *d_y, float *d_z, int size)\n {\n     int idx_x = blockIdx.x * blockDim.x + threadIdx.x;\n     int stride = gridDim.x * blockDim.x;\n\n     half2 *dual_x = reinterpret_cast<half2*>(d_x);\n     half2 *dual_y = reinterpret_cast<half2*>(d_y);\n     float2 *dual_z = reinterpret_cast<float2*>(d_z);\n\n     extern __shared__ float2 s_data[];\n\n #if __CUDA_ARCH__ >= 530\n     for (int i = idx_x; i < size; i+=stride) {\n         s_data[threadIdx.x] = __half22float2(__hmul2(dual_y[i], \n                                                      dual_x[i]));\n         __syncthreads();\n         dual_z[i] = s_data[threadIdx.x];\n     }\n     #else\n     for (int i = idx_x; i < size; i+=stride) {\n         s_data[threadIdx.x] = __half22float2(dual_x[i]) * \n                               __half22float2(dual_y[i]);\n         __syncthreads();\n         dual_z[i] = s_data[threadIdx.x];\n     }\n     #endif\n }\n```\n\n对于那些不支持本机半精度操作的图形处理器，我们的代码在编译时检查 CUDA 的计算能力，并确定它应该采取哪种操作。\n\n由于每个 CUDA 线程将操作两个数据，下面的代码使用一半大小的网格调用内核函数:\n\n```cpp\nint n_threads = 256;\nint num_sms;\nint num_blocks_per_sm;\ncudaDeviceGetAttribute(&num_sms, cudaDevAttrMultiProcessorCount, 0);\ncudaOccupancyMaxActiveBlocksPerMultiprocessor(&num_blocks_per_sm,   \n    hfma_kernel, n_threads, n_threads*sizeof(float2));\nint n_blocks = min(num_blocks_per_sm * num_sms, \n                   (size/2 + n_threads - 1) / n_threads);\nhfma_kernel<<< n_blocks, n_threads, n_threads * sizeof(float2) \n               >>>(X.d_ptr_, Y.d_ptr_, Z.d_ptr_, size/2);\n```\n\n其他初始化代码和基准代码在示例配方代码中实现，请查看。\n\n我们已经报道了 FP16 精确操作中的 FMA 操作。CUDA C 提供各种半精度运算([https://docs . NVIDIA . com/CUDA/CUDA-MATH-API/group _ _ CUDA _ _ MATH _ _ INTRINSIC _ _ half . html](https://docs.nvidia.com/cuda/cuda-math-api/group__CUDA__MATH__INTRINSIC__HALF.html))。请检查其他操作。\n\n# 8 位整数和 16 位数据的点积运算和累加(DP4A 和 DP2A)\n\n对于 8 位/16 位整数，CUDA 提供矢量化点积运算。它们是 DP4A(具有累加的四元点积)和 DP2A(具有累加的二元点积)。使用这些功能，CUDA 开发人员可以进行更快的操作。CUDA 8.0 开发博客用直观的图形介绍了这些功能([https://devblogs . NVIDIA . com/mixed-precision-programming-CUDA-8/](https://devblogs.nvidia.com/mixed-precision-programming-cuda-8/))。下面显示了图形处理器的点积和累加操作是如何工作的:\n\n![](img/4c84a489-8c06-421c-baf2-0930fb1b51c4.png)\n\n使用它，您可以只编写 8 位或 8 位/16 位混合运算，并进行 32 位整数累加。其他操作，如求和、相加和比较，也适用于 SIMD 内在函数。\n\n如前所述，有特定的 GPU 可以支持具有特殊功能的 INT8/INT16 操作(`dp4a`和`dp2a`)。支持图形处理器的计算能力必须高于 6.1。\n\n现在，让我们实现一个使用`dp4a` API 的内核函数，如下所示:\n\n```cpp\n__global__ void dp4a_kernel(char *d_x, char *d_y, int *d_z, int size)\n {\n     int idx_x = blockIdx.x * blockDim.x + threadIdx.x;\n     int stride = gridDim.x * blockDim.x;\n\n #if __CUDA_ARCH__ >= 610\n     char4 *quad_x = (char4 *)d_x;\n     char4 *quad_y = (char4 *)d_y;\n\n     for (int i = idx_x; i < size; i+=stride)\n         d_z[i] = __dp4a(quad_y[i], quad_x[i], 0);\n #else\n     for (int i = idx_x; i < size; i+=4*stride) {\n         int sum = 0;\n         for (int j = 0; j < 4; j++)\n             sum += d_y[4 * i + j] * d_x[4 * i + j];\n         d_z[i] = sum + 0;\n     }\n #endif\n }\n```\n\n在该函数中，`__dp4a`获取两个字符数组合并四个项目，并输出其点积输出。由于 Pascal 具有 CUDA 计算能力(版本 6.1)，因此支持该应用编程接口。但是低于 6.1 版本的老 GPU 架构，需要使用原来的操作。\n\n下面的代码显示了我们将如何调用实现的内核函数。它的网格大小减少了四个，因为每个 CUDA 线程将对四个项目进行操作:\n\n```cpp\nint n_threads = 256;\nint num_sms;\nint num_blocks_per_sm;\ncudaDeviceGetAttribute(&num_sms, cudaDevAttrMultiProcessorCount, 0);\ncudaOccupancyMaxActiveBlocksPerMultiprocessor(&num_blocks_per_sm, \n    dp4a_kernel, n_threads, n_threads*sizeof(int));\nint n_blocks = min(num_blocks_per_sm * num_sms, (size/4 + n_threads \n                                                  - 1) / n_threads);\ndp4a_kernel<<< n_blocks, n_threads, n_threads * sizeof(int) >>>  \n    (X.d_ptr_, Y.d_ptr_, Z.d_ptr_, size/4);\n```\n\n其他初始化代码和基准代码在示例代码中实现，例如前面的示例代码。\n\n我们已经介绍了 INT8 的点操作，但是 CUDA C 还提供了其他 INT8 类型的 SIMD 内在函数([https://docs . NVIDIA . com/CUDA/CUDA-MATH-API/group _ _ CUDA _ _ MATH _ _ INTRINSIC _ _ SIMD . html](https://docs.nvidia.com/cuda/cuda-math-api/group__CUDA__MATH__INTRINSIC__SIMD.html))。请检查此文档以了解其他操作。\n\n# 衡量绩效\n\n示例代码有三个版本的混合精度操作:单精度、半精度和 INT8。随着精度的下降，我们可以为每个 CUDA 线程添加更多的操作。\n\n针对单精度、半精度和 INT8 操作运行以下命令:\n\n```cpp\n# Single-precision\n$ nvcc -run -m64 -gencode arch=compute_70,code=sm_70 -I/usr/local/cuda/samples/common/inc -o mixed_precision_single ./mixed_precision.cu\n\n# Half-precision\n$ nvcc -run -m64 -gencode arch=compute_70,code=sm_70 -I/usr/local/cuda/samples/common/inc -o mixed_precision_half ./mixed_precision_half.cu\n\n# INT8 \n$ nvcc -run -m64 -gencode arch=compute_70,code=sm_70 -I/usr/local/cuda/samples/common/inc -o mixed_precision_int ./mixed_precision_int.cu\n```\n\n下表显示了每个精密操作的估计性能:\n\n| 精确 | 测量性能 |\n| FP32 | 59，441 格弗洛姆 |\n| FP16 | 86.037 格弗洛普斯 |\n| INT8 | 196，225 戈巴 |\n\n由于我们的实现没有优化，测量的性能比特斯拉 V100 的理论性能低得多。当你分析它们时，它们会报告说它们是高度内存受限的。换句话说，我们需要优化它们，使其在算术上有界，以获得接近理论的性能。\n\n# 摘要\n\n在本章中，我们介绍了如何配置 CUDA 并行操作并对其进行优化。为此，我们必须了解 CUDA 的分层架构线程块和流式多处理器之间的关系。通过一些性能模型——占用率、性能限制器分析和屋顶模型——我们可以优化更多的性能。然后，我们介绍了一些新的 CUDA 线程可编程性，协作组，并了解了这如何简化并行编程。我们优化了并行约简问题，用![](img/204bf10b-1a7d-4b62-ad48-17dbbb31f177.png)元素实现了 0.259 ms，在相同的 GPU 下速度提升了 17.8 倍。最后，我们了解了 CUDA 的半精度(FP16)和 INT8 精度的 SIMD 操作。\n\n我们本章的经验集中在 GPU 的并行处理级编程上。然而，CUDA 编程包括系统级编程。基本上，GPU 是一种额外的计算资源，独立于主机工作。这带来了额外的计算能力，但另一方面也会带来延迟。CUDA 提供的 API 函数可以利用这一点，隐藏延迟，并实现 GPU 的全部性能。我们将在下一章讨论这个问题。"
  },
  {
    "path": "docs/learn-cuda-prog/04.md",
    "content": "# 四、内核执行模型及优化策略\n\nCUDA 编程有一个主机操作的程序。例如，我们需要分配全局内存，将数据传输到 GPU，执行内核函数，将数据传输回主机，并清理全局内存。这是因为 GPU 是系统中的一个额外处理单元，所以我们需要关心它的执行和数据传输。这是 GPU 编程的另一个方面，与 CPU 编程不同。\n\n在本章中，我们将介绍控制 CUDA 操作的 CUDA 内核执行模型和 CUDA 流。然后，我们将讨论系统级的优化策略。然后，我们将介绍 CUDA 事件来度量 GPU 事件时间，以及如何使用 CUDA 事件来度量内核执行时间。之后，我们将介绍各种 CUDA 内核执行模型，并讨论这些特性给 GPU 操作带来了什么。\n\n本章将涵盖以下主题:\n\n*   用 CUDA 流执行内核\n*   流水线图形处理器执行\n*   CUDA 回调函数\n*   具有优先级的 CUDA 流\n*   使用 CUDA 事件估计内核执行时间\n*   CUDA 动态并行\n*   网格级协作组\n*   用 OpenMP 调用 CUDA 内核\n*   多进程服务\n*   内核执行开销比较\n\n# 技术要求\n\n本章要求我们使用 9.x 以后的 CUDA 版本，GPU 架构应该是 Volta 或者 Turing。如果您使用具有 Pascal 架构的 GPU，请跳过*网格级协作组*部分，因为该功能是为 Volta 架构引入的。\n\n# 用 CUDA 流执行内核\n\n流是在 CUDA 编程中与图形处理器相关的命令序列。换句话说，所有内核调用和数据传输都由 CUDA 流处理。默认情况下，CUDA 提供一个默认流，所有命令都隐式使用该流。因此，我们不必自己处理这个问题。\n\nCUDA 支持显式创建的附加流。虽然流中的操作是顺序的，但 CUDA 可以通过使用多个流同时执行多个操作。让我们学习如何处理流，以及它们有什么特性。\n\n# CUDA 流的使用\n\n以下代码显示了如何创建、使用和终止 CUDA 流的示例:\n\n```cpp\ncudaStream_t stream;\ncudaStreamCreate(&stream);\nfoo_kernel<<< grid_size, block_size, 0, stream >>>();\ncudaStreamDestroy(stream);\n```\n\n如您所见，我们可以使用`cudaStream_t`处理一个 CUDA 流。并且，我们可以使用`cudaStreamCreate()`创建它，并使用`cudaStreamDestroy()`终止它。注意，我们应该提供一个指向`cudaStreamCreate()`的指针。创建的流被传递给内核的第四个参数。\n\n但是，我们之前没有提供这样的流。这是因为 CUDA 提供了一个默认流，这样所有的 CUDA 操作都可以运行。现在，让我们编写一个使用默认流和多个流的应用。然后，我们将看到如何更改我们的应用。\n\n首先，让我们编写一个使用默认 CUDA 流的应用，如下所示:\n\n```cpp\n__global__ void foo_kernel(int step)\n{\n    printf(\"loop: %d\\n\", step);\n}\n\nint main()\n{\n    for (int i = 0; i < 5; i++)\n // CUDA kernel call with the default stream\n foo_kernel<<< 1, 1, 0, 0 >>>(i);\n    cudaDeviceSynchronize();\n    return 0;\n}\n```\n\n在代码中可以看到，我们调用的内核函数的流 ID 为`0`，因为默认流的标识值为`0`。编译代码并查看执行输出:\n\n```cpp\n$ nvcc -m64 -run -gencode arch=compute_70,code=sm_70 -I/usr/local/cuda/samples/common/inc -o cuda_default_stream ./1_cuda_default_stream.cu\n```\n\n产量如何？我们可以预期输出将是循环索引的顺序。以下时间线视图显示了此代码的操作:\n\n![](img/870c3559-e2cf-4a27-a481-53697923141d.png)\n\n可以预期，在同一个流中进行循环操作会显示内核执行的顺序。那么，如果我们使用多个 CUDA 流，并且每个循环步骤使用不同的流，那么可以改变什么呢？下面的代码显示了用不同的流从 CUDA 内核函数打印循环索引的示例:\n\n```cpp\n__global__ void foo_kernel(int step)\n{\n    printf(\"loop: %d\\n\", step);\n}\n\nint main()\n{\n    int n_stream = 5;\n    cudaStream_t *ls_stream;\n    ls_stream = (cudaStream_t*) new cudaStream_t[n_stream];\n\n    // create multiple streams\n    for (int i = 0; i < n_stream; i++)\n        cudaStreamCreate(&ls_stream[i]);\n\n    // execute kernels with the CUDA stream each\n    for (int i = 0; i < n_stream; i++)\n        foo_kernel<<< 1, 1, 0, ls_stream[i] >>>(i);\n\n    // synchronize the host and GPU\n    cudaDeviceSynchronize();\n\n    // terminates all the created CUDA streams\n    for (int i = 0; i < n_stream; i++)\n        cudaStreamDestroy(ls_stream[i]);\n    delete [] ls_stream;\n\n    return 0;\n}\n```\n\n在这段代码中，我们有五个调用，与前面的代码相同，但是这里我们将使用五个不同的流。为此，我们构建了一个`cudaStream_t`数组，并为每个数组创建了流。你对这种变化有什么期待？打印输出将与以前的版本相同。运行以下命令编译此代码:\n\n```cpp\n$ nvcc -m64 -run -gencode arch=compute_70,code=sm_70 -I/usr/local/cuda/samples/common/inc -o cuda_mutli_stream ./2_cuda_multi_stream.cu\n```\n\n然而，这并不能保证他们有相同的操作。正如我们在开始时所讨论的，这段代码显示了多个流的并发性，如下图所示:\n\n![](img/c34f9def-ab64-4d9e-a159-f9ecb23c807d.png)\n\n正如你在截图底部看到的，五个独立的流并发执行同一个内核函数，它们的操作相互重叠。由此，我们可以看出溪流的两个特征，如下所示:\n\n1.  内核执行与主机异步。\n2.  不同流中的 CUDA 操作相互独立。\n\n利用流的并发性，我们可以通过重叠独立的操作来获得额外的优化机会。\n\n# 流级同步\n\nCUDA 流通过`cudaStreamSynchronize()`功能提供流级同步。使用此函数会强制主机等待某个流的操作结束。这为我们到目前为止使用的`cudaDeviceSynchronize()`功能提供了重要的优化。\n\n我们将在接下来的章节中讨论如何利用这个特性，但是让我们在这里讨论它的基本操作。前面的示例显示了循环中没有同步的并发操作。但是，我们可以使用`cudaStreamSynchronize()`函数暂停主机执行下一个内核执行。下面的代码显示了在内核执行结束时使用流同步的示例:\n\n```cpp\n// execute kernels with the CUDA stream each\nfor (int i = 0; i < n_stream; i++) {\n   foo_kernel<<< 1, 1, 0, ls_stream[i] >>>(i);\n   cudaStreamSynchronize(ls_stream[i]);\n}\n```\n\n我们可以很容易地预测，内核操作的并发性会因为同步而消失。为了证实这一点，让我们对其进行概要分析，看看这如何影响内核执行:\n\n```cpp\n$ nvcc -m64 -run -gencode arch=compute_70,code=sm_70 -I/usr/local/cuda/samples/common/inc -o cuda_mutli_stream_with_sync ./3_cuda_multi_stream_with_sync.cu\n```\n\n下面的截图显示了结果:\n\n![](img/f9a4b008-4225-4766-ac33-c26bec6a0fd6.png)\n\n如您所见，所有内核执行都没有重叠点，尽管它们是用不同的流执行的。使用这个特性，我们可以让主机等待特定的流操作以结果开始。\n\n# 使用默认流\n\n为了让多个流同时运行，我们应该使用我们显式创建的流，因为所有流操作都与默认流同步。下面的截图显示了默认流的同步操作效果:\n\n![](img/e31e5516-2fb9-434c-a0c7-9d729fbdcde0.png)\n\n我们可以通过修改我们的多流内核调用操作来实现这一点，如下所示:\n\n```cpp\nfor (int i = 0; i < n_stream; i++)\n    if (i == 3)\n        foo_kernel<<< 1, 1, 0, 0 >>>(i);\n    else\n        foo_kernel<<< 1, 1, 0, ls_stream[i] >>>(i);\n```\n\n运行以下命令编译代码:\n\n```cpp\n$ nvcc -m64 -run -gencode arch=compute_70,code=sm_70 -I/usr/local/cuda/samples/common/inc -o cuda_multi_stream_with_default ./4_cuda_multi_stream_with_default.cu\n```\n\n因此，我们可以看到最后一个操作不能与之前的内核执行重叠，但是我们必须等到第四个内核执行完成。\n\n# 流水线化图形处理器的执行\n\n多个流的主要好处之一是将数据传输与内核执行相重叠。通过重叠内核操作和数据传输，我们可以隐藏数据传输开销并提高整体性能。\n\n# 图形处理器流水线的概念\n\n当我们执行内核函数时，我们需要将数据从主机传输到 GPU。然后，我们将结果从 GPU 传输回主机。下图显示了在主机和内核执行之间传输数据的迭代操作示例:\n\n![](img/d8db7a9f-522f-4e8f-aa07-19efc6233fc8.png)\n\n然而，内核执行基本上是异步的，因为主机和 GPU 可以同时运行。如果主机和 GPU 之间的数据传输具有相同的特性，我们将能够重叠它们的执行，正如我们在前面部分中看到的。下图显示了当数据传输可以像正常内核操作一样执行，并与流一起处理时的操作:\n\n![](img/3b531d26-ec42-4576-9b9c-511bd175b612.png)\n\n在这个图中，我们可以看到主机和设备之间的数据传输可以与内核执行重叠。然后，这种重叠操作的好处是减少了应用的执行时间。通过比较两张图片的长度，您将能够确认哪个操作具有更高的操作吞吐量。\n\n关于 CUDA 流，所有 CUDA 操作——数据传输和内核执行——在同一个流中都是顺序的。然而，它们可以与不同的流同时运行。下图显示了多个流的内核操作的重叠数据传输:\n\n![](img/c3141f46-c05d-4e22-85b4-bd4a2c193f9a.png)\n\n为了实现这样的流水线操作，CUDA 有三个先决条件:\n\n1.  主机内存应该作为固定内存分配——CUDA 为此提供了`cudaMallocHost()`和`cudaFreeHost()`功能。\n2.  在不阻塞主机的情况下，在主机和图形处理器之间传输数据——CUDA 为此提供了`cudaMemcpyAsync()`功能。\n3.  管理每个操作以及不同的 CUDA 流，以实现并发操作。\n\n现在，让我们编写一个简单的应用来传递工作负载。\n\n# 构建流水线执行\n\n下面的代码显示了异步数据传输的一个片段，以及在执行结束时 CUDA 流的同步:\n\n```cpp\ncudaStream_t stream;\nfloat *h_ptr, *d_ptr;    size_t byte_size = sizeof(float) * BUF_SIZE;\n\ncudaStreamCreate(&stream);               // create CUDA stream\ncudaMallocHost(h_ptr, byte_size);        // allocates pinned memory\ncudaMalloc((void**)&d_ptr, byte_size);   // allocates a global memory\n\n// transfer the data from host to the device asynchronously\ncudaMemcpyAsync(d_ptr, h_ptr, byte_size, cudaMemcpyHostToDevice, stream);\n\n... { kernel execution } ...\n\n// transfer the data from the device to host asynchronously\ncudaMemcpyAsync(h_ptr, d_ptr, byte_size, cudaMemcpyDeviceToHost, stream);\ncudaStreamSynchronize(stream);\n\n// terminates allocated resources\ncudaStreamDestroy(stream);\ncudaFree(d_ptr);\ncudaFreeHost(h_ptr);\n```\n\n这段代码展示了如何分配固定内存，以及如何使用用户创建的流传输数据。通过合并这个例子和多个 CUDA 流操作，我们可以得到流水线化的 CUDA 操作。\n\n现在，让我们构建一个具有数据传输和内核执行的流水线操作的应用。在这个应用中，我们将使用一个内核函数，通过对流的数量进行切片来添加两个向量，并输出其结果。然而，内核实现并不需要对其进行任何更改，因为我们将在宿主代码级别进行更改。但是，我们将重复加法操作 500 次，以延长内核执行时间。因此，实现的内核代码如下:\n\n```cpp\n__global__ void\nvecAdd_kernel(float *c, const float* a, const float* b)\n{\n    int idx = blockIdx.x * blockDim.x + threadIdx.x;\n\n    for (int i = 0; i < 500; i++)\n        c[idx] = a[idx] + b[idx];\n}\n```\n\n为了处理每个流的操作，我们将创建一个管理 CUDA 流和 CUDA 操作的类。这个类将允许我们管理 CUDA 流和索引。下面的代码显示了该类的基本体系结构:\n\n```cpp\nclass Operator\n{\nprivate:\n    int index;\n\npublic:\n    Operator() {\n        cudaStreamCreate(&stream);    // create a CUDA stream\n    }\n\n    ~Operator() {\n        cudaStreamDestroy(stream);    // terminate the CUDA stream\n    }\n\n    cudaStream_t stream;\n    void set_index(int idx) { index = idx; }\n    void async_operation(float *h_c, const float *h_a, \n                         const float *h_b,\n                         float *d_c, float *d_a, float *d_b,\n                         const int size, const int bufsize);\n\n}; // Operator\n```\n\n现在，让我们编写一些顺序的 GPU 执行代码，我们在前面的部分中已经使用过，但是作为`Operator`类的成员函数，如下所示:\n\n```cpp\nvoid Operator::async_operation(float *h_c, const float *h_a, \n                          const float *h_b,\n                          float *d_c, float *d_a, float *d_b,\n                          const int size, const int bufsize)\n{\n    // start timer\n    sdkStartTimer(&_p_timer);\n\n    // copy host -> device\n    cudaMemcpyAsync(d_a, h_a, bufsize, \n                    cudaMemcpyHostToDevice, stream);\n    cudaMemcpyAsync(d_b, h_b, bufsize, \n                    cudaMemcpyHostToDevice, stream);\n\n    // launch cuda kernel\n    dim3 dimBlock(256);\n    dim3 dimGrid(size / dimBlock.x);\n    vecAdd_kernel<<< dimGrid, dimBlock, 0, \n                     stream >>>(d_c, d_a, d_b);\n\n    // copy device -> host\n    cudaMemcpyAsync(h_c, d_c, bufsize, \n                    cudaMemcpyDeviceToHost, stream);\n\n    printf(\"Launched GPU task %d\\n\", index);\n}\n```\n\n该函数的操作与我们之前使用的基本 CUDA 主机编程模式没有什么不同，只是我们在给定的`_stream`上应用了`cudaMemcpyAsync()`。然后，我们编写`main()`来处理多个运算符实例和页锁定内存:\n\n```cpp\nint main(int argc, char* argv[])\n{\n    float *h_a, *h_b, *h_c;\n    float *d_a, *d_b, *d_c;\n    int size = 1 << 24;\n    int bufsize = size * sizeof(float);\n    int num_operator = 4;\n\n    if (argc != 1)\n        num_operator = atoi(argv[1]);\n```\n\n现在，我们将使用`cudaMallocHost()`来分配主机内存以拥有固定内存，并初始化它们:\n\n```cpp\n    cudaMallocHost((void**)&h_a, bufsize);\n    cudaMallocHost((void**)&h_b, bufsize);\n    cudaMallocHost((void**)&h_c, bufsize);\n\n    srand(2019);\n    init_buffer(h_a, size);\n    init_buffer(h_b, size);\n    init_buffer(h_c, size);\n```\n\n而且，我们将拥有同样大小的设备存储器:\n\n```cpp\n    cudaMalloc((void**)&d_a, bufsize);\n    cudaMalloc((void**)&d_b, bufsize);\n    cudaMalloc((void**)&d_c, bufsize);\n```\n\n现在，我们将使用我们使用的类创建一个 CUDA 操作符列表:\n\n```cpp\n    Operator *ls_operator = new Operator[num_operator];\n```\n\n我们准备执行流水线操作。在开始执行之前，让我们放置一个秒表来查看整体执行时间，并查看重叠数据传输的好处，如下所示:\n\n```cpp\n    StopWatchInterface *timer;\n    sdkCreateTimer(&timer);\n    sdkStartTimer(&timer);\n```\n\n让我们使用一个循环来执行每个操作符，每个操作符将根据它们的顺序访问主机和设备内存。我们还将测量循环的执行时间:\n\n```cpp\n    for (int i = 0; i < num_operator; i++) {\n        int offset = i * size / num_operator;\n        ls_operator[i].set_index(i);\n        ls_operator[i].async_operation(&h_c[offset], \n                                       &h_a[offset], &h_b[offset],\n                                       &d_c[offset], \n                                       &d_a[offset], &d_b[offset],\n                                       size / num_operator, \n                                       bufsize / num_operator);\n    }\n\n    cudaDeviceSynchronize();\n    sdkStopTimer(&timer);\n```\n\n最后，我们将比较一个示例的结果，并打印出总体测量性能:\n\n```cpp\n    // prints out the result\n    int print_idx = 256;\n    printf(\"compared a sample result...\\n\");\n    printf(\"host: %.6f, device: %.6f\\n\", h_a[print_idx] + \n           h_b[print_idx], h_c[print_idx]);\n\n    // prints out the performance\n    float elapsed_time_msed = sdkGetTimerValue(&timer);\n    float bandwidth = 3 * bufsize * sizeof(float) / \n                      elapsed_time_msed / 1e6;\n    printf(\"Time= %.3f msec, bandwidth= %f GB/s\\n\", \n           elapsed_time_msed, bandwidth);\n```\n\n终止句柄和内存，如下所示:\n\n```cpp\n    sdkDeleteTimer(&timer);\n    delete [] ls_operator;\n    cudaFree(d_a);    cudaFree(d_b);    cudaFree(d_c);\n    cudaFreeHost(h_a);cudaFreeHost(h_b);cudaFreeHost(h_c);\n```\n\n为了执行代码，让我们重用前面菜谱中的主机初始化函数和 GPU 内核函数。我们现在不必修改这些函数。使用以下命令编译代码:\n\n```cpp\n$ nvcc -m64 -run -gencode arch=compute_70,code=sm_70 -I/usr/local/cuda/samples/common/inc -o cuda_pipelining ./cuda_pipelining.cu\n```\n\n您必须将您的图形处理器的计算能力版本号用于`gencode`选项。编译的输出如下:\n\n```cpp\nLaunched GPU task 0\nLaunched GPU task 1\nLaunched GPU task 2\nLaunched GPU task 3\ncompared a sample result...\nhost: 1.523750, device: 1.523750\nTime= 29.508 msec, bandwidth= 27.291121 GB/s\n```\n\n正如我们所看到的，GPU 任务是按照内核执行的顺序和流一起执行的。\n\n现在，让我们回顾一下应用内部是如何运行的。默认情况下，示例代码将主机数据分成四部分，并发执行四个 CUDA 流。我们可以看到每个内核的输出以及流的执行。要查看重叠操作，您需要使用以下命令分析执行情况:\n\n```cpp\n$ nvprof -o overlapping_exec.nvvp ./overlapping_exec\n```\n\n下面的截图显示了四个 CUDA 流的操作，通过重叠数据传输和内核执行:\n\n![](img/1dc75db9-3e38-4f1f-8309-e4942488014e.png)\n\nOverlaps between the kernel executions and data transfers\n\n因此，GPU 可以一直忙到最后一次内核执行完成，我们可以隐藏大部分的数据传输。这不仅提高了图形处理器的利用率，还减少了应用的总执行时间。\n\n在内核执行之间，我们可以发现虽然它们属于不同的 CUDA 流，但是没有一个没有争用。这是因为 GPU 调度器知道执行请求，并服务于第一个请求。然而，当当前任务完成时，流式多处理器可以服务于另一个 CUDA 流中的下一个内核，因为它们仍然被占用。\n\n在所有多个 CUDA 流操作结束时，我们需要同步主机和 GPU，以确认 GPU 上的所有 CUDA 操作都已完成。为此，我们在循环后立即使用`cudaDeviceSynchronize()`。该功能可以在调用点同步所有选中的 GPU 操作。\n\n对于同步任务，我们可以用下面的代码替换`cudaDeviceSynchronize()`函数。为此，我们还必须将私有成员`_stream`更改为公共成员:\n\n```cpp\nfor (int i = 0; i < num_operator; i++) {\n    cudaStreamSynchronize(ls_operator[i]._stream);\n}\n```\n\n当我们需要在每个流结束后，从单个主机线程和流一起提供特定的操作时，可以使用这种方法。但是，这不是一个好的操作设计，因为下面的操作无法避免与其他流同步。\n\n在循环中使用`cudaStreamSynchronize()`怎么样？在这种情况下，我们无法执行之前的重叠操作。下面的截图显示了这种情况:\n\n![](img/7c3c1e57-e8fb-4393-9809-7e27ea4e2afc.png)\n\n这是因为`cudaStreamSynchronize()`将同步每次迭代，应用将相应地序列化所有的 CUDA 执行。在这种情况下，执行时间被测量为 41.521 毫秒，比重叠的执行时间慢大约 40%。\n\n# CUDA 回调函数\n\n**CUDA 回调函数**是由 GPU 执行上下文执行的可调用的宿主函数。利用这一点，程序员可以在 GPU 操作之后指定主机期望的主机操作。\n\nCUDA 回调函数有一个名为`CUDART_CB`的特殊数据类型，所以应该用这个类型来定义。通过这种类型，程序员可以指定哪个 CUDA 流启动该功能，传递 GPU 错误状态，并提供用户数据。\n\n注册回调函数，CUDA 提供`cudaStreamAddCallback()`。该函数接受 CUDA 流、CUDA 回调函数及其参数，这样就可以从指定的 CUDA 流中调用指定的 CUDA 回调函数并获取用户数据。这个函数有四个输入参数，但最后一个是保留的。因此，我们不使用该参数，它保持为`0`。\n\n现在，让我们增强代码以使用回调函数并输出单个流的性能。如果你想把之前的工作和这个分开，请复制源代码。\n\n首先，将这些函数声明放入`Operator`类的`private`区域:\n\n```cpp\nStopWatchInterface *_p_timer;\nstatic void CUDART_CB Callback(cudaStream_t stream, cudaError_t status, void* userData);\nvoid print_time();\n```\n\n在每个流的操作完成后将调用`Callback()`函数，并且`print_time()`函数将使用主机端定时器`_p_timer`报告估计的性能。这些功能的实现如下:\n\n```cpp\nvoid Operator::CUDART_CB Callback(cudaStream_t stream, cudaError_t status, void* userData) {\n    Operator* this_ = (Operator*) userData;\n    this_->print_time();\n}\n\nvoid Operator::print_time() {\n    sdkStopTimer(&p_timer);    // end timer\n    float elapsed_time_msed = sdkGetTimerValue(&p_timer);\n    printf(\"stream %2d - elapsed %.3f ms \\n\", index, \n           elapsed_time_msed);\n}\n```\n\n为了有正确的定时器操作，我们需要在`Operator`类的构造器上有一个定时器初始化器，在类的终止器上有一个定时器破坏器。另外，我们必须在`Operator::async_operation()`功能开始时启动计时器。然后，在函数的末尾插入以下代码块。这允许 CUDA 流在完成之前的 CUDA 操作时调用主机端函数:\n\n```cpp\n// register callback function\ncudaStreamAddCallback(stream, Operator::Callback, this, 0);\n```\n\n现在，让我们编译并查看执行结果。您必须为`gencode`选项使用您的图形处理器的计算能力版本号:\n\n```cpp\n$ nvcc -m64 -run -gencode arch=compute_70,code=sm_70 -I/usr/local/cuda/samples/common/inc -o cuda_callback ./cuda_callback.cu\n```\n\n这是我们更新的执行结果:\n\n```cpp\nstream 0 - elapsed 11.136 ms\nstream 1 - elapsed 16.998 ms\nstream 2 - elapsed 23.283 ms\nstream 3 - elapsed 29.487 ms\ncompared a sample result...\nhost: 1.523750, device: 1.523750\nTime= 29.771 msec, bandwidth= 27.050028 GB/s\n```\n\n在这里，我们可以看到预计的执行时间以及 CUDA 流。回调函数估计其序列的执行时间。由于与其他流有重叠，并且后期 CUDA 流有延迟，所以我们可以看到后期 CUDA 流的执行时间延长了。我们可以通过与分析结果进行匹配来确认这些经过的时间，如下所示:\n\n![](img/9cf56a50-a62a-4ccc-839f-a86dbf6526c1.png)\n\n虽然它们测量的运行时间随着流的执行而延长，但是流之间的增量是有规律的，我们可以从分析的输出中看到这些操作。\n\n因此，我们可以得出结论，我们可以编写主机代码，在每个单独的 CUDA 流操作完成后立即运行。并且，这是一个高级的方法来同步来自主线程的每个流。\n\n# 具有优先级的 CUDA 流\n\n默认情况下，所有 CUDA 流具有相同的优先级，因此它们可以以正确的顺序执行操作。除此之外，CUDA 流也可以有优先级，并且可以被更高优先级的流取代。有了这个特性，我们就可以拥有满足时间要求的 GPU 操作。\n\n# 统一数据自动化系统的优先事项\n\n为了使用具有优先级的流，我们需要首先从 GPU 获得可用的优先级。我们可以使用`cudaDeviceGetStreamPriorityRange()`函数获得这些。它的输出是两个数值，分别是最低优先级值和最高优先级值。然后，我们可以使用`cudaStreamCreaetWithPriority()`功能创建优先级流，如下所示:\n\n```cpp\ncudaError_t cudaStreamCreateWithPriority(cudaStream_t* pStream, unsigned int flags, int priority)\n```\n\n我们应该提供另外两个参数。第一个用默认流确定创建的流的行为。我们可以使用`cudaStreamDefault`使新流与默认流同步，就像普通流一样。另一方面，我们可以使用`cudaStreamNonBlocking`使其与默认流并发运行。最后，我们可以在优先级范围内设置流的优先级。在 CUDA 编程中，最低的值具有最高的优先级。\n\n此外，我们可以使用以下代码来确认 GPU 是否支持这一点。但是，我们不必对此过于担心，因为自 CUDA 计算能力 3.5 以来，优先级流已经可用:\n\n```cpp\ncudaDeviceProp prop;\ncudaGetDeviceProperties(&prop, 0);\nif (prop.streamPrioritiesSupported == 0) { ... }\n```\n\n如果设备属性值为`0`，我们应该停止应用，因为 GPU 不支持流优先级。\n\n# 具有优先级的流执行\n\n现在，我们将使用回调重用前面的多流应用。在这段代码中，我们可以看到这些流可以按顺序运行，我们将看到如何根据优先级来改变这个顺序。我们将从`Operator`类中生成一个派生类，它将处理流的优先级。因此，我们将成员变量流的保护级别从私有成员更改为受保护成员。此外，构造函数可以选择性地创建流，因为这可以由派生类来完成。更改显示为以下代码:\n\n```cpp\n... { middle of the class Operator } ...\nprotected:\n    cudaStream_t stream = nullptr;\n\npublic:\n    Operator(bool create_stream = true) {\n        if (create_stream)\n            cudaStreamCreate(&stream);\n        sdkCreateTimer(&p_timer);\n    }\n... { middle of the class Operator } ...\n```\n\n派生类`Operator_with_priority`将具有一个函数，该函数以给定的优先级手动创建 CUDA 流。该类配置如下:\n\n```cpp\nclass Operator_with_priority: public Operator {\npublic:\n    Operator_with_priority() : Operator(false) {}\n\n    void set_priority(int priority) {\n        cudaStreamCreateWithPriority(&stream, \n            cudaStreamNonBlocking, priority);\n    }\n};\n```\n\n当我们用类处理每个流的操作时，我们将更新`ls_operator`创建代码以使用`main()`中的`Operator_with_priority`类，从而使用我们之前编写的类，如下所示:\n\n```cpp\nOperator_with_priority *ls_operator = new Operator_with_priority[num_operator];\n```\n\n当我们更新类时，这个类不会在我们请求它之前创建流。如前所述，我们需要使用以下代码获得 GPU 的可用优先级范围:\n\n```cpp\n// Get priority range\nint priority_low, priority_high;\ncudaDeviceGetStreamPriorityRange(&priority_low, &priority_high);\nprintf(\"Priority Range: low(%d), high(%d)\\n\", priority_low, priority_high);\n```\n\n然后，让我们创建每个操作来拥有不同的优先级流。为了缓解这个任务，我们将让最后一个操作拥有最高的流，并看看 CUDA 流中的抢占是如何工作的。这可以通过以下代码来完成:\n\n```cpp\nfor (int i = 0; i < num_operator; i++) {\n    ls_operator[i].set_index(i);\n\n    // let the latest CUDA stream to have the high priority\n    if (i + 1 == num_operator)\n        ls_operator[i].set_priority(priority_high);\n    else\n        ls_operator[i].set_priority(priority_low);\n}\n```\n\n之后，我们将执行每个操作，就像之前一样:\n\n```cpp\nfor (int i = 0 ; i < num_operator; i++) { \n    int offset = i * size / num_operator;\n    ls_operator[i].async_operation(&h_c[offset], \n                                   &h_a[offset], &h_b[offset],\n                                   &d_c[offset], \n                                   &d_a[offset], &d_b[offset],\n                                   size / num_operator, \n                                   bufsize / num_operator);\n}\n```\n\n为了获得正确的输出，让我们使用`cudaDeviceSynchronize()`功能同步主机和 GPU。最后，我们可以终止 CUDA 流。具有优先级的流可以用`cudaStreamDestroy()`功能终止，所以我们在这个应用中没有什么可做的，因为我们已经做了需要做的事情。\n\n现在，让我们编译代码并看看效果。一如既往，您需要向编译器提供正确的 GPU 计算能力版本:\n\n```cpp\n$ nvcc -m64 -run -gencode arch=compute_70,code=sm_70 -I/usr/local/cuda/samples/common/inc -o prioritized_cuda_stream ./prioritized_cuda_stream.cu\n```\n\n下面显示了应用的输出:\n\n```cpp\nPriority Range: low(0), high(-1)\nstream 0 - elapsed 11.119 ms\nstream 3 - elapsed 19.126 ms\nstream 1 - elapsed 23.327 ms\nstream 2 - elapsed 29.422 ms\ncompared a sample result...\nhost: 1.523750, device: 1.523750\nTime= 29.730 msec, bandwidth= 27.087332 GB/s\n```\n\n从输出中，您可以看到操作顺序已经更改。流 3 在流 1 和流 2 之前。下面的截图显示了它是如何改变的配置文件结果:\n\n![](img/84f4080c-7d72-4b4c-ad7c-b0192fe69d97.png)\n\n在这个截屏中，第二个 CUDA 流(在这个例子中是流 19)被按优先级排序的最后一个 CUDA 流(流 21)抢占，因此流 19 可以在流 21 完成执行后完成它的工作。请注意，数据传输的顺序不会根据此优先级而改变。\n\n# 使用 CUDA 事件估计内核执行时间\n\n以前的 GPU 操作时间估计有一个限制，即它不能测量内核执行时间。这是因为我们在主机端使用了定时 API。因此，我们需要与主机和 GPU 同步来测量内核执行时间，考虑到开销和对应用性能的影响，这是不切实际的。\n\n这可以使用 CUDA 事件来解决。CUDA 事件记录 GPU 端事件以及 CUDA 流。CUDA 事件可以是基于 GPU 状态的事件，并记录计划的时序。利用这一点，我们可以触发以下操作或估计内核执行时间。在本节中，我们将介绍如何使用 CUDA 事件来测量内核执行时间。\n\nCUDA 事件用`cudaEvent_t`句柄管理。我们可以使用`cudaEventCreate()`创建一个 CUDA 事件句柄，并用`cudaEventDestroy()`终止它。要记录事件时间，可以使用`cudaEventRecord()`。然后，CUDA 事件句柄为 GPU 记录事件时间。这个函数也接受 CUDA 流，这样我们就可以枚举事件时间到具体的 CUDA 流。获取内核执行的开始和结束事件后，可以使用`cudaEventElapsedTime()`获取经过的时间，单位为毫秒。\n\n现在，让我们介绍如何使用这些 API 来使用 CUDA 事件。\n\n# 使用 CUDA 事件\n\n在本节中，我们将重用第二节中的前一个多流应用。然后，我们使用 CUDA 事件枚举每个 GPU 内核的执行时间:\n\n1.  我们将使用一个简单的向量加法核函数，如下所示:\n\n```cpp\n__global__ void\nvecAdd_kernel(float *c, const float* a, const float* b) {\n    int idx = blockIdx.x * blockDim.x + threadIdx.x;\n    for (int i = 0; i < 500; i++)\n        c[idx] = a[idx] + b[idx];\n}\n```\n\n这段代码有一个延长内核执行时间的迭代。\n\n2.  然后，我们将使用下面的代码片段来测量内核执行时间。为了比较结果，我们将使用主机端的计时器和 CUDA 事件:\n\n```cpp\n... { memory initializations } ...\n\n// initialize the host timer\nStopWatchInterface *timer;\nsdkCreateTimer(&timer);\n\ncudaEvent_t start, stop;\n// create CUDA events\ncudaEventCreate(&start);\ncudaEventCreate(&stop);\n\n// start to measure the execution time\nsdkStartTimer(&timer);\ncudaEventRecord(start);\n\n// launch cuda kernel\ndim3 dimBlock(256);\ndim3 dimGrid(size / dimBlock.x);\nvecAdd_kernel<<< dimGrid, dimBlock >>>(d_c, d_a, d_b);\n\n// record the event right after the kernel execution finished\ncudaEventRecord(stop);\n\n// Synchronize the device to measure the execution time from the host side\ncudaEventSynchronize(stop); // we also can make synchronization based on CUDA event\nsdkStopTimer(&timer);\n```\n\n正如您在这段代码中看到的，我们可以在内核调用后立即记录 CUDA 事件。然而，定时器需要 GPU 和主机之间的同步。对于同步，我们使用`cudaEventSynchronize(stop)`函数，因为我们还可以使宿主线程与事件同步。同时，这段代码只涉及处理定时资源和内核执行。但是，您还必须初始化所需的内存才能使其工作。\n\n3.  内核执行后，让我们编写代码，报告每个定时资源的执行时间:\n\n```cpp\n// print out the result\nint print_idx = 256;\nprintf(\"compared a sample result...\\n\");\nprintf(\"host: %.6f, device: %.6f\\n\", h_a[print_idx] + h_b[print_idx], h_c[print_idx]);\n\n// print estimated kernel execution time\nfloat elapsed_time_msed = 0.f;\ncudaEventElapsedTime(&elapsed_time_msed, start, stop);\nprintf(\"CUDA event estimated - elapsed %.3f ms \\n\", elapsed_time_msed);\n```\n\n4.  现在，我们将通过终止计时资源来完成我们的应用，使用以下代码:\n\n```cpp\n// delete timer\nsdkDeleteTimer(&timer);\n\n// terminate CUDA events\ncudaEventDestroy(start);\ncudaEventDestroy(stop);\n```\n\n5.  让我们使用以下命令编译并查看输出:\n\n```cpp\n$ nvcc -m64 -run -gencode arch=compute_70,code=sm_70 -I/usr/local/cuda/samples/common/inc -o cuda_event ./cuda_event.cu\ncompared a sample result...\nhost: 1.523750, device: 1.523750\nCUDA event estimated - elapsed 23.408 ms \nHost measured time= 35.063 msec/s\n```\n\n如您所见，我们可以使用 CUDA 事件来测量内核执行时间。然而，测量的时间在 CUDA 事件和计时器之间有间隙。我们可以使用 NVIDIA Profiler 来验证哪个提供了更准确的信息。当我们使用`# nvprof ./cuda_event`命令时，输出如下:\n\n![](img/533f3b7e-846e-4f23-a97d-b277876a05c4.png)\n\n如您所见，与从主机进行测量相比，CUDA 事件提供了准确的结果。\n\n使用 CUDA 事件的另一个好处是，我们可以用多个 CUDA 流同时测量多个内核执行时间。让我们实现一个示例应用，看看它的操作。\n\n# 多流估计\n\n`cudaEventRecord()`功能与主机异步。换句话说，没有同步来测量示例代码的内核执行时间。为了与事件和主机同步，我们需要使用`cudaEventSynchronize()`。例如，内核函数打印可以通过同步效应放在从设备到主机的异步数据传输之前，此时我们将该函数放在`cudaEventRecord(stop)`之后。\n\n在多个 CUDA 流应用中测量内核执行时间也很有用:\n\n1.  让我们将此应用于`04_stream_priority`示例代码中多个 CUDA 流重叠的配方代码。用以下代码更新代码:\n\n```cpp\nclass Operator\n{\nprivate:\n    int _index;\n    cudaStream_t stream;\n    StopWatchInterface *p_timer;\n    cudaEvent_t start, stop;\n\npublic:\n    Operator() {\n        cudaStreamCreate(&stream);\n\n // create cuda event\n cudaEventCreate(&start);\n cudaEventCreate(&stop);\n    }\n\n    ~Operator() {\n        cudaStreamDestroy(stream);\n\n // destroy cuda event\n cudaEventDestroy(start);\n cudaEventDestroy(stop);\n    }\n\n    void set_index(int idx) { index = idx; }\n    void async_operation(float *h_c, const float *h_a, \n                          const float *h_b,\n                          float *d_c, float *d_a, float *d_b,\n                          const int size, const int bufsize);\n void print_kernel_time();\n\n}; // Operator\n```\n\n2.  然后，我们将定义此时包含的`print_time()`函数，如下所示:\n\n```cpp\nvoid Operator::print_time() {\n    float milliseconds = 0;\n    cudaEventElapsedTime(&milliseconds, start, stop);\n    printf(\"Stream %d time: %.4f ms\\n\", index, milliseconds);\n}\n```\n\n3.  现在，在`Operator::async_operation()`的开头和结尾插入`cudaEventRecord()`函数调用，如下面的代码:\n\n```cpp\nvoid Operator::async_operation( ... )\n{\n    // start timer\n    sdkStartTimer(&p_timer);\n\n    // copy host -> device\n    cudaMemcpyAsync(d_a, h_a, bufsize, \n                    cudaMemcpyHostToDevice, stream);\n    cudaMemcpyAsync(d_b, h_b, bufsize, \n                    cudaMemcpyHostToDevice, stream);\n\n    // record the event before the kernel execution\n cudaEventRecord(start, stream);\n\n    // launch cuda kernel\n    dim3 dimBlock(256);\n    dim3 dimGrid(size / dimBlock.x);\n    vecAdd_kernel<<< dimGrid, dimBlock, 0, \n                     stream >>>(d_c, d_a, d_b);\n\n    // record the event right after the kernel execution finished\n cudaEventRecord(stop, stream);\n\n    // copy device -> host\n    cudaMemcpyAsync(h_c, d_c, bufsize, \n                    cudaMemcpyDeviceToHost, stream);\n\n    // what happen if we include CUDA event synchronize?\n    // QUIZ: cudaEventSynchronize(stop);\n\n    // register callback function\n    cudaStreamAddCallback(stream, Operator::Callback, this, 0);\n}\n```\n\n对于这个函数，在函数的末尾放置同步是一个挑战。完成本节后，请尝试此操作。这将影响应用的行为。建议尝试向自己解释输出，然后使用 profiler 确认。\n\n现在，让我们编译并查看执行时间报告，如下所示；它显示了与以前的执行类似的性能:\n\n```cpp\n$ nvcc -m64 -run -gencode arch=compute_70,code=sm_70 -I/usr/local/cuda/samples/common/inc -o cuda_event_with_streams ./cuda_event_with_streams.cu\nPriority Range: low(0), high(-1)\nstream 0 - elapsed 11.348 ms \nstream 3 - elapsed 19.435 ms \nstream 1 - elapsed 22.707 ms \nstream 2 - elapsed 35.768 ms \nkernel in stream 0 - elapsed 6.052 ms \nkernel in stream 1 - elapsed 14.820 ms \nkernel in stream 2 - elapsed 17.461 ms \nkernel in stream 3 - elapsed 6.190 ms \ncompared a sample result...\nhost: 1.523750, device: 1.523750\nTime= 35.993 msec, bandwidth= 22.373972 GB/s\n```\n\n在这个输出中，由于 CUDA 事件，我们还可以看到每个内核的执行时间。从这个结果中，我们可以看到内核执行时间被延长了，正如我们在上一节中看到的。\n\n如果想了解更多关于 CUDA 事件的特性，可以查看 NVIDIA 的 CUDA 事件文档:[https://docs . NVIDIA . com/CUDA/CUDA-runtime-API/group _ _ CUDART _ _ event . html](https://docs.nvidia.com/cuda/cuda-runtime-api/group__CUDART__EVENT.html)。\n\n现在，我们将介绍管理 CUDA 网格的其他一些方面。第一项是动态并行，支持从 GPU 内核函数调用内核。\n\n# CUDA 动态并行\n\n**CUDA 动态并行** ( **CDP** )是一个设备运行时特性，支持来自设备函数的嵌套调用。这些嵌套调用允许子网格有不同的并行性。当您根据问题需要不同的块大小时，此功能非常有用。\n\n# 理解动态并行\n\n像来自主机的正常内核调用一样，GPU 内核调用也可以进行内核调用。以下示例代码显示了它的工作原理:\n\n```cpp\n__global__ void child_kernel(int *data) {\n    int idx = blockIdx.x * blockDim.x + threadIdx.x;\n    atomicAdd(&data[idx], seed);\n}\n\n__global__ void parent_kernel(int *data)\n{\n if (threadIdx.x == 0) {\n        int child_size = BUF_SIZE/gridDim.x;\n        child_kernel<<< child_size/BLOCKDIM, BLOCKDIM >>>\n                        (&data[child_size*blockIdx.x], blockIdx.x+1);\n    }\n    // synchronization for other parent's kernel output\n    cudaDeviceSynchronize();\n}\n```\n\n正如您在这些函数中看到的，我们需要确保哪个 CUDA 线程进行内核调用来控制网格创建的数量。为了进一步了解这一点，让我们使用它来实现第一个应用。\n\n# 动态并行的使用\n\n我们的动态并行代码将创建一个父网格，该父网格将创建几个子网格:\n\n1.  首先，我们将使用以下代码编写`parent_kernel()`函数和`child_kernel()`函数:\n\n```cpp\n#define BUF_SIZE (1 << 10)\n#define BLOCKDIM 256\n\n__global__ void child_kernel(int *data)\n{\n    int idx = blockIdx.x * blockDim.x + threadIdx.x;\n    atomicAdd(&data[idx], 1);\n}\n\n__global__ void parent_kernel(int *data)\n{\n    if (blockIdx.x * blockDim.x + threadIdx.x == 0)\n    {\n        int child_size = BUF_SIZE/gridDim.x;\n        child_kernel<<< child_size/BLOCKDIM, BLOCKDIM >>> \\\n                        (&data[child_size*blockIdx.x], \n                         blockIdx.x+1);\n    }\n    // synchronization for other parent's kernel output\n    cudaDeviceSynchronize();\n}\n```\n\n正如您在这段代码中看到的，父内核函数创建子内核网格作为块数。并且，子网格将指定的内存增加`1`来标记它们的操作。内核执行后，父内核等待，直到所有子网格使用`cudaDeviceSynchronize()`函数完成它们的工作。当我们进行同步时，我们应该确定同步的范围。如果需要在块级同步，应该选择`__synchthread()`代替。\n\n2.  使用以下代码编写`main()`函数:\n\n```cpp\n#define BUF_SIZE (1 << 10)\n#define BLOCKDIM 256\nint main()\n{\n    int *data;\n    int num_child = 4;\n\n    cudaMallocManaged((void**)&data, BUF_SIZE * sizeof(int));\n    cudaMemset(data, 0, BUF_SIZE * sizeof(int));\n\n    parent_kernel<<<num_child, 1>>>(data);\n    cudaDeviceSynchronize();\n\n    // Count elements value\n    int counter = 0;\n    for (int i = 0; i < BUF_SIZE; i++)\n        counter += data[i];\n\n    // getting answer\n    int counter_h = 0;\n    for (int i = 0; i < num_child; i++)\n        counter_h += (i+1);\n    counter_h *= BUF_SIZE / num_child;\n\n    if (counter_h == counter)\n        printf(\"Correct!!\\n\");\n    else\n        printf(\"Error!! Obtained %d. It should be %d\\n\", \n               counter, counter_h);\n\n    cudaFree(data);\n    return 0;\n}\n```\n\n如前所述，我们将创建子网格以及块数。因此，我们将执行网格大小为`4`的父内核函数，而块大小为`1`。\n\n3.  要编译一个 CDP 应用，我们应该向`nvcc`编译器提供`-rdc=true`选项。因此，编译源代码的命令如下:\n\n```cpp\n$ nvcc -run -rdc=true -lcudadevrt -gencode arch=compute_70,code=sm_70 -o host_callback host_callback.cu -I/usr/local/cuda/samples/common/inc \n```\n\n4.  让我们分析一下这个应用，了解它的操作。下面的截图显示了这个嵌套调用是如何工作的:\n\n![](img/765aef65-f755-4eb1-9697-ff07d3efb6d7.png)\n\n正如我们在这个截图中看到的，父内核创建了一个子网格，我们可以看到它们与左侧面板的直角标记的关系。然后，父网格(parent_kernel)等待它的执行，直到子网格完成它的工作。CUDA 目前不支持 SM70 (Volta 架构)的 CDT 评测，所以我已经用特斯拉 P40 获得了这个输出。\n\n# 递归\n\n动态并行的好处之一是我们可以创建一个递归。下面的代码显示了一个递归内核函数的示例:\n\n```cpp\n__global__ void recursive_kernel(int *data, int size, int depth) {\n  int x_0 = blockIdx.x * size;\n\n  if (depth > 0) {\n    __syncthreads();\n if (threadIdx.x == 0) {\n        int dimGrid = size / dimBlock;\n        recursive_kernel<<<dimGrid, \n              dimBlock>>>(&data[x_0], size/dimGrid, depth-1);\n        cudaDeviceSynchronize();\n      }\n      __syncthreads();\n   }\n}\n```\n\n可以看到，与之前的动态并行内核函数没有太大区别。但是，考虑到资源使用和限制，我们应该谨慎使用。一般来说，动态并行内核可以保守地保留多达 150 MB 的设备内存，以通过在子网格启动时同步来跟踪挂起的网格启动和父网格状态。此外，同步必须跨多个级别小心进行，而嵌套内核启动的深度被限制在 24 个级别。最后，控制嵌套内核启动的运行时会影响整体性能。\n\n如果需要了解动态并行的限制和局限，请参阅以下编程指南:[https://docs . NVIDIA . com/cuda/cuda-c-programming-guide/index . html #实现-限制和局限](https://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html#implementation-restrictions-and-limitations)。\n\n我们将在[第 7 章](07.html)、*CUDA*中介绍其快速排序实现的应用。要了解有关动态并行的更多信息，请参见以下文档:\n\n*   [https://dev blogs . NVIDIA . com/cuda-dynamic-parallelism-API-principles/](https://devblogs.nvidia.com/cuda-dynamic-parallelism-api-principles/)\n*   [http://on-demand . gputechconf . com/GTC/2012/presentations/s 0338-新功能-CUDA-编程-模型. pdf](http://on-demand.gputechconf.com/gtc/2012/presentations/S0338-New-Features-in-the-CUDA-Programming-Model.pdf)\n\n# 网格级协作组\n\n如[第三章](03.html)、 *CUDA 线程编程、* CUDA 提供协作组。协作组可以按照它们的分组目标进行分类:扭曲级、块级和网格级组。这个食谱涵盖了网格级别的协作组，并研究了协作组如何处理 CUDA 网格。\n\n协作组最突出的好处是目标并行对象的显式同步。使用协作组，程序员可以设计他们的应用来显式同步 CUDA 并行对象、线程块或网格。使用[第 3 章](03.html)、 *CUDA 线程编程*中所涵盖的块级协作组，我们可以通过指定需要同步哪些 CUDA 线程或块来编写可读性更强的代码。\n\n# 理解网格级别的协作组\n\n从 9.0 版本开始，CUDA 提供了另一个层次的协作组，与网格一起工作。具体来说，有两个网格级的协作组:`grid_group`和`multi_grid_group`。使用这些组，程序员可以描述网格在单个或多个图形处理器上同步的操作。\n\n在这个食谱中，我们将探索`grid_group`的功能，它可以将网格与约简问题同步，正如[第 3 章](03.html)、 *CUDA 线程编程*中提到的，关于之前基于块级约简的约简设计。每个线程块产生自己的缩减结果，并将它们存储到全局内存中。然后，另一个分块缩减内核启动，直到我们获得一个缩减值。那是因为完成内核操作可以保证下一个**缩减**内核从多个线程块中读取一个缩减值。它的设计由左边的图表描述:\n\n![](img/e70bce0c-8537-4ad1-a5c6-82cc22c93cb0.png)\n\n另一方面，网格级同步支持另一种内核设计，它在内部同步分块**约简**结果，这样主机只能有一次内核调用来获得约简**结果**。在协作组中，`grid_group.sync()`提供了这样的功能，所以我们不用内核级迭代就可以编写约简内核。\n\n要使用`grid_group.sync()`函数，我们需要使用`cudaLaunchCooperativeKernel()`函数调用内核函数。其界面设计如下:\n\n```cpp\n__host__ cudaError_t cudaLaunchCooperativeKernel\n    ( const T* func, dim3 gridDim, dim3 blockDim, \n      void** args, size_t sharedMem = 0, cudaStream_t stream = 0 )\n```\n\n所以，它的用法和`cudaLaunchKernel()`函数一样，启动一个内核函数。\n\n要使`grid_group`中的所有线程块同步，网格中活动线程块的总数不应超过内核函数和设备的最大活动块数。GPU 上的最大活动块大小是每个 SM 的最大活动块数量和流式多处理器数量的乘积。违反此规则会导致死锁或未定义的行为。通过传递内核函数和块大小信息，我们可以使用`cudaOccupancyMaxActiveBlocksPerMultiprocessor()`函数获得每个内核函数的最大活动线程块数量。\n\n# 网格组的使用\n\n现在，让我们将`grid_group`应用于并行约简问题，看看 GPU 编程如何改变:\n\n1.  我们将在`03_cuda_thread_programming/07_cooperative_groups`中重用之前并行约简代码中的宿主代码。换句话说，我们将通过主机代码的微小变化来改变 GPU 的操作。您也可以使用`07_grid_level_cg`目录中的代码。\n2.  现在，让我们编写一些块级简化代码。当我们有网格级别的协作组时，所有的线程块都必须是活动的。换句话说，除了支持图形处理器的活动块，我们不能执行多个线程块。因此，这种减少将首先累积输入数据，以覆盖具有有限数量线程块的所有数据。然后，它将在块级别进行并行约简，如我们在[第 3 章](03.html)、 *CUDA 线程编程*中所述。\n\n下面的代码显示了它的实现:\n\n```cpp\n__device__ void\nblock_reduction(float *out, float *in, float *s_data, int active_size, int size, \n          const cg::grid_group &grid, const cg::thread_block &block)\n{\n  int tid = block.thread_rank();\n\n  // Stride over grid and add the values to a shared memory buffer\n  s_data[tid] = 0.f;\n  for (int i = grid.thread_rank(); i < size; i += active_size)\n    s_data[tid] += in[i];\n\n  block.sync();\n\n  for (unsigned int stride = blockDim.x / 2; \n       stride > 0; stride >>= 1) {\n    if (tid < stride)\n      s_data[tid] += s_data[tid + stride];\n    block.sync();\n  }\n\n  if (block.thread_rank() == 0)\n    out[block.group_index().x] = s_data[0];\n}\n```\n\n3.  然后，让我们编写一个内核函数，考虑活动块的数量和`grid_group`，执行分块缩减。在这个函数中，我们将调用块级简化代码，并在网格级同步它们。然后，我们将对输出执行并行缩减，如我们在[第 3 章](03.html)、 *CUDA 线程编程*中所述。下面的代码显示了它的实现:\n\n```cpp\n__global__ void\nreduction_kernel(float *g_out, float *g_in, unsigned int size)\n{\n  cg::thread_block block = cg::this_thread_block();\n  cg::grid_group grid = cg::this_grid();\n  extern __shared__ float s_data[];\n\n  // do reduction for multiple blocks\n  block_reduction(g_out, g_in, s_data, grid.size(), \n                  size, grid, block);\n\n  grid.sync();\n\n  // do reduction with single block\n  if (block.group_index().x == 0)\n    block_reduction(g_out, g_out, s_data, block.size(), gridDim.x, grid, block);\n}\n```\n\n4.  最后，我们将使用可用的活动线程块维度来实现调用内核函数的宿主代码。为此，该功能使用`cudaoccupancyMaxActiveBlocksPerMultiprocessor()`功能。此外，网格级协作组要求我们通过`cudaLaunchCooperativeKernel()`函数调用内核函数。您可以在这里看到实现:\n\n```cpp\nint reduction_grid_sync(float *g_outPtr, float *g_inPtr, int size, int n_threads)\n{ \n  int num_blocks_per_sm;\n  cudaDeviceProp deviceProp;\n\n  // Calculate the device occupancy to know \n  // how many blocks can be run concurrently\n  cudaGetDeviceProperties(&deviceProp, 0);\n  cudaOccupancyMaxActiveBlocksPerMultiprocessor(&num_blocks_per_sm, \n      reduction_kernel, n_threads, n_threads*sizeof(float));\n  int num_sms = deviceProp.multiProcessorCount;\n  int n_blocks = min(num_blocks_per_sm * num_sms, \n                     (size + n_threads - 1) / n_threads);\n\n  void *params[3];\n  params[0] = (void*)&g_outPtr;\n  params[1] = (void*)&g_inPtr;\n  params[2] = (void*)&size;\n  cudaLaunchCooperativeKernel((void*)reduction_kernel, \n                              n_blocks, n_threads, params, \n                              n_threads * sizeof(float), NULL);\n\n  return n_blocks;\n}\n```\n\n5.  现在，确保可以从`reduction.cpp`文件调用宿主函数。\n\n6.  然后，让我们编译代码，看看它的操作。下面的 shell 命令编译代码并执行应用。计算能力应等于或大于`70`:\n\n```cpp\n$ nvcc -run -m64 -gencode arch=compute_70,code=sm_70 -I/usr/local/cuda/samples/common/inc -rdc=true -o reduction ./reduction.cpp ./reduction_kernel.cu\nTime= 0.474 msec, bandwidth= 141.541077 GB/s\nhost: 0.996007, device 0.996007\n```\n\n输出性能远远落后于我们在[第三章](03.html)、 *CUDA 线程编程*的最终结果。由于`block_reduction()`函数在开始时使用高内存吞吐量，因此它是高度内存受限的:\n\n![](img/fbd18a08-d2a1-4bc1-a9b3-647c67a72240.png)\n\n主要影响因素是我们只能使用活动线程块。所以，我们无法隐藏内存访问时间。其实`grid_group`的使用还有其他的目的，比如图搜索、遗传算法、粒子模拟等，为了性能需要我们长时间保持状态活跃。\n\n这种网格级同步可以为性能和可编程性带来更多好处。因为这使得内核能够自己同步，我们可以让内核自己迭代。因此，它有助于解决图形搜索，遗传算法和实际模拟。要了解更多关于`grid_groups`中合作小组的信息，请参考[中提供的文档。](http://on-demand.gputechconf.com/gtc/2017/presentation/s7622-Kyrylo-perelygin-robust-and-scalable-cuda.pdf)\n\n# 用 OpenMP 调用 CUDA 内核\n\n为了扩大应用的并发性，我们可以从主机的并行任务中进行内核调用。例如，OpenMP 提供了多核架构的简单并行性。这个食谱涵盖了 CUDA 如何操作 OpenMP。\n\n# OpenMP 和 CUDA 调用\n\nOpenMP 使用并行的分叉连接模型来定位多核 CPU。主线程启动并行操作并创建工作线程。宿主线程并行操作自己的作业，并在完成工作后加入。\n\n使用 OpenMP，CUDA 内核调用可以与多个线程并行执行。这有助于程序员不必维护单独的内核调用，而是允许它们根据主机线程的索引执行内核。\n\n在本节中，我们将使用以下 OpenMP APIs:\n\n*   `omp_set_num_threads()`设置并行工作的工作线程数。\n*   `omp_get_thread_num()`返回一个工作线程的索引，这样每个线程都可以识别自己的任务。\n*   `#pragma omp parallel {}`指定将被工作线程覆盖的并行区域。\n\n现在，让我们编写一些 OpenMP 调用 CUDA 内核函数的代码。\n\n# 用 OpenMP 调用 CUDA 内核\n\n在本节中，我们将实现一个使用 OpenMP 的多流矢量添加应用。为此，我们将修改以前的版本，并查看不同之处:\n\n1.  为了用 CUDA 测试 OpenMP，我们将从`03_cuda_callback`目录修改代码。我们将修改`main()`函数的主体，或者您可以使用放置在`08_openmp_cuda`目录中的提供的示例代码。\n\n2.  现在，让我们包含 OpenMP 头文件并修改代码。要在代码中使用 OpenMP，我们应该使用`#include <omp.h>`。并且，我们将更新迭代`for`每个流的代码以使用 OpenMP:\n\n```cpp\n// execute each operator collesponding data\nomp_set_num_threads(num_operator);\n#pragma omp parallel\n{\n    int i = omp_get_thread_num();\n    printf(\"Launched GPU task %d\\n\", i);\n\n    int offset = i * size / num_operator;\n    ls_operator[i].set_index(i);\n    ls_operator[i].async_operation(&h_c[offset], &h_a[offset],   \n                                   &h_b[offset],&d_c[offset], \n                                   &d_a[offset], &d_b[offset],\n                                   size / num_operator, bufsize \n                                   / num_operator);\n}\n```\n\n3.  使用以下命令编译代码:\n\n```cpp\n$ nvcc -run -m64 -gencode arch=compute_70,code=sm_70 -I/usr/local/cuda/samples/common/inc -Xcompiler -fopenmp -lgomp -o openmp ./openmp.cu\nstream 0 - elapsed 10.734 ms \nstream 2 - elapsed 16.153 ms \nstream 3 - elapsed 21.968 ms \nstream 1 - elapsed 27.668 ms \ncompared a sample result...\nhost: 1.523750, device: 1.523750\nTime= 27.836 msec, bandwidth= 28.930389 GB/s\n```\n\n每当您执行这个应用时，您将看到每个流都无序地完成它们的工作。此外，每个流显示不同的时间。那是因为 OpenMP 可以创建多个线程，操作是在运行时决定的。\n\n为了理解它的操作，让我们分析一下这个应用。下面的截图显示了应用的概要时间线。由于时间安排的原因，这可能与您的不同:\n\n![](img/50012166-e450-4d38-8ebf-a44a55ad9844.png)\n\n正如您在这张截图中看到的，您将能够看到与流 17 相比，数据传输发生了逆转。由于这个原因，我们可以看到第二个流终于可以完成它的工作了。\n\n# 多进程服务\n\n图形处理器能够从并发的中央处理器进程中执行内核。然而，默认情况下，它们只以时间分片的方式执行，即使每个内核没有充分利用 GPU 计算资源。为了解决这种不必要的序列化，GPU 提供了**多进程服务** ( **MPS** )模式。这使得不同的进程能够在一个图形处理器上同时执行它们的内核，以充分利用图形处理器资源。启用后，`nvidia-cuda-mps-control`守护程序监控目标图形处理器，并使用该图形处理器管理进程内核操作。此功能仅在 Linux 上可用。在这里，我们可以看到多进程共享同一个 GPU 的 MPS:\n\n![](img/2f97d170-6740-41d4-aa8b-7552745e9838.jpg)\n\n如我们所见，每个进程都有一部分在 GPU 中并行运行(绿色条)，而一些部分在 CPU 上运行(蓝色条)。理想情况下，您需要蓝色条和绿色条来获得最佳性能。这可以通过使用 MPS 功能来实现，所有最新的图形处理器都支持该功能。\n\nPlease note that multiple MPI processes running on the same GPU are beneficial when one MPI process is unable to saturate the whole GPU and a significant part of the code is also running on the CPU. If one MPI process utilizes the whole GPU, even though the CPU part (blue bar) will reduce, the green bar time will not as the GPU is completely utilized by one MPI process. The other MPI processes will access the GPU one after another in a time-sliced manner based on the GPU architecture. This is similar to the launching-concurrent-kernels scenario. If one kernel utilizes the whole GPU, then the other kernel will either wait for the first kernel to finish or be time-sliced. \n\n这样做的好处是，使用 MPS 不需要对应用进行任何更改。MPS 进程作为守护进程运行，如以下命令所示:\n\n```cpp\n$nvidia-smi -c EXCLUSIVE_PROCESS \n$nvidia-cuda-mps-control –d\n```\n\n运行此命令后，所有进程都将其命令提交给 MPS 守护程序，该守护程序负责将 CUDA 命令提交给 GPU。对于图形处理器，只有一个进程访问图形处理器，因此多个内核可以从多个进程并发运行。这有助于将一个进程的内存副本与其他 MPI 进程的内核执行重叠。\n\n# 消息传递接口介绍\n\n**消息传递接口** ( **MPI** )是一个并行计算接口，能够跨计算单元(中央处理器内核、图形处理器和节点)触发多个进程。典型的密集多 GPU 系统包含 4-16 个 GPU，而 CPU 内核的数量在 20-40 个 CPU 之间。在支持 MPI 的代码中，应用的某些部分作为不同的 MPI 进程在多个内核上并行运行。每个 MPI 进程都会调用 CUDA。理解将 MPI 进程映射到相应的 GPU 是非常重要的。最简单的映射是 1:1，也就是说，每个 MPI 进程都可以独占访问各自的 GPU。此外，我们可以理想地将多个 MPI 进程映射到单个 GPU。\n\n为了将多进程应用场景集成到单个 GPU 中，我们将使用 MPI。要使用 MPI，您需要为您的系统安装 OpenMPI。按照以下步骤为 Linux 安装 OpenMPI。此操作已在 Ubuntu 18.04 上测试过，因此如果您使用另一个发行版，这可能会有所不同:\n\n```cpp\n$ wget -O /tmp/openmpi-3.0.4.tar.gz https://www.open-mpi.org/software/ompi/v3.0/downloads/openmpi-3.0.4.tar.gz\n$ tar xzf /tmp/openmpi-3.0.4.tar.gz -C /tmp\n$ cd /tmp/openmpi-3.0.4\n$ ./configure --enable-orterun-prefix-by-default --with-cuda=/usr/local/cuda\n$ make -j $(nproc) all && sudo make install\n$ sudo ldconfig\n$ mpirun --version\nmpirun (Open MPI) 3.0.4\n\nReport bugs to http://www.open-mpi.org/community/help/\n```\n\n现在，让我们实现一个可以使用 MPI 和 CUDA 的应用。\n\n# 实现支持 MPI 的应用\n\n为了让一个应用能够使用 MPI，我们需要在应用中放入一些能够理解 MPI 命令的代码:\n\n1.  我们将重用 OpenMP 示例代码，所以复制`08_openmp_cuda`目录中的`openmp.cu`文件。\n2.  在代码开头插入`mpi`表头`include`语句:\n\n```cpp\n#include <mpi.h>\n```\n\n3.  在`main()`功能中创建秒表后，立即插入以下代码:\n\n```cpp\n// set num_operator as the number of requested process\nint np, rank;\nMPI_Init(&argc, &argv);\nMPI_Comm_size(MPI_COMM_WORLD, &np);\nMPI_Comm_rank(MPI_COMM_WORLD, &rank);\n```\n\n4.  在步骤 3 中提到的代码之后，将所需的内存大小除以进程数，如下所示:\n\n```cpp\nbufsize /= np;\nsize /= np;\n```\n\n5.  我们需要让每个线程报告它们所属的进程。让我们更新并行执行代码块中的`printf()`函数，如下所示:\n\n```cpp\n// execute each operator collesponding data\nomp_set_num_threads(num_operator);\n#pragma omp parallel\n{\n    int i = omp_get_thread_num();\n    int offset = i * size / num_operator;\n    printf(\"Launched GPU task (%d, %d)\\n\", rank, i);\n\n    ls_operator[i].set_index(i);\n    ls_operator[i].async_operation(&h_c[offset], \n                                   &h_a[offset], &h_b[offset],\n                                   &d_c[offset], &d_a[offset], \n                                   &d_b[offset],\n                                   size / num_operator, \n                                   bufsize / num_operator);\n}\n```\n\n6.  在`main()`结束时，放置`MPI_Finalize()`功能关闭 MPI 实例。\n7.  使用以下命令编译代码:\n\n```cpp\n$ nvcc -m64 -gencode arch=compute_70,code=sm_70 -I/usr/local/cuda/samples/common/inc -I/usr/local/include/ -Xcompiler -fopenmp -lgomp -lmpi -o simpleMPI ./simpleMPI.cu\n```\n\n您必须将您的图形处理器的计算能力版本号用于`gencode`选项。\n\n8.  使用以下命令测试编译的应用:\n\n```cpp\n$ ./simpleMPI 2\n```\n\n9.  现在，使用以下命令测试 MPI 的执行情况:\n\n```cpp\n$ mpirun -np 2 ./simpleMPI 2\nNumber of process: 2\nNumber of operations: 2\nLaunched GPU task (1, 0)\nLaunched GPU task (1, 1)\nNumber of operations: 2\nLaunched GPU task (0, 0)\nLaunched GPU task (0, 1)\nstream 0 - elapsed 13.390 ms \nstream 1 - elapsed 25.532 ms \ncompared a sample result...\nhost: 1.306925, device: 1.306925\nTime= 25.749 msec, bandwidth= 15.637624 GB/s\nstream 0 - elapsed 21.334 ms \nstream 1 - elapsed 26.010 ms \ncompared a sample result...\nhost: 1.306925, device: 1.306925\nTime= 26.111 msec, bandwidth= 15.420826 GB/s\n```\n\n# 启用主生产计划\n\n在 GPU 中启用 MPS 需要对 GPU 操作模式进行一些修改。但是，你需要一个比开普勒架构更晚的 GPU 架构。\n\n让我们按照如下所示的步骤启用主生产计划:\n\n1.  使用以下命令启用主生产计划模式:\n\n```cpp\n$ export CUDA_VISIBLE_DEVICES=0\n$ sudo nvidia-smi -i 0 -c 3\n$ sudo nvidia-cuda-mps-control -d\n```\n\n或者，您可以对该配方样本代码使用`make enable_mps`命令，该代码在`Makefile`中预定义。然后，我们可以从`nivida-smi`输出中看到更新的计算模式:\n\n![](img/329cace9-b5f9-43e7-8716-c2aa40c0547f.png)\n\n2.  现在，使用以下命令测试 MPI 在 MPS 模式下的执行情况:\n\n```cpp\n$ mpirun -np 2 ./simpleMPI 2\nNumber of process: 2\nNumber of operations: 2\nLaunched GPU task (1, 0)\nLaunched GPU task (1, 1)\nstream 0 - elapsed 10.203 ms \nstream 1 - elapsed 15.903 ms \ncompared a sample result...\nhost: 1.306925, device: 1.306925\nTime= 16.129 msec, bandwidth= 24.964548 GB/s\nNumber of operations: 2\nLaunched GPU task (0, 0)\nLaunched GPU task (0, 1)\nstream 0 - elapsed 10.203 ms \nstream 1 - elapsed 15.877 ms \ncompared a sample result...\nhost: 1.306925, device: 1.306925\nTime= 15.997 msec, bandwidth= 25.170544 GB/s\n```\n\n如您所见，与之前的执行相比，每个进程的运行时间都有所减少。\n\n3.  现在，让我们恢复原始模式。要禁用主生产计划模式，请使用以下命令:\n\n```cpp\n$ echo \"quit\" | sudo nvidia-cuda-mps-control\n$ sudo nvidia-smi -i 0 -c 0\n```\n\n或者，您可以对该配方样本代码使用`make disable_mps`命令，该代码在`Makefile`中预定义。\n\n要了解更多关于 MPS 的信息，请使用以下链接:\n\n*   [http://on-demand . gputechconf . com/GTC/2015/presentation/s 5584-Priyanka-SAH . pdf](http://on-demand.gputechconf.com/gtc/2015/presentation/S5584-Priyanka-Sah.pdf)\n*   [https://docs . NVIDIA . com/deploy/pdf/CUDA _ Multi _ Process _ Service _ overview . pdf](https://docs.nvidia.com/deploy/pdf/CUDA_Multi_Process_Service_Overview.pdf)\n\n# 分析 MPI 应用并了解 MPS 操作\n\n使用 MPI，来自多个进程的内核可以同时共享 GPU 资源，增强了 GPU 的整体利用率。在没有多处理器系统的情况下，由于时间片共享和上下文切换开销，GPU 资源的共享效率很低。\n\n以下截图显示了没有 MPS 的多个流程的时间线概要结果:\n\n![](img/13ccf0f1-20b5-40be-8738-7b460f2f5cac.png)\n\n在这个概要文件中，我们可以看到两个 CUDA 上下文共享一个 GPU，并且由于上下文之间的时间共享，内核执行时间被延长。\n\n另一方面，MPS 模式管理内核执行请求，因此所有内核执行都像使用单个进程一样启动。下面的截图显示了 MPS 模式下的内核执行:\n\n![](img/37c34d58-26dd-41ce-8289-65c68a535761.png)\n\n如您所见，一个图形处理器上只有一个 CUDA 流，并控制所有 CUDA 流。此外，所有的内核执行时间都是稳定的，并且使用 MPS 减少了总运行时间。总之，使用 MPS 模式有利于多个 GPU 进程的整体性能，并共享 GPU 资源。\n\n`nvprof`支持将多个 MPI 进程的探查器信息转储到不同的文件中。例如，对于基于开放 MPI 的应用，以下命令将转储多个文件中的分析信息，每个文件都有一个基于 MPI 进程等级的唯一名称:\n\n```cpp\n$ mpirun -np 2 nvprof -f -o simpleMPI.%q{OMPPI_COMM_WORLD_RANK}_2.nvvp ./simpleMPI 2\n```\n\n或者，您可以对示例配方代码使用以下命令:\n\n```cpp\n$ PROCS=2 STREAMS=2 make nvprof\n```\n\n然后，您将为每个流程获得两个`nvvp`文件。\n\n现在，我们将使用 NVIDIA 可视化探查器按照以下步骤查看这些`nvvp`文件:\n\n1.  打开文件|导入菜单，通过导入`nvvp`文件来创建分析会话:\n\n![](img/3f802cd1-25da-4e02-b172-9e41b730799f.png)\n\n在 Windows 或 Linux 中，快捷键是 *Ctrl* + *I* ，OSX 使用*命令* + *I* 。\n\n2.  然后从列表中选择“无教授”后，单击“下一步”按钮:\n\n![](img/2f84e5d6-9933-4c8d-81d1-e6a6bd03545e.png)\n\n3.  从 Nvprof 选项中，选择多个进程，然后单击下一步>:\n\n![](img/e70799cb-c6f8-4e70-abb6-f8704a433c83.png)\n\n4.  从导入虚拟教授数据中，单击浏览...按钮，选择`nvprof`生成的`nvvp`文件。要使用多进程分析应用，您需要导入`nvvp`文件，因为有多个进程:\n\n![](img/1582fbfc-ddd9-42b4-882e-23772b33e767.png)\n\n5.  单击“完成”，然后 NVIDIA 可视化探查器在时间线视图中显示分析结果，如下所示:\n\n![](img/75dd51b7-edb6-4d14-80d3-a73b6725191d.png)\n\n请注意，只有同步 MPI 调用才会被`nvprof`标注。在使用异步 MPI 应用编程接口的情况下，需要使用其他 MPI 专用的分析工具。一些最著名的工具包括:\n\n*   **TAU** : TAU 是一个性能评测工具包，目前由俄勒冈大学维护。\n*   **Vampir** :这是一个商用的工具，为数百个 MPI 进程提供了良好的可扩展性。\n*   **英特尔 VTune 放大器**:说到商用工具的另一个选择就是英特尔 VTune 放大器。它是可用的最佳工具之一，可用于 MPI 应用分析。\n\n最新的 CUDA 工具包也允许对 MPI 应用编程接口进行注释。为此需要将`--annotate-mpi`标志传递给`nvprof`，如下命令所示:\n\n```cpp\nmpirun -np 2 nvprof --annotate-mpi openmpi -o myMPIApp.%q{OMPI_COMM_WORLD_RANK}.nvprof ./myMPIApplciation\n```\n\n# 内核执行开销比较\n\n对于迭代并行 GPU 任务，我们有三种内核执行方法:迭代内核调用，有一个内部循环，有使用动态并行的递归。最佳操作由算法和应用决定。但是，您也可以考虑其中的内核执行选项。这个方法帮助您比较那些内核执行开销，并检查它们的可编程性。\n\n首先，让我们确定要测试哪个操作。这个食谱将使用一个简单的 SAXPY 操作。这有助于我们集中精力，进行迭代执行代码。此外，随着操作变得更简单，操作控制开销将变得更大。但是，你当然可以尝试任何你想做的手术。\n\n# 实现三种内核执行\n\n以下步骤涵盖了三种不同迭代操作的性能比较:\n\n1.  创建并导航`10_kernel_execution_overhead`目录。\n2.  用以下代码编写`simple_saxpy_kernel()`函数:\n\n```cpp\n__global__ void\nsimple_saxpy_kernel(float *y, const float* x, const float alpha, const float beta)\n{\n    int idx = blockIdx.x * blockDim.x + threadIdx.x;\n\n    y[idx] = alpha * x[idx] + beta;\n}\n```\n\n3.  用以下代码编写`iterative_saxpy_kernel()`函数:\n\n```cpp\n__global__ void\niterative_saxpy_kernel(float *y, const float* x, \n                       const float alpha, const float beta, \n                       int n_loop)\n{\n    int idx = blockIdx.x * blockDim.x + threadIdx.x;\n\n    for (int i = 0; i < n_loop; i++)\n        y[idx] = alpha * x[idx] + beta;\n}\n\n```\n\n4.  用以下代码编写`recursive_saxpy_kernel()`函数:\n\n```cpp\n__global__ void\nrecursive_saxpy_kernel(float *y, const float* x, \n                       const float alpha, const float beta, \n                       int depth)\n{\n    int idx = blockIdx.x * blockDim.x + threadIdx.x;\n\n    if (depth == 0)\n        return;\n    else\n        y[idx] = alpha * x[idx] + beta;\n\n    if (idx == 0)\n        vecAdd_kernel_C<<< gridDim.x, blockDim.x \n                           >>>(y, x, alpha, beta, depth - 1);\n}\n```\n\n5.  编写启动这些 CUDA 内核函数的主机代码。首先，我们将对`simple_saxpy_kernel()`函数进行迭代函数调用:\n\n```cpp\nfor (int i = 0; i < n_loop; i++) {\n    simple_saxpy_kernel<<< dimGrid, dimBlock >>>(\n                           d_y, d_x, alpha, beta);\n}\n```\n\n其次，我们将调用`iterative_saxpy_kernel()`内核函数，它内部有一个迭代循环:\n\n```cpp\niterative_saxpy_kernel<<< dimGrid, dimBlock >>>(\n                          d_y, d_x, alpha, beta, n_loop);\n```\n\n最后，我们将调用`recursive_saxpy_kernel()`内核函数，它以递归方式调用自己:\n\n```cpp\nrecursive_saxpy_kernel<<< dimGrid, dimBlock >>>(\n                          d_y, d_x, alpha, beta, n_loop);\n```\n\n循环数小于或等于 24，因为最大递归深度为 24。除了简单的循环操作之外，您不必在主机上放置循环操作，因为它已经在内核代码中定义了。\n\n6.  使用以下命令编译代码:\n\n```cpp\n$ nvcc -run -m64 -gencode arch=compute_70,code=sm_70 -I/usr/local/cuda/samples/common/inc -rdc=true -o cuda_kernel ./cuda_kernel.cu\n```\n\n您必须将您的图形处理器的计算能力版本号用于`gencode`选项。\n\n7.  测试编译后的应用。该结果是使用特斯拉 P40 测量的，因为 CUDA 9.x 不支持 Volta GPUs 的 **CUDA 动态并行度** ( **CDP** )配置文件:\n\n```cpp\nElapsed Time...\nsimple loop: 0.094 ms\ninner loop : 0.012 ms\nrecursion : 0.730 ms\n```\n\n# 三次处决的比较\n\n从结果中，我们可以确认内循环是迭代运算的最快方法。以下屏幕截图显示了此示例应用的分析结果:\n\n![](img/0040c495-f138-4acc-bf8a-c2e2b02ebbda.png)\n\n迭代内核调用显示了每个内核调用的内核启动开销。GPU 需要从设备内存中获取所有需要的数据，需要调度 GPU 资源等等。另一方面，内部循环内核显示一个打包的操作，因为所有需要的资源都是预先定位的，不需要重新安排它的执行。由于我们前面讨论的动态并行限制，递归内核操作显示了最长的执行时间。\n\n一般来说，建议使用开销最小的方法。然而，很难说哪个内核调用设计优于其他设计，因为算法及其问题比我们在这里讨论的更多。例如，CDP 在某些情况下用于增强并行性，例如用于 GPU 树和搜索。\n\n# 摘要\n\n在本章中，我们已经介绍了几种内核执行机制。我们介绍了什么是 CUDA 流，以及如何使用它们来并发执行多个内核函数。通过利用主机和 GPU 之间的异步操作，我们了解到可以通过使流水线架构具有数据传输和内核执行来隐藏内核执行时间。此外，我们可以使用回调函数进行 CUDA 流调用宿主函数。我们可以创建一个优先流，并确认其优先执行。为了测量内核函数的确切执行时间，我们使用了 CUDA 事件，我们还了解到 CUDA 事件可以用来与主机同步。在最后一节中，我们还讨论了每个内核执行方法的性能。\n\n我们还介绍了其他内核操作模型:动态并行和网格级协作组。动态并行支持内核函数内部的内核调用，因此我们可以用它进行递归操作。网格级协作组实现了通用的网格级同步，我们讨论了这个特性如何在特定领域有用:图搜索、遗传算法和粒子模拟。\n\n然后，我们将覆盖范围扩大到了主机。可以从多个线程或多个进程调用 CUDA 内核。为了执行多个线程，我们将 OpenMP 与 CUDA 结合使用，并讨论了它的实用性。我们使用 MPI 来模拟多个流程操作，并可以看到 MPS 如何提高整体应用性能。\n\n正如我们在本章中看到的，选择正确的内核执行模型是一个重要的主题，线程编程也是如此。这可以优化应用执行时间。现在，我们将讨论扩展到多 GPU 编程来解决大问题。"
  },
  {
    "path": "docs/learn-cuda-prog/05.md",
    "content": "# 五、应用分析和调试\n\nCUDA 为开发者提供了很多编程工具。这些工具是编译器、分析器、集成开发环境及其插件、调试器和内存检查器。了解这些工具将帮助您分析您的应用，并帮助您完成我们将要介绍的开发项目。在本章中，我们将介绍这些工具的基本用法，并讨论如何将它们应用于应用开发。\n\n本章将涵盖以下主题:\n\n*   GPU 应用中聚焦目标范围的剖析\n*   针对远程计算机的可视化分析\n*   调试有 CUDA 错误的 CUDA 应用\n*   使用 CUDA 断言来断言本地 GPU 值\n*   用 Nsight Visual Studio 版调试 CUDA 应用\n*   用 Nsight Eclipse 版调试 CUDA 应用\n*   用 CUDA-GDB 调试 CUDA 应用\n*   利用 CUDA-memcheck 进行运行时验证\n\n# 技术要求\n\n要完成本章，建议您使用比帕斯卡架构更晚的 NVIDIA GPU 卡。换句话说，你的图形处理器的计算能力应该等于或大于 60。如果您不确定您的 GPU 的架构，请访问英伟达的网站[https://developer.nvidia.com/cuda-gpus](https://developer.nvidia.com/cuda-gpus)，并确认您的 GPU 的计算能力。\n\n本章的示例代码已经用 CUDA 工具包的 10.1 版进行了开发和测试。一般来说，如果适用，建议您使用最新的 CUDA 版本。\n\n# GPU 应用中聚焦目标范围的剖析\n\n英伟达的视觉分析器是一个方便的工具，用于发现 GPU 应用中的瓶颈并了解它们的操作。虽然它提供了流畅的应用操作信息，但如果您只想专注于特定的代码领域，这些信息可能是多余的。在这种情况下，限制分析的范围更有成效。\n\n剖析目标可以是特定的代码块、GPU 和时间。指定代码块被称为**聚焦剖析**。当您希望专注于特定内核函数的分析，或者大型 GPU 应用的分析时，这种技术非常有用。锁定图形处理器或时间将在我们介绍重点分析之后介绍。\n\n# 在代码中限制分析目标\n\n为了从重点分析中获益，您可能希望在源代码中包含特色头文件，如下所示:\n\n```cpp\n#include <cuda_profiler_api.h>\n```\n\n然后，您可以使用`cudaProfilerStart()`和`cudaProfilerStop()`指定您的分析目标范围:\n\n```cpp\ncudaProfilerStart();\n... {target of profile} ...\ncudaProfilerStop();\n```\n\n现在，您需要用一个特定的标志`--profile-from-start`来描述您的应用。\n\n在请求到达之前，此选项不会让探查器开始探查。如果您想使用英伟达可视化评测器来评测您的应用，请确保在设置视图中勾选“在启用评测的情况下开始执行”复选框。\n\n以下步骤介绍了如何使用一些简单的示例代码来控制 NVIDIA profiler。为了使这变得更容易，我们将重用我们在[第 3 章](03.html)、 *CUDA 线程编程*中用于操作矩阵乘法的示例代码:\n\n1.  用两个简单的 SGEMM CUDA 内核函数编写一个 CUDA 应用。内核函数相同但名称不同，即`sgemm_kernel_A()`和`sgemm_kernel_B()`。\n2.  进行两次迭代调用，如下所示:\n\n```cpp\nint n_iter = 5;\nfor (int i = 0; i < n_iter; i++)\n    sgemm_gpu_A(d_A, d_B, d_C, N, M, K, alpha, beta);\nfor (int i = 0; i < n_iter; i++)\n    sgemm_gpu_B(d_A, d_B, d_C, N, M, K, alpha, beta);\n```\n\n3.  现在，让我们使用`nvprof`来编译代码和概要文件:\n\n```cpp\n$ nvcc -m64 -gencode arch=compute_70,code=sm_70 -o sgemm sgemm.cu\n$ nvprof -f -o profile-original.nvvp ./sgemm\n```\n\n当您使用可视化探查器打开生成的`profile-original.nvvp`文件时，您将获得如下分析结果:\n\n![](img/2bdf10e5-d9dc-48b0-b300-bff170a481a5.png)\n\n该时间表包括应用启动时的完整概要信息。然而，当我们想要优化我们的内核函数时，我们可以说概要结果包含不必要的信息。\n\n以下步骤介绍了如何指定轮廓聚焦区域:\n\n1.  将`#include <cuda_profiler_api.h>`放在源代码的顶部，以启用聚焦的概要文件 API。然后，我们可以拥抱我们感兴趣使用`cudaProfilerStart()`和`cudaProfilerStop()`的区域，如下所示:\n\n```cpp\ncudaProfilerStart();\nfor (int i = 0; i < n_iter; i++)\n    sgemm_gpu_B(d_A, d_B, d_C, N, M, K, alpha, beta);\ncudaProfilerStop();\n```\n\n2.  编译您的代码，并使用可视化探查器查看更新的分析结果。我们必须向探查器提供`--profile-from-start off`选项，如下所示:\n\n```cpp\n$ nvcc -m64 -gencode arch=compute_70,code=sm_70 -o sgemm sgemm.cu\n$ nvprof -f -o profile-start-stop.nvvp --profile-from-start off ./sgemm\n```\n\n当您打开新生成的配置文件结果时，探查器仅报告应用的指定部分，如下所示:\n\n![](img/9bcc2ff9-c3a3-4af8-b741-593489860e3d.png)\n\n配置文件结果受到限制。前面的截图显示了内核从开始执行 GPU 时的执行情况。因此，您可以不必分析应用的初始化和其他不相关的操作。\n\n总之，重点简介有以下几个好处:\n\n*   它帮助你专注于你正在开发的模块。\n*   它允许您从探查器的报告中删除不相关的操作，例如:\n\n    *   与您的代码没有任何关系的外部模块行为\n    *   应用初始化延迟\n*   在时间轴视图中查找目标函数时，它可以帮助您节省时间。\n\n# 用时间或图形处理器限制剖析目标\n\nNVIDIA profiler 还有其他选项可以限制配置文件目标。您也可以使用以下选项进行重点分析:\n\n*   `--timeout <second>`选项限制应用执行时间。当您需要通过迭代操作来分析执行时间较长的应用时，此选项非常有用。\n*   `--devices <gpu ids>`选项指定要分析的图形处理器。此选项可帮助您缩小多图形处理器应用中图形处理器内核操作的范围。\n\n此外，如果您只想关注少数几个内核函数，则不必收集所有的度量。您可以通过`--kernels`、`--event`和`--metrics`选项向轮廓仪表达您的兴趣。您可以将这些选项与其他配置文件选项一起使用，如下所示:\n\n```cpp\n$ nvprof -f -o profile_kernels_metric.nvvp --kernels sgemm_kernel_B --metrics all ./sgemm\n```\n\n在将收集的度量导入时间线概要文件结果后，您将发现目标内核只有度量信息。\n\nThere are many other versatile profile features in CPU sampling, such as marking profile range, OpenMP and OpenACC profiles, and so on. If you want to take a look at the features of the NVIDIA profiler, check out the following profiler introduction talk by Jeff Larkin from NVIDIA: [https://www.olcf.ornl.gov/wp-content/uploads/2018/12/summit_workshop_Profilers.pdf](https://www.olcf.ornl.gov/wp-content/uploads/2018/12/summit_workshop_Profilers.pdf).\n\nNVIDIA's official profiler user guide provides details about the NVIDIA profiler's functions ([https://docs.nvidia.com/cuda/profiler-users-guide/index.html](https://docs.nvidia.com/cuda/profiler-users-guide/index.html.)). \n\n# 使用 NVTX 进行分析\n\n通过聚焦剖析，我们可以使用`cudaProfilerStart()`和`cudaProfilerStop()`来剖析有限的特定区域。然而，如果我们想要分析复杂应用中的功能性能，它是有限的。对于这种情况，CUDA 探查器通过 **NVIDIA 工具扩展** ( **NVTX** )提供时间线注释。\n\n使用 NVTX，我们可以注释 CUDA 代码。我们可以如下使用 NVTX 应用编程接口:\n\n```cpp\nnvtxRangePushA(\"Annotation\");\n.. { Range of GPU operations } ..\ncudaDeviceSynchronization();     // in case if the target code block is pure kernel calls\nnvtxRangePop();\n```\n\n如您所见，我们可以将一个范围定义为一组代码，并手动注释该范围。然后，CUDA 探查器提供了注释的时间线跟踪，这样我们就可以测量代码块的执行时间。这样做的一个缺点是 NVTX APIs 是主机函数，所以如果目标代码块是纯 GPU 内核调用，我们需要同步主机和 GPU。\n\n为了进一步了解这一点，让我们将这个 NVTX 代码应用到前面的重点分析示例中。首先，我们应该包含一个 NVTX 头文件，如下所示:\n\n```cpp\n#include \"nvToolsExt.h\"\n```\n\n然后，我们将`nvtxRangePushA()`和`nvtxRangePop()`插入几个地方，如下:\n\n```cpp\n    cudaProfileStart();\n    // copy initial value for gpu memory\n    nvtxRangePushA(\"Data Transfer\");\n    cudaMemcpy(d_A, A, N * K * sizeof(float), cudaMemcpyHostToDevice);\n    cudaMemcpy(d_B, A, K * M * sizeof(float), cudaMemcpyHostToDevice);\n    cudaMemcpy(d_C, A, N * M * sizeof(float), cudaMemcpyHostToDevice);\n    nvtxRangePop();\n\n    nvtxRangePushA(\"Kernel Execution\");\n    // do operation\n    nvtxRangePushA(\"Kernel A\");\n    for (int i = 0; i < n_iter; i++)\n        sgemm_gpu_A(d_A, d_B, d_C, N, M, K, alpha, beta);\n    cudaDeviceSynchronize();\n    nvtxRangePop();    // Kernel A\n\n    nvtxRangePushA(\"Kernel B\");\n    for (int i = 0; i < n_iter; i++)\n        sgemm_gpu_B(d_A, d_B, d_C, N, M, K, alpha, beta);\n    cudaDeviceSynchronize();\n\n    nvtxRangePop();    // Kernel B\n    nvtxRangePop();    // Kernel Execution\n    cudaProfileStop();\n```\n\n在前面的代码中，我们扩大了聚焦轮廓区域，以监控 NVTX 操作。我们还有`Data Transfer`、`Kernel A`、`Kernel B`和`Kernel Execution`作为 NVTX 范围。NVTX 支持多级标注，因此`Kernel A`和`Kernel B`范围将包含在`Kernel Execution`时间线中。\n\n为了编译代码，我们应该向`nvcc`编译器提供`-lnvToolsExt`选项，以提供 NVTX API 的定义。我们可以使用以下命令编译代码:\n\n```cpp\n$ nvcc -m64 -gencode arch=compute_70,code=sm_70 -lnvToolsExt -o sgemm sgemm.cu\n```\n\n然后，NVIDIA profiler 可以在没有额外选项的情况下收集 NVTX 注释。我们可以使用以下命令分析应用:\n\n```cpp\n$ nvprof -f --profile-from-start off -o sgemm.nvvp ./sgemm.nvvp\n```\n\n以下屏幕截图显示了时间线分析结果。在这个截图中，我们可以看到绿色的标记和范围。这些绿色条有注释:\n\n![](img/c6943cf2-08ba-4c8e-974b-03436038efdf.png)\n\n前面的截图为我们提供了以下信息:\n\n*   我们可以识别在 NVTX 注释之后调用内存复制操作的位置。\n*   我们可以通过包裹区域来划分功能位置，例如`kernel A`和`kernel B`。\n*   NVTX 注释可以堆叠多级注释。我们可以看到，`kernel A`和`kernel B`包含在`kernel execution`注释中。\n\n以下文档不仅介绍了 NVTX，还解释了如何使用 NVTX 使用不同的颜色:[https://dev blogs . NVIDIA . com/cuda-pro-tip-generate-custom-application-profile-timeline-NVTX](https://devblogs.nvidia.com/cuda-pro-tip-generate-custom-application-profile-timelines-nvtx)。NVTX 的应用之一是用 NVTX 注释来描述深度学习网络。这提供了对网络运行瓶颈的洞察。我们将在本书第 10 章*用 CUDA* 进行深度学习加速中讨论这一点。\n\n# 针对远程计算机的可视化分析\n\nNVIDIA 可视化探查器还可以分析远程应用。当涉及到远程应用开发时，特别是当您在服务器端开发应用时，这个特性简化了分析任务。\n\n有几种使用可视化 profilers 的方法，如下所示:\n\n*   使用主机 CUDA 应用在主机上进行分析\n*   通过使用目标端的`nvprof` CLI 收集配置文件数据，将文件复制到主机，并使用可视化 Profiler 打开它\n*   使用主机在目标平台上分析应用\n\n直接在主机中进行可视化概要分析既方便又能节省开发时间。此外，远程分析提供了与分析主机上的图形处理器应用相同的用户体验。一个例外是我们应该建立远程连接。操作系统主机管理的可视化分析提供的另一个好处是，分析工具可以按需自动收集度量信息。\n\nNVIDIA profiler 与主机中的 NVIDIA profiler 通信，并收集分析数据。因此，您需要确认您的主机(台式机或笔记本电脑)应该连接到远程机器。下图显示了此连接的概述:\n\n![](img/5b19fd33-9512-4762-b55b-76746e212d43.png)\n\n让我们尝试远程剖析一个 GPU 应用。以下步骤介绍了如何在 NVIDIA 可视化探查器中分析远程图形处理器应用:\n\n1.  首先，转到文件|新会话。单击“新建会话”菜单时，您将看到以下对话框窗口:\n\n![](img/98f0371c-d094-481c-a716-ba64d95ddc8c.png)\n\n2.  然后，我们需要添加一个连接，这是通过转到管理连接来完成的...菜单。然后，将出现新建远程连接对话框。单击“添加”按钮，将您的远程计算机信息放入相应的部分，以添加您的远程计算机信息。然后，单击“完成”按钮关闭对话框。完成后，您将看到以下输出:\n\n![](img/a163c51c-0e85-4386-8545-6527b9445305.png)\n\n如前所述，主机和远程机器通过 SSH 通信，SSH 的默认端口号是 22。如果主机使用另一个端口进行 SSH，您必须在新的远程会话创建对话框中通知它该端口号。\n\n3.  现在，我们需要通过单击“管理”在远程机器中设置 CUDA 工具包路径...工具箱/脚本*右侧的按钮。*一个好的开始是使用检测按钮。找到`nvcc`路径，自动设置配置信息。如果自动检测失败，您必须手动输入配置信息。完成配置过程后，单击“完成”按钮，如下所示:\n\n![](img/214bb1ee-1f3e-413a-a15a-6ee7ab8ecbc9.png)\n\n4.  单击文件文本框右侧的浏览按钮，指定图形处理器应用的二进制文件。它会询问您的远程机器登录密码。找到您的应用路径并设置应用的路径。如果需要控制应用的行为，也可以放入应用的参数。设置完应用和连接后，单击“下一步”按钮设置探查器的选项。\n\n5.  现在，我们将设置分析选项。NVIDIA 可视化探查器允许我们使用复选框设置探查器的选项，如下图所示。通过单击“完成”，探查器从应用收集配置文件数据:\n\n![](img/258424fc-ee37-4d52-be8b-9572821710c5.png)\n\n您将看到在主机上分析的时间线分析输出。\n\n4.  最后，分析概要时间轴图的性能。单击要分析的任何内核函数。单击执行内核分析按钮；探查器工具将收集相关的度量信息。通过这样做，您可以快速获得关于性能限制器的报告，并找到内核函数的瓶颈。\n\n# 调试有 CUDA 错误的 CUDA 应用\n\n拥有专门的异常检查和错误检查是高质量软件的基本特征之一。CUDA 函数通过返回每个函数调用的状态来报告错误。不仅仅是 CUDA APIs，内核函数和 CUDA 库的 API 调用都遵循这个规则。因此，检测重复出现的错误是识别 CUDA 执行中错误的开始。例如，假设我们已经使用`cudaMalloc()`函数分配了全局内存，如下所示:\n\n```cpp\ncudaMalloc((void**)&ptr, byte_size);\n```\n\n如果全局内存没有足够的可用空间来分配新的内存空间怎么办？在这种情况下，`cudaMalloc()`函数返回一个错误来报告内存不足异常。内核调用触发的错误可以使用`cudaGetLastError()`从标志中捕获。这将返回记录的错误状态，并重置标志的值。但是，要小心处理这个标志:它的返回不能保证错误发生在 GPU 的最后一次执行，并且标志是手动重置的。\n\nCUDA APIs 的返回和`cudaGetLastError()`函数的返回属于`cudaError_t`类型。这个`cudaError_t`类型是一个预定义的整数类型，应用识别哪种类型的错误已经发生。例如，此类型定义如下:\n\n```cpp\nEnum cudaErorr_t {\n    cudaSuccess = 0,\n    cudaErrorMemoryAllocation = 2, \n    cudaErrorUnknown = 30,\n    cudaErrorNoDevice = 38,\n    cudaErrorAssert = 59,\n    cudaErrorTooManyPeers = 60,\n    cudaErrorNotSupported = 71,\n    ....\n};\n```\n\n记忆或翻译所有这些价值观是不切实际的。为此，CUDA 样例代码提供了一个助手函数`checkCudaError()`，它位于`common/inc/cuda_helper.h`中。当 CUDA 函数返回错误时，该函数打印出错误消息。其功能定义如下:\n\n```cpp\n#define checkCudaErrors(err) { \\\n    if (err != cudaSuccess) {  \\\n\n        fprintf(stderr, \"checkCudaErrors() API error = %04d \\\"%s\\\" from file <%s>, line %i.\\n\", \\\n                err, cudaGetErrorString(err), __FILE__, __LINE__); \\\n        exit(-1); \\\n    } \\\n}\n#endif\n```\n\n因为这个函数被定义为一个宏，所以我们可以识别发生错误的行。\n\n我们有两种方法可以使用这个函数。一是在源代码中包含`cuda_helper.h`文件。或者，我们可以将函数代码复制到代码中的某个地方。\n\n然后，我们将用`checkCudaErrors()`拥抱所有的 CUDA API 类，如下所示:\n\n```cpp\ncheckCudaErrors(cudaMalloc((void **)&d_A, N * K * sizeof(float)));\ncheckCudaErrors(cudaMalloc((void **)&d_B, K * M * sizeof(float)));\ncheckCudaErrors(cudaMalloc((void **)&d_C, N * M * sizeof(float)));\n```\n\n对于内核函数调用，我们将使用`cudaGetLastError()`函数获取内核调用的错误标志，如下所示:\n\n```cpp\nsgemm_kernel_A<<<dimGrid, dimBlock>>>(A, B, C, N, M, K, alpha, beta);\ncheckCudaErrors(cudaGetLastError());\n```\n\n但是这段代码有一个问题:内核操作与主机异步，使得`cudaGetLastError()`只能捕捉到主机端的返回值。很有可能该错误是在应用的某个地方触发的。要解决这种情况，您可以使用任何主机和设备同步功能；例如:\n\n```cpp\nsgemm_kernel_A<<<dimGrid, dimBlock>>>(A, B, C, N, M, K, alpha, beta);\ncheckCudaErrors(cudaDeviceSynchronize());\n```\n\n现在，让我们通过修改源代码生成一个错误来测试错误检测代码。例如，您可以请求`cudaMemcpy`复制比分配大小更大的内存空间。在这种情况下，应用返回一条错误消息，如下所示:\n\n```cpp\n$ nvcc -run -m64 -gencode arch=compute_70,code=sm_70 -I/usr/local/cuda/samples/common/inc -lnvToolsExt -o sgemm ./sgemm.cu\nCUDA error at sgemm.cu:93 code=11(cudaErrorInvalidValue) \"cudaMemcpy(d_A, A, N * K * sizeof(float), cudaMemcpyHostToDevice)\"\n```\n\n或者，您可以为 CUDA 内核传递一个`NULL`点，以便内核访问无效的内存空间。在这种情况下，应用在`cudaDeviceSynchronize()`中报告了一个非法地址错误:\n\n```cpp\n$ nvcc -run -m64 -gencode arch=compute_70,code=sm_70 -I/usr/local/cuda/samples/common/inc -lnvToolsExt -o sgemm ./sgemm.cu\nCUDA error at sgemm.cu:104 code=77(cudaErrorIllegalAddress) \"cudaDeviceSynchronize()\"\n```\n\n这个错误检查宏非常有用，因为它报告了错误在源代码中出现的位置。但是，该报告有一个遗漏点，即其错误检测位置与实际错误发生位置不匹配。\n\n错误消息应该报告我们复制大于分配内存的内存的位置，这会立即导致非法值错误。因此，开发人员可以在内核调用后立即识别错误消息。但是，这种错误检查代码只在主机上有效。因此，如果没有正确同步，这可能会混淆图形处理器的操作。例如，如果我们没有设置同步，只是检查错误，那么`cudaDeviceSynchronize()`功能可以从错误的地方报告错误。在这种情况下，我们可以设置`CUDA_LAUNCH_BLOCKING=1`环境变量，使所有内核执行与主机同步:\n\n```cpp\n$ ./sgemm\nCUDA error at sgemm.cu:104 code=77(cudaErrorIllegalAddress) \"cudaDeviceSynchronize()\" \n$ CUDA_LAUNCH_BLOCKING=1 ./sgemm\nCUDA error at sgemm.cu:36 code=77(cudaErrorIllegalAddress) \"cudaGetLastError()\"\n```\n\n第`sgemm.cu`行`36`是`cudaGetLastError()`呼叫，紧接在`sgemm`内核呼叫之后。这就是我们想犯的错误。我们可以在运行时识别正确的错误位置。\n\n有两个官方文档可以帮助您了解不同类型的 CUDA 错误:\n\n*   [https://docs . NVIDIA . com/cuda/cuda-runtime-API/group _ _ CUDART _ _ types . html](https://docs.nvidia.com/cuda/cuda-runtime-api/group__CUDART__TYPES.html)\n*   `include/driver_types.h`在 CUDA 工具包根路径中\n\n# 使用 CUDA 断言来断言本地 GPU 值\n\n即使您的图形处理器应用工作没有任何系统错误，您也需要检查计算结果，以确保执行按照设计进行。为此，CUDA 提供了`assert`函数，检查参数值是否为零。如果是，这个函数会引发一个错误标志，这样主机就可以识别内核函数中有错误。\n\n断言用于验证操作结果是否符合预期。在 CUDA 编程中，`assert`函数可以从设备代码中调用，并且可以在给定参数为零时停止内核的执行:\n\n```cpp\nvoid assert(int expression);\n```\n\n这是`assert`函数的声明，和 C/C++ 一样。当断言被触发时，应用停止并报告其错误消息。如果应用由调试器启动，它将作为断点工作，以便开发人员可以调试给定的信息。例如，输出消息如下所示:\n\n```cpp\n$ nvcc -run -m64 -gencode arch=compute_70,code=sm_70 -I/usr/local/cuda/samples/common/inc -lnvToolsExt -o sgemm ./sgemm.cu\nsgemm.cu:29: void sgemm_kernel_A(const float *, const float *, float *, int, int, int, float, float): block: [16,64,0], thread: [0,0,0] Assertion `sum == 0.f` failed.     \n```\n\n由于输出消息指导确切的 CUDA 块和线程索引，开发人员可以轻松地分析被指导的 CUDA 线程的执行。\n\n现在，让我们应用断言，看看它如何检测预期的错误。我们将修改我们在*中使用的 SGEMM 操作代码，在 GPU 应用*部分中分析聚焦目标范围。\n\n首先，将断言代码放在内核函数的中间。我们会看到表达式的效果，应该是假的。断言代码可以编写如下:\n\n```cpp\n__global__ void sgemm_kernel_A(const float *A, const float *B, float *C, int N, int M, int K, float alpha, float beta)\n  {\n      int col = blockIdx.x * blockDim.x + threadIdx.x;\n      int row = blockIdx.y * blockDim.y + threadIdx.y;\n      float sum = 0.f;\n      for (int i = 0; i < K; ++ i) \n          sum += A[row * K + i] * B[i * K + col];\n\n      if (row == 0 && col == 0)\n assert(sum == 0.f);\n\n      C[row * M + col] = alpha * sum + beta * C[row * M + col];\n  }\n```\n\n您可以尝试其他索引值，也可以尝试其他可能的错误。编译代码并运行它以查看输出。以下代码显示了此修改的输出错误:\n\n```cpp\nsgemm.cu:29: void sgemm_kernel_A(const float *, const float *, float *, int, int, int, float, float): block: [0,0,0], thread: [0,0,0] Assertion `sum == 0.f` failed.\n```\n\n错误消息报告断言触发了代码位置、内核函数的名称和 GPU 的线程索引。有了这些信息，我们可以很容易地找到应该从哪里开始分析。\n\n其实`assert`函数的用法和正常 C/C++ 编程中的`assert`函数是一样的。一个区别是`assert`功能在设备代码中工作。因此，它不仅报告事件位置和表达式，还显示块和线程索引。\n\n然而，使用断言会影响应用的性能。因此，我们应该只将断言用于调试目的。当我们在生产环境中运行时，建议禁用它。通过在包含`assert.h`之前添加`NDEBUG`预处理宏，可以在编译时禁用断言。\n\n# 用 Nsight Visual Studio 版调试 CUDA 应用\n\n对于 Windows 应用开发人员来说，CUDA 工具包提供了 Nsight Visual Studio Edition，它可以在 Visual Studio 中实现 GPU 计算。该工具是 Visual Studio 的扩展，但是您可以与主机一起构建、调试、分析和跟踪 GPU 应用。如果你的工作平台不是 Windows，本节内容不适用，可以跳过。\n\nCUDA 调试器允许我们监控每个 CUDA 线程的 GPU 内核上的本地值。像正常的主机调试一样，您可以在内核代码中设置断点并触发它们。您也可以放置条件，如其他正常断点。有了这个特性，您可以为特定的 CUDA 线程索引触发断点，并查看它们的局部变量。\n\n该工具可以与 CUDA 工具包一起安装。你可以从网站上获得最新版本。它不是强制性的，但是当您的开发环境在最新的 GPU 及其驱动程序上使用旧的 CUDA 工具包时，建议使用它。访问 NVIDIA Nsight 网页([https://developer.nvidia.com/nsight-visual-studio-edition](https://developer.nvidia.com/nsight-visual-studio-edition))下载并安装 Nsight。您需要获得英伟达开发人员会员资格才能获得该软件。您还需要安装推荐的显示驱动程序版本。\n\n您可以通过转到 Visual Studio 菜单栏中的菜单|右键来找到 CUDA 工具。该菜单中有几个工具，其中一些如下:\n\n*   **图形调试**:图形(Direct3D、OpenGL 和 Vulkan)应用的调试器\n*   **CUDA 调试(Next-Gen)** :同时调试 CPU 和 GPU 代码的调试器(图灵、Volta、Pascal 用最新的驱动)\n*   **CUDA 调试(Legacy)** :一个只针对 GPU 内核的调试器(Pascal 和旧的驱动程序，Maxwell 和 Kepler)\n*   **性能分析**:针对当前 GPU 应用性能的分析\n*   **CUDA 内存检查器**:用于在运行时检查 GPU 内存违规情况(如前一节所述)\n\n在本节中，我们将重点讨论 CUDA 调试(下一代)。这是因为下一代调试器可以支持最新的架构，包括图灵和 Volta。本章末尾将介绍 CUDA 内存检查器。\n\n现在，让我们配置一个示例项目，看看如何使用 Nsight Visual Studio Edition 调试应用。您可以使用默认的示例代码，或者用我们之前介绍的 CUDA 代码替换该代码。也可以使用`05_debug/05_debug_with_vs`文件中给定的样例代码。这是一些简单的 SAXPY 代码。\n\n设置项目属性以生成正确的设备目标代码。在项目的属性页中，可以指定目标代码版本。在 CUDA C/C++ |代码生成文本框中列出您想要使用的架构版本:\n\n![](img/db945ffd-c8da-46ed-9229-1b098affb0ee.png)\n\n前面的截图显示了 CUDA 设备代码的生成属性页。您可以设置几个`nvcc`选项，例如目标 GPU 的计算能力、每个线程的寄存器限制以及编译时冗长的 CUDA 内核信息。\n\n在内核函数中间的第 34 行和我们将数据从主机复制到设备的第 75 行放置断点。然后，使用以下方法之一编译并开始调试:\n\n*   导航到 Visual Studio 菜单栏中的“调试”，然后单击“开始 CUDA 调试(下一代)”。\n*   在解决方案资源管理器中右键单击项目，然后选择调试|开始 CUDA 调试(下一代)。\n*   转到 Nsight CUDA 调试工具栏，然后单击开始 CUDA 调试(下一代)。\n\nWindow's firewall may ask if you trust and want to allow the network connection of Nsight. This is normal, since Nsight uses the internal network to monitor GPU devices. Click *Accept* and continue the debugging. The current Nsight Visual Studio Edition provides two types of debugging options. It depends on the target GPU architecture version. If your GPU is Volta or Turing, it is recommended to use Next-Gen debugging. If your GPU is Pascal, the proper debugger differs, depending on the driver version. To clarify, please visit the supported GPU list from NVIDIA: [http://developer.nvidia.com/nsight-visual-studio-edition-supported-gpus-full-list](http://developer.nvidia.com/nsight-visual-studio-edition-supported-gpus-full-list).\n\n应用将在应用启动的地方停止。继续追踪。应用将在主机上的第 75 行和设备上的第 34 行停止。由此，我们可以了解到，Nsight 可以同时跟踪主机和设备上的 GPU 应用。\n\n当黄色箭头在内核函数中停止时，您可以查看局部变量。线程索引在全局索引中为`0`。由于 CUDA 并行发布多个 CUDA 经线和 CUDA 线程，您可以通过更改`blockIdx`和`threadIdx`来查看其他线程的局部变量。基本的 CUDA 线程调试控制单元是一个 warp。换句话说，您可以控制调试器，使其遍历活动扭曲。n 右侧调试器在右侧菜单栏的上一个活动扭曲/下一个活动扭曲菜单中提供了此功能。\n\n下面的屏幕截图显示了我们调试时出现的调试控件:\n\n![](img/dcf5eef0-05d5-4e19-8c5b-8ccc28f89508.png)\n\n如果更改扭曲，您会发现在“自动”面板中监控的局部变量会随着扭曲更新索引。例如，下面的屏幕截图显示了“自动”窗口，该窗口报告活动扭曲中所选线程的局部变量，即主导线程正在监控的局部变量的值:\n\n![](img/0989a8ac-17c6-4b59-822b-da1874dd3056.png)\n\n自动值会随着所选线程的更改而更新。以下屏幕截图显示了通过移动到下一个活动扭曲所做的更改:\n\n![](img/384f174f-f511-412d-bc3c-8a2a020852fe.png)\n\n新一代 CUDA 调试器提供三种类型的窗口——扭曲信息、通道和图形处理器寄存器。黄色箭头表示当前 GPU 执行情况，其信息分三个方面显示:\n\n*   “扭曲信息”窗口提供了另一种选择活动扭曲的方式。您可以从菜单栏中的窗口|扭曲信息打开窗口。窗口如下所示:\n\n![](img/792d956c-f385-4966-95e6-d0568f22959c.png)\n\n每行表示 CUDA 网格中的活动扭曲。第四列，着色器信息，显示每个扭曲的块和主导线程索引。第五列，线程，显示了 CUDA 线程在经线中的状态。单元格的颜色代表每个线程的状态。它们都是红色的，因为我们在断点处观察它们，但是在调试过程中您会看到其他颜色。下面的截图解释了每种颜色在线程状态方面的含义:\n\n![](img/b3d55d2f-75c5-4ea0-9b0a-66750ca6ef39.png)\n\n双击任何扭曲以了解自动窗口中的局部变量是如何更新的。\n\n*   “通道”窗口允许您在选定的活动扭曲中选择特定的 CUDA 线程。经线是指经线中的一根线。您可以从右侧|窗口|车道打开窗口。通过双击一个通道，您可以发现自动窗口中的局部变量会根据更新的索引进行更新:\n\n![](img/36e26550-f466-4939-947b-7577ff253123.png)\n\n车道在活动曲速中赢得信息。\n\n“寄存器”窗口显示图形处理器寄存器的当前状态。如果它们的值被更新，它们将是红色的。\n\nIf you want to learn how to use Nsight Visual Studio Edition, please read the official user guide from NVIDIA. It introduces how to configure a debugging environment, how to use it, and many detailed tips for various situations ([https://docs.nvidia.com/nsight-visual-studio-edition/Nsight_Visual_Studio_Edition_User_Guide.htm](https://docs.nvidia.com/nsight-visual-studio-edition/Nsight_Visual_Studio_Edition_User_Guide.htm)).\n\n# 用 Nsight Eclipse 版调试 CUDA 应用\n\n对于 Linux 和 OSX 平台开发，CUDA 工具包提供了 Nsight Eclipse 版。这个工具是基于 Eclipse 的，让开发人员在 CUDA C 开发中很容易习惯这个工具。\n\nNsight Eclipse Edition 是在 Eclipse 之上构建的，用于 CUDA 应用开发。您可以使用它来编辑、构建、调试和分析您的 CUDA 应用。它使得在 Linux 和 OSX 开发 CUDA C/C++ 变得容易。这个工具是和 CUDA 工具包作为一个包一起安装的，所以你不需要单独安装这个工具。但是，如果您使用的是 Linux，则需要为其操作配置 Java 7。\n\nNsight Eclipse Edition was built with Eclipse version 4.4.0 (Luna, released in 2014) and was built based on Java 7.\n\n可以通过终端或 X 窗口应用列表中的`nsight`命令来执行此操作。\n\n现在，让我们从您的终端或 X 窗口桌面打开 Nsight，这样我们就可以编译和分析给定的示例。要么创建一个新的 CUDA 项目，要么在`05_debug/06_debug_with_eclipse`中打开提供的样本项目。如果要创建项目，请选择 CUDA C/C++ 项目。空项目只是给你一个空项目，而 CUDA 运行时项目给你一个里面有一些示例代码的项目。如果要使用示例项目，请使用文件|导入|将现有项目导入工作区。\n\n让我们在`sgemm`内核函数中放置一个断点。就像 Eclipse 中一个普通的 C/C++ 项目一样，可以在`nsight`中构建和调试 CUDA 应用。在第 23 行放置一个断点作为内核函数的起始点，如下所示:\n\n![](img/b942282b-f014-4cca-8c91-4cd000f0f6f9.png)\n\n内核函数调试的一个很好的起点就是线程索引计算之后。放置一个断点来暂停 GPU 的执行。现在，通过单击菜单面板中的绿色 bug 来编译并开始调试。当调试窗口切换调试视角时，单击继续，直到到达我们放置的断点。\n\nNsight 允许您监控活动扭曲中的局部变量和寄存器。首先，它在 CUDA 网格中的领先 CUDA 线程(CUDA 线程`0`)处停止应用。然后，您可以从调试窗口移到另一个 CUDA 活动 warp，并使用 CUDA 窗口检查每个 CUDA 线程，如下所示:\n\n![](img/85a86088-303b-4e26-9837-80e14a25fc35.png)\n\n下面的截图显示了所选 CUDA 线程的局部变量信息。每当更新这些值时，Nsight 都会更新它们:\n\n![](img/9510862e-2a8b-4fec-b0df-5b181ca0f452.png)\n\n前面的截图显示了 Eclipse 调试透视图窗口中的调试窗口和 CUDA 窗口。调试窗口在选定的图形处理器和 CUDA 窗口上的活动扭曲中提供 CUDA 扭曲选择，并在选定的活动扭曲中启用通道选择。\n\nNVIDIA also has an Nsight Eclipse Edition user guide. You can learn more about this tool by going to [https://docs.nvidia.com/cuda/nsight-eclipse-edition-getting-started-guide/index.html](https://docs.nvidia.com/cuda/nsight-eclipse-edition-getting-started-guide/index.html).\n\n# 用 CUDA-GDB 调试 CUDA 应用\n\nCUDA 工具包提供了 CUDA-GDB，支持 C/C++ GDB 等程序的 CUDA C/C++ 调试。这对于直接调试没有 X 窗口环境或远程调试的 CUDA C/C++ 应用非常有用。\n\n要调试图形处理器应用，`Makefile`应该包括主机的`-g`调试标志和图形处理器的`-G`调试标志。基本上，CUDA 的 GDB 用法与主机调试相同，除了 CUDA 操作之外还有一些额外的调试功能。例如，我们可以设置特定的 CUDA 线程和 CUDA 感知断点。\n\n# CUDA-GDB 的断点\n\n让我们介绍一下`cuda-gdb`如何帮助我们检测代码中的错误。我们将在代码中设置断点，并查看主机和 GPU 上的本地值。为此，请将您的工作目录移动到`05_debug/07_debug_with_gdb directory`。我们将通过匹配适当的线来检查`cuda-gdb`操作。\n\n首先，让我们使用以下命令编译源代码:\n\n```cpp\n$ nvcc -run -m64 -g -G -Xcompiler -rdynamic -gencode arch=compute_70,code=sm_70 -I/usr/local/cuda/samples/common/inc -o simple_sgemm ./simple_sgemm.cu\n```\n\n然后，我们应该执行`cuda-gdb`以便在终端上调试应用，如下所示:\n\n```cpp\n$ cuda-gdb simple_sgemm\n```\n\n我们可以在代码的特定行上放置一个断点，如下所示:\n\n```cpp\n(cuda-gdb) break simple_gemm.cu:21\n```\n\n或者，我们可以在内核函数的名称上放置一个断点，如下所示。这将在函数的入口点触发断点:\n\n```cpp\n(cuda-gdb) break sgemm_kernel\n```\n\n如果`cuda-gdb`警告指出*断点想要在未来共享库加载*上挂起，则回答`y`。您还可以在宿主代码上设置断点。\n\n使用断点的一个问题是，断点将根据 CUDA 线程的数量来触发。因此，我们应该提供条件信息，以便针对特定的 CUDA 线程设置断点。条件断点如下:\n\n```cpp\n(cuda-gdb) break sgemm_kernel if blockIdx.y == 2\n```\n\n当然，我们可以修改预定义断点的条件，如下所示:\n\n```cpp\n(cuda-gdb) cond 3 // break 3 is defined previously\n```\n\n让我们使用`run`命令执行示例应用。如果应用遇到任何断点，CUDA-GDB 会提供相关信息。以下代码显示了应用在第`21`行遇到断点时的`cuda-gdb`报告:\n\n```cpp\n(cuda-gdb) run\n[Switching focus to CUDA kernel 0, grid 1, block (0,0,0), thread (5,0,0), device 0, sm 0, warp 0, lane 5]\nThread 1 \"simple_sgemm\" hit Breakpoint 1, sgemm_kernel<<<(128,128,1),(16,16,1)>>> (A=0x7fffb6000000, B=0x7fffb7000000, C=0x7fffb4000000, N=2048, M=2048, K=2048, alpha=2, beta=1) at simple_sgemm.cu:21\n21 int col = blockIdx.x * blockDim.x + threadIdx.x;\n```\n\n现在，是时候使用 GDB 命令来跟踪代码或监控活动变量了。我们可以用 next(或`n`)、step(或`s`)、continue(或`c`)和 finish(或`fin`来追踪内核函数。然而，当我们到达内核代码的末尾，需要在主机和设备之间切换目标硬件时，我们应该使用`continue`命令。\n\n# 用 CUDA-GDB 检验变量\n\n在默认的 GDB 命令之上，CUDA-GDB 提供了可以与 CUDA 内核一起工作的调试功能。以下是你可以用 CUDA-GDB 做的事情。\n\n# 列出内核函数\n\n像普通函数一样，CUDA-GDB 可以在内核函数上设置断点。一旦应用被断点停止，您可以按如下方式列出它们:\n\n```cpp\n(cuda-gdb) info cuda kernels\nKernel Parent Dev Grid Status   SMs Mask     GridDim  BlockDim Invocation\n*      0      -   0    1 Active 0xffffffff (128,128,1) (16,16,1) sgemm_kernel(A=0x7ffff5a79010, B=0x7ffff4a78010, C=0x7ffff3a77010, N=2048, M=2048, K=2048, alpha=2, beta=1)\n```\n\n如您所见，前面的输出显示了内核的配置信息和输入参数变量。\n\n# 变量调查\n\nCUDA-GDB 通过选择特定的线程块索引和线程索引来帮助我们跟踪特定的 CUDA 线程。使用此功能，您可以将当前焦点移动到指定的线程。在本例中，块大小为 16，`col`变量被定义为`x`维度中的 CUDA 线程索引。以下代码显示了 CUDA-GDB 如何通过更改线程索引来报告所选局部变量的值:\n\n```cpp\n(cuda-gdb) print col\n$1 = <optimized out>\n(cuda-gdb) cuda kernel 0 block 1,2,0 thread 3,4,0\n21 int col = blockIdx.x * blockDim.x + threadIdx.x;\n(cuda-gdb) s\n22 int row = blockIdx.y * blockDim.y + threadIdx.y;\n(cuda-gdb) p col\n$2 = 19\n```\n\n检查当前聚焦线程信息:\n\n```cpp\n(cuda-gdb) cuda device kernel block thread\nkernel 3, block (1,2,0), thread (3,4,0), device 0\n```\n\n有了手头的信息，我们就可以追踪 CUDA 线程了。\n\nIf you want to learn more about CUDA-GDB, please check the user guide documentation from NVIDIA: [https://docs.nvidia.com/cuda/cuda-gdb/index.html](https://docs.nvidia.com/cuda/cuda-gdb/index.html).\n\n# 利用 CUDA-memcheck 进行运行时验证\n\nCUDA 编程的一个难点是处理内存空间。由于 CUDA 线程并行运行，边界条件或意外的索引操作可能会破坏有效的内存空间。CUDA memcheck 是一个运行时测试工具，如果任何 GPU 操作超过无效内存空间，它将验证内存访问。此工具检测以下内存错误:\n\n| \n\n名字\n\n | \n\n位置\n\n | \n\n描述\n\n | \n\n精确的\n\n |\n| 内存访问错误 | 设备 | 无效的内存访问(越界、未对齐) | O |\n| 硬件异常 | 设备 | 硬件错误 | X |\n| malloc/自由错误 | 设备 | CUDA 内核中`malloc()` / `free()`使用不正确 | O |\n| CUDA 应用编程接口错误 | 主持 | CUDA 应用编程接口的错误返回 | O |\n| cudaMalloc 内存泄漏 | 主持 | 应用没有释放使用`cudaMalloc()`分配的设备内存 | O |\n| 设备堆内存泄漏 | 设备 | 应用不会释放在设备代码中使用`malloc()`分配的设备内存 | X |\n\n精确(0)意味着 memcheck 可以指定崩溃的行和文件。另一方面，不精确(X)意味着工具可以识别错误，但由于并发状态而无法指定错误点。`cuda-memcheck`测试不需要重新编译。然而，如果我们用一些额外的`nvcc`选项进行编译，我们可以跟踪错误点。`nvcc`选项包括生成行号信息的`-lineinfo`和用于保留功能符号的`-Xcompiler -rdynamic`。\n\n基本上，`cuda-memcheck`是一个独立的工具，在运行时验证 GPU 应用。以下命令在独立模式下显示其格式:\n\n```cpp\n$ cuda-memcheck [options] <application>\n```\n\n该工具还可以与 CUDA-GDB 一起工作，帮助开发人员识别错误并进行调试。在 CUDA-GDB 命令行中，使用`set cuda memcheck on`命令启用内存检查。这样，CUDA-GDB 可以识别与内存相关的异常。\n\n# 检测到内存越界\n\n现在，让我们看看`cuda-memcheck`如何检测内存异常并使用 CUDA-GDB。为了缓解这种情况，我们将制作一些错误的代码，看看`cuda-memcheck`如何报告结果。让我们从一些干净的代码开始。您可以为此使用`05_debug/08_cuda_memcheck`中给定的示例代码。让我们使用`cuda-memcheck`测试代码并验证它:\n\n```cpp\n$ nvcc -m64 -g -G -Xcompiler -rdynamic -gencode arch=compute_70,code=sm_70 -I/usr/local/cuda/samples/common/inc -o simple_sgemm ./simple_sgemm.cu\n$ cuda-memcheck simple_sgemm\n========= CUDA-MEMCHECK\nApplication finished successfully.========= ERROR SUMMARY: 0 errors\n```\n\n现在，让我们将一些错误的代码放入内核函数，如下所示。如果愿意，您可以输入另一个错误:\n\n```cpp\nFor instance, you may add one to the row value.\n__global__ void sgemm_kernel(const float *A, const float *B, float *C, int N, int M, int K, float alpha, float beta)\n{\n    int col = blockIdx.x * blockDim.x + threadIdx.x;\n    int row = blockIdx.y * blockDim.y + threadIdx.y;\n    row += 1;\n\n    float sum = 0.f;\n    for (int i = 0; i < K; ++ i)\n        sum += A[row * K + i] * B[i * K + col];\n    C[row * M + col] = alpha * sum + beta * C[row * M + col];\n}\n```\n\n让我们编译并启动代码。内核将返回一个 CUDA 错误，`checkCudaErrors()`将报告一条错误消息，如下所示:\n\n```cpp\nCUDA error at simple_sgemm_oob.cu:78 code=77(cudaErrorIllegalAddress) \"cudaDeviceSynchronize()\"\n```\n\n然而，如果我们想确定内核代码中的哪一行是问题的根本原因，这些信息是不够的。使用`cuda-memcheck`，我们可以用一个堆栈地址来识别是哪个 CUDA 线程和内存空间触发了错误:\n\n```cpp\n$ cuda-memcheck simple_sgemm_oob\n```\n\n输出如下:\n\n![](img/2be60221-e0bb-4993-a55c-d90934c2cd8a.png)\n\n前面的截图显示了`cuda-memcheck`独立执行的一部分，它显示了从发生错误的内核中检测到的所有错误。在这种情况下，`cuda-memcheck`报告在第`27`行检测到内存违规错误。默认情况下，`cuda-memcheck`在检测到错误时停止应用的执行。\n\n在这种情况下，我们可以通过使用`cuda-gdb`检查相关变量来轻松找到根本原因。为此，我们需要使用`cuda-gdb`启动应用并启用`cuda-memcheck`，如下所示:\n\n```cpp\n$ cuda-gdb simple_sgemm_oob\n(cuda-gdb) set cuda memcheck on\n(cuda-gdb) run\n```\n\n本程序使`cuda-gdb`从`cuda-memcheck`报告非法内存访问检测:\n\n![](img/7718fc0f-e576-4c4e-9d31-2a921c5045bf.png)\n\n上面的截图显示了一个来自`cuda-gdb`和`cuda-memcheck`的报告。开发人员可以很容易地识别出`simple_sgemm_oob.cu`中的`27`行触发了报告的错误。根据给定的信息，我们可以开始调查哪块内存访问了无效空间，如下所示:\n\n```cpp\n(cuda-gdb) print A[row * K + i]\nError: Failed to read generic memory at address 0x7fffc7600000 on device 0 sm 41 warp 20 lane 16, error=CUDBG_ERROR_INVALID_MEMORY_SEGMENT(0x7).\n(cuda-gdb) print row * K + i\n$1 = 4194304\n```\n\n无需费力，我们就可以确定访问`A[row * K + i]`会触发错误，并且请求的值超过了全局内存的(`A`)分配空间。通过这种方式，你可以不费力地缩小根本原因。\n\n# 检测其他内存错误\n\nCUDA memcheck 工具提供了额外的软件验证功能，其中一些如下:\n\n| **名称** | **描述** | **选项** |\n| 内存泄漏 | 用于识别内存泄漏 | `--leak-check full` |\n| 比赛检查 | 为了分析多线程之间对共享存储器的冲突访问的竞争危险 | `--tool racecheck` |\n| 初始化检查 | 无需初始化即可识别设备全局内存访问 | `--tool initcheck` |\n| 同步检查 | 验证同步原语的正确使用，如`__syncthreads()`、`__syncwarp()`和协作组 API | `--tool synccheck` |\n\n这些工具假设内存访问是正确的或经过验证的，并且不检查内存错误。因此，您需要确认应用中不存在内存错误。其他有用的 memcheck 选项包括`--save`，我们可以使用它将输出保存到磁盘，以及`--print-level`，我们可以使用它来控制输出细节级别。\n\nNVIDIA provides a user guide for `cuda-memcheck`. This document will help you validate your application using a GPU and detect unexpected errors ([https://docs.nvidia.com/cuda/cuda-memcheck/index.html](https://docs.nvidia.com/cuda/cuda-memcheck/index.html)).\n\n# 用嵌入式系统分析图形处理器应用\n\n在这一节中，我们将介绍新引入的 CUDA 探查器工具，即 Nsight Systems 和 Nsight Compute。这些 profilers 支持 Volta 架构和更高版本的图形处理器。它是图灵架构图形处理器中的主要剖析器。我们将在下一节介绍恩西计算之前，先介绍恩西系统。\n\nnsight Systems([https://developer.nvidia.com/nsight-systems](https://developer.nvidia.com/nsight-systems))是一款全系统性能分析工具，可以在时间轴中可视化操作，轻松找到优化点。在时间线分析方面，恩希特系统公司提供系统端利用率信息，以便我们分析瓶颈点。我们可以从 NVIDIA 网站获得 Nsight Systems，但是默认情况下 CUDA 10 在工具包包中包含了 Nsight Systems。我们要做的就是确保它安装正确。\n\n对于命令行界面，我们应该设置`PATH`来简化我们的操作，因为它的路径与普通的 CUDA 二进制文件是分开的。让我们使用以下命令将它包含在`PATH`环境变量中:\n\n```cpp\nexport PATH=$PATH:/usr/local/cuda/bin:/usr/local/cuda-10.1/NsightSystems-2019.3/Target-x86_64/x86_64\n```\n\n系统提供两个界面:一个用于图形用户界面，一个用于命令行界面。在主机上，我们可以通过图形用户界面运行应用来收集应用的采样信息。在远程计算机上，我们可以通过命令行界面使用以下命令收集分析数据:\n\n```cpp\n$ nsys profile -t osrt,cuda,nvtx,cublas,cudnn -o baseline -w true <command>\n```\n\n该选项可以解释如下:\n\n|  | [计]选项 | 开关 |\n| 描摹 | `-t` / `--trace` | `cuda`:对于追溯 CUDA 操作，`nvtx`:追踪`nvtx`标签，`cublas`、`cudnn`、`opengl`、`openacc`:为了跟踪 API 操作，`osrt`:跟踪操作系统运行时库，`none`:无 API 痕迹 |\n| 输出文件 | `-o` / `--output` | 输出文件名 |\n| 显示输出 | `-w` / `--show-`输出 | `true` / `false`:打印出终端上探查器的行为 |\n\n例如，我们可以从`02_nvtx` SGEMM 应用中获取一个名为`sgemm.qdrep`的概要文件。让我们比较一下 Nsight 系统和 NVIDIA 视觉分析器之间的分析输出。我们可以使用以下命令收集导航系统的配置文件数据:\n\n```cpp\n$ nsys profile -t osrt,cuda,nvtx -o sgemm -w true ./sgemm\n```\n\n这是恩塞特系统公司的时间线视图:\n\n![](img/56430c7c-9262-4bd9-baac-e5f00e655e18.png)\n\n下面的屏幕截图显示了 NVIDIA 可视化探查器中的时间线概要视图:\n\n![](img/52c8ca52-275f-4dd2-a7cc-d17b30f0dfc0.png)\n\n视觉探查器显示操作事件块，但夜间系统显示系统利用率。因此，我们可以很容易地看出哪种资源——中央处理器内核、图形处理器或 PCIe 总线——对性能有影响。此外，恩希特系统公司还提供了更具互动性的分析体验。当您双击任何功能操作时，系统查看器会扩展时间线以适合窗口，并帮助我们检查操作。此外，Nsight Systems 使我们能够轻松发现在某个 NVTX 区域下发生的内核执行数量。在可视化探查器时间线视图中，内核执行看起来像一个单独的执行，但是 Nsight Systems 显示了单独的执行。\n\n现在，我们已经确定了一个函数应该被优化，我们可以继续进行 Nsight Compute，这是另一个新的探查器，用于检查内核函数的 GPU 操作。\n\n# 使用智能计算分析内核\n\nNsight Compute 是一个用于计算的内核级分析器。它收集 GPU 度量信息，并帮助我们专注于 CUDA 内核的优化。换句话说，该工具涵盖了可视化探查器的性能分析功能。\n\nn 智能计算提供了两个界面:图形用户界面和命令行界面。图形用户界面支持主机和远程应用配置文件，而命令行界面在目标计算机上工作。但是，我们可以获取概要数据，并使用图形用户界面查看结果。\n\n# 使用命令行界面进行分析\n\n为了方便使用恩西计算命令行界面，我们需要在`/usr/local/cuda-10.1/NsightCompute-2019.3/nv-nsight-cu-cli`中为恩西计算路径设置`PATH`环境变量。然后，我们可以使用以下命令收集配置文件数据:\n\n```cpp\n$ nv-nsight-cu-cli -o <output filename> <application command>\n```\n\n此命令收集 GPU 执行度量信息，并将数据保存到指定文件中。如果我们不提供输出文件名，Nsight Compute 会将收集到的指标报告报告给控制台，控制台会通过控制台提供快速的指标性能报告。\n\n由于我们可以指定分析目标，因此我们可以将恩西计算限制为收集以下信息:\n\n*   `--kernel-regex`:指定概要文件的内核\n*   `--devices`:专注于剖析特定的图形处理器\n\n当我们必须在控制台上查看报告时，此功能非常有用。\n\n# 使用图形用户界面进行分析\n\n通过在 Nsight Compute 中打开一个新项目，我们可以启动概要文件操作。下面的截图显示了配置文件配置。对于主机应用开发，连接到本地主机。或者，您可以指定我们要配置的目标 GPU 服务器:\n\n![](img/56cc2622-8952-4b5f-b66b-c9f5ae257f4f.png)\n\n当然，我们也可以打开`nsight-cuprof-report`文件，它是用 CLI 工具在目标机器上生成的。例如，我们可以使用以下命令创建 sgemm 概要文件:\n\n```cpp\n$ nv-nsight-cu-cli -o reduction reduction\n```\n\nFor OSX users, Nsight Systems will require the target `glib` library for remote profiling. In this case, we should copy the library from the Nsight Compute installation image. It provides the required libraries as a directory named target and copies that directory to the `Applications/NVIDIA Nsight Compute.app/target` directory.\n\n为了便于本实验，我们将使用来自 [第 3 章](07.html)*CUDA 线程编程*的简化示例代码。它有两种不同寻址的并行约简实现。可以从`03_cuda_thread_programming/05_warp_divergence`目录找到代码。完成后，单击启动按钮设置连接和应用可执行文本栏，如连接进度图所示。然后，放 *Ctrl* + *I* ， *Ctrl* + *K* 键运行到下一个内核函数，这时 profiler 会停在`reduction_kernel_1`。放 *Ctrl* + *I* ， *Ctrl* + *P* 键对这个内核进行轮廓化。然后，您将获得以下输出。此图显示了 Nsight Compute 基于图形用户界面的第一个内核概要分析:\n\n![](img/970759b9-b14a-437c-a647-92e4659b6e9a.png)\n\nOutput showing GUI-based profiling (for the first kernel profiling)\n\n它提供交互式分析和调试。使用步骤控制调试按钮，我们可以调试 CUDA 应用编程接口和内核函数。我们还可以使用左侧 API 流面板上的控制按钮，移动到下一个内核函数或下一个概要文件范围。在右侧面板上，您可以获得内核的详细概要信息。\n\n我们还可以通过以下步骤启用自动配置文件来自动获取配置结果—转到菜单栏并选择配置文件|自动配置文件。然后，继续申请。系统将分析所有的内核函数。或者，您可以通过单击窗口顶部的“配置内核”按钮来手动配置内核函数。当我们使用 CLI 收集的概要结果时，我们将只看到来自所有内核函数的概要数据。\n\n# 性能分析报告\n\n正如我们在交互式配置文件窗口的右侧面板中所看到的，Nsight Compute 提供了一个性能分析报告。从报告中，我们可以发现性能限制因素，并调查未充分利用的资源。此外，Nsight Compute 还根据资源利用率统计数据提供优化建议。我们也可以从直接侧面识别它们。\n\n此外，恩希特计算通过分析图形处理器组件的利用率提供优化建议。它发现了一个瓶颈，并提出了一个优化内核的建议调查。\n\n此报告页面提供了每个组件的利用率，如计算、内存、调度程序、指令、扭曲等。此外，通过扩展每个组件的左上角箭头，您可以获得更多细节。下图显示了内存工作负载分析的示例报告:\n\n![](img/21b05b61-2775-47c0-9f4c-66a009420ec4.png)\n\n在智能计算中，我们可以很容易地获得如此详细的信息。在之前的 profiler，NVIDIA Profiler 中，我们应该执行每个分析来获取这样的信息。\n\n# 基线比较\n\n在优化过程中，我们应该比较基线操作的新结果。为了让我们轻松完成这项任务，恩塞特计算公司提供了基线比较功能。单击性能报告面板顶部的添加基线按钮，并将其更改为其他内核函数。然后，我们可以使用 Nsight Compute 来比较内核函数的利用率。以下屏幕显示了这一点:\n\n![](img/e2f2eadc-5230-453e-911b-038a21d64ac6.png)\n\nComparison of kernel function utilizations\n\n如果我们希望跟踪我们的优化工作并确定有效的组件，这将非常有用。\n\n# 源视图\n\nNsight Compute 提供了我们可以调查的各种页面。其中一个有用的页面是源页面。如果 CUDA 应用是用`-lineinfo`选项构建的，那么 Nsight Compute 可以用 CUDA SASS 代码显示 CUDA C/C++ 源码的相关信息。然后，我们可以分析瓶颈代码，并研究它与 SASS 代码级别的关系。此外，它还提供了一个活动寄存器数，这样我们就可以研究内核函数中所需寄存器的数量。以下屏幕截图显示了“来源”页面:\n\n![](img/69711bc7-a6e5-461d-a97d-bdff04a62418.png)\n\n如果您需要了解该功能的更多信息，可以在本文档中找到相关信息—[https://docs . NVIDIA . com/n right-compute/NsightCompute/index . html # profiler-report-source-page](https://docs.nvidia.com/nsight-compute/NsightCompute/index.html#profiler-report-source-page)。\n\nNsight Compute 提供了一个以 CUDA 内核性能分析为中心的操作，我们可以用它来验证 Night Systems 和 Nsight Compute 有不同的优化范围。\n\n# 摘要\n\n在这一章中，我们已经介绍了如何分析 GPU 应用并调试它。了解这些 CUDA 工具将有助于您高效和有效地开发，因为它们可以帮助您务实地找到瓶颈，并在短时间内找到错误和 bug。\n\n到目前为止，我们一直专注于单 GPU 应用开发。然而，许多 GPU 应用使用多个 GPU 来实现更好的性能。在下一章中，我们将介绍如何编写在多个 GPU 上工作的代码，并以可扩展的性能为目标。您将了解什么会对绩效产生影响，以及如何达到良好的绩效水平。您还将能够应用我们在本章下一章的问题中介绍的工具来增强多个 GPU 系统及其体验。"
  },
  {
    "path": "docs/learn-cuda-prog/06.md",
    "content": "# 六、可扩展的多图形处理器编程\n\n到目前为止，我们专注于在单个 GPU 上获得最佳性能。具有多个 GPU 的密集节点已经成为对即将到来的超级计算机的迫切需求，尤其是因为 ExaFLOP(每秒五千万次操作)系统正在成为现实。GPU 架构是高能效的，因此，近年来，拥有 GPU 的系统占据了绿色 500 强榜单([https://www.top500.org/green500](https://www.top500.org/green500))的大部分榜首位置。在 green 500 2018 年 11 月的榜单中，前 10 名系统中有 7 款基于 NVIDIA GPU。\n\n英伟达的 DGX 系统现在在一台服务器中有 16 V100 32 GB。借助统一内存和 NVLink、NvSwitch 等互连技术，开发人员可以将所有 GPU 视为一个拥有 512 GB 内存(每个 16 GPU * 32 GB)的大 GPU。在这一章中，我们将深入到编写 CUDA 代码的细节，并利用 CUDA 感知库在节点内和跨节点的多 GPU 环境中高效地获得可伸缩性。\n\n在本章中，我们将涵盖以下主题:\n\n*   用高斯消去法求解线性方程\n*   点对点\n*   MPI 简介\n*   GPUDirect RDMA\n*   CUDA 流\n*   其他技巧\n\n# 技术要求\n\n这一章需要一台带有现代 NVIDIA GPU (Pascal 架构以后)的 Linux 电脑，安装所有必要的 GPU 驱动程序和 CUDA 工具包(10.0 以后)。如果您不确定自己的 GPU 架构，请访问 NVIDIA GPU 的网站([https://developer.nvidia.com/cuda-gpus](https://developer.nvidia.com/cuda-gpus))并确认您的 GPU 架构。本章的代码也可以在 https://github.com/PacktPublishing/Learn-CUDA-Programming 的 GitHub 上找到。\n\n本章中的示例代码已经用 CUDA 版本 10.1 进行了开发和测试。但是，建议您使用最新版本(CUDA)或更高版本。\n\n由于本章需要展示多 GPU 交互，我们将需要至少两个相同类型和架构的 GPU。此外，请注意，某些功能，如 GPUDirect RDMA 和 NVLink，仅在 NVIDIA 的特斯拉卡上受支持。如果你没有特斯拉 P100 或特斯拉 V100 这样的特斯拉卡，不要灰心丧气。您可以放心地忽略其中一些功能。与我们在这里展示的相比，性能数字将会有所变化，但是相同的代码将按原样工作。\n\n在下一节中，我们将看一个流行的高斯算法求解一系列线性方程的例子，以演示如何编写多 GPU。\n\n# 用高斯消去法求解线性方程\n\n为了演示节点内和跨节点的多个图形处理器的用法，我们将从一些顺序代码开始，然后将其转换为节点内和跨节点的多个图形处理器。我们将求解一个包含 *M* 方程和 *N* 未知数的线性方程组。该方程可以表示如下:\n\n***A × x = b***\n\n这里 *A* 是一个有 *M* 行和 *N* 列的矩阵， *x* 是一个有 *N* 行的列向量(也称为解向量)，而 *b* 也是一个有 *M* 行的列向量。求解向量涉及到给定 *A* 和 *b* 时计算向量 *x* 。求解线性方程组的标准方法之一是高斯消去法。在高斯消去法中，通过执行初等行变换，第一矩阵 *A* 被简化为上三角矩阵或下三角矩阵。然后，通过使用反向替换步骤来求解所得的三角形方程组。\n\n下面的伪代码解释了求解线性方程的步骤:\n\n```cpp\n1\\. For iteration 1 to N (N: number of unknowns) \n    1.1 Find a row with non-zero pivot\n    1.2 Extract the pivot row\n    1.3 Reduce other rows using pivot row\n2 Computing the solution vector through back substitution\n```\n\n让我们看一个例子来理解这个算法。假设方程组如下:\n\n![](img/2ce99d49-d0fa-45b2-9185-fd05c2242e1e.png)\n\n首先，我们将尝试设置基线系统，如下所示:\n\n1.  准备好你的 GPU 应用。这段代码可以在本书 GitHub 储存库中的`06_multigpu/gaussian`文件夹中找到。\n\n2.  使用`nvcc`编译器编译您的应用，如下所示:\n\n```cpp\n$ nvcc -o gaussian_sequential.out gaussian_sequential.cu\n$ nvcc -o gaussian_single_gpu.out gaussian_single_gpu.cu\n$ $ time ./gaussian_sequential.out\n$ time ./gaussian_single_gpu.out\n```\n\n前面的步骤编译并运行本章中的两个版本的代码:\n\n*   按顺序运行的中央处理器代码\n*   CUDA 代码，运行在单个图形处理器上\n\n现在，让我们来看看高斯消除的单个 GPU 实现中的热点。\n\n# 高斯消去法的单 GPU 热点分析\n\n让我们尝试理解和剖析顺序和单个 GPU 代码，以设置基线。在此基础上，我们将增强和增加对在多 GPU 上运行的支持。\n\n**顺序 CPU 代码**:以下代码为顺序实现的提取代码:\n\n```cpp\nfor( int n = 0; n < N; n++ ){\n// M: number of equations, N: number of unknowns\n    for( int pr = 0; pr < M; pr++ ){\n        // finding the pivot row \n        //if pr satisfies condition for pivot i.e. is non zero \n        break; \n    }\n    for( int r = 0; r < M; r++ ){\n        // reduce all other eligible rows using the pivot row\n        double ratio = AB[r*N+n]/AB[pr*N+n]\n        for( int nn = n; nn < N + 1; nn++ ){\n            AB[r * N + nn] -= (ratio*AB[pr * N + nn]);\n        }\n    }\n}\n```\n\n视觉上，发生的操作如下:\n\n![](img/b938a13f-62d4-416d-a911-ad1be7c4db3d.png)\n\n这里，在这个高斯消去法中，行数等于方程数，列数等于未知数数。上图中所示的 **pr** 行是枢轴行，将用于使用枢轴元素减少其他行。\n\n我们可以做的第一个观察是，我们正在对一个增广矩阵进行操作，以合并 *A* 矩阵和 *b* 向量。因此，未知数的大小是 *N+1* ，因为增广矩阵的最后一列是 *b* 向量。创建一个增广矩阵可以帮助我们处理一个数据结构，也就是矩阵。您可以使用以下命令分析这段代码。分析结果将向您显示`guassian_elimination_cpu()`功能花费的时间最多:\n\n```cpp\n$ nvprof --cpu-profiling on ./guassian_sequential.out\n```\n\n**CUDA 单 GPU 代码**:看完前面的章节，我们希望大家已经熟悉了如何编写最优的 GPU 代码，因此我们就不深入单 GPU 实现的细节了。以下摘录显示，在单个 GPU 实现中，这三个步骤被称为用于寻找 *N* 未知数的三个核:\n\n*   `findPivotRowAndMultipliers<<<...>>>`:内核找到轴心行和乘数，应该用于行消除。\n*   `extractPivotRow<<<>>>`:内核提取轴心行，然后用于执行行消除。\n*   `rowElimination<<<>>>`:这是最后一次内核调用，在 GPU 上并行进行行消除。\n\n下面的代码片段显示了在数据被复制到 GPU 后迭代调用的三个内核:\n\n```cpp\n<Copy input augmented matrix AB to GPU>\n...\nfor( int n = 0; n < N; n++ ){\n// M: number of equations, N: number of unknowns\n    findPivotRowAndMultipliers<<<...>>>(); \n    extractPivotRow<<<...>>>(); \n    rowElimination<<<...>>>(); \n\n}\n```\n\n本章的重点是如何增强单个图形处理器的实现以支持多个图形处理器。然而，为了填补 GPU 实现中缺失的部分，我们需要对单个 GPU 实现进行一些优化更改:\n\n*   高斯消除算法的性能受到内存访问模式的严重影响。基本上，这取决于 AB 矩阵是如何存储的:\n    *   查找数据透视表行更喜欢以列为主的格式，因为如果矩阵以列为主的格式存储，它会提供合并访问。\n    *   另一方面，提取数据透视表行更喜欢以行为主的格式。\n*   无论我们如何存储 *AB* 矩阵，一个合并和一个交错/非合并的内存访问都是不可避免的。\n*   列主格式也有利于行消除核，因此，对于我们的高斯消除核，我们决定存储 AB 矩阵的转置，而不是 AB。AB 矩阵被转置一次，在`transposeMatrixAB()`函数的代码开始处。\n\n在下一节中，我们将启用多 GPU P2P 访问，并将工作分配给多个 GPU。\n\n# 点对点\n\nGPUDirect 技术的创建是为了允许不同节点内和跨节点的 GPU 之间进行高带宽、低延迟的通信。引入这项技术是为了消除一个图形处理器需要与另一个图形处理器通信时的 CPU 开销。GPUDirect 可以分为以下几大类:\n\n*   **GPU 之间的对等(P2P)传输**:允许 CUDA 程序使用高速**直接内存传输** ( **DMA** )在同一系统的两个 GPU 之间复制数据。它还允许对同一系统中其他图形处理器的内存进行优化访问。\n*   **网络和存储之间的加速通信**:这项技术有助于从第三方设备(如 InfiniBand 网络适配器或存储)直接访问 CUDA 内存。它消除了不必要的内存拷贝和 CPU 开销，因此减少了传输和访问的延迟。从 CUDA 3.1 开始支持该功能。\n*   **视频的 GPUDirect】:这项技术优化了基于帧的视频设备的流水线。它允许与 OpenGL、DirectX 或 CUDA 进行低延迟通信，并且从 CUDA 4.2 开始支持。**\n*   **远程直接内存访问(RDMA)** :该功能允许集群中的 GPU 之间直接通信。CUDA 5.0 及更高版本支持此功能。\n\n在这一节中，我们将转换我们的顺序代码，以利用 GPUDirect 的 P2P 特性，以便它可以在同一系统中的多个 GPU 上运行。\n\nGPUDirect P2P 功能允许以下内容:\n\n*   **GPU 直接传输** : `cudaMemcpy()`启动从 GPU 1 的内存到 GPU 2 的内存的 DMA 复制。\n*   **直接访问** : GPU 1 可以读写 GPU 2 的内存(加载/存储)。\n\n下图展示了这些功能:\n\n![](img/937e7aae-d492-4615-a17f-8e2a3e2f48bc.png)\n\n要了解 P2P 的优势，就要了解 PCIe 总线规范。这是为了通过互连(如 InfiniBand)与其他节点进行最佳通信而创建的。当我们希望以最佳方式发送和接收来自单个图形处理器的数据时，情况就不同了。以下是一个 PCIe 拓扑示例，其中八个图形处理器连接到不同的中央处理器和网卡/无限带宽卡:\n\n![](img/33fbb94b-f6e0-4951-8285-c0e03c6c8bf4.png)\n\n在上图中，GPU0 和 GPU1 之间允许 P2P 传输，因为它们都位于同一个 PCIe 交换机中。但是，GPU0 和 GPU4 无法执行 P2P 传输，因为两个**输入/输出集线器** ( **IOHs** )之间不支持 PCIe P2P 通信。对于远程对等 MMIO 事务，IOH 不支持来自 PCI Express 的非连续字节。连接两个处理器的 QPI 链路的性质确保了如果两个处理器驻留在不同的 PCIe 域上，则不可能在两个处理器之间进行直接的 P2P 复制。因此，从 GPU0 的内存复制到 GPU4 的内存需要通过 PCIe 链路复制到连接到 CPU0 的内存，然后通过 QPI 链路将其传输到 CPU1，并通过 PCIe 再次传输到 GPU4。可以想象，这个过程在延迟和带宽方面都增加了大量的开销。\n\n下图显示了另一个系统，其中 GPU 通过支持 P2P 传输的 NVLink 互连相互连接:\n\n![](img/88e4972a-9fab-4b7c-9643-8b4b16c550fb.png)\n\n上图显示了一个示例 NVLink 拓扑，它产生了一个八立方体网格，其中每个 GPU 连接到另一个 GPU，最大跳数为 1。\n\n更重要的问题是，*我们如何知道这种拓扑以及哪些图形处理器支持 P2P 传输？*好在有工具可以做到这一点。`nvidia-smi`就是这样一个工具，作为 NVIDIA 驱动程序安装的一部分进行安装。下面的截图显示了在网络拓扑如上图所示的英伟达 DGX 服务器上运行`nvidia-smi`的输出:\n\n![](img/806e629d-8aa4-4dc4-a453-5f803bb512e5.jpg)\n\n前面的截图代表了在 DGX 系统上运行`nvidia-smi topo -m`命令的结果，该系统有 8 个图形处理器。如您所见，任何通过 SMP 互连(`QPI` / `UPI`)连接到另一个 GPU 的 GPU 都无法执行 P2P 传输。比如`GPU0`就无法和`GPU5`、`GPU6`、`GPU7`做 P2P。另一种方法是通过 CUDA APIs 来解决这个传输问题，我们将在下一节中使用它来转换我们的代码。\n\n现在我们已经了解了系统拓扑，我们可以开始将我们的应用转换为单个节点/服务器中的多个 GPU。\n\n# 单节点–多 GPU 高斯消除\n\n准备你的多图形处理器应用。这段代码可以在本书的 GitHub 库中的`06_multigpu/gaussian`找到。使用`nvcc`编译器编译您的应用，如下所示:\n\n```cpp\n$ nvcc -o gaussian_multi_gpu_p2p.out gaussian_multi_gpu_p2p.cu\n$ time ./gaussian_multi_gpu_p2p.out\n```\n\n从单 GPU 实现到多 GPU 实现，我们在前面小节中定义的三个内核将按原样使用。然而，线性系统被分成与图形处理器数量相等的多个部分。这些部分分配给每个 GPU 一个部分。每个图形处理器负责对分配给该图形处理器的部件执行操作。矩阵是按列拆分的。这意味着每个图形处理器从所有行中获得相等数量的连续列。寻找枢轴的内核在保存包含枢轴元素的列的 GPU 上启动。透视元素的行索引被广播给其他图形处理器。提取的枢轴行和行消除内核在所有图形处理器上启动，每个图形处理器在矩阵中自己的部分工作。下图显示了在多个 GPU 之间拆分的行，以及如何将透视行广播给其余进程:\n\n![](img/97345168-60cd-4663-9836-2f391027e26d.png)\n\n上图显示了多个图形处理器之间的分工。目前，透视行属于 **GPU1** ，负责向其他 GPU 广播透视行。\n\n让我们尝试理解这些代码变化，以及用于启用 P2P 功能的 CUDA 应用编程接口:\n\n1.  在支持的图形处理器之间启用 P2P 访问。下面的代码显示了在 GPU 之间启用 P2P 访问的第一步:\n\n```cpp\nfor( int i = 0; i < nGPUs; i++ ){   \n    // setup P2P \n    cudaSetDevice(i);   \n    for( int j = 0; j < nGPUs; j++ ) {      \n        if (i == j) continue;      \n        cudaDeviceCanAccessPeer(&canAccessPeer, i, j);\n        if (canAccessPeer)      \n            cudaDeviceEnablePeerAccess(j, 0);    \n    } \n}\n```\n\n前面代码中使用的关键 API 如下:\n\n2.  将内容拆分并传输到相应的图形处理器:\n\n```cpp\nfor( int g = 0; g < nGPUs; g++ ){       \n    cudaSetDevice(g);       \n    //Copy  part ‘g’ of ABT to GPU ‘g’; \n}\n```\n\n前面代码中使用的关键应用编程接口是`cudaSetDevice()`。这将当前上下文设置为作为参数传递的图形处理器标识。\n\n3.  找到轴心行并通过 P2P 广播:\n\n```cpp\nfor( int n = 0; n < N; n++ ){        \n    gp = GPU that holds n;        \n    cudaSetDevice(gp);        \n    findPivotRowAndMultipliers<<<...>>>();\n    for( int g = 0; g < nGPUs; g++ ){ \n        if (g == gp) continue;\n        cudaMemcpyPeer(pivotDatag, g, pivotDatagp, gp, numBytes);\n     }  ... \n```\n\n用于向图形处理器广播传输的应用编程接口是`cudaMemcpyPeer()`。\n\n4.  提取透视行并执行行消除:\n\n```cpp\nfor( int n = 0; n < N; n++ ){\n    ...\n    for( int g = 0; g < nGPUs; g++ ){  \n        cudaSetDevice(g); \n        extractPivotRow<<<...>>>(); \n        rowElimination<<<...>>>();   \n    }  \n}  \n```\n\n如您所见，我们仍然在重用相同的内核。唯一不同的是，我们使用`cudaSetDevice()` API 来告诉 CUDA 运行时内核应该在哪个 GPU 上启动。请注意`cudaSetDevice()`是一个昂贵的调用，尤其是在老一代图形处理器上。因此，建议您利用`OpenMP` / `OpenACC`或 CPU 上的任何其他线程机制在 CPU 上并行调用`nGPUs`的 for 循环。\n\n5.  从各自的中央处理器复制回数据:\n\n```cpp\nfor( int g = 0; g < nGPUs; g++ ){ \n    cudaSetDevice(g);  \n    Copy  part ‘g’ of reduced ABT from GPU ‘g’ to Host; \n}\n```\n\n这五个步骤完成了将单个 GPU 实现转换为单个节点上的多个 GPU 的练习。\n\n作为 CUDA 安装的一部分发货的 CUDA 示例包括一些测试 P2P 带宽性能的示例代码。可以在`samples/1_Utilities/p2pBandwidthLatencyTest`文件夹中找到。建议您在系统上运行此应用，以便了解系统的 P2P 带宽和延迟。\n\n现在我们已经在单个节点上实现了多 GPU 实现，我们将改变策略，在多个 GPU 上运行这段代码。但是在将我们的代码转换成多个 GPU 之前，我们将提供一个简短的 MPI 编程入门，主要用于节间通信。\n\n# MPI 简介\n\n**消息传递接口** ( **MPI** )标准是一个消息传递库标准，已经成为在 HPC 平台上编写消息传递程序的行业标准。基本上，MPI 用于跨多个 MPI 进程的消息传递。相互通信的 MPI 进程可以驻留在同一个节点上，也可以跨多个节点。\n\n以下是 Hello World MPI 程序的示例:\n\n```cpp\n#include <mpi.h> \nint main(int argc, char *argv[]) {     \n    int rank,size;     \n    /* Initialize the MPI library */     \n    MPI_Init(&argc,&argv);     \n    /* Determine the calling process rank and total number of ranks */\n    MPI_Comm_rank(MPI_COMM_WORLD,&rank);     \n    MPI_Comm_size(MPI_COMM_WORLD,&size);     \n    /* Compute based on process rank */     \n    /* Call MPI routines like MPI_Send, MPI_Recv, ... */     \n    ...     \n    /* Shutdown MPI library */     \n    MPI_Finalize();     \n    return 0; \n}\n```\n\n如您所见，MPI 程序中涉及的一般步骤如下:\n\n1.  我们包含头文件`mpi.h`，它包含了所有 MPI API 调用的声明。\n2.  我们通过调用`MPI_Init`并向其传递可执行参数来初始化 MPI 环境。在此语句之后，创建多个 MPI 等级，并开始并行执行。\n3.  所有 MPI 进程并行工作，并使用消息传递 API(如`MPI_Send()`、`MPI_Recv()`等)相互通信。\n4.  最后，我们通过调用`MPI_Finalize()`来终止 MPI 环境。\n\n我们可以使用不同的 MPI 实现库(如 OpenMPI、MVPICH、英特尔 MPI 等)来编译这段代码:\n\n```cpp\n$ mpicc -o helloWorldMPI helloWorldMPI.c\n$ mpirun -n 4 --hostfile hostsList ./helloWorldMPI\n```\n\n我们正在利用`mpicc`编译器来编译我们的代码。`mpicc`基本上是一个包装脚本，它在内部扩展编译指令，以包括相关库和头文件的路径。此外，运行一个 MPI 可执行文件需要将其作为参数传递给`mpirun`。`mpirun`是一个包装器，帮助跨应用应该执行的多个节点设置环境。`-n 4`参数表示我们希望运行四个进程，这些进程将在主机名存储在文件主机列表中的节点上运行。\n\n在这一章中，我们的目标是将 GPU 内核与 MPI 集成，使其跨多个 MPI 进程运行。然而，我们不会涉及 MPI 编程的细节。不熟悉 MPI 编程的朋友应该先看看[https://computing.llnl.gov/tutorials/mpi/](https://computing.llnl.gov/tutorials/mpi/)了解一下分布式并行编程，然后再进入下一节。\n\n# GPUDirect RDMA\n\n在集群环境中，我们希望跨多个节点使用图形处理器。我们将允许我们的并行求解器将 CUDA 代码与 MPI 集成，以在多节点、多 GPU 系统上利用多级并行。一个 CUDA 感知的 MPI 被用来利用 GPUDirect RDMA 来优化节点间的通信。\n\nGPUDirect RDMA 允许跨集群的 GPU 之间直接通信。它最初是由 CUDA 5.0 用开普勒 GPU 卡支持的。在下图中，我们可以看到 GPUDirect RDMA，即**服务器 1** 中的 **GPU 2** 与【服务器 2】中的 **GPU 1** 直接通信:\n\n![](img/38bb750c-b2e8-44c6-9293-69582300c70e.png)\n\nGPUDirect RDMA 工作的唯一理论要求是**网卡**和 **GPU** 共享同一个根联合体。图形处理器和第三方设备(如网络适配器)之间的路径决定了是否支持 RDMA。让我们重温一下我们在上一节中运行的 DGX 系统上的`nvidia-smi topo -m`命令的输出:\n\n![](img/5cca0b09-e6e3-48c2-8d73-d4603bfc5ba7.png)\n\n如果我们看一下`GPU4`行，它显示`GPU4`到`mlx5_2`的连接类型是`PIX`(通过 PCIe 开关遍历)。我们还可以看到`GPU4`到`mlx_5_0`的连接类型是`SYS`(穿越经过`QPI`)。这意味着`GPU4`可以通过 Mellanox InfiniBand 适配器`mlx_5_2`执行 RDMA 传输，但如果传输需要从`mlx_5_0`开始，则不能执行，因为`QPI`不允许使用 RDMA 协议。\n\n# 支持 CUDA 的 MPI\n\n所有最新版本的 MPI 库都支持 GPUDirect 功能。支持 NVIDIA GPUDirect 和**统一虚拟寻址** ( **UVA** )的 MPI 库支持以下功能:\n\n*   MPI 可以传输应用编程接口，将数据直接复制到图形处理器内存(RDMA)。\n*   MPI 库还可以区分设备内存和主机内存，而无需用户的任何提示，因此它对 MPI 程序员来说是透明的。\n*   随着跨多个 MPI 级别的数据传输需要更改的应用代码越来越少，程序员的工作效率也随之提高。\n\n正如我们前面提到的，CPU 内存和 GPU 内存是不同的。没有 CUDA 感知的 MPI，开发人员只能将指向 CPU/主机内存的指针传递给 MPI 调用。以下代码是使用不支持 CUDA 的 MPI 调用的示例:\n\n```cpp\n //MPI rank 0:Passing s_buf residing in GPU memory \n // requires it to be transferred to CPU memory\ncudaMemcpy(s_buf_h,s_buf_d,size,cudaMemcpyDeviceToHost);\nMPI_Send(s_buf_h,size,MPI_CHAR,1,100,MPI_COMM_WORLD);\n\n//MPI rank 1: r_buf received buffer needs to be \n// transferred to GPU memory before being used in GPU\nMPI_Recv(r_buf_h,size,MPI_CHAR,0,100,MPI_COMM_WORLD, &status);\ncudaMemcpy(r_buf_d,r_buf_h,size,cudaMemcpyHostToDevice);\n```\n\n有了一个 CUDA 感知的 MPI 库，这就不是必须的了；GPU 缓冲区可以直接传递给 MPI，如下面的代码所示:\n\n```cpp\n//MPI rank 0\nMPI_Send(s_buf_d,size,MPI_CHAR,1,100,MPI_COMM_WORLD);\n\n//MPI rank n-1\nMPI_Recv(r_buf_d,size,MPI_CHAR,0,100,MPI_COMM_WORLD, &status);\n```\n\n例如，对于开放 MPI，在开放 MPI 1.7 系列和更高版本中存在 CUDA 感知支持。要启用此功能，需要在编译时使用 CUDA 支持配置开放 MPI 库，如下所示:\n\n```cpp\n$ ./configure --with-cuda\n```\n\n拥有一个支持 CUDA 的 MPI 并不意味着总是使用 GPUDirect RDMA。如果数据传输发生在共享同一根联合体的网卡和 GPU 之间，则使用 GPUDirect 功能。尽管如此，即使没有启用 RDMA 支持，拥有一个支持 CUDA 的 MPI 也能通过利用消息传输等功能提高应用的效率，如下图所示:\n\n![](img/d0de7272-eae9-4914-aee2-400f73f125bf.png)\n\n上图显示了带有 GPUDirect 的支持 CUDA 的 MPI 和不带 GPUDirect 的支持 CUDA 的 MPI。这两个调用都来自于 CUDA 感知 MPI，但是左侧是带 GPUDirect 传输的，右侧是不带 GPUDirect 传输的。\n\n非 GPUDirect 传输有以下几个阶段:\n\n*   节点 1:从 GPU1 到主机内存的传输\n*   节点 1:从主机内存传输到网络适配器暂存区\n*   网络:通过网络传输\n*   节点 2:从网络临时区域传输到主机内存\n*   节点 2:从主机内存到 GPU 内存的传输\n\n如果支持 GPUDirect RDMA，那么来自 GPU 的传输将直接通过网络进行，涉及主机内存的额外拷贝将全部删除。\n\n既然我们已经掌握了这个概念，让我们开始转换代码，使用 CUDA 感知的 MPI 编程来支持多 GPU。\n\n# 多节点–多 GPU 高斯消除\n\n准备好你的 GPU 应用。这段代码可以在本书的 GitHub 存储库中的`06_multigpu/gaussian`找到。使用`nvcc`编译器编译并运行应用，如下所示:\n\n```cpp\n$ mpicc-o gaussian_multi_gpu_rdma.out gaussian_multi_gpu_rdma.cu\n$ mpirun -np 8 ./gaussian_multi_gpu_rdma.out\n```\n\n我们用`mpicc`代替`nvcc`来编译 MPI 程序。我们使用`mpirun`命令运行可执行文件，而不是直接运行编译后的可执行文件。您将在本节中看到的结果是在 DGX 系统上运行的输出，在同一系统上有 8 V100。我们利用最大 8 个 MPI 进程，因为我们为每个 GPU 映射了 1 个 MPI 进程。要了解如何将多个 MPI 进程映射到同一个 GPU，请阅读本章后面的 *MPS* 小节。在本练习中，我们使用了 Open MPI 1.10，如前一节所述，它已经过编译以支持 CUDA。\n\n多图形处理器实现中涉及的步骤如下:\n\n1.  MPI 过程的秩 0 为线性系统(矩阵 A，B)生成数据。\n2.  转置后的增广矩阵(AB <sup>T</sup> )通过使用`MPI_Scatterv()`在 MPI 进程之间逐行拆分。\n\n3.  每个 MPI 进程并行计算其输入部分:\n    *   处理这三个内核发生在图形处理器上。\n    *   使用`MPI_Send()` / `Recv()`进行`findPivot`操作后，达成枢轴的一致。\n4.  使用`MPI_Gatherv()`将简化的**转置增强矩阵** ( **ABT** )聚集在根部。\n5.  根执行反向替换来计算解决方案 x。\n\n展示前面代码的提取的高斯代码示例如下:\n\n```cpp\nvoid gaussianEliminationOnGPU() {\n    cudaSetDevice(nodeLocalRank); //Set CUDA Device based on local rank\n    //Copy  chuck of AB Transpose from Host to GPU; \n   for( int n = 0; n < N; n++ ){ \n       prank = MPI rank that holds n; \n       if (myRank == prank) \n           findPivotRowAndMultipliers<<<...>>>(); \n       bCastPivotInfo(); // from prank to other ranks \n       extractPivotRow<<<...>>>(); \n       rowElimination<<<...>>>(); \n   //Copy  myPartOfReducedTransposeAB from GPU to Host;\n}\n```\n\n现在，让我们添加多图形处理器支持:\n\n1.  **按 MPI 等级设置 CUDA 设备** : 在 Open MPI 中，利用`MPI_COMM_TYPE_SHARED`作为`MPI_Comm_split_type`的参数，可以得到 MPI 进程的本地等级，如下代码所示:\n\n```cpp\nMPI_Comm loc_comm;\nMPI_Comm_split_type(MPI_COMM_WORLD, MPI_COMM_TYPE_SHARED, rank, MPI_INFO_NULL, &loc_comm);\nint local_rank = -1;\nMPI_Comm_rank(loc_comm,&local_rank);\nMPI_Comm_free(&loc_comm);\n```\n\n现在我们有了本地排名，每个 MPI 进程都用它来通过`cudaSetDevice()`设置当前 GPU，如下图所示:\n\n![](img/316a4f63-8ca7-4540-a2c2-47b2dcc0e122.png)\n\n2.  使用`MPI_Scatter`将输入拆分并分配给不同的 MPI 流程:\n\n```cpp\nvoid distributeInputs() {\n    MPI_Scatterv(transposeAB, ..., myPartOfTransposeAB, recvCount, MPI_UNSIGNED, 0, MPI_COMM_WORLD); \n} \n```\n\n3.  在图形处理器上执行高斯消除:\n\n```cpp\nvoid gaussianEliminationOnGPU() { \n    cudaSetDevice(nodeLocalRank);\n     for( int n = 0; n < N; n++ ){ \n        prank = MPI rank that holds n; \n        if (myRank == prank) \n            findPivotRowAndMultipliers<<<...>>>();\n        MPI_Bcast(...); // from prank to other ranks \n        extractPivotRow<<<...>>>(); \n        rowElimination<<<...>>>(); \n}\n```\n\n在执行任何操作之前，当前图形处理器是基于本地等级设置的。然后，由负责该行的进程提取数据透视表行，然后将数据透视表行广播给所有其他 MPI 等级，我们使用这些等级进行消除。\n\n通过使用异步 MPI 调用，而不是使用`MPI_Bcast`等广播 API，可以提高传输时间的整体性能。事实上，不鼓励使用广播应用编程接口；应该换成`MPI_Isend`和`MPI_Irecv`，这是可以实现相同功能的异步版本。请注意，使调用异步会增加调试等其他方面的复杂性。因此，用户需要编写额外的代码来发送和接收数据。\n\nThis chapter provides the best coding practices when it comes to adding GPU support to an existing MPI program and should not be considered an expert guide on the best programming practices for MPI programming.\n\n# CUDA 流\n\n流以先进先出的方式运行，其中操作序列按照它们被发出的顺序执行。主机代码发出的请求被放入先进先出队列。队列由驱动程序异步读取和处理，设备驱动程序确保队列中的命令按顺序处理。例如，内存拷贝在内核启动前结束，等等。\n\n使用多个流的一般思想是，在不同流中激发的 CUDA 操作可能会并发运行。这可能导致多个内核在内核执行中重叠或重叠内存副本。\n\n为了理解 CUDA 流，我们将研究两个应用。第一个应用是一个简单的矢量加法代码，添加了流，因此它可以将数据传输与内核执行重叠。第二个应用是图像合并应用，也将用于[第 9 章](09.html)、*使用 OpenACC* 进行 GPU 编程。\n\n首先，根据以下步骤配置您的环境:\n\n1.  准备好你的 GPU 应用。例如，我们将合并两幅图像。这段代码可以在本书 GitHub 储存库中的`06_multi-gpu/streams`文件夹中找到。\n2.  使用`nvcc`编译器编译您的应用，如下所示:\n\n```cpp\n$ nvcc --default-stream per-thread -o vector_addition -Xcompiler -fopenmp -lgomp vector_addition.cu\n$ nvcc --default-stream per-thread -o merging_muli_gpu -Xcompiler -fopenmp -lgomp scrImagePgmPpmPackage.cu image_merging.cu\n$ ./vector addition\n$ ./merging_muli_gpu\n```\n\n前面的命令将创建两个名为`vector_addition`和`merging_multi_gpu`的二进制文件。正如您可能已经观察到的，我们在代码中使用了额外的参数。让我们更详细地了解它们:\n\n*   `--default-stream per-thread`:这个标志告诉编译器解析代码中提供的 OpenACC 指令。\n*   `-Xcompiler -fopenmp -lgomp`:这个标志告诉`nvcc`把这些额外的标志传递给下面的 CPU 编译器，编译代码的 CPU 部分。在这种情况下，我们要求编译器向我们的应用添加 OpenMP 相关的库。\n\n我们将把这一部分分成两部分。应用 1 和应用 2 分别演示了在单个和多个图形处理器中使用流。\n\n# 应用 1–使用多个流将数据传输与内核执行重叠\n\n我们需要遵循以下步骤来将数据传输与内核执行重叠，或者同时启动多个内核:\n\n1.  声明要固定的主机内存，如下面的代码片段所示:\n\n```cpp\ncudaMallocHost(&hostInput1, inputLength*sizeof(float));\ncudaMallocHost(&hostInput2, inputLength*sizeof(float));\ncudaMallocHost(&hostOutput, inputLength*sizeof(float));\n```\n\n这里，我们使用`cudaMallocHost()`应用编程接口来分配向量作为固定内存。\n\n2.  创建一个`Stream`对象，如下面的代码片段所示:\n\n```cpp\nfor (i = 0; i < 4; i++) {\n cudaStreamCreateWithFlags(&stream[i],cudaStreamNonBlocking);\n```\n\n这里，我们利用`cudaStreamCreateWithFlags()` API，传递`cudaStreamNonBlocking`作为标志，使这个流不阻塞。\n\n3.  用`stream`标志调用 CUDA 内核和内存副本，如下面的代码片段所示:\n\n```cpp\nfor (i = 0; i < inputLength; i += Seglen * 4) {\n    for (k = 0; k < 4; k++) {\n        cudaMemcpyAsync(... , cudaMemcpyHostToDevice, stream[k]);\n        cudaMemcpyAsync(... , cudaMemcpyHostToDevice, stream[k]);\n        vecAdd<<<Gridlen, 256, 0, stream[k]>>>(...);\n    }\n}\n```\n\n正如我们所看到的，我们不是通过复制整个数组一次来一次性执行向量加法，而是将数组分成段并异步复制这些段。内核执行也是在各自的流中异步完成的。\n\n当我们通过 Visual Profiler 运行这段代码时，我们可以看到以下特征:\n\n![](img/ccb79f18-189a-4f8c-84dc-6af4ada0e01f.png)\n\n前面的 profiler 截图显示蓝色条(基本上是`vector_addition`内核)与内存副本重叠。因为我们在代码中创建了四个流，所以在分析器中也有四个流。\n\n每个 GPU 都有两个内存复制引擎。一个负责主机到设备的传输，另一个负责设备到主机的传输。因此，发生在相反方向的两个内存副本可以重叠。此外，内存副本可以与计算内核重叠。这会导致 *n* 路并发，如下图所示:\n\n![](img/c4eedad6-44a2-4498-8647-246244ec6999.png)\n\n每个 GPU 架构都有特定的约束和规则，基于这些约束和规则，我们将在执行时看到这些重叠。总的来说，以下是一些指导原则:\n\n*   CUDA 操作必须在不同的非 0 流中。\n*   `cudaMemcpyAsync`用主机应使用`cudaMallocHost()`或`cudaHostAlloc()`进行固定。\n    *   必须有足够的资源\n    *   `cudaMemcpyAsyncs`在不同的方向\n    *   启动多个并发内核的设备资源(SMEM、寄存器、块等)\n\n# 应用 2–使用多个流在多个设备上运行内核\n\n为了在多个设备上运行内核和重叠内存传输，我们之前遵循的步骤保持不变，除了一个额外的步骤:设置 CUDA 设备来创建流。让我们看看以下步骤:\n\n1.  创建与系统中 CUDA 设备数量相等的流，如以下代码片段所示:\n\n```cpp\ncudaGetDeviceCount(&noDevices);\ncudaStream_t *streams;\nstreams = (cudaStream_t*) malloc(sizeof(cudaStream_t) * noDevices);\n```\n\n我们利用`cudaGetDeviceCount()` API 获取 CUDA 设备的数量。\n\n2.  在相应的设备中创建流，如下面的代码片段所示:\n\n```cpp\n#pragma omp parallel num_threads(noDevices)\n{\n     int block = omp_get_thread_num();\n    cudaSetDevice(block);\n    cudaStreamCreate(&streams[block]);\n```\n\n我们正在启动与 CUDA 设备数量相等的 OpenMP 线程，这样每个 CPU 线程都可以为各自的设备创建自己的 CUDA 流。每个中央处理器线程执行`cudaSetDevice()`根据其标识设置当前图形处理器，然后为该设备创建流。\n\n3.  在该流中启动内核和内存副本，如下所示:\n\n```cpp\ncudaMemcpyAsync(... cudaMemcpyHostToDevice,streams[block]);\ncudaMemcpyAsync(..., cudaMemcpyHostToDevice, streams[block]);\nmerging_kernel<<<gridDim,blockDim,0,streams[block]>>>(...);\ncudaMemcpyAsync(...,streams[block]); \n```\n\n在探查器中运行代码后的输出可以在下面的屏幕截图中看到，它代表了可视化探查器的时间线视图。这显示了一个 GPU 的内存副本与另一个 GPU 的内核执行重叠:\n\n![](img/dfdd5f05-a117-4032-8736-123f1f2e9cc3.png)\n\n如您所见，我们在四个 V100s 的多 GPU 系统上运行了这段代码。不同图形处理器中的内存副本和内核相互重叠。在这段代码中，我们演示了如何利用 OpenMP 在不同的设备上并行调用 CUDA 内核。这也可以通过利用 MPI 来启动利用不同 GPU 的多个进程来实现。\n\n在下一节中，我们将了解一些其他主题，这些主题可以提高多 GPU 应用的性能，并帮助开发人员分析和调试他们的代码。\n\n# 其他技巧\n\n在本节中，我们将涵盖一些其他主题，这些主题将有助于我们理解多 GPU 系统的其他特性。\n\n# 使用 InfiniBand 网卡对现有系统进行基准测试\n\n不同的基准可用于测试 RDMA 功能。InfiniBand 适配器的一个这样的基准可以在[https://www.openfabrics.org/](https://www.openfabrics.org/)找到。您可以通过执行以下代码来测试带宽:\n\n```cpp\n$ git clone git://git.openfabrics.org/~grockah/perftest.git\n$ cd perftest \n$ ./autogen.sh \n$ export CUDA_H_PATH=<<Path to cuda.h>> \n$ ./configure –prefix=$HOME/test \n$ make all install\n```\n\n然后，您可以运行以下命令来测试带宽:\n\n```cpp\nFor example host to GPU memory (H-G) BW test:\nserver$ ~/test/bin/ib_write_bw -n 1000 -O -a --use_cuda\nclient $ ~/test/bin/ib_write_bw -n 1000 -O -a server.name.org\n\n//GPU to GPU memory (G-G) BW test:\nserver$ ~/test/bin/ib_write_bw -n 1000 -O -a --use_cuda\nclient $ ~/test/bin/ib_write_bw -n 1000 -O -a --use_cuda server.name.org\n```\n\n# 英伟达集体交流图书馆(NCCL)\n\nNCCL 提供了通信原语的实现，这些原语通常用于深度学习等领域。NCCL 1.0 从在同一个节点内的多个图形处理器之间实现通信原语开始，发展到支持多个节点中的多个图形处理器。NCCL 图书馆的一些主要特点包括:\n\n*   支持来自多线程和多个进程的调用\n*   支持多环和树形拓扑，以提高节点内和节点间的总线利用率\n*   支持 InfiniBand 节点间通信\n*   源码包可从 GitHub([https://github.com/nvidia/nccl](https://github.com/nvidia/nccl))免费下载\n\nNCCL 可以扩展到 24，000 个图形处理器，远低于 300 微秒的延迟。请注意，NCCL 已被证明是一个非常有用和方便的深度学习框架库，但在用于高性能计算应用时有其局限性，因为它不支持点对点通信。NCCL 支持集体操作，用于深度学习应用，例如:\n\n*   `AllReduce`\n*   `AllGather`\n*   `ReduceScatter`\n*   `Reduce`\n*   `Broadcast`\n\n所有 NCCL 调用都作为 CUDA 内核运行，以便更快地访问 GPU 内存。它利用了作为一个块实现的较少线程。这最终只在一个图形处理器 SM 上运行，因此不会影响其他图形处理器的利用率。让我们看看下面的代码:\n\n```cpp\nncclGroupStart(); \nfor (int i=0; i<ngpus; i++) \n{ \n    ncclAllGather(…, comms[i], streams[i]); \n} \nncclGroupEnd();\n```\n\n正如我们所看到的，NCCL 呼叫很简单，可以轻松呼叫。\n\n# 利用 NCCL 加速集体通信\n\n**NVIDIA 集体通信库** ( **NCCL** )为多个 NVIDIA GPUs 提供了性能优化的通信原语集体。在本节中，我们将了解这个库是如何工作的，以及我们如何从使用它中获益。\n\n不难找到使用多个 GPU 来训练网络的深度学习模型。由于两个图形处理器并行计算神经网络，我们可以很容易地想象这种技术将随着图形处理器数量的增加而提高训练性能。不幸的是，这个世界并没有那么简单。梯度应在多个图形处理器之间共享，一个图形处理器中的权重更新程序应等待其他图形处理器的梯度更新其权重。这是多 GPU 深度学习训练的一般流程，如下图所示:\n\n![](img/914037ea-4291-4208-a867-6281cff0b742.png)\n\n集体沟通有很多种类型:全缩减、广播、缩减、全聚集、缩减分散等等。在深度学习中，每个图形处理器收集另一个图形处理器的数据，同时将自己的数据传输到其他图形处理器。因此，我们可以确定深度学习在他们的交流中需要所有类型的减少风格的交流。\n\n在 HPC 社区，集体沟通，包括 all-reduce，是一个相当常见的话题。节点间和节点内处理器之间的通信是一个具有挑战性但又至关重要的问题，因为它直接关系到可扩展性。正如我们在[第 6 章](06.html)、*可扩展多 GPU 编程*中提到的，在*多 GPU 编程*部分，需要大量考虑与各 GPU 进行通信。开发人员应该在图形处理器中设计和实现集体通信，即使 MPI 已经支持这样的通信模式。\n\nNCCL 提供了这样一个知道图形处理器拓扑结构的集体。通过使用各种分组和通信命令，您可以应用所需的通信任务。\n\n一个先决条件是你的系统需要有一个以上的图形处理器，因为 NCCL 是一个通信库，可以与多个图形处理器一起工作。\n\n以下步骤涵盖了如何调用`ncclAllReduce()`作为测试并测量系统的 GPU 网络带宽。示例代码在`04_nccl`中实现:\n\n1.  让我们为每个 GPU 设备定义一个类型，该类型将包含发送和接收、一个缓冲区和`cudaStream`，如下所示:\n\n```cpp\ntypedef struct device\n{\n    float *d_send;\n    float *d_recv;\n    cudaStream_t stream;\n} device_t;\n```\n\n2.  在应用开始时，我们需要准备一些句柄，以便我们可以控制多个 GPU:\n\n```cpp\ncudaGetDeviceCount(&num_dev);\nncclComm_t *ls_comms = new ncclComm_t[num_dev];\nint *dev_ids = new int[num_dev];\nfor (int i = 0; i < num_dev; i++)\n    dev_ids[i] = i;\n```\n\n3.  然后，我们将创建一个缓冲区，假设我们有数据。对于每个设备，我们将初始化每个设备的项目，如下所示:\n\n```cpp\nunsigned long long size = 512 * 1024 * 1024; // 2 GB\n\n// allocate device buffers and initialize device handles\ndevice_t *ls_dev = new device_t[num_dev];\nfor (int i = 0; i < num_dev; i++) {\n    cudaSetDevice(i);\n    cudaMalloc((void**)&ls_dev[i].d_send, sizeof(float) * size);\n    cudaMalloc((void**)&ls_dev[i].d_recv, sizeof(float) * size);\n    cudaMemset(ls_dev[i].d_send, 0, sizeof(float) * size);\n    cudaMemset(ls_dev[i].d_recv, 0, sizeof(float) * size);\n    cudaStreamCreate(&ls_dev[i].stream);\n}\n```\n\n4.  在开始 NCCL 通信之前，我们需要初始化 GPU 设备，以便它们知道它们在整个 GPU 组中的排名。由于我们将使用单个进程测试带宽，因此我们可以安全地调用一个函数来初始化所有设备:\n\n```cpp\nncclCommInitAll(ls_comms, num_dev, dev_ids);\n```\n\n5.  如果我们用多个进程测试带宽，我们需要调用`ncclCommInitRank()`。我们需要提供计算进程标识和图形处理器等级的图形处理器标识。\n6.  现在，我们可以和 NCCL 一起完成全部还原操作。下面的代码是`ncclAllReduce`的一个示例实现:\n\n```cpp\nncclGroupStart();\nfor (int i = 0; i < num_dev; i++) {\n    ncclAllReduce((const void*)ls_dev[i].d_send, \n                  (void*)ls_dev[i].d_recv,\n        test_size, ncclFloat, ncclSum, \n        ls_comms[i], ls_dev[i].stream);\n}\nncclGroupEnd();\n```\n\n对于每个设备，我们需要触发流量。为此，我们需要启动和关闭 NCCL 集团的沟通。现在，我们已经实现了一些使用`ncclAllReduce()`的测试代码。让我们通过对我们的系统进行微观基准测试来了解 NCCL 是如何工作的。\n\n让我们在多 GPU 系统上测试这段代码，运行以下命令:\n\n```cpp\n$ nvcc -run -m64 -std=c++ 11 -I/usr/local/cuda/samples/common/inc -gencode arch=compute_70,code=sm_70 -lnccl -o nccl ./nccl.cu\n```\n\n下图显示了在 DGX 站使用四个 V100 32G 图形处理器测量的性能。蓝线表示基于 NVLink 的带宽，而橙线表示基于 PCIe 的带宽，它通过设置`NCCL_P2P_DISABLE=1 ./ncd`和关闭点对点 GPU 来实现:\n\n![](img/7576c92f-ae93-41d2-bd31-8943edd22b8c.png)\n\n该 NCCL 测试可能会受到系统配置的影响。这意味着结果可能会有所不同，具体取决于您系统的 GPU 拓扑。\n\n这显示了基于 PCI express 和基于 NVLINK 的全缩减性能之间的差异。我们可以用`nvprof`看到它的交流。以下截图显示了 NCCL 通过 NCCL 2.3.7 在 DGX 站的全缩减通信:\n\n![](img/8acf126f-830f-4d1a-bb3b-58ff6ab62223.png)\n\nNCCL 越来越快了。通过引入带有 NVLink 和 NVSwitch 的新 GPU 互连，我们在 NCCL 的体验正在增加，以至于我们可以实现可扩展的性能。\n\nThe following link provides a discussion about NCCL: [https://developer.nvidia.com/gtc/2019/video/S9656/video](https://developer.nvidia.com/gtc/2019/video/S9656/video).\n\n# 摘要\n\n在本章中，我们介绍了多 GPU 编程的不同方法。借助一个高斯消去的例子，我们看到了如何将单个 GPU 应用工作负载拆分到多个 GPU 中，首先是单个节点，然后是多个节点。我们看到了系统拓扑如何在利用 P2P 传输和 GPUDirect RDMA 等功能方面发挥重要作用。我们还看到了如何使用多个 CUDA 流来重叠多个图形处理器之间的通信和数据传输。我们还简要介绍了一些可以帮助 CUDA 程序员优化代码的其他主题，例如 MPS 和使用`nvprof`来分析多 GPU 应用。\n\n在下一章中，我们将研究大多数高性能计算应用中出现的常见模式，以及如何在图形处理器中实现它们。"
  },
  {
    "path": "docs/learn-cuda-prog/07.md",
    "content": "# 七、CUDA 中的并行编程模式\n\n在本章中，我们将介绍并行编程算法，这些算法将帮助您了解如何并行化不同的算法并优化 CUDA。我们将在本章中介绍的技术可以应用于各种问题，例如，我们在[第 3 章](03.html)、 *CUDA 线程编程*中看到的并行约简问题，该问题可用于设计神经网络操作中的高效 softmax 层。\n\n在本章中，我们将涵盖以下主题:\n\n*   矩阵乘法优化\n*   图像卷积\n*   前缀和\n*   打包和拆分\n*   全身手术\n*   利用动态并行在 CUDA 中快速排序\n*   基数排序\n*   直方图计算\n\n# 技术要求\n\n要完成本章，建议您使用比帕斯卡架构更晚的 NVIDIA GPU 卡。换句话说，你的图形处理器的计算能力应该等于或大于 60。如果您不确定您的图形处理器的架构，请访问英伟达图形处理器网站([https://developer.nvidia.com/cuda-gpus](https://developer.nvidia.com/cuda-gpus))并确认您的图形处理器的计算能力。\n\n本章中相同的代码已经用 CUDA 版本 10.1 开发和测试。一般来说，如果适用，建议使用最新的 CUDA 版本。\n\n# 矩阵乘法优化\n\n虽然我们在很多例子中使用了矩阵乘法代码，但我们没有调查运算是否优化。现在，让我们回顾一下它的操作，以及如何找到优化的机会。\n\n矩阵乘法是两个矩阵的一组点积运算。我们可以简单地将所有 CUDA 线程完成的操作并行化，以生成元素的点积。然而，这种操作在内存使用方面是低效的，因为从内存加载的数据不会被重用。为了证实我们的类比，让我们测量性能限制器。下图显示了使用恩西计算的特斯拉 V100 卡的图形处理器利用率:\n\n![](img/82f700d0-9e74-4188-b49c-dc58c1cd2baf.png)\n\n根据我们的性能限制分析，这个利用率可以归类为内存受限。因此，我们应该检查内存利用率以降低利用率。下面的屏幕截图显示了内存工作负载分析部分:\n\n![](img/2b579069-63f5-4a3c-bb71-e5ce6de69cb5.png)\n\n从这个分析中，我们可以看到 L2 缓存命中率低，最大带宽低。我们可以假设这是因为原始的矩阵乘法运算不重用加载的数据，正如我们前面提到的。这可以通过使用共享内存来解决，也就是说，重用加载的数据并减少全局内存使用。现在，让我们回顾一下矩阵乘法，以及我们如何优化它来使用内存空间小的共享内存。\n\n矩阵乘法是一组点积运算，具有一些小尺寸矩阵和输出的累积。小矩阵被称为瓦片，它们沿着输出矩阵映射到矩阵。每个图块将并行计算自己的输出。该操作可以通过以下步骤实现:\n\n1.  确定两个输入和输出矩阵的图块大小。\n2.  遍历输入图块及其方向(矩阵 A 向右，矩阵 B 向下)。\n3.  计算图块内的矩阵乘法。\n4.  继续第二步，直到瓷砖到达终点。\n5.  刷新输出。\n\n下图显示了平铺矩阵乘法的概念:\n\n![](img/320ce4a9-f221-4986-9909-716c76e4f6ad.png)\n\n在上图中，我们计算了一个矩阵乘法， *C = AB* 。我们从矩阵 A 和矩阵 b 计算一个较小的矩阵乘法，作为一个绿色的图块。然后，我们分别遍历输入图块位置。运算结果累加到前一个输出，生成矩阵乘法的输出。\n\n这个操作提供了一个优化的机会，因为我们可以用小问题分解大的矩阵操作，并把它放在小的内存空间中。在 CUDA 编程中，我们将小矩阵放在共享内存中，并减少全局内存访问。在我们的实现中，我们将使用 CUDA 线程块匹配切片。图块的位置将由其块索引决定，这是通过`tid_*`变量完成的。\n\n# 平铺方法的实现\n\n现在，让我们使用平铺方法实现一个优化的矩阵乘法。我们将重用之前在 [第三章](03.html)*CUDA 线程编程*中使用的矩阵乘法示例代码。优化后，我们将看看如何提高性能。按照以下步骤开始:\n\n1.  让我们创建一个内核函数，它将是矩阵乘法的优化版本。我们将在`sgemm`操作中命名内核函数`v2`。这个内核函数会计算![](img/7697ab42-f52b-40c7-971b-aaa358bf19e7.png)，所以我们要分别提供相关的参数。我们还将通过`M`、`N`和`K`传递矩阵大小信息:\n\n```cpp\n__global__ void sgemm_kernel_v2(const float *A, const float *B, float *C,\n    int M, int N, int K, float alpha, float beta) {}\n```\n\n2.  对于这个操作，我们将分别使用块索引和线程索引。如前所述，我们需要单独使用块索引来指定切片位置。我们将使用线程索引进行块级矩阵乘法。因此，我们需要创建 CUDA 索引参数，如下所示:\n\n```cpp\nint bid_x = blockIdx.x * blockDim.x;\nint bid_y = blockIdx.y * blockDim.y;\nint tid_x = threadIdx.x;\nint tid_y = threadIdx.y;\n```\n\n3.  之后，我们将使用共享内存作为切片，并使用本地寄存器保存输出值:\n\n```cpp\nfloat element_c = 0.f;\n__shared__ float s_tile_A[BLOCK_DIM][BLOCK_DIM];\n__shared__ float s_tile_B[BLOCK_DIM][BLOCK_DIM];\n```\n\n4.  然后，我们将编写一个控制图块位置的循环。下面是 for 循环代码，它根据循环的块大小来控制循环。请注意，循环大小是由`K`决定的，它考虑了块应该遍历多少次:\n\n```cpp\nfor (int k = 0; k < K; k += BLOCK_DIM)\n{\n   ... {step 5 and 6 will cover } ...\n}\n```\n\n5.  现在，我们将编写在第二个循环中提供数据的代码。正如我们之前讨论的，每个图块都有自己的移动方向，以及矩阵；平铺`A`遍历矩阵`A`的列，平铺`B`遍历矩阵`B`的行。我们根据*矩阵乘法优化*部分所示的图表放置它们。之后，我们应该将`__syncthreads()`放置在将数据从全局内存复制到共享内存之后，以避免来自上一次迭代的未更新数据:\n\n```cpp\n// Get sub-matrix from A\ns_tile_A[tid_y][tid_x] = A[ (bid_y + tid_y) * K + tid_x + k ];\n// Get sub-matrix from B \ns_tile_B[tid_y][tid_x] = B[ k * N + bid_x + tid_x ]; \n\n__syncthreads();\n```\n\n6.  然后，我们可以从瓷砖上写矩阵乘法代码。被称为`element_c`的局部变量将累加结果:\n\n```cpp\nfor (int e = 0; e < BLOCK_DIM; e++)\n    element_c += s_tile_A[tid_y][e] * s_tile_B[e][tid_x];\n```\n\n7.  我们将把结果写入全局内存。以下操作应在第二个循环结束后进行:\n\n```cpp\nC[(bid_y + tid_y) * N + (bid_x + tid_x)] = \\\n alpha * element_c + beta * C[(bid_y + tid_y) * N + (bid_x + tid_x)];\n```\n\n8.  现在，让我们回顾一下这种平铺方法如何有利于矩阵乘法运算。通过在我们的分片矩阵乘法中使用共享内存，我们可以期望通过使用输入数据来减少全局内存流量，从而提高性能。我们可以很容易地用轮廓结果来证实这一点:\n\n```cpp\n$ nvcc -m64 -I/usr/local/cuda/samples/common/inc -gencode arch=compute_70,code=sm_70 -o sgemm ./sgemm.cu \n$ nvprof ./sgemm \n\n        Type Time(%)    Time Calls      Avg      Min      Max Name\nGPU activities: 47.79% 9.9691ms     1 9.9691ms 9.9691ms 9.9691ms sgemm_kernel(...)\n 32.52% 6.7845ms     1 6.7845ms 6.7845ms 6.7845ms sgemm_kernel_v2(...)\n```\n\n9.  由于我们设计内核是为了重用输入数据，增加的块大小可能有助于提高性能。例如，考虑到翘曲大小和共享存储体的数量，32×32 的块大小可能是最佳的，以避免存储体冲突。我们可以很容易地获得它的实验结果使用轮廓:\n\n```cpp\n Type Time(%)    Time Calls      Avg       Min       Max Name\nGPU activities: 46.52% 8.1985ms     1 8.1985ms  8.1985ms  8.1985ms sgemm_kernel(...)\n 31.24% 5.4787ms     1 5.4787ms  5.4787ms  5.4787ms sgemm_kernel_v2(...)\n```\n\n如您所见，增加的图块大小有利于矩阵乘法运算的性能。现在，我们来分析一下它的性能。\n\n# 平铺方法的性能分析\n\n之前，我们研究了切片方法以及它如何实现良好的性能。让我们回顾一下切片方法解决了什么问题，并看看下一步我们可以采取什么步骤。覆盖这一部分通常是可选的，因为英伟达为`GEMM`(通用矩阵乘法的缩写)操作提供了 cuBLAS 和 CUTLASS 库，以提供优化的性能。\n\n下图显示了恩希特计算公司更新后的图形处理器利用率报告。下部配置文件的更新利用率输出是上部配置文件的结果:\n\n![](img/c864bc30-a77c-4fcf-8261-00d776982d81.png)\n\n由于两种资源的利用率都很高，我们应该检查每种资源的使用情况。首先，让我们回顾一下内存工作负载。下面的截图显示了更新后的结果:\n\n![](img/113b5a48-87e6-47cf-b505-348afeaffec6.png)\n\n从这个结果中，我们可以看到全局内存访问是通过最大化内存带宽和降低内存吞吐量来优化的。此外，L2 缓存命中率得到了提高。因此，我们的切片方法将矩阵乘法从全局内存转换为芯片级操作。\n\n然而，这并不意味着我们实现了最佳性能。从内存工作负载分析可以看出，内存管道太忙。这是由于我们共享内存的元素乘法。为了解决这个问题，我们需要在共享内存中重新映射数据。我们不会在这本书里讨论这个问题，但是你可以在这篇文章里了解到:https://github.com/NervanaSystems/maxas/wiki/SGEMM。\n\n正如我们前面所讨论的，cuBLAS 库显示了更快的性能。我们将在[第 8 章](08.html)、*用库和其他语言编程*的 *cuBLAS* 部分介绍它的用法。然而，在这个阶段理解切片方法是有用的，这样我们就可以理解图形处理器如何开始优化。\n\n# 盘旋\n\n卷积运算(或滤波)是许多应用中的另一种常见运算，尤其是在图像和信号处理以及深度学习中。虽然这个操作是基于来自输入和滤波器的顺序数据的乘积，但是我们有不同的矩阵乘法方法。\n\n# CUDA 中的卷积运算\n\n卷积运算由源数据和一个滤波器组成。过滤器也被称为内核。通过对输入数据应用过滤器，我们可以获得修改后的结果。下图显示了一个二维卷积:\n\n![](img/3992c61c-8920-487f-8b8a-f38031e6cd8e.png)\n\n在实现卷积运算时，我们需要考虑几个概念，即内核和填充。内核是我们想要应用于源数据的一组系数。这也称为过滤器。填充是源数据周围的额外虚拟空间，这样我们就可以将内核函数应用到边缘。当填充大小为 0 时，我们不允许过滤器超出源空间。但是，一般来说，填充大小是过滤器大小的一半。\n\n为了轻松开始，我们可以在设计内核函数时考虑以下几点:\n\n*   每个 CUDA 线程生成一个过滤输出。\n*   每个 CUDA 线程将滤波器的系数应用于数据。\n*   过滤器形状是一个箱式过滤器。\n\n在这些条件下，我们可以得到一个简单的卷积运算滤波器，如下所示:\n\n```cpp\n__global__ void\nconvolution_kernel_v1(float *d_output, float *d_input, float *d_filter, int num_row, int num_col, int filter_size)\n{\n    int idx_x = blockDim.x * blockIdx.x + threadIdx.x;\n    int idx_y = blockDim.y * blockIdx.y + threadIdx.y;\n\n    float result = 0.f;\n    // iterates over the every value in the filter\n    for (int filter_row = -filter_size / 2; \n         filter_row <= filter_size / 2; ++ filter_row)\n    {\n        for (int filter_col = -filter_size / 2; \n             filter_col <= filter_size / 2; ++ filter_col)\n        {\n            // Find the global position to apply the given filter\n            // clamp to boundary of the source\n            int image_row = min(max(idx_y + filter_row, 0), \n                                static_cast<int>(num_row - 1));\n            int image_col = min(max(idx_x + filter_col, 0), \n                                static_cast<int>(num_col - 1));\n\n            float image_value = static_cast<float>(\n                                d_input[image_row * num_col + \n                                image_col]);\n            float filter_value = d_filter[(filter_row + \n                                           filter_size / 2) * \n                                           filter_size \n                                           + filter_col + \n                                           filter_size / 2];\n\n            result += image_value * filter_value;\n        }\n    }\n\n    d_output[idx_y * num_col + idx_x] = result;\n}\n```\n\n这个内核函数为操作获取输入数据和过滤器，并且不重用所有的数据。考虑到内存低效对性能的影响，我们需要设计我们的内核代码，以便我们可以重用加载的数据。现在，让我们编写卷积的优化版本。\n\n# 优化策略\n\n首先，卷积滤波器是一个只读矩阵，由所有 CUDA 线程使用。在这种情况下，我们可以使用 CUDA 的恒定内存来利用其缓存操作和广播操作。\n\n在卷积实现设计中，我们使用平铺方法，每个平铺将生成映射位置的过滤输出。我们的切片设计有额外的空间来考虑卷积滤波器的大小，这为卷积运算提供了所需的数据。这个额外的空间叫做**填充**。下图显示了一个 6×6 尺寸的线程块和一个 3×3 尺寸的过滤器的例子。\n\n然后，我们需要在共享内存中为每个线程块分配一个 8×8 大小的区块，如下所示:\n\n![](img/519d0f1f-3b3b-48dd-8606-676e66d417da.png)\n\n当源的地址是无效的内存空间时，填充区域可以是输入数据，或者它们被零填充(零填充方法)。通过这样做，我们可以使图块替换输入全局内存，而不会对边界元素产生额外影响。为了填充图块，我们使用线程块大小迭代图块，并通过检查输入数据的边界条件来确定应该填充哪个值。我们的实现将输入数据设置为图块大小的倍数，以便边界条件与每个线程块图块的填充空间相匹配。将源数据映射到切片的简要示意图如下:\n\n![](img/93e820c5-8149-4d4c-adab-98325e1e6512.png)\n\n在这个设计中，我们需要进行四次迭代来填充图块。但是，这应该根据过滤器的大小进行更改。这样，填充图块的迭代次数由图块大小的天花板数量除以螺纹块大小决定。它的实现很简单，如下面的代码所示:\n\n```cpp\nfor (int row = 0; row <= tile_size / BLOCK_DIM; row++) {\n    for (int col = 0; col <= tile_size / BLOCK_DIM; col++) {\n        ... (filter update operation) ...\n    }\n}\n```\n\n现在，让我们使用共享内存作为箱式过滤器来实现优化的卷积运算。\n\n# 利用常数记忆优化滤波系数\n\n首先，我们将学习如何优化滤波器系数数据的使用。\n\n我们将制作`convolution_kernel()`的修改版本。让我们复制内核代码，并将其中一个重命名为`convolution_kernel_v2()`:\n\n1.  首先，我们将创建一个恒定的存储空间来存储滤波器系数。常量内存的大小是有限的，我们不能修改内核代码。然而，我们可以使用这个常数存储器，因为我们的卷积滤波器适合这种情况。我们可以这样使用恒定记忆:\n\n```cpp\n#define MAX_FILTER_LENGTH 128\n__constant__ float c_filter[MAX_FILTER_LENGTH * MAX_FILTER_LENGTH];\n```\n\n2.  然后，我们可以使用`cudaMemcpyToSymbol()`函数将卷积滤波器系数放入常量存储器中:\n\n```cpp\ncudaMemcpyToSymbol(c_filter, h_filter, filter_size * filter_size * sizeof(float));\n```\n\n3.  让我们切换过滤操作，这样我们就可以使用常量内存。整个内核实现如下。如您所见，只有一个变量的用法发生了变化:\n\n```cpp\n__global__ void\nconvolution_kernel_v2(float *d_output, float *d_input, float *d_filter, int num_row, int num_col, int filter_size)\n{\n    int idx_x = blockDim.x * blockIdx.x + threadIdx.x;\n    int idx_y = blockDim.y * blockIdx.y + threadIdx.y;\n\n    float result = 0.f;\n    for (int filter_row = -filter_size / 2; \n         filter_row <= filter_size / 2; ++ filter_row)\n    {\n        for (int filter_col = -filter_size / 2; \n             filter_col <= filter_size / 2; ++ filter_col)\n        {\n            int image_row = idx_y + filter_row;\n            int image_col = idx_x + filter_col;\n\n            float image_value = (image_row >= 0 \n                                 && image_row < num_row \n                                 && image_col >= 0\n                                 && image_col < num_col) ?\n                                 d_input[image_row * num_col \n                                         + image_col] : 0.f;\n            float filter_value = c_filter[(filter_row \n                                          + filter_size / 2) \n                                          * filter_size \n                                          + filter_col \n                                          + filter_size / 2];\n\n            result += image_value * filter_value;\n        }\n    }\n\n    d_output[idx_y * num_col + idx_x] = result;\n}\n```\n\n4.  现在，我们可以确认由于过滤数据重用而带来的性能提升`nvprof`:\n\n```cpp\n$ nvcc -run -m64 -I/usr/local/cuda/samples/common/inc -gencode arch=compute_70,code=sm_70 -o convolution ./convolution.cu\n$ nvprof ./convolution\n           Type Time(%) Time Calls Avg Min Max Name\n 12.85% 442.21us     1 442.21us 442.21us 442.21us convolution_kernel_v1(...)\n 11.97% 412.00us     1 412.00us 412.00us 412.00us convolution_kernel_v2(...)\n```\n\n从这个结果中，我们可以看到内核执行时间的减少。\n\n# 使用共享内存平铺输入数据\n\n现在，我们将使用共享内存优化输入数据的使用。为了区分我们的下一个优化步骤，让我们复制前面的卷积核函数，并将其命名为`convolution_kernel_v3()`:\n\n1.  首先，我们需要预先准备好共享内存空间，以便它可以存储输入数据。为了从共享内存中获得过滤操作的好处，我们需要额外的输入数据。为了创建足够的内存空间，我们需要修改内核调用，如下所示:\n\n```cpp\nint shared_mem_size = (2*filter_size+BLOCK_DIM) * (2*filter_size+BLOCK_DIM) * sizeof(float);\nconvolution_kernel_v3<<<dimGrid, dimBlock, shared_mem_size, 0 >>>(d_output, d_input, d_filter, num_row, num_col, filter_size);\n```\n\n2.  在内核代码中，我们可以如下声明共享内存空间:\n\n```cpp\nextern __shared__ float s_input[];\n```\n\n3.  然后，我们可以将输入数据复制到共享内存中，共享内存将由线程块计算。首先，让我们声明一些有助于控制内存操作的变量:\n\n```cpp\nint pad_size = filter_size / 2;\nint tile_size = BLOCK_DIM + 2 * pad_size;\n```\n\n4.  现在，我们可以按照前面讨论的切片设计将加载输入数据复制到共享内存中:\n\n```cpp\nfor (int row = 0; row <= tile_size / BLOCK_DIM; row++) {\n    for (int col = 0; col <= tile_size / BLOCK_DIM; col++) {\n        int idx_row = idx_y + BLOCK_DIM * row - pad_size; \n        // input data index row\n        int idx_col = idx_x + BLOCK_DIM * col - pad_size; \n        // input data index column\n        int fid_row = threadIdx.y + BLOCK_DIM * row; \n        // filter index row\n        int fid_col = threadIdx.x + BLOCK_DIM * col; \n        // filter index column\n\n        if (fid_row >= tile_size || fid_col >= tile_size) continue;\n\n        s_input[tile_size * fid_row + fid_col] = \\\n            (idx_row >= 0 && idx_row < num_row && idx_col >= 0 \n                && idx_col < num_col) ? \n                d_input[num_col * idx_row + idx_col] : 0.f;\n    }\n}\n\n__syncthreads();\n```\n\n5.  由于输入存储器已经改变，我们的卷积码应该更新。我们可以按如下方式编写卷积代码:\n\n```cpp\nfloat result = 0.f;\n    for (int filter_row = -filter_size / 2; \n         filter_row <= filter_size / 2; ++ filter_row)\n    {\n        for (int filter_col = -filter_size / 2; \n             filter_col <= filter_size / 2; ++ filter_col)\n        {\n            // Find the global position to apply the given filter \n            int image_row = threadIdx.y + pad_size + filter_row;\n            int image_col = threadIdx.x + pad_size + filter_col;\n\n            float image_value = s_input[tile_size \n                                        * image_row + image_col]; \n            float filter_value = c_filter[(filter_row \n                                          + filter_size / 2) \n                                          * filter_size \n                                          + filter_col \n                                          + filter_size / 2];\n\n            result += image_value * filter_value;\n        }\n    }\n```\n\n6.  最后，我们可以使用`nvprof`来测量性能增益。从结果中，我们可以确认我们的加速速度比最初的操作快了大约 35%:\n\n```cpp\n$ nvcc -run -m64 -I/usr/local/cuda/samples/common/inc -gencode arch=compute_70,code=sm_70 -o convolution ./convolution.cu\n$ nvprof ./convolution\nProcessing Time (1) -> GPU: 0.48 ms\nProcessing Time (2) -> GPU: 0.43 ms\nProcessing Time (3) -> GPU: 0.30 ms\nProcessing Time -> Host: 4104.51 ms\n... (profiler output) ...\n              type Time(%)    Time Calls .    Avg      Min .    Max Name\n   GPU activities: 66.85% 2.3007ms     3 766.91us 1.1840us 2.2979ms [CUDA memcpy HtoD]\n                   12.85% 442.21us     1 442.21us 442.21us 442.21us convolution_kernel_v1()\n                   11.97% 412.00us     1 412.00us 412.00us 412.00us convolution_kernel_v2()\n                    8.33% 286.56us     1 286.56us 286.56us 286.56us convolution_kernel_v3()\n```\n\n现在，我们已经介绍了如何利用加载的数据，以便我们可以在其他片内缓存而不是全局内存中重用它。我们将在下一节中更详细地讨论这一点。\n\n# 获得更多性能\n\n如果过滤器是对称过滤器或可分离过滤器，我们可以将箱式过滤器分解为两个过滤器:水平过滤器和垂直过滤器。使用两个方向过滤器，我们可以在共享内存使用方面有更多的优化:内存空间和内存利用率。如果你想了解更多这方面的内容，可以看看`3_Imaging/convolutionSeparable`目录中一个名为`convolutionSeparable`的 CUDA 样例。其详细说明也与`doc/convolutionSeparable.pdf`收录在同一目录中。\n\n# 前缀和(扫描)\n\n前缀和(扫描)用于从给定的输入数字数组中获取累积数字数组。例如，我们可以生成如下前缀和序列:\n\n| **输入数字** | one | Two | three | four | five | six | ... |\n| **前缀和** | one | three | six | Ten | Fifteen | Twenty-one | ... |\n\n它不同于并行约简，因为约简只是从给定的输入数据生成总运算输出。另一方面，扫描从每个操作产生输出。解决这个问题最简单的方法是迭代所有输入来生成输出。然而，这需要很长时间，而且在图形处理器中效率很低。因此，温和的方法可以并行化前缀求和操作，如下所示:\n\n![](img/40a0239f-03fc-48b4-a4b9-bcfe8d18b761.png)\n\n在这种方法中，我们可以使用多个 CUDA 内核获得输出。但是，这种方法不会减少迭代的总次数，因为应该为所有输出逐个添加第一个输入元素。此外，当数组足够大时，我们无法预测输出结果，因此应该启动多个线程块。这是因为在 CUDA 架构中，所有调度的 CUDA 线程不会同时启动，多个 CUDA 线程之间会有冲突。为了避免这种情况，我们需要对数组使用双缓冲区方法，这是另一个低效之处。下面的代码显示了它的实现:\n\n```cpp\n__global__ void\nscan_v1_kernel(float *d_output, float *d_input, int length, int offset) {\n    int idx = blockDim.x * blockIdx.x + threadIdx.x;\n\n    float element = 0.f;\n    for (int offset = 0; offset < length; offset++) {\n        if (idx - offset >= 0)\n            element += d_input[idx - offset];\n    }\n    d_output[idx] = element;\n}\n```\n\n还有另一种优化的方法叫做 **Blelloch scan** 。这种方法通过指数增加和减少步长来产生前缀和输出。下图显示了该方法的过程:\n\n![](img/037f5546-f0b2-437e-a772-a3c9cdfcaf62.png)\n\n基于步幅控制有两个步骤。当增加步幅时，它相应地获得部分和。然后，它获得部分和，同时相应地减小步幅。每一步都有不同的操作模式，但它们可以通过步幅大小来计算。现在，让我们介绍一下 Blelloch 扫描的实现，并检查一下更新后的性能。\n\n# Blelloch 扫描实现\n\n以下步骤将向您展示如何实现优化的并行扫描算法:\n\n1.  让我们创建一个可以接受输入和输出内存及其大小的内核函数:\n\n```cpp\n__global__ void scan_v2_kernel(float *d_output, float *d_input, int length)\n{\n    ...\n}\n```\n\n2.  然后，我们将创建一个 CUDA 线程索引和一个全局索引来处理输入数据:\n\n```cpp\nint idx = blockDim.x * blockIdx.x + threadIdx.x;\nint tid = threadIdx.x;\n```\n\n3.  为了加速迭代，我们将使用共享内存。该算法可以生成两倍于 CUDA 线程大小的输出，因此我们会将额外的块大小的输入数据加载到共享内存中:\n\n```cpp\nextern __shared__ float s_buffer[];\ns_buffer[threadIdx.x] = d_input[idx];\ns_buffer[threadIdx.x + BLOCK_DIM] = d_input[idx + BLOCK_DIM];\n```\n\n4.  在我们开始迭代之前，我们将声明计算左操作数和右操作数之间间隙的偏移量变量:\n\n```cpp\nint offset = 1;\n```\n\n5.  然后，我们将输入数据相加，直到偏移量大于输入长度:\n\n```cpp\nwhile (offset < length)\n{\n    __syncthreads();\n    int idx_a = offset * (2 * tid + 1) - 1;\n    int idx_b = offset * (2 * tid + 2) - 1;\n    if (idx_a >= 0 && idx_b < 2 * BLOCK_DIM) {\n        s_buffer[idx_b] += s_buffer[idx_a];\n    }\n    offset <<= 1;\n}\n```\n\n6.  之后，我们将再次迭代，同时将缩减大小减少两个:\n\n```cpp\noffset >>= 1;\nwhile (offset > 0) {\n    __syncthreads();\n    int idx_a = offset * (2 * tid + 2) - 1;\n    int idx_b = offset * (2 * tid + 3) - 1;\n    if (idx_a >= 0 && idx_b < 2 * BLOCK_DIM) {\n        s_buffer[idx_b] += s_buffer[idx_a];\n    }\n    offset >>= 1;\n}\n__syncthreads();\n```\n\n7.  最后，我们将使用内核函数将输出值存储在全局内存中:\n\n```cpp\nd_output[idx] = s_buffer[tid];\nd_output[idx + BLOCK_DIM] = s_buffer[tid + BLOCK_DIM];\n```\n\n8.  现在，我们可以这样调用这个扫描内核函数:\n\n```cpp\nvoid scan_v2(float *d_output, float *d_input, int length)\n{\n    dim3 dimBlock(BLOCK_DIM);\n    dim3 dimGrid(1);\n    scan_v2_kernel<<<dimGrid, dimBlock, \n                     sizeof(float) * BLOCK_DIM * 2>>>\n                  (d_output, d_input, length);\n    cudaDeviceSynchronize();\n}\n```\n\n你也可以用同样的功能界面写一个幼稚的扫描版本。现在，让我们回顾一下我们的新版本有多快，以及是否有任何其他优化机会可以利用。\n\n9.  以下代码显示了简单扫描和 Blelloch 扫描的性能分析结果:\n\n```cpp\n$ nvcc -m64 -std=c++ 11 -I/usr/local/cuda/samples/common/inc -gencode arch=compute_70,code=sm_70 -L/usr/local/cuda/lib -o scan ./scan.cu ./scan_v1.cu ./scan_v2.cu\n$ nvprof ./scan\n            Type Time(%)     Time Calls      Avg      Min      Max Name\n GPU activities:  68.96% 22.751us     1 22.751us 22.751us 22.751us scan_v1_kernel(float*, float*, int)\n 12.71% 4.1920us    1 4.1920us 4.1920us 4.1920us scan_v2_kernel(float*, float*, int)\n```\n\n如您所见，由于开销减少，Blolloch 扫描比朴素扫描算法快大约 5 倍。我们还可以通过比较不同实现的输出来验证操作结果:\n\n```cpp\ninput         :: -0.4508 -0.0210 -0.4774  0.2750 ... 0.0398 0.4869\nresult[cpu]   :: -0.4508 -0.4718 -0.9492 -0.6742 ... 0.3091 0.7960\nresult[gpu_v1]:: -0.4508 -0.4718 -0.9492 -0.6742 ... 0.3091 0.7960\nSUCCESS!!\nresult[cpu]   :: -0.4508 -0.4718 -0.9492 -0.6742 ... 0.3091 0.7960\nresult[gpu_v2]:: -0.4508 -0.4718 -0.9492 -0.6742 ... 0.3091 0.7960\nSUCCESS!!\n```\n\n到目前为止，我们已经介绍了如何在单个块大小上设计和实现优化的并行前缀求和操作。要对输入数据使用前缀求和操作，输入数据比块大小多，我们需要基于块级缩减代码构建块级前缀求和操作。我们将在下一节中更详细地讨论这一点。\n\n# 构建全局大小扫描\n\n我们实现的前缀和操作在单个线程块中工作。由于第一步有两个输入，并且我们在一个线程块中可以拥有的最大 CUDA 线程数是 1，024，因此最大可用大小是 2，048。在不考虑其他线程块操作的情况下，线程块进行上扫和下扫。\n\n然而，如果我们执行逐块扫描操作，这个操作可以被放大。为此，您需要额外的步骤来收集最后的前缀和结果，扫描它们，并将每个线程块的结果与每个块的块级扫描值相加。该程序可按如下方式实施:\n\n![](img/9080ba5d-b61c-4763-bc01-989c933a5255.png)\n\n# 追求更好的表现\n\n我们的实现代码执行最佳操作。但是，我们可以通过减少共享内存的存储体冲突来进一步优化。在我们的实现中，CUDA 线程在某些点访问相同的内存库。NVIDIA 的 GPU Gem3 在*第 39 章【与 CUDA*([https://developer . NVIDIA . com/gpugems/gpugems 3/gpugems 3 _ ch39 . html](https://developer.nvidia.com/gpugems/GPUGems3/gpugems3_ch39.html)中介绍了前缀求和(scan)，并在 *39.2.3【避免银行冲突】中指出了这个问题。您可以根据我们的实施调整解决方案，但如果需要，您应该将`NUM_BANKS`更新为`32`，将`LOG_NUM_BANKS`更新为`5`。如今，CUDA 架构有 32 个共享内存库。*\n\n# 并行前缀和运算的其他应用\n\n1993 年，盖尔洛克博士发表了一篇关于他的前缀和的文章，名为*前缀和及其应用*([https://www.cs.cmu.edu/~guyb/papers/Ble93.pdf](https://www.cs.cmu.edu/~guyb/papers/Ble93.pdf))。你可以通过阅读他的文章来了解更多关于并行前缀和算法及其应用。应用有压缩、分割、分段扫描、快速排序、基数排序和合并排序。\n\nAhmed Sallm 博士的视频讲座，【CUDA 并行处理入门-第 4 讲第 2\\3 部分([https://youtu.be/y2HzWKTqo3E](https://youtu.be/y2HzWKTqo3E))很好地介绍了这些。它从概念上介绍了前缀和算法如何用于裁剪图形和构建稀疏矩阵。他还提供了如何使用排序算法的说明。\n\n# 紧凑型和分体式\n\n之前，我们介绍了如何并行化顺序前缀和算法，并讨论了如何将其用于其他应用。现在，让我们来介绍其中的一些应用:紧凑型和分体式。紧凑运算是一种算法，可以合并满足数组中给定条件的值。另一方面，拆分操作是一种将值分配到指定位置的算法。一般来说，这些算法是按顺序工作的。然而，我们将看到并行前缀和操作如何改进它的功能。\n\n紧凑操作用于将满足特定条件的特定数据收集到一个数组中。例如，如果我们想对数组中的正元素使用压缩操作，那么操作如下:\n\n![](img/ec6509d7-6673-4967-898a-f387974abaca.png)\n\n在并行编程中，我们有一种不同的方法，可以使用并行前缀和操作来利用多个内核。首先，我们标记数据来检查它是否满足条件(即谓词)，然后我们进行前缀求和操作。prefix-sum 的输出将是标记值的索引，因此我们可以通过复制它们来获得聚集的数组。下图显示了一个紧凑操作的示例:\n\n![](img/16b52978-c007-437d-8b05-577c3b76efa9.png)\n\n由于所有这些任务都可以并行完成，我们可以通过四个步骤获得聚集的数组。\n\n另一方面，拆分意味着将数据分发到多个不同的地方。一般来说，我们从最初的地方分发数据。下图显示了其操作示例:\n\n![](img/517c99ed-34ed-471d-934e-7640d07b839f.png)\n\n这个例子显示了聚集的数组元素分布在它们原来的地方。我们也可以使用前缀和并行实现这一点。首先，我们参考谓词数组并进行前缀求和。由于输出是每个元素的地址，我们可以轻松地分发它们。下图显示了如何完成此操作:\n\n![](img/7af231f8-fb0d-4d46-887a-99d82c9aa08a.png)`\n\n现在，让我们实现这一点，并讨论它们的性能限制器及其应用。\n\n# 实施契约\n\n紧凑操作是谓词、扫描、寻址和聚集的序列。在这个实现中，我们将从随机生成的数字数组中构建一个正数数组。初始版本只能支持单线程块操作，因为我们将只使用单个块大小的前缀和操作。然而，我们可以了解前缀和如何对其他应用有用，并使用扩展的前缀和操作将此操作扩展到更大的数组。\n\n为了实现一个紧凑的操作，我们将编写几个内核函数来完成每一步所需的操作，并最后调用这些函数:\n\n1.  让我们编写一个内核函数，它可以通过检查每个元素的值是否大于零来生成谓词数组:\n\n```cpp\n__global__ void\npredicate_kernel(float *d_predicates, float *d_input, int length)\n{\n    int idx = blockDim.x * blockIdx.x + threadIdx.x;\n\n    if (idx >= length) return;\n\n    d_predicates[idx] = d_input[idx] > FLT_ZERO;\n}\n```\n\n2.  然后，我们必须对该谓词数组执行前缀求和操作。我们将在这里重用之前的实现。之后，我们可以编写一个内核函数，该函数可以检测被扫描数组的地址，并将目标元素收集为输出:\n\n```cpp\n__global__ void\npack_kernel(float *d_output, float *d_input, float *d_predicates, float *d_scanned, int length)\n{\n    int idx = blockDim.x * blockIdx.x + threadIdx.x;\n\n    if (idx >= length) return;\n\n    if (d_predicates[idx] != 0.f)\n    {\n        // addressing\n        int address = d_scanned[idx] - 1;\n\n        // gather\n        d_output[address] = d_input[idx];\n    }\n}\n```\n\n3.  现在，让我们把他们召集在一起，做一个紧凑的操作:\n\n```cpp\n// predicates\npredicate_kernel<<< GRID_DIM, BLOCK_DIM >>>(d_predicates, d_input, length);\n// scan\nscan_v2(d_scanned, d_predicates, length);\n// addressing & gather (pack)\npack_kernel<<< GRID_DIM, BLOCK_DIM >>>(d_output, d_input, d_predicates, d_scanned, length);\n```\n\n4.  现在，我们有一个从随机生成的数组中收集的正数数组:\n\n```cpp\n$ nvcc -run -m64 -std=c++ 11 -I/usr/local/cuda/samples/common/inc -gencode arch=compute_70,code=sm_70 -L/usr/local/cuda/lib -o pack_n_split ./pack_n_split.cu\ninput    :: -0.4508 -0.0210 -0.4774  0.2750 .... 0.0398  0.4869\npack[cpu]::  0.2750  0.3169  0.1248  0.4241 .... 0.3957  0.2958\npack[gpu]::  0.2750  0.3169  0.1248  0.4241 .... 0.3957  0.2958\nSUCCESS!!\n```\n\n通过使用并行前缀和运算，我们可以很容易地并行实现紧凑运算。我们的实现压缩了给定数组中的正值，但是我们可以将其切换到另一个条件，并毫无困难地应用压缩操作。现在，让我们介绍如何将这些紧凑的元素分布到原始数组中。\n\n# 实施拆分\n\n拆分操作是谓词、扫描、寻址和拆分的序列。在这个实现中，我们将重用上一节中创建的地址数组。因此，我们可以跳过前面的步骤，直接从地址数组中执行拆分操作:\n\n1.  让我们编写拆分内核函数，如下所示:\n\n```cpp\n__global__ void\nsplit_kernel(float *d_output, float *d_input, float *d_predicates, float *d_scanned, int length)\n{\n    int idx = blockDim.x * blockIdx.x + threadIdx.x;\n\n    if (idx >= length) return;\n\n    if (d_predicates[idx] != 0.f)\n    {\n        // address\n        int address = d_scanned[idx] - 1;\n\n        // split\n        d_output[idx] = d_input[address];\n    }\n}\n```\n\n2.  现在，我们可以调用内核函数，如下所示:\n\n```cpp\ncudaMemcpy(d_input, d_output, sizeof(float) * length, cudaMemcpyDeviceToDevice);\n    cudaMemset(d_output, 0, sizeof(float) * length);\n    split_kernel<<<GRID_DIM, BLOCK_DIM>>>(d_output, d_input, d_predicates, d_scanned, length);\n```\n\n3.  由于我们将使用上一步的扫描输出，我们将把它复制到输入中，并清除原始数组。总之，我们可以使用 CUDA 进行并行压缩和拆分。这是我们实现的输出。您可以确认它是否按要求运行:\n\n```cpp\n$ nvcc -run -m64 -std=c++ 11 -I/usr/local/cuda/samples/common/inc -gencode arch=compute_70,code=sm_70 -L/usr/local/cuda/lib -o pack_n_split ./pack_n_split.cu\ninput    :: -0.4508 -0.0210 -0.4774  0.2750 .... 0.0398  0.4869\npack[cpu]::  0.2750  0.3169  0.1248  0.4241 .... 0.3957  0.2958\npack[gpu]::  0.2750  0.3169  0.1248  0.4241 .... 0.3957  0.2958\nSUCCESS!!\nsplit[gpu]   0.0000  0.0000  0.0000  0.2750 .... 0.0398  0.4869\nSUCCESS!!\n```\n\n在我们的实现中，我们为正元素生成了一个紧凑数组和一个拆分数组。多亏了并行前缀-sum，我们也可以并行地做到这一点。我们版本的一个主要限制是它只支持少于 2，048 个元素，因为我们的实现是基于我们以前的并行前缀和实现。\n\n# n 体\n\n任何 N 体模拟都是对在物理力影响下演化的动力系统的模拟。数值近似是在物体不断相互作用时进行的。例如，在物理学和天文学中广泛进行 n 体模拟，以便科学家能够理解宇宙中粒子的动力学。n 体模拟用于许多其他领域，包括计算流体动力学，以便理解湍流流动模拟。\n\n求解 N 体模拟的一个相对简单的方法是使用具有 *O(N <sup>2</sup> )* 复杂度的蛮力技术。令人尴尬的是，这种方法本质上是平行的。有各种算法规模的优化可以降低计算复杂度。它可以用来确定近距离相互作用中的力，而不是将全对应用于整个模拟。即使在这种情况下，创建一个内核来解决 CUDA 上的力也是非常有用的，因为它也将提高远场组件的性能。加速一个组件将从其他组件中卸载工作，因此整个应用都受益于加速一个内核。\n\n# 在图形处理器上实现 N 体模拟\n\n该算法基本上是一个计算力的全对算法，*f<sub>ij</sub>T3】，为一个 N ![](img/5b868d65-55c7-40fd-a496-8c31b23a562d.png) N 网格。物体上的总力/加速度*F<sub>I</sub>**I*是第 *i* 行中所有条目相加的结果。从并行的角度来看，这是 *O(N <sup> 2 </sup> )* 的一个令人尴尬的并行任务。*\n\n从性能的角度来看，应用是受内存限制的，并且会受到内存带宽的限制。好的一面是，大部分数据可以重用，并存储在高带宽和低延迟的内存中，例如共享内存。共享内存中的数据重用和存储减少了全局内存的负载，因此有助于达到峰值计算性能。\n\n下图显示了我们将使用的策略:\n\n![](img/b5eefd12-c31d-480d-a0b8-a473c3eed048.png)\n\n我们使用平铺，而不是一次又一次地从全局内存加载内存。我们已经演示了平铺在矩阵乘法中的应用，并在前面的章节中介绍了它在成像应用中的用途。上图显示每行都是并行计算的。图块大小由共享内存中可以存储的最大元素数量定义，并且不影响内核的占用率。每个块将数据加载到共享内存中，然后执行同步。一旦数据被加载到共享内存中，力/加速度计算就在每个块中完成。可见，即使并行计算一个单独的行，为了实现最佳的数据重用，每行中的交互都是按顺序进行的。\n\n# N 体仿真实现概述\n\n让我们以伪代码格式回顾一下它的实现，然后解释它的逻辑。在这个例子中，我们使用引力势来说明所有对 N 体模拟中计算的基本形式。实现的代码可以在`07_parallel_programming_pattern/05_n-body`中找到。按照以下步骤开始:\n\n1.  用随机变量初始化 n 空间:\n\n```cpp\ndata[i] = 2.0f * (rand() / max) - 1.0f\n```\n\n2.  声明数据并将其存储在中间共享内存空间中，以便有效地重用。同步它以保证块中的所有线程都能看到共享内存中的更新值:\n\n```cpp\nfor (int tile = 0; tile < gridDim.x; tile++) {\n... \n__shared__ float3 shared_position[blockDim.x];\nfloat4 temp_position = p[tile * blockDim.x + threadIdx.x];\nshared_position[threadIdx.x] = make_float3(temp_position.x, temp_position.y, temp_position.z);\n__syncthreads();\n...\n}\n```\n\n3.  通过迭代每个块来计算力:\n\n```cpp\nfor (int j = 0; j < BLOCK_SIZE; j++) {\n    //Calculate Force\n    __syncthreads();\n}\n```\n\n4.  最后，用`nvcc`编译器用以下命令编译应用:\n\n```cpp\n$nvcc -run --gpu-architecture=sm_70 -o n-body n_body.cu \n```\n\n如您所见，实现 N 体模拟是一项令人尴尬的并行任务，而且非常简单。虽然我们已经在这里实现了代码的基本版本，但是仍然存在各种不同的算法。您可以根据对算法所做的更改，将此版本用作可以改进的模板。\n\n# 直方图计算\n\n在一个令人尴尬的并行作业中，理想情况下，您会将计算分配给每个处理独立数据的线程，从而不会产生数据竞争。到现在，你会意识到有些模式不适合这个类别。其中一种模式是当我们计算直方图时。直方图模式显示数据项的频率，例如，我们在每个 ch 中使用单词 CUDA 的次数\n\napter，本章中每个字母出现的次数，等等。直方图采用以下形式:\n\n![](img/713c8091-8a43-4097-8ba0-192a4fe97b41.png)\n\n在本节中，我们将使用原子操作来序列化对数据的访问，以便获得正确的结果。\n\n# 编译和执行步骤\n\n直方图提供了关于手头数据集的重要特征，以及关于数据集的有用见解。例如，在整个图像中，只有几个区域可能是感兴趣的区域。创建直方图有时用于找出感兴趣区域在图像中的位置。在这个例子中，我们将利用计算图像上的直方图，其中整个图像被分成块。让我们开始吧:\n\n1.  准备好你的 GPU 应用。这个代码可以在`07_parallel_programming_pattern/08_histogram`找到。\n2.  使用以下命令，使用`nvcc`编译器编译您的应用:\n\n```cpp\n$ nvcc -c scrImagePgmPpmPackage.cpp \n$ nvcc -c image_histogram.cu\n$ nvcc -run -o image_histogram image_histogram.o scrImagePgmPpmPackage.o\n```\n\n`scrImagePgmPpmPackage.cpp`文件提供了我们可以用来读写扩展名为`.pgm`的图像的源代码。直方图计算代码可以在`image_histogram.cu`找到。\n\n# 理解平行直方图\n\n直方图等模式需要原子操作，这意味着以序列化方式更新特定地址的值，以消除多个线程的争用，从而更新同一地址。这需要多线程之间的协调。在这个七步走的过程中，你可能已经注意到我们利用了私有化。私有化是一种利用共享内存等低延迟内存来降低吞吐量和延迟的技术，如下图所示:\n\n![](img/02786dd6-3c15-4c9f-9d7a-770d31b640b4.png)\n\n基本上，我们不是在全局内存上使用原子操作，而是在共享内存上使用原子。你现在应该很清楚原因了。与在共享内存/L1 缓存上执行相同操作相比，在全局内存上执行原子操作的成本更高。从麦克斯韦架构开始，原子操作是硬件支持的。从麦克斯韦架构开始，私有化的共享内存实现应该能为您带来 2 倍的性能。但是，请注意，原子操作仅限于特定的功能和数据大小。\n\n# 用 CUDA 原子函数计算直方图\n\n首先，我们将利用共享内存上的`atomicAdd()`操作来计算共享内存中每个块的直方图。按照以下步骤计算内核中的直方图:\n\n1.  每块分配的共享内存等于每块直方图的大小。由于它是一个字符图像，我们预计元素的范围为 0-255:\n\n```cpp\n__shared__ unsigned int histo_private[256];\n```\n\n2.  将共享内存阵列初始化为每块`0`:\n\n```cpp\nif(localId <256)\n    histo_private[localId] = 0;\n```\n\n3.  对此进行同步，以确保块中的所有线程都看到已初始化的数组:\n\n```cpp\n__syncthreads();\n```\n\n4.  从全局/纹理存储器中读取图像数据:\n\n```cpp\nunsigned char imageData = tex2D<unsigned char>(texObj,(float)(tidX),(float)(tidY));\n```\n\n5.  在共享内存上执行`atomicAdd()`操作:\n\n```cpp\natomicAdd(&(histo_private[imageData]), 1);\n```\n\n6.  写入全局内存之前，跨块同步:\n\n```cpp\n__syncthreads();\n```\n\n7.  将每个块的直方图写入全局内存:\n\n```cpp\nif(localId <256)\n    imageHistogram[histStartIndex+localId] = histo_private[localId];\n```\n\n现在，我们已经在 GPU 上完成了直方图计算。\n\n总而言之，直方图很容易通过共享原子内存来实现。由于在硬件中对共享原子存储器的本地支持，这种方法可以在 Maxwell 前进卡上获得高性能。\n\n# 利用动态并行在 CUDA 中快速排序\n\n排序是任何应用的基本构造块的关键算法之一。有许多排序算法已经被广泛研究。最差的时间复杂度，最好的时间复杂度，输入数据特征(数据几乎是排序的还是随机的？它是键值对吗？它是整数还是浮点数？)，就地或异地内存需求，等等定义哪种算法适合哪种应用。一些排序算法属于分治算法的范畴。这些算法适用于并行性，并适合 GPU 等架构，在这些架构中，要排序的数据可以被划分进行排序。快速排序就是这样一种算法。如前所述，Quicksort 属于“分而治之”的范畴。这是一个三步走的方法，如下所示:\n\n1.  从需要排序的数组中选择一个元素。该元素充当枢轴元素。\n2.  第二步是划分所有元素的位置。小于枢轴的所有元素都向左移动，大于或等于枢轴的所有元素都向右移动。这一步也称为分区。\n3.  递归地执行步骤 1 和 2，直到所有的子数组都被排序。\n\nQuicksort 最坏情况复杂度为 O( ![](img/79e46e31-d8d8-4612-9c6d-9e08bf0e40a0.png))，与其他最坏情况复杂度为 O( ![](img/3859e9ea-10c4-44b8-9ddc-61e8a07878fe.png))的排序过程(如合并排序和堆排序)相比，这可能看起来并不理想。然而，快速排序在实践中被认为是有效的。枢轴元素的选择可以仔细考虑，有时也可以随机选择，这样就几乎不会出现最坏情况的复杂性。此外，与其他排序算法(如需要额外存储的合并排序)相比，快速排序的内存负载和要求更低。快速排序的更实际的实现使用随机化版本。随机化版本的预期时间复杂度为 0(![](img/52cec39e-957c-4bfc-be02-07aa00f96b82.png))。在随机化的版本中，最坏情况的复杂性也是可能的，但是对于特定的模式(如排序数组)不会出现这种情况，随机化的快速排序在实践中效果很好。\n\n虽然我们可以写一整章关于排序算法的特性，但我们计划只涵盖 CUDA 的特性，这些特性将帮助您在 GPU 上高效地实现快速排序。在本节中，我们将利用动态并行，这是从 CUDA 6.0 和具有 3.5 架构的图形处理器开始引入的。\n\n现在，让我们回顾一下动态并行性对排序算法的贡献。\n\n# 快速排序和 CUDA 动态并行\n\n快速排序算法要求递归启动内核。到目前为止，我们看到的算法都是通过 CPU 调用内核一次。内核完成执行后，我们返回到 CPU 线程，然后重新启动它。这样做的结果是将控制权交还给中央处理器，还可能导致中央处理器和图形处理器之间的数据传输，这是一项昂贵的操作。过去，在需要递归等特性的 GPU 上高效实现快速排序等算法非常困难。随着 GPU 架构 3.5 和 CUDA 5.0 的发展，引入了一个新特性，称为动态并行。\n\n动态并行允许内核中的线程从图形处理器启动新内核，而无需将控制权交还给中央处理器。动态这个词来自于它是基于运行时数据的事实。线程可以同时启动多个内核。下图简化了解释:\n\n![](img/7e923707-6635-4f2b-8816-5c34e27ab0f9.png)\n\n如果我们将这个概念转化为快速排序的执行方式，它看起来会是这样的:\n\n![](img/82e06370-f447-437d-a8e3-15356096543e.jpg)\n\n深度 0 是来自 CPU 的调用。对于每个子阵列，我们启动两个内核:一个用于左阵列，一个用于右阵列。达到内核的最大深度或元素数量小于 32(即扭曲大小)后，递归停止。为了使内核的启动处于非零流和异步状态，以便子阵列内核独立启动，我们需要在每次内核启动之前创建一个流:\n\n```cpp\ncudaStream_t s;\ncudaStreamCreateWithFlags( &s, cudaStreamNonBlocking );\ncdp_simple_quicksort<<< 1, 1, 0, s >>>(data, left, nright, depth+1);\ncudaStreamDestroy( s );\n```\n\n这是非常重要的一步，因为，否则，内核的启动可能会被序列化。关于流的更多细节，请参考多 GPU 内核。\n\n# 用 CUDA 快速排序\n\n对于我们的快速排序实现，我们将利用动态并行性递归启动 GPU 内核。实施快速排序的主要步骤如下:\n\n1.  **CPU 启动第一个内核**:内核一个块一个线程启动。左边的元素是数组的开始，而右边是数组的最后一个元素(基本上是整个数组):\n\n```cpp\nint main(int argc, char **argv)\n{ ...\n    cdp_simple_quicksort<<< 1, 1 >>>(data, left, right, 0);\n}\n```\n\n2.  **极限检查**:在从内核内部启动内核之前，检查两个标准。首先，检查我们是否已经达到硬件允许的最大深度限制。其次，我们需要检查子数组中要排序的元素数量是否小于扭曲大小(32)。如果其中一个是真的，那么我们必须按顺序进行选择排序，而不是启动新的内核:\n\n```cpp\n__global__ void cdp_simple_quicksort( unsigned int *data, int left, int right, int depth )\n{ ...\n\nif( depth >= MAX_DEPTH || right-left <= INSERTION_SORT )\n {\n     selection_sort( data, left, right );\n     return;\n }\n```\n\n3.  **划分**:如果满足前面的条件，那么将数组划分为两个子数组，并启动两个新的内核，一个用于左数组，另一个用于右数组。如果您仔细查看下面的代码，您会看到我们正在从内核内部启动一个内核:\n\n```cpp\n__global__ void cdp_simple_quicksort( unsigned int *data, int left, int right, int depth ) {\n...\nwhile(lptr <= rptr)\n {\n     // Move the left pointer as long as the \n     // pointed element is smaller than the pivot.\n     // Move the right pointer as long as the \n     // pointed element is larger than the pivot.\n     // If the swap points are valid, do the swap!\n\n     // Launch a new block to sort the left part.\n     if(left < (rptr-data))\n     { // Create a new stream for the eft sub array\n        cdp_simple_quicksort<<< 1, 1, 0, s \n                            >>>(data, left, nright, depth+1);\n     }\n    // Launch a new block to sort the right part.\n    if((lptr-data) < right)\n     {//Create stream for the right sub array\n         cdp_simple_quicksort<<< 1, 1, 0, s1 \n                             >>>(data, nleft, right, depth+1);\n     }\n }\n```\n\n4.  **执行代码**:执行的代码可以在`07_parallel_programming_pattern/06_quicksort`找到。使用以下命令用`nvcc`编译器编译您的应用:\n\n```cpp\n$nvcc -o quick_sort --gpu-architecture=sm_70 -rdc=true quick_sort.cu \n```\n\n如您所见，我们在编译中添加了两个标志:\n\n*   `-- gpu-architecture=sm_70`:这个标志告诉`nvcc`为 Volta GPU 编译生成二进制/ `ptx`。如果您特别没有添加这个标志，编译器会尝试编译从`sm_20`也就是费米生成卡兼容的代码，直到新的架构，也就是`sm_70`，也就是 Volta。编译将失败，因为老一代卡不支持动态并行。\n*   `-rdc=true`:这是在 GPU 上实现动态并行的一个关键参数。\n\n# 动态并行准则和约束\n\n尽管动态并行为我们提供了在 GPU 上移植快速排序等算法的机会，但仍有一些基本规则和准则需要遵循。\n\n**编程模型规则**:基本上所有的 CUDA 编程模型规则都适用:\n\n*   每个线程的内核启动是异步的。\n*   仅允许每个块同步。\n*   创建的流在一个块内共享。\n*   事件可用于创建流间依赖关系。\n\n**内存一致性规则**:\n\n*   子内核在启动时看到父内核的状态。\n*   父内核可以看到子内核所做的更改，但是只能在同步之后。\n*   本地和共享内存通常是私有的，不能被父内核传递或访问。\n\n**指引**:\n\n*   理解每次内核启动都会增加延迟也很重要。随着时间的推移，从另一个内核内部启动一个内核的延迟随着新架构逐渐减少。\n*   虽然发射吞吐量比来自主机的高一个数量级，但是可以对最大深度进行限制。最新一代卡允许的最大深度是 24。\n*   从内核内部执行`cudaDeviceSynchronize()`是一个非常昂贵的操作，应该尽可能避免。\n*   在全局内存上预先分配了额外的内存，这样我们就可以在内核启动之前存储它们。\n*   如果内核出现故障，错误只能从主机上看到。因此，建议您使用`-lineinfo`标志和`cuda-memcheck`来定位错误的位置。\n\n# 基数排序\n\n另一种非常流行的排序算法是基数排序，因为它在顺序机器上非常快。基数排序的基本策略是每个元素按数字排序。让我们看一个简单的例子来解释基数排序的步骤:\n\n假设要排序的元素如下:\n\n| 价值 | seven | Fourteen | four | one |\n\n这些数字的等效二进制值如下:\n\n| 位 | 0111 | One thousand one hundred and ten | 0100 | 0001 |\n\n第一步是根据位 0 进行排序。数字的位 0 如下:\n\n| 0 <sup>第</sup>位 | one | Zero | Zero | one |\n\n根据第位的*进行排序，基本上意味着所有的零都在左边。所有的都在右边，同时保持元素的顺序:*\n\n| 第 0<sup>位的排序值</sup> | Fourteen | four | seven | one |\n| 基于第 0 <sup>位</sup>位的排序位 | One thousand one hundred and ten | 0100 | 0111 | 0001 |\n\n第 0<sup>位完成后，我们继续第一位。基于第一位排序后的结果如下:</sup>\n\n| 第一位的排序值 | four | Fourteen | seven | one |\n| 基于第一位的排序位 | 0100 | One thousand one hundred and ten | 0111 | 0001 |\n\n然后，我们继续到下一个更高的位，直到所有的位都结束。最终结果如下:\n\n| 所有位的排序值 | one | four | seven | one |\n| 基于所有位的排序位 | 0001 | 0100 | 0111 | One thousand one hundred and ten |\n\n如您所见，我们在本例中设置的上限是 4 位。对于较大的数字，如整数，这将持续到 32 位，因为整数是 32 位。\n\n现在我们已经理解了这个算法，让我们看看如何在 GPU 中实现它。与本章的其他部分相比，我们将采用两种方法来展示 CUDA 生态系统，以便我们可以实现/使用基数排序。\n\n**选项 1** :我们将利用一个扭曲等级对 32 个元素进行基数排序。这样做的原因是，我们想利用基数排序向您介绍扭曲级原语。\n\n**选项 2** :我们将使用推力库，它是 CUDA 工具包的一部分。它实现了通用基数排序。最好的实现是重用。由于推力已经提供了基数排序的最佳实现之一，我们将使用它。\n\n# 两种方法\n\n为了便于理解，让我们从示例代码开始。在这个例子中，我们将利用扭曲级原语和推力库来实现/使用基数排序。示例代码可以在`07_parallel_programming_pattern/07_radixsort`找到。\n\n使用以下命令，使用`nvcc`编译器编译您的应用:\n\n*   扭曲级原始版本:\n\n```cpp\n$ nvcc -run -o radix_warp_sort radix_warp_sort.cu\n```\n\n*   推力库版本:\n\n```cpp\n$ nvcc -run -o radix_thrust_sort thrust_radix_sort.cu \n```\n\n这两个例子显示了 GPU 给出的排序输出。现在，让我们详细回顾一下这些操作是如何实现的。\n\n# 方法 1–扭曲级原语\n\n让我们看看 CUDA 扭曲级原语是如何在代码中实现我们的算法的:\n\n1.  首先，将数据从全局内存加载到共享内存:\n\n```cpp\n__shared__ unsigned int s_data[WARP_SIZE*2];\n```\n\n内存的大小等于翘曲大小`*2`，这样就可以实现乒乓缓冲。\n\n2.  从低位循环到高位:\n\n```cpp\nfor (int i = MIN_BIT_POS; i <= MAX_BIT_POS; i++){ ... }\n```\n\n3.  获取当前位掩码:\n\n```cpp\nunsigned int bit  = data&bit_mask;\n```\n\n4.  获取 1 和 0 的数量(直方图):\n\n```cpp\nunsigned int active = __activemask();\nunsigned int ones = __ballot_sync(active,bit);\nunsigned int zeroes = ~ones;\n```\n\n5.  获取当前位(前缀和)中为零(0)的线程的位置。\n6.  获取当前位(前缀和)中有一个(1)的线程的位置:\n\n```cpp\nif (!bit) // threads with a zero bit\n // get my position in ping-pong buffer\n pos = __popc(zeroes&thread_mask);\n else // threads with a one bit\n // get my position in ping-pong buffer\n pos = __popc(zeroes)+__popc(ones&thread_mask);\n```\n\n7.  将数据存储在乒乓共享缓冲存储器中:\n\n```cpp\n s_data[pos-1+offset] = data;\n```\n\n8.  重复步骤 2-6，直到达到高位。\n9.  将最终结果从共享内存存储到全局内存中:\n\n```cpp\nd_data[threadIdx.x] = s_data[threadIdx.x+offset];\n```\n\n你可能不清楚直方图和前缀和突然出现在哪里。让我们详细讨论一下这个实现，这样我们就可以理解如何使用 warp 级原语来实现它。\n\n在本节的开头，我们用一个例子描述了如何排序。然而，我们没有涉及的是如何找到需要交换的元素的位置。基数排序可以使用直方图和前缀和等基本原语来实现，因此可以很容易地在 GPU 中实现。\n\n让我们重新回顾一下我们看到的例子，并收集它的细节，包括直方图和前缀总和的步骤。下表显示了在每个位迭代进行的各种计算:\n\n| 价值 | seven | Fourteen | four | one |\n| 二进制的 | 0111 | One thousand one hundred and ten | 0100 | 0001 |\n| 位 0 | one | Zero | Zero | one |\n| 直方图前缀和 | Two | Zero | Two | Two |\n| 抵消 | Zero | Zero | one | one |\n| 新索引(前缀总和和偏移量) | Two | Zero | one | three |\n\n让我们解释上表中显示的每个计算，如下所示:\n\n1.  首先，我们构建第 0 位位置为 0 的元素数量和第 0 位位置为 1 的元素数量的直方图:\n\n*直方图:0 位(2 个值)，1 位(2 个值)*\n\n2.  然后，我们对这些值执行独占前缀求和。前缀和可以定义为所有先前值的和。在我们的例子中，我们分别对 0 <sup>第</sup>位和 1 <sup>第</sup>位执行此操作。\n3.  最后，我们根据前缀和值移动元素。\n\n我们用来找到直方图和前缀和的扭曲级图元分别是`__ballot_sync()`和`__popc()`。\n\n`__ballot_sync()`应用编程接口评估所有经纱活动线程的谓词，并返回一个整数，当且仅当第 n 个经纱的谓词评估为非零时，该整数的第 n 位被设置。计算整数数量的`__popc()`被设置为 1。\n\n在 CUDA 编程模型中，我们已经看到最小的执行单元是一个 warp (32 个线程)。CUDA 提供了各种具有细粒度控制的扭曲级原语，在许多应用中，这些原语可以带来更好的性能。在前一节中，我们介绍了一个这样的原语`__bllot__sync()`。其他重要的扭曲级原语包括`shuffle`指令，这些指令特别用于进行扭曲级缩减。`shuffle`说明书已经包含在这本书里了。如果你在 CUDA 方面已经达到忍者程序员级别的熟练程度，那么我们建议你查看 CUDA API 指南，了解更多这些扭曲级原语。\n\n这就完成了使用扭曲级原语描述基数排序。现在，让我们看看基于推力的库实现。\n\n# 方法 2–基于推力的基数排序\n\n基于推力的基数排序是基数排序的一般实现，对于不同类型的数据非常有效，例如整数、浮点或键值对。我们想再次强调这样一个事实:排序是一种经过大量研究的算法，它的并行实现也是如此。因此，我们建议您在自己实现一个库之前重用现有的库。\n\n将推力用于基数排序的步骤如下:\n\n1.  导入相关头文件(推力是一个只包含头文件的库，类似于 STL):\n\n```cpp\n#include <thrust/device_vector.h>\n#include <thrust/sort.h>\n```\n\n2.  声明并初始化设备向量:\n\n```cpp\n//declare a device vector of size N\nthrust::device_vector<int> keys(N);\n//Generate a random number generator engine\nthrust::default_random_engine r(12);\n//create a distribution engine which will create integer values\nthrust::uniform_int_distribution<int> d(10, 99);\n//Fill the array with randon values\nfor(size_t i = 0; i < v.size(); i++)\n    v[i] = d(r);\n```\n\n3.  对初始化的设备向量执行排序:\n\n```cpp\nthrust::sort(keys.begin(), keys.end());\n```\n\n使用这个库提供了一个更容易和健壮的方法。推力提供不同类型的排序方法，包括整数和浮点数的基数排序。或者，您可以创建一个自定义比较器来进行自定义排序，例如对所有事件编号进行排序，然后是奇数，按降序排序，等等。如果您想了解更多基于推力的排序示例，建议您查看 CUDA 提供的示例。\n\n现在，我们已经研究了在 GPU 上实现基数排序的两种方法。\n\n# 摘要\n\n在这一章中，我们研究了 CUDA 中常用算法和模式的实现。这些算法和模式通常是可用的。我们介绍了矩阵乘法和卷积滤波中的基本优化技术。然后，我们扩展了关于如何通过使用前缀和、N 体、直方图和排序来并行化问题的讨论。为此，我们使用了专用的 GPU 知识、库和低级原语。\n\n我们介绍的许多算法都是在 CUDA 库中实现的。比如矩阵乘法在 cuBLAS 库中，卷积在 CUDNN 库中。此外，我们还介绍了基数排序实现中的两种方法:使用推力库或扭曲级图元进行直方图计算。\n\n既然您已经看到了如何在常用的库中实现这些模式，接下来的逻辑步骤就是看看我们如何使用这些库。这就是我们在下一章将要做的。"
  },
  {
    "path": "docs/learn-cuda-prog/08.md",
    "content": "# 八、使用库和其他语言编程\n\n本章涵盖了其他 GPU 编程方法——使用 GPU 加速库和其他语言进行编程。使用 GPU 加速库编程使我们能够使用优化的内核开发应用。此外，我们可以使用其他编程语言开发 CUDA 软件，这些语言知道 CUDA 加速。这两种方式都提高了可编程性和生产率。此外，我们不必花时间优化已经优化的常见操作。\n\nCUDA 工具包提供了许多线性代数、图像和信号处理以及随机处理方面的 GPU 加速库。它们是 cuBLAS(基本线性代数子程序)、cuFFT(快速傅里叶变换)、cuRAND(随机数生成)、NPP(图像和信号处理)、cuSPARSE(稀疏线性代数)、nvGRAPH(图形分析)、cussolver(GPU 中的 LAPACK)、推力(CUDA 中的 STL)等等。我们也可以用 OpenCV 库编写 GPU 加速程序。我们将在本章中介绍其中的一些库。\n\n我们也可以使用图形处理器加速使用 R，MATLAB，Octave 和 Python。如今，Python 集成流行且功能强大，因为 GPU 可以加速许多机器学习和数据科学任务。我们还将这些语言作为入门级产品。\n\n本章将涵盖以下主题:\n\n*   用 cuBLAS 进行线性代数运算\n*   使用 cuBLAS 的混合精度操作\n*   并行随机数生成的 cuRAND\n*   图形处理器中快速傅里叶变换的快速傅里叶变换\n*   用图形处理器进行图像和信号处理的 NPP\n*   在 OpenCV 中编写 GPU 加速代码\n*   编写可与 CUDA 一起使用的 Python 代码\n*   针对八度音程和 R 中零编码加速的 NVBLAS\n*   MATLAB 中的 CUDA 加速\n\n# 用 cuBLAS 进行线性代数运算\n\ncuBLAS 库是一个 GPU 优化的标准实现**基本线性代数子程序** ( **BLAS** )。使用它的应用编程接口，程序员可以将 GPU 优化的计算密集型代码写入单个或多个 GPU。cuBLAS 有三个等级。第一级执行向量-向量运算，第二级执行矩阵-向量运算，第三级执行矩阵-矩阵运算。\n\n涵盖每一个层次都超出了本书的范围。我们只是关注如何使用 cuBLAS APIs，并为多个 GPU 扩展其性能。具体来说，这张收据将包括一次**单精度浮动矩阵乘法** ( **SGEMM** )操作——一次三级操作。\n\ncuBLAS 库是 CUDA 工具包的一部分，因此您可以使用 cuBLAS 而无需额外安装。此外，您可以使用`cc`或`cpp`文件扩展名，而不是`.cu`，因为您不需要使用特定于 CUDA 的内置关键字，如`__global__`或`threadIdx`。以下代码片段显示了 cuBLAS 函数(`cubalsSgemm`)的基本应用:\n\n```cpp\ncublasHandle_t handle;\ncublasCreate(&handle);\n\n.. { data operation } ..\n\ncublasSgemm(...);\n\n.. { data operation } ..\n\ncublasDestroy(handle);\n```\n\n如您所见，cuBLAS APIs 与`cublasHandle_t`类型句柄一起工作。\n\n# cuBLAS SGEMM 操作\n\nGEMM 操作可由以下等式表示:\n\n![](img/e837544a-a0b8-4716-9c9d-0943d86de399.png)\n\n其中*α*和*β*是标量， *A* 、 *B* 和 *C* 是列主格式的矩阵。这与以下框中的 cuBLAS 功能界面相匹配:\n\n```cpp\ncublasStatus_t cublasSgemm(cublasHandle_t handle,\n                          cublasOperation_t transa, \n                           cublasOperation_t transb,\n                           int m, int n, int k,\n                           const float *alpha,\n                           const float *A, int lda,\n                           const float *B, int ldb,\n                           const float *beta,\n                           float *C, int ldc);\n```\n\n在使用这个 GEMM 函数之前，让我们看一下参数的细节:\n\n*   `transa`和`transb`:对 cuBLAS 功能的指示:矩阵 *A* 和 *B* 是否应进行转置操作。\n*   `m`、`n`和`k`:矩阵的尺寸大小。\n*   `alpha`和`beta`:决定如何从源配置输出值的参数。\n*   `*A`、`*B`、`*C`:矩阵数据的线性缓冲区。\n*   `lda`:矩阵 *A* 的前导列维度。cuBLAS 将矩阵元素与该值对齐。\n*   `ldb`:矩阵 *B* 的前导列尺寸。cuBLAS 将矩阵元素与该值对齐。\n\n要在主机和设备之间传输数据，可以使用 cuBLAS 的`cublasSetMatrix()`和`cublasGetMatrix()`助手功能。它们是`cudaMemcpy()`的包装函数，但是有矩阵的维度信息，这样有助于增强代码的可读性；当然，你可以简单地使用`cudaMemcpy()`来代替。\n\n让我们使用 cuBLAS SGEMM 函数实现一个具有 GEMM 操作的应用。我们将包括`cublas_v2.h`来使用更新的 cuBLAS API。为了方便起见，我们将使用`getMatrix()`函数从给定维度获取随机生成的矩阵，并使用`printMatrix()`函数打印矩阵元素。这些代码在给定的示例代码中实现。在主功能中，我们将从给定的`M`、`N`和`K`中初始化三个矩阵— `A`、`B`和`C`。那么我们将如下计算`cublasSgemm()`:\n\n```cpp\ncublasHandle_t handle;\n\n    // Prepare input matrices\n    float *A, *B, *C;\n    int M, N, K;\n    float alpha, beta;\n\n    M = 3;    N = 4;    K = 7;\n    alpha = 1.f;    beta = 0.f;\n\n    // create cuBLAS handle\n    cublasCreate(&handle);\n\n    srand(2019);\n    A = getMatrix(K, M);\n    B = getMatrix(N, K);\n    C = getMatrix(M, N);\n\n    std::cout << \"A:\" << std::endl;\n    printMatrix(A, K, M);\n    std::cout << \"B:\" << std::endl;\n    printMatrix(B, N, K);\n    std::cout << \"C:\" << std::endl;\n    printMatrix(C, M, N);\n\n    // Gemm\n    cublasSgemm(handle, CUBLAS_OP_T, CUBLAS_OP_T, \n        M, N, K, &alpha, A, K, B, N, &beta, C, M);\n\n    cudaDeviceSynchronize();\n    std::cout << \"C out:\" << std::endl;\n    printMatrix(C, M, N);\n\n    cublasDestroy(handle);\n    cudaFree(A);    cudaFree(B);    cudaFree(C);\n    return 0;\n```\n\n通过链接 cuBLAS 库用`nvcc`编译代码:\n\n```cpp\n$ nvcc -run -gencode arch=compute_70,code=sm_70 -lcublas -o cublasSgemm ./cublasSgemm.cpp\n```\n\n下面的代码片段显示了执行的输出:\n\n```cpp\nA:\n 0.0492 0.4790 0.0226\n 0.7750 0.2794 0.8169\n 0.3732 0.6248 0.2636\n 0.9241 0.5841 0.8532\n 0.7188 0.5052 0.5398\n 0.9869 0.6572 0.0520\n 0.6815 0.7814 0.5988\nB:\n 0.8957 0.0481 0.7958 0.7825 0.3264 0.5189 0.5018\n 0.4559 0.6342 0.0759 0.5051 0.1132 0.0985 0.2802\n 0.3926 0.9153 0.6534 0.0174 0.1790 0.5775 0.6015\n 0.0322 0.2963 0.1068 0.5720 0.2832 0.7640 0.6240\nC:\n 0.9647 0.5454 0.2229 0.8604\n 0.5935 0.0186 0.6430 0.9198\n 0.5375 0.1448 0.3757 0.1718\nC out:\n 1.1785 2.5682 2.4854 0.6066\n 0.5817 0.8091 1.1724 2.0773\n 2.0882 1.4503 2.1331 1.8450\n```\n\n如`cublasSgemm()`函数调用中所述，矩阵 *A* 和 *B* 是转置矩阵。我们将原来的引导列大小传递给`cublasSgemm()`函数，分别为`lda`、`ldb`和`ldc`，可以看到操作按预期进行。\n\n# 多图形处理器操作\n\ncuBLAS 库的 cuBLAS-XT API 提供了 cuBLAS 在多个 GPU 上工作时的三级操作。有了这个 API，你的应用可以使用多 GPU 计算操作。这个片段展示了使用 cuBLAS-XT 的基本操作:\n\n```cpp\ncublasXtHandle_t handle;\ncublasXtCreate(&handle);\n\ncudaGetDeviceCount(&num_of_total_devices);\ndevices = (int *)calloc(num_of_devices, sizeof(int));\nfor (int i = 0; i < num_of_devices; i++)\n    devices[i] = i;\ncublasXtDeviceSelect(handle, num_of_devices, devices);\n\ncublasXtSgemm( ... );\n\ncublasXtDestroy(handle);\n```\n\n`cublasXtSgemm()`界面与`cublasSgemm()`功能相同，可以放心使用多个 GPU 的计算性能。例如，我们可以使用带有两个图形处理器的存储库中的示例代码获得以下结果。根据您的 GPU 和系统配置，此性能可能会有所不同:\n\n![](img/d2ce6a4a-c2c3-4fe3-a141-2416f6ce45de.png)\n\ncuBLAS 库提供了很多通用的线性代数运算。所以你应该检查你的必要功能是如何在库中提供的。此外，您还需要一个如何使用该函数的示例。以下项目是文档和示例的链接。因此，当您需要实现基于 cuBLAS 的应用时，建议您经常检查这两个文档:\n\n*   英伟达 cuBLAS 编程指南——参考指南:[https://docs.nvidia.com/cuda/cublas/index.html](https://docs.nvidia.com/cuda/cublas/index.html)\n*   <q>GPU 上的矩阵计算</q> *: CUBLAS、CUSOLVER 和 MAGMA 举例*安杰伊·chrzȩszczyk 和雅各布·安德斯:[https://developer . NVIDIA . com/sites/default/files/akamai/cuda/files/misc/mygpu . pdf](https://developer.nvidia.com/sites/default/files/akamai/cuda/files/Misc/mygpu.pdf)\n\n# 使用 cuBLAS 的混合精度操作\n\ncuBLAS 库支持混合精度计算。该计算意味着以不同精度操作的操作，例如，用单精度和半精度变量或用单精度和字符(`INT8`)进行计算。当我们需要使用较低的精度来获得更高的性能，同时在结果中获得更高的精度时，这种技术非常有用。\n\ncuBLAS 库提供`cublasGemmEx()`和`cublas{S/C}gemmEx()`支持混合精度操作的 GEMM 操作。它们是`cublas<t>gemm()`的扩展，接受每个 *A* 、 *B* 和 *C* 矩阵的指定数据类型。下表显示了 cuBLAS 库中`cublasGemmEx()`和其他可替换 API 的精度支持矩阵:\n\n| 计算类型 | a 型/ B 型 | c 型 | 可替换的 API |\n| `CUDA_R_16F` | `CUDA_R_16F` | `CUDA_R_16F` | `cublasHgemm()` |\n| `CUDA_R_32I` | `CUDA_R_8I` | `CUDA_R_32I` | 不适用的 |\n| `CUDA_R_32F` | `CUDA_R_16F` | `CUDA_R_16F` | `cublasSgemmEx()` |\n| `CUDA_R_8I` | `CUDA_R_32F` |\n| `CUDA_R_16F` | `CUDA_R_32F` |\n| `CUDA_R_32F` | `CUDA_R_32F` |\n| `CUDA_R_64F` | `CUDA_R_64F` | `CUDA_R_64F` | `cublasDgemm()` |\n| `CUDA_C_32F` | `CUDA_C_8I` | `CUDA_C_32F` | `cublasCgemmEx()` |\n| `CUDA_C_32F` | `CUDA_C_32F` |\n| `CUDA_C_64F` | `CUDA_C_64F` | `CUDA_C_64F` | `cublasZgemm()` |\n\n可以看到`cublasGemmEx()`可以覆盖`cublas{S/C}gemmEx()`功能的操作。因此，我们将在本节中介绍`cublasGemmEx()`。\n\n`cublasGemmEx()`函数的最后一个参数`cublasGemmAlgo_t`指定了矩阵-矩阵乘法的算法。有了这个参数，我们可以选择是否使用 TensorCore。`CUBLAS_GEMM_DEFAULT`选择 GEMM 算法，在 CUDA 核上运行。另一方面，`CUBLAS_GEMM_DEFAULT_TENSOR_OP`选择使用张量核的算法。如果 TensorCore 在给定条件下不可用，cuBLAS 将选择使用 CUDA 核心的算法。这种情况可能发生在没有张量核心或矩阵大小的图形处理器上，这与张量核心的工作方式不相符——四的倍数( ** 4* )。\n\n# 混合精度的 GEMM\n\n现在，让我们使用 cuBLAS GEMM 操作来尝试混合精度。实现之后，我们将介绍矩阵大小如何影响操作。完全实现的版本在`02_sgemm_mixed_precision/cublasGemmEx.cu`:\n\n1.  这段代码使用了一个定制的内存管理类`CBuffer`，以简化混合精度和复制的处理，但是它可以使用统一内存来代替。对于 cuBLAS 操作，我们应该在代码中包含`cublas_v2.h`:\n\n```cpp\n#include <cublas_v2.h>\n#include \"helper.cuh\"    // for CBuffer and printMatrix()\n```\n\n2.  现在，让我们实现`main()`功能。首先，我们将创建并初始化`A`、`B`和`C`矩阵。以下代码片段显示了如何使用`CBuffer`类并初始化矩阵:\n\n```cpp\nint M = 4, N = 5, K = 6;\nCBuffer<half> A, B;\nCBuffer<float> C;\n\nA.init(K * M, true);\nB.init(N * K, true);\nC.init(N * M, true);\n```\n\n3.  要指定`A`、`B`、`C`的精度类型，并一起测试各种精度，需要指定一些 CUDA 数据类型参数:\n\n```cpp\ncudaDataType TYPE_A, TYPE_B, TYPE_C;\nif (typeid(*A.h_ptr_) == typeid(float)) {\n    TYPE_A = TYPE_B = CUDA_R_32F;\n}\nelse if (typeid(*A.h_ptr_) == typeid(half)) {\n    TYPE_A = TYPE_B = CUDA_R_16F;\n}\nelse if (typeid(*A.h_ptr_) == typeid(int8_t)) {\n    TYPE_A = TYPE_B = CUDA_R_8I;\n}\nelse {\n    printf(\"Not supported precision\\n\");\n    return -1;\n}\n\nif (typeid(*C.h_ptr_) == typeid(float)) {\n    TYPE_C = CUDA_R_32F;\n}\nelse if (typeid(*C.h_ptr_) == typeid(int)) {\n    TYPE_C = CUDA_R_32I;\n}\nelse {\n    printf(\"Not supported precision\\n\");\n    return -1;\n}\n```\n\n4.  对于 cuBLAS 操作，我们应该初始化`cublas_handle`、`alpha`和`beta`:\n\n```cpp\nfloat alpha = 1.f, beta = 0.f;\ncublasHandle_t cublas_handle;\ncublasCreate(&cublas_handle);\n```\n\n5.  然后，我们将数据复制到 GPU:\n\n```cpp\nA.cuda(true);\nB.cuda(true);\nC.cuda(true);\n```\n\n6.  然后我们调用`cublasGemmEx()`函数如下:\n\n```cpp\ncublasGemmEx(cublas_handle,\n                CUBLAS_OP_N, CUBLAS_OP_N,\n                M, N, K,\n                &alpha, A.d_ptr_, TYPE_A, M, B.d_ptr_, TYPE_B, K,\n                &beta,  C.d_ptr_, TYPE_C, M, TYPE_C,\n                CUBLAS_GEMM_DEFAULT);\n```\n\n7.  要查看矩阵值，我们可以使用`printMatrix()`，它在`helper.h`中定义:\n\n```cpp\nstd::cout << \"A:\" << std::endl;\nprintMatrix(A.h_ptr_, K, M);\nstd::cout << \"B:\" << std::endl;\nprintMatrix(B.h_ptr_, N, K);\n```\n\n8.  使用函数覆盖方法定义`printMatrix()`，以允许在其他数据类型中打印具有相同格式的半精度值。部分定义如下:\n\n```cpp\ntemplate <typename T>\nvoid printMatrix(const T *matrix, const int ldm, const int n) {\n    std::cout << \"[\" << __FUNCTION__ << \"]:: \n                Not supported type request\" << std::endl;\n}\nvoid printMatrix(const float *matrix, const int ldm, const int n) {\n    for (int j = 0; j < n; j++) {\n        for (int i = 0; i < ldm; i++)\n            std::cout << std::fixed << std::setw(8) << \n                         std::setprecision(4) << \n                         matrix[IDX2C(i, j, ldm)];\n        std::cout << std::endl;\n    }\n}\nvoid printMatrix(const half *matrix, const int ldm, const int n) {\n    for (int j = 0; j < n; j++) {\n        for (int i = 0; i < ldm; i++)\n            std::cout << std::fixed << std::setw(8) <<\n                         std::setprecision(4) << __half2float(matrix[IDX2C(i, j, ldm)]);\n        std::cout << std::endl;\n    }\n}\n... ( functions for other data types ) ...\n```\n\n9.  然后，代码将对给定的`A`、`B`和`C`矩阵进行 GEMM 运算。以下是当`M`为`4`、`N`为`5`、`M`为`6`时的输出示例:\n\n```cpp\n$ nvcc -run -m64 -std=c++ 11 -I/usr/local/cuda/samples/common/inc -gencode arch=compute_70,code=sm_70 -lcublas -o cublasGemmEx ./cublasGemmEx.cu\nA:\n 0.0049 0.0479 0.0023 0.0775 0.0279 0.0817\n 0.0373 0.0625 0.0264 0.0924 0.0584 0.0853\n 0.0719 0.0505 0.0540 0.0987 0.0657 0.0052\n 0.0682 0.0781 0.0599 0.0896 0.0048 0.0796\nB:\n 0.0624 0.0965 0.0545 0.0223 0.0861\n 0.0594 0.0019 0.0643 0.0920 0.0537\n 0.0145 0.0376 0.0172 0.0221 0.0881\n 0.0285 0.0319 0.0161 0.0677 0.0235\n 0.0814 0.0695 0.0414 0.0392 0.0296\n 0.0446 0.0688 0.0403 0.0018 0.0971\nC:\n 0.0509 0.0117 0.0877 0.0445 0.0830\n 0.0742 0.0242 0.0136 0.0625 0.0681\n 0.0362 0.0046 0.0265 0.0963 0.0638\n 0.0070 0.0446 0.0516 0.0194 0.0089\nC out:\n 0.0153 0.0228 0.0143 0.0292 0.0113\n 0.0200 0.0118 0.0214 0.0081 0.0138\n 0.0098 0.0168 0.0132 0.0199 0.0125\n 0.0269 0.0120 0.0222 0.0085 0.0228\n```\n\n现在，让我们尝试其他数据类型，看看`cublasGemmEx()`如何对给定的矩阵进行操作。提供的示例还输出操作的执行时间来衡量性能:\n\n*   如果矩阵 *A* 或者矩阵 *B* 是转置矩阵，应该修改什么？\n*   有没有比手术更好的矩阵尺寸？通过更改大小来比较执行时间。\n*   每种数据类型有没有更好的矩阵大小？如果你尝试`INT8`精度，你会看到错误。这怎么解决？改变尺寸，看看如何在`cublasGemmEx()`支持`INT8`操作。\n\n# 带 sensorcore 的 gemm\n\n张量核心提供张量点运算的加速性能。支持 Volta 架构的 FP16，图灵架构的`INT8`和`INT4`。因此，我们应该使用缩减精度或混合精度来使用 TensorCore。\n\n之前我们使用`CUBLAS_GEMM_DEFAULT`作为 cuBLAS GEMM 算法，在运算中使用 CUDA 核。要用 TensorCore，就要用`CUBLAS_GEMM_DEFAULT_TENSOR_OP`。为了利用张量核心，操作数矩阵的每个维度都应该是 4 的倍数。这是天梭内部 **WMMA** (简称，**翘曲矩阵乘积**)运营优化的单位规模。例如，用 *A* (8，192 × 8，192)和 *B* (8，192 × 8，192)进行的矩阵-矩阵乘法，与用 *A* (8，192 × 8，192)和 *B* (8，192 × 8，190)进行的运算相比，表现出高得多的性能。您也可以通过配置文件确认此操作。\n\n以下时间线是使用矩阵 *A* (8，192 × 8，192)和矩阵 *B* (8，192 × 8，190)进行矩阵乘法的结果:\n\n![](img/ceedb286-f967-4a78-b6d1-31340f23ae90.png)\n\n此外，该时间线图像是矩阵 *A* (8，192 × 8，192)和矩阵 *B* (8，192 × 8，192)的矩阵相乘的结果:\n\n![](img/3430e8a9-267e-4233-9876-ee4deb8c06af.png)\n\n两个测试都在 CUDA C/C++ 中使用`CUBLAS_GEMM_DEFAULT_TENSOR_OP`，但是使用 TensorCore 的 GEMM 运算速度比使用 CUDA 内核快 6.7 倍。由于 TensorCore 基于矩阵大小可用，`nvcc`用特殊的内核函数编译代码，从`volta_s884g`开始。总之，如果你想获得张量核心的好处，把你的矩阵填充到 4。这可能是一个开销，但是来自 TensorCore 的性能提升可能会超过这个开销。\n\n英伟达在其开发博客网站([https://devblogs.nvidia.com/programming-tensor-cores-cuda-9](https://devblogs.nvidia.com/programming-tensor-cores-cuda-9))中提供了如何使用 cuBLAS 库对 TensorCores 进行编程。本文档还介绍了其他可用的方法。但是，使用 cuBLAS 库可以为您提供最快的性能，正如橡树岭国家实验室的一篇论文所证明的那样——*NVIDIA Tensor Core professional、**Performance and Precision*([https://arxiv.org/pdf/1803.04014.pdf](https://arxiv.org/pdf/1803.04014.pdf))。\n\n# 并行随机数生成的 cuRAND\n\n许多应用使用伪随机数进行模拟或概率分析。尽管有传统的用法，大量的随机数生成过程花费了大量的时间。一种解决方案是并行生成随机数，但是每个多线程应该有不同的随机种子，以便独立生成随机数。\n\n库兰德库使图形处理器能够从图形处理器生成许多随机数。该库可从主机或设备代码获得。主机应用编程接口只允许使用主机代码生成随机数。因此，您可以将生成的数据直接用于其他内核函数。设备应用编程接口支持在内核代码中生成随机数，因此您可以让 CUDA 线程在执行过程中拥有自己的随机生成的数字。\n\n# 库和主机应用编程接口\n\n首先，您需要使用`curandGenerator()`创建所需类型的新生成器。然后，设置所需种子和顺序的生成器选项。例如，您可以使用`curandSetPseudoRandomGeneratorSeed()`生成伪随机生成器:\n\n```cpp\ncurandGenerator_t curand_gen;\ncurandCreateGenerator(&curand_gen, CURAND_RNG_PSEUDO_DEFAULT);\ncurandSetPseudoRandomGeneratorSeed(curand_gen, 2019UL);\n```\n\n然后，可以使用`curandGenerate()`生成随机数。有九种不同的生成函数。例如，您可以使用`curandGenerateUnifrom()`生成均匀分布的浮点值:\n\n```cpp\ncurandGenerateUniform(curand_gen, random_numbers_on_device_memory, length);\n```\n\nThe cuRAND programming guide provides descriptions of various kinds of generation functions: [https://docs.nvidia.com/cuda/curand/host-api-overview.html#generation-functions](https://docs.nvidia.com/cuda/curand/host-api-overview.html#generation-functions).\n\n使用随机数生成后，您可以使用毁灭者功能终止 cuRAND 生成器:\n\n```cpp\ncurandDestroyGenerator(curand_gen);\n```\n\n现在，让我们实现一个使用几个 cuRAND APIs 生成随机数的应用。完全实现的版本是`03_curand/curand_host.cpp`。因此，您可以根据需要修改代码并测试其他功能。\n\n首先，我们应该为 cuRAND 主机 API 和其他与 CPP 相关的头文件包含`curand.h`，如下所示:\n\n```cpp\n#include <iostream>\n#include <iomanip>\n#include <curand.h>\n```\n\n假设我们将创建一个用随机数初始化的矩阵。我们需要实现`printMatrix()`功能，以便对生成的随机数进行如下检查:\n\n```cpp\n#define IDX2C(i, j, ld) (((j) * (ld)) + (i))\n\ntemplate <typename T>\nvoid printMatrix(const T *matrix, const int ldm, const int n)\n{\n    for (int j = 0; j < ldm; j++) {\n        for (int i = 0; i < n; i++)\n            std::cout << std::fixed << std::setw(12) \n                    << std::setprecision(4) << matrix[IDX2C(i, j, ldm)];\n        std::cout << std::endl;\n    }\n}\n```\n\n然后，我们将按如下方式分配所需的内存空间。现在，我们将实现`main()`函数，该函数使用`printMatrix()`初始化随机数并打印结果。首先，我们将如下初始化操作的 cuRAND 句柄:\n\n```cpp\n// create curand generator & set random seed\ncurandGenerator_t curand_gen;\ncurandCreateGenerator(&curand_gen, CURAND_RNG_PSEUDO_DEFAULT);\ncurandSetPseudoRandomGeneratorSeed(curand_gen, 2019UL);\n```\n\n您可以根据需要更改随机种子。接下来就是分配内存空间。为了便于评估操作，我们将使用统一的内存，因为 cuRAND 函数将在 GPU 上生成随机数:\n\n```cpp\nsize_t size = M * N;\nunsigned int *np_random;\nfloat *fp_random;\ncudaMallocManaged((void**)&np_random, sizeof(*np_random) * size);\ncudaMallocManaged((void**)&fp_random, sizeof(*fp_random) * size);\n```\n\n接下来，我们将生成给定内存空间的随机数。我们将使用整数存储空间(`np_random`)来生成随机数，使用浮动存储空间(`fp_random`)来生成均匀分布的随机数，如下所示:\n\n```cpp\n// random number generation\nstd::cout << \"Generated random numbers\" << std::endl;\ncurandGenerate(curand_gen, np_random, size);\ncudaDeviceSynchronize();\nprintMatrix(np_random, M, N);\n\n// uniform distributed random number generation\nstd::cout << \"Generated uniform random numbers\" << std::endl;\ncurandGenerateUniform(curand_gen, fp_random, size);\ncudaDeviceSynchronize();\nprintMatrix(fp_random, M, N);\n```\n\n因为我们使用的是统一的内存，所以我们可以允许 GPU 和主机共享同一个内存地址，我们可以通过同步它们来查看输出值。最后，我们可以终止 cuRAND 句柄和内存，如下所示:\n\n```cpp\n// terminates used resources\ncurandDestroyGenerator(curand_gen);\ncudaFree(np_random);\ncudaFree(fp_random);\n```\n\n现在，是时候编译和运行代码了。使用 cuRAND APIs 编译代码应该为`nvcc`编译器提供`-lcurand`。当`M = 3`和`N = 5`时，输出如下:\n\n```cpp\n$ nvcc -run -gencode arch=compute_70,code=sm_70 -lcurand -o curand_host curand_host.cpp\nGenerated random numbers\n 3395652512 793372546 2133571103 595847267 2461872808\n 595847267 2461872808 500895635 498154070 2385617847\n 498154070 2385617847 196336856 388563169 745758309\nGenerated uniform random numbers\n 0.7134 0.0830 0.1458 0.2099 0.6066\n 0.2099 0.6066 0.3078 0.5122 0.8856\n 0.5122 0.8856 0.3530 0.8477 0.8370\n```\n\n我们已经介绍了 CUDA 如何使用主机 API 生成随机数，但是，在某些情况下，最好设计 CUDA 内核来生成随机数。我们称之为设备 API，我们可以从每个 CUDA 线程中获取随机数。\n\n# 库和设备应用编程接口\n\n使用设备应用编程接口，我们可以设置生成器种子，并在您的 CUDA 设备上生成随机数。\n\n首先，我们需要准备一个`curandState_t`的设备存储空间，以便存储生成器种子，将随机种子并行提供给 CUDA 线程。这可以像正常的设备内存分配代码一样完成，如下所示:\n\n```cpp\ncudaMalloc((void **)&devStates, length * sizeof(curandState_t));\n```\n\n在你的内核代码中，我们需要使用`curand_init()`初始化随机种子。该函数需要种子、序列号和偏移量。然后，该函数设置状态。对于相同的种子，cuFFT 总是生成相同的状态。要生成随机值，使用`curand()`功能。和主机的生成函数一样，设备 API 也有各种生成函数。例如，均匀分布的随机数生成可以这样完成:\n\n```cpp\nint idx = blockIdx.x * blockDim.x + threadIdx.x;\ncurand_init(2019UL, idx, 0, &state[idx]);\ngenerated_out[idx] = curand_uniform(&state[idx]);\n```\n\ncuRAND 库为各种数据类型和随机分布提供了各种生成函数。要找到您想要的生成函数，请查看《cuRAND 开发者指南》的设备 API 概述。随机数生成后，设备状态缓冲区应像正常内存一样终止，如下所示:\n\n```cpp\ncudaFree(devStates);\n```\n\n现在，我们将创建一个使用 cuRAND 设备 API 的应用。完全实现的代码是`curand_device.cu`，所以你也可以修改和测试代码。首先，我们应该将`curand_kernel.h`文件与其他 C++ 必需的头文件包括在内，如下所示:\n\n```cpp\n#include <iostream>\n#include <iomanip>\n#include <curand_kernel.h>\n```\n\n我们将编写`setup_kernel()`，为每个 CUDA 线程初始化一个随机种子，如下所示:\n\n```cpp\n  __global__ void setup_kernel(curandState_t *state)\n  {\n      int idx = blockIdx.x * blockDim.x + threadIdx.x;\n      // Each thread gets same seed, \n      // a different sequence number, no offset */\n      curand_init(2019UL, idx, 0, &state[idx]);\n  }\n```\n\n编写两个随机数生成函数:`generate_kernel()`和`generate_uniform_kernel()`。我们将使用均匀分布的随机数生成一个 32 位整数和一个浮点:\n\n```cpp\n__global__ void generate_kernel(unsigned int *generated_out, \n                                curandState_t *state)\n{\n    int idx = blockIdx.x * blockDim.x + threadIdx.x;\n    generated_out[idx] = curand(&state[idx]) & 0xFF;\n}\n\n__global__ void generate_uniform_kernel(float *generated_out, \n                                        curandState_t *state)\n{\n    int idx = blockIdx.x * blockDim.x + threadIdx.x;\n    generated_out[idx] = curand_uniform(&state[idx]);\n}\n```\n\n现在，我们将实现`main()`功能并初始化设备状态缓冲区:\n\n```cpp\ncudaMalloc((void **)&devStates, sizeof(curandState) * size);\nsetup_kernel<<<(size + BLOCK_DIM - 1) / BLOCK_DIM, BLOCK_DIM>>>(devStates);\n```\n\n然后，使用`generate_kernel()`生成随机数。为了方便起见，我们将使用统一内存作为空间，并验证主机的输出。之后，我们将打印出如下结果:\n\n```cpp\n// random number generation\nstd::cout << \"Generated random numbers\" << std::endl;\ncudaMallocManaged((void**)&np_random, sizeof(*np_random) * size);\ngenerate_kernel<<<(size + BLOCK_DIM - 1) / BLOCK_DIM, BLOCK_DIM>>>\n                (np_random, const_cast<curandState_t *>(devStates));\ncudaDeviceSynchronize();\nprintMatrix(np_random, M, N);\n```\n\n同样，我们将使用`generate_uniform_kernel()`创建均匀分布的随机数，如下所示:\n\n```cpp\n// uniform distributed random number generation\nstd::cout << \"Generated uniform random numbers\" << std::endl;\ncudaMallocManaged((void**)&fp_random, sizeof(*fp_random) * size);\ngenerate_uniform_kernel<<<(size + BLOCK_DIM - 1) / BLOCK_DIM, BLOCK_DIM>>>\n                (fp_random, const_cast<curandState_t *>(devStates));\ncudaDeviceSynchronize();\nprintMatrix(fp_random, M, N);\n```\n\n因为我们使用的是统一内存，所以我们可以允许 GPU 和主机共享同一个内存地址，我们可以通过同步它们来查看输出值。最后，我们可以终止 cuRAND 句柄和内存，如下所示:\n\n```cpp\n// terminates used resources\ncurandDestroyGenerator(curand_gen);\ncudaFree(np_random);\ncudaFree(fp_random);\n```\n\n现在，是时候编译和运行代码了。为了使用 cuRAND 编译代码，API 应该为`nvcc`编译器提供`-lcurand`。当`M`等于`3`、`N`等于`5`时，输出如下:\n\n```cpp\n$ nvcc -run -gencode arch=compute_70,code=sm_70 -lcurand -o curand_device curand_device.cpp\nGenerated random numbers\n 3395652512 793372546 2133571103 595847267 2461872808\n 595847267 2461872808 500895635 498154070 2385617847\n 498154070 2385617847 196336856 388563169 745758309\nGenerated uniform random numbers\n 0.8064 0.2783 0.2971 0.2386 0.7491\n 0.2386 0.7491 0.4782 0.1060 0.2922\n 0.1060 0.2922 0.1823 0.6199 0.9137\n```\n\n当您比较主机应用编程接口和设备应用编程接口的输出数字时，生成的随机数是相同的，而统一随机数不是。如果您在第二次生成随机数之前重置随机种子，就可以解决这个问题。\n\n# 混合精度库布拉斯 GEMM 库兰德\n\n之前，我们使用 C++ 随机数生成器来初始化 GEMM 运算的矩阵。一般来说，当我们想要生成随机数时，这个函数很方便。但是，您可能会发现，在最后一节中，这个函数花费了很长时间来生成大随机数。在本节中，我们将介绍 cuRAND API 如何与 cuBLAS GEMM 操作协同工作。完全实现的版本是`gemm_with_curand_host.cpp`文件。让我们看看这是如何实现的:\n\n1.  目前，我们在 cuRAND 库中没有低精度随机数生成器。此外，我们需要将半精度数字转换为浮点数，以便评估输出。由于这些原因，我们需要在 GPU 上创建如下类型转换函数:\n\n```cpp\nnamespace fp16{\n__global__ void float2half_kernel(half *out, float *in)\n{\n    int idx = blockIdx.x * blockDim.x + threadIdx.x;\n    out[idx] = __float2half(in[idx]);\n}\n\nvoid float2half(half *out, float *in, size_t length)\n{\n    float2half_kernel<<< (length + BLOCK_DIM - 1) / BLOCK_DIM, BLOCK_DIM >>>(out, in);\n}\n```\n\n2.  现在，我们将编写一个使用 cuRAND 主机 API 的随机数生成函数。正如我们之前所讨论的，当我们需要使用半精度数据时，我们应该将生成的随机数从浮点数转换为二分之一。该功能可以如下实现:\n\n```cpp\ntemplate <typename T>\ntypename std::enable_if<(std::is_same<T, float>::value), float>::type\n*curand(curandGenerator_t generator, size_t length)\n{\n    T *buffer = nullptr;\n    cudaMalloc((void **)&buffer, length * sizeof(float));\n    curandGenerateUniform(generator, buffer, length);\n    return buffer;\n}\ntemplate <typename T>\ntypename std::enable_if<std::is_same<T, half>::value, half>::type\n*curand(curandGenerator_t generator, size_t length)\n{\n    T *buffer = nullptr;\n    float *buffer_fp32;\n\n    cudaMalloc((void **)&buffer_fp32, length * sizeof(float));\n    curandGenerateUniform(generator, buffer_fp32, length);\n\n    cudaMalloc((void **)&buffer, length * sizeof(T));\n    fp16::float2half(buffer, buffer_fp32, length);\n    cudaFree(buffer_fp32);\n\n    return buffer;\n}\n```\n\n3.  在`main()`函数中定义一些控制 GEMM 运算的局部变量:\n\n```cpp\nvoid *d_A, *d_B, *d_C;\ncudaDataType AType, BType, CType, computeType;\nint M = 8192, N = 8192, K = 8192;\nfloat alpha = 1.f, beta = 1.f;\nstd::string precision = \"fp32\";\nbool tensor_core = true;\n```\n\n在这段代码中，我们确定了 GEMM 操作的大小、数据类型和操作类型。\n\n4.  现在，让我们创建输入缓冲区数组，并设置参数以及操作精度:\n\n```cpp\nif (precision == \"fp32\") {\n    auto *a = curand<float>(curand_gen, M * K);\n    auto *b = curand<float>(curand_gen, K * N);\n    auto *c = curand<float>(curand_gen, M * N);\n    AType = BType = CType = CUDA_R_32F;\n    computeType = CUDA_R_32F;\n    d_A = a, d_B = b, d_C = c;\n}\nelse if (precision == \"fp16\") {\n    auto *a = curand<half>(curand_gen, M * K);\n    auto *b = curand<half>(curand_gen, K * N);\n    auto *c = curand<float>(curand_gen, M * N);\n    AType = BType = CUDA_R_16F, CType = CUDA_R_32F;\n    computeType = CUDA_R_32F;\n    d_A = a, d_B = b, d_C = c;\n}\nelse {\n    exit(EXIT_FAILURE);\n}\n```\n\n5.  按如下方式创建 cuRAND 和 cuBLAS 句柄:\n\n```cpp\ncublasCreate(&cublas_handle);\ncurandCreateGenerator(&curand_gen, CURAND_RNG_PSEUDO_DEFAULT);\ncurandSetPseudoRandomGeneratorSeed(curand_gen, 2019UL);\n```\n\n6.  然后，我们应该确定使用张量核心的操作类型:\n\n```cpp\ncublasGemmAlgo_t gemm_algo = (tensor_core) ? \n                             CUBLAS_GEMM_DEFAULT_TENSOR_OP : CUBLAS_GEMM_DEFAULT;\n```\n\n7.  然后，我们可以调用`cublasGemmEx()`函数，该函数提供 FP32 和 FP16 操作，如下所示:\n\n```cpp\ncublasGemmEx(cublas_handle, CUBLAS_OP_N, CUBLAS_OP_N,\n             M, N, K,\n             &alpha, d_A, AType, M, d_B, BType, K,\n             &beta,  d_C, CType, M,\n             computeType, gemm_algo);\n```\n\n与之前的版本相比，GEMM 操作应该显示出类似的性能。但是，您可能会发现整个应用的速度得到了提高，因为 GPU 上的并行随机数生成比主机上的生成快得多。\n\n《cuRAND 开发者指南》将帮助您找到其他随机数生成器、选项和分布。本文件位于[https://docs.nvidia.com/pdf/CURAND_Library.pdf](https://docs.nvidia.com/pdf/CURAND_Library.pdf)。\n\n# 图形处理器中快速傅里叶变换的快速傅里叶变换\n\ncuFFT 库为 **FFT** (简称，**快速傅立叶变换**)算法提供 GPU 加速操作。程序员可以利用图形处理器的计算能力转换真实或复杂的数据，并对转换后的信号应用图形处理器内核操作。此外，支持的功能与 FFTW 库相匹配，因此我们可以将主机项目迁移到 GPU。\n\n为了处理快速傅立叶变换样本的尺寸信息，要求快速傅立叶变换相应地使用`cufftPlan1D()`、`cufftPlan2D()`或`cufftPlan3D()`创建一个平面处理。如果样本数据具有批处理和跨步布局，我们应该使用`cufftPlanMany()`。如果样本大小大于 4 GB，我们应该使用`64`作为计划函数的后缀来支持该大小。例如，`cufftPlanMany64()`在`cufftPlanMany()`功能上支持更大的样本。\n\ncuFFT 库支持多 GPU 操作。首先，您需要使用`cufftCreate()`创建一个空计划。然后，我们可以使用`cufftXtSetGPUs()`指定将要执行操作的 GPU 列表。之后，我们可以使用正常的计划生成功能来生成计划，我们之前已经介绍过了。下表显示了计划生成功能类别:\n\n|  | 基本计划 | 多图形处理器计划 |\n| 简单的计划 | `cufftPlan{1d,2d,3d}()` | `cufftMakePlan{1d,2d,3d}()` |\n| 高级数据布局 | `cufftPlanMany()` | `cufftMakePlanMany()` |\n| FP16 操作 | `cufftXtMakePlanMany()` |\n\n然后，您可以使用`cufftExec()`功能对样本数据进行正向(快速傅立叶变换)和反向(IFFT)。cuFFT 库提供了三种数据转换:复杂到复杂、真实到复杂和复杂到真实。其操作数据类型可以是浮点型或双精度型:\n\n| 变换方向 | 浮动 | 两倍 |\n| 复杂到复杂 | `cufftExecC2C()`\n`cufftXtExecDescriptorC2C()` | `cufftExecZ2Z()`\n`cufftXtExecDescriptorZ2Z()` |\n| 真实到复杂 | `cufftDExecR2C()`\n`cufftXtExecDescriptorR2C()` | `cufftExecD2Z()`\n`cufftXtExecDescriptorD2Z()` |\n| 复杂到真实 | `cufftExecC2R()`\n`cufftXtExecDescriptorC2R()` | `cufftExecZ2D()`\n`cufftXtExecDesciptorZ2D()` |\n| 全部 | `cufftXtExec()` / `cufftXtExecDesciptor()` |\n\ncuFFT 操作为*正向*或*反向*，该操作应与另一方向配对。\n\n在真实数据和复杂数据之间转换的函数，如`R2C`和`C2R`，在它们的函数名中有隐含的方向信息。此功能有助于您避免为了将真实域中的数据转换为复杂的数据类型而进行额外的操作。同时，您必须创建一个额外的计划，因为每个计划都有转型方向信息。\n\n另一方面，要提供复到复变换的变换方向信息，如`C2C`、`Z2Z`。对于反转操作，您不必创建另一个 cuFFT 句柄，因为计划应该是相同的数据类型操作。\n\n`cufftXtExec()`和`cufftXtExecDescriptor()`函数可以对任何给定的数据类型执行转换，因为当您创建 cuFFT 计划时，每个输入数据都应该提供它们的数据类型信息。\n\n# cuFFT 的基本用法\n\n现在让我们尝试使用 cuFFT。完全实现的版本是`04_cufft/cufft.1d.cpp`文件。让我们讨论一下这是如何实现的:\n\n1.  首先从一些头文件开始:C++、CUDA、cuRAND、cuFFT:\n\n```cpp\n#include <iostream>\n#include <iomanip>\n#include <cuda_runtime.h>\n#include <cufft.h>\n#include <curand.h>\n```\n\n2.  在这个快速傅立叶变换操作中，我们将有实数到复数和复数到实数的转换。因此，让我们声明一些自定义数据类型，`Real`和`Complex`，以简化代码。这可以通过以下方式实现:\n\n```cpp\ntypedef cufftReal Real;\ntypedef cufftComplex Complex;\n```\n\n3.  现在，让我们从`main()`功能开始。对于输入的样本数据，我们将使用统一内存，以便于主机和 GPU 之间的数据传输。转换后的数据只能在图形处理器上使用。因此，可以按如下方式分配内存空间:\n\n```cpp\ncudaMallocManaged((void**)&p_sample, sizeof(Real) * sample_size * batch_size);\ncudaMalloc((void**)&d_freq, sizeof(Complex) * sample_size * batch_size);\n```\n\n4.  然后，我们将使用 cuRAND 主机 API 初始化输入数据，如下所示:\n\n```cpp\ncurandGenerator_t curand_gen;\ncurandCreateGenerator(&curand_gen, CURAND_RNG_PSEUDO_DEFAULT);\ncurandSetPseudoRandomGeneratorSeed(curand_gen, 2019UL);\ncurandGenerateUniform(curand_gen, p_sample, sample_size * batch_size);\n```\n\n5.  并且，我们应该为正向和反向变换初始化 cuFFT 计划。由于它们具有不同的数据类型，我们应该分别为实数到复数和复数到实数的转换创建两个计划:\n\n```cpp\nint rank = 1;\nint stride_sample = 1, stride_freq = 1;\nint dist_sample = sample_size, dist_freq = sample_size / 2 + 1;\nint embed_sample[] = {0};\nint embed_freq[] = {0};\ncufftPlanMany(&plan_forward, rank, &sample_size,\n                             embed_sample, stride_sample, \n                             dist_sample, \n                             embed_freq, stride_freq, dist_freq,\n                             CUFFT_R2C, batch_size);\ncufftPlanMany(&plan_inverse, rank, &sample_size,\n                             embed_freq, stride_freq, dist_freq, \n                             embed_sample, stride_sample, \n                             dist_sample,\n                             CUFFT_C2R, batch_size);\n```\n\n6.  现在，我们可以使用给定的 cuFFT 计划进行正向或反向变换。为了度量执行时间，我们可以使用 CUDA 事件来包含这些操作:\n\n```cpp\ncufftExecR2C(plan_forward, p_sample, d_freq);\ncufftExecC2R(plan_inverse, d_freq, p_sample);\n\n```\n\n7.  然后，我们可以用以下命令编译代码:\n\n```cpp\n$ nvcc -run -gencode arch=compute_70,code=sm_70 -lcufft -lcurand -o cufft.1d cufft.1d.cpp\n```\n\n`cufft.1d`命令将报告每一步的变换时间，如下所示:\n\n```cpp\nFFT operation time for 1048576 elements with 512 batch..\nForward (ms): 21.5322\nInverse (ms): 21.4\n```\n\n# 混合精度 cuFFT\n\ncuFFT 库提供扩展的 CUDA 计算功能，例如 FP16 FFT 操作。完整版本是`cufft.half.cpp`文件。让我们讨论一下它的实现。\n\n在这段代码中，我们应该使用`cufftXtMakePlanMany()`进行计划创建，使用`cufftXtExec()`函数进行转换。`cufftXtMakePlanMany()`如果输入和输出数据类型是 FP16 或 FP32，则允许传递。此外，我们应该为正向和反向转换创建两个计划，以便涵盖实数到复数和复数到实数的转换。对于空 cuFFT 计划，`cufftXtMakePlanMany()`可以指定样本大小、输入数据格式和类型、批次大小等。例如，计划创建可以如下实现:\n\n```cpp\nint rank = 1;\nint stride_sample = 1, stride_freq = 1;\nlong long int dist_sample = sample_size, dist_freq = sample_size / 2 + 1;\nlong long embed_sample[] = {0};\nlong long embed_freq[] = {0};\nsize_t workSize = 0;\ncufftCreate(&plan_forward);\ncufftXtMakePlanMany(plan_forward, \n        rank, &sample_size, \n        embed_sample, stride_sample, dist_sample, CUDA_R_16F, \n        embed_freq, stride_freq, dist_freq, CUDA_C_16F, \n        batch_size, &workSize, CUDA_C_16F);\ncufftCreate(&plan_inverse);\ncufftXtMakePlanMany(plan_inverse,\n        rank, &sample_size,\n        embed_freq, stride_freq, dist_freq, CUDA_C_16F,\n        embed_sample, stride_sample, dist_sample, CUDA_R_16F,\n        batch_size, &workSize, CUDA_R_16F);\n```\n\n在这个实现中，我们还必须考虑是否以半精度提供输入数据。您可以使用主机随机函数，并将它们转换为半精度数据，但是，这段代码向您展示了如何将 cuRAND 主机 API 用于此目的，如下所示:\n\n```cpp\ntemplate <typename T>\ntypename std::enable_if<std::is_same<T, half>::value>::type\ncurand(curandGenerator_t generator, T *buffer, size_t length) {\n    float *buffer_fp32;\n\n    cudaMalloc((void **)&buffer_fp32, length * sizeof(float));\n    curandGenerateUniform(generator, buffer_fp32, length);\n\n    // convert generated single floating to half floating\n    fp16::float2half(buffer, buffer_fp32, length);\n    cudaFree(buffer_fp32);\n}\n```\n\n因此，我们可以为快速傅立叶变换提供均匀分布随机数的半精度，并且我们可以使用`cufftXtExec()`进行正向和反向变换。转化表现如下:\n\n```cpp\n FFT operation time for 1048576 elements with 512 batch..\nForward (ms): 15.3236\nInverse (ms): 15.4881\n```\n\n# 多图形处理器的快速傅立叶变换\n\n快速傅立叶变换的另一种用法是使用多个图形处理器进行大型快速傅立叶变换操作。为此，我们必须使用`cufftCreate()`创建一个空的 cuFFT 计划，并使用`cufftXtSetGPUs()`提供 GPU 编号。例如，这可以按如下方式完成:\n\n```cpp\ncufftHandle cufft_plan;\nint n_gpu = 2, devices[2] = {0,1};\ncufftCreaet(&cufft_plan); // create an empty plan\ncufftXtSetGPUs(cufft_plan, n_gpu, devices); // set multi-gpu information\n```\n\n图形处理器的总数可能因系统而异。现在，我们可以使用`cufftXtMakePlanMany()`指定样本信息来生成 cuFFT 计划。例如，`cufftXtMakePlanMany()`可以这样称呼:\n\n```cpp\nsize_t *work_size = (size_t*) new size_t[num_gpus];\ncufftXtMakePlanMany(cufft_plan, 1 &sample_size, \n                    nullptr, 1, 1, CUDA_C_32F, \n                    nullptr, 1, 1,  CUDA_C_32F, \n                    batch_size, work_size, CUDA_C_32F);\n```\n\ncuFFT 库提供`cufftXtMalloc()`，为目标 GPU 准备 GPU 内存空间。然后，我们可以使用`cufftXtMemcpy()`功能将数据复制到分配的内存中。例如，这可以如下实现:\n\n```cpp\ncudaLibXtDesc *d_sample;\ncufftXtMalloc(cufft_plan, &d_sample, CUFFT_XT_FORMAT_INPLACE);\ncufftXtMemcpy(cufft_plan, d_sample, h_sample, CUFFT_COPY_HOST_TO_DEVICE);\n```\n\n然后，我们可以用`cufftXtExecDesciptor()`功能在多 GPU 上执行 FFT:\n\n```cpp\ncufftXtExecDesciptor(cufft_plan, d_sample, d_sample, CUFFT_FORWARD);\n```\n\n使用`nvidia-smi`，我们可以监控跨 GPU 的分布式内存分配和执行。根据您的图形处理器和系统配置，经过的时间可能不同。\n\n如果你想了解更多关于 cuFFT 库及其功能的知识，cuFFT 库用户指南([https://docs.nvidia.com/cuda/cufft/index.html](https://docs.nvidia.com/cuda/cufft/index.html))是你很好的参考。\n\nCUDA 示例代码是学习如何使用 cuFFT 函数的另一个很好的参考。样本代码放在`NVIDIA_CUDA-10.x_Samples/7_CUDALibraries/CUFFT*`目录中。您可以学习如何使用 CUDA 内核代码，并通过使用 cuFFT 的前向/后向转换来应用过滤器操作。\n\n# 用图形处理器进行图像和信号处理的 NPP\n\n**NPP** (简称，**NVIDIA Performance Primitive**)库是一个默认的 CUDA 库，拥有一组专注于成像和视频处理的 GPU 加速处理功能。虽然它支持这些领域的灵活开发，但开发人员可以节省他们的应用开发时间。\n\nNPP 库有两个功能部分:成像处理 API 和信号处理 API。图像处理应用编程接口包括与图像过滤、压缩/解压缩、颜色转换、调整大小、颜色转换、统计操作等相关的工具。信号处理应用编程接口是过滤、转换等等。您可以访问核电厂的文档([https://docs.nvidia.com/cuda/npp](https://docs.nvidia.com/cuda/npp))，并查看其配置和功能的完整列表。\n\nCUDA 提供了许多基于 NPP 的样本。在本节中，我们将介绍核电厂库的基本用途，并讨论其应用。\n\n# 用 NPP 进行图像处理\n\n首先，我们将介绍 NPP 库如何简化图像处理任务。在此之前，我们应该安装 FreeImage 库，以便能够轻松加载和写入 JPEG 压缩图像文件。有三个选项可用于准备库:\n\n1.  从 Ubuntu 档案安装:\n\n```cpp\n$ sudo apt-get install libfreeimage-dev\n```\n\n2.  从源代码构建并安装:\n\n```cpp\n$ wget http://downloads.sourceforge.net/freeimage/FreeImage3180.zip\n$ unzip FreeImage3180.zip\n$ cd FreeImage && make -j && sudo make install\n```\n\n3.  使用已经安装了 CUDA 工具包的库。CUDA 示例代码中的一个 NPP 示例代码`7_CUDALibraries/freeImageInteropNPP`使用了 FreeImage 库。对于本示例，NPP 头文件和库文件安装在 CUDA 示例目录中的`7_CUDALibrires/common/FreeImage`处。如果您不想在您的机器上安装其他二进制文件，您可以使用此选项。\n\n现在，让我们实现基于 NPP 的图像处理应用。完全实现的代码是`05_npp/imageFilter.cpp`。该文件以头文件开始:\n\n```cpp\n#include <iostream>\n#include <iomanip>\n#include <cassert>\n#include <cstring>\n#include <cuda_runtime.h>\n#include <npp.h>\n#include <FreeImage.h>\n#include <helper_timer.h>\n```\n\n在该应用中，它具有`ImageInfo_t`结构，以便于管理图像信息和数据:\n\n```cpp\nstruct ImageInfo_t\n{\n    /* image information */\n    FIBITMAP* dib; // FreeImage bitmap\n    int nHeight;   // image height size\n    int nWidth;    // image width size\n    int nPitch;    // image pitch size\n    int nBPP;      // Bit Per Pixel (i.e. 24 for BGR color)\n    int nChannel;  // number of channels \n    BYTE* pData;   // bytes from freeimage library\n\n    /* CUDA */\n    Npp8u *pDataCUDA; // CUDA global memory for nppi processing\n    int nPitchCUDA;   // image pitch size on CUDA device\n};\n```\n\n编写`LoadImage()`函数，加载一个 JPEG 图像。`FreeImage`库支持任何其他图像格式，可以随意尝试其他图像。然后，我们将用加载的图像数据填充源图像信息管理结构。`loadImage()`功能实现如下:\n\n```cpp\nvoid LoadImage(const char *szInputFile, ImageInfo_t &srcImage) {\n    FIBITMAP *pSrcImageBitmap = FreeImage_Load(FIF_JPEG, szInputFile, JPEG_DEFAULT);\n    if (!pSrcImageBitmap) {\n        std::cout << \"Couldn't load \" << szInputFile << std::endl;\n        FreeImage_DeInitialise();\n        exit(1);\n    }\n\n    srcImage.dib = pSrcImageBitmap;\n    srcImage.nWidth = FreeImage_GetWidth(pSrcImageBitmap);\n    srcImage.nHeight = FreeImage_GetHeight(pSrcImageBitmap);\n    srcImage.nPitch = FreeImage_GetPitch(pSrcImageBitmap);\n    srcImage.nBPP = FreeImage_GetBPP(pSrcImageBitmap);\n    srcImage.pData = FreeImage_GetBits(pSrcImageBitmap);\n    assert(srcImage.nBPP == (unsigned int)24); // BGR color image\n    srcImage.nChannel = 3;\n}\n```\n\n然后，编写一些 NPPI 助手函数，从图像结构中提供 NPPI 图像大小和 NPPI 感兴趣区域大小数据，如下所示:\n\n```cpp\nNppiSize GetImageSize(ImageInfo_t imageInfo)\n{\n    NppiSize imageSize;\n\n    imageSize.width = imageInfo.nWidth;\n    imageSize.height = imageInfo.nHeight;\n\n    return imageSize;\n}\n\nNppiRect GetROI(ImageInfo_t imageInfo)\n{\n    NppiRect imageROI;\n\n    imageROI.x = 0;    imageROI.y = 0;\n    imageROI.width = imageInfo.nWidth;\n    imageROI.height = imageInfo.nHeight;\n\n    return imageROI;\n}\n```\n\n然后，让我们实现基于 NPPI 的图像大小调整功能如下。在这个函数中，我们将使用`nppiResize_8u_C3R()`，这在开始时已经讨论过了。NPP APIs 有命名约定规则来明确说明它们的操作。根据它们的功能类别，它们的命名以`nppi`开始进行图像处理，以`npps` 开始进行信号处理。例如，NPP 图像处理函数`nppiResize_8u_C3R()`以`nppi`前缀开头，它将三个通道中无符号字符数据类型的输入数据调整到给定的 ROI(您可以在文档中了解有关该约定的更多详细信息):\n\n```cpp\nint ResizeGPU(ImageInfo_t &dstImage, ImageInfo_t &srcImage, \n                 NppiSize &dstSize, NppiRect &dstROI, \n                 NppiSize &srcSize, NppiRect &srcROI, scale)\n{\n    // update output image size\n    dstSize.width = dstROI.width = dstImage.nWidth;\n    dstSize.height = dstROI.height = dstImage.nHeight;\n\n    nppiResize_8u_C3R(srcImage.pDataCUDA, srcImage.nPitchCUDA, \n                      srcSize, srcROI, \n                      dstImage.pDataCUDA, dstImage.nPitchCUDA, \n                      dstSize, dstROI,\n                      NPPI_INTER_LANCZOS);\n    return 0;\n}\n```\n\n为了将性能与中央处理器进行比较，我们将使用 FreeImage 的函数，如下所示:\n\n```cpp\nvoid ResizeCPU(const char* szInputFile, ImageInfo_t &dstImage) {\n    FreeImage_Rescale(dib, dstImage.nWidth, dstImage.nHeight, FILTER_LANCZOS3);\n}\n```\n\n现在，让我们实现`main()`功能。首先，我们应该初始化 FreeImage 库并加载一个映像:\n\n```cpp\nFreeImage_Initialise();\nImageInfo_t srcImage, dstImage;\nLoadImage(szInputFile, srcImage);\n```\n\n然后，我们将初始化输入图像的 GPU 内存空间，如下所示。在此过程中，我们使用 NPPI 函数初始化全局内存空间，并使用`cudaMemcpy2D()`将加载的图像传输到全局内存中:\n\n```cpp\n// copy loaded image to the device memory\nsrcImage.pDataCUDA = \n             nppiMalloc_8u_C3(srcImage.nWidth, srcImage.nHeight, \n                              &srcImage.nPitchCUDA);\ncudaMemcpy2D(srcImage.pDataCUDA, srcImage.nPitchCUDA, \n             srcImage.pData, srcImage.nPitch, \n             srcImage.nWidth * srcImage.nChannel * sizeof(Npp8u), \n             srcImage.nHeight,\n             cudaMemcpyHostToDevice);\n```\n\n之后，我们将使用调整后的图像大小信息初始化输出内存空间，如下所示:\n\n```cpp\nstd::memcpy(&dstImage, &srcImage, sizeof(ImageInfo_t));\ndstImage.nWidth *= scaleRatio;\nsrcImage.nHeight *= scaleRatio;\ndstImage.pDataCUDA = \n                nppiMalloc_8u_C3(dstImage.nWidth, dstImage.nHeight, \n                                 &dstImage.nPitchCUDA);\n```\n\n然后，我们调用`ResizeGPU()`和`ResizeCPU()`函数，这些函数我们已经实现了。对于每个操作，我们将使用`cudaEvent`来测量 GPU 上的执行时间:\n\n```cpp\nRunNppResize(dstImage, srcImage, dstImageSize, dstROI, srcImageSize, srcROI, scaleRatio);\nRunCpuResize(szInputFile, dstImage);\n```\n\n为了验证，我们将结果保存到文件中。为此，我们应该创建一个 FreeImage 位图，并将调整后的图像复制到内存空间中。然后，我们可以保存一个输出图像，如下所示:\n\n```cpp\n// Save resized image as file from the device\nFIBITMAP *pDstImageBitmap = \n                FreeImage_Allocate(dstImage.nWidth, dstImage.nHeight, \n                                   dstImage.nBPP);\n\ndstImage.nPitch = FreeImage_GetPitch(pDstImageBitmap);\ndstImage.pData = FreeImage_GetBits(pDstImageBitmap);\n\ncudaMemcpy2D(dstImage.pData, dstImage.nPitch, \n             dstImage.pDataCUDA, dstImage.nPitchCUDA, \n             dstImage.nWidth * dstImage.nChannel * sizeof(Npp8u),\n             dstImage.nHeight, cudaMemcpyDeviceToHost);\n\nFreeImage_Save(FIF_JPEG, pDstImageBitmap, szOutputFile, JPEG_DEFAULT);\n```\n\n之后，我们终于可以终止相关资源:\n\n```cpp\nnppiFree(srcImage.pDataCUDA);\nnppiFree(dstImage.pDataCUDA);\n\nFreeImage_DeInitialise();\n```\n\n使用链接的 NPP 和 FreeImage 库使用`nvcc`编译代码:\n\n```cpp\n$ nvcc -run -m64 -std=c++ 11 -I/usr/local/cuda/samples/common/inc -gencode arch=compute_70,code=sm_70 -lnppc -lnppif -lnppisu -lnppig -lnppicom -lnpps -lfreeimage -o imageFilter ./imageFilter.cpp\n```\n\n因此，当比例因子为 0.5°f 时，图像尺寸减小如下:\n\n```cpp\n$ ls -alh *.jpg\n-rw-rw-r-- 1 ubuntu ubuntu 91K Nov 13 22:31 flower.jpg\n-rw-rw-r-- 1 ubuntu ubuntu 23K Nov 17 02:46 output.jpg\n```\n\n使用 V100 测量的经过时间为`0.04576 ms`。其时间可能因图形处理器而异:\n\n```cpp\nRescale flower.jpg in 0.5 ratio.\nCPU: 23.857 ms\nGPU: 0.04576 ms\nDone (generated output.jpg)\n```\n\n有关使用 NPP 进行图像处理的更多详细信息，请访问并查看链接文档:[http://ON-demand . gputechconf . com/GTC/2014/presentations/HANDS-ON-LAB-S4793-图像处理-使用-npp.pdf](http://on-demand.gputechconf.com/gtc/2014/presentations/HANDS-ON-LAB-S4793-image-processing-using-npp.pdf) 。\n\n# 用 NPP 进行信号处理\n\n核电厂还提供信号处理功能。与图像处理 API 的主要区别在于，它们不需要与图像形状相关的信息。随着我们继续介绍 NPP 函数的基本用法，我们将了解如何从给定的数组中获得和、最小值/最大值、平均值和 L2 归一化分布值。完整编写的代码是`05_npp/statisticsNPP.cpp`。\n\n首先，让我们从所需的头文件开始:\n\n```cpp\n#include <iostream>\n#include <cuda_runtime.h>\n#include <npp.h>\n```\n\n作为输入数据，我们将使用随机生成的数字:\n\n```cpp\nvoid GetData(float** buffer, size_t size)\n{\n    (*buffer) = (float*) new float[size];\n\n    for (int i = 0; i < size; i++) {\n        (*buffer)[i] = float(rand() % 0xFFFF) / RAND_MAX;\n    }\n}\n```\n\n在我们调用统计运算函数之前，我们需要一个临时的内存空间来进行它们的运算。我们可以使用与操作相关的其他 NPP 函数来获得所需的大小，并且我们可以创建一个公共的工作空间内存空间:\n\n```cpp\nint GetWorkspaceSize(int signalSize)\n{\n    int bufferSize, tempBufferSize;\n\n    nppsSumGetBufferSize_32f(signalSize, &tempBufferSize);\n    bufferSize = std::max(bufferSize, tempBufferSize);\n    nppsMinGetBufferSize_32f(signalSize, &tempBufferSize);\n    bufferSize = std::max(bufferSize, tempBufferSize);\n    nppsMaxGetBufferSize_32f(signalSize, &tempBufferSize);\n    bufferSize = std::max(bufferSize, tempBufferSize);\n    nppsMeanGetBufferSize_32f(signalSize, &tempBufferSize);\n    bufferSize = std::max(bufferSize, tempBufferSize);\n    nppsNormDiffL2GetBufferSize_32f(signalSize, &tempBufferSize);\n    bufferSize = std::max(bufferSize, tempBufferSize);\n\n    return bufferSize;\n}\n```\n\n让我们从`main()`功能开始。首先，我们将从输入数据准备开始，并了解所需的工作空间内存空间。我们将准备两种输入数据类型，并使用 NPP 比较它们的差异:\n\n```cpp\nGetData(&h_input1, buf_size);\nGetData(&h_input2, buf_size);\nworkspace_size = GetWorkspaceSize(buf_size);\n```\n\n之后，我们将为输入/输出和工作空间分配 GPU 内存空间。我们还将按如下方式传输输入数据:\n\n```cpp\ncudaMalloc((void **)&d_input1, buf_size * sizeof(float));\ncudaMalloc((void **)&d_input2, buf_size * sizeof(float));\ncudaMalloc((void **)&d_output, sizeof(float));\ncudaMalloc((void **)&d_workspace, workspace_size * sizeof(Npp8u));\n```\n\n现在，让我们使用 NPP 函数进行一些简单的统计操作:\n\n```cpp\nnppsSum_32f(d_input1, buf_size, d_output, d_workspace);\ncudaMemcpy(&h_output, d_output, sizeof(float), cudaMemcpyDeviceToHost);\nstd::cout << \"Sum: \" << h_output << std::endl;\n\nnppsMin_32f(d_input1, buf_size, d_output, d_workspace);\ncudaMemcpy(&h_output, d_output, sizeof(float), cudaMemcpyDeviceToHost);\nstd::cout << \"Min: \" << h_output << std::endl;\n\nnppsMax_32f(d_input1, buf_size, d_output, d_workspace);\ncudaMemcpy(&h_output, d_output, sizeof(float), cudaMemcpyDeviceToHost);\nstd::cout << \"Max: \" << h_output << std::endl;\n\nnppsMean_32f(d_input1, buf_size, d_output, d_workspace);\ncudaMemcpy(&h_output, d_output, sizeof(float), cudaMemcpyDeviceToHost);\nstd::cout << \"Mean: \" << h_output << std::endl;\n```\n\nNPP 还提供报告两个输入之间差异的功能，如下所示:\n\n```cpp\nnppsNormDiff_L2_32f(d_input1, d_input2, buf_size, d_output, d_workspace); \ncudaMemcpy(&h_output, d_output, sizeof(float), cudaMemcpyDeviceToHost);\nstd::cout << \"NormDiffL2: \" << h_output << std::endl;\n```\n\n然后，我们终止用过的记忆。之后，让我们用以下命令编译代码:\n\n```cpp\n$ nvcc -run -m64 -std=c++ 11 -I/usr/local/cuda/samples/common/inc -gencode arch=compute_70,code=sm_70 -lnppc -lnppif -lnppisu -lnppig -lnppicom -lnpps -o statisticsNPP ./statisticsNPP.cpp\n```\n\n结果，我们得到如下结果:\n\n```cpp\nSum: 0.00100016\nMin: 1.30432e-06\nMax: 3.04836e-05\nMean: 1.56275e-05\nNormDiffL2: 9.46941e-05\n```\n\n# 核电厂的应用\n\n在这一节中，我们已经讨论了图像处理中的滤波和信号处理中的统计运算。虽然我们已经尝试了简单的应用，但我们可能会发现 NPP 编程比内核实现容易得多。因此，NPP 被应用于许多媒体转码过滤器、洗浴图像处理应用、计算机视觉或深度学习中的图像预处理等。\n\n# 在 OpenCV 中编写 GPU 加速代码\n\nOpenCV 库是计算机视觉中相当受欢迎的库。它支持图形处理器编程，以提高计算机视觉领域的分辨率。在这一节中，我们将介绍如何使用带有 OpenGL 的 GPU。\n\n# 支持 CUDA 的 OpenCV 安装\n\n要用 CUDA 开始 OpenCV 编程，您需要在启用 CUDA 功能的情况下编译 OpenCV 库。按照以下步骤在 Ubuntu 中启用 OpenCV:\n\n```cpp\n$ sudo apt-get install -y --no-install-recommends \\\n cmake git libgtk2.0-dev pkg-config libavcodec-dev \\\n    libavformat-dev libswscale-dev \\\n libatlas-base-dev gfortran libeigen3-dev \\\n libgtkglext1 libgtkglext1-dev\n```\n\n如果您的系统可以使用 X 窗口(不是服务器)，请安装其他软件包以启用 GTK 对话框:\n\n```cpp\n$ sudo apt-get install -y —no-install-recommends \\\n Libgtkglext1 libgtkglext1-dev\n```\n\n下载源代码，并使用以下命令解压它们。这是用 OpenCV 测试的，在撰写本文时，OpenCV 是最新的版本:\n\n```cpp\n# We are install OpenCV 4.1.1\nOPENCV_VERSION=4.1.1\nOPENCV_DIR=opencv\n\n# Download OpenCV and contrib source codes\nmkdir -p ${OPENCV_DIR}\nwget -O ${OPENCV_DIR}/opencv-${OPENCV_VERSION}.tar.gz https://github.com/opencv/opencv/archive/${OPENCV_VERSION}.tar.gz\nwget -O ${OPENCV_DIR}/opencv_contrib-${OPENCV_VERSION}.tar.gz https://github.com/opencv/opencv_contrib/archive/${OPENCV_VERSION}.tar.gz\n\n# Untar the files\ntar -C ${OPENCV_DIR} -xzf ${OPENCV_DIR}/opencv-${OPENCV_VERSION}.tar.gz\ntar -C ${OPENCV_DIR} -xzf ${OPENCV_DIR}/opencv_contrib-${OPENCV_VERSION}.tar.gz\n```\n\n现在，让我们使用以下命令编译下载的源代码。如果你愿意，你可以放其他选项。它的编译需要一段时间:\n\n```cpp\n# Build the codes and install\ncd ${OPENCV_DIR}/opencv-${OPENCV_VERSION}\nmkdir build\ncd build\ncmake -D CMAKE_BUILD_TYPE=RELEASE \\\n -D CMAKE_INSTALL_PREFIX=/usr/local \\\n -D ENABLE_PRECOMPILED_HEADERS=OFF \\\n -D OPENCV_GENERATE_PKGCONFIG=ON \\\n -D WITH_CUDA=ON -D WITH_CUVID=OFF -D BUILD_opencv_cudacodec=OFF \\\n -D ENABLE_FAST_MATH=1 \\\n -D CUDA_FAST_MATH=1 \\\n -D OPENCV_EXTRA_MODULES_PATH=../../opencv_contrib-${OPENCV_VERSION}/modules \\\n -D WITH_CUBLAS=1 \\\n -D PYTHON_DEFAULT_EXECUTABLE=`which python3` \\\n -D INSTALL_PYTHON_EXAMPLES=ON \\\n -D BUILD_EXAMPLES=ON ..\nmake -j$(nproc)\nsudo make install -j$(nproc) \n```\n\n要确认安装，请使用以下命令:\n\n```cpp\n$ pkg-config —cflags opencv4 -I/usr/local/include/opencv4/opencv -I/usr/local/include/opencv4\n```\n\n在 OpenCV 4 中，CUDA 相关的函数和类是在 CUDA 命名空间中定义的。例如，您可以使用以下命令创建 CUDA 全局内存空间:\n\n```cpp\ncv::cuda::GpuMat cuda_mem = cv::cuda::GpuMat(src.rows, src.cols, CV_8UC1);\n```\n\n然后，设备`cuda_mem`内存空间可以像正常的 CPU 内存类型(`cv::Mat`)一样处理。\n\n# 实现启用 CUDA 的模糊过滤器\n\n现在，我们将实现一个支持 GPU 的小型 OpenCV 应用，并比较其性能。让我们从包含所需的头文件开始:\n\n```cpp\n#include <iostream>\n#include <string>\n#include \"opencv2/opencv.hpp\"\n```\n\n以下是使用 OpenCV 实现的主机模糊过滤器:\n\n```cpp\nvoid BlurHost(std::string filename)\n{\n    cv::Mat src = cv::imread(filename, 1);\n    cv::Mat dst; \n    cv::TickMeter tm;\n\n    tm.reset();\n    tm.start();\n    cv::bilateralFilter(src, dst, 10, 50, 50);\n    tm.stop();\n\n    std::cout << \"CPU Time: \" << tm.getTimeMilli() << \" ms.\" << std::endl;\n    cv::imwrite(\"result_host.jpg\", dst);\n}\n```\n\n这是启用 CUDA 的模糊过滤器实现:\n\n```cpp\nvoid BlurCuda(std::string filename)\n{\n    cv::Mat src = cv::imread(filename, 1);\n    cv::Mat dst;\n    cv::cuda::GpuMat src_cuda = cv::cuda::GpuMat(src.rows, \n                                                 src.cols, CV_8UC1);\n    cv::cuda::GpuMat dst_cuda = cv::cuda::GpuMat(src.rows, \n                                                 src.cols, CV_8UC1);\n    cv::TickMeter tm;\n\n    // warm-up\n    cv::cuda::bilateralFilter(src_cuda, dst_cuda, 10, 50, 50);\n\n    tm.reset();\n    tm.start();\n    src_cuda.upload(src);\n    cv::cuda::bilateralFilter(src_cuda, dst_cuda, 10, 50, 50);\n    dst_cuda.download(dst);\n    tm.stop();\n\n    std::cout << \"GPU Time: \" << tm.getTimeMilli() \n                  << \" ms.\" << std::endl;\n    cv::imwrite(\"result_cuda.jpg\", dst);\n}\n```\n\n这个收条代码展示了`bilateralFilter()`操作如何与主机匹配，以及 CUDA 如何与 CUDA 命名空间匹配。对于 CUDA 内存操作，设备内存使用`cv::cuda::GpuMat`，设备内存提供`upload()`和`download()`成员功能，如`cudaMemcpy()`。为了测量经过的时间，使用了`cv::TickMeter`。然后，`main()`调用两个实现，如下所示:\n\n```cpp\n  int main(int argc, char *argv[])\n  {\n      std::string filename(\"flower.JPG\");\n\n      BlurHost(filename);\n      BlurCuda(filename);\n\n      return 0;\n  }\n```\n\n现在，让我们编译代码。我们应该在你的编译选项中包含使用``pkg-config --cflag opencv``的 OpenCV 头文件和库。例如，编译选项可以这样编写:\n\n```cpp\n$ nvcc -run -m64 -gencode arch=compute_70,code=sm_70 -I/usr/local/cuda/samples/common/inc `pkg-config opencv4 --cflags --libs` -o blur ./blur.cpp\n```\n\n然后，输出结果如下:\n\n```cpp\nCPU Time: 57.6544 ms.\nGPU Time: 2.97784 ms.\n```\n\n根据您的系统和图形处理器，执行时间可能会有所不同。\n\n# 启用多流处理\n\n在 OpenCV 中，CUDA 流是用`cv::cuda::Stream`来管理的。利用这一点，我们可以进行基于多流的流水线 GPU 操作:\n\n1.  我们知道，主机内存应该是固定内存，以便进行异步数据传输:\n\n```cpp\nMat::setDefaultAllocator(cuda::HostMem::getAllocator(cuda::HostMem::PAGE_LOCKED));\n```\n\n2.  然后，我们将创建多个流，如下所示:\n\n```cpp\nconst int num_stream = 4;\ncuda::Stream stream[num_stream];\n```\n\n3.  并且，我们加载源图像，并基于加载的图像信息初始化 GPU 内存，如下所示:\n\n```cpp\nMat src = imread(filename, 1);\nMat dst;\ncuda::GpuMat src_cuda[num_stream], dst_cuda[num_stream];\nfor (int i = 0; i < num_stream; i++)\n    src_cuda[i] = cuda::GpuMat(src);\n```\n\n4.  现在，我们将图像传输到 GPU，模糊图像，并将其与每个流一起传输回主机:\n\n```cpp\nfor (int i = 0; i < num_stream; i++) {\n    src_cuda[i].upload(src, stream[i]);\n    cuda::bilateralFilter(src_cuda[i], dst_cuda[i], 21, 150.f, \n                          150.f, BORDER_DEFAULT, stream[i]);\n    dst_cuda[i].download(dst, stream[i]);\n}\n```\n\n5.  然后，我们要同步主机和 GPU。为此，我们将使用`cv::Stream.waitForCompletion()`功能，该功能可以在每个流完成向主机的数据传输后对其进行同步:\n\n```cpp\nfor (int i = 0; i < num_stream; i++)\n    stream[i].waitForCompletion();\n```\n\n6.  为了与中央处理器进行性能比较，我们也称`cv::bilateralFilter()`如下:\n\n```cpp\nbilateralFilter(src, dst, 21, 150, 150);\n```\n\n其执行时间如下。GPU 执行时间是从多流执行循环到同步的测量时间的平均值:\n\n```cpp\n$ nvcc -run -m64 -gencode arch=compute_70,code=sm_70 -I/usr/local/cuda/samples/common/inc `pkg-config opencv4 --cflags --libs` -o blur ./blur.cpp\nCPU Time: 84.8649 ms.\nGPU Time: 1.60979 ms.\n```\n\n7.  为了确认多流操作，我们可以对操作进行概要分析。下面的截图显示了这一点:\n\n![](img/aea5b6e4-ce11-4d84-9057-0c5466df0094.png)\n\nProfiling the operation\n\n默认流上的第一个操作是向上翘曲执行，然后是四多流操作。在这里，我们可以看到 GPU 操作是重叠的。因此，平均执行时间比单流执行要短。\n\n我们只在 OpenCV 中介绍了双边过滤。但是，OpenCV 的很多特性都支持 CUDA 加速，让你可以获得 GPU 计算的好处。它的接口与 CPU 版本一致，所以你可以轻松地将你的 CPU 版本迁移到 GPU。\n\n作为入门，有一些来自 GTC 的有用材料:\n\n*   [http://on-demand . gputechconf . com/GTC/2013/webinar/opencv-GTC-express-shalini-Gupta . pdf](http://on-demand.gputechconf.com/gtc/2013/webinar/opencv-gtc-express-shalini-gupta.pdf)\n*   [http://on-demand . gputechconf . com/GTC/2013/webinar/GTC-express-itseez-opencv-webinar . pdf](http://on-demand.gputechconf.com/gtc/2013/webinar/gtc-express-itseez-opencv-webinar.pdf)\n*   [http://developer.download.nvidia.com/GTC/PDF/1085_Fung.pdf](http://developer.download.nvidia.com/GTC/PDF/1085_Fung.pdf)\n\n建议从 OpenCV 的参考指南开始:[https://docs.opencv.org/4.1.1/d2/dbc/cuda_intro.html](https://docs.opencv.org/4.1.1/d2/dbc/cuda_intro.html)。\n\n# 编写可与 CUDA 一起使用的 Python 代码\n\n现在很多人用 CUDA 搭配 Python。它不仅作为二进制文件的粘合剂，还使我们能够直接编写 GPU 加速代码。作为一种粘合语言，Python 可以从 CUDA C/C++ 库中调用 API，使用`pybind11`([https://github.com/pybind/pybind11](https://github.com/pybind/pybind11))或 SWIG([http://swig.org/](http://swig.org/))。但是，我们必须编写 CUDA C/C++ 代码，并将其集成到 Python 应用中。\n\n然而，有一些 Python 包——Numba、CuPy 和 PyCUDA——支持使用 Python 进行 GPU 编程。它们为 CUDA 内核提供了本地加速的 API 和包装器。换句话说，我们不必编写 C/C++ 代码，也不必花费时间执行集成。Numba 提供了一个矢量化和 CUDA **准时制** ( **jit** )编译器来加速其运行。它与 NumPy 兼容，因此您可以基于 NumPy 加速您的数值计算代码。多亏了 jit 编译器，您还可以用 Python 编写灵活的 CUDA 代码。CuPy 也与 NumPy 兼容，并加速线性代数算法。它提供了 Pythonic 可编程性和透明的自定义内核编程，例如 Numba。PyCUDA 提供了一个 CUDA C/C++ 接口，这样就可以在 Python 代码中编写和使用 CUDA 内核函数。\n\n# numba–高性能 Python 编译器\n\nnumba([https://numba.pydata.org/](https://numba.pydata.org/))翻译 Python 函数在 GPU 上执行，无需任何 C/C++ 编程。\n\n在 Numba 中，您可以通过将 Numba 装饰器应用于目标函数来轻松编写矢量化函数:\n\n```cpp\nfrom numba import vectorize\n@vectorize([\"float32(float32, float32, float32)\"], target='cuda')\ndef saxpy(scala, a, b):\nreturn scala * a + b\n```\n\n如您所见，装饰器指定参数和返回数据类型，目标指定代码将操作哪个架构。有三种目标:\n\n| \n\n目标\n\n | \n\n描述\n\n | \n\n推荐的数据大小和操作\n\n |\n| `cuda` | 瞄准 NVIDIA GPU | 大于 1 MB，计算密集型操作 |\n| `parallel` | 针对多核中央处理器进行了优化 | 小于 1 MB，正常运行 |\n| `cpu` | 针对单线程操作进行了优化 | 小于 1 KB，低计算密集型操作 |\n\n如果您的函数没有返回值，请使用`@guvectorize`，并将参数指定为向量。\n\nNumba 的另一个用途是用于`@cuda.jit`装饰器。这使您能够编写 CUDA 特定的操作，如下所示:\n\n```cpp\nfrom numba import cuda\n\n@cuda.jit\ndef matmul(d_c, d_a, d_b):\n    x, y = cuda.grid(2)\n    if (x < d_c.shape[0] and y < d_c.shape[1]):\n        sum = 0\n        for k in range(d_a.shape[1]):\n            sum += d_a[x, k] * d_b[k, y]\n        d_c[x, y] = sum\n```\n\n`cuda.grid()`关键字提供网格级的 CUDA 线程索引，这样就可以用 Python 的方式编写内核代码，比如 CUDA C/C++ 代码。调用 CUDA 内核函数可以如下进行:\n\n```cpp\nmatmul[dimGrid, dimBlock](d_c, d_a, d_b)\n```\n\n现在，让我们安装这个包，并尝试一些例子。\n\n# 安装 Numba\n\n要在 Python 代码中使用 Numba，您需要安装软件包，并配置环境变量:\n\n```cpp\n$ pip3 install numba\n$ export NUMBAPRO_NVVM=/usr/local/cuda/nvvm/lib64/libnvvm.so\n$ export NUMBAPRO_LIBDEVICE=/usr/local/cuda/nvvm/libdevice/\n```\n\n为了便于将来使用，您需要将环境变量设置放在`.bashrc`或`.zshrc`的末尾。如果没有设置，Python 将返回以下消息:\n\n```cpp\nnumba.cuda.cudadrv.error.NvvmSupportError: libNVVM cannot be found. Do `conda install cudatoolkit`:\nlibrary nvvm not found\n```\n\n# 将 Numba 与@矢量化装饰器一起使用\n\n我们将用一个简单的`saxpy`操作来测试`@vectorize`装饰器。这将特定功能转换为并行工作:\n\n1.  创建`numba_saxpy.py`。\n2.  导入`numba`、`numpy`和任何其他所需的包:\n\n```cpp\nimport numpy as np\nfrom numba import vectorize\nfrom timeit import default_timer as timer\n```\n\n3.  用`@vectorize`装饰器写一个`saxpy`代码，用`'cuda'`打靶，以便在 CUDA 设备上工作:\n\n```cpp\n@vectorize([\"float32(float32, float32, float32)\"], target='cuda')\ndef saxpy_cuda(scala, a, b):\n    return scala * a + b\n```\n\n4.  用`@vecotrize`装饰器编写 saxpy 代码，用`'parallel'`作为目标，在多核处理器(主机)上工作:\n\n```cpp\n@vectorize([\"float32(float32, float32, float32)\"], target='parallel')\ndef saxpy_host(scala, a, b):\n    return scala * a + b\n```\n\n5.  编写一个操作代码，用一些 NumPy 生成的输入数据调用函数:\n\n```cpp\nscala = 2.0\nnp.random.seed(2019)\nprint(\"size \\t\\t CUDA \\t\\t CPU\")\nfor i in range(16,20):\n    N = 1 << i\n    a = np.random.rand(N).astype(np.float32)\n    b = np.random.rand(N).astype(np.float32)\n    c = np.zeros(N, dtype=np.float32)\n\n    # warm-up\n    c = saxpy_cuda(scala, a, b)\n\n    # measuring execution time\n    start = timer()\n    c = saxpy_host(scala, a, b)\n    elapsed_time_host= (timer() - start) * 1e3\n    start = timer()\n    c = saxpy_cuda(scala, a, b)\n    elapsed_time_cuda = (timer() - start) * 1e3\n    print(\"[%d]: \\t%.3f ms\\t %.3f ms\" % (N, elapsed_time_cuda, elapsed_time_host))\n```\n\n此代码报告不同操作数大小的运行时间:\n\n```cpp\nsize         CUDA        CPU\n[65536]:   1.174 ms    0.199 ms\n[131072]:  1.362 ms    0.201 ms\n[262144]:  2.240 ms    0.284 ms\n[524288]:  2.384 ms    0.337 ms\n```\n\n在这种情况下，CUDA 表现出比 CPU 更慢的性能，因为操作简单，但是数据传输开销很大。\n\n# 将 Numba 与@cuda.jit 装饰器一起使用\n\n我们还可以使用`@cuda.jit`装饰器编写复杂的操作来使用 Numba 在 GPU 上工作:\n\n1.  创建`numba_matmul.py`。\n2.  导入`numpy`、`numba`和任何其他所需的包:\n\n```cpp\nimport numpy as np\nfrom numba import cuda\nfrom timeit import default_timer as timer\n```\n\n3.  用`@cuda.jit`装饰器写一个矩阵乘法代码:\n\n```cpp\n@cuda.jit\ndef matmul(d_c, d_a, d_b):\n    x, y = cuda.grid(2)\n    if (x < d_c.shape[0] and y < d_c.shape[1]):\n        sum = 0\n        for k in range(d_a.shape[1]):\n            sum += d_a[x, k] * d_b[k, y]\n        d_c[x, y] = sum\n```\n\n在这段代码中，我们使用`cuda.grid(dimension_size)`来指定网格之间的 CUDA 线程索引，因此，我们可以在 Python 中指定 CUDA 线程的索引。\n\n4.  将`a`和`b`矩阵创建为数字矩阵:\n\n```cpp\nN = 8192\na = np.random.rand(N, N).astype(np.float32)\nb = np.random.rand(N, N).astype(np.float32)\n```\n\n5.  将 NumP 生成的数据复制到设备:\n\n```cpp\nd_a = cuda.to_device(a)\nd_b = cuda.to_device(b)\n```\n\n6.  创建将被放置在 CUDA 设备内存中的`c`矩阵:\n\n```cpp\nd_c = cuda.device_array((N, N))\n```\n\n7.  调用矩阵乘法内核函数:\n\n```cpp\nstart = timer()\nmatmul[dimGrid, dimBlock](d_c, d_a, d_b)\nelapsed_time_gpu = (timer() - start) * 1e3\n```\n\n8.  将输出复制到主机:\n\n```cpp\nc = d_c.copy_to_host()\n```\n\n9.  将 CUDA 操作与主机进行比较:\n\n```cpp\n# matrix multiplication (cpu)\nstart = timer()\nc_host = np.matmul(a, b)\nelapsed_time_cpu = (timer() - start) * 1e3\n\n# print elapse times\nprint(\"Elapsed Time\")\nprint(\"GPU: %.3f ms\" % elapsed_time_gpu)\nprint(\"CPU: %.3f ms\" % elapsed_time_cpu)\n\nif (np.allclose(c_host, c)):\nprint(\"Done.\")\nelse:\nprint(\"GPU and host results are mismatching.\")\n```\n\n通过一个`@cuda.jit`装饰器和内置的`cuda.grid()`关键字，这个示例代码展示了在 Python 中将 Numba 实现为矩阵乘法是多么简单。此代码报告设备和主机上的运行时间:\n\n```cpp\nElapsed Time\nGPU: 104.694 ms\nCPU: 1539.005 ms\nDone.\n```\n\n现在，让我们讨论 CuPy，它在 CUDA 编程中支持更多的 Pythonic 编程。\n\n# CuPy–GPU 加速的 Python 矩阵库\n\nCuPy([https://cupy.chainer.org](https://cupy.chainer.org))使用 Python 实现线性代数加速，使用 CUDA 库充分利用 GPU。它与 NumPy 兼容，并提供令人愉快的 Pythonic 可编程性。\n\n让我们介绍一下它的安装、基本用法和手动内核开发。\n\n# 安装 CuPy\n\n我们可以使用`pip`使用以下命令安装 CuPy。然后它还安装`cupy`包和 CUDA 依赖项:\n\n```cpp\n$ pip3 install cupy-cuda101    # CUDA 10.1\n$ pip3 install cupy-cuda101    # CUDA 10.0\n$ pip3 install cupy-cuda902    # CUDA 9.2\n```\n\n现在，让我们介绍一下 CuPy 的基本用法。\n\n# CuPy 的基本用法\n\n我们可以编写一个 saxpy 操作，如下所示:\n\n```cpp\n>>> x = cp.arange(5).astype('f') \n>>> x \narray([0., 1., 2., 3., 4.], dtype=float32) \n>>> y = cp.arange(5).astype('f') \n>>> 0.5 * x + y \narray([0\\. , 1.5, 3\\. , 4.5, 6\\. ], dtype=float32)\n```\n\n我们也可以使用`matmul()`函数进行矩阵乘法，如下所示:\n\n```cpp\n>>> x = cp.random.uniform(0, 1, (2, 4)).astype('float32') \n>>> y = cp.random.uniform(0, 1, (4, 2)).astype('float32') \n>>> cp.matmul(x, y)\narray([[0.6514087, 0.826463 ], \n [0.7826104, 0.2878886]], dtype=float32)\n```\n\n正如我们之前讨论的，CuPy 与 NumPy 兼容。基本上，前一个 CuPy 的对象是 CuPy 的数组类型:\n\n```cpp\n>>> x = cp.random.uniform(0, 1, (2, 4)).astype('float32') \n>>> type(x) \n<class 'cupy.core.core.ndarray'>\n```\n\n但是，我们可以使用`cupy.asnumpy()`函数将其转换为 NumPy 数组，如下所示:\n\n```cpp\ntype(cp.asnumpy(x))\n<class 'numpy.ndarray'>\n```\n\n使用`cupy.ascupy()`功能也可以进行反向操作。因此，基于这种兼容性，我们可以执行以下操作:\n\n```cpp\n>>> gpu = cp.random.uniform(0, 1, (2, 4)).astype('float32') \n>>> cpu = np.random.uniform(0, 1, (2, 4)).astype('float32')\n>>> gpu + cp.asarray(cpu) \narray([[0.8649391 , 1.1412742 , 1.1280626 , 0.38262686],\n [0.44767308, 0.738155 , 0.8397665 , 1.5165564 ]], dtype=float32)\n>>> cpu + cp.asnumpy(gpu) \narray([[0.8649391 , 1.1412742 , 1.1280626 , 0.38262686], \n [0.44767308, 0.738155 , 0.8397665 , 1.5165564 ]], dtype=float32)\n```\n\n如您所见，我们可以轻松切换目标计算流程，并且我们可以受益于每个平台的优势。现在，让我们用 CuPy 来介绍定制内核实现。\n\n# 实现自定义内核函数\n\nCuPy 提供了三种类型的定制内核函数:elementwise、reduction 和 raw 内核。elementwise 内核有助于每个元素的自动索引。因此，我们可以只写一个元素的操作。约简内核执行约简操作，同时也执行用户定义的操作。原始内核支持在 Python 代码上直接进行 CUDA C/C++ 内核编程，这样我们就可以在上面定义任何操作。在本节中，我们不会涵盖所有这些内容。但是，您可以从相关文档中了解更多信息—[https://docs-cupy . chainer . org/en/stable/tutorial/kernel . html](https://docs-cupy.chainer.org/en/stable/tutorial/kernel.html)。\n\n让我们讨论用户定义的 elementwise 内核实现。下面是 elementwise 操作的一个示例:\n\n```cpp\n>>> squared_diff = cp.ElementwiseKernel( \n...     'float32 x, float32 y', \n...     'float32 z', \n...     'z = (x - y) * (x - y)', \n...     'squared_diff')\n```\n\n然后，我们可以在没有显式索引操作的情况下执行 elementwise 操作:\n\n```cpp\n>>> x = cp.random.uniform(0, 1, (2, 4)).astype('float32') \n>>> y = cp.random.uniform(0, 1, (2, 4)).astype('float32') \n>>> squared_diff(x, y) \narray([[0.54103416, 0.01342529, 0.01425287, 0.67101586], \n [0.04841561, 0.09939388, 0.46790633, 0.00203693]], dtype=float32)\n>>> squared_diff(x, 0.5) \narray([[0.23652133, 0.22603741, 0.08065639, 0.00647551], \n [0.00029328, 0.07454127, 0.00666 , 0.18399356]], dtype=float32)\n```\n\n如您所见，CuPy 提供了一个高度 Pythonic 化的界面，并且易于学习。内部例程很多，也兼容 NumPy—[https://docs-cupy . chainer . org/en/stable/reference/routines . html](https://docs-cupy.chainer.org/en/stable/reference/routines.html)。换句话说，当我们在 NumPy 中需要加速计算时，我们可以考虑使用 CuPy。\n\n现在，我们将介绍 PyCUDA，它提供直接内核编程和隐式内存管理包装。\n\n# PyCUDA–python 对 CUDA 应用编程接口的访问\n\nPyCUDA([https://documen.tician.de/pycuda/](https://documen.tician.de/pycuda/))让我们可以用 Python 代码编写 CUDA C/C++ 代码，不需要编译就可以执行。这样，您就可以编写 CUDA 特定操作的 CUDA C/C++ 代码。但是，您必须自己优化这段代码，因为 PyCUDA 不会优化您的内核函数。\n\n这是使用 PyCUDA 生成的一段代码:\n\n```cpp\nimport pycuda.autoinit     # initialize CUDA devices\nfrom pycuda import driver, compiler, gpuarray\nfrom string import Template\n\nkernel_code_template = Template(\"\"\"\n__global__ void matmul_kernel(float *d_C, float *d_A, float *d_B)\n{\n    int idx_x = blockIdx.x * blockDim.x + threadIdx.x;\n    ...\n}\n\"\"\")\n\nmod = compiler.SourceModule(kernel_code_template.substitute(MATRIX_SIZE=N))\nmatmul_kernel = mod.get_function(\"matmul_kernel\")\nmatmul_kernel(driver.Out(C), driver.In(A), driver.In(B), block=(dimBlock, dimBlock, 1), grid=(dimGrid, dimGrid))\n```\n\n正如您在这段代码中看到的，我们可以使用相同的 Python 代码编写内核代码。我们还可以使用`driver.In()`和`driver.Out()`保留所需数据传输的迹象。这些表明 PyCUDA 应该在调用内核之前传输数据。数据会自动传输，我们还可以按如下方式传输数据:\n\n```cpp\nd_A = driver.to_device(A) # cudaMemcpyHostToDevice\nA = driver.from_device_like(d_A) # cudaMemcpyDeviceToHost\n```\n\n现在，让我们安装 PyCUDA 并尝试一些简单的示例。\n\n# 安装 PyCUDA\n\n要使用 PyCUDA，还需要安装软件包。从网站下载 PyCUDA 源文件([https://pypi.org/project/pycuda/](https://pypi.org/project/pycuda/))。目前正在使用 2019.1.1 版本。\n\n然后按如下方式安装依赖项:\n\n```cpp\n$ sudo apt-get install build-essential python-dev python-setuptools libboost-python-dev libboost-thread-dev\n\n$ tar -xzf pycuda-2019.1.2.tar.gz\n$ cd pycuda-2019.1.2\n$ python3 ./configure.py --cuda-root=/usr/local/cuda --cudadrv-lib-dir=/usr/lib \\\n --boost-inc-dir=/usr/include --boost-lib-dir=/usr/lib \\\n --boost-python-libname=boost_python-py36 --boost-thread-libname=boost_thread\n$ python3 setup.py build\n$ sudo python3 setup.py install\n```\n\n如果你想使用 Python 2，跳过使用 Python 3 的`configure.py`命令。根据您的 Python 版本，配置命令可能会有所不同。\n\n# 使用 PyCUDA 进行矩阵乘法\n\n我们可以通过以下方式使用 PyCUDA 执行矩阵乘法:\n\n1.  创建`pycuda_matmul.py`文件。\n2.  按照以下步骤导入所需的包:\n\n```cpp\nimport pycuda.autoinit\nfrom pycuda import driver, compiler, gpuarray\nimport numpy as np\nfrom string import Template\nimport timeit\n```\n\n3.  编写一个 CUDA 内核函数代码:\n\n```cpp\nkernel_code_template = Template(\"\"\"\n__global__ void matmul_kernel(float *d_C, float *d_A, float *d_B)\n{\n    int idx_x = blockIdx.x * blockDim.x + threadIdx.x;\n    int idx_y = blockIdx.y * blockDim.y + threadIdx.y;\n\n    float sum = 0.f;\n    for (int e = 0; e < ${MATRIX_SIZE}; e++)\n        sum += d_A[idx_y * ${MATRIX_SIZE} + e] * d_B[e * ${MATRIX_SIZE} + idx_x];\n    d_C[idx_y * ${MATRIX_SIZE} + idx_x] = sum;\n}\n\"\"\")\n```\n\n4.  使用 NumPy 生成输入/输出矩阵:\n\n```cpp\nN = 8192\nnp.random.seed(2019)\nA = np.random.rand(N, N).astype(np.float32)\nB = np.random.rand(N, N).astype(np.float32)\nC = np.zeros((N, N), dtype=np.float32)\n```\n\n5.  编译内核代码:\n\n```cpp\nmod = compiler.SourceModule( \\\n        kernel_code_template.substitute(MATRIX_SIZE=N))\n```\n\n6.  从编译后的模块中获取内核函数:\n\n```cpp\nmatmul_kernel = mod.get_function(\"matmul_kernel\")\n```\n\n7.  使用主机生成的输入数据创建设备存储器:\n\n```cpp\nd_A = gpuarray.to_gpu(A)\nd_B = gpuarray.to_gpu(B)\nd_C = gpuarray.zeros((N, N), dtype=np.float32)\n```\n\n8.  配置网格和块尺寸:\n\n```cpp\ndimBlock = 16\ndimGrid = int((N + dimBlock - 1) / dimBlock)\n```\n\n9.  准备获取图形处理器事件:\n\n```cpp\nstart = driver.Event()\nstop = driver.Event()\n```\n\n10.  调用内核函数:\n\n```cpp\nprint(\"Started GPU operation...\")\nstart.record()\n\nmatmul_kernel(d_C, d_A, d_B, \n    block=(dimBlock, dimBlock, 1), \n    grid=(dimGrid, dimGrid))\n\nstop.record()\nstop.synchronize()\ngpu_time = stop.time_since(start)\nprint(\"GPU Execution Time: %.3f ms\" % (gpu_time))\n```\n\n11.  从主机启动矩阵乘法，并将其与设备的结果进行比较:\n\n```cpp\nprint(\"Started Host operation...\")\nstart = timeit.default_timer()\nc_host = np.matmul(A, B)\nhost_time = timeit.default_timer() - start\n\nprint(\"CPU Execution Time: %.3f ms\" % (host_time * 1e3))\n\nif (np.allclose(c_host, d_C.get())):\n    print(\"Done.\")\nelse:\n    print(\"GPU and host results are mismatching.\")\n```\n\n此代码还报告设备和主机上的估计时间:\n\n```cpp\nStarted GPU operation...\nGPU Execution Time: 657.547 ms\nStarted Host operation...\nCPU Execution Time: 1531.133 ms\nDone.\n```\n\n虽然 PyCUDA 公开了 CUDA C/C++ 内核代码，但这个结果给出了一个提示，即需要手动进行内核优化，因为与 Numba 执行的操作相比，它缺乏性能。\n\n# 针对八度音程和 R 中零编码加速的 NVBLAS\n\nNVBLAS 是一个 CUDA 库，用于 Octave 和 r 等其他包的 BLAS 操作，通过替换 OpenBLAS 执行的操作，Octave 或开发人员和数据科学家可以轻松享受 GPU 性能。在本章中，我们将介绍如何使用 NVBLAS 加速 Octave 和 R。\n\nNVBLAS 是 cuBLAS 操作之上的动态库。cuBLAS 库是线性代数运算的 GPU 实现。它取代了 BLAS 库，因此我们可以轻松地用零编码工作加速任何应用。让我们看看如何从 GEMM 示例代码中做到这一点。\n\n# 配置\n\n要在 Octave 和 R 中使用 NVBLAS，需要给 NVBLAS 提供一些工作环境变量。为此，让我们创建一个`nvblas.conf`文件，在那里可以找到目录，我们将使用 Octave 和 R 代码示例。`nvblas.conf`文件可以编写如下:\n\n```cpp\nNVBLAS_CPU_BLAS_LIB libopenblas.so\nNVBLAS_LOGFILE nvblas.log\nNVBLAS_GPU_LIST 0\nNVBLAS_AUTOPIN_MEM_ENABLED\n```\n\n在这个文件中，我们可以看到 NVBLAS 需要知道 CPU 端的 BLAS 库。我们将在这个会话中使用 OpenBLAS，因此我们需要在 Ubuntu 中使用以下命令安装它:\n\n```cpp\n$ sudo apt-get install libopenblas-base libopenblas-dev\n```\n\n另外，我们可以通过为`NVBLAS_GPU_LIST`提供多个 GPU IDs 来获得多 GPU 性能。这本书提供了一个 GPU 执行结果的结果，但是如果你有多个 GPU，尽量提供多个 id。\n\n要在 Octave 和 R 中使用 NVBLAS，我们应该为您的应用执行设置一个环境— `LD_PRELOAD=libnvblas.so`:\n\n*   对于 Octave 代码，按如下方式执行代码:\n\n```cpp\n$ LD_PRELOAD=libnvblas.so octave sgemm.m\n```\n\n*   对于 R 脚本，按如下方式执行您的脚本:\n\n```cpp\n$ LD_PRELOAD=libnvblas.so Rscript sgemm.R\n```\n\n当然`libnvblas.so`文件应该可以从工作目录中访问。位于`/usr/local/cuda/lib64/`。\n\nNVBLAS 与存档的包兼容。因此，在我们的测试中，使用 Octave-和 R-安装的命令和下面的命令很好地工作:\n\n```cpp\n$ sudo apt-get install octave # for octave installation\n$ sudo apt-get install r-base # for R installation\n```\n\n现在，让我们尝试使用 NVBLAS，使用 Octave 和 R 语言。\n\n# 加速八度音阶的计算\n\n首先，我们将使用 Octave 尝试 NVBLAS。完全实现的代码是`08_nvblas/sgemm.m`。具体如下:\n\n```cpp\nfor i = 1:5 \n    N = 512*(2^i);\n    A = single(rand(N,N));\n    B = single(rand(N,N));\n\n    start = clock();\n    C = A * B;\n    elapsedTime = etime(clock(), start);\n\n    gFlops = 2*N*N*N/(elapsedTime * 1e+9);\n    printf(\"Elapsed Time [%d]: %.3f ms, %.3f GFlops\\n\", N, elapsedTime, gFlops);\nend\n```\n\n对于 GPU 操作，使用以下命令执行 Octave 脚本，并将性能与 GPU、默认情况下的 NVBLAS 环境库和 CPU 进行比较:\n\n```cpp\n$ LD_PRELOAD=libnvblas.so octave sgemm.m\n```\n\n然后，我们可以用`octave sgemm.m`命令启动这个。输出结果如下:\n\n| 中央处理器 | GPU V100 |\n| \n\n*   `Elapsed Time [1024]: 0.011 ms, 188.909 GFlops`\n*   `Elapsed Time [2048]: 0.075 ms, 228.169 GFlops`\n*   `Elapsed Time [4096]: 0.212 ms, 647.022 GFlops`\n*   `Elapsed Time [8192]: 1.158 ms, 949.763 GFlops`\n*   `Elapsed Time [16384]: 7.292 ms, 1206.241 GFlops`\n\n | \n\n*   `Elapsed Time [1024]: 0.010 ms, 208.346 GFlops`\n*   `Elapsed Time [2048]: 0.024 ms, 721.731 GFlops`\n*   `Elapsed Time [4096]: 0.094 ms, 1465.538 GFlops`\n*   `Elapsed Time [8192]: 0.582 ms, 1889.193 GFlops`\n*   `Elapsed Time [16384]: 4.472 ms, 1967.037 GFlops`\n\n |\n\n如您所见，随着矩阵的大小变大，GPU 显示出更高的计算吞吐量。\n\n# 加速 R 的计算\n\n现在，我们将在以下步骤的帮助下，针对 R 语言尝试 NVBLAS:\n\n1.  首先，让我们编写一个`sgemm.R`文件，它执行一个点操作:\n\n```cpp\nset.seed(2019)\nfor(i in seq(1:5)) {\n    N = 512*(2^i)\n    A = matrix(rnorm(N^2, mean=0, sd=1), nrow=N) \n    B = matrix(rnorm(N^2, mean=0, sd=1), nrow=N) \n    elapsedTime = system.time({C = A %*% B})[3]\n    gFlops = 2*N*N*N/(elapsedTime * 1e+9);\n    print(sprintf(\"Elapsed Time [%d]: %3.3f ms, %.3f GFlops\", N, elapsedTime, gFlops))\n}\n```\n\n2.  使用以下命令执行 R 脚本，并比较性能:\n\n```cpp\n$ LD_PRELOAD=libnvblas.so Rscript sgemm.R\n```\n\n示例代码运行了几次，同时增加了数据大小。下表显示了前面命令的输出:\n\n| 中央处理器 | GPU V100 |\n| \n\n*   `Elapsed Time [1024]: 0.029 ms, 74.051 GFlops`\n*   `Elapsed Time [2048]: 0.110 ms, 156.181 GFlops`\n*   `Elapsed Time [4096]: 0.471 ms, 291.802 GFlops`\n*   `Elapsed Time [8192]: 2.733 ms, 402.309 GFlops`\n*   `Elapsed Time [16384]: 18.291 ms, 480.897 GFlops`\n\n | \n\n*   `Elapsed Time [1024]: 0.034 ms, 63.161 GFlops`\n*   `Elapsed Time [2048]: 0.063 ms, 272.696 GFlops`\n*   `Elapsed Time [4096]: 0.286 ms, 480.556 GFlops`\n*   `Elapsed Time [8192]: 1.527 ms, 720.047 GFlops`\n*   `Elapsed Time [16384]: 9.864 ms, 891.737 GFlops`\n\n |\n\n从结果可以看出 CPU 和 GPU 的性能差距。此外，我们能够确定，当我们增加样本量时，GPU 的性能增益会增加。\n\n如果你对 GPU 的 R 加速感兴趣，请访问 NVIDIA 开发博客:[https://devblogs.nvidia.com/accelerate-r-applications-cuda/](https://devblogs.nvidia.com/accelerate-r-applications-cuda/)\n\n# MATLAB 中的 CUDA 加速\n\nMATLAB 是一个高效的高级数值分析工具，具有各种工具和功能。该工具通过其**并行计算工具箱**从早期就支持 CUDA。本节将向我们展示如何使用该工具生成 CUDA 代码。\n\n为了实现 GPU 加速，我们需要安装带有并行计算工具箱的 MATLAB。如果您已经有了 MATLAB，请检查您的许可证是否涵盖了并行计算工具箱。如果没有，可以试试 MATLAB 评估代码。从 MATLAB 的评估网站，你可以下载任何种类的软件包，除了控制系统。大多数包都包含并行计算工具箱，所以您可以尝试这样做。但是如果你不考虑使用 MATLAB，你可以跳过这一节。\n\n当我们使用 MATLAB 代码在 GPU 上工作时，您需要使用 gpuArray 创建一个设备内存。就像 *Numba* 和 *PyCUDA* 将它们的主机数据发送到设备一样，MATLAB 的`gpuArray()`创建一个设备内存并将给定的主机数据传输到设备:\n\n```cpp\nd_A = gpuArray(A);\n```\n\n本课程将假设您已经安装了 MATLAB 和并行计算工具箱。在本节中，我们将重点介绍示例代码的实现，并比较主机和 GPU 的性能:\n\n1.  我们写一个`host.m`文件，可以在 CPU 上工作。代码如下:\n\n```cpp\nN = 8192;\nA = single(rand(N,N));\nB = single(rand(N,N));\n\nstart = clock();\nC = A * B; \nelapsedTime = etime(clock(), start);\ngFlops = 2*N*N*N/(elapsedTime * 1e+9);\nfprintf(\"Elapsed Time: %.3f ms, %.3f GFlops\\n\", elapsedTime, gFlops);\n```\n\n现在，让我们用下面的命令来执行这两个实现。这是 MATLAB 的命令，它的输出:\n\n```cpp\n$ matlab -r \"run('host.m'); exit;\" -nodisplay\nElapsed Time: 6.421 ms, 171.243 Gflops\n```\n\n2.  然后，我们写一个`cuda.m`文件，在 GPU 上工作。我们只需将`gpuArray()`应用于输入矩阵，如下所示:\n\n```cpp\nN = 8192;\nA = single(rand(N,N));\nB = single(rand(N,N));\n\nd_A = gpuArray(A);    'GPU memory allocation\nd_B = gpuArray(B);    'GPU memory allocation\n\nstart = clock();\nd_C = d_A * d_B;\nelapsedTime = etime(clock(), start);\ngFlops = 2*N*N*N/(elapsedTime * 1e+9);\nfprintf(\"Elapsed Time: %.3f ms, %.3f GFlops\\n\", elapsedTime, gFlops);\n```\n\n这是 GPU 版本执行代码，执行结果:\n\n```cpp\n$ matlab -r \"run('cuda.m'); exit;\" -nodisplay\nElapsed Time: 0.179 ms, 6140.739 Gflops.\n```\n\n我们可以看到，相对于 CPU，GPU 表现出了更高的性能。\n\nMathWorks 提供了大量用 MATLAB 进行 GPU 计算的例子。如果您想了解更多信息，请访问他们的网站:[https://www . mathworks . com/examples/parallel-computing/category/GPU-computing](https://www.mathworks.com/examples/parallel-computing/category/gpu-computing)。\n\n# 摘要\n\n在本章中，我们已经介绍了使用 CUDA 库和其他兼容语言的 CUDA 编程方法。我们还介绍了 cuBLAS 的基本用途及其混合精度操作特性。此外，我们还探索了 cuRAND、cuFFT、NPP 和 OpenCV 库。多亏了这些库，我们可以不费吹灰之力实现 GPU 应用，正如本章开头所讨论的。\n\n我们已经使用与 CUDA 兼容的其他语言实现了一些 GPU 应用。首先，我们介绍了几个支持 Python 和 CUDA 互操作的 Python 包。它们提供 Python 编程能力以及与其他 Python 特性的兼容性。然后，我们介绍了其他科学计算语言中的 CUDA 加速，例如 Octave、R 和 MATLAB。\n\n现在，我们还有一个 GPU 编程方法要介绍——OpenACC。有了这个，我们可以转换原始的 C/C++ 和 Fortran 主机代码，使用指令如`#pragma acc kernels`在 GPU 上工作。我们将在下一章讨论这个问题。"
  },
  {
    "path": "docs/learn-cuda-prog/09.md",
    "content": "# 八、将 OpenACC 用于图形处理器编程\n\n每个处理器架构都提供了不同的方法来编写在处理器上运行的代码。CUDA 也不例外；它还提供了不同的编码方法。最近几年变得非常流行的一种方法是使用 OpenACC，它基本上是基于指令的编程。\n\nOpenACC 基本上是一个将异构计算暴露为一流公民的标准。该标准从根本上规定有两种处理器，即主机和设备/加速器，这与 CUDA 编程模型所陈述的概念非常相似。\n\n对于想要获得最佳性能的程序员来说，使用 C、C++、Fortran 和 Python 等语言进行 CUDA 编程是表达并行性的首选方式。编程语言要求程序员从头开始重新创建顺序程序，同时保持其关键操作的串行和并行版本。程序员可以对他们程序的一切进行微观管理，并经常使用特定于设备的特性来获得最佳性能，而这些特性对于更高级别的方法来说过于特定。用并行编程语言创建的并行程序往往只能在极少数平台上运行。\n\n编译器指令融合了编程语言的灵活性和库的易用性。程序员用高级指令来注释代码，编译器可以使用这些指令来并行化代码，或者可以安全地忽略这些指令。这意味着，带有编译器指令的代码可以针对许多不同的并行平台进行编译，并且不需要维护代码的单独串行和并行版本。此外，有时需要快速测试和原型化应用，以便在图形处理器上运行。一个这样的例子是转换代码库，如天气代码，它有数百万行代码，运行在图形处理器上；使用流行的语言来完成这项工作需要花费大量的精力。在这样的场景下，OpenACC 成为一个合乎逻辑的选择。在 OpenACC 中，开发人员以指令的形式向编译器提供提示。编译器接受这些提示并生成特定于体系结构的加速器代码。\n\nOpenACC 标准还为代码开发人员提供了供应商中立性。带有 OpenACC 指令的单一源代码可以针对不同的设备进行重新编译。例如，PGI 编译器目前支持 OpenACC 后端，如英特尔 CPU 多核、NVIDIA GPU、英特尔至强融核以及 **F** **现场可编程门阵列** ( **FPGA** ) / **专用集成电路** ( **ASIC** )架构。对于想要编写供应商中立代码的开发人员来说，这是一个非常有吸引力的提议。**高处理计算** ( **HPC** )中的关键应用，如**维也纳从头模拟包** ( **VASP** )(分子动力学/量子化学)**天气研究和预报** ( **WRF** )和 ANSYS Fluent **计算流体动力学** ( **CFD** )利用 OpenACC 编程模型瞄准 NVIDIA GPU\n\n总结 OpenACC 的关键要点:\n\n*   OpenACC 标准是在异构计算被认为是新的编程模型时发展起来的。\n*   OpenACC 提供跨各种加速器的性能可移植性。\n*   OpenACC 不是 CUDA 编程语言的替代品。当目标处理器被选为 NVIDIA 时，OpenACC 编译器会在幕后生成 CUDA 代码。\n\n近年来，OpenMP 标准也开始融入异构计算 API。但是到目前为止，还没有一个编译器支持不同的处理器架构，所以我们在本书中选择了坚持使用 OpenACC。\n\n我们将在本章中讨论以下主题:\n\n*   OpenACC 准则\n*   OpenACC 中的异步编程\n*   附加的重要指令和条款\n\n# 技术要求\n\n本章需要一台带有现代英伟达图形处理器(帕斯卡架构)的 Linux/Windows 电脑。\n\n正如在介绍中提到的，OpenACC 是一个标准，这个标准由不同的编译器实现，比如 GCC、PGI 和 CRAY 编译器。本章我们将使用的编译器是 PGI。PGI 编译器在 Fortran 社区中真的很受欢迎，在实现 OpenACC 最新规范方面一直走在曲线的前面，并且提供了社区版，可以从 PGI 网站免费下载。好的一面是，从根本上来说，社区版和付费版 PGI 编译器之间的功能没有变化。本章要求您下载 PGI 社区版。\n\n本章代码也可在 GitHub 上获得，网址为:[https://github.com/PacktPublishing/Learn-CUDA-Programming](https://github.com/PacktPublishing/Learn-CUDA-Programming)。\n\n示例代码示例是用 PGI 社区版的 19.4 版本开发和测试的。但是建议你使用最新的 PGI 版本。\n\n# 基于 OpenACC 的图形处理器图像融合\n\n为了理解 OpenACC 的概念，我们选择了一种简单的计算机视觉算法来合并两幅图像。在这段代码中，我们试图合并两个图像，如下所示:\n\n![](img/18bb51b7-333b-4c18-8f05-de66809037a2.png)\n\n上图展示了一个合并两幅图像的计算机视觉算法。\n\n我们将在本章的后面讨论更多的代码结构。首先，根据以下步骤配置环境:\n\n1.  准备好你的 GPU 应用。作为一个例子，我们将使用一个核心算法来合并两幅图像。这个代码可以在`09_openacc/`找到。\n2.  使用`pgc++ `编译器编译您的应用:\n\n```cpp\n$ pgc++ -c -acc -ta=tesla:pinned scrImagePgmPpmPackage.cpp\n$ pgc++ -c -acc -ta=tesla:pinned -Minfo=accel image_merging.cpp\n$ pgc++ -o merging.out -acc -ta=tesla:pinned -Minfo=accel scrImagePgmPpmPackage.o image_merging.o\n$ ./merging.out\n```\n\n上述命令将创建一个名为`blurring.out`的二进制文件。正如你可能已经观察到的，我们正在使用`pgc++ `编译器来编译我们的代码。此外，我们向代码传递一些参数。让我们更详细地了解它们:\n\n*   `-acc`:这个标志告诉编译器解析代码中提供的 OpenACC 指令。\n*   `-ta`:代表应该为其生成设备代码的目标架构。注意`-ta=tesla`表示我们瞄准的是一个 NVIDIA GPU。其他目标的一些例子包括以多核为目标的`-ta=multi-core`、以 AMD GPUs 为目标的`-ta=radeaon`以及其他一些目标。此外，我们可以添加特定于设备的标志；例如，我们在分配所有 CPU 内存为固定(不可分页)的 GPU 中添加了固定标志。\n*   `-Minfo`:这个选项告诉编译器为我们提供更多关于编译器为使我们的代码并行而采取的步骤的信息。通过说`-Minfo-accel`，我们是要求编译器只给我们提供更多与加速器区域相关的信息。我们可以将标志更改为`-Minfo=all`，以提供非加速器区域的详细信息。以下输出显示了向我们的代码添加`Minfo`标志的部分输出:\n\n```cpp\n.... < More compiler output above>\nmerge_parallel_pragma(unsigned char *, unsigned char *, unsigned char *, long, long):\n    30, Generating copyin(in1[:w*h])\n    Generating copyout(out[:w*h])\n    Generating copyin(in2[:w*h])\n    Accelerator kernel generated\n    Generating Tesla code\n    30, #pragma acc loop gang /* blockIdx.x */\n    32, #pragma acc loop vector(128) /* threadIdx.x */\n    32, Loop is parallelizable\n... < More compile output below >\n```\n\n为了理解这个编译输出，我们需要理解 OpenACC pragmas，这将在下一节中进行。稍后我们将重新讨论这个编译输出。使用`pgc++ --help` *可以找到其他可用标志的详细信息。*\n\n运行二进制文件后的示例输出如下:\n\n```cpp\n$ ./merging.out\nReading image width height and width [1536][2048]\nTime taken for serial merge: 0.0028 seconds\nTime taken for OpenACC merge(data+kernel): 0.0010 seconds\nTime taken for OpenACC merge(kernel only) with Blocking: 0.0002 seconds\n Time taken for OpenACC merge(data _kernel) with blocking: 0.0014 seconds\nTime taken for OpenACC merge (data+kernel)with Pipeline Async: 0.0008 seconds\n```\n\n前面的输出显示我们正在读取大小为 1536*2048 的图像。代码有一个串行实现和三个使用 OpenACC pragmas 的并行实现。前面的输出显示了每个实现的时序。流水线方法的最后一个实现显示了最佳时机:`0.0008 seconds`。我们将采取一种渐进的方法，并在接下来的章节中详细介绍每个实现。\n\n这个算法的串行实现非常简单，如下面的代码片段所示:\n\n```cpp\nvoid merge_serial(unsigned char *in1, unsigned char*in2, unsigned char *out, long w, long h)\n{\n    long x, y;\n     for(y = 0; y < h; y++) {\n         for(x = 0; x < w; x++) {\n             out[y * w + x] = (in1[y * w + x]+in2[y * w + x])/2;\n         }\n     }\n}\n```\n\n代码没有什么花哨的地方；基本上是取两个输入图像数据(`in1`和`in2`)，进行平均运算合并两个输入，最后存储输出。就并行性而言，对我们来说，最关键的是循环是令人尴尬的并行，并且适用于像图形处理器这样的架构。如前面的代码输出所示，串行实现花费了`0.0028`秒。请注意，根据运行代码的系统，计时可能会略有不同。\n\n在下一节中，我们将向您介绍将示例代码转换为在 GPU 上运行所必需的 OpenACC 指令。\n\n# OpenACC 准则\n\n在本节中，我们将尝试理解 OpenACC pragmas 的语法，并为合并操作实现基本的并行和数据指令。OpenACC pragma 的基本语法如下:\n\n```cpp\n#pragma acc <directive> <clauses> \n!$acc parallel [clause [[,] clause]…] \n```\n\n前面的命令解释如下:\n\n*   `#pragma`在 C/C++ 中是所谓的“编译器提示”这些与程序员注释非常相似；然而，编译器实际上会读取我们的 pragmas。如果编译器不理解 pragma，它可以忽略它，而不是抛出语法错误。\n*   `acc`是我们 pragma 的补充。它指定这是一个 OpenACC pragma。任何非 OpenACC 编译器都会忽略这个 pragma。\n*   `directive`是 OpenACC 中的一个命令，它会告诉编译器执行一些操作。目前，我们将只使用允许编译器并行化代码的指令。\n*   `clauses`是对我们指令的补充/修改。这些包括但不限于优化。\n\n本节我们将介绍三个指令:*并行*、*循环、*和*数据*。我们将展示它们中的每一个，最后将它们应用到我们的合并算法中。\n\n# 并行和循环指令\n\n并行指令是最直接的指令。它将标记代码的一个区域进行并行化(这通常只涉及并行化单个`for`循环)，如以下代码所示:\n\n```cpp\n#pragma acc parallel loop \nfor (int i = 0; i < N; i++ ) {  \n    //loop code \n}\n```\n\n我们也可以定义一个平行区域。平行区域可能有多个循环(尽管通常不建议这样做！).平行区域是包含在最外侧大括号内的所有内容，如下面的代码片段所示:\n\n```cpp\n#pragma acc parallel\n{\n    #pragma acc loop\n    for (int i = 0; i < N; i++ )\n    {\n        < loop code >\n    }\n}\n```\n\n包含循环极其重要；否则，您将无法正确地并行化循环。并行指令告诉编译器冗余地并行化代码，如下所示:\n\n![](img/1ed25634-1b03-4d94-b6e8-f63049177ff5.png)\n\n循环指令明确告诉编译器，我们希望循环并行化，如下图所示:\n\n![](img/03886faa-19f3-4ad8-9401-a3ebd22606d7.png)\n\n循环指令有两个主要用途:\n\n*   要标记单个循环进行并行化\n*   为了允许我们明确定义循环的优化/变更\n\n我们将在本章后面介绍循环优化，以及 gang 和 vector 现在，我们将重点关注并行化方面。为了使循环指令正常工作，它必须包含在并行指令中:\n\n```cpp\n#pragma acc parallel loop\nfor (int i = 0; i < N; i++ )\n{\n    //loop code \n}\n```\n\n使用并行指令时，必须包含循环指令，代码才能正常运行。我们也可以使用循环指令来并行化多维循环嵌套。在下面的代码片段中，我们看到了一个嵌套循环，并为第二个循环明确提到了 loop 子句:\n\n```cpp\n#pragma acc parallel loop\nfor (int i = 0; i < N; i++ )\n{\n    #pragma acc loop\n    for( int j = 0; j < M; j++ )\n    {\n        //loop code\n    }\n}\n```\n\n请注意，在前面的代码片段中，我们没有将 parallel 子句再次放入内部循环，因为我们已经在从外部循环开始的作用域中提到过它。\n\n# 数据指令\n\nOpenACC 并行模型声明我们有一个主机，它运行我们的顺序代码(主要是中央处理器)。然后我们有了我们的设备，这是某种并行硬件。主机和设备通常(虽然不总是)有独立的存储器，程序员可以使用 OpenACC 在两个存储器之间移动数据。\n\n正如在第一章中所讨论的，图形处理器和中央处理器的架构有着根本的不同。图形处理器是一种基于吞吐量的架构，具有大量的计算单元和高速内存带宽。另一方面，中央处理器是一种减少延迟的架构，具有大的缓存层次结构，并且还提供了大的主内存。任何需要操作的数据都需要先拷贝到 GPU 内存中。(请注意，即使在统一内存的情况下，数据也会由驱动程序以页面的形式在幕后复制。)\n\n如下图所示，两种架构(中央处理器和图形处理器)之间的数据传输通过输入/输出总线进行:\n\n![](img/ac932965-3f84-440d-be0a-1f8428eb7e48.png)\n\n当在 OpenACC 中使用 GPU 作为目标架构时，我们的目标是只使用它来卸载我们的并行代码，顺序代码将继续在我们的 CPU 上运行。OpenACC 标准允许程序员通过使用 OpenACC **数据指令和数据条款**来明确定义数据管理。数据子句允许程序员指定主机和设备(或者，在我们的例子中，中央处理器和图形处理器)之间的数据传输。\n\n**I** **mplicit 数据管理**:我们可以将数据的传输留给编译器，如下例所示:\n\n```cpp\nint *A = (int*) malloc(N * sizeof(int));\n\n#pragma acc parallel loop\nfor( int i = 0; i < N; i++ )\n{\n    A[i] = 0;\n}\n```\n\n在前面的代码中，编译器将理解`A`向量需要从 GPU 中复制，并为开发人员生成一个隐式转移。\n\n**显式数据管理**:利用显式数据传输来获得对传输的更多控制是一个很好的做法，如下面的代码所示，其中我们使用了 copy data 子句:\n\n```cpp\nint *a = (int*) malloc(N * sizeof(int));\n#pragma acc parallel loop copy(a[0:N])\nfor( int i = 0; i < N; i++ )\n{\n     a[i] = 0;\n}\n```\n\n在前面的代码片段中，我们使用了 copy data 子句。下图解释了运行时到达复制数据指令时执行的步骤:\n\n![](img/52965e3f-256c-4efd-925f-94fd52a84cd5.png)\n\n我们将在合并代码的帮助下深入这些步骤的细节，我们将在合并代码中应用数据子句。\n\n其他可用数据条款如下:\n\n| **数据条款** | **描述** | **按键用法** |\n| `copy(list)` | \n\n*   Allocate memory on the device\n\n*   When entering this area, copy data from the host to the exit area of the device\n\n*   , copy data to the host\n\n | 这是修改后从函数返回的输入数据结构的默认值 |\n| `copyin(list)` | \n\n*   Allocate memory on the device\n\n*   When entering this area, copy the data from the host to the device\n\n. | 刚刚输入子程序的向量 |\n| `copyout(list)` | \n\n*   Allocate memory on the device\n\n*   Copy data to the host\n\nwhen exiting the area. | 不会覆盖输入数据结构的结果 |\n| `create(list)` | \n\n*   Memory is only allocated on the device\n\n*   without copying\n\n | 临时数组 |\n\n为了最大化性能，程序员应该避免所有不必要的数据传输，因此显式内存管理优于隐式数据管理。\n\n**数组整形:**数组整形是你指定数组大小的方式。如果不指定形状，编译器将尝试采用该大小。这在 Fortran 中运行良好，因为 Fortran 跟踪数组的大小；然而，它很可能在 C/C++ 中不起作用。数组整形也是从数组中复制一部分数据的唯一方法(例如，如果您只需要复制数组的一半，这可以提高性能，减少不必要的副本)，如以下代码片段所示:\n\n```cpp\n#pragma acc parallel loop copy(A[1:N-2])\n```\n\n这将复制除第一个和最后一个元素之外的所有`A`元素。\n\n# 应用并行、循环和数据指令来合并图像代码\n\n现在让我们尝试将并行、循环和数据指令应用于合并顺序代码:\n\n```cpp\nvoid merge_parallel_pragma(unsigned char *in1, unsigned char*in2,unsigned char *out, long w, long h)\n{\n    long x, y;\n    #pragma acc parallel loop gang copyin(in1[:h*w],\n                                          in2[:h*w]) \n                                          copyout(out[:h*w])\n     for(y = 0; y < h; y++) {\n        #pragma acc loop vector\n        for(x = 0; x < w; x++) {\n            out[y * w + x] = (in1[y * w + x]+in2[y * w + x])/2;\n        }\n    }\n}\n```\n\n我们使用平行循环指令使两个循环(高度:`y`和宽度:`x`)平行。此外，我们还明确添加了数据子句来复制数据。请注意，由于`in1`和`in2`向量仅是输入，因此它们是使用`copyin()`数据子句复制的。`out`向量是输出，使用`copyout()`数据子句复制。让我们试着理解这个函数的编译器输出:\n\n```cpp\nmerge_parallel_pragma(unsigned char *, unsigned char *, unsigned char *, long, long):\n    30, Generating copyin(in1[:w*h])\n        Generating copyout(out[:w*h])\n        Generating copyin(in2[:w*h])\n        Accelerator kernel generated\n        Generating Tesla code\n        30, #pragma acc loop gang /* blockIdx.x */\n        32, #pragma acc loop vector(128) /* threadIdx.x */\n32, Loop is parallelizable\n```\n\n前面的编译器输出显示，对于`merge_parallel_pragma`函数，编译器已经生成了以下动作:\n\n*   在第 30 行，为`in1`和`in2 `变量生成了`copyin`。内核启动前复制到 GPU 的数组大小为`[0:w*h]`。\n*   在第 30 行，为`out`变量生成了`copyout`。GPU 内核启动后将复制的阵列大小将是`[0:w*h]`。\n*   在第 30 行和第 32 行，生成了特斯拉内核代码:\n    *   在第 30 行，外部循环用组级并行化。\n    *   在第 32 行，用向量级并行化了内部循环\n\n当代码在 V100 上运行时，整个内核所花费的时间是`0.0010s`。这基本上是串行代码的两倍。这听起来可能并不令人印象深刻。原因是大部分时间花在数据传输上，而不是内核计算上。为了证实这一点，让我们利用`nvprof`:\n\n```cpp\n$ nvprof ./merging.out\n==26601== DoneProfiling application: ./merging.out\n==26601== Profiling result:\nType Time(%) Time Calls Avg Min Max Name\nGPU activities: 67.36% 609.41us 2 304.71us 286.34us 323.08us [CUDA memcpy HtoD]\n27.63% 250.02us 1 250.02us 250.02us 250.02us [CUDA memcpy DtoH]\n5.01% 45.344us 1 45.344us 45.344us 45.344us merge_parallel_pragma_30_gpu(unsigned char*, unsigned char*, unsigned char*, long, long)\n...\n```\n\n正如您在前面的分析输出中所观察到的，94%的时间用于数据传输，而只有 5%的时间(45 微秒)用于内核执行。您可能会有这样的疑问:我如何知道这是哪个内核？如果你仔细观察 GPU 内核的名称`merge_parallel_pragma_30_gpu`，PGI 编译器在第 30 行的`merge_parallel_pragma`函数中生成了一个 CUDA 内核，这就是我们如何将它与在该行号处放入函数的 pragmas 联系起来。\n\n所以我们知道问题，但是解决方案呢？我们将用来隐藏这种延迟的优化技术是阻塞。在接下来的章节中，我们将更多地介绍阻塞技术，并使用异步子句来覆盖这个传输。\n\n# OpenACC 中的异步编程\n\n为了在合并并行代码时获得更好的性能，我们将使用一个叫做阻塞的概念。阻塞基本上意味着，我们可以创建可以并行传输和操作的数组块，而不是一次性传输整个输入和输出数组。下图演示了在内核执行时创建块和重叠数据传输:\n\n![](img/5521327f-6b8b-4e44-b04c-abbd69131c84.png) \n\n上图显示了不同的块被传输，并且这些块的内核执行可以独立于每个块。为了实现这一点，我们需要异步激发和执行数据传输命令和内核调用。为了实现阻塞，我们将在本节中引入更多指令/子句:结构化/非结构化数据指令和`async`子句。我们将展示它们中的每一个，最后将它们应用到我们的基本 OpenACC 合并并行代码中。\n\n# 结构化数据指令\n\nOpenACC 数据指令允许程序员显式管理设备(在我们的例子中是图形处理器)上的数据。下面的代码片段显示了标记结构化数据区域的示例:\n\n```cpp\n< Initialize data on host (CPU) >\n#pragma acc data < data clauses >\n{\n    //< Code >\n}\n```\n\n设备内存分配发生在区域的开头，设备内存释放发生在区域的结尾。此外，从主机到设备(中央处理器到图形处理器)的任何数据移动都发生在区域的开头，从设备到主机(图形处理器到中央处理器)的任何数据移动都发生在区域的结尾。内存分配/解除分配和数据移动由程序员包含的子句定义。\n\n**包含多个计算区域:**单个数据区域可以包含任意数量的并行/内核区域，如下例所示:\n\n```cpp\n#pragma acc data copyin(A[0:N]) create(C[0:N])\n{\n    #pragma acc parallel loop\n    for( int i = 0; i < N; i++ )\n    {\n        C[i] = A[i] + 10;\n    }\n    #pragma acc parallel loop\n    for( int i = 0; i < N; i++ )\n    {\n        C[i] = C[i] / 10;\n    }\n}\n```\n\n# 非结构化数据指令\n\n有两个非结构化数据指令:\n\n*   **输入数据**:处理设备内存分配，从主机复制到设备。您可以在输入数据时使用的两个子句是:\n    *   `create`:这只会执行设备内存分配。\n    *   `copyin`:这将执行分配以及设备的内存拷贝。\n*   **退出数据**:处理设备内存释放，并从设备复制到主机。您可以使用退出数据的两个子句是:\n    *   `delete`:这将只执行设备内存释放。\n    *   `copyout`:这将首先进行从设备到主机的内存复制，然后进行设备内存释放。\n\n非结构化数据指令不会标记数据区域，因为在代码中可以有多个输入数据和输出数据指令。最好把它们纯粹看作是内存分配和释放。使用非结构化数据指令的最大优势是能够跨多个功能进行分支。您可以在一个函数中分配数据，在另一个函数中取消分配。我们可以看一个简单的例子:\n\n```cpp\n#define N 1024\nint* allocate(int size)\n{\n    int *ptr = (int*) malloc(size * sizeof(int));\n    #pragma acc enter data create(ptr[0:size])\n    return ptr;\n} \nvoid deallocate(int *ptr)\n{\n    #pragma acc exit data delete(ptr)\n    free(ptr);\n}\nint main()\n{\n    int *ptr = allocate(N);\n    #pragma acc parallel loop\n    for( int i = 0; i < N; i++ )\n    {\n        ptr[i] = 0;\n    }\n    deallocate(ptr);\n}\n```\n\n前面的代码片段显示分配发生在单独的`allocate()`函数中，删除发生在`deallocate()`中。你可以在 C++ 中将同样的概念链接到`enter data create`作为构造函数的一部分和`exit data delete`作为析构函数的一部分。\n\n# OpenACC 中的异步编程\n\n默认情况下，所有 OpenACC 调用本质上都是同步的。这意味着，在每次调用数据传输或每次内核调用 GPU 之后，都会隐式添加一个同步。中央处理器将等到 OpenACC 调用完成，然后开始执行下一条指令。为了使调用异步，我们可以使用`async`子句以及数据和并行指令，如以下代码所示:\n\n```cpp\n#pragma acc data copyin(a[:N]) async \n// performing copyin asynchronously \n#pragma acc parallel loop async \n//performing parallel loop asynchronously. \n```\n\n使用`async`的主要好处可以总结如下:\n\n*   如果我们想同时执行主机和设备代码，我们可以用`async`启动我们的设备代码，在执行的同时，我们可以回到主机继续不相关的(非设备相关的)代码。\n*   我们可以*将*多个设备内核启动进行排队，以便它们背靠背执行，这在某些情况下可以减少与启动设备内核相关的开销。\n*   我们可以在主机和设备之间进行数据移动的同时进行设备计算**。**这是我们将要应用到代码中的优化，也是`async`最通用的用例。\n\n在引擎盖下，每当我们使用`async`子句时，我们都会在队列中添加一些*工作*。提交到不同队列的工作可以异步执行*，同一队列中的工作将依次执行*(一个接一个)。当我们使用`async`时，我们能够指定一个队列号。如果没有指定队列号，将自动使用默认值。**\n\n **# 应用非结构化数据和异步指令来合并图像代码\n\n现在让我们尝试将数据指令与`async`子句一起应用于合并并行代码:\n\n```cpp\nvoid merge_async_pipelined(unsigned char *in1, unsigned char*in2,unsigned char *out, long w, long h)\n{\n    long x, y;\n    #pragma acc enter data create(in1[:w*h], in2[:h*w], out[:w*h])\n    const long numBlocks = 8;\n    const long rowsPerBlock = (h+(numBlocks-1))/numBlocks;\n    for(long block = 0; block < numBlocks; block++) {\n        long lower = block*rowsPerBlock; // Compute Lower\n        long upper = MIN(h, lower+rowsPerBlock); // Compute Upper\n        #pragma acc update device(in1[lower*w:(upper-lower)*w],\n                                  in2[lower*w:(upper-lower)*w]) \n                                  async(block%2)\n        #pragma acc parallel loop present(in1,in2, out) async(block%2)\n        for(y = lower; y < upper; y++) {\n            #pragma acc loop\n            for(x = 0; x < w; x++) {\n                out[y * w + x] = (in1[y * w + x]+in2[y * w + x])/2;\n            }\n        }\n        #pragma acc update self(out[lower*w:(upper-lower)*w]) \n                                async(block%2)\n    }\n#pragma acc wait\n#pragma acc exit data delete(in1, in2, out)\n}\n```\n\n我们使用了数据指令和`async`子句来实现阻塞概念。让我们分解一下整体实现，这样会更容易理解:\n\n1.  **进入数据区**:`enter data create`子句为 GPU 中的`in1`和`in2`变量以及`out`分配内存。\n2.  **创建块**:我们决定将图像分割成八个块。这些块被分成若干行。由于这个原因，块的外部`for`循环被添加。\n\n3.  **将数据从主机异步传输到设备** : `acc update device`基本上是将数据从主机异步复制到设备，因为我们在其中添加了`async`子句。\n4.  **异步启动并行循环****:并行子句中增加`async`子句，异步启动 GPU 内核。**\n***   **将数据从设备异步传输到主机** : `acc update self`基本上是将数据从设备异步复制到主机，因为我们在其中添加了`async`子句。*   **等待** : `acc wait`将确保在所有队列中前进之前，中央处理器一直等到所有 OpenACC 启动完成。*   **退出数据区** : `acc exit data delete`将删除`enter data`子句中分配的`in1`和`in2`向量和`out`。**\n\n **让我们试着理解`merge_async_pipelined `函数的编译器输出:\n\n```cpp\nmerge_async_pipelined(unsigned char *, unsigned char *, \n                      unsigned char *, long, long):\n     67, Generating enter data create(out[:h*w],in2[:h*w],in1[:h*w])\n     74, Generating update device(in1[w*lower:w*(upper-lower)],\n                                  in2[w*lower:w*(upper-lower)])\n         Generating present(in1[:],out[:],in2[:])\n         Accelerator kernel generated\n         Generating Tesla code\n         74, #pragma acc loop gang /* blockIdx.x */\n         76, #pragma acc loop vector(128) /* threadIdx.x */\n     76, Loop is parallelizable\n     81, Generating update self(out[w*lower:w*(upper-lower)])\n     84, Generating exit data delete(out[:1],in2[:1],in1[:1])\n```\n\n前面的编译器输出显示，对于`merge_async_pipelined`函数，编译器已经生成了以下动作:\n\n*   在第`67`行，已经为`in1`、`in2`和`out`变量生成了`data create`区域。\n*   在第`74`行，`update device`为`in1`和`in2`调用，数据到设备的传输被限制为阻塞上下界:`in1[w*lower:w*(upper-lower)],in2[w*lower:w*(upper-lower)]`。\n*   在第`74`和`76`行，特斯拉内核代码已经生成。\n*   在第`81`行，`out`变量调用`update self`，来自设备的数据传输被限制为阻塞上限和下限:`out[w*lower:w*(upper-lower)]`。\n*   在第`84`行，数据区结束，调用`delete`释放分配在 GPU 上的内存。\n\n当代码在 V100 上运行时，整个内核花费的时间是 0.0008 秒。为了更详细地理解这一点，让我们回到剖析器。这一次，我们将通过使用英伟达视觉分析器来可视化输出:\n\n![](img/2d3b6f80-eaf4-4292-ac0f-e1321f6907bf.png)\n\nOutput by using NVIDIA Visual Profiler\n\n上图截图显示的是****视觉轮廓仪使用`async`并阻挡后的输出。探查器窗口的主要消息如下:****\n\n ****1.  我们看到三个流被创建和使用。这是因为我们的代码使用了`async(block%2)`，这意味着我们已经请求了最大`2`个队列。第三个队列是默认队列，在管道执行期间不使用。\n2.  我们看到主机到设备和设备到主机的传输也重叠，因为 GPU 有两个**直接内存访问** ( **DMA** )引擎，因此相反方向的数据传输可以重叠。\n3.  我们还看到我们的内核执行与数据传输重叠。\n\n到目前为止，我们已经看到了一些关键指令，它们帮助我们在图形处理器上运行图像合并的顺序代码。在下一节中，我们将向您介绍更多的子句，这些子句将帮助您进一步优化您的 OpenACC 代码。\n\n# 附加的重要指令和条款\n\n在本节中，我们将介绍其他重要的、广泛使用的指令，这些指令可以应用到我们的合并算法中。\n\n# 帮派/病媒/工人\n\nGang/worker/vector 定义了我们可以使用 OpenACC 实现的各种并行级别。这种并行性在并行化多维循环嵌套时最有用。OpenACC 允许我们定义一个通用的帮派/工作者/向量模型，该模型将适用于各种硬件，但我们将更加关注 GPU 特定的实现。下图显示了一个 OpenACC 并行编程模型:\n\n![](img/cd25f916-e4c0-4563-8add-eda78ea46a13.png)\n\n上图代表了一个帮派。当并行化我们的`for`循环时，循环迭代将在多个组中被平均分解。每个组将包含多个线程。这些线程被组织成块。工作者是一排线程。\n\n在上图中，有三个工人，这意味着有三行线程。向量是指每行有多长。所以在上图中，向量是 8，因为每行有 8 个线程长。默认情况下，为图形处理器编程时，会自动应用组和向量并行。\n\n由于 OpenACC 是开放标准，针对多个硬件；它提供了通用构造。但是这个结构如何映射到特定的目标设备呢？答案很简单；它依赖于架构和编译器，因此提供了性能可移植性。如果我们要映射当前的 PGI 编译器如何将这个概念映射到 CUDA (NVIDIA GPU)上，将如下所示:\n\n*   OpenACC 组映射到一个 CUDA 块。\n*   工人本质上映射到一个 CUDA 翘曲。\n*   OpenACC 矢量映射到`threadIdx.x`和(X 维度)。\n*   OpenACC 工作人员映射到`threadIdx.y` (Y 维度)。\n\n再次强调这一点很重要，PGI 编译器就是这样映射 OpenACC 构造的。其他编译器可能会对此进行不同的映射。特别是对于 NVIDIA GPUs，帮派工作向量将定义我们 GPU 线程的组织。通过添加以下子句，开发人员可以告诉编译器在给定的循环中使用哪些并行级别:\n\n*   `gang`:标记成组并行的循环。\n*   `worker`:标记工作机并行度的循环。\n*   `vector`:标记向量平行的循环。\n\n下面的代码片段有三个循环，每个循环的并行度都被明确定义了:外部循环为`gang`，中间循环为`worker`循环，最里面的循环为`vector`循环:\n\n```cpp\n#pragma acc parallel loop gang\nfor( i = 0; i < size; i++ )\n    #pragma acc loop worker\n    for( j = 0; j < size; j++ )\n        #pragma acc loop vector\n        for( k = 0; k < size; k++ )\n          c[i][j] += a[i][k] * b[k][j];\n```\n\n**调整帮派、工人和向量:**编译器会为你选择多个帮派和工人以及一个向量长度，但你可以用以下子句进行更改:\n\n*   `num_gangs(N)`:生成平行区域的`N`帮派\n*   `num_workers(M)`:为平行区域生成`M`工人。\n*   `vector_length(Q)`:对平行区域使用`Q`的矢量长度\n\n例如，在下面的代码片段中，我们将组的数量设置为`2`，工人的数量设置为`2`，向量长度设置为`32`:\n\n```cpp\n#pragma acc parallel num_gangs(2) \\\n  num_workers(2) vector_length(32)\n{\n  #pragma acc loop gang worker\n  for(int x = 0; x < 4; x++){\n    #pragma acc loop vector\n    for(int y = 0; y < 32; y++){\n      array[x][y]++ ;\n    }\n  }\n}\n```\n\n在代码中设置帮派的数量很少是一个好主意——让编译器来决定。大多数情况下，您可以通过仅调整向量长度来有效地调整循环嵌套。此外，很少对图形处理器使用工作循环。\n\n# 托管内存\n\nOpenACC 提供了一个选项，允许编译器处理内存管理。我们将能够通过自己管理内存来实现更好的性能；然而，允许编译器使用托管内存非常简单。我们不需要对代码进行任何更改来使托管内存工作。\n\n为了利用托管内存，我们可以这样将托管标志传递给`pgc++ `编译器:\n\n```cpp\n$ pgc++ -c -acc -ta=tesla:managed scrImagePgmPpmPackage.cpp\n$ pgc++ -c -acc -ta=tesla:managed -Minfo=accel image_merging.cpp\n$ pgc++ -o merging.out -acc -ta=tesla:managed -Minfo=accel scrImagePgmPpmPackage.o image_merging.o\n$ ./blurring.out\n```\n\n添加托管子句后，编译器基本会忽略数据子句，托管内存用于在 CPU 和 GPU 之间传输数据。请注意，托管内存仅用于堆数据，而不是堆栈/静态数据。我们在前一章中介绍的统一内存概念将保持不变。\n\n# 内核指令\n\n内核指令允许程序员退后一步，只依赖编译器。使用内核指令的一些示例代码如下:\n\n```cpp\n#pragma acc kernels \nfor (int i = 0; i < N; i++ ) \n{ \n    //< loop code > \n}\n```\n\n就像在并行指令示例中一样，我们正在并行化单个循环。回想一下，当使用并行指令时，它必须总是与循环指令配对；否则，代码将被不正确地并行化。内核指令不遵循相同的规则；在某些编译器中，添加循环指令可能会限制编译器优化代码的能力。\n\n内核指令与并行指令完全相反。这意味着编译器做出了很多假设，甚至可能会推翻程序员并行化代码的决定。此外，默认情况下，编译器会尝试优化循环。编译器通常非常擅长优化循环，有时可能能够以程序员无法描述的方式优化循环。然而，通常程序员能够通过优化循环本身来获得更好的性能。\n\n如果遇到编译器拒绝并行化循环的情况，您可能会忽略编译器的决定。(但是，请记住，通过覆盖编译器的决定，您要为并行化代码时出现的任何错误负责！)在这段代码中，我们使用独立子句向编译器保证我们认为循环是可并行的:\n\n```cpp\n#pragma acc kernels loop independent\nfor (int i = 0; i < N; i++ )\n{\n    //< loop code >\n}\n```\n\n内核指令最显著的优点之一是它能够一次并行处理多个循环。例如，在下面的代码段中，我们能够通过利用一个内核区域同时有效地并行化两个循环:\n\n```cpp\n#pragma acc kernels\n{\n    for (int i = 0; i < N; i++ )\n    {\n        //< loop code >\n    } \n... some other sequential code\n    for (int j = 0; j < M; j++ )\n    {\n        //< loop code >\n    }\n}\n```\n\n# 折叠条款\n\n**折叠子句**允许我们将多维循环嵌套转换为一维循环。这个过程有助于增加循环的总长度(这通常会增加并行性)，并且通常有助于内存局部性。让我们看看语法:\n\n```cpp\n#pragma acc parallel loop collapse( 3 )\nfor(int i = 0; i < N; i++)\n{\n    for(int j = 0; j < M; j++)\n    {\n        for(int k = 0; k < Q; k++)\n        {\n            < loop code >\n        }\n    }\n}\n```\n\n代码将把三维循环嵌套组合成一个一维循环。\n\n# 平铺条款\n\n**瓦片条款**允许我们将多维循环分解成*瓦片*，或者*区块*。这对于增加某些代码中的内存局部性通常很有用。让我们看看语法:\n\n```cpp\n#pragma acc parallel loop tile( 32, 32 )\nfor(int i = 0; i < N; i++)\n{\n    for(int j = 0; j < M; j++)\n    {\n        < loop code >\n    }\n}\n```\n\n前面的代码将把我们的循环迭代分解成 32×32 块，然后并行执行这些块。\n\n# CUDA 互操作性\n\n正如本章前面提到的，OpenACC 不是 CUDA 语言的替代品；事实上，开发人员可以开始利用 OpenACC 将热点端口连接到一个 GPU。他们可以开始只为最关键的功能集成 CUDA 内核。有几种方法可以将 OpenACC/CUDA 转换成可互操作的代码。我们将在本节中研究其中的一些。\n\n# DevicePtr 条款\n\n该子句可用于映射使用`cudaMalloc`分配的 CUDA 设备指针，并将其传递给 OpenACC。下面的代码片段显示了`deviceptr`子句的用法:\n\n```cpp\ndouble *cuda_allocate(int size) {\n    double *ptr;\n    cudaMalloc((void**) &ptr, size * sizeof(double));\n    return ptr;\n}\nint main() {\n    double *cuda_ptr = cuda_allocate(100); \n    // Allocated on the device, but not the host!\n\n    #pragma acc parallel loop deviceptr(cuda_ptr)\n    for(int i = 0; i < 100; i++) {\n        cuda_ptr[i] = 0.0;\n    }\n}\n```\n\n通常，OpenACC 运行时期望获得一个主机指针，然后将该指针转换为一些相关的设备指针。`deviceptr`子句是告诉 OpenACC 运行时不应该翻译给定指针的一种方式，因为它已经是一个设备指针。\n\n# 常规指令\n\n最后要讨论的主题是在 OpenACC 并行和内核区域中使用 CUDA 设备功能。这些函数被编译成由 GPU 内核或 OpenACC 区域内的加速器调用。为了在我们的 OpenACC 循环中使用 CUDA `__device__`函数，我们还可以使用例程指令:\n\n```cpp\n//In CUDA code\nextern \"C\" __device__\nint cuda_func(int x) {\n        return x*x;\n}\n\n//In OpenACC Code\n#pragma acc routine seq\nextern int cuda_func(int);\n\n...\n\nint main() {\n    A = (int*) malloc(100 * sizeof(int));\n    #pragma acc parallel loop copyout(A[:100])\n    for(int i = 0; i < 100; i++) {\n        A[i] = cuda_func(i);\n    }\n}\n```\n\nPlease note that this chapter provides a practical approach to making use of OpenACC and does not cover the whole standard API. For extensive API information, see [https://www.openacc.org/.](https://www.openacc.org/) \n\n# 摘要\n\n在本章中，我们为您提供了一种使用图形处理器的替代方法。这种使用 OpenACC 的基于指令的编程方法非常受遗留应用的欢迎，对于新应用，它也提供了一种非常简单和可移植的方法。使用这种方法，您可以看到编译器是如何变得更加高级的。用户对指令的反馈已经被使用，利用指令可以为不同的体系结构生成最佳的并行代码。\n\n我们介绍了并行指令，这些指令向编译器提供指令/提示，告诉编译器代码中的哪一部分是并行的。我们还利用数据指令来控制数据传输，而不是依赖托管内存。通过使用异步子句，我们还尝试通过重叠内核和数据传输来优化我们的应用。我们探索了将 OpenACC 构造映射到 CUDA 层次结构，以及 OpenACC 和 CUDA C/C++ 代码如何进行互操作。\n\n在下一章中，我们将开始将我们的 CUDA 知识应用到深度学习中。********"
  },
  {
    "path": "docs/learn-cuda-prog/10.md",
    "content": "# 九、利用 CUDA 实现深度学习加速\n\n深度学习是一种基于人工神经网络解释数据的机器学习方法。具体来说，我们提供机器可以理解的数据，并构建从数据中学习表示的神经网络模型。我们可以使用这种技术来构建识别语音、从图像中分类对象、理解文本、翻译语言、转换数据域等模型。基本神经网络包括**全连接层**(**FCL**)**卷积神经网络** ( **CNN** )和**递归神经网络** ( **RNN** )。这些架构在数据分类、区域理解和顺序关系方面表现出很高的准确性。\n\n深度学习需要大量的计算，以便广泛使用。然而，这个问题得到了解决，因为我们可以通过使用 GPU 计算能力来显著减少训练时间。这是因为神经网络的基本架构是基于矩阵运算的，而 GPU 是为此而优化的硬件平台。具体来说，深度学习的创新是通过英伟达 CUDA 加速来解决的，因为深度学习中的许多算法都可以加速。\n\n在本章中，我们将简要回顾神经网络操作，并讨论如何在图形处理器上加速这些操作。作为实践，我们将使用 cuDNN 和 cuBLAS CUDA 库实现一个卷积网络。cudn 库是英伟达的 CUDA 库，专门优化深度学习操作。我们将分三个部分介绍它的实现。我们还将介绍图形处理器如何优化所需的操作。然后，我们将通过比较**长短时记忆** ( **LSTM** )网络的性能来讲述使用 cuDNN 库是如何有效的。然后，我们将使用**英伟达工具扩展** ( **英伟达**)来介绍深度学习中的评测方法。这将测量图形处理器上的网络操作，以便我们可以分析时间线中的操作并了解它们的性能。\n\n在本章中，我们将涵盖以下主题:\n\n*   利用 CUBLAS 实现全连接层加速\n*   基于 cuDNN 的元素层\n*   cuDNN/CUDA 中的 Softmax 和损失函数\n*   带有 cuDNN 的卷积神经网络\n*   带有 CUDA 的递归神经网络\n*   剖析深度学习框架\n\n# 技术要求\n\n本章要求安装 cudn 库和 CUDA 工具包。我们还需要支持 CUDA 的图形处理器。本章将涵盖深度学习的基础及其性能，因此不需要新的 GPU 功能。换句话说，如果你覆盖了前面章节的大部分内容，你将有一个合适的图形处理器可以使用。\n\n要安装 cuDNN 库，需要从[https://developer.nvidia.com/cudnn](https://developer.nvidia.com/cudnn)下载软件包。您需要登录 NVIDIA 开发人员网站才能访问下载页面。如果您还没有 NVIDIA 开发人员帐户，您需要注册该帐户。确保 cudn 是用您安装的 CUDA 版本编译的。\n\n# 利用 cuBLAS 实现全连接层加速\n\n全连接层是深度学习的基本架构。让我们回顾一下它的操作，看看 CUDA 是如何在前向和反向传播过程中加速神经网络的。然后，我们将把它们应用于图形处理器。\n\n# 神经网络运算\n\n神经网络的基本操作是在输入数据和参数之间执行点操作。我们称之为感知。在深度学习中，神经网络以分层的方式连接多个感知。我们称之为前馈神经网络。下图显示了感知器和基本神经网络:\n\n![](img/451a22fa-7568-4602-a522-e2dd3826e53e.png)\n\n感知器的基本操作是用输入数据和适当的权重创建点积。然后，它执行具有激活功能的非线性操作，例如 sigmoid 或**整流器线性单元** ( **ReLU** )。在前馈神经网络中，操作只是仿射变换，然后应用激活函数。一个向量将作为输入输入到神经网络，并将其与两层中每个节点之间的权重参数相乘。\n\n为了训练神经网络，我们执行前向传播、损耗计算和梯度反向传播，然后使用更新参数。让我们简单介绍一下。然后，我们将使用 cuBLAS 和其他 CUDA 操作来匹配每个步骤。\n\n正向操作可由以下等式表示:\n\n![](img/3bbf27ea-c1c4-4091-bfca-a7ef3640f55d.png)\n\n这里，![](img/f58e3de0-2d4a-43ab-888b-3ffe2c39d537.png)是给定输入向量的预测结果，![](img/3b5c5481-dfe4-49e6-9bfd-16777e251f7f.png)，![](img/bf47e85d-b819-4362-ba4b-2c84785e42ce.png)是权重参数矩阵，![](img/7fc375a7-123f-463e-8357-d6907f6095b5.png)是激活函数。我们可以看到，全连通层的基本运算是矩阵运算。因此，我们需要对输入和激活函数实现矩阵乘法运算。因为我们承担分类任务，所以我们使用 softmax 函数来标准化输出，并在下一层获得概率分布结果。\n\n为了获得真值之间的损失，我们对标签应用单向编码，并通过获得每个元素的熵来获得交叉熵损失，如下所示:\n\n![](img/3ff26108-c02a-4604-9eaf-e1a5d04df24f.png)\n\n我们可以通过每个交叉熵损失的和得到总损失值。然后，我们可以从前面的等式中获得梯度。这看起来是一个复杂的操作，但可以简化，如下所示:\n\n![](img/fd0cf659-df61-472c-8fae-26facd67cc53.png)\n\n现在，我们将梯度传播到上一层，这被称为反向传播。在这个任务中，我们使用链式规则来获得每个权重和偏差参数的梯度。然后，我们可以更新权重参数的设置和偏差。例如，我们可以通过以下等式获得权重和偏差的梯度:\n\n![](img/78c891c9-a052-46b4-ba22-11229094a2dc.png)\n\n我们可以通过下面的等式获得传播到前一层的梯度:\n\n![](img/29f600b2-c4fd-4055-8b00-063de51c91a9.png)\n\n这里，![](img/d6c48113-6009-476e-b53a-61484c12014e.png)是激活函数的梯度。因此，我们需要从第二层获取![](img/9daa0c33-638e-4a0f-a64c-f6ad7d3b4b60.png)作为第一层。然后，第一层的权重和偏差梯度可以通过以下等式获得:\n\n![](img/10ef2652-3473-4250-ab5a-08b258b45757.png)\n\n现在，我们可以根据梯度后代规则更新权重和偏差，如下所示:\n\n![](img/c829793e-26f4-4c44-8a50-d03e1c87a45b.png)、![](img/db73f6e5-a09f-4f6b-b850-9f78b7719aaa.png)\n\n这里，![](img/1da8983e-1bb9-4ea6-9d8f-39426d0661f3.png)是迭代步骤。\n\n激活函数![](img/2eb7280e-6e41-45df-889f-049a2ed74da8.png)的梯度和类型可以不同。下一节将介绍这个激活层的实现。激活函数的推导可由以下等式表示:\n\n![](img/3d8453c6-78df-4735-85c0-6b13d98729a1.png)、![](img/da9de664-20a0-4af2-9b7a-95841c4fd8e6.png)\n\n因此，神经网络运算是一组线性代数运算，可以被 cuBLAS 库覆盖。实现的代码可以在`01_ann`中找到。我们将在*实现全连接层、*T3】实现层操作和*实现 softmax 层*部分介绍这些实现细节。\n\n# 神经网络层的设计\n\n在我们编写代码之前，让我们介绍一下如何将操作打包到层配置中:\n\n1.  首先，我们执行正向操作。\n2.  然后，我们执行反向操作。\n3.  然后我们从梯度得到一个权重更新。\n4.  最后，输出层将获得损失。\n\n以这种方式，该层可以被配置如下:\n\n![](img/b6bf914f-00bd-414e-b88f-bc36d05bb945.png)\n\n根据工作流程，它有标准化的输入和输出以及两种类型的输入。左侧数据路径将使用输入来命名，而右侧将使用输出来命名。数据分两个阶段输入(向前和向后)。我们将使用 blobs 来管理参数和输入/输出数据。blob 是跨层处理的数据的包装，有助于管理内存空间。我们将使用这种设计来简化网络的每一层配置。每个层都有每个斑点的描述符和前向/后向处理操作。\n\n现在，让我们创建一个图层类，它将是所有图层的基类。下面的代码展示了`class`公共函数是如何堆叠的。并且，你可以在`layer.h`找到它的实现，在`01_ann/src/ directory`找到`layer.cu`。这不仅包括向前和向后操作，还包括重量更新控制和损失计算:\n\n```cpp\nclass Layer\n{\npublic:\n    Layer();\n    ~Layer();\n\n    std::string get_name() { return name_; }\n\n    virtual Blob<float> *forward(Blob<float> *input) = 0;\n    virtual Blob<float> *backward(Blob<float> *grad_input) = 0;\n\n    virtual float get_loss(Blob<float> *target);\n    virtual int   get_accuracy(Blob<float> *target);\n\n    void set_cuda_context(CudaContext *context) { cuda_ = context; }\n\n    /* weights update control */\n    void freeze() { freeze_ = true; }\n    void unfreeze() { freeze_ = false;}\n    void set_load_pretrain() { load_pretrain_ = true; }\n    void set_gradient_stop() { gradient_stop_ = true; }\n```\n\n为了支持这些操作，层类维护了几个 cuDNN 描述符、blob 指针和权重更新控制器。当我们介绍网络实施时，将会介绍详细的实施:\n\n```cpp\nprotected:\n    std::string name_;\n\n    // Tensor descriptor for the input/output tensor\n    cudnnTensorDescriptor_t input_desc_;\n    cudnnTensorDescriptor_t output_desc_;\n    // filter and bias descriptor for weights and biases\n    cudnnFilterDescriptor_t filter_desc_;\n    cudnnTensorDescriptor_t bias_desc_;\n\n    // output memory\n    Blob<float> *input_ = nullptr;       /* x */\n    Blob<float> *output_ = nullptr;      /* y */\n    Blob<float> *grad_input_ = nullptr;  /* dx */\n    Blob<float> *grad_output_ = nullptr; /* dy */\n\n    // master weights & bias\n    bool freeze_ = false;               /* control parameter updates */\n    Blob<float> *weights_ = nullptr;      /* w */\n    Blob<float> *biases_  = nullptr;      /* b */\n    Blob<float> *grad_weights_ = nullptr; /* dw */\n    Blob<float> *grad_biases_  = nullptr; /* db */\n\n    int batch_size_ = 0; // mini-batch size\n\n    // cuda handle container\n    CudaContext *cuda_ = nullptr;\n\n    // initialize weights along with the input size\n    void init_weight_bias(unsigned int seed = 0);\n    void update_weights_biases(float learning_rate);\n\n    // pretrain parameters\n    bool load_pretrain_ = false;\n    int load_parameter();\n    int save_parameter();\n\n    // gradient stop tagging\n    bool gradient_stop_ = false;\n\n    friend class Network;\n}\n```\n\n该层类将在其他部分的深度学习网络实施中使用。因此，它有用于 cuDNN 操作的`cudnnTensorDescriptor_t`变量，以及`get_loss()`和`get_accuracy()`函数。\n\n# 张量和参数容器\n\n在我们的实现中，我们将使用名为`Blob`的数据容器。它的名字是从 Caffe 借来的。这允许我们存储张量或网络参数及其尺寸信息和记忆点。我们将用这个连接每一层。这有助于每个层根据输入张量的大小信息初始化其权重。此外，每一层都可以基于`Blob`的信息验证其结果。\n\n这个斑点需要神经网络中的尺寸信息，如下面一行代码所示。然后，它的构造函数将根据大小信息创建一个主机端缓冲区:\n\n```cpp\nBlob<T>(int n, int c, int h, int w)\n```\n\n`Blob`还可以处理主机和设备中的记忆，并可以帮助我们访问这些记忆。`Blob`具有以下内存访问助手功能:\n\n```cpp\n// get specified memory pointer\nftype *ptr() { return h_ptr_; }\n\n// get cuda memory\nftype *cuda() \n{ \n    if (d_ptr_ == nullptr) \n        cudaMalloc((void**)&d_ptr_, sizeof(ftype) * len());\n    return d_ptr_;\n}\n\n// transfer data between memory\nftype *to(DeviceType target) { \n    ftype *ptr = nullptr;\n    if (target == host)\n    {\n        cudaMemcpy(h_ptr_, cuda(), sizeof(ftype) * len(), \n                   cudaMemcpyDeviceToHost);\n        ptr = h_ptr_;\n    }\n    else // DeviceType::cuda\n    {\n        cudaMemcpy(cuda(), h_ptr_, sizeof(ftype) * len(), \n                   cudaMemcpyHostToDevice);\n        ptr = d_ptr_;\n    }\n    return ptr;\n}\n```\n\n正如我们前面讨论的，`Blob`可以存储张量，我们还需要提供张量形状信息作为 cuDNN APIs 所需的描述符。因此，`Blob`可以使用以下代码创建和设置张量描述符:\n\n```cpp\n/* Tensor Control */\nbool is_tensor_ = false;\ncudnnTensorDescriptor_t tensor_desc_;\ncudnnTensorDescriptor_t tensor()\n{\n    if (is_tensor_)\n        return tensor_desc_;\n\n    cudnnCreateTensorDescriptor(&tensor_desc_);\n    cudnnSetTensor4dDescriptor(tensor_desc_, \n                                CUDNN_TENSOR_NCHW, CUDNN_DATA_FLOAT,\n                                n_, c_, h_, w_);\n    is_tensor_ = true;\n    return tensor_desc_;\n}\n```\n\n现在，让我们使用`Blob`实现一个完全连接的层。\n\n# 实现完全连接的层\n\n在本节中，我们将使用 cuBLAS 编写一个完全连接的网络。对于这一层，我们将创建一个从`Layer`类派生的`Dense`类。类构造函数将接收默认的层配置信息，如下所示:\n\n```cpp\nDense::Dense(std::string name, int output_size)\n{\n    name_ = name;\n    output_size_ = output_size;\n}\n```\n\n但这不足以配置整个层。缺失的信息将从输入中提供，因为输入大小将由前一层决定。现在，让我们讨论正向传播。\n\n# 实现正向传播\n\n在前向传播中，我们可以将前向过程分为两个步骤，如下所示:\n\n![](img/95d7cd68-8fed-4f8a-bd7c-0f1d9e9a2afb.png)\n\n由于重量大小不必受批次大小的影响，我们只考虑输入重量和输出重量的数量。另一方面，数据馈送斑点，如输入和输出，受到批次大小的影响。因此，我们使用滤波器和输入数据的 GEMM 运算可以设计如下:\n\n![](img/7a96d4c2-0e5a-44ce-8251-e09d28e5e49a.png)\n\n隐藏的输出将与偏置值相加。输入数据不限于来自数据加载器的数据。当我们堆叠层时，前一层的输出将是当前层的输入数据。正向操作可以如下实现:\n\n```cpp\nBlob<float> *Dense::forward(Blob<float> *input) {\n  .. { blob initialization } ..\n\n  // output = weights^T * input (without biases)\n  cublasSgemm(cuda_->cublas(),\n        CUBLAS_OP_T, CUBLAS_OP_N, output_size_, \n        batch_size_, input_size_,\n        &cuda_->one, weights_->cuda(), input_size_,\n        input_->cuda(), input_size_,\n        &cuda_->zero, output_->cuda(), output_size_);\n\n  // output += biases * one_vec^T\n  cublasSgemm(cuda_->cublas(), \n        CUBLAS_OP_N, CUBLAS_OP_N, output_size_, batch_size_, 1,\n        &cuda_->one, biases_->cuda(), output_size_, one_vec, 1, \n        &cuda_->one, output_->cuda(), output_size_);\n  return output_;\n}\n```\n\n在第一次迭代中，每一层都需要初始化它的权重和偏差。例如，这个`Dense`层可以初始化它的权重、偏差和输出张量元素。我们可以将这个初始化任务分成两个阶段。首先是权重和偏差，如下所示:\n\n```cpp\n// initialize weights and biases\nif (weights_ == nullptr)\n{\n    // setup parameter size information\n    input_size_ = input->c() * input->h() * input->w();\n\n    // initialize weight, bias, and output\n    weights_ = new Blob<float>(1, 1, input_size_, output_size_);\n    biases_ = new Blob<float>(1, 1, output_size_);\n}\n```\n\n接下来的阶段是关于更新输入信息和初始化输出 blob。当它是新的或需要重新配置时，我们需要执行以下操作。在这个任务中，我们还需要创建一个充满我们批量大小的向量。这将用于偏差添加:\n\n```cpp\n// initilaize input and output\nif (input_ == nullptr || batch_size_ != input->n())\n{\n  input_ = input;\n  batch_size_ = input->n();\n\n  if (output_ == nullptr)\n    output_ = new Blob<float>(batch_size_, output_size_);\n  else\n    output_->reset(batch_size_, output_size_);\n\n  output_->tensor();\n\n  if (d_one_vec != nullptr)\n    cudaFree(d_one_vec);\n  checkCudaErrors(cudaMalloc((void**)&d_one_vec, sizeof(float) * batch_size_));\n  init_one_vec<<< (batch_size_+BLOCK_DIM_1D-1)/BLOCK_DIM_1D, BLOCK_DIM_1D >>>(d_one_vec, batch_size_);\n\n  if (!freeze_)\n    init_weight_bias();\n}\n```\n\n这个初始化任务不仅触发了第一次迭代，还触发了批量大小的改变。在培训阶段不需要检查批次大小，但在测试阶段会很有用。这是因为训练和推理中的批量大小不同。在这种情况下，我们需要按照新的批处理大小创建一个输出 blob。输出张量的大小被确定为通道大小。输出 blob 的创建代码如下，创建一个大小为(`batch_size_`、`output_size_`、`1`、`1`)的 blob:\n\n```cpp\noutput_ = new Blob<float>(batch_size_, output_size_);\n```\n\n这就产生了扁平张量。然后，我们喂养这些张量，这要求它们在通道中对齐。这种对齐在 softmax 层中是特别需要的。我们将在 softmax 层的实现中介绍这一点。\n\n这个阶段的另一个重要任务是初始化权重和偏差。在我们的实现中，我们将使用 ReLU 作为激活器。我们将使用普通初始化器([https://arxiv.org/abs/1502.01852](https://arxiv.org/abs/1502.01852))技术来使网络可训练。遵循上一篇文章中的指导原则，可以使用以下等式生成所需的权重值:\n\n![](img/85eca19f-7734-471a-b5a7-8191808d66f5.png)\n\n![](img/0ed79072-61db-46e8-a6bc-66049715f0ff.png)是前一层输入的数量。为此，我们可以在更新输入张量信息后初始化参数。此外，偏置值将被初始化为`0`。下面的代码显示了这个的实现:\n\n```cpp\nvoid Layer::init_weight_bias(unsigned int seed)\n{\n    // Create random network\n    std::random_device rd;\n    std::mt19937 gen(seed == 0 ? rd() : static_cast<unsigned int>\n                                        (seed));\n\n    // He normal distribution\n    float range = sqrt(6.f / input_->size());\n    std::uniform_real_distribution<> dis(-range, range);\n\n    for (int i = 0; i < weights_->len(); i++)\n        weights_->ptr()[i] = static_cast<float>(dis(gen));\n    for (int i = 0; i < biases_->len(); i++)\n        biases_->ptr()[i] = 0.f;\n\n    // copy initialized value to the device\n    weights_->to(DeviceType::cuda);\n    biases_->to(DeviceType::cuda);\n}\n```\n\n现在，让我们讨论反向传播。\n\n# 实现反向传播\n\n正如我们前面讨论的，从下一层开始的梯度会传播到这一层。基于传播梯度，我们需要获得权重、偏差和数据的三个梯度(输入梯度)。我们需要创建可以存储它们的 blobs。它们的大小不取决于批次大小，所以我们只需要确保我们创建了它们。下面的代码展示了我们如何为此目的创建 blobs:\n\n```cpp\nif (grad_weights_ == nullptr) {\n  grad_output_ = grad_output;\n  grad_weights_ = new Blob<float>(weights_->shape());\n  grad_biases_ = new Blob<float>(biases_->shape());\n  grad_input_ = new Blob<float>(input_->shape());\n}\n```\n\n在前面的代码中，`grad_output_`表示从下一层传播的输出数据的梯度，`grad_input_`表示将传播到上一层的输入数据的梯度。因此，我们不需要创建一个`grad_output_`斑点。如果你觉得这些命名惯例很混乱，那么如果你把`grad_input_`看作![](img/84d797c7-3cb7-40e4-8b12-bf9e5be2921e.png)而把`grad_input_`看作![](img/5dbe0798-7e12-4625-adcb-b2a5044495d2.png)可能会更容易。\n\n下面的代码展示了我们如何实现这一点:\n\n```cpp\nBlob<float> *Dense::backward(Blob<float> *grad_output) {\n  .. { blob initialization } ..\n\n  // db = (dy) * one_vec\n  cublasSgemv(cuda_->cublas(),\n    CUBLAS_OP_N,\n    output_size_, batch_size_,\n    &cuda_->one,\n    grad_output_->cuda(), output_size_,\n    one_vec, 1,\n    &cuda_->zero,\n    grad_biases_->cuda(), 1); \n\n  // dw = x * (dy)^T\n  cublasSgemm(cuda_->cublas(),\n    CUBLAS_OP_N, CUBLAS_OP_T,\n    input_size_, output_size_, batch_size_,\n    &cuda_->one,\n    input_->cuda(), input_size_,\n    grad_output_->cuda(), output_size_,\n    &cuda_->zero,\n    grad_weights_->cuda(), input_size_);\n\n  // dx = W * dy\n  if (!gradients_stop_)\n    cublasSgemm(cuda_->cublas(),\n      CUBLAS_OP_N, CUBLAS_OP_N,\n      input_size_, batch_size_, output_size_,\n      &cuda_->one,\n      weights_->cuda(), input_size_,\n      grad_output_->cuda(), output_size_,\n      &cuda_->zero, \n      grad_input_->cuda(), input_size_);\n\n  return grad_input_;\n}\n```\n\n如果该层是模型中的第一层，我们也可以跳过计算输入数据的梯度，因为我们不必对它做任何事情。\n\n权重和偏差更新将在我们想要更新权重时进行。在本节中，我们将为此使用**随机梯度下降** ( **SGD** )。该操作也可以用于其他层。在这里，我们将这个函数放在`Layer`类中。重量更新也可以通过`cublas`功能完成，如下所示:\n\n```cpp\nvoid Layer::update_weights_biases(float learning_rate)\n{\n  float eps = -1.f * learning_rate;\n  if (weights_ != nullptr && grad_weights_ != nullptr) {\n    // w = w + eps * dw\n    cublasSaxpy(cuda_->cublas(),\n      weights_->len(),\n      &eps,\n      grad_weights_->cuda(), 1,\n      weights_->cuda(), 1);\n  }\n\n  if (biases_ != nullptr && grad_biases_ != nullptr)\n  {\n    // b = b + eps * db\n    cublasSaxpy(cuda_->cublas(),\n      biases_->b(),\n      &eps,\n      grad_biases_->cuda(), 1,\n      biases_->cuda(), 1);\n  }\n}\n```\n\n如您所见，我们可以用学习速率更新权重和偏差。当然，您也可以更改`eps`操作来应用其他优化算法。\n\n# 层终端\n\n在 C/C++ 编程中，程序员应该讲述如何在终止类实例时返回已使用的资源。按照我们的设计，如果图层有权重参数，并且可以从渐变中更新，那么图层最多会创建六个斑点。下面的代码显示了层终止代码，它终止内部创建的斑点:\n\n```cpp\nLayer::~Layer()\n{\n  if (output_ != nullptr) delete output_;\n  if (grad_input_ != nullptr) delete grad_input_;\n\n  if (weights_ != nullptr) delete weights_;\n  if (biases_ != nullptr) delete biases_;\n  if (grad_weights_ != nullptr) delete grad_weights_;\n  if (grad_biases_ != nullptr) delete grad_biases_;\n}\n```\n\n输入斑点或张量描述符将由其他层或斑点终端处理。图层类是其他图层的基类。因此，我们可以专注于终止自定义创建的资源，因为当我们终止任何派生的层时，这个终止代码将被一起调用。\n\n即使我们已经构建了网络和层，我们也应该开发一些额外的层来完成网络。例如，我们没有实现激活、softmax 和损失计算层。我们将在接下来的章节中介绍这些层。\n\n# 带有 cuDNN 的激活层\n\n在神经网络层中有许多元素操作。激活功能是这些操作之一。cuDNN 库提供了六个激活函数:sigmoid、ReLU、tanh、clipped ReLU、eLU 和 identity。在 cuDNN 库中，`cudnnActivationForward()`做正向运算，`cudnnActivationBackward()`做反向运算。\n\n我们来看看`cuddnnActivationForward()`函数的界面，如下图:\n\n```cpp\ncudnnStatus_t cudnnActivationForward( cudnnHandle_t handle,\n    cudnnActivationDescriptor_t activationDesc,\n    const void *alpha, const cudnnTensorDescriptor_t xDesc, \n    const void *x, const void *beta,  \n    const cudnnTensorDescriptor_t yDesc, void *y)\n```\n\n使用`cudnnActivationDescriptor_t`，我们可以确定激活功能的类型。Alpha 和 beta 是标量值，决定了要添加的输入速率。`xDesc`和`yDesc`保存张量的形状信息。可以使用`cudnnCreateTensorDescriptor()`创建它们。\n\n当你查看`cudnnActivationBackward()`函数时，`dy`是下一层的渐变输入，`dx`是上一层的渐变输出。在这种情况下，`y`成为输入。以这种方式，`dyDesc`提供梯度输入形状信息，而`dxDesc`提供梯度输出形状信息:\n\n```cpp\ncudnnStatus_t cudnnActivationBackward( cudnnHandle_t handle,\n    cudnnActivationDescriptor_t activationDesc,\n    const void *alpha, const cudnnTensorDescriptor_t yDesc,  \n    const void *y,\n    const cudnnTensorDescriptor_t dyDesc, const void *dy,\n    const cudnnTensorDescriptor_t xDesc,  const void *x,\n    const void *beta,  const cudnnTensorDescriptor_t dxDesc, void *dx)\n```\n\n一般来说，我们可以预期层与层之间的张量形状不会改变。因此，我们可以对`x`和`dx`使用相同的张量描述符。和使用`y`和`dy`一样。\n\n现在，让我们使用 cuDNN 应用编程接口实现启用 cuDNN 的激活功能。为了使用 cuDNN API，我们需要提供一个张量描述符来指定 cudn 函数的输入和输出张量维度。我们还需要指定激活操作。\n\n# 层配置和初始化\n\n虽然我们的示例实现没有使用层接口，但是我们需要将示例集成到层接口中。在我们的层设计中，激活层可以这样实现:\n\n```cpp\nclass Activation: public Layer\n{\npublic:\n  Activation(std::string name, cudnnActivationMode_t mode, \n             float coef = 0.f);\n  ~Activation();\n\n  Blob<float> *forward(Blob<float> *input);\n  Blob<float> *backward(Blob<float> *grad_input);\n\nprivate:\n  cudnnActivationDescriptor_t act_desc_;\n  cudnnActivationMode_t mode_;\n  float coef_;\n};\n```\n\n在初始化步骤，我们需要创建几个张量描述符和一个激活描述符。cuDNN 库要求开发人员提供张量大小或对应于 API 的任何其他操作句柄:\n\n```cpp\nActivation::Activation(std::string name, cudnnActivationMode_t mode, float coef)\n{\n  name_ = name;\n  mode_ = mode;\n  coef_ = coef;\n\n  cudnnCreateActivationDescriptor(&act_desc_);\n  cudnnSetActivationDescriptor(act_desc_, mode, CUDNN_PROPAGATE_NAN, coef);\n}\n```\n\n在 cuDNN 中，我们使用激活描述符来指定激活函数操作。我们通过`cudnnSetActivationDescriptor()`功能来实现。然后，可以确定`cudnnActivationForward/Backward()`功能的运行。我们将在下一节讨论这个问题。然而，在此之前，我们需要实现类析构函数，以便它销毁激活描述符，如下所示:\n\n```cpp\ncudnnDestroyActivationDescriptor(activation_desc);\n```\n\n现在，让我们来介绍一下激活层的向前和向后操作。\n\n# 实现分层操作\n\n这也称为谨慎操作。这一层不需要我们处理权重和偏差，因此它比密集层更容易实现。\n\n# 实现正向传播\n\n在第一次迭代中，我们需要初始化输入描述符、输出描述符和输出 blob。当批处理大小改变时，我们将更新输出 blob。然而，我们不必初始化权重和偏差，因为它没有这些。下面的代码展示了它的实现:\n\n```cpp\nif (input_ == nullptr || batch_size_ != input->n())\n{\n  input_ = input;\n  input_desc_ = input->tensor();\n  batch_size_ = input->n();\n\n  if (output_ == nullptr)\n    output_ = new Blob<float>(input->shape());\n  else\n    output_->reset(input->shape());\n\n  output_desc_ = output_->tensor();\n}\n```\n\n初始化后，我们使用 cuDNN 中的`cudnnActivationForward()`函数进行激活过程，如下所示:\n\n```cpp\ncudnnActivationForward(cudnnHandle, act_desc_, \n    &one, input_desc_, d_input, &zero, output_desc_, d_output);\n```\n\n这个激活函数的操作是在我们初始化这个层时决定的，正如我们前面讨论的。\n\n# 实现反向传播\n\n下一步是实现反向传播。我们将重用已经有的输入/输出张量描述符。现在，我们必须初始化我们想要反向传播的梯度:\n\n```cpp\nif (grad_input_ != grad_output_)\n{\n  grad_output_ = grad_output;\n  grad_input_ = new Blob<float>(input_->shape());\n  grad_input_->reset(input_->shape()); \n}\n```\n\n初始化后，我们可以调用`cudnnActivationBackward()`函数，如下所示:\n\n```cpp\ncudnnActivationBackward(cudnnHandle, activation_desc, \n    &one, output_desc_, output_->cuda(), output_desc_, \n    d_grad_output, input_desc_, input_->cuda(),\n    &zero, input_desc_, grad_input_->cuda());\n```\n\n请注意，我们重用了在正向传递中创建的输入张量描述符和输出张量描述符。我们可以这样做，因为激活操作不会改变张量的大小。我们可以通过使用 cuDNN 应用编程接口激活反向传播来简化我们的实现。\n\n`cudnnActivationBackward()`功能的输出为`d_grad_input`。正如我们在上一节中所描述的，这个渐变将被传递到下层。\n\n现在，我们将实现 softmax 层，并将我们的层实现集成为一个网络。然后，我们将讨论全连通层在图像分类任务中的准确性。\n\n# cuDNN/CUDA 中的 Softmax 和损失函数\n\n对于 MNIST 数据集分类，我们将使用 softmax 分类器。softmax 函数对输入进行归一化，并生成![](img/6a23c4d0-c4cb-4caa-b483-128315b59c21.png)概率的概率分布。softmax 操作可表示如下:\n\n![](img/f6b6d132-a44b-4e74-ade8-f2dd5d043ea5.png)\n\ncuDNN 的 softmax 转发功能支持该操作，以及通道和所有实例。之前，我们将密集层的输出与通道对齐。因此，我们将对通道应用 softmax 操作。\n\n为了确认我们的训练是否有效，我们需要计算损失函数。软最大损失函数称为交叉熵损失，因为它的损失函数用于获得跨![](img/0adcd213-509d-4129-8488-317b8bc434e8.png)概率的损失。损失函数如下:\n\n![](img/28d7a607-4b3b-400a-9382-74a6107c6d57.png)\n\n我们需要获得这个软最大损失的梯度来更新神经网络。幸运的是，推导后 softmax 损失的梯度很简单，如下所示:\n\n![](img/07eee5af-eac3-444a-b542-9acdf05f231e.png)\n\n对于正向操作，我们将使用 cuDNN 函数来获取 softmax 的输出。要获得渐变，自定义操作更加直观和简单。\n\n# 实现 softmax 层\n\n现在，让我们看看如何使用 cudn 和 CUDA 代码实现 softmax 层。\n\n# 实现正向传播\n\n我们可以从 cuDNN 库中使用`cudnnSoftmaxForward()`获得软最大成本函数的输出:\n\n```cpp\ncudnnSoftmaxForward(cudnnHandle, CUDNN_SOFTMAX_ACCURATE, \n      CUDNN_SOFTMAX_MODE_CHANNEL,\n      &one,  input_desc,  d_input, &zero, output_desc, d_output);\n```\n\n这种情况下最重要的参数设置之一是`CUDNN_SOFTMAX_MODE_CHANNEL`。该选项根据输入张量描述符信息启用通道级 softmax 操作。通过这样做，我们可以提供通过密集层的小批量输入的通道对齐的张量。\n\n# 实现反向传播\n\nsoftmax 层中的反向传递不同于其他层实现。该操作将输入数据的标签作为输入，并获得适当的梯度。如前所述，软最大损耗的梯度可以通过以下公式获得:\n\n![](img/4e9ecd44-3c23-4f2a-a26b-2e69c5e10894.png)\n\n我们可以使用`cublasSaxpy()`来实现这个操作，如下所示:\n\n```cpp\n// set grad_input_ as predict\ncudaMemcpyAsync(grad_input_->cuda(), output_->cuda(), \n                output_->buf_size(), cudaMemcpyDeviceToDevice));\n// set grad_input_ = predict - target \ncublasSaxpy(cuda_->cublas(), target->len(), &cuda_->minus_one,\n            target->cuda(), 1, grad_input_->cuda(), 1));\n```\n\n在前面的代码中，目标斑点包含一个热编码的目标向量，因此将负目标向量添加到预测值会产生适当的梯度。之后，我们需要在传播到上一层之前对批处理梯度进行标准化，如下所示:\n\n```cpp\nint grad_output_size = target->n() * target->c() * target->h() * target->w();\nfloat scale = 1.0f / static_cast<float>(target->n());\ncublasSscal(cuda_->cublas(), grad_output_size, &scale, grad_input_->cuda(), 1);\n```\n\n由于这引入了加权和的平均值，我们可以预期每个批次的梯度被归一化。\n\n# 实现损失函数\n\n计算 softmax 的损失值是可选的。这意味着它的价值在训练和推理中没有被考虑。但是，我们可以将此作为培训的指标。\n\n正如我们之前讨论的，softmax 损耗函数应实现以下等式:\n\n![](img/9e2f16bd-b541-4709-9d49-02e21c9a5aed.png)\n\n我们可以从每个样本的输出中获得损失，并使用核函数累积它们，如下所示:\n\n```cpp\n__global__ void\nsoftmax_loss_kernel(float *reduced_loss, float *predict, \n                    float *target, int size)\n{\n  int batch_idx = blockDim.x * blockIdx.x + threadIdx.x;\n\n  extern __shared__ float s_data[];\n  float loss = 0.f;\n\n  // each thread calculate entropy for each data \n  // and accumulate to shared memory\n  if (batch_idx > 0)\n    return;\n\n  for (int c = 0; c < num_outputs; c++)\n    loss += target[batch_idx * num_outputs + c] * \\\n                logf(predict[batch_idx * num_outputs + c]);\n                workspace[batch_idx] = -loss;\n\n  // Then, we do reduction the result to calculate loss \n  // Using 1 thread block\n  if (blockIdx.x > 0) return;\n\n  // Cumulate workspace data\n  s_data[threadIdx.x] = 0.f;\n  for (int i = 0; i < batch_size; i += blockDim.x)\n    s_data[threadIdx.x] += workspace[threadIdx.x + i];\n\n  __syncthreads();\n\n  // Reduction\n  for (unsigned int stride = blockDim.x / 2; stride > 0; stride >>= 1)\n  {\n    if (threadIdx.x + stride < batch_size)\n      s_data[threadIdx.x] += s_data[threadIdx.x + stride];\n    __syncthreads();\n  }\n\n  if (threadIdx.x == 0)\n    reduced_loss[blockIdx.x] = s_data[0];\n}\n```\n\n该操作使用并行约简，我们在[第 3 章](03.html)、 *CUDA 线程编程*中介绍过，以批量方式获取累积损失值。由于我们将只使用这个减少的损失值来确认训练，我们将简单地监控它的输出，而不是取它的平均值。\n\n现在，让我们用 MNIST 数据集加载器集成我们已经实现的所有层。\n\n# mnist 数据加载程序\n\n整个过程的一个重要部分是为特定数据集提供数据加载器。在本实验中，我们将使用包含 60，000 个样本的 MNIST 数据集。当初始化时，我们告诉数据加载器它应该加载火车还是测试集。之后，数据加载器将在数据集中加载一些神奇的数字，以及所有样本及其标签。加载的数据将存储在向量中，并用相同的随机种子进行混洗。由于数据加载器构建并混洗样本向量，训练循环或测试循环可以为每次迭代获得随机化的输入数据。完全实现的代码可以在本书 GitHub 存储库中的`src/mnist.cpp`文件中找到。\n\n# 管理和创建模型\n\n当我们有多个层时，我们需要一个可以用神经网络操作管理这些层的对象，即前向/后向传播和权重更新。在本实验中，我们将有一个层数组，并迭代该数组进行正向处理。例如，可以使用以下代码执行正向操作:\n\n```cpp\nBlob<float> *Network::forward(Blob<float> *input) {\n  output_ = input;\n  for (auto layer : layers_)\n    output_ = layer->forward(output_);\n\n  return output_;\n}\n```\n\n反向传播也可以通过以相反的顺序迭代数组来完成:\n\n```cpp\nvoid Network::backward(Blob<float> *target) {\n  Blob<float> *gradient = target;\n  // back propagation.. update weights internally.....\n  for (auto layer = layers_.rbegin(); layer != layers_.rend(); layer++) {\n    // getting back propagation status with gradient size\n    gradient = (*layer)->backward(gradient);\n  }\n}\n```\n\n如您所见，我们管理矢量中的图层，并拥有每个图层的操作。向网络中添加一个新层更简单，如下面的代码所示:\n\n```cpp\nvoid Network::add_layer(Layer *layer) {\n  layers_.push_back(layer);\n}\n```\n\n通过使用`Network`类，我们可以使用各种模型管理功能，比如参数更新、图层注册、图层初始化等等。此外，我们可以构建一个类似现代深度学习框架的神经网络。例如，我们可以创建如下模型:\n\n```cpp\n// step 1\\. loading dataset\nMNIST data_loader = MNIST(\"./dataset\");\n// create training dataset loader and shuffling the data\ndata_loader.train(batch_size, true);  \n\n// step 2\\. model initialization\nNetwork model;\nmodel.add_layer(new Dense(\"dense1\", 500));  // 1st layer\nmodel.add_layer(new Dense(\"dense2\", 10));   // 2nd layer\nmodel.cuda();     // set cuda context for each layer\n```\n\n我们还可以进行以下训练循环:\n\n```cpp\n// get data sample's shared buffer\nBlob<float> *train_data   = data_loader.get_data();   \n// get target's shared buffer\nBlob<float> *train_target = data_loader.get_target(); \n// load data and targets with the batch size\ndata_loader.get_batch();    \ntp_count = 0;  step = 0;\nwhile (step < num_steps)\n{\n  // transfer loaded data to the GPU\n  train_data->to(cuda);\n  train_target->to(cuda);\n\n  model.forward(train_data);    // forward\n  model.backward(train_target); // backward\n  learning_rate *= 1.f / (1.f + lr_decay * step);\n  model.update(learning_rate);  // update\n\n  step = data_loader.next(true); // load next data\n\n  ... monitoring logic ...\n}\n```\n\n对于测试阶段，我们为测试数据集创建另一个数据集加载器，并且只使用正向传递进行迭代。下面的代码显示了它的实现:\n\n```cpp\ntest_data_loader.test(batch_size_test);                   // create test dataset loader\nBlob<float> *test_data = test_data_loader.get_data();     // get sample data shared buffer\nBlob<float> *test_target = test_data_loader.get_target(); // get target shared buffer\ntest_data_loader.get_batch();    // load samples and targets with the batch size\ntp_count = 0; step = 0;\nwhile (step < num_steps_test) {\n  // transfer loaded data to the GPU\n  test_data->to(cuda);\n  test_target->to(cuda);\n\n  model.forward(test_data);  // forward\n  tp_count += model.get_accuracy(test_target);\n\n  step = test_data_loader.next(); // load next data\n}\nfloat accuracy = 100.f * tp_count / num_steps_test / batch_size_test;\n```\n\n在测试阶段，我们将在完成测试数据集中所有样本的测试后获得准确性。现在，我们需要在测试循环后获得精度。\n\n# 使用 MNIST 数据集进行网络训练\n\n现在，让我们运行我们实现的代码，看看它的结果。对于训练阶段，我们将迭代 2400 步，批量为 256。MNIST 数据集在训练集中有 60，000 个样本。2400 步意味着我们将经历大约 10 个时代的迭代。可以使用以下命令编译示例代码:\n\n```cpp\n$ nvcc -run -m64 -std=c++ 11 -I/usr/local/cuda/samples/common/inc -gencode arch=compute_70,code=sm_70 -lcublas -lcudnn -lnvToolsExt -o train ./train.cpp ./src/layer.cu ./src/loss.cu ./src/mnist.cpp ./src/network.cpp\n```\n\n下面的截图显示了我们实施的培训和测试输出:\n\n![](img/1308649b-62e6-4f67-a98f-876d835049b2.png)\n\n在训练迭代中，网络从训练数据集获得了 92%的准确率。然而，测试准确率只有 77%，相对于训练结果来说，这是一个相对较低的分数。推理在训练和推理之间显示出很大的准确性差距的原因有很多。一个可能的原因是，完全连接的层没有考虑前面截图中显示的区域信息。在深度学习中，我们使用卷积层使网络学习空间信息。\n\n现在，让我们用 cuDNN 实现卷积层，将其添加到网络中，并比较模型的性能。\n\n# 带有 cuDNN 的卷积神经网络\n\ncuDNN 库为卷积运算提供了优化的性能。通过创建一个卷积层，我们将覆盖应用编程接口的前向和后向操作配置。\n\n卷积网络层使用其权重对输入数据执行卷积。当你想建立一个能感知区域信息的神经网络时，这种网络架构非常有用。回想一下[第 7 章](07.html)、*CUDA*中的并行编程模式中的卷积实现，它需要相当大的内存带宽，需要进一步优化才能获得最佳性能。然而，使用 cuDNN 库，我们也可以获得最佳性能，因为我们不必重新发明轮子。\n\n卷积层的实现类似于全连接层的实现。然而，有两个不同之处，这要归功于 cuDNN 库:我们不必像以前那样完全实现那么多细节，我们需要为操作分配一个工作空间大小。对于每一个卷积操作——正向、滤波器反向和输入反向——都需要额外的存储空间，这取决于它们的算法。该算法可以根据给定的输入/输出/滤波器张量维数而变化。详细的 API 调用将在后面处理。\n\n像其他层一样，它有三个工作阶段。对于推理阶段，我们称之为`cudnnConvolutionForward()`和`cudnnAddTensor()`。对于落后阶段，我们称之为`cudnnConvolutionBackwardData()`、`cudnnConvolutionBackwardFilter()`、`cudnnConvolutionBackwardBias()`。最后，在更新阶段，我们可以重用完全连接的层中的代码。该层的配置概述如下:\n\n![](img/dfbcecb1-0c13-4d9e-a0a3-ac60623461a2.png)\n\n在深度学习神经网络中，通常使用汇集层和卷积网络。池化层只是按照一个简单的规则选择要输出的输入数据。下图显示了最大池化的示例:\n\n![](img/e821b3b9-c7ff-4bdc-817a-1b7c5fb97325.png)\n\n使用 cuDNN 库，我们将实现这两个卷积运算。\n\n# 卷积层\n\n像一个完全连接的层，这个卷积层有权重和偏差参数。在完全连接层，我们使用了 cuBLAS，它不需要 cuDNN 相关的描述符。然而，我们将使用 cuDNN 卷积函数，因此我们需要使用滤波器描述符和卷积运算描述符。下面的代码显示了在构建层时我们应该初始化哪些资源:\n\n```cpp\nConv2D::Conv2D(std::string name,\n        int out_channels, kernel_size, stride, padding, dilation):\n        out_channels_(out_channels), kernel_size_(kernel_size),\n        stride_(stride), padding_(padding), dilation_(dilation) {\n  name_ = name;\n  cudnnCreateFilterDescriptor(&filter_desc_);\n  cudnnCreateConvolutionDescriptor(&conv_desc_);\n  cudnnSetConvolution2dDescriptor(conv_desc_,\n    padding_, padding_, stride_, stride_, dilation_,dilation_,\n    CUDNN_CROSS_CORRELATION, CUDNN_DATA_FLOAT);\n}\n```\n\n由于我们在构建模型时提供卷积运算信息，因此我们可以指定卷积描述符。然而，滤波器的操作可以在推断时指定，因为我们可以在那时学习输入张量的大小。现在，让我们在卷积层实现正向传递。\n\n# 实现正向传播\n\n如前所述，我们可以用输入张量大小初始化卷积层。这个输入张量的大小对输出张量的大小有影响。下面的代码显示了正向传递中的参数初始化步骤:\n\n```cpp\n// initialize weights and bias\nif (weights_ == nullptr) {\n  // initialize containers handles\n  cudnnSetFilter4dDescriptor(filter_desc_, \n    CUDNN_DATA_FLOAT, CUDNN_TENSOR_NCHW,\n    out_channels_, input->c(), kernel_size_, kernel_size_);\n\n  weights_ = new Blob<float>(out_channels_, input->c(), kernel_size_, kernel_size_);\n  biases_ = new Blob<float>(1, out_channels_); // bias size\n  bias_desc_ = biases_->tensor();\n}\n```\n\n然后，我们需要更新输入资源，初始化输出 blob，创建 cuDNN 工作空间，并初始化权重参数，如下所示:\n\n```cpp\n// initilaize input and output\nif (input_ == nullptr || batch_size_ != input->n()) {\n  // initialize input\n  input_ = input;\n  input_desc_ = input->tensor();\n  batch_size_ = input->n();\n\n  // getting output tensor size\n  cudnnGetConvolution2dForwardOutputDim(\n    conv_desc_, input_desc_, filter_desc_,\n    &output_size_[0], &output_size_[1], \n    &output_size_[2], &output_size_[3]);\n\n  // initialize output blob\n  if (output_ == nullptr)\n    output_ = new Blob<float>(output_size_);\n  else\n    output_->reset(output_size_);\n  output_desc_ = output_->tensor();\n\n  // initialize weights\n  if (!freeze_)\n    init_weight_bias();\n\n  // initialize workspace for cudnn\n  set_workspace();\n}\n```\n\n为了获得输出张量大小，我们使用`cudnnGetConvolution2dForwardOutputDim()`函数。该函数基于输入张量大小、卷积运算和滤波器大小输出尺寸信息。然后，我们重用在完全连接层中使用的相同参数初始化代码。\n\n要调用 cuDNN 的卷积 API，需要提供它的工作算法和工作空间内存。我们这样做是因为 cuDNN 根据卷积大小选择最优卷积算法，它的测量需要立即进行。算法确定后，cuDNN 可以确定工作空间大小。卷积层需要对前向通道、输入数据梯度和权重梯度进行卷积运算。我们需要单独处理每个算法，但是我们只能分配一个工作空间，因为该工作空间专门用于每个卷积操作。\n\n因此，我们在所需的每个卷积算法工作空间大小中创建具有最大大小的工作空间。下面的代码展示了我们如何使用它们和管理工作空间:\n\n```cpp\nConv2d::set_workspace() {\n  size_t temp_size = 0;\n\n  // fwd\n  cudnnGetConvolutionForwardAlgorithm(cuda_->cudnn(),\n    input_desc_, filter_desc_, conv_desc_, output_desc_,\n    CUDNN_CONVOLUTION_FWD_PREFER_FASTEST, 0, &conv_fwd_algo_);\n  cudnnGetConvolutionForwardWorkspaceSize(cuda_->cudnn(),\n    input_desc_, filter_desc_, conv_desc_, output_desc_, \n    conv_fwd_algo_, &temp_size);\n  workspace_size = std::max(workspace_size, temp_size);\n\n  // bwd - data\n  cudnnGetConvolutionBackwardDataAlgorithm(cuda_->cudnn(), \n    filter_desc_, output_desc_, conv_desc_, input_desc_, \n    CUDNN_CONVOLUTION_BWD_DATA_PREFER_FASTEST, 0, \n    &conv_bwd_data_algo_);\n  cudnnGetConvolutionBackwardDataWorkspaceSize(cuda_->cudnn(),\n    filter_desc_, output_desc_, conv_desc_, input_desc_, \n    conv_bwd_data_algo_, &temp_size);\n  workspace_size = std::max(workspace_size, temp_size);\n\n  // bwd - filter\n  cudnnGetConvolutionBackwardFilterAlgorithm(cuda_->cudnn(),\n    input_desc_, output_desc_, conv_desc_, filter_desc_,\n    CUDNN_CONVOLUTION_BWD_FILTER_PREFER_FASTEST, 0, \n    &conv_bwd_filter_algo_);\n  cudnnGetConvolutionBackwardFilterWorkspaceSize(cuda_->cudnn(),\n    input_desc_, output_desc_, conv_desc_, filter_desc_, \n    conv_bwd_filter_algo_, &temp_size);\n  workspace_size = std::max(workspace_size, temp_size);\n\n  if (workspace_size > 0) {\n    if (d_workspace != nullptr)\n      cudaFree(d_workspace);\n    cudaMalloc((void**)&d_workspace, workspace_size);\n  }\n}\n```\n\n每个卷积算法都用单独的类型指定，即`cudnnConvolutionFwdAlgo_t`、`cudnnConvolutionBwdDataAlgo_t`和`cudnnConvolutionBwdFilterAlgo_t`。我们可以通过将它们声明为类成员变量来使用它们，即`conv_fwd_algo_`、`conv_bwd_data_algo_`和`conv_bwd_filter_algo_`。\n\n现在，我们编写初始化后的前向处理代码。我们与滤波器进行卷积，并添加一个偏差。以下代码显示了 cuDNN 卷积正向实现:\n\n```cpp\ncudnnConvolutionForward(cuda_->cudnn(), &cuda_->one, input_desc_, input_->cuda(), \\\n    filter_desc_, weights_->cuda(), conv_desc_, conv_fwd_algo_, d_workspace, workspace_size, \\\n    &cuda_->zero, output_desc_, output_->cuda());\ncudnnAddTensor(cuda_->cudnn(), &cuda_->one, bias_desc_, biases_->cuda(), \\\n    &cuda_->one, output_desc_, output_->cuda());\n```\n\n卷积的结果将使用输出斑点传递到下一层。\n\n# 实现反向传播\n\n在反向传播中，我们应该计算偏差的梯度、权重的梯度和输入数据的梯度。为此，我们需要在第一次迭代时创建 blobs，以便存储它们。它们的大小不取决于批次大小，所以我们只需要确保它们被创建。初始化步骤可以如下实现:\n\n```cpp\n// initialize grad_output back-propagation space\nif (grad_weights_ == nullptr) {\n  grad_output_  = grad_output;\n  grad_weights_ = new Blob<float>(weights_->shape());\n  grad_biases_  = new Blob<float>(1, biases_->c());\n  grad_input_   = new Blob<float>(input_->shape());\n}\n```\n\n然后，我们称之为 cuDNN 向后卷积 API，如下所示:\n\n```cpp\nBlob<float> *Conv2D::backward(Blob<float> *grad_output) {\n  ... { initialization step } ...\n\n  // gradients of biases\n  cudnnConvolutionBackwardBias(cuda_->cudnn(),\n    &cuda_->one, \n    output_desc_, grad_output->cuda(),\n    &cuda_->zero, \n    bias_desc_, grad_biases_->cuda());\n\n  // gradients of weights \n  cudnnConvolutionBackwardFilter(cuda_->cudnn(),\n    &cuda_->one, \n    input_desc_, input_->cuda(), \n    output_desc_, grad_output_->cuda(),\n    conv_desc_, conv_bwd_filter_algo_, d_workspace, workspace_size,\n    &cuda_->zero, \n    filter_desc_, grad_weights_->cuda());\n\n  // gradients of input data\n  if (!gradient_stop_)\n    cudnnConvolutionBackwardData(cuda_->cudnn(),\n      &cuda_->one, \n      filter_desc_, weights_->cuda(), \n      output_desc_, grad_output->cuda(), \n      conv_desc_, conv_bwd_data_algo_, d_workspace, workspace_size,\n      &cuda_->zero, \n      input_desc_, grad_input_->cuda());\n```\n\n然后，我们将输入数据的梯度传递到前一层，以传播梯度。我们将在更新步骤中使用基类的梯度更新代码来更新权重和偏差的梯度。当我们在完全连接的层中实现后向传播时，我们讨论了这一点。如果这是第一层，我们也可以跳过计算输入数据的梯度。\n\n# 使用 cuDNN 的池层\n\n池层有两个特征。首先，与卷积层相比，它的输出大小不同，cuDNN 为此提供了相应的 API。第二，它没有任何内部重量。\n\n要指定池操作，我们可以使用 cuDNN 的`cudnnPoolingDescriptor_t`函数，在类构造函数中创建并指定 cuDNN 的池描述符，如下所示:\n\n```cpp\ncudnnCreatePoolingDescriptor(&pool_desc_);\ncudnnSetPooling2dDescriptor(pool_desc_, mode_, CUDNN_PROPAGATE_NAN,\n  kernel_size_, kernel_size_, padding_, padding_, stride_, stride_);\n```\n\n现在，让我们实现池层的前向和后向操作。\n\n# 实现正向传播\n\n汇集层有助于减小张量的大小。因此，我们需要计算输出大小。我们可以使用`cudnnGetPooling2dForwardOutputDim()`函数计算大小，就像我们在卷积层实现中所做的那样。此外，张量大小取决于批次大小。这意味着如果批量改变，我们需要更新张量大小。下面的代码展示了如何初始化输入和输出斑点:\n\n```cpp\nif (input_ == nullptr || batch_size_ != input->n()) {\n  input_ = input;\n\n  // resource initialize\n  input_desc_ = input_->tensor();\n  batch_size_ = input->n();\n\n  // setting output\n  cudnnGetPooling2dForwardOutputDim(pool_desc_, input_desc_, \n    &output_size_[0], &output_size_[1], &output_size_[2], \n    &output_size_[3]);\n  if (output_ == nullptr)\n    output_ = new Blob<float>(output_size_);\n  else\n    output_->reset(output_size_);\n\n  output_desc_ = output_->tensor();\n}\n```\n\n对于正向传递，我们调用`cudnnPoolingForward()`函数，如下所示:\n\n```cpp\nBlob<float> *Pooling::forward(Blob<float> *input) {\n  ... { initialization step } ...\n\n  cudnnPoolingForward(cudnnHandle, pool_desc_, &one, \n    input_desc_, input_->cuda(),\n    &zero, output_desc_, output_->cuda());\n}\n```\n\n# 实现反向传播\n\n对于反向传播步骤，我们调用`cudnnPoolingBackward()`函数，如下所示:\n\n```cpp\nBlob<float> *Pooling::backward(Blob<float> *grad_output) {\n  if (grad_input_ == nullptr)\n    grad_input_ = new Blob<float>(input_->shape());\n\n  cudnnPoolingBackward(cudnnHandle, pool_desc_,\n    &one, output_desc_, output_->cuda(), \n    output_desc_, grad_output->cuda(), \n    input_desc_, input_->cuda(), \n    &zero, input_desc_, grad_input_->cuda());\n}\n```\n\n汇集层的输入张量形状和输入梯度相同，输出形状和输出梯度相同。因此，我们可以分别重用输入和输出的张量描述符。\n\n现在，让我们将这些集成到单个卷积层实现中。\n\n# 网络结构\n\n现在，我们将更新我们以前的网络，LeNet。网络代码可以编写如下:\n\n```cpp\nNetwork model;\nmodel.add_layer(new Conv2D(\"conv1\", 20, 5));\nmodel.add_layer(new Pooling(\"pool\", 2, 0, 2, CUDNN_POOLING_MAX));\nmodel.add_layer(new Conv2D(\"conv2\", 50, 5));\nmodel.add_layer(new Pooling(\"pool\", 2, 0, 2, CUDNN_POOLING_MAX));\nmodel.add_layer(new Dense(\"dense1\", 500));\nmodel.add_layer(new Activation(\"relu\", CUDNN_ACTIVATION_RELU));\nmodel.add_layer(new Dense(\"dense2\", 10));\nmodel.add_layer(new Softmax(\"softmax\"));\nmodel.cuda();\n```\n\n现在，我们可以开始训练和推理阶段，因为我们已经配置了我们的层，使它们相互连接。让我们用下面的命令编译代码:\n\n```cpp\n$ nvcc -run -m64 -std=c++ 11 -I/usr/local/cuda/samples/common/inc -gencode arch=compute_70,code=sm_70 -lcublas -lcudnn -lnvToolsExt -o train ./train.cpp ./src/layer.cu ./src/loss.cu ./src/mnist.cpp ./src/network.cpp\n```\n\n然后，我们可以看到如下的训练和测试结果:\n\n![](img/7bddab01-eac8-4ff6-9222-861e7e99c72a.png)\n\n如您所见，与仅使用完全连接的网络相比，该网络实现了更高的训练精度和推理。我们还可以通过查看 NVIDIA 配置文件来确认其操作，如下所示:\n\n![](img/60c3e92b-b87f-49e5-8a22-012f0da4be45.png)\n\n# 混合精度运算\n\n最新的 NVIDIA GPUs 支持混合精密运算深度学习。我们不会在这本书里讨论这个问题，因为它超出了我们的范围。但是，如果您想了解更多信息，可以访问英伟达在`/usr/src/cudnn_samples_v7/conv_sample`提供的示例。要访问这个示例，您需要从 cuDNN 网页下载示例。这个示例代码展示了如何使用 cuDNN 库使用混合精度操作。\n\n为了让 cuDNN APIs 与张量核一起工作，我们需要设置数学类型，如下所示:\n\n```cpp\ncudnnSetConvolutionMathType(cudnnConvDesc, CUDNN_TENSOR_OP_MATH);\n```\n\n然后，我们需要使用`cudnnSetTensorNdDescriptor()`初始化输入和输出张量的张量描述符。这为张量提供了填充，以便我们获得优化的张量核心性能。\n\n一个很好的基于 cuDNN 的实现是`cudnn-training`:[https://github.com/tbennun/cudnn-training](https://github.com/tbennun/cudnn-training)。它将 LeNet 实现为一系列 cuDNN 函数。您可以沿着每一行来查看 CUDNN 函数是如何工作的。\n\n如果您有兴趣使用 cuDNN 部署您的网络，请查看以下关于 GTC-CNN 使用 cuDNN 进行推理的视频([https://developer.nvidia.com/gtc/2019/video/S9644/video](https://developer.nvidia.com/gtc/2019/video/S9644/video))。这篇演讲介绍了使用 cuDNN 进行 CNN 推理的有用的性能优化技巧。\n\n在深度学习训练中使用半精度需要的不仅仅是 FP16 操作的利用率。我们需要在 FP16 中计算张量，同时在 FP32 中保持权重。此外，一些操作需要 FP32。我们称之为混合精度。cuDNN 库提供了一个名为 mnistCUDNN 的混合精度推理示例。此示例显示了输入和图层数据类型的转换。如果你想了解更多深度学习和训练中的混合精度运算，请阅读以下文章:[https://devblogs . NVIDIA . com/video-mixed-precision-technologies-tensor-cores-deep-learning/](https://devblogs.nvidia.com/video-mixed-precision-techniques-tensor-cores-deep-learning/)。\n\n现在，我们将从性能方面介绍深度学习中的其他 GPU 使用注意事项。\n\n# 递归神经网络优化\n\nrrn 允许您在深度学习中分析顺序数据。虽然这个网络有顺序依赖性，但仍有很大的优化空间。在这一节中，我们将介绍它的算法以及 cuDNN 如何提供优化的性能。\n\nRNN 有很多种，但是 cuDNN 只支持四种，分别是 RNN 带 ReLU，RNN 带 tanh，LSTM，GRU，它们有两个输入:来自前一个网络的隐藏参数和来自源的输入。根据它们的类型，它们有不同的操作。在本实验中，我们将讲述 LSTM 行动。下图显示了 LSTM 的正向操作:\n\n![](img/674b1b76-8aa5-44b7-a7cd-dada656f74b0.png)\n\n从计算的角度来看，有八个矩阵矩阵乘法和许多元素操作。根据这个估计，我们可以预期 LSTM 可能是内存受限的，因为每个操作都是内存受限的。另一方面，CUDNN 提供了`cudnnRNNForwardInference()`和`cudnnRNNFowardTraining()` RNN 功能。我们将通过测量这个函数的性能和模拟 LSTM 来介绍使用这个函数的好处。为此，我们将实现一个虚拟 LSTM 层，并将其性能与 cudn LSTM 函数进行比较。\n\n出于测试目的，我们将像这样设置超参数:\n\n```cpp\nint mode = 2; // LSTM in CUDNN\nint seq_length = 512;\nint num_layers = 4;\nint hidden_size = 512;\nint input_size = hidden_size;\nint batch_size = 32;\nfloat dropout_rate = 0;\nbool bidirectional = 0;\nint persistent = 0;\n```\n\n序列长度或隐藏大小可能会有所不同，具体取决于问题。在这个测试中，我们将使用`512`作为长度，这在序列研究中被大量使用。CUDNN API 需要更多的选项才能工作，例如辍学率、双向或单向以及持久 rnn。我们将只在这部分测试香草 LSTM。\n\n# 利用 LSTM 运算\n\n让我们编写一些代码，作为 LSTM 层执行`cudnnRNNForwardTraining()`函数:\n\n1.  我们需要初始化输入和输出内存空间。要执行 cuDNN 的 RNN 应用编程接口，我们需要使用以下变量:\n\n```cpp\n// hx, cx, hy, cy, dhy, dcy, dhx, and dcs can be null.\nvoid *x;            // input\nvoid *hx = nullptr; // input of initial hidden state\nvoid *cx = nullptr; // input of cell state (LSTM)\n\nvoid *y;            // output\nvoid *hy = nullptr; // output of final hidden state\nvoid *cy = nullptr; // output of final cell state (LSTM)\n\nvoid *dy;            // input of gradient \nvoid *dhy = nullptr; // input of final hidden state\nvoid *dcy = nullptr; // input of final cell state (LSTM)\n\nvoid *dx;            // output of gradient at the input of rnn\nvoid *dhx = nullptr; // output of gradient at the initial hidden state\nvoid *dcx = nullptr; // output of gradient at the initial cell state\n```\n\n这些变量是 LSTM 的输入和输出。为了提供输入和获得输出，我们需要分配适当的内存空间。遵循 LSTM 定义，我们需要考虑输入、输出和隐藏层的长度。这些尺寸可以确定如下:\n\n```cpp\nint input_length = seq_length * input_size * batch_size;\nint output_length = seq_length * hidden_size * batch_size;\nint hidden_length = hidden_size * batch_size * num_layers;\n```\n\n然后，我们可以为每个项目分配内存。\n\n2.  现在，我们需要为库登 RNN 应用编程接口设置张量描述符。下面的代码显示了我们应该设置的所需张量描述符:\n\n```cpp\ncudnnTensorDescriptor_t x_desc[seq_length], y_desc[seq_length], \\\n                        dx_desc[seq_length], dy_desc[seq_length];\ncudnnTensorDescriptor_t hx_desc, cx_desc;\ncudnnTensorDescriptor_t dhx_desc, dcx_desc;\ncudnnTensorDescriptor_t hy_desc, cy_desc;\ncudnnTensorDescriptor_t dhy_desc, dcy_desc;\n```\n\n对于输入和输出描述符，我们需要初始化每个元素，即批处理大小及其输入大小。其他隐藏张量描述符用层数、批次大小和隐藏大小初始化。本节将不介绍如何编写初始化代码。但是，如果您想了解更多信息，可以查看`10_deep_learning/03_rnn`文件中的代码。\n\n3.  我们还必须为 RNN 操作提供一个工作空间，就像我们为卷积操作所做的那样:\n\n```cpp\nvoid *workspace;\ncudnnFilterDescriptor_t w_desc, dw_desc;\ncudnnSetRNNDescriptor_v6(cudnnHandle, rnn_desc,\n                         hidden_size, num_layers, dropout_desc, CUDNN_LINEAR_INPUT,\n                         bidirectional ? CUDNN_BIDIRECTIONAL : CUDNN_UNIDIRECTIONAL,\n                         CUDNN_LSTM, CUDNN_RNN_ALGO_STANDARD, CUDNN_DATA_FLOAT));\nsize_t weight_size;\ncudnnGetRNNParamsSize(cudnnHandle, rnn_desc, x_desc[0], &weight_size, CUDNN_DATA_FLOAT);\ncudaMalloc((void**)&workspace, weight_size);\n```\n\n然后，我们可以根据工作空间的大小设置过滤器描述符，如下所示:\n\n```cpp\ndimW = {weight_size / sizeof(float), 1, 1}\ncudnnCreateFilterDescriptor(&w_desc);\ncudnnCreateFilterDescriptor(&dw_desc);\ncudnnSetFilterNdDescriptor(w_desc, CUDNN_DATA_FLOAT, CUDNN_TENSOR_NCHW, 3, dimW);\ncudnnSetFilterNdDescriptor(dw_desc, CUDNN_DATA_FLOAT, CUDNN_TENSOR_NCHW, 3, dimW);\ncudnnRNNForwardTraining(cudnnHandle, rnn_desc, seq_length,\n                x_desc, x, hx_desc, hx, cx_desc, cx,\n                w_desc, w, \n                y_desc, y, hy_desc, hy, cy_desc, cy,\n                workspace, workspace_size, reserved_space, \n                reserved_size);\n```\n\n我们可以使用`cudaEvnetRecoard()`和 flops 计算来衡量它们的性能。例如，正向操作可以用以下等式配置:\n\n![](img/b385c8ec-d143-4f0e-9cc6-d31523159f38.png)\n\n然后，我们将通过将批量从 32 增加到 256 来测试我们的实现。适用的测试范围可能不同，GPU 的内存大小也可能不同。\n\n在本节中，我们实现了基于 LSTM 的模拟和`cudnnRNNForwardTraining()`调用。我们的部分模拟版本只有 GEMM 操作，这是最计算密集型的。现在，让我们比较一下这些实现的性能。\n\n# 实施虚拟 LSTM 行动\n\n在我们的实施中，我们将重点模拟 LSTM 的主要行动，而不是完全实施。\n\n让我们确定 LSTM 网络的超参数。一般来说，输入序列长度范围从 512 到 2，048。层数不同。但是由于 *tanh* 操作，不能很大。对于输入大小，我们将使用 512。通常，就 RNN 使用率而言，批次大小在 32 到 256 之间。CUDNN 需要更多关于辍学率的输入，双向还是单向，以及我们是否在使用持久 RNN。我们只是现在不用它们。我们的 LSTM 配置信息如下:\n\n![](img/652a450a-efec-4f75-9b83-4048e9751bc0.png)\n\n现在，我们将有一个部分实现的 LSTM 操作来测量计算强度。正如我们前面讨论的，LSTM 有两个矩阵乘法，我们需要计算。LSTM 操作将为输入序列的每个元素以及每个层计算该值。然后，操作可以配置如下:\n\n```cpp\nfor (int layer = 0; layer < num_layers; layer++) {\n  for (int linear_layer = 0; linear_layer < 4; linear_layer++) {\n    for (int sequence = 0; sequence < seq_length; sequence++) {\n      cublasSgemm(cublas_handle, CUBLAS_OP_T, CUBLAS_OP_N,\n      hidden_size, input_size, batch_size,\n      &alpha, input_weight, input_size, x, input_size,\n      &beta, h, hidden_size);\n      cublasSgemm(cublas_handle, CUBLAS_OP_T, CUBLAS_OP_N,\n      hidden_size, hidden_size, batch_size,\n      &alpha, recurrent_weight, hidden_size,\n      h, hidden_size,\n      &beta, y, hidden_size);\n    }\n  }\n}\n```\n\n我们可以使用更多的元素操作，但它只是近似计算强度，所以我们现在将省略它们。\n\n# CUDNN 和 SGEMM LSTM 的性能比较\n\n让我们比较它们的性能以及不同的批次大小，如下所示在`main()`函数中实现的代码:\n\n```cpp\nfor (int step = 1; step <= 8; step++)\n{\n batch_size = 32 * step;\n printf(\"Batch Size: %3d\\n\", batch_size);\n rnn_operation(seq_length, num_layers, hidden_size, input_size,   \n   batch_size, dropout_rate, bidirectional, mode, persistent);\n cublas_operation(mode, 2ull, input_size, hidden_size, seq_length, batch_size, num_layers);\n}\n```\n\n并且，我们可以使用以下命令编译并执行示例源代码:\n\n```cpp\n$ nvcc -run -m64 -std=c++ 11 -I/usr/local/cuda/samples/common/inc -gencode arch=compute_70,code=sm_70 -lcublas -lcudnn -lcurand -o rnn ./rnn.cpp\n```\n\n下图显示了特斯拉 V100 卡上 cuBLAS 和 cuDNN 的实测性能:\n\n![](img/cecc6413-86bd-45aa-ab52-03bb687493b1.png)\n\n在上图中，这两种实现在性能上表现出巨大的差异。cuDNN 的 LSTM 性能比使用 cuBLAS 的模拟 LSTM 好得多。此外，LSTM 操作的表现遵循特斯拉 V100 图形处理器的屋顶线。另一方面，两个 SGEMM 操作没有显示出这种性能，因为矩阵大小不足以获得完全的性能。要从特斯拉 V100 获得 10 个 TFlops，矩阵大小应该与 1，024 的平方相似或更大。然而，正如我们所看到的，我们的矩阵大小大约是 512 的平方。\n\nLSTM optimization is explained in the following NVIDIA article: [https://devblogs.nvidia.com/optimizing-recurrent-neural-networks-cudnn-5](https://devblogs.nvidia.com/optimizing-recurrent-neural-networks-cudnn-5). It combines matrix-matrix multiplications, fusing element-wise operations, multiple streams, and multi-layer parallelization.\n\nOne of the optimization versions of the RNN is the persistent RNN ([https://svail.github.io/persistent_rnns](https://svail.github.io/persistent_rnns)), which was introduced by Greg Diamos. Although his implementation does not include LSTM and GRU, you can learn how the RNN can be optimized.\n\n# 剖析深度学习框架\n\n一般来说，我们使用深度学习框架(如 TensorFlow、PyTorch 和 MxNet)来开发和研究神经网络。由于这些框架，我们可以有效地开发复杂的模型。然而，当涉及到性能工程时，由于剖析工具的能力，理解框架下的 GPU 操作是一条陡峭的学习曲线。例如，当模型简单时，使用 chrome tracing 进行概要分析是有用的，但是当模型复杂时就没用了。\n\n在[第 5 章](05.html)、 *CUDA 应用分析和调试*中，我们介绍了 **NVIDIA 工具扩展** ( **NVTX** )，它允许我们在 GPU 应用中进行自定义注释，并使用 NVIDIA Nsight Systems 查看时间线。对于复杂的应用，程序员分析它们的性能并找到瓶颈是很有用的。\n\n在本节中，我们将通过修改 ResNet-50 示例代码来介绍如何在 PyTorch 和 TensorFlow 中使用 NVTX。示例代码可以在本书 GitHub 存储库中的`10_deep_learining/05_framework_profile`文件夹中找到。您可以从[https://github.com/nvidia/DeepLearningExamples](https://github.com/nvidia/DeepLearningExamples)获得原始源代码。\n\n为了进行轻松的工作环境配置，我们将为 PyTorch 和 TensorFlow 使用 **NVIDIA GPU 云** ( **NGC** )深度学习容器。如果您需要了解 NGC 或集装箱的基本用法，请访问本书中的 NGC 附录。\n\n现在，让我们先从 PyTorch 开始。\n\n# 剖析 PyTorch 模型\n\n在 PyTorch 中，我们可以使用`torch.cuda.nvtx.range_push(\"foo\")`和`torch.cuda.nvtx.range_pop()`放置自定义标签。这保持了原有的 CUDA NVTX APIs，即`nvtxRangePush()`和`nvtxRangePop()`。让我们看看 NVTX 注释如何帮助我们理解时间轴中的深度学习操作。在以下步骤中，我们将使用`05_framework_profile/pytorch/RN50v1.5`文件中的 ResNet-50 示例代码:\n\n1.  我们将在`train()`函数的训练循环中放置 NVTX 注释来注释`step`值。该功能可以在`image_classificaiton/training.py`文件中找到。下面的屏幕截图分别显示了第 234 行和第 260 行的训练循环和 NVTX 注释:\n\n![](img/a9bbdf88-5cc0-421f-b7ad-00e02655c02c.png)\n\n在前面的代码中，训练操作在`step`函数中实现，该函数由`get_train_step()`函数定义。因此，我们需要在该函数中放置 NVTX 注释来了解更多信息。\n\n2.  让我们在第 164 行给`get_train_step()`函数添加一些 NVTX 注释。该函数返回`_step()`函数，包括训练操作。因此，我们将在这个函数中放置 NVTX 注释。训练过程是前向和后向传播、全约简和优化(更新权重)。以下屏幕截图显示了第 166 行和第 171 行的前向传播注释:\n\n![](img/0c8cae13-132c-4220-afe8-224397161611.png)\n\n这样，我们可以在剩余的操作上放置其他注释。\n\n3.  我们也可以有模型层的 NVTX 注释。在本例中，ResNet-50 模型在`image_classification/resnet.py`文件中实现。以下屏幕截图显示了网络注释示例:\n\n![](img/c3ac1744-4518-41a9-bfea-8c7cbaf8be42.png)\n\n正如我们所看到的，我们可以按照 ResNet 架构放置 NVTX 注释。如果我们在每个构建块中放置注释，我们可以获得更多信息。\n\n4.  现在，让我们分析一下模型。如前所述，我们将使用名为 PyTorch 的 NGC 深度学习容器。`imagenet`数据集位于`/raid/datasets/imagenet/raw-data`文件夹中。为了限制分析时间范围，我们将使用延迟选项(`-y`)和持续时间选项(`-d`)。下面的代码显示了一个 bash shell 脚本，它执行容器并分析网络:\n\n```cpp\n#/bin/bash\n\nCODE_PATH=\"RN50v1.5\"\nDATASET_PATH=\"/raid/datasets/imagenet/raw-data/\"\nOUTPUT_NAME=\"resnet50_pyt\"\n\n# default profile\ndocker run --rm -ti --runtime=nvidia \\\n    -v $(pwd)/${CODE_PATH}:/workspace \\\n    -v ${DATASET_PATH}:/imagenet \\\n    nvcr.io/nvidia/pytorch:19.08-py3 \\\n       nsys profile -t cuda,nvtx,cudnn,cublas -o ${OUTPUT_NAME} \n         -f true -w true -y 60 -d 20 \\\n       python /workspace/main.py --arch resnet50 -b 64 \n         --fp16 /imagenet\n```\n\n执行后，前面的代码在 RN50v1.5 目录中生成分析结果，即`resnet50_pyt.qdrep`。\n\n5.  最后，使用 NVIDIA Nsight 系统打开分析输出`resnet50_pyt.qdrep`，并查看操作。下面的截图显示了带有 NVTX 注释的测量步骤:\n\n![](img/2e2912c2-781b-46c0-9e0f-b8c5f97a3f29.png)\n\n在这里，我们可以看到后向操作花费的时间是前向操作的两倍。此外，PyTorch 将训练循环和反向传播的宿主线程分开。查看内核概要分析，最耗时的点是按元素执行内核。让我们放大转发通道来查看层的执行时间，如下图所示:\n\n![](img/013b8dd6-8fa4-4942-91b5-0519a77b7f4f.png)\n\n在这里，我们可以看到第二个卷积块需要最长的时间来完成。如果这一层有低效点，我们可以进一步挖掘。如果某个特定的内核函数被确定为瓶颈，需要进行优化，我们也可以使用 NVIDIA Nsight Compute 对其进行分析。比较主机应用编程接口跟踪和图形处理器，我们可以看到持续时间是不同的。这是因为主机和 GPU 的操作是异步的。所以，我们在从主机测量 GPU 执行时间时需要谨慎。现在，让我们看看优化步骤，如下图所示:\n\n![](img/79d227f1-a5aa-4222-88a3-d62710299655.png)\n\n我们可以看到，主机和 GPU 的测量执行时间存在巨大差异。主机的测量执行时间为 25.367 毫秒，而图形处理器的时间为 4.048 毫秒。其操作主要是元素操作，其执行被延迟到反向传播完成。我们也可以找到异步执行。之后可以看到`cudaDeviceSynchronize()`功能，防止当前步骤被下一步更新。\n\n我们还可以通过设置一个环境，即`CUDA_LAUNCH_BLOCKING=1`，来禁用这些异步操作。我们可以使用环境选项(`-e`)将它传递给系统的配置文件选项。然后，我们可以用主机和内核函数分析应用的`align`操作。\n\nPyTorch 在其 CUDA 对象中有几个 NVTX 特色的 API。PyTorch 文档可在[https://py torch . org/docs/stable/_ modules/torch/cuda/nvtx . html](https://pytorch.org/docs/stable/_modules/torch/cuda/nvtx.html)找到。通过直接调用 PyTorch 中的 NVTX API，调用 CUDA NVTX APIs。这意味着我们可以在概要时间线中获得定制标记的 NVTX 标记。\n\n# 描述张量流模型\n\n分析张量流图需要我们有一个支持 NVTX 注释的 NVTX 插件。要在 TensorFlow 中使用 NVTX 注释，我们需要使用以下命令安装`nvtx-plugins-tf` Python 插件:\n\n```cpp\n$ pip install nvtx-plugins-tf\n```\n\n然而，如果我们使用的 NGC 张量流容器晚于 19.08 版本，我们就不必这样做了\n\nTensorFlow 图 API 是符号 API，所以需要特定的编程方法。NVTX 插件为此提供了两个选项:装饰器和 Python 函数。\n\n下面是一个 NVTX 装饰器的示例:\n\n```cpp\nimport nvtx.plugins.tf as nvtx_tf\nENABLE_NVTX=true\n@nvtx_tf.ops.trace(message='Dense Block', domain_name='Forward',\n        grad_domain_name='Gradient', enabled=ENABLE_NVTX, \n        trainable=True)\ndef dense_layer(x):\n    x = tf.layers.dense(x, 1000, activation=tf.nn.relu, name='dense_1')\n    x = tf.layers.dense(x, 1000, activation=tf.nn.relu, name='dense_2’) \nreturn x\n```\n\n下面是一个 NVTX Python 函数的示例:\n\n```cpp\nimport nvtx.plugins.tf as nvtx_tf\nENABLE_NVTX=true\nx, nvtx_context = nvtx_tf.ops.start(x, message='Dense Block', \\ \n        domain_name='Forward’, grad_domain_name='Gradient’, \n        enabled=ENABLE_NVTX, trainable=True)\nx = tf.layers.dense(x, 1000, activation=tf.nn.relu, name='dense_1')\nx = tf.layers.dense(x, 1000, activation=tf.nn.relu, name='dense_2’) \nx = nvtx_tf.ops.end(x, nvtx_context)\n```\n\nNVTX 插件提供了 NVTXHook，它允许我们分析 TF 估计器和会话。例如，我们可以如下使用钩子:\n\n```cpp\nfrom nvtx.plugins.tf.estimator import NVTXHook\n\nnvtx_callback = NVTXHook(skip_n_steps=1, name='Train’)\ntraining_hooks=[]\ntraining_hooks.append(nvtx_callback)\n```\n\n然后，我们可以使用以下代码将此应用于任一选项:\n\n```cpp\nwith tf.train.MonitoredSession(hooks=training_hooks) as sess:\n```\n\n或者，我们可以使用以下代码:\n\n```cpp\ntf.estimator.Estimator(hooks=training_hooks, ...)\n```\n\n现在，让我们将此应用于示例 ResNet-50 代码，并查看操作。示例代码可以在`05_framework_profile/tensorflow/RN50v1.5`文件夹中找到:\n\n1.  让我们从将`NVTXHook`应用于估计器开始。训练图的定义可以在第 312 行的`runtime/runner.py`文件中找到。在构建图表之前，我们将把`NVTXHook`添加到钩子列表中，如下面的代码块所示:\n\n![](img/e773aace-88c2-445b-9701-5506ba0bf8d1.png)\n\n2.  然后，我们将把 NVTX 注释应用到模型构建函数中。`model_build()`功能可以在`model/resnet_v1_5.py`文件的`ResnetModel`类中找到。下面的代码显示了一个使用 Python 函数在`model_build()`函数中的`conv1`层放置 NVTX 注释的示例:\n\n![](img/52fc56c0-b159-4e56-8899-67ab37fb9f4c.png)\n\n在前面的代码中，当使用`nvtx_tf.ops.start()`和`nvtx_tf.ops.end()`功能时，我们需要谨慎使用正确的输入和输出。仅将 NVTX 注释放置在其他层中。确保最终完全连接的层的输出是网络的输出。\n\n我们还必须禁用代码来检查它拥有的可训练变量的数量。如果 NVTX 的`trainable`参数值为`True`，则尺寸会发生变化。在`resnet_v1_5.py`文件的第 174 行，有一个断言代码块，用于检查该变量的编号。简单评论一下，如下:\n\n![](img/e4654a73-e106-4d11-a41b-c66ccd5d104f.png)\n\n3.  我们还使用 NVTX 装饰器作为 ResNet 构建模块。在`model/blocks`目录中，我们可以找到`conv2d_blocks.py`和`resnet_bottleneck_block.py`中的`conv2d`和 ResNet 瓶颈块实现。在`conv2d_blocks.py`文件中，我们可以修饰`conv2d_block()`函数来标注 NVTX 概要文件，如下所示:\n\n![](img/abed69dc-571e-4970-af59-ba04f3dd2ac6.png)\n\n同样，我们可以对`resnet_bottleneck_block.py`文件做同样的操作:\n\n![](img/1edce41d-4992-46c0-b7d6-532b4bfcb157.png)\n\n4.  现在，让我们分析一下模型。就像我们使用 PyTorch 容器一样，我们将使用 TensorFlow 的 NGC 容器。我们将假设`imagenet`数据集的`tfrecord`文件位于`/raid/datasets/imagenet/tfrecord`目录中。下面的代码显示了一个 bash shell 脚本，它执行容器并分析网络:\n\n```cpp\n#/bin/bash\n\nCODE_PATH=\"RN50v1.5\"\nDATASET_PATH=\"/raid/datasets/imagenet/tfrecord\"\nOUTPUT_NAME=\"resnet50_tf\"\n\n# default profile\ndocker run --rm -ti --runtime=nvidia \\\n    -v $(pwd):/result \\\n    -v $(pwd)/${CODE_PATH}:/workspace \\\n    -v ${DATASET_PATH}:/imagenet \\\n    nvcr.io/nvidia/tensorflow:19.08-py3 \\\n        nsys profile -t cuda,nvtx,cudnn,cublas -o ${OUTPUT_NAME} \n                     -f true -w true -y 40 -d 20 \\\n            python /workspace/main.py --mode=training_benchmark \n                                      --warmup_steps 200 \\\n                --num_iter 500 --iter_unit batch \n                --results_dir=results --batch_size 64\n```\n\n当我们执行这个函数时，我们会在`RN50v1.5`目录中得到`resnet50_tf.qdrep`文件。\n\n5.  最后，让我们回顾一下使用英伟达系统分析的输出:\n\n![](img/f5e3118b-c0bc-4cee-83c2-69947821d1f9.png)\n\n在这里，我们可以确认反向传播的时间是正向传播的两倍。这个示例代码没有与中央处理器和图形处理器同步。正因为如此，我们可以看到主机和 GPU 之间的时间差更大。当我们在构建块中放置附加注释时，我们将能够在层中看到子块注释。\n\n使用 NVIDIA Nsight 系统进行性能分析在监控多图形处理器训练中的全减少执行时间方面提供了额外的好处。以下屏幕截图显示了使用两个图形处理器进行训练的图形处理器的分析结果:\n\n![](img/e05ad54d-f8a5-482a-935c-f25ed3479b98.png)\n\n在高亮显示的行中，我们可以看到`ncclAllRecude()`函数，它同时调用反向传播。这样做，我们就不会得到全归约运算的延迟。这个示例代码使用 Horovod 来训练多个 GPU。如果你想了解更多，请访问 Horovod 的 GitHub 页面:[https://github.com/horovod/horovod](https://github.com/horovod/horovod)。您可以从这里获得文档和示例代码。\n\n# 摘要\n\n在本章中，我们学习了如何使用 CUDA 库来获得深度学习和性能优势。当我们回顾它们的用途时，我们将它们与每一步的深度学习机制相匹配。多亏了我们可以使用的深度学习库，我们可以实现一个简单的有线电视新闻网，而不需要实现算法。然后，我们使用 NVTX 注释在 PyTorch 和 TensorFlow 中描述了 ResNet-50 模型。\n\n对于一些深度学习的工程师和研究人员来说，实现基本算法可能是不切实际的。但是，了解性能因素和基本操作可以帮助您构建高效且有效的深度学习产品。如今，我们看到许多基于深度学习的产品化服务。工程师花费大量资源生产他们训练好的模型，以及训练他们的模型，以便他们获得尽可能低的错误率。希望您能够深入了解如何在深度学习应用中使用 NVTX 概要分析。利用这些知识，你可以从你的图形处理器中获得更多。祝你好运！"
  },
  {
    "path": "docs/learn-cuda-prog/11.md",
    "content": "# 十一、附录\n\nCUDA 是一个并行编程平台。学习 CUDA 不仅意味着学习语言，还意味着在图形处理器方面有一些工程能力。工程领域可以是监控、环境设置、性能理解、集装箱化等等。本章提供了一些帮助工程师使用图形处理器的技巧。我们可以涵盖更多的主题，但是下面的主题将对那些想了解 CUDA 及其 GPU 操作的人有所帮助。\n\n在本章中，我们将涵盖以下主题:\n\n*   有用的`nvidia-smi`命令\n*   Windows 中的 WDDM/TCC 模式\n*   性能建模\n*   探索基于容器的开发\n\n# 有用的 nvidia-smi 命令\n\n在本节中，我们将介绍`nvidia-smi`的监控功能和管理操作。`nvidia-smi`是 **NVIDIA 管理库** ( **NVML** )的**命令行界面** ( **CLI** )。该库支持对英伟达设备的管理和监控。`nvidia-smi`还通过库向设备提供直接查询和命令。数据通过`stdout`或文件以纯文本或 XML 格式呈现。它提供了几种用于更改设备统计信息的管理工具。\n\n`nvidia-smi`是包装 NVML C/c++ API 的 CLI 应用。它通过 NVML 从`NVIDIA`驱动程序获得请求的信息。NVML 还提供了与其他语言(如 Python 和 Perl)一起工作的 API。\n\n基本上，`nvidia-smi`为用户报告以下已安装的 GPU 统计信息:\n\n*   第一行报告驱动程序版本，以及支持的 CUDA 版本\n*   第二行显示了图形处理器统计格式\n*   每个连续的行包含每个图形处理器的统计信息，包括以下内容:\n    *   GPU ID\n    *   操作模式:\n        *   持续模式(开/关)\n        *   **特斯拉计算集群** ( **TCC** )/ **Windows 显示驱动型号** ( **WDDM** )模式\n    *   风扇速度\n    *   图形处理器温度\n    *   性能模式\n    *   用电量和容量\n    *   总线标识\n    *   内存使用情况和安装的内存\n    *   计数**纠错码** ( **纠错码**\n    *   图形处理器利用率\n    *   计算方式\n\n基本上`nvidia-smi`可以处理所有的 NVIDIA GPU 卡，包括特斯拉、Quadro、GeForce。启用的功能可能因型号和类型而异。例如，ECC 错误计数在特斯拉和 Quadro 卡中可用，而在 GeForce 中不可用，因为它在其设备内存中不提供 ECC 功能。\n\n`nvidia-smi`报告的格式在各操作系统之间是相同的。下面的截图显示了窗口的输出:\n\n![](img/5c002293-324c-4ee6-b99d-346e3295257c.png)\n\n下面的截图显示了 Linux 的输出:\n\n![](img/fbf9fd23-5ea3-458b-9e02-82b2b9882431.png)\n\n因此，我们可以阅读报告，并以相同的格式设置 GPU 操作。现在，让我们继续，看看经常使用的命令。默认`nvidia-smi` CLI 的用法如下:\n\n```cpp\n$ nvidia-smi [option1 [arg]] [option2 [arg]] ...\n```\n\n首先，根据监控目的，经常使用以下选项:\n\n*   `-i`、`--id=`:用于选择目标 GPU\n*   `-l`、`--loop=`:以指定的秒间隔报告 GPU 的状态\n*   `-f`、`--filename=`:用于登录指定文件\n\n该列表涵盖了`nvidia-smi`选项，可以帮助我们从图形处理器获得详细信息。\n\n# 获取图形处理器的信息\n\n当我们使用`--query` ( `-q`)选项时，`nvidia-smi`报告结构化输出。因此，我们可以了解收集了哪些信息。我们可以获得 GPU 信息，如利用率、功率、内存和时钟速度统计。另一方面，如果我们希望持续监控图形处理器的状态，这种格式是没有帮助的。\n\n# 获取格式化信息\n\n我们需要监控的基本 GPU 统计数据是功率、温度、内核利用率和内存使用情况。这可以通过`--query-gpu`命令轻松完成:\n\n```cpp\n$ nvidia-smi --query-gpu=timestamp,name,pci.bus_id,driver_version,pstate,pcie.link.gen.max,pcie.link.gen.current,temperature.gpu,utilization.gpu,utilization.memory,memory.used,memory.free,memory.used --format=csv -l 1\n```\n\n以下命令显示了一些选项，我们可以使用这些选项来检测时钟限制的性能消耗原因:\n\n```cpp\n$ nvidia-smi --query-gpu=index,clocks_throttle_reasons.active,clocks_throttle_reasons.gpu_idle,clocks_throttle_reasons.applications_clocks_setting,clocks_throttle_reasons.sw_power_cap,clocks_throttle_reasons.hw_slowdown,clocks_throttle_reasons.hw_thermal_slowdown,clocks_throttle_reasons.hw_power_brake_slowdown,clocks_throttle_reasons.sync_boost --format=csv\n```\n\nGPU 时钟节流的原因可能是电源制动、过热和同步增强。电源制动是指 GPU 的功耗受到用户设置或系统中电源供应商性能的限制。由于冷却环境差，过热也是频繁节流的原因。\n\n# 电源管理模式设置\n\n您可以使用以下命令找出每个图形处理器的最大功耗:\n\n```cpp\n$ nvidia-smi -i <device id> -pl N\n```\n\n# 设置图形处理器的时钟速度\n\n默认情况下，GPU 的时钟速度根据需求而变化，并节省功耗以最大限度地提高能效。为了最大限度地提高图形处理器的性能并减少延迟，尤其是在基准测试情况下，我们可以确保图形处理器具有最大时钟速度，并禁用图形处理器驱动程序。\n\n首先，我们需要将 GPU 设置为持久性模式。这样做意味着 GPU 驱动程序模块总是加载到内核中，减少了初始响应时间。此选项仅在 Linux 上可用，因为 Windows 不卸载图形处理器驱动程序。持续模式设置命令如下:\n\n```cpp\n$ sudo nvidia-persistenced\n```\n\n然后，我们可以设置支持的最大时钟。该值会因您使用的图形处理器而异:\n\n```cpp\n$ nvidia-smi -q -d SUPPORTED_CLOCKS\n$ sudo nvidia-smi -ac <Mem clock, Graphics clock>\n```\n\n例如，特斯拉 V100 卡可以通过以下命令进行设置:\n\n```cpp\n$ sudo nvidia-smi -ac 877,1380 # V100 PCIe\n$ sudo nvidia-smi -ac 877,1530  # V100 SMX\n```\n\n# 图形处理器设备监控\n\n该命令每秒探测所选图形处理器的设备状态:\n\n```cpp\n$ nvidia-smi dmon -s pucvmet -i -0\n```\n\n下面的截图显示了上一个命令的结果。我们正在监控的设备表示其 GPU 设备状态为`0`:\n\n![](img/fb906ac4-8892-4d60-a69a-13552a6b92ac.png)\n\n可以使用`-s`选项指定收集的信息，如下所示:\n\n*   `p`:用电量和温度\n*   `u`:利用率\n*   `c`:进程和内存时钟\n*   `v`:电力和热力违规\n*   `m` : FB 和 Bar1 内存\n*   `e` : ECC 错误和 PCIe 重放错误\n*   `t` : PCIe 接收和发送吞吐量\n\n# 监控 GPU 利用率以及多个进程\n\n如果您在单个图形处理器上使用多个进程操作，您可以考虑使用此命令。该命令收集图形处理器的统计数据，以及它们正在使用的进程。这意味着您可以确定哪个进程受到了 GPU 共享、内存计时空间等的限制:\n\n```cpp\n$ nvidia-smi pmon -i 0 -s u -o T\n```\n\n下面的截图显示了带有**进程标识** ( **进程标识**)的`nvidia-smi`的输出，这有助于确定哪个进程正在使用什么 GPU 资源:\n\n![](img/b31adbb3-a103-4fe1-9f65-b36c37da194e.png)\n\n前面截图中的每一列都显示了每个 GPU 的计算单元利用率或内存使用情况:\n\n*   `sm%` : CUDA 核心利用率\n*   `mem%`:内存操作的采样时间比\n*   `enc%` / `dec%`:硬件编码器的利用率\n*   `fb` : FB 内存使用情况\n\n# 获取图形处理器拓扑信息\n\n在多 GPU 系统中，使用`nvidia-smi`获取 GPU 拓扑信息是有用的。以下命令是显示多图形处理器系统图形处理器拓扑的`nvidia-smi`命令:\n\n```cpp\nnvidia-smi topo -m\n```\n\n下面的截图显示了`nvidia-smi`的输出，显示了系统的拓扑结构。DGX 站的结果是，我们有四个支持 NVLink 的 V100 图形处理器:\n\n![](img/c16a6ff8-8869-4136-8fc4-3b8ddd1b30db.png)\n\n根据这个结果，我们可以确认系统的 GPU 拓扑如下:\n\n![](img/b2c9864c-3133-47d2-bf3e-12687a9e7fa7.png)\n\n以下命令标识了图形处理器之间的对等可访问性。我们在[第 6 章](06.html)、*可扩展多 GPU 编程*中使用了这个命令:\n\n```cpp\n$ nvidia-smi topo -p2p rwnap\n```\n\n以下是`nvidia-smi`拓扑的输出，该拓扑在一个系统中有四个图形处理器:\n\n![](img/332cf62f-5f61-45cf-a65a-66b005de6d18.png)\n\n对等访问是可伸缩性或操作的一个重要因素。此命令帮助您确认图形处理器和您的系统可以支持图形处理器之间的对等访问。\n\n# Windows 中的 WDDM/TCC 模式\n\n在 Windows 平台上，NVIDIA GPU 有两种模式:WDDM 和 TCC。WDDM 是显卡的图形驱动程序，因此它可以渲染桌面和应用。如果安装的 GPU 只用于计算，显示渲染就是无用的开销。在这种情况下，NVIDIA GPU 可以切换到只专注于计算的模式。这种模式被称为 TCC 模式。\n\nWDDM 允许英伟达图形处理器与服务于显示器的视窗 WDDM 驱动程序合作。支持 WDDM 模式是对 Windows 图形的要求。另一方面，TCC 模式只对计算有效。根据您的图形处理器产品和配置，可以更改图形处理器的模式。\n\n操作模式遵循四种英伟达产品类别，其默认模式可能会有所不同，如下所示:\n\n*   **GeForce** :仅限 WDDM 模式。\n*   **Quadro/Titan** :默认为 WDDM 模式，但也可以在 TCC 模式下使用。\n*   **特斯拉**:通常默认为 TCC 模式。\n*   **Tegra** :仅支持 Linux。没有 WDDM/部队派遣国问题。\n\nWDDM 模式支持 CUDA 操作，用 Nsight 调试 CUDA 应用，同时还支持显示。作为一台主机，你可以做 GPU 能做的一切。但是，TCC 模式会禁用图形驱动程序上的图形，并启用 GPU 作为计算加速器。换句话说，这应该在显卡不必为显示器服务时使用。\n\n在 CUDA 处理方面，TCC 模式比 WDDM 模式有一些优势，如下所示:\n\n*   服务于大规模计算\n*   忽略 Windows 的显示超时间隔(通常为两秒钟)，以启用长于两秒钟的内核操作\n*   降低了 CUDA 在 Windows 上的内核启动开销\n*   通过 Windows 远程桌面服务支持 CUDA 处理\n*   支持在非英伟达集成显卡上使用英伟达图形处理器，从而节省全局内存\n\n因此，如果图形处理器不为显示器服务，TCC 模式会为图形处理器带来作为加速器的最佳配置。\n\n# 设置变矩器离合器/WDDM 模式\n\n要更改变矩器离合器或 WDDM 模式，使用`nvidia-smi`工具，如下所示:\n\n```cpp\n$ sudo nvidia-smi -dm {0|1}\n```\n\n`0`表示 WDDM 模式，`1`表示 TCC 模式。\n\n如果要为选定的图形处理器设置 TCC 模式，请使用`-g`选项指定目标图形处理器:\n\n```cpp\n$ nvidia-smi -g {GPU_ID} -dm {0|1}\n```\n\n当您想要将图形处理器用于显示和计算的目的分开时，此选项非常有用。应用这些设置后，您可能需要*重新启动*机器来应用这些更改。\n\n我们可以通过使用`nvidia-smi`来识别 TCC 模式是否启用。以下截图显示了 TCC 中的 GPU 运行模式:\n\n![](img/596401fd-59e4-4156-a3af-22ed06088a29.png)\n\n通过查看第一列中 GPU 名称的右侧，我们可以确认启用了 TCC 模式。\n\n# 性能建模\n\n了解应用/算法和图形处理器硬件的特性对于设定真实的加速目标非常重要。这可以通过增加并行性来实现。我们还需要在优化应用时确定是否有优化 GPU 的空间。\n\n一个简单的方法是应用阿姆达尔定律。我们可以预测，应用中可实现的性能增益受到代码顺序部分的限制。例如，只有 50%的代码可以并行，而其余的代码本质上是顺序的(例如从文件中读取)。如果是这种情况，可以达到的最大加速是 2x；也就是说，程序只能运行两倍的速度。然而，这种性能建模只显示了最大加速比。我们不得不假设我们可以完全并行化并消除代码并行部分的执行时间。\n\n另一个性能建模实践是基于目标体系结构的性能约束因素进行分析。实际上，我们有硬件规格，其操作引入了不可避免的性能限制。通过分析这些限制，我们可以确定是否有执行优化的空间，并查看下一组优化策略。\n\n# 屋顶模型\n\n每个内核函数都可以分为以下几类:\n\n*   **计算界限**:内核函数对读取或写入的每个字节数据进行更多的算术运算。这些应用对硬件的计算要求更高。\n*   **内存限制**:应用花费大部分时间从内存中读写，计算量较少。应用受系统内存带宽的影响最大，而不是硬件的 FLOP 等级。\n*   **延迟界限**:内核函数的 CUDA 线程大部分时间都在等待，而不是执行。出现这种情况有很多原因。主要原因是并行性的次优级别或内存和计算资源的非最佳使用。\n\n由于所有这些绑定都是由硬件引入的，因此我们可以绘制目标硬件的峰值性能和内存带宽以及它们的算术强度。性能曲线受硬件峰值性能的限制。我们在[第三章](03.html)、 *CUDA 线程编程*中对此进行了简要的触及，以确定下一步的优化策略。下图用在[第三章](03.html)、 *CUDA 线程编程*中，并展示了一个屋顶线模型的例子:\n\n![](img/47a31ebb-4282-499a-bcd0-c43c007e024f.png)\n\n为了进行任何计算，数据需要从内存传输到算术单元。它遍历不同级别的内存层次结构，峰值内存带宽因内存类型而异。该算法的峰值性能可以按照其算术强度进行分类。这种强度是由计算数据与加载数据的数量决定的。此外，这些延迟限制因素也带来了计算上限。通过测量性能并对照硬件规格进行分析，我们可以确认目标算法达到了峰值性能，或者受到内存或延迟的限制。无论哪种情况，我们都可以确定下一步。在[第三章](03.html)*CUDA 线程编程*中，我们对此进行了深入的探讨。在本节中，我们将通过查看一个示例来关注屋顶线模型，并了解它有多有用。\n\n屋顶线模型考虑了应用的操作强度。简单来说，这意味着从主存储器(动态随机存取存储器)每字节进行操作。虽然有更复杂的模型也考虑了高速缓存到处理器的传输，但屋顶线模型更侧重于从动态随机存取存储器到高速缓存的数据传输，因此，侧重于特定图形处理器架构上 CUDA 内核所需的动态随机存取存储器带宽。\n\n屋顶线模型陈述如下:\n\n\"Attainable performance ( GFLOP/s) = min (Peak Floating-Point Performance, Peak Memory Bandwidth * Operational Intensity)\"\n\n# 雅可比方法分析\n\n让我们试着理解这个公式，得到 V100 GPU 卡的屋顶线模型。V100 图形处理器具有以下规格:\n\n*   80 个 SMs，每个具有 32 个 FP64 内核\n*   900 GB/s 的总带宽\n*   L2 快取记忆体:6 MB\n*   L1 快取记忆体:10 MB\n*   寄存器:62 KB/SM\n\n让我们尝试分析一个简单的雅可比方法:\n\n```cpp\nfor (int iy = 1; iy < NoRows; iy++)\n{\n    for ( int ix = 1; ix < NoCols; ix++)\n    {\n        Anew[ix][iy] = rhs[iy∗nx+ix] \n                     - 0.25f*(Aref[ix-1][iy] + Aref[ix+1][iy] \n                     + Aref[ix][iy-1] + Aref[ix][iy+1]);\n    }\n}\n```\n\n让我们分析一下前面代码的数据传输:\n\n*   向量的内存加载(`Anew`、`rhs`、`Aref` ): *I <sub>加载</sub>* *= NoRow * NoCol * 3 * 8 字节(双精度)*\n*   向量存储(`Anew` ): *I <sub>存储</sub> = NoRow * NoCol * 8 字节*\n*   浮点运算:*I<sub>FP</sub>= NoRow * NoCol * 6 FLOP*\n\n下图显示了特斯拉 V100 卡的屋顶线分析和雅可比方法的算术强度:\n\n![](img/b1660d18-bfa6-463b-8ed3-569519b1db7a.jpg)\n\nV100 上雅可比的算术强度为*I<sub>FP</sub>/(I<sub>Load</sub>+I<sub>Strore</sub>)= 0.18 FLOP/字节。*\n\n屋顶线模型清楚地表明，该算法是受内存限制的，最大可实现的性能仅为 0.18 FLOP/字节，因此无法达到 V100 的峰值 FLOP 额定值，即 7.8 TFLOPS。但是，我们也可以通过重用提取的数据来预测优化后可获得的性能。\n\n屋顶线模型有助于根据算法的硬件特性来定义算法的性能上限。\n\n**Jacobi method** \nThis is an iterative algorithm for finding solutions for a system of linear equations. Its basic operations and GPU optimization are explained at [https://www.olcf.ornl.gov/wp-content/uploads/2016/01/Introduction-to-Accelerated-Computing-with-OpenACC-Jeff-Larkin.pdf](https://www.olcf.ornl.gov/wp-content/uploads/2016/01/Introduction-to-Accelerated-Computing-with-OpenACC-Jeff-Larkin.pdf).\n\n# 探索基于容器的开发\n\n维护集群的开发人员和 IT 管理员面临的一个关键挑战是软件堆栈的复杂性。每个应用/框架都有许多依赖项。当这些依赖项的版本不同时，复杂性会增加。例如，在 DL 中，Caffe 对 cuDNN 和 Python 版本的要求与 TensorFlow 不同。在一个特定的组织/机构中，有许多用户，每个用户都可能使用同一框架的不同版本。安装所有正确的依赖项并设置正确的环境会导致生产力的损失。更多的时间花在安装上，而不是工作上。面临的另一个挑战是，由于依赖性不匹配，几乎不可能由不同的人来再现结果/性能数字，即使他们可能在同一系统上运行。比如 GROMACS 分子动力学框架有很多设置，比如多线程编译或者**消息传递接口** ( **MPI** )支持，MPI 上的版本，MPI 类型。另一个挑战，尤其是在人工智能领域，是你能想到的每一个软件框架都在快速发展，新的补丁也在频繁添加。\n\n容器为这些问题提供了解决方案。使用容器的主要优势如下:\n\n*   **隔离**:容器为应用提供环境隔离\n*   **在任何地方运行**:容器提供了一种在不同环境中共享和测试应用的简单方法\n*   **轻量级**:与使用基于虚拟机的解决方案相比，容器是轻量级的，并且提供几乎可以忽略的延迟和开销\n\n最著名的两个容器环境是 Docker 和奇点。两者各有利弊。但是，请注意，这一部分不是 Docker 或奇点的详尽指南。\n\n开发人员通常会创建容器并在线发布给其他人使用。我们将详细解释一个由英伟达维护的名为**英伟达 GPU 云** ( **NGC** )的存储库。NGC 就像一个存放流行的**深度学习**(**DL**)**高性能计算** ( **HPC** )和**虚拟现实** ( **VR** )框架的容器的仓库。英伟达针对 GPU 的不同环境测试这些应用，并在向公众提供之前经历广泛的质量保证过程。这意味着性能得到保证。\n\nNGC 的一个类比是安卓应用商店，它为运行安卓操作系统的不同手机上运行的不同应用提供了一个存储库。这些应用得到验证并通过质量保证过程。NGC 这个名字有时会让人困惑，开发者认为它是一朵云。应该明确的是，它是一个容器存储库，可以被拉入一个带有 GPU 的系统中，并在本地运行。该容器可以在不同的系统上运行，具有图形处理器，就像它可以运行在桌面上的 NVIDIA 泰坦卡，服务器与特斯拉 V100 卡，或 NVIDIA AI 超级计算机 DGX。NGC 容器也可以在诸如 AWS 和 Azure 等云平台上运行。\n\n# 主机的 NGC 配置\n\n以下步骤介绍了如何配置 NGC 工作环境和在 NGC 查找可用映像:\n\n1.  **基本安装**:要在 GPU 系统上使用容器，您需要安装以下内容:\n\n`nvidia-docker`是一个将 NVIDIA 组件和模块加载到容器中的开源项目。它基本上是 Docker 的包装器。您可以在下载并查看安装说明。\n\n2.  **访问 NGC 网站**:现在可以去 NGC 网站选择集装箱([nvidia.com/ngc](https://www.nvidia.com/en-us/gpu-cloud/)，如下图截图所示:\n\n![](img/6605ca00-405e-43a5-b447-9a99c2a2f8b3.png)\n\n如您所见，容器有六个类别。选择一个与你相关的。早期版本的 NGC 要求用户注册，但最近取消了这一要求。\n\n# NGC 集装箱的基本用法\n\n在本节中，我们将介绍如何从 NGC 注册中心提取容器，以及如何定制我们自己的容器。这和使用 Docker 没什么不同，只是我们可以访问 NGC 注册表`nvcr.io`。如果您已经熟悉 Docker 命令，可以跳过这一部分。\n\n以下步骤说明了如何在终端会话中获取并启动本地 Linux 机器上的 NGC 容器:\n\n1.  找到您想要使用的软件，并从 NGC 站点复制命令。\n2.  然后，通过将命令粘贴到终端中来拉出容器图像。以下截图显示了`pull`命令及其 Docker 操作:\n\n![](img/9f5c6c17-d493-45bb-b8fc-7972af831865.png)\n\n如您所见，Docker 使用基于层的方法。CUDA 容器建立在 Ubuntu 的基础层之上。此外，Docker images 命令向我们展示了机器上本地拉取的容器。\n\n3.  使用以下命令启动拉出的容器:\n\n```cpp\ndocker run --rm -it --runtime=nvidia nvcr.io/nvidia/cuda:9.0-devel-ubuntu16.04\n```\n\n图形处理器如下图所示:\n\n![](img/85548425-3f21-4f14-afbc-a011124d86e3.png)\n\n只要我们运行 Docker，shell 登录就会改变，我们会登录到以 root 用户身份运行的容器中。因此，我们能够在容器内运行`nvidia-smi`命令。\n\n4.  我们还可以使用容器的附加选项来访问主机资源。最常用的选项如下:\n\n`nvidia-docker`的基本用法类似于正常的 Docker 用法，只是我们可以使用 GPU。这意味着您还可以获得 Docker 的额外好处。\n\n# 从 NGC 容器创建和保存新容器\n\n您也可以将图层添加到现有容器中，并保存它们以备将来使用。让我们学习如何做到这一点:\n\n1.  创建一个`Dockerfile`并在基础图像上创建一些层。例如，我们可以更新 NGC PyTorch 容器中的 APEX([https://github.com/nvidia/apex](https://github.com/nvidia/apex))，这样我们就可以使用它的最新版本:\n\n```cpp\nFROM nvcr.io/nvidia/pytorch:19.03-py3\nRUN git clone https://github.com/NVIDIA/apex /opt/apex && \\\n cd /opt/apex && \\\n pip install -v --no-cache-dir --global-option=\"--cpp_ext\" --global-option=\"--cuda_ext\" .\n```\n\n您也可以将所需的 Ubuntu 包或 Python 包安装代码添加到该文件中。\n\n2.  然后，我们可以用`docker build`命令构建一个定制的容器。以下命令显示了 Docker 图像`build`命令的基本格式:\n\n```cpp\ndocker build -t <image-name>:<tag> .\n```\n\n该命令将找到我们创建的`Dockerfile`，并逐行启动每个命令。`Dockerfile`的每一行都将创建一个 Docker 层，因此建议编写一个`RUN`命令来覆盖单个目标。\n\n3.  现在，您需要将 Docker 映像备份到您的私有注册表中或创建一个文件。完成容器后，您可能希望在其他系统中传播或重用该容器。在这种情况下，您可以将 Docker 映像推入注册表。例如，如果你在`DockerHub`上有账户，Docker 会提供免费注册。您可以使用以下命令将容器推入注册表:\n\n```cpp\ndocker push <DockerHub-ID>/<image-name>:<tag>\n```\n\n您还可以创建备份文件，并将它们复制到本地文件系统上。以下命令向您展示了如何使用压缩创建容器备份:\n\n```cpp\ndocker save <image-name>:<tag> | gzip > container.tgz\n```\n\n然后，您可以使用以下命令加载该图像:\n\n```cpp\ngunzip -c container.tgz | docker load\n```\n\n您可以在不压缩的情况下创建本地备份映像，但是输出文件太大，通常无法传送到其他系统。\n\n在本节中，我们已经介绍了 Docker 的一些基本操作。然而，Docker 也提供了其他丰富的功能和好处。尽管 Linux 仅可用于 Docker 容器中 CUDA 的使用，但 Docker 将在构建工作环境时为您节省时间，并帮助您专注于代码开发。\n\n# 将默认运行时设置为 NVIDIA Docker\n\n通过对`nvidia-docker`配置的一些修改，我们可以启动 GPU 容器，而无需通知 GPU 此用途。因为我们可以将 GPU 运行时选项设置为`nvidia-docker`，所以可以采用 Docker 的运行时设计。为此，您需要在`/etc/docker/daemon.json`中插入`default-runtime\": \"nvidia\",`作为选项。然后，如果没有其他 Docker 配置，`daemon.json`文件可以配置如下:\n\n```cpp\n{\n    \"default-runtime\": \"nvidia\",\n    \"runtimes\": {\n        \"nvidia\": {\n            \"path\": \"nvidia-container-runtime\",\n            \"runtimeArgs\": []\n        }\n    }\n}\n```\n\n执行此操作后，使用以下命令重新启动系统或重新启动 Docker 守护程序:\n\n```cpp\nsudo systemctl restart docker\n```\n\n现在，我们可以享受没有 Docker 命令中的 GPU 命令选项的 GPU 容器。\n\n英伟达开发博客提供了`nvidia-docker`的介绍，可以在[https://devblogs.nvidia.com/gpu-containers-runtime](https://devblogs.nvidia.com/gpu-containers-runtime/)找到。在这里，您不仅可以了解它的配置，还可以了解如何将其与 Docker compose 或**Linux Containers**(**LXC**)集成。它甚至允许 GPU 容器通过其 GPU 设备插件与 Kubernetes 一起工作。"
  },
  {
    "path": "docs/learn-cuda-prog/README.md",
    "content": "# CUDA 编程学习手册\n\n> 原书：[Learn CUDA Programming](https://libgen.rs/book/index.php?md5=F6DA79E769F988319EB178273ECBF55B)\n> \n> 协议：[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)\n> \n> 阶段：机翻（1）\n> \n> 自豪地采用[谷歌翻译](https://translate.google.cn/)\n> \n> 让开源界提前感受入关。\n\n* [在线阅读](https://ccpp.apachecn.org)\n* [在线阅读（Gitee）](https://apachecn.gitee.io/apachecn-c-cpp-zh/)\n* [ApacheCN 学习资源](http://docs.apachecn.org/)\n\n## 贡献指南\n\n本项目需要校对，欢迎大家提交 Pull Request。\n\n> 请您勇敢地去翻译和改进翻译。虽然我们追求卓越，但我们并不要求您做到十全十美，因此请不要担心因为翻译上犯错——在大部分情况下，我们的服务器已经记录所有的翻译，因此您不必担心会因为您的失误遭到无法挽回的破坏。（改编自维基百科）\n\n## 联系方式\n\n### 负责人\n\n* [飞龙](https://github.com/wizardforcel): 562826179\n\n### 其他\n\n*   在我们的 [apachecn/apachecn-c-cpp-zh](https://github.com/apachecn/apachecn-c-cpp-zh) github 上提 issue.\n*   发邮件到 Email: `apachecn@163.com`.\n*   在我们的 [组织学习交流群](http://www.apachecn.org/organization/348.html) 中联系群主/管理员即可.\n\n## 赞助我们\n\n![](http://data.apachecn.org/img/about/donate.jpg)\n"
  },
  {
    "path": "docs/learn-cuda-prog/SUMMARY.md",
    "content": "+   [CUDA 编程学习手册](README.md)\n+   [零、前言](00.md)\n+   [一、CUDA 编程入门](01.md)\n+   [二、内存管理](02.md)\n+   [三、线程编程](03.md)\n+   [四、内核执行模型及优化策略](04.md)\n+   [五、应用分析和调试](05.md)\n+   [六、可扩展的多图形处理器编程](06.md)\n+   [七、CUDA 中的并行编程模式](07.md)\n+   [八、使用库和其他语言编程](08.md)\n+   [八、将 OpenACC 用于图形处理器编程](09.md)\n+   [九、利用 CUDA 实现深度学习加速](10.md)\n+   [十一、附录](11.md)\n"
  },
  {
    "path": "docs/learn-qt5/0.md",
    "content": "# 零、前言\n\nQt 是一个成熟而强大的框架，用于跨多个平台交付复杂的应用。它广泛应用于嵌入式设备，包括电视、卫星机顶盒、医疗设备、汽车仪表板等。它在 Linux 领域也有着丰富的历史，KDE 和旗鱼操作系统广泛使用它，商店里的许多应用都是使用 Qt 开发的。过去几年，它在移动领域也取得了长足的进步。然而，在微软视窗和苹果 macOS X 的世界里，C#/的主导地位。NET 和 Objective-C/Cocoa 意味着 Qt 经常被忽视。\n\n这本书旨在展示 Qt 框架的强大和灵活性，并展示如何一次性编写应用并将其部署到多个操作系统桌面。读者将从零开始构建一个完整的现实世界**业务线** ( **LOB** )解决方案，具有清晰的库、用户界面和单元测试项目。\n\n我们将讨论用 QML 构建一个现代的、响应迅速的用户界面，并将其连接到丰富的 C++ 类。我们将使用 QMake 控制项目配置和输出的每个方面，包括平台检测和条件表达式。我们将构建“自我感知”的数据实体，它们可以将自己序列化到 JSON 和从 JSON 序列化。我们将把这些数据实体保存在数据库中，并学习如何查找和更新它们。我们将接触互联网，并消费一个 RSS 源。最后，我们将生成一个安装包，以便将我们的应用部署到其他机器上。\n\n这是一套基本技术，涵盖了大多数业务线应用的核心要求，并将使读者能够从空白页前进到发货应用。\n\n# 这本书是给谁的\n\n这本书面向应用开发人员，他们正在寻找一个强大而灵活的框架，用于在微软视窗、苹果 OS X 和 Linux 桌面平台上创建现代且响应迅速的应用。尽管集中在桌面应用开发上，但所讨论的技术在很大程度上也适用于移动开发。\n\n# 充分利用这本书\n\n读者应该对 C++ 很熟悉，但不需要 Qt 或 QML 的先验知识。在 Mac OS X 上，您将需要安装 XCode 并至少启动一次。在 Windows 上，您可以选择安装 Visual Studio，以便使用 MSVC 编译器。\n\n# 下载示例代码文件\n\n你可以从你在[www.packtpub.com](http://www.packtpub.com)的账户下载这本书的示例代码文件。如果您在其他地方购买了这本书，您可以访问[www.packtpub.com/support](http://www.packtpub.com/support)并注册将文件直接通过电子邮件发送给您。\n\n您可以按照以下步骤下载代码文件:\n\n1.  登录或注册[www.packtpub.com](http://www.packtpub.com/support)。\n2.  选择“支持”选项卡。\n3.  点击代码下载和勘误表。\n4.  在搜索框中输入图书的名称，并按照屏幕指示进行操作。\n\n下载文件后，请确保使用最新版本的解压缩文件夹:\n\n*   视窗系统的 WinRAR/7-Zip\n*   zipeg/izp/un ARX for MAC\n*   适用于 Linux 的 7-Zip/PeaZip\n\n这本书的代码包也托管在 https://github.com/PacktPublishing/Learn-Qt-5 的 GitHub 上。我们还有来自丰富的书籍和视频目录的其他代码包，可在获得。看看他们！\n\n# 使用的约定\n\n本书通篇使用了许多文本约定。\n\n`CodeInText`:表示文本中的码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟网址、用户输入和推特句柄。下面是一个例子:“在`cm-ui/ui/views`创建`SplashView.qml`文件”。\n\n代码块设置如下:\n\n```cpp\n<RCC>\n    <qresource prefix=\"/views\">\n        <file alias=\"MasterView\">views/MasterView.qml</file>\n    </qresource>\n    <qresource prefix=\"/\">\n        <file>views/SplashView.qml</file>\n        <file>views/DashboardView.qml</file>\n        <file>views/CreateClientView.qml</file>\n        <file>views/EditClientView.qml</file>\n        <file>views/FindClientView.qml</file>\n    </qresource>\n</RCC>\n```\n\n当我们希望将您的注意力吸引到代码块的特定部分时，相关的行或项目以粗体显示:\n\n```cpp\nQT += sql network\n```\n\n任何命令行输入或输出都编写如下:\n\n```cpp\n$ <Qt Installation Path> \\Tools \\QtInstallerFramework \\3.0\\ bin\\ binarycreator.exe -c config\\config.xml -p packages ClientManagementInstaller.exe\n```\n\n**粗体**:表示一个新的术语、一个重要的单词或者你在屏幕上看到的单词。例如，菜单或对话框中的单词像这样出现在文本中。下面是一个例子:“用客户端管理替换 Hello World 标题，并在窗口主体内插入一个文本组件”。\n\nWarnings or important notes appear like this. Tips and tricks appear like this.\n\n# 取得联系\n\n我们随时欢迎读者的反馈。\n\n**综合反馈**:发邮件`feedback@packtpub.com`并在邮件主题中提及书名。如果您对本书的任何方面有疑问，请发电子邮件至`questions@packtpub.com`。\n\n**勘误表**:虽然我们已经尽了最大的努力来保证内容的准确性，但是错误还是会发生。如果你在这本书里发现了一个错误，如果你能向我们报告，我们将不胜感激。请访问[www.packtpub.com/submit-errata](http://www.packtpub.com/submit-errata)，选择您的图书，点击勘误表提交链接，并输入详细信息。\n\n**盗版**:如果您在互联网上遇到任何形式的我们作品的非法拷贝，如果您能提供我们的位置地址或网站名称，我们将不胜感激。请通过`copyright@packtpub.com`联系我们，并提供材料链接。\n\n**如果你有兴趣成为一名作者**:如果有一个你有专长的话题，你有兴趣写或者投稿一本书，请访问[authors.packtpub.com](http://authors.packtpub.com/)。\n\n# 复习\n\n请留下评论。一旦你阅读并使用了这本书，为什么不在你购买它的网站上留下评论呢？然后，潜在的读者可以看到并使用您不带偏见的意见来做出购买决定，我们在 Packt 可以了解您对我们产品的看法，我们的作者可以看到您对他们的书的反馈。谢谢大家！\n\n更多关于 Packt 的信息，请访问[packtpub.com](https://www.packtpub.com/)。"
  },
  {
    "path": "docs/learn-qt5/1.md",
    "content": "# 一、你好，Qt\n\nQt 是一个成熟而强大的框架，用于跨多个平台交付复杂的应用。它广泛应用于嵌入式设备，包括电视、卫星机顶盒、医疗设备、汽车仪表板等。它在 Linux 领域也有着丰富的历史，KDE 和旗鱼操作系统广泛使用它，商店里的许多应用都是使用 Qt 开发的。过去几年，它在移动领域也取得了长足的进步。然而，在微软视窗和苹果 Mac OS X 的世界里，C#/的统治地位。NET 和 Objective-C/Cocoa 意味着 Qt 经常被忽略。\n\n这本书旨在展示 Qt 框架的强大和灵活性，并展示如何一次性编写应用并将其部署到多个操作系统桌面。我们将从头开始构建一个完整的真实世界的**业务线** ( **LOB** )解决方案，具有清晰的库、用户界面和单元测试项目。\n\n我们将讨论用 QML 构建一个现代的、响应迅速的用户界面，并将其连接到丰富的 C++ 类。我们将使用 QMake 控制项目配置和输出的每个方面，包括平台检测和条件表达式。我们将构建“自我感知”的数据实体，它们可以将自己序列化到 JSON 和从 JSON 序列化。我们将把这些数据实体保存在数据库中，并学习如何查找和更新它们。我们将接触互联网，并消费一个 RSS 源。最后，我们将生成一个安装包，以便将我们的应用部署到其他机器上。\n\n在本章中，我们将安装和配置 Qt 框架和相关的**集成开发环境** ( **IDE** ) Qt Creator。我们将创建一个简单的便笺式应用，我们将在本书的剩余部分中使用它来演示各种技术。我们将涵盖以下主题:\n\n*   安装 Qt\n*   维护您的安装\n*   Qt 创建者\n*   便笺项目\n*   qmake\n\n# 安装 Qt\n\n让我们从访问 Qt 网站 [https://www.qt.io](https://www.qt.io/) 开始:\n\n![](img/c940d042-dd21-4b22-98cd-da7573f1eab4.png)\n\n网站布局变化相当频繁，但您正在寻找的是下载适用于台式机和移动设备的 Qt 开源软件:\n\n1.  从顶层菜单中，选择产品，然后选择集成开发环境和工具\n2.  点击免费开始\n3.  选择桌面和移动应用\n4.  点击获取您的开源包\n\nIf you continue to use Qt beyond these personal projects, ensure that you read the licensing information available on the Qt website ([https://www.qt.io/licensing/](https://www.qt.io/licensing/)). Upgrade to the commercial Qt license if the scope of your projects requires it or if you want access to the official Qt support and the benefits of a close strategic relationship with the Qt company.\n\n该网站将检测您的操作系统，并建议您下载:\n\n![](img/8bef27df-df7a-4602-8eea-60800d22aa90.png)\n\n在 Windows 上，您将被推荐在线安装程序`*.exe`文件，而在 Linux 上，您将获得一个`*.run`文件，如果您运行的是 Mac OS X，还将获得一个`.dmg`文件。在所有情况下，下载并启动安装程序:\n\n![](img/a3f707e8-b55d-47bf-a6f0-28bdd400b734.png)\n\nOn Linux, once downloaded, you may need to first navigate to the `*.run` file and mark it as executable in order to be able to launch it. To do this, right-click on the file in the file manager and click on Properties. Click on the Permissions tab and tick the box that says Allow executing file as program.\n\n在最初的欢迎对话框后，您首先看到的是注册 Qt 帐户或使用 Qt 帐户登录的选项。如果您愿意，可以随意创建一个，但现在我们将继续跳过:\n\n![](img/8e5c2903-df00-4369-aa2f-5a626363549f.png)\n\n然后要求您选择要安装的组件。\n\n你的第一个决定是你想要 Qt 框架的哪个版本。您可以并排安装多个版本。让我们选择最新的和最棒的(在撰写本文时是 Qt 5.10)并保留所有较旧的版本。\n\n接下来，展开所选版本，您将看到选项的第二个列表。描述中写着“Qt 5.9.x 预构建组件”的所有选项被称为**套件**。工具包本质上是一个工具集，使您能够使用特定的编译器/链接器构建应用，并在特定的目标体系结构上运行它。每个工具包都带有专门为特定工具集编译的 Qt 框架二进制文件以及必要的支持文件。请注意，引用的编译器不附带工具包；你需要提前安装。在 Windows 上有一个例外，那就是 MinGW(它包含了适用于 Windows 的 GCC)，你可以通过底部的工具组件列表选择安装它。\n\n在 Windows 上，这正是我们要做的，所以我们从工具部分选择了 MinGW 5.3.0 32 位工具包和 MinGW 5.3.0 开发环境。在我的(64 位)机器上，我已经安装了 Microsoft Visual Studio 2017，因此我们还将选择 MSVC 2017 64 位工具包，以帮助在本书后面演示一些技术。在 Linux 上，我们选择 GCC 64 位，而在 Mac OS 上，我们选择 macOS 64 位(使用 Clang 编译器)。请注意，在 Mac OS 上，必须安装 XCode，最好至少启动一次 XCode，让它有机会完成初始化和配置。\n\n请随意按暂停，去安装你想使用的任何其他 IDEs 或编译器，然后回来选择匹配的工具包。你选择哪一个并不重要——整本书解释的技术都是适用的，不管套件是什么，你可能只是得到稍微不同的结果。请注意，根据您的操作系统和芯片组，提供给您的可用套件会有所不同；例如，如果你在一台 32 位机器上，你将不会得到任何 64 位套件。\n\nBelow the kits are some optional Qt APIs (such as Qt Charts), which we won’t need for the topics covered in this book, but feel free to add them in if you want to explore their functionality. Note that they may have different licensing agreements from the core Qt framework.\n\n不管工具包和应用编程接口如何，您都会在工具部分注意到，Qt Creator 是默认安装的，这是我们将在本书中使用的集成开发环境:\n\n![](img/f3fb9cc7-a583-4c20-89f5-b50d4bb5d82f.png)\n\n完成选择后，单击下一步和更新开始安装。\n\nIt's generally a good idea to leave the installation location as the default for consistency across machines, but feel free to install it wherever you want.\n\n# 维护您的安装\n\n安装后，您可以通过位于您安装 Qt 的目录中的`Maintenance Tool`应用更新、添加和删除组件(甚至整个 Qt 安装)。\n\n启动这个工具提供了与我们第一次安装 Qt 时几乎相同的体验。“添加或删除组件”选项是您想要添加到您以前可能不需要的项目中的选项，包括工具包，甚至是框架的全新版本。除非您主动取消选中它们，否则已经安装在系统上的组件不会受到影响。\n\n# Qt 创建者\n\n虽然 Qt Creator 的详细概述超出了本书的范围(Qt Creator 手册可通过这里描述的帮助模式访问)，但在我们开始第一个项目之前，有必要进行一次短暂的停留，因此启动新安装的应用，我们将看一看:\n\n![](img/82d23d25-1147-4106-9889-b59acce6dfcb.png)\n\n左上角(1)是应用的不同区域或模式:\n\n*   欢迎模式是 Qt Creator 启动时的默认模式，也是创建或打开项目的起点。有大量的例子可以帮助展示框架的各种功能以及一些教程视频。\n*   编辑模式是您花费大部分时间的地方，用于编辑所有基于文本的文件。\n*   只有当您打开一个用户界面文件并且是视图的所见即所得编辑器时，才可以访问设计。虽然有用的 UX 设计和基本布局工作，它可以很快令人沮丧，我们将在编辑模式下做我们所有的 QML 工作。以这种方式工作促进了对 QML 的理解(因为你必须写它)，并且还有一个优点，那就是编辑器不会添加你不想要的代码。\n*   调试模式用于调试应用，超出了本书的范围。\n*   项目模式是管理项目配置的地方，包括生成设置。这里所做的更改将反映在`*.pro.user`文件中。\n*   帮助模式将带您进入 Qt 创建者手册和 Qt 库参考。\n\nPressing *F1* while the cursor is on a recognized Qt symbol will automatically open context sensitive help for that symbol.\n\n下面，我们有构建/运行工具(2):\n\n*   套件/构建允许您选择套件并设置构建模式\n*   运行构建并运行应用，无需调试\n*   开始调试使用调试器构建和运行应用(请注意，您必须在选定的工具包中安装和配置调试器，这样才能工作)\n*   构建项目在不运行应用的情况下构建应用\n\n沿着底部(3)，我们有一个搜索框和几个输出窗口:\n\n问题显示任何警告或错误。对于与您的代码相关的编译器错误，双击该项将引导您找到相关的源代码。\n\n*   搜索结果允许您在不同范围内查找文本。 *Ctrl* + *F* 调出快速搜索，从那里选择高级…也调出搜索结果控制台。\n*   应用输出是控制台窗口；所有来自应用代码的输出，像`std::` cout 和 Qt 的等价物`qDebug()`出现在这里，还有来自 Qt 框架的某些消息。\n*   编译输出包含构建过程的输出，从 qmake 到编译和链接。\n*   调试器控制台包含我们在本书中不会涉及的调试信息。\n*   通用消息包含其他杂项输出，其中最有用的是来自`*.pro`文件的 qmake 解析，我们将在后面看到。\n\n搜索框真的是一个隐藏的宝石，可以让你不用在无穷无尽的文件和文件夹中点击来寻找你想要的东西。您可以开始在框中键入要查找的文件名，此时将出现一个包含所有匹配文件的筛选列表。只需点击你想要的文件，它就会在编辑器中打开。不仅如此，你还可以应用大量的过滤器。在空搜索框中单击光标，它会显示可用过滤器的列表。例如，过滤器`m`搜索 C++ 方法。所以，假设你记得写了一个叫`SomeAmazingFunction()`的方法，但是不记得它在哪里了，直接去搜索框，开始输入`m Some`，它就会出现在过滤列表中。\n\n在编辑模式下，布局会略有变化，并且会出现一些新的窗格。最初，它们将是空的，但是一旦您打开了一个项目，它们将类似于以下内容:\n\n![](img/022cfbf8-e903-4cfd-8fbc-8f674707d226.png)\n\n导航栏旁边是项目资源管理器，您可以使用它来导航解决方案的文件和文件夹。下方窗格是您当前打开的所有文档的列表。右边较大的区域是编辑器窗格，您可以在其中编写代码和编辑文档。\n\n双击项目资源管理器中的文件通常会在编辑器窗格中打开它，并将其添加到打开的文档列表中。单击打开的文档列表中的文档将在编辑器窗格中激活它，而单击文件名右侧的小 x 将关闭它。\n\n可以更改窗格以显示不同的信息，调整大小、拆分、关闭，也可以使用标题中的按钮进行筛选或与编辑器同步。尝试去感受他们能做什么。\n\n正如您对现代 IDE 的期望，chrome 和文本编辑器的外观和感觉是非常可定制的。选择工具>选项…查看可用选项。我通常会编辑以下内容:\n\n*   `Environment > Interface > Theme > Flat`\n*   `Text Editor > Fonts & Colors > Color Scheme > My own scheme`\n*   `Text Editor > Completion > Surround text selection with brackets > Off`\n*   `Text Editor > Completion > Surround text selection with quotes > Off`\n*   `C++ > Code Style > Current Settings > Copy… then Edit…`\n*   `Edit Code Style > Pointers and References > Bind to Type name > On (other options Off)`\n\n尽情玩耍，让你喜欢的东西变得更好。\n\n# 便笺项目\n\n为了演示 Qt 项目可以有多小，并给我们一个可以玩的编程沙坑，我们将创建一个简单的草稿栏项目。对于这个项目，我们甚至不会使用 IDE 来为我们做，所以您可以真正看到项目是如何构建的。\n\n首先，我们需要创建一个根文件夹来存储我们所有的 Qt 项目。在 Windows 上，我用`c:\\projects\\qt`，而在 Linux 和 Mac OS 上我用`~/projects/qt`。在任何适合你的地方创建这个文件夹。\n\nNote that file syncing tools (OneDrive, DropBox, and so on) can sometimes cause problems with project folders, so keep your project files in a regular unsynchronized folder and use version control with a remote repository for backups and sharing.\n\n对于本书的其余部分，我将松散地称这个文件夹为`<Qt Projects>`或类似的文件夹。我们也倾向于使用文件路径的 Unix 样式/分隔符，而不是 Windows 样式的反斜杠`\\`。所以，对于使用 Windows 的读者来说，`<Qt Projects>/scratchpad/amazing/code`相当于`c:\\projects\\qt\\scratchpad\\amazing\\code`。Qt 也倾向于支持这个惯例。\n\n同样，本书剩余部分的大部分截图将来自 Windows，因此 Linux/Mac 用户应该将任何对`c:\\projects\\qt`的引用解释为`~/projects/qt`。\n\n在我们的 Qt 项目文件夹中，创建一个新的文件夹草稿栏并导航到其中。创建一个名为`scratchpad.pro`的新纯文本文件，记住删除操作系统可能想为您添加的任何`.txt`扩展名。\n\n接下来，只需双击该文件，它将在 Qt Creator 中打开:\n\n![](img/0e9f2d91-6e96-4760-b1d0-0537f9cc23e5.png)\n\n在这里，Qt Creator 问我们希望如何配置我们的项目，也就是说，我们希望在构建和运行代码时使用哪些工具包。选择一个或多个可用的工具包，然后单击配置项目。以后可以轻松添加和删除套件，所以不用担心选择哪一个。\n\n如果切换回`filesystem`，你会看到 Qt Creator 已经为我们创建了一个名为`scratchpad.pro.user`的新文件。这只是一个包含配置信息的 XML 文件。如果您删除此文件并再次打开`.pro`文件，系统将提示您再次配置项目。顾名思义，配置设置与本地用户相关，因此如果您加载了由其他人创建的项目，您通常也需要完成配置项目步骤。\n\n项目配置成功后，您会看到项目已经打开，甚至有一个完全空的`.pro`文件。这大概是一个项目所能达到的最小限度了！\n\n回到`filesystem`，创建以下纯文本文件:\n\n*   `main.cpp`\n*   `main.qml`\n*   `qml.qrc`\n\n我将仔细检查这些文件，解释它们的目的，并很快添加它们的内容。在现实世界的项目中，我们当然会使用集成开发环境来为我们创建文件。事实上，这正是我们创建主解决方案文件时要做的事情。然而，这样做的目的是向您表明，当您将其归结为一个项目时，它只是一堆文本文件。永远不要害怕手动创建和编辑文件。许多现代 IDEs 会与一个又一个菜单和永无止境的选项窗口混淆和过度复杂。Qt Creator 可能会错过其他 ide 的一些高级功能，但它非常精简和简单。\n\n创建完这些文件后，双击项目窗格中的`scratchpad.pro`文件，我们将开始编辑新项目。\n\n# qmake\n\n我们的项目(`.pro`)文件由一个名为 **qmake** 的实用程序解析，该实用程序反过来生成驱动应用构建的`Makefiles`。我们定义了我们想要的项目输出类型，包括哪些源文件以及依赖项等等。这在很大程度上是通过简单地设置变量来实现的，就像我们现在在项目文件中所做的那样。\n\n在`scratchpad.pro`中增加以下内容:\n\n```cpp\nTEMPLATE = app\n\nQT += qml quick\n\nCONFIG += c++ 14\nSOURCES += main.cpp\nRESOURCES += qml.qrc\n```\n\n让我们依次浏览每一行:\n\n```cpp\nTEMPLATE = app\n```\n\n`TEMPLATE`告诉 qmake 这是什么类型的项目。在我们的例子中，它是一个可执行的应用，由`app`表示。我们感兴趣的其他值是用于构建库二进制文件的`lib`和用于多项目解决方案的`subdirs`。请注意，我们使用`=`运算符设置了一个变量:\n\n```cpp\nQT += qml quick\n```\n\nQt 是一个模块化的框架，允许你只获取你需要的部分。`QT`标志指定了我们想要使用的 Qt 模块。默认包括*核心*和*图形用户界面*模块。请注意，我们将附加值附加到一个变量，该变量需要一个带有`+=`的列表:\n\n```cpp\nCONFIG += c++ 14\n```\n\n`CONFIG`允许您添加项目配置和编译器选项。在这种情况下，我们指定要使用 C++ 14 特性。请注意，如果您使用的编译器不支持这些语言功能标志，它们将不起作用:\n\n```cpp\nSOURCES += main.cpp\n```\n\n`SOURCES`是我们想要包含在项目中的所有`*.cpp`源文件的列表。在这里，我们添加我们的空`main.cpp`文件，在这里我们将实现我们的`main()`功能。我们还没有，但是当我们有了，我们的头文件将被指定一个`HEADERS`变量:\n\n```cpp\nRESOURCES += qml.qrc \n```\n\n`RESOURCES`是包含在项目中的所有资源收集文件(`*.qrc`)的列表。资源集合文件用于管理应用资源，如图像和字体，但对我们来说最重要的是我们的 QML 文件。\n\n项目文件更新后，保存更改。\n\n每当您保存对`*.pro`文件的更改时，qmake 都会解析该文件。如果一切顺利，你会在 Qt Creator 的右下角看到一个绿色的小条。红色条表示某种问题，通常是语法错误。该过程的任何输出都将写入“常规消息”窗口，以帮助您诊断和修复问题。空白被忽略，所以不要担心空白行是否完全匹配。\n\nTo get qmake to take a fresh look at your project and generate new `Makefiles`, right-click on your project in the Projects pane and select Run qmake. It may be slightly tedious, but it’s a good habit to manually run qmake in this way on each of your projects before building and running your application. I’ve found that certain types of code changes can “slip under the radar” and leave you scratching your head when you run your application and they don’t seem to have had any effect. If you ever see your application ignoring the changes you’ve just made, run qmake on each of your projects and try again. The same applies if you get spurious linker errors.\n\n您将看到我们的其他文件现在神奇地出现在“项目”窗格中:\n\n![](img/f3dcc2a3-3a5b-49db-a081-fb4626bb65b4.png)\n\n双击`main.cpp`进行编辑，我们将编写第一段代码:\n\n```cpp\n#include <QGuiApplication>\n#include <QQmlApplicationEngine>\n\nint main(int argc, char *argv[])\n{\n    QGuiApplication app(argc, argv);\n    QQmlApplicationEngine engine;\n\n    engine.load(QUrl(QStringLiteral(\"qrc:/main.qml\")));\n\n    return app.exec();\n}\n```\n\n我们在这里所做的就是实例化一个 Qt 图形用户界面应用对象，并要求它加载我们的`main.qml`文件。它非常简短，因为 Qt 框架为我们完成了所有复杂的底层工作。我们不必担心平台检测或管理窗口句柄或 OpenGL。\n\n可能要学习的最有用的事情之一是将光标放在其中一个 Qt 对象上，然后按下 *F1* 将打开该类型的帮助。Qt 对象上的方法和属性也是如此。在帮助文件中四处看看`QGuiApplication`和`QQmlApplicationEngine`都是关于什么的。\n\n要编辑我们项目中的下一个文件—`qml.qrc`—您需要右键单击并选择要打开它的编辑器。默认为资源编辑器:\n\n![](img/b6bd98fb-9f91-45f0-aa54-13a13af39bf2.png)\n\n我个人不是这个编辑的粉丝。我不认为这比只写纯文本更容易编辑，也不是特别直观。关闭此选项，选择`Open with > Plain Text Editor`。\n\n添加以下内容:\n\n```cpp\n<RCC>\n    <qresource prefix=\"/\">\n        <file>main.qml</file>\n    </qresource>\n</RCC>\n```\n\n回到`main.cpp`，我们要求 Qt 加载`qrc:/main.qml`文件。这基本上可以分解为“在前缀为`/`和名称为`main.qml`的`qrc`文件中查找该文件”。现在在我们的`qrc`文件中，我们已经创建了一个前缀属性为`/`的`qresource`元素。在这个元素中，我们有一个名为`main.qml`的资源集合(尽管只有一个)。把`qrc`文件想象成一个可移植的文件系统。请注意，资源文件相对于引用它们的`.qrc`文件进行定位。在这种情况下，我们的`main.qml`文件和我们的`qml.qrc`文件在同一个文件夹中。例如，如果它在一个名为`views`的子文件夹中，那么`qml.qrc`中的行应该是这样的:\n\n```cpp\n<file>views/main.qml</file>\n```\n\n类似地，`main.cpp`中的字符串也是`qrc:/views/main.qml`。\n\n一旦保存了这些更改，您将看到我们的空`main.qml`文件作为`qml.qrc`文件的子文件出现在项目窗格中。双击该文件进行编辑，我们将完成我们的项目:\n\n```cpp\nimport QtQuick 2.9\nimport QtQuick.Window 2.3\n\nWindow {\n    visible: true\n    width: 1024\n    height: 768\n    title: qsTr(\"Scratchpad\")\n    color: \"#ffffff\"\n\n    Text {\n        id: message\n        anchors.centerIn: parent\n        font.pixelSize: 44\n        text: qsTr(\"Hello Qt Scratchpad!\")\n        color: \"#008000\"\n    }\n}\n```\n\n我们将在[第 2 章](2.html)、*项目结构*中详细介绍 QML，但简而言之，该文件代表应用启动时呈现给用户的屏幕或视图。\n\n导入行类似于 C++ 中的`#include`语句，尽管它们不是包含单个头文件，而是导入整个模块。在这种情况下，我们希望基本的 QtQuick 模块允许我们访问所有核心的 QML 类型，同时 QtQuick 窗口模块允许我们访问`Window`组件。模块是有版本控制的，通常，您会希望使用最新版本来发布您正在使用的 Qt。当前版本号可以在 Qt 文档中找到。请注意，虽然您在输入版本号时会得到代码完成，但所显示的选项有时并不反映最新的可用版本。\n\n顾名思义，`Window`元素为我们提供了一个顶级窗口，我们所有的其他内容都将在这个窗口中呈现。我们给它一个 1024 x 765 像素的大小，一个“草稿栏”的标题和一个以十六进制 RGB 值表示的白色背景。\n\n在该组件中(QML 是一种分层标记语言)，我们用`Text`组件添加了一条欢迎消息。我们将文本放在屏幕中央，并设置其字体大小和颜色，但除此之外，我们在这个阶段不关心花哨的格式或任何东西，所以这就像我们要做的那样复杂。同样，我们稍后会更详细地介绍这一点，所以如果它看起来有点陌生，请不要担心。\n\n就这样。要构建和运行我们令人惊叹的新应用，首先使用左下角的监视器图标选择您想要的工具包和构建配置:\n\n![](img/75cffd4f-352a-47a0-acb8-cce893e02890.png)\n\n接下来，右键单击“项目”窗格中的项目名称，然后选择“运行 qmake”。完成后，使用绿色播放图标运行应用:\n\n![](img/257dd773-1abf-41e5-96f5-53ecb8906aea.png)\n\n# 摘要\n\n在本章中，我们下载、安装和配置了 Qt。我们已经对 Qt Creator IDE 进行了一次旋风之旅，玩了它的选项，并看到了如何用它编辑各种文件。我们已经温和地介绍了 qmake，并且看到了创建项目是多么的简单，在这个过程中揭开了事情的神秘面纱。最后，我们从头开始构建了我们的首个项目(弱双关)，并在屏幕上获得了强制性的 Hello World 消息。\n\n在[第 2 章](2.html)、*项目结构*中，我们将在这些基础上建立我们的主要解决方案。"
  },
  {
    "path": "docs/learn-qt5/2.md",
    "content": "# 二、项目结构\n\n在本章中，我们将创建一个新的多项目解决方案，这将是我们的示例应用的基础。我们将应用模型视图控制器模式，分离用户界面和业务逻辑。我们还将介绍 Qt 的单元测试框架——QtTest——并演示如何将其集成到我们的解决方案中。我们将在本章中介绍这些内容:\n\n*   项目、MVC 和单元测试\n*   创建库项目\n*   创建单元测试项目\n*   创建用户界面项目\n*   掌握 MVC\n*   QObject 基类\n*   QML\n*   控制项目产出\n\n# 项目、MVC 和单元测试\n\n我们在前一章中构建的草稿栏应用是一个 Qt 项目，由`.pro`文件表示。在商业环境中，技术解决方案通常作为公司计划的一部分来开发，这些计划通常也被称为**项目**。为了尽量减少混乱(以及项目这个词出现的次数！)，我们将使用 project 来表示由`.pro`文件定义的 Qt 项目，并使用 initiative 这个词来指代商业意义上的项目。\n\n我们将着手开发一个通用的客户管理系统。这将是一种可以针对多种应用进行调整和再利用的东西，例如管理客户的供应商、管理患者的医疗服务等等。它将执行现实世界**业务线** ( **业务线**)应用中反复出现的常见任务，主要是添加、编辑和删除数据。\n\n我们的草稿栏应用完全封装在一个项目中。对于较小的应用，这是完全可行的。然而，对于更大的代码库，尤其是涉及到几个开发人员时，将事情分解成更易于管理的部分通常是有好处的。\n\n我们将使用**模型视图控制器** ( **MVC** )架构模式的超级轻量级实现。如果您以前没有遇到过 MVC，那么它主要用于从用户界面中分离业务逻辑。用户界面(视图)将命令传递给切换面板样式类(控制器)，以检索数据并执行所需的操作。控制器又将数据、逻辑和规则的责任委托给数据对象(模型):\n\n![](img/39853104-b5e4-4dc3-985b-febae3c11f56.png)\n\n关键是**视图**知道**控制器**和**模型**，因为它需要向**控制器**发送命令并显示**模型**中保存的数据。**控制器**知道**模型**，因为它需要将工作委托给它，但是它不知道**视图**。模型对**控制器**或**视图**一无所知。\n\n在业务环境中以这种方式设计应用的一个主要好处是，敬业的 UX 专家可以处理视图，而程序员则处理业务逻辑。第二个好处是，因为业务逻辑层对用户界面一无所知，所以您可以添加、编辑甚至完全替换用户界面，而不会影响逻辑层。一个很好的用例是为桌面应用提供一个“全胖”的用户界面，为移动设备提供一个“半胖”的用户界面，两者可以使用相同的业务逻辑。考虑到所有这些，我们将在物理上将用户界面和业务逻辑分离到单独的项目中。\n\n我们还将考虑将自动化单元测试集成到我们的解决方案中。单元测试和**测试驱动开发** ( **TDD** )最近真的越来越受欢迎了，当在商业环境中开发应用时，您很可能会在编写代码的同时编写单元测试。如果没有，你真的应该提议去做，因为它有很大的价值。如果你之前没有做过任何单元测试，不用担心；这非常简单，我们将在本书后面更详细地讨论它。\n\n最后，我们需要一种方法将这些子项目聚合在一起，这样我们就不必单独打开它们。我们将通过一个伞式解决方案项目来实现这一点，该项目除了将其他项目捆绑在一起之外什么也不做。我们将这样安排我们的项目:\n\n![](img/83ae32f5-c0eb-43b2-8d40-d977d7fb3a9c.png)\n\n# 项目创建\n\n在上一章中，我们看到了仅仅通过创建几个文本文件来设置一个新项目是多么容易。然而，我们将使用 Qt Creator 创建我们的新解决方案。我们将使用新项目向导来指导我们创建顶级解决方案和单个子项目。\n\n从顶部菜单中，选择文件>新建文件或项目，然后选择项目>其他项目>子项目，然后单击选择…:\n\n![](img/ab5a4f70-f441-44b3-bcc1-118ee4ccb301.png)\n\n子项目是我们顶级解决方案项目所需的模板。给它命名`cm`并在我们的`qt`项目文件夹中创建它:\n\n![](img/466aba05-c61f-436c-82d6-203ad4fe6731.png)\n\n在套件选择窗格上，检查我们安装的台式机 Qt 5 . 10 . 0 MinGW 32 位套件。如果你已经安装了额外的套件，请随意选择你想要试用的套件，但这不是必须的。点击下一步:\n\n![](img/16fccb2b-40e0-484c-a43a-7e926efe6c68.png)\n\n如前所述，版本控制超出了本书的范围，因此在“项目管理”窗格中，从“添加到版本控制”下拉列表中选择“无”。单击完成并添加子项目:\n\n![](img/04cabdf6-7274-4e07-a775-ee9320205a4a.png)\n\n我们将添加用户界面项目作为第一个子项目。该向导遵循与我们刚刚遵循的步骤大致相同的模式，因此请执行以下操作:\n\n1.  选择项目>应用>季度快速应用-空，然后单击选择...\n2.  在“项目位置”对话框中，将其命名为`cm-ui`(对于客户端管理-用户界面)，将该位置保留为我们的新`cm`文件夹，然后单击“下一步”。\n3.  在“定义构建系统”对话框中，选择构建系统，然后单击“下一步”。\n4.  在“定义项目详细信息”对话框中，保留 QT 5.9 的默认最小 QT 版本和“使用 Qt 虚拟键盘”框未选中，然后单击“下一步”。\n5.  在“套件选择”对话框中，选择桌面 Qt 5 . 10 . 0 MinGW 32 位套件以及您想要尝试的任何其他套件，然后单击“下一步”。\n6.  最后，在项目管理对话框中，跳过版本控制(保留为<none>)并点击完成。</none>\n\n我们的顶级解决方案和用户界面项目现在已经启动并运行，所以让我们添加其他子项目。接下来添加业务逻辑项目，如下所示:\n\n1.  在项目窗格中，右键单击顶层`cm`文件夹并选择新建子项目。\n2.  选择项目>库> C++ 库，然后单击选择....\n3.  在“简介和项目位置”对话框中，选择“共享库”作为类型，将其命名为`cm-lib`，在`<Qt Projects>/cm`中创建，然后单击“下一步”。\n4.  在“选择所需模块”对话框中，只需接受默认的 QtCore，然后单击“下一步”。\n5.  在**班级信息**对话框中，我们有机会创建一个新班级来开始学习。给出类名`Client`，加上`client.h`头文件和`client.cpp`源文件，然后点击下一步。\n6.  最后，在项目管理对话框中，跳过版本控制(保留为<none>)并点击完成。</none>\n\n最后，我们将重复创建单元测试项目的过程:\n\n1.  新子项目....\n2.  项目>其他项目>季度单元测试。\n3.  项目名称`cm-tests`。\n4.  包括 QtCore 和 QtTest。\n\n5.  使用`testCase1`测试槽和`client-tests.cpp`文件名创建`ClientTests`测试类。将类型设置为测试，并选中生成初始化和清理代码。\n6.  跳过版本控制并完成。\n\n这需要通过很多对话框，但是我们现在已经有了框架解决方案。您的项目文件夹应该如下所示:\n\n![](img/6a2a887e-58db-4347-8bf2-67aefbd91625.png)\n\n我们现在将依次查看每个项目，并在开始添加内容之前进行一些调整。\n\n# cm-lib\n\n首先，前往文件浏览器，在`cm-lib`下创建一个名为`source`的新子文件夹；将`cm-lib_global.h`移到那里。在`source`中创建另一个名为`models`的子文件夹，并将两个`Client`类文件移到那里。\n\n接下来，回到 Qt Creator，打开`cm-lib.pro`并编辑如下:\n\n```cpp\nQT -= gui\nTARGET = cm-lib\nTEMPLATE = lib\nCONFIG += c++ 14\nDEFINES += CMLIB_LIBRARY\nINCLUDEPATH += source\n\nSOURCES += source/models/client.cpp\n\nHEADERS += source/cm-lib_global.h \\\n    source/models/client.h\n```\n\n由于这是一个库项目，我们不需要加载默认的 GUI 模块，所以我们使用`QT`变量将其排除。`TARGET`变量是我们希望给出二进制输出的名称(例如，`cm-lib.dll`)。它是可选的，如果没有提供，将默认为项目名称，但我们会明确。接下来，不是像我们在便签簿应用中看到的那样有一个`TEMPLATE`应用，这次我们使用`lib`给我们一个库。我们通过`CONFIG`变量添加 c++ 14 特性。\n\n`cm-lib_global.h`文件是一个有用的预处理器样板，我们可以用它来导出我们的共享库符号，你很快就会看到它投入使用。我们使用`DEFINES`变量中的`CMLIB_LIBRARY`标志来触发该导出。\n\n最后，我们稍微重写了一下`SOURCES`和`HEADERS`变量列表，以便在我们移动一些东西之后说明新的文件位置，并且我们将源文件夹(这是我们所有代码将驻留的地方)添加到`INCLUDEPATH`中，以便在我们使用`#include`语句时搜索路径。\n\n右键单击项目窗格中的`cm-lib`文件夹，并选择运行质量评估。完成后，再次右键单击并选择**重建**。一切都应该是绿色和快乐的。\n\n# cm-测试\n\n创建新的`source/models`子文件夹并将`client-tests.cpp`移到那里。切换回 Qt 创建者并编辑`cm-tests.pro`:\n\n```cpp\nQT += testlib\nQT -= gui\nTARGET = client-tests\nTEMPLATE = app\n\nCONFIG += c++ 14 \nCONFIG += console \nCONFIG -= app_bundle\n\nINCLUDEPATH += source \n\nSOURCES += source/models/client-tests.cpp\n```\n\n除了我们想要一个控制台应用而不是一个库之外，这遵循了与`cm-lib`几乎相同的方法。我们不需要图形用户界面模块，但是我们将添加`testlib`模块来访问 Qt 测试功能。\n\n这个子项目还没有太多内容，但是您应该能够成功地运行 qmake 并重建。\n\n# cm-ui\n\n这次创建两个子文件夹:`source`和`views`。将`main.cpp`移至`source`，将`main.qml`移至`views`。将`qml.qrc`重命名为`views.qrc`，编辑`cm-ui.pro`:\n\n```cpp\nQT += qml quick\n\nTEMPLATE = app\n\nCONFIG += c++ 14 \n\nINCLUDEPATH += source \n\nSOURCES += source/main.cpp \n\nRESOURCES += views.qrc \n\n# Additional import path used to resolve QML modules in Qt Creator's code model \nQML_IMPORT_PATH = $$PWD\n```\n\n我们的 UI 是用 QML 写的，需要`qml`和`quick`模块，所以我们增加了这些。我们编辑`RESOURCES`变量来获取我们重命名的资源文件，并且还编辑`QML_IMPORT_PATH`变量，当我们进入定制的 QML 模块时，我们将详细讨论这个变量。\n\n接下来，编辑`views.qrc`以说明我们已经将`main.qml`文件移动到了`views`文件夹中。记得右键点击并用>纯文本编辑器打开:\n\n```cpp\n<RCC>\n    <qresource prefix=\"/\">\n        <file>views/main.qml</file>\n    </qresource>\n</RCC>\n```\n\n最后，我们还需要在`main.cpp`中编辑一行来说明文件移动:\n\n```cpp\nengine.load(QUrl(QStringLiteral(\"qrc:/views/main.qml\")));\n```\n\n您现在应该能够运行 qmake 并重建`cm-ui`项目。在运行它之前，让我们快速查看一下构建配置按钮，因为我们已经打开了多个项目:\n\n![](img/dff629a5-d049-416c-a917-5f3dccd1f080.png)\n\n请注意，现在，除了工具包和构建选项，我们还必须选择我们希望运行的可执行文件。确保选择`cm-ui`，然后运行应用:\n\n![](img/c87febe8-c33b-44c7-bafc-7e60d634a23e.png)\n\n你好，世界。这是相当没有启发性的东西，但我们有一个多项目解决方案愉快地构建和运行，这是一个很好的开始。当你不能享受更多乐趣时，请关闭应用！\n\n# 掌握 MVC\n\n现在我们的解决方案结构已经就位，我们将开始 MVC 实现。正如您将看到的，它非常小，并且非常容易设置。\n\n首先，展开`cm-ui > Resources > views.qrc > / > views`，右键点击`main.qml`，选择【重命名】，将文件重命名为`MasterView.qml`。如果您收到有关项目编辑的消息，请选择“是”继续:\n\n![](img/e30af2b8-81b4-4c6f-9ac3-df51ea1e6ecc.png)\n\n如果您确实收到错误消息，该文件仍将在“项目”窗格中显示为`main.qml`，但该文件将在文件系统中被重命名。\n\n接下来，编辑`views.qrc`(右键点击并选择>纯文本编辑器打开)。将内容替换如下:\n\n```cpp\n<RCC>\n    <qresource prefix=\"/views\">\n        <file alias=\"MasterView.qml\">views/MasterView.qml</file>\n    </qresource>\n</RCC>\n```\n\n如果你还记得我们是如何在`main.cpp`中加载这个 QML 文件的，语法是`qrc:<prefix><filename>`。我们之前有一个`/`前缀和一个`views/main.qml`相对文件名。这给了我们`qrc:/views/main.qml`。\n\n`/`的前缀并不十分具有描述性。随着您添加越来越多的 QML 文件，用有意义的前缀将它们组织成块确实很有帮助。拥有非结构化资源块还会使“项目”窗格变得丑陋且更难导航，正如您刚刚看到的，您必须深入查看`views.qrc > / > views`。所以，第一步是将前缀从`/`重命名为`/views`。\n\n然而，有了`/views`的前缀和`views/main.qml`的相对文件名，我们的网址现在是`qrc:/views/views/main.qml`。\n\n这比以前更糟糕了，我们在`views.qrc`中还有一个很深的文件夹结构。幸运的是，我们可以为我们的文件添加一个*别名*来解决这两个问题。你可以用一个资源的别名代替相对路径，所以如果我们分配一个`main.qml`的别名，我们可以用简单的`main.qml`代替`views/main.qml`，给我们`qrc:/views/main.qml`。\n\n这是简洁和描述性的，我们的项目窗格也更整洁。\n\n所以，回到我们更新的`views.qrc`版本，我们已经简单地将文件的名称从`main.qml`更新为`MasterView.qml`，与我们执行的文件重命名一致，并且我们还提供了一个快捷别名，因此我们不必指定视图两次。\n\n我们现在需要更新`main.cpp`中的代码来反映这些变化:\n\n```cpp\nengine.load(QUrl(QStringLiteral(\"qrc:/views/MasterView.qml\")));\n```\n\n您应该能够运行 qmake，并构建和运行以验证没有任何损坏。\n\n接下来，我们将创建一个`MasterController`类，因此右键单击`cm-lib`项目并选择添加新… > C++ > C++ 类>选择…:\n\n![](img/2b36133d-dd2e-4811-9167-1b302283441b.png)\n\n使用浏览…按钮创建`source/controllers`子文件夹。\n\n通过选择 QObject 作为基类并包含它，Qt Creator 将为我们编写一些样板代码。您可以稍后自己添加，所以不要觉得这是创建新类的必要部分。\n\n一旦您跳过了版本控制并创建了类，请按如下方式声明和定义它。我们的`MasterController`还没有做什么特别激动人心的事情，我们只是在做基础工作。\n\n以下是`master-controller.h`:\n\n```cpp\n#ifndef MASTERCONTROLLER_H\n#define MASTERCONTROLLER_H\n#include <QObject>\n\n#include <cm-lib_global.h>\nnamespace cm {\nnamespace controllers {\nclass CMLIBSHARED_EXPORT MasterController : public QObject\n{\n    Q_OBJECT\npublic:\n    explicit MasterController(QObject* parent = nullptr);\n};\n\n}}\n\n#endif\n```\n\n我们真正添加到 Qt Creator 给我们的默认实现中的是 Qt Creator 在`cm-lib_global.h`中为我们编写的`CMLIBSHARED_EXPORT`宏，以处理我们的共享库导出，并将该类放在一个命名空间中。\n\nI always have the project name as a root namespace and then additional namespaces that reflect the physical location of the class files within the source directory, so in this case, I use `cm::controllers`, as the class is located in the directory `source/controllers`.\n\n这是`master-controller.cpp`:\n\n```cpp\n#include \"master-controller.h\"\n\nnamespace cm {\nnamespace controllers {\nMasterController::MasterController(QObject* parent)\n    : QObject(parent)\n{\n}\n\n}}\n\n```\n\nI use a slightly unorthodox style in the implementation file—most people just add `using namespace cm::controllers;` at the top of the `.cpp` file. I often like to put the code within the scope of namespaces because it becomes collapsible in the IDE. By repeating the innermost namespace scope (*controllers* in this example), you can break your code up into collapsible regions much like you can in C#, which helps with navigation in larger files, as you can collapse the sections you’re not interested in. It makes no functional difference, so use whichever style you prefer.\n\n# Q 对象\n\n那么，我们继承的这个不断出现的古怪的东西是什么？嗯，它是所有 Qt 对象的基类，它免费给了我们一些强大的功能。\n\nQObjects 将自己组织成对象层次结构，其中*父*对象承担其*子*对象的所有权，这意味着我们不必担心(同样多！)关于内存管理。例如，如果我们有一个从 QObject 派生的 Client 类的实例，它是同样从 QObject 派生的 Address 的父类，那么当客户端被销毁时，该地址会自动被销毁。\n\nQObjects 携带的元数据允许一定程度的类型检查，并且是与 QML 交互的支柱。他们还可以通过事件订阅机制相互通信，其中事件作为*信号*发出，订阅的代表被称为*槽*。\n\n现在您需要记住的是，对于您在用户界面中想要与之交互的任何自定义类，请确保它是从 QObject 派生的。无论何时从 QObject 派生，在做任何其他事情之前，确保总是将神奇的 Q_OBJECT 宏添加到类中。它注入了一堆超级复杂的样板代码，为了有效地使用 QObjects，您不需要理解这些代码。\n\n我们现在需要引用另一个(`cm-ui`)子项目(`cm-lib`中的`MasterController`)的代码。我们首先需要能够访问我们的`#include`声明的声明。如下编辑`cm-ui.pro`中的`INCLUDEPATH`变量:\n\n```cpp\nINCLUDEPATH += source \\\n    ../cm-lib/source\n```\n\n`\\`符号是“继续到下一行”的指示符，因此您可以将一个变量设置为跨越多行的多个值。就像控制台命令一样，'..'意味着向上遍历一个级别，所以这里我们从本地文件夹(`cm-ui`)开始，然后向下进入`cm-lib`文件夹，获取它的源代码。您需要注意项目文件夹相对于彼此保持在相同的位置，否则这将不起作用。\n\n就在下面，我们将告诉我们的 UI 项目在哪里可以找到我们的库项目的实现(编译的二进制)。如果您查看顶层`cm`项目文件夹旁边的文件系统，您将看到一个或多个构建文件夹，例如，build-cm-Desktop _ Qt _ 5 _ 9 _ 0 _ MinGW _ 32 bit-Debug。每个文件夹都是在我们为给定的工具包和配置运行 qmake 时创建的，并在我们构建时用输出填充。\n\n接下来，导航到与您正在使用的工具包和配置相关的文件夹，您会发现一个 cm-lib 文件夹，其中包含另一个配置文件夹。复制此文件路径；例如，我在 Debug 配置中使用的是 MinGW 32 位套件，所以我的路径是`<Qt Projects>/build-cm-Desktop_Qt_5_10_0_MinGW_32bit-Debug/cm-lib/debug`。\n\n在该文件夹中，您将找到与您的操作系统相关的已编译二进制文件，例如，Windows 上的`cm-lib.dll`。这是我们希望我们的`cm-ui`项目为`cm-lib`库实现提供参考的文件夹。要进行设置，请在`cm-ui.pro`中添加以下语句:\n\n```cpp\nLIBS += -L$$PWD/../../build-cm-Desktop_Qt_5_10_0_MinGW_32bit-Debug/cm-lib/debug -lcm-lib\n```\n\n`LIBS`是用于向项目添加引用库的变量。`-L`前缀表示目录，`-l`表示库文件。使用该语法，我们可以忽略文件扩展名(`.a`、`.o`、`.lib`)和前缀(lib...)，这可能因操作系统而异，让 qmake 来解决。我们使用特殊的`$$`符号来访问`PWD`变量的值，该变量包含当前项目的工作目录(本例中为`cm/cm-ui`的完整路径)。从那个位置，我们接着用`../..`向上钻取两个目录，以获得 Qt 项目文件夹。从那里，我们向下钻回到我们知道构建`cm-lib`二进制文件的位置。\n\n现在，这写起来很痛苦，丑得要命，一旦我们切换套件或配置就会掉下来，但我们稍后会回来整理这一切。随着项目参考资料全部连线，我们可以直接进入`cm-ui`中的`main.cpp`。\n\n为了能够在 QML 使用给定的类，我们需要注册它，在创建 QML 应用引擎之前，我们在`main()`中注册它。首先，包括`MasterController`:\n\n```cpp\n#include <controllers/master-controller.h>\n```\n\n然后，就在`QGuiApplication`实例化之后但在`QQmlApplicationEngine`声明之前，添加以下行:\n\n```cpp\nqmlRegisterType<cm::controllers::MasterController>(\"CM\", 1, 0, \"MasterController\");\n```\n\n我们在这里做的是用 QML 发动机注册类型。请注意，模板参数必须完全符合所有命名空间。我们将该类型的元数据添加到一个名为 CM 的模块中，版本号为 1.0，我们希望在 QML 标记中将该类型称为`MasterController`。\n\n然后，我们实例化`MasterController`的一个实例，并将其注入到根 QML 上下文中:\n\n```cpp\ncm::controllers::MasterController masterController;\n\nQQmlApplicationEngine engine;\nengine.rootContext()->setContextProperty(\"masterController\", &masterController);\nengine.load(QUrl(QStringLiteral(\"qrc:/views/MasterView\")));\n```\n\n请注意，您需要在加载 QML 文件之前设置 context 属性，并且还需要添加以下标头:\n\n```cpp\n#include <QQmlContext>\n```\n\n因此，我们已经创建了一个控制器，并在 QML 引擎上注册了它，现在可以运行了。现在怎么办？让我们先来看看 QML。\n\n# QML\n\n**Qt 建模语言** ( **QML** )是一种用于用户界面布局的分层声明性语言，其语法类似于 **JavaScript 对象符号** ( **JSON** )。它可以通过 Qt 的元对象系统绑定到 C++ 对象，还支持内联 JavaScript。它很像超文本标记语言或 XAML，但没有圣诞节。如果你是一个喜欢 JSON 多于 XML 的人，这只能是一件好事！\n\n继续打开`MasterView.qml`，我们会看到发生了什么。\n\n首先你会看到几个`import`语句。它们类似于 C++ 中的`#include`语句——它们引入了我们想要在视图中使用的一些功能。它们可以像 QtQuick 2.9 一样打包和版本化模块，也可以是本地内容的相对路径。\n\n接下来，QML 层次结构从一个窗口对象开始。对象的范围由后面的`{}`表示，因此大括号内的所有内容要么是对象的属性，要么是对象的子对象。\n\n属性遵循 JSON 属性语法，形式为键:值。一个显著的区别是，除非您提供字符串作为值，否则不需要语音标记。这里我们将 Window 对象的`visible`属性设置为`true`，窗口大小为 640 x 480 像素，在标题栏显示 Hello World。\n\n让我们更改标题并添加一条简单的消息。用客户端管理替换 Hello World 标题，并在窗口正文中插入文本组件:\n\n```cpp\nWindow {\n    visible: true\n    width: 640\n    height: 480\n    title: qsTr(\"Client Management\")\n\n    Text {\n        text: \"Welcome to the Client Management system!\"\n    }\n}\n```\n\n保存您的更改，运行 qmake 并运行应用:\n\n![](img/8a1da88e-5508-40df-b5f6-20da69d33968.png)\n\n让我们让`MasterController`开始获得它的保留，而不是在用户界面中硬编码我们的欢迎消息，我们将从我们的控制器动态地获得它。\n\n编辑`master-controller.h`并添加名为`welcomeMessage`的`QString`类型的新公共属性，将其设置为初始值:\n\n```cpp\nQString welcomeMessage = \"This is MasterController to Major Tom\";\n```\n\n你还需要`#include <QString>`。\n\n为了能够从 QML 访问该成员，我们需要配置一个新的属性。在 Q_OBJECT 宏之后但在第一个公共访问修饰符之前，添加以下内容:\n\n```cpp\nQ_PROPERTY( QString ui_welcomeMessage MEMBER welcomeMessage CONSTANT )\n```\n\n这里，我们正在创建一个新的 QString 类型的属性，QML 可以访问它。QML 将该属性称为`ui_welcomeMessage`，当被调用时，将获取(或设置)名为`welcomeMessage`的`MEMBER`变量中的值。我们提前明确设置了变量的值，不会改变，所以保持`CONSTANT`。\n\nYou can simply name the property `welcomeMessage`, rather than `ui_welcomeMessage`. My personal preference is to explicitly name things that are solely intended for UI consumption with a ui_ prefix to differentiate them from member variables and methods. Do whatever works for you.\n\n返回`MasterView.qml`，我们将使用这个属性。将`Text`组件的`text`属性更改为以下内容:\n\n```cpp\ntext: masterController.ui_welcomeMessage\n```\n\n注意 QML 编辑器如何识别`masterController`，甚至为其提供代码补全。现在，QML 将访问我们在`main()`中注入根上下文的`MasterController`实例的`ui_welcomeMessage`属性，而不是显示字符串作为消息，这反过来将获得`welcomeMessage`成员变量的值。\n\n构建并运行，现在您应该会看到来自`MasterController`的消息:\n\n![](img/fe3e204d-88f4-4203-9859-f16bde93e547.png)\n\n我们现在有了一个工作机制，让 QML 调用 C++ 代码，并获得我们想要提供的任何数据和业务逻辑。这里需要注意的一点是，我们的`MasterController`对`MasterView`的存在一无所知，这是 MVC 模式的关键部分。\n\n# 项目产出\n\n为了让我们的`cm-ui`项目知道在哪里可以找到`cm-lib`的实现，我们在项目文件中使用了`LIBS`变量。这是一个相当丑陋的文件夹名称，但它只有一行，而且一切都运行得很好，所以让事情保持原样可能很有诱惑力。然而，期待当我们准备好为测试甚至生产生产我们的第一个构建时。我们已经编写了一些非常聪明的代码，一切都在完美地构建和运行。我们将配置从调试切换到发布...一切都结束了。问题是，我们已经在项目文件中对库路径进行了硬编码，以便在`Debug`文件夹中查找。换一个不同的工具包或另一个操作系统，问题会更严重，因为使用不同的编译器会产生二进制兼容性问题。\n\n让我们设定几个目标:\n\n*   扔掉笨重的文件夹\n*   将所有编译后的二进制输出汇总到一个公共文件夹中`cm/binaries`\n*   将所有临时构建构件隐藏在自己的文件夹中`cm/<project>/build`\n*   为不同的编译器和架构创建单独的构建和二进制文件夹\n*   自动检测那些编译器和架构\n\n那么，这些有趣的长文件夹名从何而来呢？在 Qt 创建器中，单击导航栏中的项目模式图标。在构建和运行部分的左侧，选择桌面 Qt 5.9.0 MinGW 32 位>构建。在这里，您将看到该解决方案中 MinGW 工具包的构建设置，并且在影子构建复选框下，您将识别长构建目录。\n\n我们需要启用影子构建，因为这使我们能够为不同的套件在不同的位置执行构建。我们将在`.pro`文件中控制构建的精确输出，但是我们仍然需要在这里指定一个构建目录来保持 Qt Creator 的快乐。进入< Qt 项目>/阴影构建。使用窗格顶部的下拉菜单为每个构建配置(调试/发布/配置文件)以及您正在使用的所有套件重复此设置:\n\n![](img/09a8328e-26ef-439f-8546-7eb661821236.png)\n\n在你的文件系统中，删除任何旧的`build-cm…`文件夹。右键单击解决方案文件夹，然后运行 qmake。qmake 完成后，您应该会看到 shell `cm-lib`、`cm-tests`和`cm-ui`文件夹已经在< Qt 项目>/阴影构建中创建，并且长的`build-cm…`文件夹没有重新出现。\n\n动态设置任何相对路径的第一步是知道您当前所在的路径。当我们使用`$$PWD`获取项目工作目录时，我们已经在 qmake 中看到了这一点。为了帮助我们可视化正在发生的事情，让我们介绍我们的第一个 qmake 函数— `message()`。\n\n在`cm.pro`中添加以下一行——它在文件中的位置并不重要:\n\n```cpp\nmessage(cm project dir: $${PWD})\n```\n\n在`cm-lib.pro`增加以下一行:\n\n```cpp\nmessage(cm-lib project dir: $${PWD})\n```\n\n`message()`是 qmake 支持的一个测试功能，将提供的字符串参数输出到控制台。请注意，您不需要用双引号将文本括起来。保存更改时，您将看到解决方案项目和库项目的**项目工作目录** ( **PWD** )已注销到通用消息控制台:\n\n`Project MESSAGE: cm project dir: C:/projects/qt/cm`\n\n`Project MESSAGE: cm-lib project dir: C:/projects/qt/cm/cm-lib`\n\nqmake actually takes multiple passes over `.pro` files, so whenever you use `message()`, you may see the same output several times over in the console. You can filter out the majority of duplicates using `message()` in conjunction with a scope—`!build_pass:message(Here is my message)`. This prevents the `message()` method from being called during the build pass.\n\n如果我们回顾一下影子构建的 Qt Creator 的默认行为，我们会发现目标是允许多个构建并排放置。这是通过构造包含套件、平台和构建配置的不同文件夹名称来实现的:\n\n`build-cm-solution-Desktop_Qt_5_10_0_MinGW_32bit-Debug`\n\n在调试模式下，通过查看文件夹名称，您可以看到内容来自使用 Qt 5.10.0 桌面 MinGW 32 位工具包构建的 **cm** 项目。我们现在将以一种更干净、更灵活的方式重新实现这种方法。\n\n我们更喜欢由`Operating System > Compiler > Processor Architecture > Build Configuration`文件夹组成的层次结构，而不是将信息串联成一个长文件夹名。\n\n让我们先对这条路径进行硬编码，然后再进行自动化。编辑`cm-lib.pro`并添加:\n\n```cpp\nDESTDIR = $$PWD/../binaries/windows/gcc/x86/debug\nmessage(cm-lib output dir: $${DESTDIR})\n```\n\n这是为了反映我们正在调试模式下用 MinGW 32 位工具包在 Windows 上构建。如果你在不同的操作系统上，用 *osx* 或 *Linux* 替换 *Windows* 。我们添加了对`message()`的另一个调用，以在通用消息控制台中输出该目标目录。记住`$$PWD`提取的是正在处理的`.pro`文件的工作目录(本例中为`cm-lib.pro`，所以这就给了我们`<Qt Projects>/cm/cm-lib`。\n\n右键点击`cm-lib`项目，运行 qmake，构建。确保选择了 MinGW 工具包以及调试模式。\n\n导航到文件系统中的`<Qt Projects>/cm/binaries/<OS>/gcc/x86/debug`，您将看到我们的库二进制文件，没有相关的杂乱构建工件。这是很好的第一步，但是如果您现在将构建配置更改为 Release 或 switch kits，目标目录将保持不变，这不是我们想要的。\n\n我们将要实现的技术将在我们所有的三个项目中使用，所以与其在我们所有的`.pro`文件中复制配置，不如将配置提取到一个共享文件中并包含它。\n\n在根`cm`文件夹中，创建两个名为`qmake-target-platform.pri`和`qmake-destination-path.pri`的新空文本文件。在`cm-lib.pro`、`cm-tests.pro`和`cm-ui.pro`中，添加以下行:\n\n```cpp\ninclude(../qmake-target-platform.pri)\ninclude(../qmake-destination-path.pri)\n```\n\n将这些行添加到靠近`*.pro`文件顶部的某个地方。确切的顺序没有太大关系，只要它们在`DESTDIR`变量设置之前。\n\n编辑`qmake-target-platform.pri`如下:\n\n```cpp\nwin32 {\n    CONFIG += PLATFORM_WIN\n    message(PLATFORM_WIN)\n    win32-g++ {\n        CONFIG += COMPILER_GCC\n        message(COMPILER_GCC)\n    }\n    win32-msvc2017 {\n        CONFIG += COMPILER_MSVC2017\n        message(COMPILER_MSVC2017)\n        win32-msvc2017:QMAKE_TARGET.arch = x86_64\n    }\n}\n\nlinux {\n    CONFIG += PLATFORM_LINUX\n    message(PLATFORM_LINUX)\n    # Make QMAKE_TARGET arch available for Linux\n    !contains(QT_ARCH, x86_64){\n        QMAKE_TARGET.arch = x86\n    } else {\n        QMAKE_TARGET.arch = x86_64\n    }\n    linux-g++{\n        CONFIG += COMPILER_GCC\n        message(COMPILER_GCC)\n    }\n}\n\nmacx {\n    CONFIG += PLATFORM_OSX\n    message(PLATFORM_OSX)\n    macx-clang {\n        CONFIG += COMPILER_CLANG\n        message(COMPILER_CLANG)\n        QMAKE_TARGET.arch = x86_64\n    }\n    macx-clang-32{\n        CONFIG += COMPILER_CLANG\n        message(COMPILER_CLANG)\n        QMAKE_TARGET.arch = x86\n    }\n}\n\ncontains(QMAKE_TARGET.arch, x86_64) {\n    CONFIG += PROCESSOR_x64\n    message(PROCESSOR_x64)\n} else {\n    CONFIG += PROCESSOR_x86\n    message(PROCESSOR_x86)\n}\nCONFIG(debug, release|debug) {\n    CONFIG += BUILD_DEBUG\n    message(BUILD_DEBUG)\n} else {\n    CONFIG += BUILD_RELEASE\n    message(BUILD_RELEASE)\n}\n```\n\n在这里，我们利用 qmake 的平台检测能力将个性化标志注入`CONFIG`变量。在每个操作系统上，不同的平台变量变得可用。例如，在 Windows 上，`win32`变量存在，Linux 用`linux`表示，Mac OS X 用`macx`表示。我们可以使用这些带有大括号的平台变量，就像 if 语句一样:\n\n```cpp\nwin32 {\n    # This block will execute on Windows only…\n}\n```\n\n我们可以考虑平台变量的不同组合，弄清楚当前选择的套件使用的是什么编译器和处理器架构，然后给`CONFIG`添加开发者友好的标志，我们可以在后面的`.pro`文件中使用。请记住，我们正在尝试构建一条构建路径— `Operating System > Compiler > Processor Architecture > Build Configuration`。\n\n保存这些更改时，您应该会在常规消息控制台中看到类似以下内容的标志:\n\n```cpp\nProject MESSAGE: PLATFORM_WIN\nProject MESSAGE: COMPILER_GCC\nProject MESSAGE: PROCESSOR_x86\nProject MESSAGE: BUILD_DEBUG\n```\n\n尝试切换套件或更改构建配置，您应该会看到不同的输出。当我在发布模式下将我的工具包切换到 Visual Studio 2017 64 位时，我现在得到了以下信息:\n\n```cpp\nProject MESSAGE: PLATFORM_WIN\nProject MESSAGE: COMPILER_MSVC2017\nProject MESSAGE: PROCESSOR_x64\nProject MESSAGE: BUILD_RELEASE\n```\n\n在一台装有 MinGW 64 位工具包的 Linux 机器上进行同样的项目，我得到了这样的结果:\n\n```cpp\nProject MESSAGE: PLATFORM_LINUX\nProject MESSAGE: COMPILER_GCC\nProject MESSAGE: PROCESSOR_x64\nProject MESSAGE: BUILD_DEBUG\n```\n\n在使用 Clang 64 位的苹果电脑上，我得到了以下信息:\n\n```cpp\nProject MESSAGE: PLATFORM_OSX\nProject MESSAGE: COMPILER_CLANG\nProject MESSAGE: PROCESSOR_x64\nProject MESSAGE: BUILD_DEBUG\n```\n\nTo get this to work on Windows, I had to make an assumption as `QMAKE_TARGET.arch` is not correctly detected for MSVC2017, so I assumed that if the compiler is MSVC2017, then it must be x64 as there was no 32 bit kit available.\n\n现在所有的平台检测都完成了，我们可以动态地构建目标路径。编辑`qmake-destination-path.pri`:\n\n```cpp\nplatform_path = unknown-platform\ncompiler_path = unknown-compiler\nprocessor_path = unknown-processor\nbuild_path = unknown-build\n\nPLATFORM_WIN {\n    platform_path = windows\n}\nPLATFORM_OSX {\n    platform_path = osx\n}\nPLATFORM_LINUX {\n    platform_path = linux\n}\n\nCOMPILER_GCC {\n    compiler_path = gcc\n}\nCOMPILER_MSVC2017 {\n    compiler_path = msvc2017\n}\nCOMPILER_CLANG {\n    compiler_path = clang\n}\n\nPROCESSOR_x64 {\n    processor_path = x64\n}\nPROCESSOR_x86 {\n    processor_path = x86\n}\n\nBUILD_DEBUG {\n    build_path = debug\n} else {\n    build_path = release\n}\n\nDESTINATION_PATH = $$platform_path/$$compiler_path/$$processor_path/$$build_path\nmessage(Dest path: $${DESTINATION_PATH})\n```\n\n在这里，我们创建了四个新变量——*platform _ path*、*编译器 _path* 、*处理器 _path* 、*build _ path*——并为它们全部赋值。然后，我们使用在前面的文件中创建的`CONFIG`标志，并构建我们的文件夹层次结构，将其存储在我们自己的变量中，称为`DESTINATION_PATH`。例如，如果我们检测到 Windows 是操作系统，我们将`PLATFORM_WIN`标志添加到`CONFIG`中，结果是将`platform_path`设置为`windows`。在 Windows 上的套件和配置之间切换时，我现在收到以下消息:\n\n```cpp\nDest path: windows/gcc/x86/debug\n```\n\n或者，我得到这个:\n\n```cpp\nDest path: windows/msvc2017/x64/release\n```\n\n在 Linux 上，我得到了以下信息:\n\n```cpp\nDest path: linux/gcc/x64/debug\n```\n\n在苹果操作系统上，我得到的是:\n\n```cpp\nDest path: osx/clang/x64/debug\n```\n\n您可以将这些平台检测和目标路径创建技巧组合在一个文件中，但是通过将它们分开，您可以在项目文件的其他地方使用这些标志。无论如何，我们现在正在基于我们的构建环境动态地创建一个路径，并将其存储在一个变量中供以后使用。\n\n接下来要做的就是将这个`DESTINATION_PATH`变量插入到我们的项目文件中。当我们在这里的时候，我们也可以通过添加一些行来使用相同的机制来构造我们的构建工件。将以下内容添加到所有三个`*.pro`文件中，替换已经在`cm-lib.pro`中的`DESTDIR`语句:\n\n```cpp\nDESTDIR = $$PWD/../binaries/$$DESTINATION_PATH\nOBJECTS_DIR = $$PWD/build/$$DESTINATION_PATH/.obj\nMOC_DIR = $$PWD/build/$$DESTINATION_PATH/.moc\nRCC_DIR = $$PWD/build/$$DESTINATION_PATH/.qrc\nUI_DIR = $$PWD/build/$$DESTINATION_PATH/.ui\n```\n\n临时构建工件现在将被放入构建文件夹中的独立目录中。\n\n最后，我们可以解决最初把我们带到这里的问题。在`cm-tests`和`cm-ui`中，我们现在可以使用新的动态目标路径设置`LIBS`变量:\n\n```cpp\nLIBS += -L$$PWD/../binaries/$$DESTINATION_PATH -lcm-lib\n```\n\n现在，您可以右键单击`cm`项目，运行 qmake，并构建以一步自动构建所有三个子项目。所有的输出将被发送到正确的地方，库二进制文件可以很容易地被其他项目找到。您可以切换套件和配置，而不必担心引用错误的库。\n\n# 摘要\n\n在这一章中，我们将我们的项目创建技能提升到了一个新的水平，我们的解决方案现在开始成形。我们实现了一个 MVC 模式，并弥合了用户界面和业务逻辑项目之间的差距。我们涉猎了 QML 的第一部分，并研究了 Qt 框架的基石——Qobject。\n\n我们移除了所有难看的文件夹，舒展了肌肉，控制了所有文件的去向。所有二进制文件现在都放在`cm/binaries`文件夹中，按照平台、编译器、处理器架构和构建配置进行组织。最终用户不需要的所有临时构建工件现在都被隐藏起来了。我们可以自由切换套件和构建配置，并让我们的输出自动重新路由到正确的位置。\n\n在[第三章](3.html)、*用户界面*中，我们将设计我们的 UI，并陷入更多的 QML。"
  },
  {
    "path": "docs/learn-qt5/3.md",
    "content": "# 三、用户界面\n\n在本章中，我们将更详细地了解 QML，并勾画出我们的用户界面布局。我们将为所有屏幕创建占位符视图，并实现一个在它们之间导航的框架。我们还将讨论这些视图中的内容，特别是如何以灵活和响应的方式锚定和调整元素的大小。我们将讨论这些主题:\n\n*   用户界面设计\n*   创建视图\n*   StackView 组件\n*   锚定元件\n*   尺寸元素\n*   在视图之间导航\n\n# UX\n\n如果您曾经使用过其他声明式用户界面技术，如 HTML 和 XAML，它们通常采用父/子方法来处理用户界面，也就是说，有一个父视图或根视图一直存在，并包含全局功能，如顶级导航。然后，它有动态内容或子视图，可以根据需要切换，并在必要时提供上下文相关的命令。\n\n我们将采取同样的方法，我们的主视图是我们的用户界面的根。我们将添加一个全局导航栏和一个内容窗格，我们可以根据需要在其中添加和删除内容。子视图将可选地呈现命令栏，用于执行操作，例如，将记录保存到数据库。\n\n让我们看看我们的目标是什么:\n\n![](img/9f05c5d3-5098-498d-8980-8d761e081b46.png)\n\n导航栏( **1** )将一直存在，并包含将用户导航到应用内关键区域的按钮。默认情况下，栏会很窄，与按钮相关的命令会用图标表示；但是，按下切换按钮将扩展栏，为每个按钮显示附带的描述性文本。\n\n内容窗格( **2** )将是一堆子视图。导航到应用的不同区域将通过替换内容窗格中的子视图来实现。例如，如果我们在导航栏上添加一个新客户端按钮并按下它，我们将把**新客户端视图**推到内容框架堆栈上。\n\n命令栏( **3** )是一个可选元素，将用于向用户呈现更多命令按钮。导航栏的主要区别在于，这些命令与当前视图相关，对上下文敏感。例如，在创建新客户端时，我们需要一个“保存”按钮，但是在搜索客户端时，“保存”按钮没有任何意义。每个子视图将可选地呈现其自己的命令栏。这些命令将由图标表示，下面有一个简短的描述。\n\n现在让我们计划一下屏幕流，或者我们称之为视图的流:\n\n![](img/88cb4886-bef2-4acf-8fec-17b997c55f39.png)\n\n# 创建视图\n\n在 **cm-ui** 中，右键点击`views.qrc`，选择【新增】。选择 Qt > QML 文件，点击选择...：\n\n![](img/ccb03d0d-cb3b-4bd8-ae67-41aa6e02c9c6.png)\n\n在`cm-ui/ui/views`中创建`SplashView.qml`文件。重复此过程，直到创建完以下所有视图:\n\n| **文件** | **目的** |\n| `SplashView.qml` | 加载用户界面时显示的占位符视图。 |\n| `DashboardView.qml` | 中央“家”观。 |\n| `CreateClientView.qml` | 用于输入新客户端详细信息的视图。 |\n| `EditClientView.qml` | 用于读取/更新现有客户端详细信息的视图。 |\n| `FindClientView.qml` | 用于搜索现有客户端的视图。 |\n\n如前所述，在纯文本编辑器中编辑`views.qrc`。您将看到我们的新视图已添加到新的`qresource`块中，默认前缀如下:\n\n```cpp\n<RCC>\n    <qresource prefix=\"/views\">\n        <file alias=\"MasterView\">views/MasterView.qml</file>\n    </qresource>\n    <qresource prefix=\"/\">\n        <file>views/SplashView.qml</file>\n        <file>views/DashboardView.qml</file>\n        <file>views/CreateClientView.qml</file>\n        <file>views/EditClientView.qml</file>\n        <file>views/FindClientView.qml</file>\n    </qresource>\n</RCC>\n```\n\n还要注意项目导航器有点乱:\n\n![](img/6a8cda8d-a97a-4137-9b5e-b475274183ae.png)\n\n将所有新文件移入`“/views”`前缀块，并移除`“/”`块。为每个新文件添加别名:\n\n```cpp\n<RCC>\n    <qresource prefix=\"/views\">\n        <file alias=\"MasterView.qml\">views/MasterView.qml</file>\n        <file alias=\"SplashView.qml\">views/SplashView.qml</file>\n        <file alias=\"DashboardView.qml\">views/DashboardView.qml</file>\n        <file alias=\"CreateClientView.qml\">views/CreateClientView.qml</file>\n        <file alias=\"EditClientView.qml\">views/EditClientView.qml</file>\n        <file alias=\"CreateAppointmentView.qml\">views/CreateAppointmentView.qml</file>\n        <file alias=\"FindClientView.qml\">views/FindClientView.qml</file>\n    </qresource>\n</RCC>\n```\n\n一旦保存这些更改，您应该会看到导航器被清理:\n\n![](img/e95063e4-88c2-4ce9-8d72-943414f8359e.png)\n\n# StackView 先生\n\n我们的子视图将通过 **StackView** 组件呈现，该组件提供了一个基于堆栈的内置历史导航模型。新视图(在本文中，视图意味着几乎所有的 QML)在显示时被推送到堆栈上，并且可以从堆栈中弹出，以便返回到上一个视图。我们不需要使用历史功能，但是它们是非常有用的特性。\n\n要访问组件，我们首先需要引用模块，因此将导入添加到**主视图**:\n\n```cpp\nimport QtQuick.Controls 2.2\n```\n\n完成后，让我们用一个`StackView`替换包含欢迎信息的**文本**元素:\n\n```cpp\nStackView {\n    id: contentFrame\n    initialItem: \"qrc:/views/SplashView.qml\"\n}\n```\n\n我们给组件分配一个唯一的标识符`contentFrame`，这样我们就可以在 QML 的其他地方引用它，并且我们指定默认情况下我们想要加载哪个子视图——新的`SplashView`。\n\n接下来，编辑`SplashView`。将`QtQuick`模块版本更新为 2.9，使其与**主视图**相匹配(如果没有明确说明，请对所有其他 QML 文件执行此操作)。这并不是严格必要的，但是避免视图之间的不一致是一个很好的做法。在 Qt 的小版本中，通常没有太多的方法来破坏变化，但是引用不同版本 QtQuick 的两个视图上的相同代码可能会表现出不同的行为，从而导致问题。\n\n目前，我们要做的就是用这个视图制作一个 400 像素宽、200 像素高的矩形，它有一个“充满活力”的背景色，这样我们就可以看到它已经加载了:\n\n```cpp\nimport QtQuick 2.9\n\nRectangle {\n    width: 400\n    height: 200\n    color: \"#f4c842\"\n}\n```\n\n颜色可以使用十六进制 RGB 值来指定，就像我们在这里所做的那样，或者命名为 SVG 颜色。我通常觉得十六进制更容易，因为我永远记不住颜色的名字！\n\nIf you hover your cursor over the hex string in Qt Creator, you get a really useful little pop-up color swatch.\n\n现在运行应用，您应该会看到欢迎消息不再显示，取而代之的是一个光荣的橙黄色矩形，这是我们的 **SplashView** 。\n\n![](img/95abb8ea-4155-403a-85f6-5cd98845a7d4.png)\n\n# 锚\n\n我们的精彩新 **SplashView** 的一个小问题是，它实际上没有填满窗口。当然，我们可以将 400 x 200 的尺寸更改为 1024 x 768，以便与**主视图**相匹配，但是如果用户调整窗口大小会发生什么？现代用户界面都是响应性设计——动态内容可以适应它所呈现的显示，因此只适合一个平台的硬编码属性并不理想。幸运的是，主播们来救我们了。\n\n让我们把我们值得信赖的旧的**草稿栏**项目投入使用，看看锚在行动。\n\n右键单击`qml.qrc`，在`scratchpad`文件夹中的现有`main.qml`文件旁边添加一个新的`AnchorsDemo.qml` QML 文件。不要担心子文件夹或`.qrc`前缀、别名或任何类似的东西。\n\n浏览`main.cpp`并加载我们的新文件，而不是`main.qml`:\n\n```cpp\nengine.load(QUrl(QStringLiteral(\"qrc:/AnchorsDemo.qml\")));\n```\n\n接下来，将以下代码粘贴到`AnchorsDemo`中:\n\n```cpp\nimport QtQuick 2.9\nimport QtQuick.Window 2.2\n\nWindow {\n    visible: true\n    width: 1024\n    height: 768\n    title: qsTr(\"Scratchpad\")\n    color: \"#ffffff\"\n    Rectangle {\n        id: paleYellowBackground\n        anchors.fill: parent\n        color: \"#cece9e\"\n    }\n    Rectangle {\n        id: blackRectangleInTheCentre\n        width: 120\n        height: 120\n        anchors.centerIn: parent\n        color: \"#000000\"\n    }\n    Rectangle {\n        id: greenRectangleInTheCentre\n        width: 100\n        height: 100\n        anchors.centerIn: parent\n        anchors.verticalCenterOffset: 20\n        color: \"#008000\"\n    }\n    Rectangle {\n        id: redRectangleTopLeftCorner\n        width: 100\n        height: 100\n        anchors {\n            top: parent.top\n            left: parent.left\n        }\n        color: \"#800000\"\n    }\n    Rectangle {\n        id: blueRectangleTopLeftCorner\n        width: 100\n        height: 100\n        anchors{\n            top: redRectangleTopLeftCorner.bottom\n            left: parent.left\n        }\n        color: \"#000080\"\n    }\n    Rectangle {\n        id: purpleRectangleTopLeftCorner\n        width: 100\n        height: 100\n        anchors{\n            top: blueRectangleTopLeftCorner.bottom\n            left: parent.left\n            leftMargin: 20\n        }\n        color: \"#800080\"\n    }\n    Rectangle {\n        id: turquoiseRectangleBottomRightCorner\n        width: 100\n        height: 100\n        anchors{\n            bottom: parent.bottom\n            right: parent.right\n            margins: 20\n        }\n        color: \"#008080\"\n    }\n}\n```\n\n构建并运行该应用，您将看到这个令人困惑的场景:\n\n![](img/d9a0d9a2-5955-42c5-9096-0b79882dc347.png)\n\n起初，这一切可能看起来有点混乱，如果你的颜色感知不是最佳的，我很抱歉，但我们所做的只是绘制了一系列不同锚点值的花哨的彩色矩形。让我们逐一浏览每个矩形，看看发生了什么:\n\n```cpp\nRectangle {\n    id: paleYellowBackground\n    anchors.fill: parent\n    color: \"#cece9e\"\n}\n```\n\n我们的第一个矩形是暗黄棕色背景；`anchors.fill: parent`告诉矩形填充它的父矩形，不管它有多大。任何给定 QML 组件的父组件都是包含它的 QML 组件，这是层次结构中的下一级。在这种情况下，是**窗口**元素。**窗口**元素是 1024 x 768 像素，这就是矩形的大小。请注意，我们不需要为矩形指定宽度和高度属性，因为它们是从锚点推断出来的。\n\n这正是我们想要的 **SplashView** 的行为，但是在回到我们的主项目之前，让我们看看主播的一些其他能力:\n\n```cpp\nRectangle {\n    id: blackRectangleInTheCentre\n    width: 120\n    height: 120\n    anchors.centerIn: parent\n    color: \"#000000\"\n}\nRectangle {\n    id: greenRectangleInTheCentre\n    width: 100\n    height: 100\n    anchors.centerIn: parent\n    anchors.verticalCenterOffset: 20\n    color: \"#008000\"\n}\n```\n\n我们将一起看下两个矩形。首先，我们有一个 120 像素见方的黑色矩形；`anchors.centerIn: parent`将其定位在其父对象的中心。我们必须指定**宽度**和**高度**，因为我们只是定位它，而不是确定它的尺寸。\n\n接下来，我们有一个稍微小一点的绿色矩形，也以它的父矩形为中心。然后，我们使用`anchors.verticalCenterOffset`属性将它向屏幕下方移动 20 个像素。用于定位的 *x* 、 *y* 坐标系的根(0，0)在屏幕左上角；`verticalCenterOffset`添加到 y 坐标。正数将项目向下移动到屏幕上，负数将项目向上移动到屏幕上。其姊妹属性—`horizontalCenterOffset`—用于在 *x* 轴上进行调整。\n\n这里要注意的最后一点是，矩形是重叠的，绿色矩形胜出并显示为完整。黑色矩形被向后推并被遮挡。同样，我们所有的小矩形都位于大背景矩形的前面。QML 是以自上而下的方式渲染的，所以当根元素( **Window** )被绘制时，它的子元素会从文件的顶部到底部被逐个处理。因此，文件底部的项目将呈现在文件顶部呈现的项目之前。同样的道理，如果你把一面墙涂成白色，然后再涂成黑色，墙就会变成黑色，因为那是最后涂(渲染)的:\n\n```cpp\nRectangle {\n    id: redRectangleTopLeftCorner\n    width: 100\n    height: 100\n    anchors {\n        top: parent.top\n        left: parent.left\n    }\n    color: \"#800000\"\n}\n```\n\n接下来，我们画一个红色的矩形，而不是一次定位或调整整个矩形的大小，我们只是锚定某些边。我们取其**顶部**侧的锚，并将其对准其母公司(**窗口**)的**顶部**侧的锚。我们将它的 l **eft** 侧固定在它父母的**左侧**侧。因此，它被“附加”到左上角。\n\n我们必须键入以下内容:\n\n```cpp\nanchors.top: parent.top\nanchors.left: parent.left\n```\n\n这里另一个有用的语法糖是，与其这样做，我们可以删除重复，并在花括号内设置`anchors`组的子属性:\n\n```cpp\nanchors {\n    top: parent.top\n    left: parent.left\n}\n```\n\n接下来，蓝色矩形:\n\n```cpp\nRectangle {\n    id: blueRectangleTopLeftCorner\n    width: 100\n    height: 100\n    anchors{\n        top: redRectangleTopLeftCorner.bottom\n        left: parent.left\n    }\n    color: \"#000080\"\n}\n```\n\n这遵循相同的模式，尽管这一次不是只附加到它的父节点，我们还锚定到一个兄弟节点(红色矩形)，我们可以通过`id`属性引用它:\n\n```cpp\nRectangle {\n    id: purpleRectangleTopLeftCorner\n    width: 100\n    height: 100\n    anchors{\n        top: blueRectangleTopLeftCorner.bottom\n        left: parent.left\n        leftMargin: 20\n    }\n    color: \"#800080\"\n}\n```\n\n紫色矩形锚定在蓝色矩形的底部和窗口的左侧，但这里我们介绍我们的第一个边距。每一面都有自己的边界，在这种情况下，我们使用`leftMargin`给我们一个从左锚的偏移量，与我们之前使用`verticalCenterOffset`看到的方式完全相同:\n\n```cpp\nRectangle {\n    id: turquoiseRectangleBottomRightCorner\n    width: 100\n    height: 100\n    anchors{\n        bottom: parent.bottom\n        right: parent.right\n        margins: 20\n    }\n    color: \"#008080\"\n}\n```\n\n最后，我们的绿松石矩形使用了屏幕右侧的一些空白空间，并演示了如何使用`margins`属性同时设置所有四个边的边距。\n\n请注意，所有这些绑定都是动态的。尝试调整窗口大小，所有的矩形会自动适应。锚点是响应用户界面设计的一个很好的工具。\n\n让我们回到我们的`cm-ui`项目中的`SplashView`，应用我们刚刚学到的知识。将固定的**宽度**和**高度**属性替换为更动态的`anchors.fill`属性:\n\n```cpp\nRectangle {\n    anchors.fill: parent\n    color: \"#f4c842\"\n}\n```\n\n现在，`SplashView`将填充它的父元素。构建并运行，您会看到，它并没有像我们预期的那样充满屏幕，而是完全消失了。让我们看看为什么会这样。\n\n# 胶料\n\n我们的矩形将填充其父矩形，因此矩形的大小完全取决于其父矩形的大小。在 QML 层次结构中，包含矩形的组件是回到**主视图**中的`StackView`元素:\n\n```cpp\nStackView {\n    id: contentFrame\n    initialItem: Qt.resolvedUrl(\"qrc:/views/SplashView.qml\")\n}\n```\n\n通常，QML 组件足够聪明，可以根据他们的孩子来确定自己的尺寸。之前，我们已经将矩形设置为 400 x 200 的固定大小。`StackView`可以看着它说“我需要包含一个 400 x 200 的单个**矩形**，所以我也要做 400 x 200。轻松！”。我们总是可以否决它，并使用它的**宽度**和**高度**属性将其设置为其他尺寸，但是它可以计算出它想要的尺寸。\n\n回到`scratchpad`中，创建一个新的`SizingDemo.qml`视图并编辑`main.cpp`以在启动时加载它，就像我们对`AnchorsDemo`所做的那样。编辑`SizingDemo`如下:\n\n```cpp\nimport QtQuick 2.9\nimport QtQuick.Window 2.2\n\nWindow {\n    visible: true\n    width: 1024\n    height: 768\n    title: qsTr(\"Scratchpad\")\n    color: \"#ffffff\"\n    Column {\n        id: columnWithText\n        Text {\n            id: text1\n            text: \"Text 1\"\n        }\n        Text {\n            id: text2\n            text: \"Text 2\"\n            width: 300\n            height: 20\n        }\n        Text {\n            id: text3\n            text: \"Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3\"\n        }\n        Text {\n            id: text4\n            text: \"Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4\"\n            width: 300\n        }\n        Text {\n            id: text5\n            text: \"Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5\"\n            width: 300\n            wrapMode: Text.Wrap\n        }\n    }\n    Column {\n        id: columnWithRectangle\n        Rectangle {\n            id: rectangle\n            anchors.fill: parent\n        }\n    }\n    Component.onCompleted: {\n        console.log(\"Text1 - implicitWidth:\" + text1.implicitWidth + \" implicitHeight:\" + text1.implicitHeight + \" width:\" + text1.width + \" height:\" + text1.height)\n        console.log(\"Text2 - implicitWidth:\" + text2.implicitWidth + \" implicitHeight:\" + text2.implicitHeight + \" width:\" + text2.width + \" height:\" + text2.height)\n        console.log(\"Text3 - implicitWidth:\" + text3.implicitWidth + \" implicitHeight:\" + text3.implicitHeight + \" width:\" + text3.width + \" height:\" + text3.height)\n        console.log(\"Text4 - implicitWidth:\" + text4.implicitWidth + \" implicitHeight:\" + text4.implicitHeight + \" width:\" + text4.width + \" height:\" + text4.height)\n        console.log(\"Text5 - implicitWidth:\" + text5.implicitWidth + \" implicitHeight:\" + text5.implicitHeight + \" width:\" + text5.width + \" height:\" + text5.height)\n        console.log(\"ColumnWithText - implicitWidth:\" + columnWithText.implicitWidth + \" implicitHeight:\" + columnWithText.implicitHeight + \" width:\" + columnWithText.width + \" height:\" + columnWithText.height)\n        console.log(\"Rectangle - implicitWidth:\" + rectangle.implicitWidth + \" implicitHeight:\" + rectangle.implicitHeight + \" width:\" + rectangle.width + \" height:\" + rectangle.height)\n        console.log(\"ColumnWithRectangle - implicitWidth:\" + columnWithRectangle.implicitWidth + \" implicitHeight:\" + columnWithRectangle.implicitHeight + \" width:\" + columnWithRectangle.width + \" height:\" + columnWithRectangle.height)\n    }\n}\n```\n\n运行这个，你会得到另一个充满废话的屏幕:\n\n![](img/464111c3-4326-4a22-bf0e-9c51aed169f8.png)\n\n我们更感兴趣的是输出到控制台的内容:\n\n`qml: Text1 - implicitWidth:30 implicitHeight:13 width:30 height:13`\n\n`qml: Text2 - implicitWidth:30 implicitHeight:13 width:300 height:20`\n\n`qml: Text3 - implicitWidth:1218 implicitHeight:13 width:1218 height:13`\n\n`qml: Text4 - implicitWidth:1218 implicitHeight:13 width:300 height:13`\n\n`qml: Text5 - implicitWidth:1218 implicitHeight:65 width:300 height:65`\n\n`qml: ColumnWithText - implicitWidth:1218 implicitHeight:124 width:1218 height:124`\n\n`qml: Rectangle - implicitWidth:0 implicitHeight:0 width:0 height:0`\n\n`qml: ColumnWithRectangle - implicitWidth:0 implicitHeight:0 width:0 height:0`\n\n发生什么事了？我们已经创建了两个**列**元素，它们是垂直排列子元素的不可见布局组件。我们在第一列填充了各种**文本**元素，并在第二列添加了一个单独的**矩形**。在视图的底部是一个 JavaScript 函数，当**窗口**组件完成时(即加载完成)，该函数将执行。所有的功能就是在视图上写出各个元素的`implicitWidth`、`implicitHeight`、`width`和`height`属性。\n\n让我们浏览一下元素和相应的控制台行:\n\n```cpp\nText {\n    id: text1\n    text: \"Text 1\"\n}\n```\n\n`qml: Text1 - implicitWidth:30 implicitHeight:13 width:30 height:13`\n\n这个文本元素包含一小段文本，我们没有指定任何大小。它的`implicitWidth`和`implicitHeight`属性是元素希望基于其内容的大小。它的`width`和`height`属性是元素实际的大小。在这种情况下，它想怎么定就怎么定，因为我们没有另外说明，所以它的`width` / `height`和它的`implicitWidth` / `implicitHeight`是一样的:\n\n```cpp\nText {\n    id: text2\n    text: \"Text 2\"\n    width: 300\n    height: 20\n}\n```\n\n`qml: Text2 - implicitWidth:30 implicitHeight:13 width:300 height:20`\n\n对于`text2`，隐式大小与`text1`相同，因为内容实际上是相同的。然而，这一次，我们明确告诉它是 300 宽，20 高。控制台告诉我们，元素正在按照它被告知的那样工作，并且确实是这个大小:\n\n```cpp\nText {\n    id: text3\n    text: \"Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3 Text 3\"\n}\n```\n\n`qml: Text3 - implicitWidth:1218 implicitHeight:13 width:1218 height:13`\n\n这个`text3`采取了与`text1`相同的不干涉方法，但是它的内容是一段更长的文本。这一次，`implicitWidth`要大得多，因为这是容纳长文本所需的空间。请注意，这实际上比窗口更宽，文本被剪掉了。同样，我们没有另外指示它，所以它会自己调整大小:\n\n```cpp\nText {\n    id: text4\n    text: \"Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4 Text 4\"\n    width: 300\n}\n```\n\n`qml: Text4 - implicitWidth:1218 implicitHeight:13 width:300 height:13`\n\n`text4`有同样长的文本块，但是我们已经告诉它这次我们想要的宽度。您会在屏幕上注意到，即使元素只有 300 像素宽，文本在整个窗口中都是可见的。内容溢出了容器的边界。您可以将`clip`属性设置为`true`来防止这种情况，但我们不太关心这一点:\n\n```cpp\nText {\n    id: text5\n    text: \"Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text \n    5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5   \n    Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text \n    5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5 Text 5\"\n    width: 300\n    wrapMode: Text.Wrap\n}\n```\n\n`qml: Text5 - implicitWidth:1218 implicitHeight:65 width:300 height:65`\n\n`text5`重复相同的长文本块，并将宽度限制为 300，但是这一次，我们通过将`wrapMode`属性设置为`Text.Wrap`来为程序带来一点秩序。使用此设置，启用的行为更像是您对文本块的期望——它填满了可用的宽度，然后换行到下一行。元素的`implicitHeight`和`height`也随之增加，以容纳内容物。但是注意`implicitHeight`还是和之前一样；考虑到我们已经定义的约束，并且我们没有定义高度约束，这仍然是控件想要的宽度，以便适合它的所有内容。\n\n然后，我们打印出包含所有这些文本的列的属性:\n\n`qml: ColumnWithText - implicitWidth:1218 implicitHeight:124 width:1218 height:124`\n\n需要注意的重要一点是，该列能够计算出它需要多宽多高来容纳它的所有子级。\n\n接下来，我们回到我们在`SplashView`中遇到的问题:\n\n```cpp\nColumn {\n    id: columnWithRectangle\n    Rectangle {\n        id: rectangle\n        anchors.fill: parent\n    }\n}\n```\n\n这里，我们有一个先有鸡还是先有蛋的场景。`Column`试图计算出它需要多大才能容纳它的孩子，所以它看了一下`Rectangle`。`Rectangle`没有明确的大小信息，也没有自己的子代，它只是设置为填充其父代`Column`。两个元素都无法计算出它们应该有多大，所以它们都默认为 0x0，这使得它们不可见。\n\n`qml: Rectangle - implicitWidth:0 implicitHeight:0 width:0 height:0`\n\n`qml: ColumnWithRectangle - implicitWidth:0 implicitHeight:0 width:0 height:0`\n\nSizing of elements is probably the thing that has caught me out the most with QML over the years. As a general guideline, if you write some QML but then can’t see it rendered on screen, it’s probably a sizing issue. I usually find that giving everything an arbitrary fixed **width** and **height** is a good start when debugging, and then one by one, make the sizes dynamic until you recreate the problem.\n\n有了这些知识，让我们回到`MasterView`并解决我们之前的问题。\n\n将`anchors.fill: parent`添加到`StackView`组件中:\n\n```cpp\nStackView {\n    id: contentFrame\n    anchors.fill: parent\n    initialItem: Qt.resolvedUrl(\"qrc:/views/SplashView.qml\")\n}\n```\n\n`StackView`现在将填充其父**窗口**，我们已经明确给出了 1024 x 768 的固定大小。再次运行该应用，您现在应该有一个可爱的橙黄色`SplashView`，如果您调整窗口大小，它会填充屏幕并愉快地调整大小:\n\n![](img/39e7344e-d964-43d5-ad69-841e8612c143.png)\n\n# 航行\n\n让我们快速补充一下我们的`SplashView`:\n\n```cpp\nRectangle {\n    anchors.fill: parent\n    color: \"#f4c842\"\n    Text {\n        anchors.centerIn: parent\n        text: \"Splash View\"\n    }\n}\n```\n\n这只是将视图的名称添加到屏幕上，因此当我们开始在视图之间移动时，我们知道我们在看哪个视图。完成后，将`SplashView`的内容复制到所有其他新视图中，更新每个视图中的文本以反映视图的名称，例如，在`DashboardView`中，文本可以是“仪表板视图”。\n\n我们要做的第一个导航是当`MasterView`完成加载，我们准备好行动时，加载`DashboardView`。我们使用我们刚刚看到的一个 QML 组件插槽来实现这一点— `Component.onCompleted()`。\n\n在`MasterView`的根`Window`组件中添加以下行:\n\n```cpp\nComponent.onCompleted: contentFrame.replace(\"qrc:/views/DashboardView.qml\");\n```\n\n现在当您构建并运行时，一旦`MasterView`完成加载，它就会将子视图切换到`DashboardView`。这可能发生得太快了，以至于你再也看不到`SplashView`，但它仍然存在。如果您有一个需要进行大量初始化的应用，并且您不能真正拥有非阻塞式用户界面，那么拥有这样的闪屏视图是非常好的。这是一个方便放置公司标志和“网状样条线”的地方正在加载消息。是的，那是一个模拟人生参考！\n\nStackView 就像网页浏览器中的历史。如果你先去[www.google.com](http://www.google.com)再去[www.packtpub.com](http://www.packtpub.com)，你就是*把*[www.packtpub.com](http://www.packtpub.com)推到栈上。如果您点击浏览器上的返回，您将返回到[www.google.com](http://www.google.com)。该历史可以由几个页面(或视图)组成，您可以在其中前后导航。有时你不需要历史，有时你又不希望用户能够回到过去。顾名思义，我们调用的`replace()`方法会将新视图推送到堆栈上，并清除任何历史记录，这样您就无法返回。\n\n在`Component.onCompleted`槽中，我们看到了一个如何直接从 QML 在视图之间导航的例子。我们可以将这种方法用于所有的应用导航。例如，我们可以为用户添加一个按钮来创建一个新的客户端，当它被点击时，将`CreateClientView`直接推到堆栈上，如下所示:\n\n```cpp\nButton {\n    onClicked: contentFrame.replace(\"qrc:/views/CreateClientView.qml\")\n}\n```\n\n对于 UX 设计或简单的用户界面繁重的应用，几乎没有业务逻辑，这是一个完全有效的方法。问题在于，您的 QML 视图和组件变得非常紧密地耦合在一起，业务逻辑层不知道用户在做什么。通常，移动到应用的新屏幕并不像显示新视图那么简单。您可能需要更新一个状态机，设置一些模型，或者从以前的视图中清除一些数据。通过我们的**主控制器**交换机路由我们所有的导航请求，我们分离我们的组件，并为我们的业务逻辑获得一个截取点，以采取它需要的任何行动，并验证请求是否合适。\n\n我们将通过从业务逻辑层发出信号并让我们的**主视图**响应这些信号并执行转换来请求导航到这些视图。与其弄乱我们的**主控制器**，我们将把导航的责任委托给`cm-lib`中的新控制器，所以创建一个新的头文件(没有这样的实现，所以我们不需要`cm/cm-lib/source/controllers`中名为`navigation-controller.h`的`.cpp`文件)，并添加以下代码:\n\n```cpp\n#ifndef NAVIGATIONCONTROLLER_H\n#define NAVIGATIONCONTROLLER_H\n\n#include <QObject>\n\n#include <cm-lib_global.h>\n#include <models/client.h>\n\nnamespace cm {\nnamespace controllers {\n\nclass CMLIBSHARED_EXPORT NavigationController : public QObject\n{\n    Q_OBJECT\n\npublic:\n    explicit NavigationController(QObject* _parent = nullptr)\n        : QObject(_parent)\n    {}\n\nsignals:\n    void goCreateClientView();\n    void goDashboardView();\n    void goEditClientView(cm::models::Client* client);\n    void goFindClientView();\n};\n\n}\n}\n#endif\n```\n\n我们已经创建了一个继承自`QObject`的最小类，并为我们的每个新视图实现了一个信号。请注意，我们不需要导航到**主视图**或**飞溅视图**，因此没有相应的信号。当我们导航到`EditClientView`时，我们需要通知用户界面我们想要编辑哪个**客户端**，所以我们将把它作为参数传递。从我们的业务逻辑代码中的任何地方调用其中的一个方法，都会向以太发出一个请求，说“我想去某某视图，请”。然后由用户界面层的**主视图**来监控这些请求并做出相应的响应。请注意，业务逻辑层仍然对 UI 实现一无所知。如果没人回应信号也没关系；这不是双向交流。\n\nWhenever you inherit from `QObject`, always remember the `Q_OBJECT` macro and also an overloaded constructor that takes a `QObject` parent. As we want to use this class outside of this project (in the UI project), we must also remember the CMLIBSHARED_EXPORT macro.\n\n我们在这里向前看了一点，并假设我们的 Client 类将位于`cm::models`命名空间中，但是 Qt 在我们创建项目时为我们添加的默认`Client`类不是，所以让我们在继续之前修复它:\n\n**客户端. h** :\n\n```cpp\n#ifndef CLIENT_H\n#define CLIENT_H\n\n#include \"cm-lib_global.h\"\n\nnamespace cm {\nnamespace models {\n\nclass CMLIBSHARED_EXPORT Client\n{\npublic:\n    Client();\n};\n\n}}\n\n#endif\n```\n\n`client.cpp`:\n\n```cpp\n#include \"client.h\"\n\nnamespace cm {\nnamespace models {\n\nClient::Client()\n{\n}\n\n}}\n```\n\n我们需要能够创建一个导航控制器的实例，并让我们的用户界面与之交互。出于单元测试的原因，将对象创建隐藏在某种对象工厂接口之后是一个很好的做法，但是我们在这个阶段并不关心这个，所以我们将简单地在**主控制器**中创建对象。让我们借此机会也给我们的**主控制器**添加私有实现(PImpl)这个成语。如果您以前没有遇到过 PImpl，它只是一种将所有私有实现细节移出头文件并放入定义中的技术。这有助于保持头文件尽可能的短和干净，只包含公共 API 的使用者所必需的内容。将宣言和执行改为:\n\n`master-controller.h`:\n\n```cpp\n#ifndef MASTERCONTROLLER_H\n#define MASTERCONTROLLER_H\n\n#include <QObject>\n#include <QScopedPointer>\n#include <QString>\n\n#include <cm-lib_global.h>\n#include <controllers/navigation-controller.h>\n\nnamespace cm {\nnamespace controllers {\n\nclass CMLIBSHARED_EXPORT MasterController : public QObject\n{\n    Q_OBJECT\n    Q_PROPERTY( QString ui_welcomeMessage READ welcomeMessage CONSTANT )\n    Q_PROPERTY( cm::controllers::NavigationController* ui_navigationController READ navigationController CONSTANT )\n\npublic:\n    explicit MasterController(QObject* parent = nullptr);\n    ~MasterController();\n\n    NavigationController* navigationController();\n    const QString& welcomeMessage() const;\n\nprivate:\n    class Implementation;\n    QScopedPointer<Implementation> implementation;\n};\n\n}}\n#endif\n```\n\n`master-controller.cpp`:\n\n```cpp\n#include \"master-controller.h\"\n\nnamespace cm {\nnamespace controllers {\n\nclass MasterController::Implementation\n{\npublic:\n    Implementation(MasterController* _masterController)\n        : masterController(_masterController)\n    {\n        navigationController = new NavigationController(masterController);\n    }\n\n    MasterController* masterController{nullptr};\n    NavigationController* navigationController{nullptr};\n    QString welcomeMessage = \"This is MasterController to Major Tom\";\n};\n\nMasterController::MasterController(QObject* parent)\n    : QObject(parent)\n{\n    implementation.reset(new Implementation(this));\n}\n\nMasterController::~MasterController()\n{\n}\n\nNavigationController* MasterController::navigationController()\n{\n    return implementation->navigationController;\n}\n\nconst QString& MasterController::welcomeMessage() const\n{\n    return implementation->welcomeMessage;\n}\n\n}}\n```\n\nYou may have noted that we don’t specify the cm::controllers namespace for the NavigationController accessor method, but we do for the `Q_PROPERTY`. This is because the property is accessed by the UI QML, which is not executing within the scope of the `cm` namespace, so we have to explicitly specify the fullyqualified name. As a general rule of thumb, be explicit about namespaces for anything that QML interacts with directly, including parameters in signals and slots.\n\n接下来，我们需要在 **cm-ui** 项目中向 QML 系统注册新的`NavigationController`类，因此在`main.cpp`中，在现有的**主控制器**类旁边添加以下注册:\n\n```cpp\nqmlRegisterType<cm::controllers::NavigationController>(\"CM\", 1, 0, \"NavigationController\");\n```\n\n我们现在准备架设**主视图**对这些导航信号做出反应。在`StackView`前增加以下元素:\n\n```cpp\nConnections {\n    target: masterController.ui_navigationController\n    onGoCreateClientView: contentFrame.replace(\"qrc:/views/CreateClientView.qml\")\n    onGoDashboardView: contentFrame.replace(\"qrc:/views/DashboardView.qml\")\n    onGoEditClientView: contentFrame.replace(\"qrc:/views/EditClientView.qml\", {selectedClient: client})\n    onGoFindClientView: contentFrame.replace(\"qrc:/views/FindClientView.qml\")\n}\n```\n\n我们正在创建一个连接组件，绑定到我们的新实例**导航控制器**，它对我们添加的每个 go 信号做出反应，并通过`contentFrame`导航到相关视图，使用与我们之前移动到仪表板相同的`replace()`方法。因此，每当`goCreateClientView()`信号在**导航控制器**上被触发时，`onGoCreateClientView()`槽被调用到我们的`Connections`组件上，并且`CreateClientView`被加载到名为`contentFrame`的**堆栈视图**中。在`onGoEditClientView`的情况下，从信号中传递一个`client`参数，我们将该对象传递给一个名为`selectedClient`的属性，稍后我们会将其添加到视图中。\n\nSome signals and slots in QML components are automatically generated and connected for us and are convention based. Slots are named `on[CapitalisedNameOfRelatedSignal]`. So, for example, if you have a signal called `mySplendidSignal()`, then the corresponding slot will be named `onMySplendidSignal`. These conventions are in play with our `NavigationController` and `Connections` components.\n\n接下来，让我们在**主视图**中添加一个带有一些占位符按钮的导航栏，这样我们就可以尝试这些信号了。\n\n添加一个`Rectangle`来形成我们酒吧的背景:\n\n```cpp\nRectangle {\n    id: navigationBar\n    anchors {\n        top: parent.top\n        bottom: parent.bottom\n        left: parent.left\n    }\n    width: 100\n    color: \"#000000\"\n}\n```\n\n这将在视图的左侧绘制一个 100 像素宽的黑色条。\n\n我们还需要调整我们的`StackView`，以便它为我们的酒吧留出一些空间。与其填充它的父项，不如将它的四个边中的三个锚定到它的父项，但是将左侧附加到我们的栏的右侧:\n\n```cpp\nStackView {\n    id: contentFrame\n    anchors {\n        top: parent.top\n        bottom: parent.bottom\n        right: parent.right\n        left: navigationBar.right\n    }\n    initialItem: Qt.resolvedUrl(\"qrc:/views/SplashView.qml\")\n}\n```\n\n现在，让我们在导航`Rectangle`中添加一些按钮:\n\n```cpp\n Rectangle {\n    id: navigationBar\n    …\n\n    Column {\n        Button {\n            text: \"Dashboard\"\n            onClicked: masterController.ui_navigationController.goDashboardView()\n        }\n        Button {\n            text: \"New Client\"\n            onClicked: masterController.ui_navigationController.goCreateClientView()\n        }\n        Button {\n            text: \"Find Client\"\n            onClicked: masterController.ui_navigationController.goFindClientView()\n        }\n    }\n\n}\n```\n\n我们使用`Column`组件来为我们布局按钮，而不是必须单独将按钮相互锚定。每个按钮显示一些文本，当点击时，在**导航控制器**上调用一个信号。我们的`Connection`组件对信号做出反应，并为我们执行视图转换:\n\n![](img/aad6160f-75eb-4122-a0f4-1b1e6e5790dd.png)\n\n好东西，我们有一个功能导航框架！但是，当您单击其中一个导航按钮时，导航栏会暂时消失，然后再次出现。在我们的**应用输出**控制台中，我们也收到了“锚冲突”的消息，这表明我们正在做一些不太正确的事情。让我们在继续前进之前解决这些问题。\n\n# 修复冲突\n\n导航栏问题很简单。如前所述，QML 在结构上是分等级的。这体现在元素的呈现方式上——首先出现的子元素首先呈现。在我们的例子中，我们绘制导航栏，然后绘制内容框架。当 **StackView** 组件加载新内容时，默认情况下，它会应用时髦的过渡来使其看起来不错。这些转换会导致内容移出控件边界，并覆盖其下的任何内容。有几种方法可以解决这个问题。\n\n首先，我们可以重新排列组件的呈现顺序，并将导航栏放在内容框架之后。这将在`StackView`的顶部绘制导航条，不管它是怎么回事。第二个选项，也是我们将要实现的选项，是简单地设置 **StackView** 的`clip`属性:\n\n```cpp\nclip: true\n```\n\n这将剪辑任何与控件边界重叠的内容，并且不会呈现它。\n\n下一个问题有点深奥。正如我们已经讨论过的，在过去几年的 QML 开发中，我遇到的最大的困惑是组件的尺寸。我们使用的一些组件，如**矩形**，本质上是视觉元素。如果没有定义它们的大小，无论是直接用`width/height`属性还是间接用**锚点**，那么它们都不会渲染。其他元素如**连接**根本不可见，尺寸属性是多余的。像**列**这样的布局元素在一个轴上可能有固定的大小，但在另一个轴上本质上是动态的。\n\n大多数组件的一个共同点是它们继承自**项**，而**项又直接继承自 **QtObject** ，而后者只是一个普通的 **QObject** 。就像 C++ 端的 Qt Framework 为普通旧的 **QObject** *实现了很多默认行为一样，QML 组件经常为**项**组件实现默认行为，我们可以在这里利用这些默认行为。**\n\n在我们的子视图中，我们使用了**矩形**作为根对象。这很有意义，因为我们想要显示一个固定大小和颜色的矩形。然而，这给 **StackView** 带来了问题，因为它不知道应该是什么尺寸。为了提供这些信息，我们尝试将它锚定到它的父视图(T4)上，但是当我们切换视图时，由于与**堆栈视图**试图执行的转换冲突，这会导致它自己的问题。\n\n我们摆脱这种困境的方法是让我们孩子观点的根源成为一个普通的旧**项**。 **StackView** 组件有处理**项**组件的内部逻辑，只会为我们调整大小。然后，我们的**矩形**组件成为已经自动调整大小的**项目**组件的子组件，我们可以锚定到该子组件:\n\n```cpp\nItem {\n    Rectangle {\n        ...\n    }\n}\n```\n\n这一切都有点令人困惑，感觉像巫毒教，但这里的要点是，将**物品**作为 QML 习俗的根元素通常是一件好事。继续以这种方式向所有子视图添加根**项**组件(但不添加**主视图**)。\n\n再次运行该应用，您现在应该有很好的平滑过渡，并且控制台中没有警告消息。\n\n# 摘要\n\n我们有一个灵活的、解耦的导航机制，并且正在不同的视图之间成功地转换。我们已经准备好了导航栏的基础知识和本章开头设计的工作内容窗格。\n\n让用户界面调用业务逻辑层来发出一个信号，然后用户界面对该信号做出反应，这看起来像是一种在视图之间导航的迂回方式，但是这种业务逻辑信号/用户界面槽设计带来了好处。它保持了用户界面的模块化，因为视图不需要相互了解。它将导航逻辑保留在业务逻辑层中，并使该层能够请求用户界面将用户导航到特定的视图，而无需了解用户界面或视图本身的任何信息。至关重要的是，它还为我们提供了拦截点，以便当用户请求导航到给定视图时，我们可以处理它并执行任何我们需要的附加处理，例如状态管理或清理。\n\n在[第 4 章](4.html) *【样式】*中，我们将介绍一个共享样式组件，以及 QML 模块和图标，然后我们用一个动态命令栏完成我们的 UI 设计。"
  },
  {
    "path": "docs/learn-qt5/4.md",
    "content": "# 四、样式\n\n一般来说，在开发过程中，将功能放在形式之前是一个好主意，但是用户界面是用户交互的应用的一部分，也是成功解决方案的关键要素。在这一章中，我们将介绍一个类似于 CSS 的样式资源，并基于我们在上一章中介绍的响应设计原则。\n\n我们将创建定制的 QML 组件和模块，以最大限度地提高代码重用。我们将把 Font Awesome 集成到我们的解决方案中，为我们提供一套可扩展的图标，并帮助我们的用户界面呈现出现代的图形外观。我们将整理导航栏，介绍命令的概念，并为动态的、上下文相关的命令栏构建框架。\n\n我们将在本章中讨论以下主题:\n\n*   自定义样式资源\n*   字体惊人\n*   自定义组件\n*   导航栏样式\n*   命令\n\n# 样式资源\n\n首先，让我们创建一个新的资源文件来包含我们需要的非 QML 视觉元素。在`cm-ui`项目中，添加新项...> Qt > Qt 资源文件:\n\n![](img/98aae715-a183-4b20-a955-8370098aad56.png)\n\n命名文件`assets.qrc`并放入`cm/cm-ui`。您的新文件将在资源编辑器中自动打开，我觉得这不是一个特别有用的编辑器，所以关闭它。您将看到`assets.qrc`文件已经被添加到`cm-ui`项目的参考资料部分。右键单击并选择添加新的… > Qt > QML 文件。调用文件`Style.qml`并保存到`cm/cm-ui/assets`。\n\n在纯文本编辑器中编辑`assets.qrc`文件，方法与我们编辑视图的方法相同:\n\n```cpp\n<RCC>\n    <qresource prefix=\"/assets\">\n        <file alias=\"Style.qml\">img/Style.qml</file>\n    </qresource>\n</RCC>\n```\n\n现在，编辑`Style.qml`，我们将添加一个样式属性用于视图的背景颜色:\n\n```cpp\npragma Singleton\nimport QtQuick 2.9\n\nItem {\n    readonly property color colourBackground: \"#f4c842\"\n}\n```\n\n我们在这里用 C++ 术语做的是用一个名为`colourBackground`的 const color 类型的公共成员变量创建一个 singleton 类，该变量的初始化值为(非常)浅灰色的十六进制 RGB 代码。\n\n现在，我们需要做一点手工操作来连接。我们需要在与`Style.qml` ( `cm/cm-ui/assets`)相同的文件夹中创建一个名为`qmldir`(无文件扩展名)的模块定义文件。这种类型的文件没有内置模板，所以我们需要自己创建。旧版本的 Windows 中的文件资源管理器曾经使这成为一个痛苦的练习，因为它总是坚持文件扩展名。需要控制台命令来强制重命名文件。Windows 10 很乐意创建没有扩展名的文件。在 Unix 世界中，没有扩展名的文件更常见。\n\n创建`qmldir`文件后，编辑`assets.qrc`，并在`/assets`前缀内的`Style.qml`旁边插入一个新条目:\n\n```cpp\n<file alias=\"qmldir\">img/qmldir</file>\n```\n\n双击新添加的`qmldir`文件，输入如下行:\n\n```cpp\nmodule assets\nsingleton Style 1.0 Style.qml\n```\n\n当我们**导入 QtQuick 2.9** 时，我们已经看到了模块。这使得 QtQuick 模块的 2.9 版本可以在我们的视图中使用。在我们的`qmldir`文件中，我们定义了一个自己的新模块`assets`，并告诉 Qt 在该模块的 1.0 版本中有一个**样式**对象，其实现在我们的`Style.qml`文件中。\n\n随着我们新样式模块的创建和连线，现在让我们使用现代的灰白色。从我们看到的第一个子视图`SplashView`开始，添加以下内容来访问我们的新模块:\n\n```cpp\nimport assets 1.0\n```\n\n你会注意到，我们看到一条愤怒的红色下划线，表明一切都不顺利。用鼠标指针悬停在线上，工具提示会告诉我们需要将导入路径添加到我们新的`qmldir`定义文件中。\n\n有几种方法可以做到这一点。第一个选项是进入项目模式，选择当前工具包的构建设置，然后选择调试模式。在构建环境部分的底部，单击详细信息。在这里，您可以看到当前套件和配置的所有环境变量的列表。添加一个名为 QML2_IMPORT_PATH 的新变量，并将其值设置到`cm-ui`文件夹:\n\n![](img/15b22f72-580d-476a-bc12-0b56ead7901c.png)\n\n这会将`cm-ui`项目(`/projects/qt/cm/cm-ui`)的项目工作目录添加到 QML 导入路径中。请注意，我们的模块名称必须反映从该导入路径到`qmldir`文件的相对路径。\n\n这种方法的问题在于，这个环境变量被绑定到`cm.pro.user`文件。如果你和其他开发人员共享这个项目，他们会有自己的`cm.pro.user`文件，他们也要记得添加这个变量。此外，它绑定到一个绝对路径，如果您将项目代码复制到另一台机器上，它可能不在那个位置。\n\n第二个，也是首选的选项是在实例化**qmlapplicationengine**后立即在`main.cpp`中添加以下行:\n\n```cpp\nengine.addImportPath(\"qrc:/\");\n```\n\n那么为什么`qrc:/`不是我们`qmldir`文件的绝对路径呢？你会记得我们在`cm-ui.pro`的`RESOURCES`变量中添加了`views.qrc`资源包。这样做是从`views.qrc`获取所有文件，并将它们编译成一种虚拟文件系统中的应用二进制文件，其中前缀充当虚拟文件夹。这个虚拟文件系统的根被称为`qrc:/`，通过在导入路径中使用它，我们实际上是要求 Qt 在所有捆绑的资源文件中查找任何模块。前往`cm-ui.pro`并确保我们的新`assets.qrc`也已添加至`RESOURCES`:\n\n```cpp\nRESOURCES += views.qrc \\\n    assets.qrc\n```\n\n这可能有点令人困惑，所以重申一下，我们添加了以下文件夹来搜索新模块，或者使用 QML2_IMPORT_PATH 环境变量来搜索本地物理文件系统上的`cm-ui`项目文件夹，或者使用`addImportPath()`方法在运行时搜索虚拟资源文件系统的根目录。\n\n在这两种情况下，定义新模块的`qmldir`文件位于名为`assets`的文件夹中，该文件夹的级别低于此级别，即物理文件系统中的`<Qt Projects>/cm/cm-ui/assets`或虚拟文件系统中的`qrc:/assets`。\n\n这就给了我们模块名称`assets`。如果我们的文件夹结构更深，像东西/獾/资产，那么我们的模块将需要被称为`stuff.badgers.assets`，因为这是相对于我们定义的导入路径的路径。类似地，如果我们想为我们现有的视图添加另一个模块，我们将在`cm-ui/views`中创建一个`qmldir`文件，并调用模块`views`。\n\nIf you see that Qt Creator is still a bit confused and the red line still persists, ensure that `cm-ui.pro` contains the `QML_IMPORT_PATH += $$PWD` line.\n\n有了所有这些，我们现在可以使用我们的新模块。包含该模块意味着我们现在可以访问我们的 singleton `Style`对象并从中读取属性。更换我们`SplashView`的`color`房产:\n\n```cpp\nRectangle {\n    ...    \n    color: Style.colourBackground\n    ...\n}\n```\n\n重复此操作，为除`MasterView`以外的所有视图设置背景颜色。每个视图中也记得`include ui.assets 1.0`。\n\n当您构建和运行应用时，您可能会想，为什么视图看起来和以前完全一样，而我们却经历了所有这些繁琐的工作。好吧，假设我们刚刚和市场部的人开了一个会，他们告诉我们黄橙色不再适合这个品牌了，我们需要改变所有的观点，变成干净的灰白色。我们以前必须进入每个视图，将颜色从`#f4c842`更改为`#efefef`。现在，它们只有七个，所以没什么大不了的，但是想象一下，如果我们必须改变 50 个复杂视图中所有组件的所有颜色；那将是一项非常痛苦的工作。\n\n但是，转到`Style.qml`将`colourBackground`属性从`#f4c842`更改为`#efefef`。构建并运行应用，享受我们更名应用的荣耀！通过尽早设置我们的共享样式组件，我们可以随时添加属性，然后在以后重新设计我们的应用变得更加容易。我们可以在这里添加所有类型的属性，而不仅仅是颜色，所以随着我们的进一步发展，我们将添加大小、字体和其他东西。\n\n# 字体惊人\n\n有了我们的样式框架，让我们回顾一下导航栏的外观，并找出我们想要实现的目标:\n\n![](img/5ff42490-1ab9-4573-88bc-ac1e8e9af10c.png)\n\n我们希望在导航栏上显示的按钮是仪表板视图(主页视图)、新客户端视图和查找客户端视图，以及顶部用于展开和折叠栏的切换按钮。\n\n一种常见的用户界面设计模式是用图标表示简单的命令。关于该命令的进一步信息可以通过多种方式获得；例如，当您将鼠标悬停在按钮上时，信息可以显示在屏幕底部的工具提示或状态栏中。我们的方法是有一个可折叠的酒吧。该栏的默认状态将被折叠，并将显示代表每个命令的图标。在展开状态下，该栏将显示命令的图标和文本描述。用户可以通过一个额外的按钮来切换状态。这是一种在移动应用开发中特别流行的模式，默认情况下，您希望消耗尽可能少的屏幕空间。\n\n有几个选项可以显示我们按钮的图标。旧的桌面应用很可能会使用某种描述的图像文件。这给了你对你的图标外观的完全艺术控制，但是有几个缺点。图像文件的大小相对较大，并且它们的大小是固定的。如果你需要以不同的尺寸绘制它们，那么它们可能看起来很糟糕，尤其是当它们被放大或者长宽比改变时。\n\n**可伸缩矢量图形** ( **SVG** )是小得多的文件，伸缩性非常好。它们更难创造，在艺术上可能更受限制，但它们对图标的目的非常有用。然而，从经验来看，在 Qt/QML 使用它们可能相当棘手。\n\n第三个选项是符号字体文件，它提供了 SVG 的小文件大小和可伸缩性优势，但更容易处理。这是 web 开发中非常常见的解决方案，这是我们将采取的方法。\n\n有许多可用的符号字体，但最受欢迎的开发可能是**字体真棒**。它提供了广泛的恐怖符号，并有一个非常有用的网站；退房:[http://fontawesome.io/](http://fontawesome.io/)。\n\nCheck any licensing applicable for fonts you choose to use, especially if you are using them commercially.\n\n下载工具包并打开存档文件。我们感兴趣的文件是`fonts` / `fontawesome-webfont.ttf`。将此文件复制到我们在`cm/cm-ui/assets`的项目文件夹中。\n\n在我们的`cm-ui`项目中，编辑`assets.qrc`并将字体添加到我们的资源中:\n\n```cpp\n<file alias=\"fontawesome.ttf\">img/fontawesome-webfont.ttf</file>\n```\n\n请记住，我们的别名不必与原始文件名相同，我们已经利用这个机会将其缩短了一点。\n\n接下来，编辑`Style.qml`，我们将把字体连接到我们的自定义样式，以便于使用。我们首先需要加载字体并使其可供使用，这是我们使用`FontLoader`组件实现的。在根**项**元素中添加以下内容:\n\n```cpp\nFontLoader {\n    id: fontAwesomeLoader\n    source: \"qrc:/img/fontawesome.ttf\"\n}    \n```\n\n在`source`属性中，我们使用在`assets.qrc`文件中定义的`/assets`前缀(或虚拟文件夹)以及`fontawesome.ttf`别名。现在，我们已经加载了字体，但是就目前的情况来看，我们无法从`Style.qml`之外引用它。这是因为只有根组件级别的属性可以在文件外部访问。子组件被认为实际上是私有的。我们解决这个问题的方法是为我们想要展示的元素创建一个`property alias`:\n\n```cpp\nItem {\n    property alias fontAwesome: fontAwesomeLoader.name\n\n    readonly property color colourBackground: \"#efefef\"\n\n    FontLoader {\n        id: fontAwesomeLoader\n        source: \"qrc:/img/fontawesome.ttf\"\n    }    \n}\n```\n\n这将创建一个名为`fontAwesome`的公共可用属性，当调用该属性时，只需将调用方重定向到内部`fontAwesomeLoader`元素的`name`属性。\n\n接线完成后，让我们找到想要使用的图标。回到字体真棒网站，导航到图标页面。在这里，你可以看到所有可用的图标。点击其中一个将显示更多关于它的信息，从这里我们可以获得显示它所需的关键信息，那就是 unicode 字符。我将为我们的菜单选择以下图标，但请随意选择您想要的图标:\n\n| **命令** | **图标** | **Unicode 字符** |\n| 切换菜单 | 酒吧 | f0c9 |\n| 仪表盘 | 家 | f015 |\n| 新客户端 | 用户-plus | f234 |\n| 查找客户 | 搜索 | f002 |\n\n现在，让我们用每个图标的`Text`组件替换`MasterView`上的`Button`组件:\n\n```cpp\nColumn {\n    Text {\n        font {\n            family: Style.fontAwesome\n            pixelSize: 42\n        }\n        color: \"#ffffff\"\n        text: \"\\uf0c9\"\n    }\n    Text {\n        font {\n            family: Style.fontAwesome\n            pixelSize: 42\n        }\n        color: \"#ffffff\"\n        text: \"\\uf015\"\n    }\n    Text {\n        font {\n            family: Style.fontAwesome\n            pixelSize: 42\n        }\n        color: \"#ffffff\"\n        text: \"\\uf234\"\n    }\n    Text {\n        font {\n            family: Style.fontAwesome\n            pixelSize: 42\n        }\n        color: \"#ffffff\"\n        text: \"\\uf002\"\n    }\n}\n```\n\n您还需要添加**资产 1.0** 导入，如果您还没有:\n\n![](img/0f8f95a5-e2a7-4511-986b-f772c7d125d3.png)\n\n接下来，我们将添加客户端命令的描述性文本。将每个`Text`组件包装在一个`Row`中，并添加另一个`Text`组件进行描述，如下所示:\n\n```cpp\nRow {\n    Text {\n        font {\n            family: Style.fontAwesome\n            pixelSize: 42\n        }\n        color: \"#ffffff\"\n        text: \"\\uf234\"\n    }\n    Text {\n        color: \"#ffffff\"\n        text: \"New Client\"\n    }\n}\n```\n\n`Row`组件将水平布局其子组件——首先是图标，然后是描述性文本。对其他命令重复此操作。为其他按钮添加描述仪表板和查找客户端，并为切换命令添加一个空字符串:\n\n![](img/877075bc-0bc9-4813-8ba1-dd98326e65dd.png)\n\n在我们过于得意忘形地进行进一步的更改之前，我们将喘口气，做一些重构，并考虑引入组件。\n\n# 成分\n\n我们刚刚写的 QML 足够实用，但是已经变得难以维护了。我们的`MasterView`变得有点长，有点难读。当我们改变命令按钮的外观时，例如，对齐图标和文本，我们将不得不在四个地方改变它。如果我们想添加第五个按钮，我们必须复制、粘贴和编辑一大堆 QML 才能这样做。这就是可重用组件发挥作用的地方。\n\n组件与我们已经创建的视图完全相同，只是 QML 的片段。区别纯粹在于语义。在这本书里，视图代表布局内容的屏幕，而组件就是内容。\n\n创建一个新组件最简单的方法是，当你已经写好了 QML，你想形成你的组件的基础，我们已经这样做了。右键单击我们为命令添加的任何`Row`元素，并选择**重构>将组件移动到单独的文件中**。\n\n命名新组件`NavigationButton`并将其保存到新文件夹— `cm/cm-ui/components`:\n\n![](img/4b37e410-961d-4855-b235-5b6102bea245.png)\n\n行元素将被移动到我们的新文件中，在`MasterView`中，您将剩下一个空的`NavigationButton`组件:\n\n```cpp\nNavigationButton {\n}\n```\n\n不幸的是，它带有一个大的红色曲线，我们的应用将不再运行。虽然重构步骤愉快地为我们创建了一个新的`NavigationButton.qml`文件，但它实际上并没有包含在我们的项目中，所以 Qt 不知道它在哪里。不过，这很容易解决，我们只需要像处理视图和资产一样设置我们的资源包:\n\n1.  在`cm/cm-ui`文件夹中创建新的`Qt Resource File`名为`components.qrc`\n2.  在`cm/cm-ui/components`中创建一个空的`qmldir`文件，就像我们对资产所做的那样\n3.  编辑`components.qrc`将我们的两个新文件包含在`/components`前缀中:\n\n```cpp\n<RCC>\n    <qresource prefix=\"/components\">\n        <file alias=\"qmldir\">components/qmldir</file>\n        <file   \n alias=\"NavigationButton.qml\">components/NavigationButton.qml</file>\n    </qresource>\n</RCC>\n```\n\n4.  编辑`qmldir`以设置我们的模块，并向其中添加我们的`NavigationButton`组件:\n\n```cpp\nmodule components\nNavigationButton 1.0 NavigationButton.qml\n```\n\n5.  确保`cm-ui.pro`中的`components.qrc`已被添加到`RESOURCES`变量中\n6.  在`MasterView`中，包括我们的新组件模块，以访问我们的新组件:\n\n```cpp\nimport components 1.0\n```\n\nSometimes, getting our module to be fully recognized and banishing the red squigglies may only be accomplished by restarting Qt Creator, as that forces the reload of all the QML modules.\n\n我们现在有了一个可重用的组件，它隐藏了实现细节，减少了代码重复，并使添加新命令和维护旧命令变得更加容易。然而，在我们可以将它用于其他命令之前，我们需要对它进行一些更改。\n\n目前，我们的`NavigationButton`有硬编码的图标和描述文本值，每当我们使用该组件时，这些值都是相同的。我们需要公开这两个文本属性，以便为每个命令设置不同的属性。正如我们所看到的，我们可以使用属性别名来实现这一点，但是我们需要向我们的`Text`元素添加唯一的标识符来实现这一点。让我们将默认值设置为通用值，并实现本书前面的建议，将`Item`组件作为根元素:\n\n```cpp\nimport QtQuick 2.9\nimport assets 1.0\n\nItem {\n    property alias iconCharacter: textIcon.text\n    property alias description: textDescription.text\n\n    Row {\n        Text {\n            id: textIcon\n            font {\n                family: Style.fontAwesome\n                pixelSize: 42\n            }\n            color: \"#ffffff\"\n            text: \"\\uf11a\"\n        }\n        Text {\n            id: textDescription\n            color: \"#ffffff\"\n            text: \"SET ME!!\"\n        }\n    }\n}\n```\n\n现在我们的组件可以用属性进行配置，我们可以在`MasterView`中替换我们的命令:\n\n```cpp\nColumn {\n    NavigationButton {\n        iconCharacter: \"\\uf0c9\"\n        description: \"\"\n    }\n    NavigationButton {\n        iconCharacter: \"\\uf015\"\n        description: \"Dashboard\"\n    }\n    NavigationButton {\n        iconCharacter: \"\\uf234\"\n        description: \"New Client\"\n    }\n    NavigationButton {\n        iconCharacter: \"\\uf002\"\n        description: \"Find Client\"\n    }\n}\n```\n\n这比我们之前复制的所有 QML 都要简洁和易于管理。现在，如果您运行该应用，您会看到，虽然我们向前迈出了几步，但我们也后退了一步:\n\n![](img/2cc949e0-1300-4208-b473-ab860a09c420.png)\n\n正如您所看到的，我们所有的组件都绘制在彼此之上。这种情况的根本原因是我们之前提到的关于规模的问题。我们有一个带有根`Item`元素的视觉组件，我们还没有明确定义它的大小。我们忽略的另一件事是我们的定制样式。接下来让我们修理那些。\n\n# 设计导航栏的样式\n\n从简单的部分开始，我们先把硬编码的颜色和图标像素大小从`NavigationButton`移到`Style.qml`:\n\n```cpp\nreadonly property color colourNavigationBarBackground: \"#000000\"\nreadonly property color colourNavigationBarFont: \"#ffffff\"\nreadonly property int pixelSizeNavigationBarIcon: 42\n```\n\n我们现在需要考虑如何确定按钮元素的大小。我们有一个图标，我们想成为正方形，所以宽度和高度将是相同的。接下来，我们有一个与图标高度相同但更宽的文本描述:\n\n![](img/1a0d5ede-e554-4bff-8009-5485a5757630.png)\n\n整个组件的宽度是图标的宽度加上描述的宽度。整个组件的高度与图标和描述的高度相同；然而，它给了我们更大的灵活性，使高度与两者中较大者相同。这样，如果我们决定让一个项目比另一个项目大，我们就知道这个组件足够大，可以包含两个项目。让我们为图标选择 80 x 80 的起始尺寸，为描述选择 80 x 240 的起始尺寸，并定义属性:\n\n```cpp\nreadonly property real widthNavigationButtonIcon: 80\nreadonly property real heightNavigationButtonIcon: widthNavigationButtonIcon\nreadonly property real widthNavigationButtonDescription: 240\nreadonly property real heightNavigationButtonDescription: heightNavigationButtonIcon\nreadonly property real widthNavigationButton: widthNavigationButtonIcon + widthNavigationButtonDescription\nreadonly property real heightNavigationButton: Math.max(heightNavigationButtonIcon, heightNavigationButtonDescription)\n```\n\n这里有几件事需要注意。属性可以直接绑定到其他属性，这减少了重复的数量，并使整个设置更加动态。我们知道我们希望我们的图标是正方形的，所以通过将高度绑定为与宽度相同，如果我们想要更改图标的总大小，我们只需要更新宽度，高度就会自动更新。QML 还与 JavaScript 引擎有很强的集成，所以我们可以使用`Math.max()`功能来帮助我们找出哪个高度更大。\n\n我们希望导航按钮做的另一件事是当用户将鼠标悬停在按钮上时提供某种视觉提示，以指示它是一个交互元素。为此，我们需要每个按钮都有自己的背景矩形。\n\n在`NavigationButton`中，用新的`Rectangle`包裹`Row`元素，并将尺寸插入我们的组件:\n\n```cpp\nItem {\n    property alias iconCharacter: textIcon.text\n    property alias description: textDescription.text\n\n    width: Style.widthNavigationButton\n    height: Style.heightNavigationButton\n\n    Rectangle {\n        id: background\n        anchors.fill: parent\n        color: Style.colourNavigationBarBackground\n\n        Row {\n            Text {\n                id: textIcon\n                width: Style.widthNavigationButtonIcon\n                height: Style.heightNavigationButtonIcon\n                font {\n                    family: Style.fontAwesome\n                    pixelSize: Style.pixelSizeNavigationBarIcon\n                }\n                color: Style.colourNavigationBarFont\n                text: \"\\uf11a\"\n            }\n            Text {\n                id: textDescription\n                width: Style.widthNavigationButtonDescription\n                height: Style.heightNavigationButtonDescription\n                color: Style.colourNavigationBarFont\n                text: \"SET ME!!\"\n            }\n        }\n    }\n}\n```\n\n再跑一次，你会看到轻微的改善:\n\n![](img/4ea8081d-d28b-40e7-9335-9a31e0c02f7c.png)\n\n我们的部分描述被删除了，因为我们的导航栏被硬编码为 100 像素宽。我们需要改变这一点，并实现切换展开/折叠功能。我们已经计算出了我们需要的尺寸，所以让我们通过向`Style.qml`添加几个新属性来做准备:\n\n```cpp\nreadonly property real widthNavigationBarCollapsed: widthNavigationButtonIcon\nreadonly property real heightNavigationBarExpanded: widthNavigationButton\n```\n\n折叠状态对于图标来说刚好足够宽，而展开状态将包含整个按钮，包括描述。\n\n接下来，让我们将导航栏封装在一个新组件中。在这种情况下不会有任何重用的好处，因为只会有一个，但它有助于保持我们的 QML 有条理，并使`MasterView`更加简洁和易于阅读。\n\n您可以右键单击`MasterView`中的`Rectangle`组件，并将我们的导航栏重构为新的 QML 文件，就像我们对我们的`NavigationButton`所做的那样。但是，让我们手动完成它，这样您就可以适应这两种方法。右键单击`components.qrc`并选择添加新… > Qt > QML 文件。在`cm/cm-ui/components`中增加`NavigationBar.qml`:\n\n![](img/0c4e1ac6-4818-49d8-8118-07d9ecf603b6.png)\n\n编辑`components.qrc`并将我们的新`NavigationBar`移动到带有别名的`/components`前缀部分:\n\n```cpp\n<file alias=\"NavigationBar.qml\">components/NavigationBar.qml</file>\n```\n\n通过编辑`qmldir`将组件添加到我们的组件模块中:\n\n```cpp\nNavigationBar 1.0 NavigationBar.qml\n```\n\n从`MasterView`中剪切`Rectangle`及其子元素，粘贴到根`Item`元素内的`NavigationBar.qml`中。如果`QtQuick`模块导入已经初始化为旧版本，请将其更新为 2.9 版本。为我们的资产模块添加一个导入来访问我们的样式对象。将矩形的`anchors`和`width`属性移动到根`Item`，并将`Rectangle`设置为填充其父级:\n\n```cpp\nimport QtQuick 2.9\nimport assets 1.0\n\nItem {\n    anchors {\n        top: parent.top\n        bottom: parent.bottom\n        left: parent.left\n    }\n    width: 100\n\n    Rectangle {\n        anchors.fill: parent\n        color: \"#000000\"\n\n        Column {\n            NavigationButton {\n                iconCharacter: \"\\uf0c9\"\n                description: \"\"\n            }\n            NavigationButton {\n                iconCharacter: \"\\uf015\"\n                description: \"Dashboard\"\n            }\n            NavigationButton {\n                iconCharacter: \"\\uf234\"\n                description: \"New Client\"\n            }\n            NavigationButton {\n                iconCharacter: \"\\uf002\"\n                description: \"Find Client\"\n            }\n        }\n    }\n}\n```\n\n回到`MasterView`，现在可以在`Rectangle`原来的位置添加新的`NavigationBar`组件:\n\n```cpp\nNavigationBar {\n    id: navigationBar\n}\n```\n\n尽管您再次看到可怕的红色曲线，但您实际上能够运行应用并验证重构没有破坏任何东西。\n\n我们新的`NavigationBar`组件的锚定没问题，但是`width`有点复杂——我们如何知道它应该是`Style.widthNavigationBarCollapsed`还是`Style.heightNavigationBarExpanded`？我们将使用一个公共可访问的布尔属性来控制这一点，该属性指示栏是否折叠。然后，我们可以使用条件`?`运算符语法，使用该属性值来决定我们想要的宽度。最初将属性设置为 true，这样默认情况下，该栏将以折叠状态呈现:\n\n```cpp\nproperty bool isCollapsed: true\n```\n\n在此基础上，将硬编码`width`替换为 100，如下所示:\n\n```cpp\nwidth: isCollapsed ? Style.widthNavigationBarCollapsed : Style.heightNavigationBarExpanded\n```\n\n接下来，将`Rectangle`的`color`属性更新为`Style.colourNavigationBarBackground`:\n\n![](img/d9041acb-28ab-4c99-99bb-b7c7b247be84.png)\n\n我们现在正在到达那里，但是一路上我们错过的一件关键的事情是，现在点击按钮实际上不再做任何事情。接下来我们来解决这个问题。\n\n# 微小静电干扰声\n\n在这本书的早期，我们研究了一个叫做`MouseArea`的组件。这很快被我们使用的`Button`组件所取代，它为我们提供了点击功能。然而，现在我们正在滚动我们自己的按钮形式，我们需要自己实现点击功能。就像`Button`组件一样，我们的`NavigationButton`在被点击时不应该做任何事情，除了通知他们的父母事件已经发生。组件应该尽可能通用，并且不了解上下文，以便您可以在多个地方使用它们。我们需要做的是添加一个`MouseArea`组件，并通过自定义信号传递`onClicked`事件。\n\n在`NavigationButton`中，我们首先添加每当组件被点击时我们想要发出的信号。在属性后添加以下内容:\n\n```cpp\nsignal navigationButtonClicked()\n```\n\nTry and give the signals quite specific names, even if they are a little long. If you simply call everything `clicked()`, then things can get a little confusing and sometimes you may find yourself referencing a different signal to the one you intended.\n\n接下来，我们将添加另一个属性来支持我们将要实现的鼠标悬停魔法。这将是一个`color`类型，我们将默认为常规背景颜色:\n\n```cpp\nproperty color hoverColour: Style.colourNavigationBarBackground\n```\n\n我们将结合`Rectangle`的`states`属性使用此颜色:\n\n```cpp\nstates: [\n    State {\n        name: \"hover\"\n        PropertyChanges {\n            target: background\n            color: hoverColour\n        }\n    }\n]\n```\n\n将阵列中的每个状态视为一个命名配置。默认配置没有名称(\"\")，由我们已经在`Rectangle`元素中设置的属性组成。“悬停”状态将更改应用于`PropertyChanges`元素中指定的属性，也就是说，它会将 ID 为`background`的元素的`color`属性更改为`hoverColour`的任何值。\n\n接下来，在`Rectangle`里面但是在`Row`下面，添加我们的`MouseArea`:\n\n```cpp\nMouseArea {\n    anchors.fill: parent\n    cursorShape: Qt.PointingHandCursor\n    hoverEnabled: true\n    onEntered: background.state = \"hover\"\n    onExited: background.state = \"\"\n    onClicked: navigationButtonClicked()\n}\n```\n\n我们使用`anchors`属性填充整个按钮背景区域，包括图标和描述。接下来，我们将通过在鼠标光标进入按钮区域时将鼠标光标变为一只指向的手，并使用`hoverEnabled`标志启用悬停来使事情变得更有趣一些。启用时，当光标进出该区域时，会发出**进入**和**退出**的信号，我们可以通过在刚刚实现的悬停状态和默认(\"\")之间切换，使用相应的槽来改变我们的背景`Rectangle`的外观。最后，我们用`onClicked()`槽响应`MouseArea`的`clicked()`信号，简单的发出自己的信号。\n\n我们现在可以对我们的`NavigationBar`组件中的`navigationButtonClicked()`信号做出反应，并添加一些悬停颜色。首先实现切换按钮:\n\n```cpp\nNavigationButton {\n    iconCharacter: \"\\uf0c9\"\n    description: \"\"\n    hoverColour: \"#993333\"\n    onNavigationButtonClicked: isCollapsed = !isCollapsed\n}\n```\n\n我们执行`<MyCapitalisedSignalName>`约定为我们的信号创建一个时隙，当它触发时，我们只需在`true`和`false`之间切换`isCollapsed`的值。\n\n现在可以运行应用了。单击切换按钮展开和折叠导航栏:\n\n![](img/180bd707-01e0-487d-a7c1-3e63e5c47372.png)\n\n请注意，由于我们使用`anchors`，子视图如何动态调整自身大小以适应导航栏。当您将鼠标悬停在按钮上时，您还会看到指针光标和闪光的颜色，这有助于用户理解它是一个交互元素，并使边界可视化。\n\n对于剩下的导航按钮，我们想要对点击的事件做出的反应是在`NavigationCoordinator`上发出`goDashboardView()`、`goCreateClientView()`和`goFindClientView()`信号。\n\n将`onNavigationButtonClicked`插槽添加到其他按钮，并向下钻取`masterController`对象，以获得我们想要调用的信号。也添加一些您选择的花哨颜色:\n\n```cpp\nNavigationButton {\n    iconCharacter: \"\\uf015\"\n    description: \"Dashboard\"\n    hoverColour: \"#dc8a00\"\n    onNavigationButtonClicked: masterController.ui_navigationController.goDashboardView();\n}\nNavigationButton {\n    iconCharacter: \"\\uf234\"\n    description: \"New Client\"\n    hoverColour: \"#dccd00\"\n    onNavigationButtonClicked: masterController.ui_navigationController.goCreateClientView();\n}\nNavigationButton {\n    iconCharacter: \"\\uf002\"\n    description: \"Find Client\"\n    hoverColour: \"#8aef63\"\n    onNavigationButtonClicked: masterController.ui_navigationController.goFindClientView();\n}\n```\n\n现在，您可以单击按钮导航到不同的子视图。\n\n完成导航栏的最后几个小调整是为了更好地对齐按钮的内容，并调整一些东西的大小。\n\n描述文本应该与图标的中心垂直对齐，而不是顶部，我们的图标应该居中，而不是靠在窗口的边缘。第一个问题很容易解决，因为我们已经对尺寸进行了一致和明确的调整。只需将以下属性添加到`NavigationButton`中的两个`Text`组件中:\n\n```cpp\nverticalAlignment: Text.AlignVCenter\n```\n\n这两个`Text`元素的大小都是为了占据按钮的整个高度，所以我们只需要在这个空间内垂直对齐文本。\n\n固定图标的对齐方式是一样的，但是这次是在水平轴上。将以下内容添加到图标的`Text`部分:\n\n```cpp\nhorizontalAlignment: Text.AlignHCenter\n```\n\n至于尺寸，我们的描述文本有点小，文本后面有很多空白。给我们的`Style`对象添加一个新属性:\n\n```cpp\nreadonly property int pixelSizeNavigationBarText: 22\n```\n\n使用描述`Text`元素中的新属性:\n\n```cpp\nfont.pixelSize: Style.pixelSizeNavigationBarText\n```\n\n接下来，将`Style`中的`widthNavigationButtonDescription`属性降低到 160。\n\n运行应用，我们就快到了。现在，尺寸和对齐更好了:\n\n![](img/d011ffc4-fc00-4900-9347-f7f6ae385647.png)\n\n但是，有一点你可能没有注意到，当栏被折叠，只显示图标时，`MouseArea`仍然是包含描述的按钮的全宽。尝试将鼠标移动到描述所在的位置，您可以看到指针光标出现。你甚至可以点击组件，然后转换发生。我们需要做的是修复这个问题，而不是`NavigationButton`中的根`Item`元素是一个固定宽度(`Style.widthNavigationButton`，我们需要使其动态化，并将其设置为`parent.width`。为了做到这一点，我们需要沿着 QML 层级向上走，并确保它的父级也有宽度。它的父元素是`NavigationBar`中的`Column`元素。将`Column`的`width`属性也设置为`parent.width`。\n\n有了这些更改，导航栏现在的行为与预期的一样。\n\n# 命令\n\n我们要做的下一件事是实现一个上下文相关的命令栏。尽管无论用户在做什么，我们的导航栏都是一个具有相同按钮的持续存在，但是命令栏会来来去去，并且会根据上下文包含不同的按钮。例如，如果用户正在添加或编辑客户端，我们将需要一个保存按钮来提交对数据库的任何更改。但是，如果我们正在搜索一个客户端，那么保存没有意义，一个“查找”按钮更相关。虽然创建命令栏的技术与导航栏大体相似，但所需的额外灵活性带来了更多挑战。\n\n为了帮助我们克服这些障碍，我们将执行命令。这种方法的另一个好处是，我们可以将逻辑从用户界面层移到业务逻辑层。我喜欢用户界面尽可能的简单和通用。这使得您的应用更加灵活，C++ 代码中的错误比 QML 代码中的错误更容易识别和解决。\n\n一个命令对象将封装一个图标、描述性文本、一个确定按钮是否被启用的功能，以及最后一个`executed()`信号，该信号将在相关按钮被按下时发出。命令栏中的每个按钮都将绑定到一个命令对象。\n\n我们的每个子视图都可能有一个命令列表和一个关联的命令栏。对于这样做的视图，我们将通过命令控制器向用户界面呈现命令列表。\n\n在`cm-lib`项目中创建两个新的`C++ `类，这两个类都应该从 QObject 继承:\n\n*   **在新文件夹`cm-lib/source/framework`中命令**\n*   **现有文件夹`cm-lib/source/controllers`中的命令控制器**\n\n`command.h`:\n\n```cpp\n#ifndef COMMAND_H\n#define COMMAND_H\n\n#include <functional>\n\n#include <QObject>\n#include <QScopedPointer>\n#include <QString>\n\n#include <cm-lib_global.h>\n\nnamespace cm {\nnamespace framework {\n\nclass CMLIBSHARED_EXPORT Command : public QObject\n{\n    Q_OBJECT\n    Q_PROPERTY( QString ui_iconCharacter READ iconCharacter CONSTANT )\n    Q_PROPERTY( QString ui_description READ description CONSTANT )\n    Q_PROPERTY( bool ui_canExecute READ canExecute NOTIFY canExecuteChanged )\n\npublic:\n    explicit Command(QObject* parent = nullptr,\n                     const QString& iconCharacter = \"\",\n                     const QString& description = \"\",\n                     std::function<bool()> canExecute = [](){ return \n                                                           true; });\n    ~Command();\n\n    const QString& iconCharacter() const;\n    const QString& description() const;\n    bool canExecute() const;\n\nsignals:\n    void canExecuteChanged();\n    void executed();\n\nprivate:\n    class Implementation;\n    QScopedPointer<Implementation> implementation;\n};\n\n}}\n\n#endif\n```\n\n`command.cpp`:\n\n```cpp\n#include \"command.h\"\n\nnamespace cm {\nnamespace framework {\n\nclass Command::Implementation\n{\npublic:\n    Implementation(const QString& _iconCharacter, const QString& \n     _description, std::function<bool()> _canExecute)\n        : iconCharacter(_iconCharacter)\n        , description(_description)\n        , canExecute(_canExecute)\n    {\n    }\n\n    QString iconCharacter;\n    QString description;\n    std::function<bool()> canExecute;\n};\n\nCommand::Command(QObject* parent, const QString& iconCharacter, const QString& description, std::function<bool()> canExecute)\n    : QObject(parent)\n{\n    implementation.reset(new Implementation(iconCharacter, description, canExecute));\n}\n\nCommand::~Command()\n{\n}\n\nconst QString& Command::iconCharacter() const\n{\n    return implementation->iconCharacter;\n}\n\nconst QString& Command::description() const\n{\n    return implementation->description;\n}\n\nbool Command::canExecute() const\n{\n    return implementation->canExecute();\n}\n\n}\n}\n```\n\nQObject、名称空间和 dll 导出代码现在应该很熟悉了。我们将希望在用户界面按钮上显示的图标字符和描述值表示为字符串。我们将成员变量隐藏在私有实现中，并为它们提供`accessor`方法。我们可以将`canExecute`成员表示为一个简单的`bool`成员，调用代码可以根据需要设置为`true`或`false`；然而，一个更优雅的解决方案是传入一个方法，在运行中为我们计算价值。默认情况下，我们将其设置为返回`true`的λ，这意味着按钮将被启用。我们提供了一个`canExecuteChanged()`信号来配合它，只要我们想让用户界面重新评估按钮是否被启用，我们就可以触发这个信号。最后一个元素是`executed()`信号，当按下相应的按钮时，该信号将由用户界面触发。\n\n`command-controller.h`:\n\n```cpp\n#ifndef COMMANDCONTROLLER_H\n#define COMMANDCONTROLLER_H\n\n#include <QObject>\n#include <QtQml/QQmlListProperty>\n#include <cm-lib_global.h>\n#include <framework/command.h>\n\nnamespace cm {\nnamespace controllers {\n\nclass CMLIBSHARED_EXPORT CommandController : public QObject\n{\n    Q_OBJECT\n    Q_PROPERTY(QQmlListProperty<cm::framework::Command> \n     ui_createClientViewContextCommands READ  \n     ui_createClientViewContextCommands CONSTANT)\n\npublic:\n    explicit CommandController(QObject* _parent = nullptr);\n    ~CommandController();\n\n    QQmlListProperty<framework::Command> \n    ui_createClientViewContextCommands();\n\npublic slots:\n    void onCreateClientSaveExecuted();\n\nprivate:\n    class Implementation;\n    QScopedPointer<Implementation> implementation;\n};\n\n}}\n\n#endif\n```\n\n`command-controller.cpp`:\n\n```cpp\n#include \"command-controller.h\"\n\n#include <QList>\n#include <QDebug>\n\nusing namespace cm::framework;\n\nnamespace cm {\nnamespace controllers {\n\nclass CommandController::Implementation\n{\npublic:\n    Implementation(CommandController* _commandController)\n        : commandController(_commandController)\n    {\n        Command* createClientSaveCommand = new Command( \n          commandController, QChar( 0xf0c7 ), \"Save\" );\n        QObject::connect( createClientSaveCommand, &Command::executed,   \n   commandController, &CommandController::onCreateClientSaveExecuted );\n        createClientViewContextCommands.append( createClientSaveCommand );\n    }\n\n    CommandController* commandController{nullptr};\n\n    QList<Command*> createClientViewContextCommands{};\n};\n\nCommandController::CommandController(QObject* parent)\n    : QObject(parent)\n{\n    implementation.reset(new Implementation(this));\n}\n\nCommandController::~CommandController()\n{\n}\n\nQQmlListProperty<Command> CommandController::ui_createClientViewContextCommands()\n{\n    return QQmlListProperty<Command>(this, implementation->createClientViewContextCommands);\n}\n\nvoid CommandController::onCreateClientSaveExecuted()\n{\n    qDebug() << \"You executed the Save command!\";\n}\n\n}}\n```\n\n这里，我们介绍一种新的类型——`QQmlListProperty`。它本质上是一个包装器，使 QML 能够与自定义对象的列表进行交互。请记住，我们需要完全限定`Q_PROPERTY`语句中的模板化类型。实际保存数据的私有成员是一个 QList，我们已经实现了一个`accessor`方法，该方法获取 QList 并将其转换为相同模板类型的`QQmlListProperty`。\n\nAs per the documentation for `QQmlListProperty`, this method of object construction should not be used in production code, but we’ll use it to keep things simple.\n\n我们已经为我们的`CreateClientView`创建了一个单一的命令列表。稍后我们将为其他视图添加命令列表。再说一遍，我们现在将保持事情简单；我们只需创建一个命令来保存新创建的客户端。当创建命令时，我们将其父级给命令协调器，这样我们就不必担心内存管理。我们给它分配一个软盘图标(unicode f0c7)和保存标签。我们暂时将`canExecute`功能保留为默认值，因此它将始终处于启用状态。接下来，我们将命令的`executed()`信号连接到`CommandController`的`onCreateClientSaveExecuted()`插槽。接线完成后，我们将命令添加到列表中。\n\n目的是我们向用户呈现一个绑定到`Command`对象的命令按钮。当用户按下按钮时，我们会从用户界面发出`executed()`信号。我们建立的连接将导致调用命令控制器上的插槽，我们将执行我们的业务逻辑。现在，我们只需在按下按钮时向控制台打印一行。\n\n接下来，让我们在`main.cpp`中注册我们的两个新类型(记住`#includes`):\n\n```cpp\nqmlRegisterType<cm::controllers::CommandController>(\"CM\", 1, 0, \"CommandController\");\nqmlRegisterType<cm::framework::Command>(\"CM\", 1, 0, \"Command\");\n```\n\n最后，我们需要将`CommandCoordinator`属性添加到`MasterController`中:\n\n```cpp\nQ_PROPERTY( cm::controllers::CommandController* ui_commandController READ commandController CONSTANT )\n```\n\n然后，我们添加一个`accessor`方法:\n\n```cpp\nCommandController* commandController();\n```\n\n最后，在`master-controller.cpp`中，实例化私有实现中的对象，并以与我们为`NavigationController`所做的完全相同的方式实现`accessor`方法。\n\n我们现在有一个(非常短！)准备好供我们`CreateClientView`消耗的命令列表。\n\n# 指令杠\n\n让我们从为命令组件向样式添加更多属性开始:\n\n```cpp\nreadonly property color colourCommandBarBackground: \"#cecece\"\nreadonly property color colourCommandBarFont: \"#131313\"\nreadonly property color colourCommandBarFontDisabled: \"#636363\"\nreadonly property real heightCommandBar: heightCommandButton\nreadonly property int pixelSizeCommandBarIcon: 32\nreadonly property int pixelSizeCommandBarText: 12\n\nreadonly property real widthCommandButton: 80\nreadonly property real heightCommandButton: widthCommandButton\n```\n\n接下来，在我们的用户界面项目中创建两个新的 QML 组件:`CommandBar.qml`和`cm-ui/components`中的`CommandButton.qml`。更新`components.qrc`并将新组件移入带有别名的`/components`前缀。编辑`qmldir`并添加新组件:\n\n```cpp\nCommandBar 1.0 CommandBar.qml\nCommandButton 1.0 CommandButton.qml\n```\n\n对于我们的按钮设计，我们希望在图标下方布局描述。图标应该位于略高于中心的位置。组件应该是方形的，如下所示:\n\n![](img/1bf95587-1026-463f-91b3-cfb511bdf1c2.png)\n\n`CommandButton.qml`:\n\n```cpp\nimport QtQuick 2.9\nimport CM 1.0\nimport assets 1.0\n\nItem {\n    property Command command\n    width: Style.widthCommandButton\n    height: Style.heightCommandButton\n\n    Rectangle {\n        id: background\n        anchors.fill: parent\n        color: Style.colourCommandBarBackground\n\n        Text {\n            id: textIcon\n            anchors {\n                centerIn: parent\n                verticalCenterOffset: -10\n            }\n            font {\n                family: Style.fontAwesome\n                pixelSize: Style.pixelSizeCommandBarIcon\n            }\n            color: command.ui_canExecute ? Style.colourCommandBarFont : \n                                          colourCommandBarFontDisabled\n            text: command.ui_iconCharacter\n            horizontalAlignment: Text.AlignHCenter\n        }\n\n        Text {\n            id: textDescription\n            anchors {\n                top: textIcon.bottom\n                bottom: parent.bottom\n                left: parent.left\n                right: parent.right\n            }\n            font.pixelSize: Style.pixelSizeNavigationBarText\n            color: command.ui_canExecute ? Style.colourCommandBarFont : \n                                          colourCommandBarFontDisabled\n            text: command.ui_description\n            horizontalAlignment: Text.AlignHCenter\n            verticalAlignment: Text.AlignVCenter\n        }\n\n        MouseArea {\n            anchors.fill: parent\n            cursorShape: Qt.PointingHandCursor\n            hoverEnabled: true\n            onEntered: background.state = \"hover\"\n            onExited: background.state = \"\"\n            onClicked: if(command.ui_canExecute) {\n                           command.executed();\n                       }\n        }\n\n        states: [\n            State {\n                name: \"hover\"\n                PropertyChanges {\n                    target: background\n                    color: Qt.darker(Style.colourCommandBarBackground)\n                }\n            }\n        ]\n    }\n}\n```\n\n这在很大程度上类似于我们的`NavigationButton`组件。我们传入一个`Command`对象，在这里我们将获得要在**文本**元素中显示的图标字符和描述，以及按下按钮时要发出的信号，只要命令可以执行。\n\n我们使用一个替代的**行/列**为基础的布局，并使用锚来定位我们的图标和描述。我们将图标放在父级`Rectangle`的中心，然后应用垂直偏移将其向上移动，并为描述留出空间。我们将描述的顶部固定在图标的底部。\n\n我们不是在按下按钮时传播信号，而是发出`Command`对象的`executed()`信号，首先验证命令是否可以执行。我们还使用这个标志来有选择地为我们的文本元素着色，如果命令被禁用，则使用浅灰色字体。\n\n我们用我们的`MouseArea`实现了一些更多的悬停功能，但是我们没有暴露一个属性来传递悬停颜色，而是简单地采用默认值，并使用内置的`Qt.darker()`方法将其变暗几个阴影。如果命令可以执行，我们也只在`MouseArea`的`onEntered()`槽中应用状态改变。\n\n`CommandBar.qml`:\n\n```cpp\nimport QtQuick 2.9\nimport assets 1.0\n\nItem {\n    property alias commandList: commandRepeater.model\n\n    anchors {\n        left: parent.left\n        bottom: parent.bottom\n        right: parent.right\n    }\n    height: Style.heightCommandBar\n\n    Rectangle {\n        anchors.fill: parent\n        color: Style.colourCommandBarBackground\n\n        Row {\n            anchors {\n                top: parent.top\n                bottom: parent.bottom\n                right: parent.right\n            }\n\n            Repeater {\n                id: commandRepeater\n                delegate: CommandButton {\n                    command: modelData\n                }\n            }\n        }\n    }\n}\n```\n\n同样，这在很大程度上与`NavigationBar`相同，但是有一个动态的命令列表，而不是硬编码的 QML 按钮。我们引入了另一个新组件-`Repeater`。通过`model`属性给定一个对象列表，`Repeater`将为列表中的每个项目实例化一个在`delegate`属性中定义的 QML 组件。列表中的对象通过内置的`modelData`变量可用。使用这种机制，我们可以为给定列表中的每个命令自动生成一个`CommandButton`元素。我们使用另一个属性别名，以便调用者可以设置命令列表。\n\n让我们在`CreateClientView`中使用这个。首先是`import components 1.0`，然后在根`Item`里面和`Rectangle`后面加上以下内容:\n\n```cpp\nCommandBar {\n    commandList: masterController.ui_commandController.ui_createClientViewContextCommands\n}\n```\n\n我们深入查看我们的属性层次结构，以获取 create client 视图的命令列表，并将该列表传递给命令栏，命令栏负责其余部分。不要担心`CommandBar`是否有红色的曲线，Qt Creator 只需要跟上我们飞速的步伐。\n\n运行应用并导航至创建客户端视图:\n\n![](img/093cca1f-f222-4039-a734-70488a159adc.png)\n\n点击按钮，您将看到消息输出到控制台。添加新命令就像将新的`Command`对象追加到`CommandController`内的 QList 一样简单——不需要修改 UI！命令栏会自动为列表中的每个命令创建一个新按钮。还要注意的是，这个命令栏只出现在`CreateClientView`上，所以它是上下文相关的。我们可以通过简单地向`CommandController`添加额外的列表和属性来轻松地向其他视图添加命令栏，我们将在后面介绍。\n\n# 摘要\n\n在这一章中，我们对导航栏进行了一次非常必要的检修。我们添加了最初的几个组件，并利用了新的自定义样式对象，Font Awesome 为我们提供了一些可爱的可伸缩图形。我们还引入了命令，并建立了框架，能够向视图中添加上下文相关的命令按钮。\n\n在[第 5 章](5.html)、*数据*中，我们将深入业务逻辑层，充实我们的第一个数据模型。"
  },
  {
    "path": "docs/learn-qt5/5.md",
    "content": "# 五、数据\n\n在本章中，我们将实现类来处理任何业务线应用中最关键的部分——数据。我们将介绍自感知数据实体，它可以自动序列化往返于 **JavaScript 对象符号** ( **JSON** )这是一种在网络通信中经常使用的流行序列化格式。我们将为我们的应用创建我们需要的核心模型，并将它们连接到我们的用户界面，以便通过自定义控件进行读写。我们将涵盖以下主题:\n\n*   JSON\n*   数据装饰者\n*   抽象数据实体\n*   数据实体的集合\n*   具体的数据模型\n*   用户界面控件和数据绑定\n\n# JSON\n\n如果你以前从未遇到过 JSON，让我们来一个快速速成课程。这是一种表达对象层次结构及其属性的简单而轻量的方法。在 HTTP 请求中发送数据时，这是一个非常流行的选择。它在意图上类似于 XML，但是不太冗长。\n\nJSON 对象被封装在大括号`{}`中，而属性用格式键表示:值。字符串用双引号`\"\"`分隔。我们可以如下表示单个客户端对象:\n\n```cpp\n{\n    \"reference\": \"CLIENT0001\",\n    \"name\": \"Dale Cooper\"\n}\n```\n\n请注意，空格和控制字符(如制表符和换行符)会被忽略，缩进属性只是为了使内容更易读。\n\nIt's usually a good idea to strip extraneous characters out of JSON when transmitting over the network (for example, in an HTTP request) in order to reduce the size of the payload; every byte counts!\n\n属性值可以是以下类型之一:`String`、`Number`、`JSON Object`、`JSON Array`，以及文字值`true`、`false`和`null`。\n\n我们可以将供应地址和账单地址作为子 JSON 对象添加到我们的客户端，为每个对象提供一个唯一的密钥。虽然密钥可以是任何格式，只要它们是唯一的，但通常的做法是使用 camel case，例如，`myAwesomeJsonKey`。我们可以用 null 表示一个空地址对象:\n\n```cpp\n{\n    \"reference\": \"CLIENT0001\",\n    \"name\": \"Dale Cooper\",\n    \"supplyAddress\": {\n         \"number\": 7,\n        \"name\": \"White Lodge\",\n        \"street\": \"Lost Highway\",\n        \"city\": \"Twin Peaks\",\n        \"postcode\": \"WS119\"\n    },\n    \"billingAddress\": null\n}\n```\n\n对象的集合(数组)用方括号`[]`括起来，用逗号隔开。我们可以简单地将方括号留空来表示没有预约:\n\n```cpp\n{\n    \"reference\": \"CLIENT0001\",\n    \"name\": \"Dale Cooper\",\n    \"supplyAddress\": {\n        \"number\": 7,\n        \"name\": \"White Lodge\",\n        \"street\": \"Lost Highway\",\n        \"city\": \"Twin Peaks\",\n        \"postcode\": \"WS119\"\n    },\n    \"billingAddress\": null,\n    \"contacts\": [\n        {\n            \"type\": 1,\n            \"address\": \"+12345678\"\n        },\n        {\n            \"type\": 2,\n            \"address\": \"dale.cooper@fbi.com\"\n        }\n    ],\n    \"appointments\": []\n}\n```\n\n# 对象层次结构\n\n大多数现实世界的应用以层次或关系的方式表示数据，数据被合理化为离散的对象。通常有一个中心“根”对象，它是其他几个子对象的父对象，或者作为单个对象，或者作为集合。每个离散对象都有自己的一组数据项，可以是任意数量的类型。我们想要涵盖的主要原则如下:\n\n*   一系列数据类型(`string`、`integer`、`datetime`)和一个枚举值\n*   对象层次结构\n*   同一类型的多个单一子实体\n*   实体集合\n\n在简单性与这些目标之间取得平衡，我们将努力实现的数据图表如下:\n\n![](img/ffa84470-9d21-4f42-93b5-1691a7bdabe0.png)\n\n下表描述了这些模型的用途:\n\n| **车型** | **描述** |\n| **客户端** | 这是我们对象层次结构的根，代表我们公司与之有关系的个人或团体，例如客户或患者。 |\n| **联系** | 我们可以用来联系客户的地址集合。可能的联系方式有电话、电子邮件和传真。每个客户端可能有一个或多个联系人。 |\n| **预约** | 与客户的预约集合，例如，现场参观或咨询。每个客户可能有零个或多个约会。 |\n| **供应地址** | 与客户关系的核心地址，例如，我们公司为患者提供能源的地点或家庭地址。每个客户端必须有一个供应地址。 |\n| **计费地址** | 一个可选地址，不同于用于开票的供应地址，例如，公司的总部。每个客户端可能有零个或一个账单地址。 |\n\nAnother perfectly valid approach would be to aggregate the addresses into a collection, much like we have done with our contacts, but I want to demonstrate using the same type of object (Address) in multiple properties.\n\n有了高级设计，我们现在可以编写我们的类了。然而，在我们开始我们的数据实体之前，让我们看一下数据项。\n\n# 数据装饰器\n\n我们的客户端模型的`name`属性的一个简单实现是将其添加为`QString`；然而，这种方法有一些缺点。每当我们在用户界面中显示这个属性时，我们可能会希望在文本框旁边显示一个信息标签，以便用户知道它是干什么的，比如说“名称”或类似的东西。每当我们想要验证用户输入的名称时，我们必须在其他地方的代码中管理它。最后，如果我们想将值序列化到 JSON 或从 JSON 序列化，同样需要一些其他组件来为我们实现。\n\n为了解决所有这些问题，我们将引入`DataDecorator`的概念，它将提升给定的基础数据类型，并为我们提供标签、验证功能和开箱即用的 JSON 序列化。我们的模型将维护`DataDecorators`的集合，允许它们通过简单地遍历数据项并执行相关的动作来验证自己并将其序列化为 JSON。\n\n在我们的`cm-lib`项目中，在一个新文件夹`cm-lib/source/data`中创建以下类:\n\n| **级** | **目的** |\n| `DataDecorator` | 数据项的基类 |\n| `StringDecorator` | 字符串属性的派生类 |\n| `IntDecorator` | 整数属性的派生类 |\n| `DateTimeDecorator` | 日期/时间属性的派生类 |\n| `EnumeratorDecorator` | 枚举属性的派生类 |\n\n我们的`DataDecorator`基类将包含所有数据项共享的特性。\n\n`data-decorator.h`:\n\n```cpp\n#ifndef DATADECORATOR_H\n#define DATADECORATOR_H\n\n#include <QJsonObject>\n#include <QJsonValue>\n#include <QObject>\n#include <QScopedPointer>\n\n#include <cm-lib_global.h>\n\nnamespace cm {\nnamespace data {\n\nclass Entity;\n\nclass CMLIBSHARED_EXPORT DataDecorator : public QObject\n{\n    Q_OBJECT\n    Q_PROPERTY( QString ui_label READ label CONSTANT )\n\npublic:\n    DataDecorator(Entity* parent = nullptr, const QString& key = \n                  \"SomeItemKey\", const QString& label = \"\");\n                                 virtual ~DataDecorator();\n\n    const QString& key() const;\n    const QString& label() const;\n    Entity* parentEntity();\n\n    virtual QJsonValue jsonValue() const = 0;\n    virtual void update(const QJsonObject& jsonObject) = 0;\n\nprivate:\n    class Implementation;\n    QScopedPointer<Implementation> implementation;\n};\n\n}}\n\n#endif\n```\n\n我们从 QObject 继承，添加我们的`dllexport`宏，并像往常一样将整个内容包装在名称空间中。此外，因为这是一个抽象基类，我们确保实现了一个虚拟析构函数。\n\n我们知道，因为我们从 QObject 继承，所以我们希望在构造函数中接收一个指向父对象的指针。我们还知道，所有数据项都将是一个**实体**的子代(我们将很快编写该实体，并在此进行转发声明)，该实体本身将从 QObject 派生。我们可以利用这两个事实将我们的`DataDecorator`直接父化为一个实体。\n\n我们用几根线构造装饰器。我们所有的数据装饰器都必须有一个在序列化到 JSON 和从 JSON 序列化时使用的键，并且它们还将共享一个`label`属性，用户界面可以使用该属性在数据控件旁边显示描述性文本。我们将这些成员藏在私有实现中，并为他们实现一些访问器方法。\n\n最后，我们开始实现我们的 JSON 序列化，通过声明虚拟方法将值表示为`QJsonValue`并从提供的`QJsonObject`更新值。由于该值在基类中是未知的，而是将在派生类中实现，因此这两种方法都是纯虚函数。\n\n`data-decorator.cpp`:\n\n```cpp\n#include \"data-decorator.h\"\n\nnamespace cm {\nnamespace data {\n\nclass DataDecorator::Implementation\n{\npublic:\n    Implementation(Entity* _parent, const QString& _key, const QString& \n                                                         _label)\n        : parentEntity(_parent)\n        , key(_key)\n        , label(_label)\n    {\n    }\n    Entity* parentEntity{nullptr};\n    QString key;\n    QString label;\n};\n\nDataDecorator::DataDecorator(Entity* parent, const QString& key, const QString& label)\n    : QObject((QObject*)parent)\n{\n    implementation.reset(new Implementation(parent, key, label));\n}\n\nDataDecorator::~DataDecorator()\n{\n}\n\nconst QString& DataDecorator::key() const\n{\n    return implementation->key;\n}\n\nconst QString& DataDecorator::label() const\n{\n    return implementation->label;\n}\n\nEntity* DataDecorator::parentEntity()\n{\n    return implementation->parentEntity;\n}\n\n}}\n```\n\n实现非常简单，本质上只是管理一些数据成员。\n\n接下来，我们将实现处理字符串的派生装饰器类。\n\n`string-decorator.h`:\n\n```cpp\n#ifndef STRINGDECORATOR_H\n#define STRINGDECORATOR_H\n\n#include <QJsonObject>\n#include <QJsonValue>\n#include <QObject>\n#include <QScopedPointer>\n#include <QString>\n\n#include <cm-lib_global.h>\n#include <data/data-decorator.h>\n\nnamespace cm {\nnamespace data {\n\nclass CMLIBSHARED_EXPORT StringDecorator : public DataDecorator\n{\n    Q_OBJECT\n\n    Q_PROPERTY( QString ui_value READ value WRITE setValue NOTIFY \n               valueChanged )\npublic:\n    StringDecorator(Entity* parentEntity = nullptr, const QString& key = \"SomeItemKey\", const QString& label = \"\", const QString& value = \"\");\n    ~StringDecorator();\n\n    StringDecorator& setValue(const QString& value);\n    const QString& value() const;\n\n    QJsonValue jsonValue() const override;\n    void update(const QJsonObject& jsonObject) override;\n\nsignals:\n    void valueChanged();\n\nprivate:\n    class Implementation;\n    QScopedPointer<Implementation> implementation;\n};\n\n}}\n\n#endif\n```\n\n这里没有太多其他事情发生——我们只是添加了一个强类型的`QString`值属性来保存我们的值。我们还覆盖了虚拟 JSON 相关的方法。\n\nWhen deriving from a class that inherits from QObject, you need to add the Q_OBJECT macro to the derived class as well as the base class if the derived class implements its own signals or slots.\n\n`string-decorator.cpp`:\n\n```cpp\n#include \"string-decorator.h\"\n\n#include <QVariant>\n\nnamespace cm {\nnamespace data {\n\nclass StringDecorator::Implementation\n{\npublic:\n    Implementation(StringDecorator* _stringDecorator, const QString& \n                                                      _value)\n        : stringDecorator(_stringDecorator)\n        , value(_value)\n    {\n    }\n\n    StringDecorator* stringDecorator{nullptr};\n    QString value;\n};\n\nStringDecorator::StringDecorator(Entity* parentEntity, const QString& key, const QString& label, const QString& value)\n    : DataDecorator(parentEntity, key, label)\n{\n    implementation.reset(new Implementation(this, value));\n}\n\nStringDecorator::~StringDecorator()\n{\n}\n\nconst QString& StringDecorator::value() const\n{\n    return implementation->value;\n}\n\nStringDecorator& StringDecorator::setValue(const QString& value)\n{\n    if(value != implementation->value) {\n        // ...Validation here if required...\n        implementation->value = value;\n        emit valueChanged();\n    }\n    return *this;\n}\n\nQJsonValue StringDecorator::jsonValue() const\n{\n    return QJsonValue::fromVariant(QVariant(implementation->value));\n}\n\nvoid StringDecorator::update(const QJsonObject& _jsonObject)\n{\n    if (_jsonObject.contains(key())) {\n        setValue(_jsonObject.value(key()).toString());\n    } else {\n        setValue(\"\");\n    }\n}\n}}\n```\n\n同样，这里没有什么特别复杂的。通过使用`READ`和`WRITE`属性语法，而不是更简单的`MEMBER`关键字，我们现在有了一种拦截用户界面设置的值的方法，并且我们可以决定是否要将更改应用于成员变量。变异器可以像您需要的那样复杂，但是我们现在所做的只是设置值并发出信号告诉用户界面它已经被改变了。我们在相等检查中包装操作，因此如果新值与旧值相同，我们不会采取任何操作。\n\nHere, the mutator returns a reference to self (*this), which is helpful because it enables method chaining, for example,  `myName.setValue(“Nick”).setSomeNumber(1234).setSomeOtherProperty(true)`. However, this is not necessary for the property bindings, so feel free to use the more common `void` return type if you prefer.\n\n我们使用两步转换过程，先将我们的`QString`值转换为`QVariant`，然后再将其转换为我们的目标`QJsonValue`类型。将使用`DataDecorator`基类的`key`将`QJsonValue`插入父实体 JSON 对象。当我们编写**实体**相关类时，我们将更详细地介绍这一点。\n\nAn alternative approach would be to simply represent the value of our various data items as a `QVariant` member in the `DataDecorator` base class, removing the need to have separate classes for `QString`, `int`, and so on. The problem with this approach is that you end up having to write lots of nasty code that says “if you have a `QVariant` containing a string then run this code if it contains an `int` then run this code...”. I prefer the additional overhead of writing the extra classes in exchange for having known types and cleaner, simpler code. This will become particularly helpful when we look at data validation. Validating a string is completely different from validating a number and different again from validating a date.\n\n`IntDecorator`和`DateTimeDecorator`实际上与`StringDecorator`相同，只是将`QString`值替换为 int 或`QDateTime`。不过我们可以补充`DateTimeDecorator`一些附加属性来帮助我们。添加以下属性和与之对应的访问器方法:\n\n```cpp\nQ_PROPERTY( QString ui_iso8601String READ toIso8601String NOTIFY valueChanged )\nQ_PROPERTY( QString ui_prettyDateString READ toPrettyDateString NOTIFY valueChanged )\nQ_PROPERTY( QString ui_prettyTimeString READ toPrettyTimeString NOTIFY valueChanged )\nQ_PROPERTY( QString ui_prettyString READ toPrettyString NOTIFY valueChanged )\n```\n\n这些属性的目的是使用户界面能够方便地访问日期/时间值，作为预格式化为几种不同样式的`QString`。让我们遍历每个访问器的实现。\n\nQt 内置了对 ISO8601 格式日期的支持，这是在系统之间传输日期时间值时非常常见的格式，例如在 HTTP 请求中。它是一种灵活的格式，支持几种不同的表示，但通常遵循格式 yyyy-MM-ddTHH:mm:ss.zt，其中 T 是字符串文字，z 是毫秒，T 是时区信息:\n\n```cpp\nQString DateTimeDecorator::toIso8601String() const\n{\n    if (implementation->value.isNull()) {\n        return \"\";\n    } else {\n        return implementation->value.toString(Qt::ISODate);\n    }\n}\n```\n\n接下来，我们提供一种以人类可读的长格式显示完整日期时间的方法，例如，2017 年 7 月 22 日星期六@ 12:07:45:\n\n```cpp\nQString DateTimeDecorator::toPrettyString() const\n{\n    if (implementation->value.isNull()) {\n        return \"Not set\";\n    } else {\n        return implementation->value.toString( \"ddd d MMM yyyy @ HH:mm:ss\" );\n    }\n}\n```\n\n最后两种方法显示日期或时间部分，例如，2017 年 7 月 22 日或下午 12:07:\n\n```cpp\nQString DateTimeDecorator::toPrettyDateString() const\n{\n    if (implementation->value.isNull()) {\n        return \"Not set\";\n    } else {\n        return implementation->value.toString( \"d MMM yyyy\" );\n    }\n}\n\nQString DateTimeDecorator::toPrettyTimeString() const\n{\n    if (implementation->value.isNull()) {\n        return \"Not set\";\n    } else {\n        return implementation->value.toString( \"hh:mm ap\" );\n    }\n}\n```\n\n我们的最终类型`EnumeratorDecorator`与`IntDecorator`大致相同，但它也接受映射器。这个容器帮助我们将存储的 int 值映射到字符串表示。如果我们考虑我们计划实现的`Contact.type`枚举器，枚举值将是 0、1、2 等等；然而，当涉及到 UI 时，这个数字对用户来说没有任何意义。我们真的需要呈现`Email`、`Telephone`，或者一些其他的字符串表示，地图允许我们这样做。\n\n`enumerator-decorator.h`:\n\n```cpp\n#ifndef ENUMERATORDECORATOR_H\n#define ENUMERATORDECORATOR_H\n\n#include <map>\n\n#include <QJsonObject>\n#include <QJsonValue>\n#include <QObject>\n#include <QScopedPointer>\n\n#include <cm-lib_global.h>\n#include <data/data-decorator.h>\n\nnamespace cm {\nnamespace data {\n\nclass CMLIBSHARED_EXPORT EnumeratorDecorator : public DataDecorator\n{\n    Q_OBJECT\n    Q_PROPERTY( int ui_value READ value WRITE setValue NOTIFY \n                                              valueChanged )\n    Q_PROPERTY( QString ui_valueDescription READ valueDescription \n                                             NOTIFY valueChanged )\n\npublic:\n    EnumeratorDecorator(Entity* parentEntity = nullptr, const QString& \n    key = \"SomeItemKey\", const QString& label = \"\", int value = 0,  \n    const std::map<int, QString>& descriptionMapper = std::map<int, \n     QString>());\n    ~EnumeratorDecorator();\n\n    EnumeratorDecorator& setValue(int value);\n    int value() const;\n    QString valueDescription() const;\n\n    QJsonValue jsonValue() const override;\n    void update(const QJsonObject& jsonObject) override;\n\nsignals:\n    void valueChanged();\n\nprivate:\n    class Implementation;\n    QScopedPointer<Implementation> implementation;\n};\n\n}}\n\n#endif\n```\n\n我们将映射作为另一个成员变量存储在私有实现类中，然后使用它来提供枚举值的字符串表示:\n\n```cpp\nQString EnumeratorDecorator::valueDescription() const\n{\n    if (implementation->descriptionMapper.find(implementation->value) \n                       != implementation->descriptionMapper.end()) {\n        return implementation->descriptionMapper.at(implementation-\n                                                    >value);\n    } else {\n        return {};\n    }\n}\n```\n\n既然我们已经介绍了实体所需的数据类型，那么让我们继续讨论实体本身。\n\n# 实体\n\n因为我们有很多功能想要在我们的数据模型中共享，我们将实现一个**实体**基类。我们需要能够表示父/子关系，以便客户可以有供应和账单地址。我们还需要为我们的联系人和约会支持实体集合。最后，每个实体层次结构必须能够将自己序列化到 JSON 对象和从 JSON 对象序列化。\n\n在`cm-lib/source/data`中创建新的类实体。\n\n`entity.h`:\n\n```cpp\n#ifndef ENTITY_H\n#define ENTITY_H\n\n#include <map>\n\n#include <QObject>\n#include <QScopedPointer>\n\n#include <cm-lib_global.h>\n#include <data/data-decorator.h>\n\nnamespace cm {\nnamespace data {\n\nclass CMLIBSHARED_EXPORT Entity : public QObject\n{\n    Q_OBJECT\n\npublic:\n    Entity(QObject* parent = nullptr, const QString& key = \n                                                  \"SomeEntityKey\");\n    Entity(QObject* parent, const QString& key, const QJsonObject& \n     jsonObject);\n    virtual ~Entity();\n\npublic:\n    const QString& key() const;\n    void update(const QJsonObject& jsonObject);\n    QJsonObject toJson() const;\n\nsignals:\n    void childEntitiesChanged();\n    void dataDecoratorsChanged();\n\nprotected:\n    Entity* addChild(Entity* entity, const QString& key);\n    DataDecorator* addDataItem(DataDecorator* dataDecorator);\n\nprotected:\n    class Implementation;\n    QScopedPointer<Implementation> implementation;\n};\n\n}}\n\n#endif\n```\n\n`entity.cpp`:\n\n```cpp\n#include \"entity.h\"\n\nnamespace cm {\nnamespace data {\n\nclass Entity::Implementation\n{\npublic:\n    Implementation(Entity* _entity, const QString& _key)\n        : entity(_entity)\n        , key(_key)\n    {\n    }\n    Entity* entity{nullptr};\n    QString key;\n    std::map<QString, Entity*> childEntities;\n    std::map<QString, DataDecorator*> dataDecorators;\n};\n\nEntity::Entity(QObject* parent, const QString& key)\n    : QObject(parent)\n{\n    implementation.reset(new Implementation(this, key));\n}\n\nEntity::Entity(QObject* parent, const QString& key, const QJsonObject& \n               jsonObject) : Entity(parent, key)\n{\n    update(jsonObject);\n}\n\nEntity::~Entity()\n{\n}\n\nconst QString& Entity::key() const\n{\n    return implementation->key;\n}\n\nEntity* Entity::addChild(Entity* entity, const QString& key)\n{\n    if(implementation->childEntities.find(key) == \n        std::end(implementation->childEntities)) {\n        implementation->childEntities[key] = entity;\n        emit childEntitiesChanged();\n    }\n    return entity;\n}\n\nDataDecorator* Entity::addDataItem(DataDecorator* dataDecorator)\n{\n    if(implementation->dataDecorators.find(dataDecorator->key()) == \n       std::end(implementation->dataDecorators)) {\n        implementation->dataDecorators[dataDecorator->key()] = \n        dataDecorator;\n        emit dataDecoratorsChanged();\n    }\n    return dataDecorator;\n}\n\nvoid Entity::update(const QJsonObject& jsonObject)\n{\n    // Update data decorators\n    for (std::pair<QString, DataDecorator*> dataDecoratorPair : \n         implementation->dataDecorators) {\n        dataDecoratorPair.second->update(jsonObject);\n    }\n    // Update child entities\n    for (std::pair<QString, Entity*> childEntityPair : implementation-\n    >childEntities) {childEntityPair.second>update(jsonObject.value(childEntityPair.first).toObject());\n    }\n}\n\nQJsonObject Entity::toJson() const\n{\n    QJsonObject returnValue;\n    // Add data decorators\n    for (std::pair<QString, DataDecorator*> dataDecoratorPair : \n                         implementation->dataDecorators) {\n        returnValue.insert( dataDecoratorPair.first, \n        dataDecoratorPair.second->jsonValue() );\n    }\n    // Add child entities\n    for (std::pair<QString, Entity*> childEntityPair : implementation->childEntities) {\n        returnValue.insert( childEntityPair.first, childEntityPair.second->toJson() );\n    }\n    return returnValue;\n}\n\n}}\n```\n\n很像我们的`DataDecorator`基类，我们给所有实体分配一个唯一的键，它将用于 JSON 序列化。我们还添加了一个重载的构造函数，我们可以向它传递一个`QJsonObject`，这样我们就可以从 JSON 实例化一个实体。与此相关的是，我们还声明了一对方法来将现有实例序列化到 JSON 和从 JSON 序列化。\n\n我们的实体将维护一些集合——表示模型属性的数据装饰器的映射，以及表示单个子代的实体的映射。我们将每个项目的键映射到实例。\n\n我们公开了几个受保护的方法，这些方法是派生类将用来添加它的数据项和子对象；例如，我们的客户端模型将添加一个名称数据项以及`supplyAddress`和`billingAddress`子级。为了补充这些方法，我们还添加了信号来告诉任何感兴趣的观察者集合已经改变。\n\n在这两种情况下，我们都会在添加之前检查该键在地图上是否已经存在。然后，我们返回提供的指针，以便消费者可以使用它进行进一步的操作。当我们开始实现数据模型时，您会看到它的价值。\n\n我们将我们填充的映射用于 JSON 序列化方法。我们已经在我们的`DataDecorator`基类上声明了一个`update()`方法，所以我们简单地遍历所有数据项，并将 JSON 对象依次传递给每个数据项。每个派生装饰器类都有自己的实现来处理解析。类似地，我们在每个子实体上递归调用`Entity::update()`。\n\n序列化为 JSON 对象遵循相同的模式。每个数据项都可以将其值转换为`QJsonValue`对象，所以我们依次获取每个值，并使用每个数据项的键将其追加到一个根 JSON 对象中。我们在每个子节点上递归调用`Entity::toJson()`，这沿着层次树向下级联。\n\n在我们完成我们的**实体**之前，我们需要声明一组类来表示一个实体集合。\n\n# 实体集合\n\n为了实现实体集合，我们需要利用一些更高级的 C++ 技术，到目前为止，我们将暂时打破常规，在一个头文件中实现多个类。\n\n在`cm-lib/source/data`中创建`entity-collection.h`，并在其中添加我们的名称空间作为普通名称空间，并转发声明实体:\n\n```cpp\n#ifndef ENTITYCOLLECTION_H\n#define ENTITYCOLLECTION_H\n\nnamespace cm {\nnamespace data {\n    class Entity;\n}}\n\n#endif\n```\n\n接下来，我们将依次浏览必要的类，每个类都必须按顺序添加到名称空间中。\n\n我们首先定义根类，它只不过是从`QObject`继承，并让我们获得它带来的所有好处，比如对象所有权和信号。这是必需的，因为直接从`QObject`派生的类不能模板化:\n\n```cpp\nclass CMLIBSHARED_EXPORT EntityCollectionObject : public QObject\n{\n    Q_OBJECT\n\npublic:\n    EntityCollectionObject(QObject* _parent = nullptr) : QObject(_parent) {}\n    virtual ~EntityCollectionObject() {}\n\nsignals:\n    void collectionChanged();\n};\n```\n\n您需要为`QObject`和我们的 DLL 导出宏添加包含。接下来，我们需要一个类型不可知的接口来使用我们的实体，就像我们已经实现的`DataDecorator`和实体映射一样。然而，这里的事情有点复杂，因为我们不会为我们拥有的每个集合派生一个新的类，所以我们需要一些获取类型化数据的方法。我们有两个要求。首先，用户界面需要一个派生类型的`QList`(例如**客户端** *)，这样它就可以访问特定于一个客户端的所有属性并显示所有数据。其次，我们的**实体**类需要一个基类型的向量(**实体** *)，这样它就可以迭代它的集合，而不需要考虑它处理的到底是哪种类型。我们实现这一点的方法是声明两个模板方法，但是将它们的定义推迟到以后。当消费者想要派生类型的集合时将使用`derivedEntities()`，而当消费者只想访问基本接口时将使用`baseEntities()`:\n\n```cpp\nclass EntityCollectionBase : public EntityCollectionObject\n{\npublic:\n    EntityCollectionBase(QObject* parent = nullptr, const QString& key \n                                         = \"SomeCollectionKey\")\n        : EntityCollectionObject(parent)\n        , key(key)\n    {}\n\n    virtual ~EntityCollectionBase()\n    {}\n\n    QString getKey() const\n    {\n        return key;\n    }\n\n    virtual void clear() = 0;\n    virtual void update(const QJsonArray& json) = 0;\n    virtual std::vector<Entity*> baseEntities() = 0;\n\n    template <class T>\n    QList<T*>& derivedEntities();\n\n    template <class T>\n    T* addEntity(T* entity);\n\nprivate:\n    QString key;\n};\n```\n\n接下来，我们声明一个完整的模板类，在其中存储派生类型的集合并实现所有方法，除了我们刚刚讨论的两个模板方法:\n\n```cpp\ntemplate <typename T>\nclass EntityCollection : public EntityCollectionBase\n{\npublic:\n    EntityCollection(QObject* parent = nullptr, const QString& key = \n             \"SomeCollectionKey\")\n        : EntityCollectionBase(parent, key)\n    {}\n\n    ~EntityCollection()\n    {}\n\n    void clear() override\n    {\n        for(auto entity : collection) {\n            entity->deleteLater();\n        }\n        collection.clear();\n    }\n\n    void update(const QJsonArray& jsonArray) override\n    {\n        clear();\n        for(const QJsonValue& jsonValue : jsonArray) {\n            addEntity(new T(this, jsonValue.toObject()));\n        }\n    }\n\n    std::vector<Entity*> baseEntities() override\n    {\n        std::vector<Entity*> returnValue;\n        for(T* entity : collection) {\n            returnValue.push_back(entity);\n        }\n        return returnValue;\n    }\n\n    QList<T*>& derivedEntities()\n    {\n        return collection;\n    }\n\n    T* addEntity(T* entity)\n    {\n        if(!collection.contains(entity)) {\n            collection.append(entity);\n            EntityCollectionObject::collectionChanged();\n        }\n        return entity;\n    }\n\nprivate:\n    QList<T*> collection;       \n};\n```\n\nYou will need `#include <QJsonValue>` and `<QJsonArray>` for these classes.\n\n`clear()`方法只是清空集合，整理内存；`update()`在概念上与我们在 Entity 中实现的 JSON 方法相同，只是我们处理的是实体的集合，所以我们取一个 JSON 数组而不是一个对象。`addEntity()`向集合中添加一个派生类的实例，`derivedEntities()`返回集合；`baseEntities()`做更多的工作，根据请求创建一个新的向量，并用集合中的所有项目填充它。它只是隐式转换指针，所以我们不关心昂贵的对象实例化。\n\n最后，我们提供了我们神奇的模板化方法的实现:\n\n```cpp\ntemplate <class T>\nQList<T*>& EntityCollectionBase::derivedEntities()\n{\n    return dynamic_cast<const EntityCollection<T>&>(*this).derivedEntities();\n}\n\ntemplate <class T>\nT* EntityCollectionBase::addEntity(T* entity)\n{\n    return dynamic_cast<const EntityCollection<T>&>(*this).addEntity(entity);\n}\n```\n\n通过延迟这些方法的实现，我们已经实现了我们的模板化`EntityCollection`类。我们现在可以将对模板化方法的任何调用“路由”到模板化类中的实现。这是一种复杂的技巧，但是当我们开始在现实模型中实现这些集合时，它将会更有意义。\n\n现在我们的实体集合已经准备好了，我们可以返回到我们的实体类，并将它们添加到组合中。\n\n在标题`#include <data/entity-collection.h>`中，添加信号:\n\n```cpp\nvoid childCollectionsChanged(const QString& collectionKey);\n```\n\n另外，添加受保护的方法:\n\n```cpp\nEntityCollectionBase* addChildCollection(EntityCollectionBase* entityCollection);\n```\n\n在实现文件中，添加私有成员:\n\n```cpp\nstd::map<QString, EntityCollectionBase*> childCollections;\n```\n\n然后，添加方法:\n\n```cpp\nEntityCollectionBase* Entity::addChildCollection(EntityCollectionBase* entityCollection)\n{\n    if(implementation->childCollections.find(entityCollection- \n     >getKey()) == std::end(implementation->childCollections)) {\n        implementation->childCollections[entityCollection->getKey()] =  \n                                        entityCollection;\n        emit childCollectionsChanged(entityCollection->getKey());\n    }\n    return entityCollection;\n}\n```\n\n这与其他映射的工作方式完全相同，将键与指向基类的指针相关联。\n\n接下来，将集合添加到`update()`方法中:\n\n```cpp\nvoid Entity::update(const QJsonObject& jsonObject)\n{\n    // Update data decorators\n    for (std::pair<QString, DataDecorator*> dataDecoratorPair :   \n         implementation->dataDecorators) {\n        dataDecoratorPair.second->update(jsonObject);\n    }\n\n    // Update child entities\n    for (std::pair<QString, Entity*> childEntityPair : implementation- \n       >childEntities) { childEntityPair.second- \n       >update(jsonObject.value(childEntityPair.first).toObject());\n    }\n\n    // Update child collections\n    for (std::pair<QString, EntityCollectionBase*> childCollectionPair \n         : implementation->childCollections) {\n            childCollectionPair.second-\n        >update(jsonObject.value(childCollectionPair.first).toArray());\n    }\n}\n```\n\n最后，将集合添加到`toJson()`方法中:\n\n```cpp\nQJsonObject Entity::toJson() const\n{\n    QJsonObject returnValue;\n\n    // Add data decorators\n    for (std::pair<QString, DataDecorator*> dataDecoratorPair : \n        implementation->dataDecorators) {\n        returnValue.insert( dataDecoratorPair.first, \n        dataDecoratorPair.second->jsonValue() );\n    }\n\n    // Add child entities\n    for (std::pair<QString, Entity*> childEntityPair : implementation-\n        >childEntities) {\n        returnValue.insert( childEntityPair.first, \n       childEntityPair.second->toJson() );\n    }\n\n    // Add child collections\n    for (std::pair<QString, EntityCollectionBase*> childCollectionPair \n        : implementation->childCollections) {\n        QJsonArray entityArray;\n            for (Entity* entity : childCollectionPair.second-\n           >baseEntities()) {\n            entityArray.append( entity->toJson() );\n        }\n        returnValue.insert( childCollectionPair.first, entityArray );\n    }\n\n    return returnValue;\n}\n```\n\nYou will need `#include <QJsonArray>` for that last snippet.\n\n我们用`baseEntities()`方法给我们集合`Entity*`。然后，我们将每个实体的 JSON 对象追加到一个 JSON 数组中，完成后，用集合的键将该数组添加到我们的根 JSON 对象中。\n\n过去的几节非常长而且复杂，可能看起来只是为了实现一些数据模型而做了很多工作。然而，所有的代码都是你一次编写的，它为你提供了很多免费的功能，无论你继续做什么，所以从长远来看，这是值得的。我们将继续研究如何在我们的数据模型中实现这些类。\n\n# 数据模型\n\n现在，我们已经有了能够定义数据对象(实体和实体集合)和各种类型的属性(数据装饰器)的基础设施，我们可以继续前进，构建我们在本章前面介绍的对象层次结构。我们已经有了一个由 Qt Creator 创建的默认**客户端**类，所以在`cm-lib/source/models`中用以下新类来补充它:\n\n| **级** | **目的** |\n| `Address` | 表示供应或计费地址 |\n| `Appointment` | 代表与客户的约会 |\n| `Contact` | 表示联系客户端的方法 |\n\n我们将从最简单的模型开始——地址。\n\n`address.h`:\n\n```cpp\n#ifndef ADDRESS_H\n#define ADDRESS_H\n\n#include <QObject>\n\n#include <cm-lib_global.h>\n#include <data/string-decorator.h>\n#include <data/entity.h>\n\nnamespace cm {\nnamespace models {\n\nclass CMLIBSHARED_EXPORT Address : public data::Entity\n{\n    Q_OBJECT\n    Q_PROPERTY(cm::data::StringDecorator* ui_building MEMBER building \n                                                      CONSTANT)\n    Q_PROPERTY(cm::data::StringDecorator* ui_street MEMBER street  \n                                                    CONSTANT)\n    Q_PROPERTY(cm::data::StringDecorator* ui_city MEMBER city CONSTANT)\n    Q_PROPERTY(cm::data::StringDecorator* ui_postcode MEMBER postcode \n                                                      CONSTANT)\n    Q_PROPERTY(QString ui_fullAddress READ fullAddress CONSTANT)\n\npublic:\n    explicit Address(QObject* parent = nullptr);\n    Address(QObject* parent, const QJsonObject& json);\n\n    data::StringDecorator* building{nullptr};\n    data::StringDecorator* street{nullptr};\n    data::StringDecorator* city{nullptr};\n    data::StringDecorator* postcode{nullptr};\n\n    QString fullAddress() const;\n};\n\n}}\n\n#endif\n```\n\n我们定义了在本章开始时设计的属性，但是我们没有使用常规的`QString`对象，而是使用了新的`StringDecorators`。为了保护数据的完整性，我们应该使用`READ`关键字，并通过访问器方法返回一个`StringDecorator* const`，但是为了简单起见，我们将使用`MEMBER`来代替。我们还提供了一个重载的构造函数，可以用来从`QJsonObject`构造地址。最后，我们添加一个助手`fullAddress()`方法和属性，将地址元素连接成一个字符串，供用户界面使用。\n\n`address.cpp`:\n\n```cpp\n#include \"address.h\"\n\nusing namespace cm::data;\n\nnamespace cm {\nnamespace models {\n\nAddress::Address(QObject* parent)\n        : Entity(parent, \"address\")\n{\n    building = static_cast<StringDecorator*>(addDataItem(new StringDecorator(this, \"building\", \"Building\")));\n    street = static_cast<StringDecorator*>(addDataItem(new StringDecorator(this, \"street\", \"Street\")));\n    city = static_cast<StringDecorator*>(addDataItem(new StringDecorator(this, \"city\", \"City\")));\n    postcode = static_cast<StringDecorator*>(addDataItem(new StringDecorator(this, \"postcode\", \"Post Code\")));\n}\n\nAddress::Address(QObject* parent, const QJsonObject& json)\n        : Address(parent)\n{\n    update(json);\n}\n\nQString Address::fullAddress() const\n{\n    return building->value() + \" \" + street->value() + \"\\n\" + city->value() + \"\\n\" + postcode->value();\n}\n\n}}\n```\n\n这就是我们所有努力工作的开始。我们需要对每个属性做两件事。首先，我们需要一个指向派生类型(`StringDecorator`)的指针，我们可以将其呈现给用户界面，以便显示和编辑该值。其次，我们需要让基本实体类知道基本类型(`DataDecorator`)，这样它就可以迭代数据项并为我们执行 JSON 序列化工作。我们可以使用`addDataItem()`方法在一句话中实现这两个目标:\n\n```cpp\nbuilding = static_cast<StringDecorator*>(addDataItem(new StringDecorator(this, \"building\", \"Building\")));\n```\n\n分解后，我们用`building`键和`Building`用户界面标签创建一个新的`StringDecorator*`。这将立即传递给`addDataItem()`，T3 将其添加到**实体**中的`dataDecorators`集合中，并将数据项作为`DataDecorator*`返回。然后，我们可以将其转换回`StringDecorator*`，然后将其存储在`building`成员变量中。\n\n这里唯一的另一个实现是获取一个 JSON 对象，通过调用默认构造函数来正常构造地址，然后使用`update()`方法更新模型。\n\n`Appointment`和`Contact`模型遵循相同的模式，只是它们的数据类型具有不同的属性和`DataDecorator`的适当变化。`Contact`变化更大的地方在于其对`contactType`属性使用了`EnumeratorDecorator`。为了支持这一点，我们首先在头文件中定义一个枚举器，它包含我们想要的所有可能的值:\n\n```cpp\nenum eContactType {\n    Unknown = 0,\n    Telephone,\n    Email,\n    Fax\n};\n\n```\n\n请注意，我们有一个由`0`表示的默认值`Unknown`。这很重要，因为它允许我们容纳一个初始未设置的值。接下来，我们定义一个映射器容器，允许我们将每个枚举类型映射到一个描述性字符串:\n\n```cpp\nstd::map<int, QString> Contact::contactTypeMapper = std::map<int, QString> {\n    { Contact::eContactType::Unknown, \"\" }\n    , { Contact::eContactType::Telephone, \"Telephone\" }\n    , { Contact::eContactType::Email, \"Email\" }\n    , { Contact::eContactType::Fax, \"Fax\" }\n};\n```\n\n当创建新的`EnumeratorDecorator`时，我们提供默认值(0 代表`eContactType::Unknown`)以及映射器:\n\n```cpp\ncontactType = static_cast<EnumeratorDecorator*>(addDataItem(new EnumeratorDecorator(this, \"contactType\", \"Contact Type\", 0, contactTypeMapper)));\n```\n\n我们的客户端模型稍微复杂一点，因为它不仅有数据项，还有子实体和集合。然而，我们创造和揭露这些东西的方式与我们已经看到的非常相似。\n\n`client.h`:\n\n```cpp\n#ifndef CLIENT_H\n#define CLIENT_H\n\n#include <QObject>\n#include <QtQml/QQmlListProperty>\n\n#include <cm-lib_global.h>\n#include <data/string-decorator.h>\n#include <data/entity.h>\n#include <data/entity-collection.h>\n#include <models/address.h>\n#include <models/appointment.h>\n#include <models/contact.h>\n\nnamespace cm {\nnamespace models {\n\nclass CMLIBSHARED_EXPORT Client : public data::Entity\n{\n    Q_OBJECT\n    Q_PROPERTY( cm::data::StringDecorator* ui_reference MEMBER \n                                           reference CONSTANT )\n    Q_PROPERTY( cm::data::StringDecorator* ui_name MEMBER name CONSTANT )\n    Q_PROPERTY( cm::models::Address* ui_supplyAddress MEMBER \n                                     supplyAddress CONSTANT )\n    Q_PROPERTY( cm::models::Address* ui_billingAddress MEMBER \n                                     billingAddress CONSTANT )\n    Q_PROPERTY( QQmlListProperty<Appointment> ui_appointments READ \n                        ui_appointments NOTIFY appointmentsChanged )\n    Q_PROPERTY( QQmlListProperty<Contact> ui_contacts READ ui_contacts \n                                          NOTIFY contactsChanged )\n\npublic:    \n    explicit Client(QObject* parent = nullptr);\n    Client(QObject* parent, const QJsonObject& json);\n\n    data::StringDecorator* reference{nullptr};\n    data::StringDecorator* name{nullptr};\n    Address* supplyAddress{nullptr};\n    Address* billingAddress{nullptr};\n    data::EntityCollection<Appointment>* appointments{nullptr};\n    data::EntityCollection<Contact>* contacts{nullptr};\n\n    QQmlListProperty<cm::models::Appointment> ui_appointments();\n    QQmlListProperty<cm::models::Contact> ui_contacts();\n\nsignals:\n    void appointmentsChanged();\n    void contactsChanged();\n};\n\n}}\n\n#endif\n```\n\n我们将子实体公开为指向派生类型的指针，将集合公开为指向模板化`EntityCollection`的指针。\n\n`client.cpp`:\n\n```cpp\n#include \"client.h\"\n\nusing namespace cm::data;\n\nnamespace cm {\nnamespace models {\n\nClient::Client(QObject* parent)\n    : Entity(parent, \"client\")\n{\n    reference = static_cast<StringDecorator*>(addDataItem(new \n                StringDecorator(this, \"reference\", \"Client Ref\")));\n    name = static_cast<StringDecorator*>(addDataItem(new \n                StringDecorator(this, \"name\", \"Name\")));\n    supplyAddress = static_cast<Address*>(addChild(new Address(this), \n                                          \"supplyAddress\"));\n    billingAddress = static_cast<Address*>(addChild(new Address(this), \n                                          \"billingAddress\"));\n    appointments = static_cast<EntityCollection<Appointment>*>\n    (addChildCollection(new EntityCollection<Appointment>(this, \n                                            \"appointments\")));\n    contacts = static_cast<EntityCollection<Contact>*>(addChildCollection(new EntityCollection<Contact>(this, \"contacts\")));\n}\n\nClient::Client(QObject* parent, const QJsonObject& json)\n    : Client(parent)\n{\n    update(json);\n}\n\nQQmlListProperty<Appointment> Client::ui_appointments()\n{\n    return QQmlListProperty<Appointment>(this, appointments->derivedEntities());\n}\n\nQQmlListProperty<Contact> Client::ui_contacts()\n{\n    return QQmlListProperty<Contact>(this, contacts->derivedEntities());\n}\n\n}}\n```\n\n添加子实体遵循与数据项相同的模式，但使用`addChild()`方法。请注意，我们添加了多个相同地址类型的子代，但确保它们具有不同的`key`值，以避免重复和无效的 JSON。实体集合是用`addChildCollection()`添加的，除了模板化，它们遵循相同的方法。\n\n虽然创建我们的实体和数据项需要大量的工作，但是创建模型真的非常简单，现在它们都包含了我们本来不会拥有的特性。\n\n在用户界面中使用我们花哨的新模型之前，我们需要在`cm-ui`中的`main.cpp`中注册类型，包括表示数据项的数据装饰器。记得先补充一下相关的`#include`语句:\n\n```cpp\nqmlRegisterType<cm::data::DateTimeDecorator>(\"CM\", 1, 0, \"DateTimeDecorator\");\nqmlRegisterType<cm::data::EnumeratorDecorator>(\"CM\", 1, 0, \"EnumeratorDecorator\");\nqmlRegisterType<cm::data::IntDecorator>(\"CM\", 1, 0, \"IntDecorator\");\nqmlRegisterType<cm::data::StringDecorator>(\"CM\", 1, 0, \"StringDecorator\");\n\nqmlRegisterType<cm::models::Address>(\"CM\", 1, 0, \"Address\");\nqmlRegisterType<cm::models::Appointment>(\"CM\", 1, 0, \"Appointment\");\nqmlRegisterType<cm::models::Client>(\"CM\", 1, 0, \"Client\");\nqmlRegisterType<cm::models::Contact>(\"CM\", 1, 0, \"Contact\");\n```\n\n完成后，我们将在`MasterController`中创建一个客户端实例，我们将使用它来填充新客户端的数据。这与我们用于添加其他控制器的模式完全相同。\n\n首先，将成员变量添加到`MasterController`的私有实现中:\n\n```cpp\nClient* newClient{nullptr};\n```\n\n然后，在`Implementation`构造函数中初始化它:\n\n```cpp\nnewClient = new Client(masterController);\n```\n\n第三，添加访问器方法:\n\n```cpp\nClient* MasterController::newClient()\n{\n    return implementation->newClient;\n}\n```\n\n最后添加`Q_PROPERTY`:\n\n```cpp\nQ_PROPERTY( cm::models::Client* ui_newClient READ newClient CONSTANT )\n```\n\n我们现在有一个空的客户端实例可供用户界面使用，特别是`CreateClientView`，接下来我们将编辑它。首先为新的客户端实例添加快捷方式属性:\n\n```cpp\nproperty Client newClient: masterController.ui_newClient\n```\n\n请记住，属性都应该在根项目级别定义，并且您需要`import CM 1.0`来访问注册的类型。这只是让我们能够使用`newClient`作为访问实例的简写，而不是每次都必须键入`masterController.ui_newClient`。\n\n此时，一切都准备就绪，您应该能够运行应用并毫无问题地导航到新的客户端视图。视图还没有对新的客户端实例做任何事情，但是它很高兴地坐在那里准备采取行动。现在，让我们看看如何与它互动。\n\n# 自定义文本框\n\n我们将从客户的`name`数据项开始。当我们在用户界面中使用另一个`QString`属性处理欢迎消息时，我们用基本的文本组件显示它。该组件是只读的，因此要查看和编辑我们的属性，我们需要获取其他东西。基地`QtQuick`模块有两个选项:`TextInput`和`TextEdit`。`TextInput`用于单行可编辑纯文本，而`TextEdit`处理多行文本块，也支持富文本。`TextInput`是我们**名字**的理想选择。\n\nImporting the `QtQuick.Controls` module makes additional text-based components like `Label`, `TextField`, and `TextArea` available. Label inherits and extends Text, `TextField` inherits and extends `TextInput` and `TextArea` inherits and extends `TextEdit`. The basic controls are enough for us at this stage, but be aware that these alternatives exist. If you find yourself trying to do something with one of the basic controls which it doesn’t seem to support, then import `QtQuick.Controls` and take a look at its more powerful cousin. It may well have the functionality you are looking for.\n\n让我们在所学的基础上，创建一个新的可重用组件。像往常一样，我们将从准备我们需要的样式属性开始:\n\n```cpp\nreadonly property real sizeScreenMargin: 20\n```\n\n```cpp\nreadonly property color colourDataControlsBackground: \"#ffffff\"\nreadonly property color colourDataControlsFont: \"#131313\" \nreadonly property int pixelSizeDataControls: 18 \nreadonly property real widthDataControls: 400 \nreadonly property real heightDataControls: 40\n```\n\n接下来，在`cm/cm-ui/components`中创建`StringEditorSingleLine.qml`。这不是最美的名字，但至少是描述性的！\n\nIt's generally helpful to use a prefix with custom QML views and components to help distinguish them from the built-in Qt components and avoid naming conflicts. If we were using that approach with this project, we could have called this component `CMTextBox` or something equally short and simple. Use whatever approach and conventions work for you, it makes no functional difference.\n\n编辑`components.qrc`和`qmldir`，就像我们之前做的那样，使新组件在组件模块中可用。\n\n我们试图通过该组件实现以下目标:\n\n*   能够从任何数据模型传入任何`StringDecorator`属性并查看/编辑该值\n*   查看在`StringDecorator`的`ui_label`属性中定义的控件描述性标签\n*   查看/编辑`TextBox`中`StringDecorator`的`ui_value`属性\n*   如果窗口足够宽，则标签和文本框水平布局\n*   如果窗口不够宽，则标签和文本框垂直布局\n\n牢记这些目标，执行`StringEditorSingleLine`，如下所示:\n\n```cpp\nimport QtQuick 2.9\nimport CM 1.0\nimport assets 1.0\n\nItem {\n    property StringDecorator stringDecorator\n\n    height: width > textLabel.width + textValue.width ? \n    Style.heightDataControls : Style.heightDataControls * 2\n\n    Flow {\n        anchors.fill: parent\n\n        Rectangle {\n            width: Style.widthDataControls\n            height: Style.heightDataControls\n            color: Style.colourBackground\n            Text {\n                id: textLabel\n                anchors {\n                    fill: parent\n                    margins: Style.heightDataControls / 4\n                }\n                text: stringDecorator.ui_label\n                color: Style.colourDataControlsFont\n                font.pixelSize: Style.pixelSizeDataControls\n                verticalAlignment: Qt.AlignVCenter\n            }\n        }\n\n        Rectangle {\n            id: background\n            width: Style.widthDataControls\n            height: Style.heightDataControls\n            color: Style.colourDataControlsBackground\n            border {\n                width: 1\n                color: Style.colourDataControlsFont\n            }\n            TextInput {\n                id: textValue\n                anchors {\n                    fill: parent\n                    margins: Style.heightDataControls / 4\n                }\n                text: stringDecorator.ui_value\n                color: Style.colourDataControlsFont\n                font.pixelSize: Style.pixelSizeDataControls\n                verticalAlignment: Qt.AlignVCenter\n            }\n        }\n\n        Binding {\n            target: stringDecorator\n            property: \"ui_value\"\n            value: textValue.text\n        }\n    }\n}\n```\n\n我们从一个公共的`StringDecorator`属性开始(公共的，因为它在根 Item 元素中)，我们可以从组件外部设置它。\n\n我们引入了一种新的元素——流——来为我们布局标签和文本框。“流”项将并排布局其子元素，直到用完可用空间，然后将它们像页面上的单词一样换行，而不是总是像行或列那样沿单个方向布局内容。我们通过将它锚定到根项目来告诉它有多少可用空间可以使用。\n\n接下来是文本控件中的描述性标签和`TextInput`控件中的可编辑值。我们将这两个控件嵌入到显式大小的矩形中。矩形帮助我们对齐元素，并给我们机会绘制背景和边框。\n\n`Binding`组件在两个不同对象的属性之间建立依赖关系；在我们的例子中，`TextInput`控件称为`textValue`，而`StringDecorator`实例称为`stringDecorator`。`target`属性定义了我们要更新的对象，`property`是我们要设置的`Q_PROPERTY`，而`value`是我们要设置的值。这是给我们真正双向绑定的关键元素。没有这个，我们就可以从`StringDecorator`查看数值，但是我们在 UI 中所做的任何更改都不会更新数值。\n\n回到`CreateClientView`，用我们的新组件替换旧的文本元素，并传入`ui_name`属性:\n\n```cpp\nStringEditorSingleLine {\n    stringDecorator: newClient.ui_name\n}\n```\n\n现在构建并运行应用，导航到“创建客户端”视图，并尝试编辑名称:\n\n![](img/91a79322-c123-4af2-8e42-736e540130a3.png)\n\n如果您切换到“查找客户端”视图并再次返回，您将看到该值被保留，这表明在字符串装饰器中成功地设置了更新。\n\n我们新绑定的视图还没有完全充满数据，但是在接下来的章节中，我们将向这个视图添加越来越多的内容，所以让我们添加一些收尾工作来为我们做准备。\n\n首先，我们只需要向视图中添加另外三四个属性，由于我们为窗口设置的默认大小非常小，我们将耗尽空间，因此在`MasterView`中，将窗口大小增加到适合您显示的大小。我自己请客，1920 x 1080 全高清。\n\n即使有一个更大的窗口可以使用，我们仍然需要为溢出的可能性做好准备，所以我们将把我们的内容添加到另一个名为`ScrollView`的新元素中。顾名思义，它以类似的方式工作，根据可用空间流动和管理内容。如果内容超出可用空间，它将为用户显示滚动条。这也是一个非常手指友好的控件，在触摸屏上，用户只需拖动内容，而不必摆弄小小的滚动条。\n\n虽然我们目前只有一个属性，但是当我们添加更多属性时，我们将需要布局它们，因此我们将添加一个列。\n\n最后，控件被固定在视图的边界上，所以我们将在视图周围添加一点檐槽，并在列中添加一些间距。\n\n修改后的视图应该如下所示:\n\n```cpp\nimport QtQuick 2.9\nimport QtQuick.Controls 2.2\nimport CM 1.0\nimport assets 1.0\nimport components 1.0\n\nItem {\n    property Client newClient: masterController.ui_newClient\n\n    Rectangle {\n        anchors.fill: parent\n        color: Style.colourBackground\n    }\n\n    ScrollView {\n        id: scrollView\n        anchors {\n            left: parent.left\n            right: parent.right\n            top: parent.top\n            bottom: commandBar. top\n            margins: Style.sizeScreenMargin\n        }\n        clip: true\n        Column {\n            spacing: Style.sizeScreenMargin\n            width: scrollView.width\n            StringEditorSingleLine {\n                stringDecorator: newClient.ui_name\n                anchors {\n                    left: parent.left\n                    right: parent.right\n                }\n            }\n        }\n    }\n\n    CommandBar {\n        id: commandBar\n        commandList: masterController.ui_commandController.ui_createClientViewContextCommands\n    }\n}\n```\n\n构建并运行，您应该会看到漂亮整洁的屏幕边距。您还应该能够将窗口的大小从宽调整到窄，并看到字符串编辑器会相应地自动调整其布局。\n\n# 摘要\n\n这是一个相当重要的章节，但是我们已经讨论了任何业务线应用中最重要的元素，那就是数据。我们已经实现了一个自我感知实体的框架，这些实体可以将自己序列化到 JSON 和从 JSON 序列化，并开始构建数据绑定控件。我们已经设计并创建了我们的数据模型，现在正进入回家阶段。在[第 6 章](6.html)、*单元测试*中，我们将向我们迄今为止被忽略的单元测试项目展示一些爱心，并检查我们的实体是否按预期运行。"
  },
  {
    "path": "docs/learn-qt5/6.md",
    "content": "# 六、单元测试\n\n在这一章中，我们将看看近年来真正流行起来的一个过程——单元测试。在介绍如何使用 Qt 自己的单元测试工具 Qt Test 将它集成到我们的解决方案之前，我们将简要地讨论它是什么以及我们为什么想要这样做。我们将涵盖以下主题:\n\n*   单元测试原则\n*   默认的 Qt 方法\n*   另一种方法\n*   数据装饰测试\n*   实体测试\n*   嘲弄的\n\n# 单元测试\n\n单元测试的本质是将一个应用分解成最小的功能块(单元)，然后在项目范围内用真实场景测试每个单元。例如，采用一个简单的方法，将两个有符号整数相加:\n\n```cpp\nint add(intx, int y);\n```\n\n一些示例场景如下所示:\n\n*   将两个正数相加\n*   将两个负数相加\n*   加两个零\n*   将一个正数和一个负数相加\n*   将零和一个正数相加\n*   将零和负数相加\n\n我们可以为这些场景中的每一个编写一个测试，然后每当我们的代码库发生变化时(任何代码，而不仅仅是我们的`add()`方法)，就可以执行这些测试，以确保代码仍然按照预期运行。这是一个非常有价值的工具，可以让您确信您所做的任何代码更改都不会对现有功能产生不利影响。\n\n从历史上看，这些测试应该是手动执行的，但是工具的存在可以让我们编写代码来自动测试代码，这听起来有点矛盾，但是它确实有效。Qt 为基于 Qt 的应用的单元测试提供了一个定制的框架，称为 Qt Test，这就是我们将要使用的。\n\nYou can use other C++ testing frameworks such as Google test, which arguably offer more power and flexibility, particularly when used with Google mock, but can be a bit more fiddly to set up.\n\n**测试驱动开发** ( **TDD** )将单元测试提升到了一个新的层次，并且实际上首先改变了你编写代码的方式。本质上，你先写一个测试。测试最初会失败(事实上，它甚至可能不会构建)，因为您没有实现。然后，编写通过测试所需的最少代码，然后继续编写下一个测试。您以这种方式迭代地构建您的实现，直到您交付了所需的功能块。最后，您将代码重构为所需的标准，使用完成的单元测试来验证重构后的代码仍然按照预期运行。这有时被称为*红绿重构*。\n\n这不是一本关于单元测试的书，也肯定不是关于 TDD 的，所以我们的方法会非常宽松，但是它是现代应用开发的关键部分，知道它如何适合您的 Qt 项目是很重要的。\n\n我们已经演示了将一段简单的数据(欢迎消息)从业务逻辑项目传递到用户界面的机制，所以一如既往，尽可能简单地开始，本章的第一个目标是为该行为编写一个基本的单元测试。完成后，我们将继续测试我们在上一章中实现的数据类。\n\n# 默认的 Qt 方法\n\n当我们创建`cm-tests`项目时，Qt Creator 帮助我们创建了一个`ClientTests`类来使用一个起点，包含一个名为`testCase1`的测试。让我们直接进入并执行这个默认测试，看看会发生什么。然后我们将查看代码并讨论发生了什么。\n\n将运行输出切换到`cm-tests`，编译运行:\n\n![](img/41069538-8323-4ea5-8784-a20ae5b91672.png)\n\n这次您不会看到任何花哨的应用，但是您会在 Qt Creator 的“应用输出”窗格中看到一些文本:\n\n```cpp\n********* Start testing of ClientTests *********\nConfig: Using QtTest library 5.10.0, Qt 5.10.0 (i386-little_endian-ilp32 shared (dynamic) debug build; by GCC 5.3.0)\nPASS : ClientTests::initTestCase()\nPASS : ClientTests::testCase1()\nPASS : ClientTests::cleanupTestCase()\nTotals: 3 passed, 0 failed, 0 skipped, 0 blacklisted, 0ms\n********* Finished testing of ClientTests *********\n```\n\n我们可以看到已经调用了三个方法，第二个是我们默认的单元测试。另外两个函数——`initTestCase()`和`cleanupTestCase()`——是在类中的一组测试之前和之后执行的特殊方法，允许您设置执行测试所需的任何先决条件，然后在之后执行任何清理。三个步骤都通过了。\n\n现在，在`client-tests.cpp`中，添加另一个方法—`testCase2()`—与`testCase1()`相同，但是用`true`条件代替`false`。注意类声明和方法定义都在同一个`.cpp`文件中，所以需要在两个地方都添加方法。再次运行测试:\n\n```cpp\n********* Start testing of ClientTests *********\nConfig: Using QtTest library 5.10.0, Qt 5.10.0 (i386-little_endian-ilp32 shared (dynamic) debug build; by GCC 5.3.0)\nPASS : ClientTests::initTestCase()\nPASS : ClientTests::testCase1()\nFAIL! : ClientTests::testCase2() 'false' returned FALSE. (Failure)\n..\\..\\cm\\cm-tests\\source\\models\\client-tests.cpp(37) : failure location\nPASS : ClientTests::cleanupTestCase()\nTotals: 3 passed, 1 failed, 0 skipped, 0 blacklisted, 0ms\n********* Finished testing of ClientTests *********\n```\n\n这次可以看到`testCase2()`尝试验证 false 为真，当然不是，我们的测试失败了，在这个过程中输出我们的失败消息。`initTestCase()`和`cleanupTestCase()`仍然在测试套件的开始和结束时执行。\n\n现在我们已经看到了通过和失败测试的样子，但是实际上是怎么回事呢？\n\n我们有一个`QObject`派生类`ClientTests`，它实现了一个空的默认构造函数。然后我们有一些方法被宣布为私有`Q_SLOTS`。很像`Q_OBJECT`，这是一个为我们注入了一堆聪明的样板代码的宏，也很像`Q_OBJECT`，使用它不需要担心理解它的内部工作方式。定义为这些私有槽之一的类中的每个方法都作为单元测试来执行。\n\n单元测试方法然后使用`QVERIFY2`宏来验证给定的布尔条件，即真就是真。如果我们在`testCase2`中设计的失败，有用的消息失败将被输出到控制台。\n\n如果有`QVERIFY2`，那么想必一定有`QVERIFY1`，对吧？嗯，差不多有`QVERIFY`，做同样的测试，但是没有故障信息参数。其他常用的宏有`QCOMPARE`，验证同类型的两个参数是否等价，`QVERIFY_EXCEPTION_THROWN`，验证执行给定表达式时是否抛出异常。这听起来可能很奇怪，因为我们不希望我们的代码抛出异常。然而，事情并不总是理想的，我们应该总是编写负测试来验证代码在出现问题时的行为。一个常见的例子是，我们有一个方法，它接受一个指向对象的指针作为参数。我们应该写一个阴性测试，验证如果我们通过了`nullptr`会发生什么(这总是一种可能性，不管你有多小心)。我们可能希望代码愉快地忽略它，不采取进一步的行动，或者我们可能希望抛出某种空参数异常，这就是`QVERIFY_EXCEPTION_THROWN`的作用。\n\n在测试用例定义之后，另一个宏`QTEST_APPLESS_MAIN`抛出一个`main()`钩子来执行测试，最后的`#include`语句将。由构建过程产生的 moc 文件。从 QObject 继承的每个类都将生成一个`companion .moc`文件，包含由`Q_OBJECT`和其他相关宏创建的所有神奇的元数据代码。\n\n现在，如果你在想“为什么你要测试真假？”，那你绝对不会，这完全是一对毫无意义的测试。本练习的目的只是看看 Qt Creator 为我们整合的默认方法是如何工作的，它确实工作了，但是它有一些关键的失败，在我们编写真正的测试之前，我们需要解决这些问题。\n\n第一个问题是`QTEST_APPLESS_MAIN`创建了一个`main()`方法，以便在`ClientTests`运行我们的测试用例。当我们编写另一个测试类时会发生什么？我们会有两种`main()`方法，事情不会顺利。另一个问题是，我们的测试输出只是通过管道传输到应用输出窗格。在业务环境中，构建服务器通常会提取应用代码、执行构建、运行单元测试套件，并标记任何测试失败以供调查。为了做到这一点，构建工具需要能够访问测试输出，并且不能像人一样读取 IDE 中的应用输出窗格。让我们看看解决这些问题的替代方法。\n\n# 定制方法\n\n我们将采用的定制方法仍然适用于我们刚才讨论的相同的基本概念。在它的核心，我们仍然会有一个测试类，它包含一套要执行的单元测试方法。我们所要做的就是用一些额外的样板代码来补充这一点，以允许我们轻松地容纳多个测试类，并将输出传递到文件而不是控制台。\n\n让我们从在源文件夹中向`cm-tests`添加一个新类`TestSuite`开始:\n\n![](img/6e6569fd-21b3-449b-9355-31fd0727841a.png)\n\n`test-suite.h`:\n\n```cpp\n#ifndef TESTSUITE_H\n#define TESTSUITE_H\n\n#include <QObject>\n#include <QString>\n#include <QtTest/QtTest>\n\n#include <vector>\n\nnamespace cm {\n\nclass TestSuite : public QObject\n{\n    Q_OBJECT\npublic:\n    explicit TestSuite(const QString& _testName = \"\");\n    virtual ~TestSuite();\n\n    QString testName;\n    static std::vector<TestSuite*>& testList();\n};\n\n}\n\n#endif\n```\n\n`test-suite.cpp`:\n\n```cpp\n#include \"test-suite.h\"\n\n#include <QDebug>\n\nnamespace cm {\n\nTestSuite::TestSuite(const QString& _testName)\n    : QObject()\n    , testName(_testName)\n{\n    qDebug() << \"Creating test\" << testName;\n    testList().push_back(this);\n    qDebug() << testList().size() << \" tests recorded\";\n}\n\nTestSuite::~TestSuite()\n{\n    qDebug() << \"Destroying test\";\n}\n\nstd::vector<TestSuite*>& TestSuite::testList()\n{\n    static std::vector<TestSuite*> instance = std::vector<TestSuite*>();\n    return instance;\n}\n\n}\n\n```\n\n这里，我们正在创建一个基类，它将用于我们的每个测试类。普通类和测试套件类之间一般是一对一的关系，例如`Client`类和`ClientTests`类。`TestSuite`的每个派生实例都将其自身添加到一个共享向量中。乍一看，这可能有点令人困惑，因此我们也在使用`qDebug()`将一些信息写入控制台，以便您可以了解正在发生的事情。当我们创建源自`TestSuite`的第一个类时，这将更有意义。\n\n接下来，添加一个新的 C++ 源文件`main.cpp`，再次添加到源文件夹:\n\n`main.cpp`:\n\n```cpp\n#include <QtTest/QtTest>\n#include <QDebug>\n\n#include \"test-suite.h\"\n\nusing namespace cm;\n\nint main(int argc, char *argv[])\n{\n    Q_UNUSED(argc);\n    Q_UNUSED(argv);\n\n    qDebug() << \"Starting test suite...\";\n    qDebug() << \"Accessing tests from \" << &TestSuite::testList();\n    qDebug() << TestSuite::testList().size() << \" tests detected\";\n\n    int failedTestsCount = 0;\n\n    for(TestSuite* i : TestSuite::testList()) {\n        qDebug() << \"Executing test \" << i->testName;\n        QString filename(i->testName + \".xml\");\n        int result = QTest::qExec(i, QStringList() << \" \" << \"-o\" << \n                                  filename << \"-xunitxml\");\n        qDebug() << \"Test result \" << result;\n        if(result != 0) {\n            failedTestsCount++ ;\n        }\n    }\n\n    qDebug() << \"Test suite complete - \" << \n          QString::number(failedTestsCount) << \" failures detected.\";\n\n    return failedTestsCount;\n}\n```\n\n这看起来比实际更复杂，因为添加了`qDebug()`语句来获取信息。我们遍历每个注册的测试类，并使用静态`QTest::qExec()`方法来检测和运行其中发现的所有测试。然而，一个关键的补充是，我们为每个类创建一个 XML 文件，并将结果传递给它。\n\n这个机制解决了我们的两个问题。我们现在有了一个单一的`main()`方法来检测和运行我们所有的测试，我们得到了一个单独的 XML 文件，其中包含了我们每个测试套件的输出。但是，在您可以构建项目之前，您需要重新访问`client-tests.cpp`并注释掉或删除`QTEST_APPLESS_MAIN`行，否则我们将回到多个`main()`方法的问题。暂时不要担心`client-tests.cpp`的其他部分；当我们开始测试我们的数据类时，我们将稍后重新讨论它。\n\n立即构建并运行，您将在`Application Output`中获得一组不同的文本:\n\n```cpp\nStarting test suite...\nAccessing tests from 0x40b040\n0 tests detected\nTest suite complete - \"0\" failures detected.\n```\n\n让我们开始实施我们的第一个`TestSuite`。我们有一个`MasterController`类，它向用户界面呈现一个消息字符串，所以让我们编写一个简单的测试来验证消息是否正确。我们需要在`cm-tests`项目中参考`cm-lib`的代码，因此确保相关的`INCLUDE`指令被添加到`cm-tests.pro`中:\n\n```cpp\nINCLUDEPATH += source \\\n    ../cm-lib/source\n```\n\n在`cm-tests/source/controllers`中创建新的名为`MasterControllerTests`的伴随测试类。\n\n`master-controller-tests.h`:\n\n```cpp\n#ifndef MASTERCONTROLLERTESTS_H\n#define MASTERCONTROLLERTESTS_H\n\n#include <QtTest>\n\n#include <controllers/master-controller.h>\n#include <test-suite.h>\n\nnamespace cm {\nnamespace controllers {\n\nclass MasterControllerTests : public TestSuite\n{\n    Q_OBJECT\n\npublic:\n    MasterControllerTests();\n\nprivate slots:\n    /// @brief Called before the first test function is executed\n    void initTestCase();\n    /// @brief Called after the last test function was executed.\n    void cleanupTestCase();\n    /// @brief Called before each test function is executed.\n    void init();\n    /// @brief Called after every test function.\n    void cleanup();\n\nprivate slots:\n    void welcomeMessage_returnsCorrectMessage();\n\nprivate:\n    MasterController masterController;\n};\n\n}}\n\n#endif\n```\n\n我们已经明确添加了`initTestCase()`和`cleanupTestCase()`脚手架方法，因此它们来自哪里并不神秘。为了完整起见，我们还增加了另外两种特殊的脚手架方法:`init()`和`cleanup()`。不同之处在于，这些方法是在每个单独测试之前和之后执行的，而不是在整个测试套件之前和之后。\n\nNone of these methods are doing anything for us and are there just for future reference. They can safely be removed if you want to streamline things.\n\n`master-controller-tests.cpp`:\n\n```cpp\n#include \"master-controller-tests.h\"\n\nnamespace cm {\nnamespace controllers { // Instance\n\nstatic MasterControllerTests instance;\n\nMasterControllerTests::MasterControllerTests()\n    : TestSuite( \"MasterControllerTests\" )\n{\n}\n\n}\n\nnamespace controllers { // Scaffolding\n\nvoid MasterControllerTests::initTestCase()\n{\n}\n\nvoid MasterControllerTests::cleanupTestCase()\n{\n}\n\nvoid MasterControllerTests::init()\n{\n}\n\nvoid MasterControllerTests::cleanup()\n{\n}\n\n}\n\nnamespace controllers { // Tests\n\nvoid MasterControllerTests::welcomeMessage_returnsCorrectMessage()\n{\n    QCOMPARE( masterController.welcomeMessage(), QString(\"Welcome to the Client Management system!\") );\n}\n\n}}\n```\n\n我们又有了一个单一的测试，但这一次，它实际上服务于一些有意义的目的。我们想测试一下，当我们实例化一个`MasterController`对象并访问它的`welcomeMessage`方法时，它会返回我们想要的消息，这个消息将会是欢迎来到客户端管理系统！。\n\n与支架方法不同，测试的命名完全取决于偏好。我倾向于松散地遵循`methodIAmTesting_givenSomeScenario_doesTheCorrectThing`格式，例如:\n\n```cpp\ndivideTwoNumbers_givenTwoValidNumbers_returnsCorrectResult()\ndivideTwoNumbers_givenZeroDivisor_throwsInvalidArgumentException()\n```\n\n我们构建一个`MasterController`的实例作为私有成员变量，我们将使用它进行测试。在实现中，我们通过构造函数指定测试套件的名称，并且我们还创建测试类的静态实例。这是将`MasterControllerTests`添加到我们在`TestSuite`类中看到的静态向量的触发器。\n\n最后，为了实现我们的测试，我们使用`QCOMPARE`宏用我们想要的消息测试我们的`masterController`实例的`welcomeMessage`的值。请注意，由于`QCOMPARE`是一个宏，您不会得到隐式类型转换，因此您需要确保预期结果和实际结果的类型相同。在这里，我们通过从文字文本构建一个`QString`对象来实现这一点。\n\n运行`qmake`，构建并运行以在应用输出窗格中查看我们的测试结果:\n\n```cpp\nCreating test \"MasterControllerTests\"\n1 tests recorded\nStarting test suite...\nAccessing tests from 0x40b040\n1 tests detected\nExecuting test \"MasterControllerTests\"\nTest result 1\nTest suite complete - \"1\" failures detected.\nDestroying test\n```\n\n这从通过静态实例注册`MasterControllerTests`类开始。`main()`方法迭代注册的测试套件集合并找到一个，然后执行该套件中的所有单元测试。测试套件包含一个运行并立即失败的单元测试。这似乎没有以前那么有帮助，因为没有迹象表明哪个测试失败了或者为什么失败。然而，请记住，这个输出只是来自我们为了获得额外信息而添加的`qDebug()`语句；这不是测试执行的真实输出。在`master-controller-tests.cpp`中，我们用`MasterControllerTests`的`testName`参数实例化了`TestSuite`，因此输出将被传送到名为`MasterControllerTests.xml`的文件中。\n\n导航至`cm/binaries`文件夹，并深入查看文件夹，我们将所选配置的项目输出定向到该文件夹，在该文件夹中，您将看到`MasterControllerTests.xml`:\n\n```cpp\n<testsuite name=\"cm::controllers::MasterControllerTests\" tests=\"3\" failures=\"1\" errors=\"0\">\n    <properties>\n       <property name=\"QTestVersion\" value=\"5.10.0\"/>\n       <property name=\"QtVersion\" value=\"5.10.0\"/>\n       <property name=\"QtBuild\" value=\"Qt 5.10.0 (i386-little_endian- \n                 ilp32 shared (dynamic) debug build; by GCC 5.3.0)\"/>\n    </properties>\n    <testcase name=\"initTestCase\" result=\"pass\"/>\n    <testcase name=\"welcomeMessage_returnsCorrectMessage\" \n                    result=\"fail\">\n    <failure result=\"fail\" message=\"Compared values are not the same Actual (masterController.welcomeMessage) : \"This is MasterController to Major Tom\" Expected (QString(\"Welcome to the Client Management system!\")): \"Welcome to the Client Management system!\"\"/>\n    </testcase>\n    <testcase name=\"cleanupTestCase\" result=\"pass\"/>\n    <system-err/>\n</testsuite>\n```\n\n在这里，我们有测试的完整输出，您可以看到失败是因为我们从`masterController`获得的欢迎消息是这是主控制器给汤姆少校，我们期望欢迎到客户管理系统！。\n\n`MasterController`的表现不如预期，我们发现了一个 bug，所以前往`master-controller.cpp`解决问题:\n\n```cpp\nQString welcomeMessage = \"Welcome to the Client Management system!\";\n```\n\n重建两个项目，再次执行测试，享受 100%通过率的荣耀:\n\n```cpp\nCreating test \"MasterControllerTests\"\n1 tests recorded\nStarting test suite...\nAccessing tests from 0x40b040\n1 tests detected\nExecuting test \"MasterControllerTests\"\nTest result 0\nTest suite complete - \"0\" failures detected.\nDestroying test\n```\n\n现在我们已经建立了测试框架，让我们测试一些比简单的字符串消息更复杂的东西，并验证我们在上一章中所做的工作。\n\n# 数据装饰测试\n\n在[第 5 章](5.html)、*数据*中，我们创建了从`DataDecorator`派生的各种类。让我们为其中的每一个创建配套的测试类，并测试以下功能:\n\n*   物体结构\n*   设置值\n*   获取值为 JSON\n*   从 JSON 更新值\n\n在`cm-tests/source/data`中，创建`DateTimeDecoratorTests`、`EnumeratorDecoratorTests`、`IntDecoratorTests`和`StringDecoratorTests`类。\n\n让我们从最简单的套件`IntDecoratorTests`开始。测试在各个套件中大致相似，因此一旦我们编写了一个套件，我们就可以将其中的大部分复制到其他套件中，然后根据需要进行补充。\n\n`int-decorator-tests.h`:\n\n```cpp\n#ifndef INTDECORATORTESTS_H\n#define INTDECORATORTESTS_H\n\n#include <QtTest>\n\n#include <data/int-decorator.h>\n#include <test-suite.h>\n\nnamespace cm {\nnamespace data {\n\nclass IntDecoratorTests : public TestSuite\n{\n    Q_OBJECT\n\npublic:\n    IntDecoratorTests();\n\nprivate slots:\n    void constructor_givenNoParameters_setsDefaultProperties();\n    void constructor_givenParameters_setsProperties();\n    void setValue_givenNewValue_updatesValueAndEmitsSignal();\n    void setValue_givenSameValue_takesNoAction();\n    void jsonValue_whenDefaultValue_returnsJson();\n    void jsonValue_whenValueSet_returnsJson();\n    void update_whenPresentInJson_updatesValue();\n    void update_whenNotPresentInJson_updatesValueToDefault();\n};\n\n}}\n\n#endif\n```\n\n一种常见的方法是遵循“方法作为一个单元”的方法，其中每个方法都是类中最小的可测试单元，然后以多种方式测试该单元。所以我们从测试构造函数开始，包括有参数和没有参数。`setValue()`方法应该只在我们实际改变值时做任何事情，所以我们测试设置不同的值和相同的值。接下来，我们测试可以将装饰器转换为 JSON 值，既有默认值(`int`的情况下为`0`)也有设置值。最后，我们针对`update()`方法进行了一些测试。如果我们传入一个包含该属性的 JSON，那么我们期望该值根据 JSON 值进行更新。但是，如果 JSON 中缺少该属性，我们希望该类能够很好地处理它，并重置为默认值。\n\n请注意，我们没有明确测试`value()`方法。这只是一个没有副作用的简单访问器方法，我们将在其他单元测试中调用它，所以我们将在那里间接测试它。如果你愿意，可以为它创建额外的测试。\n\n`int-decorator-tests.cpp`:\n\n```cpp\n#include \"int-decorator-tests.h\"\n\n#include <QSignalSpy>\n\n#include <data/entity.h>\n\nnamespace cm {\nnamespace data { // Instance\n\nstatic IntDecoratorTests instance;\n\nIntDecoratorTests::IntDecoratorTests()\n    : TestSuite( \"IntDecoratorTests\" )\n{\n}\n\n}\n\nnamespace data { // Tests\n\nvoid IntDecoratorTests::constructor_givenNoParameters_setsDefaultProperties()\n{\n    IntDecorator decorator;\n    QCOMPARE(decorator.parentEntity(), nullptr);\n    QCOMPARE(decorator.key(), QString(\"SomeItemKey\"));\n    QCOMPARE(decorator.label(), QString(\"\"));\n    QCOMPARE(decorator.value(), 0);\n}\n\nvoid IntDecoratorTests::constructor_givenParameters_setsProperties()\n{\n    Entity parentEntity;\n    IntDecorator decorator(&parentEntity, \"Test Key\", \"Test Label\", \n                                                       99);\n    QCOMPARE(decorator.parentEntity(), &parentEntity);\n    QCOMPARE(decorator.key(), QString(\"Test Key\"));\n    QCOMPARE(decorator.label(), QString(\"Test Label\"));\n    QCOMPARE(decorator.value(), 99);\n}\n\nvoid IntDecoratorTests::setValue_givenNewValue_updatesValueAndEmitsSignal()\n{\n    IntDecorator decorator;\n    QSignalSpy valueChangedSpy(&decorator, \n                               &IntDecorator::valueChanged);\n    QCOMPARE(decorator.value(), 0);\n    decorator.setValue(99);\n    QCOMPARE(decorator.value(), 99);\n    QCOMPARE(valueChangedSpy.count(), 1);\n}\n\nvoid IntDecoratorTests::setValue_givenSameValue_takesNoAction()\n{\n    Entity parentEntity;\n    IntDecorator decorator(&parentEntity, \"Test Key\", \"Test Label\", \n                                                               99);\n    QSignalSpy valueChangedSpy(&decorator, \n                               &IntDecorator::valueChanged);\n    QCOMPARE(decorator.value(), 99);\n    decorator.setValue(99);\n    QCOMPARE(decorator.value(), 99);\n    QCOMPARE(valueChangedSpy.count(), 0);\n}\n\nvoid IntDecoratorTests::jsonValue_whenDefaultValue_returnsJson()\n{\n    IntDecorator decorator;\n    QCOMPARE(decorator.jsonValue(), QJsonValue(0));\n}\nvoid IntDecoratorTests::jsonValue_whenValueSet_returnsJson()\n{\n    IntDecorator decorator;\n    decorator.setValue(99);\n    QCOMPARE(decorator.jsonValue(), QJsonValue(99));\n}\n\nvoid IntDecoratorTests::update_whenPresentInJson_updatesValue()\n{\n    Entity parentEntity;\n    IntDecorator decorator(&parentEntity, \"Test Key\", \"Test Label\", 99);\n    QSignalSpy valueChangedSpy(&decorator, \n                               &IntDecorator::valueChanged);\n    QCOMPARE(decorator.value(), 99);\n    QJsonObject jsonObject;\n    jsonObject.insert(\"Key 1\", \"Value 1\");\n    jsonObject.insert(\"Test Key\", 123);\n    jsonObject.insert(\"Key 3\", 3);\n    decorator.update(jsonObject);\n    QCOMPARE(decorator.value(), 123);\n    QCOMPARE(valueChangedSpy.count(), 1);\n}\n\nvoid IntDecoratorTests::update_whenNotPresentInJson_updatesValueToDefault()\n{\n    Entity parentEntity;\n    IntDecorator decorator(&parentEntity, \"Test Key\", \"Test Label\", \n                                                                99);\n    QSignalSpy valueChangedSpy(&decorator, \n                               &IntDecorator::valueChanged);\n    QCOMPARE(decorator.value(), 99);\n    QJsonObject jsonObject;\n    jsonObject.insert(\"Key 1\", \"Value 1\");\n    jsonObject.insert(\"Key 2\", 123);\n    jsonObject.insert(\"Key 3\", 3);\n    decorator.update(jsonObject);\n    QCOMPARE(decorator.value(), 0);\n    QCOMPARE(valueChangedSpy.count(), 1);\n}\n\n}}\n```\n\n单元测试倾向于遵循*安排>行为>断言*模式。测试的前提条件首先得到满足:变量被初始化，类被配置，等等。然后，执行一个操作，通常调用被测试的函数。最后，检查动作的结果。有时这些步骤中的一个或多个不是必需的，或者可以与另一个合并，但这是一般的模式。\n\n我们通过初始化一个新的`IntDecorator`而不传入任何参数来开始测试构造函数，然后使用`QCOMPARE`测试对象的各种属性是否已经初始化为预期的默认值，以使实际值与预期值相匹配。然后，我们重复测试，但这一次，我们传入每个参数的值，并验证它们已经在实例中更新。\n\n测试`setValue()`方法时，需要检查`valueChanged()`信号是否发出。我们可以通过将 lambda 连接到调用时设置标志的信号来实现这一点，如下所示:\n\n```cpp\nbool isCalled = false;\nQObject::connect(&decorator, &IntDecorator::valueChanged, [&isCalled](){\n    isCalled = true;\n});\n\n/*...Perform action...*/ \n\nQVERIFY(isCalled);\n```\n\n然而，我们在这里使用的一个简单得多的解决方案是使用 Qt 的`QSignalSpy`类来跟踪对指定信号的调用。然后，我们可以使用`count()`方法检查一个信号被调用了多少次。\n\n第一个`setValue()`测试确保当我们提供一个不同于现有值的新值时，该值被更新并且`valueChanged()`信号被发出一次。第二个测试确保当我们设置相同的值时，不采取任何行动，也不发出信号。请注意，在这两种情况下，我们都使用额外的`QCOMPARE`调用来断言该值是我们在采取行动之前所期望的值。考虑以下伪测试:\n\n1.  建立你的班级。\n2.  执行动作。\n3.  测试该值为`99`。\n\n如果一切正常，第 1 步将值设置为`0`，第 2 步采取正确的动作，将值更新为`99`，第 3 步通过，因为值为`99`。然而，步骤 1 可能有错误，将值错误地设置为`99`，步骤 2 甚至没有执行，也没有采取任何行动，然而步骤 3(和测试)通过了，因为值是`99`。通过步骤 1 后的`QCOMPARE`前提条件，这是可以避免的。\n\n`jsonValue()`测试是简单的相等检查，既有默认值也有设定值。\n\n最后，通过`update()`测试，我们构建了几个 JSON 对象。在一个对象中，我们添加了一个与装饰器对象具有相同键的项目(“测试键”)，我们希望匹配该项目，并将关联值(`123`)传递给`setValue()`。在第二个对象中，键不存在。在这两种情况下，我们还添加了其他无关的项，以确保类可以正确地忽略它们。动作后检查与`setValue()`测试相同。\n\n`StringDecoratorTests`类本质上与`IntDecoratorTests`相同，只是值数据类型和空字符串的默认值`\"\"`不同于`0`。\n\n`DateTimeDecorator`也遵循相同的模式，但是对字符串格式化辅助方法`toIso8601String()`进行了额外的测试，等等。\n\n`EnumeratorDecoratorTests`执行相同的测试，但是由于需要一个枚举器和相关的映射器，需要更多的设置。在测试的主体中，每当我们测试`value()`时，我们也需要测试`valueDescription()`以确保两者保持一致。例如，每当值为`eTestEnum::Value2`时，`valueDescription()`必须为`Value 2`。请注意，我们总是将枚举值与`value()`检查和`static_cast`检查结合使用，并将它们转换为`int`。考虑以下示例:\n\n```cpp\nQCOMPARE(decorator.value(), static_cast<int>(eTestEnum::Value2));\n```\n\n仅仅使用原始的`int`值可能会很有诱惑力:\n\n```cpp\nQCOMPARE(decorator.value(), 2);\n```\n\n除了数字 2 对代码读者的意义比枚举的`Value2`小得多之外，这种方法的问题在于`eTestEnum`的值可能会改变并使测试无效。考虑这个例子:\n\n```cpp\nenum eTestEnum {\n    Unknown = 0,\n    MyAmazingNewTestValue,\n    Value1,\n    Value2,\n    Value3\n};\n```\n\n由于`MyAmazingNewTestValue`的插入，`Value2`的数字等价物实际上现在是 3。任何用数字 2 表示`Value2`的测试现在都是错误的，而那些用更冗长的`static_cast<int>(eTestEnum::Value2)`的测试仍然是正确的。\n\n重新构建并运行新的测试套件，它们都应该愉快地通过，并给我们之前编写的代码带来新的信心。测试完数据装饰器后，接下来让我们继续讨论数据模型。\n\n# 实体测试\n\n既然我们对我们的数据装饰器是否如预期的那样工作有了一些信心，让我们更上一层楼，测试我们的数据实体。客户机类是我们模型层次结构的根，通过测试它，我们可以在过程中测试我们的其他模型。\n\n在`cm-tests/source/models`中我们已经有了`client-tests.cpp`，这是 Qt Creator 在我们创建项目时为我们添加的，所以继续添加一个配套头文件`client-tests.h`。\n\n`client-tests.h`:\n\n```cpp\n#ifndef CLIENTTESTS_H\n#define CLIENTTESTS_H\n\n#include <QtTest>\n#include <QJsonObject>\n\n#include <models/client.h>\n#include <test-suite.h>\n\nnamespace cm {\nnamespace models {\n\nclass ClientTests : public TestSuite\n{\n    Q_OBJECT\n\npublic:\n    ClientTests();\n\nprivate slots:\n    void constructor_givenParent_setsParentAndDefaultProperties();\n    void constructor_givenParentAndJsonObject_setsParentAndProperties();\n    void toJson_withDefaultProperties_constructsJson();\n    void toJson_withSetProperties_constructsJson();\n    void update_givenJsonObject_updatesProperties();\n    void update_givenEmptyJsonObject_updatesPropertiesToDefaults();\n\nprivate:\n    void verifyBillingAddress(const QJsonObject& jsonObject);\n    void verifyDefaultBillingAddress(const QJsonObject& jsonObject);\n    void verifyBillingAddress(Address* address);\n    void verifyDefaultBillingAddress(Address* address);\n    void verifySupplyAddress(const QJsonObject& jsonObject);\n    void verifyDefaultSupplyAddress(const QJsonObject& jsonObject);\n    void verifySupplyAddress(Address* address);\n    void verifyDefaultSupplyAddress(Address* address);\n    void verifyAppointments(const QJsonObject& jsonObject);\n    void verifyDefaultAppointments(const QJsonObject& jsonObject);\n    void verifyAppointments(const QList<Appointment*>& appointments);\n    void verifyDefaultAppointments(const QList<Appointment*>& appointments);\n    void verifyContacts(const QJsonObject& jsonObject);\n    void verifyDefaultContacts(const QJsonObject& jsonObject);\n    void verifyContacts(const QList<Contact*>& contacts);\n    void verifyDefaultContacts(const QList<Contact*>& contacts);\n\n    QByteArray jsonByteArray = R\"(\n    {\n        \"reference\": \"CM0001\",\n        \"name\": \"Mr Test Testerson\",\n        \"billingAddress\": {\n            \"building\": \"Billing Building\",\n            \"city\": \"Billing City\",\n            \"postcode\": \"Billing Postcode\",\n            \"street\": \"Billing Street\"\n        },\n        \"appointments\": [\n         {\"startAt\": \"2017-08-20T12:45:00\", \"endAt\": \"2017-08-\n                      20T13:00:00\", \"notes\": \"Test appointment 1\"},\n         {\"startAt\": \"2017-08-21T10:30:00\", \"endAt\": \"2017-08-\n                      21T11:30:00\", \"notes\": \"Test appointment 2\"}\n        ],\n        \"contacts\": [\n            {\"contactType\": 2, \"address\":\"email@test.com\"},\n            {\"contactType\": 1, \"address\":\"012345678\"}\n        ],\n        \"supplyAddress\": {\n            \"building\": \"Supply Building\",\n            \"city\": \"Supply City\",\n            \"postcode\": \"Supply Postcode\",\n            \"street\": \"Supply Street\"\n        }\n    })\";\n};\n\n}}\n\n#endif\n```\n\n我们要在这里测试三个主要方面:\n\n*   物体结构\n*   序列化到 JSON\n*   从 JSON 反序列化\n\n与以前的套件一样，我们对每个领域都有两种不同样式的测试——一种带有默认数据，另一种带有指定数据。在私有部分，您将看到许多验证方法。它们将封装测试我们数据的特定子集所需的功能。这样做的优点与常规代码相同:它们使单元测试更加简洁和可读，并且它们允许验证规则的简单重用。此外，在私有部分，我们定义了一个可以用来构建客户端实例的 JSON blob。顾名思义，一个`QByteArray`只是一个字节数组，附带了许多相关的有用功能:\n\n```cpp\nvoid ClientTests::constructor_givenParent_setsParentAndDefaultProperties()\n{\n    Client testClient(this);\n    QCOMPARE(testClient.parent(), this);\n    QCOMPARE(testClient.reference->value(), QString(\"\"));\n    QCOMPARE(testClient.name->value(), QString(\"\"));\n\n    verifyDefaultBillingAddress(testClient.billingAddress);\n    verifyDefaultSupplyAddress(testClient.supplyAddress);\n    verifyDefaultAppointments(testClient.appointments-\n                              >derivedEntities());\n    verifyDefaultContacts(testClient.contacts->derivedEntities());\n}\n\nvoid ClientTests::constructor_givenParentAndJsonObject_setsParentAndProperties()\n{\n    Client testClient(this, QJsonDocument::fromJson(jsonByteArray).object());\n    QCOMPARE(testClient.parent(), this);\n    QCOMPARE(testClient.reference->value(), QString(\"CM0001\"));\n    QCOMPARE(testClient.name->value(), QString(\"Mr Test Testerson\"));\n\n    verifyBillingAddress(testClient.billingAddress);\n    verifySupplyAddress(testClient.supplyAddress);\n    verifyAppointments(testClient.appointments->derivedEntities());\n    verifyContacts(testClient.contacts->derivedEntities());\n}\n```\n\n从构造函数测试开始，我们实例化一个新的客户机，包括和不包括 JSON 对象。请注意，为了将我们的 JSON 字节数组转换为`QJsonObject`，我们需要通过`QJsonDocument`传递它。一旦我们初始化了客户端，我们就检查 name 属性，并使用 verify 方法为我们测试子对象的状态。无论我们是否通过 JSON 对象提供任何初始数据，我们都希望自动为我们创建`supplyAddress`和`billingAddress`对象以及约会和联系人集合。默认情况下，集合应该为空:\n\n```cpp\nvoid ClientTests::toJson_withDefaultProperties_constructsJson()\n{\n    Client testClient(this);\n    QJsonDocument jsonDoc(testClient.toJson());\n    QVERIFY(jsonDoc.isObject());\n    QJsonObject jsonObject = jsonDoc.object();\n    QVERIFY(jsonObject.contains(\"reference\"));\n    QCOMPARE(jsonObject.value(\"reference\").toString(), QString(\"\"));\n    QVERIFY(jsonObject.contains(\"name\"));\n    QCOMPARE(jsonObject.value(\"name\").toString(), QString(\"\"));\n    verifyDefaultBillingAddress(jsonObject);\n    verifyDefaultSupplyAddress(jsonObject);\n    verifyDefaultAppointments(jsonObject);\n    verifyDefaultContacts(jsonObject);\n}\n\nvoid ClientTests::toJson_withSetProperties_constructsJson()\n{\n    Client testClient(this, QJsonDocument::fromJson(jsonByteArray).object());\n    QCOMPARE(testClient.reference->value(), QString(\"CM0001\"));\n    QCOMPARE(testClient.name->value(), QString(\"Mr Test Testerson\"));\n\n    verifyBillingAddress(testClient.billingAddress);\n    verifySupplyAddress(testClient.supplyAddress);\n    verifyAppointments(testClient.appointments->derivedEntities());\n    verifyContacts(testClient.contacts->derivedEntities());\n    QJsonDocument jsonDoc(testClient.toJson());\n    QVERIFY(jsonDoc.isObject());\n    QJsonObject jsonObject = jsonDoc.object();\n    QVERIFY(jsonObject.contains(\"reference\"));\n    QCOMPARE(jsonObject.value(\"reference\").toString(), QString(\"CM0001\"));\n    QVERIFY(jsonObject.contains(\"name\"));\n    QCOMPARE(jsonObject.value(\"name\").toString(), QString(\"Mr Test \n                                                  Testerson\"));\n    verifyBillingAddress(jsonObject);\n    verifySupplyAddress(jsonObject);\n    verifyAppointments(jsonObject);\n    verifyContacts(jsonObject);\n}\n```\n\n`toJson()`测试遵循大致相同的模式。我们构造一个没有 JSON 对象的对象，这样我们就可以获得所有属性和子对象的默认值。然后，我们立即使用对构造函数中的`toJson()`的调用来构造一个`QJsonDocument`，从而为我们获得序列化的 JSON 对象。测试`name`特性，然后再次使用验证方法。当使用 JSON 构建**客户端**时，我们添加了前提条件检查，以确保在再次调用`toJson()`并测试结果之前，我们的属性已经被正确设置:\n\n```cpp\nvoid ClientTests::update_givenJsonObject_updatesProperties()\n{\n    Client testClient(this);\n    testClient.update(QJsonDocument::fromJson(jsonByteArray).object());\n    QCOMPARE(testClient.reference->value(), QString(\"CM0001\"));\n    QCOMPARE(testClient.name->value(), QString(\"Mr Test Testerson\"));\n\n    verifyBillingAddress(testClient.billingAddress);\n    verifySupplyAddress(testClient.supplyAddress);\n    verifyAppointments(testClient.appointments->derivedEntities());\n    verifyContacts(testClient.contacts->derivedEntities());\n}\n\nvoid ClientTests::update_givenEmptyJsonObject_updatesPropertiesToDefaults()\n{\n    Client testClient(this, QJsonDocument::fromJson(jsonByteArray).object());\n    QCOMPARE(testClient.reference->value(), QString(\"CM0001\"));\n    QCOMPARE(testClient.name->value(), QString(\"Mr Test Testerson\"));\n    verifyBillingAddress(testClient.billingAddress);\n    verifySupplyAddress(testClient.supplyAddress);\n    verifyAppointments(testClient.appointments->derivedEntities());\n    verifyContacts(testClient.contacts->derivedEntities());\n    testClient.update(QJsonObject());\n    QCOMPARE(testClient.reference->value(), QString(\"\"));\n    QCOMPARE(testClient.name->value(), QString(\"\"));\n\n    verifyDefaultBillingAddress(testClient.billingAddress);\n    verifyDefaultSupplyAddress(testClient.supplyAddress);\n    verifyDefaultAppointments(testClient.appointments-\n                              >derivedEntities());\n    verifyDefaultContacts(testClient.contacts->derivedEntities());\n}\n```\n\n`update()`测试与`toJson()`相同，但反过来。这一次，我们使用字节数组构造了一个 JSON 对象，并将其传递给`update()`，之后检查模型的状态。\n\n各种私有验证方法都是简单的检查集，这样我们就不必一遍又一遍地重复相同的代码。考虑给定的示例:\n\n```cpp\nvoid ClientTests::verifyDefaultSupplyAddress(Address* address)\n{\n    QVERIFY(address != nullptr);\n    QCOMPARE(address->building->value(), QString(\"\"));\n    QCOMPARE(address->street->value(), QString(\"\"));\n    QCOMPARE(address->city->value(), QString(\"\"));\n    QCOMPARE(address->postcode->value(), QString(\"\"));\n}\n```\n\n再次构建并运行单元测试，新的**客户端**测试应该会顺利通过。\n\n# 嘲弄的\n\n到目前为止，我们编写的单元测试都非常简单。虽然我们的**客户端**类不是完全独立的，但是它的依赖关系是它可以拥有和随意改变的所有其他数据模型和装饰器。但是，展望未来，我们希望将客户端数据保存在数据库中。让我们看几个例子来说明这是如何工作的，并讨论我们做出的设计决策如何影响客户端类的可测试性。\n\n打开`scratchpad`项目，创建一个新的头文件`mocking.h`，在这里我们将实现一个虚拟的客户端类来玩。\n\n`mocking.h`:\n\n```cpp\n#ifndef MOCKING_H\n#define MOCKING_H\n\n#include <QDebug>\n\nclass Client\n{\npublic:\n    void save()\n    {\n        qDebug() << \"Saving Client\";\n    }\n};\n\n#endif\n```\n\n在`main.cpp`、`#include <mocking.h>`中，更新`engine.load()`行，加载默认的`main.qml`，如果还没有的话，添加几行，旋转并保存一个虚拟的客户端对象:\n\n```cpp\nengine.load(QUrl(QStringLiteral(\"qrc:/main.qml\")));\n\nClient client;\nclient.save();\n```\n\n构建并运行应用，忽略窗口，并查看应用输出控制台:\n\n```cpp\nSaving Client\n```\n\n我们有一种方法可以让客户端保存自己，但是它也需要一个数据库来保存自己。让我们将数据库管理功能封装到一个`DatabaseController`类中。在嘲讽. h 中，在 Client 类之前添加以下实现。请注意，您需要转发声明客户端:\n\n```cpp\nclass Client;\n\nclass DatabaseController\n{\npublic:\n    DatabaseController()\n    {\n        qDebug() << \"Creating a new database connection\";\n    }\n\n    void save(Client* client)\n    {\n        qDebug() << \"Saving a Client to the production database\";\n    }\n};\n```\n\n现在，编辑客户端类:\n\n```cpp\nclass Client\n{\n    DatabaseController databaseController;\n\npublic:\n    void save()\n    {\n        qDebug() << \"Saving Client\";\n        databaseController.save(this);\n    }\n};\n```\n\n回到`main.cpp`，用以下内容替换客户行:\n\n```cpp\nqDebug() << \"Running the production code...\";\n\nClient client1;\nclient1.save();\nClient client2;\nclient2.save();\n```\n\n现在我们创建并保存了两个客户端，而不仅仅是一个。再次构建、运行和检查控制台:\n\n```cpp\nRunning the production code…\nCreating a new database connection\nSaving Client\nSaving a Client to the production database\nCreating a new database connection\nSaving Client\nSaving a Client to the production database\n```\n\n好了，现在我们将客户机保存到生产数据库中，但是我们正在为每个客户机创建一个新的数据库连接，这似乎有点浪费。Client 类需要一个`DatabaseController`的实例才能运行，这就是所谓的依赖。但是，我们不需要客户端负责创建该实例；相反，我们可以通过构造函数传递——或者*注入—* 实例，并在其他地方管理`DatabaseController`的生命周期。这种依赖注入技术是一种更广泛的设计模式的形式，称为**控制反转**。让我们将对共享的`DatabaseController`的引用传递到我们的客户端类中:\n\n```cpp\nclass Client\n{\n    DatabaseController& databaseController;\n\npublic:\n    Client(DatabaseController& _databaseController)\n        : databaseController(_databaseController)\n    {\n    }\n\n    void save()\n    {\n        qDebug() << \"Saving Client\";\n        databaseController.save(this);\n    }\n};\n```\n\n`main.cpp`结束:\n\n```cpp\nqDebug() << \"Running the production code...\";\n\nDatabaseController databaseController;\n\nClient client1(databaseController);\nclient1.save();\nClient client2(databaseController);\nclient2.save();\n```\n\n构建并运行以下内容:\n\n```cpp\nRunning the production code…\nCreating a new database connection\nSaving Client\nSaving a Client to the production database\nSaving Client\nSaving a Client to the production database\n```\n\n太好了，我们有一个高效的解耦系统架构；我们来测试一下。\n\n在`mocking.h`中，在 Client 类后添加一个假装测试套件:\n\n```cpp\nclass ClientTestSuite\n{\npublic:\n    void saveTests()\n    {\n        DatabaseController databaseController;\n        Client client1(databaseController);\n        client1.save();\n        Client client2(databaseController);\n        client2.save();\n\n        qDebug() << \"Test passed!\";\n    }\n};\n```\n\n在`main.cpp`中，保存`client2`后，添加以下内容来运行我们的测试:\n\n```cpp\nqDebug() << \"Running the test code...\";\n\nClientTestSuite testSuite;\ntestSuite.saveTests();\n```\n\n构建并运行以下内容:\n\n```cpp\nRunning the production code...\nCreating a new database connection\nSaving Client\nSaving a Client to the production database\nSaving Client\nSaving a Client to the production database\nRunning the test code...\nCreating a new database connection\nSaving Client\nSaving a Client to the production database\nSaving Client\nSaving a Client to the production database\nTest passed!\n```\n\n我们的测试通过了，太棒了！有什么不爱的？事实上，我们刚刚将一些测试数据保存到我们的生产数据库中。\n\n如果您还没有为您的大多数类实现接口，那么在您开始单元测试之后，您很快就会实现接口。它不仅仅用于避免像将测试数据写入生产数据库这样的讨厌的副作用；它允许你模拟各种各样的行为，使得单元测试变得更加容易。\n\n所以，让我们把`DatabaseController`移到一个界面后面。将`mocking.h`中的普通`DatabaseController`替换为增压界面驱动版本:\n\n```cpp\nclass IDatabaseController\n{\npublic:\n    virtual ~IDatabaseController(){}\n    virtual void save(Client* client) = 0;\n};\n\nclass DatabaseController : public IDatabaseController\n{\npublic:\n    DatabaseController()\n    {\n        qDebug() << \"Creating a new database connection\";\n    }\n\n    void save(Client* client) override\n    {\n        qDebug() << \"Saving a Client to the production database\";\n    }\n};\n```\n\n有了这个接口，我们现在可以创建一个假的或模拟的实现:\n\n```cpp\nclass MockDatabaseController : public IDatabaseController\n{\npublic:\n    MockDatabaseController()\n    {\n        qDebug() << \"Absolutely not creating any database connections \n                                                           at all\";\n    }\n\n    void save(Client* client) override\n    {\n        qDebug() << \"Just testing - not saving any Clients to any \n                                                   databases\";\n    }\n};\n```\n\n接下来，调整我们的客户端以保存对接口的引用，而不是具体的实现:\n\n```cpp\nclass Client\n{\n    IDatabaseController& databaseController;\n\npublic:\n    Client(IDatabaseController& _databaseController)\n        : databaseController(_databaseController)\n    {\n    }\n\n    void save()\n    {\n        qDebug() << \"Saving Client\";\n        databaseController.save(this);\n    }\n};\n```\n\n最后，更改我们的测试套件，创建一个模拟控制器传递给客户端:\n\n```cpp\nvoid saveTests()\n{\n    MockDatabaseController databaseController;\n    ...\n}\n```\n\n构建并运行以下内容:\n\n```cpp\nRunning the production code...\nCreating a new database connection\nSaving Client\nSaving a Client to the production database\nSaving Client\nSaving a Client to the production database\nRunning the test code...\nAbsolutely not creating any database connections at all\nSaving Client\nJust testing - not saving any Clients to any databases\nSaving Client\nJust testing - not saving any Clients to any databases\nTest passed!\n```\n\n太好了。通过编程接口和注入依赖，我们可以安全地进行隔离测试。我们可以根据需要创建任意多的模拟实现，并使用它们来模拟我们想要的任何行为，使我们能够测试多个不同的场景。一旦你更多地参与到模仿中，使用像**谷歌模仿**这样的专用框架确实是有好处的，因为它们为你省去了编写一堆样板模仿类的麻烦。您可以使用助手宏轻松地模拟一次接口，然后动态地指定各个方法的行为。\n\n# 摘要\n\n在这一章中，我们已经第一次正确地研究了单元测试项目，您已经看到了如何使用 Qt Test 框架实现单元测试。我们还讨论了编程到接口以支持模仿的重要性。现在我们已经为我们的主要数据类准备好了单元测试，所以如果我们不小心改变了行为，单元测试将会失败，并为我们突出一个潜在的问题。\n\n正如我们所讨论的，这不是一本关于测试驱动开发的书，我们有时会抄近路，违背本章中的建议来保持其他概念的解释尽可能简单，但是我确实敦促您尽可能在您的项目中实现某种类型的单元测试，因为这是一个非常有价值的实践，总是值得额外的时间投资。一些开发人员喜欢成熟的 TDD 的严谨性，而另一些开发人员更喜欢事后编写单元测试来验证他们所做的工作。找到一种适合你和你的编码样式的方法。\n\n我们会偶尔回到测试项目来演示某些行为。但是我们肯定不会实现 100%的代码覆盖率。现在您已经有了测试项目和支架，这只是为您想要测试的每个类添加更多测试类的一个例子。只要你像我们在本章中一样从`TestSuite`继承，它们就会在你运行测试项目时被自动检测并执行。\n\n在[第 7 章](7.html)、*持久性*中，我们将继续实现刚才讨论的功能——将数据持久化到数据库中。"
  },
  {
    "path": "docs/learn-qt5/7.md",
    "content": "# 七、SQLite\n\n在[第 5 章](5.html)、*数据*中，我们创建了一个框架来捕获和保存内存中的数据。然而，这只是故事的一半，因为如果不将数据保存到某个外部目的地，一旦我们关闭应用，数据就会丢失。在本章中，我们将在之前工作的基础上，将数据保存到 SQLite 数据库中的磁盘上，以便它可以在应用的生命周期之外继续运行。保存后，我们还将构建查找、编辑和删除数据的方法。为了在我们的各种数据模型中免费获得所有这些操作，我们将扩展我们的数据实体，以便它们可以自动加载并保存到我们的数据库中，而无需我们在每个类中编写样板代码。我们将涵盖以下主题:\n\n*   SQLite\n*   主键\n*   创建客户端\n*   寻找客户\n*   编辑客户端\n*   删除客户端\n\n# SQLite\n\n近年来，随着 NoSQL 和 Graph 数据库的爆炸式增长，通用数据库技术已经支离破碎。然而，在许多应用中，SQL 数据库仍然是非常合适的选择。Qt 内置了对几种 SQL 数据库驱动程序类型的支持，并且可以通过自定义驱动程序进行扩展。MySQL 和 PostgreSQL 都是非常流行的开源 SQL 数据库引擎，默认情况下都支持，但它们是打算在服务器上使用的，需要管理，这使得它们对于我们的目的来说有点不必要的复杂。相反，我们将使用轻量级得多的 SQLite，它通常用作客户端数据库，并且由于占地面积小而在移动应用中非常受欢迎。\n\n根据 https://www.sqlite.org 官方网站“SQLite 是一个独立的、高可靠性的、嵌入式的、全功能的、公共领域的 SQL 数据库引擎。SQLite 是世界上使用最多的数据库引擎”。与 Qt 的 SQL 相关类结合在一起，创建一个数据库并存储您的数据非常容易。\n\n我们需要做的第一件事是将 SQL 模块添加到我们的库项目中，以访问 Qt 的所有 SQL 优点。在`cm-lib.pro`中，增加以下内容:\n\n```cpp\nQT += sql\n```\n\n接下来，我们将利用上一章中讨论的内容，在接口后面实现我们的数据库相关功能。在`cm-lib/source/controllers`中创建新的`i-database-controller.h`头文件:\n\n```cpp\n#ifndef IDATABASECONTROLLER_H\n#define IDATABASECONTROLLER_H\n\n#include <QJsonArray>\n#include <QJsonObject>\n#include <QList>\n#include <QObject>\n#include <QString>\n\n#include <cm-lib_global.h>\n\nnamespace cm {\nnamespace controllers {\n\nclass CMLIBSHARED_EXPORT IDatabaseController : public QObject\n{\n    Q_OBJECT\n\npublic:\n    IDatabaseController(QObject* parent) : QObject(parent){}\n    virtual ~IDatabaseController(){}\n\n    virtual bool createRow(const QString& tableName, const QString& id, \n                           const QJsonObject& jsonObject) const = 0;\n    virtual bool deleteRow(const QString& tableName, const QString& id) \n                                                     const = 0;\n    virtual QJsonArray find(const QString& tableName, const QString& \n                                           searchText) const = 0;\n    virtual QJsonObject readRow(const QString& tableName, const \n                                      QString& id) const = 0;\n    virtual bool updateRow(const QString& tableName, const QString& id, \n                           const QJsonObject& jsonObject) const = 0;\n};\n\n}}\n\n#endif\n```\n\n在这里，我们实现了(**创建**、**读取**、**更新**、**删除** ) **CRUD** 的四个基本功能，这些功能一般都与持久存储相关，而不仅仅是 SQL 数据库。我们用一个额外的`find()`方法来补充这些函数，我们将使用该方法根据提供的搜索文本来查找匹配客户端的数组。\n\n现在，让我们创建一个接口的具体实现。在`cm-lib/source/controllers`中创建新的`DatabaseController`类。\n\n`database-controller.h`:\n\n```cpp\n#ifndef DATABASECONTROLLER_H\n#define DATABASECONTROLLER_H\n\n#include <QObject>\n#include <QScopedPointer>\n\n#include <controllers/i-database-controller.h>\n\n#include <cm-lib_global.h>\n\nnamespace cm {\nnamespace controllers {\n\nclass CMLIBSHARED_EXPORT DatabaseController : public IDatabaseController\n{\n    Q_OBJECT\n\npublic:\n    explicit DatabaseController(QObject* parent = nullptr);\n    ~DatabaseController();\n\n    bool createRow(const QString& tableName, const QString& id, const \n                         QJsonObject& jsonObject) const override;\n    bool deleteRow(const QString& tableName, const QString& id) const \n                                                            override;\n    QJsonArray find(const QString& tableName, const QString& \n                                   searchText) const override;\n    QJsonObject readRow(const QString& tableName, const QString& id) \n                                                  const override;\n    bool updateRow(const QString& tableName, const QString& id, const \n                         QJsonObject& jsonObject) const override;\n\nprivate:\n    class Implementation;\n    QScopedPointer<Implementation> implementation;\n};\n\n}}\n\n#endif\n```\n\n现在，让我们浏览一下`database-controller.cpp`中的每个关键实现细节:\n\n```cpp\nclass DatabaseController::Implementation\n{\npublic:\n    Implementation(DatabaseController* _databaseController)\n        : databaseController(_databaseController)\n    {\n        if (initialise()) {\n            qDebug() << \"Database created using Sqlite version: \" + \n                                                sqliteVersion();\n            if (createTables()) {\n                qDebug() << \"Database tables created\";\n            } else {\n                qDebug() << \"ERROR: Unable to create database tables\";\n            }\n        } else {\n            qDebug() << \"ERROR: Unable to open database\";\n        }\n    }\n\n    DatabaseController* databaseController{nullptr};\n    QSqlDatabase database;\n\nprivate:\n    bool initialise()\n    {\n        database = QSqlDatabase::addDatabase(\"QSQLITE\", \"cm\");\n        database.setDatabaseName( \"cm.sqlite\" );\n        return database.open();\n    }\n\n    bool createTables()\n    {\n        return createJsonTable( \"client\" );\n    }\n\n    bool createJsonTable(const QString& tableName) const\n    {\n        QSqlQuery query(database);\n        QString sqlStatement = \"CREATE TABLE IF NOT EXISTS \" + \n         tableName + \" (id text primary key, json text not null)\";\n\n        if (!query.prepare(sqlStatement)) return false;\n\n        return query.exec();\n    }\n\n    QString sqliteVersion() const\n    {\n        QSqlQuery query(database);\n\n        query.exec(\"SELECT sqlite_version()\");\n\n        if (query.next()) return query.value(0).toString();\n\n        return QString::number(-1);\n    }\n};\n```\n\n从私有实现开始，我们将初始化分成了两个操作:`initialise()`用名为`cm.sqlite`的文件实例化了一个到 SQLite 数据库的连接，如果数据库文件还不存在，这个操作将首先为我们创建它。该文件将被创建在与应用可执行文件`createTables()`相同的文件夹中，然后创建数据库中不存在的任何我们需要的表。最初，我们只需要一个名为 client 的表，但这可以在以后轻松扩展。我们将创建命名表的实际工作委托给`createJsonTable()`方法，这样我们就可以在多个表中重用它。\n\n传统的规范化关系数据库方法是将我们的每个数据模型保存在它们自己的表中，其中的字段与类的属性相匹配。回想一下[第五章](5.html)*数据*中的车型图，如下:\n\n![](img/f194d8cd-8f52-4936-bebc-64a2f23f37a9.png)\n\n我们可以创建一个包含“引用”和“名称”字段的客户端表，一个包含“类型”、“地址”和其他字段的联系人表。然而，我们将利用已经实现的 JSON 序列化代码，实现一个伪文档样式的数据库。我们将使用单个客户机表，该表将存储客户机的唯一标识以及序列化为 JSON 的整个客户机对象层次结构。\n\n最后，我们还添加了一个`sqliteVersion()`实用程序方法来识别数据库使用的是哪个版本的 SQLite:\n\n```cpp\nbool DatabaseController::createRow(const QString& tableName, const QString& id, const QJsonObject& jsonObject) const\n{\n    if (tableName.isEmpty()) return false;\n    if (id.isEmpty()) return false;\n    if (jsonObject.isEmpty()) return false;\n\n    QSqlQuery query(implementation->database);\n\n    QString sqlStatement = \"INSERT OR REPLACE INTO \" + tableName + \" \n                            (id, json) VALUES (:id, :json)\";\n\n    if (!query.prepare(sqlStatement)) return false;\n\n    query.bindValue(\":id\", QVariant(id));\n    query.bindValue(\":json\",    \n   QVariant(QJsonDocument(jsonObject).toJson(QJsonDocument::Compact)));\n\n    if(!query.exec()) return false;\n\n    return query.numRowsAffected() > 0;\n}\n\nbool DatabaseController::deleteRow(const QString& tableName, const QString& id) const\n{\n    if (tableName.isEmpty()) return false;\n    if (id.isEmpty()) return false;\n\n    QSqlQuery query(implementation->database);\n\n    QString sqlStatement = \"DELETE FROM \" + tableName + \" WHERE \n                            id=:id\";\n\n    if (!query.prepare(sqlStatement)) return false;\n\n    query.bindValue(\":id\", QVariant(id));\n\n    if(!query.exec()) return false;\n\n    return query.numRowsAffected() > 0;\n}\n\nQJsonObject DatabaseController::readRow(const QString& tableName, const QString& id) const\n{\n    if (tableName.isEmpty()) return {};\n    if (id.isEmpty()) return {};\n\n    QSqlQuery query(implementation->database);\n\n    QString sqlStatement = \"SELECT json FROM \" + tableName + \" WHERE \n                            id=:id\";\n\n    if (!query.prepare(sqlStatement)) return {};\n\n    query.bindValue(\":id\", QVariant(id));\n\n    if (!query.exec()) return {};\n\n    if (!query.first()) return {};\n\n    auto json = query.value(0).toByteArray();\n    auto jsonDocument = QJsonDocument::fromJson(json);\n\n    if (!jsonDocument.isObject()) return {};\n\n    return jsonDocument.object();\n}\n\nbool DatabaseController::updateRow(const QString& tableName, const QString& id, const QJsonObject& jsonObject) const\n{\n    if (tableName.isEmpty()) return false;\n    if (id.isEmpty()) return false;\n    if (jsonObject.isEmpty()) return false;\n\n    QSqlQuery query(implementation->database);\n\n    QString sqlStatement = \"UPDATE \" + tableName + \" SET json=:json \n                            WHERE id=:id\";\n\n    if (!query.prepare(sqlStatement)) return false;\n\n    query.bindValue(\":id\", QVariant(id));\n    query.bindValue(\":json\", \n   QVariant(QJsonDocument(jsonObject).toJson(QJsonDocument::Compact)));\n\n    if(!query.exec()) return false;\n\n    return query.numRowsAffected() > 0;\n}\n```\n\nCRUD 操作都是围绕`QSqlQuery`类进行的，并准备`sqlStatements`。在所有情况下，我们首先对参数进行一些敷衍的检查，以确保我们没有试图做一些愚蠢的事情。然后，我们将表名连接成一个 SQL 字符串，用`:myParameter`语法表示参数。准备好语句后，随后在查询对象上使用`bindValue()`方法替换参数。\n\n在创建、删除或更新行时，我们只需返回一个查询执行的`true` / `false`成功指示符。假设查询准备并执行无误，我们检查受操作影响的行数是否大于`0`。读取操作返回一个 JSON 对象，该对象从存储在匹配记录中的 JSON 文本中解析而来。如果没有找到记录或者无法解析 JSON，那么我们返回一个默认的 JSON 对象:\n\n```cpp\nQJsonArray DatabaseController::find(const QString& tableName, const QString& searchText) const\n{\n    if (tableName.isEmpty()) return {};\n    if (searchText.isEmpty()) return {};\n\n    QSqlQuery query(implementation->database);\n\n    QString sqlStatement = \"SELECT json FROM \" + tableName + \" where \n                            lower(json) like :searchText\";\n\n    if (!query.prepare(sqlStatement)) return {};\n\n    query.bindValue(\":searchText\", QVariant(\"%\" + searchText.toLower() \n                                                             + \"%\"));\n\n    if (!query.exec()) return {};\n\n    QJsonArray returnValue;\n\n    while ( query.next() ) {\n        auto json = query.value(0).toByteArray();\n        auto jsonDocument = QJsonDocument::fromJson(json);\n        if (jsonDocument.isObject()) {\n            returnValue.append(jsonDocument.object());\n        }\n    }\n\n    return returnValue;\n}\n```\n\n最后，`find()`方法本质上和 CRUD 操作一样，但是编译一个 JSON 对象数组，因为可能有多个匹配。请注意，我们在 SQL 语句中使用`like`关键字，结合`%`通配符，来查找包含搜索文本的任何 JSON。我们还将比较的两边都转换为小写，以使搜索有效地不区分大小写。\n\n# 主键\n\n这些操作中的大部分操作都需要一个标识参数作为我们表中的主键。为了使用这个新的数据库控制器来支持我们的实体的持久性，我们需要向我们的`Entity`类添加一个属性，该属性唯一地标识该实体的一个实例。\n\n在`entity.cpp`中，给`Entity::Implementation`增加一个成员变量:\n\n```cpp\nQString id;\n```\n\n然后，在构造函数中初始化它:\n\n```cpp\nImplementation(Entity* _entity, IDatabaseController* _databaseController, const QString& _key)\n    : entity(_entity)\n    , databaseController(_databaseController)\n    , key(_key)\n    , id(QUuid::createUuid().toString())\n{\n}\n```\n\n当我们实例化一个新的`Entity`时，我们需要生成一个新的唯一 ID，我们使用 QUuid 类用`createUuid()`方法为我们实现这个。**通用唯一标识符** ( **UUID** ) 本质上是一个随机生成的数字，然后我们将其转换为“{ xxxxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxx }”格式的字符串，其中“x”是十六进制数字。你需要`#include <QUuid>`。\n\n接下来，为它提供一个公共访问器方法:\n\n```cpp\nconst QString& Entity::id() const\n{\n    return implementation->id;\n}\n```\n\n现在的挑战是，如果我们正在创建一个已经有一个 ID 的`Entity`(例如，从数据库加载一个客户端)，我们需要一些机制来用已知的值覆盖生成的 ID 值。我们将通过`update()`方法来实现:\n\n```cpp\nvoid Entity::update(const QJsonObject& jsonObject)\n{\n    if (jsonObject.contains(\"id\")) {\n        implementation->id = jsonObject.value(\"id\").toString();\n    }\n\n    …\n\n}\n```\n\n同样，当我们将对象序列化为 JSON 时，我们也需要包含标识:\n\n```cpp\nQJsonObject Entity::toJson() const\n{\n    QJsonObject returnValue;\n    returnValue.insert(\"id\", implementation->id);\n    …\n}\n```\n\n太好了。这为我们的所有数据模型提供了自动生成的唯一标识，我们可以将其用作数据库表中的主键。但是，数据库表的一个常见用例是，实际上有一个非常适合用作主键的现有字段，例如，国家保险或社会保险号、帐户参考或站点标识。让我们添加一种机制来指定一个数据装饰器，作为将覆盖默认 UUID 的标识(如果设置的话)。\n\n在我们的`Entity`类中，在`Implementation`中添加一个新的私有成员:\n\n```cpp\nclass Entity::Implementation\n{\n    ...\n    StringDecorator* primaryKey{nullptr};\n    ...\n}\n```\n\n您将需要`#include``StringDecorator`标题。添加受保护的 mutator 方法来设置它:\n\n```cpp\nvoid Entity::setPrimaryKey(StringDecorator* primaryKey) \n{ \n    implementation->primaryKey = primaryKey; \n}\n```\n\n然后，如果合适，我们可以调整我们的`id()`方法，返回主键值，否则默认为生成的 UUID 值:\n\n```cpp\nconst QString& Entity::id() const\n{\n    if(implementation->primaryKey != nullptr && !implementation->primaryKey->value().isEmpty()) {\n        return implementation->primaryKey->value();\n    }\n    return implementation->id;\n}\n```\n\n然后，在`client.cpp`构造函数中，在我们实例化了所有数据装饰器之后，我们可以指定我们想要使用引用字段作为我们的主键:\n\n```cpp\nClient::Client(QObject* parent)\n    : Entity(parent, \"client\")\n{\n    ...\n\n    setPrimaryKey(reference);\n}\n```\n\n让我们添加几个测试来验证这个行为。我们将验证如果设置了一个参考值，`id()`方法返回该值，否则它返回一个生成的松散的“{ xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxx }”格式的 UUID。\n\n在`cm-tests`项目的`client-tests.h`中，在私有槽范围内添加两个新的测试:\n\n```cpp\nvoid id_givenPrimaryKeyWithNoValue_returnsUuid();\nvoid id_givenPrimaryKeyWithValue_returnsPrimaryKey();\n```\n\n然后，执行`client-tests.cpp`中的测试:\n\n```cpp\nvoid ClientTests::id_givenPrimaryKeyWithNoValue_returnsUuid()\n{\n    Client testClient(this);\n\n    // Using individual character checks\n    QCOMPARE(testClient.id().left(1), QString(\"{\"));\n    QCOMPARE(testClient.id().mid(9, 1), QString(\"-\"));\n    QCOMPARE(testClient.id().mid(14, 1), QString(\"-\"));\n    QCOMPARE(testClient.id().mid(19, 1), QString(\"-\"));\n    QCOMPARE(testClient.id().mid(24, 1), QString(\"-\"));\n    QCOMPARE(testClient.id().right(1), QString(\"}\"));\n\n    // Using regular expression pattern matching\n    QVERIFY(QRegularExpression(\"\\\\{.{8}-(.{4})-(.{4})-(.{4})-(.\n                        {12})\\\\}\").match(testClient.id()).hasMatch());\n}\n\nvoid ClientTests::id_givenPrimaryKeyWithValue_returnsPrimaryKey()\n{\n    Client testClient(this, QJsonDocument::fromJson(jsonByteArray).object());\n    QCOMPARE(testClient.reference->value(), QString(\"CM0001\"));\n    QCOMPARE(testClient.id(), testClient.reference->value());\n}\n```\n\n请注意，在第一次测试中，检查被有效地执行了两次，只是为了演示您可以采取的几种不同的方法。首先，我们使用单个字符匹配(“{”、“-”和“}”)进行检查，这相当冗长，但其他开发人员很容易阅读和理解。然后，我们使用 Qt 的正则表达式助手类再次执行检查。对于不会说正则表达式语法的正常人来说，这要短得多，但更难解析。\n\n构建和运行测试，它们应该验证我们刚刚实现的变更。\n\n# 创建客户端\n\n让我们使用我们的新基础设施并连接`CreateClientView`。如果你还记得，我们提供了一个保存命令，当点击时，调用`CommandController`上的`onCreateClientSaveExecuted()`。为了能够执行任何有用的操作，`CommandController`需要序列化和保存客户端实例的可见性，并实现`IDatabaseController`接口来为我们执行创建操作。\n\n将它们注入`command-controller.h`中的构造函数，包括任何必要的标题:\n\n```cpp\nexplicit CommandController(QObject* _parent = nullptr, IDatabaseController* databaseController = nullptr, models::Client* newClient = nullptr);\n```\n\n正如我们已经看到的几次，添加成员变量到`Implementation`:\n\n```cpp\nIDatabaseController* databaseController{nullptr};\nClient* newClient{nullptr};\n```\n\n通过`CommandController`构造函数传递给实现构造函数:\n\n```cpp\nImplementation(CommandController* _commandController, IDatabaseController* _databaseController, Client* _newClient)\n    : commandController(_commandController)\n    , databaseController(_databaseController)\n    , newClient(_newClient)           \n{\n    ...\n}\n```\n\n```cpp\nCommandController::CommandController(QObject* parent, IDatabaseController* databaseController, Client* newClient)\n    : QObject(parent)\n{\n    implementation.reset(new Implementation(this, databaseController, newClient));\n}\n```\n\n现在我们可以更新`onCreateClientSaveExecuted()`方法来创建我们的新客户端:\n\n```cpp\nvoid CommandController::onCreateClientSaveExecuted()\n{\n    qDebug() << \"You executed the Save command!\";\n\n    implementation->databaseController->createRow(implementation->newClient->key(), implementation->newClient->id(), implementation->newClient->toJson());\n\n    qDebug() << \"New client saved.\";\n}\n```\n\n我们的客户端实例为我们提供了将信息保存到数据库所需的所有信息，数据库控制器执行数据库交互。\n\n我们的`CommandController`现在已经准备好了，但是我们实际上还没有注入数据库控制器或者新的客户端，所以前往`master-controller.cpp`并添加一个`DatabaseController`的实例，就像我们对`CommandController`和`NavigationController`所做的那样。添加私有成员、访问器方法和`Q_PROPERTY`。\n\n在`Implementation`构造函数中，我们需要确保在初始化`CommandController`之前，我们初始化了新的客户端和`DatabaseController`，然后传递指针:\n\n```cpp\nImplementation(MasterController* _masterController)\n    : masterController(_masterController)\n{\n    databaseController = new DatabaseController(masterController);\n    navigationController = new NavigationController(masterController);\n    newClient = new Client(masterController);\n    commandController = new CommandController(masterController, databaseController, newClient);\n}\n```\n\n构建并运行`cm-ui`，您应该会在应用输出中看到来自新实例化的`DatabaseController`的消息，告诉您它已经创建了数据库和表:\n\n```cpp\nDatabase created using Sqlite version: 3.20.1\nDatabase tables created\n```\n\n看看你的二进制文件所在的输出文件夹，你会看到一个新的`cm.sqlite`文件。\n\n如果您导航到创建客户端视图，输入名称，然后单击保存按钮，您将看到进一步的输出，确认新客户端已成功保存:\n\n```cpp\nYou executed the Save command!\nNew client saved\n```\n\n让我们看看我们的数据库内部，看看为我们做了哪些工作。有几个 SQLite 浏览应用和网络浏览器插件可用，但我倾向于使用的是在[http://sqlitebrowser.org/](http://sqlitebrowser.org/)找到的。下载并安装此软件或您为操作系统选择的任何其他客户端，并打开`cm.sqlite`文件:\n\n![](img/035414d6-2999-4408-b674-f05e382fe65d.png)\n\n正如我们所要求的，您将看到我们有一个客户端表，它有两个字段:id 和 json。浏览客户机表的数据，您将看到我们新创建的记录，其名称属性是我们在用户界面上输入的:\n\n![](img/63acbb6f-08c5-42b8-8189-e2cc4d95bb86.png)\n\n太棒了，我们在数据库中创建了第一个客户。注意`DatabaseController`初始化方法是幂等的，所以可以重新启动应用，现有的数据库不会受到影响。同样，如果手动删除`cm.sqlite`文件，那么启动应用会为你创建一个新版本(没有旧数据)，这是删除测试数据的一种简单方法。\n\n让我们快速调整一下，添加客户端的`reference`属性。在`CreateClientView`中，复制绑定到`ui_name`的`StringEditorSingleLine`组件，并将新控件绑定到`ui_reference`。构建、运行和创建新客户端:\n\n![](img/2c9bf851-c9e6-4512-9e50-8cffc2b6c766.png)\n\n我们的新客户端很乐意使用指定的客户端引用作为唯一的主键:\n\n![](img/fcbd34f0-f483-49be-beed-c0b2fb8dda95.png)\n\n# 嵌板\n\n现在，让我们稍微充实一下我们的`CreateClientView`，这样我们就可以实际保存一些有意义的数据，而不仅仅是一堆空字符串。我们仍有许多字段需要添加，因此我们将对这些字段进行一些分解，并从视觉上将数据从不同的模型中分离出来，方法是将它们封装在带有描述性标题和阴影的谨慎面板中，为我们的用户界面增添一些活力:\n\n![](img/d1af194c-0617-4db3-ac07-4fb20013f77c.png)\n\n我们将从创建一个通用面板组件开始。在名为`Panel.qml`的`cm-ui/components`中创建新的 QML 文件。更新`components.qrc`和`qmldir`，就像我们对所有其他组件所做的那样:\n\n```cpp\nimport QtQuick 2.9\nimport assets 1.0\n\nItem {\n    implicitWidth: parent.width\n    implicitHeight: headerBackground.height +    \n    contentLoader.implicitHeight + (Style.sizeControlSpacing * 2)\n    property alias headerText: title.text\n    property alias contentComponent: contentLoader.sourceComponent\n\n    Rectangle {\n        id: shadow\n        width: parent.width\n        height: parent.height\n        x: Style.sizeShadowOffset\n        y: Style.sizeShadowOffset\n        color: Style.colourShadow\n    }\n\n    Rectangle {\n        id: headerBackground\n        anchors {\n            top: parent.top\n            left: parent.left\n            right: parent.right\n        }\n        height: Style.heightPanelHeader\n        color: Style.colourPanelHeaderBackground\n\n        Text {\n            id: title\n            text: \"Set Me!\"\n            anchors {\n                fill: parent\n                margins: Style.heightDataControls / 4\n            }\n            color: Style.colourPanelHeaderFont\n            font.pixelSize: Style.pixelSizePanelHeader\n            verticalAlignment: Qt.AlignVCenter\n        }\n    }\n\n    Rectangle {\n        id: contentBackground\n        anchors {\n            top: headerBackground.bottom\n            left: parent.left\n            right: parent.right\n            bottom: parent.bottom\n        }\n        color: Style.colourPanelBackground\n\n        Loader {\n            id: contentLoader\n            anchors {\n                left: parent.left\n                right: parent.right\n                top: parent.top\n                margins: Style.sizeControlSpacing\n            }\n        }\n    }\n}\n```\n\n这是一个极其动态的组件。不像我们的其他组件，我们传入一个字符串，甚至可能是一个自定义类，这里我们传入面板的全部内容。我们使用`Loader`组件来实现这一点，该组件按需加载 QML 子树。我们给`sourceComponent`属性取别名，以便调用元素可以在运行时注入它们想要的内容。\n\n由于内容的动态性质，我们不能将组件设置为固定大小，因此我们利用`implicitWidth`和`implicitHeight`属性，根据标题栏的大小加上动态内容的大小，告诉父元素组件想要多大。\n\n为了渲染阴影，我们绘制了一个简单的`Rectangle`，通过将它放在文件顶部附近来确保它首先被渲染。然后，我们使用`x`和`y`属性将它从其余元素中偏移出来，稍微上下移动它。然后，标题条和面板背景的剩余`Rectangle`元素被绘制在阴影的顶部。\n\n为了支持这里的样式，我们需要添加一组新的`Style`属性:\n\n```cpp\nreadonly property real sizeControlSpacing: 10\n```\n\n```cpp\nreadonly property color colourPanelBackground: \"#ffffff\"\nreadonly property color colourPanelBackgroundHover: \"#ececec\"\nreadonly property color colourPanelHeaderBackground: \"#131313\"\nreadonly property color colourPanelHeaderFont: \"#ffffff\"\nreadonly property color colourPanelFont: \"#131313\"\nreadonly property int pixelSizePanelHeader: 18\nreadonly property real heightPanelHeader: 40\nreadonly property real sizeShadowOffset: 5\nreadonly property color colourShadow: \"#dedede\"\n```\n\n接下来，让我们添加一个用于地址编辑的组件，这样我们就可以在供应和计费地址中重用它。在名为`AddressEditor.qml`的`cm-ui/components`中创建新的 QML 文件。如前所述更新`components.qrc`和`qmldir`。\n\n我们将使用新的`Panel`组件作为根元素，并添加一个`Address`属性，这样我们就可以传入一个任意的数据模型来绑定:\n\n```cpp\nimport QtQuick 2.9\nimport CM 1.0\nimport assets 1.0\n\nPanel {\n    property Address address\n\n    contentComponent:\n        Column {\n            id: column\n            spacing: Style.sizeControlSpacing\n            StringEditorSingleLine {\n                stringDecorator: address.ui_building\n                anchors {\n                    left: parent.left\n                    right: parent.right\n                }\n            }\n            StringEditorSingleLine {\n                stringDecorator: address.ui_street\n                anchors {\n                    left: parent.left\n                    right: parent.right\n                }\n            }\n            StringEditorSingleLine {\n                stringDecorator: address.ui_city\n                anchors {\n                    left: parent.left\n                    right: parent.right\n                }\n            }\n            StringEditorSingleLine {\n                stringDecorator: address.ui_postcode\n                anchors {\n                    left: parent.left\n                    right: parent.right\n                }\n            }\n        }\n}\n```\n\n在这里，您可以看到我们新的`Panel`组件在运行中的灵活性，这要归功于嵌入的`Loader`元素。我们可以传入我们想要的任何 QML 内容，它将在面板中呈现。\n\n最后，我们可以更新我们的`CreateClientView`来添加我们新的重构地址组件。我们还会将客户端控件移到它们自己的面板上:\n\n```cpp\nimport QtQuick 2.9\nimport QtQuick.Controls 2.2\nimport CM 1.0\nimport assets 1.0\nimport components 1.0\n\nItem {\n    property Client newClient: masterController.ui_newClient\n\n    Column {\n        spacing: Style.sizeScreenMargin\n        anchors {\n            left: parent.left\n            right: parent.right\n            top: parent.top\n            margins: Style.sizeScreenMargin\n        }\n        Panel {\n            headerText: \"Client Details\"\n            contentComponent:\n                Column {\n                    spacing: Style.sizeControlSpacing\n                    StringEditorSingleLine {\n                        stringDecorator: newClient.ui_reference\n                        anchors {\n                            left: parent.left\n                            right: parent.right\n                        }\n                    }\n                    StringEditorSingleLine {\n                        stringDecorator: newClient.ui_name\n                        anchors {\n                            left: parent.left\n                            right: parent.right\n                        }\n                    }\n                }\n        }\n        AddressEditor {\n            address: newClient.ui_supplyAddress\n            headerText: \"Supply Address\"\n        }\n        AddressEditor {\n            address: newClient.ui_billingAddress\n            headerText: \"Billing Address\"\n        }\n    }\n    CommandBar {\n        commandList: masterController.ui_commandController.ui_createClientViewContextCommands\n    }\n}\n```\n\n在我们构建和运行之前，我们只需要调整我们的`StringEditorSingleLine` `textLabel`的背景颜色，使其与它们现在显示的面板相匹配:\n\n```cpp\nRectangle {\n    width: Style.widthDataControls\n    height: Style.heightDataControls\n    color: Style.colourPanelBackground\n    Text {\n        id: textLabel\n        …\n    }\n}\n```\n\n![](img/b6e32497-89df-4bf3-ba33-42af47bc0b7b.png)\n\n继续创建一个新的客户端并检查数据库。您现在应该看到供应和帐单地址详细信息已成功保存。现在，我们的 CRUD 中的 C 已经运行，所以让我们继续“R”。\n\n# 寻找客户\n\n我们刚刚成功地将第一批客户端保存到数据库中，现在让我们看看如何找到并查看这些数据。我们将在`cm-lib`中的一个专用类中封装我们的搜索功能，所以继续在`cm-lib/source/models`中创建一个名为`ClientSearch`的新类。\n\n`client-search.h`:\n\n```cpp\n#ifndef CLIENTSEARCH_H\n#define CLIENTSEARCH_H\n\n#include <QScopedPointer>\n\n#include <cm-lib_global.h>\n#include <controllers/i-database-controller.h>\n#include <data/string-decorator.h>\n#include <data/entity.h>\n#include <data/entity-collection.h>\n#include <models/client.h>\n\nnamespace cm {\nnamespace models {\n\nclass CMLIBSHARED_EXPORT ClientSearch : public data::Entity\n{\n    Q_OBJECT\n    Q_PROPERTY( cm::data::StringDecorator* ui_searchText READ \n                                           searchText CONSTANT )\n    Q_PROPERTY( QQmlListProperty<cm::models::Client> ui_searchResults \n                READ ui_searchResults NOTIFY searchResultsChanged )\n\npublic:\n    ClientSearch(QObject* parent = nullptr, \n    controllers::IDatabaseController* databaseController = nullptr);\n    ~ClientSearch();\n\n    data::StringDecorator* searchText();\n    QQmlListProperty<Client> ui_searchResults();\n    void search();\n\nsignals:\n    void searchResultsChanged();\n\nprivate:\n    class Implementation;\n    QScopedPointer<Implementation> implementation;\n};\n\n}}\n\n#endif\n```\n\n`client-search.cpp`:\n\n```cpp\n#include \"client-search.h\"\n#include <QDebug>\n\nusing namespace cm::controllers;\nusing namespace cm::data;\n\nnamespace cm {\nnamespace models {\n\nclass ClientSearch::Implementation\n{\npublic:\n    Implementation(ClientSearch* _clientSearch, IDatabaseController* \n                                                _databaseController)\n        : clientSearch(_clientSearch)\n        , databaseController(_databaseController)\n    {\n    }\n\n    ClientSearch* clientSearch{nullptr};\n    IDatabaseController* databaseController{nullptr};\n    data::StringDecorator* searchText{nullptr};\n    data::EntityCollection<Client>* searchResults{nullptr};\n};\n\nClientSearch::ClientSearch(QObject* parent, IDatabaseController* databaseController)\n    : Entity(parent, \"ClientSearch\")\n{\n    implementation.reset(new Implementation(this, databaseController));\n    implementation->searchText = static_cast<StringDecorator*>(addDataItem(new StringDecorator(this, \"searchText\", \"Search Text\")));\n    implementation->searchResults = static_cast<EntityCollection<Client>*>(addChildCollection(new EntityCollection<Client>(this, \"searchResults\")));\n\n    connect(implementation->searchResults, &EntityCollection<Client>::collectionChanged, this, &ClientSearch::searchResultsChanged);\n}\n\nClientSearch::~ClientSearch()\n{\n}\n\nStringDecorator* ClientSearch::searchText()\n{\n    return implementation->searchText;\n}\n\nQQmlListProperty<Client> ClientSearch::ui_searchResults()\n{\n    return QQmlListProperty<Client>(this, implementation->searchResults->derivedEntities());\n}\n\nvoid ClientSearch::search()\n{\n    qDebug() << \"Searching for \" << implementation->searchText->value() << \"...\";\n}\n\n}}\n```\n\n我们需要从用户那里获取一些文本，使用这些文本搜索数据库，并将结果显示为匹配客户端的列表。我们使用`StringDecorator`来容纳文本，实现`search()`方法来为我们执行搜索，最后，添加一个`EntitityCollection<Client>`来存储结果。这里另一个有趣的点是，当搜索结果发生变化时，我们需要向用户界面发出信号，以便它知道它需要重新绑定列表。为此，我们使用信号`searchResultsChanged()`进行通知，并将该信号直接连接到`EntityCollection`内置的`collectionChanged()`信号。现在，每当`EntityCollection`中隐藏的列表更新时，用户界面会自动收到更改通知，并根据需要重新绘制自己。\n\n接下来，向`MasterController`添加一个`ClientSearch`的实例，就像我们为新的客户端模型所做的那样。添加一个名为`clientSearch`的`ClientSearch*`类型的私有成员变量，并在`Implementation`构造函数中初始化它。记得将`databaseController`依赖项传递给构造函数。现在我们传递了越来越多的依赖项，我们需要小心初始化顺序。`ClientSearch`依赖于`DatabaseController`，当我们在`CommandController`中实现搜索命令时，就会依赖于`ClientSearch`。所以确保你在`ClientSearch`之前初始化`DatabaseController`，并且`CommandController`在两者之后。要完成对`MasterController`的更改，请添加一个`clientSearch()`访问器方法和一个名为`ui_clientSearch`的`Q_PROPERTY`。\n\n像往常一样，我们需要在 QML 子系统中注册新类，然后才能在用户界面中使用它。在`main.cpp`、`#include <models/client-search.h>`中，注册新类型:\n\n```cpp\nqmlRegisterType<cm::models::ClientSearch>(\"CM\", 1, 0, \"ClientSearch\");\n```\n\n有了这些，我们就可以连线了:\n\n```cpp\nimport QtQuick 2.9\nimport assets 1.0\nimport CM 1.0\nimport components 1.0\n\nItem {\n    property ClientSearch clientSearch: masterController.ui_clientSearch\n\n    Rectangle {\n        anchors.fill: parent\n        color: Style.colourBackground\n\n        Panel {\n            id: searchPanel\n            anchors {\n                left: parent.left\n                right: parent.right\n                top: parent.top\n                margins: Style.sizeScreenMargin\n            }\n            headerText: \"Find Clients\"\n            contentComponent:\n                StringEditorSingleLine {\n                    stringDecorator: clientSearch.ui_searchText\n                    anchors {\n                        left: parent.left\n                        right: parent.right\n                    }\n                }\n        }\n    }\n}\n```\n\n我们通过`MasterController`访问`ClientSearch`实例，并创建一个带有属性的快捷方式。我们还再次利用了我们的新`Panel`组件，它在很少工作的情况下为我们提供了跨视图的一致外观和感觉:\n\n![](img/ff6f1c18-de37-4492-952d-1c01d8775251.png)\n\n下一步是为我们添加一个命令按钮，以便能够发起搜索。我们在`CommandController`重新做这个。在我们进入命令之前，我们对`ClientSearch`实例有一个额外的依赖，所以给构造函数添加一个参数:\n\n```cpp\nCommandController::CommandController(QObject* parent, IDatabaseController* databaseController, Client* newClient, ClientSearch* clientSearch)\n    : QObject(parent)\n{\n    implementation.reset(new Implementation(this, databaseController, newClient, clientSearch));\n}\n```\n\n将参数传递给`Implementation`类，并将其存储在私有成员变量中，就像我们对`newClient`所做的那样。短暂跳回`MasterController`并将`clientSearch`实例添加到`CommandController`初始化中:\n\n```cpp\ncommandController = new CommandController(masterController, databaseController, newClient, clientSearch);\n```\n\n接下来，在`CommandController`中，复制并重命名我们为创建客户端视图添加的私有成员变量、访问器和`Q_PROPERTY`，这样您就有了一个可供用户界面使用的`ui_findClientViewContextCommands`属性。\n\n创建一个额外的公共槽，`onFindClientSearchExecuted()`，当我们点击搜索按钮时将调用它:\n\n```cpp\nvoid CommandController::onFindClientSearchExecuted()\n{\n    qDebug() << \"You executed the Search command!\";\n\n    implementation->clientSearch->search();\n}\n```\n\n现在，我们有一个空的命令列表，用于查找视图，还有一个当我们单击按钮时要调用的委托；我们现在需要做的就是给`Implementation`构造函数添加一个搜索按钮:\n\n```cpp\nCommand* findClientSearchCommand = new Command( commandController, QChar( 0xf002 ), \"Search\" );\nQObject::connect( findClientSearchCommand, &Command::executed, commandController, &CommandController::onFindClientSearchExecuted );\nfindClientViewContextCommands.append( findClientSearchCommand );\n```\n\n这就是命令管道；我们现在可以很容易地给`FindClientView`添加一个命令栏。将以下内容作为根项目中的最后一个元素插入:\n\n```cpp\nCommandBar {\n    commandList: masterController.ui_commandController.ui_findClientViewContextCommands\n} \n```\n\n![](img/1c44bc94-195d-43b8-b7c0-4129fb1fc62e.png)\n\n输入一些搜索文本并单击按钮，您将在应用输出控制台中看到一切都按预期触发:\n\n```cpp\nYou executed the Search command!\nSearching for \"Testing\"...\n```\n\n太好了，现在我们需要做的是获取搜索文本，查询 SQLite 数据库中的结果列表，并在屏幕上显示这些结果。幸运的是，我们已经完成了查询数据库的基础工作，因此我们可以轻松地实现它:\n\n```cpp\nvoid ClientSearch::search()\n{\n    qDebug() << \"Searching for \" << implementation->searchText->value() \n                                 << \"...\";\n\n    auto resultsArray = implementation->databaseController-\n         >find(\"client\", implementation->searchText->value());\n    implementation->searchResults->update(resultsArray);\n\n    qDebug() << \"Found \" << implementation->searchResults-\n             >baseEntities().size() << \" matches\";\n}\n```\n\n在 UI 端要显示结果还有一点工作要做。我们需要绑定到`ui_searchResults`属性，并为列表中的每个客户端动态显示某种 QML 子树。我们将使用一个新的 QML 组件`ListView`，为我们做繁重的工作。让我们从简单的演示原理开始，然后从那里开始构建。在`FindClientView`中，在面板元素后立即添加以下内容:\n\n```cpp\nListView {\n    id: itemsView\n    anchors {\n        top: searchPanel.bottom\n        left: parent.left\n        right: parent.right\n        bottom: parent.bottom\n        margins: Style.sizeScreenMargin\n    }\n    clip: true\n    model: clientSearch.ui_searchResults\n    delegate:\n        Text {\n            text: modelData.ui_reference.ui_label + \": \" + \n                  modelData.ui_reference.ui_value\n            font.pixelSize: Style.pixelSizeDataControls\n            color: Style.colourPanelFont\n        }\n}\n```\n\n`ListView`的两个关键属性如下:\n\n*   `model`，是您想要显示的项目列表\n*   `delegate`，这是您希望如何直观地表示每个项目\n\n在我们的例子中，我们将模型绑定到我们的`ui_searchResults`上，并用一个简单的`Text`元素表示每个项目，该元素显示客户端参考号。这里特别重要的是`modelData`属性，它被神奇地注入到我们的委托中，并公开了基础项(在本例中是一个客户端对象)。\n\n为您到目前为止创建的一个测试客户端构建、运行并搜索一段您知道存在于 JSON 中的文本，您会看到为每个结果显示了参考号。如果您得到一个以上的结果，但它们的布局不正确，请不要担心，因为我们无论如何都会替换该委托:\n\n![](img/a63d1111-ec63-4786-a8bc-0614cd56606f.png)\n\n为了保持整洁，我们将编写一个新的自定义组件作为委托。在`cm-ui/components`中创建`SearchResultDelegate`，照常更新`components.qrc`和`qmldir`:\n\n```cpp\nimport QtQuick 2.9\nimport assets 1.0\nimport CM 1.0\n\nItem {\n    property Client client\n\n    implicitWidth: parent.width\n    implicitHeight: Math.max(clientColumn.implicitHeight, \n    textAddress.implicitHeight) + (Style.heightDataControls / 2)\n\n    Rectangle {\n        id: background\n        width: parent.width\n        height: parent.height\n        color: Style.colourPanelBackground\n\n        Column {\n            id: clientColumn\n            width: parent / 2\n            anchors {\n                left: parent.left\n                top: parent.top\n                margins: Style.heightDataControls / 4\n            }\n            spacing: Style.heightDataControls / 2\n\n            Text {\n                id: textReference\n                anchors.left: parent.left\n                text: client.ui_reference.ui_label + \": \" + \n                      client.ui_reference.ui_value\n                font.pixelSize: Style.pixelSizeDataControls\n                color: Style.colourPanelFont\n            }\n            Text {\n                id: textName\n                anchors.left: parent.left\n                text: client.ui_name.ui_label + \": \" + \n                      client.ui_name.ui_value\n                font.pixelSize: Style.pixelSizeDataControls\n                color: Style.colourPanelFont\n            }\n        }\n\n        Text {\n            id: textAddress\n            anchors {\n                top: parent.top\n                right: parent.right\n                margins: Style.heightDataControls / 4\n            }\n            text: client.ui_supplyAddress.ui_fullAddress\n            font.pixelSize: Style.pixelSizeDataControls\n            color: Style.colourPanelFont\n            horizontalAlignment: Text.AlignRight\n        }\n\n        Rectangle {\n            id: borderBottom\n            anchors {\n                bottom: parent.bottom\n                left: parent.left\n                right: parent.right\n            }\n            height: 1\n            color: Style.colourPanelFont\n        }\n\n        MouseArea {\n            anchors.fill: parent\n            cursorShape: Qt.PointingHandCursor\n            hoverEnabled: true\n            onEntered: background.state = \"hover\"\n            onExited: background.state = \"\"\n            onClicked: masterController.selectClient(client)\n        }\n\n        states: [\n            State {\n                name: \"hover\"\n                PropertyChanges {\n                    target: background\n                    color: Style.colourPanelBackgroundHover\n                }\n            }\n        ]\n    }\n}\n```\n\n这里并没有什么新的东西，我们只是结合了其他组件中涉及的技术。注意`MouseArea`元素会在`masterController`上触发一个我们还没有实现的方法，所以如果你运行这个并在你点击其中一个客户端时得到一个错误，不要担心。\n\n使用`modelData`属性设置`client`，用我们的新组件替换`FindClientView`中的旧`Text`委托:\n\n```cpp\nListView {\n    id: itemsView\n    ...\n    delegate:\n        SearchResultDelegate {\n            client: modelData\n        }\n}\n```\n\n![](img/763dcfa3-7b48-4081-a618-625ed04edda2.png)\n\n现在，让我们在`MasterController`上实现`selectClient()`方法:\n\nWe can just emit the `goEditClientView()` signal directly from the `SearchResultDelegate` and bypass `MasterController` entirely. This is a perfectly valid approach and is indeed simpler; however, I prefer to route all the interactions through the business logic layer, even if all the business logic does is to emit the navigation signal. This means that if you need to add any further logic later on, everything is already wired up and you don’t need to change any of the plumbing. It’s also much easier to debug C++ than QML.\n\n在`master-controller.h`中，我们需要添加我们的新方法作为公共槽，因为它将直接从 UI 中调用，这将没有常规公共方法的可见性:\n\n```cpp\npublic slots:\n    void selectClient(cm::models::Client* client);\n```\n\n在`master-controller.cpp`中提供实现，简单调用导航协调器上的相关信号，通过客户端传递:\n\n```cpp\nvoid MasterController::selectClient(Client* client)\n{\n    implementation->navigationController->goEditClientView(client);\n}\n```\n\n随着搜索和选择的到位，我们现在可以将注意力转向编辑客户端。\n\n# 编辑客户端\n\n现有的客户端已经从数据库中找到并加载，我们需要一种机制来查看和编辑数据。让我们首先创建我们将在编辑视图中使用的上下文命令。重复我们为查找客户端视图采取的步骤，并在`CommandController`中添加一个名为`editClientViewContextCommands`的新命令列表，以及一个访问器方法和`Q_PROPERTY`。\n\n创建用户在编辑视图中保存更改时要调用的新槽:\n\n```cpp\nvoid CommandController::onEditClientSaveExecuted()\n{\n    qDebug() << \"You executed the Save command!\";\n}\n```\n\n向列表中添加一个新的 save 命令，该命令在执行时调用插槽:\n\n```cpp\nCommand* editClientSaveCommand = new Command( commandController, QChar( 0xf0c7 ), \"Save\" );\nQObject::connect( editClientSaveCommand, &Command::executed, commandController, &CommandController::onEditClientSaveExecuted );\neditClientViewContextCommands.append( editClientSaveCommand );\n```\n\n我们现在有了一个可以呈现给编辑客户端视图的命令列表；然而，我们现在需要克服的一个挑战是，当我们执行这个命令时，`CommandController`不知道它需要使用哪个客户端实例。我们不能像对待新客户端那样，将选定的客户端作为依赖项传递给构造函数，因为我们不知道用户会选择哪个客户端。一种选择是将编辑命令列表移出`CommandController`并进入客户端模型。然后，每个客户端实例都可以向用户界面呈现自己的命令。然而，这意味着命令功能被断开，我们失去了命令控制器给我们的良好封装。它也膨胀了**客户端**模型的功能，它不应该关心。相反，我们将当前选择的客户端添加为`CommandController`中的成员，并在用户导航到`editClientView`时进行设置。在`CommandController::Implementation`中，添加以下内容:\n\n```cpp\nClient* selectedClient{nullptr};\n```\n\n添加新的公共插槽:\n\n```cpp\nvoid CommandController::setSelectedClient(cm::models::Client* client)\n{\n    implementation->selectedClient = client;\n}\n```\n\n现在我们有了可用的选定客户端，我们可以继续并完成存储槽的实施。同样，我们已经在`DatabaseController`和客户端类中做了大量的工作，所以这个方法非常简单:\n\n```cpp\nvoid CommandController::onEditClientSaveExecuted()\n{\n    qDebug() << \"You executed the Save command!\";\n\n    implementation->databaseController->updateRow(implementation->selectedClient->key(), implementation->selectedClient->id(), implementation->selectedClient->toJson());\n\n    qDebug() << \"Updated client saved.\";\n}\n```\n\n从用户界面的角度来看，编辑一个现有的客户端本质上与创建一个新的客户端是一样的。事实上，我们甚至可以使用相同的视图，在每种情况下只传入不同的客户端对象。然而，我们将把这两个函数分开，只是复制和调整我们已经为创建一个客户端而编写的 QML。更新`EditClientView`:\n\n```cpp\nimport QtQuick 2.9\nimport QtQuick.Controls 2.2\nimport CM 1.0\nimport assets 1.0\nimport components 1.0\n\nItem {\n    property Client selectedClient\n    Component.onCompleted: masterController.ui_commandController.setSelectedClient(selectedClient)\n\n    Rectangle {\n        anchors.fill: parent\n        color: Style.colourBackground\n    }\n\n    ScrollView {\n        id: scrollView\n        anchors {\n            left: parent.left\n            right: parent.right\n            top: parent.top\n            bottom: commandBar. top\n            margins: Style.sizeScreenMargin\n        }\n        clip: true\n\n        Column {\n            spacing: Style.sizeScreenMargin\n            width: scrollView.width\n\n            Panel {\n                headerText: \"Client Details\"\n                contentComponent:\n                    Column {\n                        spacing: Style.sizeControlSpacing\n                        StringEditorSingleLine {\n                            stringDecorator: \n                            selectedClient.ui_reference\n                            anchors {\n                                left: parent.left\n                                right: parent.right\n                            }\n                        }\n                        StringEditorSingleLine {\n                            stringDecorator: selectedClient.ui_name\n                            anchors {\n                                left: parent.left\n                                right: parent.right\n                            }\n                        }\n                    }\n            }\n\n            AddressEditor {\n                address: selectedClient.ui_supplyAddress\n                headerText: \"Supply Address\"\n            }\n\n            AddressEditor {\n                address: selectedClient.ui_billingAddress\n                headerText: \"Billing Address\"\n            }\n        }\n    }\n\n    CommandBar {\n        id: commandBar\n        commandList: masterController.ui_commandController.ui_editClientViewContextCommands\n    }\n}\n```\n\n我们更改客户端属性以匹配在`Connections`元素中设置的`selectedClient`属性`MasterView`。我们使用`Component.onCompleted`插槽接通`CommandController`并设置当前选择的客户端。最后，我们更新`CommandBar`以引用我们刚刚添加的新上下文命令列表。\n\n构建并运行，现在您应该能够对选定的客户端进行更改，并使用“保存”按钮更新数据库。\n\n# 删除客户端\n\n我们 CRUD 操作的最后一部分是删除一个现有的客户端。让我们通过`EditClientView`上的一个新按钮来触发它。我们将从添加当按钮被按到`CommandController`时将被调用的插槽开始:\n\n```cpp\nvoid CommandController::onEditClientDeleteExecuted()\n{\n    qDebug() << \"You executed the Delete command!\";\n\n    implementation->databaseController->deleteRow(implementation->selectedClient->key(), implementation->selectedClient->id());\n    implementation->selectedClient = nullptr;\n\n    qDebug() << \"Client deleted.\";\n\n    implementation->clientSearch->search();\n}\n```\n\n这遵循与其他插槽相同的模式，除了这次我们还清除了`selectedClient`属性，因为尽管客户端实例仍然存在于应用内存中，但它已经被用户从语义上删除了。我们还会刷新搜索，以便从搜索结果中删除已删除的客户端。按照这种方法，我们已经执行了正确的数据库交互，但是对于他们刚刚要求删除的客户端，用户将留在`editClientView`上。我们想要的是将用户导航回仪表板。为了做到这一点，我们需要添加`NavigationController`作为我们的`CommandController`类的附加依赖。复制我们为`DatabaseController`依赖所做的，以便我们可以将它注入到构造函数中。记得更新`MasterController`并传入导航控制器实例。\n\n有了可用的数据库控制器实例，我们就可以将用户发送到仪表板视图:\n\n```cpp\nvoid CommandController::onEditClientDeleteExecuted()\n{\n    ...\n\n    implementation->navigationController->goDashboardView();\n}\n```\n\n现在我们有了可用的导航控制器，我们也可以在创建新客户端时改善体验。让我们搜索新创建的客户端 ID 并导航到结果，而不是将用户留在新的客户端视图中。如果他们希望查看或编辑，则可以轻松选择新客户端:\n\n```cpp\nvoid CommandController::onCreateClientSaveExecuted()\n{\n    ...\n\n    implementation->clientSearch->searchText()-\n                   >setValue(implementation->newClient->id());\n    implementation->clientSearch->search();\n    implementation->navigationController->goFindClientView();\n}\n```\n\n删除槽完成后，我们现在可以向`CommandController`中的`editClientContextCommands`列表添加新的删除命令:\n\n```cpp\nCommand* editClientDeleteCommand = new Command( commandController, QChar( 0xf235 ), \"Delete\" );\nQObject::connect( editClientDeleteCommand, &Command::executed, commandController, &CommandController::onEditClientDeleteExecuted );\neditClientViewContextCommands.append( editClientDeleteCommand );\n```\n\n我们现在可以选择删除现有客户端:\n\n![](img/a16062b1-ad68-44bf-ad11-094b3cc3c6b7.png)\n\n如果删除客户端，您将看到该行已从数据库中删除，并且用户已成功导航回仪表板。但是，您还会看到应用输出窗口充满了类似`qrc:/views/EditClientView:62: TypeError: Cannot read property 'ui_billingAddress' of null`的 QML 警告。\n\n原因是编辑视图绑定到了一个客户端实例，该实例是搜索结果的一部分。当我们刷新搜索时，我们删除了旧的搜索结果，这意味着编辑视图现在绑定到`nullptr`并且不能再访问数据。由于用于执行导航的信号/槽的异步特性，即使您在刷新搜索之前导航到仪表板，这种情况也会继续发生。修复这些警告的一种方法是对视图中的所有绑定添加 null 检查，如果主对象为 null，则绑定到本地临时对象。考虑以下示例:\n\n```cpp\nStringEditorSingleLine {\n    property StringDecorator temporaryObject\n    stringDecorator: selectedClient ? selectedClient.ui_reference : \n    temporaryObject\n    anchors {\n        left: parent.left\n        right: parent.right\n    }\n}\n```\n\n所以，如果`selectedClient`不为空，绑定到该的`ui_reference`属性，否则绑定到`temporaryObject`。您甚至可以向根客户端属性添加一个间接层，并替换整个客户端对象:\n\n```cpp\nproperty Client selectedClient\nproperty Client localTemporaryClient\nproperty Client clientToBindTo: selectedClient ? selectedClient : localTemporaryClient\n```\n\n在这里，`selectedClient`会被家长设置为正常；`localTemporaryClient`不会被设置，因此将在本地创建一个默认实例。`clientToBindTo`将选择合适的对象使用，所有子控件都可以绑定到该对象。由于这些绑定是动态的，如果`selectedClient`在加载视图后被删除(如我们的情况)，那么`clientToBindTo`将自动切换。\n\n由于这只是一个示范项目，我们可以安全地忽略警告，因此我们不会在这里采取任何行动来简化事情。\n\n# 摘要\n\n在本章中，我们为客户端模型添加了数据库持久性。我们使它变得通用和灵活，这样我们可以通过简单地向我们的`DatabaseController`类添加一个新表来轻松地持久化其他模型层次结构。我们涵盖了所有核心的 CRUD 操作，包括与整个 JSON 对象相匹配的自由文本搜索功能。\n\n在[第 8 章](8.html)、 *Web 请求*中，我们将继续探讨在我们的应用之外获取数据的主题，并研究另一个极其常见的业务线应用需求，即向 Web 服务发出 HTTP 请求。"
  },
  {
    "path": "docs/learn-qt5/8.md",
    "content": "# 八、网络请求\n\n这一章将带我们到世界各地，因为我们从应用到互联网的道路上走得更远。从为我们编写一些帮助类来管理 web 请求开始，我们将从一个实时 RSS 源中提取数据，并通过一些 XML 处理来解释它。有了手头的解析数据，我们就可以使用我们的 QML 技巧，并在新视图中显示这些项目。点击其中一个 RSS 项目将启动一个网络浏览器窗口，以便更详细地查看相关文章。我们将涵盖以下主题:\n\n*   网络存取\n*   网络请求\n*   RSS 视图\n*   简易资讯聚合\n\n# 网络存取\n\n底层的组网协议协商全部由 Qt 内部处理，我们可以通过`QNetworkAccessManager`类轻松的与外界取得联系。为了能够访问该功能，我们需要将`network`模块添加到`cm-lib.pro`:\n\n```cpp\nQT += sql network\n```\n\nQt 的弱点之一是缺少接口，这使得单元测试在某些情况下很困难。如果我们只是直接使用`QNetworkAccessManager`，那么如果不对网络进行真正的调用，我们将无法测试我们的代码，这是不可取的。然而，这个问题的一个快速简单的解决方案是将 Qt 实现隐藏在我们自己的接口后面，我们将在这里这样做。\n\n就本章而言，我们需要对网络做的就是检查我们是否有连接，并发送一个 HTTP GET 请求。考虑到这一点，在一个新的文件夹`cm-lib/source/networking`中创建一个头文件`i-network-access-manager.h`，并实现接口:\n\n```cpp\n#ifndef INETWORKACCESSMANAGER_H\n#define INETWORKACCESSMANAGER_H\n#include <QNetworkReply>\n#include <QNetworkRequest>\n\nnamespace cm {\nnamespace networking {\nclass INetworkAccessManager\n{\npublic:\n    INetworkAccessManager(){}\n    virtual ~INetworkAccessManager(){}\n    virtual QNetworkReply* get(const QNetworkRequest& request) = 0;\n    virtual bool isNetworkAccessible() const = 0;\n};\n}}\n#endif\n```\n\n`QNetworkRequest`是另一个 Qt 类，表示通过网络发送的请求，`QNetworkReply`表示通过网络接收的响应。理想情况下，我们也将把这些实现隐藏在接口后面，但是现在让我们来处理网络访问接口。准备好之后，在同一个文件夹中创建一个具体的实现类`NetworkAccessManager`:\n\n`network-access-manager.h`:\n\n```cpp\n#ifndef NETWORKACCESSMANAGER_H\n#define NETWORKACCESSMANAGER_H\n#include <QObject>\n#include <QScopedPointer>\n#include <networking/i-network-access-manager.h>\nnamespace cm {\nnamespace networking {\nclass NetworkAccessManager : public QObject, public INetworkAccessManager\n{\n    Q_OBJECT\npublic:\n    explicit NetworkAccessManager(QObject* parent = nullptr);\n    ~NetworkAccessManager();\n    QNetworkReply* get(const QNetworkRequest& request) override;\n    bool isNetworkAccessible() const override;\nprivate:\n    class Implementation;\n    QScopedPointer<Implementation> implementation;\n};\n}}\n#endif\n```\n\n`network-access-manager.cpp`:\n\n```cpp\n#include \"network-access-manager.h\"\n#include <QNetworkAccessManager>\nnamespace cm {\nnamespace networking {\nclass NetworkAccessManager::Implementation\n{\npublic:\n    Implementation()\n    {}\n    QNetworkAccessManager networkAccessManager;\n};\nNetworkAccessManager::NetworkAccessManager(QObject *parent)\n    : QObject(parent)\n    , INetworkAccessManager()\n{\n    implementation.reset(new Implementation());\n}\nNetworkAccessManager::~NetworkAccessManager()\n{\n}\nQNetworkReply* NetworkAccessManager::get(const QNetworkRequest& request)\n{\n    return implementation->networkAccessManager.get(request);\n}\nbool NetworkAccessManager::isNetworkAccessible() const\n{\n    return implementation->networkAccessManager.networkAccessible() == QNetworkAccessManager::Accessible;\n}\n}}\n```\n\n我们所做的就是持有一个私有的`QNetworkAccessManager`实例，并通过它传递对我们接口的调用。该接口可以很容易地扩展到包括额外的功能，如相同方法的 HTTP POST 请求。\n\n# 网络请求\n\n如果您以前没有使用过 HTTP 协议，那么它可以归结为客户端和服务器之间由请求和响应组成的对话。比如我们可以在自己喜欢的网页浏览器中向[www.bbc.co.uk](http://www.bbc.co.uk)提出请求，就会收到包含各种新闻条目和文章的回复。在我们的`NetworkAccessManager`包装器的`get()`方法中，我们引用了一个`QNetworkRequest`(我们对服务器的请求)和一个`QNetworkReply`(服务器对我们的响应)。虽然我们不会直接将`QNetworkRequest`和`QNetworkReply`隐藏在它们自己独立的接口后面，但是我们将采用 web 请求和相应响应的概念，并为该交互创建一个接口和实现。仍然在`cm-lib/source/networking`中，创建一个接口头文件`i-web-request.h`:\n\n```cpp\n#ifndef IWEBREQUEST_H\n#define IWEBREQUEST_H\n#include <QUrl>\nnamespace cm {\nnamespace networking {\nclass IWebRequest\n{\npublic:\n    IWebRequest(){}\n    virtual ~IWebRequest(){}\n    virtual void execute() = 0;\n    virtual bool isBusy() const = 0;\n    virtual void setUrl(const QUrl& url) = 0;\n    virtual QUrl url() const = 0;\n};\n}}\n#endif\n```\n\nHTTP 请求的关键信息是请求要发送到的网址，由`QUrl` Qt 类表示。我们为属性提供了一个`url()`取值器和`setUrl()`变异器。另外两种方法是检查`isBusy()` web 请求对象是发出请求还是接收响应，以及是发送到`execute()`还是发送请求到网络。同样，有了接口，让我们直接进入实现，在同一个文件夹中有一个新的`WebRequest`类。\n\n`web-request.h`:\n\n```cpp\n#ifndef WEBREQUEST_H\n#define WEBREQUEST_H\n#include <QList>\n#include <QObject>\n#include <QSslError>\n#include <networking/i-network-access-manager.h>\n#include <networking/i-web-request.h>\nnamespace cm {\nnamespace networking {\nclass WebRequest : public QObject, public IWebRequest\n{\n    Q_OBJECT\npublic:\n    WebRequest(QObject* parent, INetworkAccessManager* networkAccessManager, const QUrl& url);\n    WebRequest(QObject* parent = nullptr) = delete;\n    ~WebRequest();\npublic:\n    void execute() override;\n    bool isBusy() const override;\n    void setUrl(const QUrl& url) override;\n    QUrl url() const override;\nsignals:\n    void error(QString message);\n    void isBusyChanged();\n    void requestComplete(int statusCode, QByteArray body);\n    void urlChanged();\nprivate slots:\n    void replyDelegate();\n    void sslErrorsDelegate( const QList<QSslError>& _errors );\nprivate:\n    class Implementation;\n    QScopedPointer<Implementation> implementation;\n};\n}}\n#endif\n```\n\n`web-request.cpp`:\n\n```cpp\n#include \"web-request.h\"\n\n#include <QMap>\n#include <QNetworkReply>\n#include <QNetworkRequest>\nnamespace cm {\nnamespace networking { // Private Implementation\nstatic const QMap<QNetworkReply::NetworkError, QString> networkErrorMapper = {\n    {QNetworkReply::ConnectionRefusedError, \"The remote server refused the connection (the server is not accepting requests).\"},\n    /* ...section shortened in print for brevity...*/\n    {QNetworkReply::UnknownServerError, \"An unknown error related to the server response was detected.\"}\n};\nclass WebRequest::Implementation\n{\npublic:\n    Implementation(WebRequest* _webRequest, INetworkAccessManager* _networkAccessManager, const QUrl& _url)\n        : webRequest(_webRequest)\n        , networkAccessManager(_networkAccessManager)\n        , url(_url)\n    {\n    }\n    WebRequest* webRequest{nullptr};\n    INetworkAccessManager* networkAccessManager{nullptr};\n    QUrl url {};\n    QNetworkReply* reply {nullptr};\npublic: \n    bool isBusy() const\n    {\n        return isBusy_;\n    }\n    void setIsBusy(bool value)\n    {\n        if (value != isBusy_) {\n            isBusy_ = value;\n            emit webRequest->isBusyChanged();\n        }\n    }\nprivate:\n    bool isBusy_{false};\n};\n}\nnamespace networking {  // Structors\nWebRequest::WebRequest(QObject* parent, INetworkAccessManager* networkAccessManager, const QUrl& url)\n    : QObject(parent)\n    , IWebRequest()\n{\n    implementation.reset(new WebRequest::Implementation(this, networkAccessManager, url));\n}\nWebRequest::~WebRequest()\n{\n}\n}\nnamespace networking { // Methods\nvoid WebRequest::execute()\n{\n    if(implementation->isBusy()) {\n        return;\n    }\n\n    if(!implementation->networkAccessManager->isNetworkAccessible()) {\n        emit error(\"Network not accessible\");\n        return;\n    }\n    implementation->setIsBusy(true);\n    QNetworkRequest request;\n    request.setUrl(implementation->url);\n    implementation->reply = implementation->networkAccessManager->get(request);\n    if(implementation->reply != nullptr) {\n        connect(implementation->reply, &QNetworkReply::finished, this, &WebRequest::replyDelegate);\n        connect(implementation->reply, &QNetworkReply::sslErrors, this, &WebRequest::sslErrorsDelegate);\n    }\n}\nbool WebRequest::isBusy() const\n{\n    return implementation->isBusy();\n}\nvoid WebRequest::setUrl(const QUrl& url)\n{\n    if(url != implementation->url) {\n        implementation->url = url;\n        emit urlChanged();\n    }\n}\nQUrl WebRequest::url() const\n{\n    return implementation->url;\n}\n}\nnamespace networking { // Private Slots\nvoid WebRequest::replyDelegate()\n{\n    implementation->setIsBusy(false);\n    if (implementation->reply == nullptr) {\n        emit error(\"Unexpected error - reply object is null\");\n        return;\n    }\n    disconnect(implementation->reply, &QNetworkReply::finished, this, &WebRequest::replyDelegate);\n    disconnect(implementation->reply, &QNetworkReply::sslErrors, this, &WebRequest::sslErrorsDelegate);\n    auto statusCode = implementation->reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();\n    auto responseBody = implementation->reply->readAll();\n    auto replyStatus = implementation->reply->error();\n    implementation->reply->deleteLater();\n    if (replyStatus != QNetworkReply::NoError) {\n        emit error(networkErrorMapper[implementation->reply->error()]);\n    }\n    emit requestComplete(statusCode, responseBody);\n}\nvoid WebRequest::sslErrorsDelegate(const QList<QSslError>& errors)\n{\n    QString sslError;\n    for (const auto& error : errors) {\n        sslError += error.errorString() + \"\\n\";\n    }\n    emit error(sslError);\n}\n}}\n```\n\n实现看起来比实际更复杂，这完全是因为冗长的错误代码映射。如果出现某种问题，Qt 将使用枚举器报告错误。映射的目的只是将枚举器与人类可读的错误描述相匹配，我们可以将该描述呈现给用户，或者写入控制台或日志文件。\n\n除了接口方法，我们还有一些信号可以用来告诉任何感兴趣的观察者已经发生的事件:\n\n*   `error()`将在出现问题时发出，并将错误描述作为参数传递\n*   当请求开始或结束，并且请求变得繁忙或空闲时，触发`isBusyChanged()`\n*   `requestComplete()`在响应被接收和处理后发出，将包含 HTTP 状态代码和代表响应主体的字节数组\n*   当网址更新时，将触发`urlChanged()`\n\n我们还有几个私有槽，它们将是处理回复和处理任何 SSL 错误的代理。当我们执行新的请求时，它们连接到`QNetworkReply`对象上的信号，当我们收到回复时，它们再次断开连接。\n\n实现的核心其实是两种方法——发送请求的`execute()`和处理响应的`replyDelegate()`。\n\n在执行时，我们首先确保我们没有忙于执行另一个请求，然后与网络访问管理器一起检查我们是否有可用的连接。假设我们这样做了，然后我们设置忙碌标志，并使用当前设置的网址构建一个`QNetworkRequest`。然后，我们将请求传递给我们的网络访问管理器(作为接口注入，这样我们就可以改变它的行为)，最后，我们连接我们的代理插槽并等待响应。\n\n当我们收到回复时，我们会在读取我们感兴趣的响应详细信息(主要是 HTTP 状态代码和响应正文)之前，取消繁忙标志并断开插槽。我们检查回复是否成功完成(请注意，4xx 或 5xx 范围内的“否定”HTTP 响应代码在此上下文中仍算作成功完成的请求)，并发出详细信息供任何感兴趣的方捕获和处理。\n\n# RSS 视图\n\n让我们给我们的应用添加一个新的视图，在那里我们可以使用我们的新类显示来自 web 服务的一些信息。\n\n这里没有什么新的或复杂的东西，所以我不会展示所有的代码，但有几个步骤需要记住:\n\n1.  在`cm-ui/views`中创建新的`RssView.qml`视图，并从`SplashView`复制 QML，用“Rss 视图”替换“闪屏视图”文本\n2.  将视图添加到`/views`前缀块的`views.qrc`中，并使用别名`RssView.qml`\n3.  将`goRssView()`信号加到`NavigationController`上\n\n4.  在`MasterView`中，将`onGoRssView`槽添加到连接元素，并使用它导航到`RssView`\n5.  在`NavigationBar`中，添加一个新的`NavigationButton`，其中`iconCharacter`为`\\uf09e`，描述为`RSS Feed`，而`hoverColour`为`#8acece`，使用`onNavigationButtonClicked`槽在`NavigationController`上调用`goRssView()`\n\n只需几个简单的步骤，我们现在就有了一个全新的视图，我们可以使用导航栏访问它:\n\n![](img/a39dd8f0-b57c-4e65-be77-0dbb773ed7fa.png)\n\n接下来，我们将通过以下步骤向视图添加上下文命令栏:\n\n1.  在`CommandController`中，添加新的私人成员列表`rssViewContextCommands`\n2.  添加访问器方法`ui_rssViewContextCommands()`\n\n3.  添加一个名为`ui_rssViewContextCommands`的`Q_PROPERTY`\n4.  增加一个新的插槽`onRssRefreshExecuted()`，只需向控制台写入一条调试消息；表示它已经被调用\n5.  将名为`rssRefreshCommand`的新命令附加到带有`0xf021`图标字符和“刷新”标签的`rssViewContextCommands`上，并将其连接到`onRssRefreshExecuted()`插槽\n6.  在`RssView`中，添加一个`CommandBar`组件，将`commandList`连接到命令控制器上的`ui_rssViewContextCommands`\n\n前几章的辛苦付出，现在真的有回报了；我们的新视图有自己的命令栏和功能齐全的刷新按钮。当您单击它时，它应该会写出您添加到控制台的调试消息:\n\n![](img/ae3e2a0c-6631-4e7f-a51d-bea4afd64603.png)\n\n接下来，我们需要创建我们的`NetworkAccessManager`和`WebRequest`类的实例。像往常一样，我们将这些添加到`MasterController`中，并为`CommandController`注入依赖。\n\n在`MasterController`中，添加两个新的私人成员:\n\n```cpp\nNetworkAccessManager* networkAccessManager{nullptr};\nWebRequest* rssWebRequest{nullptr};\n```\n\n请记住包含相关的标题。在`Implementation`构造函数中实例化这些新成员，确保它们是在`commandController`之前创建的:\n\n```cpp\nnetworkAccessManager = new NetworkAccessManager(masterController);\nrssWebRequest = new WebRequest(masterController, networkAccessManager, QUrl(\"http://feeds.bbci.co.uk/news/rss.xml?edition=uk\"));\n```\n\n这里我们使用的是英国广播公司与英国相关的 RSS 源的网址；只需替换超链接文本，就可以随意将此内容替换为您选择的另一个提要。\n\n接下来，将`rssWebRequest`作为新参数传递给`commandController`构造器:\n\n```cpp\ncommandController = new CommandController(masterController, databaseController, navigationController, newClient, clientSearch, rssWebRequest);\n```\n\n接下来，编辑`CommandController`将这个新参数作为界面的指针:\n\n```cpp\nexplicit CommandController(QObject* _parent = nullptr, IDatabaseController* databaseController = nullptr, NavigationController* navigationController = nullptr, models::Client* newClient = nullptr, models::ClientSearch* clientSearch = nullptr, networking::IWebRequest* rssWebRequest = nullptr);\n```\n\n通过`Implementation`构造函数传递该指针，并将其存储在私有成员变量中，就像我们对所有其他依赖项所做的那样:\n\n```cpp\nIWebRequest* rssWebRequest{nullptr};\n```\n\n我们现在可以更新`onRssRefreshExecuted()`槽来执行 web 请求:\n\n```cpp\nvoid CommandController::onRssRefreshExecuted()\n{\n    qDebug() << \"You executed the Rss Refresh command!\";\n\n    implementation->rssWebRequest->execute();\n}\n```\n\n命令控制器现在对用户按下刷新按钮做出反应，并执行 web 请求。然而，当我们收到回复时，我们目前没有做任何事情。让我们在公共插槽部分向`MasterController`添加一个委托:\n\n```cpp\nvoid MasterController::onRssReplyReceived(int statusCode, QByteArray body)\n{\n    qDebug() << \"Received RSS request response code \" << statusCode << \":\";\n    qDebug() << body;\n}\n```\n\n现在，在`Implementation`中实例化`rssWebRequest`之后，我们可以将`requestComplete`信号连接到我们的新代表:\n\n```cpp\nQObject::connect(rssWebRequest, &WebRequest::requestComplete, masterController, &MasterController::onRssReplyReceived);\n```\n\n现在构建并运行应用，导航到 RSS 视图，然后单击刷新。短暂延迟后，在执行请求时，您会看到各种各样的废话被打印到应用输出控制台:\n\n```cpp\nReceived RSS request response code 200 :\n\"<?xml version=\\\"1.0\\\" encoding=\\\"UTF-8\\\"?>\\n<?xml-stylesheet title=...”\n```\n\n恭喜你！你有一个 RSS 源！现在，它是什么？\n\n# 简易资讯聚合\n\n**丰富的网站摘要** ( **RSS** )是一种用于传递定期变化的网络内容的格式，本质上是一个完整的网站、新闻广播、博客或类似的浓缩到要点的内容。每一个条目都包含一些基本的信息，比如日期和描述性的标题，并且提供了一个指向包含完整文章的网页的超链接。\n\n数据是从 XML 扩展而来的，必须遵守在[http://www.rssboard.org/rss-specification](http://www.rssboard.org/rss-specification)描述的定义标准。\n\n就本例而言，将它归结为基础，XML 如下所示:\n\n```cpp\n<rss>\n    <channel>\n        <title></title>\n        <description></description>\n        <link></link>\n        <image>\n            <url></url>\n            <title></title>\n            <link></link>\n            <width></width>\n            <height></height>\n        </image>\n        <item>\n            <title></title>\n            <description></description>\n            <link></link>\n            <pubDate></pubDate>\n        </item>\n        <item>\n                …\n          </item>\n    </channel>\n</rss>\n```\n\n在根`<rss>`节点内部，我们有一个`<channel>`节点，它又包含一个`<image>`节点和一个或多个`<item>`节点的集合。\n\n我们将把这些节点建模为类，但是首先我们需要引入 XML 模块并编写一个小的助手类来为我们做一些解析。在`cm-lib.pro`和`cm-ui.pro`中，将`xml`模块添加到`QT`变量中的模块；考虑这个例子:\n\n```cpp\nQT += sql network xml\n```\n\n接下来，在新文件夹`cm-lib/source/utilities`中创建新的`XmlHelper`类。\n\n`xml-helper.h`:\n\n```cpp\n#ifndef XMLHELPER_H\n#define XMLHELPER_H\n#include <QDomNode>\n#include <QString>\nnamespace cm {\nnamespace utilities {\nclass XmlHelper\n{\npublic:\n    static QString toString(const QDomNode& domNode);\nprivate:\n    XmlHelper(){}\n    static void appendNode(const QDomNode& domNode, QString& output);\n};\n}}\n#endif\n```\n\n`xml-helper.cpp`:\n\n```cpp\n#include \"xml-helper.h\"\n\nnamespace cm {\nnamespace utilities {\nQString XmlHelper::toString(const QDomNode& domNode)\n{\n    QString returnValue;\n    for(auto i = 0; i < domNode.childNodes().size(); ++ i) {\n        QDomNode subNode = domNode.childNodes().at(i);\n        appendNode(subNode, returnValue);\n    }\n    return returnValue;\n}\nvoid XmlHelper::appendNode(const QDomNode& domNode, QString& output)\n{\n    if(domNode.nodeType() == QDomNode::TextNode) {\n        output.append(domNode.nodeValue());\n        return;\n    }\n    if(domNode.nodeType() == QDomNode::AttributeNode) {\n        output.append(\" \");\n        output.append(domNode.nodeName());\n        output.append(\"=\\\"\");\n        output.append(domNode.nodeValue());\n        output.append(\"\\\"\");\n        return;\n    }\n    if(domNode.nodeType() == QDomNode::ElementNode) {\n        output.append(\"<\");\n        output.append(domNode.nodeName());\n        // Add attributes\n        for(auto i = 0; i < domNode.attributes().size(); ++ i) {\n            QDomNode subNode = domNode.attributes().item(i);\n            appendNode(subNode, output);\n        }\n        output.append(\">\");\n        for(auto i = 0; i < domNode.childNodes().size(); ++ i) {\n            QDomNode subNode = domNode.childNodes().at(i);\n            appendNode(subNode, output);\n        }\n        output.append(\"</\" + domNode.nodeName() + \">\");\n    }\n}\n}}\n```\n\n关于这个类做什么，我就不赘述了，因为这不是本章的重点，但本质上，如果我们收到一个包含 HTML 标记的 XML 节点(这在 RSS 中很常见)，XML 解析器会有点混乱，也会把 HTML 分解成 XML 节点，这不是我们想要的。考虑这个例子:\n\n```cpp\n<xmlNode>\n    Here is something from a website that has a <a href=”http://www.bbc.co.uk”>hyperlink</a> in it.\n</xmlNode>\n```\n\n在这种情况下，XML 解析器会将`<a>`视为 XML，并将内容分解为三个子节点，如下所示:\n\n```cpp\n<xmlNode>\n    <textNode1>Here is something from a website that has a </textNode1>\n    <a href=”http://www.bbc.co.uk”>hyperlink</a>\n    <textNode2>in it.</textNode2>\n</xmlNode>\n```\n\n这使得很难在用户界面上向用户显示 xmlNode 的内容。相反，我们使用 XmlHelper 来手动解析内容，并构造一个字符串，这要容易得多。\n\n现在，让我们继续讨论 RSS 类。在新的`cm-lib/source/rss folder`中，创建新的`RssChannel`、`RssImage`和`RssItem`类。\n\n`rss-image.h`:\n\n```cpp\n#ifndef RSSIMAGE_H\n#define RSSIMAGE_H\n#include <QObject>\n#include <QScopedPointer>\n#include <QtXml/QDomNode>\n#include <cm-lib_global.h>\nnamespace cm {\nnamespace rss {\nclass CMLIBSHARED_EXPORT RssImage : public QObject\n{\n    Q_OBJECT\n    Q_PROPERTY(quint16 ui_height READ height CONSTANT)\n    Q_PROPERTY(QString ui_link READ link CONSTANT)\n    Q_PROPERTY(QString ui_title READ title CONSTANT)\n    Q_PROPERTY(QString ui_url READ url CONSTANT)\n    Q_PROPERTY(quint16 ui_width READ width CONSTANT)\npublic:\n    explicit RssImage(QObject* parent = nullptr, const QDomNode& domNode = QDomNode());\n    ~RssImage();\n    quint16 height() const;\n    const QString& link() const;\n    const QString& title() const;\n    const QString& url() const;\n    quint16 width() const;\nprivate:\n    class Implementation;\n    QScopedPointer<Implementation> implementation;\n};\n}}\n\n#endif\n```\n\n`rss-image.cpp`:\n\n```cpp\n#include \"rss-image.h\"\n\nnamespace cm {\nnamespace rss {\nclass RssImage::Implementation\n{\npublic:\n    QString url;    // Mandatory. URL of GIF, JPEG or PNG that represents the channel.\n    QString title;  // Mandatory.  Describes the image.\n    QString link;   // Mandatory.  URL of the site.\n    quint16 width;  // Optional.  Width in pixels.  Max 144, default \n                                                                    88.\n    quint16 height; // Optional.  Height in pixels.  Max 400, default \n                                                                    31\n    void update(const QDomNode& domNode)\n    {\n        QDomElement imageUrl = domNode.firstChildElement(\"url\");\n        if(!imageUrl.isNull()) {\n            url = imageUrl.text();\n        }\n        QDomElement imageTitle = domNode.firstChildElement(\"title\");\n        if(!imageTitle.isNull()) {\n            title = imageTitle.text();\n        }\n        QDomElement imageLink = domNode.firstChildElement(\"link\");\n        if(!imageLink.isNull()) {\n            link = imageLink.text();\n        }\n        QDomElement imageWidth = domNode.firstChildElement(\"width\");\n        if(!imageWidth.isNull()) {\n            width = static_cast<quint16>(imageWidth.text().toShort());\n        } else {\n            width = 88;\n        }\n        QDomElement imageHeight = domNode.firstChildElement(\"height\");\n        if(!imageHeight.isNull()) {\n            height = static_cast<quint16>\n                                  (imageHeight.text().toShort());\n        } else {\n            height = 31;\n        }\n    }\n};\nRssImage::RssImage(QObject* parent, const QDomNode& domNode)\n    : QObject(parent)\n{\n    implementation.reset(new Implementation());\n    implementation->update(domNode);\n}\nRssImage::~RssImage()\n{\n}\nquint16 RssImage::height() const\n{\n    return implementation->height;\n}\nconst QString& RssImage::link() const\n{\n    return implementation->link;\n}\nconst QString& RssImage::title() const\n{\n    return implementation->title;\n}\nconst QString& RssImage::url() const\n{\n    return implementation->url;\n}\nquint16 RssImage::width() const\n{\n    return implementation->width;\n}\n}}\n```\n\n这个类只是一个普通的数据模型，除了它将由 Qt 的`QDomNode`类表示的一个 XML `<image>`节点构建。我们使用`firstChildElement()`方法定位`<url>`、`<title>`和`<link>`强制子节点，然后通过`text()`方法访问每个节点的值。`<width>`和`<height>`节点是可选的，如果它们不存在，我们使用 88 x 31 像素的默认图像大小。\n\n`rss-item.h`:\n\n```cpp\n#ifndef RSSITEM_H\n#define RSSITEM_H\n#include <QDateTime>\n#include <QObject>\n#include <QscopedPointer>\n#include <QtXml/QDomNode>\n#include <cm-lib_global.h>\nnamespace cm {\nnamespace rss {\nclass CMLIBSHARED_EXPORT RssItem : public QObject\n{\n    Q_OBJECT\n    Q_PROPERTY(QString ui_description READ description CONSTANT)\n    Q_PROPERTY(QString ui_link READ link CONSTANT)\n    Q_PROPERTY(QDateTime ui_pubDate READ pubDate CONSTANT)\n    Q_PROPERTY(QString ui_title READ title CONSTANT)\npublic:\n    RssItem(QObject* parent = nullptr, const QDomNode& domNode = QDomNode());\n    ~RssItem();\n    const QString& description() const;\n    const QString& link() const;\n    const QDateTime& pubDate() const;\n    const QString& title() const;\nprivate:\n    class Implementation;\n    QScopedPointer<Implementation> implementation;\n};\n}}\n#endif\n```\n\n`rss-item.cpp`:\n\n```cpp\n#include \"rss-item.h\"\n#include <QTextStream>\n#include <utilities/xml-helper.h>\nusing namespace cm::utilities;\nnamespace cm {\nnamespace rss {\nclass RssItem::Implementation\n{\npublic:\n    Implementation(RssItem* _rssItem)\n        : rssItem(_rssItem)\n    {\n    }\n    RssItem* rssItem{nullptr};\n    QString description;    // This or Title mandatory.  Either the \n                            synopsis or full story.  HTML is allowed.\n    QString link;           // Optional. Link to full story.  Populated \n                                  if Description is only the synopsis.\n    QDateTime pubDate;      // Optional. When the item was published. \n                     RFC 822 format e.g. Sun, 19 May 2002 15:21:36 GMT.\n    QString title;          // This or Description mandatory.\n    void update(const QDomNode& domNode)\n    {\n        for(auto i = 0; i < domNode.childNodes().size(); ++ i) {\n            QDomNode childNode = domNode.childNodes().at(i);\n            if(childNode.nodeName() == \"description\") {\n                description = XmlHelper::toString(childNode);\n            }\n        }\n        QDomElement itemLink = domNode.firstChildElement(\"link\");\n        if(!itemLink.isNull()) {\n            link = itemLink.text();\n        }\n        QDomElement itemPubDate = domNode.firstChildElement(\"pubDate\");\n        if(!itemPubDate.isNull()) {\n            pubDate = QDateTime::fromString(itemPubDate.text(), \n                                                     Qt::RFC2822Date);\n        }\n        QDomElement itemTitle = domNode.firstChildElement(\"title\");\n        if(!itemTitle.isNull()) {\n            title = itemTitle.text();\n        }\n    }\n};\nRssItem::RssItem(QObject* parent, const QDomNode& domNode)\n{\n    implementation.reset(new Implementation(this));\n    implementation->update(domNode);\n}\nRssItem::~RssItem()\n{\n}\nconst QString& RssItem::description() const\n{\n    return implementation->description;\n}\nconst QString& RssItem::link() const\n{\n    return implementation->link;\n}\nconst QDateTime& RssItem::pubDate() const\n{\n    return implementation->pubDate;\n}\nconst QString& RssItem::title() const\n{\n    return implementation->title;\n}\n}}\n```\n\n这个班和上一个班差不多。这次我们在解析`<description>`节点时使用我们的 XMLHelper 类，因为它很有可能包含 HTML 标签。还要注意，当使用静态`QDateTime::fromString()`方法将字符串转换为`QDateTime`对象时，Qt 还包含`Qt::RFC2822Date`格式说明符。这是 RSS 规范中使用的格式，使我们不必自己手动解析日期。\n\n`rss-channel.h`:\n\n```cpp\n#ifndef RSSCHANNEL_H\n#define RSSCHANNEL_H\n#include <QDateTime>\n#include <QtXml/QDomElement>\n#include <QtXml/QDomNode>\n#include <QList>\n#include <QObject>\n#include <QtQml/QQmlListProperty>\n#include <QString>\n#include <cm-lib_global.h>\n#include <rss/rss-image.h>\n#include <rss/rss-item.h>\nnamespace cm {\nnamespace rss {\nclass CMLIBSHARED_EXPORT RssChannel : public QObject\n{\n    Q_OBJECT\n    Q_PROPERTY(QString ui_description READ description CONSTANT)\n    Q_PROPERTY(cm::rss::RssImage* ui_image READ image CONSTANT)\n    Q_PROPERTY(QQmlListProperty<cm::rss::RssItem> ui_items READ \n                                                ui_items CONSTANT)\n    Q_PROPERTY(QString ui_link READ link CONSTANT)\n    Q_PROPERTY(QString ui_title READ title CONSTANT)\npublic:\n    RssChannel(QObject* parent = nullptr, const QDomNode& domNode = QDomNode());\n    ~RssChannel();\n    void addItem(RssItem* item);\n    const QString& description() const;\n    RssImage* image() const;\n    const QList<RssItem*>& items() const;\n    const QString& link() const;\n    void setImage(RssImage* image);\n    const QString& title() const;\n    QQmlListProperty<RssItem> ui_items();\n    static RssChannel* fromXml(const QByteArray& xmlData, QObject* \n                                            parent = nullptr);\nprivate:\n    class Implementation;\n    QScopedPointer<Implementation> implementation;\n};\n}}\n#endif\n```\n\n`rss-channel.cpp`:\n\n```cpp\n#include \"rss-channel.h\"\n#include <QtXml/QDomDocument>\nnamespace cm {\nnamespace rss {\nclass RssChannel::Implementation\n{\npublic:\n    QString description;            // Mandatory.  Phrase or sentence describing the channel.\n    RssImage* image{nullptr};       // Optional.  Image representing the channel.\n    QList<RssItem*> items;          // Optional.  Collection representing stories.\n    QString link;                   // Mandatory.  URL to the corresponding HTML website.\n    QString title;                  // Mandatory.  THe name of the Channel.\n    void update(const QDomNode& domNode)\n    {\n        QDomElement channelDescription = domNode.firstChildElement(\"description\");\n        if(!channelDescription.isNull()) {\n            description = channelDescription.text();\n        }\n        QDomElement channelLink = domNode.firstChildElement(\"link\");\n        if(!channelLink.isNull()) {\n            link = channelLink.text();\n        }\n        QDomElement channelTitle = domNode.firstChildElement(\"title\");\n        if(!channelTitle.isNull()) {\n            title = channelTitle.text();\n        }\n    }\n};\nRssChannel::RssChannel(QObject* parent, const QDomNode& domNode)\n    : QObject(parent)\n{\n    implementation.reset(new Implementation());\n    implementation->update(domNode);\n}\nRssChannel::~RssChannel()\n{\n}\nvoid RssChannel::addItem(RssItem* item)\n{\n    if(!implementation->items.contains(item)) {\n        item->setParent(this);\n        implementation->items.push_back(item);\n    }\n}\nconst QString&  RssChannel::description() const\n{\n    return implementation->description;\n}\nRssImage* RssChannel::image() const\n{\n    return implementation->image;\n}\nconst QList<RssItem*>&  RssChannel::items() const\n{\n    return implementation->items;\n}\nconst QString&  RssChannel::link() const\n{\n    return implementation->link;\n}\nvoid RssChannel::setImage(RssImage* image)\n{\n    if(implementation->image) {\n        implementation->image->deleteLater();\n        implementation->image = nullptr;\n    }\n    image->setParent(this);\n    implementation->image = image;\n}\nconst QString& RssChannel::title() const\n{\n    return implementation->title;\n}\nQQmlListProperty<RssItem> RssChannel::ui_items()\n{\n    return QQmlListProperty<RssItem>(this, implementation->items);\n}\nRssChannel* RssChannel::fromXml(const QByteArray& xmlData, QObject* parent)\n{\n    QDomDocument doc;\n    doc.setContent(xmlData);\n    auto channelNodes = doc.elementsByTagName(\"channel\");\n    // Rss must have 1 channel\n    if(channelNodes.size() != 1) return nullptr;\n    RssChannel* channel = new RssChannel(parent, channelNodes.at(0));\n    auto imageNodes = doc.elementsByTagName(\"image\");\n    if(imageNodes.size() > 0) {\n        channel->setImage(new RssImage(channel, imageNodes.at(0)));\n    }\n    auto itemNodes = doc.elementsByTagName(\"item\");\n    for (auto i = 0; i < itemNodes.size(); ++ i) {\n        channel->addItem(new RssItem(channel, itemNodes.item(i)));\n    }\n    return channel;\n}\n}}\n```\n\n这个类与前面的类大致相同，但是因为这是我们的 XML 树的根对象，所以我们还有一个静态的`fromXml()`方法。这里的目标是从包含 RSS 提要 XML 的 RSS web 请求响应中获取字节数组，并让该方法为我们创建一个 RSS 频道、图像和项目层次结构。\n\n我们将 XML 字节数组传递给 Qt `QDomDocument`类，就像我们之前对 JSON 和`QJsonDocument`类所做的那样。我们使用`elementsByTagName()`方法找到`<channel>`标签，然后使用该标签作为构造器的`QDomNode`参数构造一个新的`RssChannel`对象。感谢`update()`方法，`RssChannel`填充自己的属性。然后，我们定位`<image>`和`<item>`子节点，并创建新的`RssImage`和`RssItem`实例，将其添加到根`RssChannel`对象中。同样，这些类能够从提供的`QDomNode`中填充它们自己的属性。\n\n在我们忘记之前，让我们也在`main()`中注册课程:\n\n```cpp\nqmlRegisterType<cm::rss::RssChannel>(\"CM\", 1, 0, \"RssChannel\");\nqmlRegisterType<cm::rss::RssImage>(\"CM\", 1, 0, \"RssImage\");\nqmlRegisterType<cm::rss::RssItem>(\"CM\", 1, 0, \"RssItem\");\n```\n\n我们现在可以在`MasterController`中添加一个`RssChannel`来绑定用户界面:\n\n1.  在`MasterController`中，添加一个新的`RssChannel*`类型的`rssChannel`私有成员变量\n2.  添加一个`rssChannel()`访问器方法\n3.  增加一个`rssChannelChanged()`信号\n4.  使用`READ`的访问器和`NOTIFY`的信号添加名为`ui_rssChannel`的`Q_PROPERTY`\n\n我们不会在没有任何 RSS 数据的情况下创建一个构造，而是在 RSS 回复委托中进行:\n\n```cpp\nvoid MasterController::onRssReplyReceived(int statusCode, QByteArray body)\n{\n    qDebug() << \"Received RSS request response code \" << statusCode << \":\";\n    qDebug() << body;\n    if(implementation->rssChannel) {\n        implementation->rssChannel->deleteLater();\n        implementation->rssChannel = nullptr;\n        emit rssChannelChanged();\n    }\n    implementation->rssChannel = RssChannel::fromXml(body, this);\n    emit rssChannelChanged();\n}\n```\n\n我们执行一些内务处理，检查内存中是否已经有一个旧的通道对象，如果有，它会使用`QObject`的`deleteLater()`方法安全地删除它。然后，我们继续使用来自 web 请求的 XML 数据构建一个新的通道。\n\nAlways use `deleteLater()` on `QObject` derived classes rather than the standard C++ `delete` keyword as the destruction will be synchronized with the event loop and you will minimize the risk of unexpected exceptions.\n\n我们将以类似于管理搜索结果的方式在响应中显示 RSS 项目，带有`ListView`和相关委托。将`RssItemDelegate.qml`添加到`cm-ui/components`并执行编辑`components.qrc`和`qmldir`文件的常规步骤:\n\n```cpp\nimport QtQuick 2.9\nimport assets 1.0\nimport CM 1.0\nItem {\n    property RssItem rssItem\n    implicitWidth: parent.width\n    implicitHeight: background.height\n    Rectangle {\n        id: background\n        width: parent.width\n        height: textPubDate.implicitHeight + textTitle.implicitHeight + \n                       borderBottom.height + (Style.sizeItemMargin * 3)\n        color: Style.colourPanelBackground\n        Text {\n            id: textPubDate\n            anchors {\n                top: parent.top\n                left: parent.left\n                right: parent.right\n                margins: Style.sizeItemMargin\n            }\n            text: Qt.formatDateTime(rssItem.ui_pubDate, \"ddd, d MMM \n                                                    yyyy @ h:mm ap\")\n            font {\n                pixelSize: Style.pixelSizeDataControls\n                italic: true\n                weight: Font.Light\n            }\n            color: Style.colorItemDateFont\n        }\n        Text {\n            id: textTitle\n            anchors {\n                top: textPubDate.bottom\n                left: parent.left\n                right: parent.right\n                margins: Style.sizeItemMargin\n            }\n            text: rssItem.ui_title\n            font {\n                pixelSize: Style.pixelSizeDataControls\n            }\n            color: Style.colorItemTitleFont\n            wrapMode: Text.Wrap\n        }\n        Rectangle {\n            id: borderBottom\n            anchors {\n                top: textTitle.bottom\n                left: parent.left\n                right: parent.right\n                topMargin: Style.sizeItemMargin\n            }\n            height: 1\n            color: Style.colorItemBorder\n        }\n        MouseArea {\n            anchors.fill: parent\n            cursorShape: Qt.PointingHandCursor\n            hoverEnabled: true\n            onEntered: background.state = \"hover\"\n            onExited: background.state = \"\"\n            onClicked: if(rssItem.ui_link !== \"\") {\n                           Qt.openUrlExternally(rssItem.ui_link);\n                       }\n        }\n        states: [\n            State {\n                name: \"hover\"\n                PropertyChanges {\n                    target: background\n                    color: Style.colourPanelBackgroundHover\n                }\n            }\n        ]\n    }\n}\n```\n\n为了支持这个组件，我们需要添加一些样式属性:\n\n```cpp\nreadonly property color colourItemBackground: \"#fefefe\"\nreadonly property color colourItemBackgroundHover: \"#efefef\"\nreadonly property color colorItemBorder: \"#efefef\"\nreadonly property color colorItemDateFont: \"#636363\"\nreadonly property color colorItemTitleFont: \"#131313\"\nreadonly property real sizeItemMargin: 5\n```\n\n我们现在可以在`RssView`中利用这个委托:\n\n```cpp\nimport QtQuick 2.9\nimport assets 1.0\nimport components 1.0\nItem {\n    Rectangle {\n        anchors.fill: parent\n        color: Style.colourBackground\n    }\n    ListView {\n        id: itemsView\n        anchors {\n            top: parent.top\n            left: parent.left\n            right: parent.right\n            bottom: commandBar.top\n            margins: Style.sizeHeaderMargin\n        }\n        clip: true\n        model: masterController.ui_rssChannel ? masterController.ui_rssChannel.ui_items : 0\n        delegate: RssItemDelegate {\n            rssItem: modelData\n        }\n    }\n    CommandBar {\n        id: commandBar\n        commandList: masterController.ui_commandController.ui_rssViewContextCommands\n    }\n}\n```\n\n构建并运行，导航到 RSS 视图，然后单击刷新按钮发出 web 请求并显示响应:\n\n![](img/7db92f1b-d929-4cf3-87ab-5160b63abe11.png)\n\n将鼠标悬停在项目上以查看光标效果，然后单击项目以在默认网络浏览器中打开它。Qt 在`Qt.openUrlExternally()`方法中为我们处理这个动作，我们将 RSS Item `link`属性传递给它。\n\n# 摘要\n\n在这一章中，我们扩展了应用之外的范围，并开始使用互联网上的 HTTP 请求与外部 API 进行交互。我们使用自己的接口抽象了 Qt 功能，以改善解耦，并使我们的组件更加测试友好。我们快速了解了 RSS 及其结构，以及如何使用 Qt 的 XML 模块处理 XML 节点树。最后，我们加强了我们一直在做的出色的用户界面工作，并添加了一个交互式视图来显示一个 RSS 提要，并为给定的网址启动默认的网络浏览器。\n\n在[第 9 章](9.html)、*总结*中，我们将了解打包我们的应用以部署到其他计算机所需的步骤。"
  },
  {
    "path": "docs/learn-qt5/9.md",
    "content": "# 九、打包\n\n在这一章中，我们将清理几个没有进入前几章的主题。通过将对象创建转移到对象工厂，我们将使我们的应用更具可测试性。我们将通过增加缩放功能使我们的用户界面更加动态。`EnumeratorDecorator`属性获得自己的 UI 组件，我们在添加联系人管理的时候会用到它们。最后，我们将通过打包和部署我们的应用来总结一切。我们将涵盖以下主题:\n\n*   对象工厂\n*   动态用户界面缩放\n*   向仪表板添加图像\n*   枚举选择器\n*   管理联系人\n*   我们应用的部署和安装\n\n# 对象工厂\n\n在一个更大的系统中，有更全面的`MasterController`测试，在私有实现中硬编码所有的对象创建将会引起问题，因为`MasterController`和它的依赖项之间的紧密耦合。一种选择是在`main()`中创建所有其他对象，并将它们注入`MasterController`构造器，就像我们对其他控制器所做的那样。这将意味着注入大量的构造函数参数，并且能够将`MasterController`实例保持为所有其他对象的父对象是很方便的，因此我们将注入控制器可以用于其所有对象创建需求的单个对象工厂。\n\n这个工厂模式的关键部分是将一切隐藏在接口后面，所以在测试`MasterController`时，可以传入一个模拟工厂，控制所有的对象创建。在`cm-lib`中，在`source/framework`中创建新的`i-object-factory.h`头文件:\n\n```cpp\n#ifndef IOBJECTFACTORY_H\n#define IOBJECTFACTORY_H\n\n#include <controllers/i-command-controller.h>\n#include <controllers/i-database-controller.h>\n#include <controllers/i-navigation-controller.h>\n#include <models/client.h>\n#include <models/client-search.h>\n#include <networking/i-network-access-manager.h>\n#include <networking/i-web-request.h>\n\nnamespace cm {\nnamespace framework {\n\nclass IObjectFactory\n{\npublic:\n    virtual ~IObjectFactory(){}\n\n    virtual models::Client* createClient(QObject* parent) const = 0;\n    virtual models::ClientSearch* createClientSearch(QObject* parent, controllers::IDatabaseController* databaseController) const = 0;\n    virtual controllers::ICommandController* createCommandController(QObject* parent, controllers::IDatabaseController* databaseController, controllers::INavigationController* navigationController, models::Client* newClient, models::ClientSearch* clientSearch, networking::IWebRequest* rssWebRequest) const = 0;\n    virtual controllers::IDatabaseController* createDatabaseController(QObject* parent) const = 0;\n    virtual controllers::INavigationController* createNavigationController(QObject* parent) const = 0;\n    virtual networking::INetworkAccessManager* createNetworkAccessManager(QObject* parent) const = 0;\n    virtual networking::IWebRequest* createWebRequest(QObject* parent, networking::INetworkAccessManager* networkAccessManager, const QUrl& url) const = 0;\n};\n\n}}\n\n#endif\n```\n\n除了模型之外，我们将创建的所有对象都将被移到接口后面。这是因为它们本质上只是数据容器，我们可以在没有副作用的测试场景中轻松创建真实的实例。\n\nWe will skip that exercise here for brevity and leave it as an exercise for the reader. Use `IDatabaseController` as an example or refer to the code samples.\n\n在工厂界面可用的情况下，更改`MasterController`构造函数，将一个实例作为依赖项:\n\n```cpp\nMasterController::MasterController(QObject* parent, IObjectFactory* objectFactory)\n    : QObject(parent)\n{\n    implementation.reset(new Implementation(this, objectFactory));\n}\n```\n\n我们将对象传递给`Implementation`，并将其存储在私有成员变量中，就像我们之前多次做的那样。有了可用的工厂，我们现在可以将所有基于`new`的对象创建语句移动到`IObjectFactory`接口的具体实现中(即`ObjectFactory`类)，并用更抽象和可测试的东西替换`MasterController`中的那些语句:\n\n```cpp\nImplementation(MasterController* _masterController, IObjectFactory* _objectFactory)\n    : masterController(_masterController)\n    , objectFactory(_objectFactory)\n{\n    databaseController = objectFactory->createDatabaseController(masterController);\n    clientSearch = objectFactory->createClientSearch(masterController, databaseController);\n    navigationController = objectFactory->createNavigationController(masterController);\n    networkAccessManager = objectFactory->createNetworkAccessManager(masterController);\n    rssWebRequest = objectFactory->createWebRequest(masterController, networkAccessManager, QUrl(\"http://feeds.bbci.co.uk/news/rss.xml?edition=uk\"));\n    QObject::connect(rssWebRequest, &IWebRequest::requestComplete, masterController, &MasterController::onRssReplyReceived);\n    newClient = objectFactory->createClient(masterController);\n    commandController = objectFactory->createCommandController(masterController, databaseController, navigationController, newClient, clientSearch, rssWebRequest);\n}\n```\n\n现在，当测试`MasterController`时，我们可以传入`IObjectFactory`接口的模拟实现，并控制对象的创建。除了实现`ObjectFactory`并在实例化时将其传递给`MasterController`之外，还有一个变化是在`main.cpp`中，我们现在需要将接口注册到`NavigationController`和`CommandController`，而不是具体的实现。我们通过简单地将`qmlRegisterType`语句与`qmlRegisterUncreatableType`伴随语句交换来做到这一点:\n\n```cpp\nqmlRegisterUncreatableType<cm::controllers::INavigationController>(\"CM\", 1, 0, \"INavigationController\", \"Interface\");\nqmlRegisterUncreatableType<cm::controllers::ICommandController>(\"CM\", 1, 0, \"ICommandController\", \"Interface\");\n```\n\n# 用户界面缩放\n\n在这本书里，我们非常关注响应用户界面，在可能的情况下使用锚点和相对定位，这样当用户调整窗口大小时，内容会适当地缩放和调整。我们还把所有的“硬编码”属性，比如大小和颜色，都放到了一个集中的 Style 对象中。\n\n如果我们选择一个与尺寸相关的属性，例如`sizeScreenMargin`，它当前有一个固定值`20`。如果我们决定增加`MasterView`中**窗口**元素的起始大小，该屏幕边距大小将保持不变。现在，由于 Style 对象，增加屏幕边距也非常容易，但是如果所有硬编码属性都可以随着我们的 **Window** 元素动态地上下缩放，那就太好了。这样，我们可以尝试不同的窗口大小，而不必每次都更新样式。\n\n正如我们已经看到的，内置的 JavaScript 支持进一步扩展了 QML 的灵活性，我们可以做到这一点。\n\n首先，让我们在样式中为窗口创建新的宽度和高度属性:\n\n```cpp\nreadonly property real widthWindow: 1920\nreadonly property real heightWindow: 1080\n```\n\n在`MasterView`中使用这些新属性:\n\n```cpp\nWindow {\n    width: Style.widthWindow\n    height: Style.heightWindow\n    ….\n}\n```\n\n到目前为止，我们在 Style 中创建的所有大小属性都与 1920 x 1080 的窗口大小相关，因此让我们将它记录为 Style 中的新属性:\n\n```cpp\nreadonly property real widthWindowReference: 1920\nreadonly property real heightWindowReference: 1080\n```\n\n然后，我们可以使用参考尺寸和实际尺寸来计算水平轴和垂直轴的比例因子。所以简单来说，如果我们在设计所有东西的时候都考虑到窗宽为 1000，然后我们把窗宽设置为 2000，我们希望所有东西水平缩放 2 倍。将以下功能添加到样式中:\n\n```cpp\nfunction hscale(size) {\n    return Math.round(size * (widthWindow / widthWindowReference))\n}\nfunction vscale(size) {\n    return Math.round(size * (heightWindow / heightWindowReference))\n}\nfunction tscale(size) {\n    return Math.round((hscale(size) + vscale(size)) / 2)\n}\n```\n\n`hscale`和`vscale`功能分别计算水平和垂直比例因子。对于某些大小属性，如字体的像素大小，没有独立的宽度和高度，因此我们可以使用`tscale`函数计算水平和垂直比例的平均值。\n\n然后，我们可以在适当的函数中包装我们想要缩放的任何属性。例如，我们的屏幕边距可以使用`tscale`功能:\n\n```cpp\nreadonly property real sizeScreenMargin: tscale(20)\n```\n\n现在，您不仅可以在样式中增加窗口的初始大小，而且您选择的属性将自动缩放到新的大小。\n\nA really useful module you can add to help with sizing is `QtQuick.Window`. We already added this to `MasterView` in order to access the Window element. There is another object in that module, Screen, which makes available information regarding the user’s display. It contains properties for things like the width and height of the screen, and orientation and pixel density, which can be useful if you’re working with high DPI displays such as the Microsoft Surface or Macbook. You can use these values in conjunction with your Style properties to do things such as making your window fullscreen, or make it 50% of the screen size and positioning it in the center of the display.\n\n# 仪表盘\n\n仪表板或“主页”是欢迎用户和展示当前游戏状态的好地方。每日信息、事实和数字、绩效图表，或者仅仅是一些公司品牌都可以帮助定位和聚焦用户。让我们稍微活跃一下仪表板视图，并演示如何在启动时显示图像。\n\n抓取您选择的纵横比为 1:1 的图像，这意味着宽度与高度相同。不必是方形的，对于本例来说，管理缩放更简单。我选择了 **Packt** 标志，500 x 500 像素，保存为`packt-logo-500x500.jpg`。保存到`cm/cm-ui/assets`并添加到我们的`assets.qrc`资源:\n\n```cpp\n<file alias=\"packt-logo-500x500\">img/packt-logo-500x500.jpg</file>\n```\n\n利用我们新的扩展功能，添加一些新的样式属性:\n\n```cpp\nreadonly property color colourDashboardBackground: \"#f36f24\"\nreadonly property color colourDashboardFont: \"#ffffff\"\nreadonly property int pixelSizeDashboard: tscale(36)\nreadonly property real sizeDashboardLogo: tscale(500)\n```\n\n然后，我们可以将我们的图像添加到`DashboardView`:\n\n```cpp\nItem {\n    Rectangle {\n        anchors.fill: parent\n        color: Style.colourDashboardBackground\n        Image {\n            id: logo\n            source: \"qrc:/img/packt-logo-500x500\"\n            anchors.centerIn: parent\n            width: Style.sizeDashboardLogo\n            height: Style.sizeDashboardLogo\n        }\n        Text {\n            anchors {\n                top: logo.bottom\n                horizontalCenter: logo.horizontalCenter\n            }\n            text: \"Client Management System\"\n            color: Style.colourDashboardFont\n            font.pixelSize: Style.pixelSizeDashboard\n        }\n    }\n}\n```\n\n现在，当我们转到仪表板时，我们可以看到一些更刺激的东西:\n\n![](img/4badcae8-575c-4c7b-b610-e366d5579693.png)\n\n# 枚举选择器\n\n回到[第 5 章](5.html)、*数据*，我们创建了一个联系模型，其中我们用`EnumeratorDecorator`实现了一个`ContactType`属性。对于我们在书中使用的其他基于字符串的属性，简单的 textbox 是捕获数据的好解决方案，但是我们如何捕获枚举值呢？不能期望用户知道枚举器的基础整数值，要求他们键入他们想要的选项的字符串表示形式是自找麻烦。我们真正想要的是一个下拉列表，它以某种方式利用了我们添加到类中的`contactTypeMapper`容器。我们希望向用户呈现字符串描述以供选择，然后将整数值存储在`EnumeratorDecorator`对象中。\n\n桌面应用通常以特定的方式呈现下拉列表，您按下某种选择器，然后弹出(或者更准确地说，下拉！)可供选择的可滚动选项列表。然而，QML 不仅面向跨平台，也面向跨设备应用。许多笔记本电脑都有支持触摸的屏幕，市场上出现了越来越多的兼具笔记本电脑和平板电脑功能的混合设备。因此，考虑我们的应用有多“手指友好”是很重要的，即使我们不打算为移动商店构建下一个大的东西，经典的下拉列表可能很难在触摸屏上使用。让我们改用移动设备上使用的基于按钮的方法。\n\n不幸的是，我们不能真正直接与 QML 现有的`std::map`合作，所以我们需要增加一些新的班级来弥补我们之间的差距。我们将每个键/值对表示为一个`DropDownValue`，并将这些对象的集合保存在一个`DropDown`对象中。一个`DropDown`对象应该在其构造函数中取一个`std::map<int, QString>`，为我们创建`DropDownValue`集合。\n\n在`cm-lib/source/data`中首先创建`DropDownValue`类。\n\n`dropdown-value.h`:\n\n```cpp\n#ifndef DROPDOWNVALUE_H\n#define DROPDOWNVALUE_H\n#include <QObject>\n#include <cm-lib_global.h>\nnamespace cm {\nnamespace data {\nclass CMLIBSHARED_EXPORT DropDownValue : public QObject\n{\n    Q_OBJECT\n    Q_PROPERTY(int ui_key MEMBER key CONSTANT )\n    Q_PROPERTY(QString ui_description MEMBER description CONSTANT)\npublic:\n    DropDownValue(QObject* parent = nullptr, int key = 0, const QString& description = \"\");\n    ~DropDownValue();\npublic:\n    int key{0};\n    QString description{\"\"};\n};\n}}\n#endif\n```\n\n`dropdown-value.cpp`:\n\n```cpp\n#include \"dropdown-value.h\"\nnamespace cm {\nnamespace data {\nDropDownValue::DropDownValue(QObject* parent, int _key, const QString& _description)\n        : QObject(parent)\n{\n    key = _key;\n    description = _description;\n}\nDropDownValue::~DropDownValue()\n{\n}\n}}\n```\n\n这里没有什么复杂的，它只是一个整数值和相关字符串描述的 QML 友好包装器。\n\n接下来，再次在`cm-lib/source/data`中创建`DropDown`类。\n\n`dropdown.h`:\n\n```cpp\n#ifndef DROPDOWN_H\n#define DROPDOWN_H\n#include <QObject>\n#include <QtQml/QQmlListProperty>\n#include <cm-lib_global.h>\n#include <data/dropdown-value.h>\nnamespace cm {\nnamespace data {\nclass CMLIBSHARED_EXPORT DropDown : public QObject\n{\n    Q_OBJECT\n    Q_PROPERTY(QQmlListProperty<cm::data::DropDownValue> ui_values READ ui_values CONSTANT)\npublic:\n    explicit DropDown(QObject* _parent = nullptr, const std::map<int, QString>& values = std::map<int, QString>());\n    ~DropDown();\npublic:\n    QQmlListProperty<DropDownValue> ui_values();\npublic slots:\n    QString findDescriptionForDropdownValue(int valueKey) const;\nprivate:\n    class Implementation;\n    QScopedPointer<Implementation> implementation;\n};\n}}\n#endif\n```\n\n`dropdown.cpp`:\n\n```cpp\n#include \"dropdown.h\"\n\nnamespace cm {\nnamespace data {\nclass DropDown::Implementation\n{\npublic:\n    Implementation(DropDown* _dropdown, const std::map<int, QString>& _values)\n        : dropdown(_dropdown)\n    {\n        for(auto pair : _values) {\n             values.append(new DropDownValue(_dropdown, pair.first, pair.second));\n        }\n    }\n    DropDown* dropdown{nullptr};\n    QList<DropDownValue*> values;\n};\nDropDown::DropDown(QObject* parent, const std::map<int, QString>& values)\n   : QObject(parent)\n{\n    implementation.reset(new DropDown::Implementation(this, values));\n}\nDropDown::~DropDown()\n{\n}\nQString DropDown::findDescriptionForDropdownValue(int valueKey) const\n{\n    for (auto value : implementation->values) {\n        if (value->key == valueKey) {\n            if(!value->description.isEmpty()) {\n                return value->description;\n            }\n            break;\n        }\n    }\n    return \"Select >\";\n}\nQQmlListProperty<DropDownValue> DropDown::ui_values()\n{\n    return QQmlListProperty<DropDownValue>(this, implementation->values);\n}\n}}\n```\n\n如上所述，我们实现了一个构造函数，它采用了我们在`EnumeratorDecorator`类中使用的相同类型的`std::map`，并基于它创建了一个`DropDownValue`对象的集合。然后，用户界面可以通过`ui_values`属性访问该集合。我们为用户界面提供的另一个功能是通过`findDescriptionForDropdownValue`公共槽，这允许用户界面从`EnumeratorDecorator`中选择一个整数值并获得相应的文本描述。如果没有当前选择(即描述为空字符串)，那么我们将返回`Select >`向用户表示他们需要进行选择。\n\n由于我们将在 QML 使用这些新类型，我们需要在`main.cpp`中注册它们:\n\n```cpp\nqmlRegisterType<cm::data::DropDown>(\"CM\", 1, 0, \"DropDown\");\nqmlRegisterType<cm::data::DropDownValue>(\"CM\", 1, 0, \"DropDownValue\");\n```\n\n向名为`ui_contactTypeDropDown`的联系人添加一个新的`DropDown`属性，并在构造函数中用`contactTypeMapper`实例化成员变量。现在，每当在用户界面中显示一个联系人时，相关的`DropDown`将可用。如果您想在整个应用中重用下拉列表，这可以很容易地进入一个专门的组件，比如下拉列表管理器，但是对于这个例子，让我们避免额外的复杂性。\n\n我们还需要能够从用户界面添加一个新的联系人对象，因此在`Client`中添加一个新的公共槽:\n\n```cpp\nvoid Client::addContact()\n{\n    contacts->addEntity(new Contact(this));\n    emit contactsChanged();\n}\n```\n\nC++ 完成后，我们可以继续进行用户界面实现。\n\n我们将需要下拉选择的几个组件。当呈现一个`EnumeratorDecorator`属性时，我们希望显示当前选择的值，就像我们使用字符串编辑器一样。在视觉上，它将类似于一个按钮，相关的字符串描述作为其标签，当按下时，用户将转换到第二个组件，本质上是一个视图。这个子视图将占据整个内容框架，并呈现所有可用枚举选项的列表，同样表示为按钮。当用户通过按下其中一个按钮进行选择时，他们将转换回原始视图，并且他们的选择将在原始组件中更新。\n\n首先，我们将创建用户将转换到的视图，它将列出所有可用的选项。为了支持这一点，我们需要 Style 中的一些附加属性:\n\n```cpp\nreadonly property color colourDataSelectorBackground: \"#131313\"\nreadonly property color colourDataControlsBackgroundSelected: \"#f36f24\"\nreadonly property color colourDataSelectorFont: \"#ffffff\"\nreadonly property int sizeDataControlsRadius: tscale(5)\n```\n\n在`cm-ui/components`中创建`EnumeratorSelectorView.qml`:\n\n```cpp\nimport QtQuick 2.9\nimport QtQuick.Controls 2.2\nimport CM 1.0\nimport assets 1.0\nItem {\n    id: stringSelectorView\n    property DropDown dropDown\n    property EnumeratorDecorator enumeratorDecorator\n    property int selectedValue\n    ScrollView {\n        id: scrollView\n        visible: true\n        anchors.fill: parent\n        anchors {\n            top: parent.bottom\n             left: parent.left\n             right: parent.right\n             bottom: parent.top\n             margins: Style.sizeScreenMargin\n        }\n        Flow {\n            flow: Grid.TopToBottom\n            spacing: Style.sizeControlSpacing\n            height: scrollView.height\n            Repeater {\n                id: repeaterAnswers\n                model: dropDown.ui_values\n                delegate:\n                    Rectangle {\n                        property bool isSelected: modelData.ui_key.ui_value === enumeratorDecorator.ui_value\n                        width: Style.widthDataControls\n                        height: Style.heightDataControls\n                        radius: Style.sizeDataControlsRadius\n                        color: isSelected ? Style.colourDataControlsBackgroundSelected : Style.colourDataSelectorBackground\n                        Text {\n                            anchors {\n                                fill: parent\n                                margins: Style.heightDataControls / 4\n                            }\n                            text: modelData.ui_description\n                            color: Style.colourDataSelectorFont\n                            font.pixelSize: Style.pixelSizeDataControls\n                            verticalAlignment: Qt.AlignVCenter\n                        }\n                        MouseArea {\n                            anchors.fill: parent\n                            onClicked: {\n                                selectedValue = modelData.ui_key;\n                                contentFrame.pop();\n                            }\n                        }\n                    }\n            }\n        }\n    }\n    Binding {\n        target: enumeratorDecorator\n        property: \"ui_value\"\n        value: selectedValue\n    }\n}\n```\n\n在这里，我们首次使用了**中继器**元件。中继器为它在模型属性中找到的每个项目实例化在其委托属性中定义的 QML 元素。我们将`DropDownValue`对象的集合作为其模型传递给它，并内联创建一个委托。委托本质上是另一个带有选择代码的按钮。我们可以创建一个新的自定义组件，并将其用于委托，以保持代码更干净，但为了简洁起见，我们将在这里跳过它。这个组件的关键部分是`Binding`元素，它为我们提供了到所提供的`EnumeratorDecorator`的双向绑定，以及`MouseArea`中的`onClicked`事件委托，它执行更新并将这个组件从堆栈中弹出，让我们返回到我们来自的任何视图。\n\n在`cm-ui/components`中创建新的`EnumeratorSelector.qml`:\n\n```cpp\nimport QtQuick 2.9\nimport QtQuick.Controls 2.2\nimport CM 1.0\nimport assets 1.0\nItem {\n    property DropDown dropDown\n    property EnumeratorDecorator enumeratorDecorator\n    id: enumeratorSelectorRoot\n    height: width > textLabel.width + textAnswer.width ? \n    Style.heightDataControls : Style.heightDataControls * 2\n    Flow {\n        anchors.fill: parent\n        Rectangle {\n            width: Style.widthDataControls\n            height: Style.heightDataControls\n            Text {\n                id: textLabel\n                anchors {\n                    fill: parent\n                    margins: Style.heightDataControls / 4\n                }\n                text: enumeratorDecorator.ui_label\n                color: Style.colourDataControlsFont\n                font.pixelSize: Style.pixelSizeDataControls\n                verticalAlignment: Qt.AlignVCenter\n            }\n        }\n        Rectangle {\n            id: buttonAnswer\n            width: Style.widthDataControls\n            height: Style.heightDataControls\n            radius: Style.sizeDataControlsRadius\n            enabled: dropDown ? dropDown.ui_values.length > 0 : false\n            color: Style.colourDataSelectorBackground\n            Text {\n                id: textAnswer\n                anchors {\n                    fill: parent\n                    margins: Style.heightDataControls / 4\n                }\n                text: dropDown.findDescriptionForDropdownValue(enumeratorDecorator.ui_value)\n                color: Style.colourDataSelectorFont\n                font.pixelSize: Style.pixelSizeDataControls\n                verticalAlignment: Qt.AlignVCenter\n            }\n            MouseArea {\n                anchors.fill: parent\n                onClicked: contentFrame.push(\"qrc:/components/EnumeratorSelectorView.qml\",\n {dropDown: enumeratorSelectorRoot.dropDown,\n enumeratorDecorator: enumeratorSelectorRoot.enumeratorDecorator})\n            }\n        }\n    }\n}\n```\n\n这个组件在布局上与`StringEditorSingleLine`有很多相似之处，但是它用按钮表示代替了文本元素。我们从绑定的`EnumeratorDecorator`中获取值，并将其传递给我们在`DropDown`类上创建的槽，以获取当前所选值的字符串描述。当用户按下按钮时，`MouseArea`的`onClicked`事件执行与我们在`MasterView`中看到的相同类型的视图转换，将用户带到新的`EnumeratorSelectorView`。\n\nWe’re cheating a bit here in that we are directly referencing the `StackView` in `MasterView` by its `contentFrame` ID. At design time, Qt Creator can’t know what `contentFrame` is as it is in a totally different file, so it may flag it as an error, and you certainly won’t get auto-completion. At runtime, however, this component will be part of the same QML hierarchy as `MasterView`, so it will be able to find it. This is a risky approach, because if another element in the hierarchy is also called `contentFrame`, then bad things may happen. A safer way to do this is to pass `contentFrame` all the way down through the QML hierarchy from `MasterView` as a `QtObject` property.\n\n当我们添加或编辑客户端时，我们当前忽略联系人，并且总是有一个空集合。让我们看看如何向集合中添加对象，并在使用时使用我们闪亮的新`EnumeratorSelector`。\n\n# 联系人\n\n我们将需要一些新的用户界面组件来管理我们的联系人。我们之前已经使用了`AddressEditor`来处理我们的地址细节，所以我们将继续使用该模型并创建一个`ContactEditor`组件。该组件将显示我们的联系人集合，每个联系人将由一个`ContactDelegate`代表。在最初创建一个新的客户端对象时，不会有任何联系人，所以我们也需要一些方法让用户添加一个新的。我们将通过按下按钮来启用它，并且我们将为可以添加到内容视图中的按钮创建一个新组件。让我们先做那个。\n\n为了支持这个新组件，像往常一样，我们将继续向 Style 添加一些属性:\n\n```cpp\nreadonly property real widthFormButton: 240\nreadonly property real heightFormButton: 60\nreadonly property color colourFormButtonBackground: \"#f36f24\"\nreadonly property color colourFormButtonFont: \"#ffffff\"\nreadonly property int pixelSizeFormButtonIcon: 32\nreadonly property int pixelSizeFormButtonText: 22\nreadonly property int sizeFormButtonRadius: 5\n```\n\n在`cm-ui/components`中创建`FormButton.qml`:\n\n```cpp\nimport QtQuick 2.9\nimport CM 1.0\nimport assets 1.0\nItem {\n    property alias iconCharacter: textIcon.text\n    property alias description: textDescription.text\n    signal formButtonClicked()\n    width: Style.widthFormButton\n    height: Style.heightFormButton\n    Rectangle {\n        id: background\n        anchors.fill: parent\n        color: Style.colourFormButtonBackground\n        radius: Style.sizeFormButtonRadius\n        Text {\n            id: textIcon\n            anchors {\n                verticalCenter: parent.verticalCenter\n                left: parent.left\n                margins: Style.heightFormButton / 4\n            }\n            font {\n                family: Style.fontAwesome\n                pixelSize: Style.pixelSizeFormButtonIcon\n            }\n            color: Style.colourFormButtonFont\n            text: \"\\uf11a\"\n            horizontalAlignment: Text.AlignHCenter\n            verticalAlignment: Text.AlignVCenter\n        }\n        Text {\n            id: textDescription\n            anchors {\n                left: textIcon.left\n                bottom: parent.bottom\n                top: parent.top\n                right: parent.right\n            }\n            font.pixelSize: Style.pixelSizeFormButtonText\n            color: Style.colourFormButtonFont\n            text: \"SET ME!!\"\n            horizontalAlignment: Text.AlignHCenter\n            verticalAlignment: Text.AlignVCenter\n        }\n        MouseArea {\n            anchors.fill: parent\n            cursorShape: Qt.PointingHandCursor\n            hoverEnabled: true\n            onEntered: background.state = \"hover\"\n            onExited: background.state = \"\"\n            onClicked: formButtonClicked()\n        }\n        states: [\n            State {\n                name: \"hover\"\n                PropertyChanges {\n                    target: background\n                    color: Qt.darker(Style.colourFormButtonBackground)\n                }\n            }\n        ]\n    }\n}\n```\n\n在这里，我们结合了本书前面所写的`NavigationButton`和`CommandButton`控件的各个方面。唯一真正的区别是，它是为了在主内容框架中更自由地使用，而不是局限于其中一个工具栏。\n\n接下来，让我们添加用于显示/编辑单个联系人对象的组件。在`cm-ui/components`中创建`ContactDelegate.qml`:\n\n```cpp\nimport QtQuick 2.9\nimport CM 1.0\nimport assets 1.0\nItem {\n    property Contact contact\n    implicitWidth: flow.implicitWidth\n    implicitHeight: flow.implicitHeight + borderBottom.implicitHeight + Style.sizeItemMargin\n    height: width > selectorType.width + textAddress.width + Style.sizeScreenMargin\n            ? selectorType.height + borderBottom.height + Style.sizeItemMargin\n            : selectorType.height + textAddress.height + Style.sizeScreenMargin + borderBottom.height + Style.sizeItemMargin\n    Flow {\n        id: flow\n        width: parent.width\n        spacing: Style.sizeScreenMargin\n        EnumeratorSelector {\n            id: selectorType\n            width: Style.widthDataControls\n            dropDown: contact.ui_contactTypeDropDown\n            enumeratorDecorator: contact.ui_contactType\n        }\n        StringEditorSingleLine {\n            id: textAddress\n            width: Style.widthDataControls\n            stringDecorator: contact.ui_address\n        }\n    }\n    Rectangle {\n        id: borderBottom\n        anchors {\n            top: flow.bottom\n            left: parent.left\n            right: parent.right\n            topMargin: Style.sizeItemMargin\n        }\n        height: 1\n        color: Style.colorItemBorder\n    }\n}\n```\n\n这和我们在[第八章](8.html)*网页请求*中增加的`RssItemDelegate`差不多。我们添加新的`EnumeratorSelector`并将其绑定到`ui_contactType`属性，使用`ui_contactTypeDropDown`为控件提供所需的下拉信息。\n\n在`cm-ui/components`中创建`ContactsEditor.qml`:\n\n```cpp\nimport QtQuick 2.9\nimport CM 1.0\nimport assets 1.0\nPanel {\n    property Client client\n    id: contactsEditorRoot\n    contentComponent:\n        Column {\n            id: column\n            spacing: Style.sizeControlSpacing\n            Repeater {\n                id: contactsView\n                model: client.ui_contacts\n                delegate:\n                    ContactDelegate {\n                        width: contactsEditorRoot.width\n                        contact: modelData\n                    }\n            }\n            FormButton {\n                iconCharacter: \"\\uf067\"\n                description: \"Add Contact\"\n                onFormButtonClicked: {\n                    client.addContact();\n                }\n            }\n        }\n}\n```\n\n我们已经在`ContactDelegate`和`FormButton`控件中完成了所有的辛苦工作，所以这真的很短很甜。我们将所有内容添加到一个`Panel`中，这样外观和感觉将与其余视图保持一致。我们使用另一个`Repeater`，这样我们就可以为集合中的每个联系人旋转一个`ContactDelegate`，在联系人之后，我们会立即显示一个按钮，将新联系人添加到列表中。为了做到这一点，我们称之为我们在本章前面添加的`addContact()`方法。\n\n现在，我们只需要将`ContactsEditor`的实例添加到`CreateClientView`中:\n\n```cpp\nContactsEditor {\n    width: scrollView.width\n    client: newClient\n    headerText: \"Contact Details\"\n}\n```\n\n我们也可以在`EditClientView`中使用相同的组件:\n\n```cpp\nContactsEditor {\n    width: scrollView.width\n    client: selectedClient\n    headerText: \"Contact Details\"\n}\n```\n\n就这样。构建并运行，您可以添加和编辑联系人到您的心的内容:\n\n![](img/c1b86286-e84d-43b7-9f18-ba51c1a8023a.png)\n\n保存新客户端后，如果查看数据库，您会看到联系人阵列已相应更新，如下图所示:\n\n![](img/cf2f77a9-4754-4ab2-9509-63d059de3288.png)\n\n现在剩下的就是约会集合了，我们已经介绍了解决这个问题所需的所有技巧，所以我们将把它作为读者的练习，然后进入最后一个主题——向最终用户部署我们的应用。\n\n# 部署准备\n\n我们应用的中心部分是`cm-ui`可执行文件。这是最终用户启动的文件，它打开图形窗口，编排我们写的所有花哨的东西。当我们在 Qt Creator 中运行`cm-ui`项目时，它为我们打开了可执行文件，一切都很完美。然而，不幸的是，将我们的应用分发给另一个用户比简单地在他们的机器上复制可执行文件并启动它要复杂得多。\n\n我们的可执行文件有各种各样的依赖项，需要这些依赖项才能运行。依赖的一个主要例子是我们自己的`cm-lib`库。我们几乎所有的业务逻辑都隐藏在那里，没有这些功能，我们的用户界面就做不了什么。跨各种操作系统的依赖关系解决的实现细节是复杂的，远远超出了本书的范围。然而，我们的应用的基本要求是相同的，与平台无关。\n\n我们需要考虑四类依赖关系，并确保它们在目标用户的机器上就位，以便我们的应用正常运行:\n\n*   第 1 项:我们手动编写或添加到解决方案中的自定义库。在这种情况下，我们需要担心的只是`cm-lib`库。\n*   第 2 项:我们的应用直接或间接链接到的 Qt 框架部分。通过我们添加到`.pro`文件中的模块，我们已经知道了其中的一些，例如`qml`和快速模块需要`QtQml`和`QtQuick`组件。\n*   第 3 项:Qt 框架本身的任何内部依赖。这包括特定于平台的文件、QML 子系统的资源以及第三方库，如`sqlite`或`openssl`。\n*   第 4 项:我们用来构建应用的 C++ 编译器所需的任何库。\n\n我们已经对第 1 项进行了广泛的研究，回到[第 2 章](2.html)、*项目结构*中，我们投入了大量的工作来精确控制输出的去向。我们真的不需要担心第 2 项和第 3 项，因为我们已经在我们的开发机器上完全安装了 Qt 框架，这为我们处理了一切。同样，第 4 项由我们使用的工具包决定，如果我们的机器上有可用的编译器，那么我们也有它需要的库。\n\n明确我们需要为最终用户(他们很可能没有安装 Qt 或其他开发工具)复制什么是一项非常痛苦的工作。即使我们做到了这一点，将所有东西打包成一个简洁的包或安装程序，让用户可以简单地运行，这本身就是一个项目。幸运的是，Qt 以捆绑工具的形式为我们提供了一些帮助。\n\nLinux 和 macOS X 有一个应用包的概念，由此应用的可执行文件和所有依赖项可以汇总到一个文件中，然后只需点击一个按钮就可以轻松分发和启动。Windows 更自由一点，如果我们想把所有文件打包成一个可安装的文件，我们需要做更多的工作，但是 Qt 又来了，它提供了出色的 Qt Installer Framework，为我们简化了它。\n\n让我们依次看看每个操作系统，并为每个操作系统生成一个应用包或安装程序。\n\n# x 是什么\n\n首先，在发布模式下使用您选择的工具包构建解决方案。你已经知道，如果我们在 Qt Creator 中按下运行按钮，我们的应用就会启动，一切都很好。但是，导航到 Finder 中的`cm-ui.app`文件，尝试直接启动；有了这个，事情就不那么乐观了:\n\n![](img/57963241-c098-441c-84b2-01a504c479a6.png)\n\n这里的问题是缺少依赖项。我们可以使用**otol**来看看这些依赖项是什么。首先，将`cm-ui.app`包复制到一个新目录— `cm/installer/osx`。\n\nThis isn’t strictly necessary, but I like to keep build and deployment files separate. This way, if we make a code change and rebuild the solution, we will only update the app in the binaries folder, and our deployment files remain untouched.\n\n接下来，在应用包中四处看看，看看我们在做什么。在 Finder 中，按住 *Ctrl* 并单击我们刚刚复制到安装程序文件夹中的`cm-ui.app`，然后选择“显示包内容”。我们感兴趣的是`Contents/MacOS`文件夹。在那里，你会发现我们的`cm-ui`应用可执行。\n\n识别后，打开命令终端，导航至`cm/installer/osx`，在可执行文件上运行`otool`:\n\n```cpp\n$ otool -L cm-ui.app/Contents/MacOS/cm-ui\n```\n\n您将看到与以下内容相同(或相似)的输出:\n\n```cpp\ncm-ui:\nlibcm-lib.1.dylib (compatibility version 1.0.0, current version 1.0.0)\n@rpath/QtQuick.framework/Versions/5/QtQuick (compatibility version 5.9.0, current version 5.9.1)\n@rpath/QtQml.framework/Versions/5/QtQml (compatibility version 5.9.0, current version 5.9.1)\n@rpath/QtNetwork.framework/Versions/5/QtNetwork (compatibility version 5.9.0, current version 5.9.1)\n@rpath/QtCore.framework/Versions/5/QtCore (compatibility version 5.9.0, current version 5.9.1)\n/System/Library/Frameworks/DiskArbitration.framework/Versions/A/DiskArbitration (compatibility version 1.0.0, current version 1.0.0)\n/System/Library/Frameworks/IOKit.framework/Versions/A/IOKit (compatibility version 1.0.0, current version 275.0.0)\n@rpath/QtGui.framework/Versions/5/QtGui (compatibility version 5.9.0, current version 5.9.1)\n@rpath/QtXml.framework/Versions/5/QtXml (compatibility version 5.9.0, current version 5.9.1)\n/System/Library/Frameworks/OpenGL.framework/Versions/A/OpenGL (compatibility version 1.0.0, current version 1.0.0)\n/System/Library/Frameworks/AGL.framework/Versions/A/AGL (compatibility version 1.0.0, current version 1.0.0)\n/usr/lib/libc++.1.dylib (compatibility version 1.0.0, current version 307.5.0)\n/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1238.50.2)\n```\n\n让我们提醒自己需要考虑的依赖关系，并看看它们如何与我们刚刚看到的输出相关联:\n\n*   我们手动编写或添加到解决方案中的自定义库(`cm-lib`)。这是`libcm-lib.1.dylib`的参考。没有路径组件的事实表明，该工具不太确定该文件的位置。它应该和可执行文件在同一个文件夹吗？应该在标准的`/usr/lib/`文件夹里吗？幸运的是，我们可以在打包应用时指定该文件的位置。\n*   我们的应用链接到的 Qt 框架部分。`QtQuick`、`QtQml`等都是我们在`cm-ui`代码中直接引用的框架模块。其中一些是通过我们的`cm-ui.pro`文件中的 QT 变量显式引入的，而其他的是使用像 QML 这样的东西隐式引入的。\n*   Qt 框架本身的任何内部依赖。我们没有看到前面列出的那些，但是如果我们对`QtQuick`模块运行 otool，你会看到它依赖于`QtQml`、`QtNetwork`、`QtGui`和`QtCore`。还需要几个系统级的库，比如 OpenGL，我们没有明确针对它进行编码，但是 Qt 使用了它。\n*   我们用来构建应用的 C++ 编译器所需的任何库；`libc++.1.dylib`在这里脱颖而出。\n\n为了手动绑定我们所有的依赖项，我们可以将它们全部复制到应用包中，然后执行一些重新配置步骤来更新我们从 otool 中看到的位置元数据。\n\n让我们选择一个框架依赖项——`QtQuick`——并快速完成我们必须做的事情来实现这一点，然后我们将转向真正方便的工具，它为我们完成所有这些非常不愉快的繁重工作。\n\n首先，我们将创建一个`Frameworks`目录，系统将在其中搜索捆绑的依赖项:\n\n```cpp\n$ mkdir cm-ui.app/Contents/Frameworks\n```\n\n接下来，我们将把引用的文件物理复制到新目录中。由于前面的`LC_RPATH`条目，我们知道在我们的开发机器上哪里可以找到现有的文件，在本例中为`/Users/<Your Username>/Qt5.9.1/5.9.1/clang_64/lib`:\n\n```cpp\n$ cp -R /Users/<Your Username>  /Qt5.9.1 /5.9.1/clang_64 /lib/ QtQuick.framework cm-ui.app/Contents/Frameworks\n```\n\n然后，我们需要使用`install_name_tool`为复制的库文件更改共享库标识名:\n\n```cpp\n$ install_name_tool -id @executable_path /../Frameworks / QtQuick.framework/Versions/5/QtQuick cm-ui.app /Contents /Frameworks / QtQuick.framework/Versions/5/QtQuick\n```\n\n这里的语法是`install_name_tool -id [New name] [Shared library file]`。为了获得库文件(不是框架包，这是我们复制的)，我们深入到`Versions/5/QtQuick`。我们将该二进制文件的标识设置为可执行文件将查找它的位置，在本例中，该位置在可执行文件本身的上一级(`../`)的`Frameworks`文件夹中。\n\n接下来，我们还需要更新可执行文件的依赖项列表，以便在正确的位置查找这个新文件:\n\n```cpp\n$ install_name_tool -change @rpath/QtQuick.framework/Versions/5/QtQuick @executable_path/../Frameworks/QtQuick.framework/Versions/5/QtQuick cm-ui.app/Contents/MacOs/cm-ui\n```\n\n这里的语法是`install_name_tool -change [old value] [new value] [executable file]`。我们希望将旧的`QtQuick`条目改为我们刚刚添加的新框架路径。同样，我们使用`@executable_path`变量，以便依赖项总是位于相对于可执行文件的相同位置。现在，可执行文件和共享库中的元数据相互匹配，并且与`Frameworks`文件夹相关，我们现在已经将该文件夹添加到我们的应用包中。\n\n记住，这还不是全部，因为`QtQuick`本身有依赖关系，所以我们也需要复制和重新配置所有那些文件，然后检查它们的依赖关系。一旦我们用完了我们的`cm-ui`可执行文件的整个依赖树，我们还需要为我们的`cm-lib`库重复这个过程。可以想象，这很快就会变得乏味。\n\n幸运的是`macdeployqt` Qt Mac 部署工具正是我们这里需要的。它扫描一个可执行文件中的 Qt 依赖项，并将它们复制到我们的应用包中，以便我们处理重新配置工作。该工具位于已安装工具包的`bin`文件夹中，您已经使用例如`/Qt/5.9.1/5.9.1/clang_64/bin`构建了应用。\n\n在命令终端中，如下执行`macdeployqt`(假设你在`cm/installer/osx`目录中):\n\n```cpp\n$ <Path to bin>/macdeployqt cm-ui.app -qmldir=<Qt Projects>/cm/cm-ui -libpath=<Qt Projects>/cm/binaries/osx/clang/x64/release\n```\n\n请记住用系统上的完整路径替换尖括号中的参数(或者将可执行路径添加到系统 PATH 变量中)。\n\n`qmldir`标志告诉工具在哪里扫描 QML 进口，并设置为我们的用户界面项目文件夹。`libpath`标志用于指定我们编译的`cm-lib`文件的位置。\n\n该操作的输出如下:\n\n```cpp\nFile exists, skip copy: \"cm-ui.app/Contents/PlugIns/quick/libqtquick2plugin.dylib\"\nFile exists, skip copy: \"cm-ui.app/Contents/PlugIns/quick/libqtquickcontrols2plugin.dylib\"\nFile exists, skip copy: \"cm-ui.app/Contents/PlugIns/quick/libqtquickcontrols2materialstyleplugin.dylib\"\nFile exists, skip copy: \"cm-ui.app/Contents/PlugIns/quick/libqtquickcontrols2universalstyleplugin.dylib\"\nFile exists, skip copy: \"cm-ui.app/Contents/PlugIns/quick/libwindowplugin.dylib\"\nFile exists, skip copy: \"cm-ui.app/Contents/PlugIns/quick/libqtquicktemplates2plugin.dylib\"\nFile exists, skip copy: \"cm-ui.app/Contents/PlugIns/quick/libqtquickcontrols2materialstyleplugin.dylib\"\nFile exists, skip copy: \"cm-ui.app/Contents/PlugIns/quick/libqtquickcontrols2materialstyleplugin.dylib\"\nFile exists, skip copy: \"cm-ui.app/Contents/PlugIns/quick/libqtquickcontrols2universalstyleplugin.dylib\"\nFile exists, skip copy: \"cm-ui.app/Contents/PlugIns/quick/libqtquickcontrols2universalstyleplugin.dylib\"\nWARNING: Plugin \"libqsqlodbc.dylib\" uses private API and is not Mac App store compliant.\nWARNING: Plugin \"libqsqlpsql.dylib\" uses private API and is not Mac App store compliant.\nERROR: no file at \"/opt/local/lib/mysql55/mysql/libmysqlclient.18.dylib\"\nERROR: no file at \"/usr/local/lib/libpq.5.dylib\"\n```\n\nQt 在 SQL 模块上有点古怪，如果你使用一个 SQL 驱动程序，它会尝试将它们打包；但是，我们知道我们只使用 SQLite，不需要 MySQL 或 PostgreSQL，因此我们可以放心地忽略那些错误。\n\n一旦执行，您应该能够在 Finder 中再次显示包内容，并看到所有已准备好并等待部署的依赖项，如图所示:\n\n![](img/ded5936e-733d-4c71-8b31-9786ceda7e81.png)\n\n多么节省时间啊！它已经创建了适当的文件结构，并为我们复制了所有 Qt 模块和插件，以及我们的`cm-lib`共享库。现在尝试执行`cm-ui.app`文件，应该可以成功启动应用。\n\n# Linux 操作系统\n\nLinux 打包和部署与 OS X 大体相似，我们不会在相同的细节层次上讨论它，所以如果还没有，至少先浏览一下 OS X 部分。与所有平台一样，首先要做的是在**发布**模式下使用您选择的工具包构建解决方案，以便生成二进制文件。\n\nWhen building in Release mode for the first time, I received the “cannot find -lGL” error. This was because the `dev` libraries for OpenGL were not installed on my system. One way of obtaining these libraries is to install FreeGlut:\n`$ sudo apt-get update`\n`$ sudo apt-get install build-essential`\n`$ sudo apt-get install freeglut3-dev`\n\n编译完成后，将`cm-ui`二进制文件复制到新的`cm/installer/linux`目录中。\n\n接下来，我们可以看看我们的应用有哪些依赖关系。在命令终端中，切换到`cm/installer/linux`文件夹并运行`ldd`:\n\n```cpp\n$ ldd <Qt Projects>/cm/binaries/linux/gcc/x64/release/cm-ui\n```\n\n您将看到类似如下的输出:\n\n```cpp\nlinux-vdso.so.1 => (0x00007ffdeb1c2000)\nlibcm-lib.so.1 => /usr/lib/libcm-lib.so.1 (0x00007f624243d000)\nlibQt5Gui.so.5 => /home/nick/Qt/5.9.1/gcc_64/lib/libQt5Gui.so.5 (0x00007f6241c8f000)\nlibQt5Qml.so.5 => /home/nick/Qt/5.9.1/gcc_64/lib/libQt5Qml.so.5 (0x00007f6241698000)\nlibQt5Xml.so.5 => /home/nick/Qt/5.9.1/gcc_64/lib/libQt5Xml.so.5 (0x00007f624145e000)\nlibQt5Core.so.5 => /home/nick/Qt/5.9.1/gcc_64/lib/libQt5Core.so.5 (0x00007f6240d24000)\nlibstdc++.so.6 => /usr/lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007f62409a1000)\nlibgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f624078b000)\nlibc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f62403c1000)\nlibQt5Sql.so.5 => /home/nick/Qt/5.9.1/gcc_64/lib/libQt5Sql.so.5 (0x00007f6240179000)\nlibQt5Network.so.5 => /home/nick/Qt/5.9.1/gcc_64/lib/libQt5Network.so.5 (0x00007f623fde8000)\nlibpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f623fbcb000)\nlibGL.so.1 => /usr/lib/x86_64-linux-gnu/mesa/libGL.so.1 (0x00007f623f958000)\nlibz.so.1 => /lib/x86_64-linux-gnu/libz.so.1 (0x00007f623f73e000)\nlibm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f623f435000)\nlibrt.so.1 => /lib/x86_64-linux-gnu/librt.so.1 (0x00007f623f22c000)\nlibicui18n.so.56 => /home/nick/Qt/5.9.1/gcc_64/lib/libicui18n.so.56 (0x00007f623ed93000)\nlibicuuc.so.56 => /home/nick/Qt/5.9.1/gcc_64/lib/libicuuc.so.56 (0x00007f623e9db000)\nlibicudata.so.56 => /home/nick/Qt/5.9.1/gcc_64/lib/libicudata.so.56 (0x00007f623cff7000)\nlibdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f623cdf3000)\nlibgthread-2.0.so.0 => /usr/lib/x86_64-linux-gnu/libgthread-2.0.so.0 (0x00007f623cbf1000)\nlibglib-2.0.so.0 => /lib/x86_64-linux-gnu/libglib-2.0.so.0 (0x00007f623c8df000)\n/lib64/ld-linux-x86-64.so.2 (0x0000562f21a5c000)\nlibexpat.so.1 => /lib/x86_64-linux-gnu/libexpat.so.1 (0x00007f623c6b6000)\nlibxcb-dri3.so.0 => /usr/lib/x86_64-linux-gnu/libxcb-dri3.so.0 (0x00007f623c4b2000)\nlibxcb-present.so.0 => /usr/lib/x86_64-linux-gnu/libxcb-present.so.0 (0x00007f623c2af000)\nlibxcb-sync.so.1 => /usr/lib/x86_64-linux-gnu/libxcb-sync.so.1 (0x00007f623c0a8000)\nlibxshmfence.so.1 => /usr/lib/x86_64-linux-gnu/libxshmfence.so.1 (0x00007f623bea4000)\nlibglapi.so.0 => /usr/lib/x86_64-linux-gnu/libglapi.so.0 (0x00007f623bc75000)\nlibXext.so.6 => /usr/lib/x86_64-linux-gnu/libXext.so.6 (0x00007f623ba63000)\nlibXdamage.so.1 => /usr/lib/x86_64-linux-gnu/libXdamage.so.1 (0x00007f623b85f000)\nlibXfixes.so.3 => /usr/lib/x86_64-linux-gnu/libXfixes.so.3 (0x00007f623b659000)\nlibX11-xcb.so.1 => /usr/lib/x86_64-linux-gnu/libX11-xcb.so.1 (0x00007f623b457000)\nlibX11.so.6 => /usr/lib/x86_64-linux-gnu/libX11.so.6 (0x00007f623b11c000)\nlibxcb-glx.so.0 => /usr/lib/x86_64-linux-gnu/libxcb-glx.so.0 (0x00007f623af03000)\nlibxcb-dri2.so.0 => /usr/lib/x86_64-linux-gnu/libxcb-dri2.so.0 (0x00007f623acfe000)\nlibxcb.so.1 => /usr/lib/x86_64-linux-gnu/libxcb.so.1 (0x00007f623aadb000)\nlibXxf86vm.so.1 => /usr/lib/x86_64-linux-gnu/libXxf86vm.so.1 (0x00007f623a8d5000)\nlibdrm.so.2 => /usr/lib/x86_64-linux-gnu/libdrm.so.2 (0x00007f623a6c4000)\nlibpcre.so.3 => /lib/x86_64-linux-gnu/libpcre.so.3 (0x00007f623a453000)\nlibXau.so.6 => /usr/lib/x86_64-linux-gnu/libXau.so.6 (0x00007f623a24e000)\nlibXdmcp.so.6 => /usr/lib/x86_64-linux-gnu/libXdmcp.so.6 (0x00007f623a048000)\n```\n\n这是一些依赖列表！关键是，请注意对我们的`cm-lib`库的依赖:\n\n```cpp\nlibcm-lib.so.1 => /usr/lib/libcm-lib.so.1\n```\n\n这表明可执行文件将在`/usr/lib`文件夹中查找我们的库，所以在我们继续之前，让我们通过将`libcm-lib.so.1`复制到`/usr/lib`来确保它在那里可用:\n\n```cpp\n$ sudo cp <Qt Projects>/cm/binaries/linux/gcc/x64/release/libcm-lib.so.1 /usr/lib\n```\n\n我们已经可以猜测手动管理所有这些依赖项将会是一场怎样的噩梦，已经讨论了 OS X 过程并看到有多少依赖项，所以在我们的 Kit 的`bin`文件夹中一定有一个工具可以为我们完成这一切，对吗？嗯，有也没有。没有官方的 Qt 工具，我们可以像 OS X 和视窗系统那样开箱即用。幸运的是，Qt 社区`probonopd`的一名优秀成员前来救援，填补了与`linuxdeployqt`的空白。\n\n你可以在[https://github.com/probonopd/linuxdeployqt](https://github.com/probonopd/linuxdeployqt)的 GitHub 项目发布页面获得一个`linuxdeployqt`应用图片。下载文件(`linuxdeployqt-continuous-x86_64.AppImage`)然后使其可执行:\n\n```cpp\n$ chmod a+x <Path to downloaded file>/linuxdeployqt-continuous-x86_64.AppImage\n```\n\n然后我们可以执行它，让它为我们发挥它基于依赖的魔力。先把目录改成`cm/installer/linux`:\n\n```cpp\n$ <Path to downloaded file>/linuxdeployqt-continuous-x86_64.AppImage cm-ui -qmldir=<Qt Projects>/cm/cm-ui -appimage\n```\n\n`qmldir`标志告诉工具在哪里扫描 QML 进口，并设置为我们的用户界面项目文件夹。`appimage`标志用来让工具为我们创建一个应用镜像文件，这是一个里面捆绑了所有东西的单个文件。\n\n事情第一次可能不会完美。您的输出可能如下所示:\n\n```cpp\nERROR: Desktop file missing, creating a default one (you will probably want to edit it)\nERROR: Icon file missing, creating a default one (you will probably want to edit it)\nERROR: \"/usr/bin/qmake -query\" exited with 1 : \"qmake: could not exec '/usr/lib/x86_64-linux-gnu/qt4/bin/qmake': No such file or directory\\n\"\nERROR: Qt path could not be determined from qmake on the $PATH\nERROR: Make sure you have the correct Qt on your $PATH\nERROR: You can check this with qmake -v\n```\n\n前两个错误只是因为我们没有提供桌面文件或图标，已经为我们生成了默认值；我们可以忽略这些。剩下的都是因为`linuxdeployqt`不知道`qmake`在哪里。我们可以提供路径作为一个额外的参数(`-qmake=<PATH>`)，或者为了避免我们每次都必须这样做，我们可以将其添加到我们的 path 环境变量中:\n\n```cpp\n$ export PATH=<Qt Path>/5.9.1/gcc_64/bin/:$PATH\n```\n\n然后，我们可以通过尝试检索版本信息来检查是否可以找到 qmake:\n\n```cpp\n$ qmake -v\n```\n\n如果是快乐的，你会看到版本信息:\n\n```cpp\nQMake version 3.1\nUsing Qt version 5.9.1 in /home/nick/Qt/5.9.1/gcc_64/lib\n```\n\n修复后，我们现在可以再次尝试运行`linuxdeployqt`命令。然而，我们已经解决了一个问题，但现在遇到了另一个问题:\n\n```cpp\nERROR: Desktop file missing, creating a default one (you will probably want to edit it)\nERROR: Icon file missing, creating a default one (you will probably want to edit it)\nERROR: ldd outputLine: \"libmysqlclient.so.18 => not found\"\nERROR: for binary: \"/home/nick/Qt/5.9.1/gcc_64/plugins/sqldrivers/libqsqlmysql.so\"\nERROR: Please ensure that all libraries can be found by ldd. Aborting.\n```\n\n再次忽略前两个错误。现在它找不到 MySQL 驱动程序，这很烦人，因为我们甚至不是 MySQL，它与我们在 OS X 上看到的 Qt SQL 怪癖是一样的。作为一种变通方法，让我们通过临时重命名来有效地“隐藏”我们不想要的工具中的 SQL 驱动程序:\n\n```cpp\n$ cd <Qt Path>/5.9.1/gcc_64/plugins/sqldrivers\n$ mv libqsqlmysql.so libqsqlmysql.so_ignore\n$ mv libqsqlpsql.so libqsqlpsql.so_ignore\n```\n\n再次运行`linuxdeployqt`命令。这次您将获得大量输出，最终得到一条成功消息，包括以下内容:\n\n```cpp\nApp name for filename: Application\ndest_path: Application-x86_64.AppImage\n```\n\n这是在告诉我们，我们的 app 图像已经被命名为`Application-x86_64.AppImage`，并保存到`Downloads`文件夹中。\n\n看看文件管理器，您会发现它在我们的可执行文件旁边添加了各种文件和目录:\n\n![](img/12883b0f-bef0-4b40-b640-55dda2680b47.png)\n\n它还将`Application-x86_64.AppImage`文件存放在`Downloads`文件夹中，该文件夹是一个包含所有依赖项的独立可执行包。但是，如果您前往`Downloads`并尝试启动`AppImage`，则可能会出现错误(通过终端命令执行以查看错误消息):\n\n```cpp\nQXcbIntegration: Cannot create platform OpenGL context, neither GLX nor EGL are enabled\n```\n\n这似乎是`linuxdeployqt`缺少一些依赖关系的问题，但是出于某种原因，第二次运行工具会神奇地获得它们。再次执行`linuxdeployqt`命令，嘿，很快，`AppImage`就可以正常工作了。\n\n# Windows 操作系统\n\n首先，在**发布**模式下使用您选择的套件构建解决方案。完成后，将`cm-ui.exe`和`cm-lib.dll`应用二进制文件复制到新的`cm/installer/windows/packages/com.packtpub.cm/data`目录。这种奇怪的目录结构将在下一节——Qt Installer Framework——中解释，我们只是在后面为自己保存一些额外的拷贝。\n\n接下来，让我们提醒自己需要考虑的依赖性:\n\n*   第 1 项:我们手动编写或添加到解决方案中的自定义库(`cm-lib`)\n*   第 2 项:我们的应用链接到的 Qt 框架部分\n*   第 3 项:Qt 框架本身的任何内部依赖\n*   第 4 项:我们用来构建应用的 C++ 编译器所需的任何库\n\n好消息是第一项已经完成了！Windows 将在可执行文件所在的文件夹中查找该可执行文件的依赖项。这真的很有帮助，通过简单地将 DLL 复制到与可执行文件相同的文件夹中，我们已经处理了这种依赖性。Qt Installer 框架从一个给定的文件夹中获取所有文件，并将它们部署到目标机器上彼此相对的相同位置，因此我们知道这在部署后也会被保留。\n\n坏消息是，手动管理剩余的步骤有点像噩梦。通过查看我们明确添加到`*.pro`文件中的模块，我们可以对我们需要 Qt 的哪些部分进行初步尝试。这将是来自`cm-ui`和`sql`的`qml`、`quick`和`xml`，默认情况下还包括来自`cm-lib`核心的网络和`xml`。在文件浏览器中，导航至`<Qt Installation Folder>/5.9.1/<Kit>/bin`。在那里，您可以找到与这些模块相关的所有二进制文件，例如`qml`模块的`Qt5Qml.dll`。\n\n我们可以使用我们为`cm-lib.dll`所做的方法，并简单地手动将每个 Qt 动态链接库文件复制到数据文件夹中。这将完成第 2 项，虽然非常乏味，但相当简单。然而，第 3 项是一项我们自己真的不想做的痛苦练习。\n\n幸运的是`windeployqt` Qt Windows 部署工具正是我们这里需要的。它扫描一个`.exe`文件寻找 Qt 依赖项，并将它们复制到我们的安装文件夹中。该工具位于已安装工具包的`bin`文件夹中，您已经使用例如`/Qt/5.9.1/mingw32/bin`构建了应用。\n\n在命令终端中，执行`windeployqt`如下:\n\n```cpp\n$ <Path to bin>/windeployqt.exe --qmldir <Qt Projects>/cm/cm-ui <Qt Projects>/cm/installer/windows/packages/com.packtpub.cm/data/cm-ui.exe --compiler-runtime\n```\n\n请记住用系统上的完整路径替换尖括号中的参数(或者将可执行路径添加到系统 PATH 变量中)。\n\n`qmldir`标志告诉工具在哪里扫描 QML 进口，并设置为我们的用户界面项目文件夹。在我们告诉工具要扫描哪个`.exe`依赖项后，`compiler-runtime`标志表示我们也想要编译器运行时文件，所以它甚至会为我们处理第 4 项作为奖励！\n\nBy default, found dependencies will subsequently be copied to the same folder as the executable being scanned. This is a good reason to copy the compiled binaries to a dedicated installer folder first so that development project output and content for deployment remain separate.\n\n一旦执行，您应该会看到一大块输出。虽然很容易让人认为“哦，那是已经完成的事情，所以一切都必须正常”，但浏览输出是个好主意，即使你不确定它在做什么，因为你有时会发现一些明显的问题，你可以采取行动来解决。\n\n例如，当第一次部署 MinGW 工具包构建时，我遇到了给定的行:\n\n```cpp\nWarning: Cannot find GCC installation directory. g++.exe must be in the path.\n```\n\n尽管该命令已经成功执行，并且我可以在安装程序文件夹中看到一大堆 Qt 依赖项，但实际上我遗漏了 GCC 依赖项。按照说明将`<Qt Installation path>/Tools/mingw530_32/bin`添加到我的系统环境变量中的 PATH 变量是一个简单的修复。在重新启动命令终端并再次运行`windeployqt`命令后，它随后成功完成，没有警告，并且 GCC 文件与所有 Qt 二进制文件一起出现在数据中。如果没有听到这个安静的小警告，我会继续处理一些潜在的关键缺失文件。\n\n如您所见，`windeployqt`是一个巨大的时间节省器，但不幸的是，它不是银弹，有时会错过所需的文件。像 Dependency Walker 这样的工具是存在的，可以帮助详细分析依赖树，但是一个很好的起点就是从数据文件夹手动启动`cm-ui`可执行文件，看看会发生什么。在我们的例子中，是这样的:\n\n![](img/9705df93-43d7-4d61-a747-23ea31e54225.png)\n\n坏消息是它不起作用，但好消息是至少它清楚地告诉我们为什么它不起作用——它缺少`Qt5Sql.dll`依赖。我们知道我们确实有依赖关系，因为当我们开始做数据库工作时，我们必须把`sql`模块添加到我们的`.pro`文件中。但是，等等，我们刚刚执行了一个命令，应该会为我们引入所有的 Qt 依赖项，对吗？是的，我不知道为什么这个工具遗漏了一些它真正应该知道的依赖关系，但是它确实遗漏了。我不知道这是 bug、疏忽还是与底层第三方 SQLite 实现相关的许可限制，但无论如何，简单的解决方案是我们只需要自己复制它。\n\n前往`<Qt Installation>/5.9.1/<kit>/bin`并将`Qt5Sql.dll`复制到我们的数据文件夹。再次启动`cm-ui.exe`并欢呼，它成功开启！\n\nOne other thing to look out for apart from missing `.dll` files from the bin directory is missing files/folders from the plugins directory. You will see in our case that several folders have been copied successfully (bearer, iconengines, and such), but sometimes they don’t, and can be very difficult to figure out as you don’t get a helpful error message like we did with the missing DLL. I can only recommend three things in that situation: trial, error, and the internet.\n\n因此，我们现在有了一个包含我们可爱的应用二进制文件和一大堆类似可爱的其他文件和文件夹的文件夹。现在怎么办？我们可以简单地将文件夹批量复制到用户的机器上，让他们像我们一样启动可执行文件。然而，一个更整洁、更专业的解决方案是将所有东西打包成一个漂亮的安装包，这就是 Qt Installer Framework 工具的用途。\n\n# Qt 安装程序框架\n\n让我们编辑我们的 Qt 安装，并获取 Qt 安装程序框架。\n\n从您的 Qt 安装目录启动维护工具应用，您将看到一个与我们第一次安装 Qt 时看到的向导几乎相同的向导。要将 Qt 安装程序框架添加到现有安装中，请执行以下步骤:\n\n1.  登录您的 Qt 帐户或跳过\n2.  选择添加或删除组件，然后单击下一步\n3.  在选择组件对话框中，选择工具> Qt 安装程序框架 3.0，然后单击下一步\n4.  单击更新开始安装\n\n一旦完成，您可以在`Qt/Tools/QtInstallerFramework/3.0`中找到安装的工具。\n\nYou can add further modules, kits, and such in exactly the same way. Any components you already have installed will be unaffected unless you actively deselect them.\n\nQt 安装程序框架需要两个特定的目录:配置和包。Config 是一个单一的配置，它将安装程序描述为一个整体，而您可以将多个包(或组件)捆绑在同一个安装包中。每个组件在 packages 文件夹中都有自己的子目录，一个数据文件夹包含要为该组件安装的所有项目，一个元文件夹保存该包的配置数据。\n\n在我们的例子中，虽然我们有两个项目(`cm-lib`和`cm-ui`)，但是分发一个而不分发另一个是没有意义的，所以我们将把文件聚合到一个包中。包的一个常见命名约定是`com.<publisher>.<component>`，所以我们将命名我们的`com.packtpub.cm.`我们已经在前一节中创建了所需的数据文件夹(对未来计划来说太棒了！)并且`windeployqt`给我们塞了满满的文件。\n\n这里没有必要的命名约定，所以如果您愿意，可以随意将包命名为其他名称。如果我们想将一个额外的可选组件与我们的应用捆绑在一起，我们只需创建一个包含相关数据和元文件的额外包文件夹(例如`com.packtpub.amazingcomponent`)，包括一个单独的`package.xml`来配置该组件。\n\n创建任何丢失的文件夹，以便在`cm/installer/windows`中得到以下文件夹结构:\n\n![](img/08e3a715-ebf8-4bec-acb8-bab8f8b1c958.png)\n\n为了补充这些文件夹，我们还需要提供两个 XML 配置文件。\n\n在配置子文件夹中创建`config.xml`:\n\n```cpp\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Installer>\n    <Name>Client Management</Name>\n    <Version>1.0.0</Version>\n    <Title>Client Management Application Installer</Title>\n    <Publisher>Packt Software Publishing</Publisher>\n    <StartMenuDir>Client Management</StartMenuDir>\n    <TargetDir>@HomeDir@/ClientManagement</TargetDir>\n</Installer>\n```\n\n此配置文件自定义安装程序的行为。我们在此指定的属性如下:\n\n| 财产 | 目的 |\n| `Name` | 应用名称 |\n| `Version` | 应用版本 |\n| `Title` | 标题栏中显示的安装程序名称 |\n| `Publisher` | 软件的发行者 |\n| `StartMenuDir` | “开始”菜单中的默认程序组 |\n| `TargetDir` | 应用安装的默认目标目录 |\n\nYou will note strange @ symbols in the `TargetDir` property, and they define a predefined variable `HomeDir` that allows us to dynamically obtain a path to the end user’s home directory. You can also access the values of other properties in the same way, for example, `@ProductName@` will return “Client Management”. Further information is available at [http://doc.qt.io/qtinstallerframework/scripting.html#predefined-variables](http://doc.qt.io/qtinstallerframework/scripting.html#predefined-variables).\n\n接下来，在`packages/com.packtpub.cm/meta`子文件夹中创建`package.xml`:\n\n```cpp\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Package>\n    <DisplayName>Client Management application</DisplayName>\n    <Description>Install the Client Management application.</Description>\n    <Version>1.0.0</Version>\n    <ReleaseDate>2017-10-30</ReleaseDate>\n    <Licenses>\n        <License name=\"Fictional Training License Agreement\" file=\"license.txt\" />\n    </Licenses>\n    <Default>true</Default>\n</Package>\n```\n\n该文件使用以下属性配置`com.packtpub.cm`包(我们的客户端管理应用):\n\n| 财产 | 目的 |\n| `DisplayName` | 组件的名称。 |\n| `Description` | 选择组件时显示的文本。 |\n| `Version` | 组件的版本(用于促进组件更新)。 |\n| `ReleaseDate` | 组件发布的日期。 |\n| `Licenses` | 安装软件包必须同意的许可证集合。许可协议的文本是从元文件夹中配置文件旁边的指定文件中获取的。 |\n| `Default` | 表示默认情况下是否选择组件的布尔标志。 |\n\n您还需要在元文件夹中创建`license.txt`；在这种情况下，内容并不重要，因为它只是为了演示，所以在里面写任何旧的废话。\n\n有了所有的二进制文件、依赖项和配置，我们现在可以在命令终端中运行 Qt 框架安装程序来生成我们的安装包。首先将目录改为`cm/installer/windows`文件夹，然后执行`binarycreator`:\n\n```cpp\n$ <Qt Installation Path> \\Tools \\QtInstallerFramework \\3.0\\ bin\\ binarycreator.exe -c config\\config.xml -p packages ClientManagementInstaller.exe\n```\n\n`-c`标志告诉工具`config.xml`文件的位置和`-p`所有包的位置。最后一个参数是您想要给结果安装程序的名称。\n\n随着我们的应用被整齐地打包到一个安装程序文件`ClientManagementInstaller.exe`中，我们现在可以轻松地将其分发给最终用户进行安装。\n\n# 装置\n\n启动安装程序后，您将看到一个欢迎对话框，其内容来自我们的`config.xml`文件:\n\n![](img/3f37cf0b-bffa-4217-aa87-6b2ae057355f.png)\n\n然后，系统会提示我们指定安装的目标目录，我们希望安装后，该文件夹将包含我们在数据文件夹中找到的所有文件和文件夹:\n\n![](img/205b591a-cfc4-4cf2-ba78-58238f6a3a10.png)\n\n然后，我们会看到一个通过包目录定义的所有组件的列表，在这种情况下，它只是`com.packtpub.cm`文件夹中的应用和依赖项:\n\n![](img/01881dcf-369b-463f-b905-267d30053ea9.png)\n\n接下来，我们将看到我们在`packages.xml`中定义的任何许可证，包括文本文件中提供的许可证信息:\n\n![](img/ccff9ba7-834c-4e72-8ce1-0a5d5ac24cf5.png)\n\n然后系统会提示我们输入开始菜单快捷方式，默认值由`config.xml`提供:\n\n![](img/058b12dd-aa3e-4869-9137-3904578bf55e.png)\n\n我们现在准备安装，并在确认之前提供磁盘使用统计信息:\n\n![](img/7af94f05-a95f-48d5-a092-0d683ae56cfc.png)\n\n安装完成后，经过短暂的等待，我们会看到一个最终确认对话框:\n\n![](img/16c6a965-1105-4705-bf3a-4487e1f71473.png)\n\n您应该会在目标目录中看到一个新的`ClientManagement`文件夹，其中包含我们安装的应用！\n\n# 摘要\n\n在这一章中，我们通过介绍我们的第一个对象工厂，使我们的应用更加可测试。它们是一个非常有用的抽象层，使得单元测试变得非常容易，在更大的项目中，通常会有几个工厂。然后，我们使我们的用户界面更加动态，拥有可以随窗口缩放的样式属性。`EnumeratorDecorators`得到了一些爱和一个自己的编辑器组件，完全手指友好启动。然后，我们使用该编辑器并实现了联系人管理，展示了如何轻松查看和编辑对象集合。\n\n随着我们的应用变得更加充实，我们接下来看看如何将我们闪亮的天才新作交到最终用户手中。不同的操作系统各有各的特色，你无疑会在自己的特定环境中发现怪癖并遇到挑战，但希望你现在有了能够解决这些问题的工具。\n\n这种情绪不仅适用于部署，也适用于整个项目生命周期。这本书的目的不是讨论理论问题，这些问题虽然有趣，但在你作为开发人员的日常工作中永远不会出现。目标是提出现实世界问题的解决方案。我们从头到尾开发了一个功能性的业务线应用，处理您每天都会遇到的常见任务，无论是工作中的计划还是家中的个人项目。\n\n我希望本书中详细介绍的一些方法对您有用，并且您会像我一样喜欢使用 Qt。"
  },
  {
    "path": "docs/learn-qt5/README.md",
    "content": "# Qt5 学习手册\n\n> 原书：[Learn Qt 5](https://libgen.rs/book/index.php?md5=4A4D315AB7F7A0EDEC4FCFD04355C0BC)\n> \n> 协议：[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)\n> \n> 阶段：机翻（1）\n> \n> 自豪地采用[谷歌翻译](https://translate.google.cn/)\n> \n> 让开源界提前感受入关。\n\n* [在线阅读](https://ccpp.apachecn.org)\n* [在线阅读（Gitee）](https://apachecn.gitee.io/apachecn-c-cpp-zh/)\n* [ApacheCN 学习资源](http://docs.apachecn.org/)\n\n## 贡献指南\n\n本项目需要校对，欢迎大家提交 Pull Request。\n\n> 请您勇敢地去翻译和改进翻译。虽然我们追求卓越，但我们并不要求您做到十全十美，因此请不要担心因为翻译上犯错——在大部分情况下，我们的服务器已经记录所有的翻译，因此您不必担心会因为您的失误遭到无法挽回的破坏。（改编自维基百科）\n\n## 联系方式\n\n### 负责人\n\n* [飞龙](https://github.com/wizardforcel): 562826179\n\n### 其他\n\n*   在我们的 [apachecn/apachecn-c-cpp-zh](https://github.com/apachecn/apachecn-c-cpp-zh) github 上提 issue.\n*   发邮件到 Email: `apachecn@163.com`.\n*   在我们的 [组织学习交流群](http://www.apachecn.org/organization/348.html) 中联系群主/管理员即可.\n\n## 赞助我们\n\n![](http://data.apachecn.org/img/about/donate.jpg)\n"
  },
  {
    "path": "docs/learn-qt5/SUMMARY.md",
    "content": "+   [Qt5 学习手册](README.md)\n+   [零、前言](0.md)\n+   [一、你好，Qt](1.md)\n+   [二、项目结构](2.md)\n+   [三、用户界面](3.md)\n+   [四、样式](4.md)\n+   [五、数据](5.md)\n+   [六、单元测试](6.md)\n+   [七、SQLite](7.md)\n+   [八、网络请求](8.md)\n+   [九、打包](9.md)\n"
  },
  {
    "path": "docs/learn-wasm/00.md",
    "content": "# 零、前言\n\n这本书向读者介绍了 WebAssembly，这是一种令人兴奋的新技术，能够在浏览器中执行除 JavaScript 之外的其他语言。这本书描述了如何从头开始构建一个使用 WebAssembly 的 C/JavaScript 应用，以及在 Emscripten 的帮助下移植现有 C++ 代码库在浏览器中运行的过程。\n\nWebAssembly 代表了 web 平台的一个重要转变。作为 C、C++ 和 Rust 等语言的编译目标，它提供了构建新的应用类的能力。所有主要浏览器供应商都支持 WebAssembly，它代表了一种协作努力。\n\n在这本书里，我们将描述组成 WebAssembly 的元素，以及它的起源。我们将逐步完成安装所需工具、设置开发环境以及与 WebAssembly 交互的过程。我们将通过简单的例子进行工作，并通过越来越高级的用例进行进展。到本书结束时，您将准备好在 C、C++ 或 JavaScript 项目中使用 WebAssembly。\n\n# 这本书是给谁的\n\n如果你是一个希望为 web 构建应用的 C/C++ 程序员，或者是一个希望提高其 JavaScript 应用性能的 web 开发人员，那么这本书就是为你准备的。这本书面向熟悉 JavaScript 的开发人员，他们不介意学习一些 C 和 C++(反之亦然)。这本书通过提供两个示例应用来适应 C/C++ 程序员和 JavaScript 程序员。\n\n# 这本书涵盖了什么\n\n[第一章](01.html)*什么是 WebAssembly？*，描述了 WebAssembly 的起源，并提供了该技术的高级概述。它涵盖了如何使用 WebAssembly，支持哪些编程语言，以及它目前的局限性。\n\n[第 2 章](02.html)、*WebAssembly 的元素——Wat、Wasm 和 JavaScript API* ，概述了构成 WebAssembly 的元素。它详细解释了文本和二进制格式，以及相应的 JavaScript 和 Web APIs。\n\n[第 3 章](03.html)、*设置开发环境*，走完使用 WebAssembly 开发的工具。它提供了每个平台的安装说明，并提供了改善开发体验的建议。\n\n[第 4 章](04.html)、*安装所需的依赖项*，提供了安装每个平台的工具链要求的说明。到本章结束时，您将能够将 C 和 C++ 编译成 WebAssembly 模块。\n\n[第 5 章](05.html)、*创建和加载 WebAssembly 模块*解释了如何使用 Emscripten 生成 WebAssembly 模块，以及如何将标志传递给编译器来影响结果输出。它描述了在浏览器中加载 WebAssembly 模块的技术。\n\n[第 6 章](06.html)、*与 JavaScript 交互和调试*，详细阐述了 Emscripten 的 Module 对象和浏览器的全局 WebAssembly 对象的区别。本章介绍 Emscripten 提供的功能以及生成源地图的说明。\n\n[第 7 章](07.html)*从头开始创建应用*，介绍了与 WebAssembly 模块交互的 JavaScript 记帐应用的创建。我们将编写 C 代码来计算会计交易的值，并在 JavaScript 和编译后的 WebAssembly 模块之间传递数据。\n\n[第 8 章](08.html)*用 Emscripten 移植游戏*采用循序渐进的方法，使用 Emscripten 将现有的 C++ 游戏移植到 WebAssembly。查看现有的 C++ 代码库后，对适当的文件进行更改，以使游戏能够在浏览器中运行。\n\n[第 9 章](09.html)、*与 Node.js* 集成，演示了 Node.js 和 npm 如何在服务器端和客户端与 WebAssembly 一起使用。本章介绍了在快速应用中使用网络程序集、将网络程序集与网络包集成以及使用 Jest 测试网络程序集模块。\n\n[第 10 章](10.html)、*高级工具和即将推出的特性*，涵盖了高级工具、用例和目前正在标准化过程中的新 WebAssembly 特性。本章介绍了 WABT、比纳莱恩和在线工具。在本章中，您将学习如何使用 LLVM 编译 WebAssembly 模块，以及 WebAssembly 模块如何与网络工作者一起使用。本章最后描述了标准化过程，并回顾了被添加到规范过程中的一些令人兴奋的特性。\n\n# 充分利用这本书\n\n你应该有一些编程经验，理解变量和函数等概念。如果您从未见过一行 JavaScript 或 C/C++ 代码，您可能想在阅读本书中的示例之前做一些初步研究。我选择使用 JavaScript ES6/7 特性，例如析构和箭头函数，所以如果您在过去 3 - 4 年中没有使用过 JavaScript，语法可能会看起来略有不同。\n\n# 下载示例代码文件\n\n你可以从你在[www.packtpub.com](http://www.packtpub.com)的账户下载这本书的示例代码文件。如果您在其他地方购买了这本书，您可以访问[www.packtpub.com/support](http://www.packtpub.com/support)并注册将文件直接通过电子邮件发送给您。\n\n您可以按照以下步骤下载代码文件:\n\n1.  登录或注册[www.packtpub.com](http://www.packtpub.com/support)。\n2.  选择“支持”选项卡。\n3.  点击代码下载和勘误表。\n4.  在搜索框中输入图书的名称，并按照屏幕指示进行操作。\n\n下载文件后，请确保使用最新版本的解压缩文件夹:\n\n*   视窗系统的 WinRAR/7-Zip\n*   zipeg/izp/un ARX for MAC\n*   适用于 Linux 的 7-Zip/PeaZip\n\n这本书的代码包也托管在 https://github.com/PacktPublishing/Learn-WebAssembly 的 GitHub 上。如果代码有更新，它将在现有的 GitHub 存储库中更新。\n\n我们还有来自丰富的图书和视频目录的其他代码包，可在**[【https://github.com/PacktPublishing/】](https://github.com/PacktPublishing/)**获得。看看他们！\n\n# 下载彩色图像\n\n我们还提供了一个 PDF 文件，其中包含本书中使用的截图/图表的彩色图像。可以在这里下载:[https://www . packtpub . com/sites/default/files/downloads/9781788997379 _ color images . pdf](https://www.packtpub.com/sites/default/files/downloads/9781788997379_ColorImages.pdf)。\n\n# 使用的约定\n\n本书通篇使用了许多文本约定。\n\n`CodeInText`:表示文本中的码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟网址、用户输入和推特句柄。这里有一个例子:“`instantiate()`是编译和实例化 WebAssembly 代码的主要 API。”\n\n代码块设置如下:\n\n```cpp\nint addTwo(int num) {\n return num + 2;\n}\n```\n\n当我们希望将您的注意力吸引到代码块的特定部分时，相关的行或项目以粗体显示:\n\n```cpp\nint calculate(int firstVal, int secondVal) {\nreturn firstVal - secondVal;\n}\n```\n\n任何命令行输入或输出都编写如下:\n\n```cpp\nnpm install -g webassembly\n```\n\n**粗体**:表示一个新的术语、一个重要的单词或者你在屏幕上看到的单词。例如，菜单或对话框中的单词像这样出现在文本中。下面是一个例子:“您可以通过按下开始菜单按钮，右键单击命令提示符应用并选择以管理员身份运行来实现这一点。”\n\nWarnings or important notes appear like this. Tips and tricks appear like this.\n\n# 取得联系\n\n我们随时欢迎读者的反馈。\n\n**综合反馈**:发邮件`feedback@packtpub.com`并在邮件主题中提及书名。如果您对本书的任何方面有疑问，请发电子邮件至`questions@packtpub.com`。\n\n**勘误表**:虽然我们已经尽了最大的努力来保证内容的准确性，但是错误还是会发生。如果你在这本书里发现了一个错误，如果你能向我们报告，我们将不胜感激。请访问[www.packtpub.com/submit-errata](http://www.packtpub.com/submit-errata)，选择您的图书，点击勘误表提交链接，并输入详细信息。\n\n**盗版**:如果您在互联网上遇到任何形式的我们作品的非法拷贝，如果您能提供我们的位置地址或网站名称，我们将不胜感激。请通过`copyright@packtpub.com`联系我们，并提供材料链接。\n\n**如果你有兴趣成为一名作者**:如果有一个你有专长的话题，你有兴趣写或者投稿一本书，请访问[authors.packtpub.com](http://authors.packtpub.com/)。\n\n# 复习\n\n请留下评论。一旦你阅读并使用了这本书，为什么不在你购买它的网站上留下评论呢？然后，潜在的读者可以看到并使用您不带偏见的意见来做出购买决定，我们在 Packt 可以了解您对我们产品的看法，我们的作者可以看到您对他们的书的反馈。谢谢大家！\n\n更多关于 Packt 的信息，请访问[packtpub.com](https://www.packtpub.com/)。"
  },
  {
    "path": "docs/learn-wasm/01.md",
    "content": "# 一、什么是 WebAssembly？\n\n**WebAssembly** ( **Wasm** )代表了 web 平台的重要垫脚石。使开发人员能够在没有插件或浏览器锁定的情况下在网络上运行编译代码带来了许多新的机会。对于什么是 WebAssembly 存在一些困惑，对于它的持久力也存在一些怀疑。\n\n在本章中，我们将讨论 WebAssembly 是如何产生的，WebAssembly 相对于官方定义是什么，以及它包含的技术。将涵盖潜在的用例、支持的语言和限制，以及在哪里可以找到其他信息。\n\n本章的目标是理解以下内容:\n\n*   引领 WebAssembly 的技术\n*   什么是 WebAssembly 以及它的一些潜在用例\n*   WebAssembly 可以使用哪些编程语言\n*   WebAssembly 目前的局限性\n*   WebAssembly 如何与 Emscripten 和 asm.js 相关联\n\n# WebAssembly 之路\n\n至少可以说，网络开发有一段有趣的历史。已经进行了几次(失败的)尝试来扩展平台以支持不同的语言。诸如插件等笨拙的解决方案经不起时间的考验，将用户限制在一个浏览器内是灾难的根源。\n\nWebAssembly 是作为一个优雅的解决方案开发的，这个问题从浏览器第一次能够执行代码时就已经存在了:*如果你想为 web 开发，你必须使用 JavaScript* 。幸运的是，使用 JavaScript 没有 2000 年代早期的负面含义，但作为一种编程语言，它仍然有一定的局限性。在本节中，我们将讨论导致 WebAssembly 的技术，以更好地理解为什么需要这项新技术。\n\n# JavaScript 的演变\n\n1995 年，仅仅 10 天，Brendan Eich 就创建了 JavaScript。最初被程序员视为*玩具*语言，它主要用于让按钮闪烁或横幅出现在网页上。在过去的十年里，JavaScript 已经从一个玩具发展成为一个拥有强大功能和大量追随者的平台。\n\n2008 年浏览器市场的激烈竞争导致了**适时** ( **JIT** )编译器的加入，使得 JavaScript 的执行速度提升了 10 倍。Node.js 于 2009 年首次亮相，代表了 web 开发的范式转变。Ryan Dahl 结合了谷歌的 V8 JavaScript 引擎、事件循环和低级 I/O API，构建了一个允许在服务器端和客户端使用 JavaScript 的平台。Node.js 导致了`npm`，一个允许开发在 Node.js 生态系统中使用的库的包管理器。截至本文撰写之时，有超过 600，000 个软件包可供使用，每天增加数百个:\n\n![](img/d473abb9-dda2-4db0-acfb-0a63607c8190.png)\n\nPackage count growth on npm since 2012, taken from Modulecounts\n\n增长的不仅仅是 Node.js 生态系统；JavaScript 本身也在积极开发中。ECMA **技术委员会 39** ( **TC39** )规定了 JavaScript 的标准，并监督新语言特性的添加，通过社区驱动的提案流程，每年发布*JavaScript 更新。凭借其丰富的库和工具、对语言的不断改进以及拥有最大的程序员社区之一，JavaScript 已经成为一股不可忽视的力量。*\n\n但是这种语言确实有一些缺点:\n\n*   直到最近，JavaScript 还只包含 64 位浮点数。这可能会导致数量非常大或非常小的问题。`BigInt`，一个可以缓解其中一些问题的新数值原语，正在被添加到 ECMAScript 规范中，但是它可能需要一段时间才能在浏览器中得到完全支持。\n*   JavaScript 是弱类型的，这增加了它的灵活性，但是会导致混乱和错误。它本质上给了你足够的绳子来上吊。\n*   尽管浏览器厂商尽了最大努力，JavaScript 的性能还是不如编译语言。\n*   如果开发人员想要创建一个 web 应用，他们需要学习 JavaScript——不管他们喜不喜欢。\n\n为了避免不得不写几行以上的 JavaScript，一些开发人员构建了**transfilers**来将其他语言转换为 JavaScript。Transpilers(或源到源编译器)是将一种编程语言的源代码转换为另一种编程语言的等效源代码的编译器类型。TypeScript 是一种流行的前端 JavaScript 开发工具，它将 TypeScript 转换为针对浏览器或 Node.js 的有效 JavaScript。选择任何编程语言，都很有可能有人为其创建了一个 JavaScript transpiler。例如，如果您更喜欢编写 Python，那么您可以使用大约 15 种不同的工具来生成 JavaScript。然而，最终它仍然是 JavaScript，所以你仍然受制于这种语言的特性。\n\n随着 web 发展成为构建和分发应用的有效平台，越来越多复杂和资源密集型的应用被创建出来。为了满足这些应用的需求，浏览器供应商开始研究新技术，以便在不中断 web 开发正常进程的情况下集成到他们的软件中。分别是 Chrome 和 Firefox 的创作者谷歌和 Mozilla，为了实现这个目标，走了两条不同的道路，最终创建了 WebAssembly。\n\n# 谷歌和本地客户端\n\n谷歌开发了**原生客户端** ( **氯化钠**)，目的是在网络浏览器中安全运行原生代码。可执行代码将在**沙箱**中运行，并提供本机代码执行的性能优势。\n\nIn the context of software development, a sandbox is an environment that prevents executable code from interacting with other parts of your system. It is intended to prevent the spread of malicious code and place restrictions on what software can do.\n\n氯化钠与特定的体系结构相关联，而**便携式本地客户端** ( **PNaCl** )是氯化钠的独立于体系结构的版本，可以在任何平台上运行。这项技术包括两个要素:\n\n*   可以将 C/C++ 代码转换成氯化钠模块的工具链\n*   运行时组件，即嵌入浏览器中允许执行氯化钠模块的组件:\n\n![](img/ee7ab5c5-f671-4caa-8073-2c3ef941c399.png)\n\nThe Native Client toolchains and their outputs\n\nNaCl 的特定于架构的可执行文件(`nexe`)仅限于从谷歌 Chrome Web Store 安装的应用和扩展，但 PNaCl 可执行文件(`pexe`)可以在网络上自由分发并嵌入到 Web 应用中。通过创建氯化钠模块的开源应用编程接口 Pepper 及其相应的插件应用编程接口(PPAPI)，可移植性成为可能。Pepper 支持氯化钠模块和宿主浏览器之间的通信，并允许以安全和可移植的方式访问系统级功能。通过包含一个清单文件和一个带有相应的 HTML、CSS 和 JavaScript 的编译模块(`pexe`)，可以很容易地分发应用:\n\n![](img/0a230248-b946-4f66-b811-ff2530fc48d1.png)\n\nPepper's role in a Native Client application\n\n氯化钠为克服网络的性能限制提供了有希望的机会，但它也有一些缺点。尽管 Chrome 内置了对 PNaCl 可执行文件和 Pepper 的支持，但其他主要浏览器没有。该技术的批评者对应用的黑盒性质以及潜在的安全风险和复杂性提出了质疑。\n\nMozilla 专注于通过`asm.js`提高 JavaScript 的性能。由于其应用编程接口规范的不完整和有限的文档，他们不会在火狐中添加对胡椒的支持。最终，氯化钠在 2017 年 5 月被否决，转而支持 WebAssembly。\n\n# Mozilla 和 asm.js\n\nMozilla 于 2013 年首次亮相`asm.js`，并为开发人员提供了一种将他们的 C 和 C++ 源代码翻译成 JavaScript 的方法。`asm.js`的官方规范将其定义为 JavaScript 的一个严格子集，可以作为编译器的一种低级、高效的目标语言。它仍然是有效的 JavaScript，但是语言特性仅限于那些可以提前 ( **AOT** )优化的语言。AOT 是一种技术，浏览器的 JavaScript 引擎通过将代码编译成本机代码来更有效地执行代码。`asm.js`通过 100%的类型一致性和手动内存管理实现了这些性能提升。\n\n使用像 Emscripten 这样的工具，C/C++ 代码可以被编译到`asm.js`，并使用与普通 JavaScript 相同的方法轻松分发。要访问`asm.js`模块中的函数，需要**链接**，这涉及到调用其函数来获取模块导出的对象。\n\n`asm.js`非常灵活，但是，与模块的某些交互会导致性能下降。例如，如果一个`asm.js`模块被授予访问一个未通过动态或静态验证的自定义 JavaScript 函数的权限，那么代码就不能利用 AOT，只能求助于解释器:\n\n![](img/3b40bcf8-0a50-4ed5-806e-3f3a5f64679a.png)\n\nThe asm.js AOT compilation workflow\n\n`asm.js`不仅仅是垫脚石。它构成了 WebAssembly 的**最低可行产品** ( **最有价值产品**)的基础。官方 WebAssembly 网站在标题为*WebAssembly 高级目标*的部分明确提到`asm.js`。\n\n那么，当你可以使用`asm.js`的时候，为什么还要创建 WebAssembly 呢？除了潜在的性能损失之外，`asm.js`模块是一个文本文件，在进行任何编译之前必须通过网络传输。WebAssembly 模块采用二进制格式，由于它的尺寸较小，因此传输效率更高。\n\nWebAssembly 模块使用基于承诺的实例化方法，该方法利用了现代 JavaScript 并消除了对任何*的需求。这个加载了*T2 了吗？代码。\n\n# WebAssembly 诞生了\n\n**万维网联盟** ( **W3C** )是一个旨在开发网络标准的国际社区，于 2015 年 4 月成立了 WebAssembly 工作组，以标准化 WebAssembly 并监督规范和提案流程。此后，*核心规范*以及对应的 *JavaScript API* 、 *Web API* 相继发布。浏览器中 WebAssembly 支持的最初实现是基于`asm.js`的特性集。WebAssembly 的二进制格式和相应的`.wasm`文件将`asm.js`输出的方面与 PNaCl 的分布式可执行文件的概念相结合。\n\n那么，在氯化钠失败的地方，WebAssembly 将如何成功？据 Axel Rauschmayer 博士介绍，有三个原因在[详细说明:](http://2ality.com/2015/06/web-assembly.html#what-is-different-this-time)\n\n\"First, this is a collaborative effort, no single company goes it alone. At the moment, the following projects are involved: Firefox, Chromium, Edge and WebKit.\n\nSecond, the interoperability with the web platform and JavaScript is excellent. Using WebAssembly code from JavaScript will be as simple as importing a module.\n\nThird, this is not about replacing JavaScript engines, it is more about adding a new feature to them. That greatly reduces the amount of work to implement WebAssembly and should help with getting the support of the web development community.\"\n\n- Dr. Axel Rauschmayer\n\n# 什么是 WebAssembly，我可以在哪里使用它？\n\nWebAssembly 在官方网站上有一个简洁的描述性定义，但这只是拼图的一部分。还有其他几个组件属于 WebAssembly 的范畴。了解每个组件所扮演的角色将使您更好地了解整个技术。在本节中，我们将提供 WebAssembly 定义的详细分解，并描述潜在的用例。\n\n# 官方定义\n\n官网([https://webassembly.org](https://webassembly.org))给出了这个定义:\n\nWasm is a binary instruction format for a stack-based virtual machine. Wasm is designed as a portable target for compilation of high-level languages like C/C++/Rust, enabling deployment on the web for client and server applications.\n\n让我们把这个定义分解成几个部分来增加一些说明。\n\n# 二进制指令格式\n\nWebAssembly 实际上包含几个元素——二进制格式和文本格式，它们记录在*核心规范*中，对应的 API(JavaScript 和 web)和编译目标。二进制和文本格式都以**抽象语法**的形式映射到一个公共结构。为了更好地理解抽象语法，可以在**抽象语法树** ( **AST** )的上下文中进行解释。AST 是编程语言源代码结构的树形表示。ESLint 等工具使用 JavaScript 的 AST 来查找林挺错误。以下示例包含一个函数和相应的 JavaScript AST(摘自[https://astexplorer.net](https://astexplorer.net))。\n\n一个简单的 JavaScript 函数如下:\n\n```cpp\nfunction doStuff(thingToDo) {\n  console.log(thingToDo);\n}\n```\n\n相应的 AST 如下:\n\n```cpp\n{\n  \"type\": \"Program\",\n  \"start\": 0,\n  \"end\": 57,\n  \"body\": [\n    {\n      \"type\": \"FunctionDeclaration\",\n      \"start\": 9,\n      \"end\": 16,\n      \"id\": {\n        \"type\": \"Identifier\",\n        \"start\": 17,\n        \"end\": 26,\n        \"name\": \"doStuff\"\n      },\n      \"generator\": false,\n      \"expression\": false,\n      \"params\": [\n        {\n          \"type\": \"Identifier\",\n          \"start\": 28,\n          \"end\": 57,\n          \"name\": \"thingToDo\"\n        }\n      ],\n      \"body\": {\n        \"type\": \"BlockStatement\",\n        \"start\": 32,\n        \"end\": 55,\n        \"body\": [\n          {\n            \"type\": \"ExpressionStatement\",\n            \"start\": 32,\n            \"end\": 55,\n            \"expression\": {\n              \"type\": \"CallExpression\",\n              \"start\": 32,\n              \"end\": 54,\n              \"callee\": {\n                \"type\": \"MemberExpression\",\n                \"start\": 32,\n                \"end\": 43,\n                \"object\": {\n                  \"type\": \"Identifier\",\n                  \"start\": 32,\n                  \"end\": 39,\n                  \"name\": \"console\"\n                },\n                \"property\": {\n                  \"type\": \"Identifier\",\n                  \"start\": 40,\n                  \"end\": 43,\n                  \"name\": \"log\"\n                },\n                \"computed\": false\n              },\n              \"arguments\": [\n                {\n                  \"type\": \"Identifier\",\n                  \"start\": 44,\n                  \"end\": 53,\n                  \"name\": \"thingToDo\"\n                }\n              ]\n            }\n          }\n        ]\n      }\n    }\n  ],\n  \"sourceType\": \"module\"\n}\n```\n\nAST 可能很冗长，但它在描述程序组件方面做得很好。在 AST 中表示源代码使验证和编译变得简单而高效。文本格式的 WebAssembly 代码被序列化为 AST 并编译为二进制格式(作为`.wasm`文件)，由网页获取、加载和使用。当模块被加载时，浏览器的 JavaScript 引擎利用**解码栈**将`.wasm`文件解码成 AST，执行类型检查，并解释它以执行功能。WebAssembly 最初是一个 AST 的二进制指令格式。由于验证返回`void`的 Wasm 表达式的性能影响，二进制指令格式被更新为以**堆栈机器**为目标。\n\n堆栈机器由两个元素组成:堆栈和指令。栈是一个数据结构，有两个操作:*推*和*弹出*。项目被推到堆栈上，随后从堆栈中弹出，顺序为**后进先出** ( **后进先出**)。堆栈还包括一个**指针**，它指向堆栈顶部的项目。指令表示对堆栈中的项目执行的操作。例如，`ADD`指令可能会弹出堆栈中的前两个项目(值`100`和`10`，并将带有总和的单个项目推回到堆栈中(值`110`):\n\n![](img/1e2d75b9-2021-4486-8049-726f6134410d.png)\n\nA simple stack machine\n\nWebAssembly 的堆栈机器以同样的方式运行。程序计数器(指针)保持代码中的执行位置，虚拟控制堆栈在`blocks`和`if`结构被输入(推入)和退出(弹出)时跟踪它们。这些指令的执行不涉及 AST。因此，定义的**二进制指令格式**部分指的是浏览器中解码栈可读格式的指令的二进制表示。\n\n# 编译的可移植目标\n\nWebAssembly 从一开始就考虑到了可移植性。在这种情况下，可移植性意味着 WebAssembly 的二进制格式可以在网络内外的各种操作系统和指令集架构上高效执行。WebAssembly 规范定义了执行环境中的可移植性。WebAssembly 旨在满足某些特性的环境中高效运行，其中大多数特性与内存相关。WebAssembly 的可移植性也可以归因于缺乏围绕核心技术的特定 API。相反，它定义了一个`import`机制，其中可用导入集由主机环境定义。\n\n简而言之，这意味着 WebAssembly 不依赖于特定的环境，比如 web 或桌面。WebAssembly 工作组已经定义了一个*网络应用编程接口*，但这与*核心规范*是分开的。*网络应用编程接口*迎合 WebAssembly，而不是相反。\n\n定义的**编译**方面表明，从用高级语言编写的源代码开始，WebAssembly 将很容易编译成其二进制格式。MVP 侧重于两种语言，C 和 C++，但是考虑到 Rust 与 C++ 的相似性，也可以使用它。编译将通过使用 Clang/LLVM 后端来实现，尽管我们将在本书中使用 Emscripten 来生成我们的 Wasm 模块。计划是最终增加对其他语言和编译器的支持(比如 GCC)，但是 MVP 的重点是 LLVM。\n\n# 核心规范\n\n官方定义对整体技术给出了一些高层次的洞察，但为了完整性，值得深入挖掘一下。WebAssembly 的*核心规范*是官方文档，如果你想在非常细粒度的层面上理解 WebAssembly，可以参考。如果您有兴趣了解关于执行环境的运行时结构的特征，请查看第 4 部分:*执行*。我们在这里不讨论这个问题，但是理解*核心规范*的适用范围将有助于建立 WebAssembly 的完整定义。\n\n# 语言概念\n\n*核心规范*声明网络汇编编码了一种低级的、类似汇编的编程语言。该规范定义了这种语言的结构、执行和验证，以及二进制和文本格式的细节。语言本身是围绕以下概念构建的:\n\n*   **值**，或者更确切地说是 WebAssembly 提供的值类型\n*   **在堆叠机内执行的指令**\n*   **捕捉错误条件下产生的**并中止执行\n*   **代码组织成的函数**，每个函数以一系列值作为参数，并返回一系列值作为结果\n*   **表**，是可由执行程序选择的特定元素类型(如函数引用)的值数组\n*   **线性存储器**，它是一个原始字节数组，可以用来存储和加载值\n*   **模块**，包含函数、表格和线性存储器的网络汇编二进制(`.wasm`文件)\n*   **嵌入器**，一种可以在主机环境中执行 WebAssembly 的机制，例如 web 浏览器\n\n函数、表、内存和模块与 *JavaScript API* 直接相关，需要注意。这些概念描述了语言本身的底层结构，以及如何编写或编码 WebAssembly。关于用法，理解 WebAssembly 的相应语义阶段提供了该技术的完整定义:\n\n![](img/52382cbb-fa93-4206-adde-c38848bf1429.png)\n\nLanguage concepts and their relationship\n\n# 语义阶段\n\n*核心规范*描述了编码模块(`.wasm`文件)在主机环境(如网络浏览器)中使用时所经历的不同阶段。规范的这一方面表示如何处理和执行输出:\n\n*   **解码**:二进制格式转换成模块\n*   **验证**:解码后的模块经过验证检查(如类型检查)，以确保模块完好且安全\n*   **执行，第 1 部分:实例化**:模块实例是模块的动态表示，通过初始化**全局**、**内存**和**表**来实例化，并调用模块的`start()`功能\n*   **执行，第 2 部分:调用**:从模块实例中调用导出的函数:\n\n下图提供了语义阶段的可视化表示:\n\n![](img/51755ca6-f4c7-43b3-93d3-81575523ae30.png)\n\nSemantic phases of module use\n\n# JavaScript 和网络应用接口\n\nWebAssembly 工作组还发布了用于与 JavaScript 和网络交互的应用编程接口规范，这使它们有资格包含在 WebAssembly 技术领域中。 *JavaScript 应用编程接口*的范围仅限于 JavaScript 语言本身，并不特别受限于环境(例如，网络浏览器或 Node.js)。它定义了类、方法和对象，用于与 WebAssembly 交互并管理编译和实例化过程。*网络应用编程接口*是 *JavaScript 应用编程接口*的扩展，定义了特定于网络浏览器的功能。 *Web API* 规范目前只定义了`compileStreaming`和`instantiateStreaming`两种方法，都是简化浏览器中 Wasm 模块使用的便捷方法。这些将在[第 2 章](02.html)、*网络组件的元素——Wat、Wasm 和 JavaScript API* 中有更详细的介绍。\n\n# 那么它会取代 JavaScript 吗？\n\nWebAssembly 的最终目标不是取代 JavaScript，而是对其进行补充。JavaScript 丰富的生态系统和灵活性仍然使其成为网络的理想语言。WebAssembly 的 JavaScript API 使得这两种技术之间的互操作性变得相对简单。那么，您能够仅仅使用 WebAssembly 来构建一个网络应用吗？WebAssembly 的明确目标之一是可移植性，复制 JavaScript 的所有功能可能会抑制这一目标。然而，官方网站包括一个目标，即执行并与现有的网络平台很好地集成，所以只有时间才能证明。用向下编译到 WebAssembly 的语言编写整个代码库可能不太实际，但是将一些应用逻辑转移到 Wasm 模块在性能和加载时间方面可能是有益的。\n\n# 哪里可以用？\n\nWebAssembly 的官方网站有一个潜在用例的广泛列表。我不打算在这里一一介绍，但是有几个是对 web 平台功能的显著增强:\n\n*   图像/视频编辑\n*   比赛\n*   音乐应用(流、缓存)\n*   图像识别\n*   实时视频增强\n*   虚拟现实和增强现实\n\n虽然这些用例中的一些在技术上可以用 JavaScript、HTML 和 CSS 来实现，但是使用 WebAssembly 可以提供显著的性能提升。提供一个二进制文件(而不是一个单独的 JavaScript 文件)可以大大减少包的大小，并且在页面加载时实例化 Wasm 模块可以加快代码的执行。\n\nWebAssembly 不仅仅局限于浏览器。在浏览器之外，您可以使用它在移动设备上构建混合本机应用，或者对不受信任的代码执行服务器端计算。在手机应用中使用 Wasm 模块在功耗和性能方面都非常有益。\n\nWebAssembly 还提供了如何使用它的灵活性。您可以在 WebAssembly 中编写整个代码库，尽管这在当前形式或 web 应用的上下文中可能并不实用。考虑到 WebAssembly 强大的 JavaScript API，您可以用 JavaScript/HTML 编写 UI，并使用 Wasm 模块实现不直接访问 DOM 的功能。一旦支持了额外的语言，对象就可以很容易地在 Wasm 模块和 JavaScript 代码之间传递，这将大大简化集成并提高开发人员的采用率。\n\n# 支持哪些语言？\n\nWebAssembly 对其 MVP 的高级目标是提供与`asm.js`大致相同的功能。这两项技术关系非常密切。C、C++ 和 Rust 是非常流行的支持手动内存分配的语言，这使得它们成为初始实现的理想候选。在本节中，我们将简要概述每种编程语言。\n\n# C 和 C++\n\nC 和 C++ 是已经存在了 30 多年的低级编程语言。c 是过程化的，本质上不支持面向对象的编程概念，例如类和继承，但是它快速、可移植，并且被广泛使用。\n\nC++ 是为了填补 C 语言中的空白而构建的，它增加了操作符重载和改进的类型检查等功能。这两种语言始终位列最受欢迎的 10 种编程语言，这使得它们非常适合 MVP:\n\n![](img/049542c7-ebe1-4b35-b4a1-de0ec95a532c.png)\n\nTIOBE Very Long Term History of the top 10 programming languages\n\nC 和 C++ 支持也被烘焙到 Emscripten 中，因此除了简化编译过程之外，它还允许您利用 WebAssembly 的全部功能。也可以使用 LLVM 将 C/C++ 代码编译成`.wasm`文件。LLVM 是模块化和可重用的编译器和工具链技术的集合。简而言之，它是一个框架，简化了从源代码到机器代码的编译过程的配置。如果您制作了自己的编程语言，并希望构建一个编译器，LLVM 有工具来简化这个过程。我将在[第 10 章](10.html)、*高级工具和即将推出的功能*中介绍如何使用 LLVM 将 C/C++ 编译成`.wasm`文件。\n\n下面的代码片段演示了如何使用 C++ 将`Hello World!`打印到控制台:\n\n```cpp\n#include <iostream>\n\nint main() {\n    std::cout << \"Hello, World!\\n\";\n    return 0;\n}\n```\n\n# 锈\n\nC 和 C++ 本来是用于 WebAssembly 的主要语言，但是 Rust 是一个非常合适的替代品。Rust 是一种系统编程语言，在语法上类似于 C++。它在设计时考虑了内存安全，但仍然保留了 C 和 C++ 的性能优势。Rust 的编译器目前每夜的构建都可以从 Rust 源代码生成`.wasm`文件，所以如果你比较喜欢 Rust 并且熟悉 C++，那么对于本书的大部分例子应该都可以使用 Rust。\n\n以下代码片段演示了如何使用 Rust 将`Hello World!`打印到控制台:\n\n```cpp\nfn main() {\n    println!(\"Hello World!\");\n}\n```\n\n# 其他语言\n\n存在各种工具来支持 WebAssembly 与其他一些流行的编程语言一起使用，尽管它们大多是实验性的:\n\n*   C#通过 Blazor\n*   把 WebIDL 带走\n*   Java 通过 TeaVM 或字节码\n*   Kotlin 通过 TeaVM\n*   通过程序集脚本键入脚本\n\n技术上也可以将一种语言转换成 C 语言，然后编译成 Wasm 模块，但是编译的成功取决于转换程序的输出。更有可能的是，您必须对代码进行重大更改才能让它工作。\n\n# 有什么局限性？\n\n诚然，WebAssembly 并非没有局限性。新特性正在积极开发，技术也在不断发展，但是 MVP 功能只代表了 WebAssembly 的一部分功能。在本节中，我们将介绍其中的一些限制，以及它们如何影响开发过程。\n\n# 没有垃圾收集\n\nWebAssembly 支持平面线性内存，这本身并不是一个限制，但是需要对如何显式分配内存来执行代码有所了解。C 和 C++ 是 MVP 的逻辑选择，因为内存管理内置于语言中。一些更受欢迎的高级语言(如 Java)最初没有被包括在内的原因是由于一种叫做**垃圾收集** ( **GC** )的东西。\n\n垃圾收集是一种自动内存管理的形式，其中被程序不再使用的对象占用的内存被自动回收。气相色谱类似于汽车上的自动变速器。熟练的工程师对其进行了大量优化，以尽可能高效地运行，但限制了驾驶员的控制能力。手动分配内存就像开一辆手动变速箱的车。它可以更好地控制速度和扭矩，但误用或缺乏经验会让你被困在一辆严重受损的汽车上。C 和 C++ 出色的性能和速度部分归功于内存的手动分配。\n\nGC 语言允许您在不担心内存可用性或分配的情况下进行编程。JavaScript 是垃圾收集语言的一个例子。浏览器引擎采用一种叫做标记-清除的算法来收集不可到达的对象并释放相应的内存。对 GC 语言的支持目前正在 WebAssembly 中进行，但是很难说它将在什么时候完成。\n\n# 没有直接的 DOM 访问\n\nWebAssembly 无法访问 DOM，因此任何 DOM 操作都需要通过 JavaScript 或者使用 Emscripten 之类的工具间接完成。有计划增加直接引用 DOM 和其他网络应用编程接口对象的能力，但这仍处于建议阶段。DOM 操作可能会与 GC 语言齐头并进，因为它允许对象在 WebAssembly 和 JavaScript 代码之间无缝传递。\n\n# 旧浏览器不支持\n\n旧的浏览器没有全局`WebAssembly`对象来实例化和加载 Wasm 模块。如果找不到对象，可以使用`asm.js`进行实验性聚合填充，但是 WebAssembly 工作组目前没有计划创建一个。由于`asm.js`和 WebAssembly 密切相关，如果`WebAssembly`对象不可用，只需提供一个`asm.js`文件，仍然可以提高性能，同时考虑到向后兼容性。你可以在[https://caniuse.com/#feat=wasm](https://caniuse.com/#feat=wasm)看到目前哪些浏览器支持 WebAssembly。\n\n# 它与 Emscripten 有什么关系？\n\nEmscripten 是可以从 C 和 C++ 源代码生成`asm.js`的源到源编译器。我们将使用它作为构建工具来生成 Wasm 模块。在本节中，我们将快速回顾 Emscripten 与 WebAssembly 的关系。\n\n# Emscripten 的角色\n\nEmscripten 是一个 LLVM 到 JavaScript 的编译器，这意味着它接受编译器如 Clang(对于 C 和 C++)的 LLVM 位代码输出，并将其转换为 JavaScript。它不是一种特定的技术，而是共同构建、编译和运行`asm.js`的技术组合。为了生成 Wasm 模块，我们将使用**Emscripten SDK**(**EMSDK**)管理器:\n\n![](img/4d92b3fc-da38-44bc-97b4-e1be3b1fc6a3.png)\n\nWasm module generation with the EMSDK\n\n# EMSDK 和 Binaryen\n\n在[第 4 章](04.html)、*安装所需依赖项*中，我们将安装 EMSDK，并使用它来管理将 C 和 C++ 编译为 Wasm 模块所需的依赖项。Emscripten 使用 Binaryen 的`asm2wasm`工具将 Emscripten 输出的`asm.js`编译成`.wasm`文件。Binaryen 是一个编译器和工具链基础设施库，其中包括将各种格式编译成 WebAssembly 模块的工具，反之亦然。使用 WebAssembly 并不需要了解 Binaryen 的内部工作原理，但是了解底层技术以及它们是如何协同工作的非常重要。通过将某些标志传递到 Emscripten ( `emcc`)的编译命令中，我们可以将得到的`asm.js`代码传输到 Binaryen，以输出我们的`.wasm`文件。\n\n# 摘要\n\n在这一章中，我们讨论了 WebAssembly 的历史，以及导致它产生的技术。提供了 WebAssembly 定义的详细概述，以便更好地理解所涉及的底层技术。\n\n上的*核心规范*、 *JavaScript API* 和 *Web API* 作为 WebAssembly 的重要元素进行了展示，并展示了技术将如何发展。我们还回顾了潜在的用例、当前支持的语言以及支持使用不支持的语言的工具。\n\nWebAssembly 的局限性是缺少 GC，无法直接与 DOM 通信，以及缺乏对旧浏览器的支持。这些讨论传达了新的技术，并揭示了它的一些缺点。最后，我们讨论了 Emscripten 在开发过程中的角色，以及它在 WebAssembly 开发工作流中的位置。\n\n在 [第 2 章](02.html)*WebAssembly 的元素- Wat、Wasm* 和*JavaScript API*中，我们将深入探讨构成 Web assembly 的元素: **WebAssembly 文本格式** ( **Wat** )、二进制格式(Wasm)、JavaScript 和 Web APIs。\n\n# 问题\n\n1.  哪两项技术影响了 WebAssembly 的创建？\n2.  什么是栈机，它与 WebAssembly 有什么关系？\n3.  WebAssembly 在哪些方面补充了 JavaScript？\n4.  哪三种编程语言可以编译成 Wasm 模块？\n5.  LLVM 在 WebAssembly 方面扮演什么角色？\n6.  WebAssembly 的三个潜在用例是什么？\n7.  DOM 访问和 GC 是如何相关的？\n8.  Emscripten 用什么工具生成 Wasm 模块？\n\n# 进一步阅读\n\n*   官方 WebAssembly 网站:[https://webassembly.org](https://webassembly.org)\n*   本地客户端技术概述:[https://developer.chrome.com/native-client/overview](https://developer.chrome.com/native-client/overview)\n*   LLVM 编译器基础设施项目:[https://llvm.org](https://llvm.org)\n*   关于 Emscripten:[http://kripken . github . io/Emscripten-site/docs/introduction _ Emscripten/about _ Emscripten . html](http://kripken.github.io/emscripten-site/docs/introducing_emscripten/about_emscripten.html)\n*   asm.js 规范:[http://asmjs.org/spec/latest](http://asmjs.org/spec/latest)"
  },
  {
    "path": "docs/learn-wasm/02.md",
    "content": "# 二、WebAssembly 的元素——Wat、Wasm 和 JavaScript 应用编程接口\n\n[第一章](01.html)*什么是 WebAssembly？*，描述了 WebAssembly 的历史，并提供了该技术的高级概述以及潜在的用例和限制。WebAssembly 被描述为由多个元素组成，而不仅仅是官方定义中指定的二进制指令格式。\n\n在本章中，我们将深入研究与 WebAssembly 工作组创建的官方规范相对应的元素。我们将更详细地研究 Wat 和二进制格式，以便更好地理解它们与模块的关系。我们将回顾 *JavaScript API* 和 *Web API* 以确保您能够在浏览器中有效利用 WebAssembly。\n\n本章的目标是理解以下内容:\n\n*   文本和二进制格式是如何关联的\n*   什么是水，它在开发过程中处于什么位置\n*   二进制格式和模块(Wasm)文件\n*   JavaScript 和网络应用编程接口的组件，以及它们与 Wasm 模块的关系\n*   如何利用 WasmFiddle 评估 WebAssembly 的各个阶段(C/C++ > Wat > Wasm)\n\n# 通用结构和抽象语法\n\n在[第一章](01.html)*什么是 WebAssembly？*，我们讨论了 WebAssembly 的二进制和文本格式如何以抽象语法的形式映射到一个公共结构。在深入这些格式的具体细节之前，值得一提的是这些在*核心规范*中是如何关联的。下图是目录的直观表示(为清楚起见，某些部分被排除在外):\n\n![](img/24a8a8a0-7afd-4ffe-892c-bb162b09b784.png)\n\n*Core Specification* table of contents\n\n如您所见，**文本格式**和**二进制格式**部分包含与**结构**部分相关的**值**、**类型**、**说明**和**模块**的子部分。因此，我们在下一节中讨论的文本格式与二进制格式有着直接的必然联系。考虑到这一点，让我们进入文本格式。\n\n# 泰国或高棉的佛教寺或僧院\n\n*核心规范*的*文本格式*部分提供了通用语言概念的技术描述，如值、类型和说明。如果您计划为 WebAssembly 构建工具，这些是需要了解和理解的重要概念，但是如果您只是计划在应用中使用它，这些概念就没有必要了。也就是说，文本格式是 WebAssembly 的一个重要部分，所以有些概念你应该知道。在本节中，我们将从*核心规范*中挖掘文本格式的一些细节，并强调一些要点。\n\n# 定义和特殊表达式\n\n为了理解 Wat，让我们从直接取自 WebAssembly *核心规范*的描述的第一句话开始:\n\n\"The textual format for WebAssembly modules is a rendering of their abstract syntax into S-expressions.\"\n\n那么什么是**符号表达式**(**S-表达式**)？s 表达式是嵌套列表(树形结构)数据的符号。本质上，它们提供了一种简单而优雅的方式来以文本形式表示基于列表的数据。为了理解嵌套列表的文本表示如何映射到树结构，让我们从一个 HTML 页面推断树结构。以下示例包含一个简单的 HTML 页面和相应的树结构图。\n\n一个简单的网页:\n\n```cpp\n<html>\n<head>\n  <link rel=\"icon\" href=\"favicon.ico\">\n  <title>Page Title</title>\n</head>\n<body>\n  <div>\n    <h1>Header</h1>\n    <p>This is a paragraph.</p>\n  </div>\n  <div>Some content</div>\n  <nav>\n    <ul>\n      <li>Item 1</li>\n      <li>Item 2</li>\n      <li>Item 3</li>\n    </ul>\n  </nav>\n</body>\n</html>\n```\n\n对应的树形结构是:\n\n![](img/fbdfac80-c6f7-45cb-beb9-5ebdc2ed8ffe.png)\n\nA tree structure diagram for an HTML page\n\n即使您以前从未见过树结构，从结构和层次上看 HTML 如何映射到树仍然是很清楚的。映射 HTML 元素相对简单，因为它是一种标记语言，具有定义良好的标签，没有实际的逻辑。\n\nWat 代表可以具有多种不同参数功能的模块。为了演示源代码、Wat 和相应的树结构之间的关系，让我们从一个简单的 C 函数开始，该函数将作为参数传入的数字加 2:\n\n这里有一个 C 函数，它将`2`添加到传入的`num`参数中，并返回结果:\n\n```cpp\nint addTwo(int num) {\n    return num + 2;\n}\n```\n\n将`addTwo`函数转换为有效的水会产生以下结果:\n\n```cpp\n(module\n  (table 0 anyfunc)\n  (memory $0 1)\n  (export \"memory\" (memory $0))\n  (export \"addTwo\" (func $addTwo))\n  (func $addTwo (; 0 ;) (param $0 i32) (result i32)\n    (i32.add\n      (get_local $0)\n      (i32.const 2)\n    )\n  )\n)\n```\n\n在[第一章](01.html)*什么是 WebAssembly？*中，我们谈到了与*核心规范* ( *函数*、*线性内存*、*表*等)相关的语言概念。在该规范中，*结构*部分在抽象语法的上下文中定义了每个概念。规范的*文本格式*部分也与这些概念相对应，您可以在前面的片段中看到它们是由关键字定义的(`func`、`memory`、`table`)。\n\n树形结构:\n\n![](img/28fa4fdc-1b9e-4672-b752-8bb1a272e9e0.png)\n\nA tree structure diagram for Wat\n\n整个树太大，无法放在一个页面上，所以这个图只限于 Wat 源文本的前五行。每个填充的点代表一个列表节点(或一组括号的内容)。如您所见，用 s 表达式编写的代码可以清晰简洁地以树形结构表示，这就是为什么选择 s 表达式作为 WebAssembly 的文本格式。\n\n# 值、类型和说明\n\n虽然*核心规范*的*文本格式*部分的详细内容不在本文的讨论范围内，但是值得演示一些语言概念是如何映射到相应的 Wat 的。下图在一个样例 Wat 片段中演示了这些映射。编译该代码的 C 代码表示一个函数，该函数将一个单词作为参数，并返回字符数的平方根:\n\n![](img/7c3b4124-7f45-49c6-a933-de3bf3861864.png)\n\nWat example with language concept details\n\n如果您打算编写或编辑 Wat，请注意它支持块和行注释。指令被分成块，包括设置和获取与有效类型的变量相关联的内存。您可以使用`if`语句控制逻辑流，并且使用`loop`关键字支持循环。\n\n# 在发展过程中的作用\n\n文本格式允许以文本形式表示二进制 Wasm 模块。这对于开发和调试的容易性有一些深刻的影响。拥有一个 WebAssembly 模块的文本表示允许开发人员在浏览器中查看加载模块的来源，这消除了阻碍采用氯化钠的黑盒问题。它还允许围绕故障排除模块构建工具。官方网站描述了推动文本格式设计的用例:\n\n• View Source on a WebAssembly module, thus fitting into the Web (where every source can be viewed) in a natural way. \n\n• Presentation in browser development tools when source maps aren't present (which is necessarily the case with the Minimum Viable Product (MVP)).\n\n• Writing WebAssembly code directly for reasons including pedagogical, experimental, debugging, optimization, and testing of the spec itself.\n\n列表中的最后一项反映了文本格式并不打算在正常的开发过程中手工编写，而是由 Emscripten 这样的工具生成的。生成模块时，您可能不会看到或操作任何`.wat`文件，但您可能会在调试上下文中查看它们。\n\n文本格式不仅在调试方面很有价值，而且拥有这种中间格式减少了对单一编译工具的依赖。目前存在几种不同的工具来使用和发出这种 s 表达式语法，其中一些工具被 Emscripten 用来将您的代码编译成一个`.wasm`文件。\n\n# 二进制格式和模块文件(Wasm)\n\n*核心规范*的*二进制格式*部分提供了与*文本格式*部分相同的语言概念细节。在本节中，我们将简要介绍二进制格式的一些高级细节，并讨论组成 Wasm 模块的各个部分。\n\n# 定义和模块概述\n\n二进制格式被定义为抽象语法的密集线性编码。在不太专业的情况下，这意味着它是一种高效的二进制形式，允许快速解码、小文件大小和减少内存使用。二进制格式的文件表示是一个`.wasm`文件，它将是我们在示例中使用的 Emscripten 的编译输出。\n\n二进制格式的*核心规范*的*值*、*类型*和*说明*小节与*文本格式*部分直接相关。这些概念中的每一个都包含在编码的上下文中。例如，根据规范，整数类型使用 LEB128 可变长度整数编码进行编码，采用无符号或有符号变体。如果您希望为 WebAssembly 开发工具，这些都是需要了解的重要细节，但是如果您只是打算在您的网站上使用它，这些细节就没有必要了。\n\n*核心规范*的*结构*、*二进制格式*和*文本格式* *(wat)* 部分有一个*模块*小节。在前一节中，我们没有涉及模块的各个方面，因为在二进制的上下文中描述它们更为谨慎。官方网站为模块提供了以下描述:\n\n\"The distributable, loadable, and executable unit of code in WebAssembly is called a **module**. At runtime, a module can be **instantiated** with a set of import values to produce an **instance**, which is an immutable tuple referencing all the state accessible to the running module.\"\n\n我们将在本章后面讨论如何使用 JavaScript 和 Web APIs 与模块进行交互，因此让我们建立一些上下文来理解模块元素如何映射到 API 方法。\n\n# 模块部分\n\n一个模块由几个部分组成，您将通过 JavaScript API 与其中一些部分进行交互:\n\n*   导入(`import`)是可以在模块中访问的元素，可以是以下元素之一:\n    *   函数，可以使用`call`运算符在模块内部调用\n    *   全局，可通过`global`操作器在模块内部访问\n    *   线性存储器，可通过`memory`操作器在模块内部访问\n    *   表格，可使用`call_indirect`在模块内部访问\n*   导出(`export`)是消费 API 可以访问的元素(即由 JavaScript 函数调用)\n*   模块实例初始化后调用模块启动函数(`start`)\n*   全局(`global`)包含全局变量的内部定义\n*   线性内存(`memory`)包含线性内存的内部定义，具有初始内存大小和可选的最大大小\n*   数据(`data`)包含指定给定存储器的固定范围的初始内容的数据段的阵列\n*   表(`table`)是线性存储器，其元素是特定表元素类型的不透明值:\n    *   在 MVC 中，它的主要目的是在 C/C++ 中实现间接函数调用\n*   Elements ( `elements`)是允许模块使用模块中的任何其他定义来初始化任何导入或内部定义表的元素的部分\n*   功能和代码:\n    *   函数部分声明模块中定义的每个内部函数的签名\n    *   代码段包含由函数段声明的每个函数的函数体\n\n有些关键词(`import`、`export`等等)应该看起来比较眼熟；它们出现在上一节中 Wat 文件的内容中。WebAssembly 的组件遵循直接对应于 API 的逻辑映射(例如，您将一个`memory`和`table`实例传递到 JavaScript 的`WebAssembly.instantiate()`函数中)。您与二进制格式模块的主要交互将通过这些 API 进行。\n\n# JavaScript 和网络应用接口\n\n除了 *WebAssembly 核心规范*之外，还有两个用于与 WebAssembly 模块交互的 API 规范: *WebAssembly JavaScript 接口* (JavaScript API)和 *WebAssembly Web API* 。在前几节中，我们介绍了*核心规范*的相关方面，以熟悉底层技术。如果您从未阅读过*核心规范*(或者如果您跳过了本章的前几节)，它不会禁止在您的应用中使用 WebAssembly。对于 API 来说，情况并非如此，因为它们描述了实例化和与编译后的 Wasm 模块交互所需的方法和接口。在本节中，我们将回顾网络和 JavaScript APIs，并描述如何使用 JavaScript 加载 Wasm 模块并与之通信。\n\n# WebAssembly 存储和对象缓存\n\n在深入研究交互之前，让我们讨论一下 JavaScript 和 WebAssembly 在执行环境中的关系。*核心规范*在*执行*部分包含以下描述:\n\n\"WebAssembly code is executed when instantiating a module or invoking an exported function on the resulting module instance.\n\nExecution behavior is defined in terms of an abstract machine that models the program state. It includes a stack, which records operand values and control constructs, and an abstract store containing global state.\"\n\n在幕后，JavaScript 使用一种叫做**代理**的东西来管理执行。定义中提到的*商店*包含在代理商中。下图显示了一个 JavaScript 代理:\n\n![](img/d01b7a99-69fe-4b97-b7f2-b4fed202aff1.png)\n\nJavaScript agent elements\n\n存储表示抽象机器的状态。WebAssembly 操作获取一个存储并返回一个更新的存储。每个代理都与将 JavaScript 对象映射到 WebAssembly 地址的缓存相关联。那么为什么这很重要呢？它代表了 WebAssembly 模块和 JavaScript 之间交互的底层方法。JavaScript 对象对应于 *JavaScript 应用编程接口*中的网络组件命名空间。考虑到这一点，让我们深入了解一下界面。\n\n# 加载模块和网络程序集命名空间方法\n\n*JavaScript API* 涵盖了浏览器中全局`WebAssembly`对象上可用的各种对象。在我们讨论这些之前，我们将从`WebAssembly`对象上可用的方法开始，简要概述它们的预期目的:\n\n*   `instantiate()`是编译和实例化 WebAssembly 代码的主要 API\n*   `instantiateStreaming()`执行与`instantiate()`相同的功能，但是它使用流来编译和实例化模块，这消除了中间步骤\n*   `compile()`只编译一个 WebAssembly 模块，不实例化\n*   `compileStreaming()`也只编译一个 WebAssembly 模块，但是它使用类似`instantiateStreaming()`的流\n*   `validate()`检查 WebAssembly 二进制代码以确保字节有效，如果有效则返回 true，如果无效则返回 false\n\n`instantiateStreaming()`和`compileStreaming()`方法目前只存在于*网络应用编程接口*中。事实上，这两种方法构成了整个规范。`WebAssembly`对象上可用的方法主要集中在编译和实例化模块上。考虑到这一点，让我们讨论如何获取和实例化一个 Wasm 模块。\n\n当您执行提取调用来获取模块时，它会返回一个 Promise，该 Promise 用该模块的原始字节进行解析，这些字节需要加载到`ArrayBuffer`中并进行实例化。接下来，我们将把这个过程称为加载模块。\n\n下图演示了这一过程:\n\n![](img/368f1c3b-c24a-4265-967f-6347f5fa8b34.png)\n\nFetching and loading a WebAssembly module\n\n使用 Promises，这个过程实际上非常简单。下面的代码演示了如何加载模块。`importObj`参数将任何数据或函数传递给 Wasm 模块。您现在可以忽略它，因为我们将在[第 5 章](05.html)、*创建和加载 WebAssembly 模块*中更详细地讨论它:\n\n```cpp\nfetch('example.wasm')\n  .then(response => response.arrayBuffer())\n  .then(buffer => WebAssembly.instantiate(buffer, importObj))\n  .then(({ module, instance }) => {\n    // Do something with module or instance\n  });\n```\n\n前面的例子说明了使用`instantiate()`方法加载模块的方法。`instantiateStreaming()`方法有一点不同，它通过一步获取、编译和实例化一个模块来简化过程。以下代码使用此方法实现了相同的目标(加载模块):\n\n```cpp\nWebAssembly.instantiateStreaming(fetch('example.wasm'), importObj)\n  .then(({ module, instance }) => {\n    // Do something with module or instance\n  });\n```\n\n实例化方法返回一个 Promise，该 Promise 用一个包含编译后的`WebAssembly.Module` ( `module`)和`WebAssembly.Instance` ( `instance`)的对象进行解析，这两个都将在本节后面介绍。在大多数情况下，您将使用这些方法之一在您的站点上加载一个 Wasm 模块。该实例包含所有导出的 WebAssembly 函数，您可以从 JavaScript 代码中调用这些函数。\n\n`compile()`和`compileStreaming()`方法返回一个只有编译后的`WebAssembly.Module`才能解析的承诺。如果您想要编译一个模块并在以后实例化它，这是很有用的。 **Mozilla 开发者网络** ( **MDN** )由 Mozilla 管理的 web docs 站点提供了一个例子，其中编译后的模块被传递给 Web Worker。\n\n就`validate()`方法而言，它的唯一目的是测试作为参数传入的类型化数组或`ArrayBuffer`是否有效。这将在响应的原始字节被加载到`ArrayBuffer`之后被调用。这个方法没有包含在代码示例中，因为试图实例化或编译一个无效的 Wasm 模块将会抛出一个`TypeError`或一个存在于`WebAssembly`对象上的`Error`对象。我们将在本节稍后介绍这些`Error`对象。\n\n# WebAssembly 对象\n\n除了在*加载模块和网络组件命名空间方法*一节中介绍的方法外，全局`WebAssembly`对象还有用于与网络组件进行交互和故障排除的子对象。这些对象与我们在 WebAssembly 二进制和文本格式部分讨论的概念直接相关。下面的列表包含这些对象以及它们从 MDN 中得到的定义:\n\n*   `WebAssembly.Module`对象包含无状态的 WebAssembly 代码，该代码已经由浏览器编译，可以有效地与工作人员共享，缓存在`IndexedDB`中，并多次实例化\n*   `WebAssembly.Instance`对象是一个`WebAssembly.Module`的有状态的可执行实例，它包含所有导出的网络组件函数，允许从 JavaScript 调用网络组件代码\n*   当用构造函数调用`WebAssembly.Memory`时，创建一个新的`Memory`对象，该对象是一个可调整大小的`ArrayBuffer`，保存由网络组件`Instance`访问的原始内存字节\n*   当用构造函数调用`WebAssembly.Table`时，创建一个给定大小和元素类型的新`Table`对象，该对象代表一个网络组件`Table`(存储函数引用)\n*   当使用构造函数调用`WebAssembly.CompileError`时，会创建一个错误，指示在网络程序集解码或验证过程中出现了问题\n*   当使用构造函数调用`WebAssembly.LinkError`时，会产生一个错误，表明模块实例化过程中出现了问题\n*   当使用构造函数调用`WebAssembly.RuntimeError`时，会创建一个错误，指示网络程序集指定了一个陷阱(例如，发生了堆栈溢出)\n\n让我们从`WebAssembly.Module`对象开始，逐一挖掘。\n\n# WebAssembly。组件\n\n`WebAssembly.Module`对象是`ArrayBuffer`和实例化模块之间的中间步骤。`compile()`和`instantiate()`方法(以及它们的流对应方法)返回一个用模块解析的 Promise(小写的模块代表编译后的`Module`)。也可以通过将类型化数组或`ArrayBuffer`直接传递给构造函数来同步创建模块，但对于大型模块来说，这是不鼓励的。\n\n`Module`对象也有三种静态方法:`exports()`、`imports()`和`customSections()`。这三个都以一个模块作为参数，但是`customSections()`以一个代表部分名称的字符串作为其第二个参数。自定义部分在*核心规范*的*二进制格式*部分进行了描述，旨在用于调试信息或第三方扩展。在大多数情况下，您不需要定义这些。如果您使用的是您没有创建的 Wasm 模块，则`exports()`功能非常有用，尽管您只能看到每个导出的名称和种类(例如，`function`)。\n\n对于简单的用例，您不会直接处理`Module`对象或编译模块。大部分的互动将通过`Instance`进行。\n\n# WebAssembly。情况\n\n`WebAssembly.Instance`对象是实例化的 WebAssembly 模块，这意味着您可以从中调用导出的 WebAssembly 函数。调用`instantiate()`或`instantiateStreaming()`返回一个承诺，该承诺通过包含实例的对象来解析。您可以通过引用实例的`export`属性上的函数名来调用 WebAssembly 函数。例如，如果一个模块包含一个名为`sayHello()`的导出函数，您可以使用`instance.exports.sayHello()`调用该函数。\n\n# WebAssembly。记忆\n\n`WebAssembly.Memory`对象保存网络组件`Instance`访问的内存。这个内存可以从 JavaScript 和 WebAssembly 中访问和更改。要创建`Memory`的新实例，需要将一个带有`initial`和(可选)`maximum`值的对象传递给`WebAssembly.Memory()`构造函数。这些值以网页组件页面为单位，其中一个页面为 64 KB。通过调用带有单个参数的`grow()`函数来增加内存实例的大小，该参数表示要增长的 WebAssembly 页面的数量。您也可以通过其`buffer`属性访问内存实例中包含的当前缓冲区。\n\nMDN 描述了两种获取`WebAssembly.Memory`对象的方法。第一种方法是从 JavaScript ( `var memory = new WebAssembly.Memory(...)`)中构造它，而第二种方法是通过 WebAssembly 模块导出它。重要的一点是，内存可以很容易地在 JavaScript 和 WebAssembly 之间传递。\n\n# WebAssembly。桌子\n\n`WebAssembly.Table`对象是一个类似数组的结构，用于存储函数引用。就像`WebAssembly.Memory`一样，`Table`可以从 JavaScript 和 WebAssembly 中访问和更改。在撰写本文时，表只能存储函数引用，但是随着技术的发展，其他实体也可能存储在表中。\n\n要创建一个新的`Table`实例，需要传递一个具有`element`、`initial`和(可选)`maximum`值的对象。`element`成员是表示存储在表中的值的类型的字符串；目前唯一的有效值是`\"anyfunc\"`(针对功能)。`initial`和`maximum`值代表网络组件`Table`中的元素数量。\n\n您可以使用`length`属性访问`Table`实例中的元素数量。该实例还包括操作和查询表中元素的方法。`get()`方法允许您访问给定索引处的元素，该元素作为参数传入。`set()`方法允许您将指定为第一个参数的索引处的元素设置为指定为第二个参数的值(根据前面的注释，仅支持函数)。最后，`grow()`允许您通过作为参数传入的数量来增加`Table`实例的大小(元素数量)。\n\n# WebAssembly 错误（CompileError、LinkError、RuntimeError）\n\nJavaScript API 提供了构造函数来创建特定于 WebAssembly 的`Error`对象的实例，但是我们不会花太多时间来覆盖这些对象。本节开头的对象定义列表描述了每个错误的性质，如果满足指定的条件，可能会引发该错误。所有三个错误都可以用一个消息、文件名和行号参数(所有这些参数都是可选的)来构造，并且具有与标准 JavaScript `Error`对象相同的属性和方法。\n\n# 用 WasmFiddle 连接这些点\n\n本章我们回顾了 WebAssembly 的各种元素以及相应的 JavaScript 和 Web APIs，但是理解这些部分是如何结合在一起的仍然是令人困惑的。随着本书中示例的深入，您可以看到 C/C++、WebAssembly 和 JavaScript 是如何交互的，这些概念将变得更加清晰。\n\n也就是说，这种互动的演示可能有助于消除一些困惑。在本节中，我们将使用一个名为 WasmFiddle 的在线工具来演示这些元素之间的关系，这样您就可以看到正在运行的 WebAssembly，并获得开发工作流的高级概述。\n\n# 什么是 WasmFiddle？\n\n位于[https://wasdk.github.io/WasmFiddle/](https://wasdk.github.io/WasmFiddle/)的 WasmFiddle 是一款在线代码编辑工具，可以编写一些 C 或 C++ 代码并将其转换为 Wat，编译为 Wasm，或者直接使用 JavaScript 与之交互。C/C++ 和 JavaScript 编辑器非常少，不打算用作您的主要开发环境，但它在 Wasm 编译器中提供了一项有价值的服务。在[第三章](03.html)、*设置开发环境*中，你会发现从第一步到生成 Wasm 文件需要一点工作——能够将你的 C 代码粘贴到浏览器中并点击几个按钮让事情变得更加方便。下图简要介绍了该界面:\n\n![](img/a30bacbd-3e98-45bf-a2af-c4c7b35c24ea.png)\n\nComponents of the WasmFiddle user interface\n\n可以看到，界面相对简单。让我们尝试一些代码！\n\n# 水的代码\n\n下面截图中的左上角窗格包含一个简单的 C 函数，它将指定为参数的数字加 2。左下窗格包含相应的 Wat:\n\n![](img/14cdb18b-d295-4891-a9a6-a4ab72246a22.png)\n\nC function and the corresponding Wat\n\n如果这看起来很熟悉，那是因为本章开头解释沃特的表达式时使用了相同的代码。再深入一点，你可以看到 C 代码是如何对应 Wat 输出的。`addTwo()`功能在`5`线上以字符串形式从模块中导出。线路`5`还包含`(func $addTwo)`，参考线路`6`上的`$addTwo`功能。第`6`行指定可以传入类型为`i32`(整数)的单个参数，返回的结果也是`i32`。按右上角(或 C/C++ 编辑器上方)的 Build 按钮将把 C 代码编译成 Wasm 文件。一旦构建完成，Wasm 将可以下载或与 JavaScript 交互。\n\n# Wasm 到 JavaScript\n\n下面截图中的右上角窗格包含一些 JavaScript 代码，用于编译上一步生成的 Wasm。`wasmCode`是在构建完成时生成的，所以它应该是自动可用的。WasmFiddle 不是使用`instantiate()`方法，而是创建一个编译后的`WebAssembly.Module`实例，并将其传递给新的`WebAssembly.Instance`的构造函数。`wasmImports`对象当前是空的，尽管如果需要，我们可以传入一个`WebAssembly.Memory`和`WebAssembly.Table`实例:\n\n![](img/82dade82-c419-4ccb-85d2-89f58e525bcb.png)\n\nJavaScript code calling the C function from the compiled Wasm module\n\n当传递数字`2`时，JavaScript 的最后一行将`addTwo()`的结果打印到右下窗格的输出中。`log()`方法是一个自定义功能，确保输出被打印到右下窗格(数字`4`)。注意 JavaScript 代码是如何与`wasmInstance`交互的。从实例的`exports`对象调用`addTwo()`函数。虽然这是一个人为的例子，但它演示了 C 或 C++ 代码在被 JavaScript 用作 Wasm 模块之前所经历的步骤。\n\n# 摘要\n\n在本章中，我们讨论了 WebAssembly 的元素及其关系。*核心规范*的结构用于描述文本和二进制格式到通用抽象语法的映射。我们强调了文本格式(Wat)在调试和开发环境中有用的方面，以及为什么 s 表达式非常适合抽象语法的文本表示。我们还回顾了与二进制格式和组成模块的各种元素相关的细节。JavaScript 和 Web APIs 中的方法和对象是通过描述它们在 WebAssembly 交互中的角色来定义的。最后，使用 WasmFiddle 工具给出了源代码、Wat 和 JavaScript 之间关系的一个简单示例。\n\n在[第 3 章](03.html)、*设置开发环境*中，我们将安装开发工具，我们将使用它来有效地与 WebAssembly 一起工作。\n\n# 问题\n\n1.  s 表达式擅长表示什么样的数据？\n2.  二进制和文本格式共有的四个语言概念是什么？\n3.  文本格式的用例之一是什么？\n4.  一个 WebAssembly `Table`中唯一可以存储的元素类型是什么？\n5.  JavaScript 引擎使用什么来管理执行？\n6.  哪个方法实例化一个模块需要更少的代码，`instantiate()`或`instantiateStreaming()`？\n7.  `WebAssembly` JavaScript 对象上有哪些错误对象，每一个都是什么事件导致的？\n\n# 进一步阅读\n\n*   MDN 上的 WebAssembly:[https://developer.mozilla.org/en-US/docs/WebAssembly](https://developer.mozilla.org/en-US/docs/WebAssembly)\n*   wamfidle:https://wadk . github . io/wamfidle\n*   维基百科上的 s-表达式:[https://en.wikipedia.org/wiki/S-expression](https://en.wikipedia.org/wiki/S-expression)\n*   树的例子:[http://interactivepython . org/符文石/static/python ons/Trees/examplesoftrees . html](http://interactivepython.org/runestone/static/pythonds/Trees/ExamplesofTrees.html)"
  },
  {
    "path": "docs/learn-wasm/03.md",
    "content": "# 三、建立开发环境\n\n既然您已经熟悉了 WebAssembly 的元素，那么是时候设置一个合适的开发环境了。用 WebAssembly 开发相当于用 C 或 C++ 开发。区别在于构建过程和输出。在本章中，我们将介绍开发工具，以及如何在您的系统上安装和配置它。\n\n本章的目标是理解以下内容:\n\n*   如何安装所需的开发工具(Git、Node.js 和 Visual Studio 代码)\n*   如何使用扩展将 Visual Studio 代码配置为与 C/C++ 和 WebAssembly 一起使用\n*   如何设置本地 HTTP 服务器来提供 HTML、JavaScript 和`.wasm`文件\n*   正在检查您的浏览器是否支持 WebAssembly\n*   有哪些有用的工具可以简化和改进开发过程\n\n# 安装开发工具\n\n您需要安装一些应用和工具来开始开发 WebAssembly。我们将使用 Visual Studio Code(一个文本编辑器)来编写我们的 C/C++、JavaScript、HTML 和 Wat。我们还将使用 Node.js 来提供文件，并使用 Git 来管理我们的代码。我们将使用包管理器来安装这些工具，这使得安装过程比手动下载和安装它们简单得多。在本节中，我们将介绍操作系统，以及每个平台的包管理器。我们还将回顾每个应用，简要概述它们在开发过程中的作用。\n\n# 操作系统和硬件\n\n为了确保安装和配置过程顺利进行，了解我将在本书示例中使用的操作系统非常重要。如果您遇到问题，可能是因为您正在使用的平台和我正在使用的平台不兼容。在大多数情况下，你不应该有问题。为了避免操作系统版本成为潜在的问题原因，我在下面的列表中提供了我正在使用的操作系统的详细信息:\n\n# 苹果电脑\n\n*   高塞拉，版本 10.13.x\n*   2.2 GHz 英特尔 i7 处理器\n*   16 GB 内存\n\n# 人的本质\n\n*   运行在 VMware Fusion 中的 Ubuntu 16.04 LTS\n*   2.2 千兆赫英特尔 i7 处理器\n*   4 GB 内存\n\n# Windows 操作系统\n\n*   在 VMware Fusion 中运行的 Windows 10 Pro\n*   2.2 千兆赫英特尔 i7 处理器\n*   8 GB 内存\n\n# 包管理器\n\n包管理器是简化软件安装过程的工具。它们允许我们从命令行升级、配置、卸载和搜索可用软件，而不必去网站下载和运行安装程序。它们还简化了软件的安装过程，这些软件可能有多个依赖项，或者在使用前需要手动配置。在本节中，我将介绍每个平台的包管理器。\n\n# 苹果电脑自制程序\n\nHomebrew 是一个优秀的 macOS 软件包管理器，它允许我们安装大部分开箱即用的工具。自制就像在终端中粘贴以下命令并运行它一样简单:\n\n```cpp\n/usr/bin/ruby -e \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)\"\n```\n\n您将在终端中看到消息，这些消息将引导您完成安装过程。一旦完成，你将需要为自制程序安装一个名为**自制程序-木桶**的扩展，允许你安装 macOS 应用，而不必下载安装程序，安装它，并将应用拖到`Applications`文件夹中。您可以通过运行以下命令来安装:\n\n```cpp\nbrew tap caskroom/cask\n```\n\n就这样！您现在可以通过运行以下任一命令来安装应用:\n\n```cpp\n# For command line tools: brew install <Tool Name> \n# For desktop applications:\nbrew cask install <Application Name>\n```\n\n# 适合 Ubuntu\n\nApt 是 Ubuntu 提供的包管理器；没有必要安装它。它允许您安装现成的命令行工具和应用。如果 Apt 的存储库中没有应用，您可以使用以下命令添加存储库:\n\n```cpp\nadd-apt-repository \n```\n\n# 窗户巧克力\n\n巧克力是 Windows 的一个包管理器。它类似于 Apt，因为它允许您安装命令行工具和应用。要安装巧克力，您需要以管理员身份运行命令提示符(`cmd.exe`)。您可以通过按“开始”菜单按钮，键入 cmd，右键单击命令提示符应用并选择“以管理员身份运行”来完成此操作:\n\n![](img/257d1dc9-7c8b-4e3e-91ff-2697e0749527.png)\n\nRunning the Command Prompt as an administrator\n\n然后只需运行以下命令:\n\n```cpp\n@\"%SystemRoot%\\System32\\WindowsPowerShell\\v1.0\\powershell.exe\" -NoProfile -InputFormat None -ExecutionPolicy Bypass -Command \"iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))\" &amp;&amp; SET \"PATH=%PATH%;%ALLUSERSPROFILE%\\chocolatey\\bin\"\n```\n\nThe easiest way to get the command text is through Chocolatey's installation page at [https://chocolatey.org/install](https://chocolatey.org/install). There's a button to copy the text to your clipboard under the *Install with* *cmd.exe* section. You could also install the application using PowerShell if you follow the steps on the Installation page.\n\n# 饭桶\n\nGit 是一个**版本控制系统**(**【VCS】**)，允许您跟踪文件的更改，并管理贡献相同代码库的多个开发人员之间的工作。Git 是为 GitHub 和 GitLab 提供动力的 VCS，也可以在 Bitbucket 上获得(他们也提供 Mercurial，这是另一个 VCS)。Git 将允许我们从 GitHub 克隆存储库，并且是 EMSDK 的先决条件，我们将在下一章中介绍。在本节中，我们将介绍 Git 的安装过程。\n\n# 在 macOS 上安装 Git\n\n如果你正在使用苹果操作系统，Git 可能已经可用了。macOS 与 Apple Git 捆绑在一起，这可能会比最新版本落后几个版本。就本书而言，您已经安装的版本应该足够了。如果您希望升级，您可以通过在终端中运行以下命令，使用自制程序安装最新版本的 Git:\n\n```cpp\n# Install Git to the Homebrew installation folder (/usr/local/bin/git):\nbrew install git\n\n# Ensure the default Git is pointing to the Homebrew installation:\nsudo mv /usr/bin/git /usr/bin/git-apple\n```\n\n如果运行这个命令，应该会看到`/usr/local/bin/git`:\n\n```cpp\nwhich git\n```\n\n您可以通过运行以下命令来确保安装成功:\n\n```cpp\ngit --version\n```\n\n# 在 Ubuntu 上安装 Git\n\n可以使用`apt`安装 Git 只需在终端中运行以下命令:\n\n```cpp\nsudo apt install git\n```\n\n您可以通过运行以下命令来确保安装成功:\n\n```cpp\ngit --version\n```\n\n# 在 Windows 上安装 Git\n\n你可以用巧克力来安装 Git。打开命令提示符或 PowerShell 并运行以下命令:\n\n```cpp\nchoco install git\n```\n\n您可以通过运行以下命令来确保安装成功:\n\n```cpp\ngit --version\n```\n\nYou can bypass the confirmation messages by adding a `-y` to the end of the install command (for example, `choco install git -y`). You can also opt to always skip the confirmation by entering the  \n**`choco feature enable -n allowGlobalConfirmation`** command.\n\n# Node.js\n\nNode.js 的官方网站将其描述为异步事件驱动的 JavaScript 运行时。Node 旨在构建可扩展的网络应用。我们将在本书中使用它来提供我们的文件，并在浏览器中使用它们。Node.js 自带`npm`，这是一个用于 JavaScript 的包管理器，它将允许我们全局安装包并通过命令行访问它们。在本节中，我们将介绍使用**节点版本管理器** ( **nvm** )的每个平台的安装过程。\n\n# nvm\n\n我们将使用 Node.js(版本 8)的**长期稳定的** ( **LTS** )版本来确保我们使用的是平台最稳定的版本。我们将使用`nvm`来管理 Node.js 版本。如果您的计算机上已经安装了较高(或较低)版本的 Node.js，这将防止冲突。`nvm`允许您安装多个版本的 Node.js，您可以在单个终端窗口的上下文中快速切换和隔离。\n\n# 在 macOS 上安装 nvm\n\n在终端中运行以下命令:\n\n```cpp\nbrew install nvm\n```\n\n遵循家酿指定的安装后步骤，以确保您可以开始使用它(您可能需要重新启动您的终端会话)。如果在执行这些步骤之前清除了终端内容，则可以运行此命令再次查看安装步骤:\n\n```cpp\nbrew info nvm\n```\n\n您可以通过运行以下命令来确保安装成功:\n\n```cpp\nnvm --version\n```\n\n# 在 Ubuntu 上安装 nvm\n\nUbuntu 附带了`wget`，可以使用 HTTP/S 和 FTP/S 协议检索文件。`nvm`([https://github.com/creationix/nvm](https://github.com/creationix/nvm))的 GitHub 页面包含以下使用`wget`进行安装的命令:\n\n```cpp\nwget -qO- https://raw.githubusercontent.com/creationix/nvm/v0.33.11/install.sh | bash\n```\n\n安装完成后，重新启动终端以完成安装。您可以通过运行以下命令来检查以确保安装成功:\n\n```cpp\nnvm --version\n```\n\n# 在 Windows 上安装 nvm\n\n`nvm`目前不支持 Windows，所以你实际上是在安装一个名为`nvm`的不同应用——Windows。`nvm`视窗的 GitHub 页面可以在[https://github.com/coreybutler/nvm-windows](https://github.com/coreybutler/nvm-windows)找到。有些命令略有不同，但我们运行的安装命令将是相同的。要安装`nvm`窗口，请打开命令提示符或 PowerShell 并运行以下命令:\n\n```cpp\nchoco install nvm\n```\n\n您可以通过运行以下命令来检查以确保安装成功:\n\n```cpp\nnvm --version\n```\n\n# 使用 nvm 安装 Node.js\n\n安装`nvm`后，需要安装我们在本书中要用到的 Node.js 版本:8.11.1 版本。要安装它，请运行以下命令:\n\n```cpp\nnvm install 8.11.1\n```\n\n如果您之前没有安装 Node.js 或`nvm`，它会自动将其设置为默认的 Node.js 安装，因此该命令的输出应该是`v8.11.1`:\n\n```cpp\nnode --version\n```\n\n如果您已经安装了现有的 Node.js 版本，您可以使用 v8.11.1 作为默认版本，或者确保在阅读本书中的示例时运行此命令来使用 v8.11.1:\n\n```cpp\nnvm use 8.11.1\n```\n\nYou can create a file named `.nvmrc` in the folder with your code and populate it with the contents `v8.11.1`. You can run `nvm use` within this directory and it will set the version to `8.11.1` without having to specify it.\n\n# GNU make 和 rimraf\n\n在`learn-webassembly`存储库中，代码示例使用 GNU Make 和 VS Code 的 Tasks 特性(我们将在[第 5 章](05.html)、*创建和加载 WebAssembly 模块*中介绍)来执行本书中定义的构建任务。GNU Make 是一个优秀的跨平台工具，用于自动化构建过程。你可以在[https://www.gnu.org/software/make](https://www.gnu.org/software/make)阅读更多关于 GNU Make 的内容。让我们回顾一下每个平台的安装步骤。\n\n# macOS 和 Ubuntu 上的 GNU Make\n\n如果你用的是 macOS 或者 Linux，应该已经安装了 GNU `make`。要验证这一点，请在终端中运行以下命令:\n\n```cpp\nmake -v\n```\n\n如果你看到版本信息，你就准备好了。跳到*安装轮辋*部分。否则，请遵循您的平台的 GNU Make 安装说明。\n\n# 在 macOS 上安装 GNU Make\n\n要在 macOS 上安装 GNU Make，请从终端运行以下命令:\n\n```cpp\nbrew install make\n```\n\n您可以通过运行以下命令来确保安装成功:\n\n```cpp\nmake -v\n```\n\n如果您看到版本信息，请跳到*安装* *rimraf* 部分。\n\n# 在 Ubuntu 上安装 GNU Make\n\n要在 Ubuntu 上安装 GNU Make，请从终端运行以下命令:\n\n```cpp\nsudo apt-get install make\n```\n\n您可以通过运行以下命令来确保安装成功:\n\n```cpp\nmake -v\n```\n\n如果看到版本信息，跳到*安装轮圈*部分。\n\n# 在 Windows 上安装 GNU make\n\n你可以用巧克力在 Windows 上安装 GNU `make`。打开命令提示符或 PowerShell 并运行以下命令:\n\n```cpp\nchoco install make\n```\n\n您可能需要重新启动命令行界面才能使用`make`命令。重新启动后，运行以下命令来验证安装:\n\n```cpp\nmake -v\n```\n\n如果您看到版本信息，请继续下一节。如果遇到问题，可能需要在[http://gnuwin32.sourceforge.net/packages/make.htm](http://gnuwin32.sourceforge.net/packages/make.htm)下载安装安装包。\n\n# 安装 rimraf\n\n在生成文件或 VS 代码任务中定义的一些构建步骤会删除文件或目录。删除文件或文件夹所需的命令因平台和 shell 而异。为了解决这个问题，我们将使用`rimraf npm`包([https://www.npmjs.com/package/rimraf](https://www.npmjs.com/package/rimraf))。全局安装软件包提供了一个`rimraf`命令，用于对操作系统和外壳执行正确的删除操作。\n\n要安装`rimraf`，请确保安装了 Node.js，并从命令行界面运行以下命令:\n\n```cpp\nnpm install -g rimraf\n```\n\n要确保安装成功，请运行以下命令:\n\n```cpp\nrimraf --help\n```\n\n您应该会看到使用说明和命令行标志列表。让我们继续进行 VS 代码安装。\n\n# VS 代码\n\nVS Code 是一个跨平台的文本编辑器，具有多语言支持和丰富的扩展生态系统。内置了集成调试和 Git 支持，并且一直在添加新功能。在本书的整个过程中，我们可以在整个 WebAssembly 开发过程中使用它。在本节中，我们将介绍每个平台的安装步骤:\n\n![](img/d936d227-0ea4-44a0-8ffa-f43768420eb0.png)\n\nScreenshot from Visual Studio Code's website\n\n# 在苹果电脑上安装 Visual Studio 代码\n\n使用自制酒桶安装 VS 代码。在终端中运行以下命令进行安装:\n\n```cpp\nbrew cask install visual-studio-code\n```\n\n一旦完成，您应该能够从`Applications`文件夹或启动板启动它。\n\n# 在 Ubuntu 上安装 Visual Studio 代码\n\n在 Ubuntu 上安装 VS 代码的过程有一些额外的步骤，但仍然相对简单。首先从 VS Code 的下载页面([https://code.visualstudio.com/Download](https://code.visualstudio.com/Download)下载`.deb`文件。下载完成后，运行以下命令来完成安装:\n\n```cpp\n# Change directories to the Downloads folder\ncd ~/Downloads\n\n# Replace <file> with the name of the downloaded file\nsudo dpkg -i <file>.deb\n\n# Complete installation\nsudo apt-get install -f\n```\n\n如果出现缺失依赖错误，可以在`sudo dpkg`之前运行以下命令进行修复:\n\n```cpp\nsudo apt-get install libgconf-2-4\nsudo apt --fix-broken install\n```\n\n你现在应该可以从启动器打开 VS 代码了。\n\n# 在 Windows 上安装 VS 代码\n\n你可以用巧克力来安装 VS 代码。从命令提示符或 PowerShell 运行此命令:\n\n```cpp\nchoco install visualstudiocode\n```\n\n安装后，您可以从“开始”菜单访问它。\n\nYou can open VS Code with the current working directory as the project by running `code .` in the CLI.\n\n# 配置 VS 代码\n\n开箱即用，VS Code 是一个功能强大的文本编辑器，有很多很棒的功能。除了高度可配置和可定制之外，它还拥有极其丰富的扩展生态系统。我们需要安装其中的一些扩展，这样我们就不需要为不同的编程语言使用不同的编辑器。在本节中，我们将介绍如何配置 VS 代码以及安装哪些扩展来简化 WebAssembly 开发过程。\n\n# 管理设置和自定义\n\n定制和配置 VS 代码既简单又直观。您可以通过选择“代码|首选项| macOS 上的设置”或“文件|首选项| Windows 上的设置”来管理自定义设置，如编辑器字体和选项卡大小。用户和工作区设置是在 JSON 文件中单独管理的，并且提供了自动完成功能，以防您记不住设置的确切名称。您也可以通过在“首选项”菜单中选择适当的选项来更改主题或键盘快捷键。设置文件也是您可以为安装的任何扩展设置自定义设置的地方。默认情况下，有些设置是在安装扩展时添加的，因此更改它们就像更新和保存此文件一样简单。\n\n# 扩展概述\n\n作为配置过程的一部分，我们需要安装一些扩展。在 VS 代码中有多种方法可以找到并安装扩展。我更喜欢单击扩展按钮(编辑器左侧活动栏顶部的第四个按钮)，在搜索框中输入我要查找的内容，然后按下我要安装的扩展的绿色安装按钮。您也可以访问位于[https://marketplace.visualstudio.com/vscode](https://marketplace.visualstudio.com/vscode)的 VS 代码市场，搜索并选择您想要安装的扩展，然后按下扩展页面上的绿色安装按钮。您也可以通过命令行管理扩展。更多信息，请访问[https://code.visualstudio.com/docs/editor/extension-gallery](https://code.visualstudio.com/docs/editor/extension-gallery):\n\n![](img/a4681032-4748-44a9-a937-d92678e2636f.png)\n\nInstalling extensions in VS Code\n\n# C/C++ 和网络程序集的配置\n\nVS Code 不支持现成的 C 和 C++ 语言，但是有一个很好的扩展可以让你使用这些语言。它也不支持 WebAssembly 文本格式的语法高亮显示，但是有一个扩展也添加了这一功能。在本节中，我们将介绍用于 VS 代码的 *C/C++ 和用于 VS 代码*扩展的*WebAssembly 工具包的安装和配置。*\n\n# 为 VS 代码安装 C/C++\n\nVS 代码的 C/C++ 扩展包括几个用于编写和调试 C 和 C++ 代码的特性，例如自动完成、符号搜索、类/方法导航、逐行代码步进以及许多其他特性。要安装扩展，请在扩展中搜索 C/C++，然后安装名为 C/C++(由微软创建)的扩展，或者导航到位于[https://marketplace.visualstudio.com/items?的扩展官方页面 itemName=ms-vscode.cpptools](https://marketplace.visualstudio.com/items?itemName=ms-vscode.cpptools) 并按下绿色的安装按钮。\n\n安装后，您可以通过从 VS 代码中的扩展列表中选择扩展并选择贡献选项卡来查看扩展的配置详细信息。此选项卡包含各种设置、命令和调试器详细信息:\n\n![](img/db5aeb52-94d7-4d7b-9af0-e3637e5cb4ce.png)\n\n*Contributions* tab for the C/C++ extension\n\n# 为 VS 代码配置 C/C++\n\n微软有一个扩展的官方页面，你可以在[https://code.visualstudio.com/docs/languages/cpp](https://code.visualstudio.com/docs/languages/cpp)查看。该页面描述了如何通过使用 JSON 文件进行配置。让我们从创建一个新的配置文件来管理我们的 C/C++ 环境开始。按下 *F1* 键，输入 C/C，选择 C/Cpp:编辑配置…，即可生成新的配置文件；\n\n![](img/653b245b-f3a0-4fb5-bacd-9624555a6bde.png)\n\nCommand Palette with C/C++ extension options\n\n这将在当前项目的`.vscode`文件夹中生成一个新的`c_cpp_properties.json`。该文件将包含基于您的平台的 C/C++ 编译器的配置选项、要使用的 C 和 C++ 标准以及头文件的包含路径。文件生成后，您可以将其关闭。我们将在配置 EMSDK 时重新讨论它。\n\n# 虚拟代码 WebAssembly 工具包\n\n目前有几种不同的 VS 代码的 WebAssembly 扩展可用。我使用的是 VSCode 扩展的 WebAssembly 工具包，因为它允许你右击一个`.wasm`文件并选择 Show WebAssembly，它会显示文件的 Wat 表示。您可以通过扩展面板(搜索 WebAssembly)或从 VS 代码市场([https://marketplace.visualstudio.com/items?)的官方扩展页面安装此扩展 itemName=dtsvet.vscode-wasm](https://marketplace.visualstudio.com/items?itemName=dtsvet.vscode-wasm) ):\n\n![](img/e5df8895-859b-40c0-a647-5c7d51fd567e.png)\n\nViewing the Wat for a `.wasm` file using the WebAssembly Toolkit for the VS Code extension\n\n安装完成后，您就可以出发了！现在您已经获得了所有必需的扩展，让我们来评估一些可以简化常见任务的可选扩展。\n\n# 其他有用的扩展\n\nVS Code 有一些很棒的扩展来提高效率和定制界面。在本节中，我将介绍一些我已经安装的扩展，它们简化了常见任务以及用户界面/图标主题。对于本书中的示例，您不需要安装任何这些扩展，但是您可能会发现其中一些很有用。\n\n# 自动重命名标签\n\n这个扩展在处理 HTML 时非常有用。如果您更改标签类型，它会自动更改结束标签的名称。例如，如果您有一个`<div>`元素，并且您想要使它成为一个`<span>`，将开始元素的文本更改为`span`将会更新结束元素的文本(`</div>`到`</span>`):\n\n![](img/94ea0207-f80b-43fd-8907-fca386c945c7.png)\n\nAuto renaming tag renaming HTML tag\n\n# 括号对着色\n\n这个扩展为代码中的括号、大括号和圆括号着色，以便快速识别左括号和右括号。WebAssembly 的文本格式广泛使用括号，因此能够确定哪些元素包含在哪个列表中使得调试和评估更加简单:\n\n![](img/6a7bd70b-1c52-43b9-ba23-3db2d688ca43.png)\n\nBracket pair colorizer color matching parentheses in a Wat file\n\n# 材质图标主题和原子一光主题\n\nVS 代码市场上有 1000 多个图标和界面主题。我将材质图标主题和 Atom One Light 主题包含在这一部分中，因为它们在本书的截图中使用。材质图标主题非常受欢迎，下载量超过 200 万次，而 Atom One Light 主题的下载量超过 7 万次:\n\n![](img/c32d88eb-7850-49c3-9ff4-84e0eb89969d.png)\n\nIcons in the Material Icons theme\n\n# 为网络设置\n\n与 Wasm 模块的交互和调试将在浏览器中完成，这意味着我们需要一种方法来提供一个包含示例文件的文件夹。正如我们在[第 2 章](02.html)、*中讨论的 WebAssembly 元素——Wat、Wasm 和 JavaScript API* ，WebAssembly 被集成到浏览器的 JavaScript 引擎中，但是您需要确保您使用的是支持它的浏览器。在本节中，我们将提供克隆书籍示例存储库的说明。我们还将回顾如何快速设置本地 web 服务器来测试和评估浏览器选项，以确保您能够在本地开发。\n\n# 克隆图书范例库\n\n现在，您可能想用本书中包含的所有示例来克隆 GitHub 存储库。您肯定需要有第 7 章*的可用代码，从零开始创建应用*，因为应用的代码库太大，无法放入单个章节。选择硬盘上的一个文件夹，并运行以下命令来克隆存储库:\n\n```cpp\ngit clone https://github.com/mikerourke/learn-webassembly\n```\n\n克隆过程完成后，您会发现示例是按章节组织的。如果一个章节中有几个例子，它们会被章节文件夹中的子文件夹分解。\n\nIf you're using Windows, do not clone the repository into the `\\Windows` folder or any other folder with limited permissions. Otherwise, you will run into issues when attempting to compile the examples.\n\n# 安装本地服务器\n\n我们将使用`npm`包`serve`来提供文件。要安装，只需运行以下命令:\n\n```cpp\nnpm install -g serve\n```\n\n安装完成后，您可以在任何文件夹中提供文件。为了确保它正常工作，让我们尝试提供一个本地文件夹。该部分的代码位于`learn-webassembly`存储库的`/chapter-03-dev-env`文件夹中。按照以下说明验证您的服务器安装:\n\n1.  首先，让我们创建一个文件夹，其中包含我们将在本书剩余部分中处理的代码示例(示例使用名称`book-examples`)。\n2.  启动 VS 代码并选择文件|打开...从 macOS/Linux 的菜单栏中，选择文件|打开文件夹...对于 Windows。\n3.  接下来，选择文件夹，`book-examples`，并按下打开(或选择文件夹)按钮。\n\n4.  一旦 VS 代码完成加载，在 VS 代码文件浏览器中右键单击，从菜单中选择新建文件夹，并命名文件夹`chapter-03-dev-env`。\n5.  选择`chapter-03-dev-env`文件夹，按【新建文件】按钮(或*Cmd*/*Ctrl*+*N*)新建一个文件。命名文件`index.html`并用以下内容填充:\n\n```cpp\n<!doctype html>\n<html lang=\"en-us\">\n  <title>Test Server</title>\n</head>\n<body>\n  <h1>Test</h1>\n  <div>\n    This is some text on the main page. Click <a href=\"stuff.html\">here</a>\n    to check out the stuff page.\n  </div>\n</body>\n</html>\n```\n\n6.  在名为`stuff.html`的`chapter-03-dev-env`文件夹中创建另一个文件，并用以下内容填充:\n\n```cpp\n<!doctype html>\n<html lang=\"en-us\">\n<head>\n  <title>Test Server</title>\n</head>\n<body>\n  <h1>Stuff</h1>\n  <div>\n    This is some text on the stuff page. Click <a href=\"index.html\">here</a>\n    to go back to the index page.\n  </div>\n</body>\n</html>\n```\n\n7.  我们将使用 VS Code 的集成终端来提供文件。您可以通过选择查看|集成终端，或使用键盘快捷键 *Ctrl* + *`* (在 *Esc* 键下的 *`* 是倒勾键)进行访问。加载后，运行以下命令来提供工作文件夹:\n\n```cpp\nserve -l 8080 chapter-03-dev-env\n```\n\n您应该会看到以下内容:\n\n![](img/00cc7407-20b7-42e2-8831-9dcedd076726.png)\n\nResults of running the serve command in terminal\n\n`-l 8080`标志告诉`serve`在港口`8080`提供文件夹。第一个链接(`http://127.0.0.1:8080`)只能在您的计算机上访问。以下可用于从本地网络上的另一台计算机访问页面的任何链接。如果您导航到浏览器中的第一个链接(`http://127.0.0.1:8080/index.html`)，您应该会看到:\n\n![](img/2c6d3092-1cb0-4e6e-8aa7-436774502b04.png)\n\nTest page served up in Google Chrome\n\n点击此处链接将带您进入素材页面(地址栏将显示`127.0.0.1:8080/stuff.html`。如果一切正常，是时候验证你的浏览器了。\n\n# 验证您的浏览器\n\n为了确保您能够在浏览器中测试示例，您需要确保有一个全局`WebAssembly`对象可用。为了防止与浏览器兼容性相关的任何问题，我建议您安装谷歌 Chrome 或 Mozilla Firefox 进行开发。如果您事先安装了这些浏览器中的任何一个，那么您的浏览器很有可能已经有效了。为了彻底起见，我们仍将介绍验证过程。在本节中，我将回顾您可以采取的步骤，以确保您的浏览器支持 WebAssembly。\n\n# 验证谷歌浏览器\n\n验证 Chrome 的过程非常简单。选择看起来像三个垂直点的按钮(地址栏旁边)，选择**更多工具** | **开发者工具**或使用键盘快捷键*Cmd*/*Ctrl*+*Shift*+*I*:\n\n![](img/a8ed36a6-4d8f-4db0-9a43-316d02af59ee.png)\n\nAccessing Developer Tools in Google Chrome\n\n出现开发者工具窗口后，选择控制台选项卡，输入`WebAssembly`，按*回车。*如果你看到这个，你的浏览器是有效的:\n\n![](img/8a26528d-e9a8-4b91-920d-52a4472250e0.png)\n\nResults of WebAssembly validation in Google Chrome's Developer Tools console\n\n# 正在验证 Mozilla Firefox\n\n火狐的验证过程与谷歌 Chrome 几乎完全相同。从菜单栏中选择**工具** | **网络开发人员** | **切换工具**或使用键盘快捷键*Cmd*/*Ctrl*+*Shift*+*I*:\n\n![](img/ec99f38f-7129-4e8c-a154-e616dc89e593.png)\n\nAccessing Developer Tools in Mozilla Firefox\n\n选择控制台选项卡，在命令输入框内点击，输入`WebAssembly`，按*进入*。如果您的火狐版本有效，您将会看到以下内容:\n\n![](img/a9aa4970-18d1-42f9-94fa-a572856d2b08.png)\n\nResults of WebAssembly validation in Mozilla Firefox's Developer Tools console\n\n# 正在验证其他浏览器\n\n其他浏览器的验证过程基本相同；不同浏览器之间唯一不同的验证方面是如何访问开发人员工具。如果通过您正在使用的浏览器的控制台可以获得一个`WebAssembly`对象，您可以使用该浏览器进行 WebAssembly 开发。\n\n# 其他工具\n\n除了我们在前面几节中介绍的应用和工具之外，还有一些非常棒的工具可以免费使用，并且功能丰富，可以极大地改进您的开发过程。我没有时间一一介绍，但我想强调一下我经常使用的那些。在这一节中，我将简要回顾一些适用于每个平台的流行工具和应用。\n\n# 用于 macOS 的 iTerm2\n\n默认的 macOS 安装包括终端应用，终端，这在本书中已经足够使用了。如果你想要一个功能更全的终端，iTerm2 是一个很好的选择。它提供了诸如拆分窗口、广泛定制、多个配置文件等功能，以及可以显示注释、运行作业、命令历史等的 Toolbelt 功能。您可以从官方网站([https://www.iterm2.com/](https://www.iterm2.com/))下载镜像文件并手动安装，或者使用以下命令用自制酒桶安装 iTerm:\n\n```cpp\nbrew cask install iterm2\n```\n\n这是 iTerm2 在工具带打开和多个编辑器窗口的情况下运行:\n\n![](img/25f36b2a-329f-46ea-b070-2f580add757f.png)\n\nITerm instance with multiple panes and Toolbelt\n\n# Ubuntu 的终结者\n\nTerminator 是 Ubuntu 的 iTerm 和`cmder`，终端仿真器，允许在一个窗口中有多个标签和窗格。Terminator 还提供了拖放、查找功能以及大量插件和主题等功能。可以通过`apt`安装终结者。要确保您使用的是最新版本，请在终端中运行以下命令:\n\n```cpp\nsudo add-apt-repository ppa:gnome-terminator\nsudo apt-get update\nsudo apt-get install terminator \n```\n\n参考截图:\n\n![](img/e8a7a5ed-adea-42a2-9d92-b9c55bc1a87b.png)\n\nTerminator screenshot taken from http://technicalworldforyou.blogspot.com\nB09984_03_17\n\n# Windows 的 cmder\n\n`cmder`是 Windows 的控制台模拟器，为标准的命令提示符或 PowerShell 增加了很多功能和特性。它提供了多个选项卡和自定义等功能。它允许您在同一个程序中打开不同外壳的实例。您可以从官方网站([cmder.net](https://cmder.net))下载并安装它，或者使用以下命令用巧克力安装它:\n\n```cpp\nchoco install cmder\n```\n\n事情是这样的:\n\n![](img/2ec1d160-3abc-4e51-9614-da686a21035e.png)\n\ncmder screenshot from the official website\n\n# Zsh 和 Oh-我的-Zsh\n\nZsh 是一个在 Bash 基础上改进的交互式外壳。Oh-My-Zsh 是 Zsh 的一个配置管理器，有很多有用的插件。你可以在他们的网站上看到整个名单([https://github.com/robbyrussell/oh-my-zsh](https://github.com/robbyrussell/oh-my-zsh))。例如，如果您想要命令行界面中强大的自动完成和语法突出显示功能，可以使用 zsh-autosuggestion 和 zsh-语法突出显示等插件。您可以在 macOS、Linux 和 Windows 上安装和配置 Zsh 和 Oh-My-Zsh。Oh-My-Zsh 页面有安装说明以及主题和插件列表。\n\n# 摘要\n\n在本章中，我们介绍了开发工具的安装和配置过程，我们将使用该工具开始使用 WebAssembly。我们讨论了如何使用针对您的操作系统的包管理器(例如，用于 macOS 的自制程序)快速轻松地安装 Git、Node.js 和 VS Code。介绍了配置 VS 代码的步骤，以及可以添加以增强开发体验的必需和可选扩展。我们讨论了如何安装本地 web 服务器进行测试，以及如何验证您的浏览器以确保支持 WebAssembly。最后，我们简要回顾了一些您可以为您的平台安装的辅助开发工具。\n\n在[第 4 章](04.html)、*安装所需的依赖项*中，我们将安装所需的依赖项并测试工具链。\n\n# 问题\n\n1.  操作系统应该使用的软件包管理器的名称是什么？\n2.  BitBucket 支持 Git 吗？\n3.  为什么我们使用 8 版的 Node.js 而不是最新的版本？\n4.  如何在 Visual Studio Code 中更改颜色主题？\n5.  如何在 Visual Studio 代码中访问命令面板？\n6.  如何检查浏览器是否支持 WebAssembly？\n7.  这三个操作系统都支持*其他工具*部分的哪个工具？\n\n# 进一步阅读\n\n*   自制程序: [https://brew.sh](https://brew.sh)\n*   `apt`文件:[https://help . Ubuntu . com/lt/server uide/apt . html。位于](https://help.ubuntu.com/lts/serverguide/apt.html.en)\n*   巧克力:https://chocolate . org\n*   去:t0 https://git-SCM . com\n*   node . js:https://nodejs . org/en\n*   GNU Make:[https://www.gnu.org/software/make](https://www.gnu.org/software/make)\n*   VS 代码:[https://code.visualstudio.com](https://code.visualstudio.com)"
  },
  {
    "path": "docs/learn-wasm/04.md",
    "content": "# 四、安装所需的依赖项\n\n现在您已经设置好了开发环境，并准备开始编写 C、C++ 和 JavaScript，是时候添加拼图的最后一块了。为了从我们的 C/C++ 代码中生成`.wasm`文件，我们需要安装和配置**Emscripten SDK**(**EMSDK**)。\n\n在本章中，我们将讨论开发工作流，并讨论 EMSDK 如何适应开发过程。将提供如何在每个平台上安装和配置 EMSDK 的详细说明，以及任何先决条件。一旦安装和配置过程完成，您将通过编写和编译一些 C 代码来测试它。\n\n本章的目标是理解以下内容:\n\n*   使用网络组件时的整体开发工作流程\n*   EMSDK 如何与 Emscripten 和 WebAssembly 相关联，以及为什么需要它\n*   如何安装 EMSDK 的先决条件\n*   如何安装和配置 EMSDK\n*   如何测试 EMSDK 以确保其正常工作\n\n# 开发工作流程\n\nWebAssembly 的开发工作流程与大多数其他需要编译和构建过程的语言类似。在进入工具设置之前，我们将介绍开发周期。在本节中，我们将为本章剩余部分中安装和配置的工具建立一些上下文。\n\n# 工作流程中的步骤\n\n对于这本书，我们将编写 C 和 C++ 代码，并将其编译成一个 Wasm 模块，但是工作流将适用于任何编译成`.wasm`文件的编程语言。下图概述了该过程:\n\n![](img/b43e1500-5c09-4f8d-a81c-665fad256758.png)\n\nSteps in the development workflow\n\n对于我们的例子，这一过程将在整本书中使用，因此您将了解项目结构如何对应于工作流。我们将使用一些可用的工具来加速和简化这个过程，但是步骤仍然是相同的。\n\n# 将工具集成到工作流中\n\n有许多编辑器和工具可以用来简化开发过程。幸运的是，C/C++ 和 JavaScript 已经存在了相当长的时间，因此您可以利用最适合您的选项。考虑到该技术存在的时间较短，WebAssembly 工具的列表要短得多，但它们确实存在。\n\n我们将使用的主要工具 VS Code 为简化构建和开发过程提供了一些优秀且有用的特性。除了用它来编写我们的代码，我们还将利用 VS Code 的内置 Tasks 特性，从 C/C++ 中构建`.wasm`文件。通过在项目根文件夹中创建一个`.vscode/tasks.json`文件，我们能够指定与构建步骤相关的所有参数，并使用键盘快捷键快速运行它。除了执行构建之外，我们还可以启动和停止正在运行的 Node.js 进程(即工作流图中的本地服务器)。我们将在下一章介绍如何添加和配置这些功能。\n\n# Emscripten 和 EMSDK\n\n我们将使用 Emscripten 将我们的 C/C++ 代码编译成`.wasm`文件。到目前为止，Emscripten 只是在一般的上下文中被简单地提到过。因为我们将在构建过程中使用这个工具和相应的 Emscripten SDK (EMSDK)，所以了解每种技术是什么以及它在开发工作流中扮演的角色非常重要。在本节中，我们将描述 Emscripten 的目的，并讨论它与 EMSDK 的关系。\n\n# 描述概述\n\n那么什么是 Emscripten 呢？维基百科提供了以下定义:\n\n\"Emscripten is a source-to-source compiler that runs as a back end to the LLVM compiler and produces a subset of JavaScript known as asm.js. It can also produce WebAssembly.\"\n\n我们在第一章中讨论了源到源编译器(或 transpilers)，并以 TypeScript 为例。Transpilers 将一种编程语言的源代码转换为另一种编程语言的等效源代码。为了详细说明 Emscripten 作为 LLVM 编译器的后端运行，我们需要提供一些关于 LLVM 的附加细节。\n\nLLVM 的官方网站([https://llvm.org](https://llvm.org))将 LLVM 定义为*模块化和可重用的编译器和工具链技术的集合*。有几个子项目组成了 LLVM，但是我们将重点关注 Emscripten 使用的两个:Clang 和 LLVM 核心库。为了理解这些部分是如何结合在一起的，让我们回顾一下三阶段编译器的设计:\n\n![](img/fd686aef-f7e8-4aa4-a782-25c68ec02e2a.png)\n\nDesign of a general three-stage compiler\n\n过程相对简单:三个单独的阶段或*结束*处理编译过程。这种设计允许不同编程语言和目标架构的不同前端和后端，并通过使用中间表示将机器代码与源代码完全分离。现在，让我们将每个编译阶段与工具链的一个组件相关联，我们将使用它来生成 WebAssembly:\n\n![](img/3a332b22-6a36-4623-b326-dde857731fd7.png)\n\nThree-stage compilation using the LLVM, Clang, and Emscripten\n\nClang 用于将 C/C++ 编译成 LLVM 的**中间表示** ( **IR** ，Emscripten 将其编译成 Wasm 模块(二进制格式)。这两张图也展示了 Wasm 和机器代码之间的关系。你可以把 WebAssembly 想象成浏览器中的一个 CPU，Wasm 是它运行的机器代码。\n\n# EMSDK 适合哪里？\n\nEmscripten 指的是用于将 C 和 C++ 向下编译到`asm.js`或 WebAssembly 的工具链。EMSDK 用于管理工具链中的工具和相应的配置。这消除了对复杂环境设置的需要，并防止了工具版本不兼容的问题。通过安装 EMSDK，我们拥有了使用 Emscripten 编译器所需的所有工具(前提条件除外)。下图是 Emscripten 工具链的可视化表示(EMSDK 以深灰色显示):\n\n![](img/c88f7e5e-eefe-454f-a218-816b9caebac5.png)\n\nEmscripten Toolchain (modified slightly from emscripten.org)\n\n现在，您对 Emscripten 和 EMSDK 有了更好的理解，让我们继续进行先决条件的安装过程。\n\n# 安装先决条件\n\n在安装和配置 EMSDK 之前，我们需要安装一些先决条件。您在[第 3 章](03.html)、*设置开发环境*中安装了两个先决条件:Node.js 和 Git。每个平台的安装流程和工具要求略有不同。在本节中，我们将介绍每个平台必备工具的安装过程。\n\n# 常见先决条件\n\n可能您已经安装了所有的先决条件。无论平台如何，您都需要以下三种:\n\n*   饭桶\n*   Node.js\n*   Python 2.7\n\n注意 Python 版本；这很重要，因为安装错误的版本可能会导致安装过程失败。如果你在[第 2 章](02.html)、*中学习了 WebAssembly 的元素——Wat、Wasm 和 JavaScript API* ，并安装了 Node.js 和 Git，剩下的就是安装 Python 2.7 和为你的平台指定的任何附加先决条件。每个平台的 Python 安装过程将在以下小节中详细说明。\n\nPython is a high-level programming language used for general-purpose programming. If you'd like to learn more, check out the official website at [https://www.python.org/](https://www.python.org/).\n\n# 在 macOS 上安装先决条件\n\n在安装 EMSDK 之前，您还需要安装另外三个工具:\n\n*   x mode(x mode)-x mode(x mode)-x mode(x mode)(x mode)(x mode)(x mode)(x mode)(x mode)(x mode)(x mode)\n*   Xcode 命令行工具\n*   CMake\n\n您可以从 macOS 应用商店安装 Xcode。如果您已经安装了 Xcode，您可以通过转到 Xcode |首选项|位置并检查命令行工具选项是否有值来检查命令行工具是否已安装。如果您安装了自制软件包管理器，命令行工具应该已经安装:\n\n![](img/1fec0953-9278-4312-b286-c5ed34ec45e3.png)\n\nChecking the current version of the Xcode Command Line Tools\n\n如果您没有看到，请打开终端并运行以下命令:\n\n```cpp\nxcode-select --install\n```\n\n完成后，您可以通过运行以下命令来安装 CMake:\n\n```cpp\nbrew install cmake\n```\n\n安装 Python 之前，请运行以下命令:\n\n```cpp\npython --version\n```\n\n如果看到`Python 2.7.xx`(其中`xx`是补丁版本，可以是任意数字)，就可以安装 EMSDK 了。如果你得到一个说找不到 Python 命令的错误或者你看到`Python 3.x.xx`，我建议你安装`pyenv`，一个 Python 版本管理器。要安装`pyenv`，运行以下命令:\n\n```cpp\nbrew install pyenv\n```\n\n您需要采取一些额外的配置步骤来完成安装。遵循[https://github.com/pyenv/pyenv#homebrew-on-mac-os-x](https://github.com/pyenv/pyenv#homebrew-on-mac-os-x)的家酿安装说明。安装配置`pyenv`后，运行此命令安装 Python 2.7:\n\n```cpp\npyenv install 2.7.15\n```\n\n安装完成后，运行以下命令:\n\n```cpp\npyenv global 2.7.15\n```\n\n要确保您使用的是正确版本的 Python，请运行以下命令:\n\n```cpp\npython --version\n```\n\n你应该看到 Python `2.7.xx`，其中`xx`是补丁版本(我看到的是`2.7.10`，会很好用)。\n\n# 在 Ubuntu 上安装先决条件\n\nUbuntu 应该已经安装了 Python 2.7。您可以通过运行以下命令来确认:\n\n```cpp\npython --version\n```\n\n如果看到 Python `2.7.xx`(其中`xx`是补丁版本，可以是任意数字)，就可以安装 EMSDK 了。如果你得到一个说找不到 python 命令的错误或者你看到`Python 3.x.xx`，我建议你安装`pyenv`，一个 Python 版本管理器。在安装`pyenv`之前，请检查您是否安装了`curl`。您可以通过运行以下命令来实现这一点:\n\n```cpp\ncurl --version\n```\n\n如果看到版本号等信息，则安装`curl`。如果没有，可以通过运行以下命令来安装`curl`:\n\n```cpp\nsudo apt-get install curl\n```\n\n一旦`curl`安装完成，运行该命令安装`pyenv`:\n\n```cpp\ncurl -L https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash\n```\n\n安装并配置 pyenv 后，运行以下命令安装 Python 2.7:\n\n```cpp\npyenv install 2.7.15\n```\n\n如果您遇到构建问题，请导航至位于[https://github.com/pyenv/pyenv/wiki/common-build-problems](https://github.com/pyenv/pyenv/wiki/common-build-problems)的*常见构建问题*页面。安装完成后，运行以下命令:\n\n```cpp\npyenv global 2.7.15\n```\n\n要确保您使用的是正确版本的 Python，请运行以下命令:\n\n```cpp\npython --version\n```\n\n你应该看到`Python 2.7.xx`，其中`xx`是补丁版本(我看到的是`2.7.10`，会很好用)。\n\n# 在 Windows 上安装先决条件\n\nWindows 的唯一附加先决条件是 Python 2.7。在尝试安装之前，请运行以下命令:\n\n```cpp\npython --version\n```\n\n如果看到`Python 2.7.xx`(其中`xx`是补丁版本，可以是任意数字)，就可以安装 EMSDK 了。如果您收到一个错误，说找不到 Python 命令，或者您看到`Python 3.x.xx`并且您的系统上没有安装 Python 2.7，请运行以下命令来安装 Python 2.7:\n\n```cpp\nchoco install python2 -y\n```\n\n如果在安装 Python 2.7 之前看到`Python 3.x.xx`，应该可以通过更新路径来改变当前的 Python 版本。在尝试安装 EMSDK 之前，运行以下命令将 Python 设置为 2.7:\n\n```cpp\nSET PATH=C:\\Python27\\python.exe\n```\n\n# 安装和配置 EMSDK\n\n如果您已经安装了所有的先决条件，就可以安装 EMSDK 了。启动和运行 EMSDK 的过程相对简单。在本节中，我们将介绍 EMSDK 的安装过程，并演示如何更新 VS Code C/C++ 配置以适应 Emscripten。\n\n# 所有平台的安装过程\n\n首先，选择一个文件夹来安装 EMSDK。我在`~/Tooling`(或 Windows 上的`C:\\Users\\Mike\\Tooling`)创建了一个文件夹。在终端中，`cd`进入您刚刚创建的文件夹并运行以下命令:\n\n```cpp\ngit clone https://github.com/juj/emsdk.git\n```\n\n克隆过程完成后，请按照下面对应于您的平台的部分中的说明完成安装。\n\n# 在 macOS 和 Ubuntu 上安装\n\n克隆过程完成后，运行以下代码片段中的每个命令。如果您看到建议您运行`git pull`而不是`./emsdk update`的消息，请在运行`./emsdk install latest`命令之前使用`git pull`命令:\n\n```cpp\n# Change directory into the EMSDK installation folder\ncd emsdk\n\n# Fetch the latest registry of available tools\n./emsdk update\n\n# Download and install the latest SDK tools\n./emsdk install latest\n\n# Make the latest SDK active for the current user (writes ~/.emscripten file)\n./emsdk activate latest\n\n# Activate PATH and other environment variables in the current Terminal\nsource ./emsdk_env.sh\n```\n\n`source ./emsdk_env.sh`命令将激活当前终端中的环境变量，这意味着每次创建新的终端实例时，都必须重新运行它。为了避免必须采取这一步骤，您可以在 Bash 或 Zsh 配置文件(即`~/.bash_profile`或`~/.zshrc`)中添加以下行:\n\n```cpp\nsource ~/Tooling/emsdk/emsdk_env.sh > /dev/null\n```\n\n如果您在不同的位置安装了 EMSDK，请确保更新路径以反映这一点。将这一行添加到您的配置文件中会自动运行该环境更新命令，因此您可以立即开始使用 EMSDK。要确保可以使用 Emscripten 编译器，请运行以下命令:\n\n```cpp\nemcc --version\n```\n\n如果看到包含版本信息的消息，说明安装成功。如果您看到一条错误消息，指出找不到该命令，请仔细检查您的配置。您可能在 Bash 或 Zsh 配置文件中为`emsdk_env.sh`指定了无效路径。\n\n# Windows 上的安装和配置\n\n在完成安装之前，我建议您继续使用 **PowerShell** 。本书中的例子将在`cmder`中使用 PowerShell。克隆过程完成后，运行下面代码片段中给出的每个命令。如果您看到建议您运行`git pull`而不是`./emsdk update`的消息，请在运行`./emsdk install latest`命令之前使用`git pull`命令:\n\n```cpp\n# Change directory into the EMSDK installation folder\ncd emsdk\n\n# Fetch the latest registry of available tools\n.\\emsdk update\n\n# Download and install the latest SDK tools\n.\\emsdk install latest\n\n# Make the latest SDK active for the current user (writes ~/.emscripten file)\n.\\emsdk activate --global latest\n```\n\n`.\\emsdk activate`命令中的`--global`标志允许您运行`emcc`，而无需运行脚本来设置每个会话的环境变量。要确保您可以使用 Emscripten 编译器，请重新启动命令行界面并运行以下命令:\n\n```cpp\nemcc --version\n```\n\n如果看到包含版本信息的消息，说明安装成功。\n\n# VS 代码中的配置\n\n如果您还没有这样做，请创建一个包含我们将要处理的代码示例的文件夹(示例使用名称`book-examples`)。在 VS Code 中打开这个文件夹，按下 *F1* 键，选择 C/Cpp:编辑配置…在你的项目根目录下创建一个`.vscode/c_cpp_properties.json`文件。它应该会自动打开文件。在`browse.path`阵中加入以下一行:`\"${env:EMSCRIPTEN}/system/include\"`。这将防止错误被抛出，如果你包括`emscripten.h`标题。如果没有自动生成`path`条目，您可能需要手动创建带有该条目的`browse`对象。以下代码片段表示 Ubuntu 上更新的配置文件:\n\n```cpp\n{\n  \"name\": \"Linux\",\n  \"includePath\": [\n    \"/usr/include\",\n    \"/usr/local/include\",\n    \"${workspaceFolder}\",\n    \"${env:EMSCRIPTEN}/system/include\"\n  ],\n  \"defines\": [],\n  \"intelliSenseMode\": \"clang-x64\",\n  \"browse\": {\n    \"path\": [\n      \"/usr/include\",\n      \"/usr/local/include\",\n      \"${workspaceFolder}\"\n      ],\n    \"limitSymbolsToIncludedHeaders\": true,\n    \"databaseFilename\": \"\"\n  }\n}\n```\n\n# 测试编译器\n\n安装和配置 EMSDK 后，您需要测试它，以确保您能够从 C/C++ 代码生成 Wasm 模块。测试它最简单的方法是使用`emcc`命令编译一些代码，并尝试在浏览器中运行。在本节中，我们将通过编写和编译一些简单的 C 代码以及评估与`.wasm`输出相关联的 Wat 来验证 EMSDK 安装。\n\n# C 代码\n\n我们将使用一些非常简单的 C 代码来测试我们的编译器安装。我们不需要导入任何头文件或外部库。我们不会使用 C++ 进行这个测试，因为我们需要用 C++ 执行一个额外的步骤来防止名称篡改，我们将在[第 6 章](06.html)、*与 JavaScript 交互和调试*中更详细地描述。该部分的代码位于`learn-webassembly`存储库的`/chapter-04-installing-deps`文件夹中。按照此处列出的说明测试 EMSDK。\n\n在`/book-examples`文件夹中创建一个名为`/chapter-04-installing-deps`的子文件夹。接下来，在这个名为`main.c`的文件夹中创建一个新文件，并用以下内容填充它:\n\n```cpp\nint addTwoNumbers(int leftValue, int rightValue) {\n    return leftValue + rightValue;\n}\n```\n\n# 编译 C 代码\n\n为了用 Emscripten 编译一个 C/C++ 文件，我们将使用`emcc`命令。我们需要向编译器传递一些参数，以确保我们得到一个可以在浏览器中使用的有效输出。要从 C/C++ 文件生成 Wasm 文件，命令遵循以下格式:\n\n```cpp\nemcc <file.c> -Os -s WASM=1 -s SIDE_MODULE=1 -s BINARYEN_ASYNC_COMPILATION=0 -o <file.wasm>\n```\n\n以下是`emcc`命令的每个参数的分解:\n\n| **参数** | **描述** |\n| `<file.c>` | 将被编译成 Wasm 模块的 C 或 C++ 输入文件的路径；我们将在运行命令时用实际的文件路径替换它。 |\n| `-Os` | 编译器优化级别。这个优化标志允许模块实例化，而不需要 Emscripten 的粘合代码。 |\n| `-s WASM=1` | 告诉编译器将代码编译到网络程序集。 |\n| `-s SIDE_MODULE=1` | 确保仅输出一个`WebAssembly`模块(无粘合代码)。 |\n| `-s BINARYEN_ASYNC_COMPILATION=0` | 来自官方文件:Whether to compile the wasm asynchronously, which is more efficient and does not block the main thread. This is currently required for all but the smallest modules to run in V8*.* |\n| `-o <file.wasm>` | 输出文件`.wasm`文件路径。当我们运行该命令时，我们将用所需的输出路径替换它。 |\n\n要测试 Emscripten 是否正常工作，请在 VS Code 中打开集成终端并运行以下命令:\n\n```cpp\n# Ensure you're in the /chapter-04-installing-deps folder:\ncd chapter-04-installing-deps\n\n# Compile the main.c file to main.wasm:\nemcc main.c -Os -s WASM=1 -s SIDE_MODULE=1 -s BINARYEN_ASYNC_COMPILATION=0 -o main.wasm\n```\n\n第一次编译文件可能需要一分钟，但是后续的构建会快得多。如果编译成功，您应该会在`/chapter-04-installing-deps`文件夹中看到一个`main.wasm`文件。如果您遇到错误，Emscripten 的错误消息应该具有足够的描述性，以帮助您纠正问题。\n\n如果一切都成功完成，您可以通过在 VS Code 的文件浏览器中右键单击`main.wasm`并从上下文菜单中选择“显示 WebAssembly”来查看与`main.wasm`文件相关联的 Wat。输出应该如下所示:\n\n```cpp\n(module\n  (type $t0 (func (param i32)))\n  (type $t1 (func (param i32 i32) (result i32)))\n  (type $t2 (func))\n  (type $t3 (func (result f64)))\n  (import \"env\" \"table\" (table $env.table 2 anyfunc))\n  (import \"env\" \"memoryBase\" (global $env.memoryBase i32))\n  (import \"env\" \"tableBase\" (global $env.tableBase i32))\n  (import \"env\" \"abort\" (func $env.abort (type $t0)))\n  (func $_addTwoNumbers (type $t1) (param $p0 i32) (param $p1 i32) (result i32)\n    get_local $p1\n    get_local $p0\n    i32.add)\n  (func $runPostSets (type $t2)\n    nop)\n  (func $__post_instantiate (type $t2)\n     get_global $env.memoryBase\n    set_global $g2\n    get_global $g2\n    i32.const 5242880\n    i32.add\n    set_global $g3)\n  (func $f4 (type $t3) (result f64)\n    i32.const 0\n    call $env.abort\n    f64.const 0x0p+0 (;=0;))\n  (global $g2 (mut i32) (i32.const 0))\n  (global $g3 (mut i32) (i32.const 0))\n  (global $fp$_addTwoNumbers i32 (i32.const 1))\n  (export \"__post_instantiate\" (func $__post_instantiate))\n  (export \"_addTwoNumbers\" (func $_addTwoNumbers))\n  (export \"runPostSets\" (func $runPostSets))\n  (export \"fp$_addTwoNumbers\" (global 4))\n  (elem (get_global $env.tableBase) $f4 $_addTwoNumbers))\n```\n\n如果编译器成功运行，您就可以进入下一步，编写 JavaScript 代码与模块进行交互，我们将在下一章介绍这一点。\n\n# 摘要\n\n在本章中，我们介绍了使用 WebAssembly 时的整体开发工作流程。为了生成我们的`.wasm`文件，我们使用了 Emscripten，这需要安装 EMSDK。在回顾任何安装细节之前，我们讨论了引擎盖下的技术，并描述了它们之间以及与 WebAssembly 之间的关系。我们介绍了让 EMDSK 在您的计算机上本地工作所需的每个步骤。介绍了 EMSDK 在每个平台上的安装过程，以及 EMSDK 的安装和配置说明。安装 EMSDK 后，我们测试了编译器(否到)。这是我们在前一节中运行的`emcc`命令。在一个简单的 C 代码文件上使用`emcc`命令来确保 Emscripten 工作正常。在下一章中，我们将介绍创建和加载第一个模块的过程！\n\n# 问题\n\n1.  开发工作流程的五个步骤是什么？\n2.  Emscripten 在编译过程中代表哪个阶段或结束？\n3.  IR 代表什么(LLVM 的输出)？\n4.  EMSDK 在 Emscripten 方面扮演什么角色？\n5.  所有三个平台(macOS、Windows 和 Linux)都需要哪些 EMSDK 先决条件？\n6.  为什么要先运行`emsdk_env`脚本才能使用 Emscripten 编译器？\n7.  为什么需要在 C/Cpp 配置文件中添加`\"${env:EMSCRIPTEN}/system/include\"`路径？\n8.  将 C/C++ 向下编译成 Wasm 模块的命令是什么？\n9.  `-Os`编译器标志代表什么？\n\n# 进一步阅读\n\n*   描述： [http://emscripten.org](http://emscripten.org)\n*   LLVM 编译器基础设施项目:[https://llvm.org](https://llvm.org)\n*   用 Visual Studio 代码进行 C++ 编程:[https://code.visualstudio.com/docs/languages/cpp](https://code.visualstudio.com/docs/languages/cpp)"
  },
  {
    "path": "docs/learn-wasm/05.md",
    "content": "# 五、创建和加载 WebAssembly 模块\n\n我们在[第 4 章](04.html)、*安装所需依赖项*中传递给`emcc`命令的标志产生了一个单独的`.wasm`文件，可以使用原生的`WebAssembly`对象在浏览器中加载和实例化。C 代码是一个非常简单的例子，旨在测试编译器，而不必适应包含的库或网络程序集的限制。通过利用 Emscripten 的一些功能，我们可以以最小的性能损失克服 C / C++ 代码中 WebAssembly 的一些限制。\n\n在本章中，我们将介绍与使用 Emscripten 的粘合代码相对应的编译和加载步骤。我们还将描述编译/输出严格的`.wasm`文件并使用浏览器的`WebAssembly`对象加载它们的过程。\n\n本章的目标是理解以下内容:\n\n*   C 代码的编译过程，利用了 Emscripten 的 JavaScript“胶水”代码\n*   如何在浏览器中加载 Emscripten 模块\n*   只输出`.wasm`文件的 C 代码的编译过程(没有“胶水”代码)\n*   如何在 VS 代码中配置构建任务\n*   如何使用全局`WebAssembly`对象在浏览器中编译加载一个 Wasm 模块\n\n# 用 Emscripten 胶水代码编译 C 语言\n\n在[第 4 章](04.html)、*安装所需的依赖项*中，您编写并编译了一个简单的三线程序，以确保您的 Emscripten 安装是有效的。我们向`emcc`命令传递了几个只需要输出一个`.wasm`文件的标志。通过向`emcc`命令传递其他标志，我们可以在`.wasm`文件和一个 HTML 文件旁边输出 JavaScript 粘合代码来处理加载过程。在本节中，我们将编写一个更复杂的 C 程序，并用 Emscripten 提供的输出选项编译它。\n\n# 编写示例 C 代码\n\n在我们在[第 4 章](04.html)、*安装所需依赖项*中介绍的示例中，我们没有包含任何头文件或传递任何函数。因为代码的目的仅仅是测试编译器安装是否有效，所以没有太多的必要。Emscripten 提供了许多额外的功能，使我们能够用 JavaScript 与 C 和 C++ 代码交互，反之亦然。其中一些功能是特定于环境的，不符合*核心规范*或其应用编程接口。在第一个例子中，我们将利用 Emscripten 的一个移植库和 Emscripten 的 API 提供的一个函数。\n\n下面的程序使用一个**简单直接媒体层** ( **SDL2** )在画布上无限循环地对角移动一个矩形。取自[https://github.com/timhutton/sdl-canvas-wasm](https://github.com/timhutton/sdl-canvas-wasm)，不过我把它从 C++ 转换成了 C，对代码稍作修改。该部分的代码位于`learn-webassembly`存储库的`/chapter-05-create-load-module`文件夹中。按照以下说明用 Emscripten 编译 C。\n\n在你的`/book-examples`文件夹中创建一个名为`/chapter-05-create-load-module`的文件夹。在这个名为`with-glue.c`的文件夹中创建一个新文件，并用以下内容填充它:\n\n```cpp\n/*\n * Converted to C code taken from:\n * https://github.com/timhutton/sdl-canvas-wasm\n * Some of the variable names and comments were also\n * slightly updated.\n */\n#include <SDL2/SDL.h>\n#include <emscripten.h>\n#include <stdlib.h>\n\n// This enables us to have a single point of reference\n// for the current iteration and renderer, rather than\n// have to refer to them separately.\ntypedef struct Context {\n  SDL_Renderer *renderer;\n  int iteration;\n} Context;\n\n/*\n * Looping function that draws a blue square on a red\n * background and moves it across the <canvas>.\n */\nvoid mainloop(void *arg) {\n    Context *ctx = (Context *)arg;\n    SDL_Renderer *renderer = ctx->renderer;\n    int iteration = ctx->iteration;\n\n    // This sets the background color to red:\n    SDL_SetRenderDrawColor(renderer, 255, 0, 0, 255);\n    SDL_RenderClear(renderer);\n\n    // This creates the moving blue square, the rect.x\n    // and rect.y values update with each iteration to move\n    // 1px at a time, so the square will move down and\n    // to the right infinitely:\n    SDL_Rect rect;\n    rect.x = iteration;\n    rect.y = iteration;\n    rect.w = 50;\n    rect.h = 50;\n    SDL_SetRenderDrawColor(renderer, 0, 0, 255, 255);\n    SDL_RenderFillRect(renderer, &rect);\n\n    SDL_RenderPresent(renderer);\n\n    // This resets the counter to 0 as soon as the iteration\n    // hits the maximum canvas dimension (otherwise you'd\n    // never see the blue square after it travelled across\n    // the canvas once).\n    if (iteration == 255) {\n        ctx->iteration = 0;\n    } else {\n        ctx->iteration++ ;\n    }\n}\n\nint main() {\n    SDL_Init(SDL_INIT_VIDEO);\n    SDL_Window *window;\n    SDL_Renderer *renderer;\n\n    // The first two 255 values represent the size of the <canvas>\n    // element in pixels.\n    SDL_CreateWindowAndRenderer(255, 255, 0, &window, &renderer);\n\n    Context ctx;\n    ctx.renderer = renderer;\n    ctx.iteration = 0;\n\n    // Call the function repeatedly:\n    int infinite_loop = 1;\n\n    // Call the function as fast as the browser wants to render\n    // (typically 60fps):\n    int fps = -1;\n\n    // This is a function from emscripten.h, it sets a C function\n    // as the main event loop for the calling thread:\n    emscripten_set_main_loop_arg(mainloop, &ctx, fps, infinite_loop);\n\n    SDL_DestroyRenderer(renderer);\n    SDL_DestroyWindow(window);\n    SDL_Quit();\n\n    return EXIT_SUCCESS;\n}\n```\n\n`main()`功能末尾的`emscripten_set_main_loop_arg()`是可用的，因为我们在文件顶部包含了`emscripten.h`。由于文件顶部的`#include <SDL2/SDL.h>`，前缀为`SDL_`的变量和函数是可用的。如果您在`<SDL2/SDL.h>`语句下看到一条弯弯曲曲的红色错误线，您可以忽略它。这是因为 SDL 的`include`路径不在你的`c_cpp_properties.json`文件中。\n\n# 编译示例 C 代码\n\n既然我们已经写好了 C 代码，我们就需要编译它。您必须传递给`emcc`命令的一个必需标志是`-o <target>`，其中`<target>`是所需输出文件的路径。该文件的扩展名不仅仅是输出该文件；它会影响编译器做出的一些决定。下表摘自位于[的 Emscripten 的`emcc`文档:](http://kripken.github.io/emscripten-site/docs/tools_reference/emcc.html#emcc-o-target)\n\n| **延伸** | **输出** |\n| `<name>.js` | JavaScript 粘合代码(如果指定了`s WASM=1`标志，则为`.wasm`)。 |\n| `<name>.html` | HTML 和单独的 JavaScript 文件(`<name>.js`)。拥有独立的 JavaScript 文件可以缩短页面加载时间。 |\n| `<name>.bc` | LLVM 位码（默认）。 |\n| `<name>.o` | LLVM 位代码(与`.bc`相同)。 |\n| `<name>.wasm` | 仅 Wasm 文件(带有从[第 4 章](04.html)、*指定的标志，用于安装所需的依赖项*)。 |\n\n您可以忽略`.bc`和`.o`文件扩展名——我们不需要输出 LLVM 位代码。`.wasm`扩展不在`emcc` *工具参考*页面上，但是如果您传递了正确的编译器标志，它是一个有效的选项。这些输出选项会影响我们编写的 C/C++ 代码。\n\n# 输出带有粘合代码的 HTML\n\n如果您为输出指定了一个 HTML 文件扩展名(例如，`-o with-glue.html`)，您将得到一个`with-glue.html`、`with-glue.js`和`with-glue.wasm`文件(假设您也指定了`-s WASM=1`)。如果你在 C/C++ 源文件中有一个`main()`函数，它会在 HTML 加载后立即执行该函数。让我们编译我们的示例 C 代码，看看这是如何操作的。要用 HTML 文件和 JavaScript 粘合代码编译它，请将`cd`放入`/chapter-05-create-load-module`文件夹并运行以下命令:\n\n```cpp\nemcc with-glue.c -O3 -s WASM=1 -s USE_SDL=2 -o with-glue.html\n```\n\n第一次运行这个命令时，Emscripten 将下载并构建`SDL2`库。完成此操作可能需要几分钟，但您只需等待一次。Emscripten 缓存了这个库，因此后续的构建会更快。构建完成后，您将在文件夹中看到三个新文件:`HTML`、`JavaScript`和`Wasm`文件。运行以下命令将文件本地`serve`:\n\n```cpp\nserve -l 8080\n```\n\n如果您打开浏览器直到`http://127.0.0.1:8080/with-glue.html`，您应该会看到以下内容:\n\n![](img/bb584a1f-c5eb-415f-b86a-5904f11404a5.png)\n\nEmscripten loading code running in the browser\n\n蓝色矩形应该从红色矩形的左上角对角移动到右下角。由于您在 C 文件中指定了一个`main()`函数，Emscripten 知道它应该立即执行它。如果在 VS 代码中打开`with-glue.html`文件，滚动到文件底部，会看到加载代码。你不会看到任何对`WebAssembly`对象的引用；这是在 JavaScript 粘合代码文件中处理的。\n\n# 输出不带 HTML 的粘合代码\n\nEmscripten 在 HTML 文件中生成的加载代码包含错误处理和其他有帮助的功能，以确保模块在执行`main()`功能之前被加载。如果您为输出文件的扩展名指定`.js`，您将不得不创建一个 HTML 文件并自己编写加载代码。在下一节中，我们将更详细地研究加载代码。\n\n# 正在加载电子脚本模块\n\n加载一个利用 Emscripten 的粘合代码的模块并与之交互与 WebAssembly 的 JavaScript API 有很大的不同。这是因为 Emscripten 提供了与 JavaScript 代码交互的附加功能。在本节中，我们将讨论在输出一个 HTML 文件时 Emscripten 提供的加载代码，并回顾在浏览器中加载 Emscripten 模块的过程。\n\n# 预先生成的加载代码\n\n如果在运行`emcc`命令时指定`-o <target>.html`，Emscripten 会生成一个 HTML 文件，并自动添加代码将模块加载到文件末尾。以下是不包括每个`Module`函数内容的 HTML 文件中的加载代码:\n\n```cpp\nvar statusElement = document.getElementById('status');\nvar progressElement = document.getElementById('progress');\nvar spinnerElement = document.getElementById('spinner');\n\nvar Module = {\n  preRun: [],\n  postRun: [],\n  print: (function() {...})(),\n  printErr: function(text) {...},\n  canvas: (function() {...})(),\n  setStatus: function(text) {...},\n  totalDependencies: 0,\n  monitorRunDependencies: function(left) {...}\n};\n\nModule.setStatus('Downloading...');\n\nwindow.onerror = function(event) {\n  Module.setStatus('Exception thrown, see JavaScript console');\n  spinnerElement.style.display = 'none';\n  Module.setStatus = function(text) {\n    if (text) Module.printErr('[post-exception status] ' + text);\n  };\n};\n```\n\n`Module`对象中的功能用于检测和解决错误，监控`Module`的加载状态，并可选地在相应的粘合代码文件中的`run()`方法执行之前或之后执行一些功能。下面代码片段中显示的`canvas`函数从加载代码之前在 HTML 文件中指定的 DOM 中返回`<canvas>`元素:\n\n```cpp\ncanvas: (function() {\n  var canvas = document.getElementById('canvas');\n  canvas.addEventListener(\n    'webglcontextlost',\n    function(e) {\n      alert('WebGL context lost. You will need to reload the page.');\n      e.preventDefault();\n    },\n    false\n  );\n\n  return canvas;\n})(),\n```\n\n这段代码便于检测错误并确保`Module`被加载，但出于我们的目的，我们不需要那么啰嗦。\n\n# 编写自定义加载代码\n\nEmscripten 生成的加载代码提供了有用的错误处理。如果您在生产中使用 Emscripten 的输出，我建议您包含它，以确保您正确处理错误。然而，我们实际上并不需要所有的代码来利用我们的`Module`。让我们编写一些简单得多的代码并测试一下。首先，让我们将 C 文件编译成没有 HTML 输出的粘合代码。为此，请运行以下命令:\n\n```cpp\nemcc with-glue.c -O3 -s WASM=1 -s USE_SDL=2 -s MODULARIZE=1 -o custom-loading.js\n```\n\n`-s MODULARIZE=1`编译器标志允许我们使用类似 Promise 的 API 来加载我们的`Module`。编译完成后，在名为`custom-loading.html`的`/chapter-05-create-load-module`文件夹中创建一个文件，并用以下内容填充:\n\n```cpp\n<!doctype html>\n<html lang=\"en-us\">\n<head>\n  <title>Custom Loading Code</title>\n</head>\n<body>\n  <h1>Using Custom Loading Code</h1>\n  <canvas id=\"canvas\"></canvas>\n  <script type=\"application/javascript\" src=\"custom-loading.js\"></script>\n  <script type=\"application/javascript\">\n    Module({\n      canvas: (() => document.getElementById('canvas'))(),\n    })\n      .then(() => {\n        console.log('Loaded!');\n      });\n  </script>\n</body>\n</html>\n```\n\n加载代码现在使用 ES6 的箭头函数语法作为画布加载函数，这减少了所需的代码行。通过在`/chapter-05-create-load-module`文件夹中运行`serve`命令启动本地服务器:\n\n```cpp\nserve -l 8080\n```\n\n当您在浏览器中导航到`http://127.0.0.1:8080/custom-loading.html`时，您应该会看到以下内容:\n\n![](img/5089d0d0-ffc7-4874-8cdf-9e307b5c3a1a.png)\n\nCustom loading code running in the browser\n\n当然，我们运行的函数并不是很复杂，但是它展示了加载 Emscripten 的`Module`的基本需求。我们将在[第 6 章](06.html)、*与 JavaScript 交互和调试*中更详细地检查`Module`对象，但是现在请注意，加载过程不同于我们将在下一节中介绍的 WebAssembly。\n\n# 不用粘合代码编译 C 语言\n\n如果我们想根据官方规范使用 WebAssembly，而没有 Emscripten 提供的额外功能，我们需要向`emcc`命令传递一些标志，并确保我们编写的代码可以相对容易地被 WebAssembly 使用。在*编写示例 C 代码*部分，我们编写了一个程序，绘制了一个在红色画布上对角移动的蓝色矩形。它利用了 Emscripten 的一个移植库，SDL2。在本节中，我们将编写和编译一些不依赖于 Emscripten 的助手方法和移植库的 C 代码。\n\n# WebAssembly 的代码\n\n在我们进入我们将用于我们的 WebAssembly 模块的 C 代码之前，让我们尝试一个实验。打开`/chapter-05-create-load-module`文件夹中的命令行界面，尝试运行以下命令:\n\n```cpp\nemcc with-glue.c -Os -s WASM=1 -s USE_SDL=2 -s SIDE_MODULE=1 -s BINARYEN_ASYNC_COMPILATION=0 -o try-with-glue.wasm\n```\n\n编译完成后，您应该会在 VS Code 的文件浏览器面板中看到一个`try-with-glue.wasm`文件。右键单击该文件，然后选择“显示网络程序集”。对应的 Wat 表示的开头应该类似于下面的代码:\n\n```cpp\n(module\n  (type $t0 (func (param i32)))\n  (type $t1 (func (param i32 i32 i32 i32 i32) (result i32)))\n  (type $t2 (func (param i32) (result i32)))\n  (type $t3 (func))\n  (type $t4 (func (param i32 i32) (result i32)))\n  (type $t5 (func (param i32 i32 i32 i32)))\n  (type $t6 (func (result i32)))\n  (type $t7 (func (result f64)))\n  (import \"env\" \"memory\" (memory $env.memory 256))\n  (import \"env\" \"table\" (table $env.table 4 anyfunc))\n  (import \"env\" \"memoryBase\" (global $env.memoryBase i32))\n  (import \"env\" \"tableBase\" (global $env.tableBase i32))\n  (import \"env\" \"abort\" (func $env.abort (type $t0)))\n  (import \"env\" \"_SDL_CreateWindowAndRenderer\" (func $env._SDL_CreateWindowAndRenderer (type $t1)))\n  (import \"env\" \"_SDL_DestroyRenderer\" (func $env._SDL_DestroyRenderer (type $t0)))\n  (import \"env\" \"_SDL_DestroyWindow\" (func $env._SDL_DestroyWindow (type $t0)))\n  (import \"env\" \"_SDL_Init\" (func $env._SDL_Init (type $t2)))\n  (import \"env\" \"_SDL_Quit\" (func $env._SDL_Quit (type $t3)))\n  (import \"env\" \"_SDL_RenderClear\" (func $env._SDL_RenderClear (type $t2)))\n  (import \"env\" \"_SDL_RenderFillRect\" (func $env._SDL_RenderFillRect (type $t4)))\n  (import \"env\" \"_SDL_RenderPresent\" (func $env._SDL_RenderPresent (type $t0)))\n  (import \"env\" \"_SDL_SetRenderDrawColor\" (func $env._SDL_SetRenderDrawColor (type $t1)))\n  (import \"env\" \"_emscripten_set_main_loop_arg\" (func $env._emscripten_set_main_loop_arg (type $t5)))\n  ...\n```\n\n如果您想在浏览器中加载并执行它，您必须将一个`importObj`对象传递给网络组件的`instantiate()`或`compile()`函数，以及一个包含这些`import \"env\"`函数的`env`对象。Emscripten 用胶水代码在幕后为我们处理所有这些，这使它成为一个非常有价值的工具。然而，我们可以通过使用 DOM 来替换 SDL2 功能，同时仍然在 c 中跟踪矩形的位置。\n\n我们将以不同的方式编写 C 代码，以确保我们只需将几个函数传递到`importObj.env`对象中即可执行代码。在`/chapter-05-create-load-module`文件夹中创建一个名为`without-glue.c`的文件，并用以下内容填充:\n\n```cpp\n/*\n * This file interacts with the canvas through imported functions.\n * It moves a blue rectangle diagonally across the canvas\n * (mimics the SDL example).\n */\n#include <stdbool.h>\n\n#define BOUNDS 255\n#define RECT_SIDE 50\n#define BOUNCE_POINT (BOUNDS - RECT_SIDE)\n\n// These functions are passed in through the importObj.env object\n// and update the rectangle on the <canvas>:\nextern int jsClearRect();\nextern int jsFillRect(int x, int y, int width, int height);\n\nbool isRunning = true;\n\ntypedef struct Rect {\n  int x;\n  int y;\n  char direction;\n} Rect;\n\nstruct Rect rect;\n\n/*\n * Updates the rectangle location by 1px in the x and y in a\n * direction based on its current position.\n */\nvoid updateRectLocation() {\n    // Since we want the rectangle to \"bump\" into the edge of the\n    // canvas, we need to determine when the right edge of the\n    // rectangle encounters the bounds of the canvas, which is why\n    // we're using the canvas width - rectangle width:\n    if (rect.x == BOUNCE_POINT) rect.direction = 'L';\n\n    // As soon as the rectangle \"bumps\" into the left side of the\n    // canvas, it should change direction again.\n    if (rect.x == 0) rect.direction = 'R';\n\n    // If the direction has changed based on the x and y\n    // coordinates, ensure the x and y points update\n    // accordingly:\n    int incrementer = 1;\n    if (rect.direction == 'L') incrementer = -1;\n    rect.x = rect.x + incrementer;\n    rect.y = rect.y + incrementer;\n}\n\n/*\n * Clear the existing rectangle element from the canvas and draw a\n * new one in the updated location.\n */\nvoid moveRect() {\n    jsClearRect();\n    updateRectLocation();\n    jsFillRect(rect.x, rect.y, RECT_SIDE, RECT_SIDE);\n}\n\nbool getIsRunning() {\n    return isRunning;\n}\n\nvoid setIsRunning(bool newIsRunning) {\n    isRunning = newIsRunning;\n}\n\nvoid init() {\n    rect.x = 0;\n    rect.y = 0;\n    rect.direction = 'R';\n    setIsRunning(true);\n}\n```\n\n我们将从 C 代码中调用函数来确定 *x* 和 *y* 坐标。`setIsRunning()`功能可以用来暂停矩形的移动。现在我们的 C 代码已经准备好了，让我们编译它。在 VS 代码终端中，`cd`进入`/chapter-05-create-load-module`文件夹，运行以下命令:\n\n```cpp\nemcc without-glue.c -Os -s WASM=1 -s SIDE_MODULE=1 -s BINARYEN_ASYNC_COMPILATION=0 -o without-glue.wasm\n```\n\n编译完成后，您可以右键单击生成的`without-glue.wasm`文件，并选择“显示网络组件”来查看 Wat 表示。对于`import \"env\"`项，您应该在文件顶部看到以下内容:\n\n```cpp\n(module\n  (type $t0 (func (param i32)))\n  (type $t1 (func (result i32)))\n  (type $t2 (func (param i32 i32 i32 i32) (result i32)))\n  (type $t3 (func))\n  (type $t4 (func (result f64)))\n  (import \"env\" \"memory\" (memory $env.memory 256))\n  (import \"env\" \"table\" (table $env.table 8 anyfunc))\n  (import \"env\" \"memoryBase\" (global $env.memoryBase i32))\n  (import \"env\" \"tableBase\" (global $env.tableBase i32))\n  (import \"env\" \"abort\" (func $env.abort (type $t0)))\n  (import \"env\" \"_jsClearRect\" (func $env._jsClearRect (type $t1)))\n  (import \"env\" \"_jsFillRect\" (func $env._jsFillRect (type $t2)))\n  ...\n```\n\n我们需要在`importObj`对象中传递`_jsClearRect`和`_jsFillRect`函数。我们将在使用 JavaScript 交互代码的 HTML 文件部分介绍如何做到这一点。\n\n# 在 VS 代码中使用构建任务进行编译\n\n`emcc`命令有点冗长，对于不同的文件，必须在命令行上手动运行这个命令可能会很麻烦。为了加快编译过程，我们可以使用 VS Code 的 Tasks 特性为我们将要使用的文件创建一个构建任务。要创建构建任务，请选择任务|配置默认构建任务…，选择从模板创建任务选项，并选择其他以在`.vscode`文件夹中生成一个简单的`tasks.json`文件。更新文件内容以包含以下内容:\n\n```cpp\n{\n  // See https://go.microsoft.com/fwlink/?LinkId=733558\n  // for the documentation about the tasks.json format\n  \"version\": \"2.0.0\",\n  \"tasks\": [\n    {\n      \"label\": \"Build\",\n      \"type\": \"shell\",\n      \"command\": \"emcc\",\n      \"args\": [\n        \"${file}\",\n        \"-Os\",\n        \"-s\", \"WASM=1\",\n        \"-s\", \"SIDE_MODULE=1\",\n        \"-s\", \"BINARYEN_ASYNC_COMPILATION=0\",\n        \"-o\", \"${fileDirname}/${fileBasenameNoExtension}.wasm\"\n       ],\n      \"group\": {\n        \"kind\": \"build\",\n        \"isDefault\": true\n       },\n       \"presentation\": {\n         \"panel\": \"new\"\n       }\n     }\n  ]\n}\n```\n\n`label`值只是运行任务时引用的一个名称。`type`和`command`值表示它应该在 shell(终端)中运行`emcc`命令。`args`值是要传递给`emcc`命令的一组参数(基于空间分隔)。`\"${file}\"`参数告诉 VS Code 编译当前打开的文件。`\"${fileDirname}/${fileBasenameNoExtension}.wasm\"`参数表示`.wasm`输出将与当前打开的文件同名(扩展名为`.wasm`，应该放在当前打开文件的活动文件夹中。如果不指定`${fileDirname}`，输出文件将放在根文件夹中(而不是本例中的`/chapter-05-create-load-module`)。\n\n`group`对象表示该任务是默认的构建步骤，所以如果使用快捷键*Cmd*/*Ctrl*+*Shift*+*B*，这就是将要运行的任务。`\"new\"`的`presentation.panel`值告诉 VS 代码在构建步骤运行时打开一个新的命令行界面实例。这是个人喜好，可以省略。\n\n一旦文件被完全填充，您可以保存并关闭`tasks.json`文件。为了测试它，首先删除你在前面部分用`emcc`命令生成的`without-glue.wasm`文件。接下来，选择 **Tasks** | Run Build Task…或使用键盘快捷键*Cmd*/*Ctrl*+*Shift*+*B*，确保您已将光标置于文件中打开`without-glue.c`并运行构建任务。集成终端中的新面板将执行编译，一两秒钟后将出现一个`without-glue.wasm`文件。\n\n# 获取并实例化一个 Wasm 文件\n\n现在我们有了一个 Wasm 文件，我们需要一些 JavaScript 代码来编译和执行它。我们必须遵循几个步骤来确保代码可以在浏览器中成功使用。在本节中，我们将编写一些常见的 JavaScript 加载代码，我们可以在其他示例中重用这些代码，创建一个演示 Wasm 模块使用的 HTML 文件，并在浏览器中测试结果。\n\n# 常见的 JavaScript 加载代码\n\n我们将在几个示例中获取并实例化一个`.wasm`文件，因此将 JavaScript 加载代码移动到一个公共文件是有意义的。实际的获取和实例化代码只有几行，但是不得不重复重新定义 Emscripten 期望的`importObj`对象是浪费时间。我们将在一个通用的可访问文件中提供这些代码，以加快代码编写过程。在`/book-examples`文件夹中新建一个名为`/common`的文件夹，并添加一个名为`load-wasm.js`的文件，内容如下:\n\n```cpp\n/**\n * Returns a valid importObj.env object with default values to pass\n * into the WebAssembly.Instance constructor for Emscripten's\n * Wasm module.\n */\nconst getDefaultEnv = () => ({\n  memoryBase: 0,\n  tableBase: 0,\n  memory: new WebAssembly.Memory({ initial: 256 }),\n  table: new WebAssembly.Table({ initial: 2, element: 'anyfunc' }),\n  abort: console.log\n});\n\n/**\n * Returns a WebAssembly.Instance instance compiled from the specified\n * .wasm file.\n */\nfunction loadWasm(fileName, importObj = { env: {} }) {\n  // Override any default env values with the passed in importObj.env\n  // values:\n  const allEnv = Object.assign({}, getDefaultEnv(), importObj.env);\n\n  // Ensure the importObj object includes the valid env value:\n  const allImports = Object.assign({}, importObj, { env: allEnv });\n\n  // Return the result of instantiating the module (instance and module):\n  return fetch(fileName)\n    .then(response => {\n      if (response.ok) return response.arrayBuffer();\n      throw new Error(`Unable to fetch WebAssembly file ${fileName}`);\n    })\n    .then(bytes => WebAssembly.instantiate(bytes, allImports));\n}\n```\n\n`getDefaultEnv()`功能为 Emscripten 的 Wasm 模块提供所需的`importObj.env`内容。我们希望能够通过任何额外的进口，这就是为什么使用`Object.assign()`声明。随着 Wasm 模块预期的任何其他导入的增加，Emscripten 的 Wasm 输出将总是需要`\"env\"`对象的这五个导入语句:\n\n```cpp\n(import \"env\" \"memory\" (memory $env.memory 256))\n(import \"env\" \"table\" (table $env.table 8 anyfunc))\n(import \"env\" \"memoryBase\" (global $env.memoryBase i32))\n(import \"env\" \"tableBase\" (global $env.tableBase i32))\n(import \"env\" \"abort\" (func $env.abort (type $t0)))\n```\n\n我们需要将这些传递到`instantiate()`函数中，以确保 Wasm 模块加载成功，否则浏览器会抛出错误。现在我们已经准备好了加载代码，让我们继续看 HTML 和矩形渲染代码。\n\n# 网页\n\n我们需要一个带有`<canvas>`元素和 JavaScript 代码的 HTML 页面来与 Wasm 模块进行交互。在`/chapter-05-create-load-module`文件夹中创建一个名为`without-glue.html`的文件，并用以下内容填充:\n\n```cpp\n<!doctype html>\n<html lang=\"en-us\">\n<head>\n  <title>No Glue Code</title>\n  <script type=\"application/javascript\" src=\"../common/load-wasm.js\"></script>\n</head>\n<body>\n  <h1>No Glue Code</h1>\n  <canvas id=\"myCanvas\" width=\"255\" height=\"255\"></canvas>\n  <div style=\"margin-top: 16px;\">\n    <button id=\"actionButton\" style=\"width: 100px; height: 24px;\">\n      Pause\n    </button>\n  </div>\n  <script type=\"application/javascript\">\n    const canvas = document.querySelector('#myCanvas');\n    const ctx = canvas.getContext('2d');\n\n    const env = {\n      table: new WebAssembly.Table({ initial: 8, element: 'anyfunc' }),\n      _jsFillRect: function (x, y, w, h) {\n        ctx.fillStyle = '#0000ff';\n        ctx.fillRect(x, y, w, h);\n      },\n      _jsClearRect: function() {\n        ctx.fillStyle = '#ff0000';\n        ctx.fillRect(0, 0, 255, 255);\n      },\n    };\n\n    loadWasm('without-glue.wasm', { env }).then(({ instance }) => {\n      const m = instance.exports;\n      m._init();\n\n      // Move the rectangle by 1px in the x and y every 20 milliseconds:\n      const loopRectMotion = () => {\n        setTimeout(() => {\n          m._moveRect();\n          if (m._getIsRunning()) loopRectMotion();\n        }, 20)\n      };\n\n      // Enable you to pause and resume the rectangle movement:\n      document.querySelector('#actionButton')\n        .addEventListener('click', event => {\n          const newIsRunning = !m._getIsRunning();\n          m._setIsRunning(newIsRunning);\n          event.target.innerHTML = newIsRunning ? 'Pause' : 'Start';\n          if (newIsRunning) loopRectMotion();\n        });\n\n      loopRectMotion();\n    });\n  </script>\n</body>\n</html>\n```\n\n这段代码将复制我们在前面几节中创建的带有一些附加功能的 SDL 示例。当矩形碰到右下角时，它会改变方向。您还可以使用`<canvas>`元素下的按钮暂停和恢复矩形的移动。您可以看到我们如何将`_jsFillRect`和`_jsClearRect`函数传递到`importObj.env`对象中，以便它们可以被 Wasm 模块引用。\n\n# 端上来\n\n让我们在浏览器中测试我们的代码。从 VS Code 终端，确保您在`/book-examples`文件夹中，并运行命令启动本地服务器:\n\n```cpp\nserve -l 8080\n```\n\n重要的是你在`/book-examples`文件夹中。如果您尝试仅在`/chapter-05-create-load-module`文件夹中提供代码，您将无法使用`loadWasm()`功能。如果你打开浏览器到`http://127.0.0.1:8080/chapter-05-create-load-module/without-glue.html`，你应该会看到这个:\n\n![](img/76e4c6ba-e85a-448f-8418-7e71adddb265.png)\n\nWithout glue code example running in the browser\n\n尝试按下暂停按钮；标题应改为开始，矩形应停止移动。再次单击它应该会导致矩形再次开始移动。\n\n# 摘要\n\n在这一章中，我们介绍了使用 Emscripten 粘合代码和 Wasm 模块的模块的编译和加载过程。通过利用 Emscripten 的一些内置特性，例如移植的库和助手方法，我们能够展示 Emscripten 提供的优势。我们讨论了一些可以传递给`emcc`命令的编译器标志，以及这将如何影响您的输出。通过利用 VS 代码的任务特性，我们能够设置一个构建命令来加速构建过程。我们还回顾了在没有粘合代码的情况下编译和加载 Wasm 模块的过程。我们编写了一些可重用的 JavaScript 代码来加载模块，以及与我们编译的 Wasm 模块交互的代码。\n\n在[第 6 章](06.html)、*与 JavaScript 的交互和调试*中，我们将讲述在浏览器中与 JavaScript 的交互和调试技术。\n\n# 问题\n\n1.  SDL 代表什么？\n2.  除了 JavaScript、HTML 和 Wasm 之外，你还能为`emcc`命令生成什么其他带有`-o`标志的输出类型？\n3.  使用 Emscripten 预先生成的加载代码有什么好处？\n4.  你必须在 C/C++ 文件中给你的函数起什么名字才能确保它在浏览器中自动执行编译后的输出？\n5.  为什么在使用移植库时，我们不能只使用 Wasm 文件输出，而不使用“胶水”代码？\n6.  VS Code 中运行默认构建任务的快捷键是什么？\n7.  为什么我们需要 Wasm 加载代码中的`getDefaultEnv()`方法？\n8.  传递到用 Emscripten 创建的 Wasm 模块的 Wasm 实例化代码中的`importObj.env`对象需要哪五项？\n\n# 进一步阅读\n\n*   关于 SDL:[https://www.libsdl.org/index.php](https://www.libsdl.org/index.php)\n*   **Emscripten 编译器前端**(**emcc**):[http://kripken . github . io/Emscripten-site/docs/tools _ reference/emcc . html](http://kripken.github.io/emscripten-site/docs/tools_reference/emcc.html)\n*   通过任务与外部工具集成:[https://code.visualstudio.com/docs/editor/tasks](https://code.visualstudio.com/docs/editor/tasks)\n*   加载并运行 WebAssembly 代码:[https://developer . Mozilla . org/en-US/docs/web assembly/Loading _ and _ running](https://developer.mozilla.org/en-US/docs/WebAssembly/Loading_and_running)"
  },
  {
    "path": "docs/learn-wasm/06.md",
    "content": "# 六、与 JavaScript 交互和调试\n\n在 WebAssembly 的工作中有很多令人兴奋的特性和建议。然而，在撰写本书时，功能集相当有限。从目前的情况来看，您可以从使用 Emscripten 提供的一些功能中受益匪浅。从 JavaScript 与 C/C++ 进行交互的过程(反之亦然)会有所不同，这取决于您是否决定使用 Emscripten。\n\n在本章中，我们将介绍如何在 C/C++ 代码中使用 JavaScript 函数，以及如何与来自 JavaScript 的 C/C++ 代码的编译输出进行交互。我们还将描述 Emscripten 的*粘合*代码如何影响 Wasm 实例的使用方式，以及如何在浏览器中调试编译后的代码。\n\n本章的目标是理解以下内容:\n\n*   Emscripten 的`Module`和浏览器的`WebAssembly`对象之间的区别\n*   如何从 JavaScript 代码中调用编译好的 C/C++ 函数\n*   如何从 C/C++ 代码中调用 JavaScript 函数\n*   使用 C++ 时需要注意的特殊注意事项\n*   在浏览器中调试编译输出的技术\n\n# 电子脚本模块与网络组件对象\n\n在前一章中，我们简要介绍了 Emscripten 的`Module`对象以及如何在浏览器中加载它。`Module`对象提供了几种方便的方法，与浏览器的`WebAssembly`对象有很大不同。在本节中，我们将更详细地回顾 Emscripten 的`Module`对象。我们还将讨论 Emscripten 的`Module`和 WebAssembly 的 *JavaScript API* 中描述的对象之间的区别。\n\n# 什么是 Emscripten 模块？\n\nEmscripten 的官方网站为`Module`对象提供了以下定义:\n\n\"Module is a global JavaScript object with attributes that Emscripten-generated code calls at various points in its execution.\"\n\n加载过程不仅不同于网络组件的`compile`和`instantiate`功能，而且`Module`提供了一些开箱即用的有用功能，否则这些功能需要在网络组件中自定义实现。在获取并加载了 Emscripten 的 JavaScript *粘附*代码后，`Module`在全局范围内(`window.Module`)可用。\n\n# 粘合代码中的默认方法\n\nEmscripten 的`Module`对象提供了一些默认的方法和属性来帮助调试和确保编译代码的成功执行。您可以利用`preRun`和`postRun`属性在调用模块的`run()`函数之前或之后执行 JavaScript 代码，或者将`print()`和`printErr()`函数的输出传送到页面上的一个 HTML 元素。我们将在本书后面利用其中的一些方法。您可以在[https://kri pken . github . io/em script en-site/docs/API _ reference/module . html](https://kripken.github.io/emscripten-site/docs/api_reference/module.html)上了解更多。\n\n# 与网络组件对象的区别\n\n我们在[第 5 章](05.html)、*创建和加载网络组件模块*中介绍了浏览器的网络组件对象和相应的加载过程。WebAssembly 的 JavaScript 和 Web APIs 定义了浏览器的`window.WebAssembly`对象中可用的对象和方法。Emscripten 的`Module`可以看作是 WebAssembly 的`Module`和`Instance`对象的组合，它们存在于 WebAssembly 的实例化函数返回的`result`对象中。通过将`-s MODULARIZE=1`标志传递给`emcc`命令，我们能够(在一定程度上)复制 WebAssembly 的实例化方法。在接下来的部分中，我们将评估集成 JavaScript 和 C/C++ 的方法，我们将更详细地研究 Emscripten 的`Module`和浏览器的`WebAssembly`对象之间的差异。\n\n# 从 JavaScript 调用编译后的 C/C++ 函数\n\n不管有没有 Emscripten 的粘合代码，从 Wasm 实例调用函数都是一个相对简单的过程。利用 Emscripten 的 API 提供了更广泛的功能和集成，代价是在`.wasm`文件旁边包含了粘合代码。在本节中，我们将回顾通过 JavaScript 和 Emscripten 提供的附加工具与编译后的 Wasm 实例进行交互的方法。\n\n# 从模块调用函数\n\nEmscripten 提供了两个从 JavaScript 调用编译好的 C/C++ 函数的函数:`ccall()`和`cwrap()`。这两种功能都存在于`Module`对象中。决定使用哪一个取决于该函数是否会被多次调用。以下章节内容摘自`preamble.js`Emscripten 的 API 参考文档，可在[http://kripken . github . io/Emscripten-site/docs/API _ reference/preamble . js . html](http://kripken.github.io/emscripten-site/docs/api_reference/preamble.js.html)查看。\n\nYou don't need to prefix function calls with `_` when using `ccall()` or `cwrap()` – just use the name specified in the C/C++ file.\n\n# Module.ccall()\n\n`Module.ccall()`从 JavaScript 调用一个编译好的 C 函数，并返回该函数的结果。`Module.ccall()`的功能签名如下:\n\n```cpp\nccall(ident, returnType, argTypes, args, opts)\n```\n\n您必须为`returnType`和`argTypes`参数指定类型名称。可能的类型有`\"number\"`、`\"string\"`、`\"array\"`和`\"boolean\"`，它们对应于适当的 JavaScript 类型。您不能为`returnType`参数指定`\"array\"`，因为无法知道数组的长度。如果函数没有返回任何内容，可以为`returnType`指定`null`(注意没有引号)。\n\n`opts`参数是一个可选的选项对象，可以包含一个名为`async`的布尔属性。为此属性指定值`true`意味着调用将执行异步操作。我们不会在任何示例中使用该参数，但是如果您希望了解更多信息，请访问[http://kripken . github . io/emscripten-site/docs/API _ reference/preamble . js . html # calling-compiled-c-functions-from-JavaScript](http://kripken.github.io/emscripten-site/docs/api_reference/preamble.js.html#calling-compiled-c-functions-from-javascript)。\n\n我们来看一个`ccall()`的例子。以下代码取自 Emscripten 网站，演示了如何从 C 文件的编译输出中调用名为`c_add()`的函数:\n\n```cpp\n// Call C from JavaScript\nvar result = Module.ccall(\n  'c_add', // name of C function\n  'number', // return type\n  ['number', 'number'], // argument types\n  [10, 20] // arguments\n);\n\n// result is 30\n```\n\n# Module.cwrap()\n\n`Module.cwrap()`与`ccall()`类似，它调用一个编译好的 C 函数。但是，它不是返回值，而是返回一个可以根据需要多次重用的 JavaScript 函数。`Module.cwrap()`的功能签名如下:\n\n```cpp\ncwrap(ident, returnType, argTypes)\n```\n\n就像`ccall()`一样，您可以指定代表`returnType`和`argTypes`参数类型的字符串值。您不能在`argTypes`中使用`\"array\"`类型，因为调用函数时无法知道数组的长度。对于不返回值的函数，使用`null`(不带引号)作为`returnType`参数。\n\n以下代码取自 Emscripten 网站，演示了如何使用`cwrap()`创建一个可重用的函数:\n\n```cpp\n// Call C from JavaScript\nvar c_javascript_add = Module.cwrap(\n  'c_add', // name of C function\n  'number', // return type\n  ['number', 'number'] // argument types\n);\n\n// Call c_javascript_add normally\nconsole.log(c_javascript_add(10, 20)); // 30\nconsole.log(c_javascript_add(20, 30)); // 50\n```\n\n# C++ 和名称 mangling\n\n你可能已经注意到了`ccall()`和`cwrap()`的描述指定了两者都是用来调用编译好的 C 函数的。C++ 的省略是有意的，因为从 C++ 文件调用函数需要一个额外的步骤。C++ 支持函数重载，这意味着您可以多次使用同一个函数名，但是向每个函数传递不同的参数以获得不同的结果。下面是一些使用函数重载的代码示例:\n\n```cpp\nint addNumbers(int num1, int num2) {\n    return num1 + num2;\n}\n\nint addNumbers(int num1, int num2, int num3) {\n    return num1 + num2 + num3;\n}\n\nint addNumbers(int num1, int num2, int num3, int num4) {\n    return num1 + num2 + num3 + num4;\n}\n\n// The function will return a value based on how many\n// arguments you pass it:\nint getSumOfTwoNumbers = addNumbers(1, 2);\n// returns 3\n\nint getSumOfThreeNumbers = addNumbers(1, 2, 3);\n// returns 6\n\nint getSumOfFourNumbers = addNumbers(1, 2, 3, 4);\n// returns 10\n```\n\n编译器需要区分这些函数。如果它使用名称`addNumbers`，并且您试图在一个地方用两个参数调用函数，在另一个地方用三个参数调用函数，它将失败。要在编译后的 Wasm 中按名称调用该函数，您需要将该函数包装在一个`extern`块中。包装函数的一个含义是，您必须为每个条件显式定义函数。下面的代码片段演示了如何实现前面的函数，而不用名称混淆:\n\n```cpp\nextern \"C\" {\nint addTwoNumbers(int num1, int num2) {\n    return num1 + num2;\n}\n\nint addThreeNumbers(int num1, int num2, int num3) {\n    return num1 + num2 + num3;\n}\n\nint addFourNumbers(int num1, int num2, int num3, int num4) {\n    return num1 + num2 + num3 + num4;\n}\n}\n```\n\n# 从网络程序集实例调用函数\n\n在前一章中，我们演示了如何从 JavaScript 调用 Wasm 实例中的函数，但这是假设您在浏览器中实例化了一个模块，并且没有粘合代码。Emscripten 还提供了从 Wasm 实例调用函数的能力。模块实例化后，通过从`instance.exports`对象调用函数来调用函数，该对象可从解析的`Promise`结果中访问。MDN 的文档为`WebAssembly.instantiateStreaming`提供了以下功能签名:\n\n```cpp\nPromise<ResultObject> WebAssembly.instantiateStreaming(source, importObject);\n```\n\nYou may need to use the `WebAssembly.instantiate()` method, depending on your browser. Chrome currently supports `WebAssembly.instantiateStreaming()`, but if you encounter an error when attempting to load your module, use the `WebAssembly.instantiate()` method instead.\n\n`ResultObject`包含`instance`对象，我们需要引用该对象来调用从模块导出的函数。下面是一些从编译后的 Wasm 实例中调用名为`_addTwoNumbers`的函数的代码:\n\n```cpp\n// Assume the importObj is already defined.\nWebAssembly.instantiateStreaming(\n  fetch('simple.wasm'),\n  importObj\n)\n  .then(result => {\n    const addedNumbers = result.instance.exports._addTwoNumbers(1, 2);\n    // result is 3\n  });\n```\n\nEmscripten 提供了一种以几乎相同的方式执行函数调用的方法，尽管实现方式略有不同。如果您使用类似承诺的应用编程接口，您可以从`Module()`的承诺解析的`asm`对象访问该函数。下面的示例演示了如何利用此功能:\n\n```cpp\n// Using Emscripten's Module\nModule()\n  .then(result => {\n    // \"asm\" is essentially \"instance\"\n    const exports = result.asm;\n    const addedNumbers = exports._addTwoNumbers(1, 2);\n    // result is 3\n  });\n```\n\n用 Emscripten 复制 WebAssembly 的 Web API 语法简化了未来的任何重构。如果你决定使用 WebAssembly 的网络应用编程接口，将来你可以很容易地用 WebAssembly 的`instantiateStreaming()`方法替换`Module()`，用`result.instance`替换`result.asm`。\n\n# 从 C/C++ 调用 JavaScript 函数\n\n从 C/C++ 代码中访问 JavaScript 的功能允许在使用 WebAssembly 时增加灵活性。利用 JavaScript 的方法和手段在 Emscripten 的粘合代码和仅 Wasm 的实现之间有很大的不同。在这一节中，我们将介绍在使用和不使用 Emscripten 的情况下，将 JavaScript 集成到 C/C++ 代码中的各种方法。\n\n# 使用粘合代码与 JavaScript 交互\n\nEmscripten 提供了几种将 JavaScript 与 C/C++ 代码集成的技术。可用的技术在实现和复杂性上有所不同，有些仅适用于特定的执行环境(例如，浏览器)。决定使用哪一个取决于您的具体用例。我们将关注`emscripten_run_script()`函数和用`EM_*`包装器内联 JavaScript。以下部分内容摘自*与代码交互*部分，可在[https://kripken . github . io/Emscripten-site/docs/porting/connecting _ CPP _ and _ JavaScript/interaction-Code . html # interaction-Code](https://kripken.github.io/emscripten-site/docs/porting/connecting_cpp_and_javascript/Interacting-with-code.html#interacting-with-code)查看。\n\n# 使用 emscripten_run_script()执行字符串\n\nEmscripten 网站将`emscripten_run_script()`函数描述为调用 C/C++ 的 JavaScript 最直接但稍慢的方法。这是一种非常适合单行 JavaScript 代码的技术，对于调试非常有用。该文档指出，它使用`eval()`有效地运行代码，这是一个将字符串作为代码执行的 JavaScript 函数。以下取自 Emscripten 网站的代码演示了如何使用`emscripten_run_script()`调用浏览器的`alert()`功能，文本为`'hi'`:\n\n```cpp\nemscripten_run_script(\"alert('hi')\");\n```\n\n对于性能是一个因素的更复杂的用例，使用*内联 JavaScript* 提供了更好的解决方案。\n\n# 使用 EM_ASM()执行内联 JavaScript\n\n您可以用`EM_ASM()`将 JavaScript 代码包装在您的 C/C++ 文件中，当编译后的代码在浏览器中运行时，它就会执行。下面的代码演示了基本用法:\n\n```cpp\n#include <emscripten.h>\n\nint main() {\n    EM_ASM(\n        console.log('This is some JS code.');\n    );\n    return 0;\n}\n```\n\nJavaScript 代码会立即执行，并且不能在包含它的 C/C++ 文件中重用。参数可以作为变量`$0`、`$1`等传递到 JavaScript 代码块中。这些争论可以是`int32_t`或`double`类型。以下代码片段取自 Emscripten 网站，演示了如何在`EM_ASM()`块中使用参数:\n\n```cpp\nEM_ASM({\n    console.log('I received: ' + [ $0, $1 ]);\n}, 100, 35.5);\n```\n\n# 用 EM_JS 重用内联 JavaScript()\n\n如果你的 C/C++ 文件中需要一个可重用的函数，你可以把 JavaScript 代码包装在一个`EM_JS()`块中，像普通的 C/C++ 函数一样执行。下面的代码片段描述了`EM_JS()`的定义:\n\n```cpp\nEM_JS(return_type, function_name, arguments, code)\n```\n\n`return_type`参数表示与 JavaScript 代码的输出相对应的 C 类型(例如，`int`或`float`)。如果 JavaScript 代码没有返回任何内容，请为`return_type`指定`void`。下一个参数`function_name`表示从 C/C++ 文件中的其他位置调用 JavaScript 代码时要使用的名称。`arguments`参数用于定义可以从 C 调用函数传递到 JavaScript 代码中的参数。`code`参数是用大括号包装的 JavaScript 代码。以下代码片段取自 Emscripten 网站，演示了在 C 文件中使用`EM_JS()`:\n\n```cpp\n#include <emscripten.h>\n\nEM_JS(void, take_args, (int x, float y), {\n    console.log(`I received ${x} and ${y}`);\n});\n\nint main() {\n    take_args(100, 35.5);\n    return 0;\n}\n```\n\n# 使用粘合代码的示例\n\n让我们编写一些利用所有这些特性的代码。在本节中，我们将修改我们在*编译 C 语言中使用的代码，而不使用胶水代码*和*来获取和实例化[第 5 章](05.html)、*的*节中的一个 Wasm 文件*来创建和加载一个 WebAssembly 模块。这是在红色画布上显示一个移动的蓝色矩形的代码，只需点击一个按钮就可以暂停和重启。该部分的代码位于`learn-webassembly`存储库中的`/chapter-06-interact-with-js`文件夹中。让我们从更新 C 代码开始。\n\n# C 代码\n\n在名为`/chapter-06-interact-with-js`的`/book-examples`文件夹中创建新文件夹。在名为`js-with-glue.c`的`/chapter-06-interact-with-js`文件夹中创建新文件，并用以下内容填充:\n\n```cpp\n/*\n * This file interacts with the canvas through imported functions.\n * It moves a blue rectangle diagonally across the canvas\n * (mimics the SDL example).\n */\n#include <emscripten.h>\n#include <stdbool.h>\n\n#define BOUNDS 255\n#define RECT_SIDE 50\n#define BOUNCE_POINT (BOUNDS - RECT_SIDE)\n\nbool isRunning = true;\n\ntypedef struct Rect {\n  int x;\n  int y;\n  char direction;\n} Rect;\n\nstruct Rect rect;\n\n/*\n * Updates the rectangle location by 1px in the x and y in a\n * direction based on its current position.\n */\nvoid updateRectLocation() {\n    // Since we want the rectangle to \"bump\" into the edge of the\n    // canvas, we need to determine when the right edge of the\n    // rectangle encounters the bounds of the canvas, which is why\n    // we're using the canvas width - rectangle width:\n    if (rect.x == BOUNCE_POINT) rect.direction = 'L';\n\n    // As soon as the rectangle \"bumps\" into the left side of the\n    // canvas, it should change direction again.\n    if (rect.x == 0) rect.direction = 'R';\n\n    // If the direction has changed based on the x and y\n    // coordinates, ensure the x and y points update\n    // accordingly:\n    int incrementer = 1;\n    if (rect.direction == 'L') incrementer = -1;\n    rect.x = rect.x + incrementer;\n    rect.y = rect.y + incrementer;\n}\n\nEM_JS(void, js_clear_rect, (), {\n    // Clear the rectangle to ensure there's no color where it\n    // was before:\n    var canvas = document.querySelector('#myCanvas');\n    var ctx = canvas.getContext('2d');\n    ctx.fillStyle = '#ff0000';\n    ctx.fillRect(0, 0, 255, 255);\n});\n\nEM_JS(void, js_fill_rect, (int x, int y, int width, int height), {\n    // Fill the rectangle with blue in the specified coordinates:\n    var canvas = document.querySelector('#myCanvas');\n    var ctx = canvas.getContext('2d');\n    ctx.fillStyle = '#0000ff';\n    ctx.fillRect(x, y, width, height);\n});\n\n/*\n * Clear the existing rectangle element from the canvas and draw a\n * new one in the updated location.\n */\nEMSCRIPTEN_KEEPALIVE\nvoid moveRect() {\n    // Event though the js_clear_rect doesn't have any\n    // parameters, we pass 0 in to prevent a compiler warning:\n    js_clear_rect(0);\n    updateRectLocation();\n    js_fill_rect(rect.x, rect.y, RECT_SIDE, RECT_SIDE);\n}\n\nEMSCRIPTEN_KEEPALIVE\nbool getIsRunning() {\n    return isRunning;\n}\n\nEMSCRIPTEN_KEEPALIVE\nvoid setIsRunning(bool newIsRunning) {\n    isRunning = newIsRunning;\n    EM_ASM({\n        // isRunning is either 0 or 1, but in JavaScript, 0\n        // is \"falsy\", so we can set the status text based\n        // without explicitly checking if the value is 0 or 1:\n        var newStatus = $0 ? 'Running' : 'Paused';\n        document.querySelector('#runStatus').innerHTML = newStatus;\n    }, isRunning);\n}\n\nEMSCRIPTEN_KEEPALIVE\nvoid init() {\n    emscripten_run_script(\"console.log('Initializing rectangle...')\");\n    rect.x = 0;\n    rect.y = 0;\n    rect.direction = 'R';\n    setIsRunning(true);\n    emscripten_run_script(\"console.log('Rectangle should be moving!')\");\n}\n```\n\n您可以看到，我们使用了 Emscripten 提供的所有三个 JavaScript 集成。有两个函数`js_clear_rect()`和`js_fill_rect()`，它们是在`EM_JS()`块中定义的，取代了原始示例中导入的函数。`setIsRunning()`函数中的`EM_ASM()`块更新了我们将添加到 HTML 代码中的新状态元素的文本。`emscripten_run_script()`功能只是注销一些状态信息。我们需要在模块外指定我们计划使用的功能之上的`EMSCRIPTEN_KEEPALIVE`。如果不指定这一点，编译器会将函数视为死代码并移除它们。\n\n# 超文本标记语言代码\n\n让我们在`/chapter-06-interact-with-js`文件夹中创建一个名为`js-with-glue.html`的文件，并用以下内容填充它:\n\n```cpp\n<!doctype html>\n<html lang=\"en-us\">\n<head>\n  <title>Interact with JS using Glue Code</title>\n</head>\n<body>\n  <h1>Interact with JS using Glue Code</h1>\n  <canvas id=\"myCanvas\" width=\"255\" height=\"255\"></canvas>\n  <div style=\"margin-top: 16px;\">\n    <button id=\"actionButton\" style=\"width: 100px; height: 24px;\">Pause</button>\n    <span style=\"width: 100px; margin-left: 8px;\">Status:</span>\n    <span id=\"runStatus\" style=\"width: 100px;\"></span>\n  </div>\n  <script type=\"application/javascript\" src=\"js-with-glue.js\"></script>\n  <script type=\"application/javascript\">\n    Module()\n      .then(result => {\n        const m = result.asm;\n        m._init();\n\n        // Move the rectangle by 1px in the x and y every 20 milliseconds:\n        const loopRectMotion = () => {\n          setTimeout(() => {\n            m._moveRect();\n            if (m._getIsRunning()) loopRectMotion();\n          }, 20)\n        };\n\n        // Enable you to pause and resume the rectangle movement:\n        document.querySelector('#actionButton')\n          .addEventListener('click', event => {\n            const newIsRunning = !m._getIsRunning();\n            m._setIsRunning(newIsRunning);\n            event.target.innerHTML = newIsRunning ? 'Pause' : 'Start';\n            if (newIsRunning) loopRectMotion();\n          });\n\n        loopRectMotion();\n      });\n  </script>\n</body>\n</html>\n```\n\n我们添加了两个`<span>`元素来显示矩形的移动状态，以及相应的标签。我们使用 Emscripten 的类似 Promise 的 API 来加载模块，并从编译的代码中引用函数。我们不再向模块传递`_jsFillRect`和`_jsClearRect`函数，因为我们在`js-with-glue.c`文件中处理这些函数。\n\n# 编译并提供结果\n\n要编译代码，请确保您在`/chapter-06-interact-with-js`文件夹中，并运行以下命令:\n\n```cpp\nemcc js-with-glue.c -O3 -s WASM=1 -s MODULARIZE=1 -o js-with-glue.js\n```\n\n完成后，运行以下命令启动本地服务器:\n\n```cpp\nserve -l 8080\n```\n\n打开浏览器，导航至`http://127.0.0.1:8080/js-with-glue.html`。你应该看到这样的东西:\n\n![](img/72e12623-7f8d-4d6a-8661-1f13822c1ef5.png)\n\nGlue code running in the browser\n\n如果您按下“暂停”按钮，按钮上的标题应更改为“开始”，状态旁边的文本应更改为“暂停”，并且矩形应停止移动。\n\n# 无需粘合代码即可与 JavaScript 交互\n\n在 C/C++ 文件中使用 JavaScript 代码遵循一种不同于用于 Emscripten 的技术的范式。您不是在 C/C++ 文件中编写 JavaScript，而是将函数传递到您的 WebAssembly 实例化代码中。在本节中，我们将更详细地描述这个过程。\n\n# 使用导入对象将 JavaScript 传递给 C/C++\n\n为了在您的 C/C++ 代码中利用 JavaScript 的功能，您需要向传递到 WebAssembly 的实例化函数中的`importObj.env`参数添加函数定义。您可以在`importObj.env`之外定义功能，也可以内联定义功能。下面的代码片段演示了每个选项:\n\n```cpp\n// You can define the function inside of the env object:\nconst env = {\n  // Make sure you prefix the function name with \"_\"!\n  _logValueToConsole: value => {\n    console.log(`'The value is ${value}'`);\n  }\n};\n\n// Or define it outside of env and reference it within env:\nconst logValueToConsole = value => {\n  console.log(`'The value is ${value}'`);\n};\n\nconst env = {\n  _logValueToConsole: logValueToConsole\n};\n```\n\n考虑到手动内存管理以及 C、C++ 和 Rust 的严格打字要求，在 Wasm 模块中可以传入和使用的内容是有限的。JavaScript 允许您在代码执行过程中轻松添加、移除和更改对象的属性值。您甚至可以通过向内置语言功能的`prototype`添加函数来扩展语言。C、C++ 和 Rust 的限制要多得多，如果不熟悉这些语言，就很难充分利用 WebAssembly。\n\n# 在 C/C++ 中调用导入的函数\n\n您需要在使用它的 C/C++ 代码中定义您传递到`importObj.env`中的 JavaScript 函数。函数签名必须与您传入的匹配。以下示例更详细地演示了这一点。下面是与编译后的 C 文件(`index.html`)交互的 JavaScript 代码:\n\n```cpp\n// index.html <script> contents\nconst env = {\n  _logAndMultiplyTwoNums: (num1, num2) => {\n    const result = num1 * num2;\n    console.log(result);\n    return result;\n  },\n};\n\nloadWasm('main.wasm', { env })\n  .then(({ instance }) => {\n    const result = instance.exports._callMultiply(5.5, 10);\n    console.log(result);\n    // 55 is logged to the console twice\n  });\n```\n\n这是`main.c`的内容，编译成`main.wasm`，在`index.html`内使用:\n\n```cpp\n// main.c (compiled to main.wasm)\nextern float logAndMultiplyTwoNums(float num1, float num2);\n\nfloat callMultiply(float num1, float num2) {\n    return logAndMultiplyTwoNums(num1, num2);\n}\n```\n\n在 C/C++ 中调用 JavaScript 函数的方式与调用普通 C/C++ 函数的方式相同。虽然当您将函数传递到`importObj.env`时，您会在函数前面加上一个`_`，但是在 C/C++ 文件中定义它时，您不需要包括这个前缀。\n\n# 一个没有胶水代码的例子\n\n来自*在没有粘合代码的情况下编译 C 的示例代码*和*在 [第 5 章](05.html)*中获取并实例化一个 Wasm 文件*创建并加载一个 WebAssembly 模块*，演示了如何在不使用 Emscripten 的粘合代码的情况下将 JavaScript 集成到我们的 C 文件中。在本节中，我们将稍微修改示例代码，并将文件类型更改为 C++。\n\n# C++ 代码\n\n在您的`/chapter-06-interact-with-js`文件夹中创建一个名为`js-without-glue.cpp`的文件，并用以下内容填充它:\n\n```cpp\n/*\n * This file interacts with the canvas through imported functions.\n * It moves a circle diagonally across the canvas.\n */\n#define BOUNDS 255\n#define CIRCLE_RADIUS 50\n#define BOUNCE_POINT (BOUNDS - CIRCLE_RADIUS)\n\nbool isRunning = true;\n\ntypedef struct Circle {\n  int x;\n  int y;\n  char direction;\n} Circle;\n\nstruct Circle circle;\n\n/*\n * Updates the circle location by 1px in the x and y in a\n * direction based on its current position.\n */\nvoid updateCircleLocation() {\n    // Since we want the circle to \"bump\" into the edge of the canvas,\n    // we need to determine when the right edge of the circle\n    // encounters the bounds of the canvas, which is why we're using\n    // the canvas width - circle width:\n    if (circle.x == BOUNCE_POINT) circle.direction = 'L';\n\n    // As soon as the circle \"bumps\" into the left side of the\n    // canvas, it should change direction again.\n    if (circle.x == CIRCLE_RADIUS) circle.direction = 'R';\n\n    // If the direction has changed based on the x and y\n    // coordinates, ensure the x and y points update accordingly:\n    int incrementer = 1;\n    if (circle.direction == 'L') incrementer = -1;\n    circle.x = circle.x + incrementer;\n    circle.y = circle.y - incrementer;\n}\n\n// We need to wrap any imported or exported functions in an\n// extern block, otherwise the function names will be mangled.\nextern \"C\" {\n// These functions are passed in through the importObj.env object\n// and update the circle on the <canvas>:\nextern int jsClearCircle();\nextern int jsFillCircle(int x, int y, int radius);\n\n/*\n * Clear the existing circle element from the canvas and draw a\n * new one in the updated location.\n */\nvoid moveCircle() {\n    jsClearCircle();\n    updateCircleLocation();\n    jsFillCircle(circle.x, circle.y, CIRCLE_RADIUS);\n}\n\nbool getIsRunning() {\n    return isRunning;\n}\n\nvoid setIsRunning(bool newIsRunning) {\n    isRunning = newIsRunning;\n}\n\nvoid init() {\n    circle.x = 0;\n    circle.y = 255;\n    circle.direction = 'R';\n    setIsRunning(true);\n}\n}\n```\n\n这段代码类似于前面的示例，但是画布上元素的形状和方向已经改变。现在，元素是一个圆，从画布的左下角开始，向右上角对角移动。\n\n# 超文本标记语言代码\n\n接下来，在您的`/chapter-06-interact-with-js`文件夹中创建一个名为`js-without-glue.html`的文件，并用以下内容填充它:\n\n```cpp\n<!doctype html>\n<html lang=\"en-us\">\n<head>\n  <title>Interact with JS without Glue Code</title>\n  <script\n    type=\"application/javascript\"\n    src=\"../common/load-wasm.js\">\n  </script>\n  <style>\n    #myCanvas {\n      border: 2px solid black;\n    }\n    #actionButtonWrapper {\n      margin-top: 16px;\n    }\n    #actionButton {\n      width: 100px;\n      height: 24px;\n    }\n  </style>\n</head>\n<body>\n  <h1>Interact with JS without Glue Code</h1>\n  <canvas id=\"myCanvas\" width=\"255\" height=\"255\"></canvas>\n  <div id=\"actionButtonWrapper\">\n    <button id=\"actionButton\">Pause</button>\n  </div>\n  <script type=\"application/javascript\">\n    const canvas = document.querySelector('#myCanvas');\n    const ctx = canvas.getContext('2d');\n\n    const fillCircle = (x, y, radius) => {\n      ctx.fillStyle = '#fed530';\n      // Face outline:\n      ctx.beginPath();\n      ctx.arc(x, y, radius, 0, 2 * Math.PI);\n      ctx.fill();\n      ctx.stroke();\n      ctx.closePath();\n\n      // Eyes:\n      ctx.fillStyle = '#000000';\n      ctx.beginPath();\n      ctx.arc(x - 15, y - 15, 6, 0, 2 * Math.PI);\n      ctx.arc(x + 15, y - 15, 6, 0, 2 * Math.PI);\n      ctx.fill();\n      ctx.closePath();\n\n      // Mouth:\n      ctx.beginPath();\n      ctx.moveTo(x - 20, y + 10);\n      ctx.quadraticCurveTo(x, y + 30, x + 20, y + 10);\n      ctx.lineWidth = 4;\n      ctx.stroke();\n      ctx.closePath();\n    };\n\n    const env = {\n      table: new WebAssembly.Table({ initial: 8, element: 'anyfunc' }),\n      _jsFillCircle: fillCircle,\n      _jsClearCircle: function() {\n        ctx.fillStyle = '#fff';\n        ctx.fillRect(0, 0, 255, 255);\n      },\n    };\n\n    loadWasm('js-without-glue.wasm', { env }).then(({ instance }) => {\n      const m = instance.exports;\n      m._init();\n\n      // Move the circle by 1px in the x and y every 20 milliseconds:\n      const loopCircleMotion = () => {\n        setTimeout(() => {\n          m._moveCircle();\n          if (m._getIsRunning()) loopCircleMotion();\n        }, 20)\n      };\n\n      // Enable you to pause and resume the circle movement:\n      document.querySelector('#actionButton')\n        .addEventListener('click', event => {\n          const newIsRunning = !m._getIsRunning();\n          m._setIsRunning(newIsRunning);\n          event.target.innerHTML = newIsRunning ? 'Pause' : 'Start';\n          if (newIsRunning) loopCircleMotion();\n        });\n\n      loopCircleMotion();\n    });\n  </script>\n</body>\n</html>\n```\n\n我们可以使用画布元素的 2D 上下文中可用的函数来手动绘制路径，而不是使用`rect()`元素。\n\n# 编译并提供结果\n\n我们只生成了一个 Wasm 模块，所以我们可以使用上一章中设置的构建任务来编译我们的代码。选择任务|运行构建任务…或使用键盘快捷键*Ctrl*/*Cmd*+*Shift*+*B*编译代码。如果您没有使用 VS 代码，请在`/chapter-06-interact-with-js`文件夹中打开一个 CLI 实例，并运行以下命令:\n\n```cpp\nemcc js-without-glue.cpp -Os -s WASM=1 -s SIDE_MODULE=1 -s BINARYEN_ASYNC_COMPILATION=0 -o js-without-glue.wasm\n```\n\n完成后，在`/book-examples`文件夹中打开一个终端，运行以下命令启动本地服务器:\n\n```cpp\nserve -l 8080\n```\n\n打开浏览器，导航至`http://127.0.0.1:8080/chapter-06-interact-with-js/js-without-glue.html`。你应该看到这样的东西:\n\n![](img/c981ffdb-10a6-4d61-b87c-fdfa19ce9d83.png)\n\nThe Wasm module running in the browser without glue code\n\n就像前面的例子一样，如果您按下暂停按钮，按钮上的标题应该更改为开始，圆圈应该停止移动。\n\n# 高级电子脚本功能\n\n在前几节中，我们介绍了在 JavaScript 和 C/C++ 之间进行通信时最常用的 Emscripten 特性，但这些并不是 Emscripten 提供的唯一功能。您需要了解一些高级特性和附加的 API，尤其是当您计划向应用添加更复杂的功能时。在本节中，我们将简要回顾其中一些高级功能，并详细介绍您可以从哪里了解更多信息。\n\n# ezmind\n\nEmbind 是 Emscripten 为连接 JavaScript 和 C++ 提供的附加功能。Emscripten 的网站提供了以下描述:\n\n\"Embind is used to bind C++ functions and classes to JavaScript, so that the compiled code can be used in a natural way by 'normal' JavaScript. Embind also supports calling JavaScript classes from C++.\"\n\nEmbind 是一个强大的特性，它允许 JavaScript 和 C++ 之间的紧密集成。您可以将一些 C++ 代码包装在一个`EMSCRIPTEN_BINDINGS()`块中，并通过浏览器中的`Module`对象引用它。让我们看一个来自 Emscripten 网站的例子。以下文件`example.cpp`用`emcc`的`--bind`旗编制:\n\n```cpp\n// example.cpp\n#include <emscripten/bind.h>\n\nusing namespace emscripten;\n\nfloat lerp(float a, float b, float t) {\n    return (1 - t) * a + t * b;\n}\n\nEMSCRIPTEN_BINDINGS(my_module) {\n    function(\"lerp\", &lerp);\n}\n```\n\n生成的模块加载到`example.html`中，调用`lerp()`函数:\n\n```cpp\n<!-- example.html -->\n<!doctype html>\n<html>\n<script src=\"example.js\"></script>\n<script>\n  // example.js was generated by running this command:\n  // emcc --bind -o example.js example.cpp\n  console.log('lerp result: ' + Module.lerp(1, 2, 0.5));\n</script>\n</html>\n```\n\n前面的例子代表了恩宾德的一小部分能力。您可以在[https://kripken . github . io/em scripten-site/docs/porting/connecting _ CPP _ and _ JavaScript/Embind . html](https://kripken.github.io/emscripten-site/docs/porting/connecting_cpp_and_javascript/embind.html)了解更多关于 Embind 的信息。\n\n# 文件系统应用编程接口\n\nEmscripten 通过使用文件系统库为文件操作提供支持，并公开了一个使用文件系统的应用编程接口。但是，当您编译项目时，默认情况下不包括它，因为它可能会显著增加文件的大小。如果您的 C/C++ 代码使用文件，库将被自动添加。文件系统类型因执行环境而异。例如，如果您在一个工作程序中运行代码，可以使用`WORKERFS`文件系统。默认情况下，使用`MEMFS`，将数据存储在内存中，当页面重新加载时，任何写入内存的数据都会丢失。您可以在[上阅读更多关于文件系统应用编程接口的信息。](https://kripken.github.io/emscripten-site/docs/api_reference/Filesystem-API.html#filesystem-api)\n\n# 获取应用编程接口\n\nEmscripten 还提供了一个提取应用编程接口。以下内容摘自文档:\n\n\"The Emscripten Fetch API allows native code to transfer files via XHR (HTTP GET, PUT, POST) from remote servers, and to persist the downloaded files locally in browser's IndexedDB storage, so that they can be re-accessed locally on subsequent page visits. The Fetch API is callable from multiple threads, and the network requests can be run either synchronously or asynchronously as desired.\"\n\nFetch API 可以用来与 Emscripten 的其他功能集成。如果需要获取 Emscripten 没有使用的数据，应该使用浏览器的 Fetch API([https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API))。你可以在[上阅读更多关于提取应用编程接口的信息。](https://kripken.github.io/emscripten-site/docs/api_reference/fetch.html)\n\n# 在浏览器中调试\n\n在浏览器中有效调试 JavaScript 代码并不总是那么容易。然而，开发工具在浏览器和具有内置调试功能的编辑器/IDEs 中有了显著的改进。不幸的是，向 web 应用添加 WebAssembly 增加了调试过程的复杂性。在本节中，我们将回顾一些利用 Wasm 调试 JavaScript 的技术，以及 Emscripten 提供的一些附加功能。\n\n# 高级概述\n\n调试 Emscripten 的`Module`相对简单。Emscripten 的错误消息格式良好且描述性强，因此您通常会立即发现导致问题的原因。您可以在浏览器的开发工具控制台中查看这些消息。\n\n如果在运行`emcc`命令时指定了`.html`输出，那么一些调试代码将已经内置(`Module.print`和`Module.printErr`)。在 HTML 文件中，加载代码将`window.onerror`事件设置为调用`Module.printErr`事件，因此您可以看到加载时发生的错误的详细信息。\n\n您可能会遇到的一个常见错误是调用了错误的函数名。如果您使用的是 Emscripten 的类似 Promise 的 API，您可以通过在浏览器的控制台中运行以下代码来打印出可用的函数:\n\n```cpp\nconsole.log(Module().asm);\n```\n\n下面的截图显示了我们在本章的“从 C/C++ 调用 JavaScript 函数”部分中使用的`js-with-glue.js`示例的输出:\n\n![](img/7a85c9a2-9f60-48eb-b1d1-196d77f5e9f8.png)\n\nLogging the contents of `Module().asm` in the browser console\n\n您的函数以及 Emscripten 生成的一些函数将以`_`作为前缀。编写被编译的代码的好处是编译器会提前捕捉到大多数错误。给定 C 和 C++ 等语言可用的大量工具，您应该能够快速理解和解决这些错误。\n\n如果您没有使用任何粘合代码，而是使用 WebAssembly 的 JavaScript 和 Web APIs 实例化了一个 Wasm 文件，那么调试可能会变得稍微复杂一些。如前所述，在 C 或 C++ 代码中，您有在编译时捕获大多数错误的优势。就像 Emscripten 一样，在浏览器的开发工具控制台中打印出来的错误消息提供了堆栈跟踪和对问题的相对清晰的描述。但是，如果您正在排除一个特别困难的错误，登录到控制台可能会变得麻烦和难以管理。幸运的是，您可以使用源映射来提高调试能力。\n\n# 使用源地图\n\nEmscripten 能够通过向编译器传递一些额外的标志来生成源映射。源映射允许您的浏览器将文件的源映射到应用中正在使用的文件。例如，您可以使用像 Webpack 这样的 JavaScript 构建工具来缩小代码，作为构建过程的一部分。然而，如果你试图找到一个 bug，导航和排除精简代码的故障是非常困难的。通过生成源代码图，您可以在浏览器的开发工具中查看原始形式的代码，并为调试设置断点。让我们为我们的`/chapter-06-interact-with-js/js-without-glue.cpp`文件生成一个源地图。在`/book-examples`文件夹中，在终端中运行以下命令:\n\n```cpp\nemcc chapter-06-interact-with-js/js-without-glue.cpp -O1 -g4 -s WASM=1 -s SIDE_MODULE=1 -s BINARYEN_ASYNC_COMPILATION=0 -o chapter-06-interact-with-js/js-without-glue.wasm --source-map-base http://localhost:8080/chapter-06-interact-with-js/\n```\n\n`-g4`参数启用源地图，而`--source-map-base`参数告诉浏览器在哪里可以找到源地图文件。编译完成后，运行以下命令，从`/book-examples`文件夹启动本地服务器:\n\n```cpp\nserve -l 8080\n```\n\n导航至`http://127.0.0.1:8080/chapter-06-interact-with-js/js-without-glue.html`，打开开发者工具，选择源码选项卡(在 Chrome 中)或调试器选项卡(在 Firefox 中)。如果您正在使用 Chrome，您应该会看到以下内容:\n\n![](img/ccc8b38d-f748-40fb-8d25-667dc176c0ff.png)\n\nWasm source maps in Chrome Developer Tools\n\n正如你所看到的，文件名不是很有用。每个文件都应该在顶部包含函数名，尽管有些名称可能已经被破坏了。如果遇到错误，可以设置断点，Chrome 的调试功能允许您导航调用堆栈。Firefox 以不同的方式处理它们的源地图。下面的截图显示了火狐开发者工具中的调试器视图:\n\n![](img/d40de2b0-f550-4e74-bbac-4249f1693ec4.png)\n\nWasm source map in Firefox Developer Tools\n\n源文件是一个包含 Wasm 文件的 Wat 表示的文件。您也可以在这里设置断点和调试代码。随着 WebAssembly 的发展，更多(更好)的工具将变得可用。同时，登录到控制台和利用源映射是您可以使用的当前调试方法。\n\n# 摘要\n\n在这一章中，我们重点介绍了 JavaScript 和 C/C++ 的相互通信，Emscripten 提供的一些特性，以及如何有效地调试在浏览器中利用 Wasm 的 web 应用。我们回顾了从 JavaScript 调用编译的 C/C++ 函数的各种方法，以及如何将 JavaScript 与 C/C++ 代码集成。Emscripten 的 API 是为了理解如何通过在编译的 Wasm 文件中包含粘合代码来克服 WebAssembly 当前的一些限制。即使 Emscripten 提供的功能没有出现在官方的 WebAssembly *核心规范*中(可能永远不会出现)，这也不应该阻止你利用它们。最后，我们简要介绍了如何在 Emscripten 模块或 WebAssembly 实例的上下文中调试浏览器中的 Wasm 文件。\n\n在下一章中，我们将从头开始构建一个真实的 WebAssembly 应用。\n\n# 问题\n\n1.  在`Module`对象上可用的两个函数的名称是什么，它们是你用来与浏览器中编译的代码进行交互的？\n2.  你需要什么来包装你的 C++ 代码，以确保函数名不会被破坏？\n3.  `EM_ASM()`和`EM_JS()`有什么区别？\n4.  `emscripten_run_script()`和`EM_ASM()` / `EM_JS()`哪个更有表演性？\n5.  如果你想在你的 C/C++ 代码之外使用它，你需要在你的函数上面的行中包含什么(提示:它以`EMSCRIPTEN`开始)？\n6.  在哪里可以定义一个在实例化模块时需要传入`importObj.env`对象的函数？\n7.  Emscripten 还提供了哪些额外的 API？\n8.  源图的目的是什么？\n\n# 进一步阅读\n\n*   emscripten API Reference:[http://kripken . github . io/emscripten-site/docs/API _ Reference/index . html](http://kripken.github.io/emscripten-site/docs/api_reference/index.html)\n*   源地图介绍:[http://blog.teamtreehouse.com/introduction-source-maps](http://blog.teamtreehouse.com/introduction-source-maps)\n*   使用浏览器调试网络组件:[http://webassemblycode.com/using-browsers-debug-webassembly](http://webassemblycode.com/using-browsers-debug-webassembly)"
  },
  {
    "path": "docs/learn-wasm/07.md",
    "content": "# 七、从头开始创建应用\n\n现在是时候应用你的知识了！由于 WebAssembly 的主要设计目标之一是在现有的 web 平台中执行并与其很好地集成，因此构建一个 web 应用来测试它是有意义的。尽管 WebAssembly 当前的功能集相当有限，但我们可以在基本层面上利用这项技术。在本章中，我们将从头开始构建一个单页应用，在*核心规范*的上下文中使用 Wasm 模块。\n\n本章结束时，您将知道如何:\n\n*   用 C 编写执行简单计算的函数\n*   用 Vue 构建一个基本的 JavaScript 应用\n*   将 Wasm 集成到您的 JavaScript 应用中\n*   确定当前形式的 WebAssembly 的功能和限制\n*   使用`browser-sync`运行并测试一个 JavaScript 应用\n\n# 烹饪书籍——让 WebAssembly 变得负责任\n\n如前所述，WebAssembly 当前的功能集相当有限。我们可以使用 Emscripten 来极大地扩展 web 应用的功能，但是这将带来不符合官方规范和添加粘合代码的代价。今天，我们仍然可以有效地使用 WebAssembly，这将带我们进入本章将要构建的应用。在本节中，我们将回顾我们将用来构建应用的库和工具，以及它的功能的简要概述。\n\n# 概述和功能\n\n在 WebAssembly 的当前形式中，我们可以相对容易地在 Wasm 模块和 JavaScript 代码之间传递数字。就现实世界的适用性而言，会计应用似乎是一个合乎逻辑的选择。我对会计软件的唯一争议是它有点无聊(无意冒犯)。我们将通过建立一些不道德的会计惯例来增加一点趣味。该应用名为*做假账*，一个与会计欺诈相关的术语。投资媒体对做账的定义如下:\n\n\"Cook the Books is an idiom describing fraudulent activities performed by corporations in order to falsify their financial statements. Typically, cooking the books involves augmenting financial data to yield previously nonexistent earnings. Examples of techniques used to cook the books involve accelerating revenues, delaying expenses, manipulating pension plans, and implementing synthetic leases.\"\n\nhttps://www.investopedia.com/terms/c/cookthebooks.asp 的投资媒体页面提供了烹饪书籍的详细例子。我们将对我们的应用采取一种简单的方法。我们将允许用户输入生熟金额的交易。原始金额代表实际存入或提取的金额，而熟金额是其他人都会看到的。该应用将生成饼图，按类别显示生交易或熟交易的费用和收入。用户将能够在两个视图之间轻松切换。该应用由以下组件组成:\n\n*   用于在事务和图表之间切换的选项卡\n*   显示交易记录的表\n*   允许用户添加、编辑或删除交易的按钮\n*   用于添加/更新事务的模式对话框\n*   按类别显示收入/支出的饼图\n\n# 使用的 JavaScript 库\n\n应用的 JavaScript 部分将使用 CDN 提供的几个库。它还将使用一个本地安装的库来观察代码的变化。以下部分将描述每个库及其在应用中的用途。\n\n# 某视频剪辑软件\n\nVue 是一个 JavaScript 框架，它允许您将一个应用拆分成单独的组件，以便于开发和调试。我们使用它来避免一个完整的 JavaScript 文件包含所有的应用逻辑，而另一个完整的 HTML 文件包含整个用户界面。之所以选择 Vue，是因为它不需要增加构建系统的复杂性，并且允许我们使用 HTML、CSS 和 JavaScript，而无需进行任何传输。官网是[https://vuejs.org](https://vuejs.org)。\n\n# UIkit\n\nUIkit 是我们将用于为应用添加样式和布局的前端框架。有几十种选择，像 Bootstrap 或布尔玛，提供类似的组件和功能。但是我选择了 UIkit，因为它提供了有用的实用程序类，并增加了 JavaScript 功能。您可以在[https://getuikit.com](https://getuikit.com)查看文档。\n\n# 洛拉斯\n\nLodash 是一个优秀的实用程序库，它提供了在 JavaScript 中执行常见操作的方法，这些方法还没有内置到语言中。我们将使用它来执行计算和操作交易数据。文件和安装说明可以在[https://lodash.com](https://lodash.com)找到。\n\n# 数据驱动文档\n\n**数据驱动文档** ( **D3** )是一个多面库，允许您将数据转换为令人印象深刻的可视化。D3 的应用编程接口由几个模块组成，从数组操作到图表和转换。我们将主要使用 D3 来创建饼图，但我们也将利用它提供的一些实用方法。你可以在[https://d3js.org](https://d3js.org)找到更多信息。\n\n# 其他库\n\n为了以正确的格式显示货币值并确保用户输入有效的美元金额，我们将利用**accounting . js**([http://openexchangerates.github.io/accounting.js](http://openexchangerates.github.io/accounting.js))和**vue-numeric**([https://kevinongko.github.io/vue-numeric](https://kevinongko.github.io/vue-numeric))库。为了简化开发，我们将设置一个基本的`npm`项目，并使用**浏览器同步**([https://www . browser sync . io](https://www.browsersync.io))立即查看运行的应用中反映的代码更改。\n\n# c 和构建过程\n\n应用使用 C 语言，因为我们用基本代数进行简单的计算。在这种情况下使用 C++ 是没有意义的。这将引入额外的步骤，确保我们需要从 JavaScript 调用的函数被包装在一个`extern`块中。我们将在一个 C 文件中编写计算函数，并将其编译成一个 Wasm 模块。我们可以继续使用 VS Code 的 Tasks 功能来执行构建，但是参数需要更新，因为我们将只编译一个文件。让我们继续项目配置。\n\n# 设置项目\n\nWebAssembly 还没有出现足够长的时间来建立关于文件夹结构、文件命名约定等方面的最佳实践。如果您要为 C/C++ 或 JavaScript 项目寻找最佳实践，您会遇到大量相互矛盾的建议和强烈的意见。考虑到这一点，让我们用这一节用所需的配置文件来设置我们的项目。\n\n这个项目的代码位于`learn-webassembly`存储库中的`/chapter-07-cook-the-books`文件夹中。当我们进入应用的 JavaScript 部分时，您必须有这些代码。我不会提供书中所有 Vue 组件的源代码，所以您需要从存储库中复制它们。\n\n# 为 Node.js 配置\n\n为了保持应用尽可能的简单，我们将避免使用像 Webpack 或 Rollup.js 这样的构建/捆绑工具。这允许我们减少所需依赖项的数量，并确保您遇到的任何问题都不是由构建依赖项的突然变化引起的。\n\n我们将创建一个 Node.js 项目，因为它允许我们运行脚本并在本地安装一个依赖项用于开发目的。到目前为止，我们已经使用了`/book-examples`文件夹，但是我们将在`/book-examples`之外创建一个新的项目文件夹，以便在 VS Code 中配置一个不同的默认构建任务。打开终端，`cd`进入所需文件夹，并输入以下命令:\n\n```cpp\n// Create a new directory and cd into it:\nmkdir cook-the-books\ncd cook-the-books\n\n// Create a package.json file with default values\nnpm init -y\n```\n\n`-y`命令放弃提示并用合理的默认值填充`package.json`文件。完成后，运行以下命令安装`browser-sync`:\n\n```cpp\nnpm install -D browser-sync@^2.24.4\n```\n\n`-D`是可选的，表示库是开发依赖项。如果您正在构建和分发应用，您将使用`-D`标志，因此我将其包括在内是为了遵守惯例。我建议安装该特定版本，以确保`start`脚本运行没有任何问题。在`browser-sync`安装后，在`package.json`文件的`scripts`条目中添加以下条目:\n\n```cpp\n...\n\"scripts\": {\n ...\n \"start\": \"browser-sync start --server \\\"src\\\" --files \\\"src/**\\\" --single --no-open --port 4000\"\n},\n…\n```\n\nIf you run `npm init` with the `-y` flag, there should be an existing script named `test`, which I omitted for clarity. If you didn't run it with the `-y` flag, you may need to create the `scripts` entry.\n\n如果需要，您可以填充`\"description\"`和`\"author\"`键。该文件最终看起来应该如下所示:\n\n```cpp\n{\n  \"name\": \"cook-the-books\",\n  \"version\": \"1.0.0\",\n  \"description\": \"Example application for Learn WebAssembly\",\n  \"main\": \"src/index.js\",\n  \"scripts\": {\n    \"start\": \"browser-sync start --server \\\"src\\\" --files \\\"src/**\\\" --single --no-open --port 4000\",\n    \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\"\n  },\n  \"keywords\": [],\n  \"author\": \"Mike Rourke\",\n  \"license\": \"MIT\",\n  \"devDependencies\": {\n    \"browser-sync\": \"^2.24.4\"\n  }\n}\n```\n\nIf you omit the `--no-open` flag from the `start` script, the browser will open automatically. The flag was included to prevent issues with users running in a headless environment.\n\n# 添加文件和文件夹\n\n在根文件夹中创建两个新文件夹:`/lib`和`/src`。JavaScript、HTML、CSS 和 Wasm 文件将位于`/src`文件夹中，而 C 文件将位于`/lib`中。我只想包括在`/src`的网络应用使用的文件。我们永远不会直接从应用中使用 C 文件，只会使用编译后的输出。\n\n将`/book-examples`项目中的`/.vscode`文件夹复制到根文件夹中。这将确保您使用现有的 C/C++ 设置，并为构建任务提供一个良好的起点。\n\nIf you're using macOS or Linux, you'll have to use the terminal to copy the folder; you can accomplish this by running the `cp -r command`.\n\n# 配置构建步骤\n\n我们需要修改`/.vscode/tasks.json`文件中的默认构建步骤，以适应我们更新的工作流程。我们在`/book-examples`项目中使用的构建步骤的参数允许我们编译编辑器中当前活动的任何文件。它还将`.wasm`文件输出到与 C 源文件相同的文件夹中。但是，这种配置对于这个项目没有意义。我们总是将输出到编译后的`.wasm`文件的同一个 C 文件编译到一个特定的文件夹中。为此，用以下内容更新`/.vscode/tasks.json`中`Build`任务中的`args`数组:\n\n```cpp\n\"args\": [\n  \"${workspaceFolder}/lib/main.c\",\n  \"-Os\",\n  \"-s\", \"WASM=1\",\n  \"-s\", \"SIDE_MODULE=1\",\n  \"-s\", \"BINARYEN_ASYNC_COMPILATION=0\",\n  \"-o\", \"${workspaceFolder}/src/img/main.wasm\"\n],\n```\n\n我们更改了输入和输出路径，这是`args`数组中的第一个和最后一个元素。现在两者都是静态路径，无论哪个文件在活动编辑器中打开，它们总是编译并输出相同的文件。\n\n# 设置模拟应用编程接口\n\n我们需要一些模拟数据和保持任何更新的方法。如果您将数据本地存储在 JSON 文件中，一旦刷新页面，您对事务所做的任何更改都将丢失。我们可以用像 Express 这样的库建立一个本地服务器，模拟一个数据库，编写路由，等等。相反，我们将利用在线提供的优秀开发工具。online too jsonstore.io 允许您为小型项目存储 JSON 数据，并提供开箱即用的端点。采取以下步骤启动并运行模拟应用编程接口:\n\n1.  导航至[https://www.jsonstore.io/](https://www.jsonstore.io/)，按复制按钮将端点复制到剪贴板；这是您将向其发出 HTTP 请求的端点。\n2.  转到[https://jsfiddle.net/mikerourke/cta0km6d](https://jsfiddle.net/mikerourke/cta0km6d)的 js 小提琴，将您的 jsonstore.io 端点粘贴到输入中，并按下填充数据按钮。\n3.  打开一个新的标签，在地址栏中粘贴你的 jsonstore.io 端点，并在 URL 的末尾添加`/transactions`，然后按*进入*。如果您在浏览器中看到 JSON 文件的内容，则表明 API 设置成功。\n\n将 jsonstore.io 端点放在手边——当我们构建应用的 JavaScript 部分时，您会需要它。\n\n# 下载 C 标准库 Wasm\n\n我们需要 C 的标准库中的`malloc()`和`free()`函数来实现 C 代码中的功能。WebAssembly 没有内置这些功能，所以我们需要提供自己的实现。\n\n幸运的是，有人已经为我们建造了它；我们只需要下载该模块并将其包含在实例化步骤中。该模块可从盖伊·贝德福德位于[https://github.com/guybedford/wasm-stdlib-hack](https://github.com/guybedford/wasm-stdlib-hack)的`wasm-stdlib-hack` GitHub 资源库下载。你需要`/dist`文件夹中的`memory.wasm`文件。文件下载后，在项目的`/src`文件夹中创建一个名为`/assets`的文件夹，并将`memory.wasm`文件复制到那里。\n\nYou can copy the `memory.wasm` file from the `/chapter-07-cook-the-books/src/assets` folder of the `learn-webassembly` repository instead of downloading it from GitHub.\n\n# 最终结果\n\n执行这些步骤后，您的项目应该如下所示:\n\n```cpp\n├── /.vscode\n│    ├── tasks.json\n│    └── c_cpp_properties.json\n├── /lib\n├── /src\n│    └── /assets\n│         └── memory.wasm\n├── package.json\n└── package-lock.json\n```\n\n# 构建 C 部分\n\n应用的 C 部分将汇总交易和类别金额。我们在 C 语言中执行的计算在 JavaScript 中同样容易完成，但是 WebAssembly 非常适合计算。我们将在[第 8 章](08.html)、*中更深入地探讨 C/C++ 更复杂的用法。用 Emscripten* 移植一个游戏，但是目前我们试图将我们的范围限制在*核心规范*的范围内。在本节中，我们将编写一些 C 代码来演示如何在不使用 Emscripten 的情况下将 WebAssembly 与 web 应用集成。\n\n# 概观\n\n我们将编写一些 C 函数来计算总计以及生交易和熟交易的期末余额。除了计算总计，我们还需要计算每个类别的总计，以显示在饼图中。所有这些计算都将在一个 C 文件中执行，并编译成一个 Wasm 文件，当应用加载时，该文件将被实例化。对于不熟悉的人来说，c 可能有点令人生畏，所以为了清晰起见，我们的代码会牺牲一些效率。我想花一点时间向读这本书的 C/C++ 程序员道歉；你不会喜欢你 c\n\n为了动态执行计算，我们需要在添加和删除事务时分配和释放内存。为此，我们将使用**双链表**。双向链表是一种数据结构，允许我们删除列表中的项目或*节点*，并根据需要添加和编辑节点。使用`malloc()`添加节点，使用`free()`移除节点，这两个都是由您在上一节下载的`memory.wasm`模块提供的。\n\n# 关于工作流程的说明\n\n从开发的角度来看，操作的顺序并不反映您通常如何构建使用 WebAssembly 的应用。工作流程将包括在 C/C++ 和 JavaScript 之间跳转，以达到预期的结果。在这种情况下，我们从 JavaScript 卸载到 WebAssembly 中的功能是已知的，所以我们将提前编写 C 代码。\n\n# c 文件内容\n\n让我们浏览一下 C 文件的每个部分。在名为`main.c`的`/lib`文件夹中创建一个文件，并在每个部分中填充以下内容。如果我们把 C 文件分成更小的块，就会更容易理解它发生了什么。让我们从*声明*部分开始。\n\n# 声明\n\n第一部分包含我们将用来创建和遍历双向链表的声明，如下所示:\n\n```cpp\n#include <stdlib.h>\n\nstruct Node {\n  int id;\n  int categoryId;\n  float rawAmount;\n  float cookedAmount;\n  struct Node *next;\n  struct Node *prev;\n};\n\ntypedef enum {\n  RAW = 1,\n  COOKED = 2\n} AmountType;\n\nstruct Node *transactionsHead = NULL;\nstruct Node *categoriesHead = NULL;\n```\n\n`Node`结构用于表示事务或类别。`transactionsHead`和`categoriesHead`节点实例代表我们将使用的每个链表中的第一个节点(一个用于事务，一个用于类别)。`AmountType``enum`不是必需的，但是我们将讨论当我们到达使用它的代码部分时它是如何有用的。\n\n# 链表操作\n\n第二部分包含用于在链表中添加和删除节点的两个函数:\n\n```cpp\nvoid deleteNode(struct Node **headNode, struct Node *delNode) {\n    // Base case:\n    if (*headNode == NULL || delNode == NULL) return;\n\n    // If node to be deleted is head node:\n    if (*headNode == delNode) *headNode = delNode->next;\n\n    // Change next only if node to be deleted is NOT the last node:\n    if (delNode->next != NULL) delNode->next->prev = delNode->prev;\n\n    // Change prev only if node to be deleted is NOT the first node:\n    if (delNode->prev != NULL) delNode->prev->next = delNode->next;\n\n    // Finally, free the memory occupied by delNode:\n    free(delNode);\n}\n\nvoid appendNode(struct Node **headNode, int id, int categoryId,\n                float rawAmount, float cookedAmount) {\n    // 1\\. Allocate node:\n    struct Node *newNode = (struct Node *) malloc(sizeof(struct Node));\n    struct Node *last = *headNode; // Used in Step 5\n\n    // 2\\. Populate with data:\n    newNode->id = id;\n    newNode->categoryId = categoryId;\n    newNode->rawAmount = rawAmount;\n    newNode->cookedAmount = cookedAmount;\n\n    // 3\\. This new node is going to be the last node, so make next NULL:\n    newNode->next = NULL;\n\n    // 4\\. If the linked list is empty, then make the new node as head:\n    if (*headNode == NULL) {\n        newNode->prev = NULL;\n        *headNode = newNode;\n        return;\n    }\n\n    // 5\\. Otherwise, traverse till the last node:\n    while (last->next != NULL) {\n        last = last->next;\n    }\n\n    // 6\\. Change the next of last node:\n    last->next = newNode;\n\n    // 7\\. Make last node as previous of new node:\n    newNode->prev = last;\n}\n```\n\n代码中的注释描述了每一步发生的事情。当我们需要向列表中添加一个节点时，我们必须使用`malloc()`分配`struct` `Node`占用的内存，并将其追加到链表中的最后一个节点。如果我们需要删除一个节点，我们必须从链表中删除它，并通过调用`free()`函数来释放该节点正在使用的内存。\n\n# 交易操作\n\n第三部分包含从`transactions`链表中添加、编辑和删除事务的功能，如下所示:\n\n```cpp\nstruct Node *findNodeById(int id, struct Node *withinNode) {\n    struct Node *node = withinNode;\n    while (node != NULL) {\n        if (node->id == id) return node;\n        node = node->next;\n    }\n    return NULL;\n}\n\nvoid addTransaction(int id, int categoryId, float rawAmount,\n                    float cookedAmount) {\n    appendNode(&transactionsHead, id, categoryId, rawAmount, cookedAmount);\n}\n\nvoid editTransaction(int id, int categoryId, float rawAmount,\n                     float cookedAmount) {\n    struct Node *foundNode = findNodeById(id, transactionsHead);\n    if (foundNode != NULL) {\n        foundNode->categoryId = categoryId;\n        foundNode->rawAmount = rawAmount;\n        foundNode->cookedAmount = cookedAmount;\n    }\n}\n\nvoid removeTransaction(int id) {\n    struct Node *foundNode = findNodeById(id, transactionsHead);\n    if (foundNode != NULL) deleteNode(&transactionsHead, foundNode);\n}\n```\n\n我们在前一节中回顾的`appendNode()`和`deleteNode()`函数并不打算从 JavaScript 代码中调用。相反，调用`addTransaction()`、`editTransaction()`和`removeTransaction()`来更新本地链表。`addTransaction()`函数调用`appendNode()`函数将作为参数传入的数据添加到本地链表中的新节点。`removeTransaction()`调用`deleteNode()`功能删除对应的交易节点。`findNodeById()`功能用于根据指定的标识确定链表中哪个节点需要更新或删除。\n\n# 交易计算\n\n第四部分包含计算生熟`transactions`总计和最终余额的函数，如下所示:\n\n```cpp\nvoid calculateGrandTotals(float *totalRaw, float *totalCooked) {\n    struct Node *node = transactionsHead;\n    while (node != NULL) {\n        *totalRaw += node->rawAmount;\n        *totalCooked += node->cookedAmount;\n        node = node->next;\n    }\n}\n\nfloat getGrandTotalForType(AmountType type) {\n    float totalRaw = 0;\n    float totalCooked = 0;\n    calculateGrandTotals(&totalRaw, &totalCooked);\n\n    if (type == RAW) return totalRaw;\n    if (type == COOKED) return totalCooked;\n    return 0;\n}\n\nfloat getFinalBalanceForType(AmountType type, float initialBalance) {\n    float totalForType = getGrandTotalForType(type);\n    return initialBalance + totalForType;\n}\n```\n\n我们在声明部分声明的`AmountType enum`在这里用来避免**幻数**。很容易记住`1`代表原始交易，`2`代表熟交易。生熟交易的总计都是在`calculateGrandTotals()`函数中计算的，尽管我们在`getGrandTotalForType()`中只要求一种类型。由于我们只能从一个 Wasm 函数中返回一个值，所以当我们为原始和熟事务调用`getGrandTotalForType()`时，我们最终会在所有事务中循环两次。交易量相对较少，计算简单，这不会带来任何问题。`getFinalBalanceForType()`返回总计加上指定的`initialBalance`。当我们在 web 应用中添加更改初始余额的功能时，您会看到这一点。\n\n# 类别计算\n\n第五部分也是最后一部分包含按类别计算总数的函数，我们将在饼图中使用这些函数，如下所示:\n\n```cpp\nvoid upsertCategoryNode(int categoryId, float transactionRaw,\n                        float transactionCooked) {\n    struct Node *foundNode = findNodeById(categoryId, categoriesHead);\n    if (foundNode != NULL) {\n        foundNode->rawAmount += transactionRaw;\n        foundNode->cookedAmount += transactionCooked;\n    } else {\n        appendNode(&categoriesHead, categoryId, categoryId, transactionRaw,\n                   transactionCooked);\n    }\n}\n\nvoid buildValuesByCategoryList() {\n    struct Node *node = transactionsHead;\n    while (node != NULL) {\n        upsertCategoryNode(node->categoryId, node->rawAmount,\n                           node->cookedAmount);\n        node = node->next;\n    }\n}\n\nvoid recalculateForCategories() {\n    categoriesHead = NULL;\n    buildValuesByCategoryList();\n}\n\nfloat getCategoryTotal(AmountType type, int categoryId) {\n    // Ensure the category totals have been calculated:\n    if (categoriesHead == NULL) buildValuesByCategoryList();\n\n    struct Node *categoryNode = findNodeById(categoryId, categoriesHead);\n    if (categoryNode == NULL) return 0;\n\n    if (type == RAW) return categoryNode->rawAmount;\n    if (type == COOKED) return categoryNode->cookedAmount;\n    return 0;\n}\n```\n\n每当调用`recalculateForCategories()`或`getCategoryTotal()`函数时，都会调用`buildValuesByCategoryList()`函数。该功能循环遍历`transactions`链表中的所有交易，并在单独的链表中为每个对应的类别创建一个节点，其中包含合计的原始金额和总金额。`upsertCategoryNode()`功能在`categories`链表中查找对应于`categoryId`的节点。如果找到它，原始和熟交易金额被添加到该节点上的现有金额，否则为所述类别创建新节点。调用`recalculateForCategories()`函数以确保类别总数随着任何交易的变化而更新。\n\n# 编译到 Wasm\n\n填充文件后，我们需要将其编译成 Wasm，以便在应用的 JavaScript 部分使用。通过选择任务|运行生成任务来运行生成任务...从菜单或使用键盘快捷键*Cmd*/*Ctrl*+*Shift*+*B*。如果构建成功，您将在`/src/assets`文件夹中看到一个名为`main.wasm`的文件。如果出现错误，终端应该提供如何解决的细节。\n\n如果您没有使用 VS 代码，请在`/cook-the-books`文件夹中打开一个终端实例并运行以下命令:\n\n```cpp\nemcc lib/main.c -Os -s WASM=1 -s SIDE_MODULE=1 -s BINARYEN_ASYNC_COMPILATION=0 -o src/img/main.wasm\n```\n\nC 代码到此为止。让我们继续讨论 JavaScript 部分。\n\n# 构建 JavaScript 部分\n\n应用的 JavaScript 部分向用户呈现事务数据，并允许他们轻松添加、编辑和删除事务。该应用被分割成几个文件，以简化开发过程，并使用本章*中描述的库。在本节中，我们将逐步构建应用，从 API 和全局状态交互层开始。我们将编写函数来实例化和交互我们的 Wasm 模块，并查看构建用户界面所需的 Vue 组件。*\n\n# 概观\n\n应用被分解成多个上下文，以简化开发过程。我们将自下而上地构建应用，以确保在编写代码时不必在不同的上下文之间来回切换。我们将从 Wasm 交互代码开始，然后进入全局存储和 API 交互。我将描述每个 Vue 组件的用途，但源代码将只提供给选定的几个。如果您正在跟进并希望在本地运行应用，您需要将`learn-webassembly`存储库中的`/chapter-07-cook-the-books`文件夹中的`/src/components`文件夹复制到项目的`/src`文件夹中。\n\n# 关于浏览器兼容性的说明\n\n在我们开始编写任何代码之前，您必须确保您的浏览器支持我们将在应用中使用的较新的 JavaScript 功能。您的浏览器必须支持专家系统模块(`import`和`export`)、提取应用编程接口和`async` / `await`。你至少需要 61 版的谷歌 Chrome 或者 60 版的火狐。您可以通过从菜单栏中选择关于 Chrome 或关于火狐来检查您当前使用的版本。我目前正在运行 Chrome 版本 67 和火狐版本 61 的应用，没有任何问题。\n\n# 在 initializeWasm.js 中创建 Wasm 实例\n\n您应该在项目的`/src/assets`文件夹中有两个已编译的 Wasm 文件:`main.wasm`和`memory.wasm`。由于我们需要利用从`main.wasm`代码中的`memory.wasm`导出的`malloc()`和`free()`功能，我们的加载代码看起来将与前面的示例不同。在名为`initializeWasm.js`的`/src/store`文件夹中创建一个文件，并用以下内容填充:\n\n```cpp\n/**\n * Returns an array of compiled (not instantiated!) Wasm modules.\n * We need the main.wasm file we created, as well as the memory.wasm file\n * that allows us to use C functions like malloc() and free().\n */\nconst fetchAndCompileModules = () =>\n  Promise.all(\n    ['../img/main.wasm', '../img/memory.wasm'].map(fileName =>\n      fetch(fileName)\n        .then(response => {\n          if (response.ok) return response.arrayBuffer();\n          throw new Error(`Unable to fetch WebAssembly file: ${fileName}`);\n        })\n        .then(bytes => WebAssembly.compile(bytes))\n    )\n  );\n\n/**\n * Returns an instance of the compiled \"main.wasm\" file.\n */\nconst instantiateMain = (compiledMain, memoryInstance, wasmMemory) => {\n  const memoryMethods = memoryInstance.exports;\n  return WebAssembly.instantiate(compiledMain, {\n    env: {\n      memoryBase: 0,\n      tableBase: 0,\n      memory: wasmMemory,\n      table: new WebAssembly.Table({ initial: 16, element: 'anyfunc' }),\n      abort: console.log,\n      _consoleLog: value => console.log(value),\n      _malloc: memoryMethods.malloc,\n      _free: memoryMethods.free\n    }\n  });\n};\n\n/**\n * Compiles and instantiates the \"memory.wasm\" and \"main.wasm\" files and\n * returns the `exports` property from main's `instance`.\n */\nexport default async function initializeWasm() {\n  const wasmMemory = new WebAssembly.Memory({ initial: 1024 });\n  const [compiledMain, compiledMemory] = await fetchAndCompileModules();\n\n  const memoryInstance = await WebAssembly.instantiate(compiledMemory, {\n    env: {\n      memory: wasmMemory\n    }\n  });\n\n  const mainInstance = await instantiateMain(\n    compiledMain,\n    memoryInstance,\n    wasmMemory\n  );\n\n  return mainInstance.exports;\n}\n```\n\n文件的默认`export`功能`initializeWasm()`执行以下步骤:\n\n1.  创建新的`WebAssembly.Memory`实例(`wasmMemory`)。\n2.  调用`fetchAndCompileModules()`函数获取`memory.wasm` ( `compiledMemory`)和`main.wasm` ( `compiledMain`)的`WebAssembly.Module`实例。\n3.  实例化`compiledMemory` ( `memoryInstance`)并将`wasmMemory`传入`importObj`。\n4.  将`compiledMain`、`memoryInstance`、`wasmMemory`传入`instantiateMain()`功能。\n5.  实例化`compiledMain`并将`memoryInstance`和`wasmMemory`导出的`malloc()`和`free()`功能传入`importObj`。\n6.  归还从`instantiateMain` ( `mainInstance`)归还的`Instance`的`exports`财产。\n\n如您所见，当您在 Wasm 模块中有依赖关系时，这个过程会更加复杂。\n\nYou may have noticed that the `malloc` and `free` methods on the `memoryInstance` `exports` property weren't prefixed with an underscore. This is because the `memory.wasm` file was compiled using LLVM without Emscripten, which doesn't add the `_`.\n\n# 在 WasmTransactions.js 中与 Wasm 交互\n\n我们将使用 JavaScript 的`class`语法来创建一个封装 Wasm 交互函数的包装器。这使得我们可以快速地对 C 代码进行修改，而不必搜索整个应用来寻找 Wasm 函数被调用的地方。如果你在 C 文件中重命名一个方法，你只需要重命名一个地方。在名为`WasmTransactions.js`的`/src/store`文件夹中创建新文件，并使用以下内容填充该文件:\n\n```cpp\nimport initializeWasm from './initializeWasm.js';\n\n/**\n * Class used to wrap the functionality from the Wasm module (rather\n * than access it directly from the Vue components or store).\n * @class\n */\nexport default class WasmTransactions {\n  constructor() {\n    this.instance = null;\n    this.categories = [];\n  }\n\n  async initialize() {\n    this.instance = await initializeWasm();\n    return this;\n  }\n\n  getCategoryId(category) {\n    return this.categories.indexOf(category);\n  }\n\n  // Ensures the raw and cooked amounts have the proper sign (withdrawals\n  // are negative and deposits are positive).\n  getValidAmounts(transaction) {\n    const { rawAmount, cookedAmount, type } = transaction;\n    const getAmount = amount =>\n      type === 'Withdrawal' ? -Math.abs(amount) : amount;\n    return {\n      validRaw: getAmount(rawAmount),\n      validCooked: getAmount(cookedAmount)\n    };\n  }\n\n  // Adds the specified transaction to the linked list in the Wasm module.\n  addToWasm(transaction) {\n    const { id, category } = transaction;\n    const { validRaw, validCooked } = this.getValidAmounts(transaction);\n    const categoryId = this.getCategoryId(category);\n    this.instance._addTransaction(id, categoryId, validRaw, validCooked);\n  }\n\n  // Updates the transaction node in the Wasm module:\n  editInWasm(transaction) {\n    const { id, category } = transaction;\n    const { validRaw, validCooked } = this.getValidAmounts(transaction);\n    const categoryId = this.getCategoryId(category);\n    this.instance._editTransaction(id, categoryId, validRaw, validCooked);\n  }\n\n  // Removes the transaction node from the linked list in the Wasm module:\n  removeFromWasm(transactionId) {\n    this.instance._removeTransaction(transactionId);\n  }\n\n  // Populates the linked list in the Wasm module. The categories are\n  // needed to set the categoryId in the Wasm module.\n  populateInWasm(transactions, categories) {\n    this.categories = categories;\n    transactions.forEach(transaction => this.addToWasm(transaction));\n  }\n\n  // Returns the balance for raw and cooked transactions based on the\n  // specified initial balances.\n  getCurrentBalances(initialRaw, initialCooked) {\n    const currentRaw = this.instance._getFinalBalanceForType(\n      AMOUNT_TYPE.raw,\n      initialRaw\n    );\n    const currentCooked = this.instance._getFinalBalanceForType(\n      AMOUNT_TYPE.cooked,\n      initialCooked\n    );\n    return { currentRaw, currentCooked };\n  }\n\n  // Returns an object that has category totals for all income (deposit)\n  // and expense (withdrawal) transactions.\n  getCategoryTotals() {\n    // This is done to ensure the totals reflect the most recent\n    // transactions:\n    this.instance._recalculateForCategories();\n    const categoryTotals = this.categories.map((category, idx) => ({\n      category,\n      id: idx,\n      rawTotal: this.instance._getCategoryTotal(AMOUNT_TYPE.raw, idx),\n      cookedTotal: this.instance._getCategoryTotal(AMOUNT_TYPE.cooked, idx)\n    }));\n\n    const totalsByGroup = { income: [], expenses: [] };\n    categoryTotals.forEach(categoryTotal => {\n      if (categoryTotal.rawTotal < 0) {\n        totalsByGroup.expenses.push(categoryTotal);\n      } else {\n        totalsByGroup.income.push(categoryTotal);\n      }\n    });\n    return totalsByGroup;\n  }\n}\n```\n\n当在类的实例上调用`initialize()`函数时，`initializeWasm()`函数的返回值被分配给类的`instance`属性。`class`方法从`this.instance`调用函数，如果适用，返回所需的结果。注意`getCurrentBalances()`和`getCategoryTotals()`功能中引用的`AMOUNT_TYPE`对象。这对应于我们 C 文件中的`AmountType enum`。`AMOUNT_TYPE`对象在加载应用的`/src/main.js`文件中全局声明。现在我们已经编写了 Wasm 交互代码，让我们继续讨论 API 交互代码。\n\n# 利用 api.js 中的 API\n\n该应用编程接口以在提取调用中定义的 HTTP 方法的形式提供了添加、编辑、删除和查询事务的方法。为了简化执行这些动作的过程，我们将编写一些 API `wrapper`函数。在名为`api.js`的`/src/store`文件夹中创建一个文件，并用以下内容填充:\n\n```cpp\n// Paste your jsonstore.io endpoint here (no ending slash):\nconst API_URL = '[JSONSTORE.IO ENDPOINT]';\n\n/**\n * Wrapper for performing API calls. We don't want to call response.json()\n * each time we make a fetch call.\n * @param {string} endpoint Endpoint (e.g. \"/transactions\" to make API call to\n * @param {Object} init Fetch options object containing any custom settings\n * @returns {Promise<*>}\n * @see https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch\n */\nconst performApiFetch = (endpoint = '', init = {}) =>\n  fetch(`${API_URL}${endpoint}`, {\n    headers: {\n      'Content-type': 'application/json'\n    },\n    ...init\n  }).then(response => response.json());\n\nexport const apiFetchTransactions = () =>\n  performApiFetch('/transactions').then(({ result }) =>\n    /*\n     * The response object looks like this:\n     * {\n     *   \"result\": {\n     *     \"1\": {\n     *       \"category\": \"Sales Revenue\",\n     *       ...\n     *     },\n     *     \"2\": {\n     *       \"category\": \"Hotels\",\n     *       ...\n     *     },\n     *     ...\n     *   }\n     * }\n     * We need the \"1\" and \"2\" values for deleting or editing existing\n     * records, so we store that in the transaction record as \"apiId\".\n     */\n    Object.keys(result).map(apiId => ({\n      ...result[apiId],\n      apiId\n    }))\n  );\n\nexport const apiEditTransaction = transaction =>\n  performApiFetch(`/transactions/${transaction.apiId}`, {\n    method: 'POST',\n    body: JSON.stringify(transaction)\n  });\n\nexport const apiRemoveTransaction = transaction =>\n  performApiFetch(`/transactions/${transaction.apiId}`, {\n    method: 'DELETE'\n  });\n\nexport const apiAddTransaction = transaction =>\n  performApiFetch(`/transactions/${transaction.apiId}`, {\n    method: 'POST',\n    body: JSON.stringify(transaction)\n  });\n```\n\n您将需要在*设置项目*部分创建的 jsonstore.io 端点，以便与应用编程接口交互。将`[JSONSTORE.IO ENDPOINT]`替换为您的 jsonstore.io 端点。确保端点不以正斜杠或单词 transactions 结尾。\n\n# 管理存储中的全局状态\n\n管理应用中全局状态的文件有许多移动部分。因此，我们将把代码分解成更小的块，并逐个遍历每个部分。在名为`store.js`的`/src/store`文件夹中创建一个文件，并用以下每个部分的内容填充它。\n\n# 导入和存储声明\n\n第一部分包含`import`语句以及导出的`store`对象的`wasm`和`state`属性，如下所示:\n\n```cpp\nimport {\n  apiFetchTransactions,\n  apiAddTransaction,\n  apiEditTransaction,\n  apiRemoveTransaction\n} from './api.js';\nimport WasmTransactions from './WasmTransactions.js';\n\nexport const store = {\n  wasm: null,\n  state: {\n    transactions: [],\n    activeTransactionId: 0,\n    balances: {\n      initialRaw: 0,\n      currentRaw: 0,\n      initialCooked: 0,\n      currentCooked: 0\n    }\n  },\n  ...\n```\n\n所有 API 交互仅限于`store.js`文件。因为我们需要操作、添加和搜索事务，所以从`api.js`导出的所有功能都是导入的。`store`对象在`wasm`属性中持有`WasmTransactions`实例，在`state`属性中持有初始状态。`state`中的值在整个应用中的多个位置被引用。当应用加载时，`store`对象将被添加到全局`window`对象中，因此所有组件都可以访问全局状态。\n\n# 交易操作\n\n第二部分包含管理 Wasm 实例(通过`WasmTransactions`实例)和 API 中事务的函数，如下所示:\n\n```cpp\n...\n  getCategories() {\n    const categories = this.state.transactions.map(\n      ({ category }) => category\n    );\n    // Remove duplicate categories and sort the names in ascending order:\n    return _.uniq(categories).sort();\n  },\n\n  // Populate global state with the transactions from the API response:\n  populateTransactions(transactions) {\n    const sortedTransactions = _.sortBy(transactions, [\n      'transactionDate',\n      'id'\n    ]);\n    this.state.transactions = sortedTransactions;\n    store.wasm.populateInWasm(sortedTransactions, this.getCategories());\n    this.recalculateBalances();\n  },\n\n  addTransaction(newTransaction) {\n    // We need to assign a new ID to the transaction, so this just adds\n    // 1 to the current maximum transaction ID:\n    newTransaction.id = _.maxBy(this.state.transactions, 'id').id + 1;\n    store.wasm.addToWasm(newTransaction);\n    apiAddTransaction(newTransaction).then(() => {\n      this.state.transactions.push(newTransaction);\n      this.hideTransactionModal();\n    });\n  },\n\n  editTransaction(editedTransaction) {\n    store.wasm.editInWasm(editedTransaction);\n    apiEditTransaction(editedTransaction).then(() => {\n      this.state.transactions = this.state.transactions.map(\n        transaction => {\n          if (transaction.id === editedTransaction.id) {\n            return editedTransaction;\n          }\n          return transaction;\n        }\n      );\n      this.hideTransactionModal();\n    });\n  },\n\n  removeTransaction(transaction) {\n    const transactionId = transaction.id;\n    store.wasm.removeFromWasm(transactionId);\n\n    // We're passing the whole transaction record into the API call\n    // for the sake of consistency:\n    apiRemoveTransaction(transaction).then(() => {\n      this.state.transactions = this.state.transactions.filter(\n        ({ id }) => id !== transactionId\n      );\n      this.hideTransactionModal();\n    });\n  },\n...\n```\n\n`populateTransactions()`函数从应用编程接口获取所有事务，并将它们加载到全局状态和 Wasm 实例中。类别名称是从`getCategories()`函数中的`transactions`数组推断出来的。当调用`store.wasm.populateInWasm()`时，结果被传递给`WasmTransactions`实例。\n\n`addTransaction()`、`editTransaction()`和`removeTransaction()`功能执行与其名称相对应的动作。这三个函数都操作 Wasm 实例，并通过一个提取调用更新应用编程接口上的数据。每个函数都调用`this.hideTransactionModal()`，因为对事务的更改只能通过`TransactionModal`组件进行。一旦更改成功，模式应该关闭。接下来看看`TransactionModal`管理代码。\n\n# 交易模式管理\n\n第三部分包含管理`TransactionModal`组件(位于`/src/components/TransactionsTab/TransactionModal.js`中)的可见性和内容的功能，如下所示:\n\n```cpp\n...\n  showTransactionModal(transactionId) {\n    this.state.activeTransactionId = transactionId || 0;\n    const transactModal = document.querySelector('#transactionModal');\n    UIkit.modal(transactModal).show();\n  },\n\n  hideTransactionModal() {\n    this.state.activeTransactionId = 0;\n    const transactModal = document.querySelector('#transactionModal');\n    UIkit.modal(transactModal).hide();\n  },\n\n  getActiveTransaction() {\n    const { transactions, activeTransactionId } = this.state;\n    const foundTransaction = transactions.find(transaction =>\n      transaction.id === activeTransactionId);\n    return foundTransaction || { id: 0 };\n  },\n...\n```\n\n`showTransactionModal()`和`hideTransactionModal()`功能应该是不言自明的。在表示`TransactionModal`的 DOM 元素上调用`UIkit.modal()`的`hide()`或`show()`方法。`getActiveTransaction()`功能返回全局状态下与`activeTransactionId`值相关联的交易记录。\n\n# 余额计算\n\n第四部分包含计算和更新全局状态下的余额对象的函数:\n\n```cpp\n...\n  updateInitialBalance(amount, fieldName) {\n    this.state.balances[fieldName] = amount;\n  },\n\n  // Update the \"balances\" object in global state based on the current\n  // initial balances:\n  recalculateBalances() {\n    const { initialRaw, initialCooked } = this.state.balances;\n    const { currentRaw, currentCooked } = this.wasm.getCurrentBalances(\n      initialRaw,\n      initialCooked\n    );\n    this.state.balances = {\n      initialRaw,\n      currentRaw,\n      initialCooked,\n      currentCooked\n    };\n  }\n};\n```\n\n`updateInitialBalance()`函数根据`amount`和`fieldName`参数设置全局状态下`balances`对象的属性值。`recalculateBalances()`功能更新`balances`对象上的所有字段，以反映对初始余额或交易所做的任何更改。\n\n# 存储初始化\n\n文件中的最后一段代码初始化存储:\n\n```cpp\n/**\n * This function instantiates the Wasm module, fetches the transactions\n * from the API endpoint, and loads them into state and the Wasm\n * instance.\n */\nexport const initializeStore = async () => {\n  const wasmTransactions = new WasmTransactions();\n  store.wasm = await wasmTransactions.initialize();\n  const transactions = await apiFetchTransactions();\n  store.populateTransactions(transactions);\n};\n```\n\n`initializeStore()`函数实例化 Wasm 模块，从 API 获取所有事务，并填充状态的内容。这个函数是从`/src/main.js`中的应用加载代码中调用的，我们将在下一节中介绍。\n\n# 在 main.js 中加载应用\n\n我们需要一个入口点来加载我们的应用。在名为`main.js`的`/src`文件夹中创建一个文件，并用以下内容填充:\n\n```cpp\nimport App from './components/App.js';\nimport { store, initializeStore } from './store/store.js';\n\n// This allows us to use the <vue-numeric> component globally:\nVue.use(VueNumeric.default);\n\n// Create a globally accessible store (without having to pass it down\n// as props):\nwindow.$store = store;\n\n// Since we can only pass numbers into a Wasm function, these flags\n// represent the amount type we're trying to calculate:\nwindow.AMOUNT_TYPE = {\n  raw: 1,\n  cooked: 2\n};\n\n// After fetching the transactions and initializing the Wasm module,\n// render the app.\ninitializeStore()\n  .then(() => {\n    new Vue({ render: h => h(App), el: '#app' });\n  })\n  .catch(err => {\n    console.error(err);\n  });\n```\n\n在`/src/index.html`中从 cdn 中提取并加载库后，加载该文件。我们使用全局`Vue`对象来指定我们想要使用`VueNumeric`组件。我们将从`/store/store.js`导出的`store`对象添加到`window`作为`$store`。这不是最可靠的解决方案，但考虑到应用范围，这已经足够了。如果您正在创建一个生产应用，您将使用像 **Vuex** 或 **Redux** 这样的库来进行全局状态管理。为了保持简单，我们将放弃这种方法。\n\n我们还将`AMOUNT_TYPE`添加到了`window`对象中。这样做是为了确保整个应用可以引用`AMOUNT_TYPE`值，而不是指定一个神奇的数字。给`window`赋值后，调用`initializeStore()`函数。如果成功触发`initializeStore()`功能，将创建一个新的`Vue`实例来呈现应用。接下来，让我们添加网络资产，然后进入 Vue 组件。\n\n# 添加网络资产\n\n在我们开始向应用添加 Vue 组件之前，让我们创建包含标记和样式的 HTML 和 CSS 文件。在名为`index.html`的`/src`文件夹中创建一个文件，并用以下内容填充:\n\n```cpp\n<!doctype html>\n<html lang=\"en-us\">\n<head>\n  <title>Cook the Books</title>\n  <link\n    rel=\"stylesheet\"\n    type=\"text/css\"\n    href=\"https://cdnjs.cloudflare.com/ajax/libs/uikit/3.0.0-rc.6/css/uikit.min.css\"\n  />\n  <link rel=\"stylesheet\" type=\"text/css\" href=\"styles.css\" />\n  <script src=\"https://cdnjs.cloudflare.com/ajax/libs/uikit/3.0.0-rc.6/js/uikit.min.js\"></script>\n  <script src=\"https://cdnjs.cloudflare.com/ajax/libs/uikit/3.0.0-rc.6/js/uikit-icons.min.js\"></script>\n  <script src=\"https://unpkg.com/accounting-js@1.1.1/dist/accounting.umd.js\"></script>\n  <script src=\"https://unpkg.com/lodash@4.17.10/lodash.min.js\"></script>\n  <script src=\"https://unpkg.com/d3@5.5.0/dist/d3.min.js\"></script>\n  <script src=\"https://unpkg.com/vue@2.5.16/dist/vue.min.js\"></script>\n  <script src=\"https://unpkg.com/vue-numeric@2.3.0/dist/vue-numeric.min.js\"></script>\n  <script src=\"main.js\" type=\"module\"></script>\n</head>\n<body>\n  <div id=\"app\"></div>\n</body>\n</html>\n```\n\n我们只使用 HTML 文件从 cdn 中获取库，指定一个 Vue 可以渲染到的`<div>`，并加载`main.js`来启动应用。注意最后一个`<script>`元素的`type=\"module\"`属性。这允许我们在整个应用中使用专家系统模块。现在让我们添加 CSS 文件。在名为`styles.css`的`/src`文件夹中创建一个文件，并用以下内容填充:\n\n```cpp\n@import url(\"https://fonts.googleapis.com/css?family=Quicksand\");\n\n:root {\n  --blue: #2889ed;\n}\n\n* {\n  font-family: \"Quicksand\", Helvetica, Arial, sans-serif !important;\n}\n\n#app {\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\n.addTransactionButton {\n  color: white;\n  height: 64px;\n  width: 64px;\n  background: var(--blue);\n  position: fixed;\n  bottom: 24px;\n  right: 24px;\n}\n\n.addTransactionButton:hover {\n  color: white;\n  background-color: var(--blue);\n  opacity: .6;\n}\n\n.errorText {\n  color: white;\n  font-size: 36px;\n}\n\n.appHeader {\n  height: 80px;\n  margin: 0;\n}\n\n.balanceEntry {\n  font-size: 2rem;\n}\n\n.tableAmount {\n  white-space: pre;\n}\n```\n\n这个文件只有几个类，因为大多数样式都是在组件级别处理的。在下一节中，我们将回顾组成我们应用的 Vue 组件。\n\n# 创建 Vue 组件\n\n使用 Vue，我们可以创建封装自己功能的独立组件，然后组合这些组件来构建应用。这使得调试、可扩展性和变更管理比将应用存储在单个整体文件中容易得多。\n\n该应用使用每个文件一个组件的开发方法。在我们开始查看组件文件之前，让我们看看成品。下面是选择了 TRANSACTIONS 选项卡的应用的屏幕截图:\n\n![](img/5fd7ee43-480d-4d13-8b0c-370bd2f851ba.png)\n\nRunning the application with TRANSACTIONS tab visible\n\n下面是选择了 CHARTS 选项卡的应用的屏幕截图:\n\n![](img/593e5d7b-16ae-416e-91c6-a44a890d495c.png)\n\nRunning the application with the CHARTS tab visible\n\n# Vue 组件的结构\n\nVue 组件只是一个带有导出对象的文件，该对象包含定义该组件外观和行为的属性。属性的名称必须符合 Vue 应用编程接口。你可以在[https://vuejs.org/v2/api](https://vuejs.org/v2/api)阅读到这些属性和 Vue API 的其他方面。下面的代码表示一个包含此应用中使用的 Vue API 元素的示例组件:\n\n```cpp\nimport SomeComponent from './SomeComponent.js';\n\nexport default {\n  name: 'dummy-component',\n\n  // Props passed from other components:\n  props: {\n    label: String,\n  },\n\n  // Other Vue components to render within the template:\n  components: {\n    SomeComponent\n  },\n\n  // Used to store local data/state:\n  data() {\n    return {\n      amount: 0\n    }\n  },\n\n  // Used to store complex logic that outside of the `template`:\n  computed: {\n    negativeClass() {\n      return {\n        'negative': this.amount < 0\n      };\n    }\n  },\n\n  // Methods that can be performed within the component:\n  methods: {\n    addOne() {\n      this.amount += 1;\n    }\n  },\n\n  // Perform actions if the local data changes:\n  watch: {\n    amount(val, oldVal) {\n      console.log(`New: ${val} | Old: ${oldVal}`);\n    }\n  },\n\n  // Contains the HTML to render the component:\n  template: `\n    <div>\n      <some-component></some-component>\n      <label for=\"someAmount\">{{ label }}</label>\n      <input\n        id=\"someAmount\"\n        :class=\"negativeClass\"\n        v-model=\"amount\"\n        type=\"number\"\n      />\n      <button @click=\"addOne\">Add One</button>\n    </div>\n  `\n};\n```\n\n每个属性上面的注释描述了它的用途，尽管级别很高。让我们通过查看`App`组件来看看 Vue 的运行情况。\n\n# 应用组件\n\n`App`组件是渲染应用中所有子组件的基础组件。我们将简要回顾一下`App`组件的代码，以便更好地理解 Vue。接下来，我们将描述每个剩余组件所扮演的角色，但仅回顾相应代码的部分。位于`/src/components/App.js`的`App`组件文件的内容如下所示:\n\n```cpp\nimport BalancesBar from './BalancesBar/BalancesBar.js';\nimport ChartsTab from './ChartsTab/ChartsTab.js';\nimport TransactionsTab from './TransactionsTab/TransactionsTab.js';\n\n/**\n * This component is the entry point for the application. It contains the\n * header, tabs, and content.\n */\nexport default {\n  name: 'app',\n  components: {\n    BalancesBar,\n    ChartsTab,\n    TransactionsTab\n  },\n  data() {\n    return {\n      balances: $store.state.balances,\n      activeTab: 0\n    };\n  },\n  methods: {\n    // Any time a transaction is added, edited, or removed, we need to\n    // ensure the balance is updated:\n    onTransactionChange() {\n      $store.recalculateBalances();\n      this.balances = $store.state.balances;\n    },\n\n    // When the \"Charts\" tab is activated, this ensures that the charts\n    // get automatically updated:\n    onTabClick(event) {\n      this.activeTab = +event.target.dataset.tab;\n    }\n  },\n  template: `\n    <div>\n      <div class=\"appHeader uk-background-primary uk-flex uk-flex-middle\">\n        <h2 class=\"uk-light uk-margin-remove-bottom uk-margin-left\">\n          Cook the Books\n        </h2>\n      </div>\n      <div class=\"uk-position-relative\">\n        <ul uk-tab class=\"uk-margin-small-bottom uk-margin-top\">\n          <li class=\"uk-margin-small-left\">\n            <a href=\"#\" data-tab=\"0\" @click=\"onTabClick\">Transactions</a>\n          </li>\n          <li>\n            <a href=\"#\" data-tab=\"1\" @click=\"onTabClick\">Charts</a>\n          </li>\n        </ul>\n        <balances-bar\n          :balances=\"balances\"\n          :onTransactionChange=\"onTransactionChange\">\n        </balances-bar>\n        <ul class=\"uk-switcher\">\n          <li>\n            <transactions-tab :onTransactionChange=\"onTransactionChange\">\n            </transactions-tab>\n          </li>\n          <li>\n            <charts-tab :isActive=\"this.activeTab === 1\"></charts-tab>\n          </li>\n        </ul>\n      </div>\n    </div>\n  `\n};\n```\n\n我们使用`components`属性来指定我们将在`template`中为`App`组件渲染的其他 Vue 组件。返回本地状态的`data()`功能用于跟踪余额和哪个选项卡处于活动状态(TRANSACTIONS 或 CHARTS)。`methods`属性包含两个功能:`onTransactionChange()`和`onTabClick()`。如果交易记录发生变化，`onTransactionChange()`函数调用`$store.recalculateBalances()`并在本地状态下更新`balances`。`onTabClick()`功能将本地状态下`activeTab`的值更改为被点击标签的`data-tab`属性。最后，`template`属性包含用于呈现组件的标记。\n\nIf you're not using single file components in Vue (`.vue` extension), you need to convert the component name to kebab case in the template property. For example, in the `App` component shown earlier, `BalancesBar` was changed to `<balances-bar>` in the `template`.\n\n# 平衡杆\n\n`/components/BalancesBar`文件夹包含两个组成文件:`BalanceCard.js`和`BalancesBar.js`。`BalancesBar`组件跨“交易”和“图表”选项卡存在，并直接位于选项卡控件下。它包含四种`BalanceCard`成分，每种平衡类型一种:初始生、当前生、初始熟和当前熟。代表初始余额的第一张和第三张卡包含输入，因此可以更改余额。代表当前余额的第二张和第四张卡在 Wasm 模块中动态计算(使用`getFinalBalanceForType()`功能)。以下片段取自`BalancesBar`组件，演示了 Vue 的绑定语法:\n\n```cpp\n<balance-card\n  title=\"Initial Raw Balance\"\n  :value=\"balances.initialRaw\"\n  :onChange=\"amount => onBalanceChange(amount, 'initialRaw')\">\n</balance-card>\n```\n\n`value`和`onChange`属性前面的`:`表示这些属性绑定到 Vue 组件。如果`balances.initialRaw`的值发生变化，`BalanceCard`中显示的值也会更新。该卡的`onBalanceChange()`功能在全局状态下更新`balances.initialRaw`的值。\n\n# TransactionsTab\n\n`/components/TransactionsTab`文件夹包含以下四个组成文件:\n\n*   `ConfirmationModal.js`\n*   `TransactionModal.js`\n*   `TransactionsTab.js`\n*   `TransactionsTable.js`\n\n`TransactionsTab`组件包含`TransactionsTable`和`TransactionsModal`组件，以及用于添加新交易的按钮。通过`TransactionModal`组件进行更改和添加。`TransactionsTable`包含所有当前交易，每行都有按钮，用于编辑或删除交易。如果用户按下删除按钮，`ConfirmationModal`组件出现并提示用户继续。如果用户按“是”，交易将被删除。以下片段取自`TransactionsTable`组件中的`methods`属性，演示了显示值的格式:\n\n```cpp\ngetFormattedTransactions() {\n  const getDisplayAmount = (type, amount) => {\n    if (amount === 0) return accounting.formatMoney(amount);\n    return accounting.formatMoney(amount, {\n      format: { pos: '%s %v', neg: '%s (%v)' }\n    });\n  };\n\n  const getDisplayDate = transactionDate => {\n    if (!transactionDate) return '';\n    const parsedTime = d3.timeParse('%Y-%m-%d')(transactionDate);\n    return d3.timeFormat('%m/%d/%Y')(parsedTime);\n  };\n\n  return $store.state.transactions.map(\n    ({\n      type,\n      rawAmount,\n      cookedAmount,\n      transactionDate,\n      ...transaction\n    }) => ({\n      ...transaction,\n      type,\n      rawAmount: getDisplayAmount(type, rawAmount),\n      cookedAmount: getDisplayAmount(type, cookedAmount),\n      transactionDate: getDisplayDate(transactionDate)\n    })\n  );\n}\n```\n\n前面显示的`getFormattedTransactions()`功能对每个`transaction`记录中的`rawAmount`、`cookedAmount`和`transactionDate`字段应用格式。这样做是为了确保显示的值包括一个美元符号(金额)，并以用户友好的格式呈现。\n\n# 图表\n\n`/components/ChartsTab`文件夹包含两个组成文件:`ChartsTab.js`和`PieChart.js`。`ChartsTab`组件包含两个`PieChart`组件实例，一个用于收入，一个用于支出。每个`PieChart`组件按类别显示生的或熟的百分比。用户可以通过图表正上方的按钮在生视图或熟视图之间切换。`PieChart.js`中的`drawChart()`方法使用 D3 渲染饼图和图例。加载时，它使用 D3 的内置动画制作饼图的每个部分:\n\n```cpp\narc\n  .append('path')\n  .attr('fill', d => colorScale(d.data.category))\n  .transition()\n  .delay((d, i) => i * 100)\n  .duration(500)\n  .attrTween('d', d => {\n    const i = d3.interpolate(d.startAngle + 0.1, d.endAngle);\n    return t => {\n      d.endAngle = i(t);\n      return arcPath(d);\n    };\n  });\n```\n\n前面的片段取自`PieChart.js`中的`drawChart()`，仅用几行代码定义了饼图的动画。如果您有兴趣了解更多关于 D3 的功能，请查看[https://bl.ocks.org](https://bl.ocks.org)的一些示例。组件评审到此为止；让我们尝试运行应用。\n\n# 运行应用\n\n您已经编写并编译了 C 代码，并添加了前端逻辑。是时候启动应用并与之交互了。在这一部分中，我们将验证您的应用的`/src`文件夹，运行应用，并测试特性以确保一切正常工作。\n\n# 正在验证/src 文件夹\n\n在启动应用之前，请参考以下结构，以确保您的`/src`文件夹结构正确，并且包含以下内容:\n\n```cpp\n├── /assets\n│    ├── main.wasm\n│    └── memory.wasm\n├── /components\n│    ├── /BalancesBar\n│    │    ├── BalanceCard.js\n│    │    └── BalancesBar.js\n│    ├── /ChartsTab\n│    │    ├── ChartsTab.js\n│    │    └── PieChart.js\n│    ├── /TransactionsTab\n│    │    ├── ConfirmationModal.js\n│    |    ├── TransactionModal.js\n│    |    ├── TransactionsTab.js\n│    |    └── TransactionsTable.js\n│    └── App.js\n├── /store\n│    ├── api.js\n│    ├── initializeWasm.js\n│    ├── store.js\n│    └── WasmTransactions.js\n├── index.html\n├── main.js\n└── styles.css\n```\n\n如果一切都符合，你就可以继续了。\n\n# 启动它！\n\n要启动应用，请在`/cook-the-books`文件夹中打开一个终端，并运行以下命令:\n\n```cpp\nnpm start\n```\n\n`browser-sync`我们在本章第一节中安装的开发依赖项充当本地服务器(类似于`serve`库)。它使应用可以从`package.json`文件中指定的端口(在本例中为`4000`)在浏览器中访问。如果您在浏览器中导航到`http://localhost:4000/index.html`，您应该会看到:\n\n![](img/fc07674b-a7b5-4fd6-8593-6a0429558e2e.png)\n\nApplication on initial load We're using `browser-sync` instead of `serve` because it watches for changes in your files and automatically reloads the application if you make a change. To see this in action, try changing the contents of the title bar in `App.js` from `Cook the Books` to `Broil the Books`. The browser will refresh and you'll see the updated text in the title bar.\n\n# 测试一下\n\n为了确保一切正常，让我们测试一下应用。以下各节描述了应用特定功能的操作和预期行为。跟着看你是否得到了预期的结果。如果遇到问题，您可以随时回到`learn-webassembly`存储库中的`/chapter-07-cook-the-books`文件夹。\n\n# 更改初始余额\n\n尝试更改初始生平衡和初始熟平衡`BalanceCard`组件上的输入值。当前原始余额和当前熟余额卡值应更新以反映您的更改。\n\n# 创建新交易\n\n记下当前的生天平和熟天平，然后按窗口右下角的蓝色“添加”按钮。它应该加载`TransactionModal`组件。填写输入，记下您输入的**类型**、**生量**和**熟量**，然后按保存按钮。\n\n余额应该已经更新，以反映新的数额。如果选择了**类型**的提取，余额应该会减少，否则会增加(对于存款)，如下图所示:\n\n![](img/dace2c59-c6f4-4039-ae01-3b6f3293777a.png)\n\nTransactionModal when adding a new transaction\n\n# 删除现有交易\n\n在`TransactionsTable`组件中选择一行，记下金额，然后按下看起来像垃圾桶的按钮记录。`ConfirmationModal`组件应该出现。当您按下**是**按钮时，交易记录不应再出现在表格中，当前余额应更新以反映与已删除交易相关的金额，如下图所示:\n\n![](img/850ece1d-3df5-4919-9380-e268ba3975b6.png)\n\nConfirmation modal shown after delete button is pressed\n\n# 编辑现有交易\n\n除了更改现有金额之外，请遵循与创建新交易相同的步骤。检查当前余额，确保它们反映更新的交易金额。\n\n# 测试图表选项卡\n\n选择图表选项卡加载`ChartsTab`组件。按下每个`PieChart`组件中的按钮，在生视图和熟视图之间切换。饼图应使用更新后的值重新呈现:\n\n![](img/24cbe04a-2d49-49cb-9d5b-09cb6f6c3699.png)\n\nContents of CHARTS tab with different amount types selected\n\n# 包裹\n\n恭喜，您刚刚构建了一个使用 WebAssembly 的应用！告诉你的朋友！现在您已经了解了 WebAssembly 的功能和局限性，是时候扩展我们的视野并使用 Emscripten 提供的一些优秀功能了。\n\n# 摘要\n\n在本章中，我们从头开始构建了一个使用 WebAssembly 的会计应用，没有 Emscripten 提供的任何额外功能。通过遵守*核心规范*，我们展示了当前形式的 WebAssembly 的局限性。然而，我们能够通过使用 Wasm 模块快速执行计算，这非常适合会计。我们使用 Vue 将我们的应用分割成组件，使用 UIkit 进行设计和布局，使用 D3 根据我们的交易数据创建饼图。在[第 8 章](08.html)、*用 Emscripten* 移植游戏中，我们将充分利用 Emscripten 将现有的 C++ 代码库移植到 WebAssembly 中。\n\n# 问题\n\n1.  为什么我们在这个应用中使用 Vue(而不是 React 或 Angular)？\n2.  为什么我们在这个项目中使用 C 而不是 C++ 呢？\n3.  为什么我们需要使用 jsonstore.io 建立一个模拟 API，而不是将数据本地存储在一个 JSON 文件中？\n4.  我们用来管理 C 文件中事务的数据结构的名称是什么？\n5.  我们需要`memory.wasm`文件中的哪些功能，它们是用来做什么的？\n6.  为什么我们要围绕 Wasm 模块创建一个包装类？\n7.  为什么我们要将`$store`对象全局化？\n8.  您可以在生产应用中使用哪些库来管理全局状态？\n9.  为什么我们用`browser-sync`而不是`serve`来运行应用？\n\n# 进一步阅读\n\n*   视图:https://vuej . org"
  },
  {
    "path": "docs/learn-wasm/08.md",
    "content": "# 八、使用电子脚本移植游戏\n\n正如[第 7 章](07.html)、*从头开始创建应用*中所展示的，WebAssembly 目前的形式仍然相对有限。Emscripten 提供了强大的 API 来扩展 WebAssembly 的功能，从而为您的应用添加功能。在某些情况下，编译到网络组件模块和 JavaScript 粘合代码(而不是可执行代码)只需要对现有的 C 或 C++ 源代码进行微小的更改。\n\n在本章中，我们将获取一个用 C++ 编写的代码库，该代码库被编译成传统的可执行文件，并更新该代码，以便可以将其编译成 Wasm/JavaScript。我们还将添加一些附加功能，以便与浏览器更紧密地集成。\n\n到本章结束时，您将知道如何执行以下操作:\n\n*   更新 C++ 代码库以编译成 Wasm 模块/JavaScript 粘合代码(而不是本机可执行文件)\n*   使用 Emscripten 的 API 将浏览器集成添加到 C++ 应用中\n*   用适当的`emcc`标志构建一个多文件 C++ 项目\n*   使用`emrun`在浏览器中运行并测试一个 C++ 应用\n\n# 游戏概述\n\n在这一章中，我们将获取一个用 C++ 编写的俄罗斯方块克隆，并更新代码以集成 Emscripten 并编译到 Wasm/JS。编译成可执行文件的原始形式的代码库利用 SDL2，可以从命令行加载。在本节中，我们将简要回顾什么是俄罗斯方块，如何获取代码(无需从头开始编写)，以及如何让它运行。\n\n# 什么是俄罗斯方块？\n\n在俄罗斯方块中，游戏的主要目标是在一个游戏场地(*井*或*矩阵*)内旋转和移动各种形状的棋子(*四亚氨基*)以创建一排没有间隙的方块。当一整行被创建时，它将从比赛场地中删除，您的分数将增加一分。在我们的游戏版本中，不会有获胜条件(尽管添加它会很简单)。\n\n理解游戏的规则和机制很重要，因为代码使用算法来处理诸如碰撞检测和评分等概念。理解函数的目标有助于理解其中的代码。如果你需要复习俄罗斯方块的技巧，我建议你在网上试一试。你可以在[https://emulatoronline.com/nes-games/classic-tetris/](https://emulatoronline.com/nes-games/classic-tetris/)玩，不用安装 Adobe Flash。它看起来就像最初的任天堂版本:\n\n![](img/60566c17-a951-44f8-a3f4-a1e2bf8c6115.png)\n\nClassic Tetris at EmulatorOnline.com\n\n我们将使用的版本不包含棋子计数器、等级或点数(我们坚持行计数)，但它将以相同的方式运行。\n\n# 来源的来源\n\n事实证明，对俄罗斯方块 C++ 的搜索提供了大量教程和示例库可供选择。为了坚持到目前为止我一直使用的格式和命名约定，我结合了这些资源来创建我自己的游戏版本。如果你有兴趣了解更多，本章末尾的*继续阅读*部分有这些资源的链接。移植代码库的概念和过程是适用的，不管它的来源是什么。在这一点上，让我们暂时不谈一下移植。\n\n# 关于移植的一个注记\n\n将现有代码库移植到 Emscripten 并不总是一件简单的任务。在评估 C、C++ 或 Rust 应用是否适合转换时，有几个变量需要考虑。例如，使用几个第三方库甚至几个相当复杂的第三方库的游戏可能需要大量的努力。Emscripten 提供了以下常用的现成库:\n\n*   `asio`:网络和低级 I/O 编程库\n*   `Bullet`:实时碰撞检测和多物理仿真库\n*   `Cocos2d`:一套开源、跨平台、游戏开发工具\n*   `FreeType`:用于渲染字体的库\n*   `HarfBuzz`:OpenType 文本整形引擎\n*   `libpng`:巴布亚新几内亚官方参考图书馆\n*   `Ogg`:一种多媒体容器格式\n*   `SDL2`:设计用于提供对音频、键盘、鼠标、操纵杆和图形硬件的低级访问的库\n*   `SDL2_image`:图像文件加载库\n*   `SDL2_mixer`:一个样本多声道混音器库\n*   `SDL2_net`:跨平台组网库小样本\n*   `SDL2_ttf`:一个示例库，允许您在 SDL 应用中使用 TrueType 字体\n*   `Vorbis`:通用音频和音乐编码格式\n*   `zlib`:无损数据压缩库\n\n如果库还没有移植，你需要自己做。这将有利于社区，但需要大量的时间和资源投入。我们的俄罗斯方块示例只使用了 SDL2，这使得移植过程相对简单。\n\n# 获取代码\n\n本章的代码位于`learn-webassembly`存储库的`/chapter-08-tetris`文件夹中。`/chapter-08-tetris`中有两个目录:`/output-native`文件夹包含原始(预移植)代码，`/output-wasm`文件夹包含移植代码。\n\nIf you want to use VS Code's Task feature for the native build step, you'll need to open the `/chapter-08-tetris/output-native` folder in VS Code, not the top-level `/learn-webassembly` folder.\n\n# 构建本地项目\n\n构建项目需要`/cmake`文件夹和`/output-native`文件夹中的`CMakeLists.txt`文件。`README.md`文件包含在每个平台上启动和运行代码的说明。构建项目对于移植过程来说不是必需的。安装所需依赖项和让项目在您的平台上成功构建的过程可能既耗时又复杂。如果您仍然希望继续，您可以通过选择任务|运行任务，通过 VS 代码的任务功能构建可执行文件...按照`README.md`文件中的说明，从菜单中选择构建可执行文件。\n\n# 行动中的游戏\n\n如果您成功构建了项目，您应该能够通过选择**任务** | **运行任务来运行它...从 VS 代码菜单中选择**，并从列表中选择开始可执行任务。如果一切都成功了，你应该看到这样的东西:\n\n![](img/34e1026f-bd47-4209-a611-671d81f98ede.png)\n\nCompiled game running natively\n\n我们版本的游戏没有输的条件；它只是为您清除的每一行增加一个 ROWS 计数。如果其中一个四亚氨基接触到棋盘的顶部，游戏结束，棋盘重置。这是游戏的基本实现，但是额外的特性增加了复杂性和所需的代码量。让我们更详细地回顾一下代码库。\n\n# 深度代码库\n\n现在您已经有了可用的代码，您需要熟悉代码库。如果不能很好地理解您想要移植的代码，您将很难成功移植它。在本章中，我们将遍历每个 C++ 类和头文件，并描述它们在应用中的角色。\n\n# 将代码分解成对象\n\nC++ 是围绕面向对象的范例设计的，这是俄罗斯方块代码库用来简化应用管理的。代码库由 C++ 类文件组成\n\n(`.cpp`)和头文件(`.h`)表示游戏上下文中的对象。我用的是*的游戏总结什么是俄罗斯方块？*部分来推断我需要哪些对象。\n\n游戏棋子(四亚氨基)和游戏场地(被称为井或矩阵)是上课的好选择。也许不那么直观，但仍然同样有效的是*游戏*本身。类不一定需要像实际对象一样具体——它们非常适合存储共享代码。我很喜欢少打字，所以我选择用`Piece`来代表一个四亚氨基，用`Board`来代表比赛场地(虽然*这个词比*更短，但不太合适)。我创建了一个头文件来存储全局变量(`constants.h`)，一个`Game`类来管理游戏性，还有一个`main.cpp`文件，作为游戏的入口点。以下是`/src`文件夹的内容:\n\n```cpp\n├── board.cpp\n├── board.h\n├── constants.h\n├── game.cpp\n├── game.h\n├── main.cpp\n├── piece.cpp\n└── piece.h\n```\n\n每个文件(除了`main.cpp`、`constants.h`)都有一个类(`.cpp`)和头(`.h`)文件。头文件允许您跨多个文件重用代码，并防止代码重复。*进一步阅读*部分包含资源，如果您感兴趣，可以了解更多关于头文件的信息。`constants.h`文件几乎用于应用中的所有其他文件，所以让我们先回顾一下。\n\n# 常数文件\n\n我选择了包含我们将要使用的常量(`constants.h`)的头文件，而不是在整个代码库中散布令人困惑的*神奇数字*。此文件的内容如下所示:\n\n```cpp\n#ifndef TETRIS_CONSTANTS_H\n#define TETRIS_CONSTANTS_H\n\nnamespace Constants {\n    const int BoardColumns = 10;\n    const int BoardHeight = 720;\n    const int BoardRows = 20;\n    const int BoardWidth = 360;\n    const int Offset = BoardWidth / BoardColumns;\n    const int PieceSize = 4;\n    const int ScreenHeight = BoardHeight + 50;\n}\n\n#endif // TETRIS_CONSTANTS_H\n```\n\n文件第一行的`#ifndef`语句是一个`#include`保护符，防止头文件在编译过程中被多次包含。这些保护用于所有应用的头文件。当我们单步执行每个类时，这些常量的目的将变得清晰。我首先包含它是为了提供各种元素大小的上下文，以及它们之间的关系。\n\n让我们继续讨论代表游戏各个方面的各个类。`Piece`类代表最低级别的对象，因此我们将从那里开始，一路向上到达`Board`和`Game`类。\n\n# 小品类\n\n棋子，或*四亚氨基*，是可以在棋盘上移动和旋转的元素。四亚氨基有七种——每种都用一个字母表示，并有相应的颜色:\n\n![](img/bc6eabd2-b522-4990-9973-6d5432055b3d.png)\n\nTetrimino colors, taken from Wikipedia\n\n我们需要一种根据形状、颜色和当前方向来定义每件作品的方法。每件作品有四个不同的方向(以 90 度增量)，这导致所有作品的 28 个总变化。颜色不变，所以只需要分配一次。考虑到这一点，我们首先来看看头文件(`piece.h`):\n\n```cpp\n#ifndef TETRIS_PIECE_H\n#define TETRIS_PIECE_H\n\n#include <SDL2/SDL.h>\n#include \"constants.h\"\n\nclass Piece {\n public:\n  enum Kind { I = 0, J, L, O, S, T, Z };\n\n  explicit Piece(Kind kind);\n\n  void draw(SDL_Renderer *renderer);\n  void move(int columnDelta, int rowDelta);\n  void rotate();\n  bool isBlock(int column, int row) const;\n  int getColumn() const;\n  int getRow() const;\n\n private:\n  Kind kind_;\n  int column_;\n  int row_;\n  int angle_;\n};\n\n#endif // TETRIS_PIECE_H\n```\n\n游戏使用 SDL2 来渲染各种图形元素和处理键盘输入，这就是为什么我们在`draw()`函数中传递一个`SDL_Renderer`的原因。您将会看到 SDL2 在`Game`类中的使用，但是现在只需注意它的包含性。头文件定义了`Piece`类的接口；让我们回顾一下`piece.cpp`的实施情况。我们将遍历每一段代码并描述功能。\n\n# 构造函数和 draw()函数\n\n第一段代码定义了`Piece`类的构造函数和`draw()`函数:\n\n```cpp\n#include \"piece.h\"\n\nusing namespace Constants;\n\nPiece::Piece(Piece::Kind kind) :\n    kind_(kind),\n    column_(BoardColumns / 2 - PieceSize / 2),\n    row_(0),\n    angle_(0) {\n}\n\nvoid Piece::draw(SDL_Renderer *renderer) {\n    switch (kind_) {\n        case I:\n            SDL_SetRenderDrawColor(renderer,\n                /* Cyan: */ 45, 254, 254, 255);\n            break;\n        case J:\n            SDL_SetRenderDrawColor(renderer,\n                /* Blue: */ 11, 36, 251, 255);\n            break;\n        case L:\n            SDL_SetRenderDrawColor(renderer,\n                /* Orange: */ 253, 164, 41, 255);\n            break;\n        case O:\n            SDL_SetRenderDrawColor(renderer,\n                /* Yellow: */ 255, 253, 56, 255);\n            break;\n       case S:\n            SDL_SetRenderDrawColor(renderer,\n                /* Green: */ 41, 253, 47, 255);\n            break;\n        case T:\n            SDL_SetRenderDrawColor(renderer,\n                /* Purple: */ 126, 15, 126, 255);\n            break;\n        case Z:\n            SDL_SetRenderDrawColor(renderer,\n                /* Red: */ 252, 13, 28, 255);\n            break;\n        }\n\n        for (int column = 0; column < PieceSize; ++ column) {\n            for (int row = 0; row < PieceSize; ++ row) {\n                if (isBlock(column, row)) {\n                    SDL_Rect rect{\n                        (column + column_) * Offset + 1,\n                        (row + row_) * Offset + 1,\n                        Offset - 2,\n                        Offset - 2\n                    };\n                SDL_RenderFillRect(renderer, &rect);\n            }\n        }\n    }\n}\n```\n\n构造函数用默认值初始化类。`BoardColumns`和`PieceSize`值是来自`constants.h`文件的常数。`BoardColumns`表示一块板上可以容纳的列数，在本例中为`10`。`PieceSize`常数表示一个零件在列中占据的面积或块，即`4`。分配给私有`columns_`变量的初始值代表板的中心。\n\n`draw()`函数循环遍历板上所有可能的行和列，并使用与其种类相对应的颜色填充由一块填充的任何单元格。确定一个单元是否由一个片段填充是在`isBlock()`函数中执行的，我们接下来将讨论这个函数。\n\n# 移动()、旋转()和锁定()函数\n\n第二部分包含移动或旋转工件并确定其当前位置的逻辑:\n\n```cpp\nvoid Piece::move(int columnDelta, int rowDelta) {\n    column_ += columnDelta;\n    row_ += rowDelta;\n}\n\nvoid Piece::rotate() {\n    angle_ += 3;\n    angle_ %= 4;\n}\n\nbool Piece::isBlock(int column, int row) const {\n    static const char *Shapes[][4] = {\n        // I\n        {\n            \" *  \"\n            \" *  \"\n            \" *  \"\n            \" *  \",\n            \"    \"\n            \"****\"\n            \"    \"\n            \"    \",\n            \" *  \"\n            \" *  \"\n            \" *  \"\n            \" *  \",\n            \"    \"\n            \"****\"\n            \"    \"\n            \"    \",\n        },\n        // J\n        {\n            \"  * \"\n            \"  * \"\n            \" ** \"\n            \"    \",\n            \"    \"\n            \"*   \"\n            \"*** \"\n            \"    \",\n            \" ** \"\n            \" *  \"\n            \" *  \"\n            \"    \",\n            \"    \"\n            \"    \"\n            \"*** \"\n            \" *  \",\n        },\n        ...\n    };\n    return Shapes[kind_][angle_][column + row * PieceSize] == '*';\n}\n\nint Piece::getColumn() const {\n return column_;\n}\nint Piece::getRow() const {\n return row_;\n}\n```\n\n`move()`函数更新私有的`column_`和`row_`变量的值，这决定了棋子在棋盘上的位置。`rotate()`功能将私有`angle_`变量的值设置为`0`、`1`、`2`或`3`(这就是使用`%= 4`的原因)。\n\n在`isBlock()`功能中确定显示哪种工件、其位置和旋转。为了避免弄乱文件，我省略了`Shapes`多维数组的前两个元素以外的所有元素，但是剩下的五种类型都存在于实际代码中。我承认这不是最优雅的实现，但它非常适合我们的目的。\n\n私有的`kind_`和`angle_`值被指定为`Shapes`数组中的尺寸，以选择四个对应的`char*`元素。这四个元素代表了该作品的四种可能的方向。如果字符串中`column + row * PieceSize`的索引是星号，则该片段出现在指定的行和列中。如果您决定浏览网络上的俄罗斯方块教程(或者查看 GitHub 上的众多俄罗斯方块存储库之一)，您会发现有几种不同的方法来计算一个单元格是否由一部分填充。我选择这种方法是因为它更容易将碎片可视化。\n\n# getColumn()和 getRow()函数\n\n代码的最后一部分包含获取片段的行和列的函数:\n\n```cpp\nint Piece::getColumn() const {\n    return column_;\n}\n\nint Piece::getRow() const {\n    return row_;\n}\n```\n\n这些函数只是返回私有`column_`或`row_`变量的值。既然你对`Piece`班有了更好的了解，让我们进入`Board`班。\n\n# 董事会\n\n`Board`包含`Piece`类的实例，需要检测棋子之间的碰撞，何时排满，何时游戏结束。让我们从头文件(`board.h`)的内容开始:\n\n```cpp\n#ifndef TETRIS_BOARD_H\n#define TETRIS_BOARD_H\n\n#include <SDL2/SDL.h>\n#include <SDL2/SDL2_ttf.h>\n#include \"constants.h\"\n#include \"piece.h\"\n\nusing namespace Constants;\n\nclass Board {\n public:\n  Board();\n  void draw(SDL_Renderer *renderer, TTF_Font *font);\n  bool isCollision(const Piece &piece) const;\n  void unite(const Piece &piece);\n\n private:\n  bool isRowFull(int row);\n  bool areFullRowsPresent();\n  void updateOffsetRow(int fullRow);\n  void displayScore(SDL_Renderer *renderer, TTF_Font *font);\n\n  bool cells_[BoardColumns][BoardRows];\n  int currentScore_;\n};\n\n#endif // TETRIS_BOARD_H\n```\n\n`Board`有一个类似于`Piece`类的`draw()`功能，以及其他几个用于管理行和跟踪板上填充了哪些单元格的功能。`SDL2_ttf`库用于渲染窗口底部带有当前分数(已清除行数)的 ROWS: text。现在，让我们看一下实现文件(`board.cpp`)的每个部分。\n\n# 构造函数和 draw()函数\n\n第一段代码定义了`Board`类的构造函数和`draw()`函数:\n\n```cpp\n#include <sstream>\n#include \"board.h\"\n\nusing namespace Constants;\n\nBoard::Board() : cells_{{ false }}, currentScore_(0) {}\n\nvoid Board::draw(SDL_Renderer *renderer, TTF_Font *font) {\n    displayScore(renderer, font);\n    SDL_SetRenderDrawColor(\n        renderer,\n        /* Light Gray: */ 140, 140, 140, 255);\n    for (int column = 0; column < BoardColumns; ++ column) {\n        for (int row = 0; row < BoardRows; ++ row) {\n            if (cells_[column][row]) {\n                SDL_Rect rect{\n                    column * Offset + 1,\n                    row * Offset + 1,\n                    Offset - 2,\n                    Offset - 2\n                };\n                SDL_RenderFillRect(renderer, &rect);\n            }\n        }\n    }\n}\n```\n\n`Board`构造函数将私有`cells_`和`currentScore_`变量的值初始化为默认值。`cells_`变量是布尔的二维数组，第一维表示列，第二行表示行。如果一个片段占据了特定的列和行，数组中对应的值就是`true`。`draw()`功能的行为类似于`Piece`的`draw()`功能，因为它用颜色填充包含片段的单元格。但是，该功能仅填充由浅灰色到达板底部的块占据的单元格，而不管它是什么类型的块。\n\n# isCollision()函数\n\n代码的第二部分包含检测冲突的逻辑:\n\n```cpp\nbool Board::isCollision(const Piece &piece) const {\n    for (int column = 0; column < PieceSize; ++ column) {\n        for (int row = 0; row < PieceSize; ++ row) {\n            if (piece.isBlock(column, row)) {\n                int columnTarget = piece.getColumn() + column;\n                int rowTarget = piece.getRow() + row;\n                if (\n                    columnTarget < 0\n                    || columnTarget >= BoardColumns\n                    || rowTarget < 0\n                    || rowTarget >= BoardRows\n                ) {\n                    return true;\n                }\n                if (cells_[columnTarget][rowTarget]) return true;\n            }\n        }\n    }\n    return false;\n}\n```\n\n`isCollision()`函数循环遍历板上的每个单元格，直到到达由作为参数传递的`&piece`填充的单元格。如果工件即将与板的任一侧碰撞或已经到达底部，该功能返回`true`，否则返回`false`。\n\n# unite()函数\n\n代码的第三部分包含逻辑，当它停止时，将一个片段与顶行结合起来:\n\n```cpp\nvoid Board::unite(const Piece &piece) {\n    for (int column = 0; column < PieceSize; ++ column) {\n        for (int row = 0; row < PieceSize; ++ row) {\n            if (piece.isBlock(column, row)) {\n                int columnTarget = piece.getColumn() + column;\n                int rowTarget = piece.getRow() + row;\n                cells_[columnTarget][rowTarget] = true;\n            }\n        }\n    }\n\n    // Continuously loops through each of the rows until no full rows are\n    // detected and ensures the full rows are collapsed and non-full rows\n    // are shifted accordingly:\n    while (areFullRowsPresent()) {\n        for (int row = BoardRows - 1; row >= 0; --row) {\n            if (isRowFull(row)) {\n                updateOffsetRow(row);\n                currentScore_ += 1;\n                for (int column = 0; column < BoardColumns; ++ column) {\n                    cells_[column][0] = false;\n                }\n            }\n        }\n    }\n}\n\nbool Board::isRowFull(int row) {\n    for (int column = 0; column < BoardColumns; ++ column) {\n        if (!cells_[column][row]) return false;\n    }\n    return true;\n}\n\nbool Board::areFullRowsPresent() {\n    for (int row = BoardRows - 1; row >= 0; --row) {\n        if (isRowFull(row)) return true;\n    }\n    return false;\n}\n\nvoid Board::updateOffsetRow(int fullRow) {\n    for (int column = 0; column < BoardColumns; ++ column) {\n        for (int rowOffset = fullRow - 1; rowOffset >= 0; --rowOffset) {\n            cells_[column][rowOffset + 1] =\n            cells_[column][rowOffset];\n        }\n    }\n}\n```\n\n`unite()`功能和相应的`isRowFull()`、`areFullRowsPresent()`和`updateOffsetRow()`功能执行多种操作。它通过将适当的数组位置设置为`true`，用指定的`&piece`参数占据的行和列更新私有的`cells_`变量。它还通过将相应的`cells_`阵列位置设置为`false`并增加`currentScore_`来清除板上的所有完整行(所有填充的列)。行被清除后，`cells_`数组被更新，将清除行上方的行下移`1`。\n\n# displayScore()函数\n\n代码的最后一部分在游戏窗口的底部显示分数:\n\n```cpp\nvoid Board::displayScore(SDL_Renderer *renderer, TTF_Font *font) {\n    std::stringstream message;\n    message << \"ROWS: \" << currentScore_;\n    SDL_Color white = { 255, 255, 255 };\n    SDL_Surface *surface = TTF_RenderText_Blended(\n        font,\n        message.str().c_str(),\n        white);\n    SDL_Texture *texture = SDL_CreateTextureFromSurface(\n        renderer,\n        surface);\n    SDL_Rect messageRect{ 20, BoardHeight + 15, surface->w, surface->h };\n    SDL_FreeSurface(surface);\n    SDL_RenderCopy(renderer, texture, nullptr, &messageRect);\n    SDL_DestroyTexture(texture);\n}\n```\n\n`displayScore()`功能使用`SDL2_ttf`库在窗口底部(棋盘下方)显示当前分数。`TTF_Font *font`参数是从`Game`类传入的，以避免每次更新分数时初始化字体。`stringstream message`变量用于创建文本值，并在`TTF_RenderText_Blended()`功能中将其设置为 C `char*`。代码的其余部分将文本绘制在`SDL_Rect`上，以确保其正确显示。\n\n`Board`班到此为止；让我们转到`Game`来看看它是如何组合在一起的。\n\n# 游戏课\n\n`Game`类包含循环功能，使您能够通过按键在电路板上移动工件。以下是头文件(`game.h`)的内容:\n\n```cpp\n#ifndef TETRIS_GAME_H\n#define TETRIS_GAME_H\n\n#include <SDL2/SDL.h>\n#include <SDL2/SDL2_ttf.h>\n#include \"constants.h\"\n#include \"board.h\"\n#include \"piece.h\"\n\nclass Game {\n public:\n  Game();\n  ~Game();\n  bool loop();\n\n private:\n  Game(const Game &);\n  Game &operator=(const Game &);\n\n  void checkForCollision(const Piece &newPiece);\n  void handleKeyEvents(SDL_Event &event);\n\n  SDL_Window *window_;\n  SDL_Renderer *renderer_;\n  TTF_Font *font_;\n  Board board_;\n  Piece piece_;\n  uint32_t moveTime_;\n};\n\n#endif // TETRIS_GAME_H\n```\n\n`loop()`功能包含游戏逻辑，基于事件管理状态。`private:`标题下的前两行阻止创建多个游戏实例，这可能会导致内存泄漏。私有方法减少了`loop()`函数中的代码行数，从而简化了维护和调试。让我们进入`game.cpp`中的实现。\n\n# 构造函数和析构函数\n\n代码的第一部分定义了类实例加载(构造函数)和卸载(析构函数)时要执行的操作:\n\n```cpp\n#include <cstdlib>\n#include <iostream>\n#include <stdexcept>\n#include \"game.h\"\n\nusing namespace std;\nusing namespace Constants;\n\nGame::Game() :\n    // Create a new random piece:\n    piece_{ static_cast<Piece::Kind>(rand() % 7) },\n    moveTime_(SDL_GetTicks())\n{\n    if (SDL_Init(SDL_INIT_VIDEO) != 0) {\n        throw runtime_error(\n            \"SDL_Init(SDL_INIT_VIDEO): \" + string(SDL_GetError()));\n        }\n        SDL_CreateWindowAndRenderer(\n            BoardWidth,\n            ScreenHeight,\n            SDL_WINDOW_OPENGL,\n            &window_,\n            &renderer_);\n        SDL_SetWindowPosition(\n            window_,\n            SDL_WINDOWPOS_CENTERED,\n            SDL_WINDOWPOS_CENTERED);\n        SDL_SetWindowTitle(window_, \"Tetris\");\n\n    if (TTF_Init() != 0) {\n        throw runtime_error(\"TTF_Init():\" + string(TTF_GetError()));\n    }\n    font_ = TTF_OpenFont(\"PressStart2P.ttf\", 18);\n    if (font_ == nullptr) {\n        throw runtime_error(\"TTF_OpenFont: \" + string(TTF_GetError()));\n    }\n}\n\nGame::~Game() {\n    TTF_CloseFont(font_);\n    TTF_Quit();\n    SDL_DestroyRenderer(renderer_);\n    SDL_DestroyWindow(window_);\n    SDL_Quit();\n}\n```\n\n构造函数代表应用的入口点，因此所有需要的资源都在其中分配和初始化。`TTF_OpenFont()`函数引用了一个从谷歌字体下载的名为“按下开始 2P”的 TrueType 字体文件。可以在[https://fonts.google.com/specimen/Press+Start+2P](https://fonts.google.com/specimen/Press+Start+2P)查看字体。它存在于存储库的`/resources`文件夹中，并在项目构建时被复制到与可执行文件相同的文件夹中。如果在初始化 SDL2 资源时出现错误，则会抛出一个带有错误详细信息的`runtime_error`。析构函数(`~Game()`)在应用退出之前释放我们为 SDL2 和`SDL2_ttf`分配的资源。这样做是为了避免内存泄漏。\n\n# loop()函数\n\n最后一段代码代表`Game::loop`:\n\n```cpp\nbool Game::loop() {\n    SDL_Event event;\n    while (SDL_PollEvent(&event)) {\n        switch (event.type) {\n            case SDL_KEYDOWN:\n                handleKeyEvents(event);\n                break;\n            case SDL_QUIT:\n                return false;\n            default:\n                return true;\n        }\n    }\n\n    SDL_SetRenderDrawColor(renderer_, /* Dark Gray: */ 58, 58, 58, 255);\n    SDL_RenderClear(renderer_);\n    board_.draw(renderer_, font_);\n    piece_.draw(renderer_);\n\n    if (SDL_GetTicks() > moveTime_) {\n        moveTime_ += 1000;\n        Piece newPiece = piece_;\n        newPiece.move(0, 1);\n        checkForCollision(newPiece);\n    }\n    SDL_RenderPresent(renderer_);\n    return true;\n}\n\nvoid Game::checkForCollision(const Piece &newPiece) {\n    if (board_.isCollision(newPiece)) {\n        board_.unite(piece_);\n        piece_ = Piece{ static_cast<Piece::Kind>(rand() % 7) };\n        if (board_.isCollision(piece_)) board_ = Board();\n    } else {\n        piece_ = newPiece;\n    }\n}\n\nvoid Game::handleKeyEvents(SDL_Event &event) {\n    Piece newPiece = piece_;\n    switch (event.key.keysym.sym) {\n        case SDLK_DOWN:\n            newPiece.move(0, 1);\n            break;\n        case SDLK_RIGHT:\n            newPiece.move(1, 0);\n            break;\n        case SDLK_LEFT:\n            newPiece.move(-1, 0);\n            break;\n        case SDLK_UP:\n            newPiece.rotate();\n            break;\n        default:\n            break;\n     }\n     if (!board_.isCollision(newPiece)) piece_ = newPiece;\n}\n```\n\n只要`SDL_QUIT`事件没有触发，`loop()`函数就会返回一个布尔值。每隔`1`秒执行一次`Piece`和`Board`实例的`draw()`功能，棋盘上的棋子位置也相应更新。左箭头键、右箭头键和下箭头键控制棋子的移动，而上箭头键将棋子旋转 90 度。在`handleKeyEvents()`功能中处理对按键的适当响应。`checkForCollision()`功能确定活动棋子的新实例是否与棋盘的任一侧发生碰撞，或者是否停留在其他棋子的顶部。如果是这样，就会产生一个新的片段。清除行的逻辑(通过`Board`的`unite()`功能)也在该功能中处理。我们快完成了！让我们进入`main.cpp`文件。\n\n# 主文件\n\n没有与`main.cpp`相关联的头文件，因为它的唯一目的是作为应用的入口点。事实上，文件只有七行长:\n\n```cpp\n#include \"game.h\"\n\nint main() {\n    Game game;\n    while (game.loop());\n    return 0;\n}\n```\n\n当`loop()`功能返回`false`时，退出`while`语句，这发生在`SDL_QUIT`事件触发时。这个文件所做的就是创建一个新的`Game`实例并开始循环。代码库到此为止。我们开始移植吧！\n\n# 移植到 Emscripten\n\n您对代码库有很好的理解，所以现在是时候开始用 Emscripten 移植它了。幸运的是，我们能够利用浏览器的一些功能来简化代码，并完全删除第三方库。在本节中，我们将更新代码以编译成一个 Wasm 模块和 JavaScript *胶合*文件，并更新一些功能以利用浏览器。\n\n# 准备移植\n\n`/output-wasm`文件夹包含最终结果，但我建议您创建一个`/output-native`文件夹的副本，以便您可以继续移植过程。为本机编译和电子脚本编译都设置了 VS 代码任务。如果卡住了，可以随时参考`/output-wasm`的内容。请确保在 VS 代码中打开您复制的文件夹(文件|打开并选择您复制的文件夹)，否则您将无法使用任务功能。\n\n# 有什么变化？\n\n这款游戏是移植的理想选择，因为它使用了 SDL2，这是一个广泛使用的库，具有现有的 Emscripten 端口。在编译步骤中包含 SDL2 只需要向`emcc`命令传递一个额外的参数。`SDL2_ttf`库的一个 Emscripten 端口也存在，但是将其保存在代码库中没有多大意义。它的唯一目的是将分数(清除的行数)呈现为文本。我们需要在应用中包含 TTF 文件，并使构建过程复杂化。Emscripten 提供了在我们的 C++ 中使用 JavaScript 代码的方法，所以我们将采取一种简单得多的方法:在 DOM 中显示分数。\n\n除了更改现有的代码，我们还需要创建一个 HTML 和 CSS 文件，用于在浏览器中显示和设置游戏的样式。我们编写的 JavaScript 代码将是最少的——我们只需要加载 Emscripten 模块，所有的功能都在 C++ 代码库中处理。我们还需要添加一些`<div>`元素，并对它们进行相应的布局以显示分数。开始移植吧！\n\n# 添加网络资产\n\n在项目文件夹中创建一个名为`/public`的文件夹。将名为`index.html`的新文件添加到`/public`文件夹，并用以下内容填充:\n\n```cpp\n<!doctype html>\n<html lang=\"en-us\">\n<head>\n  <title>Tetris</title>\n  <link rel=\"stylesheet\" type=\"text/css\" href=\"styles.css\" />\n</head>\n<body>\n  <div class=\"wrapper\">\n    <h1>Tetris</h1>\n    <div>\n      <canvas id=\"canvas\"></canvas>\n      <div class=\"scoreWrapper\">\n        <span>ROWS:</span><span id=\"score\"></span>\n      </div>\n    </div>\n  </div>\n  <script type=\"application/javascript\" src=\"index.js\"></script>\n  <script type=\"application/javascript\">\n    Module({ canvas: (() => document.getElementById('canvas'))() })\n  </script>\n</body>\n</html>\n```\n\n第一个`<script>`标签中正在加载的`index.js`文件还不存在；将在编译步骤中生成。让我们给元素添加一些样式。在`/public`文件夹中创建一个`styles.css`文件，并用以下内容填充:\n\n```cpp\n@import url(\"https://fonts.googleapis.com/css?family=Press+Start+2P\");\n\n* {\n  font-family: \"Press Start 2P\", sans-serif;\n}\n\nbody {\n  margin: 24px;\n}\n\nh1 {\n  font-size: 36px;\n}\n\nspan {\n  color: white;\n  font-size: 24px;\n}\n\n.wrapper {\n  display: flex;\n  align-items: center;\n  flex-direction: column;\n}\n\n.titleWrapper {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n}\n\n.header {\n  font-size: 24px;\n  margin-left: 16px;\n}\n\n.scoreWrapper {\n  background-color: #3A3A3A;\n  border-top: 1px solid white;\n  padding: 16px 0;\n  width: 360px;\n}\n\nspan:first-child {\n  margin-left: 16px;\n  margin-right: 8px;\n}\n```\n\n由于我们正在使用的“新闻开始 2P”字体托管在谷歌字体上，我们可以导入它在网站上使用。这个文件中的 CSS 规则处理简单的布局和样式。这就是我们需要创建的网络相关文件。现在，是时候更新 C++ 代码了。\n\n# 移植现有代码\n\n我们只需要编辑几个文件就可以让 Emscripten 正常工作。为了简单和紧凑起见，将只包含受影响的代码部分(而不是整个文件)。让我们按照与上一节相同的顺序浏览文件，从`constants.h`开始。\n\n# 更新常数文件\n\n我们将在 DOM 上显示清除的行数，而不是在游戏窗口本身，因此您可以从文件中删除`ScreenHeight`常量。我们不再需要额外的空间来容纳乐谱:\n\n```cpp\nnamespace Constants {\n    const int BoardColumns = 10;\n    const int BoardHeight = 720;\n    const int BoardRows = 20;\n    const int BoardWidth = 360;\n    const int Offset = BoardWidth / BoardColumns;\n    const int PieceSize = 4;\n    // const int ScreenHeight = BoardHeight + 50; <----- Delete this line\n}\n```\n\n不需要对`Piece`类文件(`piece.cpp` / `piece.h`)进行更改。但是，我们需要更新`Board`类。让我们从头文件(`board.h`)开始。从底部开始向上，我们来更新`displayScore()`功能。在`index.html`文件的`<body>`部分，有一个带有`id=\"score\"`的`<span>`元素。我们将使用`emscripten_run_script`命令更新这个元素来显示当前分数。因此，`displayScore()`功能变得更短。前后如下所示。\n\n这是 Board 类的`displayScore()`函数的原始版本:\n\n```cpp\nvoid Board::displayScore(SDL_Renderer *renderer, TTF_Font *font) {\n    std::stringstream message;\n    message << \"ROWS: \" << currentScore_;\n    SDL_Color white = { 255, 255, 255 };\n    SDL_Surface *surface = TTF_RenderText_Blended(\n        font,\n        message.str().c_str(),\n        white);\n    SDL_Texture *texture = SDL_CreateTextureFromSurface(\n        renderer,\n        surface);\n    SDL_Rect messageRect{ 20, BoardHeight + 15, surface->w, surface->h };\n    SDL_FreeSurface(surface);\n    SDL_RenderCopy(renderer, texture, nullptr, &messageRect);\n    SDL_DestroyTexture(texture);\n }\n```\n\n以下是`displayScore()`功能的移植版本:\n\n```cpp\nvoid Board::displayScore(int newScore) {\n    std::stringstream action;\n    action << \"document.getElementById('score').innerHTML =\" << newScore;\n    emscripten_run_script(action.str().c_str());\n }\n```\n\n`emscripten_run_script`动作只是在 DOM 上找到`<span>`元素并将`innerHTML`设置为当前分数。我们不能在这里使用`EM_ASM()`功能，因为 Emscripten 不识别`document`对象。由于我们可以访问类中的私有`currentScore_`变量，我们将把`draw()`函数中的`displayScore()`调用移到`unite()`函数中。这限制了对`displayScore()`的调用量，以确保该函数仅在分数实际发生变化时被调用。我们只需要添加一行代码就可以完成。以下是`unite()`功能现在的样子:\n\n```cpp\nvoid Board::unite(const Piece &piece) {\n    for (int column = 0; column < PieceSize; ++ column) {\n        for (int row = 0; row < PieceSize; ++ row) {\n            if (piece.isBlock(column, row)) {\n                int columnTarget = piece.getColumn() + column;\n                int rowTarget = piece.getRow() + row;\n                cells_[columnTarget][rowTarget] = true;\n            }\n        }\n    }\n\n    // Continuously loops through each of the rows until no full rows are\n    // detected and ensures the full rows are collapsed and non-full rows\n    // are shifted accordingly:\n    while (areFullRowsPresent()) {\n        for (int row = BoardRows - 1; row >= 0; --row) {\n            if (isRowFull(row)) {\n                updateOffsetRow(row);\n                currentScore_ += 1;\n                for (int column = 0; column < BoardColumns; ++ column) {\n                    cells_[column][0] = false;\n                }\n            }\n        }\n        displayScore(currentScore_); // <----- Add this line\n    }\n}\n```\n\n由于我们不再使用`SDL2_ttf`库，我们可以更新`draw()`函数签名并删除`displayScore()`函数调用。更新后的`draw()`功能如下:\n\n```cpp\nvoid Board::draw(SDL_Renderer *renderer/*, TTF_Font *font */) {\n                                        // ^^^^^^^^^^^^^^ <-- Remove this argument\n    // displayScore(renderer, font); <----- Delete this line\n    SDL_SetRenderDrawColor(\n        renderer,\n        /* Light Gray: */ 140, 140, 140, 255);\n    for (int column = 0; column < BoardColumns; ++ column) {\n        for (int row = 0; row < BoardRows; ++ row) {\n            if (cells_[column][row]) {\n                SDL_Rect rect{\n                    column * Offset + 1,\n                    row * Offset + 1,\n                    Offset - 2,\n                    Offset - 2\n                };\n                SDL_RenderFillRect(renderer, &rect);\n            }\n        }\n    }\n }\n```\n\n`displayScore()`函数调用从函数的第一行被删除，并且`TTF_Font *font`参数也被删除。让我们在构造函数中添加对`displayScore()`的调用，以确保当游戏结束并开始新的游戏时，初始值被设置为`0`:\n\n```cpp\nBoard::Board() : cells_{{ false }}, currentScore_(0) {\n    displayScore(0); // <----- Add this line\n}\n```\n\n班级档案到此为止。由于我们更改了`displayScore()`和`draw()`函数的签名，并删除了`SDL2_ttf`的依赖项，我们需要更新头文件。从`board.h`中删除以下行:\n\n```cpp\n#ifndef TETRIS_BOARD_H\n#define TETRIS_BOARD_H\n\n#include <SDL2/SDL.h>\n// #include <SDL2/SDL2_ttf.h> <----- Delete this line\n#include \"constants.h\"\n#include \"piece.h\"\n\nusing namespace Constants;\n\nclass Board {\n public:\n  Board();\n  void draw(SDL_Renderer *renderer /*, TTF_Font *font */);\n                                    // ^^^^^^^^^^^^^^ <-- Remove this\n  bool isCollision(const Piece &piece) const;\n  void unite(const Piece &piece);\n\n private:\n  bool isRowFull(int row);\n  bool areFullRowsPresent();\n  void updateOffsetRow(int fullRow);\n  void displayScore(SDL_Renderer *renderer, TTF_Font *font);\n                                         // ^^^^^^^^^^^^^^ <-- Remove this\n  bool cells_[BoardColumns][BoardRows];\n  int currentScore_;\n};\n\n#endif // TETRIS_BOARD_H\n```\n\n我们正在前进！我们需要做出的最终改变也是最大的改变。现有的代码库有一个管理应用逻辑的`Game`类和一个调用`main()`函数中`Game.loop()`函数的`main.cpp`文件。循环机制是一个 while 循环，只要`SDL_QUIT`事件没有触发，它就会继续运行。我们需要改变我们的方法来适应环境。\n\nEmscripten 提供了一个`emscripten_set_main_loop`函数，该函数接受一个`em_callback_func`循环函数、`fps`和一个`simulate_infinite_loop`标志。我们不能包含`Game`类并通过`Game.loop()`作为`em_callback_func`参数，因为构建将失败。相反，我们将完全消除`Game`类，并将逻辑移到`main.cpp`文件中。将`game.cpp`的内容复制到`main.cpp`(覆盖已有内容)，删除`Game`类文件(`game.cpp` / `game.h`)。因为我们没有为`Game`声明类，所以从函数中移除`Game::`前缀。构造函数和析构函数不再有效(它们不再是类的一部分)，因此我们需要将该逻辑移动到不同的位置。我们还需要对文件重新排序，以确保我们调用的函数在调用函数之前。最终结果如下:\n\n```cpp\n#include <emscripten/emscripten.h>\n#include <SDL2/SDL.h>\n#include <stdexcept>\n#include \"constants.h\"\n#include \"board.h\"\n#include \"piece.h\"\n\nusing namespace std;\nusing namespace Constants;\n\nstatic SDL_Window *window = nullptr;\nstatic SDL_Renderer *renderer = nullptr;\nstatic Piece currentPiece{ static_cast<Piece::Kind>(rand() % 7) };\nstatic Board board;\nstatic int moveTime;\n\nvoid checkForCollision(const Piece &newPiece) {\n    if (board.isCollision(newPiece)) {\n        board.unite(currentPiece);\n        currentPiece = Piece{ static_cast<Piece::Kind>(rand() % 7) };\n        if (board.isCollision(currentPiece)) board = Board();\n    } else {\n        currentPiece = newPiece;\n    }\n}\n\nvoid handleKeyEvents(SDL_Event &event) {\n    Piece newPiece = currentPiece;\n    switch (event.key.keysym.sym) {\n        case SDLK_DOWN:\n            newPiece.move(0, 1);\n            break;\n        case SDLK_RIGHT:\n            newPiece.move(1, 0);\n            break;\n        case SDLK_LEFT:\n            newPiece.move(-1, 0);\n            break;\n        case SDLK_UP:\n            newPiece.rotate();\n            break;\n        default:\n            break;\n    }\n    if (!board.isCollision(newPiece)) currentPiece = newPiece;\n}\n\nvoid loop() {\n    SDL_Event event;\n    while (SDL_PollEvent(&event)) {\n        switch (event.type) {\n            case SDL_KEYDOWN:\n                handleKeyEvents(event);\n                break;\n            case SDL_QUIT:\n                break;\n            default:\n                break;\n        }\n    }\n\n    SDL_SetRenderDrawColor(renderer, /* Dark Gray: */ 58, 58, 58, 255);\n    SDL_RenderClear(renderer);\n    board.draw(renderer);\n    currentPiece.draw(renderer);\n\n    if (SDL_GetTicks() > moveTime) {\n        moveTime += 1000;\n        Piece newPiece = currentPiece;\n        newPiece.move(0, 1);\n        checkForCollision(newPiece);\n    }\n    SDL_RenderPresent(renderer);\n}\n\nint main() {\n    moveTime = SDL_GetTicks();\n    if (SDL_Init(SDL_INIT_VIDEO) != 0) {\n        throw std::runtime_error(\"SDL_Init(SDL_INIT_VIDEO)\");\n    }\n    SDL_CreateWindowAndRenderer(\n        BoardWidth,\n        BoardHeight,\n        SDL_WINDOW_OPENGL,\n        &window,\n        &renderer);\n\n    emscripten_set_main_loop(loop, 0, 1);\n\n    SDL_DestroyRenderer(renderer);\n    renderer = nullptr;\n    SDL_DestroyWindow(window);\n    window = nullptr;\n    SDL_Quit();\n    return 0;\n}\n```\n\n`handleKeyEvents()`和`checkForCollision()`功能没有变化；我们只是把它们移到文件的顶部。`loop()`功能返回类型根据`emscripten_set_main_loop`要求由`bool`改为`void`。最后，来自构造函数和析构函数的代码被移到`main()`函数中，对`SDL2_ttf`的任何引用都被移除。不是 while 语句调用`Game`的`loop()`功能，而是`emscripten_set_main_loop(loop, 0, 1)`调用。我们更改了文件顶部的`#include`语句，以适应 Emscripten、SDL2 以及我们的`Board`和`Piece`类。这就是改变——现在是时候配置构建和测试游戏了。\n\n# 构建和运行游戏\n\n随着代码的更新和所需的网络资产的出现，是时候构建和测试这个游戏了。编译步骤类似于本书前面的例子，但是我们将使用不同的技术来运行游戏。在本节中，我们将配置构建任务以适应 C++ 文件，并使用 Emscripten 提供的功能运行应用。\n\n# 使用 VS 代码任务构建\n\n我们将以两种方式配置构建:VS 代码任务和 Makefile。如果你喜欢使用不同于 VS 代码的编辑器，Makefiles 是不错的选择。`/.vscode/tasks.json`文件已经包含了构建项目所需的任务。Emscripten 构建步骤是默认的(还存在一组本机构建任务)。让我们浏览一下`tasks`阵列中的每个任务，并回顾正在发生的事情。第一个任务是在构建之前删除任何现有的编译输出文件:\n\n```cpp\n{\n  \"label\": \"Remove Existing Web Files\",\n  \"type\": \"shell\",\n  \"command\": \"rimraf\",\n  \"options\": {\n    \"cwd\": \"${workspaceRoot}/public\"\n  },\n  \"args\": [\n    \"index.js\",\n    \"index.wasm\"\n  ]\n}\n```\n\n第二个任务使用`emcc`命令执行构建:\n\n```cpp\n{\n  \"label\": \"Build WebAssembly\",\n  \"type\": \"shell\",\n  \"command\": \"emcc\",\n  \"args\": [\n    \"--bind\", \"src/board.cpp\", \"src/piece.cpp\", \"src/main.cpp\",\n    \"-std=c++ 14\",\n    \"-O3\",\n    \"-s\", \"WASM=1\",\n    \"-s\", \"USE_SDL=2\",\n    \"-s\", \"MODULARIZE=1\",\n    \"-o\", \"public/index.js\"\n  ],\n  \"group\": {\n    \"kind\": \"build\",\n    \"isDefault\": true\n  },\n  \"problemMatcher\": [],\n  \"dependsOn\": [\"Remove Existing Web Files\"]\n}\n```\n\n相关的参数放在同一行。对`args`数组的唯一新的和不熟悉的增加是带有相应的`.cpp`文件的`--bind`参数。这告诉 Emscripten】之后的所有文件都是构建项目所必需的。通过选择任务|运行构建任务来测试构建...从菜单或使用键盘快捷键*Cmd*/*Ctrl+Shift+B*。构建需要几秒钟，但是终端会在编译过程完成时让您知道。如果成功，您应该会在`/public`文件夹中看到一个`index.js`和`index.wasm`文件。\n\n# 使用 Makefile 构建\n\n如果您不喜欢使用 VS 代码，您可以使用 Makefile 来完成与 VS 代码任务相同的目标。在项目文件夹中创建一个名为`Makefile`的文件，并用以下内容填充它(确保该文件使用制表符，而不是空格):\n\n```cpp\n# This allows you to just run the \"make\" command without specifying\n# arguments:\n.DEFAULT_GOAL := build\n\n# Specifies which files to compile as part of the project:\nCPP_FILES = $(wildcard src/*.cpp)\n\n# Flags to use for Emscripten emcc compile command:\nFLAGS = -std=c++ 14 -O3 -s WASM=1 -s USE_SDL=2 -s MODULARIZE=1 \\\n        --bind $(CPP_FILES)\n\n# Name of output (the .wasm file is created automatically):\nOUTPUT_FILE = public/index.js\n\n# This is the target that compiles our executable\ncompile: $(CPP_FILES)\n    emcc  $(FLAGS) -o $(OUTPUT_FILE)\n\n# Removes the existing index.js and index.wasm files:\nclean:\n    rimraf $(OUTPUT_FILE)\n    rimraf public/index.wasm\n\n# Removes the existing files and builds the project:\nbuild: clean compile\n    @echo \"Build Complete!\"\n```\n\n正在执行的操作与 VS 代码任务相同，只是使用了更通用的工具，只是格式不同。默认的构建步骤是在文件中设置的，因此您可以在项目文件夹中运行以下命令来编译项目:\n\n```cpp\nmake\n```\n\n现在你已经有了一个编译好的 Wasm 文件和 JavaScript 粘合代码，让我们试着运行这个游戏。\n\n# 运行游戏\n\n我们将使用 Emscripten 工具链的内置功能`emrun`，而不是使用 serve 或`browser-sync`。它提供了捕获`stdout`和`stderr`(如果您将`--emrun`链接器标志传递给`emcc`命令)并根据需要将它们打印到终端的额外好处。我们不会使用`--emrun`标志，但是拥有一个本地网络服务器而不需要安装任何额外的依赖项是一个值得注意的额外特性。在项目文件夹中打开一个终端实例，运行以下命令开始游戏:\n\n```cpp\nemrun --browser chrome --no_emrun_detect public/index.html\n```\n\n如果您正在使用浏览器进行开发，您可以为浏览器指定`firefox`。`--no_emrun_detect`标志在终端隐藏了一条消息，说明该网页不具备`emrun`功能。如果导航到`http://localhost:6931/index.html`，应该会看到以下内容:\n\n![](img/700df992-90f3-4452-84da-49e770e1a1c7.png)\n\nTetris running in the browser\n\n尝试旋转和移动零件，以确保一切正常工作。成功清除一行后，ROWS 计数应该增加 1。你可能还会注意到，如果你离棋盘的边缘太近，你将无法旋转一些棋子。恭喜你，你已经成功地将一个 C++ 游戏移植到了 Emscripten！\n\n# 摘要\n\n在这一章中，我们移植了一个用 C++ 编写的俄罗斯方块克隆，它使用 SDL2 来编写脚本，这样它就可以在带有 WebAssembly 的浏览器中运行。我们介绍了俄罗斯方块的规则，以及它们如何映射到现有代码库中的逻辑。我们还分别查看了现有代码库中的每个文件，以及为了成功编译成 Wasm 文件和 JavaScript 粘合代码，必须进行哪些更改。在更新现有代码后，我们创建了所需的 HTML 和 CSS 文件，然后用适当的`emcc`标志配置了构建步骤。一旦建立，游戏就使用 Emscripten 的`emrun`命令运行。\n\n在[第 9 章](09.html)、*与 Node.js* 集成中，我们将讨论如何将 WebAssembly 集成到 Node.js 中，以及这种集成带来的好处。\n\n# 问题\n\n1.  俄罗斯方块里的棋子叫什么？\n2.  选择不将现有的 C++ 代码库移植到 Emscripten 的一个原因是什么？\n3.  我们使用了什么工具来编译游戏(例如，编译成可执行文件)？\n4.  `constants.h`文件的目的是什么？\n5.  为什么我们能够消除 SDL2_ttf 库？\n6.  我们用了哪个 Emscripten 函数开始运行游戏？\n7.  我们在`emcc`命令中添加了哪个参数来构建游戏，它有什么作用？\n8.  `emrun`比`serve`和 Browsersync 这样的工具有什么优势？\n\n# 进一步阅读\n\n*   C++ 头文件:[https://www.sitesbay.com/cpp/cpp-header-files](https://www.sitesbay.com/cpp/cpp-header-files)\n*   SDL 2 Tetris on github:t1]https://github . com/sandn/SDL 2-Tetris\n*   Tetris on github:https://github . com/abesary/Tetris\n*   GitHub 上的俄罗斯方块-Linux:[https://github.com/abesary/tetris-linux](https://github.com/abesary/tetris-linux)"
  },
  {
    "path": "docs/learn-wasm/09.md",
    "content": "# 九、与 Node.js 集成\n\n现代网络在开发和服务器端管理上都严重依赖 Node.js。随着越来越复杂的浏览器应用的出现，这些应用执行计算成本很高的操作，性能的提高会带来难以置信的好处。在本章中，我们将通过使用各种示例来描述将 WebAssembly 与 Node.js 集成的各种方法。\n\n本章的目标是理解以下内容:\n\n*   将 WebAssembly 与 Node.js 集成的优势\n*   如何与节点组件接口进行交互\n*   如何在使用网络包的项目中使用 Wasm 模块\n*   如何使用`npm`库为 WebAssembly 模块编写单元测试\n\n# 为什么是 Node.js？\n\n在[第 3 章](03.html)、*建立开发环境*中，Node.js 被描述为异步事件驱动的 JavaScript 运行时，这是取自官网的定义。然而，Node.js 所代表的是我们构建和管理 web 应用方式的深刻转变。在这一节中，我们将讨论 WebAssembly 和 Node.js 之间的关系，以及为什么这两种技术能够很好地互补。\n\n# 无缝集成\n\nNode.js 运行在谷歌的 V8 JavaScript 引擎上，该引擎为谷歌 Chrome 提供动力。由于 V8 的 WebAssembly 实现遵循*核心规范*，您可以使用与浏览器相同的应用编程接口与 WebAssembly 模块进行交互。您可以使用 Node.js 的`fs`模块将内容读入缓冲区，然后对结果调用`instantiate()`，而不是对`.wasm`文件执行提取调用。\n\n# 补充技术\n\nJavaScript 在服务器端也有局限性。昂贵的计算或处理大量数据可以通过 WebAssembly 的卓越性能进行优化。作为一种脚本语言，JavaScript 擅长自动化简单的任务。您可以编写一个脚本，将 C/C++ 编译成 Wasm 文件，将其复制到`build`文件夹，如果您使用的是像`Browsersync`这样的工具，则可以在浏览器中看到反映的更改。\n\n# 国家预防机制的发展\n\nNode.js 以`npm`的形式拥有广泛的工具和库生态系统。Sven Sauleau 和开源社区的其他成员已经创建了`webassemblyjs`，这是一个用 Node.js 构建的用于 WebAssembly 的广泛工具套件。位于[https://webassembly.js.org](https://webassembly.js.org)的`webassemblyjs`站点包含了用于 WebAssembly 的标语*工具链。目前有超过 20 个`npm`包来执行各种任务和帮助开发，比如一个 ESLint 插件、一个 AST 验证器和一个格式化器。AssemblyScript 是 WebAssembly 编译器的一个 TypeScript，它允许您编写高性能的代码，编译到 Wasm 模块，而无需学习 C 或 C++。Node.js 社区显然被赋予了 WebAssembly 的成功。*\n\n# 服务器端快速 WebAssembly\n\nNode.js 可以通过几种方式为 WebAssembly 项目增加价值。在本节中，我们将介绍一个集成了 WebAssembly 的示例 Node.js 应用。应用使用带有一些简单路由的 Express 从编译的 Wasm 模块调用函数。\n\n# 项目概述\n\n该项目重用了我们在[第 7 章](07.html)、*中构建的应用的一些代码，从零开始创建应用* ( *编写书籍*)来演示 Node.js 如何与 WebAssembly 一起使用。该部分的代码位于`learn-webassembly`存储库中的`/chapter-09-node/server-example`文件夹中。我们将回顾应用中直接适用于 Node.js 的部分。\n\n```cpp\n├── /lib\n│    └── main.c\n├── /src\n|    ├── Transaction.js\n|    ├── /assets\n|    │   ├── db.json\n|    │   ├── main.wasm\n|    │   └── memory.wasm\n|    ├── assign-routes.js\n|    ├── index.js\n|    └── load-assets.js\n├── package.json\n├── package-lock.json\n└── requests.js\n```\n\n关于依赖关系，应用使用`express`和`body-parser`库来建立路由，并从请求体中解析 JSON。对于数据管理，它使用`lowdb`，一个提供读取和更新 JSON 文件方法的库。JSON 文件位于`/src/img/db.json`中，包含从“烹饪书籍”数据集稍微修改的数据。我们使用`nodemon`来观察`/src`文件夹中的变化，并自动重新加载应用。我们使用`rimraf`来管理文件删除。如果您没有在[第 3 章](03.html)、*设置开发环境*中全局安装该库，则该库作为依赖项包含在内。最后，`node-fetch`库允许我们在测试应用时使用提取 API 发出 HTTP 请求。\n\nTo simplify functionality in both the JavaScript and C files, the `rawAmount` and `cookedAmount` fields were replaced with a single `amount` field, and the `category` field is now `categoryId`, which maps to a `categories` array in `db.json`.\n\n# 快速配置\n\n应用加载在`/src/index.js`中。该文件的内容如下所示:\n\n```cpp\nconst express = require('express');\nconst bodyParser = require('body-parser');\nconst loadAssets = require('./load-assets');\nconst assignRoutes = require('./assign-routes');\n\n// If you preface the npm start command with PORT=[Your Port] on\n// macOS/Ubuntu or set PORT=[Your Port] on Windows, it will change the port\n// that the server is running on, so PORT=3001 will run the app on\n// port 3001:\nconst PORT = process.env.PORT || 3000;\n\nconst startApp = async () => {\n  const app = express();\n\n  // Use body-parser for parsing JSON in the body of a request:\n  app.use(bodyParser.urlencoded({ extended: true }));\n  app.use(bodyParser.json());\n\n  // Instantiate the Wasm module and local database:\n  const assets = await loadAssets();\n\n  // Setup routes that can interact with Wasm and the database:\n  assignRoutes(app, assets);\n\n  // Start the server with the specified port:\n  app.listen(PORT, (err) => {\n    if (err) return Promise.reject(err);\n    return Promise.resolve();\n  });\n};\n\nstartApp()\n  .then(() => console.log(`Server is running on port ${PORT}`))\n  .catch(err => console.error(`An error occurred: ${err}`));\n```\n\n这个文件建立了一个新的 Express 应用，添加了`body-parser`中间件，加载了模拟数据库和 Wasm 实例，并分配了路由。让我们继续讨论在浏览器和 Node.js 中实例化一个 Wasm 模块的区别\n\n# 用 Node.js 实例化一个 Wasm 模块\n\nWasm 文件在`/src/load-assets.js`中实例化。我们使用的是《烹饪书》中的`memory.wasm`文件，但是`/img/main.wasm`文件是从位于`/lib`文件夹中的稍微不同版本的`main.c`编译而来的。`loadWasm()`功能执行的操作与烹饪书籍中的 Wasm 初始化代码相同，但是`bufferSource`到`WebAssembly.instantiate()`的传递方法不同。让我们通过查看`load-assets.js file`的`loadWasm()`功能中的部分代码来进一步研究这一点:\n\n```cpp\nconst fs = require('fs');\nconst path = require('path');\n\nconst assetsPath = path.resolve(__dirname, 'assets');\n\nconst getBufferSource = fileName => {\n  const filePath = path.resolve(assetsPath, fileName);\n  return fs.readFileSync(filePath); // <- Replaces the fetch() and .arrayBuffer()\n};\n\n// We're using async/await because it simplifies the Promise syntax\nconst loadWasm = async () => {\n  const wasmMemory = new WebAssembly.Memory({ initial: 1024 });\n  const memoryBuffer = getBufferSource('memory.wasm');\n  const memoryInstance = await WebAssembly.instantiate(memoryBuffer, {\n    env: {\n      memory: wasmMemory\n    }\n  });\n  ...\n```\n\n为了详细说明不同之处，下面是一些使用`fetch`实例化模块的代码:\n\n```cpp\nfetch('main.wasm')\n  .then(response => {\n    if (response.ok) return response.arrayBuffer();\n    throw new Error('Unable to fetch WebAssembly file');\n  })\n  .then(bytes => WebAssembly.instantiate(bytes, importObj));\n```\n\n当使用 Node.js 时，提取调用被`fs.readFileSync()`函数代替，不再需要`arrayBuffer()`函数，因为`fs.readFileSync()`返回一个可以直接传递到`instantiate()`函数的缓冲区。一旦 Wasm 模块被实例化，我们就可以开始与实例进行交互。\n\n# 创建模拟数据库\n\n`load-assets.js`文件还包含创建模拟数据库实例的方法:\n\n```cpp\nconst loadDb = () => {\n  const dbPath = path.resolve(assetsPath, 'db.json');\n  const adapter = new FileSync(dbPath);\n  return low(adapter);\n};\n```\n\n`loadDb()`功能将`/img/db.json`的内容加载到`lowdb`的实例中。从`load-assets.js`导出的默认函数调用`loadWasm()`和`loadDb()`函数，并返回一个包含模拟数据库和 Wasm 实例的对象:\n\n```cpp\nmodule.exports = async function loadAssets() {\n  const db = loadDb();\n  const wasmInstance = await loadWasm();\n  return {\n    db,\n    wasmInstance\n  };\n};\n```\n\n接下来，我将使用术语数据库来引用访问`db.json`文件的`lowdb`实例。现在已经加载了资产，让我们回顾一下应用是如何与它们交互的。\n\n# 与网络组件模块交互\n\n与数据库和 Wasm 实例的交互跨`/src`文件夹中的两个文件进行:`Transaction.js`和`assign-routes.js`。在我们的示例应用中，与应用编程接口的所有通信都是通过 HTTP 请求来执行的。向特定端点发送请求将触发与服务器上的数据库/Wasm 实例的一些交互。让我们从回顾`Transaction.js`开始，它直接与数据库和 Wasm 实例交互。\n\n# 在事务中包装交互\n\n就像库克之书一样，有一个类包装了 Wasm 交互代码，并提供了一个干净的界面。`Transaction.js`的内容和《煮书》中`/src/store/WasmTransactions.js`的内容非常相似。大部分的变化适应了交易记录中的`categoryId`和单个`amount`字段(不再有生熟数量)。增加了与数据库交互的附加功能。例如，这里有一个编辑现有事务的函数，既编辑数据库中的事务，也编辑来自 Wasm 实例的链接列表:\n\n```cpp\ngetValidAmount(transaction) {\n  const { amount, type } = transaction;\n  return type === 'Withdrawal' ? -Math.abs(amount) : amount;\n}\n\nedit(transactionId, contents) {\n  const updatedTransaction = this.db.get('transactions')\n    .find({ id: transactionId })\n    .assign(contents)\n    .write();\n\n  const { categoryId, ...transaction } = updatedTransaction;\n  const amount = this.getValidAmount(transaction);\n  this.wasmInstance._editTransaction(transactionId, categoryId, amount);\n\n  return updatedTransaction;\n}\n```\n\n`edit()`函数用`contents`参数中的值更新与`transactionId`参数对应的数据库记录。`this.db`是在`load-assets.js`文件中创建的数据库实例。由于`categoryId`字段在`updatedTransaction`记录上可用，我们可以直接将其传递给`this.wasmInstance._editTransaction()`。当`Transaction`的新实例被创建时，它被传递到构造函数中。\n\n# assign-routes.js 中的事务操作\n\n`assign-routes.js`文件定义路线并将它们添加到在`index.js`中创建的`express`实例(`app`)中。在快递中，路线可以直接在`app`上定义(例如`app.get()`，或者通过使用`Router`。在这种情况下，使用`Router`向同一路径添加多个方法。以下代码取自`assign-routes.js`文件，创建了一个`Router`实例并添加了两条路由:返回所有事务的`GET`路由和创建新事务的`POST`路由:\n\n```cpp\nmodule.exports = function assignRoutes(app, assets) {\n  const { db, wasmInstance } = assets;\n  const transaction = new Transaction(db, wasmInstance);\n  const transactionsRouter = express.Router();\n\n  transactionsRouter\n    .route('/')\n    .get((req, res) => {\n      const transactions = transaction.findAll();\n      res.status(200).send(transactions);\n    })\n    .post((req, res) => {\n      const { body } = req;\n      if (!body) {\n        return res.status(400).send('Body of request is empty');\n      }\n      const newRecord = transaction.add(body);\n      res.status(200).send(newRecord);\n    });\n\n  ...\n\n  // Set the base path for all routes on transactionsRouter:\n  app.use('/api/transactions', transactionsRouter);\n}\n```\n\n片段末尾的`app.use()`函数指定在`transactionsRouter`实例上定义的所有路线都以`/api/transactions`为前缀。如果您在端口`3000`本地运行应用，您可以在浏览器中导航到`http://localhost:3000/api/transactions`，并看到 JSON 格式的所有事务的数组。\n\n从`get()`和`post()`函数的主体中可以看到，与任何交易记录的交互都被委托给第`3`行中创建的`Transaction`实例。这就完成了我们对代码库相关部分的审查。每个文件都包含描述文件功能和用途的注释，因此您可能需要在进入下一部分之前查看这些注释。在下一节中，我们将构建、运行并与应用交互。\n\n# 构建和运行应用\n\n在我们构建和测试项目之前，您需要安装`npm`依赖项。打开`/server-example`文件夹中的终端，运行以下命令:\n\n```cpp\nnpm install\n```\n\n一旦完成，您就可以进入构建步骤了。\n\n# 构建应用\n\n在此应用中，构建是指使用`emcc`命令将`lib/main.c`编译为`.wasm`文件。由于这是一个 Node.js 项目，我们可以使用`package.json`文件中的`scripts`键来定义任务。您仍然可以使用 VS 代码的任务功能，因为它会自动从您的`package.json`文件中检测脚本，并在您选择任务|运行任务时将它们显示在任务列表中...从菜单上。以下代码包含本项目`package.json`文件中`scripts`部分的内容:\n\n```cpp\n\"scripts\": {\n  \"prebuild\": \"rimraf src/img/main.wasm\",\n  \"build\": \"emcc lib/main.c -Os -s WASM=1 -s SIDE_MODULE=1\n           -s BINARYEN_ASYNC_COMPILATION=0 -s ALLOW_MEMORY_GROWTH=1\n           -o src/img/main.wasm\",\n  \"start\": \"node src/index.js\",\n  \"watch\": \"nodemon src/* --exec 'npm start'\"\n},\n```\n\n`build`脚本被分割成多行用于显示目的，因此您必须组合这些行以获得有效的 JSON。`prebuild`脚本删除现有的 Wasm 文件，`build`脚本运行带有所需标志的`emcc`命令来编译`lib/main.c`并将结果输出到`src/img/main.wasm`。要运行脚本，请在`/server-example`文件夹中打开一个终端，并运行以下命令:\n\n```cpp\nnpm run build\n```\n\n如果`/src/assets`文件夹包含名为`main.wasm`的文件，则构建成功完成。如果发生了错误，终端应该提供错误的描述，以及堆栈跟踪。\n\nYou can create `npm` scripts that run before or after a specific script by creating an entry with the same name and prefixing it with `pre` or `post`. For example, if you wanted to run a script after the `build` script has completed, you can create a script named `\"postbuild\"` and specify the command you want to run.\n\n# 启动并测试应用\n\n如果您正在对应用进行更改或试图修复一个错误，您可以使用`watch`脚本来监视`/src`文件夹内容的任何更改，并在发生更改时自动重新启动应用。由于我们只是运行和测试应用，我们可以使用`start`命令来代替。在终端中，确保您在`/server-example`文件夹中，并运行以下命令:\n\n```cpp\nnpm start\n```\n\n你应该会看到一条信息，上面写着`Server is running on port 3000`。您现在可以向服务器发送 HTTP 请求了。要测试应用，请在`server-example`目录中打开一个新的终端实例，并运行以下命令:\n\n```cpp\nnode ./requests.js 1\n```\n\n这将注销对`/api/transactions`端点的`GET`调用的响应主体。`requests.js`文件包含允许您请求所有可用路线的功能。`getFetchActionForId()`函数返回一个带有端点和选项值的对象，该对象对应于`assign-routes.js`文件中的一条路线。`actionId`是一个任意数字，用于简化测试和减少运行命令的输入量。例如，您可以运行以下命令:\n\n```cpp\nnode ./requests.js 5\n```\n\n它将注销*计算机&互联网*类别的所有交易的总和。如果需要不同类别的总数，可以向`node`命令传递一个额外的参数。要获取*保险*类别中所有交易的总和，请运行以下命令:\n\n```cpp\nnode ./requests.js 5 3\n```\n\n尝试检查每个请求(总共有八个)。如果您提出添加、删除或编辑事务的请求，您应该会在`/src/img/db.json`文件中看到更改。Node.js 示例项目到此结束。在下一节中，我们将利用 Webpack 加载一个 Wasm 模块并与之交互。\n\n# 带有网络包的客户端网络组件\n\nWeb 应用的复杂性和规模持续增长。简单地提供一些手写的 HTML、CSS 和 JavaScript 文件对于大型应用来说是不可行的。为了管理这种复杂性，web 开发人员使用 bundlers 来实现模块化，确保浏览器兼容性，并减小 JavaScript 文件的大小。在这一节中，我们将使用一个流行的打包器，网络包，在不使用`emcc`的情况下使用 Wasm。\n\n# 项目概述\n\n示例 Webpack 应用扩展了我们在 [第 5 章](05.html)*创建和加载 WebAssembly 模块*的*编译 C 而不使用粘合代码*部分中编写的 C 代码的功能。我们将展示一个在马头星云周围跳跃的宇宙飞船中的外星人，而不是展示一个围绕红色背景跳跃的蓝色矩形。碰撞检测功能已被修改，以适应矩形内的弹跳，因此飞船的运动将是随机的。该部分的代码位于`learn-webassembly`存储库中的`/chapter-09-node/webpack-example`文件夹中。项目的文件结构如以下代码所示:\n\n```cpp\n├── /src\n│    ├── /assets\n│    │    ├── background.jpg\n│    │    └── spaceship.svg\n│    ├── App.js\n│    ├── index.html\n│    ├── index.js\n│    ├── main.c\n│    └── styles.css\n├── package.json\n├── package-lock.json\n└── webpack.config.js\n```\n\n我们将在后面的章节中查看 Webpack 配置文件。现在，让我们花点时间更详细地讨论一下 Webpack。\n\n# 什么是 Webpack？\n\n在过去的几年里，JavaScript 生态系统一直在快速发展，导致新的框架和库不断涌现。Bundlers 的出现是为了让开发人员能够将一个 JavaScript 应用拆分成几个文件，而不必担心管理全局名称空间、脚本加载顺序或 HTML 文件中令人难以置信的一长串`<script>`标签。bundler 将所有文件合并成一个文件，并解决任何命名冲突。\n\n在撰写本文时，Webpack 是最受欢迎的前端开发工具之一。然而，它不仅仅是组合 JavaScript 文件。它还执行复杂的任务，如代码拆分和树摇动(死代码消除)。Webpack 是用插件架构设计的，这导致了大量社区开发的插件。在`npm`上搜索 Webpack，目前返回超过 12000 个包裹！这个插件的详尽列表，以及它强大的内置特性集，使 Webpack 成为一个成熟的构建工具。\n\n# 安装和配置网络包\n\n在我们开始应用演练之前，在`/webpack-example`文件夹中打开一个终端并运行以下命令:\n\n```cpp\nnpm install \n```\n\n# 依赖性概述\n\n该应用使用 Webpack 的版本 4(编写本文时的最新版本)来构建我们的应用。我们需要使用网络包插件来加载应用中使用的各种文件类型，并使用巴贝尔来利用更新的 JavaScript 特性。以下片段列出了我们在项目中使用的`devDependencies`(摘自`package.json`):\n\n```cpp\n...\n\"devDependencies\": {\n  \"@babel/core\": \"^7.0.0-rc.1\",\n  \"@babel/preset-env\": \"^7.0.0-rc.1\",\n  \"babel-loader\": \"^8.0.0-beta.4\",\n  \"cpp-wasm-loader\": \"0.7.7\",\n  \"css-loader\": \"1.0.0\",\n  \"file-loader\": \"1.1.11\",\n  \"html-loader\": \"0.5.5\",\n  \"html-webpack-plugin\": \"3.2.0\",\n  \"mini-css-extract-plugin\": \"0.4.1\",\n  \"rimraf\": \"2.6.2\",\n  \"webpack\": \"4.16.5\",\n  \"webpack-cli\": \"3.1.0\",\n  \"webpack-dev-server\": \"3.1.5\"\n},\n...\n```\n\n我为一些库指定了确切的版本，以确保应用成功构建和运行。任何以`-loader`或`-plugin`结尾的库都与 Webpack 一起使用。`cpp-wasm-loader`库允许我们直接导入一个 C 或 C++ 文件，而不必先将其编译成 Wasm。Webpack 4 内置了对导入`.wasm`文件的支持，但是您不能指定`importObj`参数，这是用 Emscripten 生成的模块所必需的。\n\n# 在 webpack.config.js 中配置加载器和插件\n\n除了 JavaScript 之外，我们还为应用使用了几种不同的文件类型:CSS、SVG、HTML 等等。安装`-loader`依赖项只是等式的一部分——你还需要告诉网络包如何加载它们。您还需要为您安装的任何插件指定配置详细信息。您可以在项目根文件夹的`webpack.config.js`文件中指定加载和配置细节。以下片段包含`/webpack-example/webpack.config.js`的内容:\n\n```cpp\nconst HtmlWebpackPlugin = require('html-webpack-plugin');\nconst MiniCssExtractPlugin = require('mini-css-extract-plugin');\n\nmodule.exports = {\n  module: {\n    rules: [\n      {\n        test: /\\.js$/,\n        exclude: /node_modules/,\n        use: {\n          loader: 'babel-loader',\n          options: {\n            // We need this to use async/await:\n            presets: [\n              [\n                '@babel/preset-env', {\n                  targets: { node: '10' }\n                }\n              ]\n            ]\n          }\n        }\n      },\n      {\n        test: /\\.html$/,\n        use: {\n          loader: 'html-loader',\n          options: { minimize: true }\n        }\n      },\n      {\n        test: /\\.css$/,\n        use: [MiniCssExtractPlugin.loader, 'css-loader']\n      },\n      {\n        test: /\\.(c|cpp)$/,\n        use: {\n          loader: 'cpp-wasm-loader',\n          options: {\n            emitWasm: true\n          }\n        }\n      },\n      {\n        test: /\\.(png|jpg|gif|svg)$/,\n        use: {\n          loader: 'file-loader',\n          options: {\n            name: 'img/[name].[ext]'\n          }\n        }\n      }\n    ]\n  },\n  plugins: [\n    new HtmlWebpackPlugin({\n      template: './src/index.html',\n      filename: './index.html'\n    }),\n    // This is used for bundling (building for production):\n    new MiniCssExtractPlugin({\n      filename: '[name].css',\n      chunkFilename: '[id].css'\n    })\n  ]\n};\n```\n\n`rules`部分告诉 Webpack 使用哪个加载程序作为文件扩展名。数组中的第四项处理 C/C++ 文件(注意包含`c|cpp`的`test`字段值)。`HtmlWebpackPlugin`获取`/src/index.html`的内容，添加任何需要的`<script>`标签，将其缩小，并在`build`文件夹中创建一个`index.html`，默认为`/dist`。`MiniCssExtractPlugin`将任何导入的 CSS 复制到`/dist`文件夹中的单个 CSS 文件中。我们将在后面的小节中回顾如何构建项目，因此让我们继续讨论应用代码，从 C 文件开始。\n\n# C 代码\n\n因为我们可以直接导入 C 和 C++ 文件，所以 C 文件位于`/src`文件夹中。该文件`main.c`包含管理碰撞检测和在`<canvas>`周围移动飞船的逻辑。代码基于我们在[第 5 章](05.html)、*创建和加载 WebAssembly 模块*中创建的`without-glue.c`文件。我们不打算审查整个文件，只审查已经更改并值得解释的部分。让我们从定义和声明部分开始，其中包括一个新的`struct` : `Bounds`。\n\n# 定义和声明\n\n包含定义和声明部分的代码如下所示:\n\n```cpp\ntypedef struct Bounds {\n  int width;\n  int height;\n} Bounds;\n\n// We're using the term \"Rect\" to represent the rectangle the\n// image occupies:\ntypedef struct Rect {\n  int x;\n  int y;\n  int width;\n  int height;\n  // Horizontal direction of travel (L/R):\n  char horizDir;\n  // Vertical direction of travel (U/D):\n  char vertDir;\n} Rect;\n\nstruct Bounds bounds;\nstruct Rect rect;\n```\n\n新属性被添加到现有的`Rect`定义中，以适应在 *x* 和 *y* 方向上的灵活尺寸和跟踪移动。我们定义了一个新的`struct`、`Bounds`，并删除了现有的`#define`语句，因为`<canvas>`元素不再是静态维度的正方形。当模块加载时，会声明这两个元素的新实例。这些实例的尺寸属性在`start()`函数中指定，我们接下来将介绍该函数。\n\n# start()函数\n\n更新后的`start()`功能作为模块的入口点，如下所示:\n\n```cpp\nEMSCRIPTEN_KEEPALIVE\nvoid start(int boundsWidth, int boundsHeight, int rectWidth,\n           int rectHeight) {\n    rect.x = 0;\n    rect.y = 0;\n    rect.horizDir = 'R';\n    rect.vertDir = 'D';\n    rect.width = rectWidth;\n    rect.height = rectHeight;\n    bounds.width = boundsWidth;\n    bounds.height = boundsHeight;\n    setIsRunning(true);\n}\n```\n\n从 JavaScript 调用的任何函数都带有`EMSCRIPTEN_KEEPALIVE`语句。我们现在将`Bounds`和`Rect`元素的宽度和高度作为参数传递给`start()`函数，并将其分配给局部`bounds`和`rect`变量。这使我们可以轻松地改变其中任何一个的尺寸，而不必对碰撞检测逻辑进行任何更改。在本申请的上下文中，`rect`代表宇宙飞船图像所在的矩形。我们为`rect`设置了默认的水平和垂直方向，因此图像最初会向右下方移动。让我们进入`rect`运动/碰撞检测代码。\n\n# 函数的作用是\n\n与碰撞检测和`Rect`移动相关的代码在`updateRectLocation()`功能中处理，如下所示:\n\n```cpp\n/**\n * Updates the rectangle location by +/- 1px in the x or y based on\n * the current location.\n */\nvoid updateRectLocation() {\n    // Determine if the bounding rectangle has \"bumped\" into either\n    // the left/right side or top/bottom side. Depending on which side,\n    // flip the direction:\n    int xBouncePoint = bounds.width - rect.width;\n    if (rect.x == xBouncePoint) rect.horizDir = 'L';\n    if (rect.x == 0) rect.horizDir = 'R';\n\n    int yBouncePoint = bounds.height - rect.height;\n    if (rect.y == yBouncePoint) rect.vertDir = 'U';\n    if (rect.y == 0) rect.vertDir = 'D';\n\n    // If the direction has changed based on the x and y\n    // coordinates, ensure the x and y points update\n    // accordingly:\n    int horizIncrement = 1;\n    if (rect.horizDir == 'L') horizIncrement = -1;\n    rect.x = rect.x + horizIncrement;\n\n    int vertIncrement = 1;\n    if (rect.vertDir == 'U') vertIncrement = -1;\n    rect.y = rect.y + vertIncrement;\n}\n```\n\n该代码与我们在 [第 5 章](05.html)*中编写的创建和加载 WebAssembly 模块*的代码之间的主要区别在于冲突检测逻辑。该函数不再简单地水平跟踪`rect`实例的位置，并在它到达右侧边界时改变方向，而是跟踪水平和垂直方向，并独立管理每个方向。虽然这不是性能最好的算法，但它确实达到了确保飞船在遇到`<canvas>`边缘时改变方向的目的。\n\n# JavaScript 代码\n\n我们对应用使用的唯一生产依赖是 Vue。虽然应用由单个组件组成，但 Vue 使管理数据、功能和组件生命周期比手动操作简单得多。`index.js`文件包含 Vue 初始化代码，渲染和应用逻辑在`/src/App.js`中。这个文件有很多可移动的部分，所以我们将按照前一节所做的那样，分块查看代码。让我们从`import`的陈述开始。\n\n# 导入语句\n\n下面的代码演示了运行中的 Webpack 加载程序:\n\n```cpp\n// This is loaded using the css-loader dependency:\nimport './styles.css';\n\n// This is loaded using the cpp-wasm-loader dependency:\nimport wasm from './main.c';\n\n// These are loaded using the file-loader dependency:\nimport backgroundImage from './img/background.jpg';\nimport spaceshipImage from './img/spaceship.svg';\n```\n\n我们在`webpack.config.js`文件中配置的加载器了解如何处理 CSS、C 和图像文件。现在我们已经有了所需的可用资源，我们可以开始定义我们的组件状态了。\n\n# 组件状态\n\n下面的代码为我们的组件初始化`data()`函数中的本地状态:\n\n```cpp\nexport default {\n  data() {\n    return {\n      instance: null,\n      bounds: { width: 800, height: 592 },\n      rect: { width: 200, height: 120 },\n      speed: 5\n    };\n  },\n  ...\n```\n\n虽然`bounds`和`rect`属性从未改变，但我们在本地状态下定义了它们，以将组件使用的所有数据保存在一个位置。`speed`号的属性决定了宇宙飞船穿越`<canvas>`的速度，范围从`1`到`10`。`instance`属性被初始化为空，但将用于访问编译后的 Wasm 模块的导出函数。让我们继续看 Wasm 初始化代码，它编译 Wasm 文件并填充`<canvas>`。\n\n# Wasm 初始化\n\n编译 Wasm 文件并填充`<canvas>`元素的代码如下所示:\n\n```cpp\nmethods: {\n  // Create a new Image instance to pass into the drawImage function\n  // for the <canvas> element's context:\n  loadImage(imageSrc) {\n    const loadedImage = new Image();\n    loadedImage.src = imageSrc;\n    return new Promise((resolve, reject) => {\n      loadedImage.onload = () => resolve(loadedImage);\n      loadedImage.onerror = () => reject();\n    });\n  },\n\n  // Compile/load the contents of main.c and assign the resulting\n  // Wasm module instance to the components this.instance property:\n  async initializeWasm() {\n    const ctx = this.$refs.canvas.getContext('2d');\n\n    // Create Image instances of the background and spaceship.\n    // These are required to pass into the ctx.drawImage() function:\n    const [bouncer, background] = await Promise.all([\n      this.loadImage(spaceshipImage),\n      this.loadImage(backgroundImage)\n    ]);\n\n    // Compile the C code to Wasm and assign the resulting\n    // module.exports to this.instance:\n    const { width, height } = this.bounds;\n    return wasm\n      .init(imports => ({\n        ...imports,\n        _jsFillRect(x, y, w, h) {\n          ctx.drawImage(bouncer, x, y, w, h);\n        },\n        _jsClearRect() {\n          ctx.drawImage(background, 0, 0, width, height);\n        }\n      }))\n        .then(module => {\n          this.instance = module.exports;\n          return Promise.resolve();\n        });\n  },\n  ...\n```\n\n在组件的`methods`键中定义了额外的函数，但是现在我们将重点关注将导入的 C 文件编译成 Wasm 的代码。为宇宙飞船和背景图像创建`Image`实例后，`main.c`文件(作为`.wasm`导入)被编译到一个 Wasm 模块，生成的`exports`被分配给`this.instance`。一旦这些操作完成，就可以从导出的 Wasm 模块调用`start()`功能。由于`initializeWasm()`函数调用`<canvas>`元素的`getContext()`函数，因此需要先安装组件，然后才能调用该函数。让我们回顾一下其余的`methods`定义和`mounted()`事件处理程序。\n\n# 元件安装\n\n其余`methods`定义和`mounted()`事件处理函数如下所示:\n\n```cpp\n  ...\n  // Looping function to move the spaceship across the canvas.\n  loopRectMotion() {\n    setTimeout(() => {\n      this.instance.moveRect();\n      if (this.instance.getIsRunning()) this.loopRectMotion();\n    }, 15 - this.speed);\n  },\n  // Pauses/resumes the spaceship's movement when the button is\n  // clicked:\n  onActionClick(event) {\n    const newIsRunning = !this.instance.getIsRunning();\n    this.instance.setIsRunning(newIsRunning);\n    event.target.innerHTML = newIsRunning ? 'Pause' : 'Resume';\n    if (newIsRunning) this.loopRectMotion();\n  }\n},\nmounted() {\n  this.initializeWasm().then(() => {\n    this.instance.start(\n      this.bounds.width,\n      this.bounds.height,\n      this.rect.width,\n      this.rect.height\n    );\n    this.loopRectMotion();\n  });\n},\n```\n\n一旦 Wasm 模块被编译，就可以在`this.instance`上访问`start()`功能。`bounds`和`rect`尺寸传入`start()`功能，然后调用`loopRectFunction()`开始移动飞船。`onActionClick()`事件处理器功能根据飞船当前是否在运动来暂停或恢复飞船的运动。\n\n`loopRectMotion()`的功能与 [第 5 章](05.html)*创建和加载网络组件模块*的示例代码相同，只是速度现在可以调整。规定超时长度的`15 - this.speed`计算可能看起来有点奇怪。由于图像的移动速度是基于函数调用之间经过的时间，增加这个数字实际上会减慢飞船的速度。因此，从`15`中减去`this.speed`，选择 T3 是因为它比`10`稍大，但是如果`this.speed`增加到最大，不会使飞船变得模糊。这就是组件逻辑；让我们进入代码的渲染部分，在这里定义了`template`。\n\n# 组件渲染\n\n`template`属性的内容规定了要渲染的内容，如下所示:\n\n```cpp\ntemplate: `\n  <div class=\"flex column\">\n   <h1>SPACE WASM!</h1>\n    <canvas\n      ref=\"canvas\"\n      :height=\"bounds.height\"\n      :width=\"bounds.width\">\n    </canvas>\n    <div class=\"flex controls\">\n      <div>\n        <button class=\"defaultText\" @click=\"onActionClick\">\n          Pause\n        </button>\n      </div>\n    <div class=\"flex column\">\n      <label class=\"defaultText\" for=\"speed\">Speed: {{speed}}</label>\n      <input\n        v-model=\"speed\"\n        id=\"speed\"\n        type=\"range\"\n        min=\"1\"\n        max=\"10\"\n        step=\"1\">\n    </div>\n  </div>\n</div>\n\n```\n\n由于我们使用 Vue，我们可以将 HTML 元素的属性和事件处理程序绑定到组件中定义的属性和方法。除了暂停/恢复按钮，还有一个范围`<input>`允许你改变速度。通过向左或向右滑动，你可以减慢或加快飞船的速度，并立即看到变化。我们的审查到此结束；让我们看看如何使用 Webpack 来构建或运行应用。\n\n# 构建和运行应用\n\n使用`cpp-wasm-loader`库消除了生成 Wasm 模块的构建步骤，但是我们仍然需要打包我们的应用进行分发。在`package.json`的`scripts`部分，有一个`build`和`start`的脚本。运行`build`脚本执行生成包的`webpack`命令。为确保正常工作，在`/webpack-example`文件夹中打开一个终端实例并运行以下命令:\n\n```cpp\nnpm run build\n```\n\n第一次运行项目时，可能需要一分钟来构建它。这可以归因于 Wasm 编译步骤。然而，后续的构建应该会快得多。如果构建成功，您应该会看到一个新创建的`/dist`文件夹，其内容如下:\n\n```cpp\n├── /assets\n│    ├── background.jpg\n│    └── spaceship.svg\n├── index.html\n├── main.css\n├── main.js\n└── main.wasm\n```\n\n# 测试构建\n\n让我们尝试一下构建，以确保一切正常工作。在终端实例中运行以下命令来启动应用:\n\n```cpp\nserve -l 8080 dist\n```\n\n如果您在浏览器中导航到`http://127.0.0.1:8080/index.html`，您应该会看到:\n\n![](img/9d3c1c1f-1f92-41e2-a5e6-27673bc221d8.png)\n\nWebpack application running in the browser\n\n宇宙飞船图像(取自[https://commons . wikimedia . org/wiki/File:Alien _ 太空船 _-_SVG_Vector.svg](https://commons.wikimedia.org/wiki/File:Alien_Spaceship_-_SVG_Vector.svg) )在马头星云背景图像(取自[https://commons . wikimedia . org/wiki/File:马头星云 _ Christmas _ 2017 _ deography . jpg](https://commons.wikimedia.org/wiki/File:Horsehead_Nebula_Christmas_2017_Deography.jpg))的边界内来回跳动。当按下暂停按钮时，按钮的标题变为恢复，船只停止移动。再次按下按钮将标题更改回“暂停”,船只将再次开始移动。调整速度滑块可增加或减少船速。\n\n# 运行启动脚本\n\n该应用安装了`webpack-dev-server`库，其运行方式类似于`Browsersync.`库使用 LiveReloading，当您对`/src`中的文件进行任何更改时，它会自动更新该应用。由于我们对 C 和 C++ 文件使用了网络包加载器，如果您也更改了 C 文件，自动更新事件将会触发。运行以下命令来启动应用并观察更改:\n\n```cpp\nnpm start\n```\n\n当构建完成时，浏览器窗口应该会自动打开，然后将您导向正在运行的应用。要查看实时重装功能的运行情况，请尝试将`main.c`中`setIsRunning()`函数的`isRunning`变量的值设置为 false，而不是`newIsRunning`:\n\n```cpp\nEMSCRIPTEN_KEEPALIVE\nvoid setIsRunning(bool newIsRunning) {\n    // isRunning = newIsRunning;\n\n    // Set the value to always false:\n    isRunning = false;\n}\n```\n\n宇宙飞船应该卡在左上角。如果你把它换回来，宇宙飞船又开始移动了。在下一节中，我们将用 JavaScript 编写单元测试来测试 WebAssembly 模块。\n\n# 用 Jest 测试 WebAssembly 模块\n\n经过良好测试的代码可以防止回归错误，简化重构，并减轻添加新特性带来的一些挫折。一旦你编译了一个 Wasm 模块，你应该编写测试来确保它如预期的那样运行，即使你已经为你编译它的 C、C++ 或 Rust 代码编写了测试。在这一节中，我们将使用 **Jest** ，一个 JavaScript 测试框架，来测试编译后的 Wasm 模块中的函数。\n\n# 正在测试的代码\n\n本例中使用的所有代码都位于`/chapter-09-node/testing-example`文件夹中。代码和相应的测试非常简单，不代表真实世界的应用，但是它们旨在演示如何使用 Jest 进行测试。以下代码表示`/testing-example`文件夹的文件结构:\n\n```cpp\n├── /src\n|    ├── /__tests__\n|    │    └── main.test.js\n|    └── main.c\n├── package.json\n└── package-lock.json\n```\n\n我们将要测试的 C 文件的内容`/src/main.c`，如下所示:\n\n```cpp\nint addTwoNumbers(int leftValue, int rightValue) {\n    return leftValue + rightValue;\n}\n\nfloat divideTwoNumbers(float leftValue, float rightValue) {\n    return leftValue / rightValue;\n}\n\ndouble findFactorial(float value) {\n    int i;\n    double factorial = 1;\n\n    for (i = 1; i <= value; i++) {\n        factorial = factorial * i;\n    }\n    return factorial;\n}\n```\n\n文件中的三个函数都在执行简单的数学运算。`package.json`文件包含一个脚本，将 C 文件编译成 Wasm 文件进行测试。运行以下命令编译 C 文件:\n\n```cpp\nnpm run build\n```\n\n在`/src`目录中应该有一个名为`main.wasm`的文件。让我们继续描述测试配置步骤。\n\n# 测试配置\n\n我们将在这个例子中使用的唯一依赖项是 Jest，一个由脸书构建的 JavaScript 测试框架。Jest 是一个很好的测试选择，因为它包含了您开箱即用的大部分特性，比如覆盖、断言和嘲讽。在大多数情况下，您可以使用零配置，这取决于应用的复杂性。如果你有兴趣了解更多，请访问 Jest 的网站。在`/chapter-09-node/testing-example`文件夹中打开一个终端实例，运行以下命令安装 Jest:\n\n```cpp\nnpm install\n```\n\n在`package.json`文件中，`scripts`部分有三个条目:`build`、`pretest`和`test`。`build`脚本执行带有所需标志的`emcc`命令，将`/src/main.c`编译为`/src/main.wasm`。`test`脚本使用`--verbose flag`执行`jest`命令，这为每个测试套件提供了额外的细节。`pretest`脚本只是运行`build`脚本，以确保在运行任何测试之前`/src/main.wasm`存在。\n\n# 测试文件审查\n\n让我们遍历位于`/src/__tests__/main.test.js`的测试文件，回顾每一段代码的用途。测试文件的第一部分实例化`main.wasm`文件，并将结果分配给本地`wasmInstance`变量:\n\n```cpp\nconst fs = require('fs');\nconst path = require('path');\n\ndescribe('main.wasm Tests', () => {\n  let wasmInstance;\n\n  beforeAll(async () => {\n    const wasmPath = path.resolve(__dirname, '..', 'main.wasm');\n    const buffer = fs.readFileSync(wasmPath);\n    const results = await WebAssembly.instantiate(buffer, {\n      env: {\n        memoryBase: 0,\n        tableBase: 0,\n        memory: new WebAssembly.Memory({ initial: 1024 }),\n        table: new WebAssembly.Table({ initial: 16, element: 'anyfunc' }),\n        abort: console.log\n      }\n    });\n    wasmInstance = results.instance.exports;\n  });\n ...\n```\n\nJest 提供了生命周期方法来在运行测试之前执行任何设置或拆卸操作。您可以指定在所有测试之前或之后运行的功能(`beforeAll()` / `afterAll()`)，或者在每个测试之前或之后运行的功能(`beforeEach()` / `afterEach()`)。我们需要一个 Wasm 模块的编译实例，从中我们可以调用导出的函数，所以我们将实例化代码放在`beforeAll()`函数中。\n\n我们将整个测试套件包装在文件的`describe()`块中。Jest 使用一个`describe()`函数来封装相关测试的套件，并使用`test()`或`it()`来表示单个测试。这个概念有一个简单的例子:\n\n```cpp\nconst add = (a, b) => a + b;\n\ndescribe('the add function', () => {\n  test('returns 6 when 4 and 2 are passed in', () => {\n    const result = add(4, 2);\n    expect(result).toEqual(6);\n  });\n\n  test('returns 20 when 12 and 8 are passed in', () => {\n    const result = add(12, 8);\n    expect(result).toEqual(20);\n  });\n});\n```\n\n下一段代码包含每个导出函数的所有测试套件和测试:\n\n```cpp\n...\n  describe('the _addTwoNumbers function', () => {\n    test('returns 300 when 100 and 200 are passed in', () => {\n      const result = wasmInstance._addTwoNumbers(100, 200);\n      expect(result).toEqual(300);\n    });\n\n    test('returns -20 when -10 and -10 are passed in', () => {\n      const result = wasmInstance._addTwoNumbers(-10, -10);\n      expect(result).toEqual(-20);\n    });\n  });\n\n  describe('the _divideTwoNumbers function', () => {\n    test.each([\n      [10, 100, 10],\n      [-2, -10, 5],\n    ])('returns %f when %f and %f are passed in', (expected, a, b) => {\n      const result = wasmInstance._divideTwoNumbers(a, b);\n      expect(result).toEqual(expected);\n    });\n\n    test('returns ~3.77 when 20.75 and 5.5 are passed in', () => {\n      const result = wasmInstance._divideTwoNumbers(20.75, 5.5);\n      expect(result).toBeCloseTo(3.77, 2);\n    });\n  });\n\n  describe('the _findFactorial function', () => {\n    test.each([\n      [120, 5],\n      [362880, 9.2],\n    ])('returns %p when %p is passed in', (expected, input) => {\n      const result = wasmInstance._findFactorial(input);\n      expect(result).toEqual(expected);\n    });\n  });\n});\n```\n\n对于`_addTwoNumbers()`函数，第一个`describe()`块有两个`test()`实例，以确保函数返回作为参数传入的两个数字的和。接下来的两个`describe()`模块，用于`_divideTwoNumbers()`和`_findFactorial()`功能，使用 Jest 的`.each`功能，允许你用不同的数据运行相同的测试。`expect()`函数允许您对作为参数传入的值进行断言。最后一个`_divideTwoNumbers()`测试中的`.toBeCloseTo()`断言检查结果是否在`3.77`的两位小数以内。其余使用`.toEqual()`断言检查是否相等。\n\n用 Jest 编写测试相对简单，运行测试更容易！让我们试着运行我们的测试，并查看 Jest 提供的一些命令行界面标志。\n\n# 运行测试\n\n要运行测试，请在`/chapter-09-node/testing-example`文件夹中打开一个终端实例，并运行以下命令:\n\n```cpp\nnpm test\n```\n\n您应该会在终端中看到以下输出:\n\n```cpp\nmain.wasm Tests\n  the _addTwoNumbers function\n    ✓ returns 300 when 100 and 200 are passed in (4ms)\n    ✓ returns -20 when -10 and -10 are passed in\n  the _divideTwoNumbers function\n    ✓ returns 10 when 100 and 10 are passed in\n    ✓ returns -2 when -10 and 5 are passed in (1ms)\n    ✓ returns ~3.77 when 20.75 and 5.5 are passed in\n  the _findFactorial function\n    ✓ returns 120 when 5 is passed in (1ms)\n    ✓ returns 362880 when 9.2 is passed in\n\nTest Suites: 1 passed, 1 total\nTests: 7 passed, 7 total\nSnapshots: 0 total\nTime: 1.008s\nRan all test suites.\n```\n\n如果您有大量的测试，您可以从`package.json`中的`test`脚本中移除`--verbose`标志，并且仅在需要时将该标志传递给`npm test`命令。还有其他几个命令行界面标志可以传递给`jest`命令。以下列表包含一些更常用的标志:\n\n*   `--bail`:在第一个测试套件失败时，立即退出测试套件\n*   `--coverage`:收集测试覆盖率，并在测试运行后显示在终端中\n*   `--watch`:监视文件的更改，并重新运行与更改文件相关的测试\n\n您可以通过在`--`之后添加这些标志来将它们传递给`npm`测试命令。例如，如果您想使用`--bail`标志，您可以运行以下命令:\n\n```cpp\nnpm test -- --bail\n```\n\n您可以在官方网站[https://jestjs.io/docs/en/cli](https://jestjs.io/docs/en/cli)查看命令行界面选项的完整列表。\n\n# 摘要\n\n在本章中，我们讨论了将 WebAssembly 与 Node.js 集成的优势，并演示了如何在服务器端和客户端使用 Node.js。我们评估了一个使用 Wasm 模块对会计交易进行计算的 Express 应用。然后，我们回顾了一个基于浏览器的应用，它利用 Webpack 从一个 C 文件中导入和调用函数，而无需编写任何 Wasm 实例化代码。最后，我们看到了如何利用 Jest 测试框架来测试编译后的模块，并确保其正常运行。在[第 10 章](10.html)、*高级工具和即将推出的功能*中，我们将介绍高级工具，并讨论 WebAssembly 即将推出的功能。\n\n# 问题\n\n1.  将 WebAssembly 与 Node.js 集成的优势之一是什么？\n2.  Express 应用使用什么库来读写数据到 JSON 文件？\n3.  在浏览器中加载模块和在 Node.js 中加载模块有什么区别？\n4.  在现有的`npm`脚本之前或之后，你可以使用什么技巧来运行`npm`脚本？\n5.  Webpack 为消除死代码而执行的任务叫什么？\n6.  Webpack 中加载程序的目的是什么？\n7.  `describe()`和`test()`在 Jest 中的作用有什么区别？\n8.  如何将额外的命令行界面标志传递给`npm test`命令？\n\n# 进一步阅读\n\n*   快递:[https://expressjs.com](https://expressjs.com)\n*   web pack:https://web pack . js . org\n*   jest API:https://jestjs . io/docs/en/API"
  },
  {
    "path": "docs/learn-wasm/10.md",
    "content": "# 十、高级工具和即将推出的功能\n\nWebAssembly 的生态系统在不断发展和演变。开发人员已经看到了 WebAssembly 的潜力。他们构建工具来改善开发体验或从他们选择的语言中输出 Wasm 模块(尽管有一些限制)。\n\n在本章中，我们将评估使 WebAssembly 成功的底层技术。我们还将回顾您可以在浏览器中使用的工具，并介绍一个利用 Web Workers 的高级用例。最后，我们将快速回顾 WebAssembly 路线图中即将推出的特性和建议。\n\n本章的目标是理解以下内容:\n\n*   WABT 和比纳莱恩如何融入构建过程，以及他们可以用来做什么\n*   如何使用 LLVM(而不是 Emscripten)编译 WebAssembly 模块\n*   在线工具，如 WasmFiddle 和其他有用的在线工具\n*   如何利用网络工作者并行运行 WebAssembly\n*   未来将集成到 WebAssembly 中的即将推出的功能(建议的和正在进行的)\n\n# 想要和 binaryen\n\nWABT 和 Binaryen 允许开发人员处理源文件，并为 WebAssembly 开发工具。如果您对在较低层次上使用 WebAssembly 感兴趣，这些工具提供了实现这一目标的方法。在本节中，我们将更详细地评估这些工具，并回顾每种工具的用途和功能。\n\n# WABT——WebAssembly 二进制工具包\n\nWABT 的工作重点是操纵 WebAssembly 二进制(`.wasm`)文件和文本(`.wat`)文件，以及两种格式之间的转换。WABT 提供了将 **Wat 翻译成 Wasm**(**Watwasm**)的工具，反之亦然(**Wam2Wat**)，以及将 Wasm 文件转换成 C 源文件和头文件的工具(**Wam2c**)。您可以在位于[https://github.com/WebAssembly/wabt](https://github.com/WebAssembly/wabt)的 WABT GitHub 资源库的自述文件中查看工具的完整列表。\n\nWABT 的一个用例例子是我们在[第 3 章](03.html)、 *中安装的*VS 代码*扩展的 WebAssembly 工具包*。扩展名依赖于 WABT 查看与`.wasm`文件关联的文本格式。该存储库提供了 wat2wasm 和 wasm2wat 演示的链接，您可以使用它们来测试 wat 程序的有效性，或者使用 JavaScript 与编译后的二进制文件进行交互。下面的截图包含 wat2wasm 演示中的 Wat 和 JavaScript 实例化代码:\n\n![](img/0c941193-4415-400d-9085-4d78bd80a19a.png)\n\nWat and JavaScript loading code from wat2wasm's \"simple\" example\n\n在 JS 面板的第`3`行，您可能已经注意到来自`wasmInstance.exports`的`addTwo()`功能没有前缀`_`。Emscripten 在编译过程中自动添加`_`。您可以省略`_`，方法是将`.wasm`文件转换为`.wat`，更新函数名，并使用 WABT 将其转换回`.wasm`，尽管这不太实用。WABT 简化了将文本格式转换为二进制格式的过程，反之亦然。如果您想为 WebAssembly 构建编译工具，您可以使用 Binaryen，我们接下来将介绍它。\n\n# 二进制\n\nBinaryen 在[https://github.com/WebAssembly/binaryen](https://github.com/WebAssembly/binaryen)的 GitHub 页面将 Binaryen 描述为一个用 C++ 编写的用于 WebAssembly 的编译器和工具链基础设施库。它旨在使编译网络程序集变得简单、快速和有效。它通过提供一个简单的 C API、一个内部 IR 和一个优化器来实现这些目标。就像 WABT 一样，Binaryen 为开发 WebAssembly 工具提供了一套广泛的工具。以下列表描述了 Binaryen 提供的工具子集:\n\n*   **wasm-shell** :能够加载和解释网络组件的工具\n*   **ASM 2 ASM**:将 asm.js 代码编译成 wasm 模块\n*   **wasm2js** :将一个 wasm 模块编译成 JavaScript\n*   **wasm-合并**:将多个 Wasm 文件合并为一个\n*   **wasm.js** : JavaScript 库，包括 Binaryen 解释器、asm2wasm、Wat 解析器和其他 Binaryen 工具\n*   **binaryen.js** : JavaScript 库，为 binaryen 工具链提供 JavaScript 接口\n\n对构建 WebAssembly 工具感兴趣的 JavaScript 开发人员对 wasm.js 和 binaryen.js 工具特别感兴趣。`binaryen.js`图书馆提供`npm`套餐([https://www.npmjs.com/package/binaryen](https://www.npmjs.com/package/binaryen))。\n\n`binaryen.js`用法的一个很好的例子是 assembly script([https://github.com/AssemblyScript/assemblyscript](https://github.com/AssemblyScript/assemblyscript))。AssemblyScript 是生成 WebAssembly 模块的 TypeScript 的严格类型子集。该库附带了一个命令行界面来快速构建新项目和管理构建步骤。在*用 LLVM 编译*一节中，我们将介绍如何使用 LLVM 编译 Wasm 模块。\n\n# 用 LLVM 编译\n\n在[第一章](01.html)中*什么是 WebAssembly？*我们讨论了 Emscripten 的 EMSDK 和 LLVM 之间的关系。Emscripten 使用 LLVM 和 Clang 将 C/C++ 编译成 LLVM 位代码。Emscripten 编译器(`emcc`)将该位代码编译为 asm.js，再传递给 Binaryen 生成一个 Wasm 文件。如果你对使用 LLVM 感兴趣，你可以把 C/C++ 编译成 Wasm，而不需要安装 EMSDK。在本节中，我们将回顾使用 LLVM 启用 Wasm 编译的过程。在将一些示例 C++ 代码编译成 Wasm 文件后，我们将在浏览器中试用它。\n\n# 安装过程\n\n如果要使用 LLVM 编译 WebAssembly 模块，需要安装和配置几个工具。让这些工具正确地协同工作可能是一个艰巨而耗时的过程。幸运的是，有人经历了让这个过程变得简单得多的麻烦。Daniel Wirtz 创建了一个名为`webassembly`([https://www.npmjs.com/package/webassembly](https://www.npmjs.com/package/webassembly))的`npm`包，可以执行以下操作(使用相应的 CLI 命令):\n\n*   将 C/C++ 代码编译成 WebAssembly 模块(`wa compile`)\n*   将多个 WebAssembly 模块链接到一个(`wa link`)\n*   将 WebAssembly 模块反编译为文本格式(`wa disassemble`)\n*   将 WebAssembly 文本格式组装成一个模块(`wa assemble`)\n\n该库正在幕后使用 Binaryen、Clang、LLVM 和其他 LLVM 工具。我们将在全球范围内安装该软件包，以确保我们能够访问`wa`命令。要安装，请打开终端实例并运行以下命令:\n\n```cpp\nnpm install -g webassembly\n```\n\n安装任何必需的依赖项可能需要几分钟时间。完成后，运行以下命令来验证安装:\n\n```cpp\nwa\n```\n\n您应该会在终端中看到以下内容:\n\n![](img/02098a68-e5fa-4ef1-8370-ba78bed728b4.png)\n\nOutput of the wa command\n\n您应该准备好开始编译 Wasm 模块了。让我们继续看示例代码。\n\n# 示例代码\n\n为了测试编译器，我们将使用第 5 章、 *创建并加载一个 WebAssembly 模块*的*与 JavaScript 交互无粘合代码*部分的`without-glue.c`文件的稍微修改版本。本节的代码位于`learn-webassembly`存储库的`/chapter-10-advanced-tools/compile-with-llvm`目录中。按照以下说明创建编译器测试所需的文件。让我们从 C++ 文件开始。\n\n# C++ 文件\n\n在名为`/compile-with-llvm`的`/book-examples`目录中创建新目录。在名为`main.cpp`的`/compile-with-llvm`目录中创建新文件，并用以下内容填充:\n\n```cpp\n#include <stdbool.h>\n\n#define BOUNDS 255\n#define RECT_SIDE 50\n#define BOUNCE_POINT (BOUNDS - RECT_SIDE)\n\nbool isRunning = true;\n\ntypedef struct Rect {\n  int x;\n  int y;\n  char direction;\n} Rect;\n\nstruct Rect rect;\n\nvoid updateRectLocation() {\n    if (rect.x == BOUNCE_POINT) rect.direction = 'L';\n    if (rect.x == 0) rect.direction = 'R';\n    int incrementer = 1;\n    if (rect.direction == 'L') incrementer = -1;\n    rect.x = rect.x + incrementer;\n    rect.y = rect.y + incrementer;\n}\n\nextern \"C\" {\nextern int jsClearRect();\nextern int jsFillRect(int x, int y, int width, int height);\n\n__attribute__((visibility(\"default\")))\nvoid moveRect() {\n    jsClearRect();\n    updateRectLocation();\n    jsFillRect(rect.x, rect.y, RECT_SIDE, RECT_SIDE);\n}\n\n__attribute__((visibility(\"default\")))\nbool getIsRunning() {\n    return isRunning;\n}\n\n__attribute__((visibility(\"default\")))\nvoid setIsRunning(bool newIsRunning) {\n    isRunning = newIsRunning;\n}\n\n__attribute__((visibility(\"default\")))\nvoid init() {\n    rect.x = 0;\n    rect.y = 0;\n    rect.direction = 'R';\n    setIsRunning(true);\n}\n}\n```\n\n该文件中的代码与[第五章](05.html)、 *中`without-glue.c`创建和加载 WebAssembly 模块*的内容几乎相同。注释已从文件中删除，导入/导出的功能包装在`extern \"C\"`块中。`__attribute__((visibility(\"default\")))`行是宏语句(类似于`EMSCRIPTEN_KEEPALIVE`)，用于确保在死代码消除步骤中不会从编译输出中删除函数。就像前面的例子一样，我们将通过一个 HTML 文件与编译后的 Wasm 模块进行交互。让我们创建下一个。\n\n# 超文本标记语言文件\n\n在`/compile-with-llvm`目录中创建一个名为`index.html`的文件，并用以下内容填充:\n\n```cpp\n<!doctype html>\n<html lang=\"en-us\">\n<head>\n  <title>LLVM Test</title>\n</head>\n<body>\n  <h1>LLVM Test</h1>\n  <canvas id=\"myCanvas\" width=\"255\" height=\"255\"></canvas>\n  <div style=\"margin-top: 16px;\">\n    <button id=\"actionButton\" style=\"width: 100px; height: 24px;\">\n      Pause\n    </button>\n  </div>\n  <script type=\"application/javascript\">\n    const canvas = document.querySelector('#myCanvas');\n    const ctx = canvas.getContext('2d');\n\n    const importObj = {\n      env: {\n        memoryBase: 0,\n        tableBase: 0,\n        memory: new WebAssembly.Memory({ initial: 256 }),\n        table: new WebAssembly.Table({ initial: 8, element: 'anyfunc' }),\n        abort: console.log,\n        jsFillRect: function(x, y, w, h) {\n          ctx.fillStyle = '#0000ff';\n          ctx.fillRect(x, y, w, h);\n        },\n        jsClearRect: function() {\n          ctx.fillStyle = '#ff0000';\n          ctx.fillRect(0, 0, 255, 255);\n        }\n      }\n    };\n\n    WebAssembly.instantiateStreaming(fetch('main.wasm'), importObj)\n      .then(({ instance }) => {\n        const m = instance.exports;\n        m.init();\n\n        const loopRectMotion = () => {\n          setTimeout(() => {\n            m.moveRect();\n            if (m.getIsRunning()) loopRectMotion();\n          }, 20)\n        };\n\n    document.querySelector('#actionButton')\n      .addEventListener('click', event => {\n        const newIsRunning = !m.getIsRunning();\n        m.setIsRunning(newIsRunning);\n        event.target.innerHTML = newIsRunning ? 'Pause' : 'Start';\n        if (newIsRunning) loopRectMotion();\n      });\n\n      loopRectMotion();\n    });\n  </script>\n</body>\n</html>\n```\n\n该文件的内容与[第 5 章](05.html)、*创建和加载 WebAssembly 模块*中的`without-glue.html`文件非常相似。我们使用的是`WebAssembly.instantiateStreaming()`功能，而不是使用`/common/load-wasm.js`文件中的`loadWasm()`功能。这允许我们省略一个额外的`<script>`元素，直接从`/compile-with-llvm`目录中提供文件。\n\n`jsFillRect`中省略了`_`，将`jsClearRect`功能传递给了`importObj`。我们也可以省略`instance.exports`对象上的功能的`_`。LLVM 不会在传入或传出模块的任何数据/函数前面加上`_`。在下一节中，我们将编译`main.cpp`并在浏览器中与生成的 Wasm 文件进行交互。\n\n# 编译和运行示例\n\n我们安装了带有`-g`标志的`webassembly npm`包，所以终端中应该有`wa`命令。在`/compile-with-llvm`目录中打开一个终端实例，运行以下命令:\n\n```cpp\nwa compile main.cpp -o main.wasm\n```\n\n你应该会看到一个名为`main.wasm`的文件出现在 VS Code 的文件浏览器的`compile-with-llvm`文件夹中。为确保正确编译 Wasm 模块，在`/compile-with-llvm`目录下运行以下命令:\n\n```cpp\nserve -l 8080\n```\n\n如果您在浏览器中导航到`http://127.0.0.1:8080/index.html`，您应该会看到以下内容:\n\n![](img/bb069bef-1df9-437f-b2ce-8622b845ef2c.png)\n\nLLVM compiled module running in the browser\n\n# 在线工具\n\n诚然，在本地编译 WebAssembly 模块的安装和配置过程有点麻烦。幸运的是，有几个在线工具允许您在浏览器中开发网络组件并与之交互。在本节中，我们将回顾这些工具，并讨论它们各自提供的功能。\n\n# WasmFiddle\n\n在[第二章](02.html)、*web assembly*、*元素中*用 wasmiddle 连接点- Wat、Wasm 和 JavaScript API* 部分，我们使用 wasmiddle 编译了一个简单的 C 函数到 Wasm，并使用 JavaScript 与之交互。WasmFiddle 提供了 C/C++ 编辑器、JavaScript 编辑器、Wat/x86 查看器和 JavaScript 输出面板。如果需要，您也可以与`<canvas>`交互。WasmFiddle 使用 LLVM 生成 Wasm 模块，这就是为什么导入和导出没有前缀`_`。您可以在[https://wasdk.github.io/WasmFiddle](https://wasdk.github.io/WasmFiddle)与 WasmFiddle 互动。*\n\n# WebAssembly 资源管理器\n\n位于[https://mbebenita.github.io/WasmExplorer](https://mbebenita.github.io/WasmExplorer)的 WebAssembly 浏览器提供了与 WasmFiddle 相似的功能。它允许你编译 C 或 C++ 到一个 Wasm 模块，并查看相应的 Wat。但是，网络程序集资源管理器提供了 WasmFiddle 中没有的附加功能。比如可以将 C 或 C++ 编译成 Wasm，查看对应的 Firefox x86 和 LLVM x86 代码。您可以从代码示例列表中选择并指定优化级别(`emcc`中的`-O`标志)。它还提供了一个按钮，允许您将代码导入 WasmFiddle:\n\n![](img/cbe79b3c-c5b9-41b1-b817-e71e0e85c3fa.png)\n\nScreenshot of WebAssembly Explorer\n\n# WebAssembly 工作室\n\nWebAssembly Studio，位于[https://web assembly . Studio](https://webassembly.studio)，是一个功能丰富的编辑器和开发环境。您可以创建 C、Rust 和 AssemblyScript 项目。它提供了在浏览器中构建和运行代码的能力，并与 GitHub 很好地集成在一起。WebAssembly Studio 使您能够构建 web 应用，而不必在本地安装和配置所需的 WebAssembly 工具:\n\n![](img/586895b5-ace5-4179-9423-bae1c39361ce.png)\n\nScreenshot of WebAssembly Studio\n\n在下一节中，我们将演示如何使用 Web Workers 向您的 WebAssembly 应用添加并行性。\n\n# 与网络工作者并行的 Wasm\n\n构建执行繁重计算或其他资源密集型工作的复杂应用的过程可以从使用**线程**中受益匪浅。通过在独立运行的任务中划分功能，线程允许您并行执行操作。在撰写本文时，对 WebAssembly 中线程的支持处于*特性提案*阶段。在这个阶段，规范还没有写出来，特性也没有实现。幸运的是，JavaScript 以 Web Workers 的形式提供了线程功能。在本节中，我们将演示如何使用 JavaScript 的 Web Workers API 在单独的线程中与 Wasm 模块进行交互。\n\n# 网络工作者和 WebAssembly\n\n网络工作人员允许您利用浏览器中的线程，这可以通过从主(用户界面)线程中卸载一些逻辑来提高应用的性能。工作线程也能够使用`XMLHttpRequest`执行输入/输出。工作线程通过向事件处理程序发送消息来与主线程通信。\n\nWeb Workers 允许我们将 Wasm 模块加载到单独的线程中，并执行不妨碍 UI 性能的操作。网络工作者确实有一些局限性。他们无法直接操作 DOM 或访问`window`对象上的一些方法和属性。线程之间传递的消息必须是序列化对象，这意味着不能传递函数。既然你知道了什么是工人，让我们来讨论如何创建一个。\n\n# 创造一个工人\n\n在创建工作线程之前，您需要一个 JavaScript 文件，其中包含在工作线程中运行的代码。您可以在[https://github . com/MDN/simple-web-worker/blob/GH-pages/worker . js](https://github.com/mdn/simple-web-worker/blob/gh-pages/worker.js)上看到一个简单的 worker 定义文件示例。该文件应该包含一个`message`事件侦听器，当从其他线程接收到消息时，该侦听器执行操作并做出相应的响应。\n\n一旦创建了该文件，您就可以与工作人员一起使用它了。通过向`Worker()`构造函数传递一个 URL 参数来创建一个工作者。该网址可以是一个字符串，代表带有您的工作人员定义代码的文件的名称，或者使用`Blob`构造。如果你从服务器获取工人定义代码，T2 技术会很有用。示例应用演示了如何使用这两种方法。让我们继续讨论将 WebAssembly 与网络工作者集成的过程。\n\n# WebAssembly 工作流\n\n为了在不同的线程中使用 Wasm 模块，Wasm 文件必须在主线程中编译，并在网络工作器中实例化。让我们更详细地回顾一下这个过程:\n\n1.  使用`Worker()`构造函数创建了一个新的网络工作者(我们称之为`wasmWorker`)。\n2.  进行提取调用以检索`.wasm`文件，并在响应中调用`arrayBuffer()`函数。\n3.  `arrayBuffer()`函数的解析值被传递给`WebAssembly.compile()`函数。\n4.  `WebAssembly.compile()`函数解析一个`WebAssembly.Module`实例，该实例包含在使用`postMessage()`函数发送给`wasmWorker`的消息正文中。\n5.  在`wasmWorker`内，来自消息体的`WebAssembly.Module`实例被传递给`WebAssembly.instantiate()`函数，该函数通过`WebAssembly.Instance`解析。\n6.  `WebAssembly.Instance`导出对象被分配给`wasmWorker`中的一个局部变量，用于调用 Wasm 函数。\n\n要从`wasmWorker` Wasm 实例调用一个函数，需要向工作线程发送一条消息，其中包含要传递给 Wasm 函数的任何参数。然后，`wasmWorker`执行该函数，并将结果传递回主线程。这就是在 Web Workers 环境中如何利用线程的关键。在我们进入示例应用之前，您可能需要解决谷歌 Chrome 强加的一个限制。遵循谷歌浏览器中*限制部分的说明，确保示例应用成功运行。*\n\n# 谷歌 Chrome 的局限性\n\n谷歌 Chrome 对网络工作者的`postMessage()`功能的主体内容进行了限制。如果你试图发送一个编译好的`WebAssembly.Module`给一个工人，你会得到一个错误，操作将会失败。您可以通过设置标志来覆盖它。要启用此功能，请打开谷歌浏览器并在地址栏中输入`chrome://flags`。在页面顶部的搜索框中输入`cloning`。您应该会看到一个名为 WebAssembly 结构化克隆支持的列表项。从列表项旁边的下拉列表中选择“已启用”选项，并在出现提示时按“立即重新启动”按钮:\n\n![](img/077e795e-460d-413b-b6f4-cf82e5e797b0.png)\n\nUpdating the WebAssembly flag in Google Chrome\n\nChrome 重启后，您可以毫无问题地运行示例应用。如果您正在使用 Mozilla Firefox，则不需要任何操作。默认情况下，它支持此功能。让我们转到演示在线程中使用 WebAssembly 的示例应用。\n\n# 代码概述\n\n示例应用算不上什么应用。它是一个简单的表单，接受两个输入值，并返回这两个值的和或差。每个加减操作都是从工作线程中实例化的自己的 Wasm 模块导出的。这个例子可能是人为的，但是它有效地展示了如何将 WebAssembly 集成到网络工作者中。\n\n该部分的代码位于`learn-webassembly`存储库的`/chapter-10-advanced-tools/parallel-wasm`目录中。以下各节将介绍代码库的每一部分，并描述如何从头开始构建应用。如果你想继续，在你的`/book-examples`目录中创建一个名为`/parallel-wasm`的文件夹。\n\n# C 代码\n\n该示例使用两个工作线程:一个用于加法，另一个用于减法。因此，我们需要两个独立的 Wasm 模块。在你的`/parallel-wasm`目录中创建一个名为`/lib`的文件夹。在`/lib`目录中，创建一个名为`add.c`的文件，并用以下内容填充它:\n\n```cpp\nint calculate(int firstVal, int secondVal) {\n    return firstVal + secondVal;\n}\n```\n\n在`/lib`中创建另一个名为`subtract.c`的文件，并用以下内容填充:\n\n```cpp\nint calculate(int firstVal, int secondVal) {\n    return firstVal - secondVal;\n}\n```\n\n注意两个文件中的函数名都是`calculate`。这样做是为了我们不必在工作代码中编写任何条件逻辑来确定要调用的 Wasm 函数。代数运算是和一个工人联系在一起的，所以当我们需要把两个数字相加时，`_calculate()`函数将在`addWorker`中被调用。当我们查看代码的 JavaScript 部分时，这将变得更加清晰，我们将在接下来介绍它。\n\n# JavaScript 代码\n\n在我们深入研究 JavaScript 代码之前，在您的`/parallel-wasm`目录中创建一个名为`/src`的文件夹。让我们从包含在工作线程中运行的代码的文件开始。\n\n# 在 worker.js 中定义线程执行\n\n在名为`worker.js`的`/src`目录中创建新文件，并用以下内容填充:\n\n```cpp\nvar wasmInstance = null;\n\nself.addEventListener('message', event => {\n  /**\n   * Once the WebAssembly compilation is complete, this posts a message\n   * back with whether or not the instantiation was successful. If the\n   * payload is null, the compilation succeeded.\n   */\n  const sendCompilationMessage = (error = null) => {\n    self.postMessage({\n      type: 'COMPILE_WASM_RESPONSE',\n      payload: error\n    });\n  };\n\n  const { type, payload } = event.data;\n  switch (type) {\n    // Instantiates the compiled Wasm module and posts a message back to\n    // the main thread indicating if the instantiation was successful:\n    case 'COMPILE_WASM_REQUEST':\n      const importObj = {\n        env: {\n          memoryBase: 0,\n          tableBase: 0,\n          memory: new WebAssembly.Memory({ initial: 256 }),\n          table: new WebAssembly.Table({ initial: 2, element: 'anyfunc' }),\n          abort: console.log\n        }\n      };\n\n      WebAssembly.instantiate(payload, importObj)\n        .then(instance => {\n          wasmInstance = instance.exports;\n          sendCompilationMessage();\n        })\n        .catch(error => {\n          sendCompilationMessage(error);\n        });\n      break;\n\n    // Calls the `calculate` method associated with the instance (add or\n    // subtract, and posts the result back to the main thread:\n    case 'CALC_REQUEST':\n      const { firstVal, secondVal } = payload;\n      const result = wasmInstance._calculate(firstVal, secondVal);\n\n      self.postMessage({\n        type: 'CALC_RESPONSE',\n        payload: result\n      });\n      break;\n\n    default:\n      break;\n  }\n}, false);\n```\n\n代码封装在`message`事件(`self.addEventListener(...)`)的事件侦听器中，当在相应的工作程序上调用`postMessage()`函数时，会引发该事件。事件监听器回调函数中的`event`参数包含一个带有消息内容的`data`属性。应用中线程之间传递的所有消息都遵循**流量标准动作** ( **FSA** )约定。符合此约定的对象具有`type`和`payload`属性，其中`type`是字符串，`payload`可以是任何类型。你可以在[https://github.com/redux-utilities/flux-standard-action](https://github.com/redux-utilities/flux-standard-action)阅读更多关于金融服务管理局的信息。\n\nYou can use any format or structure for the data you pass using the `postMessage()` function, as long as the data is serializable.\n\n`switch`语句基于消息的`type`值执行一个操作，该值是一个字符串。如果`type`是`'COMPILE_WASM_REQUEST'`，则通过消息中的`payload`和`importObj`调用`WebAssembly.instantiate()`功能。结果的`exports`对象被分配给局部`wasmInstance`变量以备后用。如果`type`为`'CALC_REQUEST'`，则从`payload`对象调用`wasmInstance._calculate()`功能，调用`firstVal`和`secondVal`值。计算代码应该能解释为什么函数被命名为`_calculate()`而不是`_add()`或`_subtract()`。通过使用通用名称，工作人员不关心它正在执行什么操作，它只是调用函数来获得结果。\n\n在这两种情况下，工作人员使用`postMessage()`函数向主线程回发一条消息。我使用了`REQUEST` / `RESPONSE`惯例来计算`type`的财产价值。这允许您快速识别消息来自哪个线程。从主线程发送的消息以`type`中的`_REQUEST`结尾，而来自工作线程的响应以`_RESPONSE`结尾。让我们继续讨论 WebAssembly 交互代码。\n\n# 在 WasmWorker.js 中与 Wasm 交互\n\n在名为`WasmWorker.js`的`/src`目录中创建新文件，并用以下内容填充:\n\n```cpp\n/**\n * Web Worker associated with an instantiated Wasm module.\n * @class\n */\nexport default class WasmWorker {\n  constructor(workerUrl) {\n    this.worker = new Worker(workerUrl);\n    this.listenersByType = {};\n    this.addListeners();\n  }\n\n  // Add a listener associated with the `type` value from the\n  // Worker message:\n  addListenerForType(type, listener) {\n    this.listenersByType[type] = listener;\n  }\n\n  // Add event listeners for error and message handling.\n  addListeners() {\n    this.worker.addEventListener('error', event => {\n      console.error(`%cError: ${event.message}`, 'color: red;');\n    }, false);\n\n    // If a handler was specified using the `addListener` method,\n    // fire that method if the `type` matches:\n    this.worker.addEventListener('message', event => {\n      if (\n        event.data instanceof Object &&\n        event.data.hasOwnProperty('type') &&\n        event.data.hasOwnProperty('payload')\n      ) {\n        const { type, payload } = event.data;\n        if (this.listenersByType[type]) {\n          this.listenersByType[type](payload);\n        }\n      } else {\n        console.log(event.data);\n      }\n    }, false);\n  }\n\n  // Fetches the Wasm file, compiles it, and passes the compiled result\n  // to the corresponding worker. The compiled module is instantiated\n  // in the worker.\n  initialize(name) {\n    return fetch(`calc-${name}.wasm`)\n      .then(response => response.arrayBuffer())\n      .then(bytes => WebAssembly.compile(bytes))\n      .then(wasmModule => {\n        this.worker.postMessage({\n          type: 'COMPILE_WASM_REQUEST',\n          payload: wasmModule\n      });\n      return Promise.resolve();\n    });\n  }\n\n  // Posts a message to the worker thread to call the `calculate`\n  // method from the Wasm instance:\n  calculate(firstVal, secondVal) {\n    this.worker.postMessage({\n      type: 'CALC_REQUEST',\n        payload: {\n        firstVal,\n        secondVal\n      }\n    });\n  }\n}\n```\n\n`WasmWorker`类管理一个与 Wasm 文件相关联的工作线程。在`WasmWorker`构造函数中，创建一个新的`Worker`，并为`error`和`message`事件添加默认事件侦听器。`initialize()`函数获取与`name`参数相关联的`.wasm`文件，对其进行编译，并将结果`WebAssembly.Module`实例发送给要实例化的工作线程。\n\n当消息响应中的`type`字段与传递给函数的`type`参数匹配时，`addListenerForType()`函数用于指定要执行的`callback`函数(`listener`)。这是从工作线程获取`_calculate()`函数结果所必需的。\n\n最后，`WasmWorker`中的`calculate()`函数向工作线程发布一条消息，其中包含从`<form>`上的`<input>`元素传入的`firstVal`和`secondVal`参数。让我们继续应用加载代码，看看`WasmWorker`如何与用户界面交互。\n\n# 在 index.js 中加载应用\n\n在名为`index.js`的`/src`目录中创建新文件，并用以下内容填充:\n\n```cpp\nimport WasmWorker from './WasmWorker.js';\n\n/**\n * If you add ?blob=true to the end of the URL (e.g.\n * http://localhost:8080/index.html?blob=true), the worker will be\n * created from a Blob rather than a URL. This returns the\n * URL to use for the Worker either as a string or created from a Blob.\n */\nconst getWorkerUrl = async () => {\n  const url = new URL(window.location);\n  const isBlob = url.searchParams.get('blob');\n  var workerUrl = 'worker.js';\n  document.title = 'Wasm Worker (String URL)';\n\n  // Create a Blob instance from the text contents of `worker.js`:\n  if (isBlob === 'true') {\n    const response = await fetch('worker.js');\n    const results = await response.text();\n    const workerBlob = new Blob([results]);\n    workerUrl = window.URL.createObjectURL(workerBlob);\n    document.title = 'Wasm Worker (Blob URL)';\n  }\n\n  return Promise.resolve(workerUrl);\n};\n\n/**\n * Instantiates the Wasm module associated with the specified worker\n * and adds event listeners to the \"Add\" and \"Subtract\" buttons.\n */\nconst initializeWorker = async (wasmWorker, name) => {\n  await wasmWorker.initialize(name);\n  wasmWorker.addListenerForType('CALC_RESPONSE', payload => {\n    document.querySelector('#result').value = payload;\n  });\n\n  document.querySelector(`#${name}`).addEventListener('click', () => {\n    const inputs = document.querySelectorAll('input');\n    var [firstInput, secondInput] = inputs.values();\n    wasmWorker.calculate(+firstInput.value, +secondInput.value);\n  });\n};\n\n/**\n * Spawns (2) workers: one associated with calc-add.wasm and another\n * with calc-subtract.wasm. Adds an event listener to the \"Reset\"\n * button to clear all the input values.\n */\nconst loadPage = async () => {\n  document.querySelector('#reset').addEventListener('click', () => {\n    const inputs = document.querySelectorAll('input');\n    inputs.forEach(input => (input.value = 0));\n  });\n\n  const workerUrl = await getWorkerUrl();\n  const addWorker = new WasmWorker(workerUrl);\n  await initializeWorker(addWorker, 'add');\n\n  const subtractWorker = new WasmWorker(workerUrl);\n  await initializeWorker(subtractWorker, 'subtract');\n};\n\nloadPage()\n  .then(() => console.log('%cPage loaded!', 'color: green;'))\n  .catch(error => console.error(error));\n```\n\n应用入口点是`loadPage()`功能。在深入工人初始化代码之前，我们先讨论一下`getWorkerUrl()`函数。在本节的前面，我们了解到您可以将一个代表从`Blob`创建的文件名或网址的字符串传递给`Worker()`构造函数。下面的示例代码演示了第一种技术:\n\n```cpp\nvar worker = new Worker('worker.js');\n```\n\n第二种技术在`getWorkerUrl()`功能的`if (isBlob === 'true')`块中演示。如果当前的`window.location`值以`?blob=true`结束，则传递给`Worker()`构造函数的网址是从`Blob`创建的。唯一值得注意的区别是`document.title`值，它会更新以反映网址类型。让我们跳回到`loadPage()`函数来讨论初始化代码。\n\n将事件监听器添加到`loadPage()`功能中的重置按钮后，会创建两个`WasmWorker`实例:`addWorker`和`subtractWorker`。每个工人都被传递给`initializeWorker()`函数作为`wasmWorker`参数。在`initializeWorker()`中，调用`wasmWorker.initialize()`函数来实例化 Wasm 模块。调用`wasmWorker.addListenerForType()`函数，将结果`<input>`的值设置为相应工作器中`_calculate()`函数返回的值。最后，一个事件监听器被添加到`<button>`的`click`事件中，该事件监听器对`firstVal`和`secondVal` `<input>`值进行加减运算(基于`name`参数)。这就是 JavaScript 代码。让我们创建一个 HTML 和 CSS 文件，然后进入构建步骤。\n\n# 网络资产\n\n我们需要一个 HTML 文件作为应用的入口点。在名为`index.html`的`/src`目录中创建一个文件，并用以下内容填充:\n\n```cpp\n<!DOCTYPE html>\n<html>\n<head>\n  <meta charset=\"utf-8\">\n  <title>Wasm Workers</title>\n  <link rel=\"stylesheet\" type=\"text/css\" href=\"styles.css\" />\n</head>\n<body>\n  <form class=\"valueForm\">\n    <div class=\"valueForm\">\n      <label for=\"firstVal\">First Value:</label>\n      <input id=\"firstVal\" type=\"number\" value=\"0\" />\n    </div>\n    <div class=\"valueForm\">\n      <label for=\"secondVal\">Second Value:</label>\n      <input id=\"secondVal\" type=\"number\" value=\"0\" />\n    </div>\n    <div class=\"valueForm\">\n      <label for=\"result\">Result:</label>\n      <input id=\"result\" type=\"number\" value=\"0\" readonly />\n    </div>\n  </form>\n  <div>\n    <button id=\"add\">Add</button>\n    <button id=\"subtract\">Subtract</button>\n    <button id=\"reset\">Reset</button>\n  </div>\n  <script type=\"module\" src=\"index.js\"></script>\n</body>\n</html>\n```\n\n该应用由一个包含三个`<input>`元素的`<form>`和一个包含三个`<button>`元素的块组成。前两个`<input>`元素对应于发送到任一工作线程的`payload`中包含的`firstVal`和`secondVal`属性。最后的`<input>`是只读的，显示任一操作的结果。\n\n位于`<form>`下方的`<button>`元素块对`<input>`值进行运算。前两个`<button>`元素将`<input>`值发送到`addWorker`或`subtractWorker`螺纹(取决于按下了哪个按钮)。最终`<button>`将所有`<input>`值设置为`0`。\n\n应用在`</body>`结束标记前最后一行的`<script>`标记中初始化。就像烹饪书籍一样，`type=\"module\"`属性允许我们使用较新浏览器中可用的`import` / `export`语法。最后，我们需要向应用添加一些样式。在名为`styles.css`的`/src`目录中创建一个文件，并用以下内容填充:\n\n```cpp\n* {\n  font-family: sans-serif;\n  font-size: 14px;\n}\n\nbody {\n  margin: 16px;\n}\n\nform.valueForm {\n  display: table;\n}\n\ndiv.valueForm {\n  display: table-row;\n}\n\nlabel, input {\n  display: table-cell;\n  margin-bottom: 16px;\n}\n\nlabel {\n  font-weight: bold;\n  padding-right: 16px;\n}\n\nbutton {\n  border: 1px solid black;\n  border-radius: 4px;\n  cursor: pointer;\n  font-weight: bold;\n  height: 24px;\n  margin-right: 4px;\n  width: 80px;\n}\n\nbutton:hover {\n  background: lightgray;\n}\n```\n\n这是我们需要创建的最后一个文件，但不是运行应用所需的最后一个文件。我们仍然需要从`/lib`目录中的 C 文件生成 Wasm 文件。让我们继续进行构建步骤。\n\n# 构建和运行应用\n\n写完代码后，是时候构建和测试应用了。完成构建步骤后，我们将与正在运行的应用进行交互，并回顾如何使用浏览器的开发工具对 Web Workers 进行故障排除。\n\n# 编译 C 文件\n\n我们需要将每个 C 文件编译成单独的`.wasm`文件，这意味着执行编译步骤所需的命令是冗长的。要执行构建，请在`/parallel-wasm`目录中打开一个终端实例，并运行以下命令:\n\n```cpp\n# First, compile the add.c file: emcc -Os -s WASM=1 -s SIDE_MODULE=1 -s BINARYEN_ASYNC_COMPILATION=0 lib/add.c -o src/calc-add.wasm # Next, compile the subtract.c fileemcc -Os -s WASM=1 -s SIDE_MODULE=1 -s BINARYEN_ASYNC_COMPILATION=0 lib/subtract.c -o src/calc-subtract.wasm\n```\n\n您应该会在`/src`目录中看到两个新文件:`calc-add.wasm`和`calc-subtract.wasm`。有了所需的文件，是时候测试应用了。\n\n# 与应用交互\n\n在`/parallel-wasm`目录中打开一个终端实例，运行以下命令:\n\n```cpp\nserve -l 8080 src\n```\n\n如果您在浏览器中导航到`http://127.0.0.1:8080/index.html`，您应该会看到:\n\n![](img/de8e6b2e-53e0-49d2-9628-af2948abcb4e.png)\n\n￼Wasm Workers application running in the browser\n\n尝试更改第一个值和第二个值输入中的值，并按下加减按钮。结果输入应根据计算结果进行更新。如果导航到`http://127.0.0.1:8080/index.html?blob=true`，传递给`Worker()`构造函数的 URL 参数将使用`Blob`代替文件名。该选项卡应进行更改，以反映使用`Blob`技术构建网址:\n\n![](img/06832741-0c1c-40a3-9b02-e6694db0942c.png)\n\nTab title updated to reflect the Blob URL technique\n\n# 调试网络工作人员\n\n您可以设置断点，并使用浏览器的开发工具与工作线程进行交互。在谷歌浏览器中，打开开发者工具并选择来源标签。文件列表面板应该包含两个`worker.js`实例。调试器面板包含一个带有`main`线程和两个`worker.js`线程的线程部分。下面的截图显示了 Chrome 开发人员工具面板中正在运行的应用的线程调试元素:\n\n![](img/243575cd-3c5c-43ad-a78c-cc3c248a0158.png)\n\nThread debugging tools in the Chrome Developer Tools panel\n\n在火狐中，工作人员调试是在单独的开发人员工具窗口中完成的。要看到这一点，请在火狐中打开开发人员工具，并选择调试器面板。点击工人面板中的一个`worker.js`列表项。将出现一个与所选工作人员对应的新开发人员工具窗口。以下屏幕截图显示了从“工人”面板中选择的一个`worker.js`实例的单独开发人员工具窗口:\n\n![](img/886e9bcd-c446-4a8d-aab4-2a303255aa2e.png)\n\nThread debugging tools in the Firefox Developer Tools panel\n\n在下一节中，我们将讨论 WebAssembly 即将推出的一些功能。\n\n# 即将推出的功能\n\n在标准化过程的不同阶段，有几个即将推出的 WebAssembly 特性。其中一些比另一些更有影响力，但所有这些都是有价值的改进。在本节中，我们将描述标准化过程，并回顾代表 WebAssembly 功能重大转变的功能子集。本节的大部分内容参考了科林·埃伯哈特的博客文章，标题为*WebAssembly 的未来——查看即将推出的功能和建议*。该帖子可以在[https://blog.scottlogic.com/2018/07/20/wasm-future.html](https://blog.scottlogic.com/2018/07/20/wasm-future.html)找到。\n\n# 标准化过程\n\n位于[https://github . com/web assembly/meetings/blob/master/Process/phases . MD](https://github.com/WebAssembly/meetings/blob/master/process/phases.md)的 WebAssembly W3C 流程文档描述了标准化流程的六个阶段(从 0 到 5)。以下列表提供了这些阶段的简要描述:\n\n*   **0 期。预提案**:某网络大会**社区群** ( **CG** )成员有想法，CG 投票决定是否移至一期。\n*   **第一阶段。功能提案**:预提案流程已经成功，并且在 GitHub 上的 WebAssembly 组织中创建了一个存储库来记录该功能。\n*   **第 2 阶段。提议的规范文本可用**:完整的提议的规范文本可用，可能的实现被原型化，并且测试套件被添加。\n*   **第三阶段。实现阶段**:嵌入者实现特性，存储库被更新以包括对形式化的修订，规范被更新以包括特性在引用解释器中的实现。\n*   **第 4 阶段。标准化功能**:如果两个或多个网络虚拟机和至少一个工具链实现了该功能，则该功能将完全移交给 WebAssembly**工作组** ( **工作组**)。\n*   **第五阶段。功能已标准化**:工作组成员已就功能是否完整达成共识。\n\n现在您已经熟悉了与标准化过程相关的阶段，让我们继续讨论线程提案。\n\n# 线\n\n在前一节中，我们使用 Web Workers 将 Wasm 模块移动到工作线程中，这允许我们在不阻塞主线程的情况下调用 Wasm 函数。但是，在工作线程之间传递消息有性能限制。为了解决这个问题，为 WebAssembly 提出了一个线程特性。\n\n该提案目前处于第一阶段，在[https://github . com/WebAssembly/threads/blob/master/proposes/threads/overview . MD](https://github.com/WebAssembly/threads/blob/master/proposals/threads/Overview.md)中有详细描述。根据建议文档，threads 特性增加了一个新的共享线性内存类型和一些新的原子内存访问操作。这个建议的范围相对有限。埃伯哈特在他的博客文章中提供了以下阐述:\n\n\"Notably, this proposal does not introduce a mechanism for creating threads (which has caused a lot of debate) instead this functionality is supplied by the host. Within the context of wasm executed by the browser this will be the familiar WebWorkers.\"\n\n尽管该特性不允许创建线程，但它提供了一种更简单的方法，在我们用 JavaScript 创建的工作线程之间共享数据。\n\n# 主机绑定\n\n主机绑定提案也在第 1 阶段，它将解决 WebAssembly 在浏览器中使用时的一个重大限制:DOM 操作。[https://github . com/WebAssembly/host-binding/blob/master/propositions/host-binding/overview . MD](https://github.com/WebAssembly/host-bindings/blob/master/proposals/host-bindings/Overview.md)上的提案文档为此功能提供了以下目标列表:\n\n*   **人机工程学**:允许 WebAssembly 模块创建、传递、调用和操作 JavaScript + DOM 对象\n*   **速度**:允许很好的优化 JS/DOM 或者其他主机调用\n*   **平台一致性**:允许使用 WebIDL 注释 Wasm 导入/导出(通过工具)\n*   **渐进主义**:提供一个多元的策略\n\n提高网络组件与 JavaScript 和网络应用编程接口的互操作性将大大简化开发过程。这也将消除像 Emscripten 这样的工具所提供的“粘合”代码的需要。\n\n# 碎片帐集\n\n**垃圾收集** ( **GC** )方案目前处于一期。我们在*中讨论了垃圾收集的局限性是什么？[第一章](01.html)*的*部分什么是 WebAssembly？*提案文档位于[https://github . com/WebAssembly/GC/blob/master/proposts/GC/overview . MD](https://github.com/WebAssembly/gc/blob/master/proposals/gc/Overview.md)提供了该特性的广泛概述，并描述了需要添加到规范中的元素。埃伯哈特在他的博客文章中对该提议做了如下描述:\n\n\"This proposal adds GC capabilities to WebAssembly. Interestingly, it will not have its own GC, instead it will integrate with the GC provided by the host environment. This makes a lot of sense as this, and various other proposals (host bindings, reference types), are designed to improve the interop with the host, making it easier to share state and call APIs. Having a single GC to manage memory makes this much easier.\"\n\n这个特性将需要大量的努力来实现，但是将它添加到 WebAssembly 将是值得的。让我们用一个目前处于实现阶段的特性来结束这一部分:引用类型。\n\n# 参考类型\n\n目前处于第 3 阶段的引用类型构成了主机绑定和 GC 特性的基础。[https://github . com/WebAssembly/reference-types/blob/master/proposes/reference-types/overview . MD](https://github.com/WebAssembly/reference-types/blob/master/proposals/reference-types/Overview.md)上的提案文档描述了新类型`anyref`的添加，该类型既可以用作值类型，也可以用作表元素类型。`anyref`类型允许您将一个 JavaScript 对象传递给一个 Wasm 模块。埃伯哈特在他的博客文章中描述了这个特性的含义:\n\n\"The wasm module can't really do much with the object via the anyref type. What's more important is that the module is holding a reference to a garbage collected object on the JS heap, meaning they need to be traced during wasm execution. This proposal is seen as a stepping-stone towards the more significant garbage collection proposal.\"\n\nWebAssembly 还有其他几个令人兴奋的特性。WebAssembly CG 和工作组正投入他们的时间和资源来实现这些功能。您可以在位于[https://github.com/WebAssembly](https://github.com/WebAssembly)的 GitHub 上的 WebAssembly 组织页面查看所有提案。\n\n# 摘要\n\n在这一章中，我们回顾了高级工具和 WebAssembly 的替代编译方法。我们了解了 WABT 和比纳莱恩在 WebAssembly 开发过程中的角色以及他们提供的功能。我们通过使用 WebAssembly `npm`包用 LLVM 编译了一个 Wasm 模块，并在浏览器中与结果交互。我们回顾了一些在线可用的 WebAssembly 工具，并创建了一个简单的应用，该应用使用网络工人在单独的线程中存储 Wasm 模块。最后，我们讨论了 WebAssembly 即将推出的特性和标准化过程。既然你对 WebAssembly 有了更深的理解，那就去做点什么吧！\n\n# 问题\n\n*   WABT 代表什么？\n*   Binaryen 提供了哪三个要素来使编译到 WebAssembly *变得容易*、*变得快速*和*变得有效*？\n*   就`importObj` / `exports`而言，使用 Emscripten 编译的模块与 LLVM 的主要区别是什么？\n*   哪种在线工具允许您使用 AssemblyScript？\n*   您可以传递给`Worker()`构造函数的两种参数是什么？\n*   在主线程和工作线程之间传递消息使用了什么约定？\n*   WebAssembly 标准化过程有几个阶段？\n*   参考类型特征中定义的新类型的名称是什么？\n\n# 进一步阅读\n\n*   内存管理速成班:[https://hacks . Mozilla . org/2017/06/a-内存管理速成班](https://hacks.mozilla.org/2017/06/a-crash-course-in-memory-management)\n*   MDN Web Workers API:[https://developer . Mozilla . org/en-US/docs/Web/API/Web _ Workers _ API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API)\n*   Web assembly-Web Workers:[https://medium . com/@ c . Gerard . galt/Web assembly-Web Workers-f2ba 637 C3 e4a](https://medium.com/@c.gerard.gallant/webassembly-web-workers-f2ba637c3e4a)"
  },
  {
    "path": "docs/learn-wasm/README.md",
    "content": "# WebAssembly 学习手册\n\n> 原书：[Learn WebAssembly](https://libgen.rs/book/index.php?md5=C6E8C68B806A76576FEA09B6C84749BB)\n> \n> 协议：[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)\n> \n> 阶段：机翻（1）\n> \n> 自豪地采用[谷歌翻译](https://translate.google.cn/)\n> \n> 让开源界提前感受入关。\n\n* [在线阅读](https://ccpp.apachecn.org)\n* [在线阅读（Gitee）](https://apachecn.gitee.io/apachecn-c-cpp-zh/)\n* [ApacheCN 学习资源](http://docs.apachecn.org/)\n\n## 贡献指南\n\n本项目需要校对，欢迎大家提交 Pull Request。\n\n> 请您勇敢地去翻译和改进翻译。虽然我们追求卓越，但我们并不要求您做到十全十美，因此请不要担心因为翻译上犯错——在大部分情况下，我们的服务器已经记录所有的翻译，因此您不必担心会因为您的失误遭到无法挽回的破坏。（改编自维基百科）\n\n## 联系方式\n\n### 负责人\n\n* [飞龙](https://github.com/wizardforcel): 562826179\n\n### 其他\n\n*   在我们的 [apachecn/apachecn-c-cpp-zh](https://github.com/apachecn/apachecn-c-cpp-zh) github 上提 issue.\n*   发邮件到 Email: `apachecn@163.com`.\n*   在我们的 [组织学习交流群](http://www.apachecn.org/organization/348.html) 中联系群主/管理员即可.\n\n## 赞助我们\n\n![](http://data.apachecn.org/img/about/donate.jpg)\n"
  },
  {
    "path": "docs/learn-wasm/SUMMARY.md",
    "content": "+   [WebAssembly 学习手册](README.md)\n+   [零、前言](00.md)\n+   [一、什么是 WebAssembly？](01.md)\n+   [二、WebAssembly 的元素——Wat、Wasm 和 JavaScript 应用编程接口](02.md)\n+   [三、建立开发环境](03.md)\n+   [四、安装所需的依赖项](04.md)\n+   [五、创建和加载 WebAssembly 模块](05.md)\n+   [六、与 JavaScript 交互和调试](06.md)\n+   [七、从头开始创建应用](07.md)\n+   [八、使用电子脚本移植游戏](08.md)\n+   [九、与 Node.js 集成](09.md)\n+   [十、高级工具和即将推出的功能](10.md)\n"
  },
  {
    "path": "docs/master-cpp-game-dev/00.md",
    "content": "# 零、前言\n\n虽然现在有很多语言被用来开发游戏，但是 C++ 仍然是专业开发的标准。绝大多数库、引擎和工具链仍然是严格用 C++ 开发的。以其性能和可靠性而闻名，C++ 仍然是真正跨平台兼容性的最佳选择。\n\n拿起这本书，你就开始了掌握这门强大语言的旅程。虽然旅程会很长，但会充满发现！即使在我花了无数个小时与 C++ 打交道之后，我仍然发现自己对发现新的技术和方法充满了喜悦。在这本书里，我想给你一些工具和理解，让你为继续这个学习旅程做好准备。虽然新的和华而不实的工具和引擎会出现，也可能会出现，但对游戏、它们的工具和引擎是如何在低水平开发的有很强的理解，将为你提供宝贵的知识，你可以一直依靠。\n\n# 这本书是给谁的\n\n本书面向中级到高级的 C++ 游戏开发人员，他们希望将自己的技能提升到一个新的水平，并学习 3D 游戏开发的深层概念。读者将学习 AAA 级游戏开发中使用的关键概念。高级主题，如库创建、人工智能、着色器技术、高级效果和照明、工具创建、物理、网络和其他关键游戏系统，将在整个旅程中涵盖。\n\n# 这本书涵盖了什么\n\n[第 1 章](01.html)*C++ for Game Development*，涵盖了现代游戏开发中使用的一些更高级的 c++ 主题。我们将研究继承和多态、指针、引用以及常见的 STL 通用容器。用类、函数和变量模板模板化和构建通用代码的概念。类型推断和新的语言关键字 auto 和 decltype，以及它们与新的返回值语法的结合使用。最后，我们将通过查看今天使用的一些核心游戏模式来结束这一章。\n\n[第二章](02.html)、*了解库*，将讲授共享库的进阶话题。我们将看看可用的不同类型的库。我们将介绍创建自己的共享库的各种方法。\n\n[第三章](03.html)、*建立强大的基础*，将会看看使用面向对象编程和多态为你所有的游戏项目创建一个可重用结构的不同方法。我们将通过真实代码中的例子来介绍助手、管理器和接口类的区别。\n\n[第 4 章](04.html)*构建素材管道*，将涵盖开发中非常重要的一部分，即素材的处理。我们将了解导入、处理和管理内容(如声音、图像和 3D 对象)的过程。有了这个基础系统，我们可以继续完善游戏开发所需的其他系统。\n\n[第五章](05.html)*构建游戏系统*，将覆盖大量领域，在开发专业级项目所需的核心游戏系统方面取得强劲进展。到本章结束时，我们将拥有自己的自定义游戏状态系统，游戏引擎本身的许多其他组件都可以采用该系统。我们将开发自己的定制相机系统，同时建立对相机如何在较低水平上工作的理解。最后，我们将看看如何通过在示例引擎中添加子弹物理引擎来将完整的第三方游戏系统添加到我们的项目中。\n\n[第 6 章](06.html)*创建图形用户界面*，将讨论创建图形用户界面所需的不同方面。我们将详细介绍它的实现，深入探讨工作图形用户界面背后的核心架构。我们将开发一个面板和一个元素架构，包括控制定位的锚点。我们将使用观察者设计模式实现一个用户输入结构，并通过对在屏幕上显示图形用户界面元素所需的渲染管道进行编码来完善它。\n\n[第 7 章](07.html)、*高级渲染*，将涵盖使用着色器的基础知识。我们将学习如何构建编译器和链接抽象层来节省时间。我们将获得关于照明技术理论的知识，以及如何用着色器语言实现它们。最后，我们将通过查看着色器的其他用途来结束这一章，例如创建粒子效果。\n\n[第 8 章](08.html)*高级游戏系统*将深入探讨如何在游戏项目中加入脚本语言，比如 Lua。然后，我们将在这些知识的基础上检查在示例引擎中实现对话和任务系统的方法。\n\n[第九章](09.html)、*人工智能*，将在短时间内覆盖一个大的研究领域。我们将开发一个游戏人工智能到底是什么的基本定义，就此而言，它不是什么。我们还将考虑通过包含人工智能技术来扩展决策功能。我们将介绍如何通过使用转向力和行为来控制人工智能代理的移动。最后，我们将通过查看寻路算法的使用来为我们的人工智能代理创建从点到点的路径来结束这一章。\n\n[第 10 章](10.html)*多人游戏*，将在理解多人游戏如何在低水平上实现方面迈出一大步。您将了解 TCP/IP 堆栈以及用于游戏开发的不同网络拓扑。我们研究了使用 UDP 和 TCP 协议在客户机-服务器设置中传递数据。最后，我们将看看开发人员开始实现多人游戏功能时面临的一些问题。\n\n[第十一章](11.html)、*虚拟现实*，将快速介绍 VR 发展的世界；它应该为你的体验想法提供一个很好的测试平台。您将学习如何处理多视图截头体和各种硬件选项，最后看看我们如何使用 OpenVR SDK 向示例引擎添加 VR 支持。\n\n# 充分利用这本书\n\n这本书将假设一些先前的 C++ 知识。对游戏开发有基本的了解。一般来说，会在整本书中帮助你，但不应该被认为是先决条件。\n\n为了充分利用示例和开发经验，建议您使用相对较新的开发设备，该设备至少具备以下功能:\n\n*   **CPU** : 4 色\n*   **内存** : 8 GB 内存\n*   **磁盘空间** : 40 GB\n\n这些例子(除了少数例外)是为在苹果电脑和视窗电脑上运行而设计的。\n\n接下来，您应该安装以下软件:\n\n*   **PC** : Visual Studio 2015 社区或更好\n*   **macOS** : XCode 8.x 或更好。\n\n其他需要的软件将根据需要进行描述。\n\n# 下载示例代码文件\n\n你可以从你在[www.packtpub.com](http://www.packtpub.com)的账户下载这本书的示例代码文件。如果您在其他地方购买了这本书，您可以访问[www.packtpub.com/support](http://www.packtpub.com/support)并注册将文件直接通过电子邮件发送给您。\n\n您可以按照以下步骤下载代码文件:\n\n1.  登录或注册[www.packtpub.com](http://www.packtpub.com/support)。\n2.  选择“支持”选项卡。\n3.  点击代码下载和勘误表。\n4.  在搜索框中输入图书的名称，并按照屏幕上的说明进行操作。\n\n下载文件后，请确保使用最新版本的解压缩文件夹:\n\n*   视窗系统的 WinRAR/7-Zip\n*   zipeg/izp/un ARX for MAC\n*   适用于 Linux 的 7-Zip/PeaZip\n\n这本书的代码包也托管在 GitHub 上[https://GitHub . com/packt publishing/Mastering-Cpp-Game-Development](https://github.com/PacktPublishing/Mastering-Cpp-Game-Development)。我们还有来自丰富的书籍和视频目录的其他代码包，可在获得。看看他们！\n\n# 下载彩色图像\n\n我们还提供了一个 PDF 文件，其中包含本书中使用的截图/图表的彩色图像。你可以从[https://www . packtpub . com/sites/default/files/downloads/masteringppgamepdevelopment _ color images . pdf](https://www.packtpub.com/sites/default/files/downloads/MasteringCppGameDevelopment_ColorImages.pdf)下载。\n\n# 使用的约定\n\n本书通篇使用了许多文本约定。\n\n`CodeInText`:表示文本中的码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟网址、用户输入和推特句柄。这里有一个例子:“唯一的问题是它将包括所有的`ConsoleHelper`库。”\n\n代码块设置如下:\n\n```cpp\nint m_numberOfPlayers; \n\nvoid RunScripts(){} \n\nclass GameObject {}; \n```\n\n当我们希望将您的注意力吸引到代码块的特定部分时，相关的行或项目以粗体显示:\n\n```cpp\nint m_numberOfPlayers; \n\nvoid RunScripts(){} \n\nclass GameObject {}; \n```\n\n任何命令行输入或输出都编写如下:\n\n```cpp\ncl /c hello.cpp\n```\n\n**粗体**:表示一个新的术语、一个重要的单词或者你在屏幕上看到的单词。例如，菜单或对话框中的单词像这样出现在文本中。下面是一个例子:“当出现 VS2105 时，选择开发人员命令提示符。”\n\nWarnings or important notes appear like this. Tips and tricks appear like this.\n\n# 取得联系\n\n我们随时欢迎读者的反馈。\n\n**综合反馈**:发邮件`feedback@packtpub.com`并在邮件主题中提及书名。如果您对本书的任何方面有疑问，请发电子邮件至`questions@packtpub.com`。\n\n**勘误表**:虽然我们已经尽了最大的努力来保证内容的准确性，但是错误还是会发生。如果你在这本书里发现了一个错误，如果你能向我们报告，我们将不胜感激。请访问[www.packtpub.com/submit-errata](http://www.packtpub.com/submit-errata)，选择您的图书，点击勘误表提交链接，并输入详细信息。\n\n**盗版**:如果您在互联网上遇到任何形式的我们作品的非法拷贝，如果您能提供我们的位置地址或网站名称，我们将不胜感激。请通过`copyright@packtpub.com`联系我们，并提供材料链接。\n\n**如果你有兴趣成为一名作者**:如果有一个你有专长的话题，你有兴趣写或者投稿一本书，请访问[authors.packtpub.com](http://authors.packtpub.com/)。\n\n# 复习\n\n请留下评论。一旦你阅读并使用了这本书，为什么不在你购买它的网站上留下评论呢？然后，潜在的读者可以看到并使用您不带偏见的意见来做出购买决定，我们在 Packt 可以了解您对我们产品的看法，我们的作者可以看到您对他们的书的反馈。谢谢大家！\n\n更多关于 Packt 的信息，请访问[packtpub.com](https://www.packtpub.com/)。"
  },
  {
    "path": "docs/master-cpp-game-dev/01.md",
    "content": "# 一、面向游戏开发的 C++ 语言\n\n从小我就被告知，无论是在一项运动中追求完美，学习一种乐器，甚至是一项新的技术技能，对基础的深刻理解和实践才是区别所在。用 C++ 开发游戏也没什么不同。在掌握这个过程之前，你必须完善基础。这就是本书第一章的全部内容，涵盖了将在整本书中使用的基本概念。本章分为以下几节:\n\n*   高级 C++ 概念概述\n*   使用类型和容器\n*   游戏编程模式\n\n# 书中使用的惯例\n\n在整本书中，你会遇到代码片段和例子。为了保持代码的可读性和统一性，我将遵循一些基本的编码约定。虽然编码标准的话题是一个复杂而冗长的讨论，但我确实认为为任何高级项目制定一些指导方针是很重要的。在开始任何工作之前，至少应该有一个关于预期符号和命名约定的可访问的指南。如果您有兴趣了解更多关于 C++ 中使用的常见代码标准，一个很好的起点是位于[https://isocpp.org/wiki/faq/coding-standards](https://isocpp.org/wiki/faq/coding-standards)的 ISO C++ 网站上的编码标准常见问题部分。在那里，你会发现大量适用于各种情况的常用标准和一堆建议阅读链接，以进一步扩展你的知识。\n\n本书使用的标准和惯例是基于一些核心的 C++ 准则、行业最佳实践和我个人的经验。我们将在整本书中使用最新的国际标准化组织 C++ 标准 C++ 14。然而，有时我们可能会利用最新修订版 C++ 17(也称为 C++ 1y)中的一些功能。当这种情况发生时，会记下原因。\n\n类和函数名将遵循*混合案例*风格，而变量将遵循*卡梅洛案例*风格。一些示例如下所示:\n\n```cpp\nint m_numberOfPlayers; \n\nvoid RunScripts(){} \n\nclass GameObject {}; \n```\n\n本书中使用的另一个您应该注意的重要约定是范围前缀的使用。范围前缀是提高其他开发人员可读性的一种快速方法，当你令人羡慕地忘记变量属于什么范围时，也可以提高自己的可读性。以下是使用的前缀列表:\n\n*   `m_`:用于类成员变量。这些是`private`，通过使用前缀，告诉任何使用变量的人，它在类中或者通过 getters 或 setters(如果是外部的话)是显而易见的，例如`m_numberOfPlayers`。\n*   `s_`:用于静态类成员。这告诉任何使用该变量的人，该类的所有实例中只存在一个副本，并且它是静态的，例如`s_objDesc`。\n*   `g_`:用于全局变量。这告诉任何使用这个变量的人，它在任何地方都是可用的。这些我们在书中不会看到很多，比如`g_playerInfo`。\n\n# 高级 C++ 概念概述\n\n在我们开始构建我们的工具、库和其他游戏组件之前，浏览一些更常见的概念可能是一个好主意，这些概念会在我们继续阅读本书时经常出现。在本节中，我们将快速了解一些高级主题。这并不意味着是一个完整的列表，目标也不是对每个主题的全面概述，相反，对象更多的是对游戏开发概念的回顾和解释。\n\n我们将看一些简单的例子，并强调使用这些概念时可能出现的一些问题。一些经验更丰富的 C++ 开发人员可能会跳过这一部分，但是由于这些主题将在本书的其余部分中发挥重要作用，因此对每个主题都有一个坚定的理解是很重要的。如果你正在寻找更广泛的评论或对主题更深入的解释，请查看本章末尾*摘要*部分的一些建议阅读。\n\n# 使用命名空间\n\n与智能指针相比，名称空间似乎不是一个非常高级的话题，但是随着您在 C++ 游戏开发冒险中的进步，名称空间将成为开发工具包的重要组成部分。快速回顾一下，命名空间是一个声明，它为封装中的所有变量、类型和函数提供了范围。这很重要，因为它为我们提供了一种将代码组织成逻辑组的方法。通过将我们的代码分成这些组，我们不仅使其更容易阅读，而且还防止了所谓的**名称冲突**。当多个类、函数或类型具有相同的名称时，就会发生名称冲突。当您开始使用多个库时，这将成为一个大问题。使用名称空间通过使用范围来防止这种情况。例如，假设我们有一个特定平台的专用字符串类的实现。为了防止这个专用版本与标准库实现发生干扰和冲突，我们可以将我们的类型包装在一个名称空间中，如下所示:\n\n```cpp\nnamespace ConsoleHelper \n{ \n  class string \n  { \n    friend bool operator == (const string &string1,\n    const string &string2); \n    friend bool operator < (const string &string1,\n    const string &string2); \n    //other operators ... \n    public: \n    string (); \n    string(const char* input); \n    ~string() ; \n    //more functions ... \n  } \n} \n```\n\n然后我们可以这样调用我们特定的字符串实现:\n\n```cpp\nConsoleHelper::string name = new ConsoleHelper::string(\"Player Name\");\n```\n\n当然，如果我们不想一遍又一遍地键入`ConsoleHelper`部分，我们可以添加一个`using`语句，告诉编译器使用特定的名称空间来查找我们正在使用的函数、类型和变量。您可以用下面的代码行为我们的名称空间做到这一点:\n\n```cpp\nusing namespace ConsoleHelper; \n```\n\n唯一的问题是它将包括所有的`ConsoleHelper`库。如果我们只想包含命名空间的特定成员，我们可以使用以下语法来实现:\n\n```cpp\nusing namespace ConsoleHelper::string; \n```\n\n这将只包括字符串成员，而不包括整个名称空间。\n\n# 遗传和多态性\n\n继承和多态是可以轻松填充自己章节的主题。它们是 C++ 复杂且非常强大的组件。我在这一部分的目标不是要涵盖继承和多态性的全部来龙去脉。相反，我想快速了解一下如何使用这些概念来帮助您构建代码结构。我们将讨论重点，但我将假设您对面向对象的开发概念有基本的了解，并且熟悉访问修饰符和友谊等主题。\n\n首先，我们将关注继承。继承的概念是现代面向对象设计和开发的重要组成部分。虽然继承保存击键的能力是一个巨大的优势，但当允许程序员开发复杂的派生类层次结构时，继承确实显示了它的威力。让我们通过一个简单的例子来看看继承的用法。在这个例子中，我们创建了一个简单的`Enemy`类。这个类将处理实体的生命值、武器、交易伤害、人工智能脚本等等:\n\n```cpp\nclass Enemy \n{ \n  public: \n    void RunAIScripts(); \n    void Update(double deltaTime); \n  private: \n    int m_health; \n    int m_damage; \n};\n```\n\n当我们开始在游戏中实现更多的敌人时，我们可能会开始添加一些不同的条件语句，以允许我们的敌人有更多的变化。添加越来越多的`if`语句，甚至可能在一个开关中插入几个案例。这很快就变成了一堆混乱、难以阅读的代码。如果我们决定添加一个稍微不同的敌人，一个有自己可能条件的敌人，比如老板敌人类型，会发生什么。这个新的 boss 敌人类型和原来的`Enemy`类有着相似的结构，并且共享了许多相同的类型和功能。我们可以把重叠的代码复制到我们新的`Boss`类中。这是可行的，但不是理想的解决方案。我们会有很多代码重复，这种不必要的重复会带来更多的错误。然后，如果你必须修复一个错误，你现在必须在多个地方进行修复。这是一个不必要的维护难题。相反，我们可以使用继承。如果我们的新 boss 敌人类型继承了原来的敌人类型，这意味着我们可以使用原来类拥有的类型和函数。使继承更加强大的是，我们不仅可以采用继承类的函数，还可以用自己的实现覆盖它们。新的`Boss`类可以写成这样:\n\n```cpp\nclass Boss : public Enemy \n{ \n  public: \n    void Update(double deltaTime); \n    //more functions... \n}; \n```\n\n这种类型的结构通常被称为**父级**和**子级**层级，其中`Boss`类是`Enemy`类的子级。这意味着`Boss`现在将拥有`Enemy`级所需的所有结构。我应该指出，我们只继承了声明的函数和变量`public`。那是因为在使用继承的时候，类的`public`方法和变量会暴露给使用这个类的所有人。`protected`方法和变量只对类本身和任何派生的类可用。`private`方法和变量只对该类可用，没有其他人可以访问，即使是派生类\n\n我们用新的`Boss`类的特殊版本覆盖了`Update()`函数的实现。现在，在我们的代码中，我们可以编写如下内容:\n\n```cpp\n//Somewhere in game or level manager \nvoid UpdateObjects (double deltaTime) \n{ \n  enemy.Update(deltaTime); \n  boss.Update(deltaTime); \n} \n```\n\n当这段代码运行时，它将为对象调用`Update()`函数的每个单独的实现。另一方面，考虑我们有以下代码:\n\n```cpp\n//Somewhere in game or level manager \nvoid UpdateAI () \n{ \n  enemy.RunAIScripts(); \n  boss.RunAIScripts (); \n} \n```\n\n这里我们没有覆盖`RunAIScripts()`函数，因为它没有继承函数的原始类实现。虽然这是一个非常基本的例子，但它确实展示了单一继承的能力，这将我带到我的下一个主题——多重继承。\n\n假设我们继续前面的例子，我们决定要增加一个新的敌人类型，一个会飞的 boss。我们有一个`Boss`类，`Enemy`类，甚至还有一个从`Enemy`类继承的`FlyingEnemy`类，看起来是这样的:\n\n```cpp\nclass FlyingEnemy : public Enemy \n{ \n  public: \n    void Update(double deltaTime); \n    void FlightAI(); \n    //many more functions...  \n} \n```\n\n问题是我们想要`FlyingEnemy`的功能，但是我们也想要`Boss`的一些功能。同样，我们可以将我们想要的代码块复制到一个新的类中，但是 C++ 为我们提供了一个更好的解决方案，**多重继承**。顾名思义，多重继承允许我们从多个来源派生我们的类。然后，我们可以构建具有两个或更多父类的类，从而导致复杂的层次结构，但是正如我们将看到的，这也会导致一些有问题的情况。\n\n继续我们的示例，我们的新`FlyingBoss`类看起来如下所示:\n\n```cpp\nclass FlyingBoss : public Boss, public FlyingEnemy \n{ \n  public: \n    void Update(double deltaTime); \n    //other functions... \n} \n```\n\n乍一看，这看起来是一个完美的类，我们已经从两个父类继承了我们需要的函数和变量。然而，在处理多重继承时，有几个问题会开始出现。首先是模棱两可的问题。当从继承的两个或多个类具有同名的函数或变量时，就会出现歧义。例如，在我们的例子中，如果我们没有覆盖`Update()`函数，并且我们在对象上调用了`Update()`，编译器将查看我们从实现中继承的类。因为它们都有一个同名的实现，所以编译器抛出一个编译器时间错误，抱怨调用中的模糊性。为了解决这个问题，我们必须在函数调用上使用前缀来标识我们想要使用的实现的类。为此，我们在代码中使用范围运算符(`::`)从`FlyingEnemy`类调用实现，如下所示:\n\n```cpp\nFlyingEnemy::Update(deltaTime); \n```\n\n第二个问题可能不那么明显；在我们的例子中，它必须处理类继承树的构造方式。表面上看，一切都很好；`FlyingBoss`类继承自`Boss`类和`FlyingEnemy`类。问题出在继承树上的一个台阶上，`Boss`和`FlyingEnemy`类都是从`Enemy`类继承而来的。这在职业等级中创造了可怕的死亡钻石模式。这看起来没什么大不了的，但是这种模式会导致一些不幸的问题。首先是模棱两可的问题。每次尝试从`FlyingBoss`类访问`Enemy`类的任何成员变量或函数时，都是模棱两可的。这是因为每个变量和函数都有多个路径。为了解决这个问题，我们可以通过再次使用范围运算符(`::`)来指定我们想要遵循的路径。死亡钻石模式引发的另一个问题是复制问题。当我们创建一个`FlyingBoss`对象时，它将拥有从`Boss`类继承的所有内容的两个副本。这是因为`FlyingEnemy`和`Boss`类都有它们继承的`Enemy`类的副本。如你所见，这很混乱，会导致各种头痛。幸运的是，C++ 为我们提供了一个解决方案，**虚拟继承**的概念。通过虚拟继承，我们可以确保父类只在任何子类中出现一次。为了实现虚拟继承，我们在声明要继承的类时只需使用`virtual`关键字。在我们的示例中，类声明看起来像这样:\n\n```cpp\nclass Boss : public virtual Enemy \n{ \n  public: \n    //functions... \n}; \n\nclass FlyingEnemy : public virtual Enemy \n{ \n  public: \n    //functions...  \n} \n\nclass FlyingBoss : public Boss, public FlyingEnemy \n{ \n  public: \n    //other functions... \n} \n```\n\n现在`FlyingBoss`类只有一个通过继承获得的实例。\n\nWhile this does solve the issue of the diamond of death and other possible hierarchy issues, these issues are usually a sign of the underlying design issues. I would suggest researching all other options before automatically jumping to virtual inheritance as a solution.\n\n最后，我想快速提到两个重要的主题，它们携手合作，使继承成为不可思议的工具，即多态性和虚函数。简单来说，多态性是一种使用一个类的对象的能力，就像它是另一个类的一部分一样。为了简单起见，让我们检查以下内容:\n\n```cpp\nFlyingBoss* FlyBoss = new FlyingBoss();  \n```\n\n这一行代码创建了一个指向新的`FlyingBoss`对象的指针，这里没有什么新内容。但是，我们也可以像这样创建一个新指针:\n\n```cpp\nBoss* FlyBoss = new FlyingBoss(); \n```\n\n这得益于继承和多态性。我们能够引用`FlyBoss`对象，就像它是一个`Boss`类对象一样。现在看起来可能很简单，但是随着你对 C++ 理解的进步，你会开始看到这个概念有多强大。这也将我们引向最后一个话题，我想谈谈继承，虚函数。既然我们可以像这样创建指向对象的指针，那么如果我们在`FlyingBoss`对象的`Boss*`上调用`Update()`函数会发生什么？这就是虚函数的作用。如果某个功能标有`virtual`关键字，如是:\n\n```cpp\nvirtual void Update(double deltaTime); \n```\n\n这告诉编译器使用调用函数的对象类型来确定在这种情况下应该使用哪个实现。所以在我们的例子中，如果我们在`FlyingBoss`实现中使用一个虚函数，当从`FlyingBoss`对象的`Boss*`调用时，它将使用该实现。\n\n# 指针和引用\n\nC++ 中最被误解和恐惧的概念之一是指针和引用的概念。这通常是新开发人员不愿继续学习 C++ 的原因。已经写了很多书和教程，试图揭开这个话题的神秘面纱，老实说，我可以很容易地写一章，甚至是一本关于指针和引用的来龙去脉的单独的书。到目前为止，我希望你已经接受了经典意义上的指针和引用的主题，并对它们的力量和灵活性建立了健康的欣赏。因此，在这一节中，我们不打算讨论核心原则，而是看一下旧的或经典的指针和引用的更重要的用途，并简单介绍一下新的指针，这些指针旨在帮助消除一些神秘主义和内存管理问题。\n\n我们将从经典的指针和引用开始。虽然您会很快看到使用新指针的好处，但我仍然相信，正如许多 C++ 游戏开发人员一样，旧版本仍然有它们的位置。其中一个地方是在处理向函数传递数据时。当调用一个函数时，通常很容易写出如下内容:\n\n```cpp\nvoid MyFunction(GameObject myObj) \n{ \n  //do some object stuff \n} \n```\n\n虽然这段代码是完全合法的，但是如果对象的大小超过了可以忽略的程度，它可能会带来严重的性能问题。像这样传递对象时，编译器会自动在内存中创建对象的副本。在大多数情况下，这不是我们想要的。为了防止编译器在内存中创建副本，我们可以使用经典的指针或引用来传递对象。之前的代码看起来像这样:\n\n```cpp\nvoid MyFunction (GameObject& myObj) \n{ \n  //do some object stuff \n} \n```\n\n或者，如下所示:\n\n```cpp\nvoid MyFunction (GameObject* myObj) \n{ \n  //do some object stuff \n} \n```\n\n现在对象没有被复制到内存中，允许我们通过解引用来操作实际的对象。这是经典指针和引用的一种更常见的继续使用。经典指针和引用的另一个常见用途是在处理字符串和移动对象时。这种类型的应用在许多游戏开发库中仍然可见。因此，您应该对看到如下代码感到满意:\n\n```cpp\nconst char* pixelShader; \n```\n\n随着向现代 C++ 和 C++ 11 标准的转变，出现了一组新的托管指针，有助于简化对指针的理解和使用。除了一个关键的区别，这些新指针与经典指针非常相似；他们是被管理的。归根结底，这些新指针将处理自己的内存分配和删除。因为经典指针的主要问题之一是必要的手动内存和所有权问题，比如谁将删除它，以及何时删除，这使得指针的使用更受欢迎，也更灵活。这些托管指针(`unique_ptr`和`shared_ptr`)常用于更现代的游戏开发库中。\n\n# 唯一的和共享的\n\n`unique_ptr`或唯一指针被认为是智能指针。之所以称之为唯一，是因为这种类型的对象拥有其指针的唯一所有权。这意味着没有两个`unique_ptr`指针可以管理同一个对象，它是唯一的。`unique_ptr`最大的优势之一是它管理自己的生命。这意味着当指针超出范围时，它会自动销毁自己并释放内存。这解决了可怕的悬空指针问题，避免了内存泄漏。这也消除了所有权的问题，因为现在谁删除指针是显式的。\n\n自从 C++ 14 标准以来，我们现在可以使用一个方便的小函数来创建一个唯一的指针，`make_unique`。`make_unique`函数创建一个类型为`T`的对象，然后将其包装在一个唯一的指针中。用`make_unique`创建`unique_ptr`指针的语法如下:\n\n```cpp\n    std::unique_ptr<T> p = new std::make_unique<T>();\n```\n\n创建后，您可以像使用经典指针一样使用指针。取消引用操作符`*`和`->`的工作方式与正常情况相同。同样，这里最大的区别是，当指针超出范围时，它会被自动销毁，这样我们就不必手动跟踪每个退出点来避免任何内存泄漏问题。\n\n`shared_ptr`或共享指针很像唯一指针。它被认为是一个智能指针，它自动处理内存的删除和释放。不同的是共享指针*共享对象的*所有权。这意味着，与唯一指针不同，共享指针可以是指向单个对象的许多共享指针之一。这意味着，如果共享指针超出范围或通过`reset()`或`=`操作符指向另一个对象，该对象仍然存在。只有当拥有该对象的所有`shared_ptr`对象被销毁、超出范围或被重新分配给另一个指针时，该对象才会被销毁并释放其内存。\n\n同样，像唯一指针一样，共享指针也有一个方便的创建功能。`make_shared`函数创建一个类型为`T`的对象，然后将其包装在一个共享指针中。使用`make_shared`函数创建`shared_ptr`函数的语法如下:\n\n```cpp\nstd::shared_ptr<T> p = new std::make_shared<T>(); \n```\n\n此外，与唯一指针一样，共享指针具有典型的取消引用操作符`*`和`->`。\n\n# 常量正确性\n\n`const`正确性的话题在 C++ 社区可能是一个有争议的话题。我的第一门 C++ 课程的讲师甚至说`const`关键词是这门语言中最重要的关键词之一。当然，我也听到了另一面，开发者告诉我他们怎么从来不使用`const`，这完全是在浪费击键。我喜欢认为我在`const`上落在中间的某个地方；我相信它有重要的用途，但它可以像任何其他功能一样被过度使用。在这一节中，我想展示一下`const`的一些更好的用法。\n\n快速回顾一下，`const`关键字被用作类型限定符，让编译器知道这个值或对象不能改变，它是常量。当第一次开始 C++ 游戏开发时，你对`const`的第一次接触可能会来得很早。最常见的是，使用*常量*的介绍是在定义我们想要容易获得的重要值，比如说:\n\n```cpp\nconst int MAX_BULLETS = 100;\n```\n\n这就给了我们一个命名值，我们可以在代码的其他部分轻松地多次使用它。这样做的最大好处是，如果我们决定改变这个值，在这种情况下，改变项目符号的最大数量，我们就可以改变这个常量值，而不必改变分散在整个代码库中的大量硬编码值。\n\n随着你对 C++ 开发的深入，`const`关键字将成为一个更熟悉的网站。它在库和引擎代码中以多种方式大量使用。它也用于函数参数的定义或用作函数定义的修饰符。让我们简单地看一下这些。\n\n首先，当在参数定义中使用时，它成为一种保险，我们给它赋值的函数不会以任何方式修改它。以下面的代码为例:\n\n```cpp\nvoid ObjFunction(GameObject &myObject) \n{ \n  //do stuff \n  If(*myObject.value == 0) \n  { \n    //run some logic \n    Game.changeState(newState); \n    //possible unknown modifier function \n    *myObject.value = 1; \n  } \n} \n```\n\n好的，这是一个非常简单的例子，但是如果你确实调用了这样一个函数，而没有意识到它可以修改对象，你将会得到你可能没有预料到的结果。`const`关键字有两种方法可以帮助解决这个可能的问题。一种是在传递值时使用`const`关键字:\n\n```cpp\nvoid ObjFunction(const GameObject &myObject) \n{ \n  //do stuff \n  If(*myObject.value == 0) \n  { \n    //run some logic \n    Game.ChangeState(newState); \n    //possible unknown modifier function \n    *myObject.value = 1; //now will throw a compile error \n  } \n}\n```\n\n这使得现在不可能在函数的任何地方修改传入的值，保持它不变。\n\n另一种方法是创建`const`安全的函数。当你定义一个函数为`const`函数时，它允许`const`对象调用它。默认情况下，`const`对象不能调用非`const`函数。但是，非`const`对象仍然可以调用`const`函数。要将函数定义为`const`函数，我们可以添加`const`关键字来修改函数定义本身。您只需在函数签名的末尾添加`const`，如下所示:\n\n```cpp\nvoid ObjFunction(const GameObject &myObject) const \n{ \n  //do stuff \n  If(*myObject.value == 0) \n  { \n    //run some logic \n    Game.ChangeState(newState); \n    //possible unknown modifier function \n    *myObject.value = 1; //now will throw a compile error \n  } \n} \n```\n\n这是我编写任何不会修改任何对象或值的函数的首选方法。它允许一定的灵活性，以确保将来可以从`const`对象调用它，并且它还允许在代码中使用该函数的其他开发人员轻松识别该函数不会修改任何与其结合使用的对象或值。\n\n# 内存管理\n\nC++ 中**内存管理**的想法，往往是初学者噩梦的话题。我经常听到开发人员发表类似*的声明，我不使用 C++ 是因为它的手动内存管理*。事实是，手动内存管理在绝大多数项目中非常少见。如今，随着托管智能指针等现代概念的出现，手工构建的内存管理系统对于日常开发来说已经不重要了。只有当你开始高性能计算，比如游戏开发，控制内存分配和释放的想法才会成为一个问题。说到游戏开发，游戏机的整体内存可用性和速度仍然是开发人员关注的问题，这对于大多数移动设备来说也是如此，尽管价格合理的高内存设备在快速增长。在下一节中，我们将回顾堆栈和堆，以及如何处理内存分配的差异。这将为下一章奠定基础，在这一章中，我们将看到一个自定义内存管理器系统的示例。\n\n让我们从堆栈开始，适当命名的，**记忆结构**，你可以认为它很像一堆盘子或盘子。当您在堆栈上创建对象或变量时，它会被放在堆栈的顶部。当对象或变量超出范围时，这类似于从堆栈中移除盘子或碟子。堆栈上的分配在代码中看起来像这样:\n\n```cpp\nint number = 10; \nPlayer plr = Player(); \n```\n\n第一行创建一个整数值，并赋予其`10`的值。存储整数所需的内存在堆栈上分配。第二行有完全相同的想法，只是换成了一个`Player`对象。\n\n使用堆栈的一个好处是，当对象或变量超出范围时，我们分配的任何内存都会被清理掉。然而，这可能是一把双刃剑；许多较新的开发人员在超出范围后会遇到寻找或调用对象的问题，因为他们使用堆栈来存储对象。堆栈的另一个问题是它的大小有限，这取决于平台和编译器设置。如果您有大量的对象被创建并长时间保存，这可能会成为一个问题。试图分配超出堆栈可用内存的内存将引发运行时错误。\n\n另一种选择是堆，你可以认为它是一个大的内存块或容器。与堆栈不同，内存堆是无序的，很容易变得碎片化。好消息是现代内存，操作系统实现提供了处理这种碎片化的低级机制，通常称为**内存虚拟化**。这种虚拟化的另一个好处是，它通过在需要时将内存交换到硬盘驱动器，提供了比物理内存更多的堆存储访问。要分配和销毁堆上的内存，可以使用关键字`new`和`delete`，对象容器使用`new[]`和`delete[]`。代码如下所示:\n\n```cpp\nPlayer* plr = new Player(); \nchar* name = new char[10]; \ndelete plr; \ndelete[] name; \n```\n\n前两行在堆上创建了一个`Player`对象和一个字符数组。接下来的两行分别删除这些对象。重要的是要记住，对于您在堆上创建的每个内存块，您必须调用 delete 来销毁或释放该内存块。如果不这样做，可能会导致内存泄漏，应用会继续消耗越来越多的内存，直到设备耗尽并崩溃。这是一个常见的问题，很难跟踪和调试。内存泄漏是新开发人员倾向于认为 C++ 内存管理困难的原因之一。\n\n那么应该使用什么，堆栈还是堆？嗯，这真的取决于实现和存储的对象或值。我推荐的一个好的经验法则是，如果你可以不使用堆栈进行分配，那应该是你的默认值。如果您发现自己需要使用堆，尝试使用管理器系统来处理创建和删除。这将减少内存泄漏的机会以及处理自己的内存管理时出现的其他问题。在下一章中，我们将研究如何构建自己的内存管理器，作为核心库的一部分。\n\n# 处理错误\n\n我希望我能说我第一次写的每一行代码都完美无缺。现实是我是人，容易犯错。处理这些错误和追踪 bug 可能是花费大部分开发时间的地方。有一个好的方法来捕捉和处理这些在游戏运行期间发生的任何其他问题是至关重要的。本节介绍一些用于查找和处理错误的 C++ 技术。\n\n遇到问题时可以使用的一种技术是优雅地让程序崩溃。这意味着我们告诉计算机停止执行我们的代码并立即退出，而不是让计算机自行崩溃。要在 C++ 中做到这一点，我们可以使用`assert()`方法。一个示例类似于下面的代码:\n\n```cpp\n#include <assert.h> \n... \nvoid MyFunction(int number) \n{ \n  ... \n  assert(number != NULL); \n  ... \n} \n```\n\n当计算机命中代码行`assert(number != NULL);`时，它检查整数是否为`NULL`，这是否评估为真，在这种情况下，它将导致断言失败，立即停止执行并退出程序。这至少让我们有了一些控制。我们可以利用`assert()`功能提供的机会获取更多信息，创建一份事故报告。我们可以打印出文件、行，甚至错误的描述作为自定义消息。虽然这确实有效，但还有很多地方需要改进。\n\n另一种可以提供更多灵活性的处理错误的技术是异常。例外是这样工作的；当程序遇到问题时，它会抛出一个异常来停止执行。然后程序寻找最近的异常处理块。如果它在引发异常的函数中找不到该块，它就会在父函数中寻找处理块。这个过程展开堆栈，这意味着在堆栈上创建的所有对象都将按照它们被传入的顺序被销毁。该过程将继续，直到程序找到一个处理块或到达堆栈顶部，此时将调用默认异常处理程序，程序将退出。总的来说，C++ 中用于处理异常的语法非常简单。要抛出异常，可以使用关键字`throw`。这将触发程序寻找一个处理块，用关键字`Catch`表示。`Catch`块必须位于`Try`块之后，T3 块封装了可能引发异常的代码。一个简单的例子是:\n\n```cpp\nVoid ErroringFunction() \n{ \n  ...// do something that causes error \n  throw; \n} \nVoid MyFunction() \n{ \n  ... \n  Try //the try block \n  { \n    ... \n    ErroringFunction(); \n    ... \n  } \n  Catch(...)//catch *all exceptions block \n  { \n    ... //handle the exception \n  } \n} \n```\n\n还可以通过将异常类型作为参数传递给 catch 块来捕获和处理特定的错误，如以下代码所示:\n\n```cpp\n... \nThrow MyExeception(\"Error! Occurred in Myfunction()\"); \n... \nCatch(MyException e) \n{ \n  ...//handle exception \n}  \n```\n\n使用异常的好处是，我们可以灵活地以任何我们想要的方式处理错误。如果情况允许，我们可以纠正导致错误的问题并继续，或者我们可以简单地将一些信息转储到日志文件并退出程序。选择权在我们。\n\n您实现哪种解决方案来处理错误完全取决于您和您正在处理的项目。事实上，有些开发人员会选择忽略一起处理错误。然而，我强烈建议使用某种错误处理系统。在整本书用于演示的示例代码中，我实现了一个异常处理系统。我建议将该实现作为参考。本章末尾的建议阅读部分也包含了一些处理错误的重要参考。\n\n# 使用类型和容器\n\nC++ 是一种强类型的不安全语言。它提供了令人难以置信的控制，但它最终期望程序员知道他们在做什么。了解如何在高级水平上处理类型对于掌握游戏库和核心系统编程至关重要。游戏开发在很大程度上依赖于 C++ 中类型的灵活性，它也依赖于可用的高级库，如**标准模板库** ( **STL** )。在接下来的几节中，我们将看看游戏开发中使用的一些更常见的容器及其 STL 实现。我们还将介绍如何通过使用模板来创建通用代码。最后，我们将通过类型推断及其更常见的用例来总结类型和容器的主题。\n\n# 通用容器\n\nC++ STL 是容器类的集合，允许以不同的结构存储数据，迭代器提供对容器元素的访问，算法可以对容器及其包含的元素执行操作。这些结构、迭代器和算法都经过了极大的优化，并且在大多数情况下都使用了 C++ 语言标准的最新实现。STL 广泛使用了 C++ 中的模板特性，以便于我们自己的类型使用。我们将在下一节中研究模板化。STL 是一个巨大的主题，有许多关于概念和实现的书籍。如果你对 STL 没有什么经验，我强烈建议你读一些关于这个主题的令人惊叹的书。我在本章末尾的*总结*部分列举了几个。这一部分将着重强调一些在游戏开发中更常用的 STL 容器。我将假设您对容器有一个基本的了解，并且您有一些使用迭代器遍历容器中的元素的经验。\n\n让我们从两个序列容器开始，向量和列表。它们之所以被称为**序列容器**是因为它们以特定的顺序存储它们的元素。这允许在该顺序或序列的任何地方添加或移除元素。向量和列表是你会遇到的最流行的 STL 序列容器。了解一些关键事实将帮助你决定哪一个最适合特定的任务。我已经包括了一些建议来帮助指导你。\n\n# 矢量\n\n**Vector** 是 STL 中提供的最基本的容器之一。虽然它相对简单，但它非常灵活，是游戏开发中使用最广泛的容器之一。你最有可能看到它的地方是在替换一个 C 数组。使用数组的一个更大的缺点是必须在声明时定义数组的大小。这意味着，在大多数情况下，您需要知道所需元素的最大数量，或者您需要分配比您以往需要的更多的元素。幸运的是，我们矢量没有这个，预定义的大小，缺点；向量将增长以适应新添加的元素。要创建整数向量，我们可以使用以下语法:\n\n```cpp\nstd::vector<int> playerID ; \n```\n\n您可能在`vector`之前注意到了`std::`，这是因为`vector`类是`std`名称空间的一部分，所以我们需要确定我们希望使用该实现。查看本章前面的*使用名称空间*部分。我们可以通过在代码文件的开头添加一个`using namespace std;`语句来避免必须键入这个。我更喜欢将`std::`添加到我的标准库调用中，或者任何其他特定的名称空间调用中。由于游戏开发使用如此多的库，有大量的`using`语句会变得混乱和容易出错。虽然它需要额外的几次击键，但它可以省去一大堆麻烦。\n\n我个人在大多数情况下用向量代替数组，我建议你也这样做。但是，在将所有数组都改为向量之前，一定要注意向量中可能导致问题的一个方面。创建向量时，会为其分配一个连续的内存块。内存量取决于向量中元素的数量。总会有空间给当前向量中的所有元素加上一点额外的空间，以允许添加新的元素。这就是向量的诀窍，当你添加更多的元素，并最终开始耗尽空间时，向量会抓取更多的内存，以便它总是有空间容纳新的元素。它首先创建一个新的内存块，复制第一个内存块的所有内容，然后删除它。这就是问题可能悄悄出现的地方。为了防止不断的分配、复制和删除，当一个向量分配新的内存时，它的大小通常是以前的两倍。由于向量永远不会收缩，如果我们使用向量的方式会产生大量的元素加减，这很容易成为内存问题，尤其是对于内存较低的设备。知道这一点不应该阻止您使用向量，当在正确的情况下实现时，这应该很少成为问题，并且如果出现问题，可以通过重构轻松缓解。\n\n一些关于何时使用向量的完美例子是这样的情况:玩家列表、角色动画列表、玩家武器，实际上是任何你可能不经常添加和删除的列表。这将避免可能的内存问题，同时让你访问向量的迭代器、算法和其他好处。\n\n# 目录\n\nA **列表**是你在用 C++ 开发游戏时可能会看到的另一种序列容器。要创建整数值的列表容器，语法如下所示:\n\n```cpp\nstd::list<int> objValues; \n```\n\n列表容器在实现和开发中的一般用法上与向量有很大的不同。关键区别在于，与向量不同，列表容器不会将其所有元素存储在一个大的连续内存块中。相反，它将其元素存储为双向链表中的节点。其中每个节点都持有指向下一个和上一个节点的指针。当然，这使得向量的额外内存分配问题消失，因为只有列表中每个元素的内存是预先分配的。当添加新元素时，只为新节点创建内存，从而节省了在向量实现中可能看到的浪费内存。这也允许元素被添加到列表中的任何地方，与向量容器相比具有更好的性能。不过，也有一些缺点。由于内存中各个节点的这种设置，列表中的每个操作都很可能导致内存分配。由于每个节点可能以无保证的顺序分散在内存中，这种持续的内存分配在动态内存较慢的系统中可能是一个潜在的问题。这也意味着列表遍历其元素的速度比向量慢。同样，这并不意味着阻止你在项目中使用列表。我建议你在有一组经常添加或删除的对象或元素的地方使用列表。一个很好的例子是在每一帧中呈现一个游戏对象或网格的列表。不应将列表视为向量的替代。各有利弊，找到解决方案的最佳选择往往是最难的部分。\n\n最后，我们要看的最后一个容器是一个常用的**关联容器**。与序列容器不同，关联容器不会保留其中元素的相对位置。相反，关联容器是为了速度而构建的，更具体地说，是为了元素查找速度。在不进入**大 O 符号**的情况下，这些关联容器及其相应的算法在查找特定元素时远远优于向量和列表。之所以称它们为关联容器，是因为它们通常提供一个密钥/数据对，有助于更快的查找。需要注意的是，有时候容器中的关键是数据本身。我们将在这里关注的是地图容器。\n\n# 地图\n\n地图是游戏开发中多种用途的便捷容器。与矢量或列表相比，地图的独特之处在于，每个地图都由两个数据组成。第一段数据是一个关键字，第二段是实际存储的元素。这就是为什么地图在查找元素时如此高效的原因。一个简单的想法是，映射就像数组，但是不是使用整数值来索引元素，而是使用任何类型的键来索引它的元素。地图甚至有一个专门的`[]`操作符，允许您使用熟悉的数组语法访问元素。\n\n要创建一个以整数为键，以字符串为元素类型或值的映射，我们的代码如下所示:\n\n```cpp\nstd::map<int,string> gameObjects; \n```\n\n当涉及到内存使用时，映射不同于列表和向量容器。地图不会像矢量一样将数据存储在连续的块中，而是将元素保存在节点中，就像列表一样。列表和映射如何处理它们的分配的区别在于节点的结构方式。地图中的节点有指向下一个节点和上一个节点的指针，就像列表一样，但是这些节点是以树形模式排列的。这种树模式通过添加和删除节点来自动平衡自己。好消息是，这种平衡行为不会增加任何新的分配。映射的性能与列表非常相似，因为内存管理是相似的，唯一的区别是节点树的自动平衡开销非常小。\n\n地图常用的一种方式是字典的形式。它们通过键提供了对唯一值的快速查找；正因为如此，游戏开发中的一些好的示例地图是:具有唯一 id 的游戏元素列表，具有唯一 id 的键的多人客户端列表，以及几乎任何情况下，您有一组元素想要与某种键值对一起存储。\n\n# 模板\n\n模板在 C++ 语言中是一个较新的概念。当使用不同的数据类型或类时，模板有助于解决必须重写相同代码的常见问题。这允许我们编写所谓的通用代码。然后，我们可以在项目的其他部分使用这个通用代码。从 C++ 14 标准开始，现在有三种类型的模板可以使用:**类模板**、**函数模板**和**变量模板**。让我们在接下来的部分中仔细看看它们。\n\n# 类模板\n\n使用类模板，我们可以创建可以定义的抽象类，而无需指定类的函数将处理什么数据类型。这在构建库和容器时变得非常有用。事实上，C++ 标准库广泛使用了类模板，包括我们在本章前面看到的`vector`类。让我们看一下一个`Rectangle`类的简单实现。这可能是一个有用的类，用于查找屏幕坐标、按钮和其他图形用户界面，甚至简单的 2D 碰撞检测。\n\n不使用类模板的基本实现如下所示:\n\n```cpp\nclass Rectangle \n{ \n  public: \n    Rectangle(int topLeft, int topRight, int bottomLeft,\n    int bottomRight) : \n    m_topLeft (topLeft), m_topRight(topRight), \n    m_bottomLeft(bottomLeft), m_bottomRight(bottomRight){} \n\n    int GetWidth() { return m_topRight - m_topLeft; } \n  private: \n    int m_topLeft; \n    int m_topRight; \n    int m_bottomLeft; \n    int m_bottomRight; \n}; \n```\n\n这在大多数情况下都很好，但是如果我们想在不同的坐标系中使用这个矩形，比如使用 0.0 到 1.0 的值，我们将不得不做一些改变。我们可以复制代码，并将整数数据类型更改为 float，这样就可以了，但是使用类模板我们可以避免这种代码重复。\n\n使用模板，新的`Rectangle`类看起来像这样:\n\n```cpp\ntemplate <class T> \nclass Rectangle \n{ \n  public: \n    Rectangle(T topLeft, T topRight, T bottomLeft,\n    T bottomRight) : \n    m_topLeft(topLeft), m_topRight (topRight), \n    m_bottomLeft(bottomLeft), m_bottomRight(bottomRight){} \n\n    T GetWidth() { return m_topRight - m_topLeft; } \n    T GetHeight() { return m_bottomLeft - m_topLeft;} \n  private: \n    T m_topLeft; \n    T m_topRight; \n    T m_bottomLeft; \n    T m_bottomRight; \n}; \n```\n\n您将注意到的第一个变化是在我们的类定义之前包含了`template<class T>`。这告诉编译器这个类是一个模板。`T`是数据类型的占位符。第二个变化是所有的整数数据类型都被这个占位符替换了。现在我们可以使用`int`数据类型创建一个矩形，如下所示:\n\n```cpp\nRectangle(10,20,1,2); \n```\n\n当编译器遇到这一行代码时，它会遍历模板类并用`int`替换占位符的所有实例，然后动态编译新类。要使用浮点值创建矩形，我们可以使用以下代码:\n\n```cpp\nRectangle (1,1,0.5,0.5); \n```\n\n我们可以对任何我们喜欢的数据类型这样做；唯一的限制是类的操作必须支持这些类型。如果不是，将引发运行时错误。这方面的一个例子是一个类模板，它具有乘法函数，并试图将该模板用于字符串。\n\n# 功能模板\n\n函数模板的概念与类模板非常相似；最大的区别是函数模板不需要显式实例化。它们基于传入的数据类型自动创建。下面将交换两个值，但它不特定于任何类类型:\n\n```cpp\ntemplate<class T> \nvoid Swap (T &a, T &b) \n{ \n    T temp = a; \n    a = b; \n    b = temp; \n} \n```\n\n然后，您可以传递整数值:\n\n```cpp\nSwap(23,42); \nor float values; \nSwap(12.5, 5.2); \n```\n\n事实上，您可以将此函数用于任何支持赋值运算符和复制构造函数的类型。这里的限制是两种数据类型必须是相同的类型。即使数据类型有隐式转换，也是如此。\n\n```cpp\nSwap(1.8, 22); // Results in a compile time error \n```\n\n# 可变模板\n\n最后一类我想快速提一下的模板是变量模板，不要和**变量模板**混淆。在 C++ 14 中引入的变量模板允许在模板化的结构或类中包装变量。常用的圆锥形例子是圆周率的数学构造:\n\n```cpp\ntemplate<class T> \nconstexpr T pi = T(3.1415926535897932385); \n```\n\n这意味着您可以将`pi`称为`float`、`int`或`double`变量，并在通用函数中使用它，例如，计算给定半径的圆的面积:\n\n```cpp\ntemplate<typename T> \nT area_of_circle_with_radius(T r)  \n{ \n  return pi<T> * r * r; \n} \n```\n\n同样，这个模板化的函数可以用于各种数据类型，因此您可以将一个区域返回为整数、浮点值或任何其他支持的数据类型。你可能看不到经常使用的变量模板。在 C++ 中，它们仍然被认为是一个新的想法，但是意识到它们的存在是很重要的。他们确实有一些独特的案例，可能有一天会帮助你解决一个难题。\n\n如您所见，模板确实有其好处，我鼓励您在有意义的地方使用它们。然而，在实现模板时，注意一些可能的缺点是很重要的。第一个潜在的缺点是所有的模板必须在同一个文件中有它们的整个实现，通常是头。`export`关键字纠正了这一点，但并非所有商业编译器都支持。模板的另一个缺点是它们因难以调试而臭名昭著。当问题存在于模板化代码内部时，编译器往往会给出隐藏的错误。我最大的建议是谨慎使用它们，就像其他功能一样。仅仅因为一个特性是高级的，并不意味着它是一个很好的匹配。最后，检查您的编译器，了解实现的确切细节。\n\n# 类型推断以及何时使用它\n\nC++ 11 标准带来了一些非常有用的**型干扰**功能。这些新功能为程序员提供了更多工具来创建通用、灵活的代码。在本节中，我们将更深入地了解这些新功能。\n\n我们将从一个新的、强大的关键词开始。`auto`关键字允许您让编译器在声明时推断变量类型(如果可能)。这意味着与其这样定义变量:\n\n```cpp\nint value = 10; \n```\n\n现在可以直接使用`auto`:\n\n```cpp\nauto value = 10; \n```\n\n然而，这并不是`auto`关键词的最佳用法，事实上，这是一个你不应该做的完美例子。虽然在声明任何变量时使用`auto`可能很有诱惑力，但这不仅会给编译增加完全不必要的开销，还会使您的代码更难阅读和理解。那是你不应该用`auto`做的，那你应该用`auto`做什么？`auto`真正显示其有用性的地方，是它与模板配合使用的时候。当与`auto`关键字结合时，模板可以变得极其灵活和强大。让我们看一个简单的例子。\n\n在这个例子中，我们有一个简单的模板函数，它为我们创建了一些游戏对象，如下所示:\n\n```cpp\ntemplate <typename ObjectType, typename ObjectFactory> \nvoid CreateObject (const ObjectFactory &objFactory) \n{ \n  ObjectType obj = objFactory.makeObject(); \n  // do stuff with obj \n} \n```\n\n要调用此代码，我们将使用以下代码:\n\n```cpp\nMyObjFactory objFactory; \nCreateObject<PreDefinedObj>(objFactory); \n```\n\n这段代码运行良好，但是使用`auto`关键字可以更加灵活和容易阅读。我们的代码现在看起来如下所示:\n\n```cpp\ntemplate <typename ObjectFactory > \nvoid CreateObject (const ObjectFactory &objFactory) \n{ \n  auto obj = objFactory.MakeObject(); \n  // do stuff with obj \n} \n```\n\n然后我们调用这个函数的代码将是:\n\n```cpp\nMyObjFactory objFactory; \nCreateObject (objFactory); \n```\n\n虽然这是一个过于简单的说法，但它应该让你看到`auto`可以提供的可能性。通过不定义对象工厂将返回什么类型，我们允许工厂在其实现中有更多的自由，这反过来允许工厂在我们的代码库中有更多的使用。\n\n在模板之外，您会看到`auto`关键字在起作用的地方之一是 for 循环中迭代器的声明。这已经成为许多现代库的普遍做法。你会经常看到这样写的循环:\n\n```cpp\nfor (auto it = v.begin(); it != v.end(); ++ it)  \n{ \n  //do stuff \n}\n```\n\n`auto`关键字有一个辅助关键字`decltype`，它从一个变量中提取类型。所以在使用`auto`让编译器推断变量类型的地方，使用`decltype`来确定变量的类型。当您在最后一部分添加`auto`关键词功能时，这变得非常有用，作为`return`值。在 C++ 11 和`auto`关键字之前，`return`值必须在函数名之前声明，如下所示:\n\n```cpp\nTreeObject CreateObject (const ObjectFactory &objFactory) \n{ \n  auto obj = objFactory.MakeObject(); \n  return obj; \n} \n```\n\n这意味着`CreateObject`函数必须返回一个`TreeObject`类型，但是如前所述，让编译器推断`objFactory.MakeObject();`返回什么允许更大的灵活性。为了推断函数返回的对象类型，我们可以使用`auto`、`decltype`的概念以及新的`return`语法。我们的新功能现在将如下所示:\n\n```cpp\ntemplate <typename ObjectFactory > \nauto CreateObject(const ObjectFactory &objFactory) -> decltype (objFactory.makeObject()) \n{ \n  auto obj = objFactory.MakeObject(); \n  return obj; \n} \n```\n\n还要注意的是`auto`和`decltype`确实给我们的编译时间增加了一些开销。在大多数情况下，这将是微不足道的，但在某些情况下，这可能会成为一个问题，所以当将这些新的关键词合并到您的代码库中时，请注意这一点。\n\n随着您继续构建更多的库和工具集，拥有构建更通用、更灵活的代码的能力将变得至关重要。使用`auto`、`decltype`和新的`return`语法等技巧只是实现这一目的的一些方法。在接下来的章节中，我们将看到更多这些有用的概念。\n\n# 游戏编程模式\n\n简单地说，编程模式或开发模式是对一个常见或反复出现的问题的解决方案。它是一个描述或模板，提供了一个可以在许多不同情况下使用的解决方案。这些模式是形式化的最佳实践，通常是通过多年的迭代开发出来的。通过在您的项目中使用模式，您可以使您的代码更具性能、更强大、适应性更强。它们允许您构建本质上解耦的结构化代码。这种解耦使您的代码更通用、更易于使用。你不再需要把整个程序塞进你的脑子里来理解特定的代码段想要完成什么。相反，您可以专注于独立运行的较小块。这就是面向对象设计的真正力量。这种解耦还将通过将一个或多个问题隔离到特定的代码段，使得在测试期间跟踪错误变得更加容易。\n\n当您开始构建自己的库和引擎结构时，至少对最基本的模式有一个坚实的理解是至关重要的。在接下来的几节中，我们将研究其中的一些基本模式。\n\n# 使用循环\n\n可以说，游戏开发中最重要的概念之一就是循环的概念。如果你以前做过游戏，我几乎可以保证你用过某种循环。尽管循环很常见，但循环的特定实现通常并不常见。模式为开发人员提供了构建高性能、灵活循环的指导方针和结构。\n\n最常见的循环模式之一是**游戏循环模式**。游戏循环模式的目的是提供一种机制，将游戏时间的流逝与用户输入和其他事件分离，而不考虑处理器的时钟速度。一个简单的解释是:在游戏运行期间，或者在特定状态期间，一个游戏循环连续运行，参见后面的状态机部分。在这个连续的循环中，循环的每一次滴答声或转弯处，我们都有机会更新游戏的部分内容。这通常包括更新当前游戏状态，检查和更新任何用户输入，而不阻塞，以及调用来绘制或渲染任何游戏对象。许多平台和几乎所有引擎都有自己的实现。需要注意的是，你使用的平台或引擎是否有自己的游戏循环。如果是这样，那么您必须将代码和循环结构挂钩到提供的机制中。\n\n举个例子，Unity 游戏引擎抽象了循环过程，它们通过所有游戏对象继承的`Update()`函数向内部游戏循环公开连接性。这种 Unity 结构是一个很好的例子，说明了游戏循环模式如何与更新模式等其他模式相结合，构建一个级联循环系统，允许主游戏循环驱动每个对象的内部循环机制。我们现在不会建立一个完整的例子，但是当我们继续阅读这本书时，我们会看到更多这样的结构是如何建立的。接下来的几节将继续这种结合模式来构建一个完整的游戏系统流程的想法。\n\n为了帮助描述游戏循环是如何构建的，让我们看一个典型的、稍微简单的例子:\n\n```cpp\ndouble lastTime = getSystemTime(); \nwhile (!gameOver) \n{ \n  double currentTime = getSystemTime (); \n  double deltaTime = currentTime - lastTime; \n  CheckInput(); \n  Update(deltaTime); \n  Draw(); \n  lastTime = currentTime; \n} \n```\n\n第一行代码`double lastTime = getSystemTime();`存储循环第一次运行之前的时间。接下来我们有一个简单的`while`循环，在这种情况下，当变量`gameOver`不为真时，循环将继续运行。在`while`循环中，首先我们得到当前时间。接下来我们创建一个`deltaTime`变量，这是自循环的最后一步以来经过的时间。然后我们调用运行游戏的其他组件:`Input`、`Update`和`Draw`。这是游戏循环模式的关键；我们用这个标准的跑步循环来推动游戏前进。你可能会注意到我们将`deltaTime`传递给了`Update`方法。这是循环的另一个重要组成部分，在不深入更新模式的情况下，通过传递循环之间经过的时间，我们能够使用适当的时间片来修改像游戏对象物理这样的东西，这对于保持一切都在下沉并看起来平滑很重要。这种风格的游戏循环模式实现被称为**可变时间步长**模式，因为循环步长是基于更新所花费的时间量。更新代码花费的时间越长，步骤之间的时间就会越长。这意味着循环的每一步都将决定实际时间过去了多少。使用这种方法意味着游戏将在不同的硬件上以一致的速度运行，这也意味着拥有强大机器的用户将获得更流畅的游戏体验。然而，这个实现远非完美。它没有优化渲染或处理步骤之间可能出现的延迟，但这是一个好的开始。了解幕后发生的事情是重要的一步。在下一节中，我们将研究一种允许我们基于事件创建代码路径的模式，这与循环相结合是游戏系统流的自然进化。\n\n# 状态机\n\n我们要考察的下一个模式是**状态模式**；更具体地说，我们将研究有限状态机。状态机是一个极其强大的工程概念。虽然除了人工智能开发之外，有限状态机在大多数编程学科中并不是一种常见的模式，但它在构建分支代码中扮演着重要的角色。可能令人惊讶的是，我们日常生活中发现的许多机械逻辑电路都是由有限状态机的形式构建的。\n\n一个真实的例子是一组交通灯，它根据等待的汽车改变状态(有时可能不够快)。有限状态机可以归结为一个抽象系统，在这个系统中，机器可以处于有限数量的状态中的一个，也是唯一一个。机器将保持这种状态，称为当前状态，直到事件或触发条件导致转换。让我们看一个演示这个概念的例子:\n\n```cpp\n//simple enum to define our states \nEnum GameState \n{ \n  Waiting, \n  Playing, \n  GameOver \n} \n\nGameState currentGameState = GameState.Waiting; \n\n//Other game class functions... \n\nvoid Update(double deltaTime) \n{ \n  //switch case that acts as our machine \n  switch(currentGameState) \n  { \n    case Waiting: \n      //do things while in waiting state \n      //Transition to the next state \n      currentGameState = Playing; \n    break; \n    case Playing: \n      //do things while in playing state \n      CheckInput(); \n      UpdateObjects(deltaTime); \n      Draw(); \n      //Transition to the next state \n      currentGameState = Gameover; \n    break; \n    case Gameover: \n      //do things while in waiting state \n      UploadHighScore(); \n      ResetGame(); \n      //Transition to the next state \n      currentGameState = Waiting; \n    break; \n  } \n```\n\n首先，我们有一个容纳游戏状态的`enum`结构。接下来，我们创建一个`GameState`变量类型来保存机器当前所处的游戏状态。然后在一个`Update`循环中，我们实现一个`switch case`构造，控制从一个状态到另一个状态的转换流程。这个实现的关键是机器的每个状态都有一个到下一个状态的转换。这使机器保持运行，并允许我们根据机器的当前状态执行不同的操作。虽然这可能是游戏状态机最基本的形式之一，但它确实证明了有限状态模式的有用性。随着您继续创建库和其他组件，您将开始看到这些不可思议的工具有越来越多的用途。还有许多其他更复杂的实现，甚至更多的模式来帮助描述它们。其中一些将在本书后面的章节中看到。\n\n# 事件侦听器\n\n在游戏开发过程中，你经常会发现这样的情况:你需要基于一些用户输入或者从另一个代码块触发的条件来执行某些代码。也许你只是需要一个稳固的游戏对象交流方式。这就是使用事件或消息传递系统的想法。已经创建了许多模式来帮助解决这个问题，包括**监督**、**模型视图控制器**等。这些模式中的每一种都实现了不同的机制来处理事件；许多实际上是相互建立的。然而，在我们开始使用其中一种模式之前，我认为重要的是要了解幕后发生的事情的基础，以便为所有这些解决方案提供动力。通过构建我们自己的解决方案，我们将更好地理解问题，并对解决问题的模式有更多的欣赏。在我们的示例中，我们将使用本章中所学的概念来构建一个简单但可重用的事件系统，该系统可以在您自己的项目中使用。\n\n我们可以采取的第一种方法是使用我们刚刚看到的状态机的简单版本。在这种方法中，我们使用`switch case`构造根据传入的事件类型来分支代码。为了节省空间和时间，省略了一些基本的结构代码:\n\n```cpp\n//Event could be an enum or struct that houses the different event types \nvoid GameObject::HandleEvent(Event* event) \n{ \n  switch(event) \n  { \n    case Collision: \n      HandleCollision(); \n      //Do other things... \n    break; \n    Case Explosion: \n      HandleExplosion() \n      //More things... \n    break; \n  } \n} \n```\n\n这是一个快速而肮脏的实现，将在一些非常基本的情况下工作。如果我们对事件类型使用结构或联合，我们可以添加一些简单的消息功能，这将使它更加有用。不幸的是，这种方法最终有太多重大问题。首先，我们需要有单一的事件类型来源。然后，每当我们想要添加新的事件类型时，我们都必须编辑这个源。第二个是`switch case`构造，同样，每次我们希望添加新的事件类型时，我们都必须追加和修改这个部分。所有这些都非常繁琐，容易出错，并且在支持 OOP 的语言中是糟糕的设计。\n\n我们可以采取的第二种方法依赖于**运行时类型信息** ( **RTTI** )的能力，这是在运行时确定变量类型的概念。使用 RTTI 使我们能够使用`dynamic_cast`来确定解决方案中的事件类型。我应该指出，并非所有的 RTTI 实现都是相同的，并且可能不会在所有编译器中默认打开。请查看您的编译器文档以获取准确信息。\n\n首先，我们为将要创建的所有特定事件创建一个简单的基类:\n\n```cpp\nclass Event \n{ \n  protected: \n    virtual ~event() {}; \n}; \n```\n\n现在只需使用`dynamic_cast`来确定事件的类型，并将消息信息传递给对象自己的处理功能:\n\n```cpp\nvoid onEvent(Event* event) \n{ \n  if (Collision* collision = dynamic_cast<Collision*>(event)) \n  { \n    onCollision(collision); \n  } \n  else if (Explosion* explosion = dynamic_cast< Explosion *>(event)) \n  { \n    onExplosion(explosion); \n  } \n  //etc... \n}\n```\n\n这是一个比我们看到的第一个更优雅的解决方案。它提供了更大的灵活性，并且更容易维护。然而，我们可以重构这段代码，使其更加简单。使用我们之前学习的模板概念，以及良好的旧方式重载，我们的新代码可以这样构建:\n\n```cpp\nTemplate <class T> \nbool TryHandleEvent(const Event* event) \n{ \n  If(cosnt T* event = dynamic_cast<const T*> (event)) \n  { \n    Return HandleEvent(event); \n  } \n  Return false; \n} \n\nvoid OnEvent( const Event* event) \n{ \n  If(TryHandleEvent<Collision>(event)) return; \n  Else if(TryHandleEvent<Explosion>(event)) return; \n} \n```\n\n像本章中的其他例子一样，这个例子是基本的。虽然这种新方法确实比第一种方法更干净、适应性更强，但它也有自己的一些缺点。这包括`dynamic_cast`的开销，完全依赖于类层次结构。`if...else`链仍然存在维护和易错代码的问题。此外，我们还有更大、更重要的不当类型检测问题。例如，使用这种方法，如果我们有一个从另一个继承的类型，比如说来自`Explosion`类的`LargeExplosion`类。如果对对象类型的查询出了问题，事件指针会首先被转换到`Explosion`类，而实际上它指向的是`LargeExplosion`类，编译器会错误地检测到类型并调用错误版本的函数。更理想的解决方案是拥有一个`EventHandler`类来处理所有事件的注册、存储和多态功能。然后可以有成员函数处理程序来实现特定的事件类型，这些事件类型又可以从处理程序函数基类继承。这将解决我们在其他两种方法中看到的许多问题，同时给我们一个更通用的、可重用的实现。\n\n不过，我们将在这里停止我们的实现。由于事件处理系统在游戏系统的许多不同部分发挥着如此强大的作用，从工具链到用户输入和网络，我们将在本书的其余部分看到更多这样的模式和技术。\n\n# 摘要\n\n这一章我们讲了很多。我们讨论了现代游戏开发中使用的一些更高级的 C++ 主题。我们研究了继承和多态、指针、引用以及常见的 STL 通用容器。用类、函数和变量模板模板化和构建通用代码的概念。类型推断和新的语言关键字`auto`和`decltype`以及它们与新的`return`值语法的结合使用。最后，我们结束这一章，看看今天使用的一些核心游戏模式。\n\n在下一章中，我们将研究如何使用这些关键概念来创建可以在我们的游戏开发项目中使用和重用的核心库。"
  },
  {
    "path": "docs/master-cpp-game-dev/02.md",
    "content": "# 二、理解库\n\n了解库如何工作对于掌握 C++ 游戏开发极其重要。了解库如何在 C++ 中工作将使您能够构建更健壮的游戏和工具。通常，创建游戏引擎核心的最基本要素可以在易于使用的*可再发行*库中找到。在本章中，我们将探讨库类型之间的主要区别，以及如何创建、构建和使用它们。对于这一章，我假设你已经通读了[第一章](01.html)、*游戏开发用 C++ 第五章*，并且对编译和链接过程有了大致的了解。本章由以下几节组成:\n\n*   库构建类型\n*   构建自定义共享库\n\n# 我们为什么使用库？\n\n库是 C++ 中的一个关键概念，它们是允许语言构建模块化设计和可移植代码的机制。通过使用库，我们能够创建可重用的代码，以便在多个程序之间以及与其他开发人员共享。它让开发人员不必一遍又一遍地重写特定的代码块，从而节省时间。通过允许使用其他开发人员的解决方案来解决常见问题，这也节省了开发人员的时间。**标准模板库** ( **STL** )就是一个很好的例子。对于 C++ 中常见的大量问题，STL 都有解决方案。这些解决方案包括实现字符串等数据类型、向量等容器以及排序等算法。这些标准实现来自多年的改进和发展。因此，它们往往具有令人难以置信的性能和高度优化，作为一般规则，我建议默认使用标准实现，而不是手写实现。有成千上万的库可用于 C++ 开发。\n\n# 库构建类型\n\n创建库文件有几种不同的方法。您可以使用不同的工具，如**集成开发环境** ( **IDE** )。开发环境工具，如 Visual Studio 和 XCode，通常包含模板或启动项目来为各种平台和情况创建库文件。另一种更简单的方式，也是我们在这里使用的方式是通过命令行。更具体地说，是 Visual Studio 2015 附带的开发人员命令提示符和 macOS X 附带的终端程序。您可以在 Visual Studio 网站上获得 Visual Studio 2015 社区版的副本，这是一个面向五名或更少开发人员的团队的免费版本。\n\n要在 Windows 8 或更高版本上打开开发者命令提示符，点击 Windows 键并开始键入`developer command prompt`，当出现 VS2105 时选择开发者命令提示符:\n\n![](img/6814c5c2-614a-4c50-b03c-5462303e892f.png)\n\n要打开 OS X 终端，请打开应用启动器，并在屏幕顶部的搜索栏中键入`Terminal`:\n\n![](img/41b115fc-0c1a-4e85-a412-07398b30ca62.png)\n\n首先，让我们创建一个基本库，这样我们就可以从其他程序中使用它。在这个例子中，我们将只编写一个简单的函数，它将打印出历史悠久的行`Hello World`。如果没有至少一个 hello world 程序，这就不是一本关于编程的书。这是我们将要使用的文件，我把我的保存为`hello.cpp`。\n\n```cpp\n#include <iostream> \nvoid Hello() \n{ \n  std::cout<< \"Hello World!\"<<std::endl; \n} \n```\n\n# 静态链接库\n\n静态库是作为应用本身的一部分编译的库。这意味着所有与库相关的代码都包含在一个文件中，在 Windows 系统上是`.lib`，在 Linux/OS X 系统上是`.a`，并且直接链接到程序中。包含静态库的程序从库中创建所需代码的副本，并将该副本放在调用库实现的程序中。每次打电话到库都会这样。这导致了使用静态库的一个更大的缺点，它增加了可执行文件的整体大小。另一方面，使用静态库的优点是用户运行程序不需要外部依赖。这有助于避免用户系统上的库是错误版本或者不得不与程序一起分发的问题，这可能会产生一大堆问题。你会经常听到这个常见的问题被提到 *Dll 地狱*。静态库的另一个优点是，由于它们作为构建过程的一部分被链接，这将允许编译器和构建工具有更多的机会来优化实现。一个好的经验法则是，对于公共或标准库，大多数用户(OpenGL 或 DirectX)将使用动态或共享库。对于不太常见的库(GLFW 或 SDL)，您更可能使用静态库。\n\n要从开发人员命令提示符将我们的`hello.cpp`文件转换为静态库，我们执行以下步骤:\n\n# 在窗口上\n\n请遵循以下步骤:\n\n1.  对于 Windows，您需要键入以下命令:\n\n```cpp\n    cl /c hello.cpp\n```\n\n`cl`是编译和链接的命令。`/c`告诉编译器我们只想编译，不链接我们的文件。最后，我们传入要编译的文件。这将创建一个对象文件，`hello.obj`，然后我们可以用它来创建我们的静态库文件。\n\n2.  现在我们已经创建了目标文件，我们可以使用库构建工具来创建静态库。我们使用以下命令生成一个`.lib`文件:\n\n```cpp\n    lib /out:MyLib.lib hello.obj\n```\n\n`lib`是启动构建工具的命令。`/out:MyLib.lib`告诉编译器将库版本命名为`MyLib.lib`。\n\n3.  如果我们列出目录的内容，你会看到我们现在有了我们的静态库`MyLib.lib`:\n\n![](img/f897d059-c8f4-44d0-a468-0b6cb07d8f84.png)\n\n4.  我们现在可以在其他项目中使用我们新创建的库。让我们创建一个非常简单的程序来使用我们的库:\n\n```cpp\nvoid Hello(); //Forward declaration of our Hello function \nvoid main() \n{ \n  Hello(); \n} \n```\n\n我把文件保存为`main.cpp`。\n\n这个程序将调用`Hello`函数，然后编译器在我们的链接库中寻找实现。\n\n5.  要编译这个程序并链接我们的静态库，我们可以使用以下命令:\n\n```cpp\n    cl main.cpp /link MyLib.lib\n```\n\n6.  一旦编译完成，我们的目录中就会有一个`main.exe`:\n\n![](img/55a06c90-9d02-48d8-88c0-d59f56b3f557.png)\n\n# 在 macOS X 上\n\n请遵循以下步骤:\n\n1.  对于 macOS X，您需要键入以下命令:\n\n```cpp\n    g++ -c hello.cpp \n```\n\n`g++ `是我们正在使用的开源编译器。标志`-c`告诉`g++ `输出一个目标文件。在标志之后，我们声明在构建对象文件时使用哪个 cpp 文件。该命令将产生文件`hello.o`。\n\n2.  在 macOS X 平台上，我们使用以下命令生成一个`.a`文件:\n\n```cpp\n    arrvsMylib.ahello.o\n```\n\n`ar`，archiver 的缩写，是我们用来创建静态库的库构建工具。首先我们设置几个标志，`rvs`，告诉`ar`工具如何设置库档案。然后，我们告诉工具我们正在创建的库的名称，后跟组成库的对象文件。\n\n如果我们列出目录的内容，你会看到我们现在有了我们的静态库`Mylib.a`:\n\n![](img/eae70479-600b-483e-8dd3-14d18cd1b56a.png)\n\n3.  我们现在可以在其他项目中使用我们新创建的库。让我们创建一个非常简单的程序来使用我们的库:\n\n```cpp\nvoid Hello(); //Forward declaration of our Hello function \nvoid main() \n{ \n  Hello(); \n} \n```\n\n我把文件保存为`main.cpp`。\n\n这个程序将调用`Hello`函数，然后编译器在我们的链接库中寻找实现。\n\n4.  我们编译程序并用下面的命令链接我们的静态库:\n\n```cpp\n    g++ main.cpp MyLib.a -o Main \n```\n\n一旦编译完成，我们的目录中就会有一个 Windows 上的`main.exe`，或者 macOS X 上的一个主可执行文件。\n\n![](img/42e2d0c3-520e-4c5b-8ab2-100dc2587ece.png)\n\n请注意这个适用于 Windows 和 macOS X 的可执行文件的大小。同样，由于我们是静态链接我们的库，我们实际上将库的必要部分包含在可执行文件本身中。这消除了将库与程序单独打包的需要，从而防止了库不匹配。事实上，现在库，`.lib`文件已经编译成可执行文件，我们不再需要它，可以删除它。我们的程序仍然会运行，但是如果我们想对库进行任何更改，我们必须重复前面的步骤来重新编译库，链接它，并将其添加到我们的程序构建中。\n\n# 动态链接库\n\n动态或共享库是在运行时链接其代码实现的库。这意味着一个动态库，Windows 上的`.dll`，Linux 上的`.so`，OS X 上的`.dylib`，都是可以在程序源代码中引用的库。当编译器看到这些引用时，它会在库实现中寻找链接。当程序启动时，引用的代码通过这些创建的链接包含在内。当程序使用动态库时，它只创建对代码的引用，而不创建任何代码副本。这是使用动态库的最大优势之一，因为它们只被引用，因此不会像静态库那样增加可执行文件的总大小。使用动态库的另一大优势是可维护性或修改性。由于库是在运行时包含的，您可以进行更新或修改，而不必重新编译整个程序。这对于*补丁*风格更新和允许用户自己修改来说非常棒。最大的缺点，就是我前面提到的那个。使用动态库通常需要将库和程序包含在某种包或安装程序中。这当然会导致不匹配和可怕的 Dll 地狱。\n\n对于动态或共享库，我们必须进行一些修改，并遵循稍微不同的编译和链接步骤。首先，我们必须更改库文件，让编译器知道我们希望与其他程序共享某些部分。我们这样做，在微软平台上，用`__declspec`或者声明规范。将`dllexport`参数传递给`__declspec`让编译器知道这个函数甚至类应该作为动态链接库的一部分导出。在 OS X 平台上，我们还使用一种类型的声明来让编译器知道类或函数将被导出。这里我们用`__attribute__((visibility(\"default\")))`代替`__declspec`。\n\n# 在 Windows 上编译和链接动态库\n\n以下是在 Windows 上编译和链接动态库的步骤:\n\n1.  `hello.cpp`文件现在看起来像:\n\n```cpp\n      #include <iostream> \n      __declspec(dllexport) void Hello() \n      { \n        std::cout<< \"Hello World Dynamically\" <<std::endl; \n      } \n```\n\n现在我们已经有了为导出指定的函数，我们可以将文件编译成一个动态共享的库。\n\n2.  在 Windows 上，我们可以使用以下命令从开发人员控制台提示符创建一个`.dll`:\n\n```cpp\n    cl /LD /FeMyDynamicLib.dll hello.cpp\n```\n\n再次`cl`是启动编译器和链接器的命令。`/LD`告诉编译器我们要创建一个动态链接库。`/FeMyDynamicLib.dll`设置库的名称`/Fe`为编译器选项，`MyDynamicLib.dll`为名称。最后，我们再次传入想要用来创建库的文件。\n\n3.  当编译器完成后，我们列出目录，我们现在将同时拥有`MyDynamicLib.lib`和`MyDynamicLib.dll`:\n\n![](img/ce4c0d05-71c5-48e9-97ba-0eef07fb6301.png)\n\n您可能注意到的第一件事是这个版本的`.lib`文件比之前的静态库示例小得多。这是因为实现没有存储在这个文件中。相反，它充当指向`.dll`文件中实际实现的指针。\n\n4.  接下来，我们可以用新创建的库链接并构建我们的程序，就像前面的例子一样，使用以下命令(在 Windows 上):\n\n```cpp\n    cl main.cpp /link MyDynamicLib.lib  \n```\n\n5.  所以现在如果我们运行程序，会看到显示的行`Hello World Dynamically!`:\n\n![](img/5e5c8886-ccfd-4b55-8826-5927d003cda7.png)\n\n如果我们现在列出目录，我们会注意到新的主可执行文件，像这个例子中的`.lib`文件，比以前使用静态库的版本小得多。这也是因为我们在构建时没有包含库中所需的部分。相反，我们在运行时根据需要动态加载它们:\n\n![](img/7e50cd47-2b0c-4d89-b481-e475d34959d9.png)\n\n6.  我前面提到的一个好处是，当您对动态链接库进行更改时，您不必重新编译整个程序；我们只需要重新编译库。为了看到这一点，让我们对`hello.cpp`文件做一个小的修改:\n\n```cpp\n   #include <iostream> \n   __declspec(dllexport) void Hello() \n   { \n     std::cout<< \"Hello World Dynamically!\"<<std::endl; \n     std::cout<< \"Version 2\" <<std::endl; \n   } \n```\n\n7.  接下来，我们可以使用与之前相同的命令重新编译我们的库:\n\n```cpp\n    cl /LD /FeMyDynamicLib.dll hello.cpp\n```\n\n8.  这将添加我们的新更改，我们可以看到它们生效，而无需重新编译`main.exe`，只需运行它即可。输出现在将是两行:`Hello World Dynamically!`和`Version 2`:\n\n![](img/9f57f7e6-bf83-4448-94b4-8978166b2bb4.png)\n\n这使得升级变得非常容易，但也会很快导致没有更新库的机器上的 Dll 不匹配，通常称为 Dll Hell。\n\n# 在 macOS X 上编译和链接动态库\n\n`hello.cpp`文件现在看起来像:\n\n```cpp\n#include <iostream> \n__attribute__((visibility(\"default\"))) void Hello() \n{ \n  std::cout<< \"Hello World Dynamically\" <<std::endl; \n} \n```\n\n我们可以使用以下命令从终端外壳创建一个`.dylib`:\n\n```cpp\ng++ -dynamiclib -o MyDynamicLib.dylib hello.cpp\n```\n\n这里我们使用`g++ `编译器并设置一个标志来创建一个动态库文件，`-dynamiclib`。下一个标志`-o MyDynamicLib.dylib`，告诉编译器输出文件的名称。最后，我们指定创建库时要使用的文件。如果现在列出目录，会看到新创建的`MyDynamicLib.dylib`文件:\n\n![](img/b1018c29-10de-473a-ada8-8ca306a3afea.png)\n\n接下来，我们可以用新创建的库链接并构建我们的程序，就像前面的例子一样，使用以下命令:\n\n```cpp\ng++ main.cpp MyDynamicLib.dylib -o Main\n```\n\n所以现在如果我们运行程序，会看到显示的行`Hello World Dynamically!`:\n\n![](img/fb956f82-2de2-49f2-8bad-f813baa73d30.png)\n\n如果我们现在列出目录，您会注意到新的主可执行文件，像这个例子中的`.lib`文件，比以前使用静态库的版本小得多。这也是因为我们在构建时没有包含库中所需的部分。相反，我们在运行时根据需要动态加载它们:\n\n![](img/41fa7209-5a9f-4fc2-8560-4b2f1d32b8be.png)\n\n我前面提到的一个好处是，当您对动态链接库进行更改时，您不必重新编译整个程序；我们只需要重新编译库。为了看到这一点，让我们对`hello.cpp`文件做一个小的修改:\n\n```cpp\n#include <iostream> \n__attribute__((visibility(\"default\"))) void Hello() \n{ \n  std::cout<< \"Hello World Dynamically!\"<<std::endl; \n  std::cout<< \"Version 2\" <<std::endl; \n} \n```\n\n接下来，我们可以使用与之前相同的命令重新编译我们的库:\n\n```cpp\ng++ -dynamiclib -o MyDynamicLib.dylib hello.cpp \n```\n\n前面命令的输出如下所示:\n\n![](img/43abc55f-b279-4085-bf3b-8067abe90703.png)\n\n这使得升级变得非常容易，但也会很快导致没有更新库的机器上的 Dll 不匹配，通常称为 Dll Hell。\n\n# 仅头文件或源库\n\n还有最后一种我想提到的共享库的方法，那就是简单地共享源代码或头文件实现。这是一种完全合法的共享库的方式，在开源和小型项目中非常常见。它有一个明显的好处，那就是提供修改的来源，并且可以很容易地允许消费开发者挑选他们想要在他们的项目中实现的部分。然而，这也可以被视为一个缺点，因为现在你的源代码是公开的。通过公开和自由地提供您的代码，您放弃了对其使用的大量控制，并且依赖于许可，对它实现的解决方案几乎没有或没有所有权。\n\n要将我们的小示例更改为仅头实现，我们只需将`hello.cpp`文件更改为头文件`hello.h`，并在内部执行所有函数的实现。我们新的`hello.h`文件现在将如下所示:\n\n```cpp\n#pragma once \n#include <iostream> \nvoid Hello() \n{ \n  std::cout<< \"Hello World Header!\"<<std::endl; \n} \n```\n\n然后为了使用头库，我们将像其他头文件一样将其包含在`main.cpp`文件中:\n\n```cpp\n#include \"hello.h\" \nvoid main() \n{ \n  Hello(); \n} \n```\n\n因为我们只使用头实现，所以我们不用担心在构建过程中链接库。我们可以使用以下命令从开发人员控制台提示符编译程序。\n\n在 Windows 上:\n\n```cpp\ncl main.cpp\n```\n\n编译后，您可以运行主可执行文件并看到类似的 hello world 消息，`Hello World Header!`:\n\n![](img/ea937f03-5695-4bb1-8c9b-99b53d29a764.png)\n\n在 macOS X 上:\n\n```cpp\ng++ main.cpp -o Main\n```\n\n编译后，您可以运行主可执行文件并看到类似的 hello world 消息，`Hello World Header!`:\n\n![](img/2f6cfc05-c84b-42f4-aacf-35f27b59af36.png)\n\n# 构建自定义共享库\n\n能够创建自己的自定义库是一项非常有价值的技能。对创建、构建和使用库所需的步骤有一个深刻的理解，将允许您创建更强大的系统和解决方案。在下一节中，我们将深入探讨如何在托管开发环境中创建、构建和使用一个可共享的库项目。\n\n# 设置和结构\n\n对于这个例子，我将坚持使用 Visual Studio for Windows，XCode for macOS X。虽然每个开发环境中的一些确切细节会有所不同，但推断步骤应该不会太困难。你可以在代码库的`Chapter02`文件夹中找到这个例子的完整源代码。\n\n首先，我们将创建一个新项目。\n\n# 在 Windows 上创建新项目\n\n在 Windows 上，我们可以这样做:转到文件|新建|项目，然后展开 Visual C++ 下拉列表，最后选择 Win32 控制台应用。我给我的新项目命名为`MemoryMgr`:\n\n![](img/9cfc23a9-839d-4a3f-a4ee-33bf3e4c5572.png)\n\n选择确定后，将弹出 Win32 应用向导对话框。单击下一步将对话框移至下一页:\n\n![](img/88990b87-f00e-4255-a007-ff58e426161e.png)\n\n在这个对话框页面上，我们看到了一些不同的应用设置。对于我们的应用类型，我们将选择动态链接库。这将创建一个`.dll`和附带的`.lib`文件，然后我们可以共享和消费。我们选择动态或共享库而不是静态库的原因是因为我可以演示如何构建和编译一个可共享库。这是一个简单的内存管理器库，在大多数情况下会包含在一套其他实用程序库中。我们可以很容易地将这个库修改为静态的，请参阅前面的部分了解如何修改。\n\n选择空项目的选项，这将为我们提供一个完全空白的项目，我们可以从中构建我们的库。这也将灰显大多数其他选项，例如附加选项中的预编译头。这是一个常用的选项，通过在单个头文件中调用所有或最需要的头文件来帮助加快大型项目的编译，然后将该头文件作为单个头文件添加到其他实现文件中。您可以选择安全开发生命周期(SDL)检查，因为它不会导致任何问题。单击“完成”退出对话框并打开新项目:\n\n![](img/2024de63-431e-4619-b6b7-4229a5f99c87.png)\n\n一旦项目被加载，我们会看到一个空白的编辑器窗口和一个空的解决方案资源管理器。\n\n# 在 macOS X 上创建新项目\n\n我们通过转到文件|新建|项目，然后从平台列表中选择 OS X，然后从模板选项中选择库来创建新项目:\n\n![](img/b0dad365-da6e-4ee8-a462-52a233b808d1.png)\n\n单击“下一步”后，将出现一个包含项目设置选项的对话框。这些选项包括产品名称，我选择`MemoryMgr`作为产品名称，组织名称和组织标识符，我将它们作为默认选项。在生产环境中，您可能希望调整这些设置以匹配您的项目。最后两个选项是框架和类型。对于框架，选择 STL (C++ 库)这是在使用将包括对 STL 的访问的库时使用的模板。对于“类型选择动态”，还有静态库项目的选项:\n\n![](img/d982505a-7bde-41da-a7e1-b3ae121f3cc4.png)\n\n我们的下一步是创建库所需的源文件。在这个例子中，我们将只创建一个由单个头文件`.h`和实现文件`.cpp`组成的类。\n\n# 在 Windows 上创建源文件\n\n我们可以在 Visual Studio 中使用添加|类快速添加这个类...对话。\n\n右键单击解决方案资源管理器中的内存组项目；从菜单列表中导航到添加|类:\n\n![](img/a9af827e-b124-4e51-9211-9244e4078f8a.png)\n\n将弹出一个新屏幕，其中有几个用于创建新类的选项；我们将使用默认的通用 C++ 类选项。\n\n选择添加进入下一个对话框屏幕。我们现在在通用 C++ 类向导屏幕上。在类名称部分，添加您正在创建的新类的名称，在我的例子中，我称之为`MemoryMgr`。当您输入类名时，向导将自动填充。h 文件和。cpp 文件。由于这不是一个继承的类，我们可以将基类部分留空。我们将把 Access 保留为公共的默认设置，最后我们将取消选中虚拟析构函数和内联选项。\n\n单击“完成”将该类添加到我们的项目中:\n\n![](img/61f33ca0-b7bd-4717-ac25-ba639fb1815c.png)\n\n当然，这与我们简单地键入完整的导出说明符完全相同，如下所示:\n\n```cpp\n__declspec(dllexport) int n; //Exporting a variable \n__declspec(dllexport) intfnMemoryMgr(void); //Exporting a function \n```\n\n# 在 macOS X 上创建源文件\n\n默认情况下，这一步已经为我们完成。项目创建向导自动包括一个实现文件`.cpp`和一个头文件，但是在这种情况下头文件的扩展名是`.hpp`。自动创建的文件也有一堆存根代码来帮助事情开始。在我们的例子中，为了使事情更加连贯，我们将删除这个存根代码并删除两个`.hpp`文件。相反，我们将创建一个新的`.h`文件并插入我们自己的代码。创建一个新的`.h`文件很简单，导航到文件|新建|文件。在新建文件对话框中，从左侧的平台列表中选择 OS X，从类型选择窗口中选择头文件:\n\n![](img/e433128c-2c31-4a14-8277-9ab9bab8eaec.png)\n\n点击下一步按钮将弹出文件保存对话框。将文件保存为`MemoryMgr.h`，注意我指定了`.h`作为扩展名。如果不指定扩展名，向导将默认为`.hpp`。同样值得注意的是，确保在对话框的底部选择了目标项目，这将确保它被算作 XCode 项目解决方案的一部分。\n\n![](img/f48dc14e-33c7-458b-a7ef-a0aa29452e86.png)\n\n您的项目布局现在应该如下所示:\n\n![](img/a3ee73e6-8992-42f3-bec3-939aaf32eb7a.png)\n\n现在是时候编码了。我们将从`MemoryMgr`头文件`MemoryMgr.h`开始。在这个文件中，我们将声明我们将使用的所有函数和变量，以及提供对我们的动态库的访问的定义。这里是`MemoryMgr.h`为了简洁起见，删除了注释:\n\n```cpp\n#ifdef MEMORYMGR_EXPORTS \n#ifdef _WIN32 \n#define EXPORT __declspec(dllexport) \n#else \n#define EXPORT __declspec(dllimport) \n#elif __APPLE__ \n#define EXPORT __attribute__((visibility(\"default\"))) \n#endif \n#endif \n```\n\nThe full file contents are available in the code repository in the `Chapter02` folder.\n\n当创建新的动态库时，我们采取的第一步是一个有用的快捷方式，它允许我们节省一些击键，并简单地创建导出的类、函数或变量。使用`ifdef`指令，我们可以首先为内存管理器`MEMORYMGR_EXPORTS`创建一个标识符，然后为目标平台`_WIN32`创建一个标识符，为 Windows 创建一个标识符，`__APPLE__`创建一个标识符，为 macOS X 创建一个标识符。在每个平台的`ifdef`指令中，我们可以为宏`EXPORT`添加定义，在 Windows 上，这些是为`dllexport`和`dllimport`创建的。这是使用宏来简化导出和导入过程的标准方式。有了这些宏，任何包含这个文件的项目都将看到公开的函数被导入，而动态库将看到用这个宏定义的任何东西被导出。这意味着我们现在可以简单地使用`EXPORT`来代替`_declspec(dllexport)`或`__attribute__((visibility(\"default\")))`来指定动态库中应该提供给其他人的内容。\n\n创建内存管理器的下一步是为我们的`Block`和`Heap`对象创建一对`struct`。块是我们将存储单个对象的内存片或块。`Heap`是包含在记忆连续容器中的这些`Block`的集合。`Block`结构只是保存一个指向下一个`Block`指针的指针；这将为每个`Heap`中的`Block`对象创建一个链接列表。`Heap`结构还保存了一个指向内存中下一个`Heap`的指针，这又为`Heap`对象创建了一个链表。`Heap`结构还包含一个小助手函数，返回`Heap`中的下一个块:\n\n```cpp\nstruct Block \n{ \n  Block* next; \n}; \n\nstruct Heap \n{ \n  Heap* next; \n  Block* block() \n  { \n    return reinterpret_cast<Block*>(this + 1); \n  } \n}; \n```\n\n现在我们已经有了我们的`Heap`和`Block`结构，我们可以继续定义实际的内存管理器类，`CMemoryMgr`。这就是我们之前创建的定义派上用场的地方。在这种情况下，我们使用`EXPORT`来指定我们希望将整个类导出到我们的动态库中。当我们以这种方式导出类时，类访问与任何其他类完全一样。这意味着所有的`private`、`protected`和`public`对象继续具有相同的访问权限。\n\n```cpp\nclass EXPORT CMemoryMgr \n```\n\n虽然在我们的简单示例中，导出整个类是有意义的，但情况可能并非总是如此。如果我们想导出一个函数或变量，我们可以这样创建`EXPORT`宏:\n\n```cpp\nEXPORT int n; //Exporting a variable \nEXPORT void fnMemoryMgr(void); //Exporting a function \n```\n\n当然，这与我们简单地键入完整的导出说明符(在 macOS X 上)是完全一样的:\n\n```cpp\n__attribute__((visibility(\"default\"))) int n; //Exporting a \n variable__attribute__((visibility(\"default\"))) intfnMemoryMgr(void); \n //Exporting a function\n```\n\n关于`MemoryMgr`文件的更多信息:\n\n现在我们知道了如何导出类、函数和变量，让我们继续快速查看`MemoryMgr`头文件的其余部分。首先，我们定义了调用库时可用的公共方法。这些包括构造函数，它接受三个参数；`dataSize`，每个块的对象大小，`heapSize`，每个内存堆的大小，`memoryAlignmentSize`，这是我们用来移动内存中对象的变量。\n\n移动内存中的对象意味着我们将总是使用固定数量的内存来保存对象，无论其大小如何。我们这样做是为了使对象对齐，从而减少对实际内存硬件的调用量，这当然会提高性能。这通常是开发人员使用自定义内存管理器的主要原因。\n\n接下来，我们有一个没有参数的析构函数，后面是`Allocate`、`Deallocate`和`DeallocateAll`，它们完全按照它们的名字所暗示的那样运行。唯一带参数的函数是`Deallocate`函数，它带一个指向你想删除的内存的指针:\n\n```cpp\nclass EXPORT CMemoryMgr \n{ \npublic: \n  CMemoryMgr(unsigned int dataSize, unsigned int heapSize, unsigned int \n             memoryAlignmentSize); \n  ~CMemoryMgr(); \n  void* Allocate(); \n  void Deallocate(void* pointerToMemory); \n  void DeallocateAll(); \n```\n\n这些函数是通过我们的库公开的唯一函数，在这个简单的例子中，可以认为是这个库的基本实现接口。\n\n当然，在公共声明到来之后，我们的库需要私有声明。它们以三个静态常数开始，这三个静态常数保存了我们将使用的简单十六进制模式。这将帮助我们在调试时识别每个内存段，并提供一个简单的机制来检查我们是否在正确的时间处理正确的内存段:\n\n```cpp\nprivate: \n  static const unsigned char ALLOCATION_PATTERN = 0xBEEF; \n  static const unsigned char ALIGNMENT_PATTERN = 0xBADD; \n  static const unsigned char FREE_MEMORY_PATTERN = 0xF00D; \n```\n\n然后我们有了我们在库里用来举重的方法。助手功能`GetNextBlock`将返回`Heap`中的下一个链接`block`。`OverWriteHeap`函数接受一个指向堆的指针，该指针将写入特定的`Heap`。`OverWriteBlock`获取指向要写入的块的指针，`OverWriteAllocated`再次获取分配用于重写的`Block`指针:\n\n```cpp\nBlock* GetNextBlock(Block* block); \nvoid OverWriteHeap(Heap* heapPointer); \nvoid OverWriteBlock(Block* blockPointer); \nvoid OverWriteAllocatedBlock(Block* blockPointer); \n```\n\n在`private`方法之后，我们有了成员变量，它们将存储我们的内存管理器库所需的各种类型的数据。前两个是指针列表，我们用它们来保存我们创建的堆和可用的空闲块:\n\n```cpp\nHeap* m_heapList = nullptr; \nBlock* m_freeBlockList = nullptr; \n```\n\n最后，我们有一组保存各种数据的无符号整数。由于变量的名称很容易解释，我就不一一赘述了:\n\n```cpp\n unsigned int m_dataSize; \n unsigned int m_heapSize; \n unsigned int m_memoryAlignment; \n unsigned int m_blockSize; \n unsigned int m_blocksPerHeap; \n unsigned int m_numOfHeaps; \n unsigned int m_numOfBlocks; \n unsigned int m_numOfBlocksFree; \n}; \n```\n\n现在，在我们的实现文件(`MemoryMgr.cpp`)中，因为在这个例子中我们导出了整个类，所以我们不需要包含任何特殊的内容，所有公开访问的内容对于使用我们库的任何项目都是可用的。如果我们决定只导出选定的函数和变量，而不是整个类，我们将不得不使用我们创建的`EXPORT`宏来指定它们应该在我们的库中导出。为此，您可以简单地在实现前面添加`EXPORT`如下:\n\n```cpp\n// This is an example of an exported variable \nEXPORT int nMemoryMgr=0; \n// This is an example of an exported function. \nEXPORT int fnMemoryMgr(void) \n{ \n  return 42; \n} \n```\n\n为了节省这里的时间和空间，我不打算一一介绍`MemoryMgr.cpp`实现的每一行。该文件有很好的文档记录，应该能很好地解释内存管理器的简单机制。尽管它很简单，但这个库是构建更健壮的内存管理器系统以适应任何项目的特定需求的一个很好的起点。\n\n# 构建自定义库\n\n在您或其他任何人可以使用您的自定义库之前，您需要构建它。有几种不同的方法可以实现这一点。\n\n# 在窗口上\n\n在前一节的示例中，我们使用了 Visual Studio 2015，在这种情况下，构建库非常简单。例如，要构建`MemoryMgr`库，您可以在解决方案资源管理器中右键单击解决方案“MemoryMgr”并选择“构建解决方案”，或者使用键盘快捷键*Ctrl*+*Shift*+*B*:\n\n![](img/c180a9a9-4847-445b-a612-389e9621c9a2.png)\n\n这将在调试或发布下的项目输出文件夹中创建所需的`MemoryMgr.dll`和`MemoryMgr.lib`文件，具体取决于所选的构建设置。我们构建库的另一种方法是使用我们在本章第一部分讨论的开发人员命令行工具。在这种情况下，我们可以简单地将目录更改为项目文件，并运行包含库名和输入文件的`cl`命令:\n\n```cpp\ncl /LD /FeMemoryMgr.dll MemoryMgr.cpp\n```\n\n这将再次创建在其他项目中使用我们的库所需的`MemoryMgr.dll`和`MemoryMgr.lib`文件。\n\n# 在 macOS X 上\n\n构建一个 XCode 库项目非常容易。您可以简单地从工具栏中选择产品，然后单击构建，或者使用键盘快捷键命令+ *B* :\n\n![](img/47c237e7-bf12-4a29-a504-48ee49020189.png)\n\n这将创建`MemoryMgr.dylib`文件，当在其他项目中包含该库时，我们将需要该文件。我们构建库的另一种方法是使用我们在本章前面看到的终端外壳。在这种情况下，我们可以简单地将目录更改为项目文件，并运行包含库名和输入文件的`g++ `:\n\n```cpp\ng++ -dynamiclib -o MemoryMgr.dylib MemoryMgr.cpp\n```\n\n# 使用在 Windows 上构建动态库。def 文件\n\n我们将探索仅使用`.def`文件或使用链接器选项来构建动态库的选项。\n\n# 仅使用。def 文件\n\n还有一种方法我想提一下，我们可以用来构建我们的动态库，那就是使用`.def`文件。模块定义或`.def`文件是包含描述动态库导出属性的模块语句的文本文件。使用`.def`文件，您不需要创建任何宏或使用`__declspec(dllexport)`说明符来导出 DLL 的函数。对于我们的`MemoryMgr`示例，我们可以通过打开文本编辑器并添加以下内容来创建`.def`文件:\n\n```cpp\nLIBRARY MEMORYMGR \nEXPORTS \n  Allocate      @1 \n  Deallocate    @2 \n  DeallocateAll @3 \n```\n\n这将告诉编译器我们希望导出这三个函数:`Allocate`、`Deallocate`和`DeallocateAll`。将文件保存为`.def`文件；我称我的为`MemoryMgr.def`。\n\n在我们可以使用模块定义文件重新编译库之前，我们必须对`MemoryMgr`的源代码进行一些更改。首先，我们可以删除我们创建的宏，并删除`CMemoryMgr`类定义之前的`EXPORT`。我们之前创建的`.def`文件将告诉编译器应该导出什么，而不需要宏或`_declspec(dllexport)`说明符。\n\n要在 Windows 平台上使用模块定义文件编译动态库，我们有几个选项。我们可以使用开发人员控制台编译库，就像我们之前做的那样，但是有一个额外的选项来指定`.def`文件。从控制台编译`MemoryMgr`库的命令如下所示:\n\n```cpp\n cl /LD /DEF:MemoryMgr.def /FeMemoryMgr2.dll MemoryMgr.cpp\n```\n\n`/DEF:filename`是告诉编译器使用指定的模块定义文件来构建库的标志。该命令将产生一个名为`MemoryMgr2.dll`的动态库。\n\n# 设置链接器选项\n\n我们必须使用`.def`文件构建动态库的第二个选项是通过在 Visual Studio 开发环境中设置链接器选项。这样做相当简单。\n\n首先，我们通过在解决方案资源管理器中右键单击项目名称或使用键盘快捷键 *Alt* + *进入*并突出显示项目来打开属性页对话框。打开属性页对话框，选择链接器，点击输入属性页，最后在模块定义文件属性中输入`.def`文件的名称。最终结果应该如下所示:\n\n![](img/0b7448bd-48ac-4dd6-baef-0e3faa530d7e.png)\n\n现在，当您构建动态库项目时，编译器将使用`MemoryMgr.def`文件来确定应该导出哪些属性。\n\n接下来，我们将研究在使用 Visual Studio 和 XCode 项目时，如何使用这个库和其他库。\n\n# 共享和消费库\n\n现在我们已经构建了自定义库，我们可以开始在其他项目中使用它。正如我们在本章前面看到的，我们可以使用命令行编译器工具链接动态和静态库。如果您只有几个库，或者可能已经创建了自定义构建脚本，这是可以的，但是在大多数情况下，当使用像 Visual Studio 这样的 IDE 时，有更简单的方法来管理。事实上，在 Visual Studio 中将库添加到项目中非常容易。要先添加库，我们再次打开属性页对话框，右键单击并转到属性或 *Alt* + *进入*，在解决方案资源管理器中选择项目。接下来，展开链接器并选择输入。在对话框顶部的“附加依赖项”属性上，单击下拉列表并选择“编辑”。这将弹出一个类似于此处所示的对话框:\n\n![](img/c3702c26-2d32-464b-be0f-974942428511.png)\n\n在这个对话框的属性窗口中，我们可以指定我们希望在编译时包含的库。无论是动态库还是静态库，我们都会包含`.lib`文件。如果您已经在“配置属性”下的“VC++ 目录”文件夹中设置了库目录，您可以简单地使用库名称，如下所示:`MemoryMgr.lib`。您也可以通过指定库的路径来包含库，如`C:\\project\\lib\\MemoryMgr.lib`。此属性还接受宏，这对于使用很重要，因为将项目移动到另一个目录会破坏 include。您可以使用的一些宏有:\n\n*   `$(SolutionDir)`:这是顶级解决方案目录\n*   `$(SourceDir)`:这是项目来源的目录\n*   `$(Platform)`:这是选择的平台(Win32、x64 或 ARM)\n*   `$(Configuration)`:这是选择的配置(调试或发布)\n\n这意味着，如果我在位于解决方案目录中的名为`lib`的文件夹中有几个用于每个平台和配置的库，我可以通过使用这样的宏来为自己节省大量工作:\n\n```cpp\n$(SolutionDir)/lib/$(Platform)/$(Configuration)/MemoryMgr.lib \n```\n\n现在，如果我切换平台或配置，我不必每次都回到属性页进行更改。\n\n这负责链接库，但是在消费或共享库时还需要一个。在本章的第一组示例中，您一定注意到了，在创建小控制台程序来演示库的使用时，我使用了一个 forward 声明来指定从库中实现`Hello`函数。\n\n```cpp\nvoid Hello(); //Forward declaration of our Hello function \n```\n\n虽然这在像这个这样的小例子中是可行的，但是如果您使用的是具有多个属性的库，那么正向声明将变得相当乏味。为了在您的项目中使用库，您通常必须包含定义文件、标题。这就是为什么当你看到共享的库时，它们通常会有一个`Include`文件夹，其中包含了使用该库所需的所有头文件。就我们的`MemoryMgr`库而言，这意味着如果我想在一个新项目中使用它或者与另一个开发人员共享它，我会包含三个文件。`MemoryMgr.dll`库，其实是一个动态的库。`MemoryMgr.lib`库，是用于链接的库文件。最后，我还需要包含`MemoryMgr.h`文件，该文件包含了我的库的所有属性定义。\n\n由于您将使用的大多数库都有不止一个头文件，简单地将它们复制到项目中可能会很麻烦。好消息是，像大多数 IDEs 一样，Visual Studio 具有配置设置，允许您指定哪些文件夹包含您希望包含在项目中的文件。设置这些配置选项也很简单。首先，打开属性页对话框， *Alt* + *进入*，项目在解决方案资源管理器中高亮显示。\n\n接下来，单击 C/C++ 文件夹将其展开。然后选择常规部分。在顶部的属性窗口中，您将看到附加包含目录，从该属性中选择下拉列表，然后单击编辑。这将弹出一个类似于此处所示的对话框:\n\n![](img/a5f043fa-4637-46db-8bed-5141be2c15d7.png)\n\n在这个对话框窗口中，我们可以通过点击添加文件夹图标，或者使用快捷键 *Ctrl* + *插入*来添加新行。您可以使用“文件夹资源管理器”对话框来查找和选择需要包含的文件夹，但是该属性也支持宏，因此指定所需包含文件夹的更好方法是使用宏。如果我们在主解决方案目录中有一个名为 Include 的文件夹，其中有一个名为`MemoryMgr`的文件夹，我们可以使用以下宏来包含该文件夹:\n\n```cpp\n$(SolutionDir)Include\\MemoryMgr\\\n```\n\n选择“确定”和“应用”关闭“属性页”对话框后，您可以像项目中的任何其他头文件一样包含头文件。对于我们的`MemoryMgr`文件夹，我们将使用以下代码:\n\n```cpp\n#include<MemoryMgr\\MemoryMgr.h>;\n```\n\n请注意，文件系统层次结构是受尊重的。\n\n# 摘要\n\n在本章中，我们讨论了共享库的高级主题。我们查看了不同类型的可用库。我们介绍了创建自己的共享库的各种方法。\n\n在下一章中，我们将使用这些高级库知识来构建素材管理管道。"
  },
  {
    "path": "docs/master-cpp-game-dev/03.md",
    "content": "# 三、夯实基础\n\n虽然从头开始构建自己的库可能是一个有益的过程，但它也可能很快变成一个耗时的过程。这就是为什么大多数专业游戏开发人员依赖一些公共库来加快开发速度，更重要的是，提供一个专门的、高性能的实现。通过连接这些公共库并构建抽象这些库的助手和管理器类，您实际上是在构建最终将为您的工具和游戏引擎提供动力的结构。\n\n在接下来的几节中，我们将介绍这些库如何协同工作，并构建完善结构所需的一些库，为我们在本书剩余部分扩展演示打下坚实的基础。\n\n首先，我们将关注渲染系统，这可以说是任何游戏项目最重要的方面之一。适当的、高性能的实现不仅需要大量的时间，还需要视频驱动程序实现和计算机图形数学方面的专业知识。话虽如此，事实上，自己创建一个定制的低级图形库并不是不可能的，只是如果你的最终目标只是制作视频游戏，不建议过度使用。因此，大多数开发人员不会自己创建一个低级实现，而是转向几个不同的库，为他们提供对图形设备裸机的抽象访问。\n\n对于贯穿本书的例子，我们将使用一些不同的图形 API 来帮助加速这个过程，并帮助提供跨平台的一致性。这些应用编程接口包括:\n\n*   **OpenGL**([https://www.opengl.org/](https://www.opengl.org/)):**开放图形库** ( **OGL** )是一个开放的跨语言、跨平台的应用编程接口，即 API，用于渲染 2D 和 3D 图形。该应用编程接口提供对**图形处理单元** ( **图形处理器**)的低级访问。\n*   **SDL**([https://www.libsdl.org/](https://www.libsdl.org/)):**简易直播媒体层** ( **SDL** )是一个跨平台的软件开发库，旨在为多媒体硬件组件提供一个低级别的硬件抽象层。虽然它确实提供了自己的渲染机制，但 SDL 可以使用 OGL 来提供完整的 3D 渲染支持。\n\n虽然这些 API 在使用图形硬件时为我们提供了一些抽象，从而节省了我们的时间和精力，但很快就会发现抽象级别不够高。\n\n您将需要另一个抽象层来创建在多个项目中重用这些 API 的有效方式。这就是助手类和管理类的作用。这些类将为我们和其他程序员提供所需的结构和抽象。它们将包装设置和初始化库和硬件所需的所有公共代码。任何项目所需要的代码，无论是游戏性还是类型，都可以封装在这些类中，并将成为引擎的一部分。\n\n在本章中，我们将涵盖以下主题:\n\n*   构建助手类\n*   用管理器封装\n*   创建接口\n\n# 构建助手类\n\n在面向对象编程中，助手类用于帮助提供一些功能，而这些功能并不是使用它的应用的主要目标。助手类有多种形式，通常是提供方法或类当前范围之外的功能的类的总称。许多不同的编程模式都使用助手类。在我们的例子中，我们也将大量使用助手类。这里只是一个例子。\n\n让我们看一下创建窗口的一组非常常见的步骤。可以肯定地说，您将创建的大多数游戏都将有某种显示，并且通常在不同的目标上是典型的，在我们的例子中是 Windows 和 macOS。不得不为每个新项目不断地反复输入相同的说明似乎是一种浪费。这种情况非常适合在助手类中抽象出来，最终成为引擎本身的一部分。以下代码是演示代码示例中包含的`Window`类的标题，您可以在 GitHub 存储库的`Chapter03`文件夹下找到完整的源代码。\n\n首先，我们有几个必要的包含，`SDL`，`glew`是一个窗口创建辅助库，最后，包含标准的`string`类:\n\n```cpp\n#pragma once \n#include <SDL/SDL.h> \n#include <GL/glew.h> \n#include <string> \n```\n\n接下来，我们有一个`enum WindowFlags`。我们用它来设置一些位操作，以改变窗口的显示方式；不可见、全屏或无边界。您会注意到，我已经将代码包装在命名空间`BookEngine`中，正如我在上一章中提到的，这对于防止命名冲突的发生至关重要，一旦我们开始将引擎导入项目，这将非常有帮助:\n\n```cpp\nnamespace BookEngine\n{ \n  enum WindowFlags //Used for bitwise passing  \n  { \n    INVISIBLE = 0x1, \n    FULLSCREEN = 0x2, \n    BORDERLESS = 0x4 \n  }; \n```\n\n现在我们有了`Window`类本身。我们这个班有几个`public`方法。首先是默认构造函数和析构函数。即使缺省构造函数和析构函数是空的，也要包含它们，这是一个好主意，如这里所示，尽管有编译器，包括它自己的编译器，但是如果您计划创建类的智能或托管指针，如`unique_ptr`，这些指定的是需要的:\n\n```cpp\nclass Window \n  { \n  public: \n    Window(); \n    ~Window(); \n```\n\n接下来我们有`Create`函数，这个函数将是构建或创建窗口的函数。创建窗口需要一些参数，如窗口名称、屏幕宽度和高度，以及我们想要设置的任何标志，参见前面提到的`enum`:\n\n```cpp\nint Create(std::string windowName, int screenWidth, int \nscreenHeight, unsigned int currentFlags);\n```\n\n那么我们有两个`Get`功能。这些函数将分别返回宽度和高度:\n\n```cpp\nint GetScreenWidth() { return m_screenWidth; } \nint GetScreenHeight() { return m_screenHeight; } \n```\n\n最后一个公共功能是`SwapBuffer`功能；这是一个重要的功能，我们将很快深入了解。\n\n```cpp\nvoid SwapBuffer(); \n```\n\n为了结束类定义，我们有几个私有变量。第一个是指向一个`SDL_Window*`类型的指针，命名为足够合适的`m_SDL_Window`。然后我们有两个 holder 变量来存储屏幕的宽度和高度。这照顾到了新的`Window`类的定义，正如你所看到的，它在面值上非常简单。它提供了对窗口创建的简单访问，而无需开发人员调用它来了解实现的确切细节，这是面向对象编程的一个方面，并且这个方法非常强大:\n\n```cpp\nprivate: \n    SDL_Window* m_SDL_Window; \n    int m_screenWidth; \n    int m_screenHeight; \n  }; \n} \n```\n\n为了获得真正的抽象感，让我们遍历`Window`类的实现，并真正看到创建窗口本身所需的所有部分:\n\n```cpp\n#include \"\"Window.h\"\" \n#include \"\"Exception.h\"\" \n#include \"\"Logger.h\"\" \nnamespace BookEngine \n{ \n  Window::Window() \n  { \n  } \n  Window::~Window() \n  { \n  } \n```\n\n`Window.cpp`文件从需要开始包括，当然我们需要包括`Window.h`，但是你也会注意到我们也需要包括`Exception.h`和`Logger.h`头文件。这是另外两个帮助文件，创建它们是为了抽象它们自己的进程。`Exception.h`文件是一个助手类，提供了一个易于使用的异常处理系统。`Logger.h`文件是一个助手类，顾名思义，它提供了一个易于使用的日志记录系统。随意翻看每一个；代码位于 GitHub 代码库的`Chapter03`文件夹中。\n\n在 includes 之后，我们再次将代码包装在`BookEngine`命名空间中，并为类提供空的构造函数和析构函数。\n\n`Create`功能首先实现。在这个函数中有创建实际窗口所需的步骤。它开始设置窗口显示`flags`，使用一系列`if`语句为窗口创建选项的按位表示。我们使用之前创建的`enum`来让我们人类更容易阅读。\n\n```cpp\n  int Window::Create(std::string windowName, int screenWidth, int \n screenHeight, unsigned int currentFlags) \n  { \n    Uint32 flags = SDL_WINDOW_OPENGL; \n    if (currentFlags & INVISIBLE) \n    { \n      flags |= SDL_WINDOW_HIDDEN; \n    } \n    if (currentFlags & FULLSCREEN) \n    { \n      flags |= SDL_WINDOW_FULLSCREEN_DESKTOP; \n    } \n    if (currentFlags & BORDERLESS) \n    { \n      flags |= SDL_WINDOW_BORDERLESS; \n    } \n```\n\n在我们设置了窗口的显示选项之后，我们继续使用 SDL 库来创建窗口。正如我之前提到的，我们使用像 SDL 这样的库来帮助我们简化这种结构的创建。我们开始用`try`语句包装这些函数调用；这将允许我们捕捉任何问题，并将其传递给我们的`Exception`类，我们很快就会看到:\n\n```cpp\ntry { \n      //Open an SDL window \n      m_SDL_Window = SDL_CreateWindow(windowName.c_str(), \n              SDL_WINDOWPOS_CENTERED, \n              SDL_WINDOWPOS_CENTERED, \n              screenWidth, \n              screenHeight, \n              flags); \n```\n\n第一行使用传入的变量将私有成员变量`m_SDL_Window`设置为新创建的窗口，用于名称、宽度、高度和任何标志。我们还通过将`SDL_WINDOWPOS_CENTERED`定义传递给函数，将默认窗口的种子点设置为屏幕中心:\n\n```cpp\nif (m_SDL_Window == nullptr) \n    throw Exception(\"\"SDL Window could not be created!\"\"); \n```\n\n在我们尝试创建窗口之后，最好检查一下这个过程是否成功。我们用一个简单的 if 语句来完成，并检查变量`m_SDL_Window`是否设置为`nullptr`；如果是，我们扔一个`Exception`。我们经过`Exception`那根绳子`\"\"SDL Window could not be created!\"\"`。这是我们可以在 catch 语句中打印出来的错误消息。稍后，我们将看到一个这样的例子。使用这种方法，我们为自己提供了一些简单的错误检查。\n\n一旦我们创建了窗口并完成了一些错误检查，我们就可以继续设置一些其他组件了。其中一个组件是 OGL 库，它需要设置所谓的上下文。OGL 上下文可以被认为是描述与应用呈现相关的所有细节的一组状态。在绘制任何图形之前，必须设置 OGL 上下文。\n\n一个问题是，创建一个窗口和一个 OGL 上下文并不是 OGL 规范本身的一部分。这意味着每个平台可以以不同的方式处理这个问题。对我们来说幸运的是，SDL API 再次为我们抽象了繁重的工作，并允许我们在一行代码中完成这一切。我们创建了一个名为`glContext`的`SDL_GLContext`变量。然后我们将`glContext`赋给`SDL_GL_CreateContext`函数的返回值，该函数接受一个参数，也就是我们之前创建的`SDL_Window`。在此之后，我们当然会做一个简单的检查，以确保一切都按计划进行，就像我们之前创建窗口时所做的那样:\n\n```cpp\n//Set up our OpenGL context \nSDL_GLContext glContext = SDL_GL_CreateContext(m_SDL_Window); \n   if (glContext == nullptr) \n     throw Exception(\"\"SDL_GL context could not be created!\"\"); \n```\n\n我们需要初始化的下一个组件是`GLEW`。这又一次被我们抽象为一个简单的命令，`glewInit()`。该函数不接受参数，但返回一个错误状态代码。我们可以使用这个状态代码来执行类似的错误检查，就像我们对窗口和 OGL 所做的那样。这次改为对照定义的`GLEW_OK`进行检查。如果评估结果不是`GLEW_OK`，我们扔出一个`Exception`稍后被抓。\n\n```cpp\n//Set up GLEW (optional) \nGLenum error = glewInit(); \n  if (error != GLEW_OK) \n    throw Exception(\"\"Could not initialize glew!\"\"); \n```\n\n现在，所需的组件已经初始化，现在是记录运行应用的设备的一些信息的好时机。您可以记录关于设备的各种数据，这些数据可以在试图跟踪模糊问题时提供有价值的见解。在这种情况下，我正在系统中轮询运行应用的 OGL 版本，然后使用`Logger`助手类将其打印到运行时文本文件中:\n\n```cpp\n//print some log info \nstd::string versionNumber = (const \nchar*)glGetString(GL_VERSION);      \nWriteLog(LogType::RUN, \"\"*** OpenGL Version: \"\" + \nversionNumber + \"\"***\"\");\n```\n\n现在我们设置清晰的颜色或用于刷新显卡的颜色。在这种情况下，它将是我们应用的背景色。`glClearColor`函数取四个浮点值，代表从`0.0`到`1.0`范围内的红、绿、蓝和阿尔法值。Alpha 是透明值，其中`1.0f`不透明，`0.0f`完全透明:\n\n```cpp\n//Set the background color to blue \nglClearColor(0.0f, 0.0f, 1.0f, 1.0f); \n```\n\n下一行设置`VSYNC`值，这是一种尝试将应用的帧率与物理显示器的帧率相匹配的机制。`SDL_GL_SetSwapInterval`函数接受一个参数，一个可以是开的`1`或关的`0`的整数:\n\n```cpp\n//Enable VSYNC \nSDL_GL_SetSwapInterval(1);\n```\n\n组成`try`语句块的最后两行，启用混合并设置执行 alpha 混合时使用的方法。有关这些特定功能的更多信息，请查看 OGL 发展文档:\n\n```cpp\n //Enable alpha blend \n glEnable(GL_BLEND); \n glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); \n} \n```\n\n在我们的`try`区块之后，我们现在必须包含一个或多个`catch`区块。这是我们将捕获任何已经发生的抛出错误的地方。在我们的例子中，我们只需要抓住所有的异常。我们使用`Logger`助手类中的`WriteLog`函数将异常消息`e.reason`添加到错误日志文本文件中。这是一个非常基本的情况，但是当然，我们可以在这里做得更多，如果可能的话，甚至可能从错误中恢复过来:\n\n```cpp\ncatch (Exception e) \n { \n    //Write Log \n    WriteLog(LogType::ERROR, e.reason); \n  } \n  } \n```\n\n最后，`Window.cpp`文件中的最后一个函数是`SwapBuffer`函数。在不太深入实现的情况下，交换缓冲区所做的是交换 GPU 的前后缓冲区。简而言之，这使得屏幕绘制更加流畅。这是一个复杂的过程，再次被 SDL 库抽象化。我们的`SwapBuffer`函数再次抽象了这个过程，因此当我们想要交换缓冲区时，我们只需调用`SwapBuffer`，而不必调用 SDL 函数并指定窗口，这正是函数中所做的:\n\n```cpp\nvoid Window::SwapBuffer() \n { \n   SDL_GL_SwapWindow(m_SDL_Window); \n } \n} \n```\n\n如您所见，构建这些助手函数可以让开发和迭代过程变得更快、更简单。接下来，我们将看看另一种编程方法，它再次从开发人员手中抽象出繁重的工作，并提供一种对流程的控制形式，即管理系统。\n\n# 用管理器封装\n\n在处理输入和音频系统等复杂系统时，直接控制和检查系统的每个状态和其他内部组件很容易变得繁琐和笨拙。这就是经理编程模式的想法。使用抽象和多态性，我们可以创建允许我们模块化和简化与这些系统交互的类。管理器类可以在许多不同的用例中找到。本质上，如果您看到需要对某个系统进行结构化控制，这可能是经理类的候选人。接下来是我为本书中的示例代码创建的一个管理器类的示例。随着我们的继续，你会看到更多。\n\n离开渲染系统一秒钟，让我们看看任何游戏都需要执行的一个非常常见的任务，处理输入。因为每一个游戏都需要某种形式的输入，所以只有将处理这种输入的代码转移到一个我们可以反复使用的类中才有意义。让我们看看`InputManager`类，从头文件开始:\n\n```cpp\n#pragma once \n#include <unordered_map> \n#include <glm/glm.hpp> \nnamespace BookEngine { \n  class InputManager \n  { \n  public: \n    InputManager(); \n    ~InputManager(); \n```\n\n`InputManager`类和其他类一样开始，我们有需要的包含，并且我们再次将该类包装在`BookEngine`名称空间中，以确保说服力和安全性。还定义了标准构造函数和析构函数。\n\n接下来，我们还有一些公共功能。首先是`Update`功能，不出意外会更新输入系统。然后我们有`KeyPress`和`KeyReleased`函数，这些函数都取一个对应于键盘按键的整数值。当`key`分别被按下或释放时，下列功能将会启动:\n\n```cpp\nvoid Update(); \nvoid KeyPress(unsigned int keyID);  \nvoid KeyRelease(unsigned int keyID);\n```\n\n在`KeyPress`和`KeyRelease`功能之后，我们还有两个关键的相关功能`isKeyDown`和`isKeyPressed`。像`KeyPress`和`KeyRelease`功能一样，`isKeyDown`和`isKeyPressed`功能采用对应于键盘按键的整数值。值得注意的区别是，这些函数根据键的状态返回一个布尔值。我们将在接下来的实现文件中看到更多相关信息:\n\n```cpp\n bool isKeyDown(unsigned int keyID); //Returns true if key is \n held    bool isKeyPressed(unsigned int keyID); //Returns true if key \n was pressed this update\n```\n\n`InputManager`类中的最后两个公共函数是`SetMouseCoords`和`GetMouseCoords`，它们完全按照名称建议的那样工作，分别设置或获取鼠标坐标。\n\n```cpp\nvoid SetMouseCoords(float x, float y); \nglm::vec2 GetMouseCoords() const { return m_mouseCoords; }; \n```\n\n接下来是私有成员和函数，我们声明了一些变量来存储一些关于键和鼠标的信息。首先，我们有一个存储按键被按下与否状态的布尔值。接下来，我们有两个无序地图，将存储当前`keymap`和以前的关键地图。我们存储的最后一个值是鼠标坐标。我们从另一个辅助库**中得到一个`vec2`构造，OpenGL 数学** ( **GLM** )。我们使用这个`vec2`，它只是一个二维向量，来存储鼠标光标的 *x* 和 *y* 坐标值，因为它在 2D 平面上，即屏幕上。如果你正在寻找向量和笛卡尔坐标系的复习资料，我强烈推荐约翰·弗林特博士的*游戏开发人员数学概念入门书*:\n\n```cpp\nprivate: \n   bool WasKeyDown(unsigned int keyID); \nstd::unordered_map<unsigned int, bool> m_keyMap; \n   std::unordered_map<unsigned int, bool> m_previousKeyMap; \n   glm::vec2 m_mouseCoords;\n}; \n```\n\n现在我们来看看实现，`InputManager.cpp`文件。\n\n我们再次从 includes 和名称空间包装器开始。然后我们有了构造函数和析构函数。这里需要注意的重点是构造函数中`m_mouseCoords`到`0.0f`的设置:\n\n```cpp\nnamespace BookEngine \n{ \n  InputManager::InputManager() : m_mouseCoords(0.0f) \n  { \n  } \n  InputManager::~InputManager() \n  { \n  } \n```\n\n接下来是`Update`功能。这是一个简单的更新，我们逐步通过`keyMap`中的每个键，并复制到以前的`keyMap`持有人\n\n`m_previousKeyMap`:\n\n```cpp\nvoid InputManager::Update() \n { \n   for (auto& iter : m_keyMap) \n   { \n     m_previousKeyMap[iter.first] = iter.second;  \n   } \n } \n```\n\n下一个功能是`KeyPress`功能。在这个函数中，我们使用关联数组的技巧来测试并插入与传入的 ID 相匹配的按键。诀窍在于，如果位于`keyID`索引的索引处的项目不存在，它将被自动创建:\n\n```cpp\nvoid InputManager::KeyPress(unsigned int keyID) \n { \n   m_keyMap[keyID] = true; \n } \n. We do the same for the KeyRelease function below. \n void InputManager::KeyRelease(unsigned int keyID) \n { \n   m_keyMap[keyID] = false; \n  } \n```\n\n`KeyRelease`功能与`KeyPressed`功能的设置相同，只是我们将`keyID`索引处的`keyMap`项目设置为假:\n\n```cpp\nbool InputManager::isKeyDown(unsigned int keyID) \n { \n   auto key = m_keyMap.find(keyID); \n   if (key != m_keyMap.end()) \n     return key->second;   // Found the key \n   return false; \n }\n```\n\n在`KeyPress`和`KeyRelease`功能之后，我们实现`isKeyDown`和`isKeyPressed`功能。首先是`isKeydown`功能；这里我们要测试一个键是否已经被按下。在这种情况下，我们采用不同于`KeyPress`和`KeyRelease`函数的方法来测试密钥，并避免关联数组技巧。这是因为如果密钥尚不存在，我们不想创建它，所以我们手动创建它:\n\n```cpp\nbool InputManager::isKeyPressed(unsigned int keyID) \n { \n   if(isKeyDown(keyID) && !m_wasKeyDown(keyID)) \n   { \n     return true; \n   } \n   return false; \n } \n```\n\n`isKeyPressed`功能相当简单。在这里，我们通过使用`isKeyDown`功能来测试是否按下了与传入的标识匹配的键，并且还通过将该标识传递给`m_wasKeyDown`来测试该键是否没有被按下。如果这两个条件都满足，我们返回真，否则返回假。接下来，我们有`WasKeyDown`函数，很像`isKeyDown`函数，我们进行手动查找以避免使用关联数组技巧意外创建对象:\n\n```cpp\nbool InputManager::WasKeyDown(unsigned int keyID) \n { \n   auto key = m_previousKeyMap.find(keyID); \n   if (key != m_previousKeyMap.end()) \n     return key->second;   // Found the key \n   return false; \n} \n```\n\n`InputManager`中的最后一个功能是`SetMouseCoords`。这是一个非常简单的`Set`函数，它接受传入的浮点数，并将它们分配给二维向量`m_mouseCoords`的`x`和`y`成员:\n\n```cpp\nvoid InputManager::SetMouseCoords(float x, float y) \n { \n   m_mouseCoords.x = x; \n   m_mouseCoords.y = y; \n } \n}\n```\n\n# 创建接口\n\n有时，您会面临这样一种情况，即您需要描述功能并提供对类的一般行为的访问，而不需要提交特定的实现。这就是接口或抽象类的想法开始发挥作用的地方。使用接口提供了一个简单的基类，其他类可以从中继承，而不必担心内在的细节。构建强接口可以通过提供一个标准类来进行交互，从而实现快速开发。虽然从理论上讲，接口可以由任何类创建，但是更常见的是在代码被重用的情况下使用它们。以下是为该书的示例代码创建的示例接口，该代码创建了一个游戏主类的接口。\n\n让我们看一下存储库中示例代码的界面。这个界面将提供对游戏核心组件的访问。我已经将这个类命名为`IGame`，使用前缀`I`将这个类标识为一个接口。以下是从定义文件`IGame.h`开始的实现。\n\n首先，我们拥有所需的 includes 和名称空间包装器。您会注意到，我们包含的文件是我们刚刚创建的一些文件。这是抽象延续的一个主要例子。我们使用这些构建块继续构建允许这种无缝抽象的结构:\n\n```cpp\n#pragma once \n#include <memory> \n#include \"\"BookEngine.h\"\" \n#include \"\"Window.h\"\" \n#include \"\"InputManager.h\"\" \n#include \"\"ScreenList.h\"\" \nnamespace BookEngine \n{ \n```\n\n接下来，我们有一个前进宣言。这个声明是为屏幕创建的另一个接口。此接口及其支持的帮助器类的完整源代码可在代码存储库中找到。类`IScreen`；像这样使用正向声明在 C++ 中是一种常见的做法。\n\nIf the definition file only requires the simple definition of a class, not adding the header for that class will speed up compile times.\n\n继续讨论公共成员和函数，我们从构造函数和析构函数开始。你会注意到这个析构函数在这种情况下是虚拟的。我们将析构函数设置为虚拟的，以允许我们通过指针在派生类的实例上调用 delete。当我们希望我们的界面也能直接处理一些清理工作时，这就很方便了:\n\n```cpp\nclass IGame \n  { \n  public: \n    IGame(); \n    virtual ~IGame(); \n```\n\n接下来我们有`Run`函数和`ExitGame`函数的声明。\n\n```cpp\n    void Run(); \n    void ExitGame(); \n```\n\n然后我们有一些纯虚函数，`OnInit`、`OnExit`和`AddScreens`。纯虚函数是必须被继承类重写的函数。通过在定义的末尾添加`=0;`，我们告诉编译器这些函数纯粹是虚拟的。\n\n在设计接口时，在定义哪些函数必须被覆盖时，保持谨慎是很重要的。同样非常重要的是要注意，拥有纯虚函数会隐式地使为其定义的类变得抽象。因此，抽象类不能直接实例化，任何派生类都需要实现所有继承的纯虚函数。如果没有，它们也会变得抽象:\n\n```cpp\n    virtual void OnInit() = 0; \n    virtual void OnExit() = 0; \n    virtual void AddScreens() = 0; \n```\n\n在我们的纯虚函数声明之后，我们有一个函数`OnSDLEvent`，我们用它来连接到 SDL 事件系统。这为我们的输入和其他事件驱动系统提供了支持:\n\n```cpp\nvoid OnSDLEvent(SDL_Event& event);\n```\n\n`IGame`接口类中的公共函数是一个简单的辅助函数`GetFPS`，返回当前的`fps`。注意`const`修饰符，它们很快识别出该函数不会以任何方式修改变量值:\n\n```cpp\nconst float GetFPS() const { return m_fps; } \n```\n\n在我们受保护的空间中，我们从一些函数声明开始。首先是`Init`或初始化功能。这将是处理大部分设置的功能。然后我们有两个虚函数`Update`和`Draw`。\n\n像纯虚函数一样，虚函数是可以被派生类的实现重写的函数。与纯虚函数不同，虚函数在默认情况下不会使类抽象，也不必被重写。虚拟和纯虚拟功能是多态设计的基石。随着您继续发展旅程，您将很快看到它们的好处:\n\n```cpp\nprotected: \n   bool Init(); \n   virtual void Update(); \n   virtual void Draw(); \n```\n\n为了关闭`IGame`定义文件，我们有几个成员来存放不同的对象和值。我不打算一行一行地讨论这些问题，因为我觉得它们不言自明:\n\n```cpp\n    std::unique_ptr<ScreenList> m_screenList = nullptr; \n    IGameScreen* m_currentScreen = nullptr; \n    Window m_window; \n    InputManager m_inputManager; \n    bool m_isRunning = false; \n    float m_fps = 0.0f; \n  }; \n} \n```\n\n现在我们已经了解了接口类的定义，让我们快速浏览一下实现。以下是`IGame.cpp`文件。为了节省时间和空间，我将突出重点。在大多数情况下，代码是不言自明的，并且为了更清楚起见，位于存储库中的源代码得到了很好的注释:\n\n```cpp\n#include \"\"IGame.h\"\" \n#include \"\"IScreen.h\"\" \n#include \"\"ScreenList.h\"\" \n#include \"\"Timing.h\"\" \nnamespace BookEngine \n{ \n  IGame::IGame() \n  { \n    m_screenList = std::make_unique<ScreenList>(this); \n  } \n\n  IGame::~IGame() \n  { \n  } \n```\n\n我们的实现从构造函数和析构函数开始。构造器很简单，它唯一的工作就是使用这个`IGame`对象作为传入的参数来添加一个新屏幕的唯一指针。有关屏幕创建的更多信息，请参见`IScreen`类。接下来，我们来实现`Run`功能。这个函数在被调用时会启动发动机。在函数内部，我们做了一个快速检查，以确保我们已经初始化了我们的对象。然后，我们使用另一个助手类`fpsLimiter`，来`SetMaxFPS`我们的游戏可以运行。之后，我们将`isRunning`布尔值设置为`true`，然后用它来控制游戏循环:\n\n```cpp\nvoid IGame::Run() \n  { \n    if (!Init()) \n      return; \n    FPSLimiter fpsLimiter; \n    fpsLimiter.SetMaxFPS(60.0f); \n    m_isRunning = true; \n```\n\n接下来是游戏循环。在游戏循环中，我们做一些简单的调用。首先，我们启动`fpsLimiter`。然后我们调用`InputManager`上的更新函数。\n\nIt is a good idea always to check input before doing other updates or drawing since their calculations are sure to use the new input values.\n\n在我们更新`InputManager`之后，我们递归调用我们的`Update`和`Draw`类，我们很快就会看到。我们通过结束`fpsLimiter`函数并在`Window`对象上调用`SwapBuffer`来结束循环:\n\n```cpp\n///Game Loop \n    while (m_isRunning) \n    { \n      fpsLimiter.Begin(); \n      m_inputManager.Update(); \n      Update(); \n      Draw(); \n      m_fps = fpsLimiter.End(); \n      m_window.SwapBuffer(); \n    } \n  } \n```\n\n我们实现的下一个功能是`ExitGame`功能。最终，这将是游戏最终退出时调用的函数。我们关闭、销毁并释放屏幕列表创建的任何内存，并将`isRunning`布尔设置为`false`，这将结束循环:\n\n```cpp\nvoid IGame::ExitGame() \n { \n   m_currentScreen->OnExit(); \n   if (m_screenList) \n   { \n     m_screenList->Destroy(); \n     m_screenList.reset(); //Free memory \n   } \n   m_isRunning = false; \n } \n```\n\n接下来是`Init`功能。该函数将初始化所有内部对象设置，并在连接的系统上调用初始化。同样，这是面向对象编程和多态的一个很好的例子。以这种方式处理初始化允许级联效应，保持代码模块化，并且更容易修改:\n\n```cpp\n  bool IGame::Init() \n  { \n    BookEngine::Init(); \n    SDL_GL_SetAttribute(SDL_GL_ACCELERATED_VISUAL, 1); \n    m_window.Create(\"\"BookEngine\"\", 1024, 780, 0); \n    OnInit(); \n    AddScreens(); \n    m_currentScreen = m_screenList->GetCurrentScreen(); \n    m_currentScreen->OnEntry();     \n    m_currentScreen->Run(); \n    return true; \n}\n```\n\n接下来，我们有`Update`功能。在这个`Update`函数中，我们创建了一个结构，允许我们基于当前屏幕所处的状态执行某些代码。我们使用一个简单的开关案例方法来实现这一点，以`ScreenState`类型的枚举元素作为案例。这个设置被认为是一个简单的有限状态机，是一个非常强大的设计方法，在整个游戏开发中使用。在本书的示例中，您肯定会再次看到这个弹出窗口:\n\n```cpp\nvoid IGame::Update() \n  { \n    if (m_currentScreen) \n    { \n      switch (m_currentScreen->GetScreenState()) \n      { \n      case ScreenState::RUNNING: \n        m_currentScreen->Update(); \n        break; \n      case ScreenState::CHANGE_NEXT: \n        m_currentScreen->OnExit(); \n        m_currentScreen = m_screenList->MoveToNextScreen(); \n        if (m_currentScreen) \n        { \n          m_currentScreen->Run(); \n          m_currentScreen->OnEntry(); \n        } \n        break; \n      case ScreenState::CHANGE_PREVIOUS: \n        m_currentScreen->OnExit(); \n        m_currentScreen = m_screenList->MoveToPreviousScreen(); \n        if (m_currentScreen) \n        { \n          m_currentScreen->Run(); \n          m_currentScreen->OnEntry(); \n        } \n        break; \n      case ScreenState::EXIT_APP: \n          ExitGame(); \n          break; \n      default: \n          break; \n      } \n    } \n    else \n    { \n      //we have no screen so exit \n      ExitGame(); \n    } \n  }\n```\n\n在我们的`Update`之后，我们实现`Draw`功能。在我们的功能中，我们只做几件事。首先，我们重置`Viewport`作为一个简单的安全检查，然后如果当前屏幕的状态与枚举值`RUNNING`匹配，我们再次使用多态性传递`Draw`调用对象行:\n\n```cpp\nvoid IGame::Draw() \n  { \n    //For safety \n    glViewport(0, 0, m_window.GetScreenWidth(), m_window.GetScreenHeight()); \n\n    //Check if we have a screen and that the screen is running \n    if (m_currentScreen && \n      m_currentScreen->GetScreenState() == ScreenState::RUNNING) \n    { \n      m_currentScreen->Draw(); \n    } \n  } \n```\n\n我们需要实现的最后一个功能是`OnSDLEvent`功能。就像我在这个类的定义部分提到的，我们将使用这个函数把我们的`InputManager`系统连接到 SDL 内置事件系统。\n\n每次按键或鼠标移动都被当作一个事件来处理。基于已经发生的事件类型，我们再次使用 switch case 语句来创建一个简单的有限状态机。关于每个功能是如何实现的，请参考前面的经理模式讨论部分。\n\n```cpp\n  void IGame::OnSDLEvent(SDL_Event & event) \n  { \n    switch (event.type) { \n    case SDL_QUIT: \n      m_isRunning = false; \n      break; \n    case SDL_MOUSEMOTION: \n      m_inputManager.SetMouseCoords((float)event.motion.x, \n(float)event.motion.y); \n      break; \n    case SDL_KEYDOWN: \n      m_inputManager.KeyPress(event.key.keysym.sym); \n      break; \n    case SDL_KEYUP: \n      m_inputManager.KeyRelease(event.key.keysym.sym); \n      break; \n    case SDL_MOUSEBUTTONDOWN: \n      m_inputManager.KeyPress(event.button.button); \n      break; \n    case SDL_MOUSEBUTTONUP: \n      m_inputManager.KeyRelease(event.button.button); \n      break; \n    } \n  } \n}\n```\n\n嗯，这就解决了`IGame`界面的问题。有了这个，我们现在可以创建一个新的项目，它可以利用示例引擎中的这个和其他接口来创建一个游戏，并且只用几行代码就可以初始化它。这是位于代码库的`Chapter03`文件夹中的示例项目的`App`类:\n\n```cpp\n#pragma once \n#include <BookEngine/IGame.h> \n#include \"\"GamePlayScreen.h\"\" \nclass App : public BookEngine::IGame \n{ \npublic: \n  App(); \n  ~App(); \n  virtual void OnInit() override; \n  virtual void OnExit() override; \n  virtual void AddScreens() override; \nprivate: \n  std::unique_ptr<GameplayScreen> m_gameplayScreen = nullptr; \n}; \n```\n\n这里需要注意的重点是，第一，`App`类继承自`BookEngine::IGame`接口，第二，我们拥有继承类所需的所有必要覆盖。接下来，如果我们看一下应用的入口点`main.cpp`文件，您将会看到简单的命令来设置和启动我们的界面、管理器和助手为我们抽象的所有令人惊奇的事情:\n\n```cpp\n#include <BookEngine/IGame.h> \n#include \"\"App.h\"\" \nint main(int argc, char** argv) \n{ \n  App app; \n  app.Run(); \n  return 0; \n} \n```\n\n正如您所看到的，每次我们想要创建一个新项目时，这比不得不从头开始不断地重新创建框架要简单得多。\n\n要查看本章中描述的框架的输出，构建`BookEngine`项目，然后构建并运行示例项目。XCode 和 Visual Studio 项目可以在 GitHub 代码存储库的`Chapter03`文件夹中找到。\n\n在 Windows 上，运行时的示例项目如下所示:\n\n>![](img/83deb1cf-51a2-4f66-b3dd-72b7204a62c0.png)\n\n在 macOS 上，运行时的示例项目如下所示:\n\n![](img/657f1f45-5315-4e53-88dc-22a2d014fa73.png)\n\n# 摘要\n\n在这一章中，我们讲述了很多。我们看了使用面向对象编程和多态性为所有游戏项目创建可重用结构的不同方法。我们用真实代码中的例子来演示助手类、管理器类和接口类的区别。\n\n在接下来的章节中，我们将看到这个结构被重用，并在此基础上创建演示。事实上，在下一章中，我们将构建更多的管理器和助手类来创建素材管理管道。"
  },
  {
    "path": "docs/master-cpp-game-dev/04.md",
    "content": "# 四、构建素材管道\n\n游戏本质上是以有趣和吸引人的方式包装的素材或内容的集合。处理视频游戏所需的所有内容本身就是一个巨大的挑战。在任何实际项目中，都需要一个适当的结构来导入、转换和消费这些素材。在本章中，我们将探讨开发和实现素材管道的主题。以下是我们将讨论的主题:\n\n*   处理音频\n*   使用图像\n*   导入模型网格\n\n# 什么是素材管道？\n\n在[第 3 章](03.html)、*构建强大的基础*中，我们看了如何使用 helper 和 manager 类的结构将多个方法包装到一个易于使用的接口中，以处理项目的各个部分。我们将在接下来的几节中使用这些技术来构建我们自己的定制框架/内容管道。\n\n# 处理音频\n\n首先，我们将通过研究如何在我们的游戏项目中处理音频素材来让自己轻松进入这个过程。为了帮助我们完成这个过程，我们将再次使用一个助手库。实际上有数百个不同的库来帮助使用音频。这里列出了一些更受欢迎的选择:\n\n*   FMOD([http://www.fmod.org](http://www.fmod.org/))\n*   https://www.audiokinetic.com/products/wwise/\n*   xaudio 2([https://msdn . Microsoft . com/en-us/library/windows/desktop/ee 415813(v = vs . 85)。aspx](https://msdn.microsoft.com/en-us/library/windows/desktop/ee415813(v=vs.85).aspx) )\n*   OpenAL([https://www.openal.org/](https://www.openal.org/))\n*   SDL _ 混音器([https://www.libsdl.org/projects/SDL_mixer/](https://www.libsdl.org/projects/SDL_mixer/))\n\n每个库都有自己的长处和短处。为你的项目选择正确的一个归结为几个不同的问题，你应该问自己。\n\n这个库满足你的技术需求吗？它有你想要的所有功能吗？\n\n是否符合项目的预算限制？许多更健壮的库都有很大的价格标签。\n\n这个库的学习曲线在你自己或者团队的技能范围内吗？选择一个包含一堆很酷的特性的高级应用编程接口看起来是个好主意，但是如果你花更多的时间去理解应用编程接口而不是实现它，这可能是有害的。\n\n对于本书中的例子，我选择使用`SDL_mixer API`有几个原因。首先，与其他一些方法相比，它很容易上手。其次，它非常符合我的项目需求。它支持 FLAC、MP3，甚至 Ogg Vorbis 文件。第三，它与项目框架的其余部分连接良好，因为它是我们已经在使用的 SDL 库的扩展。最后，我选择了这个应用编程接口，因为它是开源的，并且有一个简单的许可证，不需要我向创作者支付游戏收益的一部分来换取使用这个库。\n\n让我们先来看看我们需要的几个不同类的声明和实现。我们看到的文件是`AudioManager.h`文件，可以在代码库的`Chapter04`文件夹中找到。\n\n我们从必要的包括、`SDL/SDL_mixer.h`、`string`和`map`实现开始。像我们一直在构建的所有其他引擎组件一样，我们将这些声明包装在`BookEngine`名称空间中:\n\n```cpp\n#pragma once \n#include <SDL/SDL_mixer.h> \n#include <string> \n#include <map> \n\nnamespace BookEngine \n{\n```\n\n在`\"AudioManager.h\"`文件中，我们有几个助手类的声明。第一个是`SoundEffect`班。这个类定义了我们游戏中使用的音效对象的结构:\n\n```cpp\nclass SoundEffect \n { \n  public: \n    friend class AudioManager; \n    ///Plays the sound file \n    ///@param numOfLoops: If == -1, loop forever, \n    ///otherwise loop of number times provided + 1 \n    void Play(int numOfLoops = 0); \n\n  private: \n    Mix_Chunk* m_chunk = nullptr; \n  }; \n```\n\n这些可以包括玩家跳跃、武器开火的声音，以及我们将在短时间内玩的任何东西。\n\n在类定义中，我们需要一个`friend`类语句，允许这个类访问`AudioManager`类方法和变量，包括私有方法和变量。接下来我们有`Play`函数的定义。这个函数将简单地播放声音效果，只取一个参数，即循环播放声音的次数。默认情况下，我们将此设置为`0`，如果您通过`-1`作为循环次数，它会将音效设置为无限循环。最后一个定义是`Mix_Chunk`类型的私有变量。`Mix_Chunk`是将音频数据存储在内存中的`SDL_mixer`对象类型。\n\n`Mix_Chunk`对象结构如下:\n\n```cpp\ntypedef struct { \n        int allocated; \n        Uint8 *abuf; \n        Uint32 alen; \n        Uint8 volume; \n} Mix_Chunk; \n```\n\n以下是该对象的内部结构:\n\n*   `allocated`:如果设置为`1`，`struct`有自己分配的缓冲区\n*   `abuf`:这是指向音频数据的指针\n*   `alen`:这是音频数据的长度，以字节为单位\n*   `volume`:这是 0 到 128 之间的每样本体积值\n\n我们在`AudioManager.h`文件中的下一个助手类是`Music`类。像音效一样，`Music`类定义了一个`Music`对象的结构。这可用于加载屏幕音乐、背景音乐等声音，以及我们希望长时间播放或需要停止、开始和暂停的任何声音:\n\n```cpp\nclass Music \n  { \n  public: \n    friend class AudioManager; \n    ///Plays the music file \n    ///@param numOfLoops: If == -1, loop forever, \n    ///otherwise loop of number times provided \n    void Play(int numOfLoops = -1); \n\n    static void Pause() { Mix_PauseMusic(); }; \n    static void Stop() { Mix_HaltMusic(); }; \n    static void Resume() { Mix_ResumeMusic(); }; \n\n  private: \n    Mix_Music* m_music = nullptr; \n  }; \n```\n\n对于类定义，我们再次以`friend`类语句开始，这样`Music`类就可以访问`AudioManager`类所需的部分。接下来我们有一个`Play`函数，就像`SoundEffect`类一样，它只需要一个参数来设置声音将经过的循环数量。在`Play`功能之后，我们还有三个功能，`Pause()`、`Stop()`和`Resume()`功能。这三个函数只是 SDL 调音台 API 调用的包装器，分别用于暂停、停止和恢复音乐。\n\n最后，我们有一个`Mix_Music`对象的私有声明。`Mix_Music`是用于音乐数据的 SDL 调音台数据类型。它支持加载 WAV，MOD，MID，OGG 和 MP3 声音文件。我们将在接下来的实现部分看到更多相关信息:\n\n```cpp\nclass AudioManager \n  { \n  public: \n    AudioManager(); \n    ~AudioManager(); \n\n    void Init(); \n    void Destroy(); \n\n    SoundEffect LoadSoundEffect(const std::string& filePath); \n    Music LoadMusicEffect(const std::string& filePath); \n  private: \n    std::map<std::string, Mix_Chunk*> m_effectList; \n    std::map<std::string, Mix_Music*> m_musicList; \n    bool m_isInitialized = false; \n  }; \n} \n```\n\n在两个`Music`和`SoundEffect`助手类之后，我们现在来看`AudioManager`类的定义。`AudioManager`类将完成我们这边的大部分繁重工作，它将加载、保存和管理所有音乐和音效的创建和删除。\n\n我们的类声明像大多数其他声明一样，从默认构造函数和析构函数开始。接下来我们有一个`Init()`函数。该功能将处理我们音频系统的设置或初始化。然后我们有一个`Destroy()`功能，将处理我们的音频系统的删除和清理。在`Init`和`Destroy`功能之后，我们有两个加载器功能，`LoadSoundEffect()`和`LoadMusicEffent()`功能。这两个函数都有一个参数，一个保存音频文件路径的标准字符串。这些功能将加载音频文件，并根据功能返回一个`SoundEffect`或`Music`对象。\n\n我们班的私人部分有三个对象。前两个私有对象是类型为`Mix_Chunk`或`Mix_Music`的地图。这是我们将存储我们需要的所有效果和音乐的地方。通过存储我们加载的音效和音乐文件列表，我们创建了一个缓存。如果我们在项目后期需要该文件，我们可以检查这些列表并节省一些宝贵的加载时间。最后一个变量`m_isInitialized`，保存一个布尔值来指定`AudioManager`类是否已经初始化。\n\n这就完成了`AudioManager`和助手类的声明，让我们继续到实现，在这里我们可以更仔细地看看一些函数。您可以在代码库的`Chapter04`文件夹中找到`AudioManager.cpp`文件:\n\n```cpp\n#include \"AudioManager.h\"\n#include \"Exception.h\" \n#include \"Logger.h\"\n\nnamespace BookEngine \n{ \n\n  AudioManager::AudioManager() \n  { \n  } \n\n  AudioManager::~AudioManager() \n  { \n    Destroy(); \n  } \n```\n\n我们的实现从包含、默认构造函数和析构函数开始。这里没什么新鲜的，唯一值得注意的是我们从析构函数调用`Destroy()`函数。这允许我们通过析构函数或通过显式调用对象本身的`Destroy()`函数来清理类的两种方法:\n\n```cpp\nvoid BookEngine::AudioManager::Init() \n  { \n    //Check if we have already been initialized \n    if (m_isInitialized) \n      throw Exception(\"Audio manager is already initialized\"); \n```\n\n`AudioManager`类实现中的下一个函数是`Init()`函数。这个功能将为我们的经理设置所有需要的组件。这个函数从一个简单的检查开始，看看我们是否已经初始化了这个类；如果有，我们抛出一个异常，并显示一条调试消息:\n\n```cpp\n//Can be Bitwise combination of  \n//MIX_INIT_FAC, MIX_INIT_MOD, MIX_INIT_MP3, MIX_INIT_OGG \nif(Mix_Init(MIX_INIT_OGG || MIX_INIT_MP3) == -1) \n throw Exception(\"SDL_Mixer could not initialize! Error: \" + \n std::string(Mix_GetError()));\n```\n\n在我们检查了还没有之后，我们继续初始化 SDL 调音台对象。我们通过调用`Mix_Init()`函数并传入标志的位组合来设置支持的文件类型。这可以是 FLAC、MOD、MP3 和 OGG 的组合。在这个例子中，我们传递了支持 OGG 和 MP3 的标志。我们用 if 语句包装这个调用，以检查`Mix_Init()`函数调用是否有任何问题。如果遇到错误，我们会抛出另一个异常，并显示一条调试消息，其中包含从`Mix_Init()`函数返回的错误信息:\n\n```cpp\nif(Mix_OpenAudio(MIX_DEFAULT_FREQUENCY, MIX_DEFAULT_FORMAT, 2, \n 1024) == -1)      throw Exception(\"Mix_OpenAudio Error: \" + \n std::string(Mix_GetError()));\n```\n\n一旦`SDL_mixer`功能被初始化，我们可以调用`Mix_OpenAudio`来配置`frequency`、`format`、`channels`和`chunksize`来使用。需要注意的是，该函数必须在任何其他`SDL_mixer`函数之前调用。函数定义如下所示:\n\n```cpp\nint Mix_OpenAudio(int frequency, Uint16 format, int channels, int chunksize)\n```\n\n以下是这些论点的含义:\n\n*   `frequency`:这是以每秒采样数为单位的输出采样频率，Hz。在示例中，我们使用`MIX_DEFAULT_FREQUENCY`定义，它是 22050，对于大多数情况来说是一个很好的值。\n*   `format`:这是输出样本格式；同样，在示例中，我们通过使用`MIX_DEFAULT_FORMAT` define 将其设置为默认值，这与按照系统字节顺序使用`AUDIO_S16SYS`或带符号 16 位样本相同。要查看完整格式，定义列表，请参见`SDL_audio.h`文件。\n*   `channels`:这是输出中的声道数。2 个立体声通道，1 个单声道通道。值 2 用于我们的示例。\n*   `chunksize`:这是每个输出样本使用的字节数。我们使用`1024`字节或 1 **兆字节** ( **兆字节**)作为组块大小。\n\n最后，我们在这个函数中做的最后一件事是将`m_isInitalized`布尔值设置为真。这将防止我们意外地再次尝试初始化该类:\n\n```cpp\nm_isInitialized = true; \n  } \n```\n\n`AudioManager`类中的下一个函数是`Destroy()`方法:\n\n```cpp\n  void BookEngine::AudioManager::Destroy() \n  { \n    if (m_isInitialized) \n    { \n      m_isInitialized = false; \n\n      //Release the audio resources \n      for(auto& iter : m_effectList) \n        Mix_FreeChunk(iter.second); \n      for(auto& iter : m_musicList) \n        Mix_FreeMusic(iter.second); \n      Mix_CloseAudio(); \n      Mix_Quit(); \n    } \n  } \n```\n\n我不会一行一行地讨论这个函数，因为它是不言自明的。基本概况是；检查`AudioManager`是否已经初始化，如果已经初始化，那么我们使用`Mix_FreeChunk()`功能来释放我们创建的每个声音和音乐资源。最后我们使用`Mix_CloseAudio()`和`Mix_Quit()`关闭、清理并关闭 SDL _ 混合器 API。\n\n`LoadSoundEffect`是我们接下来要看的功能。这个函数就像它的名字一样，是加载声音效果的函数:\n\n```cpp\n SoundEffect BookEngine::AudioManager::LoadSoundEffect(const std::string & filePath)\n  { \n    SoundEffect effect; \n```\n\n这个函数的第一步是创建一个`SoundEffect`对象来暂时保存数据，直到我们将效果返回给调用方法。我们简单地把这个变量叫做效果。\n\n在我们创建了我们的保持变量之后，我们做一个快速的检查，看看我们需要的这个效果是否已经被创建并存储在我们的缓存中，映射对象，`m_effectList`:\n\n```cpp\n//Lookup audio file in the cached list \nauto iter = m_effectList.find(filePath); \n```\n\n我们在这里做这件事的有趣方式是创建一个迭代器变量，并给它分配`Map.find()`的结果，这里传递的参数是我们想要加载的声音文件的位置。这个方法很酷的一点是，如果在缓存中找不到声音效果，迭代器值将被设置为映射的结束对象的索引，允许我们做一个简单的检查，如下所示:\n\n```cpp\n//Failed to find in cache, load \n    if (iter == m_effectList.end()) \n    { \n      Mix_Chunk* chunk = Mix_LoadWAV(filePath.c_str()); \n      //Error Loading file \n      if(chunk == nullptr) \n        throw Exception(\"Mix_LoadWAV Error: \" + \n              std::string(Mix_GetError())); \n\n      effect.m_chunk = chunk; \n      m_effectList[filePath] = chunk; \n    } \n\n```\n\n使用迭代器值技巧，我们只需检查`iter`变量的值是否与`Map.end()`函数的返回值匹配；如果有，这意味着声音效果不在缓存列表中，应该创建。\n\n为了加载音效，我们调用`Mix_LoadWAV()`函数，将文件路径位置的参数作为`c`字符串。我们将返回的对象分配给一个名为 chunk 的`Mix_Chunk`指针。\n\n然后，我们检查块的值是否是一个`nullptr`指针，表明加载函数遇到了错误。如果它是一个`nullptr`指针，我们抛出一个异常，其中包含一些由便利的`Mix_GetError()`函数提供的调试信息。如果成功，我们分配我们的临时持有者，效果成员`m_chunk`，块的值，这是我们加载的声音效果数据。\n\n接下来，我们将这个新加载的效果添加到我们的缓存中，这样我们就可以在将来节省一些精力。\n\n或者，如果我们对`iter`值的检查返回 false，这意味着我们试图加载的音效在缓存中:\n\n```cpp\nelse //Found in cache \n    { \n      effect.m_chunk = iter->second; \n    } \n\n    return effect; \n  } \n```\n\n迭代器的真正妙处现已显露。查找结果，即第`auto iter = m_effectList.find(filePath);`行的结果，当它找到声音效果时，就会指向列表中的那个效果。所以我们所要做的就是将持有者变量效果成员值`m_chunk`分配给`iter`第二个值，这是效果的数据值。我们在`LoadSoundEffect()`函数中做的最后一件事是将效果变量返回给调用方法。这就完成了这个过程，我们的音效现在可以使用了。\n\n`LoadSoundEffect()`功能之后，是`LoadMusic()`功能:\n\n```cpp\nMusic BookEngine::AudioManager::LoadMusic(const std::string & filePath) \n  { \n    Music music; \n\n    //Lookup audio file in the cached list \n    auto iter = m_musicList.find(filePath); \n\n    //Failed to find in cache, load \n    if (iter == m_musicList.end()) \n    { \n      Mix_Music* chunk = Mix_LoadMUS(filePath.c_str()); \n      //Error Loading file \n      if (chunk == nullptr) \n           throw Exception(\"Mix_LoadMUS Error: \" +\n            std::string(Mix_GetError())); \n\n      music.m_music = chunk; \n      m_musicList[filePath] = chunk; \n    } \n    else //Found in cache \n    { \n      music.m_music = iter->second; \n    } \n\n    return music; \n  } \n```\n\n我不打算详细讨论这个函数，因为正如你所看到的，它非常像`LoadSoundEffect()`函数，但是它没有包装`Mix_LoadWAV()`函数，而是包装了`SDL_mixer`库的`Mix_LoadMUS()`。\n\n`AudioManager.cpp`文件中的最后两个函数实现不属于`AudioManager`类本身，而是`SoundEffect`和`Music`助手类的`Play`函数的实现:\n\n```cpp\n void SoundEffect::Play(int numOfLoops) \n  { \n    if(Mix_PlayChannel(-1, m_chunk, numOfLoops) == -1) \n      if (Mix_PlayChannel(0, m_chunk, numOfLoops) == -1) \n          throw Exception(\"Mix_PlayChannel Error: \" + \n                std::string(Mix_GetError())); \n  } \n\n  void Music::Play(int numOfLoops) \n  { \n    if (Mix_PlayMusic(m_music, numOfLoops) == -1) \n      throw Exception(\"Mix_PlayMusic Error: \" + \n                 std::string(Mix_GetError())); \n  }   \n} \n```\n\n我不会一行一行地遍历每个函数，相反，我想简单地指出这些函数是如何围绕 SDL_mixer、`Mix_PlayChannel`和`Mix_PlayMusic`函数创建包装器的。这本质上是`AudioManager`类的重点，它只是一个抽象加载文件和直接创建对象过程的包装器。这有助于我们创建一个可扩展的框架，管道，而不用担心底层机制。这意味着，理论上，我们可以随时用另一个甚至多个库替换底层库，而不会干扰调用管理器类函数的代码。\n\n为了完善这个例子，让我们看看如何在演示项目中使用这个`AudioManager`。您可以在代码库的`Chapter04`文件夹中找到这个演示，标签为`SoundExample`。音乐的功劳归于本声([http://www.bensound.com](http://www.bensound.com/))。\n\n从`GameplayScreen.h`文件开始:\n\n```cpp\nprivate: \n  void CheckInput(); \n  BookEngine::AudioManager m_AudioManager; \n  BookEngine::Music m_bgMusic; \n}; \n```\n\n我们向私有声明中添加了两个新对象，一个用于名为`m_AudioManager`的`AudioManager`，另一个用于名为`m_bgMusic`的`Music`对象。\n\n在`GameplayScreen.cpp`文件中:\n\n```cpp\nvoid GameplayScreen::OnEntry() \n{ \n  m_AudioManager.Init(); \n  m_bgMusic = m_audioManager.LoadMusic(\"Audio/bensound-epic.mp3\"); \n  m_bgMusic.Play(); \n} \n```\n\n为了初始化、加载和播放我们的音乐文件，我们需要向`GameplayScreen`类`OnEntry()`添加三行。\n\n*   第一行`m_AudioManager.Init()`设置`AudioManager`并初始化所有组件，正如我们之前看到的。\n\n*   接下来我们加载音乐文件，在这种情况下是`bensound-epic.mp3`文件，并将其分配给`m_bgMusic`变量。\n\n*   最后一行`m_bgMusic.Play()`，开始播放音乐曲目。通过不传入循环音乐曲目的次数，默认为`-1`，这意味着它将继续循环，直到程序停止。\n\n处理音乐轨道的播放，但是我们需要增加一些函数调用来清理游戏结束时的`AudioManager`，如果我们切换屏幕，停止音乐。\n\n为了在我们离开此屏幕时停止播放音乐，我们在`GameplayScreen`类`OnExit`功能中添加了以下内容:\n\n```cpp\nm_bgMusic.Stop(); \n```\n\n为了清理`AudioManager`并阻止任何潜在的内存泄漏，我们在`GameplayScreen`类`Destroy`函数中调用以下内容:\n\n```cpp\n  m_AudioManager.Destroy(); \n```\n\n这将依次处理我们在上一节中介绍的任何音频素材的销毁和清理。\n\n现在所有这些都准备好了，如果你运行`SoundExample`演示，你会听到一些史诗冒险音乐开始播放，如果你足够耐心，会不断循环播放。现在，我们在游戏中有了一些声音，让我们加快速度，看看如何在我们的项目中获得一些视觉素材。\n\n# 使用纹理\n\n一个纹理，如果你不熟悉这个术语，基本上可以认为是一个图像。这些纹理可以应用于一个简单的几何正方形，两个三角形，以制作一个图像。这种类型的图像通常被称为`Sprite`。我们在本节末尾的演示中使用了一个`Sprite`类。还需要注意的是，纹理可以应用于更复杂的几何图形，并用于皮肤对象的 3D 建模。在本书后面的演示中，纹理将扮演更重要的角色。\n\n# 资源管理程序\n\n让我们从高水平的课程`ResourceManager`开始。这个 manager 类将负责维护缓存中的资源对象，并提供一个简单的抽象接口来获取资源:\n\n```cpp\n#pragma once \n#include \"TextureCache.h\"\n#include <string> \nnamespace BookEngine \n{ \nclass ResourceManager \n  { \n  public: \n    static GLTexture GetTexture(std::string pathToTextureFile); \n  private: \n    static TextureCache m_textureCache; \n  }; \n} \n```\n\n声明文件`ResourceManager.h`是一个简单的类，由一个公共函数`GetTexture`和一个类型为`TextureCache`的私有成员组成。`GetTexure`将是我们向其他类公开的函数。它将负责返回纹理对象。`TextureCache`就像我们在`AudioManager`中使用的缓存，它会保存加载的纹理以备后用。让我们继续讨论实现，这样我们就可以看到这是如何设置的:\n\n```cpp\n#include \"ResourceManager.h\"\nnamespace BookEngine \n{ \n  TextureCache ResourceManager::m_textureCache; \n\n  GLTexture ResourceManager::GetTexture(std::string texturePath) \n  { \n    return m_textureCache.GetTexture(texturePath); \n  } \n} \n```\n\n`ResourceManager`实现实际上只是对底层结构的抽象调用。当我们调用`ResourceManager`类的`GetTexture`函数时，我们期望得到一个`GLTexture`类型。作为这个函数的调用者，我不需要担心`TextureCache`的内部工作方式或者对象是如何被解析的。我所要做的就是指定我想要加载的纹理的路径，素材管道完成剩下的工作。这应该是素材管道系统的最终目标，不管方法如何，接口都应该足够抽象，以允许开发人员和设计人员在项目中导入和使用素材，而底层系统的实现不会成为阻碍。\n\n接下来我们将看看这个纹理系统的例子，它是简单的`ResourceManager`类接口的核心。\n\n# 纹理和纹理贴图\n\n之前我们看到引入了两个新的对象，它们构成了`ResourceManager`类的结构，即`GLTexture`和`TextureCache`。在接下来的章节中，我们将更详细地了解这两个类，这样我们就可以看到这些类如何连接到其他系统来构建一个健壮的素材管理系统，所有这些都将回到`ResourceManager`的简单界面。\n\n首先我们来看看这个类，`GLTexture`。这个类仅由描述我们纹理属性的`struct`组成。以下是`GLTexture`类的全部代码:\n\n```cpp\n#pragma once \n#include <GL/glew.h> \nnamespace BookEngine \n{ \n  struct GLTexture \n  { \n    GLuint id; \n    int width; \n    int height; \n  }; \n} \n```\n\n如前所述，`GLTexture`类实际上只是`struct`的包装器，也称为`GLTexture`。这个`struct`保存了一些简单的值。一个`GLuint id`，用来识别纹理和两个整数值，`width`和`height`，当然是保存纹理/图像的高度和宽度。这个`struct`很容易包含在`TextureClass`中，我选择这样实现它，一是让它更容易阅读，二是为了给未来的开发留出一些灵活性。同样，我们希望确保我们的素材管道能够适应不同的需求并包含新的素材类型。\n\n接下来我们有`TextureCache`类，就像我们对音频资源所做的那样，为我们的图像文件创建一个缓存是一个好主意。这将再次通过将所需的图像文件保存在地图中并根据需要返回它们，为我们提供更快的访问。我们只需要创建一个新的纹理，如果它还不存在于缓存中。在构建任何使用素材的系统时，我倾向于使用缓存机制来支持这种类型的实现。\n\n虽然这些示例提供了基本的实现，但它们是创建更健壮的系统的良好起点，集成了内存管理和其他组件。下面是`TextureCache`类的声明，从前面的音频例子看应该很熟悉:\n\n```cpp\n#pragma once \n#include <map> \n#include \"GLTexture.h\"\n\nnamespace BookEngine \n{ \n  class TextureCache \n  { \n  public: \n    TextureCache(); \n    ~TextureCache(); \n\n    GLTexture GetTexture(std::string texturePath);  \n  private: \n    std::map<std::string, GLTexture> m_textureMap; \n\n  }; \n} \n```\n\n接下来是`TextureCache`类的实现，在`TextureCache.cpp`文件中，让我们看一下`GetTexture()`:\n\n```cpp\nGLTexture TextureCache::GetTexture(std::string texturePath) { \n\n    //lookup the texture and see if it''''s in the map \n    auto mit = m_textureMap.find(texturePath); \n\n    //check if its not in the map \n    if (mit == m_textureMap.end()) \n    { \n      //Load the texture \n      GLTexture newTexture = ImageLoader::LoadPNG(texturePath); \n\n      //Insert it into the map \n      m_textureMap.insert(std::make_pair(texturePath, newTexture)); \n\n      //std::cout << \"Loaded Texture!\\n\"; \n      return newTexture; \n    } \n    //std::cout << \"Used Cached Texture!\\n\"; \n    return mit->second; \n  }\n```\n\n这个实现看起来与我们之前看到的`AudioManager`示例非常相似。这里要注意的主线是调用`ImageLoader`类加载图像文件的那一行，`GLTexture newTexture = ImageLoader::LoadPNG(texturePath);`。这个调用是类的重载方面，正如您所看到的，我们再次抽象了底层系统，并简单地提供了一个`GLTexture`作为我们的`GetTexture`类的返回类型。让我们跳到下一节，看看`ImageLoader`类的实现。\n\n# ImageLoader 类\n\n现在我们已经有了将我们的纹理对象传递回调用资源管理器的结构，我们需要实现一个实际加载图像文件的类。`ImageLoader`就是那个班。它将处理纹理的加载、处理和创建。这个简单的例子将加载一个**便携式网络图形** ( **PNG** )格式的图像。\n\n因为我们在这里关注的是素材管道的结构，所以我将继续关注这个类的核心部分。我将假设一些 OpenGL 的缓冲和纹理创建的知识。如果你不熟悉 OpenGL，我强烈推荐 OpenGL 圣经系列作为很好的参考。稍后，当我们在未来的章节中查看一些高级渲染和动画技术时，我们将会看到其中的一些特性。\n\n对于这个例子，`ImageLoader.h`文件只有一个`LoadPNG`函数的声明。该函数接受一个参数，即图像文件的路径，它将返回一个`GLTexture`。以下是`ImageLoader`的全部内容:\n\n```cpp\n#pragma once \n#include \"GLTexture.h\" \n#include <string> \nnamespace BookEngine \n{ \n  class ImageLoader \n  { \n  public: \n    static GLTexture LoadPNG(std::string filePath);\n    static GLTexture LoadDDS(const char * imagepath);\n  }; \n} \n```\n\n接下来是实现，在`ImageLoader.cpp`文件内部，让我们浏览一下`LoadPNG`功能:\n\n```cpp\n... \n  GLTexture ImageLoader::LoadPNG(std::string filePath) { \nunsigned long width, height;     \nGLTexture texture = {}; \nstd::vector<unsigned char> in; \n  std::vector<unsigned char> out; \n```\n\n我们做的第一件事是创建一些临时变量来保存我们的工作数据。用于`height`和`width`的未签名`long`，一个`GLTexture`对象，然后我们将其所有字段初始化为`0`。然后我们有两个无符号字符的向量容器。`in`矢量将是存放从巴布亚新几内亚读入的原始编码数据的容器。`out`向量将保存已转换的解码数据。\n\n```cpp\n  ... \n  //Read in the image file contents into a buffer \n    if (IOManager::ReadFileToBuffer(filePath, in) == false) {\n      throw Exception(\"Failed to load PNG file to buffer!\");\n    }\n\n    //Decode the .png format into an array of pixels\n    int errorCode = DecodePNG(out, width, height, &(in[0]), in.size());\n    if (errorCode != 0) {\n      throw Exception(\"decodePNG failed with error: \" + std::to_string(errorCode));\n    }\n  ... \n```\n\n接下来我们有两个函数调用。首先我们调用一个函数，该函数使用`IOManager`类`ReadFileToBuffer`函数读入图像文件的原始数据。我们通过了`pathToFile`，而矢量在；然后，该函数将使用原始编码数据填充向量。第二个调用是`DecodePNG`功能；这是对我之前提到的单一函数库的调用。这个库将处理原始数据的读取、解码以及用解码数据填充外部向量容器。该函数采用四个参数:\n\n*   第一个是保存解码数据的向量，在我们的例子中是`out`向量\n*   第二个是`width`和`height`变量，`DecodePNG`函数将使用图像值填充这些变量\n*   第三个是对保存编码数据的容器的引用，在我们的例子中是`in`向量\n*   最后一个参数是缓冲区的大小，矢量的大小`in`\n\n这两个调用是这个类的主要部分，它们完成了构成我们素材管道的图像加载组件的系统。我们现在不会深入阅读原始数据和解码。在下一节中，我们将看到加载 3D 模型的类似技术，在这里我们将详细了解如何读取和解码数据。\n\n这个函数的其余部分将在 OpenGL 中处理图像的上传和处理，同样，我不会在这个函数的这一部分花费时间。随着我们的前进，我们将看到更多的 OpenGL 框架的调用，届时我将深入探讨。这个例子是专门为 OpenGL 构建的，但是它很容易被更通用的代码或者特定于另一个图形库的代码所取代。\n\n减去`IOManger`和`DecodePNG`类，这就完成了素材管道的图像处理。希望你能看到，有一个合适的结构，就像我们已经看到的，允许在引擎盖下有很大的灵活性，同时提供一个简单的界面，需要很少的底层系统的知识。\n\n现在我们有一个简单的一行调用返回的纹理，`ResourceManger::GetTexture(std::string pathToTextureFile)`，让我们把这个例子完整的循环，看看我们如何插入这个系统，从加载的纹理创建一个`Sprite` (2D 图像):\n\n```cpp\nvoid Sprite::Init(float x, float y, float width, float height, std::string texturePath) { \n        //Set up our private vars \n        m_x = x; \n        m_y = y; \n        m_width = width; \n        m_height = height; \n\n        m_texture = ResourceManager::GetTexture(texturePath); \n```\n\n在纹理示例项目中，跳转到`Sprite`类，如果我们关注`Init()`，我们会看到我们的简单界面允许我们调用`ResourceManager`类`GetTexture`来返回处理后的图像。就是这样，很简单！当然，这不仅限于精灵，我们可以使用这个功能来加载纹理用于其他用途，例如建模和图形用户界面用途。我们还可以扩展这个系统来加载更多的文件，而不仅仅是巴布亚新几内亚的文件，事实上，我建议您花一些时间来构建更多的文件格式，如 DDS、BMP、JPG 和其他。`ResourceManager`本身有很大的提升和成长空间。这种基本结构对于其他素材来说很容易重复，例如声音、3D 模型、字体和其他所有东西。在下一节中，我们将深入一点，看看通常所说的 3D 模型或网格的加载。\n\n要看到整个系统在工作，运行纹理示例项目，您将看到一个非常好的太阳图像，由美国宇航局的好心人提供。\n\n以下是 Windows 上的输出:\n\n>![](img/3a736c0d-67d0-48d3-af09-1d555fcdf520.png)\n\n以下是 macOS 上的输出:\n\n![](img/aa594644-04dd-475e-8dbf-230a26edc3bf.png)\n\n# 导入模型–网格\n\n模型或网格是三维空间中对象的表示。这些模型可以是任何东西，从玩家的角色到一个小的风景物体，如桌子或椅子。加载和操作这些对象是游戏引擎和底层系统的重要组成部分。在本节中，我们将研究在三维网格中加载的过程。我们将浏览一个用三维术语描述对象的简单文件格式。我们将了解如何加载这种文件格式，并将其解析为可读格式，以便与图形处理器共享。最后，我们将讨论 OpenGL 用来渲染对象的步骤。让我们直接开始`Mesh`课:\n\n```cpp\nnamespace BookEngine \n{ \n  class Mesh \n  { \n  public: \n    Mesh(); \n    ~Mesh(); \n    void Init(); \n    void Draw(); \n  private: \n    GLuint m_vao; \n    GLuint m_vertexbuffer; \n    GLuint m_uvbuffer; \n    GLTexture m_texture;   \n\n    std::vector<glm::vec3> m_vertices; \n    std::vector<glm::vec2> m_uvs; \n    std::vector<glm::vec3> m_normals; \n    // Won''''t be used at the moment. \n  }; \n} \n```\n\n我们的`Mesh`类声明文件，`Mesh.h`，还是蛮简单的。我们有`normal`构造器和析构器。然后我们又有两个功能公开为`public`。`Init()`功能，它将初始化所有的`Mesh`组件，以及`Draw`功能，它将进行实际处理，将信息传递给渲染器。在`private`声明中，我们有一堆变量来保存网格的数据。首先是`GLuint m_vao`变量。这个变量将持有一个 OpenGL 顶点数组对象的句柄，我现在不会详细讨论这个，请参考 OpenGL 文档进行快速分解。\n\n接下来的两个变量`GLuint`、`m_vertexbuffer`和`m_uvbuffer`就像它们的名字一样，是`vertex`和`uv`信息的数据缓冲区。在接下来的实现中详细介绍这一点。在缓冲区之后，我们有一个`GLTexture`变量`m_texture`。您会记得以前的对象类型；这将容纳网格的纹理。最后三个变量是`glm vec3`的向量。这些是`Mesh`的`vertices`、纹理`uvs`和`normal`的笛卡尔坐标。在当前示例中，我们将不使用正常值。\n\n这让我们很好地理解了我们的`Mesh`课需要什么；现在我们可以开始实施了。我们将走完这堂课，当其他课出现时，我们将转移到其他课。让我们从`Mesh.cpp`文件开始:\n\n```cpp\nnamespace BookEngine \n{ \n  Mesh::Mesh() \n  { \n    m_vertexbuffer = 0; \n    m_uvbuffer = 0; \n    m_vao == 0; \n  }\n```\n\n`Mesh.cpp`文件从构造函数实现开始。`Mesh`构造函数将两个缓冲区和顶点数组对象的值设置为零。我们这样做是为了稍后进行一个简单的检查，看看它们是否已经初始化或删除，接下来我们将看到:\n\n```cpp\nOBJModel::~OBJModel() \n  { \n    if (m_vertexbuffer != 0) \n      glDeleteBuffers(1, &m_vertexbuffer); \n    if (m_uvbuffer != 0)  \n      glDeleteBuffers(1, &m_uvbuffer); \nif (m_vao != 0) \n      glDeleteVertexArrays(1, &m_vao); \n  } \n```\n\n`Mesh`类的析构函数处理`Buffer`和`Vertex`数组的删除。我们做了一个简单的检查，看看它们是否没有设置为零，这意味着它们已经被创建，然后删除它们，如果它们没有:\n\n```cpp\nvoid OBJModel::Init() \n  {   \n    bool res = LoadOBJ(\"Meshes/Dwarf_2_Low.obj\", m_vertices, m_uvs, m_normals); \n    m_texture = ResourceManager::GetTexture(\"Textures/dwarf_2_1K_color.png\"); \n```\n\n进入`Init()`功能，我们开始加载我们的素材。这里我们使用一个熟悉的辅助函数`ResourceManager`类`GetTexture`函数来描述我们的模型需要的纹理。我们还加载了`Mesh`，在本例中是由仙女座 vfx 在[TurboSquid.com](https://www.turbosquid.com/)上提供的名为`Dwarf_2_Low.obj`的 OBJ 格式模型。这是通过使用`LoadOBJ`功能实现的。让我们跳出我们的`Mesh`类一分钟，看看这个功能是如何实现的。\n\n在`MeshLoader.h`文件中，我们看到了`LoadOBJ`函数的声明:\n\n```cpp\nbool LoadOBJ( \n    const char * path, \n    std::vector<glm::vec3> & out_vertices, \n    std::vector<glm::vec2> & out_uvs, \n    std::vector<glm::vec3> & out_normals \n  ); \n```\n\n`LoadOBJ`函数接受四个参数、要加载的 OBJ 文件的文件路径和三个向量，这三个向量将填充 OBJ 文件中的数据。该函数还有一个布尔类型的返回，这是为了一个简单的错误检查能力。\n\n在我们继续之前，看看这个函数是如何组合在一起的，以及它将如何解析数据来填充我们创建的向量，了解我们正在使用的文件的结构是很重要的。幸运的是，OBJ 文件是一个开放的文件格式，实际上可以在任何文本编辑器中以纯文本阅读。您也可以用 OBJ 格式手工创建非常简单的模型。举个例子，让我们看看在文本编辑器中看到的`cube.obj`文件。侧注，可以在 Visual Studio 中查看一个 OBJ 格式的模型三维渲染；它甚至有基本的编辑工具:\n\n```cpp\n# Simple 3D Cube Model \nmtllib cube.mtl \nv 1.000000 -1.000000 -1.000000 \nv 1.000000 -1.000000 1.000000 \nv -1.000000 -1.000000 1.000000 \nv -1.000000 -1.000000 -1.000000 \nv 1.000000 1.000000 -1.000000 \nv 0.999999 1.000000 1.000001 \nv -1.000000 1.000000 1.000000 \nv -1.000000 1.000000 -1.000000 \nvt 0.748573 0.750412 \nvt 0.749279 0.501284 \nvt 0.999110 0.501077 \nvt 0.999455 0.750380 \nvt 0.250471 0.500702 \nvt 0.249682 0.749677 \nvt 0.001085 0.750380 \nvt 0.001517 0.499994 \nvt 0.499422 0.500239 \nvt 0.500149 0.750166 \nvt 0.748355 0.998230 \nvt 0.500193 0.998728 \nvt 0.498993 0.250415 \nvt 0.748953 0.250920 \nvn 0.000000 0.000000 -1.000000 \nvn -1.000000 -0.000000 -0.000000 \nvn -0.000000 -0.000000 1.000000 \nvn -0.000001 0.000000 1.000000 \nvn 1.000000 -0.000000 0.000000 \nvn 1.000000 0.000000 0.000001 \nvn 0.000000 1.000000 -0.000000 \nvn -0.000000 -1.000000 0.000000 \nusemtl Material_ray.png \ns off \nf 5/1/1 1/2/1 4/3/1 \nf 5/1/1 4/3/1 8/4/1 \nf 3/5/2 7/6/2 8/7/2 \nf 3/5/2 8/7/2 4/8/2 \nf 2/9/3 6/10/3 3/5/3 \nf 6/10/4 7/6/4 3/5/4 \nf 1/2/5 5/1/5 2/9/5 \nf 5/1/6 6/10/6 2/9/6 \nf 5/1/7 8/11/7 6/10/7 \nf 8/11/7 7/12/7 6/10/7 \nf 1/2/8 2/9/8 3/13/8 \nf 1/2/8 3/13/8 4/14/8 \n```\n\n如您所见，这些文件中包含了大量数据。记住这只是一个简单的立方体模型。看看矮人 OBJ 的文件，对其中包含的数据有更深入的了解。对我们来说重要的部分是`v`、`vt`、`vn`和`f`线。`v`线描述的是`Mesh`的几何顶点，即模型在局部空间中的`x`、`y`、`z`值(原点相对于模型本身的坐标)。`vt`线描述了模型的纹理顶点，这一次的值是归一化的 x 和 y 坐标，归一化意味着它们是介于`0`和`1`之间的值。`vn`线是顶点法线的描述，我们不会在当前示例中使用这些，但是这些值给出了垂直于顶点的归一化矢量单位。在计算光照和阴影等问题时，这些都是非常有用的值。下图描绘了十二面体形状网格的顶点法线:\n\n![](img/ad51a2a6-61ac-40c0-a653-2051ae5fa0e2.png)\n\n最后一组线`f`线描述了网格的面。这是三个矢量值的组，构成网格的一个面，即三角形。这些也是局部空间 x，y 和 z 坐标。\n\n该文件一旦在我们的示例引擎中呈现，将如下所示:\n\n![](img/86bfbce9-6b72-4de1-a863-f627e9343062.png)\n\n好了，简单来说，这就是 OBJ 文件格式，现在让我们继续，看看我们将如何解析这些数据，并将其存储在缓冲区中，供渲染器使用。在`MeshLoader.cpp`文件中，我们找到了`LoadOBJ()`功能的实现:\n\n```cpp\n... \nbool LoadOBJ( \n    std::string path, \n    std::vector<glm::vec3> & out_vertices, \n    std::vector<glm::vec2> & out_uvs, \n    std::vector<glm::vec3> & out_normals \n    )  \n{ \n    WriteLog(LogType::RUN, \"Loading OBJ file \" + path + \" ...\"); \n    std::vector<unsigned int> vertexIndices, uvIndices, normalIndices; \n    std::vector<glm::vec3> temp_vertices; \n    std::vector<glm::vec2> temp_uvs; \n    std::vector<glm::vec3> temp_normals; \n```\n\n为了启动`LoadOBJ`功能，创建了几个保持器变量。变量声明的第一行是一组三个整数向量。这些将保存`vertices`、`uvs`和`normals`的指数。在指数之后，我们还有三个向量。两个`vec3`向量用于`vertices`和`normal`，一个`vec2`向量用于`uvs`。这些向量将保存每个向量的临时值，允许我们执行一些计算:\n\n```cpp\n    try  \n{ \nstd::ifstream in(path, std::ios::in); \n```\n\n接下来，我们启动一个`try`块，它将容纳函数的核心逻辑。我们这样做是为了在出现任何问题时抛出一些异常，并在这个函数结束时在内部捕获它们。`std::ifstream in(path, std::ios::in);`块中的第一行试图在我们传入的位置加载文件。您可能已经注意到，`ifstream`是标准库的一部分，用于定义一个流对象，该对象可用于从文件中读入字符数据。在现代 I/O 系统中很常见看到`ifstream`，它是常见的`fopen`的 C++ 替代品，实际上是 C:\n\n```cpp\nif (!in) {\nthrow Exception(\"Error opening OBJ file: \" + path); }\n```\n\n然后我们可以用简单的 if 语句`if(!in)`测试加载文件是否有错误，这与直接检查状态标志如`in.bad() == true; or in.fail() == true`是一样的。如果我们确实遇到错误，我们会抛出一个带有调试消息的异常。我们稍后在函数中处理这个异常:\n\n```cpp\nstd::string line; \nwhile (std::getline(in, line)) \n  { \n```\n\n接下来，我们需要创建一个循环，这样我们就可以遍历文件并根据需要解析数据。我们使用`std::getline(in, line)`函数作为参数的`while()`循环来实现这一点。`std::getline`返回一行字符，直到到达行字符的末尾。`parameters std::getline()`取值是包含字符的流，在我们的例子中是`in`和保存函数输出的`std::string`对象。\n\n通过使用这个作为`while`循环的条件参数，我们将继续一行一行地遍历输入文件，直到到达文件的末尾。条件变为假的时间，我们将停止循环。这是一种非常方便的方法，可以遍历文件进行解析:\n\n```cpp\n  if (line.substr(0, 2) == \"v \") { \n    std::istringstream v(line.substr(2)); \n    glm::vec3 vert; \n    double x, y, z; \n    v >> x; v >> y; v >> z; \n    vert = glm::vec3(x, y, z); \n    temp_vertices.push_back(vert); \n  } \n```\n\n在我们的`while`循环中，我们首先要尝试和解析的是 OBJ 文件中的顶点数据。如果你还记得我们之前的解释，顶点数据包含在一条直线上，用`v`表示。然后为了解析我们的顶点数据，我们应该首先测试该线是否是顶点(`v`)线。`std::string()`对象有一个方便的方法，允许您从字符串中选择定义数量的字符。这个方法就是`substr()`，`substr()`方法可以取两个参数，字符串中字符的起始位置和结束位置。这将创建一个子字符串对象，然后我们可以对其进行测试。\n\n在这个例子中，我们使用`substr()`方法获取字符串的前两个字符，行，然后测试它们是否匹配字符串`\"v \"`(注意空格)。如果这个条件是`true`，那就意味着我们有了一条顶点线，然后可以把它解析成对我们的系统有用的形式。\n\n代码很容易解释，但是让我们突出一些重要的部分。首先是`std::istringstream`对象`v`。`stringstream`是一个特殊的对象，它为字符串缓冲区提供了一种方便的方式来操作字符串，就像它是一个输入/输出对象(`std::cout`)一样。这意味着您可以使用`>>`和`<<`运算符将其视为一个流，也可以使用`str()`方法将其视为一个`std::string`流。我们使用字符串流对象来存放新的字符集合。这些新字符由对`line.substr(2)`的方法调用提供。这一次，通过仅将一个参数`2`传递给`substr`方法，我们告诉它从第二个字符开始返回该行的剩余部分。这样做是返回顶点线的值`x`、`y`和`z`，而不返回`v`表示。一旦我们有了这个新的字符集合，我们就可以逐步遍历每个字符，并将其分配给与之匹配的双变量。如您所见，这是我们使用字符串流对象的独特性质将字符流输出到其变量`v >> x;` `v >> y; v >> x;`行的地方。在`if`语句的末尾，我们将这些`x`、`y`、`z`变成一个`vec3`，最后将新创建的`vec3`推到 temp `vertices`向量的后面:\n\n```cpp\nelse if (line.substr(0, 2) == \"vt\")  \n{ \nstd::istringstream v(line.substr(3)); \n          glm::vec2 uv; \n          double U, V; \n          v >> U;v >> V; \n          uv = glm::vec2(U, V); \n          uv.y = -uv.y; \n          temp_uvs.push_back(uv); \n        } \n```\n\n对于纹理，我们做很多相同的事情。除了检查`\"vt\"`之外，主要的区别是我们只寻找两个值，或者`vec2`向量。这里的另一个注意点是我们反转`v`坐标，因为我们使用的是纹理格式，这是反转的。如果要使用 TGA 或 BMP 格式加载程序，请删除:\n\n```cpp\n        else if (line.substr(0, 2) == \"vn\") \n { \n\n          std::istringstream v(line.substr(3)); \n          glm::vec3 normal; \n          double x, y, z; \n          v >> x;v >> y;v >> z; \n          normal = glm::vec3(x, y, z); \n          temp_normals.push_back(normal); \n        } \n```\n\n对于法线，我们做的和顶点完全一样，但是寻找`vn`线:\n\n```cpp\n\n        else if (line.substr(0, 2) == \"f \") \n        { \n          unsigned int vertexIndex[3], uvIndex[3], normalIndex[3]; \n          const char* cstring = line.c_str(); \n          int matches = sscanf_s(cstring, \"f %d/%d/%d %d/%d/%d %d/%d/%d\\n\", &vertexIndex[0], &uvIndex[0], &normalIndex[0], &vertexIndex[1], &uvIndex[1], &normalIndex[1], &vertexIndex[2], &uvIndex[2], &normalIndex[2]); \n```\n\n对于面，一个三角形的集合，我们做一些稍微不同的事情。首先我们检查一下是否有`\"f \"`线。如果是的话，我们设置一些数组来保存`vertex`、`uv`和`normal`的索引。然后，我们将我们的`std::string`行转换成一个字符数组，它被称为 C 字符串，行为`const char* cstring = line.c_str();`。然后，我们使用另一个 C 函数`sscanf_s`来解析实际的字符串，并将每个字符分离到特定的索引数组元素中。一旦该语句结束，`sscanf_s()`函数将返回元素集合的一个整数值，我们将其赋予匹配的变量:\n\n```cpp\nif (matches != 9) \n    throw Exception(\"Unable to parse format\"); \n```\n\n然后，我们使用`matches`变量来检查并查看它是否等于`9`，这意味着我们有九个元素，并且它是我们可以使用的格式。如果 matches 的值不是`9`，这意味着我们有一个格式，我们没有设置来处理，所以我们抛出一个简单的调试消息异常:\n\n```cpp\n          vertexIndices.push_back(vertexIndex[0]); \n          vertexIndices.push_back(vertexIndex[1]); \n          vertexIndices.push_back(vertexIndex[2]); \n          uvIndices.push_back(uvIndex[0]); \n          uvIndices.push_back(uvIndex[1]); \n          uvIndices.push_back(uvIndex[2]); \n          normalIndices.push_back(normalIndex[0]); \n          normalIndices.push_back(normalIndex[1]); \n          normalIndices.push_back(normalIndex[2]); \n        } \n      }\n```\n\n在`\"f \"`或 face line if 语句中，我们做的最后一件事是获取所有分离的元素，并将它们推入相应的索引向量中。接下来，我们使用这些值来构建实际的网格数据:\n\n```cpp\n      for (unsigned int i = 0; i < vertexIndices.size(); i++)  \n{ \n        // Get the indices of its attributes \n        unsigned int vertexIndex = vertexIndices[i]; \n        unsigned int uvIndex = uvIndices[i]; \n        unsigned int normalIndex = normalIndices[i]; \n```\n\n为了创建最终的网格数据以给出输出向量，我们创建了另一个循环来遍历模型数据，这次使用 for 循环和顶点数量作为条件。然后我们创建三个变量来保存每个`vertex`、`uv`和`normal`的当前指数。每次我们通过这个循环，我们将这个索引设置为`i`的值，该值通过以下步骤递增:\n\n```cpp\n        glm::vec3 vertex = temp_vertices[vertexIndex - 1]; \n        glm::vec2 uv = temp_uvs[uvIndex - 1]; \n        glm::vec3 normal = temp_normals[normalIndex - 1]; \n```\n\n然后，由于这些索引值，我们可以获得每个`vertex`、`uv`和`normal`的属性。我们将这些设置在`vec2`或`vec3`中，这是我们需要的输出向量:\n\n```cpp\n        out_vertices.push_back(vertex); \n        out_uvs.push_back(uv); \n        out_normals.push_back(normal); \n      } \n    } \n```\n\n最后，最后一步是将这些新值推送到它们特定的输出向量中:\n\n```cpp\n    catch (Exception e) \n    { \n      WriteLog(LogType::ERROR, e.reason); \n      return false; \n    } \n    return true; \n  } \n  ...\n```\n\n最后，我们有`catch`区块来从顶部匹配`try`区块。这个捕获非常简单，我们从传入的`Exception`对象中获取原因成员对象，并使用它将调试消息打印到错误日志文件中。我们还从`LoadOBJ()`函数返回 false，让调用对象知道有错误。如果没有什么可捕捉的，我们只需返回 true，让调用对象知道一切都按预期进行。我们现在准备使用这个函数来加载我们的 OBJ 文件，并为渲染系统生成有用的数据。\n\n现在，回到`Mesh.cpp`文件，我们将继续使用这个加载的数据，用示例引擎绘制模型。我不会在每个函数上花费太多时间，这也是特定于 OpenGL API 的，但是可以用更通用的方式编写，或者使用另一个图形库，如 DirectX:\n\n```cpp\n    if (m_vao == 0)  \n      glGenVertexArrays(1, &m_vao); \n    glBindVertexArray(m_vao); \n```\n\n这里我们检查顶点数组对象是否已经生成；如果没有，我们继续使用我们的`m_vao`作为参考对象制作一个。接下来我们绑定 VAO，这将允许我们在这个类的所有后续 OpenGL 调用中使用它:\n\n```cpp\n    if (m_vertexbuffer == 0) \nglGenBuffers(1, &m_vertexbuffer); \n    if (m_uvbuffer == 0)  \n      glGenBuffers(1, &m_uvbuffer); \n```\n\n接下来，我们检查我们的顶点缓冲区是否已经创建；如果没有，我们使用`m_vertexbuffer`变量作为被引用对象来创建一个。我们对`uvbuffer`也是如此:\n\n```cpp\n    glBindBuffer(GL_ARRAY_BUFFER, m_vertexbuffer); \n    glBufferData(GL_ARRAY_BUFFER, m_vertices.size() * sizeof(glm::vec3), &m_vertices[0], GL_STATIC_DRAW); \n    glBindBuffer(GL_ARRAY_BUFFER, m_uvbuffer); \n    glBufferData(GL_ARRAY_BUFFER, m_uvs.size() * sizeof(glm::vec2), &m_uvs[0], GL_STATIC_DRAW); \n  }\n```\n\n我们在`Meshes Init()`功能中做的最后一件事是绑定`vertex`和`uv`缓冲区，然后使用 OpenGL、`glBindBuffer()`和`glBufferData()`功能将数据上传到显卡上的那些缓冲区。有关这些功能的更多详细信息，请查看 OpenGL 文档:\n\n```cpp\n  void Mesh::Draw() \n  {   \n    glActiveTexture(GL_TEXTURE0); \n    glBindTexture(GL_TEXTURE_2D, m_texture.id); \n```\n\n对于`Mesh`类`Draw()`函数，我们开始在 OpenGL API 框架中设置纹理。我们通过函数调用`glActiveTexture()`和`glBindTexture()`来实现这一点，前者激活纹理，后者实际绑定内存中的纹理数据:\n\n```cpp\n    glBindBuffer(GL_ARRAY_BUFFER, m_vertexbuffer); \n    glVertexAttribPointer( 0,  3,  GL_FLOAT,  GL_FALSE,  0, (void*)0); \n    glBindBuffer(GL_ARRAY_BUFFER, m_uvbuffer); \n    glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 0, (void*)0); \n```\n\n接下来，我们绑定缓冲区，并为顶点数据和纹理坐标数据设置属性。同样，我不会在这里关注细节，代码有注释来解释每个参数。有关函数的更多信息，我建议在线查看 OpenGL 文档。\n\n```cpp\n    glDrawArrays(GL_TRIANGLES, 0, m_vertices.size()); \n```\n\n数据全部绑定，属性全部设置好之后，我们就可以调用函数实际绘制`Mesh`对象了。在这种情况下，我们使用`glDrawArrays()`功能，传入`GL_TRIANGLES`作为绘图方法。这意味着我们希望使用三角形来渲染顶点数据。为了好玩，尝试将该值更改为`GL_POINTS`。\n\n```cpp\n    glDisableVertexAttribArray(0); \n    glDisableVertexAttribArray(1); \n    glBindBuffer(GL_ARRAY_BUFFER, 0); \n  } \n}\n```\n\n在抽奖结束时，我们还有最后一步要完成，那就是清理。每次调用 OpenGL 绘图后，需要禁用已设置的已用属性，并解除已用缓冲区的绑定。`glDisableVertexAttribArray()`和`glBindBuffer()`功能用于这些任务。\n\n在`GameplayScreen.cpp`文件中，我们添加我们的调用来初始化模型:\n\n```cpp\n ... \n//Init Model \n  m_model.Init(\"Meshes/Dwarf_2_Low.obj\", \"Textures/dwarf_2_1K_color.png\"); \n  ... \n```\n\n然后，我们可以通过简单地在`GameplayScreen`的`Draw()`函数中添加对模型的`Draw()`函数的调用来开始绘制它:\n\n```cpp\n  ... \n//Draw Model \n  m_model.Draw(); \n... \n```\n\n就这样！如果运行`ModelExample`，屏幕上会看到矮人模型的输出。我还在游戏中添加了一个简单的 3D 相机，这样你就可以在模型周围移动。`W`、`A`、`S`和`D`用于在游戏空间中上下左右移动相机。用鼠标四处看看。\n\n以下是 Windows 上的输出:\n\n![](img/3b10abf5-6f28-4d57-9bda-a5ada9fa57a4.png)\n\n以下是 macOS 上的输出:\n\n![](img/f83fd587-2824-447e-867f-26f6bff4ddee.png)\n\n# 摘要\n\n在本章中，我们讲述了开发的一个非常重要的部分，素材的处理。我们看了一下导入、处理和管理内容(如声音、图像和 3D 对象)的过程。有了这个基础系统，我们可以继续完善游戏开发所需的其他系统。\n\n在下一章中，我们将研究开发所需的核心游戏系统，包括状态系统、物理、相机和图形用户界面/平显系统。"
  },
  {
    "path": "docs/master-cpp-game-dev/05.md",
    "content": "# 五、构建游戏系统\n\n在我们的旅程中，我们已经到了能够开始将我们将用来驱动我们的游戏和工具的各种系统拼凑在一起的地步。这些系统是引擎的一部分，为与我们现在能够导入游戏的所有惊人素材的交互提供动力:\n\n*   理解状态\n*   设计摄像系统\n*   研究物理\n\n# 理解状态\n\n我们以许多不同的方式使用状态。它们可以用于控制游戏流程，处理角色的不同行为和反应方式，甚至用于简单的菜单导航。不用说，状态是强大且可管理的代码库的重要要求。\n\n有许多不同类型的状态机；我们将在本节中重点介绍的是**有限状态机(FSM)** 模式。你们中善于观察的读者会注意到，我们已经在已经实现的屏幕系统的功能中看到了一个有限状态机模式。事实上，我们将在这里创建的与为该系统创建的非常相似，只是有一些关键的区别，这将使它成为一个更通用和灵活的状态机。\n\n有几种方法可以在游戏中实现简单的状态机。一种方法是简单地使用一个开关盒来控制状态和状态类型的`enum`结构。这方面的一个例子如下:\n\n```cpp\nenum PlayerState \n{ \n    Idle, \n      Walking \n} \n... \nPlayerState currentState = PlayerState::Idle; //A holder variable for the state currently in \n... \n// A simple function to change states \nvoid ChangeState(PlayState nextState) \n{ \n    currentState = nextState; \n} \nvoid Update(float deltaTime) \n{ \n    ... \n    switch(currentState) \n{ \n    case PlayerState::Idle: \n        ... //Do idle stuff \n        //Change to next state \nChangeState(PlayerState::Walking); \nbreak; \n        case PlayerState::Walking: \n            ... //Do walking stuff \n            //Change to next state \n            ChangeState(PlayerState::Idle); \nbreak; \n    } \n    ... \n} \n```\n\n像这样使用开关/外壳在很多情况下都是有效的，但是它也有一些很大的缺点。如果我们决定再增加几个州呢？如果我们决定增加分支和更多的`if`条件句呢？\n\n我们最开始使用的简单开关/外壳突然变得非常大，无疑也很笨重。每次我们想要做一个改变或者增加一些功能，我们就增加了复杂性，并且引入了更多的 bug。我们可以通过采用稍微不同的方法并使用类来表示我们的状态，来帮助缓解其中的一些问题并提供更多的灵活性。通过使用继承和多态性，我们可以构建一个结构，允许我们将状态链接在一起，并提供在许多情况下重用它们的灵活性。\n\n让我们从将来继承的基类`IState`开始，在演示示例中演示如何实现这一点:\n\n```cpp\n... \nnamespace BookEngine \n{ \n    class IState { \n    public: \n        IState() {} \n        virtual ~IState(){} \n        // Called when a state enters and exits  \n        virtual void OnEntry() = 0; \n        virtual void OnExit() = 0; \n\n        // Called in the main game loop \n        virtual void Update(float deltaTime) = 0; \n    }; \n} \n```\n\n如您所见，这只是一个非常简单的类，它有一个构造函数、一个虚拟析构函数和三个完全虚拟的函数，每个继承的状态都必须重写这些函数。`OnEntry`，在首次进入状态时将被调用，每次状态改变只执行一次。`OnExit`和`OnEntry`一样，每次状态改变只执行一次，当状态即将退出时调用。最后一个功能是`Update`功能；这将在每个游戏循环中调用一次，并将包含状态的大部分逻辑。虽然这看起来很简单，但它为我们构建更复杂的状态提供了一个很好的起点。现在，让我们在示例中实现这个基本的`IState`类，看看如何使用它来满足状态机的一个常见需求:创建游戏状态。\n\n首先，我们将创建一个名为`GameState`的新类，它将从`IState`继承。这将是我们游戏需要的所有状态的新基类。`GameState.h`文件包括以下内容:\n\n```cpp\n#pragma once \n#include <BookEngine\\IState.h> \nclass GameState : BookEngine::IState \n{ \npublic: \n    GameState(); \n    ~GameState(); \n    //Our overrides \n    virtual void OnEntry() = 0; \n    virtual void OnExit() = 0; \n    virtual void Update(float deltaTime) = 0; \n    //Added specialty function \n    virtual void Draw() = 0; \n}; \n```\n\n`GameState`类非常像它继承的`IState`类，除了一个关键的区别。在这个类中，我们添加了一个新的虚拟方法`Draw()`，所有类现在将从`GameState`继承的虚拟方法将被实现。每次我们使用`IState`并创建一个新的专门化基类、玩家状态、菜单状态等等，我们都可以添加这些新的功能来根据状态机的要求对其进行定制。这就是我们如何使用继承和多态来创建更复杂的状态和状态机。\n\n继续我们的例子，现在让我们创建一个新的`GameState`。我们从创建一个名为`GameWaiting`的继承自`GameState`的新类开始。为了更容易理解，我将所有新的`GameState`继承类组合成一组文件`GameStates.h`和`GameStates.cpp`。`GamStates.h`文件如下所示:\n\n```cpp\n#pragma once \n#include \"GameState.h\" \n\nclass GameWaiting: GameState \n{ \n    virtual void OnEntry() override; \n    virtual void OnExit() override; \n    virtual void Update(float deltaTime) override; \n    virtual void Draw() override; \n}; \n\nclass GameRunning: GameState \n{ \n    virtual void OnEntry() override; \n    virtual void OnExit() override; \n    virtual void Update(float deltaTime) override; \n    virtual void Draw() override; \n}; \n\nclass GameOver : GameState \n{ \n    virtual void OnEntry() override; \n    virtual void OnExit() override; \n    virtual void Update(float deltaTime) override; \n    virtual void Draw() override; \n}; \n```\n\n这里没有什么新鲜事；我们只是为我们的每个`GameState`类声明函数。现在，在我们的`GameStates.cpp`文件中，我们可以实现前面代码中描述的每个单独状态的功能:\n\n```cpp\n#include \"GameStates.h\" \n    void GameWaiting::OnEntry() \n{ \n...  \n//Called when entering the GameWaiting state's OnEntry function \n... \n} \n\nvoid GameWaiting::OnExit() \n{ \n...  \n//Called when entering the GameWaiting state's OnEntry function \n... \n} \n\nvoid GameWaiting::Update(float deltaTime) \n{ \n...  \n//Called when entering the GameWaiting state's OnEntry function \n... \n\n} \n\nvoid GameWaiting::Draw() \n{ \n...  \n//Called when entering the GameWaiting state's OnEntry function \n... \n\n} \n...  \n//Other GameState implementations  \n... \n```\n\n为了页面空间，我只展示了`GameWaiting`的实现，但是其他状态也是如此。每一个都有自己独特的实现这些功能，这允许您控制代码流，并根据需要实现更多的状态，而不会创建难以遵循的代码路径迷宫。\n\n现在我们已经定义了我们的状态，我们可以在我们的游戏中实现它们。当然，我们可以用许多不同的方式来做这件事。我们可以遵循与我们的屏幕系统相同的模式，实现一个`GameState`列表类，其定义如下:\n\n```cpp\n    class GameState; \n\n    class GameStateList { \n    public: \n        GameStateList (IGame* game); \n        ~ GameStateList (); \n\n        GameState* GoToNext(); \n        GameState * GoToPrevious(); \n\n        void SetCurrentState(int nextState); \n        void AddState(GameState * newState); \n\n        void Destroy(); \n\n        GameState* GetCurrent(); \n\n    protected: \n        IGame* m_game = nullptr; \n        std::vector< GameState*> m_states; \n        int m_currentStateIndex = -1; \n    }; \n} \n```\n\n或者我们可以简单地使用我们用一个简单的`enum`和一个开关盒创建的`GameState`类。状态模式的使用允许这种灵活性。在示例案例中，我选择遵循与屏幕系统相同的设计；您可以在源代码库中看到`GameStateExample`项目的完整实现。值得仔细阅读源代码，因为我们将在整本书中继续使用这些状态设计。尝试修改示例；添加一个新的状态，在屏幕上创建不同于其他状态的打印输出。您甚至可以尝试在状态中嵌套状态，以创建更强大的代码分支能力。\n\n# 使用相机\n\n在这一点上，我们已经讨论了大量的系统结构，现在已经能够继续设计与我们的游戏和 3D 环境交互的方式。这给我们带来了一个重要的话题:虚拟摄像机系统的设计。相机为我们提供了三维世界的视觉表现。这是我们沉浸其中的方式，它为我们选择的互动提供了反馈。在这一节中，我们将讨论计算机图形学中虚拟摄像机的概念。\n\n在我们开始为我们的相机编写代码之前，重要的是要充分了解它是如何工作的。让我们从能够在 3D 世界中导航的想法开始。为了做到这一点，我们需要使用所谓的转换管道。变换管道可以被认为是相对于相机视点的位置和方向变换所有对象和点所采取的步骤。下面是一个简单的图表，详细说明了转换管道的流程:\n\n![](img/3452a528-196c-4721-87af-496f5aca1405.png)\n\n从管道的第一步局部空间开始，当创建网格时，它有一个局部原点 0 x，0 y，0 z。这个局部原点通常位于对象的中心，或者在某些玩家角色的情况下，位于脚的中心。然后，组成该网格的所有点都基于该局部原点。当谈到一个没有被变换的网格时，我们称之为在局部空间中:\n\n![](img/39326e41-9c69-40b2-af8d-ee369ac280a0.png)\n\n上图描绘了模型编辑器中的 gnome 网格。这就是我们认为的局部空间。\n\n下一步，我们想把一个网格带入我们的环境，世界空间。为了做到这一点，我们必须将我们的网格点乘以所谓的模型矩阵。然后，这将把网格放置在世界空间中，这将所有网格点设置为相对于单个世界原点。世界空间最容易被认为是构成游戏环境的所有物体的布局描述。一旦网格被放置在世界空间中，我们就可以开始做一些事情，比如比较距离和角度。这一步的一个很好的例子是在世界/关卡编辑器中放置游戏对象的时候；这是相对于其他对象和单个世界原点(0，0，0)创建模型网格的描述。我们将在下一章更详细地讨论编辑器。\n\n接下来，为了在这个世界空间中导航，我们必须重新排列这些点，使它们相对于相机的位置和方向。为了实现这一点，我们执行一些简单的操作。首先是将物体平移到原点。首先，我们将摄像机从其当前的世界坐标移开。\n\n在下面的示例图中， *x* 轴上有 **20** ，在 *y* 轴上有 **2** ，在 *z* 轴上有 **-15** ，到世界原点或 **0，0，0** 。然后，我们可以通过减去摄像机的位置来映射对象，该位置是用于平移摄像机对象的值，在这种情况下将是 **-20** 、 **-2** 、 **15** 。因此，如果我们的游戏对象在 *x* 轴上的 **10.5** 处开始，在 *y* 轴上的 **1** 处开始，在 *z* 轴上的 **-20** 处开始，新平移的坐标将是 **-9.5** ， **-1** ， **-5** 。最后一个操作是旋转相机，使其面向想要的方向；在我们当前的情况下，这将指向向下- *z* 轴。对于以下示例，这意味着将对象点旋转 **-90** 度，使示例游戏对象的新位置 **5** 、 **-1** 、 **-9.5** 。这些操作组合成了所谓的视图矩阵:\n\n![](img/a47e37f1-60e7-46d4-93c7-82d8ce00cbb5.png)\n\n在我们进一步讨论之前，我想简单介绍一下处理矩阵时的一些重要细节，特别是处理矩阵乘法和运算顺序。使用 OpenGL 时，所有矩阵都是以列为主的布局定义的。相反的是行为主的布局，在其他图形库中也可以找到，比如微软的 DirectX。以下是列主视图矩阵的布局，其中 U 是指向上方的单位向量，F 是我们指向前方的向量，R 是右侧向量，P 是摄像机的位置:\n\n![](img/21b8ec10-e037-4bb2-b309-2fb5b85d7d33.png)\n\n当构建具有平移和旋转组合的矩阵时，例如前面的视图矩阵，通常不能将旋转和平移值粘贴到单个矩阵中。为了创建一个合适的视图矩阵，我们需要使用矩阵乘法将两个或多个矩阵组合成一个最终矩阵。记住我们使用的是列主符号，因此操作的顺序是从右到左。这很重要，因为使用方向(R)和平移(T)矩阵，如果我们说 V = T×R，这将产生不希望的效果，因为这将首先围绕世界原点旋转点，然后移动它们以与作为原点的相机位置对齐。我们想要的是 V = R×T，其中点将首先与相机对齐作为原点，然后应用旋转。在以行为主的布局中，当然情况正好相反:\n\n![](img/1a829f1e-437e-43fb-b03c-e69e8fdbf898.png)\n\n好消息是，我们不一定需要手动处理视图矩阵的创建。OpenGL 的旧版本和大多数现代数学库，包括 GLM，都有一个`lookAt()`函数的实现。大多数以摄像机位置、目标或观察位置以及向上方向作为参数，并返回完全创建的视图矩阵。我们将很快了解如何使用`lookAt()`函数的 GLM 实现，但是如果您想查看刚才描述的想法的完整代码实现，请查看项目源代码库中包含的 GLM 源代码。\n\n继续通过变换管道，下一步是从眼睛空间转换到同质剪辑空间。这个阶段将构建一个投影矩阵。投影矩阵负责一些事情。\n\n首先是定义近剪裁平面和远剪裁平面。这是沿定义的前向轴的可见范围(通常为 *z* )。任何落在近距离前面的东西和任何落在远距离后面的东西都被认为在射程之外。在此范围之外的任何几何对象都将在后续步骤中从管道中被*修剪掉*(移除)。\n\n二是定义**视场** ( **FOV** )。不管名字是什么，视野不是一个领域，而是一个角度。对于 FOV，我们实际上只规定了垂直范围；大多数现代游戏都使用 66 或 67 度角。一旦我们提供了长宽比(宽高比)，水平范围将由矩阵为我们计算。为了演示，长宽比为 4:3 的显示器上的 67 度垂直角度的 FOV 为 89.33 度( *67 * 4/3 = 89.33* )。\n\n这两个步骤结合起来形成一个顶部被切掉的金字塔形状的体积。这个创建的体积被称为视平截头体。落在这个平截头体之外的任何几何图形都被认为是不可见的。\n\n下图说明了视图平截头体的外观:\n\n![](img/8f7c74c5-f2d2-4d17-86f8-d91a3f38037e.png)\n\n您可能会注意到，平截头体的末端比前面有更多的可见空间。为了在 2D 屏幕上正确显示，我们需要告诉硬件如何计算视角。这是管道中的下一步。平截头体较大的远端将被推到一起，形成一个盒子形状。在这个宽端可见的物体集合也将被挤压在一起；这将为我们提供一个透视图。要理解这一点，想象一下沿着一段笔直的铁轨看的现象。随着轨道继续向远处延伸，它们看起来越来越小，靠得越来越近。\n\n定义裁剪空间后，管道中的下一步是使用所谓的透视分割将点规范化为尺寸为(-1 到 1，-1 到 1，-1 到 1)的箱形。这被称为**标准化设备空间**。通过*将*尺寸归一化为单位尺寸，我们允许将点相乘以放大或缩小到任何视口尺寸。\n\n变换管道中的最后一个主要步骤是创建将要显示的 3D 的 2D 表示。为此，我们将标准化的设备空间展平，将更远的对象绘制在更靠近相机的对象后面(绘制深度)。尺寸从 *X* 和 *Y* 归一化值缩放为视口的实际像素值。经过这一步，我们有了一个 2D 空间，称为**视口空间**。\n\n这就完成了转换管道阶段。有了这个理论，我们现在可以转向实现并编写一些代码。我们将从创建一个基本的第一人称 3D 相机开始，这意味着我们是通过玩家角色的眼睛来观察的。先说相机的头文件`Camera3D.h`，可以在源代码库中的`Chapter05`项目文件夹中找到:\n\n```cpp\n... \n#include <glm/glm.hpp> \n#include <glm/gtc/matrix_transform.hpp> \n..., \n```\n\n我们从必要的包括开始。正如我刚才提到的，GLM 包括对使用矩阵的支持，因此我们包括`glm.hpp`和`matrix_transform.hpp`来访问 GLM 的`lookAt()`功能:\n\n```cpp\n... \n   public: \n      Camera3D(); \n      ~Camera3D(); \n      void Init(glm::vec3 cameraPosition = glm::vec3(4,10,10), \n              float horizontalAngle = -2.0f,  \n              float verticalAngle = 0.0f,  \n              float initialFoV = 45.0f); \n      void Update(); \n```\n\n接下来，我们为 Camera3D 类提供了公共可访问的功能。前两个只是标准的构造函数和析构函数。然后我们有`Init()`功能。我们用为参数提供的一些默认值来声明这个函数；这样，如果没有传入任何值，我们在第一次更新调用中仍然有值来计算矩阵。这就把我们带到了下一个声明的函数，`Update()`函数。这是游戏引擎将调用每个循环来保持相机更新的功能:\n\n```cpp\nglm::mat4 GetView() { return m_view; };\nglm::mat4 GetProjection() { return m_projection; };\nglm::vec3 GetForward() { return m_forward; };\nglm::vec3 GetRight() { return m_right; };\nglm::vec3 GetUp() { return m_up; };\n```\n\n在`Update()`函数之后，有一组五个 getter 函数来返回视图和投影矩阵，以及相机的前向、向上和向右向量。为了保持实现的整洁，我们可以简单地在头文件中声明并实现这些 *getter* 函数:\n\n```cpp\nvoid SetHorizontalAngle(float angle) { m_horizontalAngle = angle; };\nvoid SetVerticalAngle(float angle) { m_verticalAngle = angle; };\n```\n\n在 getter 函数集之后，我们有两个 setter 函数。第一个将设置水平角度，第二个将设置垂直角度。这在屏幕尺寸或纵横比发生变化时非常有用:\n\n```cpp\nvoid MoveCamera(glm::vec3 movementVector) { m_position +=   movementVector; };\n```\n\nCamera3D 类中最后一个公共函数是`MoveCamera()`函数。这个简单的函数接受一个向量 3，然后将该向量累加到`m_position`变量中，该变量是当前摄像机位置:\n\n```cpp\n...\n  private:\n    glm::mat4 m_projection;\n    glm::mat4 m_view; // Camera matrix\n```\n\n对于类的私有声明，我们从两个`glm::mat4`变量开始。`glm::mat4`是 4x4 矩阵的数据类型。我们为视图或相机矩阵创建一个，为投影矩阵创建一个:\n\n```cpp\nglm::vec3 m_position;\nfloat m_horizontalAngle;\nfloat m_verticalAngle;\nfloat m_initialFoV;\n```\n\n接下来，我们有一个向量 3 变量来保存相机的位置，后面是三个浮点值——一个用于水平角度，一个用于垂直角度，还有一个变量来保存视野:\n\n```cpp\nglm::vec3 m_right;\nglm::vec3 m_up;\nglm::vec3 m_forward; \n```\n\n然后，我们还有三个矢量 3 变量类型，用于保存相机对象的右、上和前向值。\n\n现在我们已经有了三维相机类的声明，下一步是实现头文件中尚未实现的任何功能。我们只需要提供两个功能，`Init()`和`Update()`功能。让我们从`Camera3D.cpp`文件中的`Init()`功能开始:\n\n```cpp\nvoid Camera3D::Init(glm::vec3 cameraPosition, \n     float horizontalAngle, \n     float verticalAngle, \n     float initialFoV)\n   {\n     m_position = cameraPosition;\n     m_horizontalAngle = horizontalAngle;\n     m_verticalAngle = verticalAngle;\n     m_initialFoV = initialFoV;\n\n     Update();\n    }\n    ...\n\n```\n\n我们的`Init()`功能很简单；我们在函数中所做的就是接受提供的值，并将它们设置为我们声明的相应变量。一旦我们设置了这些值，我们只需调用`Update()`函数来处理新创建的摄像机对象的计算:\n\n```cpp\n...\n   void Camera3D::Update()\n   {\n      m_forward = glm::vec3(\n          glm::cos(m_verticalAngle) * glm::sin(m_horizontalAngle),\n          glm::sin(m_verticalAngle),\n          glm::cos(m_verticalAngle) * glm::cos(m_horizontalAngle)\n        );\n```\n\n`Update()`功能是完成所有课程繁重工作的地方。从计算我们相机的新前进开始。这是通过利用 GLM 余弦和正弦函数的简单公式来完成的。正在发生的是，我们正在从球面坐标转换到笛卡尔坐标，这样我们就可以在创建视图矩阵时使用该值。\n\n```cpp\n  m_right = glm::vec3(\n        glm::sin(m_horizontalAngle - 3.14f / 2.0f),\n        0,\n        glm::cos(m_horizontalAngle - 3.14f / 2.0f)\n     );  \n```\n\n在我们计算新的前向之后，我们再一次使用利用 GLM 正弦和余弦函数的简单公式，为我们的相机计算新的右向量:\n\n```cpp\n m_up = glm::cross(m_right, m_forward);\n```\n\n现在我们已经计算了向前和向上的向量，我们可以使用 GLM 的叉积函数来为我们的相机计算新的向上向量。每次相机改变位置或旋转时，以及在创建相机的视图矩阵之前，这三个步骤都要进行，这一点很重要:\n\n```cpp\n  float FoV = m_initialFoV;\n```\n\n接下来，我们指定 FOV。目前，我只是将其设置回初始化相机对象时指定的初始 FOV。如果相机放大或缩小，这将是重新计算 FOV 的地方(提示:鼠标滚动在这里可能有用):\n\n```cpp\nm_projection = glm::perspective(glm::radians(FoV), 4.0f / 3.0f, 0.1f, 100.0f);\n```\n\n一旦我们指定了视野，我们就可以计算相机的投影矩阵。对我们来说幸运的是，GLM 有一个非常方便的函数叫做`glm::perspective()`，它以弧度为单位获取视野、长宽比、近裁剪距离和远裁剪距离，然后它会为我们返回一个创建的投影矩阵。由于这是一个例子，我直接指定了 4:3 的纵横比(4.0f/3.0f)和 0.1 单位到 100 单位的裁剪空间。在生产中，您最好将这些值移动到可以在运行时更改的变量中:\n\n```cpp\n m_view = glm::lookAt(\n            m_position,           \n            m_position + m_forward, \n            m_up\n         );\n      }\n```\n\n最后，我们在`Update()`函数中做的最后一件事是创建视图矩阵。正如我之前提到的，我们很幸运，GLM 库提供了一个`lookAt()`函数来抽象我们在本节前面讨论的所有步骤。这个`lookAt()`函数取三个参数。首先是摄像头的位置。第二个是摄像机指向的向量值，或者说*看着*，我们通过简单的添加摄像机的当前位置来提供这个向量值，并且它是向前计算的。最后一个参数是摄像机的当前向上向量，我们之前也计算过。一旦完成，这个函数将返回新更新的视图矩阵，用于我们的图形管道。\n\n简而言之，这是一个简单的 3D 相机类。继续运行 CameraDemo 项目，查看系统的运行情况。你可以用 WASD 键移动相机，用鼠标改变视角。接下来，我们将进入另一个重要的游戏引擎系统，物理！\n\n# 研究物理\n\n如今，一个游戏不实现至少一些基本的物理形式是非常罕见的。游戏物理学的主题相当庞大和复杂，在你认为它已经被很好地涵盖之前，很容易就能填满几卷。正因为如此，整个团队都致力于创建*物理引擎*，构建生产级系统可能需要多年的开发。既然是这样，我们就不试图在这里涵盖所有方面，而是采取更高级别的方法。我们将讨论物理系统中一些更常用的方面，特别是基本的碰撞检测。对于更高级的需求，例如对重力、摩擦和高级碰撞检测的支持，我们将介绍第三方物理库的实现。在这一节的最后，我们的演示引擎将有先进的物理支持。\n\n# AABB 点\n\n首先，让我们看一下您可以在 3D 中执行的更简单的碰撞检查之一，找出一个点是在**轴对齐边界框** ( **AABB** )的内部还是外部。AABBs 非常容易创建。你基本上可以认为这些是不可旋转的立方体或盒子。下图描绘了 AABB 和点碰撞:\n\n![](img/c502b9d4-9123-486a-b50f-59304240a90f.png)\n\n要创建边界框，可以指定矢量格式的最大点和最小点，或者指定一个中心点，然后指定高度、宽度和深度。对于本例，我们将使用最小和最大点数方法创建我们的 AABB:\n\n```cpp\nstruct BoundingBox\n{\n glm::vec3 m_vecMax;\n glm::vec3 m_vecMin;\n};  \n```\n\n前面的代码是一个简单的 AABB 结构示例。\n\n现在我们有了一个 AABB，我们可以开发一种方法来检查是否有一个点落入了 AABB。这个检查很简单；我们只需要检查它的所有值，x，y 和 z，是否都大于 AABB 的最小值，小于 AABB 的最大值。代码中的这个检查看起来像下面这样，形式最简单:\n\n```cpp\nbool PointInAABB(const BoundingBox& box, const glm::vec3 & vecPoint)\n {\n   if(vecPoint.x > tBox.m_vecMin.x && vecPoint.x < tBox.m_vecMax.x &&\n      vecPoint.y > tBox.m_vecMin.y && vecPoint.y < tBox.m_vecMax.y &&\n      vecPoint.z > tBox.m_vecMin.z && vecPoint.z < tBox.m_vecMax.z)\n     {\n         return true;\n     }\n    return false;\n  }\n\n```\n\n# AABB 到 AABB\n\n现在我们已经看到了如何测试一个点是否在某个 AABB 内，下一个非常有用的碰撞检查是 AABB 到 AABB 检查，这是一个快速测试，可以找出两个 AABB 是否发生碰撞。下图描述了这种碰撞检查:\n\n![](img/8e4f7aad-e66f-4e3a-8635-cc8f2162973e.png)\n\n两个 AABBs 之间的冲突检查非常简单快速。对于大多数需要某种形式的碰撞检测的对象来说，这是一个非常常见的选择。\n\nAABBs 不好的地方是不能轮换。一旦它们被旋转，它们就不再是 AABBs，因为它们不再与 *x* 、 *y* 和 *z* 轴对齐。对于旋转的物体，更好的选择是使用球体、胶囊，甚至是**方向的边界框** ( **OBBs** )。\n\n要检查两个 AABB 是否碰撞，我们只需要检查第一个 AABB 的最大点是否大于第二个的最小点，以及第一个的最小点是否小于第二个的最大点。下面是这个检查在代码中的样子，最简单的形式:\n\n```cpp\nbool AABBtoAABB(const BoundingBox& box1, const BoundingBox& box2) \n{ \n if (box1.m_vecMax.x > tBox2.m_vecMin.x &&  \n    box1.m_vecMin.x < tBox2.m_vecMax.x && \n    box1.m_vecMax.y > tBox2.m_vecMin.y && \n    box1.m_vecMin.y < tBox2.m_vecMax.y && \n    box1.m_vecMax.z > tBox2.m_vecMin.z && \n    box1.m_vecMin.z < tBox2.m_vecMax.z)  \n{  \n   return true; \n} \nreturn false; \n} \n```\n\n当然，盒子的顺序，哪个是第一个，哪个是第二个，并不重要。\n\n由于本次检查包含大量`&&`比较，如果第一次检查为假，则不会继续检查其余部分；这是允许非常快速测试的原因。\n\n# 球体对球体\n\n我想在这里涉及的最后一个简单的碰撞检查是测试两个球体是否相互碰撞。测试球体之间的碰撞非常简单，易于执行。球体相对于 AABBs 之类的东西的一个优点是，如果物体旋转，球体将保持不变，这并不重要。下图描述了两个球体之间的碰撞检查:\n\n![](img/ee43bc1c-46b3-4415-8767-a9bd4b83e28d.png)\n\n为了执行检查，我们只需要计算球体中心之间的距离，并将其与它们的半径之和进行比较。如果这个距离小于它们的半径之和，那么球体是重叠的。如果是一样的，那么球体只是接触而已。下面是这个碰撞测试在代码中的样子，最简单的形式:\n\n```cpp\n... \nstruct BoundingSphere \n{ \nglm::vec3    m_vecCenter; \nfloat          m_radius; \n}; \n... \nbool SphereToSphere(const BoundingSphere & Sphere1, const BoundingSphere & Sphere2) \n{ \n\nglm::vec3 distance(Sphere2.m_vecCenter - Sphere1.m_vecCenter); \nfloat distanceSqaured(glm::dot( & distance, & distance) ); \n\n```\n\n为了得到球体中心之间的距离，我们需要在它们的中心点之间创建一个向量:\n\n```cpp\nfloat radiiSumSquared( Sphere1.m_radius + Sphere2.m_radius ); \nradiiSumSquared *= radiiSumSquared; \n```\n\n然后，我们可以用半径之和来计算向量的长度:\n\nThere is a more efficient way to do this. Since the dot product of a vector with itself equals the squared length of that vector, we could just calculate the squared length of the vector against the square of the sum of the radii. If we do it this way, we don't need to calculate the length of the vector, which is an expensive operation in itself.\n\n```cpp\nif( distanceSqaured <= radiiSumSquared ) \n{ \n    return true; \n} \nreturn false; \n} \n... \n```\n\n最后，我们可以执行碰撞检查。如果距离的平方小于或等于平方和，那么球体已经碰撞，否则，物体没有碰撞，我们返回 false。\n\n有了这些简单的检查，就可以处理最基本的碰撞检测。事实上，正如我们将在下一节中看到的，大多数高级检查由许多较小的检查组成。然而，总有一天你会发现自己需要更先进或优化的方法来处理物理；这时您可以求助于第三方库来提供这种支持。在下一节中，我们将研究其中一个第三方库的实现。\n\n# 实现子弹物理库。\n\nBullet 是一个物理引擎，它模拟碰撞检测以及柔体和刚体动力学。它已被用于许多已发布的视频游戏以及电影中的视觉效果。子弹物理库是受 zlib 许可证条款约束的免费开源软件。\n\nBullet 必须提供的一些功能包括:\n\n*   具有离散和连续碰撞检测的刚体和软体模拟\n*   碰撞形状:球体、长方体、圆柱体、圆锥体、使用 GJK 的凸包、非凸和三角形网格\n*   柔软的身体支撑:布料、绳索和可变形物体\n\n一组丰富的刚体和柔体约束，带有约束限制和马达\n\n你可以在[http://bulletphysics.org](http://bulletphysics.org)找到源代码链接和更多信息。\n\n让我们来看看如何将 Bullet 合并到您自己的游戏项目中。我不打算花时间讨论如何将库链接到我们的演示项目，因为我们已经讨论过几次了。如果你确实需要复习，倒过来看几章。我们要做的是将子弹引擎合并到我们的演示引擎中，然后使用子弹引擎的计算来实时定位我们的游戏对象。在这个例子中，我们将创建一个简单的地平面，然后一个球(球体)下落并与地面碰撞。我们将使用子弹的内置类型来支持这一点，包括重力给它一个现实的效果。\n\n从地面`GameObject`开始，我们设置变量来保存一些我们需要的物理值。第一种是`btCollisionShape`型。这是一种项目符号类型，允许在为物理测试创建边界对象时使用简单形状的定义。第二种是`btDefaultMotionState`类型，它也是一种子弹数据类型，描述了物体在运动时的行为方式。我们需要的最后一个变量是`btRigidBody`类型，这是一个子弹数据类型，将保存我们的物理引擎将关注的对象的所有物理属性:\n\n```cpp\nclass GroundObject : BookEngine::GameObject \n{ \n   ... \n\n   btCollisionShape* groundShape = nullptr; \n   btDefaultMotionState* groundMotionState = nullptr; \n   btRigidBody* groundRigidBody = nullptr; \n```\n\n一旦我们定义了这些变量，我们就可以在其`Init()`函数中构建地面物体的物理表示:\n\n```cpp\nvoid GroundObject::Init(const glm::vec3& pos, const glm::vec3& scale) \n{ \n   ... \n   groundShape = new btStaticPlaneShape(btVector3(0, 1, 0), 1); \n   groundMotionState = \n      new btDefaultMotionState(btTransform(btQuaternion(0, 0, 0, 1), btVector3(m_position.x, m_position.y, m_position.z))); \n```\n\n我们首先将`groundShape`变量设置为`btStaticPlanShape`。这是一个 Bullet 对象，它指定了一个简单的平面对象，非常适合我们的需求，也是一个简单的地面对象。接下来，我们设置`groundMotionState`。我们通过使用`btDefaultMotionState`子弹物体来实现。`btDefaultMotionState`是用于指定物体运动方式的类型。创建新的`btDefaultMotionState`时，我们需要传入一些关于对象变换的信息，也就是对象的旋转和位置。为此，我们传递一个`btTransform`对象，它有自己的四元数格式的旋转参数(`btQuaternion(0, 0, 0, 1)`)和矢量 3 格式的位置参数(`btVector3(m_position.x, m_position.y, m_position.z)`):\n\n```cpp\nbtRigidBody::btRigidBodyConstructionInfo \n groundRigidBodyCI(0, groundMotionState, groundShape, btVector3(0, 0,  0)); \n groundRigidBody = new btRigidBody(groundRigidBodyCI); \n```\n\n现在，通过设置`groundShape`和`groundMotionState`，我们可以继续创建和设置刚体信息。首先，我们为名为`groundRigidBodyCI`的构造信息定义一个持有人`btRigidBodyConstuctionInfo`变量。该对象接受几个参数值、指定质量的缩放值、对象的运动状态、碰撞形状以及指定局部惯性值的矢量 3。惯性是任何物理物体对其运动状态的任何变化的阻力。基本上是物体保持匀速直线运动的趋势。\n\n由于我们的地面对象是静态的，不需要基于物理输入的任何改变，我们可以放弃`Update()`功能，转到我们将用来测试系统的球对象。\n\n进入`BallObject.h`文件，我们定义一些我们需要的变量，就像我们为我们的地面物体所做的一样。我们创建一个运动状态，一个质量、碰撞形状的标量(整数)值，最后是一个刚体:\n\n```cpp\nbtDefaultMotionState* fallMotionState;\nbtScalar mass = 1;\nbtCollisionShape* fallShape;\nbtRigidBody* fallRigidBody;\n...  \n```\n\n现在，进入`BallObject.cpp`文件，我们给刚刚定义的变量赋值:\n\n```cpp\nvoid BallObject::Init(const glm::vec3& pos, const glm::vec3& scale)\n {\n    ...\n\n    fallShape = new btSphereShape(10);\n    btVector3 fallInertia(0.0f, 0.0f, 0.0f);  \n```\n\n首先，我们设置碰撞形状。在这种情况下，我们将使用类型`btSphereShape`。这是球体的默认形状，并接受一个参数来设置球体的半径。接下来，我们为球体的惯性创建一个矢量 3 保持器。我们将它设置为全零，因为我们希望这个球自由下落，没有阻力，基于物体的质量和重力值，我们将很快设置:\n\n```cpp\nfallMotionState =\n       new btDefaultMotionState(btTransform(btQuaternion(0, 0, 0, 1),     \n       btVector3(m_position.x, m_position.y, m_position.z)));\n```\n\n接下来，我们设置球的运动状态，就像我们为地面物体所做的那样。我们将旋转设置为 0，并将位置设置为球对象的当前位置:\n\n```cpp\n fallShape->calculateLocalInertia(mass, fallInertia);\n    btRigidBody::btRigidBodyConstructionInfo fallRigidBodyCI(mass,  fallMotionState, fallShape, fallInertia);\n    fallRigidBody = new btRigidBody(fallRigidBodyCI);\n     }\n\n```\n\n然后，我们使用方便的`calculateLocalInertia()`函数计算局部惯性值，传递质量和`fallInertia`值。这将为我们的球对象设置下降向量，用于物理引擎的第一个刻度。最后，我们以设置刚体对象结束，就像我们之前对地面对象所做的那样。\n\n对于球对象，我们确实期望物理引擎输出影响球对象。正因为如此，我们需要在球对象的`Update()`功能中做一些调整:\n\n```cpp\nvoid BallObject::Update(float deltaTime)\n {\n    btTransform trans;\n    fallRigidBody->getMotionState()->getWorldTransform(trans);\n    m_position.x = trans.getOrigin().getX();\n    m_position.y = trans.getOrigin().getY();\n    m_position.z = trans.getOrigin().getZ();\n  }\n```\n\n球对象更新循环的第一步是从刚体获得物理对象的变换。一旦我们有了这个变换对象，我们就可以设置球对象的网格(可见对象)来确定物理变换对象的位置。对象本身就是这样。球和地面物体现在容纳了所有需要的物理信息。我们现在可以在我们的游戏循环中实现物理引擎循环，让球滚动起来，没有双关语！\n\n为了将物理引擎实现到我们现有游戏引擎的循环中，我们需要首先设置几个值。跳到我们的`Gameplayscreen.h,`中，我们定义变量来保存这些值:\n\n```cpp\nbtBroadphaseInterface* broadphase = new btDbvtBroadphase();  \n```\n\n首先是`btBroadphaseInterface`类对象的定义，它提供了一个 Bullet 接口来检测 AABB 重叠对象对。在本例中，我们将其设置为`btDbvtBroadphase`，它使用两个动态 AABB 包围体层次结构/树来实现`btBroadphase`。当处理许多移动对象时，这往往是最佳的宽相位；其插入/添加和移除对象的速度通常快于`btAxisSweep3`和`bt32BitAxisSweep3`中的扫掠和修剪阶段:\n\n```cpp\nbtDefaultCollisionConfiguration* collisionConfiguration = new     \n       btDefaultCollisionConfiguration();\nbtCollisionDispatcher* dispatcher = new              \n       btCollisionDispatcher(collisionConfiguration); btSequentialImpulseConstraintSolver* solver = new    \n       btSequentialImpulseConstraintSolver;\n```\n\n接下来，我们定义了碰撞配置、碰撞调度器和顺序脉冲约束求解器。我们不会对其中的每一个进行太深入的讨论，但是主要的一点是冲突配置设置了一些 Bullet 内部值，例如冲突检测堆栈分配器和池内存分配器。冲突调度程序是如何处理冲突的定义。它支持处理*凸凸*和*凸凹*碰撞对、碰撞时间、最近点和穿透深度的算法。最后，顺序脉冲约束求解器将决定如何解决对象之间的冲突，它定义了可以被认为是算法的东西。对于希望了解的人来说，这是投影高斯-塞德尔(迭代 LCP)方法的**单指令多数据** ( **SIMD** )实现:\n\n```cpp\nbtDiscreteDynamicsWorld* dynamicsWorld = new       \n     btDiscreteDynamicsWorld(dispatcher, broadphase, solver,    \n     collisionConfiguration);\n```\n\n我们需要定义的最后一个变量是我们的动力学世界对象。A `btDiscreteDynamicsWorld`提供离散刚体模拟。这可以被认为是物理模拟发生的环境或*世界*。一旦我们定义了这个，我们就有了开始物理模拟的所有部分。\n\n让我们跳到`GameplayScreen.cpp`文件，看看我们将用来初始化物理模拟的`OnEntry()`函数:\n\n```cpp\nvoid GameplayScreen::OnEntry() \n{ \n   ... \n\n   dynamicsWorld->setGravity(btVector3(0, -1, 0)); \n   dynamicsWorld->addRigidBody(m_ground.groundRigidBody); \n   dynamicsWorld->addRigidBody(m_ball.fallRigidBody); \n... \n} \n```\n\n我们设置的第一件事是我们的重力矢量。在我们的简单示例中，我们将其设置为位于 *y* 轴上的`-1`。接下来，我们将两个创建的刚体添加到模拟环境中，一个用于地面，一个用于球。处理我们物理引擎的初始化；现在我们需要在每个引擎上更新它:\n\n```cpp\nvoid GameplayScreen::Update(float deltaTime) \n{ \n   CheckInput(deltaTime); \n   dynamicsWorld->stepSimulation(1 / 60.f, 10); \n   m_ball.Update(deltaTime); \n```\n\n在`GameplayScreen::Update()`函数内部，我们首先检查输入，然后在物理引擎上调用更新，最后在游戏对象本身上调用更新。注意这个顺序很重要。我们想首先接受用户的输入，但我们想确保我们已经在对象之前更新了物理引擎。原因是物理计算应该对物体有一些影响，我们不想造成我们的绘图循环在物理循环之前的情况，因为这肯定会造成一些不必要的影响。您还会注意到物理更新功能`stepSimulation`，它接受两个参数。第一个是模拟的时间。这通常是自您上次调用它以来的时间。在本例中，我们将其设置为 1/60 秒，即 60 FPS。第二个参数是每次调用 Bullet 时允许执行的最大步骤数。如果您传递一个非常大的值作为第一个参数，比如说，固定内部时间步长或游戏时钟大小的五倍，那么您必须增加`maxSubSteps`的数量来对此进行补偿；否则，您的模拟将失去时间，这将再次导致一些不需要的物理计算输出。\n\n就这样！我们现在有一个物理引擎运行它的模拟，并影响我们在屏幕上绘制的世界中的物体。您可以通过在`Chapter05` GitHub 存储库中运行`PhysicsDemo`示例项目来看到这一点。输出如下所示:\n\n![](img/587278a9-af53-427b-b6bc-759b806a024a.png)\n\n# 摘要\n\n在这一章中，我们涵盖了很多领域，并在开发专业级项目所需的核心游戏系统方面取得了长足的进步。我们现在有了自己的自定义游戏状态系统，可以被游戏引擎本身的许多其他组件采用。我们开发了自己的定制相机系统，同时建立了对相机如何在较低水平上工作的理解。最后，我们研究了如何通过在示例引擎中添加子弹物理引擎来将完整的第三方游戏系统添加到我们的项目中。"
  },
  {
    "path": "docs/master-cpp-game-dev/06.md",
    "content": "# 六、创建图形用户界面\n\n在游戏中，用户交互是设计中极其重要的一部分。能够为用户提供视觉信息和视觉选择是**图形用户界面** ( **图形用户界面**)的作用。像本书中讨论的许多其他系统一样，也有现有的库可供使用。开源游戏开发世界中最常见的一个是**疯狂漩涡 GUI** ( **CEGUI** )。虽然 CEGUI 是一个非常健壮的 GUI 系统实现，但是这种健壮带来了复杂性，老实说，大多数时候你真的只需要一个文本标签，一个简单的按钮，也许还有一个复选框和图标支持。有了这些简单的构件，你可以创造很多。\n\n在本章中，我们将构建构建块并创建一个简单的图形用户界面系统。应该注意的是，从头开始创建一个完整的、生产就绪的图形用户界面系统是一项艰巨的任务，而不是一个章节的任务。因此，我们将专注于核心概念，并构建一个可以在以后扩展和扩展的系统。我们的图形用户界面将不使用任何应用编程接口细节，并将继续建立在前面章节创建的结构基础上。本章涵盖的主题如下:\n\n*   坐标系和定位\n*   添加控制逻辑\n*   呈现图形用户界面\n\n本章的完整代码示例可以在代码存储库中的`Chapter06`文件夹中找到。为了简洁起见，我将从这些部分中省略一些不重要的代码行，并且可能会更频繁地跳转到文件和类。\n\n# 坐标系和定位\n\n每个图形用户界面系统最重要的部分之一是对象/元素如何在屏幕上定位。在大多数情况下，图形应用编程接口使用称为屏幕空间的坐标，通常以绝对范围[-1，1]表示。虽然这有利于渲染，但在尝试开发我们的图形用户界面系统时，这可能会导致一些问题。让我们以使用绝对系统的想法为例。在这个系统中，我们将显式地将图形用户界面中的每个元素设置为一个真实的像素坐标。这可能很容易实现，但只有当游戏的分辨率保持不变时才会起作用。如果我们在任何时候改变分辨率，元素将保持锁定在它们的像素坐标，并且不会缩放以匹配新的分辨率。\n\n另一种选择是创建一个相对系统，其中每个图形用户界面元素的位置将相对于其他元素或屏幕位置进行描述。这种方法比绝对系统好得多，但仍然存在一些缩放问题。例如，如果我们有一个元素放在屏幕的左上角，有一个小的偏移量，如果在任何时候游戏的分辨率改变了，我们使用的间距也会改变。\n\n我们要构建的是 CEGUI 采用的一种有点类似的方法，它是前面提到的两种解决方案的组合。在此过程中，我们还将添加现代图形用户界面中使用的另一种常见约定:在*面板*中包含分组元素。我们希望将 GUI 元素分组到面板中有几个很好的理由。第一个是，如果我们想移动一堆元素，比如一个带有生命值、弹药和物品指示器的状态栏，如果我们将它们分组在一个面板中，我们只需要移动面板，所有的元素就会跟着移动，正确定位。这就引出了原因二:通过将面板中的元素组合在一起，我们可以定义元素相对于面板位置的位置，而不是将元素位置设置为像素坐标，甚至相对于屏幕位置。\n\n以下是描述该设计布局的示意图:\n\n![](img/00836859-220f-4875-a4cd-a99dcc87cd00.png)\n\n可以看到，采用了相对和绝对定位相结合的方式，但这次相对的起点不是整个屏幕的原点**【0，0】**，而是我们面板的原点**【0，0】**。虽然面板的原点在屏幕上已经有了一些坐标，但是我们并不使用这些坐标来设置元素的位置。\n\n理论上，我们现在在面板中有可扩展的元素，但是我们仍然需要一种方法来将面板锁定在适当的位置，而不管屏幕分辨率如何。这就是图形用户界面锚系统的概念。很有可能，如果你以前使用过图形用户界面，你会看到主播们在行动。在我们的例子中，为了节省时间，我们将稍微简化这个概念。在我们的系统中，每个面板都能够相对于五个锚点之一设置其原点:左上角、右上角、左下角、右下角和中心。\n\n下图演示了这一概念:\n\n![](img/6b2ce10a-aa70-43a8-bdfe-a91cdf9d5897.png)\n\n好的，那么我们如何实现这些概念并在代码中设计它们呢？让我们从一个所有其他元素都将继承的`IGUIElement`类开始。看看`IGUIElement`班:\n\n```cpp\nclass IGUIElement\n{\npublic:\nvirtual void Update() = 0;\nglm::vec2 GetPosition() { return m_position; };\nprotected:\nglm::vec2 m_position;\n};\n}\n```\n\n首先，我们的元素没有那么复杂。每个元素都有一个`Update()`函数，以及一个返回元素位置的 getter 函数。我们将在本章的后面详细介绍这门课。\n\n我们可以实现的系统的下一部分是面板的概念。先来看看`IGUIPanel.h`的头文件:\n\n```cpp\n...\nstatic enum class GUIAnchorPos {\nTopRight,\nTopLeft,\nBottomRight,\nBottomLeft,\nCenter\n};\n...\n```\n\n文件以一个名为`GUIAnchorPos`的`enum class`的声明开始；这`enum`将让元素访问计算的锚点。我们将它变成一个枚举类，而不仅仅是`IGUIPanel`类中的`enum`，因为这将允许元素访问锚点，而不需要`IGUIPanel`实例。稍后，我们将看到一个函数，它将这些枚举值连接到已经计算的屏幕位置:\n\n```cpp\n...\nIGUIPanel(glm::vec4 panelBounds = glm::vec4(0,0,200,480),\nglm::vec2 panelAnchor = glm::vec2(0,0),\nglm::vec2 offset = glm::vec2(0,0));\n...\n```\n\n感兴趣的文件的下一部分是构造函数。这里，我们请求传入一个向量 4 来定义要创建的面板的边界。接下来，我们请求向量二作为面板锚点的原点位置，以及向量二作为面板位置的偏移量或*填充*。您还会注意到，我们还为每个参数提供了一些默认值。我们这样做有几个原因，但最大的原因是我们希望能够创建图形用户界面元素，并默认将它们附加到面板上。通过提供默认值，如果我们确实创建了一个图形用户界面元素，并且没有现有的面板来连接它，我们可以创建一个，而不需要在创建时传递值。我们将在本章稍后部分重新讨论这一点。让我们继续执行:\n\n```cpp\nIGUIPanel::IGUIPanel(glm::vec4 panelBounds, glm::vec2 panelAnchor, glm::vec2 offset) : m_bounds(panelBounds), m_offset(offset)\n{\n  m_Pos = panelAnchor + m_offset;\n  m_panelWidth = m_bounds.z;\n  m_panelHeight = m_bounds.w;\n}\n```\n\n对于`IGUIPanel`构造函数的实现，我们首先要计算的是面板在屏幕上的位置。我们通过将面板的锚点与已经传递的偏移量相加，并将其存储在受保护的成员变量`m_Pos`中来实现这一点。接下来，我们计算面板的宽度和高度；我们使用传入的边界值来实现这一点。我们将它们分别存储在名为`m_panelWidth`和`m_panelHeight`的受保护成员变量中。\n\n现在我们已经有了面板构造器，我们可以继续设置面板如何保存它们的元素。为此，我们只需创建一个名为`m_GUIElementList`的`IGUIElements`指针向量。然后，我们可以开始创建一些公共方法来访问和操作面板的元素列表:\n\n```cpp\n...\n  void IGUIPanel::AddGUIElement(IGUIElement & GUIElement)\n  {\n     m_GUIElement.List.push_back(&GUIElement);\n  }\n...\n```\n\n首先，在`IGUIPanel.cpp`文件中，我们创建一个`AddGUIElement()`函数，向面板中添加新元素。该函数实现对面板元素列表的`push_back()`方法的调用，推入给定的`GUIElement`引用:\n\n```cpp\nvirtual std::vector<IGUIElements*>& GetGUIElementList() \n{ \n   return m_ GetGUIElementList; \n};\n```\n\n跳到`IGUIPanel.h`文件，我们实现一个 getter 函数，`GetGUIElementList()`，提供对私有元素列表的公共访问:\n\n```cpp\nvoid IGUIPanel::Update()\n{\n  for (auto const& element : m_ m_GUIElement.List)\n  {\n     element ->Update();\n  }\n}\n```\n\n切换回`IGUIPanel.cpp`文件，我们可以看看面板类的`Update()`函数的实现。该更新将遍历面板的元素列表，然后为列表中的每个元素调用`Update()`函数。这将允许面板控制其元素的更新，并提供实现概念的结构，例如在隐藏的面板上暂停元素更新:\n\n```cpp\nIGUIPanel::~IGUIPanel()\n{\n  std::for_each(m_GUIElementList.begin(),\n  m_ GUIElementList.end(),\n  std::default_delete<IGUIElement>());\n}\n```\n\n最后，我们需要记住在调用析构函数时清理所有属于面板的元素。为此，我们将使用`standard`库的`for_each()`方法。我们之所以使用这种方法，主要是因为这是一个例子，也是因为我想向你们介绍这种方法。`for_each()`方法采用三个参数。前两个应用于范围，第三个是要执行的函数。在我们的例子中，我们将在我们所经过的每个元素上调用`default_delete()`，并且我们再次使用这个方法作为向您介绍函数的手段。`default_delete()`函数实际上是一个函数对象类，其类似函数的调用采用模板化的对象类型并删除它。这可以与简单地使用 delete 进行删除操作的非专用版本或专用于数组的版本`delete[]`进行比较。这个类是专门为与`unique_ptr`一起使用而设计的，并且提供了一种删除`unique_ptr`对象的方法，没有开销。\n\n好了，现在我们已经有了`IGUIPanel`类，我们可以继续构建我们的图形用户界面系统所需的一些更复杂的元素。对于本例，我们将添加一个支持标签的基本按钮:\n\n```cpp\n...\nclass IGUIButton : public IGUIElement\n{\n public:\n IGUIButton(glm::vec4& bounds,\n glm::vec2& position,\n GLTexture* texture,\n std::string label,\n SpriteFont* font,\n glm::vec2& fontScale = glm::vec2(1.0f),\n IGUIPanel* panel = NULL);\n ~IGUIButton();\n virtual void Update() override;\n...\n```\n\n在`IGUIButton.h`文件中，我们可以看到按钮继承了我们的基本`IGUIElement`。这当然意味着我们可以访问父类的所有函数和受保护成员，包括`m_position`和`GetPosition()`函数，因此我们在这里不重新定义它们。当我们在看`IGUIButton.h`的时候，我们也可以看一下构造器，我们在那里定义按钮在创建时需要传递什么。在我们的示例按钮中，我们正在寻找按钮的边界(大小)、位置、绘制按钮时要使用的纹理、按钮的标签(要显示的文本)、标签要使用的字体、字体的比例(我们给出的默认值为`1.0f`)，最后是添加按钮的面板，除非另有说明，否则我们默认为`NULL`。当我们继续这一章时，我们将更深入地研究这些参数。\n\n转移到构造函数的实现，在`IGUIButton::IGUIButton(glm::vec4 & bounds, glm::vec2 & position, std::string label, GLTexture * texture, SpriteFont* font, glm::vec2& fontScale, IGUIPanel* panel)`之前的`IGUIButton.cpp`中:\n\n```cpp\n\nm_texture(*texture),\nm_buttonLabel(label),\nm_spriteFont(font),\nm_fontScale(fontScale),\nm_panel(panel)\n{\n   m_bounds = bounds;\n   if (m_panel != NULL)\n   {\n   m_position = *m_panel->GetPosition() + position;\n```\n\n在大多数情况下，我们只是将内部成员变量设置为传入的值，但是有一点需要注意的是我们如何处理面板值。在构造函数体中，我们执行检查，看存储在`m_panel`中的值是否不为空。如果该检查为真，我们可以继续设置按钮元素相对于面板位置的位置。为此，我们首先调用面板的`GetPosition()`函数，将返回值与传入的位置值相加，并将计算结果保存在`m_position`成员变量中。通过将按钮的位置设置为面板的关系原点，这将部分地给出我们想要的，但是由于我们的默认面板元素的原点是左下角，结果将是按钮被放置在面板的底部。这不一定是期望的行为。为了纠正这一点，我们需要根据面板顶部以及面板中任何已有的元素计算按钮新的 *y* 轴值:\n\n```cpp\n//Move to just below the last element in the list\nif (!m_panel->GetGUIElementList().empty())\n{\n  IGUIElement* lastElement = m_panel-> GetGUIElementList().back();\n  m_position.y = lastElement ->GetPosition().y -\n  lastElement ->GetBounds().w -\n  10.0f; // Used as default padding (should be dynamic)\n}\nelse\n{\n   //Move to top of panel\n   m_position.y += m_panel->GetBounds()->w - m_bounds.w;\n   }\n  }\n}\n```\n\n首先，我们要检查添加按钮的面板中是否已经存在任何元素。我们通过使用`GetGUIElementList().empty()`函数检查面板的向量来做到这一点。如果面板的元素列表不是空的，那么我们需要面板列表中最后一个元素的位置。为此，我们创建了一个名为`lastElement`的临时元素，并使用`GetGUIElementList().back()`将其分配给面板列表中的最后一个元素。有了存储的元素，我们就可以用它来计算按钮的 *y* 轴值。我们通过从存储元素的高度(`GetBounds().w`)和默认填充值中减去存储元素的 *y* 轴值来实现这一点，在本例中，我们将默认填充值设置为`10.0f`。在一个完整的图形用户界面实现中，您可能希望将这个填充值动态化。最后，如果面板是空的，这是第一个元素，我们通过计算面板的高度(`GetBounds()->w`)减去新按钮的高度来设置按钮的 *y* 轴。这将把按钮元素放在面板的最顶端。\n\n我们现在有一个面板系统，它创建了一个元素类和一个实现的按钮元素。我们需要做的最后一件事是构建一个高级类来将系统粘合在一起。我们将创建一个`IGUI`类，它将容纳面板，为其他游戏系统提供对图形用户界面方法的访问，并且，正如我们将在下一节中看到的，提供输入、更新和绘制机制。让我们跳到`IGUI.cpp`文件中的构造函数实现:\n\n```cpp\nIGUI::IGUI(Window& window) : m_window(window)\n{\n...\nm_BL = new glm::vec2( \n                      0,\n                      0\n                      );\nm_BR = new glm::vec2( \n                      m_window.GetScreenWidth(),\n                      0\n                      );\nm_TL = new glm::vec2( \n                      0,\n                      m_window.GetScreenHeight()\n                      );\nm_TR = new glm::vec2( \n                      m_window.GetScreenWidth(),                     \n                      m_window.GetScreenHeight()\n                     );\nm_C = new glm::vec2( \n                     m_window.GetScreenWidth() * 0.5f,                 \n                     m_window.GetScreenHeight() * 0.5f\n                     );\n ...\n```\n\n在`IGUI`类的构造函数中，我们将为`IGUI`实例持有的所有面板定义锚点。我们将这些值存储在私有成员变量中:屏幕左下方为`m_BL`，屏幕右下方为`m_BR`，左上角为`m_TL`，右上角为`m_TR`，屏幕中央为`m_C`。我们使用设置`m_window`窗口对象返回用于计算锚点的屏幕的宽度和高度。我们将在稍后的课程中看到如何使用这些点为面板提供锚。\n\n接下来，让我们看看我们将用于向`IGUI`实例添加元素和面板的函数:\n\n```cpp\nvoid IGUI::AddGUIElement(IGUIElement& GUIElement)\n{\n   if (!m_GUIPanelsList.empty())\n  {\n   m_GUIPanelsList[0]->AddGUIObject(GUIElement);\n   }\n   else\n   {\n   IGUIPanel* panel = new IGUIPanel();\n   m_GUIPanelsList.push_back(panel);\n   m_GUIPanelsList[0]->AddGUIObject(GUIElement);\n   }\n}\n```\n\n从`AddGUIElement`函数开始，这个函数，顾名思义，给 GUI 增加了一个 GUI 元素。默认情况下，该元素将被添加到图形用户界面面板列表中的第一个面板中，该列表存储在`m_GUIPanelsList`向量中。如果面板列表为空，我们将创建一个新的面板，将其添加到列表中，最后将元素添加到该面板中:\n\n```cpp\nvoid IGUI::AddGUIPanel(IGUIPanel& GUIPanel)\n{\n  m_GUIPanelsList.push_back(&GUIPanel);\n}\n```\n\n`AddGUIPanel()`功能很简单。我们采用`push_back()`向量方法，将传入的`IGUIPanel`对象添加到图形用户界面的面板列表中。\n\n我们需要看的定位系统的最后一部分是`GetAnchorPos()`功能。该函数将根据计算出的屏幕值返回面板的锚定位置，我们之前在`IGUI`构造器中看到了这些值以及面板本身的大小:\n\n```cpp\n...\nglm::vec2* IGUI::GetAnchorPos(GUIAnchorPos anchorPos, glm::vec4 bounds)\n{\n  switch (anchorPos)\n  {\n    case(GUIAnchorPos::TopRight):\n    m_TR->y -= bounds.w;\n    m_TR->x -= bounds.z;\n    return m_TR;\n    break;\n    case(GUIAnchorPos::TopLeft):\n    m_TL->y -= bounds.w;\n    return m_TL;\n    break;\n    case(GUIAnchorPos::BottomRight):\n    m_BR->x -= bounds.z;\n    return m_BR;\n    break;\n    case(GUIAnchorPos::BottomLeft):\n    return m_BL;\n    break;\n    case(GUIAnchorPos::Center):\n    m_C->y -= bounds.w;\n    return m_C;\n    break;\n  }\n}\n...\n```\n\n我们从传递两个值开始。第一个是`GUIAnchorPos`，你可能还记得之前我们在`IGUIPanel.h`文件中定义`enum`类的那一章。第二个是面板的边界，用矢量四对象描述。在函数内部，我们有一个 switch case 语句，用来确定要计算的锚点。\n\n如果情况与`TopRight`枚举值匹配，首先我们修改锚点的 *y* 轴值。我们这样做是因为我们使用左下角作为默认原点，所以我们需要修改它，这样左上角就是锚点的新原点。接下来，我们修改锚点的 *x* 轴值。我们这样做是因为我们需要将锚点从屏幕的右上角移动面板对象的宽度。如果不修改 *x* 轴值，面板会向右拉屏。\n\n接下来，如果案例与 TopLeft 枚举值匹配，我们修改锚点的 *y* 轴值。如前所述，我们这样做是为了说明我们坐标系的原点是左下角。这次我们不需要修改 *x* 轴值，因为当我们从左向右绘制时，我们的面板会出现在屏幕上。\n\n如果案例匹配`BottomRight`枚举值，我们需要修改 *x* 轴值。如前所述，我们需要将锚点向左移动面板的宽度，以确保面板将在屏幕上绘制。这次我们不需要修改 *y* 轴，因为锚点将匹配屏幕底部默认坐标系的 *y* 原点。\n\n如果情况与`BottomLeft`枚举值匹配，我们只需返回未修改的锚点，因为它与坐标系的默认原点匹配。\n\n最后，如果案例匹配`Center`枚举值，我们将只修改 *y* 轴值，因为我们只需要考虑左下角的默认原点。构造器中计算的 *x* 轴值将面板向右移动，将其正确定位在屏幕中心。\n\n这照顾到我们的图形用户界面系统的定位和锚定系统。我们现在有了一个坚实的框架，可以在本章的其余部分继续发展。接下来，我们将看看如何向我们的图形用户界面系统添加输入控件。\n\n# 添加控制逻辑\n\n图形用户界面不仅仅是你在屏幕上看到的。可以说，幕后还运行着逻辑，提供与对象交互所需的功能。如果鼠标在一个元素上移动，如果选择了一个复选框，或者如果点击了一个按钮，那么处理将会发生什么都是图形用户界面输入系统的一部分。在本节中，我们将构建必要的体系结构来为我们的图形用户界面处理鼠标输入。\n\n虽然有几种不同的方法可以实现系统来处理图形用户界面的输入，但我认为这是向您介绍我最喜欢的编程模式之一观察者的绝佳机会。`Observer`是最广为人知的模式之一，来自**四人帮**。`Observer`非常常用，以至于 Java 有一个专门的核心库，`java.util.Observer`和 C#以事件关键字的形式将其合并到语言本身中。\n\n我认为解释`Observer`模式最简单的方法是，当你有对象在做另一个类或对象感兴趣的各种事情时，你可以*订阅*到*事件*，并在这些对象执行它们感兴趣的功能时得到通知。很可能你在你的开发冒险中已经见过和/或使用过`Observer`模式。事实上，我们已经在这本书里看到了。SDL 库使用自己的`Observer`模式来处理输入。我们利用它根据用户的输入执行任务。以下是我们用来处理游戏输入的 SDL 事件实现:\n\n```cpp\nSDL_Event event;\nwhile (SDL_PollEvent(&event))\n{\n  m_game->OnSDLEvent(event);\n}\n```\n\n我们将要构建的是一些更基本的东西，但是它将让您很好地理解如何为图形用户界面实现输入系统，并且您有望熟悉未来开发的灵活模式。\n\n首先，在`IGUIElement`头文件中，我们创建了一个名为`GUIEvent`的新`enum`类:\n\n```cpp\nenum class GUIEvent\n{\n HoverOver,\n Released,\n Clicked,\n};\n```\n\n这个`enum`类定义了我们的图形用户界面元素可以监听的不同类型的事件。接下来，仍然在我们的`IGUIElement`类头文件中，我们需要添加一个完全虚拟的函数，`OnNotify()`:\n\n```cpp\nvirtual void OnNotify(IGUIElement& element, GUIEvent event) = 0;\n```\n\n该函数将被每个元素类型覆盖，并在事件发生时被调用。实现该功能的元素可以*监听*与它们相关的事件，并在必要时执行操作。`OnNotify()`取两个参数:一个`IGUIElement()`，定义哪个元素受影响，事件类型。这两个参数将为我们提供确定如何处理发送的每个事件所需的所有信息。\n\n让我们看看我们的`IGUIButton()`对象类中的`OnNotify()`实现:\n\n```cpp\nvoid IGUIButton::OnNotify(IGUIElement & button, GUIEvent event)\n{\n   If(event == GUIEvent::HoverOver)\n  {\n   //Handle Hover\n  }\n}\n```\n\n在`IGUIButton::OnNotify`实现中，我们可以听到不同类型的事件被传入。在本例中，我们正在检查传入的事件是否是`HoverOver`事件。如果是，我们添加一个注释，说明当按钮悬停在上面时，我们将在哪里执行任何需要的操作。说到设置*监听器*，真的是这样。接下来，我们需要将我们的图形用户界面输入系统连接到当前的输入系统，并开始发送事件通知。让我们继续，看看`IGUI`对象类中的`CheckInput()`函数实现:\n\n```cpp\nvoid IGUI::CheckInput(InputManager inputManager)\n{\n   float pointX = inputManager.GetMouseCoords().x;\n   float pointY = inputManager.GetMouseCoords().y;\n   for (auto &panel : m_GUIPanelsList) // access by reference to avoid                  \n                                          copying\n   {\n    for (auto& object : panel->GetGUIElementList())\n    {\n    //Convert Y coordinate position to top upper left origin, y-down\n     float convertedY =\n     m_window.GetScreenHeight() -\n     (object->GetPosition().y + object->GetBounds().w);\n     if (pointX < object->GetPosition().x + (object->GetBounds().z) &&\n     pointX >(object->GetPosition().x - (object->GetBounds().z)) &&\n     pointY < convertedY + object->GetBounds().w &&\n     pointY > convertedY - object->GetBounds().w)\n    {\n      object->OnNotify(*object, GUIEvent::HoverOver); \n      }\n    }\n  }\n}\n```\n\n我们将一件一件地看一看。首先，我们从传入的`InputManager`对象中获取当前鼠标坐标，并将它们保存到临时变量中:\n\n```cpp\nvoid IGUI::CheckInput(InputManager inputManager)\n{\nfloat pointX = inputManager.GetMouseCoords().x;\nfloat pointY = inputManager.GetMouseCoords().y;\n```\n\n接下来，我们需要使用一个嵌套的`for`循环来遍历 GUI 中的所有面板，并依次遍历附加到每个面板的所有元素:\n\n```cpp\nfor (auto &panel : m_GUIPanelsList) // access by reference to avoid copying\n{\nfor (auto& object : panel->GetGUIElementList())\n{\n```\n\n在嵌套循环内部，我们将做一个简单的*点击*测试，看看我们是否在按钮的绑定中。然而，首先，我们需要做一个快速的计算。在本章前面的坐标和位置部分，您可能记得我们进行了转换，将锚点的 *y* 轴移动到左上角。现在我们需要反其道而行之，将元素位置的 *y* 轴转换回左下角。我们需要这样做的原因是，鼠标光标的屏幕坐标系与按钮的位置相同:\n\n```cpp\nfloat convertedY = m_window.GetScreenHeight() -\n                  (object->GetPosition().y + object->GetBounds().w);\n```\n\n我们在循环中需要做的最后一件事是执行实际的*击中*或边界检查。为此，我们检查并查看鼠标光标的 *x* 轴值是否在按钮的屏幕区域内。我们还使用之前转换的 *y* 值在 *y* 轴上检查同样的事情。如果所有这些条件都满足，那么我们可以向元素发送`HoverOver`事件通知:\n\n```cpp\nif (pointX <element->GetPosition().x + (element->GetBounds().z) &&\npointX >(element->GetPosition().x - (element->GetBounds().z)) &&\npointY < convertedY + element->GetBounds().w &&\npointY > convertedY - element->GetBounds().w)\n{\n   object->OnNotify(*object, GUIEvent::HoverOver);\n}\n...\n```\n\n有了这个，我们就有了一个工作事件系统，虽然很粗糙。我们需要放置的最后一块拼图是将其连接到游戏引擎的当前输入处理系统。为此，我们在`ExampleScreen`类`m_gui->CheckInput(m_game->GetInputManager());`的`CheckInput()`函数中添加了一行简单的代码:\n\n```cpp\nvoid ExampleScreen::CheckInput(float deltaTime)\n{\n   SDL_Event event;\n   while (SDL_PollEvent(&event))\n   {\n   m_game->OnSDLEvent(event);\n   }\n   ...\n   m_gui->CheckInput(m_game->GetInputManager());\n   ...\n}\n```\n\n这照顾到了本章示例的逻辑实现。重构和调优肯定还有空间，但这应该会为您提供一个扩展的良好起点。我建议继续下一步，添加更多的功能，甚至是新的元素。在下一节中，我们将通过向我们的图形用户界面系统添加渲染并最终在屏幕上绘制我们的示例来结束这一章。\n\n# 呈现图形用户界面\n\n有了所有的定位和输入逻辑，我们现在可以通过实现一些基本的渲染来完成我们的图形用户界面系统。好消息是，我们已经有了一个强大的基础设施，用于我们在本书前面构建的主要渲染。我们将利用这个基础设施在屏幕上呈现我们的图形用户界面。基本上，在渲染图形用户界面时，您有两个真正的选择。您可以将图形用户界面渲染为纹理，然后将创建的纹理混合到最终绘制的场景中。另一个选项是在场景顶部的每一帧中将所有内容渲染为几何图形。两者都有各自的问题，但我认为在大多数情况下，创建一个纹理并混合该纹理会比将图形用户界面元素渲染为几何图形慢。\n\n为了让事情稍微简单一点，并更专注于实现，我们从一个更简单的方法开始，分别呈现每个元素。当然，如果图形用户界面中有很多元素，这并不是最有利于性能的渲染方式。在我们的例子中，我们不会有大量的元素，如果您正在构建诸如开始游戏/菜单图形用户界面之类的东西，这个解决方案以其当前的形式将是绰绰有余的。注意你的帧速率，如果你注意到一个下降，那么很可能你有太多的平局。\n\n我们处理解决方案的最佳方式是使用与渲染模型时相同的方法，只是略有不同。我们将再次使用着色器来绘制几何图形，因为这将为我们提供许多控制，并能够执行我们可能想要添加的任何混合、遮罩、图案和效果。对于我们的图形用户界面示例，我们将重用前面章节中的纹理顶点和片段着色器。在下一章中，我们将深入研究高级着色器和绘制技术。\n\n那么，让我们深入研究一下实现。将此添加到`IGUI.h`文件中:\n\n```cpp\nstd::unique_ptr<Camera2D> m_camera = nullptr; \n\n        std::unique_ptr<ShaderManager> m_textureProgram = nullptr; \n        std::unique_ptr<SpriteBatch> m_spriteBatch = nullptr; \n\n```\n\n然后将其添加到`IGUI`对象的构造函数中:\n\n```cpp\nIGUI::IGUI(Window& window) : m_window(window)\n{\n   m_camera = std::make_unique<Camera2D>();\n   ...\n   m_textureProgram = std::make_unique<BookEngine::ShaderManager>();\n   m_spriteBatch = std::make_unique<BookEngine::SpriteBatch>();\n}\n```\n\n这里，我们指定了一个着色器纹理程序，一个精灵批处理，和一个 2D 相机。这款相机与我们之前在书中创建的 3D 版本略有不同。我不会深入探讨 2D 相机，因为它稍微超出了本章的范围，但我要提到的是，主要的变化是，我们正在为 2D 绘图构建一个正交矩阵。我们给了每个图形用户界面实例自己的着色器，相机和精灵批处理。将由实例来处理最终设置。\n\n对于我们的例子来说，`ExampleGUI`是`IGUI`类的实现。看看`OnInit()`功能，我们可以看到这些资源的设置:\n\n```cpp\nvoid ExampleGUI::OnInit()\n{\nm_textureProgram->CompileShaders(\n                        \"Shaders/textureShading.vert\",\n                        \"Shaders/textureShading.frag\");\nm_textureProgram->AddAttribute(\"vertexPosition\");\nm_textureProgram->AddAttribute(\"vertexColor\");\nm_textureProgram->AddAttribute(\"vertexUV\");\nm_textureProgram->LinkShaders();\nm_spriteBatch->Init();\nm_camera->Init(m_window.GetScreenWidth(), \n               m_window.GetScreenHeight());\nm_camera->SetPosition(glm::vec2(\n                                m_window.GetScreenWidth() * 0.5f, \n                                m_window.GetScreenHeight()* 0.5f));\npanel = new BookEngine::IGUIPanel(\n                                glm::vec4(0, 0, 150, 500),\n                                *GetAnchorPos(\n                                   BookEngine::GUIAnchorPos:BottomLeft,\n                                    glm::vec4(0, 0, 150, 500)\n                                  ),\n                                  glm::vec2(0,0));\nAddGUIPanel(*panel);\n\n      BookEngine::GLTexture texture\n    =BookEngine::ResourceManager::GetTexture(\"Textures/button.png\");\n\nbutton = new BookEngine::IGUIButton(\n    glm::vec4(0, 0, 100, 50),\n    glm::vec2(10, -10),\"My Button\", &texture,\n    new BookEngine::SpriteFont(\"Fonts/Impact_Regular.ttf\", 72),\n       glm::vec2(0.2f), panel);\n\n       AddGUIElement (*button);\n}\n```\n\n我们将把它一点一点地分解。首先，我们需要编译我们的图形用户界面所需的`Shaders`，所以我们添加着色器所需的属性，最后链接它们以供使用。这应该很熟悉:\n\n```cpp\nm_textureProgram->CompileShaders(\n\"Shaders/textureShading.vert\",\n\"Shaders/textureShading.frag\");\nm_textureProgram->AddAttribute(\"vertexPosition\");\nm_textureProgram->AddAttribute(\"vertexColor\");\nm_textureProgram->AddAttribute(\"vertexUV\");\nm_textureProgram->LinkShaders();\nNext, we call Init on the sprite batch for the GUI instance:\nm_spriteBatch->Init();\n```\n\n然后，我们在 2D 相机实例上调用`Init`，传递屏幕宽度和高度。在`Init`之后，我们通过将屏幕的高度和宽度值分成两半来将摄像机的位置设置到屏幕的中间:\n\n```cpp\nm_camera->Init(m_window.GetScreenWidth(), \n               m_window.GetScreenHeight());\nm_camera->SetPosition(glm::vec2(\n                       m_window.GetScreenWidth() * 0.5f,\n                       m_window.GetScreenHeight()* 0.5f));\n```\n\n现在我们已经有了着色器程序、精灵批处理和相机设置，我们继续创建图形用户界面元素。首先是面板元素，它是我们使用本章前面创建的架构创建的。我们将其锚点设置为屏幕的左下方。创建面板后，我们通过调用类继承的`AddGUIPanel`函数将其添加到图形用户界面实例中:\n\n```cpp\npanel = new BookEngine::IGUIPanel(glm::vec4(0, 0, 150, 500),\n                                 *GetAnchorPos(\n                                 BookEngine::GUIAnchorPos:BottomLeft,                                  \n                                 glm::vec4(0, 0, 150, 500)\n                                 ),\n  glm::vec2(0,0));\n  AddGUIPanel(*panel);\n```\n\n创建面板并将其添加到图形用户界面实例的面板列表中，然后我们向该面板添加一个按钮。为此，我们首先创建一个临时变量来保存我们想要为这个按钮加载的纹理。然后我们创建按钮本身。我们再次使用本章前面构建的结构。我们传入`My Button`的标签和刚加载的纹理。一旦完成，我们调用`AddGUIElement()`功能并将按钮添加到面板:\n\n```cpp\nBookEngine::GLTexture texture = BookEngine::ResourceManager::GetTexture(\"Textures/button.png\");\nbutton = new BookEngine::IGUIButton(\n           glm::vec4(0, 0, 100, 50),\n           glm::vec2(10, -10),\n           \"My Button\",\n           &texture,\n           new BookEngine::SpriteFont(\"Fonts/Impact_Regular.ttf\", 72),\nglm::vec2(0.2f), panel);\nAddGUIElement (*button);\n```\n\n现在我们的元素已经就位，并且渲染组件已经创建和设置好了，我们可以最终确定图形用户界面系统的渲染管道。为了做到这一点，我们将依靠我们在对象中创建的继承结构。为了启动绘制调用链，我们从`ExampleGUI`类及其`Draw()`函数实现开始:\n\n```cpp\nvoid ExampleGUI::Draw() \n{ \n\n    ... \n\n    m_textureProgram->Use(); \n\n    ... \n\n    m_spriteBatch->Begin(); \n\n    //Draw all of the panels \n    for (auto const&panel : m_GUIPanelsList) \n    { \n        panel->Draw(*m_spriteBatch); \n    } \n\n    m_spriteBatch->End(); \n    m_spriteBatch->BatchRender(); \n    m_textureProgram->UnUse(); \n\n} \n```\n\n着眼于我们的图形用户界面实现的一个重要方面，我们从指定渲染图形用户界面元素时要使用的着色器程序开始`Draw()`功能。接下来，我们启动将用于图形用户界面元素的精灵批处理。然后，在精灵批处理的开始和精灵批处理的结束之间，我们使用`for`循环遍历图形用户界面面板列表中的所有面板，并调用其`Draw()`函数实现。一旦`for`循环完成，我们就结束精灵批处理，调用`BatchRender()`方法渲染批处理中的所有对象，最后通过调用着色器程序上的`UnUse()`方法关闭该函数。\n\n让我们沿着绘制链往下走一层，看看 IGUIPanel 的绘制函数实现:\n\n```cpp\nvoid IGUIPanel::Draw(SpriteBatch& spriteBatch) \n    { \nspriteBatch.Draw(glm::vec4(m_Pos.x,  \nm_Pos.y, \nm_panelWidth,  \nm_panelHeight), \n glm::vec4(0,0,1,1), \nBookEngine::ResourceManager::GetTexture( \n\"Textures/background.png\").id,  \n-0.1f,  \nColorRGBA8(0,0,0,75) \n); \n\n        for (auto const&element : m_GUIElementList) \n        { \n            element->Draw(spriteBatch); \n        } \n    } \n```\n\n在`IGUIPanel::Draw()`函数中，我们首先将面板本身添加到从调用对象传入的 sprite 批处理中。这将绘制一个稍微不透明的黑色背景。理想情况下，您希望将用于背景的纹理设为非硬编码值，并允许为每个实例设置该值。将面板添加到 sprite 批次进行绘制后，我们再次使用`for`循环遍历面板元素列表中的每个元素，并调用其`Draw()`函数实现。这有效地将其应用到绘制链的下一层。\n\n对于`IGUIElement`类，我们只需创建一个纯虚函数，继承的元素必须实现它:\n\n```cpp\nvirtual void Draw(SpriteBatch& spriteBatch) = 0;\n```\n\n这意味着我们现在可以进入绘制链示例中的最后一个环节，看看`IGUIButton::Draw()`函数的实现:\n\n```cpp\nvoid IGUIButton::Draw(SpriteBatch& spriteBatch)   { \n        ... \n\n        spriteBatch.Draw(glm::vec4(m_position.x, \n m_position.y,  \nm_bounds.z,  \nm_bounds.w),  \nuvRect,  \nm_texture.id,  \n0.0f,  \nColorRGBA8(255, 255, 255, 255)); \n\n        char buffer[256]; \n        m_spriteFont->Draw(spriteBatch,  \nbuffer,  \nglm::vec2( \nm_position.x + (m_bounds.z * 0.5f),  \n(m_position.y + (m_bounds.w * 0.5f)) - ((m_spriteFont->GetFontHeight() * m_fontScale.y) * 0.5f) \n), \n                            m_fontScale,  \n0.2f,  \nBookEngine::ColorRGBA8(255, 255, 255, 255), \nJustification::MIDDLE);         \n    } \n\n```\n\n同样，这些功能的实现并不太复杂。我们将元素添加到要绘制的调用对象传入的 sprite 批处理中。这样做的效果是，所有面板及其元素将被添加到单个图形用户界面实例的 sprite 批处理中，这将比每个面板和对象按顺序绘制本身的性能高得多。`Draw()`函数中的最后一个代码块是对 Sprite Font 实例的`Draw()`方法的调用。我不会详细讨论 Sprite Font 类是如何工作的，因为它不在本章的讨论范围之内，但我会查看代码文件，以了解它是如何工作的。`SpriteFont`类的作用很像`Sprite`类，只是它提供了一种在屏幕上绘制字体/文本的方法。在这个例子中，我们使用它来绘制按钮的标签。\n\n这就结束了牵引链。我们现在需要做的就是把 GUI 的头部`Draw()`调用连接到主游戏的`Draw()`调用。为此，我们在`ExampleScreen`类的`Draw()`函数中添加一行来调用图形用户界面实例的`Draw()`方法:\n\n```cpp\nvoid EditorScreen::Draw()\n{ \n... \n    m_gui->Draw(); \n} \n\n```\n\n现在，我很高兴地说，我们有一个简单但完整的工作图形用户界面系统。您可以运行示例演示来查看完整的图形用户界面运行情况。如果您想要查看面板如何受到每个定义的锚点的影响，您只需要在`ExampleGUI`类中设置面板时更改`BookEngine::GUIAnchorPos`值:\n\n```cpp\n panel = new BookEngine::IGUIPanel(glm::vec4(0, 0, 150, 500), \n*GetAnchorPos( \nBookEngine::GUIAnchorPos::BottomRight, \nglm::vec4(0, 0, 150, 500) \n), \n glm::vec2(0,0)); \n```\n\n以下是正在运行的图形用户界面截图，其锚点更改为`BottomLeft`、`BottomRight`、`TopLeft`、`TopRight`和`Center`:\n\n`BottomRight`截图如下图所示:\n\n![](img/0595a630-669f-412c-adb4-9e951d649152.png)\n\n`BottomLeft`截图如下图所示:\n\n![](img/8ee5b8ea-aa35-454f-8e9a-c4fcf8a37907.png)\n\n`TopLeft`截图如下图所示:\n\n![](img/ebbf1359-bb61-446e-9caa-461cc74cab06.png)\n\n`TopRight`截图如下图所示:\n\n![](img/770ecc8b-c2d8-4e5f-bfca-eed425cd420c.png)\n\n`Center`截图如下图所示:\n\n![](img/14a196c6-26aa-4e71-a784-45126f8d26d3.png)\n\n# 摘要\n\n在本章中，我们介绍了大量信息。我们讨论了创建图形用户界面所需的不同方面。我们完成了它的实现，深入到了工作 GUI 背后的核心架构。我们开发了一个面板和元素架构，包括控制定位的锚点。我们使用`Observer`设计模式实现了一个用户输入结构，并通过对在屏幕上显示图形用户界面元素所需的渲染管道进行编码来完善它。在下一章中，我们将深入探讨游戏开发中使用的一些高级渲染技术。"
  },
  {
    "path": "docs/master-cpp-game-dev/07.md",
    "content": "# 七、高级渲染\n\n通常，玩家对你游戏的第一印象是屏幕上的视觉效果。对创建高级渲染技术有深刻的理解对于构建引人注目的沉浸式体验至关重要。在本章中，我们将了解如何通过实现着色器技术来创建一些高级渲染效果。\n\n*   着色器简介\n*   照明技术\n*   使用着色器创建效果\n\n# 着色器简介\n\n简而言之，着色器是一个计算机程序，用于进行图像处理，如特效、色彩效果、照明以及着色。使用着色器程序中构建的算法，可以在运行时更改用于在屏幕上生成最终图像的所有像素、顶点或纹理的位置、亮度、对比度、色调和其他效果。如今，大多数着色器程序都是直接在**图形处理单元** ( **图形处理器**)上运行的。着色器程序并行执行。这意味着，例如，一个着色器可能每个像素执行一次，每个执行同时在图形处理器的不同线程上运行。并发线程的数量取决于显卡特定的图形处理器，现代显卡配备的处理器数以千计。这意味着着色器程序可以非常高效，并为开发人员提供了很多创造性的灵活性。在本节中，我们将了解着色器，并为示例引擎实现我们自己的着色器基础架构。\n\n# 着色器语言\n\n随着显卡技术的进步，渲染流水线增加了更多的灵活性。在过去，开发人员几乎无法控制固定功能管道渲染等概念，但新的进步使程序员能够更深入地控制图形硬件来渲染他们的作品。最初，这种更深层次的控制是通过用汇编语言编写着色器来实现的，这是一项复杂而繁琐的任务。没过多久，开发人员就渴望一个更好的解决方案。输入着色器编程语言。让我们简单地看一下一些更常用的语言。\n\n**C for graphics** ( **Cg** )是最初由英伟达图形公司开发的着色语言。Cg 基于 C 编程语言，尽管它们共享相同的语法，但 C 的一些特性被修改，并添加了新的数据类型，以使 Cg 更适合编程 GPU。Cg 编译器可以输出 DirectX 和 OpenGL 都支持的着色器程序。虽然 Cg 大部分被否决了，但随着它在 Unity 游戏引擎中的使用，它已经以一种新的形式复兴了。\n\n**高级着色语言** ( **HLSL** )是微软公司开发的着色语言，用于 DirectX 图形 API。HLSL 再次模仿 C 编程语言，与 Cg 着色语言有许多相似之处。HLSL 仍在开发中，并继续是 DirectX 的首选着色语言。自发布以来，HLSL 语言的 DirectX 12 支持更低级别的硬件控制，并且性能有了显著提高。\n\n**OpenGL 着色语言** ( **GLSL** )也是基于 C 语言编程的着色语言。它是由 **OpenGL 架构评审委员会** ( **OpenGL ARB** )创建的，旨在让开发人员无需使用 ARB 汇编语言或其他硬件特定语言就能更直接地控制图形管道。该语言仍处于开放开发阶段，并将成为我们在示例中重点关注的语言。\n\n# 构建着色器程序基础架构\n\n大多数现代着色器程序最多由五种不同类型的着色器文件组成:片段或像素着色器、顶点着色器、几何着色器、计算着色器和镶嵌着色器。在构建着色器程序时，这些着色器文件中的每一个都必须被编译和链接在一起才能使用，就像 C++ 程序是如何被编译和链接的一样。接下来，我们将向您介绍这个过程是如何工作的，并看看我们如何构建一个基础设施，以便更容易地与我们的着色器程序进行交互。\n\n首先，让我们看看如何编译 GLSL 着色器。GLSL 编译器是 OpenGL 库本身的一部分，我们的着色器可以在 OpenGL 程序中编译。我们将构建一个架构来支持这种内部编译。编译着色器的整个过程可以分解为一些简单的步骤。首先，我们必须创建一个着色器对象，然后向着色器对象提供源代码。然后我们可以要求着色器对象被编译。这些步骤可以用下面对 OpenGL API 的三个基本调用来表示。\n\n首先，我们创建着色器对象:\n\n```cpp\nGLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);\n```\n\n我们使用`glCreateShader()`函数创建着色器对象。我们传递的参数是我们试图创建的着色器的类型。着色器的类型可以是`GL_VERTEX_SHADER`、`GL_FRAGMENT_SHADER`、`GL_GEOMETRY_SHADER`、`GL_TESS_EVALUATION_SHADER`、`GL_TESS_CONTROL_SHADER`或`GL_COMPUTE_SHADER`。在我们的例子中，我们试图编译一个顶点着色器，所以我们使用`GL_VERTEX_SHADER`类型。\n\n接下来，我们将着色器源代码复制到着色器对象中:\n\n```cpp\nGLchar* shaderCode = LoadShader(\"shaders/simple.vert\");\nglShaderSource(vertexShader, 1, shaderCode, NULL);\n```\n\n这里我们使用`glShaderSource()`函数将我们的着色器源加载到内存中。这个函数接受一个字符串数组，所以在我们调用`glShaderSource()`之前，我们使用一个尚待创建的方法创建一个指向`shaderCode`数组对象开始的指针。`glShaderSource()`的第一个参数是着色器对象的句柄。第二个是数组中包含的源代码字符串的数量。第三个参数是指向源代码字符串数组的指针。最后一个参数是一个`GLint`值数组，包含了前一个参数中每个源代码字符串的长度。\n\n最后，我们编译着色器:\n\n```cpp\nglCompileShader(vertexShader);\n```\n\n最后一步是编译着色器。我们通过调用 OpenGL API 方法`glCompileShader()`，并将句柄传递给我们想要编译的着色器来实现这一点。\n\n当然，因为我们使用内存来存储着色器，所以我们应该知道在完成后如何清理。要删除一个着色器对象，我们可以调用`glDeleteShader()`函数。\n\n删除着色器`ObjectShader`可以通过调用`glDeleteShader()`在不再需要时删除对象。这将释放着色器对象使用的内存。应该注意的是，如果着色器对象已经附加到程序对象，如链接到着色器程序，它将不会被立即删除，而是被标记为删除。如果对象被标记为删除，当它从链接着色器程序对象分离时，它将被删除。\n\n一旦我们编译了着色器，在我们可以在程序中使用它们之前，我们需要采取的下一步是将它们链接到一个完整的着色器程序中。链接步骤的核心方面之一涉及在一个着色器的输入变量和另一个着色器的输出变量之间建立连接，以及在着色器的输入/输出变量和 OpenGL 程序本身的适当位置之间建立连接。\n\n链接很像编译着色器。我们创建一个新的着色器程序，并将每个着色器对象附加到它上面。然后，我们告诉着色器程序对象将所有内容链接在一起。在 OpenGL 环境中实现这一点的步骤可以分解为对 API 的一些调用，如下所示:\n\n首先，我们创建着色器程序对象:\n\n```cpp\nGLuint shaderProgram = glCreateProgram();\n```\n\n首先，我们调用`glCreateProgram()`方法创建一个空的程序对象。该函数返回着色器程序对象的句柄，在本例中，我们将它存储在名为`shaderProgram`的变量中。\n\n接下来，我们将着色器附加到程序对象:\n\n```cpp\nglAttachShader(shaderProgram, vertexShader);\nglAttachShader(shaderProgram, fragmentShader);\n```\n\n为了将每个着色器加载到着色器程序中，我们使用`glAttachShader()`方法。这个方法需要两个参数。第一个参数是着色器程序对象的句柄，第二个参数是要附加到着色器程序的着色器对象的句柄。\n\n最后，我们链接程序:\n\n```cpp\nglLinkProgram(programHandle);\n```\n\n当我们准备将着色器链接在一起时，我们称之为`glLinkProgram()`方法。这个方法只有一个参数:我们想要链接的着色器程序的句柄。\n\n重要的是，我们要记得清理所有不再使用的着色器程序。为了从 OpenGL 内存中移除一个着色器程序，我们调用`glDeleteProgram()`方法。`glDeleteProgram()`方法有一个参数:要删除的着色器程序的句柄。此方法调用使句柄无效，并释放着色器程序使用的内存。需要注意的是，如果着色器程序对象当前正在使用，它不会被立即删除，而是被标记为删除。这类似于删除着色器对象。还需要注意的是，删除着色器程序会分离链接时附加到着色器程序的任何着色器对象。然而，这确实意味着着色器对象将被立即删除，除非那些着色器对象已经被先前对`glDeleteShader()`方法的调用标记为删除。\n\n这些就是创建、编译和链接着色器程序所需的简化的 OpenGL API 调用。现在我们将着手实现一些结构，使整个过程更容易操作。为此，我们将创建一个名为`ShaderManager`的新类。这个类将作为编译、链接和管理着色器程序清理的接口。首先，让我们看看`ShaderManager.cpp`文件中`CompileShaders()`方法的实现。我应该注意到，我将重点关注与架构实现相关的代码的重要方面。本章的完整源代码可以在 GitHub 存储库中的`Chapter07`文件夹中找到。\n\n```cpp\nvoid ShaderManager::CompileShaders(const std::string&                        \n                        vertexShaderFilePath, const std::string&      \n                        fragmentShaderFilepath)\n{\n   m_programID = glCreateProgram();\n   m_vertexShaderID = glCreateShader(GL_VERTEX_SHADER);\n   if (m_vertexShaderID == 0){\n      Exception(\"Vertex shader failed to be created!\");\n   }\n   m_fragmentShaderID = glCreateShader(GL_FRAGMENT_SHADER);\n   if (m_fragmentShaderID == 0){\n    Exception(\"Fragment shader failed to be created!\");\n   }\n   CompileShader(vertexShaderFilePath, m_vertexShaderID);\n   CompileShader(fragmentShaderFilepath, m_fragmentShaderID);\n}\n```\n\n首先，在这个例子中，我们关注两种着色器类型，因此我们的`ShaderManager::CompileShaders()`方法接受两个参数。第一个参数是顶点着色器文件的文件路径位置，第二个参数是片段着色器文件的文件路径位置。两者都是字符串。在方法体内，我们首先使用`glCreateProgram()`方法创建着色器程序句柄，并将其存储在`m_programID`变量中。接下来，我们使用`glCreateShader()`命令为顶点和片段着色器创建控制柄。我们在创建着色器句柄时会检查任何错误，如果发现任何错误，我们会抛出一个着色器名称失败的异常。一旦创建了句柄，我们就调用`CompileShader()`方法，接下来我们将会看到它。`CompileShader()`函数接受两个参数:第一个是着色器文件的路径，第二个是编译后的着色器将存储在其中的句柄。\n\n以下是完整的`CompileShader()`功能。它处理从存储中查找和加载着色器文件，以及调用着色器文件上的 OpenGL 编译命令。我们将一大块一大块地分解它:\n\n```cpp\nvoid ShaderManager::CompileShader(const std::string& filePath, GLuint id) \n{\n  std::ifstream shaderFile(filePath);\n  if (shaderFile.fail()){\n     perror(filePath.c_str());\n     Exception(\"Failed to open \" + filePath);\n  }\n    //File contents stores all the text in the file\n     std::string fileContents = \"\";\n    //line is used to grab each line of the file\n    std::string line;\n   //Get all the lines in the file and add it to the contents\n    while (std::getline(shaderFile, line)){\n    fileContents += line + \"n\";\n }\n   shaderFile.close();\n   //get a pointer to our file contents c string\n   const char* contentsPtr = fileContents.c_str();   //tell opengl that        \n   we want to use fileContents as the contents of the shader file \n  glShaderSource(id, 1, &contentsPtr, nullptr);\n  //compile the shader\n  glCompileShader(id);\n  //check for errors\n  GLint success = 0;\n  glGetShaderiv(id, GL_COMPILE_STATUS, &success);\n  if (success == GL_FALSE){\n    GLint maxLength = 0;\n    glGetShaderiv(id, GL_INFO_LOG_LENGTH, &maxLength);\n    //The maxLength includes the NULL character\n    std::vector<char> errorLog(maxLength);\n    glGetShaderInfoLog(id, maxLength, &maxLength, &errorLog[0]);\n    //Provide the infolog in whatever manor you deem best.\n    //Exit with failure.\n    glDeleteShader(id); //Don't leak the shader.\n    //Print error log and quit\n    std::printf(\"%sn\", &(errorLog[0]));\n        Exception(\"Shader \" + filePath + \" failed to compile\");\n  }\n}\n```\n\n要启动该功能，我们首先使用一个`ifstream`对象打开包含着色器代码的文件。我们还会检查加载文件是否有任何问题，如果有，我们会抛出一个异常，通知我们文件无法打开:\n\n```cpp\nstd::ifstream shaderFile(filePath);\nif (shaderFile.fail()) {\n  perror(filePath.c_str());\n  Exception(\"Failed to open \" + filePath);\n}\n```\n\n接下来，我们需要解析着色器。为此，我们创建了一个名为`fileContents`的字符串变量，它将保存着色器文件中的文本。然后我们创建另一个名为 line 的字符串变量；这将是我们试图解析的着色器文件的每一行的临时持有者。接下来，我们使用`while`循环遍历着色器文件，逐行解析内容并将每个循环保存到`fileContents`字符串中。一旦所有行都被读入 holder 变量，我们调用`shaderFile` `ifstream`对象上的 close 方法来释放用于读取文件的内存:\n\n```cpp\nstd::string fileContents = \"\";\nstd::string line;\nwhile (std::getline(shaderFile, line)) {\n  fileContents += line + \"n\";\n}\nshaderFile.close();\n```\n\n您可能还记得在本章前面我提到过，当我们使用`glShaderSource()`函数时，我们必须将着色器文件文本作为指针传递给字符数组的开头。为了满足这一要求，我们将使用一个巧妙的技巧，使用字符串类中内置的 C 字符串对话方法，允许我们将指针传递回着色器字符数组的开头。如果你不熟悉，这就是字符串的本质:\n\n```cpp\nconst char* contentsPtr = fileContents.c_str();\n```\n\n现在我们有了一个指向着色器文本的指针，我们可以调用`glShaderSource()`方法来告诉 OpenGL 我们想要使用文件的内容来编译我们的着色器。然后，最后，我们调用`glCompileShader()`方法，将着色器的句柄作为参数:\n\n```cpp\nglShaderSource(id, 1, &contentsPtr, nullptr);\nglCompileShader(id);\n```\n\n它处理编译，但是为我们自己提供一些调试支持是一个好主意。我们通过关闭`CompileShader()`函数来实现这种编译调试支持，首先检查编译过程中是否有任何错误。我们通过`glGetShaderiv()`函数向着色器编译器请求信息来实现这一点，该函数的参数中包含一个枚举值，该值指定了我们想要返回的信息。在这个调用中，我们请求编译状态:\n\n```cpp\nGLint success = 0;\nglGetShaderiv(id, GL_COMPILE_STATUS, &success);\n```\n\n接下来，我们检查返回值是否为`GL_FALSE`，如果是，这意味着我们出现了错误，应该向编译器询问有关编译问题的更多信息。为此，我们首先询问编译器错误日志的最大长度是多少。我们使用这个最大长度值来创建一个称为错误日志的字符值向量。然后，我们可以使用`glGetShaderInfoLog()`方法请求着色器编译日志，将我们正在提取的字符数以及我们想要保存日志的位置传递给着色器文件的句柄:\n\n```cpp\nif (success == GL_FALSE){\n  GLint maxLength = 0;\n  glGetShaderiv(id, GL_INFO_LOG_LENGTH, &maxLength);\n  std::vector<char> errorLog(maxLength); \n  glGetShaderInfoLog(id, maxLength, &maxLength, &errorLog[0]);\n```\n\n一旦我们保存了日志文件，我们继续使用`glDeleteShader()`方法删除着色器。这确保我们的着色器不会有任何内存泄漏:\n\n```cpp\nglDeleteShader(id);\n```\n\n最后，我们首先将错误日志打印到控制台窗口。这对运行时调试非常有用。我们还抛出了着色器名称/文件路径的异常，以及它未能编译的消息:\n\n```cpp\nstd::printf(\"%sn\", &(errorLog[0]));\nException(\"Shader \" + filePath + \" failed to compile\");\n}\n...\n```\n\n通过为底层的应用编程接口调用提供一个简单的接口，这确实简化了编译我们的着色器的过程。现在，在我们的示例程序中，为了加载和编译我们的着色器，我们使用了一行简单的代码，如下所示:\n\n```cpp\nshaderManager.CompileShaders(\"Shaders/SimpleShader.vert\",\n\"Shaders/SimpleShader.frag\");\n```\n\n现在已经编译了着色器，我们已经接近一个可用的着色器程序。我们还需要增加一个环节，链接。为了抽象出一些链接着色器的过程，并为我们提供一些调试功能，我们将为我们的`ShaderManager`类创建`LinkShaders()`方法。让我们看一看，然后分解一下:\n\n```cpp\nvoid ShaderManager::LinkShaders() {\n//Attach our shaders to our program\nglAttachShader(m_programID, m_vertexShaderID);\nglAttachShader(m_programID, m_fragmentShaderID);\n//Link our program\nglLinkProgram(m_programID);\n//Note the different functions here: glGetProgram* instead of glGetShader*.\nGLint isLinked = 0;\nglGetProgramiv(m_programID, GL_LINK_STATUS, (int *)&isLinked);\nif (isLinked == GL_FALSE){\n  GLint maxLength = 0;\n  glGetProgramiv(m_programID, GL_INFO_LOG_LENGTH, &maxLength);\n  //The maxLength includes the NULL character\n  std::vector<char> errorLog(maxLength);\n  glGetProgramInfoLog(m_programID, maxLength, &maxLength,   \n  &errorLog[0]);\n  //We don't need the program anymore.\n  glDeleteProgram(m_programID);\n  //Don't leak shaders either.\n  glDeleteShader(m_vertexShaderID);\n  glDeleteShader(m_fragmentShaderID);\n  //print the error log and quit\n  std::printf(\"%sn\", &(errorLog[0]));\n  Exception(\"Shaders failed to link!\");\n}\n  //Always detach shaders after a successful link.\n  glDetachShader(m_programID, m_vertexShaderID);\n  glDetachShader(m_programID, m_fragmentShaderID);\n  glDeleteShader(m_vertexShaderID);\n  glDeleteShader(m_fragmentShaderID);\n}\n```\n\n为了启动我们的`LinkShaders()`函数，我们调用`glAttachShader()`方法两次，分别使用之前创建的着色器程序对象的句柄和我们希望链接的每个着色器的句柄:\n\n```cpp\nglAttachShader(m_programID, m_vertexShaderID);\nglAttachShader(m_programID, m_fragmentShaderID);\n```\n\n接下来，我们通过调用`glLinkProgram()`方法，使用程序对象的句柄作为参数，将着色器实际链接到可用的着色器程序中:\n\n```cpp\nglLinkProgram(m_programID);\n```\n\n然后，我们可以检查链接过程是否已经完成，没有任何错误，并为自己提供任何调试信息，如果有任何错误，我们可能需要这些信息。我不打算一行一行地浏览这段代码，因为它几乎与我们使用`CompileShader()`函数所做的相同。但是，请注意，从链接器返回信息的函数略有不同，它使用的是`glGetProgram*`而不是之前的`glGetShader*`函数:\n\n```cpp\nGLint isLinked = 0;\nglGetProgramiv(m_programID, GL_LINK_STATUS, (int *)&isLinked);\nif (isLinked == GL_FALSE){\n  GLint maxLength = 0;\n  glGetProgramiv(m_programID, GL_INFO_LOG_LENGTH, &maxLength);\n  //The maxLength includes the NULL character\n  std::vector<char> errorLog(maxLength);  \n  glGetProgramInfoLog(m_programID, maxLength, &maxLength,   \n  &errorLog[0]);\n  //We don't need the program anymore.\n  glDeleteProgram(m_programID);\n  //Don't leak shaders either.\n  glDeleteShader(m_vertexShaderID);\n  glDeleteShader(m_fragmentShaderID);\n  //print the error log and quit\n  std::printf(\"%sn\", &(errorLog[0]));\n  Exception(\"Shaders failed to link!\");\n}\n```\n\n最后，如果我们在链接过程中成功了，我们需要清理一下。首先，我们使用`glDetachShader()`方法从链接器分离着色器。接下来，由于我们有一个完整的着色器程序，我们不再需要将着色器保存在内存中，因此我们通过调用`glDeleteShader()`方法来删除每个着色器。同样，这将确保我们在着色器程序创建过程中不会泄漏任何内存:\n\n```cpp\n  glDetachShader(m_programID, m_vertexShaderID);\n  glDetachShader(m_programID, m_fragmentShaderID);\n  glDeleteShader(m_vertexShaderID);\n  glDeleteShader(m_fragmentShaderID);\n}\n```\n\n我们现在有了一种将着色器链接到工作着色器程序的简化方法。我们可以通过简单地使用一行代码将这个接口调用到底层的 API 调用，类似于下面的代码:\n\n```cpp\n  shaderManager.LinkShaders();\n```\n\n这样就处理了编译和链接我们的着色器的过程，但是使用着色器还有另一个关键方面，那就是在运行的程序/游戏和运行在 GPU 上的着色器程序之间传递数据。接下来，我们将研究这个过程，以及如何将其抽象成一个易于使用的界面，供我们的引擎使用。\n\n# 使用着色器数据\n\n使用着色器最重要的一个方面是将数据传入和传出运行在 GPU 上的着色器程序的能力。这可以是一个很深的话题，而且很像本书中的其他话题都有自己的专属书籍。在讨论这个主题时，我们将停留在更高的层次，并将再次关注基本渲染所需的两种着色器类型:顶点着色器和片段着色器。\n\n首先，让我们看看如何使用顶点属性和**顶点缓冲对象** ( **VBO** )将数据发送到着色器。顶点着色器的工作是处理连接到顶点的数据，进行任何修改，然后将其传递到渲染管道的下一阶段。每个顶点发生一次。为了让着色器做它的事情，我们需要能够传递它的数据。为此，我们使用所谓的顶点属性，它们通常与所谓的 VBO 一起工作。\n\n对于顶点着色器，所有逐顶点输入属性都是使用关键字`in`定义的。例如，如果我们想要定义一个名为 VertexColour 的向量 3 输入属性，我们可以编写如下内容:\n\n```cpp\nin vec3 VertexColour;\n```\n\n现在，`VertexColour`属性的数据必须由程序/游戏提供。这就是 VBO 进来的地方。在我们的主游戏或程序中，我们在输入属性和顶点缓冲区对象之间建立连接，我们还必须定义如何解析或遍历数据。这样，当我们渲染时，OpenGL 可以为顶点着色器的每次调用从缓冲区中提取属性数据。\n\n让我们看看一个非常简单的顶点着色器:\n\n```cpp\n#version 410\nin vec3 VertexPosition;\nin vec3 VertexColour;\nout vec3 Colour;\nvoid main(){\n  Colour = VertexColour;\n  gl_Position = vec4(VertexPosition, 1.0);\n}\n```\n\n在这个例子中，这个顶点着色器只有两个输入变量，`VertexPosition`和`VertexColor`。我们的主 OpenGL 程序需要为每个顶点提供这两个属性的数据。我们将通过将我们的多边形/网格数据映射到这些变量来实现。我们还有一个名为 Colour 的输出变量，它将被发送到渲染管道的下一个阶段，即片段着色器。在这个例子中，颜色只是`VertexColour`的未被触及的拷贝。`VertexPosition`属性被简单地扩展并传递给 OpenGL API 输出变量`gl_Position`进行更多处理。\n\n接下来，让我们看看一个非常简单的片段着色器:\n\n```cpp\n#version 410\nin vec3 Colour;\nout vec4 FragColour;\nvoid main(){\n  FragColour = vec4(Colour, 1.0);\n}\n```\n\n在这个片段着色器示例中，只有一个输入属性`Colour`。该输入对应于前一渲染阶段的输出，即顶点着色器的`Colour`输出。为了简单起见，我们只是扩展了`Colour`并将其作为变量`FragColour`输出到下一个渲染阶段。\n\n这总结了连接的着色器方面，那么我们如何从引擎内部合成和发送数据呢？我们基本上可以通过四个步骤来完成。\n\n首先，我们创建一个**顶点数组对象** ( **VAO** )实例来保存我们的数据:\n\n```cpp\nGLunit vao;\n```\n\n接下来，我们为每个着色器的输入属性创建并填充 VBO。我们首先创建一个 VBO 变量，然后使用`glGenBuffers()`方法，为缓冲区对象生成内存。然后，我们为需要缓冲区的不同属性创建句柄，将它们分配给 VBO 数组中的元素。最后，我们通过首先调用`glBindBuffer()`方法来填充每个属性的缓冲区，指定存储的对象类型。在这种情况下，两种属性都是`GL_ARRAY_BUFFER`。然后我们调用`glBufferData()`方法，传递类型、大小和句柄进行绑定。`glBufferData()`方法的最后一个论点是给 OpenGL 一个关于如何使用数据的提示，以便它可以决定如何最好地管理内部缓冲区。有关该参数的完整细节，请查看 OpenGL 文档:\n\n```cpp\nGLuint vbo[2];\nglGenBuffers(2, vbo);\nGLuint positionBufferHandle = vbo[0];\nGLuint colorBufferHandle = vbo[1];\nglBindBuffer(GL_ARRAY_BUFFER,positionBufferHandle);\nglBufferData(GL_ARRAY_BUFFER,\n             9 * sizeof(float),\n             positionData,\n             GL_STATIC_DRAW);\nglBindBuffer(GL_ARRAY_BUFFER,\n             colorBufferHandle);\nglBufferData(GL_ARRAY_BUFFER,\n             9 * sizeof(float),\n             colorData,\n             GL_STATIC_DRAW);\n```\n\n第三步是创建和定义 VAO。这就是我们如何定义着色器的输入属性和我们刚刚创建的缓冲区之间的关系。VAO 包含了这些联系的信息。为了创建一个 VAO，我们使用`glGenVertexArrays()`方法。这给了我们一个新对象的句柄，我们将其存储在之前创建的 VAO 变量中。然后，我们通过调用`glEnableVertexAttribArray()`方法来启用通用顶点属性索引 0 和 1。通过调用来启用属性，我们指定它们将被访问并用于呈现。最后一步使我们创建的缓冲区对象和通用顶点属性索引之间的连接也匹配:\n\n```cpp\nglGenVertexArrays( 1, &vao );\nglBindVertexArray(vao);\nglEnableVertexAttribArray(0);\nglEnableVertexAttribArray(1);\nglBindBuffer(GL_ARRAY_BUFFER, positionBufferHandle);\nglVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, NULL);\nglBindBuffer(GL_ARRAY_BUFFER, colorBufferHandle);\nglVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 0, NULL);\n```\n\n最后，在我们的`Draw()`函数调用中，我们绑定到 VAO 并调用`glDrawArrays()`来执行实际渲染:\n\n```cpp\nglBindVertexArray(vaoHandle);glDrawArrays(GL_TRIANGLES, 0, 3 );\n```\n\n在我们转向另一种将数据传递给着色器的方式之前，我们还需要讨论这个属性连接结构的另一部分。如上所述，在链接时，着色器中的输入变量链接到我们刚才看到的通用顶点属性。当我们需要指定关系结构时，我们有几个不同的选择。我们可以在着色器代码本身中使用所谓的布局限定符。以下是一个例子:\n\n```cpp\nlayout (location=0) in vec3 VertexPosition;\n```\n\n另一种选择是让链接器在链接时创建映射，然后再查询它们。第三种，也是我个人比较喜欢的一种，是在链接过程之前通过调用`glBindAttribLocation()`方法来指定关系。当我们讨论如何抽象这些过程时，我们将很快看到这是如何实现的。\n\n我们已经描述了如何使用属性将数据传递给着色器，但是还有另一个选项:统一变量。统一变量专门用于不常变化的数据。例如，矩阵是统一变量的最佳候选。在着色器中，统一变量是只读的。这意味着只能从着色器外部更改该值。它们也可以出现在同一着色器程序中的多个着色器中。它们可以在程序中的一个或多个着色器中声明，但是如果在多个着色器中声明了具有给定名称的变量，则其类型在所有着色器中必须相同。这让我们深入了解了这样一个事实:统一变量实际上被保存在整个着色器程序的共享命名空间中。\n\n要在着色器中使用统一变量，首先必须使用统一标识符关键字在着色器文件中声明它。以下是可能的情况:\n\n```cpp\nuniform mat4 ViewMatrix;\n```\n\n然后我们需要从我们的游戏/程序内部提供统一变量的数据。为此，我们首先使用`glGetUniformLocation()`方法找到变量的位置。然后，我们使用`glUniform()`方法之一为找到的位置赋值。这个过程的代码可能如下所示:\n\n```cpp\nGLuint location = glGetUniformLocation(programHandle,\" ViewMatrix \");\nif( location >= 0 )\n{\nglUniformMatrix4fv(location, 1, GL_FALSE, &viewMatrix [0][0])\n}\n```\n\n然后，我们使用`glUniformMatrix4fv()`方法为统一变量的位置赋值。第一个参数是统一变量的位置。第二个参数是被赋值的矩阵的数量。第三种是 GL `bool`类型，指定矩阵是否应该转置。因为我们的矩阵使用 GLM 库，所以不需要转置。如果您正在使用以行为主的顺序而不是以列为主的顺序来实现矩阵，您可能需要使用`GL_TRUE`类型作为这个参数。最后一个参数是指向统一变量数据的指针。\n\n统一变量可以是任何 GLSL 类型，这包括复杂类型，如结构和数组。OpenGL API 提供了一个`glUniform()`函数，该函数具有与每种类型匹配的不同后缀。例如，要分配给类型为`vec3`的变量，我们将使用`glUniform3f()`或`glUniform3fv()`方法。( *v* 表示数组中的多个值)。\n\n因此，这些是在我们的着色器程序之间传递数据的概念和技术。然而，正如我们对着色器的编译和链接所做的那样，我们可以将这些过程抽象成我们的`ShaderManager`类中包含的函数。我们将专注于处理属性和统一变量。我们确实有一个很棒的类，它抽象了 VAO 和 VBO 为模型/网格创建的内容，我们在[第 4 章](05.html)、*构建游戏系统*中详细介绍过，当时我们讨论了构建素材管道。要了解这是如何构建的，请返回到[第 4 章](https://cdp.packtpub.com/mastering_c___game_development/wp-admin/post.php?post=325&action=edit#post_245)、*构建游戏系统、*或查看`BookEngine`解决方案的`Mesh.h`和`Mesh.cpp`文件中的实现。\n\n首先，我们将看看使用`ShaderManger`类的`AddAttribute()`函数添加属性绑定的抽象。这个函数接受一个参数，属性的名称，作为一个字符串绑定。然后我们调用`glBindAttribLocation()`函数，传递程序的句柄和当前的索引或属性数量，我们在调用时增加，最后是`attributeName`字符串的 C 字符串转换，它提供了指向字符串数组中第一个字符的指针。此函数必须在编译之后，但在链接着色器程序之前调用:\n\n```cpp\nvoid ShaderManager::AddAttribute(const std::string& attributeName)\n{\nglBindAttribLocation(m_programID,\n                     m_numAttributes++,\n                     attributeName.c_str());\n }\n```\n\n对于统一变量，我们创建了一个抽象函数，在着色器程序中查找统一的位置，`GetUniformLocation()`函数。这个函数同样只接受一个变量，它是一个字符串形式的统一名称。然后，我们为该位置创建一个临时持有者，并为其分配`glGetUniformLocation()`方法调用的返回值。我们检查以确保位置是有效的，如果不是，我们抛出一个异常，让我们知道错误。最后，如果找到，我们返回有效位置:\n\n```cpp\nGLint ShaderManager::GetUniformLocation(const std::string& uniformName)\n{\n    GLint location = glGetUniformLocation(m_programID,\n    uniformName.c_str());\n    if (location == GL_INVALID_INDEX) \n    {\n     Exception(\"Uniform \" + uniformName + \" not found in shader!\");\n    }\n  return location;\n}\n```\n\n这给了我们绑定数据的抽象，但是我们仍然需要为某个绘制调用分配应该使用的着色器，并激活我们需要的任何属性。为此，我们在`ShaderManager`中创建了一个名为`Use()`的函数。该函数将首先使用`glUseProgram()`应用编程接口方法调用将当前着色器程序设置为活动着色器程序。然后，我们使用 for 循环遍历着色器程序的属性列表，激活每个属性:\n\n```cpp\nvoid ShaderManager::Use(){\n  glUseProgram(m_programID);\n  for (int i = 0; i < m_numAttributes; i++) { \n    glEnableVertexAttribArray(i);\n  }\n}\n```\n\n当然，因为我们有一个抽象的方法来启用着色器程序，所以我们应该有一个函数来禁用着色器程序才是有意义的。这个函数与`Use()`函数非常相似，但是在这种情况下，我们将正在使用的程序设置为 0，有效地使其成为`NULL`，并且我们使用`glDisableVertexAtrribArray()`方法禁用 for 循环中的属性:\n\n```cpp\nvoid ShaderManager::UnUse() {\n  glUseProgram(0);\n  for (int i = 0; i < m_numAttributes; i++) {\n    glDisableVertexAttribArray(i);\n }\n}\n```\n\n这种抽象的净效果是，我们现在可以通过几个简单的调用来设置整个着色器程序结构。类似于下面的代码将创建和编译着色器，添加必要的属性，将着色器链接到程序中，定位统一变量，并为网格创建 VAO 和 VBO:\n\n```cpp\nshaderManager.CompileShaders(\"Shaders/SimpleShader.vert\",\n                             \"Shaders/SimpleShader.frag\");\nshaderManager.AddAttribute(\"vertexPosition_modelspace\");\nshaderManager.AddAttribute(\"vertexColor\");\nshaderManager.LinkShaders();\nMatrixID = shaderManager.GetUniformLocation(\"ModelViewProjection\");\nm_model.Init(\"Meshes/Dwarf_2_Low.obj\", \"Textures/dwarf_2_1K_color.png\");\n```\n\n然后，在我们的`Draw`循环中，如果我们想使用这个着色器程序来绘制，我们可以简单地使用抽象的函数来激活和停用我们的着色器，类似于下面的代码:\n\n```cpp\n  shaderManager.Use();\n  m_model.Draw();\n  shaderManager.UnUse();\n```\n\n这使得我们更容易使用着色器来使用和测试高级渲染技术。我们将使用这个结构来构建本章剩余部分以及本书剩余部分的例子。\n\n# 灯光效果\n\n着色器最常见的用途之一是创建照明和反射效果。使用着色器获得的灯光效果有助于提供每个现代游戏都追求的抛光和细节水平。在下一节中，我们将了解一些用于创建不同表面外观效果的著名模型，以及可以实现来复制所讨论的照明效果的着色器示例。\n\n# 逐顶点漫射\n\n首先，我们将看一个更简单的光照顶点着色器，漫反射着色器。漫射被认为是更简单的，因为我们假设我们渲染的表面看起来向各个方向均匀地散射光线。使用此着色器，光线会与表面接触并轻微穿透，然后向各个方向投射出去。这意味着一些光的波长将至少被部分吸收。漫射着色器外观的一个很好的例子是考虑哑光绘画。表面看起来非常暗淡，没有光泽。\n\n让我们快速看一下漫反射的数学模型。这个反射模型采用两个向量。一个是表面接触点到初始光源的方向，第二个是同一个表面接触点的法向量。这看起来如下所示:\n\n![](img/61cd3123-d51e-46a5-9e49-fe95a5c7b232.png)\n\n值得注意的是，照射到表面的光量部分取决于与光源相关的表面，到达单个点的光量沿法向量将达到最大值，垂直于法向量时达到最小值。掸掉我们的物理知识工具箱，我们能够通过计算点法向量和入射光向量的点积来表达这种关系，给定与点接触的光量。这可以通过以下公式表示:\n\n*Light Density(Source Vector) Normal Vector*\n\n假设这个方程中的源和法向量是归一化的。\n\n如前所述，撞击表面的一些光线会在重新投射之前被吸收。为了将这种行为添加到我们的数学模型中，我们可以添加一个反射系数，也称为漫反射率。该系数值成为入射光的比例因子。我们指定光出射强度的新公式如下所示:\n\n*出射光=(漫射系数×光密度×源矢量)法向矢量*\n\n有了这个新公式，我们现在有了一个代表全向均匀散射的照明模型。\n\n好了，现在我们知道了这个理论，让我们来看看如何在 GLSL 着色器中实现这个照明模型。本例的完整源代码可以在 GitHub 存储库的`Chapter07`文件夹中找到，从顶点着色器开始，如下所示:\n\n```cpp\n#version 410\nin vec3 vertexPosition_modelspace;\nin vec2 vertexUV;\nin vec3 vertexNormal;\nout vec2 UV;\nout vec3 LightIntensity;\nuniform vec4 LightPosition;\nuniform vec3 DiffuseCoefficient ;\nuniform vec3 LightSourceIntensity;\nuniform mat4 ModelViewProjection;\nuniform mat3 NormalMatrix;\nuniform mat4 ModelViewMatrix;\nuniform mat4 ProjectionMatrix;\nvoid main(){\n    vec3 tnorm = normalize(NormalMatrix * vertexNormal);\n    vec4 CameraCoords = ModelViewMatrix *\n    vec4(vertexPosition_modelspace,1.0);\n    vec3 IncomingLightDirection = normalize(vec3(LightPosition -\n    CameraCoords));\n    LightIntensity = LightSourceIntensity * DiffuseCoefficient *\n                     max( dot( IncomingLightDirection, tnorm ), 0.0 );\n    gl_Position = ModelViewProjection *                   \n                  vec4(vertexPosition_modelspace,1);\n                  UV = vertexUV;\n }\n```\n\n我们将一个块一个块地检查这个着色器。首先，我们有我们的属性，`vertexPosition_modelspace`、`vertexUV`和`vertexNormal`。这些将由我们的游戏应用设置，我们将在通过着色器后查看。然后我们有我们的输出变量，紫外线和`LightIntensity`。这些值将在着色器本身中计算。然后我们有我们的制服。这些包括我们讨论的反射计算所需的值。它还包括所有必要的矩阵。像属性一样，这些统一的值将通过我们的游戏来设置。\n\n在这个着色器的主要功能中，我们的漫反射将在相机相对坐标中计算。为此，我们首先通过将顶点法线乘以法线矩阵，并将结果存储在名为`tnorm`的向量 3 变量中，从而对顶点法线进行归一化。接下来，我们通过用模型视图矩阵对其进行变换，将当前在模型空间中的顶点位置转换为相机坐标。然后，我们通过从光线的位置中减去相机坐标中的顶点位置来计算入射光线的方向(归一化)。接下来，我们使用前面的公式计算出出射光强。这里需要注意的一点是 max 函数的使用。这是光线方向大于 90 度的情况，因为光线来自物体内部。因为在我们的情况下，我们不需要支持这种情况，所以当这种情况出现时，我们只使用`0.0`的值。为了关闭着色器，我们将在剪辑空间中计算的模型视图投影矩阵存储在内置的出站变量`gl_position`中。我们也传递纹理的紫外线，没有改变，在这个例子中我们没有实际使用。\n\n现在我们已经有了着色器，我们需要提供计算所需的值。正如我们在本章第一节中所学的，我们通过设置属性和制服来做到这一点。我们构建了一个抽象层来帮助这个过程，所以让我们看看如何在我们的游戏代码中设置这些值。在`GamePlayScreen.cpp`文件中，我们在`Draw()`功能中设置这些值。我应该指出，这是示例，在生产环境中，出于性能原因，您只想在循环中设置不断变化的值。因为这是一个例子，所以我想让它更容易理解:\n\n```cpp\nGLint DiffuseCoefficient =    \n        shaderManager.GetUniformLocation(\"DiffuseCoefficient \");\nglUniform3f(DiffuseCoefficient, 0.9f, 0.5f, 0.3f);\nGLint LightSourceIntensity =    \n       shaderManager.GetUniformLocation(\"LightSourceIntensity \");\nglUniform3f(LightSourceIntensity, 1.0f, 1.0f, 1.0f);\nglm::vec4 lightPos = m_camera.GetView() * glm::vec4(5.0f, 5.0f, 2.0f,              \n                     1.0f);\nGLint lightPosUniform =      \n                shaderManager.GetUniformLocation(\"LightPosition\");\nglUniform4f(lightPosUniform, lightPos[0], lightPos[1], lightPos[2],    \n             lightPos[3]);\nglm::mat4 modelView = m_camera.GetView() * glm::mat4(1.0f);\nGLint modelViewUniform =           \n               shaderManager.GetUniformLocation(\"ModelViewMatrix\");\nglUniformMatrix4fv(modelViewUniform, 1, GL_FALSE, &modelView[0][0]);\nglm::mat3 normalMatrix = glm::mat3(glm::vec3(modelView[0]),     \n                         glm::vec3(modelView[1]),  \n                         glm::vec3(modelView[2]));\nGLint normalMatrixUniform =     \n                   shaderManager.GetUniformLocation(\"NormalMatrix\");\nglUniformMatrix3fv(normalMatrixUniform, 1, GL_FALSE, &normalMatrix[0][0]);\nglUniformMatrix4fv(MatrixID, 1, GL_FALSE, &m_camera.GetMVPMatrix()[0][0]);\n```\n\n我不会仔细检查每一行，因为我相信你能看到模式。我们首先使用着色器管理器的`GetUniformLocation()`方法返回制服的位置。接下来，我们使用与值类型匹配的 OpenGL `glUniform*()`方法来设置这个统一的值。我们这样做是为了所有需要的统一价值。我们还必须设置我们的属性，正如本章开头所讨论的，我们在编译和链接过程之间进行设置。在本例中，我们在`GamePlayScreen()`类的`OnEntry()`方法中设置这些值:\n\n```cpp\nshaderManager.AddAttribute(\"vertexPosition_modelspace\");\nshaderManager.AddAttribute(\"vertexColor\");\nshaderManager.AddAttribute(\"vertexNormal\");\n```\n\n这将处理顶点着色器并传递所需的值，所以接下来，让我们看看这个示例的片段着色器:\n\n```cpp\n#version 410\nin vec2 UV;\nin vec3 LightIntensity;\n// Ouput data\nout vec3 color;\n// Values that stay constant for the whole mesh.\nuniform sampler2D TextureSampler;\nvoid main(){\n  color = vec3(LightIntensity);\n}\n```\n\n对于这个例子，我们的片段着色器非常简单。首先，我们有紫外线和`LightIntensity`的 in 值，这次我们只使用`LightIntensity`。然后我们声明我们的输出颜色值，指定为矢量 3。接下来，我们有了用于纹理的`sampler2D`制服，但是在示例中我们不会使用这个值。最后，我们有主要功能。这是我们通过简单地将`LightIntensity`传递到管道中的下一阶段来设置最终输出颜色的地方。\n\n如果运行示例项目，您将看到漫反射在起作用。输出应该如下图所示。如您所见，这种反射模型适用于非常沉闷但在实际环境中用途有限的表面。接下来，我们将看一个反射模型，它将允许我们描绘更多的表面类型:\n\n![](img/8e83e885-ac5e-4c00-91b7-03998dd9380e.png)\n\n# 逐顶点环境、漫反射和镜面反射\n\n**环境**、**漫反射**、**和镜面** ( **ADS** )反射模型，也称为 **Phong 反射模型**，提供了一种创建反射照明着色器的方法。这种技术使用三种不同成分的组合来模拟光在表面上的相互作用。环境组件模拟来自环境的光线；这是为了模拟如果光被多次反射会发生什么，在那里它看起来好像是从任何地方发出的。我们在前面的示例中建模的漫射分量代表全向反射。最后一个分量，镜面分量，是指代表在优选方向上的反射，提供光*眩光*或亮点的外观。\n\n组件的这种组合可以使用下图进行可视化:\n\n![](img/45633898-8754-47a0-b4b8-f102297cca8f.png)\n\nSource: Wikipedia\n\n这个过程可以分解成单独的部分进行讨论。首先，我们有代表光线的环境分量，该光线将均等地照亮所有表面，并在所有方向上均匀反射。这种照明效果不依赖于光的入射或出射矢量，因为它是均匀分布的，并且可以通过简单地将光源强度乘以表面反射率来表示。这表现在数学公式*I<sub>a</sub>= L<sub>a</sub>K<sub>a</sub>T7】中。*\n\n下一个组件是我们前面讨论过的漫射组件。漫射组件模拟一个暗淡或粗糙的表面，将光线散射到各个方向。同样，这可以用数学公式*I<sub>d</sub>= L<sub>d</sub>K<sub>d</sub>(sn)*来表示。\n\n最后一个组件是镜面组件，用于建模表面的*光泽*。这会产生一种*眩光*或亮点，这在具有光泽特性的表面上很常见。我们可以使用下图来可视化这种反射效果:\n\n![](img/46525544-c9ce-4200-b03c-bf4de5fc5118.png)\n\n对于镜面反射分量，理想情况下，我们希望反射在与反射矢量对齐时最明显，然后随着角度的增加或减少而减弱。我们可以使用我们的观察向量和反射角之间的角度的余弦来模拟这种效果，该角度随后被提高一些幂，如这个等式所示: *(r v) <sup>p</sup>* 。在这个等式中， *p* 代表镜面高光，*眩光*光斑。为 *p* 输入的数值越大，斑点出现的越小，表面看起来*越闪亮*。将表示表面反射率和镜面光强的值相加后，计算表面镜面效果的公式如下:*I<sub>s</sub>= L<sub>s</sub>K<sub>s</sub>(r v)<sup>p</sup>*。\n\n所以，现在，如果我们把所有的组件放在一个公式中，我们会得出*I = I<sub>a</sub>+I<sub>d</sub>+I<sub>s</sub>T7】或者更细分一些，*I = L<sub>a</sub>K<sub>a</sub>+L<sub>d</sub>K<sub>d</sub>(sn)+L<sub>s</sub>K**\n\n有了我们的理论，让我们看看如何在逐顶点着色器中实现这一点，从顶点着色器开始，如下所示:\n\n```cpp\n#version 410\n// Input vertex data, different for all executions of this shader.\nin vec3 vertexPosition_modelspace;\nin vec2 vertexUV;\nin vec3 vertexNormal;\n// Output data ; will be interpolated for each fragment.\nout vec2 UV;\nout vec3 LightIntensity;\nstruct LightInfo {\n  vec4 Position; // Light position in eye coords.\n  vec3 La; // Ambient light intensity\n  vec3 Ld; // Diffuse light intensity\n  vec3 Ls; // Specular light intensity\n};\nuniform LightInfo Light;\nstruct MaterialInfo {\n  vec3 Ka; // Ambient reflectivity\n  vec3 Kd; // Diffuse reflectivity\n  vec3 Ks; // Specular reflectivity\n  float Shininess; // Specular shininess factor\n};\n  uniform MaterialInfo Material;\n  uniform mat4 ModelViewMatrix;\n  uniform mat3 NormalMatrix;\n  uniform mat4 ProjectionMatrix;\n  uniform mat4 ModelViewProjection;\n  void main(){\n     vec3 tnorm = normalize( NormalMatrix * vertexNormal);\n     vec4 CameraCoords = ModelViewMatrix *                \n                     vec4(vertexPosition_modelspace,1.0);\n     vec3 s = normalize(vec3(Light.Position - CameraCoords));\n     vec3 v = normalize(-CameraCoords.xyz);\n     vec3 r = reflect( -s, tnorm );\n     float sDotN = max( dot(s,tnorm), 0.0 );\n     vec3 ambient = Light.La * Material.Ka;\n     vec3 diffuse = Light.Ld * Material.Kd * sDotN;\n     vec3 spec = vec3(0.0);\n     if( sDotN > 0.0 )\n      spec = Light.Ls * Material.Ks *\n      pow( max( dot(r,v), 0.0 ), Material.Shininess );\n      LightIntensity = ambient + diffuse + spec;\n      gl_Position = ModelViewProjection *\n                vec4(vertexPosition_modelspace,1.0);\n}\n```\n\n让我们先来看看有什么不同。在这个着色器中，我们引入了一个新概念，统一结构。我们声明两个`struct`，一个描述光，`LightInfo`，一个描述物质，`MaterialInfo`。这是一种非常有用的包含值的方式，这些值将公式中的一部分表示为集合。我们将很快看到如何从游戏代码中设置这些`struct`元素的值。转到函数的主要功能。首先，我们从前面的例子开始。我们计算`tnorm`、`CameraCoords`和光源矢量。接下来，我们计算观察者/摄像机方向的向量(v)，它是归一化`CameraCoords`的负值。然后我们使用提供的 GLSL 方法计算*纯*反射的方向，反射。然后我们继续计算三个分量的值。环境通过将环境光强度与表面的环境反射值相乘来计算。`diffuse`是利用光强度、表面的表面漫反射值以及光源矢量和`tnorm`的点积的结果来计算的，我们就在环境值之前计算的。在计算镜面值之前，我们检查`sDotN`的值。如果`sDotN`为零，那么没有光到达表面，所以计算镜面分量没有意义。如果`sDotN`大于零，我们计算镜面分量。与前面的示例一样，我们使用 GLSL 方法将点积的值的范围限制在`1`和`0`之间。GLSL 函数`pow`将点积提升到表面亮度指数的幂，我们之前在着色器方程中将其定义为`p`。\n\n最后，我们将所有三个分量值相加，并以 out 变量`LightIntensity`的形式将它们的总和传递给片段着色器。最后，我们将顶点位置转换为剪辑空间，并通过将其分配给`gl_Position`变量将其传递给下一阶段。\n\n对于着色器所需的属性和制服的设置，我们像在前面的示例中一样处理该过程。这里的主要区别是，我们需要指定我们在获取统一位置时分配的`struct`的元素。一个示例看起来类似于下面，同样，您可以在 GitHub 存储库的`Chapter07`文件夹中看到示例解决方案的完整代码:\n\n```cpp\nGLint Kd = shaderManager.GetUniformLocation(\"Material.Kd\");\nglUniform3f(Kd, 0.9f, 0.5f, 0.3f);\n```\n\n本例中使用的片段着色器与我们在漫反射示例中使用的片段着色器相同，因此我不再赘述。\n\n当您从 GitHub 存储库的`Chapter07`代码解决方案中运行 ADS 示例时，您将看到我们新创建的着色器生效，其输出如下所示:\n\n![](img/1b255e28-0a5a-4456-8efc-86158d5d6514.png)\n\n在这个例子中，我们计算了顶点着色器中的着色方程；这被称为逐顶点着色器。这种方法可能产生的一个问题是，我们的\n*眩光*点，即镜面高光，可能会扭曲或消失。这是由于着色是内插的，而不是为面上的每个点计算的。例如，由于方程是在镜面反射分量接近于零的顶点处计算的，所以设置在面中间附近的点可能不会出现。在下一个示例中，我们将研究一种通过计算片段着色器中的反射来消除该问题的技术。\n\n# 每片段 Phong 插值\n\n在前面的例子中，我们已经使用顶点着色器来处理光照计算。使用顶点着色器评估每个顶点的颜色时，如最后一个示例中所述，一个问题是颜色随后会在整个面上进行插值。这会导致一些不太好的效果。有另一种方法可以实现同样的照明效果，但精度有所提高。我们可以将计算转移到片段着色器。在片段着色器中，我们不是在整个面上插值，而是插值法线和位置，并使用这些值来计算每个片段。这种技术通常被称为 **Phong 插值**。这种技术的结果比使用每个顶点的实现要精确得多。然而，由于这种每个片段的实现评估每个片段，而不仅仅是顶点，这种实现将比每个顶点的技术运行得慢。\n\n让我们先看一下这个例子中的顶点着色器来开始我们对着色器实现的研究:\n\n```cpp\n#version 410\nin vec3 vertexPosition_modelspace;\nin vec2 vertexUV;\nin vec3 vertexNormal;\nout vec2 UV;\nout vec3 Position;\nout vec3 Normal;\nuniform mat4 ModelViewMatrix;\nuniform mat3 NormalMatrix;\nuniform mat4 ProjectionMatrix;\nuniform mat4 ModelViewProjection;\nvoid main(){\n    UV = vertexUV;\n    Normal = normalize( NormalMatrix * vertexNormal);\n    Position = vec3( ModelViewMatrix *        \n               vec4(vertexPosition_modelspace,1.0));\n    gl_Position = ModelViewProjection *\n                 vec4(vertexPosition_modelspace,1.0);\n}\n```\n\n因为这项技术使用片段着色器来执行计算，所以我们的顶点着色器相当轻。在大多数情况下，我们做一些简单的方程来计算法线和位置，然后将值传递到下一个阶段。\n\n接下来，我们将看看这个技术在片段着色器中实现的核心。下面是完整的片段着色器，我们将介绍与前面示例的不同之处:\n\n```cpp\n#version 410\nin vec3 Position;\nin vec3 Normal;\nin vec2 UV;\nuniform sampler2D TextureSampler;\nstruct LightInfo {\n  vec4 Position; // Light position in eye coords.\n  vec3 Intensity; // A,D,S intensity\n};\nuniform LightInfo Light;\nstruct MaterialInfo {\n  vec3 Ka; // Ambient reflectivity\n  vec3 Kd; // Diffuse reflectivity\n  vec3 Ks; // Specular reflectivity\n  float Shininess; // Specular shininess factor\n};\nuniform MaterialInfo Material;\nout vec3 color;\nvoid phongModel( vec3 pos, vec3 norm, out vec3 ambAndDiff, out vec3\nspec ) {\n  vec3 s = normalize(vec3(Light.Position) - pos);\n  vec3 v = normalize(-pos.xyz);\n  vec3 r = reflect( -s, norm );\n  vec3 ambient = Light.Intensity * Material.Ka;\n  float sDotN = max( dot(s,norm), 0.0 );\n  vec3 diffuse = Light.Intensity * Material.Kd * sDotN;\n  spec = vec3(0.0);\n  if( sDotN > 0.0 )\n   spec = Light.Intensity * Material.Ks *\n        pow( max( dot(r,v), 0.0 ), Material.Shininess );\n        ambAndDiff = ambient + diffuse;\n}\nvoid main() {\n   vec3 ambAndDiff, spec;\n   vec3 texColor = texture( TextureSampler, UV ).rbg;\n   phongModel( Position, Normal, ambAndDiff, spec );\n   color = (vec3(ambAndDiff * texColor) + vec3(spec));\n }\n```\n\n这个片段着色器看起来应该很熟悉，因为它与我们前面例子中的顶点着色器几乎相同。最大的区别，除了这将在每个片段而不是每个顶点运行的事实之外，是我们已经通过实现一个函数来处理 Phong 模型计算来清理着色器。我们这次也要通过一个纹理，把我们的纹理还给侏儒。Phong 模型的计算与我们之前看到的完全相同，所以我不再赘述。我们把它移到一个函数中的原因主要是为了可读性，因为它保持了主函数的整洁。在 GLSL 创建一个函数几乎和在 C++ 和 C 中一样。你有一个返回类型、一个后跟参数的函数名和一个主体。我强烈建议在任何比几行更复杂的着色器中使用函数。\n\n为了将我们的着色器连接到我们游戏中的值，我们遵循与之前相同的技术，在这里我们设置所需的属性和统一的值。对于本例，我们必须提供 Ka、Kd、Ks、材料光泽、`LightPosition`和`LightIntensity`的值。这些值与前面描述的 ADS 方程相匹配。我们还需要传入通常的矩阵值。完整的代码可以在 GitHub 存储库的`Chapter07`文件夹中找到。\n\n如果我们从`Chapter07`解决方案运行`Phong_Example`，我们将看到新的着色器正在运行，完成了纹理和更精确的反射表示。以下是输出的屏幕截图:\n\n![](img/5fcd5f76-14f7-4684-a7a6-45e392f66a21.png)\n\n我们将在这里结束我们关于照明技术的讨论，但我鼓励你继续你对这个话题的研究。使用着色器可以实现许多有趣的灯光效果，我们才真正开始触及表面。在下一节中，我们将了解着色器的另一个常见用途:渲染效果。\n\n# 使用着色器创建效果\n\n着色器不仅仅局限于创建灯光效果。您可以使用不同的着色器技术创建许多不同的视觉效果。在本节中，我们将介绍几个您可以实现的有趣效果，包括使用 discard 关键字*扔掉*像素，以及使用着色器创建一个简单的粒子效果系统。\n\n# 丢弃碎片\n\n通过使用片段着色器工具，我们能够创建一些很酷的效果。这些工具之一是使用 discard 关键字。顾名思义，discard 关键字删除或丢弃片段。当使用 discard 关键字时，着色器会立即停止执行并跳过片段，不会将任何数据写入输出缓冲区。创建的效果是多边形面中的孔，而不使用混合效果。discard 关键字还可以与 alpha 贴图的使用相结合，以允许纹理指定应该丢弃哪些片段。当对物体的损伤等效果建模时，这可能是一种方便的技术。\n\n对于这个例子，我们将创建一个片段着色器，它将使用 discard 关键字根据 UV 纹理坐标移除某些片段。对于我们的 gnome 模型，效果将是一个格子或穿孔外观。\n\n让我们从这个例子的顶点着色器开始:\n\n```cpp\n#version 410\n// Input vertex data, different for all executions of this shader.\nin vec3 vertexPosition_modelspace;\nin vec2 vertexUV;\nin vec3 vertexNormal;\nout vec3 FrontColor;\nout vec3 BackColor;\nout vec2 UV;\nstruct LightInfo {\nvec4 Position; // Light position in eye coords.\nvec3 La; // Ambient light intensity\nvec3 Ld; // Diffuse light intensity\nvec3 Ls; // Specular light intensity\n};\nuniform LightInfo Light;\nstruct MaterialInfo {vec3 Ka; // Ambient reflectivity\nvec3 Kd; // Diffuse reflectivity\nvec3 Ks; // Specular reflectivity\nfloat Shininess; // Specular shininess factor\n};\nuniform MaterialInfo Material;\nuniform mat4 ModelViewMatrix;\nuniform mat3 NormalMatrix;\nuniform mat4 ProjectionMatrix;\nuniform mat4 ModelViewProjection;\nvoid getCameraSpace( out vec3 norm, out vec4 position )\n{\nnorm = normalize( NormalMatrix * vertexNormal);\nposition = ModelViewMatrix * vec4(vertexPosition_modelspace,1.0);\n}\nvec3 phongModel( vec4 position, vec3 norm )\n{\n...\n//Same as previous examples\n...}\nvoid main()\n{\nvec3 cameraNorm;\nvec4 cameraPosition;\nUV = vertexUV;\n// Get the position and normal in eye space\ngetCameraSpace(cameraNorm, cameraPosition);\nFrontColor = phongModel( cameraPosition, cameraNorm );\nBackColor = phongModel( cameraPosition, -cameraNorm );\ngl_Position = ModelViewProjection *\nvec4(vertexPosition_modelspace,1.0);\n}\n```\n\n在本例中，我们将照明计算移回顶点着色器。您可能已经注意到，这个顶点着色器与前面的示例非常相似，只是略有变化。需要注意的第一个变化是，我们在这个例子中使用了紫外线纹理坐标。我们使用纹理坐标来确定要扔掉的碎片，这次我们不打算渲染模型的纹理。由于我们将丢弃 gnome 模型的一些片段，我们将能够看透模型的其他部分和内部。这意味着我们需要计算脸部前后的照明方程。我们通过计算每一侧的 Phong 模型，改变传入的法向量来实现这一点。然后，我们将每个顶点的这些值存储在`FrontColor`和`BackColor`变量中，以传递给片段着色器。为了再次使我们的主类稍微容易阅读，我们还将相机空间转换移动到一个函数。\n\n接下来，让我们看看这个例子的片段着色器:\n\n```cpp\n#version 410\nin vec3 FrontColor;\nin vec3 BackColor;\nin vec2 UV;\nout vec4 FragColor;\nvoid main() {\nconst float scale = 105.0;\nbvec2 toDiscard = greaterThan( fract(UV * scale), vec2(0.2,0.2) );\nif( all(toDiscard) )\ndiscard;\nelse {\nif( gl_FrontFacing )\nFragColor = vec4(FrontColor, 1.0);\nelse\nFragColor = vec4(BackColor, 1.0);\n}\n}\n```\n\n在我们的片段着色器中，我们正在计算丢弃哪个片段以获得所需的穿孔效果。为了实现这一点，我们首先使用我们的缩放因子缩放紫外线坐标。该比例因子表示每个纹理坐标的穿孔矩形数量。接下来，我们使用 GLSL 函数`fract()`计算纹理坐标分量的分数部分。然后，我们使用另一个 GLSL 函数`greaterThan()`将每个 *x* 和 *y* 组件与 0.2 的浮点值进行比较。\n\n如果`toDiscard`变量中向量的 *x* 和 *y* 分量都评估为真，这意味着片段位于穿孔矩形的框架内，我们想要丢弃它。我们可以使用 GLSL 函数来帮助我们执行此检查。如果参数向量的所有分量都为真，函数调用将返回真。如果函数返回真，我们执行`discard`语句扔掉那个片段。\n\n接下来，我们有一个`else`块，我们根据碎片是面向后的还是面向前的多边形来给它上色。为了帮助我们，我们使用`gl_FronFacing()`函数根据多边形的法线返回真或假。\n\n就像我们在前面的例子中一样，我们必须再次确保在我们的游戏程序中设置着色器所需的属性和统一变量。要查看示例的完整实现，请参见`Chapter07`、`DiscardExample`项目。如果我们运行这个示例程序，你会看到我们的 gnome 模型看起来好像是由格子做成的。以下是输出的屏幕截图:\n\n![](img/e90cca70-3782-4848-8bee-a2e50889ee8d.png)\n\n# 产生粒子\n\n通过使用着色器可以实现的另一个效果是通常所说的粒子效果。您可以将粒子系统视为一组对象，这些对象一起用于创建烟雾、火灾、爆炸等的视觉外观。系统中的单个粒子被认为是有位置但没有大小的点对象。为了渲染这些点对象，`GL_POINTS`图元通常是最常用的方法。但是，您可以像渲染任何其他对象一样，使用三角形或四边形渲染粒子。\n\n对于我们的例子，我们将实现一个简单的粒子系统，它将具有喷泉外观。我们系统中的每个粒子都会遵循这些规则。它将有一个有限的生命周期，它将根据定义的标准创建和动画，然后终止。在一些粒子系统中，你可以回收粒子，但为了简单起见，我们这里的例子不会。粒子的动画标准通常基于运动学方程，该方程基于重力加速度、风、摩擦和其他因素定义粒子的运动。同样，为了保持我们的例子简单，我们将使用恒定加速度下对象的标准运动学计算来动画化我们的粒子。以下方程描述了粒子在给定时间 *t* 的位置，其中*P<sub>0</sub>T5】为初始位置， *V <sub>0</sub> t* 为初始速度， *a* 代表加速度:*\n\n*P(t) = P<sub>0</sub>+ V­<sub>0</sub>t + ½at<sup>2</sup>*\n\n在我们的例子中，我们将定义粒子的初始位置在原点(0，0，0)。初始速度将在一定范围内随机计算。因为在我们的方程中，每个粒子将在不同的时间间隔产生，所以时间将与该粒子的产生时间相关。\n\n由于所有粒子的初始位置都是相同的，因此我们不需要将其作为属性提供给着色器。我们只需要提供两个顶点属性:粒子的初始速度和开始时间。如前所述，我们将使用`GL_POINTS`渲染每个粒子。使用`GL_POINTS`的最酷之处在于它很容易将纹理应用于点精灵，因为 OpenGL 会自动生成纹理坐标，并通过 GLSL 变量`gl_PointCoord`将其传递给片段着色器。为了使粒子的外观逐渐消失，我们还将在粒子的生命周期内线性增加点对象的透明度。\n\n让我们从这个例子的顶点着色器开始:\n\n```cpp\n#version 410\nin vec3 VertexInitVel; // Particle initial velocity\nin float StartTime; // Particle \"birth\" time\nout float Transp; // Transparency of the particle\nuniform float Time; // Animation time\nuniform vec3 Gravity = vec3(0.0,-0.05,0.0); // world coords\nuniform float ParticleLifetime; // Max particle lifetime\nuniform mat4 ModelViewProjection;\nvoid main()\n{\n// Assume the initial position is (0,0,0).\nvec3 pos = vec3(0.0);\nTransp = 0.0;\n// Particle dosen't exist until the start time\nif( Time > StartTime ) {\nfloat t = Time - StartTime;\nif( t < ParticleLifetime ) {\npos = VertexInitVel * t + Gravity * t * t;\nTransp = 1.0 - t / ParticleLifetime;\n}\n}\n// Draw at the current position\ngl_Position = ModelViewProjection * vec4(pos, 1.0);\n}\n```\n\n我们的着色器从两个必需的输入属性开始，粒子的初始速度`VertexInitVel`和粒子的开始时间`StartTime`。然后，我们有了输出变量`Transp`，它将保存粒子透明度的计算，以传递给下一个着色器阶段。接下来，我们有我们的统一变量:时间，动画运行时间，重力，用于计算恒定加速度，以及`ParticleLifetime`，它指定了粒子可以保持活动的最长时间。在主函数中，我们首先将粒子的初始位置设置为原点，在本例中为(0，0，0)。然后我们将透明度设置为 0。接下来，我们有一个条件来检查粒子是否已经被激活。如果当前时间大于开始时间，则粒子处于活动状态，否则粒子不处于活动状态。如果粒子未激活，该位置将保留在原点，粒子将以完全透明的方式渲染。然后，如果粒子是活的，我们通过从当前时间中减去开始时间来确定粒子的当前*年龄*，并将结果存储在浮点值`t`中。然后我们对照`ParticleLiftime`值检查`t`，如果`t`大于粒子的寿命值，则粒子已经运行了它的寿命动画，然后被渲染为完全透明。如果`t`不大于寿命值，则粒子处于活动状态，我们对粒子进行动画制作。我们使用前面讨论的等式来完成这个动画。透明度是根据粒子的运行时间或*年龄*通过插值确定的。\n\n现在让我们看看这个例子的片段着色器:\n\n```cpp\n#version 410\nin float Transp;\nuniform sampler2D ParticleTex;\nout vec4 FragColor;\nvoid main()\n{\nFragColor = texture(ParticleTex, gl_PointCoord);\nFragColor.a *= Transp;\n}\n```\n\n这个例子中的片段着色器非常基础。这里，我们根据片段的纹理查找值来设置它的颜色。如前所述，因为我们使用的是`GL_POINT`图元，所以纹理坐标是由 OpenGL 的`gl_PointCoord`变量自动计算的。总结一下，我们将片段最终颜色的 alpha 值乘以`Transp`输入变量。随着粒子运行时间的流逝，这将会给我们带来渐弱效果。\n\n在我们的游戏代码中，我们需要创建两个缓冲。第一个缓冲区将存储每个粒子的初始速度。第二个缓冲区将存储每个粒子的开始时间。我们还必须设置所需的统一变量，包括粒子纹理的`ParticleTex`、动画开始后经过的时间量的`Time`变量、表示加速度常数的`Gravity`变量以及定义粒子动画运行时间的`ParticleLifetime`变量。为了简洁起见，我不在此赘述代码，但是您可以看到`Chapter07`文件夹的粒子示例项目的实现。\n\n在测试我们的示例之前，我们还需要确保关闭深度测试，并启用 alpha 混合。您可以通过下面几行代码来实现这一点:\n\n```cpp\nglDisable(GL_DEPTH_TEST);\nglEnable(GL_BLEND);\nglBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);\n```\n\n您可能还想将点对象大小更改为更合理的值。您可以使用下面一行代码将该值设置为 10 像素:\n\n```cpp\nglPointSize(10.0f);\n```\n\n如果我们现在运行我们的示例项目，我们将看到类似喷泉的粒子效果。几个捕获的帧可以如下所示:\n\n![](img/245c79e2-56a5-44c1-9414-d1040716729a.png)\n\n![](img/e5b65410-4810-4b6b-94d4-6aeba4eca42e.png)\n\n虽然这是一个简单的例子，但它有很大的性能和灵活性提高的空间，它应该为您实现基于 GPU 的粒子系统提供一个很好的起点。随意尝试不同的输入值，甚至可能在粒子动画计算中添加更多的因素。实验可以带来很多有趣的结果。\n\n# 摘要\n\n在本章中，我们介绍了使用着色器的基础知识。我们学习了如何构建编译器和链接抽象层来节省时间。我们获得了关于照明技术理论以及如何用着色器语言实现它们的知识。最后，我们通过查看着色器的其他用途来结束这一章，例如创建粒子效果。在下一章中，我们将通过创建高级游戏系统来进一步扩展我们的示例游戏框架。"
  },
  {
    "path": "docs/master-cpp-game-dev/08.md",
    "content": "# 八、高级游戏系统\n\n游戏不仅仅是简单的机械和底层引擎。它们由复杂的游戏系统组成，允许我们与游戏世界互动，让我们感到被包容和沉浸。这些系统通常需要大量的时间和开发人员的专业知识来实现。在这一章中，我们将看几个这样的高级游戏系统，以及当我们在自己的项目中实现它们时，我们如何给自己一层帮助。\n\n本章包括以下主题:\n\n*   实现脚本语言\n*   构建对话系统\n*   编写任务脚本\n\n# 实现脚本语言\n\n如前所述，实现一个先进的游戏系统通常需要花费许多编码时间，并且可能需要开发人员在该特定系统中拥有专业知识。然而，通过包含对脚本语言的支持，我们可以让自己和其他从事该项目的人更容易做到这一点。\n\n# 为什么是脚本语言\n\n你可能想知道为什么我们花时间谈论脚本语言，毕竟这是一本关于 C++ 的书。为什么要加入脚本语言？我们就不能用 C++ 构建整个引擎和游戏吗？是的，我们可以！然而，一旦你开始在越来越大的项目上工作，你会很快注意到每次你需要做出改变时，在编译和重新编译上所损失的时间。虽然有一些方法可以解决这个问题，比如将游戏和引擎分成更小的模块并动态加载它们，或者使用 JSON 或 XML 描述性文件系统，但是像这样的技术不能提供实现脚本系统的所有好处。\n\n那么，在游戏引擎中添加脚本语言有什么好处呢？首先，您将使用的大多数脚本语言都是解释语言，这意味着与 C++ 不同，您不必编译代码。相反，您的代码是在运行时加载和执行的。这样做的最大好处是，您可以对脚本文件进行更改，并快速看到结果，而不必重新编译整个游戏。事实上，您可以在游戏运行时动态重新加载脚本，并立即看到更改。与 C++ 这样的语言相比，使用脚本语言的另一个可能的好处是使用起来感觉很容易。大多数脚本语言都是动态类型的，具有简化的语法和结构。这可以为团队中有创造力的一方，如艺术家和设计师，提供机会，使他们能够对项目进行小的更改，而不需要理解像 C++ 这样的语言的复杂性。想象一下，图形用户界面设计者能够创建、放置和修改图形用户界面元素，而不需要知道 IGUI 框架是如何实现的。添加脚本支持也为社区内容支持开辟了一条道路——认为地图、关卡和项目都是由游戏玩家设计的。这正在成为新游戏的一个巨大卖点，并为你的头衔提供了一些可能的寿命。关于长寿这个话题，DLC 的实现可以通过脚本来完成。这允许更快的开发周转，并且可以放入游戏中而不需要很大的补丁。\n\n这些是使用脚本语言的一些好处，但它们并不总是每种情况下的最佳解决方案。脚本语言因运行速度比本机代码慢而臭名昭著，正如我们所知，在构建游戏时，性能很重要。那么，什么时候应该使用脚本而不是使用 C++？我们将仔细研究一些系统示例，但是作为一个简单的规则，您应该始终使用 C++ 来处理任何可以被认为是 CPU 密集型的事情。程序流和其他高级逻辑是脚本的绝佳选择。让我们看看脚本可以在我们的游戏引擎组件中使用的地方。\n\n让我们从物理成分开始。当然，当我们想到物理时，我们会立即想到大量的 CPU 使用。在很大程度上，这是真的。物理系统的核心应该用 C++ 构建，但是也有机会将脚本引入这个系统。以物理材料的概念为例。我们可以在脚本中定义材料的属性，比如质量、摩擦力、粘度等等。我们甚至可以从脚本内部修改这些值。脚本在物理系统中的另一个潜在用途是定义对碰撞的反应。我们可以在脚本中处理声音、特效和其他事件的生成。\n\nAI 系统怎么样？可以说，这是脚本语言在游戏引擎中最常见的用途之一，我们将在下一章中更深入地研究它。人工智能系统的许多组件都可以移动到脚本中。这些包括复杂行为定义，人工智能目标的规范，人工智能间的交流，人工智能个性和特征的定义，以及更多。虽然列表很大，但您应该注意到，给出的示例不是 CPU 密集型的，而且人工智能系统的复杂组件，如寻路、模糊逻辑和其他密集型算法，也应该用 C++ 代码来处理。\n\n你甚至可以给看似 CPU 和 GPU 繁重的系统添加脚本，比如图形引擎。脚本可以处理照明参数的设置，调整像雾一样的效果，甚至可以在屏幕上添加和删除游戏元素。正如您所看到的，引擎中几乎没有什么是不能用某种形式的脚本抽象来补充的。\n\n那么，应该使用什么脚本语言呢？有很多选择，从游戏特定的语言，如 GameMonkey(在撰写本书时似乎已经不存在)，到更通用的语言，如 Python 和 JavaScript。选择真的要看你的具体需求。虽然像 Python 和 JavaScript 这样的语言有一些惊人的特性，但是它们增加了学习和执行的复杂性来获得这些特性。对于本书中的例子，我们将使用一种叫做 Lua 的语言。Lua 已经存在多年，虽然近年来它的受欢迎程度有所下降，但它在游戏开发行业有着非常强大的记录。在本章的下一部分，我们将更好地了解 Lua，并看看我们如何将其整合到现有的引擎系统中。\n\n# 介绍 LUA\n\nLua，发音为 LOO-ah，是一种轻量级、可嵌入的脚本语言。它支持现代编程方法，如面向对象、数据驱动、函数和过程编程。Lua 是一种可移植的语言，几乎可以在所有提供标准 C 编译器的系统上构建。Lua 运行在所有风格的 Unix、Windows 和 Mac 上。Lua 甚至可以在运行 Android、iOS、Windows Phone 和 Symbian 的移动设备上找到。这使得它非常适合大多数游戏标题，也是包括暴雪娱乐在内的公司将其用于《魔兽世界》等标题的主要原因之一。Lua 也是免费的，根据麻省理工学院许可许可证分发，可以用于任何商业目的，不产生任何费用。\n\nLua 也是一种简单但强大的语言。在 Lua 中，只有一个称为**表**的数据结构。这种表数据结构可以像简单的数组、键值字典一样使用，我们甚至可以通过使用表作为原型来实现一种 OOP 形式。这非常类似于用 JavaScript 等其他语言来做 OPP。\n\n虽然我们不会详细介绍这种语言，但是有一些很好的资源可以利用，包括 Lua 文档网站。我们要做的是略读一些关键的语言概念，我们将在整个例子中看到这些概念。\n\n让我们从变量和简单的程序流程开始。在 Lua 中，所有的数字都是双精度的。您可以使用以下语法分配一个号码:\n\n```cpp\nnumber = 42 \n```\n\n请注意，缺少类型标识符和分号来表示语句结束。\n\nLua 中的字符串可以用几种方式定义。您可以用单引号来定义它们，如下所示:\n\n```cpp\nstring = 'single quote string' \n```\n\n您也可以使用双引号:\n\n```cpp\nstring = \"double quotes string\" \n```\n\n对于跨越多行的字符串，可以使用双方括号来表示字符串的开始和结束:\n\n```cpp\nstring  = [[ multi-line  \n             string]] \n```\n\nLua 是一种垃圾收集语言。您可以通过将对象设置为`nil`来移除定义，相当于 C++ 中的*空*:\n\n```cpp\nstring = nil \n```\n\nLua 中的语句块用`do`、`end`等语言关键字表示。一个`while`循环块如下所示:\n\n```cpp\nwhile number < 100 do \n    number = number + 1 \nend \n```\n\n您可能注意到我们在这里使用了数字`+ 1`，因为在 Lua 语言中没有递增和递减运算符(`++ `、`--`)。\n\n`if`条件代码块如下所示:\n\n```cpp\nif number > 100 then \n    print('Number is over 100') \nelseif number == 50 then \n    print('Number is 50') \nelse \n    print(number) \nend \n```\n\nLua 中的函数以类似的方式构造，使用 end 表示函数代码语句块的完成。一个计算斐波那契数的简单函数类似于下面的例子:\n\n```cpp\nfunction fib(number) \n    if number < 2 then \n        return 1 \n    end \n    return fib(number - 2) + fib(number -1) \nend \n```\n\n如上所述，表是 Lua 语言中唯一的复合数据结构。它们被认为是关联数组对象，非常类似于 JavaScript 对象。表是哈希查找字典，也可以被视为列表。将表用作地图/字典看起来像下面的示例:\n\n```cpp\ntable = { key1 = 'value1', \n          key2 = 100, \n          key3 = false }\n```\n\n处理表格时，还可以使用类似 JavaScript 的点符号。例如:\n\n```cpp\nprint (table.key1) \nPrints the text value1 \n\ntable.key2 = nil \n```\n\n这将从表格中删除`key2`。\n\n```cpp\ntable.newKey = {}  \n```\n\n这将向表中添加一个新的键/值对。\n\n我们对 Lua 语言细节的快速了解到此结束；在我们构建示例的过程中，您将有机会了解更多信息。如果你想了解更多关于 Lua 的知识，我再次推荐你阅读官方网站[http://www.lua.org/manual/5.3/](http://www.lua.org/manual/5.3/)上的文档。\n\n在下一节中，我们将看看在我们的示例游戏引擎项目中包含 Lua 语言支持的过程。\n\n# 实施 LUA\n\n为了在我们的示例引擎中使用 Lua，我们需要采取一些步骤。首先，我们需要获得 Lua 解释器作为一个库，然后我们可以将其包含在我们的项目中。接下来，我们必须获得或者构建我们自己的助手桥，以使我们的 C++ 代码和 Lua 脚本之间的交互更加容易。最后，我们将不得不*公开*或*绑定*函数、变量和其他我们希望访问 Lua 脚本的对象。虽然这些步骤对于每个实现可能略有不同，但这将为我们接下来的示例提供一个良好的起点。\n\n首先，我们需要一个 Lua 的副本作为我们可以在引擎中使用的库。对于我们的例子，我们将使用 Lua 5.3.4，在编写本文时，它是该语言的最新版本。在示例中，我选择使用动态库。您可以在 Lua 项目网站([http://luabinaries.sourceforge.net/](http://luabinaries.sourceforge.net/))上的预编译二进制文件页面下载库的动态和静态版本，以及必要的包含文件。下载预编译库后，提取它，然后在我们的项目中包含必要的文件。我不会再经历在我们的项目中包括一个库的过程。如果您确实需要复习，请翻到[第二章](02.html)、*了解库*，我们在这里详细介绍了各个步骤。\n\n正如我们在整本书中看到的其他例子一样，有时创建助手类和函数以允许各种库和组件之间更容易的互操作是很重要的。当我们使用 Lua 时，情况又是这样。为了让作为开发人员的我们更容易进行交互，我们需要创建一个桥类和函数来提供我们需要的功能。我们可以使用 Lua 本身提供的接口来构建这个桥，Lua 本身有很好的文档，但是也可以选择使用为此目的而创建的众多可用库中的一个。对于本章和本书其余部分中的示例，我选择使用`sol2`库([https://github.com/ThePhD/sol2](https://github.com/ThePhD/sol2)，因为该库是轻量级的(仅头部库)，速度快，并且提供了我们示例所需的所有功能。有了这个库，将会抽象出很多桥的维护，让我们能够专注于实现。要在我们的项目中使用这个库，我们所要做的就是将单头实现复制到我们的`include`文件夹中，它就可以使用了。\n\n现在我们已经有了 Lua 引擎和`sol2`桥库，我们可以进入最后一步，脚本的实现。如上所述，为了让我们使用底层的游戏引擎组件，它们必须首先暴露给 Lua。这就是`sol2`库的位置。为了演示如何在我们的示例引擎中实现这一点，我创建了一个名为`Bind_Example`的小项目。您可以在代码库中的`Chapter08`文件夹中找到完整的源代码。\n\n首先，让我们看看 Lua 脚本本身。在这种情况下，我调用了我的`BindExample.lua`，并将其放在我的示例项目父目录的`Scripts`文件夹中:\n\n```cpp\nplayer = { \n    name = \"Bob\", \n    isSpawned = false \n} \n\nfunction fib(number) \n    if number < 2 then \n        return 1 \n    end \n    return fib(number - 2) + fib(number -1) \nend \n```\n\n在这个例子中，我们的 Lua 脚本非常基础。我们有一个名为`player`的表，它有两个元素。一个键为`name`值为`Bob`的元素，一个键为`isSpawned`值为`false`的元素。接下来，我们有一个名为`fib`的简单 Lua 函数。该函数将计算斐波那契数列中的所有数字，直到传入的数字。我认为在这个例子中偷偷加入一些数学知识会很有趣。我应该注意到，这个计算在序列越高的情况下会变得越密集，所以如果你想让它快速处理，不要传入大于 20 的数字。\n\n这给了我们一些使用 Lua 代码的快速例子。现在我们需要将我们的程序及其逻辑连接到这个新创建的脚本。对于这个例子，我们将把这个连接代码添加到我们的`GameplayScreen`类中。\n\n我们首先为`sol2`库添加必要的包含:\n\n```cpp\n#include <sol/sol.hpp> \n```\n\n接下来，我们将创建 Lua 状态。Lua 中的`state`可以被认为类似于您的代码的操作环境。把它想象成一个虚拟机。这个`state`是您的代码将被执行的地方，通过这个`state`，您将可以访问在其中运行的代码:\n\n```cpp\n    sol::state lua; \n```\n\n然后，我们打开一些我们在 Lua 代码交互中需要的助手库。这些库可以看作是 C++ 中`#include`的等价物。Lua 的理念是保持核心小，并通过这些库提供更多的功能:\n\n```cpp\n    lua.open_libraries(sol::lib::base, sol::lib::package); \n```\n\n打开库之后，我们可以继续加载实际的 Lua 脚本文件。我们通过调用我们之前创建的 Lua `state`的`script_file`方法来做到这一点。此方法接受一个参数:文件的位置为字符串。执行此方法时，文件将被自动加载和执行:\n\n```cpp\n    lua.script_file(\"Scripts/PlayerTest.lua\"); \n```\n\n现在加载了脚本，我们可以开始与它交互了。首先，让我们看看如何在 Lua 中从变量(表)中提取数据，并在我们的 C++ 代码中使用它:\n\n```cpp\n    std::string stringFromLua = lua[\"player\"][\"name\"]; \n    std::cout << stringFromLua << std::endl; \n```\n\n从 Lua 脚本中检索数据的过程非常简单。在这种情况下，我们创建了一个名为`stringFromLua`的字符串，并为它分配了存储在 Lua 表玩家的`name`元素中的值。语法看起来类似于调用数组元素，但这里我们用字符串指定元素。如果我们想要`isSpawned`元素值，我们将使用`lua[\"player\"][\"isSpawned\"]`，在我们的例子中，它将返回一个布尔值`false`。\n\n调用 Lua 函数和检索值一样简单，非常相似:\n\n```cpp\n    double numberFromLua = lua[\"fib\"](20); \n    std::cout << numberFromLua << std::endl; \n```\n\n这里我们创建了一个类型为 double 的变量，称为`numberFromLua`，并赋予它 Lua 函数`fib`的返回值。这里，我们将函数名指定为一个字符串，`fib`，然后指定该函数所需的任何参数。在这个例子中，我们传递值 20 来计算斐波那契数列，直到第二十个数字。\n\n如果您运行`Bind_Example`项目，您将在引擎的命令窗口中看到以下输出:\n\n![](img/5c1eeb50-54c3-410b-bb35-75080880eaef.png)\n\n虽然这涵盖了我们的 C++ 代码和 Lua 脚本系统之间交互的基础，但还有很多需要发现的地方。在接下来的几节中，我们将研究如何利用这种脚本结构来增强各种先进的游戏系统，并为我们提供一种灵活的方式来扩展我们的游戏项目。\n\n# 构建对话系统\n\n与游戏世界互动的最常见形式之一是通过某种形式的对话。能够与`NPC`类交流，获取信息和任务，当然，通过对话推动故事叙述是大多数现代游戏标题的必备条件。虽然您可以轻松地对交互进行硬编码，但这种方法留给我们的灵活性很小。每次我们想要对任何对话框或交互进行细微的更改时，我们都必须打开源代码，深入项目，进行任何必要的更改，然后重新编译以查看效果。显然，这是一个繁琐的过程。只要想想你玩过多少个出现拼写、语法或其他错误的游戏。好消息是我们可以采取另一种方法。使用脚本语言，比如 Lua，我们可以以动态的方式驱动我们的交互，这将允许我们进行快速的更改，而不需要前面描述的繁琐过程。在本节中，我们将了解构建对话系统的详细过程，在高级描述中，该系统将加载脚本，将其附加到`NPC`上，向玩家呈现带有选项的对话，最后，基于返回的玩家输入驱动对话树。\n\n# 构建 C++ 基础设施\n\n首先，我们需要在示例引擎中构建基础设施，以支持对话系统的脚本编写。有数千种不同的方法可以实现这一点。对于我们的例子，我将尽最大努力保持简单。我们将使用前几章中学习的一些技术和模式，包括状态和更新模式，以及我们构建的用于处理交互和显示的图形用户界面系统。\n\n他们说一张图片胜过千言万语，所以为了让您大致了解这个系统将如何连接，让我们看一下描述所有类之间连接的代码映射图:\n\n![](img/c014d784-590f-4a51-aba9-60a48686ceb6.png)\n\n这里发生了一点事情，所以我们将按类进行分类。首先，我们来看看`DialogGUI`课。这个类建立在我们在前一章构建的 IGUI 示例的基础上。因为我们已经深入研究了 IGUI 类的设计，所以我们将只讨论我们正在添加的特定方面，以提供我们的对话系统所需的功能。\n\n首先，我们需要一些变量来保存对话框和任何我们想提供给玩家的选择。在`DialogGUI.h`中，我们有以下内容:`IGUILabel`对象的矢量用于选择，单个`IGUILabel`用于对话框。关于`IGUILabel`类的实现，请看一下它的源代码:\n\n```cpp\nstd::vector<BookEngine::IGUILabel*> choices; \nBookEngine::IGUILabel* m_dialog;\n```\n\n接下来，我们需要添加一些新的功能，为我们的图形用户界面和脚本提供的数据提供所需的交互。为此，我们将向我们的`DialogGUI`类添加三种方法:\n\n```cpp\nvoid SetDialog(std::string text); \nvoid SetOption(std::string text, int choiceNumber); \nvoid RemoveAllPanelElements(); \n```\n\n`SetDialog`功能，顾名思义，将处理每个交互屏幕的对话文本的设置。该函数只接受一个参数，也就是我们要放在图形用户界面上进行交互的文本:\n\n```cpp\nvoid DialogGUI::SetDialog(std::string text) \n{ \n    m_dialog = new BookEngine::IGUILabel(glm::vec4(0, 110, 250, 30), \n        glm::vec2(110, -10), \n        text, \n        new BookEngine::SpriteFont(\"Fonts/Impact_Regular.ttf\", 72), \n        glm::vec2(0.3f), m_panel); \n\n    AddGUIElement(*m_dialog); \n} \n```\n\n在函数体中，我们将`m_dialog`标签变量分配给一个`IGUILabel`对象的新实例。构造函数看起来应该类似于前面看到的`IGUIButton`，文本值被传入。最后，我们通过调用`AddGUIElement`方法将标签添加到图形用户界面面板。\n\n`SetOption`功能，同样如其名称所示，为当前交互屏幕上的每个选项设置文本。这个函数有两个参数。第一个是我们想要设置`IGUILabel`的文本，第二个是选项号，这是它在正在呈现的选项列表中的编号。我们用它来查看选择了哪个选项:\n\n```cpp\nvoid DialogGUI::SetOption(std::string text, int choiceNumber) \n{ \n    choices.resize(m_choices.size() + 1); \n    choices[choiceNumber] =  \nnew BookEngine::IGUILabel(glm::vec4(0, 110, 250, 20), \n            glm::vec2(110, 10), \n            text, \n            new BookEngine::SpriteFont(\"Fonts/Impact_Regular.ttf\", 72), \n            glm::vec2(0.3f), m_panel); \n\n    AddGUIObject(*choices[choiceNumber]); \n}\n```\n\n在函数体中，我们正在做一个与`SetDialog`函数非常相似的过程。不同的是，我们将把`IGUILabel`实例添加到选择向量中。首先，我们执行一个小技巧，将向量的大小增加一，这将允许我们将新的标签实例分配给传入的选择编号值处的向量位置。最后，我们使用`AddGUIElement`方法调用将`IGUILabel`添加到面板中。\n\n我们添加到`DialogGUI`类的最后一个函数是`RemoveAllPanelElements`，它当然会处理移除我们添加到当前对话框屏幕的所有元素。我们删除了这些元素，这样我们就可以重用面板，避免每次更改交互时都重新创建面板:\n\n```cpp\nvoid DialogGUI::RemoveAllPanelElements() \n{ \n    m_panel->RemoveAllGUIElements(); \n} \n```\n\n`RemoveAllGUIElements`函数反过来只是在`m_panel`对象上调用相同的方法。`IGUIPanel`类的实现简单地调用向量上的 clear 方法，移除其所有元素:\n\n```cpp\nvoid RemoveAllGUIObjects() { m_GUIObjectsList.clear(); }; \n```\n\n这照顾到了我们的对话系统的图形用户界面设置，所以现在我们可以继续构建`NPC`类，它将处理大部分脚本到引擎的桥接。\n\n正如我之前提到的，我们将使用我们在前面例子中学习的一些模式来帮助我们构建我们的对话系统。为了帮助我们控制何时构建图形用户界面元素，何时等待玩家做出选择，我们将使用有限状态机和更新模式。首先，在`NPC.h`文件中，我们有`enum`，它将定义我们将使用的状态。在这种情况下，我们只有两个州，`Display`和`WaitingForInput`:\n\n```cpp\n... \n    enum InteractionState \n    { \n        Display, \n        WaitingForInput, \n    }; \n...\n```\n\n当然，我们还需要一种方法来跟踪状态，所以我们有一个名为`currentState`的`InteractionState`变量，我们会将其设置为当前状态。稍后，我们将在`Update`功能中看到这个状态机的完成:\n\n```cpp\nInteractionState currentState; \n```\n\n我们还需要一个变量来保存我们的 Lua 状态，我们在本章的前一节中已经看到了:\n\n```cpp\n    sol::state lua; \n```\n\n您可能还记得之前显示的代码图，我们的`NPC`将有一个`DialogGUI`的实例，用于处理对话框内容的显示和与玩家的交互，因此我们还需要一个变量来保存它:\n\n```cpp\n    DialogGUI* m_gui; \n```\n\n接下来是`NPC`类的实现，我们将首先查看`NPC.cpp`文件中该类的构造函数:\n\n```cpp\nNPC::NPC(DialogGUI& gui) : m_gui(&gui) \n{ \n    std::cout << \"Loading Scripts n\"; \n    lua.open_libraries(sol::lib::base, sol::lib::package, sol::lib::table); \n    lua.script_file(\"Scripts/NPC.lua\"); \n    currentState = InteractionState::Display; \n} \n```\n\n构造函数接受一个参数，一个我们将用于交互的对话框实例的引用。我们将此引用设置为成员变量`m_gui`以备后用。然后，我们处理将要使用的 Lua 脚本的加载。最后，我们将内部状态机的当前状态设置为`Display`状态。\n\n让我们重新审视我们的代码图，看看我们需要实现哪些不同的连接来将`NPC`类加载的脚本信息传递给我们附加的图形用户界面实例:\n\n![](img/eae789ce-a9e9-4382-b250-d184582e7da9.png)\n\n正如我们所看到的，我们有两种处理连接的方法。`Say`功能是两者中最简单的。这里，`NPC`类只是在附加的图形用户界面上调用`SetDialog`方法，传递一个包含要显示的对话框的字符串:\n\n```cpp\n void NPC::Say(std::string stringToSay) \n{ \n    m_gui->SetDialog(stringToSay); \n} \n```\n\n`PresentOptions`功能对其影响稍大。首先，该函数从 Lua 脚本中检索一个表，该表表示当前交互的选项，我们将很快看到脚本中是如何设置的。接下来，我们将遍历该表，如果它有效，只需在附加的图形用户界面上调用`SetOption`方法，将选择文本作为字符串和用于选择的选择号传递:\n\n```cpp\nvoid NPC::PresentOptions() \n{ \n\n    sol::table choices = lua[\"CurrentDialog\"][\"choices\"]; \n    int i = 0; \n    if (choices.valid()) \n    { \n        choices.for_each([&](sol::object const& key, sol::object const& value) \n        { \n            m_gui->SetOption(value.as<std::string>(), i); \n            i++ ; \n        }); \n    } \n}\n```\n\n对话系统引擎端的最后一块我们需要放入的地方是`Update`方法。正如我们多次看到的，这种方法将推动系统向前发展。通过连接到引擎的现有`Update`事件系统，我们的`NPC `类`Update`方法将能够控制我们的对话系统在每一帧上发生的事情:\n\n```cpp\nvoid NPC::Update(float deltaTime) \n{ \n    switch (currentState) \n    { \n    case InteractionState::Display: \n        Say(lua[\"CurrentDialog\"][\"say\"]); \n        PresentOptions(); \n        currentState = InteractionState::WaitingForInput; \n        break; \n    case InteractionState::WaitingForInput: \n        for (int i = 0; i < m_gui->choices.size(); i++) \n        { \n            if (m_gui->choices[i]->GetClickedStatus() == true) \n            { \n                lua[\"CurrentDialog\"][\"onSelection\"](m_gui-> \nchoices[i]->GetLabelText()); \n                currentState = InteractionState::Display; \n                m_gui->choices.clear(); \n                m_gui->RemoveAllPanelElements (); \n            } \n        } \n        break; \n    } \n} \n```\n\n与我们之前的有限状态机实现一样，我们将使用一个开关案例来确定基于当前状态应该运行什么代码。对于这个例子，我们的`Display`状态是我们将要调用连接方法`Say`和`PresentOptions`的地方。在这里，`Say`调用正在单独传递它从已经加载的脚本文件中提取的文本。接下来我们将在脚本中看到这是如何工作的。在这个例子中，如果我们处于`WaitingForInput`状态，我们将遍历我们加载的每个选项，看看玩家是否已经选择了其中的任何一个。如果找到一个，我们将回调脚本，告诉它选择了哪个选项。然后我们将状态切换到`Display`状态，这将启动下一个对话屏幕的加载。然后，我们将在附加的`DisplayGUI`中清除我们的选择向量，允许它加载下一组选择，最后调用`RemoveAllPanelElements`方法来清理我们的图形用户界面以供重用。\n\n有了`Update`方法，我们现在有了处理`NPC`交互脚本所需的加载、显示和输入处理的所有框架。接下来，我们将看看如何构建这些脚本中的一个，用于我们引擎新创建的对话系统。\n\n# 创建对话树脚本\n\n对话或对话树可以被认为是确定的交互流。本质上，它的工作原理是首先提供一个声明，然后，基于所呈现的响应的选择，交互可以分支到不同的路径。下图显示了我们的示例对话流是如何确定的:\n\n![](img/6ce1e167-083a-4863-a04d-41230fa52cf7.png)\n\n这里，我们以一个介绍开始对话树。然后给用户两个选择:**是，需要帮助**和**不，别管我**。如果用户选择**是**路径，那么我们进入**快速帮助**对话框。如果用户选择**否**，我们进入**再见人**对话框。在**表达帮助**对话框中，我们给出了三个选择:**好**、**重新开始**、**弱**。基于这个选择，我们再次进入对话树的下一个阶段。**好的**进入**离开美好**对话框。**弱**引出**再见人**对话框，**重新开始**嗯，重新开始。这是一个基本的例子，但是它展示了对话树如何工作的整体概念。\n\n现在让我们看看如何在 Lua 脚本引擎中实现这个示例树。以下是完整的脚本，接下来我们将深入了解细节:\n\n```cpp\nintro = { \n    say = 'Hello I am the Helper NPC, can I help you?', \n    choices = { \n                 choice1 = \"Yes! I need help\", \n                 choice2 = \"No!! Leave me alone\" \n    }, \n\n    onSelection = function (choice)  \n        if choice == CurrentDialog[\"choices\"][\"choice1\"] then CurrentDialog = getHelp end \n        if choice  == CurrentDialog[\"choices\"][\"choice2\"] then CurrentDialog = goodbye_mean end \n    end \n} \n\ngetHelp = { \n    say = 'Ok I am still working on my helpfulness', \n    choices = { \n                 choice1 = \"That's okay! Thank you!\", \n                 choice2 = \"That's weak, what a waste!\", \n                 choice3 = \"Start over please.\" \n        }, \n    onSelection = function (choice)  \n        if choice  == CurrentDialog[\"choices\"][\"choice1\"] then CurrentDialog = goodbye  \n        elseif choice  == CurrentDialog[\"choices\"][\"choice2\"] then CurrentDialog = goodbye_mean  \n        elseif choice  == CurrentDialog[\"choices\"][\"choice3\"] then CurrentDialog = intro end \n    end \n\n} \n\ngoodbye = { \n    say = \"See you soon, goodbye!\" \n} \n\ngoodbye_mean = { \n    say = \"Wow that is mean, goodbye!\" \n} \n\nCurrentDialog = intro \n```\n\n如你所见，整个剧本没那么长。我们有几个概念使这个脚本工作。首先是一个非常简单的状态机版本。我们有一个名为`CurrentDialog`的变量，这个变量将指向活动对话框。在我们脚本的最后，我们首先将其设置为`intro`对话框对象，这将在加载脚本时启动对话框树。我们在脚本设计中的下一个重要概念是每个交互屏幕都被描述为一个表对象的概念。让我们以介绍对话框表格为例:\n\n```cpp\nintro = { \n    say = 'Hello I am the Helper NPC, can I help you?', \n    choices = { \n                 choice1 = \"Yes! I need help\", \n                 choice2 = \"No!! Leave me alone\" \n    }, \n\n    onSelection = function (choice)  \n        if choice == CurrentDialog[\"choices\"][\"choice1\"] then CurrentDialog = getHelp end \n        if choice  == CurrentDialog[\"choices\"][\"choice2\"] then CurrentDialog = goodbye_mean end \n    end \n} \n```\n\n每个对话框表格对象都有一个`Say`元素，这个元素是当`Say`函数向脚本询问其对话框内容时将显示的文本。接下来，我们有两个可选的元素，但如果你想与玩家互动，这是必需的。第一个是一个名为`choices`的嵌套表，其中包含了对话系统请求时将呈现给玩家的选项。第二个选项元素实际上是一个函数。当用户选择一个选项时调用该函数，该函数由一些`if`语句组成。这些`if`语句将测试选择了哪个选项，并基于该选项将`CurrentDialog`对象设置到对话框树路径上的下一个对话框。\n\n真的是这样。以这种方式设计我们的对话树系统的最大好处是，在很少的指导下，即使是非程序员也可以设计一个像前面所示的简单脚本。\n\n如果您继续使用`Chapter08`解决方案运行`Dialog_Example`项目，您将看到这个脚本正在运行，并且能够与之交互。以下是一些截图，显示了输出的样子:\n\n![](img/90b8913b-b9d6-4f62-9c06-f50dcb30720d.png)\n\n虽然这是一个简单的系统实现，但它非常灵活。需要再次注意的是，这些脚本不需要重新编译来进行更改。你自己试试。对`NPC.lua`文件做一些修改，重新运行示例程序，你会看到你的修改出现。\n\n在下一节中，我们将看到如何通过实现由 Lua 脚本驱动的 quest 系统来进一步包含脚本系统。\n\n# 编写任务脚本\n\n另一个非常常见的高级游戏系统是任务系统。虽然在角色扮演游戏中更常见，但任务也可以出现在其他类型中。通常，这些其他的流派会用一个不同的名字来掩饰一个任务系统。比如有些游戏有挑战，本质上真的和任务一样。\n\n一个探索可以被简单地认为是一个达到特定结果的尝试。通常，在任务被认为完成之前，任务会包含一定数量的必须执行的步骤。一些类型的常见任务包括杀死任务，玩家通常必须杀死特定数量的敌人，通常称为**打磨**和**运送**任务，玩家必须扮演快递员的角色，并且经常必须前往游戏世界的新地点运送货物。当然，这是让玩家在不强迫他们的情况下前往下一个期望地点的好方法。在收集任务时，玩家必须收集一定数量的特定物品。在护送任务中，由于历史上糟糕的实现，玩家经常害怕，玩家经常不得不伴随一个`NPC`到一个新的位置，同时保护他们免受伤害。最后，混合任务通常是上述类型的混合，更典型的是更长的任务。\n\n任务系统的另一个常见部分是支持所谓的任务链或任务线。在任务链中，每个任务的完成是开始序列中下一个任务的先决条件。随着玩家在链条中前进，这些任务通常会涉及越来越多的复杂任务。这些任务是逐渐揭示剧情的好方法。\n\n这就解释了什么是任务。在下一节中，我们将讨论一些不同的方法，我们可以在我们的游戏项目中增加对任务的支持。然而，在我们看实现的细节之前，定义我们期望每个任务对象需要什么是有用的。\n\n出于简单示例的目的，我们假设 quest 对象由以下内容组成:\n\n*   **任务名称**:任务的名称\n*   **目标**:完成任务必须采取的行动\n*   **奖励**:玩家完成任务将获得什么\n*   **描述**:关于任务的一点信息，也许是一些关于玩家为什么要承担任务的背景故事\n*   **任务给予者**:给予任务的`NPC`\n\n有了这些简单的元素，我们可以构建我们的基本任务系统。\n\n正如我们在以前的游戏系统示例中看到的，在示例引擎中，我们有许多不同的方法来实现我们的任务系统。现在让我们简单看一下其中的几个，并讨论它们的优缺点。\n\n# 在发动机支架中\n\n我们支持任务系统的一种方法是将它构建到游戏引擎本身中。整个系统将被设计成接近引擎代码，并且使用本地引擎语言，在我们的例子中是 C++。我们将使用我们见过无数次的技术来创建一个基础设施来支持这一探索。使用继承，我们可以公开所需的基本函数和变量，并让开发人员构建这个结构。一个简单的高级任务类可能看起来如下所示:\n\n```cpp\nclass Quest \n{ \npublic: \n    Quest(std::string name,  \n    std::vector<GameObjects> rewards,  \n    std::string description,  \n    NPC questGiver); \n    ~Quest(); \n    Accept(); //accept the quest \n    TurnIn(); //complete the quest \nprivate: \n     std::string m_questName; \n       std::vector<GameObjects> m_rewards; \n       std::string m_questDescription; \n       NPC m_questGiver; \n     Bool isActive; \n}; \n```\n\n当然，这只是一个简单的演示，在这种情况下，我们将跳过实现。\n\n这种实现方法的优点是，它是用本机代码编写的，这意味着它会运行得很快，而且它离引擎很近，这意味着它可以更好地访问底层系统，而不需要接口层或其他库。\n\n这种实现方法的缺点包括，因为它是游戏引擎或游戏代码本身的一部分，这意味着所做的任何更改都需要重新编译。这也使得非程序员更难为任务添加自己的想法，或者在发布后处理任务系统的扩展。\n\n虽然这种方法确实有效，但它更适合较小的项目，在这些项目中，一旦任务或系统就位，您就不必或不想对其进行更改。\n\n# 引擎/脚本桥\n\n这个方法和我们之前实现`NPC`对话系统的方法是一样的。在这个设计中，我们创建了一个接口类来处理脚本的加载和 quest 脚本之间的数据传递。因为我们以前见过类似的实现，所以我将跳过这里的示例代码，转而讨论这种方法的优缺点。\n\n与仅引擎实现相比，这种实现方法的优点包括灵活性。如果我们想做任何更改，我们只需要在编辑器中加载脚本，进行更改，然后重新加载游戏。这也让非程序员更容易创建自己的任务。\n\n这种实现方法的缺点包括它仍然部分地依赖于引擎本身。脚本只能访问引擎接口公开的元素和函数。如果你想给一个任务增加更多的功能，你必须在任何脚本使用它之前把它构建到引擎端。\n\n这种方法更适合较大的项目，但如上所述，仍然有其缺点。\n\n# 基于脚本的系统\n\n我们可以采取的另一种方法是用我们的脚本语言构建整个系统，只从引擎中公开通用方法。这些通用方法可能是模板函数的良好候选。在这种方法中，任务系统内部和任务脚本都是用脚本语言编写的。在一个脚本中编写的每个任务都会包含一个处理管理的任务系统脚本的引用。这种方法非常类似于只使用发动机的方法；它刚刚被移出引擎，进入脚本系统。\n\n让我们来看看 quest 系统脚本的一个简单版本。为了简洁起见，省略了一些部分:\n\n```cpp\nlocal questsys = {} \nquestsys.quest = {} \n\nfunction questsys.new(questname, objectives, reward, description, location, level, questgiver) \nfor keys, value in ipairs(objectives) do \n    value.value = 0 \n  end \n  questsys.quest[#questsys.quest+1] = { \n    questname = questname, \n    objectives = objectives, \n    reward = reward, \n    description = description, \n    questgiver = questgiver, \n    accepted = false, \n    completed = false, \n    isAccepted = function(self) return self.accepted end, \n    isCompleted = function(self) return self.completed end \n  } \nend \n\nfunction questsys.accept(questname) \n  for key, value in ipairs(questsys.quest) do \n    if value.questname == questname then \n      if not value.accepted then \n        value.accepted = true \n      end \n  end \nend \n\n... \n\nfunction questsys.turnin(questname) \n  rejectMsg = \"You have not completed the quest.\" \n  for key, value in ipairs(questsys.quest) do \n    if value.questname == questname then \n      for i, j in ipairs(questsys.quest[key].objectives) do \n        if j.value == j.maxValue then \n          value.completed = true \n          value.reward() \n        else return rejectMsg end \n      end \n  end \nend \n\n... \n\nquestsys.get(questname, getinfo) \n  for key, value in ipairs(questsys.quest) do \n    if value.questname == questname then \n      if getinfo == \"accepted\" then return value:isAccepted() end \n      if getinfo == \"completed\" then return value:isCompleted() end \n      if getinfo == \"questname\" then return value.questname end \n      if getInfo == \"description\" then return value.description end \n      if getInfo == \"location\" then return value.location end \n      if getInfo == \"level\" then return value.level end \n      if getInfo == \"questgiver\" then return value.questgiver end \n    else error(\"No such quest name!\") \n  end \nend \n\nreturn questsys \n```\n\n同样，为了节省空间，我省略了一些功能，但是理解系统所需的核心组件在这里。首先，我们有一个创建新任务的功能，包括名称、目标、描述和任务给予者。然后我们有一个接受函数，将任务设置为活动的。请注意，我们是如何使用键/对查找方法来遍历我们的表的——我们将经常这样做。然后我们在任务中有一个函数要转，最后是一个返回所有任务信息的简单函数。这里没有描述的功能是获取和设置任务的各种目标值。要了解完整的实现，请看一下代码库的`Chapter08`文件夹中的`Quest_Example`项目。\n\n现在，有了 quest 系统脚本，我们有几个选择。首先，我们可以通过使用`require`系统中的 Lua 构建将这个系统添加到其他脚本中，这将允许我们在其他脚本中使用该脚本。其语法如下所示:\n\n```cpp\nlocal questsys = require('questsys') \n```\n\n或者我们可以简单地在我们的游戏引擎中加载脚本，并使用一个界面，就像我们在前面的例子中所做的那样，并以这种方式与我们的 quest 系统交互。有了这种灵活性，选择取决于开发人员和情况。\n\n这种实现方法的优点包括很大的灵活性。在这种方法中，不仅任务的变化，而且任务系统本身也可以动态修改，而不需要重建游戏或引擎。这通常是一种用于在产品发布后包含可下载内容(DLC)、游戏修改(mods)和其他额外内容的方法。\n\n这种实现的缺点包括，尽管它非常灵活，但增加了额外的复杂性。它也可能更慢，因为系统是用解释的脚本语言编写的，性能可能会受到影响。它还要求开发人员对脚本语言有更多的了解，并且可能需要更多的学习时间。\n\n像其他方法一样，这种方法也有它的位置和时间。虽然我倾向于在大型项目中使用这样的系统，但是如果团队没有做好准备，这种方法可能会增加更多的开销，而不是易用性。\n\n# 摘要\n\n在这一章中，我们谈到了实现高级游戏系统的大量内容。我们深入研究了如何在游戏项目中包含像 Lua 这样的脚本语言。然后，我们在这些知识的基础上，检查在示例引擎中实现对话和任务系统的方法。虽然我们确实讨论了很多，但我们几乎没有触及这个话题的表面。在下一章中，我们将继续利用这些新发现的知识为我们的游戏构建一些人工智能。"
  },
  {
    "path": "docs/master-cpp-game-dev/09.md",
    "content": "# 九、人工智能\n\n大多数游戏都建立在竞争取胜的理念上。这种形式的竞争可以采取多种形式。从最早的电子游戏开始，玩家就发现自己在与机器竞争。包含思考、反应和挑战电脑对手，让游戏感觉充满活力并与玩家联系在一起。在这一章中，我们将学习如何将人工智能加入到我们的游戏中。\n\n本章包括以下内容:\n\n*   什么是游戏 AI？\n*   做决定\n*   运动和寻路技术\n\n# 什么是游戏 AI？\n\n经常被误解的是，定义什么是游戏人工智能，什么不是游戏人工智能是一项非常具有挑战性的工作。有了人工智能这样一个包罗万象的领域，就很容易填满许多关于这个主题的书籍。鉴于我们只有一章来讨论概念和实现，在这一节中，我们将尽最大努力为什么是游戏人工智能，什么不是游戏人工智能制定一个合理的定义。\n\n# 定义游戏人工智能\n\n如前所述，准确定义什么是游戏 AI 可能是一项艰巨的任务，但我将尽我所能来描述我觉得在电子视频游戏方面是一个简洁的解释。当一个设计师创造一个游戏世界时，他们通过塑造一个愿景和定义这个世界中一些共同的交互规则来做到这一点。通常，玩家会通过观察世界的元素来体验这个世界。互动，例如与世界上的 NPC、对手和环境的互动，以及通过叙事方面的互动，给玩家一种沉浸在游戏世界中的感觉。这些相互作用可以采取多种形式。在游戏中，玩家不断通过无生命的物体与世界进行一些互动，但真正引人注目的是与他人的互动。这让游戏感觉更身临其境，更有形，更有活力。\n\n在游戏世界中感觉到有东西活着的感觉通常来自于对游戏世界和物体的观察，比如一个 NPC 在做决定。这是寻找游戏人工智能定义的一面伟大旗帜。从更广泛的意义上说，人工智能可以被认为是这种感知决策的应用。通常，这种对决策的感知以自主人工智能代理的形式出现，例如，常见的 NPC。这些决策可能包括从移动、对话框选择，甚至可能传达开发人员试图创建的体验的环境变化。这又是我定义游戏 AI 时的另一面旗帜。本质上，它是关于开发者试图创造的体验。为此，游戏 AI 更多的是一种获得预期效果的近似，不一定是完美的科学解释。\n\n重要的是，当开发人员开始创建人工智能体验时，他们会考虑玩家的乐趣和沉浸感的关键方面。没有人想和完美的对手比赛。我们希望在交互的另一端感知智能，我们只是不希望它变得更聪明。这就是开发游戏人工智能和一般人工智能开发领域开始产生分歧的地方。我们将在下一节更深入地探讨这种转移，但现在，让我们看看人工智能在游戏开发中的一些用途。\n\n# 会话\n\n通过对话进行某种互动的游戏，通过角色与玩家的联系以及玩家如何投入到他们的故事中，给人一种沉浸在世界中的感觉。然而，这是一个挑战，正如我们在上一章中看到的那样，通常通过对话框树来实现。这种对话树方法虽然在某些情况下是可靠的，但很容易变得复杂。\n\n完全脚本化对话的另一个问题是，随着对话的持续，玩家很快就会从这是一种智能交互的错觉中摆脱出来。它让互动感到被约束，反过来也让世界感到被约束。解决这个问题的一个方法是在对话中引入人工智能。你可以用决策算法来增强脚本化的交互，以在响应中给人一种更深层次的智能感。在这个概念的极端方面，您可以使用一种方法来解析玩家输入，并动态地自定义生成响应。这样的方法可能包括所谓的**自然语言处理** ( **自然语言处理**)。通过利用类似聊天机器人的东西，设计师和工程师可以创建由代理组成的世界，代理在响应用户交互时进行思考。虽然这听起来非常诱人，但自然语言处理领域仍被认为处于起步阶段。借助云计算支持的应用编程接口，如微软的认知服务应用编程接口，创建支持自然语言处理的基础设施的过程变得越来越容易。然而，语言模型的正确实现和训练可能相当耗时。\n\n# 竞争对手\n\n很多游戏都包含了敌人或者竞争对手的概念，供玩家互动。事实上，我会说这是大多数人认为的游戏人工智能的一个例子。这些对手如何与玩家、他们的环境和其他人工智能控制的对手互动，都是他们人工智能设计的一部分。通常，这种人工智能设计将包括决策的概念，例如行为树、反馈循环、状态和其他模式。它们还通常包括其他人工智能组件，如运动算法和寻路技术，这两者我们将在本章后面更深入地讨论。创造乐趣同时挑战对手并不是一件容易的事情。正如我之前所说的，没有人想玩一个他们觉得没有机会赢的游戏。拥有一个不断比玩家更快更聪明的 AI 不应该是设计对手 AI 的目标；相反，你应该专注于给用户一个有竞争力的人工智能，它可能会扩展以满足玩家不断增长的技能。正是在这种情况下，先进的技术，如使用机器学习来构建自适应人工智能，开始获得吸引力。尽管这些技术仍处于探索阶段，但量身定制人工智能对手的日子可能很快就会到来。\n\n# 运动和寻路\n\n可以说，与对手使用人工智能一样常见的是利用人工智能进行运动和寻路的概念。在运动中使用人工智能包括实现算法来处理游戏元素的自主运动。诸如转向、追击和躲避等想法都是你可以在 AI 算法中表达的概念。运动人工智能通常也用于处理简单的碰撞避免。寻路是在将游戏对象从一个位置移动到下一个位置时，使用 AI 来寻找最高效或最有效的路线的概念。像 **Dijkstra** 和 **A*** 这样的算法从 60 年代就已经出现了，并为寻路人工智能的发展提供了一个主要部分。我们将在本章后面更深入地探讨运动和寻路算法和技术。\n\n# AI 不是什么游戏\n\n人工智能作为一个研究领域，是非常庞大的，真的不仅仅包括游戏使用的东西。最近，开发人员领域围绕人工智能的讨论变得更加广泛，越来越多的开发人员在他们的项目中寻找利用人工智能技术的方法。出于这个原因，我认为在游戏开发领域之外，接触一些更常见的人工智能用例是很重要的。\n\n除了游戏开发之外，人工智能最热门的领域之一是机器学习。**机器学习** ( **ML** )最恰当的描述可能是亚瑟·李·萨缪尔，他创造了机器学习这个术语:*一种计算机在没有被明确编程的情况下学习如何实现结果或预测的能力。*在数据分析领域，机器学习被用作设计复杂模型和算法的方法，以帮助预测给定问题的结果。这也被称为预测分析。这些分析模型允许研究人员和数据科学家创建可靠、可重复的计算和结果，并通过数据中的历史关系和趋势发现其他见解。如前一节所述，量身定制的人工智能从你的游戏风格中学习并适应的想法是一个非常吸引人的概念。然而，这可能是一个滑坡；如果 AI 变得太聪明，那么游戏的乐趣水平可以而且会迅速下降。游戏中如何使用 ML 的一个很好的例子是 Forza 赛车游戏系列。在这里，赛车人工智能化身在云计算支持的机器学习实现中进行处理，以根据您当前的能力水平定制您遇到的人工智能赛车的竞争水平。\n\n人工智能在游戏开发领域之外的另一个日益增长的用途是在数据挖掘场景中的实现。虽然人工智能的这一领域仍处于早期阶段，但它在理解用户和客户数据方面的应用对许多业务部门极具吸引力。这个人工智能用例的边界及其与游戏开发概念的潜在重叠还有待定义。然而，用于理解玩家如何与游戏及其各种组件交互的数据挖掘的一些核心组件很容易被视为对游戏开发者有益。准确了解玩家如何与游戏图形用户界面等元素互动，将使开发人员能够为每个用户创造更好的体验。\n\n我想谈的最后一个游戏开发之外的 AI 用例，可能是一般人想到 AI 时最认可的用途之一，那就是 AI 在认知加工研究中的用途。在人工智能的学术解释中，认知加工是为这些过程开发科学上可证明的模型的过程。这基本上可以概括为 AI 过程中人类智能的建模。虽然这种方法对于科学研究非常重要，但是目前游戏开发的用例仍然过于抽象，不被认为有用。话虽如此，如前所述，机器人和自然语言处理的使用正开始悄悄进入游戏开发。\n\n通常，学术和研究 AI 的具体目标与游戏 AI 目标完全不同。这是因为固有的差异，例如每个中使用的实现和技术完全不同。更常见的是，游戏人工智能解决方案会倾向于一种简单的方法，允许简单的更改和调整，而研究方法更有可能选择最科学完整的实现。在接下来的部分中，我们将看一些更简单的游戏开发实现，并讨论它们的用例和理论。\n\n# 做决定\n\n更多时候，AI 的目标是给人智能的表象。智能感知的一个关键方面是人工智能代理正在做出决策的想法。对某些动作有选择，即使是照本宣科，给玩家一种思维世界的感觉，由思维实体组成。在下一节中，我们将介绍游戏 AI 中一些比较知名的决策技术。\n\n# 人工智能状态机\n\n如果你一直在一章一章地阅读这本书，你可能不止一次注意到状态模式的使用。这种模式是一种非常强大的模式，因此在我们的各种组件设计中普遍使用。在人工智能领域，状态模式再次成为一颗闪亮的星星。状态机的使用，特别是**有限状态机** ( **有限状态机**)允许代码执行流程的详细表示。它非常适合在游戏中实现人工智能，允许在没有复杂代码的情况下设计强大的交互。\n\n我不打算在有限状态机实现的概念和理论上花费太多时间，因为我们已经令人作呕地讨论过了。相反，我们将看一个在人工智能脚本中实现它的例子。如果你确实需要复习一下模式，复习[第 5 章](05.html)、*构建游戏系统*，关于理解状态的部分。\n\n以下是描绘敌人简单大脑的图表。在此示例中，每个状态代表一个操作，如搜索或攻击:\n\n![](img/b5527c00-1545-4e52-bce6-e5ed22d608fa.png)\n\n虽然这是一个简单的例子，但它确实为许多情况提供了有用的人工智能。我们可以使用 C++ 在游戏项目中实现这一点，正如我们在*屏幕*示例和其他地方看到的那样。但是，如果您已经通读了前一章，您将会看到我们如何在脚本中实现这样的逻辑。当然，这允许我们灵活地编写脚本，比如不必重新构建项目来调整代码的元素。这对人工智能非常有利，因此在本章中，我将展示使用 Lua 脚本的示例代码，这些脚本可以使用上一章中描述的步骤来实现。\n\n这种人工智能设计在 Lua 脚本中的可能实现看起来类似于以下内容:\n\n```cpp\nSearch = function () \n{ \n    //Do search actions.. \n    if playerFound == true then currentState = Attack end \n} \nAttack = function() \n{ \n    //Do attack actions \n    if playerAttacks == true then currentState = Evade  \n    elseif playerOutOfSight == true then currentState = Search end \n} \nEvade = function() \n{ \n    //Do evade actions \n    If healthIsLow == true then currentState = FindHealth \n    Elseif playerRetreats == true then currentState == Attack end \n} \nFindHealth = function() \n{ \n    //Do finding health actions \n    If healthFound == true then currentState = Search end \n} \ncurrentState = Search \n```\n\n这对于上一章的 NPC 对话示例来说应该很熟悉。在这里，为了完成系统，我们首先将脚本加载到一个人工智能代理或 NPC 的实例中，然后在游戏代码本身的`Update`循环中调用`currentState`变量当前分配的函数。有了这个代码实现，我们就有了构建基本人工智能交互的有效方法。这种技术从游戏开发的早期就已经存在了。事实上，这将是一个非常类似于街机经典 PAC-MAN 中的幽灵对手 AI 的实现。\n\n我们还可以扩展这个简单的有限状态机实现，并在解决方案中添加一个基于堆栈的有限状态机。这也和我们在[第 5 章](05.html)、*构建游戏系统*中看到的实现示例非常相似，所以我不打算详细介绍基于堆栈的有限状态机的理论。基于堆栈的有限状态机的基本原理是，我们可以按照先进先出的顺序在堆栈中添加和移除对象。通常用于向堆栈中添加项目的术语称为推送，从堆栈中移除对象称为弹出。因此，对于状态示例，在不同的函数中，堆栈看起来类似于下图:\n\n![](img/8f905652-25f7-47ee-b792-c05b26056e84.png)\n\n使用基于堆栈的有限状态机的一个主要优点是，您现在可以使用堆栈来控制当前状态。每个状态都可以从堆栈中弹出，允许执行下一个状态。我们还可以在入口上实现*的概念，在出口*上实现*，允许我们在州内有州。我们可以在每个状态下进行设置和清理等操作，这让我们的人工智能状态系统更加灵活。*\n\n在 Lua 脚本中为基于堆栈的有限状态机实现我们的状态可能看起来类似于以下内容:\n\n```cpp\nStateA =  \n{ \n    Update = function () \n    { \n        //Do state update actions \n} \nOnEnter = function() \n{ \n    //Do actions for first load \n} \nOnExit = function() \n{ \n    //Do action for last call for this state \n} \n} \n```\n\n然后，在我们的 C++ 代码中，我们将添加为基于状态的有限状态机供电所需的架构的其余部分。在这里，我们将创建一个向量或数组对象，它将保存指向从 Lua 脚本加载的状态对象的指针。然后，我们将为当前占据数组中最后一个元素的状态对象调用函数`OnEnter`、`OnExit`和`Update`。如前所述，我们可以通过简单地创建一个枚举并切换案例来处理状态流来处理这个问题。我们也可以走创建一个`StateList`类的路线，该类将实现所需的功能来包装密克罗尼西亚联邦。例如，这个`StateList`类可能如下所示:\n\n```cpp\nclass StateList { \n    public: \n        StateList (); \n        ~ StateList (); \n\n        LuaState * GoToNext(); \n        LuaState * GoToPrevious(); \n\n        void SetCurrentState(int nextState); \n        void AddState(State * newState); \n\n        void Destroy(); \n\n        LuaState* GetCurrent(); \n\n    protected: \n        std::vector< LuaState*> m_states; \n        int m_currentStateIndex = -1; \n    }; \n} \n```\n\n无论您选择哪种方式来实现基于状态的 FSM，您都将获得堆栈控制就位的额外好处。如您所见，状态模式，当用于人工智能开发时，为我们创建人工智能交互提供了一个很好的、灵活的起点。接下来，我们将研究一些其他技术，将决策引入您的人工智能设计。\n\n# 决策树\n\n决策树是由树枝和树叶组成的类似流程图的结构。树的每一个分支都是有条件的，在那里做出决定。每一片叶子都是条件中选择的动作。在树的最远范围，叶子是控制人工智能代理的实际命令。使用决策树结构可以更容易地设计和理解人工智能实现的流程。在决策树中实现的简单人工智能大脑看起来类似于下图:\n\n![](img/f564e89d-5d08-456f-8ce9-0844c823035f.png)\n\n你可能会想，这看起来和听起来很像我们在[第 8 章](08.html)、*高级游戏系统*中实现的对话树。那是因为他们是！就像在使用对话框和选项的情况下一样，使用树形结构是编写人工智能交互流程的好方法。决策树可以非常深，分支和节点调用执行特定功能的子树。这使设计者能够使用大量不同决策的库，这些决策可以链接在一起，提供令人信服的人工智能交互深度。你甚至可以开发出基于当前任务的分支，这些分支可以根据它们的总体需求进行排序，如果期望的任务失败了，这些分支可以退回到其他决策。这种弹性和灵活性是树形结构真正突出的地方。\n\n那些熟悉 C++ 数据结构的人可能已经在考虑如何用代码实现这种树结构。也许你会想到列表。有许多不同的方法来实现决策树。我们可以用外部格式定义树，比如 XML。我们可以用 C++ 和脚本语言(如 Lua)的混合来实现它以及结构和体系结构，但是由于我真的想深入了解树设计，我们将把整个实现放在 Lua 中。大卫·杨在《用 Lua 学习游戏人工智能编程》一书中展示了如何实现这一点的一个很好的例子，所以我们将把我们的简单例子建立在大卫更详细的例子的基础上。\n\n首先，让我们看看树对象的结构。在`DecisionTree.lua`文件中，我们可以有类似于以下代码的内容:\n\n```cpp\nDecisionTree = {}; \n\nfunction DecisionTree.SetBranch(self, branch)     \nself.branch_ = branch; \nend \n\nfunction DecisionTree.Update(self, deltaTime)     \n-- Skip execution if the tree hasn't been setup yet.     \nif (self.branch_ == nil) then \n        return; \n    end \n    -- Search the tree for an Action to run if not currently     \n    -- executing an Action. \n    if (self.currentAction_ == nil) then \n        self.currentAction_ = self.branch_:Evaluate(); \n        self.currentAction_:Initialize(); \n    end \n        local status = self.currentAction_:Update(deltaTime); \nend \nfunction DecisionTree.new() \n    local decisionTree = {}; \n        -- The DecisionTree's data members. \n    decisionTree.branch_ = nil; \n    decisionTree.currentAction_ = nil; \n        -- The DecisionTree's accessor functions. \n    decisionTree.SetBranch = decisionTree.SetBranch; \n    decisionTree.Update = decisionTree.Update; \n        return decisionTree; \nend \n```\n\n在这里，在我们的树结构中，我们实现了一个更新循环，它评估树中的根分支并处理结果操作。一旦动作被创建、处理和完成，决策树将从根分支开始重新评估自己，以确定要执行的下一个动作。\n\n接下来是分支对象。在我们的实现中，分支将由一个条件组成，该条件将决定下一步执行哪个元素。条件求值的职责是返回一个值，该值的范围从 1 到分支中的最大子代数。这将表示下一步应该执行哪个元素。我们的决策分支 Lua 类对象将具有添加附加子对象的基本功能，以及设置在分支计算期间使用的条件函数。在`DecisionBranch.lua`文件中，我们可以有一个类似于下面的实现:\n\n```cpp\nDecisionBranch = {} \nDecisionBranch.Type = \" DecisionBranch \"; \nfunction DecisionBranch.new() \n    local branch = {}; \n    -- The DecisionBranch data members. \n    branch.children_ = {}; \n    branch.conditional_ = nil; \n    branch.type_ = DecisionBranch.Type; \n    -- The DecisionBranch accessor functions. \n    branch.AddChild = DecisionBranch.AddChild; \n    branch.Evaluate = DecisionBranch.Evaluate; \n    branch. SetConditional = DecisionBranch. SetConditional; \n    return branch; \nend \nfunction DecisionBranch.AddChild(self, child, index) \n    -- Add the child at the specified index, or as the last child. \n    index = index or (#self.children_ + 1); \n        table.insert(self.children_, index, child); \nend \nfunction DecisionBranch.SetConditional (self, conditional) \n    self. conditional _ = conditional; \nend \n```\n\n正如大卫在他的例子中指出的那样，由于叶子只是动作，我们可以将每个叶子的动作包含在树枝本身中。这允许我们获得所需的功能，而不需要在代码中添加额外的结构。使用`type_ variable`，我们可以确定该分支的一个子分支是另一个分支还是需要执行的动作。\n\n对于分支本身的评估，我们执行条件，然后使用返回值来确定树中的下一步。应该注意的是，树中的每个分支最终都必须以一个动作结束。如果树中有未以操作结束的叶子，则树的格式不正确，并且将无法正确评估。\n\n停留在`DecisionBranch.lua`文件中，评估分支的代码如下所示:\n\n```cpp\nfunction DecisionBranch.Evaluate(self) \n    -- Execute the branch's evaluator function, this will return a \n    -- numeric value which indicates what child should execute. \n    local conditional = self. conditional _(); \n    local choice = self.children_[conditional]; \n    if (choice.type_ == DecisionBranch.Type) then \n        -- Recursively evaluate children to see if they are decision branches. \n        return choice:Evaluate(); \n    else \n        -- Return the leaf action. \n        return choice; \n    end \nend \n```\n\n现在我们已经有了树形数据结构，我们可以继续构建一个供使用。为此，我们首先创建决策树的新实例，在树中创建所需的每个分支，连接条件分支，最后添加动作叶子。在`AILogic.lua`文件中，我们可以有类似于下面的内容:\n\n```cpp\nfunction AILogic_DecisionTree() \n    --Create a new instance of the tree \n    local tree = DecisionTree.new(); \n--Add branches \nlocal moveBranch = DecisionBranch.new(); \n    local shootBranch = DecisionBranch.new(); \n    --Connect the conditional branches and action leaves \n... \nmoveBranch:AddChild(MoveAction()); \n      moveBranch:AddChild(randomBranch); \n      moveRandomBranch:SetConditional( \n        function() \n            if Conditional_HasMovePosition() then \n                return 1; \n            end \n            return 2; \n        end); \n... \n    --Set initial branch \n    tree:SetBranch(moveBranch); \nreturn tree; \nend \n```\n\n有了决策树，我们现在可以调用这个脚本，并将树加载到人工智能代理对象中。我们可以随时做出改变，添加更多的决策和行动，甚至添加其他人工智能技术来增强决策。虽然决策树允许开发人员和设计人员创建易于理解和阅读的人工智能结构，但它确实有其缺点。最显著的缺点之一是它对复杂逻辑条件的建模，在这种情况下，您需要考虑条件的每个可能结果。此外，随着更多的分支可能性，一棵树也将开始需要平衡。如果这种平衡没有发生，树的一部分将需要被复制，迅速增加树结构的复杂性并导致更容易出错的代码。\n\n# 反馈回路\n\n在人工智能决策中，我想简要谈及的最后一个话题是反馈循环的概念。反馈回路是系统的某个输出值反馈给系统的一种情况，反过来又影响系统的状态，从而影响其后续值。理想情况下，在视频游戏中，尤其是在 AI 交互中，每个循环都应该是一个稳定的反馈循环。稳定反馈回路的简单定义是这样一种情况，即系统的输出被用来逆转最初导致反馈值的情况，使反馈系统移向稳定状态的收敛。这可以防止你的人工智能反馈造成负面或正面反馈循环的失控效应。\n\n为了帮助你理解什么是真正的反馈回路，让我们举一个在电子游戏中最常见的例子，耐力。耐力可以在很多场景中看到，比如角色的冲刺或奔跑能力，或者角色的攀爬能力。在我们的例子中，我们将看拳击比赛的例子。下图显示了我们想要实现的反馈回路:\n\n![](img/3a09fb51-5a12-447c-8e09-6f492d48f86b.png)\n\n如前所述，我们需要确保拳击示例的耐力反馈回路是稳定的。这意味着当我们达到预定义的低耐力水平时，我们需要将循环切换到防守，让我们恢复耐力。如果我们达到一个预定的补充水平，我们反其道而行之，转而攻击来降低耐力水平。这种切换允许我们保持环路稳定，被称为振荡反馈环路。\n\n在代码中实现这一点非常简单:\n\n```cpp\nvoid Update(float deltaTime) \n{ \n    if(currentState == attacking) \n    { \n        ReduceStamina(); \n    if(player.stamina <= depleted) \n{ \n        currentState = defending; \n} \n} \nelse if (currentState == defending) \n{ \n    IncreaseStamina(); \n    if(stamina >= replenished) \n    { \n        currentState = attacking; \n    } \n} \n} \n```\n\n说实话，就是这样。编写这种技术的实现代码并不复杂。我们确实跳过了一些事情，比如如何处理减少和增加耐力。考虑到这是在人工智能系统中，我们希望它看起来更真实，所以静态地增加这些值并不好。找到一个好的随机值放在这里可以给它一个更真实的感觉。最终，这是一种易于实现的技术，它提供了一种改变结果的好方法，并提供了与人工智能组件更独特的交互。\n\n# 运动和寻路技术\n\n人工智能代理和其他非玩家角色经常需要在游戏世界中移动。实施这一运动，使其以逼真的方式出现是一个具有挑战性的过程。在下一节中，我们将看看如何实现算法和技术，将人工智能代理移动和寻路添加到我们的游戏开发项目中。\n\n# 运动算法和技术\n\n使用运动算法来控制人工智能代理在整个关卡或游戏世界中的移动是视频游戏中人工智能算法的一个非常常见的用例。这些算法可以实现行为来给人一种思考和反应的人工智能代理的印象，并且它们还可以执行其他任务，例如简单的对象回避。在下一节中，我们将研究其中的一些运动技术。\n\n# 转向行为\n\n转向行为是运动算法的一个子集，包括基于外部和内部变量控制人工智能代理运动的各种技术。在我们的示例引擎中，我们已经合并了一个 3D 物理计算库——请再次参考[第 5 章](05.html)、*构建游戏系统*，了解更多信息——并且我们已经有了一个 NPC 类作为我们的人工智能代理的概念。这意味着我们有很大一部分所需的框架来创建一个基于牛顿物理的转向系统，也称为基于转向的运动系统。基于转向的移动系统由几个不同的分类组成，用于向人工智能代理添加力。这些分类包括寻找、逃离、逃避、流浪、追捕等等。这些算法的完全详细的实现将单独占据章节，因此我们将关注每个算法的高级概念和用例。为了帮助您实现，我在示例引擎中包含了`OpenSteer`库。`OpenSteer`将处理计算的细节，使我们的引擎和我们的 AI Lua 脚本更容易使用这些算法来控制代理的移动。\n\n以下是运行寻道和规避算法的库程序的截图:\n\n![](img/43b0b05a-2f94-4307-b446-e3279bd3c877.png)\n\n# 寻求\n\n让我们从寻道算法开始。搜索算法的目标是将人工智能代理导向游戏空间中的特定位置。此行为会施加力，使当前航向和所需航向对准目标目标点。下图描述了这一过程:\n\n![](img/f1d19952-67a0-4381-b46c-cda9dfd4ce6b.png)\n\n**期望航向**实际上是从角色到目标方向的矢量。**所需标题**的长度可以设置为一个值，例如角色的当前速度。转向矢量或**寻道**是这个期望的航向和角色当前航向之间的差值。这个等式可以简化为如下所示:\n\n```cpp\n    desiredHeading = normalize (position - target) * characterSpeed \n    steeringPath = desiredHeading - velocity \n```\n\n寻道算法的一个有趣的副作用是，如果 AI 代理继续寻道，它最终会穿过目标，然后翻转方向再次接近目标。这产生了一个运动路径，看起来有点像一只飞蛾在灯泡周围嗡嗡叫。要使用`OpenSteer`计算转向力，需要调用`steerForSeek`函数，传递一个 3 点矢量来描述目标的位置:\n\n```cpp\nVec3 steerForSeek (const Vec3& target); \n```\n\n# 消失\n\n逃离转向行为只是寻道的逆过程。逃离算法不是将航向对准特定目标，而是引导人工智能代理的航向对准远离目标点的方向。这样，期望的航向指向相反的方向。下图显示了此过程:\n\n![](img/248bf05a-120d-4975-97c3-1368cf84ca4d.png)\n\n要使用`OpenSteer`计算逃跑的人工智能代理的转向力，您需要调用`steerForFlee`函数，传递一个 3 点矢量来描述目标的位置:\n\n```cpp\n Vec3 steerForFlee (const Vec3& target); \n```\n\n# 追求\n\n追击转向行为与寻道行为非常相似，但这里的区别在于目标点实际上是一个移动的物体或玩家。下图说明了这种行为:\n\n![](img/cc201d75-cfb0-4d7f-be36-63174f2b835b.png)\n\n为了创造有效的追求行为，当涉及到目标的未来位置时，我们需要做一些预测。我们可以采取的一种方法是使用一种预测方法，我们可以在每个更新循环中对其进行重新评估。在我们的简单预测器中，我们将假设我们的目标在这个更新循环中不会转动。虽然这种假设通常是错误的，但预测结果仅用于几分之一秒(1/30)。这意味着，如果目标确实偏离，将在下一个模拟步骤中计算基于目标变化方向的快速修正。同样在这个假设下，目标的位置是未来的 X 个时间单位，可以通过按 X 缩放其速度并将该偏移量添加到其当前位置来计算。那么实际上是将寻道转向行为应用于预测的目标位置以实现寻道行为的问题。\n\n要使用`OpenSteer`来计算追赶的人工智能代理的转向力，您可以调用`steerForPursuit`函数，传递一个对象作为我们追赶的目标:\n\n```cpp\nVec3 steerForPursuit (const TargetObject& target); \n```\n\n# 借口\n\n就像逃跑是寻求的对立面，逃避是追求的对立面。这意味着，我们不是将人工智能代理导向目标计算的未来位置，而是逃离目标的当前位置。下图说明了这种行为:\n\n![](img/d565e6ae-e0fc-4f4e-8276-c69a6bd33bc3.png)\n\n当使用规避转向行为时，人工智能代理将远离预测的拦截点。这通常会导致不太自然的行为，因为大多数真正逃离的实体可能会有一个随机的逃避模式。实现更自然效果的一种方法是修改用另一种行为施加的力，例如我们接下来将谈到的流浪行为。\n\n要使用`OpenSteer`来计算躲避 AI 代理的转向力，您需要调用`steerforEvasion`函数，传递一个要用作我们躲避的目标的对象，以及一个浮点值来指定将来在计算要施加的力时要使用的最大时间量:\n\n```cpp\nVec3 steerForEvasion (const AbstractVehicle& menace, \n                      const float maxPredictionTime); \n```\n\n# 漫游的\n\n正如我之前提到的，有时最好通过添加另一个行为来修改力，从而在一个行为中有一些波动。流浪行为是改变行为的一个很好的例子。徘徊行为基本上返回一个与代理的正向矢量相关的切线转向力。应该注意的是，由于游走行为意味着给代理的移动增加一些偏差，所以它不应该单独用作转向力。\n\n要使用`OpenSteer`计算人工智能代理的漂移转向力，需要调用`steerForWander`函数，传递一个浮点值来指定漂移之间的时间步长。当帧时间变化时，时间步长值允许漂移速率保持一致:\n\n```cpp\nVec3 steerForWander (float dt); \n```\n\n虽然在这本书里，这是我们能奉献给人工智能控制行为的所有时间，但我们只是真正开始触及可用的表面。遗憾的是，像群集和简单对象避免这样的概念不在本章的讨论范围内，但是`OpenSteer`库完全支持。如果您有兴趣了解更多关于这些行为的信息，我强烈建议阅读`OpenSteer`文档。\n\n# 搜索算法和寻路技术\n\n在游戏中的很多情况下，我们经常需要找到一条从一个位置到下一个位置的路径。游戏开发中对人工智能的另一个非常常见的需求，也是我们将在本章中谈到的最后一个需求，是使用搜索算法来找到在人工智能代理周围移动的最佳路径。\n\n例如，这里我们将重点讨论图搜索算法。顾名思义，图形搜索算法将图形作为数据输入的来源。在我们的地图示例中，图形是一组位置以及它们之间的连接。这些通常分别称为节点和边。下面是一个非常基本的图形数据的示例:\n\n![](img/d09f3fab-ad19-4a00-ac89-5e885b73a3a2.png)\n\n这些图搜索算法的输出可以用来开发人工智能代理需要采取的路径。这条路径由图的节点和边组成。需要注意的是，算法会告诉你的人工智能往哪里移动，但不会提供如何移动。这些算法不像本章前面的转向力算法，因为它们不会移动人工智能代理。然而，结合转向算法，这些寻路算法将创造伟大的整体人工智能行为。\n\n现在，我们已经对图形如何表示地图以及我们想要在其中找到路径的点有了基本的了解，让我们看看一些最常用的算法。\n\n# 广度优先\n\n广度优先搜索是最简单的搜索算法。它平等地探索所有方向。那么它是如何探索的呢？在所有这些搜索算法中，关键思想是跟踪一个不断扩大的区域，称为前沿。广度优先算法通过从起点移出并首先检查其邻居，然后检查其邻居的邻居，等等来扩展这一边界。下图显示了这种扩展是如何在网格上发生的。数字表示网格正方形被访问的顺序:\n\n![](img/b90ad766-5e37-42ba-b7ff-2f12df3882f0.png)\n\n下面是一个简单的例子，说明我们如何在 C++ 中实现这一点。为了节省篇幅，我省略了几段代码。完整的实现可以在`Chapter09`示例项目中找到，在源代码库中:\n\n```cpp\nvoid SearchGraph::BreadthFirst(int s) \n{ \n    // Mark all the vertices as not visited \n    bool *visited = new bool[V]; \n    for(int i = 0; i < V; i++) \n        visited[i] = false; \n\n    // Create a queue for BFS \n    list<int> queue; \n\n    // Mark the current node as visited and enqueue it \n    visited[s] = true; \n    queue.push_back(s); \n\n    // 'i' will be used to get all adjacent vertices of a vertex \n    list<int>::iterator i; \n\n    while(!queue.empty()) \n    { \n        // Dequeue a vertex from queue and print it \n        s = queue.front(); \n        cout << s << \" \"; \n        queue.pop_front(); \n\n        // Get all adjacent vertices of the dequeued vertex s \n        // If a adjacent has not been visited, then mark it visited \n        // and enqueue it \n        for(i = adj[s].begin(); i != adj[s].end(); ++ i) \n        { \n            if(!visited[*i]) \n            { \n                visited[*i] = true; \n                queue.push_back(*i); \n            } \n        } \n    } \n} \n```\n\n您可能已经从源代码中注意到，该算法的一个技巧是我们需要避免重复处理一个节点不止一次。在这个简单的例子中，我们实现了一个被访问节点的布尔值数组。如果我们在这个例子中不标记访问过的顶点，我们会创建一个无休止的循环过程。\n\n这是一个非常有用的算法，不仅适用于常规寻路，还适用于程序地图生成、流场寻路、距离地图和其他类型的地图分析。\n\n# 迪克斯特拉算法\n\n在某些情况下，当每个步骤可能有不同的相关成本时，我们需要找到最短的路径。例如在*文明*游戏系列中，穿越不同的土地类型需要每次移动不同的回合数。在这种情况下，我们可以实现迪克斯特拉算法，也称为**统一成本搜索**。这个算法可以让我们优先选择要探索的路径。它没有平等地探索所有可能的路径，而是倾向于成本较低的路径。为了完成路径的优先排序，我们需要跟踪移动成本。本质上，我们希望在决定如何评估每个位置时考虑移动成本。在这个算法中，我们需要一个优先级队列或堆。使用堆代替常规队列改变了边界扩展的方式。下面是用 C++ 演示 Dijkstra 算法的示例代码的摘录，为了节省篇幅，我再次跳过了几段。您可以在源存储库的`Chapter09`文件夹中找到完整的 Dijkstra 示例:\n\n```cpp\n// Prints shortest paths from src to all other vertices \nvoid SearchGraph:: Dijkstra(int src) \n{ \n    // Create a priority queue to store vertices that are being preprocessed \n    priority_queue< iPair, vector <iPair> , greater<iPair> > pq; \n\n    // Create a vector for distances and initialize all distances as infinite (INF) \n    vector<int> dist(V, INF); \n\n    // Insert source itself in priority queue and initialize its distance as 0\\. \n    pq.push(make_pair(0, src)); \n    dist[src] = 0; \n\n    /* Looping till priority queue becomes empty (or all \n      distances are not finalized) */ \n    while (!pq.empty()) \n    { \n        int u = pq.top().second; \n        pq.pop(); \n\n        // 'i' is used to get all adjacent vertices of a vertex \n        list< pair<int, int> >::iterator i; \n        for (i = adj[u].begin(); i != adj[u].end(); ++ i) \n        { \n            // Get vertex label and weight of current adjacent of u. \n            int v = (*i).first; \n            int weight = (*i).second; \n\n            // If there is shorted path to v through u. \n            if (dist[v] > dist[u] + weight) \n            { \n                // Updating distance of v \n                dist[v] = dist[u] + weight; \n                pq.push(make_pair(dist[v], v)); \n            } \n        } \n    } \n\n    // Print shortest distances stored in dist[] \n    printf(\"Vertex   Distance from Sourcen\"); \n    for (int i = 0; i < V; ++ i) \n        printf(\"%d tt %dn\", i, dist[i]); \n}  \n```\n\n当使用不同的成本寻找最短路径时，这种算法很棒，但是它确实浪费时间在各个方向上探索。接下来，我们将研究另一种算法，它可以让我们找到到达单个目的地的最短路径。\n\n# A*\n\n可以说，寻路中使用的最佳和最受欢迎的技术之一是 **A*** 算法。A*是对 Dijkstra 算法的修改，针对单个目的地进行了优化。在 Dijkstra 的算法可以找到所有位置的路径的地方，A*找到一个位置的路径。它优先考虑那些看起来更接近目标的路径。该实现与 Dijkstra 实现非常相似，但不同之处在于使用启发式搜索函数来增强算法。这种启发式搜索用于估计到目标的距离。这相当于 A*使用一个 Dijkstra 搜索和一个启发式搜索的总和来计算到达某个点的最快路径。\n\n以下是解释 A*算法过程的伪代码实现的一个很好的例子，由维基百科提供([https://en.wikipedia.org/wiki/A*_search_algorithm](https://en.wikipedia.org/wiki/A*_search_algorithm)):\n\n```cpp\nfunction A*(start, goal) \n    // The set of nodes already evaluated \n    closedSet := {} \n\n    // The set of currently discovered nodes that are not evaluated yet. \n    // Initially, only the start node is known. \n    openSet := {start} \n\n    // For each node, which node it can most efficiently be reached from. \n    // If a node can be reached from many nodes, cameFrom will eventually contain the \n    // most efficient previous step. \n    cameFrom := the empty map \n\n    // For each node, the cost of getting from the start node to that node. \n    gScore := map with default value of Infinity \n\n    // The cost of going from start to start is zero. \n    gScore[start] := 0 \n\n    // For each node, the total cost of getting from the start node to the goal \n    // by passing by that node. That value is partly known, partly heuristic. \n    fScore := map with default value of Infinity \n\n    // For the first node, that value is completely heuristic. \n    fScore[start] := heuristic_cost_estimate(start, goal) \n\n    while openSet is not empty \n        current := the node in openSet having the lowest fScore[] value \n        if current = goal \n            return reconstruct_path(cameFrom, current) \n\n        openSet.Remove(current) \n        closedSet.Add(current) \n\n        for each neighbor of current \n            if neighbor in closedSet \n                continue        // Ignore the neighbor which is already evaluated. \n\n            if neighbor not in openSet    // Discover a new node \n                openSet.Add(neighbor) \n\n            // The distance from start to a neighbor \n            tentative_gScore := gScore[current] + dist_between(current, neighbor) \n            if tentative_gScore >= gScore[neighbor] \n                continue        // This is not a better path. \n\n            // This path is the best until now. Record it! \n            cameFrom[neighbor] := current \n            gScore[neighbor] := tentative_gScore \n            fScore[neighbor] := gScore[neighbor] + heuristic_cost_estimate(neighbor, goal) \n\n    return failure \n\nfunction reconstruct_path(cameFrom, current) \n    total_path := [current] \n    while current in cameFrom.Keys: \n        current := cameFrom[current] \n        total_path.append(current) \n    return total_path \n```\n\n这就结束了我们对一些更常见的寻路技术的快速浏览。虽然我们在这一部分确实看到了一些实现，但是如果您正在为您的产品游戏寻找一个很好的起点，我强烈建议您查看一些可用的开源库。这些都是非常有价值的学习资源，并提供了您可以利用的行之有效的实现技术。\n\n# 摘要\n\n在这一章中，我们在短时间内覆盖了一个很大的研究领域。我们开发了一个游戏人工智能到底是什么，以及它不是什么的基本定义。在这一章中，我们还研究了用人工智能技术扩展决策功能。我们介绍了如何通过使用转向力和行为来控制人工智能代理的移动。最后，我们通过观察寻路算法的使用来为我们的人工智能代理创建从点到点的路径，从而结束了这一章。虽然我们在这一章中确实涵盖了相当多的内容，但在游戏人工智能的世界中，仍有很多东西有待发掘。我恳求你继续你的旅程。在下一章，我们将看看如何将多人游戏和其他网络功能添加到我们的示例游戏引擎中。"
  },
  {
    "path": "docs/master-cpp-game-dev/10.md",
    "content": "# 十、多个玩家\n\n自从我最早的游戏冒险以来，我发现分享经历总是会让它更加难忘。回想当年，多人游戏的概念围绕着在沙发上和朋友一起玩，或者和其他游戏爱好者聚在一起进行史诗般的 **LAN** ( **局域网**)聚会。自那以后，情况发生了巨大变化，在线、全球共享的游戏体验成为新的常态。在这一章中，我们将介绍在你的游戏项目中增加多人支持的概念，特别关注联网多人。正如我以前说过的那样，计算机网络是一个非常庞大和多样化的主题，需要的时间和空间比我们必须全面讨论的还要多。相反，我们将专注于高级概述，并深入到需要的地方。在本章中，我们将涵盖以下主题:\n\n*   游戏中的多人游戏介绍\n*   网络设计和协议开发\n*   创建客户端/服务器\n\n# 游戏中的多人游戏介绍\n\n简单来说，多人游戏是一种多人同时玩的电子游戏。虽然单人电子游戏通常是围绕一个玩家与人工智能对手竞争并实现预定目标而设计的，但多人游戏是围绕与其他人类玩家的互动而设计的。这些互动可以是竞争、伙伴关系或简单的社会参与。这些多玩家互动是如何实现的，取决于位置和类型等因素，从同屏多人对战的战斗机游戏到在线多人角色扮演游戏，用户共享一个共同的环境。在下一节中，我们将了解视频游戏中多人互动的各种方式。\n\n# 本地多人游戏\n\n游戏中多人游戏的想法最早是以本地多人游戏的形式出现的。很早以前，很多游戏都有双人模式。一些游戏会实现一个两人模式，即多人游戏，玩家可以轮流玩游戏。尽管如此，但即使在早期，开发人员也看到了共享体验的好处。甚至最早的游戏如*太空战！* (1962 年)和 *PONG* (1972 年)让选手们相互对抗。街机游戏场景的兴起有助于推动本地多人游戏，如*战书* (1985)等游戏提供了多达四名玩家的合作游戏体验。\n\n大多数本地多人游戏可以分为几个类别之一，基于回合的，共享的单屏或分屏多人游戏。\n\n基于回合的，顾名思义，是一种多人模式，玩家轮流使用一个屏幕玩游戏。回合制多人游戏的一个很好的例子是最初的超级马里奥兄弟(T1)，用于任天堂娱乐系统(T3)(T4 NES)。在这个游戏中，如果选择两人模式，第一个玩家扮演马里奥角色；当玩家死亡时，第二个玩家就轮到他们了，并扮演另一个兄弟，路易吉。\n\n共享单屏多人模式是一种常见的本地多人模式，每个玩家的角色都在同一个屏幕上。每个玩家可以同时控制自己的角色/头像。这种模式非常适合对抗游戏，如体育和格斗游戏，以及合作游戏，如平台和解谜游戏。这种模式在今天仍然非常流行，最近发布的 Cuphead 标题就是一个很好的例子。\n\n# 单屏幕多人游戏\n\n分屏多人模式是另一种流行的本地多人模式，每个玩家都有整个本地屏幕的一部分作为他们的游戏视图。每个玩家同时控制自己的角色/头像。这种模式非常适合对战游戏，比如射击游戏。尽管大多数选择实现分屏模式的游戏都是双人游戏，但有些游戏支持多达四名本地玩家，本地屏幕被垂直和水平分成四个部分。实现分屏多人游戏的一个很好的例子是第一人称射击游戏*光环*。\n\n# 局域网\n\n随着 20 世纪 90 年代初个人电脑的激增，将电脑连接在一起共享信息的想法很快发展成为大多数电脑用户的核心需求。将多台计算机连接在一起的一种早期方法是通过局域网。局域网允许有限区域内的计算机，如大学、办公室、学校，甚至个人住宅。默认情况下，局域网是不可连接，除非您在局域网所在的有限区域内。虽然商业计算世界已经采用了局域网计算的理念，但随着 1993 年 *DOOM* 的发布，游戏行业真正开始将该技术用于多人游戏。\n\n自从互联网被广泛采用以来，基于局域网的多人游戏的普及已经停止了。也就是说，局域网仍然是当今电子竞技联盟等比赛中多人游戏的玩法。基于局域网的多人游戏的想法也催生了一种被称为“局域网聚会”的现象。局域网聚会是游戏玩家聚集在同一个物理位置，将他们所有的电脑连接在一起，以便相互游戏。这些活动通常持续多天，玩家要长途跋涉才能参加。局域网聚会是 20 世纪 90 年代初至后期游戏领域的主要活动，对于任何参与的游戏玩家来说，这都是一种与其他游戏玩家联系的难忘方式。\n\n# 在线多人游戏\n\n互联网的普及为全世界的游戏玩家带来了一种全新的联系和游戏方式。与过去的局域网聚会不同，游戏玩家现在可以在不离开自己舒适的家的情况下，与来自世界各地的游戏玩家一起玩耍和竞争。在线多人游戏的历史可以追溯到早期的例子，比如 **MUD** ( **多用户地下城**)，用户可以在互联网上玩简单的 RPG。在线多人游戏几乎涵盖了当今的所有游戏类型，从第一人称射击游戏到实时战略游戏。基于互联网的游戏也催生了一种新的游戏类型，称为**大型多人在线** ( **MMO** )游戏。在多人在线游戏中，大量的玩家可以在一个单一的实例或世界中连接和互动。迄今为止最受欢迎的 MMO 游戏之一是*魔兽世界*。\n\n# 网络设计和协议开发\n\n在设计和开发多人游戏时，最大的两个考虑因素是决定要使用的网络拓扑和连接协议。每一个选择都对实现和游戏本身有重大影响。在本章的下一部分，我们将介绍不同的网络拓扑和使用的协议，并讨论它们的各种影响和注意事项。\n\n# 网络拓扑结构\n\n简单来说，网络拓扑就是网络上的计算机相互连接的方式。对于在线游戏，网络拓扑将决定网络上的计算机如何组织，以允许用户接收游戏更新。计算机如何联网将决定整个多人游戏设计的许多方面，每种类型的拓扑都有自己的优缺点。在下一节中，我们将介绍游戏开发中最流行的两种拓扑，客户端/服务器和对等模型。\n\n# 点对点\n\n在对等网络中，每个玩家都与游戏实例中的其他玩家相连:\n\n![](img/d3d659e4-2a88-41f3-89bb-e8c3d6ec723a.png)\n\n对等网络通常以非权威的设计实现。这意味着没有控制游戏状态的单一实体，因此每个玩家都必须处理自己的游戏状态，并将任何本地更改传达给连接的其他玩家。这意味着，作为这种拓扑的结果，我们有几个问题需要考虑。首先是带宽；正如你可能想象的那样，这种设计有大量的数据需要在玩家之间传递。事实上，连接数可以表示为二次函数，其中每个参与者将有 O(n-1)个连接，这意味着对于该网络拓扑，总共将有 O(2n)个连接。这种网络设计也是对称的，这意味着每个玩家都必须有相同的可用带宽用于上传和下载流。我们需要考虑的另一个问题是权威的概念。\n\n正如我在这里提到的，在对等网络中处理权限的最常见方法是让所有玩家共享网络上其他玩家的更新。以这种方式处理权限的结果是，玩家同时看到两种情况，玩家自己的输入即时更新游戏状态，并且模拟另一个玩家的动作。因为来自其他玩家的更新必须经过网络，所以更新不是即时的。当本地玩家收到更新时，比如将对手移动到(x，y，z)，在收到更新时对手仍然在那个位置的几率很低，这就是为什么模拟其他玩家的更新。模拟更新的最大问题是，随着延迟的增加，模拟变得越来越不准确。我们将在本章的下一节讨论处理更新延迟和模拟问题的技术。\n\n# 客户端/服务器\n\n在客户机-服务器拓扑中，一个实例被指定为服务器，所有其他播放器实例都连接到它。每个玩家实例(客户端)将只与服务器通信。服务器反过来负责将玩家的所有更新传送给网络上连接的其他客户端。下图演示了该网络拓扑:\n\n![](img/15371b3c-817a-4dd0-8f36-3efc7ed10f5d.png)\n\n虽然不是唯一的方法，但是客户机-服务器网络通常实现权威的设计。这意味着，当玩家执行一个动作时，比如将他们的角色移动到另一个地方，该信息会以更新的形式发送到服务器。服务器检查更新是否被认为是正确的，如果是，则服务器将此更新信息转发给网络上连接的其他玩家。如果客户端和服务器在更新信息上存在分歧，则服务器被认为是正确的版本。就像对等拓扑一样，在实现时需要考虑一些事情。说到带宽，理论上每个玩家的带宽需求不会因为连接的玩家数量而改变。如果我们以二次公式的形式来看，给定 n 个玩家，连接的总数将是 O(2n)。但是，与对等拓扑不同，客户机-服务器拓扑是非对称的，这意味着服务器将只有 O(n)个连接，或者每个客户机有一个到一个连接。这意味着随着连接的玩家数量增加，支持连接所需的带宽将线性增加。也就是说，在实践中，随着更多玩家的加入，需要模拟更多的对象，这可能会导致客户端和服务器的带宽需求略有增加。\n\nAn authoritative design is considered more secure against cheating. This is because the server fully controls the game states and update. If a suspicious update is passed from a player, the server can ignore it and provide the correct update information to the other clients instead.\n\n# 理解协议\n\n在开始实施多人游戏之前，重要的是要了解如何在幕后处理事情。最重要的方面之一是两台计算机之间的数据交换方式。这就是协议的作用。虽然网络上有很多不同的数据交换方式，但在本节中，我们将关注**传输控制协议/互联网协议** ( **TCP/IP** )模型，重点是主机到主机层协议。\n\n# TCP/IP 模型\n\nTCP/IP 模型是对协议套件的描述，协议套件是一组协议的集合，旨在协同工作，将数据从一台计算机传输到另一台计算机。它以两种主要协议(TCP 和 IP)命名。TCP/IP 被认为是当今事实上的标准协议，已经取代了旧的协议套件，如 IPX 和 SPX。TCP/IP 协议套件可以分解为 4 层模型，如下图所示:\n\n![](img/8101e9f1-4a5d-41d6-b776-6243c8ca3b9d.png)\n\nMost modern networking courses teach the 7-layer OSI model. The OSI model is an idealized networking model and, as of yet, not a practical implementation.\n\n这四层分为应用层、传输层、网络层和数据链路层。应用是向用户表示数据并处理编码和对话控制的层。一种常见的应用层协议是**超文本传输协议(HTTP** ，这是一种为我们日常使用的网站提供动力的协议。传输层，也称为主机到主机层，是支持各种设备和网络之间的低层通信的层，与所使用的硬件无关。接下来我们将深入到这一层。网络层是确定数据通过网络的最佳路径并处理寻址的层。这一层最常见的协议是**互联网协议** ( **IP** )。IP 有两个版本:IPv4 标准和 IPv6 标准。第四层也是最后一层是数据链路层或网络接入层。数据链路层指定组成网络的硬件设备和介质。常见的数据链路协议是以太网和无线网络。\n\n现在我们已经对各层有了一个大致的了解，让我们来仔细看看游戏开发中最常用的两个网络层协议:TCP 和 UDP。\n\n# 用户数据报协议\n\n首先，让我们看看**用户数据报协议** ( **UDP** )。UDP 是一种非常轻量级的协议，可用于将数据从一台主机上的指定端口传递到另一台主机上的另一个指定端口。在一个实例中发送的一组数据被称为数据报。数据报由一个 8 字节的报头和要传递的数据组成，称为有效载荷。下表描述了一个 UDP 报头:\n\n| **位#** | Zero | Sixteen |\n| 0-31 | 源端口 | 目的港 |\n| 32-63 | 长度 | 校验和 |\n\nUDP header\n\n要一点一点地分解它:\n\n*   **源端口** : (16 位)这标识了传输数据的起始端口。\n*   **目的端口:** (16 位)这是正在传递的数据的目标端口。\n*   **长度** : (16 位)这是 UDP 报头和数据有效负载的总长度。\n*   **校验和** : (16 位，可选)这是根据 UDP 报头、有效负载和 IP 报头的某些字段计算的校验和。默认情况下，该字段设置为全零。\n\n因为 UDP 是如此简单的协议，所以它放弃了一些特性来保持它的轻量级。缺少的一个特性是两台主机之间的共享状态。这意味着没有努力确保数据报的完全通过。即使数据到达，也不能保证数据到达时的顺序是正确的。这与我们将看到的下一个协议，TCP 协议有很大不同。\n\n# 传输控制协议\n\n与传输单个数据报的 UDP 不同，TCP 协议，顾名思义，为两台主机之间的传输创建了一个持续的连接。这允许可靠的数据流在主机之间来回传递。TCP 还试图确保发送的所有数据都被实际接收，并且顺序正确。有了这些额外的功能，就有了一些额外的开销。TCP 连接的报头比 UDP 的报头大得多。表格式的 TCP 报头描述如下:\n\n![](img/477d5bac-3328-4025-b9d0-fb7219a94faa.png)\n\nTCP header\n\n对于 TCP 连接，数据传输的单位称为段。一个数据段由 TCP 报头和在该数据段中传递的数据组成。\n\n让我们一点一点地把它分解如下:\n\n*   **源端口** : (16 位)这标识了传输数据的起始端口。\n*   **目的端口** : (16 位)这是正在传递的数据的目标端口。\n*   **序列号** : (32 位)这是一个唯一的标识符号码。由于 TCP 试图让接收方按照发送的顺序接收数据，因此通过 TCP 传输的每个字节都会收到一个序列号。这些数字允许收件人和发件人通过遵循这些数字的顺序来确保顺序。\n*   **确认号** : (32 位)这是发送方正在传递的下一字节数据的序列号。本质上，这是对序列号低于该数字的所有数据的确认。\n*   **数据偏移量** : (4 位)这指定了 32 位字中报头的长度。如果需要，它允许添加自定义标题组件。\n*   **控制位** : (9 位)该位保存关于报头的元数据。\n*   **接收窗口** : (16 位)这传达了发送方用于传入数据的剩余缓冲空间量。当试图保持流量控制时，这一点很重要。\n*   **紧急指针** : (16 位)这是该段数据的第一个字节和紧急数据的第一个字节之间的差值。这是可选的，并且只有在标题的元数据中设置了`URG`标志时才相关。\n\n# 引入插座\n\n在现场视察模型中，有几种不同类型的套接字决定了传输层的结构。两种最常见的类型是流套接字和数据报套接字。在本节中，我们将简要介绍它们以及它们的不同之处。\n\n# 流套接字\n\n流套接字用于不同主机之间的可靠双向通信。您可以认为流套接字类似于打电话。当一台主机呼叫时，另一台主机的连接被启动；一旦建立了连接，双方就可以来回通信。这种联系就像一条小溪一样持续不断。\n\n我们在本章前面讨论的传输控制协议中可以看到流套接字的使用示例。使用 TCP 允许数据以序列或数据包的形式发送。如前所述，TCP 维护状态，并提供一种方法来确保数据到达，并且与发送的顺序相同。这对于许多类型的应用都很重要，包括 web 服务器、邮件服务器及其客户端应用之间的通信。\n\n在后面的部分，我们将研究如何使用传输控制协议实现自己的流套接字。\n\n# 数据报套接字\n\n与流套接字相反，数据报套接字不太像打电话，更像是通过邮件发送信件。数据报套接字连接是单向的，并且是不可靠的连接。不可靠是指您不能确定数据报套接字数据何时或是否会到达接收方。没有办法保证数据到达的顺序。\n\n如前一节所述，用户数据报协议使用数据报套接字。虽然 UDP 和数据报套接字更轻量级，但当您只需要发送数据时，它们提供了一个很好的选择。在许多情况下，创建流套接字、建立并维护套接字连接的开销会过大。\n\n数据报套接字和 UDP 通常用于网络游戏和流媒体。当客户端需要向服务器进行简短查询，并且期望收到单个响应时，UDP 通常是一个很好的选择。为了提供这种发送和接收服务，我们需要使用套接字实现中的 UDP 特定函数调用`sendto()`和`recvfrom()`、`instead of read()`和`write()`。\n\n# 创建一个简单的 TCP 服务器\n\n在本节中，我们将研究使用前面章节中讨论的套接字技术实现一个简单的 TCP 服务器示例的过程。这个例子可以扩展到支持各种游戏需求和功能。\n\n由于每个平台创建服务器的过程略有不同，因此我将示例分成了两个不同的版本。\n\n# Windows 操作系统\n\n首先，让我们看看如何使用 Windows 平台上的 WinSock 库来创建一个简单的套接字服务器，该服务器将侦听连接，并在建立连接时打印出一条简单的调试消息。要获得完整的实现，请查看代码库的`Chapter10`目录:\n\n```cpp\n…\n#include <stdio.h>\n#include <windows.h>\n#include <winsock2.h>\n#include <ws2tcpip.h>\n\n#define PORT \"44000\" /* Port to listen on */\n\n…\n```\n\n首先，我们有自己的选择。这使我们能够访问创建套接字所需的库(这与其他平台不同)。\n\n```cpp\n…\n if ((iResult = WSAStartup(wVersion, &wsaData)) != 0) {\n     printf(\"WSAStartup failed: %d\\n\", iResult);\n     return 1;\n }\n```\n\n跳到主方法，我们从初始化底层库开始。在这种情况下，我们使用的是 WinSock 库。\n\n```cpp\n\n ZeroMemory(&hints, sizeof hints);\n hints.ai_family = AF_INET;\n hints.ai_socktype = SOCK_STREAM;\n if (getaddrinfo(NULL, PORT, &hints, &res) != 0) {\n     perror(\"getaddrinfo\");\n     return 1;\n }\n```\n\n接下来，我们设置套接字的寻址信息。\n\n```cpp\n sock = socket(res->ai_family, res->ai_socktype, res->ai_protocol);\n if (sock == INVALID_SOCKET) {\n     perror(\"socket\");\n     WSACleanup();\n     return 1;\n }\n```\n\n然后我们创建我们的套接字，传递我们在寻址阶段创建的元素。\n\n```cpp\n    /* Enable the socket to reuse the address */\n    if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (const char *)&reuseaddr,\n        sizeof(int)) == SOCKET_ERROR) {\n        perror(\"setsockopt\");\n        WSACleanup();\n        return 1;\n    }\n```\n\n在我们创建了套接字之后，最好设置我们的套接字，以便能够重用我们在 closer 或 reset 上定义的地址。\n\n```cpp\n    if (bind(sock, res->ai_addr, res->ai_addrlen) == SOCKET_ERROR) {\n        perror(\"bind\");\n        WSACleanup();\n        return 1;\n    }\n    if (listen(sock, 1) == SOCKET_ERROR) {\n        perror(\"listen\");\n        WSACleanup();\n        return 1;\n    }\n```\n\n现在我们可以绑定我们的地址，并最终监听连接。\n\n```cpp\n…\n    while(1) {\n        size_t size = sizeof(struct sockaddr);\n        struct sockaddr_in their_addr;\n        SOCKET newsock;\n        ZeroMemory(&their_addr, sizeof (struct sockaddr));\n        newsock = accept(sock, (struct sockaddr*)&their_addr, &size);\n        if (newsock == INVALID_SOCKET) {\n            perror(\"accept\\n\");\n        }\n        else {\n            printf(\"Got a connection from %s on port %d\\n\",\n                inet_ntoa(their_addr.sin_addr), ntohs(their_addr.sin_port));\n …\n        }\n    }\n```\n\n在我们的主循环中，我们检查新的连接，在收到一个有效的连接时，我们向控制台打印一个简单的调试消息。\n\n```cpp\n    /* Clean up */\n    closesocket(sock);\n    WSACleanup();\n    return 0;\n}\n```\n\n最后，我们要自己清理。我们关闭套接字，调用`WSACleanup`函数初始化清理 WinSock 库。\n\n就这样。我们现在有了一个简单的服务器，它将在我们指定的端口上侦听传入的连接，在本例中为`44000`。\n\n# 苹果电脑\n\n对于 macOS(和其他基于*nix 的操作系统)来说，这个过程与 Windows 示例非常相似，但是，我们需要使用不同的库来帮助支持我们。\n\n```cpp\n#include <stdio.h>\n#include <string.h> /* memset() */\n#include <sys/socket.h>\n#include <netinet/in.h>\n#include <arpa/inet.h>\n#include <unistd.h>\n#include <netdb.h>\n#define PORT    \"44000\"\n…\n```\n\n首先，我们有 include，这里我们使用系统套接字，它在*nix 系统上基于 BSD 实现。\n\n```cpp\nint main(void)\n{\n    int sock;\n    struct addrinfo hints, *res;\n    int reuseaddr = 1; /* True */\n    /* Get the address info */\n    memset(&hints, 0, sizeof hints);\n    hints.ai_family = AF_INET;\n    hints.ai_socktype = SOCK_STREAM;\n    if (getaddrinfo(NULL, PORT, &hints, &res) != 0) {\n        perror(\"getaddrinfo\");\n        return 1;\n    }\n```\n\n在我们的主要功能中，我们从设置寻址信息开始。\n\n```cpp\n    /* Create the socket */\n    sock = socket(res->ai_family, res->ai_socktype, res->ai_protocol);\n    if (sock == -1) {\n        perror(\"socket\");\n        return 1;\n    }\n```\n\n然后我们创建我们的套接字，传递我们在寻址阶段创建的元素。\n\n```cpp\n    /* Enable the socket to reuse the address */\n    if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &reuseaddr, sizeof(int)) == -1) {\n        perror(\"setsockopt\");\n        return 1;\n    }\n```\n\n创建套接字后，最好设置套接字，以便能够重用我们在 closer 或 reset 上定义的地址。\n\n```cpp\n    if (bind(sock, res->ai_addr, res->ai_addrlen) == -1) {\n        perror(\"bind\");\n        return 1;\n    }\n\n    if (listen(sock, 1) == -1) {\n        perror(\"listen\");\n        return 1;\n    }\n```\n\n现在我们可以绑定我们的地址，并最终监听连接。\n\n```cpp\n    while (1) {\n        socklen_t size = sizeof(struct sockaddr_in);\n        struct sockaddr_in their_addr;\n        int newsock = accept(sock, (struct sockaddr*)&their_addr, &size);\n        if (newsock == -1) {\n            perror(\"accept\");\n        }\n        else {\n            printf(\"Got a connection from %s on port %d\\n\",\n                    inet_ntoa(their_addr.sin_addr), htons(their_addr.sin_port));\n            handle(newsock);\n        }\n    }\n```\n\n在我们的主循环中，我们检查新的连接，在收到一个有效的连接时，我们向控制台打印一个简单的调试消息。\n\n```cpp\n    close(sock);\n    return 0;\n}\n```\n\n最后，我们要自己清理。在这种情况下，我们只需关闭插座。\n\n就这样。我们现在有了一个简单的服务器，它将在我们指定的端口上侦听传入的连接，在本例中为`44000`。\n\n为了测试我们的例子，我们可以使用一个现有的程序，比如 **putty** 来连接到我们的服务器。或者我们可以创建一个简单的客户，我会留给你一个外卖项目。虽然只是一个简单的服务器，但这为构建您自己的实现创造了一个起点。\n\n# 摘要\n\n在这一章中，我们采取了很大的步骤来理解多人游戏是如何在较低的层次上实现的。您了解了 TCP/IP 堆栈以及用于游戏开发的不同网络拓扑。我们研究了使用 UDP 和 TCP 协议在客户机-服务器设置中传递数据。最后，我们研究了开发人员开始实现多人游戏功能时面临的一些问题。在下一章中，我们将看看如何将我们的游戏带到一个新的领域——虚拟现实。"
  },
  {
    "path": "docs/master-cpp-game-dev/11.md",
    "content": "# 十一、虚拟现实\n\n**虚拟现实** ( **VR** )是这几天游戏开发非常热门的话题。在本章中，我们将了解如何利用 C++ 的强大功能来创建沉浸式虚拟现实体验。需要注意的是，虽然用于示例集成的 SDK 可用于 macOS，但本章中介绍的硬件和示例代码尚未在 macOS 上测试过，也不能保证得到支持。还应该注意的是，您将需要一个虚拟现实耳机和一个功能强大的电脑和显卡来运行本章的结束示例。建议您使用与英特尔 i5-4590 或 AMD FX 8350 相匹配或超越的中央处理器，以及与英伟达 GeForce GTX 960 或 AMD 镭龙 R9 290 相匹配或超越的图形处理器。在本章中，我们将涵盖以下主题:\n\n*   当前虚拟现实硬件\n*   虚拟现实渲染概念\n*   SDKs 耳机\n*   实施虚拟现实支持\n\n# 快速虚拟现实概述\n\n虚拟现实是一种计算机技术，它使用各种形式的硬件，通过使用逼真的图像、声音和其他感觉，在重建或想象的环境中生成用户身体存在的模拟。处于虚拟现实环境中的用户能够环视人工世界，并且随着虚拟现实技术的新进步，在虚拟世界中移动并与虚拟项目或对象交互。虽然虚拟现实技术可以追溯到 20 世纪 50 年代，但随着计算机图形、处理和电源的最新发展，虚拟现实已经复苏。脸书、索尼、谷歌和微软等知名科技巨头在虚拟和增强现实技术上下了大赌注。自从鼠标发明以来，用户与计算机交互的方式还没有如此大的创新潜力。虚拟现实的用例不仅仅是游戏开发。许多其他领域都将虚拟现实技术视为扩展自己独特互动的一种方式。医疗保健、教育、培训、工程、社会科学、市场营销，当然还有电影和娱乐，都为拥有本书和游戏开发中所学技能的开发人员提供了充满希望的机会。我经常建议寻求改变节奏或新挑战的游戏开发人员将新兴的虚拟现实开发场景作为他们知识和技能库的替代用途。\n\n# 当前虚拟现实硬件\n\n作为开发者，我们正处于一个非常幸运的 VR 硬件发展时期。说到虚拟现实硬件，有许多不同的选择，包括投影系统，如**洞穴自动虚拟环境** ( **洞穴**)、**头戴式显示器** ( **头盔显示器**)，甚至基于手机的系统，如谷歌白日梦和纸板。在这里，我们将重点关注沉浸式电脑和控制台驱动的头盔显示器。这些头盔显示器背后的大多数技术非常相似。这里列出的每个头盔显示器在运动、3D 空间头部跟踪方面至少有 **s** **ix 自由度** ( **6DOF** )，有的甚至有基本的空间感知，常被称为*房间感*。这些耳机的开发在很大程度上可以用相同的方式进行，但是对这些不同的设备有一个大致的了解是很好的。接下来，我们将快速了解一下目前可供消费者使用的一些最常见的耳机。\n\n# Oculus Rift CV1\n\nOculus Rift 最初是一个由人群资助的项目，现已成为目前最受欢迎的耳机之一。Oculus 裂谷经历了几次迭代。第一个和第二个硬件版本面向开发者(DK1 和 DK2)。在脸书收购 Oculus 初创公司后，这家社交媒体巨头发布了第一款商用版硬件，名为**消费版 1** ( **CV1** )。虽然支持在**蒸汽**游戏平台，Oculus 是非常绑在自己的发射器和软件平台。耳机目前仅支持 PC 开发:\n\n![](img/233cdd18-da4f-49ba-aaf2-df46b2c88dad.png)\n\n以下是 Oculus Rift CV1 的特点:\n\n*   **屏幕类型** : AMOLED\n*   **分辨率**:每只眼睛 1080 x 1200\n*   **视野** : ~110 <sup>0</sup>\n*   **头部跟踪** : IMU(指南针、加速度计、陀螺仪)、IR 光学跟踪\n\n推荐的最低电脑规格如下:\n\n*   **GPU** : NVIDIA GeForce GTX 970 或 AMD 镭龙 R9 290\n*   **CPU** :英特尔 i5-4590 或 AMD FX 8350\n*   **RAM** : 8 GB\n*   **OS** : Windows 7\n\n# HTC 还活着\n\n可以说是目前最受欢迎的耳机，HTC Vive 是由 HTC(一家智能手机和平板电脑制造商)和 Valve corporation(一家游戏公司，以蒸汽游戏平台而闻名)创建的。通常直接与 Oculus Rift 相比，HTC Vive 在设计上确实有许多相似之处，只是略有不同，在许多开发人员看来，这使得 HTC Vive 成为了一款出色的硬件:\n\n![](img/7c31b745-88c6-4288-b02e-d2871d5f1be2.jpg)\n\n![](img/520be352-da41-4fa4-9afe-90559bc7d79b.jpg)\n\n以下是 HTC Vive 的特点:\n\n*   **屏幕类型** : AMOLED\n*   **分辨率**:每只眼睛 1080 x 1200\n*   **视野** : 110 <sup>0</sup>\n*   **头部跟踪** : IMU(罗盘、加速度计、陀螺仪)、2 个 IR 基站\n\n推荐的最低电脑规格如下:\n\n*   **GPU** : NVIDIA GeForce GTX 970 或 AMD 镭龙 R9 290\n*   **CPU** :英特尔 i5-4590 或 AMD FX 8350\n*   **RAM** : 4 GB\n*   **OS** : Windows 7，Linux\n\n# 开源虚拟现实开发工具包\n\n另一个非常有趣的硬件选项是 OSVR 套件，由 Razer 和 Sensics 开发。OSVR 的独特之处在于，它是一个开放许可、非专有的硬件平台和生态系统。这给了开发者在设计他们的增强现实/虚拟现实体验时很大的自由。OSVR 也是一个软件框架，我们将在稍后介绍。与硬件一样，该框架是开放许可的，旨在跨平台:\n\n![](img/1155357a-8f9e-40d5-af16-a4ce34315a52.jpg)\n\n以下是 OSVR 的特性:\n\n*   **屏幕类型** : AMOLED\n*   **分辨率**:每只眼睛 960 x 1080\n*   **视野** : 100 <sup>0</sup>\n*   **头部跟踪** : IMU(指南针、加速度计、陀螺仪)、IR 光学跟踪\n\n推荐的最低电脑规格如下:\n\n*   **GPU** : NVIDIA GeForce GTX 970 或 AMD 镭龙 R9 290\n*   **CPU** :英特尔 i5-4590 或 AMD FX 8350\n*   **RAM** : 4 GB\n*   **OS** :跨平台支持\n\n# 索尼游戏机 VR\n\n索尼 PlayStation VR 最初被称为**Mopheus 项目**，是索尼公司进入虚拟现实领域的入口。与此列表中的其他耳机不同，索尼 PlayStation VR 耳机不是由 PC 驱动，而是连接到索尼 PlayStation 4 游戏控制台。虽然不是最高保真度或最先进的技术，但通过使用 PS4 作为平台，索尼 PlayStation VR 耳机拥有 3000 多万的控制台基础:\n\n![](img/38bde5a4-a6e8-407b-b44f-30801ab071cf.jpg)\n\n以下是索尼 PlayStation VR 的功能:\n\n*   **屏幕类型** : AMOLED\n*   **分辨率**:每只眼睛 960 x 1080\n*   **视野** : ~100 <sup>0</sup>\n*   **头部跟踪** : IMU(指南针、加速度计、陀螺仪)、IR 光学跟踪\n*   **控制台硬件**:索尼 PlayStation 4\n\n# 视窗混合现实耳机\n\n虚拟现实硬件领域的最新条目之一是支持视窗混合现实的耳机组。虽然不是单一的耳机设计，但视窗混合现实有一套规范和软件支持，可以从视窗 10 桌面实现虚拟现实。被称为**混合现实** ( **MR** )，这些耳机的独特之处在于其内置的空间感知或房间感。其他耳机，如 Oculus Rift 和 HTC Vive，也支持类似的功能，但与 Windows MR 设备不同，它们需要额外的硬件来支持跟踪。这种额外硬件的缺乏意味着 Windows MR 头戴式耳机应该更容易设置，并有可能使电脑驱动的虚拟现实体验更加便携:\n\n![](img/7b8ab8b2-99a6-4c0a-a83a-8a28695a7fb8.jpg)\n\n以下是视窗磁流变耳机的特点:\n\n*   **屏幕类型**:各种\n*   **分辨率**:各种\n*   **视野**:各种\n*   **头部跟踪**:基于耳机的 9 自由度内外跟踪系统\n\n推荐的最低电脑规格如下:\n\n*   **GPU** : NVIDIA GeForce GTX 960、AMD 镭龙 RX 460 或集成英特尔高清显卡 620\n*   **CPU** :英特尔 i5-4590 或 AMD FX 8350\n*   **RAM** : 8 GB\n*   **OS** : Windows 10\n\n# 虚拟现实渲染概念\n\n从渲染的角度来看虚拟现实，很快就会发现虚拟现实带来了一些独特的挑战。部分原因是需要达到一些必要的性能基准，以及渲染硬件目前的局限性。渲染 VR 内容时，需要以比标准高清更高的分辨率进行渲染，通常是两倍或更多。渲染也需要非常快，以每只眼睛 90 fps 或更高的帧速率为基准。这与抗锯齿和采样技术的使用相结合，意味着渲染一个虚拟现实场景需要的计算能力是以 1080p 和 60 fps 运行的标准游戏的五倍以上。在接下来的几节中，我们将讨论渲染虚拟现实内容时的一些主要差异，并涉及一些您可以实现以保持性能的概念。\n\n# 与挫折一起工作\n\n当开发一个虚拟现实就绪引擎时，最大的区别是理解如何在处理多个视点时构建一个合适的、裁剪的视图平截头体。在一个典型的非虚拟现实游戏中，你只有一个视点(摄像机)，你可以从这个视点创建一个视图平截头体。如果你需要一个完整的复习，请参考本书前面的内容，但是这个视图截锥决定了什么将被呈现并最终显示在屏幕上给用户。以下是描绘典型视平截头体的示意图:\n\n![](img/09ce5ebe-8a4b-42a3-9907-400cfc948b74.png)\n\n在虚拟现实中渲染时，每只眼睛至少有一个平截头体，通常以立体方式显示到单个 HMD，这意味着在单个屏幕上显示一对图像，从而产生深度错觉。这些图像通常描绘场景的左眼和右眼视图。这意味着我们必须考虑两个*眼睛*截头体的位置，并通过组合它们来产生用于渲染的最终视图截头体。下面是这些视图墩的图示:\n\n![](img/7feba28c-0c2e-47e8-8980-0bd056436155.png)\n\n当涉及到创建一个结合了左眼和右眼截头体的单个截头体时，实际上是相当容易的。如下图所示，您需要将新平截头体的顶点放在两只眼睛之间，并稍微靠后。然后移动近裁剪平面位置，使其与任意一个眼平截头体的裁剪平面对齐。这对于最终显示**平截头体剔除**很重要:\n\n![](img/820bfde5-6d8c-4ea7-9574-32035e768475.png)\n\n您可以使用**瞳孔间距** ( **IPD** )通过一些简单的数学运算来计算这个平截头体，正如 Oculus Rift 团队的 Cass Everitt 在下图中完美展示的那样:\n\n![](img/e8cbe7fd-05cc-48cb-862d-f9af0cf32b60.png)\n\n我们也可以通过简单地剔除共享的眼锥顶面和底面来简化这个过程。虽然在技术上没有形成完美的平截头体，但使用剔除算法在给定时间测试单个平面将产生所需的效果。\n\n好消息是，这些大部分都可以抽象出来，在许多耳机 SDK 中都有方法可以帮助你。然而，重要的是要理解与标准的非虚拟现实场景渲染相比，如何使用截头体的区别。\n\n# 提高渲染性能\n\n当使用单个相机和视点时，就像大多数非虚拟现实游戏一样，我们可以简单地将渲染过程视为引擎中的一个步骤。当使用多个视点时，这是不同的。当然，我们可以将每个视点视为一个渲染任务，一个接一个地处理，但是这会导致渲染速度变慢。\n\n如前一节所示，每只*眼睛*看到的东西有相当大的重叠。这为我们提供了通过共享和重用数据来优化渲染过程的绝佳机会。为此，我们可以实现**数据上下文**的概念。利用这个概念，我们可以分类哪些元素对于单眼来说是唯一存在的，哪些元素是可以共享的。让我们看看这些数据上下文，以及我们如何使用它们来加快渲染速度:\n\n*   **框架上下文**:简单来说，框架上下文用于任何需要渲染的元素，与视图方向无关。这将包括诸如天空盒子、全局反射、水纹理等元素。任何可以跨视点共享的东西都可以放在这个上下文中。\n*   **眼睛上下文**:这是视点之间不能共享的元素的上下文。这将包括渲染时需要立体视差的任何元素。也是在这种情况下，我们可以存储用于着色器计算的每只眼睛的数据。\n\n使用这种将数据分成不同上下文的简单方法，我们可以重新组织渲染过程，如下所示:\n\n```cpp\nRenderScene(Frame f)\n{\n  ProcessFrame(f); //Handle any needed globally shared calculations\n  RenderFrame(f); //Render any globally shared elements\n  for(int i=0; i<numview points; i++) //numview points would be 2 for                            stereo\n    {\n      ProcessEye(i, f); //Handle any per eye needed calculations\n      RenderEye(i, f); //Render any per eye elements\n    }\n}\n```\n\n虽然这表面上看起来是基本的，但它是一个非常强大的概念。通过以这种方式分离渲染，并尽可能地共享，我们大大提高了渲染器的整体性能。这是最简单的优化之一，也是回报最大的优化之一。我们还可以将这一点延续到如何设置着色器制服，将它们分解为上下文片段:\n\n```cpp\nlayout (binding = 0) uniform FrameContext\n{\n  Mat4x4 location; //modelview\n  Mat4x4 projection;\n  Vec3 viewerPosition;\n  Vec3 position;\n}frame;\nlayout (binding = 1) uniform EyeContext\n{\n  Mat4x4 location; //modelview\n  Mat4x4 projection;\n  Vec3 position;\n}eye;\n```\n\n从概念的角度来看，这种数据划分非常有效，并且这些数据片段中的每一个都可以在不同的时间更新，从而提供更好的整体性能。\n\n这基本上从高层次上描述了处理虚拟现实中多个视点渲染的高效方式。如前所述，在正在开发的软件开发工具包中，与连接硬件和管道相关的大量设置被抽象出来。在下一节中，我们将研究其中的一些 SDK，并通过研究 SDK 在示例引擎中的实现来结束这一章。\n\n# SDKs 耳机\n\n有许多 SDK 可用于实现各种耳机和支持硬件，大多数制造商以某种形式提供自己的 SDK。在接下来的章节中，我们将快速了解开发个人电脑驱动的 HMD 虚拟现实体验时最常用的三个软件开发工具包:\n\n*   **Oculus PC SDK**([https://developer . Oculus . com/downloads/package/Oculus-SDK-for-windows/](https://developer.oculus.com/downloads/package/oculus-sdk-for-windows/)):这个 SDK 是专门为用 C++ 开发 Oculus Rift HMD 体验和游戏时使用而创建的。核心软件开发工具包提供了开发人员访问渲染、跟踪、输入和其他核心硬件功能所需的一切。核心 SDK 由其他支持音频、平台和头像的 SDK 升华而来。\n*   **OpenVR**([https://github.com/ValveSoftware/openvr](https://github.com/ValveSoftware/openvr)):这是 Valve 公司提供的 SDK，作为 SteamVR 平台的默认 API 和运行时。这也是 HTC Vive HMD 开发的默认 SDK，但旨在拥有多个供应商的支持。这意味着你有能力瞄准多个头盔显示器，而不必确切知道哪个 HMD 与之相连。这将是我们为示例引擎实现的 SDK。\n*   **OSVR**([http://osvr.github.io/](http://osvr.github.io/)):OSVR SDK，就像它的名字所说的那样，是一个开源 SDK，旨在与多个硬件供应商合作。这个软件开发工具包是同名 HMD 的默认软件开发工具包，即 OSVR 耳机。该项目由 Razer 和 Sensics 牵头，许多大型游戏合作伙伴都加入了进来。OSVR SDK 可用于微软视窗、Linux、安卓和苹果操作系统。\n\n# 实施虚拟现实支持\n\n正如我们在整本书中看到的许多其他系统一样，从头开始实现虚拟现实支持可能是一个非常具有挑战性和耗时的过程。然而，很像那些其他系统，库和 SDK 的存在有助于简化和简化这个过程。在下一节中，我们将介绍如何使用 Valve 公司提供的 OpenVR SDK 向示例引擎添加 VR 渲染支持。我们将只讨论全部要点。要查看每种方法的更完整概述，请参考示例代码中的注释，并访问 OpenVR SDK Wiki 了解更多特定于 SDK 的信息([https://github.com/ValveSoftware/openvr/wiki](https://github.com/ValveSoftware/openvr/wiki))。\n\n# 核实 HMD\n\n首先，我们需要做一些事情来设置我们的硬件和环境。我们需要首先测试电脑是否连接了耳机。然后我们需要检查 OpenVR 运行时是否已经安装。然后我们可以初始化硬件，最后问它几个关于其性能的问题。为此，我们将向我们的`GameplayScreen`类添加一些代码；为了简洁起见，我们将跳过一些部分。完整的代码可以在代码库的`Chapter11`文件夹中的示例项目中找到。\n\n让我们从检查是否有虚拟现实耳机连接到计算机开始，以及是否安装了 OpenVR(StemVR)运行时。为此，我们将在`Build()`方法中添加以下内容:\n\n```cpp\nvoid GameplayScreen::Build()\n{\n  if (!vr::VR_IsHmdPresent())\n   {\n      throw BookEngine::Exception(\"No HMD attached to the system\");\n   }\n  if (!vr::VR_IsRuntimeInstalled())\n   {\n      throw BookEngine::Exception(\"OpenVR Runtime not found\");\n   }\n}\n```\n\n在这里，我们抛出一个异常，如果这些检查中的任何一个失败，我们将处理并记录该异常。现在我们知道我们有一些硬件和所需的软件，我们可以初始化框架。为此，我们调用`InitVR`函数:\n\n```cpp\nInitVR();\n```\n\n`InitVR`函数的主要目的是依次调用 OpenVR SDK 的`VR_Init`方法。为此，它需要首先创建并设置一个错误处理程序。它还要求我们定义这将是什么类型的应用。在我们的案例中，我们声明这将是一个场景应用，`vr::VRApplication_Scene`。这意味着我们正在创建一个将绘制环境的 3D 应用。还有其他选项，如创建实用程序或仅覆盖应用。最后，一旦我们初始化了 HMD，没有错误，我们要求耳机告诉我们一些关于它自己的信息。我们使用`GetTrackedDeviceString`方法来实现这一点，我们将在稍后介绍。整个`InitVR`方法看起来如下:\n\n```cpp\nvoid GameplayScreen::InitVR()\n{\n   vr::EVRInitError err = vr::VRInitError_None;\n   m_hmd = vr::VR_Init(&err, vr::VRApplication_Scene);\n   if (err != vr::VRInitError_None)\n   {\n     HandleVRError(err);\n   }\n   std::cout << GetTrackedDeviceString(m_hmd,\n   vr::k_unTrackedDeviceIndex_Hmd,vr::Prop_TrackingSystemName_String)\n   << std::endl;\n   std::clog << GetTrackedDeviceString(m_hmd,                  vr::k_unTrackedDeviceIndex_Hmd, vr::Prop_SerialNumber_String)<<        std::endl;\n}\n```\n\n`HandleVRError`方法只是一个简单的助手方法，它接受传入的错误并抛出一个要处理和记录的错误，同时提供所抛出错误的英文翻译。以下是整个方法:\n\n```cpp\nvoid GameplayScreen::HandleVRError(vr::EVRInitError err)\n{\n  throw BookEngine::Exception(vr::VR_GetVRInitErrorAsEnglishDescription(err));\n}\n```\n\n`InitVR`函数调用的另一个方法是`GetTrackedDeviceString`函数。这是作为 OpenVR 示例代码的一部分提供的一个函数，它允许我们返回一些关于附加设备的信息。在我们的案例中，我们要求连接设备的系统名称和序列号属性(如果有):\n\n```cpp\nstd::string GameplayScreen::GetTrackedDeviceString(vr::IVRSystem * pHmd, vr::TrackedDeviceIndex_t unDevice, vr::TrackedDeviceProperty prop, vr::TrackedPropertyError * peError)\n{\n  uint32_t unRequiredBufferLen = pHmd-                  >GetStringTrackedDeviceProperty(unDevice, prop, NULL, 0, peError);\n    if (unRequiredBufferLen == 0)\n      return \"\";\n\n   char *pchBuffer = new char[unRequiredBufferLen];\n   unRequiredBufferLen = pHmd->GetStringTrackedDeviceProperty(unDevice,   prop, pchBuffer, unRequiredBufferLen, peError);\n   std::string sResult = pchBuffer;\n   delete[] pchBuffer;\n   return sResult;\n}\n```\n\n最后，回到我们的`Build`方法，现在我们已经完成了初始化步骤，我们可以通过询问系统`VRCompositor`函数是否被设置为非空值来检查是否一切正常。如果是，这意味着一切都准备好了，然后我们可以询问我们的 HMD 希望我们的渲染目标大小是多少，并在我们的控制台窗口中显示为字符串输出:\n\n```cpp\nif (!vr::VRCompositor())\n {\n   throw BookEngine::Exception(\"Unable to initialize VR compositor!\\n \");\n }\nm_hmd->GetRecommendedRenderTargetSize(&m_VRWidth, &m_VRHeight);\n\nstd::cout << \"Initialized HMD with suggested render target size : \" << m_VRWidth << \"x\" << m_VRHeight << std::endl;\n}\n```\n\n我们需要做的最后一件事是确保我们在程序完成时清理干净。这里，在`GamplayScreen`的`Destroy`方法中，我们首先检查 HMD 是否被初始化；如果是，我们调用`VR_Shutdown`方法并将`m_hmd`变量设置为空。在应用关闭时调用`VR_Shutdown`是非常重要的，如果您没有这样做，OpenVR/StemVR 可能会挂起，并可能需要重新启动才能再次运行:\n\n```cpp\nvoid GameplayScreen::Destroy()\n{\n   if (m_hmd)\n    {\n       vr::VR_Shutdown();\n       m_hmd = NULL;\n    }\n}\n```\n\n现在，如果我们继续运行这个示例，在控制台窗口中，您应该会看到类似如下的内容:\n\n**![](img/3587d745-2294-4081-b48f-3a57744be739.png)** \n\n# 翻译\n\n现在，我们已经建立了 HMD 和我们的引擎交谈，下一步是渲染它。过程其实没那么复杂；如前所述，SDK 为我们处理了大量事务。为了使事情尽可能简单，这个例子只是一个简单的渲染例子。我们不处理头部跟踪或输入，我们只是简单地在每只眼睛中显示不同的颜色。和前面的例子一样，为了节省时间和空间，我们只打算覆盖重要的元素，让你掌握概念。完整的代码可以在代码库的`Chapter11`文件夹中的示例项目中找到。\n\n正如我们之前所讨论的，在立体渲染时，您通常会渲染一个已经分成两半的显示器。然后，我们将适当的数据传递给另一半，这取决于在那只眼睛中可以看到什么。回顾*与截头体*一起工作部分，了解为什么会这样。归根结底，我们需要为每只眼睛创建一个帧缓冲区。为此，我们有一个`RenderTarget`类来创建帧缓冲区，附加纹理，最后创建所需的视口(它是总显示宽度的一半)。为了节省空间，我不会打印出`RenderTarget`类；它相当简单明了，我们以前从未见过。相反，让我们继续设置和处理 HMD 场景显示的实际功能。首先，我们需要将我们的`RenderTarget`连接到我们的纹理，为了正确的实现，清除并设置缓冲区。为此，我们在`GameplayScreen`的`OnEntry`方法中添加了以下内容:\n\n```cpp\nBasicRenderTarget leftRT(1, vrApp.rtWidth, vrApp.rtHeight);\nBasicRenderTarget rightRT(1, vrApp.rtWidth, vrApp.rtHeight);\n\nleftRT.Init(leftEyeTexture.name);\nrightRT.Init(rightEyeTexture.name);\n\nglClearColor(1.0f, 0.0f, 0.0f, 1.0f);\nleftRT.fbo.Bind(GL_FRAMEBUFFER);\nglClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);\n\nif (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)\n  {\n    throw std::runtime_error(\"left rt incomplete\");\n  }\nglClearColor(0.0f, 1.0f, 0.0f, 1.0f);\nrightRT.fbo.Bind(GL_FRAMEBUFFER);\nglClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);\nif (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)\n  {\n    throw std::runtime_error(\"right rt incomplete\");\n  }\nglBindFramebuffer(GL_FRAMEBUFFER, 0);\n\nglClearColor (0.0f, 0.0f, 1.0f, 1.0f);\n```\n\n我不会一行一行地重复前面的代码，因为我们之前已经看到了所有这些。现在，随着我们的缓冲区和纹理设置，我们可以继续添加绘图调用。\n\nOpenVR SDK 提供了处理显示虚拟现实场景的复杂片段所需的方法。这项复杂工作的大部分是由排字系统完成的。如 Valve 所述，*“合成器通过处理失真、预测、同步和其他微妙的问题，简化了向用户显示图像的过程，这些问题对于获得稳定的虚拟现实体验来说是一个挑战。”*\n\n为了连接到合成器子系统，我们创建了一个名为`SubmitFrames`的简单方法。这个方法有三个参数——每只眼睛的纹理和一个布尔值来指定颜色空间是否应该是`linear`。在写的时候，我们总想指定颜色空间应该是`Gamma`为`OpenGL`。在该方法中，我们获得想要渲染的设备，设置颜色空间，转换纹理，然后将这些纹理提交给`VRCompositor`，然后在引擎盖下，处理纹理显示给正确的眼睛。整个方法如下所示:\n\n```cpp\nvoid GameplayScreen::SubmitFrames(GLint leftEyeTex, GLint rightEyeTex, bool linear = false)\n{\n if (!m_hmd)\n  {\n    throw std::runtime_error(\"Error : presenting frames when VR system handle is NULL\");\n  }\n  vr::TrackedDevicePose_t trackedDevicePose[vr::k_unMaxTrackedDeviceCount];\n  vr::VRCompositor()->WaitGetPoses(trackedDevicePose,        vr::k_unMaxTrackedDeviceCount, nullptr, 0);\n\n  vr::EColorSpace colorSpace = linear ? vr::ColorSpace_Linear :    vr::ColorSpace_Gamma;\n\n  vr::Texture_t leftEyeTexture = { (void*)leftEyeTex,    vr::TextureType_OpenGL, colorSpace };\n  vr::Texture_t rightEyeTexture = { (void*)rightEyeTex,   vr::TextureType_OpenGL, colorSpace };\n\n  vr::VRCompositor()->Submit(vr::Eye_Left, &leftEyeTexture);\n  vr::VRCompositor()->Submit(vr::Eye_Right, &rightEyeTexture);\n\n  vr::VRCompositor()->PostPresentHandoff();\n}\n```\n\n有了我们的`SubmitFrames`函数，我们就可以在`glClear`函数调用之后调用游戏屏幕更新中的方法表单:\n\n```cpp\n…\nglClear(GL_COLOR_BUFFER_BIT);\nSubmitFrames(leftEyeTexture.id, rightEyeTexture.id);\n```\n\n如果您现在运行示例项目，假设您安装了必要的 SteamVR 框架，您应该会看到耳机的每只眼睛都显示不同的颜色。\n\n# 摘要\n\n虽然这是对虚拟现实开发世界的快速介绍，但它应该为您的体验想法提供一个很好的测试平台。我们学习了如何处理多视图截头体，了解了各种硬件选项，最后研究了如何使用 OpenVR SDK 向示例引擎添加 VR 支持。随着硬件的进步，虚拟现实将继续获得势头，并将继续推进到新的领域。了解 VR 渲染作为一个整体是如何工作的，可以为您的开发知识库提供新的深度。"
  },
  {
    "path": "docs/master-cpp-game-dev/README.md",
    "content": "# 精通 C++ 游戏开发\n\n> 原书：[Mastering C++ Game Development](https://libgen.rs/book/index.php?md5=C9DEE6A3AC368562ED493911597C48C0)\n> \n> 协议：[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)\n> \n> 阶段：机翻（1）\n> \n> 自豪地采用[谷歌翻译](https://translate.google.cn/)\n> \n> 让开源界提前感受入关。\n\n* [在线阅读](https://ccpp.apachecn.org)\n* [在线阅读（Gitee）](https://apachecn.gitee.io/apachecn-c-cpp-zh/)\n* [ApacheCN 学习资源](http://docs.apachecn.org/)\n\n## 贡献指南\n\n本项目需要校对，欢迎大家提交 Pull Request。\n\n> 请您勇敢地去翻译和改进翻译。虽然我们追求卓越，但我们并不要求您做到十全十美，因此请不要担心因为翻译上犯错——在大部分情况下，我们的服务器已经记录所有的翻译，因此您不必担心会因为您的失误遭到无法挽回的破坏。（改编自维基百科）\n\n## 联系方式\n\n### 负责人\n\n* [飞龙](https://github.com/wizardforcel): 562826179\n\n### 其他\n\n*   在我们的 [apachecn/apachecn-c-cpp-zh](https://github.com/apachecn/apachecn-c-cpp-zh) github 上提 issue.\n*   发邮件到 Email: `apachecn@163.com`.\n*   在我们的 [组织学习交流群](http://www.apachecn.org/organization/348.html) 中联系群主/管理员即可.\n\n## 赞助我们\n\n![](http://data.apachecn.org/img/about/donate.jpg)\n"
  },
  {
    "path": "docs/master-cpp-game-dev/SUMMARY.md",
    "content": "+   [精通 C++ 游戏开发](README.md)\n+   [零、前言](00.md)\n+   [一、面向游戏开发的 C++ 语言](01.md)\n+   [二、理解库](02.md)\n+   [三、夯实基础](03.md)\n+   [四、构建素材管道](04.md)\n+   [五、构建游戏系统](05.md)\n+   [六、创建图形用户界面](06.md)\n+   [七、高级渲染](07.md)\n+   [八、高级游戏系统](08.md)\n+   [九、人工智能](09.md)\n+   [十、多个玩家](10.md)\n+   [十一、虚拟现实](11.md)\n"
  },
  {
    "path": "docs/master-cpp-multithrd/00.md",
    "content": "# 零、前言\n\n多线程应用在单处理器环境中执行多个线程，以实现。这本书充满了实际的例子，将帮助你成为用 C++ 编写健壮的并发和并行应用的大师。在本书中，您将深入研究多线程和并发的基础，并了解如何实现它们。在此过程中，您将探索原子操作来优化代码性能，并将并发性应用于分布式计算和 GPU 处理。\n\n# 这本书涵盖了什么\n\n[第 1 章](01.html)、*重访多线程*，总结了 C++ 中的多线程，重访了所有你应该已经熟悉的概念，并通过一个使用 c++ 2011 修订版中添加的本机线程支持的多线程基本示例。\n\n[第 2 章](02.html)、*处理器和操作系统上的多线程实现*建立在上一章讨论的硬件实现所提供的基础之上，展示了操作系统是如何利用这些功能并使其对应用可用的。它还讨论了如何允许进程和线程使用内存和处理器，以防止应用和线程相互干扰。\n\n[第 3 章](03.html)、 *C++ 多线程应用编程接口*，探讨了作为操作系统级应用编程接口(例如 Win32 和 POSIX)或由框架(例如 Boost、Qt 和 POCO)提供的各种多线程应用编程接口。它简要介绍了每个应用编程接口，列出了与其他应用编程接口相比的不同之处，以及它对您的应用的优缺点。\n\n[第 4 章](04.html)、*线程同步和通信*采用了前几章中学习的主题，并探索了一种使用 C++ 14 的本机线程 API 实现的高级多线程实现，该 API 允许多个线程在没有任何线程安全问题的情况下进行通信。它还涵盖了许多类型的同步机制之间的差异，包括互斥、锁和条件变量。\n\n[第 5 章](05.html)、*原生 C++ 线程和原语*，包括线程、并发、本地存储以及该 API 支持的线程安全。在前一章的示例基础上，讨论并探索了如何使用 C++ 11 和 C++ 14 中的完整功能集提供的功能来扩展和优化线程安全性。\n\n[第 6 章](06.html)、*调试多线程代码*，教你如何使用 Valgrind (Memcheck、DRD、Helgrind 等)等工具分析应用的多线程性能，发现热点，解决或防止并发访问带来的问题。\n\n[第 7 章](07.html)、*最佳实践、*涵盖了常见的陷阱和陷阱，以及如何在它们回来困扰你之前发现它们。它还通过示例探索了许多常见和不太常见的场景。\n\n[第 8 章](08.html)、*原子操作–与硬件*一起工作，详细介绍了原子操作:它们是什么以及如何最好地使用它们。编译器支持是跨 CPU 体系结构来考虑的，并且会评估何时值得在代码中投入时间来实现原子操作。它还研究了这种优化如何限制代码的可移植性。\n\n[第 9 章](09.html)、*分布式计算多线程*，吸取了前面章节中的许多经验教训，并将其应用于多系统、集群级别的规模。使用一个基于 OpenMPI 的例子，它展示了多线程是如何跨多个系统完成的，例如计算机集群中的节点。\n\n[第 10 章](10.html)、*GPU 多线程*，展示了 GPU 应用中多线程的使用(例如 CUDA 和 OpenCL)。使用基于 OpenCL 的示例，探索了一个可以并行执行任务的基本多线程应用。本章吸取了前几章中的经验教训，并将其应用于显卡和衍生硬件(例如，机架式矢量处理器硬件)上的处理。\n\n# 这本书你需要什么\n\n要遵循本书中的说明，您需要在系统上安装任何操作系统(Windows、Linux 或 macOS)和任何 C++ 编译器。\n\n# 这本书是给谁的\n\n这本书是为希望扩展多线程和并发处理知识的中级 C++ 开发人员编写的。您应该对多线程有基本的经验，并且能够在命令行上使用 C++ 开发工具链。\n\n# 约定\n\n在这本书里，你会发现许多区分不同种类信息的文本样式。以下是这些风格的一些例子和对它们的意义的解释。\n\n文本中的码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、伪 URL、用户输入和 Twitter 句柄如下所示:`randGen()`方法采用两个参数，定义返回值的范围\n\n代码块设置如下:\n\n```cpp\ncout_mtx.lock();\n cout << \"Thread \" << tid << \" adding \" << rval << \". New value: \" << val << \".\\n\";\n cout_mtx.unlock();\n\n values_mtx.lock();\n values.push_back(val);\n values_mtx.unlock();\n}\n\n```\n\n当我们希望将您的注意力吸引到代码块的特定部分时，相关的行或项目以粗体显示:\n\n```cpp\ncout_mtx.lock();\n cout << \"Thread \" << tid << \" adding \" << rval << \". New value: \" << val << \".\\n\";\n cout_mtx.unlock();\n\n values_mtx.lock();\n values.push_back(val);\n values_mtx.unlock();\n}\n\n```\n\n任何命令行输入或输出都编写如下:\n\n```cpp\n$ make\ng++ -o ch01_mt_example -std=c++ 11 ch01_mt_example.cpp\n\n```\n\n新术语和重要词汇以粗体显示。您在屏幕上看到的单词(例如，菜单或对话框中的单词)会出现在文本中。\n\nWarnings or important notes appear like this. Tips and tricks appear like this.\n\n# 读者反馈\n\n我们随时欢迎读者的反馈。让我们知道你对这本书的看法——你喜欢或不喜欢什么。读者反馈对我们来说很重要，因为它有助于我们开发出你真正能从中获益的标题。要给我们发送一般反馈，只需发送电子邮件`feedback@packtpub.com`，并在您的邮件主题中提及书名。如果您对某个主题有专业知识，并且对写作或投稿感兴趣，请参见我们位于[www.packtpub.com/authors](http://www.packtpub.com/authors)的作者指南。\n\n# 下载示例代码\n\n你可以从你在[http://www.packtpub.com](http://www.packtpub.com)的账户下载这本书的示例代码文件。如果您在其他地方购买了这本书，您可以访问[http://www.packtpub.com/support](http://www.packtpub.com/support)并注册，以便将文件直接通过电子邮件发送给您。您可以按照以下步骤下载代码文件:\n\n1.  使用您的电子邮件地址和密码登录或注册我们的网站。\n2.  将鼠标指针悬停在顶部的“支持”选项卡上。\n3.  点击代码下载和勘误表。\n4.  在搜索框中输入图书的名称。\n5.  选择要下载代码文件的书籍。\n6.  从您购买这本书的下拉菜单中选择。\n7.  点击代码下载。\n\n下载文件后，请确保使用最新版本的解压缩文件夹:\n\n*   视窗系统的 WinRAR / 7-Zip\n*   zipeg/izp/un ARX for MAC\n*   适用于 Linux 的 7-Zip / PeaZip\n\n这本书的代码包也托管在 GitHub 上[https://GitHub . com/PacktPublishing/Mastering-CPP-多线程](https://github.com/PacktPublishing/Mastering-CPP-Multithreading)。我们还有来自丰富的图书和视频目录的其他代码包，可在[https://github.com/PacktPublishing/](https://github.com/PacktPublishing/)获得。看看他们！\n\n# 正误表\n\n尽管我们尽了最大努力来确保我们内容的准确性，但错误还是会发生。如果你在我们的某本书里发现一个错误，也许是文本或代码中的错误，如果你能向我们报告，我们将不胜感激。通过这样做，你可以让其他读者免受挫折，并帮助我们改进这本书的后续版本。如果您发现任何勘误表，请访问[http://www.packtpub.com/submit-errata](http://www.packtpub.com/submit-errata)，选择您的书籍，点击勘误表提交表格链接，并输入您的勘误表的详细信息。一旦您的勘误表得到验证，您的提交将被接受，勘误表将上传到我们的网站或添加到该标题勘误表部分下的任何现有勘误表列表中。要查看之前提交的勘误表，请前往[https://www.packtpub.com/books/content/support](https://www.packtpub.com/books/content/support)并在搜索栏中输入图书名称。所需信息将出现在勘误表部分。\n\n# 海盗行为\n\n互联网上版权材料的盗版是所有媒体的一个持续问题。在 Packt，我们非常重视版权和许可证的保护。如果您在互联网上遇到任何形式的我们作品的非法拷贝，请立即向我们提供位置地址或网站名称，以便我们寻求补救。请通过`copyright@packtpub.com`联系我们，获取疑似盗版资料的链接。我们感谢您在保护我们的作者方面的帮助，以及我们为您带来有价值内容的能力。\n\n# 问题\n\n如果您对本书的任何方面有问题，可以在`questions@packtpub.com`联系我们，我们将尽最大努力解决问题。"
  },
  {
    "path": "docs/master-cpp-multithrd/01.md",
    "content": "# 一、重温多线程\n\n如果你正在读这本书，你可能已经用 C++ 或者其他语言完成了一些多线程编程。本章旨在从 C++ 的角度来回顾这个主题，介绍一个基本的多线程应用，同时也涵盖了我们将在整本书中使用的工具。本章结束时，您将拥有继续下一章所需的所有知识和信息。\n\n本章涵盖的主题包括:\n\n*   使用本机应用编程接口的 C++ 基本多线程\n*   编写基本的 makefiles 和使用 GCC/MinGW\n*   使用`make`编译程序并在命令行上执行\n\n# 入门指南\n\n在本书的过程中，我们将假设使用基于 GCC 的工具链(GCC 或 Windows 上的 MinGW)。如果您希望使用替代工具链(铿锵、MSVC、ICC 等)，请参考这些工具链提供的文档以获得兼容的命令。\n\n为了编译本书中提供的例子，将使用 makefiles。对于那些不熟悉 makefiles 的人来说，它们是一种简单但强大的基于文本的格式，与`make`工具一起使用，用于自动化构建任务，包括编译源代码和调整构建环境。`make`于 1977 年首次发布，至今仍是最受欢迎的构建自动化工具之一。\n\n假设熟悉命令行(Bash 或同等工具)，建议使用 Windows 的人使用 MSYS2(Windows 上的 Bash)。\n\n# 多线程应用\n\n在最基本的形式中，多线程应用由一个具有两个或更多线程的单一进程组成。这些线程可以以多种方式使用；例如，通过每个传入事件或事件类型使用一个线程，允许进程以异步方式响应事件，或者通过将工作拆分到多个线程来加快数据处理速度。\n\n对事件的异步响应的例子包括在单独的线程上处理图形用户界面和网络事件，使得两种类型的事件都不必等待另一种，或者可以阻止事件被及时响应。通常，单个线程执行单个任务，如图形用户界面或网络事件的处理，或数据的处理。\n\n对于这个基本示例，应用将从一个单一的线程开始，然后启动多个线程，并等待它们完成。这些新线程中的每一个都将在完成之前执行自己的任务。\n\n![](img/4bbbef63-cd36-47c6-b234-039b4e0f5e12.png)\n\n让我们从应用的包含变量和全局变量开始:\n\n```cpp\n#include <iostream>\n#include <thread>\n#include <mutex>\n#include <vector>\n#include <random>\n\nusing namespace std;\n\n// --- Globals\nmutex values_mtx;\nmutex cout_mtx;\nvector<int> values;\n\n```\n\n任何使用过 C++ 的人都应该熟悉输入/输出流和向量头:前者在这里用于标准输出(`cout`)，向量用于存储一系列值。\n\n随机头在`c++ 11`中是新的，顾名思义，它提供了生成随机序列的类和方法。我们在这里使用它来让我们的线程做一些有趣的事情。\n\n最后，线程和互斥包含是我们多线程应用的核心；它们提供了创建线程的基本方法，并允许它们之间的线程安全交互。\n\n接下来，我们创建两个互斥体:一个用于全局向量，一个用于`cout`，因为后者不是线程安全的。\n\n接下来，我们创建如下主要功能:\n\n```cpp\nint main() {\n    values.push_back(42);\n\n```\n\n我们将一个固定值推送到向量实例上；我们稍后创建的线程将使用这个:\n\n```cpp\n    thread tr1(threadFnc, 1);\n    thread tr2(threadFnc, 2);\n    thread tr3(threadFnc, 3);\n    thread tr4(threadFnc, 4);\n\n```\n\n我们创建新的线程，并向它们提供要使用的方法的名称，传递任何参数——在本例中，只是一个整数:\n\n```cpp\n\n    tr1.join();\n    tr2.join();\n    tr3.join();\n    tr4.join();\n\n```\n\n接下来，我们等待每个线程完成，然后在每个线程实例上调用`join()`继续:\n\n```cpp\n\n    cout << \"Input: \" << values[0] << \", Result 1: \" << values[1] << \", Result 2: \" << values[2] << \", Result 3: \" << values[3] << \", Result 4: \" << values[4] << \"\\n\";\n\n    return 1;\n}\n\n```\n\n在这一点上，我们期望每个线程已经做了它应该做的任何事情，并将结果添加到向量中，然后我们将向量读出并展示给用户。\n\n当然，这几乎没有显示应用中真正发生了什么，大部分只是使用线程的基本简单性。接下来，让我们看看传递给每个线程实例的这个方法内部发生了什么:\n\n```cpp\nvoid threadFnc(int tid) {\n    cout_mtx.lock();\n    cout << \"Starting thread \" << tid << \".\\n\";\n    cout_mtx.unlock();\n\n```\n\n在前面的代码中，我们可以看到传递给 thread 方法的整数参数是一个线程标识符。为了指示线程正在启动，输出包含线程标识符的消息。由于我们对此使用了`non-thread-safe`方法，因此我们使用`cout_mtx`互斥体实例来安全地做到这一点，确保在任何时候只有一个线程可以写入`cout`:\n\n```cpp\n    values_mtx.lock();\n    int val = values[0];\n    values_mtx.unlock();\n\n```\n\n当我们获得向量中的初始值集时，我们将其复制到一个局部变量中，这样我们就可以立即释放向量的互斥体，以使其他线程能够使用该向量:\n\n```cpp\n    int rval = randGen(0, 10);\n    val += rval;\n\n```\n\n最后两行包含了所创建的线程的本质:它们获取初始值，并向其添加随机生成的值。`randGen()`方法取两个参数，定义返回值的范围:\n\n```cpp\n\n    cout_mtx.lock();\n    cout << \"Thread \" << tid << \" adding \" << rval << \". New value: \" << val << \".\\n\";\n    cout_mtx.unlock();\n\n    values_mtx.lock();\n    values.push_back(val);\n    values_mtx.unlock();\n}\n\n```\n\n最后，在向向量添加新值之前，我们(安全地)记录一条消息，通知用户该操作的结果。在这两种情况下，我们使用各自的互斥来确保在访问资源时不会与任何其他线程重叠。\n\n一旦方法到达这一点，包含它的线程将终止，主线程将少一个等待重新加入的线程。线程的连接基本上意味着它停止存在，通常将返回值传递给创建线程的线程。这可以显式发生，主线程等待子线程完成，或者在后台发生。\n\n最后，我们来看看`randGen()`方法。在这里，我们还可以看到一些多线程的特定添加:\n\n```cpp\nint randGen(const int& min, const int& max) {\n    static thread_local mt19937 generator(hash<thread::id>()(this_thread::get_id()));\n    uniform_int_distribution<int> distribution(min, max);\n    return distribution(generator)\n}\n\n```\n\n前面的方法采用前面解释的最小值和最大值，这限制了该方法可以返回的随机数的范围。其核心是使用基于 mt19937 的`generator`，该算法采用 32 位**默森扭转器**算法，状态大小为 19937 位。对于大多数应用来说，这是一个常见且合适的选择。\n\n这里值得注意的是`thread_local`关键字的使用。这意味着即使它被定义为一个静态变量，它的范围也将被限制在使用它的线程上。因此，每个线程都将创建自己的`generator`实例，这在 STL 中使用随机数应用编程接口时非常重要。\n\n内部线程标识符的散列被用作`generator`的种子。这确保了每个线程为其`generator`实例获得一个相当唯一的种子，从而允许更好的随机数序列。\n\n最后，我们使用提供的最小和最大限制创建一个新的`uniform_int_distribution`实例，并将其与`generator`实例一起使用来生成我们返回的随机数。\n\n# Makefile\n\n为了编译前面描述的代码，可以使用集成开发环境，或者在命令行上键入命令。正如本章开头提到的，我们将使用 makefiles 作为本书的示例。这样做的最大好处是不需要重复输入相同的扩展命令，并且可以移植到任何支持`make`的系统中。\n\n进一步的优势包括能够自动移除先前生成的工件，并且只编译那些已经改变的源文件，以及对构建步骤的详细控制。\n\n这个例子的 makefile 相当基本:\n\n```cpp\nGCC := g++\n\nOUTPUT := ch01_mt_example\nSOURCES := $(wildcard *.cpp)\nCCFLAGS := -std=c++ 11 -pthread\n\nall: $(OUTPUT)\n\n$(OUTPUT):\n    $(GCC) -o $(OUTPUT) $(CCFLAGS) $(SOURCES)\n\nclean:\n    rm $(OUTPUT)\n\n.PHONY: all\n\n```\n\n从上到下，我们首先定义我们将使用的编译器(`g++ `)，设置输出二进制文件的名称(Windows 上的`.exe`扩展将自动后修复)，然后收集源代码和任何重要的编译器标志。\n\n通配符功能允许一次收集与其后面的字符串匹配的所有文件的名称，而不必单独定义文件夹中每个源文件的名称。\n\n对于编译器标志，我们只对启用`c++ 11`特性感兴趣，为此 GCC 仍然需要一个来提供这个编译器标志。\n\n对于`all`方法，我们只是告诉`make`用提供的信息运行`g++ `。接下来我们定义一个简单的清理方法，它只是删除生成的二进制文件，最后，我们告诉`make`不要解释文件夹中任何名为`all`的文件夹或文件，而是在`.PHONY`部分使用内部方法。\n\n当我们运行这个 makefile 时，我们会看到下面的命令行输出:\n\n```cpp\n$ make\ng++ -o ch01_mt_example -std=c++ 11 ch01_mt_example.cpp\n\n```\n\n之后，我们在同一个文件夹中找到了一个名为`ch01_mt_example`的可执行文件(Windows 上附带了`.exe`扩展名)。执行该二进制文件将产生类似于以下内容的命令行输出:\n\n```cpp\n$ ./ch01_mt_example.exe\n\nStarting thread 1.\n\nThread 1 adding 8\\. New value: 50.\n\nStarting thread 2.\n\nThread 2 adding 2\\. New value: 44.\n\nStarting thread 3.\n\nStarting thread 4.\n\nThread 3 adding 0\\. New value: 42.\n\nThread 4 adding 8\\. New value: 50.\n\nInput: 42, Result 1: 50, Result 2: 44, Result 3: 42, Result 4: 50\n\n```\n\n这里已经可以看到线程及其输出的异步特性。虽然线程`1`和`2`看起来是同步运行的，似乎是按顺序启动和退出，但是线程`3`和`4`显然是异步运行的，因为两者在记录它们的动作之前同时启动。由于这个原因，尤其是在运行时间较长的线程中，实际上不可能说出日志输出和结果将按什么顺序返回。\n\n虽然我们用一个简单的向量来收集线程的结果，但无法说明`Result 1`是否真正来源于我们在开始时分配了 ID 1 的线程。如果我们需要这些信息，我们需要通过使用一个信息结构来扩展我们返回的数据，该信息结构包含处理线程或类似的细节。\n\n例如，可以这样使用`struct`:\n\n```cpp\nstruct result {\n    int tid;\n    int result;\n};\n\n```\n\n然后，向量将被更改为包含结果实例，而不是整数实例。可以将初始整数值作为其参数的一部分直接传递给线程，或者通过其他方式传递。\n\n# 其他应用\n\n本章中的示例主要适用于必须并行处理数据或任务的应用。对于前面提到的具有业务逻辑和网络相关特性的基于图形用户界面的应用的用例，启动所需线程的主应用的基本设置将保持不变。然而，每个线程不是相同的，而是完全不同的方法。\n\n对于这种类型的应用，线程布局如下所示:\n\n![](img/1001d2c0-f5ce-4b9b-9a37-15ce15d85be5.png)\n\n如图所示，主线程将启动图形用户界面、网络和业务逻辑线程，后者与网络线程通信以发送和接收数据。业务逻辑线程还将从图形用户界面线程接收用户输入，并将更新发送回以在图形用户界面上显示。\n\n# 摘要\n\n在这一章中，我们学习了使用本机线程应用编程接口的 C++ 多线程应用的基础知识。我们研究了如何让多个线程并行执行一个任务，并探讨了如何在多线程应用中正确使用 STL 中的随机数应用编程接口。\n\n在下一章中，我们将讨论如何在硬件和操作系统中实现多线程。我们将看到这种实现如何因处理器架构和操作系统而异，以及这如何影响我们的多线程应用。"
  },
  {
    "path": "docs/master-cpp-multithrd/02.md",
    "content": "# 二、处理器和操作系统上的多线程实现\n\n任何多线程应用的基础都是由处理器硬件实现所需的功能，以及由操作系统将这些功能转换为应用使用的应用接口的方式形成的。了解这一基础对于直观了解如何最好地实现多线程应用至关重要。\n\n本章着眼于硬件和操作系统在过去几年中是如何发展的，以达到今天使用的当前实现和应用编程接口。它展示了前一章的示例代码如何最终转化为处理器和相关硬件的命令。\n\n本章涵盖的主题包括:\n\n*   支持多线程概念的处理器硬件的发展\n*   操作系统如何改变以使用这些硬件功能\n*   各种架构中内存安全和内存模型背后的概念\n*   操作系统不同进程和线程模型之间的差异\n\n# 定义进程和线程\n\n本质上，对于**操作系统** ( **操作系统**，一个进程由一个或多个线程组成，每个线程处理自己的状态和变量。人们会认为这是一种分层配置，以操作系统为基础，为(用户)进程的运行提供支持。每个进程都由一个或多个线程组成。进程间通信由操作系统提供的**进程间通信** ( **IPC** )处理。\n\n在图形视图中，如下所示:\n\n![](img/2e9933cf-1c8e-4280-8f02-b88c987b2d2e.png)\n\n操作系统中的每个进程都有自己的状态，进程中的每个线程都有自己的状态以及相对于同一进程中其他线程的状态。虽然 IPC 允许进程之间相互通信，但线程可以通过各种方式与进程内的其他线程进行通信，我们将在后面的章节中对此进行更深入的探讨。这通常涉及线程之间的某种共享内存。\n\n应用是以特定的可执行格式从二进制数据加载的，例如通常在 Linux 和许多其他操作系统上使用的**可执行和可链接格式** ( **ELF** )。对于 ELF 二进制文件，应该始终存在以下数量的部分:\n\n*   `.bss`\n*   `.data`\n*   `.rodata`\n*   `.text`\n\n`.bss`部分本质上被分配了未初始化的内存，包括不占用二进制空间的空数组，因为在可执行文件中存储纯零的行是没有意义的。同样，还有`.data`部分，有初始化的数据。这包含全局表、变量等。最后，`.rodata`部分类似于`.data`，但顾名思义，它是只读的。它包含诸如硬编码字符串之类的东西。\n\n在`.text`部分，我们找到将由处理器执行的实际应用指令(代码)。整个过程将由操作系统加载，从而创建一个进程。这样一个过程的布局如下图所示:\n\n![](img/d3611435-38e1-45b6-873a-e7a50ab06a14.png)\n\n这就是从 ELF 格式二进制文件启动的进程的样子，尽管内存中的最终格式在基本上任何操作系统中都大致相同，包括从 PE 格式二进制文件启动的 Windows 进程。二进制文件中的每个部分都被加载到各自的部分中，BSS 部分被分配到指定的大小。`.text`部分与其他部分一起被加载，一旦完成，它的初始指令就被执行，从而开始该过程。\n\n在 C++ 等系统语言中，可以看到变量和其他程序状态信息是如何存储在堆栈(变量存在于范围内)和堆(使用新的运算符)中的。堆栈是内存的一部分(每个线程分配一个)，其大小取决于操作系统及其配置。创建新线程时，通常还可以通过编程设置堆栈大小。\n\n在操作系统中，一个进程由一个内存地址块组成，内存地址块的大小是恒定的，并受内存指针大小的限制。对于 32 位操作系统，这将把该数据块限制为 4 GB。在这个虚拟内存空间中，操作系统分配一个基本堆栈和堆，这两者都可以增长，直到所有内存地址都用完，进程分配更多内存的进一步尝试将被拒绝。\n\n堆栈是操作系统和硬件的概念。本质上，它是所谓堆栈帧的集合(堆栈)，每个堆栈帧都由变量、指令和其他与任务执行帧相关的数据组成。\n\n在硬件方面，堆栈是任务(x86)或进程状态(ARM)的一部分，这是处理器定义执行实例(程序或线程)的方式。这个硬件定义的实体包含单个执行线程的整个状态。有关这方面的更多详细信息，请参见以下部分。\n\n# x86 (32 位和 64 位)中的任务\n\n英特尔 IA-32 系统编程指南第 3A 卷中对任务的定义如下:\n\n*“任务是处理器可以分派、执行和挂起的工作单元。它可以用来执行程序、任务或进程、操作系统服务实用程序、中断或异常处理程序、内核或执行实用程序。”*\n\n*“IA-32 架构提供了一种机制，用于保存任务的状态、分派任务以供执行以及从一个任务切换到另一个任务。当在保护模式下运行时，所有的处理器执行都发生在一个任务中。即使是简单的系统也必须定义至少一个任务。更复杂的系统可以使用处理器的任务管理设施来支持多任务应用。”*\n\n本节选自 IA-32(英特尔 x86)手册，总结了硬件如何支持和实现对操作系统、进程以及这些进程之间的切换的支持。\n\n这里重要的是要认识到，对于处理器来说，没有进程或线程这种东西。它只知道执行线程，定义为一系列指令。这些指令被加载到内存中的某个地方，并且随着应用在进程的数据部分中执行，这些指令中的当前位置与正在创建的变量数据(变量)一起被跟踪。\n\n每个任务也在硬件定义的保护环内运行，操作系统的任务通常在环 0 上运行，用户任务在环 3 上运行。除了 x86 体系结构上现代操作系统的特定用例之外，很少使用环 1 和环 2。这些环是由硬件强制执行的特权级别，例如允许内核和用户级任务的严格分离。\n\n32 位和 64 位任务的任务结构在概念上非常相似。它的官方名称是**任务状态结构** ( **TSS** )。对于 32 位 x86 CPUs，它具有以下布局:\n\n![](img/b2953e0f-911b-480f-8a40-5babe3bf6604.png)\n\n以下是第一部分:\n\n*   **SS0** :第一个堆栈段选择器字段\n*   **ESP0** :第一个 SP 字段\n\n对于 64 位 x86 _ 64 CPUs，TSS 布局看起来有些不同，因为在这种模式下不支持基于硬件的任务切换:\n\n![](img/d93acf01-30e7-4b52-93e8-ddfba382f8c5.png)\n\n在这里，我们有相似的相关字段，只是名称不同:\n\n*   **RSPn** :权限级别 0 到 2 的 SP\n*   **ISTn** :中断堆栈表指针\n\n即使在 32 位模式的 x86 上，CPU 支持基于硬件的任务间切换，但大多数操作系统在不考虑模式的情况下，每个 CPU 将仅使用单个 TSS 结构，并在软件中进行任务间的实际切换。这部分是由于效率原因(仅换出发生变化的指针)，部分是由于只有这种方式才可能实现的功能，例如测量进程/线程使用的 CPU 时间，以及调整线程或进程的优先级。在软件中这样做也简化了 64 位和 32 位系统之间代码的可移植性，因为前者不支持基于硬件的任务切换。\n\n在基于软件的任务切换期间(通常通过中断)，电潜泵/RSP 等存储在内存中，并用下一个计划任务的值替换。这意味着一旦执行恢复，TSS 结构现在将拥有新任务的**堆栈指针** ( **SP** )、段指针、寄存器内容和所有其他细节。\n\n中断的来源可以基于硬件或软件。设备通常使用硬件中断向中央处理器发出信号，表示它们需要操作系统的注意。调用硬件中断的行为称为中断请求(IRQ)。\n\n软件中断可能是由于中央处理器本身的异常情况，或者是中央处理器指令集的特征。操作系统内核切换任务的动作也是通过触发软件中断来执行的。\n\n# ARM 中的进程状态\n\n在 ARM 架构中，应用通常运行在非特权**异常级别 0** ( **EL0** )级别，相当于 x86 架构上的 ring 3，以及 EL1 中的 OS 内核。ARMv7 (AArch32，32 位)架构在通用寄存器 13 中具有 SP。对于 ARMv8 (AArch64，64 位)，为每个异常级别实现一个专用的 SP 寄存器:`SP_EL0`、`SP_EL1`等等。\n\n对于任务状态，ARM 架构使用**程序状态寄存器** ( **PSR** )实例作为**当前程序状态寄存器** ( **CPSR** )或**保存程序状态寄存器** ( **SPSR** )程序状态的寄存器。PSR 是**过程状态** ( **PSTATE** )的一部分，它是过程状态信息的抽象。\n\n虽然 ARM 架构与 x86 架构有很大的不同，但在使用基于软件的任务切换时，基本原则不会改变:保存当前任务的 SP，注册状态，并在恢复处理之前将下一个任务的细节放在那里。\n\n# 堆栈\n\n正如我们在前面几节中看到的，堆栈和中央处理器寄存器一起定义了一个任务。如前所述，这个堆栈由堆栈帧组成，每个堆栈帧定义了特定任务执行实例的(局部)变量、参数、数据和指令。值得注意的是，尽管堆栈和堆栈框架主要是一个软件概念，但它是任何现代操作系统的一个基本特征，在许多中央处理器指令集中都有硬件支持。从图形上看，它可以像下面这样可视化:\n\n![](img/9906f3c8-d234-4a60-84c1-fea9928fd643.png)\n\nSP(x86 上的 e SP)指向堆栈顶部，另一个指针(**扩展基本指针** ( **EBP** )代表 x86)。每个帧都包含对前一帧的引用(调用者返回地址)，由操作系统设置。\n\n当在自己的 C++ 应用中使用调试器时，这基本上是在请求回溯时看到的——堆栈的各个帧显示了直到当前帧之前的初始堆栈帧。在这里，人们可以检查每个单独的帧的细节。\n\n# 定义多线程\n\n在过去的几十年里，许多与计算机处理任务的方式有关的不同术语被创造出来并被普遍使用。不管正确与否，其中许多也可以互换使用。与多处理相比，多线程就是一个例子。\n\n这里，后者意味着在具有多个物理处理器的系统中，每个处理器运行一个任务，而前者意味着在单个处理器上同时运行多个任务，因此给人一种它们都在同时执行的错觉:\n\n![](img/4490b4b1-5789-4b3e-8da5-db197666ab85.png)\n\n多处理和多任务之间的另一个有趣的区别是，后者使用时间片在单个处理器内核上运行多个线程。这与多线程不同，因为在多任务系统中，没有任务会在同一个中央处理器内核上以并发方式运行，尽管任务仍然会被中断。\n\n从软件的角度来看，进程和包含在所述进程中的线程之间的共享内存空间的概念是多线程系统的核心。尽管硬件通常没有意识到这一点——只看到操作系统的一项任务。然而，这样的多线程进程包含两个或更多线程。然后，这些线程中的每一个都执行自己的一系列任务。\n\n在其他实现中，例如英特尔在 x86 处理器上的**超线程** ( **HT** )，这种多线程是在硬件本身中实现的，通常称为 SMT(详见*同步多线程(SMT)* 一节)。启用超线程后，每个物理中央处理器内核作为两个内核呈现给操作系统。然后，硬件本身将尝试同时执行分配给这些所谓的虚拟内核的任务，调度可以同时使用处理内核的不同元素的操作。实际上，这可以显著提升性能，而无需操作系统或应用进行任何类型的优化。\n\n当然，操作系统仍然可以进行自己的调度，以进一步优化任务的执行，因为硬件不知道它正在执行的指令的许多细节。\n\n启用超线程在视觉格式中如下所示:\n\n![](img/f98180f1-715e-4af4-b0a5-cadeb6210484.png)\n\n在上图中，我们看到了内存中四个不同任务的指令。其中，两个任务(线程)同时执行，中央处理器的调度器(在前端)试图调度指令，以便尽可能多的指令可以并行执行。在不可能的地方，所谓的管道气泡(白色)出现在执行硬件空闲的地方。\n\n加上内部中央处理器优化，这导致了非常高的指令吞吐量，也称为每秒**指令** ( **IPC** )。对于确定一个中央处理器的绝对性能来说，这个 IPC 值通常远比一个中央处理器的千兆赫等级更重要。\n\n# 弗林分类法\n\n早在 1966 年，迈克尔·j·弗林就首次提出用一个系统对不同类型的计算机体系结构进行分类。这个分类系统知道四个类别，根据输入和输出流的数量定义处理硬件的能力:\n\n*   **单指令、单数据** ( **SISD** ):取单指令对单数据流进行操作。这是 CPU 的传统模式。\n*   **单指令多数据** ( **SIMD** ):采用这种模型，单个指令并行操作多个数据流。这是矢量处理器如**图形处理单元** ( **图形处理器**)使用的。\n*   **多指令、单数据** ( **MISD** ):该模型最常用于冗余系统，不同的处理单元对同一数据执行相同的操作，在最后验证结果以检测硬件故障。这是航空电子系统和类似系统常用的。\n*   **多指令、多数据** ( **MIMD** ):对于这个模型，多处理系统非常适合自己。多个处理器上的多个线程处理多个数据流。这些线索并不完全相同，SIMD 的情况就是如此。\n\n这些类别需要注意的一点是，它们都是根据多处理来定义的，这意味着它们指的是硬件的内在能力。使用软件技术，几乎任何方法都可以在一个常规的 SISD 风格的架构上进行近似。然而，这是多线程的一部分。\n\n# 对称与非对称多处理\n\n在过去的几十年里，许多包含多个处理单元的系统被创造出来。这些可大致分为**对称多处理** ( **SMP** )和**非对称多处理** ( **AMP** )系统。\n\nAMP 的主要定义特征是第二个处理器作为外围设备连接到主 CPU。这意味着它不能运行控制软件，只能运行用户应用。这种方法也被用来连接使用不同架构的 CPU，例如，允许在基于 Amiga 68k 的系统上运行 x86 应用。\n\n在 SMP 系统中，每个 CPU 都是能够访问相同硬件资源的对等体，并且以协作方式建立。最初，SMP 系统涉及多个物理 CPU，但后来，多个处理器内核集成在单个 CPU 芯片上:\n\n![](img/9c024b2b-a17b-4a4f-a8ef-a0dc7b8e3096.png)\n\n随着多核 CPU 的激增，SMP 是嵌入式开发之外最常见的处理类型，其中单处理(单核、单处理器)仍然非常普遍。\n\n从技术上讲，系统中的声音、网络和图形处理器可以被认为是与 CPU 相关的非对称处理器。随着**通用 GPU**(**GPU**)处理的增加，AMP 变得更加相关。\n\n# 松散和紧密耦合的多处理\n\n多处理系统不一定必须在单个系统中实现，但也可以由连接在网络中的多个系统组成。这样的集群被称为松散耦合的多处理系统。我们在[第 9 章](09.html)、*分布式计算的多线程*中介绍了分布式计算。\n\n这与紧密耦合的多处理系统形成对比，该系统使用相同的低级高速总线或类似设备集成在单个**印刷电路板** ( **印刷电路板**)上。\n\n# 将多处理和多线程结合起来\n\n几乎任何现代系统都将多处理和多线程结合在一起，这得益于多核处理器，它在单个处理器芯片上结合了两个或更多的处理核心。这对操作系统来说意味着，它必须跨多个处理内核调度任务，同时还必须在特定的内核上调度任务，以获得最大的性能。\n\n这是任务调度器的领域，我们稍后会看到。只要说这是一个值得自己写本书的话题就够了。\n\n# 多线程类型\n\n像多处理一样，没有一个单一的实现，而是两个主要的实现。这两者之间的主要区别是处理器在单个周期内可以并发执行的最大线程数。多线程实现的主要目标是获得尽可能接近 100%的处理器硬件利用率。多线程利用线程级和进程级并行来实现这一目标。\n\n是两种类型的多线程，我们将在下面的小节中介绍。\n\n# 时态多线程\n\n也称为超线程，**时态多线程** ( **TMT** )的主要子类型为粗粒度和细粒度(或交错)。前者在不同任务之间快速切换，在切换到另一个任务的上下文之前保存每个任务的上下文。后一种类型在每个周期内切换任务，从而产生一个包含来自各种任务的指令的中央处理器流水线，术语*交错*就是由此而来的。\n\n细粒度类型在桶处理器中实现。与 x86 和其他体系结构相比，它们有一个优势，即它们可以保证特定的时序(对硬实时嵌入式系统有用)，而且由于可以做出假设，实现起来不太复杂。\n\n# 同时多线程\n\nSMT 在超标量 CPU(实现指令级并行)上实现，包括 x86 和 ARM 架构。SMT 的定义特征也由其名称来表示，具体来说，它能够在每个内核上并行执行多个线程。\n\n通常，每个内核两个线程是常见的，但是一些设计支持每个内核最多八个并发线程。这样做的主要优点是能够在线程之间共享资源，而明显的缺点是多线程的需求冲突，这是必须管理的。另一个优点是，由于缺乏硬件资源复制，它使最终的中央处理器更节能。\n\n英特尔的 HT 技术本质上是英特尔的 SMT 实现，提供了一个基本的两线程 SMT 引擎，从 2002 年的一些奔腾 4 CPUs 开始。\n\n# 调度程序\n\n存在许多任务调度算法，每种算法都关注不同的目标。一些人可能寻求最大化吞吐量，另一些人可能寻求最小化延迟，而另一些人可能寻求最大化响应时间。哪个调度程序是最佳选择完全取决于系统的应用。\n\n对于桌面系统，调度程序通常尽可能通用，通常优先考虑前台应用而不是后台应用，以便给用户最好的桌面体验。\n\n对于嵌入式系统，尤其是实时系统，工业应用反而会寻求保证时序。这使得过程能够在正确的时间执行，这在例如驱动机械、机器人或化学过程中至关重要，在这些过程中，即使几毫秒的延迟也可能是昂贵的，甚至是致命的。\n\n调度程序的类型也取决于操作系统的多任务状态——协作多任务系统无法提供关于何时可以将正在运行的进程切换到另一个进程的许多保证，因为这取决于活动进程何时退出。\n\n使用抢占式调度器，进程可以在不被察觉的情况下进行切换，从而允许调度器更好地控制进程何时在哪个时间点运行。\n\n基于 Windows NT 的操作系统(Windows NT、2000、XP 等)使用所谓的多级反馈队列，具有 32 个优先级。这种类型的优先级调度器允许用户将任务优先于其他任务，允许用户微调结果体验。\n\nLinux 最初(内核 2.4)也使用了类似 Windows NT 的基于多级反馈队列的优先级调度器，带有 O(n)调度器。在 2.6 版本中，这被一个 O(1)调度程序所取代，允许在固定的时间内调度进程。从 Linux 内核 2.6.23 开始，默认的调度程序是**完全公平调度程序** ( **CFS** )，它确保所有任务都获得相当份额的 CPU 时间。\n\n下表列出了用于许多常用或知名操作系统的调度算法类型:\n\n| **操作系统** | **先发制人** | **算法** |\n| 朋友 OS | 是 | 优先循环调度 |\n| FreeBSD | 是 | 多级反馈队列 |\n| 2.6.0 之前的 Linux 内核 | 是 | 多级反馈队列 |\n| Linux 内核 2.6.0-2.6.23 | 是 | O(1)调度程序 |\n| 2.6.23 之后的 Linux 内核 | 是 | 完全公平调度程序 |\n| 经典的 Mac OS 之前版本 | 没有人 | 合作调度程序 |\n| Mac OS 9 | 一些 | MP 任务的抢先调度程序，进程和线程的协作调度程序 |\n| X/macOS | 是 | 多级反馈队列 |\n| NetBSD | 是 | 多级反馈队列 |\n| Solaris | 是 | 多级反馈队列 |\n| Windows 3.1x | 没有人 | 合作调度程序 |\n| Windows 95、98、我 | 一半 | 32 位进程的抢先调度程序，16 位进程的协作调度程序 |\n| Windows NT(包括 2000、XP、Vista、7 和服务器) | 是 | 多级反馈队列 |\n\n(来源:[https://en . Wikipedia . org/wiki/Scheduling _(计算)](https://en.wikipedia.org/wiki/Scheduling_(computing)))\n\n抢先列指示调度程序是否抢先，下一列提供进一步的细节。可以看到，抢占式调度器非常常见，所有现代桌面操作系统都使用它。\n\n# 跟踪演示应用\n\n在[第 1 章](01.html)、*重访多线程*的演示代码中，我们看了一个简单的`c++ 11`应用，它使用四个线程来执行一些处理。在本节中，我们将从硬件和操作系统的角度来看同一个应用。\n\n当我们查看`main`函数中代码的开始时，我们看到我们创建了一个包含单个(整数)值的数据结构:\n\n```cpp\nint main() {\n    values.push_back(42);\n\n```\n\n在操作系统创建新的任务和相关的堆栈结构后，向量数据结构的实例(为整数类型定制)被分配到堆栈上。这个文件的大小是在二进制文件的全局数据部分指定的。\n\n当应用使用其入口函数(默认为`main()`)开始执行时，数据结构被修改为包含新的整数值。\n\n接下来，我们创建四个线程，为每个线程提供一些初始数据:\n\n```cpp\n    thread tr1(threadFnc, 1);\n    thread tr2(threadFnc, 2);\n    thread tr3(threadFnc, 3);\n    thread tr4(threadFnc, 4);\n\n```\n\n对于操作系统，这意味着创建新的数据结构，并为每个新线程分配一个堆栈。对于硬件，如果不使用基于硬件的任务切换，这最初不会改变任何事情。\n\n此时，操作系统的调度程序和中央处理器可以结合起来，利用硬件的特性，包括 SMP、SMT 等，尽可能高效、快速地执行这组任务(线程)。\n\n此后，主线程等待，直到其他线程停止执行:\n\n```cpp\n    tr1.join();\n    tr2.join();\n    tr3.join();\n    tr4.join();\n\n```\n\n这些是阻塞调用，将主线程标记为阻塞，直到这四个线程(任务)完成执行。此时，操作系统的调度程序将恢复主线程的执行。\n\n在每个新创建的线程中，我们首先在标准输出上输出一个字符串，确保锁定互斥体以确保同步访问:\n\n```cpp\nvoid threadFnc(int tid) {\n    cout_mtx.lock();\n    cout << \"Starting thread \" << tid << \".\\n\";\n    cout_mtx.unlock();\n\n```\n\n本质上，互斥体是存储在堆堆栈上的单个值，然后使用原子操作访问它。这意味着需要某种形式的硬件支持。使用这个，任务可以检查它是否被允许继续，或者必须等待并重试。\n\n在最后这段特殊的代码中，这个互斥锁允许我们在标准的 C++ 输出流上输出，而没有其他线程的干扰。\n\n之后，我们将向量中的初始值复制到一个局部变量，再次确保同步完成:\n\n```cpp\n    values_mtx.lock();\n    int val = values[0];\n    values_mtx.unlock();\n\n```\n\n这里也发生了同样的事情，只是现在互斥锁允许我们读取向量中的第一个值，而不会有另一个线程访问甚至在我们使用它时改变它的风险。\n\n接下来生成一个随机数，如下所示:\n\n```cpp\n    int rval = randGen(0, 10);\n    val += rval;\n\n```\n\n这使用`randGen()`方法，如下所示:\n\n```cpp\nint randGen(const int& min, const int& max) {\n    static thread_local mt19937 generator(hash<thread::id>() (this_thread::get_id()));\n    uniform_int_distribution<int> distribution(min, max);\n    return distribution(generator);\n}\n\n```\n\n这个方法很有趣，因为它使用了一个线程局部变量。线程本地存储是线程内存中特定于它的一部分，用于全局变量，然而，全局变量必须保持限于该特定线程。\n\n这对于像这里使用的静态变量非常有用。`generator`实例是静态的，因为我们不想每次使用这个方法时都重新初始化它，但是我们也不想在所有线程之间共享这个实例。通过使用线程本地的静态实例，我们可以实现这两个目标。创建并使用一个静态实例，但是对于每个线程是分开的。\n\n`Thread`函数随后以相同的互斥锁序列被锁定，新值被复制到数组中而结束。\n\n```cpp\n    cout_mtx.lock();\n    cout << \"Thread \" << tid << \" adding \" << rval << \". New value: \" << val << \".\\n\";\n    cout_mtx.unlock();\n\n    values_mtx.lock();\n    values.push_back(val);\n    values_mtx.unlock();\n}\n\n```\n\n在这里，我们看到对标准输出流的同步访问，然后是对值数据结构的同步访问。\n\n# 互斥实现\n\n互斥是多线程应用中线程安全访问数据的基础原则。人们可以在硬件和软件中实现这一点。**互斥** ( **互斥**)是大多数实现中这个功能最基本的形式。\n\n# 五金器具\n\n在单处理器(单处理器内核)非 SMT 系统上，最简单的基于硬件的实现是禁用中断，从而防止任务被更改。更常见的是，采用所谓的忙-等原则。这是互斥体背后的基本原理——由于处理器如何获取数据，只有一个任务可以在共享内存中获取和读/写一个原子值，这意味着一个大小与中央处理器寄存器相同(或更小)的变量。这在[第 8 章](02.html)、*原子操作-使用硬件*中有进一步的详细说明。\n\n当我们的代码试图锁定一个互斥体时，它所做的是读取这样一个原子内存段的值，并试图将其设置为锁定值。因为这是单个操作，所以在任何给定时间只有一个任务可以更改该值。其他任务将不得不等待，直到它们可以在这个繁忙等待周期中获得访问权限，如下图所示:\n\n![](img/df8b8297-74e1-477f-877c-4823af8f143e.png)\n\n# 软件\n\n软件定义的互斥实现都是基于繁忙等待的。一个例子是**德克尔的**算法，它定义了一个系统，其中两个进程可以同步，采用忙等待来等待另一个进程离开关键部分。\n\n该算法的伪代码如下:\n\n```cpp\n    variables\n        wants_to_enter : array of 2 booleans\n        turn : integer\n\n    wants_to_enter[0] ← false\n    wants_to_enter[1] ← false\n    turn ← 0 // or 1\n\np0:\n    wants_to_enter[0] ← true\n    while wants_to_enter[1] {\n        if turn ≠ 0 {\n            wants_to_enter[0] ← false\n            while turn ≠ 0 {\n                // busy wait\n            }\n            wants_to_enter[0] ← true\n        }\n    }\n    // critical section\n    ...\n    turn ← 1\n    wants_to_enter[0] ← false\n    // remainder section\n\np1:\n    wants_to_enter[1] ← true\n    while wants_to_enter[0] {\n        if turn ≠ 1 {\n            wants_to_enter[1] ← false\n            while turn ≠ 1 {\n                // busy wait\n            }\n            wants_to_enter[1] ← true\n        }\n    }\n    // critical section\n    ...\n    turn ← 0\n    wants_to_enter[1] ← false\n    // remainder section\n\n```\n\n(参考自:[https://en.wikipedia.org/wiki/Dekker's_algorithm](https://en.wikipedia.org/wiki/Dekker's_algorithm)\n\n在前面的算法中，进程指示进入关键部分的意图，检查是否轮到它们(使用进程标识)，然后在它们进入该部分后将它们进入该部分的意图设置为假。只有当一个流程再次将其输入意图设置为真时，它才会再次进入关键部分。如果它希望进入，但`turn`与它的进程标识不匹配，它将忙碌等待，直到条件变为真。\n\n基于软件的互斥算法的一个主要缺点是，只有当代码的**无序** ( **OoO** )执行被禁用时，它们才能工作。OoO 意味着硬件主动对传入的指令重新排序，以便优化它们的执行，从而改变它们的顺序。由于这些算法要求按顺序执行各种步骤，因此它们不再适用于 OoO 处理器。\n\n# 摘要\n\n在本章中，我们看到了进程和线程是如何在操作系统和硬件中实现的。我们还研究了处理器硬件的各种配置和调度中涉及的操作系统元素，以了解它们如何提供各种类型的任务处理。\n\n最后，我们采用了上一章的多线程程序示例，并再次运行了一遍，这一次考虑了在执行时操作系统和处理器中发生的情况。\n\n在下一章中，我们将看看通过操作系统和基于库的实现提供的各种多线程应用编程接口，以及比较这些应用编程接口的例子。"
  },
  {
    "path": "docs/master-cpp-multithrd/03.md",
    "content": "# 三、C++ 多线程应用编程接口\n\n虽然 C++ 在**标准模板库** ( **STL** 中有本机多线程实现，但操作系统级和基于框架的多线程应用编程接口仍然非常常见。这些应用编程接口的例子包括窗口和 **POSIX** ( **可移植操作系统接口**)线程，以及`Qt`、`Boost`和`POCO`库提供的线程。\n\n本章详细介绍了每种 API 提供的特性，以及它们之间的异同。最后，我们将使用示例代码来看看常见的使用场景。\n\n本章涵盖的主题包括:\n\n*   可用多线程应用编程接口的比较\n*   这些应用编程接口的使用示例\n\n# API 概述\n\n在 **C++ 2011** ( **C++ 11** )标准之前，开发了许多不同的线程实现，其中许多仅限于特定的软件平台。其中一些在今天仍然适用，例如 Windows 线程。其他的已经被标准所取代，其中 **POSIX 线程** ( **Pthreads** )已经成为了类 UNIX 操作系统上事实上的标准。这包括基于 Linux 和基于 BSD 的操作系统，以及 OS X (macOS)和 Solaris。\n\n开发许多库是为了使跨平台开发更容易。虽然 Pthreads 有助于使类似 UNIX 的操作系统或多或少地兼容，这是使软件可移植到所有主要操作系统的先决条件之一，但需要一个通用的线程应用编程接口。这就是为什么创建了诸如 Boost、POCO 和 Qt 这样的库。应用可以使用这些，并依靠库来处理平台之间的任何差异。\n\n# POSIX 线程\n\n从 1995 年开始，Pthreads 首先在`POSIX.1c`标准(*线程扩展*，IEEE 标准 1003.1c-1995)中被定义为 POSIX 标准的扩展。当时，UNIX 被选为制造商中立的接口，POSIX 统一了其中的各种 API。\n\n尽管做了这种标准化努力，但由于不可移植的扩展(在方法名中用`_np`标记)，实现它的操作系统之间(例如，Linux 和 OS X 之间)的 Pthread 实现仍然存在差异。\n\n对于`pthread_setname_np`方法，Linux 实现采用两个参数，允许一个参数设置当前线程以外的线程名称。在 OS X(从 10.6 开始)，这个方法只接受一个参数，只允许设置当前线程的名称。如果可移植性是一个问题，人们必须注意这些差异。\n\n1997 年后，POSIX 标准的修订由奥斯汀联合工作组管理。这些修订将线程扩展合并到主标准中。目前的修订版是 7 版，也称为 POSIX.1-2008 版和 IEEE Std 1003.1，2013 版-该标准的免费副本可在网上获得。\n\n操作系统可以被认证为符合 POSIX 标准。目前，如下表所示:\n\n| **名称** | **显影剂** | **自版本**起 | **架构(当前)** | **注释** |\n| [计]高级交互执行程序（Advanced Interactive Executive） | 国际商用机器公司 | 5L | 力量 | 操作系统服务器 |\n| 惠普-UX | 惠普公司 | 11i v3 | PA-RISC、IA-64 (Itanium) | 操作系统服务器 |\n| 伊里克斯 | 硅图形 | six | 每秒百万条指令 | 停止 |\n| K-UX 警探 | -探长 | Two | X86_64， | 基于 Linux 的 |\n| 完整 | 青山软件 | five | ARM、XScale、Blackfin、飞思卡尔酷火、MIPS、PowerPC、x86。 | 实时操作系统 |\n| X/MacOS | 苹果 | 10.5(豹式) | X86_64 | 操作系统桌面 |\n| QNX 中微子 | 黑莓 | one | 英特尔 8088，x86，MIPS，PowerPC，SH-4，ARM，StrongARM，XScale | 实时嵌入式操作系统 |\n| Solaris | Sun/Oracle | Two point five | SPARC、IA-32 (<11)、x86_64、PowerPC (2.5.1) | 操作系统服务器 |\n| Tru64 | DEC、惠普、IBM、康柏 | 5.1B-4 | 希腊字母的第一个字母 | 停止 |\n| UnixWare(终极格斗锦标赛) | Novell、SCO、Xinuos | 7.1.3 | x86 | 操作系统服务器 |\n\n其他操作系统大多是兼容的。以下是同样的例子:\n\n| **名称** | **平台** | **注释** |\n| 机器人 | ARM、x86、MIPS | 基于 Linux。仿生 C 库。 |\n| 贝奥(俳句) | IA-32 臂 x64 | 仅限用于 x86 的 GCC 2.x。 |\n| 达尔文 | PowerPC、x86、ARM | 使用 macOS 所基于的开源组件。 |\n| FreeBSD | IA-32、x86_64、sparc64、PowerPC、ARM、MIPS 等等 | 本质上符合 POSIX。人们可以依赖记录在案的 POSIX 行为。总的来说，在合规性上比 Linux 更严格。 |\n| Linux 操作系统 | Alpha、ARC、ARM、AVR32、Blackfin、H8/300、Itanium、m68k、Microblaze、MIPS、Nios II、OpenRISC、PA-RISC、PowerPC、s390、S+core、SuperH、SPARC、x86、Xtensa 等等 | 一些 Linux 发行版(见上表)被认证为符合 POSIX。这并不意味着每个 Linux 发行版都符合 POSIX。一些工具和库可能与标准不同。对于 Pthreads，这可能意味着 Linux 发行版之间的行为有时是不同的(不同的调度程序等等)，与实现 Pthreads 的其他操作系统相比也是不同的。 |\n| MINIX 3 电脑 | IA-32，手臂 | 符合 POSIX 规范标准 3 (SUSv3，2004)。 |\n| NetBSD | Alpha、ARM、PA-RISC、68k、MIPS、PowerPC、SH3、SPARC、RISC-V、VAX、x86 等等 | 几乎完全兼容 POSX.1 (1990)，并且大部分符合 POSIX.2 (1992)。 |\n| 核 RTOS | ARM、MIPS、PowerPC、Nios II、MicroBlaze、SuperH 等等 | 来自 Mentor Graphics 的专有 RTOS，面向嵌入式应用。 |\n| NuttX | ARM、AVR、AVR32、HCS12、SuperH、Z80 等等 | 重量轻的 RTOS，可从 8 位系统扩展到 32 位系统，重点关注 POSIX 合规性。 |\n| OpenBSD | Alpha、x86_64、ARM、PA-RISC、IA-32、MIPS、PowerPC、SPARC 等等 | 1995 年从 NetBSD 分叉。类似的 POSIX 支持。 |\n| opensolaris/illus | IA-32、x86_64、SPARC、ARM | 符合被认证为兼容的商业 Solaris 发行版。 |\n| VxWorks | ARM，SH-4，x86，x86_64，MIPS，PowerPC | POSIX 兼容，具有用户模式执行环境的认证。 |\n\n从这一点来看，很明显，遵循 POSIX 规范并不是一件容易的事情，并且可以指望在这些平台上编译自己的代码。每个平台还将有自己的一套标准扩展，用于标准中省略但仍然需要的功能。然而，Pthreads 被 Linux、BSD 和类似的软件广泛使用。\n\n# Windows 支持\n\n也可以使用有限的方式使用 POSIX APIs，例如，使用以下内容:\n\n| **名称** | **符合性** |\n| Cygwin | 大部分是完整的。为 POSIX 应用提供完整的运行时环境，它可以作为普通的 Windows 应用分发。 |\n| 明哥 | 有了 MinGW-w64(MinGW 的重新开发)，Pthreads 的支持相当完整，尽管有些功能可能会缺失。 |\n| 面向 Linux 的视窗子系统 | WSL 是 Windows 10 的一个特性，它允许一个 Ubuntu Linux 14.04 (64 位)映像的工具和实用程序在它上面本地运行，尽管不是那些使用 GUI 特性或缺少内核特性的工具和实用程序。否则，它会提供与 Linux 相似的合规性。该功能目前要求运行 Windows 10 周年更新，并使用微软提供的说明手动安装 WSL。 |\n\n一般不建议在 Windows 上使用 POSIX。除非有充分的理由使用 POSIX(例如，现有的大型代码库)，否则使用跨平台 API 之一要容易得多(本章稍后将介绍)，它可以消除任何平台问题。\n\n在接下来的部分中，我们将看看 Pthreads API 提供的特性。\n\n# PThreads 线程管理\n\n这些都是以`pthread_`或`pthread_attr_`开始的功能。这些函数都适用于线程本身及其属性对象。\n\nPthreads 线程的基本用法如下:\n\n```cpp\n#include <pthread.h> \n#include <stdlib.h> \n\n#define NUM_THREADS     5 \n\n```\n\n主 Pthreads 表头是`pthread.h`。这使得除了信号量之外的所有东西都可以访问(这一部分稍后将介绍)。我们还为希望从这里开始的线程数定义了一个常数:\n\n```cpp\nvoid* worker(void* arg) { \n    int value = *((int*) arg); \n\n    // More business logic. \n\n    return 0; \n} \n\n```\n\n我们定义了一个简单的`Worker`函数，稍后我们将把它传递给新线程。出于演示和调试的目的，可以先添加一个简单的`cout`或`printf`为基础的业务逻辑位，打印出发送给新线程的值。\n\n接下来，我们定义`main`函数如下:\n\n```cpp\nint main(int argc, char** argv) { \n    pthread_t threads[NUM_THREADS]; \n    int thread_args[NUM_THREADS]; \n    int result_code; \n\n    for (unsigned int i = 0; i < NUM_THREADS; ++ i) { \n        thread_args[i] = i; \n        result_code = pthread_create(&threads[i], 0, worker, (void*) &thread_args[i]); \n    } \n\n```\n\n我们在前面的函数中创建了一个循环中的所有线程。除了`pthread_create()`函数返回的结果代码(成功时为零)之外，每个线程实例在创建时都会获得一个分配的线程标识(第一个参数)。线程标识是在未来调用中引用线程的句柄。\n\n函数的第二个参数是`pthread_attr_t`结构实例，如果没有，则为 0。这允许新线程的配置特征，例如初始堆栈大小。传递零时，将使用默认参数，这些参数因平台和配置而异。\n\n第三个参数是指向新线程将开始的函数的指针。该函数指针被定义为返回指向无效数据(即自定义数据)的指针，并接受指向无效数据的指针的函数。这里，作为参数传递给新线程的数据是线程标识:\n\n```cpp\n    for (int i = 0; i < NUM_THREADS; ++ i) { \n        result_code = pthread_join(threads[i], 0); \n    } \n\n    exit(0); \n} \n\n```\n\n接下来，我们等待每个工作线程使用完`pthread_join()`函数。这个函数有两个参数，等待线程的标识，以及`Worker`函数返回值的缓冲区(或零)。\n\n管理线程的其他功能如下:\n\n*   `void pthread_exit` ( `void *value_ptr` ):\n    这个函数终止调用它的线程，使得提供的参数值对任何调用它的`pthread_join()`的线程可用。\n\n*   `int pthread_cancel` ( `pthread_t`线程):\n    该函数请求取消指定的线程。根据目标线程的状态，这将调用其取消处理程序。\n\n除此之外，还有`pthread_attr_*`功能来操纵和获取关于`pthread_attr_t`结构的信息。\n\n# 互斥体\n\n这些是前缀为`pthread_mutex_`或`pthread_mutexattr_`的功能。它们适用于互斥体及其属性对象。\n\nPthreads 中的互斥体可以被初始化、销毁、锁定和解锁。他们还可以使用`pthread_mutexattr_t`结构定制他们的行为，该结构具有相应的`pthread_mutexattr_*`功能，用于初始化和销毁其上的属性。\n\n使用静态初始化的 Pthread 互斥体的基本用法如下:\n\n```cpp\nstatic pthread_mutex_t func_mutex = PTHREAD_MUTEX_INITIALIZER; \n\nvoid func() { \n    pthread_mutex_lock(&func_mutex); \n\n    // Do something that's not thread-safe. \n\n    pthread_mutex_unlock(&func_mutex); \n} \n\n```\n\n在这最后一段代码中，我们使用`PTHREAD_MUTEX_INITIALIZER`宏，它为我们初始化互斥体，而不必每次都为它键入代码。与其他 API 相比，我们必须手动初始化和销毁互斥锁，尽管宏的使用有些帮助。\n\n之后，我们锁定和解锁互斥体。还有`pthread_mutex_trylock()`函数，类似于常规的锁版本，但是如果引用的互斥体已经被锁定，它将立即返回，而不是等待它被解锁。\n\n在这个例子中，互斥体没有被显式销毁。然而，这是基于 Pthreads 的应用中正常内存管理的一部分。\n\n# 条件变量\n\n这些是前缀为`pthread_cond_`或`pthread_condattr_`的功能。它们适用于条件变量及其属性对象。\n\nPthreads 中的条件变量遵循相同的模式，除了具有相同的用于管理`pthread_condattr_t`属性结构的功能之外，还具有初始化和`destroy`功能。\n\n本示例介绍了 Pthreads 条件变量的基本用法:\n\n```cpp\n#include <pthread.h> \n#include <stdlib.h>\n#include <unistd.h>\n\n   #define COUNT_TRIGGER 10 \n   #define COUNT_LIMIT 12 \n\n   int count = 0; \n   int thread_ids[3] = {0,1,2}; \n   pthread_mutex_t count_mutex; \n   pthread_cond_t count_cv; \n\n```\n\n在前面的代码中，我们获得了标准的头，并定义了计数触发器和限制，其目的将在稍后变得清晰。我们还定义了几个全局变量:一个计数变量，我们希望创建的线程的标识，以及一个互斥和条件变量:\n\n```cpp\nvoid* add_count(void* t)  { \n    int tid = (long) t; \n    for (int i = 0; i < COUNT_TRIGGER; ++ i) { \n        pthread_mutex_lock(&count_mutex); \n        count++ ; \n        if (count == COUNT_LIMIT) { \n            pthread_cond_signal(&count_cv); \n        } \n\n        pthread_mutex_unlock(&count_mutex); \n        sleep(1); \n    } \n\n    pthread_exit(0); \n} \n\n```\n\n这个前面的函数，本质上，只是在用`count_mutex`获得对全局计数器变量的独占访问权后添加到该变量中。它还检查是否已经达到计数触发值。如果有，它将向条件变量发出信号。\n\n为了给同样运行这个函数的第二个线程一个获得互斥锁的机会，我们在循环的每个周期中休眠 1 秒钟:\n\n```cpp\nvoid* watch_count(void* t) { \n    int tid = (int) t; \n\n    pthread_mutex_lock(&count_mutex); \n    if (count < COUNT_LIMIT) { \n        pthread_cond_wait(&count_cv, &count_mutex); \n    } \n\n    pthread_mutex_unlock(&count_mutex); \n    pthread_exit(0); \n} \n\n```\n\n在第二个函数中，我们在检查是否已经达到计数限制之前锁定全局互斥体。这是我们的保险，以防运行这个函数的线程在计数达到极限之前没有被调用。\n\n否则，我们等待提供条件变量和锁定互斥的条件变量。一旦发出信号，我们就解锁全局互斥体，并退出线程。\n\n这里需要注意的一点是，这个例子没有考虑虚假唤醒。Pthreads 条件变量易受这种唤醒的影响，这需要使用循环来检查是否满足某种条件:\n\n```cpp\nint main (int argc, char* argv[]) { \n    int tid1 = 1, tid2 = 2, tid3 = 3; \n    pthread_t threads[3]; \n    pthread_attr_t attr; \n\n    pthread_mutex_init(&count_mutex, 0); \n    pthread_cond_init (&count_cv, 0); \n\n    pthread_attr_init(&attr); \n    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE); \n    pthread_create(&threads[0], &attr, watch_count, (void *) tid1); \n    pthread_create(&threads[1], &attr, add_count, (void *) tid2); \n    pthread_create(&threads[2], &attr, add_count, (void *) tid3); \n\n    for (int i = 0; i < 3; ++ i) { \n        pthread_join(threads[i], 0); \n    } \n\n    pthread_attr_destroy(&attr); \n    pthread_mutex_destroy(&count_mutex); \n    pthread_cond_destroy(&count_cv); \n    return 0; \n}  \n\n```\n\n最后，在`main`函数中，我们创建了三个线程，其中两个运行添加到计数器的函数，第三个运行等待其条件变量发出信号的函数。\n\n在这个方法中，我们还初始化了全局互斥体和条件变量。我们创建的线程进一步明确设置了“可连接”属性。\n\n最后，我们等待每个线程完成，之后我们进行清理，在退出之前销毁属性结构实例、互斥体和条件变量。\n\n使用`pthread_cond_broadcast()`函数，还可以向所有等待条件变量的线程发送信号，而不仅仅是队列中的第一个线程。这使我们能够在一些应用中更好地使用条件变量，比如有许多工作线程等待新数据集的到来，而不必单独通知每个线程。\n\n# 同步\n\n实现同步的功能以`pthread_rwlock_`或`pthread_barrier_`为前缀。这些实现了读/写锁和同步障碍。\n\n一个**读/写锁** ( **rwlock** )非常类似于一个互斥体，只是它有一个额外的特性，允许无限线程同时读取，同时只限制对单个线程的写访问。\n\n使用`rwlock`与使用互斥体非常相似:\n\n```cpp\n#include <pthread.h> \nint pthread_rwlock_init(pthread_rwlock_t* rwlock, const pthread_rwlockattr_t* attr); \npthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER; \n\n```\n\n在最后一段代码中，我们包含相同的通用头，并使用初始化函数或通用宏。有趣的是当我们锁定`rwlock`时，这可以只针对只读访问进行:\n\n```cpp\nint pthread_rwlock_rdlock(pthread_rwlock_t* rwlock); \nint pthread_rwlock_tryrdlock(pthread_rwlock_t* rwlock); \n\n```\n\n在这里，如果锁已经被锁定，那么第二个变化立即返回。还可以锁定它进行写访问，如下所示:\n\n```cpp\nint pthread_rwlock_wrlock(pthread_rwlock_t* rwlock); \nint pthread_rwlock_trywrlock(pthread_rwlock_t * rwlock); \n\n```\n\n这些函数的工作原理基本相同，只是在任何给定时间只允许一个编写器，而多个读取器可以获得一个只读锁。\n\n障碍是 Pthreads 的另一个概念。这些同步对象对许多线程来说就像一个屏障。所有这些都必须先到达障碍，然后才能通过。在屏障初始化函数中，指定线程计数。只有当所有这些线程都使用`pthread_barrier_wait()`函数调用了`barrier`对象时，它们才会继续执行。\n\n# 旗语\n\n如前所述，信号量不是 POSIX 规范最初 Pthreads 扩展的一部分。为此，它们在`semaphore.h`标题中声明。\n\n本质上，信号量是简单的整数，通常用作资源计数。为了使它们线程安全，使用了原子操作(检查和锁定)。POSIX 信号量支持信号量的初始化、销毁、递增和递减，以及等待信号量达到非零值。\n\n# 线程本地存储\n\n使用 Pthreads，TLS 是通过使用键和方法来设置线程特定的数据来实现的:\n\n```cpp\npthread_key_t global_var_key;\n\nvoid* worker(void* arg) {\n    int *p = new int;\n    *p = 1;\n    pthread_setspecific(global_var_key, p);\n    int* global_spec_var = (int*) pthread_getspecific(global_var_key);\n    *global_spec_var += 1;\n    pthread_setspecific(global_var_key, 0);\n    delete p;\n    pthread_exit(0);\n}\n\n```\n\n在工作线程中，我们在堆上分配一个新的整数，并将全局键设置为它自己的值。在将全局变量增加 1 之后，它的值将是 2，而不管其他线程做什么。一旦我们完成了这个线程，我们可以将全局变量设置为 0，并删除分配的值:\n\n```cpp\nint main(void) {\n    pthread_t threads[5];\n\n    pthread_key_create(&global_var_key, 0);\n    for (int i = 0; i < 5; ++ i)\n        pthread_create(&threads[i],0,worker,0);\n    for (int i = 0; i < 5; ++ i) {\n        pthread_join(threads[i], 0);\n    }\n    return 0;\n}\n\n```\n\n全局键被设置并用于引用 TLS 变量，但是我们创建的每个线程都可以为这个键设置自己的值。\n\n虽然线程可以创建自己的密钥，但是与我们在本章中看到的其他 API 相比，这种处理 TLS 的方法相当复杂。\n\n# Windows 线程\n\n相对于 Pthreads，Windows 线程仅限于 Windows 操作系统及类似系统(例如 ReactOS 和其他使用 Wine 的 OS)。这提供了一个相当一致的实现，很容易由支持对应的 Windows 版本来定义。\n\n在 Windows Vista 之前，线程支持错过的功能，如条件变量，而具有在 Pthreads 中找不到的功能。根据个人的观点，不得不使用由窗口标题定义的无数“类型定义”类型也是一件麻烦的事情。\n\n# 线程管理\n\n使用 Windows 线程的一个基本示例，改编自 MSDN 官方文档示例代码，如下所示:\n\n```cpp\n#include <windows.h> \n#include <tchar.h> \n#include <strsafe.h> \n\n#define MAX_THREADS 3 \n#define BUF_SIZE 255  \n\n```\n\n在为线程函数、字符串等包含一系列特定于窗口的头之后，我们定义了我们希望创建的线程数量以及`Worker`函数中消息缓冲区的大小。\n\n我们还定义了一个结构类型(由`void pointer: LPVOID`传递)来包含我们传递给每个工作线程的样本数据:\n\n```cpp\ntypedef struct MyData { \n int val1; \n int val2; \n} MYDATA, *PMYDATA;\n\nDWORD WINAPI worker(LPVOID lpParam) { \n    HANDLE hStdout = GetStdHandle(STD_OUTPUT_HANDLE); \n    if (hStdout == INVALID_HANDLE_VALUE) { \n        return 1; \n    } \n\n    PMYDATA pDataArray =  (PMYDATA) lpParam; \n\n    TCHAR msgBuf[BUF_SIZE]; \n    size_t cchStringSize; \n    DWORD dwChars; \n    StringCchPrintf(msgBuf, BUF_SIZE, TEXT(\"Parameters = %d, %dn\"),  \n    pDataArray->val1, pDataArray->val2);  \n    StringCchLength(msgBuf, BUF_SIZE, &cchStringSize); \n    WriteConsole(hStdout, msgBuf, (DWORD) cchStringSize, &dwChars, NULL); \n\n    return 0;  \n}  \n\n```\n\n在`Worker`函数中，我们将提供的参数转换为我们的自定义结构类型，然后使用它将其值打印为字符串，并在控制台上输出。\n\n我们还验证有一个活动的标准输出(控制台或类似的)。用于打印字符串的函数都是线程安全的。\n\n```cpp\nvoid errorHandler(LPTSTR lpszFunction) { \n    LPVOID lpMsgBuf; \n    LPVOID lpDisplayBuf; \n    DWORD dw = GetLastError();  \n\n    FormatMessage( \n        FORMAT_MESSAGE_ALLOCATE_BUFFER |  \n        FORMAT_MESSAGE_FROM_SYSTEM | \n        FORMAT_MESSAGE_IGNORE_INSERTS, \n        NULL, \n        dw, \n        MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), \n        (LPTSTR) &lpMsgBuf, \n        0, NULL); \n\n        lpDisplayBuf = (LPVOID) LocalAlloc(LMEM_ZEROINIT,  \n        (lstrlen((LPCTSTR) lpMsgBuf) + lstrlen((LPCTSTR) lpszFunction) + 40) * sizeof(TCHAR));  \n        StringCchPrintf((LPTSTR)lpDisplayBuf,  \n        LocalSize(lpDisplayBuf) / sizeof(TCHAR), \n        TEXT(\"%s failed with error %d: %s\"),  \n        lpszFunction, dw, lpMsgBuf);  \n        MessageBox(NULL, (LPCTSTR) lpDisplayBuf, TEXT(\"Error\"), MB_OK);  \n\n        LocalFree(lpMsgBuf); \n        LocalFree(lpDisplayBuf); \n} \n\n```\n\n这里定义了一个错误处理函数，它获取最后一个错误代码的系统错误消息。获取最后一个错误的代码后，要输出的错误消息被格式化，并显示在消息框中。最后，释放分配的内存缓冲区。\n\n最后，`main`功能如下:\n\n```cpp\nint _tmain() {\n         PMYDATA pDataArray[MAX_THREADS];\n         DWORD dwThreadIdArray[MAX_THREADS];\n         HANDLE hThreadArray[MAX_THREADS];\n         for (int i = 0; i < MAX_THREADS; ++ i) {\n               pDataArray[i] = (PMYDATA) HeapAlloc(GetProcessHeap(),\n                           HEAP_ZERO_MEMORY, sizeof(MYDATA));                     if (pDataArray[i] == 0) {\n                           ExitProcess(2);\n             }\n             pDataArray[i]->val1 = i;\n             pDataArray[i]->val2 = i+100;\n             hThreadArray[i] = CreateThread(\n                  NULL,          // default security attributes\n                  0,             // use default stack size\n                  worker,        // thread function name\n                  pDataArray[i], // argument to thread function\n                  0,             // use default creation flags\n                  &dwThreadIdArray[i]);// returns the thread identifier\n             if (hThreadArray[i] == 0) {\n                         errorHandler(TEXT(\"CreateThread\"));\n                         ExitProcess(3);\n             }\n   }\n         WaitForMultipleObjects(MAX_THREADS, hThreadArray, TRUE, INFINITE);\n         for (int i = 0; i < MAX_THREADS; ++ i) {\n               CloseHandle(hThreadArray[i]);\n               if (pDataArray[i] != 0) {\n                           HeapFree(GetProcessHeap(), 0, pDataArray[i]);\n               }\n         }\n         return 0;\n}\n\n```\n\n在`main`函数中，我们在一个循环中创建我们的线程，为线程数据分配内存，并在启动线程之前为每个线程生成唯一的数据。每个线程实例都被传递了自己唯一的参数。\n\n之后，我们等待线程完成并重新加入。这本质上与用 Pthreads 在单个线程上调用`join`函数相同——仅在这里，一次函数调用就足够了。\n\n最后，关闭每个线程句柄，我们清理之前分配的内存。\n\n# 高级管理\n\nWindows 线程的高级线程管理包括作业、纤维和线程池。作业本质上允许一个人将多个线程链接到一个单一的单元中，使一个人能够一次改变所有这些线程的属性和状态。\n\n纤维是轻量级的线，在制造它们的线的上下文中运行。创建线程需要自己调度这些光纤。纤维也有类似于 TLS 的**纤维本地存储** ( **FLS** )。\n\n最后，视窗线程应用编程接口提供了一个线程池应用编程接口，允许人们在自己的应用中轻松使用这样的线程池。每个进程还提供了一个默认线程池。\n\n# 同步\n\n使用 Windows 线程，互斥和同步可以使用关键部分、互斥体、信号量、**纤细的读/写器** ( **SRW** )锁、屏障和变体来完成。\n\n同步对象包括以下内容:\n\n| **名称** | **描述** |\n| 事件 | 允许使用命名对象在线程和进程之间发送事件信号。 |\n| 互斥（体）… | 用于线程间和进程同步，以协调对共享资源的访问。 |\n| 旗语 | 标准信号量计数器对象，用于线程间和进程同步。 |\n| 可等待计时器 | 计时器对象可由多个进程使用，具有多种使用模式。 |\n| 临界截面 | 关键部分本质上是互斥体，仅限于单个进程，由于缺少内核空间调用，这使得它们比使用互斥体更快。 |\n| 超薄读取器/写入器锁 | SRWs 类似于 Pthreads 中的读/写锁，允许多个读取器或单个写入器线程访问共享资源。 |\n| 互锁变量访问 | 允许对一系列变量进行原子访问，否则这些变量不能保证是原子的。这使得线程能够共享一个变量，而不必使用互斥体。 |\n\n# 条件变量\n\n用 Windows 线程实现条件变量相当简单。它使用临界区(`CRITICAL_SECTION`)和条件变量(`CONDITION_VARIABLE`)以及条件变量函数来等待特定的条件变量，或者发出信号。\n\n# 线程本地存储\n\n【Windows 线程的线程本地存储 ( **TLS** )与 Pthreads 类似，首先必须创建一个中心键(TLS 索引)，之后各个线程可以使用该全局索引来存储和检索本地值。\n\n与 Pthreads 一样，这也涉及类似数量的手动内存管理，因为 TLS 值必须手动分配和删除。\n\n# 促进\n\nBoost 线程是 Boost 库集合中相对较小的一部分。然而，它被用作 C++ 11 中多线程实现的基础，类似于其他 Boost 库最终如何将其全部或部分纳入新的 C++ 标准。有关多线程应用编程接口的详细信息，请参考本章中的 C++ 线程部分。\n\n在 Boost 线程中可用的 C++ 11 标准中缺少的特性包括:\n\n*   线程组(如 Windows 作业)\n*   线程中断(取消)\n*   超时线程连接\n*   其他互斥锁类型(用 C++ 14 改进)\n\n除非绝对需要这样的特性，或者如果不能使用支持 C++ 11 标准的编译器(包括 STL 线程)，那么在 C++ 11 实现上使用 Boost 线程是没有什么理由的。\n\n由于 Boost 提供了本机操作系统特性的包装器，因此根据 STL 实现的质量，使用本机 C++ 线程可能会减少开销。\n\n# 夸脱\n\nQt 是一个比较高级的框架，这也体现在它的多线程 API 上。Qt 的另一个定义特性是它包装了自己的代码(QApplication 和 QMainWindow)，并使用元编译器(`qmake`)来实现它的信号槽架构和框架的其他定义特性。\n\n因此，Qt 的线程支持不能按原样添加到现有代码中，而是需要修改代码以适应框架。\n\n# QThread\n\nQt 中的`QThread`类不是一个线程，而是一个围绕线程实例的广泛包装器，它增加了信号槽通信、运行时支持和其他特性。这反映在 QThread 的基本用法中，如以下代码所示:\n\n```cpp\nclass Worker : public QObject { \n    Q_OBJECT \n\n    public: \n        Worker(); \n        ~Worker(); \n\n    public slots: \n        void process(); \n\n    signals: \n        void finished(); \n        void error(QString err); \n\n    private: \n}; \n\n```\n\n前面这段代码是一个基本的`Worker`类，它将包含我们的业务逻辑。它源自`QObject`类，也允许我们使用信号槽和其他固有的`QObject`特性。信号槽体系结构的核心只是侦听器注册(连接)由 QObject 派生类声明的信号的一种方式，允许跨模块、跨线程和异步通信。\n\n它有一个单一的，可以被调用来开始处理，和两个信号-一个信号表示完成，一个信号表示错误。\n\n实现如下所示:\n\n```cpp\nWorker::Worker() { }  \nWorker::~Worker() { } \n\nvoid Worker::process() { \n    qDebug(\"Hello World!\"); \n    emit finished(); \n} \n\n```\n\n构造函数可以扩展到包括参数。任何堆分配的变量(使用`malloc`或`new`)必须在`process()`方法中分配，而不是在构造函数中分配，因为`Worker`实例将在线程上下文中操作，我们稍后会看到。\n\n要创建新的 QThread，我们将使用以下设置:\n\n```cpp\nQThread* thread = new QThread; \nWorker* worker = new Worker(); \nworker->moveToThread(thread); \nconnect(worker, SIGNAL(error(QString)), this, SLOT(errorString(QString))); \nconnect(thread, SIGNAL(started()), worker, SLOT(process())); \nconnect(worker, SIGNAL(finished()), thread, SLOT(quit())); \nconnect(worker, SIGNAL(finished()), worker, SLOT(deleteLater())); \nconnect(thread, SIGNAL(finished()), thread, SLOT(deleteLater())); \nthread->start(); \n\n```\n\n基本过程是在堆上创建一个新的 QThread 实例(这样它就不会超出范围)以及我们的`Worker`类的一个堆分配实例。然后，这个新的工作者将使用其`moveToThread()`方法被移动到新的线程实例。\n\n接下来，我们将把各种信号连接到相关的插槽，包括我们自己的`finished()`和`error()`信号。来自线程实例的`started()`信号将被连接到我们的工人上的槽，该槽将启动它。\n\n最重要的是，必须将来自工人的某种完成信号连接到螺纹上的`quit()`和`deleteLater()`槽。然后，来自螺纹的`finished()`信号将连接到工人身上的`deleteLater()`槽。这将确保工作线程和工作线程实例在工作线程完成后被清理。\n\n# 线程池\n\nQt 提供线程池。这些需要从`QRunnable`类继承一个，并实现`run()`函数。这个定制类的一个实例然后被传递给线程池的`start`方法(全局默认池，或者一个新的)。然后，这个工作线程的生命周期由线程池处理。\n\n# 同步\n\nQt 提供以下同步对象:\n\n*   `QMutex`\n*   `QReadWriteLock`\n*   `QSemaphore`\n*   `QWaitCondition`(条件变量)\n\n这些应该是不言自明的。Qt 信号槽体系结构的另一个很好的特性是，这些也允许一个人在线程之间异步通信，而不必关心底层的实现细节。\n\n# QtConcurrent\n\nQtConcurrent 命名空间包含高级应用编程接口，旨在使编写多线程应用成为可能，而不必关心低级细节。\n\n函数包括并发过滤和映射算法，以及允许在单独线程中运行函数的方法。所有这些都返回一个`QFuture`实例，其中包含异步操作的结果。\n\n# 线程本地存储\n\nQt 通过其`QThreadStorage`类提供 TLS。指针类型值的内存管理由它处理。通常，可以将某种数据结构设置为 TLS 值，以便每个线程存储多个值，例如在`QThreadStorage`类文档中描述的:\n\n```cpp\nQThreadStorage<QCache<QString, SomeClass> > caches; \n\nvoid cacheObject(const QString &key, SomeClass* object) { \n    caches.localData().insert(key, object); \n} \n\nvoid removeFromCache(const QString &key) { \n    if (!caches.hasLocalData()) { return; } \n\n    caches.localData().remove(key); \n} \n\n```\n\n# 少\n\n概念验证库是操作系统功能的轻量级包装。它不需要 C++ 11 兼容的编译器或任何类型的预编译或元编译。\n\n# 线程类\n\n`Thread`类是操作系统级线程的简单包装器。它采用从`Runnable`类继承的`Worker`类实例。官方文档对此提供了如下基本示例:\n\n```cpp\n#include \"Poco/Thread.h\" \n#include \"Poco/Runnable.h\" \n#include <iostream> \n\nclass HelloRunnable: public Poco::Runnable { \n    virtual void run() { \n        std::cout << \"Hello, world!\" << std::endl; \n    } \n}; \n\nint main(int argc, char** argv) { \n    HelloRunnable runnable; \n    Poco::Thread thread; \n    thread.start(runnable); \n    thread.join(); \n    return 0; \n} \n\n```\n\n前面的代码是一个非常简单的“Hello world”示例，工作人员只通过标准输出输出一个字符串。线程实例被分配在堆栈上，并保持在入口函数的范围内，等待工作者完成使用`join()`函数。\n\n由于它的许多线程功能，POCO 很像 Pthreads，尽管它在配置线程和其他对象等方面确实有很大的不同。作为一个 C++ 库，它使用类方法设置属性，而不是填充结构并将其作为参数传递。\n\n# 线程池\n\nPOCO 提供了 16 个线程的默认线程池。这个数字可以动态改变。与普通线程一样，线程池需要传递一个从`Runnable`类继承而来的`Worker`类实例:\n\n```cpp\n#include \"Poco/ThreadPool.h\" \n#include \"Poco/Runnable.h\" \n#include <iostream> \n\nclass HelloRunnable: public Poco::Runnable { \n    virtual void run() { \n        std::cout << \"Hello, world!\" << std::endl; \n    } \n}; \n\nint main(int argc, char** argv) { \n    HelloRunnable runnable; \n    Poco::ThreadPool::defaultPool().start(runnable); \n    Poco::ThreadPool::defaultPool().joinAll(); \n    return 0; \n} \n\n```\n\n工作实例被添加到运行它的线程池中。当我们添加另一个工作实例、改变容量或调用`joinAll()`时，线程池会清理已经空闲了一定时间的线程。结果，单个工作线程将加入，并且没有活动线程，应用退出。\n\n# 线程本地存储\n\n使用 POCO，TLS 被实现为一个类模板，允许人们在几乎任何类型中使用它。\n\n如官方文件所详述:\n\n```cpp\n#include \"Poco/Thread.h\" \n#include \"Poco/Runnable.h\" \n#include \"Poco/ThreadLocal.h\" \n#include <iostream> \n\nclass Counter: public Poco::Runnable { \n    void run() { \n        static Poco::ThreadLocal<int> tls; \n        for (*tls = 0; *tls < 10; ++(*tls)) { \n            std::cout << *tls << std::endl; \n        } \n    } \n}; \n\nint main(int argc, char** argv) { \n    Counter counter1; \n    Counter counter2; \n    Poco::Thread t1; \n    Poco::Thread t2; \n    t1.start(counter1); \n    t2.start(counter2); \n    t1.join(); \n    t2.join(); \n    return 0; \n} \n\n```\n\n在前面的工作示例中，我们使用`ThreadLocal`类模板创建了一个静态 TLS 变量，并将其定义为包含一个整数。\n\n因为我们将其定义为静态的，所以每个线程只会创建一次。为了使用我们的 TLS 变量，我们可以使用箭头(`->`)或星号(`*`)运算符来访问它的值。在本例中，我们在`for`循环的每个周期增加一次 TLS 值，直到达到极限。\n\n这个例子演示了两个线程将生成它们自己的 10 个整数的序列，通过相同的数字计数而不会相互影响。\n\n# 同步\n\nPOCO 提供的同步原语如下所示:\n\n*   互斥（体）…\n*   快速互斥体\n*   事件\n*   情况\n*   旗语\n*   鲁洛克\n\n这里值得注意的是`FastMutex`班。这通常是非递归互斥类型，除了在 Windows 上，它是递归的。这意味着人们通常应该假设这两种类型都是递归的，因为同一线程可以多次锁定同一个互斥体。\n\n也可以在`ScopedLock`类中使用互斥体，这确保了它封装的互斥体在当前范围的末尾被释放。\n\n事件类似于窗口事件，只是它们仅限于单个进程。它们构成了概念验证中条件变量的基础。\n\nPOCO 条件变量的功能与 Pthreads 和其他变量非常相似，只是它们不会受到虚假唤醒的影响。通常情况下，由于优化原因，条件变量会受到这些随机唤醒的影响。由于不必在条件变量 wait 返回时显式检查其条件是否满足，因此给开发人员带来了更少的负担。\n\n# C++ 线程\n\n在[第 5 章](05.html)、*原生 C++ 线程和原语*中广泛介绍了 C++ 中的原生多线程支持。\n\n正如本章前面的 Boost 部分提到的，C++ 多线程支持在很大程度上基于 Boost 线程 API，使用几乎相同的头和名称。该应用编程接口本身再次让人想起 Pthreads，尽管在条件变量等方面有很大的不同。\n\n接下来的章节将专门使用 C++ 线程支持作为示例。\n\n# 把它放在一起\n\n在本章介绍的 API 中，只有 Qt 多线程 API 可以被认为是真正的高级。虽然其他 API(包括 C++ 11)有一些更高级的概念，包括线程池和异步运行器，它们不需要一个直接使用线程，但是 Qt 提供了一个成熟的信号槽架构，这使得线程间的通信异常容易。\n\n正如本章所述，这种便利也是有代价的，即必须开发自己的应用来适应 Qt 框架。根据项目的不同，这可能不可接受。\n\n这些 API 中哪一个是正确的取决于一个人的需求。然而，相对公平地说，当一个人可以使用诸如 C++ 11 线程、POCO 等 API 时，使用直接的 Pthreads、Windows 线程和 kin 没有太大意义，这些 API 在不显著降低性能的情况下简化了开发过程，同时还获得了跨平台的广泛可移植性。\n\n所有的应用编程接口至少在它们的核心特性上有一定的可比性。\n\n# 摘要\n\n在本章中，我们详细介绍了一些比较流行的多线程 API 和框架，将它们放在一起，以便了解它们的优缺点。我们通过一些例子展示了如何使用这些 API 实现基本功能。\n\n在下一章中，我们将详细介绍如何同步线程以及它们之间的通信。"
  },
  {
    "path": "docs/master-cpp-multithrd/04.md",
    "content": "# 四、线程同步和通信\n\n虽然一般来说，线程或多或少独立于其他线程来处理任务，但在许多情况下，人们会希望在线程之间传递数据，甚至控制其他线程，例如从中央任务调度器线程。本章介绍如何使用 C++ 11 线程应用编程接口完成这些任务。\n\n本章涵盖的主题包括:\n\n*   使用互斥、锁和类似的同步结构\n*   使用条件变量和信号来控制线程\n*   在线程之间安全地传递和共享数据\n\n# 安全第一\n\n并发的核心问题是确保对共享资源的安全访问，即使在线程间通信时也是如此。还有线程能够相互通信和同步的问题。\n\n多线程编程之所以面临如此大的挑战，是因为能够跟踪线程之间的每一次交互，并确保每一种形式的访问都是安全的，同时不会陷入死锁和数据竞争的陷阱。\n\n在本章中，我们将看一个相当复杂的涉及任务调度器的例子。这是一种高并发、高吞吐量的情况，其中许多不同的需求与许多潜在的陷阱结合在一起，我们稍后会看到。\n\n# 调度程序\n\n线程间大量同步和通信的多线程的一个很好的例子是任务调度。这里，目标是接受传入的任务，并尽快将它们分配给工作线程。\n\n在这种情况下，许多不同的方法都是可能的。通常有工作线程在活动循环中运行，不断轮询中央队列以寻找新任务。这种方法的缺点包括在所述轮询上浪费处理器周期，以及在所使用的同步机制(通常是互斥体)上形成拥塞。此外，当工作线程数量增加时，这种主动轮询方法的扩展性非常差。\n\n理想情况下，每个工作线程都会无所事事地等待，直到再次需要它。为了实现这一点，我们必须从另一个角度来处理这个问题:不是从工作线程的角度，而是从队列的角度。很像操作系统的调度程序，它知道需要处理的任务以及可用的工作线程。\n\n在这种方法中，中央调度器实例将接受新任务，并主动将它们分配给工作线程。所述调度器实例还可以管理这些工作线程，例如它们的数量和优先级，这取决于传入任务的数量和任务的类型或其他属性。\n\n# 高级视图\n\n就其核心而言，我们的调度程序或调度程序非常简单，就像一个队列，所有的调度逻辑都内置在其中，如下图所示:\n\n![](img/e317a2ab-0210-4213-a38a-69e26322d3eb.png)\n\n从前面的高级视图中可以看出，这并没有什么大不了的。然而，正如我们稍后将看到的，实际实现确实有许多复杂之处。\n\n# 履行\n\n像往常一样，我们从包含在`main.cpp`中的`main`功能开始:\n\n```cpp\n#include \"dispatcher.h\"\n#include \"request.h\"\n\n#include <iostream>\n#include <string>\n#include <csignal>\n#include <thread>\n#include <chrono>\n\nusing namespace std;\n\nsig_atomic_t signal_caught = 0;\nmutex logMutex; \n\n```\n\n我们包含的自定义头是我们的调度器实现的头，以及我们将使用的`request`类。\n\n在全局范围内，我们定义了一个与信号处理程序一起使用的原子变量，以及一个将同步我们的日志记录方法的输出(在标准输出上)的互斥体:\n\n```cpp\nvoid sigint_handler(int sig) {\n    signal_caught = 1;\n} \n\n```\n\n我们的信号处理函数(针对`SIGINT`信号)只是设置了我们之前定义的全局原子变量:\n\n```cpp\nvoid logFnc(string text) {\n    logMutex.lock();\n    cout << text << \"\\n\";\n    logMutex.unlock();\n} \n\n```\n\n在我们的日志记录函数中，我们使用全局互斥来确保对标准输出的写入是同步的:\n\n```cpp\nint main() {\n    signal(SIGINT, &sigint_handler);\n    Dispatcher::init(10); \n\n```\n\n在`main`功能中，我们为`SIGINT`安装了信号处理程序，允许我们中断应用的执行。我们还调用`Dispatcher`类上的静态`init()`函数来初始化它:\n\n```cpp\n    cout << \"Initialised.\\n\";\n        int cycles = 0;\n    Request* rq = 0;\n    while (!signal_caught && cycles < 50) {\n        rq = new Request();\n        rq->setValue(cycles);\n        rq->setOutput(&logFnc);\n        Dispatcher::addRequest(rq);\n        cycles++ ;\n    } \n\n```\n\n接下来，我们设置循环，在其中我们将创建新的请求。在每个周期中，我们创建一个新的`Request`实例，并使用其`setValue()`函数设置一个整数值(当前周期数)。在使用静态`addRequest()`功能向`Dispatcher`添加这个新请求之前，我们还在请求实例上设置了日志记录功能。\n\n该循环将继续，直到达到最大循环数，或使用 *Ctrl* + *C* 或类似方法发出`SIGINT`信号:\n\n```cpp\n        this_thread::sleep_for(chrono::seconds(5));\n        Dispatcher::stop();\n    cout << \"Clean-up done.\\n\";\n    return 0; \n} \n\n```\n\n最后，我们使用线程的`sleep_for()`功能和来自`chrono` STL 头的`chrono::seconds()`功能等待 5 秒钟。\n\n我们在返回之前也调用`Dispatcher`上的`stop()`函数。\n\n# 请求类别\n\n对`Dispatcher`的请求总是来自纯虚拟的`AbstractRequest`类:\n\n```cpp\n#pragma once\n#ifndef ABSTRACT_REQUEST_H\n#define ABSTRACT_REQUEST_H\n\nclass AbstractRequest {\n    //\n    public:\n    virtual void setValue(int value) = 0;\n    virtual void process() = 0;\n    virtual void finish() = 0;\n};\n#endif \n\n```\n\n这个`AbstractRequest`类定义了一个具有三个函数的 API，一个派生类总是要实现这三个函数。在这些函数中，`process()`和`finish()`函数是最通用的，并且可能在任何实际实现中使用。`setValue()`功能专用于本演示实施，可能会进行调整或扩展以适应现实生活场景。\n\n使用抽象类作为请求基础的优势在于，它允许`Dispatcher`类处理许多不同类型的请求，只要它们都遵守这个相同的基本 API。\n\n使用这个抽象接口，我们实现了一个基本的`Request`类，如下所示:\n\n```cpp\n#pragma once\n#ifndef REQUEST_H\n#define REQUEST_H\n\n#include \"abstract_request.h\"\n\n#include <string>\n\nusing namespace std;\n\ntypedef void (*logFunction)(string text);\n\nclass Request : public AbstractRequest {\n    int value;\n    logFunction outFnc;\n    public:    void setValue(int value) { this->value = value; }\n    void setOutput(logFunction fnc) { outFnc = fnc; }\n    void process();\n    void finish();\n};\n#endif \n\n```\n\n在其头文件中，我们首先定义函数指针的格式。之后，我们实现请求 API，并在基础 API 中添加`setOutput()`函数，接受一个函数指针进行日志记录。这两个 setter 函数只是将提供的参数分配给它们各自的私有类成员。\n\n接下来，类函数实现如下:\n\n```cpp\n#include \"request.h\"\nvoid Request::process() {\n    outFnc(\"Starting processing request \" + std::to_string(value) + \"...\");\n    //\n}\nvoid Request::finish() {\n    outFnc(\"Finished request \" + std::to_string(value));\n} \n\n```\n\n这两种实现都非常基础；他们只是使用函数指针来输出一个指示工作线程状态的字符串。\n\n在实际实现中，可以将业务逻辑添加到`process()`函数中，其中`finish()`函数包含完成请求的任何功能，例如将地图写入字符串。\n\n# 工人阶级\n\n接下来是`Worker`班。这包含将由`Dispatcher`调用以处理请求的逻辑。\n\n```cpp\n#pragma once\n#ifndef WORKER_H\n#define WORKER_H\n\n#include \"abstract_request.h\"\n\n#include <condition_variable>\n#include <mutex>\n\nusing namespace std;\n\nclass Worker {\n    condition_variable cv;\n    mutex mtx;\n    unique_lock<mutex> ulock;\n    AbstractRequest* request;\n    bool running;\n    bool ready;\n    public:\n    Worker() { running = true; ready = false; ulock = unique_lock<mutex>(mtx); }\n    void run();\n    void stop() { running = false; }\n    void setRequest(AbstractRequest* request) { this->request = request; ready = true; }\n    void getCondition(condition_variable* &cv);\n};\n#endif \n\n```\n\n虽然向`Dispatcher`添加请求不需要任何特殊的逻辑，但是`Worker`类确实需要使用条件变量来与调度程序同步。对于 C++ 11 线程 API，这需要一个条件变量、一个互斥体和一个唯一锁。\n\n唯一锁封装了互斥锁，最终将与条件变量一起使用，我们稍后会看到。\n\n除此之外，我们定义方法来启动和停止工作进程，设置新的处理请求，并获取对其内部条件变量的访问。\n\n接下来，实现的其余部分编写如下:\n\n```cpp\n#include \"worker.h\"\n#include \"dispatcher.h\"\n\n#include <chrono>\n\nusing namespace std;\n\nvoid Worker::getCondition(condition_variable* &cv) {\n    cv = &(this)->cv;\n}\n\nvoid Worker::run() {\n    while (running) {\n        if (ready) {\n            ready = false;\n            request->process();\n            request->finish();\n        }\n        if (Dispatcher::addWorker(this)) {\n            // Use the ready loop to deal with spurious wake-ups.\n            while (!ready && running) {\n                if (cv.wait_for(ulock, chrono::seconds(1)) == cv_status::timeout) {\n                    // We timed out, but we keep waiting unless  \n                    // the worker is \n                    // stopped by the dispatcher. \n                }\n            }\n        }\n    }\n} \n\n```\n\n除了条件变量的`getter`函数外，我们定义了`run()`函数，`dispatcher`将在启动时为每个工作线程运行该函数。\n\n它的主循环只是检查`stop()`函数还没有被调用，这会将运行的布尔值设置为`false`，并结束工作线程。这由`Dispatcher`在关闭时使用，允许它终止工作线程。因为布尔值通常是原子的，所以设置和检查可以同时进行，而没有风险或不需要互斥。\n\n继续，对`ready`变量的检查是为了确保在线程第一次运行时请求实际上正在等待。在工作线程的第一次运行中，不会有任何请求等待，因此，试图处理一个请求会导致崩溃。在`Dispatcher`设置新请求时，该布尔变量将被设置为`true`。\n\n如果请求正在等待，则`ready`变量将再次设置为`false`，之后请求实例将调用其`process()`和`finish()`函数。这将在工作线程的线程上运行请求的业务逻辑，并最终完成它。\n\n最后，工作线程使用其静态`addWorker()`函数将自己添加到调度程序中。如果没有新的请求可用，该函数将返回`false`，并使工作线程等待直到有新的请求可用。否则，工作线程将继续处理`Dispatcher`在其上设置的新请求。\n\n如果被要求等待，我们进入一个新的循环。这个循环将确保当条件变量被唤醒时，这是因为我们得到了`Dispatcher` ( `ready`变量设置为`true`)的信号，而不是因为虚假的唤醒。\n\n最后，我们使用之前创建的唯一锁实例以及超时时间来输入条件变量的实际`wait()`函数。如果发生超时，我们可以终止线程，或者继续等待。在这里，我们选择什么都不做，只是重新进入等待循环。\n\n# 分配器\n\n作为最后一项，我们有`Dispatcher`类本身:\n\n```cpp\n    #pragma once\n    #ifndef DISPATCHER_H\n    #define DISPATCHER_H\n\n    #include \"abstract_request.h\"\n    #include \"worker.h\"\n\n    #include <queue>\n    #include <mutex>\n    #include <thread>\n    #include <vector>\n\n    using namespace std;\n\n    class Dispatcher {\n        static queue<AbstractRequest*> requests;\n        static queue<Worker*> workers;\n        static mutex requestsMutex;\n        static mutex workersMutex;\n        static vector<Worker*> allWorkers;\n        static vector<thread*> threads;\n        public:\n        static bool init(int workers);\n        static bool stop();\n        static void addRequest(AbstractRequest* request);\n        static bool addWorker(Worker* worker);\n     };\n     #endif \n\n```\n\n大部分看起来都很熟悉。正如你现在已经猜到的，这是一个完全静态的类。\n\n接下来，它的实现如下:\n\n```cpp\n    #include \"dispatcher.h\"\n\n    #include <iostream>\n    using namespace std;\n\n    queue<AbstractRequest*> Dispatcher::requests;\n    queue<Worker*> Dispatcher::workers;\n    mutex Dispatcher::requestsMutex;\n    mutex Dispatcher::workersMutex;\n    vector<Worker*> Dispatcher::allWorkers;\n    vector<thread*> Dispatcher::threads; \n\n    bool Dispatcher::init(int workers) {\n        thread* t = 0;\n        Worker* w = 0;\n        for (int i = 0; i < workers; ++ i) {\n            w = new Worker;\n            allWorkers.push_back(w);\n            t = new thread(&Worker::run, w);\n            threads.push_back(t);\n        }\n   return true;\n } \n\n```\n\n设置静态类成员后，定义`init()`函数。它启动指定数量的工作线程，在各自的向量数据结构中保持对每个工作线程和线程实例的引用:\n\n```cpp\n    bool Dispatcher::stop() {\n        for (int i = 0; i < allWorkers.size(); ++ i) {\n            allWorkers[i]->stop();\n        }\n            cout << \"Stopped workers.\\n\";\n            for (int j = 0; j < threads.size(); ++ j) {\n            threads[j]->join();\n                    cout << \"Joined threads.\\n\";\n        }\n    }\n\n```\n\n在`stop()`函数中，每个工作实例都有其被调用的`stop()`函数。这将导致每个工作线程终止，正如我们之前在`Worker`类描述中看到的。\n\n最后，在返回之前，我们等待每个线程加入(即完成):\n\n```cpp\n    void Dispatcher::addRequest(AbstractRequest* request) {\n        workersMutex.lock();\n        if (!workers.empty()) {\n            Worker* worker = workers.front();\n            worker->setRequest(request);\n            condition_variable* cv;\n            worker->getCondition(cv);\n            cv->notify_one();\n            workers.pop();\n            workersMutex.unlock();\n        }\n        else {\n            workersMutex.unlock();\n            requestsMutex.lock();\n            requests.push(request);\n            requestsMutex.unlock();\n        }\n    } \n\n```\n\n`addRequest()`功能是事情变得有趣的地方。在这个函数中，添加了一个新的请求。接下来会发生什么取决于工作线程是否在等待新的请求。如果没有工作线程在等待(工作队列为空)，请求将被添加到请求队列中。\n\n互斥锁的使用确保了对这些队列的访问安全进行，因为工作线程将同时尝试访问这两个队列。\n\n这里需要注意的一个重要问题是僵局的可能性。也就是说，两个线程将持有一个资源的锁，第二个线程等待第一个线程释放它的锁，然后再释放它自己的锁。在单个作用域中使用多个互斥体的每种情况都有这种可能性。\n\n在这个函数中，死锁的可能性在于释放工作互斥体上的锁，并且当获得请求互斥体上的锁时。在这个函数持有 workers 互斥体并试图获得 requests 锁的情况下(当没有 worker 线程可用时)，有可能另一个线程持有 requests 互斥体(寻找新的请求来处理)，同时试图获得 workers 互斥体(没有找到请求并将其自身添加到 workers 队列中)。\n\n这里的解决方案很简单:在获取下一个互斥体之前释放一个互斥体。在感觉必须持有多个互斥锁的情况下，检查和测试代码中潜在的死锁是最重要的。在这种特殊情况下，当不再需要工作互斥锁时，或者在获得请求互斥锁之前，会显式释放工作互斥锁，从而防止死锁。\n\n这段代码的另一个重要方面是它向工作线程发出信号的方式。正如在 if/else 块的第一部分中可以看到的那样，当 workers 队列不为空时，从队列中取出一个 worker，对其设置请求，然后引用其条件变量并发出信号或通知。\n\n在内部，条件变量使用我们之前在`Worker`类定义中交给它的互斥体来保证对它的原子访问。当对条件变量调用`notify_one()`函数(在其他 API 中一般称为`signal()`)时，它会通知等待条件变量返回并继续的线程队列中的第一个线程。\n\n在`Worker`类`run()`函数中，我们将等待这个通知事件。收到请求后，工作线程将继续处理新的请求。线程引用将从队列中移除，直到它处理完请求后再次添加自己:\n\n```cpp\n    bool Dispatcher::addWorker(Worker* worker) {\n        bool wait = true;\n        requestsMutex.lock();\n        if (!requests.empty()) {\n            AbstractRequest* request = requests.front();\n            worker->setRequest(request);\n            requests.pop();\n            wait = false;\n            requestsMutex.unlock();\n        }\n        else {\n            requestsMutex.unlock();\n            workersMutex.lock();\n            workers.push(worker);\n            workersMutex.unlock();\n        }\n            return wait;\n    } \n\n```\n\n使用最后一个函数，工作线程将在处理完请求后将其自身添加到队列中。它类似于前面的函数，因为传入的工作进程首先与可能在请求队列中等待的任何请求进行主动匹配。如果没有可用的工作队列，则该工作队列将被添加到工作队列中。\n\n这里需要注意的是，我们返回一个布尔值，该值指示调用线程是否应该等待一个新的请求，或者它是否已经在尝试将自己添加到队列时收到了一个新的请求。\n\n虽然这段代码没有前一个函数复杂，但由于在同一范围内处理两个互斥体，它仍然存在同样的潜在死锁问题。在这里，我们也首先释放我们持有的互斥体，然后再获取下一个互斥体。\n\n# Makefile\n\n这个`Dispatcher`例子的 makefile 还是很基础的——它收集了当前文件夹中的所有 C++ 源文件，并使用`g++ `将其编译成二进制文件:\n\n```cpp\n    GCC := g++\n\n    OUTPUT := dispatcher_demo\n    SOURCES := $(wildcard *.cpp)\n    CCFLAGS := -std=c++ 11 -g3\n\n    all: $(OUTPUT)\n        $(OUTPUT):\n        $(GCC) -o $(OUTPUT) $(CCFLAGS) $(SOURCES)\n        clean:\n        rm $(OUTPUT)\n        .PHONY: all\n\n```\n\n# 输出\n\n编译应用后，运行它会为总共 50 个请求生成以下输出:\n\n```cpp\n    $ ./dispatcher_demo.exe\n    Initialised.\n    Starting processing request 1...\n    Starting processing request 2...\n    Finished request 1\n    Starting processing request 3...\n    Finished request 3\n    Starting processing request 6...\n    Finished request 6\n    Starting processing request 8...\n    Finished request 8\n    Starting processing request 9...\n    Finished request 9\n    Finished request 2\n    Starting processing request 11...\n    Finished request 11\n    Starting processing request 12...\n    Finished request 12\n    Starting processing request 13...\n    Finished request 13\n    Starting processing request 14...\n    Finished request 14\n    Starting processing request 7...\n    Starting processing request 10...\n    Starting processing request 15...\n    Finished request 7\n    Finished request 15\n    Finished request 10\n    Starting processing request 16...\n    Finished request 16\n    Starting processing request 17...\n    Starting processing request 18...\n    Starting processing request 0...\n\n```\n\n在这一点上，我们已经可以清楚地看到，即使每个请求几乎不需要花费时间来处理，这些请求显然是并行执行的。第一个请求(请求 0)仅在第十六个请求之后才开始处理，而第二个请求早在第九个请求之后就已经完成了。\n\n决定首先处理哪个线程以及哪个请求的因素取决于操作系统调度程序和基于硬件的调度，如[第 2 章](02.html)、*处理器和操作系统上的多线程实现*所述。这清楚地表明，即使在单个平台上，对多线程应用的执行方式也几乎没有什么假设。\n\n```cpp\n    Starting processing request 5...\n    Finished request 5\n    Starting processing request 20...\n    Finished request 18\n    Finished request 20\n    Starting processing request 21...\n    Starting processing request 4...\n    Finished request 21\n    Finished request 4   \n\n```\n\n在前面的代码中，第四个和第五个请求也以延迟的方式结束。\n\n```cpp\n\n    Starting processing request 23...\n    Starting processing request 24...\n    Starting processing request 22...\n    Finished request 24\n    Finished request 23\n    Finished request 22\n    Starting processing request 26...\n    Starting processing request 25...\n    Starting processing request 28...\n    Finished request 26\n    Starting processing request 27...\n    Finished request 28\n    Finished request 27\n    Starting processing request 29...\n    Starting processing request 30...\n    Finished request 30\n    Finished request 29\n    Finished request 17\n    Finished request 25\n    Starting processing request 19...\n    Finished request 0\n\n```\n\n至此，第一个请求终于完成。这可能表明与连续请求相比，第一个请求的初始化时间将总是被延迟。多次运行应用可以证实这一点。重要的是，如果处理的顺序是相关的，这种随机性不会对一个人的应用产生负面影响。\n\n```cpp\n    Starting processing request 33...\n    Starting processing request 35...\n    Finished request 33\n    Finished request 35\n    Starting processing request 37...\n    Starting processing request 38...\n    Finished request 37\n    Finished request 38\n    Starting processing request 39...\n    Starting processing request 40...\n    Starting processing request 36...\n    Starting processing request 31...\n    Finished request 40\n    Finished request 39\n    Starting processing request 32...\n    Starting processing request 41...\n    Finished request 32\n    Finished request 41\n    Starting processing request 42...\n    Finished request 31\n    Starting processing request 44...\n    Finished request 36\n    Finished request 42\n    Starting processing request 45...\n    Finished request 44\n    Starting processing request 47...\n    Starting processing request 48...\n    Finished request 48\n    Starting processing request 43...\n    Finished request 47\n    Finished request 43\n    Finished request 19\n    Starting processing request 34...\n    Finished request 34\n    Starting processing request 46...\n    Starting processing request 49...\n    Finished request 46\n    Finished request 49\n    Finished request 45\n\n```\n\n请求 19 也变得相当延迟，再次表明多线程应用是多么不可预测。如果我们在这里并行处理一个大数据集，每个请求中有大量数据，我们可能必须在某些时候暂停，以解决这些延迟，否则，我们的输出缓存可能会变得太大。\n\n因为这样做会对应用的性能产生负面影响，所以人们可能不得不关注低级优化，以及特定处理器内核上的线程调度，以防止这种情况发生。\n\n```cpp\n    Stopped workers.\n    Joined threads.\n    Joined threads.\n    Joined threads.\n    Joined threads.\n    Joined threads.\n    Joined threads.\n    Joined threads.\n    Joined threads.\n    Joined threads.\n    Joined threads.\n    Clean-up done.\n\n```\n\n开始时启动的所有 10 个工作线程在这里终止，我们称之为`Dispatcher`的`stop()`函数。\n\n# 共享数据\n\n在本章给出的示例中，我们看到了除了同步线程之外，如何在线程之间共享信息——这表现为我们从主线程传递到调度器的请求的形式，每个请求从调度器传递到不同的线程。\n\n线程之间共享数据背后的基本思想是，要共享的数据以两个或更多线程都可以访问的方式存在于某个地方。在此之后，我们必须确保只有一个线程可以修改数据，并且数据在被读取时不会被修改。通常，我们会使用互斥体或类似的东西来确保这一点。\n\n# 使用拆装锁\n\n读写锁在这里是一种可能的优化，因为它们允许多个线程同时从单个数据源读取。如果一个应用中有多个工作线程重复读取相同的信息，那么使用读写锁会比使用基本互斥锁更有效，因为读取数据的尝试不会阻塞其他线程。\n\n因此，读写锁可以用作互斥锁的更高级版本，也就是说，使其行为适应访问类型。在内部，它建立在互斥体(或信号量)和条件变量上。\n\n# 使用共享指针\n\n共享指针首先通过 Boost 库提供，并在 C++ 11 中引入，它是内存管理的抽象，对堆分配的实例使用引用计数。它们是部分线程安全的，因为可以创建多个共享指针实例，但是被引用的对象本身不是线程安全的。\n\n然而，根据应用，这可能就足够了。为了使它们适当地线程安全，可以使用原子。我们将在[第 8 章](08.html)、*原子操作-使用硬件*中更详细地了解这一点。\n\n# 摘要\n\n在本章中，我们研究了如何以安全的方式在线程之间传递数据，作为相当复杂的调度器实现的一部分。我们还查看了所述调度程序的异步处理结果，并考虑了一些在线程间传递数据的潜在替代方案和优化。\n\n此时，您应该能够在线程之间安全地传递数据，并同步对其他共享资源的访问。\n\n在下一章中，我们将研究本机 C++ 线程和原语 API。"
  },
  {
    "path": "docs/master-cpp-multithrd/05.md",
    "content": "# 五、本机 C++ 线程和原语\n\n从 2011 年 C++ 标准修订版开始，多线程 API 正式成为 C++ **标准模板库** ( **STL** )的一部分。这意味着任何新的 C++ 应用都可以使用线程、线程原语和同步机制，而不需要安装第三方库，也不需要依赖操作系统的 API。\n\n本章将介绍该本机 API 中提供的多线程功能，以及 2014 标准增加的功能。将显示一些详细使用这些功能的示例。\n\n本章的主题包括以下内容:\n\n*   C++ 的 STL 中多线程应用编程接口所涵盖的特性\n*   每个功能使用的详细示例\n\n# STL 线程应用编程接口\n\n在[第 3 章](03.html)、 *C++ 多线程 API*中，我们查看了开发多线程 C++ 应用时可用的各种 API。在[第 4 章](04.html)、*线程同步和通信*中，我们使用本机 C++ 线程应用编程接口实现了一个多线程调度器应用。\n\n# 助推。线程应用编程接口\n\n通过包含来自 STL 的`<thread>`头，我们获得了对`std::thread`类的访问，该类具有由其他头提供的互斥(互斥等)工具。这个应用编程接口本质上与`Boost.Thread`**的多线程应用编程接口相同，主要区别在于对线程的更多控制(通过超时、线程组和线程中断进行连接)，以及在诸如互斥体和条件变量等原语之上实现的一些额外的锁类型。**\n\n **一般来说，当 C++ 11 不支持时，或者当这些额外的`Boost.Thread`特性是一个人的应用的要求，并且不容易添加时，`Boost.Thread`应该作为一个后备。由于`Boost.Thread`建立在可用(本机)线程支持的基础上，与 C++ 11 STL 实现相比，它也可能增加开销。\n\n# 2011 年标准\n\nC++ 标准(通常称为 C++ 11)的 2011 年修订版增加了广泛的新功能，其中最关键的是增加了本机多线程支持，它增加了在 C++ 中创建、管理和使用线程的能力，而无需使用第三方库。\n\n该标准将核心语言的内存模型标准化，以允许多线程共存，并支持线程本地存储等功能。最初的支持是在 C++ 03 标准中添加的，但 C++ 11 标准是第一个充分利用这一点的标准。\n\n如前所述，实际的线程应用编程接口本身是在 STL 中实现的。C++ 11 (C++ 0x)标准的目标之一是在 STL 中拥有尽可能多的新特性，而不是作为核心语言的一部分。因此，为了使用线程、互斥体和同族，必须首先包含相关的 STL 头。\n\n致力于新多线程应用编程接口的标准委员会每个人都有自己的目标集，因此，一些人想要的一些特性没有进入最终标准。这包括终止另一个线程或线程取消等功能，POSIX 代表强烈反对这种做法，因为取消线程可能会导致正在销毁的线程中的资源清理问题。\n\n以下是此应用编程接口实现提供的功能:\n\n*   `std::thread`\n*   `std::mutex`\n*   `std::recursive_mutex`\n*   `std::condition_variable`\n*   `std::condition_variable_any`\n*   `std::lock_guard`\n*   `std::unique_lock`\n*   `std::packaged_task`\n*   `std::async`\n*   `std::future`\n\n稍后，我们将查看这些特性的详细示例。首先，我们将看到 C++ 标准的下一个修订版对这个初始集增加了什么。\n\n# C++ 14\n\n2014 标准向标准库中添加了以下功能:\n\n*   `std::shared_lock`\n*   `std::shared_timed_mutex`\n\n这两者都在`<shared_mutex>` STL 标题中定义。因为锁是基于互斥的，所以共享锁依赖于共享互斥。\n\n# C++ 17\n\n2017 标准为标准库增加了另一组功能，即:\n\n*   `std::shared_mutex`\n*   `std::scoped_lock`\n\n这里，作用域锁是一个互斥包装器，它提供了一个 RAII 风格的机制，在作用域块期间拥有一个互斥体。\n\n# STL 组织\n\n在 STL 中，我们可以找到以下标题组织及其提供的功能:\n\n| 页眉 | 提供 |\n| `<thread>` | `std::thread`班。`std::this_thread`命名空间下的方法:\n\n*   `yield`\n*   `get_id`\n*   `sleep_for`\n*   `sleep_until`\n\n |\n| `<mutex>` | 类:\n\n*   `mutex`\n*   `timed_mutex`\n*   `recursive_mutex`\n*   `recursive_timed_mutex`\n*   `lock_guard`\n*   `scoped_lock` (c++ 17)\n*   `unique_lock`\n\n函数:\n\n*   `try_lock`\n`lock`\n*   `call_once`\n*   `std::swap` ( `std::unique_lock`)\n\n |\n| `<shared_mutex>` | 类:\n\n*   `shared_mutex` (c++ 17)\n*   `shared_timed_mutex` (c++ 14)\n*   `shared_lock` (c++ 14)\n\n函数:\n\n*   `std::swap` ( `std::shared_lock`)\n\n |\n| `<future>` | 类:\n\n*   `promise`\n*   `packaged_task`\n*   `future`\n*   `shared_future`\n\n功能:\n\n*   `async`\n*   `future_category`\n*   `std::swap` ( `std::promise`)\n*   `std::swap` ( `std::packaged_task`)\n\n |\n| `<condition_variable>` | 类:\n\n*   `condition_variable`\n*   `condition_variable_any`\n\n功能:\n\n*   `notify_all_at_thread_exit`\n\n |\n\n在上表中，我们可以看到每个报头提供的功能，以及 2014 年和 2017 年标准引入的特性。在接下来的部分中，我们将详细了解每个函数和类。\n\n# 线程类\n\n`thread`类是整个线程 API 的核心；它包装了底层操作系统的线程，并提供了我们启动和停止线程所需的功能。\n\n通过包含`<thread>`标题，可以访问该功能。\n\n# 基本用途\n\n创建线程后，它会立即启动:\n\n```cpp\n#include <thread> \n\nvoid worker() { \n   // Business logic. \n} \n\nint main () { \n   std::thread t(worker);\n   return 0; \n} \n\n```\n\n前面这段代码将启动线程，然后立即终止应用，因为我们没有等待新线程完成执行。\n\n为了正确地做到这一点，我们需要等待线程完成，或者如下重新加入:\n\n```cpp\n#include <thread> \n\nvoid worker() { \n   // Business logic. \n} \n\nint main () { \n   std::thread t(worker); \n   t.join(); \n   return 0; \n} \n\n```\n\n最后这段代码将执行，等待新线程完成，然后返回。\n\n# 传递参数\n\n还可以将参数传递给新线程。这些参数值必须是可移动构造的，这意味着它是一个具有移动或复制构造函数的类型(对于右值引用被调用)。实际上，这是所有基本类型和大多数(用户定义的)类的情况:\n\n```cpp\n#include <thread> \n#include <string> \n\nvoid worker(int n, std::string t) { \n   // Business logic. \n} \n\nint main () { \n   std::string s = \"Test\"; \n   int i = 1; \n   std::thread t(worker, i, s); \n   t.join(); \n   return 0; \n} \n\n```\n\n在前面的代码中，我们将一个整数和字符串传递给`thread`函数。该函数将接收两个变量的副本。当传递引用或指针时，随着生命周期问题、数据竞争等成为潜在问题，事情变得更加复杂。\n\n# 返回值\n\n传递给`thread`类构造函数的函数返回的任何值都会被忽略。要向创建新线程的线程返回信息，必须使用线程间同步机制(如互斥体)和某种共享变量。\n\n# 移动线程\n\n2011 年标准在`<utility>`表头增加了`std::move`。使用这个模板方法，可以在对象之间移动资源。这意味着它还可以移动线程实例:\n\n```cpp\n#include <thread> \n#include <string> \n#include <utility> \n\nvoid worker(int n, string t) { \n   // Business logic. \n} \n\nint main () { \n   std::string s = \"Test\"; \n   std::thread t0(worker, 1, s); \n   std::thread t1(std::move(t0)); \n   t1.join(); \n   return 0; \n} \n\n```\n\n在这个版本的代码中，我们先创建一个线程，然后再将其移动到另一个线程。线程 0 因此不再存在(因为它立即结束)，并且`thread`函数的执行在我们创建的新线程中恢复。\n\n因此，我们不必等待第一个线程重新加入，而只需等待第二个线程。\n\n# 线程标识\n\n每个线程都有一个与之关联的标识符。该标识或句柄是由 STL 实现提供的唯一标识符。可以通过调用`thread`类实例的`get_id()`函数，或者通过调用`std::this_thread::get_id()`获取调用该函数的线程的 ID 来获得:\n\n```cpp\n#include <iostream>\n #include <thread>\n #include <chrono>\n #include <mutex>\n\n std::mutex display_mutex;\n\n void worker() {\n     std::thread::id this_id = std::this_thread::get_id();\n\n     display_mutex.lock();\n     std::cout << \"thread \" << this_id << \" sleeping...\\n\";\n     display_mutex.unlock();\n\n     std::this_thread::sleep_for(std::chrono::seconds(1));\n }\n\n int main() {\n    std::thread t1(worker);\n    std::thread::id t1_id = t1.get_id();\n\n    std::thread t2(worker);\n    std::thread::id t2_id = t2.get_id();\n\n    display_mutex.lock();\n    std::cout << \"t1's id: \" << t1_id << \"\\n\";\n    std::cout << \"t2's id: \" << t2_id << \"\\n\";\n    display_mutex.unlock();\n\n    t1.join();\n    t2.join();\n\n    return 0;\n } \n\n```\n\n这段代码将产生类似如下的输出:\n\n```cpp\nt1's id: 2\nt2's id: 3\nthread 2 sleeping...\nthread 3 sleeping...\n\n```\n\n这里，可以看到相对于初始线程(ID 1)，内部线程 ID 是一个整数(`std::thread::id`类型)。这与大多数本机线程标识(如 POSIX 的那些)相当。这些也可以使用`native_handle()`获得。该函数将返回底层本机线程句柄。当希望使用 STL 实现中不可用的非常特定的 PThread 或 Win32 线程功能时，它特别有用。\n\n# 睡着的\n\n可以使用两种方法中的任何一种来延迟线程的执行(休眠)。一个是`sleep_for()`，它至少延迟了指定的持续时间，但可能更长:\n\n```cpp\n#include <iostream> \n#include <chrono> \n#include <thread> \n        using namespace std::chrono_literals;\n\n        typedef std::chrono::time_point<std::chrono::high_resolution_clock> timepoint; \nint main() { \n         std::cout << \"Starting sleep.\\n\"; \n\n         timepoint start = std::chrono::high_resolution_clock::now(); \n\n         std::this_thread::sleep_for(2s); \n\n         timepoint end = std::chrono::high_resolution_clock::now(); \n         std::chrono::duration<double, std::milli> elapsed = end - \n         start; \n         std::cout << \"Slept for: \" << elapsed.count() << \" ms\\n\"; \n} \n\n```\n\n上面这段代码展示了如何休眠大约 2 秒钟，在当前操作系统上使用最高精度的计数器测量精确的持续时间。\n\n请注意，我们可以直接指定秒数，使用秒后修复。这是一个 C++ 14 特性，被添加到`<chrono>`头。对于 C++ 11 版本，必须创建一个 std::chrono::seconds 的实例，并将其传递给`sleep_for()`函数。\n\n另一种方法是`sleep_until()`，取类型为`std::chrono::time_point<Clock, Duration>`的单个参数。使用此功能，可以将线程设置为睡眠，直到达到指定的时间点。由于操作系统的调度优先级，该唤醒时间可能不是指定的准确时间。\n\n# 产量\n\n可以向操作系统指示当前线程可以被重新调度，以便其他线程可以代替运行。为此，使用`std::this_thread::yield()`功能。该功能的确切结果取决于底层操作系统实现及其调度程序。在先进先出调度器的情况下，调用线程很可能会被放在队列的后面。\n\n这是一个高度专门化的功能，有特殊的用例。在没有首先验证它对应用性能的影响之前，不应该使用它。\n\n# 派遣\n\n启动线程后，可以在线程对象上调用`detach()`。这有效地将新线程与调用线程分离，这意味着前者将继续执行，即使在调用线程退出之后。\n\n# 交换\n\n使用`swap()`，无论是作为独立的方法还是作为线程实例的函数，都可以交换线程对象的底层线程句柄:\n\n```cpp\n#include <iostream> \n#include <thread> \n#include <chrono> \n\nvoid worker() { \n   std::this_thread::sleep_for(std::chrono::seconds(1)); \n} \n\nint main() { \n         std::thread t1(worker); \n         std::thread t2(worker); \n\n         std::cout << \"thread 1 id: \" << t1.get_id() << \"\\n\"; \n         std::cout << \"thread 2 id: \" << t2.get_id() << \"\\n\"; \n\n         std::swap(t1, t2); \n\n         std::cout << \"Swapping threads...\" << \"\\n\"; \n\n         std::cout << \"thread 1 id: \" << t1.get_id() << \"\\n\"; \n         std::cout << \"thread 2 id: \" << t2.get_id() << \"\\n\"; \n\n         t1.swap(t2); \n\n         std::cout << \"Swapping threads...\" << \"\\n\"; \n\n         std::cout << \"thread 1 id: \" << t1.get_id() << \"\\n\"; \n         std::cout << \"thread 2 id: \" << t2.get_id() << \"\\n\"; \n\n         t1.join(); \n         t2.join(); \n} \n\n```\n\n这段代码的可能输出如下所示:\n\n```cpp\nthread 1 id: 2\nthread 2 id: 3\nSwapping threads...\nthread 1 id: 3\nthread 2 id: 2\nSwapping threads...\nthread 1 id: 2\nthread 2 id: 3\n\n```\n\n这样做的效果是，每个线程的状态与另一个线程的状态交换，本质上是交换它们的身份。\n\n# 互斥（体）…\n\n`<mutex>`头包含多种类型的互斥体和锁。互斥类型是最常用的类型，它提供了基本的锁定/解锁功能，没有任何进一步的复杂性。\n\n# 基本用途\n\n互斥锁的核心目标是排除同时访问的可能性，以防止数据损坏，并防止由于使用非线程安全例程而导致的崩溃。\n\n需要使用互斥体的一个例子是下面的代码:\n\n```cpp\n#include <iostream> \n#include <thread> \n\nvoid worker(int i) { \n         std::cout << \"Outputting this from thread number: \" << i << \"\\n\"; \n} \n\nint main() { \n         std::thread t1(worker, 1);\n         std::thread t2(worker, 2); \n\n         t1.join(); \n   t2.join(); \n\n   return 0; \n} \n\n```\n\n如果您尝试按原样运行前面的代码，您会注意到来自两个线程的文本输出将被混合在一起，而不是一个接一个地输出。原因是标准输出(无论是 C 还是 C++ 风格)不是线程安全的。虽然应用不会崩溃，但输出将是一片混乱。\n\n对此的解决方法很简单，如下所示:\n\n```cpp\n#include <iostream> \n#include <thread> \n#include <mutex> \n\nstd::mutex globalMutex; \n\nvoid worker(int i) { \n   globalMutex.lock(); \n         std::cout << \"Outputting this from thread number: \" << i << \"\\n\"; \n   globalMutex.unlock(); \n} \n\nint main() { \n         std::thread t1(worker, 1);\n         std::thread t2(worker, 2); \n\n         t1.join(); \n   t2.join(); \n\n   return 0; \n} \n\n```\n\n在这种情况下，每个线程首先需要访问`mutex`对象。由于只有一个线程可以访问`mutex`对象，另一个线程将最终等待第一个线程完成对标准输出的写入，两个字符串将按照预期一个接一个地出现。\n\n# 无阻塞锁定\n\n有可能不希望线程阻塞并等待`mutex`对象变得可用:例如，当一个线程只想知道一个请求是否已经被另一个线程处理，等待它完成是没有用的。\n\n为此，互斥体附带了`try_lock()`函数，该函数正是这么做的。\n\n在下面的示例中，我们可以看到两个线程试图递增同一个计数器，但是当一个线程无法立即获得对共享计数器的访问时，它就会递增自己的计数器:\n\n```cpp\n#include <chrono> \n#include <mutex> \n#include <thread> \n#include <iostream> \n\nstd::chrono::milliseconds interval(50); \n\nstd::mutex mutex; \nint shared_counter = 0;\nint exclusive_counter = 0; \n\nvoid worker0() { \n   std::this_thread::sleep_for(interval);\n\n         while (true) { \n               if (mutex.try_lock()) { \n                     std::cout << \"Shared (\" << job_shared << \")\\n\"; \n                     mutex.unlock(); \n                     return; \n               } \n         else { \n                     ++ exclusive_counter; \n                           std::cout << \"Exclusive (\" << exclusive_counter << \")\\n\"; \n                           std::this_thread::sleep_for(interval); \n               } \n         } \n} \n\nvoid worker1() { \n   mutex.lock(); \n         std::this_thread::sleep_for(10 * interval); \n         ++ shared_counter; \n         mutex.unlock(); \n} \n\nint main() { \n         std::thread t1(worker0); \n         std::thread t2(worker1); \n\n         t1.join(); \n         t2.join(); \n}\n\n```\n\n前面这个例子中的两个线程都运行不同的`worker`函数，但是它们都有一个共同的事实，那就是它们会休眠一段时间，并在醒来时尝试获取共享计数器的互斥量。如果他们这样做，他们会增加计数器，但只有第一个工人会输出这个事实。\n\n第一个工作进程在没有获得共享计数器时也进行日志记录，但只增加了它的独占计数器。结果输出可能如下所示:\n\n```cpp\nExclusive (1)\nExclusive (2)\nExclusive (3)\nShared (1)\nExclusive (4)\n\n```\n\n# 定时互斥\n\n定时互斥体是一种常规的互斥体类型，但是增加了一些函数，这些函数在试图获取锁的时间段内提供一种控制，即`try_lock_for`和`try_lock_until`。\n\n前者在返回结果(真或假)之前，试图在指定的时间段内获得锁(`std::chrono`对象)。后者会等到未来某个特定的时间点再返回结果。\n\n这些函数的使用主要在于在常规互斥体的阻塞(`lock`)和非阻塞(`try_lock`)方法之间提供一条中间路径。人们可能希望仅使用单个线程来等待多个任务，而不知道某个任务何时变得可用，或者某个任务可能会在某个时间点过期，此时等待它不再有意义。\n\n# 锁护板\n\n锁保护是一个简单的互斥包装器，它处理在`mutex`对象上获取锁，以及当锁保护超出范围时释放锁。这是一个很有帮助的机制，可以确保人们不会忘记释放互斥锁，并且当人们不得不在多个位置释放同一个互斥锁时，有助于减少代码中的混乱。\n\n例如，重构 big if/else 块可以减少需要释放互斥锁的情况，但是只使用锁保护包装器而不用担心这些细节会容易得多:\n\n```cpp\n#include <thread> \n#include <mutex> \n#include <iostream> \n\nint counter = 0; \nstd::mutex counter_mutex; \n\nvoid worker() { \n         std::lock_guard<std::mutex> lock(counter_mutex); \n   if (counter == 1) { counter += 10; } \n   else if (counter >= 10) { counter += 15; } \n   else if (counter >= 50) { return; } \n         else { ++ counter; } \n\n   std::cout << std::this_thread::get_id() << \": \" << counter << '\\n'; \n} \n\nint main() { \n    std::cout << __func__ << \": \" << counter << '\\n'; \n\n    std::thread t1(worker); \n    std::thread t2(worker); \n\n    t1.join(); \n    t2.join(); \n\n    std::cout << __func__ << \": \" << counter << '\\n'; \n} \n\n```\n\n在前面的例子中，我们看到我们有一个小的 if/else 块，其中一个条件导致`worker`函数立即返回。如果没有锁保护，我们必须确保在从函数返回之前也在这种情况下解锁了互斥锁。\n\n然而，有了锁卫士，我们就不必担心这些细节，这使得我们可以专注于业务逻辑，而不用担心互斥管理。\n\n# 唯一锁\n\n唯一锁是通用互斥包装器。它类似于定时互斥体，但具有额外的特性，主要是所有权的概念。与其他锁类型不同，唯一锁不一定拥有它所包装的互斥体，如果它包含任何互斥体的话。使用`swap()`功能，可以在唯一锁实例之间转移互斥锁以及所述互斥锁的所有权。\n\n一个唯一的锁实例是否拥有其互斥体的所有权，以及它是否被锁定，首先是在创建锁时确定的，这可以从它的构造函数中看到。例如:\n\n```cpp\nstd::mutex m1, m2, m3; \nstd::unique_lock<std::mutex> lock1(m1, std::defer_lock); \nstd::unique_lock<std::mutex> lock2(m2, std::try_lock); \nstd::unique_lock<std::mutex> lock3(m3, std::adopt_lock); \n\n```\n\n最后一段代码中的第一个构造函数不会锁定所分配的互斥体(延迟)。第二次尝试使用`try_lock()`锁定互斥体。最后，第三个构造函数假设它已经拥有所提供的互斥体。\n\n除此之外，其他构造函数允许定时互斥的功能。也就是说，它将等待一段时间，直到到达某个时间点，或者直到获得锁。\n\n最后，使用`release()`函数打破锁和互斥之间的关联，并返回一个指向`mutex`对象的指针。然后调用者负责释放互斥体上任何剩余的锁，并进一步处理它。\n\n这种类型的锁不会经常单独使用，因为它非常通用。大多数其他类型的互斥体和锁都没有那么复杂，并且可能在 99%的情况下满足所有需求。因此，独特锁的复杂性既是好处也是风险。\n\n然而，它通常被 C++ 11 线程应用编程接口的其他部分使用，例如条件变量，我们稍后会看到。\n\n唯一锁可能有用的一个方面是作为作用域锁，允许使用作用域锁，而不必依赖 C++ 17 标准中的本机作用域锁。请看这个例子:\n\n```cpp\n#include <mutex>\nstd::mutex my_mutex\nint count = 0;\nint function() {\n         std::unique_lock<mutex> lock(my_mutex);\n   count++ ;\n}  \n\n```\n\n当我们进入函数时，我们用全局互斥实例创建一个新的 unique_lock。互斥体此时被锁定，之后我们可以执行任何关键操作。\n\n当函数作用域结束时，调用 unique_lock 的析构函数，这将导致互斥锁再次解锁。\n\n# 作用域锁\n\n在 2017 年标准中首次引入，作用域锁是一个互斥体包装器，它获取对所提供互斥体的访问(锁定)，并确保当作用域锁超出作用域时，它被解锁。它不同于锁保护，因为它不是一个而是多个互斥锁的包装器。\n\n当在一个作用域中处理多个互斥体时，这可能很有用。使用作用域锁的一个原因是为了避免意外引入死锁和其他令人不快的复杂情况，例如，一个互斥体被作用域锁锁定，另一个锁仍在等待，而另一个线程实例的情况正好相反。\n\n作用域锁的一个属性是它试图避免这种情况，理论上使这种类型的锁死锁安全。\n\n# 递归互斥\n\n递归互斥是互斥的另一个子类型。即使它具有与常规互斥体完全相同的功能，它也允许最初锁定互斥体的调用线程重复锁定同一个互斥体。通过这样做，互斥体不会对其他线程可用，直到拥有它的线程解锁互斥体的次数与锁定它的次数一样多。\n\n使用递归互斥体的一个很好的理由是，例如当使用递归函数时。对于常规互斥体，需要发明某种入口点，在进入递归函数之前锁定互斥体。\n\n对于递归互斥体，递归函数的每次迭代都会再次锁定递归互斥体，并且在完成一次迭代后，它会解锁互斥体。结果，互斥体将被解锁，并且解锁的次数相同。\n\n因此，一个潜在的复杂问题是，标准中没有定义递归互斥锁可以被锁定的最大次数。当达到实现的限制时，如果试图锁定它，将抛出`std::system_error`，或者在使用非阻塞`try_lock`函数时返回 false。\n\n# 递归定时互斥\n\n递归定时互斥体，顾名思义，是定时互斥体和递归互斥体功能的融合。因此，它允许使用定时条件函数递归锁定互斥体。\n\n尽管这增加了确保互斥锁在线程锁定的时候被解锁的次数的挑战，但是它仍然为更复杂的算法提供了可能性，比如前面提到的任务处理程序。\n\n# 共享互斥体\n\n`<shared_mutex>`表头是 2014 年标准首次增加的，增加了`shared_timed_mutex`类。随着 2017 年的标准，还增加了`shared_mutex`类。\n\n共享互斥头从 C++ 17 开始就存在了。除了通常的互斥访问之外，这个`mutex`类还增加了对互斥体提供共享访问的能力。例如，这允许多线程提供对资源的读访问，而写线程仍然能够获得独占访问。这类似于 Pthreads 的读写锁。\n\n添加到该互斥类型的函数如下:\n\n*   `lock_shared()`\n*   `try_lock_shared()`\n*   `unlock_shared()`\n\n这个互斥体的共享功能的使用应该是不言自明的。理论上，无限数量的读取器可以获得对互斥体的读取权限，同时确保在任何时候只有一个线程可以写入资源。\n\n# 共享定时互斥\n\n这个头文件从 C++ 14 开始就有了。它通过以下功能向定时互斥体添加共享锁定功能:\n\n*   `lock_shared()`\n*   `try_lock_shared()`\n*   `try_lock_shared_for()`\n*   `try_lock_shared_until()`\n*   `unlock_shared()`\n\n顾名思义，这个类本质上是共享互斥体和定时互斥体的合并。有趣的是，它是在更基本的共享互斥体之前被添加到标准中的。\n\n# 条件变量\n\n本质上，条件变量提供了一种机制，通过这种机制，一个线程的执行可以被另一个线程控制。这是通过拥有一个共享变量来实现的，一个线程将等待这个变量，直到另一个线程发出信号。这是我们在[第 4 章](04.html)、*线程同步和通信*中看到的调度器实现的重要部分。\n\n对于 C++ 11 API，条件变量及其相关功能在`<condition_variable>`头中定义。\n\n条件变量的基本用法可以从[第 4 章](04.html)、*线程同步和通信*中的调度程序代码中总结出来。\n\n```cpp\n #include \"abstract_request.h\"\n\n #include <condition_variable>\n #include <mutex> \n\nusing namespace std;\n\n class Worker {\n    condition_variable cv;\n    mutex mtx;\n    unique_lock<mutex> ulock;\n    AbstractRequest* request;\n    bool running;\n    bool ready;\n    public:\n    Worker() { running = true; ready = false; ulock = unique_lock<mutex>(mtx); }\n    void run();\n    void stop() { running = false; }\n    void setRequest(AbstractRequest* request) { this->request = request; ready = true; }\n    void getCondition(condition_variable* &cv);\n }; \n\n```\n\n在构造函数中，如前面的`Worker`类声明中所定义的，我们看到了 C++ 11 API 中条件变量的初始化方式。步骤如下:\n\n1.  创建一个`condition_variable`和`mutex`实例。\n2.  将互斥体分配给一个新的`unique_lock`实例。使用我们在这里用于锁的构造函数，分配的互斥体也在分配时被锁定。\n3.  条件变量现在可以使用了:\n\n```cpp\n#include <chrono>\nusing namespace std;\nvoid Worker::run() {\n    while (running) {\n        if (ready) {\n            ready = false;\n            request->process();\n            request->finish();\n        }\n        if (Dispatcher::addWorker(this)) {\n            while (!ready && running) {\n                if (cv.wait_for(ulock, chrono::seconds(1)) == \n                cv_status::timeout) {\n                    // We timed out, but we keep waiting unless the \n                    worker is\n                    // stopped by the dispatcher.\n                }\n            }\n        }\n    }\n} \n\n```\n\n这里，我们使用条件变量的`wait_for()`函数，传递我们之前创建的唯一锁实例和我们想要等待的时间量。我们在这里等 1 秒钟。如果我们在这个等待中超时，我们可以在一个连续的循环中重新进入等待(就像这里所做的那样)，或者继续执行。\n\n也可以使用简单的`wait()`功能执行阻塞等待，或者使用`wait_for()`等待某个时间点。\n\n如前所述，当我们第一次查看这段代码时，这个工作代码使用`ready`布尔变量的原因是为了检查它是否真的是另一个线程发出了条件变量的信号，而不仅仅是一个虚假的唤醒。大多数条件变量实现(包括 C++ 11)都很容易出现这种情况，这是一个不幸的复杂情况。\n\n由于这些随机的唤醒事件，有必要有某种方法来确保我们确实是故意醒来的。在调度程序代码中，这是通过让唤醒工作线程的线程也设置一个工作线程可以唤醒的`Boolean`值来实现的。\n\n我们是否超时，或被通知，或遭受虚假唤醒可以用`cv_status`枚举来检查。这个枚举知道这两种可能的情况:\n\n*   `timeout`\n*   `no_timeout`\n\n信号或通知本身非常简单:\n\n```cpp\nvoid Dispatcher::addRequest(AbstractRequest* request) {\n    workersMutex.lock();\n    if (!workers.empty()) {\n          Worker* worker = workers.front();\n          worker->setRequest(request);\n          condition_variable* cv;\n          worker->getCondition(cv);\n          cv->notify_one();\n          workers.pop();\n          workersMutex.unlock();\n    }\n    else {\n          workersMutex.unlock();\n          requestsMutex.lock();\n          requests.push(request);\n          requestsMutex.unlock();\n    }\n          } \n\n```\n\n在前面这个来自`Dispatcher`类的函数中，我们试图获得一个可用的工作线程实例。如果找到，我们将获得对工作线程条件变量的引用，如下所示:\n\n```cpp\nvoid Worker::getCondition(condition_variable* &cv) {\n    cv = &(this)->cv;\n } \n\n```\n\n在工作线程上设置新请求也会将`ready`变量的值更改为 true，从而允许工作线程检查是否确实允许继续。\n\n最后，条件变量被通知任何等待它的线程现在可以继续使用`notify_one()`。这个特殊的函数将向先进先出队列中的第一个线程发出信号，让这个条件变量继续。这里，只有一个线程会被通知，但是如果有多个线程在等待同一个条件变量，调用`notify_all()`将允许先进先出队列中的所有线程继续。\n\n# 条件变量任意\n\n`condition_variable_any`类是`condition_variable`类的推广。它与后者的不同之处在于，它允许在`unique_lock<mutex>`之外使用其他互斥机制。唯一的要求是使用的锁满足`BasicLockable`要求，也就是说提供了`lock()`和`unlock()`功能。\n\n# 线程退出时通知所有人\n\n`std::notify_all_at_thread_exit()`函数允许(分离的)线程通知其他线程它已经完全完成，并且正在销毁其范围内(线程本地)的所有对象。它的功能是在发送所提供的条件变量之前，将所提供的锁移动到内部存储中。\n\n结果就好像锁被解锁了，条件变量上调用了`notify_all()`。\n\n一个基本的(非功能性的)例子如下:\n\n```cpp\n#include <mutex> \n#include <thread> \n#include <condition_variable> \nusing namespace std; \n\nmutex m; \ncondition_variable cv;\nbool ready = false; \nThreadLocal result;\n\nvoid worker() { \n   unique_lock<mutex> ulock(m); \n   result = thread_local_method(); \n         ready = true; \n         std::notify_all_at_thread_exit(cv, std::move(ulock)); \n} \n\nint main() { \n         thread t(worker); \n         t.detach(); \n\n         // Do work here. \n\n         unique_lock<std::mutex> ulock(m); \n         while(!ready) { \n               cv.wait(ulock); \n         } \n\n         // Process result \n} \n\n```\n\n这里，工作线程执行一个创建线程本地对象的方法。因此，主线程必须先等待分离的工作线程完成。如果后者在主线程完成任务时还没有完成，它将使用全局条件变量进入等待状态。在工作线程中，设置`ready`布尔后调用`std::notify_all_at_thread_exit()`。\n\n这实现了双重目的。调用函数后，不允许更多线程等待条件变量。它还允许主线程等待分离的工作线程的结果变得可用。\n\n# 将来的\n\nC++ 11 线程支持 API 的最后一部分在`<future>`中定义。它提供了一系列的类，这些类实现了更高级别的多线程概念，这些概念更倾向于简单的异步处理，而不是多线程体系结构的实现。\n\n这里我们必须区分两个概念:未来和承诺。前者是读者/消费者将使用的最终结果(未来产品)。后者是编剧/制片人使用的。\n\n未来的一个基本例子是:\n\n```cpp\n#include <iostream>\n#include <future>\n#include <chrono>\n\nbool is_prime (int x) {\n  for (int i = 2; i < x; ++ i) if (x%i==0) return false;\n  return true;\n}\n\nint main () {\n  std::future<bool> fut = std::async (is_prime, 444444443);\n  std::cout << \"Checking, please wait\";\n  std::chrono::milliseconds span(100);\n  while (fut.wait_for(span) == std::future_status::timeout) {               std::cout << '.' << std::flush;\n   }\n\n  bool x = fut.get();\n  std::cout << \"\\n444444443 \" << (x?\"is\":\"is not\") << \" prime.\\n\";\n  return 0;\n}\n\n```\n\n这段代码异步调用一个函数，给它传递一个参数(潜在的质数)。然后，它进入一个活动循环，同时等待从异步函数调用接收到的未来完成。它在其等待功能上设置 100 ms 超时。\n\n一旦未来结束(等待函数没有返回超时值)，我们就获得了结果值，在这种情况下告诉我们，我们为函数提供的值实际上是一个质数。\n\n在本章的*异步*部分，我们将更多地了解异步函数调用。\n\n# 承诺\n\n一个`promise`允许一个人在线程之间转移状态。例如:\n\n```cpp\n#include <iostream> \n#include <functional>\n#include <thread> \n#include <future> \n\nvoid print_int (std::future<int>& fut) {\n  int x = fut.get();\n  std::cout << \"value: \" << x << '\\n';\n}\n\nint main () {\n  std::promise<int> prom;\n  std::future<int> fut = prom.get_future();\n  std::thread th1 (print_int, std::ref(fut));\n  prom.set_value (10);                            \n  th1.join();\n  return 0;\n\n```\n\n前面的代码使用传递给工作线程的`promise`实例将一个值传递给另一个线程，在本例中是一个整数。新线程等待着我们从承诺中创造的未来，以及它从主线程中收到的未来来完成。\n\n当我们设定承诺的价值时，承诺就完成了。这就完成了未来并完成了工作线程。\n\n在这个特殊的例子中，我们对`future`对象使用阻塞等待，但是也可以使用`wait_for()`和`wait_until()`，分别等待一个时间段或一个时间点，就像我们在前面的例子中看到的未来一样。\n\n# 共享未来\n\n一个`shared_future`就像一个普通的`future`对象，但是可以复制，允许多个线程读取它的结果。\n\n创建`shared_future`类似于常规的`future.`\n\n```cpp\nstd::promise<void> promise1; \nstd::shared_future<void> sFuture(promise1.get_future()); \n\n```\n\n最大的区别是正则`future`传递给了它的构造函数。\n\n之后，所有可以访问`future`对象的线程都可以等待它，并获取它的值。这也可以用于以类似于条件变量的方式向线程发送信号。\n\n# 打包的任务\n\n`packaged_task`是任何可调用目标(函数、绑定、lambda 或其他函数对象)的包装器。它允许异步执行，结果在`future`对象中可用。它类似于`std::function`，但会自动将其结果传输到一个`future`对象。\n\n例如:\n\n```cpp\n#include <iostream> \n#include <future> \n#include <chrono>\n#include <thread>\n\nusing namespace std; \n\nint countdown (int from, int to) { \n   for (int i = from; i != to; --i) { \n         cout << i << '\\n'; \n         this_thread::sleep_for(chrono::seconds(1)); \n   } \n\n   cout << \"Finished countdown.\\n\"; \n   return from - to; \n} \n\nint main () { \n   packaged_task<int(int, int)> task(countdown);\n   future<int> result = task.get_future();\n   thread t (std::move(task), 10, 0);\n\n   //  Other logic. \n\n   int value = result.get(); \n\n   cout << \"The countdown lasted for \" << value << \" seconds.\\n\"; \n\n   t.join(); \n   return 0; \n} \n\n```\n\n上面这段代码实现了一个简单的倒计时功能，从 10 数到 0。创建任务并获得对其`future`对象的引用后，我们将其与`worker`函数的参数一起推送到线程上。\n\n倒计时工作线程的结果一结束就变得可用。我们可以像使用`promise`一样使用`future`对象的等待功能。\n\n# 异步ˌ非同步(asynchronous)\n\n`promise`和`packaged_task`更直接的版本可以在`std::async()`找到。这是一个简单的函数，它接受一个可调用的对象(函数、bind、lambda 等)以及它的任何参数，并返回一个`future`对象。\n\n以下是`async()`功能的基本示例:\n\n```cpp\n#include <iostream>\n#include <future>\n\nusing namespace std; \n\nbool is_prime (int x) { \n   cout << \"Calculating prime...\\n\"; \n   for (int i = 2; i < x; ++ i) { \n         if (x % i == 0) { \n               return false; \n         } \n   } \n\n   return true; \n} \n\nint main () { \n   future<bool> pFuture = std::async (is_prime, 343321); \n\n   cout << \"Checking whether 343321 is a prime number.\\n\"; \n\n   // Wait for future object to be ready. \n\n   bool result = pFuture.get(); \n   if (result) {\n         cout << \"Prime found.\\n\"; \n   } \n   else { \n         cout << \"No prime found.\\n\"; \n   } \n\n   return 0; \n} \n\n```\n\n前面代码中的`worker`函数确定提供的整数是否为素数。正如我们所看到的，结果代码比使用`packaged_task`或`promise`简单得多。\n\n# 启动策略\n\n除了基本版本的`std::async(),`之外，还有第二个版本，允许用户指定启动策略作为其第一个参数。这是类型为`std::launch`的位掩码值，可能有以下值:\n\n```cpp\n* launch::async \n* launch::deferred \n\n```\n\n`async`标志意味着立即为`worker`函数创建一个新的线程和执行上下文。`deferred`标志表示推迟到`wait()`或`get()`被调用到`future`对象上。指定这两个标志会导致函数根据当前系统情况自动选择方法。\n\n`std::async()`版本没有明确指定位掩码值，默认为后者，自动方法。\n\n# 原子学\n\n对于多线程，原子的使用也非常重要。为此，C++ 11 STL 提供了一个`<atomic>`头。本主题在 [C](08.html) 第 8 章*原子操作-使用硬件*中有广泛的介绍。\n\n# 摘要\n\n在本章中，我们探讨了 C++ 11 API 中的全部多线程支持，以及 C++ 14 和 C++ 17 中添加的特性。\n\n我们看到了如何使用描述和示例代码来使用每个特性。我们现在可以使用本机 C++ 多线程应用编程接口来实现多线程、线程安全的代码，以及使用异步执行功能来加速和并行执行功能。\n\n在下一章中，我们将了解多线程代码实现中不可避免的下一步:调试和验证生成的应用。**"
  },
  {
    "path": "docs/master-cpp-multithrd/06.md",
    "content": "# 六、调试多线程代码\n\n理想情况下，一个人的代码第一次就能正常工作，并且不包含等待应用崩溃、数据损坏或导致其他问题的隐藏 bug。实际上，这当然是不可能的。因此，开发的工具使得检查和调试多线程应用变得容易。\n\n在这一章中，我们将研究其中的一些，包括一个常规的调试器，以及一些属于 Valgrind 套件的工具，特别是 Helgrind 和 DRD。我们还将分析多线程应用，以便发现其设计中的热点和潜在问题。\n\n本章涵盖的主题包括:\n\n*   介绍 Valgrind 工具套件\n*   使用赫尔格林和 DRD 工具\n*   解读赫尔格林和 DRD 的分析结果\n*   分析应用，并分析结果\n\n# 何时开始调试\n\n理想情况下，每次达到某个里程碑时，无论是单个模块、多个模块还是整个应用，都要测试和验证自己的代码。确定一个人做出的假设与最终的功能相匹配是很重要的。\n\n特别是，对于多线程代码，很大程度上是一致的，因为在应用的每次运行期间，都不能保证达到特定的错误状态。多线程应用实现不当的迹象可能会导致一些症状，如看似随机的崩溃。\n\n很可能你得到的第一个暗示是某个东西不正确是在应用崩溃的时候，剩下的是一个核心转储。这是一个文件，包含应用崩溃时的内存内容，包括堆栈。\n\n这种核心转储的使用方式几乎与在运行进程中运行调试器的方式相同。检查代码中我们崩溃的位置以及在哪个线程中是特别有用的。我们也可以用这种方法检查记忆内容。\n\n处理多线程问题的最佳指标之一是应用从不在同一个位置崩溃(不同的堆栈跟踪)，或者总是在执行互斥操作(如操作全局数据结构)的某个点崩溃。\n\n首先，在深入研究 Valgrind 工具套件之前，我们将首先更深入地了解使用调试器进行诊断和调试。\n\n# 不起眼的调试器\n\n在开发人员可能会有的所有问题中，*为什么我的应用会崩溃？*大概是其中最重要的。这也是调试器最容易回答的问题之一。无论是实时调试进程，还是分析崩溃进程的核心转储，调试器都可以(希望)生成一个回溯，也称为堆栈跟踪。此跟踪包含自应用启动以来调用的所有函数的时间顺序列表，因为人们可以在堆栈上找到它们(有关堆栈如何工作的详细信息，请参见[第 2 章](02.html)、*处理器和操作系统上的多线程实现*)。\n\n因此，这个回溯的最后几个条目将向我们显示代码的哪一部分出错了。如果调试信息被编译成二进制文件，或者被提供给调试器，我们还可以在那一行看到代码以及变量的名称。\n\n更好的是，由于我们正在查看堆栈框架，我们还可以检查该堆栈框架中的变量。这意味着传递给函数的参数以及任何局部变量及其值。\n\n为了获得可用的调试信息(符号)，必须使用适当的编译器标志集来编译源代码。对于 GCC，可以选择大量的调试信息级别和类型。最常见的是，使用带有整数的`-g`标志，指定附加的调试级别，如下所示:\n\n*   `-g0`:不产生调试信息(否定`-g`)\n*   `-g1`:关于函数描述和外部变量的最少信息\n*   `-g3`:包括宏定义在内的所有信息\n\n该标志指示 GCC 为操作系统生成本机格式的调试信息。还可以使用不同的标志以特定的格式生成调试信息；但是，这对于 GCC 的调试器(GDB)以及 Valgrind 工具来说并不是必需的。\n\nGDB 和瓦格林都将使用这些调试信息。虽然在没有调试信息的情况下使用两者在技术上是可能的，但这最好留给真正绝望的时候。\n\n# 基因组数据库\n\n基于 C 和 C++ 的代码最常用的调试器之一是 GNU 调试器，简称 GDB 调试器。在下面的例子中，我们将使用这个调试器，因为它被广泛使用并且免费提供。最初写于 1986 年，现在它被广泛用于各种编程语言，并且已经成为个人和专业使用中最常用的调试器。\n\nGDB 最基本的界面是命令行外壳，但是它可以与图形前端一起使用，图形前端还包括许多 ide，例如 Qt Creator、Dev-C++，以及 Code::Blocks。这些前端和 IDEs 可以使管理断点、设置监视变量和执行其他常见操作变得更加容易和直观。然而，并不要求使用它们。\n\n在 Linux 和 BSD 发行版上，gdb 可以很容易地从一个包中安装，就像它在带有 MSYS2 和类似 UNIX 的环境的 Windows 上一样。对于 OS X/苹果操作系统，你可能不得不使用第三方软件包管理器安装 gdb，比如 Homebrew。\n\n由于 gdb 通常不在 MacOS 上进行代码签名，因此它无法获得正常操作所需的系统级访问权限。在这里，您可以以 root 用户身份运行 gdb(不推荐)，也可以遵循与您的 MacOS 版本相关的教程。\n\n# 调试多线程代码\n\n如前所述，有两种使用调试器的方法，或者通过在调试器中启动应用(或者附加到正在运行的进程)，或者通过加载核心转储文件。在调试会话中，可以中断运行过程(通过发送`SIGINT`信号的 *Ctrl* + *C* ，或者为加载的核心转储加载调试符号。之后，我们可以检查这个框架中的活动线程:\n\n```cpp\nThread 1 received signal SIGINT, Interrupt.\n0x00007fff8a3fff72 in mach_msg_trap () from /usr/lib/system/libsystem_kernel.dylib\n(gdb) info threads\nId   Target Id         Frame \n* 1    Thread 0x1703 of process 72492 0x00007fff8a3fff72 in mach_msg_trap () from /usr/lib/system/libsystem_kernel.dylib\n3    Thread 0x1a03 of process 72492 0x00007fff8a406efa in kevent_qos () from /usr/lib/system/libsystem_kernel.dylib\n10   Thread 0x2063 of process 72492 0x00007fff8a3fff72 in mach_msg_trap () from /usr/lib/system/libsystem_kernel.dylibs\n14   Thread 0x1e0f of process 72492 0x00007fff8a405d3e in __pselect () from /usr/lib/system/libsystem_kernel.dylib\n(gdb) c\nContinuing.\n\n```\n\n在前面的代码中，我们可以看到在向应用(一个运行在 OS X 的基于 Qt 的应用)发送`SIGINT`信号后，我们如何请求在这个时间点存在的所有线程的列表，以及它们的线程号、标识和它们当前正在执行的功能。这也清楚地显示了哪些线程可能正在基于后一种信息等待，就像这种图形用户界面应用的情况一样。在这里，我们还可以看到应用中当前活动的线程，在其编号(线程 1)前面用星号标记。\n\n我们也可以使用`thread <ID>`命令在线程之间随意切换，并在线程的堆栈帧之间移动`up`和`down`。这允许我们检查单个线程的每个方面。\n\n当完整的调试信息可用时，通常还会看到线程正在执行的确切代码行。这意味着，在应用的开发阶段，尽可能多的调试信息可用以使调试变得更加容易是有意义的。\n\n# 断点\n\n对于我们在[第 4 章](04.html)、*线程同步和通信*中看到的调度程序代码，我们可以设置一个断点来允许我们检查活动线程:\n\n```cpp\n$ gdb dispatcher_demo.exe \nGNU gdb (GDB) 7.9 \nCopyright (C) 2015 Free Software Foundation, Inc. \nReading symbols from dispatcher_demo.exe...done. \n(gdb) break main.cpp:67 \nBreakpoint 1 at 0x4017af: file main.cpp, line 67\\. \n(gdb) run \nStarting program: dispatcher_demo.exe \n[New Thread 10264.0x2a90] \n[New Thread 10264.0x2bac] \n[New Thread 10264.0x2914] \n[New Thread 10264.0x1b80] \n[New Thread 10264.0x213c] \n[New Thread 10264.0x2228] \n[New Thread 10264.0x2338] \n[New Thread 10264.0x270c] \n[New Thread 10264.0x14ac] \n[New Thread 10264.0x24f8] \n[New Thread 10264.0x1a90] \n\n```\n\n正如我们在上面的命令行输出中看到的，我们用我们希望调试的应用的名称作为参数来启动 GDB，这里是从 Windows 下的一个 Bash shell 开始的。在这之后，我们可以在这里设置一个断点，使用源文件的文件名和我们希望在 gdb 命令行输入的(gdb)之后中断的行。我们选择循环后的第一行，其中请求被发送到调度程序，然后运行应用。接下来是调度程序正在创建的新线程列表，如 GDB 所报告的。\n\n接下来，我们等到断点命中:\n\n```cpp\nBreakpoint 1, main () at main.cpp:67 \n67              this_thread::sleep_for(chrono::seconds(5)); \n(gdb) info threads \nId   Target Id         Frame \n11   Thread 10264.0x1a90 0x00000000775ec2ea in ntdll!ZwWaitForMultipleObjects () from /c/Windows/SYSTEM32/ntdll.dll \n10   Thread 10264.0x24f8 0x00000000775ec2ea in ntdll!ZwWaitForMultipleObjects () from /c/Windows/SYSTEM32/ntdll.dll \n9    Thread 10264.0x14ac 0x00000000775ec2ea in ntdll!ZwWaitForMultipleObjects () from /c/Windows/SYSTEM32/ntdll.dll \n8    Thread 10264.0x270c 0x00000000775ec2ea in ntdll!ZwWaitForMultipleObjects () from /c/Windows/SYSTEM32/ntdll.dll \n7    Thread 10264.0x2338 0x00000000775ec2ea in ntdll!ZwWaitForMultipleObjects () from /c/Windows/SYSTEM32/ntdll.dll \n6    Thread 10264.0x2228 0x00000000775ec2ea in ntdll!ZwWaitForMultipleObjects () from /c/Windows/SYSTEM32/ntdll.dll \n5    Thread 10264.0x213c 0x00000000775ec2ea in ntdll!ZwWaitForMultipleObjects () from /c/Windows/SYSTEM32/ntdll.dll \n4    Thread 10264.0x1b80 0x0000000064942eaf in ?? () from /mingw64/bin/libwinpthread-1.dll \n3    Thread 10264.0x2914 0x00000000775c2385 in ntdll!LdrUnloadDll () from /c/Windows/SYSTEM32/ntdll.dll \n2    Thread 10264.0x2bac 0x00000000775c2385 in ntdll!LdrUnloadDll () from /c/Windows/SYSTEM32/ntdll.dll \n* 1    Thread 10264.0x2a90 main () at main.cpp:67 \n(gdb) bt \n#0  main () at main.cpp:67 \n(gdb) c \nContinuing. \n\n```\n\n到达断点后，*信息线程*命令列出活动线程。这里我们可以清楚地看到条件变量的使用，其中一个线程正在`ntdll!ZwWaitForMultipleObjects()`中等待。如[第 3 章](03.html)、 *C++ 多线程应用编程接口*所述，这是使用本机多线程应用编程接口在 Windows 上实现条件变量的一部分。\n\n当我们创建一个回溯(`bt`命令)时，我们看到线程 1(当前线程)的当前堆栈只是一个帧，只针对主方法，因为我们没有从这一行的这个起点调入另一个函数。\n\n# 回溯痕迹\n\n在正常的应用执行过程中，例如使用我们前面看到的图形用户界面应用，向应用发送`SIGINT`也可以后跟命令来创建如下的回溯:\n\n```cpp\nThread 1 received signal SIGINT, Interrupt.\n0x00007fff8a3fff72 in mach_msg_trap () from /usr/lib/system/libsystem_kernel.dylib\n(gdb) bt\n#0  0x00007fff8a3fff72 in mach_msg_trap () from /usr/lib/system/libsystem_kernel.dylib\n#1  0x00007fff8a3ff3b3 in mach_msg () from /usr/lib/system/libsystem_kernel.dylib\n#2  0x00007fff99f37124 in __CFRunLoopServiceMachPort () from /System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation\n#3  0x00007fff99f365ec in __CFRunLoopRun () from /System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation\n#4  0x00007fff99f35e38 in CFRunLoopRunSpecific () from /System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation\n#5  0x00007fff97b73935 in RunCurrentEventLoopInMode ()\nfrom /System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/HIToolbox.framework/Versions/A/HIToolbox\n#6  0x00007fff97b7376f in ReceiveNextEventCommon ()\nfrom /System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/HIToolbox.framework/Versions/A/HIToolbox\n#7  0x00007fff97b735af in _BlockUntilNextEventMatchingListInModeWithFilter ()\nfrom /System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/HIToolbox.framework/Versions/A/HIToolbox\n#8  0x00007fff9ed3cdf6 in _DPSNextEvent () from /System/Library/Frameworks/AppKit.framework/Versions/C/AppKit\n#9  0x00007fff9ed3c226 in -[NSApplication _nextEventMatchingEventMask:untilDate:inMode:dequeue:] ()\nfrom /System/Library/Frameworks/AppKit.framework/Versions/C/AppKit\n#10 0x00007fff9ed30d80 in -[NSApplication run] () from /System/Library/Frameworks/AppKit.framework/Versions/C/AppKit\n#11 0x0000000102a25143 in qt_plugin_instance () from /usr/local/Cellar/qt/5.8.0_1/plugins/platforms/libqcocoa.dylib\n#12 0x0000000100cd3811 in QEventLoop::exec(QFlags<QEventLoop::ProcessEventsFlag>) () from /usr/local/opt/qt5/lib/QtCore.framework/Versions/5/QtCore\n#13 0x0000000100cd80a7 in QCoreApplication::exec() () from /usr/local/opt/qt5/lib/QtCore.framework/Versions/5/QtCore\n#14 0x0000000100003956 in main (argc=<optimized out>, argv=<optimized out>) at main.cpp:10\n(gdb) c\nContinuing.\n\n```\n\n在前面的代码中，我们可以通过入口点(main)看到线程 ID 1 的执行。每个后续的函数调用都被添加到堆栈中。当一个函数完成时，它会从堆栈中移除。这既是好处也是坏处。虽然它确实保持了后跟踪的良好和干净，但它也意味着在最后一次函数调用之前发生的历史不再存在。\n\n如果我们用一个核心转储文件创建一个回溯，没有这些历史信息可能会非常烦人，并且当我们试图缩小崩溃的假定原因时，可能会开始徒劳无功。这意味着成功的调试需要一定的经验。\n\n在应用崩溃的情况下，调试器将在遭受崩溃的线程上启动我们。通常，这是代码有问题的线程，但真正的错误可能在于另一个线程执行的代码，甚至是变量的不安全使用。如果一个线程要改变另一个线程当前正在读取的信息，后一个线程可能会以垃圾数据结束。这样做的结果可能是应用的崩溃，甚至更糟的是应用的损坏。\n\n最坏的情况是堆栈被例如一个通配符指针覆盖。在这种情况下，堆栈上的缓冲区或类似区域被写入超过其限制，从而通过用新数据填充来擦除堆栈的部分区域。这是一种缓冲区溢出，可能会导致应用崩溃，或者(恶意)利用应用。\n\n# 动态分析工具\n\n尽管调试器的价值很难忽视，但有时人们需要不同类型的工具来回答诸如内存使用、泄漏等问题，并诊断或防止线程问题。这就是诸如瓦尔基林动态分析工具套件中的工具可以提供很大帮助的地方。作为构建动态分析工具的框架，Valgrind 发行版目前包含我们感兴趣的以下工具:\n\n*   梅姆切克\n*   他妈的\n*   DRD\n\nMemcheck 是一个内存错误检测器，它允许我们发现内存泄漏、非法读写以及分配、解除分配和类似的内存相关问题。\n\n赫尔格林和 DRD 都是线程错误检测器。这基本上意味着他们将尝试检测任何多线程问题，如数据竞争和互斥锁的不正确使用。它们的不同之处在于，Helgrind 可以检测锁定顺序违规，并且 DRD 支持分离线程，同时使用的内存也比 Helgrind 少。\n\n# 限制\n\n动态分析工具的一个主要限制是，它们需要与主机操作系统紧密集成。这是 Valgrind 专注于 POSIX 线程的主要原因，目前在 Windows 上不工作。\n\nValgrind 网站(位于[http://valgrind.org/info/platforms.html](http://valgrind.org/info/platforms.html))对该问题的描述如下:\n\n\"Windows is not under consideration because porting to it would require so many changes it would almost be a separate project. (However, Valgrind + Wine can be made to work with some effort.) Also, non-open-source OSes are difficult to deal with; being able to see the OS and associated (libc) source code makes things much easier. However, Valgrind is quite usable in conjunction with Wine, which means that it is possible to run Windows programs under Valgrind with some effort.\"\n\n基本上，这意味着 Windows 应用可以在 Linux 下用 Valgrind 进行调试，但使用 Windows 作为操作系统不会很快发生。\n\nValgrind 确实在 OS X/macOS 上工作，从 OS X 10.8(山狮)开始。然而，由于苹果公司所做的改变，对最新版本的苹果电脑的支持可能有些不完整。与 Linux 版本的 Valgrind 一样，通常最好总是使用最新版本的 Valgrind。和 gdb 一样，使用发行版的包管理器，或者像 MacOS 上的 Homebrew 这样的第三方包管理器。\n\n# 可供选择的事物\n\nWindows 和其他平台上 Valgrind 工具的替代产品包括下表中列出的产品:\n\n| **名称** | **类型** | **平台** | **牌照** |\n| 记忆博士 | 内存检查器 | 所有主要平台 | 开放源码 |\n| gperftools (Google) | 堆、中央处理器和调用分析器 | Linux (x86) | 开放源码 |\n| 视觉检漏仪 | 内存检查器 | 视窗(视觉工作室) | 开放源码 |\n| 英特尔检查员 | 内存和线程调试器 | Windows、Linux | 所有人 |\n| 普里菲加 | 内存、性能 | Windows、Linux | 所有人 |\n| parasoft insurate ++ | 内存和线程调试器 | Windows、Solaris、Linux、AIX | 所有人 |\n\n# 梅姆切克\n\n当在其可执行文件的参数中没有指定其他工具时，Memcheck 是默认的 Valgrind 工具。Memcheck 本身是一个内存错误检测器，能够检测以下类型的问题:\n\n*   访问分配边界之外的内存、堆栈溢出以及访问以前释放的内存块\n*   使用未定义的值，这些值是尚未初始化的变量\n*   堆内存释放不当，包括重复释放块\n*   不匹配地使用 C 和 C++ 风格的内存分配以及数组分配器和解除分配器(`new[]`和`delete[]`)\n*   在函数中重叠源指针和目标指针，如`memcpy`\n*   将无效值(例如负值)作为尺寸参数传递给`malloc`或类似函数\n*   内存泄漏；也就是说，没有任何有效引用的堆块\n\n使用调试器或简单的任务管理器，几乎不可能检测到前面列表中给出的问题。Memcheck 的价值在于能够在开发的早期检测和修复问题，否则这些问题会导致数据损坏和神秘的崩溃。\n\n# 基本用途\n\n使用 Memcheck 相当容易。如果我们以我们在[第 4 章](04.html)、*线程同步和通信*中创建的演示应用为例，我们知道通常我们使用以下内容启动它:\n\n```cpp\n$ ./dispatcher_demo\n\n```\n\n要使用默认的 Memcheck 工具运行 Valgrind，同时将结果输出记录到日志文件中，我们可以按如下方式启动它:\n\n```cpp\n$ valgrind --log-file=dispatcher.log --read-var-info=yes --leak-check=full ./dispatcher_demo\n\n```\n\n使用前面的命令，我们将把 Memcheck 的输出记录到一个名为`dispatcher.log`的文件中，并使用二进制文件中的可用调试信息对内存泄漏进行全面检查，包括详细报告这些泄漏发生的位置。同样通过读取变量信息(`--read-var-info=yes`)，我们可以获得更多关于内存泄漏发生位置的详细信息。\n\n一个人不能登录到一个文件，但是除非它是一个非常简单的应用，否则从 Valgrind 产生的输出可能会太多，以至于它可能无法放入终端缓冲区。将输出作为一个文件可以让用户在以后使用它作为参考，也可以使用比终端通常提供的更高级的工具来搜索它。\n\n运行此程序后，我们可以检查生成的日志文件的内容，如下所示:\n\n```cpp\n==5764== Memcheck, a memory error detector\n==5764== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.\n==5764== Using Valgrind-3.11.0 and LibVEX; rerun with -h for copyright info\n==5764== Command: ./dispatcher_demo\n==5764== Parent PID: 2838\n==5764==\n==5764==\n==5764== HEAP SUMMARY:\n==5764==     in use at exit: 75,184 bytes in 71 blocks\n==5764==   total heap usage: 260 allocs, 189 frees, 88,678 bytes allocated\n==5764==\n==5764== 80 bytes in 10 blocks are definitely lost in loss record 1 of 5\n==5764==    at 0x4C2E0EF: operator new(unsigned long) (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)\n==5764==    by 0x402EFD: Dispatcher::init(int) (dispatcher.cpp:40)\n==5764==    by 0x409300: main (main.cpp:51)\n==5764==\n==5764== 960 bytes in 40 blocks are definitely lost in loss record 3 of 5\n==5764==    at 0x4C2E0EF: operator new(unsigned long) (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)\n==5764==    by 0x409338: main (main.cpp:60)\n==5764==\n==5764== 1,440 (1,200 direct, 240 indirect) bytes in 10 blocks are definitely lost in loss record 4 of 5\n==5764==    at 0x4C2E0EF: operator new(unsigned long) (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)\n==5764==    by 0x402EBB: Dispatcher::init(int) (dispatcher.cpp:38)\n==5764==    by 0x409300: main (main.cpp:51)\n==5764==\n==5764== LEAK SUMMARY:\n==5764==    definitely lost: 2,240 bytes in 60 blocks\n==5764==    indirectly lost: 240 bytes in 10 blocks\n==5764==      possibly lost: 0 bytes in 0 blocks\n==5764==    still reachable: 72,704 bytes in 1 blocks\n==5764==         suppressed: 0 bytes in 0 blocks\n==5764== Reachable blocks (those to which a pointer was found) are not shown.\n==5764== To see them, rerun with: --leak-check=full --show-leak-kinds=all\n==5764==\n==5764== For counts of detected and suppressed errors, rerun with: -v\n==5764== ERROR SUMMARY: 3 errors from 3 contexts (suppressed: 0 from 0) \n\n```\n\n这里我们可以看到，我们总共有三个内存泄漏。两个来自第 38 行和第 40 行`dispatcher`类的分配:\n\n```cpp\nw = new Worker; \n\n```\n\n另一个是这样的:\n\n```cpp\nt = new thread(&Worker::run, w); \n\n```\n\n我们还在第 60 行`main.cpp`中看到分配的泄漏:\n\n```cpp\nrq = new Request(); \n\n```\n\n虽然这些分配本身没有问题，但是如果我们在应用生命周期中跟踪它们，我们会注意到我们从未在这些对象上调用`delete`。如果我们要修复这些内存泄漏，我们需要删除那些`Request`实例，并清理`dispatcher`类析构函数中的`Worker`和`thread`实例。\n\n因为在这个演示应用中，整个应用在运行结束时被操作系统终止和清理，所以这并不是一个真正的问题。对于一个应用来说，在这个应用中，相同的调度器被用于不断生成和添加新的请求，同时还可能动态地扩展工作线程的数量，然而，这将是一个真正的问题。在这种情况下，必须小心解决这种内存泄漏。\n\n# 错误类型\n\nMemcheck 可以检测各种内存相关问题。以下各节总结了这些错误及其含义。\n\n# 非法读取/非法写入错误\n\n这些错误通常以以下格式报告:\n\n```cpp\nInvalid read of size <bytes>\nat 0x<memory address>: (location)\nby 0x<memory address>: (location)\nby 0x<memory address>: (location)\nAddress 0x<memory address> <error description>\n\n```\n\n前面错误消息的第一行告诉我们这是无效的读或写访问。接下来的几行将是详细描述执行无效读写的位置(可能还有源文件中的行)以及调用该代码的位置的回溯。\n\n最后，最后一行将详细说明发生的非法访问类型，例如读取已经释放的内存块。\n\n这种类型的错误表示写入或读取不应访问的内存部分。发生这种情况的原因可能是访问了一个通配符指针(即引用了一个随机的内存地址)，或者是代码中的早期问题导致计算了一个错误的内存地址，或者内存边界没有得到遵守，并且读取超过了数组或类似对象的边界。\n\n通常，当报告这种类型的错误时，应该高度重视，因为它表明一个基本问题，不仅会导致数据损坏和崩溃，还会导致其他人利用的错误。\n\n# 使用未初始化的值\n\n简而言之，这就是变量的值在没有被赋值的情况下被使用的问题。在这一点上，这些内容很可能只是内存中刚刚分配的那部分字节。因此，无论何时使用或访问这些内容，都会导致不可预测的行为。\n\n遇到时，Memcheck 将抛出类似以下的错误:\n\n```cpp\n$ valgrind --read-var-info=yes --leak-check=full ./unval\n==6822== Memcheck, a memory error detector\n==6822== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.\n==6822== Using Valgrind-3.11.0 and LibVEX; rerun with -h for copyright info\n==6822== Command: ./unval\n==6822== \n==6822== Conditional jump or move depends on uninitialised value(s)\n==6822==    at 0x4E87B83: vfprintf (vfprintf.c:1631)\n==6822==    by 0x4E8F898: printf (printf.c:33)\n==6822==    by 0x400541: main (unval.cpp:6)\n==6822== \n==6822== Use of uninitialised value of size 8\n==6822==    at 0x4E8476B: _itoa_word (_itoa.c:179)\n==6822==    by 0x4E8812C: vfprintf (vfprintf.c:1631)\n==6822==    by 0x4E8F898: printf (printf.c:33)\n==6822==    by 0x400541: main (unval.cpp:6)\n==6822== \n==6822== Conditional jump or move depends on uninitialised value(s)\n==6822==    at 0x4E84775: _itoa_word (_itoa.c:179)\n==6822==    by 0x4E8812C: vfprintf (vfprintf.c:1631)\n==6822==    by 0x4E8F898: printf (printf.c:33)\n==6822==    by 0x400541: main (unval.cpp:6)\n==6822== \n==6822== Conditional jump or move depends on uninitialised value(s)\n==6822==    at 0x4E881AF: vfprintf (vfprintf.c:1631)\n==6822==    by 0x4E8F898: printf (printf.c:33)\n==6822==    by 0x400541: main (unval.cpp:6)\n==6822== \n==6822== Conditional jump or move depends on uninitialised value(s)\n==6822==    at 0x4E87C59: vfprintf (vfprintf.c:1631)\n==6822==    by 0x4E8F898: printf (printf.c:33)\n==6822==    by 0x400541: main (unval.cpp:6)\n==6822== \n==6822== Conditional jump or move depends on uninitialised value(s)\n==6822==    at 0x4E8841A: vfprintf (vfprintf.c:1631)\n==6822==    by 0x4E8F898: printf (printf.c:33)\n==6822==    by 0x400541: main (unval.cpp:6)\n==6822== \n==6822== Conditional jump or move depends on uninitialised value(s)\n==6822==    at 0x4E87CAB: vfprintf (vfprintf.c:1631)\n==6822==    by 0x4E8F898: printf (printf.c:33)\n==6822==    by 0x400541: main (unval.cpp:6)\n==6822== \n==6822== Conditional jump or move depends on uninitialised value(s)\n==6822==    at 0x4E87CE2: vfprintf (vfprintf.c:1631)\n==6822==    by 0x4E8F898: printf (printf.c:33)\n==6822==    by 0x400541: main (unval.cpp:6)\n==6822== \n==6822== \n==6822== HEAP SUMMARY:\n==6822==     in use at exit: 0 bytes in 0 blocks\n==6822==   total heap usage: 1 allocs, 1 frees, 1,024 bytes allocated\n==6822== \n==6822== All heap blocks were freed -- no leaks are possible\n==6822== \n==6822== For counts of detected and suppressed errors, rerun with: -v\n==6822== Use --track-origins=yes to see where uninitialised values come from\n==6822== ERROR SUMMARY: 8 errors from 8 contexts (suppressed: 0 from 0)\n\n```\n\n这一系列特殊的错误是由以下一小部分代码引起的:\n\n```cpp\n#include <cstring>\n #include <cstdio>\n\n int main() {\n    int x;  \n    printf (\"x = %d\\n\", x); \n    return 0;\n } \n\n```\n\n正如我们在前面的代码中看到的，我们从不初始化我们的变量，它将被设置为任何随机值。如果幸运的话，它将被设置为零，或者一个同样(希望)无害的值。这段代码展示了我们未初始化的变量是如何进入库代码的。\n\n使用未初始化的变量是否有害很难说，很大程度上取决于变量的类型和受影响的代码。然而，简单地分配一个安全的默认值要比寻找和调试可能由未初始化的变量(随机)引起的神秘问题容易得多。\n\n有关未初始化变量来源的更多信息，可以将`-track-origins=yes`标志传递给 Memcheck。这将告诉它为每个变量保留更多的信息，这将使跟踪这类问题变得更加容易。\n\n# 未初始化或不可寻址的系统调用值\n\n每当调用函数时，未初始化的值都有可能作为参数传递，甚至是指向不可寻址缓冲区的指针。无论是哪种情况，Memcheck 都将记录以下内容:\n\n```cpp\n$ valgrind --read-var-info=yes --leak-check=full ./unsyscall\n==6848== Memcheck, a memory error detector\n==6848== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.\n==6848== Using Valgrind-3.11.0 and LibVEX; rerun with -h for copyright info\n==6848== Command: ./unsyscall\n==6848== \n==6848== Syscall param write(buf) points to uninitialised byte(s)\n==6848==    at 0x4F306E0: __write_nocancel (syscall-template.S:84)\n==6848==    by 0x4005EF: main (unsyscall.cpp:7)\n==6848==  Address 0x5203040 is 0 bytes inside a block of size 10 alloc'd\n==6848==    at 0x4C2DB8F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)\n==6848==    by 0x4005C7: main (unsyscall.cpp:5)\n==6848== \n==6848== Syscall param exit_group(status) contains uninitialised byte(s)\n==6848==    at 0x4F05B98: _Exit (_exit.c:31)\n==6848==    by 0x4E73FAA: __run_exit_handlers (exit.c:97)\n==6848==    by 0x4E74044: exit (exit.c:104)\n==6848==    by 0x4005FC: main (unsyscall.cpp:8)\n==6848== \n==6848== \n==6848== HEAP SUMMARY:\n==6848==     in use at exit: 14 bytes in 2 blocks\n==6848==   total heap usage: 2 allocs, 0 frees, 14 bytes allocated\n==6848== \n==6848== LEAK SUMMARY:\n==6848==    definitely lost: 0 bytes in 0 blocks\n==6848==    indirectly lost: 0 bytes in 0 blocks\n==6848==      possibly lost: 0 bytes in 0 blocks\n==6848==    still reachable: 14 bytes in 2 blocks\n==6848==         suppressed: 0 bytes in 0 blocks\n==6848== Reachable blocks (those to which a pointer was found) are not shown.\n==6848== To see them, rerun with: --leak-check=full --show-leak-kinds=all\n==6848== \n==6848== For counts of detected and suppressed errors, rerun with: -v\n==6848== Use --track-origins=yes to see where uninitialised values come from\n==6848== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 0 from 0)\n\n```\n\n前面的日志是由以下代码生成的:\n\n```cpp\n#include <cstdlib>\n #include <unistd.h> \n\n int main() {  \n    char* arr  = (char*) malloc(10);  \n    int*  arr2 = (int*) malloc(sizeof(int));  \n    write(1, arr, 10 ); \n    exit(arr2[0]);\n } \n\n```\n\n就像上一节详细介绍的未初始化值的一般使用一样，传递未初始化的或不可靠的参数至少是有风险的，在最坏的情况下，是崩溃、数据损坏或更糟的来源。\n\n# 非法免费\n\n非法释放或删除通常是试图在已经释放的内存块上重复调用`free()`或`delete()`。虽然不一定是有害的，但这可能表明设计不好，绝对需要修复。\n\n当试图使用不指向存储块开头的指针来释放该存储块时，也会发生这种情况。这就是为什么我们不应该对从调用`malloc()`或`new()`获得的原始指针执行指针运算，而应该使用副本的主要原因之一。\n\n# 不匹配的解除分配\n\n内存块的分配和解除分配应该始终使用匹配函数来执行。这意味着，当我们使用 C 风格的函数进行分配时，我们从同一个 API 中用匹配的函数进行解除分配。C++ 风格的分配和解除分配也是如此。\n\n简而言之，这意味着:\n\n*   如果使用`malloc`、`calloc`、`valloc`、`realloc,`或`memalign`进行分配，则使用`free`解除分配\n*   如果我们用新的分配，我们用`delete`解除分配\n*   如果我们用`new[]`分配，我们就用`delete[]`解除分配\n\n把这些混在一起不一定会引起问题，但这样做是不明确的行为。后一种类型的分配和解除分配特定于阵列。对于分配了`new[]`的数组，不使用`delete[]`可能会导致内存泄漏，甚至更糟。\n\n# 源和目标重叠\n\n这种类型的错误表示为源和目标内存块传递的指针重叠(基于预期大小)。这种错误的结果通常是某种形式的损坏或系统崩溃。\n\n# 可疑的论点价值\n\n对于内存分配函数，Memcheck 验证传递给它们的参数是否真正有意义。这方面的一个例子是传递一个负的大小，或者它会远远超过一个合理的分配大小:例如，一个 1pb 内存的分配请求。最有可能的是，这些值可能是代码早期错误计算的结果。\n\nMemcheck 会像本例中的 Memcheck 手册一样报告此错误:\n\n```cpp\n==32233== Argument 'size' of function malloc has a fishy (possibly negative) value: -3\n==32233==    at 0x4C2CFA7: malloc (vg_replace_malloc.c:298)\n==32233==    by 0x400555: foo (fishy.c:15)\n==32233==    by 0x400583: main (fishy.c:23)\n\n```\n\n这里试图将-3 的值传递给`malloc`，这显然没有多大意义。因为这显然是一个无意义的操作，它表明代码中有一个严重的错误。\n\n# 内存泄漏检测\n\nMemcheck 报告内存泄漏时要记住的最重要的一点是，大量报告的*泄漏*实际上可能不是泄漏。这反映在 Memcheck 报告其发现的任何潜在问题的方式中，如下所示:\n\n*   肯定输了\n*   间接损失\n*   可能丢失了\n\n在三种可能的报告类型中，**肯定丢失了**类型是唯一一种绝对确定所讨论的内存块不再可达的类型，没有剩余的指针或引用，这使得应用不可能释放内存。\n\n在**间接丢失**类型的情况下，我们没有丢失指向这些内存块本身的指针，而是丢失了指向引用这些块的结构的指针。例如，当我们直接失去对数据结构(如红/黑或二叉树)的根节点的访问时，就会出现这种情况。结果，我们也失去了访问任何子节点的能力。\n\n最后，**可能丢失**是一个包罗万象的类型，Memcheck 不能完全确定是否还有对内存块的引用。这可能发生在存在内部指针的地方，例如特定类型的数组分配。也可以通过使用多重继承来实现，其中 C++ 对象使用自引用。\n\n如前所述，在 Memcheck 的基本使用部分，建议总是在指定`--leak-check=full`的情况下运行 Memcheck，以获得关于内存泄漏的确切位置的详细信息。\n\n# 他妈的\n\nHelgrind 的目的是检测多线程应用中同步实现的问题。它可以检测 POSIX 线程的错误使用、由于错误的锁定顺序而导致的潜在死锁问题以及数据争用——在没有线程同步的情况下读写数据。\n\n# 基本用途\n\n我们通过以下方式在应用中启动 Helgrind:\n\n```cpp\n$ valgrind --tool=helgrind --read-var-info=yes --log-file=dispatcher_helgrind.log ./dispatcher_demo\n\n```\n\n与运行 Memcheck 类似，这将运行应用并将所有生成的输出记录到日志文件中，同时显式使用二进制文件中的所有可用调试信息。\n\n运行应用后，我们检查生成的日志文件:\n\n```cpp\n==6417== Helgrind, a thread error detector\n==6417== Copyright (C) 2007-2015, and GNU GPL'd, by OpenWorks LLP et al.\n==6417== Using Valgrind-3.11.0 and LibVEX; rerun with -h for copyright info\n==6417== Command: ./dispatcher_demo\n==6417== Parent PID: 2838\n==6417== \n==6417== ---Thread-Announcement------------------------------------------\n==6417== \n==6417== Thread #1 is the program's root thread \n\n```\n\n在获得关于应用和 Valgrind 版本的初始基本信息后，我们被告知根线程已经创建:\n\n```cpp\n==6417== \n==6417== ---Thread-Announcement------------------------------------------\n==6417== \n==6417== Thread #2 was created\n==6417==    at 0x56FB7EE: clone (clone.S:74)\n==6417==    by 0x53DE149: create_thread (createthread.c:102)\n==6417==    by 0x53DFE83: pthread_create@@GLIBC_2.2.5 (pthread_create.c:679)\n==6417==    by 0x4C34BB7: ??? (in /usr/lib/valgrind/vgpreload_helgrind-amd64-linux.so)\n==6417==    by 0x4EF8DC2: std::thread::_M_start_thread(std::shared_ptr<std::thread::_Impl_base>, void (*)()) (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21)\n==6417==    by 0x403AD7: std::thread::thread<void (Worker::*)(), Worker*&>(void (Worker::*&&)(), Worker*&) (thread:137)\n==6417==    by 0x4030E6: Dispatcher::init(int) (dispatcher.cpp:40)\n==6417==    by 0x4090A0: main (main.cpp:51)\n==6417== \n==6417== ----------------------------------------------------------------\n\n```\n\n第一个线程由调度程序创建并记录。接下来我们得到第一个警告:\n\n```cpp\n==6417== \n==6417==  Lock at 0x60F4A0 was first observed\n==6417==    at 0x4C321BC: ??? (in /usr/lib/valgrind/vgpreload_helgrind-amd64-linux.so)\n==6417==    by 0x401CD1: __gthread_mutex_lock(pthread_mutex_t*) (gthr-default.h:748)\n==6417==    by 0x402103: std::mutex::lock() (mutex:135)\n==6417==    by 0x40337E: Dispatcher::addWorker(Worker*) (dispatcher.cpp:108)\n==6417==    by 0x401DF9: Worker::run() (worker.cpp:49)\n==6417==    by 0x408FA4: void std::_Mem_fn_base<void (Worker::*)(), true>::operator()<, void>(Worker*) const (in /media/sf_Projects/Cerflet/dispatcher/dispatcher_demo)\n==6417==    by 0x408F38: void std::_Bind_simple<std::_Mem_fn<void (Worker::*)()> (Worker*)>::_M_invoke<0ul>(std::_Index_tuple<0ul>) (functional:1531)\n==6417==    by 0x408E3F: std::_Bind_simple<std::_Mem_fn<void (Worker::*)()> (Worker*)>::operator()() (functional:1520)\n==6417==    by 0x408D47: std::thread::_Impl<std::_Bind_simple<std::_Mem_fn<void (Worker::*)()> (Worker*)> >::_M_run() (thread:115)\n==6417==    by 0x4EF8C7F: ??? (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21)\n==6417==    by 0x4C34DB6: ??? (in /usr/lib/valgrind/vgpreload_helgrind-amd64-linux.so)\n==6417==    by 0x53DF6B9: start_thread (pthread_create.c:333)\n==6417==  Address 0x60f4a0 is 0 bytes inside data symbol \"_ZN10Dispatcher12workersMutexE\"\n==6417== \n==6417== Possible data race during write of size 1 at 0x5CD9261 by thread #1\n==6417== Locks held: 1, at address 0x60F4A0\n==6417==    at 0x403650: Worker::setRequest(AbstractRequest*) (worker.h:38)\n==6417==    by 0x403253: Dispatcher::addRequest(AbstractRequest*) (dispatcher.cpp:70)\n==6417==    by 0x409132: main (main.cpp:63)\n==6417== \n==6417== This conflicts with a previous read of size 1 by thread #2\n==6417== Locks held: none\n==6417==    at 0x401E02: Worker::run() (worker.cpp:51)\n==6417==    by 0x408FA4: void std::_Mem_fn_base<void (Worker::*)(), true>::operator()<, void>(Worker*) const (in /media/sf_Projects/Cerflet/dispatcher/dispatcher_demo)\n==6417==    by 0x408F38: void std::_Bind_simple<std::_Mem_fn<void (Worker::*)()> (Worker*)>::_M_invoke<0ul>(std::_Index_tuple<0ul>) (functional:1531)\n==6417==    by 0x408E3F: std::_Bind_simple<std::_Mem_fn<void (Worker::*)()> (Worker*)>::operator()() (functional:1520)\n==6417==    by 0x408D47: std::thread::_Impl<std::_Bind_simple<std::_Mem_fn<void (Worker::*)()> (Worker*)> >::_M_run() (thread:115)\n==6417==    by 0x4EF8C7F: ??? (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21)\n==6417==    by 0x4C34DB6: ??? (in /usr/lib/valgrind/vgpreload_helgrind-amd64-linux.so)\n==6417==    by 0x53DF6B9: start_thread (pthread_create.c:333)\n==6417==  Address 0x5cd9261 is 97 bytes inside a block of size 104 alloc'd\n==6417==    at 0x4C2F50F: operator new(unsigned long) (in /usr/lib/valgrind/vgpreload_helgrind-amd64-linux.so)\n==6417==    by 0x40308F: Dispatcher::init(int) (dispatcher.cpp:38)\n==6417==    by 0x4090A0: main (main.cpp:51)\n==6417==  Block was alloc'd by thread #1\n==6417== \n==6417== ----------------------------------------------------------------\n\n```\n\n在前面的警告中，Helgrind 告诉我们线程 IDs 1 和 2 之间的大小为 1 的冲突读取。因为 C++ 11 线程应用编程接口使用了大量的模板，所以跟踪可能有些难以阅读。精髓就在这几行字里:\n\n```cpp\n==6417==    at 0x403650: Worker::setRequest(AbstractRequest*) (worker.h:38) ==6417==    at 0x401E02: Worker::run() (worker.cpp:51) \n\n```\n\n这对应于以下代码行:\n\n```cpp\nvoid setRequest(AbstractRequest* request) { this->request = request; ready = true; }\nwhile (!ready && running) { \n\n```\n\n在这些代码行中，大小为 1 的唯一变量是布尔变量`ready`。由于这是一个布尔变量，我们知道它是一个原子操作(详见[第 8 章](08.html)、*原子操作-使用硬件*)。因此，我们可以忽略这个警告。\n\n接下来，我们得到了这个线程的另一个警告:\n\n```cpp\n==6417== Possible data race during write of size 1 at 0x5CD9260 by thread #1\n==6417== Locks held: none\n==6417==    at 0x40362C: Worker::stop() (worker.h:37)\n==6417==    by 0x403184: Dispatcher::stop() (dispatcher.cpp:50)\n==6417==    by 0x409163: main (main.cpp:70)\n==6417== \n==6417== This conflicts with a previous read of size 1 by thread #2 ==6417== Locks held: none\n==6417==    at 0x401E0E: Worker::run() (worker.cpp:51)\n==6417==    by 0x408FA4: void std::_Mem_fn_base<void (Worker::*)(), true>::operator()<, void>(Worker*) const (in /media/sf_Projects/Cerflet/dispatcher/dispatcher_demo)\n==6417==    by 0x408F38: void std::_Bind_simple<std::_Mem_fn<void (Worker::*)()> (Worker*)>::_M_invoke<0ul>(std::_Index_tuple<0ul>) (functional:1531)\n==6417==    by 0x408E3F: std::_Bind_simple<std::_Mem_fn<void (Worker::*)()> (Worker*)>::operator()() (functional:1520)\n==6417==    by 0x408D47: std::thread::_Impl<std::_Bind_simple<std::_Mem_fn<void (Worker::*)()> (Worker*)> >::_M_run() (thread:115)\n==6417==    by 0x4EF8C7F: ??? (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21)\n==6417==    by 0x4C34DB6: ??? (in /usr/lib/valgrind/vgpreload_helgrind-amd64-linux.so)\n==6417==    by 0x53DF6B9: start_thread (pthread_create.c:333)\n==6417==  Address 0x5cd9260 is 96 bytes inside a block of size 104 alloc'd\n==6417==    at 0x4C2F50F: operator new(unsigned long) (in /usr/lib/valgrind/vgpreload_helgrind-amd64-linux.so)\n==6417==    by 0x40308F: Dispatcher::init(int) (dispatcher.cpp:38)\n==6417==    by 0x4090A0: main (main.cpp:51)\n==6417==  Block was alloc'd by thread #1 \n\n```\n\n与第一个警告类似，这也指的是一个布尔变量——这里是`Worker`实例中的`running`变量。由于这也是原子操作，我们可以再次忽略这个警告。\n\n在这个警告之后，我们得到了其他线程的这些警告的重复。我们也看到这个警告重复了很多次:\n\n```cpp\n==6417==  Lock at 0x60F540 was first observed\n==6417==    at 0x4C321BC: ??? (in /usr/lib/valgrind/vgpreload_helgrind-amd64-linux.so)\n==6417==    by 0x401CD1: __gthread_mutex_lock(pthread_mutex_t*) (gthr-default.h:748)\n==6417==    by 0x402103: std::mutex::lock() (mutex:135)\n==6417==    by 0x409044: logFnc(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >) (main.cpp:40)\n==6417==    by 0x40283E: Request::process() (request.cpp:19)\n==6417==    by 0x401DCE: Worker::run() (worker.cpp:44)\n==6417==    by 0x408FA4: void std::_Mem_fn_base<void (Worker::*)(), true>::operator()<, void>(Worker*) const (in /media/sf_Projects/Cerflet/dispatcher/dispatcher_demo)\n==6417==    by 0x408F38: void std::_Bind_simple<std::_Mem_fn<void (Worker::*)()> (Worker*)>::_M_invoke<0ul>(std::_Index_tuple<0ul>) (functional:1531)\n==6417==    by 0x408E3F: std::_Bind_simple<std::_Mem_fn<void (Worker::*)()> (Worker*)>::operator()() (functional:1520)\n==6417==    by 0x408D47: std::thread::_Impl<std::_Bind_simple<std::_Mem_fn<void (Worker::*)()> (Worker*)> >::_M_run() (thread:115)\n==6417==    by 0x4EF8C7F: ??? (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21)\n==6417==    by 0x4C34DB6: ??? (in /usr/lib/valgrind/vgpreload_helgrind-amd64-linux.so)\n==6417==  Address 0x60f540 is 0 bytes inside data symbol \"logMutex\"\n==6417== \n==6417== Possible data race during read of size 8 at 0x60F238 by thread #1\n==6417== Locks held: none\n==6417==    at 0x4F4ED6F: std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long) (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21)\n==6417==    by 0x4F4F236: std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*) (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21)\n==6417==    by 0x403199: Dispatcher::stop() (dispatcher.cpp:53)\n==6417==    by 0x409163: main (main.cpp:70)\n==6417== \n==6417== This conflicts with a previous write of size 8 by thread #7\n==6417== Locks held: 1, at address 0x60F540\n==6417==    at 0x4F4EE25: std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long) (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21)\n==6417==    by 0x409055: logFnc(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >) (main.cpp:41)\n==6417==    by 0x402916: Request::finish() (request.cpp:27)\n==6417==    by 0x401DED: Worker::run() (worker.cpp:45)\n==6417==    by 0x408FA4: void std::_Mem_fn_base<void (Worker::*)(), true>::operator()<, void>(Worker*) const (in /media/sf_Projects/Cerflet/dispatcher/dispatcher_demo)\n==6417==    by 0x408F38: void std::_Bind_simple<std::_Mem_fn<void (Worker::*)()> (Worker*)>::_M_invoke<0ul>(std::_Index_tuple<0ul>) (functional:1531)\n==6417==    by 0x408E3F: std::_Bind_simple<std::_Mem_fn<void (Worker::*)()> (Worker*)>::operator()() (functional:1520)\n==6417==    by 0x408D47: std::thread::_Impl<std::_Bind_simple<std::_Mem_fn<void (Worker::*)()> (Worker*)> >::_M_run() (thread:115)\n==6417==  Address 0x60f238 is 24 bytes inside data symbol \"_ZSt4cout@@GLIBCXX_3.4\"  \n\n```\n\n此警告是由于线程之间没有同步使用标准输出而触发的。即使这个演示应用的日志记录功能使用互斥来同步工作线程记录的文本，我们也在一些位置以不安全的方式写入标准输出。\n\n通过使用中央线程安全的日志记录功能，这相对容易修复。尽管它不太可能导致任何稳定性问题，但它很可能会导致任何日志输出最终变成一团乱麻，无法使用。\n\n# 滥用 pthreads 应用编程接口\n\nHelgrind 检测到大量涉及 pthreads API 的错误，其手册对此进行了总结，下面列出:\n\n*   解锁无效互斥体\n*   解锁未锁定的互斥体\n*   解锁由不同线程持有的互斥体\n*   销毁无效或锁定的互斥体\n*   递归锁定非递归互斥体\n*   释放包含锁定互斥体的内存\n*   将互斥参数传递给需要读写锁参数的函数，反之亦然\n*   POSIX pthread 函数失败，出现必须处理的错误代码\n*   线程退出时仍持有锁定的锁\n*   用未锁定的互斥体、无效互斥体或被不同线程锁定的互斥体调用`pthread_cond_wait`\n*   条件变量及其关联互斥体之间的绑定不一致\n*   pthread 屏障的初始化无效或重复\n*   线程仍在等待的 pthread 屏障的初始化\n*   销毁从未初始化或线程仍在等待的 pthread barrier 对象\n*   等待未初始化的 pthread 屏障\n\n除此之外，如果 Helgrind 本身没有检测到错误，但是 pthreads 库本身为 Helgrind 截获的每个函数返回一个错误，Helgrind 也会报告一个错误。\n\n# 锁定订单问题\n\n锁顺序检测使用的假设是，一旦一系列锁以特定的顺序被访问，这就是它们将总是被使用的顺序。例如，想象一个由两个锁保护的资源。正如我们在[第 4 章](04.html)、*线程同步和通信*中看到的那样，我们在其 dispatcher 类中使用了两个互斥锁，一个管理对工作线程的访问，另一个管理对请求实例的访问。\n\n在该代码的正确实现中，我们总是确保在尝试获取另一个互斥体之前解锁一个互斥体，因为有可能另一个线程已经获得了对第二个互斥体的访问，并试图获得对第一个互斥体的访问，从而造成死锁情况。\n\n虽然有用，但重要的是要认识到，到目前为止，这种检测算法在某些领域还不完善。这在使用例如条件变量时最为明显，条件变量自然会使用一个锁定顺序，该顺序往往会被 Helgrind 报告为*错误*。\n\n这里的要点是，我们必须检查这些日志消息并判断它们的价值，但与直接滥用多线程应用编程接口不同，报告的问题是否是假阳性远没有那么明确。\n\n# 数据竞赛\n\n本质上，数据竞争是指两个以上的线程试图在没有任何同步机制的情况下读取或写入同一资源。在这里，只有一次并发读写，或者两次同时写入，实际上是有害的；因此，只报告这两种类型的访问。\n\n在关于 Helgrind 基本用法的前面一节中，我们在日志中看到了一些这类错误的例子。在那里，它涉及变量的同时写入和读取。正如我们在那一节中也谈到的，Helgrind 并不关心写或读是否是原子的，而只是报告一个潜在的问题。\n\n就像锁顺序问题一样，这再次意味着必须根据每个数据竞争报告的优点来判断，因为许多报告可能是误报。\n\n# DRD\n\nDRD 与赫尔格林非常相似，因为它也能检测应用中的线程和同步问题。DRD 与赫尔格伦的主要不同之处如下:\n\n*   DRD 使用较少的内存\n*   DRD 没有检测到违反锁定命令的情况\n*   DRD 支持分离线程\n\n一般来说，我们希望同时运行 DRD 和赫尔格林来比较两者的输出。由于许多潜在的问题是高度不确定的，因此使用这两种工具通常有助于找出最严重的问题。\n\n# 基本用途\n\n启动 DRD 非常类似于启动其他工具-我们只需要像这样指定我们想要的工具:\n\n```cpp\n$ valgrind --tool=drd --log-file=dispatcher_drd.log --read-var-info=yes ./dispatcher_demo\n\n```\n\n应用完成后，我们检查生成的日志文件的内容。\n\n```cpp\n==6576== drd, a thread error detector\n==6576== Copyright (C) 2006-2015, and GNU GPL'd, by Bart Van Assche.\n==6576== Using Valgrind-3.11.0 and LibVEX; rerun with -h for copyright info\n==6576== Command: ./dispatcher_demo\n==6576== Parent PID: 2838\n==6576== \n==6576== Conflicting store by thread 1 at 0x05ce51b1 size 1\n==6576==    at 0x403650: Worker::setRequest(AbstractRequest*) (worker.h:38)\n==6576==    by 0x403253: Dispatcher::addRequest(AbstractRequest*) (dispatcher.cpp:70)\n==6576==    by 0x409132: main (main.cpp:63)\n==6576== Address 0x5ce51b1 is at offset 97 from 0x5ce5150\\. Allocation context:\n==6576==    at 0x4C3150F: operator new(unsigned long) (in /usr/lib/valgrind/vgpreload_drd-amd64-linux.so)\n==6576==    by 0x40308F: Dispatcher::init(int) (dispatcher.cpp:38)\n==6576==    by 0x4090A0: main (main.cpp:51)\n==6576== Other segment start (thread 2)\n==6576==    at 0x4C3818C: pthread_mutex_unlock (in /usr/lib/valgrind/vgpreload_drd-amd64-linux.so)\n==6576==    by 0x401D00: __gthread_mutex_unlock(pthread_mutex_t*) (gthr-default.h:778)\n==6576==    by 0x402131: std::mutex::unlock() (mutex:153)\n==6576==    by 0x403399: Dispatcher::addWorker(Worker*) (dispatcher.cpp:110)\n==6576==    by 0x401DF9: Worker::run() (worker.cpp:49)\n==6576==    by 0x408FA4: void std::_Mem_fn_base<void (Worker::*)(), true>::operator()<, void>(Worker*) const (in /media/sf_Projects/Cerflet/dispatcher/dispatcher_demo)\n==6576==    by 0x408F38: void std::_Bind_simple<std::_Mem_fn<void (Worker::*)()> (Worker*)>::_M_invoke<0ul>(std::_Index_tuple<0ul>) (functional:1531)\n==6576==    by 0x408E3F: std::_Bind_simple<std::_Mem_fn<void (Worker::*)()> (Worker*)>::operator()() (functional:1520)\n==6576==    by 0x408D47: std::thread::_Impl<std::_Bind_simple<std::_Mem_fn<void (Worker::*)()> (Worker*)> >::_M_run() (thread:115)\n==6576==    by 0x4F04C7F: ??? (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21)\n==6576==    by 0x4C3458B: ??? (in /usr/lib/valgrind/vgpreload_drd-amd64-linux.so)\n==6576==    by 0x53EB6B9: start_thread (pthread_create.c:333)\n==6576== Other segment end (thread 2)\n==6576==    at 0x4C3725B: pthread_mutex_lock (in /usr/lib/valgrind/vgpreload_drd-amd64-linux.so)\n==6576==    by 0x401CD1: __gthread_mutex_lock(pthread_mutex_t*) (gthr-default.h:748)\n==6576==    by 0x402103: std::mutex::lock() (mutex:135)\n==6576==    by 0x4023F8: std::unique_lock<std::mutex>::lock() (mutex:485)\n==6576==    by 0x40219D: std::unique_lock<std::mutex>::unique_lock(std::mutex&) (mutex:415)\n==6576==    by 0x401E33: Worker::run() (worker.cpp:52)\n==6576==    by 0x408FA4: void std::_Mem_fn_base<void (Worker::*)(), true>::operator()<, void>(Worker*) const (in /media/sf_Projects/Cerflet/dispatcher/dispatcher_demo)\n==6576==    by 0x408F38: void std::_Bind_simple<std::_Mem_fn<void (Worker::*)()> (Worker*)>::_M_invoke<0ul>(std::_Index_tuple<0ul>) (functional:1531)\n==6576==    by 0x408E3F: std::_Bind_simple<std::_Mem_fn<void (Worker::*)()> (Worker*)>::operator()() (functional:1520)\n==6576==    by 0x408D47: std::thread::_Impl<std::_Bind_simple<std::_Mem_fn<void (Worker::*)()> (Worker*)> >::_M_run() (thread:115)\n==6576==    by 0x4F04C7F: ??? (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21)\n==6576==    by 0x4C3458B: ??? (in /usr/lib/valgrind/vgpreload_drd-amd64-linux.so) \n\n```\n\n前面的总结基本上重复了我们在 Helgrind 日志中看到的内容。我们看到相同的数据竞争报告(冲突存储)，由于原子性，我们可以安全地忽略它。至少对于这个特定的代码，DRD 的使用没有增加任何我们在使用 Helgrind 时不知道的东西。\n\n无论如何，使用两种工具总是一个好主意，以防一种工具发现另一种没有发现的东西。\n\n# 特征\n\nDRD 将检测到以下错误:\n\n*   数据竞赛\n*   锁争用(死锁和延迟)\n*   滥用 pthreads 应用编程接口\n\n关于第三点，根据 DRD 的手册，其检测到的错误列表与 Helgrind 的非常相似:\n\n*   将一种类型的同步对象(例如互斥体)的地址传递给 POSIX API 调用，该调用需要指向另一种类型的同步对象(例如条件变量)的指针\n*   尝试解锁尚未锁定的互斥体\n*   尝试解锁被另一个线程锁定的互斥体\n*   尝试递归锁定类型为`PTHREAD_MUTEX_NORMAL`或自旋锁的互斥体\n*   锁定互斥体的销毁或解除分配\n*   当与条件变量相关联的互斥体上没有锁时，向条件变量发送信号\n*   在未被锁定的互斥体上调用`pthread_cond_wait`，也就是说，被另一个线程锁定或者已经被递归锁定\n*   通过`pthread_cond_wait`将两个不同的互斥体与一个条件变量相关联\n*   正在等待的条件变量的销毁或解除分配\n*   销毁或解除分配锁定的读写同步对象\n*   尝试解锁未被调用线程锁定的读取器-写入器同步对象\n*   试图以独占方式递归锁定读取器-写入器同步对象\n*   尝试将用户定义的读写器同步对象的地址传递给 POSIX 线程函数\n*   尝试将 POSIX 读写同步对象的地址传递给用户定义的读写同步对象的注释之一\n*   互斥体、条件变量、读写锁、信号量或障碍的重新初始化\n*   正在等待的信号量或障碍的销毁或解除分配\n*   屏障等待和屏障破坏之间缺少同步\n*   退出线程而不首先解锁该线程锁定的自旋锁、互斥锁或读写同步对象\n*   将无效的线程标识传递给`pthread_join`或`pthread_cancel`\n\n如前所述，DRD 也支持分离线程，这一点很有帮助。锁定顺序检查是否重要取决于个人的应用。\n\n# C++ 11 线程支持\n\nDRD 手册包含关于 C++ 11 线程支持的这一部分。\n\n如果您想使用`c++ 11`类`std::thread`，您需要做以下操作来注释在该类的实现中使用的`std::shared_ptr<>`对象:\n\n*   在包含任何 C++ 头文件之前，在公共头文件的开头或每个源文件的开头添加以下代码:\n\n```cpp\n    #include <valgrind/drd.h>\n    #define _GLIBCXX_SYNCHRONIZATION_HAPPENS_BEFORE(addr)\n    ANNOTATE_HAPPENS_BEFORE(addr)\n    #define _GLIBCXX_SYNCHRONIZATION_HAPPENS_AFTER(addr)\n    ANNOTATE_HAPPENS_AFTER(addr)\n\n```\n\n*   下载 GCC 源代码，从源文件`libstdc++-v3/src/c++ 11/thread.cc`中，将`execute_native_thread_routine()`和`std::thread::_M_start_thread()`函数的实现复制到与您的应用链接的源文件中。确保也在这个源文件中正确定义了`_GLIBCXX_SYNCHRONIZATION_HAPPENS_*()`宏。\n\n当在使用 C++ 11 线程应用编程接口的应用中使用 DRD 时，可能会看到很多误报，这可以通过前面的*修复*来修复。\n\n然而，当使用 GCC 5.4 和 Valgrind 3.11 时(可能也使用旧版本)，这个问题似乎不再存在。然而，当使用 C++ 11 线程应用编程接口时，突然发现自己的 DRD 输出中有很多误报时，需要记住这一点。\n\n# 摘要\n\n在本章中，我们将了解如何调试多线程应用。我们探讨了在多线程环境中使用调试器的基础知识。接下来，我们看到了如何在 Valgrind 框架中使用三个工具，它们可以帮助我们跟踪多线程和其他关键问题。\n\n在这一点上，我们可以使用前面章节中的信息编写应用，并分析它们是否存在任何应该修复的问题，包括内存泄漏和同步机制的不当使用。\n\n在下一章中，我们将带着我们所学的一切，看看多线程编程和开发的一些最佳实践。"
  },
  {
    "path": "docs/master-cpp-multithrd/07.md",
    "content": "# 七、最佳实践\n\n和大多数事情一样，最好避免犯错，而不是事后改正。本章讨论了多线程应用的一些常见错误和设计问题，并展示了避免常见和不常见问题的方法。\n\n本章的主题包括:\n\n*   常见的多线程问题，如死锁和数据竞争。\n*   互斥、锁和陷阱的正确使用。\n*   使用静态初始化时的潜在问题。\n\n# 适当的多线程\n\n在前面的章节中，我们已经看到了编写多线程代码时可能出现的各种潜在问题。这些问题从显而易见的，比如两个线程不能同时写入同一个位置，到更微妙的，比如互斥锁的不正确使用。\n\n还有许多不是多线程代码直接组成部分的元素的问题，然而这些问题可能会导致看似随机的崩溃和其他令人沮丧的问题。这方面的一个例子是变量的静态初始化。在接下来的几节中，我们将研究所有这些问题以及更多的问题，以及防止不得不处理这些问题的方法。\n\n就像生活中的许多事情一样，它们是有趣的经历，但你通常不愿意重复它们。\n\n# 错误的期望-僵局\n\n死锁的名字已经被描述得非常简洁了。当两个或多个进程试图访问另一个进程持有的资源，而另一个线程同时等待访问它持有的资源时，就会发生这种情况。\n\n例如:\n\n1.  线程 1 获得对资源 A 的访问\n2.  线程 1 和 2 都想访问资源 B\n3.  线程 2 获胜，现在拥有 B，线程 1 仍在等待 B\n4.  线程 2 现在想使用 A，等待访问\n5.  线程 1 和 2 永远等待资源\n\n在这种情况下，我们假设线程将能够在某个时候访问每个资源，而事实恰恰相反，这要归功于每个线程都持有另一个线程需要的资源。\n\n想象一下，这个死锁过程如下所示:\n\n![](img/2c309731-2af1-4511-95bf-5aa2efc7a60e.png)\n\n这清楚地表明，防止死锁的两个基本规则是:\n\n*   任何时候都不要超过一把锁。\n*   尽快释放所有锁。\n\n我们在[第 4 章](04.html)、*线程同步和通信*中看到了一个真实的例子，当时我们看了调度器演示代码。这段代码包含两个互斥锁，用于安全访问两个数据结构:\n\n```cpp\nvoid Dispatcher::addRequest(AbstractRequest* request) {\n    workersMutex.lock();\n    if (!workers.empty()) {\n          Worker* worker = workers.front();\n          worker->setRequest(request);\n          condition_variable* cv;\n          mutex* mtx;\n          worker->getCondition(cv);\n          worker->getMutex(mtx);\n          unique_lock<mutex> lock(*mtx);\n          cv->notify_one();\n          workers.pop();\n          workersMutex.unlock();\n    }\n    else {\n          workersMutex.unlock();\n          requestsMutex.lock();\n          requests.push(request);\n          requestsMutex.unlock();\n    }\n } \n\n```\n\n这里的互斥体是`workersMutex`和`requestsMutex`变量。我们可以清楚地看到，在试图访问另一个互斥体之前，我们在任何时候都没有抓住这个互斥体。我们在方法的开始显式锁定`workersMutex`，这样我们就可以安全地检查 workers 数据结构是否为空。\n\n如果不是空的，我们会将新的请求交给一个工作人员。然后，当我们处理完工人、数据结构时，我们释放互斥体。此时，我们保留零互斥体。这里没有什么太复杂的，因为我们只使用一个互斥体。\n\n有趣的是在 else 语句中，因为当没有等待的工作者并且我们需要获得第二个互斥体时。当我们进入这个范围时，我们保留一个互斥体。我们可以尝试获取`requestsMutex`并假设它会工作，然而这可能会陷入僵局，原因很简单:\n\n```cpp\nbool Dispatcher::addWorker(Worker* worker) {\n    bool wait = true;\n    requestsMutex.lock();\n    if (!requests.empty()) {\n          AbstractRequest* request = requests.front();\n          worker->setRequest(request);\n          requests.pop();\n          wait = false;\n          requestsMutex.unlock();\n    }\n    else {\n          requestsMutex.unlock();\n          workersMutex.lock();\n          workers.push(worker);\n          workersMutex.unlock();\n    }\n          return wait;\n } \n\n```\n\n前面我们看到的伴随函数也使用了这两个互斥体。更糟糕的是，这个函数在单独的线程中运行。结果，当第一个函数在试图获得`requestsMutex`时保持`workersMutex`，而第二个函数同时保持后者，同时试图获得前者时，我们遇到了死锁。\n\n然而，在函数中，正如我们在这里看到的，这两个规则都已成功实现；我们从不一次持有一把以上的锁，我们会尽快释放我们持有的任何锁。这可以在其他两种情况下看到，当我们进入它们时，我们首先释放任何不再需要的锁。\n\n无论哪种情况，我们都不再需要分别检查工作人员或请求数据结构；我们可以在做其他事情之前释放相关的锁。这将产生以下可视化效果:\n\n![](img/4c5be456-f21c-4092-8b32-f625282b716d.png)\n\n当然，我们可能需要使用包含在两个或多个数据结构或变量中的数据；其他线程同时使用的数据。可能很难确保生成的代码中没有死锁的可能。\n\n在这里，可能需要考虑使用临时变量或类似的东西。通过锁定互斥体，复制相关数据，并立即释放锁，就没有机会与该互斥体死锁。即使必须将结果写回数据结构，也可以在单独的操作中完成。\n\n这又增加了两条防止死锁的规则:\n\n*   尽量不要同时持有一把以上的锁。\n*   尽快释放所有锁。\n*   锁的时间不要超过绝对必要的时间。\n*   当持有多把锁时，注意它们的顺序。\n\n# 粗心-数据竞赛\n\n当两个或多个线程试图同时写入同一个共享内存时，就会发生数据争用，也称为争用情况。因此，在每个线程执行的指令序列期间和结束时，共享存储器的状态根据定义是非确定性的。\n\n正如我们在[第 6 章](06.html)、*调试多线程代码*中看到的，用于调试多线程应用的工具经常报告数据竞争。例如:\n\n```cpp\n    ==6984== Possible data race during write of size 1 at 0x5CD9260 by thread #1\n ==6984== Locks held: none\n ==6984==    at 0x40362C: Worker::stop() (worker.h:37)\n ==6984==    by 0x403184: Dispatcher::stop() (dispatcher.cpp:50)\n ==6984==    by 0x409163: main (main.cpp:70)\n ==6984== \n ==6984== This conflicts with a previous read of size 1 by thread #2\n ==6984== Locks held: none\n ==6984==    at 0x401E0E: Worker::run() (worker.cpp:51)\n ==6984==    by 0x408FA4: void std::_Mem_fn_base<void (Worker::*)(), true>::operator()<, void>(Worker*) const (in /media/sf_Projects/Cerflet/dispatcher/dispatcher_demo)\n ==6984==    by 0x408F38: void std::_Bind_simple<std::_Mem_fn<void (Worker::*)()> (Worker*)>::_M_invoke<0ul>(std::_Index_tuple<0ul>) (functional:1531)\n ==6984==    by 0x408E3F: std::_Bind_simple<std::_Mem_fn<void (Worker::*)()> (Worker*)>::operator()() (functional:1520)\n ==6984==    by 0x408D47: std::thread::_Impl<std::_Bind_simple<std::_Mem_fn<void (Worker::*)()> (Worker*)> >::_M_run() (thread:115)\n ==6984==    by 0x4EF8C7F: ??? (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21)\n ==6984==    by 0x4C34DB6: ??? (in /usr/lib/valgrind/vgpreload_helgrind-amd64-linux.so)\n ==6984==    by 0x53DF6B9: start_thread (pthread_create.c:333)\n ==6984==  Address 0x5cd9260 is 96 bytes inside a block of size 104 alloc'd\n ==6984==    at 0x4C2F50F: operator new(unsigned long) (in /usr/lib/valgrind/vgpreload_helgrind-amd64-linux.so)\n ==6984==    by 0x40308F: Dispatcher::init(int) (dispatcher.cpp:38)\n ==6984==    by 0x4090A0: main (main.cpp:51)\n ==6984==  Block was alloc'd by thread #1\n\n```\n\n产生上述警告的代码如下:\n\n```cpp\nbool Dispatcher::stop() {\n    for (int i = 0; i < allWorkers.size(); ++ i) {\n          allWorkers[i]->stop();\n    }\n          cout << \"Stopped workers.\\n\";\n          for (int j = 0; j < threads.size(); ++ j) {\n          threads[j]->join();\n                      cout << \"Joined threads.\\n\";\n    }\n } \n\n```\n\n考虑`Worker`实例中的这段代码:\n\n```cpp\n   void stop() { running = false; } \n\n```\n\n我们还有:\n\n```cpp\nvoid Worker::run() {\n    while (running) {\n          if (ready) {\n                ready = false;\n                request->process();\n                request->finish();\n          }\n                      if (Dispatcher::addWorker(this)) {\n                while (!ready && running) {\n                      unique_lock<mutex> ulock(mtx);\n                      if (cv.wait_for(ulock, chrono::seconds(1)) == cv_status::timeout) {\n                      }\n                }\n          }\n    }\n } \n\n```\n\n这里，`running`是一个被设置为`false`的布尔变量(从一个线程向其写入)，通知工作线程它应该终止其等待循环，其中从不同的进程读取布尔变量，主线程对工作线程:\n\n![](img/2ea08a5c-7c70-454f-bdf7-2bc776d3bf76.png)\n\n这个特殊例子的警告是由于一个布尔变量被同时写入和读取。自然，这种特定情况安全的原因与原子有关，如[第 8 章](08.html)、*原子操作-使用硬件*中详细解释的那样。\n\n之所以即使这样的操作也有潜在的风险，是因为读取操作可能会在变量仍处于更新过程中时发生。例如，在 32 位整数的情况下，根据硬件架构，更新该变量可能在一次操作中完成，也可能在多次操作中完成。在后一种情况下，读取操作可能会读取一个具有不可预测结果的中间值:\n\n![](img/0a0ab30d-f915-4018-bab0-e92839000266.png)\n\n更滑稽的情况发生在多个线程使用例如`cout`向一个标准写入数据时。由于该流不是线程安全的，因此无论何时任一线程有机会写入，结果输出流都将包含输入流的位和片段:\n\n![](img/7004ceae-0bc9-40ea-b30b-5dfb3ac595bc.png)\n\n因此，防止数据竞争的基本规则是:\n\n*   切勿写入未锁定的非原子共享资源\n*   从不从未锁定的非原子共享资源中读取\n\n这实质上意味着任何写或读都必须是线程安全的。如果一个线程写入共享内存，那么其他线程应该不能同时写入。同样，当我们读取共享资源时，我们需要确保最多只有其他线程也在读取共享资源。\n\n正如我们在前面的章节中看到的，这种级别的互斥自然是通过互斥来实现的，读写锁提供了一种改进，允许同时读取，同时将写入作为完全互斥的事件。\n\n当然，互斥体也有问题，我们将在下一节中看到。\n\n# 互斥体不是魔法\n\n互斥体构成了几乎所有形式互斥 API 的基础。在它们的核心，它们看起来极其简单，只有一个线程可以拥有一个互斥体，其他线程整齐地在队列中等待，直到它们可以获得互斥体的锁。\n\n人们甚至可以这样描述这个过程:\n\n![](img/31b17d06-a180-4cd8-adbe-0691f896b88b.png)\n\n现实当然不那么美好，主要是因为硬件强加给我们的实际限制。一个明显的限制是同步原语不是免费的。即使它们在硬件中实现，也需要多次调用才能使它们工作。\n\n在硬件中实现互斥体最常见的两种方法是使用**测试和设置**(**【TAS】**)或**比较和交换** ( **CAS** ) CPU 功能。\n\n测试集通常被实现为两条汇编级指令，它们是自主执行的，这意味着它们不能被中断。第一条指令测试某个内存区域是设置为 1 还是 0。第二条指令仅在值为零时执行(`false`)。这意味着互斥锁还没有被锁定。因此，第二条指令将内存区域设置为 1，锁定互斥体。\n\n在伪代码中，这看起来像这样:\n\n```cpp\nbool TAS(bool lock) { \n   if (lock) { \n         return true; \n   } \n   else { \n         lock = true; \n         return false; \n   } \n} \n\n```\n\n比较和交换是一种较少使用的变体，它对一个内存位置和一个给定值执行比较操作，只有在前两者匹配时才替换该内存位置的内容:\n\n```cpp\nbool CAS(int* p, int old, int new) { \n   if (*p != old) { \n               return false; \n         } \n\n   *p = new; \n         return true; \n} \n\n```\n\n无论是哪种情况，都必须主动重复任一函数，直到返回正值:\n\n```cpp\nvolatile bool lock = false; \n\n void critical() { \n     while (TAS(&lock) == false); \n     // Critical section \n     lock = 0; \n } \n\n```\n\n这里，一个简单的 while 循环用于不断轮询内存区域(标记为易失性，以防止可能有问题的编译器优化)。通常，使用一种算法来缓慢降低轮询速率。这是为了减少处理器和内存系统的压力。\n\n这表明互斥锁的使用不是自由的，而是等待互斥锁的每个线程都在主动使用资源。因此，这里的一般规则是:\n\n*   确保线程尽可能短暂地等待互斥体和类似的锁。\n*   使用条件变量或定时器来延长等待时间。\n\n# 锁是奇特的互斥体\n\n正如我们在前面关于互斥体的章节中看到的，在使用互斥体时有一些问题需要记住。当然，当使用锁和其他基于互斥的机制时，这些也适用，即使其中一些问题被这些 API 解决了。\n\n第一次使用多线程 API 时，人们可能会感到困惑的一件事是，不同同步类型之间的实际区别是什么。正如我们在本章前面所述，互斥体实际上是所有同步机制的基础，只是它们使用互斥体来实现所提供的功能的方式不同。\n\n这里重要的是，它们不是不同的同步机制，而只是基本互斥类型的专门化。一个人是否会使用常规互斥体、读/写锁、信号量，甚至像可重入(递归)互斥体或锁这样深奥的东西，完全取决于他试图解决的特定问题。\n\n对于调度器，我们首先在[第 4 章](04.html)、*线程同步和通信*中遇到，我们使用常规互斥来保护包含排队的工作线程和请求的数据结构。由于对任一数据结构的任何访问都可能不仅涉及读取操作，还涉及对该结构的操作，因此在那里使用读/写锁是没有意义的。类似地，递归锁对于普通的互斥锁没有任何作用。\n\n因此，对于每一个同步问题，都必须提出以下问题:\n\n*   我有哪些要求？\n*   哪种同步机制最符合这些要求？\n\n因此，选择复杂的类型是有吸引力的，但通常最好选择满足所有要求的简单类型。当调试一个人的实现时，可以比一个更好的实现节省宝贵的时间。\n\n# 线程与未来\n\n最近，建议不要使用线程变得流行起来，转而提倡使用其他异步处理机制，如`promise`。这背后的原因是线程的使用和所涉及的同步复杂且容易出错。通常一个人只想并行运行一个任务，而不关心结果是如何获得的。\n\n对于只会短暂运行的简单任务，这肯定是有意义的。基于线程的实现的主要优势总是可以完全定制其行为。有了`promise`，一个人发送一个任务运行，最后，一个人从`future`实例中得到结果。这对于简单的任务来说很方便，但显然没有涵盖很多情况。\n\n这里最好的方法是首先很好地学习线程和同步机制，以及它们的局限性。只有在那之后，才真正有意义考虑一个人是希望使用一个承诺，`packaged_task`，还是一个成熟的线程。\n\n这些更高级的、基于未来的 API 的另一个主要考虑是，它们在很大程度上是基于模板的，这使得调试和排除任何可能出现的问题比使用更简单的低级 API 时要容易得多。\n\n# 初始化的静态顺序\n\n静态变量是只声明一次的变量，本质上存在于全局范围内，尽管可能只在特定类的实例之间共享。也可能有完全静态的类:\n\n```cpp\nclass Foo { \n   static std::map<int, std::string> strings; \n   static std::string oneString; \n\npublic: \n   static void init(int a, std::string b, std::string c) { \n         strings.insert(std::pair<int, std::string>(a, b)); \n         oneString = c; \n   } \n}; \n\nstd::map<int, std::string> Foo::strings; \nstd::string Foo::oneString; \n\n```\n\n正如我们在这里所看到的，静态变量和静态函数看起来是一个非常简单但强大的概念。虽然从本质上来说这是真的，但是当涉及到静态变量和类的初始化时，有一个主要的问题会引起人们的疏忽。这是初始化顺序的形式。\n\n想象一下，如果我们希望使用另一个类的静态初始化中的前一个类，会发生什么，如下所示:\n\n```cpp\nclass Bar { \n   static std::string name; \n   static std::string initName(); \n\npublic: \n   void init(); \n}; \n\n// Static initializations. \nstd::string Bar::name = Bar::initName(); \n\nstd::string Bar::initName() { \n   Foo::init(1, \"A\", \"B\"); \n   return \"Bar\"; \n} \n\n```\n\n虽然这看起来很好，但是将第一个字符串添加到以整数为键的类映射结构中意味着这段代码很有可能会崩溃。原因很简单，不能保证`Foo::string`是在我们调用`Foo::init()`的时候初始化的。试图使用未初始化的映射结构将导致异常。\n\n简而言之，静态变量的初始化顺序基本上是随机的，如果不考虑这一点，就会导致不确定的行为。\n\n这个问题的解决方法相当简单。基本上，目标是使更复杂的静态变量的初始化显式化，而不是像前面的例子那样隐式化。为此，我们修改了 Foo 类:\n\n```cpp\nclass Foo { \n   static std::map<int, std::string>& strings(); \n   static std::string oneString; \n\npublic: \n   static void init(int a, std::string b, std::string c) { \n         static std::map<int, std::string> stringsStatic = Foo::strings(); \n         stringsStatic.insert(std::pair<int, std::string>(a, b)); \n         oneString = c; \n   } \n}; \n\nstd::string Foo::oneString; \n\nstd::map<int, std::string>& Foo::strings() { \n   static std::map<int, std::string>* stringsStatic = new std::map<int, std::string>(); \n   return *stringsStatic; \n} \n\n```\n\n从顶部开始，我们看到不再直接定义静态映射。相反，我们有一个同名的私有函数。这个函数的实现可以在这个示例代码的底部找到。在其中，我们有一个静态指针指向具有熟悉的地图定义的地图结构。\n\n当这个函数被调用时，由于它是一个静态变量，当还没有实例时，会创建一个新的映射。在修改后的`init()`函数中，我们看到我们调用`strings()`函数来获取对这个实例的引用。这是显式的初始化部分，因为调用函数将始终确保在我们使用之前对映射结构进行初始化，从而解决了我们之前遇到的问题。\n\n我们在这里还看到了一个小优化:我们创建的`stringsStatic`变量也是静态的，这意味着我们只会调用`strings()`函数一次。这使得重复的函数调用变得不必要，并恢复了我们在前面简单但不稳定的实现中所拥有的速度。\n\n因此，静态变量初始化的基本规则是，总是对非平凡静态变量使用显式初始化。\n\n# 摘要\n\n在这一章中，我们研究了在编写多线程代码时需要记住的一些好的实践和规则，以及一些一般性的建议。在这一点上，当编写这样的代码时，应该能够避免一些更大的陷阱和主要的混乱来源。\n\n在下一章中，我们将研究如何利用底层硬件进行原子操作，以及 C++ 11 中引入的`<atomics>`头。"
  },
  {
    "path": "docs/master-cpp-multithrd/08.md",
    "content": "# 八、原子操作——使用硬件\n\n许多优化和线程安全取决于一个人对底层硬件的理解:从某些架构上的对齐内存访问，到知道哪些数据大小和 C++ 类型可以安全地处理，而不会影响性能或需要互斥体等。\n\n本章介绍如何利用多种处理器体系结构的特性，例如，在原子操作可以防止任何访问冲突的情况下，防止使用互斥锁。编译器特定的扩展，比如 GCC 中的扩展，也会被检查。\n\n本章的主题包括:\n\n*   原子操作的类型以及如何使用它们\n*   如何针对特定的处理器架构\n*   基于编译器的原子操作\n\n# 原子操作\n\n简而言之，原子操作是处理器可以用单个指令执行的操作。这使得它具有原子性，因为没有任何东西(除了中断)可以干扰它，或者改变它可能使用的任何变量或数据。\n\n应用包括保证指令执行的顺序、无锁实现以及指令执行顺序和内存访问保证很重要的相关用途。\n\n在 2011 年 C++ 标准之前，处理器提供的对这种原子操作的访问仅由编译器使用扩展来提供。\n\n# Visual C++\n\n对于微软的 MSVC 编译器，有一些互锁的函数，如 MSDN 文档中所总结的，从添加特性开始:\n\n| **联锁功能** | **描述** |\n| `InterlockedAdd` | 对指定的`LONG`值执行原子加法运算。 |\n| `InterlockedAddAcquire` | 对指定的`LONG`值执行原子加法运算。使用获取内存排序语义来执行该操作。 |\n| `InterlockedAddRelease` | 对指定的`LONG`值执行原子加法运算。该操作使用释放内存排序语义来执行。 |\n| `InterlockedAddNoFence` | 对指定的`LONG`值执行原子加法运算。操作以原子方式执行，但不使用内存屏障(本章将介绍)。 |\n\n这些是该功能的 32 位版本。API 中也有这个方法和其他方法的 64 位版本。原子函数倾向于关注特定的变量类型，但是为了简洁起见，这个概要中省略了这个 API 的变体。\n\n我们还可以看到获取和发布的变化。这些保证了相应的读或写访问将受到保护，不会因任何后续的读或写操作而导致存储器重新排序(在硬件级别上)。最后，无围栏变体(也称为内存屏障)在不使用任何内存屏障的情况下执行操作。\n\n通常情况下，中央处理器执行指令(包括内存读取和写入)的顺序不对，以优化性能。由于这种类型的行为并不总是令人满意的，所以添加了内存屏障来防止这种指令重新排序。\n\n接下来是原子`AND`特性:\n\n| **联锁功能** | **描述** |\n| --- | --- |\n| `InterlockedAnd` | 对指定的`LONG`值执行原子`AND`操作。 |\n| `InterlockedAndAcquire` | 对指定的`LONG`值执行原子`AND`操作。使用获取内存排序语义来执行该操作。 |\n| `InterlockedAndRelease` | 对指定的`LONG`值执行原子`AND`操作。该操作使用释放内存排序语义来执行。 |\n| `InterlockedAndNoFence` | 对指定的`LONG`值执行原子`AND`操作。操作以原子方式执行，但不使用内存屏障。 |\n\n位测试功能如下:\n\n| **联锁功能** | **描述** |\n| --- | --- |\n| `InterlockedBitTestAndComplement` | 测试指定`LONG`值的指定位并对其进行补充。 |\n| `InterlockedBitTestAndResetAcquire` | 测试指定`LONG`值的指定位，并将其设置为`0`。操作为`atomic`，使用获取内存排序语义执行。 |\n| `InterlockedBitTestAndResetRelease` | 测试指定`LONG`值的指定位，并将其设置为`0`。操作为`atomic`，使用内存释放语义执行。 |\n| `InterlockedBitTestAndSetAcquire` | 测试指定`LONG`值的指定位，并将其设置为`1`。操作为`atomic`，使用获取内存排序语义执行。 |\n| `InterlockedBitTestAndSetRelease` | 测试指定`LONG`值的指定位，并将其设置为`1`。操作为`atomic`，执行释放内存排序语义。 |\n| `InterlockedBitTestAndReset` | 测试指定`LONG`值的指定位，并将其设置为`0`。 |\n| `InterlockedBitTestAndSet` | 测试指定`LONG`值的指定位，并将其设置为`1`。 |\n\n比较特征可以如下所示列出:\n\n| **联锁功能** | **描述** |\n| --- | --- |\n| `InterlockedCompareExchange` | 对指定值执行原子比较和交换操作。该函数比较两个指定的 32 位值，并根据比较结果与另一个 32 位值进行交换。 |\n| `InterlockedCompareExchangeAcquire` | 对指定值执行原子比较和交换操作。该函数比较两个指定的 32 位值，并根据比较结果与另一个 32 位值进行交换。使用获取内存排序语义来执行该操作。 |\n| `InterlockedCompareExchangeRelease` | 对指定值执行原子比较和交换操作。该函数比较两个指定的 32 位值，并根据比较结果与另一个 32 位值进行交换。交换是用释放内存排序语义来执行的。 |\n| `InterlockedCompareExchangeNoFence` | 对指定值执行原子比较和交换操作。该函数比较两个指定的 32 位值，并根据比较结果与另一个 32 位值进行交换。操作以原子方式执行，但不使用内存屏障。 |\n| `InterlockedCompareExchangePointer` | 对指定的指针值执行原子比较和交换操作。该函数比较两个指定的指针值，并根据比较结果与另一个指针值进行交换。 |\n| `InterlockedCompareExchangePointerAcquire` | 对指定的指针值执行原子比较和交换操作。该函数比较两个指定的指针值，并根据比较结果与另一个指针值进行交换。使用获取内存排序语义来执行该操作。 |\n| `InterlockedCompareExchangePointerRelease` | 对指定的指针值执行原子比较和交换操作。该函数比较两个指定的指针值，并根据比较结果与另一个指针值进行交换。该操作使用释放内存排序语义来执行。 |\n| `InterlockedCompareExchangePointerNoFence` | 对指定值执行原子比较和交换操作。该函数比较两个指定的指针值，并根据比较结果与另一个指针值进行交换。操作以原子方式执行，但不使用内存屏障 |\n\n减量功能包括:\n\n| **联锁功能** | **描述** |\n| --- | --- |\n| `InterlockedDecrement` | 将指定的 32 位变量的值递减(减一)为`atomic`操作。 |\n| `InterlockedDecrementAcquire` | 将指定的 32 位变量的值递减(减一)为`atomic`操作。使用获取内存排序语义来执行该操作。 |\n| `InterlockedDecrementRelease` | 将指定的 32 位变量的值递减(减一)为`atomic`操作。该操作使用释放内存排序语义来执行。 |\n| `InterlockedDecrementNoFence` | 将指定的 32 位变量的值递减(减一)为`atomic`操作。操作以原子方式执行，但不使用内存屏障。 |\n\n交换(交换)功能包括:\n\n| **联锁功能** | **描述** |\n| --- | --- |\n| `InterlockedExchange` | 将 32 位变量设置为指定值作为`atomic`操作。 |\n| `InterlockedExchangeAcquire` | 将 32 位变量设置为指定值作为`atomic`操作。使用获取内存排序语义来执行该操作。 |\n| `InterlockedExchangeNoFence` | 将 32 位变量设置为指定值作为`atomic`操作。操作以原子方式执行，但不使用内存屏障。 |\n| `InterlockedExchangePointer` | 原子地交换一对指针值。 |\n| `InterlockedExchangePointerAcquire` | 原子地交换一对指针值。使用获取内存排序语义来执行该操作。 |\n| `InterlockedExchangePointerNoFence` | 原子地交换一对地址。操作以原子方式执行，但不使用内存屏障。 |\n| `InterlockedExchangeSubtract` | 执行两个值的原子减法。 |\n| `InterlockedExchangeAdd` | 执行两个 32 位值的原子加法。 |\n| `InterlockedExchangeAddAcquire` | 执行两个 32 位值的原子加法。使用获取内存排序语义来执行该操作。 |\n| `InterlockedExchangeAddRelease` | 执行两个 32 位值的原子加法。该操作使用释放内存排序语义来执行。 |\n| `InterlockedExchangeAddNoFence` | 执行两个 32 位值的原子加法。操作以原子方式执行，但不使用内存屏障。 |\n\n增量功能包括:\n\n| **联锁功能** | **描述** |\n| --- | --- |\n| `InterlockedIncrement` | 将指定的 32 位变量的值递增(增加 1)作为`atomic`操作。 |\n| `InterlockedIncrementAcquire` | 将指定的 32 位变量的值递增(增加 1)作为`atomic`操作。使用获取内存排序语义来执行该操作。 |\n| `InterlockedIncrementRelease` | 将指定的 32 位变量的值递增(增加 1)作为`atomic`操作。使用释放内存排序语义来执行该操作。 |\n| `InterlockedIncrementNoFence` | 将指定的 32 位变量的值递增(增加 1)作为`atomic`操作。操作以原子方式执行，但不使用内存屏障。 |\n\n`OR`功能:\n\n| **联锁功能** | **描述** |\n| --- | --- |\n| `InterlockedOr` | 对指定的`LONG`值执行原子`OR`操作。 |\n| `InterlockedOrAcquire` | 对指定的`LONG`值执行原子`OR`操作。使用获取内存排序语义来执行该操作。 |\n| `InterlockedOrRelease` | 对指定的`LONG`值执行原子`OR`操作。该操作使用释放内存排序语义来执行。 |\n| `InterlockedOrNoFence` | 对指定的`LONG`值执行原子`OR`操作。操作以原子方式执行，但不使用内存屏障。 |\n\n最后，专属的`OR` ( `XOR`)功能有:\n\n| **联锁功能** | **描述** |\n| --- | --- |\n| `InterlockedXor` | 对指定的`LONG`值执行原子`XOR`操作。 |\n| `InterlockedXorAcquire` | 对指定的`LONG`值执行原子`XOR`操作。使用获取内存排序语义来执行该操作。 |\n| `InterlockedXorRelease` | 对指定的`LONG`值执行原子`XOR`操作。该操作使用释放内存排序语义来执行。 |\n| `InterlockedXorNoFence` | 对指定的`LONG`值执行原子`XOR`操作。操作以原子方式执行，但不使用内存屏障。 |\n\n# （同 groundcontrolcenter）地面控制中心\n\n和 Visual C++ 一样，GCC 也自带一套内置的原子函数。这些基于 GCC 版本和标准库使用的底层架构而不同。由于 GCC 在比 VC++ 多得多的平台和操作系统上使用，在考虑可移植性时，这肯定是一个很大的因素。\n\n例如，不是 x86 平台上提供的每个内置原子功能都可以在 ARM 上使用，部分原因是架构差异，包括特定 ARM 架构的变化。例如，ARMv6、ARMv7 或当前的 ARMv8，以及 Thumb 指令集，等等。\n\n在 C++ 11 标准之前，GCC 使用了原子的`__sync-prefixed`扩展:\n\n```cpp\ntype __sync_fetch_and_add (type *ptr, type value, ...) \ntype __sync_fetch_and_sub (type *ptr, type value, ...) \ntype __sync_fetch_and_or (type *ptr, type value, ...) \ntype __sync_fetch_and_and (type *ptr, type value, ...) \ntype __sync_fetch_and_xor (type *ptr, type value, ...) \ntype __sync_fetch_and_nand (type *ptr, type value, ...) \n\n```\n\n这些操作从内存中获取一个值，并对其执行指定的操作，返回内存中的值。这些都使用记忆屏障。\n\n```cpp\ntype __sync_add_and_fetch (type *ptr, type value, ...) \ntype __sync_sub_and_fetch (type *ptr, type value, ...) \ntype __sync_or_and_fetch (type *ptr, type value, ...) \ntype __sync_and_and_fetch (type *ptr, type value, ...) \ntype __sync_xor_and_fetch (type *ptr, type value, ...) \ntype __sync_nand_and_fetch (type *ptr, type value, ...) \n\n```\n\n这些操作与第一组类似，只是它们在指定的操作之后返回新值。\n\n```cpp\nbool __sync_bool_compare_and_swap (type *ptr, type oldval, type newval, ...) \ntype __sync_val_compare_and_swap (type *ptr, type oldval, type newval, ...) \n\n```\n\n如果旧值与提供的值匹配，这些比较操作将写入新值。如果新值已经写入，布尔变量返回真。\n\n```cpp\n__sync_synchronize (...) \n\n```\n\n这个函数创建了一个完整的内存屏障。\n\n```cpp\ntype __sync_lock_test_and_set (type *ptr, type value, ...) \n\n```\n\n这个方法实际上是一个交换操作，不像它的名字所暗示的那样。它更新指针值并返回前一个值。这使用的不是完全内存屏障，而是获取屏障，这意味着它不会释放屏障。\n\n```cpp\nvoid __sync_lock_release (type *ptr, ...) \n\n```\n\n此函数释放由前面的方法获得的屏障。\n\n为了适应 C++ 11 内存模型，GCC 增加了`__atomic`内置方法，这也大大改变了 API:\n\n```cpp\ntype __atomic_load_n (type *ptr, int memorder) \nvoid __atomic_load (type *ptr, type *ret, int memorder) \nvoid __atomic_store_n (type *ptr, type val, int memorder) \nvoid __atomic_store (type *ptr, type *val, int memorder) \ntype __atomic_exchange_n (type *ptr, type val, int memorder) \nvoid __atomic_exchange (type *ptr, type *val, type *ret, int memorder) \nbool __atomic_compare_exchange_n (type *ptr, type *expected, type desired, bool weak, int success_memorder, int failure_memorder) \nbool __atomic_compare_exchange (type *ptr, type *expected, type *desired, bool weak, int success_memorder, int failure_memorder) \n\n```\n\n首先是通用的加载、存储和交换函数。它们相当不言自明。加载函数在内存中读取一个值，存储函数在内存中存储一个值，交换函数用一个新值交换现有值。比较和交换函数使交换成为条件。\n\n```cpp\ntype __atomic_add_fetch (type *ptr, type val, int memorder) \ntype __atomic_sub_fetch (type *ptr, type val, int memorder) \ntype __atomic_and_fetch (type *ptr, type val, int memorder) \ntype __atomic_xor_fetch (type *ptr, type val, int memorder) \ntype __atomic_or_fetch (type *ptr, type val, int memorder) \ntype __atomic_nand_fetch (type *ptr, type val, int memorder) \n\n```\n\n这些函数本质上与旧的应用编程接口相同，返回特定操作的结果。\n\n```cpp\ntype __atomic_fetch_add (type *ptr, type val, int memorder) \ntype __atomic_fetch_sub (type *ptr, type val, int memorder) \ntype __atomic_fetch_and (type *ptr, type val, int memorder) \ntype __atomic_fetch_xor (type *ptr, type val, int memorder) \ntype __atomic_fetch_or (type *ptr, type val, int memorder) \ntype __atomic_fetch_nand (type *ptr, type val, int memorder) \n\n```\n\n同样，为新的应用编程接口更新了相同的功能。这些返回原始值(在操作之前获取)。\n\n```cpp\nbool __atomic_test_and_set (void *ptr, int memorder) \n\n```\n\n与旧 API 中类似命名的函数不同，该函数执行真正的测试和设置操作，而不是旧 API 函数的交换操作，这仍然需要一个函数在之后释放内存屏障。测试是针对某个定义的值。\n\n```cpp\nvoid __atomic_clear (bool *ptr, int memorder) \n\n```\n\n该功能清除指针地址，将其设置为`0`。\n\n```cpp\nvoid __atomic_thread_fence (int memorder) \n\n```\n\n使用这个函数可以创建线程之间的同步内存屏障。\n\n```cpp\nvoid __atomic_signal_fence (int memorder) \n\n```\n\n这个函数在一个线程和同一个线程中的信号处理程序之间创建了一个内存屏障。\n\n```cpp\nbool __atomic_always_lock_free (size_t size, void *ptr) \n\n```\n\n该函数检查指定大小的对象是否总是为当前处理器体系结构创建无锁原子指令。\n\n```cpp\nbool __atomic_is_lock_free (size_t size, void *ptr) \n\n```\n\n这与前面的功能基本相同。\n\n# 记忆顺序\n\n在用于原子操作的 C++ 11 内存模型中，内存屏障(栅栏)并不总是被使用。在 GCC 内置的 atomics API 中，这反映在其函数中的`memorder`参数中。这个的可能值直接映射到 C++ 11 原子应用编程接口中的值:\n\n*   `__ATOMIC_RELAXED`:表示没有线程间排序约束。\n*   `__ATOMIC_CONSUME`:由于 C++ 11 对`memory_order_consume`的语义存在缺陷，目前使用更强的`__ATOMIC_ACQUIRE`内存顺序来实现。\n*   `__ATOMIC_ACQUIRE`:从发布(或更强的)语义存储中创建线程间先发生约束来获取负载\n*   `__ATOMIC_RELEASE`:创建线程间先发生约束，以获取(或更强的)从这个发布存储读取的语义负载\n*   `__ATOMIC_ACQ_REL`:结合了`__ATOMIC_ACQUIRE`和`__ATOMIC_RELEASE`的效果。\n*   `__ATOMIC_SEQ_CST`:强制所有其他`__ATOMIC_SEQ_CST`操作的总排序。\n\n前面的列表是从 GCC 手册中关于 GCC 7.1 原子的章节中复制过来的。连同那一章中的评论，它非常清楚地表明，在内存模型和编译器的实现中实现 C++ 11 atomics 支持时进行了权衡。\n\n由于 atomics 依赖于底层的硬件支持，因此使用 atomics 的代码绝不会有一段能够在各种各样的体系结构中工作。\n\n# 其他编译器\n\n当然，C/C++ 的编译器工具链比 VC++ 和 GCC 多得多，包括英特尔编译器集合(ICC)和其他通常是专有的工具..这些都有自己的内置原子函数集合。幸运的是，由于 C++ 11 标准，我们现在有了一个完全可移植的编译器间原子标准。一般来说，这意味着在非常具体的用例(或现有代码的维护)之外，人们将使用 C++ 标准而不是编译器特定的扩展。\n\n# C++ 11 原子\n\n为了使用原生的 C++ 11 atomics 特性，你所要做的就是包含`<atomic>`头。这使得`atomic`类可用，该类使用模板使自己适应所需的类型，并具有大量预定义的类型定义:\n\n| 类型定义名称 | **完全特殊化** |\n| `std::atomic_bool` | `std::atomic<bool>` |\n| `std::atomic_char` | `std::atomic<char>` |\n| `std::atomic_schar` | `std::atomic<signed char>` |\n| `std::atomic_uchar` | `std::atomic<unsigned char>` |\n| `std::atomic_short` | `std::atomic<short>` |\n| `std::atomic_ushort` | `std::atomic<unsigned short>` |\n| `std::atomic_int` | `std::atomic<int>` |\n| `std::atomic_uint` | `std::atomic<unsigned int>` |\n| `std::atomic_long` | `std::atomic<long>` |\n| `std::atomic_ulong` | `std::atomic<unsigned long>` |\n| `std::atomic_llong` | `std::atomic<long long>` |\n| `std::atomic_ullong` | `std::atomic<unsigned long long>` |\n| `std::atomic_char16_t` | `std::atomic<char16_t>` |\n| `std::atomic_char32_t` | `std::atomic<char32_t>` |\n| `std::atomic_wchar_t` | `std::atomic<wchar_t>` |\n| `std::atomic_int8_t` | `std::atomic<std::int8_t>` |\n| `std::atomic_uint8_t` | `std::atomic<std::uint8_t>` |\n| `std::atomic_int16_t` | `std::atomic<std::int16_t>` |\n| `std::atomic_uint16_t` | `std::atomic<std::uint16_t>` |\n| `std::atomic_int32_t` | `std::atomic<std::int32_t>` |\n| `std::atomic_uint32_t` | `std::atomic<std::uint32_t>` |\n| `std::atomic_int64_t` | `std::atomic<std::int64_t>` |\n| `std::atomic_uint64_t` | `std::atomic<std::uint64_t>` |\n| `std::atomic_int_least8_t` | `std::atomic<std::int_least8_t>` |\n| `std::atomic_uint_least8_t` | `std::atomic<std::uint_least8_t>` |\n| `std::atomic_int_least16_t` | `std::atomic<std::int_least16_t>` |\n| `std::atomic_uint_least16_t` | `std::atomic<std::uint_least16_t>` |\n| `std::atomic_int_least32_t` | `std::atomic<std::int_least32_t>` |\n| `std::atomic_uint_least32_t` | `std::atomic<std::uint_least32_t>` |\n| `std::atomic_int_least64_t` | `std::atomic<std::int_least64_t>` |\n| `std::atomic_uint_least64_t` | `std::atomic<std::uint_least64_t>` |\n| `std::atomic_int_fast8_t` | `std::atomic<std::int_fast8_t>` |\n| `std::atomic_uint_fast8_t` | `std::atomic<std::uint_fast8_t>` |\n| `std::atomic_int_fast16_t` | `std::atomic<std::int_fast16_t>` |\n| `std::atomic_uint_fast16_t` | `std::atomic<std::uint_fast16_t>` |\n| `std::atomic_int_fast32_t` | `std::atomic<std::int_fast32_t>` |\n| `std::atomic_uint_fast32_t` | `std::atomic<std::uint_fast32_t>` |\n| `std::atomic_int_fast64_t` | `std::atomic<std::int_fast64_t>` |\n| `std::atomic_uint_fast64_t` | `std::atomic<std::uint_fast64_t>` |\n| `std::atomic_intptr_t` | `std::atomic<std::intptr_t>` |\n| `std::atomic_uintptr_t` | `std::atomic<std::uintptr_t>` |\n| `std::atomic_size_t` | `std::atomic<std::size_t>` |\n| `std::atomic_ptrdiff_t` | `std::atomic<std::ptrdiff_t>` |\n| `std::atomic_intmax_t` | `std::atomic<std::intmax_t>` |\n| `std::atomic_uintmax_t` | `std::atomic<std::uintmax_t>` |\n\n这个`atomic`类定义了以下通用函数:\n\n| **功能** | **描述** |\n| `operator=` | 为原子对象赋值。 |\n| `is_lock_free` | 如果原子对象是无锁的，则返回 true。 |\n| `store` | 以原子方式用非原子参数替换原子对象的值。 |\n| `load` | 原子地获取原子对象的值。 |\n| `operator T` | 从原子对象加载值。 |\n| `exchange` | 用新值自动替换对象的值，并返回旧值。 |\n| `compare_exchange_weak``compare_exchange_strong` | 自动比较对象的值，如果相等则交换值，否则返回当前值。 |\n\n随着 C++ 17 的更新，增加了`is_always_lock_free`常量。这允许查询类型是否总是无锁的。\n\n最后，我们有专门的`atomic`功能:\n\n| **功能** | **描述** |\n| `fetch_add` | 自动将参数添加到存储在`atomic`对象中的值，并返回旧值。 |\n| `fetch_sub` | 从存储在`atomic`对象中的值中自动减去参数，并返回旧值。 |\n| `fetch_and` | 自动在参数和`atomic`对象的值之间执行按位`AND`，并返回旧值。 |\n| `fetch_or` | 自动在参数和`atomic`对象的值之间执行按位`OR`，并返回旧值。 |\n| `fetch_xor` | 自动在参数和`atomic`对象的值之间执行按位`XOR`，并返回旧值。 |\n| `operator++ ``operator++(int)``operator--``operator--(int)` | 将原子值递增或递减 1。 |\n| `operator+=``operator-=``operator&=``operator&#124;=``operator^=` | 用原子值进行加法、减法或按位`AND`、`OR`、`XOR`运算。 |\n\n# 例子\n\n使用`fetch_add`的基本示例如下所示:\n\n```cpp\n#include <iostream> \n#include <thread> \n#include <atomic> \n\nstd::atomic<long long> count; \nvoid worker() { \n         count.fetch_add(1, std::memory_order_relaxed); \n} \n\nint main() { \n         std::thread t1(worker); \n         std::thread t2(worker); \n         std::thread t3(worker); \n         std::thread t4(worker); \n         std::thread t5(worker); \n\n         t1.join(); \n         t2.join(); \n         t3.join(); \n         t4.join(); \n         t5.join(); \n\n         std::cout << \"Count value:\" << count << '\\n'; \n} \n\n```\n\n这个示例代码的结果将是`5`。正如我们在这里看到的，我们可以用 atomics 以这种方式实现一个基本的计数器，而不是为了提供线程同步而必须使用任何互斥或类似的东西。\n\n# 非类函数\n\n除了`atomic`类，在`<atomic>`头中还定义了许多基于模板的函数，我们可以用更类似于编译器内置原子函数的方式来使用这些函数:\n\n| **功能** | **描述** |\n| `atomic_is_lock_free` | 检查原子类型的操作是否是无锁的。 |\n| `atomic_storeatomic_store_explicit` | 用非原子参数自动替换`atomic`对象的值。 |\n| `atomic_load``atomic_load_explicit` | 自动获取存储在`atomic`对象中的值。 |\n| `atomic_exchange``atomic_exchange_explicit` | 用非原子参数自动替换`atomic`对象的值，并返回`atomic`的旧值。 |\n| `atomic_compare_exchange_weak``atomic_compare_exchange_weak_explicit``atomic_compare_exchange_strong``atomic_compare_exchange_strong_explicit` | 自动将`atomic`对象的值与非原子参数进行比较，如果相等，则执行原子交换；如果不相等，则执行`atomic`加载。 |\n| `atomic_fetch_add``atomic_fetch_add_explicit` | 向`atomic`对象添加非原子值，并获得`atomic`的前一个值。 |\n| `atomic_fetch_sub``atomic_fetch_sub_explicit` | 从`atomic`对象中减去一个非原子值，得到`atomic`的前一个值。 |\n| `atomic_fetch_and``atomic_fetch_and_explicit` | 用非原子参数替换逻辑`AND`结果的`atomic`对象，得到原子的前一个值。 |\n| `atomic_fetch_or``atomic_fetch_or_explicit` | 用非原子参数替换逻辑`OR`结果的`atomic`对象，得到`atomic`的前一个值。 |\n| `atomic_fetch_xor``atomic_fetch_xor_explicit` | 用非原子参数替换逻辑`XOR`结果的`atomic`对象，得到`atomic`的前一个值。 |\n| `atomic_flag_test_and_set``atomic_flag_test_and_set_explicit` | 自动将标志设置为`true`并返回其先前值。 |\n| `atomic_flag_clear``atomic_flag_clear_explicit` | 自动将标志的值设置为`false`。 |\n| `atomic_init` | 默认构造的`atomic`对象的非原子初始化。 |\n| `kill_dependency` | 从`std::memory_order_consume`依赖关系树中移除指定的对象。 |\n| `atomic_thread_fence` | 通用内存顺序相关的围栏同步原语。 |\n| `atomic_signal_fence` | 线程和在同一线程中执行的信号处理程序之间的隔离。 |\n\n常规函数和显式函数的区别在于，后者允许用户实际设置要使用的内存顺序。前者总是用`memory_order_seq_cst`作为记忆顺序。\n\n# 例子\n\n在这个使用`atomic_fetch_sub`的例子中，一个索引容器由多个线程并发处理，不使用锁:\n\n```cpp\n#include <string> \n#include <thread> \n#include <vector> \n#include <iostream> \n#include <atomic> \n#include <numeric> \n\nconst int N = 10000; \nstd::atomic<int> cnt; \nstd::vector<int> data(N); \n\nvoid reader(int id) { \n         for (;;) { \n               int idx = atomic_fetch_sub_explicit(&cnt, 1, std::memory_order_relaxed); \n               if (idx >= 0) { \n                           std::cout << \"reader \" << std::to_string(id) << \" processed item \" \n                                       << std::to_string(data[idx]) << '\\n'; \n               }  \n         else { \n                           std::cout << \"reader \" << std::to_string(id) << \" done.\\n\"; \n                           break; \n               } \n         } \n} \n\nint main() { \n         std::iota(data.begin(), data.end(), 1); \n         cnt = data.size() - 1; \n\n         std::vector<std::thread> v; \n         for (int n = 0; n < 10; ++ n) { \n               v.emplace_back(reader, n); \n         } \n\n         for (std::thread& t : v) { \n               t.join(); \n         } \n} \n\n```\n\n这个示例代码使用一个填充了大小为 *N* 的整数的向量作为数据源，用 1 填充它。原子计数器对象被设置为数据向量的大小。之后，创建 10 个线程(使用向量的`emplace_back` C++ 11 特性就地初始化)，运行`reader`函数。\n\n在该函数中，我们使用`atomic_fetch_sub_explicit`函数从内存中读取索引计数器的当前值，这允许我们使用`memory_order_relaxed`内存顺序。这个函数还从这个旧值中减去我们传递的值，将索引向下计数 1。\n\n只要我们通过这种方式得到的索引号高于或等于零，函数就继续，否则就会退出。一旦所有线程完成，应用就退出。\n\n# 原子旗帜\n\n`std::atomic_flag`是原子布尔类型。与`atomic`类的其他专门化不同，它保证是无锁的。但是，它不提供任何加载或存储操作。\n\n取而代之的是，它提供赋值操作符，并用于清除或`test_and_set`标志。前者由此将标志设置为`false`，后者将测试并将其设置为`true`。\n\n# 记忆顺序\n\n该属性被定义为`<atomic>`标题中的枚举:\n\n```cpp\nenum memory_order { \n    memory_order_relaxed, \n    memory_order_consume, \n    memory_order_acquire, \n    memory_order_release, \n    memory_order_acq_rel, \n    memory_order_seq_cst \n}; \n\n```\n\n在 GCC 部分，我们已经简单地谈到了记忆顺序的话题。如上所述，这是底层硬件体系结构的特征在某种程度上显现出来的部分之一。\n\n基本上，内存顺序决定了围绕原子操作的非原子内存访问的顺序(内存访问顺序)。这会影响不同线程在执行指令时如何看到内存中的数据:\n\n| **枚举** | **描述** |\n| `memory_order_relaxed` | 宽松操作:没有对其他读取或写入施加同步或排序约束，只有这个操作的原子性得到保证。 |\n| `memory_order_consume` | 具有此内存顺序的加载操作在受影响的内存位置上执行*消耗操作*:根据当前加载的值，当前线程中的任何读或写都不能在此加载之前重新排序。对释放相同原子变量的其他线程中的数据相关变量的写入在当前线程中可见。在大多数平台上，这只会影响编译器优化。 |\n| `memory_order_acquire` | 具有此内存顺序的加载操作在受影响的内存位置上执行*获取操作*:在此加载之前，当前线程中的任何读或写都不能被重新排序。释放相同原子变量的其他线程中的所有写入在当前线程中都是可见的。 |\n| `memory_order_release` | 具有该存储顺序的存储操作执行*释放操作*:在该存储之后，当前线程中的任何读或写都不能被重新排序。当前线程中的所有写操作在获取相同原子变量的其他线程中都是可见的，并且将依赖关系携带到原子变量中的写操作在使用相同原子变量的其他线程中变得可见。 |\n| `memory_order_acq_rel` | 具有该存储顺序的读-修改-写操作既是*获取操作*又是*释放操作*。当前线程中的任何内存读取或写入都不能在此存储之前或之后重新排序。在修改之前，释放相同原子变量的其他线程中的所有写入都是可见的，并且修改在获取相同原子变量的其他线程中是可见的。 |\n| `memory_order_seq_cst` | 任何具有此内存顺序的操作都是一个*获取操作*和一个*释放操作*，加上一个单一的总顺序，其中所有线程以相同的顺序观察所有修改。 |\n\n# 宽松排序\n\n使用宽松的内存排序，并发内存访问之间没有强制顺序。这种排序保证的只是原子性和修改顺序。\n\n这种排序的典型用途是计数器，无论是递增还是递减，正如我们在前面的示例代码中看到的。\n\n# 发布-获取订单\n\n如果线程 A 中的原子存储被标记为`memory_order_release`，而线程 B 中来自同一变量的原子加载被标记为`memory_order_acquire`，那么从线程 A 的角度来看，在原子存储之前发生的所有内存写入(非原子的和宽松的原子的)*都变成了线程 B 中的*可见副作用*，也就是说，一旦原子加载完成，线程 B 保证会看到线程 A 写入内存的所有内容。*\n\n这种类型的操作在所谓的强有序架构上是自动的，包括 x86、SPARC 和 POWER。弱有序架构，如 ARM、PowerPC 和 Itanium，将需要在这里使用内存屏障。\n\n这种内存排序的典型应用包括互斥机制，如互斥或原子自旋锁。\n\n# 发布-消费订购\n\n如果线程 A 中的原子存储被标记为`memory_order_release`，而来自同一变量的线程 B 中的原子加载被标记为`memory_order_consume`，则从线程 A 的角度来看，原子存储之前的所有内存写入(非原子的和宽松的原子的)*依赖排序的*，在加载操作*携带依赖的线程 B 中的那些操作中变成*可见的副作用*。*也就是说，一旦原子加载完成，线程 B 中那些使用从加载中获得的值的操作符和函数就可以保证看到线程 A 写入内存的内容。\n\n这种排序在几乎所有的架构上都是自动的。唯一的主要例外是(过时的)阿尔法架构。这种排序方式的一个典型用例是读取很少被更改的数据。\n\nAs of C++ 17, this type of memory ordering is being revised, and the use of `memory_order_consume` is temporarily discouraged.\n\n# 顺序一致排序\n\n标记为`memory_order_seq_cst`的原子操作不仅以与释放/获取排序相同的方式对内存进行排序(在一个线程中存储之前发生的所有事情在进行加载的线程中都变成了*可见的副作用*，而且还为所有标记为`memory_order_seq_cst`的原子操作建立了*单一的总修改顺序*。\n\n这种排序对于所有使用者都必须以完全相同的顺序观察其他线程所做的更改的情况可能是必要的。在多核或多 CPU 系统上，它需要完全的内存屏障。\n\n由于如此复杂的设置，这种类型的排序明显慢于其他类型。它还要求每一个原子操作都必须用这种类型的内存排序进行标记，否则顺序排序将会丢失。\n\n# 易变关键字\n\n`volatile`这个关键词对于任何一个写过复杂多线程代码的人来说，可能都是相当熟悉的。它的基本用途是告诉编译器，相关变量应该总是从内存中加载，永远不要对其值做假设。它还确保编译器不会对变量进行任何激进的优化。\n\n对于多线程应用，它通常是无效的，但是，不鼓励使用它。volatile 规范的主要问题是它没有定义多线程内存模型，这意味着这个关键字的结果可能不会跨平台、CPU 甚至工具链确定。\n\n在原子领域，这个关键字不是必需的，事实上也不太可能有帮助。为了保证获得在多个中央处理器内核和它们的高速缓存之间共享的变量的当前版本，必须使用像`atomic_compare_exchange_strong`、`atomic_fetch_add`或`atomic_exchange`这样的操作来让硬件获取正确的当前值。\n\n对于多线程代码，建议不要使用 volatile 关键字，而是使用 atomics，以保证正确的行为。\n\n# 摘要\n\n在这一章中，我们研究了原子操作，以及如何将它们集成到编译器中，以使代码尽可能地与底层硬件紧密合作。读者现在将熟悉原子操作的类型、内存屏障(栅栏)的使用，以及各种类型的内存排序及其含义。\n\n读者现在能够在自己的代码中使用原子操作来完成无锁设计，并正确使用 C++ 11 内存模型。\n\n在下一章中，我们将带着到目前为止所学的一切，远离 CPU，转而看一下 GPU，显卡(GPU)上数据的通用处理。"
  },
  {
    "path": "docs/master-cpp-multithrd/09.md",
    "content": "# 九、分布式计算中的多线程\n\n分布式计算是多线程编程的最初应用之一。当每台个人电脑只有一个单核处理器时，政府和研究机构以及一些公司会有多处理器系统，通常是集群形式。这些能够进行多线程处理；通过在处理器之间拆分任务，他们可以加速各种任务，包括模拟、渲染 CGI 电影等。\n\n如今，几乎每个桌面级或更好的系统都有不止一个处理器内核，使用廉价的以太网布线将多个系统组装成一个集群非常容易。结合 OpenMP 和 Open MPI 等框架，扩展基于 C++ 的(多线程)应用以在分布式系统上运行是非常容易的。\n\n本章的主题包括:\n\n*   在多线程 C++ 应用中集成 OpenMP 和 MPI\n*   实现分布式多线程应用\n*   分布式多线程编程的常见应用和问题\n\n# 简而言之，分布式计算\n\n当涉及到并行处理大型数据集时，理想的情况是可以将数据分割成许多小部分，并将其推送到许多线程，从而显著缩短处理所述数据所花费的总时间。\n\n分布式计算背后的思想是这样的:在分布式系统的每个节点上，运行我们的应用的一个或多个实例，由此这个应用可以是单个的，也可以是多线程的。由于进程间通信的开销，使用多线程应用通常更有效，也由于其他可能的优化——资源共享。\n\n如果已经有了一个可以使用的多线程应用，那么可以直接使用 MPI 来使它在分布式系统上工作。否则，OpenMP 是一个编译器扩展(用于 C/C++ 和 Fortran)，它可以使一个应用在没有重构的情况下实现多线程相对无痛。\n\n为此，OpenMP 允许标记一个公共代码段，在所有从线程上执行。一个主线程创建多个从线程，这些从线程将同时处理同一个代码段。一个基本的 *Hello World* OpenMP 应用如下所示:\n\n```cpp\n/******************************************************************************\n * FILE: omp_hello.c\n * DESCRIPTION:\n *   OpenMP Example - Hello World - C/C++ Version\n *   In this simple example, the master thread forks a parallel region.\n *   All threads in the team obtain their unique thread number and print it.\n *   The master thread only prints the total number of threads.  Two OpenMP\n *   library routines are used to obtain the number of threads and each\n *   thread's number.\n * AUTHOR: Blaise Barney  5/99\n * LAST REVISED: 04/06/05\n ******************************************************************************/\n #include <omp.h>\n #include <stdio.h>\n #include <stdlib.h>\n\n int main (int argc, char *argv[])  {\n    int nthreads, tid;\n\n    /* Fork a team of threads giving them their own copies of variables */\n #pragma omp parallel private(nthreads, tid) {\n          /* Obtain thread number */\n          tid = omp_get_thread_num();\n          printf(\"Hello World from thread = %d\\n\", tid);\n\n          /* Only master thread does this */\n          if (tid == 0) {\n                nthreads = omp_get_num_threads();\n                printf(\"Number of threads = %d\\n\", nthreads);\n                }\n\n    }  /* All threads join master thread and disband */ \n} \n\n```\n\n从这个基本示例中可以很容易地看出，OpenMP 通过`<omp.h>`头提供了一个基于 C 的 API。我们还可以看到将由每个线程执行的部分，由`#pragma omp`预处理器宏标记。\n\n与我们在前面章节中看到的多线程代码示例相比，OpenMP 的优势在于，可以轻松地将一段代码标记为多线程，而无需进行任何实际的代码更改。随之而来的明显限制是，每个线程实例将执行完全相同的代码，并且进一步的优化选项是有限的。\n\n# 平均弹着点\n\n为了调度特定节点上代码的执行，常用 **MPI** ( **消息传递接口**)。Open MPI 是这方面的一个免费库实现，被很多高级超级计算机使用。MPICH 是另一个流行的实现。\n\nMPI 本身被定义为并行计算机编程的通信协议。目前正在进行第三次修订(MPI-3)。\n\n总之，MPI 提供了以下基本概念:\n\n*   **通信器**:通信器对象连接 MPI 会话中的一组进程。它为进程分配唯一的标识符，并在有序的拓扑中排列进程。\n*   **点对点操作**:这种类型的操作允许特定进程之间的直接通信。\n*   **集合功能**:这些功能涉及在流程组内广播通信。它们也可以以相反的方式使用，这将从一个组中的所有进程获取结果，例如，在单个节点上对它们求和。更具选择性的版本将确保特定的数据项被发送到特定的节点。\n*   **派生数据类型**:由于 MPI 集群中并不是每个节点都保证有相同的数据类型定义、字节顺序和解释，所以 MPI 要求指定每个数据段是什么类型，这样 MPI 才能进行数据转换。\n*   **单侧通信**:这些操作允许一个人向或从远程存储器写入或读取，或者跨多个任务执行缩减操作，而不必在任务之间同步。这对于某些类型的算法非常有用，例如那些涉及分布式矩阵乘法的算法。\n*   **动态进程管理**:这是一个允许 MPI 进程创建新的 MPI 进程，或者与新创建的 MPI 进程建立通信的功能。\n*   **并行 I/O** :也叫 MPI-IO，这是分布式系统上 I/O 管理的一个抽象，包括文件访问，便于 MPI 使用。\n\n其中，MPI-IO、动态进程管理和单边通信是 MPI-2 的特色。从基于 MPI-1 的代码的迁移和动态过程管理与某些设置的不兼容性，以及许多不需要 MPI-2 特性的应用，意味着 MPI-2 的采用相对较慢。\n\n# 履行\n\nMPI 的最初实施是由**阿贡国家实验室** ( **ANL** )和密西西比州立大学实施的 **MPICH** 。它是目前最流行的实现之一，被用作 MPI 实现的基础，包括由 IBM(蓝色基因)、英特尔、QLogic、Cray、杨梅、微软、俄亥俄州立大学(MVAPICH)和其他公司开发的实现。\n\n另一个非常常见的实现是开放 MPI，它是由三个 MPI 实现合并而成的:\n\n*   田纳西大学\n*   洛斯阿拉莫斯国家实验室\n*   印第安纳大学\n\n这些，连同斯图加特大学的 PACX-MPI 团队，是开放 MPI 团队的创始成员。开放 MPI 的主要目标之一是创建一个高质量的开源 MPI-3 实现。\n\nMPI 实现被授权支持 C 和 Fortran。C/C++ 和 Fortran 以及汇编支持非常普遍，其他语言的绑定也是如此。\n\n# 使用 MPI\n\n无论选择哪种实现，最终的应用编程接口都将始终与官方的 MPI 标准相匹配，不同之处仅在于库所支持的 MPI 版本。然而，所有 MPI-1(修订版 1.3)特性都应该被任何 MPI 实现所支持。\n\n这意味着，无论选择哪个库，MPI 的规范 Hello World(例如，在 MPI 教程网站:[http://mpitutorial.com/tutorials/mpi-hello-world/](http://mpitutorial.com/tutorials/mpi-hello-world/))都应该工作:\n\n```cpp\n#include <mpi.h> \n#include <stdio.h> \n\nint main(int argc, char** argv) { \n         // Initialize the MPI environment \n         MPI_Init(NULL, NULL); \n\n         // Get the number of processes \n         int world_size; \n         MPI_Comm_size(MPI_COMM_WORLD, &world_size); \n\n         // Get the rank of the process \n         int world_rank; \n         MPI_Comm_rank(MPI_COMM_WORLD, &world_rank); \n\n         // Get the name of the processor \n         char processor_name[MPI_MAX_PROCESSOR_NAME]; \n         int name_len; \n         MPI_Get_processor_name(processor_name, &name_len); \n\n         // Print off a hello world message \n         printf(\"Hello world from processor %s, rank %d\" \n                     \" out of %d processors\\n\", \n                     processor_name, world_rank, world_size); \n\n         // Finalize the MPI environment. \n         MPI_Finalize(); \n} \n\n```\n\n通读这个基于 MPI 的应用的基本示例时，熟悉 MPI 使用的术语非常重要，尤其是:\n\n*   **世界**:该作业的注册 MPI 进程\n*   **通信器**:连接一个会话中所有 MPI 进程的对象\n*   **等级**:通信器中进程的标识符\n*   **处理器**:物理 CPU，多核 CPU 的单个核心，或者系统的主机名\n\n在这个 Hello World 的例子中，我们可以看到我们包含了`<mpi.h>`头。不管我们使用什么实现，这个 MPI 头总是相同的。\n\n初始化 MPI 环境需要对`MPI_Init()`进行一次调用，可以取两个参数，这两个参数在这一点上都是可选的。\n\n下一步是获得世界的大小(即可用进程的数量)。这是使用`MPI_Comm_size()`完成的，它采用`MPI_COMM_WORLD`全局变量(由 MPI 定义供我们使用)，并使用该世界中的进程数更新第二个参数。\n\n然后我们获得的等级本质上是 MPI 分配给这个进程的唯一标识。通过`MPI_Comm_rank()`获得该 UID。同样，这将`MPI_COMM_WORLD`变量作为第一个参数，并返回我们的数值排名作为第二个参数。这个等级对于进程间的自我识别和交流非常有用。\n\n获取正在运行的特定硬件的名称也很有用，特别是对于诊断目的。为此我们可以称之为`MPI_Get_processor_name()`。返回的字符串将具有全局定义的最大长度，并将以某种方式标识硬件。该字符串的确切格式由实现定义。\n\n最后，我们打印出我们收集的信息，并在终止应用之前清理 MPI 环境。\n\n# 编译 MPI 应用\n\n为了编译 MPI 应用，使用了`mpicc`编译器包装器。这个可执行文件应该是已经安装的任何 MPI 实现的一部分。\n\n然而，使用它与使用例如海湾合作委员会是一样的:\n\n```cpp\n    $ mpicc -o mpi_hello_world mpi_hello_world.c\n\n```\n\n这可以比作:\n\n```cpp\n    $ gcc mpi_hello_world.c -lmsmpi -o mpi_hello_world\n\n```\n\n这将把我们的 Hello World 示例编译并链接成二进制文件，准备执行。但是，执行这个二进制文件不是通过直接启动它来完成的，而是使用一个启动器，如下所示:\n\n```cpp\n    $ mpiexec.exe -n 4 mpi_hello_world.exe\n    Hello world from processor Generic_PC, rank 0 out of 4 processors\n    Hello world from processor Generic_PC, rank 2 out of 4 processors\n    Hello world from processor Generic_PC, rank 1 out of 4 processors\n    Hello world from processor Generic_PC, rank 3 out of 4 processors\n\n```\n\n前面的输出来自运行在视窗系统的 Bash 外壳中的开放 MPI。如我们所见，我们总共启动了四个流程(4 个级别)。处理器名称被报告为每个进程的主机名(“PC”)。\n\n用来启动 MPI 应用的二进制文件称为 mpiexec 或 mpirun 或 orterun。这些是同一个二进制的同义词，尽管不是所有的实现都有同义词。对于开放 MPI，这三个都存在，并且可以使用其中的任何一个。\n\n# 集群硬件\n\n基于 MPI 或类似应用运行的系统由多个独立的系统(节点)组成，每个系统都使用某种网络接口与其他系统相连。对于高端应用，这些往往是具有高速、低延迟互连的定制节点。另一端是所谓的贝奥武夫和类似类型的集群，由标准(台式)计算机制成，通常使用普通以太网连接。\n\n在撰写本文时，最快的超级计算机(根据 TOP500 榜单)是位于中国无锡国家超级计算中心的 Sunway TaihuLight 超级计算机。它总共使用了 40，960 个中国设计的 SW26010 多核 RISC 架构 CPU，每个 CPU 有 256 个内核(分为 4 个 64 核组)，以及 4 个管理内核。术语*多核*指的是一种专门的中央处理器设计，它更注重显式并行性，而不是大多数中央处理器核心的单线程和通用重点。这种类型的中央处理器一般类似于图形处理器架构和矢量处理器。\n\n每个节点都包含一个 SW26010 和 32 GB 的 DDR3 内存。它们通过基于 PCIe 3.0 的网络连接，该网络本身由三层结构组成:中央交换网络(用于超级节点)、超级节点网络(连接超级节点中的所有 256 个节点)和资源网络，后者提供对 I/O 和其他资源服务的访问。单个节点之间的网络带宽为 12gb/秒，延迟约为 1 微秒。\n\n下图(来自“太阳之路太湖光超级计算机:系统和应用”，DOI: 10.1007/s11432-016-5588-7)提供了该系统的可视化概述:\n\n![](img/600f03ad-1a17-4141-bafa-74b44e2fa12f.png)\n\n对于预算不允许如此复杂和高度定制的系统的情况，或者特定任务不保证采用这种方法的情况，始终存在“贝奥武夫”方法。贝奥武夫集群是一个术语，用来指由普通计算机系统构建的分布式计算系统。这些可以是基于英特尔或 AMD 的 x86 系统，基于 ARM 的处理器现在变得越来越流行。\n\n让集群中的每个节点与其他节点大致相同通常会有所帮助。虽然有可能有一个不对称的集群，但当人们可以对每个节点进行广泛的假设时，管理和作业调度就变得容易得多。\n\n至少，人们会希望匹配处理器架构，具有基本级别的 CPU 扩展，例如 SSE2/3，可能还有 AVX 和同族，在所有节点上都通用。这样做将允许跨节点使用相同的编译二进制，以及相同的算法，极大地简化了作业的部署和代码库的维护。\n\n对于节点之间的网络，以太网是一个非常受欢迎的选择，它提供以几十到几百微秒为单位的通信时间，而成本仅为更快选择的一小部分。通常每个节点都连接到一个以太网，如下图所示:\n\n![](img/eaf411b6-c59f-43cf-b8ed-998e5cef23e0.png)\n\n还可以选择为每个节点或特定节点添加第二条甚至第三条以太网链路，使它们能够访问文件、输入/输出和其他资源，而不必与主网络层的带宽竞争。对于非常大的集群，人们可以考虑一种方法，例如 Sunway TaihuLight 和许多其他超级计算机使用的方法:将节点拆分为超级节点，每个节点都有自己的节点间网络。这将允许人们通过仅将流量限制到相关联的节点来优化网络上的流量。\n\n这种优化的贝奥武夫集群的示例如下所示:\n\n![](img/31762d73-6c29-4b4a-86fe-999e8581327d.png)\n\n显然，基于 MPI 的集群有多种可能的配置，可以使用定制的、现成的或两种类型硬件的组合。群集的预期目的通常决定了特定群集的最佳布局，例如运行模拟或处理大型数据集。每种类型的工作都有自己的一套限制和要求，这也反映在软件实现中。\n\n# 安装开放 MPI\n\n在本章的剩余部分，我们将关注开放 MPI。为了获得一个开放 MPI 的工作开发环境，必须安装它的头文件和库文件，以及它的支持工具和二进制文件。\n\n# Linux 和 BSDs\n\n在带有包管理系统的 Linux 和 BSD 发行版上，这非常容易:只需安装 Open MPI 包，一切都应该设置和配置好，随时可以使用。请查阅手册了解具体的发行版本，了解如何搜索和安装特定的软件包。\n\n在基于 Debian 的发行版上，可以使用:\n\n```cpp\n    $ sudo apt-get install openmpi-bin openmpi-doc libopenmpi-dev\n\n```\n\n前面的命令将安装开放 MPI 二进制文件、文档和开发头。计算节点上可以省略最后两个包。\n\n# Windows 操作系统\n\n在 Windows 上，事情变得稍微复杂，主要是因为 Visual C++ 和伴随的编译器工具链占据了主导地位。如果您希望使用与 Linux 或 BSD 相同的开发环境，使用 MinGW，您必须采取一些额外的步骤。\n\nThis chapter assumes the use of either GCC or MinGW. If one wishes to develop MPI applications using the Visual Studio environment, please consult the relevant documentation for this.\n\n最容易使用和最新的 MinGW 环境是 MSYS2，它提供了一个 Bash 外壳和大多数在 Linux 和 BSD 下熟悉的工具。它还具有从 Linux Arch 发行版中已知的 Pacman 包管理器。使用它，很容易安装开放 MPI 开发所需的包。\n\n从[https://msys2.github.io/](https://msys2.github.io/)安装 MSYS2 环境后，安装 MinGW 工具链:\n\n```cpp\n    $ pacman -S base-devel mingw-w64-x86_64-toolchain\n\n```\n\n这假设安装了 64 位版本的 MSYS2。对于 32 位版本，请选择 i686 而不是 x86_64。安装完这些软件包后，我们将安装 MinGW 和基本开发工具。为了使用它们，使用名称中的 MinGW 64 位后缀启动一个新的 shell，或者通过开始菜单中的快捷方式，或者通过使用 MSYS2 `install`文件夹中的可执行文件。\n\nMinGW 准备好了，是时候安装 MS-MPI 7 . x 版本了，这是微软对 MPI 的实现，也是 Windows 上使用 MPI 最简单的方法。它是 MPI-2 规范的一个实现，主要与 MPICH2 参考实现兼容。由于 MS-MPI 库在不同版本之间不兼容，所以我们使用这个特定的版本。\n\n虽然 MS-MPI 第 7 版已经存档，但仍可通过 https://www.microsoft.com/en-us/download/details.aspx?[微软下载中心下载 id=49926](https://www.microsoft.com/en-us/download/details.aspx?id=49926) 。\n\nMS-MPI 7 版自带`msmpisdk.msi`和`MSMpiSetup.exe`两个安装程序。两者都需要安装。之后，我们应该能够打开一个新的 MSYS2 外壳，并找到以下环境变量设置:\n\n```cpp\n    $ printenv | grep \"WIN\\|MSMPI\"\n    MSMPI_INC=D:\\Dev\\MicrosoftSDKs\\MPI\\Include\\\n    MSMPI_LIB32=D:\\Dev\\MicrosoftSDKs\\MPI\\Lib\\x86\\\n    MSMPI_LIB64=D:\\Dev\\MicrosoftSDKs\\MPI\\Lib\\x64\\\n    WINDIR=C:\\Windows\n\n```\n\nprintenv 命令的输出显示 MS-MPI 软件开发工具包和运行时安装正确。接下来，我们需要将静态库从 Visual C++ LIB 格式转换为 MinGW A 格式:\n\n```cpp\n    $ mkdir ~/msmpi\n    $ cd ~/msmpi\n    $ cp \"$MSMPI_LIB64/msmpi.lib\" .\n    $ cp \"$WINDIR/system32/msmpi.dll\" .\n    $ gendef msmpi.dll\n    $ dlltool -d msmpi.def -D msmpi.dll -l libmsmpi.a\n    $ cp libmsmpi.a /mingw64/lib/.\n\n```\n\n我们首先将原始的 LIB 文件和运行时 DLL 一起复制到主文件夹中的一个新的临时文件夹中。接下来，我们在 DLL 上使用 gendef 工具来创建我们将需要的定义，以便将其转换为新的格式。\n\n最后一步是用 dlltool 完成的，它将定义文件和 DLL 一起使用，并输出一个与 MinGW 兼容的静态库文件。这个文件，然后我们复制到一个位置，MinGW 可以找到它以后当链接。\n\n接下来，我们需要复制 MPI 头:\n\n```cpp\n    $ cp \"$MSMPI_INC/mpi.h\" .\n\n```\n\n复制此头文件后，我们必须打开它并找到以下列内容开头的部分:\n\n```cpp\ntypedef __int64 MPI_Aint \n\n```\n\n在该行的正上方，我们需要添加以下行:\n\n```cpp\n    #include <stdint.h>\n\n```\n\n这包括增加了`__int64`的定义，我们需要它来正确编译代码。\n\n最后，将头文件复制到 MinGW `include`文件夹:\n\n```cpp\n    $ cp mpi.h /mingw64/include\n\n```\n\n有了这个，我们就有了库和头文件，可以和 MinGW 一起开发 MPI 了。允许我们编译和运行早期的 Hello World 示例，并继续本章的其余部分。\n\n# 跨节点分发作业\n\n为了在集群中的节点间分配 MPI 作业，必须将这些节点指定为`mpirun` / `mpiexec`命令的参数，或者使用主机文件。该主机文件包含网络上可用于运行的节点的名称，以及主机上可用插槽的数量。\n\n在远程节点上运行 MPI 应用的先决条件是 MPI 运行时安装在该节点上，并且已经为该节点配置了无密码访问。这意味着只要主节点安装了 SSH 密钥，它就可以登录到这些节点中的每一个，以便在其上启动 MPI 应用。\n\n# 设置 MPI 节点\n\n在节点上安装 MPI 后，下一步是为主节点设置无密码 SSH 访问。这需要在节点上安装 SSH 服务器(基于 Debian 的发行版上的 *ssh* 包的一部分)。之后，我们需要生成并安装 SSH 密钥。\n\n一种简单的方法是在主节点和其他节点上有一个公共用户，并使用 NFS 网络共享或类似方式将用户文件夹装载到计算节点上的主节点上。这样，所有节点都将拥有相同的 SSH 密钥和已知主机文件。这种方法的一个缺点是缺乏安全性。对于一个互联网连接的集群，这不是一个很好的方法。\n\n然而，作为同一个用户在每个节点上运行作业绝对是一个好主意，以防止任何可能的权限问题，尤其是在使用文件和其他资源时。使用在每个节点上创建的公共用户帐户和生成的 SSH 密钥，我们可以使用以下命令将公钥传输到节点:\n\n```cpp\n    $ ssh-copy-id mpiuser@node1\n\n```\n\n或者，我们可以在设置时将公钥复制到节点系统上的`authorized_keys`文件中。如果创建和配置大量节点，使用映像复制到每个节点的系统驱动器、使用设置脚本或可能通过 PXE 引导从映像引导是有意义的。\n\n完成此步骤后，主节点现在可以登录到每个计算节点来运行作业。\n\n# 创建 MPI 主机文件\n\n如前所述，为了在其他节点上运行作业，我们需要指定这些节点。最简单的方法是创建一个包含我们希望使用的计算节点名称以及可选参数的文件。\n\n为了允许我们使用节点名称而不是 IP 地址，我们必须首先修改操作系统的主机文件:例如，Linux 上的`/etc/hosts`:\n\n```cpp\n    192.168.0.1 master\n    192.168.0.2 node0\n    192.168.0.3 node1\n\n```\n\n接下来，我们创建一个新文件，它将成为 MPI 使用的主机文件:\n\n```cpp\n    master\n    node0\n    node1\n\n```\n\n使用这种配置，作业将在两个计算节点以及主节点上执行。我们可以从这个文件中取出主节点来防止这种情况。\n\n如果不提供任何可选参数，MPI 运行时将使用节点上所有可用的处理器。如果需要，我们可以限制这个数字:\n\n```cpp\n    node0 slots=2\n    node1 slots=4\n\n```\n\n假设两个节点都是四核 CPU，这将意味着节点 0 上只有一半的核心会被使用，而所有核心都在节点 1 上。\n\n# 运行作业\n\n跨多个 MPI 节点运行 MPI 作业与仅在本地执行基本相同，如本章前面的示例所示:\n\n```cpp\n    $ mpirun --hostfile my_hostfile hello_mpi_world\n\n```\n\n该命令将告诉 MPI 启动器使用名为`my_hostfile`的主机文件，并在该主机文件中找到的每个节点的每个处理器上运行指定 MPI 应用的副本。\n\n# 使用集群调度程序\n\n除了使用手动命令和主机文件在特定节点上创建和启动作业之外，还有集群调度程序应用。这些通常包括在每个节点和主节点上运行守护进程。使用提供的工具，可以管理资源和作业，安排分配和跟踪作业状态。\n\n最受欢迎的集群管理调度程序之一是 Slurm，它是简单 Linux 资源管理实用程序的缩写(虽然现在更名为 SLURM 工作负载管理器，网站位于[https://slurm.schedmd.com/](https://slurm.schedmd.com/))。它通常被超级计算机和许多计算机集群使用。其主要功能包括:\n\n*   使用时隙向特定用户分配对资源(节点)的独占或非独占访问\n*   在一组节点上启动和监控作业，如基于 MPI 的应用\n*   管理挂起作业的队列以仲裁共享资源的争用\n\n基本群集操作不需要设置群集调度程序，但是当同时运行多个作业时，或者当群集的多个用户希望运行他们自己的作业时，设置群集调度程序对于较大的群集非常有用。\n\n# MPI 通信\n\n此时，我们有了一个功能性的 MPI 集群，它可以用来以并行方式执行基于 MPI 的应用(以及其他应用)。虽然对于某些任务来说，在途中发送几十个或几百个进程并等待它们完成可能没问题，但通常这些并行进程能够相互通信是至关重要的。\n\n这就是 MPI(作为“消息传递接口”)的真正意义发挥作用的地方。在 MPI 作业创建的层次结构中，进程可以通过多种方式进行通信和共享数据。最根本的是，他们可以分享和接收信息。\n\nMPI 消息具有以下属性:\n\n*   寄件人\n*   接收器\n*   信息标签\n*   消息中元素的计数\n*   一个 MPI 数据类型\n\n发送方和接收方应该相当明显。消息标签是一个数字标识，发送者可以设置，接收者可以使用它来过滤消息，例如，允许对特定消息进行优先排序。数据类型决定了消息中包含的信息类型。\n\n发送和接收功能如下所示:\n\n```cpp\nint MPI_Send( \n         void* data, \n         int count, \n         MPI_Datatype datatype, \n         int destination, \n         int tag, \n         MPI_Comm communicator) \n\nint MPI_Recv( \n         void* data, \n         int count, \n         MPI_Datatype datatype, \n         int source, \n         int tag, \n         MPI_Comm communicator, \n         MPI_Status* status) \n\n```\n\n这里需要注意的一件有趣的事情是，send 函数中的 count 参数指示该函数将发送的元素数量，而 receive 函数中的相同参数指示该线程将接受的最大元素数量。\n\n通信器是指正在使用的 MPI 通信器实例，接收函数包含一个最终参数，可用于检查 MPI 消息的状态。\n\n# MPI 数据类型\n\nMPI 定义了许多可以直接使用的基本类型:\n\n| **MPI datatype** | **碳当量** |\n| --- | --- |\n| `MPI_SHORT` | 短整型 |\n| `MPI_INT` | （同 Internationalorganizations）国际组织 |\n| `MPI_LONG` | 长整型 |\n| `MPI_LONG_LONG` | 长整型 |\n| `MPI_UNSIGNED_CHAR` | 无符号字符 |\n| `MPI_UNSIGNED_SHORT` | 无符号短整型 |\n| `MPI_UNSIGNED` | 无符号整数 |\n| `MPI_UNSIGNED_LONG` | 无符号长整型 |\n| `MPI_UNSIGNED_LONG_LONG` | 无符号长整型 |\n| `MPI_FLOAT` | 漂浮物 |\n| `MPI_DOUBLE` | 两倍 |\n| `MPI_LONG_DOUBLE` | 长双 |\n| `MPI_BYTE` | 茶 |\n\nMPI 保证当使用这些类型时，接收方将总是以它期望的格式获得消息数据，而不管字符顺序和其他与平台相关的问题。\n\n# 自定义类型\n\n除了这些基本格式，还可以创建新的 MPI 数据类型。这些使用了许多 MPI 功能，包括`MPI_Type_create_struct`:\n\n```cpp\nint MPI_Type_create_struct( \n   int count,  \n   int array_of_blocklengths[], \n         const MPI_Aint array_of_displacements[],  \n   const MPI_Datatype array_of_types[], \n         MPI_Datatype *newtype) \n\n```\n\n使用这个函数，可以创建一个包含结构的 MPI 类型，就像基本的 MPI 数据类型一样传递:\n\n```cpp\n#include <cstdio> \n#include <cstdlib> \n#include <mpi.h> \n#include <cstddef> \n\nstruct car { \n        int shifts; \n        int topSpeed; \n}; \n\nint main(int argc, char **argv) { \n         const int tag = 13; \n         int size, rank; \n\n         MPI_Init(&argc, &argv); \n         MPI_Comm_size(MPI_COMM_WORLD, &size); \n\n         if (size < 2) { \n               fprintf(stderr,\"Requires at least two processes.\\n\"); \n               MPI_Abort(MPI_COMM_WORLD, 1); \n         } \n\n         const int nitems = 2; \n         int blocklengths[2] = {1,1}; \n   MPI_Datatype types[2] = {MPI_INT, MPI_INT}; \n         MPI_Datatype mpi_car_type; \n         MPI_Aint offsets[2]; \n\n         offsets[0] = offsetof(car, shifts); \n         offsets[1] = offsetof(car, topSpeed); \n\n         MPI_Type_create_struct(nitems, blocklengths, offsets, types, &mpi_car_type); \n         MPI_Type_commit(&mpi_car_type); \n\n         MPI_Comm_rank(MPI_COMM_WORLD, &rank); \n         if (rank == 0) { \n               car send; \n               send.shifts = 4; \n               send.topSpeed = 100; \n\n               const int dest = 1; \n\n         MPI_Send(&send, 1, mpi_car_type, dest, tag, MPI_COMM_WORLD); \n\n               printf(\"Rank %d: sent structure car\\n\", rank); \n         } \n\n   if (rank == 1) { \n               MPI_Status status; \n               const int src = 0; \n\n         car recv; \n\n         MPI_Recv(&recv, 1, mpi_car_type, src, tag, MPI_COMM_WORLD, &status); \n         printf(\"Rank %d: Received: shifts = %d topSpeed = %d\\n\", rank, recv.shifts, recv.topSpeed); \n    } \n\n    MPI_Type_free(&mpi_car_type); \n    MPI_Finalize(); \n\n         return 0; \n} \n\n```\n\n这里我们看到一个新的叫做`mpi_car_type`的 MPI 数据类型是如何定义的，并用来在两个进程之间传递消息。要创建这样的结构类型，我们需要定义结构中的项数、每个块中的元素数、它们的字节位移以及它们的基本 MPI 类型。\n\n# 基本通信\n\nMPI 通信的一个简单例子是将单个值从一个进程发送到另一个进程。为此，需要使用下面列出的代码并运行编译后的二进制文件来启动至少两个进程。这些进程是在本地运行还是在两个计算节点上运行并不重要。\n\n以下代码是从[http://mpitutorial.com/tutorials/mpi-hello-world/](http://mpitutorial.com/tutorials/mpi-hello-world/)借来的:\n\n```cpp\n#include <mpi.h> \n#include <stdio.h> \n#include <stdlib.h> \n\nint main(int argc, char** argv) { \n   // Initialize the MPI environment. \n   MPI_Init(NULL, NULL); \n\n   // Find out rank, size. \n   int world_rank; \n   MPI_Comm_rank(MPI_COMM_WORLD, &world_rank); \n   int world_size; \n   MPI_Comm_size(MPI_COMM_WORLD, &world_size); \n\n   // We are assuming at least 2 processes for this task. \n   if (world_size < 2) { \n               fprintf(stderr, \"World size must be greater than 1 for %s.\\n\", argv[0]); \n               MPI_Abort(MPI_COMM_WORLD, 1); \n   } \n\n   int number; \n   if (world_rank == 0) { \n         // If we are rank 0, set the number to -1 and send it to process 1\\. \n               number = -1; \n               MPI_Send(&number, 1, MPI_INT, 1, 0, MPI_COMM_WORLD); \n   }  \n   else if (world_rank == 1) { \n               MPI_Recv(&number, 1, MPI_INT, 0, 0,  \n                           MPI_COMM_WORLD,  \n                           MPI_STATUS_IGNORE); \n               printf(\"Process 1 received number %d from process 0.\\n\", number); \n   } \n\n   MPI_Finalize(); \n} \n\n```\n\n这段代码没有太多内容。我们通过通常的 MPI 初始化工作，然后检查以确保我们的世界大小至少有两个进程大。\n\n等级为 0 的进程将发送数据类型为`MPI_INT`且值为`-1`的 MPI 消息。有等级的进程`1`会等待收到这个消息。接收过程指定`MPI_Status MPI_STATUS_IGNORE`表示该过程将不检查消息的状态。这是一种有用的优化技术。\n\n最后，预期输出如下:\n\n```cpp\n    $ mpirun -n 2 ./send_recv_demo\n    Process 1 received number -1 from process 0\n\n```\n\n在这里，我们从总共两个过程开始编译演示代码。输出显示第二个进程收到了来自第一个进程的 MPI 消息，值正确。\n\n# 高级通信\n\n对于高级 MPI 通信，可以使用`MPI_Status`字段获取关于消息的更多信息。您可以使用`MPI_Probe`在通过`MPI_Recv`接受消息之前发现消息的大小。这对于事先不知道消息大小的情况很有用。\n\n# 广播\n\n广播一条消息意味着世界上所有的进程都会收到它。这相对于发送功能简化了广播功能:\n\n```cpp\nint MPI_Bcast( \n   void *buffer,  \n   int count,  \n   MPI_Datatype datatype, \n         int root,    \n   MPI_Comm comm) \n\n```\n\n接收过程将简单地使用正常的`MPI_Recv`功能。广播功能所做的只是使用一种算法来优化许多消息的发送，该算法同时使用多个网络链路，而不是一个。\n\n# 分散和聚集\n\n分散非常类似于广播消息，有一个非常重要的区别:它不是在每个消息中发送相同的数据，而是向每个接收者发送数组的不同部分。其功能定义如下:\n\n```cpp\nint MPI_Scatter( \n         void* send_data, \n         int send_count, \n         MPI_Datatype send_datatype, \n         void* recv_data, \n         int recv_count, \n         MPI_Datatype recv_datatype, \n         int root, \n         MPI_Comm communicator) \n\n```\n\n每个接收流程将获得相同的数据类型，但我们可以指定将向每个流程发送多少个项目(`send_count`)。该功能用于发送和接收端，后者只需定义与接收数据相关的最后一组参数，并提供根进程和相关通信器的世界排名。\n\n聚集是散射的逆过程。在这里，多个进程将发送最终到达单个进程的数据，这些数据按照发送它的进程的等级排序。其功能定义如下:\n\n```cpp\nint MPI_Gather( \n         void* send_data, \n         int send_count, \n         MPI_Datatype send_datatype, \n         void* recv_data, \n         int recv_count, \n         MPI_Datatype recv_datatype, \n         int root, \n         MPI_Comm communicator) \n\n```\n\n人们可能会注意到这个函数看起来非常类似于散射函数。这是因为它的工作方式基本相同，只是这一次各地的发送节点都要填写与发送数据相关的参数，而接收过程则要填写与接收数据相关的参数。\n\n这里需要注意的是`recv_count`参数与从每个发送进程接收的数据量有关，而不是总量。\n\n这两个基本功能还有进一步的专门化，但这里不涉及。\n\n# MPI 与线程\n\n人们可能会认为使用 MPI 将 MPI 应用的一个实例分配给每个集群节点上的单个 CPU 内核是最容易的，这是真的。然而，这不是最快的解决办法。\n\n尽管对于跨网络的进程之间的通信，MPI 可能是这种情况下的最佳选择，但是在单个系统(单个或多 CPU 系统)中，使用多线程很有意义。\n\n主要原因很简单，线程之间的通信比进程间通信快得多，尤其是在使用 MPI 这样的通用通信层时。\n\n可以编写一个应用，使用 MPI 在集群的网络上进行通信，从而为每个 MPI 节点分配一个应用实例。应用本身将检测该系统上的 CPU 内核数量，并为每个内核创建一个线程。因此，人们通常称之为混合 MPI，因为它具有以下优点而被广泛使用:\n\n*   **更快的通信**–使用快速线程间通信。\n*   **更少的 MPI 消息**–更少的消息意味着带宽和延迟的减少。\n*   **避免数据重复**–数据可以在线程之间共享，而不是向一系列进程发送相同的消息。\n\n通过使用 C++ 11 和后续版本中的多线程特性，可以按照我们在前面章节中看到的方式实现这一点。另一个选择是使用 OpenMP，正如我们在本章开头看到的。\n\n使用 OpenMP 的明显优势是它只需要开发人员很少的努力。如果你需要的只是运行更多相同例程的实例，那么只需要做一些小的修改来标记工作线程要使用的代码。\n\n例如:\n\n```cpp\n#include <stdio.h>\n#include <mpi.h>\n#include <omp.h>\n\nint main(int argc, char *argv[]) {\n  int numprocs, rank, len;\n  char procname[MPI_MAX_PROCESSOR_NAME];\n  int tnum = 0, tc = 1;\n\n  MPI_Init(&argc, &argv);\n  MPI_Comm_size(MPI_COMM_WORLD, &numprocs);\n  MPI_Comm_rank(MPI_COMM_WORLD, &rank);\n  MPI_Get_processor_name(procname, &len);\n\n  #pragma omp parallel default(shared) private(tnum, tc) {\n      np = omp_get_num_threads();\n      tnum = omp_get_thread_num();\n      printf(\"Thread %d out of %d from process %d out of %d on %s\\n\", \n      tnum, tc, rank, numprocs, procname);\n  }\n\n  MPI_Finalize();\n}\n\n```\n\n上面的代码结合了一个 OpenMP 应用和 MPI。为了编译它，我们将运行例如:\n\n```cpp\n$ mpicc -openmp hellohybrid.c -o hellohybrid\n\n```\n\n接下来，要运行该应用，我们将使用 mpirun 或等效工具:\n\n```cpp\n$ export OMP_NUM_THREADS=8\n$ mpirun -np 2 --hostfile my_hostfile -x OMP_NUM_THREADS ./hellohybrid\n\n```\n\nmpirun 命令将使用 hellohybrid 二进制文件运行两个 MPI 进程，将我们用-x 标志导出的环境变量传递给每个新进程。OpenMP 运行时将使用该变量中包含的值来创建该数量的线程。\n\n假设我们的 MPI 主机文件中至少有两个 MPI 节点，我们将在两个节点上有两个 MPI 进程，每个进程运行八个线程，这将适合一个具有超线程的四核 CPU 或一个八核 CPU。\n\n# 潜在问题\n\n当编写基于 MPI 的应用并在多核 CPU 或集群上执行它们时，可能遇到的问题与我们在前面几章中已经遇到的多线程代码的问题非常相似。\n\n然而，MPI 的另一个担心是依赖于网络资源的可用性。由于用于`MPI_Send`调用的发送缓冲区在网络堆栈能够处理该缓冲区之前不能被回收，并且该调用是阻塞类型的，发送大量小消息会导致一个进程等待另一个进程，而另一个进程又在等待调用完成。\n\n在设计 MPI 应用的消息结构时，应该记住这种类型的死锁。例如，可以确保一侧没有建立发送呼叫，这将导致这样的情况。提供关于队列深度等的反馈信息可以用来缓解压力。\n\nMPI 还包含一个使用所谓屏障的同步机制。这意味着在 MPI 进程之间使用，以允许它们在例如任务上同步。使用 MPI 屏障(`MPI_Barrier`)调用与互斥体类似，问题在于，如果 MPI 进程无法获得同步，此时一切都会挂起。\n\n# 摘要\n\n在这一章中，我们详细研究了 MPI 标准及其一些实现，特别是开放 MPI，并研究了如何设置集群。我们还看到了如何使用 OpenMP 轻松地将多线程添加到现有代码中。\n\n此时，读者应该能够设置一个基本的 Beowulf 或类似的集群，将其配置为 MPI，并在其上运行基本的 MPI 应用。应该知道 MPI 进程之间如何通信，以及如何定义自定义数据类型。此外，读者将意识到为 MPI 编程时的潜在陷阱。\n\n在下一章中，我们将学习前几章的所有知识，并在最后一章中看看我们如何将其结合起来，就像我们在视频卡(GPU)上查看通用计算一样。"
  },
  {
    "path": "docs/master-cpp-multithrd/10.md",
    "content": "# 十、图形处理器多线程\n\n最近的发展是将显卡用于通用计算。例如，使用 CUDA 和 OpenCL 等框架，可以加快医疗、军事和科学应用中大型数据集的并行处理。在本章中，我们将了解如何使用 C++ 和 OpenCL 来实现这一点，以及如何在 C++ 中将这样的特性集成到多线程应用中。\n\n本章的主题包括:\n\n*   将 OpenCL 集成到基于 C++ 的应用中\n*   以多线程方式使用 OpenCL 的挑战\n*   延迟和调度对多线程性能的影响\n\n# 图形处理器处理模型\n\n在[第 9 章](09.html)、*分布式计算的多线程*中，我们研究了在集群系统中的多个计算节点上运行相同的任务。这种设置的主要目标是以高度并行的方式处理数据，理论上相对于具有更少 CPU 内核的单个系统来说加快了所述处理。\n\n**GPU**(**图形处理单元上的通用计算**)在某些方面与此类似，但有一个主要区别:虽然只有常规 CPU 的计算集群擅长标量任务——这意味着在单个数据集上执行一个任务(SISD)——GPU 是擅长 SIMD(单输入、多数据)任务的矢量处理器。\n\n本质上，这意味着可以将一个大数据集连同单个任务描述一起发送到 GPU，GPU 将在其数百或数千个内核上对该数据的部分并行执行相同的任务。因此，我们可以将图形处理器视为一种非常特殊的集群:\n\n![](img/0c0ada1d-90eb-4934-a57e-59a7ac0fab1e.png)\n\n# 履行\n\n当图形处理器的概念第一次被提出时(大约在 2001 年)，编写图形处理器程序最常见的方法是使用 GLSL (OpenGL 着色语言)和类似的着色语言。由于这些着色器语言已经瞄准了 SIMD 任务(图像和场景数据)的处理，因此将它们调整为更通用的任务相当简单。\n\n从那时起，出现了许多更专业的实现:\n\n| **名称** | **自**起 | **车主** | **注释** |\n| 库达 | Two thousand and six | NVidia | 这是专有的，只在 NVidia 图形处理器上运行 |\n| 接近金属 | Two thousand and six | ATi/AMD | 这被 OpenCL 放弃了 |\n| DirectCompute | Two thousand and eight | 微软 | 这是与 DX11 一起发布的，运行在 DX10 GPUs 上，并且仅限于 Windows 平台 |\n| OpenCL(打开 CL) | Two thousand and nine | Khronos 集团 | 这是一个开放标准，适用于所有主流平台以及移动平台上的 AMD、英特尔和 NVidia GPUs |\n\n# OpenCL(打开 CL)\n\n在目前各种图形处理器实现中，由于没有限制，OpenCL 是迄今为止最有趣的图形处理器应用编程接口。它几乎适用于所有主流图形处理器和平台，甚至在特定的移动平台上也能获得支持。\n\nOpenCL 的另一个显著特点是它也不仅限于 GPU。作为其名称(开放计算语言)的一部分，它将一个系统抽象成所谓的*计算设备*，每个设备都有自己的功能。GPU 是最常见的应用，但是这个特性使得首先在 CPU 上测试实现变得相当容易，以便于调试。\n\nOpenCL 的一个可能的缺点是，它对内存和硬件细节采用了高度抽象，这可能会对性能产生负面影响，即使它增加了代码的可移植性。\n\n在本章的剩余部分，我们将关注 OpenCL。\n\n# 常见的 OpenCL 应用\n\n许多程序结合了基于 OpenCL 的代码，以加快操作速度。这些项目包括图形处理，以及三维建模和计算机辅助设计，音频和视频处理。一些例子是:\n\n*   Adobe Photoshop 中\n*   GIMP\n*   ImageMagick\n*   欧特克玛雅\n*   搅拌机\n*   手闸\n*   拉斯维加斯专业版\n*   OpenCV\n*   Libav\n*   最终切割专业版\n*   芬佩格\n\n在包括 LibreOffice Calc 和微软 Excel 在内的办公应用中，可以发现某些操作的进一步加速。\n\n也许更重要的是，OpenCL 也常用于科学计算和密码学，包括 BOINC 和 GROMACS 以及许多其他库和程序。\n\n# OpenCL 版本\n\n自 2008 年 12 月 8 日 OpenCL 规范发布以来，到目前为止已经进行了五次更新，使其达到了 2.2 版本。接下来将提到这些版本的重要变化。\n\n# OpenCL 1.0 版\n\n第一次公开发布是苹果在 2009 年 8 月 28 日发布的 macOS X 雪豹版本的一部分。\n\n与此同时，AMD 宣布将支持 OpenCL，并退役自己的 Close to Metal (CtM)框架。NVidia、RapidMind 和 IBM 也在自己的框架中增加了对 OpenCL 的支持。\n\n# OpenCL 1.1 版\n\nOpenCL 1.1 规范于 2010 年 6 月 14 日获得了 Khronos 集团的批准。它为并行编程和性能增加了额外的功能，包括:\n\n*   新的数据类型包括三分量矢量和附加图像格式\n*   处理来自多个主机线程的命令，并处理跨多个设备的缓冲区\n*   对缓冲区区域的操作，包括读取、写入和复制 1D、2D 或三维矩形区域\n*   增强使用事件来驱动和控制命令执行\n*   额外的 OpenCL 内置 C 函数，如整数箝位、洗牌和异步步进(不连续，但数据之间有间隙)拷贝\n*   通过链接 OpenCL 和 OpenGL 事件，高效共享图像和缓冲区，提高 OpenGL 互操作性\n\n# OpenCL 1.2 版\n\nOpenCL 1.2 版本于 2011 年 11 月 15 日发布。其最重要的特点包括以下几点:\n\n*   **设备分区:**这使应用能够将设备划分为子设备，以直接控制对特定计算单元的工作分配，保留设备的一部分用于高优先级/延迟敏感任务，或者有效地使用共享硬件资源，如缓存。\n*   **对象的单独编译和链接**:这提供了传统编译器的能力和灵活性，使得能够创建 OpenCL 程序库供其他程序链接。\n*   **增强的图像支持**:这个 **i** 包括对 1D 图像和 1D & 2D 图像阵列的支持。此外，OpenGL 共享扩展现在支持从 OpenGL 1D 纹理和 1D & 2D 纹理阵列创建 OpenCL 图像。\n\n*   **内置内核:**这代表了专用或不可编程硬件和相关固件(如视频编码器/解码器和数字信号处理器)的能力，使这些定制设备能够从 OpenCL 框架中驱动并与之紧密集成。\n*   **DX9 媒体表面共享**:这实现了 OpenCL 和 DirectX 9 或 DXVA 媒体表面之间的高效共享。\n*   **DX11 曲面共享**:用于 OpenCL 和 DirectX 11 曲面之间的无缝共享。\n\n# OpenCL 2.0 版\n\nOpenCL2.0 版本于 2013 年 11 月 18 日发布。此版本有以下重大更改或增加:\n\n*   **共享虚拟内存**:主机和设备内核可以直接共享复杂的、包含指针的数据结构，如树和链表，提供了显著的编程灵活性，并消除了主机和设备之间昂贵的数据传输。\n*   **动态并行**:设备内核可以在没有主机交互的情况下将内核入队到同一个设备，实现了灵活的工作调度范例，避免了在设备和主机之间传输执行控制和数据的需要，通常会显著卸载主机处理器瓶颈。\n*   **通用地址空间**:函数可以在不为参数指定命名地址空间的情况下编写，尤其是对于那些声明为指向类型的指针的参数非常有用，无需为应用中使用的每个命名地址空间编写多个函数。\n*   **图像**:改进的图像支持，包括 sRGB 图像和 3D 图像写入，内核读取和写入同一图像的能力，以及从 mip 映射或多采样 OpenGL 纹理创建 OpenCL 图像，以改进 OpenGL 互操作。\n*   **C11 原子**:C11 原子和同步操作的子集，使一个工作项中的分配对一个工作组中的其他工作项可见，跨在设备上执行的工作组可见，或者用于在 OpenCL 设备和主机之间共享数据。\n*   **Pipes** : Pipes 是存储组织为 FIFO 的数据的内存对象，OpenCL 2.0 为内核提供了从 Pipes 读取或向 Pipes 写入的内置函数，提供了可以由 OpenCL 实现者高度优化的 Pipes 数据结构的直接编程。\n*   **安卓可安装客户端驱动扩展**:支持 OpenCL 实现在安卓系统上作为共享对象被发现和加载。\n\n# OpenCL 2.1 版\n\nOpenCL 2.1 对 2.0 标准的修订版于 2015 年 11 月 16 日发布。这个版本最值得注意的是 OpenCL C++ 内核语言的引入，比如 OpenCL 语言最初是如何基于带有扩展的 C 的，C++ 版本是基于 C++ 14 的子集，向后兼容 C 内核语言。\n\nOpenCL 应用编程接口的更新包括以下内容:\n\n*   **子组**:这些实现了硬件线程的更精细的控制，现在在核心中，与附加的子组查询操作一起增加了灵活性\n*   **内核对象和状态的复制** : clCloneKernel 支持内核对象和状态的复制，以便在包装类中安全地实现复制构造函数\n*   **低延迟设备定时器查询**:这些允许在设备和主机代码之间对齐分析数据\n*   **运行时的中间 SPIR-伏代码**:\n    *   LLVM 到 SPIR-V 之间的双向翻译器，支持在工具链中灵活使用两种中间语言。\n    *   一个 OpenCL C 到 LLVM 编译器，通过上面的翻译器生成 SPIR-V。\n    *   一个 SPIR-V 汇编器和反汇编器。\n\n标准可移植中间表示(SPIR)及其后继版本 SPIR-V 是一种提供跨 OpenCL 设备使用的独立于设备的二进制文件的方法。\n\n# OpenCL 2.2 版\n\n2017 年 5 月 16 日，也就是现在的 OpenCL 版本发布了。根据 Khronos 集团的说法，它包括以下变化:\n\n*   OpenCL 2.2 将 OpenCL C++ 内核语言引入了核心规范，显著提高了并行编程的效率\n*   OpenCL C++ 内核语言是 C++ 14 标准的静态子集，包括类、模板、Lambda 表达式、函数重载以及许多其他用于泛型和元编程的构造\n*   利用新的 Khronos SPIR-V 1.1 中间语言，该语言完全支持 OpenCL C++ 内核语言\n*   OpenCL 库函数现在可以利用 C++ 语言，在访问诸如原子、迭代器、图像、采样器、管道和设备队列内置类型和地址空间等特性时，提供更高的安全性并减少未定义的行为\n*   管道存储是 OpenCL 2.2 中的一种新的设备端类型，通过在编译时使连接大小和类型已知，并在内核之间实现高效的设备范围通信，对 FPGA 实现非常有用\n*   OpenCL 2.2 还包括增强生成代码优化的特性:应用可以在 SPIR-V 编译时提供专门化常量的值，新的查询可以检测程序范围全局对象的非平凡构造函数和析构函数，用户回调可以在程序发布时设置\n*   在任何支持 OpenCL 2.0 的硬件上运行(仅需要驱动程序更新)\n\n# 建立开发环境\n\n不管你有哪种平台和 GPU，做 OpenCL 开发最重要的部分是从制造商那里获得一个人的 GPU 的 OpenCL 运行时。在这里，AMD、英特尔和 NVidia 都为所有主流平台提供了一个 SDK。对于 NVidia，OpenCL 支持包含在 CUDA SDK 中。\n\n除了图形处理器供应商的软件开发工具包，人们还可以在他们的网站上找到这个软件开发工具包支持哪些图形处理器的详细信息。\n\n# Linux 操作系统\n\n在使用提供的说明安装了供应商的 GPU SDK 之后，我们仍然需要下载 OpenCL 头。与供应商提供的共享库和运行时文件不同，这些头文件是通用的，将适用于任何 OpenCL 实现。\n\n对于基于 Debian 的发行版，只需执行以下命令行:\n\n```cpp\n    $ sudo apt-get install opencl-headers\n\n```\n\n对于其他发行版，包可能被称为相同的，或不同的东西。关于如何找到软件包名称，请查阅手册的发行版。\n\n在安装了 SDK 和 OpenCL 头之后，我们准备编译我们的第一个 OpenCL 应用。\n\n# Windows 操作系统\n\n在 Windows 上，我们可以选择用 Visual Studio (Visual C++)开发还是用 GCC (MinGW)的 Windows 端口开发。为了与 Linux 版本保持一致，我们将使用 MinGW 和 MSYS2。这意味着我们将拥有相同的编译器工具链、相同的 Bash shell 和实用程序，以及 Pacman 包管理器。\n\n如前所述，安装供应商的 GPU SDK 后，只需在 MSYS2 shell 中执行以下命令行，即可安装 OpenCL 头:\n\n```cpp\n    $ pacman -S mingw64/mingw-w64-x86_64-opencl-headers\n\n```\n\n或者，在使用 32 位版本的 MinGW 时，执行以下命令行:\n\n```cpp\n    mingw32/mingw-w64-i686-opencl-headers \n\n```\n\n有了这个，OpenCL 头就就位了。我们现在只需要确保 MinGW 链接器可以找到 OpenCL 库。使用 NVidia CUDA SDK，您可以为此使用`CUDA_PATH`环境变量，或者浏览 SDK 的安装位置，并将相应的 OpenCL LIB 文件从那里复制到 MinGW lib 文件夹，确保不要混合 32 位和 64 位文件。\n\n现在共享库也就位了，我们可以编译 OpenCL 应用。\n\n# X/MacOS\n\n从 OS X 10.7 开始，操作系统提供了一个 OpenCL 运行时。在为开发头和库安装了 XCode 之后，可以立即开始 OpenCL 开发。\n\n# 一个基本的 OpenCL 应用\n\n图形处理器应用的一个常见例子是计算快速傅立叶变换。这种算法通常用于音频处理等，允许您从时域转换到频域进行分析。\n\n它所做的是对数据集应用分治法，以便计算离散傅立叶变换。它通过将输入序列分成固定的、少量的更小的子序列，计算它们的离散傅立叶变换，并组合这些输出来组成最终的序列。\n\n这是相当先进的数学，但可以说，它之所以对 GPU 如此理想，是因为它是一种高度并行的算法，采用数据细分来加快离散傅立叶变换的计算，如下图所示:\n\n![](img/7a7e3beb-138c-46d8-895f-14a0f07a5dea.png)\n\n每个 OpenCL 应用至少由两部分组成:设置和配置 OpenCL 实例的 C++ 代码，以及实际的 OpenCL 代码，也称为内核，例如基于维基百科 FFT 演示示例的代码:\n\n```cpp\n// This kernel computes FFT of length 1024\\.  \n// The 1024 length FFT is decomposed into calls to a radix 16 function,  \n// another radix 16 function and then a radix 4 function\n __kernel void fft1D_1024 (__global float2 *in,  \n                     __global float2 *out,  \n                     __local float *sMemx,  \n                     __local float *sMemy) {\n          int tid = get_local_id(0);\n          int blockIdx = get_group_id(0) * 1024 + tid;\n          float2 data[16];\n\n          // starting index of data to/from global memory\n          in = in + blockIdx;  out = out + blockIdx;\n\n          globalLoads(data, in, 64); // coalesced global reads\n          fftRadix16Pass(data);      // in-place radix-16 pass\n          twiddleFactorMul(data, tid, 1024, 0);\n\n          // local shuffle using local memory\n          localShuffle(data, sMemx, sMemy, tid, (((tid & 15) * 65) + (tid >> 4)));\n          fftRadix16Pass(data);               // in-place radix-16 pass\n          twiddleFactorMul(data, tid, 64, 4); // twiddle factor multiplication\n\n          localShuffle(data, sMemx, sMemy, tid, (((tid >> 4) * 64) + (tid & 15)));\n\n          // four radix-4 function calls\n          fftRadix4Pass(data);      // radix-4 function number 1\n          fftRadix4Pass(data + 4);  // radix-4 function number 2\n          fftRadix4Pass(data + 8);  // radix-4 function number 3\n          fftRadix4Pass(data + 12); // radix-4 function number 4\n\n          // coalesced global writes\n    globalStores(data, out, 64);\n } \n\n```\n\n这个 OpenCL 内核表明，像 GLSL 着色器语言一样，OpenCL 的内核语言本质上是带有许多扩展的 C 语言。虽然可以使用 OpenCL C++ 内核语言，但这种语言仅在 OpenCL 2.1 (2015)之后才可用，因此，对它的支持和示例不如 C 内核语言常见。\n\n接下来是 C++ 应用，使用它，我们运行前面的 OpenCL 内核:\n\n```cpp\n#include <cstdio>\n #include <ctime>\n #include \"CL\\opencl.h\"\n\n #define NUM_ENTRIES 1024\n\n int main() { // (int argc, const char * argv[]) {\n    const char* KernelSource = \"fft1D_1024_kernel_src.cl\"; \n\n```\n\n正如我们在这里看到的，只有一个头，我们必须包括在内，以获得对 OpenCL 函数的访问。我们还指定了包含 OpenCL 内核源代码的文件名。由于每个 OpenCL 设备可能是不同的体系结构，因此当我们加载目标设备时，会为其编译内核:\n\n```cpp\n          const cl_uint num = 1;\n    clGetDeviceIDs(0, CL_DEVICE_TYPE_GPU, 0, 0, (cl_uint*) num); \n\n   cl_device_id devices[1];\n    clGetDeviceIDs(0, CL_DEVICE_TYPE_GPU, num, devices, 0);\n\n```\n\n接下来，我们必须获得一个我们可以使用的 OpenCL 设备列表，通过 GPU 对其进行过滤:\n\n```cpp\n    cl_context context = clCreateContextFromType(0, CL_DEVICE_TYPE_GPU,  \n                                                   0, 0, 0); \n\n```\n\n然后，我们使用找到的图形处理器设备创建一个 OpenCL `context`。上下文管理一系列设备上的资源:\n\n```cpp\n    clGetDeviceIDs(0, CL_DEVICE_TYPE_DEFAULT, 1, devices, 0);\n    cl_command_queue queue = clCreateCommandQueue(context, devices[0], 0, 0); \n\n```\n\n最后，我们将创建命令队列，其中包含要在 OpenCL 设备上执行的命令:\n\n```cpp\n    cl_mem memobjs[] = { clCreateBuffer(context, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, sizeof(float) * 2 * NUM_ENTRIES, 0, 0),              \n   clCreateBuffer(context, CL_MEM_READ_WRITE, sizeof(float) * 2 * NUM_ENTRIES, 0, 0) }; \n\n```\n\n为了与设备通信，我们需要分配缓冲区对象，这些对象将包含我们将复制到其内存中的数据。这里，我们将分配两个缓冲区，一个用于读取，一个用于写入:\n\n```cpp\n    cl_program program = clCreateProgramWithSource(context, 1, (const char **)& KernelSource, 0, 0); \n\n```\n\n我们现在已经获得了设备上的数据，但是仍然需要在设备上加载内核。为此，我们将使用前面介绍的 OpenCL 内核源代码，使用前面定义的文件名来创建一个内核:\n\n```cpp\n    clBuildProgram(program, 0, 0, 0, 0, 0); \n\n```\n\n接下来，我们将如下编译源代码:\n\n```cpp\n   cl_kernel kernel = clCreateKernel(program, \"fft1D_1024\", 0); \n\n```\n\n最后，我们将根据我们创建的二进制文件创建实际的内核:\n\n```cpp\n    size_t local_work_size[1] = { 256 };\n\n    clSetKernelArg(kernel, 0, sizeof(cl_mem), (void *) &memobjs[0]);\n    clSetKernelArg(kernel, 1, sizeof(cl_mem), (void *) &memobjs[1]);\n    clSetKernelArg(kernel, 2, sizeof(float) * (local_work_size[0] + 1) * 16, 0);\n    clSetKernelArg(kernel, 3, sizeof(float) * (local_work_size[0] + 1) * 16, 0); \n\n```\n\n为了将参数传递给我们的内核，我们必须在这里设置它们。在这里，我们将添加指向缓冲区和工作大小维度的指针，如下所示:\n\n```cpp\n    size_t global_work_size[1] = { 256 };\n          global_work_size[0] = NUM_ENTRIES;\n    local_work_size[0]  =  64;  // Nvidia: 192 or 256\n    clEnqueueNDRangeKernel(queue, kernel, 1, 0, global_work_size, local_work_size, 0, 0, 0); \n\n```\n\n现在我们可以设置工作项维度并执行内核。在这里，我们将使用内核执行方法来定义工作组的大小:\n\n```cpp\n          cl_mem C = clCreateBuffer(context, CL_MEM_WRITE_ONLY, (size), 0, &ret);\n                      cl_int ret = clEnqueueReadBuffer(queue, memobjs[1], CL_TRUE, 0, sizeof(float) * 2 * NUM_ENTRIES, C, 0, 0, 0); \n\n```\n\n在执行内核之后，我们希望读回结果信息。为此，我们告诉 OpenCL 将我们作为内核参数传递的已分配写缓冲区复制到新分配的缓冲区中。我们现在可以自由使用这个缓冲区中我们认为合适的数据。\n\n但是，在本例中，我们将不使用以下数据:\n\n```cpp\n    clReleaseMemObject(memobjs[0]);\n    clReleaseMemObject(memobjs[1]); \n   clReleaseCommandQueue(queue); \n   clReleaseKernel(kernel); \n   clReleaseProgram(program); \n   clReleaseContext(context); \n   free(C);\n } \n\n```\n\n最后，我们释放分配的资源并退出。\n\n# 图形处理器内存管理\n\n当使用一个中央处理器时，必须处理许多内存层次结构，以主内存(最慢)、中央处理器缓存(较快)和中央处理器寄存器(最快)的形式。图形处理器也是如此，因为一个人必须处理一个内存层次，这个层次会极大地影响应用的速度。\n\n在图形处理器上最快的也是寄存器(或私有)内存，我们比一般的中央处理器多一点。在此之后，我们获得本地内存，这是一个由多个处理元素共享的内存。GPU 本身最慢的是内存数据缓存，也叫纹理内存。这是卡上的内存，通常被称为视频随机存取存储器(VRAM)，使用高带宽但相对高延迟的内存，如 GDDR5。\n\n绝对最慢的是使用主机系统的内存(系统内存)，因为这必须通过 PCIe 总线和各种其他子系统来传输任何数据。相对于设备上的存储系统，主机设备之间的通信最好称为“冰川”。\n\n对于 AMD、Nvidia 和类似的专用 GPU 设备，内存架构可以这样可视化:\n\n![](img/3f41b888-e6a8-4259-8e60-9a2986be6a39.png)\n\n由于这种内存布局，建议在大块中传输任何数据，如果可能，使用异步传输。理想情况下，内核将在 GPU 内核上运行，并让数据流向它，以避免任何延迟。\n\n# 图形处理器和多线程\n\n将多线程代码与图形处理器相结合比试图管理运行在 MPI 集群上的并行应用要容易得多。这主要是由于以下工作流程:\n\n1.  准备数据:通过将数据发送到图形处理器的内存中，准备好我们要处理的数据，例如一大组图像或单个大图像。\n2.  准备内核:加载 OpenCL 内核文件，编译成 OpenCL 内核。\n3.  执行内核:将内核发送到 GPU，指示其开始处理数据。\n4.  读取数据:一旦我们知道处理已经完成，或者达到了一个特定的中间状态，我们将读取一个缓冲区，作为一个参数传递给 OpenCL 内核，以便获得我们的结果。\n\n由于这是一个异步进程，我们可以将它视为一个“一劳永逸”的操作，只需要一个线程来监控活动内核的进程。\n\n多线程和 GPU 应用方面的最大挑战不在于基于主机的应用，而在于 GPU 内核或运行在 GPU 上的着色器程序，因为它必须协调本地和远程处理单元之间的内存管理和处理，根据数据类型确定使用哪些内存系统，而不会在处理的其他地方造成问题。\n\n这是一个微妙的过程，涉及大量的试错、剖析和优化。一次内存拷贝优化或使用异步操作代替同步操作可能会将处理时间从几个小时缩短到几个小时。对内存系统的良好理解对于防止数据匮乏和类似问题至关重要。\n\n由于 GPU 通常用于加速持续时间较长的任务(几分钟到几小时或更长)，因此从多线程的角度来看，它可能最好被视为一个普通的工作线程，尽管有一些重要的复杂性，主要是延迟。\n\n# 潜伏\n\n正如我们在前面关于图形处理器内存管理的部分中提到的，最好首先使用最靠近图形处理器处理单元的内存，因为它们是最快的。这里的“最快”主要意味着它们的延迟更少，这意味着从内存请求信息和接收响应所需的时间。\n\n每个 GPU 的确切延迟会有所不同，但例如，对于英伟达的开普勒(特斯拉 K20)架构，可以预期延迟为:\n\n*   **全局**内存:450 周期。\n*   **常量**内存缓存:45–125 个周期。\n*   **本地** ( **共享**)内存:45 周期。\n\n这些测量都是在 CPU 本身。对于 PCIe 总线，一旦开始传输几兆字节的缓冲区，每次传输就需要几毫秒的时间。例如，用千兆字节大小的缓冲区填充图形处理器的内存可能需要相当长的时间。\n\n对于 PCIe 总线上的简单往返，可以测量以微秒为单位的延迟，对于运行在 1+ GHz 的 GPU 内核来说，这似乎是永恒的。这基本上定义了为什么主机和 GPU 之间的通信应该是绝对最小和高度优化的。\n\n# 潜在问题\n\nGPU 应用的一个常见错误是在处理完成之前读取结果缓冲区。在将缓冲区转移到设备并执行内核之后，必须插入同步点来通知主机它已经完成了处理。这些通常应该使用异步方法来实现。\n\n正如我们在延迟部分所述，重要的是要记住请求和响应之间潜在的巨大延迟，这取决于内存子系统或总线。如果做不到这一点，可能会导致奇怪的故障，冻结和崩溃，以及数据损坏和一个似乎永远等待的应用。\n\n对一个图形处理器应用进行分析是至关重要的，这样才能很好地了解图形处理器的利用率，以及处理流程是否接近最佳。\n\n# 调试图形处理器应用\n\nGPU 应用的最大挑战是调试内核。因为这个原因，CUDA 附带了一个模拟器，允许在 CPU 上运行和调试内核。OpenCL 允许在 CPU 上运行内核而无需修改，尽管这可能不会获得与在特定 GPU 设备上运行时完全相同的行为(和 bug)。\n\n一种稍微高级一点的方法是使用一个专用调试器，比如英伟达的 Nsight，它有两个版本，分别是 Visual Studio 版本([https://developer . Nvidia . com/Nvidia-Nsight-Visual Studio-edition](https://developer.nvidia.com/nvidia-nsight-visual-studio-edition))和 Eclipse 版本([https://developer.nvidia.com/nsight-eclipse-edition 版本](https://developer.nvidia.com/nsight-eclipse-edition))。\n\n根据 Nsight 网站上的营销简介:\n\nNVIDIA Nsight Visual Studio Edition brings GPU computing into Microsoft Visual Studio (including multiple instances of VS2017). This application development environment for GPUs allows you to build, debug, profile and trace heterogeneous compute, graphics, and virtual reality applications built with CUDA C/C++, OpenCL, DirectCompute, Direct3D, Vulkan API, OpenGL, OpenVR, and the Oculus SDK.\n\n以下屏幕截图显示了一个活动的 CUDA 调试会话:\n\n![](img/9a0125ea-440c-4542-8d5b-6237cc78fee7.png)\n\n这种调试器工具的一大优势是，它允许人们通过识别瓶颈和潜在问题来监控、分析和优化自己的 GPU 应用。\n\n# 摘要\n\n在这一章中，我们研究了如何以 OpenCL 的形式将 GPU 处理集成到 C++ 应用中。我们还研究了 GPU 内存层次结构，以及这如何影响性能，尤其是在主机设备通信方面。\n\n现在，您应该熟悉 GPU 的实现和概念，以及如何创建一个 OpenCL 应用，以及如何编译和运行它。如何避免常见错误也要知道。\n\n由于这是这本书的最后一章，希望所有的主要问题都已经得到回答，前面的几章以及这一章在某种程度上提供了信息和帮助。\n\n从这本书开始，读者可能会对更详细的主题感兴趣，这方面的许多资源在线上和线下都有。多线程和相关领域的主题非常广泛，涉及许多应用，从商业到科学、艺术和个人应用\n\n读者可能想建立一个自己的贝奥武夫集群，或者专注于图形处理器，或者两者结合。也许有一个复杂的应用，他们已经想写了一段时间，或者只是享受编程的乐趣。"
  },
  {
    "path": "docs/master-cpp-multithrd/README.md",
    "content": "# 精通 C++ 多线程\n\n> 原书：[Mastering C++ Multithreading](https://libgen.rs/book/index.php?md5=D8BD7CE4843A1A81E0B93B3CA07CBEC9)\n> \n> 协议：[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)\n> \n> 阶段：机翻（1）\n> \n> 自豪地采用[谷歌翻译](https://translate.google.cn/)\n> \n> 让开源界提前感受入关。\n\n* [在线阅读](https://ccpp.apachecn.org)\n* [在线阅读（Gitee）](https://apachecn.gitee.io/apachecn-c-cpp-zh/)\n* [ApacheCN 学习资源](http://docs.apachecn.org/)\n\n## 贡献指南\n\n本项目需要校对，欢迎大家提交 Pull Request。\n\n> 请您勇敢地去翻译和改进翻译。虽然我们追求卓越，但我们并不要求您做到十全十美，因此请不要担心因为翻译上犯错——在大部分情况下，我们的服务器已经记录所有的翻译，因此您不必担心会因为您的失误遭到无法挽回的破坏。（改编自维基百科）\n\n## 联系方式\n\n### 负责人\n\n* [飞龙](https://github.com/wizardforcel): 562826179\n\n### 其他\n\n*   在我们的 [apachecn/apachecn-c-cpp-zh](https://github.com/apachecn/apachecn-c-cpp-zh) github 上提 issue.\n*   发邮件到 Email: `apachecn@163.com`.\n*   在我们的 [组织学习交流群](http://www.apachecn.org/organization/348.html) 中联系群主/管理员即可.\n\n## 赞助我们\n\n![](http://data.apachecn.org/img/about/donate.jpg)\n"
  },
  {
    "path": "docs/master-cpp-multithrd/SUMMARY.md",
    "content": "+   [精通 C++ 多线程](README.md)\n+   [零、前言](00.md)\n+   [一、重温多线程](01.md)\n+   [二、处理器和操作系统上的多线程实现](02.md)\n+   [三、C++ 多线程应用编程接口](03.md)\n+   [四、线程同步和通信](04.md)\n+   [五、本机 C++ 线程和原语](05.md)\n+   [六、调试多线程代码](06.md)\n+   [七、最佳实践](07.md)\n+   [八、原子操作——使用硬件](08.md)\n+   [九、分布式计算中的多线程](09.md)\n+   [十、图形处理器多线程](10.md)\n"
  },
  {
    "path": "docs/master-cpp-prog/00.md",
    "content": "# 零、前言\n\nC++ 是一种有趣的编程语言，已经存在了将近 30 年。它用于使用第三方小部件框架开发复杂的桌面应用、web 应用、网络应用、设备驱动程序、内核模块、嵌入式应用和 GUI 应用；从字面上讲，C++ 可以用于任何领域。\n\n自从我 1993 年开始编程以来，我一直珍惜与我不时遇到的许多同事和行业专家进行的良好的旧技术讨论。在所有的技术讨论中，有一个话题得到了一次又一次的重复，那就是:*“你认为 C++ 是当今一种相关的编程语言吗？我应该继续研究 C++ 还是应该转向其他现代编程语言，如 Java、C#、Scala 或 Angular/Node.js？”*\n\n我一直觉得一个人应该对学习其他技术持开放态度，但这并不意味着必须放弃 C++。然而，好消息是，随着新的 C++ 17 特性的到位，C++ 已经重生，它将在未来的几十年里保持不变，这也是我写这本书的动机。\n\n人们一直觉得 Java 会接管 C++，但一直持续不变。当 C#进入这个行业时，同样的讨论又开始了，今天，当 Angular/Node.js 和 Scala 似乎对快速编程更有吸引力时，讨论又开始了。然而，C++ 有它自己的位置，到目前为止，没有任何编程语言能够取代 C++ 的位置。\n\n已经有很多 C++ 书籍可以帮助你理解这种语言，但是很少有书籍涉及用 C++ 开发 GUI 应用，用 C++ 开发 TDD，用 C++ 开发 BDD。\n\nC++ 已经走过了很长的路，现在已经在几个环境中被采用。它的主要优势是其软件基础设施和资源受限的应用。C++ 17 版本将改变开发人员编写代码的方式，本书将帮助您掌握使用 C++ 开发的技巧。\n\n通过解释每个概念的真实例子，本书将首先向您介绍 C++ 17 的最新特性。它将鼓励 C++ 中的整洁的代码实践，并展示 C++ 中的图形用户界面应用开发选项。您将深入了解如何使用智能指针来避免内存泄漏。接下来，您将学习多线程编程如何帮助您在应用中实现并发。\n\n接下来，您还将深入了解 C++ 标准模板库。我们将解释在您的 C++ 程序中实现 TDD 和 BDD 的概念，以及基于模板的泛型编程，以使您具备构建强大应用的专业知识。最后，我们将用调试技术和最佳实践来充实这本书。当你读完这本书的时候，你会对这门语言及其各个方面有一个深入的了解。\n\n# 这本书涵盖了什么\n\n[第 1 章](01.html#KVCC0-240c4d898e2d4108b0645aefc6c58389)、 *C++ 17 特性*，解释了 C++ 17 的新特性以及已经移除的特性。它还通过易于理解的示例演示了 C++ 17 的关键特性。\n\n[第 2 章](02.html#12AK80-240c4d898e2d4108b0645aefc6c58389)、*标准模板库*，给出了 STL 的概述，演示了各种容器和迭代器，并解释了如何在容器上应用有用的算法。本章还涉及所使用的内部数据结构及其运行时效率。\n\n[第三章](03.html#2BASE0-240c4d898e2d4108b0645aefc6c58389)、*模板编程*，概述了泛型编程及其好处。它演示了如何编写函数模板和类模板，以及如何重载函数模板。它还涉及到编写泛型类、显式类专门化和部分专门化。\n\n[第 4 章](00.html)、*智能指针*，解释了使用原始指针的问题，并激发了智能指针的使用。本章逐步向您介绍 auto_ptr、unique_ptr、shared_ptr 和 weak_ptr 的用法，并解释解决循环依赖问题的方法。\n\n[第 5 章](05.html#344B20-240c4d898e2d4108b0645aefc6c58389)、*用 C++ 开发 GUI 应用*，概述了 Qt，并为您提供了在 Linux 和 Windows 上安装 Qt 的分步说明。这一章逐步帮助您开发令人印象深刻的图形用户界面应用，具有有趣的小部件和各种布局。\n\n[第 6 章](06.html#3JCK20-240c4d898e2d4108b0645aefc6c58389)、*多线程编程和进程间通信*，介绍了 POSIX pthreads 库并讨论了原生 C++ 线程库。它还讨论了使用 C++ 线程库的好处。稍后，它将帮助您编写多线程应用，探索管理线程的方法，并解释同步机制的使用。本章讨论了死锁和可能的解决方案。在本章的最后，它向您介绍了并发库。\n\n[第 7 章](07.html#4MLOS0-240c4d898e2d4108b0645aefc6c58389)、*测试驱动开发*，对 TDD 进行了简要概述，澄清了 TDD 的常见问题。本章为您提供了安装谷歌测试框架并将其与 Linux 和 Windows 平台集成的分步说明。它帮助您使用易于理解的教程风格开发应用。\n\n[第 8 章](08.html#55U1S0-240c4d898e2d4108b0645aefc6c58389)、*行为驱动开发*，给出了 BDD 的概述，并指导您在 Linux 平台上完成黄瓜框架的安装、集成和配置。它还解释了小黄瓜，并帮助您编写 BDD 测试用例。\n\n[第 9 章](00.html)、*调试技术*，讨论了行业内为调试您的应用问题所遵循的各种策略和技术。稍后，它将帮助您了解如何使用 GDB 和 Valgrind 工具进行逐步调试、观察变量、修复各种与内存相关的问题，包括内存泄漏。\n\n[第 10 章](10.html#66BL00-240c4d898e2d4108b0645aefc6c58389)、*代码异味和整洁的代码实践*，讨论了各种代码异味和重构技术。\n\n# 这本书你需要什么\n\n在开始阅读本书之前，您需要配备以下工具:\n\n*   版本 5.4.0 20160609 或更高版本的 g++ 编译器\n*   GDB\n*   瓦尔格林德 3.11.0\n*   黄瓜-cpp Git 2.7.4\n*   谷歌测试框架(gtest 1.6 或更高版本)\n*   CMake 3.5.1\n*   Ruby 2.5.1\n*   Qt 5.7.0\n*   Bundler 诉 1.14.6\n\n所需的操作系统是 Ubuntu 16.04 64 位或更高版本。硬件配置至少应为 1 GB 内存和 20 GB 只读存储器。具有这种配置的虚拟机也应该足够了。\n\n# 这本书是给谁的\n\n这本书是给有经验的 C++ 开发人员看的。如果你是一个 C++ 开发新手，那么强烈建议你在阅读这本书之前先对 C++ 语言有一个扎实的了解。\n\n# 约定\n\n在这本书里，你会发现许多区分不同种类信息的文本样式。以下是这些风格的一些例子和对它们的意义的解释。文本中的码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、伪 URL、用户输入和 Twitter 句柄如下所示:“方法`initialize()`将`deque`迭代器`pos`初始化为存储在`deque`中的第一个数据元素。”\n\n代码块设置如下:\n\n```cpp\n#include <iostream>\n\nint main ( ) {\n\n        const int x = 5, y = 5;\n\n        static_assert ( 1 == 0, \"Assertion failed\" );\n        static_assert ( 1 == 0 );\n        static_assert ( x == y );\n\n        return 0;\n}\n```\n\n当我们希望将您的注意力吸引到代码块的特定部分时，相关的行或项目以粗体显示:\n\n```cpp\n#include <iostream>\n#include <thread>\n#include <mutex>\n#include \"Account.h\"\nusing namespace std;\n\nenum ThreadType {\n  DEPOSITOR,\n  WITHDRAWER\n};\n\nmutex locker;\n```\n\n任何命令行输入或输出都编写如下:\n\n```cpp\ng++ main.cpp -std=c++ 17\n./a.out\n```\n\n**新名词**和**重要词语**以粗体显示。您在屏幕上看到的单词(例如，在菜单或对话框中)会出现在文本中，如下所示:“您需要通过导航到“新建项目| Visual Studio | Windows | Win32 | Win32 控制台应用”来创建一个名为 MathApp 的新项目。”\n\nWarnings or important notes appear like this. Tips and tricks appear like this.\n\n# 读者反馈\n\n我们随时欢迎读者的反馈。让我们知道你对这本书的看法——你喜欢或不喜欢什么。读者反馈对我们来说很重要，因为它有助于我们开发出你真正能从中获益的标题。要给我们发送一般反馈，只需发送电子邮件`feedback@packtpub.com`，并在您的邮件主题中提及书名。如果您对某个主题有专业知识，并且对写作或投稿感兴趣，请参见我们位于[www.packtpub.com/authors](http://www.packtpub.com/authors)的作者指南。\n\n# 客户支持\n\n现在，您已经自豪地拥有了一本书，我们有许多东西可以帮助您从购买中获得最大收益。\n\n# 下载示例代码\n\n你可以从你在[http://www.packtpub.com](http://www.packtpub.com)的账户下载这本书的示例代码文件。如果您在其他地方购买了这本书，您可以访问[http://www.packtpub.com/support](http://www.packtpub.com/support)并注册将文件直接通过电子邮件发送给您。您可以按照以下步骤下载代码文件:\n\n1.  使用您的电子邮件地址和密码登录或注册我们的网站。\n2.  将鼠标指针悬停在顶部的“支持”选项卡上。\n3.  点击代码下载和勘误表。\n4.  在搜索框中输入图书的名称。\n5.  选择要下载代码文件的书籍。\n6.  从您购买这本书的下拉菜单中选择。\n7.  点击代码下载。\n\n下载文件后，请确保使用最新版本的解压缩文件夹:\n\n*   视窗系统的 WinRAR / 7-Zip\n*   zipeg/izp/un ARX for MAC\n*   适用于 Linux 的 7-Zip / PeaZip\n\n这本书的代码包也托管在 GitHub 上[https://GitHub . com/packt publishing/Mastering-Cpp-Programming](https://github.com/PacktPublishing/Mastering-Cpp-Programming)。我们还有来自丰富的图书和视频目录的其他代码包，可在[https://github.com/PacktPublishing/](https://github.com/PacktPublishing/)获得。看看他们！\n\n# 正误表\n\n尽管我们尽了最大努力来确保我们内容的准确性，但错误还是会发生。如果你在我们的某本书里发现一个错误，也许是文本或代码中的错误，如果你能向我们报告，我们将不胜感激。通过这样做，你可以让其他读者免受挫折，并帮助我们改进这本书的后续版本。如果您发现任何勘误表，请访问[http://www.packtpub.com/submit-errata](http://www.packtpub.com/submit-errata)，选择您的书籍，点击勘误表提交表格链接，并输入您的勘误表的详细信息。一旦您的勘误表得到验证，您的提交将被接受，勘误表将上传到我们的网站或添加到该标题勘误表部分下的任何现有勘误表列表中。要查看之前提交的勘误表，请前往[https://www.packtpub.com/books/content/support](https://www.packtpub.com/books/content/support)并在搜索栏中输入图书名称。所需信息将出现在勘误表部分。\n\n# 海盗行为\n\n在互联网上盗版受版权保护的材料是一个贯穿所有媒体的持续问题。在 Packt，我们非常重视版权和许可证的保护。如果您在互联网上遇到任何形式的我们作品的非法拷贝，请立即向我们提供位置地址或网站名称，以便我们寻求补救。请通过`copyright@packtpub.com`联系我们，获取疑似盗版资料的链接。我们感谢您在保护我们的作者方面的帮助，以及我们为您带来有价值内容的能力。\n\n# 问题\n\n如果您对本书的任何方面有问题，可以在`questions@packtpub.com`联系我们，我们将尽最大努力解决问题。"
  },
  {
    "path": "docs/master-cpp-prog/01.md",
    "content": "# 一、C++ 17 特性\n\n在本章中，您将学习以下概念:\n\n*   C++ 17 背景\n*   C++ 17 有什么新功能？\n*   C++ 17 中有哪些功能被弃用或删除了？\n*   C++ 17 中的关键特性\n\n# C++ 17 背景\n\n众所周知，C++ 语言是比雅尼·斯特劳斯特鲁普的大脑产物，他在 1979 年开发了 C++。C++ 编程语言由国际标准化组织(ISO)标准化。\n\n最初的标准化发布于 1998 年，通常称为 C++ 98，下一个标准化 C++ 03 发布于 2003 年，它主要是一个 bug 修复版本，只有一个用于值初始化的语言特性。2011 年 8 月，C++ 11 标准发布，对核心语言做了几处补充，包括对**标准模板库** ( **STL** )做了几处有意义的改动；C++ 11 基本取代了 C++ 03 标准。C++ 14 于 2014 年 12 月发布，新增了一些功能，后来 C++ 17 标准于 2017 年 7 月 31 日发布。\n\n在撰写本书时，C++ 17 是针对 C++ 编程语言的 ISO/IEC 标准的最新修订版。\n\n本章要求编译器支持 C++ 17 特性:gcc 版本 7 或更高版本。由于 gcc 版本 7 是撰写本书时的最新版本，所以我将在本章中使用 gcc 版本 7.1.0。\n\nIn case you haven't installed g++ 7 that supports C++ 17 features, you can install it with the following commands:\n`sudo add-apt-repository ppa:jonathonf/gcc-7.1\nsudo apt-get update\nsudo apt-get install gcc-7 g++-7`\n\n# C++ 17 有什么新功能？\n\nC++ 17 特性的完整列表可以在[http://en . CP preference . com/w/CPP/compiler _ support # C . 2b . 2b 17 _ 特性](http://en.cppreference.com/w/cpp/compiler_support#C.2B.2B17_features)找到。\n\n为了给出一个高层次的想法，下面是一些新的 C++ 17 特性:\n\n*   直接列表初始化的新自动规则\n*   `static_assert`无消息\n*   嵌套命名空间定义\n*   内联变量\n*   命名空间和枚举器的属性\n*   C++ 异常规范是类型系统的一部分\n*   改进的 lambda 功能为服务器带来了性能优势\n*   NUMA 建筑\n*   使用属性命名空间\n*   过度对齐数据的动态内存分配\n*   类模板的模板参数推导\n*   自动类型的非类型模板参数\n*   保证复制省略\n*   继承构造函数的新规范\n*   枚举的直接列表初始化\n*   更严格的表达式求值顺序\n*   `shared_mutex`\n*   字符串转换\n\n除此之外，还有许多有趣的新特性被添加到核心 C++ 语言中:STL、lambadas 等等。新功能让 C++ 焕然一新，从`C++ 17`开始，作为一名 C++ 开发人员，你会觉得自己在用一种现代编程语言工作，比如 Java 或者 C#。\n\n# C++ 17 中有哪些功能被弃用或删除了？\n\n现在，C++ 17 中删除了以下功能:\n\n*   `register`关键字在 C++ 11 中被弃用，在 C++ 17 中被删除\n*   `bool`的`++ `运算符在 C++ 98 中被弃用，在 C++ 17 中被移除\n*   动态异常规范在 C++ 11 中被弃用，在 C++ 17 中被删除\n\n# C++ 17 中的关键特性\n\n让我们在以下几节中逐一探讨以下 C++ 17 关键特性:\n\n*   更简单的嵌套命名空间\n*   从支撑初始化列表中检测类型的新规则\n*   简化`static_assert`\n*   `std::invoke`\n*   结构化绑定\n*   `if`和`switch`局部变量\n*   类模板的模板类型自动检测\n*   内联变量\n\n# 更简单的嵌套命名空间语法\n\n直到 C++ 14 标准，C++ 中嵌套命名空间支持的语法如下:\n\n```cpp\n#include <iostream>\nusing namespace std;\n\nnamespace org {\n    namespace tektutor {\n        namespace application {\n             namespace internals {\n                  int x;\n             }\n        }\n    }\n}\n\nint main ( ) {\n    org::tektutor::application::internals::x = 100;\n    cout << \"\\nValue of x is \" << org::tektutor::application::internals::x << endl;\n\n    return 0;\n}\n```\n\n可以使用以下命令编译前面的代码并查看输出:\n\n```cpp\ng++-7 main.cpp -std=c++ 17\n./a.out\n```\n\n前面程序的输出如下:\n\n```cpp\nValue of x is 100\n```\n\n每个命名空间级别都以花括号开始和结束，这使得在大型应用中很难使用嵌套的命名空间。C++ 17 嵌套命名空间语法真的很酷；只要看看下面的代码，你就会很容易同意我的观点:\n\n```cpp\n#include <iostream>\nusing namespace std;\n\nnamespace org::tektutor::application::internals {\n    int x;\n}\n\nint main ( ) {\n    org::tektutor::application::internals::x = 100;\n    cout << \"\\nValue of x is \" << org::tektutor::application::internals::x << endl;\n\n    return 0;\n}\n```\n\n可以使用以下命令编译前面的代码并查看输出:\n\n```cpp\ng++-7 main.cpp -std=c++ 17\n./a.out\n```\n\n输出与前一个程序相同:\n\n```cpp\nValue of x is 100\n```\n\n# 从支撑初始化列表中自动检测类型的新规则\n\nC++ 17 引入了初始化列表自动检测的新规则，补充了 C++ 14 的规则。C++ 17 规则坚持认为，如果声明了`std::initializer_list`的显式或部分专门化，则程序是不良的:\n\n```cpp\n#include <iostream>\nusing namespace std;\n\ntemplate <typename T1, typename T2>\nclass MyClass {\n     private:\n          T1 t1;\n          T2 t2;\n     public:\n          MyClass( T1 t1 = T1(), T2 t2 = T2() ) { }\n\n          void printSizeOfDataTypes() {\n               cout << \"\\nSize of t1 is \" << sizeof ( t1 ) << \" bytes.\" << endl;\n               cout << \"\\nSize of t2 is \" << sizeof ( t2 ) << \" bytes.\" << endl;\n     }\n};\n\nint main ( ) {\n\n    //Until C++ 14\n    MyClass<int, double> obj1;\n    obj1.printSizeOfDataTypes( );\n\n    //New syntax in C++ 17\n    MyClass obj2( 1, 10.56 );\n\n    return 0;\n}\n```\n\n可以使用以下命令编译前面的代码并查看输出:\n\n```cpp\ng++-7 main.cpp -std=c++ 17\n./a.out\n```\n\n前面程序的输出如下:\n\n```cpp\nValues in integer vectors are ...\n1 2 3 4 5 \n\nValues in double vectors are ...\n1.5 2.5 3.5 \n```\n\n# 简化的静态断言\n\n`static_assert`宏有助于在编译时识别断言失败。从 C++ 11 开始就支持这个特性；然而，`static_assert`宏过去接受强制断言失败消息，现在在 C++ 17 中变成可选的。\n\n以下示例演示了带有和不带有消息的`static_assert`的使用:\n\n```cpp\n#include <iostream>\n#include <type_traits>\nusing namespace std;\n\nint main ( ) {\n\n        const int x = 5, y = 5;\n\n        static_assert ( 1 == 0, \"Assertion failed\" );\n        static_assert ( 1 == 0 );\n        static_assert ( x == y );\n\n        return 0;\n}\n```\n\n前面程序的输出如下:\n\n```cpp\ng++-7 staticassert.cpp -std=c++ 17\nstaticassert.cpp: In function ‘int main()’:\nstaticassert.cpp:7:2: error: static assertion failed: Assertion failed\n static_assert ( 1 == 0, \"Assertion failed\" );\n\nstaticassert.cpp:8:2: error: static assertion failed\n static_assert ( 1 == 0 );\n```\n\n从前面的输出中，您可以看到消息`Assertion failed`作为编译错误的一部分出现，而在第二次编译中，默认的编译器错误消息出现，因为我们没有提供断言失败消息。当没有断言失败时，断言错误消息不会出现，如`static_assert ( x == y )`所示。这个特性的灵感来自于 BOOST C++ 库中的 C++ 社区。\n\n# 标准::invoke()方法\n\n`std::invoke()`方法可以用相同的语法调用函数、函数指针和成员指针:\n\n```cpp\n#include <iostream>\n#include <functional>\nusing namespace std;\n\nvoid globalFunction( ) {\n     cout << \"globalFunction ...\" << endl;\n}\n\nclass MyClass {\n    public:\n        void memberFunction ( int data ) {\n             std::cout << \"\\nMyClass memberFunction ...\" << std::endl;\n        }\n\n        static void staticFunction ( int data ) {\n             std::cout << \"MyClass staticFunction ...\" << std::endl;\n        }\n};\n\nint main ( ) {\n\n    MyClass obj;\n\n    std::invoke ( &MyClass::memberFunction, obj, 100 );\n    std::invoke ( &MyClass::staticFunction, 200 );\n    std::invoke ( globalFunction );\n\n    return 0;\n}\n```\n\n可以使用以下命令编译前面的代码并查看输出:\n\n```cpp\ng++-7 main.cpp -std=c++ 17\n./a.out\n```\n\n前面程序的输出如下:\n\n```cpp\nMyClass memberFunction ...\nMyClass staticFunction ...\nglobalFunction ...\n```\n\n`std::invoke( )`方法是一个模板函数，帮助您无缝调用内置和用户定义的可调用对象。\n\n# 结构化绑定\n\n现在，您可以用非常酷的语法用返回值初始化多个变量，如下面的代码示例所示:\n\n```cpp\n#include <iostream>\n#include <tuple>\nusing namespace std;\n\nint main ( ) {\n\n    tuple<string,int> student(\"Sriram\", 10);\n    auto [name, age] = student;\n\n    cout << \"\\nName of the student is \" << name << endl;\n    cout << \"Age of the student is \" << age << endl;\n\n    return 0;\n}\n```\n\n在前面的程序中，**粗体**突出显示的代码是 C++ 17 中引入的结构化绑定特性。有趣的是，我们没有声明`string name`和`int age`变量。这些都是由 C++ 编译器自动推导出来的`string`和`int`，这使得 C++ 的语法就像任何现代编程语言一样，不会失去其性能和系统编程的好处。\n\n可以使用以下命令编译前面的代码并查看输出:\n\n```cpp\ng++-7 main.cpp -std=c++ 17\n./a.out\n```\n\n前面程序的输出如下:\n\n```cpp\nName of the student is Sriram\nAge of the student is 10\n```\n\n# If 和 Switch 局部作用域变量\n\n有一个有趣的新特性，允许你声明一个局部变量绑定到`if`和`switch`语句的代码块。在`if`和`switch`语句中使用的变量的范围将超出各自块的范围。通过一个简单易懂的例子可以更好地理解，如下所示:\n\n```cpp\n#include <iostream>\nusing namespace std;\n\nbool isGoodToProceed( ) {\n    return true;\n}\n\nbool isGood( ) {\n     return true;\n}\n\nvoid functionWithSwitchStatement( ) {\n\n     switch ( auto status = isGood( ) ) {\n          case true:\n                 cout << \"\\nAll good!\" << endl;\n          break;\n\n          case false:\n                 cout << \"\\nSomething gone bad\" << endl;\n          break;\n     } \n\n}\n\nint main ( ) {\n\n    if ( auto flag = isGoodToProceed( ) ) {\n         cout << \"flag is a local variable and it loses its scope outside the if block\" << endl;\n    }\n\n     functionWithSwitchStatement();\n\n     return 0;\n}\n```\n\n可以使用以下命令编译前面的代码并查看输出:\n\n```cpp\ng++-7 main.cpp -std=c++ 17\n./a.out\n```\n\n前面程序的输出如下:\n\n```cpp\nflag is a local variable and it loses its scope outside the if block\nAll good!\n```\n\n# 类别模板的模板类型自动扣减\n\n我相信您会喜欢您即将在示例代码中看到的内容。虽然模板很有用，但是很多人不喜欢它，因为它的语法很难理解，也很奇怪。但是你不用再担心了；看看下面的代码片段:\n\n```cpp\n#include <iostream>\nusing namespace std;\n\ntemplate <typename T1, typename T2>\nclass MyClass {\n     private:\n          T1 t1;\n          T2 t2;\n     public:\n          MyClass( T1 t1 = T1(), T2 t2 = T2() ) { }\n\n          void printSizeOfDataTypes() {\n               cout << \"\\nSize of t1 is \" << sizeof ( t1 ) << \" bytes.\" << endl;\n               cout << \"\\nSize of t2 is \" << sizeof ( t2 ) << \" bytes.\" << endl;\n     }\n};\n\nint main ( ) {\n\n    //Until C++ 14\n    MyClass<int, double> obj1;\n    obj1.printSizeOfDataTypes( );\n\n    //New syntax in C++ 17\n    MyClass obj2( 1, 10.56 );\n\n    return 0;\n}\n```\n\n可以使用以下命令编译前面的代码并查看输出:\n\n```cpp\ng++-7 main.cpp -std=c++ 17\n./a.out\n```\n\n程序的输出如下:\n\n```cpp\nSize of t1 is 4 bytes.\nSize of t2 is 8 bytes.\n```\n\n# 内联变量\n\n就像 C++ 中的内联函数一样，现在可以使用内联变量定义。这对于初始化静态变量非常有用，如下面的示例代码所示:\n\n```cpp\n#include <iostream>\nusing namespace std;\n\nclass MyClass {\n    private:\n        static inline int count = 0;\n    public:\n        MyClass() { \n              ++ count;\n        }\n\n    public:\n         void printCount( ) {\n              cout << \"\\nCount value is \" << count << endl;\n         } \n};\n\nint main ( ) {\n\n    MyClass obj;\n\n    obj.printCount( ) ;\n\n    return 0;\n}\n```\n\n可以使用以下命令编译前面的代码并查看输出:\n\n```cpp\ng++-7 main.cpp -std=c++ 17\n./a.out\n```\n\n前面代码的输出如下:\n\n```cpp\nCount value is 1\n```\n\n# 摘要\n\n在本章中，您将了解 C++ 17 中引入的有趣的新特性。您学习了超级简单的 C++ 17 嵌套命名空间语法。您还学习了使用支撑初始化列表的数据类型检测以及 C++ 17 标准中的新规则。\n\n您还注意到`static_assert`可以在不断言失败消息的情况下完成。此外，使用`std::invoke()`，您现在可以调用全局函数、函数指针、成员函数和静态类成员函数。而且，使用结构化绑定，您现在可以用一个返回值初始化多个变量。\n\n您还了解到`if`和`switch`语句可以在`if`条件和`switch`语句之前有一个局部变量。您已经学习了类模板的自动类型检测。最后，你使用了`inline`变量。\n\n还有更多的 C++ 17 特性，但本章试图涵盖大多数开发人员可能需要的最有用的特性。在下一章中，您将学习标准模板库。"
  },
  {
    "path": "docs/master-cpp-prog/02.md",
    "content": "# 二、标准模板库\n\n本章将涵盖以下主题:\n\n*   STL 概述\n*   STL 体系结构\n    *   容器\n    *   迭代程序\n    *   算法\n    *   函子\n*   STL 容器\n    *   顺序\n    *   联合的\n    *   无序的\n    *   转接器，适配器；改编者\n\n让我们在下面的章节中一个接一个地研究 STL 主题。\n\n# 标准模板库体系结构\n\nC++ **标准模板库** ( **STL** )提供了现成的通用容器、可应用于容器的算法以及导航容器的迭代器。STL 是用 C++ 模板实现的，模板允许用 C++ 进行泛型编程。\n\nSTL 通过将开发人员从编写低级数据结构和算法中解放出来，鼓励 C++ 开发人员专注于手头的任务。STL 是一个经过时间考验的库，允许快速应用开发。\n\nSTL 是一个有趣的作品和架构。它的秘密公式是编译时多态性。为了获得更好的性能，STL 避免了动态多态，告别了虚函数。总的来说，补充交易日志有以下四个组成部分:\n\n*   算法\n*   函子\n*   迭代程序\n*   容器\n\nSTL 架构将上述四个组件缝合在一起。它有许多常用的算法，并有性能保证。STL 算法的有趣之处在于，它们可以无缝工作，而不需要任何关于保存数据的容器的知识。这之所以成为可能，是因为迭代器提供了高级遍历 API，它完全抽象了容器中使用的底层数据结构。STL 非常广泛地使用了运算符重载。让我们逐一了解 STL 的主要组成部分，从概念上很好地掌握 STL。\n\n# 算法\n\nSTL 算法是由 C++ 模板驱动的；因此，无论处理什么类型的数据，或者独立于容器如何组织数据，相同的算法都可以工作。有趣的是，STL 算法足够通用，可以使用模板支持内置和用户定义的数据类型。事实上，算法通过迭代器与容器交互。因此，对算法来说重要的是容器支持的迭代器。话虽如此，算法的性能取决于容器中使用的底层数据结构。因此，某些算法只在选择性容器上工作，因为 STL 支持的每个算法都需要某种类型的迭代器。\n\n# 迭代程序\n\n迭代器是一种设计模式，但有趣的是，STL 的工作在“四人帮”向软件社区发布他们的设计模式相关工作之前就已经开始了。迭代器本身是允许遍历容器来访问、修改和操作存储在容器中的数据的对象。迭代器如此神奇地做到这一点，以至于我们意识不到或者不需要知道数据是如何存储和检索的。\n\n下图直观地表示了一个迭代器:\n\n![](img/00005.jpeg)\n\n从上图可以理解，每个迭代器都支持`begin()` API，它返回第一个元素位置，`end()` API 返回容器中最后一个元素之后的一个位置。\n\nSTL 广泛支持以下五种类型的迭代器:\n\n*   输入迭代器\n*   输出迭代器\n*   向前迭代器\n*   双向迭代器\n*   随机存取迭代器\n\n容器实现了迭代器，让我们可以轻松地检索和操作数据，而无需深入研究容器的技术细节。\n\n下表解释了五个迭代器中的每一个:\n\n| **迭代器的类型** | **描述** |\n| 输入迭代器 | \n\n*   Used to point to elements from\n*   Read in, which is valid for single navigation. Once the end of the container is reached, the iterator will fail.\n*   Support before and after increment operators.\n*   Decreasing operator is not supported\n*   Support dereference\n*   Support `==` `!=` operator and other iterators.\n*   Compare the `istream_iterator` iterator to the input iterator.\n\n |\n| 输出迭代器 | \n\n*   Used to modify pointing elements.\n*   Valid for single navigation. Once the end of the container is reached, the iterator will fail.\n*   Support before and after increment operator\n*   Decreasing operator is not supported\n*   Support dereference\n*   The `==` and `!=` operators are not supported.\n*   `ostream_iterator` `back_inserter` `front_inserter` iterator\n\n |\n| 向前迭代器 | \n\n*   Support functions of input iterator and output iterator.\n*   Support multi-pass navigation\n*   Pre-increment and post-increment operators are supported\n*   Support dereference\n*   `forward_list` The container supports forward iterators.\n\n |\n| 双向迭代器 | \n\n*   Is a forward iterator that supports bidirectional navigation.\n*   Allow multiple navigation\n*   The operator before increment and after increment is supported.\n*   Supports the operator of pre-subtraction and post-subtraction\n*   Support dereference\n*   Support `[]` operator\n*   `list` `set` `map` `multiset` `multimap` The container supports the bidirectional iterator 【 T20\n\n |\n| 随机存取迭代器 | \n\n*   Elements can be accessed using any offset position.\n*   It supports pre-increment and post-increment operators.\n*   It supports pre-decrement and post-decrement operators\n*   It supports dereference.\n*   It is the most complete iterator because it supports all the functions of other iterators listed earlier.\n*   `array` `vector` and `deque` containers support random access iterators.\n*   A support\n\n |\n\n# 容器\n\nSTL 容器是通常动态增长和收缩的对象。容器使用复杂的数据结构将数据存储在引擎盖下，并提供高级功能来访问数据，而无需我们深入研究数据结构的复杂内部实现细节。STL 容器非常高效，并且经过时间考验。\n\n每个容器都使用不同类型的数据结构来高效地存储、组织和操作数据。尽管许多容器看起来相似，但它们在引擎盖下的行为却不同。因此，容器的错误选择会导致应用性能问题和不必要的复杂性。\n\n容器有以下几种口味:\n\n*   连续的\n*   联合的\n*   容器适配器\n\n存储在容器中的对象被复制或移动，而不是被引用。在接下来的章节中，我们将用简单而有趣的例子来探索每一种类型的容器。\n\n# 函子\n\n函子是行为类似常规函数的对象。好处是函子可以代替函数指针。函子是方便的对象，允许您扩展或补充 STL 函数的行为，而不会损害面向对象的编码原则。\n\n函子易于实现；您所需要做的就是重载函数运算符。函子也被称为函子。\n\n下面的代码将演示如何实现一个简单的函子:\n\n```cpp\n#include <iostream>\n#include <vector>\n#include <iterator>\n#include <algorithm>\nusing namespace std;\n\ntemplate <typename T>\nclass Printer {\npublic:\n  void operator() ( const T& element ) {\n    cout << element << \"\\t\";\n  }\n};\n\nint main () {\n  vector<int> v = { 10, 20, 30, 40, 50 };\n\n  cout << \"\\nPrint the vector entries using Functor\" << endl;\n\n  for_each ( v.begin(), v.end(), Printer<int>() );\n\n  cout << endl;\n\n  return 0;\n}\n```\n\n让我们使用以下命令快速编译程序:\n\n```cpp\ng++ main.cpp -std=c++ 17\n./a.out\n```\n\n让我们检查程序的输出:\n\n```cpp\nPrint the vector entries using Functor\n10  20  30  40  50\n```\n\n我们希望你意识到一个函子是多么容易和酷。\n\n# 序列容器\n\nSTL 支持各种有趣的序列容器。序列容器以线性方式存储同类数据类型，可以按顺序访问。STL 支持以下序列容器:\n\n*   数组\n*   向量\n*   列表\n*   `forward_list`\n*   双端队列\n\n由于存储在 STL 容器中的对象只不过是值的副本，因此 STL 期望用户定义的数据类型满足某些基本要求，以便将这些对象保存在容器中。存储在 STL 容器中的每个对象必须至少满足以下要求:\n\n*   默认构造函数\n*   复制构造函数\n*   赋值运算符\n\n让我们在下面的小节中一个接一个地探索序列容器。\n\n# 排列\n\nSTL 数组容器是一个固定大小的序列容器，就像 C/C++ 内置数组一样，只是 STL 数组是大小感知的，比内置的 C/C++ 数组聪明一点。让我们用一个例子来理解一个 STL 数组:\n\n```cpp\n#include <iostream>\n#include <array>\nusing namespace std;\nint main () {\n  array<int,5> a = { 1, 5, 2, 4, 3 };\n\n  cout << \"\\nSize of array is \" << a.size() << endl;\n\n  auto pos = a.begin();\n\n  cout << endl;\n  while ( pos != a.end() ) \n    cout << *pos++ << \"\\t\";\n  cout << endl;\n\n  return 0;\n}\n```\n\n可以使用以下命令编译前面的代码并查看输出:\n\n```cpp\ng++ main.cpp -std=c++ 17\n./a.out \n```\n\n程序的输出如下:\n\n```cpp\nSize of array is 5\n1     5     2     4     3\n```\n\n# 代码走查\n\n下面一行声明一个固定大小的数组(`5`)并用五个元素初始化该数组:\n\n```cpp\narray<int,5> a = { 1, 5, 2, 4, 3 };\n```\n\n提到的大小一旦声明就不能更改，就像 C/C++ 内置数组一样。`array::size()`方法返回数组的大小，不管初始化列表中初始化了多少个整数。`auto pos = a.begin()`方法声明一个`array<int,5>`的迭代器，并指定数组的起始位置。`array::end()`方法指向数组中最后一个元素之后的一个位置。迭代器的行为类似于或模仿 C++ 指针，对迭代器取消引用会返回迭代器指向的值。迭代器位置可以分别通过`++ pos`和`--pos`前后移动。\n\n# 数组中常用的 API\n\n下表显示了一些常用的阵列 API:\n\n| API | **描述** |\n| `at( int index )` | 这将返回存储在索引引用位置的值。该索引是从零开始的索引。如果索引在数组的索引范围之外，这个应用编程接口将抛出`std::out_of_range`异常。 |\n| `operator [ int index ]` | 这是一个不安全的方法，因为如果索引超出数组的有效范围，它不会引发任何异常。这往往比`at`稍快，因为这个 API 不执行边界检查。 |\n| `front()` | 这将返回数组中的第一个元素。 |\n| `back()` | 这将返回数组中的最后一个元素。 |\n| `begin()` | 这将返回数组中第一个元素的位置 |\n| `end()` | 这将返回数组中最后一个元素之后的一个位置 |\n| `rbegin()` | 这将返回相反的开始位置，即返回数组中最后一个元素的位置 |\n| `rend()` | 这将返回反向结束位置，也就是说，它将返回数组中第一个元素之前的一个位置 |\n| `size()` | 这将返回数组的大小 |\n\n数组容器支持随机访问；因此，给定一个索引，数组容器可以获取一个运行时复杂度为*0(1)*或恒定时间的值。\n\n可以使用反向迭代器以反向方式访问数组容器元素:\n\n```cpp\n#include <iostream>\n#include <array>\nusing namespace std;\n\nint main () {\n\n    array<int, 6> a;\n    int size = a.size();\n    for (int index=0; index < size; ++ index)\n         a[index] = (index+1) * 100;   \n\n    cout << \"\\nPrint values in original order ...\" << endl;\n\n    auto pos = a.begin();\n    while ( pos != a.end() )\n        cout << *pos++ << \"\\t\";\n    cout << endl;\n\n    cout << \"\\nPrint values in reverse order ...\" << endl;\n\n    auto rpos = a.rbegin();\n    while ( rpos != a.rend() )\n    cout << *rpos++ << \"\\t\";\n    cout << endl;\n\n    return 0;\n}\n```\n\n我们将使用以下命令获得输出:\n\n```cpp\n./a.out\n```\n\n输出如下:\n\n```cpp\nPrint values in original order ...\n100   200   300   400   500   600\n\nPrint values in reverse order ...\n600   500   400   300   200   100\n```\n\n# 矢量\n\nVector 是一个非常有用的序列容器，它完全像一个数组一样工作，只是当数组的大小固定时，vector 可以在运行时增长和收缩。然而，在数组和向量中使用的数据结构是一个简单的内置 C/C++ 风格的数组。\n\n让我们看下面的例子来更好地理解向量:\n\n```cpp\n#include <iostream>\n#include <vector>\n#include <algorithm>\nusing namespace std;\n\nint main () {\n  vector<int> v = { 1, 5, 2, 4, 3 };\n\n  cout << \"\\nSize of vector is \" << v.size() << endl;\n\n  auto pos = v.begin();\n\n  cout << \"\\nPrint vector elements before sorting\" << endl;\n  while ( pos != v.end() )\n    cout << *pos++ << \"\\t\";\n  cout << endl;\n\n  sort( v.begin(), v.end() );\n\n  pos = v.begin();\n\n  cout << \"\\nPrint vector elements after sorting\" << endl;\n\n  while ( pos != v.end() )\n    cout << *pos++ << \"\\t\";\n  cout << endl;\n\n  return 0;\n}\n```\n\n可以使用以下命令编译前面的代码并查看输出:\n\n```cpp\ng++ main.cpp -std=c++ 17\n./a.out\n```\n\n程序的输出如下:\n\n```cpp\nSize of vector is 5\n\nPrint vector elements before sorting\n1     5     2     4     3\n\nPrint vector elements after sorting\n1     2     3     4     5\n```\n\n# 代码走查\n\n下面一行声明一个向量，并用五个元素初始化该向量:\n\n```cpp\nvector<int> v = { 1, 5, 2, 4, 3 };\n```\n\n然而，向量也允许通过使用`vector::push_back<data_type>( value )`应用编程接口将值附加到向量的末尾。`sort()`算法采用两个随机访问迭代器，表示必须排序的数据范围。由于向量内部使用了内置的 C/C++ 数组，就像 STL 数组容器一样，向量也支持随机访问迭代器；因此`sort()`函数是一种高效算法，其运行时复杂度是对数的，即 *O(N log2 (N))* 。\n\n# 常用载体原料药\n\n下表显示了一些常用的矢量应用编程接口:\n\n| API | **描述** |\n| `at ( int index )` | 这将返回存储在索引位置的值。如果索引无效，则抛出`std::out_of_range`异常。 |\n| `operator [ int index ]` | 这将返回存储在索引位置的值。它比`at( int index )`更快，因为该函数不执行边界检查。 |\n| `front()` | 这将返回存储在向量中的第一个值。 |\n| `back()` | 这将返回存储在向量中的最后一个值。 |\n| `empty()` | 如果向量为空，则返回 true，否则返回 false。 |\n| `size()` | 这将返回存储在向量中的值的数量。 |\n| `reserve( int size )` | 这保留了向量的初始大小。当向量大小达到其容量时，尝试插入新值需要调整向量大小。这使得插入消耗 *O(N)* 运行时的复杂性。`reserve()`方法是上述问题的一种解决方法。 |\n| `capacity()` | 这将返回向量的总容量，而大小是存储在向量中的实际值。 |\n| `clear()` | 这将清除所有值。 |\n| `push_back<data_type>( value )` | 这将在向量的末尾添加一个新值。 |\n\n使用`istream_iterator`和`ostream_iterator`从矢量中读取和打印将会非常有趣和方便。下面的代码演示了向量的使用:\n\n```cpp\n#include <iostream>\n#include <vector>\n#include <algorithm>\n#include <iterator>\nusing namespace std;\n\nint main () {\n    vector<int> v;\n\n    cout << \"\\nType empty string to end the input once you are done feeding the vector\" << endl;\n    cout << \"\\nEnter some numbers to feed the vector ...\" << endl;\n\n    istream_iterator<int> start_input(cin);\n    istream_iterator<int> end_input;\n\n    copy ( start_input, end_input, back_inserter( v ) );\n\n    cout << \"\\nPrint the vector ...\" << endl;\n    copy ( v.begin(), v.end(), ostream_iterator<int>(cout, \"\\t\") );\n    cout << endl;\n\n    return 0;\n}\n```\n\nNote that the output of the program is skipped, as the output depends on the input entered by you. Feel free to try the instructions on the command line.\n\n# 代码走查\n\n基本上，复制算法接受一系列迭代器，其中前两个参数代表源，第三个参数代表目的地，恰好是向量:\n\n```cpp\nistream_iterator<int> start_input(cin);\nistream_iterator<int> end_input;\n\ncopy ( start_input, end_input, back_inserter( v ) );\n```\n\n`start_input`迭代器实例定义了一个从`istream`和`cin`接收输入的`istream_iterator`迭代器，`end_input`迭代器实例定义了一个文件结束分隔符，默认情况下是一个空字符串(`\"\"`)。因此，可以通过在命令行输入终端中键入`\"\"`来终止输入。\n\n同样，让我们理解下面的代码片段:\n\n```cpp\ncout << \"\\nPrint the vector ...\" << endl;\ncopy ( v.begin(), v.end(), ostream_iterator<int>(cout, \"\\t\") );\ncout << endl;\n```\n\n复制算法用于一次一个元素地将向量中的值复制到`ostream`，用制表符(`\\t`)分隔输出。\n\n# 向量的陷阱\n\n每个 STL 容器都有自己的优缺点。没有一个单一的 STL 容器在所有场景中都能更好地工作。向量在内部使用数组数据结构，在 C/C++ 中数组的大小是固定的。因此，当您试图在向量大小已经达到其最大容量时向向量添加新值时，向量将分配新的连续位置，该位置可以容纳相邻位置中的旧值和新值。然后，它开始将旧值复制到新位置。一旦复制了所有数据元素，向量将使旧位置无效。\n\n每当这种情况发生时，向量插入将花费 *O(N)* 运行时复杂度。随着向量的大小随着时间的推移而增长，根据需要， *O(N)* 运行时的复杂性将表现出相当糟糕的性能。如果您知道所需的最大尺寸，您可以提前预留这么多初始尺寸来克服这个问题。然而，并不是在所有情况下都需要使用向量。当然，向量支持动态大小和随机访问，这在某些场景中有性能优势，但是您正在使用的功能可能并不真正需要随机访问，在这种情况下，list、deque 或其他一些容器可能更适合您。\n\n# 目录\n\n列表 STL 容器在内部使用双向链表数据结构。因此，列表只支持顺序访问，在最坏的情况下，在列表中搜索随机值可能会增加运行时的复杂性。然而，如果你确定你只需要顺序访问，这个列表确实有它自己的好处。列表 STL 容器允许您以恒定的时间复杂度在末尾、前面或中间插入数据元素，即在最佳、平均和最坏情况下的 *O(1)* 运行时复杂度。\n\n下图演示了列表 STL 使用的内部数据结构:\n\n![](img/00006.jpeg)\n\n让我们编写一个简单的程序来获得使用列表 STL 的第一手经验:\n\n```cpp\n#include <iostream>\n#include <list>\n#include <iterator>\n#include <algorithm>\nusing namespace std;\n\nint main () {\n\n  list<int> l;\n\n  for (int count=0; count<5; ++ count)\n    l.push_back( (count+1) * 100 );\n\n  auto pos = l.begin();\n\n  cout << \"\\nPrint the list ...\" << endl;\n  while ( pos != l.end() )\n    cout << *pos++ << \"-->\";\n  cout << \" X\" << endl;\n\n  return 0;\n}\n```\n\n我相信现在你已经尝到了 C++ STL 的味道，它的优雅和它的力量。观察到所有 STL 容器的语法都保持不变，这不是很酷吗？您可能已经观察到，无论您使用数组、向量还是列表，语法都保持不变。相信我，当您探索其他 STL 容器时，您也会得到同样的印象。\n\n话虽如此，前面的代码是不言自明的，因为我们对其他容器做了几乎相同的事情。\n\n让我们尝试对列表进行排序，如以下代码所示:\n\n```cpp\n#include <iostream>\n#include <list>\n#include <iterator>\n#include <algorithm>\nusing namespace std;\n\nint main () {\n\n    list<int> l = { 100, 20, 80, 50, 60, 5 };\n\n    auto pos = l.begin();\n\n    cout << \"\\nPrint the list before sorting ...\" << endl;\n    copy ( l.begin(), l.end(), ostream_iterator<int>( cout, \"-->\" ));\n    cout << \"X\" << endl;\n\n    l.sort();\n\n    cout << \"\\nPrint the list after sorting ...\" << endl;\n    copy ( l.begin(), l.end(), ostream_iterator<int>( cout, \"-->\" ));\n    cout << \"X\" << endl; \n\n    return 0;\n}\n```\n\n你注意到`sort()`方法了吗？是的，列表容器有自己的排序算法。列表容器支持自己版本的排序算法的原因是通用`sort()`算法需要随机访问迭代器，而列表容器不支持随机访问。在这种情况下，相应的容器将提供自己的高效算法来克服这个缺点。\n\n有趣的是，列表支持的`sort`算法的运行时复杂度为 *O (N log2 N)* 。\n\n# 列表中常用的 API\n\n下表显示了 STL 列表中最常用的应用编程接口:\n\n| API | **描述** |\n| `front()` | 这将返回列表中存储的第一个值 |\n| `back()` | 这将返回列表中存储的最后一个值 |\n| `size()` | 这将返回列表中存储的值的计数 |\n| `empty()` | 列表为空时返回`true`，否则返回`false` |\n| `clear()` | 这将清除列表中存储的所有值 |\n| `push_back<data_type>( value )` | 这会在列表的末尾添加一个值 |\n| `push_front<data_type>( value )` | 这会在列表的前面添加一个值 |\n| `merge( list )` | 这将合并两个具有相同类型值的排序列表 |\n| `reverse()` | 这将颠倒列表 |\n| `unique()` | 这将从列表中删除重复的值 |\n| `sort()` | 这会对存储在列表中的值进行排序 |\n\n# 转发列表\n\nSTL 的`forward_list`容器建立在单链表数据结构之上；因此，它只支持正向导航。由于`forward_list`在内存和运行时间方面为每个节点少消耗一个指针，因此与列表容器相比，它被认为更有效。然而，由于价格对于性能优势的额外优势，`forward_list`不得不放弃一些功能。\n\n下图显示了`forward_list`中使用的内部数据结构:\n\n![](img/00007.jpeg)\n\n让我们探索以下示例代码:\n\n```cpp\n#include <iostream>\n#include <forward_list>\n#include <iterator>\n#include <algorithm>\nusing namespace std;\n\nint main ( ) {\n\n  forward_list<int> l = { 10, 10, 20, 30, 45, 45, 50 };\n\n  cout << \"\\nlist with all values ...\" << endl;\n  copy ( l.begin(), l.end(), ostream_iterator<int>(cout, \"\\t\") );\n\n  cout << \"\\nSize of list with duplicates is \" << distance( l.begin(), l.end() ) << endl;\n\n  l.unique();\n\n  cout << \"\\nSize of list without duplicates is \" << distance( l.begin(), l.end() ) << endl;\n\n  l.resize( distance( l.begin(), l.end() ) );\n\n  cout << \"\\nlist after removing duplicates ...\" << endl;\n  copy ( l.begin(), l.end(), ostream_iterator<int>(cout, \"\\t\") );\n  cout << endl;\n\n  return 0;\n\n}\n```\n\n可以使用以下命令查看输出:\n\n```cpp\n./a.out\n```\n\n输出如下:\n\n```cpp\nlist with all values ...\n10    10    20    30    45    45    50\nSize of list with duplicates is 7\n\nSize of list without duplicates is 5\n\nlist after removing duplicates ...\n10    20   30   45   50\n```\n\n# 代码走查\n\n下面的代码用一些唯一的值和一些重复的值声明并初始化`forward_list`容器:\n\n```cpp\nforward_list<int> l = { 10, 10, 20, 30, 45, 45, 50 };\n```\n\n由于`forward_list`容器不支持`size()`功能，我们使用`distance()`功能来查找列表的大小:\n\n```cpp\ncout << \"\\nSize of list with duplicates is \" << distance( l.begin(), l.end() ) << endl;\n```\n\n下面的`forward_list<int>::unique()`函数删除重复的整数，只保留唯一的值:\n\n```cpp\nl.unique();\n```\n\n# 转发列表容器中常用的 API\n\n下表显示了常用的`forward_list`原料药:\n\n| API | **描述** |\n| `front()` | 这将返回存储在`forward_list`容器中的第一个值 |\n| `empty()` | 当`forward_list`容器为空时返回真，否则返回假 |\n| `clear()` | 这将清除存储在`forward_list`中的所有值 |\n| `push_front<data_type>( value )` | 这会在`forward_list`的前面增加一个值 |\n| `merge( list )` | 这将合并两个具有相同类型值的排序的`forward_list`容器 |\n| `reverse()` | 这会反转`forward_list`容器 |\n| `unique()` | 这将从`forward_list`容器中删除重复的值 |\n| `sort()` | 这将对存储在`forward_list`中的值进行排序 |\n\n让我们再探索一个例子，来深入了解`forward_list`容器:\n\n```cpp\n#include <iostream>\n#include <forward_list>\n#include <iterator>\n#include <algorithm>\nusing namespace std;\n\nint main () {\n\n    forward_list<int> list1 = { 10, 20, 10, 45, 45, 50, 25 };\n    forward_list<int> list2 = { 20, 35, 27, 15, 100, 85, 12, 15 };\n\n    cout << \"\\nFirst list before sorting ...\" << endl;\n    copy ( list1.begin(), list1.end(), ostream_iterator<int>(cout, \"\\t\") );\n    cout << endl; \n\n    cout << \"\\nSecond list before sorting ...\" << endl;\n    copy ( list2.begin(), list2.end(), ostream_iterator<int>(cout, \"\\t\") );\n    cout << endl;\n\n    list1.sort();\n    list2.sort();\n\n    cout << \"\\nFirst list after sorting ...\" << endl;\n    copy ( list1.begin(), list1.end(), ostream_iterator<int>(cout, \"\\t\") );\n    cout << endl; \n\n    cout << \"\\nSecond list after sorting ...\" << endl;\n    copy ( list2.begin(), list2.end(), ostream_iterator<int>(cout, \"\\t\") );\n    cout << endl;    \n\n    list1.merge ( list2 );\n\n    cout << \"\\nMerged list ...\" << endl;\n    copy ( list1.begin(), list1.end(), ostream_iterator<int>(cout, \"\\t\") );\n\n    cout << \"\\nMerged list after removing duplicates ...\" << endl;\n    list1.unique(); \n    copy ( list1.begin(), list1.end(), ostream_iterator<int>(cout, \"\\t\") );\n\n    return 0;\n}\n```\n\n前面的代码片段是一个有趣的例子，演示了`sort()`、`merge()`和`unique()` STL 算法的实际使用。\n\n可以使用以下命令查看输出:\n\n```cpp\n./a.out\n```\n\n程序的输出如下:\n\n```cpp\nFirst list before sorting ...\n10   20   10   45   45   50   25\nSecond list before sorting ...\n20   35   27   15   100  85   12   15\n\nFirst list after sorting ...\n10   10   20   25   45   45   50\nSecond list after sorting ...\n12   15   15   20   27   35   85   100\n\nMerged list ...\n10   10   12   15   15   20   20   25   27   35   45   45  50   85  100\nMerged list after removing duplicates ...\n10   12   15   20   25   27   35   45   50   85  100\n```\n\n输出和程序都很容易理解。\n\n# 双端队列\n\ndeque 容器是一个双端队列，使用的数据结构可以是动态数组或向量。在一个德格中，可以在前面和后面都插入一个元素，时间复杂度为 *O(1)* ，不像向量，在后面插入一个元素的时间复杂度为 *O(1)* ，而在前面插入一个元素的时间复杂度为 *O(N)* 。德格没有向量遇到的重新分配问题。然而，与矢量相比，矢量的所有优点都存在于 deque 中，只是 deque 在性能方面稍好一些，因为每行有几行动态数组或矢量。\n\n下图显示了 deque 容器中使用的内部数据结构:\n\n![](img/00008.jpeg)\n\n让我们编写一个简单的程序来测试 deque 容器:\n\n```cpp\n#include <iostream>\n#include <deque>\n#include <algorithm>\n#include <iterator>\nusing namespace std;\n\nint main () {\n  deque<int> d = { 10, 20, 30, 40, 50 };\n\n  cout << \"\\nInitial size of deque is \" << d.size() << endl;\n\n  d.push_back( 60 );\n  d.push_front( 5 );\n\n  cout << \"\\nSize of deque after push back and front is \" << d.size() << endl;\n\n  copy ( d.begin(), d.end(), ostream_iterator<int>( cout, \"\\t\" ) );\n  d.clear();\n\n  cout << \"\\nSize of deque after clearing all values is \" << d.size() <<\nendl;\n\n  cout << \"\\nIs the deque empty after clearing values ? \" << ( d.empty()\n? \"true\" : \"false\" ) << endl;\n\nreturn 0;\n}\n```\n\n可以使用以下命令查看输出:\n\n```cpp\n./a.out\n```\n\n程序的输出如下:\n\n```cpp\nIntitial size of deque is 5\n\nSize of deque after push back and front is 7\n\nPrint the deque ...\n5  10  20  30  40  50  60\nSize of deque after clearing all values is 0\n\nIs the deque empty after clearing values ? true\n```\n\n# 一个文件中常用的应用接口\n\n下表显示了常用的 deque APIs:\n\n| API | **描述** |\n| `at ( int index )` | 这将返回存储在索引位置的值。如果索引无效，则抛出`std::out_of_range`异常。 |\n| `operator [ int index ]` | 这将返回存储在索引位置的值。它比`at( int index )`更快，因为该函数不执行边界检查。 |\n| `front()` | 这将返回存储在 deque 中的第一个值。 |\n| `back()` | 这将返回存储在 deque 中的最后一个值。 |\n| `empty()` | 如果德格为空，则返回`true`，否则返回`false`。 |\n| `size()` | 这将返回存储在 deque 中的值的数量。 |\n| `capacity()` | 这将返回德格的总容量，而`size()`将返回德格中存储的实际值数。 |\n| `clear()` | 这将清除所有值。 |\n| `push_back<data_type>( value )` | 这会在 deque 的末尾添加一个新值。 |\n\n# 关联容器\n\n与序列容器不同，关联容器以排序的方式存储数据。因此，关联容器不会保留数据插入的顺序。关联容器在搜索运行时复杂度为 *O( log n )* 的值时非常高效。每当一个新的值被添加到容器中时，如果需要，容器将对内部存储的值重新排序。\n\nSTL 支持以下类型的关联容器:\n\n*   一组\n*   地图\n*   多组\n*   多点触控\n*   无序集\n*   无序多集\n*   无序地图\n*   无序多重映射\n\n关联容器将数据组织为键值对。数据将根据密钥进行排序，以便随机和更快地访问。联合容器有两种风格:\n\n*   整齐的\n*   无序的\n\n以下关联容器属于有序容器，因为它们是以特定方式排序的。有序关联容器一般使用某种形式的**二叉查找树**(**BST**)；通常，红黑树用于存储数据:\n\n*   一组\n*   地图\n*   多组\n*   多点触控\n\n以下关联容器属于无序容器，因为它们不以任何特定方式排序，并且使用哈希表:\n\n*   无序集\n*   无序地图\n*   无序多集\n*   无序多重映射\n\n让我们通过以下小节中的示例来理解前面提到的容器。\n\n# 一组\n\n集合容器只以排序的方式存储唯一的值。集合使用值作为关键字来组织值。集合容器是不可变的，即存储在集合中的值不能被修改；但是，这些值可以删除。集合通常使用红黑树数据结构，这是一种平衡的 BST 形式。集合运算的时间复杂度保证为 *O ( log N )* 。\n\n让我们用一个集合编写一个简单的程序:\n\n```cpp\n#include <iostream>\n#include <set>\n#include <vector>\n#include <iterator>\n#include <algorithm>\nusing namespace std;\n\nint main( ) {\n    set<int> s1 = { 1, 3, 5, 7, 9 };\n    set<int> s2 = { 2, 3, 7, 8, 10 };\n\n    vector<int> v( s1.size() + s2.size() );\n\n    cout << \"\\nFirst set values are ...\" << endl;\n    copy ( s1.begin(), s1.end(), ostream_iterator<int> ( cout, \"\\t\" ) );\n    cout << endl;\n\n    cout << \"\\nSecond set values are ...\" << endl;\n    copy ( s2.begin(), s2.end(), ostream_iterator<int> ( cout, \"\\t\" ) );\n    cout << endl;\n\n    auto pos = set_difference ( s1.begin(), s1.end(), s2.begin(), s2.end(), v.begin() ); \n    v.resize ( pos - v.begin() );\n\n    cout << \"\\nValues present in set one but not in set two are ...\" << endl;\n    copy ( v.begin(), v.end(), ostream_iterator<int> ( cout, \"\\t\" ) );\n    cout << endl; \n\n    v.clear();\n\n    v.resize ( s1.size() + s2.size() );\n\n    pos = set_union ( s1.begin(), s1.end(), s2.begin(), s2.end(), v.begin() );\n\n    v.resize ( pos - v.begin() );\n\n    cout << \"\\nMerged set values in vector are ...\" << endl;\n    copy ( v.begin(), v.end(), ostream_iterator<int> ( cout, \"\\t\" ) );\n    cout << endl; \n\n    return 0;\n}\n```\n\n可以使用以下命令查看输出:\n\n```cpp\n./a.out\n```\n\n程序的输出如下:\n\n```cpp\nFirst set values are ...\n1   3   5   7   9\n\nSecond set values are ...\n2   3   7   8   10\n\nValues present in set one but not in set two are ...\n1   5   9\n\nMerged values of first and second set are ...\n1   2   3   5   7   8   9  10\n```\n\n# 代码走查\n\n下面的代码声明并初始化了两组，`s1`和`s2`:\n\n```cpp\nset<int> s1 = { 1, 3, 5, 7, 9 };\nset<int> s2 = { 2, 3, 7, 8, 10 };\n```\n\n下一行将确保向量有足够的空间来存储结果向量中的值:\n\n```cpp\nvector<int> v( s1.size() + s2.size() );\n```\n\n以下代码将打印`s1`和`s2`中的值:\n\n```cpp\ncout << \"\\nFirst set values are ...\" << endl;\ncopy ( s1.begin(), s1.end(), ostream_iterator<int> ( cout, \"\\t\" ) );\ncout << endl;\n\ncout << \"\\nSecond set values are ...\" << endl;\ncopy ( s2.begin(), s2.end(), ostream_iterator<int> ( cout, \"\\t\" ) );\ncout << endl;\n```\n\n`set_difference()`算法将用仅出现在集合`s1`中而不出现在`s2`中的值填充向量`v`。迭代器`pos`将指向向量中的最后一个元素；因此，向量`resize`将确保移除向量中的额外空格:\n\n```cpp\nauto pos = set_difference ( s1.begin(), s1.end(), s2.begin(), s2.end(), v.begin() ); \nv.resize ( pos - v.begin() );\n```\n\n以下代码将打印向量`v`中填充的值:\n\n```cpp\ncout << \"\\nValues present in set one but not in set two are ...\" << endl;\ncopy ( v.begin(), v.end(), ostream_iterator<int> ( cout, \"\\t\" ) );\ncout << endl;\n```\n\n`set_union()`算法将集合`s1`和`s2`的内容合并到向量中，然后调整向量的大小以仅适合合并的值:\n\n```cpp\npos = set_union ( s1.begin(), s1.end(), s2.begin(), s2.end(), v.begin() );\nv.resize ( pos - v.begin() );\n```\n\n以下代码将打印向量`v`中填充的合并值:\n\n```cpp\ncout << \"\\nMerged values of first and second set are ...\" << endl;\ncopy ( v.begin(), v.end(), ostream_iterator<int> ( cout, \"\\t\" ) );\ncout << endl;\n```\n\n# 集合中常用的应用编程接口\n\n下表描述了常用的应用编程接口集:\n\n| API | **描述** |\n| `insert( value )` | 这将在集合中插入一个值 |\n| `clear()` | 这将清除集合中的所有值 |\n| `size()` | 这将返回集合中存在的条目总数 |\n| `empty()` | 如果设置为空，将打印`true`，否则返回`false` |\n| `find()` | 这会找到具有指定键的元素，并返回迭代器位置 |\n| `equal_range()` | 这将返回与特定键匹配的元素范围 |\n| `lower_bound()` | 这会返回一个不小于给定键的第一个元素的迭代器 |\n| `upper_bound()` | 这会返回一个迭代器到大于给定键\n的第一个元素 |\n\n# 地图\n\n地图存储按键组织的值。与集合不同，地图每个值都有一个专用键。地图通常使用红黑树作为内部数据结构，这是一个平衡的 BST，保证了在地图中搜索或定位值的 *O( log N )* 运行时效率。存储在地图中的值使用红黑树根据关键字进行排序。地图中使用的键必须是唯一的。地图不会保留输入序列，因为它会根据关键点重新组织值，也就是说，红黑树将被旋转以平衡红黑树的高度。\n\n让我们编写一个简单的程序来理解地图的用法:\n\n```cpp\n#include <iostream>\n#include <map>\n#include <iterator>\n#include <algorithm>\nusing namespace std;\n\nint main ( ) {\n\n  map<string, long> contacts;\n\n  contacts[\"Jegan\"] = 123456789;\n  contacts[\"Meena\"] = 523456289;\n  contacts[\"Nitesh\"] = 623856729;\n  contacts[\"Sriram\"] = 993456789;\n\n  auto pos = contacts.find( \"Sriram\" );\n\n  if ( pos != contacts.end() )\n    cout << pos->second << endl;\n\n  return 0;\n}\n```\n\n让我们编译并检查程序的输出:\n\n```cpp\ng++ main.cpp -std=c++ 17\n./a.out\n```\n\n输出如下:\n\n```cpp\nMobile number of Sriram is 8901122334\n```\n\n# 代码走查\n\n下面一行声明了一个以`string`名称为关键字，以`long`手机号码为存储在地图中的值的地图:\n\n```cpp\nmap< string, long > contacts;\n```\n\n下面的代码片段添加了四个按姓名组织的联系人作为关键字:\n\n```cpp\n contacts[ \"Jegan\" ] = 1234567890;\n contacts[ \"Meena\" ] = 5784433221;\n contacts[ \"Nitesh\" ] = 4567891234;\n contacts[ \"Sriram\" ] = 8901122334;\n```\n\n下面一行将尝试在联系人地图中定位名为`Sriram`的联系人；如果找到`Sriram`，那么`find()`函数将返回指向键值对位置的迭代器；否则返回`contacts.end()`位置:\n\n```cpp\n auto pos = contacts.find( \"Sriram\" );\n```\n\n下面的代码验证迭代器`pos`是否已经到达`contacts.end()`并打印联系号码。因为地图是一个关联容器，它存储一对`key=>value`；因此，`pos->first`表示键，`pos->second`表示值:\n\n```cpp\n if ( pos != contacts.end() )\n cout << \"\\nMobile number of \" << pos->first << \" is \" << pos->second << endl;\n else\n cout << \"\\nContact not found.\" << endl;\n```\n\n# 地图中常用的应用编程接口\n\n下表显示了常用的地图应用编程接口:\n\n| API | **描述** |\n| `at ( key )` | 如果找到相应的键，则返回该键的值；否则会抛出`std::out_of_range`异常 |\n| `operator[ key ]` | 如果找到相应的键，这将更新该键的现有值；否则，它将添加一个新条目，并提供相应的`key=>value` |\n| `empty()` | 如果地图为空，则返回`true`，否则返回`false` |\n| `size()` | 这会返回存储在地图中的`key=>value`对的计数 |\n| `clear()` | 这将清除存储在地图中的条目 |\n| `count()` | 这将返回与给定键匹配的元素数量 |\n| `find()` | 这会找到具有指定键的元素 |\n\n# 多组\n\n多集容器的工作方式类似于集合容器，只是集合只允许存储唯一的值，而多集允许存储重复的值。如您所知，在集合和多集合容器的情况下，值本身被用作组织数据的键。多集容器就像一个集合；它不允许修改存储在 multiset 中的值。\n\n让我们使用多集编写一个简单的程序:\n\n```cpp\n#include <iostream>\n#include <set>\n#include <iterator>\n#include <algorithm>\nusing namespace std;\n\nint main() {\n  multiset<int> s = { 10, 30, 10, 50, 70, 90 };\n\n  cout << \"\\nMultiset values are ...\" << endl;\n\n  copy ( s.begin(), s.end(), ostream_iterator<int> ( cout, \"\\t\" ) );\n  cout << endl;\n\n  return 0;\n}\n```\n\n可以使用以下命令查看输出:\n\n```cpp\n./a.out\n```\n\n程序的输出如下:\n\n```cpp\nMultiset values are ...\n10 30 10 50 70 90\n```\n\n有趣的是，在前面的输出中，您可以看到多集包含重复的值。\n\n# 多点触控\n\n多重映射完全像映射一样工作，除了多重映射容器允许用同一个键存储多个值。\n\n让我们用一个简单的例子来探索 multimap 容器:\n\n```cpp\n#include <iostream>\n#include <map>\n#include <vector>\n#include <iterator>\n#include <algorithm>\nusing namespace std;\n\nint main() {\n  multimap< string, long > contacts = {\n    { \"Jegan\", 2232342343 },\n    { \"Meena\", 3243435343 },\n    { \"Nitesh\", 6234324343 },\n    { \"Sriram\", 8932443241 },\n    { \"Nitesh\", 5534327346 }\n  };\n\n  auto pos = contacts.find ( \"Nitesh\" );\n  int count = contacts.count( \"Nitesh\" );\n  int index = 0;\n\n  while ( pos != contacts.end() ) {\n    cout << \"\\nMobile number of \" << pos->first << \" is \" <<\n    pos->second << endl;\n    ++ index;\n    if ( index == count )\n      break;\n  }\n\n  return 0;\n}\n```\n\n可以使用以下命令编译程序并查看输出:\n\n```cpp\ng++ main.cpp -std=c++ 17\n\n./a.out\n```\n\n程序的输出如下:\n\n```cpp\nMobile number of Nitesh is 6234324343\nMobile number of Nitesh is 5534327346\n```\n\n# 无序集\n\n无序集的工作方式类似于集合，只是这些容器的内部行为不同。一个集合使用红黑树，而一个无序集合使用散列表。集合运算的时间复杂度为 *O( log N)* ，而无序集合运算的时间复杂度为*O(1)*；因此，无序集往往比有序集快。\n\n存储在无序集中的值没有以任何特定的方式组织，这与以排序方式存储值的集合不同。如果性能是标准，那么无序集合是一个很好的选择；但是，如果需要以排序的方式迭代值，那么 set 是一个不错的选择。\n\n# 无序地图\n\n无序地图的工作方式类似于地图，只是这些容器的内部行为不同。地图使用红黑树，而无序地图使用散列表。地图操作的时间复杂度为 *O( log N)* ，而无序地图操作的时间复杂度为*O(1)；*因此，无序地图往往比地图更快。\n\n存储在无序映射中的值没有以任何特定的方式进行组织，这与按键对值进行排序的映射不同。\n\n# 无序多集\n\n无序多集的工作方式类似于多集，只是这些容器的内部行为不同。多集利用红黑树，而无序多集利用散列表。多集运算的时间复杂度为 *O( log N)* ，而无序多集运算的时间复杂度为 *O(1)* 。因此，无序多集往往比多集更快。\n\n存储在无序多集合中的值没有以任何特定的方式组织，不像存储在有序多集合中的值。如果性能是标准，无序多集是一个很好的选择；然而，如果需要以排序的方式迭代值，那么 multiset 是一个不错的选择。\n\n# 无序多重映射\n\n无序多映射的工作方式类似于多映射，只是这些容器的内部行为不同。多映射利用红黑树，而无序的多映射利用散列表。多 ap 操作的时间复杂度为 *O( log N)* ，而无序多 ap 操作的时间复杂度为*O(1)*；因此，无序多映射往往比多映射更快。\n\n存储在无序多映射中的值没有以任何特定的方式组织，不像在多映射中，值是按键排序的。如果性能是标准，那么无序的多重映射是一个很好的选择；然而，如果需要以排序的方式迭代值，那么 multimap 是一个不错的选择。\n\n# 容器适配器\n\n容器适配器调整现有容器以提供新容器。简单来说，STL 扩展是通过组合而不是继承来完成的。\n\nSTL 容器不能通过继承来扩展，因为它们的构造函数不是虚拟的。在整个 STL 中，您可以观察到，虽然静态多态性在运算符重载和模板方面都有使用，但出于性能原因，动态多态性被有意避免。因此，通过子类化现有的容器来扩展 STL 不是一个好主意，因为它会导致内存泄漏，因为容器类的行为不像基类。\n\nSTL 支持以下容器适配器:\n\n*   堆\n*   长队\n*   优先级队列\n\n让我们在下面的小节中探讨容器适配器。\n\n# 堆\n\n堆栈不是新容器；这是一个模板适配器类。适配器容器包装现有容器并提供高级功能。堆栈适配器容器提供堆栈操作，同时隐藏与堆栈无关的不必要功能。默认情况下，STL 堆栈使用一个 deque 容器；但是，我们可以指示堆栈在堆栈实例化期间使用任何满足堆栈要求的现有容器。\n\ndeq、lists 和 vectors 满足堆栈适配器的要求。\n\n堆栈按照**后进先出** ( **后进先出**)的原则运行。\n\n# 堆栈中常用的 API\n\n下表显示了常用的堆栈 API:\n\n| API | **描述** |\n| `top()` | 这将返回堆栈中最上面的值，即最后添加的值 |\n| `push<data_type>( value )` | 这将把提供的值推到栈顶 |\n| `pop()` | 这将从堆栈中移除最上面的值 |\n| `size()` | 这将返回堆栈中存在的值的数量 |\n| `empty()` | 如果堆栈为空，则返回`true`；否则返回`false` |\n\n是时候弄脏我们的手了；让我们编写一个简单的程序来使用堆栈:\n\n```cpp\n#include <iostream>\n#include <stack>\n#include <iterator>\n#include <algorithm>\nusing namespace std;\n\nint main ( ) {\n\n  stack<string> spoken_languages;\n\n  spoken_languages.push ( \"French\" );\n  spoken_languages.push ( \"German\" );\n  spoken_languages.push ( \"English\" );\n  spoken_languages.push ( \"Hindi\" );\n  spoken_languages.push ( \"Sanskrit\" );\n  spoken_languages.push ( \"Tamil\" );\n\n  cout << \"\\nValues in Stack are ...\" << endl;\n  while ( ! spoken_languages.empty() ) {\n              cout << spoken_languages.top() << endl;\n        spoken_languages.pop();\n  }\n  cout << endl;\n\n  return 0;\n}\n```\n\n使用以下命令可以编译程序并查看输出:\n\n```cpp\ng++ main.cpp -std=c++ 17\n\n./a.out\n```\n\n程序的输出如下:\n\n```cpp\nValues in Stack are ...\nTamil\nKannada\nTelugu\nSanskrit\nHindi\nEnglish\nGerman\nFrench\n```\n\n从前面的输出中，我们可以看到堆栈的后进先出行为。\n\n# 长队\n\n队列基于**先进先出** ( **先进先出**原则工作。队列不是新的容器；它是一个模板化的适配器类，包装现有的容器，提供队列操作所需的高级功能，同时隐藏与队列无关的不必要功能。默认情况下，STL 队列使用一个 deque 容器；但是，我们可以指示队列在队列实例化期间使用任何满足队列要求的现有容器。\n\n在队列中，新值可以在后面添加，也可以从前面删除。去 q、列表和向量满足队列适配器的要求。\n\n# 队列中常用的应用编程接口\n\n下表显示了常用的队列 API:\n\n| API | **描述** |\n| `push()` | 这将在队列的后面追加一个新值 |\n| `pop()` | 这将删除队列前面的值 |\n| `front()` | 这将返回队列前面的值 |\n| `back()` | 这将返回队列后面的值 |\n| `empty()` | 当队列为空时，返回`true`；否则返回`false` |\n| `size()` | 这将返回队列中存储的值的数量 |\n\n让我们在下面的程序中使用队列:\n\n```cpp\n#include <iostream>\n#include <queue>\n#include <iterator>\n#include <algorithm>\nusing namespace std;\n\nint main () {\n  queue<int> q;\n\n  q.push ( 100 );\n  q.push ( 200 );\n  q.push ( 300 );\n\n  cout << \"\\nValues in Queue are ...\" << endl;\n  while ( ! q.empty() ) {\n    cout << q.front() << endl;\n    q.pop();\n  }\n\n  return 0;\n}\n```\n\n可以使用以下命令编译程序并查看输出:\n\n```cpp\ng++ main.cpp -std=c++ 17\n\n./a.out\n```\n\n程序的输出如下:\n\n```cpp\nValues in Queue are ...\n100\n200\n300\n```\n\n从前面的输出中，您可以观察到值是按照推入的相同顺序弹出的，即先进先出。\n\n# 优先队列\n\n优先级队列不是新的容器；它是一个模板化的适配器类，包装现有的容器，提供优先级队列操作所需的高级功能，同时隐藏与优先级队列无关的不必要功能。默认情况下，优先级队列使用向量容器；然而，deque 容器也满足优先级队列的要求。因此，在优先级队列实例化期间，您可以指示优先级队列也使用一个 deque。\n\n优先级队列以最高优先级值最先出现的方式组织数据；换句话说，这些值是按降序排序的。\n\ndeque 和 vector 满足优先级队列适配器的要求。\n\n# 优先级队列中常用的应用编程接口\n\n下表显示了常用的优先级队列 API:\n\n| API | **描述** |\n| `push()` | 这将在优先级队列的后面追加一个新值 |\n| `pop()` | 这将删除优先级队列前面的值 |\n| `empty()` | 当优先级队列为空时，返回`true`；否则返回`false` |\n| `size()` | 这将返回优先级队列中存储的值的数量 |\n| `top()` | 这将返回优先级队列前面的值 |\n\n让我们写一个简单的程序来理解`priority_queue`:\n\n```cpp\n#include <iostream>\n#include <queue>\n#include <iterator>\n#include <algorithm>\nusing namespace std;\n\nint main () {\n  priority_queue<int> q;\n\n  q.push( 100 );\n  q.push( 50 );\n  q.push( 1000 );\n  q.push( 800 );\n  q.push( 300 );\n\n  cout << \"\\nSequence in which value are inserted are ...\" << endl;\n  cout << \"100\\t50\\t1000\\t800\\t300\" << endl;\n  cout << \"Priority queue values are ...\" << endl;\n\n  while ( ! q.empty() ) {\n    cout << q.top() << \"\\t\";\n    q.pop();\n  }\n  cout << endl;\n\n  return 0;\n}\n```\n\n使用以下命令可以编译程序并查看输出:\n\n```cpp\ng++ main.cpp -std=c++ 17\n\n./a.out\n```\n\n程序的输出如下:\n\n```cpp\nSequence in which value are inserted are ...\n100   50   1000  800   300\n\nPriority queue values are ...\n1000  800   300   100   50\n```\n\n从前面的输出中，您可以观察到`priority_queue`是一种特殊类型的队列，它以最高值最先出现的方式对输入进行重新排序。\n\n# 摘要\n\n在本章中，您学习了现成的泛型容器、函子、迭代器和算法。您还学习了集合、映射、多集合和多映射关联容器、它们的内部数据结构以及可以应用于它们的常见算法。此外，您还学习了如何通过实际操作代码示例来使用各种容器。\n\n在下一章中，您将学习模板编程，这有助于您掌握模板的要领。"
  },
  {
    "path": "docs/master-cpp-prog/03.md",
    "content": "# 三、模板编程\n\n在本章中，我们将涵盖以下主题:\n\n*   泛型编程\n*   功能模板\n*   类模板\n*   重载函数模板\n*   泛型类\n*   显式类专门化\n*   部分专业化\n\n现在让我们开始学习泛型编程。\n\n# 泛型编程\n\n泛型编程是一种帮助您开发可重用代码或泛型算法的编程风格，这些代码或算法可以应用于各种各样的数据类型。无论何时调用通用算法，数据类型都将作为具有特殊语法的参数提供。\n\n假设我们想写一个`sort()`函数，它接受一个需要按升序排序的输入数组。其次，我们需要`sort()`功能对`int`、`double`、`char`和`string`数据类型进行排序。有几种方法可以解决这个问题:\n\n*   我们可以为每种数据类型编写四个不同的`sort()`函数\n*   我们还可以编写一个宏函数\n\n嗯，这两种方法各有优缺点。第一种方法的优点是，由于`int`、`double`、`char`和`string`数据类型有专用函数，如果提供了不正确的数据类型，编译器将能够执行类型检查。第一种方法的缺点是，我们必须编写四个不同的函数，即使所有函数的逻辑保持不变。如果在算法中发现 bug，必须在所有四个函数中分别修复；因此，需要大量的维护工作。如果我们需要支持另一种数据类型，我们最终会再编写一个函数，并且随着我们需要支持更多的数据类型，这个函数会不断增长。\n\n第二种方法的优点是，我们可以只为所有数据类型编写一个宏。然而，一个非常令人沮丧的缺点是，编译器将无法执行类型检查，并且这种方法更容易出错，可能会带来许多意想不到的麻烦。这种方法完全违背了面向对象的编码原则。\n\nC++ 支持使用模板的泛型编程，这有以下好处:\n\n*   我们只需要使用模板编写一个函数\n*   模板支持静态多态性\n*   模板提供了上述两种方法的所有优点，没有任何缺点\n*   泛型编程支持代码重用\n*   生成的代码是面向对象的\n*   C++ 编译器可以在编译时执行类型检查\n*   易于维护\n*   支持多种内置和用户定义的数据类型\n\n然而，缺点如下:\n\n*   不是所有的 C++ 程序员都觉得写基于模板的编码很舒服，但这只是一个开始\n*   在某些情况下，模板可能会膨胀您的代码并增加二进制占用空间，从而导致性能问题\n\n# 功能模板\n\n函数模板允许您参数化数据类型。之所以称之为泛型编程，是因为单个模板函数将支持许多内置和用户定义的数据类型。模板化函数的工作方式类似于 **C 风格的宏**，只是当我们在调用模板函数时提供不兼容的数据类型时，C++ 编译器会对函数进行类型检查。\n\n用一个简单的例子更容易理解模板概念，如下所示:\n\n```cpp\n#include <iostream>\n#include <algorithm>\n#include <iterator>\nusing namespace std;\n\ntemplate <typename T, int size>\nvoid sort ( T input[] ) {\n\n     for ( int i=0; i<size; ++ i) { \n         for (int j=0; j<size; ++ j) {\n              if ( input[i] < input[j] )\n                  swap (input[i], input[j] );\n         }\n     }\n\n}\n\nint main () {\n        int a[10] = { 100, 10, 40, 20, 60, 80, 5, 50, 30, 25 };\n\n        cout << \"\\nValues in the int array before sorting ...\" << endl;\n        copy ( a, a+10, ostream_iterator<int>( cout, \"\\t\" ) );\n        cout << endl;\n\n        ::sort<int, 10>( a );\n\n        cout << \"\\nValues in the int array after sorting ...\" << endl;\n        copy ( a, a+10, ostream_iterator<int>( cout, \"\\t\" ) );\n        cout << endl;\n\n        double b[5] = { 85.6d, 76.13d, 0.012d, 1.57d, 2.56d };\n\n        cout << \"\\nValues in the double array before sorting ...\" << endl;\n        copy ( b, b+5, ostream_iterator<double>( cout, \"\\t\" ) );\n        cout << endl;\n\n        ::sort<double, 5>( b );\n\n        cout << \"\\nValues in the double array after sorting ...\" << endl;\n        copy ( b, b+5, ostream_iterator<double>( cout, \"\\t\" ) );\n        cout << endl;\n\n        string names[6] = {\n               \"Rishi Kumar Sahay\",\n               \"Arun KR\",\n               \"Arun CR\",\n               \"Ninad\",\n               \"Pankaj\",\n               \"Nikita\"\n        };\n\n        cout << \"\\nNames before sorting ...\" << endl;\n        copy ( names, names+6, ostream_iterator<string>( cout, \"\\n\" ) );\n        cout << endl;\n\n        ::sort<string, 6>( names );\n\n        cout << \"\\nNames after sorting ...\" << endl;\n        copy ( names, names+6, ostream_iterator<string>( cout, \"\\n\" ) );\n        cout << endl;\n\n        return 0;\n}\n\n```\n\n运行以下命令:\n\n```cpp\ng++ main.cpp -std=c++ 17\n./a.out\n```\n\n前面程序的输出如下:\n\n```cpp\nValues in the int array before sorting ...\n100  10   40   20   60   80   5   50   30   25\n\nValues in the int array after sorting ...\n5    10   20   25   30   40   50   60   80   100\n\nValues in the double array before sorting ...\n85.6d 76.13d 0.012d 1.57d 2.56d\n\nValues in the double array after sorting ...\n0.012   1.57   2.56   76.13   85.6\n\nNames before sorting ...\nRishi Kumar Sahay\nArun KR\nArun CR\nNinad\nPankaj\nNikita\n\nNames after sorting ...\nArun CR\nArun KR\nNikita\nNinad\nPankaj\nRich Kumar Sahay\n```\n\n看到仅仅一个模板函数就能完成所有的魔法，难道不是很有趣吗？是的，这就是 C++ 模板有多酷！\n\nAre you curious to see the assembly output of a template instantiation? Use the command, **`g++ -S main.cpp`**.\n\n# 代码走查\n\n下面的代码定义了一个函数模板。关键字`template <typename T, int size>`告诉编译器接下来是一个函数模板:\n\n```cpp\ntemplate <typename T, int size>\nvoid sort ( T input[] ) {\n\n for ( int i=0; i<size; ++ i) { \n     for (int j=0; j<size; ++ j) {\n         if ( input[i] < input[j] )\n             swap (input[i], input[j] );\n     }\n }\n\n}\n```\n\n第`void sort ( T input[] )`行定义了一个名为`sort`的函数，该函数返回`void`，并接收类型为`T`的输入数组。`T`类型不表示任何特定的数据类型。`T`将在编译时实例化函数模板时推导出来。\n\n下面的代码用一些未排序的值填充一个整数数组，并将其打印到终端:\n\n```cpp\n int a[10] = { 100, 10, 40, 20, 60, 80, 5, 50, 30, 25 };\n cout << \"\\nValues in the int array before sorting ...\" << endl;\n copy ( a, a+10, ostream_iterator<int>( cout, \"\\t\" ) );\n cout << endl;\n```\n\n下面一行将为`int`数据类型实例化一个函数模板实例。此时，`typename T`被替换，并且为`int`数据类型创建了一个专门的函数。`sort`前面的作用域解析运算符，即`::sort()`，确保它调用我们在全局命名空间中定义的自定义函数`sort()`；否则，C++ 编译器将尝试调用在`std namespace`中定义的`sort()`算法，或者从任何其他名称空间调用(如果存在这样的函数的话)。`<int, 10>`变量告诉编译器创建一个函数的实例，用`int`代替`typename T`，`10`表示模板函数中使用的数组的大小:\n\n```cpp\n::sort<int, 10>( a );\n```\n\n以下几行将分别实例化支持`5`元素的`double`数组和`6`元素的`string`数组的另外两个实例:\n\n```cpp\n::sort<double, 5>( b );\n::sort<string, 6>( names );\n```\n\n如果你想知道更多关于 C++ 编译器如何实例化函数模板来支持`int`、`double`和`string`的细节，你可以试试 Unix 实用程序、`nm`和`c++ filt`。`nm` Unix 实用程序将在符号表中列出符号，如下所示:\n\n```cpp\nnm ./a.out | grep sort\n\n00000000000017f1 W _Z4sortIdLi5EEvPT_\n0000000000001651 W _Z4sortIiLi10EEvPT_\n000000000000199b W _Z4sortINSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEELi6EEvPT_\n```\n\n可以看到，二进制中有三个不同的重载`sort`函数；然而，我们只定义了一个模板函数。由于 C++ 编译器在处理函数重载时使用了错误的名称，我们很难解释这三个函数中的哪一个函数是针对`int`、`double`和`string`数据类型的。\n\n不过有一个线索:第一个功能是给`double`用的，第二个是给`int`用的，第三个是给`string`用的。更名功能有`double`的`_Z4sortIdLi5EEvPT_`、`int`的`_Z4sortIiLi10EEvPT_`、`string`的`_Z4sortINSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEELi6EEvPT_`。还有另一个很酷的 Unix 工具可以帮助你毫不费力地解释函数签名。检查`c++ filt`实用程序的以下输出:\n\n```cpp\nc++ filt _Z4sortIdLi5EEvPT_\nvoid sort<double, 5>(double*)\n\nc++ filt _Z4sortIiLi10EEvPT_\nvoid sort<int, 10>(int*)\n\nc++ filt _Z4sortINSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEELi6EEvPT_\nvoid sort<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, 6>(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >*)\n```\n\n希望在使用 C++ 模板时，您会发现这些实用程序很有用。我相信这些工具和技术将帮助您调试任何 C++ 应用。\n\n# 重载函数模板\n\n重载函数模板的工作方式与 C++ 中的常规函数重载完全一样。然而，我会帮助你回忆 C++ 函数重载的基础。\n\nC++ 编译器的函数重载规则和期望如下:\n\n*   重载的函数名将是相同的。\n*   C++ 编译器将无法区分仅在返回值上不同的重载函数。\n*   重载函数参数的数量、这些参数的数据类型或它们的顺序应该不同。除了其他规则之外，当前要点中描述的这些规则中至少有一条应该得到满足，但是更多的遵守不会有什么坏处。\n*   重载函数必须在同一命名空间或同一类范围内。\n\n如果不满足上述任何一条规则，C++ 编译器就不会将它们视为重载函数。如果在区分重载函数时有任何歧义，C++ 编译器会立即将其报告为编译错误。\n\n是时候用一个例子来探讨这个问题了，如下面的程序所示:\n\n```cpp\n#include <iostream>\n#include <array>\nusing namespace std;\n\nvoid sort ( array<int,6> data ) {\n\n     cout << \"Non-template sort function invoked ...\" << endl;\n\n     int size = data.size();\n\n     for ( int i=0; i<size; ++ i ) { \n          for ( int j=0; j<size; ++ j ) {\n                if ( data[i] < data[j] )\n                    swap ( data[i], data[j] );\n          }\n     }\n\n}\n\ntemplate <typename T, int size>\nvoid sort ( array<T, size> data ) {\n\n     cout << \"Template sort function invoked with one argument...\" << endl;\n\n     for ( int i=0; i<size; ++ i ) {\n         for ( int j=0; j<size; ++ j ) {\n             if ( data[i] < data[j] )\n                swap ( data[i], data[j] );\n         }\n     }\n\n}\n\ntemplate <typename T>\nvoid sort ( T data[], int size ) {\n     cout << \"Template sort function invoked with two arguments...\" << endl;\n\n     for ( int i=0; i<size; ++ i ) {\n         for ( int j=0; j<size; ++ j ) {\n             if ( data[i] < data[j] )\n                swap ( data[i], data[j] );\n         }\n     }\n\n}\n\nint main() {\n\n    //Will invoke the non-template sort function\n    array<int, 6> a = { 10, 50, 40, 30, 60, 20 };\n    ::sort ( a );\n\n    //Will invoke the template function that takes a single argument\n    array<float,6> b = { 10.6f, 57.9f, 80.7f, 35.1f, 69.3f, 20.0f };\n    ::sort<float,6>( b );\n\n    //Will invoke the template function that takes a single argument\n    array<double,6> c = { 10.6d, 57.9d, 80.7d, 35.1d, 69.3d, 20.0d };\n    ::sort<double,6> ( c );\n\n    //Will invoke the template function that takes two arguments\n    double d[] = { 10.5d, 12.1d, 5.56d, 1.31d, 81.5d, 12.86d };\n    ::sort<double> ( d, 6 );\n\n    return 0;\n\n}\n```\n\n运行以下命令:\n\n```cpp\ng++ main.cpp -std=c++ 17\n\n./a.out\n```\n\n前面程序的输出如下:\n\n```cpp\nNon-template sort function invoked ...\n\nTemplate sort function invoked with one argument...\n\nTemplate sort function invoked with one argument...\n\nTemplate sort function invoked with two arguments...\n```\n\n# 代码走查\n\n以下代码是我们自定义`sort()`函数的非模板版本:\n\n```cpp\nvoid sort ( array<int,6> data ) { \n\n     cout << \"Non-template sort function invoked ...\" << endl;\n\n     int size = data.size();\n\n     for ( int i=0; i<size; ++ i ) { \n         for ( int j=0; j<size; ++ j ) {\n             if ( data[i] < data[j] )\n                 swap ( data[i], data[j] );\n         }\n     }\n\n}\n```\n\n非模板函数和模板函数可以共存，参与函数重载。前面函数的一个奇怪行为是数组的大小是硬编码的。\n\n我们的`sort()`函数的第二个版本是一个模板函数，如下面的代码片段所示。有趣的是，我们在第一个非模板`sort()`版本中注意到的奇怪问题在这里得到了解决:\n\n```cpp\ntemplate <typename T, int size>\nvoid sort ( array<T, size> data ) {\n\n     cout << \"Template sort function invoked with one argument...\" << endl;\n\n     for ( int i=0; i<size; ++ i ) {\n         for ( int j=0; j<size; ++ j ) {\n             if ( data[i] < data[j] )\n                swap ( data[i], data[j] );\n         }\n     }\n\n}\n```\n\n在前面的代码中，数据类型和数组大小都作为模板参数传递，然后传递给函数调用参数。这种方法使函数通用，因为该函数可以为任何数据类型实例化。\n\n我们自定义的第三个版本`sort()`函数也是一个模板函数，如下面的代码片段所示:\n\n```cpp\ntemplate <typename T>\nvoid sort ( T data[], int size ) {\n\n     cout << \"Template sort function invoked with two argument...\" << endl;\n\n     for ( int i=0; i<size; ++ i ) {\n         for ( int j=0; j<size; ++ j ) {\n             if ( data[i] < data[j] )\n                swap ( data[i], data[j] );\n         }\n     }\n\n}\n```\n\n前面的模板函数采用 C 风格的数组；因此，它也期望用户指出它的大小。然而，数组的大小可以在函数中计算，但是为了演示的目的，我需要一个接受两个参数的函数。不推荐使用前面的函数，因为它使用了 C 风格的数组；理想情况下，我们会使用其中一个 STL 容器。\n\n现在，让我们了解一下主要的函数代码。下面的代码用六个值声明并初始化 STL 数组容器，然后将这些值传递给我们在默认命名空间中定义的`sort()`函数:\n\n```cpp\n //Will invoke the non-template sort function\n array<int, 6> a = { 10, 50, 40, 30, 60, 20 };\n ::sort ( a );\n```\n\n前面的代码将调用非模板`sort()`函数。需要注意的一点是，每当 C++ 遇到函数调用时，它首先会寻找非模板版本；如果 C++ 找到匹配的非模板函数版本，它对正确函数定义的搜索就到此为止。如果 C++ 编译器无法识别与函数调用签名相匹配的非模板函数定义，那么它将开始寻找能够支持函数调用的任何模板函数，并为所需的数据类型实例化一个专用函数。\n\n让我们理解以下代码:\n\n```cpp\n//Will invoke the template function that takes a single argument\narray<float,6> b = { 10.6f, 57.9f, 80.7f, 35.1f, 69.3f, 20.0f };\n::sort<float,6>( b );\n```\n\n这将调用接收单个参数的模板函数。由于没有接收`array<float,6>`数据类型的非模板`sort()`函数，C++ 编译器将从我们的用户定义的`sort()`模板函数中实例化这样的函数，单个参数取`array<float, 6>`。\n\n同样，以下代码触发编译器实例化接收`array<double, 6>`的模板`sort()`函数的`double`版本:\n\n```cpp\n  //Will invoke the template function that takes a single argument\n array<double,6> c = { 10.6d, 57.9d, 80.7d, 35.1d, 69.3d, 20.0d };\n ::sort<double,6> ( c );\n```\n\n最后，下面的代码将实例化接收两个参数并调用函数的模板`sort()`的一个实例:\n\n```cpp\n //Will invoke the template function that takes two arguments\n double d[] = { 10.5d, 12.1d, 5.56d, 1.31d, 81.5d, 12.86d };\n ::sort<double> ( d, 6 );\n```\n\n如果您已经走了这么远，我相信您会喜欢到目前为止讨论的 C++ 模板主题。\n\n# 类别模板\n\nC++ 模板也将函数模板的概念扩展到类，并使我们能够编写面向对象的通用代码。在前几节中，您学习了函数模板和重载的使用。在这一节中，您将学习编写模板类，这些类将打开更有趣的泛型编程概念。\n\n`class`模板允许您通过模板类型表达式在类级别参数化数据类型。\n\n让我们用下面的例子来理解一个`class`模板:\n\n```cpp\n//myalgorithm.h\n#include <iostream>\n#include <algorithm>\n#include <array>\n#include <iterator>\nusing namespace std;\n\ntemplate <typename T, int size>\nclass MyAlgorithm {\n\npublic:\n        MyAlgorithm() { } \n        ~MyAlgorithm() { }\n\n        void sort( array<T, size> &data ) {\n             for ( int i=0; i<size; ++ i ) {\n                 for ( int j=0; j<size; ++ j ) {\n                     if ( data[i] < data[j] )\n                         swap ( data[i], data[j] );\n                 }\n             }\n        }\n\n        void sort ( T data[size] );\n\n};\n\ntemplate <typename T, int size>\ninline void MyAlgorithm<T, size>::sort ( T data[size] ) {\n       for ( int i=0; i<size; ++ i ) {\n           for ( int j=0; j<size; ++ j ) {\n               if ( data[i] < data[j] )\n                  swap ( data[i], data[j] );\n           }\n       }\n}\n```\n\nC++ template function overloading is a form of static or compile-time polymorphism.\n\n让我们在下面的`main.cpp`程序中使用`myalgorithm.h`如下:\n\n```cpp\n#include \"myalgorithm.h\"\n\nint main() {\n\n    MyAlgorithm<int, 10> algorithm1;\n\n    array<int, 10> a = { 10, 5, 15, 20, 25, 18, 1, 100, 90, 18 };\n\n    cout << \"\\nArray values before sorting ...\" << endl;\n    copy ( a.begin(), a.end(), ostream_iterator<int>(cout, \"\\t\") );\n    cout << endl;\n\n    algorithm1.sort ( a );\n\n    cout << \"\\nArray values after sorting ...\" << endl;\n    copy ( a.begin(), a.end(), ostream_iterator<int>(cout, \"\\t\") );\n    cout << endl;\n\n    MyAlgorithm<int, 10> algorithm2;\n    double d[] = { 100.0, 20.5, 200.5, 300.8, 186.78, 1.1 };\n\n    cout << \"\\nArray values before sorting ...\" << endl;\n    copy ( d.begin(), d.end(), ostream_iterator<double>(cout, \"\\t\") );\n    cout << endl;\n\n    algorithm2.sort ( d );\n\n    cout << \"\\nArray values after sorting ...\" << endl;\n    copy ( d.begin(), d.end(), ostream_iterator<double>(cout, \"\\t\") );\n    cout << endl;\n\n    return 0;  \n\n}\n```\n\n让我们使用以下命令快速编译程序:\n\n```cpp\ng++ main.cpp -std=c++ 17\n\n./a.out\n```\n\n输出如下:\n\n```cpp\n\nArray values before sorting ...\n10  5   15   20   25   18   1   100   90   18\n\nArray values after sorting ...\n1   5   10   15   18   18   20   25   90   100\n\nArray values before sorting ...\n100   20.5   200.5   300.8   186.78   1.1\n\nArray values after sorting ...\n1.1     20.5   100   186.78  200.5  300.8\n```\n\n# 代码走查\n\n下面的代码声明了一个类模板。关键字`template <typename T, int size>`可以用`<class T, int size>`代替。两个关键字在函数和类模板中都可以互换；但是，作为行业最佳实践，`template<class T>`只能与类模板一起使用，以避免混淆:\n\n```cpp\ntemplate <typename T, int size>\nclass MyAlgorithm \n```\n\n重载的`sort()`方法之一定义如下:\n\n```cpp\n void sort( array<T, size> &data ) {\n      for ( int i=0; i<size; ++ i ) {\n          for ( int j=0; j<size; ++ j ) {\n              if ( data[i] < data[j] )\n                 swap ( data[i], data[j] );\n          }\n      }\n } \n```\n\n第二个重载`sort()`函数只是在类范围内声明，没有任何定义，如下:\n\n```cpp\ntemplate <typename T, int size>\nclass MyAlgorithm {\n      public:\n           void sort ( T data[size] );\n};\n```\n\n前面的`sort()`函数是在类范围之外定义的，如下面的代码片段所示。奇怪的是，我们需要为类模板之外定义的每个成员函数重复模板参数:\n\n```cpp\ntemplate <typename T, int size>\ninline void MyAlgorithm<T, size>::sort ( T data[size] ) {\n       for ( int i=0; i<size; ++ i ) {\n           for ( int j=0; j<size; ++ j ) {\n               if ( data[i] < data[j] )\n                  swap ( data[i], data[j] );\n           }\n       }\n}\n```\n\n否则，类模板的概念与函数模板的概念相同。\n\nWould you like to see the compiler-instantiated code for templates? Use the **`g++ -fdump-tree-original main.cpp -std=c++ 17`** command.\n\n# 显式类专门化\n\n到目前为止，在本章中，您已经学习了如何使用函数模板和类模板进行泛型编程。当您理解类模板时，单个模板类可以支持任何内置和用户定义的数据类型。然而，有时我们需要用相对于其他数据类型的一些特殊处理来处理某些数据类型。在这种情况下，C++ 为我们提供了显式的类专门化支持，以区别对待的方式处理选择性数据类型。\n\n考虑 STL `deque`容器；虽然`deque`看起来很适合存储，比如说`string`、`int`、`double`和`long`，但是如果我们决定使用`deque`来存储一堆`boolean`类型，`bool`数据类型至少需要一个字节，尽管它可能会因编译器供应商的实现而异。虽然单个位可以有效地表示真或假，但布尔至少需要一个字节，即 8 位，其余 7 位不使用。这可能看起来好像没关系；然而，如果你必须储存大量的布尔值，这显然不是一个有效的想法，对吗？你可能会想，有什么大不了的？我们可以为`bool`编写另一个专业类或模板类。但是这种方法需要最终用户为不同的数据类型显式地使用不同的类，这听起来也不是一个好的设计，对吗？这正是 C++ 的显式类专门化派上用场的地方。\n\nThe explicit template specialization is also referred to as full-template specialization.\n\n如果你还没有被说服，不要紧；下面的示例将帮助您理解显式类专门化的必要性以及显式类专门化是如何工作的。\n\n让我们开发一个`DynamicArray`类来支持任何数据类型的动态数组。让我们从一个类模板开始，如下面的程序所示:\n\n```cpp\n#include <iostream>\n#include <deque>\n#include <algorithm>\n#include <iterator>\nusing namespace std;\n\ntemplate < class T >\nclass DynamicArray {\n      private:\n           deque< T > dynamicArray;\n           typename deque< T >::iterator pos;\n\n      public:\n           DynamicArray() { initialize(); }\n           ~DynamicArray() { }\n\n           void initialize() {\n                 pos = dynamicArray.begin();\n           }\n\n           void appendValue( T element ) {\n                 dynamicArray.push_back ( element );\n           }\n\n           bool hasNextValue() { \n                 return ( pos != dynamicArray.end() );\n           }\n\n           T getValue() {\n                 return *pos++ ;\n           }\n\n};\n```\n\n前面的`DynamicArray`模板类在内部使用了 STL `deque`类。因此，您可以将`DynamicArray`模板类视为自定义适配器容器。让我们通过下面的代码片段来探索如何在`main.cpp`中使用`DynamicArray`模板类:\n\n```cpp\n#include \"dynamicarray.h\"\n#include \"dynamicarrayforbool.h\"\n\nint main () {\n\n    DynamicArray<int> intArray;\n\n    intArray.appendValue( 100 );\n    intArray.appendValue( 200 );\n    intArray.appendValue( 300 );\n    intArray.appendValue( 400 );\n\n    intArray.initialize();\n\n    cout << \"\\nInt DynamicArray values are ...\" << endl;\n    while ( intArray.hasNextValue() )\n          cout << intArray.getValue() << \"\\t\";\n    cout << endl;\n\n    DynamicArray<char> charArray;\n    charArray.appendValue( 'H' );\n    charArray.appendValue( 'e' );\n    charArray.appendValue( 'l' );\n    charArray.appendValue( 'l' );\n    charArray.appendValue( 'o' );\n\n    charArray.initialize();\n\n    cout << \"\\nChar DynamicArray values are ...\" << endl;\n    while ( charArray.hasNextValue() )\n          cout << charArray.getValue() << \"\\t\";\n    cout << endl;\n\n    DynamicArray<bool> boolArray;\n\n    boolArray.appendValue ( true );\n    boolArray.appendValue ( false );\n    boolArray.appendValue ( true );\n    boolArray.appendValue ( false );\n\n    boolArray.initialize();\n\n    cout << \"\\nBool DynamicArray values are ...\" << endl;\n    while ( boolArray.hasNextValue() )\n         cout << boolArray.getValue() << \"\\t\";\n    cout << endl;\n\n    return 0;\n\n}\n```\n\n让我们使用以下命令快速编译程序:\n\n```cpp\ng++ main.cpp -std=c++ 17\n\n./a.out\n```\n\n输出如下:\n\n```cpp\nInt DynamicArray values are ...\n100   200   300   400\n\nChar DynamicArray values are ...\nH   e   l   l   o\n\nBool DynamicArray values are ...\n1   0   1   0\n```\n\n太好了。我们的定制适配器容器似乎工作正常。\n\n# 代码走查\n\n让我们放大，试着理解前面的程序是如何工作的。下面的代码告诉 C++ 编译器，接下来是一个类模板:\n\n```cpp\ntemplate < class T >\nclass DynamicArray {\n      private:\n           deque< T > dynamicArray;\n           typename deque< T >::iterator pos;\n```\n\n如您所见，`DynamicArray`类在内部使用了 STL `deque`，并且为`deque`声明了一个名为`pos`的迭代器。这个迭代器`pos`被`Dynamic`模板类用来提供高级方法，如`initialize()`、`appendValue()`、`hasNextValue()`和`getValue()`方法。\n\n`initialize()`方法将`deque`迭代器`pos`初始化为存储在`deque`中的第一个数据元素。`appendValue( T element )`方法允许您在`deque`的末尾添加一个数据元素。`hasNextValue()`方法告知`DynamicArray`类是否存储了更多的数据值- `true`表示它有更多的值，`false`表示`DynamicArray`导航已经到达`deque`的末尾。需要时可以使用`initialize()`方法将`pos`迭代器复位到起点。`getValue()`方法返回此时`pos`迭代器指向的数据元素。`getValue()`方法不进行任何验证；因此，在调用`getValue()`之前，它必须与`hasNextValue()`相结合，以安全地访问存储在`DynamicArray`中的值。\n\n现在，我们来了解一下`main()`功能。下面的代码声明了一个存储`int`数据类型的`DynamicArray`类；`DynamicArray<int> intArray`将触发 C++ 编译器实例化一个专门用于`int`数据类型的`DynamicArray`类:\n\n```cpp\nDynamicArray<int> intArray;\n\nintArray.appendValue( 100 );\nintArray.appendValue( 200 );\nintArray.appendValue( 300 );\nintArray.appendValue( 400 );\n```\n\n值`100`、`200`、`300`和`400`在`DynamicArray`类中背靠背存储。下面的代码确保`intArray`迭代器指向第一个元素。迭代器初始化后，存储在`DynamicArray`类中的值用`getValue()`方法打印，而`hasNextValue()`确保导航没有到达`DynamicArray`类的末尾:\n\n```cpp\nintArray.initialize();\ncout << \"\\nInt DynamicArray values are ...\" << endl;\nwhile ( intArray.hasNextValue() )\n      cout << intArray.getValue() << \"\\t\";\ncout << endl;\n```\n\n同样，在主函数中，创建一个`char DynamicArray`类，填充一些数据，并打印出来。让我们跳过`DynamicArray`直接进入储存`bool`的`DynamicArray`类。\n\n```cpp\nDynamicArray<bool> boolArray;\n\nboolArray.appendValue ( \"1010\" );\n\nboolArray.initialize();\n\ncout << \"\\nBool DynamicArray values are ...\" << endl;\n\nwhile ( boolArray.hasNextValue() )\n      cout << boolArray.getValue() << \"\\t\";\ncout << endl;\n```\n\n从前面的代码片段中，我们可以看到一切看起来都很好，对吗？是的，前面的代码工作得非常好；然而，`DynamicArray`设计方法存在性能问题。而`true`可以用`1`表示，`false`可以用`0`表示，只需要 1 位，前面的`DynamicArray`类用 8 位表示`1`，用 8 位表示`0`，我们必须修正这个问题，不要强迫终端用户选择一个对`bool`有效的不同的`DynamicArray`类。\n\n让我们通过使用显式类模板专门化来解决这个问题，代码如下:\n\n```cpp\n#include <iostream>\n#include <bitset>\n#include <algorithm>\n#include <iterator>\nusing namespace std;\n\ntemplate <>\nclass DynamicArray<bool> {\n      private:\n          deque< bitset<8> *> dynamicArray;\n          bitset<8> oneByte;\n          typename deque<bitset<8> * >::iterator pos;\n          int bitSetIndex;\n\n          int getDequeIndex () {\n              return (bitSetIndex) ? (bitSetIndex/8) : 0;\n          }\n      public:\n          DynamicArray() {\n              bitSetIndex = 0;\n              initialize();\n          }\n\n         ~DynamicArray() { }\n\n         void initialize() {\n              pos = dynamicArray.begin();\n              bitSetIndex = 0;\n         }\n\n         void appendValue( bool value) {\n              int dequeIndex = getDequeIndex();\n              bitset<8> *pBit = NULL;\n\n              if ( ( dynamicArray.size() == 0 ) || ( dequeIndex >= ( dynamicArray.size()) ) ) {\n                   pBit = new bitset<8>();\n                   pBit->reset();\n                   dynamicArray.push_back ( pBit );\n              }\n\n              if ( !dynamicArray.empty() )\n                   pBit = dynamicArray.at( dequeIndex );\n\n              pBit->set( bitSetIndex % 8, value );\n              ++ bitSetIndex;\n         }\n\n         bool hasNextValue() {\n              return (bitSetIndex < (( dynamicArray.size() * 8 ) ));\n         }\n\n         bool getValue() {\n              int dequeIndex = getDequeIndex();\n\n              bitset<8> *pBit = dynamicArray.at(dequeIndex);\n              int index = bitSetIndex % 8;\n              ++ bitSetIndex;\n\n              return (*pBit)[index] ? true : false;\n         }\n};\n```\n\n你注意到模板类声明了吗？模板类专门化的语法是`template <> class DynamicArray<bool> { };`。`class`模板表达式为空`<>`，适用于所有数据类型的`class`模板的名称和适用于`bool`数据类型的类的名称与模板表达式`<bool>`保持一致。\n\n如果仔细观察的话，`bool`的专用`DynamicArray`类内部使用了`deque< bitset<8> >`，也就是 8 位的`bitsets`的`deque`，需要时`deque`会自动分配更多的`bitset<8>`位。`bitset`变量是一个内存高效的 STL 容器，只消耗 1 位来表示`true`或`false`。\n\n我们来看看`main`功能:\n\n```cpp\n#include \"dynamicarray.h\"\n#include \"dynamicarrayforbool.h\"\n\nint main () {\n\n    DynamicArray<int> intArray;\n\n    intArray.appendValue( 100 );\n    intArray.appendValue( 200 );\n    intArray.appendValue( 300 );\n    intArray.appendValue( 400 );\n\n    intArray.initialize();\n\n    cout << \"\\nInt DynamicArray values are ...\" << endl;\n\n    while ( intArray.hasNextValue() )\n          cout << intArray.getValue() << \"\\t\";\n    cout << endl;\n\n    DynamicArray<char> charArray;\n\n    charArray.appendValue( 'H' );\n    charArray.appendValue( 'e' );\n    charArray.appendValue( 'l' );\n    charArray.appendValue( 'l' );\n    charArray.appendValue( 'o' );\n\n    charArray.initialize();\n\n    cout << \"\\nChar DynamicArray values are ...\" << endl;\n    while ( charArray.hasNextValue() )\n          cout << charArray.getValue() << \"\\t\";\n    cout << endl;\n\n    DynamicArray<bool> boolArray;\n\n    boolArray.appendValue ( true );\n    boolArray.appendValue ( false );\n    boolArray.appendValue ( true );\n    boolArray.appendValue ( false );\n\n    boolArray.appendValue ( true );\n    boolArray.appendValue ( false );\n    boolArray.appendValue ( true );\n    boolArray.appendValue ( false );\n\n    boolArray.appendValue ( true );\n    boolArray.appendValue ( true);\n    boolArray.appendValue ( false);\n    boolArray.appendValue ( false );\n\n    boolArray.appendValue ( true );\n    boolArray.appendValue ( true);\n    boolArray.appendValue ( false);\n    boolArray.appendValue ( false );\n\n    boolArray.initialize();\n\n    cout << \"\\nBool DynamicArray values are ...\" << endl;\n    while ( boolArray.hasNextValue() )\n          cout << boolArray.getValue() ;\n    cout << endl;\n\n    return 0;\n\n}\n```\n\n有了类模板专门化，我们可以从下面观察到主代码对于`bool`、`char`和`double`似乎是相同的，尽管主模板类、`DynamicArray`和专门化的`DynamicArray<bool>`类是不同的:\n\n```cpp\nDynamicArray<char> charArray;\ncharArray.appendValue( 'H' );\ncharArray.appendValue( 'e' );\n\ncharArray.initialize();\n\ncout << \"\\nChar DynamicArray values are ...\" << endl;\nwhile ( charArray.hasNextValue() )\ncout << charArray.getValue() << \"\\t\";\ncout << endl;\n\nDynamicArray<bool> boolArray;\nboolArray.appendValue ( true );\nboolArray.appendValue ( false );\n\nboolArray.initialize();\n\ncout << \"\\nBool DynamicArray values are ...\" << endl;\nwhile ( boolArray.hasNextValue() )\n      cout << boolArray.getValue() ;\ncout << endl;\n```\n\n我相信你会发现这个 C++ 模板专门化特性非常有用。\n\n# 部分模板专门化\n\n与显式模板专门化不同，显式模板专门化用它自己对特定数据类型的完整定义来替换主模板类，部分模板专门化允许我们专门化主模板类支持的模板参数的特定子集，而其他泛型类型可以与主模板类相同。\n\n当部分模板专门化与继承相结合时，它可以创造更多奇迹，如下例所示:\n\n```cpp\n#include <iostream>\nusing namespace std;\n\ntemplate <typename T1, typename T2, typename T3>\nclass MyTemplateClass {\npublic:\n     void F1( T1 t1, T2 t2, T3 t3 ) {\n          cout << \"\\nPrimary Template Class - Function F1 invoked ...\" << endl;\n          cout << \"Value of t1 is \" << t1 << endl;\n          cout << \"Value of t2 is \" << t2 << endl;\n          cout << \"Value of t3 is \" << t3 << endl;\n     }\n\n     void F2(T1 t1, T2 t2) {\n          cout << \"\\nPrimary Tempalte Class - Function F2 invoked ...\" << endl;\n          cout << \"Value of t1 is \" << t1 << endl;\n          cout << \"Value of t2 is \" << 2 * t2 << endl;\n     }\n};\n```\n\n```cpp\ntemplate <typename T1, typename T2, typename T3>\nclass MyTemplateClass< T1, T2*, T3*> : public MyTemplateClass<T1, T2, T3> {\n      public:\n          void F1( T1 t1, T2* t2, T3* t3 ) {\n               cout << \"\\nPartially Specialized Template Class - Function F1 invoked ...\" << endl;\n               cout << \"Value of t1 is \" << t1 << endl;\n               cout << \"Value of t2 is \" << *t2 << endl;\n               cout << \"Value of t3 is \" << *t3 << endl;\n          }\n};\n```\n\n`main.cpp`文件将包含以下内容:\n\n```cpp\n#include \"partiallyspecialized.h\"\n\nint main () {\n    int x = 10;\n    int *y = &x;\n    int *z = &x;\n\n    MyTemplateClass<int, int*, int*> obj;\n    obj.F1(x, y, z);\n    obj.F2(x, x);\n\n    return 0;\n}\n```\n\n从前面的代码中，您可能已经注意到主模板类名和部分专门化类名与完全或显式模板类专门化的情况相同。但是，模板参数表达式中有一些语法变化。在完全模板类专门化的情况下，模板参数表达式将为空，而在部分专门化的模板类的情况下，将出现列出的，如下所示:\n\n```cpp\ntemplate <typename T1, typename T2, typename T3>\nclass MyTemplateClass< T1, T2*, T3*> : public MyTemplateClass<T1, T2, T3> { };\n```\n\n表达式`template<typename T1, typename T2, typename T3>`是主类模板类中使用的模板参数表达式，`MyTemplateClass< T1, T2*, T3*>`是第二类完成的部分特化。如你所见，第二类对`typename T2`和`typename T3`做了一些特殊化，因为它们在第二类中被用作指针；然而，`typename T1`被用作是在第二类。\n\n除了到目前为止讨论的事实之外，第二个类还继承了主模板类，这有助于第二个类重用主模板类的公共和受保护的方法。然而，部分模板专门化并不能阻止专门化类支持其他功能。\n\n当主模板类的`F1`函数被部分专门化的模板类替换时，它通过继承重用主模板类的`F2`函数。\n\n让我们使用以下命令快速编译程序:\n\n```cpp\ng++ main.cpp -std=c++ 17\n\n./a.out\n```\n\n程序的输出如下:\n\n```cpp\nPartially Specialized Template Classs - Function F1 invoked ...\nValue of t1 is 10\nValue of t2 is 10\nValue of t3 is 10\n\nPrimary Tempalte Class - Function F2 invoked ...\nValue of t1 is 10\nValue of t2 is 20\n```\n\n我希望你发现部分专门化的模板类是有用的。\n\n# 摘要\n\n在本章中，您学习了以下内容:\n\n*   您现在知道了使用泛型编程的动机\n*   您现在已经熟悉了函数模板\n*   你知道如何霸王功能模板\n*   您知道类模板\n*   您知道什么时候使用显式模板专门化，什么时候使用部分专门化的模板专门化\n\n恭喜你！总的来说，你对 C++ 的模板编程有很好的理解。\n\n在下一章中，您将学习智能指针。"
  },
  {
    "path": "docs/master-cpp-prog/04.md",
    "content": "# 四、智能指针\n\n在前一章中，您学习了模板编程和泛型编程的好处。在本章中，您将了解以下智能指针主题:\n\n*   内存管理\n*   原始指针的问题\n*   循环依赖\n*   智能指针:\n    *   `auto_ptr`\n    *   `unique_ptr`\n    *   `shared_ptr`\n    *   `weak_ptr`\n\n让我们探索 C++ 提供的内存管理工具。\n\n# 内存管理\n\n在 C++ 中，内存管理通常是软件开发人员的责任。这是因为 C++ 标准没有在 C++ 编译器中强制垃圾收集支持；因此，这是留给编译器供应商的选择。例外的是，Sun C++ 编译器附带了一个名为`libgc`的垃圾收集库。\n\nC++ 语言有许多强大的特性。不用说，在这些特性中，指针是最强大、最有用的特性之一。说了这么多，指针非常有用，它们确实有自己奇怪的问题，因此必须负责任地使用它们。当内存管理不被重视或做得不太正确时，会导致许多问题，包括应用崩溃、核心转储、分段故障、调试问题的间歇性困难、性能问题等。悬空指针或流氓指针有时会干扰其他不相关的应用，而罪魁祸首应用会静默执行；事实上，受害者应用可能会受到多次指责。关于内存泄漏最糟糕的部分是，在某些时候，它变得非常棘手，甚至有经验的开发人员最终会调试受害者的代码无数个小时，而罪魁祸首的代码却没有被触及。有效的内存管理有助于避免内存泄漏，并允许您开发高性能的内存高效应用。\n\n由于每个操作系统的内存模型不同，对于相同的内存泄漏问题，每个操作系统在不同的时间点可能会有不同的行为。内存管理是一个很大的话题，C++ 提供了很多方法来做好它。我们将在接下来的章节中讨论一些有用的技术。\n\n# 原始指针的问题\n\n大多数 C++ 开发人员都有一个共同点:我们都喜欢编写复杂的代码。你问一个开发人员，“嘿，伙计，你是想重用已经存在并且有效的代码，还是想自己开发一个？”虽然在外交上，大多数开发人员会说尽可能重用已经存在的东西，但他们的内心会说，“我希望我能自己设计和开发它。”复杂的数据结构和算法往往需要指针。在遇到麻烦之前，使用原始指针真的很酷。\n\n原始指针在使用前必须分配内存，一旦完成就需要释放；就这么简单。然而，在一个产品中事情变得复杂，指针分配可能发生在一个地方，而释放可能发生在另一个地方。如果内存管理决策做得不正确，人们可能会认为释放内存是调用者或被调用者的责任，有时，这两个地方的内存都可能释放不出来。还有一种可能是，同一个指针在不同的地方被多次删除，这可能导致应用崩溃。如果这发生在 Windows 设备驱动程序中，它很可能会以死亡的蓝屏告终。\n\n试想一下，如果有一个应用异常，并且抛出异常的函数有一堆指针，这些指针在异常发生之前就已经被分配了内存，那该怎么办？谁都说不准:会有内存泄漏。\n\n让我们举一个利用原始指针的简单例子:\n\n```cpp\n#include <iostream>\nusing namespace std;\n\nclass MyClass {\n      public:\n           void someMethod() {\n\n                int *ptr = new int();\n                *ptr = 100;\n                int result = *ptr / 0;  //division by zero error expected\n                delete ptr;\n\n           }\n};\n\nint main ( ) {\n\n    MyClass objMyClass;\n    objMyClass.someMethod();\n\n    return 0;\n\n}\n```\n\n现在，运行以下命令:\n\n```cpp\ng++ main.cpp -g -std=c++ 17\n```\n\n检查这个程序的输出:\n\n```cpp\nmain.cpp: In member function ‘void MyClass::someMethod()’:\nmain.cpp:12:21: warning: division by zero [-Wdiv-by-zero]\n int result = *ptr / 0;\n```\n\n现在，运行以下命令:\n\n```cpp\n./a.out\n[1] 31674 floating point exception (core dumped) ./a.out\n```\n\nC++ 编译器真的很酷。看这条警告信息，它在指出问题方面敲了几下。我喜欢 Linux 操作系统。Linux 在发现行为不端的流氓应用方面相当聪明，它会在它们对其余应用或操作系统造成任何损害之前及时将其关闭。核心转储实际上是好的，尽管它被诅咒了，而不是庆祝 Linux 方法。你猜怎么着，微软的 Windows 操作系统也同样聪明。当他们发现一些应用进行可疑的内存访问时，他们会进行错误检查，并且视窗操作系统也支持迷你转储和完全转储，这相当于 Linux 操作系统中的核心转储。\n\n让我们看一下 Valgrind 工具的输出，以检查内存泄漏问题:\n\n```cpp\nvalgrind --leak-check=full --show-leak-kinds=all ./a.out\n\n==32857== Memcheck, a memory error detector\n==32857== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.\n==32857== Using Valgrind-3.12.0 and LibVEX; rerun with -h for copyright info\n==32857== Command: ./a.out\n==32857== \n==32857== \n==32857== Process terminating with default action of signal 8 (SIGFPE)\n==32857== Integer divide by zero at address 0x802D82B86\n==32857== at 0x10896A: MyClass::someMethod() (main.cpp:12)\n==32857== by 0x1088C2: main (main.cpp:24)\n==32857== \n==32857== HEAP SUMMARY:\n==32857== in use at exit: 4 bytes in 1 blocks\n==32857== total heap usage: 2 allocs, 1 frees, 72,708 bytes allocated\n==32857== \n==32857== 4 bytes in 1 blocks are still reachable in loss record 1 of 1\n==32857== at 0x4C2E19F: operator new(unsigned long) (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)\n==32857== by 0x108951: MyClass::someMethod() (main.cpp:8)\n==32857== by 0x1088C2: main (main.cpp:24)\n==32857== \n==32857== LEAK SUMMARY:\n==32857== definitely lost: 0 bytes in 0 blocks\n==32857== indirectly lost: 0 bytes in 0 blocks\n==32857== possibly lost: 0 bytes in 0 blocks\n==32857== still reachable: 4 bytes in 1 blocks\n==32857== suppressed: 0 bytes in 0 blocks\n==32857== \n==32857== For counts of detected and suppressed errors, rerun with: -v\n==32857== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)\n[1] 32857 floating point exception (core dumped) valgrind --leak-check=full --show-leak-kinds=all ./a.out\n```\n\n在这个输出中，如果你注意到文本的**粗体**部分，你会注意到 Valgrind 工具确实指出了导致这个核心转储的源代码行号。`main.cpp`文件第 12 行如下:\n\n```cpp\n int result = *ptr / 0; //division by zero error expected \n```\n\n当异常出现在`main.cpp`文件的第 12 行时，出现在异常下面的代码将永远不会被执行。在`main.cpp`文件的第 13 行，出现一个`delete`语句，由于异常，该语句将永远不会被执行:\n\n```cpp\n delete ptr;\n```\n\n分配给前面的原始指针的内存不会被释放，因为指针所指向的内存在堆栈展开过程中不会被释放。每当一个函数抛出异常，而该异常又不是由同一个函数处理时，栈展开就得到保证。然而，在堆栈展开过程中，只有自动局部变量会被清理，而不是指针所指向的内存。这会导致内存泄漏。\n\n这是使用原始指针带来的奇怪问题之一；还有许多其他类似的场景。希望你现在确信使用原始指针的刺激是有代价的。但是付出的代价并不值得，因为在 C++ 中有很好的替代方法来处理这个问题。你说得对，使用智能指针是一种解决方案，它提供了使用指针的好处，而无需支付原始指针的附加成本。\n\n因此，智能指针是在 C++ 中安全使用指针的方法。\n\n# 智能指针\n\n在 C++ 中，智能指针可以让您专注于手头的问题，将您从处理自定义垃圾收集技术的烦恼中解放出来。智能指针允许您安全地使用原始指针。他们负责清理原始指针使用的内存。\n\nC++ 支持多种类型的智能指针，可用于不同的场景:\n\n*   `auto_ptr`\n*   `unique_ptr`\n*   `shared_ptr`\n*   `weak_ptr`\n\n在 C++ 11 中引入了`auto_ptr`智能指针。`auto_ptr`智能指针有助于在堆内存超出范围时自动释放堆内存。然而，由于`auto_ptr`将所有权从一个`auto_ptr`实例转移到另一个实例的方式，它被否决了，而`unique_ptr`被引入作为它的替代。`shared_ptr`智能指针帮助多个共享智能指针引用同一个对象，并承担内存管理负担。当应用设计中存在循环依赖问题时，`weak_ptr`智能指针有助于解决因使用`shared_ptr`而出现的内存泄漏问题。\n\n还有其他类型的智能指针和相关的东西不常用，它们列在下面的项目符号列表中。但是，我强烈建议您自己探索它们，因为您永远不知道什么时候会发现它们有用:\n\n*   `owner_less`\n*   `enable_shared_from_this`\n*   `bad_weak_ptr`\n*   `default_delete` \n\n如果两个或多个智能指针共享相同的原始指向对象，则`owner_less`智能指针有助于比较它们。`enable_shared_from_this`智能指针有助于获得`this`指针的智能指针。`bad_weak_ptr`智能指针是一个异常类，意味着`shared_ptr`是使用无效的智能指针创建的。`default_delete`智能指针引用`unique_ptr`使用的默认销毁策略，调用`delete`语句，同时也支持使用`delete[]`的数组类型部分特殊化。\n\n在本章中，我们将逐一探讨`auto_ptr`、`shared_ptr`、`weak_ptr`和`unique-ptr`。\n\n# auto_ptr\n\n`auto_ptr`智能指针获取一个原始指针，将其包装，并确保每当`auto_ptr`对象超出范围时，原始指针指向的内存都会释放回来。任何时候，只有一个`auto_ptr`智能指针可以指向一个对象。因此，每当一个`auto_ptr`指针被分配给另一个`auto_ptr`指针时，所有权被转移到已经接收到该分配的`auto_ptr`实例；复制`auto_ptr`智能指针时也会发生同样的情况。\n\n用一个简单的例子来观察正在发生的事情会很有趣，如下所示:\n\n```cpp\n#include <iostream>\n#include <string>\n#include <memory>\n#include <sstream>\nusing namespace std;\n\nclass MyClass {\n      private:\n           static int count;\n           string name;\n      public:\n           MyClass() {\n                 ostringstream stringStream(ostringstream::ate);\n                 stringStream << \"Object\";\n                 stringStream << ++ count;\n                 name = stringStream.str();\n                 cout << \"\\nMyClass Default constructor - \" << name << endl;\n           }\n           ~MyClass() {\n                 cout << \"\\nMyClass destructor - \" << name << endl;\n           }\n\n           MyClass ( const MyClass &objectBeingCopied ) {\n                 cout << \"\\nMyClass copy constructor\" << endl;\n           }\n\n           MyClass& operator = ( const MyClass &objectBeingAssigned ) {\n                 cout << \"\\nMyClass assignment operator\" << endl;\n           }\n\n           void sayHello( ) {\n                cout << \"Hello from MyClass \" << name << endl;\n           }\n};\n\nint MyClass::count = 0;\n\nint main ( ) {\n\n   auto_ptr<MyClass> ptr1( new MyClass() );\n   auto_ptr<MyClass> ptr2( new MyClass() );\n\n   return 0;\n\n}\n```\n\n前面程序的编译输出如下:\n\n```cpp\ng++ main.cpp -std=c++ 17\n\nmain.cpp: In function ‘int main()’:\nmain.cpp:40:2: warning: ‘template<class> class std::auto_ptr’ is deprecated [-Wdeprecated-declarations]\n auto_ptr<MyClass> ptr1( new MyClass() );\n\nIn file included from /usr/include/c++/6/memory:81:0,\n from main.cpp:3:\n/usr/include/c++/6/bits/unique_ptr.h:49:28: note: declared here\n template<typename> class auto_ptr;\n\nmain.cpp:41:2: warning: ‘template<class> class std::auto_ptr’ is deprecated [-Wdeprecated-declarations]\n auto_ptr<MyClass> ptr2( new MyClass() );\n\nIn file included from /usr/include/c++/6/memory:81:0,\n from main.cpp:3:\n/usr/include/c++/6/bits/unique_ptr.h:49:28: note: declared here\n template<typename> class auto_ptr;\n```\n\n如您所见，C++ 编译器警告我们不推荐使用`auto_ptr`。因此，我不再推荐使用`auto_ptr`智能指针；它被`unique_ptr`取代。\n\n现在，我们可以忽略警告，继续前进，如下所示:\n\n```cpp\ng++ main.cpp -Wno-deprecated\n\n./a.out\n\nMyClass Default constructor - Object1\n\nMyClass Default constructor - Object2\n\nMyClass destructor - Object2\n\nMyClass destructor - Object1 \n```\n\n正如您在前面的程序输出中看到的，堆中分配的`Object1`和`Object2`都被自动删除了。这要归功于`auto_ptr`智能指针。\n\n# 代码演练-第 1 部分\n\n您可能已经从`MyClass`定义中了解到，它定义了默认的`constructor`、`copy`构造函数和析构函数、`assignment`运算符和`sayHello()`方法，如下所示:\n\n```cpp\n//Definitions removed here to keep it simple \nclass MyClass {\npublic:\n      MyClass() { }  //Default constructor\n      ~MyClass() { } //Destructor \n      MyClass ( const MyClass &objectBeingCopied ) {} //Copy Constructor \n      MyClass& operator = ( const MyClass &objectBeingAssigned ) { } //Assignment operator\n      void sayHello();\n}; \n```\n\n`MyClass`的方法只不过是一个打印语句，表示方法被调用了；它们纯粹是为了演示的目的。\n\n`main()`函数创建两个`auto_ptr`智能指针，指向两个不同的`MyClass`对象，如下所示:\n\n```cpp\nint main ( ) {\n\n   auto_ptr<MyClass> ptr1( new MyClass() );\n   auto_ptr<MyClass> ptr2( new MyClass() );\n\n   return 0;\n\n}\n```\n\n如您所知，`auto_ptr`是包装原始指针的本地对象，而不是指针。当控件命中`return`语句时，堆栈展开过程开始，作为此过程的一部分，堆栈对象，即`ptr1`和`ptr2`，被销毁。这又会调用`auto_ptr`的析构函数，最终删除堆栈对象`ptr1`和`ptr2`所指向的`MyClass`对象。\n\n我们还没有完全完成。让我们探索一下`auto_ptr`更多有用的功能，如下图`main`功能所示:\n\n```cpp\nint main ( ) {\n\n    auto_ptr<MyClass> ptr1( new MyClass() );\n    auto_ptr<MyClass> ptr2( new MyClass() );\n\n    ptr1->sayHello();\n    ptr2->sayHello();\n\n    //At this point the below stuffs happen\n    //1\\. ptr2 smart pointer has given up ownership of MyClass Object 2\n    //2\\. MyClass Object 2 will be destructed as ptr2 has given up its \n    //   ownership on Object 2\n    //3\\. Ownership of Object 1 will be transferred to ptr2\n    ptr2 = ptr1;\n\n    //The line below if uncommented will result in core dump as ptr1 \n    //has given up its ownership on Object 1 and the ownership of \n    //Object 1 is transferred to ptr2.\n    // ptr1->sayHello();\n\n    ptr2->sayHello();\n\n    return 0;\n\n}\n```\n\n# 代码演练-第 2 部分\n\n我们刚刚看到的`main()`函数代码展示了`auto_ptr`智能指针的许多有用的技术和一些有争议的行为。下面的代码创建了两个`auto_ptr`实例，即`ptr1`和`ptr2`，它们将两个创建的`MyClass`对象包装在一个堆中:\n\n```cpp\n auto_ptr<MyClass> ptr1( new MyClass() );\n auto_ptr<MyClass> ptr2( new MyClass() );\n```\n\n接下来，下面的代码演示了如何使用`auto_ptr`调用`MyClass`支持的方法:\n\n```cpp\n ptr1->sayHello();\n ptr2->sayHello();\n```\n\n希望你遵守`ptr1->sayHello()`声明。它会让你相信`auto_ptr` `ptr1`对象是指针，但实际上，`ptr1`和`ptr2`只是作为局部变量在栈中创建的`auto_ptr`对象。由于`auto_ptr`类重载了`->`指针操作符和`*`解引用操作符，它看起来像一个指针。事实上，`MyClass`暴露的所有方法只能使用`->`指针操作符访问，而所有`auto_ptr`方法都可以像您经常访问堆栈对象一样访问。\n\n下面的代码演示了`auto_ptr`智能指针的内部行为，请密切关注；这将会非常有趣:\n\n```cpp\nptr2 = ptr1;\n```\n\n看起来前面的代码是一个简单的`assignment`语句，但是它触发了`auto_ptr`中的许多活动。由于前述`assignment`声明，发生了以下活动:\n\n*   `ptr2`智能指针将放弃`MyClass`对象 2 的所有权。\n*   `MyClass`对象 2 将被销毁，因为`ptr2`已经放弃了对`object 2`的所有权。\n*   `object 1`的所有权将转移给`ptr2`。\n*   此时，`ptr1`既不指向`object 1`，也不负责管理`object 1`使用的内存。\n\n以下评论行有一些事实要告诉你:\n\n```cpp\n// ptr1->sayHello();\n```\n\n由于`ptr1`智能指针已经释放了对`object 1`的所有权，试图访问`sayHello()`方法是非法的。这是因为`ptr1`在现实中不再指向`object 1`，而`object 1`归`ptr2`所有。当`ptr2`超出范围时，`ptr2`智能指针负责释放`object 1`使用的内存。如果前面的代码没有注释，将导致核心转储。\n\n最后，下面的代码让我们使用`ptr2`智能指针调用`object 1`上的`sayHello()`方法:\n\n```cpp\nptr2->sayHello();\nreturn 0;\n```\n\n我们刚才看到的`return`语句将在`main()`功能中启动堆栈展开过程。这将最终调用`ptr2`的析构函数，反过来将释放`object 1`使用的内存。美在于这一切都是自动发生的。当我们专注于手头的问题时，`auto_ptr`智能指针在幕后为我们努力工作。\n\n但是，由于以下原因，`C++ 11`以后不推荐使用`auto_ptr`:\n\n*   `auto_ptr`对象不能存储在 STL 容器中\n*   `auto_ptr`复制构造函数将从原始源中移除所有权，即``auto_ptr``\n*   `auto_ptr`副本`assignment`操作者将从原始来源，即`auto_ptr`中移除所有权\n*   `auto_ptr`违背了复制构造函数和`assignment`运算符的初衷，因为`auto_ptr`复制构造函数和`assignment`运算符将从右侧对象中移除源对象的所有权，并将所有权分配给左侧对象\n\n# S7-1200 可编程控制器\n\n`unique_ptr`智能指针的工作方式与`auto_ptr`完全相同，只是`unique_ptr`解决了`auto_ptr`引入的问题。因此，`unique_ptr`是`auto_ptr`的替代品，从`C++ 11`开始。`unique_ptr`智能指针只允许一个智能指针独占一个堆分配的对象。从一个`unique_ptr`实例到另一个实例的所有权转移只能通过`std::move()`功能完成。\n\n因此，让我们重构前面的例子，用`unique_ptr`代替`auto_ptr`。\n\n重构的代码示例如下:\n\n```cpp\n#include <iostream>\n#include <string>\n#include <memory>\n#include <sstream>\nusing namespace std;\n\nclass MyClass {\n      private:\n          static int count;\n          string name;\n\n      public:\n          MyClass() {\n                ostringstream stringStream(ostringstream::ate);\n                stringStream << \"Object\";\n                stringStream << ++ count;\n                name = stringStream.str();\n                cout << \"\\nMyClass Default constructor - \" << name << endl;\n          }\n\n          ~MyClass() {\n                cout << \"\\nMyClass destructor - \" << name << endl;\n          }\n\n          MyClass ( const MyClass &objectBeingCopied ) {\n                cout << \"\\nMyClass copy constructor\" << endl;\n          }\n\n          MyClass& operator = ( const MyClass &objectBeingAssigned ) {\n                cout << \"\\nMyClass assignment operator\" << endl;\n          }\n\n          void sayHello( ) {\n                cout << \"\\nHello from MyClass\" << endl;\n          }\n\n};\n\nint MyClass::count = 0;\n\nint main ( ) {\n\n unique_ptr<MyClass> ptr1( new MyClass() );\n unique_ptr<MyClass> ptr2( new MyClass() );\n\n ptr1->sayHello();\n ptr2->sayHello();\n\n //At this point the below stuffs happen\n //1\\. ptr2 smart pointer has given up ownership of MyClass Object 2\n //2\\. MyClass Object 2 will be destructed as ptr2 has given up its \n // ownership on Object 2\n //3\\. Ownership of Object 1 will be transferred to ptr2\n ptr2 = move( ptr1 );\n\n //The line below if uncommented will result in core dump as ptr1 \n //has given up its ownership on Object 1 and the ownership of \n //Object 1 is transferred to ptr2.\n // ptr1->sayHello();\n\n ptr2->sayHello();\n\n return 0;\n}\n```\n\n前面程序的输出如下:\n\n```cpp\ng++ main.cpp -std=c++ 17\n\n./a.out\n\nMyClass Default constructor - Object1\n\nMyClass Default constructor - Object2\n\nMyClass destructor - Object2\n\nMyClass destructor - Object1 \n```\n\n在前面的输出中，您可以注意到编译器没有报告任何警告，程序的输出与`auto_ptr`相同。\n\n# 代码走查\n\n注意`auto_ptr`和`unique_ptr`之间`main()`功能的差异很重要。让我们来看看`main()`功能，如下面的代码所示。这段代码创建了两个`unique_ptr`实例，即`ptr1`和`ptr2`，它们包装了堆中创建的两个`MyClass`对象:\n\n```cpp\n unique_ptr<MyClass> ptr1( new MyClass() );\n unique_ptr<MyClass> ptr2( new MyClass() );\n```\n\n接下来，下面的代码演示了如何使用`unique_ptr`调用`MyClass`支持的方法:\n\n```cpp\n ptr1->sayHello();\n ptr2->sayHello();\n```\n\n就像`auto_ptr`一样，`unique_ptr`智能指针`ptr1`对象重载了`->`指针操作符和`*`解引用操作符；因此，它看起来像一个指针。\n\n下面的代码演示了`unique_ptr`不支持将一个`unique_ptr`实例分配给另一个实例，所有权转移只能通过`std::move()`功能实现:\n\n```cpp\nptr2 = std::move(ptr1);\n```\n\n`move`功能触发以下活动:\n\n*   `ptr2`智能指针放弃`MyClass`对象 2 的所有权\n*   `MyClass`对象 2 被破坏，因为`ptr2`放弃了对`object 2`的所有权\n*   `object 1`的所有权转移至`ptr2`\n*   此时，`ptr1`既不指向`object 1`，也不负责管理`object 1`使用的内存\n\n以下代码如果未注释，将导致核心转储:\n\n```cpp\n// ptr1->sayHello();\n```\n\n最后，下面的代码让我们使用`ptr2`智能指针调用`object 1`上的`sayHello()`方法:\n\n```cpp\nptr2->sayHello();\nreturn 0;\n```\n\n我们刚才看到的`return`语句将在`main()`功能中启动堆栈展开过程。这将最终调用`ptr2`的析构函数，反过来将释放`object 1`使用的内存。注意`unique_ptr`对象可以存储在 STL 容器中，不像`auto_ptr`对象。\n\n# 共享 _ptr\n\n当一组`shared_ptr`对象共享堆分配对象的所有权时，使用`shared_ptr`智能指针。当使用共享对象完成所有`shared_ptr`实例时，`shared_ptr`指针释放共享对象。`shared_ptr`指针使用引用计数机制来检查对共享对象的总引用；每当引用计数变为零时，最后一个`shared_ptr`实例将删除共享对象。\n\n我们通过一个例子来看看`shared_ptr`的用法，如下:\n\n```cpp\n#include <iostream>\n#include <string>\n#include <memory>\n#include <sstream>\nusing namespace std;\n\nclass MyClass {\n  private:\n    static int count;\n    string name;\n  public:\n    MyClass() {\n      ostringstream stringStream(ostringstream::ate);\n      stringStream << \"Object\";\n      stringStream << ++ count;\n\n      name = stringStream.str();\n\n      cout << \"\\nMyClass Default constructor - \" << name << endl;\n    }\n\n    ~MyClass() {\n      cout << \"\\nMyClass destructor - \" << name << endl;\n    }\n\n    MyClass ( const MyClass &objectBeingCopied ) {\n      cout << \"\\nMyClass copy constructor\" << endl;\n    }\n\n    MyClass& operator = ( const MyClass &objectBeingAssigned ) {\n      cout << \"\\nMyClass assignment operator\" << endl;\n    }\n\n    void sayHello() {\n      cout << \"Hello from MyClass \" << name << endl;\n    }\n\n};\n\nint MyClass::count = 0;\n\nint main ( ) {\n\n  shared_ptr<MyClass> ptr1( new MyClass() );\n  ptr1->sayHello();\n  cout << \"\\nUse count is \" << ptr1.use_count() << endl;\n\n  {\n      shared_ptr<MyClass> ptr2( ptr1 );\n      ptr2->sayHello();\n      cout << \"\\nUse count is \" << ptr2.use_count() << endl;\n  }\n\n  shared_ptr<MyClass> ptr3 = ptr1;\n  ptr3->sayHello();\n  cout << \"\\nUse count is \" << ptr3.use_count() << endl;\n\n  return 0;\n}\n```\n\n前面程序的输出如下:\n\n```cpp\nMyClass Default constructor - Object1\nHello from MyClass Object1\nUse count is 1\n\nHello from MyClass Object1\nUse count is 2\n\nNumber of smart pointers referring to MyClass object after ptr2 is destroyed is 1\n\nHello from MyClass Object1\nUse count is 2\n\nMyClass destructor - Object1\n```\n\n# 代码走查\n\n下面的代码创建了一个指向`MyClass`堆分配对象的`shared_ptr`对象的实例。就像其他智能指针一样，`shared_ptr`也有过载的`->`和`*`操作符。因此，所有`MyClass`对象方法都可以像使用原始指针一样被调用。`use_count()`方法告诉引用共享对象的智能指针的数量:\n\n```cpp\n shared_ptr<MyClass> ptr1( new MyClass() );\n ptr1->sayHello();\n cout << \"\\nNumber of smart pointers referring to MyClass object is \"\n      << ptr1->use_count() << endl;\n```\n\n在下面的代码中，智能指针`ptr2`的范围被包装在花括号包围的块中。因此，`ptr2`将在下面的代码块末尾被销毁。代码块中预期的`use_count`函数是 2:\n\n```cpp\n { \n      shared_ptr<MyClass> ptr2( ptr1 );\n      ptr2->sayHello();\n      cout << \"\\nNumber of smart pointers referring to MyClass object is \"\n           << ptr2->use_count() << endl;\n }\n```\n\n在下面的代码中，预期的`use_count`值为 1，因为`ptr2`将被删除，这将使参考计数减少 1:\n\n```cpp\n cout << \"\\nNumber of smart pointers referring to MyClass object after ptr2 is destroyed is \"\n << ptr1->use_count() << endl; \n```\n\n下面的代码将打印一条 Hello 消息，后面跟着`use_count`作为 2。这是因为`ptr1`和`ptr3`现在引用堆中的`MyClass`共享对象:\n\n```cpp\nshared_ptr<MyClass> ptr3 = ptr2;\nptr3->sayHello();\ncout << \"\\nNumber of smart pointers referring to MyClass object is \"\n     << ptr2->use_count() << endl;\n```\n\n`main`功能结束时的`return 0;`语句将破坏`ptr1`和`ptr3`，将参考计数减少到零。因此，我们可以观察到`MyClass`析构函数在输出的末尾打印语句。\n\n# 弱 _ptr\n\n到目前为止，我们已经用例子讨论了`shared_ptr`的积极一面。然而，当应用设计中存在循环依赖时，`shared_ptr`无法清理内存。要么必须重构应用设计以避免循环依赖，要么我们可以利用`weak_ptr`来解决循环依赖问题。\n\nYou can check out my YouTube channel to understand the `shared_ptr` issue and how it can be resolved with `weak_ptr`: [https://www.youtube.com/watch?v=SVTLTK5gbDc](https://www.youtube.com/watch?v=SVTLTK5gbDc).\n\n假设有三个类:A、B 和 C，类 A 和 B 有一个 C 的实例，而 C 有一个 A 和 B 的实例，这里有一个设计问题。甲依赖丙，丙也依赖甲。同样，乙依赖丙，丙也依赖乙。\n\n考虑以下代码:\n\n```cpp\n#include <iostream>\n#include <string>\n#include <memory>\n#include <sstream>\nusing namespace std;\n\nclass C;\n\nclass A {\n      private:\n           shared_ptr<C> ptr;\n      public:\n           A() {\n                 cout << \"\\nA constructor\" << endl;\n           }\n\n           ~A() {\n                 cout << \"\\nA destructor\" << endl;\n           }\n\n           void setObject ( shared_ptr<C> ptr ) {\n                this->ptr = ptr;\n           }\n};\n\nclass B {\n      private:\n           shared_ptr<C> ptr;\n      public:\n           B() {\n                 cout << \"\\nB constructor\" << endl;\n           }\n\n           ~B() {\n                 cout << \"\\nB destructor\" << endl;\n           }\n\n           void setObject ( shared_ptr<C> ptr ) {\n                this->ptr = ptr;\n           }\n};\n\nclass C {\n      private:\n           shared_ptr<A> ptr1;\n           shared_ptr<B> ptr2;\n      public:\n           C(shared_ptr<A> ptr1, shared_ptr<B> ptr2) {\n                   cout << \"\\nC constructor\" << endl;\n                   this->ptr1 = ptr1;\n                   this->ptr2 = ptr2;\n           }\n\n           ~C() {\n                   cout << \"\\nC destructor\" << endl;\n           }\n};\n\nint main ( ) {\n                shared_ptr<A> a( new A() );\n                shared_ptr<B> b( new B() );\n                shared_ptr<C> c( new C( a, b ) );\n\n                a->setObject ( shared_ptr<C>( c ) );\n                b->setObject ( shared_ptr<C>( c ) );\n\n                return 0;\n}\n```\n\n前面程序的输出如下:\n\n```cpp\ng++ problem.cpp -std=c++ 17\n\n./a.out\n\nA constructor\n\nB constructor\n\nC constructor\n```\n\n在前面的输出中，您可以观察到，即使我们使用了`shared_ptr`，对象 A、B 和 C 使用的内存也从未被释放。这是因为我们没有看到各个类的析构函数被调用。其原因是`shared_ptr`内部利用引用计数算法来决定共享对象是否需要析构。但是，它在这里失败了，因为除非删除对象 C，否则不能删除对象 A。除非删除对象 A，否则不能删除对象 C。另外，除非删除对象 A 和对象 B，否则不能删除对象 C。同样，除非删除对象 C，否则不能删除对象 A，除非删除对象 C，否则不能删除对象 B。\n\n底线是这是一个循环依赖设计问题。为了解决这个问题，从 C++ 11 开始，C++ 引入了`weak_ptr`。`weak_ptr`智能指针不是强引用。因此，与`shared_ptr`不同的是，所引用的对象可以在任何时候被删除。\n\n# 循环依赖\n\n循环依赖是对象 A 依赖于 B，对象 B 依赖于 A 时出现的问题，现在让我们看看如何通过`shared_ptr`和`weak_ptr`的组合来解决这个问题，最终打破循环依赖，如下所示:\n\n```cpp\n#include <iostream>\n#include <string>\n#include <memory>\n#include <sstream>\nusing namespace std;\n\nclass C;\n\nclass A {\n      private:\n weak_ptr<C> ptr;\n      public:\n           A() {\n                  cout << \"\\nA constructor\" << endl;\n           }\n\n           ~A() {\n                  cout << \"\\nA destructor\" << endl;\n           }\n\n           void setObject ( weak_ptr<C> ptr ) {\n                  this->ptr = ptr;\n           }\n};\n\nclass B {\n      private:\n weak_ptr<C> ptr;\n      public:\n           B() {\n               cout << \"\\nB constructor\" << endl;\n           }\n\n           ~B() {\n               cout << \"\\nB destructor\" << endl;\n           }\n\n           void setObject ( weak_ptr<C> ptr ) {\n                this->ptr = ptr;\n           }\n};\n\nclass C {\n      private:\n           shared_ptr<A> ptr1;\n           shared_ptr<B> ptr2;\n      public:\n           C(shared_ptr<A> ptr1, shared_ptr<B> ptr2) {\n                   cout << \"\\nC constructor\" << endl;\n                   this->ptr1 = ptr1;\n                   this->ptr2 = ptr2;\n           }\n\n           ~C() {\n                   cout << \"\\nC destructor\" << endl;\n           }\n};\n\nint main ( ) {\n         shared_ptr<A> a( new A() );\n         shared_ptr<B> b( new B() );\n         shared_ptr<C> c( new C( a, b ) );\n\n         a->setObject ( weak_ptr<C>( c ) );\n         b->setObject ( weak_ptr<C>( c ) );\n\n         return 0;\n}\n```\n\n前面重构代码的输出如下:\n\n```cpp\ng++ solution.cpp -std=c++ 17\n\n./a.out\n\nA constructor\n\nB constructor\n\nC constructor\n\nC destructor\n\nB destructor\n\nA destructor\n```\n\n# 摘要\n\n在本章中，您了解了\n\n*   由于原始指针引起的内存泄漏问题\n*   `auto_ptr`关于赋值和复制构造函数的问题\n*   `unique_ptr`它的优势\n*   `shared_ptr`在内存管理中的作用及其与循环依赖相关的局限性。\n*   您还可以通过`weak_ptr`解决循环依赖问题\n\n在下一章中，您将学习如何用 C++ 开发图形用户界面应用。"
  },
  {
    "path": "docs/master-cpp-prog/05.md",
    "content": "# 五、使用 C++ 开发图形用户界面应用\n\n在本章中，您将学习以下主题:\n\n*   Qt 概述\n*   Qt 框架\n*   在 Ubuntu 上安装 Qt\n*   开发 Qt 核心应用\n*   开发 Qt 图形用户界面应用\n*   在 Qt 图形用户界面应用中使用布局\n*   理解事件处理的信号和槽\n*   在 Qt 应用中使用多个布局\n\nQt 是用 C++ 开发的跨平台应用框架。各种平台都支持，包括 Windows、Linux、Mac OS、安卓、iOS、嵌入式 Linux、QNX、VxWorks、Windows CE/RT、Integrity、Wayland、X11、嵌入式设备等等。主要用作**人机界面** ( **人机界面**)或**图形用户界面** ( **图形用户界面**)框架；但是，它也用于开发一个**命令行界面** ( **CLI** )应用。Qt 的正确发音方式是*可爱*。Qt 应用框架有两种风格:开源和商业许可。\n\nQt 是最初的开发者 Haavard Nord 和 Eirik Chambe-Eng 的想法，他们早在 1991 年就开发了它。\n\n由于 C++ 语言本身不支持图形用户界面，您一定已经猜到 C++ 语言中没有现成的事件管理支持。因此，Qt 需要支持它自己的事件处理机制，这导致了信号和槽技术。在引擎盖下，信号和插槽使用**观察者设计模式**，允许 Qt 对象相互对话。这听起来是不是太难理解了？不用担心！信号只不过是事件，如按钮点击或窗口关闭，而槽是事件处理程序，可以按照您希望的方式对这些事件做出响应。\n\n为了让我们在 Qt 应用开发方面的生活更轻松，Qt 支持各种宏和 Qt 特定的关键字。由于 C++ 无法理解这些关键词，Qt 必须将它们和宏翻译成纯 C++ 代码，这样 C++ 编译器才能照常工作。为了以更流畅的方式实现这一点，Qt 支持名为**元对象编译器**的东西，也称为 **moc** 。\n\nQt 是 C++ 项目的自然选择，因为它是彻头彻尾的 C++ 代码；因此，作为一名 C++ 开发人员，当您在应用中使用 Qt 时，您会有宾至如归的感觉。典型的应用既有复杂的逻辑，又有令人印象深刻的用户界面。在小型产品团队中，通常一个开发人员做多项工作，这有好有坏。\n\n一般来说，专业的开发人员都有很好的解决问题的能力。解决问题的技能是以最佳方式解决一个复杂问题所必需的，要有好的数据结构和算法选择。\n\n开发一个令人印象深刻的用户界面需要创造性的设计技能。虽然有数不清的开发人员擅长解决问题或创造性的用户界面设计，但并不是所有的开发人员都擅长这两者。这就是 Qt 脱颖而出的地方。\n\n比方说，一家初创公司想要为他们的内部目的开发一个应用。为此，一个简单的图形用户界面应用就足够了，一个漂亮的人机界面/图形用户界面可能对团队有用，因为该应用只用于内部目的。在这种情况下，整个应用可以用 C++ 和 Qt Widgets 框架开发。唯一的前提是开发团队必须精通 C++。\n\n然而，在必须开发移动应用的情况下，令人印象深刻的人机界面就成了强制性的。同样，移动应用可以用 C++ 和 Qt Widgets 开发。但是现在这个选择有两个部分。好的一面是，移动应用团队必须只擅长 C++。这种选择不好的地方在于，不能保证所有优秀的 C++ 开发人员都会擅长设计移动应用的 HMI/GUI。\n\n让我们假设团队中有一两个专门的 Photoshop 专业人员，他们擅长创建可以在 GUI 中使用的朗朗上口的图像，还有一两个 UI 设计师，他们可以用 Photoshop 专家创建的图像制作出令人印象深刻的 HMI/GUI。通常，用户界面设计者擅长前端技术，如 JavaScript、HTML 和 CSS。复杂的业务逻辑可以在强大的 Qt 框架中开发，而 HMI/GUI 可以在 QML 开发。\n\nQML 是一种声明性脚本语言，与 Qt 应用框架一起出现。它接近于 JavaScript，并且有特定于 Qt 的扩展。它有利于应用的快速开发，并允许 UI 设计人员专注于 HMI/GUI，C++ 开发人员专注于可以在 Qt Framework 中开发的复杂业务逻辑。\n\n由于 C++ Qt 框架和 QML 都是同一个 Qt 应用框架的一部分，它们无缝地结合在一起。\n\nQt 是一个庞大而强大的框架；因此，本章将重点介绍 Qt 的基本要素，帮助您开始使用 Qt。如果你想了解更多，你可能想看看我正在写的另一本即将出版的书，即《掌握 Qt》和《QML 编程》。\n\n# 夸脱\n\nQt 框架是用 C++ 开发的，因此对于任何优秀的 C++ 开发人员来说，这都是小菜一碟。它支持命令行界面和基于图形用户界面的应用开发。在撰写本章时，Qt 应用框架的最新版本是 Qt 5.7.0。当你读这本书的时候，可能会有不同版本的 Qt 可供你下载。可以从 [https://www.qt.io](https://www.qt.io) 下载最新版本。\n\n# 在 Ubuntu 16.04 中安装 Qt 5.7.0\n\n在本章中，我将使用 Ubuntu 16.04 操作系统；但是，本章列出的程序应该可以在任何支持 Qt 的平台上运行。\n\n详细安装说明参见[https://wiki.qt.io/install_Qt_5_on_Ubuntu](https://wiki.qt.io/install_Qt_5_on_Ubuntu)。\n\n此时，您的系统上应该安装了一个 C++ 编译器。如果不是这样，首先确保您安装了一个 C++ 编译器，如下所示:\n\n```cpp\nsudo apt-get install build-essential\n```\n\n从 Ubuntu 终端，您应该能够下载 Qt 5.7.0，如以下命令所示:\n\n```cpp\nw**get** **http://download.qt.io/official_releases/qt/5.7/5.7.0/qt-\nopensource-linux-x64-5.7.0.run** \n```\n\n向下载的安装程序提供执行权限，如以下命令所示:\n\n```cpp\nchmod +x qt-opensource-linux-x64-5.7.0.run \n```\n\nI strongly recommend that you install Qt along with its source code. You can get help directly from the source code if you prefer to look up Qt Help the geeky way.\n\n启动安装程序，如以下命令所示:\n\n```cpp\n./qt-opensource-linux-x64-5.7.0.run\n```\n\n由于 Qt 使用 OpenGL，在开始用 Qt 编写第一个程序之前，请确保安装了以下内容。要安装`libfontconfig1`，运行以下命令:\n\n```cpp\n sudo apt-get install libfontconfig1\n```\n\n要安装`mesa-common-dev`，运行以下命令:\n\n```cpp\nsudo apt-get install mesa-common-dev  \n```\n\n此时，您应该有一个工作正常的 Qt 设置。您可以通过在 Linux 终端中发出以下命令来验证安装:\n\n![](img/00009.jpeg)\n\nFigure 5.1\n\n如果无法识别`qmake`命令，请确保导出 Qt 安装文件夹的`bin`路径，如前面的截图所示。此外，创建一个软链接可能也很有用。该命令如下:\n\n```cpp\n sudo ln -s /home/jegan/Qt5.7.0/5.7/gcc_64/bin/qmake /usr/bin/qmake  \n```\n\nQt 安装在您的系统上的路径可能与我的不同，因此请相应地替换 Qt 路径。\n\n# Qt 核心\n\nQt Core 是 Qt 支持的模块之一。这个模块有很多有用的类，比如`QObject`、`QCoreApplication`、`QDebug`等等。几乎每个 Qt 应用都需要这个模块，因此它们由 Qt 框架隐式链接。每个 Qt 类都继承自`QObject`，而`QObject`类为 Qt 应用提供事件处理支持。`QObject`是支撑事件处理机制的关键件；有趣的是，即使是基于控制台的应用也可以支持 Qt 中的事件处理。\n\n# 编写我们的第一个 Qt 控制台应用\n\n如果你得到一个类似于*图 5.1* 所示的输出，你的手就都脏了。让我们编写第一个 Qt 应用，如下图所示:\n\n**![](img/00010.jpeg)** Figure 5.2\n\n在第一行中，我们包含了来自 **QtCore** 模块的 QDebug 头。如果仔细观察，`qDebug()`函数类似于 C++ `cout ostream`运算符。当你在调试代码时，`qDebug()`函数将成为你在 Qt 世界中的好朋友。`QDebug`类重载了 C++ `ostream`运算符，以便增加对 C++ 编译器不支持的 Qt 数据类型的支持。\n\n按照老派的方式，我有点痴迷于终端在编码的同时实现几乎任何事情，而不是使用一些花哨的**集成开发环境** ( **IDEs** )。你可能喜欢也可能讨厌这种方法，这很自然。好的一面是，你和 Qt/C++ 之间不会有任何障碍，因为你将使用简单明了但功能强大的文本编辑器，如 Vim、Emacs、Sublime Text、Atom、括号或 Neovim，所以你将学习几乎所有关于 Qt 项目和 qmake 如何工作的基本知识；IDEs 让你的生活变得轻松，但它们隐藏了许多每个认真的开发人员都必须知道的基本东西。所以这是一个交易。我让你来决定是否使用你最喜欢的纯文本编辑器或 Qt Creator ide 或任何其他花哨的 IDE。我将继续使用名为 Neovim 的重构 Vim 编辑器，它看起来真的很酷。*图 5.2* 会让你对 Neovim 编辑器的观感有所了解。\n\n让我们回到正题。让我们看看如何以令人讨厌的方式在命令行中编译这段代码。在此之前，你可能想了解一下 qmake 工具。这是 Qt 专有的`make`工具。`qmake`实用程序只不过是一个制造工具，但是它知道 Qt 特有的东西，所以它知道 moc、信号、插槽等等，而这是典型的`make`实用程序所不知道的。\n\n以下命令将帮助您创建一个`.pro`文件。`.pro`文件的名称将由`qmake`实用程序根据项目文件夹名称决定。`.pro`文件是 Qt Creator IDE 将相关文件组合为单个项目的方式。由于我们不打算使用 Qt Creator，我们将使用`.pro`文件来创建`Makefile`，以便像普通的 C++ 项目一样编译我们的 Qt 项目。\n\n![](img/00011.jpeg)\n\nFigure 5.3\n\n当您发出`qmake -project`命令时，qmake 将扫描当前文件夹和当前文件夹下的所有子文件夹，并将头文件和源文件包含在`Ex1.pro`中。顺便说一下，`.pro`文件是一个纯文本文件，可以使用任何文本编辑器打开，如图*图 5.4* :\n\n![](img/00012.jpeg)\n\nFigure 5.4\n\n现在是以`Ex1.pro`为输入文件创建`Makefile`的时候了。由于`Ex1.pro`文件存在于当前目录中，我们不必显式提供`Ex1.pro`作为自动生成`Makefile`的输入文件。这个想法是，一旦我们有了一个`.pro`文件，我们所需要做的就是从`.pro`文件发布命令生成`Makefile`:`qmake`。这将实现为您的项目创建一个完整的`Makefile`的所有魔力，您可以使用`make`实用程序来构建您的项目，如下图所示:\n\n![](img/00013.jpeg)\n\nFigure 5.5\n\n这是我们一直在等待的一点，对吗？是的，让我们执行我们的第一个 Qt Hello World 程序，如下图所示:\n\n![](img/00014.jpeg)\n\nFigure 5.6\n\n恭喜你！您已经完成了第一份 Qt 申请。在本练习中，您学习了如何在 Ubuntu 中设置和配置 Qt，以及如何编写一个简单的 Qt 控制台应用，然后构建和运行它。最棒的是你从命令行学到了所有这些。\n\n# Qt 小部件\n\nQt Widgets 是一个有趣的模块，它支持相当多的小部件，比如按钮、标签、编辑、组合、列表、对话框等等。`QWidget`是所有小部件的基类，而`QObject`是几乎每个 Qt 类的基类。虽然许多编程语言称之为用户界面控件，但 Qt 称之为小部件。虽然 Qt 在许多平台上工作，但它的家仍然是 Linux 小部件在 Linux 世界中很常见。\n\n# 编写我们第一个 Qt 图形用户界面应用\n\n我们的第一个控制台应用真的很酷，不是吗？让我们继续深入探索。这一次，让我们编写一个简单的基于 GUI 的 Hello World 程序。除了`main.cpp`中的一些小变化外，程序将保持几乎相同。有关完整代码，请参考以下内容:\n\n![](img/00015.jpeg)\n\nFigure 5.7\n\n等一下。我来解释一下第 23 行和第 29 行`QApplication`的必要性。每个 Qt 图形用户界面应用必须恰好有一个`QApplication`实例。`QApplication`为我们的应用提供命令行开关支持，因此需要提供**参数计数** ( **argc** )和**参数值** ( **argv** )。基于图形用户界面的应用是事件驱动的，所以它们必须响应事件，或者准确地说，Qt 世界中的信号。在第 29 行中，`exec`功能启动`event`循环，确保应用等待用户交互，直到用户关闭窗口。其思想是所有的用户事件将由事件队列中的`QApplication`实例接收，然后将通知给它的`Child`小部件。事件队列确保存放在队列中的所有事件都按照它们发生的相同顺序进行处理，即**先进先出** ( **先进先出**)。\n\n如果你想知道如果你注释第 29 行会发生什么，应用仍然会编译和运行，但是你可能看不到任何窗口。原因是`main`线程或`main`函数在第 25 行创建了一个`QWidget`的实例，这是我们启动应用时看到的窗口。\n\n在第 27 行，显示窗口实例，但是在没有第 29 行的情况下，`main`函数将立即终止应用，而不给你机会检查你的第一个 Qt GUI 应用。值得一试，所以继续看看有没有 29 号线会发生什么。\n\n让我们生成`Makefile`，如下图截图所示:\n\n![](img/00016.jpeg)\n\nFigure 5.8\n\n现在让我们试着用`make`工具编译我们的项目，如下图所示:\n\n![](img/00017.jpeg)\n\nFigure 5.9\n\n很有趣，对吧？我们全新的 Qt 图形用户界面程序无法编译。你注意到致命的错误了吗？没什么大不了的；让我们理解为什么会发生这种情况。原因是我们还没有链接 Qt Widgets 模块，因为`QApplication`类是 Qt Widgets 模块的一部分。在这种情况下，你可能想知道你的第一个 Hello World 程序是如何编译而没有任何问题的。在我们的第一个程序中，`QDebug`类是 **QtCore** 模块的一部分，隐式链接，而其他模块必须显式链接。让我们看看如何做到这一点:\n\n![](img/00018.jpeg)\n\nFigure 5.10\n\n我们需要将`QT += widgets`添加到`Ex2.pro`文件中，以便`qmake`实用程序理解它需要在创建最终可执行文件的同时，在 Linux 中链接 Qt Widgets 的**共享对象**(即`.so`文件)，也称为**动态链接库**(即`.dll`文件)。一旦解决了这个问题，我们必须`qmake`使`Makefile`能够反映我们的`Ex2.pro`文件中的新变化，如下图所示:\n\n![](img/00019.jpeg)\n\nFigure 5.11\n\n酷。让我们现在来看看我们的第一个基于图形用户界面的 Qt 应用。在我的系统中，应用输出如图*图 5.12 所示；*如果你这边一切顺利，你也应该得到类似的输出:\n\n![](img/00020.jpeg)\n\nFigure 5.12\n\n如果我们把窗口的标题设为`Hello Qt`就好了，对吧？让我们马上开始吧:\n\n![](img/00021.jpeg)\n\nFigure 5.13\n\n在测试新的变更之前，添加第 26 行显示的代码，以确保您使用`make`工具构建项目:\n\n![](img/00022.jpeg)\n\nFigure 5.14\n\n# 布局\n\nQt 是跨平台的应用框架，因此它支持一些概念，例如开发在所有平台上看起来一致的应用的布局，而不管不同的屏幕分辨率如何。当我们开发基于图形用户界面/人机界面的 Qt 应用时，在一个系统中开发的应用不应该在另一个具有不同屏幕大小和分辨率的系统中出现不同。这是通过布局在 Qt 框架中实现的。布局有不同的风格。这有助于开发人员通过在窗口或对话框中组织各种小部件来设计专业外观的人机界面/图形用户界面。布局在排列子部件的方式上有所不同。当一个以水平方式排列其子部件时，另一个将以垂直或网格方式排列它们。当窗口或对话框调整大小时，布局会调整其子小部件的大小，这样它们就不会被截断或失去焦点。\n\n# 编写具有水平布局的图形用户界面应用\n\n让我们编写一个在对话框中有几个按钮的 Qt 应用。Qt 支持各种有用的布局管理器，它们充当了一个不可见的画布，许多`QWidgets`可以在它们被附加到窗口或对话框之前被排列。每个对话框或窗口只能有一种布局。每个小部件只能添加到一个布局中；但是，许多布局可以组合起来设计专业的 UI。\n\n让我们现在开始编写代码。在这个项目中，我们将以模块化的方式编写代码，因此我们将创建三个名为`MyDlg.h`、`MyDlg.cpp`和`main.cpp`的文件。\n\n游戏计划如下:\n\n1.  创建`QApplication`的单个实例。\n2.  继承`QDialog`创建自定义对话框。\n3.  创建三个按钮。\n4.  创建水平方框布局。\n5.  将这三个按钮添加到不可见的水平框布局中。\n6.  将水平框布局的实例设置为对话框的布局。\n7.  显示对话框。\n8.  在`QApplication`上启动事件循环。\n\n我们遵循整洁的代码实践是很重要的，这样我们的代码就很容易理解，并且可以被任何人维护。由于我们将遵循行业最佳实践，让我们在名为`MyDlg.h`的头文件中声明对话框，在名为`MyDlg.cpp`的源文件中定义对话框，并在具有`main`功能的`main.cpp`中使用`MyDlg.cpp`。每次`MyDlg.cpp`需要一个头文件的时候，让我们把所有的头文件都只包含在`MyDlg.h`中作为惯例；有了这个，我们在`MyDlg.cpp`中唯一会看到的标题就是`MyDlg.h`。\n\n顺便问一下，我有没有告诉过你 Qt 遵循骆驼套管编码惯例？是的，我现在确实提到了。到目前为止，您已经观察到所有 Qt 类都以字母 *Q* 开头，因为 Qt 发明者喜欢 Emacs 中的字母“Q”，他们对这种字体类型非常着迷，以至于他们决定在 Qt 中的任何地方都使用字母 Q。\n\n最后一个建议。如果文件的名称和类的名称相似，那么其他人定位对话框类不是很容易吗？我能听到你答应了。一切就绪！让我们开始编写我们的 Qt 应用。首先，参考下面的截图:\n\n![](img/00023.jpeg)\n\nFigure 5.15\n\n在前面的截图中，我们声明了一个名为`MyDlg`的类。它有一个布局、三个按钮和一个构造函数。现在参考这张截图:\n\n![](img/00024.jpeg)\n\nFigure 5.16\n\n正如您在前面的截图中看到的，我们定义了`MyDlg`构造函数，并实例化了布局和三个按钮。在第 27 行到第 29 行，我们在布局中添加了三个按钮。在第 31 行，我们将布局与我们的对话框相关联。仅此而已。在下面的截图中，我们定义了我们的`main`函数，它创建了一个`QApplication`的实例:\n\n![](img/00025.jpeg)\n\nFigure 5.17\n\n随后，我们创建了自定义对话框实例并显示了该对话框。最后，在第 27 行，我们开始`event`循环，以便`MyDlg`可以响应用户交互。参考以下截图:\n\n![](img/00026.jpeg)\n\nFigure 5.18\n\n前面的截图演示了构建和执行过程，这是我们可爱的应用。其实你可以试着玩一下对话框，更好的理解水平布局。首先，水平拉伸对话框，注意所有按钮的宽度增加；然后，看看是否可以减小对话框的宽度，以注意到所有按钮的宽度都减小了。这是任何布局经理的工作。布局管理器排列窗口小部件，检索窗口大小，并在所有子窗口小部件之间平均分配高度和宽度。布局管理器不断通知所有子小部件任何调整大小的事件。然而，由各自的子小部件来决定他们是想要调整自己的大小还是忽略布局调整信号。\n\n要检查此行为，请尝试垂直伸展对话框。随着对话框高度的增加，对话框的高度也会增加，但按钮不会增加高度。这是因为每个 Qt Widget 都有自己的首选大小策略；根据他们的尺寸策略，他们可能会响应或忽略某些布局调整信号。\n\n如果您希望按钮也能垂直伸展，`QPushButton`提供了一种方法来实现这一点。事实上，`QPushButton`就像其他小部件一样，是从`QWidget`延伸出来的。`setSizePolicy()`法源于`QPushButton`的基类，即`QWidget`:\n\n![](img/00027.jpeg)\n\nFigure 5.19\n\n你注意到 37 号线了吗？是的，我已经在`MyDlg`的构造器中设置了窗口标题，以保持我们的`main`功能简洁干净。\n\n在启动应用之前，请确保您已经使用`make`实用程序构建了项目:\n\n![](img/00028.jpeg)\n\nFigure 5.20\n\n在突出显示的部分，我们已经覆盖了所有按钮的默认大小策略。第 27 行第一个参数`QSizePolicy::Expanding`是指横向政策，第二个参数是指纵向政策。要查找`QSizePolicy`的其他可能值，请参考 Qt API 引用附带的助手，如下图所示:\n\n![](img/00029.jpeg)\n\nFigure 5.21\n\n# 编写具有垂直布局的图形用户界面应用\n\n在上一节中，您学习了如何使用水平框布局。在本节中，您将看到如何在应用中使用垂直框布局。\n\n事实上，水平和垂直的盒子布局只是在如何排列小部件方面有所不同。例如，水平框布局将以水平方式从左到右排列其子部件，而垂直框布局将以垂直方式从上到下排列其子部件。\n\n您可以从上一节复制源代码，因为这些更改本质上是微小的。复制代码后，项目目录应该如下所示:\n\n![](img/00030.jpeg)\n\nFigure 5.22\n\n让我演示一下从`MyDlg.h`头文件开始的更改，如下所示:\n\n![](img/00031.jpeg)\n\nFigure 5.23\n\n我已经把`QHBoxLayout`换成了`QVBoxLayout`；仅此而已。是的，让我们继续进行与`MyDlg.cpp`相关的文件更改:\n\n![](img/00032.jpeg)\n\nFigure 5.24\n\n`main.cpp`无变化可做；但是，我已经显示了`main.cpp`供您参考，如下所示:\n\n![](img/00033.jpeg)\n\nFigure 5.25\n\n现在我们需要做的就是自动生成`Makefile`，然后如下制作并运行程序:\n\n![](img/00034.jpeg)\n\nFigure 5.26\n\n让我们执行我们全新的程序并检查输出。下面的输出演示了`QVBoxLayout`以从上到下垂直的方式排列小部件。当窗口被拉伸时，所有按钮的宽度将根据窗口是被拉伸还是被拉伸而增加/减少:\n\n![](img/00035.jpeg)\n\nFigure 5.27\n\n# 编写带有方框布局的图形用户界面应用\n\n在前几节中，您学习了如何利用`QHBoxLayout`和`QVBoxLayout`。其实这两个班就是`QBoxLayout` **的便民班。**在`QHBoxLayout`的情况下，`QHBoxLayout`类子类化`QBoxLayout`并将`QBoxLayout::Direction`配置为`QBoxLayout::LeftToRight`，而`QVBoxLayout`类子类化`QBoxLayout`并将`QBoxLayout::Direction`配置为`QBoxLayout::TopToBottom`。\n\n除了这些值之外，`QBoxLayout::Direction`还支持各种其他值，如下所示:\n\n*   `QBoxLayout::LeftToRight`:这将从左到右排列小部件\n*   `QBoxLayout::RightToLeft`:这将从右向左排列小部件\n*   `QBoxLayout::TopToBottom`:这将从上到下排列小部件\n*   `QBoxLayout::BottomToTop`:这将从下往上排列小部件\n\n让我们用带有五个按钮的`QBoxLayout`编写一个简单的程序。\n\n先说`MyDlg.h`头文件。我在`MyDlg`类中声明了五个按钮指针和一个`QBoxLayout`指针:\n\n![](img/00036.jpeg)\n\nFigure 5.28\n\n让我们来看看我们的`MyDlg.cpp`源文件。如果您注意到下面截图中的第 21 行，`QBoxLayout`构造函数接受两个参数。第一个参数是您希望排列小部件的方向，第二个参数是一个可选参数，它需要布局实例的父地址。\n\n正如您可能已经猜到的那样，`this`指针指的是`MyDlg`实例指针，它恰好是布局的父级。\n\n![](img/00037.jpeg)\n\nFigure 5.29\n\n同样，正如您可能已经猜到的那样，`main.cpp`文件不会从我们过去的练习中改变，如下图所示:\n\n![](img/00038.jpeg)\n\nFigure 5.30\n\n让我们编译并运行程序，如下所示:\n\n![](img/00039.jpeg)\n\nFigure 5.31\n\n如果你注意到输出，它看起来像一个水平的盒子布局输出，对吗？没错，因为我们已经把方向设置为`QBoxLayout::LeftToRight`。如果您将方向修改为`QBoxLayout::RightToLeft`，那么按钮 1 将出现在按钮 1 的右侧，按钮 2 将出现在按钮 1 的左侧，以此类推。因此，输出如下图所示:\n\n*   如果方向设置为`QBoxLayout::RightToLeft`，会看到如下输出:\n\n![](img/00040.jpeg)\n\nFigure 5.32\n\n*   如果方向设置为`QBoxLayout::TopToBottom`，会看到如下输出:\n\n![](img/00041.jpeg)\n\nFigure 5.33\n\n*   如果方向设置为`QBoxLayout::BottomToTop`，会看到如下输出:\n\n![](img/00042.jpeg)\n\nFigure 5.34\n\n在前面的所有场景中，按钮以完全相同的顺序添加到布局中，分别从按钮 1 到按钮 5 开始。但是，根据在`QBoxLayout`构造器中选择的方向，框布局将排列按钮，因此输出会有所不同。\n\n# 编写具有网格布局的图形用户界面应用\n\n网格布局允许我们以表格的方式排列小部件。这相当容易，就像一个盒子布局。我们需要做的就是指出每个小部件必须添加到布局中的行和列。由于行和列索引从从零开始的索引开始，行 0 的值表示第一行，列 0 的值表示第一列。理论够了；让我们开始写一些代码。\n\n让我们声明 10 个按钮，并将其添加到两行五列中。除了具体的`QGridLayout`差异之外，其余的东西将保持与前面的练习相同，所以如果你已经理解了到目前为止讨论的概念，那么继续创建`MyDlg.h` **、** `MyDl.cpp`和`main.cpp`。\n\n下面我来展示一下`MyDlg.h`的源代码截图:\n\n![](img/00043.jpeg)\n\nFigure 5.35\n\n以下是`MyDlg.cpp`的代码片段:\n\n![](img/00044.jpeg)\n\nFigure 5.36\n\n`main.cpp`源文件内容将保持与我们之前的练习相同；因此，我跳过了`main.cpp`代码片段。因为您熟悉构建过程，所以我也跳过了它。如果您忘记了这一点，只需查看前面的部分来了解构建过程。\n\n如果您正确键入了代码，您应该会得到以下输出:\n\n![](img/00045.jpeg)\n\nFigure 5.37\n\n事实上，网格布局有更多的东西可以提供。让我们探索如何让一个按钮跨越多个单元格。我保证你将要看到的会更有趣。\n\n我将修改`MyDlg.h`和`MyDlg.cpp`，并保持`main.cpp`与前面的练习相同:\n\n![](img/00046.jpeg)\n\nFigure 5.38\n\n我们的`MyDlg.cpp`来了:\n\n![](img/00047.jpeg)\n\nFigure 5.39\n\n注意第 35 到 38 行。现在详细讨论一下`addWidget()`功能。\n\n在第 35 行中，`pLayout->addWidget ( pBttn1, 0, 0, 1, 1 )`代码执行以下操作:\n\n*   前三个参数将 Button 1 添加到网格布局的第一行和第一列\n*   第四个参数`1`指示按钮 1 将只占据一行\n*   第五个参数`1`指示按钮 1 将只占据一列\n*   因此，很明显`pBttn1`应该在单元格(0，0)处渲染，并且应该只占据一个网格单元格\n\n在第 36 行中，`pLayout->addWidget ( pBttn2, 0, 1, 1, 2 )`代码执行以下操作:\n\n*   前三个参数将`Button 2`添加到第一行第二列的网格布局中\n*   第四个参数指示`Button 2`将占据一行\n*   第五个参数指示`Button 2`将占据两列(即第一行的第二列和第三列)\n*   在底线，按钮 2 将呈现在单元格(0，1)中，它应该占据一行和两列\n\n在第 37 行中，`pLayout->addWidget ( pBttn3, 0, 3, 2, 1 )`代码执行以下操作:\n\n*   前三个参数将 Button 3 添加到第一行第四列的网格布局中\n*   第四个参数指示按钮 3 将占据两行(即第一行和第四列以及第二行和第四列)\n*   第五个参数指示按钮 3 将占据一列\n\n在第 38 行中，`pLayout->addWidget ( pBttn4, 1, 0, 1, 3 )`代码执行以下操作:\n\n*   前三个参数将 Button 4 添加到第二行第一列的网格布局中\n*   第四个参数指示按钮 4 将占据一行\n*   第五个参数指示按钮 4 将占据三列(即第二行第一，然后是第二和第三列)\n\n检查程序的输出:\n\n![](img/00048.jpeg)\n\nFigure 5.40\n\n# 信号和插槽\n\n信号和插槽是 Qt 框架不可分割的一部分。到目前为止，我们已经编写了一些简单但有趣的 Qt 应用，但是我们还没有处理事件。现在是时候了解如何在我们的应用中支持事件了。\n\n让我们用一个按钮编写一个简单的应用。当按钮被点击时，检查我们是否可以在控制台上打印一些东西。\n\n`MyDlg.h`标题演示了如何声明`MyDlg`类:\n\n![](img/00049.jpeg)\n\nFigure 5.41\n\n下面的截图演示了如何定义`MyDlg`构造函数来为我们的对话框窗口添加一个按钮:\n\n![](img/00050.jpeg)\n\nFigure 5.42\n\n`main.cpp`如下图所示:\n\n![](img/00051.jpeg)\n\nFigure 5.43\n\n让我们构建并运行我们的程序，稍后添加对信号和插槽的支持。如果您按照说明正确操作，您的输出应该类似于下面的截图:\n\n![](img/00052.jpeg)\n\nFigure 5.44\n\n如果你点击按钮，你会注意到什么都没有发生，因为我们还没有在我们的应用中添加对信号和插槽的支持。好了，是时候揭示秘密指令了，它将帮助你让按钮对按钮点击信号做出反应。等等，是时候了解更多信息了。别担心，这和 Qt 有关。\n\nQt 信号只不过是事件，而 slot 函数是事件处理函数。有趣的是，信号和槽都是正常的 C++ 函数；只有当它们被标记为信号或槽时，Qt Framework 才会理解它们的目的，并提供必要的样板代码。\n\nQt 中的每个小部件都支持一个或多个信号，也可以选择支持一个或多个插槽。因此，在我们编写任何进一步的代码之前，让我们探索一下`QPushButton`支持哪些信号。\n\n让我们将 Qt 助手用于应用编程接口参考:\n\n![](img/00053.jpeg)\n\nFigure 5.45\n\n如果您观察前面的截图，它有一个内容部分，似乎涵盖了公共插槽，但我们没有看到任何信号列在那里。这是很多信息。如果内容部分没有列出信号，`QPushButton`将不直接支持信号。然而，也许它的基类，也就是`QAbstractButton`，会支持一些信号。`QPushButton`类部分给出了大量有用的信息，比如标题文件名，哪个 Qt 模块必须链接到应用——也就是说，必须添加到`.pro`的 qmake 条目——等等。还提到了`QPushButton`的基类。如果您进一步向下滚动，您的 Qt 助手窗口应该如下所示:\n\n![](img/00054.jpeg)\n\nFigure 5.46\n\n如果您观察附加继承成员下突出显示的部分，显然 Qt 助手暗示`QPushButton`从`QAbstractButton`继承了四个信号。所以我们需要探索`QAbstractButton`支持的信号，以支持`QPushButton`中的信号。\n\n![](img/00055.jpeg)\n\nFigure 5.47\n\n在 Qt 助手的帮助下，如上图截图所示，`QAbstractButton`类显然支持四种信号，这四种信号同样适用于`QPushButton`，因为`QPushButton`是`QAbstractButton`的子类。因此，让我们在本练习中使用`clicked()`信号。\n\n为了使用`clicked()`信号，我们需要在`MyDlg.h`和`MyDlg.cpp`中做一些小的改变。因此，我在下面的截图中突出显示了这两个文件的变化:\n\n![](img/00056.jpeg)\n\nFigure 5.48\n\n如您所知，`QDebug`类用于调试目的。它为 Qt 应用提供了类似于`cout`的功能，但它们并不是信号和插槽真正需要的。我们在这里使用它们只是为了调试。在*图 5.48* 中，第 34 行，void `MyDlg::onButtonClicked()`是我们打算用作事件处理函数的槽函数，该函数必须在按钮点击时调用。\n\n下面的截图应该会让你知道为了支持信号和插槽，你需要在`MyDlg.cpp`中做哪些改变:\n\n![](img/00057.jpeg)\n\nFigure 5.49\n\n如果观察前面截图中的第 40 行到第 42 行，`MyDlg::onButtonClicked()`方法是一个槽函数，只要点击按钮就必须调用。但是除非按钮的`clicked()`信号被映射到`MyDlg::onButtonClicked()`槽，否则当按钮被点击时，Qt 框架不会知道它必须调用`MyDlg::onButtonClicked()`。因此，在第 32 到 37 行中，我们将按钮信号`clicked()`与`MyDlg`实例的`onButtonClicked()`插槽功能连接起来。连接功能由`MyDlg`从`QDialog`继承。这反过来又继承了其最终基类`QObject`的功能。\n\n口头禅是每个想要参与信号和时隙通信的类必须是`QObject`或者它的子类。`QObject`提供了大量的信号和插槽支持，`QObject`是`QtCore`模块的一部分。令人惊讶的是，Qt 框架甚至为命令行应用提供了信号和插槽。这就是信号和插槽支持被内置到最终基类`QObject`中的原因，它是 **QtCore** 模块的一部分。\n\n好的，让我们构建并运行我们的程序，看看这些信号在我们的应用中是否有效:\n\n![](img/00058.jpeg)\n\nFigure 5.50\n\n有趣的是，我们没有得到编译错误，但当我们点击按钮时，突出显示的警告消息会自动出现。这是来自 Qt 框架的一个提示，我们错过了一个重要的过程，这个过程是使信号和槽工作的强制性的。\n\n让我们回忆一下在头文件和源文件中自动生成`Makefile`的过程:\n\n1.  `qmake -project`命令确保当前文件夹中存在的所有头文件和源文件都包含在`.pro`文件中。\n2.  `qmake`命令拾取当前文件夹中的`.pro`文件，并为我们的项目生成`Makefile`。\n3.  `make`命令将调用`make`实用程序。然后它在当前目录中执行`Makefile`，并基于`Makefile`中定义的 make 规则构建我们的项目。\n\n在第 1 步中，`qmake`实用程序会扫描我们所有的自定义头文件，并检查它们是否需要信号和插槽支持。任何含有`Q_OBJECT`宏的头文件都会提示`qmake`实用程序它需要信号和插槽支持。因此我们必须在我们的`MyDlg.h`头文件中使用`Q_OBJECT`宏:\n\n![](img/00059.jpeg)\n\nFigure 5.51\n\n一旦在头文件中完成了推荐的更改，我们需要确保发出`qmake`命令。现在`qmake`工具将打开`Ex8.pro`文件来获取我们的项目标题和源文件。当`qmake`解析`MyDlg.h`并找到`Q_OBJECT`宏时，它会了解到我们的`MyDlg.h`需要信号和插槽，然后它会确保在`MyDlg.h`上调用 moc 编译器，以便在名为`moc_MyDlg.cpp`的文件中自动生成样板代码。然后，这将继续并将必要的规则添加到`Makefile`中，以便自动生成的`moc_MyDlg.cpp`文件与其他源文件一起构建。\n\n现在您已经知道了 Qt 信号和插槽的秘密，继续尝试这个过程，并检查您的按钮点击是否打印了按钮点击...消息。我已经开始按照建议的变更构建我们的项目。在下面的截图中，我强调了幕后发生的有趣的事情；与使用花哨的 IDEs 相比，在命令行中工作会有一些优势:\n\n![](img/00060.jpeg)\n\nFigure 5.52\n\n现在是我们测试支持信号和插槽的简单应用输出的时候了。输出显示在下面的截图中:\n\n![](img/00061.jpeg)\n\nFigure 5.53\n\n恭喜你！你可以拍拍你的背。你已经学会了足够多在 Qt 中做很酷的事情。\n\n# 在 Qt 应用中使用堆叠布局\n\n由于您已经了解了信号和插槽，在本节中，让我们探讨如何在具有多个窗口的应用中使用堆叠布局；每个窗口可以是 **QWidget** 或 **QDialog。**每个页面都可能有自己的子小部件。我们即将开发的应用将演示堆叠布局的使用，以及如何在堆叠布局中从一个窗口导航到另一个窗口。\n\n![](img/00062.jpeg)\n\nFigure 5.54\n\n这个应用将需要相当数量的代码，因此我们必须确保我们的代码结构严谨，以满足结构和功能的质量，尽可能避免代码的味道。\n\n让我们创建四个小部件/窗口，这些小部件/窗口可以堆叠在一个堆叠布局中，其中每个页面可以被开发为一个单独的类，该类被分成两个文件:`HBoxDlg.h`和`HBoxDlg.cpp`等等。\n\n先说`HBoxDlg.h`。由于您熟悉布局，在本练习中，我们将用一种布局创建每个对话框，以便在子窗口之间导航时，您可以区分页面。否则，堆叠布局和其他布局之间将没有连接。\n\n![](img/00063.jpeg)\n\nFigure 5.55\n\n以下代码片段来自`HBoxDlg.cpp`文件:\n\n![](img/00064.jpeg)\n\nFigure 5.56\n\n同样，让我们写`VBoxDlg.h`如下:\n\n![](img/00065.jpeg)\n\nFigure 5.57\n\n让我们用方框布局创建第三个对话框`BoxDlg.h`，如下所示:\n\n![](img/00066.jpeg)\n\nFigure 5.58\n\n相应的`BoxDlg.cpp`源文件如下所示:\n\n![](img/00067.jpeg)\n\nFigure 5.59\n\n我们要叠加的第四个对话框是`GridDlg`，那么我们来看看`GridDlg.h`怎么写，如下图截图所示:\n\n![](img/00068.jpeg)\n\nFigure 5.60\n\n相应的`GridDlg.cpp`将如下所示:\n\n![](img/00069.jpeg)\n\nFigure 5.61\n\n酷，我们已经完成了创建四个可以在`MainDlg`中堆叠的小部件。`MainDlg`将使用`QStackedLayout`，所以本练习的关键是理解堆叠布局是如何工作的。\n\n让我们看看`MainDlg.h`应该怎么写:\n\n![](img/00070.jpeg)\n\nFigure 5.62\n\n在`MainDlg`中，我们已经声明了三个槽函数，每个按钮一个，以支持四个窗口之间的导航逻辑。堆叠布局类似于选项卡式小部件，不同之处在于选项卡式小部件将提供自己的可视方式在选项卡之间切换，而在堆叠布局的情况下，则由我们提供切换逻辑。\n\n`MainDlg.cpp`会是这样的:\n\n![](img/00071.jpeg)\n\nFigure 5.63\n\n您可以选择一个框布局来容纳三个按钮，因为我们更喜欢向右对齐的按钮。然而，为了确保额外的空间被一些看不见的胶水消耗掉，我们在第 44 行增加了一个拉伸项。\n\n在第 30 行到第 33 行之间，我们添加了堆叠布局中的所有四个子窗口，这样可以一次显示一个窗口。在索引 0 处添加`HBox`对话框，在索引 1 处添加`VBox`对话框，以此类推。\n\n第 53 行到第 58 行演示了前一个按钮的点击信号是如何与其对应的`MainDlg::onPrevPage()`槽功能连接的。必须为“下一步”和“退出”按钮配置类似的连接:\n\n![](img/00072.jpeg)\n\nFigure 5.64\n\n第 78 行的`if`条件确保只有当我们在第二个或更晚的子窗口中时，切换逻辑才会发生。由于水平对话框位于索引 0 处，在当前窗口恰好是水平对话框的情况下，我们无法导航到上一个窗口。对于切换到第 85 行的下一个子窗口，也进行了类似的验证。\n\n堆叠布局支持`setCurrentIndex()`方法切换到特定的索引位置；或者，您也可以尝试`setCurrentWidget()`方法，如果它在您的场景中效果更好的话。\n\n`main.cpp`看起来简短，如下所示:\n\n![](img/00073.jpeg)\n\nFigure 5.65\n\n我们的`main`函数最棒的地方在于，不管应用逻辑有多复杂，`main`函数都没有任何业务逻辑。这使得我们的代码干净且易于维护。\n\n# 编写一个结合多种布局的简单数学应用\n\n在这一节中，让我们探索如何编写一个简单的数学应用。作为本练习的一部分，我们将使用`QLineEdit`和`QLabel`小部件以及`QFormLayout`。我们需要设计一个 UI，如下图截图所示:\n\n![](img/00074.gif)\n\nFigure 5.66\n\n`QLabel`是一个通常用于静态文本的小部件，`QLineEdit`将允许用户提供单行输入。如前截图所示，我们将使用`QVBoxLayout`作为主布局，以便垂直排列`QFormLayout`和`QBoxLayout`。`QFormLayout`在你需要创建一个表单的时候会派上用场，这个表单的左边有一个标题，右边是一些小部件。`QGridLayout`可能也可以，但是`QFormLayout`在这种情况下很容易使用。\n\n在本练习中，我们将创建三个文件，即`MyDlg.h`、`MyDlg.cpp`和`main.cpp`。让我们从`MyDlg.h`源代码开始，然后进入其他文件:\n\n![](img/00075.jpeg)\n\nFigure 5.67\n\n在上图中，声明了三种布局。垂直方框布局用作主布局，而方框布局用于以右对齐方式排列按钮。表单布局用于添加标签，即行编辑小部件。本练习还将帮助您了解如何组合多种布局来设计专业的人机界面。\n\nQt 对单个窗口中可以组合的布局数量没有任何限制。然而，如果可能的话，如果你正在努力开发一个内存占用小的应用，考虑用最少的布局设计一个人机界面是一个好主意。否则，在应用中使用多种布局肯定没有坏处。\n\n在下面的截图中，你会了解到`MyDlg.cpp`源文件应该如何实现。在`MyDlg`构造器中，所有按钮都被实例化，并在方框布局中进行布局，以便正确对齐。表单布局用于以类似网格的方式保存`QLineEdit`小部件及其对应的`QLabel`小部件。`QLineEdit`小部件通常帮助提供单行输入；在这个特定的练习中，它们帮助我们根据用户的选择提供必须加、减等的数字输入。\n\n![](img/00076.jpeg)\n\nFigure 5.68\n\n我们的`main.cpp`源文件最棒的部分是它几乎保持不变，不管我们的应用有多复杂。在这个练习中，我想告诉你一个关于`MyDlg`的秘密。你有没有注意到`MyDlg`构造函数是在堆栈中实例化的，而不是在堆中？其思想是当`main()`函数退出时，`main`函数使用的堆栈将被取消，最终释放堆栈中存在的所有堆栈变量。当`MyDlg`被释放时，会导致调用`MyDlg`析构函数。在 Qt 框架中，每个小部件构造器都有一个可选的父小部件指针，它被最顶端的窗口析构器用来释放其子小部件。有趣的是，Qt 维护了一个树状数据结构来管理所有子部件的内存。因此，如果一切顺利，Qt 框架将负责“自动”释放所有子小部件的内存位置。\n\n这有助于 Qt 开发人员专注于应用方面，而 Qt 框架将负责内存管理。\n\n![](img/00077.jpeg)\n\nFigure 5.69\n\n检查我们新应用的输出，你不兴奋吗？如果您构建并执行应用，那么您应该会得到类似于下面截图**的输出。**当然，我们还没有添加信号和插槽支持，但是设计出让我们满意的 GUI，然后将我们的重点转移到事件处理上，这是个好主意:\n\n![](img/00078.jpeg)\n\nFigure 5.70\n\n如果仔细观察，虽然按钮在`QBoxLayout`上是从右向左排列的，但是按钮并没有向右对齐。出现这种情况的原因是，当窗口展开时，框布局似乎已经划分并分配了所有按钮中可用的额外水平空间。让我们继续，在盒子布局最左边的位置插入一个拉伸项，这样拉伸会吃掉所有多余的空间，使按钮没有扩展的空间。这将给我们带来右对齐的效果。添加拉伸后，代码将如下图所示:\n\n![](img/00079.jpeg)\n\nFigure 5.71\n\n继续检查您的输出是否如下图所示。有时候，作为开发人员，我们看到输出时会很兴奋，却忘了编译我们的更改，所以要确保项目再次构建。如果你看不到产出有任何变化，不用担心；只需尝试水平伸展窗口，您应该会看到右对齐的效果，如下图所示:\n\n![](img/00080.jpeg)\n\nFigure 5.72\n\n现在，既然我们有了一个外观不错的应用，让我们添加信号和插槽支持来添加对按钮点击的响应。我们现在不要急于包含加减功能。我们将使用一些`qDebug()`打印语句来检查信号和插槽是否连接正确，然后用实际功能逐渐替换它们。\n\n如果你还记得之前的信号和槽练习，任何对支持信号和槽感兴趣的 Qt 窗口都必须是`QObject`，并且应该在`MyDlg.h`头文件中包含`Q_OBJECT`宏，如下图所示:\n\n![](img/00081.jpeg)\n\nFigure 5.73\n\n在从 41 到 45 开始的行中，在私有部分声明了四个槽方法。槽函数是常规的 C++ 函数，可以像其他 C++ 函数一样直接调用。然而，在这种情况下，槽函数只打算用`MyDlg`调用。因此，它们被声明为私有函数，但是如果您相信其他人可能会发现连接到您的公共插槽是有用的，它们可以被公开。\n\n酷，如果你已经走到这一步，说明你已经理解了到目前为止讨论的事情。好了，让我们继续执行`MyDlg.cpp`中的插槽功能定义，然后将`clicked()`按钮的信号与相应的插槽功能连接起来:\n\n![](img/00082.jpeg)\n\nFigure 5.74\n\n现在是将信号连接到各自插槽的时候了。您可能已经猜到了，我们需要使用`MyDlg`构造函数中的`connect`函数，如下图所示，来获得对应插槽的按钮点击:\n\n![](img/00083.jpeg)\n\nFigure 5.75\n\n我们都准备好了。是的，现在是表演时间。由于我们已经处理了大部分内容，让我们编译并检查我们的小 Qt 应用的输出:\n\n![](img/00084.jpeg)\n\nFigure 5.76\n\n哎呀！我们遇到一些链接器错误。这个问题的根本原因是我们在应用中启用信号和槽位支持后忘记调用`qmake`。别担心，让我们调用`qmake`和`make`并运行我们的应用:\n\n![](img/00085.jpeg)\n\nFigure 5.77\n\n太好了，我们已经解决了这个问题。make 实用程序这次似乎没有发出任何噪音，我们能够启动应用。让我们检查信号和插槽是否按预期工作。为此，单击添加按钮，看看会发生什么:\n\n![](img/00086.jpeg)\n\nFigure 5.78\n\n哇哦！当我们点击添加按钮时，`qDebug()`控制台消息确认`MyDlg::onAddButtonClicked()`插槽被调用。如果你很想查看其他按钮的插槽，请点击其余按钮。\n\n没有业务逻辑，我们的应用将是不完整的。因此，让我们将业务逻辑添加到`MyDlg::onAddButtonClicked()`槽函数中，以执行添加并显示结果。一旦您学习了如何集成添加的业务逻辑，您就可以遵循相同的方法并实现剩余的 slot 功能:\n\n![](img/00087.jpeg)\n\nFigure 5.79\n\n在`MyDlg::onAddButtonClicked()`功能中，集成了业务逻辑。在第 82 行和第 83 行，我们试图提取用户在`QLineEdit`小部件中键入的值。`QLineEdit`中的`text()`功能返回`QString` **。**`QString`对象提供`toInt()`，方便提取`QString`表示的整数值。一旦这些值被添加并存储在结果变量中，我们需要将结果整数值转换回`QString`，如第 86 行所示，这样结果就可以被送入`QLineEdit`，如第 88 行所示。\n\n同样，您可以继续集成其他数学运算的业务逻辑。一旦你彻底测试了应用，你可以移除`qDebug()`控制台的输出。我们添加了`qDebug()`消息用于调试目的，因此现在可以清理它们了。\n\n# 摘要\n\n在本章中，您学习了使用 Qt 应用框架开发 C++ 图形用户界面应用。关键要点如下。\n\n*   您学习了在 Linux 中安装 Qt 和所需工具。\n*   您已经学习了用 Qt 框架编写简单的基于控制台的应用。\n*   您已经学习了使用 Qt 框架编写简单的基于图形用户界面的应用。\n*   您学习了 Qt 信号和槽机制的事件处理，以及元对象编译器如何帮助我们生成信号和槽所需的关键锅炉板代码。\n*   您学习了在应用开发中使用各种 Qt 布局来开发一个吸引人的人机界面，在许多 Qt 支持的平台上看起来都很棒。\n*   您学习了在单个人机界面中组合多种布局来开发专业的人机界面。\n*   您学习了相当多的 Qt 小部件，以及它们如何帮助您开发令人印象深刻的人机界面。\n*   总的来说，您学习了使用 Qt 应用框架开发跨平台图形用户界面应用。\n\n在下一章中，您将学习 C++ 中的多线程编程和 IPC。"
  },
  {
    "path": "docs/master-cpp-prog/06.md",
    "content": "# 六、多线程编程和进程间通信\n\n本章将涵盖以下主题:\n\n*   POSIX pthreads 简介\n*   使用 pthreads 库创建线程\n*   线程创建和自我识别\n*   开始线程\n*   停止线程\n*   使用 C++ 线程支持库\n*   数据竞争和线程同步\n*   接合和分离螺纹\n*   从线程发送信号\n*   将参数传递给线程\n*   死锁和解决方案\n*   并发\n*   未来、承诺、`packaged_task`等等\n*   线程支持库的并发性\n*   并发应用中的异常处理\n\n让我们通过本章中讨论的一些有趣、易于理解的例子来学习这些主题。\n\n# POSIX pthreads 简介\n\nUnix、Linux 和 macOS 基本上都符合 POSIX 标准。**Unix 的可移植操作系统接口** ( **POSIX** )是一个 IEEE 标准，帮助所有的 Unix 和类似 Unix 的操作系统，也就是 Linux 和 macOS，通过一个接口进行通信。\n\n有趣的是，POSIX 还得到符合 POSIX 的工具的支持——Cygwin、MinGW 和 Linux 的 Windows 子系统——它们在 Windows 平台上提供了一个类似于伪 Unix 的运行时和开发环境。\n\n请注意，pthread 是一个在 Unix、Linux 和 macOS 中使用的符合 POSIX 的 C 库。从 C++ 11 开始，C++ 通过 C++ 线程支持库和并发库本地支持线程。在本章中，我们将了解如何以面向对象的方式使用 pthreads、线程支持和并发库。此外，我们将讨论使用本机 C++ 线程支持和并发库相对于使用 POSIX pthreads 或其他第三方线程框架的优点。\n\n# 使用 pthreads 库创建线程\n\n让我们直接进入正题。你需要理解我们将要讨论的 pthread APIs，这样才能把你的手弄脏。首先，该函数用于创建一个新线程:\n\n```cpp\n #include <pthread.h>\n int pthread_create(\n              pthread_t *thread,\n              const pthread_attr_t *attr,\n              void *(*start_routine)(void*),\n              void *arg\n )\n```\n\n下表简要说明了前面函数中使用的参数:\n\n| **API 参数** | **评论** |\n| `pthread_t *thread` | 线程句柄指针 |\n| `pthread_attr_t *attr` | 线程属性 |\n| `void *(*start_routine)(void*)` | 线程函数指针 |\n| `void * arg` | 线程参数 |\n\n此函数阻塞调用方线程，直到第一个参数中传递的线程退出，如代码所示:\n\n```cpp\nint pthread_join ( pthread_t *thread, void **retval )\n```\n\n下表简要描述了前面函数中的参数:\n\n| **API 参数** | **评论** |\n| `pthread_t thread` | 螺纹手柄 |\n| `void **retval` | 指示线程过程退出代码的输出参数 |\n\n接下来的函数应该在线程上下文中使用。这里，`retval`是线程的退出代码，表示调用该函数的线程的退出代码:\n\n```cpp\nint pthread_exit ( void *retval )\n```\n\n以下是此函数中使用的参数:\n\n| **API 参数** | comment |\n| `void *retval` | 线程过程的退出代码 |\n\n以下函数返回线程标识:\n\n```cpp\npthread_t pthread_self(void)\n```\n\n让我们编写第一个多线程应用:\n\n```cpp\n#include <pthread.h>\n#include <iostream>\n\nusing namespace std;\n\nvoid* threadProc ( void *param ) {\n  for (int count=0; count<3; ++ count)\n    cout << \"Message \" << count << \" from \" << pthread_self()\n         << endl;\n  pthread_exit(0);\n}\n\nint main() {\n  pthread_t thread1, thread2, thread3;\n\n  pthread_create ( &thread1, NULL, threadProc, NULL );\n  pthread_create ( &thread2, NULL, threadProc, NULL );\n  pthread_create ( &thread3, NULL, threadProc, NULL );\n\n  pthread_join( thread1, NULL );\n  pthread_join( thread2, NULL );\n\n  pthread_join( thread3, NULL );\n\n  return 0;\n\n}\n```\n\n# 如何编译和运行\n\n可以使用以下命令编译程序:\n\n```cpp\ng++ main.cpp -lpthread\n```\n\n如您所见，我们需要动态链接 POSIX `pthread`库。\n\n查看下面的截图，并可视化多线程程序的输出:\n\n![](img/00088.jpeg)\n\n用 ThreadProc 编写的代码在线程上下文中运行。前面的程序总共有四个线程，包括主线程。我已经用`pthread_join`阻塞了主线程，迫使它等待其他三个线程先完成它们的任务，否则主线程会在它们之前退出。当主线程退出时，应用也会退出，这最终会过早地破坏新创建的线程。\n\n虽然我们按照各自的顺序创建了`thread1`、`thread2`和`thread3`，但不能保证它们会按照创建时的完全相同的顺序启动。\n\n操作系统调度程序根据操作系统调度程序使用的算法来决定线程必须启动的顺序。有趣的是，线程开始的顺序在同一个系统的不同运行中可能会有所不同。\n\n# C++ 本身支持线程吗？\n\n从 C++ 11 开始，C++ 本身确实支持线程，它通常被称为 C++ 线程支持库。C++ 线程支持库提供了对 POSIX pthreads C 库的抽象。随着时间的推移，C++ 原生线程支持得到了更大程度的改进。\n\n我强烈建议您在 pthreads 上使用 C++ 本机线程。所有平台都支持 C++ 线程支持库，因为它正式成为标准 C++ 的一部分，而不是 POSIX `pthread`库，后者仅在 Unix、Linux 和 macOS 上受支持，但不直接在 Windows 上受支持。\n\n最好的部分是线程支持在 C++ 17 中已经成熟到了一个新的水平，并且它在 C++ 20 中已经准备好达到下一个水平。因此，考虑在项目中使用 C++ 线程支持库是一个好主意。\n\n# 如何使用本机 C++ 线程特性编写多线程应用\n\n有趣的是，使用 C++ 线程支持库编写多线程应用非常简单:\n\n```cpp\n#include <thread>\nusing namespace std;\nthread instance ( thread_procedure )\n```\n\n`thread`类是在 C++ 11 中引入的。这个函数可以用来创建一个线程。这个函数的等价物是 POSIX `pthread`库中的`pthread_create`。\n\n| **参数** | comment |\n| `thread_procedure` | 线程函数指针 |\n\n现在来看一下下面代码中返回线程标识的参数:\n\n```cpp\nthis_thread::get_id ()\n```\n\n这个函数相当于 POSIX `pthread`库中的`pthread_self()`函数。参考以下代码:\n\n```cpp\nthread::join()\n```\n\n`join()`函数用于阻塞调用线程或主线程，以便它将等待，直到已经加入的线程完成其任务。这是非静态函数，因此必须在线程对象上调用。\n\n让我们看看如何使用前面的函数编写一个基于 C++ 的简单多线程程序。请参考以下程序:\n\n```cpp\n#include <thread>\n#include <iostream>\nusing namespace std;\n\nvoid threadProc() {\n  for( int count=0; count<3; ++ count ) {\n    cout << \"Message => \"\n         << count\n         << \" from \"\n         << this_thread::get_id()\n         << endl;\n  }\n}\n\nint main() {\n  thread thread1 ( threadProc );\n  thread thread2 ( threadProc );\n  thread thread3 ( threadProc );\n\n  thread1.join();\n  thread2.join();\n  thread3.join();\n\n  return 0;\n}\n```\n\n多线程程序的 C++ 版本看起来比 C 版本简单干净得多。\n\n# 如何编译和运行\n\n以下命令帮助您编译程序:\n\n```cpp\ng++ main.cpp -std=c++ 17 -lpthread\n```\n\n在前面的命令中，`-std=c++ 17`指示 C++ 编译器启用 C++ 17 特性；但是，程序会在任何支持 C++ 11 的 C++ 编译器上编译，你只需要把`c++ 17`换成`c++ 11`。\n\n程序的输出如下所示:\n\n![](img/00089.jpeg)\n\n前面截图中所有以`140`开头的数字都是线程 id。由于我们创建了三个线程，因此`pthread`库分别分配了三个唯一的线程标识。如果您真的很想找到操作系统分配的线程标识，那么在应用运行时，您必须在 Linux 中发出以下命令:\n\n```cpp\n ps -T -p <process-id>\n```\n\n可能让你惊讶的是`pthread`库分配的线程 ID 会和操作系统分配的不同。因此，从技术上讲，`pthread`库分配的线程标识只是一个线程句柄标识，与操作系统分配的线程标识不同。您可能想考虑的另一个有趣的工具是`top`命令，用于探索流程中的线程:\n\n```cpp\n top -H -p <process-id>\n```\n\n这两个命令都需要多线程应用的进程标识。以下命令将帮助您找到此标识:\n\n```cpp\nps -ef | grep -i <your-application-name>\n```\n\n您也可以探索 Linux 中的`htop`实用程序。\n\n如果你想以编程方式获取操作系统分配的线程标识，你可以在 Linux 中使用以下函数:\n\n```cpp\n#include <sys/types.h>\npid_t gettid(void)\n```\n\n但是，如果您想编写一个可移植的应用，不建议这样做，因为只有在 Unix 和 Linux 中才支持这样做。\n\n# 以面向对象的方式使用标准::线程\n\n如果您一直在寻找看起来类似于 Java 或 Qt 线程中的`Thread`类的 C++ 线程类，我相信您会发现这很有趣:\n\n```cpp\n#include <iostream>\n#include <thread>\nusing namespace std;\n\nclass Thread {\nprivate:\n      thread *pThread;\n      bool stopped;\n      void run();\npublic:\n      Thread();\n      ~Thread();\n\n      void start();\n      void stop();\n      void join();\n      void detach();\n};\n```\n\n这是一个包装类，在本书中作为 C++ 线程支持库的便利类。`Thread::run()`方法是我们自定义的线程程序。因为我不想让客户端代码直接调用`Thread::run()`方法，所以我声明了运行方法`private`。为了启动线程，客户端代码必须调用`thread`对象上的 start 方法。\n\n对应的`Thread.cpp`源文件如下图:\n\n```cpp\n#include \"Thread.h\"\n\nThread::Thread() {\n     pThread = NULL;\n     stopped = false;\n}\n\nThread::~Thread() {\n     delete pThread;\n     pThread = NULL;\n}\n\nvoid Thread::run() {\n\n     while ( ! stopped ) {\n         cout << this_thread::get_id() << endl;\n         this_thread::sleep_for ( 1s );\n     }\n     cout << \"\\nThread \" << this_thread::get_id()\n          << \" stopped as requested.\" << endl;\n     return;\n}\n\nvoid Thread::stop() {\n    stopped = true;\n}\n\nvoid Thread::start() {\n    pThread = new thread( &Thread::run, this );\n}\n\nvoid Thread::join() {\n     pThread->join();\n}\n\nvoid Thread::detach() {\n     pThread->detach();\n}\n```\n\n从之前的`Thread.cpp`源文件中，你就已经明白了通过调用`stop`方法可以在需要的时候停止线程。这是一个简单而体面的实现；然而，在它可以用于生产之前，还有许多其他的角落情况需要处理。然而，这个实现足够好，可以理解本书中的线程概念。\n\n酷，让我们看看我们的`Thread`类如何在`main.cpp`中使用:\n\n```cpp\n#include \"Thread.h\"\n\nint main() {\n\n      Thread thread1, thread2, thread3;\n\n      thread1.start();\n      thread2.start();\n      thread3.start();\n\n      thread1.detach();\n      thread2.detach();\n      thread3.detach();\n\n      this_thread::sleep_for ( 3s );\n\n      thread1.stop();\n      thread2.stop();\n      thread3.stop();\n\n      this_thread::sleep_for ( 3s );\n\n      return 0;\n}\n```\n\n我已经创建了三个线程，按照`Thread`类的设计方式，线程只会在`start`函数被调用时才会启动。分离的线程在后台运行；通常，如果您想让线程成为守护进程，您需要分离一个线程。但是，在应用退出之前，这些线程会安全地停止。\n\n# 如何编译和运行\n\n以下命令有助于编译程序:\n\n```cpp\ng++ Thread.cpp main.cpp -std=c++ 17 -o threads.exe -lpthread\n```\n\n程序的输出将如下图所示:\n\n![](img/00090.jpeg)\n\n哇哦！我们可以按照设计启动和停止线程，也可以以面向对象的方式启动和停止线程。\n\n# 你学到了什么？\n\n让我们试着回忆一下到目前为止我们讨论过的内容:\n\n*   您已经学习了如何使用 POSIX `pthread` C 库编写多线程应用\n*   C++ 编译器从 C++ 11 开始支持线程\n*   您已经学习了常用的基本 C++ 线程支持库 API\n*   您学习了如何使用 C++ 线程支持库编写多线程应用\n*   现在您知道为什么您应该考虑使用 C++ 线程支持库而不是`pthread` C 库了\n*   C++ 线程支持库是跨平台的，不像 POSIX `pthread`库\n*   您知道如何以面向对象的方式使用 C++ 线程支持库\n*   您知道如何编写不需要同步的简单多线程应用\n\n# 同步线程\n\n在理想情况下，线程会提供更好的应用性能。但是，有时，注意到应用性能由于多线程而降低并不罕见。这个性能问题可能并不真正与多线程有关；真正的罪魁祸首可能是设计。过多使用同步会导致许多与线程相关的问题，这些问题也会导致应用性能下降。\n\n无锁线程设计不仅避免了与线程相关的问题，还提高了应用的整体性能。然而，在实际世界中，不止一个线程可能必须共享一个或多个公共资源。因此，需要同步访问或修改共享资源的代码的关键部分。有多种同步机制可用于特定场景。在接下来的部分中，我们将通过一些有趣且实用的用例来逐一探讨它们。\n\n# 如果线程不同步会发生什么？\n\n当进程边界内有多个线程共享一个公共资源时，代码的关键部分可以与互斥锁同步。互斥锁是一种互斥锁，只允许一个线程访问由互斥锁保护的关键代码块。让我们举一个简单的例子来实际理解互斥锁应用的需求。\n\n让我们来看一个允许三个简单操作的`Bank Savings Account`类，即`getBalance`、`withdraw`和`deposit`。`Account`类可以实现如下代码所示。出于演示的目的，`Account`类以简单的方式设计，忽略了现实世界中需要的角落案例和验证。简化到`Account`类连账号都懒得捕捉的程度。我相信有很多这样的要求为了简单而被悄悄地忽略了。不用担心！我们的重点是通过所示的例子来学习互斥体:\n\n```cpp\n#include <iostream>\nusing namespace std;\n\nclass Account {\nprivate:\n  double balance;\npublic:\n  Account( double );\n  double getBalance( );\n  void deposit ( double amount );\n  void withdraw ( double amount ) ;\n};\n```\n\n`Account.cpp`源文件是这样的:\n\n```cpp\n#include \"Account.h\"\n\nAccount::Account(double balance) {\n  this->balance = balance;\n}\n\ndouble Account::getBalance() {\n  return balance;\n}\n\nvoid Account::withdraw(double amount) {\n  if ( balance < amount ) {\n    cout << \"Insufficient balance, withdraw denied.\" << endl;\n    return;\n  }\n\n  balance = balance - amount;\n}\n\nvoid Account::deposit(double amount) {\n  balance = balance + amount;\n}\n```\n\n现在，让我们创建两个线程，即`DEPOSITOR`和`WITHDRAWER`。`DEPOSITOR`线程将存入 2000.00 印度卢比，而`WITHDRAWER`线程将每隔一秒提取 1000.00 印度卢比。根据我们的设计，`main.cpp`源文件可以实现如下:\n\n```cpp\n#include <thread>\n#include \"Account.h\"\nusing namespace std;\n\nenum ThreadType {\n  DEPOSITOR,\n  WITHDRAWER\n};\n\nAccount account(5000.00);\n\nvoid threadProc ( ThreadType typeOfThread ) {\n\n  while ( 1 ) {\n  switch ( typeOfThread ) {\n    case DEPOSITOR: {\n      cout << \"Account balance before the deposit is \"\n           << account.getBalance() << endl;\n\n      account.deposit( 2000.00 );\n\n      cout << \"Account balance after deposit is \"\n           << account.getBalance() << endl;\n      this_thread::sleep_for( 1s );\n}\nbreak;\n\n    case WITHDRAWER: {\n      cout << \"Account balance before withdrawing is \"\n           << account.getBalance() << endl;\n\n      account.deposit( 1000.00 );\n      cout << \"Account balance after withdrawing is \"\n           << account.getBalance() << endl;\n      this_thread::sleep_for( 1s );\n    }\n    break;\n  }\n  }\n}\n\nint main( ) {\n  thread depositor ( threadProc, ThreadType::DEPOSITOR );\n  thread withdrawer ( threadProc, ThreadType::WITHDRAWER );\n\n  depositor.join();\n  withdrawer.join();\n\n  return 0;\n}\n```\n\n如果观察`main`函数，线程构造器接受两个参数。第一个参数是您现在已经熟悉的线程过程。第二个参数是可选参数，如果您想将一些参数传递给线程函数，可以提供该参数。\n\n# 如何编译和运行\n\n可以使用以下命令编译程序:\n\n```cpp\ng++ Account.cpp main.cpp -o account.exe -std=c++ 17 -lpthread\n```\n\n如果您已经按照指示完成了所有步骤，那么您的代码应该会编译成功。\n\n是时候执行和观察我们的程序如何工作了！\n\n别忘了`WITHDRAWER`螺纹总是退 INR 1000.00，而`DEPOSITOR`螺纹总是存 INR 2000.00。以下输出首先传达了这一点。`WITHDRAWER`线开始退出，接着是`DEPOSITOR`线，看起来是存了钱。\n\n虽然我们先启动了`DEPOSITOR`线程，然后启动了`WITHDRAWER`线程，但是看起来操作系统调度器似乎已经先调度了`WITHDRAWER`线程。不能保证这种情况会一直这样发生。\n\n从输出来看，`WITHDRAWER`线程和`DEPOSITOR`线程似乎交替工作。他们会这样持续一段时间。在某个时候，两个线程似乎同时工作，这就是事情会分崩离析的时候，如前面的输出所示:\n\n![](img/00091.jpeg)\n\n观察输出的最后四行非常有趣。看起来`WITHDRAWER`和`DEPOSITOR`线程都在检查余额，是 9000.00 INR。您可能会注意到`DEPOSITOR`线程的打印语句不一致；根据`DEPOSITOR`线程，当前余额为 9000.00 印度卢比。因此，当其存款 2000.00 印度卢比时，余额合计应达到 11000.00 印度卢比。但实际上，存款后的余额是 10000.00 印度卢比。这种不一致的原因是`WITHDRAWER`线程在`DEPOSITOR`线程可以存款之前提取了 1000.00 印度卢比。虽然从技术上来说，这种平衡似乎是正确的，但事情很快就会出错；这是需要线程同步的时候。\n\n# 让我们使用互斥体\n\n现在，让我们重构`threadProc`函数，同步修改和访问余额的关键部分。我们需要一个只允许一个线程读取或写入余额的锁定机制。C++ 线程支持库提供了一个名为`mutex`的合适的锁。`mutex`锁是一个排他锁，只允许一个线程在同一个进程边界内操作临界区代码。在获得锁的线程释放`mutex`锁之前，所有其他线程都必须等待轮到它们。一旦线程获得了`mutex`锁，该线程就可以安全地访问共享资源。\n\n`main.cpp`文件可以重构如下；这些更改以粗体突出显示:\n\n```cpp\n#include <iostream>\n#include <thread>\n#include <mutex>\n#include \"Account.h\"\nusing namespace std;\n\nenum ThreadType {\n  DEPOSITOR,\n  WITHDRAWER\n};\n\nmutex locker;\n\nAccount account(5000.00);\n\nvoid threadProc ( ThreadType typeOfThread ) {\n\n  while ( 1 ) {\n  switch ( typeOfThread ) {\n    case DEPOSITOR: {\n\n      locker.lock();\n\n      cout << \"Account balance before the deposit is \"\n           << account.getBalance() << endl;\n\n      account.deposit( 2000.00 );\n\n      cout << \"Account balance after deposit is \"\n           << account.getBalance() << endl;\n\n      locker.unlock();\n      this_thread::sleep_for( 1s );\n}\nbreak;\n\n    case WITHDRAWER: {\n\n      locker.lock();\n\n      cout << \"Account balance before withdrawing is \"\n           << account.getBalance() << endl;\n\n      account.deposit( 1000.00 );\n      cout << \"Account balance after withdrawing is \"\n           << account.getBalance() << endl;\n\n      locker.unlock();\n      this_thread::sleep_for( 1s );\n    }\n    break;\n  }\n  }\n}\n\nint main( ) {\n  thread depositor ( threadProc, ThreadType::DEPOSITOR );\n  thread withdrawer ( threadProc, ThreadType::WITHDRAWER );\n\n  depositor.join();\n  withdrawer.join();\n\n  return 0;\n}\n```\n\n您可能已经注意到互斥体是在全局范围内声明的。理想情况下，我们可以将类内部的互斥体声明为静态成员，而不是全局变量。由于所有线程都应该由同一个互斥体同步，所以要确保使用全局`mutex`锁或静态`mutex`锁作为类成员。\n\n`main.cpp`源文件中重构的`threadProc`如下图；这些更改以粗体突出显示:\n\n```cpp\nvoid threadProc ( ThreadType typeOfThread ) {\n\n  while ( 1 ) {\n  switch ( typeOfThread ) {\n    case DEPOSITOR: {\n\n      locker.lock();\n\n      cout << \"Account balance before the deposit is \"\n           << account.getBalance() << endl;\n\n      account.deposit( 2000.00 );\n\n      cout << \"Account balance after deposit is \"\n           << account.getBalance() << endl;\n\n      locker.unlock();\n      this_thread::sleep_for( 1s );\n}\nbreak;\n\n    case WITHDRAWER: {\n\n      locker.lock();\n\n      cout << \"Account balance before withdrawing is \"\n           << account.getBalance() << endl;\n\n      account.deposit( 1000.00 );\n      cout << \"Account balance after withdrawing is \"\n           << account.getBalance() << endl;\n\n      locker.unlock();\n      this_thread::sleep_for( 1s );\n    }\n    break;\n  }\n  }\n}\n```\n\n包装在`lock()`和`unlock()`之间的代码是互斥锁同步的关键部分。\n\n可以看到`threadProc`函数中有两个临界段块，所以要明白只有一个线程可以进入临界段，这一点很重要。例如，如果淀积器线程已经进入它的临界区，那么提取线程必须等待，直到淀积器线程释放锁，反之亦然。\n\nTechnically speaking, we could replace all the raw `lock()` and `unlock()` mutex methods with `lock_guard` as this ensures the mutex is always unlocked even if the critical section block of the code throws an exception. This will avoid starving and deadlock scenarios.\n\n是时候检查我们重构程序的输出了:\n\n![](img/00092.jpeg)\n\n太好了，你查过`DEPOSITOR`和`WITHDRAWER`线程上报的余额了吗？是的，他们总是一致的，不是吗？是的，输出确认代码是同步的，并且现在是线程安全的。\n\n虽然我们的代码在功能上是正确的，但是还有改进的空间。让我们重构代码，使其面向对象且高效。\n\n让我们重用`Thread`类，抽象`Thread`类中所有与线程相关的东西，去掉全局变量和`threadProc`。\n\n首先，让我们观察重构的`Account.h`头，如下所示:\n\n```cpp\n#ifndef __ACCOUNT_H\n#define __ACCOUNT_H\n\n#include <iostream>\nusing namespace std;\n\nclass Account {\nprivate:\n  double balance;\npublic:\n  Account( double balance );\n  double getBalance();\n  void deposit(double amount);\n  void withdraw(double amount);\n};\n\n#endif\n```\n\n可以看到`Account.h`表头没有变化，因为已经看起来很干净了。\n\n相应的`Account.cpp`源文件如下所示:\n\n```cpp\n#include \"Account.h\"\n\nAccount::Account(double balance) {\n  this->balance = balance;\n}\n\ndouble Account::getBalance() {\n  return balance;\n}\n\nvoid Account::withdraw(double amount) {\n  if ( balance < amount ) {\n    cout << \"Insufficient balance, withdraw denied.\" << endl;\n    return;\n  }\n\n  balance = balance - amount;\n}\n\nvoid Account::deposit(double amount) {\n  balance = balance + amount;\n}\n```\n\n最好将`Account`类从线程相关的功能中分离出来，以保持整洁。此外，让我们了解一下如何重构我们编写的`Thread`类，以使用前面显示的互斥同步机制:\n\n```cpp\n#ifndef __THREAD_H\n#define __THREAD_H\n\n#include <iostream>\n#include <thread>\n#include <mutex>\nusing namespace std;\n#include \"Account.h\"\n\nenum ThreadType {\n   DEPOSITOR,\n   WITHDRAWER\n};\n\nclass Thread {\nprivate:\n      thread *pThread;\n      Account *pAccount;\n      static mutex locker;\n      ThreadType threadType;\n      bool stopped;\n      void run();\npublic:\n      Thread(Account *pAccount, ThreadType typeOfThread);\n      ~Thread();\n      void start();\n      void stop();\n      void join();\n      void detach();\n};\n\n#endif\n```\n\n在前面显示的`Thread.h`头文件中，作为重构的一部分，做了一些更改。因为我们希望使用互斥来同步线程，所以`Thread`类包含了 C++ 线程支持库的互斥头。由于所有线程都应该使用相同的`mutex`锁，因此`mutex`实例被声明为静态的。由于所有线程将共享同一个`Account`对象，`Thread`类有一个指向`Account`对象的指针，而不是堆栈对象。\n\n`Thread::run()`方法是我们将要提供给 C++ 线程支持库的`Thread`类构造器的`Thread`函数。由于预计没有人会直接调用`run`方法，因此`run`方法被声明为私有。根据我们的`Thread`类设计，类似于 Java 和 Qt，客户端代码只会调用`start`方法；当操作系统调度程序向`run`发出绿色信号时，`run`线程程序将被自动调用。实际上，这里没有魔法，因为`run`方法地址在创建线程时注册为`Thread`函数。\n\nGenerally, I prefer to include all the dependent headers in the user-defined header file, and the user-defined source file includes only its own header. This helps organize the headers in one place, and this discipline helps maintain the code cleaner and also improves the overall readability and code maintainability.\n\n`Thread.cpp`源可以重构如下:\n\n```cpp\n#include \"Thread.h\"\n\nmutex Thread::locker;\n\nThread::Thread(Account *pAccount, ThreadType typeOfThread) {\n  this->pAccount = pAccount;\n  pThread = NULL;\n  stopped = false;\n  threadType = typeOfThread;\n}\n\nThread::~Thread() {\n  delete pThread;\n  pThread = NULL;\n}\n\nvoid Thread::run() {\n    while(1) {\n  switch ( threadType ) {\n    case DEPOSITOR:\n      locker.lock();\n\n      cout << \"Depositor: current balance is \" << pAccount->getBalance() << endl;\n      pAccount->deposit(2000.00);\n      cout << \"Depositor: post deposit balance is \" << pAccount->getBalance() << endl;\n\n      locker.unlock();\n\n      this_thread::sleep_for(1s);\n      break;\n\n    case WITHDRAWER:\n      locker.lock();\n\n      cout << \"Withdrawer: current balance is \" << \n               pAccount->getBalance() << endl;\n      pAccount->withdraw(1000.00);\n      cout << \"Withdrawer: post withraw balance is \" << \n               pAccount->getBalance() << endl;\n\n      locker.unlock();\n\n      this_thread::sleep_for(1s);\n      break;\n  }\n    }\n}\n\nvoid Thread::start() {\n  pThread = new thread( &Thread::run, this );\n}\n\nvoid Thread::stop() {\n  stopped = true;\n}\n\nvoid Thread::join() {\n  pThread->join();\n}\n\nvoid Thread::detach() {\n  pThread->detach();\n}\n```\n\n`main.cpp`中的`threadProc`功能已经移到了`Thread`类的`run`方法中。毕竟，`main`函数或`main.cpp`源文件不应该有任何类型的业务逻辑，因此它们被重构以提高代码质量。\n\n现在来看看重构后的`main.cpp`源文件有多干净:\n\n```cpp\n#include \"Account.h\"\n#include \"Thread.h\"\n\nint main( ) {\n\n  Account account(5000.00);\n\n  Thread depositor ( &account, ThreadType::DEPOSITOR );\n  Thread withdrawer ( &account, ThreadType::WITHDRAWER );\n\n  depositor.start();\n  withdrawer.start();\n\n  depositor.join();\n  withdrawer.join();\n\n  return 0;\n}\n```\n\n之前显示的`main()`函数和整个`main.cpp`源文件看起来简短，没有任何令人讨厌的复杂业务逻辑。\n\nC++ supports five types of mutexes, namely `mutex`, `timed_mutex`, `recursive_mutex`, `recursive_timed_mutex`, and `shared_timed_mutex`.\n\n# 如何编译和运行\n\n以下命令帮助您编译重构的程序:\n\n```cpp\ng++ Thread.cpp Account.cpp main.cpp -o account.exe -std=c++ 17 -lpthread\n```\n\n太棒了！如果一切顺利，程序应该能顺利编译，不会发出任何噪音。\n\n在我们进入下一个主题之前，请快速查看此处显示的输出:\n\n![](img/00093.jpeg)\n\n太好了。效果不错。`DEPOSITOR`和`WITHDRAWER`线程似乎可以协同工作，而不会打乱平衡和打印语句。毕竟，我们已经重构了代码，在不修改功能的情况下使代码更加清晰。\n\n# 什么是僵局？\n\n在多线程应用中，在我们遇到死锁之前，一切看起来都很酷很有趣。假设有两个线程，即`READER`和`WRITER`。当`READER`线程等待一个已经被`WRITER`获得的锁，而`WRITER`线程等待读取器释放一个属于`READER`的锁时，可能会发生死锁，反之亦然。通常，在死锁情况下，两个线程会无休止地等待对方。\n\n通常，死锁是设计问题。有时，死锁可以很快被检测到，但有时找到根本原因可能会变得非常棘手。因此，底线是同步机制必须在正确的意义上深思熟虑地使用。\n\n让我们用一个简单而实用的例子来理解死锁的概念。我将重用我们的`Thread`类，做一些细微的修改来创建一个死锁场景。\n\n修改后的`Thread.h`表头如下:\n\n```cpp\n#ifndef __THREAD_H\n#define __THREAD_H\n\n#include <iostream>\n#include <string>\n#include <thread>\n#include <mutex>\n#include <string>\nusing namespace std;\n\nenum ThreadType {\n  READER,\n  WRITER\n};\n\nclass Thread {\nprivate:\n  string name;\n  thread *pThread;\n  ThreadType threadType;\n  static mutex commonLock;\n  static int count;\n  bool stopped;\n  void run( );\npublic:\n  Thread ( ThreadType typeOfThread );\n  ~Thread( );\n  void start( );\n  void stop( );\n  void join( );\n  void detach ( );\n  int getCount( );\n  int updateCount( );\n};\n#endif\n```\n\n`ThreadType`枚举有助于将特定任务分配给线程。`Thread`类有两个新方法:`Thread::getCount()`和`Thread::updateCount()`。这两种方法都将与一个公共的`mutex`锁同步，这样就会产生死锁的情况。\n\n好了，让我们继续回顾`Thread.cpp`源文件:\n\n```cpp\n#include \"Thread.h\"\n\nmutex Thread::commonLock;\n\nint Thread::count = 0;\n\nThread::Thread( ThreadType typeOfThread ) {\n  pThread = NULL;\n  stopped = false;\n  threadType = typeOfThread;\n  (threadType == READER) ? name = \"READER\" : name = \"WRITER\";\n}\n\nThread::~Thread() {\n  delete pThread;\n  pThread = NULL;\n}\n\nint Thread::getCount( ) {\n  cout << name << \" is waiting for lock in getCount() method ...\" <<\nendl;\n  lock_guard<mutex> locker(commonLock);\n  return count;\n}\n\nint Thread::updateCount( ) {\n  cout << name << \" is waiting for lock in updateCount() method ...\" << endl;\n  lock_guard<mutex> locker(commonLock);\n  int value = getCount();\n  count = ++ value;\n  return count;\n}\n\nvoid Thread::run( ) {\n  while ( 1 ) {\n    switch ( threadType ) {\n      case READER:\n        cout << name<< \" => value of count from getCount() method is \" << getCount() << endl;\n        this_thread::sleep_for ( 500ms );\n      break;\n\n      case WRITER:\n        cout << name << \" => value of count from updateCount() method is\" << updateCount() << endl;\n        this_thread::sleep_for ( 500ms );\n      break;\n    }\n  }\n}\n\nvoid Thread::start( ) {\n  pThread = new thread ( &Thread::run, this );\n}\n\nvoid Thread::stop( ) {\n  stopped = true;\n}\n\nvoid Thread::join( ) {\n  pThread->join();\n}\n\nvoid Thread::detach( ) {\n  pThread->detach( );\n}\n```\n\n到现在，你对`Thread`课已经相当熟悉了。因此，让我们集中讨论`Thread::getCount()`和`Thread::updateCount()`方法。`std::lock_guard<std::mutex>`是一个模板类，让我们不用再叫`mutex::unlock()`。在堆栈展开过程中，`lock_guard`析构函数将被调用；这将调用`mutex::unlock()`。\n\n底线是从创建`std::lock_guard<std::mutex>`实例开始，直到方法结束出现的所有语句都由互斥锁保护。\n好的，让我们进入`main.cpp`文件:\n\n```cpp\n#include <iostream>\nusing namespace std;\n\n#include \"Thread.h\"\n\nint main ( ) {\n\n      Thread reader( READER );\n      Thread writer( WRITER );\n      reader.start( );\n      writer.start( );\n      reader.join( );\n      writer.join( );\n      return 0;\n}\n```\n\n`main()`函数非常不言自明。我们创建了两个线程，即`reader`和`writer`，它们是在创建了各自的线程之后开始的。主线程被迫等待，直到读取器和写入器线程退出。\n\n# 如何编译和运行\n\n您可以使用以下命令编译该程序:\n\n```cpp\ng++ Thread.cpp main.cpp -o deadlock.exe -std=c++ 17 -lpthread\n```\n\n观察程序的输出，如下所示:\n\n![](img/00094.jpeg)\n\n参考`Thread::getCount()`和`Thread::updateCount()`方法的代码片段:\n\n```cpp\nint Thread::getCount() {\n         cout << name << \" is waiting for lock in getCount() method ...\" << endl;\n         lock_guard<mutex> locker(commonLock);\n         cout << name << \" has acquired lock in getCount() method ...\" << endl;\n         return count;\n}\nint Thread::updateCount() {\n        count << name << \" is waiting for lock in updateCount() method ...\" << endl;\n        lock_guard<mutex> locker(commonLock);\n        cout << name << \" has acquired lock in updateCount() method ...\" << endl;\n        int value = getCount();\n        count = ++ value;\n        return count;\n}\n```\n\n从前面输出的截图图片中，我们可以了解到`WRITER`线程似乎先开始了。根据我们的设计，`WRITER`线程将调用`Thread::updateCount()`方法，该方法又将调用`Thread::getCount()`方法。\n\n从输出的截图中，从打印语句中可以明显看出`Thread::updateCount()`方法首先获取了锁，然后调用了`Thread::getCount()`方法。但是由于`Thread::updateCount()`方法没有释放互斥锁，所以`WRITER`线程调用的`Thread::getCount()`方法无法继续。同时，操作系统调度器已经启动`READER`线程，似乎在等待`WRITER`线程获取的`mutex`锁。因此，`READER`线程要完成任务，必须获得`Thread::getCount()`方法上的锁；然而，在`WRITER`线程释放锁之前，这是不可能的。更糟糕的是，`WRITER`线程只有在自己的`Thread::getCount()`方法调用完成任务后才能完成任务。这就是所谓的**死锁**。\n\n这不是设计问题，就是逻辑问题。在 Unix 或 Linux 中，我们可以利用 Helgrind 工具通过竞争类似的同步问题来寻找死锁。Helgrind 工具与 Valgrind 工具一起提供。最棒的是 Valgrind 和 Helgrind 都是开源工具。\n\n为了获得导致死锁或争用问题的源代码行号，我们需要在调试模式下编译我们的代码，如现在显示的`-g`标志所示:\n\n```cpp\ng++ main.cpp Thread.cpp -o deadlock.exe -std=c++ 17 -lpthread -g\n```\n\nHelgrind 工具可用于检测死锁和类似问题，如下所示:\n\n```cpp\nvalgrind --tool=helgrind ./deadlock.exe\n```\n\n以下是瓦尔基林输出的简短摘录:\n\n![](img/00095.jpeg)\n\n解决这个问题的一个简单方法是重构`Thread::updateCount()`方法，如下所示:\n\n```cpp\nint Thread::updateCount() {\n        int value = getCount();\n\n        count << name << \" is waiting for lock in updateCount() method ...\" << endl;\n        lock_guard<mutex> locker(commonLock);\n        cout << name << \" has acquired lock in updateCount() method ...\" << endl;\n        count = ++ value;\n\n        return count;\n}\n```\n\n重构程序的输出如下:\n\n![](img/00096.jpeg)\n\n有趣的是，对于大多数复杂的问题，解决方案通常会非常简单。换句话说，愚蠢的错误有时可能会导致严重的关键错误。\n\nIdeally, we should strive to prevent the deadlock issue during the design phase so that we wouldn't have to break our head doing complex debugging. The C++ thread support library mutex class offers `mutex::try_lock()` (since C++ 11 ), `std::timed_mutex` (since C++ 11), and `std::scoped_lock` ( since C++ 17 ) to avoid deadlocks and similar issues.\n\n# 你学到了什么？\n\n让我们总结一下要点:\n\n*   我们应该尽可能设计无锁线程\n*   与高度同步/顺序线程相比，无锁线程的性能更好\n*   互斥是互斥的同步原语\n*   互斥有助于同步共享资源的访问，一次一个线程\n*   死锁的发生是因为互斥锁的使用不当，或者一般来说，是因为任何同步原语的使用不当\n*   死锁是逻辑或设计问题的结果\n*   可以使用 Unix 和 Linux 操作系统中的 Helgrind/Valgrind 开源工具来检测死锁\n\n# 共享互斥体\n\n共享互斥同步原语支持两种模式，即共享和独占。在共享模式下，共享互斥体将允许许多线程同时共享资源，而没有任何数据竞争问题。在独占模式下，它就像常规互斥体一样工作，也就是说，它只允许一个线程访问资源。如果您有多个可以安全访问资源的读取器，并且只允许一个线程修改共享资源，那么这是一个合适的锁原语。更多细节请参考 C++ 17 章节。\n\n# 条件变量\n\n条件变量同步原语用于两个或多个线程需要相互通信时，并且仅当它们接收到特定信号或事件时才继续。等待特定信号或事件的线程必须在开始等待信号或事件之前获取互斥体。\n\n让我们试着理解生产者/消费者问题的条件变量的用例。我将创建两个线程，即`PRODUCER`和`CONSUMER`。`PRODUCER`线程将向队列中添加一个值，并通知`CONSUMER`线程。`CONSUMER`线程将等待`PRODUCER`的通知。收到`PRODUCER`线程的通知后，`CONSUMER`线程将从队列中删除条目并打印。\n\n让我们了解一下这里显示的`Thread.h`头是如何利用条件变量和互斥体的:\n\n```cpp\n#include <iostream>\n#include <thread>\n#include <mutex>\n#include <condition_variable>\n#include <queue>\n#include <string>\n\nusing namespace std;\n\nenum ThreadType {\n  PRODUCER,\n  CONSUMER\n};\n\nclass Thread {\nprivate:\n  static mutex locker;\n  static condition_variable untilReady;\n  static bool ready;\n  static queue<int> appQueue;\n  thread *pThread;\n  ThreadType threadType;\n  bool stopped;\n  string name;\n\n  void run();\npublic:\n  Thread(ThreadType typeOfThread);\n  ~Thread();\n  void start();\n  void stop();\n  void join();\n  void detach();\n};\n```\n\n由于`PRODUCER`和`CONSUMER`线程应该使用相同的互斥体和`conditional_variable`，它们被声明为静态的。条件变量同步原语需要一个谓词函数来使用就绪布尔标志。因此，我也在静态范围内声明了就绪标志。\n\n让我们进入`Thread.cpp`源文件，如下所示:\n\n```cpp\n#include \"Thread.h\"\n\nmutex Thread::locker;\ncondition_variable Thread::untilReady;\nbool Thread::ready = false;\nqueue<int> Thread::appQueue;\n\nThread::Thread( ThreadType typeOfThread ) {\n  pThread = NULL;\n  stopped = false;\n  threadType = typeOfThread;\n  (CONSUMER == typeOfThread) ? name = \"CONSUMER\" : name = \"PRODUCER\";\n}\n\nThread::~Thread( ) {\n  delete pThread;\n  pThread = NULL;\n}\n\nvoid Thread::run() {\n  int count = 0;\n  int data = 0;\n  while ( 1 ) {\n    switch ( threadType ) {\n    case CONSUMER: \n    {\n\n      cout << name << \" waiting to acquire mutex ...\" << endl;\n\n      unique_lock<mutex> uniqueLocker( locker );\n\n      cout << name << \" acquired mutex ...\" << endl;\n      cout << name << \" waiting for conditional variable signal...\" << endl;\n\n      untilReady.wait ( uniqueLocker, [] { return ready; } );\n\n      cout << name << \" received conditional variable signal ...\" << endl;\n\n      data = appQueue.front( ) ;\n\n      cout << name << \" received data \" << data << endl;\n\n      appQueue.pop( );\n      ready = false;\n    }\n      cout << name << \" released mutex ...\" << endl;\n    break;\n\n    case PRODUCER:\n    {\n      cout << name << \" waiting to acquire mutex ...\" << endl;\n      unique_lock<mutex> uniqueLocker( locker );\n      cout << name << \" acquired mutex ...\" << endl;\n      if ( 32000 == count ) count = 0;\n      appQueue.push ( ++ count );\n      ready = true;\n      uniqueLocker.unlock();\n      cout << name << \" released mutex ...\" << endl;\n      untilReady.notify_one();\n      cout << name << \" notified conditional signal ...\" << endl;\n    }\n    break;\n  }\n  }\n}\n\nvoid Thread::start( ) {\n  pThread = new thread ( &Thread::run, this );\n}\n\nvoid Thread::stop( ) {\n  stopped = true;\n}\n\nvoid Thread::join( ) {\n  pThread->join( );\n}\n\nvoid Thread::detach( ) {\n  pThread->detach( );\n}\n```\n\n在前面的`Thread`课中，我使用了`unique_lock<std::mutex>`。`conditional_variable::wait()`方法预期`unique_lock`，因此我在这里使用`unique_lock`。现在`unique_lock<std::mutex>`支持所有权转移、递归锁定、延迟锁定、手动锁定、解锁不删除`unique_lock`，不像`lock_guard<std::mutex>`。`lock_guard<std::mutex>`实例立即锁定互斥体，当`lock_guard<std::mutex>`实例超出范围时，互斥体自动解锁。但是`lock_guard`不支持手动解锁。\n\n因为我们没有用延迟锁定选项创建`unique_lock`实例，`unique_lock`将立即锁定互斥体，就像`lock_guard`一样。\n\n`Thread::run()`方法是我们的线程函数。根据提供给`Thread`构造器的`ThreadType`，线程实例将表现为`PRODUCER`或`CONSUMER`线程。\n\n`PRODUCER`线程首先锁定互斥体，并向队列追加一个整数，该整数在`PRODUCER`和`CONSUMER`线程之间共享。一旦队列更新，`PRODUCER`会在通知`CONSUMER`前解锁互斥体；否则，`CONSUMER`将无法获取互斥并接收条件变量信号。\n\n`CONSUMER`线程首先获取互斥体，然后等待条件变量信号。接收到条件信号后，`CONSUMER`线程从队列中检索该值并打印该值并重置就绪标志，以便重复该过程，直到应用终止。\n\nIt is recommended to make use of `unique_lock<std::mutex>`, `lock_guard<std::mutex>`, or `scoped_lock<std::mutex>` to avoid deadlocks. At times, it is possible we may not unlock the mutex that leads to deadlocks; hence, the use of mutex directly isn't recommended.\n\n现在让我们看看`main.cpp`文件中的代码:\n\n```cpp\n#include \"Thread.h\"\n\nint main ( ) {\n\n  Thread producer( ThreadType::PRODUCER );\n  Thread consumer( ThreadType::CONSUMER );\n\n  producer.start();\n  consumer.start();\n\n  producer.join();\n  consumer.join();\n\n  return 0;\n} \n```\n\n# 如何编译和运行\n\n使用以下命令编译程序:\n\n```cpp\ng++ Thread.cpp main.cpp -o conditional_variable.exe -std=c++ 17 -lpthread\n```\n\n以下快照演示了程序的输出:\n\n![](img/00097.jpeg)\n\n太好了。我们的条件变量演示工作正常。生产者线程和消费者线程在条件变量的帮助下协同工作。\n\n# 你学到了什么？\n\n让我总结一下您在本节中学到的要点:\n\n*   多个线程可以通过使用条件变量相互发送信号来协同工作\n*   条件变量要求等待线程在等待条件信号之前获取互斥体\n*   每个条件变量都需要接受互斥体的`unique_lock`\n*   `unique_lock<std::mutex>`方法的工作方式与`lock_guard<std::mutex>`完全一样，具有一些额外的有用功能，如延迟锁定、手动锁定/解锁、所有权转移等\n*   `Unique_lock`有助于避免死锁，就像`lock_guard`一样，当`unique_lock`实例超出范围时，`unique_lock`包装的互斥体会自动解锁\n*   您学习了如何编写一个多线程应用，该应用包含相互发送同步信号的线程\n\n# 旗语\n\n信号量是另一种有用的线程同步机制。但是与互斥不同，信号量允许多个线程同时访问相似的共享资源。其同步原语支持两种类型，即二进制信号量和计数信号量。\n\n二进制信号量就像互斥体一样工作，也就是说，在任何时候只有一个线程可以访问共享资源。但是，不同的是互斥锁只能由拥有它的同一个线程释放；然而，任何线程都可以释放信号量锁。另一个显著的区别是，通常互斥体在进程边界内工作，而信号量在进程边界内使用。这是因为它是一个重量级锁，不像互斥锁。但是，如果在共享内存区域中创建，互斥体也可以在整个进程中使用。\n\n计算信号量可以让多个线程共享有限数量的共享资源。互斥体允许一个线程一次访问共享资源，而计数信号量允许多个线程共享有限数量的资源，通常至少两个或更多。如果一次一个线程地访问一个共享资源，但是线程跨越了进程边界，那么可以使用二进制信号量。虽然在同一个进程中使用二进制信号量是可能的，因为二进制信号量很大，但它并不高效，但它也可以在同一个进程中工作。\n\n不幸的是，C++ 线程支持库在 C++ 17 之前不支持信号量和共享内存。C++ 17 支持使用原子操作的无锁编程，这必须确保原子操作是线程安全的。信号量和共享内存允许来自其他进程的线程修改共享资源，这对并发模块来说是一个相当大的挑战，以确保跨进程边界的原子操作的线程安全。C++ 20 似乎在并发性上下了很大的赌注，因此我们需要等待并观察它的变化。\n\n然而，这并没有阻止您使用线程支持库提供的互斥体和条件变量来实现自己的信号量。开发一个在进程边界内共享公共资源的自定义信号量类相对容易，但是信号量有两种类型:命名的和未命名的。命名信号量用于跨边界同步公共资源，这很棘手。\n\n或者，您可以围绕 POSIX pthreads 信号量原语编写一个包装类，它支持命名和未命名信号量。如果您正在开发一个跨平台的应用，编写跨所有平台工作的可移植代码是一项要求。如果你沿着这条路走下去，你可能最终会为每个平台编写特定于平台的代码——是的，我听说过；听起来很奇怪，对吧？\n\nQt 应用框架本地支持信号量。使用 Qt 框架是一个很好的选择，因为它是跨平台的。缺点是 Qt 框架是第三方框架。\n\n底线是，你可能必须在 pthreads 和 Qt 框架之间做出选择，或者重构你的设计，尝试用原生 C++ 特性来解决问题。仅使用 C++ 原生特性来限制应用开发很困难，但可以保证跨所有平台的可移植性。\n\n# 并发\n\n每种现代编程语言都支持并发，提供高级 API，允许同时执行许多任务。C++ 支持从 C++ 11 开始的并发性，并且在 C++ 14 和 C++ 17 中进一步添加了更复杂的 API。虽然 C++ 线程支持库允许多线程，但它需要使用复杂的同步编写冗长的代码；然而，并发性让我们可以执行独立的任务——甚至循环迭代也可以并发运行，而无需编写复杂的代码。底线是并行化通过并发性变得更加容易。\n\n并发支持库是对 C++ 线程支持库的补充。这两个强大的库的结合使用使得 C++ 中的并发编程更加容易。\n\n让我们在下面名为`main.cpp`的文件中使用 C++ 并发编写一个简单的`Hello World`程序:\n\n```cpp\n#include <iostream>\n#include <future>\nusing namespace std;\n\nvoid sayHello( ) {\n  cout << endl << \"Hello Concurrency support library!\" << endl;\n}\n\nint main ( ) {\n  future<void> futureObj = async ( launch::async, sayHello );\n  futureObj.wait( );\n\n  return 0;\n}\n```\n\n我们试着了解一下`main()`功能。Future 是并发模块的一个对象，它帮助调用者函数以异步方式检索线程传递的消息。`future<void>`中的 void 表示`sayHello()`线程函数，该函数不会向调用者传递任何消息，即`main`线程函数。`async`类允许我们在两种模式下执行一个功能，即`launch::async`或`launch::deferred`模式。\n\n`launch::async`模式允许`async`对象在单独的线程中启动`sayHello()`方法，而`launch::deferred`模式允许`async`对象在不创建单独线程的情况下调用`sayHello()`功能。在`launch::deferred`模式下，`sayHello()`方法调用会有所不同，直到调用线程调用`future::get()`方法。\n\n`futureObj.wait()`语音用于阻塞主线程，让`sayHello()`功能完成任务。`future::wait()`功能类似于线程支持库中的`thread::join()`。\n\n# 如何编译和运行\n\n让我们继续使用以下命令编译程序:\n\n```cpp\ng++ main.cpp -o concurrency.exe -std=c++ 17 -lpthread\n```\n\n让我们启动`concurrency.exe`，如前所示，并了解其工作原理:\n\n![](img/00098.jpeg)\n\n# 使用并发支持库的异步消息传递\n\n让我们稍微修改一下`main.cpp`，上一节写的 Hello World 程序。让我们了解如何将消息从`Thread`函数异步传递给调用者函数:\n\n```cpp\n#include <iostream>\n#include <future>\nusing namespace std;\n\nvoid sayHello( promise<string> promise_ ) {\n  promise_.set_value ( \"Hello Concurrency support library!\" );\n}\n\nint main ( ) {\n  promise<string> promiseObj;\n\n  future<string> futureObj = promiseObj.get_future( );\n  async ( launch::async, sayHello, move( promiseObj ) );\n  cout << futureObj.get( ) << endl;\n\n  return 0;\n}\n```\n\n在之前的程序中，`sayHello()`线程函数使用`promiseObj`将消息异步传递给主线程。注意`promise<string>`暗示`sayHello()`函数预期传递一个字符串消息，因此主线程检索`future<string>`。`future::get()`函数调用将被阻止，直到`sayHello()`线程函数调用`promise::set_value()`方法。\n\n然而，重要的是要理解`future::get()`必须只被调用一次，因为对应的`promise`对象将在调用`future::get()`方法后被析构。\n\n你注意到`std::move()`功能的使用了吗？`std::move()`功能基本上将`promiseObj`的所有权转移到`sayHello()`线程功能，因此`std::move()`被调用后`promiseObj`不能从`main`线程访问。\n\n# 如何编译和运行\n\n让我们继续使用以下命令编译程序:\n\n```cpp\ng++ main.cpp -o concurrency.exe -std=c++ 17 -lpthread\n```\n\n通过启动`concurrency.exe`观察`concurrency.exe`应用如何工作，如下图所示:\n\n![](img/00099.jpeg)\n\n你可能已经猜到了，这个程序的输出和我们之前的版本完全一样。但是我们程序的这个版本使用了 promise 和 future 对象，不像以前的版本不支持消息传递。\n\n# 并发任务\n\n并发支持模块支持名为**任务**的概念。任务是跨线程并发发生的工作。可以使用`packaged_task`类创建并发任务。`packaged_task`类方便地连接了`thread`函数、相应的承诺和特征对象。\n\n让我们用一个简单的例子来理解`packaged_task`的用法。下面的程序让我们有机会尝试一下使用 lambda 表达式和函数的函数式编程:\n\n```cpp\n#include <iostream>\n#include <future>\n#include <promise>\n#include <thread>\n#include <functional>\nusing namespace std;\n\nint main ( ) {\n     packaged_task<int (int, int)>\n        addTask ( [] ( int firstInput, int secondInput ) {\n              return firstInput + secondInput;\n     } );\n\n     future<int> output = addTask.get_future( );\n     addTask ( 15, 10 );\n\n     cout << \"The sum of 15 + 10 is \" << output.get() << endl;\n     return 0;\n}\n```\n\n在之前显示的程序中，我创建了一个名为`addTask`的`packaged_task`实例。`packaged_task< int (int,int)>`实例意味着加法任务将返回一个整数并接受两个整数参数:\n\n```cpp\naddTask ( [] ( int firstInput, int secondInput ) {\n              return firstInput + secondInput;\n}); \n```\n\n前面的代码片段表明它是匿名定义的 lambda 函数。\n\n有趣的是`main.cpp`中的`addTask( )`调用看起来像是一个常规的函数调用。从`packaged_task`实例`addTask`中提取`future<int>`对象，然后使用该对象通过未来对象实例，即`get()`方法检索`addTask`的输出。\n\n# 如何编译和运行\n\n让我们继续使用以下命令编译程序:\n\n```cpp\ng++ main.cpp -o concurrency.exe -std=c++ 17 -lpthread\n```\n\n让我们快速启动`concurrency.exe`并观察下面显示的输出:\n\n![](img/00100.jpeg)\n\n酷！您学习了如何在并发支持库中使用 lambda 函数。\n\n# 使用带有线程支持库的任务\n\n在上一节中，您学习了如何优雅地使用`packaged_task`。我非常喜欢 lambda 函数。它们看起来很像数学。但是不是每个人都喜欢 lambda 函数，因为它们在某种程度上降低了可读性。因此，如果您不喜欢 lambda，那么在并发任务中使用 lambda 函数并不是强制性的。在本节中，您将了解如何使用线程支持库来执行并发任务，如以下代码所示:\n\n```cpp\n#include <iostream>\n#include <future>\n#include <thread>\n#include <functional>\nusing namespace std;\n\nint add ( int firstInput, int secondInput ) {\n  return firstInput + secondInput;\n}\n\nint main ( ) {\n  packaged_task<int (int, int)> addTask( add);\n\n  future<int> output = addTask.get_future( );\n\n  thread addThread ( move(addTask), 15, 10 );\n\n  addThread.join( );\n\n  cout << \"The sum of 15 + 10 is \" << output.get() << endl;\n\n  return 0;\n}\n```\n\n# 如何编译和运行\n\n让我们继续使用以下命令编译程序:\n\n```cpp\ng++ main.cpp -o concurrency.exe -std=c++ 17 -lpthread\n```\n\n让我们启动`concurrency.exe`，如下图截图所示，了解一下上一个程序和当前版本的区别:\n\n![](img/00101.jpeg)\n\n是的，输出与上一节相同，因为我们只是重构了代码。\n\n太棒了！您刚刚学习了如何将 C++ 线程支持库与并发组件集成在一起。\n\n# 将线程过程及其输入绑定到 packaged_task\n\n在本节中，您将学习如何将`thread`函数及其各自的参数与`packaged_task`绑定。\n\n让我们从上一节获取代码，并对其进行修改以理解绑定特性，如下所示:\n\n```cpp\n#include <iostream>\n#include <future>\n#include <string>\nusing namespace std;\n\nint add ( int firstInput, int secondInput ) {\n  return firstInput + secondInput;\n}\n\nint main ( ) {\n\n  packaged_task<int (int,int)> addTask( add );\n  future<int> output = addTask.get_future();\n  thread addThread ( move(addTask), 15, 10);\n  addThread.join();\n  cout << \"The sum of 15 + 10 is \" << output.get() << endl;\n  return 0;\n}\n```\n\n`std::bind( )`函数将`thread`函数及其参数与各自的任务绑定在一起。因为参数是预先绑定的，所以不需要再次提供输入参数 15 或 10。这些是在 C++ 中使用`packaged_task`的一些便利方式。\n\n# 如何编译和运行\n\n让我们继续使用以下命令编译程序:\n\n```cpp\ng++ main.cpp -o concurrency.exe -std=c++ 17 -lpthread\n```\n\n让我们启动`concurrency.exe`，如下图截图所示，了解一下上一个程序和当前版本的区别:\n\n![](img/00102.jpeg)\n\n恭喜你！到目前为止，您已经学习了很多关于 C++ 并发性的知识。\n\n# 并发库的异常处理\n\n并发支持库还支持通过未来的对象传递异常。\n\n让我们用一个简单的例子来理解异常并发处理机制，如下所示:\n\n```cpp\n#include <iostream>\n#include <future>\n#include <promise>\nusing namespace std;\n\nvoid add ( int firstInput, int secondInput, promise<int> output ) {\n  try {\n         if ( ( INT_MAX == firstInput ) || ( INT_MAX == secondInput ) )\n             output.set_exception( current_exception() ) ;\n        }\n  catch(...) {}\n\n       output.set_value( firstInput + secondInput ) ;\n\n}\n\nint main ( ) {\n\n     try {\n    promise<int> promise_;\n          future<int> output = promise_.get_future();\n    async ( launch::deferred, add, INT_MAX, INT_MAX, move(promise_) );\n          cout << \"The sum of INT_MAX + INT_MAX is \" << output.get ( ) << endl;\n     }\n     catch( exception e ) {\n  cerr << \"Exception occured\" << endl;\n     }\n}\n\n```\n\n就像我们将输出消息传递给调用者函数/线程的方式一样，并发支持库也允许您设置在任务或异步函数中发生的异常。当调用者线程调用`future::get()`方法时，将会抛出相同的异常，因此通信异常变得容易。\n\n# 如何编译和运行\n\n让我们继续用下面的命令编译程序。水果叔叔和尤达斯·马尔特:\n\n```cpp\ng++ main.cpp -o concurrency.exe -std=c++ 17 -lpthread\n```\n\n![](img/00103.jpeg)\n\n# 你学到了什么？\n\n让我总结一下要点:\n\n*   并发支持库提供了高级组件，可以同时执行几个任务\n*   未来的对象允许调用线程检索异步函数的输出\n*   异步函数使用 promise 对象来设置输出或异常\n*   `FUTURE`和`PROMISE`对象的类型必须与异步函数设置的值的类型相同\n*   并发组件可以无缝地与 C++ 线程支持库结合使用\n*   lambda 函数和表达式可以与并发支持库一起使用\n\n# 摘要\n\n在本章中，您了解了 C++ 线程支持库和 pthread C 库之间的区别、互斥同步机制、死锁和防止死锁的策略。您进一步学习了如何使用并发库编写同步函数，并进一步学习了 lambda 函数和表达式。\n\n在下一章中，您将了解测试驱动开发作为一种极端的编程方法。"
  },
  {
    "path": "docs/master-cpp-prog/07.md",
    "content": "# 七、测试驱动开发\n\n本章将涵盖以下主题:\n\n*   测试驱动开发概述\n*   关于 TDD 的常见神话和问题\n*   开发人员编写单元测试是否需要更多的努力\n*   代码覆盖率度量是好是坏\n*   TDD 是否适用于复杂的遗留项目\n*   TDD 是否甚至适用于嵌入式产品或涉及硬件的产品\n*   C++ 的单元测试框架\n*   谷歌测试框架\n*   在 Ubuntu 上安装谷歌测试框架\n*   构建谷歌测试和模拟作为一个单一的静态库而不安装它们的过程\n*   使用谷歌测试框架编写我们的第一个测试用例\n*   在 Visual Studio IDE 中使用谷歌测试框架\n*   行动中的 TDD\n*   测试有依赖性的遗留代码\n\n让我们深入探讨这些 TDD 主题。\n\n# TimeDivisionDuplex 时分双工\n\n**测试驱动开发** ( **TDD** )是一种极限编程实践。在 TDD 中，我们从一个测试用例开始，并逐步编写使测试用例成功所需的生产代码。这个想法是，一次应该专注于一个测试用例或场景，一旦测试用例通过，他们就可以继续下一个场景。在这个过程中，如果新的测试用例通过了，我们就不应该修改生产代码。换句话说，在开发一个新特性的过程中，或者在修复一个 bug 的时候，我们修改产品代码只有两个原因:要么确保测试用例通过，要么重构代码。TDD 的主要焦点是单元测试；然而，它可以在某种程度上扩展到集成和交互测试。\n\n下图直观地展示了 TDD 过程:\n\n![](img/00104.jpeg)\n\n当严格遵循 TDD 时，可以同时实现代码的功能和结构质量。与在开发阶段结束时编写测试用例相反，在编写产品代码之前先编写测试用例是非常重要的。这有很大的不同。例如，当开发人员在开发结束时编写单元测试用例时，测试用例不太可能在代码中发现任何缺陷。原因是当测试用例在开发结束时编写时，开发人员会不自觉地倾向于证明他们的代码正在做正确的事情。然而，当开发人员提前编写测试用例时，由于还没有编写代码，他们开始从最终用户的角度思考，这将鼓励他们从需求规范的角度提出许多场景。\n\n换句话说，针对已经编写的代码编写的测试用例通常不会发现任何错误，因为它倾向于证明编写的代码是正确的，而不是针对需求进行测试。当开发人员在编写代码之前考虑各种场景时，这有助于他们以增量方式编写更好的代码，确保代码能够处理这些场景。然而，当代码有漏洞时，是测试用例帮助他们发现问题，因为如果测试用例不满足需求，它们就会失败。\n\nTDD 不仅仅是使用一些单元测试框架。在开发或修复代码中的缺陷时，需要改变文化和思维方式。开发人员的重点应该是使代码在功能上正确。一旦以这种方式开发了代码，强烈建议开发人员也应该通过重构代码来消除任何代码异味；这将确保代码的结构质量也很好。从长远来看，是代码的结构质量会让团队更快地交付特性。\n\n# 关于 TDD 的常见神话和问题\n\n当每个人即将开始他们的 TDD 之旅时，他们脑海中都有很多关于 TDD 的神话和常见的疑问。让我澄清我遇到的大多数问题，因为我咨询了全球许多产品巨头。\n\n# 开发人员编写单元测试需要付出更多的努力吗？\n\n大多数开发人员心中出现的一个常见疑问是，“当我们适应 TDD 时，我应该如何评估我的努力？”由于开发人员应该将单元和集成测试用例作为 TDD 的一部分来编写，所以难怪您会关心如何与客户或管理层协商编写代码之外的测试用例所需的额外工作。别担心，你并不孤单；作为一名自由软件顾问，很多开发者都问过我这个问题。\n\n作为开发人员，您手动测试代码；相反，现在就编写自动化测试用例。好消息是，这是一次性的努力，从长远来看，肯定会帮助你。虽然开发人员需要重复的手动工作来测试他们的代码，但是每次他们更改代码时，已经存在的自动化测试用例将通过在他们集成一段新代码时给他们立即的反馈来帮助开发人员。\n\n底线是它需要一些额外的努力，但从长远来看，它有助于减少所需的努力。\n\n# 代码覆盖率度量是好是坏？\n\n代码覆盖工具帮助开发人员识别自动化测试用例中的差距。毫无疑问，很多时候它会给出关于缺失测试场景的线索，这最终会进一步加强自动化测试用例。但是当一个组织开始强制执行代码覆盖率作为检查测试覆盖率有效性的措施时，它有时会把开发人员推向错误的方向。从我的实际咨询经验中，我了解到许多开发人员开始为构造函数和私有及受保护函数编写测试用例，以显示更高的代码覆盖率。在这个过程中，开发者开始追逐数字，失去了 TDD 的最终目标。\n\n在具有 20 个方法的类的特定源代码中，可能只有 10 个方法符合单元测试的条件，而其他方法是复杂的功能。在这种情况下，代码覆盖率工具将只显示 50%的代码覆盖率，按照 TDD 的理念，这是绝对可以的。但是，如果组织策略强制要求至少 75%的代码覆盖率，那么开发人员将别无选择，只能测试构造函数、析构函数、私有函数、受保护函数和复杂函数，以显示良好的代码覆盖率。\n\n测试私有方法和受保护方法的问题在于，当它们被标记为实现细节时，它们往往会更频繁地改变。当私有的和受保护的方法变化很大时，这就需要修改测试用例，这使得开发人员在维护测试用例方面的生活更加艰难。\n\n因此，代码覆盖工具是发现测试场景差距的非常好的开发工具，但是应该由开发人员根据方法的复杂性，明智地选择是编写测试用例还是忽略为某些方法编写测试用例。然而，如果代码覆盖率被用作项目度量，它往往会驱使开发人员寻找错误的方法来显示更好的覆盖率，从而导致糟糕的测试用例实践。\n\n# TDD 是否适用于复杂的遗留项目？\n\n当然可以！TDD 适用于任何类型的软件项目或产品。TDD 不仅仅意味着新产品或项目；它也被证明对复杂的遗留项目或产品更有效。在维护项目中，绝大多数情况下，一个人必须修复缺陷，很少需要支持一个新特性。即使在这样的遗留代码中，也可以在修复缺陷时遵循 TDD。\n\n作为一个开发人员，你会很乐意同意我的观点，一旦你能够重现这个问题，从开发人员的角度来看，几乎一半的问题都可以被认为是固定的。因此，您可以从重现问题的测试用例开始，然后调试并修复问题。当您修复问题时，测试用例将开始通过；现在是时候考虑另一个可能的测试用例了，它可能会重现相同的缺陷并重复这个过程。\n\n# TDD 甚至适用于嵌入式或涉及硬件的产品吗？\n\n就像应用软件可以从 TDD 中受益一样，嵌入式项目或涉及硬件交互的项目也可以从 TDD 方法中受益。有趣的是，涉及硬件的嵌入式项目或产品从 TDD 中获益更多，因为它们可以通过隔离硬件依赖性来测试大部分代码，而无需硬件。TDD 有助于缩短上市时间，因为大多数软件都可以由团队测试，而无需等待硬件。由于大部分代码已经在没有硬件的情况下进行了彻底的测试，它有助于避免在电路板出现故障时出现最后的意外或灭火。这是因为大部分场景都经过了彻底的测试。\n\n根据软件工程最佳实践，一个好的设计本质上是松散耦合和强内聚的。尽管我们都努力编写松散耦合的代码，但不可能一直编写绝对独立的代码。大多数情况下，代码具有某种类型的依赖性。在应用软件的情况下，依赖可以是数据库或网络服务器；在嵌入式产品的情况下，依赖性可能是一件硬件。但是使用依赖反转，被测试的**代码** ( **CUT** )可以从它的依赖中隔离出来，使我们能够测试没有依赖的代码，这是一种强大的技术。因此，只要我们愿意重构代码，使其更加模块化和原子化，任何类型的代码和项目或产品都将从 TDD 方法中受益。\n\n# C++ 的单元测试框架\n\n作为一名 C++ 开发人员，在单元测试框架之间进行选择时，您有相当多的选择。虽然有更多的框架，但这些是一些流行的框架:CppUnit、CppUnitLite、Boost、MSTest、Visual Studio 单元测试和谷歌测试框架。\n\nThough older articles, I recommend you to take a look at [http://gamesfromwithin.com/exploring-the-c-unit-testing-framework-jungle](http://gamesfromwithin.com/exploring-the-c-unit-testing-framework-jungle) and [https://accu.org/index.php/journals/](https://accu.org/index.php/journals/). They might give you some insight into this topic.\n\n不用多想，谷歌测试框架是最受欢迎的 C++ 测试框架之一，因为它在各种各样的平台上得到支持，被积极开发，最重要的是，得到了谷歌的支持。\n\n在本章中，我们将使用谷歌测试和谷歌模拟框架。然而，本章讨论的概念适用于所有单元测试框架。在接下来的章节中，我们将深入研究谷歌测试框架及其安装过程。\n\n# 谷歌测试框架\n\n谷歌测试框架是一个开源的测试框架，可以在很多平台上运行。TDD 只关注单元测试和某种程度上的集成测试，但是 Google 测试框架可以用于各种各样的测试。它将测试用例分为小、中、大、保真、弹性、精确和其他类型的测试用例。单元测试用例属于小类，集成测试用例属于中等类，复杂功能和验收测试用例属于大类。\n\n它还捆绑了谷歌模拟框架作为其一部分。因为他们在技术上来自同一个团队，所以他们可以无缝地相互配合。然而，谷歌模拟框架可以与其他测试框架一起使用，例如 CppUnit。\n\n# 在 Ubuntu 上安装谷歌测试框架\n\n可以从[https://github.com/google/googletest](https://github.com/google/googletest)下载谷歌测试框架作为源代码。然而，最好的下载方式是通过终端命令行的 Git 克隆:\n\n```cpp\ngit clone https://github.com/google/googletest.git\n```\n\nGit is an open source **distributed version control system** (**DVCS**). If you haven't installed it on your system, you will find more information on why you should, at [https://git-scm.com/](https://git-scm.com/). However, in Ubuntu, it can be easily installed with the `sudo apt-get install git` command.\n\n一旦下载了代码，如图 7.1 、所示，你就可以在`googletest`文件夹中找到谷歌测试框架源代码:\n\n![](img/00105.jpeg)\n\nFigure 7.1\n\n`googletest`文件夹的`googletest`和`googlemock`框架都在不同的文件夹中。现在我们可以调用`cmake`实用程序来配置我们的构建并自动生成`Makefile`，如下所示:\n\n```cpp\ncmake CMakeLists.txt\n```\n\n![](img/00106.jpeg)\n\nFigure 7.2\n\n当`cmake`实用程序被调用时，它会检测到从源代码构建谷歌测试框架所需的 C/C++ 头文件及其路径。此外，它将尝试找到构建源代码所需的工具。一旦找到所有必要的集管和工具，它将自动生成`Makefile`。一旦你有了`Makefile`，你可以用它在你的系统上编译和安装谷歌测试和谷歌模拟:\n\n```cpp\nsudo make install\n```\n\n下面的截图展示了如何在您的系统上安装 google test:\n\n![](img/00107.jpeg)\n\nFigure 7.3\n\n在上图中，make install 命令已经编译并在`/usr/local/lib`文件夹中安装了`libgmock.a`和`libgtest.a`静态库文件。由于`/usr/local/lib`文件夹路径通常在系统的 path 环境变量中，因此可以从系统中的任何项目访问它。\n\n# 如何在不安装的情况下将谷歌测试和模拟构建成一个静态库？\n\n如果您不喜欢在公共系统文件夹上安装`libgmock.a`和`libgtest.a`静态库文件和各自的头文件，那么还有另一种方法来构建谷歌测试框架。\n\n以下命令将创建三个对象文件，如图*图 7.4* :\n\n```cpp\ng++ -c googletest/googletest/src/gtest-all.cc googletest/googlemock/src/gmock-all.cc googletest/googlemock/src/gmock_main.cc -I googletest/googletest/ -I googletest/googletest/include -I googletest/googlemock -I googletest/googlemock/include -lpthread -\n```\n\n![](img/00108.jpeg)\n\nFigure 7.4\n\n下一步是使用以下命令将所有对象文件合并到一个静态库中:\n\n```cpp\nar crf libgtest.a gmock-all.o gmock_main.o gtest-all.o\n```\n\n如果一切顺利，你的文件夹应该有全新的`libgtest.a`静态库，如图*图 7.5* 。让我们理解以下命令说明:\n\n```cpp\ng++ -c googletest/googletest/src/gtest-all.cc    googletest/googlemock/src/gmock-all.cc googletest/googlemock/src/gmock_main.cc -I googletest/googletest/ -I googletest/googletest/include \n-I googletest/googlemock  -I googletest/googlemock/include -lpthread -std=c++ 14\n```\n\n前面的命令将帮助我们创建三个目标文件: **gtest-all.o** 、 **gmock-all.o** 和 **gmock_main.o** 。`googletest`框架利用了一些 C++ 11 的特性，我有目的地使用 c++ 14 是为了更安全。`gmock_main.cc`源文件有一个主要功能，将初始化谷歌模拟框架，反过来将在内部初始化谷歌测试框架。这种方法最好的一点是，我们不必为单元测试应用提供主要功能。请注意，编译命令包括以下`include`路径，以帮助 g++ 编译器在谷歌测试和谷歌模拟框架中找到必要的头文件:\n\n```cpp\n-I googletest/googletest\n-I googletest/googletest/include\n-I googletest/googlemock\n-I googletest/googlemock/include\n```\n\n现在下一步是创建我们的`libgtest.a`静态库，将 gtest 和 gmock 框架捆绑到一个静态库中。由于谷歌测试框架使用了多线程，因此必须将`pthread`库作为我们静态库的一部分进行链接:\n\n```cpp\nar crv libgtest.a gtest-all.o gmock_main.o gmock-all.o\n```\n\n`ar`存档命令有助于将所有目标文件合并到一个静态库中。\n\n下图演示了终端中的实际讨论过程:\n\n![](img/00109.jpeg)\n\nFigure 7.5\n\n# 使用谷歌测试框架编写我们的第一个测试用例\n\n学习谷歌测试框架相当容易。让我们创建两个文件夹:一个用于生产代码，另一个用于测试代码。这个想法是将生产代码和测试代码分开。一旦你创建了这两个文件夹，从`Math.h`标题开始，如图 7.6 所示:\n\n![](img/00110.jpeg)\n\nFigure 7.6\n\n`Math`类只有一个功能来演示单元测试框架的使用。首先，我们的`Math`类有一个简单的 add 函数，足以理解谷歌测试框架的基本用法。\n\nIn the place of the Google test framework, you could use CppUnit as well and integrate mocking frameworks such as the Google mock framework, mockpp, or opmock.\n\n让我们在下面的`Math.cpp`源文件中实现我们简单的`Math`类:\n\n![](img/00111.jpeg)\n\nFigure 7.7\n\n前面两个文件应该在`src`文件夹，如图*图 7.8* 所示。所有的生产代码进入`src`文件夹，任何数量的文件都可以成为`src`文件夹的一部分。\n\n![](img/00112.jpeg)\n\nFigure 7.8\n\n由于我们已经编写了一些生产代码，让我们看看如何为前面的生产代码编写一些基本的测试用例。作为一般的最佳实践，建议将测试用例文件命名为`MobileTest`或`TestMobile`，这样任何人都很容易预测文件的目的。在 C++ 或谷歌测试框架中，文件名和类名保持一致并不是强制性的，但这通常被认为是最佳实践，因为它可以帮助任何人通过查看文件名来定位特定的类。\n\nBoth the Google test framework and Google mock framework go hand in hand as they are from the same team, hence this combination works pretty well in the majority of the platforms, including embedded platforms.\n\n由于我们已经将谷歌测试框架编译为静态库，让我们直接从`MathTest.cpp`源文件开始:\n\n![](img/00113.jpeg)\n\nFigure 7.9\n\n在*图 7.9***第 18 行中，我们包含了谷歌测试框架的 gtest 头文件。在谷歌测试框架中，测试用例是用一个带两个参数的`TEST`宏编写的。第一个参数，即`MathTest`，代表测试模块名称，第二个参数是测试用例的名称。测试模块帮助我们将一堆相关的测试用例组合在一个模块下。因此，恰当地命名测试模块和测试用例对于提高测试报告的可读性非常重要。**\n\n **如你所知，`Math`是我们打算测试的班级；我们已经在第 22 行实例化了`Math`对象的一个对象。在*第 25 行*中，我们调用了数学对象上的 add 函数，该函数应该返回实际结果。最后，在*行 27* 处，我们检查预期结果是否与实际结果匹配。如果预期结果和实际结果匹配，谷歌测试宏`EXPECT_EQ`会将测试用例标记为通过；否则，框架会将测试用例结果标记为失败。\n\n酷，我们都准备好了。现在让我们看看如何编译和运行我们的测试用例。以下命令将帮助您编译测试用例:\n\n```cpp\ng++ -o tester.exe src/Math.cpp test/MathTest.cpp -I googletest/googletest \n-I googletest/googletest/include -I googletest/googlemock     \n-I googletest/googlemock/include -I src libgtest.a -lpthread\n\n```\n\n请注意，编译命令包括以下包含路径:\n\n```cpp\n-I googletest/googletest\n-I googletest/googletest/include\n-I googletest/googlemock\n-I googletest/googlemock/include\n-I src\n```\n\n此外，值得注意的是，我们还链接了我们的谷歌测试静态库`libgtest.a`和 POSIX pthreads 库，因为谷歌测试框架使用了多个。\n\n![](img/00114.jpeg)\n\n**Figure 7.10**\n\n恭喜你！我们已经成功编译并执行了第一个测试用例。\n\n# 在 Visual Studio IDE 中使用谷歌测试框架\n\n首先，我们需要从[https://github.com/google/googletest/archive/master.zip](https://github.com/google/googletest/archive/master.zip)下载谷歌测试框架`.zip`文件。下一步是提取某个目录下的`.zip`文件。就我而言，我已经将其提取到`googletest`文件夹中，并将`googletest googletest-master\\googletest-master`的所有内容复制到`googletest`文件夹中，如图 7.11 所示:\n\n![](img/00115.jpeg)\n\nFigure 7.11\n\n是时候在 Visual Studio 中创建一个简单的项目了。我用过微软 Visual Studio 社区 2015。但是，这里遵循的过程对于 Visual Studio 的其他版本应该基本保持不变，只是选项可能在不同的菜单中可用。\n\n您需要通过导航到新建项目| Visual Studio | Windows | Win32 | Win32 控制台应用来创建一个名为`MathApp`的新项目，如图 7.12 所示。这个项目将是要测试的产品代码。\n\n![](img/00116.jpeg)\n\nFigure 7.12\n\n让我们将`MyMath`类添加到`MathApp`项目中。`MyMath`类是将在`MyMath.h`中声明并在`MyMath.cpp`中定义的生产代码。\n\n我们来看看*图 7.13* 所示的`MyMath.h`头文件:\n\n![](img/00117.jpeg)\n\nFigure 7.13\n\n`MyMath`类的定义如*图 7.14* 所示:\n\n![](img/00118.jpeg)\n\nFigure 7.14\n\n由于是控制台应用，必须提供主要功能，如图*图 7.15* :\n\n![](img/00119.jpeg)\n\nFigure 7.15\n\n接下来，我们将为同一个`MathApp`项目解决方案添加一个名为`GoogleTestLib`的静态库项目，如图*图 7.16* :\n\n![](img/00120.jpeg)\n\nFigure 7.16\n\n接下来，我们需要将 Google 测试框架中的以下源文件添加到我们的静态库项目中:\n\n```cpp\nC:\\Users\\jegan\\googletest\\googletest\\src\\gtest-all.cc\nC:\\Users\\jegan\\googletest\\googlemock\\src\\gmock-all.cc\nC:\\Users\\jegan\\googletest\\googlemock\\src\\gmock_main.cc\n```\n\n为了编译静态库，我们需要在`GoogleTestLib/Properties/VC++ Directories/Include`目录中包含以下头文件路径:\n\n```cpp\nC:\\Users\\jegan\\googletest\\googletest\nC:\\Users\\jegan\\googletest\\googletest\\include\nC:\\Users\\jegan\\googletest\\googlemock\nC:\\Users\\jegan\\googletest\\googlemock\\include\n```\n\n您可能需要根据您在系统中复制/安装谷歌测试框架的位置来定制路径。\n\n现在是时候将`MathTestApp` Win32 控制台应用添加到`MathApp`解决方案中了。我们需要将`MathTestApp`作为`StartUp`项目，这样我们就可以直接执行这个应用。在向`MathTestApp`项目添加名为`MathTest.cpp`的新源文件之前，让我们确保`MathTestApp`项目中没有源文件。\n\n我们需要配置同一套谷歌测试框架，包括我们添加到`GoogleTestLib`静态库中的路径。除此之外，我们还必须添加`MathApp`项目目录，因为测试项目将引用`MathApp`项目中的头文件，如下所示。但是，根据您在系统中为此项目遵循的目录结构自定义路径:\n\n```cpp\nC:\\Users\\jegan\\googletest\\googletest\nC:\\Users\\jegan\\googletest\\googletest\\include\nC:\\Users\\jegan\\googletest\\googlemock\nC:\\Users\\jegan\\googletest\\googlemock\\include\nC:\\Projects\\MasteringC++ Programming\\MathApp\\MathApp\n```\n\n在`MathAppTest`项目中，确保已经添加了对`MathApp`和`GoogleTestLib`的引用，以便`MathAppTest`项目在感知到其他两个项目的变化时会编译它们。\n\n太好了。我们快完成了。现在来实现`MathTest.cpp`，如图*图 7.17* :\n\n![](img/00121.jpeg)\n\nFigure 7.17\n\n现在一切都准备好了；让我们运行测试用例并检查结果:\n\n![](img/00122.jpeg)\n\nFigure 7.18\n\n# 行动中的 TDD\n\n让我们看看如何开发一个遵循 TDD 方法的**逆波兰符号** ( **RPN** )计算器应用。RPN 也称为后缀符号。RPN Calculator 应用的期望是接受后缀数学表达式作为输入，并返回计算结果作为输出。\n\n我想一步一步地演示如何在开发应用时遵循 TDD 方法。作为第一步，我想解释一下项目目录结构，然后我们继续。让我们创建一个名为`Ex2`的文件夹，其结构如下:\n\n![](img/00123.jpeg)\n\nFigure 7.19\n\n`googletest`文件夹是包含必要的`gtest`和`gmock`头文件的 gtest 测试库。现在`libgtest.a`是我们在前面练习中创建的 Google 测试静态库。我们将使用`make`工具来构建我们的项目，因此我在项目`home`目录中放置了一个`Makefile`。`src`目录将保存生产代码，而测试目录将保存我们将要编写的所有测试用例。\n\n在开始编写测试用例之前，我们先来看一个后缀数学*“2 5 * 4+3 3 * 1+/*，了解一下我们要应用于评估 RPN 数学表达式的标准后缀算法。根据后缀算法，我们将一次解析一个令牌的 RPN 数学表达式。每当我们遇到一个操作数(数字)，我们就要把它推入堆栈。每当我们遇到一个运算符时，我们将从堆栈中弹出两个值，应用数学运算，将中间结果推回到堆栈中，并重复该过程，直到所有标记都在 RPN 表达式中求值。最后，当输入字符串中没有更多的标记时，我们将弹出值并将其作为结果打印出来。下图中逐步演示了该过程:\n\n![](img/00124.jpeg)\n\nFigure 7.20\n\n首先，让我们采用一个简单的后缀数学表达式，并将场景转换为一个测试用例:\n\n```cpp\nTest Case : Test a simple addition\nInput: \"10 15 +\"\nExpected Output: 25.0\n```\n\n让我们将前面的测试用例翻译成测试文件夹中的谷歌测试，如下所示:\n\n```cpp\ntest/RPNCalculatorTest.cpp\n\nTEST ( RPNCalculatorTest, testSimpleAddition ) { \n         RPNCalculator rpnCalculator; \n         double actualResult = rpnCalculator.evaluate ( \"10 15 +\" ); \n         double expectedResult = 25.0; \n         EXPECT_EQ ( expectedResult, actualResult ); \n}\n```\n\n为了编译前面的测试用例，让我们编写`src`文件夹中所需的最小生产代码，如下所示:\n\n```cpp\nsrc/RPNCalculator.h\n\n#include <iostream>\n#include <string>\nusing namespace std;\n\nclass RPNCalculator {\n  public:\n      double evaluate ( string );\n};\n```\n\n由于 RPN 数学表达式将以空格分隔的字符串形式提供，因此 evaluate 方法将采用字符串输入参数:\n\n```cpp\nsrc/RPNCalculator.cpp\n\n#include \"RPNCalculator.h\"\n\ndouble RPNCalculator::evaluate ( string rpnMathExpression ) {\n    return 0.0;\n}\n```\n\n下面的`Makefile`类帮助我们在每次编译生产代码时运行测试用例:\n\n![](img/00125.jpeg)\n\nFigure 7.21\n\n现在让我们构建并运行测试用例，并检查测试用例的结果:\n\n![](img/00126.jpeg)\n\nFigure 7.22\n\n在 TDD 中，我们总是从失败的测试用例开始。失败的根本原因是预期结果是 25，而实际结果是 0。原因是我们没有实现 evaluate 方法，因此我们硬编码为返回 0，而不考虑任何输入。因此，让我们实现评估方法，以使测试用例通过。\n\n我们需要修改`src/RPNCalculator.h`和`src/RPNCalculator.cpp`如下:\n\n![](img/00127.jpeg)\n\nFigure 7.23\n\n在 RPNCalculator.h 头文件中，观察包含的新头文件，以处理字符串标记化和字符串双转换，并将 RPN 标记复制到向量中:\n\n![](img/00128.jpeg)\n\nFigure 7.24\n\n根据标准的后缀算法，我们使用一个堆栈来保存我们在 RPN 表达式中找到的所有数字。每当我们遇到`+`数学运算符时，我们都会从堆栈中弹出两个值，并将它们相加，并将结果推回到堆栈中。如果令牌不是`+`运算符，我们可以放心地假设它是一个数字，所以我们只需将值推送到堆栈中。\n\n有了前面的实现，让我们尝试测试用例并检查测试用例是否通过:\n\n![](img/00129.jpeg)\n\nFigure 7.25\n\n酷，我们的第一个测试用例已经按预期通过了。是时候考虑另一个测试用例了。这次，让我们为减法添加一个测试用例:\n\n```cpp\nTest Case : Test a simple subtraction\nInput: \"25 10 -\"\nExpected Output: 15.0\n```\n\n让我们将前面的测试用例翻译成测试文件夹中的谷歌测试，如下所示:\n\n```cpp\ntest/RPNCalculatorTest.cpp\n\nTEST ( RPNCalculatorTest, testSimpleSubtraction ) { \n         RPNCalculator rpnCalculator; \n         double actualResult = rpnCalculator.evaluate ( \"25 10 -\" ); \n         double expectedResult = 15.0; \n         EXPECT_EQ ( expectedResult, actualResult ); \n}\n```\n\n将前面的测试用例添加到`test/RPNCalculatorTest`中，现在应该是这样的:\n\n![](img/00130.jpeg)\n\nFigure 7.26\n\n让我们执行测试用例，并检查我们的新测试用例是否通过:\n\n![](img/00131.jpeg)\n\nFigure 7.27\n\n正如预期的那样，新测试失败了，因为我们还没有在应用中增加对减法的支持。这是非常明显的，基于 C++ 异常，因为代码试图将减法`-`运算符转换为数字。让我们在评估方法中增加对减法逻辑的支持:\n\n![](img/00132.jpeg)\n\nFigure 7.28\n\n是时候测试了。让我们执行测试用例，检查事情是否正常:\n\n![](img/00133.jpeg)\n\nFigure 7.29\n\n酷！你注意到我们的测试用例在这个例子中失败了吗？等一下。如果测试用例失败了，我们为什么会兴奋？我们应该高兴的原因是我们的测试用例发现了一个 bug 毕竟，这是 TDD 的主要意图，不是吗？\n\n![](img/00134.jpeg)\n\nFigure 7.30\n\n失败的根本原因是堆栈基于**后进先出** ( **后进先出**)进行操作，而我们的代码假设先进先出。你有没有注意到我们的代码假设它会先弹出第一个数字，而实际情况是它应该先弹出第二个数字？有意思，这个 bug 也在加法运算中；然而，由于加法是关联的，这个错误被抑制了，但是减法测试用例检测到了它。\n\n![](img/00135.jpeg)\n\nFigure 7.31\n\n让我们修复前面截图中显示的错误，并检查测试用例是否会通过:\n\n![](img/00136.jpeg)\n\nFigure 7.32\n\n太棒了。我们修复了这个错误，我们的测试用例似乎证明它们已经被修复了。让我们添加更多的测试用例。这次，让我们添加一个测试用例来验证乘法:\n\n```cpp\nTest Case : Test a simple multiplication\nInput: \"25 10 *\"\nExpected Output: 250.0\n```\n\n让我们将前面的测试用例翻译成测试文件夹中的 google 测试，如下所示:\n\n```cpp\ntest/RPNCalculatorTest.cpp\n\nTEST ( RPNCalculatorTest, testSimpleMultiplication ) { \n         RPNCalculator rpnCalculator; \n         double actualResult = rpnCalculator.evaluate ( \"25 10 *\" ); \n         double expectedResult = 250.0; \n         EXPECT_EQ ( expectedResult, actualResult ); \n}\n```\n\n我们知道这一次测试用例将会失败，所以让我们快进并看看分部测试用例:\n\n```cpp\nTest Case : Test a simple division\nInput: \"250 10 /\"\nExpected Output: 25.0\n```\n\n让我们将前面的测试用例翻译成测试文件夹中的 google 测试，如下所示:\n\n```cpp\ntest/RPNCalculatorTest.cpp\n\nTEST ( RPNCalculatorTest, testSimpleDivision ) { \n         RPNCalculator rpnCalculator; \n         double actualResult = rpnCalculator.evaluate ( \"250 10 /\" ); \n         double expectedResult = 25.0; \n         EXPECT_EQ ( expectedResult, actualResult );\n}\n```\n\n让我们跳过测试结果，继续进行涉及许多操作的最终复杂表达式测试用例:\n\n```cpp\nTest Case : Test a complex rpn expression\nInput: \"2  5  *  4  + 7  2 -  1  +  /\"\nExpected Output: 25.0\n```\n\n让我们将前面的测试用例翻译成测试文件夹中的 google 测试，如下所示:\n\n```cpp\ntest/RPNCalculatorTest.cpp\n\nTEST ( RPNCalculatorTest, testSimpleDivision ) { \n         RPNCalculator rpnCalculator; \n         double actualResult = rpnCalculator.evaluate ( \"250 10 /\" ); \n         double expectedResult = 25.0; \n         EXPECT_EQ ( expectedResult, actualResult );\n}\n```\n\n让我们用下面的测试用例来检查我们的 RPNCalculator 应用是否能够在单个表达式中计算复杂的 RPN 表达式，该表达式涉及加法、减法、乘法和除法:\n\n```cpp\ntest/RPNCalculatorTest.cpp\n\nTEST ( RPNCalculatorTest, testComplexExpression ) { \n         RPNCalculator rpnCalculator; \n         double actualResult = rpnCalculator.evaluate ( \"2  5  *  4  +  7  2 - 1 +  /\" ); \n         double expectedResult = 2.33333; \n         ASSERT_NEAR ( expectedResult, actualResult, 4 );\n}\n```\n\n在前面的测试案例中，我们正在检查预期结果是否与实际结果匹配，接近小数点后四位。如果这些值超出了这个近似值，那么测试用例应该会失败。\n\n现在让我们检查测试用例输出:\n\n![](img/00137.jpeg)\n\nFigure 7.33\n\n太好了。所有的测试用例都是绿色的。\n\n现在让我们看看我们的生产代码，看看是否有改进的空间:\n\n![](img/00138.jpeg)\n\nFigure 7.34\n\n代码在功能上是好的，但是有很多代码异味。这是一个嵌套了`if-else`条件和重复代码的长方法。TDD 不仅仅是测试自动化；它也是关于编写没有代码味道的好代码。因此，我们必须重构代码，使其更加模块化，并降低代码复杂性。\n\n我们可以在这里应用多态或者策略设计模式来代替嵌套的`if-else`条件。此外，我们可以使用工厂方法设计模式来创建各种子类型。还可以使用空对象设计模式。\n\n最棒的是，我们不必担心在重构过程中破坏代码的风险，因为我们有足够数量的测试用例来给我们反馈，以防我们破坏代码。\n\n首先，让我们了解如何重构*图 7.35* 所示的 RPNCalculator 设计:\n\n![](img/00139.gif)\n\nFigure 7.35\n\n基于前面的设计重构方法，我们可以重构 RPNCalculator，如图*图 7.36* :\n\n![](img/00140.jpeg)\n\nFigure 7.36\n\n如果你对比一下重构前后的`RPNCalculator`代码，你会发现重构后的代码复杂度已经下降到了一个相当可观的数量。\n\n`MathFactory`类可以实现，如图*图 7.37* :\n\n![](img/00141.jpeg)\n\nFigure 7.37\n\n尽可能地，我们必须努力避免`if-else`条件，或者一般来说，我们必须尽可能地避免代码分支。因此，STL 映射用于避免 if-else 条件。这也促进了相同数学对象的重用，而不管 RPN 表达式的复杂性如何。\n\n如果你参考*图 7.38* ，你会了解到`MathOperator Add`类是如何实现的:\n\n![](img/00142.jpeg)\n\nFigure 7.38\n\n`Add`类定义如图*图 7.39* 所示:\n\n![](img/00143.jpeg)\n\nFigure 7.39\n\n减法、乘法和除法类可以以类似的方式实现，如`Add`类。底线是，重构之后，我们可以将单个`RPNCalculator`类重构为更小且可维护的类，这些类可以单独测试。\n\n让我们看看图 7.40 中重构的`Makefile`类，并在重构过程完成后测试我们的代码:\n\n![](img/00144.jpeg)\n\nFigure 7.40\n\n如果一切顺利的话，如果没有任何功能被破坏，我们应该会看到重构后的所有测试用例都通过了，如图*图 7.41* :\n\n![](img/00145.jpeg)\n\nFigure 7.41\n\n酷！所有的测试用例都通过了，因此可以保证我们在重构过程中没有破坏功能。TDD 的主要目的是编写功能和结构都干净的可测试代码。\n\n# 测试一段有依赖性的遗留代码\n\n在前一节中，CUT 是独立的，没有依赖性，因此它测试代码的方式很简单。然而，让我们讨论如何对有依赖关系的 CUT 进行单元测试。为此，请参考下图:\n\n![](img/00146.gif)\n\nFigure 7.42\n\n在*图 7.42* 中，很明显 **Mobile** 对**摄像头**有依赖关系， **Mobile** 和**摄像头**之间的关联是*构图*。让我们看看`Camera.h`头文件是如何在遗留应用中实现的:\n\n![](img/00147.jpeg)\n\nFigure 7.43\n\n出于演示目的，让我们来看这个简单的`Camera`类，它具有`ON()`和`OFF()`功能。让我们假设开/关功能将在内部与摄像机硬件交互。查看*图 7.44* 中的`Camera.cpp`源文件:\n\n![](img/00148.jpeg)\n\nFigure 7.44\n\n出于调试的目的，我添加了一些打印语句，当我们测试手机的`powerOn()`和`powerOff()`功能时，这些语句会派上用场。现在我们来查看一下*图 7.45* 中的`Mobile`类头文件:\n\n![](img/00149.jpeg)\n\nFigure 7.45\n\n我们继续移动实现，如图 7.46 所示:\n\n![](img/00150.jpeg)\n\nFigure 7.46\n\n从`Mobile`构造器的实现来看，很明显手机有摄像头或者说是精确的构图关系。换句话说，`Mobile`类就是构造`Camera`对象的类，如图*图 7.46* 、*第 21 行*所示，在构造器中。让我们试着看看测试移动`powerOn()`功能的复杂性；依赖关系与移动的 CUT 有合成关系。\n\n假设摄像机开启成功，我们编写`powerOn()`测试用例，如下所示:\n\n```cpp\nTEST ( MobileTest, testPowerOnWhenCameraONSucceeds ) {\n\n     Mobile mobile;\n     ASSERT_TRUE ( mobile.powerOn() );\n\n}\n```\n\n现在让我们尝试运行`Mobile`测试用例并检查测试结果，如图*图 7.47* 所示:\n\n![](img/00151.jpeg)\n\nFigure 7.47\n\n从*图 7.47* 可以了解到`Mobile`的`powerOn()`测试用例已经通过。然而，我们也明白真正的`Camera`类的`ON()`方法也被调用了。反过来，这将与相机硬件交互。归根结底，这不是一个单元测试，因为测试结果并不完全依赖于 CUT。如果测试用例失败了，我们将无法确定失败是由于移动设备的`powerOn()`逻辑中的代码还是摄像机的`ON()`逻辑中的代码，这将违背我们测试用例的目的。理想的单元测试应该使用依赖注入将 CUT 与其依赖项隔离开来，并测试代码。这种方法将帮助我们识别正常或异常场景中 CUT 的行为。理想情况下，当一个单元测试用例失败时，我们应该能够在不调试代码的情况下猜测失败的根本原因；只有当我们设法隔离 CUT 的依赖关系时，这才是可能的。\n\n这种方法的主要好处是，甚至在实现依赖项之前就可以测试 CUT，这有助于测试 60 ~ 70%没有依赖项的代码。这自然会减少软件产品的上市时间。\n\n这就是谷歌模拟或 gmock 派上用场的地方。让我们检查一下如何重构代码来启用依赖注入。虽然听起来很复杂，但是重构代码所需的工作并没有那么复杂。实际上，重构产品代码所需的工作可能更复杂，但这是值得的。让我们来看看*图 7.48* 中所示的重构`Mobile`类:\n\n![](img/00152.jpeg)\n\nFigure 7.48\n\n在`Mobile`类中，我添加了一个以 camera 为参数的重载构造函数。这种技术被称为**构造函数依赖注入**。让我们看看这种简单而强大的技术如何帮助我们在测试移动设备的`powerOn()`功能时隔离相机依赖性。\n\n此外，我们必须重构`Camera.h`头文件并将`ON()`和`OFF()`方法声明为虚拟的，以便 gmock 框架帮助我们存根这些方法，如图 7.49 所示:\n\n![](img/00153.jpeg)\n\nFigure 7.49\n\n现在让我们重构我们的测试用例，如图 7.50*所示:*\n\n![](img/00154.jpeg)\n\nFigure 7.50\n\n我们都准备好构建和执行测试用例了。测试结果预计如*图 7.51* 所示:\n\n![](img/00155.jpeg)\n\nFigure 7.51\n\n酷！我们的测试用例不仅通过了，而且我们还将我们的 CUT 从它的相机依赖中分离出来，这是显而易见的，因为我们没有看到来自相机的`ON()`方法的打印语句。最重要的是，您现在已经学会了如何通过隔离代码的依赖关系来进行单元测试。\n\nTDD 快乐！\n\n# 摘要\n\n在这一章中，您学习了很多关于 TDD 的知识，以下是关键要点的总结:\n\n*   TDD 是一种极限编程实践\n*   TDD 是一种自下而上的方法，它鼓励我们从测试用例开始，因此它通常被称为低成本测试优先开发\n*   您学习了如何在 Linux 和 Windows 中使用谷歌测试和谷歌模拟框架编写测试用例\n*   您还学习了如何在 Windows 平台上编写遵循 Linux 和 Visual Studio 中 TDD 的应用\n*   您已经学习了依赖项反转技术，以及如何使用谷歌模拟框架隔离代码的依赖项来进行单元测试\n*   谷歌测试框架支持单元测试、集成测试、回归测试、性能测试、功能测试等等\n*   TDD 主要坚持单元测试、集成测试和交互测试，而复杂的功能测试必须通过行为驱动开发来完成\n*   您学习了如何将代码重构为整洁的代码，同时您编写的单元测试用例给出持续的反馈\n\n您已经学习了 TDD 以及如何以自下而上的方式自动化单元测试用例、集成测试用例和交互测试用例。使用 BDD，您将学习自上而下的开发方法，编写端到端的功能和测试用例以及其他复杂的测试场景，这些我们在讨论 TDD 时没有涉及到。\n\n在下一章中，您将学习行为驱动开发。**"
  },
  {
    "path": "docs/master-cpp-prog/08.md",
    "content": "# 八、行为驱动开发\n\n本章涵盖以下主题:\n\n*   行为驱动开发概述\n*   TDD 与 BDD\n*   C++ BDD 框架\n*   小黄瓜语言\n*   在 Ubuntu 中安装`cucumber-cpp`\n*   特征文件\n*   小黄瓜支持的口语\n*   推荐的`cucumber-cpp`项目文件夹结构\n*   编写我们的第一个黄瓜测试用例\n*   试运行我们的黄瓜测试用例\n*   BDD——一种测试优先的开发方法\n\n在接下来的几节中，让我们以一种实用的方式，用易于理解且有趣的代码示例来研究每个主题。\n\n# 行为驱动开发\n\n**行为驱动开发** ( **BDD** )是一种由外而内的开发技术。BDD 鼓励将需求捕获为一组场景或用例，描述最终用户将如何使用该特性。该场景将精确地表达所提供的输入以及该特性的预期响应。BDD 最棒的地方在于它使用了一种叫做**小黄瓜**的**特定领域语言** ( **DSL** )来描述 BDD 场景。\n\n小黄瓜是一种类似英语的语言，被所有的 BDD 测试框架使用。小黄瓜是一个业务可读的 DSL，它帮助您描述测试用例场景，保留了实现细节。小黄瓜语言关键词是一堆英文单词；因此，软件产品或项目团队中的技术和非技术成员都可以理解这些场景。\n\n我有没有告诉过你，用小黄瓜语言编写的 BDD 场景既是文档又是测试用例？由于小黄瓜语言易于理解，并使用类似英语的关键词，因此产品需求可以作为 BDD 场景直接捕获，而不是枯燥的 Word 或 PDF 文档。根据我的咨询和行业经验，我观察到，当设计在适当的时候被重构时，大多数公司从不更新需求文档。这会导致文档陈旧和不更新，开发团队不会信任这些文档作为参考。因此，从长远来看，为准备需求、高级设计文档和低级设计文档所做的努力是徒劳的，而黄瓜测试用例将一直保持更新和相关。\n\n# TDD 与 BDD\n\nTDD 是一种由内而外的开发技术，而 BDD 是一种由外而内的开发技术。TDD 主要关注单元测试和集成测试用例自动化。\n\nBDD 侧重于端到端的功能测试用例和用户接受测试用例。然而，BDD 也可以用于单元测试、冒烟测试，以及字面上的各种类型的测试。\n\nBDD 是 TDD 方法的延伸；因此，BDD 也大力鼓励测试优先开发。在同一产品中同时使用 BDD 和 TDD 是很自然的；因此，BDD 不能替代 TDD。BDD 可以认为是高级设计文档，而 TDD 是低级设计文档。\n\n# C++ BDD 框架\n\n在 C++ 中，TDD 测试用例是使用 CppUnit、gtest 等测试框架编写的，这些测试框架需要技术背景才能理解它们，因此通常只由开发人员使用。\n\n在 C++ 中，BDD 测试用例是使用一个流行的测试框架编写的，该框架被称为黄瓜-cpp。黄瓜-cpp 框架期望测试用例用 Gherkin 语言编写，而实际的测试用例实现可以用任何测试框架来完成，比如 gtest 或 CppUnit。\n\n然而，在本书中，我们将使用黄瓜-cpp 和 gtest 框架。\n\n# 小黄瓜语言\n\n对于享受 BDD 支持的各种编程语言，小黄瓜是每个 BDD 框架使用的通用语言。\n\n小黄瓜是一种面向行的语言，类似于 YAML 或 Python。小黄瓜将根据缩进解释测试用例的结构。\n\n`#`字符用于小黄瓜中的单行注释。在写这本书的时候，小黄瓜支持 60 个左右的关键词。\n\n小黄瓜是黄瓜框架使用的 DSL。\n\n# 在 Ubuntu 中安装黄瓜 cpp\n\n在 Linux 中安装黄瓜-cpp 框架非常简单。你所需要做的就是下载或者克隆最新的黄瓜 cpp。\n\n以下命令可用于克隆黄瓜-cpp 框架:\n\n```cpp\ngit clone https://github.com/cucumber/cucumber-cpp.git\n```\n\nLinux、Windows 和 Macintosh 都支持黄瓜-cpp 框架。可以在 Windows 上与 Visual Studio 集成，也可以在 macOS 上与 Xcode 集成。\n\n下面的截图演示了 Git 克隆过程:\n\n![](img/00156.jpeg)\n\n由于黄瓜-cpp 依赖于有线协议来允许用 C++ 语言编写 BDD 测试用例步骤定义，我们需要安装 Ruby。\n\n# 安装黄瓜-cpp 框架必备软件\n\n以下命令帮助您在 Ubuntu 系统上安装 Ruby。这是黄瓜-cpp 框架所需的必备软件之一:\n\n```cpp\nsudo apt install ruby\n```\n\n下面的截图演示了 Ruby 的安装过程:\n\n![](img/00157.jpeg)\n\n安装完成后，请通过检查版本来确保 Ruby 安装正确。以下命令应该打印系统上安装的 Ruby 版本:\n\n```cpp\nruby --version\n```\n\n为了完成 Ruby 安装，我们需要安装`ruby-dev`包，如下所示:\n\n```cpp\nsudo apt install ruby-dev\n```\n\n接下来，我们需要确保安装 bundler 工具，以便 bundler 工具无缝地安装 Ruby 依赖项:\n\n```cpp\nsudo gem install bundler\nbundle install\n```\n\n如果一切顺利，您可以继续检查黄瓜、Ruby 和 Ruby 工具的正确版本是否安装正确。`bundle install`命令将确保安装了黄瓜和其他 Ruby 依赖项。确保不要以 sudo 用户的身份安装`bundle install`；这将阻止非 root 用户访问 Ruby gem 包:\n\n![](img/00158.jpeg)\n\n我们差不多完成了，但我们还没有到那一步。我们需要建立黄瓜-cpp 项目；作为其中的一部分，让我们获取黄瓜-cpp 框架的最新测试套件:\n\n```cpp\ngit submodule init\ngit submodule update\n```\n\n在开始构建之前，我们继续安装忍者和增强库。虽然我们不会在本章中使用 boost 测试框架，但是`travis.sh`脚本文件会查找 boost 库。因此，我建议一般安装增强库，作为黄瓜的一部分:\n\n```cpp\nsudo apt install ninja-build\nsudo apt-get install libboost-all-dev\n```\n\n# 构建和执行测试用例\n\n现在，是时候构建黄瓜-cpp 框架了。让我们创建`build`文件夹。在`cucumber-cpp`文件夹中，会有一个名为`travis.sh`的 shell 脚本。您必须执行脚本来构建和执行测试用例:\n\n```cpp\nsudo ./travis.sh\n```\n\n虽然前面的方法有效，但我个人的偏好和建议是下面的方法。推荐以下方法的原因是`build`文件夹应该作为非根用户创建，因为一旦`cucumber-cpp`设置完成，任何人都应该能够执行构建。您应该可以在`cucumber-cpp`文件夹下的`README.md`文件中找到说明:\n\n```cpp\ngit submodule init\ngit submodule update\ncmake -E make_directory build\ncmake -E chdir build cmake --DCUKE_ENABLE_EXAMPLES=on ..\ncmake --build build\ncmake --build build --target test\ncmake --build build --target features\n```\n\n如果您能够完全按照说明完成前面的所有安装步骤，您就可以开始玩`cucumber-cpp`了。恭喜你！！！\n\n# 特征文件\n\n每个产品特性都有一个专用的特性文件。特征文件是扩展名为`.feature`的文本文件。一个特性文件可以包含任意数量的场景，每个场景相当于一个测试用例。\n\n让我们看一个简单的特性文件示例:\n\n```cpp\n1   # language: en\n2\n3   Feature: The Facebook application should authenticate user login.\n4\n5     Scenario: Successful Login\n6        Given I navigate to Facebook login page https://www.facebook.com\n7        And I type jegan@tektutor.org as Email\n8        And I type mysecretpassword as Password\n9        When I click the Login button\n10       Then I expect Facebook Home Page after Successful Login\n```\n\n很酷，看起来像简单的英语，对吧？但是相信我，黄瓜测试用例就是这样写的！我理解你的疑问——看起来简单又酷，但这如何验证功能，验证功能的代码在哪里？`cucumber-cpp`框架是一个很酷的框架，但是它本身并不支持任何测试功能；因此`cucumber-cpp`依赖于 gtest、`CppUnit`、其他测试框架。测试用例实现写在`Steps`文件中，在我们的案例中可以使用 gtest 框架用 C++ 编写。然而，任何测试框架都可以工作。\n\n每个特征文件将以`Feature`关键字开始，后跟一行或多行描述，简要描述特征。在特征文件中，`Feature`、`Scenario`、`Given`、`And`、`When`、`Then`都是小黄瓜关键词。\n\n一个特性文件可以包含任意数量的特性场景(测试用例)。例如，在我们的案例中，登录是一项功能，可能有如下多个登录场景:\n\n*   `Success Login`\n*   `Unsuccessful Login`\n*   `Invalid password`\n*   `Invalid username`\n*   `The user attempted to login without supplying credentials.`\n\n场景后面的每一行都将转化为`Steps_definition.cpp`源文件中的一个函数。基本上，`cucumber-cpp`框架使用正则表达式将特征文件步骤映射到`Steps_definition.cpp`文件中的相应函数。\n\n# 小黄瓜支持的口语\n\n小黄瓜支持 60 多种口语。作为最佳实践，特性文件的第一行将向黄瓜框架表明我们希望使用英语:\n\n```cpp\n1   # language: en\n```\n\n以下命令将列出`cucumber-cpp`框架支持的所有口语:\n\n```cpp\ncucumber -i18n help\n```\n\n名单如下:\n\n![](img/00159.jpeg)\n\n# 推荐的黄瓜-cpp 项目文件夹结构\n\n像 TDD 一样，黄瓜框架也推荐项目文件夹结构。推荐的`cucumber-cpp`项目文件夹结构如下:\n\n![](img/00160.jpeg)\n\n`src`文件夹将包含生产代码，也就是说，您的所有项目文件都将保存在`src`目录下。BDD 特性文件将保存在`features`文件夹及其各自的`Steps`文件下，该文件包含增强测试用例或 gtest 用例。在本章中，我们将使用`cucumber-cpp`的 gtest 框架。`wire`文件包含有线协议相关的连接细节，如端口等。`CMakeLists.txt`是构建脚本，它包含构建项目的指令及其依赖细节，就像`MakeBuild`实用程序使用的`Makefile`一样。\n\n# 编写我们的第一个黄瓜测试用例\n\n让我们编写第一个黄瓜测试用例！由于这是我们的第一次练习，我想保持简短。首先，让我们为我们的`HelloBDD`项目创建文件夹结构。\n\n要创建黄瓜项目文件夹结构，我们可以使用`cucumber`实用程序，如下所示:\n\n```cpp\ncucumber --init\n```\n\n这将确保`features`和`steps_definitions`文件夹是根据黄瓜最佳实践创建的:\n\n![](img/00161.jpeg)\n\n一旦创建了基本的文件夹结构，让我们手动创建其余的文件:\n\n```cpp\nmkdir src\ncd HelloBDD\ntouch CMakeLists.txt\ntouch features/hello.feature\ntouch features/step_definitions/cucumber.wire\ntouch features/step_definitions/HelloBDDSteps.cpp\ntouch src/Hello.h\ntouch src/Hello.cpp\n```\n\n一旦创建了文件夹结构和空文件，项目文件夹结构应该如下图所示:\n\n![](img/00162.jpeg)\n\n是时候开始将我们的小黄瓜知识应用到行动中了；因此，让我们首先从特征文件开始:\n\n```cpp\n# language: en\n\nFeature: Application should be able to print greeting message Hello BDD!\n\n   Scenario: Should be able to greet with Hello BDD! message\n      Given an instance of Hello class is created\n      When the sayHello method is invoked\n      Then it should return \"Hello BDD!\"\n```\n\n我们来看看`cucumber.wire`文件:\n\n```cpp\nhost: localhost\nport: 3902\n```\n\nAs Cucumber is implemented in Ruby, the Cucumber steps implementation has to be written in Ruby. This approach discourages using the cucumber-cpp framework for projects that are implemented in platforms other than Ruby. The wire protocol is the solution offered by the cucumber-cpp framework to extend cucumber support for non-Ruby platforms. Basically, whenever the cucumber-cpp framework executes the test cases, it looks for steps definitions, but if Cucumber finds a `.wire` file, it will instead connect to that IP address and port, in order to query the server if the process has definitions for the steps described in the `.feature` file. This helps Cucumber support many platforms apart from Ruby. However, Java and .NET have native Cucumber implementations: Cucumber-JVM and Specflow, respectively. Hence, in order to allow the test cases to be written in C++, the wire protocol is used by cucumber-cpp.\n\n现在让我们看看如何使用 gtest 框架编写步骤文件。\n\nThanks to Google! The Google Test Framework (gtest) includes **Google Mock Framework** (**gmock**). For C/C++, the gtest framework is one of the best frameworks I have come across, as this is pretty close to the JUnit and Mockito/PowerMock offerings for Java. For a relatively modern language like Java compared to C++, it should be much easier to support mocking with the help of reflection, but from a C/C++ point of view, without the reflection feature from C++, gtest/gmock is nothing short of JUnit/TestNG/Mockito/PowerMock.\n\n您可以在下面的屏幕截图中观察使用 gtest 编写的步骤文件:\n\n![](img/00163.jpeg)\n\n以下头文件确保包含编写黄瓜步骤所需的 gtest 头和黄瓜头:\n\n```cpp\n#include <gtest/gtest.h>\n#include <cucumber-cpp/autodetect.hpp>\n```\n\n现在让我们继续编写步骤:\n\n```cpp\nstruct HelloCtx {\n     Hello *ptrHello;\n     string actualResponse;\n};\n```\n\n`HelloCtx`结构是一个用户定义的测试上下文，用于保存被测对象实例及其测试响应。黄瓜-cpp 框架提供了一个智能的`ScenarioScope`类，允许我们在黄瓜测试场景的所有步骤中访问被测对象及其输出。\n\n对于我们在特征文件中写的每一条`Given`、`When`、`Then`语句，steps 文件中都有对应的函数。借助正则表达式映射对应于`Given`、`When`和`Then`的适当的 cpp 函数。\n\n例如，考虑特征文件中的以下`Given`行:\n\n```cpp\nGiven an instance of Hello class is created\n```\n\n这对应于下面的 cpp 函数，它在 regex 的帮助下被映射。正则表达式中的`^`字符表示模式以`an`开始，`$`字符表示模式以`created`结束:\n\n```cpp\nGIVEN(\"^an instance of Hello class is created$\")\n{\n       ScenarioScope<HelloCtx> context;\n       context->ptrHello = new Hello();\n}\n```\n\n正如`GIVEN`步骤所说，此时，我们必须确保创建了`Hello`对象的一个实例；相应的 C++ 代码写在这个函数中，用来实例化`Hello`类的一个对象。\n\n类似地，以下`When`步骤及其对应的 cpp 函数由黄瓜-cpp 映射:\n\n```cpp\nWhen the sayHello method is invoked\n```\n\n正则表达式精确匹配很重要；否则，黄瓜-cpp 框架将报告它找不到 steps 函数:\n\n```cpp\nWHEN(\"^the sayHello method is invoked$\")\n{\n       ScenarioScope<HelloCtx> context;\n       context->actualResponse = context->ptrHello->sayHello();\n}\n```\n\n现在我们来看看`Hello.h`文件:\n\n```cpp\n#include <iostream>\n#include <string>\nusing namespace std;\n\nclass Hello {\npublic:\n       string sayHello();\n};\n```\n\n下面是各自的源文件，即`Hello.cpp`:\n\n```cpp\n#include \"Hello.h\"\n\nstring Hello::sayHello() {\n     return \"Hello BDD!\";\n}\n```\n\nAs an industry best practice, the only header file that should be included in the source file is its corresponding header file. The rest of the headers required should go into the header files corresponding to the source file. This helps the development team to locate the headers quite easily. BDD is not just about test automation; the expected end result is clean, defectless, and maintainable code.\n\n最后我们来写`CMakeLists.txt`:\n\n![](img/00164.jpeg)\n\n第一行暗示了项目的名称。第三行确保黄瓜标题目录和我们项目的`include_directories`在`INCLUDE`路径中。第五行基本上指示`cmake`实用程序从`src`文件夹下的文件，即`Hello.cpp`及其`Hello.h`文件中创建一个库。第七行检测我们的系统上是否安装了 gtest 框架，第八行保证`HelloBDDSteps.cpp`文件编译完成。最后，在第九行，创建了最终的可执行文件，链接了所有具有我们的生产代码的`HelloBDD`库、`HelloBDDSteps`对象文件以及相应的黄瓜和 gtest 库文件。\n\n# 将我们的项目集成到黄瓜中\n\n在开始构建项目之前，我们还需要做最后一个配置:\n\n![](img/00165.jpeg)\n\n基本上，我已经评论了`examples`子目录，并在`cucumber-cpp`文件夹下的`CMakeLists.txt`中添加了我们的`HelloBDD`项目，如前所示。\n\n由于我们已经根据黄瓜-cpp 最佳实践创建了项目，让我们导航到`HelloBDD`项目主页并发出以下命令:\n\n```cpp\ncmake --build  build\n```\n\nIt isn't mandatory to comment `add_subdirectory(examples)`. But commenting definitely helps us focus on our project.\n\n下面的屏幕截图显示了构建过程:\n\n![](img/00166.jpeg)\n\n# 执行我们的测试用例\n\n现在让我们执行测试用例。这包括两个步骤，因为我们使用的是有线协议。让我们首先在后台模式下启动可执行的测试用例，然后启动黄瓜，如下所示:\n\n```cpp\ncmake --build build\nbuild/HelloBDD/HelloBDDSteps > /dev/null &\ncucumber HelloBDD\n```\n\n重定向到`/dev/null`并不是强制性的。重定向到空设备的主要目的是避免应用可能在终端输出中吐出的打印语句分散注意力。因此，这是个人偏好。如果您希望从应用中看到调试或一般打印语句，请随意发出命令，无需重定向:\n\n`build/HelloBDD/HelloBDDSteps &`\n\n下面的截图演示了构建和测试执行过程:\n\n![](img/00167.jpeg)\n\n恭喜你！我们的第一个黄瓜-cpp 测试用例已经通过。每个场景代表一个测试用例，测试用例包括三个步骤；随着所有步骤的通过，场景被报告为通过。\n\n# 运行你的黄瓜测试用例\n\n要不要在不真正执行的情况下，快速检查特征文件和步骤文件是否写对？黄瓜有一个快速又酷的特点:\n\n```cpp\nbuild/HelloBDD/HelloBDDSteps > /dev/null &\n```\n\n该命令将在后台模式下执行我们的测试应用。`/dev/null`是 Linux 操作系统中的一个空设备，我们正在将所有不需要的打印语句从`HelloBDDSteps`可执行文件重定向到空设备，以确保它不会在我们执行黄瓜测试用例时分散我们的注意力。\n\n下一个命令将允许我们模拟黄瓜测试场景:\n\n```cpp\ncucumber --dry-run \n```\n\n下面的截图显示了测试的执行情况:\n\n![](img/00168.jpeg)\n\n# BDD——一种测试优先的开发方法\n\n和 TDD 一样，BDD 也坚持遵循测试优先的开发方法。因此，在这一节中，让我们探索如何按照测试优先的开发方法 BDD 的方式来编写端到端的特性！\n\n让我们举一个简单的例子来帮助我们理解 BDD 的编码风格。我们将编写一个`RPNCalculator`应用，它可以进行加法、减法、乘法、除法和复杂的数学表达式，这些表达式在同一个输入中涉及许多数学运算。\n\n让我们按照黄瓜标准创建我们的项目文件夹结构:\n\n```cpp\nmkdir RPNCalculator\ncd RPNCalculator\ncucumber --init\ntree\nmkdir src\ntree\n```\n\n下面的截图直观地演示了该过程:\n\n![](img/00169.jpeg)\n\n太好了。文件夹结构现已创建。现在，让我们使用 touch 实用程序创建空文件，以帮助我们可视化最终的项目文件夹结构以及文件:\n\n```cpp\ntouch features/rpncalculator.feature\ntouch features/step_definitions/RPNCalculatorSteps.cpp\ntouch features/step_definitions/cucumber.wire\ntouch src/RPNCalculator.h\ntouch src/RPNCalculator.cpp\ntouch CMakeLists.txt\n```\n\n创建虚拟文件后，最终的项目文件夹结构将如下图所示:\n\n![](img/00170.jpeg)\n\n像往常一样，黄瓜线文件将如下所示。事实上，在本章中，这个文件看起来是一样的:\n\n```cpp\nhost: localhost\nport: 3902\n```\n\n现在我们从`rpncalculator.feature`文件开始，如下图截图所示:\n\n![](img/00171.jpeg)\n\n如您所见，特性描述可能非常复杂。你注意到了吗？我用`Scenario Outline`代替了场景。`Scenario Outline`的有趣之处在于，它允许在`Examples`黄瓜部分下以表格的形式描述一组输入和相应的输出。\n\nIf you are familiar with SCRUM, does the Cucumber scenario look pretty close to the user story? Yes, that's the idea. Ideally, the SCRUM user stories or use cases can be written as Cucumber scenarios. The Cucumber feature file is a live document that can be executed.\n\n我们需要在`cucumber-cpp`主目录的`CMakeLists.txt`文件中添加我们的项目，如下所示:\n\n![](img/00172.jpeg)\n\n确保`RPNCalculator`文件夹下的`CMakeLists.txt`如下图所示:\n\n![](img/00173.jpeg)\n\n现在，让我们使用`cucumber-cpp`主目录中的以下命令来构建我们的项目:\n\n```cpp\ncmake --build build\n```\n\n让我们使用以下命令来执行我们全新的`RPNCalculator`黄瓜测试用例:\n\n```cpp\nbuild/RPNCalculator/RPNCalculatorSteps &\n\ncucumber RPNCalculator\n```\n\n输出如下所示:\n\n![](img/00174.jpeg)\n\n在上一个截图中，我们在特征文件中写的每一个`Given`、`When`和`Then`语句都有两个建议。第一个版本是为 Ruby 准备的，第二个版本是为 C++ 准备的；因此，我们可以放心地忽略步骤建议，如下所示:\n\n```cpp\nThen(/^the actualResult should match the (d+).(d+)$/) do |arg1, arg2|\n pending # Write code here that turns the phrase above into concrete actions\nend \n```\n\n由于我们还没有实现`RPNCalculatorSteps.cpp`文件，黄瓜框架建议我们为前面的步骤提供实现。让我们将它们复制并粘贴到`RPNCalculatorSteps.cpp`文件中，完成步骤实现，如下所示:\n\n![](img/00175.jpeg) `REGEX_PARAM` is a macro supported by the cucumber-cpp BDD framework, which comes in handy to extract the input arguments from the regular expression and pass them to the Cucumber step functions.\n\n现在，让我们尝试使用以下命令再次构建我们的项目:\n\n```cpp\ncmake --build  build\n```\n\n构建日志如下所示:\n\n![](img/00176.jpeg)\n\n每个成功的开发人员或顾问背后的秘密公式是，他们有很强的调试和解决问题的技能。分析构建报告，尤其是构建失败，是一个人成功应用 BDD 应该获得的关键品质。每个构建错误都会教会我们一些东西！\n\n构建错误很明显，因为我们还没有实现`RPNCalculator`，因为文件是空的。让我们编写最少的代码，以便代码编译:\n\n![](img/00177.jpeg)\n\n与瀑布模型不同，BDD 导致增量设计和开发。瀑布模型鼓励前期设计。通常，在瀑布模型中，设计是最初完成的，它消耗了整个项目工作的 30-40%。前期设计的主要问题是，我们最初对该特性的了解会更少；通常，我们会有一个模糊的特征知识，但它会随着时间的推移而改进。因此，在前期的设计活动中投入更多的精力并不是一个好主意；相反，在必要时，对重构设计和代码持开放态度。\n\n因此，BDD 是复杂项目的自然选择。\n\n有了这个最小的实现，让我们尝试构建和运行测试用例:\n\n![](img/00178.jpeg)\n\n酷！由于代码编译没有错误，让我们现在执行测试用例并观察发生了什么:\n\n![](img/00179.jpeg)\n\n黄瓜-cpp 框架用红色突出显示了这些错误，如前面的截图所示。这是意料之中的；测试用例失败，因为`RPNCalculator::evaluate`方法被硬编码为返回`0.0`。\n\nIdeally, we had to write only minimal code to make this pass, but I took the liberty of fast forwarding the steps, with the assumption that you have already read [Chapter 7](07.html#4MLOS0-240c4d898e2d4108b0645aefc6c58389), *Test Driven Development* before reading the current chapter. In that chapter, I have demonstrated every step in detail, including the refactoring.\n\n现在，让我们继续执行代码，让这个测试用例通过。修改后的`RPNCalculator`头文件如下:\n\n![](img/00180.jpeg)\n\n相应的`RPNCalculator`源文件如下所示:\n\n![](img/00181.jpeg)\n\n根据 BDD 的实践，请注意，根据我们当前的黄瓜场景需求，我们只实现了支持加法操作所必需的代码。像 TDD 一样，在 BDD 中，我们应该只编写满足当前场景所需的代码量；这样，我们可以确保每一行代码都被有效的测试用例覆盖。\n\n# 让我们构建并运行我们的 BDD 测试用例\n\n现在让我们构建和测试。以下命令可分别用于构建、在后台启动步骤以及使用有线协议运行黄瓜测试用例:\n\n```cpp\ncmake --build build\n build/RPNCalculator/RPNCalculatorSteps &\n\ncucumber RPNCalculator\n```\n\n下面的截图演示了构建和执行黄瓜测试用例的过程:\n\n![](img/00182.jpeg)\n\n太好了。我们的测试场景现在都是绿色的！让我们继续下一个测试场景。\n\n让我们在特征文件中添加一个场景来测试减法操作，如下所示:\n\n![](img/00183.jpeg)\n\n测试输出如下所示:\n\n![](img/00184.jpeg)\n\n我们以前见过这个，不是吗？我相信你猜对了；预期结果是`85`，而实际结果是`0`，因为我们还没有增加任何对减法的支持。现在，让我们在应用中添加必要的代码来添加减法逻辑:\n\n![](img/00185.jpeg)\n\n随着代码的改变，让我们重新运行测试用例，看看测试结果是什么:\n\n![](img/00186.jpeg)\n\n酷，检测报告变回绿色了！\n\n让我们继续，在特征文件中添加一个场景来测试乘法运算:\n\n![](img/00187.jpeg)\n\n是时候运行测试用例了，如下图所示:\n\n![](img/00188.jpeg)\n\n你说得对。是的，我们需要在生产代码中增加对乘法的支持。好了，我们现在就开始吧，如下图所示:\n\n![](img/00189.jpeg)\n\n# 测试时间到了！\n\n以下命令分别帮助您构建、启动 steps 应用和运行黄瓜测试用例。准确地说，第一个命令构建测试用例，而第二个命令在后台模式下启动黄瓜步骤测试可执行文件。第三个命令执行我们为`RPNCalculator`项目编写的黄瓜测试用例。`RPNCalculatorSteps`可执行文件将作为一个服务器工作，黄瓜可以通过有线协议与之交谈。黄瓜框架将从保存在`step_definitions`文件夹下的`cucumber.wire`文件中获取服务器的连接细节:\n\n```cpp\ncmake --build build\n build/RPNCalculator/RPNCalculatorSteps &\n\ncucumber RPNCalculator\n```\n\n下面的截图演示了黄瓜测试用例的执行过程:\n\n![](img/00190.jpeg)\n\n我相信你已经掌握了 BDD 的窍门！是的，BDD 非常简单明了。现在让我们为除法运算添加一个场景，如下图所示:\n\n![](img/00191.jpeg)\n\n让我们快速运行测试用例，观察测试结果，如下图所示:\n\n![](img/00192.jpeg)\n\n是的，我听到你说你知道失败的原因。让我们快速添加对除法的支持，并重新运行测试用例，看看它变成绿色！BDD 让编码变得非常有趣。\n\n我们需要在`RPNCalculator.cpp`中添加以下代码片段:\n\n```cpp\nelse if ( *token == \"/\" ) {\n      secondNumber = numberStack.top();\n      numberStack.pop();\n      firstNumber = numberStack.top();\n      numberStack.pop();\n\n      result = firstNumber / secondNumber;\n\n      numberStack.push ( result );\n}\n\n```\n\n随着代码的改变，让我们检查测试输出:\n\n```cpp\ncmake --build build\nbuild/RPNCalculator/RPNCalculatorSteps &\ncucumber RPNCalculator\n```\n\n下面的截图直观地演示了该过程:\n\n![](img/00193.jpeg)\n\n目前为止一切顺利。我们到目前为止测试的所有场景都通过了，这是一个好的迹象。但是让我们尝试一个复杂的表达式，它涉及许多数学运算。比如我们试试 *10.0 5.0 * 1.0 + 100.0 2.0 / -* 。\n\n**Did you know?**\nReverse Polish Notation (postfix notation) is used by pretty much every compiler to evaluate mathematical expressions.\n\n下面的截图演示了复杂表达式测试用例的集成:\n\n![](img/00194.jpeg)\n\n让我们再运行一次测试场景，因为这将是对到目前为止实现的整个代码的真正测试，因为这个表达式涉及到我们的简单应用支持的所有操作。\n\n以下命令可用于在后台模式下启动应用并执行黄瓜测试用例:\n\n```cpp\nbuild/RPNCalculator/RPNCalculatorSteps &\ncucumber RPNCalculator\n```\n\n下面的截图直观地演示了该过程:\n\n![](img/00195.jpeg)\n\n太好了。如果你已经走了这么远，我相信你会理解黄瓜-cpp 和 BDD 编码风格。\n\n**Refactoring and Removing Code Smells**\nThe `RPNCalculator.cpp` code has too much branching, which is a code smell; hence, the code could be refactored. The good news is that `RPNCalculator.cpp` can be refactored to remove the code smells and has the scope to use the Factory Method, Strategy, and Null Object Design Patterns.\n\n# 摘要\n\n在本章中，您学习了以下内容\n\n*   简而言之，行为驱动开发被称为 BDD。\n*   BDD 是一种自顶向下的开发方法，使用小黄瓜语言作为领域特定语言(DSL)。\n*   在一个项目中，BDD 和 TDD 可以并行使用，因为它们是相辅相成的，而不是相互替代的。\n*   黄瓜-cpp BDD 框架利用有线协议支持非 ruby 平台编写测试用例。\n*   通过使用测试优先开发方法实现一个 RPNCalculator，您以一种实用的方式学习了 BDD。\n*   BDD 类似于 TDD，它通过以增量方式在短时间内重构代码来鼓励开发整洁的代码。\n*   您已经学习了使用小黄瓜编写 BDD 测试用例，以及使用谷歌测试框架定义步骤。\n\n在下一章中，您将学习 C++ 调试技术。"
  },
  {
    "path": "docs/master-cpp-prog/09.md",
    "content": "# 九、调试技术\n\n在本章中，我们将涵盖以下主题:\n\n*   有效调试\n*   调试策略\n*   调试工具\n*   使用 GDB 调试应用\n*   用 Valgrind 调试内存泄漏\n*   记录\n\n# 有效调试\n\n调试是一门艺术而不是科学，本身就是一个非常大的课题。强大的调试技能是优秀开发人员的优势。所有的专家开发人员都有一些共同的特点，其中强大的解决问题和调试技能是最重要的。修复 bug 的第一步是重现问题。非常有效地捕捉复制错误所涉及的步骤是至关重要的。经验丰富的质量保证工程师将知道捕获详细的重现步骤的重要性，因为如果开发人员不能重现问题，他们将发现很难修复它。\n\n在我看来，一个无法复制的 bug 是无法修复的。人们可以猜测和拐弯抹角，但不能确定这个问题是否真的得到了解决，首先不能重现这个 bug。\n\n以下细节将帮助开发人员更快地重现和调试问题:\n\n*   重现问题的详细步骤\n*   bug 的截图图像\n*   优先级和严重性\n*   重现问题的输入和场景\n*   预期和实际产出\n*   错误日志\n*   应用日志和跟踪\n*   转储文件以防应用崩溃\n*   环境详细信息\n*   操作系统详细信息\n*   软件版本\n\n一些常用的调试技术如下:\n\n*   使用`cout` / `cerr`打印报表非常方便\n*   核心转储、迷你转储和完整转储有助于远程分析错误\n*   通过检查变量、参数、中间值等，使用调试工具逐步执行代码\n*   测试框架首先有助于防止这个问题\n*   性能分析工具对于发现性能问题非常有帮助\n*   扣除内存泄漏、资源泄漏、死锁等的工具\n\nThe `log4cpp` open source C++ library is an elegant and useful log utility which helps add debug messages that support debugging, which can be disabled in the release mode or in the production environment.\n\n# 调试策略\n\n调试策略在快速复制、调试、检测和有效修复问题方面有很大帮助。下面的列表解释了一些高级调试策略:\n\n*   使用缺陷跟踪系统，如 JIRA、布奇拉、TFS、优酷、团队合作等\n*   应用崩溃或冻结必须包括核心转储、小型转储或完全转储\n*   应用跟踪日志在所有情况下都是很好的帮助\n*   启用多级错误日志\n*   在调试和发布模式下捕获应用跟踪日志\n\n# 调试工具\n\n调试工具通过使用断点、变量检查等逐步执行来帮助缩小问题的范围。虽然一步一步地调试问题可能是一项耗时的任务，但它绝对是确定问题的可靠方法，而且我可以说它几乎总是有效的。\n\n下面是 C++ 调试工具的列表:\n\n*   **GDB** :这是一个开源的 CLI 调试器\n*   **Valgrind** :这是一个开源的 CLI，对内存泄漏、死锁、竞速检测等都有好处\n*   **亲和调试器**:这是 GDB 的商业 GUI 工具\n*   **GNU DDD** :这是一个面向 GDB、DBX、JDB、XDB 等的开源图形调试器\n*   **GNU Emacs GDB 模式**:这是一个开源工具，支持最少的图形调试器\n*   **KDevelop** :这是一个支持图形调试器的开源工具\n*   **nemirver**:这是一个开源工具，在 GNOME 桌面环境下运行良好\n*   **SlickEdit** :这对于调试多线程和多处理器代码很有好处\n\nIn C++, there are quite a lot of open source and commercially licensed debugging tools. However, in this book, we will explore the GDB and Valgrind open source command-line interface tools.\n\n# 使用 GDB 调试应用\n\n传统的 C++ 开发人员使用打印语句来调试代码。但是，使用打印跟踪消息进行调试是一项耗时的任务，因为您需要花费大量精力在多个地方编写打印语句、重新编译和执行应用。\n\n旧式的调试方法需要多次这样的迭代，通常，每次迭代都需要添加更多的打印语句来缩小问题的范围。一旦问题得到解决，我们需要清理代码并删除打印语句，因为过多的打印语句往往会降低应用的性能。此外，调试打印消息会分散注意力，并且对于在生产环境中使用您的产品的最终客户来说是不相关的。\n\n带有`<cassert>`头的 C++ 调试`assert()`宏语句可以用于调试。C++ `assert()`宏可以在发布模式下禁用，只能在调试模式下启用。\n\n调试工具可以把你从这些乏味的工作中解救出来。GDB 调试器是一个开源的命令行界面工具，它是 Unix/Linux 世界中 C++ 的调试器。对于 Windows 平台，Visual Studio 是最受欢迎的一站式 IDE，内置了调试功能。\n\n让我们举一个简单的例子:\n\n```cpp\n#include <iostream>\n#include <vector>\n#include <iterator>\n#include <algorithm>\nusing namespace std; //Use this judiciously - this is applicable throughout the book\n\nclass MyInteger {\n      private:\n           int number;\n\n      public:\n           MyInteger( int value ) {\n                this->number = value;\n           }\n\n           MyInteger(const MyInteger & rhsObject ) {\n                this->number = rhsObject.number;\n           }\n\n           MyInteger& operator = (const MyInteger & rhsObject ) {\n\n                if ( this != &rhsObject )\n                     this->number = rhsObject.number;\n\n                return *this;\n           }\n\n           bool operator < (const MyInteger &rhsObject) {\n                return this->number > rhsObject.number;\n           }\n\n           bool operator > (const MyInteger &rhsObject) {\n                return this->number > rhsObject.number;\n           }\n\n           friend ostream & operator << ( ostream &output, const MyInteger &object );\n};\n\nostream & operator << (ostream &o, const MyInteger& object) {\n    o << object.number;\n}\n\nint main ( ) {\n\n    vector<MyInteger> v = { 10, 100, 40, 20, 80, 70, 50, 30, 60, 90 };\n\n    cout << \"\\nVectors entries before sorting are ...\" << endl;\n    copy ( v.begin(), v.end() , ostream_iterator<MyInteger>( cout, \"\\t\" ) );\n    cout << endl;\n\n    sort ( v.begin(), v.end() );\n\n    cout << \"\\nVectors entries after sorting are ...\" << endl;\n    copy ( v.begin(), v.end() , ostream_iterator<MyInteger>( cout, \"\\t\" ) );\n    cout << endl;\n\n    return 0;\n}\n```\n\n程序的输出如下:\n\n```cpp\nVectors entries before sorting are ...\n10 100 40 20 80 70 50 30 60 90\n\nVectors entries after sorting are ...\n100 90 80 70 60 50 40 30 20 10\n```\n\n但是，我们的预期输出如下:\n\n```cpp\nVectors entries before sorting are ...\n10 100 40 20 80 70 50 30 60 90\n\nVectors entries after sorting are ...\n10 20 30 40 50 60 70 80 90 100\n```\n\nbug 很明显；让我们放松对 GDB 的学习。让我们首先在调试模式下编译程序，即启用调试元数据和符号表，如下所示:\n\n```cpp\ng++ main.cpp -std=c++ 17 -g\n```\n\n# GDB 需要快速参考\n\n以下 GDB 快速提示图表将帮助您找到调试应用的 GDB 命令:\n\n| **命令** | **短命令** | **描述** |\n| `gdb yourappln.exe` | `-` | 在 GDB 打开应用进行调试 |\n| `break main` | `b main` | 将断点设置到`main`功能 |\n| `run` | `r` | 执行程序，直到到达断点，以便逐步执行 |\n| `next` | `n` | 一步一步地执行程序 |\n| `step` | `s` | 逐步进入函数以逐步执行函数 |\n| `continue` | `c` | 继续执行程序，直到下一个断点；如果没有设置断点，它将继续正常执行应用 |\n| `backtrace` | `bt` | 打印整个调用堆栈 |\n| `quit` | `q`或`Ctrl + d` | GDB 出口 |\n| `-help` | `-h` | 显示可用选项并简要显示其用途 |\n\n有了前面的基本 GDB 快速参考，让我们开始调试我们的错误应用来检测错误。让我们首先用以下命令启动 GDB:\n\n```cpp\ngdb ./a.out\n```\n\n然后，让我们在`main()`处添加一个断点来执行分步执行:\n\n```cpp\njegan@ubuntu:~/MasteringC++ Programming/Debugging/Ex1$ g++ main.cpp -g\njegan@ubuntu:~/MasteringC++ Programming/Debugging/Ex1$ ls\na.out main.cpp\njegan@ubuntu:~/MasteringC++ Programming/Debugging/Ex1$ gdb ./a.out\n\nGNU gdb (Ubuntu 7.12.50.20170314-0ubuntu1.1) 7.12.50.20170314-git\nCopyright (C) 2017 Free Software Foundation, Inc.\nLicense GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>\nThis is free software: you are free to change and redistribute it.\nThere is NO WARRANTY, to the extent permitted by law. Type \"show copying\"\nand \"show warranty\" for details.\nThis GDB was configured as \"x86_64-linux-gnu\".\nType \"show configuration\" for configuration details.\nFor bug reporting instructions, please see:\n<http://www.gnu.org/software/gdb/bugs/>.\nFind the GDB manual and other documentation resources online at:\n<http://www.gnu.org/software/gdb/documentation/>.\nFor help, type \"help\".\nType \"apropos word\" to search for commands related to \"word\"...\nReading symbols from ./a.out...done.\n(gdb) b main\nBreakpoint 1 at 0xba4: file main.cpp, line 46.\n(gdb) l\n32 \n33 bool operator > (const MyInteger &rhsObject) {\n34 return this->number < rhsObject.number;\n35 }\n36 \n37 friend ostream& operator << ( ostream &output, const MyInteger &object );\n38 \n39 };\n40 \n41 ostream& operator << (ostream &o, const MyInteger& object) {\n(gdb)\n```\n\n用`gdb`启动我们的应用后，`b main`命令将在`main()`函数的第一行添加一个断点。现在让我们尝试执行应用:\n\n```cpp\n(gdb) run\nStarting program: /home/jegan/MasteringC++ Programming/Debugging/Ex1/a.out \n\nBreakpoint 1, main () at main.cpp:46\n46 int main ( ) {\n(gdb) \n```\n\n正如您可能已经观察到的，在我们的`main()`函数中，程序执行在行号`46`处暂停，因为我们在`main()`函数中添加了一个断点。\n\n现在，让我们一步一步地执行应用，如下所示:\n\n```cpp\n(gdb) run\nStarting program: /home/jegan/MasteringC++ Programming/Debugging/Ex1/a.out \n\nBreakpoint 1, main () at main.cpp:46\n46 int main ( ) {\n(gdb) next\n48   vector<MyInteger> v = { 10, 100, 40, 20, 80, 70, 50, 30, 60, 90 };\n(gdb) next\n50   cout << \"\\nVectors entries before sorting are ...\" << endl;\n(gdb) n\nVectors entries before sorting are ...51   copy ( v.begin(), v.end() , ostream_iterator<MyInteger>( cout, \"\\t\" ) );\n(gdb) n\n52   cout << endl;\n(gdb) n\n10 100 40 20 80 70 50 30 60 90 \n54   sort ( v.begin(), v.end() );\n(gdb) \n```\n\n现在，让我们在行号`29`和`33`处再添加两个断点，如下所示:\n\n```cpp\nBreakpoint 1 at 0xba4: file main.cpp, line 46.Breakpoint 1 at 0xba4: file main.cpp, line 46.(gdb) run\nStarting program: /home/jegan/Downloads/MasteringC++ Programming/Debugging/Ex1/a.out \nBreakpoint 1, main () at main.cpp:46\n46 int main ( ) {\n(gdb) l\n41 ostream& operator << (ostream &o, const MyInteger& object) {\n42    o << object.number;\n43 }\n44 \n45 \n46 \nint main ( ) {\n47 \n48   vector<MyInteger> v = { 10, 100, 40, 20, 80, 70, 50, 30, 60, 90 };\n49    \n50   cout << \"\\nVectors entries before sorting are ...\" << endl;\n(gdb) n\n48   vector<MyInteger> v = { 10, 100, 40, 20, 80, 70, 50, 30, 60, 90 };\n(gdb) n\n50   cout << \"\\nVectors entries before sorting are ...\" << endl;\n(gdb) n\nVectors entries before sorting are ...\n51   copy ( v.begin(), v.end() , ostream_iterator<MyInteger>( cout, \"\\t\" ) );\n(gdb) break 29\nBreakpoint 2 at 0x555555554f88: file main.cpp, line 29.\n(gdb) break 33\nBreakpoint 3 at 0x555555554b80: file main.cpp, line 33.\n(gdb) \n```\n\n由此，您将理解断点可以通过函数名或行号来添加。现在让程序继续执行，直到到达我们设置的一个断点:\n\n```cpp\n(gdb) break 29\nBreakpoint 2 at 0x555555554f88: file main.cpp, line 29.\n(gdb) break 33\nBreakpoint 3 at 0x555555554b80: file main.cpp, line 33.\n(gdb) continue Continuing.\nBreakpoint 2, MyInteger::operator< (this=0x55555576bc24, rhsObject=...) at main.cpp:30 30 return this->number > rhsObject.number; (gdb) \n```\n\n如您所见，程序执行在行号`29`处暂停，因为每当`sort`函数需要决定在按升序排序向量条目的过程中是否必须交换这两个项目时，它都会被调用。\n\n让我们探索如何检查或打印变量，`this->number`和`rhsObject.number`:\n\n```cpp\n(gdb) break 29\nBreakpoint 2 at 0x400ec6: file main.cpp, line 29.\n(gdb) break 33\nBreakpoint 3 at 0x400af6: file main.cpp, line 33.\n(gdb) continue\nContinuing.\nBreakpoint 2, MyInteger::operator< (this=0x617c24, rhsObject=...) at main.cpp:30\n30 return this->number > rhsObject.number;\n(gdb) print this->number $1 = 100 (gdb) print rhsObject.number $2 = 10 (gdb) \n```\n\n你看到`<`和`>`运算符的实现方式了吗？操作员检查*小于*操作，而实际执行检查*大于*操作，并且在`>`操作员过载方法中也观察到类似的错误。请检查以下代码:\n\n```cpp\nbool operator < ( const MyInteger &rhsObject ) {\n        return this->number > rhsObject.number;\n}\n\nbool operator > ( const MyInteger &rhsObject ) {\n        return this->number < rhsObject.number;\n}\n```\n\n虽然`sort()`函数应该是以升序对`vector`条目进行排序，但输出显示它是以降序对它们进行排序，而前面的代码是问题的根本原因。因此，让我们解决这个问题，如下所示:\n\n```cpp\nbool operator < ( const MyInteger &rhsObject ) {\n        return this->number < rhsObject.number;\n}\n\nbool operator > ( const MyInteger &rhsObject ) {\n        return this->number > rhsObject.number;\n}\n```\n\n有了这些变化，让我们编译并运行程序:\n\n```cpp\ng++ main.cpp -std=c++ 17 -g\n\n./a.out\n```\n\n这是您将获得的输出:\n\n```cpp\nVectors entries before sorting are ...\n10   100   40   20   80   70   50   30   60   90\n\nVectors entries after sorting are ...\n10   20   30   40   50   60   70   80   90   100\n```\n\n酷，我们修好了窃听器！不用说，您将认识到 GDB 调试工具有多有用。虽然我们只是触及了 GDB 工具功能的表面，但它提供了许多强大的调试功能。然而，在本章中，涵盖 GDB 工具支持的每一个特性是不切实际的；因此，我强烈建议您探索 GDB 文档，以便在[https://sourceware.org/gdb/documentation/](https://sourceware.org/gdb/documentation/)进一步学习。\n\n# 用 Valgrind 调试内存泄漏\n\nValgrind 是一个开源的 C/C++ 调试和分析工具的集合，适用于 Unix 和 Linux 平台。Valgrind 支持的工具集合如下:\n\n*   **Cachegrind** :这是缓存剖析器\n*   **Callgrind** :这与缓存分析器的工作方式类似，但是支持调用者-被调用者序列\n*   **Helgrind** :这有助于检测线程同步问题\n*   **DRD** :这是螺纹误差检测仪\n*   **地块**:这是堆剖面仪\n*   **拉克**:这提供了关于应用的基本性能相关统计和测量\n*   **exp-sgcheck** :这检测堆栈溢出；它通常有助于发现 Memcheck 找不到的问题\n*   **exp-bbv** :这对于计算机架构 R & D 相关的工作很有用\n*   **exp-dhat** :这是另一个堆剖析器\n*   **内存检查**:这有助于检测与内存问题相关的内存泄漏和崩溃\n\n在这一章中，我们将只探索 Memcheck，因为演示每个 Valgrind 工具都不在本书的范围内。\n\n# 记忆检查工具\n\nValgrind 使用的默认工具是 Memcheck。Memcheck 工具可以检测到相当详尽的问题列表，其中一些如下:\n\n*   访问数组、堆栈或堆溢出的边界之外\n*   使用未初始化的内存\n*   访问已经释放的内存\n*   内存泄漏\n*   `new`和`free`或`malloc`和`delete`使用不匹配\n\n让我们在接下来的小节中看看一些这样的问题。\n\n# 检测数组边界之外的内存访问\n\n下面的示例演示了数组边界之外的内存访问:\n\n```cpp\n#include <iostream>\nusing namespace std;\n\nint main ( ) {\n    int a[10];\n\n    a[10] = 100;\n    cout << a[10] << endl;\n\n    return 0;\n}\n```\n\n以下输出显示了 valgrind 调试会话，该会话精确指向数组边界之外的内存访问:\n\n```cpp\ng++ arrayboundsoverrun.cpp -g -std=c++ 17 \n\njegan@ubuntu  ~/MasteringC++/Debugging  valgrind --track-origins=yes --read-var-info=yes ./a.out\n==28576== Memcheck, a memory error detector\n==28576== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.\n==28576== Using Valgrind-3.11.0 and LibVEX; rerun with -h for copyright info\n==28576== Command: ./a.out\n==28576== \n100\n*** stack smashing detected ***: ./a.out terminated\n==28576== \n==28576== Process terminating with default action of signal 6 (SIGABRT)\n==28576== at 0x51F1428: raise (raise.c:54)\n==28576== by 0x51F3029: abort (abort.c:89)\n==28576== by 0x52337E9: __libc_message (libc_fatal.c:175)\n==28576== by 0x52D511B: __fortify_fail (fortify_fail.c:37)\n==28576== by 0x52D50BF: __stack_chk_fail (stack_chk_fail.c:28)\n==28576== by 0x4008D8: main (arrayboundsoverrun.cpp:11)\n==28576== \n==28576== HEAP SUMMARY:\n==28576== in use at exit: 72,704 bytes in 1 blocks\n==28576== total heap usage: 2 allocs, 1 frees, 73,728 bytes allocated\n==28576== \n==28576== LEAK SUMMARY:\n==28576== definitely lost: 0 bytes in 0 blocks\n==28576== indirectly lost: 0 bytes in 0 blocks\n==28576== possibly lost: 0 bytes in 0 blocks\n==28576== still reachable: 72,704 bytes in 1 blocks\n==28576== suppressed: 0 bytes in 0 blocks\n==28576== Rerun with --leak-check=full to see details of leaked memory\n==28576== \n==28576== For counts of detected and suppressed errors, rerun with: -v\n==28576== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)\n[1] 28576 abort (core dumped) valgrind --track-origins=yes --read-var-info=yes ./a.out\n```\n\n正如您将注意到的，由于非法内存访问，应用因核心转储而崩溃。在前面的输出中，Valgrind 工具准确地指向导致崩溃的那条线。\n\n# 检测对已经释放的存储器位置的存储器访问\n\n下面的示例代码演示了对已经释放的内存位置的内存访问:\n\n```cpp\n#include <iostream>\nusing namespace std;\n\nint main( ) {\n\n    int *ptr = new int();\n\n    *ptr = 100;\n\n    cout << \"\\nValue stored at pointer location is \" << *ptr << endl;\n\n    delete ptr;\n\n    *ptr = 200;\n    return 0;\n}\n```\n\n让我们编译前面的程序，了解 Valgrind 如何报告试图访问已经释放的内存位置的非法内存访问:\n\n```cpp\n==118316== Memcheck, a memory error detector\n==118316== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.\n==118316== Using Valgrind-3.11.0 and LibVEX; rerun with -h for copyright info\n==118316== Command: ./a.out\n==118316== \n\nValue stored at pointer location is 100\n==118316== Invalid write of size 4\n==118316== at 0x400989: main (illegalaccess_to_released_memory.cpp:14)\n==118316== Address 0x5ab6c80 is 0 bytes inside a block of size 4 free'd\n==118316== at 0x4C2F24B: operator delete(void*) (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)\n==118316== by 0x400984: main (illegalaccess_to_released_memory.cpp:12)\n==118316== Block was alloc'd at\n==118316== at 0x4C2E0EF: operator new(unsigned long) (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)\n==118316== by 0x400938: main (illegalaccess_to_released_memory.cpp:6)\n==118316== \n==118316== \n==118316== HEAP SUMMARY:\n==118316== in use at exit: 72,704 bytes in 1 blocks\n==118316== total heap usage: 3 allocs, 2 frees, 73,732 bytes allocated\n==118316== \n==118316== LEAK SUMMARY:\n==118316== definitely lost: 0 bytes in 0 blocks\n==118316== indirectly lost: 0 bytes in 0 blocks\n==118316== possibly lost: 0 bytes in 0 blocks\n==118316== still reachable: 72,704 bytes in 1 blocks\n==118316== suppressed: 0 bytes in 0 blocks\n==118316== Rerun with --leak-check=full to see details of leaked memory\n==118316== \n==118316== For counts of detected and suppressed errors, rerun with: -v\n==118316== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)\n```\n\nValgrind 精确指向试图访问在行号`12`处释放的存储位置的行号(`14`)。\n\n# 检测未初始化的内存访问\n\n下面的示例代码演示了未初始化的内存访问的使用，以及如何使用 Memcheck 检测相同的情况:\n\n```cpp\n#include <iostream>\nusing namespace std;\n\nclass MyClass {\n    private:\n       int x;\n    public:\n      MyClass( );\n  void print( );\n}; \n\nMyClass::MyClass() {\n    cout << \"\\nMyClass constructor ...\" << endl;\n}\n\nvoid MyClass::print( ) {\n     cout << \"\\nValue of x is \" << x << endl;\n}\n\nint main ( ) {\n\n    MyClass obj;\n    obj.print();\n    return 0;\n\n}\n```\n\n现在，让我们使用 Memcheck 编译并检测未初始化的内存访问问题:\n\n```cpp\ng++ main.cpp -g\n\nvalgrind ./a.out --track-origins=yes\n\n==51504== Memcheck, a memory error detector\n==51504== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.\n==51504== Using Valgrind-3.11.0 and LibVEX; rerun with -h for copyright info\n==51504== Command: ./a.out --track-origins=yes\n==51504== \n\nMyClass constructor ...\n\n==51504== Conditional jump or move depends on uninitialised value(s)\n==51504== at 0x4F3CCAE: std::ostreambuf_iterator<char, std::char_traits<char> > std::num_put<char, std::ostreambuf_iterator<char, std::char_traits<char> > >::_M_insert_int<long>(std::ostreambuf_iterator<char, std::char_traits<char> >, std::ios_base&, char, long) const (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21)\n==51504== by 0x4F3CEDC: std::num_put<char, std::ostreambuf_iterator<char, std::char_traits<char> > >::do_put(std::ostreambuf_iterator<char, std::char_traits<char> >, std::ios_base&, char, long) const (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21)\n==51504== by 0x4F493F9: std::ostream& std::ostream::_M_insert<long>(long) (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21)\n==51504== by 0x40095D: MyClass::print() (uninitialized.cpp:19)\n==51504== by 0x4009A1: main (uninitialized.cpp:26)\n==51504== \n==51504== Use of uninitialised value of size 8\n==51504== at 0x4F3BB13: ??? (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21)\n==51504== by 0x4F3CCD9: std::ostreambuf_iterator<char, std::char_traits<char> > std::num_put<char, std::ostreambuf_iterator<char, std::char_traits<char> > >::_M_insert_int<long>(std::ostreambuf_iterator<char, std::char_traits<char> >, std::ios_base&, char, long) const (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21)\n==51504== by 0x4F3CEDC: std::num_put<char, std::ostreambuf_iterator<char, std::char_traits<char> > >::do_put(std::ostreambuf_iterator<char, std::char_traits<char> >, std::ios_base&, char, long) const (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21)\n==51504== by 0x4F493F9: std::ostream& std::ostream::_M_insert<long>(long) (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21)\n==51504== by 0x40095D: MyClass::print() (uninitialized.cpp:19)\n==51504== by 0x4009A1: main (uninitialized.cpp:26)\n==51504== \n==51504== Conditional jump or move depends on uninitialised value(s)\n==51504== at 0x4F3BB1F: ??? (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21)\n==51504== by 0x4F3CCD9: std::ostreambuf_iterator<char, std::char_traits<char> > std::num_put<char, std::ostreambuf_iterator<char, std::char_traits<char> > >::_M_insert_int<long>(std::ostreambuf_iterator<char, std::char_traits<char> >, std::ios_base&, char, long) const (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21)\n==51504== by 0x4F3CEDC: std::num_put<char, std::ostreambuf_iterator<char, std::char_traits<char> > >::do_put(std::ostreambuf_iterator<char, std::char_traits<char> >, std::ios_base&, char, long) const (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21)\n==51504== by 0x4F493F9: std::ostream& std::ostream::_M_insert<long>(long) (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21)\n==51504== by 0x40095D: MyClass::print() (uninitialized.cpp:19)\n==51504== by 0x4009A1: main (uninitialized.cpp:26)\n==51504== \n==51504== Conditional jump or move depends on uninitialised value(s)\n==51504== at 0x4F3CD0C: std::ostreambuf_iterator<char, std::char_traits<char> > std::num_put<char, std::ostreambuf_iterator<char, std::char_traits<char> > >::_M_insert_int<long>(std::ostreambuf_iterator<char, std::char_traits<char> >, std::ios_base&, char, long) const (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21)\n==51504== by 0x4F3CEDC: std::num_put<char, std::ostreambuf_iterator<char, std::char_traits<char> > >::do_put(std::ostreambuf_iterator<char, std::char_traits<char> >, std::ios_base&, char, long) const (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21)\n==51504== by 0x4F493F9: std::ostream& std::ostream::_M_insert<long>(long) (in /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21)\n==51504== by 0x40095D: MyClass::print() (uninitialized.cpp:19)\n==51504== by 0x4009A1: main (uninitialized.cpp:26)\n==51504== \nValue of x is -16778960\n==51504== \n==51504== HEAP SUMMARY:\n==51504== in use at exit: 72,704 bytes in 1 blocks\n==51504== total heap usage: 2 allocs, 1 frees, 73,728 bytes allocated\n==51504== \n==51504== LEAK SUMMARY:\n==51504== definitely lost: 0 bytes in 0 blocks\n==51504== indirectly lost: 0 bytes in 0 blocks\n==51504== possibly lost: 0 bytes in 0 blocks\n==51504== still reachable: 72,704 bytes in 1 blocks\n==51504== suppressed: 0 bytes in 0 blocks\n==51504== Rerun with --leak-check=full to see details of leaked memory\n==51504== \n==51504== For counts of detected and suppressed errors, rerun with: -v\n==51504== Use --track-origins=yes to see where uninitialised values come from\n==51504== ERROR SUMMARY: 18 errors from 4 contexts (suppressed: 0 from 0)\n\n```\n\n前面输出中以粗体突出显示的行清楚地指向访问未初始化变量的确切行:\n\n```cpp\n==51504== by 0x40095D: MyClass::print() (uninitialized.cpp:19)\n==51504== by 0x4009A1: main (uninitialized.cpp:26)\n\n 18 void MyClass::print() {\n 19 cout << \"\\nValue of x is \" << x << endl;\n 20 } \n```\n\n显示前面的代码片段供您参考；但是，Valgrind 不会显示代码细节。底线是 Valgrind 精确地指向访问未初始化变量的行，这通常很难用其他方法检测到。\n\n# 检测内存泄漏\n\n让我们以一个有一些内存泄漏的简单程序为例，探索 Valgrind 工具如何在 Memcheck 的帮助下帮助我们检测内存泄漏。由于 Memcheck 是 Valgrind 使用的默认工具，因此在发出 Valgrind 命令时没有必要显式调用 Memcheck 工具:\n\n```cpp\nvalgrind application_debugged.exe --tool=memcheck\n```\n\n下面的代码实现了一个单链表:\n\n```cpp\n#include <iostream>\nusing namespace std;\n\nstruct Node {\n  int data;\n  Node *next;\n};\n\nclass List {\nprivate:\n  Node *pNewNode;\n  Node *pHead;\n  Node *pTail;\n  int __size;\n  void createNewNode( int );\npublic:\n  List();\n  ~List();\n  int size();\n  void append ( int data );\n  void print( );\n};\n```\n\n正如您可能已经观察到的，前面的类声明有方法来`append()`一个新节点，`print()`列表，和一个`size()`方法返回列表中的节点数。\n\n让我们探索实现`append()`方法、`print()`方法、构造函数和析构函数的`list.cpp`源文件:\n\n```cpp\n#include \"list.h\"\n\nList::List( ) {\n  pNewNode = NULL;\n  pHead = NULL;\n  pTail = NULL;\n  __size = 0;\n}\n\nList::~List() {}\n\nvoid List::createNewNode( int data ) {\n  pNewNode = new Node();\n  pNewNode->next = NULL;\n  pNewNode->data = data;\n}\n\nvoid List::append( int data ) {\n  createNewNode( data );\n  if ( pHead == NULL ) {\n    pHead = pNewNode;\n    pTail = pNewNode;\n    __size = 1;\n  }\n  else {\n    Node *pCurrentNode = pHead;\n    while ( pCurrentNode != NULL ) {\n      if ( pCurrentNode->next == NULL ) break;\n      pCurrentNode = pCurrentNode->next;\n    }\n\n    pCurrentNode->next = pNewNode;\n    ++ __size;\n  }\n}\n\nvoid List::print( ) {\n  cout << \"\\nList entries are ...\" << endl;\n  Node *pCurrentNode = pHead;\n  while ( pCurrentNode != NULL ) {\n    cout << pCurrentNode->data << \"\\t\";\n    pCurrentNode = pCurrentNode->next;\n  }\n  cout << endl;\n}\n```\n\n以下代码演示了`main()`功能:\n\n```cpp\n#include \"list.h\"\n\nint main ( ) {\n  List l;\n\n  for (int count = 0; count < 5; ++ count )\n    l.append ( (count+1) * 10 );\n  l.print();\n\n  return 0;\n}\n```\n\n让我们编译程序，并尝试检测前面程序中的内存泄漏:\n\n```cpp\ng++ main.cpp list.cpp -std=c++ 17 -g\n\nvalgrind ./a.out --leak-check=full \n\n==99789== Memcheck, a memory error detector\n==99789== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.\n==99789== Using Valgrind-3.11.0 and LibVEX; rerun with -h for copyright info\n==99789== Command: ./a.out --leak-check=full\n==99789== \n\nList constructor invoked ...\n\nList entries are ...\n10 20 30 40 50 \n==99789== \n==99789== HEAP SUMMARY:\n==99789== in use at exit: 72,784 bytes in 6 blocks\n==99789== total heap usage: 7 allocs, 1 frees, 73,808 bytes allocated\n==99789== \n==99789== LEAK SUMMARY:\n==99789== definitely lost: 16 bytes in 1 blocks\n==99789== indirectly lost: 64 bytes in 4 blocks\n==99789== possibly lost: 0 bytes in 0 blocks\n==99789== still reachable: 72,704 bytes in 1 blocks\n==99789== suppressed: 0 bytes in 0 blocks\n==99789== Rerun with --leak-check=full to see details of leaked memory\n==99789== \n==99789== For counts of detected and suppressed errors, rerun with: -v\n==99789== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)\n\n```\n\n从前面的输出来看，很明显我们的应用泄漏了 80 个字节。虽然`definitely lost`和`indirectly lost`表示我们的应用泄漏的内存，`still reachable`不一定表示我们的应用，也可能是第三方库或者 C++ 运行时库泄漏的。它们可能不是真正的内存泄漏，因为 C++ 运行时库可能使用内存池。\n\n# 修复内存泄漏\n\n让我们尝试通过在`List::~List()`析构函数中添加以下代码来修复内存泄漏问题:\n\n```cpp\nList::~List( ) {\n\n        cout << \"\\nList destructor invoked ...\" << endl;\n        Node *pTemp = NULL;\n\n        while ( pHead != NULL ) {\n\n                pTemp = pHead;\n                pHead = pHead->next;\n\n                delete pTemp;\n        }\n\n        pNewNode = pHead = pTail = pTemp = NULL;\n        __size = 0;\n\n}\n```\n\n从以下输出中，您将观察到内存泄漏已经修复:\n\n```cpp\ng++ main.cpp list.cpp -std=c++ 17 -g\n\nvalgrind ./a.out --leak-check=full\n\n==44813== Memcheck, a memory error detector\n==44813== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.\n==44813== Using Valgrind-3.11.0 and LibVEX; rerun with -h for copyright info\n==44813== Command: ./a.out --leak-check=full\n==44813== \n\nList constructor invoked ...\n\nList entries are ...\n10 20 30 40 50 \nMemory utilised by the list is 80\n\nList destructor invoked ...\n==44813== \n==44813== HEAP SUMMARY:\n==44813== in use at exit: 72,704 bytes in 1 blocks\n==44813== total heap usage: 7 allocs, 6 frees, 73,808 bytes allocated\n==44813== \n==44813== LEAK SUMMARY:\n==44813== definitely lost: 0 bytes in 0 blocks\n==44813== indirectly lost: 0 bytes in 0 blocks\n==44813== possibly lost: 0 bytes in 0 blocks\n==44813== still reachable: 72,704 bytes in 1 blocks\n==44813== suppressed: 0 bytes in 0 blocks\n==44813== Rerun with --leak-check=full to see details of leaked memory\n==44813== \n==44813== For counts of detected and suppressed errors, rerun with: -v\n==44813== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)\n\n```\n\n如果您仍然不相信前面输出中报告的`still reachable`问题，让我们在`simple.cpp`中尝试以下代码，以了解这是否在我们的控制范围内:\n\n```cpp\n#include <iostream>\nusing namespace std;\n\nint main ( ) {\n\n    return 0;\n\n} \n```\n\n执行以下命令:\n\n```cpp\ng++ simple.cpp -std=c++ 17 -g\n\nvalgrind ./a.out --leak-check=full\n\n==62474== Memcheck, a memory error detector\n==62474== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.\n==62474== Using Valgrind-3.11.0 and LibVEX; rerun with -h for copyright info\n==62474== Command: ./a.out --leak-check=full\n==62474== \n==62474== \n==62474== HEAP SUMMARY:\n==62474== in use at exit: 72,704 bytes in 1 blocks\n==62474== total heap usage: 1 allocs, 0 frees, 72,704 bytes allocated\n==62474== \n==62474== LEAK SUMMARY:\n==62474== definitely lost: 0 bytes in 0 blocks\n==62474== indirectly lost: 0 bytes in 0 blocks\n==62474== possibly lost: 0 bytes in 0 blocks\n==62474== still reachable: 72,704 bytes in 1 blocks\n==62474== suppressed: 0 bytes in 0 blocks\n==62474== Rerun with --leak-check=full to see details of leaked memory\n==62474== \n==62474== For counts of detected and suppressed errors, rerun with: -v\n==62474== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)\n\n```\n\n如你所见，`main()`函数除了返回`0`什么也不做，Valgrind 报告这个程序也有相同的部分:`still reachable\": 72, 704 bytes in 1 blocks`。因此，`Valgrind`泄漏总结中真正重要的是以下任何或所有部分是否有泄漏报告:`definitely lost`、`indirectly lost`和`possibly lost`。\n\n# 不匹配地使用 new 和 free 或 malloc 和 delete\n\n这类问题很少，但也不能排除发生的可能性。当一个遗留的基于 C 的工具移植到 C++ 时，一些内存分配被错误地分配，但是使用`delete`关键字被释放，反之亦然。\n\n以下示例演示了如何使用 Valgrind 检测问题:\n\n```cpp\n#include <stdlib.h>\n\nint main ( ) {\n\n        int *ptr = new int();\n\n        free (ptr); // The correct approach is delete ptr\n\n        char *c = (char*)malloc ( sizeof(char) );\n\n        delete c; // The correct approach is free ( c )\n\n        return 0;\n}\n```\n\n以下输出演示了一个 Valgrind 会话，该会话检测到`free`和`delete`的不匹配使用:\n\n```cpp\ng++ mismatchingnewandfree.cpp -g\n\nvalgrind ./a.out \n==76087== Memcheck, a memory error detector\n==76087== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.\n==76087== Using Valgrind-3.11.0 and LibVEX; rerun with -h for copyright info\n==76087== Command: ./a.out\n==76087== \n==76087== Mismatched free() / delete / delete []\n==76087== at 0x4C2EDEB: free (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)\n==76087== by 0x4006FD: main (mismatchingnewandfree.cpp:7)\n==76087== Address 0x5ab6c80 is 0 bytes inside a block of size 4 alloc'd\n==76087== at 0x4C2E0EF: operator new(unsigned long) (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)\n==76087== by 0x4006E7: main (mismatchingnewandfree.cpp:5)\n==76087== \n==76087== Mismatched free() / delete / delete []\n==76087== at 0x4C2F24B: operator delete(void*) (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)\n==76087== by 0x400717: main (mismatchingnewandfree.cpp:11)\n==76087== Address 0x5ab6cd0 is 0 bytes inside a block of size 1 alloc'd\n==76087== at 0x4C2DB8F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)\n==76087== by 0x400707: main (mismatchingnewandfree.cpp:9)\n==76087== \n==76087== \n==76087== HEAP SUMMARY:\n==76087== in use at exit: 72,704 bytes in 1 blocks\n==76087== total heap usage: 3 allocs, 2 frees, 72,709 bytes allocated\n==76087== \n==76087== LEAK SUMMARY:\n==76087== definitely lost: 0 bytes in 0 blocks\n==76087== indirectly lost: 0 bytes in 0 blocks\n==76087== possibly lost: 0 bytes in 0 blocks\n==76087== still reachable: 72,704 bytes in 1 blocks\n==76087== suppressed: 0 bytes in 0 blocks\n==76087== Rerun with --leak-check=full to see details of leaked memory\n==76087== \n==76087== For counts of detected and suppressed errors, rerun with: -v\n==76087== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 0 from 0)\n```\n\n# 摘要\n\n在本章中，您学习了各种 C++ 调试工具和 Valgrind 工具的应用，例如检测未初始化的变量访问和检测内存泄漏。您还了解了 GDB 工具，以及检测由于非法访问已释放的内存位置而出现的问题。\n\n在下一章中，您将学习代码异味和整洁的代码实践。"
  },
  {
    "path": "docs/master-cpp-prog/10.md",
    "content": "# 十、代码异味和整洁的代码实践\n\n本章将涵盖以下主题:\n\n*   代码异味介绍\n*   整洁的代码的概念\n*   敏捷和整洁的代码实践是如何关联的\n*   固体设计原理\n*   代码重构\n*   将代码重构为整洁的代码\n*   将代码重构为设计模式\n\n整洁的代码是在功能上以准确的方式工作并且在结构上写得很好的源代码。通过彻底的测试，我们可以确保代码在功能上是正确的。我们可以通过代码自我审查、同行代码审查、代码分析来提高代码质量，最重要的是通过代码重构。\n\n以下是整洁的代码的一些品质:\n\n*   容易理解\n*   易于增强\n*   添加新功能不需要很多代码更改\n*   易于重复使用\n*   不言自明\n*   必要时有注释\n\n最后，编写整洁的代码的最大好处是参与项目或产品的开发团队和客户都会感到高兴。\n\n# 代码重构\n\n重构有助于提高源代码的结构质量。它不会修改代码的功能；它只是提高了代码质量的结构方面。重构使代码更加清晰，但有时它可能会帮助您提高整体代码性能。但是，您需要理解性能调优不同于代码重构。\n\n下图展示了开发过程概述:\n\n![](img/00196.gif)\n\n代码重构是如何安全完成的？这个问题的答案如下:\n\n*   拥抱 DevOps\n*   适应测试驱动的开发\n*   适应行为驱动的开发\n*   使用验收测试驱动的开发\n\n# 代码异味\n\n源代码有两个方面的质量，即**功能性**和**结构性**。一段源代码的功能质量可以通过对照客户规范测试代码来实现。大多数开发人员犯的最大错误是，他们倾向于将代码提交给版本控制软件，而不进行重构；也就是说，他们在认为代码功能完整的那一刻就提交了代码。\n\n事实上，将代码提交给版本控制通常是一个好习惯，因为这使得持续集成和 DevOps 成为可能。在将代码提交给版本控制后，绝大多数开发人员忽略的是对其进行重构。重构代码以确保它是干净的是非常重要的，没有它敏捷是不可能的。\n\n看起来像面条(意大利面)的代码需要更多的努力来增强或维护。因此，快速响应客户的请求实际上是不可能的。这就是为什么保持整洁的代码对于敏捷是至关重要的。无论您的组织遵循什么样的敏捷框架，这都是适用的。\n\n# 什么是敏捷？\n\n敏捷就是**快速失败**。敏捷团队将能够快速响应客户的需求，而不需要开发团队的参与。团队使用哪种敏捷框架并不重要:Scrum、看板、XP 或其他。真正重要的是，你是否认真地跟随他们？\n\n作为一名独立的软件顾问，我个人观察和了解了一般谁会抱怨，以及他们为什么会抱怨敏捷。\n\n由于 Scrum 是最受欢迎的敏捷框架之一，让我们假设一家产品公司，比如中航科技私人有限公司，已经决定跟随 Scrum 开发他们计划开发的新产品。好消息是，ABC Tech 也像大多数组织一样，高效地主持了 Sprint 计划会议、每日站起来会议、Sprint 回顾、Sprint 回顾以及所有其他 Scrum 仪式。假设 ABC Tech 已经确保他们的 Scrum 大师是 Scrum 认证的，并且产品经理是 Scrum 认证的产品所有者。太好了。到目前为止一切听起来都很好。\n\n假设 ABC Tech 产品团队不使用 TDD、BDD、ATDD 和 DevOps。您认为中航科技产品团队是否敏捷？当然不是。事实上，开发团队会因为紧张而不切实际的日程安排而高度紧张。说到底，自然减员率会非常高，因为球队不会高兴。因此，顾客会不高兴，因为产品质量会受到严重影响。\n\n您认为中航科技产品团队出了什么问题？\n\nScrum 有两组过程，即项目管理过程，它被 Scrum 仪式所覆盖。然后是流程的工程方面，这是大多数组织不太关注的。这一点从 IT 行业对**认证 SCRUM 开发人员** ( **CSD** )认证的兴趣或意识中可以明显看出。IT 行业对 CSM、CSPO 或 CSP 表现出的兴趣程度很难向 CSD 显示，而这是开发人员所必需的。然而，我不相信仅仅认证就能让一个人成为主题专家；它只显示了个人或组织在接受敏捷框架和向客户交付高质量产品时所表现出的严肃性。\n\n除非代码保持干净，否则开发团队怎么可能快速响应客户的需求？换句话说，除非开发团队中的工程师在产品开发中接受 TDD、BDD、ATDD、持续集成和 DevOps，否则没有团队能够在 Scrum 中取得成功，或者说，在任何其他敏捷框架中取得成功。\n\n底线是，除非你的组织将工程 Scrum 过程和项目管理 Scrum 过程同等重视，否则没有一个开发团队能够宣称在敏捷中取得成功。\n\n# 固体设计原理\n\nSOLID 是一组重要设计原则的首字母缩略词，如果遵循这些原则，可以避免代码异味并提高代码质量，包括结构和功能。\n\n如果您的软件体系结构符合 SOLID 设计原则，代码异味可以被防止或重构为整洁的代码。以下原则统称为固体设计原则:\n\n*   单一责任原则\n*   开放封闭原则\n*   利斯科夫替代原理\n*   界面分离\n*   依赖倒置\n\n最好的部分是大多数设计模式也遵循并符合 SOLID 设计原则。\n\n让我们在接下来的部分中逐一介绍前面的每个设计原则。\n\n# 单一责任原则\n\n**单一责任原则**简称 **SRP** 。SRP 说每个班级必须只有一个责任。换句话说，每个类必须恰好代表一个对象。当一个类代表多个对象时，它往往会违反 SRP，并为多个代码异味打开机会。\n\n例如，我们来看一个简单的`Employee`类，如下所示:\n\n![](img/00197.gif)\n\n在上一个类图中，`Employee`类似乎代表了三个不同的对象:`Employee`、`Address`和`Contact`。因此，它违反了自律公约。按照这个原则，从前面的`Employee`类中，可以提取另外两个类，即`Address`和`Contact`，如下:\n\n![](img/00198.gif)\n\n为了简单起见，本节中使用的类图没有显示任何被各自的类支持的方法，因为我们的重点是用一个简单的例子来理解 SRP。\n\n在前面重构的设计中，雇员有一个或多个地址(个人和官方)和一个或多个联系人(个人和官方)。最好的部分是重构设计后，每个类抽象出一个唯一的东西；也就是说，它只有一个责任。\n\n# 开放封闭原则\n\n当设计支持在不改变代码或不修改现有源代码的情况下添加新功能时，架构或设计符合**开放封闭原则** ( **OCP** )。正如你所知，基于你的专业行业经验，你遇到的每个项目都可以以这样或那样的方式扩展。这就是你如何能够给你的产品增加新的功能。然而，当这样的功能扩展完成时，设计将符合 OCP，而无需您修改现有的代码。\n\n我们来看一个简单的`Item`类，如下代码所示。为简单起见，仅在`Item`类中捕获基本细节:\n\n```cpp\n#include <iostream>\n#include <string>\nusing namespace std;\nclass Item {\n       private:\n         string name;\n         double quantity;\n         double pricePerUnit;\n       public:\n         Item ( string name, double pricePerUnit, double quantity ) {\n         this-name = name; \n         this->pricePerUnit = pricePerUnit;\n         this->quantity = quantity;\n    }\n    public double getPrice( ) {\n           return quantity * pricePerUnit;\n    }\n    public String getDescription( ) {\n           return name;\n    }\n};\n```\n\n假设前面的`Item`类是一个小商店的简单计费应用的一部分。由于`Item`类将能够代表笔、计算器、巧克力、笔记本等，因此它足够通用，可以支持商店处理的任何可计费项目。然而，如果店主应该征收**商品及服务税** ( **商品及服务税**)或**增值税** ( **增值税**)，现有的`Item`类似乎不支持税收部分。一种常见的方法是修改`Item`类以支持税收部分。然而，如果我们修改现有的代码，我们的设计将不符合 OCP。\n\n因此，让我们使用 Visitor 设计模式重构我们的设计，使其符合 OCP 标准。让我们探索重构的可能性，如下面的代码所示:\n\n```cpp\n#ifndef __VISITABLE_H\n#define __VISITABLE_H\n#include <string>\n using namespace std;\nclass Visitor;\n\nclass Visitable {\n public:\n        virtual void accept ( Visitor * ) = 0;\n        virtual double getPrice() = 0;\n        virtual string getDescription() = 0;\n };\n#endif\n```\n\n`Visitable`类是一个抽象类，有三个纯虚函数。`Item`类将继承`Visitable`抽象类，如下所示:\n\n```cpp\n#ifndef __ITEM_H\n#define __ITEM_H\n#include <iostream>\n#include <string>\nusing namespace std;\n#include \"Visitable.h\"\n#include \"Visitor.h\"\nclass Item : public Visitable {\n private:\n       string name;\n       double quantity;\n       double unitPrice;\n public:\n       Item ( string name, double quantity, double unitPrice );\n       string getDescription();\n       double getQuantity();\n       double getPrice();\n       void accept ( Visitor *pVisitor );\n };\n\n #endif\n```\n\n接下来，我们来看看`Visitor`类，如下代码所示。它表示未来可以实现任意数量的`Visitor`子类来添加新功能，所有这些都不需要修改`Item`类:\n\n```cpp\nclass Visitable;\n#ifndef __VISITOR_H\n#define __VISITOR_H\nclass Visitor {\n protected:\n double price;\n\n public:\n virtual void visit ( Visitable * ) = 0;\n virtual double getPrice() = 0;\n };\n\n #endif\n```\n\n`GSTVisitor`类是允许我们在不修改`Item`类的情况下添加商品及服务税功能的类。`GSTVisitor`的实现是这样的:\n\n```cpp\n#include \"GSTVisitor.h\"\n\nvoid GSTVisitor::visit ( Visitable *pItem ) {\n     price = pItem->getPrice() + (0.18 * pItem->getPrice());\n}\n\ndouble GSTVisitor::getPrice() {\n     return price;\n}\n```\n\n`Makefile`如下图所示:\n\n```cpp\nall: GSTVisitor.o Item.o main.o\n     g++ -o gst.exe GSTVisitor.o Item.o main.o\n\nGSTVisitor.o: GSTVisitor.cpp Visitable.h Visitor.h\n     g++ -c GSTVisitor.cpp\n\nItem.o: Item.cpp\n     g++ -c Item.cpp\n\nmain.o: main.cpp\n     g++ -c main.cpp\n\n```\n\n重构后的设计符合 OCP 标准，因为我们可以在不修改`Item`类的情况下添加新的功能。试想一下:如果商品及服务税的计算不时变化，而不修改`Item`类，我们将能够添加`Visitor`的新子类，并解决即将到来的变化。\n\n# 利斯科夫替代原理\n\n**利斯科夫替代原则** ( **LSP** )强调子类遵守基类建立的契约的重要性。在理想的继承层次结构中，当设计焦点在类层次结构中上移时，我们应该注意到泛化；随着设计焦点在类层次结构中下移，我们应该注意到专门化。\n\n继承契约在两个类之间，因此基类有责任强加所有子类都可以遵循的规则，一旦达成一致，子类同样有责任遵守契约。折衷这些设计理念的设计将不符合 LSP。\n\nLSP 说，如果一个方法以基类或接口作为参数，人们应该能够无条件地替换任何一个子类的实例。\n\n事实上，继承违反了最基本的设计原则:继承弱内聚，强耦合。因此，继承的真正好处是多态性，与为继承付出的代价相比，代码重用只是一个微小的好处。当违反 LSP 时，我们不能用它的一个子类实例替换基类实例，最糟糕的是我们不能多形态地调用方法。尽管付出了使用继承的设计代价，如果我们不能获得多态性的好处，就没有使用它的真正动机。\n\n识别 LSP 违规的技术如下:\n\n*   子类将有一个或多个带有空实现的重写方法\n*   基类将有一个专门化的行为，这将强制某些子类，不管那些专门化的行为是否是子类感兴趣的\n*   不是所有的一般化方法都可以多形态调用\n\n以下是重构 LSP 违规的方法:\n\n*   将专门化的方法从基类转移到需要那些专门化行为的子类。\n*   避免强迫模糊相关的类参与继承关系。除非子类是基类型，否则不要仅仅为了代码重用而使用继承。\n*   不要寻找小的好处，比如代码重用，而是尽可能寻找使用多态性、聚合或组合的方法。\n\n# 界面分离\n\n**接口隔离**设计原则建议为特定目的建模许多小接口，而不是为代表许多事物的一个更大的接口建模。在 C++ 的情况下，具有纯虚函数的抽象类可以被认为是一个接口。\n\n让我们举一个简单的例子来理解接口隔离:\n\n```cpp\n#include <iostream>\n#include <string>\nusing namespace std;\n\nclass IEmployee {\n      public:\n          virtual string getDoor() = 0;\n          virtual string getStreet() = 0;\n          virtual string getCity() = 0;\n          virtual string getPinCode() = 0;\n          virtual string getState() = 0;\n          virtual string getCountry() = 0;\n          virtual string getName() = 0;\n          virtual string getTitle() = 0;\n          virtual string getCountryDialCode() = 0;\n          virtual string getContactNumber() = 0;\n};\n```\n\n在前面的例子中，抽象类演示了一个混乱的设计。这个设计很混乱，因为它似乎代表了许多东西，比如员工、地址和联系人。可以重构前面抽象类的方法之一是将单个接口分成三个独立的接口:`IEmployee`、`IAddress`和`IContact`。在 C++ 中，接口只不过是带有纯虚函数的抽象类:\n\n```cpp\n#include <iostream>\n#include <string>\n#include <list>\nusing namespace std;\n\nclass IEmployee {\n  private:\n     string firstName, middleName, lastName,\n     string title;\n     string employeeCode;\n     list<IAddress> addresses;\n     list<IContact> contactNumbers;\n  public:\n     virtual string getAddress() = 0;\n     virtual string getContactNumber() = 0;\n};\n\nclass IAddress {\n     private:\n          string doorNo, street, city, pinCode, state, country;\n     public:\n          IAddress ( string doorNo, string street, string city, \n            string pinCode, string state, string country );\n          virtual string getAddress() = 0;\n};\n\nclass IContact {\n      private:\n           string countryCode, mobileNumber;\n      public:\n           IContact ( string countryCode, string mobileNumber );\n           virtual string getMobileNumber() = 0;\n};\n```\n\n在重构的代码片段中，每个接口只代表一个对象，因此它符合接口隔离设计原则。\n\n# 依赖倒置\n\n一个好的设计将是强内聚和松散耦合的。因此，我们的设计必须具有较少的依赖性。使代码依赖于许多其他对象或模块的设计被认为是糟糕的设计。如果违反**依赖反转** ( **DI** )，依赖模块中发生的任何变化都会对我们的模块产生不好的影响，导致连锁反应。\n\n让我们举一个简单的例子来理解 DI 的力量。一个`Mobile`类“有一个”`Camera`对象，注意，有一个形式就是构图。合成是一种专有所有权，其中`Camera`对象的寿命由`Mobile`对象直接控制:\n\n![](img/00199.gif)\n\n在上图中可以看到，`Mobile`类有一个`Camera`的实例，*有一个*使用的形式是 composition，这是一个排他的所有权关系。\n\n我们来看看`Mobile`类的实现，如下:\n\n```cpp\n#include <iostream>\nusing namespace std;\n\nclass Mobile {\n     private:\n          Camera camera;\n     public:\n          Mobile ( );\n          bool powerOn();\n          bool powerOff();\n};\n\nclass Camera {\n      public:\n          bool ON();\n          bool OFF();\n};\n\nbool Mobile::powerOn() {\n       if ( camera.ON() ) {\n           cout << \"\\nPositive Logic - assume some complex Mobile power ON logic happens here.\" << endl;\n           return true;\n       }\n       cout << \"\\nNegative Logic - assume some complex Mobile power OFF logic happens here.\" << endl;\n            << endl;\n       return false;\n}\n\nbool Mobile::powerOff() {\n      if ( camera.OFF() ) {\n              cout << \"\\nPositive Logic - assume some complex Mobile power OFF             logic happens here.\" << endl;\n      return true;\n }\n      cout << \"\\nNegative Logic - assume some complex Mobile power OFF logic happens here.\" << endl;\n      return false;\n}\n\nbool Camera::ON() {\n     cout << \"\\nAssume Camera class interacts with Camera hardware here\\n\" << endl;\n     cout << \"\\nAssume some Camera ON logic happens here\" << endl;\n     return true;\n}\n\nbool Camera::OFF() {\n cout << \"\\nAssume Camera class interacts with Camera hardware here\\n\" << endl;\n cout << \"\\nAssume some Camera OFF logic happens here\" << endl;\n return true;\n}\n```\n\n在前面的代码中，`Mobile`有关于`Camera`的实现级知识，这是一个糟糕的设计。理想情况下，`Mobile`应该通过一个接口或者一个带有纯虚函数的抽象类与`Camera`进行交互，因为这将`Camera`的实现从它的契约中分离出来。这种方法有助于在不影响`Mobile`的情况下替换`Camera`，并且还提供了一个支持一堆`Camera`子类来代替单个摄像机的机会。\n\n想知道为什么叫**依赖注入** ( **DI** )或者**控制反转** ( **IOC** )？之所以称之为依赖注入，是因为目前`Camera`的寿命由`Mobile`对象控制；也就是说，`Camera`被`Mobile`对象实例化并销毁。在这种情况下，在没有`Camera`的情况下几乎不可能对`Mobile`进行单元测试，因为`Mobile`对`Camera`有很强的依赖性。除非实现`Camera`，否则我们无法测试`Mobile`的功能，这是一种糟糕的设计方法。当我们反转依赖关系时，它让`Mobile`对象使用`Camera`对象，同时放弃控制`Camera`对象寿命的责任。这一过程被正确地称为国际奥委会。这样做的好处是，您将能够独立地对`Mobile`和`Camera`对象进行单元测试，并且由于 IOC，它们将具有很强的内聚性和松散耦合性。\n\n让我们用 DI 设计原则重构前面的代码:\n\n```cpp\n#include <iostream>\nusing namespace std;\n\nclass ICamera {\n public:\n virtual bool ON() = 0;\n virtual bool OFF() = 0;\n};\n\nclass Mobile {\n      private:\n ICamera *pCamera;\n      public:\n Mobile ( ICamera *pCamera );\n            void setCamera( ICamera *pCamera ); \n            bool powerOn();\n            bool powerOff();\n};\n\nclass Camera : public ICamera {\npublic:\n            bool ON();\n            bool OFF();\n};\n\n//Constructor Dependency Injection\nMobile::Mobile ( ICamera *pCamera ) {\n this->pCamera = pCamera;\n}\n\n//Method Dependency Injection\nMobile::setCamera( ICamera *pCamera ) {\n this->pCamera = pCamera;\n}\n\nbool Mobile::powerOn() {\n if ( pCamera->ON() ) {\n            cout << \"\\nPositive Logic - assume some complex Mobile power ON logic happens here.\" << endl;\n            return true;\n      }\ncout << \"\\nNegative Logic - assume some complex Mobile power OFF logic happens here.\" << endl;\n<< endl;\n      return false;\n}\n\nbool Mobile::powerOff() {\n if ( pCamera->OFF() ) {\n           cout << \"\\nPositive Logic - assume some complex Mobile power OFF logic happens here.\" << endl;\n           return true;\n}\n      cout << \"\\nNegative Logic - assume some complex Mobile power OFF logic happens here.\" << endl;\n      return false;\n}\n\nbool Camera::ON() {\n       cout << \"\\nAssume Camera class interacts with Camera hardware here\\n\" << endl;\n       cout << \"\\nAssume some Camera ON logic happens here\" << endl;\n       return true;\n}\n\nbool Camera::OFF() {\n       cout << \"\\nAssume Camera class interacts with Camera hardware here\\n\" << endl;\n       cout << \"\\nAssume some Camera OFF logic happens here\" << endl;\n       return true;\n}\n```\n\n在前面的代码片段中，更改以粗体突出显示。IOC 是一种如此强大的技术，它让我们可以像刚才演示的那样分离依赖关系；然而，它的实现相当简单。\n\n# 代码异味\n\n代码异味是一个术语，用来指一段缺乏结构质量的代码；然而，代码在功能上可能是正确的。代码异味违反了 SOLID 设计原则，因此必须认真对待，因为从长远来看，写得不好的代码会导致沉重的维护成本。然而，代码异味可以重构为整洁的代码。\n\n# 评论气味\n\n作为一名独立的软件顾问，我有很多机会与伟大的开发人员、架构师、质量保证人员、系统管理员、首席技术官和首席执行官、企业家等进行互动和学习。每当我们的讨论跨越十亿美元的问题，“什么是整洁的代码或好的代码？”，我或多或少得到了一个全球通用的回答，“好的代码会得到很好的评价。”虽然这是部分正确的，但这确实是问题的开始。理想情况下，整洁的代码应该是不言自明的，不需要任何注释。但是，在某些情况下，注释可以提高整体可读性和可维护性。不是所有的注释都是代码异味，因此有必要区分好的注释和坏的注释。看看下面的代码片段:\n\n```cpp\nif ( condition1 ) {\n     // some block of code\n}\nelse if ( condition2 ) {\n     // some block of code\n}\nelse {\n     // OOPS - the control should not reach here ### Code Smell ###\n}\n```\n\n我相信你已经见过这种评论了。不用解释，前面的场景是一种代码味道。理想情况下，开发人员应该重构代码来修复错误，而不是编写这样的注释。我曾经在半夜调试一个关键问题，我注意到控件到达了神秘的空代码块，其中只有一个注释。我相信你遇到过更有趣的代码，可以想象它带来的挫败感；有时，您也会编写这样的代码。\n\n一个好的评论会表达*为什么*代码是以特定的方式编写的，而不是表达*代码是如何做某事的。传达代码如何做某事的注释是代码异味，而传达代码的为什么部分是好的注释的注释，因为为什么部分不是由代码表达的；因此，一个好的评论可以增加价值。*\n\n# 长法\n\n当一个方法被确定具有多重责任时，它就是长的。自然，一个拥有超过 20-25 行代码的方法往往有不止一个责任。话虽如此，代码行越多的方法越长。这并不意味着少于 25 行代码的方法不会更长。看看下面的代码片段:\n\n```cpp\nvoid Employee::validateAndSave( ) {\n        if ( ( street != \"\" ) && ( city != \"\" ) )\n              saveEmployeeDetails();\n}\n```\n\n显然，前面的方法有多重责任；也就是说，它似乎验证并保存了细节。虽然保存前验证没有错，但同样的方法不应该两者兼而有之。因此，前面的方法可以重构为两个更小的方法，它们只有一个职责:\n\n```cpp\nprivate:\nvoid Employee::validateAddress( ) {\n     if ( ( street == \"\" ) || ( city == \"\" ) )\n          throw exception(\"Invalid Address\");\n}\n\npublic:\nvoid Employee::save() {\n      validateAddress();\n}\n```\n\n前面代码中显示的每个重构方法都只有一个职责。将`validateAddress()`方法变成谓词方法是很有诱惑力的；也就是说，返回 bool 的方法。但是如果把`validateAddress()`写成谓词方法，那么客户端代码就会被强制做`if`检查，这是一个代码味。通过返回错误代码来处理错误不被认为是面向对象的代码，因此错误处理必须使用 C++ 异常来完成。\n\n# 长参数列表\n\n一个面向对象的方法需要更少的参数，因为一个设计良好的对象将是强内聚和松散耦合的。一个接受太多参数的方法是一种症状，它通知做出决策所需的知识是从外部接收的，这意味着当前对象没有自己做出决策所需的所有知识。\n\n这意味着当前对象是弱内聚和强耦合的，因为它依赖太多的外部数据来做出决定。成员函数通常接收较少的参数，因为它们需要的数据成员通常是成员变量。因此，将成员变量传递给成员函数的需求听起来是人为的。\n\n让我们看看为什么一个方法会接收太多参数的一些常见原因。这里列出了最常见的症状和原因:\n\n*   对象弱内聚，强耦合；也就是说，它太依赖其他物体了\n*   这是一种静态方法\n*   这是一种错位的方法；也就是说，它不属于那个对象\n*   它不是面向对象的代码\n*   违反了 SRP\n\n重构一个需要**长参数表** ( **LPL** )的方法的方法如下:\n\n*   避免零碎地提取和传递数据；考虑传递一个完整的对象，让方法提取它需要的细节\n*   确定向接收 LPL 的方法提供参数的对象，并考虑将该方法移到那里\n*   将参数列表分组，创建一个参数对象，并将接收 LPL 的方法移到新对象中\n\n# 重复代码\n\n重复代码是一种常见的重复代码异味，不需要太多解释。复制和粘贴代码区域性本身不能成为代码重复的原因。重复的代码使代码维护变得更加麻烦，因为相同的问题可能必须在多个地方修复，集成新功能需要太多的代码更改，这往往会破坏意想不到的功能。重复的代码还会增加应用二进制文件的占用空间，因此必须重构它来清理代码。\n\n# 条件复杂性\n\n条件复杂性代码异味是关于复杂的大型条件，随着时间的推移，这些条件会变得越来越大，越来越复杂。这种代码味道可以用策略设计模式重构。由于策略设计模式处理许多相关的对象，所以有使用`Factory`方法的余地，**空对象设计模式**可以用来处理`Factory`方法中不支持的子类:\n\n```cpp\n//Before refactoring\nvoid SomeClass::someMethod( ) {\n      if (  ! conition1 && condition2 )\n         //perform some logic\n      else if ( ! condition3 && condition4 && condition5 )\n         //perform some logic\n      else\n         //do something \n} \n\n//After refactoring\nvoid SomeClass::someMethod() {\n     if ( privateMethod1() )\n          //perform some logic\n     else if ( privateMethod2() )\n          //perform some logic\n     else\n         //do something\n}\n```\n\n# 大班\n\n大量的类代码异味使得代码难以理解和维护。一个大班可以为一个班做太多事情。可以通过将大型类分解成具有单一职责的小型类来重构它们。\n\n# 死代码\n\n死代码是注释代码或从未使用或集成的代码。它可以用代码覆盖工具检测到。通常，开发人员会因为缺乏信心而保留这些代码实例，这种情况在遗留代码中更常见。由于每个代码都在版本控制软件工具中被跟踪，死代码可以被删除，并且如果需要，总是可以从版本控制软件中检索回来。\n\n# 原始的痴迷\n\n**原语痴迷** ( **PO** )是一个错误的设计选择:使用原语数据类型来表示复杂的域实体。例如，如果使用字符串数据类型来表示日期，尽管这在最初听起来是一个明智的想法，但从长远来看，它会带来很多维护麻烦。\n\n假设您使用字符串数据类型来表示日期，以下问题将是一个挑战:\n\n*   你需要根据日期来分类\n*   随着字符串的引入，日期算法将变得非常复杂\n*   根据地区设置支持各种日期格式将变得复杂\n\n理想情况下，日期必须由类来表示，而不是原始数据类型。\n\n# 数据类\n\n数据类只提供 getter 和 setter 函数。虽然它们非常适合将数据从一个层传输到另一个层，但是它们往往会加重依赖于数据类的类的负担。由于数据类不会提供任何有用的功能，交互或依赖数据类的类最终会用数据类中的数据添加功能。以这种方式，数据类周围的类违反了 SRP，并且往往是一个大类。\n\n# 特征嫉妒\n\n如果某些类对其他类的其他内部细节有太多的了解，它们就被称为功能嫉妒。通常，当其他类是数据类时会发生这种情况。代码异味是相互关联的；打破一种代码异味往往会吸引其他代码异味。\n\n# 摘要\n\n在本章中，您了解了以下主题:\n\n*   代码异味和重构代码的重要性\n*   立体设计原则:\n\n    *   单一责任原则\n    *   开放封闭原则\n    *   利斯科夫替代\n    *   界面分离\n    *   依赖注入\n\n*   各种代码异味:\n    *   评论有味道\n    *   长法\n    *   长参数列表\n    *   重复代码\n    *   条件复杂性\n    *   大班\n    *   死代码\n\n*   面向对象的代码散发着原始的痴迷\n    *   数据类\n    *   特征嫉妒\n\n您还学习了许多重构技术，这些技术将帮助您保持代码的整洁。快乐编码！"
  },
  {
    "path": "docs/master-cpp-prog/README.md",
    "content": "# 精通 C++ 编程\n\n> 原书：[Mastering C++ Programming](https://libgen.rs/book/index.php?md5=0E32826EC8D4CA7BCD89E795AD6CBF05)\n> \n> 协议：[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)\n> \n> 阶段：机翻（1）\n> \n> 自豪地采用[谷歌翻译](https://translate.google.cn/)\n> \n> 让开源界提前感受入关。\n\n* [在线阅读](https://ccpp.apachecn.org)\n* [在线阅读（Gitee）](https://apachecn.gitee.io/apachecn-c-cpp-zh/)\n* [ApacheCN 学习资源](http://docs.apachecn.org/)\n\n## 贡献指南\n\n本项目需要校对，欢迎大家提交 Pull Request。\n\n> 请您勇敢地去翻译和改进翻译。虽然我们追求卓越，但我们并不要求您做到十全十美，因此请不要担心因为翻译上犯错——在大部分情况下，我们的服务器已经记录所有的翻译，因此您不必担心会因为您的失误遭到无法挽回的破坏。（改编自维基百科）\n\n## 联系方式\n\n### 负责人\n\n* [飞龙](https://github.com/wizardforcel): 562826179\n\n### 其他\n\n*   在我们的 [apachecn/apachecn-c-cpp-zh](https://github.com/apachecn/apachecn-c-cpp-zh) github 上提 issue.\n*   发邮件到 Email: `apachecn@163.com`.\n*   在我们的 [组织学习交流群](http://www.apachecn.org/organization/348.html) 中联系群主/管理员即可.\n\n## 赞助我们\n\n![](http://data.apachecn.org/img/about/donate.jpg)\n"
  },
  {
    "path": "docs/master-cpp-prog/SUMMARY.md",
    "content": "+   [精通 C++ 编程](README.md)\n+   [零、前言](00.md)\n+   [一、C++ 17 特性](01.md)\n+   [二、标准模板库](02.md)\n+   [三、模板编程](03.md)\n+   [四、智能指针](04.md)\n+   [五、使用 C++ 开发图形用户界面应用](05.md)\n+   [六、多线程编程和进程间通信](06.md)\n+   [七、测试驱动开发](07.md)\n+   [八、行为驱动开发](08.md)\n+   [九、调试技术](09.md)\n+   [十、代码异味和整洁的代码实践](10.md)\n"
  },
  {
    "path": "docs/mod-cpp/00.md",
    "content": "# 零、前言\n\nC++ 是使用最广泛的编程语言之一。它快速、灵活、高效，用于解决许多编程问题。\n\n这个学习路径的目标是让你熟悉和适应 C++。通过学习语言结构、函数和类，您将熟悉 C++ 编程的结构，这将有助于您识别代码中的执行流。您将探索并理解 C++ 标准库以及内存分配对于编写更好更快的程序的重要性。\n\n本学习路径还涉及理解高级 C++ 编程带来的挑战。您将学习高级主题，如多线程、网络、并发、性能、元编程、lambda 表达式、正则表达式、测试以及食谱形式的更多内容。\n\n在这条学习之路结束时，你将成为一名 C++ 专家。\n\n# 这本书是给谁的\n\n这个学习路径是为想用 C++ 获得坚实基础的开发人员设计的。一台电脑，一个互联网连接，以及学习如何用 C++ 编程的愿望，是你开始学习这条学习之路所需要的一切。\n\n# 这本书涵盖了什么\n\n[第 1 章](01.html)、*了解语言特性*，涵盖 C++ 语句和表达式、常量、变量、运算符，以及如何控制应用中的执行流程。\n\n[第 2 章](02.html)*使用内存、数组和指针*，讲述了内存在 C++ 应用中是如何分配和使用的，如何使用内置数组，C++ 引用的作用，以及如何使用 C++ 指针访问内存。\n\n[第 3 章](03.html)、*使用函数*，解释了如何定义函数，如何使用可变数量的参数按引用和按值传递参数，创建和使用指向函数的指针，以及定义模板函数和重载运算符。\n\n[第 4 章](04.html)、*类*，描述了如何通过类定义新的类型以及类中使用的各种特殊函数，如何将类实例化为对象以及如何销毁它们，如何通过指针访问对象以及如何编写模板类。\n\n[第 5 章](05.html)、*使用标准库容器*，涵盖了所有的 C++ 标准库容器类，以及如何将它们与迭代器和标准算法一起使用，以便您可以操作容器中的数据。\n\n[第 6 章](06.html)、*使用字符串*描述了标准 C++ 字符串类的特性，在数字数据和字符串之间进行转换，字符串国际化，以及使用正则表达式来搜索和操作字符串。\n\n[第 7 章](07.html)、*诊断和调试*，解释了如何准备代码以提供诊断并使其能够被调试，如何突然或优雅地终止应用，以及如何使用 C++ 异常。\n\n[第 8 章](08.html)、*学习现代核心语言特性*，教你现代核心语言特性，包括类型推断、统一初始化、范围枚举、基于范围的 for 循环、结构化绑定等。\n\n[第九章](09.html)*处理数字和字符串*，讨论了如何在数字和字符串之间进行转换，生成伪随机数，处理正则表达式以及各种类型的字符串。\n\n[第 10 章](10.html)、*探索函数*，深入到默认和删除函数、变量模板、λ表达式和高阶函数。\n\n[第 11 章](11.html)、*标准库容器、算法和迭代器*，介绍几个标准容器、很多算法，教你如何编写自己的随机访问迭代器。\n\n[第 12 章](12.html)、*数学题*，包含一系列数学练习，让你为接下来几章更具挑战性的问题热身。\n\n[第 13 章](13.html)*语言特性*，为您练习运算符重载、移动语义、用户定义的文字以及模板元编程方面(如变量函数、折叠表达式和类型特征)提出了问题。\n\n[第 14 章](14.html)、*字符串和正则表达式*对于字符串操作有几个问题，比如在字符串和其他数据类型之间转换、拆分和连接字符串，以及处理正则表达式。\n\n[第 15 章](15.html)*流和文件系统*，介绍了输出流操作以及使用 C++ 17 `filesystem`库处理文件和目录。\n\n[第 16 章](16.html)、*日期和时间*，为即将到来的`chrono`库的 C++ 20 扩展做好准备，有几个日历和时区问题可以用`date`库解决，新的标准添加就是基于这个库。\n\n[第 17 章](17.html)、*算法和数据结构*，是最大的章节之一，包含了各种需要利用现有标准算法的问题；其他是需要实现自己的通用算法或数据结构的地方，比如循环缓冲区和优先级队列。这一章以两个相当有趣的问题结束，道金斯的黄鼠狼程序和康威的生命游戏程序，在那里你可以了解进化算法和细胞自动机。\n\n# 充分利用这本书\n\n读者应配备以下环境配置:\n\n1.  C++ 11(英特尔、IBM、Sun、苹果和微软，以及开源 GCC)\n2.  Visual C++ 2017 社区版\n3.  Windows 上的 VC++ 2017\n4.  Linux 和 Mac 上的 GCC 7.0 或 Clang 5.0\n\n如果您没有最新版本的编译器，或者您想尝试其他编译器，可以使用在线提供的编译器。虽然有各种各样的在线平台可以使用，但我推荐 GCC 和 Clang 的[https://wandbox.org/](https://wandbox.org/)，VC++ 的[http://webcompiler.cloudapp.net/](http://webcompiler.cloudapp.net/)。\n\n在使用支持 C++ 17 的编译器时，您将需要一个所需库的完整列表。\n\n# 如何为 Visual Studio 2017 生成项目\n\n请执行以下操作，以生成面向 x86 平台的 Visual Studio 2017 项目:\n\n1.  打开命令提示符，转到源代码根文件夹中的`build`目录。\n2.  执行以下命令:\n\n    ``cmake -G \"Visual Studio 15 2017\" .. -DCMAKE_USE_WINSSL=ON -DCURL_WINDOWS_SSPI=ON -DCURL_LIBRARY=libcurl -DCURL_INCLUDE_DIR=..\\libs\\curl\\include -DBUILD_TESTING=OFF -DBUILD_CURL_EXE=OFF -DUSE_MANUAL=OFF``\n\n3.  完成后，可以在`build/cppchallenger.sln`找到 Visual Studio 解决方案。\n\n如果你想以 x64 平台为目标，可以使用名为`\"Visual Studio 15 2017 Win64\"`的生成器。Visual Studio 2017 15.4 同时支持`filesystem`(作为实验库)和`std::optional`。如果您使用以前的版本，或者只想使用 Boost 库，则可以在正确安装 Boost 后，使用以下命令生成项目:\n\n```cpp\ncmake -G \"Visual Studio 15 2017\" .. -DCMAKE_USE_WINSSL=ON -DCURL_WINDOWS_SSPI=ON -DCURL_LIBRARY=libcurl -DCURL_INCLUDE_DIR=..\\libs\\curl\\include -DBUILD_TESTING=OFF -DBUILD_CURL_EXE=OFF -DUSE_MANUAL=OFF -DBOOST_FILESYSTEM=ON -DBOOST_OPTIONAL=ON -DBOOST_INCLUDE_DIR=<path_to_headers> -DBOOST_LIB_DIR=<path_to_libs>\n```\n\n确保头文件和静态库文件的路径不包含尾随反斜杠(即`\\`)。\n\n# 下载示例代码文件\n\n你可以从你在[www.packt.com](http://www.packt.com)的账户下载这本书的示例代码文件。如果您在其他地方购买了这本书，您可以访问[www.packt.com/support](http://www.packt.com/support)并注册将文件直接通过电子邮件发送给您。\n\n您可以按照以下步骤下载代码文件:\n\n1.  登录或注册[www.packt.com](http://www.packt.com)。\n2.  选择“支持”选项卡。\n3.  点击代码下载和勘误表。\n4.  在搜索框中输入图书的名称，并按照屏幕指示进行操作。\n\n下载文件后，请确保使用最新版本的解压缩文件夹:\n\n*   视窗系统的 WinRAR/7-Zip\n*   zipeg/izp/un ARX for MAC\n*   适用于 Linux 的 7-Zip/PeaZip\n\n这本书的代码包也托管在 GitHub 上，网址为[。如果代码有更新，它将在现有的 GitHub 存储库中更新。](https://github.com/PacktPublishing/Modern-C-plus-plus-Efficient-and-Scalable-Application-Development)\n\n我们还有来自丰富的图书和视频目录的其他代码包，可在**[【https://github.com/PacktPublishing/】](https://github.com/PacktPublishing/)**获得。看看他们！\n\n# 下载彩色图像\n\n我们还提供了一个 PDF 文件，其中包含本书中使用的截图/图表的彩色图像。可以在这里下载:[https://www . packtpub . com/sites/default/files/downloads/Cplusplus _ Efficient _ and _ Scalable _ Application _ development . pdf](https://www.packtpub.com/sites/default/files/downloads/Cplusplus_Efficient_and_Scalable_Application_Development.pdf)\n\n# 使用的约定\n\n文本中的码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟网址、用户输入和推特句柄如下所示:“作者打算键入`c = a + 8 / b + 1;`和`:`，他们按下了逗号而不是`/`\n\n代码块设置如下:\n\n```cpp\ninline auto mult(int lhs, int rhs) -> int \n    { \n        return lhs * rhs; \n    }\n```\n\n当我们希望将您的注意力吸引到代码块的特定部分时，相关的行或项目以粗体显示:\n\n```cpp\nif (op == ',' || op == '.' || op < '+' || op > '/') \n    { \n        cout << endl << \"operator not recognized\" << endl; \n        usage(); \n        return 1; \n    }\n```\n\n任何命令行输入或输出都编写如下:\n\n```cpp\nC:\\Beginning_C++ Chapter_02\\cl /EHsc calc.cpp\n```\n\n**粗体**:新词、重要词以粗体显示。你在屏幕上看到的单词，例如在菜单或对话框中看到的单词，出现在这样的文本中:“函数的**调用约定**决定了是调用函数还是被调用函数有责任这样做。”\n\nWarnings or important notes appear like this. Tips and tricks appear like this.\n\n# 取得联系\n\n我们随时欢迎读者的反馈。\n\n**一般反馈**:如果你对这本书的任何方面有疑问，在你的信息主题中提到书名，发邮件给我们`customercare@packtpub.com`。\n\n**勘误表**:虽然我们已经尽了最大的努力来保证内容的准确性，但是错误还是会发生。如果你在这本书里发现了一个错误，如果你能向我们报告，我们将不胜感激。请访问[www.packt.com/submit-errata](http://www.packt.com/submit-errata)，选择您的图书，点击勘误表提交链接，并输入详细信息。\n\n**盗版**:如果您在互联网上遇到任何形式的我们作品的非法拷贝，如果您能提供我们的位置地址或网站名称，我们将不胜感激。请通过`copyright@packt.com`联系我们，并提供材料链接。\n\n**如果你有兴趣成为一名作者**:如果有一个你有专长的话题，你有兴趣写或者投稿一本书，请访问[authors.packtpub.com](http://authors.packtpub.com/)。\n\n# 复习\n\n请留下评论。一旦你阅读并使用了这本书，为什么不在你购买它的网站上留下评论呢？然后，潜在的读者可以看到并使用您不带偏见的意见来做出购买决定，我们在 Packt 可以了解您对我们产品的看法，我们的作者可以看到您对他们的书的反馈。谢谢大家！\n\n更多关于 Packt 的信息，请访问[packt.com](http://www.packt.com/)。"
  },
  {
    "path": "docs/mod-cpp/01.md",
    "content": "# 一、理解语言特性\n\n在本章中，您将深入学习各种语言特性来控制代码中的流程。\n\n# 写 C++\n\n在格式化和编写代码方面，C++ 是一种非常灵活的语言。它也是一种强类型语言，这意味着有关于声明变量类型的规则，您可以通过让编译器帮助您编写更好的代码来利用这些规则。在本节中，我们将介绍如何格式化 C++ 代码以及声明和限定变量范围的规则。\n\n# 使用空白\n\n除了字符串文字之外，您可以自由使用空格(空格、制表符、换行符)，并且可以随意使用。C++ 语句由分号分隔，因此在下面的代码中有三个语句，它们将编译并运行:\n\n```cpp\n    int i = 4; \n    i = i / 2; \n    std::cout << \"The result is\" << i << std::endl;\n```\n\n整个代码可以编写如下:\n\n```cpp\n    int i=4;i=i/2; std::cout<<\"The result is \"<<i<<std::endl;\n```\n\n有些情况下需要空格(例如，在声明变量时，类型和变量名之间必须有空格)，但是惯例是尽可能明智地使代码可读。虽然从语言的角度来看，将所有语句放在一行是完全正确的(就像 JavaScript 一样)，但它会使代码几乎完全不可读。\n\nIf you are interested in some of the more creative ways of making code unreadable, have a look at the entries for the annual International Obfuscated C Code Contest ([http://www.ioccc.org/](http://www.ioccc.org/)). As the progenitor of C++, many of the lessons in C shown at IOCCC apply to C++ code too.\n\n请记住，如果您编写的代码是可行的，它可能会使用几十年，这意味着您可能必须在编写代码几年后再回来，这意味着其他人也会支持您的代码。让您的代码可读不仅是对其他开发人员的一种礼貌，而且不可读的代码总是可能被替换的目标。\n\n# 格式化代码\n\n不可避免地，无论你为谁编写代码，都将决定你如何格式化代码。有时这是有意义的，例如，如果您使用某种形式的预处理来提取代码和定义，从而为代码创建文档。在许多情况下，强加给你的风格是别人的个人偏好。\n\nVisual C++ allows you to place XML comments in your code. To do this you use a three--slash comment (`///`) and then compile the source file with the `/doc` switch. This creates an intermediate XML file called an `xdc` file with a `<doc>` root element and containing all the three--slash comments. The Visual C++ documentation defines standard XML tags (for example, `<param>`, `<returns>` to document the parameters and return value of a function). The intermediate file is compiled to the final document XML file with the `xdcmake` utility.\n\nC++ 中有两大风格: **K & R** 和**奥尔曼**。\n\n克尼根和里奇(K&R)写了第一本，也是最有影响力的关于 C 语言的书(丹尼斯·里奇是 C 语言的作者)。K&R 风格被用来描述那本书中使用的格式风格。通常，K&R 将代码块的左大括号放在最后一条语句的同一行。如果您的代码有嵌套语句(通常会)，那么这种风格可能会有点混乱:\n\n```cpp\n    if (/* some test */) { \n        // the test is true  \n        if (/* some other test */) { \n            // second test is true  \n        } else { \n            // second test is false    \n        } \n    } else { \n        // the test is false  \n    }\n```\n\n这种风格通常用在 Unix(以及类似 Unix 的)代码中。\n\nAllman 样式(以开发人员 Eric Allman 的名字命名)将左大括号放在一个新行上，因此嵌套示例如下所示:\n\n```cpp\n        if (/* some test */)  \n        { \n            // the test is true  \n            if (/* some other test */)  \n            { \n                // second test is true   \n            }  \n            else  \n            { \n                // second test is false     \n            } \n        }  \n        else  \n        { \n           // the test is false  \n        }\n```\n\n奥尔曼风格是微软的典型风格。\n\n请记住，您的代码不太可能出现在纸面上，因此 K&R 更紧凑的事实不会拯救任何树木。如果有选择，就应该选择可读性最强的款式；这位作者的决定，对于这本书来说，是奥尔曼更具可读性。\n\n如果有多个嵌套块，缩进可以让您知道代码驻留在哪个块中。然而，评论可以有所帮助。特别是，如果一个代码块有大量的代码，注释代码块的原因通常是有帮助的。例如，在`if`语句中，将测试结果放在代码块中是有帮助的，这样您就知道该块中的变量值是什么。在测试的右括号上加上注释也很有用:\n\n```cpp\n    if (x < 0)  \n    { \n       // x < 0 \n       /* lots of code */ \n    }  // if (x < 0) \n\n    else  \n    { \n       // x >= 0 \n       /* lots of code */ \n    }  // if (x < 0)\n```\n\n如果您将测试作为注释放在右大括号上，这意味着您有一个搜索词可以用来查找导致代码块的测试。前面几行使这种注释变得多余，但是当您的代码块有几十行代码，并且有许多嵌套级别时，这样的注释会非常有帮助。\n\n# 撰写陈述\n\n语句可以是变量的声明、计算值的表达式，也可以是类型的定义。语句也可能是一种控制结构，影响代码的执行流程。\n\n语句以分号结束。除此之外，很少有关于如何格式化语句的规则。您甚至可以单独使用分号，这称为空语句。null 语句没有任何作用，所以分号太多通常是良性的。\n\n# 使用表达式\n\n表达式是一系列运算符和操作数(变量或文字)，它们会产生一些值。请考虑以下几点:\n\n```cpp\n    int i; \n    i = 6 * 7;\n```\n\n右侧`6 * 7`为表达式，赋值(从左侧`i`到右侧分号)为语句。\n\n每个表达式要么是左值，要么是右值**。您最有可能在错误描述中看到这些关键词。实际上，左值是指某个内存位置的表达式。赋值左侧的项必须是左值。然而，左值可以出现在赋值的左侧或右侧。所有变量都是左值。右值是一个临时项，它的存在时间不会长于使用它的表达式；它将有一个值，但不能有赋值，所以它只能存在于赋值的右侧。文字是右值。下面显示了左值和右值的一个简单示例:**\n\n```cpp\n    int i; \n    i = 6 * 7;\n```\n\n在第二行中，`i`是左值，表达式`6 * 7`产生右值(`42`)。由于左侧有一个右值，以下内容将无法编译:\n\n```cpp\n    6 * 7 = i;\n```\n\n广义地说，当你添加一个分号时，一个表达式就变成了一个语句。例如，以下是两种说法:\n\n```cpp\n    42;\n    std::sqrt(2);\n```\n\n第一行是`42`的右值，但是因为是临时的，所以没有效果。C++ 编译器会马上优化它。第二行调用标准库函数计算`2`的平方根。同样，结果是一个右值，并且该值没有被使用，所以编译器将对此进行优化。但是，它说明了可以在不使用返回值的情况下调用函数。虽然`std::sqrt`的情况并非如此，但许多函数除了它们的返回值之外，还有持久的影响。事实上，一个函数的全部意义通常是做一些事情，返回值通常只是用来指示函数是否成功；通常开发人员会假设一个函数会成功，并忽略返回值。\n\n# 使用逗号运算符\n\n操作符将在本章后面介绍；然而，在这里引入逗号操作符是有用的。作为一条语句，可以有一系列用逗号分隔的表达式。例如，以下代码在 C++ 中是合法的:\n\n```cpp\n    int a = 9;\n    int b = 4;\n    int c;\n    c = a + 8, b + 1;\n```\n\n作者打算输入`c = a + 8 / b + 1;`和`:`，他们用逗号代替了`/`。目的是将`c`分配到 9 + 2 + 1 或 12。这段代码将编译并运行，变量`c`将被赋值为 17 ( `a + 8`)。原因是逗号将赋值的右侧分隔为`a + 8`和`b + 1`两个表达式，它使用第一个表达式的值来赋值`c`。在本章的后面，我们将研究运算符优先级。不过这里值得一说的是，逗号的优先级最低，`+`的优先级比`=`高，所以语句执行的顺序是加法:赋值，然后是逗号运算符(结果`b + 1`扔掉)。\n\n您可以使用括号将表达式分组来更改优先级。例如，输入错误的代码可能如下:\n\n```cpp\n    c = (a + 8, b + 1);\n```\n\n这个语句的结果是:变量`c`赋给 5(或`b + 1`)。原因是使用逗号运算符，表达式是从左到右执行的，因此表达式组的值是最紧密的。有些情况下，例如在`for`循环的初始化或循环表达式中，您会发现逗号运算符很有用，但正如您在这里看到的，即使是有意使用的，逗号运算符也会产生难以阅读的代码。\n\n# 使用类型和变量\n\n在这里提供基本信息是有用的。C++ 是一种强类型语言，这意味着你必须声明你使用的变量的类型。这样做的原因是编译器需要知道要为变量分配多少内存，它可以通过变量的类型来确定这一点。此外，如果变量没有被显式初始化，编译器需要知道如何初始化变量，并且为了执行这个初始化，编译器需要知道变量的类型。\n\nC++ 11 provides the `auto` keyword, which relaxes this concept of strong typing. However, the type checking of the compiler is so important that you should use type checking as much as possible.\n\nC++ 变量可以在代码中的任何地方声明，只要它们是在使用前声明的。*在哪里声明一个变量决定了*如何使用它(这被称为变量的**作用域**)。一般来说，最好在尽可能接近您将使用它的地方，并且在最严格的范围内声明变量。这可以防止*名称冲突*，在这种情况下，您必须添加额外的信息来消除两个或更多变量的歧义。**\n\n你可以*并且应该*给你的变量起描述性的名字。这使得您的代码可读性更强，更容易理解。C++ 名称必须以字母字符或下划线开头。除空格外，它们可以包含字母数字字符，但也可以包含下划线。因此，以下是有效的名称:\n\n```cpp\n    numberOfCustomers \n    NumberOfCustomers \n    number_of_customers\n```\n\nC++ 名称区分大小写，前`2,048`个字符有意义。变量名可以以下划线开头，但不能使用两个下划线，也不能使用下划线后跟大写字母(这些是 C++ 保留的)。C++ 也保留了关键字(例如`while`和`if`，很明显你不能使用类型名作为变量名，既不能使用内置的类型名(`int`、`long`等)，也不能使用自己的自定义类型。\n\n在语句中声明一个变量，以分号结束。声明变量的基本语法是，先指定类型，然后指定名称，以及可选的变量初始化。\n\n在使用内置类型之前，必须对其进行初始化:\n\n```cpp\n    int i; \n    i++ ;           // C4700 uninitialized local variable 'i' used \n    std::cout << i;\n```\n\n初始化变量基本上有三种方法。可以赋值，可以调用类型构造函数(类的构造函数将在[第 4 章](04.html)、*类*中定义)或者可以使用函数语法初始化变量:\n\n```cpp\n    int i = 1; \n    int j = int(2); \n    int k(3);\n```\n\n这三个都是合法的 C++，但是风格上第一个更好，因为更明显:变量是整数，叫做`i`，赋值 1。第三个看起来很混乱；它看起来像是一个函数的声明，实际上它是在声明一个变量。\n\n[第四章](04.html)、*类*将涵盖类，你自己的自定义类型。自定义类型可能被定义为具有默认值，这意味着您可能决定在使用自定义类型的变量之前不初始化它。然而，这将导致较差的性能，因为编译器将用默认值初始化变量，随后您的代码将赋值，导致赋值被执行两次。\n\n# 使用常数和文字\n\n每种类型都有一个文字表示。整数是没有小数点的数字，如果是有符号的整数，文字也可以用加号或减号来表示符号。同样，实数可以有包含小数点的文字值，甚至可以使用包含指数的科学(或工程)格式。在代码中指定文字时，C++ 有各种规则可以使用。这里显示了一些文字的示例:\n\n```cpp\n    int pos = +1; \n    int neg = -1; \n    double micro = 1e-6; \n    double unit = 1.; \n    std::string name = \"Richard\";\n```\n\n注意对于`unit`变量，编译器知道文字是实数，因为数值有小数点。对于整数，您可以在代码中提供十六进制文字，方法是在数字前面加上`0x`，因此`0x100`是十进制的`256`。默认情况下，输出流将以 10 为基数打印数值；但是，您可以在输出流中插入一个**操纵器**，告诉它使用不同的基数。默认行为为`std::dec`，表示数字应以 10 为基数显示，`std::oct`表示以八进制(8 为基数)显示，`std::hex`表示以十六进制(7 为基数)显示。如果您希望看到前缀被打印，那么您可以使用流操纵器`std::showbase`(更多细节将在[第 5 章](05.html)、*使用标准库容器*中给出)。\n\nC++ 定义了一些文字。对于逻辑类型`bool`，有`true`和`false`常量，其中`false`为零，`true`为 1。还有`nullptr`常量，同样是零，它被用作任何指针类型的无效值。\n\n# 定义常数\n\n在某些情况下，您可能希望提供可以在整个代码中使用的常量值。例如，你可以决定为`π`声明一个常数。您不应该允许更改该值，因为它会更改代码中的底层逻辑。这意味着您应该将变量标记为常量。当您执行此操作时，编译器将检查变量的使用情况，如果它在更改变量值的代码中使用，编译器将发出错误:\n\n```cpp\n    const double pi = 3.1415; \n    double radius = 5.0; \n    double circumference = 2 * pi * radius;\n```\n\n在这种情况下，符号`pi`被声明为常数，因此它不能改变。如果您随后决定更改常数，编译器将发出一个错误:\n\n```cpp\n    // add more precision, generates error C3892 \n    pi += 0.00009265359;\n```\n\n一旦你声明了一个常量，你可以确信编译器会确保它保持不变。您可以使用如下表达式分配常数:\n\n```cpp\n    #include <cmath> \n    const double sqrtOf2 = std::sqrt(2);\n```\n\n在这段代码中，一个名为`sqrtOf2`的全局常数被声明，并使用`std::sqrt`函数赋值。因为这个常数是在函数外声明的，所以它对文件是全局的，可以在整个文件中使用。\n\n这种方法的问题是预处理器做了一个简单的替换。对于用`const`声明的常量，C++ 编译器将执行类型检查，以确保常量被正确使用。\n\n您也可以使用`const`来声明一个常量，该常量将被用作**常量表达式**。例如，您可以使用方括号语法声明数组(更多详细信息将在[第 2 章](02.html)、*使用内存、数组和指针*中给出):\n\n```cpp\n    int values[5];\n```\n\n这在堆栈上声明了一个由五个整数组成的数组，这些项通过`values`数组变量来访问。这里的`5`是一个常量表达式。当您在堆栈上声明一个数组时，您必须向编译器提供一个常量表达式，这样它就知道要分配多少内存，这意味着在编译时必须知道数组的大小。(您可以分配一个只有在运行时才知道大小的数组，但这需要动态内存分配，在[第 2 章](02.html)、*中解释了如何使用内存、数组和指针。*)在 C++ 中，可以声明一个常量来做以下事情:\n\n```cpp\n    const int size = 5;  \n    int values[size];\n```\n\n在代码的其他地方，当您访问`values`数组时，您可以使用`size`常量来确保您不会访问超过数组末尾的项目。由于`size`变量只在一个地方声明，如果您需要在稍后阶段更改数组的大小，您只有一个地方可以进行更改。`const`关键字也可用于指针和引用(参见[第 2 章](02.html)、*处理内存、数组和指针*)以及对象(参见[第 4 章](04.html)、*类*)；通常，你会看到它用在函数的参数上(见[第三章](03.html)、*使用函数*)。这用于让编译器帮助确保指针、引用和对象按照您的意图得到适当的使用。\n\n# 使用常量表达式\n\nC++ 11 引入了一个名为`constexpr`的关键词。这适用于表达式，并指示应在编译类型而不是运行时计算表达式:\n\n```cpp\n    constexpr double pi = 3.1415; \n    constexpr double twopi = 2 * pi;\n```\n\n这类似于初始化用`const`关键字声明的常数。但是，`constexpr`关键字也可以应用于返回可以在编译时计算的值的函数，因此这允许编译器优化代码:\n\n```cpp\n    constexpr int triang(int i) \n    { \n       return (i == 0) ? 0 : triang(i - 1) + i;\n    }\n```\n\n在本例中，函数`triang`递归计算三角数。代码使用条件运算符。在括号中，测试函数参数看它是否为零，如果是，函数返回零，实际上结束了递归并将函数返回给原始调用方。如果参数不为零，则返回值为参数之和，用参数调用的`triang`的返回值递减。\n\n当在代码中使用文字调用该函数时，可以在编译时进行计算。`constexpr`是对编译器的指示，检查函数的使用情况，看它能否在编译时确定参数。如果是这种情况，编译器可以评估返回值，并比在运行时调用函数更有效地生成代码。如果编译器在编译时无法确定参数，该函数将被调用为**正常**。标有`constexpr`关键字的函数必须只有一个表达式(因此在`triang`函数中使用了条件运算符`?:`)。\n\n# 使用枚举\n\n提供常量的最后一种方法是使用`enum`变量。实际上，`enum`是一组命名常数，这意味着您可以使用`enum`作为函数的参数。例如:\n\n```cpp\n    enum suits {clubs, diamonds, hearts, spades};\n```\n\n这定义了一个名为`suits`的枚举，为一副牌中的花色命名值。枚举是一个整数类型，默认情况下编译器会采用`int`，但是您可以通过在声明中指定整数类型来更改它。由于卡牌套装只有四种可能的值，所以使用`int`(通常是`4`字节)是浪费内存，我们可以使用`char`(单个字节):\n\n```cpp\n    enum suits : char {clubs, diamonds, hearts, spades};\n```\n\n当使用枚举值时，可以只使用名称；但是，通常用枚举的名称来限定它的范围，这使得代码更易读:\n\n```cpp\n    suits card1 = diamonds; \n    suits card2 = suits::diamonds;\n```\n\n这两种形式都是允许的，但后者更明确地表明该值是从枚举中获取的。要强制开发者指定范围，可以应用关键字`class`:\n\n```cpp\n    enum class suits : char {clubs, diamonds, hearts, spades};\n```\n\n有了这个定义和前面的代码，声明`card2`的行将编译，但是声明`card1`的行将不编译。使用限定作用域的`enum`，编译器将枚举视为新类型，并且没有从新类型到整数变量的内置转换。例如:\n\n```cpp\n    suits card = suits::diamonds; \n    char c = card + 10; // errors C2784 and C2676\n```\n\n`enum`类型是基于`char`的，但是当您将`suits`变量定义为作用域时(使用`class`，第二行将不会编译。如果枚举被定义为不在范围内(没有`class`)，那么在枚举值和`char`之间有一个内置的转换。\n\n默认情况下，编译器将为第一个枚举数赋值 0，然后为后续枚举数递增该值。因此`suits::diamonds`的值为 1，因为它是`suits`中的第二个值。您可以自己赋值:\n\n```cpp\n    enum ports {ftp=21, ssh, telnet, smtp=25, http=80};\n```\n\n在这种情况下，`ports::ftp`的值为 21，`ports::ssh`的值为 22 (21 递增)，`ports::telnet`为 22，`ports::smtp`为 25，`ports::http`为 80。\n\nOften the point of enumerations is to provide named symbols within your code and their values are unimportant. Does it matter what value is assigned to `suits::hearts`? The intention is usually to ensure that it is different from the other values. In other cases, the values are important because they are a way to provide values to other functions.\n\n枚举在`switch`语句中很有用(见后面)，因为命名值比只使用整数更清楚。您也可以将枚举用作函数的参数，从而限制通过该参数传递的值:\n\n```cpp\n    void stack(suits card) \n    { \n        // we know that card is only one of four values \n    }\n```\n\n# 声明指针\n\n由于我们讨论的是变量的使用，所以解释用于定义指针和数组的语法是值得的，因为存在一些潜在的陷阱。[第 2 章](02.html)、*使用内存、数组和指针*对此进行了更详细的介绍，因此我们将只介绍语法，以便您熟悉它。\n\n在 C++ 中，您将使用类型化指针来访问内存。类型指示内存中指向的数据的类型。因此，如果指针是一个(`4`字节)整数指针，它将指向四个可以用作整数的字节。如果整数指针递增，那么它将指向接下来的四个字节，可以用作整数。\n\nDon't worry if you find pointers confusing at this point. [Chapter 2](02.html), *Working with Memory, Arrays, and Pointers*, will explain this in more detail. The purpose of introducing pointers at this time is to make you aware of the syntax.\n\n在 C++ 中，指针是用`*`符号声明的，您可以用`&`运算符访问内存地址:\n\n```cpp\n    int *p; \n    int i = 42; \n    p = &i;\n```\n\n第一行声明了一个变量`p`，它将被用来保存一个整数的内存地址。第二行声明一个整数并给它赋值。第三行给指针`p`赋值，作为刚刚声明的整数变量的地址。需要强调的是，`p` *的值不是*`42`；这将是一个存储`42`值的存储地址。\n\n注意声明如何在变量名上有`*`。这是常见的惯例。原因是如果在一条语句中声明几个变量，`*`只适用于立即变量。例如:\n\n```cpp\n    int* p1, p2;\n```\n\n最初，这看起来像是在声明两个整数指针。然而，这条线并没有做到这一点；它只声明了一个指向整数`p1`的指针。第二个变量是一个名为`p2`的整数。前一行相当于下面一行:\n\n```cpp\n    int *p1;  \n    int p2;\n```\n\n如果您希望在一个语句中声明两个整数，那么您应该这样做:\n\n```cpp\n    int *p1, *p2;\n```\n\n# 使用命名空间\n\n名称空间为您提供了一种模块化代码的机制。命名空间允许您用一个唯一的名称来标记您的类型、函数和变量，这样，使用范围解析操作符，您可以给出一个*完全限定名*。这样做的好处是，你确切地知道哪个项目会被调用。缺点是使用完全限定名实际上是关闭了 C++ 的*参数依赖查找*机制，该机制用于重载函数，编译器将根据传递给函数的参数选择最适合的函数。\n\n定义命名空间很简单:用`namespace`关键字和给它起的名字来修饰类型、函数和全局变量。在以下示例中，`utilities`命名空间中定义了两个函数:\n\n```cpp\n    namespace utilities \n    { \n        bool poll_data() \n        { \n            // code that returns a bool \n        } \n        int get_data() \n        { \n            // code that returns an integer \n        } \n    }\n```\n\nDo not use semicolon after the closing bracket.\n\n现在，当您使用这些符号时，您需要用命名空间限定名称:\n\n```cpp\n    if (utilities::poll_data()) \n    { \n        int i = utilities::get_data(); \n        // use i here... \n    }\n```\n\n命名空间声明可能只声明函数，在这种情况下，实际的函数必须在其他地方定义，并且您需要使用限定名:\n\n```cpp\n    namespace utilities \n    { \n        // declare the functions \n        bool poll_data(); \n        int get_data(); \n    } \n\n    //define the functions \n    bool utilities::poll_data() \n    { \n        // code that returns a bool \n    } \n\n    int utilities::get_data() \n    { \n       // code that returns an integer \n    }\n```\n\n名称空间的一个用途是对代码进行版本化。您的代码的第一个版本可能有一个不在您的功能规范中的副作用，在技术上是一个错误，但是一些调用方会使用它并依赖它。当您更新代码以修复错误时，您可以决定允许调用方选择使用旧版本，这样他们的代码就不会中断。您可以通过命名空间来实现这一点:\n\n```cpp\n    namespace utilities \n    { \n        bool poll_data(); \n        int get_data(); \n\n        namespace V2 \n        { \n            bool poll_data(); \n            int get_data(); \n            int new_feature(); \n        } \n    }\n```\n\n现在，想要特定版本的调用者可以调用完全限定名，例如，调用者可以使用`utilities::V2::poll_data`来使用较新的版本，使用`utilities::poll_data`来使用较旧的版本。当特定命名空间中的项调用同一命名空间中的项时，它不必使用限定名。所以，如果`new_feature`函数调用`get_data`，被调用的将是`utilities::V2::get_data`。需要注意的是，要声明嵌套的名称空间，必须手动进行嵌套(如下所示)；您不能简单地声明一个名为`utilities::V2`的名称空间。\n\n前面的例子是这样写的，代码的第一个版本将使用名称空间`utilities`调用它。C++ 11 提供了一种称为**内联**命名空间的工具，允许您定义嵌套的命名空间，但允许编译器在执行依赖于参数的查找时将这些项视为在父命名空间中:\n\n```cpp\n    namespace utilities \n    { \n        inline namespace V1 \n        { \n            bool poll_data(); \n            int get_data(); \n        } \n\n        namespace V2 \n        { \n            bool poll_data(); \n            int get_data(); \n            int new_feature(); \n        } \n    }\n```\n\n现在要调用第一版`get_data`，可以用`utilities::get_data`或者`utilities::V1::get_data`。\n\n完全限定的名称会使代码难以阅读，尤其是如果您的代码只使用一个命名空间。为了帮助你，你有几个选择。您可以放置一个`using`语句来指示在指定命名空间中声明的符号可以在没有完全限定名的情况下使用:\n\n```cpp\n    using namespace utilities; \n    int i = get_data(); \n    int j = V2::get_data();\n```\n\n您仍然可以使用完全限定的名称，但是这个语句允许您放宽要求。请注意，嵌套的名称空间是名称空间的成员，因此前面的`using`语句意味着您可以使用`utilities::V2::get_data`或`V2::get_data`调用第二个版本的`get_data`。如果你用了不合格的名字，那就意味着你会叫`utilities::get_data`。\n\n一个命名空间可以包含许多项，您可能会决定只使用其中的几项来放宽对完全限定名的使用。为此，使用`using`并给出项目名称:\n\n```cpp\n    using std::cout; \n    using std::endl; \n    cout << \"Hello, World!\" << endl;\n```\n\n该代码表示，每当使用`cout`时，都是指`std::cout`。您可以在函数中使用`using`，也可以将其作为文件范围，并使意图对文件是全局的。\n\n您不必在一个地方声明一个名称空间，您可以在几个文件中声明它。以下内容可能位于与先前`utilities`声明不同的文件中:\n\n```cpp\n    namespace utilities \n    { \n        namespace V2 \n        { \n            void print_data(); \n        } \n    }\n```\n\n`print_data`函数仍然是`utilities::V2`命名空间的一部分。\n\n您也可以在一个名称空间中放置一个`#include`，在这种情况下，头文件中声明的项目现在将成为名称空间的一部分。前缀为`c`(例如`cmath`、`cstdlib`和`ctime`)的标准库头文件通过在`std`命名空间中包含适当的 C 头文件来访问 C 运行时函数。\n\n命名空间的最大优点是能够用可能很常见的名称来定义您的项，但是对于不知道命名空间名称的其他代码来说，这些名称是隐藏的。命名空间意味着这些项仍然可以通过完全限定名在代码中使用。但是，只有当您使用唯一的名称空间名称时，这才有效，并且名称空间名称越长，它就越可能是唯一的。Java 开发人员经常使用 URI 命名他们的类，您可以决定做同样的事情:\n\n```cpp\n    namespace com_packtpub_richard_grimes \n    { \n        int get_data(); \n    }\n```\n\n问题是完全限定名变得相当长:\n\n```cpp\n    int i = com_packtpub_richard_grimes::get_data();\n```\n\n您可以使用别名来解决这个问题:\n\n```cpp\n    namespace packtRG = com_packtpub_richard_grimes; \n    int i = packtRG::get_data();\n```\n\nC++ 允许你定义一个没有名字的名字空间，一个**匿名的**名字空间。如前所述，名称空间允许您防止在几个文件中定义的代码之间的名称冲突。如果您打算只在一个文件中使用这样的名称，您可以定义一个唯一的名称空间名称。但是，如果您必须为几个文件执行此操作，这可能会很乏味。一个没有名字的命名空间有一个特殊的含义，它有**内部链接**，即项目只能在当前翻译单元、当前文件中使用，不能在其他任何文件中使用。\n\n未在命名空间中声明的代码将成为`global`命名空间的成员。您可以在没有名称空间名称的情况下调用代码，但是您可能希望使用没有名称空间名称的范围解析运算符来明确指示该项在`global`名称空间中:\n\n```cpp\n    int version = 42; \n\n    void print_version() \n    { \n        std::cout << \"Version = \" << ::version << std::endl; \n    }\n```\n\n# 变量的 C++ 作用域\n\n编译器会将你的源文件编译成单独的项目，称为**翻译单元**。编译器将确定您声明的对象和变量以及您定义的类型和函数，一旦声明，您就可以在声明范围内的后续代码中使用这些对象和变量。从最广泛的角度来看，您可以通过在项目中所有源文件都将使用的头文件中声明一个项来声明全局范围内的项。如果不使用命名空间，那么使用这样的全局变量将它们命名为全局命名空间的一部分通常是明智的:\n\n```cpp\n    // in version.h \n    extern int version; \n\n    // in version.cpp \n    #include \"version.h\"  \n    version = 17; \n\n    // print.cpp \n    #include \"version.h\" \n    void print_version() \n    { \n        std::cout << \"Version = \" << ::version << std::endl; \n    }\n```\n\n这段代码有两个源文件(`version.cpp`和`print.cpp`)的 C++ 和两个源文件都包含的头文件(`version.h`)。头文件声明全局变量`version`，两个源文件都可以使用；它声明变量，但不定义它。实际变量在`version.cpp`中定义和初始化；编译器将在这里为变量分配内存。表头声明上使用的`extern`关键字向编译器表明`version`有**外部链接**，即该名称在定义变量以外的文件中可见。`version`变量在`print.cpp`源文件中使用。在该文件中，使用的范围解析运算符(`::`)没有名称空间名称，因此指示变量`version`在全局名称空间中。\n\n您也可以声明仅在当前翻译单元中使用的项目，方法是在使用之前在源文件中声明它们(通常在文件的顶部)。这产生了一定程度的模块化，并允许您对其他源文件中的代码隐藏实现细节。例如:\n\n```cpp\n    // in print.h \n    void usage(); \n\n    // print.cpp \n    #include \"version.h\" \n    std::string app_name = \"My Utility\"; \n    void print_version() \n    { \n       std::cout << \"Version = \" << ::version << std::endl; \n    } \n\n    void usage() \n    { \n       std::cout << app_name << \" \"; \n       print_version(); \n    }\n```\n\n`print.h`头包含文件`print.cpp`中代码的接口。只有那些在头文件中声明的函数可以被其他源文件调用。调用者不需要知道`usage`函数的实现，正如您在这里看到的，它是使用对一个名为`print_version`的函数的调用来实现的，该函数只对`print.cpp`中的代码可用。变量`app_name`是在文件范围内声明的，因此它只能由`print.cpp`中的代码访问。\n\n如果另一个源文件在文件范围内声明了一个变量，叫做`app_name`，也是`std::string`，文件会编译，但是链接器在尝试链接目标文件时会抱怨。原因是链接器会看到在两个地方定义了同一个变量，并且不知道使用哪个变量。\n\n函数也定义了一个范围；函数中定义的变量只能通过该名称访问。函数的参数也作为变量包含在函数中，因此在声明其他变量时，必须使用不同的名称。如果某个参数没有标记为`const`，则可以在函数中更改该参数的值。\n\n只要在使用变量之前声明变量，就可以在函数中的任何地方声明变量。花括号(`{}`)用于定义代码块，同时也定义局部范围；如果您在代码块中声明了一个变量，那么您只能在那里使用它。这意味着您可以在代码块之外声明同名的变量，编译器将使用最接近它被访问的范围的变量。\n\n在结束本节之前，重要的是要提到 C++ **存储类**的一个方面。在函数中声明的变量意味着编译器将在为该函数创建的堆栈框架上为该变量分配内存。当函数完成时，堆栈框架被拆除，内存被回收。这意味着，在函数返回后，任何局部变量中的值都会丢失；当再次调用该函数时，变量被重新创建并再次初始化。\n\nC++ 提供了`static`关键字来改变这种行为。`static`关键字意味着程序启动时分配变量，就像在全局范围声明的变量一样。将`static`应用于函数中声明的变量意味着该变量具有内部链接，即编译器将对该变量的访问限制在该函数中:\n\n```cpp\n    int inc(int i) \n    { \n        static int value; \n        value += i; \n        return value; \n    } \n\n    int main() \n    { \n        std::cout << inc(10) << std::endl; \n        std::cout << inc(5) << std::endl; \n    }\n```\n\n默认情况下，编译器会将一个静态变量初始化为`0`，但您可以提供一个初始化值，这将在首次分配变量时使用。当该程序启动时，在调用`main`函数之前，`value`变量将被初始化为`0`。第一次调用`inc`函数时，`value`变量增加到 10，由函数返回并打印到控制台。当`inc`功能返回时，`value`变量被保留，因此当再次调用`inc`功能时，`value`变量增加`5`至`15`值。\n\n# 使用运算符\n\n运算符用于根据一个或多个操作数计算值。下表以相等的*优先级*对所有运算符进行分组，并列出它们的*关联性*。表中的值越高，运算符在表达式中的执行优先级就越高。如果表达式中有几个运算符，编译器将在低优先级运算符之前执行高优先级运算符。如果表达式包含同等优先级的运算符，则编译器将使用结合律来决定操作数是与其左边的运算符还是右边的运算符分组。\n\nThere are some ambiguities in this table. A pair of parentheses can mean a function call or a cast and in the table these are listed as `function()` and `cast()`; in your code you will simply use `()`. The `+` and `-` symbols are either used to indicate sign (unary plus and unary minus, given in the table as `+x` and `-x`), or addition and subtraction (given in the table as `+` and `-`). The `&` symbol means either \"take the address of\" (listed in the table as `&x`) or bitwise `AND` (listed in the table as `&`). Finally, the postfix increment and decrement operators (listed in the table as `x++ ` and `x--`) have a higher precedence than the prefix equivalents (listed as `++ x` and `--x`).\n\n| **优先和结合** | **操作员** |\n| **1** :无关联性 | `::` |\n| **2** :从左到右的关联性 | `.`或`-> [] function() {} x++ x-- typeid const_cast dynamic_cast reinterpret_cast static_cast` |\n| **3** :从右向左关联性 | `sizeof ++ x --x ~ ! -x +x &x * new delete cast()` |\n| **4** :从左到右的关联性 | `.*`或`->*` |\n| **5** :从左到右的关联性 | `* / %` |\n| **6** :从左到右的关联性 | `+ -` |\n| **7** :从左到右的关联性 | `<< >>` |\n| **8** :从左到右的关联性 | `< > <= >=` |\n| **9** :从左到右的关联性 | `== !=` |\n| **10** :从左到右的关联性 | `&` |\n| **11** :从左到右的关联性 | `^` |\n| **12** :从左到右关联性 | `&#124;` |\n| **13** :从左到右关联性 | `&&` |\n| **14** :从左到右关联性 | `&#124;&#124;` |\n| **15** :从右向左关联性 | `? :` |\n| **16** :从右向左关联性 | `= *= /= %= += -= <<= >>= &= &#124;= ^=` |\n| **17** :从右向左关联性 | `throw` |\n| **18** :从左到右关联性 | `,` |\n\n例如，看看下面的代码:\n\n```cpp\n    int a = b + c * d;\n```\n\n这被解释为先执行乘法，然后执行加法。编写相同代码的更清晰的方法是:\n\n```cpp\n    int a = b + (c * d);\n```\n\n原因是`*`的优先级比`+`高，所以先进行乘法，再进行加法:\n\n```cpp\n    int a = b + c + d;\n```\n\n在这种情况下，`+`运算符具有相同的优先级，高于赋值的优先级。由于`+`具有从左到右的关联性，该语句解释如下:\n\n```cpp\n    int a = ((b + c) + d);\n```\n\n也就是第一个动作是`b`和`c`的相加，结果加到`d`上，就是这个结果用来赋值`a`。这看起来并不重要，但请记住，加法可以在函数调用之间进行(函数调用的优先级高于`+`):\n\n```cpp\n    int a = b() + c() + d();\n```\n\n这意味着这三个函数按照`b`、`c`、`d`的顺序调用，然后根据从左到右的关联性对它们的返回值求和。这可能很重要，因为`d`可能依赖于由其他两个函数改变的全局数据。\n\n如果通过用括号将表达式分组来显式指定优先级，会使代码更易读、更容易理解。写`b + (c * d)`可以立即清楚哪个表达式先执行，而`b + c * d`意味着你必须知道每个运算符的优先级。\n\n内置运算符是重载的，也就是说，无论操作数使用哪种内置类型，都使用相同的语法。操作数必须是相同的类型；如果使用不同的类型，编译器将执行一些默认转换，但在其他情况下(特别是在对不同大小的类型进行操作时)，您将必须执行强制转换，以明确表明您的意思。\n\n# 探索内置运算符\n\nC++ 自带广泛的内置运算符；大多数是算术或逻辑运算符，这将在本节中介绍。内存操作符将包含在[第 2 章](02.html)、*使用内存、数组和指针、*以及 [第 4 章](04.html)、*类*中的对象相关操作符中。\n\n# 算术运算符\n\n算术运算符`+`、`-`、`/`、`*`和`%`除了除法和模数运算符之外，几乎不需要其他解释。所有这些运算符都作用于整数和实数类型，除了`%`，它只能用于整数类型。如果您混合了这些类型(比如，将一个整数加到一个浮点数上)，那么编译器将执行自动转换。除法运算符`/`的行为与您对浮点变量的预期一样:它产生两个操作数的除法结果。当你在两个整数之间执行除法`a / b`时，结果是被除数(`a`)中除数(`b`)的整数。除法的余数由模数`%`获得。因此，对于任何整数`b`(除了零)，可以说，整数`a`可以表示如下:\n\n```cpp\n    (a / b) * b + (a % b)\n```\n\n请注意，模数运算符只能用于整数。如果你想得到浮点除法的余数，使用标准函数`std:;remainder`。\n\n使用整数除法时要小心，因为小数部分会被丢弃。如果您需要小数部分，那么您可能需要显式地将数字转换为实数。例如:\n\n```cpp\n    int height = 480; \n    int width = 640; \n    float aspect_ratio = width / height;\n```\n\n这给出了`1`的纵横比，而它应该是`1.3333`(或`4 : 3`)。为了确保执行浮点除法，而不是整数除法，可以将被除数或除数转换为浮点数。\n\n# 递增和递减运算符\n\n这些运算符有两个版本，前缀和后缀。顾名思义，前缀表示运算符放在操作数的左边(例如`++ i`)，后缀运算符放在右边(`i++ `)。`++ `运算符将增加操作数，`--`运算符将减少操作数。前缀运算符表示“在操作后返回数值*，后缀运算符表示“在*操作前返回数值*因此，下面的代码将增加一个变量，并使用它来分配另一个变量:*\n\n```cpp\n    a = ++ b;\n```\n\n这里，使用前缀运算符，因此变量`b`增加，并且变量`a`在`b`增加后被赋值。另一种表达方式是:\n\n```cpp\n    a = (b = b + 1);\n```\n\n下面的代码使用后缀运算符赋值:\n\n```cpp\n    a = b++ ;\n```\n\n这意味着变量`b`增加，但是变量`a`在`b`增加之前被赋值。另一种表达方式是:\n\n```cpp\n    int t; \n    a = (t = b, b = b + 1, t);\n```\n\nNote that this statement uses the comma operator, so `a` is assigned to the temporary variable `t` in the right-most expression.\n\n递增和递减运算符可以应用于整数和浮点数。运算符也可以应用于指针，在指针中它们有特殊的含义。当您增加一个指针变量时，这意味着*将指针增加操作符*所指向的类型的大小。\n\n# 按位运算符\n\n整数可以看作是一系列的位，`0`或`1`。与其他操作数中相同位置的位相比，按位运算符对这些位起作用。有符号整数使用一位来表示符号，但是按位运算符作用于整数中的每一位，因此通常只对无符号整数使用它们才是明智的。在下文中，所有类型都标记为`unsigned`，因此它们被视为没有符号位。\n\n`&`运算符是按位“与”，这意味着左侧操作数中的每一位都与右侧操作数中相同位置的位进行比较。如果两者都是 1，则相同位置的结果位将是 1；否则，结果位为零:\n\n```cpp\n    unsigned int a = 0x0a0a; // this is the binary 0000101000001010 \n    unsigned int b = 0x00ff; // this is the binary 0000000000001111 \n    unsigned int c = a & b;  // this is the binary 0000000000001010 \n    std::cout << std::hex << std::showbase << c << std::endl;\n```\n\n在本例中，对`0x00ff`使用按位`&`与提供屏蔽除最低字节之外的所有字节的屏蔽具有相同的效果。\n\n如果同一位置的任一位或两位都为 1，则按位“或”运算符`|`将返回值 1，只有当两位都为 0 时，才返回值 0:\n\n```cpp\n    unsigned int a = 0x0a0a; // this is the binary 0000101000001010 \n    unsigned int b = 0x00ff; // this is the binary 0000000000001111 \n    unsigned int c = a & b;  // this is the binary 0000101000001111 \n    std::cout << std::hex << std::showbase << c << std::endl;\n```\n\n`&`运算符的一个用途是查找是否设置了特定的位(或特定的位集合):\n\n```cpp\n    unsigned int flags = 0x0a0a; // 0000101000001010 \n    unsigned int test = 0x00ff;  // 0000000000001111 \n\n    // 0000101000001111 is (flags & test) \n    if ((flags & test) == flags)  \n    { \n        // code for when all the flags bits are set in test \n    } \n    if ((flags & test) != 0) \n    { \n        // code for when some or all the flag bits are set in test  \n    }\n```\n\n`flags`变量有我们需要的位，`test`变量是我们正在检查的值。值`(flags & test)`将只有那些也在`flags`中设置的`test`变量中的位。因此，如果结果为非零，则表示`test`中至少有一位也设置在`flags`中；如果结果与`flags`变量完全相同，则`flags`中的所有位都设置在`test`中。\n\n异或运算符`^`用于测试位不同时；如果操作数中的位不同，则结果位为`1`，如果它们相同，则结果位为`0`。异或可用于翻转特定位:\n\n```cpp\n    int value = 0xf1; \n    int flags = 0x02; \n    int result = value ^ flags; // 0xf3 \n    std::cout << std::hex << result << std::endl;\n```\n\n最后的按位运算符是按位补码`~`。此运算符应用于单个整数操作数，并返回一个值，其中每个位都是操作数中相应位的补码；所以如果操作数位是 1，结果中的位是 0，如果操作数中的位是 0，结果中的位是 1。请注意，所有位都会被检查，因此您需要知道整数的大小。\n\n# 布尔运算符\n\n`==`操作者测试两个值是否完全相同。如果你测试两个整数，那么测试是显而易见的；比如`x`是 2，`y`是 3，那么`x == y`显然就是`false`。然而，即使你这样认为，两个实数也可能不一样:\n\n```cpp\n    double x = 1.000001 * 1000000000000; \n    double y = 1000001000000; \n    if (x == y) std::cout << \"numbers are the same\";\n```\n\n`double`类型是一个 8 字节的浮点类型，但这对于这里使用的精度来说是不够的；存储在`x`变量中的值是`1000000999999.9999`(到小数点后四位)。\n\n`!=`操作员测试两个值是否不正确。运算符`>`和`<`测试两个值，查看左侧操作数是否大于或小于右侧操作数，`>=`运算符测试左侧操作数是否大于或等于右侧操作数，`<=`运算符测试左侧操作数是否小于或等于右侧操作数。这些运算符可用于`if`语句，类似于前面示例中`==`的用法。使用运算符的表达式返回类型为`bool`的值，因此您可以使用它们为布尔变量赋值:\n\n```cpp\n    int x = 10; \n    int y = 11; \n    bool b = (x > y); \n    if (b) std::cout << \"numbers same\"; \n    else   std::cout << \"numbers not same\";\n```\n\n赋值运算符(`=`)比大于(`>=`)运算符具有更高的优先级，但是我们使用了圆括号来明确表示该值在用于赋值变量之前已经过测试。您可以使用`!`运算符来否定逻辑值。因此，使用之前获得的`b`的值，您可以编写以下内容:\n\n```cpp\n    if (!b) std::cout << \"numbers not same\"; \n    else    std::cout << \"numbers same\";\n```\n\n您可以使用`&&`(与)和`||`(或)运算符组合两个逻辑表达式。带有`&&`运算符的表达式只有在两个操作数都为`true`时才成立，而带有`||`运算符的表达式只有在其中一个或两个操作数都为`true`时才成立:\n\n```cpp\n    int x = 10, y = 10, z = 9; \n    if ((x == y) || (y < z)) \n        std::cout << \"one or both are true\";\n```\n\n这段代码涉及三个测试；第一个测试`x`和`y`变量是否具有相同的值，第二个测试变量`y`是否小于`z`，然后进行一个测试，看前两个测试中的一个或两个是否为`true`。\n\n在像这样的`||`表达式中，第一个操作数(`x==y`)是`true`，则总的逻辑表达式将是`true`，而不考虑右操作数的值(这里，`y < z`)。所以测试第二个表达式没有意义。相应地，在`&&`表达式中，如果第一个操作数是`false`，那么整个表达式必须是`false`，所以表达式的右边部分不需要测试。\n\n编译器将为您提供执行此*短路*的代码:\n\n```cpp\n    if ((x != 0) && (0.5 > 1/x))  \n    { \n        // reciprocal is less than 0.5 \n    }\n```\n\n该代码测试`x`的倒数是否小于 0.5(或者相反，`x`是否大于 2)。如果`x`变量的值为 0，那么测试`1/x`是错误的，但是在这种情况下，表达式将永远不会被执行，因为`&&`的左操作数是`false`。\n\n# 按位移位运算符\n\n按位移位运算符将左操作数整数中的位按指定方向移位右操作数中给定的指定位数。向左移动一位会将数字乘以 2，向右移动一位会除以 2。在下面，一个 2 字节的整数被移位:\n\n```cpp\n    unsigned short s1 = 0x0010; \n    unsigned short s2 = s1 << 8; \n    std::cout << std::hex << std::showbase; \n    std::cout << s2 << std::endl; \n    // 0x1000  \n    s2 = s2 << 3; \n    std::cout << s2 << std::endl; \n    // 0x8000\n```\n\n在本例中，`s1`变量设置了第五位(`0x0010`或 16)。`s2`变量有此值，左移 8 位，因此单个位被移至第 13 位，底部 8 位全部设置为 0 ( `0x10000`或 4，096)。这意味着`0x0010`已经乘以 2 <sup>8</sup> ，或者 256，得到`0x1000`。接下来，该值再左移 3 位，结果为`0x8000`；最高位被置位。\n\n运算符丢弃任何溢出的位，因此如果您设置了最高位并将整数左移一位，则最高位将被丢弃:\n\n```cpp\n    s2 = s2 << 1; \n    std::cout << s2 << std::endl; \n    // 0\n```\n\n最后左移一位会得到值 0。\n\n重要的是要记住，当与流一起使用时，运算符`<<`意味着*插入到流中*，当与整数一起使用时，它意味着*按位移位*。\n\n# 赋值运算符\n\n赋值运算符`=`将左边的左值(变量)和右边的右值(变量或表达式)的结果赋值:\n\n```cpp\n    int x = 10; \n    x = x + 10;\n```\n\n第一行声明一个整数并将其初始化为 10。第二行通过再加 10 来改变变量，所以现在变量`x`的值是 20。这是作业。C++ 允许您使用缩写语法根据变量值更改变量值。前面几行可以写成如下:\n\n```cpp\n    int x = 10; \n    x += 10;\n```\n\n像这样的递增运算符(以及递减运算符)可以应用于整数和浮点类型。如果运算符应用于指针，则操作数指示指针改变了多少个整项地址。例如，如果`int`是 4 个字节，并且您将`10`添加到`int`指针，则实际指针值将增加 40 (10 乘以 4 个字节)。\n\n除了增量(`+=`)和减量(`-=`)赋值，您还可以有乘法(`*=`)、除法(`/=`)和余数(`%=`)赋值。除了最后一个(`%=`)之外，所有这些都可以用于浮点类型和整数。余数赋值只能用于整数。\n\n还可以对整数执行按位赋值操作:左移位(`<<=`)、右移位(`>>=`)、按位“与”(`&=`)、按位“或”(`|=`)和按位“异或”(`^=`)。通常只有将这些应用于无符号整数才有意义。所以，乘以 8 可以通过这两条线来实现:\n\n```cpp\n    i *= 8; \n    i <<= 3;\n```\n\n# 控制执行流程\n\nC++ 提供了许多测试值和循环代码的方法。\n\n# 使用条件语句\n\n最常用的条件语句是`if`。最简单的形式是，`if`语句在一对括号中取一个逻辑表达式，紧接着是语句，如果条件为`true`，则执行该语句:\n\n```cpp\n    int i; \n    std::cin >> i; \n    if (i > 10) std::cout << \"much too high!\" << std::endl;\n```\n\n也可以使用`else`语句捕捉条件为`false`的场合:\n\n```cpp\n    int i; \n    std::cin >> i; \n    if (i > 10) std::cout << \"much too high!\" << std::endl; \n    else        std::cout << \"within range\" << std::endl;\n```\n\n如果要执行几条语句，可以用大括号(`{}`)定义一个代码块。\n\n条件是一个逻辑表达式，C++ 将从数字类型转换为`bool`，其中 0 是`false`，任何不为 0 的都是`true`。如果你不小心，这可能是一个错误的来源，不仅很难注意到，而且会有意想不到的副作用。考虑下面的代码，它要求从控制台输入，然后测试用户是否输入-1:\n\n```cpp\n    int i; \n    std::cin >> i; \n    if (i == -1) std::cout << \"typed -1\" << endl; \n    std::cout << \"i = \" << i << endl;\n```\n\n这是人为的，但是您可能在循环中要求值，然后对这些值执行操作，除非用户输入-1，此时循环结束。如果输入错误，可能会出现以下代码:\n\n```cpp\n    int i; \n    std::cin >> i; \n    if (i = -1) std::cout << \"typed -1\" << endl; \n    std::cout << \"i = \" << i << endl;\n```\n\n在这种情况下，使用赋值运算符(`=`)代替*等式*运算符(`==`)。只有一个字符的区别，但是这段代码仍然是正确的 C++ 并且编译器很乐意编译它。\n\n结果是，不管您在控制台上键入什么，变量`i`都被赋值为-1，并且由于-1 不是零，`if`语句中的条件是`true`，因此执行该语句的 true 子句。由于变量被赋给了-1，这可能会进一步改变代码中的逻辑。避免这个错误的方法是利用赋值中左边必须是左值的要求。按照以下步骤进行测试:\n\n```cpp\n    if (-1 == i) std::cout << \"typed -1\" << endl;\n```\n\n这里的逻辑表达式是`(-1 == i)`，由于`==`运算符是可交换的(操作数的顺序无关紧要；您得到了相同的结果)，这与您在前面的测试中预期的完全相同。但是，如果您输入了错误的运算符，您会得到以下结果:\n\n```cpp\n    if (-1 = i) std::cout << \"typed -1\" << endl;\n```\n\n在这种情况下，赋值在左侧有一个右值，这将导致编译器发出错误(在 Visual C++ 中这是`C2106 '=' : left operand must be l-value`)。\n\n允许在`if`语句中声明一个变量，变量的作用域在语句块中。例如，可以按如下方式调用返回整数的函数:\n\n```cpp\n    if (int i = getValue()) {    \n        // i != 0    // can use i here  \n    } else {    \n        // i == 0    // can use i here  \n    }\n```\n\n虽然这是完全合法的 C++，但有几个原因让你想要这么做。\n\n在某些情况下，可以使用条件运算符`?:`来代替`if`语句。运算符执行`?`运算符左侧的表达式，如果条件表达式为`true`，则执行`?`右侧的表达式。如果条件表达式为`false`，则执行`:`右侧的表达式。运算符执行的表达式提供条件运算符的返回值。\n\n例如，下面的代码确定了两个变量的最大值，`a`和`b`:\n\n```cpp\n    int max; \n    if (a > b) max = a; \n    else       max = b;\n```\n\n这可以用以下一句话来表达:\n\n```cpp\n    int max = (a > b) ? a : b;\n```\n\n主要选择代码中可读性最好的。显然，如果赋值表达式很大，最好在`if`语句中将它们拆分成行。但是，在其他语句中使用条件语句很有用。例如:\n\n```cpp\n    int number;  \n    std::cin  >> number; \n    std::cout << \"there \" \n              << ((number == 1) ? \"is \" : \"are \")  \n              << number << \" item\"            \n              << ((number == 1) ? \"\" : \"s\") \n              << std::endl;\n```\n\n该代码确定变量`number`是否为 1，如果是，则在控制台`there is 1 item`上打印。这是因为在这两种条件下，如果`number`变量的值为 1，则测试为`true`，并使用第一个表达式。请注意，整个运算符周围有一对括号。原因是流`<<`运算符重载，您希望编译器选择接受字符串的版本，这是运算符返回的类型，而不是表达式`(number == 1)`的类型`bool`。\n\n如果条件运算符返回的值是左值，则可以在赋值的左侧使用它。这意味着您可以编写以下相当奇怪的代码:\n\n```cpp\n    int i = 10, j = 0; \n    ((i < j) ? i : j) = 7; \n    // i is 10, j is 7 \n\n    i = 0, j = 10; \n    ((i < j) ? i : j) = 7; \n    // i is 7, j is 10\n```\n\n条件运算符检查`i`是否小于`j`，如果是，则为`i`赋值；否则，它会为`j`分配该值。这段代码很简洁，但缺乏可读性。在这种情况下，使用`if`语句要好得多。\n\n# 选择\n\n如果你想测试一个变量是否是几个值中的一个，使用多个`if`语句会变得很麻烦。C++ `switch`语句更好地实现了这个目的。基本语法如下所示:\n\n```cpp\n    int i; \n    std::cin >> i; \n    switch(i) \n    { \n        case 1:  \n            std::cout << \"one\" << std::endl; \n            break; \n        case 2:  \n            std::cout << \"two\" << std::endl; \n            break; \n        default: \n            std::cout << \"other\" << std::endl; \n    }\n```\n\n如果所选变量是指定值，每个`case`本质上是要运行的特定代码的标签。`default`条款适用于不存在`case`的价值观。您不必有`default`条款，这意味着您只针对特定情况进行测试。`default`条款可以用于最常见的情况(在这种情况下，案例过滤掉不太可能的值)，也可以用于例外值(在这种情况下，案例处理最可能的值)。\n\n一个`switch`语句只能测试整数类型(包括`enum`)，只能测试常量。`char`类型为整数，这意味着您可以在`case`项中使用字符，但只能使用单个字符；您不能使用字符串:\n\n```cpp\n    char c; \n    std::cin >> c; \n    switch(c) \n    { \n        case 'a':  \n            std::cout << \"character a\" << std::endl; \n            break; \n        case 'z':   \n            std::cout << \"character z\" << std::endl; \n            break; \n        default: \n            std::cout << \"other character\" << std::endl; \n    }\n```\n\n`break`语句表示为`case`执行的语句的结束。如果您没有指定，将通过执行*，以下`case`语句将被执行，即使它们是为不同的情况指定的:*\n\n```cpp\n    switch(i) \n    { \n        case 1:  \n            std::cout << \"one\" << std::endl; \n            // fall thru \n        case 2:  \n            std::cout << \"less than three\" << std::endl; \n            break; \n        case 3:  \n            std::cout << \"three\" << std::endl; \n            break; \n        case 4: \n            break; \n            default: \n            std::cout << \"other\" << std::endl; \n    }\n```\n\n这段代码显示了`break`语句的重要性。值 1 会将`one`和`less than three`打印到控制台，因为执行*会通过*到达前面的`case`，即使`case`是另一个值。\n\n对于不同的情况，通常会有不同的代码，所以您最常使用`break`来完成一个`case`。很容易误错过一个`break`，这会导致不寻常的行为。当故意遗漏`break`语句时，记录您的代码是一个很好的做法，这样您就知道如果遗漏了一个`break`，那很可能是一个错误。\n\n您可以为每个`case`提供零个或多个语句。如果有一个以上的语句，它们都是针对该特定情况执行的。如果您没有提供任何语句(如本例中的`case 4`)，则意味着不会执行任何语句，甚至不会执行`default`条款中的语句。\n\n`break`语句意味着*脱离这个代码块*，在循环语句`while`和`for`中也是如此。还有其他方法可以突破`switch`。A `case`可以调用`return`来完成`switch`声明的功能；它可以调用`goto`跳转到一个标签，也可以调用`throw`抛出一个异常，该异常将被`switch`之外的异常处理程序捕获，甚至在函数之外。\n\n到目前为止，这些案例是按数字顺序排列的。这不是一个要求，但它确实使代码更具可读性，显然，如果您想让*通过*`case`语句(如这里的`case 1`所示)，您应该注意`case`项的顺序。\n\n如果您需要在`case`处理程序中声明一个临时变量，那么您必须使用大括号定义一个代码块，这将使变量的范围只局限于该代码块。当然，您可以在任何`case`处理程序中使用在`switch`语句之外声明的任何变量。\n\n由于枚举常数是整数，您可以在`switch`语句中测试`enum`:\n\n```cpp\n    enum suits { clubs, diamonds, hearts, spades }; \n\n    void print_name(suits card) \n    { \n        switch(card) \n        { \n            case suits::clubs: \n                std::cout << \"card is a club\"; \n                break; \n            default: \n                std::cout << \"card is not a club\"; \n        } \n    }\n```\n\n这里的`enum`虽然没有限定作用域(既不是`enum class`也不是`enum struct`，但并不要求在`case`中指定值的作用域，反而让代码更清楚常量指的是什么。\n\n# 环\n\n大多数程序需要循环一些代码。C++ 提供了几种方法来实现这一点，要么用索引值迭代，要么测试逻辑条件。\n\n# 迭代循环\n\n`for`语句有两个版本，迭代和基于范围。后者是在 C++ 11 中引入的。迭代版本具有以下格式:\n\n```cpp\n    for (init_expression; condition; loop_expression) \n        loop_statement;\n```\n\n您可以提供一个或多个循环语句，对于多个语句，您应该使用大括号提供一个代码块。循环表达式可以满足循环的目的，在这种情况下，您可能不希望执行循环语句；这里，您使用了 null 语句，`;`表示*什么也不做*。\n\n括号内是三个用分号分隔的表达式。第一个表达式允许您声明和初始化循环变量。该变量的作用域是`for`语句，因此只能在`for`表达式或后面的循环语句中使用。如果需要多个循环变量，可以使用逗号运算符在此表达式中声明它们。\n\n当条件表达式为`true`时，`for`语句将循环；因此，如果使用循环变量，可以使用这个表达式来检查循环变量的值。第三个表达式在循环结束时调用，在循环语句被调用之后；接下来，调用条件表达式来查看循环是否应该继续。这个最终表达式通常用于更新循环变量值。例如:\n\n```cpp\n    for (int i = 0; i < 10; ++ i)   \n    { \n        std::cout << i; \n    }\n```\n\n在该代码中，循环变量为`i`，并初始化为零。接下来，检查条件，由于`i`将小于 10，将执行语句(将值打印到控制台)。下一个动作是循环表达式；调用`++ i`，递增循环变量`i`，然后检查条件，依此类推。由于条件是`i < 10`，这意味着此循环将运行十次，值为 0 到 9 之间的`i`(因此您将在控制台上看到 0123456789)。\n\n循环表达式可以是您喜欢的任何表达式，但通常它会增加或减少一个值。您不必将循环变量值更改 1；例如，您可以使用`i -= 5`作为循环表达式，在每个循环上将变量减少 5。循环变量可以是您喜欢的任何类型；它不必是整数，甚至不必是数字(例如，它可以是指针，或者是使用标准库容器的 [第 5 章](05.html)、*中描述的**迭代器对象**，并且条件和循环表达式不必使用循环变量。事实上，您根本不需要声明循环变量！*\n\n如果不提供循环条件，则循环将是无限的，除非您在循环中提供检查:\n\n```cpp\nfor (int i = 0; ; ++ i)  \n{ \n   std::cout << i << std::endl; \n   if (i == 10) break; \n}\n```\n\n这使用了之前与`switch`语句一起引入的`break`语句。表示执行退出`for`循环，也可以使用`return`、`goto`或`throw`。你很少会看到使用`goto`结束的语句；但是，您可能会看到以下内容:\n\n```cpp\nfor (;;)  \n{ \n   // code \n}\n```\n\n在这种情况下，没有循环变量，没有循环表达式，也没有条件。这是一个永久的循环，循环中的代码决定了循环何时结束。\n\n`for`语句中的第三个表达式，循环表达式，可以是任何你喜欢的；唯一的属性是它在循环结束时执行。您可以选择更改该表达式中的另一个变量，或者您甚至可以提供由逗号运算符分隔的几个表达式。例如，如果您有两个函数，一个名为`poll_data`的函数返回`true`如果有更多的数据可用，而`false`当没有更多的数据可用，以及一个名为`get_data`的函数返回下一个可用的数据项，您可以使用`for`如下(记住；这是一个做作的例子，说明一点):\n\n```cpp\nfor (int i = -1; poll_data(); i = get_data()) \n{ \n   if (i != -1) std::cout << i << std::endl; \n}\n```\n\n当`poll_data`返回一个`false`值时，循环将结束。需要`if`语句，因为第一次调用循环时，`get_data`尚未被调用。更好的版本如下:\n\n```cpp\nfor (; poll_data() ;) \n{ \n   int i = get_data();  \n   std::cout << i << std::endl; \n}\n```\n\n下一节请记住这个例子。\n\n在`for`循环中还有一个关键词可以使用。在许多情况下，您的`for`循环将有许多行代码，并且在某个时候，您可能会决定当前循环已经完成，并且您想要开始下一个循环(或者，更具体地说，执行循环表达式，然后测试条件)。为此，你可以致电`continue`:\n\n```cpp\nfor (float divisor = 0.f; divisor < 10.f; ++ divisor)  \n{ \n   std::cout << divisor; \n   if (divisor == 0)  \n   {  \n      std::cout << std::endl; \n      continue; \n   } \n   std::cout << \" \" << (1 / divisor) << std::endl; \n}\n```\n\n在这段代码中，我们打印了数字 0 到 9 的倒数(`0.f`是一个 4 字节的浮点文字)。`for`循环的第一行打印循环变量，下一行检查变量是否为零。如果是，则打印新行并继续，即不执行`for`循环中的最后一行。原因是最后一行打印倒数，用零除任何数字都是错误的。\n\nC++ 11 引入了另一种使用`for`循环的方式，该循环旨在与容器一起使用。C++ 标准库包含用于容器类的**模板**。这些类包含对象的集合，并以标准方式提供对这些项的访问。标准方法是使用**迭代器**对象迭代集合。关于如何做到这一点的更多细节将在 [第 5 章](05.html)*使用标准库容器*中给出；语法要求理解指针和迭代器，所以我们在这里不涉及它们。基于范围的`for`循环提供了一种简单的机制来访问容器中的项目，而无需显式使用迭代器。\n\n语法很简单:\n\n```cpp\nfor (for_declaration : expression) loop_statement;\n```\n\n首先要指出的是，表达式只有两个，用冒号(`:`)隔开。第一个表达式用于声明循环变量，该变量属于正在迭代的集合中的项的类型。第二个表达式提供对集合的访问。\n\nIn C++ terms, the collections that can be used are those that define a `begin` and `end` function that gives access to iterators, and also to stack-based arrays (that the compiler knows the size of).\n\n标准库定义了一个名为`vector`的容器对象。`vector`模板是一个包含尖括号(`<>`)中指定类型项目的类；在下面的代码中，`vector`以 C++ 11 中新的特殊方式初始化，称为**列表初始化**。该语法允许您在花括号之间的列表中指定向量的初始值。下面的代码创建并初始化一个`vector`，然后使用一个迭代`for`循环打印出所有的值:\n\n```cpp\nusing namespace std; \nvector<string> beatles = { \"John\", \"Paul\", \"George\", \"Ringo\" }; \n\nfor (int i = 0; i < beatles.size(); ++ i)  \n{ \n   cout << beatles.at(i) << endl; \n}\n```\n\nHere a `using` statement is used so that the classes `vector` and `string` do not have to be used with fully qualified names.\n\n`vector`类有一个名为`size`的成员函数(通过`.`操作符调用，意思是“在这个对象上调用这个函数”)，它返回`vector`中的项数。使用传递项目索引的`at`函数访问每个项目。这段代码的一个大问题是它使用随机访问，也就是说，它使用索引访问每个项目。这是`vector`的一个属性，但是其他标准库容器类型没有随机访问。以下使用基于范围的`for`:\n\n```cpp\nvector<string> beatles = { \"John\", \"Paul\", \"George\", \"Ringo\" }; \n\nfor (string musician : beatles)  \n{ \n   cout << musician << endl; \n}\n```\n\n此语法适用于任何标准容器类型和堆栈上分配的数组:\n\n```cpp\nint birth_years[] = { 1940, 1942, 1943, 1940 }; \n\nfor (int birth_year : birth_years)  \n{ \n   cout << birth_year << endl; \n}\n```\n\n在这种情况下，编译器知道数组的大小(因为编译器已经分配了数组)，因此它可以确定范围。基于范围的`for`循环将遍历容器中的所有项目，但是与上一版本一样，您可以使用`break`、`return`、`throw`或`goto`离开`for`循环，并且您可以使用`continue`语句指示下一个循环应该被执行。\n\n# 条件循环\n\n在前一节中，我们给出了一个人为的例子，其中`for`循环中的条件轮询数据:\n\n```cpp\nfor (; poll_data() ;) \n{ \n   int i = get_data();  \n   std::cout << i << std::endl; \n}\n```\n\n在本例中，条件中没有使用循环变量。这是`while`条件循环的候选:\n\n```cpp\nwhile (poll_data()) \n{ \n   int i = get_data();  \n   std::cout << i << std::endl; \n}\n```\n\n该语句将继续循环，直到表达式(本例中为`poll_data`)的值为`false`。与`for`一样，您可以使用`break`、`return`、`throw`或`goto`退出`while`循环，并且您可以使用`continue`语句指示下一个循环应该被执行。\n\n第一次调用`while`语句，在执行循环之前测试条件；在某些情况下，您可能希望循环至少执行一次，然后测试条件(很可能取决于循环中的操作)以查看循环是否应该重复。方法是使用`do-while`循环:\n\n```cpp\nint i = 5; \ndo \n{ \n   std::cout << i-- << std::endl; \n} while (i > 0);\n```\n\n注意`while`子句后的分号。这是必需的。\n\n该循环将以相反的顺序打印 5 比 1。原因是循环从`i`初始化为 5 开始。循环中的语句通过后缀运算符递减变量，这意味着递减之前的值被传递给流。循环结束时，`while`子句测试变量是否大于零。如果这个测试是`true`，循环重复。当调用循环时`i`被赋值为 1，1 的值被打印到控制台，变量减为零，`while`子句将测试表达式`false`，循环将结束。\n\n两种循环的区别是在`while`循环中执行循环之前先测试条件，所以可能不执行循环。在`do-while`循环中，条件在循环之后被调用，这意味着，在`do-while`循环中，循环语句总是被调用至少一次。\n\n# 跳跃的\n\nC++ 支持跳转，在大多数情况下，有更好的分支代码的方法；然而，为了完整起见，我们将在这里介绍该机制。跳转有两个部分:要跳转到的标记语句和`goto`语句。标签与变量具有相同的命名规则；它被声明为以冒号为后缀，并且必须在语句之前。使用标签的名称调用`goto`语句:\n\n```cpp\n    int main() \n    { \n        for (int i = 0; i < 10; ++ i) \n        { \n            std::cout << i << std::endl; \n            if (i == 5) goto end; \n        } \n\n    end:\n        std::cout << \"end\"; \n    }\n```\n\n标签必须与调用`goto`功能相同。\n\n跳转很少使用，因为它们鼓励您编写非结构化代码。但是，如果您有一个带有高度嵌套循环或`if`语句的例程，那么使用`goto`跳转来清理代码可能会更有意义，可读性也更好。\n\n# 使用 C++ 语言特性\n\n现在让我们使用您在本章中学到的功能来编写应用。这个例子是一个简单的命令行计算器；您键入一个表达式，如`*6 * 7*`，应用解析输入并执行计算。\n\n启动 Visual C++ 并单击“文件”菜单，然后单击“新建”，最后单击“文件”...选项来获取“新建文件”对话框。在左侧窗格中，单击 Visual C++，在中间窗格中，单击 C++ 文件(。cpp)，然后单击打开按钮。在你做任何其他事情之前，保存这个文件。使用 Visual C++ 控制台(一个命令行，有 Visual C++ 环境)，导航到`Beginning_C++ `文件夹，创建一个名为`Chapter_02`的新文件夹。现在，在 Visual C++ 中，在“文件”菜单上，单击“将源 1.cpp 另存为”...在“文件另存为”对话框中，找到您刚刚创建的`Chapter_02`文件夹。在文件名框中，键入 calc.cpp，然后单击保存按钮。\n\n应用将使用`std::cout`和`std::string`；所以在文件的顶部，添加定义这些的标题，这样就不必使用完全限定的名称，添加一个`using`语句:\n\n```cpp\n    #include <iostream> \n    #include <string> \n\n    using namespace std;\n```\n\n您将通过命令行传递表达式，因此在文件底部添加一个采用命令行参数的`main`函数:\n\n```cpp\n    int main(int argc, char *argv[]) \n    { \n    }\n```\n\n该应用处理形式为`arg1 op arg2`的表达式，其中`op`是运算符，`arg1`和`arg2`是参数。这意味着，当调用应用时，它必须有四个参数；第一个是用于启动应用的命令，最后三个是表达式。`main`函数中的第一个代码应确保提供正确数量的参数，因此在该函数的顶部添加一个条件，如下所示:\n\n```cpp\n    if (argc != 4) \n    { \n        usage(); \n        return 1; \n    }\n```\n\n如果用多于或少于四个参数调用命令，则调用函数`usage`，然后`main`函数返回，停止应用。\n\n在`main`功能前增加`usage`功能，如下:\n\n```cpp\n    void usage() \n    { \n        cout << endl; \n        cout << \"calc arg1 op arg2\" << endl; \n        cout << \"arg1 and arg2 are the arguments\" << endl; \n        cout << \"op is an operator, one of + - / or *\" << endl; \n    }\n```\n\n这只是解释了如何使用该命令并解释了参数。此时，您可以编译应用。由于您使用的是 C++ 标准库，因此您需要在支持 C++ 异常的情况下进行编译，因此请在命令行中键入以下内容:\n\n```cpp\nC:\\Beginning_C++ Chapter_02\\cl /EHsc calc.cpp\n```\n\n如果你输入的代码没有任何错误，文件应该编译。如果您从编译器得到任何错误，请检查源文件，查看代码是否与前面代码中给出的完全一样。您可能会得到以下错误:\n\n```cpp\n'cl' is not recognized as an internal or external command,  \noperable program or batch file.\n```\n\n这意味着控制台不是用 Visual C++ 环境设置的，所以要么关闭它并通过 Windows“开始”菜单启动控制台，要么运行 vcvarsall.bat 批处理文件。\n\n一旦代码编译完毕，你就可以运行它了。首先用正确数量的参数(例如`calc 6 * 7`)运行它，然后用不正确数量的参数(例如`calc 6 * 7 / 3`)尝试它。请注意，参数之间的间距很重要:\n\n```cpp\nC:\\Beginning_C++ Chapter_02>calc 6 * 7 \n\nC:\\Beginning_C++ Chapter_02>calc 6 * 7 / 3 \n\ncalc arg1 op arg2 \narg1 and arg2 are the arguments \nop is an operator, one of + - / or *\n```\n\n在第一种情况下，应用什么也不做，所以您看到的只是一个空行。在第二个示例中，代码已经确定没有足够的参数，因此它将使用信息打印到控制台。\n\n接下来，您需要对参数进行一些简单的解析，以检查用户是否传递了有效值。在`main`功能的底部，添加以下内容:\n\n```cpp\n    string opArg = argv[2]; \n    if (opArg.length() > 1) \n    { \n        cout << endl << \"operator should be a single character\" << endl; \n        usage(); \n        return 1; \n    }\n```\n\n第一行用第三个命令行参数初始化一个 C++ `std::string`对象，该参数应该是表达式中的运算符。这个简单的例子只允许操作符只有一个字符，所以后面的几行检查以确保操作符是一个字符。C++ `std::string`类有一个名为`length`的成员函数，它返回字符串中的字符数。\n\n`argv[2]`参数的长度至少为一个字符(没有长度的参数不会被视为命令行参数！)，所以我们必须检查用户输入的运算符是否超过一个字符。\n\n接下来，您需要进行测试，以确保该参数是允许的受限设置之一，如果用户键入另一个运算符，则打印一个错误并停止处理。在`main`功能的底部，添加以下内容:\n\n```cpp\n    char op = opArg.at(0); \n    if (op == 44 || op == 46 || op < 42 || op > 47) \n    { \n        cout << endl << \"operator not recognized\" << endl; \n        usage(); \n        return 1; \n    }\n```\n\n测试将在一个角色上进行，所以你需要从`string`对象中提取这个角色。这段代码使用`at`函数，传递给你需要的字符的索引。([第 5 章](05.html)、*使用标准库容器*，将给出更多关于`std::string`类成员的细节。)下一行检查该字符是否不受支持。代码依赖于我们支持的字符的以下值:\n\n| **字符** | **值** |\n| `+` | `42` |\n| `*` | `43` |\n| `-` | `45` |\n| `/` | `47` |\n\n可以看到，如果字符小于`42`或者大于`47`就会不正确，但是在`42`和`47`之间还有两个我们也要拒绝的字符:`,` ( `44`)和`.` ( `46`)。这就是为什么我们有前面的条件:“如果字符小于 42 或大于`47`，或者是`44`或`46`，那么拒绝它。”\n\n`char`数据类型是整数，这就是测试使用整数文字的原因。您可以使用字符文字，因此以下更改同样有效:\n\n```cpp\n if (op == ',' || op == '.' || op < '+' || op > '/') \n    { \n        cout << endl << \"operator not recognized\" << endl; \n        usage(); \n        return 1; \n    }\n```\n\n你应该使用你认为最易读的。由于检查一个字符是否*大于另一个字符*意义不大，本书将使用前者。\n\n此时，您可以编译代码并测试它。首先尝试使用一个多于一个字符的运算符(例如，`**`)，并确认您得到的消息是运算符应该是单个字符。其次，用不被识别的操作符进行测试；尝试除`+`、`*`、`-`或`/`以外的任何角色，但也值得尝试`.`和`,`。\n\n请记住，命令提示符对某些符号有特殊的操作，如“`&`”和“`|`”，甚至在调用代码之前，命令提示符可能会通过解析命令行来给你一个错误。\n\n接下来要做的是将参数转换成代码可以使用的形式。命令行参数以字符串数组的形式传递给程序；但是，我们将其中一些参数解释为浮点数(实际上是双精度浮点数)。C 运行库提供了一个名为`atof`的函数，该函数可通过 C++ 标准库获得(在本例中，`<iostream>`包括包含`<cmath>`的文件，其中`atof`被声明)。\n\nIt is a bit counter-intuitive to get access to a math function such as `atof` through including a file associated with stream input and output. If this makes you uneasy, you can add a line after the `include` lines to include the `<cmath>` file. The C++ Standard Library headers have been written to ensure that a header file is only included once, so including `<cmath>` twice has no ill effect. This was not done in the preceding code, because it was argued that `atof` is a string function and the code includes the `<string>` header and, indeed, `<cmath>` is included via the files the `<string>` header includes.\n\n在`main`功能的底部增加以下几行。前两行将第二个和第四个参数(记住，C++ 数组是从零开始索引的)转换为`double`值。最后一行声明一个变量来保存结果:\n\n```cpp\n    double arg1 = atof(argv[1]); \n    double arg2 = atof(argv[3]); \n    double result = 0;\n```\n\n现在我们需要确定传递了哪个操作符，并执行请求的操作。我们将用`switch`语句来实现这一点。我们知道`op`变量是有效的，所以我们不需要提供`default`子句来获取我们没有测试过的值。在函数底部添加`switch`语句:\n\n```cpp\n    double arg1 = atof(argv[1]); \n    double arg2 = atof(argv[3]); \n    double result = 0; \n\n    switch(op) \n    { \n    }\n```\n\n前三种情况`+`、`-`和`*`，都很简单:\n\n```cpp\n    switch (op) \n    { \n case '+': result = arg1 + arg2; break; case '-': result = arg1 - arg2; break; case '*': result = arg1 * arg2; break; \n    }\n```\n\n同样，由于`char`是一个整数，您可以在`switch`语句中使用它，但是 C++ 允许您检查字符值。在这种情况下，使用字符而不是数字使代码更易读。\n\n在`switch`后，添加最终代码打印出结果:\n\n```cpp\n    cout << endl; \n    cout << arg1 << \" \" << op << \" \" << arg2; \n    cout << \" = \" << result << endl;\n```\n\n现在，您可以编译代码并用涉及`+`、`-`和`*`的计算来测试它。\n\n除法是个问题，因为除以零是无效的。为了测试这一点，在`switch`的底部添加以下行:\n\n```cpp\n case '/': result = arg1 / arg2; break;\n```\n\n编译并运行代码，传递零作为最终参数:\n\n```cpp\nC:\\Beginning_C++ Chapter_02>calc 1 / 0 \n1 / 0 = inf\n```\n\n代码运行成功，打印出了表达式，但是上面说结果是一个奇数`inf`。这里发生了什么？\n\n除以零将`result`分配给`NAN`的值，这是在`<math.h>`中定义的常数(通过`<cmath>`包含)，表示“不是数字”`cout`对象的插入运算符的`double`重载测试该数字是否有有效值，如果该数字有值`NAN`，则打印字符串 inf。在我们的应用中，我们可以测试一个零除数，并且我们将传递零的用户操作视为一个错误。因此，更改代码，使其如下所示:\n\n```cpp\n    case '/': \n if (arg2 == 0) { cout << endl << \"divide by zero!\" << endl; return 1; } else { \n        result = arg1 / arg2; \n } \n    break;\n```\n\n现在当用户通过零作为除数时，你会得到一条`divide by zero!`信息。\n\n现在，您可以编译完整的示例并进行测试。应用支持使用`+`、`-`、`*`和`/`运算符的浮点运算，并将处理被零除的情况。\n\n# 摘要\n\n在本章中，您已经学习了如何格式化代码，以及如何识别表达式和语句。您已经学习了如何识别变量的范围，以及如何将函数和变量的集合分组到名称空间中，以便防止名称冲突。您还学习了 C++ 中循环和分支代码的基本管道，以及内置运算符的工作原理。最后，您将所有这些放在一个简单的应用中，允许您在命令行执行简单的计算。\n\n在下一章中，您将学习如何使用内存、数组和指针。**"
  },
  {
    "path": "docs/mod-cpp/02.md",
    "content": "# 二、使用内存、数组和指针\n\nC++ 允许你通过指针直接访问内存。这为您提供了很大的灵活性，并有可能通过消除一些不必要的数据复制来提高代码的性能。然而，它也提供了额外的错误来源；有些可能对您的应用是致命的，或者更糟(是的，比致命更糟！)因为内存缓冲区使用不当会在代码中打开安全漏洞，从而让恶意软件接管机器。很明显，指针是 C++ 的一个重要方面。\n\n在本章中，您将看到如何声明指针并将其初始化到内存位置，如何在堆栈和上分配内存，C++ 自由存储，以及如何使用 C++ 数组。\n\n# 在 C++ 中使用内存\n\nC++ 使用与 C 相同的语法来声明指针变量并将其分配给内存地址，并且它具有类似 C 的指针算法。像 C 一样，C++ 也允许你在堆栈上分配内存，所以当堆栈框架被破坏时，会自动清理内存，程序员有责任释放内存的动态分配(在 C++ 自由存储上)。本节将介绍这些概念。\n\n# 使用 C++ 指针语法\n\n用 C++ 访问内存的语法很简单。`&`运算符返回对象的地址。*对象*可以是变量、内置类型或自定义类型的实例，甚至是函数(函数指针将在下一章讨论)。地址被分配了一个类型化的指针变量或`void*`指针。`void*`指针应该被视为仅仅是存储内存地址，因为您不能访问数据，也不能对`void*`指针执行指针算术(即，使用算术运算符操作指针值)。指针变量通常使用类型和`*`符号来声明。例如:\n\n```cpp\n    int i = 42; \n    int *pi = &i;\n```\n\n在这段代码中，变量`i`是一个整数，编译器和链接器将决定这个变量分配到哪里。通常，函数中的一个变量会在堆栈框架上，这将在后面的章节中描述。运行时，将创建堆栈(本质上是分配一大块内存)，并将在堆栈内存中为变量`i`保留空间。然后，该程序在该存储器中放入一个值(42)。接下来，分配给变量`i`的内存地址被放置在变量`pi`中。下图说明了之前代码的内存使用情况:\n\n![](img/5f58fa8b-8c10-4012-90e1-7732ae5a4de1.png)\n\n指针保存`0x007ef8c`的值(注意，最低字节存储在内存的最低字节中；这是针对 x86 机器的)。内存位置`0x007ef8c`的值为`0x0000002a`，即变量`i`的值 42。由于`pi`也是一个变量，它也占用内存空间，在这种情况下，编译器将内存中的指针*放在比它所指向的数据更低的*位置，在这种情况下，这两个变量是不连续的。\n\n像这样在堆栈上分配变量，您不应该假设变量在内存中的分配位置，也不应该假设它们相对于其他变量的位置。\n\n这段代码假设一个 32 位的操作系统，因此指针`pi`占据 32 位并且包含一个 32 位的地址。如果操作系统是 64 位，那么指针将是 64 位宽(但是整数可能仍然是 32 位)。在本书中，我们将使用 32 位指针，因为 32 位地址比 64 位地址需要更少的输入。\n\n类型化的指针用`*`符号声明，我们称之为`int*`指针，因为指针指向保存`int`的内存。声明指针时，惯例是将`*`放在变量名旁边，而不是类型旁边。这个语法强调了*类型指向的*是一个`int`。但是，如果在一条语句中声明了多个变量，则使用以下语法非常重要:\n\n```cpp\n    int *pi, i;\n```\n\n很明显，第一个变量是`int*`指针，第二个是`int`。以下不太清楚:\n\n```cpp\n    int* pi, i;\n```\n\n你可能会把这解释为两个变量的类型都是`int*`、*，但事实并非如此*，因为这声明了一个指针和一个`int`。如果要声明两个指针，那么对每个变量应用`*`:\n\n```cpp\n    int *p1, *p2;\n```\n\n最好是在单独的行上声明这两个指针。\n\n当您将`sizeof`运算符应用于指针时，您将获得指针的大小，而不是它所指向的内容。因此，在 x86 机器上，`sizeof(int*)`将返回 4；在 x64 机器上，它将返回 8。这是一个重要的观察，尤其是当我们在后面的部分讨论 C++ 内置数组时。\n\n要访问指针指向的数据，您必须使用`*`操作符**取消引用**:\n\n```cpp\n    int i = 42; \n    int *pi = &i; \n    int j = *pi;\n```\n\n像这样在赋值的右边使用，取消引用的指针给出了对指针所指向的值的访问，因此`j`被初始化为 42。将此与指针的声明进行比较，其中也使用了`*`符号，但具有不同的含义。\n\n取消引用操作符不仅仅是对内存位置的数据进行读访问。只要指针不限制它(使用`const`关键字；请参阅后面的内容)，您也可以取消引用指针来写入内存位置:\n\n```cpp\n    int i = 42; \n    cout << i << endl; \n    int *pi { &i }; \n    *pi = 99; \n    cout << i << endl;\n```\n\n在这段代码中，指针`pi`指向变量`i`在内存中的位置(在这种情况下，使用大括号语法)。分配取消引用的指针会将值分配给指针指向的位置。结果是在最后一行，变量`i`的值将是 99，而不是 42。\n\n# 使用空指针\n\n指针可以指向安装在计算机内存中的任何位置，通过取消引用的指针进行分配意味着您可能会覆盖操作系统使用的敏感内存，或者(通过直接内存访问)写入计算机硬件使用的内存。但是，操作系统通常会给可执行文件一个它可以访问的特定内存范围，试图访问该范围之外的内存将导致操作系统内存访问冲突。\n\n因此，您应该总是使用`&`操作符或从对操作系统函数的调用中获取指针值。你不应该给指针一个绝对地址。唯一的例外是无效内存地址的 C++ 常量`nullptr`:\n\n```cpp\n    int *pi = nullptr; \n    // code \n    int i = 42; \n    pi = &i; \n    // code \n    if (nullptr != pi) cout << *pi << endl;\n```\n\n该代码将指针`pi`初始化为`nullptr`。在代码的后面，指针被初始化为一个整数变量的地址。在代码的后面，使用指针，但是不是立即调用它，而是首先检查指针以确保它已经被初始化为非空值。编译器将检查您是否要使用尚未初始化的变量，但是如果您正在编写库代码，编译器将不知道代码的调用方是否会正确使用指针。\n\nThe type of constant `nullptr` is not an integer, it is `std::nullptr_t`. All pointer types can be implicitly converted to this type, so `nullptr` can be used to initialize variables of all pointer types.\n\n# 记忆的类型\n\n一般来说，你可以把记忆看作四种类型之一:\n\n*   静态或全局\n*   字符串池\n*   自动或堆叠\n*   免费商店\n\n当您在全局级别声明一个变量时，或者如果您有一个在函数中声明为`static`的变量，那么编译器将确保该变量是从与应用具有相同生存期的内存中分配的——该变量在应用启动时创建，在应用结束时删除。\n\n当您使用字符串文字时，数据实际上也是一个全局变量，但存储在可执行文件的不同部分。对于 Windows 可执行文件，字符串文字存储在可执行文件的`.rdata` PE/COFF 部分。文件的`.rdata`部分用于只读初始化数据，因此您不能更改数据。Visual C++ 允许您更进一步，并为您提供了**字符串池**选项。考虑一下:\n\n```cpp\n    char *p1 { \"hello\" }; \n    char *p2 { \"hello\" }; \n    cout << hex; \n    cout << reinterpret_cast<int>(p1) << endl; \n    cout << reinterpret_cast<int>(p2) << endl;\n```\n\n在这段代码中，两个指针用字符串`hello`的地址初始化。在下面两行中，每个指针的地址都打印在控制台上。由于`char*`的`<<`运算符将变量视为指向字符串的指针，因此它将打印字符串而不是指针的地址。为了解决这个问题，我们调用`reinterpret_cast`运算符将指针转换为整数并打印该整数的值。\n\n如果使用 Visual C++ 编译器在命令行编译代码，您将看到打印了两个不同的地址。这两个地址在`.rdata`部分，都是只读的。如果您使用`/GF`开关编译此代码以启用字符串池(这是 Visual C++ 项目的默认设置)，编译器将看到两个字符串是相同的，并且将只在`.rdata`部分存储一个副本，因此此代码的结果将是一个地址在控制台上打印两次。\n\n在这段代码中，两个变量`p1`和`p2`是自动变量，也就是说，它们是在为当前函数创建的堆栈上创建的。调用函数时，会为函数分配一大块内存，其中包含传递给函数的参数和调用函数的代码的返回地址的空间，以及函数中声明的自动变量的空间。当函数完成时，堆栈框架被破坏。\n\nThe **calling convention** of the function determines whether the calling function or the called function has the responsibility to do this. In Visual C++, the default is the `__cdecl` calling convention, which means the calling function cleans up the stack. The `__stdcall` calling convention is used by Windows operating system functions and the stack clean up is carried out by the called function. More details will be given in the next chapter.\n\n只要函数和这些变量的地址在函数中没有任何意义，自动变量就不会存在。在本章的后面，您将看到如何创建数据数组。作为自动变量分配的数组在堆栈上被分配到编译时确定的固定大小。大型数组可能会超出堆栈的大小，尤其是递归调用的函数。在 Windows 上，默认堆栈大小是 1 MB，在 x86 Linux 上，是 2 MB。Visual C++ 允许您使用`/F`编译器开关(或`/STACK`链接器开关)指定更大的堆栈。gcc 编译器允许您使用`--stack`开关更改默认堆栈大小。\n\n最后一种内存是在**免费商店**上创建的**动态内存**，有时也称为**堆**。这是使用内存最灵活的方式。顾名思义，您在运行时分配的内存大小是在运行时确定的。免费存储的实现依赖于 C++ 实现，但是您应该将免费存储视为与您的应用具有相同的生存期，因此从免费存储分配的内存应该至少与您的应用一样长。\n\n然而，这里有潜在的危险，特别是对于长寿命的应用。当你用完空闲存储时，从空闲存储分配的所有内存都应该返回到空闲存储，这样空闲存储管理器就可以重用这些内存。如果您没有适当地返回内存，那么空闲存储管理器可能会耗尽内存，这将提示它向操作系统请求更多内存，因此，应用的内存使用量将随着时间的推移而增长，从而由于内存分页而导致性能问题。\n\n# 指针算法\n\n指针指向内存，指针的类型决定了可以通过指针访问的数据的类型。所以一个`int*`指针会指向内存中的一个整数，你去引用这个指针(`*`)得到这个整数。如果指针允许(没有标记为`const`，可以通过指针算术改变其值)。例如，您可以递增或递减指针。内存地址值的变化取决于指针的类型。由于一个类型化指针指向一个类型，任何指针算法都会以该类型的*大小*为单位改变指针。\n\n如果增加一个`int*`指针，它将指向内存中下一个整数*，内存地址的变化取决于整数的大小。这相当于数组索引，其中像`v[1]`这样的表达式意味着您应该从`v`中第一个项目的内存位置开始，然后在内存中再移动一个项目，并在那里返回该项目:*\n\n```cpp\n    int v[] { 1, 2, 3, 4, 5 };\n    int *pv = v;\n    *pv = 11;\n    v[1] = 12;\n    pv[2] = 13;\n    *(pv + 3) = 14;\n```\n\n第一行在堆栈上分配一个五个整数的数组，并将值初始化为数字 1 到 5。在这个例子中，因为使用了初始化列表，编译器将为所需数量的项目创建空间，因此没有给出数组的大小。如果在括号之间给出数组的大小，那么初始化列表中的项目不能超过数组的大小。如果列表中的项目较少，那么数组中的其余项目将被初始化为默认值(通常为零)。\n\n这段代码的下一行获取指向数组中第一项的指针。这一行很重要:数组名被视为指向数组中第一项的指针。以下几行以各种方式改变数组项。第一个(`*pv`)通过取消指针引用并为其赋值来更改数组中的第一项。第二个(`v[1]`)使用数组索引为数组中的第二项赋值。第三个(`pv[2]`)使用索引，但这次使用指针，并为数组中的第三个值赋值。最后一个例子(`*(pv + 3)`)使用指针算法来确定数组中第四项的地址(记住第一项的索引为 0)，然后取消指针引用来为该项赋值。在这些之后，数组包含值`{ 11, 12, 13, 14, 5 }`和内存布局如下图所示:\n\n![](img/1ca5a666-72a0-4412-895f-6e331bb5042d.png)\n\n如果您有一个包含值的内存缓冲区(在本例中，通过数组分配)，并且您想要将每个值乘以 3，您可以使用指针算法来实现:\n\n```cpp\n    int v[] { 1, 2, 3, 4, 5 }; \n    int *pv = v; \n    for (int i = 0; i < 5; ++ i) \n    { \n        *pv++ *= 3; \n    }\n```\n\n循环语句比较复杂，需要参考[第 1 章](01.html)、*理解语言特性*中给出的运算符优先级。后缀增量运算符的优先级最高，次高的优先级是取消引用运算符(`*`)，最后，`*=`运算符是三个运算符中最低的，因此运算符按以下顺序运行:`++ `、`*`、`*=`。后缀运算符在增量之前返回值*，因此尽管指针增加到内存中的下一项，表达式使用增量之前的地址。该地址随后被取消引用，该地址由赋值运算符赋值，该运算符用乘以 3 的值替换该项。这说明了指针和数组名之间的一个重要区别；您可以递增指针，但不能递增数组:*\n\n```cpp\n    pv += 1; // can do this \n    v += 1; // error\n```\n\n当然，您可以在数组名和指针上使用索引(带`[]`)。\n\n# 使用数组\n\n顾名思义，C++ 内置数组是零个或多个相同类型的数据项。在 C++ 中，方括号用于声明数组和访问数组元素:\n\n```cpp\n    int squares[4]; \n    for (int i = 0; i < 4; ++ i)  \n    { \n        squares[i] = i * i; \n    }\n```\n\n`squares`变量是整数数组。第一行为*四个*整数分配足够的内存，然后`for`循环用前四个方块初始化内存。编译器从堆栈中分配的内存是连续的，数组中的项目是顺序的，因此`squares[3]`的内存位置是`squares[2]`之后的`sizeof(int)`。因为数组是在堆栈上创建的，所以数组的大小是编译器的一条指令；这不是动态分配，因此大小必须是常数。\n\n这里有一个潜在的问题:数组的大小被提到两次，一次在声明中，然后再次在`for`循环中。如果使用两个不同的值，则初始化的项目可能太少，或者可能会访问数组之外的内存。ranged `for`语法允许您访问数组中的每个项目；编译器可以确定数组的大小，并在范围内的`for`循环中使用它。在下面的代码中，有一个故意的错误显示了数组大小的问题:\n\n```cpp\n    int squares[5]; \n    for (int i = 0; i < 4; ++ i)  \n    { \n        squares[i] = i * i; \n    } \n    for(int i : squares) \n    { \n        cout << i << endl; \n    }\n```\n\n数组的大小和第一个`for`循环的范围不一致，因此最后一项不会被初始化。然而，范围内的`for`循环将循环所有五个项目，因此将打印出最后一个值的一些随机值。如果使用相同的代码，但声明`squares`数组有三个项目，该怎么办？这取决于您正在使用的编译器以及您是否正在编译调试版本，但显然您将写入分配给数组的内存之外的内存*。*\n\n有一些方法可以缓解这些问题。第一种方法是为数组的大小声明一个常量，并在代码需要知道数组大小时使用它:\n\n```cpp\n    constexpr int sq_size = 4; \n    int squares[sq_size]; \n    for (int i = 0; i < sq_size; ++ i) \n    { \n        squares[i] = i * i; \n    }\n```\n\n数组声明必须有一个大小常量，并且使用`sq_size`常量变量进行管理。\n\n您可能还想计算已经分配的数组的大小。当应用于数组时，`sizeof`运算符返回整个数组的字节大小，因此您可以通过将该值除以单个项目的大小来确定数组的大小:\n\n```cpp\n    int squares[4]; \n    for (int i = 0; i < sizeof(squares)/sizeof(squares[0]); ++ i) \n    { \n        squares[i] = i * i; \n    }\n```\n\n这是更安全的代码，但显然是冗长的。C 运行时库包含一个名为`_countof`的宏，它执行这个计算。\n\n# 功能参数\n\n如图所示，数组会自动转换为适当的指针类型，如果将数组传递给函数或从函数返回数组，就会出现这种情况。这种向哑指针的衰减意味着其他代码无法假设数组的大小。指针可以指向堆栈上分配的内存，其中内存寿命由函数确定，或者指向全局变量，其中内存寿命是程序的内存寿命，或者它可以指向动态分配的内存，并且内存由程序员确定。指针声明中没有任何内容指示内存的类型或谁负责内存的释放。哑指针中也没有指针指向多少内存的信息。当您使用指针编写代码时，您必须遵守如何使用指针的规则。\n\n一个函数可以有一个数组参数，但这意味着比它显示的要少得多:\n\n```cpp\n    // there are four tires on each car \n    bool safe_car(double tire_pressures[4]);\n```\n\n该函数将检查数组的每个成员是否有一个介于允许的最小值和最大值之间的值。一辆车上任何时候都有四个轮胎在使用，所以函数*应该用四个值的数组来调用*。问题是，虽然看起来编译器*应该*检查传递给函数的数组大小是否合适，但事实并非如此。您可以这样调用这个函数:\n\n```cpp\n    double car[4] = get_car_tire_pressures(); \n    if (!safe_car(car)) cout << \"take off the road!\" << endl; \n    double truck[8] = get_truck_tire_pressures(); \n    if (!safe_car(truck)) cout << \"take off the road!\" << endl;\n```\n\n当然，对于开发人员来说，卡车不是汽车应该是显而易见的，所以这个开发人员不应该编写这段代码，但是编译语言的通常优势是编译器会为您执行一些*健全性检查*。在数组参数的情况下，不会。\n\n原因是数组是作为指针传递的，所以虽然参数看起来是一个内置数组，但是您不能使用您习惯于在像 ranged `for`这样的数组中使用的工具。事实上，如果`safe_car`函数调用`sizeof(tire_pressures)`，它将得到双指针的大小，而不是四`int`数组的字节大小 16。\n\n数组参数的这个*衰减为指针*的特性意味着，只有当你明确告诉一个数组参数的大小时，函数才会知道它的大小。您可以使用一对空方括号来指示应该向该项传递数组，但它实际上与指针相同:\n\n```cpp\n    bool safe_car(double tire_pressures[], int size);\n```\n\n这里，该函数有一个指示数组大小的参数。前面的函数与将第一个参数声明为指针完全相同。以下不是函数的重载；它是*相同的*功能:\n\n```cpp\n    bool safe_car(double *tire_pressures, int size);\n```\n\n重要的一点是，当你把一个数组传递给一个函数时，数组的*第一维*被当作一个指针。到目前为止，数组是一维的，但它们可能不止一维。\n\n# 多维数组\n\n数组可以是多维的，要添加另一个维度，需要添加另一组方括号:\n\n```cpp\n    int two[2]; \n    int four_by_three[4][3];\n```\n\n第一个示例创建一个两个整数的数组，第二个示例创建一个二维数组，其中 12 个整数排列成四行三列。当然，*行*和*列*是任意的，将二维数组当作常规的电子表格，但这有助于可视化数据在内存中的排列方式。\n\n请注意，每个维度周围都有方括号。C++ 在这方面与其他语言不同，所以`int x[10,10]`的声明将被 C++ 编译器报告为错误。\n\n初始化多维数组涉及一对大括号和数据，其顺序将用于初始化维度:\n\n```cpp\n    int four_by_three[4][3] { 11,12,13,21,22,23,31,32,33,41,42,43 };\n```\n\n在本例中，具有最高数字的值反映最左边的索引，较低数字反映最右边的索引(在这两种情况下，都比实际索引多一个)。显然，您可以将它拆分成几行，并使用空白来将值组合在一起，以使其更易读。也可以使用嵌套大括号。例如:\n\n```cpp\n    int four_by_three[4][3] = { {11,12,13}, {21,22,23}, \n                                {31,32,33}, {41,42,43} };\n```\n\n如果从左到右读取维度，就可以读取更深层次嵌套的初始化。有四行，因此在外部大括号中有四组嵌套的大括号。有三列，因此在嵌套的大括号中，有三个初始化值。\n\n嵌套大括号不仅仅是为了方便格式化 C++ 代码，因为如果您提供一对空大括号，编译器将使用默认值:\n\n```cpp\n    int four_by_three[4][3] = { {11,12,13}, {}, {31,32,33}, {41,42,43} };\n```\n\n这里，第二行项目被初始化为 0。\n\n增加尺寸时，原则适用:增加最右侧尺寸的嵌套:\n\n```cpp\n    int four_by_three_by_two[4][3][2]  \n       = { { {111,112}, {121,122}, {131,132} }, \n           { {211,212}, {221,222}, {231,232} }, \n           { {311,312}, {321,322}, {331,332} }, \n           { {411,412}, {421,422}, {431,432} }  \n         };\n```\n\n这是四行三列的对(如您所见，当尺寸增加时，很明显术语**行**和**列**在很大程度上是任意的)。\n\n您可以使用相同的语法访问项目:\n\n```cpp\n    cout << four_by_three_by_two[3][2][0] << endl; // prints 431\n```\n\n就内存布局而言，编译器以下列方式解释语法。第一个索引以六个整数(3 * 2)的块来确定从数组开始的偏移量，第二个索引以两个整数的块来指示这六个整数之一*块*本身内的偏移量，第三个索引是以单个整数表示的偏移量。因此`[3][2][0]`从一开始就是 *(3 * 6) + (2 * 2) + 0 = 22* 整数，将第一个整数视为索引零。\n\n多维数组被视为数组的数组，所以每个“行”的类型是`int[3][2]`，从声明中我们知道有四个。\n\n# 将多维数组传递给函数\n\n您可以将多维数组传递给函数:\n\n```cpp\n    // pass the torque of the wheel nuts of all wheels \n    bool safe_torques(double nut_torques[4][5]);\n```\n\n这将编译，您可以以 4x5 数组的形式访问该参数，假设该车辆有四个车轮，每个车轮上有五个螺母。\n\n如前所述，当您传递数组时，第一维将被视为指针，因此，虽然您可以将 4x5 数组传递给此函数，但您也可以传递 2x5 数组，编译器不会抱怨。但是，如果您传递一个 4x3 数组(也就是说，第二个维度与函数中声明的维度不同)，编译器将发出一个数组不兼容的错误。该参数可以更准确地描述为`double row[][5]`。由于第一个维度的大小不可用，函数应该用该维度的大小声明:\n\n```cpp\n    bool safe_torques(double nut_torques[][5], int num_wheels);\n```\n\n这说明`nut_torques`是一个或多个“行”，每个“行”有五个项目。由于数组不提供关于其行数的信息，所以您应该提供它。另一种声明方式是:\n\n```cpp\n    bool safe_torques(double (*nut_torques)[5], int num_wheels);\n```\n\n括号在这里很重要，如果省略使用`double *nut_torques[5]`，那么意味着`*`会引用数组中的类型，也就是编译器会把`nut_torques`当作`double*`指针的五元数组。我们以前见过这样一个数组的例子:\n\n```cpp\n    void main(int argc, char *argv[]);\n```\n\n`argv`参数是一组`char*`指针。也可以将`argv`参数声明为`char**`，意义相同。\n\n通常，如果您打算将数组传递给函数，最好使用自定义类型，或者使用 C++ 数组类型。\n\n对多维数组使用 ranged `for`比第一眼看到的要复杂一点，需要使用引用，这将在本章后面的章节中解释。\n\n# 使用字符数组\n\n字符串将在[第 6 章](06.html)、*使用字符串*中详细介绍，但这里值得指出的是，C 字符串是字符数组，通过指针变量访问。这意味着，如果您想要操纵字符串，您必须操纵指针指向的内存，而不是操纵指针本身。\n\n# 比较字符串\n\n下面分配两个字符串缓冲区，并调用`strcpy_s`函数用相同的字符串初始化每个缓冲区:\n\n```cpp\n    char p1[6]; \n    strcpy_s(p1, 6, \"hello\"); \n    char p2[6]; \n    strcpy_s(p2, 6, p1); \n    bool b = (p1 == p2);\n```\n\n`strcpy_c`函数将把最后一个参数给出的指针中的字符(直到终止的`NUL`)复制到第一个参数给出的缓冲区中，第二个参数给出了缓冲区的最大大小。这两个指针在最后一行进行比较，这将返回一个值`false`。问题是比较函数比较的是指针的值，而不是指针指向的内容。两个缓冲区有相同的字符串，但是指针不同，所以`b`将是`false`。\n\n比较字符串的正确方法是逐个字符地比较数据，看它们是否相等。C 运行时提供`strcmp`来逐个字符地比较两个字符串缓冲区，`std::string`类定义了一个名为`compare`的函数，该函数也将执行这样的比较；但是，请注意这些函数返回的值:\n\n```cpp\n    string s1(\"string\"); \n    string s2(\"string\"); \n    int result = s1.compare(s2);\n```\n\n返回值不是表示两个字符串是否相同的`bool`类型；它是一个`int`。这些比较函数执行字典式比较，如果参数(本代码中的`s2`)字典式大于操作数(`s1`)，则返回负值，如果操作数大于参数，则返回正数。如果两个字符串相同，函数返回 0。记住一个`bool`是 0 值的`false`，非零值的`true`。标准库为`std::string`的`==`操作符提供了一个重载，所以这样写代码是安全的:\n\n```cpp\n    if (s1 == s2) \n    { \n        cout << \"strings are the same\" << endl; \n    }\n```\n\n运算符将比较两个变量中包含的字符串。\n\n# 防止缓冲区溢出\n\n用于操作字符串的 C 运行时库因允许缓冲区溢出而臭名昭著。例如，`strcpy`函数将一个字符串复制到另一个字符串，您可以通过`<cstring>`头来访问这个字符串，它包含在`<iostream>`头中。你可能会想写这样的东西:\n\n```cpp\n    char pHello[5];          // enough space for 5 characters \n    strcpy(pHello, \"hello\");\n```\n\n问题是`strcpy`将复制到的所有字符，包括终止的`NULL`字符，因此您将复制六个字符到一个只有*五个*的空格数组中。您可能从用户输入中获取一个字符串(例如，从网页上的文本框中获取)，并认为您分配的数组足够大，但是恶意用户可能会故意提供一个过长的字符串，使其大于缓冲区，从而覆盖程序的其他部分。这样的*缓冲区溢出*导致了很多程序被黑客控制服务器，以至于 C 字符串函数都被更安全的版本取代。事实上，如果你想输入前面的代码，你会发现`strcpy`是可用的，但是 Visual C++ 编译器会发出一个错误:\n\n```cpp\nerror C4996: 'strcpy': This function or variable may be unsafe. \nConsider using strcpy_s instead. To disable deprecation, use _CRT_SECURE_NO_WARNINGS. See online help for details.\n```\n\n如果您有使用`strcpy`的现有代码，并且您需要编译该代码，您可以在`<cstring>`之前定义符号:\n\n```cpp\n    #define _CRT_SECURE_NO_WARNINGS \n    #include <iostream>\n```\n\n防止这个问题的最初尝试是调用`strncpy`，它将复制特定数量的字符:\n\n```cpp\n    char pHello[5];             // enough space for 5 characters \n    strncpy(pHello, \"hello\", 5);\n```\n\n该功能最多复制五个字符，然后停止。问题是要复制的字符串有五个字符，因此结果不会是`NULL`终止。这个函数的安全版本有一个参数，可以用来表示目标缓冲区有多大:\n\n```cpp\n    size_t size = sizeof(pHello)/sizeof(pHello[0]); \n    strncpy_s(pHello, size, \"hello\", 5);\n```\n\n在运行时，这仍然会引起问题。您已经告诉函数，缓冲区的大小是五个字符，它将确定这个大小不足以容纳您要求它复制的六个字符。更安全的字符串函数将调用一个名为**约束处理程序**的函数，默认版本将关闭程序，理由是缓冲区溢出意味着程序被破坏，而不是让程序安静地继续，缓冲区溢出导致问题。\n\nC 运行时库字符串函数最初是为了返回函数的结果而编写的，现在更安全的版本会返回一个错误值。也可以告诉`strncpy_s`函数截断副本，而不是调用约束处理程序:\n\n```cpp\n    strncpy_s(pHello, size, \"hello\", _TRUNCATE);\n```\n\nC++ `string`类保护您免受此类问题的影响。\n\n# 在 C++ 中使用指针\n\n指针在 C++ 中显然非常重要，但与任何强大的功能一样，也有问题和危险，因此值得指出一些主要问题。指针指向内存中的单个位置，指针的类型指示应该如何解释内存位置。你最多可以假设内存中那个位置的字节数是指针类型的大小。就这样。这意味着指针本身是不安全的。然而，在 C++ 中，它们是使您的进程中的代码能够访问大量数据的最快方法。\n\n# 越界访问\n\n当您分配一个缓冲区时，无论是在堆栈上还是在空闲存储区上，当您得到一个指针时，几乎没有什么可以阻止您访问尚未分配的内存，无论是在缓冲区位置之前还是之后。这意味着当您使用指针算法或数组上的索引访问时，您要仔细检查您不会访问超出界限的数据。有时错误可能不会立即显现:\n\n```cpp\n    int arr[] { 1, 2, 3, 4 }; \n    for (int i = 0; i < 4; ++ i)  \n    { \n        arr[i] += arr[i + 1]; // oops, what happens when i == 3? \n    }\n```\n\n当您使用索引时，您必须不断提醒自己数组是从零开始索引的，因此最高索引是数组的大小减 1。\n\n# 指向解除分配内存的指针\n\n这适用于堆栈上分配的内存和动态分配的内存。下面是一个写得不好的函数，它返回在函数堆栈上分配的字符串:\n\n```cpp\n    char *get() \n    { \n        char c[] { \"hello\" };\n        return c;\n    }\n```\n\n前面的代码分配了一个六个字符的缓冲区，然后用字符串文字`hello`的五个字符和`NULL`终止字符初始化它。问题是，一旦函数完成，堆栈框架就会被拆除，这样内存就可以被重新使用，指针会指向其他东西可以使用的内存。这个错误是由糟糕的编程引起的，但是它可能没有这个例子中那么明显。如果函数使用几个指针并执行指针赋值，您可能不会立即注意到您已经返回了指向堆栈分配对象的指针。最好的做法是不要从函数返回原始指针，但是如果您确实想使用这种编程风格，请确保内存缓冲区是通过参数传入的(因此函数不拥有缓冲区)，或者是动态分配的，并且您将所有权传递给调用方。\n\n这就引出了另一个问题。如果您在指针上调用`delete`，然后在代码的后面，尝试访问指针，您将访问可能被其他变量使用的内存。为了缓解这个问题，你可以养成在删除时给`null_ptr`分配一个指针的习惯，并在使用指针前检查`null_ptr`。或者，您可以使用一个智能指针对象来实现这一点。智能指针将包含在[第 4 章](04.html)、*类*中。\n\n# 转换指针\n\n您可以输入指针，也可以输入`void*`指针。类型化指针将像访问指定类型一样访问内存(当您继承类时，这将产生有趣的结果，但这将留给[第 4 章](04.html)、*类*。因此，如果您将指针转换为不同的类型并取消引用它，内存将被视为包含转换类型。这样做很少有意义。`void*`指针不能被取消引用，所以你永远不能通过`void*`指针访问数据，要访问数据你必须转换指针。\n\n`void*`指针类型的全部原因是它可以指向任何东西。一般来说，`void*`指针应该只在类型与函数无关时使用。例如，C `malloc`函数返回一个`void*`指针，因为该函数只是分配内存；它不在乎那段记忆会被用来做什么。\n\n# 常数指针\n\n指针可以被声明为`const`，这取决于您应用它的位置，意味着指针指向的内存通过指针是只读的，或者指针的值是只读的:\n\n```cpp\n    char c[] { \"hello\" }; // c can be used as a pointer \n    *c = 'H';             // OK, can write thru the pointer \n    const char *ptc {c};  // pointer to constant \n    cout << ptc << endl;  // OK, can read the memory pointed to \n    *ptc =  'Y';          // cannot write to the memory \n    char *const cp {c};   // constant pointer \n    *cp = 'y';            // can write thru the pointer \n    cp++ ;                 // cannot point to anything else\n```\n\n这里，`ptc`是一个指向常量`char`的指针，也就是说，虽然你可以改变`ptc`指向的东西，你可以读取它指向的东西，但是你不能用它来改变记忆。另一方面，`cp`是一个常量指针，意味着你可以读写指针指向的内存，但不能改变它指向的位置。典型的做法是将`const char*`指针传递给函数，因为函数不知道字符串被分配到了哪里，也不知道缓冲区的大小(调用者可能会传递一个无法更改的文字)。请注意，没有`const*`运算符，因此`char const*`被视为`const char*`，一个指向常量缓冲区的指针。\n\n您可以使用强制转换使指针成为常量、更改指针或移除指针。为了证明这一点，下面对`const`关键词做了一些相当无意义的改变:\n\n```cpp\n    char c[] { \"hello\" }; \n    char *const cp1 { c }; // cannot point to any other memory \n    *cp1 = 'H';            // can change the memory \n    const char *ptc = const_cast<const char*>(cp1); \n    ptc++ ;                 // change where the pointer points to \n    char *const cp2 = const_cast<char *const>(ptc); \n    *cp2 = 'a';            // now points to Hallo\n```\n\n指针`cp1`和`cp2`可以用来改变它们所指向的内存，但是一旦被赋值，它们都不能指向其他内存。第一个`const_cast`将`const`的属性丢弃到一个指针上，该指针可以被更改为指向其他内存，但不能用于更改该内存，`ptc`。第二个`const_cast`去掉了`ptc`的`const`属性，这样就可以通过指针`cp2`改变记忆。\n\n# 更改指向的类型\n\n`static_cast`运算符用于通过编译时检查进行转换，而不是运行时检查，因此这意味着指针必须相关。`void*`指针可以转换成任何指针，所以下面的编译是有意义的:\n\n```cpp\n    int *pi = static_cast<int*>(malloc(sizeof(int))); \n    *pi = 42; \n    cout << *pi << endl; \n    free(pi);\n```\n\nC `malloc`函数返回一个`void*`指针，所以你必须转换它才能使用内存。(当然，C++ `new`运算符不需要这样的强制转换。)内置类型对于`static_cast`在指针类型之间转换来说不够“相关”，所以不能使用`static_cast`将`int*`指针转换为`char*`指针，即使`int`和`char`都是整数类型。对于通过继承相关的自定义类型，可以使用`static_cast`强制转换指针，但是没有运行时检查强制转换是否正确。要进行运行时检查，您应该使用`dynamic_cast`，更多细节将在[第 4 章](04.html)、*类*中给出。\n\n`reinterpret_cast`运算符是强制转换运算符中最灵活、最危险的，因为它将在任何指针类型之间转换，而无需任何类型检查。它本身就不安全。例如，下面的代码用文字初始化一个宽字符数组。阵列`wc`将有六个字符，`hello`后跟`NULL`。`wcout`对象将`wchar_t*`指针解释为指向`wchar_t`字符串中第一个字符的指针，因此插入`wc`将打印该字符串(直到`NUL`的每个字符)。要获得实际的内存位置，您必须将指针转换为整数:\n\n```cpp\n    wchar_t wc[] { L\"hello\" }; \n    wcout << wc << \" is stored in memory at \"; \n    wcout << hex; \n    wcout << reinterpret_cast<int>(wc) << endl;\n```\n\n同样，如果在`wcout`对象中插入一个`wchar_t`，它将打印字符，而不是数值。因此，为了打印出单个字符的代码，我们需要将指针转换为合适的整数指针。本代码假设`short`与`wchar_t`大小相同:\n\n```cpp\n    wcout << \"The characters are:\" << endl; \n    short* ps = reinterpret_cast<short*>(wc); \n    do  \n    {  \n        wcout << *ps << endl;  \n    } while (*ps++);\n```\n\n# 在代码中分配内存\n\nC++ 定义了两个运算符`new`和`delete`，它们从空闲存储区分配内存并将内存释放回空闲存储区。\n\n# 分配单个对象\n\n`new`运算符与类型一起使用来分配内存，它将返回一个指向该内存的类型化指针:\n\n```cpp\n    int *p = new int; // allocate memory for one int\n```\n\n`new`操作符将调用*默认构造函数*为其创建的每个对象定制类型(如[第 4 章](04.html)、*类*中所述)。内置类型没有构造函数，因此会发生类型初始化，这通常会将对象初始化为零(在本例中为零整数)。\n\n通常，如果没有显式初始化，就不应该使用为内置类型分配的内存。事实上，在 Visual C++ 中，`new`运算符的调试版本将为每个字节将内存初始化为一个值`0xcd`，以在调试器中直观地提醒您尚未初始化内存。对于自定义类型，由类型的作者来初始化分配的内存。\n\n重要的是，当您用完内存时，您要将它返回到空闲存储区，以便分配器可以重用它。您可以通过呼叫`delete`操作员来完成:\n\n```cpp\n    delete p;\n```\n\n删除指针时，对象的**析构函数**被调用。对于内置类型，这没有任何作用。在你删除了一个指向`nullptr`的指针之后，初始化它是一个很好的做法，如果你在使用它之前使用检查指针值的惯例，这将保护你不使用被删除的指针。C++ 标准规定，如果删除一个值为`nullptr`的指针，`delete`运算符将无效。\n\nC++ 允许您在调用`new`运算符时初始化一个值，有两种方式:\n\n```cpp\n    int *p1 = new int (42); \n    int *p2 = new int {42};\n```\n\n对于自定义类型，`new`运算符将调用该类型的构造函数；对于内置类型，最终结果是相同的，并通过将该项初始化为提供的值来实现。您也可以使用初始化的列表语法，如前面代码的第二行所示。需要注意的是，初始化是指向的内存，而不是指针变量。\n\n# 分配对象数组\n\n您也可以使用`new`操作符在动态内存中创建对象数组。您可以通过在一对方括号中提供想要创建的项目数来实现这一点。下面的代码为两个整数分配内存:\n\n```cpp\n    int *p = new int[2]; \n    p[0] = 1; \n    *(p + 1) = 2; \n    for (int i = 0; i < 2; ++ i) cout << p[i] << endl; \n    delete [] p;\n```\n\n运算符返回一个指向所分配类型的指针，您可以使用指针算术或数组索引来访问内存。您不能在`new`语句中初始化内存；您必须在创建缓冲区后这样做。当您使用`new`为多个对象创建缓冲区时，您必须使用适当版本的`delete`运算符:`[]`用于指示删除多个项目，并将调用每个对象的析构函数。重要的是，您要始终使用与用于创建指针的`new`版本相适应的正确版本的`delete`。\n\n自定义类型可以为单个对象定义自己的运算符`new`和运算符`delete`，也可以为对象数组定义运算符`new[]`和运算符`delete[]`。自定义类型作者可以使用这些为其对象使用自定义内存分配方案。\n\n# 处理失败的分配\n\n如果`new`运算符不能为对象分配内存，它将抛出`std::bad_alloc`异常，返回的指针将是`nullptr`。例外情况在[第 7 章](07.html)、*诊断和调试*中有所介绍，因此这里只给出语法的简要概述。检查生产代码中分配内存的失败是很重要的。下面的代码展示了如何保护分配，以便您可以捕获`std::bad_alloc`异常并处理它:\n\n```cpp\n    // VERY_BIG_NUMER is a constant defined elsewhere \n    int *pi; \n    try \n    { \n        pi = new int[VERY_BIG_NUMBER]; \n        // other code \n    } \n    catch(const std::bad_alloc& e)  \n    {  \n        cout << \"cannot allocate\" << endl;  \n        return; \n    } \n    // use pointer \n    delete [] pi;\n```\n\n如果`try`块中的任何代码抛出异常控制，它将被传递到`catch`子句，忽略任何其他尚未执行的代码。`catch`子句检查异常对象的类型，如果它是正确的类型(在这种情况下是分配错误)，它创建对该对象的引用，并将控制传递给`catch`块，并且异常引用的范围是该块。在这个例子中，代码只是打印一个错误，但是您可以使用它来采取措施，以确保内存分配失败不会影响后续代码。\n\n# 使用新运算符的其他版本\n\n此外，自定义类型可以定义放置操作符`new`，允许您为自定义`new`功能提供一个或多个参数。放置的语法`new`是通过括号提供放置字段。\n\n`new`运算符的 C++ 标准库版本提供了一个可以将常量`std::nothrow`作为放置字段的版本。如果分配失败，此版本不会引发异常，相反，只能从返回指针的值来评估失败:\n\n```cpp\n    int *pi = new (std::nothrow) int [VERY_BIG_NUMBER]; \n    if (nullptr == pi)  \n    { \n        cout << \"cannot allocate\" << endl; \n    } \n    else \n    { \n        // use pointer \n        delete [] pi; \n    }\n```\n\n类型前的括号用于传递放置字段。如果在类型后使用括号，如果分配成功，这些括号将给出一个值来初始化对象。\n\n# 记忆寿命\n\n由`new`分配的内存将保持有效，直到你调用`delete`。这意味着您可能有较长生命周期的内存，并且代码可能在代码中的各种函数之间传递。请考虑以下代码:\n\n```cpp\n    int *p1 = new int(42); \n    int *p2 = do_something(p1); \n    delete p1; \n    p1 = nullptr; \n    // what about p2?\n```\n\n这段代码创建一个指针，并初始化它所指向的内存，然后将指针传递给函数，函数本身返回一个指针。由于不再需要`p1`指针，因此将其删除并分配给`nullptr`使其不能再次使用。这段代码看起来很好，但问题是如何处理函数返回的指针？假设该函数只是操纵指针指向的数据:\n\n```cpp\n    int *do_something(int *p) \n    { \n        *p *= 10; \n        return p; \n    }\n```\n\n实际上，调用`do_something`会创建指针的副本，但不会创建它所指向的内容的副本。这意味着当`p1`指针被删除时，它所指向的内存不再可用，因此指针`p2`指向无效内存。\n\n这个问题可以通过一种叫做**资源获取是初始化** ( **RAII** )的机制来解决，这意味着使用 C++ 对象的特性来管理资源。C++ 中的 RAII 需要类，特别是复制构造函数和析构函数。智能指针类可以用来管理指针，这样当指针被复制时，它所指向的内存也会被复制。析构函数是当对象超出范围时自动调用的函数，因此智能指针可以使用它来释放内存。智能指针和析构函数将包含在[第 4 章](04.html)、*类*中。\n\n# 视窗软件开发工具包和指针\n\n从函数中返回指针有其固有的危险:内存的责任被传递给调用者，调用者必须确保内存被适当地取消分配，否则这可能会导致内存泄漏并导致相应的性能损失。在这一节中，我们将了解 Window 的**软件开发工具包** ( **SDK** )提供访问内存缓冲区的一些方式，并学习一些 C++ 中使用的技术。\n\n首先，值得指出的是，Windows SDK 中任何返回字符串或具有字符串参数的函数都将有两个版本。以`A`为后缀的版本表示函数使用 ANSI 字符串，`W`版本将使用宽字符串。出于讨论的目的，使用 ANSI 函数更容易。\n\n`GetCommandLineA`功能有以下原型(考虑到 Windows SDK `typedef`):\n\n```cpp\n    char * __stdcall GetCommandLine();\n```\n\n所有的窗口函数都被定义为使用`__stdcall`调用约定。通常，你会看到`WINAPI`的`typedef`用于`__stdcall`呼叫惯例。\n\n这个函数可以这样调用:\n\n```cpp\n    //#include <windows.h>\n    cout << GetCommandLineA() << endl;\n```\n\n请注意，我们没有努力释放返回的缓冲区。原因是指针指向了你生命过程中的记忆，所以你*不应该*释放它。的确，如果你要释放它，你会怎么做？您不能保证该函数是用您正在使用的相同编译器或相同库编写的，因此您不能使用 C++ `delete`运算符或 C `free`函数。\n\n当一个函数返回一个缓冲区时，重要的是查阅文档，看看谁分配了缓冲区，谁应该释放它。\n\n再比如`GetEnvironmentStringsA`:\n\n```cpp\n    char * __stdcall GetEnvironmentStrings();\n```\n\n这也会返回一个指向缓冲区的指针，但是这一次文档很清楚，在使用缓冲区之后，您应该释放它。SDK 提供了一个名为`FreeEnvironmentStrings`的函数来实现这一点。缓冲区为每个环境变量包含一个形式为`name=value`的字符串，每个字符串以一个`NUL`字符结束。缓冲区中的最后一个字符串只是一个`NUL`字符，也就是说，缓冲区的末尾有两个`NUL`字符。这些功能可以这样使用:\n\n```cpp\n    char *pBuf = GetEnvironmentStringsA(); \n    if (nullptr != pBuf) \n    { \n        char *pVar = pBuf; \n        while (*pVar) \n        { \n            cout << pVar << endl; \n            pVar += strlen(pVar) + 1; \n        } \n\n        FreeEnvironmentStringsA(pBuf); \n    }\n```\n\n`strlen`函数是 C 运行时库的一部分，它返回字符串的长度。您不需要知道`GetEnvironmentStrings`函数如何分配缓冲区，因为`FreeEnvironmentStrings`将调用正确的解除分配代码。\n\n在某些情况下，开发人员有责任分配缓冲区。视窗软件开发工具包提供了一个名为`GetEnvironmentVariable`的函数来返回一个命名环境变量的值。当您调用这个函数时，您不知道环境变量是否被设置，或者它是否被设置，或者它的值有多大，所以这意味着您很可能必须分配一些内存。该功能的原型是:\n\n```cpp\n    unsigned long __stdcall GetEnvironmentVariableA(const char *lpName,   \n        char *lpBuffer, unsigned long nSize);\n```\n\n有两个参数是指向 C 字符串的指针。这里有一个问题，一个`char*`指针可能正在将中的*传递给函数，或者它可能被用来传递一个缓冲区，以便将一个字符串从*中返回*。你怎么知道`char*`指针是用来做什么的？*\n\n完整的参数声明为您提供了一个线索。`lpName`指针被标记为`const`，因此函数不会改变它所指向的字符串；这意味着它是参数中的*。此参数用于传入要获取的环境变量的名称。另一个参数只是一个`char*`指针，因此它可以用来将一个字符串*在*中传递给函数，或者将*从*中传递出去，或者将*在*中传递出去，将*从*中传递出去。了解如何使用该参数的唯一方法是阅读文档。在这种情况下，它是一个*出*参数；如果变量存在，函数将在`lpBuffer`中返回环境变量的值，或者如果变量不存在，函数将保持缓冲区不变，并将值返回 0。您有责任以您认为合适的任何方式分配该缓冲区，并且您可以在最后一个参数`nSize`中传递该缓冲区的大小。*\n\n该函数的返回值有两个目的。它用于指示发生了错误(只有一个值 0，这意味着您必须调用`GetLastError`函数来获取错误)，它还用于为您提供关于缓冲区的信息`lpBuffer`。如果函数成功，则返回值是复制到缓冲区中的字符数，不包括`NULL`终止字符。但是，如果函数确定缓冲区太小(它从`nSize`参数知道缓冲区的大小)而无法保存环境变量值，则不会发生复制，并且函数将返回所需的缓冲区大小，即环境变量中包含`NULL`终止符的字符数。\n\n调用此函数的常见方法是调用它两次，首先使用大小为零的缓冲区，然后在再次调用它之前使用返回值分配缓冲区:\n\n```cpp\n    unsigned long size = GetEnvironmentVariableA(\"PATH\", nullptr, 0); \n    if (0 == size)  \n    { \n        cout << \"variable does not exist \" << endl; \n    } \n    else \n    { \n        char *val = new char[size]; \n        if (GetEnvironmentVariableA(\"PATH\", val, size) != 0) \n        { \n            cout << \"PATH = \";\n            cout << val << endl; \n        } \n        delete [] val; \n    }\n```\n\n一般来说，与所有库一样，您必须阅读文档来确定如何使用参数。窗口文档将告诉您指针参数是输入、输出还是输入/输出。它还会告诉您谁拥有内存，以及您是否有责任分配和/或释放内存。\n\n每当您看到函数的指针参数时，请特别注意查看文档，了解指针的用途以及如何管理内存。\n\n# 内存和 C++ 标准库\n\nC++ 标准库提供了各种类，允许您操作对象集合。这些类被称为**标准模板库** ( **STL** )，提供了将项目插入集合对象的标准方法，以及访问项目和遍历整个集合的方法(称为迭代器)。STL 定义了集合类，这些集合类被实现为队列、堆栈或具有随机访问的向量。这些类将在[第 5 章](05.html)、*中使用标准库容器*进行深入讨论，因此在这一节中，我们将只讨论两个行为类似于 C++ 内置数组的类。\n\n# 标准库阵列\n\nc++ 标准库提供了两个容器，通过索引器对数据进行随机访问。这两个容器还允许您访问底层内存，并且由于它们保证了在内存中顺序且连续地存储项目，所以当您需要提供指向缓冲区的指针时，可以使用它们。这两种类型都是模板，这意味着您可以使用它们来保存内置类型和自定义类型。这两个采集类分别是`array`和`vector`。\n\n# 使用基于堆栈的数组类\n\n`array`类在`<array>`头文件中定义。该类允许您在堆栈上创建固定大小的数组，并且与内置数组一样，它们在运行时不能收缩或扩展。因为它们是在堆栈上分配的，所以在运行时不需要调用内存分配器，但是很明显，它们应该小于堆栈帧的大小。这意味着一个`array`是一个小物品阵列的好选择。`array`的大小必须在编译时知道，并作为模板参数传递:\n\n```cpp\n    array<int, 4> arr { 1, 2, 3, 4 };\n```\n\n在这段代码中，尖括号(`<>`)中的第一个模板参数是数组中每个项目的类型，第二个参数是项目的数量。这段代码用一个初始化列表初始化数组，但是请注意，您仍然需要在模板中提供数组的大小。这个对象将像一个内置数组(或者任何标准库容器)一样工作，排列为`for`:\n\n```cpp\n    for (int i : arr) cout << i << endl;\n```\n\n原因是`array`实现了该语法所需的`begin`和`end`功能。您也可以使用索引来访问项目:\n\n```cpp\n    for (int i = 0; i < arr.size(); ++ i) cout << arr[i] << endl;\n```\n\n`size`函数将返回数组的大小，方括号索引器对数组成员进行随机访问。您可以访问数组边界之外的内存，因此对于之前定义的有四个成员的数组，您可以访问`arr[10]`。这可能会导致运行时出现意外行为，甚至出现某种内存故障。为了防止这种情况，类提供了一个函数`at`，它将执行范围检查，如果索引超出范围，类将抛出 C++ 异常`out_of_range`。\n\n使用`array`对象的主要优点是，您可以获得编译时检查，以查看您是否无意中将该对象作为哑指针传递给了函数。考虑这个函数:\n\n```cpp\n    void use_ten_ints(int*);\n```\n\n在运行时，函数不知道传递给它的缓冲区的大小，在这种情况下，文档中说您必须传递一个具有 10 个`int`类型变量的缓冲区，但是，正如我们所看到的，C++ 允许使用一个内置数组作为指针:\n\n```cpp\n    int arr1[] { 1, 2, 3, 4 }; \n    use_ten_ints(arr1); // oops will read past the end of the buffer\n```\n\n没有编译器检查，也没有任何运行时检查来捕获此错误。`array`类不会允许这样的错误发生，因为没有自动转换成哑指针:\n\n```cpp\n    array<int, 4> arr2 { 1, 2, 3, 4 };  \n    use_ten_ints(arr2); // will not compile\n```\n\n如果您真的坚持要获取一个哑指针，那么您可以这样做，并保证可以将数据作为连续的内存块进行访问，其中的项目是按顺序存储的:\n\n```cpp\n    use_ten_ints(&arr2[0]);    // compiles, but on your head be it \n    use_ten_ints(arr2.data()); // ditto\n```\n\n该类不仅仅是内置数组的包装器，它还提供了一些附加功能。例如:\n\n```cpp\n    array<int, 4> arr3; \n    arr3.fill(42);   // put 42 in each item \n    arr2.swap(arr3); // swap items in arr2 with items in arr3\n```\n\n# 使用动态分配的向量类\n\n标准库还在`<vector>`头中提供了`vector`类。同样，这个类是一个模板，因此您可以将其与内置类型和自定义类型一起使用。然而，与`array`不同，内存是动态分配的，这意味着`vector`可以在运行时扩展或收缩。这些项目是连续存储的，因此您可以通过调用`data`函数或访问第一个项目的地址来访问底层缓冲区(为了支持调整集合的大小，缓冲区可能会发生变化，因此此类指针只能临时使用)。当然，和`array`一样，没有自动转换成哑指针。`vector`类提供带方括号语法的索引随机访问和`at`函数的范围检查。该类还实现了允许容器与标准库函数一起使用的方法，并设置了`for`。\n\n`vector`类比`array`类更灵活，因为你可以插入物品，移动物品，但这确实会带来一些开销。因为类的实例在运行时动态分配内存，所以使用分配器是有成本的，并且在初始化和销毁(当`vector`对象超出范围时)时会有一些额外的开销。`vector`类的对象占用的内存也比它保存的数据多。因此不适合少量物品(此时`array`是更好的选择)。\n\n# 参考\n\n引用是对象的别名。也就是说，它是对象的另一个名称，因此通过引用访问对象与通过对象的变量名访问对象是一样的。引用使用引用名称上的`&`符号来声明，其初始化和访问方式与变量完全相同:\n\n```cpp\n    int i = 42; \n    int *pi = &i;  // pointer to an integer \n    int& ri1 = i;  // reference to a variable \n    i = 99;        // change the integer thru the variable \n    *pi = 101;     // change the integer thru the pointer \n    ri1 = -1;      // change the integer thru the reference \n    int& ri2 {i};  // another reference to the variable \n    int j = 1000; \n    pi = &j;       // point to another integer\n```\n\n在这段代码中，一个变量被声明和初始化，然后一个指针被初始化指向这个数据，一个引用被初始化为变量的别名。引用`ri1`用赋值运算符初始化，而引用`ri2`用初始化列表语法初始化。\n\nThe pointer and reference have two different meanings. The reference is not initialized to the value of the variable, the variable's data; it is an alias for the variable name.\n\n无论在哪里使用变量，都可以使用引用；无论您对引用做什么，实际上都与对变量执行相同的操作相同。指针指向数据，因此您可以通过取消指针引用来更改数据，但同样，您也可以使指针指向任何数据，并通过取消指针引用来更改该数据(这在前面代码的最后两行中有说明)。一个变量可以有多个别名，每个别名都必须在声明时初始化为该变量。一旦声明，就不能使引用引用不同的对象。\n\n以下代码不会编译:\n\n```cpp\n    int& r1;           // error, must refer to a variable \n    int& r2 = nullptr; // error, must refer to a variable\n```\n\n因为引用是另一个变量的别名，所以如果没有初始化为变量，它就不能存在。同样，您不能将其初始化为除变量名之外的任何东西，因此不存在*空引用*的概念。\n\n一旦初始化，引用就只是一个变量的别名。实际上，当您将引用用作任何运算符的操作数时，操作是在变量上执行的:\n\n```cpp\n    int x = 1, y = 2;  \n    int& rx = x; // declaration, means rx is an alias for x \n    rx = y;      // assignment, changes value of x to the value of y\n```\n\n在这段代码中，`rx`是变量`x`的别名，所以最后一行的赋值只是给`x`赋值`y`:赋值是对别名变量进行的。此外，如果你取一个引用的地址，你会得到它引用的变量的地址。虽然可以有对数组的引用，但不能有引用数组。\n\n# 常量引用\n\n到目前为止使用的引用允许您更改它是别名的变量，因此它具有左值语义。还有`const`左值引用，也就是对一个可以读，但不能写的对象的引用。\n\n与`const`指针一样，在左值引用上使用`const`关键字声明`const`引用。这基本上使引用成为只读的:您可以访问变量的数据来读取它，但不能更改它。\n\n```cpp\n    int i = 42; \n    const int& ri = i; \n    ri = 99;           // error!\n```\n\n# 返回引用\n\n有时一个对象会被传递给一个函数，而函数的语义是该对象应该被返回。这方面的一个例子是与流对象一起使用的`<<`运算符。对该操作员的呼叫是*链式的*:\n\n```cpp\n    cout << \"The value is \" << 42;\n```\n\n这实际上是对名为`operator<<`的函数的一系列调用，一个使用`const char*`指针，另一个使用`int`参数。这些函数还有一个用于将要使用的流对象的`ostream`参数。然而，如果这仅仅是一个`ostream`参数，那么这将意味着该参数将被复制，并且插入将在该副本上执行。流对象通常使用缓冲，因此对流对象副本的更改可能不会产生预期的效果。此外，为了启用插入操作符的*链接*，插入函数将返回作为参数传递的流对象。目的是通过多个函数调用传递同一个流对象。如果这样的函数返回一个对象，那么它将是一个副本，这不仅意味着一系列的插入将涉及大量的副本，这些副本也将是临时的，因此对流的任何更改(例如，操纵器，如`std::hex`)都不会持续。为了解决这些问题，使用了引用。这种功能的典型原型是:\n\n```cpp\n    ostream& operator<<(ostream& _Ostr, int _val);\n```\n\n显然，您必须小心返回引用，因为您必须确保对象生存期与引用持续的时间一样长。这个`operator<<`函数将返回在第一个参数中传递的引用，但是在下面的代码中，一个引用被返回到一个自动变量:\n\n```cpp\n    string& hello() \n    { \n        string str (\"hello\"); \n        return str; // don't do this! \n    }   // str no longer exists at this point\n```\n\n在前面的代码中，`string`对象只和函数一样长，所以这个函数返回的引用会引用一个不存在的对象。当然，您可以返回对函数中声明的`static`变量的引用。\n\n从函数中返回引用是一个常见的习惯用法，但是每当您考虑这样做时，请确保别名变量的生存期不在函数的范围内。\n\n# 临时和参考文献\n\n左值引用必须引用一个变量，但是 C++ 在引用堆栈上声明的`const`引用时有一些奇怪的规则。如果引用是`const`，编译器将在引用的生存期内延长临时的生存期。例如，如果使用初始化列表语法，编译器将创建一个临时的:\n\n```cpp\n    const int& cri { 42 };\n```\n\n在这段代码中，编译器将创建一个临时的`int`并将其初始化为一个值，然后将其别名为`cri`引用(重要的是该引用是`const`)。当临时处于范围内时，可通过引用获得。这可能看起来有点奇怪，但是考虑在该函数中使用`const`引用:\n\n```cpp\n    void use_string(const string& csr);\n```\n\n您可以使用`string`变量调用该函数，该变量将显式转换为`string`或使用`string`文字:\n\n```cpp\n    string str { \"hello\" }; \n    use_string(str);      // a std::string object \n    const char *cstr = \"hello\"; \n    use_string(cstr);     // a C string can be converted to a std::string \n    use_string(\"hello\");  // a literal can be converted to a std::string\n```\n\n在大多数情况下，您不会希望有一个对内置类型的`const`引用，但是对于定制类型来说，复制会有开销，这是一个优势，正如您在这里看到的，如果需要，编译器将返回到创建临时的。\n\n# 右值引用\n\nC++ 11 定义了一种新的引用类型，右值引用。在 C++ 11 之前，代码(比如赋值运算符)无法判断传递给它的右值是否是临时对象。如果这样的函数被传递了一个对象的引用，那么函数必须小心不要改变引用，因为这会影响它所引用的对象。如果引用的是一个临时对象，那么函数可以对临时对象做它喜欢做的事情，因为在函数完成后，对象将不会存在。C++ 11 允许您专门为临时对象编写代码，因此在赋值的情况下，临时对象的操作符只需*将数据从临时对象移动到正在被赋值的对象中。相比之下，如果引用不是临时对象，则数据必须被*复制*。如果数据很大，这就防止了潜在的昂贵的分配和复制。这使得所谓的*移动语义*成为可能。*\n\n考虑一下这段相当做作的代码:\n\n```cpp\n    string global{ \"global\" }; \n\n    string& get_global() \n    { \n        return global; \n    } \n\n    string& get_static() \n    { \n        static string str { \"static\" }; \n        return str; \n    } \n\n    string get_temp() \n    { \n        return \"temp\"; \n    }\n```\n\n这三个函数返回一个`string`对象。在前两种情况下，`string`具有程序的生存期，因此可以返回一个引用。在最后一个函数中，该函数返回一个字符串，因此构建了一个临时的`string`对象。这三个都可以用来提供`string`值。例如:\n\n```cpp\n    cout << get_global() << endl; \n    cout << get_static() << endl; \n    cout << get_temp() << endl;\n```\n\n这三个都可以提供一个字符串，用于分配一个`string`对象。重要的一点是，前两个函数沿着一个活动对象返回，但是第三个函数返回一个临时对象，但是这些对象可以被相同地使用。\n\n如果这些函数返回了对一个大对象的访问，您就不会希望将该对象传递给另一个函数，因此，在大多数情况下，您会希望将这些函数返回的对象作为引用传递。例如:\n\n```cpp\n    void use_string(string& rs);\n```\n\n引用参数阻止字符串的另一个副本。然而，这只是故事的一半。`use_string`功能可以操纵字符串。例如，下面的函数根据参数创建了一个新的`string`，但是用下划线替换了字母 A、B 和 O(表示没有这些字母的单词中的空格，复制了没有血型 A、B 和 O 的人的生活)。一个简单的实现如下所示:\n\n```cpp\n    void use_string(string& rs) \n    { \n        string s { rs }; \n        for (size_t i = 0; i < s.length(); ++ i) \n        { \n            if ('a' == s[i] || 'b' == s[i] || 'o' == s[i])  \n            s[i] = '_'; \n        } \n        cout << s << endl; \n    }\n```\n\n字符串对象有一个索引操作符(`[]`)，因此您可以将其视为一个字符数组，既可以读取字符的值，也可以为字符位置赋值。`string`的大小通过`length`功能获得，该功能返回一个`unsigned int` ( `typedef`到`size_t`)。由于参数是一个引用，这意味着对`string`的任何更改都将反映在传递给函数的`string`中。这段代码的目的是保持其他变量不变，所以它首先复制了参数。然后在副本上，代码遍历所有字符，在打印结果之前将`a`、`b`和`o`字符改为下划线。\n\n这个代码显然有一个复制开销——从引用创建`string`、`s`、`rs`；但是，如果我们想将类似于`get_global`或`get_static`的字符串传递给这个函数，这是必要的，因为否则将对实际的全局变量和`static`变量进行更改。\n\n但是`get_temp`返回的临时`string`是另外一种情况。这个临时对象只存在于调用`get_temp`的语句结束之前。因此，在知道变量不会影响其他任何东西的情况下，可以对其进行更改。这意味着您可以使用移动语义:\n\n```cpp\n    void use_string(string&& s) \n    { \n        for (size_t i = 0; i < s.length(); ++ i) \n        { \n            if ('a' == s[i] || 'b' == s[i] || 'o' == s[i]) s[i] = '_'; \n        } \n        cout << s << endl; \n    }\n```\n\n这里只有两个变化。首先，使用类型的`&&`后缀将参数标识为右值引用。另一个变化是对引用的对象进行了更改，因为我们知道它是临时的，更改将被丢弃，所以它不会影响其他变量。请注意，现在有两个函数，重载具有相同的名称:一个带有左值引用，一个带有右值引用。当您调用此函数时，编译器将根据传递给它的参数调用正确的函数:\n\n```cpp\n    use_string(get_global()); // string&  version \n    use_string(get_static()); // string&  version \n    use_string(get_temp());   // string&& version \n    use_string(\"C string\");   // string&& version \n    string str{\"C++ string\"}; \n    use_string(str);          // string&  version\n```\n\n回想一下`get_global`和`get_static`返回对将在程序生命周期中存在的对象的引用，因此编译器选择采用左值引用的`use_string`版本。更改是在函数中的临时变量上进行的，这有一个复制开销。`get_temp`返回一个临时对象，因此编译器调用接受右值引用的`use_string`的重载。这个函数会改变引用所引用的对象，但这并不重要，因为对象不会超过行尾的分号。用类似 C 的字符串文字调用`use_string`也是如此:编译器将创建一个临时的`string`对象，并调用具有右值引用参数的重载。在这段代码的最后一个例子中，在堆栈上创建了一个 C++ `string`对象，并将其传递给`use_string`。\n\n编译器发现这个对象是一个左值，并且有可能被改变，所以它调用重载，该重载接受左值引用，该引用以只改变函数中临时局部变量的方式实现。\n\n此示例显示，C++ 编译器将检测参数何时是临时对象，并将使用右值引用调用重载。通常，在编写*复制构造函数*(用于从现有实例创建新的自定义类型的特殊函数)和赋值运算符时使用该工具，以便这些函数可以实现左值引用重载来从参数复制数据，右值引用重载来将数据从临时对象移动到新对象。其他用途是编写自定义类型，即*仅移动*，在这里它们使用不可复制的资源，例如文件句柄。\n\n# 范围为和参考文献\n\n作为一个你可以用引用做什么的例子，值得看看 C++ 11 中的远程`for`工具。下面的代码非常简单；数组`squares`用 0 到 4 的平方初始化:\n\n```cpp\n    constexpr int size = 4; \n    int squares[size]; \n\n    for (int i = 0; i < size; ++ i) \n    { \n        squares[i] = i * i; \n    }\n```\n\n编译器知道数组的大小，所以你可以使用 ranged `for`打印出数组中的值。在下文中，在每次迭代中，局部变量`j`是数组中该项的副本。作为副本，这意味着您可以读取该值，但是对变量所做的任何更改都不会反映到数组中。因此，下面的代码工作正常；它打印出数组的内容:\n\n```cpp\n    for (int j : squares) \n    { \n        cout << J << endl; \n    }\n```\n\n如果您想要更改数组中的值，那么您必须能够访问实际值，而不是副本。在范围`for`中这样做的方法是使用一个引用作为循环变量:\n\n```cpp\n    for (int& k : squares) \n    { \n        k *= 2; \n    }\n```\n\n现在，在每次迭代中，`k`变量是数组中实际成员的别名，所以无论您对`k`变量做什么，实际上都是在数组成员上执行的。在这个例子中，`squares`数组的每个成员都乘以 2。您不能将`int*`用于`k`的类型，因为编译器看到数组中项目的类型是`int`，并将此用作范围内的`for`的循环变量。由于引用是变量的别名，编译器将允许引用作为循环变量，此外，由于引用是别名，您可以使用它来更改实际的数组成员。\n\n对于多维数组来说，Ranged `for`变得很有趣。例如，在下面，声明了一个二维数组，并尝试使用`auto`变量使用嵌套循环:\n\n```cpp\n    int arr[2][3] { { 2, 3, 4 }, { 5, 6, 7} };   \n    for (auto row : arr) \n    { \n        for (auto col : row) // will not compile\n        { \n            cout << col << \" \" << endl; \n        } \n    }\n```\n\n由于二维数组是数组的数组(每一行都是一维数组)，目的是在外循环中获得每一行，然后在内循环中访问该行中的每一项。这种方法有几个问题，但直接的问题是这段代码无法编译。\n\n编译器会抱怨内部循环，说找不到类型`int*`的`begin`或`end`函数。原因是 ranged `for`使用迭代器对象，而对于数组，它使用 C++ 标准库函数`begin`和`end,`来创建这些对象。编译器将从外部范围的`arr`数组中看到每个项目都是一个`int[3]`数组，因此在外部`for`循环中，循环变量将是每个元素的*副本*，在本例中是一个`int[3]`数组。你不能像这样复制数组，所以编译器会提供一个指向第一个元素的指针，一个`int*`，这个在内部`for`循环中使用。\n\n编译器将尝试获取`int*`的迭代器，但这是不可能的，因为`int*`不包含它指向多少项的信息。有一个版本的`begin`和`end`是为`int[3]`(和所有尺寸的阵列)定义的，但不是为`int*`定义的。\n\n一个简单的改变就可以编译这段代码。只需将`row`变量转换为引用即可:\n\n```cpp\n    for (auto& row : arr) \n    { \n        for (auto col : row) \n        { \n            cout << col << \" \" << endl; \n        } \n    }\n```\n\n参考参数表示别名用于`int[3]`数组，当然，别名与元素相同。使用`auto`隐藏了实际情况的丑陋。当然，内部循环变量是一个`int`，因为这是数组中项目的类型。外环变量实际上是`int (&)[3]`。也就是说，它引用了一个`int[3]`(括号用来表示它引用了一个`int[3]`而不是一个`int&`数组)。\n\n# 在实践中使用指针\n\n一个常见的要求是拥有一个可以是任意大小的集合，并且可以在运行时增长和收缩。C++ 标准库提供了各种类来允许您这样做，这将在[第 5 章](05.html)、*使用标准库容器*中描述。以下示例说明了如何实现这些标准集合的一些原则。一般来说，您应该使用 C++ 标准库类，而不是实现自己的类。此外，标准库类*将*代码封装在一个类中，由于我们还没有涉及类，下面的代码将使用可能被错误调用的函数。所以，你应该把这个例子看作是示例代码。链表是一种常见的数据结构。这些通常用于项目顺序很重要的队列。例如，先进先出队列，其中任务按插入队列的顺序执行。在此示例中，每个任务都表示为一个结构，该结构包含任务描述和指向要执行的下一个任务的指针。\n\n如果指向下一个任务的指针是`nullptr`，那么这意味着当前任务是列表中的最后一个任务:\n\n```cpp\n    struct task \n    { \n        task* pNext; \n        string description; \n    };\n```\n\n您可以通过实例使用点运算符访问结构的成员:\n\n```cpp\n    task item; \n    item.descrription = \"do something\";\n```\n\n在这种情况下，编译器将创建一个用字符串`do something`初始化的`string`对象，并将其分配给名为`item`的实例的`description`成员。您也可以使用`new`操作符在免费商店创建一个`task`:\n\n```cpp\n    task* pTask = new task; \n    // use the object \n    delete pTask;\n```\n\n在这种情况下，必须通过指针来访问对象的成员，C++ 提供了`->`操作符来为您提供这种访问:\n\n```cpp\n    task* pTask = new task; \n    pTask->descrription = \"do something\"; \n    // use the object \n    delete pTask;\n```\n\n这里`description`成员被分配给字符串。请注意，由于`task`是一个结构，因此没有访问限制，这一点对于类很重要，并在[第 4 章](04.html)、*类*中进行了描述。\n\n# 创建项目\n\n在`C:\\Beginning_C++ `下创建一个名为`Chapter_04`的新文件夹。启动 Visual C++ 并创建一个 C++ 源文件，并将其保存到您刚刚创建的文件夹中，作为`tasks.cpp`。增加一个没有参数的简单`main`函数，使用 C++ 流提供输入输出支持:\n\n```cpp\n    #include <iostream> \n    #include <string> \n    using namespace std; \n\n    int main() \n    {\n    }\n```\n\n在`main`函数上方，为列表中代表任务的结构添加一个定义:\n\n```cpp\n    using namespace std;  \n struct task { task* pNext; string description; };\n```\n\n这有两个成员。物体的内脏是`description`项。在我们的例子中，执行一个任务将涉及到将`description`项打印到控制台。在实际项目中，您很可能会有许多与任务相关联的数据项，您甚至可能有成员函数来执行任务，但是我们还没有涉及成员函数；这是[第四章](04.html)、*类*的话题。\n\n链表的管道是另一个成员，`pNext`。请注意，`task`结构在`pNext`成员声明时尚未完全定义。这不是问题，因为`pNext`是*指针*。不能有未定义或部分定义类型的数据成员，因为编译器不知道要为其分配多少内存。您可以让指针成员指向部分定义的类型，因为无论指针成员指向什么，其大小都是相同的。\n\n如果我们知道列表中的第一个链接，那么我们就可以访问整个列表，在我们的示例中，这将是一个全局变量。当构造列表时，构造函数需要知道列表的结尾，以便它们可以向列表附加新的链接。同样，为了方便起见，我们将把它作为一个全局变量。在`task`结构的定义后添加以下指针:\n\n```cpp\n task* pHead = nullptr; task* pCurrent = nullptr;  \n    int main() \n    {\n    }\n```\n\n就目前的情况来看，代码没有任何作用，但是这是一个编译文件来测试没有错别字的好机会:\n\n```cpp\ncl /EHsc tasks.cpp\n```\n\n# 向列表中添加任务对象\n\n提供代码的下一步是向任务列表中添加一个新任务。这需要创建一个新的`task`对象，并对其进行适当的初始化，然后通过改变列表中的最后一个链接来将其添加到列表中，以指向新的链接。\n\n在`main`功能之上，增加以下功能:\n\n```cpp\n    void queue_task(const string& name) \n    { \n        ...\n    }\n```\n\n该参数是一个`const`引用，因为我们不会更改该参数，也不希望产生复制开销。该函数必须做的第一件事是创建一个新链接，因此添加以下行:\n\n```cpp\n    void queue_task(const string& name) \n    { \n task* pTask = new task; pTask->description = name; pTask->pNext = nullptr; \n    }\n```\n\n第一行在自由存储上创建一个新的链接，接下来的几行初始化它。这不一定是初始化这样一个对象的最佳方式，更好的机制，构造器，将在[第 4 章](04.html)、*类*中介绍。注意`pNext`项初始化为`nullptr`；这表示链接将位于列表的末尾。\n\n这个函数的最后一部分将链接添加到列表中，也就是说，它使链接成为列表中的最后一个。但是，如果列表为空，则表示该链接也是列表中的第一个*链接。代码必须执行这两个操作。在函数末尾添加以下代码:*\n\n```cpp\n    if (nullptr == pHead) \n    { \n        pHead = pTask; \n        pCurrent = pTask; \n    } \n    else \n    { \n        pCurrent->pNext = pTask; \n        pCurrent = pTask; \n    }\n```\n\n第一行检查列表是否为空。如果`pHead`是`nullptr`，则表示没有其他链接，因此当前链接是第一个链接，因此`pHead`和`pCurrent`都被初始化为新的链接指针。如果列表中存在现有链接，则必须将该链接添加到最后一个链接，因此在`else`子句中，第一行使最后一个链接指向新链接，第二行用新链接指针初始化`pCurrent`，使新链接成为列表中任何新插入的最后一个链接。\n\n通过在`main`函数中调用该函数，项目被添加到列表中。在本例中，我们将对任务进行排队，为房间贴壁纸。这包括去除旧墙纸，填充墙上的任何洞，调整墙壁的大小(用稀释的浆糊涂在墙上，使墙壁变粘)，然后将粘贴的墙纸挂在墙上。您必须按此顺序执行这些任务，不能更改顺序，因此这些任务非常适合链接列表。在`main`功能中添加以下行:\n\n```cpp\n    queue_task(\"remove old wallpaper\"); \n    queue_task(\"fill holes\"); \n    queue_task(\"size walls\"); \n    queue_task(\"hang new wallpaper\");\n```\n\n在最后一行之后，列表已经创建。`pHead`变量指向列表中的第一个项目，您可以通过跟随`pNext`成员从一个链接到下一个链接来访问列表中的任何其他项目。\n\n您可以编译代码，但是没有输出。更糟糕的是，从代码来看，存在内存泄漏。该程序没有代码来`delete`由`new`操作者在自由存储上创建的`task`对象所占用的内存。\n\n# 删除任务列表\n\n遍历列表很简单，你跟随`pNext`指针从一个链接到下一个链接。在此之前，让我们先修复上一节中介绍的内存泄漏。在`main`功能之上，增加以下功能:\n\n```cpp\n    bool remove_head() \n    { \n        if (nullptr == pHead) return false; \n        task* pTask = pHead; \n        pHead = pHead->pNext; \n        delete pTask; \n        return (pHead != nullptr); \n    }\n```\n\n该功能将删除列表开头的链接，并确保`pHead`指针指向下一个链接，该链接将成为列表的新开头。该函数返回一个`bool`值，指示列表中是否还有其他链接。如果此函数返回`false`，则表示整个列表已被删除。\n\n第一行检查是否用空列表调用了这个函数。一旦我们确信列表至少有一个链接，我们就创建这个指针的临时副本。原因是意图是删除第一项并使`pHead`指向下一项，而要做到这一点，我们必须反过来做那些步骤:使`pHead`指向下一项，然后删除`pHead`之前指向的项。\n\n要删除整个列表，您需要遍历链接，这可以使用`while`循环来实现。在`remove_head`功能下方，添加以下内容:\n\n```cpp\n    void destroy_list() \n    { \n        while (remove_head()); \n    }\n```\n\n要删除整个列表，并解决内存泄漏问题，请在主函数的底部添加以下行\n\n```cpp\n destroy_list(); \n    }\n```\n\n现在，您可以编译代码并运行它。但是，您将看不到任何输出，因为所有代码都是创建一个列表，然后删除它。\n\n# 迭代任务列表\n\n下一步是从每个`pNext`指针后面的第一个链接开始迭代列表，直到我们到达列表的末尾。对于访问的每个链接，都应该执行任务。首先编写一个函数，通过打印出任务的描述，然后返回一个指向下一个任务的指针来执行任务。在`main`功能的正上方，添加以下代码:\n\n```cpp\n    task *execute_task(const task* pTask) \n    { \n        if (nullptr == pTask) return nullptr; \n        cout << \"executing \" << pTask->description << endl; \n        return pTask->pNext; \n    }\n```\n\n这里的参数标记为`const`，因为我们不会改变指针指向的`task`对象。这向编译器表明，如果代码试图更改对象，就会出现问题。第一行检查以确保函数没有用空指针调用。如果是，那么下面的行将取消引用无效指针，并导致内存访问错误。最后一行返回指向下一个链接的指针(列表中最后一个链接可以是`nullptr`)，这样就可以在循环中调用该函数。在此函数之后，添加以下内容来迭代整个列表:\n\n```cpp\n    void execute_all() \n    { \n        task* pTask = pHead; \n        while (pTask != nullptr) \n        { \n            pTask = execute_task(pTask); \n        } \n    }\n```\n\n这段代码从开头`pHead`开始，在列表中的每个链接上调用`execute_task`，直到函数返回一个`nullptr`。在`main`函数的末尾添加对该函数的调用:\n\n```cpp\n execute_all(); \n        destroy_list(); \n    }\n```\n\n现在，您可以编译并运行代码。结果将是:\n\n```cpp\n    executing remove old wallpaper\nexecuting fill holes\n executing size walls executing hang new wallpaper\n```\n\n# 插入项目\n\n链接列表的一个优点是，您可以通过只分配一个新项目并更改指向它的适当指针，并使其指向列表中的下一个项目，来将项目插入列表。这与分配一组`task`对象形成对比；如果你想在中间的某个地方插入一个新的项目，你必须为旧项目和新项目分配一个足够大的新数组，然后将旧项目复制到新数组中，在正确的位置复制新项目。\n\n墙纸任务清单的问题是房间里有一些油漆过的木头，正如任何装饰师所知，最好在挂墙纸之前，通常是在给墙壁定尺寸之前，油漆一下木制品。我们需要在填充任何孔洞和确定墙壁尺寸之间插入一项新任务。此外，在你做任何装饰之前，你应该在做任何其他事情之前覆盖房间里的任何家具，所以你需要在开始时添加一个新任务。\n\n第一步是找到我们新任务的位置，油漆木制品。我们将在插入任务之前寻找我们想要的任务。在`main`前增加以下内容:\n\n```cpp\n    task *find_task(const string& name) \n    { \n        task* pTask = pHead; \n\n        while (nullptr != pTask) \n        { \n            if (name == pTask->description) return pTask; \n            pTask = pTask->pNext; \n        }  \n        return nullptr; \n    }\n```\n\n该代码在整个列表中搜索与参数匹配的`description`链接。这是通过使用`string`比较运算符的循环来实现的，如果找到了所需的链接，将返回指向该链接的指针。如果比较失败，循环将循环变量初始化为下一个链接的地址，如果这个地址是`nullptr`，则意味着所需的任务不在列表中。\n\n在主功能中创建列表后，添加以下代码搜索`fill holes`任务:\n\n```cpp\n    queue_task(\"hang new wallpaper\"); \n\n // oops, forgot to paint woodworktask\n    * pTask = find_task(\"fill holes\"); if (nullptr != pTask) { // insert new item after pTask } \n    execute_all();\n```\n\n如果`find_task`函数返回一个有效的指针，那么我们可以在这一点上添加一个项目。\n\n这样做的功能将允许您在您传递给它的列表中的任何项目后添加一个新项目，如果您传递`nullptr`，它将在开头添加新项目。它叫做`insert_after`，但是很明显，如果你经过`nullptr`它也意味着*在开头*之前插入。在`main`功能的正上方添加以下内容:\n\n```cpp\n    void insert_after(task* pTask, const string& name) \n    { \n        task* pNewTask = new task; \n        pNewTask->description = name; \n        if (nullptr != pTask) \n        { \n            pNewTask->pNext = pTask->pNext; \n            pTask->pNext = pNewTask; \n        } \n    }\n```\n\n第二个参数是`const`引用，因为我们不会改变`string`，但是第一个参数不是`const`指针，因为我们将改变它所指向的对象。该函数创建一个新的`task`对象，并将`description`成员初始化为新的任务名称。然后检查传递给函数的`task`指针是否为空。如果不是，则可以在列表中的指定链接后插入新项目*。为此，新链接`pNext`成员被初始化为列表中的下一项，并且先前链接的`pNext`成员被初始化为新链接的地址。*\n\n *当传递函数`nullptr`作为后面要插入的项时，在开头插入一个项怎么样？增加以下`else`条款。\n\n```cpp\n    void insert_after(task* pTask, const string& name) \n    { \n        task* pNewTask = new task; \n        pNewTask->description = name; \n        if (nullptr != pTask) \n        { \n            pNewTask->pNext = pTask->pNext; \n            pTask->pNext = pNewTask; \n        } \n        else { pNewTask->pNext = pHead; pHead = pNewTask; } \n    }\n```\n\n在这里，我们使新项目的`pNext`成员指向列表的旧开头，然后将`pHead`更改为指向新项目。\n\n现在，在`main`功能中，您可以添加一个调用来插入一个新任务来油漆木制品，由于我们也忘记了指出最好在用防尘布覆盖所有家具后装饰房间，所以在列表中添加一个任务来首先完成该任务:\n\n```cpp\n    task* pTask = find_task(\"fill holes\"); \n    if (nullptr != pTask) \n    { \n insert_after(pTask, \"paint woodwork\"); \n    } \n insert_after(nullptr, \"cover furniture\");\n```\n\n现在可以编译代码了。当您运行代码时，您应该看到按要求的顺序执行的任务:\n\n```cpp\n executing cover furniture executing remove old wallpaper\nexecuting fill holes\nexecuting paint woodwork\nexecuting size walls\nexecuting hang new wallpaper\n```\n\n# 摘要\n\n可以说，使用 C++ 的主要原因之一是您可以使用指针直接访问内存。这是大多数其他语言的程序员无法做到的一个特性。这意味着作为一名 C++ 程序员，你是一种特殊类型的程序员:一个被信任有记忆的人。在本章中，您已经看到了如何获取和使用指针，以及一些不恰当地使用指针会使您的代码严重出错的例子。\n\n在下一章中，我们将讨论函数，其中将包括另一种类型指针的描述:函数指针。如果你被数据指针和函数指针所信任，你真的是一个特殊类型的程序员。**"
  },
  {
    "path": "docs/mod-cpp/03.md",
    "content": "# 三、使用函数\n\n函数是 C++ 的基础设施；代码包含在函数中，要执行该代码，必须调用函数。C++ 在定义和调用函数的方式上非常灵活:可以用固定数量的参数或可变数量的参数定义函数；您可以编写泛型代码，以便相同的代码可以用于不同的类型；您甚至可以编写类型数量可变的泛型代码。\n\n# 定义 C++ 函数\n\n在最基本的层次上，函数有参数，有操作参数的代码，并返回一个值。C++ 提供了几种方法来确定这三个方面。在下一节中，我们将从声明的左边到右边介绍 C++ 函数的那些部分。功能也可以**模板化**，但这将留给后面的部分。\n\n# 声明和定义函数\n\n一个函数必须只定义一次，但是通过重载，可以有许多同名的函数，它们的参数不同。使用函数的代码必须能够访问函数的名称，因此它需要能够访问函数定义(例如，函数是在源文件中较早定义的)或者函数声明(也称为函数原型)。编译器使用原型类型检查*调用代码*是否正在使用正确的类型调用函数。\n\n通常，库被实现为单独的编译库文件，并且库函数的原型被提供在头文件中，以便许多源文件可以通过包含头文件来使用这些函数。但是，如果您知道函数名、参数和返回类型，您可以自己在文件中键入原型。\n\n无论您做什么，您都只是为编译器提供信息，以对调用函数的表达式进行类型检查。链接器负责在库中定位函数，并将代码复制到可执行文件中，或者设置基础结构以使用共享库中的函数。包含库的头文件并不意味着您可以使用该库中的函数，因为在标准 C++ 中，头文件没有包含函数的库的信息。\n\nVisual C++ 提供了一个名为`comment`的`pragma`，它可以与`lib`选项一起使用，作为链接器链接特定库的消息。所以头文件中的`#pragma comment(lib, \"mylib\")`会告诉链接器与`mylib.lib`链接。一般来说，最好使用项目管理工具，如 **nmake** 或 **MSBuild** ，以确保项目中链接了正确的库。\n\n大多数 C 运行时库都是这样实现的:函数在静态库或动态链接库中编译，函数原型在头文件中提供。您可以在链接器命令行中提供库，并且通常会包含库的头文件，以便编译器可以使用函数原型。只要链接器知道这个库，就可以在代码中键入原型(并将其描述为*外部链接*，这样编译器就知道这个函数是在其他地方定义的)。这可以让你避免在源文件中包含一些大文件，这些文件大部分都是你不会用到的函数原型。\n\n然而，许多 C++ 标准库是在头文件中实现的，这意味着这些文件可能相当大。通过将这些头文件包含在预编译头中，可以节省编译时间。\n\n到目前为止，在本书中，我们使用了一个源文件，所以所有的函数都被定义在它们被使用的同一个文件中，并且我们在调用它之前已经定义了函数，也就是说，函数是在调用它的代码上面*定义的。只要在调用函数之前定义了函数原型，就不必在使用函数之前定义函数:*\n\n```cpp\n    int mult(int, int); \n\n    int main() \n    { \n        cout << mult(6, 7) << endl; \n        return 0; \n    } \n\n    int mult(int lhs, int rhs) \n    { \n        return lhs * rhs; \n    }\n```\n\n`mult`函数是在`main`函数之后定义的，但是这段代码会编译，因为原型是在`main`函数之前给出的。这叫做**前进宣言**。原型不必有参数名。这是因为编译器只需要知道参数的类型，而不需要知道它们的名称。但是，由于参数名称应该是自文档化的，所以给出参数名称通常是一个好主意，这样您就可以看到函数的用途。\n\n# 指定链接\n\n在上例中，函数是在同一个源文件中定义的，因此存在*内部链接*。如果在另一个文件中定义了函数，那么原型将具有*外部链接*，因此原型必须这样定义:\n\n```cpp\n    extern int mult(int, int);        // defined in another file\n```\n\n`extern`关键字是可以添加到函数声明中的许多说明符之一。例如`static`说明符可以在原型上使用，表示函数有内部链接，名称只能在当前源文件中使用。在前面的例子中，在原型中将功能标记为`static`是合适的。\n\n```cpp\n    static int mult(int, int);        // defined in this file\n```\n\n您也可以将函数声明为`extern \"C\"`，这将影响函数名称在对象文件中的存储方式。这对图书馆来说很重要，我们将很快介绍。\n\n# 内联\n\n如果一个函数计算了一个可以在编译时计算的值，可以用`constexpr`标记在声明的左边，表示编译器可以通过在编译时计算该值来优化代码。如果函数值可以在编译时计算，这意味着函数调用中的参数必须在编译时已知，因此它们必须是文本。函数也必须是单行的。如果不满足这些限制，编译器可以忽略说明符。\n\n相关的是`inline`说明符。这可以放在函数声明的左边，作为对编译器的一个建议，当其他代码调用函数时，而不是编译器在内存中插入函数的跳转(以及创建堆栈框架)，编译器应该在调用函数中放入实际代码的副本。同样，编译器可以忽略这个说明符。\n\n# 确定返回类型\n\n可以编写函数来运行例程，而不返回值。如果是这种情况，必须指定函数返回`void`。在大多数情况下，函数将返回一个值，如果只是为了指示函数已经正确完成的话。不要求调用函数获取返回值或对其做任何事情。调用函数可以简单地忽略返回值。\n\n有两种方法可以指定返回类型。第一种方法是在函数名之前给出类型。这是迄今为止大多数例子中使用的方法。第二种方法称为**尾随返回类型**，要求您将`auto`作为返回类型放在函数名之前，并使用`->`语法在参数列表之后给出实际的返回类型:\n\n```cpp\n    inline auto mult(int lhs, int rhs) -> int \n    { \n        return lhs * rhs; \n    }\n```\n\n这个函数非常简单，很适合内联。左边的返回类型给出为`auto`，表示实际返回类型在参数表后指定。`-> int`表示返回类型为`int`。该语法与左侧使用`int`的效果相同。当函数是模板化的并且返回类型可能不明显时，此语法很有用。\n\n在这个简单的例子中，可以完全省略返回类型，只需使用函数名左边的`auto`。该语法意味着编译器将从返回的实际值中推导出返回类型。很明显，编译器将只知道函数体的返回类型，所以您不能为这样的函数提供原型。\n\n最后，如果一个函数根本不返回(例如，如果它进入一个永无止境的循环来轮询某个值)，您可以用 C++ 11 属性`[[noreturn]]`来标记它。编译器可以使用这个属性来编写更高效的代码，因为它知道它不需要提供代码来返回值。\n\n# 命名函数\n\n通常，函数名对变量有相同的规则:它们必须以字母或下划线开头，并且不能包含空格或其他标点符号。遵循自文档化代码的一般原则，您应该根据函数的功能来命名函数。有一个例外，这些是用于为运算符(主要是标点符号)提供重载的特殊函数。这些函数以`operatorx`的形式命名，其中`x`是您将在代码中使用的运算符。后面一节将解释如何用全局函数实现运算符。\n\n运算符是重载的一个例子。您可以重载任何函数，即使用相同的名称，但为实现提供不同的参数类型或不同数量的参数。\n\n# 功能参数\n\n函数可能没有参数，在这种情况下，函数是用一对空括号定义的。函数定义必须在括号中给出参数的类型和名称。在许多情况下，函数将有固定数量的参数，但是您可以编写具有可变数量参数的函数。您也可以为某些参数定义具有默认值的函数，实际上，提供了一个函数，该函数根据传递给该函数的参数数量来重载自身。变量参数列表和默认参数将在后面介绍。\n\n# 指定例外\n\n还可以标记函数，以指示它们是否会引发异常。关于异常的更多细节将在[第 7 章](07.html)、*诊断和调试*中给出，但是有两个语法你需要注意。\n\nC++ 的早期版本允许您以三种方式在函数上使用`throw`说明符:首先，您可以提供一个逗号分隔的列表，列出函数中代码可能引发的异常类型；其次，可以提供省略号(`...`)，表示函数可能抛出任何异常；第三，您可以提供一对空括号，这意味着函数不会抛出异常。语法如下所示:\n\n```cpp\n    int calculate(int param) throw(overflow_error) \n    { \n        // do something which potentially may overflow \n    }\n```\n\n`throw`说明符在 C++ 11 中已经被弃用，主要是因为指示异常类型的能力没有用。然而，表明不会抛出异常的版本`throw`被发现是有用的，因为它使编译器能够通过不提供处理异常的代码基础设施来优化代码。C++ 11 使用`noexcept`说明符保留了这个行为:\n\n```cpp\n    // C++ 11 style: \n    int increment(int param) noexcept \n    { \n        // check the parameter and handle overflow appropriately \n    }\n```\n\n# 功能体\n\n在确定了返回类型、函数名和参数之后，您需要定义函数的主体。函数的代码必须出现在一对大括号(`{}`)之间。如果函数返回值，那么函数必须至少有一行(函数中的最后一行)带有`return`语句。这必须返回适当的类型或可以隐式转换为函数返回类型的类型。如前所述，如果函数被声明为返回`auto`，那么编译器将推导出返回类型。在这种情况下，所有的`return`语句*必须*返回相同的类型。\n\n# 使用函数参数\n\n调用函数时，编译器检查函数的所有重载，以找到与调用代码中的参数匹配的重载。如果没有完全匹配，则执行标准和用户定义的类型转换，因此调用代码提供的值可能与参数的类型不同。\n\n默认情况下，参数按值传递并复制，这意味着参数在函数中被视为局部变量。函数的作者可以决定通过引用传递参数，或者通过指针，或者通过 C++ 引用。**通过引用传递**意味着函数可以改变调用代码中的变量，但这可以通过设置参数`const`来控制，在这种情况下，通过引用传递的原因是为了防止复制(可能成本很高)。内置数组总是作为指向数组第一项的指针传递。编译器将在需要时创建临时对象。例如，当一个参数是一个`const`引用，并且调用代码传递一个文字时，一个临时对象被创建，并且只对函数中的代码可用:\n\n```cpp\n    void f(const float&); \n    f(1.0);              // OK, temporary float created \n    double d = 2.0; \n    f(d);                // OK, temporary float created\n```\n\n# 传递初始化列表\n\n如果初始值设定项列表可以转换为参数的类型，则可以将该列表作为参数传递。例如:\n\n```cpp\n    struct point { int x; int y; }; \n\n    void set_point(point pt); \n\n    int main() \n    { \n        point p; \n        p.x = 1; p.y = 1; \n        set_point(p); \n        set_point({ 1, 1 });  \n        return 0; \n    }\n```\n\n这段代码定义了一个有两个成员的结构。在`main`函数中，`point`的新实例在堆栈上创建，并通过直接访问成员进行初始化。然后，实例被传递给一个具有`point`参数的函数。由于`set_point`的参数是按值传递的，编译器在函数的堆栈上创建一个结构的副本。`set_point`的第二次调用也是如此:编译器将在函数的堆栈上创建一个临时的`point`对象，并用初始化列表中的值初始化它。\n\n# 使用默认参数\n\n在某些情况下，您有一个或多个参数的值被频繁使用，以至于您希望它们被视为参数的默认值，同时仍然可以选择允许调用方在必要时提供不同的值。为此，在定义的参数列表中提供默认值:\n\n```cpp\n    void log_message(const string& msg, bool clear_screen = false) \n    { \n        if (clear_screen) clear_the_screen(); \n        cout << msg << endl; \n    }\n```\n\n在大多数情况下，该功能预期用于打印单个消息，但有时用户可能希望首先清除屏幕(例如，对于第一条消息，或者在预定的行数之后)。为了适应此功能的使用，`clear_screen`参数被赋予默认值`false`，但是调用者仍然可以选择传递一个值:\n\n```cpp\n    log_message(\"first message\", true); \n    log_message(\"second message\"); \n    bool user_decision = ask_user(); \n    log_message(\"third message\", user_decision);\n```\n\n请注意，默认值出现在函数定义中，而不是函数原型中，因此如果在头文件中声明了`log_message`函数，那么原型应该是:\n\n```cpp\n    extern void log_message(const string& msg, bool clear_screen);\n```\n\n可以有默认值的参数是最右边的参数。\n\n您可以将每个具有默认值的参数视为代表函数的单独重载，因此从概念上来说`log_message`函数应该被视为两个函数:\n\n```cpp\n    extern void log_message(const string& msg, bool clear_screen); \n    extern void log_message(const string& msg); // conceptually\n```\n\n如果您定义了一个只有一个`const string&`参数的`log_message`函数，那么编译器将不知道是调用该函数还是给`clear_screen`一个默认值`false`的版本。\n\n# 可变参数数量\n\n具有默认参数值的函数可以被视为具有可变数量的用户提供的参数，如果调用者选择不提供值，那么您在编译时就知道参数的最大数量及其值。C++ 还允许您编写参数数量和传递给函数的值不确定的函数。\n\n有三种方法可以获得可变数量的参数:初始化列表、C 风格的变量参数列表和变量模板化函数。这三个中的后一个将在本章后面讨论，一旦模板化的函数已经被覆盖。\n\n# 初始化列表\n\n到目前为止，在本书中，初始化列表被视为一种 C++ 11 构造，有点像内置数组。事实上，当您使用使用大括号的初始化列表语法时，编译器实际上创建了模板化的`initialize_list`类的实例。如果初始化列表用于初始化另一个类型(例如，初始化一个`vector`，编译器用括号之间给出的值创建一个`initialize_list`对象，容器对象使用`initialize_list`迭代器初始化。这种从支撑初始化列表中创建`initialize_list`对象的能力可用于给函数提供可变数量的参数，尽管所有参数必须是相同的类型:\n\n```cpp\n    #include <initializer_list> \n\n    int sum(initializer_list<int> values) \n    { \n        int sum = 0; \n        for (int i : values) sum += i; \n        return sum; \n    } \n\n    int main() \n    { \n        cout << sum({}) << endl;                       // 0 \n        cout << sum({-6, -5, -4, -3, -2, -1}) << endl; // -21 \n        cout << sum({10, 20, 30}) << endl;             // 60 \n        return 0; \n    }\n```\n\n`sum`函数只有一个参数`initializer_list<int>`，只能用整数列表初始化。`initializer_list`类的功能很少，因为它的存在只是为了访问支撑列表中的值。值得注意的是，它实现了返回列表中项目数量的`size`函数，以及返回指向列表中第一个项目和最后一个项目之后的位置的指针的`begin`和`end`函数。这两个函数是让迭代器访问列表所必需的，它使您能够使用带有 ranged- `for`语法的对象。\n\nThis is typical in the C++ Standard Library. If a container holds data in a contiguous block of memory, then pointer arithmetic can use the pointer to the first item and a pointer immediately after the last item to determine how many items are in the container. Incrementing the first pointer gives sequential access to every item, and pointer arithmetic allows random access. All containers implement a `begin` and `end` function to give access to the container *iterators*.\n\n在这个例子中，`main`函数调用这个函数三次，每次都有一个支撑初始化列表，这个函数将返回列表中项目的总和。\n\n显然，这种技术意味着*变量*参数列表中的每一项都必须是相同的类型(或者可以转换为指定类型的类型)。如果参数是`vector`，你会得到同样的结果；区别在于一个`initializer_list`参数需要较少的初始化。\n\n# 参数列表\n\nC++ 从 C 继承了参数列表的思想。为此，您可以使用省略号语法(`...`)作为最后一个参数，以指示调用方可以提供零个或多个参数。编译器将检查函数是如何被调用的，并将在堆栈上为这些额外的参数分配空间。要访问额外的参数，您的代码必须包含`<cstdarg>`头文件，该文件包含宏，您可以使用这些宏从堆栈中提取额外的参数。\n\n这本质上是类型不安全的，因为编译器无法检查函数在运行时从堆栈中获取的参数是否与调用代码放入堆栈的参数类型相同。例如，下面是一个将对整数求和的函数的实现:\n\n```cpp\n    int sum(int first, ...) \n    { \n        int sum = 0;    \n        va_list args; \n        va_start(args, first); \n        int i = first; \n        while (i != -1) \n        { \n            sum += i; \n            i = va_arg(args, int); \n        } \n        va_end(args); \n        return sum; \n    }\n```\n\n函数的定义必须至少有一个参数，这样宏才能工作；在这种情况下，该参数被称为`first`。重要的是，您的代码以一致的状态离开堆栈，这是使用`va_list`类型的变量来实现的。通过调用`va_start`宏在函数开始时初始化该变量，通过调用`va_end`宏在函数结束时将堆栈恢复到其先前状态。\n\n这个函数中的代码只是遍历参数列表，并保持一个和，当参数的值为-1 时，循环结束。没有宏来给出堆栈上有多少参数的信息，也没有宏来给出堆栈上参数类型的指示。您的代码必须假定变量的类型，并在`va_arg`宏中提供所需的类型。在这个例子中，调用`va_arg`，假设堆栈上的每个参数都是一个`int`。\n\n一旦从堆栈中读取了所有参数，代码在返回总和之前调用`va_end`。这个函数可以这样调用:\n\n```cpp\n    cout << sum(-1) << endl;                       // 0 \n    cout << sum(-6, -5, -4, -3, -2, -1) << endl;   // -20 !!! \n    cout << sum(10, 20, 30, -1) << endl;           // 60\n```\n\n由于`-1`用来表示列表的结束，意味着要对零个数的参数求和，至少要传递一个参数，那就是`-1`。此外，第二行显示，如果您正在传递负数列表(在这种情况下`-1`不能是参数)，您会遇到问题。这个问题可以通过选择另一个*标记值*来解决。\n\n另一个实现可以避免在列表末尾使用标记，而是使用第一个必需的参数来给出后面参数的计数:\n\n```cpp\n    int sum(int count, ...) \n    { \n        int sum = 0; \n        va_list args; \n        va_start(args, count); \n        while(count--) \n        { \n            int i = va_arg(args, int); \n            sum += i; \n        } \n        va_end(args); \n        return sum; \n    }\n```\n\n这一次，第一个值是后面的参数*数量*，因此例程将从堆栈中提取整数的精确数量并对它们求和。代码是这样调用的:\n\n```cpp\n    cout << sum(0) << endl;                         // 0 \n    cout << sum(6, -6, -5, -4, -3, -2, -1) << endl; // -21 \n    cout << sum(3, 10, 20, 30) << endl;             // 60\n```\n\n对于如何处理确定传递了多少参数的问题，没有约定。\n\n例程假设堆栈上的每一项都是一个`int`，但是在函数的原型中没有关于这个的信息，所以编译器不能对实际用来调用函数的参数进行类型检查。如果调用者提供了不同类型的参数，可能会从堆栈中读取错误的字节数，使得对`va_arg`的所有其他调用的结果无效。考虑一下:\n\n```cpp\n    cout << sum(3, 10., 20, 30) << endl;\n```\n\n同时按下逗号和句点键很容易，在输入`10`参数后就发生了这种情况。句点表示`10`是一个`double`，因此编译器会在堆栈上放置一个`double`值。当该函数使用`va_arg`宏从堆栈中读取值时，它会将 8 字节`double`读取为两个 4 字节`int`值，对于 Visual C++ 生成的代码，这将导致`1076101140`的总和。这说明了参数列表的类型不安全方面:您无法让编译器对传递给函数的参数进行类型检查。\n\n如果您的函数有不同的类型传递给它，那么您必须实现一些机制来确定这些参数是什么。参数列表的一个很好的例子是 C `printf`函数:\n\n```cpp\n    int printf(const char *format, ...);\n```\n\n这个函数所需的参数是一个格式字符串，重要的是它有一个变量参数及其类型的有序列表。格式字符串提供了通过`<cstdarg>`宏无法获得的信息:变量参数的数量和每个变量参数的类型。`printf`函数的实现将遍历格式字符串，当遇到参数的格式说明符(以`%`开头的字符序列)时，它将使用`va_arg`从堆栈中读取期望的类型。应该清楚的是，C 风格的参数列表并不像第一眼看到时那么灵活；此外，它们可能相当危险。\n\n# 功能特征\n\n函数是模块化的代码片段，被定义为应用的一部分，或者在库中。如果一个函数是由另一个供应商编写的，那么重要的是，您的代码要按照供应商指定的方式调用该函数。这意味着理解所使用的调用约定以及它如何影响堆栈。\n\n# 调用栈\n\n当您调用一个函数时，编译器将为新的函数调用创建一个堆栈框架，并将项目推送到堆栈上。放在堆栈上的数据取决于您的编译器，以及代码是为调试还是发布版本而编译的；但是，通常会有传递给函数的参数、返回地址(函数调用后的地址)和函数中分配的自动变量的信息。\n\n这意味着，当您在运行时进行函数调用时，在函数运行之前创建堆栈帧会产生内存开销和性能开销，在函数完成之后，在清理时会产生性能开销。如果函数是内联的，则不会产生这种开销，因为函数调用将使用当前的堆栈帧，而不是新的堆栈帧。显然，内联函数应该很小，无论是在代码方面还是在堆栈上使用的内存方面。编译器可以忽略`inline`说明符，用单独的栈帧调用函数。\n\n# 指定调用约定\n\n当您的代码使用自己的函数时，您不需要注意*调用约定*，因为编译器会确保使用适当的约定。但是，如果您正在编写可以被其他 C++ 编译器甚至其他语言使用的库代码，那么调用约定就变得很重要。由于这本书不是关于可互操作的代码，我们不会深入讨论，而是会关注两个方面:函数命名和堆栈维护。\n\n# 使用 C 链接\n\n当您给 C++ 函数起一个名字时，这是您将在 C++ 代码中用来调用该函数的名字。但是，在封面下，C++ 编译器会用返回类型和参数的额外符号来修饰*名称，这样重载函数都有不同的名称。对于 C++ 开发人员来说，这也就是大家熟知的**名字“mangling** ”。*\n\n *如果需要通过共享库导出函数(在 Windows 中是**动态链接库**，必须使用其他语言可以使用的类型和名称。为此，您可以用`extern \"C\"`标记一个功能。这意味着该函数具有 C 链接，编译器不会使用 C++ 名称 mangling。显然，您应该只在将由外部代码使用的函数上使用它，而不应该在具有使用 C++ 自定义类型的返回值和参数的函数上使用它。\n\n但是，如果这样的函数确实返回 C++ 类型，编译器只会发出警告。原因是 C 是一种灵活的语言，一个 C 程序员将能够解决如何将 C++ 类型转化为可用的东西，但是这样滥用它们是不良的做法！\n\nThe `extern \"C\"` linkage can also be used with global variables, and you can use it on a single item or (using braces) on many items.\n\n# 指定如何维护堆栈\n\nVisual C++ 支持六种调用约定，可用于函数。`__clrcall`说明符意味着该函数应该作为. NET 函数调用，并允许您编写混合了本机代码和托管代码的代码。C++/CLR(微软的语言扩展，以 C++ 编写。NET 代码)超出了本书的范围。其他五个用于指示参数如何传递给函数(在堆栈上或使用中央处理器寄存器)，以及谁负责维护堆栈。我们将只讨论三个:`__cdecl`、`__stdcall`和`__thiscall`。\n\n你很少会明确使用`__thiscall`；它是用于定义为自定义类型成员的函数的调用约定，表示函数有一个隐藏的参数，该参数是指向可以通过函数中的`this`关键字访问的对象的指针。更多的细节将在下一章给出，但是重要的是要意识到这样的成员函数有不同的调用约定，尤其是当你需要初始化函数指针的时候。\n\n默认情况下，C++ 全局函数将使用`__cdecl`调用约定。堆栈由调用代码维护，因此在调用代码中，对`__cdecl`函数的每次调用后面都有清理堆栈的代码。这使得每个函数调用稍微大一点，但是需要使用变量参数列表。`__stdcall`调用约定被大多数的 Windows SDK 函数使用，它表示被调用的函数清理堆栈，所以不需要在调用代码中生成这样的代码。显然，编译器知道某个函数使用`__stdcall`很重要，因为否则，它将生成代码来清理已经被该函数清理的堆栈帧。您通常会看到标有`WINAPI,`的窗口功能，这是`__stdcall`的`typedef`。\n\n# 使用递归\n\n在大多数情况下，调用堆栈的内存开销并不重要。然而，当您使用递归时，可能会建立一个长长的堆栈框架链。顾名思义，递归就是函数调用自己。一个简单的例子是计算阶乘的函数:\n\n```cpp\n    int factorial(int n) \n    { \n        if (n > 1) return n ∗ factorial(n − 1); \n        return 1; \n    }\n```\n\n如果您将此呼叫设为 4，则会进行以下呼叫:\n\n```cpp\n    factorial(4) returns 4 * factorial(3) \n        factorial(3) returns 3 * factorial(2) \n            factorial(2) returns 2 * factorial(1) \n                factorial(1) returns 1\n```\n\n重要的一点是，在递归函数中，必须至少有一种方法使函数不递归。在这种情况下，将是在参数为 1 的情况下调用`factorial`时。实际上，像这样的函数应该标记为`inline`，以避免创建任何堆栈帧。\n\n# 重载函数\n\n可以有几个同名的函数，但是参数列表不同(参数的数量和/或参数的类型)。这是*重载*的函数名。当调用这样的函数时，编译器将试图找到最适合所提供参数的函数。如果没有合适的函数，编译器将尝试转换参数，以查看是否存在具有这些类型的函数。编译器将从简单的转换开始(例如，指向指针的数组名，指向`const`类型的类型)，如果失败，编译器将尝试提升该类型(例如，`bool`到`int`)。如果失败，编译器将尝试标准转换(例如，对类型的引用)。如果这样的转换导致多个可能的候选，那么编译器将发出函数调用不明确的错误。\n\n# 职能和范围\n\n编译器在寻找合适的函数时也会考虑函数的范围。您不能在函数中定义函数，但是您可以在函数的范围内提供函数原型，编译器将尝试(如果需要，通过转换)首先调用具有这样的原型的函数。请考虑以下代码:\n\n```cpp\n    void f(int i)    { /*does something*/ } \n    void f(double d) { /*does something*/ } \n\n    int main() \n    { \n        void f(double d); \n        f(1); \n        return 0; \n    }\n```\n\n在这段代码中，函数`f`被重载，一个版本采用`int`，另一个版本采用`double`。通常情况下，如果你调用`f(1)`，那么编译器会调用第一个版本的函数。然而在`main`中，有一个原型版本需要一个`double`，一个`int`可以转换成一个`double`而不会丢失信息。原型与函数调用在同一个范围内，所以在这段代码中，编译器将调用采用`double`的版本。这种技术本质上是用`int`参数隐藏版本。\n\n# 删除的功能\n\n有一种比使用作用域更正式的方法来隐藏函数。C++ 将尝试显式转换内置类型。例如:\n\n```cpp\n    void f(int i);\n```\n\n你可以用`int`或者任何可以转换成`int`的东西来称呼它:\n\n```cpp\n    f(1); \n    f('c'); \n    f(1.0); // warning of conversion\n```\n\n在第二种情况下，a `char`是整数，所以提升为 a`int`，调用函数。在第三种情况下，编译器会发出一个警告，指出转换可能会导致数据丢失，但这是一个警告，因此代码将会编译。如果你想阻止这种隐式转换，你可以*删除*你不想让调用者使用的功能。为此，提供一个原型并使用语法`= delete`:\n\n```cpp\n    void f(double) = delete; \n\n    void g() \n    { \n        f(1);   // compiles \n        f(1.0); // C2280: attempting to reference a deleted function \n    }\n```\n\n现在，当代码试图用`char`或`double`(或`float`，将隐式转换为`double`)调用函数时，编译器将发出错误。\n\n# 按值传递和按引用传递\n\n默认情况下，编译器将按值传递参数，即进行复制。如果您传递一个自定义类型，那么它的*复制构造函数*将被调用来创建一个新对象。如果将指针传递给内置类型或自定义类型的对象，则*指针*将按值传递，即在函数堆栈上为参数创建一个新指针，并使用传递给函数的内存地址对其进行初始化。这意味着，在函数中，您可以更改指针以指向其他内存(如果您想在该指针上使用指针算法，这很有用)。指针指向的数据将通过引用传递，也就是说，数据保留在函数之外，但函数可以使用指针来更改数据。类似地，如果在参数上使用引用，那么这意味着对象是由引用传递的。很明显，如果在指针或引用参数上使用`const`，那么这将影响函数是否可以改变指向或引用的数据。\n\n在某些情况下，您可能希望从函数中返回几个值，并且可以选择使用函数的返回值来指示函数是否正确执行。一种方法是将其中一个参数设为 *out* 参数，也就是说，它要么是指针，要么是对函数将更改的对象或容器的引用:\n\n```cpp\n    // don't allow any more than 100 items \n    bool get_items(int count, vector<int>& values) \n    { \n        if (count > 100) return false; \n        for (int i = 0; i < count; ++ i) \n        { \n            values.push_back(i); \n        } \n        return true; \n    }\n```\n\n要调用该函数，必须创建一个`vector`对象，并将其传递给函数:\n\n```cpp\n    vector<int> items {}; \n    get_items(10, items); \n    for(int i : items) cout << i << ' '; \n    cout << endl\n```\n\n因为`values`参数是一个引用，这意味着当`get_values`调用`push_back`在`values`容器中插入一个值时，实际上是将该值插入到`items`容器中。\n\n如果 out 参数是通过指针传递的，那么查看指针声明是很重要的。单个`*`表示变量是指针，两个表示它是指针的指针。以下函数通过输出参数返回`int`:\n\n```cpp\n    bool get_datum(/*out*/ int *pi);\n```\n\n代码是这样调用的:\n\n```cpp\n    int value = 0; \n    if (get_datum(&value)) { cout << \"value is \" << value << endl; } \n    else                   { cout << \"cannot get the value\" << endl;}\n```\n\n这种返回一个指示成功的值的模式经常被使用，特别是对于跨进程或机器边界访问数据的代码。函数返回值可用于给出调用失败原因的详细信息(无网络访问？，无效的安全凭据？，以此类推)，并指示 out 参数中的数据应该被丢弃。\n\n如果 out 参数有一个双`*`，那么它意味着返回值本身就是一个指针，指向一个单值或一个数组:\n\n```cpp\n    bool get_data(/*in/out*/ int *psize, /*out*/ int **pi);\n```\n\n在这种情况下，使用第一个参数传入所需的缓冲区大小，返回时，通过该参数(它是 in/out)和第二个参数中的缓冲区指针接收缓冲区的实际大小:\n\n```cpp\n    int size = 10; \n    int *buffer = nullptr; \n    if (get_data(size, &buffer)) \n    { \n        for (int i = 0; i < size; ++ i) \n        { \n            cout << buffer[i] << endl; \n        } \n        //delete [] buffer; \n    }\n```\n\n任何返回内存缓冲区的函数都必须记录谁负责释放内存。在大多数情况下，它通常是调用者，如本示例代码中假设的那样。\n\n# 设计功能\n\n函数通常会作用于全局数据，或者调用者传入的数据。重要的是，当函数完成时，它会使这些数据保持一致的状态。同样重要的是，函数可以在访问数据之前对数据进行假设。\n\n# 前置和后置条件\n\n函数通常会改变一些数据:传入函数的值、函数返回的数据或一些全局数据。在设计函数时，确定哪些数据将被访问和更改以及这些规则是否被记录是很重要的。\n\n一个函数将有它将使用的数据的前提条件和假设。例如，如果一个函数被传递了一个文件名，意图是该函数将从文件中提取一些数据，那么检查文件是否存在是谁的责任？您可以让它成为函数的责任，因此前几行将检查该名称是否是文件的有效路径，并调用操作系统函数来检查该文件是否存在。但是，如果您有几个函数将对文件执行操作，那么您将在每个函数中复制这个检查代码，最好将这个责任放在调用代码上。显然，这样的操作可能很昂贵，因此避免调用代码和函数来执行检查是很重要的。\n\n[第 7 章](07.html)*诊断与调试*，将描述如何添加调试代码，称为**断言**，您可以在函数中放置这些代码来检查参数值，以确保调用代码遵循您设置的前置条件规则。断言是使用条件编译定义的，因此只会出现在**调试版本**中(即，使用调试信息编译的 C++ 代码)。**发布版本**(将交付给最终用户的完整代码)将有条件地编译断言；这使得代码更快，如果您的测试足够彻底，您可以确保满足先决条件。\n\n您还应该记录您的功能的后置条件。也就是说，关于函数返回的数据的假设(通过函数返回值、out 参数或引用传递的参数)。后置条件是调用代码将做出的假设。例如，您可以返回一个有符号的整数，其中该函数旨在返回一个正值，但负值用于指示错误。通常，如果函数失败，返回指针的函数将返回`nullptr`。在这两种情况下，调用代码都知道它需要检查返回值，并且只有在返回值为正或不为正时才使用它`nullptr`。\n\n# 使用不变量\n\n您应该注意记录函数如何使用函数外部的数据。如果函数的目的是更改外部数据，您应该记录函数将做什么。如果您没有明确记录函数对外部数据的作用，那么您必须确保当函数完成时，这些数据保持不变。原因是调用代码只会假设您在文档中说过的话，并且更改全局数据的副作用可能会导致问题。有时需要存储全局数据的状态，并在函数返回之前将项目返回到该状态。\n\n这方面的一个例子，就是`cout`对象。`cout`对象对您的应用是全局的，可以通过操纵器进行更改，使其以某种方式解释数值。如果您在功能中更改它(例如，通过插入`hex`操纵器)，那么当`cout`对象在功能外使用时，此更改将保持不变。\n\n创建一个名为`read16`的函数，从文件中读取 16 个字节，并将值以十六进制形式输出到控制台，并解释为 ASCII 字符:\n\n```cpp\n    int read16(ifstream& stm) \n    { \n        if (stm.eof()) return -1;  \n\n        int flags = cout.flags(); \n        cout << hex; \n        string line; \n\n        // code that changes the line variable \n\n        cout.setf(flags); \n        return line.length(); \n    }\n```\n\n该代码将`cout`对象的状态存储在临时变量`flags`中。`read16`功能可以以任何必要的方式改变`cout`对象，但是因为我们有存储的状态，这意味着该对象可以在返回之前恢复到其原始状态。\n\n# 函数指针\n\n当应用运行时，它将调用的函数将存在于内存的某个地方。这意味着你可以得到一个函数的地址。C++ 允许使用函数调用运算符(一对括住参数`()`的圆括号)通过函数指针调用函数。\n\n# 记住括号！\n\n首先，用一个简单的例子来说明函数指针是如何在代码中造成难以察觉的错误的。名为`get_status`的全局函数执行各种验证操作，以确定系统状态是否有效。该函数返回零值，表示系统状态有效，零值以上是错误代码:\n\n```cpp\n    // values over zero are error codes \n    int get_status() \n    { \n        int status = 0;  \n        // code that checks the state of data is valid \n        return status; \n    }\n```\n\n代码可以这样调用:\n\n```cpp\n    if (get_status > 0) \n    { \n        cout << \"system state is invalid\" << endl; \n    }\n```\n\n这是一个错误，因为开发人员错过了`()`，所以编译器不会将其视为函数调用。相反，它将此视为对函数内存地址的测试，由于函数永远不会位于零内存地址，因此比较将始终为`true`，即使系统状态有效，也会打印消息。\n\n# 声明函数指针\n\n最后一部分强调了获取函数地址是多么容易:您只需使用不带括号的函数名称:\n\n```cpp\n    void *pv = get_status;\n```\n\n指针`pv`只是轻度兴趣；您现在知道了函数在内存中的存储位置，但是要打印这个地址，您仍然需要将其转换为整数。为了使指针有用，您需要能够声明一个指针，通过它可以调用函数。为了了解如何做到这一点，让我们回到函数原型:\n\n```cpp\n    int get_status()\n```\n\n函数指针必须能够调用不传递任何参数并且期望返回值为整数的函数。函数指针是这样声明的:\n\n```cpp\n    int (*fn)() = get_status;\n```\n\n`*`表示变量`fn`是指针；然而，这是绑定到左边的，所以如果没有围绕`*fn`的括号，编译器会将其解释为声明是针对`int*`指针的。声明的其余部分指出了这个函数指针是如何被调用的:不取任何参数并返回一个`int`。\n\n通过函数指针调用很简单:在通常给出函数名称的地方给出指针的名称:\n\n```cpp\n    int error_value = fn();\n```\n\n再次注意括号有多重要；它们表示在函数指针`fn`中保存的地址上的函数被调用。\n\n函数指针会使代码看起来相当混乱，尤其是当您使用它们来指向模板化函数时，因此代码通常会定义一个别名:\n\n```cpp\n    using pf1 = int(*)();\n    typedef int(*pf2)();\n```\n\n这两行声明了调用`get_status`函数所需的函数指针类型的别名。两者都有效，但是`using`版本更易读，因为很明显`pf1`是正在定义的别名。要了解原因，请考虑这个别名:\n\n```cpp\n    typedef bool(*MyPtr)(MyType*, MyType*);\n```\n\n类型别名叫做`MyPtr`，它是一个返回一个`bool`并接受两个`MyType`指针的函数。这一点用`using`就清楚多了:\n\n```cpp\n    using MyPtr = bool(*)(MyType*, MyType*);\n```\n\n这里的告示牌是`(*)`，表示该类型是一个函数指针，因为您正在使用括号来断开`*`的关联。然后，您可以向外阅读以查看函数的原型:向左查看返回类型，向右获取参数列表。\n\n一旦声明了别名，就可以创建指向函数的指针并调用它:\n\n```cpp\n    using two_ints = void (*)(int, int); \n\n    void do_something(int l, int r){/* some code */} \n\n    void caller() \n    { \n        two_ints fn = do_something; \n        fn(42, 99); \n    }\n```\n\n请注意，因为`two_ints`别名被声明为指针，所以在声明这种类型的变量时不要使用`*`。\n\n# 使用函数指针\n\n函数指针只是一个指针。这意味着您可以将其用作变量；您可以从函数中返回它，或者将其作为参数传递。例如，您可能有一些代码执行一些冗长的例程，并且您希望在例程期间提供一些反馈。为了灵活起见，您可以定义您的函数来获取一个**回调指针**，并在例程中定期调用该函数来指示进度:\n\n```cpp\n    using callback = void(*)(const string&); \n\n    void big_routine(int loop_count, const callback progress) \n    { \n        for (int i = 0; i < loop_count; ++ i) \n        { \n            if (i % 100 == 0) \n            { \n                string msg(\"loop \"); \n                 msg += to_string(i); \n                 progress(msg); \n            } \n            // routine \n        } \n    }\n```\n\n这里`big_routine`有一个函数指针参数叫做`progress`。该函数有一个将被多次调用的循环，每第 100 次循环调用一次回调函数，传递一个给出进度信息的`string`。\n\nNote that the `string` class defines a `+=` operator that can be used to append a string to the end of the `string` in the variable and the `<string>` header file defines a function called `to_string` that is overloaded for each of the built-in types to return a `string` formatted with the value of the function parameter.\n\n该函数将函数指针声明为`const`，只是为了让编译器知道函数指针不应该被更改为指向该函数中另一个函数的指针。代码可以这样调用:\n\n```cpp\n    void monitor(const string& msg) \n    { \n        cout << msg << endl; \n    } \n\n    int main() \n    { \n        big_routine(1000, monitor); \n        return 0; \n    }\n```\n\n`monitor`函数与`callback`函数指针描述的原型相同(例如，如果函数参数是`string&`而不是`const string&,`，则代码不会编译)。然后调用`big_routine`函数，传递一个指向`monitor`函数的指针作为第二个参数。\n\n如果将回调函数传递给库代码，则必须注意函数指针的调用约定。例如，如果将函数指针传递给一个 Windows 函数，如`EnumWindows`，它必须指向一个用`__stdcall`调用约定声明的函数。\n\nC++ 标准使用另一种技术来调用运行时定义的函数，即函子。很快就会谈到。\n\n# 模板函数\n\n当您编写库代码时，您通常必须编写几个函数，这些函数只在传递给函数的类型之间有所不同；常规动作是一样的，只是类型变了。C++ 提供*模板*让你可以写更多的泛型代码；您使用一个*泛型类型*编写例程，在编译时编译器将生成一个具有适当类型的函数。模板化函数使用`template`关键字和尖括号(`<>`)中的参数列表进行标记，这些参数为将要使用的类型提供占位符。重要的是要理解这些模板参数是类型，并引用参数的类型(并返回函数的值)，这些参数将被调用函数所使用的实际类型所替换。它们不是函数的参数，当您调用函数时，您(通常)不会提供它们。\n\n最好用一个例子来解释模板函数。一个简单的`maximum`函数可以这样写:\n\n```cpp\n    int maximum(int lhs, int rhs) \n    { \n        return (lhs > rhs) ? lhs : rhs; \n    }\n```\n\n你可以用其他整数类型来调用这个，较小的类型(`short`、`char`、`bool`等)会提升为`int`，较大类型的值(`long long`)会被截断。类似地，`unsigned`类型的变量将被转换为`signed int`，这可能会导致问题。考虑函数的这个调用:\n\n```cpp\n    unsigned int s1 = 0xffffffff, s2 = 0x7fffffff; \n    unsigned int result = maximum(s1, s2);\n```\n\n`result`变量的值是多少:`s1`还是`s2`？是`s2`。原因是两个值都被转换为`signed int`并且当转换为有符号类型时`s1`将是`-1`的值并且`s2`将是`2147483647`的值。\n\n要处理无符号类型，需要*重载*函数，为有符号和无符号整数编写一个版本:\n\n```cpp\n    int maximum(int lhs, int rhs) \n    { \n        return (lhs > rhs) ? lhs : rhs; \n    } \n\n    unsigned maximum(unsigned lhs, unsigned rhs) \n    { \n        return (lhs > rhs) ? lhs : rhs; \n    }\n```\n\n套路是一样的，只是类型变了。还有一个问题——如果调用者把类型混在一起怎么办？以下表达式有意义吗:\n\n```cpp\n    int i = maximum(true, 100.99);\n```\n\n这段代码将被编译，因为一个`bool`和一个`double`可以被转换成一个`int`，并且第一个重载将被调用。既然这样的调用是无稽之谈，那么如果编译器捕捉到这个错误就更好了。\n\n# 定义模板\n\n返回到`maximum`功能的两个版本，两者的例程相同；改变的只是类型。如果你有一个泛型类型，让我们称之为`T`，其中`T`可以是任何实现`operator>`的类型，例程可以用这个伪代码来描述:\n\n```cpp\n    T maximum(T lhs, T rhs) \n    { \n        return (lhs > rhs) ? lhs : rhs; \n    }\n```\n\n这不会编译，因为我们没有定义类型`T`。模板允许您告诉编译器代码使用了一个类型，并且将根据传递给函数的参数来确定。将编译以下代码:\n\n```cpp\n    template<typename T> \n    T maximum(T lhs, T rhs) \n    { \n        return (lhs > rhs) ? lhs : rhs; \n    }\n```\n\n模板声明指定了将使用`typename`标识符的类型。类型`T`为占位符；您可以使用任何您喜欢的名称，只要它不是在同一范围的其他地方使用的名称，当然，它必须在函数的参数列表中使用。可以用`class`代替`typename`，但是意思是一样的。\n\n您可以调用这个函数，传递任何类型的值，编译器将为该类型创建代码，为该类型调用`operator>`。\n\nIt is important to realize that, the first time the compiler comes across a templated function, it will create a version of the function for the specified type. If you call the templated function for several different types, the compiler will create, or instantiate, a *specialized* function for each of these types.\n\n此模板的定义表明将只使用一种类型，因此您只能使用两个相同类型的参数来调用它:\n\n```cpp\n    int i = maximum(1, 100);\n    double d = maximum(1.0, 100.0);\n    bool b = maximum(true, false);\n```\n\n所有这些都将被编译，前两个将给出预期的结果。最后一行将`b`赋值为`true`，因为`bool`是整数，`true`的值为`1+`，`false`的值为`0`。这可能不是您想要的，因此我们将在稍后返回这个问题。请注意，由于模板规定两个参数必须是相同的类型，因此不会编译以下内容:\n\n```cpp\n    int i = maximum(true, 100.99);\n```\n\n原因是`template`参数表只给出单一类型。如果您想要定义一个具有不同类型参数的函数，那么您必须为模板提供额外的参数:\n\n```cpp\n    template<typename T, typename U> \n    T maximum(T lhs, U rhs) \n    { \n        return (lhs > rhs) ? lhs : rhs; \n    }\n```\n\nThis is done to illustrate how templates work; it really does not make sense to define a maximum function that takes two different types.\n\n这个版本是为两种不同的类型编写的，模板声明提到了两种类型，这两种类型用于两个参数。但是注意，函数返回`T`，第一个参数的类型。这个函数可以这样调用:\n\n```cpp\n    cout << maximum(false, 100.99) << endl; // 1 \n    cout << maximum(100.99, false) << endl; // 100.99\n```\n\n第一行的输出是`1`(或者如果使用`bool alpha`机械手，`true`)，第二行的结果是`100.99`。原因并不明显。在这两种情况下，比较将从函数中返回`100.99`，但是因为返回值的类型是`T`，所以返回值的类型将是第一个参数的类型。在第一种情况下，`100.99`首先被转换为`bool`，由于`100.99`不为零，返回的值为`true`(或`1`)。在第二种情况下，第一个参数是`double`，所以函数返回一个`double`，这意味着返回`100.99`。如果`maximum`的模板版本更改为返回`U`(第二个参数的类型)，那么前面代码返回的值是相反的:第一行返回`100.99`，第二行返回`1`。\n\n注意，当你*调用*模板函数时，你不必给出模板参数的类型，因为编译器会推导出来。需要指出的是，这仅适用于参数。返回类型不是由调用者分配给函数值的变量的类型决定的，因为函数可以在不使用返回值的情况下被调用。\n\n虽然编译器会从您调用函数的方式中推导出模板参数，但是您可以显式地提供被调用函数中的类型来调用特定版本的函数，并(如果需要)让编译器执行隐式转换:\n\n```cpp\n    // call template<typename T> maximum(T,T); \n    int i = maximum<int>(false, 100.99);\n```\n\n这段代码会调用有两个`int`参数的`maximum`版本，返回一个`int`，所以返回值是`100`，也就是`100.99`转换成一个`int`。\n\n# 使用模板参数值\n\n到目前为止定义的模板都有类型作为模板的参数，但是您也可以提供整数值。下面是一个相当做作的例子来说明这一点:\n\n```cpp\n    template<int size, typename T> \n    T* init(T t) \n    { \n        T* arr = new T[size]; \n        for (int i = 0; i < size; ++ i) arr[i] = t; \n        return arr; \n    }\n```\n\n有两个模板参数。第二个参数提供了类型的名称，其中`T`是用于函数参数类型的占位符。第一个参数看起来像函数参数，因为它的使用方式类似。参数`size`可以作为局部(只读)变量在函数中使用。函数参数是`T`，所以编译器可以从函数调用中推导出第二个模板参数，但是它不能推导出第一个参数，所以您*必须在调用中提供一个值。下面是一个为`T`调用`int`的模板函数和为`size`调用`10`的值的例子:*\n\n```cpp\n    int *i10 = init<10>(42); \n    for (int i = 0; i < 10; ++ i) cout << i10[i] << ' '; \n    cout << endl; \n    delete [] i10;\n```\n\n第一行调用函数，以`10`为模板参数，`42`为函数参数。由于`42`是一个`int`，`init`函数将创建一个有十个成员的`int`数组，每个成员初始化为一个值`42`。编译器推导出`int`作为第二个参数，但是这段代码本可以调用带有`init<10,int>(42)`的函数来明确表示您需要一个`int`数组。\n\n非类型参数在编译时必须是常量:值可以是整数(包括枚举)，但不能是浮点。您可以使用整数数组，但是这些数组可以通过模板参数作为指针使用。\n\n虽然在大多数情况下，编译器无法推导出 value 参数，但如果将该值定义为数组的大小，则可以。这可以用来让一个函数看起来可以决定一个内置数组的大小，但是当然不能，因为编译器会为每个需要的大小创建一个函数的版本。例如:\n\n```cpp\n    template<typename T, int N> void print_array(T (&arr)[N]) \n    { \n        for (int i = 0; i < N; ++ i) \n        { \n            cout << arr[i] << endl; \n        } \n    }\n```\n\n这里有两个模板参数:一个是数组的类型，另一个是数组的大小。函数的参数看起来有点奇怪，但它只是一个由引用传递的内置数组。如果不使用括号，那么参数是`T& arr[N]`，也就是一个 N 大小的内置数组，引用类型为`T`的对象，这不是我们想要的。我们想要一个类型为`T`的 N 尺寸内置数组对象。这个函数是这样调用的:\n\n```cpp\n    int squares[] = { 1, 4, 9, 16, 25 }; \n    print_array(squares);\n```\n\n前面代码的有趣之处在于，编译器看到初始值设定项列表中有五项。内置数组有五项，因此调用如下函数:\n\n```cpp\n    print_array<int,5>(squares);\n```\n\n如上所述，编译器将为您的代码调用的`T`和`N`的每个组合实例化这个函数。如果模板函数有大量代码，那么这可能是一个问题。解决这个问题的一种方法是使用助手函数:\n\n```cpp\n    template<typename T> void print_array(T* arr, int size) \n    { \n        for (int i = 0; i < size; ++ i) \n        { \n            cout << arr[i] << endl; \n        } \n    } \n\n    template<typename T, int N> inline void print_array(T (&arr)[N]) \n    { \n        print_array(arr, N); \n    }\n```\n\n这有两个作用。首先，有一个版本的`print_array`接受一个指针和指针指向的项目数。这意味着`size`参数是在运行时确定的，因此该函数的版本只在编译时为所使用的数组类型进行实例化，而不是同时为类型和数组大小进行实例化。需要注意的第二件事是，以数组大小为模板的函数被声明为`inline`，它调用函数的第一个版本。虽然类型和数组大小的每个组合都有一个版本，但是实例化将是内联的，而不是一个完整的函数。\n\n# 专业模板\n\n在某些情况下，您可能有一个适用于大多数类型的例程(以及模板化函数的候选例程)，但是您可能会发现某些类型需要不同的例程。为了处理这个问题，您可以编写一个专门的模板函数，也就是说，一个将用于特定类型的函数，当调用方使用符合这个专门性的类型时，编译器将使用这个代码。举个例子，这里有一个相当无意义的函数；它返回类型的大小:\n\n```cpp\n    template <typename T> int number_of_bytes(T t) \n    { \n        return sizeof(T); \n    }\n```\n\n这适用于大多数内置类型，但是如果您用指针调用它，您将获得指针的大小，而不是指针指向的内容。因此，对于`char`数组的大小，`number_of_bytes(\"x\")`将返回 4(在 32 位系统上)，而不是 2。您可以决定希望对使用 C 函数`strlen`的`char*`指针进行专门化，以计算字符串中的字符数，直到出现`NUL`字符。为此，您需要一个类似于模板化函数的原型，用实际类型替换模板参数，由于不需要模板参数，因此您错过了这一点。由于此函数是针对特定类型的，您需要将专用类型添加到函数名称中:\n\n```cpp\n    template<> int number_of_bytes<const char *>(const char *str) \n    { \n        return strlen(str) + 1; \n    }\n```\n\n现在，当您调用`number_of_bytes(\"x\")`时，将调用专门化，它将返回值 2。\n\n之前，我们定义了一个模板化函数，最多返回两个相同类型的参数:\n\n```cpp\n    template<typename T> \n    T maximum(T lhs, T rhs) \n    { \n        return (lhs > rhs) ? lhs : rhs; \n    }\n```\n\n使用专门化，您可以为没有使用`>`运算符进行比较的类型编写版本。既然找不到两个布尔的最大值，可以删除`bool`的特殊化:\n\n```cpp\n    template<> bool maximum<bool>(bool lhs, bool rhs) = delete;\n```\n\n现在这意味着，如果代码用`bool`参数调用`maximum`，编译器将产生一个错误。\n\n# 可变模板\n\n可变模板是指模板参数的数量可变。语法类似于函数的变量参数；您使用省略号，但是在参数列表中的参数左侧使用省略号，参数列表将其声明为*参数包*:\n\n```cpp\n    template<typename T, typename... Arguments>  \n    void func(T t, Arguments... args);\n```\n\n`Arguments`模板参数是零个或多个类型，这些类型是函数相应数量的参数`args`的类型。在本例中，函数至少有一个类型为`T`的参数，但是您可以有任意数量的固定参数，包括一个都没有。\n\n在函数中，您需要解包参数包来访问调用者传递的参数。您可以使用特殊运算符`sizeof...`确定参数包中有多少项(注意省略号是名称的一部分)；与`sizeof`运算符不同，这是项目计数，而不是字节大小。要打开参数包，您需要使用参数包名称右侧的省略号(例如，`args...`)。此时，编译器将展开参数包，用参数包的内容替换符号。\n\n但是，您在设计时不知道有多少参数或它们是什么类型，因此有一些策略来解决这个问题。第一种使用递归:\n\n```cpp\n    template<typename T> void print(T t) \n    { \n        cout << t << endl; \n    } \n\n    template<typename T, typename... Arguments>  \n    void print(T first, Arguments ... next) \n    { \n        print(first); \n        print(next...); \n    }\n```\n\n可变模板`print`函数可以用`ostream`类可以处理的任何类型的一个或多个参数来调用:\n\n```cpp\n    print(1, 2.0, \"hello\", bool);\n```\n\n调用此函数时，参数列表被分成两部分:第一个参数中的第一个参数(`1`)`first,`，另外三个参数放在参数包`next`中。然后函数体调用第一个版本的`print`，将`first`参数打印到控制台。变量函数中的下一行然后在对`print`的调用中扩展参数包，也就是说，这递归地调用自己。在此调用中，`first`参数将为`2.0`，其余参数将放入参数包中。这种情况一直持续到参数包扩展到没有更多参数为止。\n\n解包参数包的另一种方法是使用初始化列表。在这种情况下，编译器将使用每个参数创建一个数组:\n\n```cpp\n    template<typename... Arguments>  \n    void print(Arguments ... args) \n    { \n        int arr [sizeof...(args)] = { args... }; \n        for (auto i : arr) cout << i << endl; \n    }\n```\n\n数组`arr,`是用参数包的大小创建的，初始值设定项大括号使用的解包语法将用参数填充数组。尽管这可以处理任意数量的参数，但所有参数都必须是相同类型的数组，`arr`。\n\n一个技巧是使用逗号运算符:\n\n```cpp\n    template<typename... Arguments>  \n    void print(Arguments ... args) \n    { \n        int dummy[sizeof...(args)] = { (print(args), 0)... }; \n    }\n```\n\n这将创建一个名为`dummy`的虚拟数组。除了在参数包的扩展中，不使用此数组。数组以`args`参数包的大小创建，省略号使用括号中的*表达式*扩展参数包。表达式使用逗号运算符，该运算符将返回逗号的右侧。由于这是一个整数，这意味着`dummy`的每个条目都有一个零值。有趣的部分是逗号运算符的左侧。这里，带有单个模板化参数的`print`版本与`args`参数包中的每个项目一起调用。\n\n# 过载运算符\n\n前面我们说过函数名不应该包含标点符号。严格来说，这是不正确的，因为如果您正在编写一个操作符，您只需要在函数名中使用标点符号。运算符用于作用于一个或多个操作数的表达式中。一元运算符有一个操作数，二元运算符有两个操作数，一个运算符返回运算结果。很明显，这描述了一个函数:一个返回类型、一个名称和一个或多个参数。\n\nC++ 提供关键字`operator`来表示函数不与函数调用语法一起使用，而是使用与运算符相关联的语法来调用(通常，一元运算符的第一个参数位于运算符的右侧，对于二元运算符，第一个参数位于左侧，第二个参数位于右侧，但也有例外)。\n\n一般来说，您将提供运算符作为自定义类型的一部分(因此运算符作用于该类型的变量)，但在某些情况下，您可以在全局范围内声明运算符。两者都有效。如果您正在编写自定义类型(类，如下一章所述)，那么将运算符的代码封装为自定义类型的一部分是有意义的。在本节中，我们将集中讨论定义运算符的另一种方式:作为全局函数。\n\n您可以提供自己版本的下列一元运算符:\n\n```cpp\n    ! & + - * ++ -- ~\n```\n\n您还可以提供自己版本的以下二进制运算符:\n\n```cpp\n    != == < <= > >= && ||\n    % %= + += - -= * *= / /= & &= | |= ^ ^= << <<= = >> =>>\n    -> ->* ,\n```\n\n您还可以编写函数调用运算符`()`、数组下标`[]`、转换运算符、转换运算符`(),`和`new`以及`delete`的版本。您不能重新定义`.`、`.*`、`::`、`?:`、`#`或`##`运算符，也不能重新定义“命名”运算符、`sizeof`、`alignof`或`typeid`。\n\n定义运算符时，编写一个函数，函数名为`operator*x*`，`*x*`为运算符符号(注意没有空格)。例如，如果你定义一个`struct`，它有两个成员定义一个笛卡尔点，你可能想要比较两个点是否相等。`struct`可以这样定义:\n\n```cpp\n    struct point \n    { \n        int x; \n        int y; \n    };\n```\n\n比较两个`point`物体很容易。如果一个对象的`x`和`y`等于另一个对象中的相应值，它们是相同的。如果定义了`==`运算符，那么也应该使用相同的逻辑定义`!=`运算符，因为`!=`应该给出与`==`运算符完全相反的结果。这些运算符可以这样定义:\n\n```cpp\n    bool operator==(const point& lhs, const point& rhs) \n    { \n        return (lhs.x == rhs.x) && (lhs.y == rhs.y); \n    } \n\n    bool operator!=(const point& lhs, const point& rhs) \n    { \n        return !(lhs == rhs); \n    }\n```\n\n这两个参数是运算符的两个操作数。第一个参数是操作符左侧的操作数，第二个参数是操作符右侧的操作数。这些作为参考传递，因此不会复制，并且它们被标记为`const`，因为操作员不会改变对象。定义后，您可以像这样使用`point`类型:\n\n```cpp\n    point p1{ 1,1 }; \n    point p2{ 1,1 }; \n    cout << boolalpha; \n    cout << (p1 == p2) << endl; // true \n    cout << (p1 != p2) << endl; // false\n```\n\n您可以定义一对名为`equals`和`not_equals`的函数，并使用它们来代替:\n\n```cpp\n    cout << equals(p1,p2) << endl;     // true \n    cout << not_equals(p1,p2) << endl; // false\n```\n\n但是，定义运算符会使代码更易读，因为您使用的类型类似于内置类型。运算符重载通常被称为*语法糖*，这是一种使代码更容易阅读的语法——但这使一项重要技术变得无关紧要。例如，智能指针是一种涉及类**析构函数**来管理资源生存期的技术，它之所以有用，只是因为您可以像调用指针一样调用这些类的对象。您可以这样做，因为智能指针类实现了`->`和`*`运算符。另一个例子是**函子**，即函数对象，其中类实现了`()`运算符，因此可以像访问函数一样访问对象。\n\n当您编写自定义类型时，您应该问问自己，为您的类型重载运算符是否有意义。如果类型是数字类型，例如，复数或矩阵，那么实现算术运算符是有意义的，但是由于类型没有逻辑方面，实现逻辑运算符有意义吗？有一种诱惑是重新定义运算符的*含义*，以涵盖您的特定操作，但这将使您的代码可读性降低。\n\n一般来说，一元运算符被实现为接受单个参数的全局函数。后缀递增和递减运算符是一个例外，它允许不同于前缀运算符的实现。前缀运算符将引用对象作为参数(运算符将递增或递减该参数)，并返回对此已更改对象的引用。然而，后缀运算符必须在递增或递减之前返回对象的值。因此，运算符函数有两个参数:对将被更改的对象的引用和一个整数(它将始终是值 1)；它将返回原始对象的副本。\n\n二元运算符有两个参数，返回一个对象或对一个对象的引用。例如，对于我们之前定义的`struct`，我们可以为`ostream`对象定义一个插入操作符:\n\n```cpp\n    struct point \n    { \n        int x; \n        int y; \n    }; \n\n    ostream& operator<<(ostream& os, const point& pt) \n    { \n        os << \"(\" << pt.x << \",\" << pt.y << \")\"; \n        return os; \n    }\n```\n\n这意味着您现在可以在`cout`对象中插入一个`point`对象，并将其打印在控制台上:\n\n```cpp\n    point pt{1, 1}; \n    cout << \"point object is \" << pt << endl;\n```\n\n# 函数对象\n\n函数对象或**函子**是实现函数调用运算符的自定义类型:(`operator()`)。这意味着可以用看起来像函数的方式调用函数运算符。由于我们还没有涉及类，在这一节中，我们将只探讨标准库提供的函数对象类型以及如何使用它们。\n\n`<functional>`头文件包含各种可以作为函数对象的类型。下表列出了这些内容:\n\n| **目的** | **类型** |\n| 算术 | `divides`、`minus`、`modulus`、`multiplies`、`negate`、`plus` |\n| 按位 | `bit_and`、`bit_not`、`bit_or`、`bit_xor` |\n| 比较 | `equal_to`、`greater`、`greater_equal`、`less`、`less_equals`、`not_equal_to` |\n| 逻辑学的 | `logical_and`、`logical_not`、`logical_or` |\n\n这些都是二元函数类，除了`bit_not`、`logical_not,`和`negate`，都是一元的。二元函数对象作用于两个值并返回一个结果，一元函数对象作用于单个值并返回一个结果。例如，您可以使用以下代码计算两个数字的模数:\n\n```cpp\n    modulus<int> fn; \n    cout << fn(10, 2) << endl;\n```\n\n这声明了一个名为`fn`的函数对象，它将执行模数转换。第二行使用对象，用两个参数调用对象上的`operator()`函数，所以下面一行相当于前面一行:\n\n```cpp\n    cout << fn.operator()(10, 2) << endl;\n```\n\n结果是`0`的值被打印在控制台上。`operator()`函数仅执行两个参数的模数，在本例中为`10 % 2`。这看起来不太令人兴奋。`<algorithm>`标题包含对功能对象起作用的功能。大多数采用谓词，即逻辑函数对象，但有一个`transform`采用执行动作的函数对象:\n\n```cpp\n    // #include <algorithm> \n    // #include <functional> \n\n    vector<int> v1 { 1, 2, 3, 4, 5 }; \n    vector<int> v2(v1.size()); \n    fill(v2.begin(), v2.end(), 2); \n    vector<int> result(v1.size()); \n\n    transform(v1.begin(), v1.end(), v2.begin(), \n        result.begin(), modulus<int>()); \n\n    for (int i : result) \n    { \n        cout << i << ' '; \n    } \n    cout << endl;\n```\n\n该代码将对两个向量中的值执行五次模数计算。从概念上讲，它是这样做的:\n\n```cpp\n    result = v1 % v2;\n```\n\n即`result`中的每一项都是`v1`和`v2`中对应项的模数。在代码中，第一行用五个值创建一个`vector`。我们将使用`2`计算这些值的模数，因此第二行声明一个空的`vector`，但容量与第一行`vector`相同。第二个`vector`通过调用`fill`函数来填充。第一个参数是`vector`中第一项的地址，`end`函数返回`vector`中最后一项*之后的地址。函数调用中的最后一项是从第一个参数所指向的项开始直到第二个参数所指向的项(但不包括第二个参数所指向的项)的每个项中的`vector`中的值。*\n\n此时，第二个`vector`将包含五个项目，每个项目都是`2`。接下来，为结果创建一个`vector`；同样，它的大小与第一个数组相同。最后，通过`transform`功能进行计算，如下图所示:\n\n```cpp\n    transform(v1.begin(), v1.end(),  \n       v2.begin(), result.begin(), modulus<int>());\n```\n\n前两个参数给出了第一个`vector`的迭代器，由此可以计算出项目的数量。由于所有三个`vector`都是相同的大小，您只需要`v2`和`result`的`begin`迭代器。\n\n最后一个参数是函数对象。这是一个临时对象，仅在此语句期间存在；它没有名字。这里使用的语法是对类的构造函数的显式调用；它是模板化的，所以您需要给出模板参数。`transform`函数将调用该函数对象上的`operator(int,int)`函数，将`v1`中的每一项作为第一个参数，`v2`中的相应项作为第二个参数，并将结果存储在`result`中的相应位置。\n\n由于`transform`以任意二进制函数对象为第二个参数，所以可以传递`plus<int>`的一个实例给`v1`中的每一项增加一个值 2，或者传递`multiplies<int>`的一个实例给`v1`中的每一项乘以 2。\n\n函数对象有用的一种情况是使用谓词执行多次比较。谓词是比较值并返回布尔值的函数对象。`<functional>`标题包含几个类，允许您比较项目。让我们看看`result`容器里有多少物品是零。为此，我们使用`count_if`功能。这将遍历一个容器，将谓词应用于每个项目，并计算谓词返回`true`值的次数。有几种方法可以做到这一点。第一个定义了一个谓词函数:\n\n```cpp\n    bool equals_zero(int a) \n    { \n        return (a == 0); \n    }\n```\n\n指向这个的指针可以传递到`count_if`函数:\n\n```cpp\n    int zeros = count_if( \n       result.begin(), result.end(), equals_zero);\n```\n\n前两个参数指示要检查的值的范围。最后一个参数是用作谓词的函数的指针。当然，如果您正在检查不同的值，您可以使其更通用:\n\n```cpp\n    template<typename T, T value> \n    inline bool equals(T a) \n    { \n        return a == value; \n    }\n```\n\n这样称呼:\n\n```cpp\n    int zeros = count_if( \n       result.begin(), result.end(), equals<int, 0>);\n```\n\n这段代码的问题在于，我们是在使用它的地方以外的地方定义操作的。`equals`函数可以在另一个文件中定义；然而，有了谓词，在需要谓词的代码附近定义进行检查的代码更容易阅读。\n\n`<functional>`头还定义了可以用作函数对象的类。例如，`equal_to<int>`，比较两个值。然而，`count_if`函数需要一个一元函数对象，它将向该对象传递一个值(参见前面描述的`equals_zero`函数)。`equal_to<int>`是一个二元函数对象，比较两个值。我们需要提供第二个操作数，为此，我们使用名为`bind2nd`的辅助函数:\n\n```cpp\n    int zeros = count_if( \n       result.begin(), result.end(), bind2nd(equal_to<int>(), 0));\n```\n\n`bind2nd`将参数`0`绑定到从`equal_to<int>`创建的功能对象。使用这样的函数对象会使谓词的定义更接近将使用它的函数调用，但是语法看起来相当混乱。C++ 11 提供了一种让编译器确定所需函数对象并将参数绑定到这些对象的机制。这些被称为 lambda 表达式。\n\n# 引入 lambda 表达式\n\nlambda 表达式用于在将要使用函数对象的位置创建匿名函数对象。这使得您的代码更加易读，因为您可以看到将要执行的内容。乍一看，lambda 表达式看起来就像是一个函数参数的函数定义:\n\n```cpp\n    auto less_than_10 = [](int a) {return a < 10; }; \n    bool b = less_than_10(4);\n```\n\n为了避免使用谓词的函数的复杂性，在这段代码中，我们为 lambda 表达式分配了一个变量。这通常不是您使用它的方式，但它使描述更加清晰。lambda 表达式开头的方括号称为**捕获列表**。这个表达式不捕获变量，所以括号是空的。您可以使用在 lambda 表达式之外声明的变量，这些变量必须被*捕获*。捕获列表指示所有这些变量是由引用(使用`[&]`)还是由值(使用`[=]`)捕获。您还可以为将要捕获的变量命名(如果有多个变量，请使用逗号分隔的列表)，如果它们被某个值捕获，则只使用它们的名称。如果他们被引用者捕获，在他们的名字上使用`&`。\n\n通过引入一个在表达式外部声明的名为`limit`的变量，可以使前面的 lambda 表达式更通用:\n\n```cpp\n    int limit = 99; \n    auto less_than = [limit](int a) {return a < limit; };\n```\n\n如果将 lambda 表达式与全局函数进行比较，捕获列表有点像标识全局函数可以访问的全局变量。\n\n在标题列表之后，在括号中给出参数列表。同样，如果将 lambda 与函数进行比较，lambda 参数列表相当于函数参数列表。如果 lambda 表达式没有任何参数，那么您可以完全忽略括号。\n\nlambda 的主体在一对大括号中给出。这可以包含任何可以在函数中找到的内容。lambda 体可以声明局部变量，甚至可以声明`static`个变量，看起来很离奇，但却是合法的:\n\n```cpp\n    auto incr = [] { static int i; return ++ i; }; \n    incr(); \n    incr(); \n    cout << incr() << endl; // 3\n```\n\nlambda 的返回值是从返回的项中推导出来的。lambda 表达式不必返回值，在这种情况下，表达式将返回`void`:\n\n```cpp\n    auto swap = [](int& a, int& b) { int x = a; a = b; b = x; }; \n    int i = 10, j = 20; \n    cout << i << \" \" << j << endl; \n    swap(i, j); \n    cout << i << \" \" << j << endl;\n```\n\nlambda 表达式的强大之处在于，您可以在需要函数对象或谓词的情况下使用它们:\n\n```cpp\n    vector<int> v { 1, 2, 3, 4, 5 }; \n    int less_than_3 = count_if( \n       v.begin(), v.end(),  \n       [](int a) { return a < 3; }); \n    cout << \"There are \" << less_than_3 << \" items less than 3\" << endl;\n```\n\n这里我们声明一个`vector`并用一些值初始化它。`count_if`功能用于统计容器中有多少物品小于 3。因此，前两个参数用于给出要检查的项目的范围，第三个参数是执行比较的 lambda 表达式。`count_if`函数将为通过λ的`a`参数传入的范围内的每个项目调用该表达式。`count_if`功能记录λ返回`true`的次数。\n\n# 在 C++ 中使用函数\n\n本章中的示例使用您在本章中学习的技术，按照文件大小的顺序列出文件夹和子文件夹中的所有文件，并列出文件名及其大小。该示例相当于在命令行中键入以下内容:\n\n```cpp\ndir /b /s /os /a-d folder\n```\n\n这里，`folder`是你正在列出的文件夹。`/s`选项重复出现，`/a-d`从列表中删除文件夹，`/os`按大小排序。问题是如果没有`/b`选项，我们会得到每个文件夹的信息，但是使用它会删除列表中的文件大小。我们想要一个文件名(和它们的路径)的列表，它们的大小，先按最小的排序。\n\n首先在`Beginning_C++ `文件夹下为本章(`Chapter_05`)创建一个新文件夹。在 Visual C++ 中创建新的 C++ 源文件，并将其保存为这个新文件夹下的`files.cpp`。该示例将使用基本输出和字符串。它将采用一个命令行参数；如果传递了更多的命令行参数，我们只使用第一个。在`files.cpp`中增加以下内容:\n\n```cpp\n    #include <iostream> \n    #include <string> \n    using namespace std; \n\n    int main(int argc, char* argv[]) \n    { \n        if (argc < 2) return 1; \n        return 0; \n    }\n```\n\n该示例将使用窗口函数`FindFirstFile`和`FindNextFile`来获取符合文件规范的文件的信息。这些以`WIN32_FIND_DATAA`结构返回数据，其中包含关于文件名、文件大小和文件属性的信息。这些函数也返回关于文件夹的信息，所以这意味着我们可以测试子文件夹并递归。`WIN32_FIND_DATAA`结构将文件大小分为两部分，即高 32 位和低 32 位，为 64 位数字。我们将创建自己的结构来保存这些信息。在文件的顶部，在 C++ 包含文件之后，添加以下内容:\n\n```cpp\n    using namespace std; \n\n    #include <windows.h> struct file_size { unsigned int high; unsigned int low; };\n```\n\n第一行是 Windows SDK 头文件，以便您可以访问 Windows 函数，该结构用于保存关于文件大小的信息。我们想根据文件的大小进行比较。`WIN32_FIND_DATAA`结构提供两个`unsigned long`成员的大小(一个高 4 字节，另一个低 4 字节)。我们可以将它存储为 64 位数字，但是为了有借口编写一些运算符，我们将大小存储在我们的`file_size`结构中。该示例将打印出文件大小，并将比较文件大小，因此我们将编写一个运算符来将一个`file_size`对象插入到输出流中；因为我们想按大小排序文件，所以我们需要一个操作员来确定一个`file_size`对象是否大于另一个。\n\n该代码将使用窗口函数来获取关于文件的信息，特别是它们的名称和大小。该信息将存储在一个`vector`中，因此在文件的顶部添加这两个高亮显示的行:\n\n```cpp\n    #include <string> \n    #include <vector>\n #include <tuple>\n```\n\n需要`tuple`类，这样我们就可以在`vector`中存储一个`string`(文件名)和一个`file_size`对象作为每个项目。为了使代码更易读，在结构定义后添加以下别名:\n\n```cpp\n    using file_info = tuple<string, file_size>;\n```\n\n然后在`main`函数的正上方，为将获取文件夹中文件的函数添加框架代码:\n\n```cpp\n    void files_in_folder( \n       const char *folderPath, vector<file_info>& files) \n    { \n    }\n```\n\n该函数引用一个`vector`和一个文件夹路径。代码将遍历指定文件夹中的每个项目。如果是文件，会将详细信息存储在`vector`中；否则，如果该项是一个文件夹，它将调用自己来获取该子文件夹中的文件。在`main`函数的底部添加对该函数的调用:\n\n```cpp\n    vector<file_info> files; \n    files_in_folder(argv[1], files);\n```\n\n代码已经检查了至少有一个命令行参数，我们将它用作要检查的文件夹。`main`函数应该打印出文件信息，所以我们在堆栈上声明一个`vector`，并通过引用`files_in_folder`函数传递这个信息。这段代码到目前为止没有任何作用，但是您可以编译代码以确保没有错别字(记得使用`/EHsc`参数)。\n\n大部分工作在`files_in_folder`功能中进行。首先，向该函数添加以下代码:\n\n```cpp\n    string folder(folderPath); \n    folder += \"*\"; \n    WIN32_FIND_DATAA findfiledata {}; \n    void* hFind = FindFirstFileA(folder.c_str(), &findfiledata); \n\n    if (hFind != INVALID_HANDLE_VALUE) \n    { \n       do \n       { \n       } while (FindNextFileA(hFind, &findfiledata)); \n       FindClose(hFind); \n    }\n```\n\n我们将使用函数的 ASCII 版本(因此在结构和函数名上使用结尾的`A`)。`FindFirstFileA`函数采用搜索路径，在这种情况下，我们使用以`*`为后缀的文件夹名称，意思是*该文件夹中的所有内容*。请注意，窗口函数需要一个`const char*`参数，所以我们在`string`对象上使用`c_str`函数。\n\n如果函数调用成功，并且它找到了一个符合这个标准的项目，那么函数会填充引用传递的`WIN32_FIND_DATAA`结构，并且它还会返回一个不透明的指针，该指针将用于在这个搜索中进行后续调用(您不需要知道它指向什么)。代码检查调用是否成功，如果成功，则反复调用`FindNextFileA`获取下一项，直到该函数返回 0，表示没有更多项。不透明的指针被传递到`FindNextFileA`以便它知道正在检查哪个搜索。搜索完成后，代码调用`FindClose`释放窗口为搜索分配的任何资源。\n\n搜索将返回文件和文件夹项目；为了不同地处理每一个，我们可以测试`WIN32_FIND_DATAA`结构的`dwFileAttributes`成员。在`do`循环中添加以下代码:\n\n```cpp\n    string findItem(folderPath); \n    findItem += \"\"; \n    findItem += findfiledata.cFileName; \n    if ((findfiledata.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0) \n    { \n        // this is a folder so recurse \n    } \n    else \n    { \n        // this is a file so store information \n    }\n```\n\n`WIN32_FIND_DATAA`结构只包含文件夹中项目的相对名称，所以前几行创建一个绝对路径。下面几行测试项目是文件夹(目录)还是文件。如果该项是一个文件，那么我们只需将其添加到传递给函数的向量中。在`else`条款中增加以下内容:\n\n```cpp\n    file_size fs{}; \n    fs.high = findfiledata.nFileSizeHigh; \n    fs.low = findfiledata.nFileSizeLow; \n    files.push_back(make_tuple(findItem, fs));\n```\n\n前三行用大小数据初始化一个`file_size`结构，最后一行用文件名及其大小添加一个`tuple`到`vector`。为了让您可以看到对该函数的简单调用结果，请在`main`函数的底部添加以下内容:\n\n```cpp\n    for (auto file : files) \n    { \n        cout << setw(16) << get<1>(file) << \" \"  \n            << get<0>(file) << endl; \n    }\n```\n\n这将遍历`files`向量中的项目。每一项都是一个`tuple<string, file_size>`对象，要得到`string`项，可以使用标准库函数，`get,`用 0 作为函数模板参数，要得到你调用的`file_size`对象，用 1 作为函数模板参数。该代码调用`setw`操纵器，以确保文件大小始终打印在 16 个字符宽的列中。要使用这个，您需要在文件的顶部为`<iomanip>`添加一个 include。注意`get<1>`将返回一个`file_size`对象，这个对象被插入到`cout`中。照目前的情况来看，这段代码不会编译，因为没有操作符来执行这个操作。我们需要写一个。\n\n在结构定义之后，添加以下代码:\n\n```cpp\n    ostream& operator<<(ostream& os, const file_size fs) \n    { \n        int flags = os.flags(); \n        unsigned long long ll = fs.low + \n            ((unsigned long long)fs.high << 32); \n        os << hex << ll; \n        os.setf(flags); \n        return os; \n    }\n```\n\n这个操作符会改变`ostream`对象，所以我们在函数开始时存储初始状态，在结束时将对象恢复到这个状态。由于文件大小是 64 位数字，我们转换`file_size`对象的组成部分，然后将其打印为十六进制数字。\n\n现在您可以编译并运行这个应用了。例如:\n\n```cpp\nfiles C: \\windows\n```\n\n这将列出`windows`文件夹中文件的名称和大小。\n\n还有两件事需要做——递归子文件夹和排序数据。两者都很容易实现。在`files_in_folder`功能中，将以下代码添加到`if`语句的代码块中:\n\n```cpp\n    // this is a folder so recurse \n    string folder(findfiledata.cFileName); \n    // ignore . and .. directories \n    if (folder != \".\" && folder != \"..\") \n    { \n        files_in_folder(findItem.c_str(), files); \n    }\n```\n\n搜索将返回`.`(当前)文件夹和`..`(父)文件夹，因此我们需要检查这些并忽略它们。下一步操作是递归调用`files_in_folder`函数获取子文件夹中的文件。如果你愿意，你可以编译和测试应用，但是这次最好使用`Beginning_C++ `文件夹测试代码，因为递归列出 Windows 文件夹会产生很多文件。\n\n代码返回获得的文件列表，但是我们希望按照文件大小的顺序来查看它们。为此，我们可以使用`<algorithm>`标题中的排序功能，因此在`<tuple>`的 include 之后添加一个 include。在`main`功能中，调用`files_in_folder,`后添加以下代码:\n\n```cpp\n    files_in_folder(argv[1], files); \n\n    sort(files.begin(), files.end(), \n        [](const file_info& lhs, const file_info& rhs) { \n            return get<1>(rhs) > get<1>(lhs);    \n    } );\n```\n\n`sort`功能的前两个参数表示要检查的项目范围。第三项是谓词，函数将从`vector`向谓词传递两项。如果这两个参数是有序的(第一个比第二个小)，你必须返回一个值`true`。\n\n谓词由 lambda 表达式提供。没有捕获的变量，所以表达式以`[]`开始，后面是由`sort`算法比较的项目的参数表(由`const`引用传递，因为它们不会改变)。实际比较是在支架之间进行的。因为我们想按升序列出文件，所以我们必须确保两个文件中的第二个文件比第一个文件大。在本代码中，我们对两个`file_size`对象使用`>`运算符。为了编译这段代码，我们需要定义这个操作符。在插入运算符后添加以下内容:\n\n```cpp\n    bool operator>(const file_size& lhs, const file_size& rhs) \n    { \n        if (lhs.high > rhs.high) return true; \n        if (lhs.high == rhs.high) { \n            if (lhs.low > rhs.low) return true; \n        } \n        return false; \n    }\n```\n\n现在，您可以编译并运行该示例。您应该会发现，指定文件夹和子文件夹中的文件是按照文件大小的顺序列出的。\n\n# 摘要\n\n函数允许您将代码分割成逻辑例程，这使得您的代码更易读，并提供了重用代码的灵活性。C++ 提供了大量定义函数的选项，包括变量参数列表、模板、函数指针和 lambda 表达式。然而，全局函数有一个主要问题:数据与函数是分离的。这意味着函数必须通过全局数据项访问数据，或者数据必须在每次调用函数时通过参数传递给函数。在这两种情况下，数据都存在于函数之外，并且可以被与数据无关的其他函数使用。下一章将给出一个解决方案:类。A `class`允许您将数据封装在自定义类型中，并且您可以在该类型上定义函数，以便只有这些函数能够访问数据。**"
  },
  {
    "path": "docs/mod-cpp/04.md",
    "content": "# 四、类\n\nC++ 允许您创建自己的类型。这些自定义类型可以有运算符，也可以转换为其他类型；事实上，它们可以像内置类型一样与您定义的行为一起使用。这个工具使用一种叫做类的语言特性。能够定义自己的类型的优势在于，您可以将数据封装在所选类型的对象中，并使用该类型来管理该数据的生存期。您还可以定义可以对该数据执行的操作。换句话说，您能够定义具有状态和行为的自定义类型，这是面向对象编程的基础。\n\n# 写作课\n\n当您使用内置类型时，数据对于任何能够访问该数据的代码都是直接可用的。C++ 提供了一种机制(`const`)来防止写访问，但是任何代码都可以使用`const_cast`来丢弃`const` -ness。您的数据可能很复杂，例如指向映射到内存中的文件的指针，其目的是让您的代码改变几个字节，然后将文件写回磁盘。这种原始指针是危险的，因为其他可以访问指针的代码可能会改变缓冲区中不应该改变的部分。我们需要的是一种机制，将数据封装成一种知道要更改哪些字节的类型，并且只允许该类型访问数据。这是班级背后的基本理念。\n\n# 审查结构\n\n我们已经在 C++ 中看到了一种封装数据的机制:`struct`。结构允许您声明内置类型、指针或引用的数据成员。当您从该`struct`创建变量时，您正在创建结构的**实例**，也称为**对象**。您可以创建引用该对象的变量或指向该对象的指针。您甚至可以通过值将对象传递给一个函数，在该函数中编译器将复制该对象(它将调用`struct`的*复制构造函数*)。\n\n我们已经看到，使用`struct`任何可以访问实例的代码(甚至通过指针或引用)都可以访问对象的成员(尽管这是可以改变的)。这样使用，一个`struct`可以认为是包含状态的**集合**类型。\n\n`struct`实例的成员可以通过直接使用点运算符或通过指向对象的指针使用`->`运算符来初始化。我们还看到，您可以用初始化列表(在大括号中)初始化`struct`的实例。这是非常严格的，因为初始化列表必须匹配`struct`中的数据成员。在[第 2 章](02.html)、*使用内存、数组和指针*中，您看到您可以拥有一个指针作为`struct`的成员，但是您必须明确采取适当的措施来释放指针所指向的内存；如果没有，那么这可能会导致内存泄漏。\n\nA `struct`是 C++ 中可以使用的类类型之一；另外两个是`union`和`class`。定义为`struct`或`class`的自定义类型可以有行为和状态，C++ 允许您定义一些特殊的函数来控制如何创建和销毁、复制和转换实例。此外，您可以在`struct`或`class`类型上定义运算符，以便您可以像在内置类型上使用运算符一样在实例上使用运算符。`struct`和`class`之间有区别，我们将在后面讨论，但总的来说，本章的其余部分将是关于类的，当提到`class`时，你通常可以假设同样的情况也适用于`struct`。\n\n# 定义类\n\n一个类在一个语句中定义，它将在一个块中定义它的成员，多个语句用大括号`{}`括起来。因为它是一个语句，所以您必须在最后一个大括号后放置一个分号。一个类可以在头文件中定义(许多 **C++ 标准库**类也是如此)，但是你必须采取措施确保这样的文件在源文件中只包含一次。但是，关于类中的特定项，有一些规则必须在源文件中定义，这将在后面介绍。\n\n如果您仔细阅读 C++ 标准库，您会看到类包含成员函数，并且试图将一个类的所有代码放入一个头文件中，这使得代码难以阅读和理解。对于一个由大量专业 C++ 程序员维护的库文件来说，这可能是合理的，但是对于您自己的项目来说，可读性应该是一个关键的设计目标。因此，C++ 类可以在 C++ 头文件中声明，包括它的成员函数，函数的实际实现可以放在源文件中。这使得头文件更容易维护和重用。\n\n# 定义类行为\n\n类可以定义只能通过类的实例调用的函数；这样的函数通常被称为**方法**。一个对象会有状态；这是由类定义的数据成员提供的，并在创建对象时初始化。对象上的方法定义了对象的行为，通常作用于对象的状态。当你设计一个类时，你应该这样考虑方法:它们描述了做某事的对象。\n\n```cpp\n    class cartesian_vector \n    { \n    public: \n        double x; \n        double y; \n        // other methods \n        double get_magnitude() { return std::sqrt((x * x) + (y * y)); } \n    };\n```\n\n这个类有两个数据成员，`x`和`y`，它们表示在笛卡尔 x 和 y 方向上解析的二维向量的方向。`public`关键字意味着在这个说明符之后定义的任何成员都可以被类外定义的代码访问。默认情况下，一个类的所有成员都是`private`，除非您另有说明。`private`表示该成员只能被该类的其他成员访问。\n\nThis is the difference between a `struct` and a `class`: by default, members of a `struct` are `public` and by default, members of a `class` are `private`.\n\n这个类有一个叫做`get_magnituide`的方法，它将返回笛卡尔向量的长度。该函数作用于类的两个数据成员，并返回一个值。这是一种**访问器**方法；它提供对对象状态的访问。这样的方法在`class`上是典型的，但是没有要求方法返回值。像函数一样，方法也可以接受参数。`get_magnituide`的方法可以这样称呼:\n\n```cpp\n    cartesian_vector vec { 3.0, 4.0 }; \n    double len = vec.get_magnitude(); // returns 5.0\n```\n\n这里在堆栈上创建一个`cartesian_vector`对象，并使用列表初始化器语法将其初始化为表示`(3,4)`向量的值。这个向量的长度是 5，是在对象上调用`get_magnitude`返回的值。\n\n# 使用这个指针\n\n类中的方法有一个特殊的调用约定，在 Visual C++ 中称为`__thiscall`。原因是类中的每个方法都有一个名为`this`的隐藏参数，它是类类型指向当前实例的指针:\n\n```cpp\n    class cartesian_vector \n    { \n    public: \n        double x; \n        double y; \n        // other methods \n        double get_magnitude() \n        { \n             return std::sqrt((this->x * this->x) + (this->y * this->y)); \n        } \n    };\n```\n\n这里`get_magnitude`方法返回`cartesian_vector`对象的长度。通过`->`操作符访问对象的成员。如前所示，可以在没有`this`指针的情况下访问类的成员，但是它确实明确了这些项是`class`的成员。\n\n您可以在`cartesian_vector`类型上定义一个允许您更改其状态的方法:\n\n```cpp\n    class cartesian_vector \n    { \n    public: \n        double x; \n        double y; \n        reset(double x, double y) { this->x = x; this->y = y; } \n        // other methods \n    };\n```\n\n`reset`方法的参数与类的数据成员同名；然而，由于我们使用了`this`指针，编译器知道这并不含糊。\n\n您可以用`*`操作符取消引用`this`指针来访问对象。当成员函数必须返回对当前对象的引用时，这很有用(正如一些操作符所做的，我们将在后面看到)，您可以通过返回`*this`来做到这一点。类中的方法也可以将`this`指针传递给外部函数，这意味着它正在通过类型化指针通过引用传递当前对象。\n\n# 使用范围解析运算符\n\n您可以在`class`语句中内联定义一个方法，但也可以将声明和实现分开，因此该方法在`class`语句中声明，但在其他地方定义。当在`class`语句之外定义方法时，需要使用范围解析运算符为方法提供类型的名称。例如，使用前面的`cartesian_vector`示例:\n\n```cpp\n    class cartesian_vector \n    { \n    public: \n        double x; \n        double y; \n        // other methods \n        double magnitude(); \n    }; \n\n    double cartesian_vector::magnitude() \n    { \n        return sqrt((this->x * this->x) + (this->y * this->y)); \n    }\n```\n\n方法是在类定义之外定义的；然而，它仍然是类方法，所以它有一个`this`指针，可以用来访问对象的成员。通常，类将在带有方法原型的头文件中声明，而实际的方法将在单独的源文件中实现。在这种情况下，使用`this`指针来访问类成员(方法和数据成员)可以很明显地看出，当您粗略地查看一个源文件时，函数是一个类的方法。\n\n# 定义类状态\n\n您的类可以有内置类型作为数据成员，也可以有自定义类型。这些数据成员可以在类中声明(并在构造类的实例时创建)，也可以是指向在自由存储中创建的对象的指针或对在其他地方创建的对象的引用。请记住，如果您有一个指向在自由存储中创建的项目的指针，您需要知道谁负责释放指针指向的内存。如果您有一个指向在某个堆栈框架上创建的对象的引用(或指针)，您需要确保您的类的对象不会比该堆栈框架活得更长。\n\n当您将数据成员声明为`public`时，这意味着外部代码可以读写数据成员。\n\n您可以决定只授予只读访问权限，在这种情况下，您可以创建成员`private`并通过访问器提供读访问权限:\n\n```cpp\n    class cartesian_vector \n    { \n        double x; \n        double y; \n    public: \n        double get_x() { return this->x; } \n        double get_y() { return this->y; } \n        // other methods \n    };\n```\n\n当您创建数据成员`private`时，这意味着您不能使用初始化列表语法来初始化一个对象，但是我们将在后面解决这个问题。您可以决定使用访问器来授予对数据成员的写访问权限，并使用它来检查值。\n\n```cpp\n    void cartesian_vector::set_x(double d) \n    { \n        if (d > -100 && d < 100) this->x = d; \n    }\n```\n\n这适用于值的范围必须介于(但不包括)—`100`和`100`之间的类型。\n\n# 创建对象\n\n您可以在堆栈或自由存储中创建对象。使用前面的示例，如下所示:\n\n```cpp\n    cartesian_vector vec { 10, 10 }; \n    cartesian_vector *pvec = new cartesian_vector { 5, 5 }; \n    // use pvec \n    delete pvec\n```\n\n这是对象的**直接初始化**，假设`cartesian_vector`的数据成员是`public`。`vec`对象在堆栈上创建，并用初始化列表初始化。在第二行，在自由存储中创建一个对象，并用初始化列表初始化。空闲存储上的对象必须在某个时刻被释放，这是通过删除指针来实现的。`new`运算符将在空闲存储中为该类的数据成员和该类所需的任何基础设施分配足够的内存。\n\nC++ 11 的一个新特性是允许直接初始化来提供类中的默认值:\n\n```cpp\n    class point \n    { \n    public: \n        int x = 0; \n        int y = 0; \n    };\n```\n\n这意味着如果您创建了一个没有任何其他初始化值的`point`实例，它将被初始化为`x`和`y`都为零。如果数据成员是内置数组，则可以使用类中的初始化列表提供直接初始化:\n\n```cpp\n    class car \n    { \n    public: \n        double tire_pressures[4] { 25.0, 25.0, 25.0, 25.0 }; \n    };\n```\n\nC++ 标准库容器可以用一个初始化列表来初始化，所以，在这个类中，对于`tire_pressures`，我们可以使用`vector<double>`或`array<double,4>`来初始化它，而不是将类型声明为`double[4]`。\n\n# 物体的构造\n\nC++ 允许您定义特殊的方法来执行对象的初始化。这些被称为**构造器**。在 C++ 11 中，默认情况下会为您生成三个这样的函数，但是如果您愿意，您可以提供自己的版本。这三个构造函数以及其他三个相关函数如下:\n\n*   **默认构造函数:**调用该函数创建一个具有*默认*值的对象。\n*   **复制构造函数:**用于根据已有对象的值创建新对象。\n*   **移动构造器:**这用于使用从现有对象移动的数据创建新对象。\n*   **析构函数:**这个函数被调用来清理一个对象使用的资源。\n*   **复制分配:**这将数据从一个现有对象复制到另一个现有对象。\n*   **移动分配:**这将数据从一个现有对象移动到另一个现有对象。\n\n这些函数的编译器创建版本将隐式为`public`；但是，您可以决定通过定义自己的版本并使其成为`private`来防止复制或分配，或者您可以使用`=delete`语法删除它们。\n\n您还可以提供自己的构造函数，这些构造函数将采用您决定初始化新对象所需的任何参数。\n\n构造函数是与类型同名的成员函数，但不返回值，因此如果构造失败，则不能返回值，这可能意味着调用方将接收部分构造的对象。处理这种情况的唯一方法就是抛出一个异常(解释见[第 7 章](07.html)、*诊断调试*)。\n\n# 定义构造函数\n\n当创建的对象没有值时，使用默认构造函数，因此对象必须用默认值初始化。之前声明的`point`可以这样实现:\n\n```cpp\n    class point \n    { \n        double x; double y; \n    public: \n        point() { x = 0; y = 0; } \n    };\n```\n\n这将显式地将项初始化为零值。如果要使用默认值创建实例，则不包括括号。\n\n```cpp\n    point p;   // default constructor called\n```\n\n了解这个语法很重要，因为很容易写错以下内容:\n\n```cpp\n    point p();  // compiles, but is a function prototype!\n```\n\n这将编译，因为编译器会认为您提供了一个函数原型作为正向声明。然而，当你试图使用符号`p`作为变量时，你会得到一个错误。您也可以使用带空括号的初始化列表语法调用默认构造函数:\n\n```cpp\n    point p {};  // calls default constructor\n```\n\n虽然在这种情况下，数据成员是内置类型并不重要，但是像这样在构造函数的主体中初始化数据成员需要调用成员类型的赋值运算符。更有效的方法是使用直接初始化一个**成员列表**。\n\n下面是一个接受两个参数的构造函数，它说明了一个成员列表:\n\n```cpp\n    point(double x, double y) : x(x), y(y) {}\n```\n\n括号外的标识符是类成员的名称，括号内的项是用于初始化该成员的表达式(在本例中是构造函数参数)。本示例使用`x`和`y`作为参数名称。你不必这样做；这里给出的只是编译器区分参数和数据成员的一个例子。您也可以在构造函数的成员列表中使用支撑初始值设定项语法:\n\n```cpp\n    point(double x, double y) : x{x}, y{y} {}\n```\n\n当您创建如下对象时，可以调用此构造函数:\n\n```cpp\n    point p(10.0, 10.0);\n```\n\n您也可以创建对象数组:\n\n```cpp\n    point arr[4];\n```\n\n这将创建四个`point`对象，可以通过索引`arr`数组来访问这些对象。请注意，当您创建一个对象数组时，*默认的*构造函数在项目上被调用；没有办法调用任何其他构造函数，因此您必须分别初始化每个构造函数。\n\n您还可以为构造函数参数提供默认值。在下面的代码中，`car`级有四个轮胎(前两个是前轮胎)和备胎的值。有一个构造函数具有用于前后轮胎的强制值，以及备用轮胎的可选值。如果没有为备胎压力提供值，则将使用默认值:\n\n```cpp\n    class car \n    { \n        array<double, 4> tire_pressures;; \n        double spare; \n    public: \n        car(double front, double back, double s = 25.0)  \n          : tire_pressures{front, front, back, back}, spare{s} {} \n    };\n```\n\n可以用两个值或三个值调用此构造函数:\n\n```cpp\n    car commuter_car(25, 27); \n    car sports_car(26, 28, 28);\n```\n\n# 委托构造函数\n\n构造函数可以使用相同的成员列表语法调用另一个构造函数:\n\n```cpp\n    class car \n    { \n        // data members \n    public: \n        car(double front, double back, double s = 25.0)  \n           : tire_pressures{front, front, back, back}, spare{s} {} \n        car(double all) : car(all, all) {} \n    };\n```\n\n这里，接受一个值的构造函数委托给接受三个参数的构造函数(在这种情况下，使用备用参数的默认值)。\n\n# 复制构造函数\n\n当您按值传递对象(或按值返回)或基于另一个对象显式构造对象时，将使用复制构造函数。下面的最后两行都从另一个`point`对象创建了一个`point`对象，并且在这两种情况下都调用了复制构造函数:\n\n```cpp\n    point p1(10, 10); \n    point p2(p1); \n    point p3 = p1;\n```\n\n最后一行看起来像是赋值操作符，但实际上它调用了复制构造函数。复制构造函数可以这样实现:\n\n```cpp\n    class point \n    { \n        int x = 0;int y = 0; \n    public: \n        point(const point& rhs) : x(rhs.x), y(rhs.y) {} \n    };\n```\n\n初始化访问另一个对象(`rhs`)上的`private`数据成员。这是可以接受的，因为构造函数参数与正在创建的对象属于同一类型。复制操作可能没有这么简单。例如，如果类包含指针数据成员，您很可能希望复制指针指向的数据，这将涉及在新对象中创建新的内存缓冲区。\n\n# 在类型之间转换\n\n您也可以执行转换。在数学中，你可以定义一个代表方向的向量，这样两点之间画的线就是向量。在我们的代码中，我们已经定义了一个`point`类和一个`cartesian_vector`类。您可以决定使用一个构造函数，在原点和一个点之间创建一个向量，在这种情况下，您将把一个`point`对象转换成一个`cartesian_vector`对象:\n\n```cpp\n    class cartesian_vector \n    { \n        double x; double y;  \n    public: \n        cartesian_vector(const point& p) : x(p.x), y(p.y) {} \n    };\n```\n\n这里有一个问题，我们稍后会解决。转换可以这样调用:\n\n```cpp\n    point p(10, 10); \n    cartesian_vector v1(p); \n    cartesian_vector v2 { p }; \n    cartesian_vector v3 = p;\n```\n\n# 交朋友\n\n上面代码的问题是`cartesian_vector`类访问`point`类的`private`成员。既然我们两个班都写了，我们很乐意通融一下，所以我们把`cartesian_vector`班定为`point`班的`friend`班:\n\n```cpp\n    class cartesian_vector; // forward decalartion \n\n    class point \n    { \n        double x; double y; \n    public: \n        point(double x, double y) : x(x), y(y){} \n        friend class cartesian_point; \n    };\n```\n\n由于`cartesian_vector`类是在`point`类之后声明的，我们必须提供一个正向声明，它本质上告诉编译器名称`cartesian_vector`即将被使用，它将在其他地方声明。重要的一行从`friend`开始。这表明整个类的代码`cartesian_vector`可以访问`point`类的私有成员(数据和方法)。\n\n也可以声明`friend`函数。例如，您可以声明一个操作符，以便将一个`point`对象插入到`cout`对象中，这样就可以将它打印到控制台上。您不能更改`ostream`类，但可以定义一个全局方法:\n\n```cpp\n    ostream& operator<<(ostream& stm, const point& pt) \n    { \n        stm << \"(\" << pt.x << \",\" << pt.y << \")\"; \n        return stm; \n    }\n```\n\n该函数访问`point`的`private`成员，因此您必须使用以下命令将该函数设为`point`类的`friend`:\n\n```cpp\n    friend ostream& operator<<(ostream&, const point&);\n```\n\n这样的`friend`声明必须在`point`类中声明，但是放在`public`还是`private`部分无关紧要。\n\n# 将构造函数标记为显式\n\n在某些情况下，您不希望允许在作为另一种类型的构造函数的参数传递的一种类型之间进行隐式转换。为此，需要用`explicit`说明符标记构造函数。这意味着现在调用构造函数的唯一方法是使用括号语法:*显式地*调用构造函数。在下面的代码中，您不能将`double`隐式转换为`mytype`的对象:\n\n```cpp\n    class mytype  \n    { \n    public: \n        explicit mytype(double x); \n    };\n```\n\n现在，如果您想要创建一个带有`double`参数的对象，您必须显式地*调用构造函数:*\n\n```cpp\n    mytype t1 = 10.0; // will not compile, cannot convert \n    mytype t2(10.0);  // OK\n```\n\n# 析构对象\n\n当一个对象被销毁时，调用一个称为析构函数的特殊方法。该方法的类名以`~`符号为前缀，不返回值。\n\n如果对象是堆栈上的自动变量，那么当变量超出范围时，它将被销毁。当通过值传递对象时，在被调用函数的堆栈上进行复制，并且当被调用函数完成时，对象将被销毁。此外，函数如何完成并不重要，无论是对`return`的显式调用还是到达最后一个大括号，或者是否引发异常；在所有这些情况下，都会调用析构函数。如果一个函数中有多个对象，析构函数的调用顺序与同一作用域中对象的构造顺序相反。如果您创建了一个对象数组，那么在声明数组的语句上，将为数组中的每个对象调用默认构造函数，所有对象都将被销毁，并且当数组超出范围时，将调用每个对象上的析构函数。\n\n以下是一些例子，对于一个类`mytype`:\n\n```cpp\n    void f(mytype t) // copy created \n    { \n        // use t \n    }   // t destroyed \n\n    void g() \n    { \n        mytype t1; \n        f(t1); \n        if (true) \n        { \n            mytype t2; \n        }   // t2 destroyed \n\n        mytype arr[4]; \n    }  // 4 objects in arr destroyed in reverse order to creation \n       // t1 destroyed\n```\n\n当您返回一个对象时，会发生一个有趣的动作。以下注释是您所期望的:\n\n```cpp\n    mytype get_object() \n    { \n        mytype t;               // default constructor creates t \n        return t;               // copy constructor creates a temporary \n    }                           // t destroyed \n\n    void h() \n    { \n        test tt = get_object(); // copy constructor creates tt \n    }                           // temporary destroyed, tt destroyed\n```\n\n事实上，流程更加精简。在调试版本中，编译器将看到在`get_object`函数返回时创建的临时对象是将用作变量`tt`的对象，因此在`get_object`函数的返回值上没有额外的副本。这个函数实际上是这样的:\n\n```cpp\n    void h() \n    { \n        mytype tt = get_object();  \n    }   // tt destroyed\n```\n\n然而，编译器能够进一步优化代码。在发布版本中(启用优化)，不会创建临时对象，调用函数中的对象`tt`将是在`get_object`中创建的实际对象`t`。\n\n当您显式删除指向在空闲存储上分配的对象的指针时，对象将被销毁。在这种情况下，对析构函数的调用是确定性的:当您的代码调用`delete`时，它被调用。同样，对于同一个类`mytype`，如下所示:\n\n```cpp\n    mytype *get_object() \n    { \n        return new mytype; // default constructor called \n    } \n\n    void f() \n    { \n        mytype *p = get_object(); \n        // use p \n        delete p;        // object destroyed \n    }\n```\n\n有些时候，你想使用删除一个对象的确定性方面(可能有忘记调用`delete`的危险)，有些时候，你更希望得到一个对象将在适当的时候被销毁的保证(可能会在更晚的时候)。\n\n如果类中的数据成员是带有析构函数的自定义类型，那么当包含对象被销毁时，包含对象上的析构函数也被调用。尽管如此，请注意，这只是在*对象*是班级成员的情况下。如果类成员是指向自由存储中对象的指针，那么您必须在包含对象的析构函数中显式删除该指针。但是你需要知道指针指向的对象在哪里，因为如果不在自由存储区，或者该对象被其他对象使用，调用`delete`就会出现问题。\n\n# 分配对象\n\n当已经创建的*对象被赋值给另一个对象的值时，调用赋值运算符。默认情况下，您将获得复制所有数据成员的复制赋值运算符。这不一定是您想要的，特别是如果对象有一个数据成员是指针，在这种情况下，您的意图更可能是进行深度复制并复制所指向的数据，而不是指针的值(在后一种情况下，*两个*对象将指向相同的数据)。*\n\n *如果定义了复制构造函数，仍然会得到默认的复制赋值运算符；但是，如果您认为编写自己的复制构造函数很重要，那么您还应该提供一个自定义的复制赋值运算符，这是有意义的。(同样，如果定义了复制赋值运算符，除非定义了，否则将获得默认的复制构造函数。)\n\n复制赋值操作符通常是类的一个`public`成员，它引用将用于提供赋值的对象。赋值运算符的语义是您可以将它们链接起来，因此，例如，这段代码在两个对象上调用赋值运算符:\n\n```cpp\n    buffer a, b, c;              // default constructors called \n    // do something with them \n    a = b = c;                   // make them all the same value \n    a.operator=(b.operator=(c)); // make them all the same value\n```\n\n最后两行做同样的事情，但显然第一行更易读。要启用这些语义，赋值运算符必须返回对已赋值对象的引用。所以，类`buffer`会有以下方法:\n\n```cpp\n    class buffer \n    { \n        // data members \n    public: \n        buffer(const buffer&);            // copy constructor \n        buffer& operator=(const buffer&); // copy assignment \n    };\n```\n\n虽然复制构造函数和复制赋值方法看起来类似，但有一个关键的区别。复制构造函数创建一个在调用之前不存在的新对象。调用代码知道，如果构造失败，将引发异常。通过赋值，两个对象都已经存在，因此您正在将值从一个对象复制到另一个对象。这应该被视为一个原子操作，所有的复制都应该被执行；不可接受的是，分配中途失败，导致对象同时是两个对象的一部分。\n\n此外，在构造中，对象只在构造成功后才存在，因此复制构造不能发生在对象本身上，但是代码将对象分配给它自己是完全合法的(如果没有意义的话)。复印作业需要检查这种情况并采取适当的措施。\n\n有各种各样的策略可以做到这一点，其中一个常见的被称为复制和交换习惯用法，因为它使用了标为`noexcept`的标准库`swap`函数，并且不会抛出异常。这个习惯用法包括在赋值的右边创建一个对象的临时副本，然后用左边对象的数据成员交换它的数据成员。\n\n# 移动语义\n\nC++ 11 通过移动构造函数和移动赋值操作符提供移动语义，当临时对象用于创建另一个对象或被分配给现有对象时，将调用这些操作符。在这两种情况下，因为临时对象不会超出语句，所以临时对象的内容可以移动到另一个对象，使临时对象处于无效状态。编译器将通过将数据从临时对象移动到新创建(或分配给)的对象的默认操作来为您创建这些函数。\n\n您可以编写自己的版本，为了指示移动语义，这些版本有一个参数是右值引用(`&&`)。\n\nIf you want the compiler to provide you with a default version of any of these methods, you can provide the prototype in the class declaration suffixed with `=default`. In most cases, this is self-documenting rather than being a requirement, but if you are writing a POD class you must use the default versions of these functions, otherwise `is_pod` will not return `true`.\n\n如果你想只使用移动而不使用复制(例如，一个文件句柄类)，那么你可以*删除*的复制功能:\n\n```cpp\n    class mytype \n    { \n        int *p; \n    public: \n        mytype(const mytype&) = delete;             // copy constructor \n        mytype& operator= (const mytype&) = delete; // copy assignment \n        mytype&(mytype&&);                          // move constructor \n        mytype& operator=(mytype&&);                // move assignment \n    };\n```\n\n这个类有一个指针数据成员，并允许移动语义，在这种情况下，移动构造函数将通过引用临时对象来调用。由于对象是临时的，因此在移动构造函数调用后，它将不会存在。这意味着新对象可以*将*临时对象的状态移入自身:\n\n```cpp\n    mytype::mytype(mytype&& tmp) \n    { \n        this->p = tmp.p; \n        tmp.p = nullptr; \n    }\n```\n\n移动构造函数将临时对象的指针分配给`nullptr`，这样为类定义的任何析构函数都不会试图删除指针。\n\n# 声明静态成员\n\n您可以声明类的成员-数据成员或方法- `static`。这在某些方面类似于如何在文件范围内声明的自动变量和函数上使用`static`关键字，但是当在类成员上使用时，该关键字有一些重要且不同的属性。\n\n# 定义静态成员\n\n当您在类成员上使用`static`时，这意味着该项与类相关联，而不是与特定的实例相关联。在数据成员的情况下，这意味着类的所有实例共享一个数据项。同样，一个`static`方法没有附加到一个对象上，它不是`__thiscall`，也没有`this`指针。\n\n一个`static`方法是一个类的命名空间的一部分，所以它可以为这个类创建对象，并且可以访问它们的`private`成员。一个`static`方法默认有`__cdecl`调用约定，但是如果愿意可以声明为`__stdcall`。这意味着，您可以在类中编写一个方法，用于初始化 C 类指针，许多库都使用这些指针。注意`static`函数不能调用类上的非静态方法，因为非静态方法需要`this`指针，但是非静态方法可以调用`static`方法。\n\n通过对象调用非静态方法，使用点运算符(对于类实例)或对象指针的`->`运算符。一个`static`方法不需要关联对象，但是可以通过一个来调用。\n\n这给出了两种调用`static`方法的方法，通过对象或通过`class`名称:\n\n```cpp\n    class mytype \n    { \n    public: \n        static void f(){} \n        void g(){ f(); } \n    };\n```\n\n这里，类定义了一个名为`f`的`static`方法和一个名为`g`的非静态方法。非静态方法`g`可以调用`static`方法，但是`static`方法`f`不能调用非静态方法。既然`static`方法`f`是`public`，那么`class`之外的代码可以称之为:\n\n```cpp\n    mytype c; \n    c.g();       // call the nonstatic method \n    c.f();       // can also call the static method thru an object \n    mytype::f(); // call static method without an object\n```\n\n虽然`static`函数可以通过一个对象来调用，但是你根本不需要创建任何对象来调用它。\n\n静态数据成员需要多一点工作，因为当你使用`static`时，它表示数据成员不是对象的一部分，通常数据成员是在创建对象时分配的。您必须在类外定义`static`数据成员:\n\n```cpp\n    class mytype \n    { \n    public: \n        static int i; \n        static void incr() { i++ ; } \n    }; \n\n    // in a source file \n    int mytype::i = 42;\n```\n\n数据成员是在类之外的文件范围内定义的。它是使用`class`名称命名的，但请注意，它也必须使用类型定义。在这种情况下，数据成员用一个值初始化；如果不这样做，那么在第一次使用该变量时，它将具有该类型的默认值(在本例中为零)。如果选择在头文件中声明类(这是常见的)，则`static`数据成员的定义必须在源文件中。\n\n也可以在`static`方法中声明一个变量。在这种情况下，该值在所有对象中的方法调用之间保持不变，因此它具有与`static class`成员相同的效果，但是您没有在类外部定义变量的问题。\n\n# 使用静态和全局对象\n\n全局函数中的`static`变量将在第一次调用该函数之前的某个时间点创建。类似地，作为类成员的`static`对象将在首次被访问之前的某个时刻被初始化。\n\n静态和全局对象在调用`main`函数之前被构造，在`main`函数完成之后被销毁。初始化的顺序有一些问题。C++ 标准规定`static`和源文件中定义的全局对象的初始化将在使用该源文件中定义的任何函数或对象之前进行，如果源文件中有几个全局对象，它们将按照*定义的顺序进行初始化。问题是，如果您有几个源文件，每个源文件中都有`static`对象。无法保证这些对象的初始化顺序。如果一个`static`对象依赖于另一个`static`对象，这就成了一个问题，因为你不能保证依赖对象会在它所依赖的对象之后被创建。*\n\n# 命名构造函数\n\n这是`public static`方法的一个应用。这个想法是因为`static`方法是`class`的成员，这意味着它可以访问`class`实例的`private`成员，所以这样的方法可以创建一个对象，执行一些额外的初始化，然后将对象返回给调用者。这是一个**工厂方法**。到目前为止使用的`point`类是使用笛卡尔点构建的，但是我们也可以基于极坐标创建一个点，其中`(x, y)`笛卡尔坐标可以计算为:\n\n```cpp\n    x = r * cos(theta) \n    y = r * sin(theta)\n```\n\n这里`r`是矢量到点的长度，`theta`是这个矢量逆时针方向相对于 x 轴的角度。`point`类已经有了一个接受两个`double`值的构造函数，所以我们不能用它来传递极坐标；相反，我们可以使用一个`static`方法作为一个*命名的构造函数*:\n\n```cpp\n    class point \n    { \n        double x; double y; \n    public: \n        point(double x, double y) : x(x), y(y){} \n        static point polar(double r, double th) \n        { \n            return point(r * cos(th), r * sin(th)); \n        } \n    };\n```\n\n方法可以这样调用:\n\n```cpp\n    const double pi = 3.141529; \n    const double root2 = sqrt(2); \n    point p11 = point::polar(root2, pi/4);\n```\n\n物体`p11`是笛卡尔坐标为(1，1)的`point`。在这个例子中，`polar`方法调用了一个`public`构造函数，但是它可以访问私有成员，所以同样的方法可以写成(效率较低):\n\n```cpp\n    point point::polar(double r, double th) \n    { \n        point pt; \n        pt.x = r * cos(th); \n        pt.y = r * sin(th); \n        return pt; \n    }\n```\n\n# 嵌套类\n\n您可以在类中定义一个类。如果嵌套类被声明为`public`，那么您可以在容器类中创建对象，并将它们返回给外部代码。但是，通常您会想要声明一个由该类使用的类，并且应该是`private`。下面声明一个`public`嵌套类:\n\n```cpp\n    class outer \n    { \n    public: \n        class inner  \n        { \n        public: \n            void f(); \n        }; \n\n        inner g() { return inner(); } \n    }; \n\n    void outer::inner::f() \n    { \n         // do something \n    }\n```\n\n请注意嵌套类的名称如何以包含类的名称作为前缀。\n\n# 访问常量对象\n\n到目前为止，您已经看到了许多使用`const`的例子，最常见的可能是将其作为函数参数应用于引用，以向编译器指示该函数对对象只有只读访问权。使用这样的`const`引用是为了通过引用传递对象，以避免如果通过值传递对象时会发生的复制开销。`class`上的方法可以访问对象数据成员，并可能更改它们，因此如果您通过`const`引用传递对象，编译器将只允许该引用调用不更改对象的方法。前面定义的`point`类有两个访问器来访问类中的数据:\n\n```cpp\n    class point \n    { \n        double x; double y; \n    public: \n        double get_x() { return x; } \n        double get_y() { return y: } \n    };\n```\n\n如果您定义了一个引用了`const`的函数，并且您试图调用这些访问器，您将从编译器得到一个错误:\n\n```cpp\n    void print_point(const point& p) \n    { \n        cout << \"(\" << p.get_x() << \",\" << p.get_y() << \")\" << endl; \n    }\n```\n\n编译器的错误有点模糊:\n\n```cpp\ncannot convert 'this' pointer from 'const point' to 'point &'\n```\n\n这个消息是编译器抱怨对象是`const`，是不可变的，不知道这些方法是否会保留对象的状态。解决方法很简单——将`const`关键字添加到不改变对象状态的方法中，如下所示:\n\n```cpp\n    double get_x() const { return x; } \n    double get_y() const { return y: }\n```\n\n这实际上意味着`this`指针是`const`。`const`关键字是函数原型的一部分，因此该方法可以重载于此。可以有一个方法在`const`对象上被调用，另一个方法在非常数对象上被调用。这使您能够实现写时复制模式，例如，`const`方法将返回对数据的只读访问，而非常规方法将返回可写数据的*副本*。\n\n当然，标有`const`的方法不能改变数据成员，哪怕是暂时的。所以，这样的方法只能调用`const`方法。可能很少有数据成员被设计为通过`const`对象进行更改的情况；在这种情况下，成员的声明标有`mutable`关键字。\n\n# 使用带指针的对象\n\n可以在自由存储上创建对象，并通过类型化指针进行访问。这提供了更多的灵活性，因为将指针传递给函数是有效的，并且您可以显式地确定对象的生存期，因为对象是通过调用`new`创建的，而通过调用`delete`销毁的。\n\n# 获取指向对象成员的指针\n\n如果需要通过实例访问类数据成员的地址(假设数据成员为`public`，只需使用`&`运算符:\n\n```cpp\n    struct point { double x; double y; }; \n    point p { 10.0, 10.0 }; \n    int *pp = &p.x;\n```\n\n在这种情况下`struct`用来声明`point`，这样成员默认为`public`。第二行使用初始化列表构造一个具有两个值的`point`对象，然后最后一行获得一个指向其中一个数据成员的指针。当然，指针不能在对象被销毁后使用。数据成员是在内存中分配的(在本例中是在堆栈上)，所以地址操作符只是获取一个指向该内存的指针。\n\n函数指针是另一种情况。不管创建了多少个`class`实例，内存中只会有一个方法副本，但是因为方法是使用`__thiscall`调用约定(带有隐藏的`this`参数)调用的，所以您必须有一个函数指针，该指针可以用指向对象的指针来初始化，以提供`this`指针。考虑一下这个`class`:\n\n```cpp\n    class cartesian_vector \n    { \n    public: \n        // other items \n        double get_magnitude() const \n        { \n            return std::sqrt((this->x * this->x) + (this->y * this->y)); \n        }  \n    };\n```\n\n我们可以这样定义一个指向`get_magnitude`方法的函数指针:\n\n```cpp\n    double (cartesian_vector::*fn)() const = nullptr; \n    fn = &cartesian_vector::get_magnitude;\n```\n\n第一行声明一个函数指针。这类似于 C 函数指针声明，除了在指针类型中包含`class`名称。这是需要的，以便编译器知道它必须通过这个指针在任何调用中提供一个`this`指针。第二行获得指向该方法的指针。请注意，不涉及任何对象。您没有获得指向对象上的方法的函数指针；您将获得一个指向必须通过对象调用的`class`上的方法的指针。要通过这个指针调用方法，需要使用指向对象上的成员操作符`.*`的指针:\n\n```cpp\n    cartesian_vector vec(1.0, 1.0); \n    double mag = (vec.*fn)();\n```\n\n第一行创建一个对象，第二行调用方法。指向成员操作符的指针表示*右侧*上的函数指针被*左侧*上的对象调用。左侧对象的地址用于调用方法时的`this`指针。由于这是一个方法，我们需要提供一个参数列表，在这种情况下它是空的(如果您有参数，它们将在这个语句右边的一对括号中)。如果您有一个对象指针，那么语法是相似的，但是您使用`->*`指针指向成员操作符:\n\n```cpp\n    cartesian_vector *pvec = new cartesian_vector(1.0, 1.0); \n    double mag = (pvec->*fn)(); \n    delete pvec;\n```\n\n# 操作员超载\n\n类型的行为之一是您可以对其应用的操作。C++ 允许您将 C++ 运算符作为类的一部分重载，这样就可以清楚地看到运算符对类型起作用。这意味着对于一元运算符，成员方法应该没有参数，对于二元运算符，您只需要一个参数，因为当前对象将位于运算符的左侧，因此方法参数是右侧的项。下表总结了如何实现一元和二元运算符以及四种异常:\n\n| **表达式** | **名称** | **成员法** | **非成员功能** |\n| +a/-a | 前缀一元 | 操作员() | 操作员(a) |\n| a，b | 二进制的 | 操作员(b) | 操作员(a，b) |\n| a+/a- | 后缀一元 | 运算符(0) | 运算符(a，0) |\n| a=b | 分配 | 运算符=(b) |  |\n| a(b) | 函数调用 | 操作员()(b) |  |\n| a[b] | 索引 | 运算符[](b) |  |\n| a-> | 指针访问 | 操作员->( |  |\n\n这里■符号用于表示任何可接受的一元或二元运算符，表中提到的四个运算符除外。\n\n对于运算符应该返回什么没有严格的规则，但是如果自定义类型上的运算符的行为类似于内置类型上的运算符，这将会有所帮助。还必须有一些一致性。如果实现`+`操作符将两个对象加在一起，那么`+=`操作符应该使用相同的加号动作。此外，你可能会争辩说，正动作也将决定负动作应该是什么样子，因此`-`和`-=`操作符。同样，如果要定义`<`算子，那么也要定义`<=. >`、`>=`、`==`和`!=`。\n\n标准库的算法(例如`sort`)将只期望在自定义类型上定义`<`运算符。\n\n该表显示，您可以将几乎所有运算符实现为自定义类型类的成员或全局函数(列出的四个必须是成员方法的运算符除外)。一般来说，最好将运算符作为类的一部分来实现，因为它维护封装:成员函数可以访问类的非公共成员。\n\n一元运算符的一个例子是一元负运算符。这通常不会改变一个对象，但会返回一个新的对象，即该对象的*负*。对于我们的`point class`，这意味着使两个坐标都为负，这相当于一条直线上笛卡尔点的镜像 *y = -x* :\n\n```cpp\n    // inline in point \n    point operator-() const \n    { \n        return point(-this->x, -this->y); \n    }\n```\n\n运算符被声明为`const`，因为很明显运算符不会改变对象，因此在`const`对象上被调用是安全的。操作符可以这样调用:\n\n```cpp\n    point p1(-1,1); \n    point p2 = -p1; // p2 is (1,-1)\n```\n\n为了理解我们为什么这样实现操作符，回顾一下一元操作符在应用于内置类型时会做什么。这里的第二种说法`int i, j=0; i = -j;`，只会改动`i`，不会改动`j`，所以成员`operator-`不应该影响对象的价值。\n\n二元负运算符有不同的含义。首先，它有两个操作数，其次，在这个例子中，结果与操作数是不同的类型，因为结果是一个向量，通过从一个点离开另一个点来指示方向。假设`cartesian_vector`已经用具有两个参数的构造函数定义，那么我们可以写:\n\n```cpp\n    cartesian_vector point::operator-(point& rhs) const \n    { \n        return cartesian_vector(this->x - rhs.x, this->y - rhs.y); \n    }\n```\n\n递增和递减操作符有一种特殊的语法，因为它们是一元操作符，可以加前缀或后缀，而且它们会改变应用到的对象。这两个操作符的主要区别在于后置操作符在增量/减量操作之前返回对象*的值，因此必须创建一个临时的。因此，前缀运算符几乎总是比后缀运算符具有更好的性能。在类定义中，为了区分两者，前缀运算符没有参数，后缀运算符有一个伪参数(在上表中，给出了 0)。对于一个类`mytype`，如下所示:*\n\n```cpp\n    class mytype  \n    { \n    public: \n        mytype& operator++() \n        {  \n            // do actual increment \n            return *this; \n        } \n        mytype operator++(int) \n        { \n            mytype tmp(*this); \n            operator++(); // call the prefix code \n            return tmp; \n        } \n    };\n```\n\n实际的增量代码由前缀运算符实现，后缀运算符通过显式调用方法来使用该逻辑。\n\n# 定义函数类\n\n函子是实现`()`运算符的类。这意味着您可以使用与函数相同的语法来调用对象。考虑一下:\n\n```cpp\n    class factor \n    { \n        double f = 1.0; \n    public: \n        factor(double d) : f(d) {} \n        double operator()(double x) const { return f * x; }  \n    };\n```\n\n这段代码可以这样调用:\n\n```cpp\n    factor threeTimes(3);        // create the functor object \n    double ten = 10.0; \n    double d1 = threeTimes(ten); // calls operator(double) \n    double d2 = threeTimes(d1);  // calls operator(double)\n```\n\n这段代码表明 functor 对象不仅提供了一些行为(在这种情况下，对参数执行一个操作)，而且它还可以有一个状态。前面两行是通过对象上的`operator()`方法调用的:\n\n```cpp\n    double d2 = threeTimes.operator()(d1);\n```\n\n看看语法。functor 对象被调用时就好像它是这样声明的函数:\n\n```cpp\n    double multiply_by_3(double d) \n    { \n        return 3 * d;  \n    }\n```\n\n假设你想传递一个指向函数的指针——也许你想让函数的行为被外部代码改变。为了能够使用函子或方法指针，您需要重载您的函数:\n\n```cpp\n    void print_value(double d, factor& fn); \n    void print_value(double d, double(*fn)(double));\n```\n\n第一个引用了一个函子对象。第二个有一个 C 型函数指针(可以传递一个指向`multiply_by_3`的指针)而且相当不可读。在这两种情况下，`fn`参数在实现代码中以相同的方式被调用，但是您需要声明两个函数，因为它们是不同的类型。现在，考虑函数模板的魔力:\n\n```cpp\n    template<typename Fn> \n    void print_value(double d, Fn& fn) \n    { \n        double ret = fn(d); \n        cout << ret << endl; \n    }\n```\n\n这是通用代码；`Fn`类型可以是 C 函数指针，也可以是函子`class`，编译器会生成合适的代码。\n\nThis code can be called by either passing a function pointer to a global function, which will have the `__cdecl` calling convention, or a functor object where the `operator()` operator will be called, which has a `__thiscall` calling convention.\n\n这仅仅是一个实现细节，但它确实意味着您可以编写一个泛型函数，该函数可以采用类似 C 的函数指针或 functor 对象作为参数。C++ 标准库使用了这种魔力，这意味着它提供的算法可以用*全局函数*或*函子*或*λ表达式*来调用。\n\n标准库算法使用三种类型的函数类、生成器以及一元和二元函数；即具有零个、一个或两个参数的函数。此外，标准库调用返回`bool` **谓词**的函数对象(一元或二元)。文档将告诉您是否需要谓词、一元函数或二元函数。旧版本的标准库需要知道返回值的类型和函数对象的参数(如果有的话)才能工作，因此，函子类必须基于标准类`unary_function`和`binary_function`。在 C++ 11 中，这个需求已经被移除，所以不需要使用这些类。\n\n在某些情况下，当需要一元函子时，您会希望使用二元函子。例如，标准库定义了`greater`类，当用作函数对象时，该类采用两个参数和一个`bool`来确定第一个参数是否大于第二个参数，使用由两个参数的类型定义的`operator>`。这将用于需要二进制函子的函数，因此该函数将比较两个值；例如:\n\n```cpp\n    template<typename Fn>  \n    int compare_vals(vector<double> d1, vector<double> d2, Fn compare) \n    { \n        if (d1.size() > d2.size()) return -1; // error \n        int c = 0; \n        for (size_t i = 0; i < d1.size(); ++ i) \n        { \n            if (compare(d1[i], d2[i])) c++ ; \n        } \n        return c; \n    }\n```\n\n这将获取两个集合，并使用作为最后一个参数传递的 functor 来比较相应的项。可以这样称呼:\n\n```cpp\n    vector<double> d1{ 1.0, 2.0, 3.0, 4.0 }; \n    vector<double> d2{ 1.0, 1.0, 2.0, 5.0 }; \n    int c = compare_vals(d1, d2, greater<double>());\n```\n\n`greater`函子类在`<functional>`头中定义，并使用为该类型定义的`operator>`比较两个数字。如果您想将容器中的项目与固定值进行比较，该怎么办；也就是调用函子上的`operator()(double, double)`方法时，一个参数总是有固定值？一种选择是定义一个有状态的 functor 类(如前所示)，这样固定值就是 functor 对象的成员。另一种方法是用固定值填充另一个`vector`，并继续比较两个`vector` s(对于大型`vector` s 来说，这可能会变得相当昂贵)。\n\n另一种方法是重用函子类，但是要将*的一个值绑定到它的一个参数上。一个版本的`compare_vals`功能可以这样写，就拿一个`vector`来说:*\n\n```cpp\n    template<typename Fn>  \n    int compare_vals(vector<double> d, Fn compare) \n    { \n        int c = 0; \n        for (size_t i = 0; i < d.size(); ++ i) \n        { \n            if (compare(d[i]) c++ ; \n        } \n        return c; \n    }\n```\n\n编写代码是为了只对一个值调用 functor 参数，因为它假设 functor 对象包含另一个要比较的值。这是通过将 functor 类绑定到参数来实现的:\n\n```cpp\n    using namespace::std::placeholders; \n    int c = compare_vals(d1, bind(greater<double>(), _1, 2.0));\n```\n\n`bind`函数是可变的。第一个参数是函子对象，后面是将传递给函子的`operator()`方法的参数。`compare_vals`函数被传递一个**绑定器**对象，该对象将函子绑定到值。在`compare_vals`函数中，`compare(d[i])`中对函子的调用实际上是对 binder 对象的`operator()`方法的调用，该方法将参数`d[i]`和绑定值转发给函子的`operator()`方法。\n\n在对`bind`的调用中，如果提供了一个实际值(这里是`2.0`)，那么该值将被传递给在对函子的调用中该位置的函子(这里是`2,0`被传递给第二个参数)。如果使用以下划线开头的符号，则它是**占位符**。在`std::placeholders`命名空间中定义了 20 个这样的符号(`_1`到`_20`)。占位符的意思是“使用在此位置传递给 binder 对象的值`operator()`方法调用来调用占位符指示的函子调用`operator()`方法。”因此，这个调用中的占位符意味着“从调用绑定器传递第一个参数，并将其传递给`greater`函子`operator()`的第一个参数。”\n\n之前的代码将`vector`和`2.0`中的每一项进行比较，并记录大于`2.0`的项目。您可以这样调用它:\n\n```cpp\n    int c = compare(d1, bind(greater<double>(), 2.0, _1));\n```\n\n参数列表被交换，这意味着`2.0`与`vector`中的每个项目进行比较，并且该功能将记录`2.0`大于该项目的次数。\n\n`bind`函数和占位符是 C++ 11 的新功能。在以前的版本中，您可以使用`bind1st`和`bind2nd`函数将值绑定到函子的第一个或第二个参数。\n\n# 定义转换运算符\n\n我们已经看到，如果您的自定义类型有一个采用您正在转换的类型的构造函数，则构造函数可以用于从另一种类型转换为您的自定义类型。您也可以在另一个方向上执行转换:将对象转换为另一种类型。为此，您需要为不带返回类型的运算符提供要转换为的类型的名称。在这种情况下，您需要在`operator`关键字和名称之间有一个空格:\n\n```cpp\n    class mytype \n    { \n        int i; \n    public: \n        mytype(int i) : i(i) {} \n        explicit mytype(string s) : i(s.size()) {} \n        operator int () const { return i; } \n    };\n```\n\n该代码可以将一个`int`或一个`string`转换为`mytype`；在后一种情况下，只有通过明确提到构造函数。\n\n最后一行允许您将对象转换回`int`:\n\n```cpp\n    string s = \"hello\"; \n    mytype t = mytype(s); // explicit conversion \n    int i = t;            // implicit conversion\n```\n\n您可以制作这样的转换运算符`explicit`，以便仅在使用显式强制转换时调用它们。在许多情况下，您会希望不使用这个关键字，因为当您想要包装类中的资源并使用析构函数为您执行自动资源管理时，隐式转换非常有用。\n\n使用转换运算符的另一个例子是从有状态函子返回值。这里的想法是`operator()`将执行一些动作，结果由函子维护。问题是如何获得函子的这种状态，尤其是当它们经常被创建为临时对象时？转换运算符可以提供此功能。\n\n例如，当您计算平均值时，您分两个阶段进行:第一阶段是累积值，然后第二阶段是通过将其除以项目数来计算平均值。下面的函子类在转换为`double`时执行除法:\n\n```cpp\n    class averager \n    { \n        double total; \n        int count; \n    public: \n        averager() : total(0), count(0) {} \n        void operator()(double d) { total += d; count += 1; } \n        operator double() const \n        {        \n            return (count != 0) ? (total / count) : \n                numeric_limits<double>::signaling_NaN(); \n        } \n    };\n```\n\n这可以这样称呼:\n\n```cpp\n    vector<double> vals { 100.0, 20.0, 30.0 }; \n    double avg = for_each(vals.begin(), vals.end(), averager());\n```\n\n`for_each`函数为`vector`中的每个项目调用函子，`operator()`简单地对传递给它的项目求和并保持计数。有趣的是在`for_each`函数迭代了`vector`中的所有项目之后，它返回了函子，因此有一个到`double`的隐式转换，它调用计算平均值的转换运算符。\n\n# 管理资源\n\n我们已经看到了一种需要小心管理的资源:内存。你用`new`分配内存，当你用完内存后，你必须用`delete`释放内存。未能释放内存将导致内存泄漏。内存可能是最基本的系统资源，但大多数操作系统还有很多其他资源:文件句柄、图形对象句柄、同步对象句柄、线程和进程。有时，拥有这样的资源是排他的，会阻止其他代码访问通过该资源访问的资源。因此，重要的是在某个时候释放这些资源，并且通常是及时释放。\n\n类在这里通过一种叫做**资源获取是初始化** (RAII)的机制提供帮助，这种机制是由 C++ 的作者比雅尼·斯特劳斯特鲁普发明的。简单地说，资源在对象的构造函数中分配，在析构函数中释放，所以这意味着资源的生存期就是对象的生存期。通常，这样的包装对象是在堆栈上分配的，这意味着保证当对象超出范围时资源将被释放*，而不管这是如何发生的*。\n\n因此，如果在循环语句的代码块中声明了对象(`while`、`for`)，那么在每个循环结束时，将调用每个对象的析构函数(按照与创建相反的顺序)，并且当循环重复时，将再次创建对象。无论循环是因为到达了代码块的末尾而重复，还是通过调用`continue`而重复，都会出现这种情况。另一种离开代码块的方法是通过调用`break`，一个`goto`，或者如果代码调用`return`离开该功能。如果代码引发异常(参见[第 7 章](07.html)、*诊断和调试*)，则当对象超出范围时将调用析构函数，因此如果代码由`try`块保护，则在调用`catch`子句之前将调用块中声明的对象的析构函数。如果没有保护块，那么将在函数堆栈被销毁和异常传播之前调用析构函数。\n\n# 编写包装类\n\n编写包装资源的类时，必须解决几个问题。将使用构造函数，或者使用某种库函数(通常通过某种不透明句柄访问)获取资源，或者将资源作为参数。此资源存储为数据成员，因此类中的其他方法可以使用它。资源将在析构函数中使用您的库提供的任何函数来释放。这是最低限度。此外，你还必须考虑如何使用对象。\n\n如果您可以将实例当作资源句柄来使用，这样的包装类通常是最方便的。这意味着您保持相同的编程风格来访问资源，但是您不必太担心释放资源。\n\n您应该考虑是否希望能够在包装类和资源句柄之间进行转换。如果您允许这样做，这意味着您可能必须考虑克隆资源，这样您就不会有句柄的两个副本——一个副本由类管理，另一个副本可以由外部代码释放。您还需要考虑是否允许复制或分配对象，如果是，那么您将需要适当地实现复制构造函数、移动构造函数以及复制和移动分配操作符。\n\n# 使用智能指针\n\nC++ 标准库提供了几个类来包装通过指针访问的资源。为了防止内存泄漏，您必须确保在空闲存储上分配的内存在某个时候被释放。智能指针的思想是，您将一个实例视为指针，因此您可以使用`*`运算符取消引用以访问它所指向的对象，或者使用`->`运算符访问包装对象的成员。智能指针类将管理它包装的指针的生存期，并将适当地释放资源。\n\n标准库有三个智能指针类:`unique_ptr`、`shared_ptr`和`weak_ptr`。每个都处理如何以不同的方式释放资源，以及如何或是否可以复制指针。\n\n# 管理独家所有权\n\n`unique_ptr`类是用指向它将维护的对象的指针构建的。这个类提供操作符`*`来访问对象，取消对包装指针的引用。它还提供了`->`操作符，这样，如果指针是针对一个类的，您可以通过包装的指针访问成员。\n\n下面在空闲存储上分配一个对象，并手动维护其生存期:\n\n```cpp\n    void f1() \n    { \n       int* p = new int; \n       *p = 42; \n       cout << *p << endl; \n       delete p; \n    }\n```\n\n在这种情况下，您会得到一个指向为`int`分配的空闲存储上的内存的指针。要访问内存，无论是对其进行写入还是读取，都需要用`*`操作符取消指针引用。当你使用完指针后，你必须调用`delete`来释放内存并将其返回到空闲存储区。现在考虑相同的代码，但是使用智能指针:\n\n```cpp\n    void f2() \n    { \n       unique_ptr<int> p(new int); \n       *p = 42; \n       cout << *p << endl; \n       delete p.release(); \n    }\n```\n\n两个主要区别在于，智能指针对象是通过调用构造函数显式构造的，该构造函数接受用作模板参数的类型的指针。这种模式强化了资源应该只由智能指针管理的思想。\n\n第二个变化是通过调用智能指针对象上的`release`方法来获取包装指针的所有权，从而释放内存，这样我们就可以显式删除指针。\n\n想想`release`方法从智能指针的所有权中释放指针。在此调用之后，智能指针不再包装资源。`unique_ptr`类还有一个方法`get`，可以访问包装的指针，但是智能指针对象仍然保留所有权；*不要删除通过这种方式获得的指针*！\n\n请注意，一个`unique_ptr`对象包装了一个指针，并且只是指针。这意味着对象在内存中的大小与它包装的指针相同。到目前为止，智能指针增加的很少，所以让我们看看另一种释放资源的方法:\n\n```cpp\n    void f3() \n    { \n       unique_ptr<int> p(new int); \n       *p = 42; \n       cout << *p << endl; \n       p.reset(); \n    }\n```\n\n这是*确定性的*释放资源，意味着资源在你希望它发生的时候被释放，类似于指针的情况。这里的代码不是释放资源本身；它允许智能指针使用**删除器**进行操作。`unique_ptr`的默认删除程序是一个名为`default_delete`的函子类，它调用包装指针上的`delete`运算符。\n\n如果打算使用确定性破坏，首选`reset`方法。您可以通过将自定义函子类的类型作为第二个参数传递给`unique_ptr`模板来提供自己的删除程序:\n\n```cpp\n    template<typename T> struct my_deleter \n    { \n        void operator()(T* ptr)  \n        { \n            cout << \"deleted the object!\" << endl; \n            delete ptr; \n        } \n    };\n```\n\n在代码中，您将指定需要自定义删除程序，如下所示:\n\n```cpp\n    unique_ptr<int, my_deleter<int> > p(new int);\n```\n\n在删除指针之前，您可能需要进行额外的清理，或者指针可以通过`new`以外的机制获得，因此您可以使用自定义删除器来确保调用适当的释放函数。请注意，deleter 是智能指针类的一部分，因此，如果您有两个不同的智能指针以这种方式使用两个不同的 deleter，则智能指针类型是不同的，即使它们包装了相同类型的资源。\n\nWhen you use a custom deleter, the size of a `unique_ptr` object may be larger than the pointer wrapped. If the deleter is a functor object, each smart pointer object will need memory for this, but if you use a lambda expression, no more extra space will be required.\n\n当然，您最有可能允许智能指针为您管理资源生存期，为此，您只需允许智能指针对象超出范围:\n\n```cpp\n    void f4() \n    { \n       unique_ptr<int> p(new int); \n       *p = 42; \n       cout << *p << endl; \n    } // memory is deleted\n```\n\n由于创建的指针是单个对象，这意味着您可以在适当的构造函数上调用`new`运算符来传入初始化参数。`unique_ptr`的构造函数被传递一个指向已经构造的对象的指针，然后类管理该对象的生存期。虽然`unique_ptr`对象可以通过调用其构造函数直接创建，但是不能调用复制构造函数，因此在构造过程中不能使用初始化语法。相反，标准库提供了一个名为`make_unique`的功能。\n\n这有几个重载，因此它是基于此类创建智能指针的首选方式:\n\n```cpp\n    void f5() \n    { \n       unique_ptr<int> p = make_unique<int>(); \n       *p = 42; \n       cout << *p << endl; \n    } // memory is deleted\n```\n\n这段代码将调用包装类型(`int`)上的默认构造函数，但是您可以提供将传递给该类型的适当构造函数的参数。例如，对于具有两个参数的构造函数的`struct`，可以使用以下内容:\n\n```cpp\n    void f6() \n    { \n       unique_ptr<point> p = make_unique<point>(1.0, 1.0); \n       p->x = 42; \n       cout << p->x << \",\" << p->y << endl; \n    } // memory is deleted\n```\n\n`make_unique`函数调用为成员分配非默认值的构造函数。`->`运算符返回一个指针，编译器将通过该指针访问对象成员。\n\n数组还有`unique_ptr`和`make_unique`的特殊化。这个版本的`unique_ptr`的默认删除程序将在指针上调用`delete[]`，因此它将删除数组中的每个对象(并调用每个对象的析构函数)。该类实现了一个索引器操作符(`[]`)，因此您可以访问数组中的每一项。但是，请注意，没有范围检查，因此，像内置数组变量一样，您可以访问数组末尾以外的内容。没有解引用操作符(`*`或`->`，因此基于数组的`unique_ptr`对象只能用数组语法访问。\n\n`make_unique`函数有一个重载，允许您传递要创建的数组的大小，但是您必须单独初始化每个对象:\n\n```cpp\n    unique_ptr<point[]> points = make_unique<point[]>(4);     \n    points[1].x = 10.0; \n    points[1].y = -10.0;\n```\n\n这将创建一个数组，其中四个`point`对象最初设置为默认值，下面的行将第二个点初始化为`(10.0, -10.0)`值。使用`vector`或`array`管理对象数组几乎总是比使用`unique_ptr`更好。\n\nEarlier versions of the C++ Standard Library had a smart pointer class called `auto_ptr`. This was a first attempt, and worked in most cases, but also had some limitations; for example, `auto_ptr` objects could not be stored in Standard Library containers. C++ 11 introduces rvalue references and other language features such as move semantics, and, through these, `unique_ptr` objects can be stored in containers. The `auto_ptr` class is still available through the `<new>` header, but only so that older code can still compile.\n\n关于`unique_ptr`类的重要一点是，它确保指针只有一个副本。这很重要，因为类析构函数会释放资源，所以如果你*可以*复制一个`unique_ptr`对象，这意味着不止一个析构函数会试图释放资源。`unique_ptr`标的拥有*独家所有权*；实例总是拥有它所指向的内容。\n\n您不能复制分配`unique_ptr`智能指针(复制分配操作符和复制构造函数被删除)，但是您可以通过将资源的所有权从源指针转移到目标指针来*移动*智能指针。因此，一个函数可以返回一个`unique_ptr`，因为所有权通过移动语义转移到被赋予该函数值的变量。如果将智能指针放入容器中，会有另一个动作。\n\n# 共享所有权\n\n有时您需要共享一个指针:您可以创建几个对象，并将一个指向单个对象的指针传递给每个对象，这样它们就可以调用这个对象。通常，当一个对象有一个指向另一个对象的指针时，该指针代表一个应该在包含该对象的销毁过程中被销毁的资源。如果一个指针被共享，这意味着当其中一个对象删除该指针时，所有其他对象中的指针都将无效(这被称为**悬空指针**，因为它不再指向一个对象)。您需要一种机制，其中几个对象可以持有一个指针，该指针将保持有效，直到所有使用该指针的对象都表示不再需要使用它为止。\n\nC++ 11 为这个工具提供了`shared_ptr`类。该类在资源上维护一个**引用计数**，该资源的`shared_ptr`的每个副本将增加引用计数。当该资源的`shared_ptr`的一个实例被销毁时，它将减少引用计数。引用计数是共享的，因此它意味着非零值表示至少有一个`shared_ptr`正在访问资源。当最后一个`shared_ptr`对象将引用计数递减为零时，释放资源是安全的。\n\n这意味着引用计数必须以原子方式进行管理，以处理多线程代码。\n\n由于引用计数是共享的，这意味着每个`shared_ptr`对象持有一个指向被称为**控制块**的共享缓冲区的指针，这意味着它持有原始指针和指向控制块的指针，因此每个`shared_ptr`对象将持有比一个`unique_ptr`更多的数据。控制块不仅仅用于参考计数。\n\n可以创建一个`shared_ptr`对象来使用自定义删除器(作为构造函数参数传递)，该删除器存储在控制块中。这很重要，因为这意味着自定义删除器不是智能指针类型的一部分，所以包装相同资源类型但使用不同删除器的几个`shared_ptr`对象仍然是相同的类型，可以放在该类型的容器中。\n\n您可以从另一个`shared_ptr`对象创建一个`shared_ptr`对象，这将使用原始指针和指向控制块的指针初始化新对象，*和*增加参考计数。\n\n```cpp\n    point* p = new point(1.0, 1.0); \n    shared_ptr<point> sp1(p); // Important, do not use p after this! \n    shared_ptr<point> sp2(sp1); \n    p = nullptr; \n    sp2->x = 2.0; \n    sp1->y = 2.0; \n    sp1.reset(); // get rid of one shared pointer\n```\n\n这里，第一个共享指针是使用原始指针创建的。这不是推荐使用`shared_ptr`的方式。第二个共享指针是使用第一个智能指针创建的，因此现在有两个指向同一资源的共享指针(`p`被分配给`nullptr`以防止其进一步使用)。之后，可以使用`sp1`或`sp2`访问*相同的*资源。在这段代码的末尾，一个共享指针被重置为`nullptr`；这意味着`sp1`在资源上不再有引用计数，您不能使用它来访问资源。但是，您仍然可以使用`sp2`来访问资源，直到它超出范围，或者您调用`reset`。\n\n在这段代码中，智能指针是从一个单独的原始指针创建的。由于共享指针现在已经接管了资源的生存期管理，因此不再使用原始指针是很重要的，在这种情况下，它被分配给`nullptr`。最好避免使用原始指针，标准库通过一个名为`make_shared`的函数来实现，可以这样使用:\n\n```cpp\n    shared_ptr<point> sp1 = make_shared<point>(1.0,1.0);\n```\n\n该函数将使用对`new`的调用来创建指定的对象，由于它需要可变数量的参数，因此您可以使用它来调用包装类上的任何构造函数。\n\n您可以从一个`unique_ptr`对象创建一个`shared_ptr`对象，这意味着指针被*移动*到新对象并创建参考计数控制块。由于资源现在将被共享，这意味着资源不再有独占所有权，因此`unique_ptr`对象中的指针将成为`nullptr`。这意味着您可以拥有一个工厂函数，该函数返回一个指向包装在`unique_ptr`对象中的对象的指针，并且调用代码可以确定它是使用`unique_ptr`对象来独占访问资源，还是使用`shared_ptr`对象来共享资源。\n\n对对象数组使用`shared_ptr`没有什么意义；有很多更好的方法来存储对象集合(`vector`或`array`)。无论如何，都有一个索引操作符(`[]`)，默认删除程序调用`delete`，而不是`delete[]`。\n\n# 处理悬空指针\n\n在这本书的前面，我们指出，当您删除一个资源时，您应该将指针设置为`nullptr`，并且您应该在使用它之前检查一个指针，看看它是否是`nullptr`。这样就不会为已删除的对象调用指向内存的指针:悬空指针。\n\n有些情况下，悬空指针可以通过设计出现。例如，*父*对象可以创建*子*对象，这些对象具有指向父对象的**反向指针**，以便子对象可以访问父对象。(这方面的一个例子是创建子控件的窗口；子控件访问父窗口通常很有用。)在这种情况下使用共享指针的问题是，父控件在每个子控件上都有一个引用计数，而每个子控件在父控件上都有一个引用计数，这就产生了循环依赖。\n\n另一个例子是，如果您有一个观察者对象的容器，目的是能够在事件发生时通过调用每个观察者对象上的方法来通知每个观察者对象。维护这个列表可能很复杂，特别是如果一个观察者对象可以被删除，因此你必须提供一种方法从容器中移除该对象(这里将有一个`shared_ptr`引用计数)，然后你才能完全删除该对象。如果您的代码可以简单地以不维护引用计数的方式将指向对象的指针添加到容器中，但允许您检查指针是否悬空或指向现有对象，那么这将变得更加容易。\n\n这样的指针叫做**弱指针**，C++ 11 标准库提供了一个叫做`weak_ptr`的类。不能直接使用`weak_ptr`对象，也没有取消引用操作符。\n\n相反，您可以从一个`shared_ptr`对象创建一个`weak_ptr`对象，当您想要访问资源时，您可以从`weak_ptr`对象创建一个`shared_ptr`对象。这意味着一个`weak_ptr`对象具有与`shared_ptr`对象相同的原始指针，并且访问相同的控制块，但是它不参与引用计数。\n\n创建后，`weak_ptr`对象将使您能够测试包装指针是指向现有资源还是指向已被销毁的资源。有两种方法可以做到这一点:要么调用成员函数`expired`，要么尝试从`weak_ptr`创建一个`shared_ptr`。如果您正在维护一个`weak_ptr`对象的集合，您可以决定定期迭代集合，对每个对象调用`expired`，如果方法返回`true`，则从集合中移除该对象。由于`weak_ptr`对象可以访问原始`shared_ptr`对象创建的控制块，因此它可以测试参考计数是否为零。\n\n测试`weak_ptr`对象是否悬空的第二种方法是从该对象创建一个`shared_ptr`对象。有两种选择。您可以通过将弱指针传递给其构造函数来创建`shared_ptr`对象，如果指针已经过期，构造函数将抛出`bad_weak_ptr`异常。另一种方法是在弱指针上调用`lock`方法，如果弱指针已经过期，那么`shared_ptr`对象将被分配给`nullptr`，您可以为此进行测试。这里显示了这三种方式:\n\n```cpp\n    shared_ptr<point> sp1 = make_shared<point>(1.0,1.0); \n    weak_ptr<point> wp(sp1); \n\n    // code that may call sp1.reset() or may not \n\n    if (!wp.expired())  { /* can use the resource */} \n\n    shared_ptr<point> sp2 = wp.lock(); \n    if (sp2 != nullptr) { /* can use the resource */} \n\n    try \n    { \n        shared_ptr<point> sp3(wp); \n        // use the pointer \n    } \n    catch(bad_weak_ptr& e) \n    { \n        // dangling weak pointer \n    }\n```\n\n由于弱指针不会改变资源上的引用计数，这意味着您可以将它用作反向指针来打破循环依赖(尽管，通常使用原始指针是有意义的，因为没有父对象，子对象就不能存在)。\n\n# 模板\n\n类可以是模板化的，这意味着您可以编写泛型代码，编译器将使用您的代码使用的类型生成一个类。参数可以是类型、常量整数值或变量版本(零个或多个参数，由使用类的代码提供)。例如:\n\n```cpp\n    template <int N, typename T> class simple_array \n    { \n        T data[N]; \n    public: \n        const T* begin() const { return data; } \n        const T* end() const { return data + N; } \n        int size() const { return N; } \n\n        T& operator[](int idx)  \n        { \n            if (idx < 0 || idx >= N) \n                throw range_error(\"Range 0 to \" + to_string(N)); \n            return data[idx]; \n        }  \n    };\n```\n\n这里有一个非常简单的数组类，它定义了基本的迭代器函数和索引运算符，因此您可以这样调用它:\n\n```cpp\n    simple_array<4, int> four; \n    four[0] = 10; four[1] = 20; four[2] = 30; four[3] = 40; \n    for(int i : four) cout << i << \" \"; // 10 20 30 40 \n    cout << endl; \n    four[4] = -99;            // throws a range_error exception\n```\n\n如果您选择从`class`声明中定义一个函数，那么您需要给出模板及其参数作为`class`名称的一部分:\n\n```cpp\n    template<int N, typename T> \n    T& simple_array<N,T>::operator[](int idx) \n    { \n        if (idx < 0 || idx >= N) \n            throw range_error(\"Range 0 to \" + to_string(N)); \n        return data[idx]; \n    }\n```\n\n模板参数也可以有默认值:\n\n```cpp\n    template<int N, typename T=int> class simple_array \n    { \n        // same as before \n    };\n```\n\n如果您认为应该有模板参数的特定实现，那么您可以提供该版本的代码作为模板的专门化:\n\n```cpp\n    template<int N> class simple_array<N, char> \n    { \n        char data[N]; \n    public: \n        simple_array<N, char>(const char* str)  \n        {  \n            strncpy(data, str, N);  \n        } \n        int size() const { return N; } \n        char& operator[](int idx) \n        { \n            if (idx < 0 || idx >= N) \n                throw range_error(\"Range 0 to \" + to_string(N)); \n            return data[idx]; \n        } \n        operator const char*() const { return data; } \n    };\n```\n\n请注意，使用专门化，您不会从完全模板化的类中获得任何代码；您必须实现您想要提供的所有方法，并且如这里所示，实现与专门化相关但在完全模板化的类中不可用的方法。这个例子是一个**部分特殊化**，意思是它只特殊化一个参数(`T`，数据的类型)。该类将用于类型为`simple_array<n, char>`的声明变量，其中`n`是一个整数。您可以自由使用完全专门化的模板，在这种情况下，它将是固定大小和指定类型的专门化:\n\n```cpp\n    template<> class simple_array<256, char> \n    { \n        char data[256]; \n    public: \n        // etc \n    };\n```\n\n这在这种情况下可能没什么用，但想法是需要 256 个字符的变量会有特殊的代码。\n\n# 使用类\n\n**资源获取是初始化**技术对于管理其他库提供的资源很有用，比如 C 运行时库或 Windows SDK。它简化了您的代码，因为您不必考虑资源句柄将在哪里超出范围，并在每个点提供清理代码。如果清理代码很复杂，通常在 C 代码中看到它被放在一个函数的末尾，函数中的每个退出点都会有一个`goto`跳转到该代码。这会导致混乱的代码。在这个例子中，我们将使用一个类包装 C 文件函数，这样文件句柄的生命周期就可以自动维护。\n\nC 运行时`_findfirst`和`_findnext`功能允许您搜索与模式匹配的文件或目录(包括通配符)。`_findfirst`函数返回一个`intptr_t`，它只与该搜索相关，并被传递给`_findnext`函数以获取后续值。这个`intptr_t`是一个不透明的指针，指向 C 运行时为搜索维护的资源，所以当你完成搜索时，你必须调用`_findclose`来清理与之相关的任何资源。为了防止内存泄漏，调用`_findclose`很重要。\n\n在`Beginning_C++ `文件夹下，创建一个名为`Chapter_06`的文件夹。在 Visual C++ 中，新建一个 C++ 源文件，保存到`Chapter_06`文件夹，调用`search.cpp`。应用将使用标准库控制台和字符串，并且它将使用 C 运行时文件函数，因此将这些行添加到文件的顶部:\n\n```cpp\n    #include <iostream> \n    #include <string> \n    #include <io.h> \n    using namespace std;\n```\n\n应用将以文件搜索模式调用，它将使用 C 函数来搜索文件，因此您将需要一个具有参数的`main`函数。在文件底部添加以下内容:\n\n```cpp\n    void usage() \n    { \n        cout << \"usage: search pattern\" << endl; \n        cout << \"pattern is the file or folder to search for \" \n             << \"with or without wildcards * and ?\" << endl; \n    } \n\n    int main(int argc, char* argv[]) \n    { \n        if (argc < 2) \n        { \n            usage(); \n            return 1; \n        } \n    }\n```\n\n第一件事是为管理这个资源的搜索句柄创建一个包装类。在使用功能上面，增加一个名为`search_handle`的类:\n\n```cpp\n    class search_handle \n    { \n        intptr_t handle; \n    public: \n        search_handle() : handle(-1) {} \n        search_handle(intptr_t p) : handle(p) {} \n        void operator=(intptr_t p) { handle = p; } \n        void close()  \n        { if (handle != -1) _findclose(handle); handle = 0; } \n        ~search_handle() { close(); } \n    };\n```\n\n这个类有一个单独的函数来释放句柄。这是为了让这个类的用户能够尽快释放包装器资源。如果在可能引发异常的代码中使用该对象，将不会直接调用`close`方法，而是调用析构函数。可以使用`intptr_t`值创建包装对象。如果这个值是-1，那么句柄是无效的，所以 close 方法只会在句柄没有这个值的情况下调用`_findclose`。\n\n我们希望此类的对象拥有句柄的独占所有权，因此通过将以下内容放入类的公共部分来删除复制构造函数和复制赋值:\n\n```cpp\n    void operator=(intptr_t p) { handle = p; } \n search_handle(search_handle& h) = delete; void operator=(search_handle& h) = delete;\n```\n\n如果一个对象被移动，那么现有对象中的任何句柄都必须被释放，因此在您刚刚添加的行之后添加以下内容:\n\n```cpp\n    search_handle(search_handle&& h)  { close(); handle = h.handle; } \n    void operator=(search_handle&& h) { close(); handle = h.handle; }\n```\n\n包装类将通过对`_findfirst`的调用来分配，并将被传递给对`_findnext`的调用，因此包装类需要两个操作符:一个转换为`intptr_t`，因此该类的对象可以在任何需要`intptr_t`的地方使用，另一个这样的对象可以在需要`bool`的时候使用。将这些添加到课程的`public`部分:\n\n```cpp\n    operator bool() const { return (handle != -1); } \n    operator intptr_t() const { return handle; }\n```\n\n到`bool`的转换允许您编写如下代码:\n\n```cpp\n    search_handle handle = /* initialize it */; \n    if (!handle) { /* handle is invalid */ }\n```\n\n如果你有一个返回指针的转换操作符，那么编译器会优先调用这个来转换到`bool`。\n\n你应该可以编译这段代码(记得使用`/EHsc`开关)确认没有错别字。\n\n接下来，编写一个包装类来执行搜索。在`search_handle`类下面，增加一个`file_search`类:\n\n```cpp\n    class file_search \n    { \n        search_handle handle; \n        string search; \n    public: \n        file_search(const char* str) : search(str) {} \n        file_search(const string& str) : search(str) {} \n    };\n```\n\n这个类是用搜索条件创建的，我们可以选择传递一个 C 或 C++ 字符串。该类有一个`search_handle`数据成员，由于默认析构函数将调用成员对象的析构函数，我们不需要自己提供析构函数。但是，我们将添加一个`close`方法，以便用户可以显式释放资源。此外，为了让类的用户能够确定搜索路径，我们需要一个访问器。在类的底部，添加以下内容:\n\n```cpp\n    const char* path() const { return search.c_str(); } \n    void close() { handle.close(); }\n```\n\n我们不希望复制`file_search`对象的实例，因为这意味着要复制两个搜索句柄。您可以删除复制构造函数和赋值操作符，但是没有必要。试试这个:在`main`功能中，添加这个测试代码(在哪里不重要):\n\n```cpp\n    file_search f1(\"\"); \n    file_search f2 = f1;\n```\n\n编译代码。你会得到一个错误和一个解释:\n\n```cpp\n error C2280: 'file_search::file_search(file_search &)': attempting to reference a deleted function\n note: compiler has generated 'file_search::file_search' here\n```\n\n如果没有复制构造函数，编译器将生成一个(这是第二行)。第一行有点奇怪，因为它说你试图调用编译器生成的删除的方法！实际上，错误是说生成的复制构造函数试图复制`handle`数据成员和已经被删除的`search_handle`复制构造函数。因此，您可以在不添加任何其他代码的情况下防止复制`file_search`对象。删除您刚刚添加的测试行。\n\n接下来在`main`功能的底部添加以下几行。这将创建一个`file_search`对象，并将信息打印到控制台。\n\n```cpp\n    file_search files(argv[1]); \n    cout << \"searching for \" << files.path() << endl;\n```\n\n然后，您需要添加代码来执行搜索。这里使用的模式将是一个具有 out 参数并返回`bool`的方法。如果对方法的调用成功，那么找到的文件将在 out 参数中返回，方法将返回`true`。如果调用失败，则 out 参数保持不变，方法返回`false`。在`file_search`类的`public`部分，添加此功能:\n\n```cpp\n    bool next(string& ret) \n    { \n        _finddata_t find{}; \n        if (!handle) \n        { \n            handle = _findfirst(search.c_str(), &find); \n            if (!handle) return false; \n        } \n        else \n        { \n            if (-1 == _findnext(handle, &find)) return false; \n        } \n\n        ret = find.name; \n        return true; \n    }\n```\n\n如果这是对该方法的第一次调用，那么`handle`将无效，因此调用`_findfirst`。这将用搜索结果填充一个`_finddata_t`结构，并返回一个`intptr_t`值。将`search_handle`对象数据成员分配给该函数返回的值，如果`_findfirst`返回`-1`，则该方法返回`false`。如果调用成功，则使用`_finddata_t`结构中的 C 字符串指针初始化 out 参数(对`string`的引用)。\n\n如果有更多的文件与模式匹配，那么您可以重复调用`next`函数，在这些后续调用中，调用`_findnext`函数来获取下一个文件。在这种情况下，`search_handle`对象被传递给函数，并通过类的转换运算符隐式转换为`intptr_t`。如果`_findnext`功能返回`-1`，则表示搜索中不再有文件。\n\n在`main`功能的底部，添加以下行来执行搜索:\n\n```cpp\n    string file; \n    while (files.next(file)) \n    { \n        cout << file << endl; \n    }\n```\n\n现在，您可以编译代码并使用搜索标准运行它。请记住，这受到`_findfirst` / `_findnext`功能的限制，因此您可以进行的搜索将非常简单。尝试在命令行中运行该命令，并使用参数搜索`Beginning_C++ `文件夹中的子文件夹:\n\n```cpp\n search Beginning_C++ Ch*\n```\n\n这将给出以`Ch`开始的子文件夹列表。既然`search_handle`没有理由成为一个单独的类，就把整个类移到`search_handle`的`private`部分，在`handle`数据成员的声明上面。编译并运行代码。\n\n# 摘要\n\n通过类，C++ 提供了一种强大而灵活的机制来封装数据和方法，以提供作用于数据的行为。您可以将此代码模板化，以便编写泛型代码，并让编译器为您需要的类型生成代码。在示例中，您已经看到了类是如何成为面向对象的基础的。一个类封装了数据，因此调用者只需要知道预期的行为(在这个例子中，获取搜索的下一个结果)，而不需要知道该类如何做到这一点的细节。**"
  },
  {
    "path": "docs/mod-cpp/05.md",
    "content": "# 五、使用标准库容器\n\n标准库提供了几种类型的容器；每一个都是通过模板类提供的，因此容器的行为可以用于任何类型的项。有顺序容器的类，其中容器中项目的顺序取决于项目插入容器的顺序。还有排序和未排序的关联容器，它们将一个值与一个键相关联，随后使用该键访问该值。\n\n虽然不是容器本身，但在本章中我们还将涉及两个相关的类:`pair`将两个值链接到一个对象中，以及`tuple`，可以在一个对象中保存一个或多个值。\n\n# 使用对和元组\n\n在许多情况下，您会希望将两个项目关联在一起；例如，关联容器允许您创建一种数组类型，其中除了数字之外的项用作索引。`<utility>`头文件包含一个名为`pair`的模板类，它有两个名为`first`和`second`的数据成员。\n\n```cpp\n    template <typename T1, typename T2> \n    struct pair \n    { \n        T1 first; \n        T2 second; \n        // other members \n    };\n```\n\n由于类是模板化的，这意味着您可以关联任何项，包括指针或引用。访问成员很简单，因为它们是公开的。您也可以使用`get`模板化函数，因此对于一个`pair`对象`p`，您可以调用`get<0>(p)`而不是`p.first`。该类还有一个复制构造函数和一个移动构造函数，这样您就可以从另一个对象创建一个对象。还有一个名为`make_pair`的函数，可以从参数中推导出成员的类型:\n\n```cpp\n    auto name_age = make_pair(\"Richard\", 52);\n```\n\n要小心，因为编译器会使用它认为最合适的类型；在这种情况下，创建的`pair`对象将是`pair<const char*, int>`，但是如果您希望`first`项是一个`string`，那么使用构造函数更简单。你可以比较`pair`对象；对第一个成员执行比较，只有当它们相等时，才会比较第二个成员:\n\n```cpp\n    pair <int, int> a(1, 1); \n    pair <int, int> a(1, 2); \n    cout << boolalpha; \n    cout << a << \" < \" << b << \" \" << (a < b) << endl;\n```\n\n这些参数可以是参考:\n\n```cpp\n    int i1 = 0, i2 = 0; \n    pair<int&, int&> p(i1, i2); \n    ++ p.first; // changes i1\n```\n\n`make_pair`函数将从参数中推导出类型。编译器无法区分变量和对变量的引用。在 C++ 11 中，您可以使用`ref`功能(在`<functional>`中)指定`pair`作为参考:\n\n```cpp\n    auto p2 = make_pair(ref(i1), ref(i2)); \n    ++ p2.first; // changes i1\n```\n\n如果您想从一个函数中返回两个值，您可以通过引用传递的参数来实现，但是代码可读性较差，因为您希望返回值来自函数的返回而不是它的参数。`pair`类允许您在一个对象中返回两个值。一个例子是`<algorithm>`中的`minmax`功能。这将返回一个`pair`对象，该对象包含按最小优先顺序排列的参数，并且存在一个重载，如果不应该使用默认运算符`<`，您可以在该重载中提供一个谓词对象。以下将打印`{10,20}`:\n\n```cpp\n    auto p = minmax(20,10);  \n    cout << \"{\" << p.first << \",\" << p.second << \"}\" << endl;\n```\n\n`pair`类关联两个项目。标准库提供了具有类似功能的`tuple`类，但是由于模板是可变的，这意味着您可以拥有任何类型的任意数量的参数。但是，数据成员并不像在`pair`中那样命名，而是通过模板化的`get`函数来访问它们:\n\n```cpp\n    tuple<int, int, int> t3 { 1,2,3 }; \n    cout << \"{\" \n        << get<0>(t3) << \",\" << get<1>(t3) << \",\" << get<2>(t3)  \n        << \"}\" << endl; // {1,2,3}\n```\n\n第一行创建一个保存三个`int`项的`tuple`，并使用初始化列表(您可以使用构造函数语法)对其进行初始化。然后通过使用版本的`get`函数访问对象中的每个数据成员，将`tuple`打印到控制台上，其中模板参数指示项目的索引。请注意，索引是一个模板参数，因此您不能在运行时使用变量来提供它。如果这是你想做的，那么这是一个明确的指示，你需要使用一个容器，如`vector`。\n\n`get`函数返回一个引用，因此这可以用来改变项目的值。对于`tuple t3`，该代码将第一项更改为`42`，第二项更改为`99`:\n\n```cpp\n    int& tmp = get<0>(t3); \n    tmp = 42; \n    get<1>(t3) = 99;\n```\n\n您也可以通过使用`tie`功能，一次调用提取所有项目:\n\n```cpp\n    int i1, i2, i3; \n    tie(i1, i2, i3) = t3; \n    cout << i1 << \",\" << i2 << \",\" << i3 << endl;\n```\n\n`tie`函数返回一个`tuple`，其中每个参数都是一个引用，并初始化为作为参数传递的变量。如果您这样写，前面的代码更容易理解:\n\n```cpp\n    tuple<int&, int&, int&> tr3 = tie(i1, i2, i3); \n    tr3 = t3;\n```\n\n可以从`pair`对象创建`tuple`对象，因此您也可以使用`tie`功能从`pair`对象提取值。\n\n有一个叫做`make_tuple`的辅助函数，它会推导出参数的类型。和`make_pair`函数一样，你必须小心扣除，所以一个浮点数会被推导为`double`，一个整数会被推导为`int`。如果您希望参数是对特定变量的引用，您可以使用`ref`函数或`cref`函数作为`const`引用。\n\n只要项目数量相等，类型相同，就可以比较`tuple`个对象。编译器将拒绝编译具有不同项目数的`tuple`对象的比较，或者如果一个`tuple`对象的项目类型不能转换为另一个`tuple`对象的类型。\n\n# 容器\n\n标准库容器允许您将零个或多个相同类型的项目组合在一起，并通过迭代器串行访问它们。每个这样的对象都有一个`begin`方法，该方法返回第一个项目的迭代器对象，以及一个`end`函数，该函数返回容器中最后一个项目之后的项目的迭代器对象。迭代器对象支持类似指针的运算，因此`end() - begin()`会给出容器中的项目数。所有容器类型都将执行`empty`方法来指示容器中是否没有物品，并且(除了`forward_list`)方法是容器中的物品数量。您很容易像遍历数组一样遍历容器:\n\n```cpp\n    vector<int> primes{1, 3, 5, 7, 11, 13}; \n    for (size_t idx = 0; idx < primes.size(); ++ idx)  \n    { \n        cout << primes[idx] << \" \"; \n    } \n    cout << endl;\n```\n\n问题是，不是所有的容器都允许随机访问，如果您决定使用另一个容器更有效，您将不得不更改容器的访问方式。如果您想使用模板编写通用代码，此代码也不能很好地工作。前面的代码最好使用迭代器编写:\n\n```cpp\n    template<typename container> void print(container& items) \n    { \n        for (container::iterator it = items.begin();  \n        it != items.end(); ++ it) \n        { \n            cout << *it << \" \"; \n        } \n        cout << endl; \n    }\n```\n\n所有容器都有一个名为`iterator`的`typedef`成员，它给出了从`begin`方法返回的迭代器的类型。迭代器对象的行为类似于指针，因此您可以使用取消引用操作符获取迭代器引用的项，并使用增量操作符移动到下一项。\n\n对于除`vector`之外的所有容器，即使删除了其他元素，也可以保证迭代器保持有效。如果您插入项目，那么只有`lists`、`forward_lists`和关联的容器保证迭代器保持有效。迭代器将在后面更深入地讨论。\n\n所有容器都必须有一个名为`swap`的异常安全(nothrow)方法，并且(除了两个异常)它们必须有*事务*语义；也就是说，一个操作必须成功或失败。如果操作失败，容器将处于调用操作之前的状态。对于每个容器，当涉及到多元素插入时，这个规则是宽松的。例如，如果您使用迭代器区域一次插入许多项，并且该区域中的一项插入失败，则该方法将无法撤消以前的插入。\n\n需要指出的是，对象被复制到容器中，所以放入容器中的对象类型必须有一个复制和复制赋值操作符。此外，请注意，如果您将派生类对象放入需要基类对象的容器中，那么复制将对该对象进行切片，这意味着与派生类相关的任何内容都将被移除(数据成员和虚拟方法指针)。\n\n# 序列容器\n\n序列容器存储一系列项目及其存储顺序，当您使用迭代器访问它们时，这些项目将按照它们放入容器的顺序进行检索。创建容器后，可以使用库函数更改排序顺序。\n\n# 目录\n\n顾名思义，一个`list`对象是通过一个双向链表来实现的，其中每一项都有一个到下一项和前一项的链接。这意味着可以快速插入项目(如[第 2 章](02.html)、*使用内存、数组和指针*中的示例，用单链表显示)，但是由于在链表中，一个项目只能访问它前面和后面的项目，所以不能使用`[]` indexoperator 进行随机访问。\n\n类允许您通过构造函数提供值，或者您可以使用成员方法。例如，`assign`方法允许您使用初始化列表在一个动作中填充容器，或者使用迭代器填充另一个容器中的范围。您也可以使用`push_back`或`push_front`方法插入单个项目:\n\n```cpp\n    list<int> primes{ 3,5,7 }; \n    primes.push_back(11); \n    primes.push_back(13); \n    primes.push_front(2); \n    primes.push_front(1);\n```\n\n第一行创建一个包含`3`、`5`和`7`的`list`对象，然后将`11`和`13`推到最后(按此顺序)，这样`list`包含`{3,5,7,11,13}`。然后代码将数字`2`和`1`推到前面，这样最后的`list`就是`{1,2,3,5,7,11,13}`。不管名字如何，`pop_front`和`pop_back`方法只是删除列表前面或后面的项目，而不会返回项目。如果您想获得已移除的项目，您必须先通过`front`或`back`方法*访问该项目:*\n\n```cpp\n    int last = primes.back(); // get the last item \n    primes.pop_back();        // remove it\n```\n\n`clear`方法将删除`list`中的所有项目，`erase`方法将删除项目。有两个版本:一个有一个标识单个项目的迭代器，另一个有两个指示一个范围的迭代器。通过提供范围中的第一个项目和范围后的项目来指示范围。\n\n```cpp\n    auto start = primes.begin(); // 1 \n    start++ ;                     // 2 \n    auto last = start;           // 2 \n    last++ ;                      // 3 \n    last++ ;                      // 5 \n    primes.erase(start, last);   // remove 2 and 3\n```\n\n这是迭代器和标准库容器的一般原则；迭代器通过第一个项目和最后一个项目之后的项目*来指示一个范围。`remove`方法将删除指定值的所有项目:*\n\n```cpp\n    list<int> planck{ 6,6,2,6,0,7,0,0,4,0 }; \n    planck.remove(6);            // {2,0,7,0,0,4,0}\n```\n\n还有一种方法`remove_if`接受一个谓词，只有当谓词返回`true`时才会移除一个项目。类似地，您可以使用迭代器将项目插入列表，并且该项目被插入到指定项目之前:\n\n```cpp\n    list<int> planck{ 6,6,2,6,0,7,0,0,4,0 }; \n    auto it = planck.begin(); \n    ++ it; \n    ++ it; \n    planck.insert(it, -1); // {6,6,-1,2,6,0,7,0,0,4,0}\n```\n\n您还可以指出该项目应该在该位置插入多次(如果插入，插入多少份)，并且您可以提供几个要在一个点插入的项目。当然，如果你传递的迭代器是通过调用`begin`方法获得的，那么这个项就被插入到`list`的开头。调用`push_front`方法也可以达到同样的效果。类似地，如果迭代器是通过调用`end`方法获得的，那么该项插入到`list`的末尾，这与调用`push_back`相同。\n\n当您调用`insert`方法时，您提供了一个对象，该对象要么被复制到`list`中，要么被移动到`list`中(通过右值语义)。该类还提供了几种**定位**的方法(`emplace`、`emplace_front`和`emplace_back`)，这些方法将根据您提供的数据构造一个新的对象，并将该对象插入到`list`中。例如，如果您有一个可以从两个`double`值创建的`point`类，则可以通过提供两个`double`值来创建一个构造的`point`对象或一个`emplace`对象:\n\n```cpp\n    struct point \n    { \n        double x = 0, y = 0; \n        point(double _x, double _y) : x(_x), y(_y) {} \n    }; \n\n    list<point> points; \n    point p(1.0, 1.0); \n    points.push_back(p); \n    points.emplace_back(2.0, 2.0);\n```\n\n一旦你创建了一个`list`，你就可以用成员函数来操纵它。`swap`方法以合适的`list`对象为参数，将参数中的项目移动到当前对象中，将当前`list`中的项目移动到参数中。由于`list`对象是使用链表实现的，所以这个操作很快。\n\n```cpp\n    list<int> num1 { 2,7,1,8,2,8 }; // digits of Euler's number \n    list<int> num2 { 3,1,4,5,6,8 }; // digits of pi \n    num1.swap(num2);\n```\n\n之后，代码`num1`将包含`{3,1,4,5,6,8}`，`num2`将包含`{2,7,1,8,2,8}`，如下图所示:\n\n![](img/18cb70c4-6f3d-413a-8eb2-b45b67438dc2.png)\n\nA `list`将物品按照插入容器的顺序放置；但是，您可以通过调用`sort`方法对它们进行排序，默认情况下，该方法将使用`<`运算符对`list`容器中的项目进行升序排序。您也可以传递函数对象进行比较操作。排序后，可以通过调用`reverse`方法颠倒项目的顺序。可以合并两个排序列表，包括从参数列表中取出项目，并将其插入调用列表，顺序如下:\n\n```cpp\n    list<int> num1 { 2,7,1,8,2,8 }; // digits of Euler's number \n    list<int> num2 { 3,1,4,5,6,8 }; // digits of pi \n    num1.sort();                    // {1,2,2,7,8,8} \n    num2.sort();                    // {1,3,4,5,6,8} \n    num1.merge(num2);               // {1,1,2,2,3,4,5,6,7,8,8,8}\n```\n\n合并两个列表可能会导致重复，可以通过调用`unique`方法来删除这些重复:\n\n```cpp\n    num1.unique(); // {1,2,3,4,5,6,7,8}\n```\n\n# 转发列表\n\n顾名思义，`forward_list`类类似于`list`类，但它只允许在列表的前面插入和移除项目。这也意味着类使用的迭代器只能递增；编译器将拒绝允许您递减这样的迭代器。该类有`list`方法的子集，所以有`push_front`、`pop_front`和`emplace_front`方法，但没有对应的`_back`方法。它还实现了其他一些方法，并且由于列表项只能向前访问，这意味着插入将发生在现有项之后，因此类实现了`insert_after`和`emplace_after`。\n\n同样，您可以移除列表开头(`pop_front`)或指定项目之后(`erase_after`)的项目，或者告诉类在列表中向前迭代并移除具有特定值的项目(`remove`和`remove_if`):\n\n```cpp\n    forward_list<int> euler { 2,7,1,8,2,8 }; \n    euler.push_front(-1);       // { -1,2,7,1,8,2,8 } \n    auto it = euler.begin();    // iterator points to -1 \n    euler.insert_after(it, -2); // { -1,-2,2,7,1,8,2,8 } \n    euler.pop_front();          // { -2,2,7,1,8,2,8 } \n    euler.remove_if([](int i){return i < 0;}); \n                                // { 2,7,1,8,2,8 }\n```\n\n在前面的代码中，`euler`用欧拉数的数字初始化，`-1`的值被推到前面。接下来，获得指向容器中第一个值的迭代器；也就是到了`-1`的价值位置。在迭代器的位置后插入`-2`值；也就是说，`-2`插入在`-1`的值之后。最后两行显示了如何删除项目；`pop_front`移除容器前面的项目，`remove_if`将移除满足谓词的项目(在这种情况下，当项目小于零时)。\n\n# 矢量\n\n`vector`类具有动态数组的行为；也就是说，存在对项目的索引随机访问，并且容器将随着更多的项目被插入其中而增长。您可以创建一个带有初始化列表的`vector`对象，以及一个项目的指定数量的副本。您也可以通过传递指示另一个容器中项目范围的迭代器，将`vector`基于该容器中的值。您可以通过提供容量作为构造函数参数来创建一个具有预定大小的向量，并且将在容器中创建指定数量的默认项。如果在稍后阶段，您需要指定容器大小，您可以调用`reserve`方法来指定最小大小或`resize`方法，这可能意味着根据现有的`vector`对象是大于还是小于请求的大小来删除多余的项目或创建新的项目。\n\n当您将项目插入`vector`容器并且没有分配足够的内存时，容器将分配足够的内存。这将涉及分配新内存、将现有项目复制到新内存、创建新项目，最后销毁项目的旧副本并释放旧内存。显然，如果您知道项目的数量，并且知道如果没有新的分配，`vector`容器将无法容纳它们，您应该通过调用`reserve`方法来指示您需要多少空间。\n\n插入构造函数之外的项很简单。你可以使用`push_back`在末尾插入一个项目(这是一个快速动作，假设不需要分配)，还有`pop_back`移除最后一个项目。您也可以使用`assign`方法清除整个容器并插入指定的项目(或者是同一项目的倍数、项目的初始化列表，或者是用迭代器指定的另一个容器中的项目)。与`list`对象一样，您可以清除整个`vector`，擦除某个位置的项目，或在指定位置插入项目。但是，没有等同于`remove`的方法来移除具有特定值的项目。\n\n使用`vector`类的主要原因是使用`at`方法或`[]`索引运算符进行随机访问:\n\n```cpp\n   vector<int> distrib(10); // ten intervals \n   for (int count = 0; count < 1000; ++ count) \n   { \n      int val = rand() % 10; \n      ++ distrib[val]; \n   } \n   for (int i : distrib) cout << i << endl;\n```\n\n第一行创建一个有十个项目的`vector`，然后在循环中每次调用 C 运行时函数`rand`一千次，得到一个 0 到 32767 之间的伪随机数。模数运算符用于近似地得到一个介于 0 和 9 之间的随机数。该随机数随后被用作`distrib`对象的索引，以选择指定的项目，该项目随后被递增。最后，这个分布被打印出来，正如你所期望的，这给出了每个项目大约 100 的值。\n\n该代码依赖于`[]`运算符返回对该项的引用，这就是该项可以以这种方式递增的原因。`[]`操作符可用于对容器中的项目进行读写。容器通过`begin`和`end`方法以及(因为它们是容器适配器所需要的)`front`和`back`方法提供迭代器访问。\n\n一个`vector`对象可以保存任何有复制构造函数和赋值运算符的类型，这意味着所有的内置类型。照目前的情况来看，`bool`项的`vector`会浪费内存，因为一个布尔值可以存储为一个位，编译器会将`bool`视为一个整数(32 位)。标准图书馆有一个专为更有效存储物品的`bool`而设的`vector`类。然而，虽然这个类乍一看是一个好主意，但问题是，由于容器将布尔值保存为位，这意味着`[]`运算符不会返回对`bool`的引用(相反，它会返回一个行为类似于 T7 的对象)。\n\n如果你想保存布尔值，然后操纵它们，只要你在编译时知道有多少项，那么`bitset`类可能是一个更好的选择。\n\n# 双端队列\n\n`deque`这个名字的意思是*双头队列*，意思是可以从两头长，虽然中间可以插物品，但是比较贵。作为一个队列，它意味着项目是有序的，但是，因为项目可以从任何一端放入队列，所以顺序不一定与您将项目放入容器的顺序相同。\n\n`deque`的界面类似于一个`vector`，所以你可以使用`at`函数和`[]`操作符进行迭代器访问和随机访问。与`vector`一样，您可以使用`push_back`、`pop_back`和`back`方法从`deque`容器的末端获取物品，但是与`vector`不同，您也可以使用`push_front`、`pop_front`和`front`方法获取`deque`容器的前部。虽然`deque`类有方法允许您在容器中插入和删除项目，但是对于`resize`来说，这些都是昂贵的操作，如果您需要使用它们，那么您应该重新考虑使用这种容器类型。此外，`deque`类没有预分配内存的方法，因此，当您向该容器添加一个项目时，它可能会导致内存分配。\n\n# 关联容器\n\n使用类似 C 的`array`或`vector`，每个项目都与其数字索引相关联。早先在`vector`一节的一个例子中利用了这一点，在这个例子中，指数提供了分布的十分位数，方便的是，分布被分成十个十分位数的数据。\n\n关联容器允许您提供非数字的索引；这些是键，您可以将值与它们相关联。当您将键值对插入到容器中时，它们将被排序，以便容器随后可以通过其键有效地访问值。通常，这个顺序对您来说并不重要，因为您不会使用容器来顺序访问项目，而是通过它们的键来访问值。典型的实现将使用二叉树或哈希表，这意味着根据项的键查找项是一个快速的操作。\n\n对于有序容器，如`map`，将使用`<`(较少的谓词)对容器中的键和现有键进行比较。默认谓词意味着比较键，如果这是一个智能指针，那么将被比较并用于排序的将是智能指针对象，而不是它们包装的对象。在这种情况下，您将希望编写自己的谓词来执行适当的比较，并将其作为模板参数传递。\n\n这意味着插入或删除项目通常很昂贵，并且密钥被视为不可变的，因此您不能为项目更改它。对于所有关联容器，没有删除方法，但有擦除方法。但是，对于那些保持项目排序的容器，擦除项目可能会影响性能。\n\n有几种类型的关联容器，主要区别在于它们如何处理重复的键以及出现的排序级别。`map`类有按唯一键排序的键值对，所以不允许重复键。如果要允许重复按键，那么可以使用`multimap`类。`set`类本质上是一个键与值相同的映射，同样不允许重复。`multiset`类不允许重复。\n\n有一个键与值相同的关联类可能看起来很奇怪，但将该类包含在本节中的原因是，像`map`类一样，`set`类有一个类似的接口来查找值。同样类似于`map`班，`set`班能快速找到物品。\n\n# 地图和多重地图\n\n一个`map`容器存储两个不同的项目，一个键和值，它根据键以排序顺序维护项目。排序的`map`表示快速定位一个项目。该类具有与其他容器相同的接口来添加项目:您可以通过构造函数将它们放入容器中，或者您可以使用成员方法`insert`和`emplace`。您还可以通过迭代器访问项目。当然，迭代器提供对单个值的访问，因此通过映射，这将是对同时具有键和值的`pair`对象的访问:\n\n```cpp\n    map<string, int> people; \n    people.emplace(\"Washington\", 1789); \n    people.emplace(\"Adams\", 1797); \n    people.emplace(\"Jefferson\", 1801); \n    people.emplace(\"Madison\", 1809); \n    people.emplace(\"Monroe\", 1817); \n\n    auto it = people.begin(); \n    pair<string, int> first_item = *it; \n    cout << first_item.first << \" \" << first_item.second << endl;\n```\n\n对`emplace`的调用将项目放入`map`中，其中键是`string`(总统的名字)，值是`int`(总统开始任期的年份)。然后，代码获得容器中第一项的迭代器，通过对迭代器解引用来访问该项，从而给出一个`pair`对象。由于项目以排序顺序存储在`map`中，第一个项目将被设置为`\"Adams\"`。您也可以将项目作为`pair`对象插入，或者作为对象插入，或者使用`insert`方法通过迭代器插入到另一个容器中的`pair`对象。\n\n大多数`emplace`和`insert`方法将返回以下形式的`pair`对象，其中`iterator`类型与`map`相关:\n\n```cpp\n    pair<iterator, bool>\n```\n\n你用这个对象来测试两件事。首先，`bool`表示插入是否成功(如果容器中已经有一个具有相同密钥的项目，则插入会失败)。其次，`pair`的`iterator`部分要么指示新项目的位置，要么指示不会被替换的现有项目的位置(将导致插入失败)。\n\n*故障*取决于*等效*而不是*等效*。如果有一个项的键与您试图插入的项相同，则插入将失败。等价的定义取决于与`map`对象一起使用的比较器谓词。因此，如果`map`使用谓词`comp`，那么两个项目`a`和`b`之间的等价性通过测试`!comp(a,b) && !comp(b,a)`来确定。这和`(a==b)`的测试不一样。\n\n假设前面的`map`对象，可以这样做:\n\n```cpp\n    auto result = people.emplace(\"Adams\", 1825); \n    if (!result.second) \n       cout << (*result.first).first << \" already in map\" << endl;\n```\n\n测试`result`变量中的第二项，看插入是否成功，如果不成功，那么第一项是对现有项`pair<string,int>`的迭代器，代码解引用迭代器得到`pair`对象，然后打印出第一项，这是关键字(在本例中是人名)。\n\n如果你知道物品应该放在`map`的什么位置，那么你可以打电话给`emplace_hint`:\n\n```cpp\n    auto result = people.emplace(\"Monroe\", 1817); \n    people.emplace_hint(result.first, \"Polk\", 1845);\n```\n\n这里我们知道`Polk`在`Monroe`之后，所以我们可以将迭代器传递给`Monroe`作为提示。该类通过迭代器提供对项目的访问，因此您可以使用 ranged `for`(基于迭代器访问):\n\n```cpp\n    for (pair<string, int> p : people) \n    { \n        cout << p.first << \" \" << p.second << endl; \n    }\n```\n\n此外，还可以使用`at`方法和`[]`操作符访问单个项目。在这两种情况下，类都将使用提供的键搜索项，如果找到该项，将返回该项值的引用。在没有带指定键的项目的情况下，`at`方法和`[]`操作符的行为不同。\n\n如果键不存在，`at`方法会抛出异常；如果`[]`运算符找不到指定的键，它将使用该键并调用值类型的默认构造函数来创建一个新项。如果键存在，`[]`运算符将返回对该值的引用，因此您可以编写如下代码:\n\n```cpp\n    people[\"Adams\"] = 1825; \n    people[\"Jackson\"] = 1829;\n```\n\n第二行的行为如您所料:不会有键为`Jackson`的项，因此`map`将使用该键创建一个项，通过调用值类型的默认构造函数来初始化它(`int`，因此该值被初始化为零)，然后它返回对该值的引用，该值被赋值为`1829`。然而，第一行将查找`Adams`，看到有一个项目，并返回对其值的引用，然后为其赋值`1825`。没有迹象表明与插入新项目相反，项目的值已经改变。在某些情况下，您可能需要这种行为，但这不是这段代码的意图，显然，这段代码需要一个允许重复键的关联容器(如`multimap`)。此外，在这两种情况下，都会搜索关键字，返回引用，然后执行赋值。请注意，虽然以这种方式插入项目是有效的，但是在容器中放置新的键值对更有效，因为您没有这种额外的分配。\n\n填写完`map`后，您可以使用以下内容搜索一个值:\n\n*   `at`方法，它被传递一个键并返回对该键的值的引用\n*   `[]`运算符，当传递一个键时，返回该键的值的引用\n*   `find`函数，它将使用模板中指定的谓词(不像后面提到的全局`find`函数)，它将为您提供一个作为`pair`对象的整个项目的迭代器\n*   `begin`方法会给你第一项的迭代器，`end`方法会给你最后一项后的迭代器\n**   `lower_bound`方法返回一个迭代器，该迭代器的键*等于或大于作为参数传递的键* *的键**   `upper_bound`方法返回地图中第一项的迭代器，该迭代器的键*大于所提供的键**   `equal_range`方法返回一个`pair`对象的上下限值*\n\n *# 集合和多集合\n\n集合的行为就像它们是映射，但键与值相同；例如，以下内容:\n\n```cpp\n    set<string> people{ \n       \"Washington\",\"Adams\", \"Jefferson\",\"Madison\",\"Monroe\",  \n       \"Adams\", \"Van Buren\",\"Harrison\",\"Tyler\",\"Polk\"}; \n    for (string s : people) cout << s << endl;\n```\n\n这将按字母顺序打印出*九个*人，因为有两个项目叫做`Adams`，并且`set`类将拒绝重复。当项目被插入到集合中时，它将被排序，在这种情况下，顺序由比较两个`string`对象的词典排序来确定。如果要允许重复，让容器里放十个人，那就用`multiset`代替。\n\n与`map`一样，您不能更改容器中某个项目的键，因为该键用于确定订单。对于一个`set`来说，键和值是一样的，所以这意味着你根本不能改变项目。如果目的是执行查找，那么最好使用排序的`vector`来代替。一个`set`将比一个`vector`有更多的内存分配开销。如果搜索是连续的，在`set`容器上的查找可能会比在`vector`容器上更快，但是如果您使用对`binary_search`的调用(稍后在*排序项目*一节中解释)，它可能会比关联容器更快。\n\n`set`类的接口是`map`类的受限版本，因此您可以在容器中`insert`和`emplace`项，将其分配给另一个容器中的值，并且您可以访问迭代器(`begin`和`end`方法)。\n\n由于没有明确的键，这意味着`find`方法寻找的是一个值，而不是一个键(边界方法也是如此；例如，`equal_range`)。没有`at`法，也没有`[]`符。\n\n# 无序容器\n\n`map`和`set`类允许您快速找到对象，这是通过这些类以排序顺序保存项目来实现的。如果你遍历这些条目(从`begin`到`end`，那么你会得到那些按照排序顺序排列的条目。如果你想选择键值范围内的对象，你可以调用`lower_bound`和`upper_bound`方法，让迭代器到达合适的键值范围。\n\n这是这些关联容器的两个重要特性:查找和排序。在某些情况下，值的实际顺序并不重要，您需要的行为是高效的查找。在这种情况下，您可以使用`map`和`set`类的`unordered_`版本。因为顺序不重要，所以这些是使用哈希表实现的。\n\n# 特殊用途容器\n\n到目前为止描述的容器是灵活的，可以用于各种目的。标准库提供了具有特定目的的类，但是，因为它们是通过包装其他类来实现的，所以它们被称为**容器适配器**。例如，一个`deque`对象可以用作**先进先出** ( **FIFO** )队列，方法是将对象推到`deque`的后面(用`push_back`)，然后使用`front`方法从队列的前面访问对象(用`pop_front`移除它们)。标准库实现了一个名为`queue`的容器适配器，它具有这种先进先出行为，并且基于`deque`类。\n\n```cpp\n    queue<int> primes; \n    primes.push(1); \n    primes.push(2); \n    primes.push(3); \n    primes.push(5); \n    primes.push(7); \n    primes.push(11); \n    while (primes.size() > 0) \n    { \n        cout << primes.front() << \",\"; \n        primes.pop(); \n    } \n    cout << endl; // prints 1,2,3,5,7,11\n```\n\n您将`push`项放入队列并用`pop`移除它们，然后使用`front`方法访问下一项。可以被这个适配器包装的标准库容器必须实现`push_back`、`pop_front`和`front`方法。也就是说，项目从一端放入容器，从另一端访问(和移除)。\n\n一个**后进先出** ( **后进先出**)的容器将放入物品，并从同一端存取(和取出)物品。同样，通过使用`push_back`推送项目，使用`front`访问项目，并使用`pop_back`方法移除项目，可以使用`deque`对象来实现此行为。标准库提供了一个名为`stack`的适配器类来提供这种行为。这有一个名为`push`的方法将项目推入容器，一个名为`pop`的方法移除项目，但是奇怪的是，您使用`top`方法访问下一个项目，即使它是使用包装容器的`back`方法实现的。\n\n适配器类`priority_queue`，不管名字是什么，都像`stack`容器一样使用；也就是说，使用`top`方法访问项目。容器确保当一个项目被推入时，队列的顶部总是具有最高优先级的项目。谓词(默认为`<`)用于对队列中的项目进行排序。例如，我们可以有一个聚合类型，它具有任务的名称以及与其他任务相比您必须完成任务的优先级:\n\n```cpp\n    struct task \n    { \n    string name; \n    int priority; \n    task(const string& n, int p) : name(n), priority(p) {} \n    bool operator <(const task& rhs) const { \n        return this->priority < rhs.priority; \n        } \n    };\n```\n\n聚合类型很简单；它有两个由构造函数初始化的数据成员。为了能够对任务进行排序，我们需要能够比较两个任务对象。一个选项(前面已经给出)是定义一个单独的谓词类。在这个例子中，我们使用默认谓词，文档中说它将是`less<task>`，这将基于`<`运算符来比较项目。为了使用默认谓词，我们为`task`类定义了`<`运算符。现在我们可以向`priority_queue`容器添加任务:\n\n```cpp\n    priority_queue<task> to_do; \n    to_do.push(task(\"tidy desk\", 1)); \n    to_do.push(task(\"check in code\", 10)); \n    to_do.push(task(\"write spec\", 8)); \n    to_do.push(task(\"strategy meeting\", 8)); \n\n    while (to_do.size() > 0) \n    { \n        cout << to_do.top().name << \" \" << to_do.top().priority << endl; \n        to_do.pop(); \n    }\n```\n\n该代码的结果是:\n\n```cpp\n    check in code 10\nwrite spec 8\nstrategy meeting 8\ntidy desk 1\n```\n\n队列已经根据`priority`数据项对任务进行了排序，`top`和`pop`方法调用的组合按优先级顺序读取这些项，并将其从队列中移除。具有相同优先级的项目按照推入的顺序放入队列。\n\n# 使用迭代器\n\n到目前为止，在本章中，我们已经指出容器通过迭代器来访问项目。这意味着迭代器只是指针，这是故意的，因为迭代器的行为*就像*指针。然而，它们通常是迭代器类的对象(参见`<iterator>`头)。所有迭代器都有以下行为:\n\n| **操作员** | **行为** |\n| * | 允许访问当前位置的元素 |\n| ++ | 向前移动到下一个元素(通常使用前缀运算符)(这仅在迭代器允许向前移动的情况下) |\n| - | 向后移动到前一个元素(通常使用前缀运算符)(这仅在迭代器允许向后移动的情况下) |\n| `==`和`!=` | 比较两个迭代器是否在同一位置 |\n| = | 分配一个迭代器 |\n\n与 C++ 指针假设数据在内存中是连续的不同，迭代器可以用于更复杂的数据结构，例如链表，其中的项可能不是连续的。操作符`++ `和`--`按预期工作，不考虑底层存储机制。\n\n`<iterator>`头声明将迭代器递增的`next`全局函数和将迭代器改变指定数量位置的`advance`函数(向前或向后，取决于参数是否为负以及迭代器允许的方向)。还有一个`prev`函数可以将迭代器减少一个或多个位置。`distance`函数可以用来确定两个迭代器之间有多少项。\n\n所有容器都有一个`begin`方法，返回第一项的迭代器，还有一个`end`方法，在最后一项之后返回迭代器*。这意味着您可以通过调用`begin`迭代容器中的所有项目，然后递增迭代器，直到它具有从`end`返回的值。迭代器上的`*`操作符提供了对容器中元素的访问，如果迭代器是读写的(如果从 begin 方法返回的话)，这意味着该项可以被更改。*\n\n容器也有`cbegin`和`cend`方法，它们将返回一个常量迭代器，该迭代器只提供对元素的只读访问:\n\n```cpp\n    vector<int> primes { 1,2,3,5,7,11,13 }; \n    const auto it = primes.begin(); // const has no effect \n    *it = 42; \n    auto cit = primes.cbegin(); \n    *cit = 1;                       // will not compile\n```\n\n这里`const`没有作用，因为变量是`auto`，类型是从初始化变量的项中推导出来的。`cbegin`方法被定义为返回一个`const`迭代器，所以你不能改变它所引用的项目。\n\n`begin`和`cbegin`方法返回**向前迭代器**，以便`++ `操作符向前移动迭代器。容器也可以支持**反向迭代器**，其中`rbegin`是容器中的最后一项(即之前的项*`end`返回的位置)`rend`是第一项*之前的位置*。(还有`crbegin`和`crend`，返回`const`迭代器。)重要的是要认识到反向迭代器的`++ `操作符向后移动*，如下例所示:**\n\n```cpp\n    vector<int> primes { 1,2,3,5,7,11,13 }; \n    auto it = primes.rbegin(); \n    while (it != primes.rend()) \n    { \n        cout << *it++ << \" \"; \n    } \n    cout << endl; // prints 13,11,7,5,4,3,2,1\n```\n\n`++ `运算符根据迭代器的类型递增迭代器。需要注意的是，这里使用`!=`运算符来确定循环是否应该结束，因为`!=`运算符将在所有迭代器上定义。\n\n这里的迭代器类型通过使用`auto`关键字被忽略。事实上，所有容器对于它们使用的所有迭代器类型都有`typedef`，所以在前面的例子中，我们可以使用以下内容:\n\n```cpp\n    vector<int> primes { 1,2,3,5,7,11,13 }; \n    vector<int>::iterator it = primes.begin();\n```\n\n允许正向迭代的容器将具有用于`iterator`和`const_iterator`的`typedef`，而允许反向迭代的容器将具有用于`reverse_iterator`和`const_reverse_iterator`的`typedef`。\n\n为了完整起见，容器还将具有用于返回指向元素的指针的方法的`pointer`和`const_pointer`，以及用于返回对元素的引用的方法的`reference`和`const_reference`。这些类型定义使您能够在不知道容器中的类型的情况下编写泛型代码，但是代码仍然能够声明正确类型的变量。\n\n尽管它们看起来像指针，迭代器通常由类实现。这些类型可能只允许一个方向的迭代:前向迭代器将只有`++ `运算符，反向迭代器将有`-`运算符，或者该类型可能允许两个方向的迭代(双向迭代器)，因此它们实现了`++ `和`--`运算符。例如，`list`、`set`、`multiset`、`map`和`multimap`类上的迭代器是双向的。`vector`、`deque`、`array`、`string`类都有允许随机访问的迭代器，所以这些迭代器类型有和双向迭代器一样的行为，但是也有像算术一样的指针，所以一次可以被多个项位置改变。\n\n# 输入和输出迭代器\n\n顾名思义，输入迭代器只会向前移动并具有读访问权限，输出迭代器只会向前移动但具有写访问权限。这些迭代器没有随机访问，也不允许向后移动。例如，输出流可以与输出迭代器一起使用:您将数据项分配给取消引用的迭代器，以便将该数据项写入流。类似地，输入流可以有一个输入迭代器，您可以取消对迭代器的引用来访问流中的下一项。这种行为意味着对于输出迭代器，取消引用操作符(`*`)的唯一有效用法是在赋值的左侧。用`!=`检查迭代器的值是没有意义的，你不能检查通过输出迭代器赋值是否成功。\n\n例如，`transform`函数需要三个迭代器和一个函数。前两个迭代器是输入迭代器，指示函数要转换的项目范围。结果将放在一个项目范围内(与输入迭代器的范围大小相同)，第一个项目由第三个迭代器指示，第三个迭代器是输出迭代器。一种方法如下:\n\n```cpp\n    vector<int> data { 1,2,3,4,5 }; \n    vector<int> results; \n    results.resize(data.size()); \n    transform( \n       data.begin(), data.end(),  \n       results.begin(), \n       [](int x){ return x*x; } );\n```\n\n这里`begin`和`end`方法返回`data`容器上的迭代器，这些迭代器可以安全地用作输入迭代器。`results`容器上的`begin`方法只能用作输出迭代器，只要该容器有足够的已分配项，而本代码中就是这种情况，因为它们是用`resize`分配的。然后，该函数将通过将每个输入项传递给最后一个参数中给出的 lambda 函数来转换每个输入项(该函数只返回值的平方)。重要的是重新评估这里正在发生的事情；`transform`函数的第三个参数是一个输出迭代器，这意味着你应该期望函数通过这个迭代器来写值。\n\n这段代码可以工作，但是它需要额外的步骤来分配空间，并且容器中有默认对象的额外分配，以便您可以覆盖它们。同样重要的是要提到，输出迭代器不必指向另一个容器。它可以是同一个容器，只要它引用了可以写入的范围:\n\n```cpp\n    vector<int> vec{ 1,2,3,4,5 }; \n    vec.resize(vec.size() * 2); \n    transform(vec.begin(), vec.begin() + 5, \n       vec.begin() + 5, [](int i) { return i*i; });\n```\n\n调整`vec`容器的大小，以便为结果留出空间。要转换的值的范围是从第一项到第五项(`vec.begin() + 5`是下一项)，写入转换值的位置是第六项到第十项。如果你打印出矢量，你会得到`{1,2,3,4,5,1,4,9,16,25}`。\n\n另一种类型的输出迭代器是插入器。`back_inserter`用在带`push_back`的容器上，`front_inserter`用在带`push_front`的容器上。顾名思义，插入器在容器上调用`insert`方法。比如你可以这样用一个`back_inserter`:\n\n```cpp\n    vector<int> data { 1,2,3,4,5 }; \n    vector<int> results; \n    transform( \n       data.begin(), data.end(),  \n       back_inserter(results), \n       [](int x){ return x*x; } ); // 1,4,9,16,25\n```\n\n转换的结果被插入到`results`容器中，其中包含从`back_inserter`类创建的临时对象。使用一个`back_inserter`对象确保当`transform`函数通过迭代器写入时，该项目被*插入到使用`push_back`包装的容器中。请注意，结果容器应该不同于源容器。*\n\n如果你想要相反顺序的值，那么如果容器支持`push_front`(例如`deque`)，那么你可以使用`front_inserter`。`vector`类没有`push_front`方法，但是它有反向迭代器，所以你可以用它们来代替:\n\n```cpp\n    vector<int> data { 1,2,3,4,5 }; \n    vector<int> results; \n    transform( \n data.rbegin(), data.rend(), \n       back_inserter(results), \n       [](int x){ return x*x; } ); // 25,16,9,4,1\n```\n\n你只需要把`begin`改成`rbegin`、`end`改成`rend`就可以颠倒结果的顺序。\n\n# 流迭代器\n\n这些是`<iterators>`中的适配器类，可用于从输入流中读取项目或将项目写入输出流。例如，到目前为止，我们已经使用迭代器通过范围`for`循环打印出容器的内容:\n\n```cpp\n    vector<int> data { 1,2,3,4,5 }; \n    for (int i : data) cout << i << \" \"; \n    cout << endl;\n```\n\n相反，您可以基于`cout`创建一个输出流迭代器，这样`int`值将使用流操作符`<<`通过这个迭代器写入`cout`流。要打印出一个包含`int`值的容器，只需将容器复制到输出迭代器中:\n\n```cpp\n    vector<int> data { 1,2,3,4,5 }; \n    ostream_iterator<int> my_out(cout, \" \"); \n    copy(data.cbegin(), data.cend(), my_out); \n    cout << endl;\n```\n\n`ostream_iterator`类的第一个参数是它将适配的输出流，可选的第二个参数是每个项目之间使用的分隔符字符串。`copy`函数(在`<algorithm>`中)将把输入迭代器指示的范围内的项目复制到输出迭代器，作为前两个参数传递，作为最后一个参数传递。\n\n类似地，有一个`istream_iterator`类将包装一个输入流对象并提供一个输入迭代器。这个类将使用流`>>`操作符提取指定类型的对象，这些对象可以通过流迭代器读取。然而，从一个流中读取数据比向一个流中写入数据更复杂，因为必须检测输入流中何时没有更多数据可供迭代器读取(文件结束的情况)。\n\n`istream_iterator`类有两个构造函数。一个构造函数有一个参数作为要读取的输入流，而另一个构造函数(默认构造函数)没有参数，用于创建流迭代器的**末端**。流迭代器的结尾用于指示流中没有更多的数据:\n\n```cpp\n    vector<int> data; \n    copy( \n       istream_iterator<int>(cin), istream_iterator<int>(), \n       back_inserter(data)); \n\n    ostream_iterator<int> my_out(cout, \" \"); \n    copy(data.cbegin(), data.cend(), my_out); \n    cout << endl;\n```\n\n对`copy`的第一次调用提供了两个输入迭代器，作为第一个参数，以及一个输出迭代器。该函数将数据从第一个迭代器复制到最后一个参数中的输出迭代器。由于最后一个参数是从`back_inserter`创建的，这意味着项目被插入到`vector`对象中。输入迭代器基于输入流(`cin`)，因此`copy`函数将从控制台读取`int`值(每个值由空格分隔)，直到没有更多值可用为止(例如，如果您按下 *CTRL* + *Z* 结束流或您键入非数字项)。由于可以用迭代器给出的一系列值初始化容器，因此可以使用`istream_iterator`作为构造函数参数:\n\n```cpp\n    vector<int> data {  \n       istream_iterator<int>(cin), istream_iterator<int>() };\n```\n\n这里使用初始化列表语法调用构造函数；如果使用圆括号，编译器会将此解释为函数的声明！\n\n如前所述，`istream_iterator`将使用流的`>>`运算符从流中读取指定类型的对象，并且该运算符使用空格来分隔项目(因此它只忽略所有空格)。如果你在一个包含`string`对象的容器中阅读，那么你在控制台上键入的每个单词都将是容器中的一个项目。一个`string`是一个字符容器，它也可以用迭代器初始化，所以你可以试着用一个`istream_iterator`从控制台输入数据到一个`string`中:\n\n```cpp\n    string data { \n            istream_iterator<char>(cin), istream_iterator<char>() };\n```\n\n在这种情况下，流是`cin`，但它很容易成为文件的`ifstream`对象。问题是`cin`对象会去掉空格，所以`string`对象会包含除了空格之外的所有你键入的内容，所以不会有空格和换行符。\n\n这个问题是由`istream_iterator`使用流的`>>`运算符引起的，只能通过使用另一个类`istreambuf_iterator`来避免:\n\n```cpp\n    string data { \n        istreambuf_iterator<char>(cin), istreambuf_iterator<char>() };\n```\n\n该类从流中读取每个字符，并将每个字符复制到容器中，无需`>>`处理。\n\n# 在 C 标准库中使用迭代器\n\nC 标准库通常需要指向数据的指针。例如，当一个 C 函数需要一个字符串时，它将需要一个指向包含该字符串的字符数组的`const char*`指针。C++ 标准库被设计成允许您将它的类与 C 标准库一起使用；事实上，C 标准库是 C++ 标准库的一部分。对于`string`对象，解决方法很简单:当您需要一个`const char*`指针时，您只需在一个`string`对象上调用`c_str`方法。\n\n将数据存储在连续内存中的容器(`array`、`string`或`data`)有一个名为`data`的方法，该方法以 C 数组的形式访问容器的数据。此外，这些容器拥有`[]`运算符来访问它们的数据，因此您也可以将第一个项目的地址视为`&container[0]`(其中`container`是容器对象)，就像使用 C 数组一样。但是，如果容器是空的，这个地址将是无效的，所以在使用它之前，您应该调用`empty`方法。这些容器中的项数是从`size`方法返回的，所以对于任何指向 C 数组开始及其大小的 C 函数，都可以用`&container[0]`和`size`方法的值来调用它。\n\n您可能想通过调用其`begin`函数来获取具有连续内存的容器的开头，但这将返回一个迭代器(通常是一个对象)。所以，要得到指向第一项的 C 指针，应该调用`&*begin`；也就是说，取消引用从`begin`函数返回的迭代器来获取第一项，然后使用地址运算符来获取它的地址。坦率地说，`&container[0]`更简单，可读性更强。\n\n如果容器没有将其数据存储在连续的内存中(例如`deque`和`list`，那么您可以通过简单地将数据复制到临时向量中来获得一个 C 指针。\n\n```cpp\n    list<int> data; \n    // do some calculations and fill the list \n    vector<int> temp(data.begin(), data.end()); \n    size_t size = temp.size(); // can pass size to a C function \n    int *p = &temp[0];         // can pass p to a C function\n```\n\n在这种情况下，我们选择使用`list`，例程将操纵`data`对象。在例程的后面，这些值将被传递给一个 C 函数，因此`list`被用来初始化一个`vector`对象，并且这些值是从`vector`获得的。\n\n# 算法\n\n标准库在`<algorithm>`头文件中有大量的通用函数。我们所说的泛型是指它们通过迭代器访问数据，而不知道迭代器指的是什么，因此这意味着您可以编写泛型代码来为任何合适的容器工作。但是，如果您知道容器类型，并且该容器有一个成员方法来执行相同的操作，则应该使用成员。\n\n# 项目的迭代\n\n`<algorithm>`中的许多例程将获取范围，并在这些范围内迭代执行一些操作。顾名思义，`fill`函数将为容器填充一个值。该函数使用两个迭代器来指定将放入容器每个位置的范围和值:\n\n```cpp\n    vector<int> vec; \n    vec.resize(5); \n    fill(vec.begin(), vec.end(), 42);\n```\n\n由于将为一个范围调用`fill`函数，这意味着您必须将迭代器传递给已经有值的容器，这就是这段代码调用`resize`方法的原因。该代码将把`42`的值放入容器的每个项目中，这样当它完成`vector`时就包含了`{42,42,42,42,42}`。这个函数的另一个版本叫做`fill_n`，它通过一个迭代器指定范围的开始和范围中项目的计数。\n\n`generate`函数是相似的，但是它有一个函数，可以是函数、函数对象或 lambda 表达式，而不是单个值。调用该函数来提供容器中的每个项目，因此它没有参数，并返回迭代器访问的类型的对象:\n\n```cpp\n    vector<int> vec(5); \n    generate(vec.begin(), vec.end(),  \n        []() {static int i; return ++ i; });\n```\n\n同样，您必须确保`generate`函数被传递到一个已经存在的范围，并且这段代码通过传递初始大小作为构造函数参数来实现这一点。在这个例子中，lambda 表达式有一个`static`变量，它随着每次调用而递增，所以这意味着在`generate`函数完成后`vector`包含`{1,2,3,4,5}`。这个函数的另一个版本叫做`generate_n`，它通过一个迭代器指定范围的开始和范围中项目的计数。\n\n`for_each`函数将遍历由两个迭代器提供的一个范围，并且对于该范围中的每个项目，调用一个指定的函数。此函数必须有一个与容器中的项目类型相同的参数:\n\n```cpp\n    vector<int> vec { 1,4,9,16,25 }; \n    for_each(vec.begin(), vec.end(),  \n         [](int i) { cout << i << \" \"; }); \n    cout << endl;\n```\n\n`for_each`函数迭代迭代器指定的所有项目(在这种情况下是整个范围)，取消迭代器的引用，并将项目传递给函数，这段代码的效果是打印容器的内容。该函数可以通过值(如本例中)或引用来获取项目。如果通过引用传递项目，则函数可以更改项目:\n\n```cpp\n    vector<int> vec { 1,2,3,4,5 }; \n    for_each(vec.begin(), vec.end(),  \n         [](int& i) { i *= i; });\n```\n\n调用该代码后，`vector`中的项目将被替换为这些项目的方块。如果使用 functor 或 lambda 表达式，可以传递一个容器来捕获函数的结果；例如:\n\n```cpp\n    vector<int> vec { 1,2,3,4,5 }; \n    vector<int> results; \n    for_each(vec.begin(), vec.end(),  \n         [&results](int i) { results.push_back(i*i); });\n```\n\n这里，声明了一个容器来接受对 lambda 表达式的每次调用的结果，并通过捕获变量来传递对表达式的引用。\n\nRecall from [Chapter 3](03.html), *Using Functions*, that the square brackets contain the names of the captured variables declared outside the expression. Once captured, it means that the expression is able to access the object.\n\n在本例中，每次迭代的结果(`i*i`)被推入捕获的集合中，以便存储结果供以后使用。\n\n`transform`功能有两种形式；它们都提供了一个函数(指针、函子或 lambda 表达式),并且它们都有一个通过迭代器传递的容器中的输入项范围。在这方面，它们类似于`for_each`。`transform`函数还允许您将迭代器传递给用于存储函数结果的容器。该函数必须有一个与输入迭代器引用的类型(或引用)相同类型的参数，并且它必须返回输出迭代器访问的类型。\n\n`transform`的另一个版本使用一个函数来组合两个范围内的值，所以这意味着函数必须有两个参数(这将是两个迭代器中的对应项)并返回输出迭代器的类型。您只需要给出一个输入范围内的全部项目，因为假设另一个范围至少同样大，因此您只需要提供第二个范围的开始迭代器:\n\n```cpp\n    vector<int> vec1 { 1,2,3,4,5 }; \n    vector<int> vec2 { 5,4,3,2,1 }; \n    vector<int> results; \n    transform(vec1.begin(), vec1.end(), vec2.begin(), \n       back_inserter(results), [](int i, int j) { return i*j; });\n```\n\n# 获取信息\n\n一旦容器中有了值，就可以调用函数来获取这些项的信息。`count`功能用于统计某一范围内具有指定值的项目数:\n\n```cpp\n    vector<int> planck{ 6,6,2,6,0,7,0,0,4,0 }; \n    auto number = count(planck.begin(), planck.end(), 6);\n```\n\n该代码将返回值`3`，因为容器中有三份`6`。函数的返回类型是容器的`difference_type` `typedef`中指定的类型，在这种情况下将是`int`。`count_if`函数以类似的方式工作，但是您传递了一个谓词，该谓词接受一个参数(容器中的当前项目)并返回一个`bool`，指定这是否是正在计数的值。\n\n`count`功能计算特定值的出现次数。如果你想聚合所有的值，那么你可以使用`<numeric>`中的`accumulate`功能。这将遍历该范围，访问每个项目，并保持所有项目的累计。\n\n求和将使用该类型的`+`运算符进行，但也有一个版本采用二进制函数(容器类型的两个参数并返回相同的类型)，该函数指定当您将两个这样的类型相加时会发生什么。\n\n`all_of`、`any_of`和`none_of`函数被传递一个谓词，该谓词带有同类型容器的单个参数；还有给定的迭代器，指示它们迭代的范围，用谓词测试每个项目。只有当所有项目的谓词为`true`时，`all_of`函数才会返回`true`，如果至少一个项目的谓词为`true`，则`any_of`函数会返回`true`，而只有当所有项目的谓词为`false`时，`none_of`函数才会返回`true`。\n\n# 比较容器\n\n如果您有两个数据容器，有多种方法可以比较它们。对于每个容器类型，都定义了`<`、`<=`、`==`、`!=`、`>`和`>=`运算符。`==`和`!=`操作员比较容器，包括它们有多少物品以及这些物品的价值。所以，如果项目有不同的项目数，不同的值，或者两者都有，那么它们就不相等。其他比较更喜欢值而不是项目数:\n\n```cpp\n    vector<int> v1 { 1,2,3,4 }; \n    vector<int> v2 { 1,2 }; \n    vector<int> v3 { 5,6,7 }; \n    cout << boolalpha; \n    cout << (v1 > v2) << endl; // true \n    cout << (v1 > v3) << endl; // false\n```\n\n在第一次比较中，两个向量有相似的项目，但是`v2`较少，所以`v1`比`v2`大。第二种情况，`v3`的数值比`v1`大，但是数值少，所以`v3`比 T6 大。\n\n您也可以使用`equal`功能比较范围。这被传递给两个范围(假设它们的大小相同，因此只需要一个迭代器来开始第二个范围)，并且它使用迭代器访问的类型的`==`运算符或用户提供的谓词来比较两个范围中的对应项。只有当所有这些比较都是`true`时，函数才会返回`true`。类似地，`mismatch`功能比较两个范围内的相应项目。然而，这个函数为第一个不相同的项目返回一个`pair`对象，该对象在两个范围的每一个中都有迭代器。还可以提供比较功能。`is_permutation is`的相似之处在于它比较两个范围内的值，但是如果两个范围具有相同的值，但不一定是相同的顺序，则它返回`true`。\n\n# 时代的变化\n\n**反转**功能作用于容器中的一个范围，并反转项目的顺序；这意味着迭代器必须是可写的。`copy`和`copy_n`功能将每个项目从一个范围向前复制到另一个范围；对于`copy`，输入范围由两个输入迭代器给出，对于`copy_n`，范围是一个输入迭代器和一个项目计数。`copy_backward`功能将从范围的末尾开始复制项目，这样输出范围将具有与原始顺序相同的项目。这意味着输出迭代器将指示要复制到的范围的*结束*。只有当项目满足谓词指定的某些条件时，才可以复制项目。\n\n*   `reverse_copy`功能将按照与输入范围相反的顺序创建一个副本；实际上，该函数在原始文件中向后迭代，并将项目向前复制到输出范围。\n*   不管名字如何，`move`和`move_backward`函数在语义上等同于`copy`和`copy_backward`函数。因此，在下面的示例中，操作后原始容器将具有相同的值:\n\n```cpp\n        vector<int> planck{ 6,6,2,6,0,7,0,0,4,0 }; \n        vector<int> result(4);          // we want 4 items \n        auto it1 = planck.begin();      // get the first position \n        it1 += 2;                       // move forward 2 places \n        auto it2 = it1 + 4;             // move 4 items \n        move(it1, it2, result.begin()); // {2,6,0,7}\n```\n\n*   这段代码将从第三个位置的项目开始，将四个项目从第一个容器复制到第二个容器。\n*   `remove_copy`和`remove_copy_if`函数遍历源范围，复制指定值以外的项目。\n\n```cpp\n        vector<int> planck{ 6,6,2,6,0,7,0,0,4,0 }; \n        vector<int> result; \n        remove_copy(planck.begin(), planck.end(),  \n            back_inserter(result), 6);\n```\n\n*   这里`planck`对象保持不变，`result`对象将包含`{2,0,7,0,0,4,0}`。`remove_copy_if`函数的行为类似，但被赋予了一个谓词而不是实际值。\n*   `remove`和`remove_if`函数并不完全按照它们的名字所暗示的那样运行。这些函数作用于单个范围，并迭代寻找特定的值(`remove`)，或者将每个项目传递给一个谓词，该谓词将指示该项目是否应该被移除(`remove_if`)。当移除一个项目时，容器中后面的项目会向前移动，但容器保持相同的大小，这意味着末端的项目会保持原样。`remove`函数之所以会这样，是因为它们只知道通过迭代器读写项目(迭代器对所有容器都是通用的)。要删除一个项目，函数需要访问容器的`erase`方法，而`remove`函数只能访问迭代器。\n*   如果您想删除末尾的项目，则必须相应地调整容器的大小。通常，这意味着在容器上调用合适的`erase`方法，这是可能的，因为`remove`方法将迭代器返回到新的结束位置:\n\n```cpp\n        vector<int> planck { 6,6,2,6,0,7,0,0,4,0 }; \n        auto new_end = remove(planck.begin(), planck.end(), 6); \n                                             // {2,0,7,0,0,4,0,0,4,0} \n        planck.erase(new_end, planck.end()); // {2,0,7,0,0,4,0}\n```\n\n*   `replace`和`replace_if`函数遍历单个范围，如果该值是指定值(`replace`)或从谓词(`replace_if`)返回`true`，则该项被替换为指定的新值。还有`replace_copy`和`replace_copy_if`两个功能，将原来的功能单独保留，改为另一个范围(类似于`remove_copy`和`remove_copy_if`功能)。\n*   `rotate`功能将范围视为结束与开始相连，因此您可以向前移动项目，以便当项目从结束处掉落时，它会被放在第一个位置。如果您想将每个项目向前移动四个位置，您可以这样做:\n\n```cpp\n        vector<int> planck{ 6,6,2,6,0,7,0,0,4,0 }; \n        auto it = planck.begin(); \n        it += 4; \n        rotate(planck.begin(), it, planck.end());\n```\n\n*   这个旋转的结果就是`{0,7,0,0,4,0,6,6,2,6}`。`rotate_copy`函数做同样的事情，但是，它不会影响原始容器，而是将项目复制到另一个容器中。\n*   `unique`函数作用于一个范围，并“移除”(以前面解释的方式)与相邻项目重复的项目，您可以为该函数提供一个谓词来测试两个项目是否相同。该函数只检查相邻的项目，因此容器中稍后会保留一个副本。如果您想删除所有重复项，那么您应该首先对容器进行排序，以便相似的项是相邻的。\n*   `unique_copy`函数只有在项目是唯一的情况下才会将项目从一个范围复制到另一个范围，因此删除重复项目的一种方法是在临时容器上使用该函数，然后将原始项目分配给临时项目:\n\n```cpp\n        vector<int> planck{ 6,6,2,6,0,7,0,0,4,0 }; \n        vector<int> temp; \n        unique_copy(planck.begin(), planck.end(), back_inserter(temp)); \n        planck.assign(temp.begin(), temp.end());\n```\n\n*   此代码后，`planck`容器将有`{6,2,6,0,7,0,4,0}`。\n*   最后，`iter_swap`将交换两个迭代器指示的项目，`swap_ranges`函数将一个范围内的项目交换到另一个范围内(第二个范围由一个迭代器指示，并假设引用与第一个范围大小相同的范围)。\n\n# 查找项目\n\n标准库具有广泛的搜索项目的功能:\n\n*   `min_element`函数将迭代器返回到一个范围内的最小项，`max_element`函数将迭代器返回到最大项。这些函数被传递给要检查的项目范围的迭代器和从两个项目的比较中返回`bool`的预测器。如果不提供预测器，将使用该类型的`<`运算符。\n\n```cpp\n        vector<int> planck{ 6,6,2,6,0,7,0,0,4,0 }; \n        auto imin = min_element(planck.begin(), planck.end()); \n        auto imax = max_element(planck.begin(), planck.end()); \n        cout << \"values between \" << *imin << \" and \"<< *imax << endl;\n```\n\n*   `imin`和`imax`值是迭代器，这就是为什么它们被解引用以获得值。如果你想一次得到最小元素和最大元素，你可以调用`minmax_element`，它将返回一个带有这些项目迭代器的`pair`对象。顾名思义，`adjacent_find`函数将返回具有相同值的前两个项目的位置(您可以提供一个谓词来确定*相同值*的含义)。这允许您搜索重复项并获取这些重复项的位置。\n\n```cpp\n        vector<int> vec{0,1,2,3,4,4,5,6,7,7,7,8,9}; \n        vector<int>::iterator it = vec.begin(); \n\n        do \n        { \n            it = adjacent_find(it, vec.end()); \n            if (it != vec.end()) \n            {  \n                cout << \"duplicate \" << *it << endl; \n                ++ it; \n            } \n        } while (it != vec.end());\n```\n\n*   这个代码有一个数字序列，其中有一些数字是重复的，彼此相邻。在这种情况下有三个*相邻的重复项:`4`后面是`4`，顺序`7,7,7`后面是`7`后面是`7`，`7`后面是`7`。`do`循环反复调用`adjacent_find`，直到返回`end`迭代器，表示已经搜索了所有项目。当找到重复对时，代码打印出该值，然后增加下一次搜索的开始位置。*\n*   `find`函数在容器中搜索单个值，如果找不到值，则返回该项的迭代器或`end`迭代器。`find_if`函数被传递一个谓词，它返回一个迭代器到它找到的第一个满足谓词的项；类似地，`find_if_not`函数查找第一个不满足谓词的项。\n*   有几个函数被赋予了两个范围，一个是要搜索的范围，另一个是要查找的值。不同的函数要么在搜索条件中查找一个项目，要么查找所有项目。这些函数将`==`操作符用于容器保存的类型或谓词。\n*   `find_first_of`函数返回它在搜索列表中找到的第一个项目的位置。`search`函数查找特定序列，返回整个序列的第一个*位置，而`find_end`函数返回整个搜索序列的最后一个*位置。最后，`search_n`函数寻找一个序列，该序列是一个在指定容器范围内重复多次的值(给出了该值和重复次数)。**\n\n# 对项目排序\n\n序列容器可以被排序，一旦您完成了这一步，您就可以使用方法来搜索项目，合并容器，或者获取容器之间的差异。`sort`函数将根据您提供的`<`运算符或谓词在一个范围内对项目进行排序。如果范围内有相等的项目，则排序后这些项目的顺序不能保证；如果这个顺序很重要，你应该调用`stable_sort`函数来代替。如果您想保留输入范围并将排序后的项目复制到另一个范围，您可以使用名称混乱的`partial_sort_copy`功能。这不是局部分类。这个函数被传递给输入范围的迭代器和输出范围的迭代器，所以你必须确保输出范围有一个合适的容量。\n\n您可以通过调用`is_sorted`函数来检查一个范围是否排序，如果它发现一个没有排序顺序的项目，这将遍历所有项目并返回`false`，在这种情况下，您可以通过调用`is_sorted_until`函数来定位第一个没有排序顺序的项目。\n\n顾名思义，`partial_sort`函数并不会将每一个项目按照相对于其他项目的精确顺序放置。相反，它将创建两个组或分区，其中第一个分区将具有最小的项目(不一定按任何顺序)，而另一个分区将具有最大的项目。保证最小的项目在第一个分区中。要调用这个函数，您需要传递三个迭代器，其中两个是要排序的范围，第三个迭代器是另外两个迭代器之间的某个位置，它指示边界，在边界之前是最小值。\n\n```cpp\n    vector<int> vec{45,23,67,6,29,44,90,3,64,18}; \n    auto middle = vec.begin() + 5; \n    partial_sort(vec.begin(), middle, vec.end()); \n    cout << \"smallest items\" << endl; \n    for_each(vec.begin(), middle, [](int i) {cout << i << \" \"; }); \n    cout << endl; // 3 6 18 23 29 \n    cout << \"biggest items\" << endl; \n    for_each(middle, vec.end(), [](int i) {cout << i << \" \"; }); \n    cout << endl; // 67 90 45 64 44\n```\n\n在这个例子中，有一个十个项目的向量，所以我们从一开始就把`middle`迭代器定义为五个项目(这只是一个选择，它可能是一些其他的值，取决于你想要获得多少个项目)。在这个例子中，您可以看到五个最小的项目已经被排序到前半部分，后半部分有最大的项目。\n\n这个奇怪命名的`nth_element`函数就像`partial_sort`一样。您为第 n 个*元素提供了一个迭代器，该函数确保范围中的第一个 *n* 项最小。`nth_element`功能比`partial_sort`快，虽然保证*第 n 个*元素之前的项目小于或等于*第 n 个*元素，但是分区内的排序顺序没有其他保证。*\n\n *`partial_sort`和`nth_element`函数是分区排序函数的版本。`partition`功能是一个更通用的版本。您向该函数传递一个范围和一个谓词，该谓词确定一个项将被放置在两个分区中的哪个分区中。满足谓词的项将被放在范围的第一个分区中，其他项将被放在第一个分区之后的范围中。第二个分区的第一项称为分区点，它是从`partition`函数返回的，但是您可以稍后通过将迭代器传递给分区范围并将谓词传递给`partition_point`函数来计算它。`partition_copy`功能也将对值进行分区，但它将保持原始范围不变，并将值放在已经分配的范围内。这些分区函数不能保证等价项目的顺序，如果这个顺序很重要，那么你应该调用`stable_partitian`函数。最后，您可以通过调用`is_partitioned`函数来确定容器是否被分区。\n\n`shuffle`功能会将容器中的物品重新排列成随机顺序。该功能需要`<random>`库中的统一随机数发生器。例如，下面将用十个整数填充一个容器，然后以随机顺序放置它们:\n\n```cpp\n    vector<int> vec; \n    for (int i = 0; i < 10; ++ i) vec.push_back(i); \n    random_device rd; \n    shuffle(vec.begin(), vec.end(), rd);\n```\n\n堆是一个部分排序的序列，其中第一个项目总是最大的，项目在对数时间内从堆中添加和移除。堆是基于序列容器的，奇怪的是，不是标准库提供适配器类，而是必须在现有容器上使用函数调用。要从现有容器创建一个堆，您需要将范围迭代器传递给`make_heap`函数，该函数将容器作为一个堆进行排序。\n\n然后，您可以使用其`push_back`方法向容器中添加新项目，但是每次这样做时，您都必须调用`push_heap`来重新排序堆。类似地，要从堆中获取一个项目，您需要调用容器上的`front`方法，然后通过调用`pop_heap`函数移除该项目，这可以确保堆保持有序。您可以通过调用`is_heap`来测试容器是否被安排为一个堆，如果容器没有被完全安排为一个堆，您可以通过调用`is_heap_until`来获取不满足堆标准的第一个项目的迭代器。最后，您可以使用`sort_heap`将一个堆排序为一个排序序列。\n\n一旦对容器进行了排序，就可以调用一些函数来获取关于序列的信息。`lower_bound`和`upper_bound`方法已经针对容器进行了描述，函数的行为方式相同:`lower_bound`返回第一个元素的位置，该元素的值大于或等于提供的值，`upper_bound`返回下一个项目的位置，该位置大于提供的值。`includes`功能测试一个排序范围是否包含第二个排序范围中的项目。\n\n以`set_`开头的函数将把两个排序的序列合并到第三个容器中。`set_difference`功能将复制第一序列中的项目，但不复制第二序列中的项目。这不是对称操作，因为它不包括第二个序列中的项目，但不包括第一个序列中的项目。如果你想要一个对称差，那么你应该调用`set_symmetric_difference`函数。`set_intersection`将复制两个序列中的项目。`set_union`功能将结合这两个序列。还有一个功能会把两个序列结合起来，就是`merge`功能。这两个函数的区别在于，使用`set_union`函数时，如果一个项目在两个序列中，结果容器中只放一个副本，而使用`merge`时，结果容器中将放两个副本。\n\n如果对一个范围进行了排序，那么可以调用`equal_range`函数来获取元素的范围，这些元素相当于传递给函数或谓词的值。这个函数返回一对迭代器，代表容器中的值的范围。\n\n最后一个需要排序容器的方法是`binary_search`。此函数用于测试容器中是否有值。向函数传递指示要测试的范围和一个值的迭代器，如果范围中有一个项目等于该值，它将返回`true`(您可以提供一个谓词来执行这个相等测试)。\n\n# 使用数字库\n\n标准库有几个类库来执行数字操作。在这一节中，我们将讨论两个:使用`<ratio>`的编译时算术和使用`<complex>`的复数。\n\n# 编译时算术\n\n分数是一个问题，因为有些分数没有足够的有效数字来准确地表示它们，导致当你在进一步的算术中使用它们时会失去准确性。此外，计算机是二进制的，仅仅将十进制小数部分转换成二进制将会失去准确性。`<ratio>`库提供了允许您将分数表示为整数比率的对象，并将分数计算作为比率执行的类。只有当你完成所有的小数运算后，你才会把数字转换成十进制，这意味着潜在的精度损失被最小化了。`<ratio>`库中的类执行的计算是在*编译时*进行的，所以编译器会捕捉到诸如被零除和溢出等错误。\n\n使用库很简单；您使用`ratio`类，并提供分子和分母作为模板参数。分子和分母将被分解存储，您可以通过对象的`num`和`den`成员访问这些值:\n\n```cpp\n    ratio<15, 20> ratio; \n    cout << ratio.num << \"/\" << ratio.den << endl;\n```\n\n这将打印出`3/4`。\n\n分数运算是使用模板进行的(事实上，这些是`ratio`模板的专门化)。乍一看可能有点奇怪，但你很快就会习惯的！\n\n```cpp\n    ratio_add<ratio<27, 11>, ratio<5, 17>> ratio; \n    cout << ratio.num << \"/\" << ratio.den << endl;\n```\n\n这将打印出`514/187`(你可能想拿一些纸，做分数计算来确认这一点)。数据成员实际上是`static`成员，所以创建变量没有什么意义。此外，因为算术是使用*类型*而不是*变量*进行的，所以最好通过这些类型访问成员:\n\n```cpp\n    typedef ratio_add<ratio<27, 11>, ratio<5, 17>> sum; \n    cout << sum::num << \"/\" << sum::den << endl;\n```\n\n现在，您可以将 sum 类型用作可以执行的任何其他操作的参数。四个二进制算术运算用`ratio_add`、`ratio_subtract`、`ratio_multiply`和`ratio_divide`进行。通过`ratio_equal`、`ratio_not_equal`、`ratio_greater`、`ratio_greater_equal`、`ratio_less`和`ratio_less_equal`进行比较。\n\n```cpp\n    bool result = ratio_greater<sum, ratio<25, 19> >::value; \n    cout << boolalpha << result << endl;\n```\n\n该操作测试之前进行的计算(`514/187`)是否大于分数`25/19`(是)。编译器会拾取被零除的错误和溢出，因此以下内容不会编译:\n\n```cpp\n    typedef ratio<1, 0> invalid; \n    cout << invalid::num << \"/\" << invalid::den << endl;\n```\n\n然而，重要的是要指出，当分母被访问时，编译器将在第二行发出错误。国际单位制前缀也有不同的比例。这意味着您可以以纳米为单位进行计算，当您需要以米为单位显示数据时，您可以使用`nano`类型来获得比率:\n\n```cpp\n    double radius_nm = 10.0; \n    double volume_nm = pow(radius_nm, 3) * 3.1415 * 4.0 / 3.0; \n    cout << \"for \" << radius_nm << \"nm \" \n        \"the volume is \" << volume_nm << \"nm3\" << endl; \n    double factor = ((double)nano::num / nano::den); \n    double vol_factor = pow(factor, 3); \n    cout << \"for \" << radius_nm * factor << \"m \" \n        \"the volume is \" << volume_nm * vol_factor << \"m3\" << endl;\n```\n\n在这里，我们在一个球体上进行计算，其单位为**纳米** ( **纳米**)。球体的半径为 10 nm，因此第一次计算得出的体积为 4188.67 nm3。第二种计算将纳米转换为米；因子由`nano`比率确定(注意，对于体积，因子是立方的)。您可以定义一个类来进行这样的转换:\n\n```cpp\n    template<typename units> \n    class dist_units \n    { \n        double data; \n        public: \n            dist_units(double d) : data(d) {} \n\n        template <class other> \n        dist_units(const dist_units<other>& len) : data(len.value() *  \n         ratio_divide<units, other>::type::den / \n         ratio_divide<units, other>::type::num) {} \n\n        double value() const { return data; } \n    };\n```\n\n该类是为特定类型的单元定义的，这将通过`ratio`模板的实例化来表达。该类有一个构造函数来初始化它，以获取这些单位中的值，还有一个构造函数从其他单位转换而来，它只是将当前单位除以其他类型的单位。这个类可以这样使用:\n\n```cpp\n    dist_units<kilo> earth_diameter_km(12742); \n    cout << earth_diameter_km.value() << \"km\" << endl; \n    dist_units<ratio<1>> in_meters(earth_diameter_km); \n    cout << in_meters.value()<< \"m\" << endl; \n    dist_units<ratio<1609344, 1000>> in_miles(earth_diameter_km); \n    cout << in_miles.value()<< \"miles\" << endl;\n```\n\n第一个变量基于`kilo`，因此单位是公里。要将其转换为米，第二个变量类型基于`ratio<1>`，与`ratio<1,1>`相同。结果是当放置在`in_meters`中时，`earth_diameter_km`中的值乘以 1000。换算成英里数要复杂一点。一英里有 1609.344 米。`in_miles`变量使用的比值为 1609344/1000 或 1609.344。我们正在用`earth_diameter_km`初始化变量，那么这个值是不是大了 1000 倍？不是，原因是`earth_diameter_km`的类型是`dist_units<kilo>`，所以公里和英里之间的换算会包含 1000 这个因子。\n\n# 复数\n\n复数不仅仅是数学上的兴趣，它们在工程和科学中也是至关重要的，因此`complex`类型是任何类型库的重要组成部分。一个复数由两部分组成——实部和虚部。顾名思义，虚数不是实数，不能当作实数。\n\n在数学中，复数通常表示为二维空间中的坐标。如果一个实数可以被认为是 x 轴上无穷多个点之一，那么一个虚数可以被认为是 y 轴上无穷多个点之一。这两者之间唯一的交集是原点，因为零是零，所以什么也不是，它可以是零实数或零虚数。一个复数既有实部也有虚部，因此这可以想象成一个笛卡尔点。事实上，将复数可视化的另一种方式是将其表示为一个极坐标数，其中该点表示为与 x 轴(正实数轴)上的位置成指定角度的指定长度的向量。\n\n`complex`类基于浮点类型，有`float`、`double`和`long double`的专门化。上课简单；它有一个构造器，该构造器有两个参数用于数字的实部和虚部，它定义了用于赋值、比较、`+`、`-`、`/`和`*`的运算符(成员方法和全局函数)，作用于实部和虚部。\n\nAn operation like `+` is simple for a complex number: you just add the real parts together and the imaginary parts together, and these two sums are the real and imaginary parts of the result. However, multiplication and division are a bit more, umm, complex. In multiplication, you get a quadratic: the aggregation of the two real parts multiplied, the two imaginary parts multiplied, the two values of the real part of the first multiplied with the imaginary part of the second, and the imaginary part of the first multiplied with the real part of the second. The complication is that two imaginary numbers multiplied is equivalent to the multiplication of two equivalent real numbers multiplied by -1\\. Furthermore, multiplying a real and an imaginary number results in an imaginary number that is equivalent in size to the multiplication of two equivalent real numbers.\n\n还有对复数进行三角运算的函数:`sin`、`cos`、`tan`、`sinh`、`cosh`、`tanh`；以及`log`、`exp`、`log10`、`pow`、`sqrt`等基本数学运算。您还可以调用函数来创建复数并获取有关它们的信息。所以，`polar`函数会取两个代表向量长度和角度极坐标的浮点数。如果你有一个`complex`数字对象，你可以通过调用`abs`(获取长度)和`arg`(获取角度)来获取极坐标。\n\n```cpp\n    complex<double> a(1.0, 1.0); \n    complex<double> b(-0.5, 0.5); \n    complex<double> c = a + b; \n    cout << a << \" + \" << b << \" = \" << c << endl; \n    complex<double> d = polar(1.41421, -3.14152 / 4); \n    cout << d << endl;\n```\n\n首先要说明的是，有一个为`complex`号定义的`ostream`插入操作符，因此您可以将它们插入到`cout`流对象中。这段代码的输出如下:\n\n```cpp\n    (1,1) + (-0.5,0.5) = (0.5,1.5)\n(1.00002,-0.999979)\n```\n\n第二行显示了 2 和-1/4π的平方根只用 5 个小数位的局限性，这个数实际上就是复数`(1, -1)`。\n\n# 使用标准库\n\n在这个例子中，我们将为**逗号分隔值** ( **CSV** )文件开发一个简单的解析器。我们将遵循以下规则:\n\n*   每条记录将占用一行，换行符表示一条新记录\n*   记录中的字段由逗号分隔，除非它们位于带引号的字符串中\n*   字符串可以使用单引号(`'`)或双引号(`\"`)来引用，在这种情况下，它们可以包含逗号作为字符串的一部分\n*   立即重复的引号(`''`或`\"\"`)是一个文字，是字符串的一部分，而不是字符串的分隔符\n*   如果引用字符串，则忽略字符串外的空格\n\n这是一个非常基本的实现，并且省略了引用字符串可以包含换行符的通常要求。\n\n在本例中，大部分操作将使用`string`对象作为单个字符的容器。\n\n首先在本书的文件夹中为名为`Chapter_08`的章节创建一个文件夹。在该文件夹中，创建一个名为`csv_parser.cpp`的文件。由于应用将使用控制台输出和文件输入，因此在文件顶部添加以下行:\n\n```cpp\n    #include <iostream> \n    #include <fstream> \n\n    using namespace std;\n```\n\n该应用还将使用一个命令行参数来解析 CSV 文件，因此在文件底部添加以下代码:\n\n```cpp\n    void usage() \n    { \n        cout << \"usage: csv_parser file\" << endl; \n        cout << \"where file is the path to a csv file\" << endl; \n    } \n\n    int main(int argc, const char* argv[]) \n    { \n        if (argc <= 1) \n        { \n            usage(); \n            return 1; \n        } \n        return 0; \n    }\n```\n\n应用会将一个文件一行一行地读入到一个`string`对象的`vector`中，因此将`<vector>`添加到包含文件的列表中。为了使编码更容易，在`usage`功能上面定义以下内容:\n\n```cpp\n    using namespace std; \n    using vec_str = vector<string>;\n```\n\n`main`函数将逐行读取文件，最简单的方法是使用`getline`函数，因此将`<string>`头文件添加到包含文件列表中。在`main`功能的末尾添加以下几行:\n\n```cpp\n    ifstream stm; \n    stm.open(argv[1], ios_base::in); \n    if (!stm.is_open()) \n    { \n        usage(); \n        cout << \"cannot open \" << argv[1] << endl; \n        return 1; \n    } \n\n    vec_str lines; \n    for (string line; getline(stm, line); ) \n    { \n        if (line.empty()) continue; \n        lines.push_back(move(line)); \n    } \n    stm.close();\n```\n\n前几行使用`ifstream`类打开文件。如果找不到文件，则打开文件的操作失败，这通过调用`is_open`来测试。接下来，声明一个`string`对象的`vector`，并用从文件中读取的行填充。`getline`函数有两个参数:第一个是打开的文件流对象，第二个是包含字符数据的字符串。这个函数返回流对象，它有一个`bool`转换操作符，因此`for`语句将循环，直到这个流对象表明它不能再读取数据。当流到达文件末尾时，会设置一个内部文件结束标志，这将导致`bool`转换运算符返回一个值`false`。\n\n如果`getline`函数读取一个空行，那么`string`将无法解析，所以对此有一个测试，这样的空行不会被存储。每一个合法的行都被推入到`vector`中，但是，由于这个`string`变量在这个操作之后将不被使用，我们可以使用移动语义，因此这通过调用`move`函数变得显式。\n\n这段代码现在将编译并运行(尽管它不会产生任何输出)。您可以在符合前面给出的标准的任何 CSV 文件上使用它，但是作为测试文件，我们使用了以下文件:\n\n```cpp\n    George Washington,1789,1797 \n    \"John Adams, Federalist\",1797,1801 \n    \"Thomas Jefferson, Democratic Republican\",1801,1809 \n    \"James Madison, Democratic Republican\",1809,1817 \n    \"James Monroe, Democratic Republican\",1817,1825 \n    \"John Quincy Adams, Democratic Republican\",1825,1829 \n    \"Andrew Jackson, Democratic\",1829,1837 \n    \"Martin Van Buren, Democratic\",1837,1841 \n    \"William Henry Harrison, Whig\",1841,1841 \n    \"John Tyler, Whig\",1841,1841 \n    John Tyler,1841,1845\n```\n\n这些是 1845 年以前的美国总统；第一个字符串是总统的名字和他们的从属关系，但是当总统没有从属关系时，它就被遗漏了(华盛顿和泰勒)。名字后面是他们任期的开始和结束年份。\n\n接下来，我们希望解析向量中的数据，并根据前面给出的规则将项目拆分为单个字段(字段之间用逗号分隔，但会使用引号)。为此，我们将每一行表示为一组`list`字段，每个字段为一个`string`。在文件顶部附近为`<list>`添加一个包含。在文件顶部`using`声明处，添加以下内容:\n\n```cpp\n    using namespace std; \n    using vec_str = vector<string>; \n    using list_str = list<string>;using vec_list = vector<list_str>;\n```\n\n现在，在`main`功能的底部，添加:\n\n```cpp\n    vec_list parsed; \n    for (string& line : lines) \n    { \n        parsed.push_back(parse_line(line)); \n    }\n```\n\n第一行创建`list`对象的`vector`，`for`循环遍历每一行，调用一个名为`parse_line`的函数，该函数解析一个字符串并返回一个`string`对象的`list`。该函数的返回值将是一个临时对象，因此是一个右值，因此这意味着将调用带有移动语义的`push_back`版本。\n\n在使用功能上面，增加`parse_line`功能的开始:\n\n```cpp\n    list_str parse_line(const string& line) \n    { \n        list_str data; \n        string::const_iterator it = line.begin(); \n\n        return data; \n    }\n```\n\n该函数将字符串视为一个字符容器，因此它将使用`const_iterator`遍历行参数。解析将在`do`循环中进行，因此添加以下内容:\n\n```cpp\n    list_str data; \n    string::const_iterator it = line.begin(); \n    string item; bool bQuote = false; bool bDQuote = false; do{++ it; } while (it != line.end()); data.push_back(move(item)); \n    return data;\n```\n\n布尔变量将在稍后解释。`do`循环递增迭代器，当达到`end`值时，循环结束。`item`变量将保存解析后的数据(此时为空)，最后一行将值放入`list`；这样，任何未保存的数据都会在功能完成前存储在`list`中。由于项目变量即将被销毁，对`move`的调用确保其内容被移动到`list`中，而不是被复制。如果没有这个调用，在将项目放入`list`时将调用字符串复制构造函数。\n\n接下来，您需要对数据进行解析。为此，添加一个开关来测试三种情况:逗号(表示字段的结尾)和引号或双引号(表示带引号的字符串)。其思想是使用`item`变量逐个字符地读取每个字段并建立其值。\n\n```cpp\n    do \n    { \n        switch (*it) { case ''': break; case '\"': break; case ',': break; default: item.push_back(*it); }; \n        ++ it; \n    } while (it != line.end());\n```\n\n默认操作很简单:它将字符复制到临时字符串中。如果字符是单引号，我们有两个选项。要么报价在双引号内，在这种情况下，我们希望报价存储在`item`中，要么报价是分隔符，在这种情况下，我们通过设置`bQuote`值来存储它是开始报价还是结束报价。对于单引号，添加以下内容:\n\n```cpp\n    case ''': \n    if (bDQuote) item.push_back(*it); else { bQuote = !bQuote; if (bQuote) item.clear(); } \n    break;\n```\n\n这很简单。如果这是在一个双引号字符串中(`bDQuote`被设置)，那么我们存储该引号。如果不是，那么我们翻转`bQuote bool`，这样如果这是第一个引用，我们注册字符串被引用，否则我们注册它是字符串的结尾。如果我们在一个带引号的字符串的开头，我们清除 item 变量以忽略前面逗号(如果有)和引号之间的任何空格。但是，这段代码没有考虑使用两个相邻的引号，这意味着引号是一个文字，也是字符串的一部分。更改代码以添加针对这种情况的检查:\n\n```cpp\n    if (bDQuote) item.push_back(*it); \n    else \n    { \n        if ((it + 1) != line.end() && *(it + 1) == ''') { item.push_back(*it); ++ it; } else \n        { \n            bQuote = !bQuote; \n            if (bQuote) item.clear(); \n        } \n    }\n```\n\n`if`语句进行检查，以确保如果我们增加迭代器，我们不会在行尾(在这种情况下，短路将在这里发生，表达式的其余部分将不会被计算)。我们可以测试下一个项目，然后我们查看下一个项目，看它是否是单引号；如果是，那么我们将其添加到`item`变量中，并递增迭代器，以便在循环中使用两个引号。\n\n双引号的代码类似，但切换布尔变量并测试双引号:\n\n```cpp\n    case '\"': \n    if (bQuote) item.push_back(*it); else { if ((it + 1) != line.end() && *(it + 1) == '\"') { item.push_back(*it); ++ it; } else { bDQuote = !bDQuote; if (bDQuote) item.clear(); } } \n    break;\n```\n\n最后，我们需要代码来测试逗号。同样，我们有两种情况:要么这是引用字符串中的逗号，在这种情况下，我们需要存储字符，要么这是字段的结尾，在这种情况下，我们需要完成对该字段的解析。代码非常简单:\n\n```cpp\n    case ',': \n    if (bQuote || bDQuote)  item.push_back(*it); else                    data.push_back(move(item)); \n    break;\n```\n\n`if`语句测试我们是否在引用的字符串中(在这种情况下`bQuote`或`bDQuote`将为真)，如果是，则存储字符。如果这是字段的结尾，我们将`string`推入`list`，但是我们使用`move`，以便变量数据被移动，并且`string`对象保持未初始化状态。\n\n这段代码将编译并运行。然而，仍然没有输出，所以在我们纠正之前，回顾一下您编写的代码。在`main`函数的末尾，你会有一个`vector`，其中每个项目都有一个`list`对象，代表 CSV 文件中的每一行，而`list`中的每个项目都是一个字段。现在，您已经解析了文件，并且可以相应地使用这些数据。为了让您看到数据已经被解析，在`main`函数的底部添加以下几行:\n\n```cpp\n    int count = 0; \n    for (list_str row : parsed) \n    { \n        cout << ++ count << \"> \"; \n        for (string field : row) \n        { \n            cout << field << \" \"; \n        } \n        cout << endl; \n    }\n```\n\n现在，您可以编译代码(使用`/EHsc`开关)并运行传递 CSV 文件名称的应用。\n\n# 摘要\n\n在本章中，您已经看到了 C++ 标准库中的一些主要类，并深入研究了容器和迭代器类。一个这样的容器是`string`类；这是一门非常重要的课程，将在下一章中更深入地介绍。****"
  },
  {
    "path": "docs/mod-cpp/06.md",
    "content": "# 六、使用字符串\n\n在某些时候，您的应用需要与人交流，这意味着使用文本；例如输出文本，将数据作为文本接收，然后将该数据转换为适当的类型。C++ 标准库拥有丰富的类集合，用于操作字符串、在字符串和数字之间进行转换，以及获取针对指定语言和区域性的本地化字符串值。\n\n# 使用字符串类作为容器\n\nC++ 字符串基于`basic_string`模板类。这个类是一个容器，所以它使用迭代器访问和方法来获取信息，并且有模板参数，这些参数包含关于它所保存的字符类型的信息。特定的字符类型有不同的`typedef`:\n\n```cpp\n    typedef basic_string<char,\n       char_traits<char>, allocator<char> > string; \n    typedef basic_string<wchar_t,\n       char_traits<wchar_t>, allocator<wchar_t> > wstring; \n    typedef basic_string<char16_t,\n       char_traits<char16_t>, allocator<char16_t> > u16string; \n    typedef basic_string<char32_t,\n       char_traits<char32_t>, allocator<char32_t> > u32string;\n```\n\n`string`类基于`char`，`wstring`基于`wchar_t`宽字符，`16string`和`u32string`类分别基于 16 位和 32 位字符。对于本章的其余部分，我们将只关注`string`类，但它同样适用于其他类。\n\n比较、复制和访问字符串中的字符将需要不同大小字符的不同代码，而 traits 模板参数提供了实现。对于`string`来说，这是`char_traits`班。例如，当这个类复制字符时，它会将这个动作委托给`char_traits`类及其`copy`方法。流类也使用特性类，因此它们也定义了适合文件流的文件结束值。\n\n字符串本质上是零个或多个字符的数组，当需要时分配内存，当`string`对象被破坏时释放内存。在某些方面，它非常类似于`vector<char>`对象。作为一个容器，`string`类通过`begin`和`end`方法提供迭代器访问:\n\n```cpp\n    string s = \"hellon\"; \n    copy(s.begin(), s.end(), ostream_iterator<char>(cout));\n```\n\n这里调用`begin`和`end`方法从`string,`中的项目获取迭代器，迭代器从`<algorithm>`传递到`copy`函数，通过`ostream_iterator`临时对象将每个字符复制到控制台。在这方面，`string`对象类似于一个`vector`，所以我们使用之前定义的`s`对象:\n\n```cpp\nvector<char> v(s.begin(), s.end()); \ncopy(v.begin(), v.end(), ostream_iterator<char>(cout));\n```\n\n这将使用在`string`对象上使用`begin`和`end`方法提供的字符范围填充`vector`对象，然后使用`copy`功能将这些字符打印到控制台，方式与我们之前使用的完全相同。\n\n# 获取关于字符串的信息\n\n`max_size`方法将给出计算机体系结构上指定字符类型的字符串的最大大小，这可能会非常大。例如，在具有 2 GB 内存的 64 位 Windows 计算机上，`string`对象的`max_size`将返回 40 亿个字符，而对于`wstring`对象，该方法将返回 20 亿个字符。这显然超过了机器中的内存！其他大小方法返回更有意义的值。`length`方法返回与`size`方法相同的值，即字符串中有多少项(字符)。`capacity`方法根据字符数指示已经为字符串分配了多少内存。\n\n您可以通过调用其`compare`方法来比较一个`string`和另一个。这将返回一个`int`而不是一个`bool`(但是注意一个`int`可以无声地转换成一个`bool`，其中`0`的返回值意味着两个字符串是相同的。如果它们不相同，如果参数字符串大于操作数字符串，则此方法返回负值；如果参数小于操作数字符串，则返回正值。在这方面*大于*和*小于*将测试字符串的字母顺序。此外，还有为`<`、`<=`、`==`、`>=`和`>`定义的全局运算符来比较字符串对象。\n\n一个`string`对象可以通过`c_str`方法像 C 弦一样使用。返回的指针是`const`；您应该知道，如果`string`对象发生变化，指针可能会失效，因此您不应该存储该指针。您不应该使用`&str[0]`为 C++ 字符串`str`获取 C 字符串指针，因为字符串类使用的内部缓冲区不能保证`NUL`被终止。提供`c_str`方法是为了返回一个*可以*作为 C 字符串的指针，因此`NUL`终止。\n\n如果你想把数据从 C++ 字符串复制到 C 缓冲区，你可以调用`copy`方法。您将目标指针和要复制的字符数作为参数(以及可选的偏移量)传递，并且该方法将尝试最多将指定数量的字符复制到目标缓冲区:*，但没有空终止字符*。此方法假设目标缓冲区足够大，可以容纳复制的字符(您应该采取措施来确保这一点)。如果您想传递缓冲区的大小，以便该方法为您执行该检查，请调用`_Copy_s`方法。\n\n# 改变字符串\n\n字符串类有标准的容器访问方法，因此您可以使用`at`方法和`[]`运算符通过引用(读写访问)访问单个字符。您可以使用`assign`方法替换整个字符串，或者使用`swap`方法交换两个字符串对象的内容。此外，您可以使用`insert`方法在指定位置插入字符，使用`erase`方法删除指定字符，使用`clear`方法删除所有字符。该类还允许您使用`push_back`和`pop_back`方法将字符推到字符串的末尾(并删除最后一个字符):\n\n```cpp\n    string str = \"hello\"; \n    cout << str << \"n\"; // hello \n    str.push_back('!'); \n    cout << str << \"n\"; // hello! \n    str.erase(0, 1); \n    cout << str << \"n\"; // ello!\n```\n\n您可以使用`append`方法或`+=`运算符在字符串末尾添加一个或多个字符:\n\n```cpp\n    string str = \"hello\"; \n    cout << str << \"n\";  // hello \n    str.append(4, '!'); \n    cout << str << \"n\";  // hello!!!! \n    str += \" there\"; \n    cout << str << \"n\";  // hello!!!! there\n```\n\n`<string>`库还定义了一个全局`+`运算符，该运算符将两个字符串连接成第三个字符串。\n\n如果要更改字符串中的字符，可以使用`[]`运算符通过索引访问该字符，使用引用覆盖该字符。您也可以使用`replace`方法将指定位置的一个或多个字符替换为来自 C 字符串、C++ 字符串或通过迭代器访问的其他容器的字符:\n\n```cpp\n    string str = \"hello\"; \n    cout << str << \"n\";    // hello \n    str.replace(1, 1, \"a\"); \n    cout << str << \"n\";    // hallo\n```\n\n最后，您可以提取字符串的一部分作为新字符串。`substr`方法采用偏移和可选计数。如果省略字符数，则子字符串将从指定位置开始，直到字符串结束。这意味着您可以通过传递偏移量 0 和小于字符串大小的计数来复制字符串的左边部分，或者您可以通过仅传递第一个字符的索引来复制字符串的右边部分。\n\n```cpp\n    string str = \"one two three\"; \n    string str1 = str.substr(0, 3);  \n    cout << str1 << \"n\";          // one \n    string str2 = str.substr(8); \n    cout << str2 << \"n\";          // three\n```\n\n在这段代码中，第一个示例将前三个字符复制到一个新字符串中。在第二个示例中，复制从第八个字符开始，一直持续到结尾。\n\n# 搜索字符串\n\n使用字符、C 字符串或 C++ 字符串传递`find`方法，您可以提供一个初始搜索位置来开始搜索。`find`方法返回搜索文本所在的位置(而不是迭代器)，如果找不到文本，则返回`npos`值。offset 参数和来自`find`方法的成功返回值使您能够重复解析字符串以查找特定的项目。`find`方法向前搜索指定的文本，还有一个`rfind`方法反向搜索。\n\n注意`rfind`并不是`find`方法的完全相反。`find`方法在字符串中向前移动搜索点，并在每一点将搜索字符串与来自搜索点的字符进行向前比较(因此第一个搜索文本字符，然后第二个字符，依此类推)。`rfind`方法将搜索点*向后*移动，但仍进行*向前*的比较。因此，假设`rfind`方法没有给定偏移量，第一次比较将在距离字符串末尾的偏移量处进行搜索文本的大小。然后，通过将搜索文本中的第一个字符与搜索字符串中搜索点处的字符进行比较来进行比较，如果成功，则将搜索文本中的第二个字符与搜索点之后的字符进行比较。因此，比较是在与搜索点移动方向相反的方向上进行的。\n\n这变得很重要，因为如果您想使用来自`find`方法的返回值作为偏移量来解析字符串，那么在每次搜索之后，您应该将搜索偏移量*向前移动**，对于`rfind`您应该将其向后移动*。**\n\n **例如，要搜索以下字符串中`the`的所有位置，可以调用:\n\n```cpp\n    string str = \"012the678the234the890\"; \n    string::size_type pos = 0; \n    while(true) \n    { \n        pos++ ; \n        pos = str.find(\"the\",pos); \n        if (pos == string::npos) break; \n        cout << pos << \" \" << str.substr(pos) << \"n\"; \n    } \n    // 3 the678the234the890 \n    // 9 the234the890 \n    // 15 the890\n```\n\n这将在字符位置 3、9 和 15 找到搜索文本。要向后搜索字符串，可以调用:\n\n```cpp\n    string str = \"012the678the234the890\"; \n    string::size_type pos = string::npos; \n    while(true) \n    { \n        pos--; pos = str.rfind(\"the\",pos); \n        if (pos == string::npos) break; \n        cout << pos << \" \" << str.substr(pos) << \"n\"; \n    } \n    // 15 the890 \n    // 9 the234the890 \n    // 3 the678the234the890\n```\n\n突出显示的代码显示了应该进行的更改，显示您需要从头开始搜索并使用`rfind`方法。当你有一个成功的结果时，你需要在下一次搜索前减少位置。与`find`方法一样，`rfind`方法如果找不到搜索文本，则返回`npos`。\n\n有四种方法可以让您搜索几个单独的字符之一。例如:\n\n```cpp\n    string str = \"012the678the234the890\"; \n    string::size_type pos = str.find_first_of(\"eh\"); \n    if (pos != string::npos) \n    { \n        cout << \"found \" << str[pos] << \" at position \"; \n        cout << pos << \" \" << str.substr(pos) << \"n\"; \n    } \n    // found h at position 4 he678the234the890\n```\n\n搜索字符串为`eh`，当在字符串中找到字符`e`或`h`时，`find_first_of`将返回。在本例中，字符`h`首先出现在位置 4。您可以提供一个偏移参数来开始搜索，因此您可以使用来自`find_first_of`的返回值来解析字符串。`find_last_of`方法与此类似，但它会以相反的方向在字符串中搜索搜索文本中的一个字符。\n\n除了搜索文本中提供的字符外，还有两种搜索方法可以查找字符*:`find_first_not_of`和`find_last_not_of`。例如:*\n\n```cpp\n    string str = \"012the678the234the890\"; \n    string::size_type pos = str.find_first_not_of(\"0123456789\"); \n    cout << \"found \" << str[pos] << \" at position \"; \n    cout << pos << \" \" << str.substr(pos) << \"n\"; \n    // found t at position 3 the678the234the890\n```\n\n该代码寻找数字以外的字符，因此它在位置 3(第四个字符)找到`t`。\n\n没有库函数可以修剪`string`中的空白，但是您可以通过使用 find 函数查找非空白来修剪字符串左侧和右侧的空白，然后将其用作`substr`方法的适当索引。\n\n```cpp\n    string str = \"  hello  \"; \n    cout << \"|\" << str << \"|n\";  // |  hello  | \n    string str1 = str.substr(str.find_first_not_of(\" trn\")); \n    cout << \"|\" << str1 << \"|n\"; // |hello  | \n    string str2 = str.substr(0, str.find_last_not_of(\" trn\") + 1); \n    cout << \"|\" << str2 << \"|n\"; // |  hello|\n```\n\n在前面的代码中，创建了两个新的字符串:一个左对齐空格，另一个右对齐空格。第一次向前搜索第一个非空白字符，并将其用作子字符串的起始索引(不提供计数，因为所有剩余的字符串都被复制)。在第二种情况下，对字符串进行反向搜索，寻找非空白字符，但返回的位置将是`hello`的最后一个字符；因为我们需要从第一个字符开始的子字符串，所以我们增加这个索引来获得要复制的字符数。\n\n# 国际化\n\n`<locale>`头包含用于本地化时间、日期和货币格式的类，还为字符串比较和排序提供本地化规则。\n\nThe C Runtime Library also has global functions to carry out localization. However, it is important in the following discussion that we distinguish between C functions and the C locale. The C locale is the default locale, including the rules for localization, used in C and C++ programs and it can be replaced with a locale for a country or culture. The C Runtime Library provides functions to change the locale, as does the C++ Standard Library.\n\n由于 C++ 标准库提供了本地化类，这意味着您可以创建多个代表一个区域设置的对象。区域设置对象可以在函数中创建，并且只能在函数中使用，或者可以全局应用于线程，并且只能由该线程上运行的代码使用。这与 C 本地化函数形成对比，在 C 本地化函数中，更改区域设置是全局的，因此所有代码(以及所有执行线程)都会受到影响。\n\n`locale`类的实例要么通过类构造函数创建，要么通过类的静态成员创建。C++ 流类将使用区域设置(如后面所解释的)，如果您想要更改区域设置，您可以在流对象上调用`imbue`方法。在某些情况下，您可能希望直接访问这些规则中的一个，并且您可以通过 locale 对象访问它们。\n\n# 使用刻面\n\n国际化规则被称为**方面**。区域设置对象是一个方面的容器，您可以使用`has_facet`函数测试区域设置是否有特定的方面；如果是这样的话，你可以通过调用`use_facet`函数得到一个对方面的`const`引用。下表中有六种类型的方面，由七个类别概括。方面类是`locale::facet`嵌套类的子类。\n\n| **刻面类型** | **描述** |\n| `codecvt`、`ctype` | 在一种编码方案和另一种编码方案之间进行转换，用于对字符进行分类并将其转换为大写或小写 |\n| `collate` | 控制字符串中字符的排序和分组，包括字符串的比较和散列 |\n| `messages` | 从目录中检索本地化邮件 |\n| `money` | 将表示货币的数字转换成字符串或从字符串转换成数字 |\n| `num` | 将数字转换为字符串或从字符串转换 |\n| `time` | 音乐会时间和日期以数字形式往返于字符串 |\n\nfacet 类用于将数据转换为字符串，因此它们都有一个用于所用字符类型的模板参数。`money`、`num,`和`time`面分别由三个类表示。带有`_get`后缀的类处理字符串解析，而带有`_put`后缀的类处理字符串格式。对于`money`和`num`方面，有一个带有`punct`后缀的类，它包含标点符号的规则和符号。\n\n由于`_get`方面用于将字符序列转换为数字类型，因此类有一个模板参数，您可以使用它来指示`get`方法将用来表示一系列字符的输入迭代器类型。类似地，`_put`方面类有一个模板参数，您可以使用它来提供输出迭代器类型`put`方法将把转换后的字符串写入其中。两种迭代器类型都有默认类型。\n\n`messages`方面用于与 POSIX 代码兼容。该类旨在允许您为应用提供本地化字符串。其思想是用户界面中的字符串被索引，在运行时，您通过`messages`方面使用索引访问本地化的字符串。然而，Windows 应用通常使用使用**消息编译器**编译的消息资源文件。也许正是因为这个原因，作为标准库的一部分提供的`messages`方面没有做任何事情，但是基础设施在那里，并且您可以派生自己的`messages`方面类。\n\n`has_facet`和`use_facet`函数是为您想要的特定类型的方面模板化的。所有方面类都是`locale::facet`类的子类，但是通过这个模板参数，编译器将实例化一个返回您请求的特定类型的函数。例如，如果您想为法语区域设置设置时间和日期字符串的格式，可以调用以下代码:\n\n```cpp\n    locale loc(\"french\"); \n    const time_put<char>& fac = use_facet<time_put<char>>(loc);\n```\n\n这里`french`字符串标识了区域设置，这是 C 运行时库`setlocale`函数使用的语言字符串。第二行获取将数字时间转换为字符串的方面，因此函数模板参数为`time_put<char>`。这个类有一个名为`put`的方法，您可以调用它来执行转换:\n\n```cpp\n    time_t t = time(nullptr); \n    tm *td = gmtime(&t); \n    ostreambuf_iterator<char> it(cout); \n    fac.put(it, cout, ' ', td, 'x', '#'); \n    cout << \"n\";\n```\n\n`time`函数(通过`<ctime>`)返回一个带有当前时间和日期的整数，并使用`gmtime`函数将其转换为`tm`结构。`tm`结构包含年、月、日、小时、分钟和秒的单个成员。`gmtime`函数将地址返回到一个在函数中静态分配的结构中，因此您不必删除它所占用的内存。\n\n刻面将通过作为第一个参数传递的输出迭代器将`tm`结构中的数据格式化为字符串。在这种情况下，输出流迭代器是由`cout`对象构造的，因此 facet 将格式流写入控制台(第二个参数没有使用，但是因为它是一个引用，所以您必须传递一些东西，所以`cout`对象也在那里使用)。第三个参数是分隔符(同样，这是不使用的)。第五个和第六个参数(可选)表示您需要的格式。这些格式字符与 C 运行时库函数`strftime`中使用的格式字符相同，是两个单一字符，而不是 C 函数使用的格式字符串。在本例中，`x`用于获取日期，`#`用作修饰符以获取字符串的长版本。\n\n代码将给出以下输出:\n\n```cpp\n    samedi 28 janvier 2017\n```\n\n注意单词没有大写，也没有标点符号，还要注意顺序:工作日名称，日数，月，然后年。\n\n如果`locale`对象构造器参数变为`german`，那么输出将是:\n\n```cpp\n    Samstag, 28\\. January 2017\n```\n\n项目的顺序与法语相同，但单词大写，使用标点符号。如果使用`turkish`，那么结果是:\n\n```cpp\n    28 Ocak 2017 Cumartesi\n```\n\n在这种情况下，星期几在字符串的末尾。\n\n被共同语言划分的两个国家会给出两个不同的字符串，以下是`american`和`english-uk`的结果:\n\n```cpp\n    Saturday, January 28, 2017\n28 January 2017\n```\n\n这里以时间为例，因为没有流，对`tm`结构使用了插入运算符，这是一种不常见的情况。对于其他类型，有插入操作符将它们放入流中，因此流可以使用区域设置来国际化它显示类型的方式。例如，您可以在`cout`对象中插入一个`double`，该值将被打印到控制台。默认区域设置美国英语使用句点来分隔整数和小数，但在其他文化中使用逗号。\n\n`imbue`函数将改变定位，直到随后调用该方法:\n\n```cpp\n    cout.imbue(locale(\"american\")); \n    cout << 1.1 << \"n\"; \n    cout.imbue(locale(\"french\")); \n    cout << 1.1 << \"n\"; \n    cout.imbue(locale::classic());\n```\n\n这里，流对象被本地化为美国英语，然后浮点数`1.1`被打印在控制台上。接下来本地化改为法语，这次控制台会显示`1,1`。在法语中，小数点是逗号。最后一行通过传递从`static classic`方法返回的区域设置来重置流对象。这将返回所谓的 **C 语言环境**，这是 C 和 C++ 中的默认值，是美式英语。\n\n`static`方法`global`可用于设置每个流对象默认使用的语言环境。当一个对象从一个流类创建时，它调用`locale::global`方法来获取默认区域设置。流克隆这个对象，这样它就有自己的副本，独立于随后通过调用`global`方法设置的任何本地副本。请注意，`cin`和`cout`流对象是在调用`main`函数之前创建的，这些对象将使用默认的 C 语言环境，直到您输入另一个语言环境。但是，需要指出的是，一旦创建了流，`global`方法对流没有影响，`imbue`是更改流所使用的区域设置的唯一方法。\n\n`global`方法还将调用 C `setlocale`函数来更改 C 运行时库函数使用的区域设置。这很重要，因为一些 C++ 函数(例如`to_string`、`stod`，如下文所述)将使用 C 运行时库函数来转换值。然而，C 运行时库对 C++ 标准库一无所知，因此调用 C `setlocale`函数来更改默认区域设置不会影响随后创建的流对象。\n\n值得指出的是，`basic_string`类使用模板参数指示的字符特征类来比较字符串。`string`类使用`char_traits`类及其版本的`compare`方法对两个字符串中的相应字符进行直接比较。这种比较没有考虑比较人物的文化规则。如果你想做一个使用文化规则的比较，你可以通过`collate`方面来做:\n\n```cpp\n    int compare( \n       const string& lhs, const string& rhs, const locale& loc) \n    { \n        const collate<char>& fac = use_facet<collate<char>>(loc); \n        return fac.compare( \n            &lhs[0], &lhs[0] + lhs.size(), &rhs[0], &rhs[0] + rhs.size()); \n    }\n```\n\n# 字符串和数字\n\n标准库包含在 C++ 字符串和数值之间转换的各种函数和类。\n\n# 将字符串转换为数字\n\nC++ 标准库包含名称类似`stod`和`stoi`的函数，用于将 C++ `string`对象转换为数值(`stod`转换为`double`和`stoi`转换为`integer`)。例如:\n\n```cpp\n    double d = stod(\"10.5\"); \n    d *= 4; \n    cout << d << \"n\"; // 42\n```\n\n这将使用值`10.5`初始化浮点变量`d`，然后在计算中使用该值，并将结果打印在控制台上。输入字符串可能包含无法转换的字符。如果是这种情况，那么字符串的解析就在这一点上结束。您可以提供一个指向`size_t`变量的指针，该变量将被初始化为第一个不能转换的字符的位置:\n\n```cpp\n    string str = \"49.5 red balloons\"; \n    size_t idx = 0; \n    double d = stod(str, &idx); \n    d *= 2; \n    string rest = str.substr(idx); \n    cout << d << rest << \"n\"; // 99 red balloons\n```\n\n在前面的代码中，`idx`变量将被初始化为值`4`，表示`5`和`r`之间的空格是第一个不能转换为`double`的字符。\n\n# 将数字转换为字符串\n\n`<string>`库提供了`to_string`函数的各种重载，将整数类型和浮点类型转换成一个`string`对象。此函数不允许您提供任何格式细节，因此对于整数，您无法指示字符串表示的基数(例如十六进制)，对于浮点转换，您无法控制有效数字的数量等选项。`to_string`功能是设施有限的简单功能。更好的选择是使用流类，如下节所述。\n\n# 使用流类\n\n您可以使用`cout`对象(一个`ostream`类的实例)将浮点数和整数打印到控制台，或者打印到一个带有`ofstream`实例的文件。这两个类都将使用成员方法和操纵器将数字转换为字符串，以影响输出字符串的格式。类似地，`cin`对象(T4】类的一个实例)和`ifstream`类可以从格式化的流中读取数据。\n\n操纵器是引用流对象并返回该引用的函数。标准库有各种全局插入操作符，其参数是对流对象和函数指针的引用。适当的插入操作符将以流对象作为参数调用函数指针。这意味着操纵器可以访问并操纵它所插入的流。对于输入流，还有提取操作符，它们有一个函数参数，可以用流对象调用函数。\n\nC++ 流的体系结构意味着在代码中调用的流接口和获取数据的底层基础设施之间有一个缓冲区。C++ 标准库提供了以字符串对象作为缓冲区的流类。对于输出流，在将项插入到流中之后访问字符串，这意味着字符串将包含根据这些插入操作符格式化的项。同样，您可以提供一个带有格式化数据的字符串作为输入流的缓冲区，当您使用提取操作符从流中提取数据时，您实际上是在解析字符串并将部分字符串转换为数字。\n\n此外，流类有一个`locale`对象，流对象将调用该区域的转换方面，将字符序列从一种编码转换为另一种编码。\n\n# 输出浮点数\n\n`<ios>`库有操纵器，可以改变流处理数字的方式。默认情况下，输出流将以十进制格式为范围`0.001`到`100000,`内的数字打印浮点数，对于超出该范围的数字，它将使用带有尾数和指数的科学格式。这种混合格式是`defaultfloat`操纵器的默认行为。如果您总是想使用科学符号，那么您应该将`scientific`操纵器插入到输出流中。\n\n如果您想仅使用十进制格式显示浮点数(即小数点左侧的整数和右侧的小数部分)，则使用`fixed`操纵器修改输出流。通过调用`precision`方法可以改变小数位数:\n\n```cpp\n    double d = 123456789.987654321; \n    cout << d << \"n\"; \n    cout << fixed; \n    cout << d << \"n\"; \n    cout.precision(9); \n    cout << d << \"n\"; \n    cout << scientific; \n    cout << d << \"n\";\n```\n\n前面代码的输出是:\n\n```cpp\n 1.23457e+08\n 123456789.987654\n 123456789.987654328\n 1.234567900e+08\n```\n\n第一行显示科学符号用于大数。第二行显示`fixed`的默认行为，就是给小数加 6 位小数。这在代码中是通过调用`precision`方法给出 9 个小数位来改变的(在流中的`<iomanip>`库中插入`setprecision`操纵器可以达到同样的效果)。最后，格式从调用`precision`方法切换到尾数有 9 个小数位的科学格式。默认情况下，指数由小写的`e`标识。如果您愿意，您可以使用`uppercase`操纵器将其大写(小写使用`nouppercase`)。请注意，小数部分的存储方式意味着在有 9 个小数位的固定格式中，我们看到第九位数字是`8`而不是预期的`1`。\n\n也可以指定正数是否显示`+`符号；`showpos`操纵器将显示符号，但默认的`noshowpos`操纵器不会显示符号。`showpoint`机械手将确保即使浮点数是整数也显示小数点。默认为`noshowpoint`，表示没有小数部分，不显示小数点。\n\n`setw`操纵器(在`<iomanip>`头中定义)可以用于整数和浮点数。实际上，该操纵器定义了流中的下一个(也是唯一的下一个)项目在控制台上打印时将占据的最小空间宽度:\n\n```cpp\n    double d = 12.345678; \n    cout << fixed; \n    cout << setfill('#'); \n    cout << setw(15) << d << \"n\";\n```\n\n为了说明`setw`操纵器的效果，这段代码称为`setfill`操纵器，它表示应该打印一个散列符号(`#`)来代替空格。代码的其余部分表示应该使用固定格式(默认情况下为 6 位小数)打印数字，空格宽 15 个字符。结果是:\n\n```cpp\n    ######12.345678\n```\n\n如果数字为负(或使用`showpos`，则默认情况下，符号与数字在一起；如果使用`internal`操纵器(在`<ios>`中定义)，则符号将在为数字设置的空间中左对齐:\n\n```cpp\n    double d = 12.345678; \n    cout << fixed; \n    cout << showpos << internal; \n    cout << setfill('#'); \n    cout << setw(15) << d << \"n\";\n```\n\n前面代码的结果如下:\n\n```cpp\n    +#####12.345678\n```\n\n请注意，空格右侧的`+`符号由磅符号表示。\n\n`setw`操纵器通常用于在格式化的列中输出数据表:\n\n```cpp\n    vector<pair<string, double>> table \n    { { \"one\",0 },{ \"two\",0 },{ \"three\",0 },{ \"four\",0 } }; \n\n    double d = 0.1; \n    for (pair<string,double>& p : table) \n    { \n        p.second = d / 17.0; \n        d += 0.1; \n    } \n\n    cout << fixed << setprecision(6); \n\n    for (pair<string, double> p : table) \n    { \n        cout << setw(6)  << p.first << setw(10) << p.second << \"n\"; \n    }\n```\n\n这会用一个字符串和一个数字填充一对`vector`。`vector`用字符串值和零初始化，然后在`for`循环中改变浮点数(实际计算与此无关；重点是创建一些有多个小数位的数字)。数据分两列打印，数字以 6 位小数位打印。这意味着，包括前导零和小数点，每个数字将占用 8 个空格。文本列被指定为 6 个字符宽，数字列被指定为 10 个字符宽。默认情况下，当您指定列宽时，输出将右对齐，这意味着每个数字前面都有两个空格，并且根据字符串的长度填充文本。输出如下所示:\n\n```cpp\n one  0.005882\n two  0.011765\n three  0.017647\n four  0.023529\n```\n\n如果您希望一列中的项目左对齐，则可以使用`left`操纵器。这将影响所有列，直到使用`right`操纵器将对正改为向右:\n\n```cpp\n    cout << fixed << setprecision(6) << left;\n```\n\n由此产生的输出将是:\n\n```cpp\n one   0.005882\n two   0.011765\n three 0.017647\n four  0.023529\n```\n\n如果您希望两列有不同的对齐方式，则需要在打印值之前设置对齐方式。例如，要左对齐文本，右对齐数字，请使用以下命令:\n\n```cpp\n    for (pair<string, double> p : table) \n    { \n        cout << setw(6) << left << p.first  \n            << setw(10) << right << p.second << \"n\"; \n    }\n```\n\n前面代码的结果如下:\n\n```cpp\n one     0.005882\n two     0.011765\n three   0.017647\n four    0.023529\n```\n\n# 输出整数\n\n整数也可以使用`setw`和`setfill`方法按列打印。您可以插入操纵器来打印基数为 8 ( `oct`)、基数为 10 ( `dec`)和基数为 16 ( `hex`)的整数。(也可以使用`setbase`操纵器，传递想要使用的基数，但只允许 8、10、16 三个值。)数字可以用指示的基数打印(八进制以`0`为前缀，六进制以`0x`为前缀)，也可以不用`showbase`和`noshowbase`操纵器打印。如果使用`hex`，那么`9`上面的数字就是字母`a`到`f`，默认为小写。如果您希望这些是大写的，那么您可以使用`uppercase`操纵器(小写的带有`nouppercase`)。\n\n# 输出时间和金钱\n\n`<iomanip>`中的`put_time`函数被传递一个用时间和日期以及格式字符串初始化的`tm`结构。该函数返回`_Timeobj`类的一个实例。顾名思义，你并不真的需要创建这个类的变量；相反，应该使用函数将特定格式的时间/日期插入到流中。有一个插入操作符将打印一个`_Timeobj`对象。该函数是这样使用的:\n\n```cpp\n    time_t t = time(nullptr); \n    tm *pt = localtime(&t); \n    cout << put_time(pt, \"time = %X date = %x\") << \"n\";\n```\n\n由此产生的输出是:\n\n```cpp\n    time = 20:08:04 date = 01/02/17\n```\n\n该函数将使用流中的区域设置，因此如果您在流中注入一个区域设置，然后调用`put_time`，时间/日期将使用该区域设置的格式字符串和时间/日期本地化规则进行格式化。格式字符串使用`strftime`的格式标记:\n\n```cpp\n    time_t t = time(nullptr); \n    tm *pt = localtime(&t); \n    cout << put_time(pt, \"month = %B day = %A\") << \"n\"; \n    cout.imbue(locale(\"french\")); \n    cout << put_time(pt, \"month = %B day = %A\") << \"n\";\n```\n\n前面代码的输出是:\n\n```cpp\n month = March day = Thursday\n month = mars day = jeudi\n```\n\n类似地，`put_money`函数返回一个`_Monobj`对象。同样，这只是传递给此函数的参数的容器，不需要使用此类的实例。相反，您应该将此函数插入到输出流中。实际工作发生在获取当前区域设置的 money 方面的插入操作符中，该操作符使用它将数字格式化为适当的小数位数并确定小数点字符；如果使用千位分隔符，在插入到适当的位置之前，使用什么字符。\n\n```cpp\n    Cout << showbase; \n    cout.imbue(locale(\"German\")); \n    cout << \"German\" << \"n\"; \n    cout << put_money(109900, false) << \"n\"; \n    cout << put_money(\"1099\", true) << \"n\"; \n    cout.imbue(locale(\"American\")); \n    cout << \"American\" << \"n\"; \n    cout << put_money(109900, false) << \"n\"; \n    cout << put_money(\"1099\", true) << \"n\";\n```\n\n前面代码的输出是:\n\n```cpp\n German\n 1.099,00 euros\n EUR10,99\n American\n $1,099.00\n USD10.99\n```\n\n您可以在`double`或字符串中提供一个数字作为欧分或美分，`put_money`函数使用适当的小数点(`,`表示德国人，`.`表示美国人)和适当的千位分隔符(`.`表示德国人，`,`表示美国人)将数字格式化为欧元或美元。将`showbase`操纵器插入输出流意味着`put_money`功能将显示货币符号，否则将只显示格式化的数字。`put_money`功能的第二个参数指定是使用货币字符(`false`)还是国际符号(`true`)。\n\n# 使用流将数字转换为字符串\n\n流缓冲类负责从适当的源(文件、控制台等)获取字符和写入字符，并从抽象类`basic_streambuf`派生自`<streambuf>`。这个基类定义了两个虚拟方法，`overflow`和`underflow,`，它们被派生类重写，以向与派生类相关联的设备写入字符和从该设备读取字符。stream buffer 类执行获取或放入项到流中的基本操作，由于 buffer 处理字符，该类是用字符类型和字符特征的参数模板化的。\n\n顾名思义，如果使用`basic_stringbuf`，流缓冲区将是一个字符串，因此读取字符的来源和写入字符的目的地是该字符串。如果使用此类为流对象提供缓冲区，这意味着您可以使用为流编写的插入或提取运算符将格式化数据写入字符串或从字符串中读取格式化数据。`basic_stringbuf`缓冲区是可扩展的，因此当您在流中插入项目时，缓冲区将适当扩展。有`typedef`，这里的缓冲是一个`string` ( `stringbuf`)或者一个`wstring` ( `wstringbuf`)。\n\n例如，假设您已经定义了一个类，并且还定义了一个插入操作符，这样您就可以将其与`cout`对象一起使用，将值打印到控制台:\n\n```cpp\n    struct point \n    { \n        double x = 0.0, y = 0.0; \n        point(){} \n        point(double _x, double _y) : x(_x), y(_y) {} \n    }; \n```\n\n```cpp\n\n    ostream& operator<<(ostream& out, const point& p) \n    { \n        out << \"(\" << p.x << \",\" << p.y << \")\"; \n        return out; \n    }\n```\n\n将它用于`cout`对象很简单——考虑下面这段代码:\n\n```cpp\n    point p(10.0, -5.0); \n    cout << p << \"n\";         // (10,-5)\n```\n\n您可以使用`stringbuf`将格式化的输出定向到字符串，而不是控制台:\n\n```cpp\n    stringbuf buffer;  \n    ostream out(&buffer); \n    out << p; \n    string str = buffer.str(); // contains (10,-5)\n```\n\n由于流对象处理格式化，这意味着您可以插入任何有插入操作符的数据类型，并且您可以使用任何`ostream`格式化方法和任何操纵器。所有这些方法和操纵器的格式化输出将被插入缓冲区中的字符串对象。\n\n另一种选择是使用`<sstream>`中的`basic_ostringstream`类。该类基于用作缓冲区的字符串的字符类型(因此`string`版本为`ostringstream`)。它是从`ostream`类派生出来的，所以你可以在任何使用`ostream`对象的地方使用实例。格式化结果可通过`str`方法访问:\n\n```cpp\n    ostringstream os; \n    os << hex; \n    os << 42; \n    cout << \"The value is: \" << os.str() << \"n\";\n```\n\n该代码获取十六进制(`2a`)中`42`的值；这是通过在流中插入`hex`操纵器，然后插入整数来实现的。通过调用`str`方法获得格式化字符串。\n\n# 使用流从字符串中读取数字\n\n`cin`对象是`istream`类的一个实例(在`<istream>`库中)，可以从控制台输入字符，并将其转换为您指定的数字形式。`<ifstream>`库中的`ifstream`类还允许您从文件中输入字符并将其转换为数字形式。与输出流一样，可以将流类与字符串缓冲区一起使用，这样就可以从字符串对象转换为数值。\n\n`<sstream>`库中的`basic_istringstream`类是从`basic_istream`类派生的，因此您可以创建流对象并从这些对象中提取项目(数字和字符串)。该类在一个字符串对象上提供这个流接口(关键字`typedef`的`istringstream`基于一个`string`，`wistringstream`基于一个`wstring`)。当您构造这个类的对象时，您用一个包含数字的`string`初始化对象，然后您使用`>>`操作符提取基本内置类型的对象，就像您使用`cin`从控制台提取那些项目一样。\n\n需要重申的是，提取操作符将空格视为流中各项之间的分隔符，因此它们将忽略所有前导空格，读取下一个空格之前的非空格字符，并尝试将该子字符串转换为适当的类型，如下所示:\n\n```cpp\n    istringstream ss(\"-1.0e-6\"); \n    double d; \n    ss >> d;\n```\n\n这将使用`-1e-6`的值初始化`d`变量。如同`cin,`一样，你必须知道流中项目的格式；因此，如果您试图提取一个整数，而不是从前面示例中的字符串中提取一个`double`，那么当它到达小数点时，对象将停止提取字符。如果某些字符串没有转换，您可以将其余的提取到字符串对象中:\n\n```cpp\n    istringstream ss(\"-1.0e-6\"); \n    int i; \n    ss >> i; \n    string str; \n    ss >> str; \n    cout << \"extracted \" << i << \" remainder \" << str << \"n\";\n```\n\n这将在控制台上打印以下内容:\n\n```cpp\n    extracted -1 remainder .0e-6\n```\n\n如果字符串中有多个数字，可以通过多次调用`>>`运算符来提取这些数字。该流还支持一些操纵器。例如，如果字符串中的数字是`hex`格式，您可以使用`hex`操纵器通知流这种情况，如下所示:\n\n```cpp\n    istringstream ss(\"0xff\"); \n    int i; \n    ss >> hex; \n    ss >> i;\n```\n\n这表示字符串中的数字是十六进制格式，变量`i`将被初始化为值 255。如果字符串包含非数值，那么 stream 对象仍会尝试将字符串转换为适当的格式。在下面的代码片段中，您可以通过调用`fail`函数来测试这样的提取是否失败:\n\n```cpp\n    istringstream ss(\"Paul was born in 1942\"); \n    int year; \n    ss >> year; \n    if (ss.fail()) cout << \"failed to read number\" << \"n\";\n```\n\n如果知道字符串包含文本，可以将其提取到字符串对象中，但请记住，空白字符被视为分隔符:\n\n```cpp\n    istringstream ss(\"Paul was born in 1942\"); \n    string str; \n    ss >> str >> str >> str >> str; \n    int year; \n    ss >> year;\n```\n\n这里，数字前有四个字，所以代码读一个`string`四次。如果不知道数字在字符串中的位置，但知道字符串中有一个数字，可以移动内部缓冲区指针，直到它指向一个数字:\n\n```cpp\n    istringstream ss(\"Paul was born in 1942\"); \n    string str;    \n    while (ss.eof() && !(isdigit(ss.peek()))) ss.get(); \n    int year; \n    ss >> year; \n    if (!ss.fail()) cout << \"the year was \" << year << \"n\";\n```\n\n`peek`方法返回当前位置的字符，但不移动缓冲区指针。该代码检查该字符是否为数字，如果不是，则通过调用`get`方法移动内部缓冲区指针。(这段代码测试`eof`方法，以确保在缓冲区结束后没有读取字符的尝试。)如果您知道数字从哪里开始，那么您可以调用`seekg`方法将内部缓冲区指针移动到指定位置。\n\n`<istream>`库有一个名为`ws`的操纵器，可以从流中移除空白。回想一下，我们之前说过，没有从字符串中删除空白的函数。这是真的，因为`ws`操纵器从*流*中移除空白，而不是从*字符串*中移除空白，但是由于您可以使用字符串作为流的缓冲区，这意味着您可以使用此函数间接从字符串中移除空白:\n\n```cpp\n    string str = \"  hello  \"; \n    cout << \"|\" << str1 << \"|n\"; // |  hello  | \n    istringstream ss(str); \n    ss >> ws; \n    string str1; \n    ss >> str1; \n    ut << \"|\" << str1 << \"|n\";   // |hello|\n```\n\n`ws`函数本质上遍历输入流中的项目，当字符不是空白时返回。如果流是文件或控制台流，则`ws`功能将从这些流中读入字符；在这种情况下，缓冲区由已经分配的字符串提供，因此它跳过了字符串开头的空白。请注意，流类将后续空格视为流中值之间的分隔符，因此在本例中，流将从缓冲区读入字符，直到出现空格，并且本质上将*向左-* *和向右修剪*字符串。然而，这不一定是你想要的。如果字符串中有几个单词用空格填充，那么这段代码将只提供第一个单词。\n\n`<iomanip>`库中的`get_money`和`get_time`操纵器允许您使用区域设置的金钱和时间方面从字符串中提取金钱和时间:\n\n```cpp\n    tm indpday = {}; \n    string str = \"4/7/17\"; \n    istringstream ss(str); \n    ss.imbue(locale(\"french\")); \n    ss >> get_time(&indpday, \"%x\"); \n    if (!ss.fail())  \n    { \n       cout.imbue(locale(\"american\")); \n       cout << put_time(&indpday, \"%x\") << \"n\";  \n    }\n```\n\n在前面的代码中，首先使用法语格式的日期(日/月/年)初始化流，然后使用区域设置的标准日期表示通过`get_time`提取日期。日期被解析成`tm`结构，然后使用`put_time`以美国地区的标准日期表示打印出来。结果是:\n\n```cpp\n    7/4/2017\n```\n\n# 使用正则表达式\n\n正则表达式是一种文本模式，正则表达式解析器可以使用它在字符串中搜索与该模式匹配的文本，如果需要，可以用其他文本替换匹配的项。\n\n# 定义正则表达式\n\n一个**正则表达式** ( **正则表达式**)由定义模式的字符组成。表达式包含对解析器有意义的特殊符号，如果您想在表达式的搜索模式中使用这些符号，那么您可以用反斜杠(`\\`)来转义它们。您的代码通常会将表达式作为`string`对象传递给`regex`类的实例作为构造函数参数。该对象然后被传递给`<regex>`中的函数，该函数将使用表达式来解析匹配模式的序列的文本。\n\n下表总结了*和`regex`类可以匹配的一些*模式。\n\n| **图案** | **解释** | **例** |\n| 文字 | 匹配精确的字符 | `li`匹配`flip` `lip` `plier` |\n| [组] | 匹配组中的单个字符 | `[at]`匹配`cat`、`cat`、`top`、`pear` |\n| [^group] | 匹配不在组中的单个字符 | `[^at]`匹配 **c** at，t **o** p，到 **p** ， **p** 耳朵，p **e** ar，豌豆 **r** |\n| [第一个-最后一个] | 匹配范围`first`到`last`中的任何字符 | `[0-9]`匹配数字 **1** 02，1 **0** 2，10 **2** |\n| {n} | 该元素被精确匹配 n 次 | **91{2}** 匹配 **911** |\n| {n，} | 元素被匹配 n 次或更多次 | `wel{1,}`匹配`well`和**欢迎**到来 |\n| {n，m} | 该元素匹配 n 到 m 次 | `9{2,4}`匹配`99`、`999`、`9999`、`9999` 9 但不匹配 9 |\n| 。 | 通配符，除`n`外的任何字符 | `a.e`匹配`ate`和`are` |\n| * | 元素被匹配零次或多次 | `d*.d`匹配`.1`、`0.1`、`10.1`但不匹配 10 |\n| + | 元素被匹配一次或多次 | `d*.d`匹配`0.1`、`10.1`但不是 10 或. 1 |\n| ？ | 元素被匹配零次或一次 | `tr?ap`匹配`trap`和`tap` |\n| &#124; | 匹配由&#124; | `th(e&#124;is&#124;at)`匹配`the`、`this`、`that` |\n| [[:class:]] | 匹配字符类 | `[[:upper:]]`匹配大写字符:`I` am `R` ichard |\n| n | 匹配换行符 |  |\n| s | 匹配任何单个空格 |  |\n| d | 匹配任何一个数字 | `d`是`[0-9]` |\n| w | 匹配可以在单词中的字符(大写和小写字符) |  |\n| b | 匹配字母数字字符和非字母数字字符之间的边界 | `d{2}b`匹配 9 `99`和 99 `99 bd{2}`匹配`99` 9 和`99` 99 |\n| $ | 行尾 | `s$`匹配一行末尾的一个空格 |\n| ^ | 行首 | `^d`如果一行以数字开头，则匹配 |\n\n您可以使用正则表达式来定义要匹配的模式——Visual c++ 编辑器允许您在搜索对话框中这样做(这是开发表达式的一个很好的测试平台)。\n\n定义要匹配的模式要比定义要匹配的模式*而不是*容易得多。例如，表达式`w+b<w+>`将匹配字符串`\"vector<int>\"`，因为它有一个或多个单词字符后跟一个非单词字符(`<`)，后跟一个或多个单词字符后跟`>`。该模式与字符串`\"#include <regex>\"`不匹配，因为`include`后面有一个空格，`b`表示字母数字字符和非字母数字字符之间有边界。\n\n表格中的`th(e|is|at)`示例显示，当您想要提供替代方案时，可以使用括号来对模式进行分组。然而，括号还有另一个用途——它们允许您捕获组。因此，如果您想要执行替换操作，您可以搜索一个模式作为一个组，然后稍后将该组称为一个命名的子组(例如，搜索`(Joe)`以便您可以将`Joe`替换为`Tom`)。您也可以引用表达式中由括号指定的子表达式(称为反向引用):\n\n```cpp\n    ([A-Za-z]+) +1\n```\n\n这个表达式表示:*搜索一个或多个字符在 A 到 Z 和 A 到 Z 范围内的单词；这个单词叫做 1，所以找到它出现两次的地方，中间留一个空格*。\n\n# 标准库类\n\n要执行匹配或替换，必须创建一个正则表达式对象。这是类`basic_regex`的一个对象，它有字符类型的模板参数和一个正则表达式特征类。这个类有两个`typedef`:`char`的`regex`和【宽字符】的`wregex`，它们具有`regex_traits`和`wregex_traits`类所描述的特征。\n\ntraits 类决定了 regex 类如何解析表达式。例如，回想一下以前的文本，您可以使用`w`表示单词，`d`表示数字，`s`表示空格。`[[::]]`语法允许您为字符类使用更具描述性的名称:`alnum`、`digit`、`lower`等等。由于这些是依赖于字符集的文本序列，因此 traits 类将有适当的代码来测试表达式是否使用了受支持的字符类。\n\n适当的正则表达式类将解析表达式，以使`<regex>`库中的函数能够使用该表达式来识别某些文本中的模式:\n\n```cpp\n    regex rx(\"([A-Za-z]+) +1\");\n```\n\n这将使用反向引用来搜索重复的单词。请注意，正则表达式使用`1`作为反向引用，但是在字符串中，反斜杠必须被转义(`\\`)。如果你使用像`s`和`d`这样的角色类，那么你需要进行大量的逃跑。相反，您可以使用原始字符串(`R\"()\"`)，但请记住，引号内的第一组括号是原始字符串语法的一部分，并不构成正则表达式组:\n\n```cpp\n    regex rx(R\"(([A-Za-z]+) +1)\");\n```\n\n哪个更易读完全取决于你；两者都在双引号中引入了额外的字符，这可能会混淆快速浏览正则表达式匹配的内容。\n\n请记住，正则表达式本身本质上是一个程序，因此`regex`解析器将确定该表达式是否有效，如果它不是对象，构造函数将抛出类型为`regex_error`的异常。异常处理将在下一章中解释，但重要的是要指出，如果异常没有被捕获，它将导致应用在运行时中止。异常的`what`方法将返回错误的基本描述，`code`方法将返回`regex_constants`命名空间中的`error_type`枚举中的一个常量。没有指示表达式中错误发生的位置。您应该在外部工具中彻底测试您的表达式(例如 Visual C++ 搜索)。\n\n构造函数可以用一个字符串(C 或 C++)或一对迭代器来调用字符串(或其他容器)中的一系列字符，或者您可以传递一个初始化列表，其中列表中的每个项目都是一个字符。regex 语言有各种不同的风格；`basic_regex`类的默认值是 **ECMAScript** 。如果您想要不同的语言(基本 POSIX、扩展 POSIX、awk、grep 或 egrep)，您可以将在`regex_constants`命名空间中的`syntax_option_type`枚举中定义的常量之一(副本也可以作为在`basic_regex`类中定义的常量)作为构造函数参数传递。\n\n您只能指定一种语言风格，但可以将其与其他一些`syntax_option_type`常量结合使用:`icase`指定不区分大小写，`collate`在匹配中使用区域设置，`nosubs`表示您不想捕获组，`optimize`优化匹配。\n\n该类使用方法`getloc`获取解析器使用的区域设置，`imbue`重置区域设置。如果你`imbue`一个地区，那么你将不能使用`regex`对象做任何匹配，直到你用`assign`方法重置它。这意味着有两种方法可以使用`regex`对象。如果要使用当前区域设置，则将正则表达式传递给构造函数:如果要使用不同的区域设置，用默认构造函数创建一个空的`regex`对象，然后用区域设置调用`imbue`，用`assign`方法传递正则表达式。解析完正则表达式后，您可以调用`mark_count`方法来获取表达式中的捕获组数量(假设您没有使用`nosubs`)。\n\n# 匹配表达式\n\n一旦你构造了一个`regex`对象，你可以把它传递给`<regex>`库中的方法来搜索字符串中的模式。`regex_match`函数通过字符串(C 或 C++)或迭代器传递给容器中的一系列字符和一个构造的`regex`对象。最简单的形式是，只有当表达式与搜索字符串完全匹配时，函数才会返回`true`:\n\n```cpp\n    regex rx(\"[at]\"); // search for either a or t \n    cout << boolalpha; \n    cout << regex_match(\"a\", rx) << \"n\";  // true \n    cout << regex_match(\"a\", rx) << \"n\";  // true \n    cout << regex_match(\"at\", rx) << \"n\"; // false\n```\n\n在前面的代码中，搜索表达式是针对给定范围内的单个字符(`a`或`t`，因此对`regex_match`的前两次调用返回`true`，因为搜索到的字符串是一个字符。最后一次调用返回`false`，因为匹配和搜索到的字符串不一样。如果去掉正则表达式中的`[]`，那么第三次调用将返回`true`，因为您正在寻找精确的字符串`at`。如果正则表达式是`[at]+`以便您寻找一个或多个字符`a`和`t`，那么这三个调用都会返回`true`。您可以通过传递`match_flag_type`枚举中的一个或多个常量来改变匹配的确定方式。\n\n如果您将对`match_results`对象的引用传递给该函数，那么在搜索之后，该对象将包含关于匹配的位置和字符串的信息。`match_results`对象是一个包含`sub_match`对象的容器。如果函数成功，则意味着整个搜索字符串与表达式匹配，在这种情况下，返回的第一个`sub_match`项将是整个搜索字符串。如果表达式有子组(用括号标识的模式)，那么这些子组将是`match_results`对象中的附加`sub_match`对象。\n\n```cpp\n    string str(\"trumpet\"); \n    regex rx(\"(trump)(.*)\"); \n    match_results<string::const_iterator> sm; \n    if (regex_match(str, sm, rx)) \n    { \n        cout << \"the matches were: \"; \n        for (unsigned i = 0; i < sm.size(); ++ i)  \n        { \n            cout << \"[\" << sm[i] << \",\" << sm.position(i) << \"] \"; \n        } \n        cout << \"n\"; \n    } // the matches were: [trumpet,0] [trump,0] [et,5]\n```\n\n这里，表达式是文字`trump`后跟任意数量的字符。整个字符串与这个表达式匹配，并且有两个子组:文字字符串`trump`和去除`trump`后剩下的任何东西。\n\n`match_results`类和`sub_match`类都是基于用于指示匹配项的迭代器类型的模板。模板参数分别为`const char*`和`const wchar_t*`的有`typedef`调用的`cmatch`和`wcmatch`，参数分别为`string`和`wstring`对象中使用的迭代器的有`smatch`和`wsmatch`(类似的还有子匹配类:`csub_match`、`wcsub_match`、`ssub_match`和`wssub_match`)。\n\n`regex_match`函数可能非常严格，因为它寻找模式和搜索字符串之间的精确匹配。`regex_search`函数更灵活，因为如果搜索字符串中有一个子字符串与表达式匹配，它将返回`true`。请注意，即使搜索字符串中有多个匹配项，`regex_search`功能也只会找到第一个。如果您想解析字符串，您必须多次调用该函数，直到它指示不再有匹配项。这就是迭代器访问搜索字符串的重载变得有用的地方:\n\n```cpp\n    regex rx(\"bd{2}b\"); \n    smatch mr; \n    string str = \"1 4 10 42 100 999\"; \n    string::const_iterator cit = str.begin(); \n    while (regex_search(cit, str.cend(), mr, rx)) \n    { \n        cout << mr[0] << \"n\"; \n        cit += mr.position() + mr.length(); \n    }\n```\n\n这里，表达式将匹配一个由空白包围的 2 位数(`d{2}`)(两个`b`模式意味着前后的边界)。这个循环从一个指向字符串开头的迭代器开始，当找到匹配项时，这个迭代器会增加到那个位置，然后增加匹配项的长度。`regex_iterator`对象，进一步解释，包装这种行为。\n\n类赋予迭代器对所包含的对象的访问权，这样你就可以使用远程的 T2。最初，容器似乎以一种奇怪的方式工作，因为它知道`sub_match`对象在搜索到的字符串中的位置(通过`position`方法，该方法获取子匹配对象的索引)，但是`sub_match`对象似乎只知道它所引用的字符串。然而，仔细观察`sub_match`类，它显示它是从`pair`派生的，其中两个参数都是字符串迭代器。这意味着`sub_match`对象具有指定子字符串的原始字符串中的范围的迭代器。`match_result`对象知道原始字符串的开始，可以使用`sub_match.first`迭代器来确定子字符串开始的字符位置。\n\n`match_result`对象有一个`[]`运算符(和`str`方法)，返回指定组的子串；这将是一个使用原始字符串中字符范围的迭代器构造的字符串。`prefix`方法返回匹配前的字符串，`suffix`方法返回匹配后的字符串。所以，在之前的代码中，第一个匹配将是`10`，前缀将是`1 4`，后缀将是`42 100 999`。相比之下，如果访问`sub_match`对象本身，它只知道它的长度和字符串，这是通过调用`str`方法获得的。\n\n`match_result`对象也可以通过`format`方法返回结果。这采用一个格式字符串，其中匹配的组通过由`$`符号(`$1`、`$2`等)标识的编号占位符来标识。输出可以是流，也可以作为字符串从方法返回:\n\n```cpp\n    string str(\"trumpet\"); \n    regex rx(\"(trump)(.*)\"); \n    match_results<string::const_iterator> sm; \n    if (regex_match(str, sm, rx)) \n    { \n        string fmt = \"Results: [$1] [$2]\"; \n        cout << sm.format(fmt) << \"n\"; \n    } // Results: [trump] [et]\n```\n\n使用`regex_match`或`regex_search,`可以使用括号来标识子组。如果模式匹配，那么您可以使用通过引用该函数传递的适当的`match_results`对象来获得这些子组。如前所示，`match_results`对象是`sub_match`对象的容器。子匹配可以与`<`、`!=`、`==`、`<=`、`>`和`>=`运算符进行比较，这些运算符比较迭代器指向的项(即子字符串)。此外，`sub_match`对象可以插入到流中。\n\n# 使用迭代器\n\n该库还为正则表达式提供了一个迭代器类，这为解析字符串提供了一种不同的方法。因为类将涉及字符串的比较，所以它是以元素类型和特征为模板的。该类将需要遍历字符串，因此第一个模板参数是字符串迭代器类型，元素和特征类型可以从中推导出来。`regex_iterator`类是一个前向迭代器，所以它有一个`++ `操作符，并且它提供了一个`*`操作符来访问一个`match_result`对象。在前面的代码中，您看到一个`match_result`对象被传递给了`regex_match`和`regex_search`函数，这些函数使用它来包含它们的结果。这就提出了什么代码填充通过`regex_iterator`访问的`match_result`对象的问题。答案在于迭代器的`++ `运算符:\n\n```cpp\n    string str = \"the cat sat on the mat in the bathroom\"; \n    regex rx(\"(b(.at)([^ ]*)\"); \n    regex_iterator<string::iterator> next(str.begin(), str.end(), rx); \n    regex_iterator<string::iterator> end; \n\n    for (; next != end; ++ next) \n    { \n        cout << next->position() << \" \" << next->str() << \", \"; \n    } \n    cout << \"n\"; \n    // 4 cat, 8 sat, 19 mat, 30 bathroom\n```\n\n在该代码中，在字符串中搜索第二个和第三个字母为`at`的单词。`b`表示模式必须在单词的开头(`.`表示单词可以以任何字母开头)。这三个字符周围有一个捕获组，除空格外，还有一个或多个字符的第二个捕获组。\n\n迭代器对象`next`由要搜索的字符串和`regex`对象的迭代器构成。`++ `操作员基本上调用`regex_search`功能，同时保持该地点的位置以执行下一次搜索。如果搜索没有找到模式，那么操作符返回序列的**结尾**迭代器，它是由默认构造函数(在这个代码中是`end`对象)创建的迭代器。这段代码打印出了完全匹配，因为我们使用了`str`方法的默认参数(`0`)。如果您想要匹配实际的子字符串，使用`str(1)`，结果将是:\n\n```cpp\n    4 cat, 8 sat, 19 mat, 30 bat\n```\n\n由于`*`(和`->`)操作符赋予了对`match_result`对象的访问权，所以您也可以访问`prefix`方法来获取匹配之前的字符串，`suffix`方法将返回匹配之后的字符串。\n\n`regex_iterator`类允许您迭代匹配的子字符串，而`regex_token_iterator`更进一步，它还允许您访问所有子匹配。在使用上，本类除了构造上与`regex_iterator,`相同。`regex_token_iterator`构造器有一个参数来指示您希望通过`*`操作器访问哪个子匹配。值`-1`表示您想要前缀，值`0`表示您想要整个匹配，值`1`或以上表示您想要编号的子匹配。如果你愿意，你可以传递一个带有你想要的子匹配类型的`int vector`或 C 数组:\n\n```cpp\n    using iter = regex_token_iterator<string::iterator>; \n    string str = \"the cat sat on the mat in the bathroom\"; \n    regex rx(\"b(.at)([^ ]*)\");  \n    iter next, end; \n\n    // get the text between the matches \n    next = iter(str.begin(), str.end(), rx, -1); \n    for (; next != end; ++ next) cout << next->str() << \", \"; \n    cout << \"n\"; \n    // the ,  ,  on the ,  in the , \n\n    // get the complete match \n    next = iter(str.begin(), str.end(), rx, 0); \n    for (; next != end; ++ next) cout << next->str() << \", \"; \n    cout << \"n\"; \n    // cat, sat, mat, bathroom, \n\n    // get the sub match 1 \n    next = iter(str.begin(), str.end(), rx, 1); \n    for (; next != end; ++ next) cout << next->str() << \", \"; \n    cout << \"n\"; \n    // cat, sat, mat, bat, \n\n    // get the sub match 2 \n    next = iter(str.begin(), str.end(), rx, 2); \n    for (; next != end; ++ next) cout << next->str() << \", \"; \n    cout << \"n\"; \n    // , , , hroom,\n```\n\n# 替换字符串\n\n`regex_replace`方法与其他方法类似，它采用一个字符串(一个 C 字符串或 C++ `string`对象，或一系列字符的迭代器)、`regex`对象和可选标志。此外，该函数有一个格式字符串，并返回一个`string`。格式字符串本质上是从匹配结果传递到正则表达式的每个`results_match`对象的`format`方法。然后，该格式化字符串被用作相应匹配子字符串的替换。如果没有匹配项，则返回搜索到的字符串的副本。\n\n```cpp\n    string str = \"use the list<int> class in the example\"; \n    regex rx(\"b(list)(<w*> )\"); \n    string result = regex_replace(str, rx, \"vector$2\"); \n    cout << result << \"n\"; // use the vector<int> class in the example\n```\n\n在前面的代码中，我们说整个匹配的字符串(应该是`list<`后面跟着一些文本后面跟着`>`和一个空格)应该替换为`vector,`后面跟着第二子匹配(`<`后面跟着一些文本后面跟着`>`和一个空格)。结果是`list<int>`将被`vector<int>`取代。\n\n# 使用字符串\n\n该示例将以文本文件形式读入电子邮件并进行处理。互联网邮件格式的电子邮件分为两部分:邮件头和邮件正文。这是简单的处理，因此不会尝试处理 MIME 电子邮件正文格式(尽管这段代码可以作为起点)。邮件正文将在第一个空行后开始，互联网标准规定该行长度不应超过 78 个字符。如果长度较长，不得超过 998 个字符。这意味着换行符(回车符、换行符对)用于维护这个规则，并且一个段落的结尾用一个空行来表示。\n\n标题更复杂。最简单的形式是，一个标题在一行上，形式为`name:value`。标头名称与标头值之间用冒号隔开。标题可以使用一种称为折叠空白的格式拆分为多行，其中拆分标题的换行符位于空白(空格、制表符等)之前。这意味着以空白开始的一行是前一行标题的延续。标题通常包含由分号分隔的`name=value`对，因此能够分隔这些子项非常有用。有时这些子项没有值，即会有一个以分号结尾的子项。\n\n该示例将一封电子邮件作为一系列字符串，并使用这些规则创建一个对象，该对象包含一组标题和一个包含正文的字符串。\n\n# 创建项目\n\n为项目创建一个文件夹，并创建一个名为`email_parser.cpp`的 C++ 文件。由于此应用将读取文件并处理字符串，因此添加适当库的 includes 并添加代码以从命令行获取文件名:\n\n```cpp\n    #include <iostream> \n    #include <fstream> \n    #include <string> \n\n    using namespace std; \n\n    void usage() \n    { \n        cout << \"usage: email_parser file\" << \"n\"; \n        cout << \"where file is the path to a file\" << \"n\"; \n    } \n\n    int main(int argc, char *argv[]) \n    { \n        if (argc <= 1) \n        { \n            usage(); \n            return 1; \n        } \n\n        ifstream stm; \n        stm.open(argv[1], ios_base::in); \n        if (!stm.is_open()) \n        { \n            usage(); \n            cout << \"cannot open \" << argv[1] << \"n\"; \n            return 1; \n        } \n\n        return 0; \n    }\n```\n\n标题将有一个名称和一个正文。正文可以是单个字符串，也可以是一个或多个子项。创建一个类来表示头的主体，并且暂时把它当作一行。在`usage`函数上方添加以下类:\n\n```cpp\n    class header_body \n    { \n        string body; \n    public: \n        header_body() = default; \n        header_body(const string& b) : body(b) {} \n        string get_body() const { return body; } \n    };\n```\n\n这只是将类包裹在一个`string`周围；稍后我们将添加代码来分离`body`数据成员中的子项。现在创建一个类来表示电子邮件。在`header_body`类后添加以下代码:\n\n```cpp\n    class email \n    { \n        using iter = vector<pair<string, header_body>>::iterator; \n        vector<pair<string, header_body>> headers; \n        string body; \n\n    public: \n        email() : body(\"\") {} \n\n        // accessors \n        string get_body() const { return body; } \n        string get_headers() const; \n        iter begin() { return headers.begin(); } \n        iter end() { return headers.end(); } \n\n        // two stage construction \n        void parse(istream& fin); \n    private: \n        void process_headers(const vector<string>& lines); \n    };\n```\n\n`headers`数据成员将标题保存为名称/值对。这些项目存储在一个`vector`而不是一个`map`中，因为当一封电子邮件从一个邮件服务器传递到另一个邮件服务器时，邮件中已经存在的每个服务器可能会添加邮件头，所以邮件头是重复的。我们可以使用`multimap`，但是我们将失去标题的顺序，因为`multimap`将按照有助于搜索项目的顺序存储项目。\n\nA `vector`按照项目插入容器的顺序保存项目，由于我们将连续解析电子邮件，这意味着`headers`数据成员的标题项目与电子邮件中的顺序相同。添加适当的包含，以便您可以使用`vector`类。\n\n正文和标题作为单个字符串有访问器。此外，还有从`headers`数据成员返回迭代器的访问器，这样外部代码就可以遍历`headers`数据成员(这个类的完整实现将有允许您按名称搜索标题的访问器，但是对于本例来说，只允许迭代)。\n\n该类支持两阶段构造，其中大部分工作是通过将输入流传递给`parse`方法来完成的。`parse`方法在电子邮件中读取为一个`vector`对象中的一系列行，并调用私有函数`process_headers`，将这些行解释为标题。\n\n`get_headers`方法很简单:它只是遍历标题，并在格式为`name: value`的每一行放置一个标题。添加内联函数:\n\n```cpp\n    string get_headers() const \n    { \n        string all = \"\"; \n        for (auto a : headers) \n        { \n            all += a.first + \": \" + a.second.get_body(); \n            all += \"n\"; \n        } \n        return all; \n    }\n```\n\n接下来，您需要从文件中读入电子邮件，并提取邮件正文和邮件头。`main`函数已经有了打开文件的代码，所以创建一个`email`对象，并将文件的`ifstream`对象传递给`parse`方法。现在使用访问器打印出解析后的电子邮件。在`main`功能的末尾添加以下内容:\n\n```cpp\n email eml; eml.parse(stm); cout << eml.get_headers(); cout << \"n\"; cout << eml.get_body() << \"n\"; \n\n        return 0; \n    }\n```\n\n在`email`类声明之后，添加`parse`函数的定义:\n\n```cpp\n    void email::parse(istream& fin) \n    { \n        string line; \n        vector<string> headerLines; \n        while (getline(fin, line)) \n        { \n            if (line.empty()) \n            { \n                // end of headers \n                break; \n            } \n            headerLines.push_back(line); \n        } \n\n        process_headers(headerLines); \n\n        while (getline(fin, line)) \n        { \n            if (line.empty()) body.append(\"n\"); \n            else body.append(line); \n        } \n    }\n```\n\n这个方法很简单:它反复调用`<string>`库中的`getline`函数来读取一个`string`，直到检测到一个换行符。在方法的前半部分，字符串存储在`vector`中，然后传递给`process_headers`方法。如果读入的字符串是空的，这意味着一个空行已经被读入——在这种情况下，所有的标题都已经被读入。在方法的后半部分，读入电子邮件的正文。\n\n`getline`函数将用于格式化电子邮件的换行符剥离为 78 个字符的行长度，因此循环只是将这些行作为一个字符串追加。如果一个空行被读入，它表示一个段落的结束，因此一个换行符被添加到正文字符串中。\n\n在`parse`方法后，增加`process_headers`方法:\n\n```cpp\n    void email::process_headers(const vector<string>& lines) \n    { \n        string header = \"\"; \n        string body = \"\"; \n        for (string line : lines) \n        { \n            if (isspace(line[0])) body.append(line); \n            else \n            { \n                if (!header.empty()) \n                { \n                    headers.push_back(make_pair(header, body)); \n                    header.clear(); \n                    body.clear(); \n                } \n\n                size_t pos = line.find(':'); \n                header = line.substr(0, pos); \n                pos++ ; \n                while (isspace(line[pos])) pos++ ; \n                body = line.substr(pos); \n            } \n        } \n\n        if (!header.empty()) \n        { \n            headers.push_back(make_pair(header, body)); \n        } \n    }\n```\n\n这段代码遍历集合中的每一行，当它有一个完整的头时，它将字符串拆分为冒号上的名称/正文对。在循环中，第一行测试第一个字符是否是空白；如果没有，则检查`header`变量是否有值；如果是，在清除`header`和`body`变量之前，名称/主体对存储在类`headers`数据成员中。\n\n下面的代码对从集合中读取的行进行操作。这段代码假设这是标题行的开始，因此在字符串中搜索冒号并在这一点上进行拆分。标题的名称在冒号之前，标题的正文(去掉前导空白)在冒号之后。由于我们不知道标题正文是否会折叠到下一行，因此不存储名称/正文；取而代之的是，允许`while`循环再重复一次，这样就可以测试下一行的第一个字符，看它是否是空白，如果是，就将其追加到正文中。保持名称/主体对直到`while`循环的下一次迭代的动作意味着最后一行不会存储在循环中，因此在方法的末尾有一个测试来查看`header`变量是否为空，如果不是，则存储名称/主体对。\n\n现在可以编译代码(记得使用`/EHsc`开关)测试没有错别字。要测试代码，您应该将来自电子邮件客户端的电子邮件保存为文件，然后使用该文件的路径运行`email_parser`应用。以下是互联网消息格式 RFC 5322 中给出的示例电子邮件之一，您可以将其放入文本文件中以测试代码:\n\n```cpp\n    Received: from x.y.test\n by example.net\n via TCP\n with ESMTP\n id ABC12345\n for <mary@example.net>;  21 Nov 1997 10:05:43 -0600\nReceived: from node.example by x.y.test; 21 Nov 1997 10:01:22 -0600\nFrom: John Doe <jdoe@node.example>\nTo: Mary Smith <mary@example.net>\nSubject: Saying Hello\nDate: Fri, 21 Nov 1997 09:55:06 -0600\nMessage-ID: <1234@local.node.example>\n\nThis is a message just to say hello.\nSo, \"Hello\".\n```\n\n您可以用一封电子邮件来测试应用，以表明解析已经考虑了标题格式，包括折叠空白。\n\n# 正在处理标题子项\n\n下一个操作是将标题正文处理为子项。为此，将以下突出显示的声明添加到`header_body`类的`public`部分:\n\n```cpp\n    public: \n        header_body() = default; \n        header_body(const string& b) : body(b) {} \n        string get_body() const { return body; } \n        vector<pair<string, string>> subitems(); \n    };\n```\n\n每个子项都是一个名称/值对，由于子项的顺序可能很重要，因此子项存储在`vector`中。更改`main`功能，取消对`get_headers`的调用，改为单独打印每个标题:\n\n```cpp\n    email eml; \n    eml.parse(stm); \n    for (auto header : eml) { cout << header.first << \" : \"; vector<pair<string, string>> subItems = header.second.subitems(); if (subItems.size() == 0) { cout << header.second.get_body() << \"n\"; } else { cout << \"n\"; for (auto sub : subItems) { cout << \"   \" << sub.first; if (!sub.second.empty()) \n                cout << \" = \" << sub.second;         \n                cout << \"n\"; } } } \n    cout << \"n\"; \n    cout << eml.get_body() << endl;\n```\n\n由于`email`类实现了`begin`和`end`方法，这意味着远程`for`循环将调用这些方法来访问`email::headers`数据成员上的迭代器。每个迭代器将访问一个`pair<string,header_body>`对象，所以在这段代码中，我们首先打印出标题名，然后访问`header_body`对象上的子项。如果没有子项，表头还是会有一些文字，但是不会拆分成子项，所以我们调用`get_body`方法，让字符串打印出来。如果有子项，则打印出来。有些物品会有身体，有些不会。如果项目有正文，则该子项以`name = value`的形式打印。\n\n最后一个操作是解析标题正文，将它们拆分为子项。在`header_body`类下面，添加方法的定义:\n\n```cpp\n    vector<pair<string, string>> header_body::subitems() \n    { \n        vector<pair<string, string>> subitems; \n        if (body.find(';') == body.npos) return subitems; \n\n        return subitems; \n    }\n```\n\n由于子项是用分号分隔的，因此有一个简单的测试来寻找`body`字符串上的分号。如果没有分号，则返回空的`vector`。\n\n现在代码必须重复解析字符串，提取子项。有几个案例需要解决。大多数子项都是`name=value;,`形式，因此必须提取该子项，并在等于字符处拆分，并丢弃分号。\n\n有些子项没有值，并且是`name;`形式，在这种情况下，分号将被丢弃，并且子项值以空字符串存储。最后，标题中的最后一项不能以分号结束，因此必须考虑这一点。\n\n添加以下`while`循环:\n\n```cpp\n    vector<pair<string, string>> subitems; \n    if (body.find(';') == body.npos) return subitems; \n    size_t start = 0;\n size_t end = start; while (end != body.npos){}\n```\n\n顾名思义，`start`变量是子项的起始索引，`end`是子项的结束索引。第一个动作是忽略任何空白，所以在`while`循环内添加:\n\n```cpp\n    while (start != body.length() && isspace(body[start])) \n    { \n        start++ ; \n    } \n    if (start == body.length()) break;\n```\n\n这只是在引用空白字符时增加`start`索引，只要它没有到达字符串的末尾。如果到达字符串的末尾，这意味着没有更多的字符，因此循环结束。\n\n接下来，添加以下内容来搜索`=`和`;`字符，并处理其中一种搜索情况:\n\n```cpp\n    string name = \"\"; \n    string value = \"\"; \n    size_t eq = body.find('=', start); \n    end = body.find(';', start); \n\n    if (eq == body.npos) \n    { \n        if (end == body.npos) name = body.substr(start); \n        else name = body.substr(start, end - start); \n    } \n    else \n    {\n    } \n    subitems.push_back(make_pair(name, value)); \n    start = end + 1;\n```\n\n如果找不到搜索到的项目，`find`方法将返回`npos`值。第一个调用寻找`=`字符，第二个调用寻找分号。如果找不到`=`，那么该物品没有价值，只有一个名字。如果找不到分号，则表示`name`是从`start`索引到字符串末尾的整个字符串。如果有分号，则`name`是从`start`索引开始直到`end`指示的索引(因此要复制的字符数是`end-start`)。如果找到一个`=`字符，那么字符串需要在这一点上被拆分，并且该代码将在一会儿后显示。一旦`name`和`value`变量被赋予值，这些变量被插入到`subitems`数据成员中，并且`start`索引被移动到`end`索引之后的字符。如果`end`指数为`npos`，则`start`指数的值将无效，但这并不重要，因为`while`循环将测试`end`指数的值，如果指数为`npos`，则将打破循环。\n\n最后，您需要添加子项中有`=`字符时的代码。添加以下突出显示的文本:\n\n```cpp\n    if (eq == body.npos) \n    { \n        if (end == body.npos) name = body.substr(start); \n        else name = body.substr(start, end - start); \n    } \n    else \n    { \n if (end == body.npos) { name = body.substr(start, eq - start); value = body.substr(eq + 1); } else { if (eq < end) { name = body.substr(start, eq - start); value = body.substr(eq + 1, end - eq - 1); } else { name = body.substr(start, end - start); } } \n    }\n```\n\n第一行测试分号搜索是否失败。在这种情况下，名称从`start`索引开始，直到等于字符之前的字符，值是等于符号之后的文本，直到字符串结束。\n\n如果等号和分号字符有有效的索引，那么还有一种情况需要检查。equals 字符的位置可能在分号之后，这意味着该子项没有值，而 equals 字符将用于后续子项。\n\n此时，您可以编译代码，并使用包含电子邮件的文件进行测试。程序的输出应该是邮件拆分为标题和正文，每个标题拆分为子项，可以是简单的字符串，也可以是`name=value`对。\n\n# 摘要\n\n在本章中，您已经看到了支持字符串的各种 C++ 标准库类。您已经看到了如何从流中读取字符串，如何将字符串写入流，如何在数字和字符串之间转换，以及如何使用正则表达式操作字符串。当您编写代码时，您将不可避免地花时间运行您的代码，以检查它是否按照您的规范工作。这将包括提供检查算法结果的代码、将中间代码记录到调试设备的代码，当然还有在调试器下运行代码。下一章是关于调试代码的！**"
  },
  {
    "path": "docs/mod-cpp/07.md",
    "content": "# 七、诊断和调试\n\n软件复杂；然而，当你设计你的代码时，无论是在开发代码的正常测试阶段，还是在发布 bug 报告时，在某个时候你都必须调试它。谨慎的做法是设计代码，使测试和调试尽可能简单。这意味着添加跟踪和报告代码，确定不变量以及前置和后置条件，以便您有一个测试代码的起点，并编写具有可理解和有意义的错误代码的函数。\n\n# 准备您的代码\n\nC++ 和 C 标准库具有广泛的功能，允许您应用跟踪和报告功能，以便您可以测试代码是否以预期的方式处理数据。这些工具大多使用条件编译，因此报告只发生在调试版本中，但是如果您为跟踪提供有意义的消息，它们将成为代码文档的一部分。在报告代码的行为之前，您首先必须知道从中可以得到什么。\n\n# 不变量和条件\n\n类不变量是条件，即对象状态，你知道它仍然是真的。在方法调用期间，对象状态将会改变，可能会使对象失效，但是一旦公共方法完成，对象状态必须保持一致。不能保证用户会以什么顺序调用类的方法，或者即使他们调用方法，所以无论用户调用什么方法，对象都必须是可用的。对象的不变方面适用于方法调用级别:在方法调用之间，对象必须一致且可用。\n\n例如，假设您有一个表示日期的类:它保存 1 到 31 之间的日数字、1 到 12 之间的月数字和年数字。类的不变量是，无论你对日期类的对象做什么，它总是保持一个有效的日期。这意味着用户可以安全地使用日期类的对象。这也意味着类中的其他方法(比如，确定两个日期之间间隔多少天的方法，`operator-`)可以假设日期对象中的值是有效的，因此这些方法不必检查它们所作用的数据的有效性。\n\n但是，有效日期大于范围 1 到 31(天)和 1 到 12(月)，因为不是每个月都有 31 天。所以，如果你有一个有效的日期，比如说 1997 年 4 月 5 日，并且你调用一个`set_day`方法将日数设置为 31，由于 4 月 31 日不是一个有效的日期，所以违反了类不变条件。如果要更改日期对象中的值，唯一安全的方法是同时更改所有值:日、月和年，因为这是保持类不变性的唯一方法。\n\n一种方法是在调试构建中定义一个私有方法，该方法测试类的不变量条件，并通过断言(见后面)确保不变量得到维护。您可以在可公开访问的方法离开之前调用这样的方法，以确保对象保持一致的状态。方法还应该定义前置条件和后置条件。前置条件是在调用方法之前您要求为真的条件，后置条件是在方法完成之后您保证为真的条件。对于类上的方法，类不变量是前置条件(因为在调用方法之前对象的状态应该是一致的)，不变量也是后置条件(因为在方法完成之后对象的状态应该是一致的)。\n\n还有一些先决条件是方法调用方的责任。先决条件是呼叫者确保的记录责任。例如，日期类将有一个前提条件，即日期在 1 到 31 之间。这简化了类代码，因为采用天数的方法可以假设传递的值永远不会超出范围(尽管，由于某些月份少于 31 天，值可能仍然无效)。同样，在调试版本中，您可以使用断言来检查这些先决条件是否为真，并且断言中的测试将在发布版本中被编译掉。在一个方法的末尾会有后置条件，也就是说，类不变量将被保持(并且对象的状态将是有效的)，返回值将是有效的。\n\n# 条件编译\n\n正如[第 1 章](08.html)、*中所解释的，从 C++* 开始，当编译你的 C++ 程序时，有一个预编译步骤，将 C++ 源文件中包含的所有文件整理成一个文件，然后进行编译。预处理器还扩展宏，根据符号的值，包括一些代码并排除其他代码。\n\n最简单的形式是，条件编译将代码用`#ifdef`和`#endif`括起来(也可以选择使用`#else`，这样，只有定义了指定的符号，这些指令之间的代码才会被编译。\n\n```cpp\n    #ifdef TEST \n       cout << \"TEST defined\" << endl;     \n    #else \n       cout << \"TEST not defined\" << endl; \n    #endif\n```\n\n保证只编译这些行中的一行，并且保证至少编译其中一行。如果符号`TEST`被定义，那么第一行将被编译，就编译器而言，第二行不存在。如果符号`TEST`未定义，则编译第二行。如果你想以相反的顺序输入这些行，你可以使用`#ifndef`指令。通过条件编译提供的文本可以是 C++ 代码，也可以使用当前翻译单元中的其他符号用`#define`定义，或者使用`#undef`定义未定义的现有符号。\n\n`#ifdef`指令只是确定符号是否存在:它不测试它的值。`#if`指令允许您测试表达式。您可以将符号设置为有值，并根据该值编译特定的代码。表达式必须是整数，因此单个`#if`块可以使用`#if`和多个`#elif`指令以及(最多)一个`#else`来测试多个值:\n\n```cpp\n    #if TEST < 0 \n       cout << \"negative\" << endl; \n    #elif TEST > 0 \n       cout << \"positive\" << endl; \n    #else \n       cout << \"zero or undefined\" << endl; \n    #endif\n```\n\n如果符号未定义，则`#if`指令将符号视为具有值`0`；如果您想区分这些情况，您可以使用`defined`操作符来测试是否定义了符号。最多只能编译`#if` / `#endif`块中的一个部分，如果某个值不匹配，则不会编译任何代码。表达式可以是宏，在这种情况下，宏将在测试条件之前展开。\n\n定义符号有三种方法。第一种方式不受你的控制:编译器将定义一些符号(通常带有`__`或`_`前缀)，这些符号为你提供关于编译器和编译过程的信息。这些符号中的一些将在后面的章节中描述。另外两种方法完全由您控制-您可以使用`#define`在源文件(或头文件)中定义符号，或者使用`/D`开关在命令行中定义它们:\n\n```cpp\n    cl /EHsc prog.cpp /DTEST=1\n```\n\n这将编译符号`TEST`设置为值`1`的源代码。\n\n您通常会使用条件编译来提供不应在生产代码中使用的代码，例如，在调试模式或测试代码时使用的额外跟踪代码。例如，假设您有从数据库返回数据的库代码，但是您怀疑库函数中的 SQL 语句有错误，并且返回了太多值。在这里，您可以决定测试、添加代码来记录返回值的数量:\n\n```cpp\n    vector<int> data = get_data(); \n    #if TRACE_LEVEL > 0 \n    cout << \"number of data items returned: \" << data.size() << endl; \n    #endif\n```\n\n像这样的跟踪消息污染了您的用户界面，您将希望在生产代码中避免它们。然而，在调试中，它们在确定问题发生的位置方面是无价的。\n\n你在调试模式下调用的任何代码，条件代码都应该是`const`方法(这里是`vector::size`)，也就是说，它们不应该影响任何对象或应用数据的状态。您必须确保您的代码的逻辑在调试模式和发布模式下完全相同*。*\n\n *# 使用实用程序\n\nPragmas 是特定于编译器的，通常关注目标文件中代码部分的技术细节。有几个 Visual C++ 实用程序在调试代码时很有用。\n\n一般来说，您希望代码编译时尽可能少出现警告。Visual C++ 编译器的默认警告是`/W1`，这意味着只列出最严重的警告。将该值增加到 2、3 或最高值 4 会逐渐增加编译期间给出的警告数量。使用`/Wall`将给出四级警告和默认禁用的警告。即使对于最简单的代码，这最后一个选项也会产生充满警告的屏幕。当你有数百个警告时，有用的错误信息会隐藏在大量不重要的警告之间。因为 C++ 标准库很复杂，并且使用了一些几十年前的代码，所以编译器会警告您一些结构。为了防止这些警告污染生成的输出，选择性文件中的特定警告已被禁用。\n\n如果您支持旧的库代码，您可能会发现代码编译时出现警告。您可能会尝试使用编译器`/W`开关来降低警告级别，但这将抑制所有高于您启用的警告，并且它同样适用于您的代码，就像您可能包含在项目中的库代码一样。`warning`实用程序给了你更多的灵活性。有两种方法可以调用它——您可以重置警告级别以覆盖编译器`/W`开关，并且您可以更改特定警告的警告级别或完全禁用警告报告。\n\n例如`<iostream>`表头的顶部是一行:\n\n```cpp\n    #pragma warning(push,3)\n```\n\n这表示存储当前的警告级别，并且对于这个文件的其余部分(或者直到它被改变)，将警告级别设置为 3。文件的底部是一行:\n\n```cpp\n    #pragma warning(pop)\n```\n\n这会将警告级别恢复到先前存储的级别。\n\n您还可以更改一个或多个警告的报告方式。例如，在`<istream>`的顶部是:\n\n```cpp\n    #pragma warning(disable: 4189)\n```\n\n该`pragma`的第一部分是说明符`disable`，表示禁止报告警告类型(在本例中为 4189)。如果选择，可以使用警告级别(`1`、`2`、`3`或`4`)作为说明符来更改警告的警告级别。这样做的一个用途是降低您正在处理的一段代码的警告级别，然后在代码之后将其返回到默认级别。例如:\n\n```cpp\n    #pragma warning(2: 4333) \n    unsigned shift8(unsigned char c)  \n    { \n        return c >> 8;  \n    } \n    #pragma warning(default: 4333)\n```\n\n该功能将一个字符右移 8 位，将产生 1 级警告 4333 ( *右移量过大，数据丢失*)。这是一个问题，需要修复，但目前，您希望编译代码时没有来自该代码的警告，因此警告级别更改为 2 级。使用默认警告级别(`/W1`)时，将不会显示警告。但是，如果您使用更敏感的警告级别(例如，`/W2`)进行编译，则会报告此警告。警告级别的这种变化只是暂时的，因为最后一行将警告级别重置回默认值(即 1)。在这种情况下，警告级别会增加，这意味着您只会在编译器上看到更敏感的警告级别。您也可以降低警告级别，这意味着更有可能报告警告。您甚至可以将警告级别更改为`error`，这样当代码中存在这种类型的警告时，代码就不会编译。\n\n# 添加信息性消息\n\n在测试和调试代码时，您不可避免地会遇到一些地方，在那里您可以看到潜在的问题，但是与您正在处理的问题相比，它的优先级较低。记下这个问题很重要，这样你就可以在以后解决这个问题。在 Visual C++ 中，有两种方法可以良性地做到这一点，还有两种方法会产生错误。\n\n第一种方式是添加一个`TODO:`注释，如下所示:\n\n```cpp\n    // TODO: potential data loss, review use of shift8 function \n    unsigned shift8(unsigned char c)  \n    { \n        return c >> 8;  \n    }\n```\n\nVisual Studio 编辑器有一个名为**任务列表**的工具窗口。这将列出项目中以预定任务之一开始的注释(默认值为`TODO`、`HACK`和`UNDONE`)。\n\n如果“任务列表”窗口不可见，请通过“视图”菜单启用它。Visual Studio 2015 中的默认设置是在 C++ 中启用任务。早期版本不是这样的，但是可以通过“工具”菜单、“选项”对话框，然后通过文本编辑器、C/C++、格式、视图，将“枚举注释任务”设置为“是”来启用它。任务标签列表可以在“选项”对话框的“环境”、“任务列表”项下找到。\n\n任务列表列出了带有文件和行号的任务，您可以通过双击条目打开文件并找到注释。\n\n第二种识别需要注意的代码的方法是`message` pragma。顾名思义，这只是允许您在代码中放置信息性消息。当编译器遇到这个 pragma 时，它只是将消息放在输出流中。考虑以下代码:\n\n```cpp\n    #pragma message(\"review use of shift8 function\") \n    unsigned shift8(unsigned char c)  \n    { \n        return c >> 8;  \n    }\n```\n\n如果用此代码和`/W1`(默认)警告级别编译`test.cpp`文件，输出将如下所示:\n\n```cpp\n Microsoft (R) C/C++ Optimizing Compiler Version 19.00.24215.1 for x86\nCopyright (C) Microsoft Corporation.  All rights reserved.\n\ntest.cpp\nreview the use of shift8 function\ntest.cpp(8): warning C4333: '>>': right shift by too large amount, data loss\n```\n\n正如您所看到的，字符串被打印出来，就像编译器看到的那样，并且与警告消息相比，没有文件或行号的指示。有几种方法可以使用编译器符号来解决这个问题。\n\n如果条件很重要，您将需要发出一个错误，一种方法是使用`#error`指令。当编译器到达这个指令时，它会发出一个错误。这是一个严肃的动作，所以你只有在有其他选择的时候才会使用它。您很可能希望将其用于条件编译。典型的用途是只能用 C++ 编译器编译的代码:\n\n```cpp\n    #ifndef __cplusplus \n    #error C++ compiler required. \n    #endif\n```\n\n如果使用`/Tc`开关将代码编译为 C，用该代码编译一个文件，那么`__cplusplus`预处理器符号将不会被定义，并且将产生一个错误。\n\nC++ 11 增加了一个新的指令`static_assert`。这就像一个函数一样被调用(而*调用*以分号结束)，但它不是一个函数，因为它只在编译时使用。此外，指令可以用在不使用函数调用的地方。该指令有两个参数:表达式和字符串。如果表达式是`false`，那么字符串文字将在编译时与源文件和行号一起输出，并产生一个错误。在最简单的层次上，您可以使用它来发出一条消息:\n\n```cpp\n    #ifndef __cplusplus \n    static_assert(false, \"Compile with /TP\"); \n    #endif \n    #include <iostream> // needs the C++ compiler\n```\n\n由于第一个参数是`false`，指令会在编译时发出错误信息。同样的事情也可以通过`#error`指令来实现。`<type_traits>`库有各种谓词来测试类型的属性。例如，`is_class`模板类有一个简单的模板参数，它是一个类型，如果类型是一个`class`，那么`static`成员`value`被设置为`true`。如果您有一个只应该为类实例化的模板化函数，您可以添加这个`static_assert`:\n\n```cpp\n    #include <type_traits> \n\n    template <class T> \n    void func(T& value) \n    { \n        static_assert(std::is_class<T>::value, \"T must be a class\"); \n        // other code \n    }\n```\n\n在编译时，编译器将尝试实例化该函数，并使用`value`实例化该类型上的`is_class`，以确定编译是否应该继续。例如，以下代码:\n\n```cpp\n    func(string(\"hello\")); \n    func(\"hello\");\n```\n\n第一行将正确编译，因为编译器将实例化一个函数`func<string>,`，参数是一个`class`。但是第二行不会编译，因为实例化的函数是`func<const char*>`，`const char*`不是`class`。输出结果是:\n\n```cpp\nMicrosoft (R) C/C++ Optimizing Compiler Version 19.00.24215.1 for x86\nCopyright (C) Microsoft Corporation.  All rights reserved.\n\ntest.cpp\ntest.cpp(25): error C2338: T must be a class\ntest.cpp(39): note: see reference to function template instantiation \n\n'void func<const char*>(T)' being compiled\nwith\n[\n T=const char *\n]\n```\n\n`static_assert`在*线路 25* 上，因此产生`T must be a class`的误差。*第 39 行*是对`func<const char*>`的第一次调用，给出了错误的上下文。\n\n# 用于调试的编译器开关\n\n为了允许您使用调试器单步执行程序，您必须提供信息以允许调试器将机器代码与源代码相关联。至少，这意味着关闭所有优化，因为在试图优化代码时，C++ 编译器会重新排列代码。优化在默认情况下是关闭的(因此使用`/Od`开关是多余的)，但是很明显，为了能够调试一个进程并单步执行 C++ 代码，您需要移除所有的`/O`优化开关。\n\n由于 C++ 标准库使用 C 运行时，您将需要编译代码来使用后者的调试版本。您使用的开关取决于您是构建进程还是**动态链接库** ( **DLL** )，以及您是静态链接 C 运行时还是通过 DLL 访问它。如果你正在编译一个进程，你使用`/MDd`在一个 DLL 中获得 C 运行时的调试版本，如果你使用`/MTd`你将获得静态链接 C 运行时的调试版本。如果你正在编写一个动态链接库，除了使用一个 C 运行时开关之外，你还必须使用`/LDd`(默认为`/MTd`)。这些开关将定义一个名为`_DEBUG`的预处理器符号。\n\n调试器需要知道调试器符号信息——变量的名称和类型，函数的名称以及与代码相关的行号。公认的方法是通过一个名为**程序数据库**的文件，扩展名为`pdb`。使用其中一个`/Z`开关生成一个`pdb`文件:`/Zi`或`/ZI`开关将创建两个文件，一个文件的名称以`VC`开头(例如`VC140.pdb`)，包含所有`obj`文件的调试信息，另一个文件的名称包含过程调试。如果编译时没有链接(`/c`)，那么只创建第一个文件。默认情况下，Visual C++ 项目向导将使用`/Od /MDd /ZI`进行调试版本。`/ZI`开关是指以允许 Visual C++ 调试器执行`Edit`和`Continue`的格式创建程序数据库，也就是说，您可以更改一些代码并继续单步执行代码，而无需重新编译。当您编译发布版本时，向导将使用`/O2 /MD /Zi`开关，这意味着代码针对速度进行了优化，但仍将创建程序数据库(没有`Edit`和`Continue`支持)。代码不需要程序数据库来运行(事实上，您不应该将它与您的代码一起分发)，但是如果您有一个崩溃报告并且需要在调试器下运行发布构建代码，那么它是非常有用的。\n\n这些`/Z`编译器开关假定链接器与`/debug`开关一起运行(如果编译器调用链接器，它将传递这个开关)。链接器将根据`VC`程序数据库文件中的调试信息创建项目程序数据库。\n\n这就提出了为什么发布构建文件需要程序数据库的问题。如果您在调试器下运行一个程序并查看调用堆栈，您通常会在操作系统文件中看到一长串堆栈帧。这些通常有相当无意义的名称，由 DLL 名称和一些数字和字符组成。可以为 Windows 安装这些符号(即`pdb`文件)，如果没有安装，可以指示 Visual C++ 调试器从网络上一台名为**符号服务器**的计算机上下载正在使用的库的符号。这些符号不是库的源代码，但它们确实给了您函数的名称和参数的类型，这为您提供了关于单步执行时调用堆栈状态的附加信息。\n\n# 预处理符号\n\n要访问代码中的跟踪、断言和报告工具，您必须启用调试运行时库，这是通过使用`/MDd`、`/MTd`或`/LDd`编译器开关来完成的，这些开关将定义`_DEBUG`预处理器符号。`_DEBUG`预处理器符号支持许多功能，相反，不定义该符号将有助于优化您的代码。\n\n```cpp\n    #ifdef _DEBUG \n       cout << \"debug build\" << endl; \n    #else \n       cout << \"release built\" << endl; \n    #endif\n```\n\nC++ 编译器还将通过一些标准的预处理器符号来提供信息。其中大部分只对图书馆作者有用，但也有一些你可能想用。\n\nANSI 标准说`__cplusplus`符号应该在编译器以 C++(而不是 C)的形式编译代码时定义，它还规定`__FILE__`符号应该包含文件名，`__LINE__`符号在你访问它的地方会有行号。`__func__`符号将具有当前功能名称。这意味着您可以创建如下跟踪代码:\n\n```cpp\n    #ifdef _DEBUG \n    #define TRACE cout << __func__ << \" (\" << __LINE__ << \")\" << endl; \n    #else \n    #define TRACE \n    #endif\n```\n\n如果该代码是为了调试而编译的(例如`/MTd`)，那么每当使用`TRACE`时，`cout`行将被内联；如果代码没有被编译用于调试，那么`TRACE`将什么也不做。`__func__`符号仅仅是函数名，它是不合格的，所以如果你在一个类方法中使用它，它将不会提供关于这个类的信息。\n\nVisual C++ 还定义了特定于微软的符号。`__FUNCSIG__`符号给出完整的签名，包括类名(和任何`namespace`名称)、返回类型和参数。如果只是想要全限定名，那么可以使用`__FUNCTION__`符号。在 Windows 头文件中你会经常看到的一个符号是`_MSC_VER`。它有一个数字，是当前 C++ 编译器的版本，它与条件编译一起使用，因此较新的语言功能只能由支持它们的编译器编译。\n\nVisual C++ 项目页面定义了名为`$(ProjectDir)`和`$(Configuration)`的*构建宏*。这些仅由 MSBuild 工具使用，因此它们在编译期间不会自动出现在源文件中，但是，如果您将预处理器符号设置为生成宏的值，则该值将在编译时通过该符号可用。系统环境变量也可以作为构建宏使用，因此可以使用它们来影响构建。例如，在 Windows 上，系统环境变量`USERNAME`有当前登录用户的名称，因此您可以使用它来设置一个符号，然后在编译时访问它。\n\n在 Visual C++ 项目页面中，可以在 C/C++ 预处理器项目页面上添加一个名为**的预处理器定义**:\n\n```cpp\n    DEVELOPER=\"$(USERNAME)\"\n```\n\n然后，在代码中，您可以使用这个符号添加一行:\n\n```cpp\n    cout << \"Compiled by \" << DEVELOPER << endl;\n```\n\n如果您正在使用 make 文件，或者只是从命令行调用`cl`，您可以添加一个开关来定义符号，如下所示:\n\n```cpp\n    /DDEVELOPER=\"$(USERNAME)\"\n```\n\n这里转义双引号很重要，因为没有双引号，编译器会吃掉引号。\n\n前面，您已经看到了如何使用`#pragma message`和`#error`指令将消息放入编译器的输出流中。在 Visual Studio 中编译代码时，编译器和链接器输出将出现在输出窗口中。如果消息的格式为:\n\n```cpp\n    path_to_source_file(line) message\n```\n\n其中`path_to_source_file`是文件的完整路径，`line`是出现`message`的行号。然后，当您双击输出窗口中的这一行时，文件将被加载(如果还没有)并将插入点放在线上。\n\n`__FILE__`和`__LINE__`符号为您提供了使`#pragma message`和`#error`指令更加有用所需的信息。输出`__FILE__`很简单，因为它是一个字符串，C++ 将连接字符串文字:\n\n```cpp\n    #define AT_FILE(msg) __FILE__ \" \" msg \n\n    #pragma message(AT_FILE(\"this is a message\"))\n```\n\n宏作为 pragma 的一部分被调用，以正确格式化消息；但是，您不能从宏中调用 pragma，因为`#`有一个特殊的用途(一会儿就会有用)。这段代码的结果如下:\n\n```cpp\n    c:\\Beginning_C++ Chapter_10test.cpp this is a message\n```\n\n通过宏输出`__LINE__`需要更多的工作，因为它包含一个数字。这个问题在 C 语言中很常见，所以有一个使用两个宏和串线操作符`#`的标准解决方案。\n\n```cpp\n    #define STRING2(x) #x \n    #define STRING(x) STRING2(x) \n    #define AT_FILE(msg) __FILE__ \"(\" STRING(__LINE__) \") \" msg\n```\n\n`STRING`宏用于将`__LINE__`符号展开为一个数字，而`STRING2`宏用于将该数字拉长。`AT_FILE`宏以正确的格式格式化整个字符串。\n\n# 生成诊断消息\n\n诊断消息的有效使用是一个广泛的话题，因此本节将只向您介绍基本知识。当您设计代码时，您应该使编写诊断消息变得容易，例如，提供转储对象内容的机制，并提供对测试类不变量以及前置和后置条件的代码的访问。您还应该分析代码，以确保记录了适当的消息。例如，在循环中发出诊断消息通常会填满您的日志文件，从而难以读取日志文件中的其他消息。然而，某件事在一个循环中不断失败的事实本身可能是一个重要的诊断，就像执行失败行为的尝试次数一样，所以你可能需要记录下来。\n\n对诊断消息使用`cout`的好处是可以将这些消息与您的用户输出集成在一起，这样您就可以看到中间结果的最终效果。缺点是诊断消息与用户输出集成在一起，由于通常有大量的诊断消息，这些消息将完全淹没程序的用户输出。\n\nC++ 有两个流对象，可以用来代替`cout`。`clog`和`cerr`流对象将字符数据写入标准错误流(C 流指针`stderr`)，这通常会显示在控制台上，就像您正在使用`cout`(输出到标准输出流，C 流指针`stdout`)一样，但是您可以将其重定向到其他地方。`clog`和`cerr`的区别在于`clog`使用缓冲输出，这可能比无缓冲的`cerr`性能更好。但是，如果应用在没有刷新缓冲区的情况下意外停止，数据可能会丢失。\n\n由于`clog`和`cerr`流对象在发布版本和调试版本中都可用，因此您应该只将它们用于您希望最终用户看到的消息。这使得它们不适用于跟踪消息(稍后将介绍)。相反，您应该将它们用于用户能够处理的诊断消息(可能是找不到文件或者进程没有执行操作的安全访问权限)。\n\n```cpp\n    ofstream file; \n    if (!file.open(argv[1], ios::out)) \n    { \n        clog << \"cannot open \" << argv[1] << endl; \n        return 1; \n    }\n```\n\n这段代码分两步打开文件(而不是使用构造函数)，如果文件无法打开，`open`方法将返回`false`。代码检查打开文件是否成功，如果失败，它将通过`clog`对象告诉用户，然后从包含代码的任何函数返回，因为`file`对象现在无效，不能使用。`clog`对象被缓冲，但在这种情况下，我们想立即通知用户，这是由`endl`操纵器执行的，它在流中插入一个换行符，然后刷新流。\n\n默认情况下，`clog`和`cerr`流对象将输出到标准错误流，这意味着对于控制台应用，您可以通过重定向流来分离输出流和错误流。在命令行上，标准流可以通过使用值 0(代表`stdin`)、1(代表`stdout,`)和 2(代表`stderr`)以及重定向操作符`>`进行重定向。例如，应用`app.exe`可以在`main`功能中包含以下代码:\n\n```cpp\n    clog << \"clog\" << endl; \n    cerr << \"cerrn\"; \n    cout << \"cout\" << endl;\n```\n\n`cerr`对象没有被缓冲，所以你是否使用`n`或`endl`作为换行符是无关紧要的。当您在命令行上运行该命令时，您将看到如下内容:\n\n```cpp\nC:\\Beginning_C++ \\Chapter_10>app\nclog\ncerr\ncout\n```\n\n要将流重定向到文件，请将流句柄(1 代表`stdout`，2 代表`stderr`)重定向到文件；控制台将打开文件并将流写入文件:\n\n```cpp\nC:\\Beginning_C++ \\Chapter_10>app 2>log.txt\ncout\n\nC:\\Beginning_C++ \\Chapter_10>type log.txt\nclog\ncerr\n```\n\n正如上一章所展示的，C++ 流对象是分层的，这样，根据流的类型，无论有无缓冲，向流中插入数据的调用都会将数据写入底层流对象。使用`rdbuf`方法获取并替换该流缓冲区对象。如果希望应用将`clog`对象重定向到文件，可以编写如下代码:\n\n```cpp\n    extern void run_code(); \n\n    int main() \n    { \n        ofstream log_file; \n        if (log_file.open(\"log.txt\")) clog.rdbuf(log_file.rdbuf()); \n\n        run_code(); \n\n        clog.flush(); \n        log_file.close(); \n        clog.rdbuf(nullptr); \n        return 0; \n    }\n```\n\n在这段代码中，应用代码将在`run_code`函数中，其余代码设置`clog`对象重定向到文件。\n\n注意当`run_code`函数返回时(应用已经完成)，文件被显式关闭；这并不完全是因为`ofstream`析构函数会关闭文件，在这种情况下，当`main`函数返回时就会发生这种情况。最后一行很重要。标准流对象是在调用`main`函数之前创建的，并且它们将在`main`函数返回之后的某个时间被销毁，也就是说，在文件对象被销毁之后。为防止`clog`对象访问被破坏的文件对象，调用`rdbuf`方法传递`nullptr`表示没有缓冲区。\n\n# 用 C 运行时跟踪消息\n\n通常，您会希望通过实时运行应用并输出*跟踪消息*来测试您的算法是否工作，从而测试您的代码。有时您会想要测试调用函数的顺序(例如，正确的分支发生在`switch`语句或`if`语句中)，在其他情况下，您会想要测试中间值，以查看输入数据是否正确以及对该数据的计算是否正确。\n\n跟踪消息会产生大量数据，因此将这些数据发送到控制台是不明智的。跟踪消息只在调试版本中生成是非常重要的。如果您在产品代码中留下跟踪消息，它可能会严重影响应用的性能(这将在后面解释)。此外，跟踪消息不太可能本地化，也不会检查它们是否包含可用于逆向工程算法的信息。发布版本中跟踪消息的最后一个问题是，您的客户会认为您向他们提供的代码没有经过完全测试。因此，当`_DEBUG`符号被定义时，跟踪消息只在调试版本中生成是很重要的。\n\nC Runtime 提供了一系列名称以`_RPT`开头的宏，可以在定义`_DEBUG`时用来跟踪消息。这些宏有`char`和宽字符版本，也有只报告跟踪消息的版本，还有报告消息和消息位置(源文件和行号)的版本。最终，这些宏将调用一个名为`_CrtDbgReport`的函数，该函数将使用其他地方确定的设置生成消息。\n\n`_RPTn`宏(其中`n`是`0`、`1`、`2`、`3`、`4`或`5`)将采用一个格式字符串和 0 到 5 个参数，这些参数将在报告前放入字符串中。宏的第一个参数指示要报告的消息类型:`_CRT_WARN`、`_CRT_ERROR`或`_CRT_ASSERT`。最后两个类别是相同的，指的是断言，这将在后面的章节中介绍。报表宏的第二个参数是一个格式字符串，后面是所需数量的参数。`_RPTFn`宏的格式相同，但会报告源文件和行号以及格式化的消息。\n\n默认操作是`_CRT_WARN`消息不产生输出，`_CRT_ERROR`和`_CRT_ASSERT`消息将生成一个弹出窗口，允许您中止或调试应用。您可以通过调用`_CrtSetReportMode`函数并提供类别和指示要采取的操作的值来更改对这些消息类别的响应。如果您使用`_CRTDBG_MODE_DEBUG`，那么消息将被写入调试器输出窗口。如果您使用`_CRTDBG_MODE_FILE`，那么消息将被写入一个文件，您可以打开该文件并将句柄传递给`_CrtSetReportFile`功能。(也可以使用`_CRTDBG_FILE_STDERR`或`_CRTDBG_FILE_STDOUT`作为文件句柄，将消息发送到标准输出或错误输出。)如果您使用`_CRTDBG_MODE_WNDW`作为报告模式，那么将使用中止/重试/忽略对话框显示消息。因为这将暂停当前的执行线程，所以它应该只用于断言消息(默认操作):\n\n```cpp\n    include <crtdbg.h> \n\n    extern void run_code(); \n\n    int main() \n    { \n        _CrtSetReportMode(_CRT_WARN, _CRTDBG_MODE_DEBUG); \n        _RPTF0(_CRT_WARN, \"Application startedn\"); \n\n        run_code(); \n\n        _RPTF0(_CRT_WARN, \"Application endedn\"); \n        return 0; \n    }\n```\n\n如果您没有在消息中提供`n`，那么下一条消息将被附加到您的消息的末尾，并且在大多数情况下这不是您想要的(尽管您可以通过对`_RPTn`宏的一系列调用来证明这一点，其中最后一条以`n`结束)。\n\n编译项目时会显示 Visual Studio 输出窗口(要在调试时显示该窗口，请选择“视图”菜单中的“输出”选项)，顶部是一个标记为“显示输出来源”的组合框，通常设置为“生成”。如果将此设置为调试，您将看到调试会话期间生成的调试消息。这些将包括关于加载调试符号的消息和从`_RPTn`宏重定向到输出窗口的消息。\n\n如果您希望消息指向一个文件，那么您需要使用 Win32 `CreateFile`函数打开该文件，并在调用`_CrtSetReportFile`函数时使用该函数的句柄。为此，您需要包含 Windows 头文件:\n\n```cpp\n    #define WIN32_LEAN_AND_MEAN \n    #include <Windows.h> \n    #include <crtdbg.h>\n```\n\n`WIN32_LEAN_AND_MEAN`宏将减小包含的窗口文件的大小。\n\n```cpp\n    HANDLE file =  \n       CreateFileA(\"log.txt\", GENERIC_WRITE, 0, 0, CREATE_ALWAYS, 0, 0); \n    _CrtSetReportMode(_CRT_WARN, _CRTDBG_MODE_FILE); \n    _CrtSetReportFile(_CRT_WARN, file); \n    _RPTF0(_CRT_WARN, \"Application startedn\"); \n\n    run_code(); \n\n    _RPTF0(_CRT_WARN, \"Application endedn\"); \n    CloseHandle(file);\n```\n\n该代码将警告消息导向文本文件`log.txt`，该文件将在每次应用运行时被新建。\n\n# 使用窗口跟踪邮件\n\n`OutputDebugString`功能用于向调试器发送消息。该功能通过一个名为`DBWIN_BUFFER`的*共享内存部分*来实现。共享内存意味着任何进程都可以访问该内存，因此 Windows 提供了两个*事件对象*，称为`DBWIN_BUFFER_READY`和`DBWIN_DATA_READY`，控制对该内存的读写访问。这些事件对象在进程之间共享，可以处于有信号或无信号状态。调试器将通过发送`DBWIN_BUFFER_READY`事件来指示它不再使用共享内存，此时`OutputDebugString`函数可以将数据写入共享内存。调试器将等待`DBWIN_DATA_READY`事件，当它完成对存储器的写入并且可以安全地读取缓冲区时，`OutputDebugString`函数将发出信号。写入内存部分的数据将是调用`OutputDebugString`函数的进程的进程标识，后跟一个高达 4 KB 的数据字符串。\n\n问题是，当您调用`OutputDebugString`函数时，它将等待`DBWIN_BUFFER_READY`事件，这意味着当您使用该函数时，您正在将应用的性能耦合到另一个进程的性能，该进程通常是调试器(但可能不是)。编写一个进程来访问`DBWIN_BUFFER`共享内存部分并访问相关联的事件对象是非常容易的，因此您的生产代码可能会运行在有人运行此类应用的机器上。因此，使用条件编译非常重要，以便`OutputDebugString`函数仅用于调试构建，这些代码永远不会发布给客户:\n\n```cpp\n    extern void run_code(); \n\n    int main() \n    { \n        #ifdef _DEBUG \n            OutputDebugStringA(\"Application startedn\"); \n        #endif \n\n        run_code(); \n\n        #ifdef _DEBUG \n           OutputDebugStringA(\"Application endedn\"); \n        #endif \n        return 0; \n    }\n```\n\n您需要包含`windows.h`头文件来编译该代码。至于`_RPT`的例子，你必须在调试器下运行这段代码才能看到输出，或者运行像 **DebugView** 这样的应用(可从微软的 Technet 网站获得)。\n\nWindows 提供了`DBWinMutex`互斥对象作为访问共享内存和事件对象的整体*键*。顾名思义，当您拥有互斥体的句柄时，您将拥有对资源的互斥访问权。问题是，进程不一定要有这个互斥体的句柄才能使用这些资源，因此你不能保证，如果你的应用认为它有独占访问权，它真的会有独占访问权。\n\n# 使用资产\n\n断言检查条件是否为真。这个断言仅仅意味着:如果条件不成立，程序就不应该继续。明确声明不应该在发布代码中调用，因此必须使用条件编译。断言应该用于检查永远不会发生的情况:永远不会发生的事件。因为这些条件不会发生，所以在发布版本中不需要断言。\n\nC 运行时提供通过`<cassert>`头文件可用的`assert`宏。除非定义了`NDEBUG`符号，否则将调用宏和作为其唯一参数传递的表达式中调用的任何函数。也就是说，您不必定义`_DEBUG`符号来使用断言，并且您应该已经采取了额外的措施来显式地防止`assert`被调用。\n\n值得重复一遍。即使没有定义`_DEBUG`，也定义了`assert`宏，因此可以在发布代码中调用断言。为了防止这种情况发生，您必须在发布版本中定义`NDEBUG`符号。相反，您可以在调试版本中定义`NDEBUG`符号，以便可以使用跟踪，但不必使用断言。\n\n通常，您将在调试版本中使用断言来检查函数中的前置和后置条件是否满足，以及类不变条件是否满足。例如，您可能有一个二进制缓冲区，它在第十个字节位置有一个特殊值，因此编写了一个函数来提取该字节:\n\n```cpp\n    const int MAGIC=9; \n\n    char get_data(char *p, size_t size) \n    { \n        assert((p != nullptr)); \n        assert((size >= MAGIC)); \n        return p[MAGIC]; \n    }\n```\n\n这里对`assert`的调用是用来检查指针是否不是`nullptr`以及缓冲区是否足够大。如果这些断言为真，则意味着通过指针访问第十个字节是安全的。\n\n虽然在这段代码中并没有严格的必要，但是断言表达式在括号中给出。养成这样做的习惯是好的，因为`assert`是一个宏，所以表达式中的逗号会被当作宏参数分隔符；括号对此进行了保护。\n\n由于默认情况下`assert`宏将在发布版本中定义，因此您必须通过在编译器命令行、make 文件中定义`NDEBUG`来禁用它们，或者您可能希望显式使用条件编译:\n\n```cpp\n    #ifndef _DEBUG \n    #define NDEBUG \n    #endif\n```\n\n如果一个断言被调用并且它失败了，那么一个断言消息连同源文件和行号信息一起被打印在控制台上，然后这个过程以调用`abort`结束。如果流程是用发布构建标准库构建的，那么流程`abort`是简单的，但是，如果使用调试构建，那么用户将看到标准的中止/重试/忽略消息框，其中中止和忽略选项中止流程。重试选项将使用**及时** ( **准时**)调试将注册的调试器附加到进程。\n\n相比之下，`_ASSERT`和`_ASSERTE`宏仅在`_DEBUG`被定义时才被定义，因此这些宏在发布版本中不可用。当表达式为`false`时，两个宏都接受一个表达式并生成一个断言消息。`_ASSERT`宏的消息将包括源文件和行号，以及声明断言失败的消息。`_ASSERTE`宏的消息类似，但包含失败的表达式。\n\n```cpp\n    _CrtSetReportMode(_CRT_ASSERT, _CRTDBG_MODE_FILE); \n    _CrtSetReportFile(_CRT_ASSERT, _CRTDBG_FILE_STDOUT); \n\n    int i = 99; \n    _ASSERTE((i > 100));\n```\n\n此代码设置报告模式，以便失败的断言将是控制台上打印的消息(而不是默认消息，即中止/重试/忽略对话框)。由于变量明显小于 100，断言将失败，因此进程将终止，控制台上将打印以下消息:\n\n```cpp\n    test.cpp(23) : Assertion failed: (i > 100)\n```\n\n“中止/重试/忽略”对话框为测试应用的人员提供了将调试器附加到进程的选项。如果您认为断言的失败令人发指，您可以通过调用`_CrtDbgBreak`来强制调试器附加到进程。\n\n```cpp\n    int i = 99; \n    if (i <= 100) _CrtDbgBreak();\n```\n\n您不需要使用条件编译，因为在发布版本中`_CrtDbgBreak`函数是一个非操作。在调试版本中，此代码将触发 JIT 调试，这为您提供了关闭应用或启动调试器的选项，如果您选择后者，将启动注册的 JIT 调试器。\n\n# 应用终止\n\n`main`功能是您应用的入口点。但是，这不是操作系统直接调用的，因为 C++ 会在调用`main`之前执行初始化。这包括构建标准库全局对象(`cin`、`cout`、`cerr`、`clog,`和宽字符版本)，并且为支持 C++ 库的 C 运行时库执行大量初始化。此外，还有您的代码创建的全局和静态对象。当`main`函数返回时，必须调用全局和静态对象的析构函数，并在 C 运行时执行清理。\n\n有几种方法可以故意停止一个过程。最简单的是从`main`函数返回，但是这假设从您的代码想要完成这个过程的点有一个返回`main`函数的简单路径。当然，进程终止必须是有序的，您应该避免编写代码，在代码中的任何地方停止进程都是正常的。但是，如果您遇到数据损坏且不可恢复的情况，并且任何其他操作都可能损坏更多数据，则除了终止应用之外，您可能别无选择。\n\n`<cstdlib>`头文件提供了对头文件的访问，允许您终止和处理应用终止的功能。当一个 C++ 程序正常关闭时，C++ 基础设施将调用在`main`函数中创建的对象的析构函数(与它们的构造顺序相反)和`static`对象的析构函数(可能是在`main`函数以外的函数中创建的)。`atexit`函数允许您注册在`main`函数完成和`static`对象析构函数被调用后将被调用的函数(没有参数和返回值)。您可以通过多次调用这个函数来注册多个函数，在终止时，这些函数将按照与注册相反的顺序被调用。在用`atexit`函数注册的函数被调用后，任何全局对象的析构函数都将被调用。\n\n还有一个叫做`_onexit`的微软函数，也可以让你注册正常终止时要调用的函数。\n\n`exit`和`_exit`函数执行进程的正常退出，也就是说，它们在关闭进程之前清理 C 运行时并刷新任何打开的文件。`exit`函数通过调用任何注册的终止函数来做额外的工作；`_exit`函数不调用这些终止函数，因此是快速退出。这些函数不会调用临时或自动对象的析构函数，所以如果使用堆栈对象来管理资源，在调用`exit`之前，必须先显式调用析构函数代码。然而，静态和全局对象的析构函数将被调用。\n\n`quick_exit`函数导致正常关机，但不调用任何析构函数，也不刷新任何流，所以没有资源清理。用`atexit`注册的函数不被调用，但是你可以通过用`at_quick_exit`函数注册它们来注册终止函数被调用。调用这些终止函数后，`quick_exit`函数调用关闭进程的`_Exit`函数。\n\n您也可以调用`terminate`函数来关闭没有清理的进程。这个过程将调用一个已经在`set_terminate`函数中注册的函数，然后调用`abort`函数。如果程序中出现异常，并且没有被捕获，从而传播到`main`函数，C++ 基础设施将调用`terminate`函数。`abort`功能是终止进程的最严重的机制。该函数将退出进程，而不调用对象的析构函数或执行任何其他清理。该函数发出`SIGABORT`信号，因此可以向`signal`函数注册一个函数，该函数将在进程终止前被调用。\n\n# 误差值\n\n有些函数被设计为执行一个动作并基于该动作返回值，例如，`sqrt`将返回一个数字的平方根。其他函数执行更复杂的操作，并使用返回值来指示函数是否成功。对于这样的错误值没有通用的约定，所以如果一个函数返回一个简单的整数，就不能保证一个库使用的值与另一个库中的函数返回的值具有相同的含义。这意味着您必须仔细检查您使用的任何库代码的文档。\n\nWindows 确实提供了常见的错误值，可以在`winerror.h`头文件中找到，Windows **软件开发工具包** ( **SDK** )中的函数只在这个文件中返回值。如果您编写将在 Windows 应用中独占使用的库代码，请考虑使用此文件中的错误值，因为您可以使用 Win32 `FormatMessage`函数来获取错误的描述，如下一节所述。\n\nC 运行时库提供了一个名为`errno`的全局变量(实际上它是一个你可以当作变量对待的宏)。c 函数将返回一个值来指示它们已经失败，您访问`errno`值来确定错误是什么。`<errno.h>`头文件定义了标准的 POSIX 错误值。`errno`变量并不表示成功，它只表示错误，所以您应该只在某个函数表示有错误时才访问它。`strerror`函数将返回一个 C 字符串，该字符串描述了作为参数传递的错误值；这些消息根据通过调用`setlocale`函数设置的当前 C 语言环境进行本地化。\n\n# 获取消息描述\n\n要在运行时获取 Win32 错误代码的描述，可以使用 Win32 `FormatMessage`函数。这将获得系统消息或自定义消息的描述(在下一节中描述)。如果您想使用自定义消息，您必须加载绑定了消息资源的可执行文件(或动态链接库)，并将`HMODULE`句柄传递给`FormatMessage`函数。如果你想得到系统消息的描述，你不需要加载一个模块，因为 Windows 会为你做这件事。例如，如果您调用 Win32 `CreateFile`函数打开一个文件，但找不到该文件，该函数将返回一个值`INVALID_HANDLE_VALUE,`，表示有错误。要获取错误的详细信息，您需要调用`GetLastError`函数(该函数返回一个 32 位无符号值，有时称为`DWORD`或`HRESULT`)。然后，您可以将错误值传递给`FormatMessage`:\n\n```cpp\n    HANDLE file = CreateFileA( \n        \"does_not_exist\", GENERIC_READ, 0, 0, OPEN_EXISTING, 0, 0); \n    if (INVALID_HANDLE_VALUE == file) \n    { \n        DWORD err = GetLastError(); \n        char *str; \n        DWORD ret = FormatMessageA( \n            FORMAT_MESSAGE_FROM_SYSTEM|FORMAT_MESSAGE_ALLOCATE_BUFFER, \n            0, err, LANG_USER_DEFAULT, reinterpret_cast<LPSTR>(&str),  \n            0, 0); \n        cout << \"Error: \"<< str << endl; \n        LocalFree(str); \n    } \n    else \n    { \n        CloseHandle(file); \n    }\n```\n\n该代码试图打开一个不存在的文件，并获得与失败相关的错误值(这将是`ERROR_FILE_NOT_FOUND`的值)。然后代码调用`FormatMessage`函数来获取描述错误的字符串。函数的第一个参数是一个标志，指示函数应该如何工作；在这种情况下，`FORMAT_MESSAGE_FROM_SYSTEM`标志表示错误是系统错误，`FORMAT_MESSAGE_ALLOCATE_BUFFER`标志表示函数应该使用 Win32 `LocalAlloc`函数分配一个足够大的缓冲区来保存字符串。\n\nIf the error is a custom value that you have defined then you should use the `FORMAT_MESSAGE_FROM_HMODULE` flag, open the file with `LoadLibrary` and use the resulting `HMODULE` as the parameter passed in through the second parameter.\n\n第三个参数是错误信息编号(从`GetLastError`开始)，第四个参数是`LANGID`，表示要使用的语言标识(在本例中`LANG_USER_DEFAULT`获取当前登录用户的语言标识)。`FormatMessage`函数将生成一个格式化的错误值，该字符串可能有替换参数。格式化的字符串在缓冲区中返回，您有两个选项:您可以分配一个字符缓冲区，并将指针作为第五个参数传入，将长度作为第六个参数传入，或者您可以请求函数使用`LocalAlloc`函数分配一个缓冲区，如本例所示。要访问函数分配的缓冲区，需要通过第五个参数传递指针变量的*地址*。\n\n请注意，第五个参数用于获取指向用户分配的缓冲区的指针，或者返回系统分配的缓冲区的地址，这就是为什么在这种情况下，指向指针的指针必须被强制转换。\n\n有些格式字符串可能有参数，如果有，则通过第七个参数中的数组传递值(在这种情况下，不传递数组)。前面代码的结果是字符串:\n\n```cpp\n    Error: The system cannot find the file specified.\n```\n\n使用消息编译器、资源文件和`FormatMessage`，您可以提供一种机制，从您的函数返回错误值，然后根据当前区域设置将这些值转换为本地化字符串。\n\n# 使用消息编译器\n\n前面的示例表明，您可以获取 Win32 错误的本地化字符串，但是您也可以创建自己的错误，并提供作为资源绑定到您的进程或库的本地化字符串。如果您打算向最终用户报告错误，您必须确保描述是本地化的。Windows 提供了一个名为消息编译器(`mc.exe`)的工具，该工具将获取一个包含各种语言消息条目的文本文件，并将它们编译成可以绑定到模块的二进制资源。\n\n例如:\n\n```cpp\n    LanguageNames = (British = 0x0409:MSG00409) \n    LanguageNames = (French  = 0x040c:MSG0040C) \n\n    MessageId       = 1 \n    SymbolicName    = IDS_GREETING \n    Language        = English \n    Hello \n    . \n    Language        = British \n    Good day \n    . \n    Language        = French \n    Salut \n    .\n```\n\n这为同一消息定义了三个本地化字符串。这里的消息是简单的字符串，但是您可以使用运行时提供的占位符来定义消息格式。*中性*语言是美国英语，此外我们还为英国英语和法语定义了字符串。用于语言的名称在文件顶部的`LanguageNames`行中定义。这些条目具有稍后将在文件中使用的名称、语言的代码页以及包含消息资源的二进制资源的名称。\n\n`MessageId`是`FormatMessage`函数将使用的标识符，`SymbolicName`是将在头文件中定义的预处理器符号，因此您可以在 C++ 代码中使用该消息，而不是数字。这个文件是通过传递给命令行工具`mc.exe`来编译的，它将创建五个文件:一个带有符号定义的头文件，三个二进制源(`MSG00001.bin`，默认为中性语言创建，以及`MSG00409.bin`和`MSG0040C.bin,`，因为`LanguageNames`行而创建)，以及一个资源编译器文件。\n\n对于本例，资源编译器文件(扩展名为`.rc`)将包含:\n\n```cpp\n    LANGUAGE 0xc,0x1 \n    1 11 \"MSG0040C.bin\" \n    LANGUAGE 0x9,0x1 \n    1 11 \"MSG00001.bin\" \n    LANGUAGE 0x9,0x1 \n    1 11 \"MSG00409.bin\"\n```\n\n这是一个可以由 Windows SDK 资源编译器(`rc.exe`)编译的标准资源文件，它会将消息资源编译成`.res`文件，该文件可以绑定到可执行文件或 DLL。绑定了类型为`11`的资源的进程或动态链接库可以被`FormatMessage`函数用作描述性错误字符串的来源。\n\n通常，您不会使用消息 ID 1，因为它不太可能是唯一的，并且您可能想要利用*设施代码*和*严重性代码*(有关设施代码的详细信息，请查看`winerror.h`头文件)。此外，要指示消息不是 Windows，您可以在运行`mc.exe`时使用`/c`开关设置错误代码的客户位。这将意味着您的错误代码不会是像 1 这样的简单值，但这并不重要，因为您的代码将使用头文件中定义的符号。\n\n# C++ 异常\n\n顾名思义，例外是针对特殊情况的。它们不是正常情况。它们不是你想发生的情况，而是可能发生的情况。任何异常情况通常意味着您的数据将处于不一致的状态，因此使用异常意味着您需要从事务的角度来思考，也就是说，操作要么成功，要么对象的状态应该保持与尝试操作之前相同。当代码块中出现异常时，代码块中发生的一切都将无效。如果代码块是一个更宽的代码块的一部分(比如，一个函数是另一个函数的一系列函数调用)，那么在那个代码块中的工作将是无效的。这意味着异常可能会传播到调用堆栈更高层的其他代码块，从而使依赖于成功操作的对象失效。在某个时候，异常情况将是可恢复的，因此您将希望防止异常进一步发展。\n\n# 异常规格\n\nC++ 11 中不推荐使用异常规范，但是您可以在早期代码中看到它们。规范是通过应用于函数声明的`throw`表达式给出可以从函数中抛出的异常。`throw`规范可以是省略号，这意味着函数可以抛出异常，但类型没有指定。如果规范为空，则意味着函数不会抛出异常，这与在 C++ 11 中使用`noexcept`说明符是一样的。\n\n`noexcept`说明符告诉编译器不需要异常处理，因此如果函数中出现异常，异常不会从函数中冒泡出来，并且`terminate`函数将被立即调用。在这种情况下，不能保证自动对象的析构函数被调用。\n\n# C++ 异常语法\n\n在 C++ 中，异常情况是通过抛出异常对象而产生的。该异常对象可以是您喜欢的任何对象:对象、指针或内置类型，但是因为异常可能由其他人编写的代码处理，所以最好将用于表示异常的对象标准化。为此，标准库提供了`exception`类，可以作为基类使用。\n\n```cpp\n    double reciprocal(double d) \n    { \n        if (d == 0)  \n        { \n            // throw 0; \n            // throw \"divide by zero\"; \n            // throw new exception(\"divide by zero\"); \n            throw exception(\"divide by zero\"); \n        } \n        return 1.0 / d; \n    }\n```\n\n这段代码测试参数，如果它为零，则抛出一个异常。给出了四个例子，它们都是有效的 C++，但只有最后一个版本是可以接受的，因为它使用了一个标准库类(或从标准库类派生的一个)，并且遵循了按值抛出异常的惯例。\n\n当抛出异常时，异常处理基础结构接管。执行将在当前代码块中停止，异常将向上传播到调用堆栈。当异常在代码块中传播时，所有自动对象都将被销毁，但是在代码黑堆中创建的对象不会被销毁。这是一个称为**堆栈展开的过程，**在异常移动到调用堆栈中它上面的堆栈帧之前，尽可能地清理每个堆栈帧。如果异常没有被捕获，它将传播到`main`函数，此时将调用`terminate`函数来处理异常(因此它将终止进程)。\n\n您可以保护代码以处理传播的异常。代码由`try`块保护，并由相关的`catch`块捕获:\n\n```cpp\n    try  \n    { \n        string s(\"this is an object\"); \n        vector<int> v = { 1, 0, -1}; \n        reciprocal(v[0]); \n        reciprocal(v[1]); \n        reciprocal(v[2]); \n    } \n    catch(exception& e) \n    { \n        cout << e.what() << endl; \n    }\n```\n\n与 C++ 中的其他代码块不同，即使`try`和`catch`块包含单行代码，大括号也是强制性的。在前面的代码中，对`reciprocal`函数的第二次调用将引发异常。该异常将停止该块中任何更多代码的执行，因此不会发生对`reciprocal`函数的第三次调用。相反，异常会传播出代码块。`try`块是在大括号之间定义的对象的范围，这意味着这些对象的析构函数将被调用(`s`和`v`)。然后，控制被传递给相关的`catch`块，在这种情况下，只有一个处理程序。`catch`块是`try`块的独立块，因此您不能访问在`try`块中定义的任何变量。这很有意义，因为当生成异常时，整个代码块都被*污染了*，所以您不能信任在该块中创建的任何对象。这段代码使用公认的约定，即通过引用捕获异常，因此捕获的是实际的异常对象，而不是副本。\n\n惯例是:抛出我的值，被引用捕获。\n\n标准库提供了一个名为`uncaught_exception`的函数，如果已经抛出异常但尚未处理，该函数将返回`true`。能够对此进行测试似乎有些奇怪，因为当异常发生时，除了异常基础结构之外，不会调用任何代码(例如`catch`处理程序)，您应该将异常代码放在那里。然而*还有*当抛出异常时调用的其他代码:在堆栈清理期间被销毁的自动对象的析构函数。`uncaught_exception`函数应该在析构函数中使用，以确定对象是否由于异常而被销毁，而不是由于对象超出范围或被删除而被正常销毁。例如:\n\n```cpp\n    class test \n    { \n        string str; \n    public: \n        test() : str(\"\") {} \n        test(const string& s) : str(s) {} \n        ~test() \n        { \n            cout << boolalpha << str << \" uncaught exception = \" \n             << uncaught_exception() << endl; \n        } \n    };\n```\n\n这个简单的对象指示它是否因为异常堆栈展开而被销毁。可以这样测试:\n\n```cpp\n    void f(bool b) \n    { \n        test t(\"auto f\"); \n        cout << (b ? \"f throwing exception\" : \"f running fine\")  \n            << endl; \n        if (b) throw exception(\"f failed\"); \n    } \n\n    int main() \n    { \n        test t1(\"auto main\"); \n        try \n        { \n            test t2(\"in try in main\"); \n            f(false); \n            f(true); \n            cout << \"this will never be printed\"; \n        } \n        catch (exception& e) \n        { \n            cout << e.what() << endl; \n        } \n        return 0; \n    }\n```\n\n`f`函数只有在用`true`值调用时才会抛出异常。`main`函数调用`f`两次，一次使用`false`值(因此在`f`中不抛出异常)，第二次使用`true`。输出结果是:\n\n```cpp\n f running fine\n auto f uncaught exception = false\n f throwing exception\n auto f uncaught exception = true\n in try in main uncaught exception = true\n f failed\n auto main uncaught exception = false\n```\n\n第一次`f`被调用，`test`对象被正常破坏，所以`uncaught_exception`会返回`false`。第二次`f`调用的是函数中的`test`对象在异常被捕捉之前就被破坏了，所以`uncaught_exception`会返回`true`。由于抛出异常，执行离开`try`块，因此`try`块中的`test`对象被销毁，`uncaught_exception`将返回`true`。最后，当异常处理完毕，控制返回到`catch`块后的代码时，`main`函数中栈上创建的`test`对象将在`main`函数返回时被销毁，因此`uncaught_exception`将返回`false`。\n\n# 标准异常类\n\n`exception`类是一个简单的 C 字符串容器:该字符串作为构造函数参数传递，并可通过`what`访问器获得。标准库在`<exception>`库中声明了异常类，并且鼓励您从中派生自己的异常类。标准库提供以下派生类；大多数在`<stdexcept>`中定义。\n\n| **级** | **投掷** |\n| `bad_alloc` | 当`new`操作员无法分配内存时(在`<new>`中) |\n| `bad_array_new_length` | 当`new`运算符被要求创建一个长度无效的数组时(在`<new>`中) |\n| `bad_cast` | 当`dynamic_cast`到参考类型失败时(在`<typeinfo>`中) |\n| `bad_exception` | 出现了意外情况(在`<exception>`中) |\n| `bad_function_call` | 调用了一个空的`function`对象(在`<functional>`中) |\n| `bad_typeid` | 当`typeid`的自变量为空时(在`<typeinfo>`中) |\n| `bad_weak_ptr` | 当访问一个弱指针时，它指的是一个已经被破坏的对象(在`<memory>`中) |\n| `domain_error` | 当试图在定义操作的域之外执行操作时 |\n| `invalid_argument` | 当参数使用了无效值时 |\n| `length_error` | 当试图超过为对象定义的长度时 |\n| `logic_error` | 当存在逻辑错误时，例如，类不变量或前置条件 |\n| `out_of_range` | 当试图访问对象定义范围之外的元素时 |\n| `overflow_error` | 当计算得出的值大于目标类型时 |\n| `range_error` | 当计算得出的值超出该类型的范围时 |\n| `runtime_error` | 当错误发生在代码范围之外时 |\n| `system_error` | 包装操作系统错误的基类(在`<system_error>`中) |\n| `underflow_error` | 当计算导致下溢时 |\n\n上表中提到的所有类都有一个接受`const char*`或`const string&`参数的构造函数，这与接受 C 字符串的`exception`类不同(因此，如果描述是通过`string`对象传递的，基类是使用`c_str`方法构造的)。没有宽字符版本，所以如果你想从宽字符串构造一个异常描述，你必须转换它。此外，请注意，标准异常类只有一个构造函数参数，这可以通过继承的`what`访问器获得。\n\n关于异常可以保存的数据，没有绝对的规则。您可以从`exception`中派生一个类，并用您想要提供给异常处理程序的任何值来构造它。\n\n# 按类型捕获异常\n\n每个`try`块可以有多个`catch`块，这意味着您可以根据异常类型定制异常处理。`catch`条款中的参数类型将按照其声明的顺序对照异常类型进行测试。异常将由与异常类型匹配的第一个处理程序处理，或者是一个基类。这突出了通过引用捕获异常对象的约定。如果您将 catch 作为基类对象，将会创建一个副本，对派生类对象进行切片。在许多情况下，代码将抛出从`exception`类派生的类型的对象，因此这意味着`exception`的捕获处理程序将捕获所有异常。\n\n因为代码可以抛出任何对象，所以异常可能会传播出处理程序。C++ 允许你通过在`catch`子句中使用省略号来捕捉一切。显然，您应该将`catch`处理程序从派生最多的到派生最少的排序，并且(如果您使用它的话)在末尾使用省略号处理程序:\n\n```cpp\n    try  \n    { \n        call_code(); \n    } \n    catch(invalid_argument& iva) \n    { \n        cout << \"invalid argument: \" << e.what() << endl; \n    } \n    catch(exception& exc) \n    { \n        cout << typeid(exc).name() << \": \" << e.what() << endl; \n    } \n    catch(...) \n    { \n        cout << \"some other C++ exception\" << endl; \n    }\n```\n\n如果受保护的代码没有抛出异常，则不执行`catch`块。\n\n当您的处理程序检查异常时，它可能决定不抑制异常；这被称为重新抛出异常。为此，您可以使用不带操作数的`throw`语句(这仅在`catch`处理程序中允许)，该语句将重新引发被捕获的实际异常对象，而不是副本。\n\n异常是基于线程的，因此很难将异常传播到另一个线程。`exception_ptr`类(在`<exception>`中)为任何类型的异常对象提供共享所有权语义。您可以通过调用`make_exception_ptr`对象获得异常对象的共享副本，或者您甚至可以使用`current_exception`获得正在`catch`块中处理的异常的共享副本。两个函数都返回一个`exception_ptr`对象。一个`exception_ptr`对象可以保存任何类型的异常，而不仅仅是那些从`exception`类派生的异常，所以从包装的异常中获取信息是特定于异常类型的。`exception_ptr`对象对这些细节一无所知，因此您可以将其传递给想要使用共享异常(另一个线程)的上下文中的`rethrow_exception`，然后捕获适当的异常对象。在下面的代码中，有两个线程正在运行。`first_thread`功能在一个线程上运行，`second_thread`功能在另一个线程上运行:\n\n```cpp\n    exception_ptr eptr = nullptr; \n\n    void first_thread() \n    { \n        try  \n        { \n            call_code(); \n        } \n        catch (...)  \n        { \n            eptr = current_exception();  \n        } \n        // some signalling mechanism ... \n    } \n\n    void second_thread() \n    { \n        // other code \n\n        // ... some signalling mechanism \n        if (eptr != nullptr)  \n        { \n            try \n            { \n                rethrow_exception(eptr); \n            } \n            catch(my_exception& e) \n            { \n                // process this exception \n            } \n            eptr = nullptr; \n        } \n        // other code \n    }\n```\n\n前面的代码看起来像是在使用`exception_ptr`作为指针。事实上，`eptr`被创建为一个全局对象，`nullptr`的赋值使用复制构造函数来创建一个空对象(其中包装的异常是`nullptr`)。类似地，与`nullptr`的比较实际上测试了包装异常。\n\n这本书不是关于 C++ 线程的，所以我们不会详细讨论两个线程之间的信号传递。这段代码表明，一个异常的共享副本*任何异常*都可以存储在一个上下文中，然后在另一个上下文中重新抛出和处理。\n\n# 函数尝试块\n\n您可以决定使用`try`块保护整个函数，在这种情况下，您可以编写如下代码:\n\n```cpp\n    void test(double d) \n    { \n        try \n        { \n            cout << setw(10) << d << setw(10) << reciprocal(d) << endl; \n        } \n\n        catch (exception& e) \n        { \n            cout << \"error: \" << e.what() << endl; \n        } \n    }\n```\n\n这使用了前面定义的`reciprocal`函数，如果参数为零，该函数将抛出`exception`。另一种语法是:\n\n```cpp\n    void test(double d) \n    try \n    { \n        cout << setw(10) << d << setw(10) << reciprocal(d) << endl; \n    } \n    catch (exception& e) \n    { \n        cout << \"error: \" << e.what() << endl; \n    }\n```\n\n这看起来相当奇怪，因为函数原型后面紧跟着`try... catch`块，并且没有外部的大括号集。功能体是`try`块中的代码；当这段代码完成时，函数返回。如果函数返回值，它必须在`try`块中执行。在大多数情况下，您会发现这种语法会降低代码的可读性，但是有一种情况可能会有用——对于构造函数中的初始值设定项列表。\n\n```cpp\n    class inverse \n    { \n        double recip; \n    public: \n        inverse() = delete; \n        inverse(double d) recip(reciprocal(d)) {} \n        double get_recip() const { return recip; } \n    };\n```\n\n在这段代码中，我们包装了一个`double`值，它只是传递给构造函数的参数的倒数。通过调用初始化列表中的`reciprocal`函数来初始化数据成员。因为这是在构造函数体之外，所以这里发生的异常将直接传递给调用构造函数的代码。如果您想做一些额外的处理，那么您可以在构造函数体内调用倒数函数:\n\n```cpp\n    inverse::inverse(double d)  \n    {  \n        try { recip = reciprocal(d); } \n        catch(exception& e) { cout << \"invalid value \" << d << endl; } \n    }\n```\n\n需要注意的是，异常将被自动重新抛出，因为构造函数中的任何异常都意味着对象无效。但是，这确实允许您在必要时进行一些额外的处理。此解决方案不适用于基对象构造函数中引发的异常，因为尽管您可以在派生构造函数体中调用基构造函数，但编译器会自动调用默认构造函数。如果你想让编译器调用默认构造函数以外的构造函数，你必须在初始化列表中调用它。在`inverse`构造函数中提供异常代码的另一种语法是使用函数`try`块:\n\n```cpp\n    inverse::inverse(double d)  \n    try \n        : recip (reciprocal(d)) {}  \n    catch(exception& e) { cout << \"invalid value \" << d << endl; }\n```\n\n这看起来有点混乱，但是构造器主体仍然在初始化列表之后，给`recip`数据成员一个初始值。对`reciprocal`的调用中的任何异常都将被捕获，并在处理后自动重新抛出。初始化列表可以包含对基类和任何数据成员的调用，所有这些都将受到`try`块的保护。\n\n# 系统错误\n\n`<system_error>`库定义了一系列的类来封装系统错误。`error_category`类提供了一种将数字错误值转换成本地化描述字符串的机制。通过`<system_error>`中的`generic_category`和`system_category`功能可以获得两个对象，`<ios>`有一个名为`isostream_category`的功能；所有这些函数都返回一个`error_category`对象。`error_category`类有一个名为`message`的方法，该方法返回您作为参数传递的错误号的字符串描述。从`generic_category`函数返回的对象将返回 POSIX 错误的描述性字符串，因此您可以使用它来获取`errno`值的描述。从`system_category`函数返回的对象将通过使用`FORMAT_MESSAGE_FROM_SYSTEM`作为标志参数的 Win32 `FormatMessage`函数返回一个错误描述，因此这可用于获取`string`对象中的窗口错误消息的描述性消息。\n\nNote that `message` has no extra parameters to pass in values for a Win32 error message that takes parameters. Consequently, in those situations you will get back a message that has formatting placeholders.\n\n不管名称如何，`isostream_category`对象本质上返回与`generic_category`对象相同的描述。\n`system_error`例外是报告由其中一个`error_category`对象描述的值的类。例如，这是先前用于`FormatMessage`但使用`system_error`重写的示例:\n\n```cpp\n    HANDLE file = CreateFileA( \n       \"does_not_exist\", GENERIC_READ, 0, 0, OPEN_EXISTING, 0, 0); \n    if (INVALID_HANDLE_VALUE == file) \n    { \n        throw system_error(GetLastError(), system_category()); \n    } \n    else \n    { \n        CloseHandle(file); \n    }\n```\n\n这里使用的`system_error`构造函数将错误值作为第一个参数(一个从 Win32 函数`GetLastError`返回的`ulong`)和一个`system_category`对象，用于在调用`system_error::what`方法时将错误值转换为描述性字符串。\n\n# 嵌套异常\n\n一个`catch`块可以在没有任何操作数的情况下通过调用`throw`来重新引发当前异常，并且在调用堆栈中到达下一个`try`块之前将会有堆栈展开。您也可以重新抛出嵌套在另一个异常中的当前异常*。这是通过调用`<exception>`中的`throw_with_nested`函数并传递新的异常来实现的。该函数调用`current_exception`并将异常对象与参数一起包装在嵌套异常中，然后抛出。调用栈更高层的一个`try`块可以捕获这个异常，但它只能访问外部异常；它不能直接访问内部异常。相反，可以通过调用`rethrow_if_nested`来引发内部异常。例如，下面是打开文件的另一个版本的代码:*\n\n```cpp\n    void open(const char *filename) \n    { \n        try  \n        { \n            ifstream file(filename); \n            file.exceptions(ios_base::failbit); \n            // code if the file exists \n        } \n        catch (exception& e)  \n        { \n            throw_with_nested( \n                system_error(ENOENT, system_category(), filename)); \n        } \n    }\n```\n\n该代码打开一个文件，如果该文件不存在，则设置一个状态位(您可以稍后通过调用`rdstat`方法来测试这些位)。下一行表示抛出异常的类应该处理的状态位的值，在这种情况下提供`ios_base::failbit`。如果构造函数未能打开文件，则该位将被设置，因此`exceptions`方法将通过抛出异常来响应。在本例中，异常被捕获并包装到嵌套异常中。外部异常是一个`system_error`异常，用一个错误值`ENOENT`(这意味着文件不存在)和一个`error_category`对象来解释它，传递文件的名称作为附加信息。\n\n这个函数可以这样调用:\n\n```cpp\n    try \n    { \n        open(\"does_not_exist\"); \n    } \n    catch (exception& e) \n    { \n        cout << e.what() << endl; \n    }\n```\n\n这里捕获的异常是可以访问的，但是它只提供了关于外部对象的信息:\n\n```cpp\n does_not_exist: The system cannot find the file specified.\n```\n\n该消息由`system_error`对象使用传递给其构造器的附加信息和来自类别对象的描述来构造。要获取嵌套异常中的内部对象，您必须告诉系统通过调用`rethrow_if_nested`来抛出内部异常。因此，不打印外部异常，而是调用如下函数:\n\n```cpp\n    void print_exception(exception& outer) \n    { \n        cout << outer.what() << endl; \n        try { rethrow_if_nested(outer); } \n        catch (exception& inner) { print_exception(inner); } \n    }\n```\n\n这将打印外部异常的描述，然后调用`rethrow_if_nested,`，只有嵌套时才会抛出异常。如果是，它抛出内部异常，然后被捕获并递归调用`print_exception`函数。结果是:\n\n```cpp\n    does_not_exist: The system cannot find the file specified. \n    ios_base::failbit set: iostream stream error\n```\n\n最后一行是调用`ifstream::exception`方法时抛出的内部异常。\n\n# 结构化异常处理\n\nWindows 中的本机异常是**结构化异常处理** ( **SEH** )，Visual C++ 有一个语言扩展允许您捕捉这些异常。重要的是要明白，它们与 C++ 异常不一样，后者被编译器认为是*同步*，也就是说，编译器知道一个方法是否可能(或者确切地说，不会)抛出一个 C++ 异常，并且它在分析代码时使用这个信息。C++ 异常也是通过类型捕获的。SEH 不是一个 C++ 概念，因此编译器将结构化异常视为*异步*，这意味着它将 SEH 保护块中的任何代码视为可能引发结构化异常，因此编译器无法执行优化。异常代码也会捕获 SEH 异常。\n\nSEH 的语言扩展是微软 C/C++ 的扩展，也就是说，它们可以在 C 和 C++ 中使用，所以处理基础设施不知道对象析构函数。此外，当您捕获到 SEH 异常时，不会对堆栈或进程的任何其他部分的状态做出任何假设。\n\n虽然大多数 Windows 函数会以适当的方式捕获内核生成的 SEH 异常，但有些函数会故意允许它们传播(例如，**远程过程调用** ( **RPC** )函数，或用于内存管理的函数)。对于一些窗口函数，您可以明确请求使用 SEH 异常来处理错误。例如，`HeapCreate`函数集将允许一个 Windows 应用创建一个私有堆，并且您可以传递`HEAP_GENERATE_EXCEPTIONS`标志来指示在创建堆以及在私有堆中分配或重新分配内存时的错误将生成一个 SEH 异常。这是因为调用这些函数的开发人员可能认为故障非常严重，不可恢复，因此流程应该终止。由于 SEH 是一个如此严重的情况，你应该仔细审查是否是适当的(这并不是完全不可能的)做更多的报告细节的例外和终止该进程。\n\nSEH 异常本质上是低级操作系统异常，但是熟悉语法很重要，因为它看起来类似于 C++ 异常。例如:\n\n```cpp\n    char* pPageBuffer; \n    unsigned long curPages = 0; \n    const unsigned long PAGESIZE = 4096; \n    const unsigned long PAGECOUNT = 10; \n\n    int main() \n    { \n        void* pReserved = VirtualAlloc( \n        nullptr, PAGECOUNT * PAGESIZE, MEM_RESERVE, PAGE_NOACCESS); \n        if (nullptr == pReserved)  \n        { \n            cout << \"allocation failed\" << endl; \n            return 1; \n        } \n\n        char *pBuffer = static_cast<char*>(pReserved); \n        pPageBuffer = pBuffer; \n\n        for (int i = 0; i < PAGECOUNT * PAGESIZE; ++ i) \n        { \n            __try { pBuffer[i] = 'X'; } __except (exception_filter(GetExceptionCode())) { cout << \"Exiting process.n\"; ExitProcess(GetLastError()); } \n        } \n        VirtualFree(pReserved, 0, MEM_RELEASE); \n        return 0; \n    }\n```\n\n这里突出显示了 SEH 异常代码。这段代码使用了 Windows 的`VirtualAlloc`功能来保留一定数量的内存页面。保留不会分配内存，该操作必须在一个名为**的单独操作中执行，提交内存**。Windows 将在名为**页面**的块中保留(并提交)内存，在大多数系统中，一个页面是 4096 字节，如这里所假设的。对`VirtualAlloc`函数的调用表明它应该保留 10 页 4096 字节的内容，这些内容将在以后提交(和使用)。\n\n`VirtualAlloc`的第一个参数表示内存的位置，但是由于我们保留内存，这并不重要，所以`nullptr`被传递。如果保留成功，那么指针返回到内存。`for`循环只是一次向内存中写入一个字节的数据。突出显示的代码通过结构化异常处理来保护这种内存访问。受保护的块以`__try`关键字开始。当一个 SEH 被提出，执行传递到`__except`块。这与 C++ 异常中的`catch`块非常不同。首先，`__except`异常处理程序接收三个值中的一个来指示它应该如何表现。只有当这是`EXCEPTION_EXECUTE_HANDLER`时，才会运行处理程序块中的代码(在该代码中，突然关闭进程)。如果该值为`EXCEPTION_CONTINUE_SEARCH`，则异常未被识别，搜索将继续向上堆栈，*，但不展开 C++ 堆栈*。令人惊讶的值是`EXCEPTION_CONTINUE_EXECUTION,`，因为这取消了异常，并且`__try`块中的执行将继续。*你不能用 C++ 异常*来做这个。通常，SEH 代码将使用异常过滤函数来确定`__except`处理程序需要什么动作。在这段代码中，这个过滤器被称为`exception_filter,`，它被传递了通过调用 Windows 函数`GetExceptionCode`获得的异常代码。这个语法很重要，因为这个函数只能在`__except`上下文中调用。\n\n循环第一次运行时，不会提交任何内存，因此写入内存的代码将引发异常:页面错误。执行将传递给异常处理程序并通过`exception_filter`:\n\n```cpp\n    int exception_filter(unsigned int code) \n    { \n        if (code != EXCEPTION_ACCESS_VIOLATION) \n        { \n            cout << \"Exception code = \" << code << endl; \n            return EXCEPTION_EXECUTE_HANDLER; \n        } \n\n        if (curPage >= PAGECOUNT) \n        { \n            cout << \"Exception: out of pages.n\"; \n            return EXCEPTION_EXECUTE_HANDLER; \n        } \n\n        if (VirtualAlloc(static_cast<void*>(pPageBuffer), PAGESIZE, \n         MEM_COMMIT, PAGE_READWRITE) == nullptr) \n        { \n            cout << \"VirtualAlloc failed.n\"; \n            return EXCEPTION_EXECUTE_HANDLER; \n        } \n\n        curPage++ ; \n        pPageBuffer += PAGESIZE; \n        return EXCEPTION_CONTINUE_EXECUTION; \n    }\n```\n\n在 SEH 代码中，重要的是只处理您知道的异常，并且只有在您知道条件已经完全解决的情况下才使用异常。如果您访问尚未提交的 Windows 内存，操作系统会生成一个称为页面错误的异常。在这段代码中，测试异常代码，看它是否是页面错误，如果不是，过滤器返回，告诉异常处理程序在终止进程的异常处理程序块中运行代码。如果异常是页面错误，那么我们可以提交下一页。首先，有一个测试，看看页码是否在我们将使用的范围内(如果不在，则关闭该过程)。然后，通过再次调用`VirtualAlloc`提交下一页，以识别要提交的页和该页中的字节数。如果函数成功，它将返回一个指向已提交页面的指针或一个空值。只有在提交页面成功的情况下，过滤器才会返回一个值`EXCEPTION_CONTINUE_EXECUTION`，表示异常已经被处理，并且可以在异常被引发时继续执行。这段代码是使用`VirtualAlloc`的标准方式，因为它意味着只有在需要的时候才会提交内存页面。\n\nSEH 也有终止处理器的概念。当执行通过调用`return`离开`__try`代码块时，或者通过完成代码块中的所有代码，或者通过调用微软扩展`__leave`指令，或者已经引发了 SEH，则调用标有`__finally`的终止处理程序代码块。由于无论如何退出`__try`块，终止处理程序总是被调用，因此可以将其用作释放资源的一种方式。但是，因为 SEH 不进行 C++ 堆栈展开(也不调用析构函数)，这意味着您不能在具有 C++ 对象的函数中使用这些代码。事实上，编译器会拒绝编译具有 SEH 并创建了 C++ 对象的函数，无论是在函数堆栈上还是在堆上分配。(但是，您可以使用全局对象或在调用函数中分配并作为参数传入的对象。)构造`__try` / `__finally`看起来很有用，但是受限于不能在创建 C++ 对象的代码中使用它的要求。\n\n# 编译器异常开关\n\n在这一点上，值得解释一下为什么用`/EHsc`开关编译代码。简单的答案是，如果您不使用此开关，编译器将从标准库代码中发出警告，并且由于标准库使用异常，您必须使用`/EHsc`开关。警告告诉你这样做，所以这就是你要做的。\n\n长答案是`/EH`开关有三个参数可以用来影响异常的处理方式。使用`s`参数告诉编译器为同步异常提供基础结构，也就是说，可以在`try`块中抛出并在`catch`块中处理的 C++ 异常，以及调用自动 C++ 对象析构函数的堆栈展开异常。`c`参数表明`extern C`函数(也就是所有的 Windows SDK 函数)从不抛出 C++ 异常(因此编译器可以进行额外的优化)。因此，您可以使用`/EHs`或`/EHsc`编译标准库代码，但后者将生成更优化的代码。还有一个额外的参数，其中`/EHa`表示代码将通过`try` / `catch`块捕获*同步和异步异常(SEH)。*\n\n *# 混合使用 C++ 和 SEH 异常处理\n\n`RaiseException`窗口函数将抛出一个 SEH 异常。第一个参数是异常代码，第二个参数表示处理完这个异常后流程是否可以继续(`0`表示可以)。第三和第四个参数给出了关于异常的附加信息。第四个参数是指向带有这些附加参数的数组的指针，参数的数量在第三个参数中给出。\n\n有了`/EHa`，你可以这样写代码:\n\n```cpp\n    try  \n    { \n        RaiseException(1, 0, 0, nullptr); \n    } \n    // legal code, but don't do it \n    catch(...) \n    { \n        cout << \"SEH or C++ exception caught\" << endl; \n    }\n```\n\n这段代码的问题在于它处理所有 SEH 异常。这是非常危险的，因为一些 SEH 异常可能表明进程状态被破坏，所以进程继续是危险的。C 运行时库提供了一个名为`_set_se_translator`的函数，该函数提供了一种机制来指示哪些 SEH 异常由`try`处理。这个函数由您用这个原型编写的函数传递一个指针:\n\n```cpp\n    void func(unsigned int, EXCEPTION_POINTERS*);\n```\n\n第一个参数是异常代码(将从`GetExceptionCode`函数返回)，第二个参数是从`GetExceptionInformation`函数返回，并且具有与异常相关联的任何附加参数(例如，通过`RaiseException`中的第三和第四个参数传递的参数)。您可以使用这些值抛出一个 C++ 异常来代替 SEH。如果您提供此功能:\n\n```cpp\n    void seh_to_cpp(unsigned int code, EXCEPTION_POINTERS*) \n    { \n        if (code == 1) throw exception(\"my error\"); \n    }\n```\n\n现在，您可以在处理 SEH 异常之前注册该函数:\n\n```cpp\n    _set_se_translator(seh_to_cpp); \n    try  \n    { \n        RaiseException(1, 0, 0, nullptr); \n    } \n    catch(exception& e) \n    { \n        cout << e.what() << endl; \n    }\n```\n\n在这段代码中，`RaiseException`函数使用值 1 提升一个自定义 SEH。这个翻译也许不是最有用的，但它说明了这一点。`winnt.h`头文件定义了可以在窗口代码中引发的标准 SEH 异常的异常代码。更有用的翻译功能是:\n\n```cpp\n    double reciprocal(double d) \n    { \n        return 1.0 / d; \n    } \n\n    void seh_to_cpp(unsigned int code, EXCEPTION_POINTERS*) \n    { \n        if (STATUS_FLOAT_DIVIDE_BY_ZERO == code || \n            STATUS_INTEGER_DIVIDE_BY_ZERO == code) \n        { \n            throw invalid_argument(\"divide by zero\"); \n        } \n    }\n```\n\n这允许您调用如下倒数函数:\n\n```cpp\n    _set_se_translator(seh_to_cpp); \n    try  \n    { \n        reciprocal(0.0); \n    } \n    catch(invalid_argument& e) \n    { \n        cout << e.what() << endl; \n    }\n```\n\n# 编写异常安全类\n\n一般来说，当您编写类时，您应该确保保护类的用户免受异常的影响。异常不是错误传播机制。如果类中的一个方法失败了，但是可以恢复(对象状态保持一致)，那么您应该使用返回值(很可能是错误代码)来指示这一点。异常是针对异常情况的，这些情况使数据无效，并且在引发异常时，情况是不可恢复的。\n\n当代码中出现异常时，您有三种选择。首先，您可以允许异常在调用堆栈中向上传播，并将处理异常的责任放在调用代码上。这意味着您调用代码时没有`try`块的保护，即使代码被记录为能够抛出异常。在这种情况下，您必须确信异常对调用代码是有意义的。例如，如果您的类被记录为网络类，并使用临时文件来缓冲从网络接收的一些数据，则如果文件访问代码引发异常，则异常对象对于调用您的代码的代码没有意义，因为该客户端代码认为您的类是关于访问网络数据的，而不是文件数据。但是，如果网络代码抛出了一个错误，允许这些异常传播到调用代码可能是有意义的，尤其是当它们涉及到需要外部操作的错误时(例如，网络电缆被拔掉或存在安全问题)。\n\n在这种情况下，你可以应用你的第二个选项，那就是用一个`try`块保护可以抛出异常的代码，捕捉已知的异常，抛出一个更合适的异常，或许可以嵌套原来的异常，这样调用的代码就可以做更详细的分析。如果异常对您的调用代码有意义，您可以允许它传播出去，但是捕获原始异常允许您在重新抛出它之前采取额外的操作。\n\n使用缓冲的网络数据示例，您可以确定，由于文件缓冲中存在错误，这意味着您无法读取更多的网络数据，因此您的异常处理代码应该以适当的方式关闭网络访问。错误发生在文件代码中，而不是网络代码中，因此突然关闭网络是不合理的，允许当前网络操作完成(但忽略数据)更有意义，这样就不会将错误传播回网络代码。\n\n最后一个选项是用`try`块保护所有代码，并捕获和消费异常，这样调用代码就可以在不抛出异常的情况下完成。有两种情况是合适的。首先，错误可能是可恢复的，因此在`catch`条款中，您可以采取措施解决问题。在缓冲网络数据示例中，当打开临时文件时，如果您得到一个错误，即具有所请求名称的文件已经存在，您可以简单地使用另一个名称，然后重试。代码的用户不需要知道发生了这个问题(尽管跟踪这个错误可能是有意义的，这样您就可以在代码的测试阶段调查这个问题)。如果错误不可恢复，那么使对象的状态无效并返回错误代码可能更有意义。\n\n您的代码应该利用 C++ 异常基础结构的行为，这保证了自动对象被销毁。因此，当您使用内存或其他适当的资源时，您应该尽可能地将它们包装在智能指针中，以便在抛出异常时，智能指针析构函数释放资源。使用资源获取是初始化(RAII)的类有`vector`、`string`、`fstream,`和`make_shared`函数，所以如果对象构造(或函数调用)成功，就意味着资源已经获取，可以通过这些对象使用资源。这些类也是**资源释放销毁** ( **RRD** )，意思是对象销毁时释放资源。智能指针类`unique_ptr`和`shared_ptr`不是 RAII，因为它们只是包装资源，资源的分配由其他代码单独执行。但是，这些类是 RRD 类，所以可以确信，如果抛出异常，资源就会被释放。\n\n异常处理可以提供三个级别的异常安全性。秤最安全的级别是*无故障*方法和功能。这是不抛出异常并且不允许异常传播的代码。这样的代码将保证类不变量得到维护，对象状态保持一致。没有失败的代码不是通过简单地捕获所有异常并使用它们来实现的，相反，您必须保护所有代码并捕获和处理所有异常，以确保对象保持一致的状态。\n\n所有内置的 C++ 类型都是无故障的。您还可以保证所有的标准库类型都有不失败的析构函数，但是由于容器会在实例被销毁时调用包含的对象析构函数，这意味着您必须确保您写入容器的类型也有不失败的析构函数。\n\n编写不失败类型可能涉及相当详细的代码，所以另一个选项是*强保证*。这样的代码会抛出异常，但是它们确保没有内存泄漏，并且当抛出异常时，对象将处于与调用方法时相同的状态。这本质上是一个事务性操作:要么修改对象，要么保持不变，就好像没有尝试执行该操作一样。在大多数情况下方法，这将提供一个*异常安全的基本保证*。在这种情况下，可以保证无论发生什么都不会泄漏内存，但是当抛出异常时，对象可能会处于不一致的状态，因此调用代码应该通过丢弃对象来处理异常。\n\n文档很重要。如果对象方法标有`throw`或`noexcept`，那么你就知道它是无故障的。只有在文档中这样说的情况下，您才应该承担强有力的保证。否则，您可以假设对象将具有异常安全的基本保证，并且如果抛出异常，则对象无效。\n\n# 摘要\n\n当您编写 C++ 代码时，您应该始终用一只眼睛来观察代码的测试和调试。防止需要调试代码的理想方法是编写健壮、设计良好的代码。理想很难实现，所以最好编写易于诊断问题和调试的代码。C 运行时和 C++ 标准库提供了广泛的工具，使您能够跟踪和报告问题，并且通过错误代码处理和异常，您拥有丰富的工具集合来报告和处理函数故障。\n\n读完这本书，你应该知道 C++ 语言和标准库提供了一种丰富、灵活和强大的代码编写方式。更重要的是，一旦你知道如何使用该语言及其库，使用 C++ 是一种乐趣。**"
  },
  {
    "path": "docs/mod-cpp/08.md",
    "content": "# 八、学习现代核心语言特性\n\n本章包含的配方如下:\n\n*   尽可能使用自动\n*   创建类型别名和别名模板\n*   理解统一初始化\n*   了解各种形式的非静态成员初始化\n*   控制和查询对象对齐\n*   使用限定范围的枚举\n*   对虚拟方法使用重写和 final\n*   使用基于范围的 for 循环在范围上迭代\n*   为自定义类型的循环启用基于范围的\n*   使用显式构造函数和转换运算符来避免隐式转换\n*   使用未命名的命名空间代替静态全局\n*   使用内联命名空间进行符号版本控制\n*   使用结构化绑定处理多返回值\n\n# 尽可能使用自动\n\n自动类型演绎是现代 C++ 中最重要、应用最广泛的特性之一。新的 C++ 标准使得在各种上下文中使用`auto`作为类型的占位符成为可能，并让编译器推导出实际的类型。在 C++ 11 中，`auto`可用于声明局部变量和带有尾随返回类型的函数的返回类型。在 C++ 14 中，`auto`可以用于函数的返回类型，而无需指定尾随类型，也可以用于 lambda 表达式中的参数声明。\n\n未来的标准版本可能会将`auto`的使用范围扩大到更多的情况。在这些环境中使用`auto`有几个重要的好处。开发商应该意识到这一点，只要有可能就更喜欢`auto`。安德烈·亚历山德雷斯库为此创造了一个实际的术语，并由赫伯·萨特推广- *几乎总是自动* ( *AAA* )。\n\n# 怎么做...\n\n在以下情况下，考虑使用`auto`作为实际类型的占位符:\n\n*   当您不想提交到特定类型时，要以`auto name = expression`形式声明局部变量:\n\n```cpp\n        auto i = 42;          // int \n        auto d = 42.5;        // double \n        auto s = \"text\";      // char const * \n        auto v = { 1, 2, 3 }; // std::initializer_list<int> \n```\n\n*   当需要提交到特定类型时，用`auto name = type-id { expression }`形式声明局部变量:\n\n```cpp\n        auto b  = new char[10]{ 0 };            // char* \n        auto s1 = std::string {\"text\"};         // std::string\n        auto v1 = std::vector<int> { 1, 2, 3 }; // std::vector<int>\n        auto p  = std::make_shared<int>(42);    // std::shared_ptr<int>\n```\n\n*   以`auto name = lambda-expression`形式声明命名的 lambda 函数，除非 lambda 需要被传递或返回到函数:\n\n```cpp\n        auto upper = [](char const c) {return toupper(c); };\n```\n\n*   要声明 lambda 参数和返回值:\n\n```cpp\n        auto add = [](auto const a, auto const b) {return a + b;};\n```\n\n*   当您不想提交到特定类型时，要声明函数返回类型:\n\n```cpp\n        template <typename F, typename T> \n        auto apply(F&& f, T value) \n        { \n          return f(value); \n        }\n```\n\n# 它是如何工作的...\n\n`auto`说明符基本上是一个实际类型的占位符。使用`auto`时，编译器从以下实例推导出实际类型:\n\n*   从用于初始化变量的表达式类型来看，当`auto`用于声明变量时。\n*   从尾随返回类型或函数返回表达式的类型，当`auto`用作函数返回类型的占位符时。\n\n在某些情况下，有必要提交到特定类型。例如，在前面的例子中，编译器将`s`的类型推导为`char const *`。如果目的是有一个`std::string`，那么类型必须明确指定。同样的，`v`的类型被演绎为`std::initializer_list<int>`。然而，其意图可能是拥有一台`std::vector<int>`。在这种情况下，必须在赋值的右侧显式指定类型。\n\n使用自动说明符代替实际类型有一些重要的好处；以下可能是最重要的几个:\n\n*   不能让变量保持未初始化状态。这是开发人员在声明指定实际类型的变量时经常犯的错误，但是`auto`不可能需要初始化变量才能推导出类型。\n*   使用`auto`确保您始终使用正确的类型，并且不会发生隐式转换。考虑下面的例子，我们检索一个局部变量的向量大小。在第一种情况下，变量的类型是`int`，尽管`size()`方法返回`size_t`。这意味着从`size_t`到`int`的隐性转换将会发生。但是，对类型使用`auto`会推导出正确的类型，即`size_t`:\n\n```cpp\n        auto v = std::vector<int>{ 1, 2, 3 }; \n        int size1 = v.size();       \n        // implicit conversion, possible loss of data \n        auto size2 = v.size(); \n        auto size3 = int{ v.size() };  // ill-formed (warning in gcc/clang, error in VC++)\n```\n\n*   使用`auto`可以促进良好的面向对象实践，比如更喜欢接口而不是实现。指定的类型数量越少，代码就越通用，对未来的变化也更开放，这是面向对象编程的一个基本原则。\n*   这意味着少打字，少关心我们不关心的实际类型。很多时候，即使我们显式地指定了类型，我们实际上并不关心它。迭代器是一个非常常见的情况，但是我们可以想到更多。当您想要迭代一个范围时，您不关心迭代器的实际类型。你只对迭代器本身感兴趣；因此，使用`auto`可以节省用于键入可能很长的名称的时间，并帮助您专注于实际代码，而不是键入名称。在下面的例子中，在第一个`for`循环中，我们显式地使用迭代器的类型。需要键入大量的文本，长语句实际上会降低代码的可读性，您还需要知道实际上并不关心的类型名称。带有`auto`说明符的第二个循环看起来更简单，让您不必键入和关心实际类型。\n\n```cpp\n        std::map<int, std::string> m; \n        for (std::map<int,std::string>::const_iterator it = m.cbegin();\n          it != m.cend(); ++ it) \n        { /*...*/ } \n\n        for (auto it = m.cbegin(); it != m.cend(); ++ it)\n        { /*...*/ }\n```\n\n*   用`auto`声明变量提供了一致的编码风格，类型总是在右边。如果动态分配对象，需要在赋值的左侧和右侧都写类型，例如`int* p = new int(42)`。使用`auto`，类型只在右侧指定一次。\n\n但是，使用`auto`时有一些问题:\n\n*   `auto`说明符只是类型的占位符，而不是`const` / `volatile`和引用说明符。如果需要`const` / `volatile`和/或引用类型，则需要明确指定它们。在以下示例中，`foo.get()`返回对`int`的引用；从返回值初始化变量`x`时，编译器推导出的类型是`int`，而不是`int&`。因此，对`x`的任何更改都不会传播到`foo.x_`。为此，应使用`auto&`:\n\n```cpp\n        class foo { \n          int x_; \n        public: \n          foo(int const x = 0) :x_{ x } {} \n          int& get() { return x_; } \n        }; \n\n        foo f(42); \n        auto x = f.get(); \n        x = 100; \n        std::cout << f.get() << std::endl; // prints 42\n```\n\n*   不可移动的类型不能使用`auto`:\n\n```cpp\n        auto ai = std::atomic<int>(42); // error\n```\n\n*   多字类型不能使用自动，如`long long`、`long double`或`struct foo`。但是，在第一种情况下，可能的解决方法是使用文字或类型别名；至于第二个，为了 C 兼容性，只有在 C++ 中支持以这种形式使用`struct` / `class`，无论如何都应该避免:\n\n```cpp\n        auto l1 = long long{ 42 }; // error \n        auto l2 = llong{ 42 };     // OK \n        auto l3 = 42LL;            // OK\n```\n\n*   如果您使用`auto`说明符，但仍然需要知道类型，您可以在任何 IDE 中这样做，例如将光标放在变量上。然而，如果您离开 IDE，那就不可能了，知道实际类型的唯一方法是自己从初始化表达式中推导出来，这可能意味着在代码中搜索函数返回类型。\n\n`auto`可用于指定函数的返回类型。在 C++ 11 中，这要求在函数声明中有一个尾随返回类型。在 C++ 14 中，这一点已经放宽，返回值的类型由编译器从`return`表达式中推导出来。如果有多个返回值，它们应该具有相同的类型:\n\n```cpp\n    // C++ 11 \n    auto func1(int const i) -> int \n    { return 2*i; } \n\n    // C++ 14 \n    auto func2(int const i) \n    { return 2*i; }\n```\n\n如前所述，`auto`不保留`const` / `volatile`和参考限定词。这导致了`auto`作为函数返回类型的占位符的问题。为了解释这一点，让我们用`foo.get()`来考虑前面的例子。这次我们有一个叫做`proxy_get()`的包装函数，它引用一个`foo`，调用`get()`，并返回`get()`返回的值，这是一个`int&`。但是编译器会将`proxy_get()`的返回类型推导为`int`，而不是`int&`。尝试将该值赋给`int&`失败，出现错误:\n\n```cpp\n    class foo \n    { \n      int x_; \n    public: \n      foo(int const x = 0) :x_{ x } {} \n      int& get() { return x_; } \n    }; \n\n    auto proxy_get(foo& f) { return f.get(); } \n\n    auto f = foo{ 42 }; \n    auto& x = proxy_get(f); // cannot convert from 'int' to 'int &'\n```\n\n要解决这个问题，我们需要真正返回`auto&`。然而，这是模板和完美转发返回类型的问题，而不知道那是值还是引用。C++ 14 中这个问题的解决方案是`decltype(auto)`，它将正确推导出类型:\n\n```cpp\n    decltype(auto) proxy_get(foo& f) { return f.get(); } \n    auto f = foo{ 42 }; \n    decltype(auto) x = proxy_get(f);\n```\n\n最后一个可以使用`auto`的重要情况是使用 lambdas。从 C++ 14 开始，lambda 返回类型和 lambda 参数类型都可以是`auto`。这样的 lambda 被称为*通用 lambda* ，因为 lambda 定义的闭包类型有一个模板化的调用操作符。下面显示了一个通用的 lambda，它接受两个`auto`参数，并返回对实际类型应用`operator+`的结果:\n\n```cpp\n    auto ladd = [] (auto const a, auto const b) { return a + b; }; \n    struct \n    { \n       template<typename T, typename U> \n       auto operator () (T const a, U const b) const { return a+b; } \n    } L;\n```\n\n这个λ可以用来添加任何定义了`operator+`的东西。在下面的例子中，我们使用 lambda 来添加两个整数并连接到`std::string`对象(使用 C++ 14 用户定义的文字`operator \"\"s`):\n\n```cpp\n    auto i = ladd(40, 2);            // 42 \n    auto s = ladd(\"forty\"s, \"two\"s); // \"fortytwo\"s\n```\n\n# 请参见\n\n*   *创建类型别名和别名模板*\n*   *了解统一初始化*\n\n# 创建类型别名和别名模板\n\n在 C++ 中，可以创建同义词来代替类型名。这是通过创建一个`typedef`声明来实现的。这在一些情况下很有用，例如为类型创建更短或更有意义的名称，或者为函数指针创建名称。但是，`typedef`声明不能与模板一起使用来创建`template type aliases`。例如，`std::vector<T>`不是一种类型(`std::vector<int>`是一种类型)，而是当类型占位符`T`被实际类型替换时可以创建的所有类型的一种族。\n\n在 C++ 11 中，类型别名是另一个已声明类型的名称，别名模板是另一个已声明模板的名称。这两种类型的别名都引入了新的`using`语法。\n\n# 怎么做...\n\n*   使用形式`using identifier = type-id`创建类型别名，如下例所示:\n\n```cpp\n        using byte    = unsigned char; \n        using pbyte   = unsigned char *; \n        using array_t = int[10]; \n        using fn      = void(byte, double); \n\n        void func(byte b, double d) { /*...*/ } \n\n        byte b {42}; \n        pbyte pb = new byte[10] {0}; \n        array_t a{0,1,2,3,4,5,6,7,8,9}; \n        fn* f = func;\n```\n\n*   使用表单`template<template-params-list> identifier = type-id`创建别名模板，如下例所示:\n\n```cpp\n        template <class T> \n        class custom_allocator { /* ... */}; \n\n        template <typename T> \n        using vec_t = std::vector<T, custom_allocator<T>>; \n\n        vec_t<int>           vi; \n        vec_t<std::string>   vs; \n```\n\n为了保持一致性和可读性，您应该执行以下操作:\n\n*   不要混合使用`typedef`和`using`声明来创建别名。\n*   使用`using`语法创建函数指针类型的名称。\n\n# 它是如何工作的...\n\n`typedef`声明引入了一个类型的同义词(或者别名)。它没有引入另一种类型(如`class`、`struct`、`union`或`enum`声明)。通过`typedef`声明引入的类型名称遵循与标识符名称相同的隐藏规则。它们也可以重新声明，但只能引用同一类型(因此，您可以有多个有效的`typedef`声明，只要是同一类型的同义词，就可以在翻译单元中引入同一类型名称同义词)。以下是`typedef`声明的典型示例:\n\n```cpp\n    typedef unsigned char   byte; \n    typedef unsigned char * pbyte; \n    typedef int             array_t[10]; \n    typedef void(*fn)(byte, double); \n\n    template<typename T> \n    class foo { \n      typedef T value_type; \n    }; \n\n    typedef std::vector<int> vint_t;\n```\n\n类型别名声明相当于`typedef`声明。它可以出现在块范围、类范围或命名空间范围中。根据 C++ 11 第 7.1.3.2 段:\n\nA typedef-name can also be introduced by an alias-declaration. The identifier following the using keyword becomes a typedef-name and the optional attribute-specifier-seq following the identifier appertains to that typedef-name. It has the same semantics as if it were introduced by the typedef specifier. In particular, it does not define a new type and it shall not appear in the type-id.\n\n然而，当涉及到为数组类型和函数指针类型创建别名时，别名声明对于别名的实际类型更加易读和清晰。在*的例子中如何做...*部分，很容易理解`array_t`是 10 个整数的类型数组的名称，`fn`是取类型`byte`和`double`两个参数并返回`void`的函数类型的名称。这也与声明`std::function`对象的语法一致(例如`std::function<void(byte, double)> f`)。\n\n新语法的驱动目的是定义别名模板。这些模板在专门化时相当于用别名模板的模板参数替换`type-id`中模板参数的结果。\n\n请务必注意以下事项:\n\n*   别名模板不能部分或显式专门化。\n*   推导模板参数时，别名模板从不通过模板参数推导来推导。\n*   专门化别名模板时生成的类型不允许直接或间接使用自己的类型。\n\n# 理解统一初始化\n\n括号初始化是 C++ 11 中初始化数据的统一方法。为此也叫*统一初始化*。它可以说是 C++ 11 中开发人员应该理解和使用的最重要的特性之一。它消除了初始化基本类型、聚合和非聚合类型以及数组和标准容器之间的区别。\n\n# 准备好\n\n为了继续这个方法，您需要熟悉直接初始化和复制初始化，前者从一组显式构造函数参数初始化对象，后者从另一个对象初始化对象。以下是这两种初始化类型的简单示例，但要了解更多详细信息，您应该会看到其他资源:\n\n```cpp\n    std::string s1(\"test\");   // direct initialization \n    std::string s2 = \"test\";  // copy initialization\n```\n\n# 怎么做...\n\n无论对象的类型如何，要统一初始化对象，请使用大括号初始化形式`{}`，它既可用于直接初始化，也可用于复制初始化。当与括号初始化一起使用时，这些称为直接列表和复制列表初始化。\n\n```cpp\n    T object {other};   // direct list initialization \n    T object = {other}; // copy list initialization\n```\n\n统一初始化的示例如下:\n\n*   标准容器:\n\n```cpp\n        std::vector<int> v { 1, 2, 3 };\n        std::map<int, std::string> m { {1, \"one\"}, { 2, \"two\" }};\n```\n\n*   动态分配的阵列:\n\n```cpp\n        int* arr2 = new int[3]{ 1, 2, 3 };    \n```\n\n*   数组:\n\n```cpp\n        int arr1[3] { 1, 2, 3 }; \n```\n\n*   内置类型:\n\n```cpp\n        int i { 42 };\n        double d { 1.2 };    \n```\n\n*   用户定义的类型:\n\n```cpp\n        class foo\n        {\n          int a_;\n          double b_;\n        public:\n          foo():a_(0), b_(0) {}\n          foo(int a, double b = 0.0):a_(a), b_(b) {}\n        }; \n\n        foo f1{}; \n        foo f2{ 42, 1.2 }; \n        foo f3{ 42 };\n```\n\n*   用户定义的 POD 类型:\n\n```cpp\n        struct bar { int a_; double b_;};\n        bar b{ 42, 1.2 };\n```\n\n# 它是如何工作的...\n\n在 C++ 11 之前，对象根据其类型需要不同类型的初始化:\n\n*   基本类型可以使用赋值来初始化:\n\n```cpp\n        int a = 42; \n        double b = 1.2;\n```\n\n*   如果类对象有一个转换构造函数(在 C++ 11 之前，具有单个参数的构造函数被称为*转换构造函数*)，那么也可以使用从单个值赋值来初始化类对象:\n\n```cpp\n        class foo \n        { \n          int a_; \n        public: \n          foo(int a):a_(a) {} \n        }; \n        foo f1 = 42;\n```\n\n*   当提供参数时，非聚合类可以用圆括号(函数形式)初始化，而当执行默认初始化(调用默认构造函数)时，只能用圆括号初始化。在下一个例子中，`foo`是在*中定义的结构如何做...*一节:\n\n```cpp\n        foo f1;           // default initialization \n        foo f2(42, 1.2); \n        foo f3(42); \n        foo f4();         // function declaration\n```\n\n*   聚合和 POD 类型可以用括号初始化来初始化。在下一个例子中，`bar`是在*中定义的结构如何做...*一节:\n\n```cpp\n        bar b = {42, 1.2}; \n        int a[] = {1, 2, 3, 4, 5};\n```\n\n除了初始化数据的不同方法之外，还有一些限制。例如，初始化标准容器的唯一方法是首先声明一个对象，然后向其中插入元素；vector 是一个例外，因为它可以从一个数组中赋值，该数组可以使用聚合初始化预先初始化。但是，另一方面，动态分配的聚合不能直接初始化。\n\n*中的所有例子如何做...*部分使用直接初始化，但是使用 brake-初始化也可以进行复制初始化。在大多数情况下，直接初始化和复制初始化这两种形式可能是等价的，但是复制初始化不太许可，因为它不考虑隐式转换序列中必须直接从初始值设定项生成对象的显式构造函数，而直接初始化需要从初始值设定项到构造函数参数的隐式转换。动态分配的数组只能使用直接初始化来初始化。\n\n在前面的示例中显示的类中，`foo`是同时具有默认构造函数和带参数的构造函数的一个类。要使用默认构造函数执行默认初始化，我们需要使用空大括号，即`{}`。要使用带参数的构造函数，我们需要在大括号`{}`中提供所有参数的值。与默认初始化意味着调用默认构造函数的非聚合类型不同，对于聚合类型，默认初始化意味着用零初始化。\n\n标准容器的初始化是可能的，例如上面显示的向量和映射，因为所有标准容器在 C++ 11 中都有一个附加的构造函数，该构造函数接受类型为`std::initializer_list<T>`的参数。这基本上是对类型为`T const`的元素数组的轻量级代理。这些构造函数然后从初始化列表中的值初始化内部数据。\n\n使用`std::initializer_list`进行初始化的方式如下:\n\n*   编译器解析初始化列表中元素的类型(所有元素必须具有相同的类型)。\n*   编译器用初始值设定项列表中的元素创建一个数组。\n*   编译器创建一个`std::initializer_list<T>`对象来包装之前创建的数组。\n*   `std::initializer_list<T>`对象作为参数传递给构造函数。\n\n初始值设定项列表总是优先于使用括号初始化的其他构造函数。如果某个类存在这样的构造函数，则在执行大括号初始化时将调用它:\n\n```cpp\n    class foo \n    { \n      int a_; \n      int b_; \n    public: \n      foo() :a_(0), b_(0) {} \n\n      foo(int a, int b = 0) :a_(a), b_(b) {} \n      foo(std::initializer_list<int> l) {} \n    }; \n\n    foo f{ 1, 2 }; // calls constructor with initializer_list<int>\n```\n\n优先规则适用于任何函数，而不仅仅是构造函数。在下面的示例中，同一函数存在两个重载。使用初始值设定项列表调用函数会解析为使用`std::initializer_list`调用重载:\n\n```cpp\n    void func(int const a, int const b, int const c) \n    { \n      std::cout << a << b << c << std::endl; \n    } \n\n    void func(std::initializer_list<int> const l) \n    { \n      for (auto const & e : l) \n        std::cout << e << std::endl; \n    } \n\n    func({ 1,2,3 }); // calls second overload\n```\n\n然而，这有可能导致错误。让我们以向量类型为例。在向量的构造函数中，有一个具有表示要分配的元素的初始数量的单个参数，另一个具有作为参数的`std::initializer_list`。如果目的是创建一个预分配大小的向量，使用括号初始化将不起作用，因为带有`std::initializer_list`的构造函数将是调用的最佳重载:\n\n```cpp\n    std::vector<int> v {5};\n```\n\n前面的代码没有创建一个包含五个元素的向量，而是创建了一个包含一个元素且值为 5 的向量。为了能够实际创建具有五个元素的向量，必须使用括号形式的初始化:\n\n```cpp\n    std::vector<int> v (5);\n```\n\n另一件需要注意的事情是，大括号初始化不允许缩小转换。根据 C++ 标准(参考标准的第 8.5.4 段)，收缩转换是一种隐式转换:\n\n- From a floating-point type to an integer type\n- From long double to double or float, or from double to float, except where the source is a constant expression and the actual value after conversion is within the range of values that can be represented (even if it cannot be represented exactly)\n- From an integer type or unscoped enumeration type to a floating-point type, except where the source is a constant expression and the actual value after conversion will fit into the target type and will produce the original value when converted to its original type\n- From an integer type or unscoped enumeration type to an integer type that cannot represent all the values of the original type, except where the source is a constant expression and the actual value after conversion will fit into the target type and will produce the original value when converted to its original type.\n\n下列声明会触发编译器错误，因为它们需要收缩转换:\n\n```cpp\n    int i{ 1.2 };           // error \n\n    double d = 47 / 13; \n    float f1{ d };          // error \n    float f2{47/13};        // OK\n```\n\n要修复该错误，必须进行显式转换:\n\n```cpp\n    int i{ static_cast<int>(1.2) }; \n\n    double d = 47 / 13; \n    float f1{ static_cast<float>(d) };\n```\n\nA brace-initialization list is not an expression and does not have a type. Therefore, `decltype` cannot be used on a brace-init list, and template type deduction cannot deduce the type that matches a brace-init list.\n\n# 还有更多\n\n以下示例显示了直接列表初始化和复制列表初始化的几个示例。在 C++ 11 中，所有这些表达式的推导类型都是`std::initializer_list<int>`。\n\n```cpp\nauto a = {42};   // std::initializer_list<int>\nauto b {42};     // std::initializer_list<int>\nauto c = {4, 2}; // std::initializer_list<int>\nauto d {4, 2};   // std::initializer_list<int>\n```\n\nC++ 17 改变了列表初始化的规则，区分了直接和复制列表初始化。类型扣除的新规则如下:\n\n*   对于复制列表初始化，如果列表中的所有元素具有相同的类型，或者格式不正确，自动推导将会推导出一个`std::initializer_list<T>`。\n*   对于直接列表初始化，如果列表有单个元素，自动演绎会演绎出`T`，如果有多个元素，自动演绎会不规范。\n\n基于新的规则，前面的例子会有如下变化:`a``c`推导为`std::initializer_list<int>`；`b`被演绎为一个`int`；`d`使用直接初始化，并且在 brake-init-list 中有多个值，会触发编译器错误。\n\n```cpp\nauto a = {42};   // std::initializer_list<int>\nauto b {42};     // int\nauto c = {4, 2}; // std::initializer_list<int>\nauto d {4, 2};   // error, too many\n```\n\n# 请参见\n\n*   *尽可能使用自动*\n*   *了解非静态成员初始化的各种形式*\n\n# 了解各种形式的非静态成员初始化\n\n构造函数是完成非静态类成员初始化的地方。许多开发人员更喜欢构造函数体中的赋值。除了实际需要的几种例外情况之外，非静态成员的初始化应该在构造函数的初始化列表中完成，或者从 C++ 11 开始，当它们在类中声明时，使用默认成员初始化。在 C++ 11 之前，类的常量和非常量非静态数据成员必须在构造函数中初始化。只有静态常数才能在类的声明中初始化。正如我们将进一步看到的，C++ 11 中取消了这个限制，允许在类声明中初始化非静态。这个初始化被称为*默认成员初始化*，将在接下来的章节中解释。\n\n这个方法将探索非静态成员初始化的方式。\n\n# 怎么做...\n\n要初始化类的非静态成员，您应该:\n\n*   使用默认成员初始化为具有多个构造函数的类成员提供默认值，这些构造函数将为这些成员使用一个公共的初始化器(参见下面代码中的`[3]`和`[4]`)。\n*   使用静态和非静态常量的默认成员初始化(参见下面代码中的`[1]`和`[2]`)。\n*   使用构造函数初始化列表初始化没有默认值但依赖于构造函数参数的成员(参见下面代码中的`[5]`和`[6]`)。\n*   当其他选项不可用时，在构造函数中使用赋值(示例包括用指针`this`初始化数据成员，检查构造函数参数值，以及在用这些值初始化成员或两个非静态数据成员的自引用之前引发异常)。\n\n以下示例显示了这些初始化形式:\n\n```cpp\n    struct Control \n    { \n      const int DefaultHeigh = 14;                  // [1] \n      const int DefaultWidth = 80;                  // [2] \n\n      TextVAligment valign = TextVAligment::Middle; // [3] \n      TextHAligment halign = TextHAligment::Left;   // [4] \n\n      std::string text; \n\n      Control(std::string const & t) : text(t)       // [5] \n      {} \n\n      Control(std::string const & t, \n        TextVerticalAligment const va, \n        TextHorizontalAligment const ha):  \n      text(t), valign(va), halign(ha)                 // [6] \n      {} \n    };\n```\n\n# 它是如何工作的...\n\n非静态数据成员应该在构造函数的初始化列表中初始化，如下例所示:\n\n```cpp\n    struct Point \n    { \n      double X, Y; \n      Point(double const x = 0.0, double const y = 0.0) : X(x), Y(y)  {} \n    };\n```\n\n然而，许多开发人员并不使用初始化列表，而是更喜欢在构造函数的主体中进行赋值，甚至混合赋值和初始化列表。这可能有几个原因——对于有许多成员的大型类，构造函数赋值可能看起来比长的初始化列表更容易阅读，可能会分成许多行，也可能是因为他们熟悉没有初始化列表的其他编程语言，或者不幸的是，由于各种原因，他们甚至不知道它。\n\nIt is important to note that the order in which non-static data members are initialized is the order in which they were declared in the class definition, and not the order of their initialization in a constructor initializer list. On the other hand, the order in which non-static data members are destroyed is the reversed order of construction.\n\n在构造函数中使用赋值是没有效率的，因为这可能会创建临时对象，这些对象随后会被丢弃。如果未在初始值设定项列表中初始化，非静态成员将通过其默认构造函数初始化，然后，当在构造函数的主体中赋值时，调用赋值运算符。如果默认构造函数分配资源(如内存或文件)，并且必须在赋值操作符中解除分配和重新分配，这可能会导致低效的工作:\n\n```cpp\n    struct foo \n    { \n      foo()  \n      { std::cout << \"default constructor\" << std::endl; } \n      foo(std::string const & text)  \n      { std::cout << \"constructor '\" << text << \"'\" << std::endl; } \n      foo(foo const & other)\n      { std::cout << \"copy constructor\" << std::endl; } \n      foo(foo&& other)  \n      { std::cout << \"move constructor\" << std::endl; }; \n      foo& operator=(foo const & other)  \n      { std::cout << \"assignment\" << std::endl; return *this; } \n      foo& operator=(foo&& other)  \n      { std::cout << \"move assignment\" << std::endl; return *this;} \n      ~foo()  \n      { std::cout << \"destructor\" << std::endl; } \n    }; \n\n    struct bar \n    { \n      foo f; \n\n      bar(foo const & value) \n      { \n        f = value; \n      } \n    }; \n\n    foo f; \n    bar b(f);\n```\n\n前面的代码产生了以下输出，显示了数据成员`f`是如何首先被默认初始化，然后被分配一个新值的:\n\n```cpp\ndefault constructor \ndefault constructor \nassignment \ndestructor \ndestructor\n```\n\n将初始化从构造函数体中的赋值更改为初始值设定项列表，将对默认构造函数和赋值运算符的调用替换为对复制构造函数的调用:\n\n```cpp\n    bar(foo const & value) : f(value) { }\n```\n\n添加前一行代码会产生以下输出:\n\n```cpp\ndefault constructor \ncopy constructor \ndestructor \ndestructor\n```\n\n出于这些原因，至少对于内置类型以外的其他类型(如`bool`、`char`、`int`、`float`、`double`或指针)，您应该更喜欢构造函数初始值设定项列表。然而，为了与您的初始化风格保持一致，在可能的情况下，您应该总是更喜欢构造函数初始值设定项列表。有几种情况下使用初始化列表是不可能的；这些案例包括以下案例(但列表可以扩展到其他案例):\n\n*   如果必须用一个指针或对包含它的对象的引用来初始化一个成员，在初始化列表中使用`this`指针可能会触发一些编译器的警告，即在构造对象之前使用它。\n*   如果您有两个必须包含相互引用的数据成员。\n*   如果要在用参数值初始化非静态数据成员之前测试输入参数并引发异常。\n\n从 C++ 11 开始，非静态数据成员可以在类中声明时进行初始化。这被称为*默认成员初始化*，因为它应该用默认值来表示初始化。默认成员初始化适用于常量和未基于构造函数参数初始化的成员(换句话说，其值不依赖于对象构造方式的成员):\n\n```cpp\n    enum class TextFlow { LeftToRight, RightToLeft }; \n\n    struct Control \n    { \n      const int DefaultHeight = 20; \n      const int DefaultWidth = 100; \n\n      TextFlow textFlow = TextFlow::LeftToRight; \n      std::string text; \n\n      Control(std::string t) : text(t) \n      {} \n    };\n```\n\n在上例中，`DefaultHeight`和`DefaultWidth`都是常数；因此，这些值不依赖于对象的构造方式，因此它们在声明时被初始化。`textFlow`对象是一个非常数的非静态数据成员，它的值也不依赖于对象的初始化方式(它可以通过另一个成员函数来改变)，因此，它在声明时也是使用默认的成员初始化来初始化的。另一方面，`text`也是非常数非静态数据成员，但其初始值取决于对象的构造方式，因此它在构造函数的初始值设定项列表中使用作为参数传递给构造函数的值进行初始化。\n\n如果用默认成员初始化和构造函数初始值设定项列表初始化数据成员，则后者优先，默认值被丢弃。为了举例说明这一点，让我们再次考虑前面的`foo`类和下面使用它的`bar`类:\n\n```cpp\n    struct bar \n    { \n      foo f{\"default value\"}; \n\n      bar() : f{\"constructor initializer\"} \n      { \n      } \n    }; \n\n    bar b;\n```\n\n在这种情况下，输出不同，如下所示，因为默认初始值设定项列表中的值被丢弃，并且对象没有初始化两次:\n\n```cpp\nconstructor\nconstructor initializer\ndestructor\n```\n\nUsing the appropriate initialization method for each member leads not only to more efficient code but also to better organized and more readable code.\n\n# 控制和查询对象对齐\n\nC++ 11 为指定和查询类型的对齐要求提供了标准化的方法(这在以前只有通过编译器特定的方法才能实现)。为了提高不同处理器的性能，并允许使用一些只处理特定对齐数据的指令，控制对齐非常重要。例如，英特尔 SSE 和英特尔 SSE2 需要 16 字节的数据对齐，而对于英特尔高级矢量扩展(或英特尔 AVX)，强烈建议使用 32 字节对齐。该配方探索了用于控制对齐要求的`alignas`说明符和检索类型的对齐要求的`alignof`运算符。\n\n# 准备好\n\n您应该熟悉什么是数据对齐以及编译器执行默认数据对齐的方式。然而，关于后者的基本信息在*如何工作中提供...*段。\n\n# 怎么做...\n\n*   要控制类型(在类级别或数据成员级别)或对象的对齐，请使用`alignas`说明符:\n\n```cpp\n        struct alignas(4) foo \n        { \n          char a; \n          char b; \n        }; \n        struct bar \n        { \n          alignas(2) char a; \n          alignas(8) int  b; \n        }; \n        alignas(8)   int a; \n        alignas(256) long b[4];\n```\n\n*   要查询类型的对齐，请使用`alignof`运算符:\n\n```cpp\n        auto align = alignof(foo);\n```\n\n# 它是如何工作的...\n\n处理器不是一次访问一个字节的内存，而是以二进制幂(2、4、8、16、32 等)的更大块访问内存。因此，编译器在内存中对齐数据非常重要，以便处理器可以轻松访问。如果这些数据没有对齐，编译器必须为访问数据做额外的工作；它必须读取多个数据块，移位并丢弃不必要的字节，并将其余的字节组合在一起。\n\nC++ 编译器根据数据类型的大小对齐变量:1 字节用于`bool`和`char`，2 字节用于`short`，4 字节用于`int`、`long`和`float`，8 字节用于`double`和`long long`等等。当涉及到结构或联合时，对齐必须与最大成员的大小相匹配，以避免性能问题。举例来说，让我们考虑以下数据结构:\n\n```cpp\n    struct foo1    // size = 1, alignment = 1 \n    { \n      char a; \n    }; \n\n    struct foo2    // size = 2, alignment = 1 \n    { \n      char a; \n      char b; \n    }; \n\n    struct foo3    // size = 8, alignment = 4 \n    { \n      char a; \n      int  b; \n    };\n```\n\n`foo1`和`foo2`的大小不同，但是对齐方式是一样的——也就是 1——因为所有的数据成员都是`char`类型，大小为 1。结构`foo3`中，第二个成员为整数，大小为 4。因此，该结构的成员的对齐是在 4 的倍数的地址上完成的。为此，编译器引入了填充字节。`foo3`结构实际上转化为如下:\n\n```cpp\n    struct foo3_ \n    { \n      char a;        // 1 byte \n      char _pad0[3]; // 3 bytes padding to put b on a 4-byte boundary \n      int  b;        // 4 bytes \n    };\n```\n\n类似地，以下结构的大小为 32 字节，对齐方式为 8；那是因为最大的成员是一个`double`，它的大小是 8。但是，这种结构需要在几个地方填充，以确保所有成员都可以在 8:\n\n```cpp\n    struct foo4 \n    { \n      int a; \n      char b; \n      float c; \n      double d; \n      bool e; \n    };\n```\n\n编译器创建的等效结构如下:\n\n```cpp\n    struct foo4_ \n    { \n      int a;         // 4 bytes \n      char b;        // 1 byte \n      char _pad0[3]; // 3 bytes padding to put c on a 8-byte boundary  \n      float c;       // 4 bytes \n      char _pad1[4]; // 4 bytes padding to put d on a 8-byte boundary \n      double d;      // 8 bytes \n      bool e;        // 1 byte \n      char _pad2[7]; // 7 bytes padding to make sizeof struct multiple of 8 \n    };\n```\n\n在 C++ 11 中，使用`alignas`说明符来指定对象或类型的对齐方式。这可以采用表达式(计算结果为 0 的整数常量表达式或有效的对齐值)、类型 id 或参数包。`alignas`说明符可以应用于不表示位字段的变量或类数据成员的声明，或者应用于类、联合或枚举的声明。应用`alignas`规范的类型或对象的对齐要求等于声明中使用的所有`alignas`规范的最大值，大于零。\n\n使用`alignas`说明符有几个限制:\n\n*   唯一有效的对齐是二的幂(1，2，4，8，16，32，等等)。任何其他值都是非法的，程序被认为格式不正确；这不一定会产生错误，因为编译器可能会选择忽略规范。\n*   0 的对齐总是被忽略。\n*   如果声明中最大的`alignas`小于没有任何`alignas`说明符的自然对齐，则该程序也被视为格式不正确。\n\n在以下示例中，`alignas`说明符应用于类声明。没有`alignas`说明符的自然对齐应该是 1，但是有了`alignas(4)`就变成了 4:\n\n```cpp\n    struct alignas(4) foo \n    { \n      char a; \n      char b; \n    };\n```\n\n换句话说，编译器将前面的类转换为下面的类:\n\n```cpp\n    struct foo \n    { \n      char a; \n      char b; \n      char _pad0[2]; \n    };\n```\n\n`alignas`说明符可以应用于类声明和成员数据声明。在这种情况下，最严格(即最大)的值获胜。在以下示例中，成员`a`的自然大小为 1，需要对齐 2；成员`b`自然大小为 4，要求对齐为 8，因此，最严格的对齐为 8。整个类的对齐要求为 4，比最严格的对齐要求弱(即更小)，因此将被忽略，尽管编译器会产生警告:\n\n```cpp\n    struct alignas(4) foo \n    { \n      alignas(2) char a; \n      alignas(8) int  b; \n    };\n```\n\n结果是这样一个结构:\n\n```cpp\n    struct foo \n    { \n      char a; \n      char _pad0[7]; \n      int b; \n      char _pad1[4]; \n    };\n```\n\n`alignas`说明符也可以应用于变量。在下一个例子中，变量`a`，也就是一个整数，需要以 8 的倍数放在内存中。下一个变量，4 `a`的数组，也就是一个整数，需要以 8 的倍数放在内存中。下一个变量，4 `long`的数组，需要以 256 的倍数放入内存。因此，编译器将在两个变量之间引入多达 244 字节的填充(取决于内存中的位置，在 8 的地址倍数处，变量`a`位于):\n\n```cpp\n    alignas(8)   int a;   \n    alignas(256) long b[4]; \n\n    printf(\"%pn\", &a); // eg. 0000006C0D9EF908 \n    printf(\"%pn\", &b); // eg. 0000006C0D9EFA00\n```\n\n看地址可以看到`a`的地址确实是 8 的倍数，`b`的地址是 256(十六进制 100)的倍数。\n\n要查询一个类型的对齐，我们使用`alignof`运算符。与`sizeof`不同，该运算符只能应用于类型 id，而不能应用于变量或类数据成员。可以应用它的类型可以是完整类型、数组类型或引用类型。对于数组，返回值是元素类型的对齐方式；对于引用，返回值是被引用类型的对齐方式。以下是几个例子:\n\n| **表达式** | **评估** |\n| `alignof(char)` | 1，因为`char`的自然对线是 1 |\n| `alignof(int)` | 4，因为`int`的自然对线是 4 |\n| `alignof(int*)` | 32 位 4，64 位 8，指针对齐 |\n| `alignof(int[4])` | 4，因为元素类型的自然对齐是 4 |\n| `alignof(foo&)` | 8，因为作为引用类型的类`foo`的指定对齐方式(如最后一个示例所示)是 8 |\n\n# 使用限定范围的枚举\n\n枚举是 C++ 中的一种基本类型，它定义了一个值的集合，并且总是一个完整的基础类型。它们的命名值是常量，称为枚举数。用关键字`enum`声明的枚举称为*未计数枚举*，用`enum class`或`enum struct`声明的枚举称为*范围枚举*。后者是在 C++ 11 中引入的，旨在解决未划分枚举的几个问题。\n\n# 怎么做...\n\n*   更喜欢使用限定范围的枚举，而不是未限定范围的枚举。\n*   为了使用限定范围的枚举，您应该使用`enum class`或`enum struct`声明枚举:\n\n```cpp\n        enum class Status { Unknown, Created, Connected };\n        Status s = Status::Created;\n```\n\nThe `enum class` and `enum struct` declarations are equivalent, and throughout this recipe and the rest of the book, we will use `enum class`.\n\n# 它是如何工作的...\n\n未划分的枚举有几个问题会给开发人员带来问题:\n\n*   它们将其枚举数导出到周围的范围(因此，它们被称为未划分的枚举)，这有以下两个缺点:如果同一命名空间中的两个枚举具有同名的枚举数，并且不能使用完全限定名来使用枚举数，则可能导致名称冲突:\n\n```cpp\n        enum Status {Unknown, Created, Connected};\n        enum Codes {OK, Failure, Unknown};   // error \n        auto status = Status::Created;       // error\n```\n\n*   在 C++ 11 之前，他们不能指定需要是整型的基础类型。该类型不得大于`int`，除非枚举值不能适合有符号或无符号整数。因此，不可能预先声明枚举。原因是枚举的大小是未知的，因为直到定义了枚举器的值，编译器才能选择合适的整数类型，才知道基础类型。这在 C++ 11 中已经修复了。\n*   枚举数的值隐式转换为`int`。这意味着您可以有意或无意地混合具有特定含义的枚举和整数(甚至可能与枚举的含义无关)，编译器将无法警告您:\n\n```cpp\n        enum Codes { OK, Failure }; \n        void include_offset(int pixels) {/*...*/} \n        include_offset(Failure);\n```\n\n作用域枚举基本上是强类型枚举，其行为不同于非作用域枚举:\n\n*   它们不会将其枚举器导出到周围的范围。前面显示的两个枚举将更改如下，不再产生名称冲突，并且可以完全限定枚举器的名称:\n\n```cpp\n        enum class Status { Unknown, Created, Connected }; \n        enum class Codes { OK, Failure, Unknown }; // OK \n        Codes code = Codes::Unknown;               // OK\n```\n\n*   您可以指定基础类型。未划分枚举的基础类型的相同规则也适用于限定范围的枚举，只是用户可以显式指定基础类型。这也解决了前向声明的问题，因为在定义可用之前就可以知道基础类型:\n\n```cpp\n        enum class Codes : unsigned int; \n\n        void print_code(Codes const code) {} \n\n        enum class Codes : unsigned int \n        {  \n           OK = 0,  \n           Failure = 1,  \n           Unknown = 0xFFFF0000U \n        };\n```\n\n*   作用域枚举的值不再隐式转换为`int`。将`enum class`的值赋给整数变量会触发编译器错误，除非指定了显式强制转换:\n\n```cpp\n        Codes c1 = Codes::OK;                       // OK \n        int c2 = Codes::Failure;                    // error \n        int c3 = static_cast<int>(Codes::Failure);  // OK\n```\n\n# 对虚拟方法使用重写和 final\n\n与其他类似的编程语言不同，C++ 没有特定的语法来声明接口(基本上是只包含纯虚拟方法的类)，并且在如何声明虚拟方法方面也有一些不足。在 C++ 中，虚拟方法是用`virtual`关键字引入的。然而，关键字`virtual`对于在派生类中声明重写是可选的，这在处理大型类或层次结构时会导致混乱。您可能需要在整个层次结构中导航到底部，以确定某个函数是否是虚拟的。另一方面，有时，确保虚函数甚至派生类不再被重写或进一步派生是有用的。在这个食谱中，我们将看到如何使用 C++ 11 特殊标识符`override`和`final`来声明虚拟函数或类。\n\n# 准备好\n\n您应该熟悉 C++ 中的继承和多态性以及概念，例如抽象类、纯说明符、虚拟和重写方法。\n\n# 怎么做...\n\n要确保基类和派生类中虚拟方法的正确声明，同时提高可读性，请执行以下操作:\n\n*   在派生类中声明虚函数时，总是使用`virtual`关键字，这些派生类应该重写基类的虚函数，并且\n*   在虚函数声明或定义的声明部分之后，始终使用`override`特殊标识符。\n\n```cpp\n        class Base \n        { \n          virtual void foo() = 0;\n          virtual void bar() {} \n          virtual void foobar() = 0; \n        };\n\n        void Base::foobar() {}\n\n        class Derived1 : public Base \n        { \n          virtual void foo() override = 0;\n          virtual void bar() override {}\n          virtual void foobar() override {} \n        }; \n\n        class Derived2 : public Derived1 \n        { \n          virtual void foo() override {} \n        };\n```\n\nThe declarator is the part of the type of a function that excludes the return type.\n\n为了确保函数不能被进一步覆盖或者类不能再被派生，使用`final`特殊标识符:\n\n*   在虚拟函数声明或定义的声明部分之后，以防止派生类中的进一步重写:\n\n```cpp\n        class Derived2 : public Derived1 \n        { \n          virtual void foo() final {} \n        };\n```\n\n*   在类声明中的类名后面，以防止类的进一步派生:\n\n```cpp\n        class Derived4 final : public Derived1 \n        { \n          virtual void foo() override {} \n        };\n```\n\n# 它是如何工作的...\n\n`override`的工作方式很简单；在虚函数声明或定义中，它确保函数实际上是在重写基类函数，否则，编译器将触发错误。\n\n需要注意的是`override`和`final`关键字都是特殊的标识符，只有在成员函数声明或定义中才有意义。它们不是保留关键字，仍然可以在程序的其他地方用作用户定义的标识符。\n\n使用`override`特殊标识符有助于编译器检测虚拟方法不覆盖另一个方法的情况，如下例所示:\n\n```cpp\n    class Base \n    { \n    public: \n      virtual void foo() {}\n      virtual void bar() {}\n    }; \n\n    class Derived1 : public Base \n    { \n    public:    \n      void foo() override {}\n      // for readability use the virtual keyword    \n\n      virtual void bar(char const c) override {}\n      // error, no Base::bar(char const) \n    };\n```\n\n另一个特殊标识符`final`用在成员函数声明或定义中，表示该函数是虚拟的，不能在派生类中重写。如果派生类试图重写虚函数，编译器会触发一个错误:\n\n```cpp\n    class Derived2 : public Derived1 \n    { \n      virtual void foo() final {} \n    }; \n\n    class Derived3 : public Derived2 \n    { \n      virtual void foo() override {} // error \n    };\n```\n\n`final`说明符也可以用在类声明中，表示它不能被派生:\n\n```cpp\n    class Derived4 final : public Derived1 \n    { \n      virtual void foo() override {} \n    };\n\n    class Derived5 : public Derived4 // error \n    { \n    };\n```\n\n由于`override`和`final`在定义的上下文中使用时都有这种特殊的含义，并且实际上不是保留的关键字，所以您仍然可以在 C++ 代码的任何地方使用它们。这确保了在 C++ 11 之前编写的现有代码不会因为标识符使用这些名称而中断:\n\n```cpp\n    class foo \n    { \n      int final = 0; \n      void override() {} \n    };\n```\n\n# 使用基于范围的 for 循环在范围上迭代\n\n许多编程语言支持一种称为`for each`的`for`循环变体，即在集合元素上重复一组语句。直到 C++ 11，C++ 才对此有核心语言支持。最接近的功能是标准库中的通用算法`std::for_each`，它将一个函数应用于一个范围内的所有元素。C++ 11 为`for each`带来了语言支持，它实际上被称为*基于范围的循环*。新的 C++ 17 标准对原来的语言特性进行了一些改进。\n\n# 准备好\n\n在 C++ 11 中，基于范围的 for 循环具有以下一般语法:\n\n```cpp\n    for ( range_declaration : range_expression ) loop_statement\n```\n\n为了举例说明使用基于范围的循环的各种方法，我们将使用以下返回元素序列的函数:\n\n```cpp\n    std::vector<int> getRates() \n    { \n      return std::vector<int> {1, 1, 2, 3, 5, 8, 13}; \n    } \n\n    std::multimap<int, bool> getRates2() \n    { \n      return std::multimap<int, bool> { \n        { 1, true }, \n        { 1, true }, \n        { 2, false }, \n        { 3, true }, \n        { 5, true }, \n        { 8, false }, \n        { 13, true } \n      }; \n    }\n```\n\n# 怎么做...\n\n基于范围的 for 循环可以以多种方式使用:\n\n*   通过为序列的元素提交特定类型:\n\n```cpp\n        auto rates = getRates();\n        for (int rate : rates) \n          std::cout << rate << std::endl; \n        for (int& rate : rates) \n          rate *= 2;\n```\n\n*   通过不指定类型并让编译器推导它:\n\n```cpp\n        for (auto&& rate : getRates()) \n          std::cout << rate << std::endl; \n\n        for (auto & rate : rates) \n          rate *= 2; \n\n        for (auto const & rate : rates) \n          std::cout << rate << std::endl;\n```\n\n*   通过在 C++ 17 中使用结构化绑定和分解声明:\n\n```cpp\n        for (auto&& [rate, flag] : getRates2()) \n          std::cout << rate << std::endl;\n```\n\n# 它是如何工作的...\n\n先前在*中显示的基于范围的循环表达式如何操作...*部分基本上是语法糖，因为编译器将其转换为其他东西。在 C++ 17 之前，编译器生成的代码曾经如下:\n\n```cpp\n    { \n      auto && __range = range_expression; \n      for (auto __begin = begin_expr, __end = end_expr; \n      __begin != __end; ++ __begin) { \n        range_declaration = *__begin; \n        loop_statement \n      } \n    }\n```\n\n该代码中的`begin_expr`和`end_expr`取决于范围的类型:\n\n*   对于类 C 数组:`__range`和`__range + __bound`(其中`__bound`是数组中的元素数)\n*   对于具有`begin`和`end`成员的类类型(无论其类型和可访问性如何):`__range.begin()`和`__range.end()`。\n*   对于其他人来说，是`begin(__range)`和`end(__range)`通过依赖于自变量的查找来确定的。\n\n需要注意的是，如果一个类包含任何名为`begin`或`end`的成员(函数、数据成员或枚举器)，无论其类型和可访问性如何，它们都将被挑选为`begin_expr`和`end_expr`。因此，这样的类类型不能用于基于范围的循环。\n\n在 C++ 17 中，编译器生成的代码略有不同:\n\n```cpp\n    { \n      auto && __range = range_expression; \n      auto __begin = begin_expr; \n      auto __end = end_expr; \n      for (; __begin != __end; ++ __begin) { \n        range_declaration = *__begin; \n        loop_statement \n      } \n    }\n```\n\n新标准删除了开始表达式和结束表达式必须具有相同类型的约束。结束表达式不需要是实际的迭代器，但是它必须能够与迭代器进行不等式比较。这样做的一个好处是，范围可以由谓词来限定。\n\n# 请参见\n\n*   *为自定义类型的循环启用基于范围的功能*\n\n# 为自定义类型的循环启用基于范围的\n\n正如我们在前面的方法中看到的，基于范围的 for 循环，在其他编程语言中被称为`for each`，允许您迭代一个范围的元素，为标准的`for`循环提供了简化的语法，并使代码在许多情况下更加可读。然而，基于范围的 for 循环不能使用任何表示范围的类型，而是需要有一个`begin()`和`end()`函数(对于非数组类型)作为成员或自由函数。在这个配方中，我们将看到如何在基于范围的 for 循环中启用自定义类型。\n\n# 准备好\n\n如果您需要了解基于范围的循环是如何工作的，以及编译器为这种循环生成的代码是什么，建议您在继续阅读本教程之前阅读使用基于范围的循环在范围上迭代的方法*。*\n\n为了展示如何为表示序列的自定义类型启用基于范围的循环，我们将使用简单数组的以下实现:\n\n```cpp\n    template <typename T, size_t const Size> \n    class dummy_array \n    { \n      T data[Size] = {}; \n\n    public: \n      T const & GetAt(size_t const index) const \n      { \n        if (index < Size) return data[index]; \n        throw std::out_of_range(\"index out of range\"); \n      } \n\n      void SetAt(size_t const index, T const & value) \n      { \n        if (index < Size) data[index] = value; \n        else throw std::out_of_range(\"index out of range\"); \n      } \n\n      size_t GetSize() const { return Size; } \n    };\n```\n\n该方法的目的是能够编写如下代码:\n\n```cpp\n    dummy_array<int, 3> arr; \n    arr.SetAt(0, 1); \n    arr.SetAt(1, 2); \n    arr.SetAt(2, 3); \n\n    for(auto&& e : arr) \n    {  \n      std::cout << e << std::endl; \n    }\n```\n\n# 怎么做...\n\n要在基于范围的`for`循环中使用自定义类型，您需要执行以下操作:\n\n*   为必须实现以下运算符的类型创建可变迭代器和常量迭代器:\n    *   `operator++ `用于递增迭代器。\n    *   `operator*`用于解引用迭代器和访问迭代器指向的实际元素。\n    *   `operator!=`用于与另一个迭代器进行不等式比较。\n\n*   为该类型提供免费的`begin()`和`end()`功能。\n\n给定一个简单范围的早期示例，我们需要提供以下内容:\n\n1.  迭代器类的以下最小实现:\n\n```cpp\n        template <typename T, typename C, size_t const Size> \n        class dummy_array_iterator_type \n        { \n        public: \n          dummy_array_iterator_type(C& collection,  \n                                    size_t const index) : \n          index(index), collection(collection) \n          { } \n\n        bool operator!= (dummy_array_iterator_type const & other) const \n        { \n          return index != other.index; \n        } \n\n        T const & operator* () const \n        { \n          return collection.GetAt(index); \n        } \n\n        dummy_array_iterator_type const & operator++ () \n        { \n          ++ index; \n          return *this; \n        } \n\n        private: \n          size_t   index; \n          C&       collection; \n        };\n```\n\n2.  可变迭代器和常数迭代器的别名模板:\n\n```cpp\n        template <typename T, size_t const Size> \n        using dummy_array_iterator =  \n           dummy_array_iterator_type< \n             T, dummy_array<T, Size>, Size>; \n\n        template <typename T, size_t const Size> \n        using dummy_array_const_iterator =  \n           dummy_array_iterator_type< \n             T, dummy_array<T, Size> const, Size>;\n```\n\n3.  释放返回相应开始和结束迭代器的`begin()`和`end()`函数，两个别名模板都有重载:\n\n```cpp\n        template <typename T, size_t const Size> \n        inline dummy_array_iterator<T, Size> begin(\n          dummy_array<T, Size>& collection) \n        { \n          return dummy_array_iterator<T, Size>(collection, 0); \n        } \n\n        template <typename T, size_t const Size> \n        inline dummy_array_iterator<T, Size> end(\n          dummy_array<T, Size>& collection) \n        { \n          return dummy_array_iterator<T, Size>(\n            collection, collection.GetSize()); \n        } \n\n        template <typename T, size_t const Size> \n        inline dummy_array_const_iterator<T, Size> begin( \n          dummy_array<T, Size> const & collection) \n        { \n          return dummy_array_const_iterator<T, Size>( \n            collection, 0); \n        } \n\n        template <typename T, size_t const Size> \n        inline dummy_array_const_iterator<T, Size> end( \n          dummy_array<T, Size> const & collection) \n        { \n          return dummy_array_const_iterator<T, Size>( \n            collection, collection.GetSize()); \n        }\n```\n\n# 它是如何工作的...\n\n有了这个实现，前面显示的基于范围的 for 循环将按预期编译和执行。当执行依赖于参数的查找时，编译器将识别我们编写的两个`begin()`和`end()`函数(它们引用了一个`dummy_array`，因此它生成的代码变得有效。\n\n在前面的例子中，我们定义了一个迭代器类模板和两个别名模板，分别叫做`dummy_array_iterator`和`dummy_array_const_iterator`。对于这两种类型的迭代器，`begin()`和`end()`函数都有两个重载。这是必要的，这样我们所考虑的容器就可以在基于范围的循环中同时使用常量和非常量实例:\n\n```cpp\n    template <typename T, const size_t Size> \n    void print_dummy_array(dummy_array<T, Size> const & arr) \n    { \n      for (auto && e : arr) \n      { \n        std::cout << e << std::endl; \n      } \n    }\n```\n\n对于我们在本食谱中考虑的简单范围类，启用基于范围的循环的一个可能的替代方法是提供成员`begin()`和`end()`函数。一般来说，只有当你拥有并能够修改源代码时，这才有意义。另一方面，该配方中显示的解决方案在所有情况下都有效，应该优先于其他替代方案。\n\n# 请参见\n\n*   *创建类型别名和别名模板*\n\n# 使用显式构造函数和转换运算符来避免隐式转换\n\n在 C++ 11 之前，具有单个参数的构造函数被认为是转换构造函数。在 C++ 11 中，每个没有`explicit`说明符的构造函数都被认为是一个转换构造函数。这样的构造函数定义了从其参数类型到类类型的隐式转换。类还可以定义将类的类型转换为另一个指定类型的转换运算符。所有这些在某些情况下都很有用，但在其他情况下会产生问题。在这个食谱中，我们将看到如何使用显式构造函数和转换运算符。\n\n# 准备好\n\n对于这个方法，您需要熟悉转换构造函数和转换运算符。在本食谱中，您将学习如何编写显式构造函数和转换运算符，以避免类型之间的隐式转换。显式构造函数和转换运算符(称为*用户定义的转换函数*)的使用使编译器能够产生错误(在某些情况下是编码错误)，并允许开发人员快速发现并修复这些错误。\n\n# 怎么做...\n\n要声明显式构造函数和转换运算符(无论它们是函数还是函数模板)，请在声明中使用`explicit`说明符。\n\n下面的示例显示了显式构造函数和转换运算符:\n\n```cpp\n    struct handle_t \n    { \n      explicit handle_t(int const h) : handle(h) {} \n\n      explicit operator bool() const { return handle != 0; }; \n    private: \n      int handle; \n    };\n```\n\n# 它是如何工作的...\n\n为了理解为什么显式构造函数是必要的以及它们是如何工作的，我们将首先研究转换构造函数。下面的类有三个构造函数:一个默认的构造函数(没有参数)，一个接受`int`的构造函数，一个接受两个参数的构造函数，一个`int`和一个`double`。除了打印信息，他们什么也不做。从 C++ 11 开始，这些都被认为是转换构造函数。该类还有一个将类型转换为`bool`的转换运算符:\n\n```cpp\n    struct foo \n    { \n      foo()\n      { std::cout << \"foo\" << std::endl; }\n      foo(int const a)\n      { std::cout << \"foo(a)\" << std::endl; }\n      foo(int const a, double const b)\n      { std::cout << \"foo(a, b)\" << std::endl; } \n\n      operator bool() const { return true; } \n    };\n```\n\n基于此，以下对象定义是可能的(注意，注释代表控制台输出):\n\n```cpp\n    foo f1;              // foo \n    foo f2 {};           // foo \n\n    foo f3(1);           // foo(a) \n    foo f4 = 1;          // foo(a) \n    foo f5 { 1 };        // foo(a) \n    foo f6 = { 1 };      // foo(a) \n\n    foo f7(1, 2.0);      // foo(a, b) \n    foo f8 { 1, 2.0 };   // foo(a, b) \n    foo f9 = { 1, 2.0 }; // foo(a, b)\n```\n\n`f1`和`f2`调用默认构造函数。`f3`、`f4`、`f5`和`f6`调用取`int`的构造函数。请注意，这些对象的所有定义都是等价的，即使它们看起来不同(`f3`使用函数形式初始化，`f4`和`f6`复制初始化，`f5`使用 brake-init-list 直接初始化)。类似地，`f7`、`f8`和`f9`用两个参数调用构造函数。\n\n需要注意的是，如果`foo`定义了一个采用`std::initializer_list`的构造函数，那么所有使用`{}`的初始化都将解析为该构造函数:\n\n```cpp\n    foo(std::initializer_list<int> l)  \n    { std::cout << \"foo(l)\" << std::endl; }\n```\n\n在这种情况下，`f5`和`f6`会`print foo(l)`，而`f8`和`f9`会产生编译器错误，因为初始化列表的所有元素都应该是整数。\n\n这些看起来都是正确的，但是隐式转换构造函数支持隐式转换可能不是我们想要的情况:\n\n```cpp\n    void bar(foo const f) \n    { \n    } \n\n    bar({});             // foo() \n    bar(1);              // foo(a) \n    bar({ 1, 2.0 });     // foo(a, b)\n```\n\n上例中`bool`的转换运算符也使我们能够在需要布尔值的地方使用`foo`对象:\n\n```cpp\n    bool flag = f1; \n    if(f2) {} \n    std::cout << f3 + f4 << std::endl; \n    if(f5 == f6) {}\n```\n\n前两个例子中`foo`被期望用作布尔，但是最后两个带有加法和相等测试的例子可能是不正确的，因为我们最有可能期望添加`foo`对象并测试`foo`对象的相等性，而不是它们隐式转换成的布尔。\n\n也许一个更现实的例子是考虑一个字符串缓冲区实现，来理解哪里会出现问题。这将是一个包含内部字符缓冲区的类。该类可以提供几个转换构造函数:一个默认构造函数，一个获取表示要预分配的缓冲区大小的`size_t`参数的构造函数，以及一个获取指向应该用于分配和初始化内部缓冲区的`char`的指针的构造函数。简而言之，这样的字符串缓冲区可能如下所示:\n\n```cpp\n    class string_buffer \n    { \n    public: \n      string_buffer() {} \n\n      string_buffer(size_t const size) {} \n\n      string_buffer(char const * const ptr) {} \n\n      size_t size() const { return ...; } \n      operator bool() const { return ...; } \n      operator char * const () const { return ...; } \n    };\n```\n\n基于这个定义，我们可以构建以下对象:\n\n```cpp\n    std::shared_ptr<char> str; \n    string_buffer sb1;             // empty buffer \n    string_buffer sb2(20);         // buffer of 20 characters \n    string_buffer sb3(str.get());   \n    // buffer initialized from input parameter\n```\n\n`sb1`是使用默认构造函数创建的，因此有一个空缓冲区；`sb2`使用带有单个参数的构造函数初始化，该参数的值代表内部缓冲区的字符大小；`sb3`用现有缓冲区初始化，用于定义内部缓冲区的大小，并将其值复制到内部缓冲区中。但是，相同的定义也支持以下对象定义:\n\n```cpp\n    enum ItemSizes {DefaultHeight, Large, MaxSize}; \n\n    string_buffer b4 = 'a'; \n    string_buffer b5 = MaxSize;\n```\n\n在这种情况下，`b4`用`char`初始化。由于存在到`size_t`的隐式转换，将调用具有单个参数的构造函数。这里的意图不一定明确；也许应该是`\"a\"`而不是`'a'`，在这种情况下，第三个构造函数就会被调用。但是`b5`很可能是一个错误，因为`MaxSize`是一个代表`ItemSizes`的枚举器，应该与字符串缓冲区大小无关。编译器不会以任何方式标记这些错误情况。\n\n在构造函数的声明中使用`explicit`说明符，该构造函数成为显式构造函数，不再允许隐式构造类类型的对象。为了举例说明这一点，我们将稍早改变`string_buffer`类，以显式声明所有构造函数:\n\n```cpp\n    class string_buffer \n    { \n    public: \n      explicit string_buffer() {} \n\n      explicit string_buffer(size_t const size) {} \n\n      explicit string_buffer(char const * const ptr) {} \n\n      explicit operator bool() const { return ...; } \n      explicit operator char * const () const { return ...; } \n    };\n```\n\n变化很小，但是前面例子中`b4`和`b5`的定义不再起作用，并且是不正确的，因为从`char`或`int`到`size_t`的隐式转换在重载解析期间不再可用，以确定应该调用什么构造函数。结果是`b4`和`b5`都出现编译器错误。注意`b1`、`b2`和`b3`即使构造函数是显式的，也仍然是有效的定义。\n\n在这种情况下，解决问题的唯一方法是提供从`char`或`int`到`string_buffer`的显式强制转换:\n\n```cpp\n    string_buffer b4 = string_buffer('a'); \n    string_buffer b5 = static_cast<string_buffer>(MaxSize); \n    string_buffer b6 = string_buffer{ \"a\" };\n```\n\n使用显式构造函数，编译器能够立即标记错误的情况，开发人员可以做出相应的反应，或者用正确的值修复初始化，或者提供显式强制转换。\n\nThis is only the case when initialization is done with copy initialization and not when using the functional or universal initialization.\n\n对于显式构造函数，以下定义仍然是可能的(并且是错误的):\n\n```cpp\n    string_buffer b7{ 'a' }; \n    string_buffer b8('a');\n```\n\n与构造函数类似，转换运算符可以声明为显式的(如前所示)。在这种情况下，从对象类型到转换运算符指定的类型的隐式转换不再可能，并且需要显式转换。考虑到前面定义的`b1`和`b2``string_buffer`对象，通过显式转换`operator bool`不再可能出现以下情况:\n\n```cpp\n    std::cout << b1 + b2 << std::endl; \n    if(b1 == b2) {}\n```\n\n相反，它们需要显式转换为`bool`:\n\n```cpp\n    std::cout << static_cast<bool>(b1) + static_cast<bool>(b2);\n    if(static_cast<bool>(b1) == static_cast<bool>(b2)) {}\n```\n\n# 请参见\n\n*   *了解统一初始化*\n\n# 使用未命名的命名空间代替静态全局\n\n一个程序越大，当你的程序被链接时，你就越有可能遇到与文件局部变量的名称冲突。在源文件中声明并被认为是翻译单元本地的函数或变量可能会与在另一个翻译单元中声明的其他类似函数或变量冲突。这是因为所有未声明为静态的符号都有外部链接，并且它们的名称在整个程序中必须是唯一的。这个问题的典型 C 解决方案是将这些符号声明为静态的，将它们的链接从外部更改为内部，从而使它们成为翻译单元的本地链接。在这个食谱中，我们将看看这个问题的 C++ 解决方案。\n\n# 准备好\n\n在这个食谱中，我们将讨论一些概念，如全局函数、静态函数、变量、名称空间和翻译单元。除此之外，还要求你明白内外联动的区别；这是这个食谱的关键。\n\n# 怎么做...\n\n当您需要将全局符号声明为静态以避免链接问题时，最好使用未命名的名称空间:\n\n1.  在源文件中声明一个没有名称的命名空间。\n2.  将全局函数或变量的定义放在未命名的名称空间中，不要使它们成为`static`。\n\n以下示例显示了两个不同翻译单元中的两个名为`print()`的函数；它们中的每一个都在一个未命名的名称空间中定义:\n\n```cpp\n    // file1.cpp \n    namespace \n    { \n      void print(std::string message) \n      { \n        std::cout << \"[file1] \" << message << std::endl; \n      } \n    } \n\n    void file1_run() \n    { \n      print(\"run\"); \n    } \n\n    // file2.cpp \n    namespace \n    { \n      void print(std::string message) \n      { \n        std::cout << \"[file2] \" << message << std::endl; \n      } \n    } \n\n    void file2_run() \n    { \n      print(\"run\"); \n    }\n```\n\n# 它是如何工作的...\n\n当一个函数在翻译单元中声明时，它具有外部链接。这意味着来自两个不同翻译单元的两个同名函数会产生链接错误，因为不可能有两个同名的符号。这个问题在 C 中解决的方式，有些人在 C++ 中也是这样，就是将函数或变量声明为静态的，并将其链接从外部更改为内部。在这种情况下，其名称不再导出到翻译单元之外，避免了联动问题。\n\nC++ 中正确的解决方案是使用未命名的名称空间。当您定义如上所示的命名空间时，编译器会将其转换为以下内容:\n\n```cpp\n    // file1.cpp \n    namespace _unique_name_ {} \n    using namespace _unique_name_; \n    namespace _unique_name_ \n    { \n      void print(std::string message) \n      { \n        std::cout << \"[file1] \" << message << std::endl; \n      } \n    } \n\n    void file1_run() \n    { \n      print(\"run\"); \n    }\n```\n\n首先，它声明了一个具有唯一名称的名称空间(名称是什么以及它如何生成该名称是编译器实现的细节，不应该成为关注点)。此时，命名空间为空，这一行的目的是基本建立命名空间。第二，一个 using 指令将来自`_unique_name_`命名空间的所有内容带入当前命名空间。第三，具有编译器生成的名称的命名空间被定义为原始源代码中的名称(当它没有名称时)。\n\n通过在未命名的名称空间中定义翻译单元本地`print()`函数，它们仅具有本地可见性，但是它们的外部链接不再产生链接错误，因为它们现在具有外部唯一的名称。\n\n未命名的名称空间也在一个可能更模糊的涉及模板的情况下工作。在 C++ 11 模板之前，非类型参数不能是具有内部链接的名称，因此使用静态变量是不可能的。另一方面，未命名命名空间中的符号具有外部链接，可以用作模板参数。尽管在 C++ 11 中取消了对模板非类型参数的这种链接限制，但它仍然存在于最新版本的 VC++ 编译器中。这个问题表现在下面的例子中，声明 t1 会产生编译器错误，因为非类型参数表达式有内部链接，但是`t2`是正确的，因为`Size2`有外部链接。(注意用 Clang 和 gcc 编译下面的代码不会产生任何错误。)\n\n```cpp\n    template <int const& Size> \n    class test {}; \n\n    static int Size1 = 10; \n\n    namespace \n    { \n      int Size2 = 10; \n    } \n\n    test<Size1> t1; \n    test<Size2> t2;\n```\n\n# 请参见\n\n*   *使用内联命名空间进行符号版本控制*\n\n# 使用内联命名空间进行符号版本控制\n\nC++ 11 标准引入了一种新类型的命名空间，称为*内联命名空间*，它基本上是一种机制，使嵌套命名空间的声明看起来和行为都像是周围命名空间的一部分。内联命名空间是使用命名空间声明中的`inline`关键字声明的(未命名的命名空间也可以内联)。这对于库版本控制是一个很有帮助的特性，在这个食谱中，我们将看到如何将内联命名空间用于版本控制符号。从这个食谱中，您将学习如何使用内联命名空间和条件编译来版本化您的源代码。\n\n# 准备好\n\n在本食谱中，我们将讨论命名空间和嵌套命名空间、模板和模板专门化，以及使用预处理器宏的条件编译。为了继续制作食谱，需要熟悉这些概念。\n\n# 怎么做...\n\n要提供库的多个版本并让用户决定使用哪个版本，请执行以下操作:\n\n*   在命名空间中定义库的内容。\n*   在内部内联命名空间中定义库的每个版本或部分版本。\n*   使用预处理器宏和`#if`指令来启用库的特定版本。\n\n以下示例显示了一个库，该库有两个版本可供客户端使用:\n\n```cpp\n    namespace modernlib \n    { \n      #ifndef LIB_VERSION_2 \n      inline namespace version_1 \n      { \n        template<typename T> \n        int test(T value) { return 1; } \n      } \n      #endif \n\n      #ifdef LIB_VERSION_2 \n      inline namespace version_2 \n      { \n        template<typename T> \n        int test(T value) { return 2; } \n      } \n      #endif \n    }\n```\n\n# 它是如何工作的...\n\n内联命名空间的成员被视为周围命名空间的成员。这样的成员可以是部分专门化的、显式实例化的或显式专门化的。这是一个可传递的属性，这意味着如果一个命名空间 A 包含一个内联的命名空间 B，该内联的命名空间 B 包含一个内联的命名空间 C，那么 C 的成员看起来就像是 B 和 A 的成员，而 B 的成员看起来就像是 A 的成员\n\n为了更好地理解为什么内联命名空间是有帮助的，让我们考虑开发一个库的情况，该库随着时间从第一个版本发展到第二个版本(以及以后的版本)。这个库在一个名为`modernlib`的命名空间下定义了它的所有类型和函数。在第一个版本中，该库可能如下所示:\n\n```cpp\n    namespace modernlib \n    { \n      template<typename T> \n      int test(T value) { return 1; } \n    }\n```\n\n库的客户端可以进行以下调用，并返回值 1:\n\n```cpp\n    auto x = modernlib::test(42);\n```\n\n然而，客户端可能决定将模板功能`test()`专门化如下:\n\n```cpp\n    struct foo { int a; }; \n\n    namespace modernlib \n    { \n      template<> \n      int test(foo value) { return value.a; } \n    } \n\n    auto y = modernlib::test(foo{ 42 });\n```\n\n在这种情况下，`y`的值不再是 1，而是 42，因为用户专用函数被调用。\n\n到目前为止，一切正常，但是作为一个库开发人员，您决定创建库的第二个版本，但是仍然提供第一个和第二个版本，并让用户控制对宏使用什么。在第二个版本中，您提供了`test()`函数的新实现，它不再返回 1，而是返回 2。为了能够提供第一个和第二个实现，您将它们放在名为`version_1`和`version_2`的嵌套名称空间中，并使用预处理器宏有条件地编译库:\n\n```cpp\n    namespace modernlib \n    { \n      namespace version_1 \n      { \n        template<typename T> \n        int test(T value) { return 1; } \n      } \n\n      #ifndef LIB_VERSION_2 \n      using namespace version_1; \n      #endif \n\n      namespace version_2  \n      { \n        template<typename T> \n        int test(T value) { return 2; } \n      } \n\n      #ifdef LIB_VERSION_2 \n      using namespace version_2; \n      #endif \n    }\n```\n\n突然，客户端代码将会崩溃，不管它使用的是库的第一个版本还是第二个版本。这是因为测试函数现在在一个嵌套的名称空间中，并且`foo`的专门化是在`modernlib`名称空间中完成的，而实际上应该在`modernlib::version_1`或`modernlib::version_2`中完成。这是因为模板的专门化需要在声明模板的同一个命名空间中完成。在这种情况下，客户端需要像这样更改代码:\n\n```cpp\n    #define LIB_VERSION_2 \n\n    #include \"modernlib.h\" \n\n    struct foo { int a; }; \n\n    namespace modernlib \n    { \n      namespace version_2  \n      { \n        template<> \n        int test(foo value) { return value.a; } \n      } \n    }\n```\n\n这是一个问题，因为库泄露了实现细节，客户端需要知道这些细节，以便进行模板专门化。这些内部细节用内联名称空间隐藏，如*如何做到这一点所示...*本食谱的一节。有了`modernlib`库的定义，在`modernlib`命名空间中具有`test()`函数专门化的客户端代码不再被破坏，因为当模板专门化完成时，`version_1::test()`或`version_2::test()`(取决于客户端实际使用的版本)充当封闭的`modernlib`命名空间的一部分。实现的细节现在对只看到周围命名空间的客户端隐藏起来了`modernlib`。\n\n但是，您应该记住:\n\n*   命名空间`std`是为标准保留的，永远不应该内联。\n*   如果命名空间在其第一个定义中没有内联，则不应内联定义该命名空间。\n\n# 请参见\n\n*   *使用未命名的名称空间代替静态全局变量*\n\n# 使用结构化绑定处理多返回值\n\n从一个函数中返回多个值是非常常见的事情，但是在 C++ 中没有第一流的解决方案来直接启用它。开发人员必须在通过函数的引用参数返回多个值、定义包含多个值的结构或返回`std::pair`或`std::tuple`之间做出选择。前两个使用命名变量的优点是它们清楚地表明了返回值的含义，但缺点是它们必须被显式定义。`std::pair`有名为`first`和`second`的成员，`std::tuple`有未命名的成员，只能通过函数调用来检索，但可以使用`std::tie().`复制到命名的变量中。这些解决方案都不理想。\n\nC++ 17 将`std::tie()`的语义使用扩展到一级核心语言特性，该特性支持将元组的值解包为命名变量。这个特性叫做*结构化绑定*。\n\n# 准备好\n\n对于这个食谱，你应该熟悉标准实用类型`std::pair`和`std::tuple`以及实用功能`std::tie()`。\n\n# 怎么做...\n\n要使用支持 C++ 17 的编译器从函数中返回多个值，您应该执行以下操作:\n\n1.  返回类型使用`std::tuple`。\n\n```cpp\n        std::tuple<int, std::string, double> find() \n        { \n          return std::make_tuple(1, \"marius\", 1234.5); \n        }\n```\n\n2.  使用结构化绑定将元组的值解包到命名对象中。\n\n```cpp\n        auto [id, name, score] = find();\n```\n\n3.  使用分解声明将返回值绑定到`if`语句或`switch`语句中的变量。\n\n```cpp\n        if (auto [id, name, score] = find(); score > 1000) \n        { \n          std::cout << name << std::endl; \n        }\n```\n\n# 它是如何工作的...\n\n结构化绑定是一种语言特性，其工作原理与`std::tie()`类似，只是我们不必为每个需要用`std::tie()`显式解包的值定义命名变量。使用结构化绑定，我们使用`auto`说明符在单个定义中定义所有命名变量，以便编译器可以为每个变量推断正确的类型。\n\n为了举例说明，让我们考虑在`std::map`中插入项目的情况。insert 方法返回一个`std::pair`，它包含插入元素或阻止插入的元素的迭代器，以及一个指示插入是否成功的布尔值。下面的代码非常明确，使用`second`或`first->second`会使代码更难阅读，因为您需要不断弄清楚它们代表什么:\n\n```cpp\n    std::map<int, std::string> m; \n\n    auto result = m.insert({ 1, \"one\" }); \n    std::cout << \"inserted = \" << result.second << std::endl \n              << \"value = \" << result.first->second << std::endl;\n```\n\n使用`std::tie`可以使前面的代码可读性更好，它将元组解包为单个对象(使用`std::pair`是因为`std::tuple`有一个来自`std::pair`的转换赋值):\n\n```cpp\n    std::map<int, std::string> m; \n    std::map<int, std::string>::iterator it; \n    bool inserted; \n\n    std::tie(it, inserted) = m.insert({ 1, \"one\" }); \n    std::cout << \"inserted = \" << inserted << std::endl \n              << \"value = \" << it->second << std::endl; \n\n    std::tie(it, inserted) = m.insert({ 1, \"two\" }); \n    std::cout << \"inserted = \" << inserted << std::endl \n              << \"value = \" << it->second << std::endl;\n```\n\n代码不一定更简单，因为它需要预先定义要解包的对象对。同样，元组中的元素越多，需要定义的对象就越多，但是使用命名对象会使代码更容易阅读。\n\nC++ 17 结构化绑定将元组元素到命名对象的解包提升到语言特性的级别；它不需要使用`std::tie()`，对象在声明时被初始化:\n\n```cpp\n    std::map<int, std::string> m; \n    { \n      auto[it, inserted] = m.insert({ 1, \"one\" }); \n      std::cout << \"inserted = \" << inserted << std::endl \n                << \"value = \" << it->second << std::endl; \n    } \n\n    { \n      auto[it, inserted] = m.insert({ 1, \"two\" }); \n      std::cout << \"inserted = \" << inserted << std::endl \n                << \"value = \" << it->second << std::endl; \n    }\n```\n\n在上面的例子中使用多个块是必要的，因为变量不能在同一个块中重新声明，并且结构化绑定意味着使用`auto`说明符的声明。因此，如果您需要像上面的例子一样进行多次调用并使用结构化绑定，您必须使用不同的变量名或多个块，如上所示。另一种方法是避免结构化绑定，使用`std::tie()`，因为它可以用同一个变量调用多次，所以只需要声明一次。\n\n在 C++ 17 中，也可以用`if(init; condition)`和`switch(init; condition)`的形式在`if`和`switch`语句中声明变量。这可以与结构化绑定相结合，以生成更简单的代码。在下面的示例中，我们尝试向地图中插入一个新值。调用的结果被解包成两个变量，`it`和`inserted`，在初始化部分的`if`语句的范围中定义。`if`语句的条件根据插入对象的值进行评估:\n\n```cpp\n    if(auto [it, inserted] = m.insert({ 1, \"two\" }); inserted)\n    { std::cout << it->second << std::endl; }\n```"
  },
  {
    "path": "docs/mod-cpp/09.md",
    "content": "# 九、使用数字和字符串\n\n本章包含的配方如下:\n\n*   在数字和字符串类型之间转换\n*   数值类型的限制和其他属性\n*   生成伪随机数\n*   初始化伪随机数发生器内部状态的所有位\n*   使用原始字符串避免转义字符\n*   创建熟的用户定义文本\n*   创建原始用户定义的文字\n*   创建字符串助手库\n*   使用正则表达式验证字符串的格式\n*   使用正则表达式解析字符串的内容\n*   使用正则表达式替换字符串的内容\n*   使用字符串视图代替常量字符串引用\n\n# 在数字和字符串类型之间转换\n\n在数字和字符串类型之间转换是一个普遍存在的操作。在 C++ 11 之前，很少有人支持将数字转换为字符串并返回，开发人员不得不主要求助于类型不安全的函数，并且通常编写自己的实用函数，以避免一遍又一遍地编写相同的代码。使用 C++ 11，标准库提供了在数字和字符串之间转换的实用函数。在本食谱中，您将学习如何使用现代 C++ 标准函数在数字和字符串之间进行转换，反之亦然。\n\n# 准备好\n\n本食谱中提到的所有实用功能都可以在`<string>`标题中找到。\n\n# 怎么做...\n\n当需要在数字和字符串之间进行转换时，请使用以下标准转换函数:\n\n*   要从整数或浮点类型转换为字符串类型，请使用`std::to_string()`或`std::to_wstring()`，如以下代码片段所示:\n\n```cpp\n        auto si = std::to_string(42);      // si=\"42\" \n        auto sl = std::to_string(42l);     // sl=\"42\" \n        auto su = std::to_string(42u);     // su=\"42\" \n        auto sd = std::to_wstring(42.0);   // sd=L\"42.000000\" \n        auto sld = std::to_wstring(42.0l); // sld=L\"42.000000\"\n```\n\n*   要从字符串类型转换为整数类型，请使用`std::stoi()`、`std::stol()`、`std::stoll()`、`std::stoul()`或`std::stoull()`；请参考以下代码片段:\n\n```cpp\n        auto i1 = std::stoi(\"42\");                 // i1 = 42 \n        auto i2 = std::stoi(\"101010\", nullptr, 2); // i2 = 42 \n        auto i3 = std::stoi(\"052\", nullptr, 8);    // i3 = 42 \n        auto i4 = std::stoi(\"0x2A\", nullptr, 16);  // i4 = 42\n```\n\n*   要从字符串类型转换为浮点类型，请使用`std::stof()`、`std::stod()`或`std::stold()`，如以下代码片段所示:\n\n```cpp\n        // d1 = 123.45000000000000 \n        auto d1 = std::stod(\"123.45\"); \n        // d2 = 123.45000000000000 \n        auto d2 = std::stod(\"1.2345e+2\"); \n        // d3 = 123.44999980926514 \n        auto d3 = std::stod(\"0xF.6E6666p3\");\n```\n\n# 它是如何工作的...\n\n要将整型或浮点型转换为字符串型，可以使用`std::to_string()`或`std::to_wstring()`功能。这些函数在`<string>`头中可用，并具有有符号和无符号整数和实数类型的重载。当为每种类型调用适当的格式说明符时，它们会产生与`std::sprintf()`和`std::swprintf()`相同的结果。下面的代码片段列出了这两个函数的所有重载。\n\n```cpp\n    std::string to_string(int value); \n    std::string to_string(long value); \n    std::string to_string(long long value); \n    std::string to_string(unsigned value); \n    std::string to_string(unsigned long value); \n    std::string to_string(unsigned long long value); \n    std::string to_string(float value); \n    std::string to_string(double value); \n    std::string to_string(long double value); \n    std::wstring to_wstring(int value); \n    std::wstring to_wstring(long value); \n    std::wstring to_wstring(long long value); \n    std::wstring to_wstring(unsigned value); \n    std::wstring to_wstring(unsigned long value); \n    std::wstring to_wstring(unsigned long long value); \n    std::wstring to_wstring(float value); \n    std::wstring to_wstring(double value); \n    std::wstring to_wstring(long double value);\n```\n\n当涉及到相反的转换时，有一整套功能的名称格式为**ston**(**string to number**)，其中 **n** 代表 **i** ( `integer`)、 **l** ( `long`)、 **ll** ( `long long`)、 **ul** ( `unsigned long`)或**full**(【T4 下面的列表显示了所有这些函数，每个函数都有两个重载，一个以`std::string`为参数，另一个以`std::wstring`为参数:\n\n```cpp\n    int stoi(const std::string& str, std::size_t* pos = 0,  \n             int base = 10); \n    int stoi(const std::wstring& str, std::size_t* pos = 0,  \n             int base = 10); \n    long stol(const std::string& str, std::size_t* pos = 0,  \n             int base = 10); \n    long stol(const std::wstring& str, std::size_t* pos = 0,  \n             int base = 10); \n    long long stoll(const std::string& str, std::size_t* pos = 0,  \n                    int base = 10); \n    long long stoll(const std::wstring& str, std::size_t* pos = 0,  \n                    int base = 10); \n    unsigned long stoul(const std::string& str, std::size_t* pos = 0, \n                        int base = 10); \n    unsigned long stoul(const std::wstring& str, std::size_t* pos = 0,  \n                        int base = 10); \n    unsigned long long stoull(const std::string& str,  \n                              std::size_t* pos = 0, int base = 10); \n    unsigned long long stoull(const std::wstring& str,  \n                              std::size_t* pos = 0, int base = 10); \n    float       stof(const std::string& str, std::size_t* pos = 0); \n    float       stof(const std::wstring& str, std::size_t* pos = 0); \n    double      stod(const std::string& str, std::size_t* pos = 0); \n    double      stod(const std::wstring& str, std::size_t* pos = 0); \n    long double stold(const std::string& str, std::size_t* pos = 0); \n    long double stold(const std::wstring& str, std::size_t* pos = 0);\n```\n\n字符串到整数类型函数的工作方式是丢弃非空白字符前的所有空白，然后尽可能多的字符组成一个有符号或无符号的数字(取决于具体情况)，然后将其转换为请求的整数类型(`stoi()`将返回一个`integer`，`stoul()`将返回一个`unsigned long`，以此类推)。在以下所有示例中，结果都是整数`42`，除了最后一个示例中的结果是`-42`:\n\n```cpp\n    auto i1 = std::stoi(\"42\");             // i1 = 42 \n    auto i2 = std::stoi(\"   42\");          // i2 = 42 \n    auto i3 = std::stoi(\"   42fortytwo\");  // i3 = 42 \n    auto i4 = std::stoi(\"+42\");            // i4 = 42 \n    auto i5 = std::stoi(\"-42\");            // i5 = -42\n```\n\n有效的整数可以由以下部分组成:\n\n*   一个符号，加号(`+`)或减号(`-`)(可选)。\n*   前缀`0`表示八进制基数(可选)。\n*   前缀`0x`或`0X`表示十六进制基数(可选)。\n*   数字序列。\n\n可选前缀`0`(八进制)仅在指定的基数为`8`或`0`时使用。同样，可选前缀`0x`或`0X`(十六进制)仅在指定的基数为`16`或`0`时使用。\n\n将字符串转换为整数的函数有三个参数:\n\n*   输入字符串。\n*   一个指针，当不为空时，它将接收已处理的字符数，并且可以包括任何被丢弃的前导空格、符号和基本前缀，因此它不应该与整数值的位数相混淆。\n*   表示基数的数字；默认情况下，这是`10`。\n\n输入字符串中的有效数字取决于基数。对于基数`2`，唯一有效的数字是`0`和`1`；对于基地`5`，他们是`01234`。对于基数`11`，有效数字为`0-9`，字符为`A`和`a`。这种情况一直持续到我们到达具有有效字符`0-9`、`A-Z`和`a-z`的基地`36`。\n\n以下是将各种基数的数字转换为十进制整数的字符串的更多示例。同样，在所有情况下，结果要么是`42`要么是`-42`:\n\n```cpp\n    auto i6 = std::stoi(\"052\", nullptr, 8); \n    auto i7 = std::stoi(\"052\", nullptr, 0); \n    auto i8 = std::stoi(\"0x2A\", nullptr, 16); \n    auto i9 = std::stoi(\"0x2A\", nullptr, 0); \n    auto i10 = std::stoi(\"101010\", nullptr, 2); \n    auto i11 = std::stoi(\"22\", nullptr, 20); \n    auto i12 = std::stoi(\"-22\", nullptr, 20); \n\n    auto pos = size_t{ 0 }; \n    auto i13 = std::stoi(\"42\", &pos);      // pos = 2 \n    auto i14 = std::stoi(\"-42\", &pos);     // pos = 3 \n    auto i15 = std::stoi(\"  +42dec\", &pos);// pos = 5\n```\n\n需要注意的一点是，如果转换失败，这些转换函数会抛出。有两种可能引发的异常:\n\n*   `std::invalid_argument`:如果无法进行转换:\n\n```cpp\n        try \n        { \n           auto i16 = std::stoi(\"\"); \n        } \n        catch (std::exception const & e) \n        { \n           // prints \"invalid stoi argument\" \n           std::cout << e.what() << std::endl; \n        }\n```\n\n*   `std::out_of_range`:如果转换后的值超出了结果类型的范围(或者基础函数将`errno`设置为`ERANGE`):\n\n```cpp\n        try \n        { \n           // OK\n           auto i17 = std::stoll(\"12345678901234\");  \n           // throws std::out_of_range \n           auto i18 = std::stoi(\"12345678901234\"); \n        } \n        catch (std::exception const & e) \n        { \n           // prints \"stoi argument out of range\"\n           std::cout << e.what() << std::endl; \n        }\n```\n\n将字符串转换为浮点类型的另一组函数非常相似，只是它们没有数字基的参数。有效的浮点值在输入字符串中可以有不同的表示形式:\n\n*   十进制浮点表达式(可选符号，带可选点的十进制数字序列，可选`e`或`E`，后跟带可选符号的指数)。\n*   二进制浮点表达式(可选符号，`0x`或`0X`前缀，带可选点的十六进制数字序列，可选的`p`或`P`后跟带可选符号的指数)。\n*   Infinity 表达式(可选符号，后跟不区分大小写的`INF`或`INFINITY`)。\n*   非数字表达式(可选符号后跟不区分大小写的`NAN`和可能的其他字母数字字符)。\n\n以下是将字符串转换为双精度的各种示例:\n\n```cpp\n    auto d1 = std::stod(\"123.45\");         // d1 =  123.45000000000000 \n    auto d2 = std::stod(\"+123.45\");        // d2 =  123.45000000000000 \n    auto d3 = std::stod(\"-123.45\");        // d3 = -123.45000000000000 \n    auto d4 = std::stod(\"  123.45\");       // d4 =  123.45000000000000 \n    auto d5 = std::stod(\"  -123.45abc\");   // d5 = -123.45000000000000 \n    auto d6 = std::stod(\"1.2345e+2\");      // d6 =  123.45000000000000 \n    auto d7 = std::stod(\"0xF.6E6666p3\");   // d7 =  123.44999980926514 \n\n    auto d8 = std::stod(\"INF\");            // d8 = inf \n    auto d9 = std::stod(\"-infinity\");      // d9 = -inf \n    auto d10 = std::stod(\"NAN\");           // d10 = nan \n    auto d11 = std::stod(\"-nanabc\");       // d11 = -nan\n```\n\n前面以`0xF.6E6666p3`形式看到的浮点数基数 2 科学记数法，不是本食谱的主题。然而，为了清楚理解，提供了简短的描述；但是，建议您查看其他参考资料以了解详细信息。基数为 2 的科学记数法中的浮点常数由几个部分组成:\n\n*   十六进制前缀`0x`。\n*   整数部分，在这个例子中是`F`，十进制是 15。\n*   小数部分，在本例中为`6E6666`，或二进制中的`011011100110011001100110`。要将其转换为十进制，我们需要加上 2 的逆幂:`1/4 + 1/8 + 1/32 + 1/64 + 1/128 + ...`。\n*   后缀，代表 2 的幂；在本例中，`p3`表示 3 的 2 次方。\n\n十进制等效值是由有效部分(由整数部分和小数部分组成)和基数乘以指数的幂来确定的。对于给定的十六进制基数为 2 的浮点文字，有效位为`15.4312499...`(注意第七位之后的数字未显示)，基数为 2，指数为 3。所以结果就是`15.4212499... * 8`，也就是`123.44999980926514`。\n\n# 请参见\n\n*   **数值类型的极限和其他属性**\n\n# 数值类型的限制和其他属性\n\n有时，需要知道并使用用数字类型表示的最小值和最大值，如`char`、`int`或`double`。很多开发者都在为此使用标准的 C 宏，比如`CHAR_MIN` / `CHAR_MAX`、`INT_MIN` / `INT_MAX`，或者`DBL_MIN` / `DBL_MAX`。C++ 提供了一个名为`numeric_limits`的类模板，它对每个数值类型都进行了专门化，使您能够查询一个类型的最小值和最大值，但不限于此，它还为类型属性查询提供了额外的常量，例如一个类型是否有符号，它需要多少位来表示它的值，浮点类型是否可以表示无穷大，以及许多其他内容。在 C++ 11 之前，`numeric_limits<T>`的使用受到限制，因为它不能在需要常量的地方使用(例子可以包括数组的大小和开关情况)。因此，开发人员更喜欢在整个代码中使用 C 宏。在 C++ 11 中，情况不再如此，因为`numeric_limits<T>`的所有静态成员现在都是`constexpr`，这意味着它们可以在任何需要常量表达式的地方使用。\n\n# 准备好\n\n`numeric_limits<T>`类模板在`<limits>`头中的名称空间`std`中可用。\n\n# 怎么做...\n\n使用`std::numeric_limits<T>`查询数字类型`T`的各种属性:\n\n*   使用`min()`和`max()`静态方法获得一个类型的最小和最大的有限个数:\n\n```cpp\n        template<typename T, typename I> \n        T minimum(I const start, I const end) \n        { \n          T minval = std::numeric_limits<T>::max(); \n          for (auto i = start; i < end; ++ i) \n          { \n            if (*i < minval) \n              minval = *i; \n          } \n          return minval; \n        } \n\n        int range[std::numeric_limits<char>::max() + 1] = { 0 }; \n\n        switch(get_value()) \n        { \n          case std::numeric_limits<int>::min(): \n          break; \n        }\n```\n\n*   使用其他静态方法和静态常数来检索数值类型的其他属性:\n\n```cpp\n        auto n = 42; \n        std::bitset<std::numeric_limits<decltype(n)>::digits>  \n          bits { static_cast<unsigned long long>(n) };\n```\n\nIn C++ 11, there is no limitation to where `std::numeric_limits<T>` can be used; therefore, preferably use it over C macros in your modern C++ code.\n\n# 它是如何工作的...\n\n`std::numeric_limits<T>`是一个类模板，允许开发人员查询数值类型的属性。实际值可通过专门化获得，标准库为所有内置数值类型提供专门化(`char`、`short`、`int`、`long`、`float`、`double`等)。此外，第三方可以为其他类型提供额外的实现。一个例子可以是实现一个`bigint`整数类型和一个`decimal`类型的数值库，并为这些类型提供`numeric_limits`的专门化(例如`numeric_limits<bigint>`和`numeric_limits<decimal>`)。\n\n`<limits>`标题中提供了以下数值类型的专门化。请注意，`char16_t`和`char32_t`的专门化在 C++ 11 中是新的；其他的之前都有。除了前面列出的专门化之外，该库还包括这些数字类型的每个`cv-qualified`版本的专门化，它们与不合格的专门化相同。比如考虑类型`int`；有四个实际的专业(它们是相同的):`numeric_limits<int>`、`numeric_limits<const int>`、`numeric_limits<volatile int>`和`numeric_limits<const volatile int>`:\n\n```cpp\n    template<> class numeric_limits<bool>; \n    template<> class numeric_limits<char>; \n    template<> class numeric_limits<signed char>; \n    template<> class numeric_limits<unsigned char>; \ntemplate<> class numeric_limits<wchar_t>; \n    template<> class numeric_limits<char16_t>; \n    template<> class numeric_limits<char32_t>; \n    template<> class numeric_limits<short>; \n    template<> class numeric_limits<unsigned short>; \n    template<> class numeric_limits<int>; \n    template<> class numeric_limits<unsigned int>; \n    template<> class numeric_limits<long>; \n    template<> class numeric_limits<unsigned long>; \n    template<> class numeric_limits<long long>; \n    template<> class numeric_limits<unsigned long long>; \n    template<> class numeric_limits<float>; \n    template<> class numeric_limits<double>; \n    template<> class numeric_limits<long double>;\n```\n\n如前所述，在 C++ 11 中，`numeric_limits`的所有静态成员都是`constexpr`，这意味着它们可以用在所有需要常量表达式的地方。与 C++ 宏相比，这些宏有几个主要优点:\n\n*   它们更容易记住，因为您唯一需要知道的是您无论如何都应该知道的类型的名称，而不是无数个宏的名称。\n*   它们支持 C 语言中没有的类型，如`char16_t`和`char32_t`。\n*   对于不知道类型的模板，它们是唯一可能的解决方案。\n*   最小值和最大值只是它提供的各种类型属性中的两个；因此，它的实际用途超出了数字限制。顺便说一下，由于这个原因，这个类应该被称为`numeric_properties`，而不是`numeric_limits`。\n\n以下函数模板`print_type_properties()`打印该类型的最小和最大有限值以及其他信息:\n\n```cpp\n    template <typename T> \n    void print_type_properties() \n    { \n      std::cout  \n        << \"min=\"  \n        << std::numeric_limits<T>::min()        << std::endl \n        << \"max=\" \n        << std::numeric_limits<T>::max()        << std::endl \n        << \"bits=\" \n        << std::numeric_limits<T>::digits       << std::endl \n        << \"decdigits=\" \n        << std::numeric_limits<T>::digits10     << std::endl \n        << \"integral=\" \n        << std::numeric_limits<T>::is_integer   << std::endl \n        << \"signed=\" \n        << std::numeric_limits<T>::is_signed    << std::endl \n        << \"exact=\" \n        << std::numeric_limits<T>::is_exact     << std::endl \n        << \"infinity=\" \n        << std::numeric_limits<T>::has_infinity << std::endl; \n    }\n```\n\n如果我们为无符号的`short`、`int`和`double`调用`print_type_properties()`函数，它将有以下输出:\n\n| `unsigned short` | `int` | `double` |\n| 最小值=0 最大值=65535 位=16decdigits=4 积分=1 有符号=0 精确=1 无穷大=0 | min=-2147483648max=2147483647 位=31decdigits=9 积分=1 有符号=1 精确=1 无穷大=0 | 最小值=2.22507e-308 最大值=1.79769e+308 位=53decdigits=15 积分=0 有符号=1 精确=0 无穷大=1 |\n\n需要注意的一点是`digits`和`digits10`常数之间的差异:\n\n*   `digits`表示整数类型的位数(不包括符号位，如果存在)和填充位(如果有)，以及浮点类型尾数的位数。\n*   `digits10`是一个类型可以表示的十进制位数，没有变化。为了更好地理解这一点，让我们考虑`unsigned short`的情况。这是一个 16 位整型。它可以表示 0 到 65536 之间的数字。它可以表示最多五个十进制数字，从 10，000 到 65，536，但它不能表示所有五个十进制数字，因为从 65，537 到 99，999 的数字需要更多的位。因此，在不需要更多位的情况下，它所能表示的最大数字有四个十进制数字(从 1000 到 9999 的数字)。这是由`digits10`表示的值。对于积分类型，与常数`digits`有直接关系；对于积分型`T`，`digits10`的值为`std::numeric_limits<T>::digits * std::log10(2)`。\n\n# 生成伪随机数\n\n从游戏到密码学，从采样到预测，生成随机数对于各种各样的应用都是必要的。然而，术语*随机数*实际上并不正确，因为通过数学公式生成的数字是确定性的，不会产生真正的随机数，而是看起来随机的数字，被称为*伪随机*。真正的随机性只能通过基于物理过程的硬件设备来实现，甚至这一点也可能受到挑战，因为人们甚至可能认为宇宙实际上是确定性的。现代 C++ 通过包含数字生成器和分布的伪随机数库来支持生成伪随机数。理论上，它也可以产生真正的随机数，但实际上，这些可能只是伪随机的。\n\n# 准备好\n\n在这个配方中，我们讨论了生成伪随机数的标准支持。理解随机数和伪随机数的区别是关键。另一方面，熟悉各种统计分布是一个优势。但是，您必须知道什么是均匀分布，因为库中的所有引擎都生成均匀分布的数字。\n\n# 怎么做...\n\n要在应用中生成伪随机数，您应该执行以下步骤:\n\n1.  包括标题`<random>`:\n\n```cpp\n        #include <random>\n```\n\n2.  使用`std::random_device`生成器为伪随机引擎播种:\n\n```cpp\n        std::random_device rd{};\n```\n\n3.  使用可用的引擎之一来生成数字，并用随机种子初始化它:\n\n```cpp\n        auto mtgen = std::mt19937{ rd() };\n```\n\n4.  使用可用分布之一将引擎输出转换为所需的统计分布之一:\n\n```cpp\n        auto ud = std::uniform_int_distribution<>{ 1, 6 };\n```\n\n5.  生成伪随机数:\n\n```cpp\n        for(auto i = 0; i < 20; ++ i) \n          auto number = ud(mtgen);\n```\n\n# 它是如何工作的...\n\n伪随机数字库包含两种类型的组件:\n\n*   *引擎*，是随机数的发生器；这些可以产生具有均匀分布的伪随机数，或者如果可用，产生实际随机数。\n*   *分布*将发动机的输出转换为统计分布。\n\n所有引擎(除`random_device`外)均产生均匀分布的整数，所有引擎执行以下方法:\n\n*   `min()`:这是一个静态方法，返回生成器可以产生的最小值。\n*   `max()`:这是一个静态方法，返回生成器可以产生的最大值。\n*   `seed()`:这会用一个起始值初始化算法(除了`random_device`，它不能被播种)。\n*   `operator()`:这将生成一个新的数字，均匀分布在`min()`和`max()`之间。\n*   `discard()`:这将生成并丢弃给定数量的伪随机数。\n\n以下发动机可供选择:\n\n*   `linear_congruential_engine`:这是一个线性同余生成器，使用以下公式产生数字:\n\n(c)调制解调器{ > t0±x(I)} =(a * x(I-1)< = m { > t1 }。\n\n*   `mersenne_twister_engine`:这是一个默森扭扭发生器，在 *W * (N-1) * R* 位保持一个值；每次需要生成数字时，它都会提取 *W* 位。当所有的位都被使用时，它通过移位和混合位来扭曲大的值，以便它有一组新的位可以从中提取。\n*   `subtract_with_carry_engine`:这是一个基于以下公式实现*进位减法*算法的生成器:\n\n*x(i) = (x(i - R) - x(i - S) - cy(i - 1)) mod M*\n\n在上式中， *cy* 定义为:\n\n*cy(i) = x(i - S) - x(i - R) - cy(i - 1) < 0 ? 1 : 0*\n\n此外，该库还提供了引擎适配器，这些适配器也是包装另一个引擎并根据基本引擎的输出生成数字的引擎。引擎适配器为基本引擎实现了前面提到的相同方法。下列发动机适配器可用:\n\n*   `discard_block_engine`:一个生成器，从基础引擎生成的每个 P 数块中只保留 R 数，丢弃其余的。\n*   `independent_bits_engine`:生成位数与基础引擎不同的数字的生成器。\n*   `shuffle_order_engine`:一个生成器，保存由基础引擎产生的 K 个号码的混洗表，并从该表中返回号码，用基础引擎产生的号码替换它们。\n\n所有这些引擎和引擎适配器都在产生伪随机数。然而，该库提供了另一个名为`random_device`的引擎，该引擎被认为会产生非确定性的数字，但这并不是一个实际的限制，因为随机熵的物理来源可能不可用。因此，`random_device`的实现实际上可以基于伪随机引擎。`random_device`类不能像其他引擎一样进行种子化，它有一个名为`entropy()`的额外方法，返回随机设备熵，对于确定性生成器为 0，对于非确定性生成器为非零。然而，这不是确定设备实际上是确定性还是非确定性的可靠方法。例如，GNU `libstdc++ `和 LLVM `libc++ `都实现了一个非确定性设备，但是返回`0`表示熵。另一方面，`VC++ `和`boost.random`分别返回`32`和`10`，表示熵。\n\n所有这些生成器生成均匀分布的整数。然而，这只是大多数应用中需要随机数的许多可能的统计分布之一。为了能够在其他分布中产生数字(整数或实数)，该库提供了几个称为*分布*的类，它们根据引擎实现的统计分布转换引擎的输出。以下发行版可用:\n\n| **类型** | **类名** | **数字** | **统计分布** |\n| 制服 | `uniform_int_distribution` | 整数 | 制服 |\n|  | `uniform_real_distribution` | 真实的 | 制服 |\n| 伯努利 | `bernoulli_distribution` | 布尔 | 伯努利 |\n|  | `binomial_distribution` | 整数 | 二项式 |\n|  | `negative_binomial_distribution` | 整数 | 负二项式 |\n|  | `geometric_distribution` | 整数 | 几何学的 |\n| 泊松 | `poisson_distribution` | 整数 | 泊松 |\n|  | `exponential_distribution` | 真实的 | 指数的 |\n|  | `gamma_distribution` | 真实的 | 微克 |\n|  | `weibull_distribution` | 真实的 | （统计学家）威伯尔（或韦布尔） |\n|  | `extreme_value_distribution` | 真实的 | 极端值 |\n| 常态 | `normal_distribution` | 真实的 | 标准正态(高斯) |\n|  | `lognormal_distribution` | 真实的 | 对数正态的 |\n|  | `chi_squared_distribution` | 真实的 | 卡方检定 |\n|  | `cauchy_distribution` | 真实的 | 柯西 |\n|  | `fisher_f_distribution` | 真实的 | 费希尔分布 |\n|  | `student_t_distribution` | 真实的 | 学生 t 分布 |\n| 抽样 | `discrete_distribution` | 整数 | 分离的 |\n|  | `piecewise_constant_distribution` | 真实的 | 分布在常数子区间上的值 |\n|  | `piecewise_linear_distribution` | 真实的 | 在定义的子区间上分布的值 |\n\n该库提供的每个引擎都有优点和缺点。线性同余发动机内部状态小，但速度不是很快。另一方面，带进位引擎的减法非常快，但是需要更多的内存用于内部状态。梅森尼绕口令是其中最慢的，也是内部状态最大的，但如果初始化得当，可以产生最长的不重复的数字序列。在下面的例子中，我们将使用`std::mt19937`，一个内部状态为 19，937 位的 32 位默森扭转器。\n\n生成随机数的最简单方法如下所示:\n\n```cpp\n    auto mtgen = std::mt19937 {}; \n    for (auto i = 0; i < 10; ++ i) \n      std::cout << mtgen() << std::endl;\n```\n\n在这个例子中，`mtgen`是一个`std::mt19937`默森龙卷风。要生成数字，您只需要使用 call 运算符，该运算符将内部状态提前并返回下一个伪随机数。然而，这个代码是有缺陷的，因为引擎没有种子。因此，它总是产生相同的数字序列，这在大多数情况下可能不是您想要的。\n\n有不同的方法来初始化引擎。C rand 库常见的一种方法是使用当前时间。在现代 C++ 中，它应该是这样的:\n\n```cpp\n    auto seed = std::chrono::high_resolution_clock::now() \n                .time_since_epoch() \n                .count(); \n    auto mtgen = std::mt19937{ static_cast<unsigned int>(seed) };\n```\n\n在这个例子中，`seed`是一个数字，表示从时钟的纪元到当前时刻的刻度数。然后，这个数字被用来为引擎播种。这种方法的问题是`seed`的值实际上是确定性的，在某些类别的应用中，它可能容易受到攻击。一种更可靠的方法是用实际的随机数给生成器播种。`std::random_device`类是一个应该返回真随机数的引擎，尽管实现实际上可能基于一个伪随机生成器:\n\n```cpp\n    std::random_device rd; \n    auto mtgen = std::mt19937 {rd()};\n```\n\n所有引擎产生的数字都遵循统一的分布。为了将结果转换成另一个统计分布，我们必须使用一个分布类。为了展示生成的数字是如何根据选定的分布进行分布的，我们将使用以下函数。该函数生成指定数量的伪随机数，并计算它们在地图中的重复次数。地图中的值随后被用于生成一个条形图，显示每个数字出现的频率:\n\n```cpp\n    void generate_and_print( \n      std::function<int(void)> gen,  \n      int const iterations = 10000) \n    { \n      // map to store the numbers and their repetition \n      auto data = std::map<int, int>{}; \n\n      // generate random numbers \n      for (auto n = 0; n < iterations; ++ n) \n        ++ data[gen()]; \n\n      // find the element with the most repetitions \n      auto max = std::max_element( \n                 std::begin(data), std::end(data),  \n                 [](auto kvp1, auto kvp2) { \n        return kvp1.second < kvp2.second; }); \n\n      // print the bars \n      for (auto i = max->second / 200; i > 0; --i) \n      { \n        for (auto kvp : data) \n        { \n          std::cout \n            << std::fixed << std::setprecision(1) << std::setw(3) \n            << (kvp.second / 200 >= i ? (char)219 : ' '); \n        } \n\n        std::cout << std::endl; \n      } \n\n      // print the numbers \n      for (auto kvp : data) \n      { \n        std::cout \n          << std::fixed << std::setprecision(1) << std::setw(3) \n          << kvp.first; \n      } \n\n      std::cout << std::endl; \n    }\n```\n\n以下代码使用`std::mt19937`引擎生成随机数，随机数在`[1, 6]`范围内均匀分布；这基本上就是你掷骰子得到的结果:\n\n```cpp\n    std::random_device rd{}; \n    auto mtgen = std::mt19937{ rd() }; \n    auto ud = std::uniform_int_distribution<>{ 1, 6 }; \n    generate_and_print([&mtgen, &ud]() {return ud(mtgen); });\n```\n\n程序的输出如下所示:\n\n![](img/0aa92f90-d587-4c3d-880b-bd0a859d81b4.png)\n\n在下一个也是最后一个例子中，我们将分布改变为均值`5`和标准差`2`的正态分布。这种分布产生实数；因此，为了使用前面的`generate_and_print()`函数，数字必须四舍五入为整数:\n\n```cpp\n    std::random_device rd{}; \n    auto mtgen = std::mt19937{ rd() }; \n    auto nd = std::normal_distribution<>{ 5, 2 }; \n\n    generate_and_print( \n      [&mtgen, &nd]() { \n        return static_cast<int>(std::round(nd(mtgen))); });\n```\n\n以下是早期代码的输出:\n\n![](img/2f94f238-cc47-44ce-a685-b2e9e437441e.png)\n\n# 请参见\n\n*   *初始化伪随机数发生器内部状态的所有位*\n\n# 初始化伪随机数发生器内部状态的所有位\n\n在前面的配方中，我们已经研究了伪随机数库及其组件，以及如何使用它来产生不同统计分布的数字。该配方中忽略的一个重要因素是伪随机数发生器的正确初始化。在本食谱中，您将学习如何初始化生成器，以便产生最佳的伪随机数序列。\n\n# 准备好\n\n您应该阅读前面的食谱*生成伪随机数*，以了解伪随机数库提供的内容。\n\n# 怎么做...\n\n要正确初始化伪随机数发生器以产生最佳的伪随机数序列，请执行以下步骤:\n\n1.  使用`std::random_device`产生用作种子值的随机数:\n\n```cpp\n        std::random_device rd;\n```\n\n2.  为引擎的所有内部位生成随机数据:\n\n```cpp\n        std::array<int, std::mt19937::state_size> seed_data {};\n        std::generate(std::begin(seed_data), std::end(seed_data), \n                      std::ref(rd));\n```\n\n3.  根据之前生成的伪随机数据创建一个`std::seed_seq`对象:\n\n```cpp\n        std::seed_seq seq(std::begin(seed_data), std::end(seed_data));\n```\n\n4.  创建一个引擎对象，并初始化代表引擎内部状态的所有位；例如，一个`mt19937`有 19937 位的内部状态:\n\n```cpp\n        auto eng = std::mt19937{ seq };\n```\n\n5.  根据应用的要求使用适当的分布:\n\n```cpp\n        auto dist = std::uniform_real_distribution<>{ 0, 1 };\n```\n\n# 它是如何工作的...\n\n在前面配方中显示的所有例子中，我们使用了`std::mt19937`引擎来产生伪随机数。虽然梅森扭扭器比其他引擎慢，但它可以产生最长的不重复数字序列，并具有最好的频谱特性。但是，以前面配方中显示的方式初始化发动机不会有这种效果。仔细分析(这超出了本食谱或本书的目的)，可以看出，引擎倾向于重复产生一些值，而忽略其他值，从而产生的数字不是均匀分布，而是二项式或泊松分布。问题是`mt19937`的内部状态有 624 个 32 位整数，在之前配方的例子中，我们只初始化了其中的一个。\n\n使用伪随机数字库时，请记住以下经验法则(显示在信息框中):\n\nIn order to produce the best results, engines must have all their internal state properly initialized before generating numbers.\n\n伪随机数库为此提供了一个名为`std::seed_seq`的类。这是一个生成器，可以植入任意数量的 32 位整数，并生成在 32 位空间中均匀分布的所需数量的整数。\n\n在前面的代码中*是怎么做到的...*部分，我们定义了一个名为`seed_data`的数组，其 32 位整数的个数等于`mt19937`生成器的内部状态；那是 624 个整数。然后，我们用一个`std::random_device`产生的随机数初始化数组。这个阵列后来被用来播种`std::seed_seq`，而`std::seed_seq`又被用来播种`mt19937`发电机。\n\n# 创建熟的用户定义文本\n\n文字是内置类型(数字、布尔、字符、字符串和指针)的常量，不能在程序中更改。该语言定义了一系列前缀和后缀来指定文字(前缀/后缀实际上是文字的一部分)。C++ 11 允许通过定义名为*文字运算符*的函数来创建用户定义的文字，该运算符引入了用于指定文字的后缀。这些仅适用于数字字符和字符串类型。这为在未来的版本中定义两种标准文字提供了可能性，并允许开发人员创建自己的文字。在这个食谱中，我们将看到如何创建我们自己的烹饪文字。\n\n# 准备好\n\n用户定义的文字可以有两种形式:*生的*和*熟的*。原始文字不被编译器处理，而熟文字是由编译器处理的值(示例可以包括处理字符串中的转义序列或识别数字值，如从文字 0xBAD 开始的整数 2898)。原始文字仅适用于整数和浮点类型，而熟文字也适用于字符和字符串文字。\n\n# 怎么做...\n\n要创建熟的用户定义文本，您应该遵循以下步骤:\n\n1.  在单独的名称空间中定义文字，以避免名称冲突。\n2.  始终用下划线(`_`)作为用户定义后缀的前缀。\n3.  为熟字面值定义以下形式的字面值运算符:\n\n```cpp\n        T operator \"\" _suffix(unsigned long long int); \n        T operator \"\" _suffix(long double); \n        T operator \"\" _suffix(char); \n        T operator \"\" _suffix(wchar_t); \n        T operator \"\" _suffix(char16_t); \n        T operator \"\" _suffix(char32_t); \n        T operator \"\" _suffix(char const *, std::size_t); \n        T operator \"\" _suffix(wchar_t const *, std::size_t); \n        T operator \"\" _suffix(char16_t const *, std::size_t); \n        T operator \"\" _suffix(char32_t const *, std::size_t);\n```\n\n以下示例创建一个用于指定千字节的用户定义文字:\n\n```cpp\n    namespace compunits \n    { \n      constexpr size_t operator \"\" _KB(unsigned long long const size) \n      { \n        return static_cast<size_t>(size * 1024); \n      } \n    } \n\n    auto size{ 4_KB };         // size_t size = 4096; \n\n    using byte = unsigned char; \n    auto buffer = std::array<byte, 1_KB>{};\n```\n\n# 它是如何工作的...\n\n当编译器遇到带有用户定义后缀`S`的用户定义文字时(对于第三方后缀，它总是有一个前导下划线，因为没有前导下划线的后缀是为标准库保留的)，它会进行非限定名称查找，以便用名称运算符`\"operator \"\" S`标识函数。如果它找到一个，那么它根据文字的类型和文字运算符的类型调用它。否则，编译器会产生错误。\n\n*中的例子是如何做到的...*部分，文字运算符称为`operator \"\" _KB`，参数类型为`unsigned long long int`。这是文字运算符处理整数类型的唯一可能的整数类型。类似地，对于浮点用户定义的文字，参数类型必须是`long double`，因为对于数值类型，文字运算符必须能够处理最大可能的值。这个文字操作符返回一个`constexpr`值，这样就可以在需要编译时值的地方使用它，比如指定数组的大小，如上例所示。\n\n当编译器识别出用户定义的文字并必须调用适当的用户定义文字运算符时，它将根据以下规则从重载集中选取重载:\n\n*   **对于整数文字**:它按照以下顺序调用:取`unsigned long long`的运算符、取`const char*`的原始文字运算符或文字运算符模板。\n*   **对于浮点文字**:它按照以下顺序调用:取`long double`的运算符、取`const char*`的原始文字运算符或文字运算符模板。\n*   **对于字符文字**:它根据字符类型调用适当的运算符(`char`、`wchar_t`、`char16_t`和`char32_t`)。\n*   **对于字符串文字**:它调用适当的运算符，这取决于指向字符串的指针的字符串类型和大小。\n\n在下面的例子中，我们定义了一个单位和数量的系统。我们希望用公斤、件、升和其他类型的单位来操作。这在可以处理订单的系统中可能很有用，您需要为每件商品指定数量和单位。命名空间`units`中定义了以下内容:\n\n*   可能的单位类型(千克、米、升和件)的限定范围的枚举:\n\n```cpp\n        enum class unit { kilogram, liter, meter, piece, };\n```\n\n*   指定特定单位数量(如 3.5 公斤或 42 件)的类别模板:\n\n```cpp\n        template <unit U> \n        class quantity \n        {\n          const double amount; \n          public: \n            constexpr explicit quantity(double const a) : \n              amount(a) {} \n\n          explicit operator double() const { return amount; } \n        };\n```\n\n*   `quantity`类模板的`operator+`和`operator-`功能是为了能够加减数量:\n\n```cpp\n        template <unit U> \n        constexpr quantity<U> operator+(quantity<U> const &q1, \n                                        quantity<U> const &q2) \n        {\n          return quantity<U>(static_cast<double>(q1) + \n                             static_cast<double>(q2)); \n        } \n\n        template <unit U> \n        constexpr quantity<U> operator-(quantity<U> const &q1, \n                                        quantity<U> const &q2)\n        {\n          return quantity<U>(static_cast<double>(q1) - \n                             static_cast<double>(q2));\n        }\n```\n\n*   用于创建`quantity`文字的文字运算符，在名为`unit_literals`的内部命名空间中定义。这样做的目的是避免可能与其他名称空间的文字发生名称冲突。如果确实发生了这样的冲突，开发人员可以在需要定义文字的范围内使用适当的名称空间来选择他们应该使用的名称空间:\n\n```cpp\n        namespace unit_literals \n        { \n          constexpr quantity<unit::kilogram> operator \"\" _kg( \n              long double const amount) \n          { \n            return quantity<unit::kilogram>  \n              { static_cast<double>(amount) }; \n          } \n\n          constexpr quantity<unit::kilogram> operator \"\" _kg( \n             unsigned long long const amount) \n          { \n            return quantity<unit::kilogram>  \n              { static_cast<double>(amount) }; \n          } \n\n          constexpr quantity<unit::liter> operator \"\" _l( \n             long double const amount) \n          { \n             return quantity<unit::liter>  \n               { static_cast<double>(amount) }; \n          } \n\n          constexpr quantity<unit::meter> operator \"\" _m( \n             long double const amount) \n          { \n            return quantity<unit::meter>  \n              { static_cast<double>(amount) }; \n          } \n\n          constexpr quantity<unit::piece> operator \"\" _pcs( \n             unsigned long long const amount) \n          { \n            return quantity<unit::piece>  \n              { static_cast<double>(amount) }; \n          } \n        }\n```\n\n通过仔细观察，您可以注意到前面定义的文字运算符是不同的:\n\n*   `_kg`是为整数和浮点文字定义的；这使我们能够创建积分和浮点值，如`1_kg`和`1.0_kg`。\n*   `_l`和`_m`仅针对浮点文字定义；这意味着我们只能用浮点来定义这些单位的数量文字，比如`4.5_l`和`10.0_m`。\n*   `_pcs`仅针对整型文字定义；也就是说我们只能定义整数个件的数量，比如`42_pcs`。\n\n有了这些文字运算符，我们就可以对各种量进行运算。以下示例显示了有效和无效操作:\n\n```cpp\n    using namespace units; \n    using namespace unit_literals; \n\n    auto q1{ 1_kg };    // OK\n    auto q2{ 4.5_kg };  // OK\n    auto q3{ q1 + q2 }; // OK\n    auto q4{ q2 - q1 }; // OK\n\n    // error, cannot add meters and pieces \n    auto q5{ 1.0_m + 1_pcs }; \n    // error, cannot have an integer number of liters \n    auto q6{ 1_l }; \n    // error, can only have an integer number of pieces \n    auto q7{ 2.0_pcs}\n```\n\n`q1`是 1 公斤的量；这是一个整数值。由于存在重载`operator \"\" _kg(unsigned long long const)`，可以从整数 1 正确创建文字。同样，`q2`是 4.5 公斤的量；这是真正的价值。由于存在`overload operator \"\" _kg(long double)`，可以从双浮点值 4.5 创建文字。\n\n另一方面，`q6`是 1 升的量。由于没有重载`operator \"\" _l(unsigned long long)`，所以无法创建文字。这需要一个需要一个`unsigned long long`的过载，但是这样的过载是不存在的。类似地，`q7`是一个 2.0 块的量，但是块文字只能从整数值创建，因此，这会产生另一个编译器错误。\n\n# 还有更多...\n\n虽然用户定义的文字可以从 C++ 11 中获得，但是标准的文字运算符只能从 C++ 14 中获得。以下是这些标准文字运算符的列表:\n\n*   `operator\"\"s`用于定义`std::basic_string`文字:\n\n```cpp\n        using namespace std::string_literals; \n\n        auto s1{  \"text\"s }; // std::string \n        auto s2{ L\"text\"s }; // std::wstring \n        auto s3{ u\"text\"s }; // std::u16string \n        auto s4{ U\"text\"s }; // std::u32string\n```\n\n*   用于创建`std::chrono::duration`值的`operator\"\"h`、`operator\"\"min`、`operator\"\"s`、`operator\"\"ms`、`operator\"\"us`和`operator\"\"ns`:\n\n```cpp\n        using namespace std::literals::chrono_literals; \n\n        // std::chrono::duration<long long> \n        auto timer {2h + 42min + 15s};\n```\n\n*   用于创建`std::complex`值的`operator\"\"if`、`operator\"\"i`和`operator\"\"il`:\n\n```cpp\n        using namespace std::literals::complex_literals; \n\n        auto c{ 12.0 + 4.5i }; // std::complex<double>\n```\n\n# 请参见\n\n*   *使用原始字符串文字来避免转义字符*\n*   *创建原始用户定义文字*\n\n# 创建原始用户定义的文字\n\n在前面的方法中，我们已经研究了 C++ 11 允许库实现者和开发人员创建用户定义的文本和 C++ 14 标准中可用的用户定义的文本的方式。但是，用户定义的文字有两种形式，一种是熟形式，其中文字值在提供给文字运算符之前由编译器处理，另一种是原始形式，其中文字不被编译器解析。后者仅适用于整型和浮点型。在本食谱中，我们将研究如何创建原始的用户定义文本。\n\n# 准备好\n\n在继续这个食谱之前，强烈建议您浏览前一个，创建熟的用户定义文字，因为这里不再重复关于用户定义文字的一般细节。\n\n为了举例说明如何创建原始的用户定义文字，我们将定义二进制文字。这些二进制文字可以是 8 位、16 位和 32 位(无符号)类型。这些类型将被称为`byte8`、`byte16`和`byte32`，我们创建的文字将被称为`_b8`、`_b16`和`_b32`。\n\n# 怎么做...\n\n要创建原始的用户定义文本，您应该遵循以下步骤:\n\n1.  在单独的名称空间中定义文字，以避免名称冲突。\n2.  始终用下划线(`_`)作为已用定义后缀的前缀。\n3.  定义以下形式的文字运算符或文字运算符模板:\n\n```cpp\n        T operator \"\" _suffix(const char*); \n\n        template<char...> T operator \"\" _suffix();\n```\n\n以下示例显示了 8 位、16 位和 32 位二进制文字的可能实现:\n\n```cpp\n    namespace binary \n    { \n      using byte8  = unsigned char; \n      using byte16 = unsigned short; \n      using byte32 = unsigned int; \n\n      namespace binary_literals \n      { \n        namespace binary_literals_internals \n        { \n          template <typename CharT, char... bits> \n          struct binary_struct; \n\n          template <typename CharT, char... bits> \n          struct binary_struct<CharT, '0', bits...> \n          { \n            static constexpr CharT value{ \n              binary_struct<CharT, bits...>::value }; \n          }; \n\n          template <typename CharT, char... bits> \n          struct binary_struct<CharT, '1', bits...> \n          { \n            static constexpr CharT value{ \n              static_cast<CharT>(1 << sizeof...(bits)) | \n              binary_struct<CharT, bits...>::value }; \n          }; \n\n          template <typename CharT> \n          struct binary_struct<CharT> \n          { \n            static constexpr CharT value{ 0 }; \n          }; \n        } \n\n        template<char... bits> \n        constexpr byte8 operator\"\"_b8() \n        { \n          static_assert( \n            sizeof...(bits) <= 8, \n            \"binary literal b8 must be up to 8 digits long\"); \n\n          return binary_literals_internals:: \n                    binary_struct<byte8, bits...>::value; \n        } \n\n        template<char... bits> \n        constexpr byte16 operator\"\"_b16() \n        { \n          static_assert( \n            sizeof...(bits) <= 16, \n            \"binary literal b16 must be up to 16 digits long\"); \n\n          return binary_literals_internals:: \n                    binary_struct<byte16, bits...>::value; \n        } \n\n        template<char... bits> \n        constexpr byte32 operator\"\"_b32() \n        { \n          static_assert( \n             sizeof...(bits) <= 32, \n             \"binary literal b32 must be up to 32 digits long\"); \n\n          return binary_literals_internals:: \n                    binary_struct<byte32, bits...>::value; \n        } \n\n      } \n    }\n```\n\n# 它是如何工作的...\n\n上一节中的实现使我们能够定义 1010_b8(十进制 10 的`byte8`值)或 000010101100_b16(十进制 2130496 的`byte16`值)形式的二进制文字。但是，我们希望确保不超过每种类型的位数。换句话说，像 111100001_b8 这样的值应该是非法的，编译器应该会产生错误。\n\n首先，我们定义名为`binary`的命名空间内的所有内容，并从引入几个类型别名(`byte8`、`byte16`和`byte32`)开始。\n\n文字运算符模板在名为`binary_literal_internals`的嵌套命名空间中定义。这是一种很好的做法，可以避免名称与其他名称空间中的其他文字运算符冲突。如果发生类似的情况，您可以选择在正确的范围内使用适当的命名空间(例如函数或块中的一个命名空间，以及另一个函数或块中的另一个命名空间)。\n\n三个文字运算符模板非常相似。唯一不同的是它们的名称(`_b8`、`_16`、`_b32`)、返回类型(`byte8`、`byte16`和`byte32`)以及静态断言中检查位数的条件。\n\n我们将在后面的食谱中探讨变量模板和模板递归的细节；然而，为了更好地理解，这就是这个特定实现的工作原理:`bits`是一个模板参数包，它不是一个单一的值，而是模板可以实例化的所有值。例如，如果我们考虑文字`1010_b8`，那么文字运算符模板将被实例化为`operator\"\" _b8<'1', '0', '1', '0'>()`。在继续计算二进制值之前，我们检查文本中的位数。对于`_b8`，不得超过八(包括任何尾随零)。同样的，`_b16`最多 16 位，`_b32`最多 32 位。为此，我们使用`sizeof...`运算符返回参数包中的元素数量(在本例中为`bits`)。\n\n如果位数正确，我们可以继续扩展参数包，并递归计算二进制文字表示的十进制值。这是在额外的类模板及其专门化的帮助下完成的。这些模板被定义在另一个嵌套的名称空间中，称为`binary_literals_internals`。这也是一个很好的实践，因为它对客户端隐藏了(没有适当的限定)实现细节(除非显式的使用命名空间指令使它们对当前命名空间可用)。\n\nEven though this looks like recursion, it is not a true runtime recursion, because after the compiler expands and generates the code from templates, what we end up with is basically calls to overloaded functions with a different number of parameters. This is later explained in the recipe *Writing a function template with a variable number of arguments*.\n\n`binary_struct`类模板有一个模板类型`CharT`用于函数的返回类型(我们需要这个，因为我们的文字运算符模板应该返回`byte8`、`byte16`或`byte32`)和一个参数包:\n\n```cpp\n    template <typename CharT, char... bits> \n    struct binary_struct;\n```\n\n参数包分解提供了这个类模板的几个专门化。当包的第一个数字为“0”时，计算值保持不变，我们继续扩展包的其余部分。如果包的第一个数字是“1”，则新值向左移动 1，包位的其余部分的数字或包的其余部分的值:\n\n```cpp\n    template <typename CharT, char... bits> \n    struct binary_struct<CharT, '0', bits...> \n    { \n      static constexpr CharT value{ \n        binary_struct<CharT, bits...>::value }; \n    }; \n\n    template <typename CharT, char... bits> \n    struct binary_struct<CharT, '1', bits...> \n    { \n      static constexpr CharT value{ \n        static_cast<CharT>(1 << sizeof...(bits)) | \n        binary_struct<CharT, bits...>::value }; \n    };\n```\n\n最后一个特殊化涵盖了包装为空时的情况；在这种情况下，我们返回 0:\n\n```cpp\n    template <typename CharT> \n    struct binary_struct<CharT> \n    { \n      static constexpr CharT value{ 0 }; \n    };\n```\n\n在定义了这些辅助类之后，我们可以按照预期实现`byte8`、`byte16`和`byte32`二进制文本。请注意，我们需要将名称空间`binary_literals`的内容带入当前名称空间，以便使用文字运算符模板:\n\n```cpp\n    using namespace binary; \n    using namespace binary_literals; \n    auto b1 = 1010_b8; \n    auto b2 = 101010101010_b16; \n    auto b3 = 101010101010101010101010_b32;\n```\n\n以下定义触发编译器错误，因为不满足`static_assert`中的条件:\n\n```cpp\n    // binary literal b8 must be up to 8 digits long \n    auto b4 = 0011111111_b8; \n    // binary literal b16 must be up to 16 digits long \n    auto b5 = 001111111111111111_b16; \n    // binary literal b32 must be up to 32 digits long \nauto b6 = 0011111111111111111111111111111111_b32;\n```\n\n# 请参见\n\n*   *使用原始字符串文字来避免转义字符* s\n*   *创建熟的用户定义文字*\n*   *编写可变参数数的函数模板 [食谱第 10 章](10.html)*探索函数**\n*   *创建类型别名和别名模板[第八章](08.html)*学习现代核心语言功能**\n\n# 使用原始字符串避免转义字符\n\n字符串可能包含特殊字符，如不可打印字符(换行符、水平和垂直制表符等)、字符串和字符分隔符(双引号和单引号)或任意八进制、十六进制或 Unicode 值。这些特殊字符以转义序列引入，转义序列以反斜杠开头，后跟字符(示例包括`'`和`\"`)、其指定字母(示例包括新行的`n`、水平制表符的`t`)或其值(示例包括八进制 050、十六进制 X7 或 Unicode U16F0)。因此，反斜杠字符本身必须用另一个反斜杠字符进行转义。这导致更复杂的文字字符串，很难阅读。\n\n为了避免转义字符，C++ 11 引入了不处理转义序列的原始字符串。在本食谱中，您将学习如何使用各种形式的原始字符串。\n\n# 准备好\n\n在本食谱中，以及本书的其余部分，我将使用`s`后缀来定义`basic_string`文字。这已经包含在食谱*创建烹饪用户定义的文字*中。\n\n# 怎么做...\n\n为了避免转义字符，请使用以下内容定义字符串文字:\n\n1.  `R\"( literal )\"`为默认形式:\n\n```cpp\n        auto filename {R\"(C:\\Users\\Marius\\Documents\\)\"s};\n        auto pattern {R\"((\\w+)=(\\d+)$)\"s}; \n\n        auto sqlselect { \n          R\"(SELECT * \n          FROM Books \n          WHERE Publisher='Paktpub' \n          ORDER BY PubDate DESC)\"s};\n```\n\n2.  `R\"delimiter( literal )delimiter\"`其中`delimiter`是实际字符串中不存在的任何字符序列，而序列`)\"`实际上应该是字符串的一部分。下面是一个用`!!`定界的例子:\n\n```cpp\n        auto text{ R\"!!(This text contains both \"( and )\".)!!\"s }; \n        std::cout << text << std::endl;\n```\n\n# 它是如何工作的...\n\n当使用字符串文字时，不处理转义，字符串的实际内容写在分隔符之间(换句话说，你看到的就是你得到的)。以下示例显示了显示为相同的原始文字字符串的内容；但是，第二个仍然包含转义字符。由于在字符串的情况下不处理这些，它们将像在输出中一样被打印:\n\n```cpp\n    auto filename1 {R\"(C:\\Users\\Marius\\Documents\\)\"s}; \n    auto filename2 {R\"(C:\\\\Users\\\\Marius\\\\Documents\\\\)\"s}; \n\n    // prints C:\\Users\\Marius\\Documents\\  \n    std::cout << filename1 << std::endl; \n\n    // prints C:\\\\Users\\\\Marius\\\\Documents\\\\  \n    std::cout << filename2 << std::endl;\n```\n\n如果文本必须包含`)\"`序列，那么必须使用不同的分隔符，以`R\"delimiter( literal )delimiter\"`形式。根据标准，分隔符中可能的字符如下:\n\nany member of the basic source character set except: space, the left parenthesis (the right parenthesis ), the backslash \\, and the control characters representing horizontal tab, vertical tab, form feed, and newline.\n\n原始字符串文字可以以`L`、`u8`、`u`和`U`之一作为前缀，以表示宽、UTF-8、UTF-16 或 UTF-32 字符串文字。以下是这种字符串文字的示例。请注意，字符串末尾出现的字符串文字`operator \"\"s`使编译器将类型推断为各种字符串类，而不是字符数组:\n\n```cpp\n    auto t1{ LR\"(text)\"  };  // const wchar_t* \n    auto t2{ u8R\"(text)\" };  // const char* \n    auto t3{ uR\"(text)\"  };  // const char16_t* \n    auto t4{ UR\"(text)\"  };  // const char32_t* \n\n    auto t5{ LR\"(text)\"s  }; // wstring \n    auto t6{ u8R\"(text)\"s }; // string \n    auto t7{ uR\"(text)\"s  }; // u16string \n    auto t8{ UR\"(text)\"s  }; // u32string\n```\n\n# 请参见\n\n*   *创建熟的用户定义文字*\n\n# 创建字符串助手库\n\n标准库中的字符串类型是一种通用实现，缺少许多有用的方法，例如改变大小写、修剪、拆分以及其他可能满足不同开发人员需求的方法。存在提供丰富字符串功能集的第三方库。然而，在这个食谱中，我们将看看实现几个简单但有帮助的方法，你可能经常需要在实践中。目的是更好地了解如何使用字符串方法和标准的通用算法来操作字符串，同时也是为了参考可在应用中使用的可重用代码。\n\n在本食谱中，我们将实现一个小型字符串实用程序库，它将提供以下功能:\n\n*   将字符串更改为小写或大写。\n*   反转一根绳子。\n*   从字符串的开头和/或结尾修剪空白。\n*   从字符串的开头和/或结尾修剪一组特定的字符。\n*   删除字符串中任何位置出现的字符。\n*   使用特定分隔符标记字符串。\n\n# 准备好\n\n我们将要实现的字符串库应该与所有标准字符串类型一起工作，`std::string`、`std::wstring`、`std::u16string`和`std::u32string`。为了避免指定像`std::basic_string<CharT, std::char_traits<CharT>, std::allocator<CharT>>`这样的长名称，我们将为字符串和字符串流使用以下别名模板:\n\n```cpp\n    template <typename CharT> \n    using tstring =  \n       std::basic_string<CharT, std::char_traits<CharT>,  \n                         std::allocator<CharT>>; \n\n    template <typename CharT> \n    using tstringstream =  \n       std::basic_stringstream<CharT, std::char_traits<CharT>,  \n                               std::allocator<CharT>>;\n```\n\n为了实现这些字符串助手函数，我们需要包含字符串的标题`<string>`和我们将使用的通用标准算法的标题`<algorithm>`。\n\n在这个食谱的所有例子中，我们将使用标准的用户定义的文字操作符来处理来自 C++ 14 的字符串，为此我们需要显式地使用`std::string_literals`命名空间。\n\n# 怎么做...\n\n1.  要将字符串转换为小写或大写，请使用通用算法`std::transform()`对字符串字符应用`tolower()`或`toupper()`功能:\n\n```cpp\n        template<typename CharT> \n        inline tstring<CharT> to_upper(tstring<CharT> text) \n        { \n          std::transform(std::begin(text), std::end(text), \n                         std::begin(text), toupper); \n          return text; \n        } \n\n        template<typename CharT> \n        inline tstring<CharT> to_lower(tstring<CharT> text) \n        { \n          std::transform(std::begin(text), std::end(text),  \n                         std::begin(text), tolower); \n          return text; \n        }\n```\n\n2.  要反转字符串，请使用通用算法`std::reverse()`:\n\n```cpp\n        template<typename CharT> \n        inline tstring<CharT> reverse(tstring<CharT> text) \n        { \n          std::reverse(std::begin(text), std::end(text)); \n          return text; \n        }\n```\n\n3.  要修剪一根弦，在开始、结束或两者时，使用`std::basic_string`的方法`find_first_not_of()`和`find_last_not_of()`:\n\n```cpp\n        template<typename CharT> \n        inline tstring<CharT> trim(tstring<CharT> const & text) \n        { \n          auto first{ text.find_first_not_of(' ') }; \n          auto last{ text.find_last_not_of(' ') }; \n          return text.substr(first, (last - first + 1)); \n        } \n\n        template<typename CharT> \n        inline tstring<CharT> trimleft(tstring<CharT> const & text) \n        { \n          auto first{ text.find_first_not_of(' ') }; \n          return text.substr(first, text.size() - first); \n        } \n\n        template<typename CharT> \n        inline tstring<CharT> trimright(tstring<CharT> const & text) \n        { \n          auto last{ text.find_last_not_of(' ') }; \n          return text.substr(0, last + 1); \n        }\n```\n\n4.  要从字符串中修剪给定集合中的字符，请使用`std::basic_string`的方法`find_first_not_of()`和`find_last_not_of()`的重载，它们采用定义要查找的字符集的字符串参数:\n\n```cpp\n        template<typename CharT> \n        inline tstring<CharT> trim(tstring<CharT> const & text,  \n                                   tstring<CharT> const & chars) \n        { \n          auto first{ text.find_first_not_of(chars) }; \n          auto last{ text.find_last_not_of(chars) }; \n          return text.substr(first, (last - first + 1)); \n        } \n\n        template<typename CharT> \n        inline tstring<CharT> trimleft(tstring<CharT> const & text,  \n                                       tstring<CharT> const & chars) \n        { \n          auto first{ text.find_first_not_of(chars) }; \n          return text.substr(first, text.size() - first); \n        } \n\n        template<typename CharT> \n        inline tstring<CharT> trimright(tstring<CharT> const &text, \n                                        tstring<CharT> const &chars) \n        { \n          auto last{ text.find_last_not_of(chars) }; \n          return text.substr(0, last + 1); \n        }\n```\n\n5.  要从字符串中删除字符，请使用`std::remove_if()`和`std::basic_string::erase()`:\n\n```cpp\n        template<typename CharT> \n        inline tstring<CharT> remove(tstring<CharT> text,  \n                                     CharT const ch) \n        { \n          auto start = std::remove_if( \n                          std::begin(text), std::end(text),  \n                          [=](CharT const c) {return c ==  ch; }); \n          text.erase(start, std::end(text)); \n          return text; \n        }\n```\n\n6.  要根据指定的分隔符分割字符串，请使用`std::getline()`从用字符串内容初始化的`std::basic_stringstream`中读取。从流中提取的标记被推入字符串向量:\n\n```cpp\n        template<typename CharT> \n        inline std::vector<tstring<CharT>> split \n           (tstring<CharT> text, CharT const delimiter) \n        {\n          auto sstr = tstringstream<CharT>{ text }; \n          auto tokens = std::vector<tstring<CharT>>{}; \n          auto token = tstring<CharT>{}; \n          while (std::getline(sstr, token, delimiter))  \n          { \n            if (!token.empty()) tokens.push_back(token); \n          } \n          return tokens; \n        }\n```\n\n# 它是如何工作的...\n\n为了实现库中的实用函数，我们有两个选项:\n\n*   函数会修改由引用传递的字符串。\n*   函数不会改变原始字符串，而是返回一个新字符串。\n\n第二个选项的优点是它保留了原始字符串，这在许多情况下可能会有所帮助。否则，在这些情况下，您将首先必须制作一个字符串的副本，并更改该副本。该配方中提供的实现采用了第二种方法。\n\n我们在*中实现的第一个功能是如何做到的...*段分别为`to_upper()`和`to_lower()`。这些函数将字符串的内容更改为大写或小写。实现这一点的最简单方法是使用`std::transform()`标准算法。这是一个通用算法，它将一个函数应用于一个范围(由开始和结束迭代器定义)的每个元素，并将结果存储在另一个只需要指定开始迭代器的范围中。输出范围可以与输入范围相同，这正是我们对字符串所做的转换。应用功能为`toupper()`或`tolower()`:\n\n```cpp\n    auto ut{ string_library::to_upper(\"this is not UPPERCASE\"s) };  \n    // ut = \"THIS IS NOT UPPERCASE\" \n\n    auto lt{ string_library::to_lower(\"THIS IS NOT lowercase\"s) };  \n    // lt = \"this is not lowercase\"\n```\n\n我们考虑的下一个函数是`reverse()`，顾名思义，它反转字符串的内容。为此，我们使用了`std::reverse()`标准算法。这个通用算法反转由开始和结束迭代器定义的范围的元素:\n\n```cpp\n    auto rt{string_library::reverse(\"cookbook\"s)}; // rt = \"koobkooc\"\n```\n\n说到修剪，可以在字符串的开头、结尾或两边进行修剪。正因为如此，我们实现了三个不同的功能:`trim()`用于两端的修剪，`trimleft()`用于字符串开头的修剪，`trimright()`用于字符串结尾的修剪。第一个版本的函数只修剪空格。为了找到合适的修剪部位，我们使用了`std::basic_string`的`find_first_not_of()`和`find_last_not_of()`方法。这些函数返回字符串中不是指定字符的第一个和最后一个字符。随后，对`std::basic_string`的`substr()`方法的调用返回一个新字符串。`substr()`方法获取字符串中的一个索引和一些要复制到新字符串中的元素:\n\n```cpp\n    auto text1{\"   this is an example   \"s}; \n    // t1 = \"this is an example\" \n    auto t1{ string_library::trim(text1) }; \n    // t2 = \"this is an example   \" \n    auto t2{ string_library::trimleft(text1) }; \n    // t3 = \"   this is an example\" \n    auto t3{ string_library::trimright(text1) };\n```\n\n从字符串中删除其他字符和空格有时会很有用。为此，我们为修剪函数提供了重载，这些重载指定了要移除的一组字符。该集合也被指定为字符串。该实现与前一个非常相似，因为`find_first_not_of()`和`find_last_not_of()`都有重载，重载采用包含要从搜索中排除的字符的字符串:\n\n```cpp\n    auto chars1{\" !%\\n\\r\"s}; \n    auto text3{\"!!  this % needs a lot\\rof trimming  !\\n\"s}; \n    auto t7{ string_library::trim(text3, chars1) };        \n    // t7 = \"this % needs a lot\\rof trimming\" \n    auto t8{ string_library::trimleft(text3, chars1) };    \n    // t8 = \"this % needs a lot\\rof trimming  !\\n\" \n    auto t9{ string_library::trimright(text3, chars1) };   \n    // t9 = \"!!  this % needs a lot\\rof trimming\"\n```\n\n如果需要从字符串的任何部分删除字符，修剪方法没有帮助，因为它们只处理字符串开头和结尾的连续字符序列。然而，为此，我们实现了一个简单的`remove()`方法。这使用`std:remove_if()`标准算法。`std::remove()`和`std::remove_if()`的工作方式起初可能不太直观。它们通过重新排列范围的内容(使用移动赋值)从第一个和最后一个迭代器定义的范围中移除满足条件的元素。需要移除的元素被放在范围的末尾，函数返回一个迭代器到范围中代表被移除元素的第一个元素。这个迭代器基本上定义了被修改的范围的新结束。如果没有删除任何元素，返回的迭代器就是原始范围的结束迭代器。这个返回的迭代器的值然后被用来调用`std::basic_string::erase()`方法，该方法实际上擦除了由两个迭代器定义的字符串的内容。我们这里的两个迭代器是`std::remove_if()`返回的迭代器和字符串的末尾:\n\n```cpp\n    auto text4{\"must remove all * from text**\"s}; \n    auto t10{ string_library::remove(text4, '*') };  \n    // t10 = \"must remove all  from text\" \n    auto t11{ string_library::remove(text4, '!') };  \n    // t11 = \"must remove all * from text**\"\n```\n\n我们实现的最后一个方法根据指定的分隔符拆分字符串的内容。有多种方法可以实现这一点。在这个实现中，我们使用了`std::getline()`。此函数从输入流中读取字符，直到找到指定的分隔符，并将字符放入字符串中。在开始从输入缓冲区读取之前，它调用输出字符串上的`erase()`来清除其内容。在循环中调用此方法会生成放置在向量中的标记。在我们的实现中，从结果集中跳过了空令牌:\n\n```cpp\n    auto text5{\"this text will be split   \"s}; \n    auto tokens1{ string_library::split(text5, ' ') };  \n    // tokens1 = {\"this\", \"text\", \"will\", \"be\", \"split\"} \n    auto tokens2{ string_library::split(\"\"s, ' ') };    \n    // tokens2 = {}\n```\n\n# 请参见\n\n*   *创建熟的用户定义文字*\n*   *创建类型别名和别名模板[第八章](08.html)*学习现代核心语言功能**\n\n# 使用正则表达式验证字符串的格式\n\n正则表达式是一种用于在文本中执行模式匹配和替换的语言。C++ 11 通过标题`<regex>`中提供的一组类、算法和迭代器支持标准库中的正则表达式。在这个方法中，我们将看到如何使用正则表达式来验证字符串是否匹配模式(示例可以包括验证电子邮件或 IP 地址格式)。\n\n# 准备好\n\n在整个食谱中，我们将在必要时解释我们使用的正则表达式的细节。但是，为了使用 C++ 正则表达式标准库，您应该至少有一些正则表达式的基本知识。对正则表达式语法和标准的描述超出了本书的目的；如果您不熟悉正则表达式，建议您在继续学习专注于正则表达式的食谱之前，先阅读更多关于正则表达式的内容。\n\n# 怎么做...\n\n为了验证字符串是否与正则表达式匹配，请执行以下步骤:\n\n1.  包括头文件`<regex>`和`<string>`以及 C++ 14 标准用户定义字符串的命名空间`std::string_literals`:\n\n```cpp\n        #include <regex> \n        #include <string> \n        using namespace std::string_literals;\n```\n\n2.  使用原始字符串文字指定正则表达式，以避免转义反斜杠(这种情况经常发生)。以下正则表达式验证大多数电子邮件格式:\n\n```cpp\n        auto pattern {R\"(^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}$)\"s};\n```\n\n3.  创建一个`std::regex` / `std::wregex`对象(取决于所使用的字符集)来封装正则表达式:\n\n```cpp\n        auto rx = std::regex{pattern};\n```\n\n4.  若要忽略大小写或指定其他解析选项，请使用重载构造函数，该构造函数具有用于正则表达式标志的额外参数:\n\n```cpp\n        auto rx = std::regex{pattern, std::regex_constants::icase}; \n```\n\n5.  使用`std::regex_match()`将正则表达式与整个字符串匹配:\n\n```cpp\n        auto valid = std::regex_match(\"marius@domain.com\"s, rx);\n```\n\n# 它是如何工作的...\n\n考虑到验证电子邮件地址格式的问题，尽管这看起来是一个微不足道的问题，但实际上很难找到一个简单的正则表达式来涵盖有效电子邮件格式的所有可能情况。在这个方法中，我们不会试图找到最终的正则表达式，而是应用一个在大多数情况下足够好的正则表达式。我们将用于此目的的正则表达式如下:\n\n```cpp\n    ^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}$\n```\n\n下表解释了正则表达式的结构:\n\n| **部分** | **描述** |\n| `^` | 字符串开头 |\n| `[A-Z0-9._%+-]+` | 范围 A-Z、0-9 或-、%、+或-中的至少一个字符，表示电子邮件地址的本地部分 |\n| `@` | 字符@ |\n| `[A-Z0-9.-]+` | A-Z、0-9 或-、%、+或-中的至少一个字符，表示域部分的主机名 |\n| `\\.` | 分隔域主机名和标签的点 |\n| `[A-Z]{2,}` | 一个域的域名系统标签，可以包含 2 到 63 个字符 |\n| `$` | 字符串的结尾 |\n\n请记住，在实践中，域名由主机名和以点分隔的域名标签列表组成。例子包括`localhost`、`gmail.com`或`yahoo.co.uk`。我们使用的这个正则表达式与没有 DNS 标签的域不匹配，比如 localhost(一封电子邮件，比如`root@localhost`是一封有效的电子邮件)。域名也可以是括号内指定的 IP 地址，如`[192.168.100.11]`(如`john.doe@[192.168.100.11]`)。包含此类域的电子邮件地址将与上面定义的正则表达式不匹配。即使这些相当罕见的格式不匹配，正则表达式也可以覆盖大多数电子邮件格式。\n\nThe regular expression in the example in this chapter is provided for didactical purposes only, and it is not intended for being used as it is in production code. As explained earlier, this sample does not cover all possible e-mail formats.\n\n我们从包含必要的头开始，正则表达式为`<regex>`，字符串为`<string>`。下面显示的`is_valid_email()`功能(基本包含了*的样本)如何操作...* section)获取一个表示电子邮件地址的字符串，并返回一个布尔值，指示电子邮件是否具有有效的格式。我们首先构造一个`std::regex`对象来封装用原始字符串表示的正则表达式。使用原始字符串文字很有帮助，因为它避免了转义反斜杠，反斜杠也用于正则表达式中的转义字符。然后，该函数调用`std::regex_match()`，传递输入文本和正则表达式:\n\n```cpp\n    bool is_valid_email_format(std::string const & email) \n    { \n      auto pattern {R\"(^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}$)\"s}; \n\n      auto rx = std::regex{pattern}; \n\n      return std::regex_match(email, rx); \n    }\n```\n\n`std::regex_match()`方法试图将正则表达式与整个字符串进行匹配。如果成功则返回`true`，否则返回`false`:\n\n```cpp\n    auto ltest = [](std::string const & email)  \n    { \n      std::cout << std::setw(30) << std::left  \n                << email << \" : \"  \n                << (is_valid_email_format(email) ?  \n                   \"valid format\" : \"invalid format\") \n                << std::endl; \n    }; \n\n    ltest(\"JOHN.DOE@DOMAIN.COM\"s);         // valid format \n    ltest(\"JOHNDOE@DOMAIL.CO.UK\"s);        // valid format \n    ltest(\"JOHNDOE@DOMAIL.INFO\"s);         // valid format \n    ltest(\"J.O.H.N_D.O.E@DOMAIN.INFO\"s);   // valid format \n    ltest(\"ROOT@LOCALHOST\"s);              // invalid format \n    ltest(\"john.doe@domain.com\"s);         // invalid format\n```\n\n在这个简单的测试中，唯一与正则表达式不匹配的电子邮件是`ROOT@LOCALHOST`和`john.doe@domain.com`。第一个包含没有带点前缀的 DNS 标签的域名，正则表达式中不包括这种情况。第二个只包含小写字母，在正则表达式中，本地部分和域名的有效字符集都是大写字母 A 到 z\n\n我们可以指定匹配可以忽略大小写，而不是用额外的有效字符(如`[A-Za-z0-9._%+-]`)使正则表达式复杂化。这可以通过给`std::basic_regex`类的构造函数增加一个参数来实现。用于此目的的可用常量在`regex_constants`命名空间中定义。以下对`is_valid_email_format()`的细微更改将使其忽略大小写，并允许小写和大写字母的电子邮件正确匹配正则表达式:\n\n```cpp\n    bool is_valid_email_format(std::string const & email) \n    { \n      auto rx = std::regex{ \n        R\"(^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}$)\"s, \n        std::regex_constants::icase}; \n\n      return std::regex_match(email, rx); \n    }\n```\n\n这个`is_valid_email_format()`函数非常简单，如果正则表达式作为参数与要匹配的文本一起提供，它可以用于匹配任何内容。然而，能够用单个函数处理多字节字符串(`std::string`)和宽字符串(`std::wstring`)就不错了。这可以通过创建一个函数模板来实现，其中字符类型作为模板参数提供:\n\n```cpp\n    template <typename CharT> \n    using tstring = std::basic_string<CharT, std::char_traits<CharT>,  \n                                      std::allocator<CharT>>; \n\n    template <typename CharT> \n    bool is_valid_format(tstring<CharT> const & pattern,  \n                         tstring<CharT> const & text) \n    { \n      auto rx = std::basic_regex<CharT>{  \n        pattern, std::regex_constants::icase }; \n\n      return std::regex_match(text, rx); \n    }\n```\n\n我们首先为`std::basic_string`创建一个别名模板，以简化其使用。新的`is_valid_format()`函数是一个非常类似于我们实现`is_valid_email()` **的函数模板。**但是，我们现在使用`std::basic_regex<CharT>`代替 typedef `std::regex,`，也就是`std::basic_regex<char>,`，并且模式作为第一个参数提供。我们现在为依赖于这个函数模板的宽字符串实现一个名为`is_valid_email_format_w()`的新函数。然而，该函数模板可以被重用来实现其他验证，例如如果车牌具有特定的格式:\n\n```cpp\n    bool is_valid_email_format_w(std::wstring const & text) \n    { \n      return is_valid_format( \n        LR\"(^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}$)\"s,  \n        text); \n    } \n\n    auto ltest2 = [](auto const & email) \n    { \n      std::wcout << std::setw(30) << std::left \n         << email << L\" : \" \n         << (is_valid_email_format_w(email) ? L\"valid\" : L\"invalid\") \n         << std::endl; \n    }; \n\n    ltest2(L\"JOHN.DOE@DOMAIN.COM\"s);       // valid\n    ltest2(L\"JOHNDOE@DOMAIL.CO.UK\"s);      // valid\n    ltest2(L\"JOHNDOE@DOMAIL.INFO\"s);       // valid\n    ltest2(L\"J.O.H.N_D.O.E@DOMAIN.INFO\"s); // valid\n    ltest2(L\"ROOT@LOCALHOST\"s);            // invalid\n    ltest2(L\"john.doe@domain.com\"s);       // valid\n```\n\n在上面显示的所有例子中，唯一不匹配的是`ROOT@LOCAHOST`，正如已经预料到的那样。\n\n事实上，`std::regex_match()`方法有几个重载，其中一些重载有一个参数，该参数是对存储匹配结果的`std::match_results`对象的引用。如果没有匹配，则`std::match_results`为空，大小为 0。否则，如果匹配，则`std::match_results`对象不为空，其大小为 1 加上匹配的子表达式数。\n\n该函数的以下版本使用上述重载，并在`std::smatch`对象中返回匹配的子表达式。请注意，正则表达式发生了变化，因为定义了三个标题组-一个用于本地部分，一个用于域的主机名部分，一个用于域名系统标签。如果匹配成功，那么`std::smatch`对象将包含四个子匹配对象:第一个匹配整个字符串，第二个匹配第一个捕获组(本地部分)，第三个匹配第二个捕获组(主机名)，第四个匹配第三个也是最后一个捕获组(DNS 标签)。结果以元组形式返回，其中第一项实际上指示成功或失败:\n\n```cpp\n    std::tuple<bool, std::string, std::string, std::string>\n    is_valid_email_format_with_result(std::string const & email) \n    { \n      auto rx = std::regex{  \n        R\"(^([A-Z0-9._%+-]+)@([A-Z0-9.-]+)\\.([A-Z]{2,})$)\"s,  \n        std::regex_constants::icase }; \n      auto result = std::smatch{}; \n      auto success = std::regex_match(email, result, rx); \n\n      return std::make_tuple( \n        success,  \n        success ? result[1].str() : \"\"s, \n        success ? result[2].str() : \"\"s,  \n        success ? result[3].str() : \"\"s); \n    }\n```\n\n按照前面的代码，我们使用 C++ 17 结构化绑定将元组的内容解包到命名变量中:\n\n```cpp\n    auto ltest3 = [](std::string const & email) \n    { \n      auto [valid, localpart, hostname, dnslabel] =  \n       is_valid_email_format_with_result(email); \n\n      std::cout << std::setw(30) << std::left \n         << email << \" : \" \n         << std::setw(10) << (valid ? \"valid\" : \"invalid\") \n         << \"local=\" << localpart  \n         << \";domain=\" << hostname  \n         << \";dns=\" << dnslabel \n         << std::endl; \n    }; \n\n    ltest3(\"JOHN.DOE@DOMAIN.COM\"s); \n    ltest3(\"JOHNDOE@DOMAIL.CO.UK\"s); \n    ltest3(\"JOHNDOE@DOMAIL.INFO\"s); \n    ltest3(\"J.O.H.N_D.O.E@DOMAIN.INFO\"s); \n    ltest3(\"ROOT@LOCALHOST\"s); \n    ltest3(\"john.doe@domain.com\"s);\n```\n\n程序的输出如下:\n\n```cpp\n JOHN.DOE@DOMAIN.COM            : valid \n local=JOHN.DOE;domain=DOMAIN;dns=COM \n JOHNDOE@DOMAIL.CO.UK           : valid \n local=JOHNDOE;domain=DOMAIL.CO;dns=UK \n JOHNDOE@DOMAIL.INFO            : valid \n local=JOHNDOE;domain=DOMAIL;dns=INFO \n J.O.H.N_D.O.E@DOMAIN.INFO      : valid \n local=J.O.H.N_D.O.E;domain=DOMAIN;dns=INFO \n ROOT@LOCALHOST                 : invalid \n local=;domain=;dns= \n john.doe@domain.com            : valid \n local=john.doe;domain=domain;dns=com\n```\n\n# 还有更多...\n\n正则表达式有多个版本，C++ 标准库支持其中 6 个:ECMAScript、basic POSIX、扩展 POSIX、awk、grep、egrep (grep 带选项`-E`)。使用的默认语法是 ECMAScript，为了使用另一种语法，您必须在定义正则表达式时明确指定语法。除了指定语法，您还可以指定解析选项，例如通过忽略大小写进行匹配。\n\n标准库提供了比我们目前看到的更多的类和算法。库中可用的主要类如下(均为类模板，为方便起见，`typedef` s 针对不同的字符类型提供):\n\n*   类模板`std::basic_regex`定义正则表达式对象:\n\n```cpp\n        typedef basic_regex<char>    regex; \n        typedef basic_regex<wchar_t> wregex;\n```\n\n*   类模板`std::sub_match`表示匹配捕获组的字符序列；这个类实际上是从`std::pair`派生出来的，它的`first`和`second`成员表示匹配序列中第一个和过去结束字符的迭代器；如果没有匹配序列，则两个迭代器相等:\n\n```cpp\n        typedef sub_match<const char *>            csub_match; \n        typedef sub_match<const wchar_t *>         wcsub_match; \n        typedef sub_match<string::const_iterator>  ssub_match; \n        typedef sub_match<wstring::const_iterator> wssub_match;\n```\n\n*   类模板`std::match_results`是匹配的集合；第一个元素始终是目标中的完全匹配，其他元素是子表达式的匹配:\n\n```cpp\n        typedef match_results<const char *>            cmatch; \n        typedef match_results<const wchar_t *>         wcmatch; \n        typedef match_results<string::const_iterator>  smatch; \n        typedef match_results<wstring::const_iterator> wsmatch;\n```\n\n正则表达式标准库中可用的算法如下:\n\n*   `std::regex_match()`:这试图将正则表达式(由`std::basic_regex`实例表示)与整个字符串进行匹配。\n*   `std::regex_search()`:这试图将正则表达式(由`std::basic_regex`实例表示)与字符串的一部分(包括整个字符串)相匹配。\n*   `std::regex_replace()`:这将根据指定的格式替换正则表达式中的匹配项。\n\n正则表达式标准库中可用的迭代器如下:\n\n*   `std::regex_interator`:常量前向迭代器，用于遍历字符串中出现的模式。它有一个指向`std::basic_regex`的指针，该指针必须存在，直到迭代器被销毁。在创建和递增时，迭代器调用`std::regex_search()`并存储算法返回的`std::match_results`对象的副本。\n*   `std::regex_token_iterator`:常量前向迭代器，用于遍历字符串中正则表达式的每个匹配的子匹配。在内部，它使用`std::regex_iterator`来遍历子匹配。因为它存储一个指向`std::basic_regex`实例的指针，正则表达式对象必须存在，直到迭代器被销毁。\n\n# 请参见\n\n*   *使用正则表达式解析字符串的内容*\n*   *使用正则表达式替换字符串的内容*\n*   *使用结构化绑定处理多返回值[第八章](08.html)*食谱学习现代核心语言特性**\n\n# 使用正则表达式解析字符串的内容\n\n在前面的食谱中，我们已经了解了如何使用`std::regex_match()`来验证字符串的内容是否符合特定的格式。该库提供了另一种称为`std::regex_search()`的算法，该算法将正则表达式与字符串的任何部分进行匹配，而不像`regex_match()`那样只匹配整个字符串。但是，该函数不允许搜索输入字符串中所有出现的正则表达式。为此，我们需要使用库中可用的迭代器类之一。\n\n在本食谱中，您将学习如何使用正则表达式解析字符串的内容。为此，我们将考虑解析包含名称-值对的文本文件的问题。每个这样的对被定义在不同的行上，格式为`name = value`，但是以`#`开头的行代表注释，必须被忽略。以下是一个例子:\n\n```cpp\n    #remove # to uncomment the following lines \n    timeout=120 \n    server = 127.0.0.1 \n\n    #retrycount=3\n```\n\n# 准备好\n\n有关 C++ 11 中正则表达式支持的一般信息，请参考*使用正则表达式验证字符串格式*方法。继续这个食谱需要基本的正则表达式知识。\n\n在以下示例中，`text`是一个变量，定义如下:\n\n```cpp\n    auto text { \n      R\"( \n        #remove # to uncomment the following lines \n        timeout=120 \n        server = 127.0.0.1 \n\n        #retrycount=3 \n      )\"s};\n```\n\n# 怎么做...\n\n为了通过字符串搜索正则表达式的出现，您应该执行以下操作:\n\n1.  包括头文件`<regex>`和`<string>`以及 C++ 14 标准用户定义字符串的命名空间`std::string_literals`:\n\n```cpp\n        #include <regex> \n        #include <string> \n        using namespace std::string_literals;\n```\n\n2.  使用原始字符串文字指定正则表达式，以避免转义反斜杠(这种情况经常发生)。以下正则表达式验证了前面建议的文件格式:\n\n```cpp\n        auto pattern {R\"(^(?!#)(\\w+)\\s*=\\s*([\\w\\d]+[\\w\\d._,\\-:]*)$)\"s};\n```\n\n3.  创建一个`std::regex` / `std::wregex`对象(取决于所使用的字符集)来封装正则表达式:\n\n```cpp\n        auto rx = std::regex{pattern};\n```\n\n4.  要在给定文本中搜索正则表达式的第一次出现，请使用通用算法`std::regex_search()`(示例 1):\n\n```cpp\n        auto match = std::smatch{}; \n        if (std::regex_search(text, match, rx)) \n        { \n          std::cout << match[1] << '=' << match[2] << std::endl; \n        }\n```\n\n5.  要查找给定文本中正则表达式的所有出现，请使用迭代器`std::regex_iterator`(示例 2):\n\n```cpp\n        auto end = std::sregex_iterator{}; \n        for (auto it=std::sregex_iterator{ std::begin(text),  \n                                           std::end(text), rx }; \n             it != end; ++ it) \n        { \n          std::cout << ''' << (*it)[1] << \"'='\"  \n                    << (*it)[2] << ''' << std::endl; \n        }\n```\n\n6.  要遍历匹配的所有子表达式，请使用迭代器`std::regex_token_iterator`(示例 3):\n\n```cpp\n        auto end = std::sregex_token_iterator{}; \n        for (auto it = std::sregex_token_iterator{ \n                          std::begin(text),  std::end(text), rx }; \n             it != end; ++ it) \n        { \n          std::cout << *it << std::endl; \n        }\n```\n\n# 它是如何工作的...\n\n可以解析前面显示的输入文件的简单正则表达式可能如下所示:\n\n```cpp\n    ^(?!#)(\\w+)\\s*=\\s*([\\w\\d]+[\\w\\d._,\\-:]*)$\n```\n\n这个正则表达式应该忽略所有以`#`开头的行；对于那些不以`#`开头的，匹配一个名称后跟等号，然后匹配一个可以由字母数字字符和其他几个字符(下划线、点、逗号等)组成的值。该正则表达式的确切含义解释如下:\n\n| **部分** | **描述** |\n| `^` | 行首 |\n| `(?!#)` | 确保不可能匹配#字符的负前瞻 |\n| `(\\w)+` | 表示至少一个单词字符的标识符的捕获组 |\n| `\\s*` | 有空白吗 |\n| `=` | 等号 |\n| `\\s*` | 有空白吗 |\n| `([\\w\\d]+[\\w\\d._,\\-:]*)` | 表示以字母数字字符开头的值的捕获组，但也可以包含点、逗号、反斜杠、连字符、冒号或下划线。 |\n| `$` | 行结束 |\n\n我们可以使用`std::regex_search()`在输入文本的任何地方搜索匹配。这个算法有几个重载，但通常它们的工作方式是相同的。您必须指定要处理的字符范围、包含匹配结果的输出`std::match_results`对象和表示正则表达式和匹配标志(定义搜索方式)的`std::basic_regex`对象。如果找到匹配项，则该函数返回`true`，否则返回`false`。\n\n在前一节的第一个例子中(见第四个列表项)，`match`是`std::smatch`的一个实例，它是以`string::const_iterator`为模板类型的`std::match_results`的一个`typedef`。如果找到匹配项，该对象将包含所有匹配子表达式的值序列中的匹配信息。索引 0 处的子匹配始终是整个匹配。索引 1 处的子匹配是第一个匹配的子表达式，索引 2 处的子匹配是第二个匹配的子表达式，依此类推。由于我们的正则表达式中有两个捕获组(子表达式)，如果成功的话，`std::match_results`将有三个子匹配。表示名称的标识符位于索引 1，等号后面的值位于索引 2。因此，该代码仅打印以下内容:\n\n```cpp\n timeout=120\n```\n\n`std::regex_search()`算法无法遍历文本中所有可能的匹配。为此，我们需要使用迭代器。`std::regex_iterator`就是为了这个目的。它不仅允许遍历所有匹配，还允许访问匹配的所有子匹配。迭代器实际上在构造和每次增量时调用`std::regex_search()`，并且它记住调用的结果`std::match_results`。默认构造函数创建一个迭代器，表示序列的结束，可用于测试匹配循环何时停止。\n\n在上一节的第二个例子中(见第五个列表项)，我们首先创建一个序列结束迭代器，然后开始遍历所有可能的匹配。构造时会调用`std::regex_match()`，如果找到匹配，我们可以通过当前迭代器访问它的结果。这将一直持续到没有找到匹配项(序列结束)。该代码将打印以下输出:\n\n```cpp\n 'timeout'='120' \n 'server'='127.0.0.1'\n```\n\n`std::regex_iterator`的替代物是`std::regex_token_iterator`。这与`std::regex_iterator`的工作方式类似，事实上，它内部包含这样一个迭代器，除了它使我们能够从匹配中访问特定的子表达式。这在*中的第三个例子中显示了如何做..*。部分(第 6 个列表项)。我们从创建序列结束迭代器开始，然后循环匹配，直到到达序列结束。在我们使用的构造函数中，我们没有指定要通过迭代器访问的子表达式的索引；因此，使用默认值 0。这意味着该程序将打印整个匹配:\n\n```cpp\n timeout=120 \n server = 127.0.0.1\n```\n\n如果我们只想访问第一个子表达式(在我们的例子中，这意味着名称)，我们所要做的就是在标记迭代器的构造函数中指定子表达式的索引。这一次，我们得到的输出只是名字:\n\n```cpp\n    auto end = std::sregex_token_iterator{}; \n    for (auto it = std::sregex_token_iterator{ std::begin(text),  \n                   std::end(text), rx, 1 }; \n         it != end; ++ it) \n    { \n      std::cout << *it << std::endl; \n    }\n```\n\n令牌迭代器的一个有趣之处在于，如果子表达式的索引为`-1`，它可以返回字符串中不匹配的部分，在这种情况下，它会返回一个`std::match_results`对象，该对象对应于最后一次匹配和序列结束之间的字符序列:\n\n```cpp\n    auto end = std::sregex_token_iterator{}; \n    for (auto it = std::sregex_token_iterator{ std::begin(text),  \n                   std::end(text), rx, -1 }; \n         it != end; ++ it) \n    { \n      std::cout << *it << std::endl; \n    }\n```\n\n该程序将输出以下内容(注意空行实际上是输出的一部分):\n\n```cpp\n\n #remove # to uncomment the following lines \n\n #retrycount=3\n```\n\n# 请参见\n\n*   *使用正则表达式验证字符串的格式*\n*   *使用正则表达式替换字符串的内容*\n\n# 使用正则表达式替换字符串的内容\n\n在最后两个菜谱中，我们已经了解了如何匹配字符串上的正则表达式或字符串的一部分，并迭代匹配和子匹配。正则表达式库还支持基于正则表达式的文本替换。在这个食谱中，我们将看到如何使用`std::regex_replace()`来执行这样的文本转换。\n\n# 准备好\n\n有关 C++ 11 中正则表达式支持的一般信息，请参考*使用正则表达式验证字符串格式*方法。\n\n# 怎么做...\n\n为了使用正则表达式执行文本转换，您应该执行以下操作:\n\n1.  包括字符串的`<regex>`和`<string>`以及 C++ 14 标准用户定义文字的名称空间`std::string_literals`:\n\n```cpp\n        #include <regex> \n        #include <string> \n        using namespace std::string_literals;\n```\n\n2.  使用带有替换字符串的`std::regex_replace()`算法作为第三个参数。考虑这个例子:用三个连字符替换所有由三个字符组成的单词，即`a`、`b`或`c`:\n\n```cpp\n        auto text{\"abc aa bca ca bbbb\"s}; \n        auto rx = std::regex{ R\"(\\b[a|b|c]{3}\\b)\"s }; \n        auto newtext = std::regex_replace(text, rx, \"---\"s);\n```\n\n3.  第三个参数使用`std::regex_replace()`算法，匹配标识符以`$`为前缀。例如，将“姓氏，名字”中的姓名替换为“名字姓氏”格式的姓名，如下所示:\n\n```cpp\n        auto text{ \"bancila, marius\"s }; \n        auto rx = std::regex{ R\"((\\w+),\\s*(\\w+))\"s }; \n        auto newtext = std::regex_replace(text, rx, \"$2 $1\"s);\n```\n\n# 它是如何工作的...\n\n`std::regex_replace()`算法有几个不同参数类型的重载，但是参数的含义如下:\n\n*   对其执行替换的输入字符串。\n*   一个`std::basic_regex`对象，它封装了用于标识要替换的字符串部分的正则表达式。\n*   用于替换的字符串格式。\n*   可选匹配标志。\n\n根据所使用的重载，返回值是作为参数提供的输出迭代器的字符串或副本。用于替换的字符串格式可以是简单的字符串，也可以是用`$`前缀表示的匹配标识符:\n\n*   `$&`表示整个比赛。\n*   `$1`、`$2`、`$3`等，表示第一、二、三子赛，以此类推。\n*   `$``表示字符串在第一次匹配之前的部分。\n*   `$'`表示最后一次匹配后的字符串部分。\n\n在第一个例子中显示的*怎么做...*部分，初始文本包含两个由三个`a`、`b`或`c`字符、`abc`和`bca`组成的单词。正则表达式表示单词边界之间正好有三个字符的表达式。这意味着一个潜台词，如`bbbb`，将与表达不匹配。替换的结果是字符串文本将是`--- aa --- ca bbbb`。\n\n匹配的附加标志可以指定给`std::regex_replace()`算法。默认情况下，匹配标志是`std::regex_constants::match_default`，它基本上将 ECMAScript 指定为用于构造正则表达式的语法。例如，如果我们想只替换第一个事件，那么我们可以指定`std::regex_constants::format_first_only`。在下一个例子中，结果是`--- aa bca ca bbbb`，因为在找到第一个匹配后替换停止:\n\n```cpp\n    auto text{ \"abc aa bca ca bbbb\"s }; \n    auto rx = std::regex{ R\"(\\b[a|b|c]{3}\\b)\"s }; \n    auto newtext = std::regex_replace(text, rx, \"---\"s, \n                     std::regex_constants::format_first_only);\n```\n\n但是，替换字符串可以包含整个匹配、特定子匹配或不匹配部分的特殊指示符，如前所述。在第二个例子中显示的*怎么做...*一节中，正则表达式标识一个至少包含一个字符的单词，后跟一个昏迷和可能的空格，然后是另一个至少包含一个字符的单词。第一个单词应该是姓，第二个单词应该是名。替换字符串具有`$2 $1`格式。这是一条用另一个字符串替换匹配表达式(在本例中，是整个原始字符串)的指令，该字符串由第二子匹配，后跟空格，然后是第一子匹配组成。\n\n在这种情况下，整个字符串是匹配的。在下一个示例中，字符串内将有多个匹配项，它们都将被指定的字符串替换。在本例中，我们将不定冠词 *a* 替换为不定冠词*a*:\n\n```cpp\n    auto text{\"this is a example with a error\"s}; \n    auto rx = std::regex{R\"(\\ba ((a|e|i|u|o)\\w+))\"s}; \n    auto newtext = std::regex_replace(text, rx, \"an $1\");\n```\n\n正则表达式将字母 *a* 识别为单个单词(`\\b`表示单词边界，因此`\\ba`表示单个字母 *a* 后跟空格的单词，以及至少两个以元音开头的字符的单词。当这样的匹配被识别时，它被替换为由固定字符串*和*组成的字符串，后跟空格和匹配的第一个子表达式，即单词本身。在这个例子中，`newtext`字符串将是*这是一个带有错误*的例子。\n\n除了子表达式的标识符(`$1`、`$2`等)，还有整个匹配的其他标识符(`$&`)、第一个匹配之前的字符串部分(`$``)和最后一个匹配之后的字符串部分(`$'`)。在最后一个示例中，我们将日期的格式从`dd.mm.yyyy`更改为`yyyy.mm.dd`，但也显示了匹配的部分:\n\n```cpp\n    auto text{\"today is 1.06.2016!!\"s}; \n    auto rx =  \n       std::regex{R\"((\\d{1,2})(\\.|-|/)(\\d{1,2})(\\.|-|/)(\\d{4}))\"s};       \n    // today is 2016.06.1!! \n    auto newtext1 = std::regex_replace(text, rx, R\"($5$4$3$2$1)\"); \n    // today is [today is ][1.06.2016][!!]!! \n    auto newtext2 = std::regex_replace(text, rx, R\"([$`][$&][$'])\");\n```\n\n正则表达式匹配一个一位数或两位数，后跟一个点、连字符或斜杠；接着是另一个一位数或两位数的数字；然后是点、连字符或斜线；最后一个四位数。\n\n对于`newtext1`，替换串为`$5$4$3$2$1`；这意味着年，其次是第二个分隔符，然后是月，第一个分隔符，最后是日。因此，对于输入字符串*“今天是 1.06.2016！”*，结果是*“今天是 2016.06.1！!\"*。\n\n对于`newtext2`，替换串为`[$`][$&][$']`；这意味着第一场比赛之前的部分，然后是整个比赛，最后是最后一场比赛之后的部分都在方括号中。然而，结果却不是*”【！！】【1.06.2016】【今天是】“*或许你乍一看可能会有所期待，但是*“今天是【今天是】【1.06.2016】【！！]!!\"*。原因是被替换的是匹配的表达式，在这种情况下，那只是日期(*“1 . 06 . 2016”*)。这个子字符串被替换为由初始字符串的所有部分组成的另一个字符串。\n\n# 请参见\n\n*   *使用正则表达式验证字符串的格式*\n*   *使用正则表达式解析字符串的内容*\n\n# 使用字符串视图代替常量字符串引用\n\n使用字符串时，即使您可能并不真正意识到，临时对象也会一直被创建。很多时候，临时对象是不相关的，只是为了将数据从一个地方复制到另一个地方(例如，从一个函数复制到它的调用者)。这代表了一个性能问题，因为它们需要内存分配和数据复制，这是需要避免的。为此，C++ 17 标准提供了一个名为`std::basic_string_view`的新字符串类模板，该模板表示对字符串(即字符序列)的非拥有常量引用。在这个食谱中，你将学习何时以及如何使用这个课程。\n\n# 准备好\n\n`string_view`类在`string_view`头中的名称空间`std`中可用。\n\n# 怎么做...\n\n您应该使用`std::string_view`将参数传递给函数(或从函数返回值)，而不是`std::string const &`，除非您的代码需要调用其他带有`std::string`参数的函数(在这种情况下，转换是必要的):\n\n```cpp\n    std::string_view get_filename(std::string_view str) \n    { \n      auto const pos1 {str.find_last_of('')}; \n      auto const pos2 {str.find_last_of('.')}; \n      return str.substr(pos1 + 1, pos2 - pos1 - 1); \n    } \n\n    char const file1[] {R\"(c:\\test\\example1.doc)\"}; \n    auto name1 = get_filename(file1); \n\n    std::string file2 {R\"(c:\\test\\example2)\"}; \n    auto name2 = get_filename(file2); \n\n    auto name3 = get_filename(std::string_view{file1, 16});\n```\n\n# 它是如何工作的...\n\n在我们看新的字符串类型是如何工作的之前，让我们考虑下面这个函数的例子，它应该提取一个没有扩展名的文件名。这基本上是在 C++ 17 之前，您将如何编写上一节中的函数。\n\nNote that in this example the file separator is `\\` (backslash) as in Windows. For Linux-based systems, it has to be changed to `/` (slash).\n\n```cpp\n    std::string get_filename(std::string const & str) \n    { \n      auto const pos1 {str.find_last_of('')}; \n      auto const pos2 {str.find_last_of('.')}; \n      return str.substr(pos1 + 1, pos2 - pos1 - 1); \n    } \n\n    auto name1 = get_filename(R\"(c:\\test\\example1.doc)\"); // example1 \n    auto name2 = get_filename(R\"(c:\\test\\example2)\");     // example2 \n    if(get_filename(R\"(c:\\test\\_sample_.tmp)\").front() == '_') {}\n```\n\n这是一个比较简单的功能。它对`std::string`进行常量引用，并标识由最后一个文件分隔符和最后一个点限定的子字符串，该点基本上代表没有扩展名(也没有文件夹名称)的文件名。\n\n然而，这种代码的问题在于，它会创建一个、两个，甚至更多的临时变量，这取决于编译器的优化。函数参数是常量`std::string`引用，但是函数是用字符串文字调用的，这意味着`std::string`需要从文字构造。这些临时人员需要分配和复制数据，这既耗时又耗费资源。在最后一个例子中，我们想要做的就是检查文件名的第一个字符是否是下划线，但是我们为此至少创建了两个临时字符串对象。\n\n`std::basic_string_view`类模板就是为了解决这个问题。这个类模板非常类似于`std::basic_string`，两者几乎有相同的接口。其原因是`std::basic_string_view`旨在代替对`std::basic_string`的持续引用，而无需进一步的代码更改。\n\n就像`std::basic_string`一样，所有类型的标准字符都有专门化:\n\n```cpp\n    typedef basic_string_view<char>     string_view; \n    typedef basic_string_view<wchar_t>  wstring_view; \n    typedef basic_string_view<char16_t> u16string_view; \n    typedef basic_string_view<char32_t> u32string_view;\n```\n\n`std::basic_string_view`类模板定义了对连续字符序列的引用。顾名思义，它代表一个视图，不能用于修改字符的引用序列。`std::basic_string_view`对象的大小相对较小，因为它只需要一个指向序列中第一个字符和长度的指针。它不仅可以由一个`std::basic_string`对象构成，还可以由一个指针和一个长度构成，或者由一个空终止的字符序列构成(在这种情况下，需要对字符串进行初始遍历才能找到长度)。因此`std::basic_string_view`类模板也可以作为多种类型字符串的通用接口(只要数据只需要读取即可)。另一方面，从`std::basic_string_view`转换到`std::basic_string`很容易，因为前者既有`to_string()`又有转换`operator std::basic_string`来创建新的`std::basic_string`对象。\n\n将`std::basic_string_view`传递给函数并返回`std::basic_string_view`仍然会创建这种类型的临时对象，但这些都是堆栈上的小对象(对于 64 位平台，指针和大小可以是 16 字节)；因此，它们应该比分配堆空间和复制数据产生更少的性能成本。\n\nNotice that all major compilers provide an implementation of std::basic_string that includes a small string optimization. Although the implementation details are different, they typically rely on having a statically allocated buffer of a number of characters (16 for VC++ and gcc 5 or newer) that does not involve heap operations, which are only required when the size of the string exceeds that number of characters.\n\n除了与`std::basic_string`相同的方法外，`std::basic_string_view`还有两种方法:\n\n*   `remove_prefix()`:缩小视图，以 *N* 字符开始，以 *N* 字符开始。\n*   `remove_suffix()`:通过减少 *N* 字符的长度来缩小视图。\n\n在以下示例中，两个成员函数用于从空格开始和结束处修剪`std::string_view`。函数的实现首先寻找第一个不是空格的元素，然后寻找最后一个不是空格的元素。然后，它从末尾移除最后一个非空格字符之后的所有内容，从开头移除所有内容，直到第一个非空格字符。该函数返回在两端修剪的新视图:\n\n```cpp\n    std::string_view trim_view(std::string_view str) \n    { \n      auto const pos1{ str.find_first_not_of(\" \") }; \n      auto const pos2{ str.find_last_not_of(\" \") }; \n      str.remove_suffix(str.length() - pos2 - 1); \n      str.remove_prefix(pos1); \n\n      return str; \n    } \n\n    auto sv1{ trim_view(\"sample\") }; \n    auto sv2{ trim_view(\"  sample\") }; \n    auto sv3{ trim_view(\"sample  \") }; \n    auto sv4{ trim_view(\"  sample  \") }; \n\n    auto s1{ sv1.to_string() }; \n    auto s2{ sv2.to_string() }; \n    auto s3{ sv3.to_string() }; \n    auto s4{ sv4.to_string() };\n```\n\nWhen using an `std::basic_string_view`, you must be aware of two things: you cannot change the underlying data referred by a view and you must manage the lifetime of the data, as the view is a non-owning reference.\n\n# 请参见\n\n*   *创建字符串助手库*"
  },
  {
    "path": "docs/mod-cpp/10.md",
    "content": "# 十、探索函数\n\n本章包含的配方如下:\n\n*   默认和删除的功能\n*   使用标准算法的 lambdas\n*   使用通用 lambdas\n*   编写递归 lambda\n*   用可变数量的参数编写函数模板\n*   使用折叠表达式简化变量函数模板\n*   实现高阶函数映射和折叠\n*   将函数组合成更高阶的函数\n*   统一调用任何可调用的东西\n\n# 默认和删除的功能\n\n在 C++ 中，类有特殊的成员(构造函数、析构函数和运算符)，这些成员可以由编译器默认实现，也可以由开发人员提供。然而，什么可以默认实现的规则有点复杂，可能会导致问题。另一方面，开发人员有时希望防止对象以特定的方式被复制、移动或构造。这可以通过使用这些特殊成员实现不同的技巧来实现。C++ 11 标准通过允许以我们将在下一节中看到的方式删除或默认函数，简化了其中的许多功能。\n\n# 入门指南\n\n对于这个食谱，你需要知道什么是特殊的成员函数，什么是可复制和可移动的手段。\n\n# 怎么做...\n\n使用以下语法指定如何处理函数:\n\n*   要默认一个函数，使用`=default`代替函数体。只能默认具有默认值的特殊类成员函数:\n\n```cpp\n        struct foo \n        { \n          foo() = default; \n        };\n```\n\n*   要删除一个函数，用`=delete`代替函数体。可以删除任何函数，包括非成员函数:\n\n```cpp\n        struct foo \n        { \n          foo(foo const &) = delete; \n        }; \n\n        void func(int) = delete;\n```\n\n使用默认和删除的功能来实现各种设计目标，例如以下示例:\n\n*   要实现不可复制且隐式不可移动的类，请将复制操作声明为已删除:\n\n```cpp\n        class foo_not_copyable \n        { \n        public: \n          foo_not_copyable() = default; \n\n          foo_not_copyable(foo_not_copyable const &) = delete; \n          foo_not_copyable& operator=(foo_not_copyable const&) = delete; \n        };\n```\n\n*   若要实现不可复制但可移动的类，请将复制操作声明为已删除，并显式实现移动操作(并提供所需的任何附加构造函数):\n\n```cpp\n        class data_wrapper \n        { \n          Data* data; \n        public: \n          data_wrapper(Data* d = nullptr) : data(d) {} \n          ~data_wrapper() { delete data; } \n\n          data_wrapper(data_wrapper const&) = delete; \n          data_wrapper& operator=(data_wrapper const &) = delete; \n\n          data_wrapper(data_wrapper&& o) :data(std::move(o.data))  \n          {  \n            o.data = nullptr;  \n          } \n\n          data_wrapper& operator=(data_wrapper&& o) \n          { \n            if (this != &o) \n            { \n              delete data; \n              data = std::move(o.data); \n              o.data = nullptr; \n            } \n\n            return *this; \n          } \n        };\n```\n\n*   为了确保只使用特定类型的对象调用函数，并且可能防止类型升级，请为函数提供已删除的重载(以下带有自由函数的示例也可以应用于任何类成员函数):\n\n```cpp\n        template <typename T> \n        void run(T val) = delete; \n\n        void run(long val) {} // can only be called with long integers\n```\n\n# 它是如何工作的...\n\n默认情况下，一个类有几个可以由编译器实现的特殊成员。这些是默认构造函数、复制构造函数、移动构造函数、复制赋值、移动赋值和析构函数。如果您不实现它们，那么编译器会这样做，以便可以创建、移动、复制和析构类的实例。但是，如果您显式地提供了这些特殊方法中的一个或多个，那么编译器将不会根据以下规则生成其他方法:\n\n*   如果存在用户定义的构造函数，默认情况下不会生成默认构造函数。\n*   如果存在用户定义的虚拟析构函数，默认情况下不会生成默认构造函数。\n*   如果存在用户定义的移动构造函数或移动赋值运算符，则默认情况下不会生成复制构造函数和复制赋值运算符。\n*   如果存在用户定义的复制构造函数、移动构造函数、复制赋值运算符、移动赋值运算符或析构函数，则默认情况下不会生成移动构造函数和移动赋值运算符。\n*   如果存在用户定义的复制构造函数或析构函数，则默认情况下会生成复制赋值运算符。\n*   如果存在用户定义的复制赋值运算符或析构函数，则默认情况下会生成复制构造函数。\n\nNote that the last two rules in the preceding list are deprecated rules and may no longer be supported by your compiler.\n\n有时，开发人员需要提供这些特殊成员的空实现或隐藏它们，以防止以特定方式构造类的实例。一个典型的例子是一个不应该被复制的类。这方面的经典模式是提供默认构造函数，并隐藏复制构造函数和复制赋值运算符。虽然这是可行的，但是显式定义的默认构造函数确保该类不再被认为是微不足道的，因此不再是 POD 类型。现代的替代方法是使用删除的函数，如前一节所示。\n\n当编译器在函数定义中遇到`=default`时，会提供默认实现。前面提到的特殊成员函数的规则仍然适用。函数可以在类体之外声明`=default`，当且仅当它们是内联的:\n\n```cpp\n    class foo \n    { \n    public: \n      foo() = default; \n\n      inline foo& operator=(foo const &); \n    }; \n\n    inline foo& foo::operator=(foo const &) = default;\n```\n\n当编译器遇到函数定义中的`=delete`时，会阻止函数的调用。但是，在重载解析过程中仍然会考虑该函数，并且只有当删除的函数是最佳匹配时，编译器才会生成错误。例如，通过给出先前为`run()`函数定义的重载，只可能调用长整数。带有任何其他类型参数的调用，包括`int`，对其存在到`long`的自动类型提升，将确定被删除的重载被认为是最佳匹配，因此编译器将生成一个错误:\n\n```cpp\n    run(42);  // error, matches a deleted overload \n    run(42L); // OK, long integer arguments are allowed\n```\n\n请注意，先前声明的函数不能删除，因为`=delete`定义必须是翻译单元中的第一个声明:\n\n```cpp\n    void forward_declared_function(); \n    // ... \n    void forward_declared_function() = delete; // error\n```\n\nThe rule of thumb (also known as *The Rule of Five*) for class special member functions is that, if you explicitly define any copy constructor, move constructor, copy assignment operator, move assignment operator, or destructor, then you must either explicitly define or default all of them.\n\n# 使用标准算法的 lambdas\n\nC++ 最重要的现代特性之一是 lambda 表达式，也称为 lambda 函数或简称 lambda。Lambda 表达式使我们能够定义匿名函数对象，这些对象可以捕获作用域中的变量，并被调用或作为参数传递给函数。Lambdas 有许多用途，在本食谱中，我们将看到如何使用标准算法。\n\n# 准备好\n\n在本食谱中，我们讨论了标准算法，该算法接受一个参数，该参数是应用于它所迭代的元素的函数或谓词。你需要知道什么是一元函数和二元函数，什么是谓词和比较函数。您还需要熟悉函数对象，因为 lambda 表达式是函数对象的语法糖。\n\n# 怎么做...\n\n您应该更喜欢使用 lambda 表达式将回调传递给标准算法，而不是函数或函数对象:\n\n*   如果只需要在一个地方使用 lambda，请在调用的地方定义匿名 lambda 表达式:\n\n```cpp\n        auto numbers =  \n          std::vector<int>{ 0, 2, -3, 5, -1, 6, 8, -4, 9 }; \n        auto positives = std::count_if( \n          std::begin(numbers), std::end(numbers),  \n          [](int const n) {return n > 0; });\n```\n\n*   如果需要在多个地方调用 lambda，定义一个命名的 lambda，即分配给变量的 lambda(通常带有类型的`auto`说明符):\n\n```cpp\n        auto ispositive = [](int const n) {return n > 0; }; \n        auto positives = std::count_if( \n          std::begin(numbers), std::end(numbers), ispositive);\n```\n\n*   如果需要参数类型不同的 lambda，请使用泛型 lambda 表达式(从 C++ 14 开始提供):\n\n```cpp\n        auto positives = std::count_if( \n          std::begin(numbers), std::end(numbers),  \n          [](auto const n) {return n > 0; });\n```\n\n# 它是如何工作的...\n\n前面第二个项目符号中显示的非泛型 lambda 表达式采用了一个常量整数，如果大于`0`则返回`true`，否则返回`false`。编译器使用具有 lambda 表达式签名的调用运算符定义了一个未命名的函数对象:\n\n```cpp\n    struct __lambda_name__ \n    { \n      bool operator()(int const n) const { return n > 0; } \n    };\n```\n\n编译器定义未命名函数对象的方式取决于我们定义 lambda 表达式的方式，该表达式可以捕获变量，使用`mutable`说明符或异常规范，或者具有尾随返回类型。前面显示的`__lambda_name__`函数对象实际上是编译器生成内容的简化，因为它还定义了默认的复制和移动构造函数、默认的析构函数和删除赋值运算符。\n\nIt must be well understood that the lambda expression is actually a class. In order to call it, the compiler needs to instantiate an object of the class. The object instantiated from a lambda expression is called a *lambda closure*.\n\n在下一个示例中，我们希望计算大于或等于 5 且小于或等于 10 的范围内的元素数量。在这种情况下，lambda 表达式如下所示:\n\n```cpp\n    auto numbers = std::vector<int>{ 0, 2, -3, 5, -1, 6, 8, -4, 9 }; \n    auto start{ 5 }; \n    auto end{ 10 }; \n    auto inrange = std::count_if( \n             std::begin(numbers), std::end(numbers),  \n             [start, end](int const n) {\n                return start <= n && n <= end;});\n```\n\n这个λ通过复制(即值)捕获两个变量`start`和`end`。编译器创建的未命名函数对象看起来很像我们前面定义的对象。使用前面提到的默认和删除的特殊成员，该类看起来如下所示:\n\n```cpp\n    class __lambda_name_2__ \n    { \n      int start_; \n      int end_; \n    public: \n      explicit __lambda_name_2__(int const start, int const end) : \n        start_(start), end_(end) \n      {} \n\n      __lambda_name_2__(const __lambda_name_2__&) = default; \n      __lambda_name_2__(__lambda_name_2__&&) = default; \n      __lambda_name_2__& operator=(const __lambda_name_2__&)  \n         = delete; \n      ~__lambda_name_2__() = default; \n\n      bool operator() (int const n) const \n      { \n        return start_ <= n && n <= end_; \n      } \n    };\n```\n\nlambda 表达式可以通过复制(或值)或引用来捕获变量，两者的不同组合也是可能的。但是一个变量不可能多次捕获，只能在捕获列表的开头有`&`或`=`。\n\nA lambda can only capture variables from an enclosing function scope. It cannot capture variables with static storage duration (that is, variables declared in a namespace scope or with the `static` or `external` specifier).\n\n下表显示了 lambda 捕获语义的各种组合。\n\n| **λ** | **描述** |\n| `[](){}` | 不捕捉任何东西 |\n| `[&](){}` | 通过引用获取所有内容 |\n| `[=](){}` | 通过拷贝捕获所有内容 |\n| `[&x](){}` | 仅通过引用捕获`x` |\n| `[x](){}` | 仅通过复制捕获`x` |\n| `[&x...](){}` | 通过引用获取包扩展`x` |\n| `[x...](){}` | 复制捕获包扩展`x` |\n| `[&, x](){}` | 通过引用捕获所有内容，除了通过拷贝捕获的`x` |\n| `[=, &x](){}` | 通过复制捕获所有内容，除了通过引用捕获的`x` |\n| `[&, this](){}` | 通过引用捕获除指针`this`以外的所有内容，该指针由副本捕获(`this`始终由副本捕获) |\n| `[x, x](){}` | 错误，`x`被捕获两次 |\n| `[&, &x](){}` | 错误，所有内容都是引用捕获的，不能再次指定引用捕获`x` |\n| `[=, =x](){}` | 错误，所有内容都被复制捕获，不能再次指定通过复制捕获`x` |\n| `[&this](){}` | 错误，指针`this`总是被拷贝捕获 |\n| `[&, =](){}` | 错误，无法通过复制和引用捕获所有内容 |\n\n从 C++ 17 开始，lambda 表达式的一般形式如下:\n\n```cpp\n    [capture-list](params) mutable constexpr exception attr -> ret\n    { body }\n```\n\n该语法中显示的所有部分实际上都是可选的，除了捕获列表(可以是空的)和正文(也可以是空的)。如果不需要参数，参数列表实际上可以省略。不需要指定返回类型，因为编译器可以从返回表达式的类型中推断出它。`mutable`说明符(告诉编译器 lambda 实际上可以修改通过复制捕获的变量)`constexpr`说明符(告诉编译器生成一个`constexpr`调用运算符)，以及异常说明符和属性都是可选的。\n\nThe simplest possible lambda expression is `[]{}`, though it is often written as `[](){}`.\n\n# 还有更多...\n\n在某些情况下，lambda 表达式只在参数类型上有所不同。在这种情况下，lambdas 可以用通用的方式编写，就像模板一样，但是使用类型参数的`auto`说明符(不涉及模板语法)。这将在下一个配方中解决，在*部分中提到。*\n\n# 请参见\n\n*   *使用通用 lambdas*\n*   *编写递归λ*\n\n# 使用通用 lambdas\n\n在前面的配方中，我们看到了如何编写 lambda 表达式并将其与标准算法一起使用。在 C++ 中，lambdas 基本上是未命名函数对象的语法糖，这些函数对象是实现调用运算符的类。然而，就像任何其他功能一样，这可以用模板来实现。C++ 14 利用了这一点，引入了不需要为参数指定实际类型的泛型 lambdas，而是使用`auto`说明符。虽然没有用这个名字来称呼，但是通用 lambda 基本上是 lambda 模板。在我们希望使用相同的 lambda 但参数类型不同的情况下，它们非常有用。\n\n# 入门指南\n\n建议您先阅读前面的食谱*，使用带有标准算法的 lambdas】，然后再继续这一个。*\n\n# 怎么做...\n\n编写通用 lambdas:\n\n*   通过使用`auto`说明符代替 lambda 表达式参数的实际类型。\n*   当需要使用多个仅参数类型不同的 lambdas 时。\n\n以下示例显示了与`std::accumulate()`算法一起使用的通用 lambda，首先是整数向量，然后是字符串向量。\n\n```cpp\n        auto numbers =\n          std::vector<int>{0, 2, -3, 5, -1, 6, 8, -4, 9};  \n        auto texts =  \n          std::vector<std::string>{\"hello\"s, \" \"s, \"world\"s, \"!\"s}; \n\n        auto lsum = [](auto const s, auto const n) {return s + n;}; \n\n        auto sum = std::accumulate( \n          std::begin(numbers), std::end(numbers), 0, lsum); \n          // sum = 22 \n\n        auto text = std::accumulate( \n          std::begin(texts), std::end(texts), \"\"s, lsum); \n          // sum = \"hello world!\"s\n```\n\n# 它是如何工作的...\n\n在上一节的示例中，我们定义了一个名为 lambda 的表达式，即一个 lambda 表达式，它的闭包被赋给了一个变量。该变量然后作为参数传递给`std::accumulate()`函数。这种通用算法采用定义范围的开始和结束迭代器、要累加的初始值，以及一个将范围内每个值累加到总数的函数。该函数将表示当前累计值的第一个参数和表示当前值的第二个参数累加到一起，并返回新的累计值。\n\n请注意，我没有使用术语`add`，因为这可以用于其他事情，而不仅仅是添加。它还可以用于计算乘积、连接或其他将值聚合在一起的操作。\n\n本例中对`std::accumulate()`的两次调用几乎相同，只是参数的类型不同:\n\n*   在第一个调用中，我们将迭代器传递给一个整数范围(从`vector<int>`开始)，0 表示初始和，λ表示两个整数相加并返回它们的和。这将产生该范围内所有整数的总和；对于这个例子，它是 22。\n*   在第二次调用中，我们将迭代器传递给一系列字符串(从`vector<string>`)、一个用于初始值的空字符串和一个通过将两个字符串相加并返回结果来连接它们的 lambda。这会生成一个字符串，该字符串包含该范围内一个接一个放在一起的所有字符串；对于这个例子，结果是*“你好世界！”*。\n\n虽然泛型 lambda 可以在它们被调用的地方匿名定义，但这并没有真正的意义，因为泛型 lambda(基本上，如前所述，是一个 lambda 表达式模板)的目的是被重用，如*中的示例所示...*段。\n\n当定义这个用于多次调用`std::accumulate()`的 lambda 表达式时，我们使用了`auto`说明符并让编译器推导出类型，而不是为 lambda 参数指定具体的类型(如`int`或`std::string`)。当遇到参数类型具有`auto`说明符的 lambda 表达式时，编译器会生成一个具有调用运算符模板的未命名函数对象。对于本例中的通用 lambda 表达式，函数对象如下所示:\n\n```cpp\n    struct __lambda_name__ \n    { \n      template<typename T1, typename T2> \n      auto operator()(T1 const s, T2 const n) const { return s + n; } \n\n       __lambda_name__(const __lambda_name__&) = default; \n       __lambda_name__(__lambda_name__&&) = default; \n       __lambda_name__& operator=(const __lambda_name__&) = delete; \n       ~__lambda_name__() = default; \n    };\n```\n\n调用运算符是一个模板，它为λ中的每个参数都指定了一个类型参数，该参数是用`auto`指定的。调用运算符的返回类型也是`auto`，这意味着编译器将从返回值的类型中推导出来。该运算符模板将使用编译器在使用泛型 lambda 的上下文中识别的实际类型进行实例化。\n\n# 请参见\n\n*   *使用标准算法的 lambdas】*\n*   *尽可能使用自动[第八章](08.html)*的*食谱学习现代核心语言功能*\n\n# 编写递归 lambda\n\nLambdas 基本上是未命名的函数对象，这意味着应该可以递归调用它们。的确，它们可以递归调用；但是，这样做的机制并不明显，因为它需要将 lambda 分配给函数包装器，并通过引用捕获包装器。虽然可以说递归 lambda 没有真正的意义，函数可能是更好的设计选择，但在这个食谱中，我们将看看如何编写递归 lambda。\n\n# 准备好\n\n为了演示如何编写递归 lambda，我们将考虑著名的斐波那契函数示例。这通常在 C++ 中递归实现，如下所示:\n\n```cpp\n    constexpr int fib(int const n) \n    { \n      return n <= 2 ? 1 : fib(n - 1) + fib(n - 2); \n    }\n```\n\n# 怎么做...\n\n为了编写递归 lambda 函数，必须执行以下操作:\n\n*   在函数范围内定义 lambda。\n*   将λ分配给`std::function`包装器。\n*   在 lambda 中通过引用捕获`std::function`对象，以便递归调用它。\n\n以下是递归 lambdas 的示例:\n\n*   函数范围内的递归斐波那契λ表达式，从定义它的范围调用:\n\n```cpp\n        void sample() \n        { \n          std::function<int(int const)> lfib =  \n            [&lfib](int const n) \n            { \n              return n <= 2 ? 1 : lfib(n - 1) + lfib(n - 2); \n            }; \n\n          auto f10 = lfib(10); \n        }\n```\n\n*   函数返回的递归斐波那契λ表达式，可以从任何范围调用:\n\n```cpp\n        std::function<int(int const)> fib_create() \n        { \n          std::function<int(int const)> f = [](int const n)  \n          { \n            std::function<int(int const)> lfib = [&lfib](int n) \n            { \n              return n <= 2 ? 1 : lfib(n - 1) + lfib(n - 2); \n            }; \n            return lfib(n); \n          }; \n          return f; \n        } \n\n        void sample() \n        { \n          auto lfib = fib_create(); \n          auto f10 = lfib(10); \n        }\n```\n\n# 它是如何工作的...\n\n编写递归 lambda 时需要考虑的第一件事是，lambda 表达式是一个函数对象，为了从 lambda 的主体递归调用它，lambda 必须捕获它的闭包(即 lambda 的实例化)。换句话说，lambda 必须捕获自身，这有几个含义:\n\n*   首先，lambda 必须有一个名称；无法捕获未命名的 lambda 以便再次调用。\n*   其次，lambda 只能在函数范围内定义。这样做的原因是 lambda 只能从函数范围捕获变量；它无法捕获任何具有静态存储持续时间的变量。在命名空间范围内或使用静态或外部说明符定义的对象具有静态存储持续时间。如果 lambda 是在命名空间范围内定义的，它的闭包将具有静态存储持续时间，因此 lambda 不会捕获它。\n*   第三个含义是 lambda 闭包的类型不能保持未指定，也就是说，不能用 auto 说明符声明。用自动类型说明符声明的变量不可能出现在自己的初始值设定项中，因为在处理初始值设定项时，变量的类型是未知的。因此，您必须指定 lambda 闭包的类型。我们可以这样做的方法是使用通用函数包装器`std::function`。\n*   最后但同样重要的是，lambda 闭包必须通过引用来捕获。如果我们通过复制(或值)来捕获，那么就会生成一个函数包装的副本，但是当捕获完成时，包装没有初始化。我们最终得到了一个我们无法调用的对象。即使编译器不会抱怨按值捕获，当调用闭包时，也会抛出`std::bad_function_call`。\n\n第一个例子来自*怎么做...*部分，递归λ是在另一个名为`sample()`的函数中定义的。lambda 表达式的签名和主体与介绍部分定义的常规递归函数`fib()`相同。lambda 闭包被分配给一个名为`lfib`的函数包装器，然后被 lambda 引用捕获并从其主体递归调用。因为闭包是由引用捕获的，所以它将在必须从 lambda 的主体中调用时被初始化。\n\n在第二个示例中，我们定义了一个函数，该函数返回 lambda 表达式的闭包，该表达式又定义并调用一个递归 lambda，其参数又被调用。当需要从函数返回递归 lambda 时，必须实现这种模式。这是必要的，因为在调用递归 lambda 时，lambda 闭包必须仍然可用。如果在此之前被销毁，我们就剩下一个悬空的引用，调用它会导致程序异常终止。以下示例说明了这种错误情况:\n\n```cpp\n    // this implementation of fib_create is faulty\n    std::function<int(int const)> fib_create() \n    { \n      std::function<int(int const)> lfib = [&lfib](int const n) \n      { \n        return n <= 2 ? 1 : lfib(n - 1) + lfib(n - 2); \n      }; \n\n      return lfib; \n    } \n\n    void sample() \n    { \n      auto lfib = fib_create();\n      auto f10 = lfib(10);       // crash \n    }\n```\n\n解决方法是创建两个嵌套的 lambda 表达式，如*所示...*节。`fib_create()`方法返回一个函数包装器，当调用该函数包装器时，它会创建捕获自身的递归 lambda。这与前面示例中显示的实现略有不同，但本质上是不同的。外部`f`λ不捕捉任何东西，尤其是通过引用；因此，我们没有悬空引用的问题。然而，当被调用时，它会创建嵌套 lambda 的闭包，也就是我们感兴趣调用的实际 lambda，并返回将该递归`lfib` lambda 应用于其参数的结果。\n\n# 用可变数量的参数编写函数模板\n\n编写具有可变数量参数的函数或具有可变数量成员的类有时很有用。典型的例子包括像`printf`这样的函数，它采用一种格式和可变数量的参数，或者像`tuple`这样的类。在 C++ 11 之前，前者只有在使用变量宏(只允许编写类型不安全的函数)的情况下才有可能，而后者根本不可能。C++ 11 引入了变量模板，变量模板是具有可变数量参数的模板，这使得既可以编写具有可变数量参数的类型安全函数模板，也可以编写具有可变数量成员的类模板。在这个食谱中，我们将研究如何编写函数模板。\n\n# 准备好\n\n变元数的函数称为*变元函数*。具有可变数量参数的函数模板称为*可变函数模板*。C++ 变量宏(`va_start`、`va_end`、`va_arg`、`va_copy`、`va_list`)的知识对于学习如何编写变量函数模板来说并不是必须的，但它代表了一个很好的起点。\n\n我们已经在之前的食谱中使用了变量模板，但是这次将提供详细的解释。\n\n# 怎么做...\n\n为了编写变量函数模板，必须执行以下步骤:\n\n1.  如果变量函数模板的语义需要，用固定数量的参数定义一个重载来结束编译时递归(参见下面代码中的`[1]`)。\n2.  定义一个模板参数包，引入一个可以容纳任意数量参数的模板参数，包括零；这些参数可以是类型、非类型或模板(参见`[2]`)。\n3.  定义一个函数参数包来保存任意数量的函数参数，包括零；模板参数包和对应的功能参数包大小相同，可以用`sizeof...`操作符确定(参考`[3]`)。\n4.  展开参数包，以便用提供的实际参数替换它(参见`[4]`)。\n\n下面的例子说明了前面的所有要点，是一个变量函数模板，它使用`operator+`添加了可变数量的参数:\n\n```cpp\n    template <typename T>                 // [1] overload with fixed \n    T add(T value)                        //     number of arguments \n    { \n      return value; \n    } \n\n    template <typename T, typename... Ts> // [2] typename... Ts \n    T add(T head, Ts... rest)             // [3] Ts... rest \n    { \n      return head + add(rest...);         // [4] rest...  \n    }\n```\n\n# 它是如何工作的...\n\n乍一看，前面的实现看起来像递归，因为函数`add()`调用自己，从某种程度上来说的确如此，但它是一个编译时递归，不会导致任何类型的运行时递归和开销。编译器实际上根据变量函数模板的使用生成了几个具有不同参数数量的函数，因此只涉及函数重载，而不涉及任何类型的递归。但是，实现就好像参数会以带有结束条件的递归方式进行处理一样。\n\n在前面的代码中，我们可以识别以下关键部分:\n\n*   `Typename... Ts`是一个模板参数包，表示可变数量的模板类型参数。\n*   `Ts... rest`是一个函数参数包，表示可变数量的函数参数。\n*   `Rest...`是功能参数包的扩展。\n\nThe position of the ellipsis is not syntactically relevant. `typename... Ts`, `typename ... Ts`, and `typename ...Ts` are all equivalent.\n\n在`add(T head, Ts... rest)`参数中，`head`是参数列表的第一个元素，`...rest`是列表中剩余参数的一个包(可以是零或更多)。在函数体中，`rest...`是函数参数包的扩展。这意味着编译器用参数包元素的顺序来替换参数包。在`add()`函数中，我们基本上是将第一个参数添加到其余参数的总和中，这给人一种递归处理的印象。当只剩下一个参数时，递归结束，在这种情况下，调用第一个`add()`重载(带有一个参数)并返回其参数值。\n\n函数模板`add()`的这个实现使我们能够编写代码，如下所示:\n\n```cpp\n    auto s1 = add(1, 2, 3, 4, 5);  \n    // s1 = 15 \n    auto s2 = add(\"hello\"s, \" \"s, \"world\"s, \"!\"s);  \n    // s2 = \"hello world!\"\n```\n\n当编译器遇到`add(1, 2, 3, 4, 5)`时，会生成如下函数(`arg1`、`arg2`等等，都不是编译器生成的实际名称)，说明这实际上只是对重载函数的调用，而不是递归:\n\n```cpp\n    int add(int head, int arg1, int arg2, int arg3, int arg4)  \n    {return head + add(arg1, arg2, arg3, arg4);} \n    int add(int head, int arg1, int arg2, int arg3)  \n    {return head + add(arg1, arg2, arg3);} \n    int add(int head, int arg1, int arg2)  \n    {return head + add(arg1, arg2);} \n    int add(int head, int arg1)  \n    {return head + add(arg1);} \n    int add(int value)  \n    {return value;}\n```\n\nWith GCC and Clang, you can use the `__PRETTY_FUNCTION__` macro to print the name and the signature of the function.\n\n通过在我们编写的两个函数的开头添加一个`std::cout << __PRETTY_FUNCTION__ << std::endl`，我们在运行代码时得到如下结果:\n\n```cpp\n    T add(T, Ts ...) [with T = int; Ts = {int, int, int, int}] \n    T add(T, Ts ...) [with T = int; Ts = {int, int, int}] \n    T add(T, Ts ...) [with T = int; Ts = {int, int}] \n    T add(T, Ts ...) [with T = int; Ts = {int}] \n    T add(T) [with T = int]\n```\n\n因为这是一个函数模板，所以可以和任何支持`operator+`的类型一起使用。另一个例子，`add(\"hello\"s, \" \"s, \"world\"s, \"!\"s)`，产生了*“你好世界！”*弦。然而，`std::basic_string`类型对于`operator+`有不同的重载，包括一个可以将字符串连接到一个字符的重载，因此我们应该也能够编写以下内容:\n\n```cpp\n    auto s3 = add(\"hello\"s, ' ', \"world\"s, '!');  \n    // s3 = \"hello world!\"\n```\n\n但是，这将产生如下编译器错误(注意，为了简单起见，我实际上用字符串*“hello world”*替换了`std::basic_string<char, std::char_traits<char>, std::allocator<char> >`):\n\n```cpp\nIn instantiation of 'T add(T, Ts ...) [with T = char; Ts = {string, char}]': \n16:29:   required from 'T add(T, Ts ...) [with T = string; Ts = {char, string, char}]' \n22:46:   required from here \n16:29: error: cannot convert 'string' to 'char' in return \n In function 'T add(T, Ts ...) [with T = char; Ts = {string, char}]': \n17:1: warning: control reaches end of non-void function [-Wreturn-type]\n```\n\n发生的事情是编译器生成下面显示的代码，其中返回类型与第一个参数的类型相同。然而，第一个参数不是`std::string`就是`char`(同样，为了简单起见，`std::basic_string<char, std::char_traits<char>, std::allocator<char> >`被`string`代替)。如果`char`是第一个参数的类型，返回值`head+add(...)`的类型`std::string`与函数返回类型不匹配，并且没有隐式转换:\n\n```cpp\n    string add(string head, char arg1, string arg2, char arg3)  \n    {return head + add(arg1, arg2, arg3);} \n    char add(char head, string arg1, char arg2)  \n    {return head + add(arg1, arg2);} \n    string add(string head, char arg1)  \n    {return head + add(arg1);} \n    char add(char value)  \n    {return value;}\n```\n\n我们可以通过修改变量函数模板使返回类型为`auto`而不是`T`来解决这个问题。在这种情况下，返回类型总是从返回表达式中推断出来，在我们的示例中，在所有情况下都是`std::string`:\n\n```cpp\n    template <typename T, typename... Ts> \n    auto add(T head, Ts... rest) \n    { \n      return head + add(rest...); \n    }\n```\n\n还应补充的是，参数包可以出现在括号初始化中，其大小可以使用`sizeof...`运算符来确定。此外，变量函数模板不一定意味着编译时递归，正如我们在本食谱中所展示的那样。所有这些都显示在下面的例子中，在这个例子中，我们定义了一个函数来创建一个成员数为偶数的元组。我们首先使用`sizeof...(a)`来确保我们有偶数个参数，否则通过生成编译器错误来断言。`sizeof...`操作符可用于模板参数包和功能参数包。`sizeof...(a)`和`sizeof...(T)`会产生相同的价值。然后，我们创建并返回一个元组。模板参数包`T`展开(带`T...`)为`std::tuple`类模板的类型参数，函数参数包`a`展开(带`a...`)为使用括号初始化的元组成员的值:\n\n```cpp\n    template<typename... T> \n    auto make_even_tuple(T... a) \n    { \n      static_assert(sizeof...(a) % 2 == 0,  \n                    \"expected an even number of arguments\"); \n      std::tuple<T...> t { a... }; \n\n      return t; \n    } \n\n    auto t1 = make_even_tuple(1, 2, 3, 4); // OK \n\n    // error: expected an even number of arguments \n    auto t2 = make_even_tuple(1, 2, 3);\n```\n\n# 请参见\n\n*   *使用折叠表达式简化变量函数模板*\n*   *创建原始用户定义的文字[第 9 章](09.html)、*使用数字和*\n    *字符串**\n\n# 使用折叠表达式简化变量函数模板\n\n在这一章中，我们讨论了几次折叠；这是一种将二进制函数应用于一系列值以产生单个值的操作。我们在讨论变量函数模板时已经看到了这一点，我们将在讨论高阶函数时再次看到这一点。事实证明，在很多情况下，变量函数模板中参数包的扩展基本上是一个折叠操作。为了简化这种变量函数模板的编写，C++ 17 引入了折叠表达式，将参数包的扩展折叠到二进制运算符上。在这个食谱中，我们将看到如何使用 fold 表达式来简化变量函数模板的编写。\n\n# 准备好\n\n本配方中的示例基于我们在上一个配方中编写的可变函数模板`add()`，*编写一个具有可变参数数量的函数模板*。这个实现是一个左折叠操作。为简单起见，我们再次展示该函数:\n\n```cpp\n    template <typename T> \n    T add(T value) \n    { \n      return value; \n    } \n\n    template <typename T, typename... Ts> \n    T add(T head, Ts... rest) \n    { \n      return head + add(rest...); \n    }\n```\n\n# 怎么做...\n\n要在二元运算符上折叠参数包，请使用以下形式之一:\n\n*   用一元形式左折叠`(... op pack)`:\n\n```cpp\n        template <typename... Ts> \n        auto add(Ts... args) \n        { \n          return (... + args); \n        }\n```\n\n*   用二进制形式左折叠`(init op ... op pack)`:\n\n```cpp\n        template <typename... Ts> \n        auto add_to_one(Ts... args) \n        { \n          return (1 + ... + args); \n        }\n```\n\n*   用一元形式右折叠`(pack op ...)`:\n\n```cpp\n        template <typename... Ts> \n        auto add(Ts... args) \n        { \n          return (args + ...); \n        }\n```\n\n*   用二进制形式右折叠`(pack op ... op init)`:\n\n```cpp\n        template <typename... Ts> \n        auto add_to_one(Ts... args) \n        { \n          return (args + ... + 1); \n        }\n```\n\nThe parentheses shown above are part of the fold expression and cannot be omitted.\n\n# 它是如何工作的...\n\n当编译器遇到 fold 表达式时，它会以下列表达式之一展开它:\n\n| **表达式** | **膨胀** |\n| `(... op pack)` | (“打包 1 美元到打包 2 美元”)上我不知道)打包课程$n |\n| `(init op ... op pack)` | ((以$1 开头)以$2 开头)我不知道)打包课程$n |\n| `(pack op ...)` | 包$1 op(-我...。op(打包课程$n-1 打包课程$n)) |\n| `(pack op ... op init)` | 打包 1 美元(我不知道上(打包$n-1)(打包$n 到初始) |\n\n当使用二进制形式时，椭圆左侧和右侧的运算符必须相同，并且初始值不得包含未展开的参数包。\n\nfold 表达式支持以下二进制运算符:\n\n| + | - | * | / | % | ^ | & | &#124; | = | < | > | << |\n| >> | += | -= | *= | /= | %= | = | &= | &#124;= | <<= | >>= | == |\n| ！= | <= | >= | && | &#124;&#124; | , | 。* | ->*. |  |  |  |  |\n\n使用一元形式时，空参数包只允许使用`*`、`+`、`&`、`|`、`&&`、`||`、`,`(逗号)等运算符。在这种情况下，空包装的值如下:\n\n| `+` | `0` |\n| `*` | `1` |\n| `&` | `-1` |\n| `&#124;` | `0` |\n| `&&` | `true` |\n| `&#124;&#124;` | `false` |\n| `,` | `void()` |\n\n现在我们已经有了前面实现的函数模板(让我们考虑左折叠版本)，我们可以编写以下代码:\n\n```cpp\n    auto sum = add(1, 2, 3, 4, 5);         // sum = 15 \n    auto sum1 = add_to_one(1, 2, 3, 4, 5); // sum = 16\n```\n\n考虑到`add(1, 2, 3, 4, 5)`调用，它将产生以下函数:\n\n```cpp\n    int add(int arg1, int arg2, int arg3, int arg4, int arg5) \n    { \n      return ((((arg1 + arg2) + arg3) + arg4) + arg5); \n    }\n```\n\nDue to the aggressive ways modern compilers do optimizations, this function can be inlined and eventually end up with an expression such as `auto sum = 1 + 2 + 3 + 4 + 5`.\n\n# 还有更多...\n\nFold 表达式适用于支持的二进制运算符的所有重载，但不适用于任意二进制函数。通过提供一个包装类型来保存一个值，并为该包装类型提供一个重载运算符，可以实现一种变通方法:\n\n```cpp\n    template <typename T> \n    struct wrapper \n    { \n      T const & value; \n    }; \n\n    template <typename T> \n    constexpr auto operator<(wrapper<T> const & lhs,  \n                             wrapper<T> const & rhs)  \n    { \n      return wrapper<T> { \n        lhs.value < rhs.value ? lhs.value : rhs.value}; \n    } \n\n    template <typename... Ts> \n    constexpr auto min(Ts&&... args)  \n    { \n      return (wrapper<Ts>{args} < ...).value; \n    }\n```\n\n在前面的代码中，`wrapper`是一个简单的类模板，它保存对类型为`T`的值的常量引用。为此类模板提供了一个重载的`operator<`；这个重载不会返回一个布尔值来指示第一个参数小于第二个参数，而是实际上是一个`wrapper`类类型的实例来保存两个参数的最小值。变量函数模板`min()`使用这个重载的`operator<`来折叠扩展到包装类模板实例的参数包:\n\n```cpp\n    auto m = min(1, 2, 3, 4, 5); // m = 1\n```\n\n# 请参见\n\n*   *实现高阶函数映射和折叠*\n\n# 实现高阶函数映射和折叠\n\n在本书前面的食谱中，我们在几个例子中使用了通用算法`std::transform()`和`std::accumulate()`，例如实现字符串实用程序来创建字符串的大写或小写副本，或者对一个范围的值求和。这些基本上都是高阶函数`map`和`fold`的实现。高阶函数是将一个或多个其他函数作为参数，并将它们应用于一个范围(列表、向量、地图、树等)的函数，生成新的范围或值。在这个食谱中，我们将看到如何实现`map`和`fold`函数来使用 C++ 标准容器。\n\n# 准备好\n\n*Map* 是一个高阶函数，它将一个函数应用于一个范围的元素，并以相同的顺序返回一个新的范围。\n\n*Fold* 是一个高阶函数，它将组合函数应用于范围的元素，产生单个结果。由于处理的顺序可能很重要，所以这个函数通常有两个版本- `foldleft`，从左到右处理元素，以及 **`foldright`** ，从右到左组合元素。\n\nMost descriptions of the function map indicate that it is applied to a `list`, but this is a general term that can indicate different sequential types, such as list, vector, and array, and also dictionaries (that is, maps), queues, and so on. For this reason, I prefer to use the term range when describing these higher-order functions.\n\n# 怎么做...\n\n要实现`map`功能，您应该:\n\n*   在支持元素迭代和赋值的容器上使用`std::transform`，如`std::vector`或`std::list`:\n\n```cpp\n        template <typename F, typename R> \n        R mapf(F&& f, R r) \n        { \n          std::transform( \n            std::begin(r), std::end(r), std::begin(r),  \n            std::forward<F>(f)); \n          return r; \n        }\n```\n\n*   对于不支持元素赋值的容器，使用其他方式，如显式迭代和插入，如`std::map`:\n\n```cpp\n        template<typename F, typename T, typename U> \n        std::map<T, U> mapf(F&& f, std::map<T, U> const & m) \n        { \n          std::map<T, U> r; \n          for (auto const kvp : m) \n            r.insert(f(kvp)); \n          return r; \n        } \n\n        template<typename F, typename T> \n        std::queue<T> mapf(F&& f, std::queue<T> q) \n        { \n          std::queue<T> r; \n          while (!q.empty()) \n          { \n            r.push(f(q.front())); \n            q.pop(); \n          } \n          return r; \n        }\n```\n\n要实现`fold`功能，您应该:\n\n*   在支持迭代的容器上使用`std::accumulate()`:\n\n```cpp\n        template <typename F, typename R, typename T> \n        constexpr T foldl(F&& f, R&& r, T i) \n        { \n          return std::accumulate( \n            std::begin(r), std::end(r),  \n            std::move(i),  \n            std::forward<F>(f)); \n        } \n\n        template <typename F, typename R, typename T> \n        constexpr T foldr(F&& f, R&& r, T i) \n        { \n          return std::accumulate( \n            std::rbegin(r), std::rend(r),  \n            std::move(i),  \n            std::forward<F>(f)); \n        }\n```\n\n*   使用其他方式明确处理不支持迭代的容器，如`std::queue`:\n\n```cpp\n        template <typename F, typename T> \n        constexpr T foldl(F&& f, std::queue<T> q, T i) \n        { \n          while (!q.empty()) \n          { \n            i = f(i, q.front()); \n            q.pop(); \n          } \n          return i; \n        }\n```\n\n# 它是如何工作的...\n\n在前面的例子中，我们以一种功能性的方式实现了地图，没有副作用。这意味着它会保留原来的范围并返回一个新的范围。函数的参数是要应用的函数和范围。为了避免与`std::map`容器混淆，我们调用了这个函数`mapf`。`mapf`有几个重载，如前所示:\n\n*   第一个重载用于支持迭代和元素赋值的容器；这包括`std::vector`、`std::list`和`std::array`，但也包括类似 C 的数组。该函数引用一个函数和一个定义了`std::begin()`和`std::end()`的范围。该范围通过值传递，因此修改本地副本不会影响原始范围。通过使用标准算法`std::transform()`将给定函数应用于每个元素来转换范围；然后返回转换后的范围。\n*   第二个重载专门用于不支持直接分配给其元素(`std::pair<T, U>`)的`std::map`。因此，此重载创建一个新地图，然后使用基于范围的 for 循环遍历其元素，并将输入函数应用于原始地图的每个元素的结果插入到新地图中。\n*   第三个重载专门用于`std::queue`，这是一个不支持迭代的容器。可以说，队列不是一个典型的映射结构，但是为了演示不同的可能实现，我们正在考虑它。为了迭代一个队列的元素，必须改变队列——你需要从前面弹出元素，直到列表为空。这就是第三个重载的作用——它处理输入队列的每个元素(通过值传递)，并将给定函数的结果推送到剩余队列的前面元素。\n\n现在我们已经实现了这些重载，我们可以将它们应用于许多容器，如下面的示例所示(请注意，这里使用的 map 和 fold 函数是在本书附带的代码中名为 funclib 的命名空间中实现的，因此以完全限定名显示):\n\n*   保留向量的绝对值。在本例中，向量包含负值和正值。应用映射后，结果是一个只有正值的新向量。\n\n```cpp\n        auto vnums =  \n          std::vector<int>{0, 2, -3, 5, -1, 6, 8, -4, 9};  \n        auto r = funclib::mapf([](int const i) { \n          return std::abs(i); }, vnums);  \n        // r = {0, 2, 3, 5, 1, 6, 8, 4, 9}\n```\n\n*   将列表的数值平方。在本例中，列表包含整数值。应用映射后，结果是包含初始值平方的列表。\n\n```cpp\n        auto lnums = std::list<int>{1, 2, 3, 4, 5}; \n        auto l = funclib::mapf([](int const i) { \n          return i*i; }, lnums); \n        // l = {1, 4, 9, 16, 25}\n```\n\n*   浮点的舍入数量。对于这个例子，我们需要使用`std::round()`；但是，所有浮点类型都有重载，这使得编译器无法选择正确的类型。因此，我们要么必须编写一个 lambda，该 lambda 接受特定浮点类型的参数并返回应用于该值的`std::round()`值，要么创建一个包装`std::round()`的函数对象模板，并仅针对浮点类型启用其调用运算符。该技术用于以下示例:\n\n```cpp\n        template<class T = double> \n        struct fround \n        {   \n          typename std::enable_if< \n            std::is_floating_point<T>::value, T>::type \n          operator()(const T& value) const \n          { \n            return std::round(value); \n          } \n        }; \n\n        auto amounts =  \n          std::array<double, 5> {10.42, 2.50, 100.0, 23.75, 12.99}; \n        auto a = funclib::mapf(fround<>(), amounts); \n        // a = {10.0, 3.0, 100.0, 24.0, 13.0}\n```\n\n*   大写单词映射的字符串键(其中键是单词，值是文本中出现的次数)。请注意，创建字符串的大写副本本身就是一个映射操作。因此，在本例中，我们使用`mapf`将`toupper()`应用于表示密钥的字符串元素，以便生成大写副本:\n\n```cpp\n        auto words = std::map<std::string, int>{  \n          {\"one\", 1}, {\"two\", 2}, {\"three\", 3}  \n        }; \n        auto m = funclib::mapf( \n          [](std::pair<std::string, int> const kvp) { \n            return std::make_pair( \n              funclib::mapf(toupper, kvp.first),  \n              kvp.second); \n          }, \n          words); \n        // m = {{\"ONE\", 1}, {\"TWO\", 2}, {\"THREE\", 3}}\n```\n\n*   对优先级队列中的值进行规范化-最初，值是从 1 到 100，但是我们希望将它们规范化为两个值，1 =高，2 =正常。所有初始优先级值不超过 30 的优先级都成为高优先级，其他优先级为正常优先级:\n\n```cpp\n        auto priorities = std::queue<int>(); \n        priorities.push(10); \n        priorities.push(20); \n        priorities.push(30); \n        priorities.push(40); \n        priorities.push(50); \n        auto p = funclib::mapf( \n          [](int const i) { return i > 30 ? 2 : 1; },  \n          priorities); \n        // p = {1, 1, 1, 2, 2}\n```\n\n要实现`fold`，我们实际上要考虑两种可能的折叠类型，即从左到右和从右到左。因此，我们提供了两个名为`foldl`(用于左折叠)和`foldr`(用于右折叠)的功能。上一节中显示的实现非常相似——它们都接受一个函数、一个范围和一个初始值，并调用`std::algorithm()`将该范围的值折叠成一个值。然而，`foldl`使用直接迭代器，而`foldr`使用反向迭代器来遍历和处理范围。第二个重载是类型`std::queue`的专门化，它没有迭代器。\n\n基于折叠的这些实现，我们可以做以下示例:\n\n*   将整数向量的值相加。在这种情况下，左右折叠将产生相同的结果。在下面的例子中，我们要么传递一个取和的 lambda 和一个数字并返回一个新的和，要么传递标准库中的函数对象`std::plus<>`，该函数对象将`operator+`应用于两个相同类型的操作数(基本上类似于 lambda 的闭包):\n\n```cpp\n        auto vnums =  \n           std::vector<int>{0, 2, -3, 5, -1, 6, 8, -4, 9};  \n\n        auto s1 = funclib::foldl( \n           [](const int s, const int n) {return s + n; },  \n           vnums, 0);                // s1 = 22 \n\n        auto s2 = funclib::foldl( \n           std::plus<>(), vnums, 0); // s2 = 22 \n\n        auto s3 = funclib::foldr( \n           [](const int s, const int n) {return s + n; },  \n           vnums, 0);                // s3 = 22 \n\n        auto s4 = funclib::foldr( \n           std::plus<>(), vnums, 0); // s4 = 22\n```\n\n*   将向量中的字符串连接成单个字符串:\n\n```cpp\n        auto texts =  \n           std::vector<std::string>{\"hello\"s, \" \"s, \"world\"s, \"!\"s}; \n\n        auto txt1 = funclib::foldl( \n           [](std::string const & s, std::string const & n) { \n           return s + n;},  \n           texts, \"\"s);    // txt1 = \"hello world!\" \n\n        auto txt2 = funclib::foldr( \n           [](std::string const & s, std::string const & n) { \n           return s + n; },  \n           texts, \"\"s);    // txt2 = \"!world hello\"\n```\n\n*   将字符数组连接成字符串:\n\n```cpp\n        char chars[] = {'c','i','v','i','c'}; \n\n        auto str1 = funclib::foldl(std::plus<>(), chars, \"\"s);  \n        // str1 = \"civic\" \n\n        auto str2 = funclib::foldr(std::plus<>(), chars, \"\"s);  \n        // str2 = \"civic\"\n```\n\n*   根据在`map<string, int>`中可用的已经计算好的外观，计算文本中的字数:\n\n```cpp\n        auto words = std::map<std::string, int>{  \n           {\"one\", 1}, {\"two\", 2}, {\"three\", 3} }; \n\n        auto count = funclib::foldl( \n           [](int const s, std::pair<std::string, int> const kvp) { \n              return s + kvp.second; }, \n           words, 0); // count = 6\n```\n\n# 还有更多...\n\n这些函数可以流水线化，也就是说，它们可以用一个函数的结果调用另一个函数。以下示例通过对其元素应用`std::abs()`函数，将整数范围映射为正整数范围。然后将结果映射到另一个正方形范围。然后，通过在范围上应用左折叠将它们相加:\n\n```cpp\n    auto vnums = std::vector<int>{ 0, 2, -3, 5, -1, 6, 8, -4, 9 }; \n\n    auto s = funclib::foldl( \n      std::plus<>(), \n      funclib::mapf( \n        [](int const i) {return i*i; },  \n        funclib::mapf( \n          [](int const i) {return std::abs(i); }, \n          vnums)), \n      0); // s = 236\n```\n\n作为一个练习，我们可以将 fold 函数实现为一个变量函数模板，就像前面的方法一样。执行实际折叠的函数作为参数提供:\n\n```cpp\n    template <typename F, typename T1, typename T2> \n    auto foldl(F&&f, T1 arg1, T2 arg2) \n    { \n      return f(arg1, arg2); \n    } \n\n    template <typename F, typename T, typename... Ts> \n    auto foldl(F&& f, T head, Ts... rest) \n    { \n      return f(head, foldl(std::forward<F>(f), rest...)); \n    }\n```\n\n当我们将其与我们在配方*中编写的`add()`函数模板进行比较时，我们可以注意到几个不同之处:*\n\n*   第一个参数是一个函数，递归调用`foldl`时完美转发。\n*   最后一种情况是需要两个参数的函数，因为我们用于折叠的函数是二进制函数(取两个参数)。\n*   我们编写的两个函数的返回类型被声明为`auto`，因为它必须与所提供的二进制函数`f`的返回类型相匹配，这在我们调用`foldl`之前是不知道的:\n\n```cpp\n    auto s1 = foldl(std::plus<>(), 1, 2, 3, 4, 5);  \n    // s1 = 15 \n    auto s2 = foldl(std::plus<>(), \"hello\"s, ' ', \"world\"s, '!');  \n    // s2 = \"hello world!\" \n    auto s3 = foldl(std::plus<>(), 1); // error, too few arguments\n```\n\n# 请参见\n\n*   *创建一个字符串助手库[第 9 章](09.html)、*处理数字和字符串**\n*   *用可变数量的参数编写函数模板*\n*   *将函数组合成更高阶的函数*\n\n# 将函数组合成更高阶的函数\n\n在前面的食谱中，我们实现了两个更高阶的函数，map 和 fold，并看到了使用它们的各种例子。在配方的最后，我们看到了如何对原始数据进行几次转换后，将它们流水线化以产生最终值。流水线是一种组合形式，这意味着从两个或多个给定函数创建一个新函数。在上面提到的例子中，我们实际上并没有组合函数；我们只调用了一个函数，结果由另一个函数产生，但是在这个食谱中，我们将看到如何将函数组合成一个新的函数。为了简单起见，我们将只考虑一元函数(只接受一个参数的函数)。\n\n# 准备好\n\n在继续之前，建议您阅读前面的配方，*实现更高阶的函数映射和 fol* d，理解这个配方并不是强制性的，但是我们会参考这里实现的映射和折叠函数。\n\n# 怎么做...\n\n要将一元函数组合成高阶函数，您应该:\n\n*   对于组合两个函数，提供一个函数，该函数以两个函数`f`和`g`作为参数，并返回一个新函数(λ)，该函数返回`f(g(x))`，其中`x`是组合函数的参数:\n\n```cpp\n        template <typename F, typename G> \n        auto compose(F&& f, G&& g) \n        {  \n          return [=](auto x) { return f(g(x)); }; \n        } \n\n        auto v = compose( \n          [](int const n) {return std::to_string(n); }, \n          [](int const n) {return n * n; })(-3); // v = \"9\"\n```\n\n*   为了组成可变数量的函数，提供前面描述的函数的可变模板重载:\n\n```cpp\n        template <typename F, typename... R> \n        auto compose(F&& f, R&&... r) \n        { \n          return [=](auto x) { return f(compose(r...)(x)); }; \n        } \n\n        auto n = compose( \n          [](int const n) {return std::to_string(n); }, \n          [](int const n) {return n * n; }, \n          [](int const n) {return n + n; }, \n          [](int const n) {return std::abs(n); })(-3); // n = \"36\"\n```\n\n# 它是如何工作的...\n\n将两个一元函数组合成一个新函数相对来说比较简单。创建一个我们在前面的例子中称为`compose()`的模板函数，用两个参数- `f`和`g` -表示函数，并返回一个接受一个参数`x`并返回`f(g(x))`的函数。重要的是`g`函数返回的值的类型与`f`函数的参数的类型相同。compose 函数的返回值是一个闭包，也就是 lambda 的一个实例。\n\n实际上，能够将两个以上的功能结合在一起是非常有用的。这可以通过编写`compose()`函数的变量模板版本来实现。可变模板在*用可变数量的参数编写函数模板*中有更详细的解释。变量模板通过扩展参数包来暗示编译时递归。这个实现与第一版`compose()`非常相似，除了以下几点:\n\n*   它采用可变数量的函数作为参数。\n*   返回的闭包用扩展的参数包递归调用`compose()`；当只剩下两个函数时，递归结束，在这种情况下，调用之前实现的重载。\n\nEven if the code looks like recursion is happening, this is not true recursion. It could be called compile-time recursion, but with every expansion, we get a call to another method with the same name but a different number of arguments, which does not represent recursion.\n\n现在我们已经实现了这些变量模板重载，我们可以重写前面配方的最后一个例子，*实现更高阶的函数映射和折叠*。有了整数的初始向量，我们通过在每个元素上应用`std::abs()`将其映射到一个只有正值的新向量。然后，通过将每个元素的值加倍，将结果映射到一个新的向量。最后，通过将结果向量中的值与初始值 0:\n\n```cpp\n    auto s = compose( \n      [](std::vector<int> const & v) { \n        return foldl(std::plus<>(), v, 0); }, \n      [](std::vector<int> const & v) { \n        return mapf([](int const i) {return i + i; }, v); }, \n      [](std::vector<int> const & v) { \n        return mapf([](int const i) {return std::abs(i); }, v); })(vnums);\n```\n\n# 还有更多...\n\n构图通常用点(`.`)或星号(`*`)表示，如`f . g`或`f * g`。我们实际上可以在 C++ 中通过重载`operator*`来做类似的事情(试图重载操作符点没有什么意义)。类似于`compose()`函数，`operator*`应该可以处理任意数量的参数；因此，我们将有两个重载，就像`compose()`的情况一样:\n\n*   第一个重载接受两个参数并调用`compose()`返回一个新函数。\n*   第二个重载是变量模板函数，它通过扩展参数包再次调用`operator*`:\n\n```cpp\n    template <typename F, typename G> \n    auto operator*(F&& f, G&& g) \n    { \n      return compose(std::forward<F>(f), std::forward<G>(g)); \n    } \n\n    template <typename F, typename... R> \n    auto operator*(F&& f, R&&... r) \n```\n\n```cpp\n    { \n      return operator*(std::forward<F>(f), r...); \n    }\n```\n\n我们现在可以通过应用`operator*`来简化函数的实际组合，而不是更冗长的调用来组合:\n\n```cpp\n    auto n = \n      ([](int const n) {return std::to_string(n); } * \n       [](int const n) {return n * n; } * \n       [](int const n) {return n + n; } * \n       [](int const n) {return std::abs(n); })(-3); // n = \"36\" \n\n    auto c =  \n      [](std::vector<int> const & v) { \n        return foldl(std::plus<>(), v, 0); } * \n      [](std::vector<int> const & v) { \n        return mapf([](int const i) {return i + i; }, v); } * \n      [](std::vector<int> const & v) { \n        return mapf([](int const i) {return std::abs(i); }, v); }; \n\n    auto s = c(vnums); // s = 76\n```\n\n# 请参见\n\n*   *用可变数量的参数编写函数模板*\n\n# 统一调用任何可调用的东西\n\n开发人员，尤其是那些实现库的开发人员，有时需要以统一的方式调用可调用对象。这可以是函数、函数指针、成员函数指针或函数对象。这种情况的例子包括`std::bind`、`std::function`、`std::mem_fn`和`std::thread::thread`。C++ 17 定义了一个名为`std::invoke()`的标准函数，它可以用提供的参数调用任何可调用的对象。这并不是要取代对函数或函数对象的直接调用，而是在模板元编程中用于实现各种库函数。\n\n# 准备好\n\n对于这个配方，您应该熟悉如何定义和使用函数指针。\n\n为了举例说明`std::invoke()`如何在不同的上下文中使用，我们将使用以下函数和类:\n\n```cpp\n    int add(int const a, int const b) \n    { \n      return a + b; \n    } \n\n    struct foo \n    { \n      int x = 0; \n\n      void increment_by(int const n) { x += n; } \n    };\n```\n\n# 怎么做...\n\n`std::invoke()`函数是一个变量函数模板，它将可调用对象作为第一个参数，并将变量列表传递给调用方。`std::invoke()`可用于调用以下内容:\n\n*   免费功能:\n\n```cpp\n        auto a1 = std::invoke(add, 1, 2);   // a1 = 3\n```\n\n*   通过指向函数的指针释放函数:\n\n```cpp\n        auto a2 = std::invoke(&add, 1, 2);  // a2 = 3 \n        int(*fadd)(int const, int const) = &add; \n        auto a3 = std::invoke(fadd, 1, 2);  // a3 = 3\n```\n\n*   成员函数通过指向成员函数的指针:\n\n```cpp\n        foo f; \n        std::invoke(&foo::increment_by, f, 10);\n```\n\n*   数据成员:\n\n```cpp\n        foo f; \n        auto x1 = std::invoke(&foo::x, f);  // x1 = 0\n```\n\n*   功能对象:\n\n```cpp\n        foo f; \n        auto x3 = std::invoke(std::plus<>(),  \n          std::invoke(&foo::x, f), 3); // x3 = 3\n```\n\n*   Lambda 表达式:\n\n```cpp\n        auto l = [](auto a, auto b) {return a + b; }; \n        auto a = std::invoke(l, 1, 2); // a = 3\n```\n\n实际上，应该在模板元编程中使用`std:invoke()`来调用具有任意数量参数的函数。为了举例说明这种情况，我们给出了我们的`std::apply()`函数的一个可能的实现，也是 C++ 17 标准库的一部分，它通过将元组的成员解包成函数的参数来调用函数:\n\n```cpp\n    namespace details \n    { \n      template <class F, class T, std::size_t... I> \n      auto apply(F&& f, T&& t, std::index_sequence<I...>) \n      { \n        return std::invoke( \n          std::forward<F>(f), \n          std::get<I>(std::forward<T>(t))...); \n      } \n    } \n\n    template <class F, class T> \n    auto apply(F&& f, T&& t) \n    { \n      return details::apply( \n        std::forward<F>(f), \n        std::forward<T>(t), \n        std::make_index_sequence< \n          std::tuple_size<std::decay_t<T>>::value> {}); \n    }\n```\n\n# 它是如何工作的...\n\n在我们了解`std::invoke()`如何工作之前，让我们先简单了解一下不同的可调用对象是如何被调用的。显然，给定一个函数，无处不在的调用方法是直接向它传递必要的参数。但是，我们也可以使用函数指针来调用函数。函数指针的问题在于定义指针的类型可能很麻烦。使用`auto`可以简化事情(如下代码所示)，但在实际中，通常需要先定义指向函数的指针的类型，然后定义一个对象，用正确的函数地址初始化。以下是几个例子:\n\n```cpp\n    // direct call \n    auto a1 = add(1, 2);    // a1 = 3 \n\n    // call through function pointer \n    int(*fadd)(int const, int const) = &add; \n    auto a2 = fadd(1, 2);   // a2 = 3 \n\n    auto fadd2 = &add; \n    auto a3 = fadd2(1, 2);  // a3 = 3\n```\n\n当需要通过作为类实例的对象调用类函数时，通过函数指针调用变得更加麻烦。定义指向成员函数的指针并调用它的语法并不简单:\n\n```cpp\n    foo f; \n    f.increment_by(3); \n    auto x1 = f.x;    // x1 = 3 \n\n    void(foo::*finc)(int const) = &foo::increment_by; \n    (f.*finc)(3); \n    auto x2 = f.x;    // x2 = 6 \n\n    auto finc2 = &foo::increment_by; \n    (f.*finc2)(3); \n    auto x3 = f.x;    // x3 = 9\n```\n\n不管这种调用看起来有多麻烦，实际的问题是编写能够以统一的方式调用这些类型的可调用对象的库组件(函数或类)。这就是标准函数在实践中的好处，例如`std::invoke()`。\n\n`std::invoke()`的实现细节比较复杂，但其工作方式可以用简单的术语来解释。假设呼叫的形式为`invoke(f, arg1, arg2, ..., argN)`，则考虑以下情况:\n\n*   如果`f`是指向`T`类的成员函数的指针，那么调用相当于:\n    *   `(arg1.*f)(arg2, ..., argN)`，如果`arg1`是`T`的实例\n    *   `(arg1.get().*f)(arg2, ..., argN)`，如果`arg1`是`reference_wrapper`的专精\n    *   `((*arg1).*f)(arg2, ..., argN)`，如果不是\n*   如果`f`是指向`T`类的数据成员的指针，并且有一个参数，换句话说，调用的形式是`invoke(f, arg1)`，那么调用相当于:\n    *   `arg1.*f`如果`arg1`是实例类`T`\n    *   `arg1.get().*f`如果`arg1`是`reference_wrapper`的专精\n    *   `(*arg1).*f`，如果不是\n*   如果`f`是一个函数对象，那么调用就相当于`f(arg1, arg2, ..., argN)`\n\n# 请参见\n\n*   *用可变数量的参数编写函数模板*"
  },
  {
    "path": "docs/mod-cpp/11.md",
    "content": "# 十一、标准库容器、算法和迭代器\n\n我们将在本章介绍以下食谱:\n\n*   使用矢量作为默认容器\n*   将位集用于固定大小的位序列\n*   将向量<bool>用于可变大小的位序列</bool>\n*   查找范围内的元素\n*   对范围进行排序\n*   初始化范围\n*   对范围使用集合运算\n*   使用迭代器在容器中插入新元素\n*   编写自己的随机访问迭代器\n*   具有非成员函数的容器访问\n\n# 使用矢量作为默认容器\n\n标准库提供了存储对象集合的各种类型的容器；该库包括序列容器(如`vector`、`array`或`list`)、有序和无序关联容器(如`set`和`map`)以及不存储数据但提供面向序列容器的适配接口(如`stack`和`queue`)的容器适配器。所有这些都实现为类模板，这意味着它们可以用于任何类型(只要它满足容器要求)。虽然您应该始终使用最适合特定问题的容器(它不仅在插入、删除、元素访问和内存使用的速度方面提供了良好的性能，而且使代码易于阅读和维护)，但默认选择应该是`vector`。在这个食谱中，我们将看到为什么`vector`应该是容器的首选，以及`vector`最常见的操作是什么。\n\n# 准备好\n\n读者应该熟悉 C 类数组，包括静态和动态分配的。\n\n类模板`vector`在`<vector>`头中的`std`命名空间中可用。\n\n# 怎么做...\n\n要初始化`std::vector`类模板，您可以使用以下任何方法，但不限于这些方法:\n\n*   从初始化列表初始化:\n\n```cpp\n        std::vector<int> v1 { 1, 2, 3, 4, 5 };\n```\n\n*   从类 C 数组初始化:\n\n```cpp\n        int arr[] = { 1, 2, 3, 4, 5 }; \n        std::vector<int> v2(arr, arr + 5); // { 1, 2, 3, 4, 5 }\n```\n\n*   从另一个容器初始化:\n\n```cpp\n        std::list<int> l{ 1, 2, 3, 4, 5 }; \n        std::vector<int> v3(l.begin(), l.end()); //{ 1, 2, 3, 4, 5 }\n```\n\n*   从计数和值初始化:\n\n```cpp\n        std::vector<int> v4(5, 1); // {1, 1, 1, 1, 1}\n```\n\n要修改`std::vector`的内容，可以使用以下任何一种方法，但不仅限于这些:\n\n*   用`push_back()`在向量末尾添加一个元素:\n\n```cpp\n        std::vector<int> v1{ 1, 2, 3, 4, 5 };\n        v1.push_back(6); // v1 = { 1, 2, 3, 4, 5, 6 }\n```\n\n*   用`pop_back()`从向量的末端移除一个元素:\n\n```cpp\n        v1.pop_back();\n```\n\n*   用`insert()`插入向量中的任意位置:\n\n```cpp\n        int arr[] = { 1, 2, 3, 4, 5 };\n        std::vector<int> v2;\n        v2.insert(v2.begin(), arr, arr + 5); // v2 = { 1, 2, 3, 4, 5 }\n```\n\n*   通过用`emplace_back()`在向量的末尾创建元素来添加元素:\n\n```cpp\n        struct foo\n        {\n          int a;\n          double b;\n          std::string c;\n\n          foo(int a, double b, std::string const & c) :\n            a(a), b(b), c(c) {}\n        };\n\n        std::vector<foo> v3;\n        v3.emplace_back(1, 1.0, \"one\"s); \n        // v3 = { foo{1, 1.0, \"one\"} }\n```\n\n*   使用`emplace()`在向量中的任意位置创建元素来插入元素:\n\n```cpp\n        v3.emplace(v3.begin(), 2, 2.0, \"two\"s);\n        // v3 = { foo{2, 2.0, \"two\"}, foo{1, 1.0, \"one\"} }\n```\n\n要修改矢量的全部内容，请使用以下任一方法，但不限于这些方法:\n\n*   用`operator=`从另一个向量赋值；这将替换容器中的内容:\n\n```cpp\n        std::vector<int> v1{ 1, 2, 3, 4, 5 };\n        std::vector<int> v2{ 10, 20, 30 };\n        v2 = v1; // v1 = { 1, 2, 3, 4, 5 }\n```\n\n*   使用`assign()`方法从开始和结束迭代器定义的另一个序列赋值；这将替换容器中的内容:\n\n```cpp\n        int arr[] = { 1, 2, 3, 4, 5 };\n        std::vector<int> v3;\n        v3.assign(arr, arr + 5); // v3 = { 1, 2, 3, 4, 5 }\n```\n\n*   用`swap()`方法交换两个向量的内容:\n\n```cpp\n        std::vector<int> v4{ 1, 2, 3, 4, 5 };\n        std::vector<int> v5{ 10, 20, 30 };\n        v4.swap(v5); // v4 = { 10, 20, 30 }, v5 = { 1, 2, 3, 4, 5 }\n```\n\n*   用`clear()`方法去除所有元素:\n\n```cpp\n        std::vector<int> v6{ 1, 2, 3, 4, 5 };\n        v6.clear(); // v6 = { }\n```\n\n*   使用`erase()`方法移除一个或多个元素(需要一个迭代器或一对迭代器来定义要移除的向量的元素范围):\n\n```cpp\n        std::vector<int> v7{ 1, 2, 3, 4, 5 };\n        v7.erase(v7.begin() + 2, v7.begin() + 4); // v7 = { 1, 2, 5 }\n```\n\n要获取向量中第一个元素的地址，通常是将向量的内容传递给类似 C 的 API，请使用以下任一方法:\n\n*   使用`data()`方法，该方法返回指向第一个元素的指针，提供对存储向量元素的底层连续内存序列的直接访问；这仅在 C++ 11:\n\n```cpp\n        void process(int const * const arr, int const size) \n        { /* do something */ }\n\n        std::vector<int> v{ 1, 2, 3, 4, 5 };\n        process(v.data(), static_cast<int>(v.size()));\n```\n\n*   获取第一个元素的地址:\n\n```cpp\n        process(&v[0], static_cast<int>(v.size()));\n```\n\n*   获取`front()`方法引用的元素地址:\n\n```cpp\n        process(&v.front(), static_cast<int>(v.size()));\n```\n\n*   获取`begin()`返回的迭代器指向的元素的地址:\n\n```cpp\n        process(&*v.begin(), static_cast<int>(v.size()));\n```\n\n# 它是如何工作的...\n\n`std::vector`类被设计成与类 C 数组最相似且可相互操作的 C++ 容器。向量是可变大小的元素序列，保证连续存储在内存中，这使得向量的内容可以很容易地传递给 C 类函数，该函数接受指向数组元素的指针，通常还接受大小。使用向量代替类似 C 的数组有很多好处，这些好处包括:\n\n*   不需要开发人员进行直接的内存管理，因为容器在内部进行内存分配、重新分配和释放。\n\nNote that a vector is intended for storing object instances. If you need to store pointers, do not store raw pointers but smart pointers. Otherwise, you need to handle the lifetime management of the pointed objects.\n\n*   两个向量的简单赋值或连接。\n*   两个向量的直接比较。\n\n`vector`类是一个非常高效的容器，所有的实现都提供了许多优化，而大多数开发人员无法用 C 类数组做到这一点。对其元素的随机访问以及在向量末尾的插入和移除是一个常数 *O(1)* 运算(前提是不需要重新分配)，而在其他地方的插入和移除是一个线性的 *O(n)* 运算。\n\n与其他标准容器相比，该载体具有多种优势:\n\n*   兼容类 C 数组和类 APIs 其他容器的内容(除了`std::array`)需要复制到一个向量，然后传递到一个期待数组的类 API。\n*   它可以最快地访问所有容器的元素。\n*   它没有存储元素的每元素内存开销，因为元素存储在一个连续的空间中，就像一个 C 数组(不像其他容器，如`list`需要指向其他元素的额外指针，或关联容器需要哈希值)。\n\n`std::vector`在语义上与类 C 数组非常相似，但大小可变。向量的大小可以增加和减少。定义向量大小有两个属性:\n\n*   *容量*是向量在不执行额外内存分配的情况下可以容纳的元素数量；这由`capacity()`方法表示。\n*   *大小*是向量中元素的实际数量；这由`size()`方法表示。\n\n大小总是小于或等于容量。当大小等于容量并且需要添加新元素时，需要修改容量，以便向量有空间容纳更多元素。在这种情况下，向量分配一个新的内存块，并将以前的内容移动到新的位置，然后释放以前分配的内存。虽然这听起来很耗时(而且确实如此)，但实现会成倍增加容量，每次需要更改时都会翻倍。因此，平均而言，向量的每个元素只需要移动一次(这是因为在容量增加期间，向量的所有元素都会移动，但是如果在向量的末尾执行插入，则可以添加相同数量的元素，而不会导致更多的移动)。\n\n如果事先知道向量中要插入多少元素，可以先调用`reserve()`方法将容量增加到至少指定的数量(如果指定的大小小于当前容量，该方法不做任何事情)，然后再插入元素。\n\n另一方面，如果需要释放额外的保留内存，可以使用`shrink_to_fit()`方法请求，但是否释放任何内存是一个实现决定。自 C++ 11 以来，这种非绑定方法的一种替代方法是用一个临时的空向量进行交换:\n\n```cpp\n    std::vector<int> v{ 1, 2, 3, 4, 5 };\n    std::vector<int>().swap(v); // v.size = 0, v.capacity = 0\n```\n\n调用`clear()`方法只会移除向量中的所有元素，但不会释放任何内存。\n\n应该注意的是，该向量实现了特定于其他类型容器的操作:\n\n*   `stack`:末尾加`push_back()``emplace_back()`，末尾去掉`pop_back()`。请记住`pop_back()`不会返回最后一个被移除的元素。如果有必要，您需要显式地访问它，例如，在移除元素之前使用`back()`方法。\n*   `list`:用`insert()`和`emplace()`在序列中间添加元素，用`erase()`从序列的任何地方移除元素。\n\n# 还有更多...\n\nThe rule of thumb for C++ containers is: use `std::vector` as the default container unless you have good reasons to use another one.\n\n# 请参见\n\n*   *将位集用于固定大小的位序列*\n*   *使用向量<布尔>用于可变大小的位序列*\n\n# 将位集用于固定大小的位序列\n\n开发人员使用位标志进行操作并不少见；这可能是因为它们与通常用 C 编写的操作系统 API 一起工作，这些 API 采用位标志形式的各种类型的参数(如选项或样式)，也可能是因为它们与做类似事情的库一起工作，或者仅仅是因为某些类型的问题自然会用位标志来解决。可以考虑使用位和位操作的替代方法，例如定义每个选项/标志都有一个元素的数组，或者定义一个具有成员和函数的结构来模拟位标志，但是这些方法通常更复杂，如果需要将表示位标志的数值传递给函数，仍然需要将数组或结构转换为位序列。为此，C++ 标准为固定大小的位序列提供了一个名为`std::bitset`的容器。\n\n# 准备好\n\n对于这个方法，您必须熟悉按位运算(and、or、xor、not 和 shift)。\n\n`bitset`类在`<bitset>`头中的`std`命名空间中可用。位集表示固定大小的位序列，其大小在编译时定义。为了方便起见，在这个配方中，所有的例子都是 8 位的位组。\n\n# 怎么做...\n\n要构造一个`std::bitset`对象，使用一个可用的构造函数:\n\n*   所有位都设置为 0 的空位集:\n\n```cpp\n        std::bitset<8> b1; // [0,0,0,0,0,0,0,0]\n```\n\n*   数值中的一个位组:\n\n```cpp\n        std::bitset<8> b2{ 10 }; // [0,0,0,0,1,0,1,0]\n```\n\n*   由一串`'0'`和`'1'`组成的位组:\n\n```cpp\n        std::bitset<8> b3{ \"1010\"s }; // [0,0,0,0,1,0,1,0]\n```\n\n*   包含任意两个代表`'0'`和`'1'`字符的字符串中的一个位组；在这种情况下，我们必须指定哪个字符代表 0，哪个字符代表 1:\n\n```cpp\n        std::bitset<8> b4 \n          { \"ooooxoxo\"s, 0, std::string::npos, 'o', 'x' }; \n          // [0,0,0,0,1,0,1,0]\n```\n\n要针对特定值测试集合中的单个位或整个集合，请使用任何可用的方法:\n\n*   `count()`将位数设置为 1:\n\n```cpp\n        std::bitset<8> bs{ 10 };\n        std::cout << \"has \" << bs.count() << \" 1s\" << std::endl;\n```\n\n*   `any()`检查是否至少有一位设置为 1:\n\n```cpp\n        if (bs.any()) std::cout << \"has some 1s\" << std::endl;\n```\n\n*   `all()`检查是否所有位都设置为 1:\n\n```cpp\n        if (bs.all()) std::cout << \"has only 1s\" << std::endl;\n```\n\n*   `none()`检查是否所有位都设置为 0:\n\n```cpp\n        if (bs.none()) std::cout << \"has no 1s\" << std::endl;\n```\n\n*   `test()`检查单个位的值:\n\n```cpp\n        if (!bs.test(0)) std::cout << \"even\" << std::endl;\n```\n\n*   `operator[]`要访问和测试单个位:\n\n```cpp\n        if(!bs[0]) std::cout << \"even\" << std::endl;\n```\n\n要修改位集的内容，请使用以下任一方法:\n\n*   成员运算符`|=`、`&=`、`^= `和`~`执行二进制或、与、异或和非运算，或非成员运算符`|`、`&`和`^`:\n\n```cpp\n        std::bitset<8> b1{ 42 }; // [0,0,1,0,1,0,1,0]\n        std::bitset<8> b2{ 11 }; // [0,0,0,0,1,0,1,1]\n        auto b3 = b1 | b2;       // [0,0,1,0,1,0,1,1]\n        auto b4 = b1 & b2;       // [0,0,0,0,1,0,1,0]\n        auto b5 = b1 ^ b2;       // [1,1,0,1,1,1,1,0]\n        auto b6 = ~b1;           // [1,1,0,1,0,1,0,1]\n```\n\n*   执行换档操作的成员操作员`<<=`、`<<`、`>>=`、`>>`:\n\n```cpp\n        auto b7 = b1 << 2;       // [1,0,1,0,1,0,0,0]\n        auto b8 = b1 >> 2;       // [0,0,0,0,1,0,1,0]\n```\n\n*   `flip()`将整组或单个位从 0 切换到 1 或从 1 切换到 0:\n\n```cpp\n        b1.flip();               // [1,1,0,1,0,1,0,1]\n        b1.flip(0);              // [1,1,0,1,0,1,0,0]\n```\n\n*   `set()`将整组或单个位更改为`true`或指定值:\n\n```cpp\n        b1.set(0, true);         // [1,1,0,1,0,1,0,1]\n        b1.set(0, false);        // [1,1,0,1,0,1,0,0]\n```\n\n*   `reset()`将整组或单个位更改为假:\n\n```cpp\n        b1.reset(2);             // [1,1,0,1,0,0,0,0]\n```\n\n要将位集转换为数值或字符串值，请使用以下方法:\n\n*   `to_ulong()`和`to_ullong()`转换为`unsigned long`或`unsigned long long`:\n\n```cpp\n        std::bitset<8> bs{ 42 };\n        auto n1 = bs.to_ulong();  // n1 = 42UL\n        auto n2 = bs.to_ullong(); // n2 = 42ULL\n```\n\n*   `to_string()`转换为`std::basic_string`；默认情况下，结果是包含`'0'`和`'1'`的字符串，但是您可以为这两个值指定不同的字符:\n\n```cpp\n        auto s1 = bs.to_string();         // s1 = \"00101010\"\n        auto s2 = bs.to_string('o', 'x'); // s2 = \"ooxoxoxo\"\n```\n\n# 它是如何工作的...\n\n如果您曾经使用过 C 或类似 C 的 API，那么很有可能您已经编写或至少已经看到了操作位来定义样式、选项或其他类型的值的代码。这通常涉及操作，例如:\n\n*   定义位标志；这些可以是枚举、类中的静态常量，也可以是用 C 风格的`#define`引入的宏。通常，有一个标志表示没有值(样式、选项等)。因为这些应该是位标志，所以它们的值是 2 的幂。\n*   从集合中添加和移除标志(即数值)。添加位标志用位或运算符(`value |= FLAG`)完成，移除位标志用位和运算符完成，取反标志(`value &= ~FLAG`)完成。\n*   测试一个标志是否被添加到集合中(`value & FLAG == FLAG`)。\n*   以标志作为参数调用函数。\n\n下面显示了一个简单的标志示例，用于定义控件的边框样式，该控件可以在左侧、右侧、顶部或底部有边框，也可以是它们的任意组合，包括没有边框:\n\n```cpp\n    #define BORDER_NONE   0x00\n    #define BORDER_LEFT   0x01\n    #define BORDER_TOP    0x02\n    #define BORDER_RIGHT  0x04\n    #define BORDER_BOTTOM 0x08\n\n    void apply_style(unsigned int const style)\n    {\n      if (style & BORDER_BOTTOM) { /* do something */ }\n    }\n\n    // initialize with no flags\n    unsigned int style = BORDER_NONE;\n    // set a flag\n    style = BORDER_BOTTOM;\n    // add more flags\n    style |= BORDER_LEFT | BORDER_RIGHT | BORDER_TOP;\n    // remove some flags\n    style &= ~BORDER_LEFT;\n    style &= ~BORDER_RIGHT;\n    // test if a flag is set\n    if ((style & BORDER_BOTTOM) == BORDER_BOTTOM) {}\n    // pass the flags as argument to a function\n    apply_style(style);\n```\n\n标准的`std::bitset`类旨在作为这种类似于 C 的工作风格的 C++ 替代，具有多组位。它使我们能够编写更健壮和更安全的代码，因为它用成员函数抽象了位操作，尽管我们仍然需要识别集合中的每个位代表什么:\n\n*   添加和删除标志是通过`set()`和`reset()`方法完成的，这两种方法将由位置指示的位的值设置为 1 或 0(或`true`和`false`)；或者，我们可以出于同样的目的使用 index 运算符。\n*   使用`test()`方法测试是否设置了一个位。\n*   从整数或字符串的转换是通过构造函数完成的，而到整数或字符串的转换是通过成员函数完成的，因此位集中的值可以用在需要整数的地方(例如函数的参数)。\n\n除了上面提到的这些操作之外，`bitset`类还有额外的方法，用于对位执行按位操作、移位、测试，以及前面部分中显示的其他操作。\n\n从概念上讲，`std::bitset`是一个数值的表示，使您能够访问和修改单个位。但是，在内部，一个位集有一个整数值数组，它在这个数组上执行位操作。位集的大小不限于数字类型的大小；它可以是任何东西，只是它是一个编译时常数。\n\n上一节中带有控件边框样式的示例可以使用`std::bitset`以下列方式编写:\n\n```cpp\n    struct border_flags\n    {\n      static const int left = 0;\n      static const int top = 1;\n      static const int right = 2;\n      static const int bottom = 3;\n    };\n\n    // initialize with no flags\n    std::bitset<4> style;\n    // set a flag\n    style.set(border_flags::bottom);\n    // set more flags\n    style\n      .set(border_flags::left)\n      .set(border_flags::top)\n      .set(border_flags::right);\n    // remove some flags\n    style[border_flags::left] = 0;\n    style.reset(border_flags::right);\n    // test if a flag is set\n    if (style.test(border_flags::bottom)) {}\n    // pass the flags as argument to a function\n    apply_style(style.to_ulong());\n```\n\n# 还有更多...\n\n位集可以从整数创建，并且可以使用`to_ulong()`或`to_ullong()`方法将其值转换为整数。但是，如果位集的大小大于这些数字类型的大小，并且超出所请求的数字类型大小的任何位被设置为`1`，则这些方法抛出`std::overflow_error`异常，因为该值不能在`unsigned long`或`unsigned long long`上表示。为了提取所有位，我们需要执行以下操作，如下面的代码所示:\n\n*   清除超出`unsigned long`或`unsigned long long`大小的位。\n*   将数值转换为`unsigned long`或`unsigned long long`。\n*   用`unsigned long`或`unsigned long long`中的位数移动位组。\n*   这样做，直到所有位都被检索到。\n\n```cpp\n    template <size_t N>\n    std::vector<unsigned long> bitset_to_vectorulong(std::bitset<N> bs)\n    {\n      auto result = std::vector<unsigned long> {};\n      auto const size = 8 * sizeof(unsigned long);\n      auto const mask = std::bitset<N>{ static_cast<unsigned long>(-1)};\n\n      auto totalbits = 0;\n      while (totalbits < N)\n      {\n        auto value = (bs & mask).to_ulong();\n        result.push_back(value);\n        bs >>= size;\n        totalbits += size;\n      }\n\n      return result;\n    }\n\n    std::bitset<128> bs =\n           (std::bitset<128>(0xFEDC) << 96) |\n           (std::bitset<128>(0xBA98) << 64) |\n           (std::bitset<128>(0x7654) << 32) |\n           std::bitset<128>(0x3210);\n\n    std::cout << bs << std::endl;\n\n    auto result = bitset_to_vectorulong(bs);\n    for (auto const v : result) \n      std::cout << std::hex << v << std::endl;\n```\n\n对于编译时无法知道`bitset`大小的情况，另一种选择是`std::vector<bool>`，我们将在下一个配方中介绍。\n\n# 请参见\n\n*   *使用向量<布尔>用于可变大小的位序列*\n\n# 将向量<bool>用于可变大小的位序列</bool>\n\n在前面的配方中，我们考虑了将`std::bitset`用于固定大小的位序列。然而，有时`std::bitset`不是一个好的选择，因为你在编译时不知道位数，仅仅定义一组足够大的位数并不是一个好主意，因为你可能会遇到位数实际上不够大的情况。对此的标准替代方案是使用`std::vector<bool>`容器，这是`std::vector`的特殊化，具有空间和速度优化，因为实现实际上并不存储布尔值，而是存储每个元素的单个位。\n\nFor this reason, however, `std::vector<bool>` does not meet the requirements of a standard container or sequential container, nor does `std::vector<bool>::iterator` meet the requirements of a forward iterator. As a result, this specialization cannot be used in generic code where a vector is expected. On the other hand, being a vector, it has a different interface from that of `std::bitset` and cannot be viewed as a binary representation of a number. There are no direct ways to construct `std::vector<bool>` from a number or string nor to convert to a number or string.\n\n# 准备好...\n\n这个食谱假设你熟悉`std::vector`和`std::bitset`。如果您没有阅读前面的食谱，*使用向量作为默认容器*和*使用位集作为固定大小的位序列*，您应该在继续之前这样做。\n\n`vector<bool>`类在`<vector>`头中的`std`命名空间中可用。\n\n# 怎么做...\n\n要操纵`std::vector<bool>`，请使用与操纵`std::vector<T>`相同的方法，如下例所示:\n\n*   创建一个空向量:\n\n```cpp\n        std::vector<bool> bv; // []\n```\n\n*   向向量添加位:\n\n```cpp\n        bv.push_back(true);  // [1]\n        bv.push_back(true);  // [1, 1]\n        bv.push_back(false); // [1, 1, 0]\n        bv.push_back(false); // [1, 1, 0, 0]\n        bv.push_back(true);  // [1, 1, 0, 0, 1]\n```\n\n*   设置各个位的值:\n\n```cpp\n        bv[3] = true;        // [1, 1, 0, 1, 1]\n```\n\n*   使用通用算法:\n\n```cpp\n        auto count_of_ones = std::count(bv.cbegin(), bv.cend(), true);\n```\n\n*   从向量中移除位:\n\n```cpp\n        bv.erase(bv.begin() + 2); // [1, 1, 1, 1]\n```\n\n# 它是如何工作的...\n\n`std::vector<bool>`不是标准向量，因为它被设计为通过为每个元素存储单个位而不是布尔值来提供空间优化。因此，它的元素不是以连续的序列存储的，不能替换布尔数组。正因为如此:\n\n*   索引运算符不能返回对特定元素的引用，因为元素不是单独存储的:\n\n```cpp\n        std::vector<bool> bv;\n        bv.resize(10);\n        auto& bit = bv[0];      // error\n```\n\n*   出于前面提到的相同原因，对迭代器取消引用不能产生对`bool`的引用:\n\n```cpp\n        auto& bit = *bv.begin(); // error\n```\n\n*   不能保证单个位可以从不同的线程同时独立操作。\n*   向量不能与需要前向迭代器的算法一起使用，例如`std::search()`。\n*   向量不能用在期望使用`std::vector<T>`的通用代码中，如果该代码需要本列表中提到的任何操作。\n\nAn alternative to `std::vector<bool>` is `std::dequeu<bool>`, which is a standard container (a double-ended queue) that meets all container and iterator requirements and can be used with all standard algorithms. However, this will not have the space optimization that `std::vector<bool>` is providing.\n\n# 还有更多...\n\n`std::vector<bool>`界面与`std::bitset`有很大不同。如果您希望能够以类似的方式编写代码，您可以在`std::vector<bool>`上创建一个包装器，在可能的情况下，它看起来像`std::bitset`。以下实现提供了类似于`std::bitset`的成员:\n\n```cpp\n    class bitvector\n    {\n      std::vector<bool> bv;\n    public:\n      bitvector(std::vector<bool> const & bv) : bv(bv) {}\n      bool operator[](size_t const i) { return bv[i]; }\n\n      inline bool any() const {\n        for (auto b : bv) if (b) return true;\n          return false;\n      }\n\n      inline bool all() const {\n        for (auto b : bv) if (!b) return false;\n          return true;\n      }\n\n      inline bool none() const { return !any(); }\n\n      inline size_t count() const {\n        return std::count(bv.cbegin(), bv.cend(), true);\n      }\n\n      inline size_t size() const { return bv.size(); }\n\n      inline bitvector & add(bool const value) {\n        bv.push_back(value);\n        return *this;\n      }\n\n      inline bitvector & remove(size_t const index) {\n        if (index >= bv.size())\n          throw std::out_of_range(\"Index out of range\");\n        bv.erase(bv.begin() + index);\n        return *this;\n      }\n\n      inline bitvector & set(bool const value = true) {\n        for (size_t i = 0; i < bv.size(); ++ i)\n          bv[i] = value;\n        return *this;\n      }\n\n      inline bitvector& set(size_t const index, bool const value = true) {\n        if (index >= bv.size())\n          throw std::out_of_range(\"Index out of range\");\n        bv[index] = value;\n        return *this;\n      }\n\n      inline bitvector & reset() {\n        for (size_t i = 0; i < bv.size(); ++ i) bv[i] = false;\n        return *this;\n      }\n\n      inline bitvector & reset(size_t const index) {\n        if (index >= bv.size())\n          throw std::out_of_range(\"Index out of range\");\n        bv[index] = false;\n        return *this;\n      }\n\n      inline bitvector & flip() {\n        bv.flip();\n        return *this;\n      }\n\n      std::vector<bool>& data() { return bv; }\n    };\n```\n\n这只是一个基本的实现，如果您想使用这样的包装器，您应该添加额外的方法，例如位逻辑操作、移位、可能从流读取和向流写入等等。但是，使用前面的代码，我们可以编写以下示例:\n\n```cpp\n    bitvector bv;\n    bv.add(true).add(true).add(false); // [1, 1, 0]\n    bv.add(false);                     // [1, 1, 0, 0]\n    bv.add(true);                      // [1, 1, 0, 0, 1]\n\n    if (bv.any()) std::cout << \"has some 1s\" << std::endl;\n    if (bv.all()) std::cout << \"has only 1s\" << std::endl;\n    if (bv.none()) std::cout << \"has no 1s\" << std::endl;\n    std::cout << \"has \" << bv.count() << \" 1s\" << std::endl;\n\n    bv.set(2, true);                   // [1, 1, 1, 0, 1]\n    bv.set();                          // [1, 1, 1, 1, 1]\n\n    bv.reset(0);                       // [0, 1, 1, 1, 1]\n    bv.reset();                        // [0, 0, 0, 0, 0]\n\n    bv.flip();                         // [1, 1, 1, 1, 1]\n```\n\n# 请参见\n\n*   *使用矢量作为默认容器*\n*   *将位集用于固定大小的位序列*\n\n# 查找范围内的元素\n\n我们在任何应用中最常见的操作之一是搜索数据。因此，标准库提供了许多通用算法来搜索标准容器或任何可以表示一个范围并由开始迭代器和结束迭代器定义的东西，这并不奇怪。在这个食谱中，我们将看到这些标准算法是什么，以及如何使用它们。\n\n# 准备好\n\n对于本食谱中的所有示例，我们将使用`std::vector`，但是所有算法都使用由开始和结束定义的范围，输入迭代器或前向迭代器，这取决于算法(有关各种类型迭代器的更多信息，请参见食谱，*编写您自己的随机访问迭代器*)。所有这些算法都可以在`<algorithm>`头中的`std`命名空间中获得。\n\n# 怎么做...\n\n以下是可用于查找范围内元素的算法列表:\n\n*   使用`std::find()`在一个范围内寻找一个值；这个算法返回一个迭代器到等于值的第一个元素:\n\n```cpp\n        std::vector<int> v{ 1, 1, 2, 3, 5, 8, 13 };\n\n        auto it = std::find(v.cbegin(), v.cend(), 3);\n        if (it != v.cend()) std::cout << *it << std::endl;\n```\n\n*   使用`std::find_if()`从一元谓词中找到符合标准的范围内的值；该算法将迭代器返回到谓词返回的第一个元素`true`:\n\n```cpp\n        std::vector<int> v{ 1, 1, 2, 3, 5, 8, 13 };\n\n        auto it = std::find_if(v.cbegin(), v.cend(), \n                               [](int const n) {return n > 10; });\n        if (it != v.cend()) std::cout << *it << std::endl;\n```\n\n*   使用`std::find_if_not()`从一元谓词中查找范围内不符合标准的值；该算法将迭代器返回到谓词返回的第一个元素`false`:\n\n```cpp\n        std::vector<int> v{ 1, 1, 2, 3, 5, 8, 13 };\n\n        auto it = std::find_if_not(v.cbegin(), v.cend(), \n                            [](int const n) {return n % 2 == 1; });\n        if (it != v.cend()) std::cout << *it << std::endl;\n```\n\n*   使用`std::find_first_of()`从一个范围中搜索另一个范围中任何值的出现；该算法返回找到的第一个元素的迭代器:\n\n```cpp\n        std::vector<int> v{ 1, 1, 2, 3, 5, 8, 13 };\n        std::vector<int> p{ 5, 7, 11 };\n\n        auto it = std::find_first_of(v.cbegin(), v.cend(),\n                                     p.cbegin(), p.cend());\n        if (it != v.cend()) \n          std::cout << \"found \" << *it\n                    << \" at index \" << std::distance(v.cbegin(), it)\n                    << std::endl;\n```\n\n*   使用`std::find_end()`查找一个范围内元素子范围的最后一次出现；这个算法返回一个迭代器到范围中最后一个子范围的第一个元素:\n\n```cpp\n        std::vector<int> v1{ 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1 };\n        std::vector<int> v2{ 1, 0, 1 };\n\n        auto it = std::find_end(v1.cbegin(), v1.cend(),\n                                v2.cbegin(), v2.cend());\n        if (it != v1.cend())\n          std::cout << \"found at index \"\n                    << std::distance(v1.cbegin(), it) << std::endl;\n```\n\n*   使用`std::search()`搜索某个范围内某个子范围的第一次出现；这个算法返回一个迭代器到范围内子范围的第一个元素:\n\n```cpp\n        auto text = \"The quick brown fox jumps over the lazy dog\"s;\n        auto word = \"over\"s;\n\n        auto it = std::search(text.cbegin(), text.cend(),\n                              word.cbegin(), word.cend());\n\n        if (it != text.cend())\n          std::cout << \"found \" << word\n                    << \" at index \" \n                    << std::distance(text.cbegin(), it) << std::endl;\n```\n\n*   将`std::search()`与*搜索器*一起使用，这是一个实现搜索算法并满足某些预定义标准的类。`std::search()`的这种过载是在 C++ 17 中引入的，可用的标准搜索程序实现了*博耶-摩尔*和*博耶-摩尔-霍斯普*字符串搜索算法:\n\n```cpp\n        auto text = \"The quick brown fox jumps over the lazy dog\"s;\n        auto word = \"over\"s;\n\n        auto it = std::search(\n          text.cbegin(), text.cend(),\n          std::make_boyer_moore_searcher(word.cbegin(), word.cend()));\n\n        if (it != text.cend())\n          std::cout << \"found \" << word\n                    << \" at index \" \n                    << std::distance(text.cbegin(), it) << std::endl;\n```\n\n*   使用`std::search_n()`搜索某个值在某个范围内连续出现的*N*；这个算法返回一个迭代器到找到的序列的第一个元素:\n\n```cpp\n        std::vector<int> v{ 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1 };\n\n        auto it = std::search_n(v.cbegin(), v.cend(), 2, 0);\n        if (it != v.cend())\n          std::cout << \"found at index \" \n                    << std::distance(v.cbegin(), it) << std::endl;\n```\n\n*   使用`std::adjacent_find()`查找范围内相等或满足二元谓词的两个相邻元素；该算法返回找到的第一个元素的迭代器:\n\n```cpp\n        std::vector<int> v{ 1, 1, 2, 3, 5, 8, 13 };\n\n        auto it = std::adjacent_find(v.cbegin(), v.cend());\n        if (it != v.cend())\n          std::cout << \"found at index \" \n                    << std::distance(v.cbegin(), it) << std::endl;\n\n       auto it = std::adjacent_find(\n         v.cbegin(), v.cend(),\n         [](int const a, int const b) {\n           return IsPrime(a) && IsPrime(b); });\n\n        if (it != v.cend())\n          std::cout << \"found at index \" \n                    << std::distance(v.cbegin(), it) << std::endl;\n```\n\n*   使用`std::binary_search()`查找排序范围内是否存在元素；该算法返回一个布尔值来指示是否找到该值:\n\n```cpp\n        std::vector<int> v{ 1, 1, 2, 3, 5, 8, 13 };\n\n        auto success = std::binary_search(v.cbegin(), v.cend(), 8);\n        if (success) std::cout << \"found\" << std::endl;\n```\n\n*   使用`std::lower_bound()`在不小于指定值的范围内找到第一个元素；该算法返回元素的迭代器:\n\n```cpp\n        std::vector<int> v{ 1, 1, 2, 3, 5, 8, 13 };\n\n        auto it = std::lower_bound(v.cbegin(), v.cend(), 1);\n        if (it != v.cend())\n          std::cout << \"lower bound at \"\n                    << std::distance(v.cbegin(), it) << std::endl;\n```\n\n*   使用`std::upper_bound()`在大于指定值的范围内找到第一个元素；该算法返回元素的迭代器:\n\n```cpp\n        std::vector<int> v{ 1, 1, 2, 3, 5, 8, 13 };\n\n        auto it = std::upper_bound(v.cbegin(), v.cend(), 1);\n        if (it != v.cend())\n          std::cout << \"upper bound at \"\n                    << std::distance(v.cbegin(), it) << std::endl;\n```\n\n*   使用`std::equal_range()`在一个值等于指定值的范围内寻找一个子范围。该算法返回一对迭代器，定义子范围的第一个迭代器和最后一个迭代器；这两个迭代器相当于`std::lower_bound()`和`std::upper_bound()`返回的迭代器:\n\n```cpp\n        std::vector<int> v{ 1, 1, 2, 3, 5, 8, 13 };\n\n        auto bounds = std::equal_range(v.cbegin(), v.cend(), 1);\n        std::cout << \"range between indexes \"\n                  << std::distance(v.cbegin(), bounds.first)\n                  << \" and \"\n                  << std::distance(v.cbegin(), bounds.second)\n                  << std::endl;\n```\n\n# 它是如何工作的...\n\n这些算法的工作方式非常相似:它们都将迭代器作为参数，迭代器定义了可搜索的范围和依赖于每个算法的附加参数。除了返回布尔值的`std::search()`和返回一对迭代器的`std::equal_range()`之外，它们都向搜索到的元素或子范围返回一个迭代器。这些迭代器必须与范围的结束迭代器(即过去最后一个元素)进行比较，以检查搜索是否成功。如果搜索没有找到元素或子范围，那么返回值就是结束迭代器。\n\n所有这些算法都有多个重载，但是在*怎么做呢...*部分，我们只看了一个特定的重载来展示如何使用该算法。有关所有重载的完整引用，您应该会看到其他来源。\n\n在前面的例子中，我们使用了常量迭代器，但是所有这些算法对于可变迭代器和反向迭代器都是一样的。因为它们将迭代器作为输入参数，所以它们可以使用标准容器、类似 C 的数组或任何表示序列并有迭代器可用的东西。\n\n关于`std::binary_search()`算法需要特别注意的是:定义搜索范围的迭代器参数至少要满足前向迭代器的要求。不管提供的迭代器的类型如何，比较的次数总是以范围的大小为对数。但是，如果迭代器是随机访问的，则迭代器增量的数量是不同的，在这种情况下，增量的数量也是对数的，或者不是随机访问的，在这种情况下，增量的数量是线性的，并且与范围的大小成比例。\n\n除了`std::find_if_not()`之外，所有这些算法在 C++ 11 之前都是可用的。然而，在较新的标准中引入了它们的一些重载。一个例子是`std::search()`，它有几个在 C++ 17 中引入的重载。其中一个重载具有以下形式:\n\n```cpp\n    template<class ForwardIterator, class Searcher>\n    ForwardIterator search(ForwardIterator first, ForwardIterator last,\n                           const Searcher& searcher );\n```\n\n这个重载搜索由标准提供了几种实现的搜索器函数对象定义的模式的出现:\n\n*   `default_searcher`基本上将搜索委托给标准的`std::search()`算法。\n*   `boyer_moore_searcher`实现了用于字符串搜索的 Boyer-Moore 算法。\n*   `boyer_moore_horspool_algorithm`实现了用于字符串搜索的 Boyer-Moore-Horspool 算法。\n\n# 还有更多...\n\n许多标准容器都有一个成员函数`find()`，用于查找容器中的元素。当这样的方法可用并且适合您的需求时，它应该优先于通用算法，因为这些成员函数是基于每个容器的特殊性进行优化的。\n\n# 请参见\n\n*   *使用矢量作为默认容器*\n*   *初始化范围*\n*   *在范围内使用设定操作*\n*   *排序范围*\n\n# 对范围进行排序\n\n在前面的食谱中，我们研究了在一定范围内搜索的标准通用算法。我们经常需要做的另一个常见操作是对一个范围进行排序，因为许多例程，包括一些搜索算法，都需要一个排序的范围。标准库提供了几种排序范围的通用算法，在本食谱中，我们将了解这些算法是什么以及如何使用它们。\n\n# 准备好\n\n排序一般算法使用由开始和结束迭代器定义的范围，因此，可以对标准容器、类似 C 的数组或任何表示序列并有随机迭代器可用的东西进行排序。但是，本食谱中的所有例子都将使用`std::vector`。\n\n# 怎么做...\n\n以下是搜索范围的标准通用算法列表:\n\n*   使用`std::sort()`对范围进行排序:\n\n```cpp\n        std::vector<int> v{3, 13, 5, 8, 1, 2, 1};\n\n        std::sort(v.begin(), v.end());\n        // v = {1, 1, 2, 3, 5, 8, 13}\n\n        std::sort(v.begin(), v.end(), std::greater<>());\n        // v = {13, 8, 5, 3, 2, 1 ,1}\n```\n\n*   使用`std::stable_sort()`对一个范围进行排序，但保持相等元素的顺序:\n\n```cpp\n        struct Task\n        {\n          int priority;\n          std::string name;\n        };\n\n        bool operator<(Task const & lhs, Task const & rhs) {\n          return lhs.priority < rhs.priority;\n        }\n\n        bool operator>(Task const & lhs, Task const & rhs) {\n          return lhs.priority > rhs.priority;\n        }\n\n        std::vector<Task> v{ \n          { 10, \"Task 1\"s }, { 40, \"Task 2\"s }, { 25, \"Task 3\"s },\n          { 10, \"Task 4\"s }, { 80, \"Task 5\"s }, { 10, \"Task 6\"s },\n        };\n\n        std::stable_sort(v.begin(), v.end());\n        // {{ 10, \"Task 1\" },{ 10, \"Task 4\" },{ 10, \"Task 6\" },\n        //  { 25, \"Task 3\" },{ 40, \"Task 2\" },{ 80, \"Task 5\" }}\n\n        std::stable_sort(v.begin(), v.end(), std::greater<>());\n        // {{ 80, \"Task 5\" },{ 40, \"Task 2\" },{ 25, \"Task 3\" },\n        //  { 10, \"Task 1\" },{ 10, \"Task 4\" },{ 10, \"Task 6\" }}\n```\n\n*   使用`std::partial_sort()`对一个范围的一部分进行排序(并以未指定的顺序保留其余部分):\n\n```cpp\n        std::vector<int> v{ 3, 13, 5, 8, 1, 2, 1 };\n\n        std::partial_sort(v.begin(), v.begin() + 4, v.end());\n        // v = {1, 1, 2, 3, ?, ?, ?}\n\n        std::partial_sort(v.begin(), v.begin() + 4, v.end(),\n                          std::greater<>());\n        // v = {13, 8, 5, 3, ?, ?, ?}\n```\n\n*   使用`std::partial_sort_copy()`通过将排序后的元素复制到第二个范围并保持原始范围不变来排序一个范围的一部分:\n\n```cpp\n        std::vector<int> v{ 3, 13, 5, 8, 1, 2, 1 };\n        std::vector<int> vc(v.size());\n\n        std::partial_sort_copy(v.begin(), v.end(), \n                               vc.begin(), vc.end());\n        // v = {3, 13, 5, 8, 1, 2, 1}\n        // vc = {1, 1, 2, 3, 5, 8, 13}\n\n        std::partial_sort_copy(v.begin(), v.end(), \n                               vc.begin(), vc.end(), std::greater<>());\n        // vc = {13, 8, 5, 3, 2, 1, 1}\n```\n\n*   使用`std::nth_element()`对一个范围进行排序，使得 *N* 第一个元素是如果该范围被完全排序的话会在该位置的元素，并且它之前的元素都较小，而它之后的元素都较大，不保证它们也被排序:\n\n```cpp\n        std::vector<int> v{ 3, 13, 5, 8, 1, 2, 1 };\n\n        std::nth_element(v.begin(), v.begin() + 3, v.end());\n        // v = {1, 1, 2, 3, 5, 8, 13}\n\n        std::nth_element(v.begin(), v.begin() + 3, v.end(),\n                         std::greater<>());\n        // v = {13, 8, 5, 3, 2, 1, 1}\n```\n\n*   使用`std::is_sorted()`检查范围是否排序:\n\n```cpp\n        std::vector<int> v { 1, 1, 2, 3, 5, 8, 13 };\n\n        auto sorted = std::is_sorted(v.cbegin(), v.cend());\n        sorted = std::is_sorted(v.cbegin(), v.cend(), \n                                std::greater<>());\n```\n\n*   使用`std::is_sorted_until()`从一个范围的开始找到一个排序的子范围:\n\n```cpp\n        std::vector<int> v{ 3, 13, 5, 8, 1, 2, 1 };\n\n        auto it = std::is_sorted_until(v.cbegin(), v.cend());\n        auto length = std::distance(v.cbegin(), it);\n```\n\n# 它是如何工作的...\n\n前面所有的通用算法都将随机迭代器作为参数来定义要排序的范围，并且其中一些算法还会获取一个输出范围。它们都有重载，一个需要比较函数对元素进行排序，一个不需要，使用`operator<`对元素进行比较。\n\n这些算法的工作方式如下:\n\n*   `std::stable_sort()`与`std::sort()`类似，但保证保持元素相等的原始顺序。\n*   `std::partial_sort()`取三个迭代器参数，表示一个范围内的第一个、中间的和最后一个元素，其中中间可以是任何元素，而不仅仅是自然中间位置的元素。结果是部分排序范围，使得来自原始范围的第一个`middle - first`最小元素，即`[first, last)`，在`[first, middle)`子范围中找到，而其余元素在`[middle, last)`子范围中以未指定的顺序出现。\n*   `std::partial_sort_copy()`不是`std::partial_copy()`的变体，正如名字所暗示的那样，而是`std::sort()`的变体。它通过将其元素复制到输出范围来对范围进行排序，而不改变它。算法的参数是输入和输出范围的第一个和最后一个迭代器。如果输出范围的大小 *M* 大于或等于输入范围的大小 *N* ，则输入范围被完全排序并复制到输出范围；输出范围的前 *N* 元素被覆盖，最后的 *M - N* 元素保持不变。如果输出范围小于输入范围，那么只有来自输入范围的第一个 *M* 排序的元素被复制到输出范围(在这种情况下被完全覆盖)。\n*   `std::nth_element()`基本上是一种选择算法的实现，这是一种寻找范围中第 *N* 个最小元素的算法。该算法采用三个迭代器参数来表示第一个、 *N* 和最后一个元素，并对该范围进行部分排序，以便在排序后，如果该范围已被完全排序，则第 *N* 个元素将位于该位置。在修改范围内，第 *n* 个元素之前的所有 *N-1* 个元素都小于它，第 *n* 个元素之后的所有元素都大于它。但是，这些其他元素的顺序没有保证。\n*   `std::is_sorted()`检查指定范围是否根据指定或默认的比较函数排序，并返回一个布尔值来指示。\n*   `std::is_sorted_until()`使用提供的比较函数或默认的`operator<`，从开始查找指定范围的排序子范围。返回值是一个迭代器，表示排序子范围的上限，也是最后一个排序元素的迭代器。\n\n# 还有更多...\n\n一些标准容器`std::list`和`std::forward_list`提供了一个成员函数`sort()`，该函数针对这些容器进行了优化。这些成员函数应该优先于通用标准算法`std::sort()`。\n\n# 请参见\n\n*   *使用矢量作为默认容器*\n*   *初始化范围*\n*   *在范围内使用设定操作*\n*   *寻找范围内的元素*\n\n# 初始化范围\n\n在前面的食谱中，我们探索了搜索范围和排序范围的通用标准算法。算法库提供了许多其他通用算法，其中有几个是用来填充值范围的。在这个食谱中，你将学习这些算法是什么，以及它们应该如何使用。\n\n# 准备好\n\n本食谱中所有的例子都使用`std::vector`。然而，像所有一般的算法一样，我们将在本食谱中看到的算法使用迭代器来定义范围的边界，因此可以与任何标准容器、C 类数组或表示定义了前向迭代器的序列的自定义类型一起使用。\n\n除了`<numeric>`头中的`std::iota()`外，其他算法都在`<algorithm>`头中。\n\n# 怎么做...\n\n要为范围赋值，请使用以下标准算法之一:\n\n*   `std::fill()`给一个范围的所有元素赋值；范围由第一个和最后一个前向迭代器定义:\n\n```cpp\n        std::vector<int> v(5);\n        std::fill(v.begin(), v.end(), 42);\n        // v = {42, 42, 42, 42, 42}\n```\n\n*   `std::fill_n()`给一个范围的多个元素赋值；该范围由第一个前向迭代器和一个计数器定义，该计数器指示有多少元素应该被赋予指定的值:\n\n```cpp\n        std::vector<int> v(10);\n        std::fill_n(v.begin(), 5, 42);\n        // v = {42, 42, 42, 42, 42, 0, 0, 0, 0, 0}\n```\n\n*   `std::generate()`将函数返回的值赋给一个范围的元素；该范围由第一个和最后一个前向迭代器定义，并且对该范围中的每个元素调用一次函数:\n\n```cpp\n        std::random_device rd{};\n        std::mt19937 mt{ rd() };\n        std::uniform_int_distribution<> ud{1, 10};\n        std::vector<int> v(5);\n        std::generate(v.begin(), v.end(), \n                      [&ud, &mt] {return ud(mt); }); \n```\n\n*   `std::generate_n()`将函数返回的值赋给一个范围的多个元素；该范围由第一个前向迭代器和一个计数器来定义，该计数器指示有多少元素应该被分配来自函数的值，该函数为每个元素调用一次:\n\n```cpp\n        std::vector<int> v(5);\n        auto i = 1;\n        std::generate_n(v.begin(), v.size(), [&i] { return i*i++ ; });\n        // v = {1, 4, 9, 16, 25}\n```\n\n*   `std::iota()`给一个范围的元素分配顺序递增的值；范围由第一个和最后一个前向迭代器定义，值使用前缀`operator++ `从初始指定值开始递增:\n\n```cpp\n        std::vector<int> v(5);\n        std::iota(v.begin(), v.end(), 1);\n        // v = {1, 2, 3, 4, 5}\n```\n\n# 它是如何工作的...\n\n`std::fill()`和`std::fill_n()`的工作原理类似，但不同之处在于指定范围的方式:前者由第一个和最后一个迭代器指定，后者由第一个迭代器和计数指定。第二个算法返回一个迭代器，如果计数器大于零，则表示最后一个赋值的元素，否则表示范围第一个元素的迭代器。\n\n`std::generate()`和`std::generate_n()`也相似，不同之处仅在于范围的规定方式。第一个使用两个迭代器，定义范围的上下限，第二个使用第一个元素的迭代器和一个计数。与`std::fill_n()`类似，`std::generate_n()`也返回一个迭代器，如果计数大于零，则表示最后一个赋值的元素，否则表示范围第一个元素的迭代器。这些算法为范围中的每个元素调用指定的函数，并将返回值赋给元素。生成函数不接受任何参数，因此参数的值不能传递给函数，因为这是用来初始化范围元素的函数。如果需要使用元素的值生成新的值，应该使用`std::transform()`。\n\n`std::iota()`它的名字来自 APL 编程语言的ι (iota)函数，虽然它是初始 STL 的一部分，但它只包含在 C++ 11 的标准库中。该函数将第一个和最后一个迭代器带到一个范围，并将初始值分配给该范围的第一个元素，然后使用前缀`operator++ `为该范围中的其余元素生成顺序递增的值。\n\n# 请参见\n\n*   *使用矢量作为默认容器*\n*   *排序范围*\n*   *在范围内使用设定操作*\n*   *寻找范围内的元素*\n*   *生成伪随机数[第九章](09.html)*的*配方处理数字和字符串*\n*   *初始化[第九章](09.html)*处理数字和字符串*的伪随机数生成器*配方的所有内部状态位\n\n# 对范围使用集合运算\n\n标准库为集合运算提供了几种算法，使我们能够对排序范围进行并集、交集或差集。在这个食谱中，我们将看到这些算法是什么以及它们是如何工作的。\n\n# 准备好\n\n集合运算的算法与迭代器一起工作，这意味着它们可以用于标准容器、类似 C 的数组或任何表示具有可用输入迭代器的序列的自定义类型。本食谱中的所有例子都将使用`std::vector`。\n\n对于下一节中的所有示例，我们将使用以下范围:\n\n```cpp\n    std::vector<int> v1{ 1, 2, 3, 4, 4, 5 };\n    std::vector<int> v2{ 2, 3, 3, 4, 6, 8 };\n    std::vector<int> v3;\n```\n\n# 怎么做...\n\n对集合运算使用以下通用算法:\n\n*   `std::set_union()`计算两个范围合并成第三个范围:\n\n```cpp\n        std::set_union(v1.cbegin(), v1.cend(),\n                       v2.cbegin(), v2.cend(),\n                       std::back_inserter(v3));\n        // v3 = {1, 2, 3, 3, 4, 4, 5, 6, 8}\n```\n\n*   `std::merge()`将两个范围的内容合并成第三个范围；这与`std::set_union()`类似，只是它将输入范围的全部内容复制到输出范围中，而不仅仅是它们的联合:\n\n```cpp\n        std::merge(v1.cbegin(), v1.cend(),\n                   v2.cbegin(), v2.cend(),\n                   std::back_inserter(v3));\n        // v3 = {1, 2, 2, 3, 3, 3, 4, 4, 4, 5, 6, 8}\n```\n\n*   `std::set_intersection()`计算两个范围的交集进入第三个范围:\n\n```cpp\n        std::set_intersection(v1.cbegin(), v1.cend(),\n                              v2.cbegin(), v2.cend(),\n                              std::back_inserter(v3));\n        // v3 = {2, 3, 4}\n```\n\n*   `std::set_difference()`将两个范围之差计算成第三个范围；输出范围将包含第一个范围中的元素，这些元素不在第二个范围中:\n\n```cpp\n        std::set_difference(v1.cbegin(), v1.cend(),\n                            v2.cbegin(), v2.cend(),\n                            std::back_inserter(v3));\n        // v3 = {1, 4, 5}\n```\n\n*   `std::set_symmetric_difference()`计算两个范围到第三范围的对偶差；输出范围将包含存在于任何输入范围中的元素，但只存在于一个范围中:\n\n```cpp\n        std::set_symmetric_difference(v1.cbegin(), v1.cend(),\n                                      v2.cbegin(), v2.cend(),\n                                      std::back_inserter(v3));\n        // v3 = {1, 3, 4, 5, 6, 8}\n```\n\n*   `std::includes()`检查一个范围是否是另一个范围的子集(即其所有元素也存在于另一个范围中):\n\n```cpp\n        std::vector<int> v1{ 1, 2, 3, 4, 4, 5 };\n        std::vector<int> v2{ 2, 3, 3, 4, 6, 8 };\n        std::vector<int> v3{ 1, 2, 4 };\n        std::vector<int> v4{ };\n\n        auto i1 = std::includes(v1.cbegin(), v1.cend(), \n                                v2.cbegin(), v2.cend()); // i1 = false\n        auto i2 = std::includes(v1.cbegin(), v1.cend(), \n                                v3.cbegin(), v3.cend()); // i2 = true\n        auto i3 = std::includes(v1.cbegin(), v1.cend(), \n                                v4.cbegin(), v4.cend()); // i3 = true\n```\n\n# 它是如何工作的...\n\n事实上，从两个输入范围生成新范围的所有 set 操作都具有相同的接口，并且工作方式相似:\n\n*   它们接受两个输入范围，每个范围由第一个和最后一个输入迭代器定义。\n*   他们将输出迭代器带到要插入元素的输出范围。\n*   它们有一个重载，该重载接受一个表示比较二进制函数对象的额外参数，如果第一个参数小于第二个参数，则该对象必须返回`true`。未指定比较函数对象时，使用`operator<`。\n*   它们在构造的输出范围结束后返回一个迭代器。\n*   根据所使用的过载，必须使用`operator<`或提供的比较功能对输入范围进行排序。\n*   输出范围不得与两个输入范围重叠。\n\n我们将使用 POD 类型的向量`Task`通过额外的例子来演示它们的工作方式，我们在之前的配方中也使用了该向量:\n\n```cpp\n    struct Task\n    {\n      int priority;\n      std::string name;\n    };\n\n    bool operator<(Task const & lhs, Task const & rhs) {\n      return lhs.priority < rhs.priority;\n    } \n\n    bool operator>(Task const & lhs, Task const & rhs) {\n      return lhs.priority > rhs.priority;\n    }\n\n    std::vector<Task> v1{\n      { 10, \"Task 1.1\"s },\n      { 20, \"Task 1.2\"s },\n      { 20, \"Task 1.3\"s },\n      { 20, \"Task 1.4\"s },\n      { 30, \"Task 1.5\"s },\n      { 50, \"Task 1.6\"s },\n    };\n\n    std::vector<Task> v2{\n      { 20, \"Task 2.1\"s },\n      { 30, \"Task 2.2\"s },\n      { 30, \"Task 2.3\"s },\n      { 30, \"Task 2.4\"s },\n      { 40, \"Task 2.5\"s },\n      { 50, \"Task 2.6\"s },\n    };\n```\n\n这里描述了每种算法产生输出范围的具体方式:\n\n*   `std::set_union()`将一个或两个输入范围中的所有元素复制到输出范围，产生一个新的排序范围。如果一个元素在第一个范围内被发现 *M* 次，在第二个范围内被发现 *N* 次，那么第一个范围内的所有 *M* 元素将以它们现有的顺序被复制到输出范围，然后第二个范围内的 *N-M* 元素被复制到输出范围，如果 *N > M* 则为 0 个元素，否则为:\n\n```cpp\n        std::vector<Task> v3;\n        std::set_union(v1.cbegin(), v1.cend(),\n                       v2.cbegin(), v2.cend(),\n                       std::back_inserter(v3));\n        // v3 = {{10, \"Task 1.1\"},{20, \"Task 1.2\"},{20, \"Task 1.3\"},\n        //       {20, \"Task 1.4\"},{30, \"Task 1.5\"},{30, \"Task 2.3\"},\n        //       {30, \"Task 2.4\"},{40, \"Task 2.5\"},{50, \"Task 1.6\"}}\n```\n\n*   `std::merge()`将两个输入范围内的所有元素复制到输出范围内，产生一个根据比较函数排序的新范围:\n\n```cpp\n        std::vector<Task> v4;\n        std::merge(v1.cbegin(), v1.cend(),\n                   v2.cbegin(), v2.cend(),\n                   std::back_inserter(v4));\n        // v4 = {{10, \"Task 1.1\"},{20, \"Task 1.2\"},{20, \"Task 1.3\"},\n        //       {20, \"Task 1.4\"},{20, \"Task 2.1\"},{30, \"Task 1.5\"},\n        //       {30, \"Task 2.2\"},{30, \"Task 2.3\"},{30, \"Task 2.4\"},\n        //       {40, \"Task 2.5\"},{50, \"Task 1.6\"},{50, \"Task 2.6\"}}\n```\n\n*   `std::set_intersection()`将在两个输入范围内找到的所有元素复制到输出范围内，产生一个根据比较函数排序的新范围:\n\n```cpp\n        std::vector<Task> v5;\n        std::set_intersection(v1.cbegin(), v1.cend(),\n                              v2.cbegin(), v2.cend(),\n                              std::back_inserter(v5));\n        // v5 = {{20, \"Task 1.2\"},{30, \"Task 1.5\"},{50, \"Task 1.6\"}}\n```\n\n*   `std::set_difference()`将第一个输入范围中在第二个输入范围中找不到的所有元素复制到输出范围。对于在两个范围内找到的等价元素，以下规则适用:如果一个元素在第一个范围内找到 *M* 次，在第二个范围内找到 *N* 次，如果 *M > N* 次，则复制 *M-N* 次；否则不复制:\n\n```cpp\n        std::vector<Task> v6;\n        std::set_difference(v1.cbegin(), v1.cend(),\n                            v2.cbegin(), v2.cend(),\n                            std::back_inserter(v6));\n        // v6 = {{10, \"Task 1.1\"},{20, \"Task 1.3\"},{20, \"Task 1.4\"}}\n```\n\n*   `std::set_symmetric_difference()`将两个输入范围中任何一个范围内的所有元素复制到输出范围，但不复制到两个范围内。如果一个元素在第一个范围内被发现 *M* 次，在第二个范围内被发现 *N* 次，那么如果来自第一个范围的那些元素的最后 *M > N、*M-N 被复制到输出范围内，否则，来自第二个范围的那些元素的最后 *N-M* 将被复制到输出范围内:\n\n```cpp\n        std::vector<Task> v7;\n        std::set_symmetric_difference(v1.cbegin(), v1.cend(),\n                                      v2.cbegin(), v2.cend(),\n                                      std::back_inserter(v7));\n        // v7 = {{10, \"Task 1.1\"},{20, \"Task 1.3\"},{20, \"Task 1.4\"}\n        //       {30, \"Task 2.3\"},{30, \"Task 2.4\"},{40, \"Task 2.5\"}}\n```\n\n另一方面，`std::includes()`不产生输出范围；它只检查第二范围是否包括在第一范围内。如果第二个范围为空或其所有元素都包含在第一个范围内，则返回一个布尔值`true`，否则返回`false`。它还有两个重载，其中一个重载指定了一个比较二进制函数对象。\n\n# 请参见\n\n*   *使用矢量作为默认容器*\n*   *排序范围*\n*   *初始化范围*\n*   *使用迭代器在容器中插入新元素*\n*   *寻找范围内的元素*\n\n# 使用迭代器在容器中插入新元素\n\n当您使用容器时，在开头、结尾或中间的某个地方插入新元素通常很有用。有一些算法，比如我们在前面的食谱中看到的，*在一个范围*上使用 set 操作，需要一个迭代器到一个要插入的范围，但是如果你简单地传递一个迭代器，比如`begin()`返回的迭代器，它不会插入而是覆盖容器的元素。而且，使用`end()`返回的迭代器是不可能在末尾插入的。为了执行这样的操作，标准库提供了一组迭代器和迭代器适配器来实现这些场景。\n\n# 准备好\n\n本食谱中讨论的迭代器和适配器可以在`<iterator>`头中的`std`命名空间中找到。如果包含诸如`<algorithm>`之类的标题，则不必明确包含`<iterator>`。\n\n# 怎么做...\n\n使用以下迭代器适配器在容器中插入新元素:\n\n*   `std::back_inserter()`在末端插入元素，对于有`push_back()`方法的容器:\n\n```cpp\n        std::vector<int> v{ 1,2,3,4,5 };\n        std::fill_n(std::back_inserter(v), 3, 0);\n        // v={1,2,3,4,5,0,0,0}\n```\n\n*   `std::front_inserter()`在开头插入元素，对于有`push_front()`方法的容器:\n\n```cpp\n        std::list<int> l{ 1,2,3,4,5 };\n        std::fill_n(std::front_inserter(l), 3, 0);\n        // l={0,0,0,1,2,3,4,5}\n```\n\n*   `std::inserter()`插入容器中的任何位置，对于具有`insert()`方法的容器:\n\n```cpp\n        std::vector<int> v{ 1,2,3,4,5 };\n        std::fill_n(std::inserter(v, v.begin()), 3, 0);\n        // v={0,0,0,1,2,3,4,5}\n\n        std::list<int> l{ 1,2,3,4,5 };\n        auto it = l.begin();\n        std::advance(it, 3);\n        std::fill_n(std::inserter(l, it), 3, 0);\n        // l={1,2,3,0,0,0,4,5}\n```\n\n# 它是如何工作的...\n\n`std::back_inserter()`、`std::front_inserter()`和`std::inserter()`都是帮助函数，它们创建类型为`std::back_insert_iterator`、`std::front_insert_iterator`和`std::insert_iterator`的迭代器适配器。这些都是输出迭代器，它们追加、前置或插入到为其构建的容器中。递增和取消引用这些迭代器没有任何作用。但是，在赋值时，这些迭代器从容器中调用以下方法:\n\n*   `std::back_insterter_iterator`呼叫`push_back()`\n*   `std::front_inserter_iterator`呼叫`push_front()`\n*   `std::insert_iterator`呼叫`insert()`\n\n以下是`std::back_inserter_iterator`的过度简化实现:\n\n```cpp\n    template<class C>\n    class back_insert_iterator {\n    public:\n      typedef back_insert_iterator<C> T;\n      typedef typename C::value_type V;\n\n      explicit back_insert_iterator( C& c ) :container( &c ) { }\n\n      T& operator=( const V& val ) { \n        container->push_back( val );\n        return *this;\n      }\n\n      T& operator*() { return *this; }\n\n      T& operator++() { return *this; }\n\n      T& operator++( int ) { return *this; }\n      protected:\n      C* container;\n    };\n```\n\n由于赋值运算符的工作方式，这些迭代器只能用于一些标准容器:\n\n*   `std::back_insert_iterator`可搭配`std::vector`、`std::list`、`std::deque`、`std::basic_string`使用。\n*   `std::front_insert_iterator`可与`std::list`、`std::forward_list`、`std:deque`配合使用。\n*   `std::insert_iterator`可用于所有标准容器。\n\n以下示例在`std::vector`的开头插入三个值为 0 的元素:\n\n```cpp\n    std::vector<int> v{ 1,2,3,4,5 };\n    std::fill_n(std::inserter(v, v.begin()), 3, 0);\n    // v={0,0,0,1,2,3,4,5}\n```\n\n`std::inserter()`适配器接受两个参数:容器和元素应该插入的迭代器。在容器上调用`insert()`时，`std::insert_iterator`递增迭代器，因此再次被赋值时，它可以在下一个位置插入一个新元素。下面是如何为这个迭代器适配器实现赋值运算符:\n\n```cpp\n    T& operator=(const V& v)\n    {  \n      iter = container->insert(iter, v);\n      ++ iter;\n      return (*this);\n    }\n```\n\n# 还有更多...\n\n这些迭代器适配器旨在与将多个元素插入一个范围的算法或函数一起使用。当然，它们可以用来插入单个元素，但这是一种反模式，因为在这种情况下，简单地调用`push_back()`、`push_front()`或`insert()`要简单和直观得多。应避免以下示例:\n\n```cpp\n    std::vector<int> v{ 1,2,3,4,5 };\n    *std::back_inserter(v) = 6; // v = {1,2,3,4,5,6}\n\n    std::back_insert_iterator<std::vector<int>> it(v);\n    *it = 7;                    // v = {1,2,3,4,5,6,7}\n```\n\n# 请参见\n\n*   *在范围内使用设定操作*\n\n# 编写自己的随机访问迭代器\n\n在[第 8 章](08.html)、*学习现代核心语言特性*中，我们看到了如何通过实现迭代器和释放`begin()`和`end()`函数将迭代器返回到自定义范围的第一个和最后一个元素，从而为自定义类型启用基于范围的循环。您可能已经注意到，我们在该方法中提供的最小迭代器实现不符合标准迭代器的要求，因为它不能被复制构造或赋值，也不能被递增。在这个食谱中，我们将建立在这个例子的基础上，并展示如何创建一个满足所有要求的随机访问迭代器。\n\n# 准备好\n\n对于这个方法，您应该知道标准定义的迭代器的类型以及它们的不同之处。在[http://www.cplusplus.com/reference/iterator/](http://www.cplusplus.com/reference/iterator/)可以很好地了解他们的需求。\n\n为了举例说明如何编写随机访问迭代器，我们将考虑在*中使用的`dummy_array`类的一个变体，为自定义类型的循环启用基于范围的[第 8 章](08.html)、*学习现代核心语言特性*的*配方。这是一个非常简单的数组概念，除了作为演示迭代器的代码库之外，没有任何实际价值:\n\n```cpp\n    template <typename Type, size_t const SIZE>\n    class dummy_array\n    {\n      Type data[SIZE] = {};\n    public:\n      Type& operator[](size_t const index)\n      {\n        if (index < SIZE) return data[index];\n        throw std::out_of_range(\"index out of range\");\n      }\n\n     Type const & operator[](size_t const index) const\n     {\n       if (index < SIZE) return data[index];\n       throw std::out_of_range(\"index out of range\");\n     }\n\n      size_t size() const { return SIZE; }\n    };\n```\n\n下一节中显示的所有代码，迭代器类，`typedef` s，`begin()`和`end()`函数，都是这个类的一部分。\n\n# 怎么做...\n\n要为上一节中显示的`dummy_array`类提供可变的和恒定的随机访问迭代器，请向该类添加以下成员:\n\n*   迭代器类模板，用元素类型和数组大小参数化。该类必须有以下定义标准同义词的公共`typedef`:\n\n```cpp\n        template <typename T, size_t const Size>\n        class dummy_array_iterator\n        {\n        public:\n          typedef dummy_array_iterator            self_type;\n          typedef T                               value_type;\n          typedef T&                              reference;\n          typedef T*                              pointer;\n          typedef std::random_access_iterator_tag iterator_category;\n          typedef ptrdiff_t                       difference_type;\n        };\n```\n\n*   迭代器类的私有成员:指向数组数据的指针和数组的当前索引:\n\n```cpp\n        private:\n           pointer ptr = nullptr;\n           size_t index = 0;\n```\n\n*   迭代器类的私有方法，用于检查两个迭代器实例是否指向相同的数组数据:\n\n```cpp\n        private:\n          bool compatible(self_type const & other) const\n          {\n            return ptr == other.ptr;\n          }\n```\n\n*   迭代器类的显式构造函数:\n\n```cpp\n        public:\n           explicit dummy_array_iterator(pointer ptr, \n                                         size_t const index) \n             : ptr(ptr), index(index) { }\n```\n\n*   迭代器类成员满足所有迭代器的通用要求:可复制构造、可复制分配、可析构、前缀和后缀可递增。在这种实现方式中，后递增运算符是根据前递增运算符实现的，以避免代码重复:\n\n```cpp\n        dummy_array_iterator(dummy_array_iterator const & o) \n           = default;\n        dummy_array_iterator& operator=(dummy_array_iterator const & o) \n           = default;\n        ~dummy_array_iterator() = default;\n\n        self_type & operator++ ()\n        {\n           if (index >= Size) \n             throw std::out_of_range(\"Iterator cannot be incremented past \n                                      the end of range.\");\n          ++ index;\n          return *this;\n        }\n\n        self_type operator++ (int)\n        {\n          self_type tmp = *this;\n          ++*this;\n          return tmp;\n        }\n```\n\n*   满足输入迭代器要求的迭代器类成员:测试等式/不等式，作为右值取消引用:\n\n```cpp\n        bool operator== (self_type const & other) const\n        {\n          assert(compatible(other));\n          return index == other.index;\n        }\n\n        bool operator!= (self_type const & other) const\n        {\n          return !(*this == other);\n        }\n\n        reference operator* () const\n        {\n          if (ptr == nullptr)\n            throw std::bad_function_call();\n          return *(ptr + index);\n        }\n\n        reference operator-> () const\n        {\n          if (ptr == nullptr)\n            throw std::bad_function_call();\n          return *(ptr + index);\n        }\n```\n\n*   满足前向迭代器要求的迭代器类成员:默认可构造:\n\n```cpp\n        dummy_array_iterator() = default;\n```\n\n*   满足双向迭代器要求的迭代器类成员:\n\n```cpp\n        self_type & operator--()\n        {\n          if (index <= 0) \n            throw std::out_of_range(\"Iterator cannot be decremented \n                                     past the end of range.\");\n          --index;\n          return *this;\n        }\n\n        self_type operator--(int)\n        {\n          self_type tmp = *this;\n          --*this;\n          return tmp;\n        }\n```\n\n*   迭代器类成员满足随机访问迭代器的要求:算术加法和减法，与其他迭代器的不等式相当，复合赋值，偏移量不可引用:\n\n```cpp\n        self_type operator+(difference_type offset) const\n        {\n          self_type tmp = *this;\n          return tmp += offset;\n        }\n\n        self_type operator-(difference_type offset) const\n        {\n          self_type tmp = *this;\n          return tmp -= offset;\n        }\n\n        difference_type operator-(self_type const & other) const\n        {\n          assert(compatible(other));\n          return (index - other.index);\n        }\n\n        bool operator<(self_type const & other) const\n        {\n          assert(compatible(other));\n          return index < other.index;\n        }\n\n        bool operator>(self_type const & other) const\n        {\n          return other < *this;\n        }\n\n        bool operator<=(self_type const & other) const\n        {\n          return !(other < *this);\n        }\n\n        bool operator>=(self_type const & other) const\n        {\n          return !(*this < other);\n        }\n\n        self_type & operator+=(difference_type const offset)\n        {\n          if (index + offset < 0 || index + offset > Size)\n            throw std::out_of_range(\"Iterator cannot be incremented \n                                     past the end of range.\");\n          index += offset;\n          return *this;\n        }\n\n        self_type & operator-=(difference_type const offset)\n        {\n          return *this += -offset;\n        }\n\n        value_type & operator[](difference_type const offset)\n        {\n          return (*(*this + offset));\n        }\n\n        value_type const & operator[](difference_type const offset) const\n        {\n          return (*(*this + offset));\n        }\n```\n\n*   将`typedef` s 添加到`dummy_array`类中，用于可变和常量迭代器同义词:\n\n```cpp\n        public:\n           typedef dummy_array_iterator<Type, SIZE> \n                   iterator;\n           typedef dummy_array_iterator<Type const, SIZE> \n                   constant_iterator;\n```\n\n*   将公共的`begin()`和`end()`函数添加到`dummy_array`类中，以将迭代器返回到数组中的第一个和最后一个元素:\n\n```cpp\n        iterator begin() \n        {\n          return iterator(data, 0);\n        }\n\n        iterator end()\n        {\n          return iterator(data, SIZE);\n        }\n\n        constant_iterator begin() const\n        {\n          return constant_iterator(data, 0);\n        }\n\n        constant_iterator end() const\n        {\n          return constant_iterator(data, SIZE);\n        }\n```\n\n# 它是如何工作的...\n\n标准库定义了五类迭代器:\n\n*   *输入迭代器*:这些是最简单的类别，只保证单程序列算法的有效性。递增后，以前的副本可能会变得无效。\n*   *输出迭代器*:这些基本上都是输入迭代器，可以用来写入指向的元素。\n*   *前向迭代器*:这些迭代器可以读取(和写入)指向元素的数据。它们满足输入迭代器的要求，此外，必须是默认可构造的，并且必须支持多遍场景，而不会使之前的副本无效。\n*   *双向迭代器*:这些是正向迭代器，另外支持递减，所以可以双向移动。\n*   *随机访问迭代器*:这些支持在恒定时间内访问容器中的任何元素。它们实现了双向迭代器的所有要求，此外，还支持算术运算`+`和`-`、复合赋值`+=`和`-=`、与其他具有`<`、`<=`、`>`、`>=`的迭代器的比较以及偏移取消引用操作符。\n\n也实现输出迭代器要求的正向、双向和随机访问迭代器称为*可变迭代器*。\n\n在上一节中，我们看到了如何实现随机访问迭代器，并逐步演练了每一类迭代器的需求(因为每一类迭代器都包含了前一类的需求并增加了新的需求)。迭代器类模板对于常量迭代器和可变迭代器都很常见，我们为它定义了两个同义词`iterator`和`constant_iterator`。\n\n在实现内部迭代器类模板之后，我们还定义了`begin()`和`end()`成员函数，它们将迭代器返回到数组中的第一个和最后一个元素。这些方法有重载来返回可变迭代器或常量迭代器，这取决于`dummy_array`类实例是可变的还是常量的。\n\n有了`dummy_array`类及其迭代器的这个实现，我们可以编写以下示例。有关更多示例，请查看本书附带的源代码:\n\n```cpp\n    dummy_array<int, 3> a;\n    a[0] = 10;\n    a[1] = 20;\n    a[2] = 30;\n\n    std::transform(a.begin(), a.end(), a.begin(), \n                   [](int const e) {return e * 2; });\n\n    for (auto&& e : a) std::cout << e << std::endl;\n\n    auto lp = [](dummy_array<int, 3> const & ca)\n    {\n      for (auto const & e : ca) \n        std::cout << e << std::endl;\n    };\n\n    lp(a);\n\n    dummy_array<std::unique_ptr<Tag>, 3> ta;\n    ta[0] = std::make_unique<Tag>(1, \"Tag 1\");\n    ta[1] = std::make_unique<Tag>(2, \"Tag 2\");\n    ta[2] = std::make_unique<Tag>(3, \"Tag 3\");\n\n    for (auto it = ta.begin(); it != ta.end(); ++ it)\n      std::cout << it->id << \" \" << it->name << std::endl;\n```\n\n# 还有更多...\n\n除了`begin()`和`end()`之外，一个容器可能还有其他方法，例如`cbegin()` / `cend()`(对于常量迭代器)、`rbegin()` / `rend()`(对于可变的反向迭代器)和`crbegin()` / `crend()`(对于常量反向迭代器)。实现这一点是留给你的练习。\n\n另一方面，在现代 C++ 中，这些返回第一个和最后一个迭代器的函数不必是成员函数，而是可以作为非成员函数提供。其实这是下一个食谱的题目，*非成员函数的容器访问*。\n\n# 请参见\n\n*   *为自定义类型的循环启用基于范围的[第 8 章](08.html)、*食谱学习现代核心语言功能**\n*   *创建类型别名和别名模板[第八章](08.html)*学习现代核心语言功能**\n\n# 具有非成员函数的容器访问\n\n标准容器提供`begin()`和`end()`成员函数，用于检索容器的第一个和最后一个元素的迭代器。这些函数实际上有四套。除了`begin()` / `end()`之外，容器提供`cbegin()` / `cend()`返回常量迭代器，`rbegin()` / `rend()`返回可变的反向迭代器，`crbegin()` / `crend()`返回常量反向迭代器。在 C++ 11/C++ 14 中，所有这些都有与标准容器、类似 C 的数组和任何专用于它们的自定义类型一起工作的非成员等价物。在 C++ 17 中，甚至增加了更多的非成员函数；`std::data()` -返回包含容器元素的内存块的指针，`std::size()` -返回容器或数组的大小，`std::empty()` -返回给定容器是否为空。这些非成员函数用于泛型代码，但可以在代码中的任何地方使用。\n\n# 准备好\n\n在这个食谱中，我们将使用我们在前面的食谱中实现的`dummy_array`类及其迭代器作为例子，*编写您自己的随机访问迭代器*。在继续这个食谱之前，你应该先看看那个食谱。\n\n非成员`begin()` / `end()`函数和其他变体以及非成员`data()`、`size()`和`empty()`在`<iterator>`头中的`std`名称空间中可用，该名称空间隐式包含在以下任何头中:`<array>`、`<deque>`、`<forward_list>`、`<list>`、`<map>`、`<regex>`、`<set>`、`<string>`、`<unordered_map>`、`<unordered_set>`和`<vector>`。\n\n在本食谱中，我们将参考`std::begin()` / `std::end()`功能，但讨论的所有内容也适用于其他功能:`std::cbegin()` / `std::cend()`、`std::rbegin()` / `std::rend()`和`std::crbegin()` / `std::crend()`。\n\n# 怎么做...\n\n使用非成员`std::begin()` / `std::end()`功能和其他变体，以及`std::data()`、`std::size()`和`std::empty()`与:\n\n*   标准容器:\n\n```cpp\n        std::vector<int> v1{ 1, 2, 3, 4, 5 };\n        auto sv1 = std::size(v1);  // sv1 = 5\n        auto ev1 = std::empty(v1); // ev1 = false\n        auto dv1 = std::data(v1);  // dv1 = v1.data()\n        for (auto i = std::begin(v1); i != std::end(v1); ++ i)\n          std::cout << *i << std::endl;\n\n        std::vector<int> v2;\n        std::copy(std::cbegin(v1), std::cend(v1),\n                  std::back_inserter(v2));\n```\n\n*   (类 C)数组:\n\n```cpp\n        int a[5] = { 1, 2, 3, 4, 5 };\n        auto pos = std::find_if(std::crbegin(a), std::crend(a), \n                                [](int const n) {return n % 2 == 0; });\n        auto sa = std::size(a);  // sa = 5\n        auto ea = std::empty(a); // ea = false\n        auto da = std::data(a);  // da = a\n```\n\n*   提供相应成员功能的自定义类型，`begin()` / `end()`、`data()`、`empty()`或`size()`:\n\n```cpp\n        dummy_array<std::string, 5> sa;\n        dummy_array<int, 5> sb;\n        sa[0] = \"1\"s;\n        sa[1] = \"2\"s;\n        sa[2] = \"3\"s;\n        sa[3] = \"4\"s;\n        sa[4] = \"5\"s;\n\n        std::transform(\n          std::begin(sa), std::end(sa), \n          std::begin(sb), \n          [](std::string const & s) {return std::stoi(s); });\n        // sb = [1, 2, 3, 4, 5]\n\n        auto sa_size = std::size(sa); // sa_size = 5\n```\n\n*   容器类型未知的通用代码:\n\n```cpp\n        template <typename F, typename C>\n        void process(F&& f, C const & c)\n        {\n          std::for_each(std::begin(c), std::end(c), \n                        std::forward<F>(f));\n        }\n\n        auto l = [](auto const e) {std::cout << e << std::endl; };\n\n        process(l, v1); // std::vector<int>\n        process(l, a);  // int[5]\n        process(l, sa); // dummy_array<std::string, 5>\n```\n\n# 它是如何工作的...\n\n这些非成员函数在标准的不同版本中被引入，但都在 C++ 17 中被修改以返回`constexpr auto`:\n\n*   C++ 11 中的`std::begin()`和`std::end()`\n*   C++ 14 中的`std::cbegin()` / `std::cend()`、`std::rbegin()` / `std::rend()`和`std::crbegin()` / `std::crend()`\n*   C++ 17 中的`std::data()`、`std::size()`和`std::empty()`\n\n`begin()` / `end()`函数族有容器类和数组的重载，它们所做的就是:\n\n*   返回调用容器的容器对应成员函数的结果。\n*   返回指向数组的第一个或最后一个元素的指针。\n\n`std::begin()` / `std::end()`的实际典型实现如下:\n\n```cpp\n    template<class C>\n    constexpr auto inline begin(C& c) -> decltype(c.begin())\n    {\n      return c.begin();\n    }\n    template<class C>\n    constexpr auto inline end(C& c) -> decltype(c.end())\n    {\n      return c.end();\n    }\n\n    template<class T, std::size_t N>\n    constexpr T* inline begin(T (&array)[N])\n    {\n      return array;\n    }\n\n    template<class T, std::size_t N>\n    constexpr T* inline begin(T (&array)[N])\n    {\n      return array+N;\n    }\n```\n\n可以为没有对应的`begin()` / `end()`成员但仍然可以迭代的容器提供自定义专门化。标准库实际上为`std::initializer_list`和`std::valarray`提供了这样的专门化。\n\nSpecializations must be defined in the same namespace where the original class or function template has been defined. Therefore, if you want to specialize any of the `std::begin()`/`std::end()` pairs you must do it in the `std` namespace.\n\nC++ 17 中引入的其他用于容器访问的非成员函数也有几个重载:\n\n*   `std::data()`有几个重载；对于类`C`它返回`c.data()`，对于数组它返回`array`，对于`std::initializer_list<T>`它返回`il.begin()`。\n\n```cpp\n        template <class C> \n        constexpr auto data(C& c) -> decltype(c.data())\n        {\n          return c.data();\n        }\n\n        template <class C> \n        constexpr auto data(const C& c) -> decltype(c.data())\n        {\n          return c.data();\n        }\n\n        template <class T, std::size_t N>\n        constexpr T* data(T (&array)[N]) noexcept\n        {\n          return array;\n        }\n\n        template <class E> \n        constexpr const E* data(std::initializer_list<E> il) noexcept\n        {\n          return il.begin();\n        }\n```\n\n*   `std::size()`有两个重载；对于类`C`它返回`c.size()`，对于数组它返回大小`N`。\n\n```cpp\n        template <class C> \n        constexpr auto size(const C& c) -> decltype(c.size())\n        {\n          return c.size();\n        }\n\n        template <class T, std::size_t N>\n        constexpr std::size_t size(const T (&array)[N]) noexcept\n        {\n          return N;\n        }\n```\n\n*   `std::empty()`有几个重载；对于类`C`它返回`c.empty()`，对于数组它返回`false`，对于`std::initializer_list<T>`它返回`il.size() == 0`。\n\n```cpp\n        template <class C> \n        constexpr auto empty(const C& c) -> decltype(c.empty())\n        {\n          return c.empty();\n        }\n\n        template <class T, std::size_t N> \n        constexpr bool empty(const T (&array)[N]) noexcept\n        {\n          return false;\n        }\n\n        template <class E> \n        constexpr bool empty(std::initializer_list<E> il) noexcept\n        {\n          return il.size() == 0;\n        }\n```\n\n# 还有更多...\n\n这些非成员函数主要用于容器未知的模板代码，可以是标准容器、C 类数组或自定义类型。使用这些函数的非成员版本使我们能够编写更简单、更少的代码来处理所有这些类型的容器。\n\n然而，这些函数的使用不仅限于泛型代码。虽然这是个人偏好的问题，但是保持一致并在代码中处处使用它们可能是一个好习惯。所有这些方法都有轻量级实现，这些实现很可能被编译器内联，这意味着使用相应的成员函数不会有任何开销。\n\n# 请参见\n\n*   *编写自己的随机访问迭代器*"
  },
  {
    "path": "docs/mod-cpp/12.md",
    "content": "# 十二、数学问题\n\n# 问题\n\n这是本章的解题部分。\n\n# 1.可被 3 和 5 整除的自然数之和\n\n编写一个程序，计算并打印所有可被 3 或 5 整除的自然数的和，直到用户输入的给定限制。\n\n# 2.最大公约数\n\n写一个程序，在给定两个正整数的情况下，计算并打印两者的最大公约数。\n\n# 3.最小公倍数\n\n写一个程序，在给定两个或更多正整数的情况下，计算并打印它们的最小公倍数。\n\n# 4.小于给定数的最大素数\n\n编写一个程序，计算并打印比用户提供的数字小的最大素数，该数字必须是正整数。\n\n# 5.性感的黄金搭档\n\n写一个程序，打印所有性感的黄金配对，直到用户输入的限制。\n\n# 6.大量的数字\n\n写一个程序，打印所有丰富的数字和他们的丰富，直到用户输入的数字。\n\n# 7.友好的数字\n\n编写一个程序，打印所有小于 1，000，000 的友好数字对的列表。\n\n# 8.阿姆斯特朗数字\n\n写一个程序，用三位数打印所有阿姆斯特朗的号码。\n\n# 9.一个数的质因数\n\n编写一个程序，打印用户输入的数字的质因数。\n\n# 10.格雷编码\n\n编写一个程序，显示所有 5 位数字的正常二进制表示、格雷码表示和解码格雷码值。\n\n# 11.将数值转换为罗马数字\n\n编写一个程序，给定用户输入的数字，打印它的罗马数字等价物。\n\n# 12.最大排序序列\n\n编写一个程序，确定并打印 100 万以内哪个数字产生最长的 Collatz 序列，以及它的长度是多少。\n\n# 13.圆周率的计算\n\n编写一个程序，计算精度为两位小数的π值。\n\n# 14.验证 ISBNs\n\n编写一个程序，验证用户以字符串形式输入的 10 位数值是否代表有效的 ISBN-10 数字。\n\n# 解决方法\n\n以下是上述问题解决部分的解决方案。\n\n# 1.可被 3 和 5 整除的自然数之和\n\n这个问题的解决方案是迭代从 3 (1 和 2 不能被 3 整除，所以测试它们没有意义)到用户输入的限制的所有数字。使用模运算检查数字除以 3 和 5 的其余部分是否为 0。然而，能够求和到更大极限的技巧是使用`long long`而不是`int`或`long`进行求和，这将导致在求和到 100，000 之前溢出:\n\n```cpp\nint main()\n{\n   unsigned int limit = 0;\n   std::cout << \"Upper limit:\";\n   std::cin >> limit;\n\n   unsigned long long sum = 0;\n   for (unsigned int i = 3; i < limit; ++ i)\n   {\n     if (i % 3 == 0 || i % 5 == 0)\n        sum += i;\n   }\n\n   std::cout << \"sum=\" << sum << std::endl;\n}\n```\n\n# 2.最大公约数\n\n两个或多个非零整数的最大公约数( *gcd* 简称)，也称为最大公约数( *gcf* )、最高公约数( *hcf* )、最大公约数( *gcm* )或最高公约数，是除以所有整数的最大正整数。有几种方法可以计算 gcd 一种有效的方法是欧几里德算法。对于两个整数，算法是:\n\n```cpp\ngcd(a,0) = a\ngcd(a,b) = gcd(b, a mod b)\n```\n\n这可以使用递归函数在 C++ 中非常简单地实现:\n\n```cpp\nunsigned int gcd(unsigned int const a, unsigned int const b)\n{\n   return b == 0 ? a : gcd(b, a % b);\n}\n```\n\n欧几里德算法的非递归实现应该如下所示:\n\n```cpp\nunsigned int gcd(unsigned int a, unsigned int b)\n{\n   while (b != 0) {\n      unsigned int r = a % b;\n      a = b;\n      b = r;\n   }\n   return a;\n}\n```\n\nIn C++ 17 there is a `constexpr` function called `gcd()` in the header `<numeric>` that computes the greatest common divisor of two numbers.\n\n# 3.最小公倍数\n\n两个或多个非零整数的**最小公倍数** ( **lcm** )也称为最小公倍数，是可被所有整数整除的最小正整数。计算最小公倍数的一种可能方法是将问题简化为计算最大公约数。在这种情况下使用以下公式:\n\n```cpp\nlcm(a, b) = abs(a, b) / gcd(a, b)\n```\n\n计算最小公倍数的函数可能如下所示:\n\n```cpp\nint lcm(int const a, int const b)\n{\n   int h = gcd(a, b);\n   return h ? (a * (b / h)) : 0;\n}\n```\n\n要计算两个以上整数的 *lcm* ，可以使用标题`<numeric>`中的`std::accumulate`算法:\n\n```cpp\ntemplate<class InputIt>\nint lcmr(InputIt first, InputIt last)\n{\n   return std::accumulate(first, last, 1, lcm);\n}\n```\n\nIn C++ 17 there is a `constexpr` function called `lcm()` in the header `<numeric>` that computes the least common multiple of two numbers.\n\n# 4.小于给定数的最大素数\n\n质数是只有两个除数的数，1 和数本身。要找到比给定数小的最大素数，你应该先写一个函数，确定一个数是否是素数，然后从给定数开始，向 1 方向调用这个函数，直到遇到第一个素数。有各种算法来确定一个数是否是质数。确定素性的常见实现如下:\n\n```cpp\nbool is_prime(int const num) \n{\n   if (num <= 3) { return num > 1; }\n   else if (num % 2 == 0 || num % 3 == 0) \n   { \n      return false; \n   }\n   else \n   {\n      for (int i = 5; i * i <= num; i += 6) \n      {\n         if (num % i == 0 || num % (i + 2) == 0) \n         {\n            return false;\n         }\n      }\n      return true;\n   }\n}\n```\n\n该功能可以如下使用:\n\n```cpp\nint main()\n{\n   int limit = 0;\n   std::cout << \"Upper limit:\";\n   std::cin >> limit;\n\n   for (int i = limit; i > 1; i--)\n   {\n      if (is_prime(i))\n      {\n         std::cout << \"Largest prime:\" << i << std::endl;\n         return 0;\n      }\n   }\n}\n```\n\n# 5.性感的黄金搭档\n\n性感质数是彼此相差 6 的质数(例如 5 和 11，或 13 和 19)。还有相差两个的*孪生素数*和相差四个的*表亲素数*。\n\n在前面的挑战中，我们实现了一个函数，该函数确定一个整数是否是素数。我们将在本练习中重用该函数。你要做的是检查如果一个数字`n`是质数，那么这个数字`n+6`也是质数，在这种情况下，把这一对打印到控制台上:\n\n```cpp\nint main()\n{\n   int limit = 0;\n   std::cout << \"Upper limit:\";\n   std::cin >> limit;\n\n   for (int n = 2; n <= limit; n++)\n   {\n      if (is_prime(n) && is_prime(n+6))\n      {\n         std::cout << n << \",\" << n+6 << std::endl;\n      }\n   }\n}\n```\n\n你可以把它作为计算和显示性感的三胞胎、四胞胎和五胞胎的进一步练习。\n\n# 6.大量的数字\n\n一个大数，也称为一个过度数，是指它的适当除数之和大于数本身的数。一个数的适当除数是这个数的正质因数，而不是这个数本身。适当因子之和超过数本身的量叫做丰度。例如，数字 12 有适当的除数 1、2、3、4 和 6。他们的总和是 16，这使 12 成为一个充裕的数字。它的丰度是 4(即 16 - 12)。\n\n为了确定适当除数的和，我们尝试从 2 到数的平方根的所有数(所有质因数都小于或等于这个值)。如果现在的数，我们称之为`i`，除以这个数，那么`i`和`num/i`都是除数。但是，如果它们相等(例如如果`i = 3`，和`n = 9`，那么`i`除 9，但是`n/i = 3`，我们只加`i`，因为适当的约数必须只加一次。否则，我们同时添加`i`和`num/i`并继续:\n\n```cpp\nint sum_proper_divisors(int const number)\n{\n   int result = 1;\n   for (int i = 2; i <= std::sqrt(number); i++)\n   {\n      if (number%i == 0)\n      {\n         result += (i == (number / i)) ? i : (i + number / i);\n      }\n   }\n   return result;\n}\n```\n\n打印大量的数字很简单，只需迭代到指定的极限，计算适当除数的总和，并将其与数字进行比较:\n\n```cpp\nvoid print_abundant(int const limit)\n{\n   for (int number = 10; number <= limit; ++ number)\n   {\n      auto sum = sum_proper_divisors(number);\n      if (sum > number)\n      {\n         std::cout << number << \", abundance=\" \n                   << sum - number << std::endl;\n      }\n   }\n}\n\nint main()\n{\n   int limit = 0;\n   std::cout << \"Upper limit:\";\n   std::cin >> limit;\n\n   print_abundant(limit);\n}\n```\n\n# 7.友好的数字\n\n如果一个数的适当除数之和等于另一个数的适当除数之和，则称两个数是友好的。一个数的适当除数是该数的正质因数，而不是该数本身。友好号码不应与*友好号码*混淆。例如，数字 220 有适当的除数 1、2、4、5、10、11、20、22、44、55 和 110，它们的和是 284。284 的适当除数是 1、2、4、71 和 142；他们的总数是 220。因此，数字 220 和 284 被认为是友好的。\n\n这个问题的解决方案是迭代所有的数字，直到给定的极限。对于每个数，计算其适当除数的和。姑且称之为`sum1`。重复该过程，计算`sum1`的适当除数之和。如果结果等于原始数字，则数字和`sum1`是友好数字:\n\n```cpp\nvoid print_amicables(int const limit)\n{\n   for (int number = 4; number < limit; ++ number)\n   {\n      auto sum1 = sum_proper_divisors(number);\n      if (sum1 < limit)\n      {\n         auto sum2 = sum_proper_divisors(sum1);\n         if (sum2 == number && number != sum1)\n         {\n            std::cout << number << \",\" << sum1 << std::endl;\n         }\n      }\n   }\n}\n```\n\n在上面的例子中，`sum_proper_divisors()`是在大数问题的解中看到的函数。\n\nThe above function prints pairs of numbers twice, such as 220,284 and 284,220\\. Modify this implementation to only print each pair a single time.\n\n# 8.阿姆斯特朗数字\n\n阿姆斯特朗数(以迈克尔·f·阿姆斯特朗的名字命名)，也称为自恋数、pluperfect 数字不变量或 pluperfect 数，是一个数，当它的位数增加到位数的幂时，它等于它自己的位数之和。举个例子，最小的阿姆斯特朗数是 153，等于![](img/8a736b24-c3af-4da2-a9da-12789af4ee9e.png)。\n\n要确定一个三位数的数字是否是自恋数字，你必须首先确定它的位数，以便对它们的幂求和。然而，这涉及除法和模运算，这是昂贵的。一种更快的计算方法是依赖于这样一个事实，即一个数是数字的总和乘以 10 的幂等于它们从零开始的位置。换句话说，对于 1000 以下的数字，我们有`a*10^2 + b*10^2 + c`。因为你只需要确定三位数的数字，这意味着`a`将从 1 开始。这将比其他方法更快，因为乘法的计算速度比除法和模运算更快。这种函数的实现如下所示:\n\n```cpp\nvoid print_narcissistics()\n{\n   for (int a = 1; a <= 9; a++)\n   {\n      for (int b = 0; b <= 9; b++)\n      {\n         for (int c = 0; c <= 9; c++)\n         {\n            auto abc = a * 100 + b * 10 + c;\n            auto arm = a * a * a + b * b * b + c * c * c;\n            if (abc == arm)\n            {\n               std::cout << arm << std::endl;\n            }\n         }\n      }\n   }\n}\n```\n\n你可以把它作为一个进一步的练习，写一个函数来决定自恋数字的上限，不管它们的位数是多少。这样的函数会比较慢，因为你首先必须确定数字的数字序列，将它们存储在一个容器中，然后将数字加到适当的幂(数字的数量)上。\n\n# 9.一个数的质因数\n\n正整数的质因数是将该整数整除的素数。例如，8 的质因数是 2×2×2，42 的质因数是 2×3×7。要确定主要因素，您应该使用以下算法:\n\n1.  而`n`可以被 2 整除，2 是质因数，必须加入列表，而`n`则成为`n/2`的结果。完成此步骤后，`n`为奇数。\n2.  从 3 迭代到`n`的平方根。而目前的数字，姑且称之为`i`，除`n`，`i`是质因数，必须加进榜单，而`n`则成为`n/i`的结果。当`i`不再除`n`时，将`i`增加 2(得到下一个奇数)。\n3.  当`n`是大于 2 的素数时，上述步骤不会导致`n`变为 1。因此，如果在第 2 步结束时`n`仍然大于 2，那么`n`就是一个主要因素。\n\n```cpp\nstd::vector<unsigned long long> prime_factors(unsigned long long n)\n{\n   std::vector<unsigned long long> factors;\n   while (n % 2 == 0) {\n      factors.push_back(2);\n      n = n / 2;\n   }\n   for (unsigned long long i = 3; i <= std::sqrt(n); i += 2)\n   {\n      while (n%i == 0) {\n         factors.push_back(i);\n         n = n / i;\n      }\n   }\n\n   if (n > 2) \n      factors.push_back(n);\n   return factors;\n}\n\nint main()\n{\n   unsigned long long number = 0;\n   std::cout << \"number:\";\n   std::cin >> number;\n\n   auto factors = prime_factors(number);\n   std::copy(std::begin(factors), std::end(factors),\n        std::ostream_iterator<unsigned long long>(std::cout, \" \"));\n}\n```\n\n作为进一步的练习，确定数字 600，851，475，143 的最大质因数。\n\n# 10.格雷编码\n\n格雷码，也称为反射二进制码或简称反射二进制码，是二进制编码的一种形式，其中两个连续的数字仅相差一位。要执行二进制反射格雷码编码，我们需要使用以下公式:\n\n```cpp\nif b[i-1] = 1 then g[i] = not b[i]\nelse g[i] = b[i]\n```\n\n这相当于以下内容:\n\n```cpp\ng = b xor (b logically right shifted 1 time)\n```\n\n对于解码二进制反射格雷码，应使用以下公式:\n\n```cpp\nb[0] = g[0]\nb[i] = g[i] xor b[i-1]\n```\n\n对于 32 位无符号整数，可以用 C++ 编写如下:\n\n```cpp\nunsigned int gray_encode(unsigned int const num)\n{\n   return num ^ (num >> 1);\n}\n\nunsigned int gray_decode(unsigned int gray)\n{\n   for (unsigned int bit = 1U << 31; bit > 1; bit >>= 1)\n   {\n      if (gray & bit) gray ^= bit >> 1;\n   }\n   return gray;\n}\n```\n\n要打印所有 5 位整数、它们的二进制表示、编码的格雷码表示和解码值，我们可以使用以下代码:\n\n```cpp\nstd::string to_binary(unsigned int value, int const digits)\n{\n   return std::bitset<32>(value).to_string().substr(32-digits, digits);\n}\n\nint main()\n{\n   std::cout << \"Number\\tBinary\\tGray\\tDecoded\\n\";\n   std::cout << \"------\\t------\\t----\\t-------\\n\";\n\n   for (unsigned int n = 0; n < 32; ++ n)\n   {\n      auto encg = gray_encode(n);\n      auto decg = gray_decode(encg);\n\n      std::cout \n         << n << \"\\t\" << to_binary(n, 5) << \"\\t\" \n         << to_binary(encg, 5) << \"\\t\" << decg << \"\\n\";\n   }\n}\n```\n\n# 11.将数值转换为罗马数字\n\n今天已知的罗马数字使用七个符号:I = 1，V = 5，X = 10，L = 50，C = 100，D = 500，M = 1000。系统在组成数字符号时使用加法和减法。从 1 到 10 的符号是 I、II、III、IV、V、VI、VII、VIII、IX 和 x。罗马人没有零的符号，他们用写 *nulla* 来表示它。在这个系统中，最大的符号在左边，最不重要的符号在右边。例如，1994 的罗马数字是 MCMXCIV。如果你不熟悉罗马数字的规则，你应该在网上多看看。\n\n要确定数字的罗马数字，请使用以下算法:\n\n1.  从最高(M)到最低(I)检查每个罗马基本符号\n2.  如果当前值大于符号的值，则将符号连接到罗马数字，并从当前值中减去它的值\n3.  重复，直到当前值为零\n\n例如，考虑 42:小于 42 的第一个罗马基本符号是 XL，即 40。我们将其连接到数字上，得到 XL，然后从当前数字中减去，得到 2。第一个小于 2 的罗马基符号是 I，也就是 1。我们把它加到数字上，得到 XLI，然后从数字中减去 1，得到 1。我们在数字上再加一个 I，变成 XLII，再从数字中减去 1，达到 0，因此停止:\n\n```cpp\nstd::string to_roman(unsigned int value)\n{\n   std::vector<std::pair<unsigned int, char const*>> roman {\n      { 1000, \"M\" },{ 900, \"CM\" }, { 500, \"D\" },{ 400, \"CD\" }, \n      { 100, \"C\" },{ 90, \"XC\" }, { 50, \"L\" },{ 40, \"XL\" },\n      { 10, \"X\" },{ 9, \"IX\" }, { 5, \"V\" },{ 4, \"IV\" }, { 1, \"I\" }};\n\n   std::string result;\n   for (auto const & kvp : roman) {\n      while (value >= kvp.first) {\n         result += kvp.second;\n         value -= kvp.first;\n      }\n   }\n   return result;\n}\n```\n\n该功能可以如下使用:\n\n```cpp\nint main()\n{\n   for(int i = 1; i <= 100; ++ i) \n   {\n      std::cout << i << \"\\t\" << to_roman(i) << std::endl; \n   }\n\n   int number = 0;\n   std::cout << \"number:\";\n   std::cin >> number;\n   std::cout << to_roman(number) << std::endl;\n}\n```\n\n# 12.最大排序序列\n\n柯拉茨猜想，也称为乌兰猜想、卡库塔尼问题、思韦特猜想、哈塞算法或锡拉丘兹问题，是一个未经证实的猜想，它指出如下所述定义的序列总是达到 1。级数定义如下:从任意正整数`n`开始，从上一项得到每个新项:如果上一项为偶数，则下一项为上一项的一半，否则为上一项的 3 倍加 1。\n\n你要解决的问题是为所有 100 万以内的正整数生成 Collatz 序列，确定其中最长的一个，并打印它的长度和产生它的起始数。虽然我们可以应用蛮力为每个数字生成序列，并计算项数直到达到 1，但更快的解决方案是保存已经生成的所有序列的长度。当从值`n`开始的序列的当前项变得小于`n`时，那么它是一个已经确定了序列的数字，所以我们可以简单地获取它的缓存长度，并将其添加到当前长度中，以确定从`n`开始的序列的长度。然而，这种方法对可以计算的 Collatz 序列引入了限制，因为在某个时刻，缓存将超过系统可以分配的内存量:\n\n```cpp\nstd::pair<unsigned long long, long> longest_collatz(\n   unsigned long long const limit)\n{\n   long length = 0;\n   unsigned long long number = 0;\n   std::vector<int> cache(limit + 1, 0);\n\n   for (unsigned long long i = 2; i <= limit; i++) \n   {\n      auto n = i;\n      long steps = 0;\n      while (n != 1 && n >= i) \n      {\n         if ((n % 2) == 0) n = n / 2;\n         else n = n * 3 + 1;\n         steps++ ;\n      }\n      cache[i] = steps + cache[n];\n\n      if (cache[i] > length) \n      {\n         length = cache[i];\n         number = i;\n```\n\n```cpp\n      }\n   }\n\n   return std::make_pair(number, length);\n}\n```\n\n# 13.圆周率的计算\n\n近似确定π值的一个合适的解决方案是使用蒙特卡罗模拟。这是一种使用随机输入样本来探索复杂过程或系统行为的方法。该方法被用于各种各样的应用和领域，包括物理、工程、计算、金融、商业和其他领域。\n\n要做到这一点，我们将依靠以下想法:直径为`d`的圆的面积为`PI * d^2 / 4`。边长等于`d`的正方形面积是`d^2`。如果我们把两者分开，我们会得到`PI/4`。如果我们把圆放在正方形里面，生成在正方形里面均匀分布的随机数，那么圆里面的个数应该和圆的面积成正比，正方形里面的个数应该和正方形的面积成正比。这意味着将方块和圆圈中的点击总数相除应该会给出`PI/4`。生成的点越多，结果应该越准确。\n\n为了生成伪随机数，我们将使用默森扭转器和均匀的统计分布:\n\n```cpp\ntemplate <typename E = std::mt19937, \n          typename D = std::uniform_real_distribution<>>\ndouble compute_pi(E& engine, D& dist, int const samples = 1000000)\n{\n   auto hit = 0;\n   for (auto i = 0; i < samples; i++)\n   {\n      auto x = dist(engine);\n      auto y = dist(engine);\n      if (y <= std::sqrt(1 - std::pow(x, 2))) hit += 1;\n   }\n   return 4.0 * hit / samples;\n}\n\nint main()\n{\n   std::random_device rd;\n   auto seed_data = std::array<int, std::mt19937::state_size> {};\n   std::generate(std::begin(seed_data), std::end(seed_data), \n                 std::ref(rd));\n   std::seed_seq seq(std::begin(seed_data), std::end(seed_data));\n   auto eng = std::mt19937{ seq };\n   auto dist = std::uniform_real_distribution<>{ 0, 1 };\n\n   for (auto j = 0; j < 10; j++)\n      std::cout << compute_pi(eng, dist) << std::endl;\n}\n```\n\n# 14.验证 ISBNs\n\n**国际标准书号** ( **国际标准书号**)是图书的唯一数字标识符。目前，使用 13 位格式。但是，对于这个问题，您需要验证使用 10 位数字的前一种格式。10 位中的最后一位是校验和。选择该数字是为了使所有十个数字的总和(每个数字乘以其(整数)权重，从 10 到 1 递减)是 11 的倍数。\n\n`validate_isbn_10`函数，如下所示，将一个 ISBN 作为一个字符串，如果字符串的长度为 10，所有十个元素都是数字，并且所有数字的总和乘以它们的权重(或位置)是 11 的倍数，则返回`true`:\n\n```cpp\nbool validate_isbn_10(std::string_view isbn)\n{\n   auto valid = false;\n   if (isbn.size() == 10 &&\n       std::count_if(std::begin(isbn), std::end(isbn), isdigit) == 10)\n   {\n      auto w = 10;\n      auto sum = std::accumulate(\n         std::begin(isbn), std::end(isbn), 0,\n         [&w](int const total, char const c) {\n            return total + w-- * (c - '0'); });\n\n     valid = !(sum % 11);\n   }\n   return valid;\n}\n```\n\nYou can take it as a further exercise to improve this function to also correctly validate ISBN-10 numbers that include hyphens, such as `3-16-148410-0`. Also, you can write a function that validates ISBN-13 numbers."
  },
  {
    "path": "docs/mod-cpp/13.md",
    "content": "# 十三、语言特性\n\n# 问题\n\n这是本章的问题解决部分。\n\n# 15.IPv4 数据类型\n\n编写一个表示 IPv4 地址的类。实现能够从控制台读写这些地址所需的功能。用户应该能够以虚线形式输入值，如`127.0.0.1`或`168.192.0.100`。这也是 IPv4 地址应该格式化为输出流的形式。\n\n# 16.枚举范围内的 IPv4 地址\n\n编写一个程序，允许用户输入代表一个范围的两个 IPv4 地址，并列出该范围内的所有地址。扩展为前面的问题定义的结构，以实现请求的功能。\n\n# 17.使用基本操作创建 2D 数组\n\n编写一个表示二维数组容器的类模板，其中包含元素访问(`at()`和`data()`)、容量查询、迭代器、填充和交换的方法。应该可以移动这种类型的对象。\n\n# 18.具有任意数量参数的最小函数\n\n编写一个函数模板，可以取任意数量的参数，并返回所有参数的最小值，使用`operator <`进行比较。写一个这个函数模板的变体，可以用二进制比较函数参数化来代替`operator <`使用。\n\n# 19.向容器添加一系列值\n\n编写一个通用函数，可以在有方法`push_back(T&& value)`的容器末尾添加任意数量的元素。\n\n# 20.容器任意、全部、无\n\n编写一组通用函数，用于检查给定容器中是否存在任何、所有或没有指定的参数。这些函数应该可以编写如下代码:\n\n```cpp\nstd::vector<int> v{ 1, 2, 3, 4, 5, 6 };\nassert(contains_any(v, 0, 3, 30));\n\nstd::array<int, 6> a{ { 1, 2, 3, 4, 5, 6 } };\nassert(contains_all(a, 1, 3, 5, 6));\n\nstd::list<int> l{ 1, 2, 3, 4, 5, 6 };\nassert(!contains_none(l, 0, 6));\n```\n\n# 21.系统句柄包装\n\n考虑一个操作系统句柄，如文件句柄。编写一个包装器来处理句柄的获取和释放，以及其他操作，例如验证句柄的有效性和将句柄所有权从一个对象转移到另一个对象。\n\n# 22.各种温标的文字\n\n写一个小的库，可以用三种最常用的刻度摄氏、华氏和开尔文来表示温度，并在它们之间转换。该库必须使您能够在所有这些刻度中写入温度文字，例如`36.5_deg`代表摄氏度，`97.7_f`代表华氏，`309.65_K`代表开尔文；使用这些值执行操作；并在它们之间转换。\n\n# 解决方法\n\n以下是上述问题解决部分的解决方案。\n\n# 15.IPv4 数据类型\n\n这个问题需要编写一个类来表示 IPv4 地址。这是一个 32 位的值，通常用十进制的点分格式表示，如`168.192.0.100`*；*它的每个部分都是一个 8 位值，范围从 0 到 255。为了便于表示和处理，我们可以使用四个`unsigned char`来存储地址值。这样的值可以由四个 T2 或一个 T3 构成。为了能够直接从控制台(或任何其他输入流)读取一个值，并且能够将该值写入控制台(或任何其他输出流)，我们必须重载`operator>>`和`operator<<`。下面的清单显示了可以满足所请求功能的最小实现:\n\n```cpp\nclass ipv4\n{\n   std::array<unsigned char, 4> data;\npublic:\n   constexpr ipv4() : data{ {0} } {}\n   constexpr ipv4(unsigned char const a, unsigned char const b, \n                  unsigned char const c, unsigned char const d):\n      data{{a,b,c,d}} {}\n   explicit constexpr ipv4(unsigned long a) :\n      data{ { static_cast<unsigned char>((a >> 24) & 0xFF), \n              static_cast<unsigned char>((a >> 16) & 0xFF),\n              static_cast<unsigned char>((a >> 8) & 0xFF),\n              static_cast<unsigned char>(a & 0xFF) } } {}\n   ipv4(ipv4 const & other) noexcept : data(other.data) {}\n   ipv4& operator=(ipv4 const & other) noexcept \n   {\n      data = other.data;\n      return *this;\n   }\n\n   std::string to_string() const\n   {\n      std::stringstream sstr;\n      sstr << *this;\n      return sstr.str();\n   }\n\n   constexpr unsigned long to_ulong() const noexcept\n   {\n      return (static_cast<unsigned long>(data[0]) << 24) |\n             (static_cast<unsigned long>(data[1]) << 16) |\n             (static_cast<unsigned long>(data[2]) << 8) |\n              static_cast<unsigned long>(data[3]);\n   }\n\n   friend std::ostream& operator<<(std::ostream& os, const ipv4& a)\n   {\n      os << static_cast<int>(a.data[0]) << '.' \n         << static_cast<int>(a.data[1]) << '.'\n         << static_cast<int>(a.data[2]) << '.'\n         << static_cast<int>(a.data[3]);\n      return os;\n   }\n\n   friend std::istream& operator>>(std::istream& is, ipv4& a)\n   {\n      char d1, d2, d3;\n      int b1, b2, b3, b4;\n      is >> b1 >> d1 >> b2 >> d2 >> b3 >> d3 >> b4;\n      if (d1 == '.' && d2 == '.' && d3 == '.')\n         a = ipv4(b1, b2, b3, b4);\n      else\n         is.setstate(std::ios_base::failbit);\n      return is;\n   }\n};\n```\n\n`ipv4`类可以如下使用:\n\n```cpp\nint main()\n{\n   ipv4 address(168, 192, 0, 1);\n   std::cout << address << std::endl;\n\n   ipv4 ip;\n   std::cout << ip << std::endl;\n   std::cin >> ip;\n   if(!std::cin.fail())\n      std::cout << ip << std::endl;\n}\n```\n\n# 16.枚举范围内的 IPv4 地址\n\n为了能够枚举给定范围内的 IPv4 地址，首先应该能够比较 IPv4 值。所以我们至少要实现`operator<`，但是下面的列表包含了所有比较运算符的实现:`==`、`!=`、`<`、`>`、`<=`和`>=`。此外，为了增加 IPv4 值，提供了前缀和后缀`operator++ `的实现。以下代码是上一个问题的 IPv4 类的扩展:\n\n```cpp\nipv4& operator++()\n{\n   *this = ipv4(1 + to_ulong());\n   return *this;\n}\n\nipv4& operator++(int)\n{\n   ipv4 result(*this);\n   ++(*this);\n   return *this;\n}\n\nfriend bool operator==(ipv4 const & a1, ipv4 const & a2) noexcept\n{\n   return a1.data == a2.data;\n}\n\nfriend bool operator!=(ipv4 const & a1, ipv4 const & a2) noexcept\n{\n   return !(a1 == a2);\n}\n\nfriend bool operator<(ipv4 const & a1, ipv4 const & a2) noexcept\n{\n   return a1.to_ulong() < a2.to_ulong();\n}\n\nfriend bool operator>(ipv4 const & a1, ipv4 const & a2) noexcept\n{\n   return a2 < a1;\n}\n\nfriend bool operator<=(ipv4 const & a1, ipv4 const & a2) noexcept\n{\n   return !(a1 > a2);\n}\n\nfriend bool operator>=(ipv4 const & a1, ipv4 const & a2) noexcept\n{\n   return !(a1 < a2);\n}\n```\n\n通过对前一个问题中的`ipv4`类进行这些更改，我们可以编写以下程序:\n\n```cpp\nint main()\n{\n   std::cout << \"input range: \";\n   ipv4 a1, a2;\n   std::cin >> a1 >> a2;\n   if (a2 > a1)\n   {\n      for (ipv4 a = a1; a <= a2; a++)\n      {\n         std::cout << a << std::endl;\n      }\n   }\n   else \n   {\n      std::cerr << \"invalid range!\" << std::endl;\n   }\n}\n```\n\n# 17.使用基本操作创建 2D 数组\n\n在考虑如何定义这样的结构之前，让我们考虑几个测试用例。以下代码片段显示了请求的所有功能:\n\n```cpp\nint main()\n{\n   // element access\n   array2d<int, 2, 3> a {1, 2, 3, 4, 5, 6};\n   for (size_t i = 0; i < a.size(1); ++ i)\n      for (size_t j = 0; j < a.size(2); ++ j)\n      a(i, j) *= 2;\n\n   // iterating\n   std::copy(std::begin(a), std::end(a), \n      std::ostream_iterator<int>(std::cout, \" \"));\n\n   // filling \n   array2d<int, 2, 3> b;\n   b.fill(1);\n\n   // swapping\n   a.swap(b);\n\n   // moving\n   array2d<int, 2, 3> c(std::move(b));\n}\n```\n\n注意，对于元素访问，我们使用的是`operator()`，比如在`a(i,j)`中，而不是`operator[]`，比如在`a[i][j]`中，因为只有前者可以接受多个参数(每个维度上的索引一个)。后者只能有一个参数，为了启用像`a[i][j]`这样的表达式，它必须返回一个中间类型(一个基本上代表一行的类型)，该中间类型反过来重载`operator[]`以返回单个元素。\n\n已经有存储固定或可变长度元素序列的标准容器。这个二维数组类应该只是这样一个容器的适配器。在`std::array`和`std::vector`之间进行选择时，我们应该考虑两件事:\n\n*   `array2d`类应该具有移动语义，以便能够移动对象\n*   应该可以列出初始化这种类型的对象\n\n`std::array`容器只有在它容纳的元素是可移动构造和可移动分配的情况下才是可移动的。另一方面，它不能由`std::initializer_list`构成。因此，更可行的选择仍然是`std::vector`。\n\n在内部，这个适配器容器可以将其数据存储在向量的向量中(每行是一个带有`C`元素的`vector<T>`，而 2D 数组的`R`这样的元素存储在一个`vector<vector<T>>`中)或者类型为`T`的`R![](img/2f9ae4c1-380b-4377-84dd-a28429c062c5.png)C`元素的单个向量中。在后一种情况下，第`i`行和第`j`列的元素位于索引`i * C + j`处。这种方法占用的内存更少，将所有数据存储在一个连续的块中，并且实现起来也更简单。由于这些原因，它是首选的解决方案。\n\n具有所请求功能的二维数组类的可能实现如下所示:\n\n```cpp\ntemplate <class T, size_t R, size_t C>\nclass array2d\n{\n   typedef T                 value_type;\n   typedef value_type*       iterator;\n   typedef value_type const* const_iterator;\n   std::vector<T>            arr;\npublic:\n   array2d() : arr(R*C) {}\n   explicit array2d(std::initializer_list<T> l):arr(l) {}\n   constexpr T* data() noexcept { return arr.data(); }\n   constexpr T const * data() const noexcept { return arr.data(); }\n\n   constexpr T& at(size_t const r, size_t const c) \n   {\n      return arr.at(r*C + c);\n   }\n\n   constexpr T const & at(size_t const r, size_t const c) const\n   {\n      return arr.at(r*C + c);\n   }\n\n   constexpr T& operator() (size_t const r, size_t const c)\n   {\n      return arr[r*C + c];\n   }\n\n   constexpr T const & operator() (size_t const r, size_t const c) const\n   {\n      return arr[r*C + c];\n   }\n\n   constexpr bool empty() const noexcept { return R == 0 || C == 0; }\n\n   constexpr size_t size(int const rank) const\n   {\n      if (rank == 1) return R;\n      else if (rank == 2) return C;\n      throw std::out_of_range(\"Rank is out of range!\");\n   }\n\n   void fill(T const & value)\n   {\n      std::fill(std::begin(arr), std::end(arr), value);\n   }\n\n   void swap(array2d & other) noexcept { arr.swap(other.arr); }\n\n   const_iterator begin() const { return arr.data(); }\n   const_iterator end() const   { return arr.data() + arr.size(); }\n   iterator       begin()       { return arr.data(); }\n   iterator       end()         { return arr.data() + arr.size(); }\n};\n```\n\n# 18.具有任意数量参数的最小函数\n\n可以使用可变函数模板编写可以接受可变数量参数的函数模板。为此，我们需要实现编译时递归(实际上只是通过一组重载函数的调用)。下面的代码片段显示了如何实现请求的函数:\n\n```cpp\ntemplate <typename T>\nT minimum(T const a, T const b) { return a < b ? a : b; }\n\ntemplate <typename T1, typename... T>\nT1 minimum(T1 a, T... args)\n{\n   return minimum(a, minimum(args...));\n}\n\nint main()\n{\n   auto x = minimum(5, 4, 2, 3);\n}\n```\n\n为了能够使用用户提供的二进制比较函数，我们需要编写另一个函数模板。比较函数必须是第一个参数，因为它不能跟随函数参数包。另一方面，这不能是先前最小函数的重载，而是一个具有不同名称的函数。原因是编译器无法区分模板参数列表`<typename T1, typename... T>`和`<class Compare, typename T1, typename... T>`。改动很小，在这个片段中应该很容易理解:\n\n```cpp\ntemplate <class Compare, typename T>\nT minimumc(Compare comp, T const a, T const b) \n{ return comp(a, b) ? a : b; }\n\ntemplate <class Compare, typename T1, typename... T>\nT1 minimumc(Compare comp, T1 a, T... args)\n{\n   return minimumc(comp, a, minimumc(comp, args...));\n}\n\nint main()\n{\n   auto y = minimumc(std::less<>(), 3, 2, 1, 0);\n}\n```\n\n# 19.向容器添加一系列值\n\n使用变量函数模板可以编写具有任意数量参数的函数。该函数应该将容器作为第一个参数，后跟一个可变数量的参数，这些参数表示要添加到容器后面的值。然而，使用 fold 表达式可以大大简化编写这样的函数模板。这里显示了这样一个实现:\n\n```cpp\ntemplate<typename C, typename... Args>\nvoid push_back(C& c, Args&&... args)\n{\n   (c.push_back(args), ...);\n}\n```\n\n在下面的列表中可以看到使用这个函数模板和各种容器类型的例子:\n\n```cpp\nint main()\n{\n   std::vector<int> v;\n   push_back(v, 1, 2, 3, 4);\n   std::copy(std::begin(v), std::end(v), \n             std::ostream_iterator<int>(std::cout, \" \"));\n\n   std::list<int> l;\n   push_back(l, 1, 2, 3, 4);\n   std::copy(std::begin(l), std::end(l), \n             std::ostream_iterator<int>(std::cout, \" \"));\n}\n```\n\n# 20.容器任意、全部、无\n\n能够检查可变数量参数存在与否的要求表明，我们应该编写可变函数模板。然而，这些函数需要一个助手函数，一个检查容器中是否找到元素并返回一个`bool`来指示成功或失败的通用函数。由于所有这些我们可以称为`contains_all`、`contains_any`和`contains_none`的函数都是对辅助函数返回的结果应用逻辑运算符，因此我们将使用 fold 表达式来简化代码。在折叠表达式扩展后，短路评估被启用，这意味着我们只评估导致确定结果的元素。因此，如果我们正在寻找所有 1、2 和 3 的存在，并且缺少 2，则函数将在容器中查找值 2 后返回，而不检查值 3:\n\n```cpp\ntemplate<class C, class T>\nbool contains(C const & c, T const & value)\n{\n   return std::end(c) != std::find(std::begin(c), std::end(c), value);\n}\n\ntemplate<class C, class... T>\nbool contains_any(C const & c, T &&... value)\n{\n   return (... || contains(c, value));\n}\n\ntemplate<class C, class... T>\nbool contains_all(C const & c, T &&... value)\n{\n   return (... && contains(c, value));\n}\n\ntemplate<class C, class... T>\nbool contains_none(C const & c, T &&... value)\n{\n   return !contains_any(c, std::forward<T>(value)...);\n}\n```\n\n# 21.系统句柄包装\n\n系统句柄是对系统资源的一种引用形式。因为所有的操作系统至少最初都是用 C 语言编写的，所以句柄的创建和释放是通过专用的系统函数来完成的。这增加了因错误处置(如在例外情况下)而导致资源泄漏的风险。在下面的代码片段中，针对 Windows，您可以看到一个打开、读取并最终关闭文件的函数。然而，这有两个问题:在一种情况下，开发人员在离开函数之前忘记关闭句柄；在另一种情况下，在句柄被正确关闭之前调用抛出的函数，而不会捕获异常。但是，由于函数抛出，清理代码永远不会执行:\n\n```cpp\nvoid bad_handle_example()\n{\n   bool condition1 = false;\n   bool condition2 = true;\n   HANDLE handle = CreateFile(L\"sample.txt\",\n                              GENERIC_READ,\n                              FILE_SHARE_READ,\n                              nullptr,\n                              OPEN_EXISTING,\n                              FILE_ATTRIBUTE_NORMAL,\n                              nullptr);\n\n   if (handle == INVALID_HANDLE_VALUE)\n      return;\n\n   if (condition1)\n   {\n      CloseHandle(handle);\n      return;\n   }\n\n   std::vector<char> buffer(1024);\n   unsigned long bytesRead = 0;\n   ReadFile(handle, \n            buffer.data(), \n            buffer.size(), \n            &bytesRead, \n            nullptr);\n\n   if (condition2)\n   {\n      // oops, forgot to close handle\n      return;\n   }\n\n   // throws exception; the next line will not execute\n   function_that_throws();\n\n   CloseHandle(handle);\n}\n```\n\nC++ 包装器类可以确保在包装器对象超出范围并被销毁时(无论是通过正常执行路径还是作为异常的结果)正确处置句柄。正确的实现应该考虑不同类型的句柄，用一定范围的值来指示无效的句柄(如 0/null 或-1)。下面显示的实现提供了:\n\n*   对象被破坏时句柄的显式获取和自动释放\n*   移动语义以实现句柄所有权的转移\n*   比较运算符检查两个对象是否引用同一个句柄\n*   交换和重置等附加操作\n\nThe implementation shown here is a modified version of the handle class implemented by Kenny Kerr and published in the article *Windows with C++ - C++ and the Windows API*, MSDN Magazine, July 2011, [https://msdn.microsoft.com/en-us/magazine/hh288076.aspx](https://msdn.microsoft.com/en-us/magazine/hh288076.aspx). Although the handle traits shown here refer to Windows handles, it should be fairly simple to write traits appropriate for other platforms.\n\n```cpp\ntemplate <typename Traits>\nclass unique_handle\n{\n   using pointer = typename Traits::pointer;\n   pointer m_value;\npublic:\n   unique_handle(unique_handle const &) = delete;\n   unique_handle& operator=(unique_handle const &) = delete;\n\n   explicit unique_handle(pointer value = Traits::invalid()) noexcept\n      :m_value{ value }\n   {}\n\n   unique_handle(unique_handle && other) noexcept\n      : m_value{ other.release() }\n   {}\n\n   unique_handle& operator=(unique_handle && other) noexcept\n   {\n      if (this != &other)\n         reset(other.release());\n      return *this;\n   }\n\n   ~unique_handle() noexcept\n   {\n      Traits::close(m_value);\n   }\n\n   explicit operator bool() const noexcept\n   {\n      return m_value != Traits::invalid();\n   }\n\n   pointer get() const noexcept { return m_value; }\n\n   pointer release() noexcept\n   {\n      auto value = m_value;\n      m_value = Traits::invalid();\n      return value;\n   }\n\n   bool reset(pointer value = Traits::invalid()) noexcept\n   {\n      if (m_value != value)\n      {\n         Traits::close(m_value);\n         m_value = value;\n      }\n      return static_cast<bool>(*this);\n   }\n\n   void swap(unique_handle<Traits> & other) noexcept\n   {\n      std::swap(m_value, other.m_value);\n   }\n};\n\ntemplate <typename Traits>\nvoid swap(unique_handle<Traits> & left, unique_handle<Traits> & right) noexcept\n{\n   left.swap(right);\n}\n\ntemplate <typename Traits>\nbool operator==(unique_handle<Traits> const & left,\n                unique_handle<Traits> const & right) noexcept\n{\n   return left.get() == right.get();\n}\n\ntemplate <typename Traits>\nbool operator!=(unique_handle<Traits> const & left,\n                unique_handle<Traits> const & right) noexcept\n{\n   return left.get() != right.get();\n}\n\nstruct null_handle_traits\n{\n   using pointer = HANDLE;\n   static pointer invalid() noexcept { return nullptr; }\n   static void close(pointer value) noexcept\n   {\n      CloseHandle(value);\n   }\n};\n\nstruct invalid_handle_traits\n{\n   using pointer = HANDLE;\n   static pointer invalid() noexcept { return INVALID_HANDLE_VALUE; }\n   static void close(pointer value) noexcept\n   {\n      CloseHandle(value);\n   }\n};\n\nusing null_handle = unique_handle<null_handle_traits>;\nusing invalid_handle = unique_handle<invalid_handle_traits>;\n```\n\n定义了这个句柄类型后，我们可以用更简单的术语重写前面的例子，避免所有那些由于异常发生而没有正确关闭句柄的问题，或者仅仅因为开发人员在不再需要时忘记释放资源。这段代码更简单、更健壮:\n\n```cpp\nvoid good_handle_example()\n{\n   bool condition1 = false;\n   bool condition2 = true;\n\n   invalid_handle handle{\n      CreateFile(L\"sample.txt\",\n                 GENERIC_READ,\n                 FILE_SHARE_READ,\n                 nullptr,\n                 OPEN_EXISTING,\n                 FILE_ATTRIBUTE_NORMAL,\n                 nullptr) };\n\n   if (!handle) return;\n\n   if (condition1) return;\n\n   std::vector<char> buffer(1024);\n   unsigned long bytesRead = 0;\n   ReadFile(handle.get(),\n            buffer.data(),\n            buffer.size(),\n            &bytesRead,\n            nullptr);\n\n   if (condition2) return;\n\n   function_that_throws();\n}\n```\n\n# 22.各种温标的文字\n\n为了满足这一要求，我们需要为几种类型、运算符和函数提供一个实现:\n\n*   支持的温标计数称为`scale`。\n*   表示温度值的类模板，用刻度参数化，称为`quantity`。\n*   比较运算符`==`、`!=`、`<`、`>`、`<=`和`>=`，它们比较同一时间的两个量。\n*   加减相同数量类型值的算术运算符`+`和`-`。另外，我们可以实现成员运营商`+=`和`-+`。\n*   将温度从一个刻度转换到另一个刻度的函数模板，称为`temperature_cast`。这个函数本身不执行转换，而是使用类型特征来完成转换。\n*   文字运算符`\"\"_deg`、`\"\"_f`和`\"\"_k`，用于创建用户定义的温度文字。\n\nFor brevity, the following snippet only contains the code that handles Celsius and Fahrenheit temperatures. You should take it as a further exercise to extend the code with support for the Kelvin scale. The code accompanying the book contains the full implementation of all three required scales.\n\n`are_equal()`函数是一个用于比较浮点值的实用函数:\n\n```cpp\nbool are_equal(double const d1, double const d2, \n               double const epsilon = 0.001)\n{\n   return std::fabs(d1 - d2) < epsilon;\n}\n```\n\n可能的温标的枚举和代表温度值的类别定义如下:\n\n```cpp\nnamespace temperature\n{\n   enum class scale { celsius, fahrenheit, kelvin };\n\n   template <scale S>\n   class quantity\n   {\n      const double amount;\n   public:\n      constexpr explicit quantity(double const a) : amount(a) {}\n      explicit operator double() const { return amount; }\n   };\n}\n```\n\n这里可以看到`quantity<S>`类的比较运算符:\n\n```cpp\nnamespace temperature \n{\n   template <scale S>\n   inline bool operator==(quantity<S> const & lhs, quantity<S> const & rhs)\n   {\n      return are_equal(static_cast<double>(lhs), static_cast<double>(rhs));\n   }\n\n   template <scale S>\n   inline bool operator!=(quantity<S> const & lhs, quantity<S> const & rhs)\n   {\n      return !(lhs == rhs);\n   }\n\n   template <scale S>\n   inline bool operator< (quantity<S> const & lhs, quantity<S> const & rhs)\n   {\n      return static_cast<double>(lhs) < static_cast<double>(rhs);\n   }\n\n   template <scale S>\n   inline bool operator> (quantity<S> const & lhs, quantity<S> const & rhs)\n   {\n      return rhs < lhs;\n   }\n\n   template <scale S>\n   inline bool operator<=(quantity<S> const & lhs, quantity<S> const & rhs)\n   {\n      return !(lhs > rhs);\n   }\n\n   template <scale S>\n   inline bool operator>=(quantity<S> const & lhs, quantity<S> const & rhs)\n   {\n      return !(lhs < rhs);\n   }\n\n   template <scale S>\n   constexpr quantity<S> operator+(quantity<S> const &q1, \n                                   quantity<S> const &q2)\n   {\n      return quantity<S>(static_cast<double>(q1) + \n                         static_cast<double>(q2));\n   }\n\n   template <scale S>\n   constexpr quantity<S> operator-(quantity<S> const &q1, \n                                   quantity<S> const &q2)\n   {\n      return quantity<S>(static_cast<double>(q1) - \n                         static_cast<double>(q2));\n   }\n}\n```\n\n为了在不同尺度的温度值之间进行转换，我们将定义一个名为`temperature_cast()`的函数模板，该模板利用几个类型特征来执行实际转换。所有这些都显示在这里，虽然不是所有的类型特征；其他的可以在本书附带的代码中找到:\n\n```cpp\nnamespace temperature\n{\n   template <scale S, scale R>\n   struct conversion_traits\n   {\n      static double convert(double const value) = delete;\n   };\n\n   template <>\n   struct conversion_traits<scale::celsius, scale::fahrenheit>\n   {\n      static double convert(double const value)\n      {\n         return (value * 9) / 5 + 32;\n      }\n   };\n\n   template <>\n   struct conversion_traits<scale::fahrenheit, scale::celsius>\n   {\n      static double convert(double const value)\n      {\n         return (value - 32) * 5 / 9;\n      }\n   };\n\n   template <scale R, scale S>\n   constexpr quantity<R> temperature_cast(quantity<S> const q)\n   {\n      return quantity<R>(conversion_traits<S, R>::convert(\n         static_cast<double>(q)));\n   }\n}\n```\n\n下面的代码片段显示了用于创建温度值的文字运算符。这些操作符被定义在一个单独的命名空间中，称为`temperature_scale_literals`，这是一个很好的做法，以便将名称与其他文字操作符冲突的风险降至最低:\n\n```cpp\nnamespace temperature\n{\n   namespace temperature_scale_literals\n   {\n      constexpr quantity<scale::celsius> operator \"\" _deg(\n         long double const amount)\n      {\n         return quantity<scale::celsius> {static_cast<double>(amount)};\n      }\n\n      constexpr quantity<scale::fahrenheit> operator \"\" _f(\n         long double const amount)\n      {\n         return quantity<scale::fahrenheit> {static_cast<double>(amount)};\n      }\n   }\n}\n```\n\n以下示例显示了如何定义两个温度值，一个以摄氏度为单位，一个以华氏度为单位，并在两者之间进行转换:\n\n```cpp\nint main()\n{\n   using namespace temperature;\n   using namespace temperature_scale_literals;\n\n   auto t1{ 36.5_deg };\n   auto t2{ 79.0_f };\n\n   auto tf = temperature_cast<scale::fahrenheit>(t1);\n   auto tc = temperature_cast<scale::celsius>(tf);\n   assert(t1 == tc);\n}\n```"
  },
  {
    "path": "docs/mod-cpp/14.md",
    "content": "# 十四、字符串和正则表达式\n\n# 问题\n\n这是本章的解题部分。\n\n# 23.二进制到字符串转换\n\n编写一个函数，在给定 8 位整数范围(如数组或向量)的情况下，返回包含输入数据的十六进制表示的字符串。该函数应该能够同时生成大写和小写内容。以下是一些输入和输出示例:\n\n输入:`{ 0xBA, 0xAD, 0xF0, 0x0D }`，输出:`\"BAADF00D\"`或`\"baadf00d\"`\n输入:`{ 1,2,3,4,5,6 }`，输出:`\"010203040506\"`\n\n# 24.字符串到二进制的转换\n\n编写一个函数，在给定包含十六进制数字作为输入参数的字符串的情况下，返回一个 8 位整数的向量，该向量表示字符串内容的数字反序列化。以下是一些例子:\n\n输入:`\"BAADF00D\"`或`\"baadF00D\"`，输出:`{0xBA, 0xAD, 0xF0, 0x0D}`\n输入`\"010203040506\"`，输出:`{1, 2, 3, 4, 5, 6}`\n\n# 25.将文章标题大写\n\n编写一个函数，将输入文本转换为大写版本，其中每个单词都以大写字母开头，所有其他字母都以小写字母开头。例如，文本`\"the c++ challenger\"`应该转换为`\"The C++ Challenger\"`。\n\n# 26.用分隔符将字符串连接在一起\n\n编写一个函数，在给定字符串列表和分隔符的情况下，通过连接用指定分隔符分隔的所有输入字符串来创建一个新字符串。分隔符不能出现在最后一个字符串之后，并且当没有提供输入字符串时，函数必须返回一个空字符串。\n\n示例:输入`{ \"this\",\"is\",\"an\",\"example\" }`和分隔符`' '`(空格)，输出:`\"this is an example\"`。\n\n# 27.使用可能的分隔符列表将字符串拆分为标记\n\n编写一个函数，给定一个字符串和一个可能的分隔符列表，将字符串拆分成由任何分隔符分隔的标记，并在`std::vector`中返回它们。\n\n示例:输入:`\"this,is.a sample!!\"`带分隔符`\",.! \"`，输出:`{\"this\", \"is\", \"a\", \"sample\"}`。\n\n# 28.最长回文子串\n\n编写一个函数，在给定输入字符串的情况下，定位并返回字符串中最长的回文序列。如果存在多个相同长度的回文，则应返回第一个回文。\n\n# 29.车牌验证\n\n考虑格式为`LLL-LL DDD`或`LLL-LL DDDD`的车牌(其中`L`是从 *A* 到 *Z* 的大写字母，`D`是数字)，写:\n\n*   验证车牌号码格式是否正确的一种功能\n*   一个函数，给定输入文本，提取并返回文本中找到的所有车牌号码\n\n# 30.正在提取网址部分\n\n编写一个函数，给定一个代表 URL 的字符串，解析并提取 URL 的各个部分(协议、域、端口、路径、查询和片段)。\n\n# 31.在字符串中转换日期\n\n编写一个函数，给定一个包含格式为`dd.mm.yyyy`或`dd-mm-yyyy`的日期的文本，转换该文本，使其包含格式为`yyyy-mm-dd`的日期。\n\n# 解决方法\n\n以下是上述问题解决部分的解决方案。\n\n# 23.二进制到字符串转换\n\n为了编写一个能够处理各种范围的通用函数，比如`std::array`、`std::vector`、类 C 数组或者其他，我们应该编写一个函数模板。在下面，有两个重载；一个使用容器作为参数和指示大小写样式的标志，另一个使用一对迭代器(标记第一个迭代器，然后一个迭代器越过范围的结束元素)和指示大小写的标志。该范围的内容被写入一个`std::ostringstream`对象，带有适当的输入/输出操纵器，例如宽度、填充字符或大小写标志:\n\n```cpp\ntemplate <typename Iter>\nstd::string bytes_to_hexstr(Iter begin, Iter end, \n                            bool const uppercase = false)\n{\n   std::ostringstream oss;\n   if(uppercase) oss.setf(std::ios_base::uppercase);\n   for (; begin != end; ++ begin)\n     oss << std::hex << std::setw(2) << std::setfill('0') \n         << static_cast<int>(*begin);\n   return oss.str();\n}\n\ntemplate <typename C>\nstd::string bytes_to_hexstr(C const & c, bool const uppercase = false)\n{\n   return bytes_to_hexstr(std::cbegin(c), std::cend(c), uppercase);\n}\n```\n\n这些功能可以如下使用:\n\n```cpp\nint main()\n{\n   std::vector<unsigned char> v{ 0xBA, 0xAD, 0xF0, 0x0D };\n   std::array<unsigned char, 6> a{ {1,2,3,4,5,6} };\n   unsigned char buf[5] = {0x11, 0x22, 0x33, 0x44, 0x55};\n\n   assert(bytes_to_hexstr(v, true) == \"BAADF00D\");\n   assert(bytes_to_hexstr(a, true) == \"010203040506\");\n   assert(bytes_to_hexstr(buf, true) == \"1122334455\");\n\n   assert(bytes_to_hexstr(v) == \"baadf00d\");\n   assert(bytes_to_hexstr(a) == \"010203040506\");\n   assert(bytes_to_hexstr(buf) == \"1122334455\");\n}\n```\n\n# 24.字符串到二进制的转换\n\n这里请求的操作与上一个问题中实现的操作相反。然而，这一次，我们可以编写一个函数，而不是函数模板。输入是一个`std::string_view`，这是一个轻量级的字符序列包装器。输出是一个 8 位无符号整数向量。下面的`hexstr_to_bytes`函数将每两个文本字符转换成一个`unsigned char`值(`\"A0\"`变成`0xA0`，将它们放入一个`std::vector`，并返回向量:\n\n```cpp\nunsigned char hexchar_to_int(char const ch)\n{\n   if (ch >= '0' && ch <= '9') return ch - '0';\n   if (ch >= 'A' && ch <= 'F') return ch - 'A' + 10;\n   if (ch >= 'a' && ch <= 'f') return ch - 'a' + 10;\n      throw std::invalid_argument(\"Invalid hexadecimal character\");\n}\n\nstd::vector<unsigned char> hexstr_to_bytes(std::string_view str)\n{\n   std::vector<unsigned char> result;\n   for (size_t i = 0; i < str.size(); i += 2) \n   {\n      result.push_back(\n         (hexchar_to_int(str[i]) << 4) | hexchar_to_int(str[i+1]));\n   }\n   return result;\n}\n```\n\nThis function assumes the input string contains an even number of hexadecimal digits. In cases where the input string contains an odd number of hexadecimal digits, the last one is discarded (so that `\"BAD\"` becomes `{0xBA}`). As a further exercise, modify the preceding function so that, instead of discarding the last odd digit, it considers a leading zero so that `\"BAD\"` becomes `{0x0B, 0xAD}`. Also, as yet another exercise, you can write a version of the function that deserializes content that has the hexadecimal digits separated by a delimiter, such as space (for example `\"BA AD F0 0D\"`).\n\n下一个代码示例显示了如何使用该函数:\n\n```cpp\nint main()\n{\n   std::vector<unsigned char> expected{ 0xBA, 0xAD, 0xF0, 0x0D, 0x42 };\n   assert(hexstr_to_bytes(\"BAADF00D42\") == expected);\n   assert(hexstr_to_bytes(\"BaaDf00d42\") == expected);\n}\n```\n\n# 25.将文章标题大写\n\n如下实现的函数模板`capitalize()`可以处理任何类型的字符串。它不修改输入字符串，而是创建一个新字符串。为此，它使用了`std::stringstream`。它会遍历输入字符串中的所有字符，并在每次遇到空格或标点符号时向`true`设置一个指示新单词的标志。当输入字符代表单词中的第一个字符时，它们会转换为大写，否则会转换为小写:\n\n```cpp\ntemplate <class Elem>\nusing tstring = std::basic_string<Elem, std::char_traits<Elem>, \n                                  std::allocator<Elem>>;\ntemplate <class Elem>\nusing tstringstream = std::basic_stringstream<\n   Elem, std::char_traits<Elem>, std::allocator<Elem>>;\n\ntemplate <class Elem>\ntstring<Elem> capitalize(tstring<Elem> const & text)\n{\n   tstringstream<Elem> result;\n   bool newWord = true;\n   for (auto const ch : text)\n   {\n      newWord = newWord || std::ispunct(ch) || std::isspace(ch);\n      if (std::isalpha(ch))\n      {\n         if (newWord)\n         {\n            result << static_cast<Elem>(std::toupper(ch));\n            newWord = false;\n         }\n         else\n            result << static_cast<Elem>(std::tolower(ch));\n      }\n      else result << ch;\n   }\n   return result.str();\n}\n```\n\n在下面的程序中，您可以看到如何使用这个函数来大写文本:\n\n```cpp\nint main()\n{\n   using namespace std::string_literals;\n   assert(\"The C++ Challenger\"s ==\n          capitalize(\"the c++ challenger\"s));\n   assert(\"This Is An Example, Should Work!\"s == \n          capitalize(\"THIS IS an ExamplE, should wORk!\"s));\n}\n```\n\n# 26.用分隔符将字符串连接在一起\n\n下面的代码中列出了两个名为`join_strings()`的重载。一个接受一个字符串容器和一个指向表示分隔符的字符序列的指针，而另一个接受两个随机访问迭代器和一个分隔符，前者表示区域的第一个元素，后者表示最后一个元素。它们都使用输出字符串流和`std::copy`函数，返回通过连接所有输入字符串而创建的新字符串。这个通用函数将指定范围内的所有元素复制到输出范围，由输出迭代器表示。我们这里使用的是一个`std::ostream_iterator`，每次迭代器被赋值时，它使用`operator<<`将赋值写入指定的输出流:\n\n```cpp\ntemplate <typename Iter>\nstd::string join_strings(Iter begin, Iter end, \n                         char const * const separator)\n{\n   std::ostringstream os;\n   std::copy(begin, end-1, \n             std::ostream_iterator<std::string>(os, separator));\n   os << *(end-1);\n   return os.str();\n}\n\ntemplate <typename C>\nstd::string join_strings(C const & c, char const * const separator)\n{\n   if (c.size() == 0) return std::string{};\n   return join_strings(std::begin(c), std::end(c), separator);\n}\n\nint main()\n{\n   using namespace std::string_literals;\n   std::vector<std::string> v1{ \"this\",\"is\",\"an\",\"example\" };\n   std::vector<std::string> v2{ \"example\" };\n   std::vector<std::string> v3{ };\n\n   assert(join_strings(v1, \" \") == \"this is an example\"s);\n   assert(join_strings(v2, \" \") == \"example\"s);\n   assert(join_strings(v3, \" \") == \"\"s);\n}\n```\n\nAs a further exercise, you should modify the overload that takes iterators as arguments so that it works with other types of iterators, such as bidirectional iterators, thereby enabling the use of this function with lists or other containers.\n\n# 27.使用可能的分隔符列表将字符串拆分为标记\n\n拆分函数的两个不同版本如下所示:\n\n*   第一个使用单个字符作为分隔符。为了分割输入字符串，它使用一个用输入字符串的内容初始化的字符串流，使用`std::getline()`从中读取数据块，直到遇到下一个分隔符或行尾字符。\n*   第二个使用可能的字符分隔符列表，在`std::string`中指定。它使用`std:string::find_first_of()`定位任何分隔符字符的第一个位置，从给定的位置开始。它循环执行，直到处理完整个输入字符串。提取的子串被添加到结果向量:\n\n```cpp\ntemplate <class Elem>\nusing tstring = std::basic_string<Elem, std::char_traits<Elem>, \n                                  std::allocator<Elem>>;\n\ntemplate <class Elem>\nusing tstringstream = std::basic_stringstream<\n   Elem, std::char_traits<Elem>, std::allocator<Elem>>;\ntemplate<typename Elem>\ninline std::vector<tstring<Elem>> split(tstring<Elem> text, \n                                        Elem const delimiter)\n{\n   auto sstr = tstringstream<Elem>{ text };\n   auto tokens = std::vector<tstring<Elem>>{};\n   auto token = tstring<Elem>{};\n   while (std::getline(sstr, token, delimiter))\n   {\n      if (!token.empty()) tokens.push_back(token);\n   }\n   return tokens;\n}\n\ntemplate<typename Elem>\ninline std::vector<tstring<Elem>> split(tstring<Elem> text, \n                                        tstring<Elem> const & delimiters)\n{\n   auto tokens = std::vector<tstring<Elem>>{};\n   size_t pos, prev_pos = 0;\n   while ((pos = text.find_first_of(delimiters, prev_pos)) != \n   std::string::npos)\n   {\n      if (pos > prev_pos)\n      tokens.push_back(text.substr(prev_pos, pos - prev_pos));\n      prev_pos = pos + 1;\n   }\n   if (prev_pos < text.length())\n   tokens.push_back(text.substr(prev_pos, std::string::npos));\n   return tokens;\n}\n```\n\n以下示例代码显示了如何使用一个分隔符或多个分隔符拆分不同字符串的两个示例:\n\n```cpp\nint main()\n{\n   using namespace std::string_literals;\n   std::vector<std::string> expected{\"this\", \"is\", \"a\", \"sample\"};\n   assert(expected == split(\"this is a sample\"s, ' '));\n   assert(expected == split(\"this,is a.sample!!\"s, \",.! \"s));\n}\n```\n\n# 28.最长回文子串\n\n这个问题最简单的解决方案是尝试蛮力方法，检查每个子串是否是回文。但是，这意味着我们需要检查 *C(N，2)* 子串(其中 *N* 是字符串中的字符数)，时间复杂度为 *![](img/76505ab6-7d29-4aab-9955-744ed0bcd1b6.png)* 。通过存储子问题的结果，复杂性可以降低到![](img/2f7e78fe-014a-40b2-9524-bc0f479781a1.png)。为此，我们需要一个大小为![](img/a4173824-4963-42ca-b9ab-fd97affe7750.png)的布尔值表，其中`[i, j]`处的元素指示从位置`i`到`j`的子串是否是回文。我们首先用`true`(单字符回文)初始化所有元素`[i,i]`，用`true`初始化所有元素`[i,i+i]`，用于所有连续的两个相同字符(双字符回文)。然后我们继续检查大于两个字符的子字符串，如果`[i+i,j-1]`处的元素是`true`，并且字符串中`i`和`j`位置上的字符也相等，则将`[i,j]`处的元素设置为`true`。一路上，我们保留最长回文子串的起始位置和长度，以便在计算完表后提取它。\n\n在代码中，此解决方案如下所示:\n\n```cpp\nstd::string longest_palindrome(std::string_view str)\n{\n   size_t const len = str.size();\n   size_t longestBegin = 0;\n   size_t maxLen = 1;\n\n   std::vector<bool> table(len * len, false);\n   for (size_t i = 0; i < len; i++)\n      table[i*len + i] = true;\n\n   for (size_t i = 0; i < len - 1; i++)\n   {\n      if (str[i] == str[i + 1]) \n      {\n         table[i*len + i + 1] = true;\n         if (maxLen < 2)\n         {\n            longestBegin = i;\n            maxLen = 2;\n         }\n      }\n   }\n\n   for (size_t k = 3; k <= len; k++)\n   {\n      for (size_t i = 0; i < len - k + 1; i++)\n      {\n         size_t j = i + k - 1;\n         if (str[i] == str[j] && table[(i + 1)*len + j - 1])\n         {\n            table[i*len +j] = true;\n            if (maxLen < k)\n            {\n               longestBegin = i;\n               maxLen = k;\n            }\n         }\n      }\n   }\n   return std::string(str.substr(longestBegin, maxLen));\n}\n```\n\n以下是`longest_palindrome()`功能的一些测试案例:\n\n```cpp\nint main()\n{\n   using namespace std::string_literals;\n   assert(longest_palindrome(\"sahararahnide\") == \"hararah\");\n   assert(longest_palindrome(\"level\") == \"level\");\n   assert(longest_palindrome(\"s\") == \"s\");\n}\n```\n\n# 29.车牌验证\n\n解决这个问题最简单的方法是使用正则表达式。符合所述格式的正则表达式为`\"[A-Z]{3}-[A-Z]{2} \\d{3,4}\"`。\n\n第一个函数只需要验证输入字符串只包含与该正则表达式匹配的文本。为此，我们可以使用`std::regex_match()`，如下所示:\n\n```cpp\nbool validate_license_plate_format(std::string_view str)\n{\n   std::regex rx(R\"([A-Z]{3}-[A-Z]{2} \\d{3,4})\");\n   return std::regex_match(str.data(), rx);\n}\n\nint main()\n{\n   assert(validate_license_plate_format(\"ABC-DE 123\"));\n   assert(validate_license_plate_format(\"ABC-DE 1234\"));\n   assert(!validate_license_plate_format(\"ABC-DE 12345\"));\n   assert(!validate_license_plate_format(\"abc-de 1234\"));\n}\n```\n\n第二个功能略有不同。它必须标识字符串中正则表达式的所有匹配项，而不是匹配输入字符串。正则表达式将因此变为`\"([A-Z]{3}-[A-Z]{2} \\d{3,4})*\"`。为了遍历所有匹配，我们必须使用`std::sregex_iterator`，如下所示:\n\n```cpp\nstd::vector<std::string> extract_license_plate_numbers(\n                            std::string const & str)\n{\n   std::regex rx(R\"(([A-Z]{3}-[A-Z]{2} \\d{3,4})*)\");\n   std::smatch match;\n   std::vector<std::string> results;\n\n   for(auto i = std::sregex_iterator(std::cbegin(str), std::cend(str), rx); \n       i != std::sregex_iterator(); ++ i) \n   {\n      if((*i)[1].matched)\n      results.push_back(i->str());\n   }\n   return results;\n}\n\nint main()\n{\n   std::vector<std::string> expected {\n      \"AAA-AA 123\", \"ABC-DE 1234\", \"XYZ-WW 0001\"};\n   std::string text(\"AAA-AA 123qwe-ty 1234 ABC-DE 123456..XYZ-WW 0001\");\n   assert(expected == extract_license_plate_numbers(text));\n}\n```\n\n# 30.正在提取网址部分\n\n这个问题也适合使用正则表达式来解决。然而，找到一个可以匹配任何网址的正则表达式是一项困难的任务。本练习的目的是帮助您使用 regex 库练习技巧，而不是为了这个特殊的目的找到最终的正则表达式。因此，这里使用的正则表达式仅用于教学目的。\n\nYou can try regular expressions using online testers and debuggers, such as [https://regex101.com/](https://regex101.com/). This can be useful in order to work out your regular expressions and try them against various datasets.\n\n对于这个任务，我们将考虑一个网址有以下几个部分:`protocol`和`domain`是强制的，`port`、`path`、`query`和`fragment`都是可选的。以下结构用于返回解析 URL 的结果(或者，您可以返回一个元组，并使用结构化绑定将变量绑定到元组的各个子部分):\n\n```cpp\nstruct uri_parts\n{\n   std::string                protocol;\n   std::string                domain;\n   std::optional<int>         port;\n   std::optional<std::string> path;\n   std::optional<std::string> query;\n   std::optional<std::string> fragment;\n};\n```\n\n一个可以解析网址并提取和返回其部分的函数可以有以下实现。请注意，返回类型是`std::optional<uri_parts>`，因为该函数可能无法将输入字符串与正则表达式匹配；在这种情况下，返回值为`std::nullopt`:\n\n```cpp\nstd::optional<uri_parts> parse_uri(std::string uri)\n{\n   std::regex rx(R\"(^(\\w+):\\/\\/([\\w.-]+)(:(\\d+))?([\\w\\/\\.]+)?(\\?([\\w=&]*)(#?(\\w+))?)?$)\");\n   auto matches = std::smatch{};\n   if (std::regex_match(uri, matches, rx))\n   {\n      if (matches[1].matched && matches[2].matched)\n      {\n         uri_parts parts;\n         parts.protocol = matches[1].str();\n         parts.domain = matches[2].str();\n         if (matches[4].matched)\n            parts.port = std::stoi(matches[4]);\n         if (matches[5].matched)\n            parts.path = matches[5];\n         if (matches[7].matched)\n            parts.query = matches[7];\n         if (matches[9].matched)\n            parts.fragment = matches[9];\n         return parts;\n      }\n   }\n   return {};\n}\n```\n\n以下程序使用包含不同部分的两个网址测试`parse_uri()`功能:\n\n```cpp\nint main()\n{\n   auto p1 = parse_uri(\"https://packt.com\");\n   assert(p1.has_value());\n   assert(p1->protocol == \"https\");\n   assert(p1->domain == \"packt.com\");\n   assert(!p1->port.has_value());\n   assert(!p1->path.has_value());\n   assert(!p1->query.has_value());\n   assert(!p1->fragment.has_value());\n\n   auto p2 = parse_uri(\"https://bbc.com:80/en/index.html?lite=true#ui\");\n   assert(p2.has_value());\n   assert(p2->protocol == \"https\");\n   assert(p2->domain == \"bbc.com\");\n   assert(p2->port == 80);\n   assert(p2->path.value() == \"/en/index.html\");\n   assert(p2->query.value() == \"lite=true\");\n   assert(p2->fragment.value() == \"ui\");\n}\n```\n\n# 31.在字符串中转换日期\n\n使用`std::regex_replace()`可以用正则表达式进行文本转换。能够匹配指定格式日期的正则表达式是`(\\d{1,2})(\\.|-|/)(\\d{1,2})(\\.|-|/)(\\d{4})`。这个正则表达式定义了五个捕获组；1 <sup>st</sup> 为日，2 <sup>nd</sup> 为分隔符(`.`或`-`)，3 <sup>rd</sup> 为月，4 <sup>th</sup> 再次为分隔符(`.`或`-`)，5 <sup>th</sup> 为年。\n\n因为我们要将日期从格式`dd.mm.yyyy`或`dd-mm-yyyy`转换为`yyyy-mm-dd`，所以`std::regex_replace()`的正则表达式替换格式字符串应该是`\"($5-$3-$1)\"`:\n\n```cpp\nstd::string transform_date(std::string_view text)\n{\n   auto rx = std::regex{ R\"((\\d{1,2})(\\.|-|/)(\\d{1,2})(\\.|-|/)(\\d{4}))\" };\n   return std::regex_replace(text.data(), rx, R\"($5-$3-$1)\");\n}\n\nint main()\n{\n   using namespace std::string_literals;\n   assert(transform_date(\"today is 01.12.2017!\"s) == \n          \"today is 2017-12-01!\"s);\n}\n```"
  },
  {
    "path": "docs/mod-cpp/15.md",
    "content": "# 十五、流和文件系统\n\n# 问题\n\n这是本章的问题解决部分。\n\n# 32.帕斯卡三角形\n\n编写一个函数，向控制台打印多达 10 行的帕斯卡三角形。\n\n# 33.进程列表的表格打印\n\n假设您有一个系统中所有进程列表的快照。每个进程的信息包括名称、标识符、状态(可以是运行中的*或暂停中的*)、帐户名(进程在该帐户名下运行)、内存大小(以字节为单位)和平台(可以是 32 位或 64 位)。您的任务是编写一个函数，获取这样一个进程列表，并以表格格式按字母顺序将它们打印到控制台。所有列都必须左对齐，除了内存列必须右对齐。内存大小的值必须以 KB 为单位显示。下面是这个函数的输出示例:**\n\n```cpp\nchrome.exe      1044   Running    marius.bancila    25180  32-bit\nchrome.exe      10100  Running    marius.bancila   227756  32-bit\ncmd.exe         512    Running    SYSTEM               48  64-bit\nexplorer.exe    7108   Running    marius.bancila    29529  64-bit\nskype.exe       22456  Suspended  marius.bancila      656  64-bit\n```\n\n# 34.从文本文件中删除空行\n\n编写一个程序，给定一个文本文件的路径，通过删除所有空行来修改文件。仅包含空格的行被认为是空的。\n\n# 35.计算目录的大小\n\n编写一个递归计算目录大小(以字节为单位)的函数。应该可以指出是否应该遵循符号链接。\n\n# 36.删除超过给定日期的文件\n\n编写一个函数，在给定目录路径和持续时间的情况下，以递归方式删除所有早于指定持续时间的条目(文件或子目录)。持续时间可以表示任何东西，例如天、小时、分钟、秒等，或者它们的组合，例如一小时二十分钟。如果指定的目录本身早于给定的持续时间，则应将其完全删除。\n\n# 37.在目录中查找与正则表达式匹配的文件\n\n编写一个函数，在给定目录路径和正则表达式的情况下，返回名称与正则表达式匹配的所有目录条目的列表。\n\n# 38.临时日志文件\n\n创建一个日志类，将文本消息写入可丢弃的文本文件。文本文件应具有唯一的名称，并且必须位于临时目录中。除非另外指定，否则当类的实例被销毁时，应该删除此日志文件。但是，应该可以通过将其移动到永久位置来保留日志文件。\n\n# 解决方法\n\n以下是上述问题解决部分的解决方案。\n\n# 32.帕斯卡三角形\n\n帕斯卡三角形是一种表示二项式系数的结构。三角形从一个值为 1 的行开始。每一行的元素都是通过对上面的数字进行求和来构造的，向左和向右，并将空白条目视为 0。下面是一个五行三角形的例子:\n\n```cpp\n 1\n 1   1\n 1   2   1\n 1   3   3   1\n1   4   6   4   1\n```\n\n要打印三角形，我们必须:\n\n*   将输出位置向右移动适当数量的空格，使顶部投影到三角形底部的中间。\n*   通过对上述左右值求和来计算每个值。一个更简单的公式是，对于一行`i`和一列`j`，每个新值`x`等于`x`的前一个值乘以`(i - j) / (j + 1)`，其中`x`从 1 开始。\n\n下面是打印三角形的函数的可能实现:\n\n```cpp\nunsigned int number_of_digits(unsigned int const i)\n{\n   return i > 0 ? (int)log10((double)i) + 1 : 1;\n}\n\nvoid print_pascal_triangle(int const n)\n{\n   for (int i = 0; i < n; i++) \n   {\n      auto x = 1;\n      std::cout << std::string((n - i - 1)*(n / 2), ' ');\n      for (int j = 0; j <= i; j++) \n      {\n         auto y = x;\n         x = x * (i - j) / (j + 1);\n         auto maxlen = number_of_digits(x) - 1;\n         std::cout << y << std::string(n - 1 - maxlen - n%2, ' ');\n      }\n      std::cout << std::endl;\n   }\n}\n```\n\n以下程序要求用户输入级别数，并将三角形打印到控制台:\n\n```cpp\nint main()\n{\n   int n = 0;\n   std::cout << \"Levels (up to 10): \";\n   std::cin >> n;\n   if (n > 10)\n      std::cout << \"Value too large\" << std::endl;\n   else\n      print_pascal_triangle(n);\n}\n```\n\n# 33.进程列表的表格打印\n\n为了解决这个问题，我们将考虑以下表示进程信息的类:\n\n```cpp\nenum class procstatus {suspended, running};\nenum class platforms {p32bit, p64bit};\n\nstruct procinfo\n{\n   int         id;\n   std::string name;\n   procstatus  status;\n   std::string account;\n   size_t      memory;\n   platforms   platform;\n};\n```\n\n为了将状态和平台打印为文本而不是数值，我们需要从枚举到`std::string`的转换函数:\n\n```cpp\nstd::string status_to_string(procstatus const status)\n{\n   if (status == procstatus::suspended) return \"suspended\";\n   else return \"running\";\n}\n\nstd::string platform_to_string(platforms const platform)\n{\n   if (platform == platforms::p32bit) return \"32-bit\";\n   else return \"64-bit\";\n}\n```\n\n流程需要按流程名称的字母顺序排序。因此，第一步是对流程的输入范围进行排序。对于打印本身，我们应该使用输入/输出操纵器:\n\n```cpp\nvoid print_processes(std::vector<procinfo> processes)\n{\n   std::sort(\n      std::begin(processes), std::end(processes),\n      [](procinfo const & p1, procinfo const & p2) {\n         return p1.name < p2.name; });\n\n   for (auto const & pi : processes)\n   {\n      std::cout << std::left << std::setw(25) << std::setfill(' ')\n                << pi.name;\n      std::cout << std::left << std::setw(8) << std::setfill(' ')\n                << pi.id;\n      std::cout << std::left << std::setw(12) << std::setfill(' ')\n                << status_to_string(pi.status);\n      std::cout << std::left << std::setw(15) << std::setfill(' ')\n                << pi.account;\n      std::cout << std::right << std::setw(10) << std::setfill(' ')\n                << (int)(pi.memory/1024);\n      std::cout << std::left << ' ' << platform_to_string(pi.platform);\n      std::cout << std::endl;\n   }\n}\n```\n\n以下程序定义了一个进程列表(您实际上可以使用特定于操作系统的 API 检索正在运行的进程列表)，并以请求的格式将其打印到控制台:\n\n```cpp\nint main()\n{\n   using namespace std::string_literals;\n\n   std::vector<procinfo> processes\n   {\n      {512, \"cmd.exe\"s, procstatus::running, \"SYSTEM\"s, \n            148293, platforms::p64bit },\n      {1044, \"chrome.exe\"s, procstatus::running, \"marius.bancila\"s, \n            25180454, platforms::p32bit},\n      {7108, \"explorer.exe\"s, procstatus::running, \"marius.bancila\"s,  \n            2952943, platforms::p64bit },\n      {10100, \"chrome.exe\"s, procstatus::running, \"marius.bancila\"s, \n            227756123, platforms::p32bit},\n      {22456, \"skype.exe\"s, procstatus::suspended, \"marius.bancila\"s, \n            16870123, platforms::p64bit }, \n   };\n\n   print_processes(processes);\n}\n```\n\n# 34.从文本文件中删除空行\n\n解决此任务的一种可能方法是执行以下操作:\n\n1.  创建一个临时文件，仅包含要从原始文件中保留的文本\n2.  从输入文件中逐行读取，并将所有非空行复制到临时文件中\n3.  完成处理后删除原始文件\n4.  将临时文件移动到原始文件的路径\n\n另一种方法是移动临时文件并覆盖原始文件。以下实现遵循列出的步骤。临时文件创建在`filesystem::temp_directory_path()`返回的临时目录中:\n\n```cpp\nnamespace fs = std::experimental::filesystem;\n\nvoid remove_empty_lines(fs::path filepath)\n{\n   std::ifstream filein(filepath.native(), std::ios::in);\n   if (!filein.is_open())\n      throw std::runtime_error(\"cannot open input file\");\n\n   auto temppath = fs::temp_directory_path() / \"temp.txt\";\n   std::ofstream fileout(temppath.native(), \n   std::ios::out | std::ios::trunc);\n   if (!fileout.is_open())\n      throw std::runtime_error(\"cannot create temporary file\");\n\n   std::string line;\n   while (std::getline(filein, line))\n   {\n      if (line.length() > 0 &&\n      line.find_first_not_of(' ') != line.npos)\n      {\n         fileout << line << '\\n';\n      }\n   }\n   filein.close();\n   fileout.close();\n\n   fs::remove(filepath);\n   fs::rename(temppath, filepath);\n}\n```\n\n# 35.计算目录的大小\n\n为了计算一个目录的大小，我们必须遍历所有的文件，并计算单个文件的大小。\n\n`filesystem::recursive_directory_iterator`是来自`filesystem`库的迭代器，允许以递归方式迭代目录的所有条目。它有各种各样的构造函数，其中一些构造函数采用类型为`filesystem::directory_options`的值，该值指示是否应该遵循符号链接。通用`std::accumulate()`算法可以用来合计文件大小。由于一个目录的总大小可能超过 2 GB，所以不应该使用`int`或`long`，而应该使用`unsigned long long`作为总和类型。以下函数显示了所需任务的可能实现:\n\n```cpp\nnamespace fs = std::experimental::filesystem;\n\nstd::uintmax_t get_directory_size(fs::path const & dir,\n                                  bool const follow_symlinks = false)\n{\n   auto iterator = fs::recursive_directory_iterator(\n      dir,\n      follow_symlinks ? fs::directory_options::follow_directory_symlink : \n                        fs::directory_options::none);\n\n   return std::accumulate(\n      fs::begin(iterator), fs::end(iterator),\n      0ull,\n      [](std::uintmax_t const total,\n         fs::directory_entry const & entry) {\n             return total + (fs::is_regular_file(entry) ?\n                    fs::file_size(entry.path()) : 0);\n   });\n}\n\nint main()\n{\n   std::string path;\n   std::cout << \"Path: \";\n   std::cin >> path;\n   std::cout << \"Size: \" << get_directory_size(path) << std::endl;\n}\n```\n\n# 36.删除超过给定日期的文件\n\n要执行文件系统操作，您应该使用`filesystem`库。对于使用时间和持续时间的工作，您应该使用`chrono`库。实现所请求功能的函数必须执行以下操作:\n\n1.  检查目标路径指示的条目是否存在，并且是否早于给定的持续时间，如果是，则将其删除\n2.  如果它不是旧的，并且是一个目录，则遍历它的所有条目并递归调用该函数:\n\n```cpp\nnamespace fs = std::experimental::filesystem;\nnamespace ch = std::chrono;\n\ntemplate <typename Duration>\nbool is_older_than(fs::path const & path, Duration const duration)\n{\n   auto ftimeduration = fs::last_write_time(path).time_since_epoch();\n   auto nowduration = (ch::system_clock::now() - duration)\n                      .time_since_epoch();\n   return ch::duration_cast<Duration>(nowduration - ftimeduration)\n                      .count() > 0;\n}\n\ntemplate <typename Duration>\nvoid remove_files_older_than(fs::path const & path, \n                             Duration const duration)\n{\n   try\n   {\n      if (fs::exists(path))\n      {\n         if (is_older_than(path, duration))\n         {\n            fs::remove(path);\n         }\n         else if(fs::is_directory(path))\n         {\n            for (auto const & entry : fs::directory_iterator(path))\n            {\n               remove_files_older_than(entry.path(), duration);\n            }\n         }\n      }\n   }\n   catch (std::exception const & ex)\n   {\n      std::cerr << ex.what() << std::endl;\n   }\n}\n```\n\n使用`directory_iterator`并递归调用`remove_files_older_than()`的另一种方法是使用`recursive_directory_iterator`并简单地删除超过给定持续时间的条目。但是，这种方法会采用未定义的行为，因为如果在创建递归目录迭代器后，文件或目录被删除或添加到目录树中，则不会指定是否会通过迭代器观察到更改。因此，应该避免这种方法。\n\n`is_older_than()`功能模板确定当前时刻和最后一次文件写入操作从系统时钟周期开始经过的时间，并检查两者的差值是否大于指定的持续时间。\n\n`remove_files_older_than()`功能可以如下使用:\n\n```cpp\nint main()\n{\n   using namespace std::chrono_literals;\n\n#ifdef _WIN32\n   auto path = R\"(..\\Test\\)\";\n#else\n   auto path = R\"(../Test/)\";\n#endif\n\n   remove_files_older_than(path, 1h + 20min);\n}\n```\n\n# 37.在目录中查找与正则表达式匹配的文件\n\n实现指定的功能应该很简单:递归地遍历指定目录中的所有条目，并保留所有名称与正则表达式匹配的常规文件条目。为此，您应该使用以下内容:\n\n*   `filesystem::recursive_directory_iterator`遍历目录条目\n*   `regex`和`regex_match()`检查文件名是否与正则表达式匹配\n*   `copy_if()`和`back_inserter`在`vector`末尾复制符合特定标准的目录条目。\n\n这样的函数可能如下所示:\n\n```cpp\nnamespace fs = std::experimental::filesystem;\n\nstd::vector<fs::directory_entry> find_files(\n   fs::path const & path,\n   std::string_view regex)\n{\n   std::vector<fs::directory_entry> result;\n   std::regex rx(regex.data());\n\n   std::copy_if(\n      fs::recursive_directory_iterator(path),\n      fs::recursive_directory_iterator(),\n      std::back_inserter(result),\n      [&rx](fs::directory_entry const & entry) {\n         return fs::is_regular_file(entry.path()) &&\n                std::regex_match(entry.path().filename().string(), rx);\n   });\n\n   return result;\n}\n```\n\n有了这些，我们可以编写以下代码:\n\n```cpp\nint main()\n{\n   auto dir = fs::temp_directory_path();\n   auto pattern = R\"(wct[0-9a-zA-Z]{3}\\.tmp)\";\n   auto result = find_files(dir, pattern);\n\n   for (auto const & entry : result)\n   {\n      std::cout << entry.path().string() << std::endl;\n   }\n}\n```\n\n# 38.临时日志文件\n\n您必须为此任务实现的日志类应该:\n\n*   有一个构造函数，它在临时目录中创建一个文本文件，并打开它进行写入\n*   在销毁过程中，如果文件仍然存在，请将其关闭并删除\n*   有一个关闭文件并将其移动到永久路径的方法\n*   将文本消息写入输出文件的时间超过了`operator<<`\n\n为了给文件创建唯一的名称，可以使用 UUID(也称为 GUID)。C++ 标准不支持任何与此相关的功能，但是有第三方库，比如`boost::uuid`、 *CrossGuid* 或者`stduuid`，这其实是我创建的一个库。对于这个实现，我将使用最后一个。你可以在[https://github.com/mariusbancila/stduuid](https://github.com/mariusbancila/stduuid)找到它:\n\n```cpp\nnamespace fs = std::experimental::filesystem;\n\nclass logger\n{\n   fs::path logpath;\n   std::ofstream logfile;\npublic:\n   logger()\n   {\n      auto name = uuids::to_string(uuids::uuid_random_generator{}());\n      logpath = fs::temp_directory_path() / (name + \".tmp\");\n      logfile.open(logpath.c_str(), std::ios::out|std::ios::trunc);\n   }\n\n   ~logger() noexcept\n   {\n      try {\n         if(logfile.is_open()) logfile.close();\n         if (!logpath.empty()) fs::remove(logpath);\n      }\n      catch (...) {}\n   }\n\n   void persist(fs::path const & path)\n   {\n      logfile.close();\n      fs::rename(logpath, path);\n      logpath.clear();\n   }\n\n   logger& operator<<(std::string_view message)\n   {\n      logfile << message.data() << '\\n';\n      return *this;\n   }\n};\n```\n\n使用此类的示例如下:\n\n```cpp\nint main()\n{\n   logger log;\n   try \n   {\n      log << \"this is a line\" << \"and this is another one\";\n      throw std::runtime_error(\"error\");\n   }\n   catch (...) \n   {\n      log.persist(R\"(lastlog.txt)\");\n   }\n}\n```**"
  },
  {
    "path": "docs/mod-cpp/16.md",
    "content": "# 十六、日期和时间\n\n# 问题\n\n这是本章的解题部分。\n\n# 39.测量功能执行时间\n\n编写一个函数，该函数可以测量函数在任何所需持续时间(如秒、毫秒、微秒等)内的执行时间(具有任意数量的参数)。\n\n# 40.两个日期之间的天数\n\n编写一个函数，在给定两个日期的情况下，返回两个日期之间的天数。无论输入日期的顺序如何，该函数都应该工作。\n\n# 41.一周中的某一天\n\n编写一个函数，在给定日期的情况下，确定一周中的哪一天。这个函数应该返回一个介于 1(星期一)和 7(星期日)之间的值。\n\n# 42.一年中的日期和星期\n\n编写一个函数，在给定日期的情况下，返回一年中的某一天(闰年从 1 到 365 或 366)，并编写另一个函数，对于相同的输入，返回一年中的日历周。\n\n# 43.多个时区的会议时间\n\n编写一个函数，给出会议参与者及其时区的列表，显示每个参与者的本地会议时间。\n\n# 44.月历\n\n编写一个函数，在给定年和月的情况下，将月历打印到控制台。预期输出格式如下(示例为 2017 年 12 月):\n\n```cpp\nMon Tue Wed Thu Fri Sat Sun\n                  1   2   3\n  4   5   6   7   8   9  10\n 11  12  13  14  15  16  17\n 18  19  20  21  22  23  24\n 25  26  27  28  29  30  31\n```\n\n# 解决方法\n\n以下是上述问题解决部分的解决方案。\n\n# 39.测量功能执行时间\n\n要测量函数的执行时间，您应该在函数执行之前检索当前时间，执行函数，然后再次检索当前时间，并确定两个时间点之间经过了多少时间。为了方便起见，这些都可以放在`variadic`函数模板中，该模板将要执行的函数及其参数作为参数，并且:\n\n*   默认使用`std::high_resolution_clock`确定当前时间。\n*   使用`std::invoke()`执行要测量的函数，带有指定的参数。\n*   返回持续时间，而不是特定持续时间的刻度数。这很重要，这样你就不会失去决心。它使您能够添加各种分辨率的执行持续时间，如秒和毫秒，这是不可能通过返回滴答计数来实现的:\n\n```cpp\ntemplate <typename Time = std::chrono::microseconds,\n          typename Clock = std::chrono::high_resolution_clock>\nstruct perf_timer\n{\n   template <typename F, typename... Args>\n   static Time duration(F&& f, Args... args)\n   {\n      auto start = Clock::now();\n      std::invoke(std::forward<F>(f), std::forward<Args>(args)...);\n      auto end = Clock::now();\n\n      return std::chrono::duration_cast<Time>(end - start);\n   }\n};\n```\n\n该功能模板可以如下使用:\n\n```cpp\nvoid f() \n{ \n   // simulate work\n   std::this_thread::sleep_for(2s); \n}\n\nvoid g(int const a, int const b) \n{ \n   // simulate work\n   std::this_thread::sleep_for(1s); \n}\n\nint main()\n{\n   auto t1 = perf_timer<std::chrono::microseconds>::duration(f);\n   auto t2 = perf_timer<std::chrono::milliseconds>::duration(g, 1, 2);\n\n   auto total = std::chrono::duration<double, std::nano>(t1 + t2).count();\n}\n```\n\n# 40.两个日期之间的天数\n\n从 C++ 17 开始，`chrono`标准库不支持使用日期、星期、日历、时区和其他有用的相关功能。这将在 C++ 20 中发生变化，因为时区和日历支持已在 2018 年 3 月的杰克逊维尔会议上被添加到标准中。新增加的内容基于一个名为`date`的开源库，该库建立在`chrono`之上，由霍华德·欣南特开发，可在 https://github.com/HowardHinnant/date 的[网站上获得。我们将使用这个库来解决本章中的几个问题。虽然在这个实现中，名称空间是`date`，但是在 C++ 20 中，它将是`std::chrono`的一部分。但是，您应该能够简单地替换名称空间，而无需任何进一步的代码更改。](https://github.com/HowardHinnant/date)\n\n要解决这个任务，您可以使用`date.h `标题中的`date::sys_days`类。它代表了自`std::system_clock`时代以来的天数。这是一个有着一天决心的`time_point`，并且可以隐式转换为`std::system_clock::time_point`。基本上，你必须构造两个这种类型的对象并减去它们。结果正好是两个日期之间的天数。下面是这样一个函数的简单实现:\n\n```cpp\ninline int number_of_days(\n   int const y1, unsigned int const m1, unsigned int const d1,\n   int const y2, unsigned int const m2, unsigned int const d2)\n{\n   using namespace date;\n\n   return (sys_days{ year{ y1 } / month{ m1 } / day{ d1 } } -\n           sys_days{ year{ y2 } / month{ m2 } / day{ d2 } }).count();\n}\n\ninline int number_of_days(date::sys_days const & first,\n                          date::sys_days const & last)\n{\n   return (last - first).count();\n}\n```\n\n下面是如何使用这些重载函数的几个例子:\n\n```cpp\nint main()\n{\n   auto diff1 = number_of_days(2016, 9, 23, 2017, 5, 15);\n\n   using namespace date::literals;\n   auto diff2 = number_of_days(2016_y/sep/23, 15_d/may/2017);\n}\n```\n\n# 41.一周中的某一天\n\n如果使用`date`库，解决这个问题也相对简单。但是，这一次，您必须使用以下类型:\n\n*   `date::year_month_day`，表示一天的结构，包含年、月(1 到 12)和日(1 到 31)字段。\n*   `date::iso_week::year_weeknum_weekday`来自`iso_week.h `表头，是一个结构，有年字段、年周数字段和周天数字段(1 到 7)。这个类可以隐式转换到`date::sys_days`和从`date::sys_days`转换，这使得它可以显式转换到任何其他可以隐式转换到`date::sys_days`和从`date::year_month_day`转换的日历系统。\n\n说了这么多，问题解决了:创建一个`year_month_day`对象来表示期望的日期，然后从中创建一个`year_weeknum_weekday`对象，并使用`weekday()`检索一周中的某一天:\n\n```cpp\nunsigned int week_day(int const y, unsigned int const m, \n                      unsigned int const d)\n{\n   using namespace date;\n\n   if(m < 1 || m > 12 || d < 1 || d > 31) return 0;\n\n   auto const dt = date::year_month_day{year{ y }, month{ m }, day{ d }};\n   auto const tiso = iso_week::year_weeknum_weekday{ dt };\n\n   return (unsigned int)tiso.weekday();\n}\n\nint main()\n{\n   auto wday = week_day(2018, 5, 9);\n}\n```\n\n# 42.一年中的日期和星期\n\n这个由两部分组成的问题的解决方案应该与前面两个简单明了:\n\n*   要计算一年中的某一天，您要减去两个`date::sys_days`对象，一个表示给定的一天，另一个表示同一年的 1 月 0 日。或者，您可以从 1 月 1 日开始，并在结果中添加 1。\n*   要确定一年中的周数，构造一个`year_weeknum_weekday`对象，就像前面的问题一样，并检索`weeknum()`值:\n\n```cpp\nint day_of_year(int const y, unsigned int const m, \n                unsigned int const d)\n{\n   using namespace date;\n\n   if(m < 1 || m > 12 || d < 1 || d > 31) return 0;\n\n   return (sys_days{ year{ y } / month{ m } / day{ d } } -\n           sys_days{ year{ y } / jan / 0 }).count();\n}\n\nunsigned int calendar_week(int const y, unsigned int const m, \n                           unsigned int const d)\n{\n   using namespace date;\n\n   if(m < 1 || m > 12 || d < 1 || d > 31) return 0;\n\n   auto const dt = date::year_month_day{year{ y }, month{ m }, day{ d }};\n   auto const tiso = iso_week::year_weeknum_weekday{ dt };\n\n   return (unsigned int)tiso.weeknum();\n}\n```\n\n这些功能可以如下使用:\n\n```cpp\nint main()\n{\n   int y = 0;\n   unsigned int m = 0, d = 0;\n   std::cout << \"Year:\"; std::cin >> y;\n   std::cout << \"Month:\"; std::cin >> m;\n   std::cout << \"Day:\"; std::cin >> d;\n\n   std::cout << \"Calendar week:\" << calendar_week(y, m, d) << std::endl;\n   std::cout << \"Day of year:\" << day_of_year(y, m, d) << std::endl;\n}\n```\n\n# 43.多个时区的会议时间\n\n要使用时区，您必须使用`date`库的`tz.h`标题。但是，这需要在您的机器上下载并解压缩 *IANA 时区数据库*。\n\n以下是如何为日期库准备时区数据库:\n\n*   从[https://www.iana.org/time-zones](https://www.iana.org/time-zones)下载最新版本的数据库。目前最新的版本叫做`tzdata2017c.tar.gz`。\n*   将它解压缩到你机器上的任何位置，在一个名为`tzdata`的子目录中。假设父目录是`c:\\work\\challenges\\libs\\date`(在 Windows 机器上)；这将有一个名为`tzdata`的子目录。\n*   对于 Windows，您需要下载一个名为`windowsZones.xml`的文件，其中包含 Windows 时区到 IANA 时区的映射。这可以在[https://unicode . org/repos/cldr/trunk/common/supplicate/windowszones . XML](https://unicode.org/repos/cldr/trunk/common/supplemental/windowsZones.xml)上找到。文件必须存储在之前创建的同一个`tzdata`子目录中。\n*   在您的项目设置中，定义一个名为`INSTALL`的预处理器宏，该宏指示`tzdata`子目录的父目录。这里举的例子，你应该有`INSTALL=c:\\\\work\\\\challenges\\\\libs\\\\date`。(请注意，双反斜杠是必要的，因为宏用于使用字符串化和串联创建文件路径，否则会导致路径不正确。)\n\n为了解决这个问题，我们将考虑具有最少信息的用户结构，例如姓名和时区。时区是使用`date::locate_zone()`功能创建的:\n\n```cpp\nstruct user\n{\n   std::string Name;\n   date::time_zone const * Zone;\n\n   explicit user(std::string_view name, std::string_view zone)\n      : Name{name.data()}, Zone(date::locate_zone(zone.data()))\n   {}\n};\n```\n\n显示用户列表及其会议开始的本地时间的函数应该将给定时间从参考区域转换为他们自己区域的时间。为此，我们可以使用`date::zoned_time`类的转换构造函数:\n\n```cpp\ntemplate <class Duration, class TimeZonePtr>\nvoid print_meeting_times(\n   date::zoned_time<Duration, TimeZonePtr> const & time,\n   std::vector<user> const & users)\n{\n   std::cout \n      << std::left << std::setw(15) << std::setfill(' ')\n      << \"Local time: \" \n      << time << std::endl;\n\n   for (auto const & user : users)\n   {\n      std::cout\n         << std::left << std::setw(15) << std::setfill(' ')\n         << user.Name\n         << date::zoned_time<Duration, TimeZonePtr>(user.Zone, time) \n         << std::endl;\n   }\n}\n```\n\n该功能可以如下使用，其中给定时间(小时和分钟)以当前时区表示:\n\n```cpp\nint main()\n{\n   std::vector<user> users{\n      user{ \"Ildiko\", \"Europe/Budapest\" },\n      user{ \"Jens\", \"Europe/Berlin\" },\n      user{ \"Jane\", \"America/New_York\" }\n   };\n\n   unsigned int h, m;\n   std::cout << \"Hour:\"; std::cin >> h;\n   std::cout << \"Minutes:\"; std::cin >> m;\n\n   date::year_month_day today = \n      date::floor<date::days>(ch::system_clock::now());\n\n   auto localtime = date::zoned_time<std::chrono::minutes>(\n      date::current_zone(), \n      static_cast<date::local_days>(today)+ch::hours{h}+ch::minutes{m});\n\n   print_meeting_times(localtime, users);\n}\n```\n\n# 44.月历\n\n解决这个任务实际上是部分基于前面的任务。为了打印问题中指出的月份，您应该知道:\n\n*   一个月的第一天是星期几。这可以使用为前一个问题创建的`week_day()`函数来确定。\n*   一个月的天数。这可以通过使用`date::year_month_day_last`结构并检索`day()`的值来确定。\n\n首先确定这些信息后，您应该:\n\n*   打印第一个工作日前第一周的空值\n*   使用从 1 到每月最后一天的正确格式打印日期\n*   每隔七天换一条新的线(从第一周的第一天开始计算，尽管这可能属于前一个月)\n\n这里显示了所有这些的实现:\n\n```cpp\nunsigned int week_day(int const y, unsigned int const m, \n                      unsigned int const d)\n{\n   using namespace date;\n\n   if(m < 1 || m > 12 || d < 1 || d > 31) return 0;\n\n   auto const dt = date::year_month_day{year{ y }, month{ m }, day{ d }};\n   auto const tiso = iso_week::year_weeknum_weekday{ dt };\n\n   return (unsigned int)tiso.weekday();\n}\n\nvoid print_month_calendar(int const y, unsigned int m)\n{\n   using namespace date;\n   std::cout << \"Mon Tue Wed Thu Fri Sat Sun\" << std::endl;\n\n   auto first_day_weekday = week_day(y, m, 1);\n   auto last_day = (unsigned int)year_month_day_last(\n      year{ y }, month_day_last{ month{ m } }).day();\n\n   unsigned int index = 1;\n   for (unsigned int day = 1; day < first_day_weekday; ++ day, ++ index)\n   {\n      std::cout << \" \";\n   }\n\n   for (unsigned int day = 1; day <= last_day; ++ day)\n   {\n      std::cout << std::right << std::setfill(' ') << std::setw(3)\n                << day << ' ';\n      if (index++ % 7 == 0) std::cout << std::endl;\n   }\n\n   std::cout << std::endl;\n}\n\nint main()\n{\n   print_month_calendar(2017, 12);\n}\n```"
  },
  {
    "path": "docs/mod-cpp/17.md",
    "content": "# 十七、算法和数据结构\n\n# 问题\n\n这是本章的解题部分。\n\n# 45.优先队列\n\n编写一个表示优先级队列的数据结构，该队列为最大的元素提供恒定的时间查找，但对于添加和移除元素具有对数时间复杂度。队列在末尾插入新元素，并从顶部移除元素。默认情况下，队列应该使用`operator<`来比较元素，但是如果第一个参数小于第二个参数，用户应该可以提供一个返回`true`的比较函数对象。实现必须至少提供以下操作:\n\n*   `push()`添加新元素\n*   `pop()`移除顶部元素\n*   `top()`提供对顶部元素的访问\n*   `size()`表示队列中元素的数量\n*   `empty()`表示队列是否为空\n\n# 46.循环缓冲器\n\n创建表示固定大小的循环缓冲区的数据结构。当循环缓冲区的填充超出其固定大小时，循环缓冲区会覆盖现有元素。您必须编写的类应该:\n\n*   禁止默认构造\n*   支持创建指定大小的对象\n*   允许检查缓冲容量和状态(`empty()`、`full()`、`size()`、`capacity()`)\n*   添加新元素，这一操作可能会覆盖缓冲区中最旧的元素\n*   从缓冲区中移除最旧的元素\n*   通过其元素支持迭代\n\n# 47.双缓冲器\n\n编写一个类，该类表示一个缓冲区，可以在两个操作不冲突的情况下同时写入和读取该缓冲区。读操作必须在写操作进行时提供对旧数据的访问。写操作完成后，新写入的数据必须可供读取。\n\n# 48.范围内最常见的元素\n\n编写一个函数，在给定的范围内，返回最频繁的元素及其在该范围内出现的次数。如果多个元素出现相同的最大次数，那么函数应该返回所有元素。比如范围`{1,1,3,5,8,13,3,5,8,8,5}`，应该返回`{5, 3}`和`{8, 3}`。\n\n# 49.文本直方图\n\n写一个程序，给定一个文本，用字母表中每个字母的频率确定并打印一个直方图。频率是每个字母出现的次数占字母总数的百分比。程序应该只计算字母的外观，而忽略数字、符号和其他可能的字符。频率必须根据字母数量而不是文本大小来确定。\n\n# 50.过滤电话号码列表\n\n编写一个函数，给定一个电话号码列表，只返回来自指定国家的号码。该国家由其电话国家代码表示，例如英国为 44。电话号码可能以国家代码开头，一个`+`后跟国家代码，或者没有国家代码。最后一类的必须忽略。\n\n# 51.转换电话号码列表\n\n编写一个函数，给定一个电话号码列表，转换它们，使它们都以一个指定的电话国家代码开始，前面有`+`符号。电话号码中的任何空格也应该删除。以下是输入和输出示例列表:\n\n```cpp\n07555 123456    => +447555123456\n07555123456     => +447555123456\n+44 7555 123456 => +447555123456\n44 7555 123456  => +447555123456\n7555 123456     => +447555123456\n```\n\n# 52.生成字符串的所有排列\n\n编写一个函数，在控制台上打印给定字符串的所有可能排列。您应该提供这个函数的两个版本:一个使用递归，一个不使用。\n\n# 53.电影的平均评分\n\n编写一个程序，计算并打印电影列表的平均评分。每部电影都有一个从 1 到 10 的分级列表(其中 1 是最低分级，10 是最高分级)。为了计算评分，您必须先移除最高和最低评分的 5%，然后再计算它们的平均值。结果必须以单个小数点显示。\n\n# 54.成对算法\n\n编写一个通用函数，在给定一个范围的情况下，从输入范围中返回一个包含成对连续元素的新范围。如果输入范围有奇数个元素，最后一个必须忽略。例如，如果输入范围是`{1, 1, 3, 5, 8, 13, 21}`，结果必须是`{ {1, 1}, {3, 5}, {8, 13}}`。\n\n# 55.Zip 算法\n\n编写一个函数，在给定两个范围的情况下，用这两个范围中的元素对返回一个新的范围。如果两个范围大小不同，结果必须包含与最小输入范围一样多的元素。例如，如果输入范围是`{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }`和`{ 1, 1, 3, 5, 8, 13, 21 }`，结果应该是`{{1,1}, {2,1}, {3,3}, {4,5}, {5,8}, {6,13}, {7,21}}`。\n\n# 56.选择算法\n\n编写一个函数，在给定一个值范围和一个投影函数的情况下，将每个值转换成一个新值，并返回一个包含所选值的新范围。例如，如果您有一个具有`id`、`title`和`author`的活字本，并且有一系列这样的书籍值，那么该功能应该可以只选择书籍的标题。以下是如何使用该函数的示例:\n\n```cpp\nstruct book\n{\n   int         id;\n   std::string title;\n   std::string author;\n};\n\nstd::vector<book> books{\n   {101, \"The C++ Programming Language\", \"Bjarne Stroustrup\"},\n   {203, \"Effective Modern C++\", \"Scott Meyers\"},\n   {404, \"The Modern C++ Programming Cookbook\", \"Marius Bancila\"}};\n\nauto titles = select(books, [](book const & b) {return b.title; });\n```\n\n# 57.分类算法\n\n编写一个函数，在给定一对随机访问迭代器来定义其上下界的情况下，使用 quicksort 算法对范围内的元素进行排序。排序函数应该有两个重载:一个使用`operator<`比较范围的元素并按升序排列，另一个使用用户定义的二进制比较函数比较元素。\n\n# 58.节点之间的最短路径\n\n编写一个程序，给定一个节点网络和它们之间的距离，计算并显示从一个指定节点到所有其他节点的最短距离，以及开始和结束节点之间的路径。作为输入，考虑以下无向图:\n\n![](img/2fe0878c-3372-40bf-a988-81c9d47ab199.png)\n\n该图的程序输出应该如下:\n\n```cpp\nA -> A : 0     A\nA -> B : 7     A -> B\nA -> C : 9     A -> C\nA -> D : 20    A -> C -> D\nA -> E : 20    A -> C -> F -> E\nA -> F : 11    A -> C -> F\n```\n\n# 59.黄鼠狼计划\n\n编写一个程序，实现理查德·道金斯的黄鼠狼计算机模拟，用道金斯的话描述如下(*盲人制表师*，第 3 章):\n\nWe again use our computer monkey, but with a crucial difference in its program. It again begins by choosing a random sequence of 28 letters, just as before ... it duplicates it repeatedly, but with a certain chance of random error – 'mutation' – in the copying. The computer examines the mutant nonsense phrases, the 'progeny' of the original phrase, and chooses the one which, however slightly, most resembles the target phrase, METHINKS IT IS LIKE A WEASEL.\n\n# 60.生活的游戏\n\n写一个程序实现*约翰·何顿·康威*提出的*生命游戏*元胞自动机。这个游戏的世界是一个正方形格子，可以有两种状态:死亡或活着。每个单元都与其相邻的单元进行交互，每一步都会发生以下事务:\n\n*   任何少于两个活邻居的活细胞都会死亡，好像是人口不足造成的\n*   任何有两个或三个活邻居的活细胞都会延续到下一代\n*   任何有三个以上活邻居的活细胞都会死亡，就像人口过剩一样\n*   任何正好有三个活邻居的死细胞都会变成活细胞，就像通过繁殖一样\n\n每次迭代的游戏状态都应该显示在控制台上，为了方便，你应该选择一个合理的大小，比如 20 行 x 50 列。\n\n# 解决方法\n\n以下是上述问题解决部分的解决方案。\n\n# 45.优先队列\n\n优先级队列是一种抽象数据类型，其元素有一个优先级。优先级队列不是作为先进先出容器工作，而是按照元素的优先级顺序使元素可用。这种数据结构被用在诸如 Dijkstra 最短路径、Prim 算法、堆排序、A*搜索算法、用于数据压缩的霍夫曼码等算法中。\n\n实现优先级队列的一个非常简单的方法是使用`std::vector`作为元素的底层容器，并始终保持其排序。这意味着最大和最小元素总是在两端。然而，这种方法不能提供最有效的操作。\n\n最适合用来实现优先级队列的数据结构是堆。这是一个基于树的数据结构，满足以下属性:如果 *P* 是 *C* 的父节点，那么 *P* 的键(值)大于或等于(在最大堆中)或小于或等于(在最小堆中)C*的键。*\n\n *标准库提供了几种处理堆的操作:\n\n*   `std::make_heap()`:这将为给定的范围创建一个最大堆，使用`operator<`或用户提供的比较函数对元素进行排序\n*   `std::push_heap()`:这将在最大堆的末尾插入一个新元素\n*   `std::pop_heap()`:这将移除堆的第一个元素(通过交换第一个和最后一个位置的值，并使子范围`[first, last-1)`成为最大堆)\n\n使用`std::vector`保存数据和堆的标准函数的优先级队列实现可以如下所示:\n\n```cpp\ntemplate <class T,\n   class Compare = std::less<typename std::vector<T>::value_type>>\nclass priority_queue\n{\n   typedef typename std::vector<T>::value_type value_type;\n   typedef typename std::vector<T>::size_type size_type;\n   typedef typename std::vector<T>::reference reference;\n   typedef typename std::vector<T>::const_reference const_reference;\npublic:\n   bool empty() const noexcept { return data.empty(); }\n   size_type size() const noexcept { return data.size(); }\n\n   void push(value_type const & value)\n   {\n      data.push_back(value);\n      std::push_heap(std::begin(data), std::end(data), comparer);\n   }\n\n   void pop()\n   {\n      std::pop_heap(std::begin(data), std::end(data), comparer);\n      data.pop_back();\n   }\n\n   const_reference top() const { return data.front(); }\n\n   void swap(priority_queue& other) noexcept\n   {\n      swap(data, other.data);\n      swap(comparer, other.comparer);\n   }\nprivate:\n   std::vector<T> data;\n   Compare comparer;\n};\n\ntemplate<class T, class Compare>\nvoid swap(priority_queue<T, Compare>& lhs,\n          priority_queue<T, Compare>& rhs) \nnoexcept(noexcept(lhs.swap(rhs)))\n{\n   lhs.swap(rhs);\n}\n```\n\n这个类可以如下使用:\n\n```cpp\nint main()\n{\n   priority_queue<int> q;\n   for (int i : {1, 5, 3, 1, 13, 21, 8})\n   {\n      q.push(i);\n   }\n\n   assert(!q.empty());\n   assert(q.size() == 7);\n\n   while (!q.empty())\n   {\n      std::cout << q.top() << ' ';\n      q.pop();\n   }\n}\n```\n\n# 46.循环缓冲器\n\n循环缓冲区是一个固定大小的容器，其行为就像它的两端被连接起来形成一个虚拟的循环内存布局。它的主要好处是，您不需要大量内存来保留数据，因为旧条目会被新条目覆盖。循环缓冲区用于输入/输出缓冲、有界日志记录(当您只想保留最后的消息时)、异步处理缓冲区等。\n\n我们可以区分两种情况:\n\n1.  添加到缓冲区的元素数量尚未达到其容量(用户定义的固定大小)。在这种情况下，它的行为就像一个常规容器，比如一个向量。\n\n2.  添加到缓冲区的元素数量已达到并超过其容量。在这种情况下，缓冲区的内存被重用，旧的元素被覆盖。\n\n我们可以使用以下方式来表示这样的结构:\n\n*   具有预先分配的元素数量的常规容器\n*   一个头指针，用于指示最后插入的元素的位置\n*   指示容器中元素数量的大小计数器，不能超过其容量(因为在这种情况下元素将被覆盖)\n\n循环缓冲区的两个主要操作是:\n\n*   向缓冲区添加新元素。我们总是在头指针(或索引)的下一个位置插入。这就是下图所示的`push()`方法。\n*   从缓冲区中移除现有元素。我们总是移除最老的元素。该元素位于位置`head - size`(这必须说明索引的循环性质)。这就是下图所示的`pop()`方法。\n\n这种数据结构的实现如下所示:\n\n```cpp\ntemplate <class T>\nclass circular_buffer\n{\n   typedef circular_buffer_iterator<T> const_iterator;\n\n   circular_buffer() = delete;\npublic:\n   explicit circular_buffer(size_t const size) :data_(size)\n   {}\n\n   bool clear() noexcept { head_ = -1; size_ = 0; }\n   bool empty() const noexcept { return size_ == 0; }\n   bool full() const noexcept { return size_ == data_.size(); }\n   size_t capacity() const noexcept { return data_.size(); }\n   size_t size() const noexcept { return size_; }\n\n   void push(T const item)\n   {\n      head_ = next_pos();\n      data_[head_] = item;\n      if (size_ < data_.size()) size_++ ;\n   }\n\n   T pop()\n   {\n      if (empty()) throw std::runtime_error(\"empty buffer\");\n      auto pos = first_pos();\n      size_--;\n      return data_[pos];\n   }\n\n   const_iterator begin() const\n   {\n      return const_iterator(*this, first_pos(), empty());\n   }\n\n   const_iterator end() const\n   {\n      return const_iterator(*this, next_pos(), true);\n   }\n\nprivate:\n   std::vector<T> data_;\n   size_t head_ = -1;\n   size_t size_ = 0;\n\n   size_t next_pos() const noexcept \n   { return size_ == 0 ? 0 : (head_ + 1) % data_.size(); }\n   size_t first_pos() const noexcept \n   { return size_ == 0 ? 0 : (head_ + data_.size() - size_ + 1) % \n                             data_.size(); }\n\n   friend class circular_buffer_iterator<T>;\n};\n```\n\n由于映射在连续内存布局上的索引的循环特性，这个类的迭代器类型不能是指针类型。迭代器必须能够通过对索引应用模运算来指向元素。这是一个迭代器的可能实现:\n\n```cpp\ntemplate <class T>\nclass circular_buffer_iterator\n{\n   typedef circular_buffer_iterator        self_type;\n   typedef T                               value_type;\n   typedef T&                              reference;\n   typedef T const&                        const_reference;\n   typedef T*                              pointer;\n   typedef std::random_access_iterator_tag iterator_category;\n   typedef ptrdiff_t                       difference_type;\npublic:\n   circular_buffer_iterator(circular_buffer<T> const & buf, \n                            size_t const pos, bool const last) :\n   buffer_(buf), index_(pos), last_(last)\n   {}\n\n   self_type & operator++ ()\n   {\n      if (last_)\n         throw std::out_of_range(\"Iterator cannot be incremented past the end of range.\");\n      index_ = (index_ + 1) % buffer_.data_.size();\n      last_ = index_ == buffer_.next_pos();\n      return *this;\n   }\n\n   self_type operator++ (int)\n   {\n      self_type tmp = *this;\n      ++*this;\n      return tmp;\n   }\n\n   bool operator== (self_type const & other) const\n   {\n      assert(compatible(other));\n      return index_ == other.index_ && last_ == other.last_;\n   }\n\n   bool operator!= (self_type const & other) const\n   {\n      return !(*this == other);\n   }\n\n   const_reference operator* () const\n   {\n      return buffer_.data_[index_];\n   }\n\n   const_reference operator-> () const\n   {\n      return buffer_.data_[index_];\n   }\nprivate:\n   bool compatible(self_type const & other) const\n   {\n      return &buffer_ == &other.buffer_;\n   }\n\n   circular_buffer<T> const & buffer_;\n   size_t index_;\n   bool last_;\n};\n```\n\n实现所有这些之后，我们可以编写如下代码。请注意，在注释中，第一个范围显示了内部向量的实际内容，第二个范围显示了迭代器访问公开的逻辑内容:\n\n```cpp\nint main()\n{\n   circular_buffer<int> cbuf(5); // {0, 0, 0, 0, 0} -> {}\n\n   cbuf.push(1);                 // {1, 0, 0, 0, 0} -> {1}\n   cbuf.push(2);                 // {1, 2, 0, 0, 0} -> {1, 2}\n   cbuf.push(3);                 // {1, 2, 3, 0, 0} -> {1, 2, 3}\n\n   auto item = cbuf.pop();       // {1, 2, 3, 0, 0} -> {2, 3}\n   cbuf.push(4);                 // {1, 2, 3, 4, 0} -> {2, 3, 4}\n   cbuf.push(5);                 // {1, 2, 3, 4, 5} -> {2, 3, 4, 5}\n   cbuf.push(6);                 // {6, 2, 3, 4, 5} -> {2, 3, 4, 5, 6}\n\n   cbuf.push(7);                 // {6, 7, 3, 4, 5} -> {3, 4, 5, 6, 7}\n   cbuf.push(8);                 // {6, 7, 8, 4, 5} -> {4, 5, 6, 7, 8}\n\n   item = cbuf.pop();            // {6, 7, 8, 4, 5} -> {5, 6, 7, 8}\n   item = cbuf.pop();            // {6, 7, 8, 4, 5} -> {6, 7, 8}\n   item = cbuf.pop();            // {6, 7, 8, 4, 5} -> {7, 8}\n\n   item = cbuf.pop();            // {6, 7, 8, 4, 5} -> {8}\n   item = cbuf.pop();            // {6, 7, 8, 4, 5} -> {}\n\n   cbuf.push(9);                 // {6, 7, 8, 9, 5} -> {9}\n}\n```\n\n# 47.双缓冲器\n\n这里描述的问题是典型的双缓冲情况。双缓冲是多缓冲最常见的情况，这是一种允许读者看到数据的完整版本而不是由作者产生的部分更新版本的技术。这是一种避免闪烁的常用技术，尤其是在计算机图形学中。\n\n为了实现所请求的功能，我们应该写入的缓冲区类必须有两个内部缓冲区:一个包含正在写入的临时数据，另一个包含已完成(或已提交)的数据。写操作完成后，临时缓冲区的内容被写入主缓冲区。对于内部缓冲区，下面的实现使用`std::vector`。当写操作完成时，我们只交换两个缓冲区的内容，而不是将数据从一个缓冲区复制到另一个缓冲区，这是一个更快的操作。对完整数据的访问提供了`read()`功能，该功能将读取缓冲区的内容复制到指定的输出，或者提供了直接元素访问(过载的`operator[]`)。对读缓冲区的访问与`std::mutex`同步，以确保在一个线程向缓冲区写入数据时从另一个线程读取数据是安全的:\n\n```cpp\ntemplate <typename T>\nclass double_buffer\n{\n   typedef T           value_type;\n   typedef T&          reference;\n   typedef T const &   const_reference;\n   typedef T*          pointer;\npublic:\n   explicit double_buffer(size_t const size) :\n      rdbuf(size), wrbuf(size)\n   {}\n\n   size_t size() const noexcept { return rdbuf.size(); }\n\n   void write(T const * const ptr, size_t const size)\n   {\n      std::unique_lock<std::mutex> lock(mt);\n      auto length = std::min(size, wrbuf.size());\n      std::copy(ptr, ptr + length, std::begin(wrbuf));\n      wrbuf.swap(rdbuf);\n   }\n\n   template <class Output>\n   void read(Output it) const\n   {\n      std::unique_lock<std::mutex> lock(mt);\n      std::copy(std::cbegin(rdbuf), std::cend(rdbuf), it);\n   }\n\n   pointer data() const\n   {\n       std::unique_lock<std::mutex> lock(mt);\n       return rdbuf.data();\n   }\n\n   reference operator[](size_t const pos)\n   {\n      std::unique_lock<std::mutex> lock(mt);\n      return rdbuf[pos];\n   }\n\n   const_reference operator[](size_t const pos) const\n   {\n      std::unique_lock<std::mutex> lock(mt);\n      return rdbuf[pos];\n   }\n\n   void swap(double_buffer other)\n   {\n      std::swap(rdbuf, other.rdbuf);\n      std::swap(wrbuf, other.wrbuf);\n   }\n\nprivate:\n   std::vector<T>     rdbuf;\n   std::vector<T>     wrbuf;\n   mutable std::mutex mt;\n};\n```\n\n下面是这个双缓冲区类如何用于两个不同实体的写入和读取的示例:\n\n```cpp\ntemplate <typename T>\nvoid print_buffer(double_buffer<T> const & buf)\n{\n   buf.read(std::ostream_iterator<T>(std::cout, \" \"));\n   std::cout << std::endl;\n}\n\nint main()\n{\n   double_buffer<int> buf(10);\n\n   std::thread t([&buf]() {\n      for (int i = 1; i < 1000; i += 10)\n      {\n         int data[] = { i, i + 1, i + 2, i + 3, i + 4, \n                        i + 5, i + 6,i + 7,i + 8,i + 9 };\n         buf.write(data, 10);\n\n         using namespace std::chrono_literals;\n         std::this_thread::sleep_for(100ms);\n       }\n   });\n\n   auto start = std::chrono::system_clock::now();\n   do\n   {\n      print_buffer(buf);\n\n      using namespace std::chrono_literals;\n      std::this_thread::sleep_for(150ms);\n   } while (std::chrono::duration_cast<std::chrono::seconds>(\n            std::chrono::system_clock::now() - start).count() < 12);\n\n   t.join();\n}\n```\n\n# 48.范围内最常见的元素\n\n为了确定并返回一个范围内最频繁的元素，您应该执行以下操作:\n\n*   在`std::map`中计算每个元素的外观。关键是元素，值是它的出现次数。\n*   使用`std::max_element()`确定地图的最大元素。结果是一个地图元素，即一对包含该元素及其出现次数的元素。\n\n*   复制所有值(外观计数)等于最大元素值的地图元素，并将其作为最终结果返回。\n\n下面的列表显示了前面描述的步骤的实现:\n\n```cpp\ntemplate <typename T>\nstd::vector<std::pair<T, size_t>> find_most_frequent(\n   std::vector<T> const & range)\n{\n   std::map<T, size_t> counts;\n   for (auto const & e : range) counts[e]++ ;\n\n   auto maxelem = std::max_element(\n      std::cbegin(counts), std::cend(counts),\n      [](auto const & e1, auto const & e2) {\n         return e1.second < e2.second;\n   });\n\n   std::vector<std::pair<T, size_t>> result;\n\n   std::copy_if(\n      std::begin(counts), std::end(counts),\n      std::back_inserter(result),\n      [maxelem](auto const & kvp) {\n         return kvp.second == maxelem->second;\n   });\n\n   return result;\n}\n```\n\n`find_most_frequent()`功能可以如下使用:\n\n```cpp\nint main()\n{\n   auto range = std::vector<int>{1,1,3,5,8,13,3,5,8,8,5};\n   auto result = find_most_frequent(range);\n\n   for (auto const & e : result)\n   {\n      std::cout << e.first << \" : \" << e.second << std::endl;\n   }\n}\n```\n\n# 49.文本直方图\n\n直方图是数字数据分布的表示。广为人知的直方图是用于摄影和图像处理的颜色和图像直方图。如这里所述，文本直方图是给定文本中字母出现频率的表示。这个问题部分类似于前面的问题，只是范围元素现在是字符，我们必须确定它们的频率。要解决这个问题，您应该:\n\n*   用地图统计每个字母的外观。关键是字母，数值是它的出现次数。\n*   计数时，忽略所有不是字母的字符。大写和小写字符必须视为相同，因为它们代表相同的字母。\n*   使用`std::accumulate()`统计给定文本中所有字母的出现总数。\n*   使用`std::for_each()`或基于范围的`for`循环遍历地图的所有元素，并将外观计数转换为频率。\n\n以下是问题的可能实现:\n\n```cpp\nstd::map<char, double> analyze_text(std::string_view text)\n{\n   std::map<char, double> frequencies;\n   for (char ch = 'a'; ch <= 'z'; ch++)\n      frequencies[ch] = 0;\n\n   for (auto ch : text)\n   {\n      if (isalpha(ch))\n         frequencies[tolower(ch)]++ ;\n   }\n\n   auto total = std::accumulate(\n      std::cbegin(frequencies), std::cend(frequencies),\n      0ull,\n      [](auto sum, auto const & kvp) {\n         return sum + static_cast<unsigned long long>(kvp.second);\n   });\n\n   std::for_each(\n      std::begin(frequencies), std::end(frequencies),\n      [total](auto & kvp) {\n         kvp.second = (100.0 * kvp.second) / total;\n   });\n\n   return frequencies;\n}\n```\n\n以下程序在控制台上打印文本中字母的频率:\n\n```cpp\nint main()\n{\n   auto result = analyze_text(R\"(Lorem ipsum dolor sit amet, consectetur \n      adipiscing elit, sed do eiusmod tempor incididunt ut labore et \n      dolore magna aliqua.)\");\n\n   for (auto const & kvp : result)\n   {\n      std::cout << kvp.first << \" : \"\n                << std::fixed\n                << std::setw(5) << std::setfill(' ')\n                << std::setprecision(2) << kvp.second << std::endl;\n   }\n}\n```\n\n# 50.过滤电话号码列表\n\n这个问题的解决方案相对简单:你必须遍历所有的电话号码，并将以国家代码开头的电话号码复制到一个单独的容器(如`std::vector`)中。例如，如果指定的国家代码是 44，则必须同时检查 44 和+44。使用`std::copy_if()`功能可以以这种方式过滤输入范围。此处显示了此问题的解决方案:\n\n```cpp\nbool starts_with(std::string_view str, std::string_view prefix)\n{\n   return str.find(prefix) == 0;\n}\n\ntemplate <typename InputIt>\nstd::vector<std::string> filter_numbers(InputIt begin, InputIt end,\n                                        std::string const & countryCode)\n{\n   std::vector<std::string> result;\n   std::copy_if(\n      begin, end,\n      std::back_inserter(result),\n      [countryCode](auto const & number) {\n         return starts_with(number, countryCode) ||\n                starts_with(number, \"+\" + countryCode);\n   });\n   return result;\n}\n\nstd::vector<std::string> filter_numbers(\n   std::vector<std::string> const & numbers,\n   std::string const & countryCode)\n{\n   return filter_numbers(std::cbegin(numbers), std::cend(numbers), \n                         countryCode);\n}\n```\n\n这是该功能的使用方法:\n\n```cpp\nint main()\n{\n   std::vector<std::string> numbers{\n      \"+40744909080\",\n      \"44 7520 112233\",\n      \"+44 7555 123456\",\n      \"40 7200 123456\",\n      \"7555 123456\"\n   };\n\n   auto result = filter_numbers(numbers, \"44\");\n\n   for (auto const & number : result)\n   {\n      std::cout << number << std::endl;\n   }\n}\n```\n\n# 51.转换电话号码列表\n\n这个问题在某些方面与前一个有些相似。但是，我们不能选择以指定国家代码开头的电话号码，而是必须转换每个号码，使它们都以前面带有`+`的国家代码开头。有几种情况必须考虑:\n\n*   电话号码以 0 开头。表示没有国家代码的数字。要修改数字以包括国家代码，我们必须用实际的国家代码替换 0，前面加`+`。\n*   电话号码以国家代码开头。在这种情况下，我们只需在开头加上`+`符号。\n*   电话号码以`+`开头，后面跟着国家代码。在这种情况下，数字已经是预期的格式。\n*   这些情况都不适用，因此结果是通过将`+`前面的国家代码和电话号码连接起来获得的。\n\nFor simplicity, we will ignore the possibility that the number is actually prefixed with another country code. You can take it as a further exercise to modify the implementation so that it can handle phone numbers with a different country prefix. These numbers should be removed from the list.\n\n在上述所有情况下，数字都可能包含空格。根据要求，这些必须拆除。`std::remove_if()`和`isspace()`功能用于此目的。\n\n以下是所述解决方案的实现:\n\n```cpp\nbool starts_with(std::string_view str, std::string_view prefix)\n{\n   return str.find(prefix) == 0;\n}\n\nvoid normalize_phone_numbers(std::vector<std::string>& numbers,\n                             std::string const & countryCode)\n{\n   std::transform(\n      std::cbegin(numbers), std::cend(numbers),\n      std::begin(numbers),\n      [countryCode](std::string const & number) {\n         std::string result;\n         if (number.size() > 0)\n         {\n            if (number[0] == '0')\n               result = \"+\" + countryCode + \n                        number.substr(1);\n            else if (starts_with(number, countryCode))\n               result = \"+\" + number;\n            else if (starts_with(number, \"+\" + countryCode))\n               result = number;\n            else\n               result = \"+\" + countryCode + number;\n      }\n\n      result.erase(\n         std::remove_if(std::begin(result), std::end(result),\n            [](const char ch) {return isspace(ch); }),\n         std::end(result));\n\n      return result;\n   });\n}\n```\n\n以下程序根据要求对给定的电话号码列表进行标准化，并将其打印在控制台上:\n\n```cpp\nint main()\n{\n   std::vector<std::string> numbers{\n      \"07555 123456\",\n      \"07555123456\",\n      \"+44 7555 123456\",\n      \"44 7555 123456\",\n      \"7555 123456\"\n   };\n\n   normalize_phone_numbers(numbers, \"44\");\n\n   for (auto const & number : numbers)\n   {\n      std::cout << number << std::endl;\n   }\n}\n```\n\n# 52.生成字符串的所有排列\n\n您可以通过利用标准库中的一些通用算法来解决这个问题。两个必选版本中最简单的就是非递归版本，至少在你使用`std::next_permutation()`的时候是这样。该函数将输入范围(需要排序)转换为所有可能排列集合中的下一个排列，按照`operator<`或指定的比较函数对象的字典顺序排列。如果这样的排列存在，那么它返回`true`，否则，它将范围转换为第一个排列并返回`false`。因此，基于`std::next_permuation()`的非递归实现如下所示:\n\n```cpp\nvoid print_permutations(std::string str)\n{\n   std::sort(std::begin(str), std::end(str));\n\n   do\n   {\n      std::cout << str << std::endl;\n   } while (std::next_permutation(std::begin(str), std::end(str)));\n}\n```\n\n递归的选择稍微复杂一点。实现它的一种方法是有一个输入和输出字符串；最初，输入字符串是我们想要为其生成置换的字符串，而输出字符串是空的。我们每次从输入字符串中提取一个字符，并将其放入输出字符串中。当输入字符串为空时，输出字符串代表下一个置换。执行此操作的递归算法如下:\n\n*   如果输入字符串为空，则打印输出字符串并返回\n*   否则，迭代输入字符串中的所有字符，并且对于每个元素:\n    *   通过从输入字符串中移除第一个字符并将其连接到输出字符串的末尾来递归调用方法\n    *   旋转输入字符串，使第一个字符成为最后一个，第二个字符成为第一个，依此类推\n\n下图直观地解释了该算法:\n\n![](img/bbcdafcd-a3e0-4a0c-9b79-9f465a5a4602.png)\n\n为了旋转输入字符串，我们可以使用标准库函数`std::rotate()`，它对一系列元素执行向左旋转。所描述的递归算法的实现如下所示:\n\n```cpp\nvoid next_permutation(std::string str, std::string perm)\n{\n   if (str.empty()) std::cout << perm << std::endl;\n   else\n   {\n      for (size_t i = 0; i < str.size(); ++ i)\n      {\n         next_permutation(str.substr(1), perm + str[0]);\n\n         std::rotate(std::begin(str), std::begin(str) + 1, std::end(str));\n      }\n   }\n}\n\nvoid print_permutations_recursive(std::string str)\n{\n   next_permutation(str, \"\");\n}\n```\n\n这就是这两种实现的使用方式:\n\n```cpp\nint main()\n{\n   std::cout << \"non-recursive version\" << std::endl;\n   print_permutations(\"main\");\n\n   std::cout << \"recursive version\" << std::endl;\n   print_permutations_recursive(\"main\");\n}\n```\n\n# 53.电影的平均评分\n\n这个问题需要使用截断平均值来计算电影等级。这是对中心趋势的统计测量，其中平均值是在丢弃概率分布或样本的高端和低端部分后计算的。通常，这是通过在两端移除等量的点来完成的。对于此问题，您需要删除最高和最低用户评分的 5%。\n\n计算给定范围的截断平均值的函数应执行以下操作:\n\n*   对范围进行排序，以便对元素进行排序(升序或降序)\n*   移除两端所需的元素百分比\n*   计算所有剩余元素的总和\n*   通过将总和除以元素的剩余数量来计算平均值\n\n这里显示的`truncated_mean()`功能实现了所描述的算法:\n\n```cpp\ndouble truncated_mean(std::vector<int> values, double const percentage)\n{\n   std::sort(std::begin(values), std::end(values));\n   auto remove_count = static_cast<size_t>(\n                          values.size() * percentage + 0.5);\n\n   values.erase(std::begin(values), std::begin(values) + remove_count);\n   values.erase(std::end(values) - remove_count, std::end(values));\n\n   auto total = std::accumulate(\n      std::cbegin(values), std::cend(values),\n      0ull,\n      [](auto const sum, auto const e) {\n         return sum + e; });\n   return static_cast<double>(total) / values.size();\n}\n```\n\n使用此功能来计算和打印电影平均评分的程序可能如下所示:\n\n```cpp\nstruct movie\n{\n   int              id;\n   std::string      title;\n   std::vector<int> ratings;\n};\n\nvoid print_movie_ratings(std::vector<movie> const & movies)\n{\n   for (auto const & m : movies)\n   {\n      std::cout << m.title << \" : \" \n                << std::fixed << std::setprecision(1)\n                << truncated_mean(m.ratings, 0.05) << std::endl;\n   }\n}\n\nint main()\n{\n   std::vector<movie> movies\n   {\n      { 101, \"The Matrix\", {10, 9, 10, 9, 9, 8, 7, 10, 5, 9, 9, 8} },\n      { 102, \"Gladiator\", {10, 5, 7, 8, 9, 8, 9, 10, 10, 5, 9, 8, 10} },\n      { 103, \"Interstellar\", {10, 10, 10, 9, 3, 8, 8, 9, 6, 4, 7, 10} }\n   };\n\n   print_movie_ratings(movies);\n}\n```\n\n# 54.成对算法\n\n针对该问题提出的成对函数必须将输入范围的相邻元素配对，并产生添加到输出范围的`std::pair`元素。下面的代码清单提供了两种实现:\n\n*   一个以迭代器为参数的通用函数模板:开始和结束迭代器定义输入范围，输出迭代器定义输出范围中要插入结果的位置\n*   以`std::vector<T>`作为输入参数并返回`std::vector<std::pair<T, T>>`作为结果的重载；这个简单地称为第一个重载:\n\n```cpp\ntemplate <typename Input, typename Output>\nvoid pairwise(Input begin, Input end, Output result)\n{\n   auto it = begin;\n   while (it != end)\n   {\n      auto v1 = *it++ ; if (it == end) break;\n      auto v2 = *it++ ;\n      result++ = std::make_pair(v1, v2);\n   }\n}\ntemplate <typename T>\nstd::vector<std::pair<T, T>> pairwise(std::vector<T> const & range)\n{\n   std::vector<std::pair<T, T>> result;\n   pairwise(std::begin(range), std::end(range),\n            std::back_inserter(result));\n   return result;\n}\n```\n\n以下程序对整数向量的元素进行配对，并在控制台上打印配对:\n\n```cpp\nint main()\n{\n   std::vector<int> v{ 1, 1, 3, 5, 8, 13, 21 };\n   auto result = pairwise(v);\n\n   for (auto const & p : result)\n   {\n      std::cout << '{' << p.first << ',' << p.second << '}' << std::endl;\n   }\n}\n```\n\n# 55.Zip 算法\n\n这个问题与前一个问题相对类似，尽管有两个输入范围，而不是只有一个。结果还是一个`std::pair`的范围。但是，这两个输入范围可能包含不同类型的元素。同样，这里显示的实现包含两个重载:\n\n*   以迭代器为参数的通用函数。每个输入范围的开始和结束迭代器定义其边界，输出迭代器定义输出范围中必须写入结果的位置。\n*   一个接受两个`std::vector`参数的函数，一个保存类型为`T`的元素，一个保存类型为`U`的元素，并返回一个`std::vector<std::pair<T, U>>`。这个重载只是调用前一个重载:\n\n```cpp\ntemplate <typename Input1, typename Input2, typename Output>\nvoid zip(Input1 begin1, Input1 end1, \n         Input2 begin2, Input1 end2, \n         Output result)\n{\n   auto it1 = begin1;\n   auto it2 = begin2;\n   while (it1 != end1 && it2 != end2)\n   {\n      result++ = std::make_pair(*it1++, *it2++);\n   }\n}\n\ntemplate <typename T, typename U>\nstd::vector<std::pair<T, U>> zip(\n   std::vector<T> const & range1, \n   std::vector<U> const & range2)\n{\n   std::vector<std::pair<T, U>> result;\n\n   zip(std::begin(range1), std::end(range1),\n       std::begin(range2), std::end(range2),\n       std::back_inserter(result));\n\n   return result;\n}\n```\n\n在下面的清单中，您可以看到两个整数向量压缩在一起，结果打印在控制台上:\n\n```cpp\nint main()\n{\n   std::vector<int> v1{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };\n   std::vector<int> v2{ 1, 1, 3, 5, 8, 13, 21 };\n\n   auto result = zip(v1, v2);\n   for (auto const & p : result)\n   {\n      std::cout << '{' << p.first << ',' << p.second << '}' << std::endl;\n   }\n}\n```\n\n# 56.选择算法\n\n您必须实现的`select()`函数将一个`std::vector<T>`作为输入参数和一个类型为`F`的函数，并返回一个`std::vector<R>`作为结果，其中`R`是将`F`应用到`T`的结果。我们可以在编译时使用`std::result_of()`来推断调用表达式的返回类型。在内部，`select()`函数应该使用`std::transform()`迭代输入向量的元素，将函数`f`应用于每个元素，并将结果插入输出向量。\n\n下面的清单显示了这个函数的实现:\n\n```cpp\ntemplate <\n   typename T, typename A, typename F,\n   typename R = typename std::decay<typename std::result_of<\n                typename std::decay<F>::type&(\n                typename std::vector<T, A>::const_reference)>::type>::type>\nstd::vector<R> select(std::vector<T, A> const & c, F&& f)\n{\n   std::vector<R> v;\n   std::transform(std::cbegin(c), std::cend(c),\n                  std::back_inserter(v),\n                  std::forward<F>(f));\n   return v;\n}\n```\n\n该功能可以如下使用:\n\n```cpp\nint main()\n{\n   std::vector<book> books{\n      {101, \"The C++ Programming Language\", \"Bjarne Stroustrup\"},\n      {203, \"Effective Modern C++\", \"Scott Meyers\"},\n      {404, \"The Modern C++ Programming Cookbook\", \"Marius Bancila\"}};\n\n   auto titles = select(books, [](book const & b) {return b.title; });\n   for (auto const & title : titles)\n   {\n      std::cout << title << std::endl;\n   }\n}\n```\n\n# 57.分类算法\n\n**快速排序**是对定义了总顺序的数组元素的比较排序算法。当实现良好时，它明显快于*合并排序*或*堆排序*。\n\n虽然在最坏的情况下，算法会进行![](img/66508cd7-1912-4285-bee6-c31db3d8d58c.png)比较(当范围已经排序时)，但平均来说复杂度只有![](img/0ce17681-465d-4ac5-9b80-114f13fa5f2c.png)。Quicksort 是一个分治算法；它将一个较大的范围划分成较小的范围，并递归地对它们进行排序。有几种分区方案。在这里显示的实现中，我们使用了由*东尼·霍尔*开发的原始实现。该方案的算法用伪代码描述如下:\n\n```cpp\nalgorithm quicksort(A, lo, hi) is\n   if lo < hi then\n      p := partition(A, lo, hi)\n      quicksort(A, lo, p)\n      quicksort(A, p + 1, hi)\n\nalgorithm partition(A, lo, hi) is\n   pivot := A[lo]\n   i := lo - 1\n   j := hi + 1\n   loop forever\n      do\n         i := i + 1\n      while A[i] < pivot\n\n      do\n         j := j - 1\n      while A[j] > pivot\n\n      if i >= j then\n         return j\n\n      swap A[i] with A[j]\n```\n\n算法的通用实现应该使用迭代器，而不是数组和索引。以下实现的要求是迭代器是随机访问的(因此它们可以在恒定时间内移动到任何元素):\n\n```cpp\ntemplate <class RandomIt>\nRandomIt partition(RandomIt first, RandomIt last)\n{\n   auto pivot = *first;\n   auto i = first + 1;\n   auto j = last - 1;\n   while (i <= j)\n   {\n      while (i <= j && *i <= pivot) i++ ;\n      while (i <= j && *j > pivot) j--;\n      if (i < j) std::iter_swap(i, j);\n   }\n\n   std::iter_swap(i - 1, first);\n\n   return i - 1;\n}\n\ntemplate <class RandomIt>\nvoid quicksort(RandomIt first, RandomIt last)\n{\n   if (first < last)\n   {\n      auto p = partition(first, last);\n      quicksort(first, p);\n      quicksort(p + 1, last);\n   }\n}\n```\n\n如下所示的`quicksort()`功能可用于分类各种类型的容器:\n\n```cpp\nint main()\n{\n   std::vector<int> v{ 1,5,3,8,6,2,9,7,4 };\n   quicksort(std::begin(v), std::end(v));\n\n   std::array<int, 9> a{ 1,2,3,4,5,6,7,8,9 };\n   quicksort(std::begin(a), std::end(a));\n\n   int a[]{ 9,8,7,6,5,4,3,2,1 };\n   quicksort(std::begin(a), std::end(a));\n}\n```\n\n要求是排序算法必须允许指定用户定义的比较函数。在这种情况下，唯一的变化是分区函数，我们使用用户定义的比较函数，而不是使用`operator <`和`>`来比较当前元素和透视:\n\n```cpp\ntemplate <class RandomIt, class Compare>\nRandomIt partitionc(RandomIt first, RandomIt last, Compare comp)\n{\n   auto pivot = *first;\n   auto i = first + 1;\n   auto j = last - 1;\n   while (i <= j)\n   {\n      while (i <= j && comp(*i, pivot)) i++ ;\n      while (i <= j && !comp(*j, pivot)) j--;\n      if (i < j) std::iter_swap(i, j);\n   }\n\n   std::iter_swap(i - 1, first);\n\n   return i - 1;\n}\n\ntemplate <class RandomIt, class Compare>\nvoid quicksort(RandomIt first, RandomIt last, Compare comp)\n{\n   if (first < last)\n   {\n      auto p = partitionc(first, last, comp);\n      quicksort(first, p, comp);\n      quicksort(p + 1, last, comp);\n   }\n}\n```\n\n有了这个重载，我们可以按降序对一个范围进行排序，如下例所示:\n\n```cpp\nint main()\n{\n   std::vector<int> v{ 1,5,3,8,6,2,9,7,4 };\n   quicksort(std::begin(v), std::end(v), std::greater<>());\n}\n```\n\n也可以实现快速排序算法的迭代版本。迭代版本的性能在大多数情况下与递归版本![](img/816b6538-e697-4a62-ac80-04b85d52d611.png)相同，但是当范围已经排序时，在最坏的情况下会退化为![](img/b96ce493-f107-43e9-b495-222dc77e2530.png)。从算法的递归版本转换为迭代版本相对简单；这是通过使用堆栈来模拟递归调用和存储分区的边界来实现的。以下是使用`operator<`比较元素的版本的迭代实现:\n\n```cpp\ntemplate <class RandomIt>\nvoid quicksorti(RandomIt first, RandomIt last)\n{\n   std::stack<std::pair<RandomIt, RandomIt>> st;\n   st.push(std::make_pair(first, last));\n   while (!st.empty())\n   {\n      auto iters = st.top();\n      st.pop();\n\n      if (iters.second - iters.first < 2) continue;\n\n      auto p = partition(iters.first, iters.second);\n\n      st.push(std::make_pair(iters.first, p));\n      st.push(std::make_pair(p+1, iters.second));\n   }\n}\n```\n\n这种迭代实现可以像递归实现一样使用:\n\n```cpp\nint main()\n{\n   std::vector<int> v{ 1,5,3,8,6,2,9,7,4 };\n   quicksorti(std::begin(v), std::end(v));\n}\n```\n\n# 58.节点之间的最短路径\n\n要解决提出的问题，您必须使用 Dijkstra 算法来寻找图中的最短路径。虽然最初的算法找到了两个给定节点之间的最短路径，但这里的要求是找到一个指定节点和图中所有其他节点之间的最短路径，这是算法的另一个版本。\n\n实现该算法的一种有效方法是使用优先级队列。算法的伪代码(见[https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm](https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm))如下:\n\n```cpp\nfunction Dijkstra(Graph, source):\n   dist[source] ← 0                 // Initialization\n\n   create vertex set Q\n   for each vertex v in Graph: \n      if v ≠ source\n         dist[v] ← INFINITY         // Unknown distance from source to v\n         prev[v] ← UNDEFINED        // Predecessor of v\n\n      Q.add_with_priority(v, dist[v])\n\n   while Q is not empty:            // The main loop\n      u ← Q.extract_min()           // Remove and return best vertex\n      for each neighbor v of u:     // only v that is still in Q\n         alt ← dist[u] + length(u, v) \n         if alt < dist[v]\n            dist[v] ← alt\n            prev[v] ← u\n            Q.decrease_priority(v, alt)\n\n   return dist[], prev[]\n```\n\n为了表示该图，我们可以使用以下数据结构，它可以用于方向图或单向图。该类支持添加新的顶点和边，并且可以返回顶点列表和指定顶点的邻居(即节点和到它们的距离):\n\n```cpp\ntemplate <typename Vertex = int, typename Weight = double>\nclass graph\n{\npublic:\n   typedef Vertex                     vertex_type;\n   typedef Weight                     weight_type;\n   typedef std::pair<Vertex, Weight>  neighbor_type;\n   typedef std::vector<neighbor_type> neighbor_list_type;\npublic:\n   void add_edge(Vertex const source, Vertex const target, \n                 Weight const weight, bool const bidirectional = true)\n   {\n      adjacency_list[source].push_back(std::make_pair(target, weight));\n      adjacency_list[target].push_back(std::make_pair(source, weight));\n   }\n\n   size_t vertex_count() const { return adjacency_list.size(); }\n   std::vector<Vertex> verteces() const\n   {\n      std::vector<Vertex> keys;\n      for (auto const & kvp : adjacency_list)\n         keys.push_back(kvp.first);\n      return keys;\n   }\n\n   neighbor_list_type const & neighbors(Vertex const & v) const\n   {\n      auto pos = adjacency_list.find(v);\n      if (pos == adjacency_list.end())\n         throw std::runtime_error(\"vertex not found\");\n      return pos->second;\n   }\n\n   constexpr static Weight Infinity = \n             std::numeric_limits<Weight>::infinity();\nprivate:\n   std::map<vertex_type, neighbor_list_type> adjacency_list;\n};\n```\n\n前面伪代码中描述的最短路径算法的实现如下所示。使用`std::set`(即自平衡二叉查找树)代替优先级队列。`std::set`具有与二进制堆(用于优先级队列)相同的添加和移除顶部元素的![](img/aeeb1f10-1443-45f7-83fb-94ec84dfcbc1.png)复杂度。另一方面，`std::set`还允许在![](img/22931c36-91c7-4cb4-912a-7a88cf095103.png)中查找和移除任何其他元素，这有助于通过再次移除和插入来实现对数时间中的递减键步骤:\n\n```cpp\ntemplate <typename Vertex, typename Weight>\nvoid shortest_path(\n   graph<Vertex, Weight> const & g,\n   Vertex const source,\n   std::map<Vertex, Weight>& min_distance,\n   std::map<Vertex, Vertex>& previous)\n{\n   auto const n = g.vertex_count();\n   auto const verteces = g.verteces();\n\n   min_distance.clear();\n   for (auto const & v : verteces)\n      min_distance[v] = graph<Vertex, Weight>::Infinity;\n   min_distance[source] = 0;\n\n   previous.clear();\n\n   std::set<std::pair<Weight, Vertex> > vertex_queue;\n   vertex_queue.insert(std::make_pair(min_distance[source], source));\n\n   while (!vertex_queue.empty())\n   {\n      auto dist = vertex_queue.begin()->first;\n      auto u = vertex_queue.begin()->second;\n\n      vertex_queue.erase(std::begin(vertex_queue));\n\n      auto const & neighbors = g.neighbors(u);\n      for (auto const & neighbor : neighbors)\n      {\n         auto v = neighbor.first;\n         auto w = neighbor.second;\n         auto dist_via_u = dist + w;\n         if (dist_via_u < min_distance[v])\n         {\n            vertex_queue.erase(std::make_pair(min_distance[v], v));\n\n            min_distance[v] = dist_via_u;\n            previous[v] = u;\n            vertex_queue.insert(std::make_pair(min_distance[v], v));\n         }\n      }\n   }\n}\n```\n\n以下助手函数以指定的格式打印结果:\n\n```cpp\ntemplate <typename Vertex>\nvoid build_path(\n   std::map<Vertex, Vertex> const & prev, Vertex const v,\n   std::vector<Vertex> & result)\n{\n   result.push_back(v);\n\n   auto pos = prev.find(v);\n   if (pos == std::end(prev)) return;\n\n   build_path(prev, pos->second, result);\n}\n\ntemplate <typename Vertex>\nstd::vector<Vertex> build_path(std::map<Vertex, Vertex> const & prev, \n                               Vertex const v)\n{\n   std::vector<Vertex> result;\n   build_path(prev, v, result);\n   std::reverse(std::begin(result), std::end(result));\n   return result;\n}\n\ntemplate <typename Vertex>\nvoid print_path(std::vector<Vertex> const & path)\n{\n   for (size_t i = 0; i < path.size(); ++ i)\n   {\n      std::cout << path[i];\n      if (i < path.size() - 1) std::cout << \" -> \";\n   }\n}\n```\n\n以下程序解决了给定的任务:\n\n```cpp\nint main()\n{\n   graph<char, double> g;\n   g.add_edge('A', 'B', 7);\n   g.add_edge('A', 'C', 9);\n   g.add_edge('A', 'F', 14);\n   g.add_edge('B', 'C', 10);\n   g.add_edge('B', 'D', 15);\n   g.add_edge('C', 'D', 11);\n   g.add_edge('C', 'F', 2);\n   g.add_edge('D', 'E', 6);\n   g.add_edge('E', 'F', 9);\n\n   char source = 'A';\n   std::map<char, double> min_distance;\n   std::map<char, char> previous;\n   shortest_path(g, source, min_distance, previous);\n\n   for (auto const & kvp : min_distance)\n   {\n      std::cout << source << \" -> \" << kvp.first << \" : \"\n                << kvp.second << '\\t';\n\n      print_path(build_path(previous, kvp.first));\n\n      std::cout << std::endl;\n   }\n}\n```\n\n# 59.黄鼠狼计划\n\n黄鼠狼计划是理查德·道金斯提出的一个思维实验，旨在证明累积的小改进(突变给个体带来好处，从而被自然选择选择)如何产生快速结果，而不是主流的误解，即进化发生在大跳跃中。维基百科上描述的黄鼠狼模拟算法(见[https://en.wikipedia.org/wiki/Weasel_program](https://en.wikipedia.org/wiki/Weasel_program))如下:\n\n1.  从 28 个字符的随机字符串开始。\n2.  制作这个字符串的 100 个副本，每个字符有 5%的几率被随机字符替换。\n3.  将每个新字符串与目标 methanks IT IS LIKE A weak 进行比较，并给每个字符串打分(字符串中正确且位置正确的字母数量)。\n4.  如果任何一个新字符串有满分(28 分)，那么停止。\n5.  否则，取得分最高的字符串，进入第 2 步。\n\n一个可能的实现如下。`make_random()`功能创建一个与目标长度相同的随机起始序列；`fitness()`函数计算每个变异字符串的得分(即与目标的相似度)；`mutate()`函数从父级产生一个新的字符串，每个字符都有一定的变异机会:\n\n```cpp\nclass weasel\n{\n   std::string target;\n   std::uniform_int_distribution<> chardist;\n   std::uniform_real_distribution<> ratedist;\n   std::mt19937 mt;\n   std::string const allowed_chars = \"ABCDEFGHIJKLMNOPQRSTUVWXYZ \";\npublic:\n   weasel(std::string_view t) :\n      target(t), chardist(0, 26), ratedist(0, 100)\n   {\n      std::random_device rd;\n      auto seed_data = std::array<int, std::mt19937::state_size> {};\n      std::generate(std::begin(seed_data), std::end(seed_data), \n      std::ref(rd));\n      std::seed_seq seq(std::begin(seed_data), std::end(seed_data));\n      mt.seed(seq);\n   }\n   void run(int const copies)\n   {\n      auto parent = make_random();\n      int step = 1;\n      std::cout << std::left << std::setw(5) << std::setfill(' ') \n                << step << parent << std::endl;\n\n      do\n      {\n         std::vector<std::string> children;\n         std::generate_n(std::back_inserter(children), copies, \n            [parent, this]() {return mutate(parent, 5); });\n\n         parent = *std::max_element(\n            std::begin(children), std::end(children),\n            [this](std::string_view c1, std::string_view c2) {\n               return fitness(c1) < fitness(c2); });\n\n         std::cout << std::setw(5) << std::setfill(' ') << step \n                << parent << std::endl;\n\n         step++ ;\n      } while (parent != target);\n   }\nprivate:\n   weasel() = delete;\n\n   double fitness(std::string_view candidate) const\n   {\n      int score = 0;\n      for (size_t i = 0; i < candidate.size(); ++ i)\n      {\n         if (candidate[i] == target[i])\n            score++ ;\n      }\n      return score;\n   }\n\n   std::string mutate(std::string_view parent, double const rate)\n   {\n      std::stringstream sstr;\n      for (auto const c : parent)\n      {\n         auto nc = ratedist(mt) > rate ? c : allowed_chars[chardist(mt)];\n         sstr << nc;\n      }\n      return sstr.str();\n    }\n\n   std::string make_random()\n   {\n      std::stringstream sstr;\n      for (size_t i = 0; i < target.size(); ++ i)\n      {\n         sstr << allowed_chars[chardist(mt)];\n      }\n      return sstr.str();\n   }\n};\n```\n\n该类可以这样使用:\n\n```cpp\nint main()\n{\n   weasel w(\"METHINKS IT IS LIKE A WEASEL\");\n   w.run(100);\n}\n```\n\n# 60.生活的游戏\n\n下面展示的类`universe`按照描述实现游戏。有几个感兴趣的功能:\n\n*   `initialize()`生成起始布局；虽然书中附带的代码包含了更多的选项，但这里只列出了两个:`random`生成随机布局，`ten_cell_row`代表网格中间的一行 10 个单元格。\n*   `reset()`将所有单元格设置为`dead`。\n*   `count_neighbors()`返回存活邻居的数量。它使用了一个辅助变量函数模板`count_alive()`。虽然这可以用 fold 表达式来实现，但是在 Visual C++ 中还不支持，因此我选择不在这里使用它。\n*   `next_generation()`根据过渡规则产生新的游戏状态。\n*   `display()`在控制台上显示游戏状态；这使用系统调用来擦除控制台，尽管您可以使用其他方法来这样做，例如特定的操作系统 API。\n*   `run()`初始化起始布局，然后以用户指定的间隔、用户指定的迭代次数或无限期(如果迭代次数设置为 0)生成新的一代。\n\n```cpp\nclass universe\n{\nprivate:\n   universe() = delete;\npublic:\n   enum class seed\n   {\n      random, ten_cell_row\n   };\npublic:\n   universe(size_t const width, size_t const height):\n      rows(height), columns(width),grid(width * height), dist(0, 4)\n   {\n      std::random_device rd;\n      auto seed_data = std::array<int, std::mt19937::state_size> {};\n      std::generate(std::begin(seed_data), std::end(seed_data), \n      std::ref(rd));\n      std::seed_seq seq(std::begin(seed_data), std::end(seed_data));\n      mt.seed(seq);\n   }\n\n   void run(seed const s, int const generations, \n            std::chrono::milliseconds const ms = \n               std::chrono::milliseconds(100))\n   {\n      reset();\n      initialize(s);\n      display();\n\n      int i = 0;\n      do \n      {\n         next_generation();\n         display();\n\n         using namespace std::chrono_literals;\n         std::this_thread::sleep_for(ms);\n      } while (i++ < generations || generations == 0);\n   }\n\nprivate:\n   void next_generation()\n   {\n      std::vector<unsigned char> newgrid(grid.size());\n\n      for (size_t r = 0; r < rows; ++ r)\n      {\n         for (size_t c = 0; c < columns; ++ c)\n         {\n            auto count = count_neighbors(r, c);\n\n            if (cell(c, r) == alive)\n            {\n               newgrid[r * columns + c] = \n                  (count == 2 || count == 3) ? alive : dead;\n            }\n            else \n            {\n               newgrid[r * columns + c] = (count == 3) ? alive : dead;\n            }\n         }\n      }\n\n      grid.swap(newgrid);\n   }\n\n   void reset_display()\n   {\n#ifdef WIN32\n      system(\"cls\");\n#endif\n   }\n\n   void display()\n   {\n      reset_display();\n\n      for (size_t r = 0; r < rows; ++ r)\n      {\n         for (size_t c = 0; c < columns; ++ c)\n         {\n            std::cout << (cell(c, r) ? '*' : ' ');\n         }\n         std::cout << std::endl;\n      }\n   }\n\n   void initialize(seed const s)\n   {\n      if (s == seed::ten_cell_row)\n      {\n         for (size_t c = columns / 2 - 5; c < columns / 2 + 5; c++)\n            cell(c, rows / 2) = alive;\n      }\n      else\n      {\n         for (size_t r = 0; r < rows; ++ r)\n         {\n            for (size_t c = 0; c < columns; ++ c)\n            {\n               cell(c, r) = dist(mt) == 0 ? alive : dead;\n            }\n         }\n      }\n   }\n\n   void reset()\n   {\n      for (size_t r = 0; r < rows; ++ r)\n      {\n         for (size_t c = 0; c < columns; ++ c)\n         {\n            cell(c, r) = dead;\n         }\n      }\n   }\n\n   int count_alive() { return 0; }\n\n   template<typename T1, typename... T>\n   auto count_alive(T1 s, T... ts) { return s + count_alive(ts...); }\n\n   int count_neighbors(size_t const row, size_t const col)\n   {\n      if (row == 0 && col == 0) \n         return count_alive(cell(1, 0), cell(1,1), cell(0, 1));\n      if (row == 0 && col == columns - 1)\n         return count_alive(cell(columns - 2, 0), cell(columns - 2, 1), \n                            cell(columns - 1, 1));\n      if (row == rows - 1 && col == 0)\n         return count_alive(cell(0, rows - 2), cell(1, rows - 2), \n                            cell(1, rows - 1));\n      if (row == rows - 1 && col == columns - 1)\n         return count_alive(cell(columns - 1, rows - 2), \n                            cell(columns - 2, rows - 2), \n                            cell(columns - 2, rows - 1));\n      if (row == 0 && col > 0 && col < columns - 1)\n         return count_alive(cell(col - 1, 0), cell(col - 1, 1), \n                            cell(col, 1), cell(col + 1, 1), \n                            cell(col + 1, 0));\n      if (row == rows - 1 && col > 0 && col < columns - 1)\n         return count_alive(cell(col - 1, row), cell(col - 1, row - 1), \n                            cell(col, row - 1), cell(col + 1, row - 1), \n                            cell(col + 1, row));\n      if (col == 0 && row > 0 && row < rows - 1)\n         return count_alive(cell(0, row - 1), cell(1, row - 1), \n                            cell(1, row), cell(1, row + 1), \n                            cell(0, row + 1));\n      if (col == columns - 1 && row > 0 && row < rows - 1)\n         return count_alive(cell(col, row - 1), cell(col - 1, row - 1), \n                            cell(col - 1, row), cell(col - 1, row + 1), \n                            cell(col, row + 1));\n\n      return count_alive(cell(col - 1, row - 1), cell(col, row - 1), \n                         cell(col + 1, row - 1), cell(col + 1, row), \n                         cell(col + 1, row + 1), cell(col, row + 1), \n                         cell(col - 1, row + 1), cell(col - 1, row));\n   }\n\n   unsigned char& cell(size_t const col, size_t const row)\n   {\n      return grid[row * columns + col];\n   }\n\nprivate:\n   size_t rows;\n   size_t columns;\n\n   std::vector<unsigned char> grid;\n   const unsigned char alive = 1;\n   const unsigned char dead = 0;\n\n   std::uniform_int_distribution<> dist;\n   std::mt19937 mt;\n};\n```\n\n这就是游戏如何从随机状态开始运行 100 次迭代:\n\n```cpp\nint main()\n{\n   using namespace std::chrono_literals;\n   universe u(50, 20);\n   u.run(universe::seed::random, 100, 100ms);\n}\n```\n\n下面是一个程序输出的例子(截图代表了生命游戏宇宙中的一次迭代):\n\n![](img/9f48c4ae-d266-4151-b814-33bf8473953b.png)*"
  },
  {
    "path": "docs/mod-cpp/README.md",
    "content": "# 现代 C++ 编程\n\n> 原书：[Modern C++](https://libgen.rs/book/index.php?md5=F02528C543403FA60BC7527E0C58459D)\n> \n> 协议：[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)\n> \n> 阶段：机翻（1）\n> \n> 自豪地采用[谷歌翻译](https://translate.google.cn/)\n> \n> 让开源界提前感受入关。\n\n* [在线阅读](https://ccpp.apachecn.org)\n* [在线阅读（Gitee）](https://apachecn.gitee.io/apachecn-c-cpp-zh/)\n* [ApacheCN 学习资源](http://docs.apachecn.org/)\n\n## 贡献指南\n\n本项目需要校对，欢迎大家提交 Pull Request。\n\n> 请您勇敢地去翻译和改进翻译。虽然我们追求卓越，但我们并不要求您做到十全十美，因此请不要担心因为翻译上犯错——在大部分情况下，我们的服务器已经记录所有的翻译，因此您不必担心会因为您的失误遭到无法挽回的破坏。（改编自维基百科）\n\n## 联系方式\n\n### 负责人\n\n* [飞龙](https://github.com/wizardforcel): 562826179\n\n### 其他\n\n*   在我们的 [apachecn/apachecn-c-cpp-zh](https://github.com/apachecn/apachecn-c-cpp-zh) github 上提 issue.\n*   发邮件到 Email: `apachecn@163.com`.\n*   在我们的 [组织学习交流群](http://www.apachecn.org/organization/348.html) 中联系群主/管理员即可.\n\n## 赞助我们\n\n![](http://data.apachecn.org/img/about/donate.jpg)\n"
  },
  {
    "path": "docs/mod-cpp/SUMMARY.md",
    "content": "+   [现代 C++ 编程](README.md)\n+   [零、前言](00.md)\n+   [一、理解语言特性](01.md)\n+   [二、使用内存、数组和指针](02.md)\n+   [三、使用函数](03.md)\n+   [四、类](04.md)\n+   [五、使用标准库容器](05.md)\n+   [六、使用字符串](06.md)\n+   [七、诊断和调试](07.md)\n+   [八、学习现代核心语言特性](08.md)\n+   [九、使用数字和字符串](09.md)\n+   [十、探索函数](10.md)\n+   [十一、标准库容器、算法和迭代器](11.md)\n+   [十二、数学问题](12.md)\n+   [十三、语言特性](13.md)\n+   [十四、字符串和正则表达式](14.md)\n+   [十五、流和文件系统](15.md)\n+   [十六、日期和时间](16.md)\n+   [十七、算法和数据结构](17.md)\n"
  },
  {
    "path": "docs/mod-cpp-challenge/00.md",
    "content": "# 零、前言\n\nC++ 是一种通用编程语言，它结合了不同的范例，如面向对象、命令式、泛型和函数式编程。C++ 是为提高效率而设计的，是性能至关重要的应用的主要选择。在过去的几十年里，C++ 一直是工业、学术界和其他地方使用最广泛的编程语言之一。该语言由国际标准化组织(ISO)进行标准化，该组织目前正在研究该标准的下一个版本，名为 C++ 20，将于 2020 年完成。\n\n标准覆盖了将近 1500 页，C++ 不是最简单的学习和掌握的语言。技能不是通过阅读或看别人练习获得的，而是通过一次又一次的练习获得的。编程也没什么不同；我们开发人员不会仅仅通过阅读书籍、文章或观看视频教程来学习新的语言或技术。相反，我们需要实践来沉淀和发展我们所学的新东西，这样我们才能最终掌握它们。然而，很多时候，找到好的练习来检验我们的知识是一项困难的任务。虽然有许多网站都有不同编程语言的问题，但大多数都是数学问题、算法或学生竞赛的问题。这些类型的问题并不能帮助你使用各种各样的编程语言功能。这就是本书的切入点。\n\n这本书收集了 100 个现实世界的问题，旨在让您练习各种各样的 C++ 语言和标准库功能，以及许多第三方、跨平台的库。然而，这些问题中有一些是 C++ 特有的，通常可以用许多编程语言来解决。当然，目的是帮助你掌握 C++，因此你应该用 C++ 来解决它们。书中提供的所有解决方案都是 C++ 语言。但是，当您学习其他编程语言时，您可以将这本书作为其建议问题集的参考，尽管在这种情况下，您不会从解决方案中受益。\n\n这本书里的问题分为 12 章。每章都包含类似或相关主题的问题。这些问题有不同的难度；有些容易，有些适中，有些困难。这本书对每个难度等级都有相对相等数量的问题。每一章都从描述提出的问题开始。这些问题的解决方案伴随着建议、解释和源代码。虽然你可以在书中找到解决方案，但建议你先尝试自己实现它们，然后——或者如果你很难完成它们——再看看建议的解决方案。书中展示的源代码只缺少一样东西——你必须包含的标题。这是故意漏掉的，这样你就可以自己弄清楚了。另一方面，这本书提供的源代码是完整的，你可以在那里找到所有需要的标题。\n\n在撰写本书时，该标准的 C++ 20 版本正在开发中，并将在未来几年内继续使用。然而，一些功能已经被投票通过，其中一个功能是扩展到带有日历和时区的`chrono`图书馆。关于这个主题的第五章有几个问题，虽然还没有编译器支持这些问题，但是您可以使用`date`库来解决这些问题，新的标准附件就是基于这个库设计的。许多其他的图书馆被用来解决书中的问题。该列表包括 Asio、Crypto++、Curl、NLohmann/json、PDF-Writer、PNGWriter、pugixml、SQLite 和 ZipLib。此外，作为本书中使用的`std::optional`和`filesystem`库的替代，您可以在编译器不可用的地方使用 Boost。所有这些库都是开源和跨平台的。选择它们的原因包括性能、良好的文档以及在社区中的广泛使用。但是，您可以自由使用任何其他想要解决问题的库。\n\n# 这本书是给谁的\n\n你是否在尝试学习 C++ 并寻找挑战来实践你所学的内容？如果是的话，这本书是给你的。这本书是为学习 C++ 的人准备的，不管他们对其他编程语言有什么经验，作为实践练习和现实问题的宝贵资源。这本书没有教你语言或标准图书馆的特点。你应该从其他资源中学习，比如书籍、文章或视频教程。这本书是一个学习的伴侣，并挑战你解决各种困难的任务，利用你以前从其他资源中学到的技能。尽管如此，本书中提出的许多问题都是语言不可知的，您可以在学习其他编程语言时使用它们；但是，在这种情况下，您不会从这里提供的解决方案中受益。\n\n# 充分利用这本书\n\n如前所述，您需要基本熟悉 C++ 语言和标准库，以便能够利用这本书，或者您可以一路学习。无论如何，这本书会教你如何解决问题，但不会教你解决方案中使用的语言和功能。您将需要一个支持 C++ 17 的编译器；在代码包中提供的*软件硬件列表*中可以找到所需库以及您可以使用的可能编译器的完整列表。在接下来的章节中，您将从本书中找到下载和构建代码的详细说明。\n\n# 下载示例代码文件\n\n您可以在[www.packtpub.com](http://www.packtpub.com)从您的账户下载包含本书问题解决方案的代码文件。如果您在其他地方购买了这本书，您可以访问[www.packtpub.com/support](http://www.packtpub.com/support)并注册将文件直接通过电子邮件发送给您。\n\n您可以按照以下步骤下载代码文件:\n\n1.  登录或注册[www.packtpub.com](http://www.packtpub.com/support)。\n2.  选择“支持”选项卡。\n3.  点击代码下载和勘误表。\n4.  在搜索框中输入图书的名称，并按照屏幕指示进行操作。\n\n下载文件后，请确保使用最新版本的解压缩文件夹:\n\n*   视窗系统的 WinRAR/7-Zip\n*   zipeg/izp/un ARX for MAC\n*   适用于 Linux 的 7-Zip/PeaZip\n\n这本书的代码包也托管在 https://github.com/PacktPublishing/The-Modern-Cpp-Challenge 的 GitHub 上。我们还有来自丰富的书籍和视频目录的其他代码包，可在获得。看看他们！\n\n# 构建代码\n\n尽管整本书使用了大量的第三方库，但所有这些库以及书中提供的所有解决方案都是跨平台的，并且在所有平台上运行。但是，该代码已经在 Windows 10 上使用 Visual Studio 2017 v15.6/7 和在 Mac OS 10.13.x 上使用 Xcode 9.3 进行了开发和测试。\n\n如果您在 Mac 上使用 Xcode，书中使用的两个功能是 Xcode 中包含的 LLVM 工具集所没有的；这些是`filesystem`库和`std::optional`。但是，这些库是基于`Boost.Filesystem`和`Boost.Optional`库设计的，在建议的解决方案中使用上述标准库很容易与 Boost 库互换。事实上，附带的代码是这样编写的，它可以与两者中的任何一个一起工作；用几个宏来控制使用哪一个。下面提供了用一个或另一个构建的说明，尽管源归档中也有相同的信息。\n\n为了支持大多数开发环境并构建可以在各种平台上使用的系统，代码附带了 CMake 脚本。这些用于为您的首选工具集生成项目或构建脚本。如果您的机器上没有安装 CMake，可以从[https://cmake.org/](https://cmake.org/)获得。下面，您可以找到使用 CMake 生成 Visual Studio 和 Xcode 脚本的说明。对于其他工具，如有必要，请参考 CMake 文档。\n\n# 如何为 Visual Studio 2017 生成项目\n\n请执行以下操作，以生成面向 x86 平台的 Visual Studio 2017 项目:\n\n1.  打开命令提示符，转到源代码根文件夹中的`build`目录。\n2.  执行以下命令:\n\n    ``cmake -G \"Visual Studio 15 2017\" .. -DCMAKE_USE_WINSSL=ON -DCURL_WINDOWS_SSPI=ON -DCURL_LIBRARY=libcurl -DCURL_INCLUDE_DIR=..\\libs\\curl\\include -DBUILD_TESTING=OFF -DBUILD_CURL_EXE=OFF -DUSE_MANUAL=OFF``\n\n3.  完成后，可以在`build/cppchallenger.sln`找到 Visual Studio 解决方案。\n\n如果你想以 x64 平台为目标，可以使用名为`\"Visual Studio 15 2017 Win64\"`的生成器。Visual Studio 2017 15.4 同时支持`filesystem`(作为实验库)和`std::optional`。如果您使用以前的版本，或者只想使用 Boost 库，则可以在正确安装 Boost 后，使用以下命令生成项目:\n\n```cpp\ncmake -G \"Visual Studio 15 2017\" .. -DCMAKE_USE_WINSSL=ON -DCURL_WINDOWS_SSPI=ON -DCURL_LIBRARY=libcurl -DCURL_INCLUDE_DIR=..\\libs\\curl\\include -DBUILD_TESTING=OFF -DBUILD_CURL_EXE=OFF -DUSE_MANUAL=OFF -DBOOST_FILESYSTEM=ON -DBOOST_OPTIONAL=ON -DBOOST_INCLUDE_DIR=<path_to_headers> -DBOOST_LIB_DIR=<path_to_libs>\n```\n\n确保头文件和静态库文件的路径不包含尾随反斜杠(即`\\`)。\n\n# 如何为 Xcode 生成项目\n\n上一章中的几个解决方案利用了`libcurl`库。对于 SSL 支持，该库需要与`OpenSSL`库链接。请执行以下操作来安装 OpenSSL:\n\n1.  从[https://www.openssl.org/](https://www.openssl.org/)下载库。\n2.  解压缩档案，在终端中，转到它的根目录。\n3.  使用以下命令(按此顺序执行)构建和安装库:\n\n    `./Configure darwin64-x86_64-cc shared enable-ec_nistp_64_gcc_128 no-ssl2 no-ssl3 no-comp --openssldir=/usr/local/ssl/macos-x86_64`\n\n    `make depend`\n\n    `sudo make install`\n\n在`std::optional`和`filesystem`库与 Xcode 的 Clang 一起可用之前，您需要使用 Boost。请执行以下操作来安装和构建 Boost 库:\n\n1.  从[https://brew.sh/](https://brew.sh/)安装家酿。\n2.  运行以下命令自动下载并安装 Boost。\n\n    `brew install boost`\n3.  安装后，Boost 库将在`/usr/local/Cellar/boost/1.65.0`可用。\n\n为了从源中为 Xcode 生成项目，您必须:\n\n1.  打开一个终端，转到源代码根目录下的`build`目录。\n2.  执行以下命令:\n\n    `cmake -G Xcode .. -DOPENSSL_ROOT_DIR=/usr/local/bin -DOPENSSL_INCLUDE_DIR=/usr/local/include/ -DBUILD_TESTING=OFF -DBUILD_CURL_EXE=OFF -DUSE_MANUAL=OFF -DBOOST_FILESYSTEM=ON -DBOOST_OPTIONAL=ON -DBOOST_INCLUDE_DIR=/usr/local/Cellar/boost/1.65.0 -DBOOST_LIB_DIR=/usr/local/Cellar/boost/1.65.0/lib`\n3.  完成后，可在`build/cppchallenger.xcodeproj`找到 Xcode 项目。\n\n# 使用的约定\n\n本书通篇使用了许多文本约定。\n\n`CodeInText`:表示文本中的码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟网址、用户输入和推特句柄。下面是一个例子:“将下载的`WebStorm-10*.dmg`磁盘镜像文件作为另一个磁盘挂载到系统中。”\n\n代码块设置如下:\n\n```cpp\nint main()\n{\n   std::cout << \"Hello, World!\\n\";\n}\n```\n\n当我们希望将您的注意力吸引到代码块的特定部分时，相关的行或项目以粗体显示:\n\n```cpp\ntemplate<typename C, typename... Args>\nvoid push_back(C& c, Args&&... args)\n{\n (c.push_back(args), ...);\n}\n```\n\n任何命令行输入或输出都编写如下:\n\n```cpp\n$ mkdir build\n$ cd build\n```\n\n**粗体**:表示一个新的术语、一个重要的单词或者你在屏幕上看到的单词。例如，菜单或对话框中的单词像这样出现在文本中。下面是一个示例:“从管理面板中选择系统信息。”\n\nWarnings or important notes appear like this. Tips and tricks appear like this.\n\n# 取得联系\n\n我们随时欢迎读者的反馈。\n\n**综合反馈**:发邮件`feedback@packtpub.com`并在邮件主题中提及书名。如果您对本书的任何方面有疑问，请发电子邮件至`questions@packtpub.com`。\n\n**勘误表**:虽然我们已经尽了最大的努力来保证内容的准确性，但是错误还是会发生。如果你在这本书里发现了一个错误，如果你能向我们报告，我们将不胜感激。请访问[www.packtpub.com/submit-errata](http://www.packtpub.com/submit-errata)，选择您的图书，点击勘误表提交链接，并输入详细信息。\n\n**盗版**:如果您在互联网上遇到任何形式的我们作品的非法拷贝，如果您能提供我们的位置地址或网站名称，我们将不胜感激。请通过`copyright@packtpub.com`联系我们，并提供材料链接。\n\n**如果你有兴趣成为一名作者**:如果有一个你有专长的话题，你有兴趣写或者投稿一本书，请访问[authors.packtpub.com](http://authors.packtpub.com/)。\n\n# 复习\n\n请留下评论。一旦你阅读并使用了这本书，为什么不在你购买它的网站上留下评论呢？然后，潜在的读者可以看到并使用您不带偏见的意见来做出购买决定，我们在 Packt 可以了解您对我们产品的看法，我们的作者可以看到您对他们的书的反馈。谢谢大家！\n\n更多关于 Packt 的信息，请访问[packtpub.com](https://www.packtpub.com/)。"
  },
  {
    "path": "docs/mod-cpp-challenge/01.md",
    "content": "# 一、数学问题\n\n# 问题\n\n# 1.可被 3 和 5 整除的自然数之和\n\n编写一个程序，计算并打印所有可被 3 或 5 整除的自然数的和，直到用户输入的给定限制。\n\n# 2.最大公约数\n\n写一个程序，在给定两个正整数的情况下，计算并打印两者的最大公约数。\n\n# 3.最小公倍数\n\n写一个程序，在给定两个或更多正整数的情况下，计算并打印它们的最小公倍数。\n\n# 4.小于给定数的最大素数\n\n编写一个程序，计算并打印比用户提供的数字小的最大素数，该数字必须是正整数。\n\n# 5.性感的黄金搭档\n\n写一个程序，打印所有性感的黄金配对，直到用户输入的限制。\n\n# 6.大量的数字\n\n写一个程序，打印所有丰富的数字和他们的丰富，直到用户输入的数字。\n\n# 7.友好的数字\n\n编写一个程序，打印所有小于 1，000，000 的友好数字对的列表。\n\n# 8.阿姆斯特朗数字\n\n写一个程序，用三位数打印所有阿姆斯特朗的号码。\n\n# 9.一个数的质因数\n\n编写一个程序，打印用户输入的数字的质因数。\n\n# 10.格雷编码\n\n编写一个程序，显示所有 5 位数字的正常二进制表示、格雷码表示和解码格雷码值。\n\n# 11.将数值转换为罗马数字\n\n编写一个程序，给定用户输入的数字，打印它的罗马数字等价物。\n\n# 12.最大排序序列\n\n编写一个程序，确定并打印 100 万以内哪个数字产生最长的 Collatz 序列，以及它的长度是多少。\n\n# 13.圆周率的计算\n\n编写一个程序，计算精度为两位小数的π值。\n\n# 14.验证 ISBNs\n\n编写一个程序，验证用户以字符串形式输入的 10 位数值是否代表有效的 ISBN-10 数字。\n\n# 解决方法\n\n# 1.可被 3 和 5 整除的自然数之和\n\n这个问题的解决方案是迭代从 3 (1 和 2 不能被 3 整除，所以测试它们没有意义)到用户输入的限制的所有数字。使用模运算检查数字除以 3 和 5 的其余部分是否为 0。然而，能够求和到更大极限的技巧是使用`long long`而不是`int`或`long`进行求和，这将导致在求和到 100，000 之前溢出:\n\n```cpp\nint main()\n{\n   unsigned int limit = 0;\n   std::cout << \"Upper limit:\";\n   std::cin >> limit;\n\n   unsigned long long sum = 0;\n   for (unsigned int i = 3; i < limit; ++ i)\n   {\n     if (i % 3 == 0 || i % 5 == 0)\n        sum += i;\n   }\n\n   std::cout << \"sum=\" << sum << std::endl;\n}\n```\n\n# 2.最大公约数\n\n两个或多个非零整数的最大公约数( *gcd* 简称)，也称为最大公约数( *gcf* )、最高公约数( *hcf* )、最大公约数( *gcm* )或最高公约数，是除以所有整数的最大正整数。有几种方法可以计算 gcd 一种有效的方法是欧几里德算法。对于两个整数，算法是:\n\n```cpp\ngcd(a,0) = a\ngcd(a,b) = gcd(b, a mod b)\n```\n\n这可以使用递归函数在 C++ 中非常简单地实现:\n\n```cpp\nunsigned int gcd(unsigned int const a, unsigned int const b)\n{\n   return b == 0 ? a : gcd(b, a % b);\n}\n```\n\n欧几里德算法的非递归实现应该如下所示:\n\n```cpp\nunsigned int gcd(unsigned int a, unsigned int b)\n{\n   while (b != 0) {\n      unsigned int r = a % b;\n      a = b;\n      b = r;\n   }\n   return a;\n}\n```\n\nIn C++ 17 there is a `constexpr` function called `gcd()` in the header `<numeric>` that computes the greatest common divisor of two numbers.\n\n# 3.最小公倍数\n\n两个或多个非零整数的**最小公倍数** ( **lcm** )也称为最小公倍数，是可被所有整数整除的最小正整数。计算最小公倍数的一种可能方法是将问题简化为计算最大公约数。在这种情况下使用以下公式:\n\n```cpp\nlcm(a, b) = abs(a, b) / gcd(a, b)\n```\n\n计算最小公倍数的函数可能如下所示:\n\n```cpp\nint lcm(int const a, int const b)\n{\n   int h = gcd(a, b);\n   return h ? (a * (b / h)) : 0;\n}\n```\n\n要计算两个以上整数的 *lcm* ，可以使用标题`<numeric>`中的`std::accumulate`算法:\n\n```cpp\ntemplate<class InputIt>\nint lcmr(InputIt first, InputIt last)\n{\n   return std::accumulate(first, last, 1, lcm);\n}\n```\n\nIn C++ 17 there is a `constexpr` function called `lcm()` in the header `<numeric>` that computes the least common multiple of two numbers.\n\n# 4.小于给定数的最大素数\n\n质数是只有两个除数的数，1 和数本身。要找到比给定数小的最大素数，你应该先写一个函数，确定一个数是否是素数，然后从给定数开始，向 1 方向调用这个函数，直到遇到第一个素数。有各种算法来确定一个数是否是质数。确定素性的常见实现如下:\n\n```cpp\nbool is_prime(int const num) \n{\n   if (num <= 3) { return num > 1; }\n   else if (num % 2 == 0 || num % 3 == 0) \n   { \n      return false; \n   }\n   else \n   {\n      for (int i = 5; i * i <= num; i += 6) \n      {\n         if (num % i == 0 || num % (i + 2) == 0) \n         {\n            return false;\n         }\n      }\n      return true;\n   }\n}\n```\n\n该功能可以如下使用:\n\n```cpp\nint main()\n{\n   int limit = 0;\n   std::cout << \"Upper limit:\";\n   std::cin >> limit;\n\n   for (int i = limit; i > 1; i--)\n   {\n      if (is_prime(i))\n      {\n         std::cout << \"Largest prime:\" << i << std::endl;\n         return 0;\n      }\n   }\n}\n```\n\n# 5.性感的黄金搭档\n\n性感质数是彼此相差 6 的质数(例如 5 和 11，或 13 和 19)。还有相差两个的*孪生素数*和相差四个的*表亲素数*。\n\n在前面的挑战中，我们实现了一个函数，该函数确定一个整数是否是素数。我们将在本练习中重用该函数。你要做的是检查如果一个数字`n`是质数，那么这个数字`n+6`也是质数，在这种情况下，把这一对打印到控制台上:\n\n```cpp\nint main()\n{\n   int limit = 0;\n   std::cout << \"Upper limit:\";\n   std::cin >> limit;\n\n   for (int n = 2; n <= limit; n++)\n   {\n      if (is_prime(n) && is_prime(n+6))\n      {\n         std::cout << n << \",\" << n+6 << std::endl;\n      }\n   }\n}\n```\n\n你可以把它作为计算和显示性感的三胞胎、四胞胎和五胞胎的进一步练习。\n\n# 6.大量的数字\n\n一个大数，也称为一个过度数，是指它的适当除数之和大于数本身的数。一个数的适当除数是这个数的正质因数，而不是这个数本身。适当因子之和超过数本身的量叫做丰度。例如，数字 12 有适当的除数 1、2、3、4 和 6。他们的总和是 16，这使 12 成为一个充裕的数字。它的丰度是 4(即 16 - 12)。\n\n为了确定适当除数的和，我们尝试从 2 到数的平方根的所有数(所有质因数都小于或等于这个值)。如果现在的数，我们称之为`i`，除以这个数，那么`i`和`num/i`都是除数。但是，如果它们相等(例如如果`i = 3`，和`n = 9`，那么`i`除 9，但是`n/i = 3`，我们只加`i`，因为适当的约数必须只加一次。否则，我们同时添加`i`和`num/i`并继续:\n\n```cpp\nint sum_proper_divisors(int const number)\n{\n   int result = 1;\n   for (int i = 2; i <= std::sqrt(number); i++)\n   {\n      if (number%i == 0)\n      {\n         result += (i == (number / i)) ? i : (i + number / i);\n      }\n   }\n   return result;\n}\n```\n\n打印大量的数字很简单，只需迭代到指定的极限，计算适当除数的总和，并将其与数字进行比较:\n\n```cpp\nvoid print_abundant(int const limit)\n{\n   for (int number = 10; number <= limit; ++ number)\n   {\n      auto sum = sum_proper_divisors(number);\n      if (sum > number)\n      {\n         std::cout << number << \", abundance=\" \n                   << sum - number << std::endl;\n      }\n   }\n}\n\nint main()\n{\n   int limit = 0;\n   std::cout << \"Upper limit:\";\n   std::cin >> limit;\n\n   print_abundant(limit);\n}\n```\n\n# 7.友好的数字\n\n如果一个数的适当除数之和等于另一个数的适当除数之和，则称两个数是友好的。一个数的适当除数是该数的正质因数，而不是该数本身。友好号码不应与*友好号码*混淆。例如，数字 220 有适当的除数 1、2、4、5、10、11、20、22、44、55 和 110，它们的和是 284。284 的适当除数是 1、2、4、71 和 142；他们的总数是 220。因此，数字 220 和 284 被认为是友好的。\n\n这个问题的解决方案是迭代所有的数字，直到给定的极限。对于每个数，计算其适当除数的和。姑且称之为`sum1`。重复该过程，计算`sum1`的适当除数之和。如果结果等于原始数字，则数字和`sum1`是友好数字:\n\n```cpp\nvoid print_amicables(int const limit)\n{\n   for (int number = 4; number < limit; ++ number)\n   {\n      auto sum1 = sum_proper_divisors(number);\n      if (sum1 < limit)\n      {\n         auto sum2 = sum_proper_divisors(sum1);\n         if (sum2 == number && number != sum1)\n         {\n            std::cout << number << \",\" << sum1 << std::endl;\n         }\n      }\n   }\n}\n```\n\n在上面的例子中，`sum_proper_divisors()`是在大数问题的解中看到的函数。\n\nThe above function prints pairs of numbers twice, such as 220,284 and 284,220\\. Modify this implementation to only print each pair a single time.\n\n# 8.阿姆斯特朗数字\n\n阿姆斯特朗数(以迈克尔·f·阿姆斯特朗的名字命名)，也称为自恋数、pluperfect 数字不变量或 pluperfect 数，是一个数，当它的位数增加到位数的幂时，它等于它自己的位数之和。举个例子，最小的阿姆斯特朗数是 153，等于![](img/f401b492-9c63-4282-85e5-1c9bba31eedd.png)。\n\n要确定一个三位数的数字是否是自恋数字，你必须首先确定它的位数，以便对它们的幂求和。然而，这涉及除法和模运算，这是昂贵的。一种更快的计算方法是依赖于这样一个事实，即一个数是数字的总和乘以 10 的幂等于它们从零开始的位置。换句话说，对于 1000 以下的数字，我们有`a*10^2 + b*10^2 + c`。因为你只需要确定三位数的数字，这意味着`a`将从 1 开始。这将比其他方法更快，因为乘法的计算速度比除法和模运算更快。这种函数的实现如下所示:\n\n```cpp\nvoid print_narcissistics()\n{\n   for (int a = 1; a <= 9; a++)\n   {\n      for (int b = 0; b <= 9; b++)\n      {\n         for (int c = 0; c <= 9; c++)\n         {\n            auto abc = a * 100 + b * 10 + c;\n            auto arm = a * a * a + b * b * b + c * c * c;\n            if (abc == arm)\n            {\n               std::cout << arm << std::endl;\n            }\n         }\n      }\n   }\n}\n```\n\n你可以把它作为一个进一步的练习，写一个函数来决定自恋数字的上限，不管它们的位数是多少。这样的函数会比较慢，因为你首先必须确定数字的数字序列，将它们存储在一个容器中，然后将数字加到适当的幂(数字的数量)上。\n\n# 9.一个数的质因数\n\n正整数的质因数是将该整数整除的素数。例如，8 的质因数是 2×2×2，42 的质因数是 2×3×7。要确定主要因素，您应该使用以下算法:\n\n1.  而`n`可以被 2 整除，2 是质因数，必须加入列表，而`n`则成为`n/2`的结果。完成此步骤后，`n`为奇数。\n2.  从 3 迭代到`n`的平方根。而目前的数字，姑且称之为`i`，除`n`，`i`是质因数，必须加进榜单，而`n`则成为`n/i`的结果。当`i`不再除`n`时，将`i`增加 2(得到下一个奇数)。\n3.  当`n`是大于 2 的素数时，上述步骤不会导致`n`变为 1。因此，如果在第 2 步结束时`n`仍然大于 2，那么`n`就是一个主要因素。\n\n```cpp\nstd::vector<unsigned long long> prime_factors(unsigned long long n)\n{\n   std::vector<unsigned long long> factors;\n   while (n % 2 == 0) {\n      factors.push_back(2);\n      n = n / 2;\n   }\n   for (unsigned long long i = 3; i <= std::sqrt(n); i += 2)\n   {\n      while (n%i == 0) {\n         factors.push_back(i);\n         n = n / i;\n      }\n   }\n\n   if (n > 2) \n      factors.push_back(n);\n   return factors;\n}\n\nint main()\n{\n   unsigned long long number = 0;\n   std::cout << \"number:\";\n   std::cin >> number;\n\n   auto factors = prime_factors(number);\n   std::copy(std::begin(factors), std::end(factors),\n        std::ostream_iterator<unsigned long long>(std::cout, \" \"));\n}\n```\n\n作为进一步的练习，确定数字 600，851，475，143 的最大质因数。\n\n# 10.格雷编码\n\n格雷码，也称为反射二进制码或简称反射二进制码，是二进制编码的一种形式，其中两个连续的数字仅相差一位。要执行二进制反射格雷码编码，我们需要使用以下公式:\n\n```cpp\nif b[i-1] = 1 then g[i] = not b[i]\nelse g[i] = b[i]\n```\n\n这相当于以下内容:\n\n```cpp\ng = b xor (b logically right shifted 1 time)\n```\n\n对于解码二进制反射格雷码，应使用以下公式:\n\n```cpp\nb[0] = g[0]\nb[i] = g[i] xor b[i-1]\n```\n\n对于 32 位无符号整数，可以用 C++ 编写如下:\n\n```cpp\nunsigned int gray_encode(unsigned int const num)\n{\n   return num ^ (num >> 1);\n}\n\nunsigned int gray_decode(unsigned int gray)\n{\n   for (unsigned int bit = 1U << 31; bit > 1; bit >>= 1)\n   {\n      if (gray & bit) gray ^= bit >> 1;\n   }\n   return gray;\n}\n```\n\n要打印所有 5 位整数、它们的二进制表示、编码的格雷码表示和解码值，我们可以使用以下代码:\n\n```cpp\nstd::string to_binary(unsigned int value, int const digits)\n{\n   return std::bitset<32>(value).to_string().substr(32-digits, digits);\n}\n\nint main()\n{\n   std::cout << \"Number\\tBinary\\tGray\\tDecoded\\n\";\n   std::cout << \"------\\t------\\t----\\t-------\\n\";\n\n   for (unsigned int n = 0; n < 32; ++ n)\n   {\n      auto encg = gray_encode(n);\n      auto decg = gray_decode(encg);\n\n      std::cout \n         << n << \"\\t\" << to_binary(n, 5) << \"\\t\" \n         << to_binary(encg, 5) << \"\\t\" << decg << \"\\n\";\n   }\n}\n```\n\n# 11.将数值转换为罗马数字\n\n今天已知的罗马数字使用七个符号:I = 1，V = 5，X = 10，L = 50，C = 100，D = 500，M = 1000。系统在组成数字符号时使用加法和减法。从 1 到 10 的符号是 I、II、III、IV、V、VI、VII、VIII、IX 和 x。罗马人没有零的符号，他们用写 *nulla* 来表示它。在这个系统中，最大的符号在左边，最不重要的符号在右边。例如，1994 的罗马数字是 MCMXCIV。如果你不熟悉罗马数字的规则，你应该在网上多看看。\n\n要确定数字的罗马数字，请使用以下算法:\n\n1.  从最高(M)到最低(I)检查每个罗马基本符号\n2.  如果当前值大于符号的值，则将符号连接到罗马数字，并从当前值中减去它的值\n3.  重复，直到当前值为零\n\n例如，考虑 42:小于 42 的第一个罗马基本符号是 XL，即 40。我们将其连接到数字上，得到 XL，然后从当前数字中减去，得到 2。第一个小于 2 的罗马基符号是 I，也就是 1。我们把它加到数字上，得到 XLI，然后从数字中减去 1，得到 1。我们在数字上再加一个 I，变成 XLII，再从数字中减去 1，达到 0，因此停止:\n\n```cpp\nstd::string to_roman(unsigned int value)\n{\n   std::vector<std::pair<unsigned int, char const*>> roman {\n      { 1000, \"M\" },{ 900, \"CM\" }, { 500, \"D\" },{ 400, \"CD\" }, \n      { 100, \"C\" },{ 90, \"XC\" }, { 50, \"L\" },{ 40, \"XL\" },\n      { 10, \"X\" },{ 9, \"IX\" }, { 5, \"V\" },{ 4, \"IV\" }, { 1, \"I\" }};\n\n   std::string result;\n   for (auto const & kvp : roman) {\n      while (value >= kvp.first) {\n         result += kvp.second;\n         value -= kvp.first;\n      }\n   }\n   return result;\n}\n```\n\n该功能可以如下使用:\n\n```cpp\nint main()\n{\n   for(int i = 1; i <= 100; ++ i) \n   {\n      std::cout << i << \"\\t\" << to_roman(i) << std::endl; \n   }\n\n   int number = 0;\n   std::cout << \"number:\";\n   std::cin >> number;\n   std::cout << to_roman(number) << std::endl;\n}\n```\n\n# 12.最大排序序列\n\n柯拉茨猜想，也称为乌兰猜想、卡库塔尼问题、思韦特猜想、哈塞算法或锡拉丘兹问题，是一个未经证实的猜想，它指出如下所述定义的序列总是达到 1。级数定义如下:从任意正整数`n`开始，从上一项得到每个新项:如果上一项为偶数，则下一项为上一项的一半，否则为上一项的 3 倍加 1。\n\n你要解决的问题是为所有 100 万以内的正整数生成 Collatz 序列，确定其中最长的一个，并打印它的长度和产生它的起始数。虽然我们可以应用蛮力为每个数字生成序列，并计算项数直到达到 1，但更快的解决方案是保存已经生成的所有序列的长度。当从值`n`开始的序列的当前项变得小于`n`时，那么它是一个已经确定了序列的数字，所以我们可以简单地获取它的缓存长度，并将其添加到当前长度中，以确定从`n`开始的序列的长度。然而，这种方法对可以计算的 Collatz 序列引入了限制，因为在某个时刻，缓存将超过系统可以分配的内存量:\n\n```cpp\nstd::pair<unsigned long long, long> longest_collatz(\n   unsigned long long const limit)\n{\n   long length = 0;\n   unsigned long long number = 0;\n   std::vector<int> cache(limit + 1, 0);\n\n   for (unsigned long long i = 2; i <= limit; i++) \n   {\n      auto n = i;\n      long steps = 0;\n      while (n != 1 && n >= i) \n      {\n         if ((n % 2) == 0) n = n / 2;\n         else n = n * 3 + 1;\n         steps++ ;\n      }\n      cache[i] = steps + cache[n];\n\n      if (cache[i] > length) \n      {\n         length = cache[i];\n         number = i;\n```\n\n```cpp\n      }\n   }\n\n   return std::make_pair(number, length);\n}\n```\n\n# 13.圆周率的计算\n\n近似确定π值的一个合适的解决方案是使用蒙特卡罗模拟。这是一种使用随机输入样本来探索复杂过程或系统行为的方法。该方法被用于各种各样的应用和领域，包括物理、工程、计算、金融、商业和其他领域。\n\n要做到这一点，我们将依靠以下想法:直径为`d`的圆的面积为`PI * d^2 / 4`。边长等于`d`的正方形面积是`d^2`。如果我们把两者分开，我们会得到`PI/4`。如果我们把圆放在正方形里面，生成在正方形里面均匀分布的随机数，那么圆里面的个数应该和圆的面积成正比，正方形里面的个数应该和正方形的面积成正比。这意味着将方块和圆圈中的点击总数相除应该会给出`PI/4`。生成的点越多，结果应该越准确。\n\n为了生成伪随机数，我们将使用默森扭转器和均匀的统计分布:\n\n```cpp\ntemplate <typename E = std::mt19937, \n          typename D = std::uniform_real_distribution<>>\ndouble compute_pi(E& engine, D& dist, int const samples = 1000000)\n{\n   auto hit = 0;\n   for (auto i = 0; i < samples; i++)\n   {\n      auto x = dist(engine);\n      auto y = dist(engine);\n      if (y <= std::sqrt(1 - std::pow(x, 2))) hit += 1;\n   }\n   return 4.0 * hit / samples;\n}\n\nint main()\n{\n   std::random_device rd;\n   auto seed_data = std::array<int, std::mt19937::state_size> {};\n   std::generate(std::begin(seed_data), std::end(seed_data), \n                 std::ref(rd));\n   std::seed_seq seq(std::begin(seed_data), std::end(seed_data));\n   auto eng = std::mt19937{ seq };\n   auto dist = std::uniform_real_distribution<>{ 0, 1 };\n\n   for (auto j = 0; j < 10; j++)\n      std::cout << compute_pi(eng, dist) << std::endl;\n}\n```\n\n# 14.验证 ISBNs\n\n**国际标准书号** ( **国际标准书号**)是图书的唯一数字标识符。目前，使用 13 位格式。但是，对于这个问题，您需要验证使用 10 位数字的前一种格式。10 位中的最后一位是校验和。选择该数字是为了使所有十个数字的总和(每个数字乘以其(整数)权重，从 10 到 1 递减)是 11 的倍数。\n\n`validate_isbn_10`函数，如下所示，将一个 ISBN 作为一个字符串，如果字符串的长度为 10，所有十个元素都是数字，并且所有数字的总和乘以它们的权重(或位置)是 11 的倍数，则返回`true`:\n\n```cpp\nbool validate_isbn_10(std::string_view isbn)\n{\n   auto valid = false;\n   if (isbn.size() == 10 &&\n       std::count_if(std::begin(isbn), std::end(isbn), isdigit) == 10)\n   {\n      auto w = 10;\n      auto sum = std::accumulate(\n         std::begin(isbn), std::end(isbn), 0,\n         [&w](int const total, char const c) {\n            return total + w-- * (c - '0'); });\n\n     valid = !(sum % 11);\n   }\n   return valid;\n}\n```\n\nYou can take it as a further exercise to improve this function to also correctly validate ISBN-10 numbers that include hyphens, such as `3-16-148410-0`. Also, you can write a function that validates ISBN-13 numbers."
  },
  {
    "path": "docs/mod-cpp-challenge/02.md",
    "content": "# 二、语言特性\n\n# 问题\n\n# 15.IPv4 数据类型\n\n编写一个表示 IPv4 地址的类。实现能够从控制台读写这些地址所需的功能。用户应该能够以虚线形式输入值，如`127.0.0.1`或`168.192.0.100`。这也是 IPv4 地址应该格式化为输出流的形式。\n\n# 16.枚举范围内的 IPv4 地址\n\n编写一个程序，允许用户输入代表一个范围的两个 IPv4 地址，并列出该范围内的所有地址。扩展为前面的问题定义的结构，以实现请求的功能。\n\n# 17.使用基本操作创建 2D 数组\n\n编写一个表示二维数组容器的类模板，其中包含元素访问(`at()`和`data()`)、容量查询、迭代器、填充和交换的方法。应该可以移动这种类型的对象。\n\n# 18.具有任意数量参数的最小函数\n\n编写一个函数模板，可以取任意数量的参数，并返回所有参数的最小值，使用`operator <`进行比较。写一个这个函数模板的变体，可以用二进制比较函数参数化来代替`operator <`使用。\n\n# 19.向容器添加一系列值\n\n编写一个通用函数，可以在有方法`push_back(T&& value)`的容器末尾添加任意数量的元素。\n\n# 20.容器任意、全部、无\n\n编写一组通用函数，用于检查给定容器中是否存在任何、所有或没有指定的参数。这些函数应该可以编写如下代码:\n\n```cpp\nstd::vector<int> v{ 1, 2, 3, 4, 5, 6 };\nassert(contains_any(v, 0, 3, 30));\n\nstd::array<int, 6> a{ { 1, 2, 3, 4, 5, 6 } };\nassert(contains_all(a, 1, 3, 5, 6));\n\nstd::list<int> l{ 1, 2, 3, 4, 5, 6 };\nassert(!contains_none(l, 0, 6));\n```\n\n# 21.系统句柄包装\n\n考虑一个操作系统句柄，如文件句柄。编写一个包装器来处理句柄的获取和释放，以及其他操作，例如验证句柄的有效性和将句柄所有权从一个对象转移到另一个对象。\n\n# 22.各种温标的文字\n\n写一个小的库，可以用三种最常用的刻度摄氏、华氏和开尔文来表示温度，并在它们之间转换。该库必须使您能够在所有这些刻度中写入温度文字，例如`36.5_deg`代表摄氏度，`97.7_f`代表华氏，`309.65_K`代表开尔文；使用这些值执行操作；并在它们之间转换。\n\n# 解决方法\n\n# 15.IPv4 数据类型\n\n这个问题需要编写一个类来表示 IPv4 地址。这是一个 32 位的值，通常用十进制的点分格式表示，如`168.192.0.100`*；*它的每个部分都是一个 8 位值，范围从 0 到 255。为了便于表示和处理，我们可以使用四个`unsigned char`来存储地址值。这样的值可以由四个 T2 或一个 T3 构成。为了能够直接从控制台(或任何其他输入流)读取一个值，并且能够将该值写入控制台(或任何其他输出流)，我们必须重载`operator>>`和`operator<<`。下面的清单显示了可以满足所请求功能的最小实现:\n\n```cpp\nclass ipv4\n{\n   std::array<unsigned char, 4> data;\npublic:\n   constexpr ipv4() : data{ {0} } {}\n   constexpr ipv4(unsigned char const a, unsigned char const b, \n                  unsigned char const c, unsigned char const d):\n      data{{a,b,c,d}} {}\n   explicit constexpr ipv4(unsigned long a) :\n      data{ { static_cast<unsigned char>((a >> 24) & 0xFF), \n              static_cast<unsigned char>((a >> 16) & 0xFF),\n              static_cast<unsigned char>((a >> 8) & 0xFF),\n              static_cast<unsigned char>(a & 0xFF) } } {}\n   ipv4(ipv4 const & other) noexcept : data(other.data) {}\n   ipv4& operator=(ipv4 const & other) noexcept \n   {\n      data = other.data;\n      return *this;\n   }\n\n   std::string to_string() const\n   {\n      std::stringstream sstr;\n      sstr << *this;\n      return sstr.str();\n   }\n\n   constexpr unsigned long to_ulong() const noexcept\n   {\n      return (static_cast<unsigned long>(data[0]) << 24) |\n             (static_cast<unsigned long>(data[1]) << 16) |\n             (static_cast<unsigned long>(data[2]) << 8) |\n              static_cast<unsigned long>(data[3]);\n   }\n\n   friend std::ostream& operator<<(std::ostream& os, const ipv4& a)\n   {\n      os << static_cast<int>(a.data[0]) << '.' \n         << static_cast<int>(a.data[1]) << '.'\n         << static_cast<int>(a.data[2]) << '.'\n         << static_cast<int>(a.data[3]);\n      return os;\n   }\n\n   friend std::istream& operator>>(std::istream& is, ipv4& a)\n   {\n      char d1, d2, d3;\n      int b1, b2, b3, b4;\n      is >> b1 >> d1 >> b2 >> d2 >> b3 >> d3 >> b4;\n      if (d1 == '.' && d2 == '.' && d3 == '.')\n         a = ipv4(b1, b2, b3, b4);\n      else\n         is.setstate(std::ios_base::failbit);\n      return is;\n   }\n};\n```\n\n`ipv4`类可以如下使用:\n\n```cpp\nint main()\n{\n   ipv4 address(168, 192, 0, 1);\n   std::cout << address << std::endl;\n\n   ipv4 ip;\n   std::cout << ip << std::endl;\n   std::cin >> ip;\n   if(!std::cin.fail())\n      std::cout << ip << std::endl;\n}\n```\n\n# 16.枚举范围内的 IPv4 地址\n\n为了能够枚举给定范围内的 IPv4 地址，首先应该能够比较 IPv4 值。所以我们至少要实现`operator<`，但是下面的列表包含了所有比较运算符的实现:`==`、`!=`、`<`、`>`、`<=`和`>=`。此外，为了增加 IPv4 值，提供了前缀和后缀`operator++ `的实现。以下代码是上一个问题的 IPv4 类的扩展:\n\n```cpp\nipv4& operator++()\n{\n   *this = ipv4(1 + to_ulong());\n   return *this;\n}\n\nipv4& operator++(int)\n{\n   ipv4 result(*this);\n   ++(*this);\n   return *this;\n}\n\nfriend bool operator==(ipv4 const & a1, ipv4 const & a2) noexcept\n{\n   return a1.data == a2.data;\n}\n\nfriend bool operator!=(ipv4 const & a1, ipv4 const & a2) noexcept\n{\n   return !(a1 == a2);\n}\n\nfriend bool operator<(ipv4 const & a1, ipv4 const & a2) noexcept\n{\n   return a1.to_ulong() < a2.to_ulong();\n}\n\nfriend bool operator>(ipv4 const & a1, ipv4 const & a2) noexcept\n{\n   return a2 < a1;\n}\n\nfriend bool operator<=(ipv4 const & a1, ipv4 const & a2) noexcept\n{\n   return !(a1 > a2);\n}\n\nfriend bool operator>=(ipv4 const & a1, ipv4 const & a2) noexcept\n{\n   return !(a1 < a2);\n}\n```\n\n通过对前一个问题中的`ipv4`类进行这些更改，我们可以编写以下程序:\n\n```cpp\nint main()\n{\n   std::cout << \"input range: \";\n   ipv4 a1, a2;\n   std::cin >> a1 >> a2;\n   if (a2 > a1)\n   {\n      for (ipv4 a = a1; a <= a2; a++)\n      {\n         std::cout << a << std::endl;\n      }\n   }\n   else \n   {\n      std::cerr << \"invalid range!\" << std::endl;\n   }\n}\n```\n\n# 17.使用基本操作创建 2D 数组\n\n在考虑如何定义这样的结构之前，让我们考虑几个测试用例。以下代码片段显示了请求的所有功能:\n\n```cpp\nint main()\n{\n   // element access\n   array2d<int, 2, 3> a {1, 2, 3, 4, 5, 6};\n   for (size_t i = 0; i < a.size(1); ++ i)\n      for (size_t j = 0; j < a.size(2); ++ j)\n      a(i, j) *= 2;\n\n   // iterating\n   std::copy(std::begin(a), std::end(a), \n      std::ostream_iterator<int>(std::cout, \" \"));\n\n   // filling \n   array2d<int, 2, 3> b;\n   b.fill(1);\n\n   // swapping\n   a.swap(b);\n\n   // moving\n   array2d<int, 2, 3> c(std::move(b));\n}\n```\n\n注意，对于元素访问，我们使用的是`operator()`，比如在`a(i,j)`中，而不是`operator[]`，比如在`a[i][j]`中，因为只有前者可以接受多个参数(每个维度上的索引一个)。后者只能有一个参数，为了启用像`a[i][j]`这样的表达式，它必须返回一个中间类型(一个基本上代表一行的类型)，该中间类型反过来重载`operator[]`以返回单个元素。\n\n已经有存储固定或可变长度元素序列的标准容器。这个二维数组类应该只是这样一个容器的适配器。在`std::array`和`std::vector`之间进行选择时，我们应该考虑两件事:\n\n*   `array2d`类应该具有移动语义，以便能够移动对象\n*   应该可以列出初始化这种类型的对象\n\n`std::array`容器只有在它容纳的元素是可移动构造和可移动分配的情况下才是可移动的。另一方面，它不能由`std::initializer_list`构成。因此，更可行的选择仍然是`std::vector`。\n\n在内部，这个适配器容器可以将其数据存储在向量的向量中(每行是一个带有`C`元素的`vector<T>`，而 2D 数组的`R`这样的元素存储在一个`vector<vector<T>>`中)或者类型为`T`的`R![](img/ed1546bc-db93-4799-9287-f1bbdc16089c.png)C`元素的单个向量中。在后一种情况下，第`i`行和第`j`列的元素位于索引`i * C + j`处。这种方法占用的内存更少，将所有数据存储在一个连续的块中，并且实现起来也更简单。由于这些原因，它是首选的解决方案。\n\n具有所请求功能的二维数组类的可能实现如下所示:\n\n```cpp\ntemplate <class T, size_t R, size_t C>\nclass array2d\n{\n   typedef T                 value_type;\n   typedef value_type*       iterator;\n   typedef value_type const* const_iterator;\n   std::vector<T>            arr;\npublic:\n   array2d() : arr(R*C) {}\n   explicit array2d(std::initializer_list<T> l):arr(l) {}\n   constexpr T* data() noexcept { return arr.data(); }\n   constexpr T const * data() const noexcept { return arr.data(); }\n\n   constexpr T& at(size_t const r, size_t const c) \n   {\n      return arr.at(r*C + c);\n   }\n\n   constexpr T const & at(size_t const r, size_t const c) const\n   {\n      return arr.at(r*C + c);\n   }\n\n   constexpr T& operator() (size_t const r, size_t const c)\n   {\n      return arr[r*C + c];\n   }\n\n   constexpr T const & operator() (size_t const r, size_t const c) const\n   {\n      return arr[r*C + c];\n   }\n\n   constexpr bool empty() const noexcept { return R == 0 || C == 0; }\n\n   constexpr size_t size(int const rank) const\n   {\n      if (rank == 1) return R;\n      else if (rank == 2) return C;\n      throw std::out_of_range(\"Rank is out of range!\");\n   }\n\n   void fill(T const & value)\n   {\n      std::fill(std::begin(arr), std::end(arr), value);\n   }\n\n   void swap(array2d & other) noexcept { arr.swap(other.arr); }\n\n   const_iterator begin() const { return arr.data(); }\n   const_iterator end() const   { return arr.data() + arr.size(); }\n   iterator       begin()       { return arr.data(); }\n   iterator       end()         { return arr.data() + arr.size(); }\n};\n```\n\n# 18.具有任意数量参数的最小函数\n\n可以使用可变函数模板编写可以接受可变数量参数的函数模板。为此，我们需要实现编译时递归(实际上只是通过一组重载函数的调用)。下面的代码片段显示了如何实现请求的函数:\n\n```cpp\ntemplate <typename T>\nT minimum(T const a, T const b) { return a < b ? a : b; }\n\ntemplate <typename T1, typename... T>\nT1 minimum(T1 a, T... args)\n{\n   return minimum(a, minimum(args...));\n}\n\nint main()\n{\n   auto x = minimum(5, 4, 2, 3);\n}\n```\n\n为了能够使用用户提供的二进制比较函数，我们需要编写另一个函数模板。比较函数必须是第一个参数，因为它不能跟随函数参数包。另一方面，这不能是先前最小函数的重载，而是一个具有不同名称的函数。原因是编译器无法区分模板参数列表`<typename T1, typename... T>`和`<class Compare, typename T1, typename... T>`。改动很小，在这个片段中应该很容易理解:\n\n```cpp\ntemplate <class Compare, typename T>\nT minimumc(Compare comp, T const a, T const b) \n{ return comp(a, b) ? a : b; }\n\ntemplate <class Compare, typename T1, typename... T>\nT1 minimumc(Compare comp, T1 a, T... args)\n{\n   return minimumc(comp, a, minimumc(comp, args...));\n}\n\nint main()\n{\n   auto y = minimumc(std::less<>(), 3, 2, 1, 0);\n}\n```\n\n# 19.向容器添加一系列值\n\n使用变量函数模板可以编写具有任意数量参数的函数。该函数应该将容器作为第一个参数，后跟一个可变数量的参数，这些参数表示要添加到容器后面的值。然而，使用 fold 表达式可以大大简化编写这样的函数模板。这里显示了这样一个实现:\n\n```cpp\ntemplate<typename C, typename... Args>\nvoid push_back(C& c, Args&&... args)\n{\n   (c.push_back(args), ...);\n}\n```\n\n在下面的列表中可以看到使用这个函数模板和各种容器类型的例子:\n\n```cpp\nint main()\n{\n   std::vector<int> v;\n   push_back(v, 1, 2, 3, 4);\n   std::copy(std::begin(v), std::end(v), \n             std::ostream_iterator<int>(std::cout, \" \"));\n\n   std::list<int> l;\n   push_back(l, 1, 2, 3, 4);\n   std::copy(std::begin(l), std::end(l), \n             std::ostream_iterator<int>(std::cout, \" \"));\n}\n```\n\n# 20.容器任意、全部、无\n\n能够检查可变数量参数存在与否的要求表明，我们应该编写可变函数模板。然而，这些函数需要一个助手函数，一个检查容器中是否找到元素并返回一个`bool`来指示成功或失败的通用函数。由于所有这些我们可以称为`contains_all`、`contains_any`和`contains_none`的函数都是对辅助函数返回的结果应用逻辑运算符，因此我们将使用 fold 表达式来简化代码。在折叠表达式扩展后，短路评估被启用，这意味着我们只评估导致确定结果的元素。因此，如果我们正在寻找所有 1、2 和 3 的存在，并且缺少 2，则函数将在容器中查找值 2 后返回，而不检查值 3:\n\n```cpp\ntemplate<class C, class T>\nbool contains(C const & c, T const & value)\n{\n   return std::end(c) != std::find(std::begin(c), std::end(c), value);\n}\n\ntemplate<class C, class... T>\nbool contains_any(C const & c, T &&... value)\n{\n   return (... || contains(c, value));\n}\n\ntemplate<class C, class... T>\nbool contains_all(C const & c, T &&... value)\n{\n   return (... && contains(c, value));\n}\n\ntemplate<class C, class... T>\nbool contains_none(C const & c, T &&... value)\n{\n   return !contains_any(c, std::forward<T>(value)...);\n}\n```\n\n# 21.系统句柄包装\n\n系统句柄是对系统资源的一种引用形式。因为所有的操作系统至少最初都是用 C 语言编写的，所以句柄的创建和释放是通过专用的系统函数来完成的。这增加了因错误处置(如在例外情况下)而导致资源泄漏的风险。在下面的代码片段中，针对 Windows，您可以看到一个打开、读取并最终关闭文件的函数。然而，这有两个问题:在一种情况下，开发人员在离开函数之前忘记关闭句柄；在另一种情况下，在句柄被正确关闭之前调用抛出的函数，而不会捕获异常。但是，由于函数抛出，清理代码永远不会执行:\n\n```cpp\nvoid bad_handle_example()\n{\n   bool condition1 = false;\n   bool condition2 = true;\n   HANDLE handle = CreateFile(L\"sample.txt\",\n                              GENERIC_READ,\n                              FILE_SHARE_READ,\n                              nullptr,\n                              OPEN_EXISTING,\n                              FILE_ATTRIBUTE_NORMAL,\n                              nullptr);\n\n   if (handle == INVALID_HANDLE_VALUE)\n      return;\n\n   if (condition1)\n   {\n      CloseHandle(handle);\n      return;\n   }\n\n   std::vector<char> buffer(1024);\n   unsigned long bytesRead = 0;\n   ReadFile(handle, \n            buffer.data(), \n            buffer.size(), \n            &bytesRead, \n            nullptr);\n\n   if (condition2)\n   {\n      // oops, forgot to close handle\n      return;\n   }\n\n   // throws exception; the next line will not execute\n   function_that_throws();\n\n   CloseHandle(handle);\n}\n```\n\nC++ 包装器类可以确保在包装器对象超出范围并被销毁时(无论是通过正常执行路径还是作为异常的结果)正确处置句柄。正确的实现应该考虑不同类型的句柄，用一定范围的值来指示无效的句柄(如 0/null 或-1)。下面显示的实现提供了:\n\n*   对象被破坏时句柄的显式获取和自动释放\n*   移动语义以实现句柄所有权的转移\n*   比较运算符检查两个对象是否引用同一个句柄\n*   交换和重置等附加操作\n\nThe implementation shown here is a modified version of the handle class implemented by Kenny Kerr and published in the article *Windows with C++ - C++ and the Windows API*, MSDN Magazine, July 2011, [https://msdn.microsoft.com/en-us/magazine/hh288076.aspx](https://msdn.microsoft.com/en-us/magazine/hh288076.aspx). Although the handle traits shown here refer to Windows handles, it should be fairly simple to write traits appropriate for other platforms.\n\n```cpp\ntemplate <typename Traits>\nclass unique_handle\n{\n   using pointer = typename Traits::pointer;\n   pointer m_value;\npublic:\n   unique_handle(unique_handle const &) = delete;\n   unique_handle& operator=(unique_handle const &) = delete;\n\n   explicit unique_handle(pointer value = Traits::invalid()) noexcept\n      :m_value{ value }\n   {}\n\n   unique_handle(unique_handle && other) noexcept\n      : m_value{ other.release() }\n   {}\n\n   unique_handle& operator=(unique_handle && other) noexcept\n   {\n      if (this != &other)\n         reset(other.release());\n      return *this;\n   }\n\n   ~unique_handle() noexcept\n   {\n      Traits::close(m_value);\n   }\n\n   explicit operator bool() const noexcept\n   {\n      return m_value != Traits::invalid();\n   }\n\n   pointer get() const noexcept { return m_value; }\n\n   pointer release() noexcept\n   {\n      auto value = m_value;\n      m_value = Traits::invalid();\n      return value;\n   }\n\n   bool reset(pointer value = Traits::invalid()) noexcept\n   {\n      if (m_value != value)\n      {\n         Traits::close(m_value);\n         m_value = value;\n      }\n      return static_cast<bool>(*this);\n   }\n\n   void swap(unique_handle<Traits> & other) noexcept\n   {\n      std::swap(m_value, other.m_value);\n   }\n};\n\ntemplate <typename Traits>\nvoid swap(unique_handle<Traits> & left, unique_handle<Traits> & right) noexcept\n{\n   left.swap(right);\n}\n\ntemplate <typename Traits>\nbool operator==(unique_handle<Traits> const & left,\n                unique_handle<Traits> const & right) noexcept\n{\n   return left.get() == right.get();\n}\n\ntemplate <typename Traits>\nbool operator!=(unique_handle<Traits> const & left,\n                unique_handle<Traits> const & right) noexcept\n{\n   return left.get() != right.get();\n}\n\nstruct null_handle_traits\n{\n   using pointer = HANDLE;\n   static pointer invalid() noexcept { return nullptr; }\n   static void close(pointer value) noexcept\n   {\n      CloseHandle(value);\n   }\n};\n\nstruct invalid_handle_traits\n{\n   using pointer = HANDLE;\n   static pointer invalid() noexcept { return INVALID_HANDLE_VALUE; }\n   static void close(pointer value) noexcept\n   {\n      CloseHandle(value);\n   }\n};\n\nusing null_handle = unique_handle<null_handle_traits>;\nusing invalid_handle = unique_handle<invalid_handle_traits>;\n```\n\n定义了这个句柄类型后，我们可以用更简单的术语重写前面的例子，避免所有那些由于异常发生而没有正确关闭句柄的问题，或者仅仅因为开发人员在不再需要时忘记释放资源。这段代码更简单、更健壮:\n\n```cpp\nvoid good_handle_example()\n{\n   bool condition1 = false;\n   bool condition2 = true;\n\n   invalid_handle handle{\n      CreateFile(L\"sample.txt\",\n                 GENERIC_READ,\n                 FILE_SHARE_READ,\n                 nullptr,\n                 OPEN_EXISTING,\n                 FILE_ATTRIBUTE_NORMAL,\n                 nullptr) };\n\n   if (!handle) return;\n\n   if (condition1) return;\n\n   std::vector<char> buffer(1024);\n   unsigned long bytesRead = 0;\n   ReadFile(handle.get(),\n            buffer.data(),\n            buffer.size(),\n            &bytesRead,\n            nullptr);\n\n   if (condition2) return;\n\n   function_that_throws();\n}\n```\n\n# 22.各种温标的文字\n\n为了满足这一要求，我们需要为几种类型、运算符和函数提供一个实现:\n\n*   支持的温标计数称为`scale`。\n*   表示温度值的类模板，用刻度参数化，称为`quantity`。\n*   比较运算符`==`、`!=`、`<`、`>`、`<=`和`>=`，它们比较同一时间的两个量。\n*   加减相同数量类型值的算术运算符`+`和`-`。另外，我们可以实现成员运营商`+=`和`-+`。\n*   将温度从一个刻度转换到另一个刻度的函数模板，称为`temperature_cast`。这个函数本身不执行转换，而是使用类型特征来完成转换。\n*   文字运算符`\"\"_deg`、`\"\"_f`和`\"\"_k`，用于创建用户定义的温度文字。\n\nFor brevity, the following snippet only contains the code that handles Celsius and Fahrenheit temperatures. You should take it as a further exercise to extend the code with support for the Kelvin scale. The code accompanying the book contains the full implementation of all three required scales.\n\n`are_equal()`函数是一个用于比较浮点值的实用函数:\n\n```cpp\nbool are_equal(double const d1, double const d2, \n               double const epsilon = 0.001)\n{\n   return std::fabs(d1 - d2) < epsilon;\n}\n```\n\n可能的温标的枚举和代表温度值的类别定义如下:\n\n```cpp\nnamespace temperature\n{\n   enum class scale { celsius, fahrenheit, kelvin };\n\n   template <scale S>\n   class quantity\n   {\n      const double amount;\n   public:\n      constexpr explicit quantity(double const a) : amount(a) {}\n      explicit operator double() const { return amount; }\n   };\n}\n```\n\n这里可以看到`quantity<S>`类的比较运算符:\n\n```cpp\nnamespace temperature \n{\n   template <scale S>\n   inline bool operator==(quantity<S> const & lhs, quantity<S> const & rhs)\n   {\n      return are_equal(static_cast<double>(lhs), static_cast<double>(rhs));\n   }\n\n   template <scale S>\n   inline bool operator!=(quantity<S> const & lhs, quantity<S> const & rhs)\n   {\n      return !(lhs == rhs);\n   }\n\n   template <scale S>\n   inline bool operator< (quantity<S> const & lhs, quantity<S> const & rhs)\n   {\n      return static_cast<double>(lhs) < static_cast<double>(rhs);\n   }\n\n   template <scale S>\n   inline bool operator> (quantity<S> const & lhs, quantity<S> const & rhs)\n   {\n      return rhs < lhs;\n   }\n\n   template <scale S>\n   inline bool operator<=(quantity<S> const & lhs, quantity<S> const & rhs)\n   {\n      return !(lhs > rhs);\n   }\n\n   template <scale S>\n   inline bool operator>=(quantity<S> const & lhs, quantity<S> const & rhs)\n   {\n      return !(lhs < rhs);\n   }\n\n   template <scale S>\n   constexpr quantity<S> operator+(quantity<S> const &q1, \n                                   quantity<S> const &q2)\n   {\n      return quantity<S>(static_cast<double>(q1) + \n                         static_cast<double>(q2));\n   }\n\n   template <scale S>\n   constexpr quantity<S> operator-(quantity<S> const &q1, \n                                   quantity<S> const &q2)\n   {\n      return quantity<S>(static_cast<double>(q1) - \n                         static_cast<double>(q2));\n   }\n}\n```\n\n为了在不同尺度的温度值之间进行转换，我们将定义一个名为`temperature_cast()`的函数模板，该模板利用几个类型特征来执行实际转换。所有这些都显示在这里，虽然不是所有的类型特征；其他的可以在本书附带的代码中找到:\n\n```cpp\nnamespace temperature\n{\n   template <scale S, scale R>\n   struct conversion_traits\n   {\n      static double convert(double const value) = delete;\n   };\n\n   template <>\n   struct conversion_traits<scale::celsius, scale::fahrenheit>\n   {\n      static double convert(double const value)\n      {\n         return (value * 9) / 5 + 32;\n      }\n   };\n\n   template <>\n   struct conversion_traits<scale::fahrenheit, scale::celsius>\n   {\n      static double convert(double const value)\n      {\n         return (value - 32) * 5 / 9;\n      }\n   };\n\n   template <scale R, scale S>\n   constexpr quantity<R> temperature_cast(quantity<S> const q)\n   {\n      return quantity<R>(conversion_traits<S, R>::convert(\n         static_cast<double>(q)));\n   }\n}\n```\n\n下面的代码片段显示了用于创建温度值的文字运算符。这些操作符被定义在一个单独的命名空间中，称为`temperature_scale_literals`，这是一个很好的做法，以便将名称与其他文字操作符冲突的风险降至最低:\n\n```cpp\nnamespace temperature\n{\n   namespace temperature_scale_literals\n   {\n      constexpr quantity<scale::celsius> operator \"\" _deg(\n         long double const amount)\n      {\n         return quantity<scale::celsius> {static_cast<double>(amount)};\n      }\n\n      constexpr quantity<scale::fahrenheit> operator \"\" _f(\n         long double const amount)\n      {\n         return quantity<scale::fahrenheit> {static_cast<double>(amount)};\n      }\n   }\n}\n```\n\n以下示例显示了如何定义两个温度值，一个以摄氏度为单位，一个以华氏度为单位，并在两者之间进行转换:\n\n```cpp\nint main()\n{\n   using namespace temperature;\n   using namespace temperature_scale_literals;\n\n   auto t1{ 36.5_deg };\n   auto t2{ 79.0_f };\n\n   auto tf = temperature_cast<scale::fahrenheit>(t1);\n   auto tc = temperature_cast<scale::celsius>(tf);\n   assert(t1 == tc);\n}\n```"
  },
  {
    "path": "docs/mod-cpp-challenge/03.md",
    "content": "# 三、字符串和正则表达式\n\n# 问题\n\n# 23.二进制到字符串转换\n\n编写一个函数，在给定 8 位整数范围(如数组或向量)的情况下，返回包含输入数据的十六进制表示的字符串。该函数应该能够同时生成大写和小写内容。以下是一些输入和输出示例:\n\n输入:`{ 0xBA, 0xAD, 0xF0, 0x0D }`，输出:`\"BAADF00D\"`或`\"baadf00d\"`\n输入:`{ 1,2,3,4,5,6 }`，输出:`\"010203040506\"`\n\n# 24.字符串到二进制的转换\n\n编写一个函数，在给定包含十六进制数字作为输入参数的字符串的情况下，返回一个 8 位整数的向量，该向量表示字符串内容的数字反序列化。以下是一些例子:\n\n输入:`\"BAADF00D\"`或`\"baadF00D\"`，输出:`{0xBA, 0xAD, 0xF0, 0x0D}`\n输入`\"010203040506\"`，输出:`{1, 2, 3, 4, 5, 6}`\n\n# 25.将文章标题大写\n\n编写一个函数，将输入文本转换为大写版本，其中每个单词都以大写字母开头，所有其他字母都以小写字母开头。例如，文本`\"the c++ challenger\"`应该转换为`\"The C++ Challenger\"`。\n\n# 26.用分隔符将字符串连接在一起\n\n编写一个函数，在给定字符串列表和分隔符的情况下，通过连接用指定分隔符分隔的所有输入字符串来创建一个新字符串。分隔符不能出现在最后一个字符串之后，并且当没有提供输入字符串时，函数必须返回一个空字符串。\n\n示例:输入`{ \"this\",\"is\",\"an\",\"example\" }`和分隔符`' '`(空格)，输出:`\"this is an example\"`。\n\n# 27.使用可能的分隔符列表将字符串拆分为标记\n\n编写一个函数，给定一个字符串和一个可能的分隔符列表，将字符串拆分成由任何分隔符分隔的标记，并在`std::vector`中返回它们。\n\n示例:输入:`\"this,is.a sample!!\"`带分隔符`\",.! \"`，输出:`{\"this\", \"is\", \"a\", \"sample\"}`。\n\n# 28.最长回文子串\n\n编写一个函数，在给定输入字符串的情况下，定位并返回字符串中最长的回文序列。如果存在多个相同长度的回文，则应返回第一个回文。\n\n# 29.车牌验证\n\n考虑格式为`LLL-LL DDD`或`LLL-LL DDDD`的车牌(其中`L`是从 *A* 到 *Z* 的大写字母，`D`是数字)，写:\n\n*   验证车牌号码格式是否正确的一种功能\n*   一个函数，给定输入文本，提取并返回文本中找到的所有车牌号码\n\n# 30.正在提取网址部分\n\n编写一个函数，给定一个代表 URL 的字符串，解析并提取 URL 的各个部分(协议、域、端口、路径、查询和片段)。\n\n# 31.在字符串中转换日期\n\n编写一个函数，给定一个包含格式为`dd.mm.yyyy`或`dd-mm-yyyy`的日期的文本，转换该文本，使其包含格式为`yyyy-mm-dd`的日期。\n\n# 解决方法\n\n# 23.二进制到字符串转换\n\n为了编写一个能够处理各种范围的通用函数，比如`std::array`、`std::vector`、类 C 数组或者其他，我们应该编写一个函数模板。在下面，有两个重载；一个使用容器作为参数和指示大小写样式的标志，另一个使用一对迭代器(标记第一个迭代器，然后一个迭代器越过范围的结束元素)和指示大小写的标志。该范围的内容被写入一个`std::ostringstream`对象，带有适当的输入/输出操纵器，例如宽度、填充字符或大小写标志:\n\n```cpp\ntemplate <typename Iter>\nstd::string bytes_to_hexstr(Iter begin, Iter end, \n                            bool const uppercase = false)\n{\n   std::ostringstream oss;\n   if(uppercase) oss.setf(std::ios_base::uppercase);\n   for (; begin != end; ++ begin)\n     oss << std::hex << std::setw(2) << std::setfill('0') \n         << static_cast<int>(*begin);\n   return oss.str();\n}\n\ntemplate <typename C>\nstd::string bytes_to_hexstr(C const & c, bool const uppercase = false)\n{\n   return bytes_to_hexstr(std::cbegin(c), std::cend(c), uppercase);\n}\n```\n\n这些功能可以如下使用:\n\n```cpp\nint main()\n{\n   std::vector<unsigned char> v{ 0xBA, 0xAD, 0xF0, 0x0D };\n   std::array<unsigned char, 6> a{ {1,2,3,4,5,6} };\n   unsigned char buf[5] = {0x11, 0x22, 0x33, 0x44, 0x55};\n\n   assert(bytes_to_hexstr(v, true) == \"BAADF00D\");\n   assert(bytes_to_hexstr(a, true) == \"010203040506\");\n   assert(bytes_to_hexstr(buf, true) == \"1122334455\");\n\n   assert(bytes_to_hexstr(v) == \"baadf00d\");\n   assert(bytes_to_hexstr(a) == \"010203040506\");\n   assert(bytes_to_hexstr(buf) == \"1122334455\");\n}\n```\n\n# 24.字符串到二进制的转换\n\n这里请求的操作与上一个问题中实现的操作相反。然而，这一次，我们可以编写一个函数，而不是函数模板。输入是一个`std::string_view`，这是一个轻量级的字符序列包装器。输出是一个 8 位无符号整数向量。下面的`hexstr_to_bytes`函数将每两个文本字符转换成一个`unsigned char`值(`\"A0\"`变成`0xA0`，将它们放入一个`std::vector`，并返回向量:\n\n```cpp\nunsigned char hexchar_to_int(char const ch)\n{\n   if (ch >= '0' && ch <= '9') return ch - '0';\n   if (ch >= 'A' && ch <= 'F') return ch - 'A' + 10;\n   if (ch >= 'a' && ch <= 'f') return ch - 'a' + 10;\n      throw std::invalid_argument(\"Invalid hexadecimal character\");\n}\n\nstd::vector<unsigned char> hexstr_to_bytes(std::string_view str)\n{\n   std::vector<unsigned char> result;\n   for (size_t i = 0; i < str.size(); i += 2) \n   {\n      result.push_back(\n         (hexchar_to_int(str[i]) << 4) | hexchar_to_int(str[i+1]));\n   }\n   return result;\n}\n```\n\nThis function assumes the input string contains an even number of hexadecimal digits. In cases where the input string contains an odd number of hexadecimal digits, the last one is discarded (so that `\"BAD\"` becomes `{0xBA}`). As a further exercise, modify the preceding function so that, instead of discarding the last odd digit, it considers a leading zero so that `\"BAD\"` becomes `{0x0B, 0xAD}`. Also, as yet another exercise, you can write a version of the function that deserializes content that has the hexadecimal digits separated by a delimiter, such as space (for example `\"BA AD F0 0D\"`).\n\n下一个代码示例显示了如何使用该函数:\n\n```cpp\nint main()\n{\n   std::vector<unsigned char> expected{ 0xBA, 0xAD, 0xF0, 0x0D, 0x42 };\n   assert(hexstr_to_bytes(\"BAADF00D42\") == expected);\n   assert(hexstr_to_bytes(\"BaaDf00d42\") == expected);\n}\n```\n\n# 25.将文章标题大写\n\n如下实现的函数模板`capitalize()`可以处理任何类型的字符串。它不修改输入字符串，而是创建一个新字符串。为此，它使用了`std::stringstream`。它会遍历输入字符串中的所有字符，并在每次遇到空格或标点符号时向`true`设置一个指示新单词的标志。当输入字符代表单词中的第一个字符时，它们会转换为大写，否则会转换为小写:\n\n```cpp\ntemplate <class Elem>\nusing tstring = std::basic_string<Elem, std::char_traits<Elem>, \n                                  std::allocator<Elem>>;\ntemplate <class Elem>\nusing tstringstream = std::basic_stringstream<\n   Elem, std::char_traits<Elem>, std::allocator<Elem>>;\n\ntemplate <class Elem>\ntstring<Elem> capitalize(tstring<Elem> const & text)\n{\n   tstringstream<Elem> result;\n   bool newWord = true;\n   for (auto const ch : text)\n   {\n      newWord = newWord || std::ispunct(ch) || std::isspace(ch);\n      if (std::isalpha(ch))\n      {\n         if (newWord)\n         {\n            result << static_cast<Elem>(std::toupper(ch));\n            newWord = false;\n         }\n         else\n            result << static_cast<Elem>(std::tolower(ch));\n      }\n      else result << ch;\n   }\n   return result.str();\n}\n```\n\n在下面的程序中，您可以看到如何使用这个函数来大写文本:\n\n```cpp\nint main()\n{\n   using namespace std::string_literals;\n   assert(\"The C++ Challenger\"s ==\n          capitalize(\"the c++ challenger\"s));\n   assert(\"This Is An Example, Should Work!\"s == \n          capitalize(\"THIS IS an ExamplE, should wORk!\"s));\n}\n```\n\n# 26.用分隔符将字符串连接在一起\n\n下面的代码中列出了两个名为`join_strings()`的重载。一个接受一个字符串容器和一个指向表示分隔符的字符序列的指针，而另一个接受两个随机访问迭代器和一个分隔符，前者表示区域的第一个元素，后者表示最后一个元素。它们都使用输出字符串流和`std::copy`函数，返回通过连接所有输入字符串而创建的新字符串。这个通用函数将指定范围内的所有元素复制到输出范围，由输出迭代器表示。我们这里使用的是一个`std::ostream_iterator`，每次迭代器被赋值时，它使用`operator<<`将赋值写入指定的输出流:\n\n```cpp\ntemplate <typename Iter>\nstd::string join_strings(Iter begin, Iter end, \n                         char const * const separator)\n{\n   std::ostringstream os;\n   std::copy(begin, end-1, \n             std::ostream_iterator<std::string>(os, separator));\n   os << *(end-1);\n   return os.str();\n}\n\ntemplate <typename C>\nstd::string join_strings(C const & c, char const * const separator)\n{\n   if (c.size() == 0) return std::string{};\n   return join_strings(std::begin(c), std::end(c), separator);\n}\n\nint main()\n{\n   using namespace std::string_literals;\n   std::vector<std::string> v1{ \"this\",\"is\",\"an\",\"example\" };\n   std::vector<std::string> v2{ \"example\" };\n   std::vector<std::string> v3{ };\n\n   assert(join_strings(v1, \" \") == \"this is an example\"s);\n   assert(join_strings(v2, \" \") == \"example\"s);\n   assert(join_strings(v3, \" \") == \"\"s);\n}\n```\n\nAs a further exercise, you should modify the overload that takes iterators as arguments so that it works with other types of iterators, such as bidirectional iterators, thereby enabling the use of this function with lists or other containers.\n\n# 27.使用可能的分隔符列表将字符串拆分为标记\n\n拆分函数的两个不同版本如下所示:\n\n*   第一个使用单个字符作为分隔符。为了分割输入字符串，它使用一个用输入字符串的内容初始化的字符串流，使用`std::getline()`从中读取数据块，直到遇到下一个分隔符或行尾字符。\n*   第二个使用可能的字符分隔符列表，在`std::string`中指定。它使用`std:string::find_first_of()`定位任何分隔符字符的第一个位置，从给定的位置开始。它循环执行，直到处理完整个输入字符串。提取的子串被添加到结果向量:\n\n```cpp\ntemplate <class Elem>\nusing tstring = std::basic_string<Elem, std::char_traits<Elem>, \n                                  std::allocator<Elem>>;\n\ntemplate <class Elem>\nusing tstringstream = std::basic_stringstream<\n   Elem, std::char_traits<Elem>, std::allocator<Elem>>;\n\ntemplate<typename Elem>\ninline std::vector<tstring<Elem>> split(tstring<Elem> text, \n                                        Elem const delimiter)\n{\n   auto sstr = tstringstream<Elem>{ text };\n   auto tokens = std::vector<tstring<Elem>>{};\n   auto token = tstring<Elem>{};\n   while (std::getline(sstr, token, delimiter))\n   {\n      if (!token.empty()) tokens.push_back(token);\n   }\n   return tokens;\n}\n\ntemplate<typename Elem>\ninline std::vector<tstring<Elem>> split(tstring<Elem> text, \n                                        tstring<Elem> const & delimiters)\n{\n   auto tokens = std::vector<tstring<Elem>>{};\n   size_t pos, prev_pos = 0;\n   while ((pos = text.find_first_of(delimiters, prev_pos)) != \n   std::string::npos)\n   {\n      if (pos > prev_pos)\n      tokens.push_back(text.substr(prev_pos, pos - prev_pos));\n      prev_pos = pos + 1;\n   }\n   if (prev_pos < text.length())\n   tokens.push_back(text.substr(prev_pos, std::string::npos));\n   return tokens;\n}\n```\n\n以下示例代码显示了如何使用一个分隔符或多个分隔符拆分不同字符串的两个示例:\n\n```cpp\nint main()\n{\n   using namespace std::string_literals;\n   std::vector<std::string> expected{\"this\", \"is\", \"a\", \"sample\"};\n   assert(expected == split(\"this is a sample\"s, ' '));\n   assert(expected == split(\"this,is a.sample!!\"s, \",.! \"s));\n}\n```\n\n# 28.最长回文子串\n\n这个问题最简单的解决方案是尝试蛮力方法，检查每个子串是否是回文。但是，这意味着我们需要检查 *C(N，2)* 子串(其中 *N* 是字符串中的字符数)，时间复杂度为 *![](img/dc267a26-dc8b-4f24-b9bf-4779c5f14379.png)* 。通过存储子问题的结果，复杂性可以降低到![](img/bf882877-3685-4be7-9ea5-050cbbec54d3.png)。为此，我们需要一个大小为![](img/9372c191-bf8e-4c7a-930f-b2019a6e7302.png)的布尔值表，其中`[i, j]`处的元素指示从位置`i`到`j`的子串是否是回文。我们首先用`true`(单字符回文)初始化所有元素`[i,i]`，用`true`初始化所有元素`[i,i+i]`，用于所有连续的两个相同字符(双字符回文)。然后我们继续检查大于两个字符的子字符串，如果`[i+i,j-1]`处的元素是`true`，并且字符串中`i`和`j`位置上的字符也相等，则将`[i,j]`处的元素设置为`true`。一路上，我们保留最长回文子串的起始位置和长度，以便在计算完表后提取它。\n\n在代码中，此解决方案如下所示:\n\n```cpp\nstd::string longest_palindrome(std::string_view str)\n{\n   size_t const len = str.size();\n   size_t longestBegin = 0;\n   size_t maxLen = 1;\n\n   std::vector<bool> table(len * len, false);\n   for (size_t i = 0; i < len; i++)\n      table[i*len + i] = true;\n\n   for (size_t i = 0; i < len - 1; i++)\n   {\n      if (str[i] == str[i + 1]) \n      {\n         table[i*len + i + 1] = true;\n         if (maxLen < 2)\n         {\n            longestBegin = i;\n            maxLen = 2;\n         }\n      }\n   }\n\n   for (size_t k = 3; k <= len; k++)\n   {\n      for (size_t i = 0; i < len - k + 1; i++)\n      {\n         size_t j = i + k - 1;\n         if (str[i] == str[j] && table[(i + 1)*len + j - 1])\n         {\n            table[i*len +j] = true;\n            if (maxLen < k)\n            {\n               longestBegin = i;\n               maxLen = k;\n            }\n         }\n      }\n   }\n   return std::string(str.substr(longestBegin, maxLen));\n}\n```\n\n以下是`longest_palindrome()`功能的一些测试案例:\n\n```cpp\nint main()\n{\n   using namespace std::string_literals;\n   assert(longest_palindrome(\"sahararahnide\") == \"hararah\");\n   assert(longest_palindrome(\"level\") == \"level\");\n   assert(longest_palindrome(\"s\") == \"s\");\n}\n```\n\n# 29.车牌验证\n\n解决这个问题最简单的方法是使用正则表达式。符合所述格式的正则表达式为`\"[A-Z]{3}-[A-Z]{2} \\d{3,4}\"`。\n\n第一个函数只需要验证输入字符串只包含与该正则表达式匹配的文本。为此，我们可以使用`std::regex_match()`，如下所示:\n\n```cpp\nbool validate_license_plate_format(std::string_view str)\n{\n   std::regex rx(R\"([A-Z]{3}-[A-Z]{2} \\d{3,4})\");\n   return std::regex_match(str.data(), rx);\n}\n\nint main()\n{\n   assert(validate_license_plate_format(\"ABC-DE 123\"));\n   assert(validate_license_plate_format(\"ABC-DE 1234\"));\n   assert(!validate_license_plate_format(\"ABC-DE 12345\"));\n   assert(!validate_license_plate_format(\"abc-de 1234\"));\n}\n```\n\n第二个功能略有不同。它必须标识字符串中正则表达式的所有匹配项，而不是匹配输入字符串。正则表达式将因此变为`\"([A-Z]{3}-[A-Z]{2} \\d{3,4})*\"`。为了遍历所有匹配，我们必须使用`std::sregex_iterator`，如下所示:\n\n```cpp\nstd::vector<std::string> extract_license_plate_numbers(\n                            std::string const & str)\n{\n   std::regex rx(R\"(([A-Z]{3}-[A-Z]{2} \\d{3,4})*)\");\n   std::smatch match;\n   std::vector<std::string> results;\n\n   for(auto i = std::sregex_iterator(std::cbegin(str), std::cend(str), rx); \n       i != std::sregex_iterator(); ++ i) \n   {\n      if((*i)[1].matched)\n      results.push_back(i->str());\n   }\n   return results;\n}\n\nint main()\n{\n   std::vector<std::string> expected {\n      \"AAA-AA 123\", \"ABC-DE 1234\", \"XYZ-WW 0001\"};\n   std::string text(\"AAA-AA 123qwe-ty 1234 ABC-DE 123456..XYZ-WW 0001\");\n   assert(expected == extract_license_plate_numbers(text));\n}\n```\n\n# 30.正在提取网址部分\n\n这个问题也适合使用正则表达式来解决。然而，找到一个可以匹配任何网址的正则表达式是一项困难的任务。本练习的目的是帮助您使用 regex 库练习技巧，而不是为了这个特殊的目的找到最终的正则表达式。因此，这里使用的正则表达式仅用于教学目的。\n\nYou can try regular expressions using online testers and debuggers, such as [https://regex101.com/](https://regex101.com/). This can be useful in order to work out your regular expressions and try them against various datasets.\n\n对于这个任务，我们将考虑一个网址有以下几个部分:`protocol`和`domain`是强制的，`port`、`path`、`query`和`fragment`都是可选的。以下结构用于返回解析 URL 的结果(或者，您可以返回一个元组，并使用结构化绑定将变量绑定到元组的各个子部分):\n\n```cpp\nstruct uri_parts\n{\n   std::string                protocol;\n   std::string                domain;\n   std::optional<int>         port;\n   std::optional<std::string> path;\n   std::optional<std::string> query;\n   std::optional<std::string> fragment;\n};\n```\n\n一个可以解析网址并提取和返回其部分的函数可以有以下实现。请注意，返回类型是`std::optional<uri_parts>`，因为该函数可能无法将输入字符串与正则表达式匹配；在这种情况下，返回值为`std::nullopt`:\n\n```cpp\nstd::optional<uri_parts> parse_uri(std::string uri)\n{\n   std::regex rx(R\"(^(\\w+):\\/\\/([\\w.-]+)(:(\\d+))?([\\w\\/\\.]+)?(\\?([\\w=&]*)(#?(\\w+))?)?$)\");\n   auto matches = std::smatch{};\n   if (std::regex_match(uri, matches, rx))\n   {\n      if (matches[1].matched && matches[2].matched)\n      {\n         uri_parts parts;\n         parts.protocol = matches[1].str();\n         parts.domain = matches[2].str();\n         if (matches[4].matched)\n            parts.port = std::stoi(matches[4]);\n         if (matches[5].matched)\n            parts.path = matches[5];\n         if (matches[7].matched)\n            parts.query = matches[7];\n         if (matches[9].matched)\n            parts.fragment = matches[9];\n         return parts;\n      }\n   }\n   return {};\n}\n```\n\n以下程序使用包含不同部分的两个网址测试`parse_uri()`功能:\n\n```cpp\nint main()\n{\n   auto p1 = parse_uri(\"https://packt.com\");\n   assert(p1.has_value());\n   assert(p1->protocol == \"https\");\n   assert(p1->domain == \"packt.com\");\n   assert(!p1->port.has_value());\n   assert(!p1->path.has_value());\n   assert(!p1->query.has_value());\n   assert(!p1->fragment.has_value());\n\n   auto p2 = parse_uri(\"https://bbc.com:80/en/index.html?lite=true#ui\");\n   assert(p2.has_value());\n   assert(p2->protocol == \"https\");\n   assert(p2->domain == \"bbc.com\");\n   assert(p2->port == 80);\n   assert(p2->path.value() == \"/en/index.html\");\n   assert(p2->query.value() == \"lite=true\");\n   assert(p2->fragment.value() == \"ui\");\n}\n```\n\n# 31.在字符串中转换日期\n\n使用`std::regex_replace()`可以用正则表达式进行文本转换。能够匹配指定格式日期的正则表达式是`(\\d{1,2})(\\.|-|/)(\\d{1,2})(\\.|-|/)(\\d{4})`。这个正则表达式定义了五个捕获组；1 <sup>st</sup> 为日，2 <sup>nd</sup> 为分隔符(`.`或`-`)，3 <sup>rd</sup> 为月，4 <sup>th</sup> 再次为分隔符(`.`或`-`)，5 <sup>th</sup> 为年。\n\n因为我们要将日期从格式`dd.mm.yyyy`或`dd-mm-yyyy`转换为`yyyy-mm-dd`，所以`std::regex_replace()`的正则表达式替换格式字符串应该是`\"($5-$3-$1)\"`:\n\n```cpp\nstd::string transform_date(std::string_view text)\n{\n   auto rx = std::regex{ R\"((\\d{1,2})(\\.|-|/)(\\d{1,2})(\\.|-|/)(\\d{4}))\" };\n   return std::regex_replace(text.data(), rx, R\"($5-$3-$1)\");\n}\n\nint main()\n{\n   using namespace std::string_literals;\n   assert(transform_date(\"today is 01.12.2017!\"s) == \n          \"today is 2017-12-01!\"s);\n}\n```"
  },
  {
    "path": "docs/mod-cpp-challenge/04.md",
    "content": "# 四、流和文件系统\n\n# 问题\n\n# 32.帕斯卡三角形\n\n编写一个函数，向控制台打印多达 10 行的帕斯卡三角形。\n\n# 33.进程列表的表格打印\n\n假设您有一个系统中所有进程列表的快照。每个进程的信息包括名称、标识符、状态(可以是运行中的*或暂停中的*)、帐户名(进程在该帐户名下运行)、内存大小(以字节为单位)和平台(可以是 32 位或 64 位)。您的任务是编写一个函数，获取这样一个进程列表，并以表格格式按字母顺序将它们打印到控制台。所有列都必须左对齐，除了内存列必须右对齐。内存大小的值必须以 KB 为单位显示。下面是这个函数的输出示例:**\n\n```cpp\nchrome.exe      1044   Running    marius.bancila    25180  32-bit\nchrome.exe      10100  Running    marius.bancila   227756  32-bit\ncmd.exe         512    Running    SYSTEM               48  64-bit\nexplorer.exe    7108   Running    marius.bancila    29529  64-bit\nskype.exe       22456  Suspended  marius.bancila      656  64-bit\n```\n\n# 34.从文本文件中删除空行\n\n编写一个程序，给定一个文本文件的路径，通过删除所有空行来修改文件。仅包含空格的行被认为是空的。\n\n# 35.计算目录的大小\n\n编写一个递归计算目录大小(以字节为单位)的函数。应该可以指出是否应该遵循符号链接。\n\n# 36.删除超过给定日期的文件\n\n编写一个函数，在给定目录路径和持续时间的情况下，以递归方式删除所有早于指定持续时间的条目(文件或子目录)。持续时间可以表示任何东西，例如天、小时、分钟、秒等，或者它们的组合，例如一小时二十分钟。如果指定的目录本身早于给定的持续时间，则应将其完全删除。\n\n# 37.在目录中查找与正则表达式匹配的文件\n\n编写一个函数，在给定目录路径和正则表达式的情况下，返回名称与正则表达式匹配的所有目录条目的列表。\n\n# 38.临时日志文件\n\n创建一个日志类，将文本消息写入可丢弃的文本文件。文本文件应具有唯一的名称，并且必须位于临时目录中。除非另外指定，否则当类的实例被销毁时，应该删除此日志文件。但是，应该可以通过将其移动到永久位置来保留日志文件。\n\n# 解决方法\n\n# 32.帕斯卡三角形\n\n帕斯卡三角形是一种表示二项式系数的结构。三角形从一个值为 1 的行开始。每一行的元素都是通过对上面的数字进行求和来构造的，向左和向右，并将空白条目视为 0。下面是一个五行三角形的例子:\n\n```cpp\n 1\n 1   1\n 1   2   1\n 1   3   3   1\n1   4   6   4   1\n```\n\n要打印三角形，我们必须:\n\n*   将输出位置向右移动适当数量的空格，使顶部投影到三角形底部的中间。\n*   通过对上述左右值求和来计算每个值。一个更简单的公式是，对于一行`i`和一列`j`，每个新值`x`等于`x`的前一个值乘以`(i - j) / (j + 1)`，其中`x`从 1 开始。\n\n下面是打印三角形的函数的可能实现:\n\n```cpp\nunsigned int number_of_digits(unsigned int const i)\n{\n   return i > 0 ? (int)log10((double)i) + 1 : 1;\n}\n\nvoid print_pascal_triangle(int const n)\n{\n   for (int i = 0; i < n; i++) \n   {\n      auto x = 1;\n      std::cout << std::string((n - i - 1)*(n / 2), ' ');\n      for (int j = 0; j <= i; j++) \n      {\n         auto y = x;\n         x = x * (i - j) / (j + 1);\n         auto maxlen = number_of_digits(x) - 1;\n         std::cout << y << std::string(n - 1 - maxlen - n%2, ' ');\n      }\n      std::cout << std::endl;\n   }\n}\n```\n\n以下程序要求用户输入级别数，并将三角形打印到控制台:\n\n```cpp\nint main()\n{\n   int n = 0;\n   std::cout << \"Levels (up to 10): \";\n   std::cin >> n;\n   if (n > 10)\n      std::cout << \"Value too large\" << std::endl;\n   else\n      print_pascal_triangle(n);\n}\n```\n\n# 33.进程列表的表格打印\n\n为了解决这个问题，我们将考虑以下表示进程信息的类:\n\n```cpp\nenum class procstatus {suspended, running};\nenum class platforms {p32bit, p64bit};\n\nstruct procinfo\n{\n   int         id;\n   std::string name;\n   procstatus  status;\n   std::string account;\n   size_t      memory;\n   platforms   platform;\n};\n```\n\n为了将状态和平台打印为文本而不是数值，我们需要从枚举到`std::string`的转换函数:\n\n```cpp\nstd::string status_to_string(procstatus const status)\n{\n   if (status == procstatus::suspended) return \"suspended\";\n   else return \"running\";\n}\n\nstd::string platform_to_string(platforms const platform)\n{\n   if (platform == platforms::p32bit) return \"32-bit\";\n   else return \"64-bit\";\n}\n```\n\n流程需要按流程名称的字母顺序排序。因此，第一步是对流程的输入范围进行排序。对于打印本身，我们应该使用输入/输出操纵器:\n\n```cpp\nvoid print_processes(std::vector<procinfo> processes)\n{\n   std::sort(\n      std::begin(processes), std::end(processes),\n      [](procinfo const & p1, procinfo const & p2) {\n         return p1.name < p2.name; });\n\n   for (auto const & pi : processes)\n   {\n      std::cout << std::left << std::setw(25) << std::setfill(' ')\n                << pi.name;\n      std::cout << std::left << std::setw(8) << std::setfill(' ')\n                << pi.id;\n      std::cout << std::left << std::setw(12) << std::setfill(' ')\n                << status_to_string(pi.status);\n      std::cout << std::left << std::setw(15) << std::setfill(' ')\n                << pi.account;\n      std::cout << std::right << std::setw(10) << std::setfill(' ')\n                << (int)(pi.memory/1024);\n      std::cout << std::left << ' ' << platform_to_string(pi.platform);\n      std::cout << std::endl;\n   }\n}\n```\n\n以下程序定义了一个进程列表(您实际上可以使用特定于操作系统的 API 检索正在运行的进程列表)，并以请求的格式将其打印到控制台:\n\n```cpp\nint main()\n{\n   using namespace std::string_literals;\n\n   std::vector<procinfo> processes\n   {\n      {512, \"cmd.exe\"s, procstatus::running, \"SYSTEM\"s, \n            148293, platforms::p64bit },\n      {1044, \"chrome.exe\"s, procstatus::running, \"marius.bancila\"s, \n            25180454, platforms::p32bit},\n      {7108, \"explorer.exe\"s, procstatus::running, \"marius.bancila\"s,  \n            2952943, platforms::p64bit },\n      {10100, \"chrome.exe\"s, procstatus::running, \"marius.bancila\"s, \n            227756123, platforms::p32bit},\n      {22456, \"skype.exe\"s, procstatus::suspended, \"marius.bancila\"s, \n            16870123, platforms::p64bit }, \n   };\n\n   print_processes(processes);\n}\n```\n\n# 34.从文本文件中删除空行\n\n解决此任务的一种可能方法是执行以下操作:\n\n1.  创建一个临时文件，仅包含要从原始文件中保留的文本\n2.  从输入文件中逐行读取，并将所有非空行复制到临时文件中\n3.  完成处理后删除原始文件\n4.  将临时文件移动到原始文件的路径\n\n另一种方法是移动临时文件并覆盖原始文件。以下实现遵循列出的步骤。临时文件创建在`filesystem::temp_directory_path()`返回的临时目录中:\n\n```cpp\nnamespace fs = std::experimental::filesystem;\n\nvoid remove_empty_lines(fs::path filepath)\n{\n   std::ifstream filein(filepath.native(), std::ios::in);\n   if (!filein.is_open())\n      throw std::runtime_error(\"cannot open input file\");\n\n   auto temppath = fs::temp_directory_path() / \"temp.txt\";\n   std::ofstream fileout(temppath.native(), \n   std::ios::out | std::ios::trunc);\n   if (!fileout.is_open())\n      throw std::runtime_error(\"cannot create temporary file\");\n\n   std::string line;\n   while (std::getline(filein, line))\n   {\n      if (line.length() > 0 &&\n      line.find_first_not_of(' ') != line.npos)\n      {\n         fileout << line << '\\n';\n      }\n   }\n   filein.close();\n   fileout.close();\n\n   fs::remove(filepath);\n   fs::rename(temppath, filepath);\n}\n```\n\n# 35.计算目录的大小\n\n为了计算一个目录的大小，我们必须遍历所有的文件，并计算单个文件的大小。\n\n`filesystem::recursive_directory_iterator`是来自`filesystem`库的迭代器，允许以递归方式迭代目录的所有条目。它有各种各样的构造函数，其中一些构造函数采用类型为`filesystem::directory_options`的值，该值指示是否应该遵循符号链接。通用`std::accumulate()`算法可以用来合计文件大小。由于一个目录的总大小可能超过 2 GB，所以不应该使用`int`或`long`，而应该使用`unsigned long long`作为总和类型。以下函数显示了所需任务的可能实现:\n\n```cpp\nnamespace fs = std::experimental::filesystem;\n\nstd::uintmax_t get_directory_size(fs::path const & dir,\n                                  bool const follow_symlinks = false)\n{\n   auto iterator = fs::recursive_directory_iterator(\n      dir,\n      follow_symlinks ? fs::directory_options::follow_directory_symlink : \n                        fs::directory_options::none);\n\n   return std::accumulate(\n      fs::begin(iterator), fs::end(iterator),\n      0ull,\n      [](std::uintmax_t const total,\n         fs::directory_entry const & entry) {\n             return total + (fs::is_regular_file(entry) ?\n                    fs::file_size(entry.path()) : 0);\n   });\n}\n\nint main()\n{\n   std::string path;\n   std::cout << \"Path: \";\n   std::cin >> path;\n   std::cout << \"Size: \" << get_directory_size(path) << std::endl;\n}\n```\n\n# 36.删除超过给定日期的文件\n\n要执行文件系统操作，您应该使用`filesystem`库。对于使用时间和持续时间的工作，您应该使用`chrono`库。实现所请求功能的函数必须执行以下操作:\n\n1.  检查目标路径指示的条目是否存在，并且是否早于给定的持续时间，如果是，则将其删除\n2.  如果它不是旧的，并且是一个目录，则遍历它的所有条目并递归调用该函数:\n\n```cpp\nnamespace fs = std::experimental::filesystem;\nnamespace ch = std::chrono;\n\ntemplate <typename Duration>\nbool is_older_than(fs::path const & path, Duration const duration)\n{\n   auto ftimeduration = fs::last_write_time(path).time_since_epoch();\n   auto nowduration = (ch::system_clock::now() - duration)\n                      .time_since_epoch();\n   return ch::duration_cast<Duration>(nowduration - ftimeduration)\n                      .count() > 0;\n}\n\ntemplate <typename Duration>\nvoid remove_files_older_than(fs::path const & path, \n                             Duration const duration)\n{\n   try\n   {\n      if (fs::exists(path))\n      {\n         if (is_older_than(path, duration))\n         {\n            fs::remove(path);\n         }\n         else if(fs::is_directory(path))\n         {\n            for (auto const & entry : fs::directory_iterator(path))\n            {\n               remove_files_older_than(entry.path(), duration);\n            }\n         }\n      }\n   }\n   catch (std::exception const & ex)\n   {\n      std::cerr << ex.what() << std::endl;\n   }\n}\n```\n\n使用`directory_iterator`并递归调用`remove_files_older_than()`的另一种方法是使用`recursive_directory_iterator`并简单地删除超过给定持续时间的条目。但是，这种方法会采用未定义的行为，因为如果在创建递归目录迭代器后，文件或目录被删除或添加到目录树中，则不会指定是否会通过迭代器观察到更改。因此，应该避免这种方法。\n\n`is_older_than()`功能模板确定当前时刻和最后一次文件写入操作从系统时钟周期开始经过的时间，并检查两者的差值是否大于指定的持续时间。\n\n`remove_files_older_than()`功能可以如下使用:\n\n```cpp\nint main()\n{\n   using namespace std::chrono_literals;\n\n#ifdef _WIN32\n   auto path = R\"(..\\Test\\)\";\n#else\n   auto path = R\"(../Test/)\";\n#endif\n\n   remove_files_older_than(path, 1h + 20min);\n}\n```\n\n# 37.在目录中查找与正则表达式匹配的文件\n\n实现指定的功能应该很简单:递归地遍历指定目录中的所有条目，并保留所有名称与正则表达式匹配的常规文件条目。为此，您应该使用以下内容:\n\n*   `filesystem::recursive_directory_iterator`遍历目录条目\n*   `regex`和`regex_match()`检查文件名是否与正则表达式匹配\n*   `copy_if()`和`back_inserter`在`vector`末尾复制符合特定标准的目录条目。\n\n这样的函数可能如下所示:\n\n```cpp\nnamespace fs = std::experimental::filesystem;\n\nstd::vector<fs::directory_entry> find_files(\n   fs::path const & path,\n   std::string_view regex)\n{\n   std::vector<fs::directory_entry> result;\n   std::regex rx(regex.data());\n\n   std::copy_if(\n      fs::recursive_directory_iterator(path),\n      fs::recursive_directory_iterator(),\n      std::back_inserter(result),\n      [&rx](fs::directory_entry const & entry) {\n         return fs::is_regular_file(entry.path()) &&\n                std::regex_match(entry.path().filename().string(), rx);\n   });\n\n   return result;\n}\n```\n\n有了这些，我们可以编写以下代码:\n\n```cpp\nint main()\n{\n   auto dir = fs::temp_directory_path();\n   auto pattern = R\"(wct[0-9a-zA-Z]{3}\\.tmp)\";\n   auto result = find_files(dir, pattern);\n\n   for (auto const & entry : result)\n   {\n      std::cout << entry.path().string() << std::endl;\n   }\n}\n```\n\n# 38.临时日志文件\n\n您必须为此任务实现的日志类应该:\n\n*   有一个构造函数，它在临时目录中创建一个文本文件，并打开它进行写入\n*   在销毁过程中，如果文件仍然存在，请将其关闭并删除\n*   有一个关闭文件并将其移动到永久路径的方法\n*   将文本消息写入输出文件的时间超过了`operator<<`\n\n为了给文件创建唯一的名称，可以使用 UUID(也称为 GUID)。C++ 标准不支持任何与此相关的功能，但是有第三方库，比如`boost::uuid`、 *CrossGuid* 或者`stduuid`，这其实是我创建的一个库。对于这个实现，我将使用最后一个。你可以在[https://github.com/mariusbancila/stduuid](https://github.com/mariusbancila/stduuid)找到它:\n\n```cpp\nnamespace fs = std::experimental::filesystem;\n\nclass logger\n{\n   fs::path logpath;\n   std::ofstream logfile;\npublic:\n   logger()\n   {\n      auto name = uuids::to_string(uuids::uuid_random_generator{}());\n      logpath = fs::temp_directory_path() / (name + \".tmp\");\n      logfile.open(logpath.c_str(), std::ios::out|std::ios::trunc);\n   }\n\n   ~logger() noexcept\n   {\n      try {\n         if(logfile.is_open()) logfile.close();\n         if (!logpath.empty()) fs::remove(logpath);\n      }\n      catch (...) {}\n   }\n\n   void persist(fs::path const & path)\n   {\n      logfile.close();\n      fs::rename(logpath, path);\n      logpath.clear();\n   }\n\n   logger& operator<<(std::string_view message)\n   {\n      logfile << message.data() << '\\n';\n      return *this;\n   }\n};\n```\n\n使用此类的示例如下:\n\n```cpp\nint main()\n{\n   logger log;\n   try \n   {\n      log << \"this is a line\" << \"and this is another one\";\n      throw std::runtime_error(\"error\");\n   }\n   catch (...) \n   {\n      log.persist(R\"(lastlog.txt)\");\n   }\n}\n```**"
  },
  {
    "path": "docs/mod-cpp-challenge/05.md",
    "content": "# 五、日期和时间\n\n# 问题\n\n# 39.测量功能执行时间\n\n编写一个函数，该函数可以测量函数在任何所需持续时间(如秒、毫秒、微秒等)内的执行时间(具有任意数量的参数)。\n\n# 40.两个日期之间的天数\n\n编写一个函数，在给定两个日期的情况下，返回两个日期之间的天数。无论输入日期的顺序如何，该函数都应该工作。\n\n# 41.一周中的某一天\n\n编写一个函数，在给定日期的情况下，确定一周中的哪一天。这个函数应该返回一个介于 1(星期一)和 7(星期日)之间的值。\n\n# 42.一年中的日期和星期\n\n编写一个函数，在给定日期的情况下，返回一年中的某一天(闰年从 1 到 365 或 366)，并编写另一个函数，对于相同的输入，返回一年中的日历周。\n\n# 43.多个时区的会议时间\n\n编写一个函数，给出会议参与者及其时区的列表，显示每个参与者的本地会议时间。\n\n# 44.月历\n\n编写一个函数，在给定年和月的情况下，将月历打印到控制台。预期输出格式如下(示例为 2017 年 12 月):\n\n```cpp\nMon Tue Wed Thu Fri Sat Sun\n                  1   2   3\n  4   5   6   7   8   9  10\n 11  12  13  14  15  16  17\n 18  19  20  21  22  23  24\n 25  26  27  28  29  30  31\n```\n\n# 解决方法\n\n# 39.测量功能执行时间\n\n要测量函数的执行时间，您应该在函数执行之前检索当前时间，执行函数，然后再次检索当前时间，并确定两个时间点之间经过了多少时间。为了方便起见，这些都可以放在`variadic`函数模板中，该模板将要执行的函数及其参数作为参数，并且:\n\n*   默认使用`std::high_resolution_clock`确定当前时间。\n*   使用`std::invoke()`执行要测量的函数，带有指定的参数。\n*   返回持续时间，而不是特定持续时间的刻度数。这很重要，这样你就不会失去决心。它使您能够添加各种分辨率的执行持续时间，如秒和毫秒，这是不可能通过返回滴答计数来实现的:\n\n```cpp\ntemplate <typename Time = std::chrono::microseconds,\n          typename Clock = std::chrono::high_resolution_clock>\nstruct perf_timer\n{\n   template <typename F, typename... Args>\n   static Time duration(F&& f, Args... args)\n   {\n      auto start = Clock::now();\n      std::invoke(std::forward<F>(f), std::forward<Args>(args)...);\n      auto end = Clock::now();\n\n      return std::chrono::duration_cast<Time>(end - start);\n   }\n};\n```\n\n该功能模板可以如下使用:\n\n```cpp\nvoid f() \n{ \n   // simulate work\n   std::this_thread::sleep_for(2s); \n}\n\n```\n\n```cpp\nvoid g(int const a, int const b) \n{ \n   // simulate work\n   std::this_thread::sleep_for(1s); \n}\n\nint main()\n{\n   auto t1 = perf_timer<std::chrono::microseconds>::duration(f);\n   auto t2 = perf_timer<std::chrono::milliseconds>::duration(g, 1, 2);\n\n   auto total = std::chrono::duration<double, std::nano>(t1 + t2).count();\n}\n```\n\n# 40.两个日期之间的天数\n\n从 C++ 17 开始，`chrono`标准库不支持使用日期、星期、日历、时区和其他有用的相关功能。这将在 C++ 20 中发生变化，因为时区和日历支持已在 2018 年 3 月的杰克逊维尔会议上被添加到标准中。新增加的内容基于一个名为`date`的开源库，该库建立在`chrono`之上，由霍华德·欣南特开发，可在 https://github.com/HowardHinnant/date 的[网站上获得。我们将使用这个库来解决本章中的几个问题。虽然在这个实现中，名称空间是`date`，但是在 C++ 20 中，它将是`std::chrono`的一部分。但是，您应该能够简单地替换名称空间，而无需任何进一步的代码更改。](https://github.com/HowardHinnant/date)\n\n要解决这个任务，您可以使用`date.h `标题中的`date::sys_days`类。它代表了自`std::system_clock`时代以来的天数。这是一个有着一天决心的`time_point`，并且可以隐式转换为`std::system_clock::time_point`。基本上，你必须构造两个这种类型的对象并减去它们。结果正好是两个日期之间的天数。下面是这样一个函数的简单实现:\n\n```cpp\ninline int number_of_days(\n   int const y1, unsigned int const m1, unsigned int const d1,\n   int const y2, unsigned int const m2, unsigned int const d2)\n{\n   using namespace date;\n\n   return (sys_days{ year{ y1 } / month{ m1 } / day{ d1 } } -\n           sys_days{ year{ y2 } / month{ m2 } / day{ d2 } }).count();\n}\n\ninline int number_of_days(date::sys_days const & first,\n                          date::sys_days const & last)\n{\n   return (last - first).count();\n}\n```\n\n下面是如何使用这些重载函数的几个例子:\n\n```cpp\nint main()\n{\n   auto diff1 = number_of_days(2016, 9, 23, 2017, 5, 15);\n\n   using namespace date::literals;\n   auto diff2 = number_of_days(2016_y/sep/23, 15_d/may/2017);\n}\n```\n\n# 41.一周中的某一天\n\n如果使用`date`库，解决这个问题也相对简单。但是，这一次，您必须使用以下类型:\n\n*   `date::year_month_day`，表示一天的结构，包含年、月(1 到 12)和日(1 到 31)字段。\n*   `date::iso_week::year_weeknum_weekday`来自`iso_week.h `表头，是一个结构，有年字段、年周数字段和周天数字段(1 到 7)。这个类可以隐式转换到`date::sys_days`和从`date::sys_days`转换，这使得它可以显式转换到任何其他可以隐式转换到`date::sys_days`和从`date::year_month_day`转换的日历系统。\n\n说了这么多，问题解决了:创建一个`year_month_day`对象来表示期望的日期，然后从中创建一个`year_weeknum_weekday`对象，并使用`weekday()`检索一周中的某一天:\n\n```cpp\nunsigned int week_day(int const y, unsigned int const m, \n                      unsigned int const d)\n{\n   using namespace date;\n\n   if(m < 1 || m > 12 || d < 1 || d > 31) return 0;\n\n   auto const dt = date::year_month_day{year{ y }, month{ m }, day{ d }};\n   auto const tiso = iso_week::year_weeknum_weekday{ dt };\n\n   return (unsigned int)tiso.weekday();\n}\n\nint main()\n{\n   auto wday = week_day(2018, 5, 9);\n}\n```\n\n# 42.一年中的日期和星期\n\n这个由两部分组成的问题的解决方案应该与前面两个简单明了:\n\n*   要计算一年中的某一天，您要减去两个`date::sys_days`对象，一个表示给定的一天，另一个表示同一年的 1 月 0 日。或者，您可以从 1 月 1 日开始，并在结果中添加 1。\n*   要确定一年中的周数，构造一个`year_weeknum_weekday`对象，就像前面的问题一样，并检索`weeknum()`值:\n\n```cpp\nint day_of_year(int const y, unsigned int const m, \n                unsigned int const d)\n{\n   using namespace date;\n\n   if(m < 1 || m > 12 || d < 1 || d > 31) return 0;\n\n   return (sys_days{ year{ y } / month{ m } / day{ d } } -\n           sys_days{ year{ y } / jan / 0 }).count();\n}\n\nunsigned int calendar_week(int const y, unsigned int const m, \n                           unsigned int const d)\n{\n   using namespace date;\n\n   if(m < 1 || m > 12 || d < 1 || d > 31) return 0;\n\n   auto const dt = date::year_month_day{year{ y }, month{ m }, day{ d }};\n   auto const tiso = iso_week::year_weeknum_weekday{ dt };\n\n   return (unsigned int)tiso.weeknum();\n}\n```\n\n这些功能可以如下使用:\n\n```cpp\nint main()\n{\n   int y = 0;\n   unsigned int m = 0, d = 0;\n   std::cout << \"Year:\"; std::cin >> y;\n   std::cout << \"Month:\"; std::cin >> m;\n   std::cout << \"Day:\"; std::cin >> d;\n\n   std::cout << \"Calendar week:\" << calendar_week(y, m, d) << std::endl;\n   std::cout << \"Day of year:\" << day_of_year(y, m, d) << std::endl;\n}\n```\n\n# 43.多个时区的会议时间\n\n要使用时区，您必须使用`date`库的`tz.h`标题。但是，这需要在您的机器上下载并解压缩 *IANA 时区数据库*。\n\n以下是如何为日期库准备时区数据库:\n\n*   从[https://www.iana.org/time-zones](https://www.iana.org/time-zones)下载最新版本的数据库。目前最新的版本叫做`tzdata2017c.tar.gz`。\n*   将它解压缩到你机器上的任何位置，在一个名为`tzdata`的子目录中。假设父目录是`c:\\work\\challenges\\libs\\date`(在 Windows 机器上)；这将有一个名为`tzdata`的子目录。\n*   对于 Windows，您需要下载一个名为`windowsZones.xml`的文件，其中包含 Windows 时区到 IANA 时区的映射。这可以在[https://unicode . org/repos/cldr/trunk/common/supplicate/windowszones . XML](https://unicode.org/repos/cldr/trunk/common/supplemental/windowsZones.xml)上找到。该文件必须存储在先前创建的同一`tzdata`子目录中。\n*   在您的项目设置中，定义一个名为`INSTALL`的预处理器宏，该宏指示`tzdata`子目录的父目录。这里举的例子，你应该有`INSTALL=c:\\\\work\\\\challenges\\\\libs\\\\date`。(请注意，双反斜杠是必要的，因为宏用于使用字符串化和串联创建文件路径，否则会导致路径不正确。)\n\n为了解决这个问题，我们将考虑具有最少信息的用户结构，例如姓名和时区。时区是使用`date::locate_zone()`功能创建的:\n\n```cpp\nstruct user\n{\n   std::string Name;\n   date::time_zone const * Zone;\n\n   explicit user(std::string_view name, std::string_view zone)\n      : Name{name.data()}, Zone(date::locate_zone(zone.data()))\n   {}\n};\n```\n\n显示用户列表及其会议开始的本地时间的函数应该将给定时间从参考区域转换为他们自己区域的时间。为此，我们可以使用`date::zoned_time`类的转换构造函数:\n\n```cpp\ntemplate <class Duration, class TimeZonePtr>\nvoid print_meeting_times(\n   date::zoned_time<Duration, TimeZonePtr> const & time,\n   std::vector<user> const & users)\n{\n   std::cout \n      << std::left << std::setw(15) << std::setfill(' ')\n      << \"Local time: \" \n      << time << std::endl;\n\n   for (auto const & user : users)\n   {\n      std::cout\n         << std::left << std::setw(15) << std::setfill(' ')\n         << user.Name\n         << date::zoned_time<Duration, TimeZonePtr>(user.Zone, time) \n         << std::endl;\n   }\n}\n```\n\n该功能可以如下使用，其中给定时间(小时和分钟)以当前时区表示:\n\n```cpp\nint main()\n{\n   std::vector<user> users{\n      user{ \"Ildiko\", \"Europe/Budapest\" },\n      user{ \"Jens\", \"Europe/Berlin\" },\n      user{ \"Jane\", \"America/New_York\" }\n   };\n\n   unsigned int h, m;\n   std::cout << \"Hour:\"; std::cin >> h;\n   std::cout << \"Minutes:\"; std::cin >> m;\n\n   date::year_month_day today = \n      date::floor<date::days>(ch::system_clock::now());\n\n   auto localtime = date::zoned_time<std::chrono::minutes>(\n      date::current_zone(), \n      static_cast<date::local_days>(today)+ch::hours{h}+ch::minutes{m});\n\n   print_meeting_times(localtime, users);\n}\n```\n\n# 44.月历\n\n解决这个任务实际上是部分基于前面的任务。为了打印问题中指出的月份，您应该知道:\n\n*   一个月的第一天是星期几。这可以使用为前一个问题创建的`week_day()`函数来确定。\n*   一个月的天数。这可以通过使用`date::year_month_day_last`结构并检索`day()`的值来确定。\n\n首先确定这些信息后，您应该:\n\n*   打印第一个工作日前第一周的空值\n*   使用从 1 到每月最后一天的正确格式打印日期\n*   每隔七天换一条新的线(从第一周的第一天开始计算，尽管这可能属于前一个月)\n\n这里显示了所有这些的实现:\n\n```cpp\nunsigned int week_day(int const y, unsigned int const m, \n                      unsigned int const d)\n{\n   using namespace date;\n\n   if(m < 1 || m > 12 || d < 1 || d > 31) return 0;\n\n   auto const dt = date::year_month_day{year{ y }, month{ m }, day{ d }};\n   auto const tiso = iso_week::year_weeknum_weekday{ dt };\n\n   return (unsigned int)tiso.weekday();\n}\n\nvoid print_month_calendar(int const y, unsigned int m)\n{\n   using namespace date;\n   std::cout << \"Mon Tue Wed Thu Fri Sat Sun\" << std::endl;\n\n   auto first_day_weekday = week_day(y, m, 1);\n   auto last_day = (unsigned int)year_month_day_last(\n      year{ y }, month_day_last{ month{ m } }).day();\n\n   unsigned int index = 1;\n   for (unsigned int day = 1; day < first_day_weekday; ++ day, ++ index)\n   {\n      std::cout << \" \";\n   }\n\n   for (unsigned int day = 1; day <= last_day; ++ day)\n   {\n      std::cout << std::right << std::setfill(' ') << std::setw(3)\n                << day << ' ';\n      if (index++ % 7 == 0) std::cout << std::endl;\n   }\n\n   std::cout << std::endl;\n}\n\nint main()\n{\n   print_month_calendar(2017, 12);\n}\n```"
  },
  {
    "path": "docs/mod-cpp-challenge/06.md",
    "content": "# 六、算法和数据结构\n\n# 问题\n\n# 45.优先队列\n\n编写一个表示优先级队列的数据结构，该队列为最大的元素提供恒定的时间查找，但对于添加和移除元素具有对数时间复杂度。队列在末尾插入新元素，并从顶部移除元素。默认情况下，队列应该使用`operator<`来比较元素，但是如果第一个参数小于第二个参数，用户应该可以提供一个返回`true`的比较函数对象。实现必须至少提供以下操作:\n\n*   `push()`添加新元素\n*   `pop()`移除顶部元素\n*   `top()`提供对顶部元素的访问\n*   `size()`表示队列中元素的数量\n*   `empty()`表示队列是否为空\n\n# 46.循环缓冲器\n\n创建表示固定大小的循环缓冲区的数据结构。当循环缓冲区的填充超出其固定大小时，循环缓冲区会覆盖现有元素。您必须编写的类应该:\n\n*   禁止默认构造\n*   支持创建指定大小的对象\n*   允许检查缓冲容量和状态(`empty()`、`full()`、`size()`、`capacity()`)\n*   添加新元素，这一操作可能会覆盖缓冲区中最旧的元素\n*   从缓冲区中移除最旧的元素\n*   通过其元素支持迭代\n\n# 47.双缓冲器\n\n编写一个类，该类表示一个缓冲区，可以在两个操作不冲突的情况下同时写入和读取该缓冲区。读操作必须在写操作进行时提供对旧数据的访问。写操作完成后，新写入的数据必须可供读取。\n\n# 48.范围内最常见的元素\n\n编写一个函数，在给定的范围内，返回最频繁的元素及其在该范围内出现的次数。如果多个元素出现相同的最大次数，那么函数应该返回所有元素。比如范围`{1,1,3,5,8,13,3,5,8,8,5}`，应该返回`{5, 3}`和`{8, 3}`。\n\n# 49.文本直方图\n\n写一个程序，给定一个文本，用字母表中每个字母的频率确定并打印一个直方图。频率是每个字母出现的次数占字母总数的百分比。程序应该只计算字母的外观，而忽略数字、符号和其他可能的字符。频率必须根据字母数量而不是文本大小来确定。\n\n# 50.过滤电话号码列表\n\n编写一个函数，给定一个电话号码列表，只返回来自指定国家的号码。该国家由其电话国家代码表示，例如英国为 44。电话号码可能以国家代码开头，一个`+`后跟国家代码，或者没有国家代码。最后一类的必须忽略。\n\n# 51.转换电话号码列表\n\n编写一个函数，给定一个电话号码列表，转换它们，使它们都以一个指定的电话国家代码开始，前面有`+`符号。电话号码中的任何空格也应该删除。以下是输入和输出示例列表:\n\n```cpp\n07555 123456    => +447555123456\n07555123456     => +447555123456\n+44 7555 123456 => +447555123456\n44 7555 123456  => +447555123456\n7555 123456     => +447555123456\n```\n\n# 52.生成字符串的所有排列\n\n编写一个函数，在控制台上打印给定字符串的所有可能排列。您应该提供这个函数的两个版本:一个使用递归，一个不使用。\n\n# 53.电影的平均评分\n\n编写一个程序，计算并打印电影列表的平均评分。每部电影都有一个从 1 到 10 的分级列表(其中 1 是最低分级，10 是最高分级)。为了计算评分，您必须先移除最高和最低评分的 5%，然后再计算它们的平均值。结果必须以单个小数点显示。\n\n# 54.成对算法\n\n编写一个通用函数，在给定一个范围的情况下，从输入范围中返回一个包含成对连续元素的新范围。如果输入范围有奇数个元素，最后一个必须忽略。例如，如果输入范围是`{1, 1, 3, 5, 8, 13, 21}`，结果必须是`{ {1, 1}, {3, 5}, {8, 13}}`。\n\n# 55.Zip 算法\n\n编写一个函数，在给定两个范围的情况下，用这两个范围中的元素对返回一个新的范围。如果两个范围大小不同，结果必须包含与最小输入范围一样多的元素。例如，如果输入范围是`{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }`和`{ 1, 1, 3, 5, 8, 13, 21 }`，结果应该是`{{1,1}, {2,1}, {3,3}, {4,5}, {5,8}, {6,13}, {7,21}}`。\n\n# 56.选择算法\n\n编写一个函数，在给定一个值范围和一个投影函数的情况下，将每个值转换成一个新值，并返回一个包含所选值的新范围。例如，如果您有一个具有`id`、`title`和`author`的活字本，并且有一系列这样的书籍值，那么该功能应该可以只选择书籍的标题。以下是如何使用该函数的示例:\n\n```cpp\nstruct book\n{\n   int         id;\n   std::string title;\n   std::string author;\n};\n\nstd::vector<book> books{\n   {101, \"The C++ Programming Language\", \"Bjarne Stroustrup\"},\n   {203, \"Effective Modern C++\", \"Scott Meyers\"},\n   {404, \"The Modern C++ Programming Cookbook\", \"Marius Bancila\"}};\n\nauto titles = select(books, [](book const & b) {return b.title; });\n```\n\n# 57.分类算法\n\n编写一个函数，在给定一对随机访问迭代器来定义其上下界的情况下，使用 quicksort 算法对范围内的元素进行排序。排序函数应该有两个重载:一个使用`operator<`比较范围的元素并按升序排列，另一个使用用户定义的二进制比较函数比较元素。\n\n# 58.节点之间的最短路径\n\n编写一个程序，给定一个节点网络和它们之间的距离，计算并显示从一个指定节点到所有其他节点的最短距离，以及开始和结束节点之间的路径。作为输入，考虑以下无向图:\n\n![](img/5162fe54-036c-4624-bdbe-6ed6cfe211be.png)\n\n该图的程序输出应该如下:\n\n```cpp\nA -> A : 0     A\nA -> B : 7     A -> B\nA -> C : 9     A -> C\nA -> D : 20    A -> C -> D\nA -> E : 20    A -> C -> F -> E\nA -> F : 11    A -> C -> F\n```\n\n# 59.黄鼠狼计划\n\n编写一个程序，实现理查德·道金斯的黄鼠狼计算机模拟，用道金斯的话描述如下(*盲人制表师*，第 3 章):\n\nWe again use our computer monkey, but with a crucial difference in its program. It again begins by choosing a random sequence of 28 letters, just as before ... it duplicates it repeatedly, but with a certain chance of random error – 'mutation' – in the copying. The computer examines the mutant nonsense phrases, the 'progeny' of the original phrase, and chooses the one which, however slightly, most resembles the target phrase, METHINKS IT IS LIKE A WEASEL.\n\n# 60.生活的游戏\n\n写一个程序实现*约翰·何顿·康威*提出的*生命游戏*元胞自动机。这个游戏的世界是一个正方形格子，可以有两种状态:死亡或活着。每个单元都与其相邻的单元进行交互，每一步都会发生以下事务:\n\n*   任何少于两个活邻居的活细胞都会死亡，好像是人口不足造成的\n*   任何有两个或三个活邻居的活细胞都会延续到下一代\n*   任何有三个以上活邻居的活细胞都会死亡，就像人口过剩一样\n*   任何正好有三个活邻居的死细胞都会变成活细胞，就像通过繁殖一样\n\n每次迭代的游戏状态都应该显示在控制台上，为了方便，你应该选择一个合理的大小，比如 20 行 x 50 列。\n\n# 解决方法\n\n# 45.优先队列\n\n优先级队列是一种抽象数据类型，其元素有一个优先级。优先级队列不是作为先进先出容器工作，而是按照元素的优先级顺序使元素可用。这种数据结构被用在诸如 Dijkstra 最短路径、Prim 算法、堆排序、A*搜索算法、用于数据压缩的霍夫曼码等算法中。\n\n实现优先级队列的一个非常简单的方法是使用`std::vector`作为元素的底层容器，并始终保持其排序。这意味着最大和最小元素总是在两端。然而，这种方法不能提供最有效的操作。\n\n最适合用来实现优先级队列的数据结构是堆。这是一个基于树的数据结构，满足以下属性:如果 *P* 是 *C* 的父节点，那么 *P* 的键(值)大于或等于(在最大堆中)或小于或等于(在最小堆中)C*的键。*\n\n *标准库提供了几种处理堆的操作:\n\n*   `std::make_heap()`:这将为给定的范围创建一个最大堆，使用`operator<`或用户提供的比较函数对元素进行排序\n*   `std::push_heap()`:这将在最大堆的末尾插入一个新元素\n*   `std::pop_heap()`:这将移除堆的第一个元素(通过交换第一个和最后一个位置的值，并使子范围`[first, last-1)`成为最大堆)\n\n使用`std::vector`保存数据和堆的标准函数的优先级队列实现可以如下所示:\n\n```cpp\ntemplate <class T,\n   class Compare = std::less<typename std::vector<T>::value_type>>\nclass priority_queue\n{\n   typedef typename std::vector<T>::value_type value_type;\n   typedef typename std::vector<T>::size_type size_type;\n   typedef typename std::vector<T>::reference reference;\n   typedef typename std::vector<T>::const_reference const_reference;\npublic:\n   bool empty() const noexcept { return data.empty(); }\n   size_type size() const noexcept { return data.size(); }\n\n   void push(value_type const & value)\n   {\n      data.push_back(value);\n      std::push_heap(std::begin(data), std::end(data), comparer);\n   }\n\n   void pop()\n   {\n      std::pop_heap(std::begin(data), std::end(data), comparer);\n      data.pop_back();\n   }\n\n   const_reference top() const { return data.front(); }\n\n   void swap(priority_queue& other) noexcept\n   {\n      swap(data, other.data);\n      swap(comparer, other.comparer);\n   }\nprivate:\n   std::vector<T> data;\n   Compare comparer;\n};\n\ntemplate<class T, class Compare>\nvoid swap(priority_queue<T, Compare>& lhs,\n          priority_queue<T, Compare>& rhs) \nnoexcept(noexcept(lhs.swap(rhs)))\n{\n   lhs.swap(rhs);\n}\n```\n\n这个类可以如下使用:\n\n```cpp\nint main()\n{\n   priority_queue<int> q;\n   for (int i : {1, 5, 3, 1, 13, 21, 8})\n   {\n      q.push(i);\n   }\n\n   assert(!q.empty());\n   assert(q.size() == 7);\n\n   while (!q.empty())\n   {\n      std::cout << q.top() << ' ';\n      q.pop();\n   }\n}\n```\n\n# 46.循环缓冲器\n\n循环缓冲区是一个固定大小的容器，其行为就像它的两端被连接起来形成一个虚拟的循环内存布局。它的主要好处是，您不需要大量内存来保留数据，因为旧条目会被新条目覆盖。循环缓冲区用于输入/输出缓冲、有界日志记录(当您只想保留最后的消息时)、异步处理缓冲区等。\n\n我们可以区分两种情况:\n\n1.  添加到缓冲区的元素数量尚未达到其容量(用户定义的固定大小)。在这种情况下，它的行为就像一个常规容器，比如一个向量。\n2.  添加到缓冲区的元素数量已达到并超过其容量。在这种情况下，缓冲区的内存被重用，旧的元素被覆盖。\n\n我们可以使用以下方式来表示这样的结构:\n\n*   具有预先分配的元素数量的常规容器\n*   一个头指针，用于指示最后插入的元素的位置\n*   指示容器中元素数量的大小计数器，不能超过其容量(因为在这种情况下元素将被覆盖)\n\n循环缓冲区的两个主要操作是:\n\n*   向缓冲区添加新元素。我们总是在头指针(或索引)的下一个位置插入。这就是下图所示的`push()`方法。\n*   从缓冲区中移除现有元素。我们总是移除最老的元素。该元素位于位置`head - size`(这必须说明索引的循环性质)。这就是下图所示的`pop()`方法。\n\n这种数据结构的实现如下所示:\n\n```cpp\ntemplate <class T>\nclass circular_buffer\n{\n   typedef circular_buffer_iterator<T> const_iterator;\n\n   circular_buffer() = delete;\npublic:\n   explicit circular_buffer(size_t const size) :data_(size)\n   {}\n\n   bool clear() noexcept { head_ = -1; size_ = 0; }\n   bool empty() const noexcept { return size_ == 0; }\n   bool full() const noexcept { return size_ == data_.size(); }\n   size_t capacity() const noexcept { return data_.size(); }\n   size_t size() const noexcept { return size_; }\n\n   void push(T const item)\n   {\n      head_ = next_pos();\n      data_[head_] = item;\n      if (size_ < data_.size()) size_++ ;\n   }\n\n   T pop()\n   {\n      if (empty()) throw std::runtime_error(\"empty buffer\");\n      auto pos = first_pos();\n      size_--;\n      return data_[pos];\n   }\n\n   const_iterator begin() const\n   {\n      return const_iterator(*this, first_pos(), empty());\n   }\n\n   const_iterator end() const\n   {\n      return const_iterator(*this, next_pos(), true);\n   }\n\nprivate:\n   std::vector<T> data_;\n   size_t head_ = -1;\n   size_t size_ = 0;\n\n   size_t next_pos() const noexcept \n   { return size_ == 0 ? 0 : (head_ + 1) % data_.size(); }\n   size_t first_pos() const noexcept \n   { return size_ == 0 ? 0 : (head_ + data_.size() - size_ + 1) % \n                             data_.size(); }\n\n   friend class circular_buffer_iterator<T>;\n};\n```\n\n由于映射在连续内存布局上的索引的循环特性，这个类的迭代器类型不能是指针类型。迭代器必须能够通过对索引应用模运算来指向元素。这是一个迭代器的可能实现:\n\n```cpp\ntemplate <class T>\nclass circular_buffer_iterator\n{\n   typedef circular_buffer_iterator        self_type;\n   typedef T                               value_type;\n   typedef T&                              reference;\n   typedef T const&                        const_reference;\n   typedef T*                              pointer;\n   typedef std::random_access_iterator_tag iterator_category;\n   typedef ptrdiff_t                       difference_type;\npublic:\n   circular_buffer_iterator(circular_buffer<T> const & buf, \n                            size_t const pos, bool const last) :\n   buffer_(buf), index_(pos), last_(last)\n   {}\n\n   self_type & operator++ ()\n   {\n      if (last_)\n         throw std::out_of_range(\"Iterator cannot be incremented past the end of range.\");\n      index_ = (index_ + 1) % buffer_.data_.size();\n      last_ = index_ == buffer_.next_pos();\n      return *this;\n   }\n\n   self_type operator++ (int)\n   {\n      self_type tmp = *this;\n      ++*this;\n      return tmp;\n   }\n\n   bool operator== (self_type const & other) const\n   {\n      assert(compatible(other));\n      return index_ == other.index_ && last_ == other.last_;\n   }\n\n   bool operator!= (self_type const & other) const\n   {\n      return !(*this == other);\n   }\n\n   const_reference operator* () const\n   {\n      return buffer_.data_[index_];\n   }\n\n   const_reference operator-> () const\n   {\n      return buffer_.data_[index_];\n   }\nprivate:\n   bool compatible(self_type const & other) const\n   {\n      return &buffer_ == &other.buffer_;\n   }\n\n   circular_buffer<T> const & buffer_;\n   size_t index_;\n   bool last_;\n};\n```\n\n实现所有这些之后，我们可以编写如下代码。请注意，在注释中，第一个范围显示了内部向量的实际内容，第二个范围显示了迭代器访问公开的逻辑内容:\n\n```cpp\nint main()\n{\n   circular_buffer<int> cbuf(5); // {0, 0, 0, 0, 0} -> {}\n\n   cbuf.push(1);                 // {1, 0, 0, 0, 0} -> {1}\n   cbuf.push(2);                 // {1, 2, 0, 0, 0} -> {1, 2}\n   cbuf.push(3);                 // {1, 2, 3, 0, 0} -> {1, 2, 3}\n\n   auto item = cbuf.pop();       // {1, 2, 3, 0, 0} -> {2, 3}\n   cbuf.push(4);                 // {1, 2, 3, 4, 0} -> {2, 3, 4}\n   cbuf.push(5);                 // {1, 2, 3, 4, 5} -> {2, 3, 4, 5}\n   cbuf.push(6);                 // {6, 2, 3, 4, 5} -> {2, 3, 4, 5, 6}\n\n   cbuf.push(7);                 // {6, 7, 3, 4, 5} -> {3, 4, 5, 6, 7}\n   cbuf.push(8);                 // {6, 7, 8, 4, 5} -> {4, 5, 6, 7, 8}\n\n   item = cbuf.pop();            // {6, 7, 8, 4, 5} -> {5, 6, 7, 8}\n   item = cbuf.pop();            // {6, 7, 8, 4, 5} -> {6, 7, 8}\n   item = cbuf.pop();            // {6, 7, 8, 4, 5} -> {7, 8}\n\n   item = cbuf.pop();            // {6, 7, 8, 4, 5} -> {8}\n   item = cbuf.pop();            // {6, 7, 8, 4, 5} -> {}\n\n   cbuf.push(9);                 // {6, 7, 8, 9, 5} -> {9}\n}\n```\n\n# 47.双缓冲器\n\n这里描述的问题是典型的双缓冲情况。双缓冲是多缓冲最常见的情况，这是一种允许读者看到数据的完整版本而不是由作者产生的部分更新版本的技术。这是一种避免闪烁的常用技术，尤其是在计算机图形学中。\n\n为了实现所请求的功能，我们应该写入的缓冲区类必须有两个内部缓冲区:一个包含正在写入的临时数据，另一个包含已完成(或已提交)的数据。写操作完成后，临时缓冲区的内容被写入主缓冲区。对于内部缓冲区，下面的实现使用`std::vector`。当写操作完成时，我们只交换两个缓冲区的内容，而不是将数据从一个缓冲区复制到另一个缓冲区，这是一个更快的操作。对完整数据的访问提供了`read()`功能，该功能将读取缓冲区的内容复制到指定的输出，或者提供了直接元素访问(过载的`operator[]`)。对读缓冲区的访问与`std::mutex`同步，以确保在一个线程向缓冲区写入数据时从另一个线程读取数据是安全的:\n\n```cpp\ntemplate <typename T>\nclass double_buffer\n{\n   typedef T           value_type;\n   typedef T&          reference;\n   typedef T const &   const_reference;\n   typedef T*          pointer;\npublic:\n   explicit double_buffer(size_t const size) :\n      rdbuf(size), wrbuf(size)\n   {}\n\n   size_t size() const noexcept { return rdbuf.size(); }\n\n   void write(T const * const ptr, size_t const size)\n   {\n      std::unique_lock<std::mutex> lock(mt);\n      auto length = std::min(size, wrbuf.size());\n      std::copy(ptr, ptr + length, std::begin(wrbuf));\n      wrbuf.swap(rdbuf);\n   }\n\n   template <class Output>\n   void read(Output it) const\n   {\n      std::unique_lock<std::mutex> lock(mt);\n      std::copy(std::cbegin(rdbuf), std::cend(rdbuf), it);\n   }\n\n   pointer data() const\n   {\n       std::unique_lock<std::mutex> lock(mt);\n       return rdbuf.data();\n   }\n\n   reference operator[](size_t const pos)\n   {\n      std::unique_lock<std::mutex> lock(mt);\n      return rdbuf[pos];\n   }\n\n   const_reference operator[](size_t const pos) const\n   {\n      std::unique_lock<std::mutex> lock(mt);\n      return rdbuf[pos];\n   }\n\n   void swap(double_buffer other)\n   {\n      std::swap(rdbuf, other.rdbuf);\n      std::swap(wrbuf, other.wrbuf);\n   }\n\nprivate:\n   std::vector<T>     rdbuf;\n   std::vector<T>     wrbuf;\n   mutable std::mutex mt;\n};\n```\n\n下面是这个双缓冲区类如何用于两个不同实体的写入和读取的示例:\n\n```cpp\ntemplate <typename T>\nvoid print_buffer(double_buffer<T> const & buf)\n{\n   buf.read(std::ostream_iterator<T>(std::cout, \" \"));\n   std::cout << std::endl;\n}\n\nint main()\n{\n   double_buffer<int> buf(10);\n\n   std::thread t([&buf]() {\n      for (int i = 1; i < 1000; i += 10)\n      {\n         int data[] = { i, i + 1, i + 2, i + 3, i + 4, \n                        i + 5, i + 6,i + 7,i + 8,i + 9 };\n         buf.write(data, 10);\n\n         using namespace std::chrono_literals;\n         std::this_thread::sleep_for(100ms);\n       }\n   });\n\n   auto start = std::chrono::system_clock::now();\n   do\n   {\n      print_buffer(buf);\n\n      using namespace std::chrono_literals;\n      std::this_thread::sleep_for(150ms);\n   } while (std::chrono::duration_cast<std::chrono::seconds>(\n            std::chrono::system_clock::now() - start).count() < 12);\n\n   t.join();\n}\n```\n\n# 48.范围内最常见的元素\n\n为了确定并返回一个范围内最频繁的元素，您应该执行以下操作:\n\n*   在`std::map`中计算每个元素的外观。关键是元素，值是它的出现次数。\n*   使用`std::max_element()`确定地图的最大元素。结果是一个地图元素，即一对包含该元素及其出现次数的元素。\n\n*   复制所有值(外观计数)等于最大元素值的地图元素，并将其作为最终结果返回。\n\n下面的列表显示了前面描述的步骤的实现:\n\n```cpp\ntemplate <typename T>\nstd::vector<std::pair<T, size_t>> find_most_frequent(\n   std::vector<T> const & range)\n{\n   std::map<T, size_t> counts;\n   for (auto const & e : range) counts[e]++ ;\n\n   auto maxelem = std::max_element(\n      std::cbegin(counts), std::cend(counts),\n      [](auto const & e1, auto const & e2) {\n         return e1.second < e2.second;\n   });\n\n   std::vector<std::pair<T, size_t>> result;\n\n   std::copy_if(\n      std::begin(counts), std::end(counts),\n      std::back_inserter(result),\n      [maxelem](auto const & kvp) {\n         return kvp.second == maxelem->second;\n   });\n\n   return result;\n}\n```\n\n`find_most_frequent()`功能可以如下使用:\n\n```cpp\nint main()\n{\n   auto range = std::vector<int>{1,1,3,5,8,13,3,5,8,8,5};\n   auto result = find_most_frequent(range);\n\n   for (auto const & e : result)\n   {\n      std::cout << e.first << \" : \" << e.second << std::endl;\n   }\n}\n```\n\n# 49.文本直方图\n\n直方图是数字数据分布的表示。广为人知的直方图是用于摄影和图像处理的颜色和图像直方图。如这里所述，文本直方图是给定文本中字母出现频率的表示。这个问题部分类似于前面的问题，只是范围元素现在是字符，我们必须确定它们的频率。要解决这个问题，您应该:\n\n*   用地图统计每个字母的外观。关键是字母，数值是它的出现次数。\n*   计数时，忽略所有不是字母的字符。大写和小写字符必须视为相同，因为它们代表相同的字母。\n*   使用`std::accumulate()`统计给定文本中所有字母的出现总数。\n*   使用`std::for_each()`或基于范围的`for`循环遍历地图的所有元素，并将外观计数转换为频率。\n\n以下是问题的可能实现:\n\n```cpp\nstd::map<char, double> analyze_text(std::string_view text)\n{\n   std::map<char, double> frequencies;\n   for (char ch = 'a'; ch <= 'z'; ch++)\n      frequencies[ch] = 0;\n\n   for (auto ch : text)\n   {\n      if (isalpha(ch))\n         frequencies[tolower(ch)]++ ;\n   }\n\n   auto total = std::accumulate(\n      std::cbegin(frequencies), std::cend(frequencies),\n      0ull,\n      [](auto sum, auto const & kvp) {\n         return sum + static_cast<unsigned long long>(kvp.second);\n   });\n\n   std::for_each(\n      std::begin(frequencies), std::end(frequencies),\n      [total](auto & kvp) {\n         kvp.second = (100.0 * kvp.second) / total;\n   });\n\n   return frequencies;\n}\n```\n\n以下程序在控制台上打印文本中字母的频率:\n\n```cpp\nint main()\n{\n   auto result = analyze_text(R\"(Lorem ipsum dolor sit amet, consectetur \n      adipiscing elit, sed do eiusmod tempor incididunt ut labore et \n      dolore magna aliqua.)\");\n\n   for (auto const & kvp : result)\n   {\n      std::cout << kvp.first << \" : \"\n                << std::fixed\n                << std::setw(5) << std::setfill(' ')\n                << std::setprecision(2) << kvp.second << std::endl;\n   }\n}\n```\n\n# 50.过滤电话号码列表\n\n这个问题的解决方案相对简单:你必须遍历所有的电话号码，并将以国家代码开头的电话号码复制到一个单独的容器(如`std::vector`)中。例如，如果指定的国家代码是 44，则必须同时检查 44 和+44。使用`std::copy_if()`功能可以以这种方式过滤输入范围。此处显示了此问题的解决方案:\n\n```cpp\nbool starts_with(std::string_view str, std::string_view prefix)\n{\n   return str.find(prefix) == 0;\n}\n\ntemplate <typename InputIt>\nstd::vector<std::string> filter_numbers(InputIt begin, InputIt end,\n                                        std::string const & countryCode)\n{\n   std::vector<std::string> result;\n   std::copy_if(\n      begin, end,\n      std::back_inserter(result),\n      [countryCode](auto const & number) {\n         return starts_with(number, countryCode) ||\n                starts_with(number, \"+\" + countryCode);\n   });\n   return result;\n}\n\nstd::vector<std::string> filter_numbers(\n   std::vector<std::string> const & numbers,\n   std::string const & countryCode)\n{\n   return filter_numbers(std::cbegin(numbers), std::cend(numbers), \n                         countryCode);\n}\n```\n\n这是该功能的使用方法:\n\n```cpp\nint main()\n{\n   std::vector<std::string> numbers{\n      \"+40744909080\",\n      \"44 7520 112233\",\n      \"+44 7555 123456\",\n      \"40 7200 123456\",\n      \"7555 123456\"\n   };\n\n   auto result = filter_numbers(numbers, \"44\");\n\n   for (auto const & number : result)\n   {\n      std::cout << number << std::endl;\n   }\n}\n```\n\n# 51.转换电话号码列表\n\n这个问题在某些方面与前一个有些相似。但是，我们不能选择以指定国家代码开头的电话号码，而是必须转换每个号码，使它们都以前面带有`+`的国家代码开头。有几种情况必须考虑:\n\n*   电话号码以 0 开头。表示没有国家代码的数字。要修改数字以包括国家代码，我们必须用实际的国家代码替换 0，前面加`+`。\n*   电话号码以国家代码开头。在这种情况下，我们只需在开头加上`+`符号。\n*   电话号码以`+`开头，后面跟着国家代码。在这种情况下，数字已经是预期的格式。\n*   这些情况都不适用，因此结果是通过将`+`前面的国家代码和电话号码连接起来获得的。\n\nFor simplicity, we will ignore the possibility that the number is actually prefixed with another country code. You can take it as a further exercise to modify the implementation so that it can handle phone numbers with a different country prefix. These numbers should be removed from the list.\n\n在上述所有情况下，数字都可能包含空格。根据要求，这些必须拆除。`std::remove_if()`和`isspace()`功能用于此目的。\n\n以下是所述解决方案的实现:\n\n```cpp\nbool starts_with(std::string_view str, std::string_view prefix)\n{\n   return str.find(prefix) == 0;\n}\n\nvoid normalize_phone_numbers(std::vector<std::string>& numbers,\n                             std::string const & countryCode)\n{\n   std::transform(\n      std::cbegin(numbers), std::cend(numbers),\n      std::begin(numbers),\n      [countryCode](std::string const & number) {\n         std::string result;\n         if (number.size() > 0)\n         {\n            if (number[0] == '0')\n               result = \"+\" + countryCode + \n                        number.substr(1);\n            else if (starts_with(number, countryCode))\n               result = \"+\" + number;\n            else if (starts_with(number, \"+\" + countryCode))\n               result = number;\n            else\n               result = \"+\" + countryCode + number;\n      }\n\n      result.erase(\n         std::remove_if(std::begin(result), std::end(result),\n            [](const char ch) {return isspace(ch); }),\n         std::end(result));\n\n      return result;\n   });\n}\n```\n\n以下程序根据要求对给定的电话号码列表进行标准化，并将其打印在控制台上:\n\n```cpp\nint main()\n{\n   std::vector<std::string> numbers{\n      \"07555 123456\",\n      \"07555123456\",\n      \"+44 7555 123456\",\n      \"44 7555 123456\",\n      \"7555 123456\"\n   };\n\n   normalize_phone_numbers(numbers, \"44\");\n\n   for (auto const & number : numbers)\n   {\n      std::cout << number << std::endl;\n   }\n}\n```\n\n# 52.生成字符串的所有排列\n\n您可以通过利用标准库中的一些通用算法来解决这个问题。两个必选版本中最简单的就是非递归版本，至少在你使用`std::next_permutation()`的时候是这样。该函数将输入范围(需要排序)转换为所有可能排列集合中的下一个排列，按照`operator<`或指定的比较函数对象的字典顺序排列。如果这样的排列存在，那么它返回`true`，否则，它将范围转换为第一个排列并返回`false`。因此，基于`std::next_permuation()`的非递归实现如下所示:\n\n```cpp\nvoid print_permutations(std::string str)\n{\n   std::sort(std::begin(str), std::end(str));\n\n   do\n   {\n      std::cout << str << std::endl;\n   } while (std::next_permutation(std::begin(str), std::end(str)));\n}\n```\n\n递归的选择稍微复杂一点。实现它的一种方法是有一个输入和输出字符串；最初，输入字符串是我们想要为其生成置换的字符串，而输出字符串是空的。我们每次从输入字符串中提取一个字符，并将其放入输出字符串中。当输入字符串为空时，输出字符串代表下一个置换。执行此操作的递归算法如下:\n\n*   如果输入字符串为空，则打印输出字符串并返回\n*   否则，迭代输入字符串中的所有字符，并且对于每个元素:\n    *   通过从输入字符串中移除第一个字符并将其连接到输出字符串的末尾来递归调用方法\n    *   旋转输入字符串，使第一个字符成为最后一个，第二个字符成为第一个，依此类推\n\n下图直观地解释了该算法:\n\n![](img/79469191-710c-4b50-a6b6-ce67c8e5cdcf.png)\n\n为了旋转输入字符串，我们可以使用标准库函数`std::rotate()`，它对一系列元素执行向左旋转。所描述的递归算法的实现如下所示:\n\n```cpp\nvoid next_permutation(std::string str, std::string perm)\n{\n   if (str.empty()) std::cout << perm << std::endl;\n   else\n   {\n      for (size_t i = 0; i < str.size(); ++ i)\n      {\n         next_permutation(str.substr(1), perm + str[0]);\n\n         std::rotate(std::begin(str), std::begin(str) + 1, std::end(str));\n      }\n   }\n}\n\nvoid print_permutations_recursive(std::string str)\n{\n   next_permutation(str, \"\");\n}\n```\n\n这就是这两种实现的使用方式:\n\n```cpp\nint main()\n{\n   std::cout << \"non-recursive version\" << std::endl;\n   print_permutations(\"main\");\n\n   std::cout << \"recursive version\" << std::endl;\n   print_permutations_recursive(\"main\");\n}\n```\n\n# 53.电影的平均评分\n\n这个问题需要使用截断平均值来计算电影等级。这是对中心趋势的统计测量，其中平均值是在丢弃概率分布或样本的高端和低端部分后计算的。通常，这是通过在两端移除等量的点来完成的。对于此问题，您需要删除最高和最低用户评分的 5%。\n\n计算给定范围的截断平均值的函数应执行以下操作:\n\n*   对范围进行排序，以便对元素进行排序(升序或降序)\n*   移除两端所需的元素百分比\n*   计算所有剩余元素的总和\n*   通过将总和除以元素的剩余数量来计算平均值\n\n这里显示的`truncated_mean()`功能实现了所描述的算法:\n\n```cpp\ndouble truncated_mean(std::vector<int> values, double const percentage)\n{\n   std::sort(std::begin(values), std::end(values));\n   auto remove_count = static_cast<size_t>(\n                          values.size() * percentage + 0.5);\n\n   values.erase(std::begin(values), std::begin(values) + remove_count);\n   values.erase(std::end(values) - remove_count, std::end(values));\n\n   auto total = std::accumulate(\n      std::cbegin(values), std::cend(values),\n      0ull,\n      [](auto const sum, auto const e) {\n         return sum + e; });\n   return static_cast<double>(total) / values.size();\n}\n```\n\n使用此功能来计算和打印电影平均评分的程序可能如下所示:\n\n```cpp\nstruct movie\n{\n   int              id;\n   std::string      title;\n   std::vector<int> ratings;\n};\n\nvoid print_movie_ratings(std::vector<movie> const & movies)\n{\n   for (auto const & m : movies)\n   {\n      std::cout << m.title << \" : \" \n                << std::fixed << std::setprecision(1)\n                << truncated_mean(m.ratings, 0.05) << std::endl;\n   }\n}\n\nint main()\n{\n   std::vector<movie> movies\n   {\n      { 101, \"The Matrix\", {10, 9, 10, 9, 9, 8, 7, 10, 5, 9, 9, 8} },\n      { 102, \"Gladiator\", {10, 5, 7, 8, 9, 8, 9, 10, 10, 5, 9, 8, 10} },\n      { 103, \"Interstellar\", {10, 10, 10, 9, 3, 8, 8, 9, 6, 4, 7, 10} }\n   };\n\n   print_movie_ratings(movies);\n}\n```\n\n# 54.成对算法\n\n针对该问题提出的成对函数必须将输入范围的相邻元素配对，并产生添加到输出范围的`std::pair`元素。下面的代码清单提供了两种实现:\n\n*   一个以迭代器为参数的通用函数模板:开始和结束迭代器定义输入范围，输出迭代器定义输出范围中要插入结果的位置\n*   以`std::vector<T>`作为输入参数并返回`std::vector<std::pair<T, T>>`作为结果的重载；这个简单地称为第一个重载:\n\n```cpp\ntemplate <typename Input, typename Output>\nvoid pairwise(Input begin, Input end, Output result)\n{\n   auto it = begin;\n   while (it != end)\n   {\n      auto v1 = *it++ ; if (it == end) break;\n      auto v2 = *it++ ;\n      result++ = std::make_pair(v1, v2);\n   }\n}\ntemplate <typename T>\nstd::vector<std::pair<T, T>> pairwise(std::vector<T> const & range)\n{\n   std::vector<std::pair<T, T>> result;\n   pairwise(std::begin(range), std::end(range),\n            std::back_inserter(result));\n   return result;\n}\n```\n\n以下程序对整数向量的元素进行配对，并在控制台上打印配对:\n\n```cpp\nint main()\n{\n   std::vector<int> v{ 1, 1, 3, 5, 8, 13, 21 };\n   auto result = pairwise(v);\n\n   for (auto const & p : result)\n   {\n      std::cout << '{' << p.first << ',' << p.second << '}' << std::endl;\n   }\n}\n```\n\n# 55.Zip 算法\n\n这个问题与前一个问题相对类似，尽管有两个输入范围，而不是只有一个。结果还是一个`std::pair`的范围。但是，这两个输入范围可能包含不同类型的元素。同样，这里显示的实现包含两个重载:\n\n*   以迭代器为参数的通用函数。每个输入范围的开始和结束迭代器定义其边界，输出迭代器定义输出范围中必须写入结果的位置。\n*   一个接受两个`std::vector`参数的函数，一个保存类型为`T`的元素，一个保存类型为`U`的元素，并返回一个`std::vector<std::pair<T, U>>`。这个重载只是调用前一个重载:\n\n```cpp\ntemplate <typename Input1, typename Input2, typename Output>\nvoid zip(Input1 begin1, Input1 end1, \n         Input2 begin2, Input1 end2, \n         Output result)\n{\n```\n\n```cpp\n   auto it1 = begin1;\n   auto it2 = begin2;\n   while (it1 != end1 && it2 != end2)\n   {\n      result++ = std::make_pair(*it1++, *it2++);\n   }\n}\n\ntemplate <typename T, typename U>\nstd::vector<std::pair<T, U>> zip(\n   std::vector<T> const & range1, \n   std::vector<U> const & range2)\n{\n   std::vector<std::pair<T, U>> result;\n\n   zip(std::begin(range1), std::end(range1),\n       std::begin(range2), std::end(range2),\n       std::back_inserter(result));\n\n   return result;\n}\n```\n\n在下面的清单中，您可以看到两个整数向量压缩在一起，结果打印在控制台上:\n\n```cpp\nint main()\n{\n   std::vector<int> v1{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };\n   std::vector<int> v2{ 1, 1, 3, 5, 8, 13, 21 };\n\n   auto result = zip(v1, v2);\n   for (auto const & p : result)\n   {\n      std::cout << '{' << p.first << ',' << p.second << '}' << std::endl;\n   }\n}\n```\n\n# 56.选择算法\n\n您必须实现的`select()`函数将一个`std::vector<T>`作为输入参数和一个类型为`F`的函数，并返回一个`std::vector<R>`作为结果，其中`R`是将`F`应用到`T`的结果。我们可以在编译时使用`std::result_of()`来推断调用表达式的返回类型。在内部，`select()`函数应该使用`std::transform()`迭代输入向量的元素，将函数`f`应用于每个元素，并将结果插入输出向量。\n\n下面的清单显示了这个函数的实现:\n\n```cpp\ntemplate <\n   typename T, typename A, typename F,\n   typename R = typename std::decay<typename std::result_of<\n                typename std::decay<F>::type&(\n                typename std::vector<T, A>::const_reference)>::type>::type>\nstd::vector<R> select(std::vector<T, A> const & c, F&& f)\n{\n   std::vector<R> v;\n   std::transform(std::cbegin(c), std::cend(c),\n                  std::back_inserter(v),\n                  std::forward<F>(f));\n   return v;\n}\n```\n\n该功能可以如下使用:\n\n```cpp\nint main()\n{\n   std::vector<book> books{\n      {101, \"The C++ Programming Language\", \"Bjarne Stroustrup\"},\n      {203, \"Effective Modern C++\", \"Scott Meyers\"},\n      {404, \"The Modern C++ Programming Cookbook\", \"Marius Bancila\"}};\n\n   auto titles = select(books, [](book const & b) {return b.title; });\n   for (auto const & title : titles)\n   {\n      std::cout << title << std::endl;\n   }\n}\n```\n\n# 57.分类算法\n\n**快速排序**是对定义了总顺序的数组元素的比较排序算法。当实现良好时，它明显快于*合并排序*或*堆排序*。\n\n虽然在最坏的情况下，算法会进行![](img/d0f773fd-4c52-4f1c-ac3a-43aad74ac919.png)比较(当范围已经排序时)，但平均来说复杂度只有![](img/052b20b9-c6ee-407f-9169-9f60e59fcd12.png)。Quicksort 是一个分治算法；它将一个较大的范围划分成较小的范围，并递归地对它们进行排序。有几种分区方案。在这里显示的实现中，我们使用了由*东尼·霍尔*开发的原始实现。该方案的算法用伪代码描述如下:\n\n```cpp\nalgorithm quicksort(A, lo, hi) is\n   if lo < hi then\n      p := partition(A, lo, hi)\n      quicksort(A, lo, p)\n      quicksort(A, p + 1, hi)\n\nalgorithm partition(A, lo, hi) is\n   pivot := A[lo]\n   i := lo - 1\n   j := hi + 1\n   loop forever\n      do\n         i := i + 1\n      while A[i] < pivot\n\n      do\n         j := j - 1\n      while A[j] > pivot\n\n      if i >= j then\n         return j\n\n      swap A[i] with A[j]\n```\n\n算法的通用实现应该使用迭代器，而不是数组和索引。以下实现的要求是迭代器是随机访问的(因此它们可以在恒定时间内移动到任何元素):\n\n```cpp\ntemplate <class RandomIt>\nRandomIt partition(RandomIt first, RandomIt last)\n{\n   auto pivot = *first;\n   auto i = first + 1;\n   auto j = last - 1;\n   while (i <= j)\n   {\n      while (i <= j && *i <= pivot) i++ ;\n      while (i <= j && *j > pivot) j--;\n      if (i < j) std::iter_swap(i, j);\n   }\n\n   std::iter_swap(i - 1, first);\n\n   return i - 1;\n}\n\ntemplate <class RandomIt>\nvoid quicksort(RandomIt first, RandomIt last)\n{\n   if (first < last)\n   {\n      auto p = partition(first, last);\n      quicksort(first, p);\n      quicksort(p + 1, last);\n   }\n}\n```\n\n如下所示的`quicksort()`功能可用于分类各种类型的容器:\n\n```cpp\nint main()\n{\n   std::vector<int> v{ 1,5,3,8,6,2,9,7,4 };\n   quicksort(std::begin(v), std::end(v));\n\n   std::array<int, 9> a{ 1,2,3,4,5,6,7,8,9 };\n   quicksort(std::begin(a), std::end(a));\n\n   int a[]{ 9,8,7,6,5,4,3,2,1 };\n   quicksort(std::begin(a), std::end(a));\n}\n```\n\n要求是排序算法必须允许指定用户定义的比较函数。在这种情况下，唯一的变化是分区函数，我们使用用户定义的比较函数，而不是使用`operator <`和`>`来比较当前元素和透视:\n\n```cpp\ntemplate <class RandomIt, class Compare>\nRandomIt partitionc(RandomIt first, RandomIt last, Compare comp)\n{\n   auto pivot = *first;\n   auto i = first + 1;\n   auto j = last - 1;\n   while (i <= j)\n   {\n      while (i <= j && comp(*i, pivot)) i++ ;\n      while (i <= j && !comp(*j, pivot)) j--;\n      if (i < j) std::iter_swap(i, j);\n   }\n\n   std::iter_swap(i - 1, first);\n\n   return i - 1;\n}\n\ntemplate <class RandomIt, class Compare>\nvoid quicksort(RandomIt first, RandomIt last, Compare comp)\n{\n   if (first < last)\n   {\n      auto p = partitionc(first, last, comp);\n      quicksort(first, p, comp);\n      quicksort(p + 1, last, comp);\n   }\n}\n```\n\n有了这个重载，我们可以按降序对一个范围进行排序，如下例所示:\n\n```cpp\nint main()\n{\n   std::vector<int> v{ 1,5,3,8,6,2,9,7,4 };\n   quicksort(std::begin(v), std::end(v), std::greater<>());\n}\n```\n\n也可以实现快速排序算法的迭代版本。迭代版本的性能在大多数情况下与递归版本![](img/3ca8d288-8880-429d-aa80-7c340746023e.png)相同，但是当范围已经排序时，在最坏的情况下会退化为![](img/93a576b4-a09d-46aa-83d2-29b43033fde1.png)。从算法的递归版本转换为迭代版本相对简单；这是通过使用堆栈来模拟递归调用和存储分区的边界来实现的。以下是使用`operator<`比较元素的版本的迭代实现:\n\n```cpp\ntemplate <class RandomIt>\nvoid quicksorti(RandomIt first, RandomIt last)\n{\n   std::stack<std::pair<RandomIt, RandomIt>> st;\n   st.push(std::make_pair(first, last));\n   while (!st.empty())\n   {\n      auto iters = st.top();\n      st.pop();\n\n      if (iters.second - iters.first < 2) continue;\n\n      auto p = partition(iters.first, iters.second);\n\n      st.push(std::make_pair(iters.first, p));\n      st.push(std::make_pair(p+1, iters.second));\n   }\n}\n```\n\n这种迭代实现可以像递归实现一样使用:\n\n```cpp\nint main()\n{\n   std::vector<int> v{ 1,5,3,8,6,2,9,7,4 };\n   quicksorti(std::begin(v), std::end(v));\n}\n```\n\n# 58.节点之间的最短路径\n\n要解决提出的问题，您必须使用 Dijkstra 算法来寻找图中的最短路径。虽然最初的算法找到了两个给定节点之间的最短路径，但这里的要求是找到一个指定节点和图中所有其他节点之间的最短路径，这是算法的另一个版本。\n\n实现该算法的一种有效方法是使用优先级队列。算法的伪代码(见[https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm](https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm))如下:\n\n```cpp\nfunction Dijkstra(Graph, source):\n   dist[source] ← 0                 // Initialization\n\n   create vertex set Q\n   for each vertex v in Graph: \n      if v ≠ source\n         dist[v] ← INFINITY         // Unknown distance from source to v\n         prev[v] ← UNDEFINED        // Predecessor of v\n\n      Q.add_with_priority(v, dist[v])\n\n   while Q is not empty:            // The main loop\n      u ← Q.extract_min()           // Remove and return best vertex\n      for each neighbor v of u:     // only v that is still in Q\n         alt ← dist[u] + length(u, v) \n         if alt < dist[v]\n            dist[v] ← alt\n            prev[v] ← u\n            Q.decrease_priority(v, alt)\n\n   return dist[], prev[]\n```\n\n为了表示该图，我们可以使用以下数据结构，它可以用于方向图或单向图。该类支持添加新的顶点和边，并且可以返回顶点列表和指定顶点的邻居(即节点和到它们的距离):\n\n```cpp\ntemplate <typename Vertex = int, typename Weight = double>\nclass graph\n{\npublic:\n   typedef Vertex                     vertex_type;\n   typedef Weight                     weight_type;\n   typedef std::pair<Vertex, Weight>  neighbor_type;\n   typedef std::vector<neighbor_type> neighbor_list_type;\npublic:\n   void add_edge(Vertex const source, Vertex const target, \n                 Weight const weight, bool const bidirectional = true)\n   {\n      adjacency_list[source].push_back(std::make_pair(target, weight));\n      adjacency_list[target].push_back(std::make_pair(source, weight));\n   }\n\n   size_t vertex_count() const { return adjacency_list.size(); }\n   std::vector<Vertex> verteces() const\n   {\n      std::vector<Vertex> keys;\n      for (auto const & kvp : adjacency_list)\n         keys.push_back(kvp.first);\n      return keys;\n   }\n\n   neighbor_list_type const & neighbors(Vertex const & v) const\n   {\n      auto pos = adjacency_list.find(v);\n      if (pos == adjacency_list.end())\n         throw std::runtime_error(\"vertex not found\");\n      return pos->second;\n   }\n\n   constexpr static Weight Infinity = \n             std::numeric_limits<Weight>::infinity();\nprivate:\n   std::map<vertex_type, neighbor_list_type> adjacency_list;\n};\n```\n\n前面伪代码中描述的最短路径算法的实现如下所示。使用`std::set`(即自平衡二叉查找树)代替优先级队列。`std::set`具有与二进制堆(用于优先级队列)相同的添加和移除顶部元素的![](img/d099c811-290c-4643-951a-91cf936fe579.png)复杂度。另一方面，`std::set`还允许在![](img/61c9a4de-7e8e-4c76-bbf0-8f86a6828406.png)中查找和移除任何其他元素，这有助于通过再次移除和插入来实现对数时间中的递减键步骤:\n\n```cpp\ntemplate <typename Vertex, typename Weight>\nvoid shortest_path(\n   graph<Vertex, Weight> const & g,\n   Vertex const source,\n   std::map<Vertex, Weight>& min_distance,\n   std::map<Vertex, Vertex>& previous)\n{\n   auto const n = g.vertex_count();\n   auto const verteces = g.verteces();\n\n   min_distance.clear();\n   for (auto const & v : verteces)\n      min_distance[v] = graph<Vertex, Weight>::Infinity;\n   min_distance[source] = 0;\n\n   previous.clear();\n\n   std::set<std::pair<Weight, Vertex> > vertex_queue;\n   vertex_queue.insert(std::make_pair(min_distance[source], source));\n\n   while (!vertex_queue.empty())\n   {\n      auto dist = vertex_queue.begin()->first;\n      auto u = vertex_queue.begin()->second;\n\n      vertex_queue.erase(std::begin(vertex_queue));\n\n      auto const & neighbors = g.neighbors(u);\n      for (auto const & neighbor : neighbors)\n      {\n         auto v = neighbor.first;\n         auto w = neighbor.second;\n         auto dist_via_u = dist + w;\n         if (dist_via_u < min_distance[v])\n         {\n            vertex_queue.erase(std::make_pair(min_distance[v], v));\n\n            min_distance[v] = dist_via_u;\n            previous[v] = u;\n            vertex_queue.insert(std::make_pair(min_distance[v], v));\n         }\n      }\n   }\n}\n```\n\n以下助手函数以指定的格式打印结果:\n\n```cpp\ntemplate <typename Vertex>\nvoid build_path(\n   std::map<Vertex, Vertex> const & prev, Vertex const v,\n   std::vector<Vertex> & result)\n{\n   result.push_back(v);\n\n   auto pos = prev.find(v);\n   if (pos == std::end(prev)) return;\n\n   build_path(prev, pos->second, result);\n}\n\ntemplate <typename Vertex>\nstd::vector<Vertex> build_path(std::map<Vertex, Vertex> const & prev, \n                               Vertex const v)\n{\n   std::vector<Vertex> result;\n   build_path(prev, v, result);\n   std::reverse(std::begin(result), std::end(result));\n   return result;\n}\n\ntemplate <typename Vertex>\nvoid print_path(std::vector<Vertex> const & path)\n{\n   for (size_t i = 0; i < path.size(); ++ i)\n   {\n      std::cout << path[i];\n      if (i < path.size() - 1) std::cout << \" -> \";\n   }\n}\n```\n\n以下程序解决了给定的任务:\n\n```cpp\nint main()\n{\n   graph<char, double> g;\n   g.add_edge('A', 'B', 7);\n   g.add_edge('A', 'C', 9);\n   g.add_edge('A', 'F', 14);\n   g.add_edge('B', 'C', 10);\n   g.add_edge('B', 'D', 15);\n   g.add_edge('C', 'D', 11);\n   g.add_edge('C', 'F', 2);\n   g.add_edge('D', 'E', 6);\n   g.add_edge('E', 'F', 9);\n\n   char source = 'A';\n   std::map<char, double> min_distance;\n   std::map<char, char> previous;\n   shortest_path(g, source, min_distance, previous);\n\n   for (auto const & kvp : min_distance)\n   {\n      std::cout << source << \" -> \" << kvp.first << \" : \"\n                << kvp.second << '\\t';\n\n      print_path(build_path(previous, kvp.first));\n\n      std::cout << std::endl;\n   }\n}\n```\n\n# 59.黄鼠狼计划\n\n黄鼠狼计划是理查德·道金斯提出的一个思维实验，旨在证明累积的小改进(突变给个体带来好处，从而被自然选择选择)如何产生快速结果，而不是主流的误解，即进化发生在大跳跃中。维基百科上描述的黄鼠狼模拟算法(见[https://en.wikipedia.org/wiki/Weasel_program](https://en.wikipedia.org/wiki/Weasel_program))如下:\n\n1.  从 28 个字符的随机字符串开始。\n2.  制作这个字符串的 100 个副本，每个字符有 5%的几率被随机字符替换。\n3.  将每个新字符串与目标 methanks IT IS LIKE A weak 进行比较，并给每个字符串打分(字符串中正确且位置正确的字母数量)。\n4.  如果任何一个新字符串有满分(28 分)，那么停止。\n5.  否则，取得分最高的字符串，进入第 2 步。\n\n一个可能的实现如下。`make_random()`功能创建一个与目标长度相同的随机起始序列；`fitness()`函数计算每个变异字符串的得分(即与目标的相似度)；`mutate()`函数从父级产生一个新的字符串，每个字符都有一定的变异机会:\n\n```cpp\nclass weasel\n{\n   std::string target;\n   std::uniform_int_distribution<> chardist;\n   std::uniform_real_distribution<> ratedist;\n   std::mt19937 mt;\n   std::string const allowed_chars = \"ABCDEFGHIJKLMNOPQRSTUVWXYZ \";\npublic:\n   weasel(std::string_view t) :\n      target(t), chardist(0, 26), ratedist(0, 100)\n   {\n      std::random_device rd;\n      auto seed_data = std::array<int, std::mt19937::state_size> {};\n      std::generate(std::begin(seed_data), std::end(seed_data), \n      std::ref(rd));\n      std::seed_seq seq(std::begin(seed_data), std::end(seed_data));\n      mt.seed(seq);\n   }\n```\n\n```cpp\n   void run(int const copies)\n   {\n      auto parent = make_random();\n      int step = 1;\n      std::cout << std::left << std::setw(5) << std::setfill(' ') \n                << step << parent << std::endl;\n\n      do\n      {\n         std::vector<std::string> children;\n         std::generate_n(std::back_inserter(children), copies, \n            [parent, this]() {return mutate(parent, 5); });\n\n         parent = *std::max_element(\n            std::begin(children), std::end(children),\n            [this](std::string_view c1, std::string_view c2) {\n               return fitness(c1) < fitness(c2); });\n\n         std::cout << std::setw(5) << std::setfill(' ') << step \n                << parent << std::endl;\n\n         step++ ;\n      } while (parent != target);\n   }\nprivate:\n   weasel() = delete;\n\n   double fitness(std::string_view candidate) const\n   {\n      int score = 0;\n      for (size_t i = 0; i < candidate.size(); ++ i)\n      {\n         if (candidate[i] == target[i])\n            score++ ;\n      }\n      return score;\n   }\n\n   std::string mutate(std::string_view parent, double const rate)\n   {\n      std::stringstream sstr;\n      for (auto const c : parent)\n      {\n         auto nc = ratedist(mt) > rate ? c : allowed_chars[chardist(mt)];\n         sstr << nc;\n      }\n      return sstr.str();\n    }\n\n   std::string make_random()\n   {\n      std::stringstream sstr;\n      for (size_t i = 0; i < target.size(); ++ i)\n      {\n         sstr << allowed_chars[chardist(mt)];\n      }\n      return sstr.str();\n   }\n};\n```\n\n该类可以这样使用:\n\n```cpp\nint main()\n{\n   weasel w(\"METHINKS IT IS LIKE A WEASEL\");\n   w.run(100);\n}\n```\n\n# 60.生活的游戏\n\n下面展示的类`universe`按照描述实现游戏。有几个感兴趣的功能:\n\n*   `initialize()`生成起始布局；虽然书中附带的代码包含了更多的选项，但这里只列出了两个:`random`生成随机布局，`ten_cell_row`代表网格中间的一行 10 个单元格。\n*   `reset()`将所有单元格设置为`dead`。\n*   `count_neighbors()`返回存活邻居的数量。它使用了一个辅助变量函数模板`count_alive()`。虽然这可以用 fold 表达式来实现，但是在 Visual C++ 中还不支持，因此我选择不在这里使用它。\n*   `next_generation()`根据过渡规则产生新的游戏状态。\n*   `display()`在控制台上显示游戏状态；这使用系统调用来擦除控制台，尽管您可以使用其他方法来这样做，例如特定的操作系统 API。\n*   `run()`初始化起始布局，然后以用户指定的间隔、用户指定的迭代次数或无限期(如果迭代次数设置为 0)生成新的一代。\n\n```cpp\nclass universe\n{\nprivate:\n   universe() = delete;\npublic:\n   enum class seed\n   {\n      random, ten_cell_row\n   };\npublic:\n   universe(size_t const width, size_t const height):\n      rows(height), columns(width),grid(width * height), dist(0, 4)\n   {\n      std::random_device rd;\n      auto seed_data = std::array<int, std::mt19937::state_size> {};\n      std::generate(std::begin(seed_data), std::end(seed_data), \n      std::ref(rd));\n      std::seed_seq seq(std::begin(seed_data), std::end(seed_data));\n      mt.seed(seq);\n   }\n\n   void run(seed const s, int const generations, \n            std::chrono::milliseconds const ms = \n               std::chrono::milliseconds(100))\n   {\n      reset();\n      initialize(s);\n      display();\n\n      int i = 0;\n      do \n      {\n         next_generation();\n         display();\n\n         using namespace std::chrono_literals;\n         std::this_thread::sleep_for(ms);\n      } while (i++ < generations || generations == 0);\n   }\n\nprivate:\n   void next_generation()\n   {\n      std::vector<unsigned char> newgrid(grid.size());\n\n      for (size_t r = 0; r < rows; ++ r)\n      {\n         for (size_t c = 0; c < columns; ++ c)\n         {\n            auto count = count_neighbors(r, c);\n\n            if (cell(c, r) == alive)\n            {\n               newgrid[r * columns + c] = \n                  (count == 2 || count == 3) ? alive : dead;\n            }\n            else \n            {\n               newgrid[r * columns + c] = (count == 3) ? alive : dead;\n            }\n         }\n      }\n\n      grid.swap(newgrid);\n   }\n\n   void reset_display()\n   {\n#ifdef WIN32\n      system(\"cls\");\n#endif\n   }\n\n   void display()\n   {\n      reset_display();\n\n      for (size_t r = 0; r < rows; ++ r)\n      {\n         for (size_t c = 0; c < columns; ++ c)\n         {\n            std::cout << (cell(c, r) ? '*' : ' ');\n         }\n         std::cout << std::endl;\n      }\n   }\n\n   void initialize(seed const s)\n   {\n      if (s == seed::ten_cell_row)\n      {\n         for (size_t c = columns / 2 - 5; c < columns / 2 + 5; c++)\n            cell(c, rows / 2) = alive;\n      }\n      else\n      {\n         for (size_t r = 0; r < rows; ++ r)\n         {\n            for (size_t c = 0; c < columns; ++ c)\n            {\n               cell(c, r) = dist(mt) == 0 ? alive : dead;\n            }\n         }\n      }\n   }\n\n   void reset()\n   {\n      for (size_t r = 0; r < rows; ++ r)\n      {\n         for (size_t c = 0; c < columns; ++ c)\n         {\n            cell(c, r) = dead;\n         }\n      }\n   }\n\n   int count_alive() { return 0; }\n\n   template<typename T1, typename... T>\n   auto count_alive(T1 s, T... ts) { return s + count_alive(ts...); }\n\n   int count_neighbors(size_t const row, size_t const col)\n   {\n      if (row == 0 && col == 0) \n         return count_alive(cell(1, 0), cell(1,1), cell(0, 1));\n      if (row == 0 && col == columns - 1)\n         return count_alive(cell(columns - 2, 0), cell(columns - 2, 1), \n                            cell(columns - 1, 1));\n      if (row == rows - 1 && col == 0)\n         return count_alive(cell(0, rows - 2), cell(1, rows - 2), \n                            cell(1, rows - 1));\n      if (row == rows - 1 && col == columns - 1)\n         return count_alive(cell(columns - 1, rows - 2), \n                            cell(columns - 2, rows - 2), \n                            cell(columns - 2, rows - 1));\n```\n\n```cpp\n      if (row == 0 && col > 0 && col < columns - 1)\n         return count_alive(cell(col - 1, 0), cell(col - 1, 1), \n                            cell(col, 1), cell(col + 1, 1), \n                            cell(col + 1, 0));\n      if (row == rows - 1 && col > 0 && col < columns - 1)\n         return count_alive(cell(col - 1, row), cell(col - 1, row - 1), \n                            cell(col, row - 1), cell(col + 1, row - 1), \n                            cell(col + 1, row));\n      if (col == 0 && row > 0 && row < rows - 1)\n         return count_alive(cell(0, row - 1), cell(1, row - 1), \n                            cell(1, row), cell(1, row + 1), \n                            cell(0, row + 1));\n      if (col == columns - 1 && row > 0 && row < rows - 1)\n         return count_alive(cell(col, row - 1), cell(col - 1, row - 1), \n                            cell(col - 1, row), cell(col - 1, row + 1), \n                            cell(col, row + 1));\n\n      return count_alive(cell(col - 1, row - 1), cell(col, row - 1), \n                         cell(col + 1, row - 1), cell(col + 1, row), \n                         cell(col + 1, row + 1), cell(col, row + 1), \n                         cell(col - 1, row + 1), cell(col - 1, row));\n   }\n\n   unsigned char& cell(size_t const col, size_t const row)\n   {\n      return grid[row * columns + col];\n   }\n\nprivate:\n   size_t rows;\n   size_t columns;\n\n   std::vector<unsigned char> grid;\n   const unsigned char alive = 1;\n   const unsigned char dead = 0;\n\n   std::uniform_int_distribution<> dist;\n   std::mt19937 mt;\n};\n```\n\n这就是游戏如何从随机状态开始运行 100 次迭代:\n\n```cpp\nint main()\n{\n   using namespace std::chrono_literals;\n   universe u(50, 20);\n   u.run(universe::seed::random, 100, 100ms);\n}\n```\n\n下面是一个程序输出的例子(截图代表了生命游戏宇宙中的一次迭代):\n\n![](img/64f1b7b8-0beb-4758-b368-be239d3cb368.png)*"
  },
  {
    "path": "docs/mod-cpp-challenge/07.md",
    "content": "# 七、并发\n\n# 问题\n\n# 61.并行变换算法\n\n编写一个通用算法，应用给定的一元函数并行转换一个范围的元素。用于转换范围的一元操作不得使范围迭代器无效或修改范围的元素。并行级别，即执行线程的数量和实现方式，是一个实现细节。\n\n# 62.使用线程的并行最小和最大元素算法\n\n实现通用并行算法，分别找出给定范围内的最小值和最大值。并行性应该使用线程来实现，尽管并发线程的数量是一个实现细节。\n\n# 63.使用异步函数的并行最小和最大元素算法\n\n实现通用并行算法，分别找出给定范围内的最小值和最大值。并行性应该使用异步函数来实现，尽管并发函数的数量是一个实现细节。\n\n# 64.并行排序算法\n\n按照问题 *53 的定义，编写排序算法的并行版本。排序算法*，在[第 6 章](06.html)、*算法和数据结构*中，给定一对随机访问迭代器来定义其上下界，使用快速排序算法对范围的元素进行排序。函数应该使用比较运算符来比较范围的元素。并行级别和实现方式是一个实现细节。\n\n# 65.控制台的线程安全日志记录\n\n编写一个类，通过同步对标准输出流的访问，使在不同线程中运行的组件能够安全地将日志消息打印到控制台，以保证输出的完整性。这个日志记录组件应该有一个名为`log()`的方法，它带有一个字符串参数，表示要打印到控制台的消息。\n\n# 66.客户服务系统\n\n编写一个程序，模拟在办公室为顾客提供服务的方式。办公室有三张桌子，可以同时接待顾客。顾客可以随时进入办公室。他们从售票机上取一张带有服务号码的票，在其中一个服务台等待下一个服务号码。顾客按照他们进入办公室的顺序，或者更准确地说，按照他们的票给出的顺序得到服务。每次服务台服务完一位顾客，下一位顾客就会得到服务。在特定数量的顾客已经得到票和服务之后，模拟应该停止。\n\n# 解决方法\n\n# 61.并行变换算法\n\n通用函数`std::transform()`将给定的函数应用于一个范围，并将结果存储在另一个(或相同的)范围内。这个问题的要求是实现这样一个函数的并行版本。通用迭代器将迭代器作为参数来定义范围的第一个和最后一个元素。因为一元函数以相同的方式应用于范围的所有元素，所以并行化操作相当简单。对于这个任务，我们将使用线程。由于没有规定应该同时运行多少个线程，我们可以使用`std::thread::hardware_concurrency()`。此函数返回实现支持的并发线程数的提示。\n\n只有当范围的大小超过特定的阈值时，并行版本的算法才能比顺序实现更好地执行，该阈值可能因编译选项、平台或硬件而异。在下面的实现中，该阈值被设置为 10，000 个元素。作为进一步的练习，您可以试验各种阈值和范围大小，看看执行时间是如何变化的。\n\n以下函数`ptransform()`按照要求实现并行变换算法。如果范围大小没有超过定义的阈值，则简单地调用`std::transform()`。否则，它会将范围分成几个相等的部分，每个线程一个，并为特定的子范围调用每个线程上的`std::transform()`。在这种情况下，函数阻塞调用线程，直到所有工作线程完成执行:\n\n```cpp\ntemplate <typename RandomAccessIterator, typename F>\nvoid ptransform(RandomAccessIterator begin, RandomAccessIterator end, \n                F&& f)\n{\n   auto size = std::distance(begin, end);\n   if (size <= 10000)\n   {\n      std::transform(begin, end, begin, std::forward<F>(f));\n   }\n   else\n   {\n      std::vector<std::thread> threads;\n      int thread_count = std::thread::hardware_concurrency();\n      auto first = begin;\n      auto last = first;\n      size /= thread_count;\n      for (int i = 0; i < thread_count; ++ i)\n      {\n         first = last;\n         if (i == thread_count - 1) last = end;\n         else std::advance(last, size);\n\n         threads.emplace_back([first, last, &f]() {\n            std::transform(first, last, first, std::forward<F>(f));\n         });\n      }\n\n      for (auto & t : threads) t.join();\n   }\n}\n```\n\n如下所示，函数`palter()`是一个辅助函数，它将`ptransform()`应用到一个`std::vector`并返回另一个`std::vector`结果:\n\n```cpp\ntemplate <typename T, typename F>\nstd::vector<T> palter(std::vector<T> data, F&& f)\n{\n   ptransform(std::begin(data), std::end(data),\n              std::forward<F>(f));\n   return data;\n}\n```\n\n该函数可以如下使用(完整的例子可以在本书附带的源代码中找到):\n\n```cpp\nint main()\n{\n   std::vector<int> data(1000000);\n   // init data\n   auto result = palter(data, [](int const e) {return e * e; });\n}\n```\n\nIn C++ 17, a series of standard general-purpose algorithms, including `std::transform()`, have overloads that implement a parallel version of the algorithm that can be executed according to a specified execution policy.\n\n# 62.使用线程的并行最小和最大元素算法\n\n这个问题及其解决方案在大多数方面与前一个问题相似。略有不同的是，在每个线程上并发执行的函数必须返回一个值，该值代表子范围中的最小或最大元素。`pprocess()`函数模板如下所示，是一个更高级别的函数，一般以下列方式实现所请求的功能:\n\n*   它的参数是范围的第一个和最后一个迭代器，以及处理我们称之为`f`的范围的函数对象。\n*   如果范围的大小小于一个特定的阈值，这里设置为 10，000 个元素，它只执行作为参数接收的函数对象`f`。\n*   否则，它会将输入范围分割成多个大小相等的子范围，每个子范围对应一个可以执行的并发线程。每个线程在选定的子范围内运行`f`。\n*   `f`的并行执行结果被收集在一个`std::vector`中，在所有线程的执行完成后，再次使用`f`从中间结果中确定整体结果:\n\n```cpp\ntemplate <typename Iterator, typename F>\nauto pprocess(Iterator begin, Iterator end, F&& f)\n{\n   auto size = std::distance(begin, end);\n   if (size <= 10000)\n   {\n      return std::forward<F>(f)(begin, end);\n   }\n   else\n   {\n      int thread_count = std::thread::hardware_concurrency();\n      std::vector<std::thread> threads;\n      std::vector<typename std::\n         iterator_traits<Iterator>::value_type> \n      mins(thread_count);\n\n      auto first = begin;\n      auto last = first;\n      size /= thread_count;\n      for (int i = 0; i < thread_count; ++ i)\n      {\n         first = last;\n         if (i == thread_count - 1) last = end;\n         else std::advance(last, size);\n\n         threads.emplace_back([first, last, &f, &r=mins[i]]() {\n         r = std::forward<F>(f)(first, last);\n         });\n      }\n\n      for (auto & t : threads) t.join();\n\n      return std::forward<F>(f)(std::begin(mins), std::end(mins));\n   }\n}\n```\n\n提供了两个称为`pmin()`和`pmax()`的函数来实现所需的通用最小和最大并行算法。这两个依次调用`pprocess()`，为第三个参数传递使用`std::min_element()`或`std::max_element()`标准算法的λ:\n\n```cpp\ntemplate <typename Iterator>\nauto pmin(Iterator begin, Iterator end)\n{\n   return pprocess(begin, end,\n                   [](auto b, auto e){return *std::min_element(b, e);});\n}\n\ntemplate <typename Iterator>\nauto pmax(Iterator begin, Iterator end)\n{\n   return pprocess(begin, end,\n                   [](auto b, auto e){return *std::max_element(b, e);});\n}\n```\n\n这些功能可以如下使用:\n\n```cpp\nint main()\n{\n   std::vector<int> data(count);\n   // init data\n   auto rmin = pmin(std::begin(data), std::end(data));\n   auto rmax = pmin(std::begin(data), std::end(data));\n}\n```\n\nYou can take it as a further exercise to implement yet another general-purpose algorithm that computes the sum of all the elements of a range in parallel using threads.\n\n# 63.使用异步函数的并行最小和最大元素算法\n\n这个问题和前一个问题的唯一区别是并行性是如何实现的。对于前面的问题，需要使用线程。对于这个，您必须使用异步函数。一个函数可以与`std::async()`异步执行。这个函数创建一个*承诺*，它是异步执行的函数结果的异步提供者。承诺有一个共享状态(可以存储函数的返回值或函数执行产生的异常)和一个相关的*未来*对象，该对象提供从不同线程对共享状态的访问。promise-future 对定义了一个能够跨线程传递值的通道。`std::async()`返回与其创造的承诺相关的未来。\n\n在`pprocess()`的以下实现中，使用来自先前版本的线程已经被对`std::async()`的调用所取代。请注意，您必须将`std::launch::async`指定为`std::async()`的第一个参数，以保证异步执行，而不是惰性计算。与以前的实现相比，变化量非常小，根据以前的实现对算法的描述，遵循代码应该很容易:\n\n```cpp\ntemplate <typename Iterator, typename F>\nauto pprocess(Iterator begin, Iterator end, F&& f)\n{\n   auto size = std::distance(begin, end);\n   if (size <= 10000)\n   {\n      return std::forward<F>(f)(begin, end);\n   }\n   else\n   {\n      int task_count = std::thread::hardware_concurrency();\n      std::vector<std::future<\n         typename std::iterator_traits<Iterator>::value_type>> tasks;\n\n      auto first = begin;\n      auto last = first;\n      size /= task_count;\n      for (int i = 0; i < task_count; ++ i)\n      {\n         first = last;\n         if (i == task_count - 1) last = end;\n         else std::advance(last, size);\n\n         tasks.emplace_back(std::async(\n            std::launch::async,\n            [first, last, &f]() {\n               return std::forward<F>(f)(first, last);\n         }));\n      }\n\n      std::vector<typename std::iterator_traits<Iterator>::value_type> \n         mins;\n\n      for (auto & t : tasks) \n         mins.push_back(t.get());\n\n      return std::forward<F>(f)(std::begin(mins), std::end(mins));\n   }\n}\n\ntemplate <typename Iterator>\nauto pmin(Iterator begin, Iterator end)\n{\n   return pprocess(begin, end,\n                   [](auto b, auto e){return *std::min_element(b, e);});\n}\n\ntemplate <typename Iterator>\nauto pmax(Iterator begin, Iterator end)\n{\n   return pprocess(begin, end,\n                   [](auto b, auto e){return *std::max_element(b, e);});\n}\n```\n\n下面的代码显示了如何使用这个函数:\n\n```cpp\nint main()\n{\n   std::vector<int> data(count);\n   // init data\n   auto rmin = pmin(std::begin(data), std::end(data));\n   auto rmax = pmax(std::begin(data), std::end(data));\n}\n```\n\nYou can again take it as a further exercise to implement a general-purpose algorithm that computes the sum of all the elements of a range in parallel using asynchronous functions.\n\n# 64.并行排序算法\n\n我们之前看到了 quicksort 算法的顺序实现。快速排序是一种分治算法，它依赖于将要排序的范围划分为两部分，一部分只包含比所选元素小的元素，称为透视，另一部分只包含比透视大的元素。然后，它继续递归地对两个分区应用相同的算法，直到这些分区只有一个元素或者没有元素。由于算法的性质，quicksort 可以很容易地并行化，以便在两个分区上同时递归应用算法。\n\n`pquicksort()`函数为此使用异步函数。然而，并行化只对较大的范围有效。存在一个阈值，在该阈值下，并行执行的上下文切换开销太大，并且并行执行时间大于顺序执行时间。在下面的实现中，该阈值被设置为 100，000 个元素，但是作为进一步的练习，您可以尝试设置不同的值，并查看并行版本与顺序版本相比的表现:\n\n```cpp\ntemplate <class RandomIt>\nRandomIt partition(RandomIt first, RandomIt last)\n{\n   auto pivot = *first;\n   auto i = first + 1;\n   auto j = last - 1;\n   while (i <= j)\n   {\n      while (i <= j && *i <= pivot) i++ ;\n      while (i <= j && *j > pivot) j--;\n      if (i < j) std::iter_swap(i, j);\n    }\n\n    std::iter_swap(i - 1, first);\n\n    return i - 1;\n}\n\n```\n\n```cpp\ntemplate <class RandomIt>\nvoid pquicksort(RandomIt first, RandomIt last)\n{\n   if (first < last)\n   {\n      auto p = partition(first, last);\n\n      if(last - first <= 100000)\n      {\n         pquicksort(first, p);\n         pquicksort(p + 1, last);\n      }\n      else\n      {\n         auto f1 = std::async(std::launch::async,\n            [first, p](){ pquicksort(first, p);});\n         auto f2 = std::async(std::launch::async,\n            [last, p]() { pquicksort(p+1, last);});\n         f1.wait();\n         f2.wait();\n      }\n   }\n}\n```\n\n下面的代码显示了如何使用`pquicksort()`函数对随机整数的大向量(值在 1 到 1000 之间)进行排序:\n\n```cpp\nint main()\n{\n   std::random_device rd;\n   std::mt19937 mt;\n   auto seed_data = std::array<int, std::mt19937::state_size> {};\n   std::generate(std::begin(seed_data), std::end(seed_data),   \n                 std::ref(rd));\n   std::seed_seq seq(std::begin(seed_data), std::end(seed_data));\n   mt.seed(seq);\n   std::uniform_int_distribution<> ud(1, 1000);\n\n   const size_t count = 1000000;\n   std::vector<int> data(count); \n   std::generate_n(std::begin(data), count, \n   [&mt, &ud]() {return ud(mt); });\n\n   pquicksort(std::begin(data), std::end(data));\n}\n```\n\n# 65.控制台的线程安全日志记录\n\n虽然 C++ 没有控制台的概念，而是使用流对文件等顺序媒体执行输入和输出操作，但是`std::cout`和`std::wcout`全局对象控制到与 C 输出流`stdout`相关联的流缓冲区的输出。无法从不同的线程安全地访问这些全局流对象。如果需要，您必须同步对它们的访问。这正是这个问题所请求的组件的目的。\n\n如下所示的`logger`类使用`std::mutex`来同步对`log()`方法中的`std::cout`对象的访问。该类实现为线程安全的单例。静态方法`instance()`返回对本地静态对象(具有存储持续时间)的引用。在 C++ 11 中，静态对象的初始化只发生一次，即使几个线程试图同时初始化同一个静态对象。在这种情况下，并发线程被阻塞，直到在第一个调用线程上执行的初始化完成。因此，不需要额外的用户定义同步机制:\n\n```cpp\nclass logger\n{\nprotected:\n   logger() {}\npublic:\n   static logger& instance()\n   {\n      static logger lg;\n      return lg;\n   }\n\n   logger(logger const &) = delete;\n   logger& operator=(logger const &) = delete;\n\n   void log(std::string_view message)\n   {\n      std::lock_guard<std::mutex> lock(mt);\n      std::cout << \"LOG: \" << message << std::endl;\n   }\n\nprivate:\n   std::mutex mt;\n};\n```\n\n前面的`logger`类可以用来从多个线程中写入控制台消息:\n\n```cpp\nint main()\n{\n   std::vector<std::thread> modules;\n\n   for(int id = 1; id <= 5; ++ id)\n   {\n      modules.emplace_back([id](){\n         std::random_device rd;\n         std::mt19937 mt(rd());\n         std::uniform_int_distribution<> ud(100, 1000);\n\n         logger::instance().log(\n            \"module \" + std::to_string(id) + \" started\");\n\n         std::this_thread::sleep_for(std::chrono::milliseconds(ud(mt)));\n\n         logger::instance().log(\n            \"module \" + std::to_string(id) + \" finished\");\n      });\n   }\n\n   for(auto & m : modules) m.join();\n}\n```\n\n# 66.客户服务系统\n\n为了根据需要实现客户服务办公室的模拟，我们可以使用几个助手类。`ticketing_machine`是一个类，它模拟了一个非常简单的机器，从一个初始的、用户指定的种子开始，发布递增的票务号码。`customer`是代表进入店铺并从售票机领取车票的顾客的类。`operator<`为了将顾客存储在优先队列中，按照他们的票号给出的顺序从优先队列中取票，这个类已经超负荷了。此外，前一个问题中的`logger`类用于向控制台打印消息:\n\n```cpp\nclass ticketing_machine\n{\npublic:\n   ticketing_machine(int const start) : \n      last_ticket(start),first_ticket(start) \n   {}\n\n   int next() { return last_ticket++ ; }\n   int last() const { return last_ticket - 1; }\n   void reset() { last_ticket = first_ticket; }\nprivate:\n   int first_ticket;\n   int last_ticket;\n};\n\nclass customer\n{\npublic:\n   customer(int const no) : number(no) {}\n\n   int ticket_number() const noexcept { return number; }\nprivate:\n   int number;\n   friend bool operator<(customer const & l, customer const & r);\n};\n\nbool operator<(customer const & l, customer const & r)\n{\n   return l.number > r.number;\n}\n```\n\n办公室的每张桌子都用不同的线建模。进入商店并在拿到票后排队的顾客使用一个单独的线程进行建模。在下面的模拟中，每 200-500 毫秒就有一个新客户进入商店，获得一张票据，并被放在优先队列中。商店线程的执行在 25 个顾客进入商店并被放入队列后结束。`std::condition_variable`用于线程之间的通信，以通知新客户已被放入队列或现有客户已从队列中移除(这发生在客户移动到开放的办公桌时)。代表办公桌的线程一直在运行，直到指示办公室已打开的标志被重置，但不是在队列中的所有客户都得到服务之前。在这个模拟中，每个客户在办公桌前花费 2，000 到 3，000 毫秒:\n\n```cpp\nint main()\n{\n   std::priority_queue<customer> customers;\n   bool office_open = true;\n   std::mutex mt;\n   std::condition_variable cv;\n\n   std::vector<std::thread> desks;\n   for (int i = 1; i <= 3; ++ i)\n   {\n      desks.emplace_back([i, &office_open, &mt, &cv, &customers]() {\n         std::random_device rd;\n         auto seed_data = std::array<int, std::mt19937::state_size> {};\n         std::generate(std::begin(seed_data), std::end(seed_data),\n                       std::ref(rd));\n         std::seed_seq seq(std::begin(seed_data), std::end(seed_data));\n         std::mt19937 eng(seq);\n         std::uniform_int_distribution<> ud(2000, 3000);\n\n         logger::instance().log(\"desk \" + std::to_string(i) + \" open\");\n\n         while (office_open || !customers.empty())\n         {\n            std::unique_lock<std::mutex> locker(mt);\n\n            cv.wait_for(locker, std::chrono::seconds(1),\n               [&customers]() {return !customers.empty(); });\n\n            if (!customers.empty())\n            {\n               auto const c = customers.top();\n               customers.pop();\n\n               logger::instance().log(\n                  \"[-] desk \" + std::to_string(i) + \" handling customer \"\n                  + std::to_string(c.ticket_number()));\n\n               logger::instance().log(\n                  \"[=] queue size: \" + std::to_string(customers.size()));\n\n               locker.unlock();\n               cv.notify_one();\n\n               std::this_thread::sleep_for(\n                  std::chrono::milliseconds(ud(eng)));\n\n               logger::instance().log(\n                  \"[ ] desk \" + std::to_string(i) + \" done with customer \"\n                  + std::to_string(c.ticket_number()));\n            }\n         }\n\n         logger::instance().log(\"desk \" + std::to_string(i) + \" closed\");\n      });\n   }\n\n   std::thread store([&office_open, &customers, &mt, &cv]() {\n      ticketing_machine tm(100);\n\n      std::random_device rd;\n      auto seed_data = std::array<int, std::mt19937::state_size> {};\n      std::generate(std::begin(seed_data), std::end(seed_data),\n                    std::ref(rd));\n      std::seed_seq seq(std::begin(seed_data), std::end(seed_data));\n      std::mt19937 eng(seq);\n      std::uniform_int_distribution<> ud(200, 500);\n\n      for (int i = 1; i <= 25; ++ i)\n      {\n         customer c(tm.next());\n         customers.push(c);\n\n         logger::instance().log(\"[+] new customer with ticket \" +\n            std::to_string(c.ticket_number()));\n         logger::instance().log(\"[=] queue size: \" +\n            std::to_string(customers.size()));\n\n         cv.notify_one();\n\n         std::this_thread::sleep_for(std::chrono::milliseconds(ud(eng)));\n      }\n\n      office_open = false;\n   });\n\n   store.join(); \n   for (auto & desk : desks) desk.join();\n}\n```\n\n下面是这个问题的一个执行输出片段:\n\n```cpp\nLOG: desk 1 open\nLOG: desk 2 open\nLOG: desk 3 open\nLOG: [+] new customer with ticket 100\nLOG: [-] desk 2 handling customer 100\nLOG: [=] queue size: 0\nLOG: [=] queue size: 0\nLOG: [+] new customer with ticket 101\nLOG: [=] queue size: 1\nLOG: [-] desk 3 handling customer 101\nLOG: [=] queue size: 0\nLOG: [+] new customer with ticket 102\nLOG: [=] queue size: 1\nLOG: [-] desk 1 handling customer 102\nLOG: [=] queue size: 0\nLOG: [+] new customer with ticket 103\nLOG: [=] queue size: 1\n...\nLOG: [+] new customer with ticket 112\nLOG: [=] queue size: 7\nLOG: [+] new customer with ticket 113\nLOG: [=] queue size: 8\nLOG: [ ] desk 2 done with customer 103\nLOG: [-] desk 2 handling customer 106\nLOG: [=] queue size: 7\n...\nLOG: [ ] desk 1 done with customer 120\nLOG: [-] desk 1 handling customer 123\nLOG: [=] queue size: 1\nLOG: [ ] desk 2 done with customer 121\nLOG: [-] desk 2 handling customer 124\nLOG: [=] queue size: 0\nLOG: [ ] desk 3 done with customer 122\nLOG: desk 3 closed\nLOG: [ ] desk 1 done with customer 123\nLOG: desk 1 closed\nLOG: [ ] desk 2 done with customer 124\nLOG: desk 2 closed\n```\n\n作为进一步的练习，您可以尝试更改顾客进入商店的时间间隔，在办公室关闭前允许多少顾客获得一张票，为他们服务需要多长时间，或者办公室打开了多少张桌子。"
  },
  {
    "path": "docs/mod-cpp-challenge/08.md",
    "content": "# 八、设计模式\n\n# 问题\n\n# 67.验证密码\n\n编写一个程序，根据预定义的规则验证密码强度，然后可以选择各种组合。至少，每个密码都必须满足最小长度要求。此外，还可以实施其他规则，例如至少存在一个符号、数字、大写和小写字母等。\n\n# 68.生成随机密码\n\n编写一个程序，可以根据一些预定义的规则生成随机密码。每个密码必须具有可配置的最小长度。此外，应该可以在生成规则中包括至少一个数字、符号、小写或大写字符等。这些附加规则必须是可配置和可组合的。\n\n# 69.生成社会保障号码\n\n编写一个程序，可以为两个国家生成社会保障号码，这两个国家的号码具有不同但相似的格式:\n\n*   在 Northeria，数字的格式为`SYYYYMMDDNNNNNC`，其中`S`是代表性别的数字，女性为 9，男性为 7，`YYYYMMDD`是出生日期，`NNNNN`是一个五位数的随机数，一天唯一(意味着同一个数字可以在两个不同的日期出现两次，但不是同一个日期)，`C`是一个选择的数字，因此后面描述的计算校验和是 11 的倍数。\n*   在南方，数字的格式为`SYYYYMMDDNNNNC`，其中`S`是代表性别的数字，1 代表女性，2 代表男性，`YYYYMMDD`是出生日期，`NNNN`是一个四位数的随机数，一天唯一，`C`是一个数字，因此如下所述计算的校验和是 10 的倍数。\n\n两种情况下的校验和都是所有数字的总和，每个数字乘以其权重(从最高有效数字到最低有效数字的位置)。例如，南方数字 12017120134895 的校验和计算如下:\n\n```cpp\ncrc = 14*1 + 13*2 + 12*0 + 11*1 + 10*7 + 9*1 + 8*2 + 7*0 + 6*1 + 5*3 \n           +  4*4 +  3*8 +  2*9 +  1*5\n    = 230 = 23 * 10\n```\n\n# 70.审批制度\n\n为公司的采购部门编写一个程序，允许员工批准新的采购(或费用)。但是，根据他们的职位，每个员工只能批准不超过预定义限额的费用。例如，正式员工最多可以批准 1000 个货币单位的费用，团队经理最多可以批准 10000 个，部门经理最多可以批准 100000 个。任何超过这个数额的费用都必须得到公司总裁的明确批准。\n\n# 71.可观测向量容器\n\n编写一个行为类似于向量的类模板，但是可以通知注册方内部状态的变化。该类必须至少提供以下操作:\n\n*   用于创建类的新实例的各种构造函数\n*   `operator=`给容器赋值\n*   `push_back()`在容器的末尾添加新元素\n*   `pop_back()`从容器中取出最后一个元素\n*   `clear()`从容器中取出所有元素\n*   `size()`返回容器中元素的数量\n*   `empty()`指示容器是空的还是有元素\n\n`operator=`、`push_back()`、`pop_back()`和`clear()`必须将状态变化通知其他人。通知应包括更改的类型，以及在这种情况下，已更改元素的索引(如添加或删除)。\n\n# 72.计算有折扣的订单价格\n\n零售商店出售各种商品，可以为选定的顾客、商品或每笔订单提供各种类型的折扣。可以获得以下类型的折扣:\n\n*   固定折扣，如 5%，与购买的商品或数量无关。\n*   当购买超过特定数量的商品时，每件商品的数量折扣，如 10%。\n*   商品总订单的价格折扣，即当顾客购买一定数量的商品，使总成本超过特定金额时，商品的折扣。例如，当一件商品的总成本超过 100 美元时，可以享受 15%的折扣。如果物品售价 5 美元，客户购买 30 台，总成本为 150 美元；因此，15%的折扣适用于该商品的订单。\n*   整个订单的价格折扣(无论订购了什么商品和数量)。\n\n编写一个程序，可以计算特定订单的最终价格。可以用不同的方法计算最终价格；例如，所有折扣都可以是累积的，或者另一方面，如果一件商品有折扣，则可能不考虑顾客折扣或总订单折扣。\n\n# 解决方法\n\n# 67.验证密码\n\n这里描述的问题是*装饰器*模式的典型案例。这种设计模式允许在不影响其他同类型对象的情况下向对象添加行为。这是通过将一个对象包装在另一个对象中来实现的。多个装饰器可以堆叠在一起，每次都添加新的功能。在我们的例子中，该功能将验证给定的密码是否满足特定的要求。\n\n下面的类图描述了验证密码的模式:\n\n![](img/a5538133-29a5-4fc2-9211-f899f1f4bae0.png)\n\n如图所示，该模式的实现如下:\n\n```cpp\n\nclass password_validator\n{\npublic:\n   virtual bool validate(std::string_view password) = 0;\n   virtual ~password_validator() {}\n};\n\nclass length_validator final : public password_validator\n{\npublic:\n   length_validator(unsigned int min_length): length(min_length)\n   {}\n\n   virtual bool validate(std::string_view password) override\n   {\n      return password.length() >= length;\n   }\n\nprivate:\n   unsigned int length;\n};\n\nclass password_validator_decorator : public password_validator\n{\npublic:\n   explicit password_validator_decorator(\n      std::unique_ptr<password_validator> validator):\n         inner(std::move(validator))\n   {\n   }\n\n   virtual bool validate(std::string_view password) override\n   {\n      return inner->validate(password);\n   }\n\nprivate:\n   std::unique_ptr<password_validator> inner;\n};\n\nclass digit_password_validator final : public password_validator_decorator\n{\npublic:\n   explicit digit_password_validator(\n      std::unique_ptr<password_validator> validator):\n         password_validator_decorator(std::move(validator))\n   {\n   }\n\n   virtual bool validate(std::string_view password) override\n   {\n      if(!password_validator_decorator::validate(password))\n         return false;\n\n      return password.find_first_of(\"0123456789\") != std::string::npos;\n   }\n};\n\nclass case_password_validator final : public password_validator_decorator\n{\npublic:\n   explicit case_password_validator(\n      std::unique_ptr<password_validator> validator):\n         password_validator_decorator(std::move(validator))\n   {\n   }\n\n   virtual bool validate(std::string_view password) override\n   {\n      if(!password_validator_decorator::validate(password))\n         return false;\n\n      bool haslower = false;\n      bool hasupper = false;\n\n      for(size_t i = 0; i < password.length() && !(hasupper && haslower); \n         ++ i)\n      {\n         if(islower(password[i])) haslower = true;\n         else if(isupper(password[i])) hasupper = true;\n      }\n\n      return haslower && hasupper;\n   }\n};\n\nclass symbol_password_validator final : public password_validator_decorator\n{\npublic:\n   explicit symbol_password_validator(\n      std::unique_ptr<password_validator> validator):\n         password_validator_decorator(std::move(validator))\n   {\n   }\n\n   virtual bool validate(std::string_view password) override\n   {\n      if(!password_validator_decorator::validate(password))\n         return false;\n\n      return password.find_first_of(\"!@#$%^&*(){}[]?<>\") != \n         std::string::npos;\n   }\n};\n```\n\n`password_validator`是基类，有一个名为`validate()`的虚拟方法，带有一个表示密码的字符串参数。`length_validator`就是从这个类派生出来的，实现了最短长度的强制密码要求。\n\n`password_validator_decorator`也来源于`password_validator`，包含一个内部的`password_validator`成分。它的`validate()`实现只是决定调用`inner->validate()`。其他类`digit_password_validator`、`symbol_password_validator`、`case_password_validator`都是从它派生出来的，实现了其他个人密码强度要求。\n\n以下是如何组成这些类来创建各种密码验证器的示例:\n\n```cpp\nint main()\n{\n   auto validator1 = std::make_unique<digit_password_validator>(\n      std::make_unique<length_validator>(8));\n\n   assert(validator1->validate(\"abc123!@#\"));\n   assert(!validator1->validate(\"abcde!@#\"));\n\n   auto validator2 = \n      std::make_unique<symbol_password_validator>(\n         std::make_unique<case_password_validator>(\n            std::make_unique<digit_password_validator>(\n               std::make_unique<length_validator>(8))));\n\n   assert(validator2->validate(\"Abc123!@#\"));\n   assert(!validator2->validate(\"Abc123567\"));\n}\n```\n\n# 68.生成随机密码\n\n这个问题可以通过使用*复合*图案或该图案的变体来解决。这种设计模式将对象组成树层次结构，并允许以处理相同类型的单个对象的相同方式来处理对象组(或树)。下面的类图显示了可用于生成密码的类的层次结构:\n\n![](img/d1c82b45-1102-4c75-aa44-575a8696909e.png)\n\n`password_generator`是基类，有几个虚方法:`generate()`返回一个新的随机字符串，`length()`指定它生成的字符串的长度，`allowed_chars()`返回一个包含它用于生成密码的所有字符的字符串，`add()`向复合生成器添加一个新的子组件。`basic_password_generator`是从这个基类派生出来的，定义了一个最小长度的生成器。`digit_generator`、`symbol_generator`、`upper_letter_generator`和`lower_letter_generator`源自`basic_password_generator`并覆盖`allowed_chars()`以定义用于生成随机文本的字符子集。\n\n`composite_password_generator`也是从`password_generator`派生出来的，它有一个`password_generator`对象的集合，用来组成一个随机的文本。这是在被覆盖的`generate()`方法中完成的，该方法将子组件生成的所有字符串连接起来，然后对它们进行随机混洗，生成代表密码的最终字符串:\n\n```cpp\nclass password_generator\n{\npublic:\n   virtual std::string generate() = 0;\n\n   virtual std::string allowed_chars() const = 0;\n   virtual size_t length() const = 0;\n\n   virtual void add(std::unique_ptr<password_generator> generator) = 0;\n\n   virtual ~password_generator(){}\n};\n\nclass basic_password_generator : public password_generator\n{\n   size_t len;\npublic:\n   explicit basic_password_generator(size_t const len) noexcept : len(len) \n   {}\n\n   virtual std::string generate() override\n   { throw std::runtime_error(\"not implemented\"); }\n\n   virtual void add(std::unique_ptr<password_generator>) override\n   { throw std::runtime_error(\"not implemented\"); }\n\n   virtual size_t length() const override final\n   {return len;}\n};\n\nclass digit_generator : public basic_password_generator\n{\npublic:\n   explicit digit_generator(size_t const len) noexcept\n   : basic_password_generator(len) {}\n\n   virtual std::string allowed_chars() const override\n   {return \"0123456789\";}\n};\n\nclass symbol_generator : public basic_password_generator\n{\npublic:\n   explicit symbol_generator(size_t const len) noexcept\n   : basic_password_generator(len) {}\n\n   virtual std::string allowed_chars() const override\n   {return \"!@#$%^&*(){}[]?<>\";}\n};\n\nclass upper_letter_generator : public basic_password_generator\n{\npublic:\n   explicit upper_letter_generator(size_t const len) noexcept\n   : basic_password_generator(len) {}\n\n   virtual std::string allowed_chars() const override\n   {return \"ABCDEFGHIJKLMNOPQRSTUVXYWZ\";}\n};\n\nclass lower_letter_generator : public basic_password_generator\n{\npublic:\n   explicit lower_letter_generator(size_t const len) noexcept\n   : basic_password_generator(len) {}\n\n   virtual std::string allowed_chars() const override\n   {return \"abcdefghijklmnopqrstuvxywz\";}\n};\n\nclass composite_password_generator : public password_generator\n{\n   virtual std::string allowed_chars() const override\n   { throw std::runtime_error(\"not implemented\"); };\n   virtual size_t length() const override\n   { throw std::runtime_error(\"not implemented\"); };\npublic:\n   composite_password_generator()\n   {\n      auto seed_data = std::array<int, std::mt19937::state_size> {};\n      std::generate(std::begin(seed_data), std::end(seed_data), \n                    std::ref(rd));\n      std::seed_seq seq(std::begin(seed_data), std::end(seed_data));\n      eng.seed(seq);\n   }\n\n   virtual std::string generate() override\n   {\n      std::string password;\n      for(auto & generator : generators)\n      {\n         std::string chars = generator->allowed_chars();\n         std::uniform_int_distribution<> ud(\n            0, static_cast<int>(chars.length() - 1));\n\n         for(size_t i = 0; i < generator->length(); ++ i)\n            password += chars[ud(eng)];\n      }\n\n      std::shuffle(std::begin(password), std::end(password), eng);\n\n      return password;\n   }\n\n   virtual void add(std::unique_ptr<password_generator> generator) override\n   {\n      generators.push_back(std::move(generator));\n   }\n\nprivate:\n   std::random_device rd;\n   std::mt19937 eng;\n   std::vector<std::unique_ptr<password_generator>> generators;\n};\n```\n\n上述代码可用于以下列方式生成密码:\n\n```cpp\nint main()\n{\n   composite_password_generator generator;\n   generator.add(std::make_unique<symbol_generator>(2));\n   generator.add(std::make_unique<digit_generator>(2));\n   generator.add(std::make_unique<upper_letter_generator>(2));\n   generator.add(std::make_unique<lower_letter_generator>(4));\n\n   auto password = generator.generate();\n}\n```\n\n您可以使用我们为前面的问题编写的密码验证器来确保以这种方式生成的密码确实符合预期的要求。\n\n# 69.生成社会保障号码\n\n这两个国家的格式非常相似，尽管几个细节不同:\n\n*   性别的数字值\n*   随机部分的长度，也就是整个数字的长度\n*   校验和必须是的倍数\n\n这个问题可以使用*模板方法*设计模式来解决，该模式定义了算法的框架，并允许子类重新定义特定的步骤:\n\n![](img/2ba24167-f7a0-4d1b-9065-75abe6616e95.png)\n\n`social_number_generator`是一个基类，它有一个名为`generate()`的公共方法，为指定的性别和出生日期生成一个新的社会保障号。这个方法在内部调用几个受保护的虚拟方法，`sex_digit()`、`next_random()`和`modulo_value()`。这些虚拟方法在两个派生类`northeria_social_number_generator`和`southeria_social_number_generator`中被重写。此外，工厂类保存这些社交号码生成器的实例，并使它们对呼叫客户端可用:\n\n```cpp\nenum class sex_type {female, male};\n\nclass social_number_generator\n{\nprotected:\n   virtual int sex_digit(sex_type const sex) const noexcept = 0;\n   virtual int next_random(unsigned const year, unsigned const month, \n                           unsigned const day) = 0;\n   virtual int modulo_value() const noexcept = 0;\n\n   social_number_generator(int const min, int const max):ud(min, max)\n   {\n      std::random_device rd;\n      auto seed_data = std::array<int, std::mt19937::state_size> {};\n      std::generate(std::begin(seed_data), std::end(seed_data), \n                    std::ref(rd));\n      std::seed_seq seq(std::begin(seed_data), std::end(seed_data));\n      eng.seed(seq);\n   }\n\npublic:\n   std::string generate(\n      sex_type const sex,\n      unsigned const year, unsigned const month, unsigned const day)\n   {\n      std::stringstream snumber;\n\n      snumber << sex_digit(sex);\n\n      snumber << year << month << day;\n\n      snumber << next_random(year, month, day);\n\n      auto number = snumber.str();\n\n      auto index = number.length();\n      auto sum = std::accumulate(std::begin(number), std::end(number), 0,\n         [&index](int const s, char const c) {\n            return s + index-- * (c-'0');});\n\n      auto rest = sum % modulo_value();\n      snumber << modulo_value() - rest;\n\n      return snumber.str();\n   }\n\n   virtual ~social_number_generator() {}\n\nprotected:\n   std::map<unsigned, int> cache;\n   std::mt19937 eng;\n   std::uniform_int_distribution<> ud;\n};\n\nclass southeria_social_number_generator final : \n   public social_number_generator\n{\npublic:\n   southeria_social_number_generator():\n      social_number_generator(1000, 9999)\n   {\n   }\n\nprotected:\n   virtual int sex_digit(sex_type const sex) const noexcept override\n   {\n      if(sex == sex_type::female) return 1;\n      else return 2;\n   }\n\n   virtual int next_random(unsigned const year, unsigned const month, \n                           unsigned const day) override\n   {\n      auto key = year * 10000 + month * 100 + day;\n      while(true)\n      {\n         auto number = ud(eng);\n         auto pos = cache.find(number);\n         if(pos == std::end(cache))\n         {\n            cache[key] = number;\n            return number;\n         }\n      }\n   }\n\n   virtual int modulo_value() const noexcept override\n   {\n      return 11;\n   }\n};\n\nclass northeria_social_number_generator final : \n   public social_number_generator\n{\npublic:\n   northeria_social_number_generator():\n      social_number_generator(10000, 99999)\n   {\n   }\n\nprotected:\n   virtual int sex_digit(sex_type const sex) const noexcept override\n   {\n      if(sex == sex_type::female) return 9;\n      else return 7;\n   }\n\n   virtual int next_random(unsigned const year, unsigned const month, \n                           unsigned const day) override\n   {\n      auto key = year * 10000 + month * 100 + day;\n      while(true)\n      {\n         auto number = ud(eng);\n         auto pos = cache.find(number);\n         if(pos == std::end(cache))\n         {\n            cache[key] = number;\n            return number;\n         }\n      }\n   }\n\n   virtual int modulo_value() const noexcept override\n   {\n      return 11;\n   }\n};\n\nclass social_number_generator_factory\n{\npublic:\n   social_number_generator_factory()\n   {\n      generators[\"northeria\"] = \n      std::make_unique<northeria_social_number_generator>();\n      generators[\"southeria\"] = \n         std::make_unique<southeria_social_number_generator>();\n   }\n\n   social_number_generator* get_generator(std::string_view country) const\n   {\n      auto it = generators.find(country.data());\n      if(it != std::end(generators))\n      return it->second.get();\n\n      throw std::runtime_error(\"invalid country\");\n   }\n\nprivate:\n   std::map<std::string, \n   std::unique_ptr<social_number_generator>> generators;\n};\n```\n\n使用此代码，可以按如下方式生成社会保险号:\n\n```cpp\nint main()\n{\n   social_number_generator_factory factory;\n\n   auto sn1 = factory.get_generator(\"northeria\")->generate(\n                 sex_type::female, 2017, 12, 25);\n   auto sn2 = factory.get_generator(\"northeria\")->generate(\n                 sex_type::female, 2017, 12, 25);\n   auto sn3 = factory.get_generator(\"northeria\")->generate(\n                 sex_type::male, 2017, 12, 25);\n\n   auto sss1 = factory.get_generator(\"southeria\")->generate(\n                 sex_type::female, 2017, 12, 25);\n   auto ss2 = factory.get_generator(\"southeria\")->generate(\n                 sex_type::female, 2017, 12, 25);\n   auto ss3 = factory.get_generator(\"southeria\")->generate(\n                 sex_type::male, 2017, 12, 25);\n}\n```\n\n# 70.审批制度\n\n所描述的问题可以用一系列`if … else if … else … endif`语句来表达。这个习语的面向对象版本是*责任链*设计模式。这个模式定义了一个接收者对象链，这些对象负责处理一个请求，或者将它传递给链中的下一个接收者(如果存在的话)。下面的类图显示了这个问题的模式的可能实现:\n\n![](img/676456df-25ff-47e3-8206-7ec6a5ea2f68.png)\n\n`employee`是代表公司某个员工的类。员工可能有一个直接经理，该经理被设置为调用`set_direct_manager()`方法。每个员工都有一个定义其职责和权限的姓名和角色。`role`是一个可能角色的抽象基类，它有一个纯虚拟方法`approval_limit()`，派生的类如`employee_role`、`team_manager_role`、`department_manager_role`和`president_role`会覆盖该方法，以指示员工可以批准费用的上限。`employee`类中的`approve()`方法用于让员工批准费用。如果员工的角色允许他们批准费用，他们就会这样做；否则，请求将被传递给他们的直接经理(如果有定义的话):\n\n```cpp\nclass role\n{\npublic:\n   virtual double approval_limit() const noexcept = 0;\n   virtual ~role() {}\n};\n\nclass employee_role : public role\n{\npublic:\n   virtual double approval_limit() const noexcept override\n   {\n      return 1000;\n   }\n};\n\nclass team_manager_role : public role\n{\npublic:\n   virtual double approval_limit() const noexcept override\n   {\n      return 10000;\n   }\n};\n\nclass department_manager_role : public role\n{\npublic:\n   virtual double approval_limit() const noexcept override\n   {\n      return 100000;\n   }\n};\n\n```\n\n```cpp\nclass president_role : public role\n{\npublic:\n   virtual double approval_limit() const noexcept override\n   {\n      return std::numeric_limits<double>::max();\n   }\n};\n\nstruct expense\n{\n   double amount;\n   std::string description;\n\n   expense(double const amount, std::string_view desc):\n      amount(amount), description(desc)\n   {\n   }\n};\n\nclass employee\n{\npublic:\n   explicit employee(std::string_view name, std::unique_ptr<role> ownrole)\n      : name(name), own_role(std::move(ownrole))\n   {\n   }\n\n   void set_direct_manager(std::shared_ptr<employee> manager)\n   {\n      direct_manager = manager;\n   }\n\n   void approve(expense const & e)\n   {\n      if(e.amount <= own_role->approval_limit())\n         std::cout << name << \" approved expense '\" << e.description \n                   << \"', cost=\" << e.amount << std::endl;\n      else if(direct_manager != nullptr)\n         direct_manager->approve(e);\n }\n\nprivate:\n   std::string               name;\n   std::unique_ptr<role>     own_role;\n   std::shared_ptr<employee> direct_manager;\n};\n```\n\n以下示例显示了如何使用此代码来批准费用:\n\n```cpp\nint main()\n{\n   auto john = std::make_shared<employee>(\"john smith\", \n                    std::make_unique<employee_role>());\n\n   auto robert = std::make_shared<employee>(\"robert booth\", \n                      std::make_unique<team_manager_role>());\n\n   auto david = std::make_shared<employee>(\"david jones\", \n                     std::make_unique<department_manager_role>());\n\n   auto cecil = std::make_shared<employee>(\"cecil williamson\", \n                     std::make_unique<president_role>());\n\n   john->set_direct_manager(robert);\n   robert->set_direct_manager(david);\n   david->set_direct_manager(cecil);\n\n   john->approve(expense{500, \"magazins\"});\n   john->approve(expense{5000, \"hotel accomodation\"});\n   john->approve(expense{50000, \"conference costs\"});\n   john->approve(expense{200000, \"new lorry\"});\n}\n```\n\n# 71.可观测向量容器\n\n这个问题中描述的可观测向量是设计模式中一个被称为观察者的主题的典型例子。这个模式描述了一个名为**主题**的对象，它维护一个名为**观察者**的依赖对象列表，并通过调用它们的一个方法通知它们任何状态变化。这里显示的类图描述了所提出问题的一种可能的模式实现:\n\n![](img/c8b339bd-6ace-409b-aeb9-b95cb3999171.png)\n\n`observable_vector`是包装`std::vector`并公开所需操作的类。它还维护一个指向`collection_observer`对象的指针列表。这是一个基类，用于希望被告知`observable_vector`中任何状态变化的对象。它有一个名为`collection_changed()`的虚拟方法，带有一个类型为`collection_changed_notification`的参数，其中包含关于变更的信息。当`observable_vector`的内部状态发生任何变化时，它在所有注册的观察器上调用该方法。观察者可以通过`add_observer()`方法添加到向量中，或者通过调用`remove_observer()`方法从向量中移除:\n\n```cpp\nenum class collection_action\n{\n   add,\n   remove,\n   clear,\n   assign\n};\n\nstd::string to_string(collection_action const action)\n{\n   switch(action)\n   {\n      case collection_action::add:    return \"add\";\n      case collection_action::remove: return \"remove\";\n      case collection_action::clear:  return \"clear\";\n      case collection_action::assign: return \"assign\";\n   }\n}\n\nstruct collection_change_notification\n{\n   collection_action action;\n   std::vector<size_t> item_indexes;\n};\n\nclass collection_observer\n{\npublic:\n   virtual void collection_changed(\n      collection_change_notification notification) = 0;\n   virtual ~collection_observer() {}\n};\n\ntemplate <typename T, class Allocator = std::allocator<T>>\nclass observable_vector final\n{\n   typedef typename std::vector<T, Allocator>::size_type size_type;\npublic:\n   observable_vector() noexcept(noexcept(Allocator()))\n      : observable_vector( Allocator() ) {}\n   explicit observable_vector( const Allocator& alloc ) noexcept\n      : data(alloc){}\n   observable_vector( size_type count, const T& value, \n                     const Allocator& alloc = Allocator())\n      : data(count, value, alloc){}\n   explicit observable_vector( size_type count, \n                               const Allocator& alloc = Allocator() )\n      :data(count, alloc){}\n   observable_vector(observable_vector&& other) noexcept\n      :data(other.data){}\n   observable_vector(observable_vector&& other, \n                     const Allocator& alloc)\n      :data(other.data, alloc){}\n   observable_vector(std::initializer_list<T> init,\n      const Allocator& alloc = Allocator())\n      :data(init, alloc){}\n   template<class InputIt>\n   observable_vector(InputIt first, InputIt last, const \n                     Allocator& alloc = Allocator())\n      :data(first, last, alloc){}\n\n   observable_vector& operator=(observable_vector const & other)\n   {\n      if(this != &other)\n      {\n         data = other.data;\n\n         for(auto o : observers)\n         {\n            if(o != nullptr)\n            {\n               o->collection_changed({\n                  collection_action::assign,\n                  std::vector<size_t> {}\n               });\n            }\n         }\n      }\n\n      return *this;\n   }\n\n   observable_vector& operator=(observable_vector&& other)\n   {\n      if(this != &other)\n      {\n         data = std::move(other.data);\n\n         for(auto o : observers)\n         {\n            if(o != nullptr)\n            {\n               o->collection_changed({\n                  collection_action::assign,\n                  std::vector<size_t> {}\n               });\n            }\n         }\n      }\n\n      return *this;\n   }\n\n   void push_back(T&& value)\n   {\n      data.push_back(value);\n\n      for(auto o : observers)\n      {\n         if(o != nullptr)\n         {\n            o->collection_changed({\n               collection_action::add,\n               std::vector<size_t> {data.size()-1}\n            });\n         }\n      }\n   }\n\n   void pop_back()\n   {\n      data.pop_back();\n\n      for(auto o : observers)\n      {\n         if(o != nullptr)\n         {\n            o->collection_changed({\n               collection_action::remove,\n               std::vector<size_t> {data.size()+1}\n            });\n         }\n      }\n   }\n\n   void clear() noexcept\n   {\n      data.clear();\n\n      for(auto o : observers)\n      {\n         if(o != nullptr)\n         {\n            o->collection_changed({\n               collection_action::clear,\n               std::vector<size_t> {}\n            });\n         }\n      }\n   }\n\n   size_type size() const noexcept\n   {\n      return data.size();\n   }\n\n   [[nodiscard]] bool empty() const noexcept\n   {\n      return data.empty();\n   }\n\n   void add_observer(collection_observer * const o)\n   {\n      observers.push_back(o);\n   }\n\n   void remove_observer(collection_observer const * const o)\n   {\n      observers.erase(std::remove(std::begin(observers), \n                                  std::end(observers), o),\n                      std::end(observers));\n   }\n\nprivate:\n   std::vector<T, Allocator> data;\n   std::vector<collection_observer*> observers;\n};\n\nclass observer : public collection_observer\n{\npublic:\n   virtual void collection_changed(\n      collection_change_notification notification) override\n   {\n      std::cout << \"action: \" << to_string(notification.action);\n      if(!notification.item_indexes.empty())\n      {\n         std::cout << \", indexes: \";\n         for(auto i : notification.item_indexes)\n            std::cout << i << ' ';\n      }\n      std::cout << std::endl;\n   }\n};\n```\n\n以下是使用`observable_vector`类并获取其内部状态变化通知的示例:\n\n```cpp\nint main()\n{\n   observable_vector<int> v;\n   observer o;\n\n   v.add_observer(&o);\n\n   v.push_back(1);\n   v.push_back(2);\n   v.pop_back();\n   v.clear();\n\n   v.remove_observer(&o);\n\n   v.push_back(3);\n   v.push_back(4);\n\n   v.add_observer(&o);\n\n   observable_vector<int> v2 {1,2,3};\n   v = v2;\n   v = observable_vector<int> {7,8,9};\n}\n```\n\nYou can take it as a further exercise to add more functionality to `observable_vector`, such as providing access to the elements using iterators.\n\n# 72.计算有折扣的订单价格\n\n这里提出的问题可以用*战略*模式来解决。这种设计模式定义了一个算法家族，并使它们在家族中可以互换。在这个特殊的问题中，折扣和最终订单价格计算器都可以基于策略模式来实现。下图描述了折扣类型的层次结构及其在其他类别`customer`、`article`、`order_line`和`order`中的可互换使用:\n\n![](img/ef515e1d-dd0e-4413-9f46-13516bc7a64b.png)\n\n折扣类型的实现如下所示:\n\n```cpp\nstruct discount_type\n{\n   virtual double discount_percent(\n      double const price, double const quantity) const noexcept = 0;\n   virtual ~discount_type() {}\n};\n\nstruct fixed_discount final : public discount_type\n{\n   explicit fixed_discount(double const discount) noexcept \n      : discount(discount) {}\n   virtual double discount_percent(\n      double const, double const) const noexcept \n   {return discount;}\n\nprivate:\n   double discount;\n};\n\nstruct volume_discount final : public discount_type\n{\n   explicit volume_discount(double const quantity, \n                            double const discount) noexcept \n     : discount(discount), min_quantity(quantity) {}\n   virtual double discount_percent(\n      double const, double const quantity) const noexcept \n   {return quantity >= min_quantity ? discount : 0;}\n\nprivate:\n   double discount;\n   double min_quantity;\n};\n\nstruct price_discount : public discount_type\n{\n   explicit price_discount(double const price, \n                           double const discount) noexcept \n      : discount(discount), min_total_price(price) {}\n   virtual double discount_percent(\n      double const price, double const quantity) const noexcept \n   {return price*quantity >= min_total_price ? discount : 0;}\n\nprivate:\n   double discount;\n   double min_total_price;\n};\n\nstruct amount_discount : public discount_type\n{\n   explicit amount_discount(double const price, \n                            double const discount) noexcept \n      : discount(discount), min_total_price(price) {}\n   virtual double discount_percent(\n      double const price, double const) const noexcept \n   {return price >= min_total_price ? discount : 0;}\n\nprivate:\n   double discount;\n   double min_total_price;\n};\n```\n\n为客户、商品和订单建模的类只有最低限度的结构，以保持解决方案的简单性。它们显示在这里:\n\n```cpp\nstruct customer\n{\n   std::string    name;\n   discount_type* discount;\n};\n\nenum class article_unit\n{\n   piece, kg, meter, sqmeter, cmeter, liter\n};\n\nstruct article\n{\n   int            id;\n   std::string    name;\n   double         price;\n   article_unit   unit;\n   discount_type* discount;\n};\n\nstruct order_line\n{\n   article        product;\n   int            quantity;\n   discount_type* discount;\n};\n\nstruct order\n{\n   int                     id;\n   customer*               buyer;\n   std::vector<order_line> lines;\n   discount_type*          discount;\n};\n```\n\n为了计算订单的最终价格，我们可以使用各种类型的计算器。这是策略模式的又一个实例:\n\n![](img/717ac2bc-3067-4808-962d-f4af920797a9.png)\n\n`price_calculator`是一个抽象基类，有一个纯虚方法，`calculate_price()`。从`price_calculator`派生的类，例如`cumulative_price_calculator`，通过覆盖`calculate_price()`方法来提供实际的算法实现。为简单起见，在该实现中，仅提供了一种具体的价格计算策略。作为进一步的练习，您可以实现其他:\n\n```cpp\nstruct price_calculator\n{\n   virtual double calculate_price(order const & o) = 0;\n};\n\nstruct cumulative_price_calculator : public price_calculator\n{\n   virtual double calculate_price(order const & o) override\n   {\n      double price = 0;\n\n      for(auto ol : o.lines)\n      {\n         double line_price = ol.product.price * ol.quantity;\n\n         if(ol.product.discount != nullptr)\n            line_price *= (1.0 - ol.product.discount->discount_percent(\n               ol.product.price, ol.quantity));\n\n         if(ol.discount != nullptr)\n            line_price *= (1.0 - ol.discount->discount_percent(\n               ol.product.price, ol.quantity));\n\n         if(o.buyer != nullptr && o.buyer->discount != nullptr)\n            line_price *= (1.0 - o.buyer->discount->discount_percent(\n               ol.product.price, ol.quantity));\n\n         price += line_price;\n      }\n\n      if(o.discount != nullptr)\n         price *= (1.0 - o.discount->discount_percent(price, 0));\n\n      return price;\n   }\n};\n```\n\n以下是如何使用`cumulative_price_calculator`计算最终订单价格的示例:\n\n```cpp\ninline bool are_equal(double const d1, double const d2, \n                      double const diff = 0.001)\n{\n   return std::abs(d1 - d2) <= diff;\n}\n\nint()\n{\n   fixed_discount  d1(0.1);\n   volume_discount d2(10, 0.15);\n   price_discount  d3(100, 0.05);\n   amount_discount d4(100, 0.05);\n\n   customer c1 {\"default\", nullptr};\n   customer c2 {\"john\", &d1};\n   customer c3 {\"joane\", &d3};\n\n   article a1 {1, \"pen\", 5, article_unit::piece, nullptr};\n   article a2 {2, \"expensive pen\", 15, article_unit::piece, &d1};\n   article a3 {3, \"scissors\", 10, article_unit::piece, &d2};\n\n   cumulative_price_calculator calc;\n\n   order o1 {101, &c1, {{a1, 1, nullptr}}, nullptr};\n   assert(are_equal(calc.calculate_price(o1), 5));\n\n```\n\n```cpp\n   order o3 {103, &c1, {{a2, 1, nullptr}}, nullptr};\n   assert(are_equal(calc.calculate_price(o3), 13.5));\n\n   order o6 {106, &c1, {{a3, 15, nullptr}}, nullptr};\n   assert(are_equal(calc.calculate_price(o6), 127.5));\n\n   order o9 {109, &c3, {{a2, 20, &d1}}, &d4};\n   assert(are_equal(calc.calculate_price(o9), 219.3075));\n}\n```"
  },
  {
    "path": "docs/mod-cpp-challenge/09.md",
    "content": "# 九、数据序列化\n\n# 问题\n\n# 73.将数据序列化为 XML 或将数据反序列化为 XML\n\n编写一个可以将电影列表序列化为 XML 文件的程序，并用电影列表反序列化 XML 文件。每部电影都有一个数字标识符、标题、上映年份、分钟长度、导演名单、编剧名单以及演员姓名和角色名称的演员名单。这样的 XML 可能如下所示:\n\n```cpp\n<?xml version=\"1.0\"?>\n<movies>\n  <movie id=\"9871\" title=\"Forrest Gump\" year=\"1994\" length=\"202\">\n    <cast>\n      <role star=\"Tom Hanks\" name=\"Forrest Gump\" />\n      <role star=\"Sally Field\" name=\"Mrs. Gump\" />\n      <role star=\"Robin Wright\" name=\"Jenny Curran\" />\n      <role star=\"Mykelti Williamson\" name=\"Bubba Blue\" />\n    </cast>\n    <directors>\n      <director name=\"Robert Zemeckis\" />\n    </directors>\n    <writers>\n      <writer name=\"Winston Groom\" />\n      <writer name=\"Eric Roth\" />\n    </writers>\n  </movie>\n  <!-- more movie elements -->\n</movies>\n```\n\n# 74.使用 XPath 从 XML 中选择数据\n\n考虑一个包含电影列表的 XML 文件，如前一个问题所述。编写一个可以选择和打印以下内容的程序:\n\n*   一年后发行的所有电影的名称\n*   文件中每部电影的演员名单中最后一个演员的姓名\n\n# 75.将数据序列化为 JSON\n\n编写一个程序，可以将前面问题中定义的电影列表序列化到一个 JSON 文件中。每部电影都有一个数字标识符、标题、上映年份、分钟长度、导演名单、编剧名单以及演员姓名和角色名称的演员名单。以下是预期 JSON 格式的示例:\n\n```cpp\n{\n  \"movies\": [{\n    \"id\": 9871,\n    \"title\": \"Forrest Gump\",\n    \"year\": 1994,\n    \"length\": 202,\n    \"cast\": [{\n        \"star\": \"Tom Hanks\",\n        \"name\": \"Forrest Gump\"\n      },\n      {\n        \"star\": \"Sally Field\",\n        \"name\": \"Mrs. Gump\"\n      },\n      {\n        \"star\": \"Robin Wright\",\n        \"name\": \"Jenny Curran\"\n      },\n      {\n        \"star\": \"Mykelti Williamson\",\n        \"name\": \"Bubba Blue\"\n      }\n    ],\n    \"directors\": [\"Robert Zemeckis\"],\n    \"writers\": [\"Winston Groom\", \"Eric Roth\"]\n  }]\n}\n```\n\n# 76.从 JSON 反序列化数据\n\n考虑一个包含电影列表的 JSON 文件，如前一个问题所示。编写一个可以反序列化其内容的程序。\n\n# 77.将电影列表打印到 PDF\n\n编写一个程序，可以将电影列表以表格形式打印到 PDF 文件中，并满足以下要求:\n\n*   列表必须有标题，内容为*电影列表*。这必须只出现在文档的第一页。\n*   对于每部电影，它应该显示标题、发行年份和长度。\n*   标题后面是括号中的发布年份，必须左对齐。\n*   以小时和分钟为单位的长度(例如，2:12)必须右对齐。\n*   每一页的电影列表上面和下面必须有一行。\n\n以下是这样一个 PDF 输出的示例:\n\n![](img/6def28ce-9395-44d5-b5b3-c7a46fe3ad37.png)\n\n# 78.从图像集合中创建 PDF\n\n编写一个程序，可以创建一个包含来自用户指定目录的图像的 PDF 文档。图像必须一个接一个地显示。如果图像不适合页面的其余部分，则必须将其放在下一页。\n\n下面是一个这样的 PDF 文件的例子，它是由阿尔伯特·爱因斯坦的几张图片创建的(这些图片是随书附上的源代码):\n\n![](img/9f6d3dea-1a46-46b2-b101-17bf5e55aa1c.jpg)\n\n# 解决方法\n\n# 73.将数据序列化为 XML 或将数据反序列化为 XML\n\nC++ 标准库不支持 XML，但是有多个开源的、跨平台的库可以使用。一些库是轻量级的，支持一组基本的 XML 特性，而另一些库更复杂，功能更丰富。由你来决定哪一个最适合某个特定的项目。\n\n您可能要考虑的库列表应该包括 *Xerces-C++* 、 *libxml++* 、 *tinyxml* 或 *tinyxml2* 、 *pugixml* 、 *gSOAP* 和 *RapidXml* 。为了解决这个特殊的任务，我将选择 *pugixml* 。这是一个跨平台的轻量级库，具有快速但不可验证的 XML 解析器。它有一个类似 DOM 的接口，具有丰富的遍历/修改功能，支持 Unicode 和 XPath 1.0。关于库的局限性，应该提到的是它缺乏对模式验证的支持。pugixml 库可在[https://pugixml.org/](https://pugixml.org/)获得。\n\n为了表现电影，如问题中所述，我们将使用以下结构:\n\n```cpp\nstruct casting_role\n{\n   std::string actor;\n   std::string role;\n};\n\nstruct movie\n{\n   unsigned int              id;\n   std::string               title;\n   unsigned int              year;\n   unsigned int              length;\n   std::vector<casting_role> cast;\n   std::vector<std::string>  directors;\n   std::vector<std::string>  writers;\n};\n\nusing movie_list = std::vector<movie>;\n```\n\n要创建一个 XML 文档，您必须使用`pugi::xml_document`类。构建 DOM 树后，可以通过调用`save_file()`将其保存到文件中。可以通过调用`append_child()`添加节点，用`append_attribute()`添加属性。以下方法以请求的格式序列化电影列表:\n\n```cpp\nvoid serialize(movie_list const & movies, std::string_view filepath)\n{\n   pugi::xml_document doc;\n   auto root = doc.append_child(\"movies\");\n\n   for (auto const & m : movies)\n   {\n      auto movie_node = root.append_child(\"movie\");\n\n      movie_node.append_attribute(\"id\").set_value(m.id);\n      movie_node.append_attribute(\"title\").set_value(m.title.c_str());\n      movie_node.append_attribute(\"year\").set_value(m.year);\n      movie_node.append_attribute(\"length\").set_value(m.length);\n\n      auto cast_node = movie_node.append_child(\"cast\");\n      for (auto const & c : m.cast)\n      { \n         auto node = cast_node.append_child(\"role\");\n         node.append_attribute(\"star\").set_value(c.actor.c_str());\n         node.append_attribute(\"name\").set_value(c.role.c_str());\n      }\n\n      auto directors_node = movie_node.append_child(\"directors\");\n      for (auto const & director : m.directors)\n      {\n         directors_node.append_child(\"director\")\n                       .append_attribute(\"name\")\n                       .set_value(director.c_str());\n      }\n\n      auto writers_node = movie_node.append_child(\"writers\");\n      for (auto const & writer : m.writers)\n      {\n         writers_node.append_child(\"writer\")\n                     .append_attribute(\"name\")\n                     .set_value(writer.c_str());\n      }\n   }\n\n   doc.save_file(filepath.data());\n}\n```\n\n对于相反的操作，您可以通过调用其`load_file()`方法将 XML 文件的内容加载到`pugi::xml_document`中。可以通过调用`child()`、`next_sibling()`等方法访问节点，通过调用`attribute()`访问属性。`deserialize()`方法，如下所示，读取 DOM 树并构建电影列表:\n\n```cpp\nmovie_list deserialize(std::string_view filepath)\n{\n   pugi::xml_document doc;\n   movie_list movies;\n\n   auto result = doc.load_file(filepath.data());\n   if (result)\n   {\n      auto root = doc.child(\"movies\");\n      for (auto movie_node = root.child(\"movie\");\n           movie_node;\n           movie_node = movie_node.next_sibling(\"movie\"))\n      {\n         movie m;\n         m.id = movie_node.attribute(\"id\").as_uint();\n         m.title = movie_node.attribute(\"title\").as_string();\n         m.year = movie_node.attribute(\"year\").as_uint();\n         m.length = movie_node.attribute(\"length\").as_uint();\n\n         for (auto role_node :       \n              movie_node.child(\"cast\").children(\"role\"))\n         {\n            m.cast.push_back(casting_role{\n               role_node.attribute(\"star\").as_string(),\n               role_node.attribute(\"name\").as_string() });\n         }\n\n         for (auto director_node : \n              movie_node.child(\"directors\").children(\"director\"))\n         {\n            m.directors.push_back(\n               director_node.attribute(\"name\").as_string());\n         }\n\n         for (auto writer_node : \n              movie_node.child(\"writers\").children(\"writer\"))\n         {\n            m.writers.push_back(\n               writer_node.attribute(\"name\").as_string());\n         }\n\n         movies.push_back(m);\n      }\n   }\n\n   return movies;\n}\n```\n\n下面的列表显示了如何使用这些函数的示例:\n\n```cpp\nint main()\n{\n   movie_list movies\n   {\n      {\n         11001, \"The Matrix\",1999, 196,\n         { {\"Keanu Reeves\", \"Neo\"},\n           {\"Laurence Fishburne\", \"Morpheus\"},\n           {\"Carrie-Anne Moss\", \"Trinity\"}, \n           {\"Hugo Weaving\", \"Agent Smith\"} },\n         {\"Lana Wachowski\", \"Lilly Wachowski\"},\n         {\"Lana Wachowski\", \"Lilly Wachowski\"},\n      },\n      {\n         9871, \"Forrest Gump\", 1994, 202,\n         { {\"Tom Hanks\", \"Forrest Gump\"},\n           {\"Sally Field\", \"Mrs. Gump\"},\n           {\"Robin Wright\",\"Jenny Curran\"},\n           {\"Mykelti Williamson\",\"Bubba Blue\"} },\n         {\"Robert Zemeckis\"},\n         {\"Winston Groom\", \"Eric Roth\"},\n      }\n   };\n\n   serialize(movies, \"movies.xml\");\n   auto result = deserialize(\"movies.xml\");\n\n   assert(result.size() == 2);\n   assert(result[0].title == \"The Matrix\");\n   assert(result[1].title == \"Forrest Gump\");\n}\n```\n\n# 74.使用 XPath 从 XML 中选择数据\n\n可以使用 *XPath* 来导航一个 XML 文件的元素和属性。XPath 为此使用 XPath 表达式，为此有一长串内置函数。 *pugixml* 支持 XPath 表达式，您可以使用`xml_document`类中的`select_nodes()`方法来实现这一目的。请注意，如果在选择 XPath 的过程中出现错误，则会抛出`xpath_exception`。以下 XPath 表达式可用于根据问题要求选择节点:\n\n*   对于给定年份(在本例中，该年份是 1995 年)之后发行的所有电影:`/movies/movie[@year>1995]`\n*   每部电影最后的选角角色:`/movies/movie/cast/role[last()]`\n\n下面的程序从字符串缓冲区加载一个 XML 文档，然后使用前面列出的 XPath 表达式执行节点选择。XML 文档的定义如下:\n\n```cpp\nstd::string text = R\"(\n<?xml version=\"1.0\"?>\n<movies>\n  <movie id=\"11001\" title=\"The Matrix\" year=\"1999\" length=\"196\">\n    <cast>\n      <role star=\"Keanu Reeves\" name=\"Neo\" />\n      <role star=\"Laurence Fishburne\" name=\"Morpheus\" />\n      <role star=\"Carrie-Anne Moss\" name=\"Trinity\" />\n      <role star=\"Hugo Weaving\" name=\" Agent Smith\" />\n    </cast>\n    <directors>\n      <director name=\"Lana Wachowski\" />\n      <director name=\"Lilly Wachowski\" />\n    </directors>\n    <writers>\n      <writer name=\"Lana Wachowski\" />\n      <writer name=\"Lilly Wachowski\" />\n    </writers>\n  </movie>\n  <movie id=\"9871\" title=\"Forrest Gump\" year=\"1994\" length=\"202\">\n    <cast>\n      <role star=\"Tom Hanks\" name=\"Forrest Gump\" />\n      <role star=\"Sally Field\" name=\"Mrs. Gump\" />\n      <role star=\"Robin Wright\" name=\"Jenny Curran\" />\n      <role star=\"Mykelti Williamson\" name=\"Bubba Blue\" />\n    </cast>\n    <directors>\n      <director name=\"Robert Zemeckis\" />\n    </directors>\n    <writers>\n      <writer name=\"Winston Groom\" />\n      <writer name=\"Eric Roth\" />\n    </writers>\n  </movie>\n</movies>\n)\";\n```\n\n可以通过以下方式选择请求的数据:\n\n```cpp\npugi::xml_document doc;\nif (doc.load_string(text.c_str()))\n{\n   try\n   {\n      auto titles = doc.select_nodes(\"/movies/movie[@year>1995]\");\n\n      for (auto it : titles)\n      {\n         std::cout << it.node().attribute(\"title\").as_string() \n                   << std::endl;\n      }\n   }\n   catch (pugi::xpath_exception const & e)\n   {\n      std::cout << e.result().description() << std::endl;\n   }\n\n   try\n   {\n      auto titles = doc.select_nodes(\"/movies/movie/cast/role[last()]\");\n\n      for (auto it : titles)\n      {\n         std::cout << it.node().attribute(\"star\").as_string() \n                   << std::endl;\n      }\n   }\n   catch (pugi::xpath_exception const & e)\n   {\n      std::cout << e.result().description() << std::endl;\n   }\n}\n```\n\n# 75.将数据序列化为 JSON\n\n与 XML 一样，不存在对 JSON 的标准支持。然而，有大量的跨平台库用于此目的。在撰写本文时，[https://github.com/miloyip/nativejson-benchmark](https://github.com/miloyip/nativejson-benchmark)提供的 *nativejson-benchmark* 项目列出了 40 多个库。这个项目是一个基准测试，评估具有 JSON 解析/生成能力的开源 C/C++ 库的一致性和性能(速度、内存和代码大小)。这使得选择合适的库可能有点困难，尽管顶级竞争者可能包括`RapidJSON`、`NLohmann`、`taocpp/json`、`Configuru`、`json_spirit`、`jsoncpp`。为了解决这个任务，我们将在这里使用`nlohmann/json`库。这是一个跨平台的 C++ 11 头文件库，具有直观的语法和良好的文档。这个图书馆可以在[https://github.com/nlohmann/json](https://github.com/nlohmann/json)找到。\n\n我们将使用相同的数据结构来表示电影，就像我们用于问题*将数据序列化和反序列化为/从 XML* 一样。`nlohmann`库使用`nlohmann::json`作为表示 JSON 对象的主要数据类型。虽然您可以使用更显式的语法创建 JSON 值，但是也有标量类型和标准容器之间的隐式转换。此外，您还可以通过在要转换的类型的命名空间中提供`to_json()`和`from_json()`方法来启用自定义类型之间的隐式转换。这些功能有一些要求，您可以在文档中阅读。\n\n在下面的代码中，这是所选择的方法。由于`movie`和`casting_role`类型是在全局命名空间中定义的，因此序列化这些类型的`to_json()`重载也是在全局命名空间中定义的。另一方面，类型`movie_list`实际上是`std::vector<movie>`的类型别名，可以直接序列化和反序列化，因为如前所述，库支持与标准容器之间的隐式转换:\n\n```cpp\nusing json = nlohmann::json;\n\nvoid to_json(json& j, casting_role const & c)\n{\n   j = json{ {\"star\", c.actor}, {\"name\", c.role} };\n}\n\nvoid to_json(json& j, movie const & m)\n{\n   j = json::object({\n      {\"id\", m.id},\n      {\"title\", m.title},\n      {\"year\", m.year},\n      {\"length\", m.length},\n      {\"cast\", m.cast },\n      {\"directors\", m.directors},\n      {\"writers\", m.writers}\n   });\n}\n\nvoid serialize(movie_list const & movies, std::string_view filepath)\n{\n   json jdata{ { \"movies\", movies } };\n\n   std::ofstream ofile(filepath.data());\n   if (ofile.is_open())\n   {\n      ofile << std::setw(2) << jdata << std::endl;\n   }\n}\n```\n\n功能`serialize()`可以使用，如下例所示:\n\n```cpp\nint main()\n{\n   movie_list movies\n   {\n      {\n         11001, \"The Matrix\", 1999, 196,\n         { {\"Keanu Reeves\", \"Neo\"},\n           {\"Laurence Fishburne\", \"Morpheus\"},\n           {\"Carrie-Anne Moss\", \"Trinity\"}, \n           {\"Hugo Weaving\", \"Agent Smith\"} },\n         {\"Lana Wachowski\", \"Lilly Wachowski\"},\n         {\"Lana Wachowski\", \"Lilly Wachowski\"},\n      },\n      {\n         9871, \"Forrest Gump\", 1994, 202,\n         { {\"Tom Hanks\", \"Forrest Gump\"},\n           {\"Sally Field\", \"Mrs. Gump\"},\n           {\"Robin Wright\",\"Jenny Curran\"},\n           {\"Mykelti Williamson\",\"Bubba Blue\"} },\n         {\"Robert Zemeckis\"},\n         {\"Winston Groom\", \"Eric Roth\"},\n      }\n   };\n\n   serialize(movies, \"movies.json\");\n}\n```\n\n# 76.从 JSON 反序列化数据\n\n为了解决这个任务，我们将再次使用`nlohmann/json`库。我们将采取更明确的方法，而不是像前面问题的解决方案中提到的那样编写`from_json()`函数。可以使用重载的`operator>>`将 JSON 文件的内容加载到`nlohmann::json`对象中。要访问对象值，应该使用`at()`方法，而不是`operator[]`，因为前者在键不存在时抛出异常(您可以处理的异常)，而后者表现出未定义的行为。要将对象值检索为特定的`T`对象，请使用`get<T>()`方法。然而，这要求类型`T`是默认可构造的。\n\n这里显示的`deserialize()`函数返回一个从指定 JSON 文件的内容构建的`std::vector<movie>`:\n\n```cpp\nusing json = nlohmann::json;\n\nmovie_list deserialize(std::string_view filepath)\n{\n   movie_list movies;\n\n   std::ifstream ifile(filepath.data());\n   if (ifile.is_open())\n   {\n      json jdata;\n\n      try\n      {\n         ifile >> jdata;\n\n         if (jdata.is_object())\n         {\n            for (auto & element : jdata.at(\"movies\"))\n            {\n               movie m;\n\n               m.id = element.at(\"id\").get<unsigned int>();\n               m.title = element.at(\"title\").get<std::string>();\n               m.year = element.at(\"year\").get<unsigned int>();\n               m.length = element.at(\"length\").get<unsigned int>();\n\n               for (auto & role : element.at(\"cast\"))\n               {\n                  m.cast.push_back(casting_role{\n                     role.at(\"star\").get<std::string>(),\n                     role.at(\"name\").get<std::string>() });\n               }\n\n               for (auto & director : element.at(\"directors\"))\n               {\n                  m.directors.push_back(director);\n               }\n\n               for (auto & writer : element.at(\"writers\"))\n               {\n                  m.writers.push_back(writer);\n               }\n\n               movies.push_back(m);\n            }\n         }\n      }\n      catch (std::exception const & ex)\n      {\n         std::cout << ex.what() << std::endl;\n      }\n   }\n\n   return movies;\n}\n```\n\n这个反序列化函数可以如下使用:\n\n```cpp\nint main()\n{\n   auto movies = deserialize(\"movies.json\");\n\n   assert(movies.size() == 2);\n   assert(movies[0].title == \"The Matrix\");\n   assert(movies[1].title == \"Forrest Gump\");\n}\n```\n\n# 77.将电影列表打印到 PDF\n\n有各种用于处理 PDF 文件的 C++ 库。 *HaHu* 、 *PoDoFo* 、*jaggpdf*和 *PDF-Writer* (也称为 *Hummus* )是您可以用于此目的的一些开源和跨平台库。在本书中，我将使用 *PDF-Writer* ，可在[https://github.com/galkahana/PDF-Writer](https://github.com/galkahana/PDF-Writer)获得。这是一个免费、快速、可扩展的库，具有基本的功能集，包括对文本、图像和形状的支持，同时支持 PDF 操作符和更高级的功能(我将使用它来解决这个问题)。\n\n功能`print_pdf()`，如下所示，实现了以下算法:\n\n*   用`PDFWriter::StartPDF()`开始一个新的 PDF 文档。\n*   每页最多打印 25 部电影。每个页面由一个`PDFPage()`对象表示，并且有一个`PageContentContext`对象，该对象是用`PDFPage::StartPageContentContext()`创建的，用于在页面上绘制项目。\n*   在第一页，放一个标题，内容为*电影列表*。使用`PageContentContext::WriteText()`将文本写在页面上。\n*   电影信息使用不同的字体书写。\n*   使用`PageContentContext::DrawPath()`在每页电影列表的顶部和底部画线。\n*   `PDFWriter::EndPageContentContext()`和`PDFWriter::WritePageAndRelease()`必须在一页写完内容后调用。\n*   `PDFWriter::EndPDF()`在完成 PDF 文档的编写时必须调用:\n\nFor information about the types and methods used in the following code, as well as more information about creating PDF documents and working with text, shapes, and images, see the project documentation available at [https://github.com/galkahana/PDF-Writer/wiki](https://github.com/galkahana/PDF-Writer/wiki).\n\n```cpp\n#ifdef _WIN32\nstatic const std::string fonts_dir = R\"(c:\\windows\\fonts\\)\";\n#elif defined (__APPLE__)\nstatic const std::string fonts_dir = R\"(/Library/Fonts/)\";\n#else\nstatic const std::string fonts_dir = R\"(/usr/share/fonts)\"; \n#endif\n\nvoid print_pdf(movie_list const & movies,\n               std::string_view path)\n{\n   const int height = 842;\n   const int width = 595;\n   const int left = 60;\n   const int top = 770;\n   const int right = 535;\n   const int bottom = 60;\n   const int line_height = 28;\n\n```\n\n```cpp\n   PDFWriter pdf;\n   pdf.StartPDF(path.data(), ePDFVersion13);\n   auto font = pdf.GetFontForFile(fonts_dir + \"arial.ttf\");\n\n   AbstractContentContext::GraphicOptions pathStrokeOptions(\n      AbstractContentContext::eStroke,\n      AbstractContentContext::eRGB,\n      0xff000000,\n      1);\n\n   PDFPage* page = nullptr;\n   PageContentContext* context = nullptr;\n   int index = 0;\n   for (size_t i = 0; i < movies.size(); ++ i)\n   {\n      index = i % 25;\n      if (index == 0)\n      {\n         if (page != nullptr)\n         {\n            DoubleAndDoublePairList pathPoints;\n            pathPoints.push_back(DoubleAndDoublePair(left, bottom));\n            pathPoints.push_back(DoubleAndDoublePair(right, bottom));\n            context->DrawPath(pathPoints, pathStrokeOptions);\n\n            pdf.EndPageContentContext(context);\n            pdf.WritePageAndRelease(page);\n         }\n\n         page = new PDFPage();\n         page->SetMediaBox(PDFRectangle(0, 0, width, height));\n         context = pdf.StartPageContentContext(page);\n\n         {\n            DoubleAndDoublePairList pathPoints;\n            pathPoints.push_back(DoubleAndDoublePair(left, top));\n            pathPoints.push_back(DoubleAndDoublePair(right, top));\n            context->DrawPath(pathPoints, pathStrokeOptions);\n         }\n      }\n\n      if (i == 0)\n      {\n         AbstractContentContext::TextOptions const textOptions(\n            font, 26, AbstractContentContext::eGray, 0);\n\n```\n\n```cpp\n         context->WriteText(left, top + 15, \n                            \"List of movies\", textOptions);\n      }\n\n      auto textw = 0;\n      {\n         AbstractContentContext::TextOptions const textOptions(\n            font, 20, AbstractContentContext::eGray, 0);\n\n         context->WriteText(left, top - 20 - line_height * index, \n                            movies[i].title, textOptions);\n         auto textDimensions = font->CalculateTextDimensions(\n                            movies[i].title, 20);\n         textw = textDimensions.width;\n      }\n\n      {\n         AbstractContentContext::TextOptions const textOptions(\n            font, 16, AbstractContentContext::eGray, 0);\n\n         context->WriteText(left + textw + 5, \n                            top - 20 - line_height * index, \n                            \" (\" + std::to_string(movies[i].year) + \")\", \n                            textOptions);\n\n         std::stringstream s;\n         s << movies[i].length / 60 << ':' << std::setw(2) \n           << std::setfill('0') << movies[i].length % 60;\n\n         context->WriteText(right - 30, top - 20 - line_height * index,\n            s.str(),\n           textOptions);\n      }\n   }\n\n   DoubleAndDoublePairList pathPoints;\n   pathPoints.push_back(\n      DoubleAndDoublePair(left, top - line_height * (index + 1)));\n   pathPoints.push_back(\n      DoubleAndDoublePair(right, top - line_height * (index + 1)));\n   context->DrawPath(pathPoints, pathStrokeOptions);\n\n   if (page != nullptr)\n   {\n```\n\n```cpp\n      pdf.EndPageContentContext(context);\n      pdf.WritePageAndRelease(page);\n   }\n\n   pdf.EndPDF();\n}\n```\n\n`print_pdf()`功能可以如下使用:\n\n```cpp\nint main()\n{\n   movie_list movies\n   {\n      { 1, \"The Matrix\", 1999, 136},\n      { 2, \"Forrest Gump\", 1994, 142},\n      // .. other movies\n      { 28, \"L.A. Confidential\", 1997, 138},\n      { 29, \"Shutter Island\", 2010, 138},\n   };\n\n   print_pdf(movies, \"movies.pdf\");\n}\n```\n\n# 78.从图像集合中创建 PDF\n\n为了解决这个问题，我们将使用与前一个问题相同的 *PDF-Writer* 库。我建议您先看看并实现前面的问题，如果您还没有这样做，然后再继续这个问题。\n\n下面的`get_images()`函数返回一个字符串向量，代表指定目录中所有 JPG 图像的路径:\n\n```cpp\nnamespace fs = std::experimental::filesystem;\n\nstd::vector<std::string> get_images(fs::path const & dirpath)\n{\n   std::vector<std::string> paths;\n\n   for (auto const & p : fs::directory_iterator(dirpath))\n   {\n```\n\n```cpp\n      if (p.path().extension() == \".jpg\")\n         paths.push_back(p.path().string());\n   }\n\n   return paths;\n}\n```\n\n`print_pdf()`功能从一个指定的目录创建一个包含所有 JPG 图像的 PDF 文档。它实现了以下算法:\n\n*   使用`PDFWriter::StartPDF()`创建新的 PDF 文档\n*   创建一个页面及其内容，并在页面上放置尽可能多的图像，一个接一个地垂直排列\n*   当新图像不适合当前页面时，用`PDFWriter::EndPageContentContext()`和`PDFWriter::SavePageAndRelease()`关闭页面并开始新页面\n*   使用`PageContentContext::DrawImage()`在页面内容上书写图像\n*   通过调用`PDFWriter::EndPDF()`结束文档\n\n```cpp\nvoid print_pdf(fs::path const & pdfpath,\n               fs::path const & dirpath)\n{\n   const int height = 842;\n   const int width = 595;\n   const int margin = 20;\n\n   auto image_paths = get_images(dirpath);\n\n   PDFWriter pdf;\n   pdf.StartPDF(pdfpath.string(), ePDFVersion13);\n\n   PDFPage* page = nullptr;\n   PageContentContext* context = nullptr;\n\n   auto top = height - margin;\n   for (size_t i = 0; i < image_paths.size(); ++ i)\n   {\n      auto dims = pdf.GetImageDimensions(image_paths[i]);\n\n      if (i == 0 || top - dims.second < margin)\n      {\n         if (page != nullptr)\n         {\n```\n\n```cpp\n            pdf.EndPageContentContext(context);\n            pdf.WritePageAndRelease(page);\n         }\n\n         page = new PDFPage();\n         page->SetMediaBox(PDFRectangle(0, 0, width, height));\n         context = pdf.StartPageContentContext(page);\n\n         top = height - margin;\n      }\n\n      context->DrawImage(margin, top - dims.second, image_paths[i]);\n\n      top -= dims.second + margin;\n   }\n\n   if (page != nullptr)\n   {\n      pdf.EndPageContentContext(context);\n      pdf.WritePageAndRelease(page);\n   }\n\n   pdf.EndPDF();\n}\n```\n\n`print_pdf()`可以像下面的例子一样使用，其中`sample.pdf`是输出的名称，`res`是包含图像的文件夹的名称:\n\n```cpp\nint main()\n{\n   print_pdf(\"sample.pdf\", \"res\");\n}\n```"
  },
  {
    "path": "docs/mod-cpp-challenge/10.md",
    "content": "# 十、归档、图像和数据库\n\n# 问题\n\n# 79.在 ZIP 存档中查找文件\n\n编写一个程序，可以搜索并打印 ZIP 存档中所有名称与用户提供的正则表达式匹配的文件(例如，使用`^.*\\.jpg$`查找扩展名为`.jpg`的所有文件)。\n\n# 80.将文件压缩到 ZIP 存档或从中解压缩\n\n编写一个可以执行以下操作的程序:\n\n*   递归地将文件或用户指定目录的内容压缩到 ZIP 存档中\n*   将 ZIP 存档的内容解压缩到用户指定的目标目录\n\n# 81.使用密码将文件压缩到 ZIP 存档或从中解压缩\n\n编写一个可以执行以下操作的程序:\n\n*   将文件或用户指定目录的内容递归压缩到受密码保护的 ZIP 存档中\n*   将受密码保护的 ZIP 存档的内容解压缩到用户指定的目标目录\n\n# 82.创建代表国旗的巴布亚新几内亚\n\n编写一个程序，生成一个代表罗马尼亚国旗的 PNG 文件，如下所示。图像的像素大小以及目标文件的路径应由用户提供:\n\n![](img/ef91c3bc-5e8e-4106-ad7a-77429c9d7572.png)\n\n# 83.创建验证文本 PNG 图像\n\n编写一个程序，可以创建类似验证码的 PNG 图像，用于向系统验证人类用户。这样的形象应该有:\n\n*   渐变背景\n*   一系列随机字母，以不同的角度左右显示\n*   图像上不同颜色的几条随机线(在文本上方)\n\n下面是这样一个图像的例子:\n\n![](img/28add9ea-14fe-40eb-9fb9-dee3ae6e6407.png)\n\n# 84.EAN-13 条形码发生器\n\n编写一个程序，可以为标准第 13 版中的任何国际商品编号生成带有 EAN-13 条形码的 PNG 图像。为简单起见，图像应该只包含条形码，并且可以跳过条形码下打印的 EAN-13 号码。以下是数字`5901234123457`的预期输出示例:\n\n![](img/c20e3077-983f-4cfe-b624-c8a09acd28c8.png)\n\n# 85.从 SQLite 数据库中读取电影\n\n编写一个程序，从 SQLite 数据库中读取电影并显示在控制台上。每部电影必须有一个数字标识符，一个标题，发行年份，以分钟为单位的长度，导演名单，编剧名单，以及演员和角色名称都包括在内的演员阵容。以下是应用于此目的的数据库的示意图:\n\n![](img/3033e8b2-df86-4975-9c79-73b0ddce1fbc.png)\n\n# 86.以事务方式将电影插入 SQLite 数据库\n\n扩展了为前面的问题编写的程序，这样它就可以将新电影添加到数据库中。电影可以从控制台或文本文件中读取。将电影数据插入数据库中的几个表必须以事务方式执行。\n\n# 87.在 SQLite 数据库中处理电影图像\n\n修改为前面的问题编写的程序，以支持将媒体文件(如图像，但也包括视频)添加到电影中。这些文件必须存储在数据库中的一个单独的表中，并且具有唯一的数字标识符、电影标识符、名称(通常是文件名)、可选描述和实际的媒体内容，以 blob 的形式存储。以下是必须添加到现有数据库中的表的结构图:\n\n![](img/057eaf49-6131-4f69-bb61-cc9b04f3362f.png)\n\n为这个问题编写的程序必须支持几个命令:\n\n*   列出符合搜索标准的所有电影(尤其是标题)...\n\n# 解决方法\n\n# 79.在 ZIP 存档中查找文件\n\n有各种各样的库为使用 ZIP 存档提供支持。免费提供的中，使用最多的包括 7z 的 *ZipLib* 、 *Info-Zip* 、 *MiniZip* 和 *LZMA SDK* 。然后，还有商业实现。对于本书中关于 ZIP 存档的问题，我选择了`ZipLib`。这是一个围绕标准库流构建的轻量级开源跨平台 C++ 11 库，没有额外的依赖关系。该图书馆及其文档可在[https://bitbucket.org/wbenny/ziplib](https://bitbucket.org/wbenny/ziplib)获得。\n\n要实现所需的功能，您必须:\n\n*   使用`ZipFile::Open()`打开 ZIP 存档\n*   使用`ZipArchive::GetEntry() ...`枚举档案中的所有条目\n\n# 80.将文件压缩到 ZIP 存档或从中解压缩\n\n为了解决这个由两部分组成的问题，我们将使用我们看到的相同的`ZipLib`库来解决前面的问题。这个问题的解决方案由两个功能组成，一个能够对 ZIP 存档执行压缩，另一个能够对 ZIP 存档执行解压缩。\n\n为了执行请求的压缩，我们应该执行以下操作:\n\n*   如果源路径代表常规文件，则使用`ZipFile::AddFile()`将该文件添加到 ZIP 存档中\n*   如果源路径表示递归目录，则:\n    *   递归地遍历目录中的所有条目\n    *   如果条目是一个目录，那么在 ZIP 存档中创建一个目录条目...\n\n# 81.使用密码将文件压缩到 ZIP 存档或从中解压缩\n\n这个问题与前面的问题非常相似，只是增加了文件必须加密的内容。`ZipLib`库仅支持 PKWare 加密。如果您需要使用另一种加密方法，那么您必须使用另一个库。如下所示的`compress()`和`decompress()`功能与前面问题的实现类似，但除了表示文件加密/解密密码的额外参数之外，还有一些不同之处:\n\n*   将加密文件添加到归档中是通过`ZipFile::AddEncryptedFile()`而不是`ZipFile::AddFile()`完成的\n*   解压时，密码必须用`ZipArchiveEntry::SetPassword() ...`设置\n\n# 82.创建代表国旗的巴布亚新几内亚\n\n使用 PNG 文件功能最丰富的库是 *libpng* ，一个用 C 语言编写的独立于平台的开源库，也有 C++ 库，其中一些是 *libpng* 的包装器，如 *png++* 、*lode pg*或 *PNGWriter* 。对于本书中的问题，我们将使用最后一个， *PNGWriter* 。它是一个开源库，可以在 Linux、Unix、macOS 和 Windows 上运行。其支持的功能包括打开现有的巴布亚新几内亚图像；绘制和读取 RGB、HSV 和 CMYK 颜色空间中的像素；基本形状；缩放；双线性插值；完整的 TrueType 抗锯齿和旋转文本支持；和贝塞尔曲线。它是 *libpng* 的包装器，也需要`FreeType2`库来存储文本...\n\n# 83.创建验证文本 PNG 图像\n\n这个问题可以用和前面有国旗的问题类似的方式解决。如果你还没有先做那个，我建议你先做，然后再继续这个。\n\n图像必须具备三个基本要素:\n\n*   渐变颜色背景。这可以通过从图像的一侧到另一侧绘制不同颜色的线(垂直或水平)来实现。可以使用`pngwriter::line()`功能画线。有几个可用的重载；下面代码中使用的一个取 RGB 颜色空间的红色、绿色和蓝色通道的开始和结束位置以及三个值。\n*   字母随机显示的随机文本...\n\n# 84.EAN-13 条形码发生器\n\n维基百科上描述的*国际商品编号*(又名*欧洲商品编号*或 *EAN* )是一种描述条形码符号和编号系统的标准，在全球贸易中用于识别特定制造商特定包装配置中的特定零售产品类型。最常用的 EAN 标准是 13 位数的 EAN-13。标准的描述，包括条形码应该如何生成的信息，可以在维基百科上的[https://en.wikipedia.org/wiki/International_Article_Number](https://en.wikipedia.org/wiki/International_Article_Number)找到，在本书中不会详细介绍。以下是编号为 5901234123457 的 EAN-13 条形码，在问题描述中作为示例给出(来源:维基百科):...\n\n# 85.从 SQLite 数据库中读取电影\n\nSQLite 是一个用 C 语言编写的进程内关系数据库管理库(尽管大量编程语言提供了对它的绑定)。SQLite 不是客户机-服务器数据库引擎，而是嵌入到应用中的引擎。整个数据库，包括表、索引、触发器和视图，都包含在一个磁盘文件中。因为访问数据库意味着访问本地磁盘文件，而不需要任何进程间通信，所以与其他关系数据库引擎相比，SQLite 具有更好的性能。SQLite，顾名思义，使用 SQL，虽然它没有实现所有的功能(比如`RIGHT OUTER JOIN`)。SQLite 不仅用于网络浏览器(几个主要的浏览器...\n\n# 86.将电影插入 SQLite 数据库\n\n这个问题的解决方案建立在前一个问题的基础上。你必须先解决这个问题，然后才能继续。此外，此处代码中使用的函数`split()`与问题 27*中的函数`split()`相同，该函数使用可能的分隔符列表*将字符串拆分为标记，该函数来自[第 3 章](03.html)、*字符串和正则表达式*。为此，这里不再一一列举。在这本书的源代码中，你会发现一个名为`cppchallenger86.db`的数据库文件，它为这个问题准备了几条记录。\n\n下面的函数`read_movie()`从控制台读取关于电影的信息(标题、发行年份、以分钟为单位的长度、导演、编剧和演员)，创建一个`movie`对象，并返回它。...\n\n# 87.在 SQLite 数据库中处理电影图像\n\n如果您还没有这样做，您必须先完成前面两个问题，然后再继续这个问题。对于这个问题，我们必须用一个额外的表来扩展数据库模型，以存储图像和可能的其他媒体文件，如视频。媒体文件的实际内容必须存储在 blob 字段中，但也应该存储其他属性，如描述和文件名。\n\nWhen you are using large objects you have two options: either store them directly in the database as blobs or keep them in separate files and store only the file paths in the database. According to the tests performed by the developers of SQLite, for objects smaller than 100KB, reads are faster when ..."
  },
  {
    "path": "docs/mod-cpp-challenge/11.md",
    "content": "# 十一、密码系统\n\n# 问题\n\n# 88.凯撒密码\n\n编写一个程序，可以使用带有右旋转和任意移位值的凯撒密码来加密和解密消息。为了简单起见，程序应该只考虑大写的文本消息，只编码字母，忽略数字、符号和其他类型的字符。\n\n# 89.Vigenère 密码\n\n编写一个程序，可以使用 Vigenère 密码对消息进行加密和解密。为简单起见，输入的加密纯文本消息应该仅由大写字母组成。\n\n# 90.Base64 编码和解码\n\n编写一个可以使用 base64 编码方案对二进制数据进行编码和解码的程序。你必须自己实现编码和解码功能，不要使用 3 <sup>rd</sup> 方库。用于编码的表格应该是 MIME 规范中的表格。\n\n# 91.正在验证用户凭据\n\n编写一个程序，模拟用户向安全系统进行身份验证的方式。要登录，用户必须已经在系统中注册。用户输入用户名和密码，程序检查是否匹配其注册用户；如果是，则授予用户访问权限，否则操作失败。出于安全原因，系统不得记录密码，而是使用 SHA 哈希。\n\n# 92.计算文件哈希\n\n编写一个程序，在给定文件路径的情况下，计算并向控制台打印文件内容的 SHA1、SHA256 和 MD5 哈希值。\n\n# 93.加密和解密文件\n\n编写一个可以使用**高级加密标准** ( **AES** 或 **Rijndael** )加密和解密文件的程序。应该可以指定源文件和目标文件路径以及密码。\n\n# 94.文件签名\n\n使用 RSA 加密技术，编写一个能够对文件进行签名并验证签名文件未被篡改的程序。签署文件时，签名应写入单独的文件，并在以后用于验证过程。程序应至少提供两个功能:一个是对文件进行签名(将文件的路径、RSA 私钥的路径和签名将被写入的文件的路径作为参数)，另一个是验证文件(将文件的路径、RSA 公钥的路径和签名文件的路径作为参数)。\n\n# 解决方法\n\n# 88.凯撒密码\n\n一种*凯撒密码*，也称为*凯撒密码*、*凯撒密码*、*凯撒移位*或*移位密码*，是一种非常古老、简单且广为人知的加密技术，它将纯文本中的每个字母替换为字母表中某个固定位置的字母。这种方法被凯撒大帝用来保护具有军事重要性的信息。他使用了三个字母的移位，因此用 D 代替 A，用 E 代替 B，等等。在这种编码中，文本 CPPCHALLENGER 成为 FSSFKDOOHQJHU。密码在维基百科[https://en.wikipedia.org/wiki/Caesar_cipher](https://en.wikipedia.org/wiki/Caesar_cipher)有详细描述。\n\nAlthough the Caesar cipher has no place in modern cryptography since it is trivial to break, it is still used on online forums or newsgroups ...\n\n# 89.Vigenère 密码\n\nVigenère 密码是一种加密技术，使用一系列交织的凯撒密码。虽然在 1553 年由乔凡·巴蒂斯塔·巴拉索描述，但它在 19 世纪被错误地归因于布莱斯·德·维根内，最终以他的名字命名。密码在维基百科[https://en.wikipedia.org/wiki/Vigen%C3%A8re_cipher](https://en.wikipedia.org/wiki/Vigen%C3%A8re_cipher)有详细描述。这里只做一个简短的总结。\n\nAlthough the Vigenère cipher took three centuries to be broken, it is nowadays trivial to break, just as in the case of the Caesar cipher, on which it is based. Like the previous problem, this one is proposed only as a fun and simple exercise and not as an argument in favor of using this cipher for cryptographic purposes.\n\n技术...\n\n# 90.Base64 编码和解码\n\nBase64 是一种编码方案，用于使用 64 个字符的字母表以 ASCII 格式表示二进制数据。虽然所有实现都使用相同的前 62 个字符(`A-Z`、`a-z`和`0-9`，但最后两个值可能不同。符号`+`和`/`在 MIME 规范中使用。base64 数字代表 6 位数据，四个 base64 数字正好编码三个字节(8 位)的二进制数据。当位数不能被 3 整除时，在转换为 base64 之前会添加额外的零字节。用`==`或`=`填充编码文本可用于指示来自普通数据的三个字节的最终组实际上只包含一个或两个字节。\n\n这是一个编码的例子...\n\n# 91.正在验证用户凭据\n\n加密方案的免费跨平台 C++ 库的一个好选择是 Crypto++。该库广泛用于非商业和商业项目，以及学术界、学生项目和其他项目，因为其加密功能的行业经验证的实现。该库支持 AES 和 AES 候选项，以及其他分组密码、消息认证码、哈希函数、公钥加密和许多其他功能，包括非加密功能，如伪随机数生成器、素数生成和验证、DEFLATE 压缩/解压缩、编码方案、校验和函数等。图书馆位于...\n\n# 92.计算文件哈希\n\n文件哈希通常用于确保内容的完整性，例如在从网络下载文件的情况下。虽然 SHA1 和 MD5 散列函数的实现可以在各种库中找到，但是我们将再次使用 Crypto++ 库。如果您没有遵循前面的问题及其解决方案*验证用户凭据*，您应该在继续此问题之前这样做，因为这里给出的关于 Crypto++ 库的一般信息将不再重复。\n\n使用 Crypto++ 库计算文件的散列相对简单。下面的代码使用了几个组件:\n\n*   `FileSource`，允许使用`BufferedTransformation`从文件中读取数据。默认情况下，...\n\n# 93.加密和解密文件\n\n为了使用 Crypto++ 库解决这个问题，我们需要使用几个组件:\n\n*   `FileSource`，允许使用`BufferedTransformation`从文件中读取数据。默认情况下，它以 4，096 字节的块或块的形式抽取数据，尽管手动抽取也是可能的。\n*   `FileSink`，允许你使用`BufferedTransformation`将数据写入文件。它是一个`FileSource`源对象的伴宿对象。\n*   `DefaultEncryptorWithMAC`和`DefaultDecryptorWithMAC`，使用认证标签对字符串和文件进行加密和解密，以检测篡改。他们使用 AES 作为默认分组密码，SHA256 作为 MAC 的默认哈希。每次运行这两个类都会产生不同的...\n\n# 94.文件签名\n\n签名和验证的过程类似于加密和解密，尽管在基本方式上有所不同；加密使用公钥完成，解密使用私钥完成，而签名使用私钥完成，验证使用公钥完成。签名有助于拥有公钥的收件人通过使用签名及其公钥来验证文件未被修改。但是，拥有公钥不足以更改文件并再次签名。Crypto++ 库也用于解决这个问题。\n\n虽然您可以使用任意一对公钥-私钥 RSA 来执行签名和验证，但是在这里提供的实现中，密钥是在以下情况下随机生成的..."
  },
  {
    "path": "docs/mod-cpp-challenge/12.md",
    "content": "# 十二、网络和服务\n\n# 问题\n\n# 95.查找主机的 IP 地址\n\n编写一个可以检索和打印主机 IPv4 地址的程序。如果找到多个地址，则应该打印所有地址。该程序应该在所有平台上运行。\n\n# 96.客户端-服务器嘶嘶声\n\n写一个可以用来玩 F *izz-Buzz* 游戏的客户端-服务器应用。根据游戏规则，客户端向服务器发送数字，服务器以嘶嘶声、嗡嗡声、嘶嘶声或数字本身作为回应。客户端和服务器之间的通信必须通过 TCP 完成。服务器应该无限期运行。只要用户输入 1 到 99 之间的数字，客户端就应该运行。\n\nFizz-Buzz 是一款儿童游戏，旨在教他们算术除法。一个玩家必须说出一个数字，另一个玩家应该回答:\n\n*   嘶，如果这个数能被 3 整除\n*   巴兹，如果这个数能被 5 整除\n*   如果这个数可以被 3 和 5 整除，那就嘶嘶作响\n*   在所有其他情况下，数字本身\n\n# 97.比特币汇率\n\n编写一个程序，显示最重要的货币(如美元、欧元或英镑)的比特币汇率。汇率必须从在线服务中获取，如: [https://blockchain.info](https://blockchain.info) 。\n\n# 98.使用 IMAP 获取电子邮件\n\n写一个程序，可以使用 IMAP 从电子邮件服务器获取信息。该计划应该能够:\n\n*   从邮箱中获取文件夹列表\n*   从特定文件夹获取未读电子邮件\n\n# 99.将文本翻译成任何语言\n\n编写一个程序，可以使用在线服务将文本从一种语言翻译成另一种语言。应该可以指定您希望翻译的文本、文本的语言以及要翻译的语言。\n\n# 100.检测图片中的人脸\n\n写一个能从图片中识别人脸的程序。至少，程序必须检测人的面部区域和性别。这些信息应该打印到控制台上。图片必须从磁盘加载。\n\n# 解决方法\n\n# 95.查找主机的 IP 地址\n\n主机信息，包括 IP 地址，可以通过系统特定的网络实用程序检索，如`gethostbyname()`。虽然这在所有平台上都可用，但使用方式不同，要求是编写一个在所有平台上都能工作的程序。有各种各样的开源跨平台网络库，比如 *POCO* 和 *Asio* / *Boost。Asio* 。 *POCO* 是一个更复杂的库，不仅支持联网，还支持数据访问、密码学、XML、JSON、Zip 等。 *Asio* 是一个独立的、只有头文件的库，具有用于网络编程的一致异步 I/O 模型。它也可以作为 Boost 库的一部分，以及基于它的标准化提案...\n\n# 96.客户端-服务器嘶嘶声\n\n为了解决这个问题，我们将再次使用 *Asio* 库。然而，这次我们需要编写两个程序:一个服务器和一个客户端。服务器接受特定端口上的 TCP 连接，打开连接的套接字，并开始在套接字上读取。当它从套接字中读取一些东西时，它会将其解释为 Fizz-Buzz 游戏的数字，写回答案，并继续等待另一个输入。客户端连接到特定端口上的主机，发送从控制台读取的数字，然后等待从服务器接收答案，然后将其打印到控制台。\n\n在服务器端，菲茨-巴斯游戏的实现相当简单，不需要额外的解释。...\n\n# 97.比特币汇率\n\n各种在线服务提供用于检查比特币市场价格和汇率的 API。您可以在`https://blockchain.info/ticker`免费使用的服务。一个`GET` HTTP 请求返回一个 JSON 对象，带有各种货币的市场价格。该原料药的文件可在:[https://blockchain.info/api/exchange_rates_api](https://blockchain.info/api/exchange_rates_api)找到。这里显示了这样一个 JSON 对象的摘录:\n\n```cpp\n{   \"USD\": {      \"15m\": 8196.491155299998,      \"last\": 8196.491155299998,      \"buy\": 8196.491155299998,      \"sell\": 8196.491155299998,      \"symbol\": \"$\"   },   \"GBP\": {      \"15m\": 5876.884158350099,      \"last\": 5876.884158350099,      \"buy\": 5876.884158350099,      \"sell\": 5876.884158350099,      \"symbol\": \"£\"   }}\n```\n\n有各种各样的库可以用来传输...\n\n# 98.使用 IMAP 获取电子邮件\n\n**互联网消息访问协议** ( **IMAP** )是一种使用 TCP/IP 从电子邮件服务器检索电子邮件的互联网协议。大多数电子邮件服务器提供商，包括像 Gmail、Outlook.com 和雅虎这样的大公司！邮件提供支持。有一些 C++ 库可以使用 IMAP，比如 *VMIME，*开源跨平台，支持 IMAP、POP、SMTP。但是，在本书中，我将使用 *cURL* (或者更具体地说， *libcurl* )使用 IMAPS 向电子邮件服务器发出 HTTP 请求。\n\n所需的操作可以通过几个 IMAP 命令来实现。在下面的列表中，`imap.domain.com`是一个示例域:\n\n*   `GET imaps://imap.domain.com`检索邮箱中的所有文件夹。如果...\n\n# 99.将文本翻译成任何语言\n\n许多云计算服务都提供文本翻译功能，包括微软认知服务、谷歌云翻译应用编程接口和亚马逊翻译。在本书中，我将在微软 Azure 中使用认知服务。Azure Cognitive Services 是机器学习和人工智能算法的集合，可用于轻松地为应用添加智能功能。包含的服务之一是*文本翻译应用编程接口*，它提供了语言检测、从一种语言到另一种语言的翻译以及将文本转换为语音等功能。我们还将使用 *libcurl* 进行 HTTP 请求。\n\n虽然有各种使用文本翻译的定价计划...\n\n# 100.检测图片中的人脸\n\n这是使用微软认知服务可以解决的另一个问题。该组中可用的服务之一名为 *Face API* ，提供了用于检测人脸、性别、年龄、情绪和各种人脸地标和属性的算法，以及查找人脸相似性、识别人、基于视觉人脸相似性对图片进行分组等功能。\n\n类似于 Text Translate API，有一个免费的计划，每月最多允许 30，000 笔交易，但每分钟只允许 20 笔。事务基本上是一个应用编程接口调用。有几个付费计划允许每月和每分钟进行更多的交易，但是为了解决这个问题，您可以使用免费层。还有..."
  },
  {
    "path": "docs/mod-cpp-challenge/13.md",
    "content": "# 十三、参考文献\n\n# 文章\n\n*   1337C0D3R，2011 年。*最长回文子串第一部分*，[https://articles . leetcode . com/最长回文子串第一部分/](https://articles.leetcode.com/longest-palindromic-substring-part-i/)\n*   Aditya Goel，2016 年。*给定字符串使用 stl 的排列*，[https://www . geeksforgeeks . org/给定字符串使用 STL 的排列/](https://www.geeksforgeeks.org/permutations-of-a-given-string-using-stl/)\n*   安德烈·雅各布，2010 年。*在 Visual Studio 2010* 、[中使用带有 SSH 支持的 libcurl](https://curl.haxx.se/libcurl/c/Using-libcurl-with-SSH-support-in-Visual-Studio-2010.pdf)\n*   阿什瓦尼·高塔姆，2017 年。*快速排序的分析是什么？*、[https://www.quora.com/What-is-the-analysis-of-quick-sort](https://www.quora.com/What-is-the-analysis-of-quick-sort)\n*   阿什温·南杰帕，2014 年。*如何使用 Visual Studio* 、[构建 Boost](https://codeyarns.com/2014/06/06/how-to-build-boost-using-visual-studio/)\n*   busycrack，2012 年。 *Telnet IMAP 命令注释*，[https://busylog.net/telnet-imap-commands-note/...](https://busylog.net/telnet-imap-commands-note/)\n\n# 图书馆文献\n\n*   *C/C++ JSON 解析器/生成器基准*，[https://github.com/miloyip/nativejson-benchmark](https://github.com/miloyip/nativejson-benchmark)\n*   *加密++* ，[https://www . crypt opp . com/wiki/main _ page](https://www.cryptopp.com/wiki/Main_Page)\n*   *鹰嘴豆泥 PDF*[http://pdfhummus.com/How-To](http://pdfhummus.com/How-To)\n*   *JSON for Modern c++*[https://github.com/nlohmann/json](https://github.com/nlohmann/json)\n*   *PDF-Writer*[https://github.com/galkahana/PDF-Writer](https://github.com/galkahana/PDF-Writer)\n*   *PNGWriter*[https://github.com/pngwriter/pngwriter](https://github.com/pngwriter/pngwriter)\n*   *普吉克斯 ml 1.8 快速入门指南*、[https://pugixml.org/docs/quickstart.html](https://pugixml.org/docs/quickstart.html)\n*   *SQLite*[https://www.sqlite.org/docs.html](https://www.sqlite.org/docs.html)\n*   *sqlite_modern_cpp* ，[https://github.com/SqliteModernCpp/sqlite_modern_cpp](https://github.com/SqliteModernCpp/sqlite_modern_cpp)\n*   *Ziplib wiki* ，[https://bit bucket . org/wbenny/ziplib/wiki/home](https://bitbucket.org/wbenny/ziplib/wiki/Home)"
  },
  {
    "path": "docs/mod-cpp-challenge/README.md",
    "content": "# 现代 C++ 的挑战\n\n> 原书：[The Modern C++ Challenge](https://libgen.rs/book/index.php?md5=13099BB5E3002C81929F8407722E8DB6)\n> \n> 协议：[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)\n> \n> 阶段：机翻（1）\n> \n> 自豪地采用[谷歌翻译](https://translate.google.cn/)\n> \n> 让开源界提前感受入关。\n\n* [在线阅读](https://ccpp.apachecn.org)\n* [在线阅读（Gitee）](https://apachecn.gitee.io/apachecn-c-cpp-zh/)\n* [ApacheCN 学习资源](http://docs.apachecn.org/)\n\n## 贡献指南\n\n本项目需要校对，欢迎大家提交 Pull Request。\n\n> 请您勇敢地去翻译和改进翻译。虽然我们追求卓越，但我们并不要求您做到十全十美，因此请不要担心因为翻译上犯错——在大部分情况下，我们的服务器已经记录所有的翻译，因此您不必担心会因为您的失误遭到无法挽回的破坏。（改编自维基百科）\n\n## 联系方式\n\n### 负责人\n\n* [飞龙](https://github.com/wizardforcel): 562826179\n\n### 其他\n\n*   在我们的 [apachecn/apachecn-c-cpp-zh](https://github.com/apachecn/apachecn-c-cpp-zh) github 上提 issue.\n*   发邮件到 Email: `apachecn@163.com`.\n*   在我们的 [组织学习交流群](http://www.apachecn.org/organization/348.html) 中联系群主/管理员即可.\n\n## 赞助我们\n\n![](http://data.apachecn.org/img/about/donate.jpg)\n"
  },
  {
    "path": "docs/mod-cpp-challenge/SUMMARY.md",
    "content": "+   [现代 C++ 的挑战](README.md)\n+   [零、前言](00.md)\n+   [一、数学问题](01.md)\n+   [二、语言特性](02.md)\n+   [三、字符串和正则表达式](03.md)\n+   [四、流和文件系统](04.md)\n+   [五、日期和时间](05.md)\n+   [六、算法和数据结构](06.md)\n+   [七、并发](07.md)\n+   [八、设计模式](08.md)\n+   [九、数据序列化](09.md)\n+   [十、归档、图像和数据库](10.md)\n+   [十一、密码系统](11.md)\n+   [十二、网络和服务](12.md)\n+   [十三、参考文献](13.md)\n"
  },
  {
    "path": "docs/qt5-cpp-gui-prog-cb/00.md",
    "content": "# 零、前言\n\n计算机软件市场的持续增长将我们带入了一个竞争激烈、充满挑战的时代。您的软件不仅需要功能强大且易于使用，还必须对用户具有吸引力和专业性。为了取得市场上其他软件产品的上风和竞争优势，你的产品的外观和感觉是至关重要的，应该在生产阶段就注意。在这本书里，我们将教你如何使用 Qt 5 开发平台创建功能性、吸引力和用户友好的软件。\n\n# 这本书是给谁的\n\n这本书是为那些想用 Qt 5 开发软件的人准备的。如果你想提高软件应用的视觉质量和内容呈现，这本书是为你准备的。\n\n# 充分利用这本书\n\n这本书是为那些刚刚开始学习 C++、QML 和 Qt 5 的人准备的。如果你已经理解了 C++ 编程语言，这将会有所帮助。然而，如果你不这样做也没关系，因为通过我们一步一步的例子，你很快就能掌握这门语言。Qt 5 有很多子版本；请确保您安装到计算机上的版本至少是 Qt 5.10 或更高版本，以便代码与书籍兼容。\n\n# 下载示例代码文件\n\n你可以从你在[www.packtpub.com](http://www.packt.com)的账户下载这本书的示例代码文件。如果您在其他地方购买了这本书，您可以访问[www.packtpub.com/support](http://www.packtpub.com/support)并注册将文件直接通过电子邮件发送给您。\n\n您可以按照以下步骤下载代码文件:\n\n1.  登录或注册[www.packtpub.com](http://www.packtpub.com/support)。\n2.  选择“支持”选项卡。\n3.  点击代码下载和勘误表。\n4.  在搜索框中输入图书的名称，并按照屏幕指示进行操作。\n\n下载文件后，请确保使用最新版本的解压缩文件夹:\n\n*   视窗系统的 WinRAR/7-Zip\n*   zipeg/izp/un ARX for MAC\n*   适用于 Linux 的 7-Zip/PeaZip\n\n这本书的代码包也托管在 GitHub 上[https://GitHub . com/PacktPublishing/Qt5-CPP-GUI-编程-烹饪书-第二版](https://github.com/PacktPublishing/Qt5-CPP-GUI-Programming-Cookbook-Second-Edition)。我们还有来自丰富的书籍和视频目录的其他代码包，可在获得。看看他们！\n\n# 下载彩色图像\n\n我们还提供了一个 PDF 文件，其中包含本书中使用的截图/图表的彩色图像。可以在这里下载:[http://www . packtpub . com/sites/default/files/downloads/9781789803822 _ color images . pdf](_ColorImages.pdf)。\n\n# 行动中的代码\n\n访问以下链接查看正在运行的代码的视频:[http://bit.ly/2TrFE2i](http://bit.ly/2TrFE2i)\n\n# 使用的约定\n\n本书通篇使用了许多文本约定。\n\n`CodeInText`:表示文本中的码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟网址、用户输入和推特句柄。这里有一个例子:“发生这种情况是因为我们专门告诉选择器用一个名为`QPushButton`的类将样式应用于所有小部件。”\n\n代码块设置如下:\n\n```cpp\nhtml, body, #map {\n height: 100%; \n margin: 0;\n padding: 0\n}\n```\n\n当我们希望将您的注意力吸引到代码块的特定部分时，相关的行或项目以粗体显示:\n\n```cpp\nint main(int argc, char *argv[]) {\n    QApplication a(argc, argv);\n    MainWindow w;\n    w.show();\n    printText(\"A\", 100);\n printText(\"B\", 100);\n    return a.exec();\n}\n```\n\n任何命令行输入或输出都编写如下:\n\n```cpp\n$ mkdir css\n$ cd css\n```\n\n**粗体**:表示一个新的术语、一个重要的单词或者你在屏幕上看到的单词。例如，菜单或对话框中的单词像这样出现在文本中。这里有一个例子:“一旦你完成了，打开 Qt Creator 并创建一个新的 Qt Widgets 应用项目。”\n\nWarnings or important notes appear like this. Tips and tricks appear like this.\n\n# 部分\n\n在这本书里，你会发现几个经常出现的标题(*准备*，*怎么做...*、*它是如何工作的...*、*还有更多...*和*参见*。要给出如何完成配方的明确说明，请使用以下章节:\n\n# 准备好\n\n本节告诉您配方中的预期内容，并描述如何设置配方所需的任何软件或任何初步设置。\n\n# 怎么做…\n\n本节包含遵循配方所需的步骤。\n\n# 它是如何工作的…\n\n这一部分通常包括对前一部分发生的事情的详细解释。\n\n# 还有更多…\n\n本节包含关于配方的附加信息，以便让您更好地了解配方。\n\n# 请参见\n\n本节提供了该配方的其他有用信息的有用链接。\n\n# 取得联系\n\n我们随时欢迎读者的反馈。\n\n**综合反馈**:发邮件`feedback@packtpub.com`并在邮件主题中提及书名。如果您对本书的任何方面有疑问，请发电子邮件至`questions@packtpub.com`。\n\n**勘误表**:虽然我们已经尽了最大的努力来保证内容的准确性，但是错误还是会发生。如果你在这本书里发现了一个错误，如果你能向我们报告，我们将不胜感激。请访问[www.packtpub.com/submit-errata](http://www.packtpub.com/submit-errata)，选择您的图书，点击勘误表提交链接，并输入详细信息。\n\n**盗版**:如果您在互联网上遇到任何形式的我们作品的非法拷贝，如果您能提供我们的位置地址或网站名称，我们将不胜感激。请通过`copyright@packtpub.com`联系我们，并提供材料链接。\n\n**如果你有兴趣成为一名作者**:如果有一个你有专长的话题，你有兴趣写或者投稿一本书，请访问[authors.packtpub.com](http://authors.packtpub.com/)。\n\n# 复习\n\n请留下评论。一旦你阅读并使用了这本书，为什么不在你购买它的网站上留下评论呢？然后，潜在的读者可以看到并使用您不带偏见的意见来做出购买决定，我们在 Packt 可以了解您对我们产品的看法，我们的作者可以看到您对他们的书的反馈。谢谢大家！\n\n更多关于 Packt 的信息，请访问[packtpub.com](https://www.packtpub.com/)。"
  },
  {
    "path": "docs/qt5-cpp-gui-prog-cb/01.md",
    "content": "# 一、将 Qt 设计器用于外观定制\n\n我们将在本章介绍以下食谱:\n\n*   在 Qt 设计器中使用样式表\n*   自定义基本样式表\n*   使用样式表创建登录屏幕\n*   在样式表中使用资源\n*   自定义属性和子控件\n*   **Qt 建模语言** ( **QML** )中的造型\n*   将 QML 对象指针暴露给 C++\n\n# 介绍\n\nQt 5 允许我们通过大多数人都熟悉的方法轻松设计程序的用户界面。Qt 不仅为我们提供了一个强大的用户界面工具包，名为 **Qt** **Designer** ，让我们不用写一行代码就能设计出自己的用户界面，它还允许高级用户通过一种简单的脚本语言 **Qt Style Sheets** 定制自己的用户界面组件。\n\n# 技术要求\n\n本章的技术要求包括 Qt 5.11.2 MinGW 32 位、Qt Creator 4.8.2 和 Windows 10。\n\n本章使用的所有代码均可从以下 GitHub 资源库下载，网址为:[https://GitHub . com/PacktPublishing/Qt5-CPP-GUI-Programming-cook book-第二版/tree/master/Chapter01](https://github.com/PacktPublishing/Qt5-CPP-GUI-Programming-Cookbook-Second-Edition/tree/master/Chapter01) 。\n\n查看以下视频，查看正在运行的代码:[http://bit.ly/2TqQJ3I](http://bit.ly/2TqQJ3I)\n\n# 在 Qt 设计器中使用样式表\n\n在这个例子中，我们将学习如何通过使用样式表和资源来改变程序的外观和感觉，并使它看起来更加专业。Qt 允许你使用一种叫做 Qt Style Sheets 的样式表语言来装饰你的**图形 u** **ser 界面****s**(**GUI**)，这种样式表语言与网页设计师用来装饰他们的网站的**层叠样式表** ( **CSS** )非常相似。\n\n# 怎么做…\n\n让我们从学习如何创建新项目开始，并熟悉 Qt 设计器:\n\n1.  打开 **Qt Creator** 创建一个新项目。如果这是你第一次使用 Qt Creator，你可以点击大按钮，上面写着+新项目，或者直接进入文件| **新文件**或项目。\n2.  在项目窗口下选择应用，然后选择 Qt 小部件应用。\n3.  单击选择...底部的按钮。将弹出一个窗口，要求您插入项目名称及其位置。\n4.  多次单击“下一步”，然后单击“完成”按钮创建项目。我们现在将保持默认设置。项目创建完成后，首先看到的是窗口左侧有成吨大图标的面板，称为模式选择器面板；我们将在后面的*中详细讨论它是如何工作的...*段。\n5.  你会看到你所有的源文件都列在模式选择器面板旁边的边栏面板上。这是您可以选择要编辑哪个文件的地方，在本例中是`mainwindow.ui`，因为我们即将开始设计程序的 UI。\n\n6.  双击`mainwindow.ui`文件，会看到一个完全不同的界面凭空出现。Qt Creator 实际上帮助您从脚本编辑器切换到 UI 编辑器(Qt Designer)，因为它在您试图打开的文件上检测到了`.ui`扩展名。\n7.  您还会注意到，模式选择器面板上突出显示的按钮已从“编辑”按钮更改为“设计”按钮。通过单击位于模式选择器面板上半部分的按钮之一，您可以切换回脚本编辑器或切换到任何其他工具。\n8.  让我们回到 Qt 设计器，看看`mainwindow.ui`文件。这基本上是我们程序的主窗口(正如文件名所暗示的)，默认情况下是空的，上面没有任何小部件。您可以尝试通过按下模式选择器面板底部的运行按钮(绿色箭头按钮)来编译和运行程序，一旦编译完成，您将看到一个空窗口弹出。\n9.  让我们在程序的用户界面中添加一个按钮，方法是点击窗口小部件框中的按钮项(在按钮类别下)，并将其拖动到表单编辑器的主窗口中。保持按钮处于选中状态，现在您将在窗口右侧的属性编辑器中看到该按钮的所有属性。向下滚动到中间，寻找一个名为样式表的属性。这是您将样式应用于小部件的地方，小部件可以递归地继承或不继承其子代或孙代，这取决于您如何设置样式表。或者，您可以在表单编辑器中右键单击用户界面中的任何小部件，然后选择“更改样式表”...从弹出式菜单中。\n10.  您可以单击样式表属性的输入字段直接编写样式表代码，或者单击输入字段旁边的…按钮打开编辑样式表窗口，该窗口有更大的空间为样式表编写更长的代码。在窗口顶部，您可以找到几个按钮，如添加资源、添加渐变、添加颜色和添加字体，如果您不记得属性的名称，这些按钮可以帮助您开始编码。让我们尝试使用“编辑样式表”窗口做一些简单的样式设置。\n11.  单击添加颜色并选择一种颜色。\n12.  从颜色选择器窗口中选择一种随机颜色；比如说，一种纯红色。然后单击确定。\n13.  在“编辑样式表”窗口的文本字段中添加了一行代码，在我的示例中如下所示:\n\n```cpp\ncolor: rgb(255, 0, 0);\n```\n\n14.  单击“确定”按钮，按钮上的文本应该会变成红色。\n\n# 它是如何工作的...\n\n在开始学习如何设计自己的 UI 之前，让我们花点时间熟悉一下 Qt Designer 的界面:\n\n![](img/6c2195e0-be23-410d-bf2c-268dbaf5c0c9.png)\n\n前面截图的解释如下:\n\n1.  **菜单栏**:菜单栏包含特定于应用的菜单，可以轻松访问基本功能，如创建新项目、保存文件、撤消、重做、复制和粘贴。它还允许您访问 Qt Creator 附带的开发工具，如编译器、调试器和分析器。\n2.  Widget Box:这里可以找到 Qt Designer 提供的所有不同类型的 Widget。您可以通过单击小部件框中的一个小部件并将其拖动到表单编辑器中，将小部件添加到程序的用户界面中。\n\n3.  **模式选择器**:模式选择器是一个侧面板，放置快捷按钮，方便用户使用不同的工具。通过单击模式选择器面板上的“编辑”或“设计”按钮，可以在脚本编辑器和表单编辑器之间快速切换，这对于多任务处理非常有用。您还可以以同样的速度和方式轻松导航到调试器和分析器工具。\n4.  **构建快捷方式**:构建快捷方式位于模式选择器面板的底部。通过按此处的快捷按钮，您可以轻松地构建、运行和调试项目。\n5.  **表单编辑器**:表单编辑器是你编辑程序 UI 的地方。您可以通过从小部件框中选择一个小部件并将其拖动到表单编辑器中，向程序中添加不同的小部件。\n6.  **表单工具栏**:从这里可以快速选择不同的表单进行编辑。单击小部件框顶部的下拉框，并选择要用 Qt 设计器打开的文件。下拉框旁边是用于在表单编辑器的不同模式之间切换的按钮，以及用于更改用户界面布局的按钮。\n7.  对象检查器:对象检查器列出当前`.ui`文件中的所有小部件。所有小部件都是根据它们在层次结构中的父子关系排列的。您可以从对象检查器中选择一个小部件，以在属性编辑器中显示其属性。\n8.  属性编辑器:属性编辑器将显示您从对象检查器窗口或表单编辑器窗口中选择的小部件的所有属性。\n9.  动作编辑器和信号和插槽编辑器:此窗口包含两个编辑器，动作编辑器和信号和插槽编辑器，可以从窗口下方的选项卡访问。操作编辑器是您创建可以添加到程序用户界面的菜单栏或工具栏的操作的地方。\n10.  **输出窗格**:输出窗格由几个不同的窗口组成，显示与脚本编译和调试相关的信息和输出消息。您可以通过按下前面带有数字的按钮在不同的输出窗格之间切换，例如 1 问题、2 搜索结果或 3 应用输出。\n\n# 还有更多…\n\n在前一节中，我们讨论了如何通过 C++ 编码将样式表应用于 Qt Widgets。虽然这种方法确实很有效，但大多数时候，负责设计程序用户界面的人不是程序员，而是专门设计用户友好界面的用户界面设计师。在这种情况下，最好让 UI 设计人员用不同的工具设计程序的布局和样式表，而不要乱动代码。Qt 提供了一个名为 **Qt Creator** 的一体化编辑器。\n\nQt Creator 由几个不同的工具组成，如脚本编辑器、编译器、调试器、分析器和 UI 编辑器。UI 编辑器，也叫 **Qt Designer** ，是设计人员不用写任何代码就能设计程序 UI 的完美工具。这是因为 Qt Designer 采用了*所见即所得*的方法，提供了最终结果的精确可视化表示，这意味着无论你用 Qt Designer 设计什么，当程序编译和运行时，在视觉上都会完全相同。\n\nQt 样式表和 CSS 的相似之处如下:\n\n*   典型的 CSS 代码是这样的:\n\n```cpp\nh1 { color: red; background-color: white;}\n```\n\n*   这就是 Qt 样式表的外观，它几乎与前面的 CSS 相同:\n\n```cpp\nQLineEdit { color: red; background-color: white;}\n```\n\n如您所见，它们都包含一个选择器和一个声明块。每个声明包含一个属性和值，用冒号分隔。在 Qt 中，通过在 C++ 代码中调用`QObject::setStyleSheet()`函数，可以将样式表应用于单个小部件。例如，考虑以下内容:\n\n```cpp\nmyPushButton->setStyleSheet(\"color : blue\");\n```\n\n前面的代码将把带有`myPushButton`变量名的按钮文本变成蓝色。您可以通过在 Qt 设计器的样式表属性字段中编写声明来获得相同的结果。我们将在下面的*定制基本样式表*一节中进一步讨论 Qt 设计器。\n\nQt Style Sheets 还支持 CSS2 标准中定义的所有不同类型的选择器，包括通用选择器、类型选择器、类选择器和 ID 选择器，这允许我们将样式应用于非常特定的单个小部件或一组小部件。例如，如果我们想用`usernameEdit`对象名更改特定行编辑小部件的背景颜色，我们可以通过使用标识选择器来引用它来实现:\n\n```cpp\nQLineEdit#usernameEdit { background-color: blue }\n```\n\nTo learn about all the selectors available in CSS2 (which are also supported by Qt Style Sheets), please refer to this document: [http://www.w3.org/TR/REC-CSS2/selector.html](http://www.w3.org/TR/REC-CSS2/selector.html).\n\n# 自定义基本样式表\n\n在前面的方法中，您学习了如何使用 Qt Designer 将样式表应用于小部件。让我们疯狂一点，通过创建一些其他类型的小部件，将它们的样式属性更改为一些奇怪的东西，以便学习。\n\n但是，这一次，我们不会将样式逐个应用于每个小部件，而是将学习将样式表应用于主窗口，并让它继承到所有其他小部件的层次结构中，这样从长远来看，样式表更容易管理和维护。\n\n# 怎么做…\n\n在下面的示例中，我们将格式化画布上不同类型的小部件，并向样式表添加一些代码来更改其外观:\n\n1.  通过选择样式表并单击样式表属性旁边的小箭头按钮，从`PushButton`中移除样式表。此按钮会将属性恢复为默认值，在本例中为空样式表。\n2.  通过将小部件框中的小部件一个接一个地拖动到表单编辑器中，向用户界面添加一些小部件。我添加了行编辑、组合框、水平滑块、单选按钮和复选框。\n\n3.  为了简单起见，通过从对象检查器中选择菜单栏、主工具栏和状态栏，右键单击并选择删除，从用户界面中删除它们。现在，您的用户界面应该如下所示:\n\n![](img/d958de85-965e-404f-a858-18707e56a9cc.png)\n\n4.  从表单编辑器或对象检查器中选择主窗口，然后右键单击并选择“更改样式表”...打开“编辑样式表”窗口。在样式表中插入以下内容:\n\n```cpp\nborder: 2px solid gray;\nborder-radius: 10px;\npadding: 0 8px;\nbackground: yellow;\n```\n\n5.  你会看到一个奇怪的用户界面，所有的东西都用黄色覆盖，有一个厚厚的边框。这是因为前面的样式表没有选择器，这意味着样式将应用于主窗口的子小部件，一直向下到层次结构。为了改变这种情况，让我们尝试一些不同的方法:\n\n```cpp\nQPushButton {\n    border: 2px solid gray;\n    border-radius: 10px;\n    padding: 0 8px;\n    background: yellow;\n}\n```\n\n6.  这一次，只有`PushButton`会获得前面代码中描述的样式，其他所有小部件都会恢复默认样式。您可以尝试在用户界面上再添加几个按钮，它们看起来都一样:\n\n![](img/0c049424-21cd-4bd5-bd3c-050ecf553672.png)\n\n7.  发生这种情况是因为我们特别告诉选择器将样式应用于所有带有`QPushButton`类的小部件。我们还可以通过在样式表中提及按钮的名称，将样式应用于其中一个按钮，如下面的代码所示:\n\n```cpp\nQPushButton#pushButton_3 {\n border: 2px solid gray;\n border-radius: 10px;\n padding: 0 8px;\n background: yellow;\n}\n```\n\n8.  一旦理解了这个方法，我们就可以将以下代码添加到样式表中:\n\n```cpp\nQPushButton {\n    color: red;\n    border: 0px;\n    padding: 0 8px;\n    background: white;\n}\n\nQPushButton#pushButton_2 {\n    border: 1px solid red;\n    border-radius: 10px;\n}\n```\n\n9.  这段代码基本上改变了所有按钮的样式，以及`pushButton_2`按钮的一些属性。我们保持`pushButton_3`的样式表不变。现在按钮将如下所示:\n\n![](img/a6c15c56-588f-44c6-b690-094ff114785b.png)\n\n10.  第一组样式表将把`QPushButton`类型的所有小部件变成没有边框和红色文本的白色矩形按钮。第二组样式表只改变一个名为`pushButton_2`的特定`QPushButton`部件的边框。请注意`pushButton_2`的背景色和文本色分别保持为白色和红色，因为我们没有在第二组样式表中覆盖它们，因此它将返回到第一组样式表中描述的样式，因为它适用于所有`QPushButton`小部件。第三个按钮的文本也变成了红色，因为我们没有在第三组样式表中描述 Color 属性。\n\n11.  使用以下代码创建另一组使用通用选择器的样式表:\n\n```cpp\n* {\n background: qradialgradient(cx: 0.3, cy: -0.4, fx: 0.3, fy: -0.4, radius: 1.35, stop: 0 #fff, stop: 1 #888);\n color: rgb(255, 255, 255);\n border: 1px solid #ffffff;\n}\n```\n\n12.  通用选择器将影响所有小部件，不管它们的类型如何。因此，前面的样式表将对所有小部件的背景应用一种漂亮的渐变颜色，并将它们的文本设置为白色，带有一个也是白色的单像素实心轮廓。我们可以使用`rgb`函数(`rgb(255, 255, 255)`)或十六进制代码(`#ffffff`)来描述颜色值，而不是写颜色的名称(即白色)。\n13.  和以前一样，前面的样式表不会影响按钮，因为我们已经给了它们自己的样式，这将覆盖通用选择器中描述的一般样式。请记住，在 Qt 中，当不止一种风格对一个小部件有影响时，最终会使用更具体的风格。这就是用户界面现在的样子:\n\n![](img/2e492e88-e1d7-4e5b-95e1-ddac7c7e3343.png)\n\n# 它是如何工作的...\n\n如果你曾经参与过使用 HTML 和 CSS 的 web 开发，那么 Qt 的样式表和 CSS 的工作方式完全一样。样式表提供了描述小部件表示的定义——小部件组中每个元素的颜色是什么，边框应该有多厚，等等。如果您在样式表中指定小部件的名称，它将使用您提供的名称更改特定`PushButton`小部件的样式。其他小部件都不会受到影响，将保持默认样式。\n\n要更改小部件的名称，请从表单编辑器或对象检查器中选择小部件，并在属性窗口中更改名为对象名称的属性。如果您以前使用过标识选择器来更改小部件的样式，更改其对象名称将会破坏样式表并丢失样式。要解决这个问题，只需在样式表中更改对象名。\n\n# 使用样式表创建登录屏幕\n\n接下来，我们将学习如何将我们在前面的示例中学习的所有知识放在一起，并为一个假想的操作系统创建一个假的图形登录屏幕。为了设计一个好的用户界面，样式表不是你唯一需要掌握的东西。您还需要学习如何使用 Qt Designer 中的布局系统整齐地排列小部件。\n\n# 怎么做…\n\n让我们从以下步骤开始:\n\n1.  在我们开始做任何事情之前，我们需要设计图形登录屏幕的布局。为了生产出好的软件，计划是非常重要的。下面是我做的一个布局设计示例，向您展示我想象的登录屏幕的外观。只要能清楚地传达信息，像这样的简单线条画就足够了:\n\n![](img/38eaf49b-ec0e-4e07-abd8-7af5765d6b02.png)\n\n2.  再次回到 Qt 设计器。\n3.  我们将首先在顶部面板放置小部件，然后在它下面放置徽标和登录表单。\n4.  选择主窗口，将其宽度和高度分别从`400`和`300`更改为`800`和`600`，因为我们需要更大的空间来放置所有小部件。\n5.  单击“显示小部件”类别下的标签，并将其从小部件框拖动到表单编辑器中。\n6.  将标签的 objectName 属性更改为`currentDateTime`，将其文本属性更改为当前日期和时间，仅用于显示目的，如`Monday, 25-10-2015 3:14 PM`。\n7.  单击按钮类别下的`PushButton`并将其拖动到表单编辑器中。再次重复这个过程，因为我们在顶部面板上有两个按钮。重命名两个按钮`restartButton`和`shutdownButton`。\n8.  选择主窗口，然后单击窗体工具栏上的小图标按钮，当您将鼠标悬停在该按钮上时，它会显示“垂直布局”。您将看到小部件自动排列在主窗口上，但这还不是我们想要的。\n9.  单击“布局”类别下的水平布局小部件并将其拖动到主窗口。\n\n10.  单击并拖动两个按钮和文本标签进入水平布局。您将看到三个小部件排列成水平行，但垂直方向它们位于屏幕中间。水平排列几乎正确，但垂直位置完全偏离。\n11.  从“间隔”类别中单击并拖动一个垂直间隔，并将其放置在我们之前在*步骤 9* 中创建的水平布局下方(红色矩形轮廓下方)。所有的小部件都被垫片推到顶部。\n12.  在文本标签和两个按钮之间放置一个水平间隔，使它们保持分开。这将确保文本标签始终向左，按钮向右对齐。\n13.  将两个按钮的水平策略和垂直策略属性设置为固定，并将最小大小属性设置为`55 x 55`。将按钮的文本属性设置为空，因为我们将使用图标而不是文本。我们将在下面的【使用样式表中的资源】一节中学习如何在按钮部件中放置图标。\n14.  您的用户界面应该如下所示:\n\n![](img/b10f425f-4451-42eb-a472-3f39e3aa2e6d.png)\n\n接下来，我们将使用以下步骤添加徽标:\n\n1.  在顶部面板和垂直隔板之间添加水平布局，作为徽标的容器。\n2.  添加水平布局后，您会发现布局高度太薄(几乎为零高度)，无法添加任何小部件。这是因为布局是空的，它被它下面的垂直间隔推到零高度。为了解决这个问题，我们可以将其**垂直边距【layoutTopMargin 或 layoutBottomMargin)设置为暂时更大，直到一个小部件添加到布局中。**\n3.  向您刚刚创建的水平布局添加一个标签，并将其重命名为`logo`。在下面的*使用样式表中的资源*一节中，我们将了解更多关于如何在标签中插入图像以将其用作徽标的信息。现在，只需清空文本属性，并将其水平策略和垂直策略属性都设置为固定。将最小尺寸属性设置为`150 x 150`。\n4.  如果您尚未将布局的垂直边距设置为零，请将其设置为零。\n\n5.  徽标现在看起来是不可见的，所以我们将只放置一个临时样式表来使其可见，直到我们在下面的*使用样式表中的资源*部分向其添加图像。样式表非常简单:\n\n```cpp\nborder: 1px solid;\n```\n\n6.  您的用户界面应该如下所示:\n\n![](img/5215e525-c4b3-414b-9540-b03a6a5a1983.png)\n\n现在，让我们使用以下步骤创建登录表单:\n\n1.  在徽标布局和垂直间隔之间添加水平布局。将 layoutTopMargin 属性设置为一个大数字(即`100`)，这样可以更容易地向其中添加小部件。\n2.  在刚刚创建的水平布局中添加垂直布局。此布局将用作登录表单的容器。将其 layoutTopMargin 设置为低于水平布局的数字(即`20`)，这样我们就可以在其中放置小部件。\n3.  右键单击刚刚创建的垂直布局，然后选择变形为| QWidget。垂直布局被转换成一个空的小部件。这一步非常重要，因为我们将为登录表单调整容器的宽度和高度。布局小部件不包含任何宽度和高度属性，只包含边距，因为布局会向其周围的空白空间扩展，考虑到它没有任何大小属性，这是有意义的。在您将布局转换为`QWidget`对象后，它将自动继承小部件类的所有属性，因此我们现在能够调整其大小以满足我们的需求。\n\n4.  将我们刚刚从布局转换的`QWidget`对象重命名为`loginForm`，并将其水平策略和垂直策略属性都更改为固定。将最小尺寸参数设置为`350 x 200`。\n5.  由于我们已经在水平布局中放置了`loginForm`小部件，我们现在可以将其 layoutTopMargin 属性设置回零。\n6.  将与徽标相同的样式表添加到`loginForm`小部件中，使其暂时可见，除了这次我们需要在前面添加一个标识选择器，这样它只会将样式应用到`loginForm`而不会应用到其子小部件中:\n\n```cpp\n#loginForm { border: 1px solid; }\n```\n\n7.  您的用户界面应该如下所示:\n\n![](img/09750f03-8111-4421-8845-ddc9cbaa9392.png)\n\n我们还没有完成登录表单。现在我们已经为登录表单创建了容器，是时候在表单中放入更多的小部件了:\n\n1.  将两个水平布局放入登录表单容器中。我们需要两种布局:一种用于用户名字段，另一种用于密码字段。\n2.  为您刚刚添加的每个布局添加标签和线条编辑。将上方标签的文本属性更改为`Username:`，将下方标签的文本属性更改为`Password:`。将两行编辑分别重命名为`username`和`password`。\n3.  在密码布局下方添加一个按钮，将其文本属性更改为`Login`。更名为`loginButton`。\n4.  您可以在密码布局和登录按钮之间添加一个垂直间隔，使它们稍微分开。放置垂直间隔后，将其“大小类型”属性更改为“固定”，并将“高度”更改为 5。\n5.  选择`loginForm`容器，将其所有边距设置为`35`。这是为了通过在登录表单的四周添加一些空间来使其看起来更好。\n6.  将`Username`、`Password`和`loginButton`小部件的“高度”属性设置为`25`，这样它们看起来就不会那么局促了。\n7.  您的用户界面应该如下所示:\n\n![](img/2ba325d8-276e-46a8-b7f0-1ab21676d059.png)\n\n我们还没完呢！如您所见，登录表单和徽标都粘贴在主窗口的顶部，因为它们下面有垂直间隔。徽标和登录表单应该放在主窗口的中央，而不是顶部。要解决此问题，请使用以下步骤:\n\n1.  在顶部面板和徽标布局之间添加另一个垂直间隔，这将抵消底部的间隔，平衡对齐。\n2.  如果您认为徽标贴得离登录表单太近，可以在徽标布局和登录表单布局之间添加一个垂直间隔。将其大小类型属性设置为固定，高度属性设置为`10`。\n3.  右键单击顶部面板的布局，然后选择变形为| QWidget。改名`topPanel`。布局必须转换为 QWidget，因为我们不能将样式表应用于布局，因为它除了边距之外没有任何属性。\n4.  主窗口的边缘有一点空白——我们不想要。要删除边距，请从“对象检查器”窗口(位于主窗口面板的正下方)中选择 centralWidget 对象，并将所有边距值设置为零。\n5.  通过单击“运行”按钮(带有绿色箭头图标)来运行项目，以查看程序的外观。如果一切顺利，你应该会看到这样的情况:\n\n![](img/aed96bee-2916-4602-98ab-36bfef320545.png)\n\n6.  让我们使用样式表来装饰用户界面！因为所有重要的小部件都被赋予了一个对象名，所以我们更容易从主窗口向其应用样式表，因为我们将只向主窗口写入样式表，并让它们继承层次树。\n7.  在对象检查器窗口中右键单击主窗口，然后选择更改样式表....\n8.  将以下代码添加到样式表中:\n\n```cpp\n#centralWidget { background: rgba(32, 80, 96, 100); }\n```\n\n9.  主窗口的背景会改变颜色。我们将在下面的*使用样式表*中的资源部分学习如何使用图像作为背景。所以颜色只是暂时的。\n10.  在 Qt 中，如果您想将样式应用到主窗口本身，您必须将它们应用到它的 centralWidget，而不是主窗口，因为窗口只是一个容器。\n11.  给顶部面板添加一个漂亮的渐变颜色:\n\n```cpp\n#topPanel { \n    background-color: qlineargradient(spread:reflect, x1:0.5, y1:0, x2:0, y2:0, stop:0 rgba(91, 204, 233, 100), stop:1 rgba(32, 80, 96, 100));\n}\n```\n\n12.  将黑色应用于登录表单，使其看起来半透明。我们还将通过设置`border-radius`属性使登录表单容器的角稍微变圆:\n\n```cpp\n#loginForm { \n background: rgba(0, 0, 0, 80);\n border-radius: 8px;\n}\n```\n\n13.  将样式应用于常规类型的小部件:\n\n```cpp\nQLabel { color: white; }\nQLineEdit { border-radius: 3px; }\n```\n\n14.  前面的样式表将所有标签的文本更改为白色，这也包括小部件上的文本，因为在内部，Qt 在有文本的小部件上使用相同类型的标签。此外，我们将线编辑小部件的角做得稍微圆一些。\n\n15.  将样式表应用于用户界面上的所有按钮:\n\n```cpp\nQPushButton {\n    color: white;\n    background-color: #27a9e3;\n    border-width: 0px;\n    border-radius: 3px;\n}\n```\n\n16.  前面的样式表将所有按钮的文本更改为白色，然后将其背景颜色设置为蓝色，并使其边角也稍微变圆。\n17.  为了将事情推得更远，我们将使用`hover`关键字使按钮的颜色在鼠标经过时发生变化:\n\n```cpp\nQPushButton:hover { background-color: #66c011; }\n```\n\n18.  当我们将鼠标悬停在按钮上时，前面的样式表会将按钮的背景颜色更改为绿色。我们将在下面的*自定义属性和子控件*部分对此进行更多讨论。\n19.  您可以进一步调整小部件的大小和边距，使它们看起来更好。记得删除登录表单的边框线，方法是删除我们之前在*步骤 6* 中直接应用到它的样式表。\n20.  您的登录屏幕应该如下所示:\n\n![](img/752e490b-bb46-4b90-b7b0-49e0225857a1.png)\n\n# 它是如何工作的...\n\n这个例子更侧重于 Qt 的布局系统。Qt 的布局系统允许我们的应用 GUI 通过排列每个小部件的子对象，在给定的空间内自动排列自己。前面示例中使用的间隔项有助于向外推动布局中包含的小部件，以沿着间隔项的宽度创建间距。\n\n要将小部件定位到布局的中间，请在布局中放置两个间隔项:一个在小部件的左侧，一个在小部件的右侧。然后，小部件将被两个间隔器推到布局的中间。\n\n# 在样式表中使用资源\n\nQt 为我们提供了一个独立于平台的资源系统，允许我们在程序的可执行文件中存储任何类型的文件以备后用。我们可以在可执行文件中存储的文件类型没有限制——图像、音频、视频、HTML、XML、文本文件、二进制文件等都是允许的。\n\n资源系统对于将资源文件(如图标和翻译文件)嵌入到可执行文件中非常有用，这样应用就可以随时访问它。为了实现这一点，我们必须在`.qrc`文件中告诉 Qt 我们想要将哪些文件添加到它的资源系统中，并且 Qt 将在构建过程中处理其余的文件。\n\n# 怎么做\n\n要向我们的项目添加新的`.qrc`文件，请转到文件|新文件或项目。然后，在文件和类类别下选择 Qt，并选择 Qt 资源文件。之后，给它一个名称(即`resources`)，然后单击“下一步”按钮，然后单击“完成”按钮。`.qrc`文件现在将由 Qt 创建者创建并自动打开。您不必直接以 XML 格式编辑`.qrc`文件，因为 Qt Creator 为您提供了管理资源的用户界面。\n\n要将图像和图标添加到项目中，您需要确保图像和图标位于项目的目录中。在 Qt 创建器中打开`.qrc`文件时，单击添加按钮，然后单击添加前缀按钮。**前缀**用于对您的资源进行分类，以便当您的项目中有大量资源时，可以更好地管理它们:\n\n1.  将刚刚创建的前缀重命名为`/icons`。\n2.  单击添加创建另一个前缀，然后单击添加前缀。\n3.  将新前缀重命名为`/images`。\n4.  选择`/icon`前缀并点击添加，然后点击添加文件。\n5.  将出现文件选择窗口；使用它来选择所有图标文件。按住键盘上的 *Ctrl* 键，同时单击文件进行选择，可以一次选择多个文件。完成后，单击打开。\n6.  选择`/images`前缀，点击添加按钮，然后点击添加文件按钮。文件选择窗口将再次弹出，这次我们将选择背景图像。\n7.  重复前面的步骤，但是这次我们将在`/images`前缀上添加标志图像。完成后，不要忘记按下 *Ctrl* + *S* 进行保存。您的`.qrc`文件现在应该如下所示:\n\n![](img/df4b08ff-6c12-4e47-89dc-ea9c0a0c42b6.png)\n\n8.  回到`mainwindow.ui`文件；让我们利用刚刚添加到项目中的资源。选择位于顶部面板的重启按钮。向下滚动属性编辑器，直到看到**图标**属性。单击带有下拉箭头图标的小按钮，然后从菜单中单击“选择资源”。\n\n9.  将弹出“选择资源”窗口。单击左侧面板上的图标前缀，并选择右侧面板上的重启图标。按确定。\n10.  按钮上会出现一个小图标。图标看起来非常小，因为默认图标大小设置为 16 x 16。将图标大小属性更改为`50 x 50`，您将看到图标变大。对关闭按钮重复上述步骤，除了这次，选择关闭图标。\n11.  这两个按钮现在应该如下所示:\n\n![](img/632042c6-72ee-4c7f-b954-3b031f8a2e65.png)\n\n12.  让我们使用添加到资源文件中的图像作为我们的徽标。选择徽标小部件，并移除我们之前添加的样式表以呈现其轮廓。\n13.  向下滚动属性编辑器，直到看到位图属性。\n14.  单击位图属性后面的小下拉按钮，并从菜单中选择“选择资源”。选择徽标图像，然后单击确定。徽标大小不再遵循您之前设置的尺寸；而是遵循图像的实际尺寸。我们不能改变它的尺寸，因为这只是 pixmap 属性的工作方式。\n15.  如果您想要更多地控制徽标的尺寸，可以从 pixmap 属性中移除图像，并改用样式表。您可以使用以下代码将图像应用于图标容器:\n\n```cpp\nborder-image: urlimg/logo.png);\n```\n\n16.  要获取图像的路径，请在文件列表窗口中右键单击图像的名称，然后选择复制路径。该路径将保存到操作系统的剪贴板中，现在您可以将其粘贴到前面的样式表中。使用此方法将确保图像符合您应用样式的小部件的尺寸。您的徽标现在应该如下图所示:\n\n![](img/5ec776c1-9afa-4d38-95db-a743a7489cce.png)\n\n17.  使用样式表将壁纸图像应用于背景。因为背景尺寸会根据窗口大小而变化，所以我们不能在这里使用 pixmap。相反，我们将在样式表中使用`border-image`属性。右键单击主窗口，然后选择更改样式表...打开“编辑样式表”窗口。我们将在 centralWidget 的样式表下添加一个新行:\n\n```cpp\n#centralWidget {\n    background: rgba(32, 80, 96, 100);\n    border-image: urlimg/login_bg.png);\n}\n```\n\n18.  真的那么简单容易！您的登录屏幕现在应该如下所示:\n\n![](img/0a384826-d4dd-4124-8585-9d81b96aec71.png)\n\n# 它是如何工作的...\n\nQt 中的资源系统在编译时将二进制文件(如图像和翻译文件)存储在可执行文件中。它读取项目中的资源集合文件(`.qrc`)来定位需要存储在可执行文件中的文件，并将它们包含在构建过程中。一个`.qrc`文件看起来是这样的:\n\n```cpp\n<!DOCTYPE RCC><RCC version=\"1.0\">\n <qresource>\n <file>img/copy.png</file>\n <file>img/cut.png</file>\n <file>img/new.png</file>\n <file>img/open.png</file>\n <file>img/paste.png</file>\n <file>img/save.png</file>\n </qresource>\n </RCC>\n```\n\n它使用 XML 格式存储资源文件的路径，这些路径与包含它们的目录相关。列出的资源文件必须与`.qrc`文件位于同一个目录，或其子目录之一。\n\n# 自定义属性和子控件\n\nQt 的样式表系统使我们能够轻松创建令人惊叹的专业外观的 ui。在本例中，我们将学习如何为小部件设置自定义属性，并使用它们在不同的样式之间切换。\n\n# 怎么做…\n\n让我们按照以下步骤自定义小部件属性和子控件:\n\n1.  让我们创建一个新的 Qt 项目。我为此准备了 UI。用户界面左侧包含三个按钮，右侧包含一个带有三个页面的选项卡小部件，如下图所示:\n\n![](img/39f8236c-c653-480e-bcad-e128d1d4c22b.png)\n\n2.  这三个按钮是蓝色的，因为我在主窗口中添加了以下样式表(而不是单个按钮):\n\n```cpp\nQPushButton {\n    color: white;\n    background-color: #27a9e3;\n    border-width: 0px;\n    border-radius: 3px;\n}\n```\n\n3.  我将通过向主窗口添加以下样式表来解释 Qt 中的伪状态，您可能很熟悉:\n\n```cpp\nQPushButton:hover {\n    color: white;\n    background-color: #66c011;\n    border-width: 0px;\n    border-radius: 3px;\n}\n```\n\n4.  我们在前面的*中使用了前面的样式表，使用样式表*创建一个登录屏幕，当有鼠标悬停事件时，使按钮改变颜色。这可以通过 Qt 样式表的伪状态来实现，在这种情况下，伪状态是由冒号将单词悬停与`QPushButton`类分开。每个小部件都有一组通用伪状态，如活动、禁用和启用，还有一组适用于其小部件类型的伪状态。例如`QPushButton`有开放、平坦等状态，但`QLineEdit`没有。让我们添加`pressed`伪状态，当用户点击按钮时，将按钮的颜色更改为`yellow`:\n\n```cpp\nQPushButton:pressed {\n    color: white;\n    background-color: yellow;\n    border-width: 0px;\n    border-radius: 3px;\n}\n```\n\n5.  **伪状态**允许用户根据适用的条件加载一组不同的样式表。Qt 通过在 Qt 样式表中实现动态属性进一步推进了这个概念。这允许我们在满足自定义条件时更改小部件的样式表。我们可以利用这个特性，根据我们可以在 Qt 中使用自定义属性设置的自定义条件来更改按钮的样式表。首先，我们将把这个样式表添加到我们的主窗口中:\n\n```cpp\nQPushButton[pagematches=true] {\n    color: white;\n    background-color: red;\n    border-width: 0px;\n    border-radius: 3px;\n}\n```\n\n6.  如果`pagematches`属性返回真，这将按钮的背景颜色变为红色。显然，`QPushButton`类中不存在这个属性。但是，我们可以使用`QObject::setProperty()`将其添加到我们的按钮中:\n\n```cpp\nui->button1->setProperty(\"pagematches\", true);\n```\n\n```cpp\nprivate slots:\nvoid on_tabWidget_currentChanged(int index);\n```\n\n```cpp\nvoid MainWindow::on_tabWidget_currentChanged(int index) {\n    // Set all buttons to false\n    ui->button1->setProperty(\"pagematches\", false);\n    ui->button2->setProperty(\"pagematches\", false);\n    ui->button3->setProperty(\"pagematches\", false);\n\n    // Set one of the buttons to true\n    if (index == 0)\n        ui->button1->setProperty(\"pagematches\", true);\n    else if (index == 1)\n        ui->button2->setProperty(\"pagematches\", true);\n    else\n        ui->button3->setProperty(\"pagematches\", true);\n\n    // Update buttons style\n    ui->button1->style()->polish(ui->button1);\n    ui->button2->style()->polish(ui->button2);\n    ui->button3->style()->polish(ui->button3);\n}\n```\n\n7.  前面的代码在 Tab Widget 切换当前页面时将所有三个按钮的`pagematches`属性设置为`false`。在我们决定哪个按钮应该变成红色之前，一定要重置一切。\n8.  检查事件信号提供的`index`变量，它会告诉你当前页面的索引号。根据索引号，将其中一个按钮的`pagematches`属性设置为`true`。\n\n9.  通过调用`polish()`刷新所有三个按钮的样式。您可能还想在`mainwindow.h`中添加以下标题:\n\n```cpp\n#include <QStyle>\n```\n\n10.  构建并运行项目。现在，每当您将选项卡小部件切换到不同的页面时，您应该会看到三个按钮变为红色。此外，当鼠标悬停时，按钮将变为绿色，当您单击它们时，按钮将变为黄色:\n\n![](img/3663a9ac-ac04-4795-a18a-f8d4334e2b01.png)\n\n# 它是如何工作的...\n\nQt 为用户提供了向任何类型的小部件添加自定义属性的自由。如果您想在满足特殊条件时更改特定的小部件，自定义属性非常有用，默认情况下，Qt 不提供这样的上下文。这允许用户扩展 Qt 的可用性，并使其成为定制解决方案的灵活工具。例如，如果我们的主窗口上有一行按钮，并且我们需要其中一个按钮根据选项卡小部件当前显示的页面来更改其颜色，则这些按钮不可能知道何时应该更改其颜色，因为 Qt 本身没有针对这种情况的内置上下文。为了解决这个问题，Qt 给了我们一个向小部件添加自己属性的方法，它使用了一个名为`QObject::setProperty()`的通用函数。要读取自定义属性，我们可以使用另一个名为`QObject::property()`的函数。\n\n接下来，我们将讨论 Qt 样式表中的子控件。通常，一个小部件不仅仅是一个单一的对象，而是多个对象或控件的组合，用于形成一个更复杂的小部件。这些对象被称为**子控件**。\n\n例如，一个旋转框小部件包含一个输入字段、一个向下按钮、一个向上按钮、一个向上箭头和一个向下箭头，这与其他一些小部件相比相当复杂。在这种情况下，如果我们愿意，Qt 允许我们使用样式表更改每个子控件，从而赋予我们更多的灵活性。我们可以通过在小部件的类名后面指定子控件的名称来做到这一点，用双冒号分隔。例如，如果我想将向下按钮的图像更改为旋转框，我可以编写如下样式表:\n\n```cpp\nQSpinBox::down-button {\n    image: urlimg/spindown.png);\n    subcontrol-origin: padding;\n    subcontrol-position: right bottom;\n}\n```\n\n这只会将图像应用到我的旋转框的向下按钮，而不会应用到小部件的任何其他部分。通过组合自定义属性、伪状态和子控件，Qt 为我们提供了一种非常灵活的方法来定制用户界面。\n\nVisit the following link to learn more about pseudo-states and sub-controls in Qt:\n[http://doc.qt.io/qt-5.12/stylesheet-reference.html](http://doc.qt.io/qt-5.12/stylesheet-reference.html).\n\n# Qt 建模语言中的样式(QML)\n\n**Qt 元语言**或 **Qt 建模语言** ( **QML** )是一种受 JavaScript 启发的用户界面标记语言，由 Qt 用来设计用户界面。Qt 为您提供 **Qt 快速组件**(由 QML 技术驱动的小部件)，无需 C++ 编程即可轻松设计触摸友好的用户界面。我们将按照下一节中给出的步骤，学习如何使用 QML 和 Qt 快速组件来设计我们程序的用户界面。\n\n# 怎么做…\n\n让我们按照以下步骤了解 QML 的造型:\n\n1.  通过转到文件|新建文件或项目来创建新项目。选择项目类别下的应用，然后选择 Qt 快速应用-空。\n2.  按选择...按钮，它会将您带到下一个窗口。插入项目名称，然后再次单击“下一步”按钮。\n3.  将出现另一个窗口，要求您选择所需的最低 Qt 版本。选择计算机上安装的最新版本，然后单击“下一步”。\n4.  再次单击“下一步”，然后单击“完成”。Qt Creator 现在将为您创建一个新项目。\n5.  QML 项目和 C++ Qt 项目之间有一些区别。您将在项目资源中看到一个`main.qml`文件。该`.qml`文件是使用 QML 标记语言编写的用户界面描述文件。如果您双击`main.qml`文件，Qt Creator 将打开脚本编辑器，您将看到如下内容:\n\n```cpp\nimport QtQuick 2.5\nimport QtQuick.Window 2.2\nWindow {\n    visible: true\n    width: 640\n    height: 480\n    title: qsTr(\"Hello World\")\n}\n```\n\n6.  这个文件告诉 Qt 创建一个 640 x 480 分辨率的空窗口，窗口标题写着`Hello World`。\n7.  如果你在你的项目中打开`main.cpp`文件，你会看到这行代码:\n\n```cpp\nQQmlApplicationEngine engine;\nengine.load(QUrl(QStringLiteral(\"qrc:/main.qml\")));\n```\n\n8.  前面的代码告诉 Qt 的 QML 引擎在程序启动时加载`main.qml`文件。如果你想加载另一个`.qml`文件，你知道去哪里找代码。\n9.  如果现在构建项目，得到的只是一个空窗口。要添加用户界面元素，让我们首先创建一个快速用户界面文件，方法是转到文件|新文件或项目，并在文件和类|快速用户界面类别下选择快速用户界面文件:\n\n![](img/4996fc8b-ff7f-4c5e-9b0e-2009033d64f5.png)\n\n10.  输入组件名称为`Main`，然后输入组件表单名称为`MainForm`。单击下一步，然后单击完成:\n\n![](img/b04cce86-83c4-4487-81dc-c84e0d943221.png)\n\n11.  名为`MainForm.ui.qml`的新文件已添加到您的项目资源中。如果 Qt 设计器(用户界面编辑器)在创建时没有自动打开`MainForm.ui.qml`文件，请尝试双击打开该文件。您将看到一个完全不同的用户界面编辑器，与我们在所有以前的食谱中所做的 C++ 项目相比。这个编辑器也叫 **Qt 快速设计器**；它是专门为编辑 QML 的用户界面而设计的。\n12.  当`main.qml`被 QML 引擎加载时，也会将`MainForm.ui.qml`导入到 UI 中，因为`MainForm`是在`main.qml`文件中被调用的。Qt 将根据命名约定通过搜索其`.qml`文件来检查`MainForm`是否是有效的用户界面。这个概念类似于我们之前所有食谱中的 C++ 项目；`main.qml`文件的作用类似于`main.cpp`文件，`MainForm.ui.qml`文件的作用类似于`MainWindow`类。也可以创建其他 UI 模板，在`main.qml`中使用。希望这种比较能让我们更容易理解 QML 是如何工作的。\n13.  打开`MainForm.ui.qml`。您应该只看到“导航”窗口中列出的一个项目:项目。Item 项基本上是窗口的基本布局，不应该被删除。它类似于我们在上一节中使用的 centralWidget。\n\n14.  画布现在真的是空的，让我们从左边的 QML 类型面板拖动鼠标区域和文本项到画布上。调整鼠标区域的大小以填充整个画布。此外，确保鼠标区域和文本项目都放置在导航器面板中的项目项目下，如下图所示:\n\n![](img/4a723aaa-44d0-4eeb-80bb-2a39adbc7a83.png)\n\n15.  M **ouse Area** 物品是无敌物品，当鼠标点击它，或者手指触摸它(对于移动平台)时就会被触发。鼠标区域项目也用于按钮组件，我们稍后将使用它。文本项是不言自明的:它是一个在应用上显示文本块的标签。\n\n16.  在导航窗口中，我们可以通过点击项目旁边类似眼睛的图标来隐藏或显示项目。当项目被隐藏时，它不会显示在画布上，也不会显示在编译的应用上。就像 C++ Qt 项目中的小部件一样，Qt 快速组件是基于父子关系以层次结构排列的。所有子项目都将以缩进位置放在父项目下。在我们的示例中，您可以看到“鼠标区域”和“文本”项目与“项目”项目相比稍微靠右，因为它们都是“项目”项目的子项目。我们可以通过使用导航窗口中的单击和拖动方法来重新排列父子关系以及它们在层次结构中的位置。您可以尝试单击文本项，并将其拖动到鼠标区域的顶部。然后，您将看到文本项改变了它的位置，现在位于鼠标区域下方，缩进更宽:\n\n![](img/b4ce2e6d-5991-49d1-90ad-01383722f3bf.png)\n\n17.  我们可以使用导航窗口顶部的箭头按钮来重新排列它们，如前面的截图所示。父项发生的任何事情也会影响它的所有子项，例如移动父项，隐藏和显示父项。\n\nYou can pan around the canvas view by holding the middle mouse button (or mouse scroll) while moving your mouse around. You can also zoom in and out by scrolling your mouse while holding the Ctrl key on your keyboard. By default, scrolling your mouse will move the canvas view up and down. However, if your mouse cursor is on top of the horizontal scroll bar of the canvas, scrolling the mouse will move the view to the left and right.\n\n18.  删除鼠标区域和文本项，因为我们将学习如何使用 QML 和 Qt Quick 从头开始创建用户界面。\n19.  将项目的大小设置为`800 x 600`，因为我们需要更大的空间来放置小部件。\n20.  将我们在之前的 C++ 项目中使用的图片，样式表中的*使用资源*配方，复制到 QML 项目的文件夹中，因为我们将使用 QML 重新创建相同的登录屏幕。\n21.  将图像添加到资源文件中，以便我们可以将它们用于用户界面。\n\n22.  打开 Qt 快速设计器并切换到“资源”窗口。单击背景图像并将其直接拖到画布上。切换到“属性”窗格上的“布局”选项卡，然后单击填充锚点按钮，此处用红色圆圈表示。这将使背景图像始终保持窗口大小:\n\n![](img/1bc2f0e5-191f-4752-813d-2ccd23debee6.png)\n\n23.  单击矩形组件并将其从“库”窗口拖到画布上。我们将把它作为我们项目的顶部面板。\n24.  对于顶部面板，启用顶部锚定、左侧锚定和右侧锚定，以便面板粘附到窗口顶部并遵循其宽度。确保所有边距都设置为零。\n25.  转到顶部面板的颜色属性，并选择渐变模式。设置第一种颜色为`#805bcce9`，第二种颜色为`#80000000`。这将创建一个带有蓝色渐变的半透明面板。\n26.  将文本小部件添加到画布中，并使其成为顶部面板的子组件。出于显示目的，将其文本属性设置为当前日期和时间(例如，2015 年 10 月 26 日星期一下午 3:14)。然后，将文本颜色设置为白色。\n27.  切换到布局选项卡，并启用顶部锚点和左侧锚点，这样文本小部件将始终粘附在屏幕的左上角。\n28.  在屏幕上添加鼠标区域，并将其大小设置为`50 x 50`。然后，通过在导航窗口中将它拖到顶部面板的顶部，使它成为顶部面板的子面板。\n29.  将鼠标区域的颜色设置为蓝色(`#27a9e3`)并将其半径设置为`2`，使其边角稍微变圆。启用顶部锚点和右侧锚点，使其贴在窗口的右上角。将顶部锚点的边距设置为`8`，将右侧锚点的边距设置为`10`，以创造一些空间。\n\n30.  打开“资源”窗口，将关闭图标拖到画布上。使它成为我们刚才创建的鼠标区域项目的子项目。然后，启用填充锚点，使其适合鼠标区域的大小。\n31.  唷，这是一个很大的步骤！现在，您的项目应该在导航窗口中按如下方式排列:\n\n![](img/8b24be52-dbc9-49c1-a921-4b66ce2f543a.png)\n\n32.  当主窗口改变大小时，父子关系和布局锚对于将小部件保持在正确的位置非常重要。您的顶部面板应该如下所示:\n\n![](img/8e695fc4-1ba7-43e0-8e55-9fb72c62516c.png)\n\n33.  让我们处理登录表单。通过从“资源库”窗口拖动矩形，将新矩形添加到画布中。将矩形调整到`360 x 200`并将其半径设置为`15`。\n34.  将其颜色设置为`#80000000`，将变为黑色，透明度为 50%。\n35.  启用垂直中心锚点和水平中心锚点，使矩形始终与窗口中心对齐。然后，将垂直中心锚的**边距**设置为`100`，使其稍微向下移动到底部，这样我们就有空间放置徽标了。下面的截图说明了锚点的设置:\n\n![](img/3ad38790-08d3-4963-8337-fc0b5631bb3b.png)\n\n36.  将文本对象添加到画布。使它们成为登录表单(矩形小部件)的子代，并将它们的文本属性设置为`Username:`和`Password:`。将它们的文本颜色更改为白色，并相应地对它们进行定位。这次我们不需要设置边距，因为它们会跟随矩形的位置。\n37.  向画布中添加两个文本输入对象，并将它们放在我们刚刚创建的文本小部件旁边。确保输入的文本也是登录表单的子项。由于文本输入不包含任何背景颜色属性，我们需要在画布上添加两个矩形作为背景。\n38.  在画布上添加两个矩形，并使每个矩形都成为我们刚刚创建的一个文本输入的子对象。将**半径**属性设置为`5`，给它们一些圆角。之后，在两个矩形上启用**填充锚点**，这样它们将跟随文本输入小部件的大小。\n39.  让我们在密码字段下面创建登录按钮。在画布上添加一个鼠标区域，使其成为登录表单的子窗体。将它调整到您喜欢的尺寸，并将其移动到位。\n40.  由于鼠标区域不包含任何背景颜色属性，我们需要添加一个矩形小部件，并使其成为鼠标区域的子对象。将矩形的颜色设置为蓝色(`#27a9e3`)并启用填充锚点，使其与鼠标区域非常匹配。\n41.  将文本对象添加到画布中，并使其成为登录按钮的子对象。将其文本颜色更改为白色，并将其文本属性设置为`Login`。最后，启用水平中心锚点和垂直中心锚点，使其与按钮的中心对齐。\n\n42.  您现在将获得一个登录表单，它看起来非常类似于我们在 C++ 项目中制作的表单:\n\n![](img/a67f9080-293e-49d3-b5bb-c57bfd555242.png)\n\n43.  该加 logo 了，其实很简单。打开“资源”窗口，将徽标图像拖到画布上。\n44.  使其成为登录表单的子表单，并将其大小设置为`512 x 200`。\n45.  把它放在登录表单的顶部，你就完成了。\n46.  这就是整个用户界面在编译时的样子。我们已经成功地从 C++ 项目中重新创建了登录屏幕，但是这次我们使用了 QML 和 Qt Quick:\n\n![](img/d169ffaa-adff-4ad5-8ef5-73fb3fff3c85.png)\n\n# 它是如何工作的...\n\n与表单编辑器相比，Qt 快速编辑器在应用中放置小部件时使用了非常不同的方法。用户可以决定哪种方法最适合他们的目的。以下截图显示了 Qt 快速设计器的外观:\n\n![](img/11238a8e-57c1-449f-83d8-2b358c115152.png)\n\n我们现在来看看编辑器用户界面的各种元素:\n\n1.  导航器:“导航器”窗口以树形结构显示当前 QML 文件中的项目。它类似于我们在前面的*中使用的另一个 Qt 设计器中的对象操作器窗口，使用带有 Qt 设计器的样式表*部分。\n2.  库:库窗口显示 QML 所有可用的 Qt 快速组件或 Qt 快速控制。您可以点按它并将其拖到画布窗口以添加到您的用户界面。您也可以创建自己的自定义 QML 组件并在此显示。\n\n3.  资源:“资源”窗口显示列表中的所有资源，这些资源可以在用户界面设计中使用。\n4.  导入:通过“导入”窗口，您可以将不同的 QML 模块导入到当前的 QML 文件中，例如蓝牙模块、网络工具包模块或定位模块，从而为 QML 项目添加附加功能。\n5.  属性窗格:类似于我们在前面的配方中使用的属性编辑器，QML 设计器中的属性窗格显示所选项目的属性。您也可以在代码编辑器中更改项的属性。\n6.  anvas:画布是您创建 QML 组件和设计应用的工作区域。\n7.  状态窗格:状态窗格显示 QML 项目中的不同状态，描述用户界面配置，如用户界面控件、它们的属性和行为以及可用的操作。\n8.  连接:这个面板是您为画布中的每个 QML 组件设置信号处理程序的地方，它支持 Qt 提供的信号和插槽机制。\n\n# 将 QML 对象指针暴露给 C++\n\n有时，我们希望通过 C++ 脚本修改 QML 对象的属性，例如更改标签的文本、隐藏/显示小部件或更改其大小。Qt 的 QML 引擎允许你将你的 QML 对象注册到 C++ 类型，这将自动公开它的所有属性。\n\n# 怎么做…\n\n我们想在 QML 创建一个标签，并偶尔更改其文本。为了将标签对象公开给 C++，我们可以执行以下步骤:\n\n1.  创建一个名为`MyLabel`的 C++ 类，它从`mylabel.h`中的`QObject`类扩展而来:\n\n```cpp\nclass MyLabel : public QObject {\nQ_OBJECT\npublic:\n    // Object pointer\n    QObject* myObject;\n    explicit MyLabel(QObject *parent = 0);\n\n    // Must call Q_INVOKABLE so that this function can be used in QML\n    Q_INVOKABLE void SetMyObject(QObject* obj);\n}\n```\n\n2.  在`mylabel.cpp`源文件中，定义一个名为`SetMyObject()`的函数保存对象指针。该功能稍后将在 QML 的`mylabel.cpp`中调用:\n\n```cpp\nvoid MyLabel::SetMyObject(QObject* obj) {\n    // Set the object pointer\n    myObject = obj;\n}\n```\n\n3.  在`main.cpp`中，包括`MyLabel`标题，并使用\n    `qmlRegisterType()`功能将其注册到 QML 发动机:\n\n```cpp\ninclude \"mylabel.h\"\nint main(int argc, char *argv[]) {\n    // Register your class to QML\n    qmlRegisterType<MyLabel>(\"MyLabelLib\", 1, 0, \"MyLabel\");\n}\n```\n\n4.  注意`qmlRegisterType()`中需要声明四个参数。除了声明你的类名(`MyLabel`)之外，还需要声明你的库名(`MyLabelLib`)及其版本(1.0)，该版本将用于将你的类导入到 QML。\n5.  将 QML 引擎映射到我们在 QML 的标签对象，并通过在我们的 QML 文件中调用`import MyLabelLib 1.0`来导入我们之前在*步骤 3* 中定义的类库。请注意，库名及其版本号必须与您在`main.cpp`中声明的匹配，否则将引发错误。在 QML 声明`MyLabel`并将其标识设置为`mylabels`后，在标签初始化后，调用`mylabel.SetMyObject(myLabel)`将其指针暴露给 C/C++ :\n\n```cpp\nimport MyLabelLib 1.0\nApplicationWindow {\n    id: mainWindow\n    width: 480\n    height: 640\n    MyLabel {\n        id: mylabel\n    }\n    Label {\n        id: helloWorldLabel\n        text: qsTr(\"Hello World!\")\n        Component.onCompleted: {\n            mylabel.SetMyObject(hellowWorldLabel);\n        }\n    }\n}\n```\n\n6.  等到标签完全初始化后，再将其指针暴露给 C/C++，否则可能会导致程序崩溃。为了确保它被完全启动，在`Component.onCompleted`中调用`SetMyObject()`函数，而不是在任何其他函数或事件回调中。现在 QML 标签已经暴露给 C/C++，我们可以通过调用`setProperty()`函数来改变它的任何属性。例如，我们可以将其可见性设置为`true`并将文本更改为`Bye bye world!`:\n\n```cpp\n// QVariant automatically detects your data type\nmyObject->setProperty(\"visible\", QVariant(true));\nmyObject->setProperty(\"text\", QVariant(\"Bye bye world!\"));\n```\n\n7.  除了更改属性，我们还可以通过调用以下内容来调用它的函数:\n\n```cpp\nQMetaObject::invokeMethod():\nQVariant returnedValue;\nQVariant message = \"Hello world!\";\nQMetaObject::invokeMethod(myObject, \"myQMLFunction\", Q_RETURN_ARG(QVariant, returnedValue), Q_ARG(QVariant, message));\nqDebug() << \"QML function returned:\" << returnedValue.toString();\n```\n\n8.  或者，简单地说，如果我们不期望从函数中返回任何值，我们可以用两个参数调用`invokedMethod()`函数:\n\n```cpp\nQMetaObject::invokeMethod(myObject, \"myQMLFunction\");\n```\n\n# 它是如何工作的...\n\nQML 的设计使得它可以通过 C++ 代码进行扩展。Qt QML 模块中的类允许 QML 对象从 C++ 中使用和操作，QML 引擎与 Qt 元对象系统相结合的能力允许从 QML 直接调用 C++ 功能。要向 QML 添加一些 C++ 数据或用法，它应该来自一个 QObject 派生类。QML 对象类型可以从 C++ 中创建，并在监督下访问它们的属性，调用它们的方法，并获得它们的信号警报。这是可能的，因为所有 QML 对象类型都是使用 QObject 派生类执行的，允许 QML 引擎通过 Qt 元对象系统强制加载和检查对象。"
  },
  {
    "path": "docs/qt5-cpp-gui-prog-cb/02.md",
    "content": "# 二、事件处理——信号和插槽\n\n在本章中，我们将介绍以下食谱:\n\n*   新的信号和时隙语法\n*   带有信号和插槽的用户界面事件\n*   异步编程变得更加容易\n*   函数回调\n\n# 介绍\n\nQt 5 中的信号和时隙机制是它最重要的特性之一。这是一种允许对象间通信的方法，是程序图形用户界面的关键部分。信号可以从任何`QObject`对象或其子类发出，这将触发连接到信号的任何对象的任何槽功能。\n\n与回调(Qt 5 也支持)相比，信号和槽机制对于程序员来说使用起来更加灵活。信号和槽机制都是类型安全的，并且没有强耦合到处理函数，这使得它比回调的实现更好。\n\nA signal of an arbitrary class can trigger any private slots of an unrelated class that is going to be invoked, which is not possible with callbacks.\n\n# 技术要求\n\n本章的技术要求包括 Qt 5.11.2 MinGW 32 位、Qt Creator 4.8.2 和 Windows 10。\n\n本章使用的所有代码可从以下 GitHub 链接下载:[https://GitHub . com/PacktPublishing/Qt5-CPP-GUI-Programming-cook book-第二版/树/主/第 02 章](https://github.com/PacktPublishing/Qt5-CPP-GUI-Programming-Cookbook-Second-Edition/tree/master/Chapter02)。\n\n查看以下视频，查看正在运行的代码:[http://bit.ly/2FozGKA](http://bit.ly/2FozGKA)\n\n# 新的信号和时隙语法\n\n信号和时隙机制在 Qt 的最新版本中经历了一些变化，最明显的是它的编码语法。Qt 5 在未来的版本中继续支持旧的语法，但是没有提到它要过多久才会被完全删除。因此，我们最好在那一天到来之前开始学习新的语法。\n\n# 怎么做...\n\n让我们从以下步骤开始:\n\n1.  让我们创建一个 Qt 小部件应用项目并打开`mainwindow.ui`。\n2.  将按钮从小部件框拖放到用户界面画布上:\n\n![](img/c3e7e642-5532-4e26-8608-415d253d8ccd.png)\n\n3.  右键单击按钮并选择转到插槽。将出现一个窗口:\n\n![](img/3d241e84-ecf4-41c8-bd2b-cf470c836f75.png)\n\n4.  您将看到按钮可用的内置插槽功能列表。让我们选择单击的()选项，然后按确定。名为`on_pushButton_clicked()`的槽函数现在将同时出现在`mainwindow.h`和`mainwindow.cpp`中。Qt Creator 会在您按下“转到插槽”窗口上的“确定”按钮后，自动将插槽功能添加到您的源代码中。如果您现在查看您的`mainwindow.h`，您应该能够在`private slots`关键字下看到一个额外的功能:\n\n```cpp\nclass MainWindow : public QMainWindow {\n    Q_OBJECT\npublic:\n    explicit MainWindow(QWidget *parent = 0);\n    ~MainWindow();\nprivate slots:\n void on_pushButton_clicked();\nprivate:\n    Ui::MainWindow *ui;\n};\n```\n\n5.  `mainwindow.cpp`也是如此，这里已经为你添加了`on_pushButton_clicked()`功能:\n\n```cpp\nvoid MainWindow::on_pushButton_clicked()\n{\n}\n```\n\n6.  现在，让我们在您的源文件顶部添加一个`QMessageBox`标题:\n\n```cpp\n#include <QMessageBox>\n```\n\n7.  然后，在`on_pushButton_clicked()`功能中添加以下代码:\n\n```cpp\nvoid MainWindow::on_pushButton_clicked() {\n    QMessageBox::information(this, \"Hello\", \"Button has been clicked!\");\n}\n```\n\n8.  现在，构建并运行该项目。然后，点击按钮；您应该会看到弹出一个消息框:\n\n![](img/d1743068-302c-450f-8a9a-6c95b4e6a925.png)\n\n9.  接下来，我们想创建自己的信号和槽函数。转到文件|新文件或项目，然后在文件和类类别下创建一个新的 C++ 类:\n\n![](img/fa6a7db3-df92-409a-814c-e36662790168.png)\n\n10.  然后，我们需要命名我们的类`MyClass`，并确保基类是 QObject:\n\n![](img/bd638556-6b13-47dd-95f9-c623cdf6eb60.png)\n\n11.  创建完类后，打开`myclass.h`并添加以下代码，为了清楚起见，这里突出显示了这些代码:\n\n```cpp\n#include <QObject>\n#include <QMainWindow>\n#include <QMessageBox>\nclass MyClass : public QObject {\n    Q_OBJECT\npublic:\n    explicit MyClass(QObject *parent = nullptr);\nsignals:\npublic slots:\n void doSomething();\n};\n```\n\n12.  然后，打开`myclass.cpp`，实现`doSomething()`槽功能。我们将复制前面例子中的消息框函数:\n\n```cpp\n#include \"myclass.h\"\n\nMyClass::MyClass(QObject *parent) : QObject(parent) {}\nvoid MyClass::doSomething() {\n    QMessageBox::information(this, \"Hello\", \"Button has been clicked!\");\n}\n```\n\n13.  现在，打开`mainwindow.h`并在顶部包含`myclass.h`标题:\n\n```cpp\n#ifndef MAINWINDOW_H\n#define MAINWINDOW_H\n\n#include \"myclass.h\"\n\nnamespace Ui {\nclass MainWindow;\n}\n```\n\n14.  另外，在`myclass.h`中声明一个`doNow()`信号:\n\n```cpp\nsignals:\n void doNow();\n\nprivate slots:\n    void on_pushButton_clicked();\n```\n\n15.  之后，打开`mainwindow.cpp`并定义一个`MyClass`对象。然后，我们将把上一步创建的`doNow()`信号与我们的`doSomething()`插槽功能连接起来:\n\n```cpp\nMainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow){\n    ui->setupUi(this);\n MyClass* myclass = new MyClass;\n connect(this, &MainWindow::doNow, myclass, &MyClass::doSomething);\n}\n```\n\n16.  然后，我们必须将`on_pushButton_clicked()`函数的代码更改如下:\n\n```cpp\nvoid MainWindow::on_pushButton_clicked() {\n emit doNow();\n}\n```\n\n17.  如果您现在构建并运行该程序，您将获得与上一个示例类似的结果。但是，我们将消息框代码放在了`MyClass`对象中，而不是`MainWindow`中。\n\n# 它是如何工作的...\n\n在过去，我们通常会将信号连接到这样的插槽:\n\n```cpp\nconnect(\n    sender, SIGNAL(valueChanged(QString)),\n    receiver, SLOT(updateValue(QString))\n);\n```\n\n然而，从那以后情况略有变化。在新的语法中，`SIGNAL`和`SLOT`宏现在已经不存在了，您必须指定对象的类型，如下面的代码所示:\n\n```cpp\nconnect(\n    sender, &Sender::valueChanged,\n    receiver, &Receiver::updateValue\n);\n```\n\n新语法还允许您将信号直接连接到函数，而不是`QObject`:\n\n```cpp\nconnect(\n    sender, &Sender::valueChanged, myFunction\n);\n```\n\n此外，您还可以将信号连接到 lambda 表达式。我们将在*异步编程变得更容易*食谱中更多地讨论这一点。\n\n# 带有信号和插槽的用户界面事件\n\n在前面的示例中，我们演示了按钮上信号和插槽的使用。现在，让我们来探索其他常见小部件类型中可用的信号和插槽。\n\n# 怎么做...\n\n要了解如何将信号和插槽用于用户界面事件，请执行以下步骤:\n\n1.  让我们创建一个新的 Qt 小部件应用项目。\n2.  将按钮、组合框、线条编辑、旋转框和滑块从小部件框拖放到用户界面画布中:\n\n![](img/7f7e6543-927f-46af-9e78-54b2bbcc2a48.png)\n\n3.  然后，右键点击按钮，选择`clicked()`，按确定按钮继续。Qt 创建者将为您创建一个槽函数:\n\n![](img/be38cf6a-53d4-444a-a4a9-463859b54c89.png)\n\n4.  重复上一步，但这一次，选择下一个选项，直到`QAbstractButton`中的每个函数都被添加到源代码中:\n\n```cpp\nvoid on_pushButton_clicked();\nvoid on_pushButton_clicked(bool checked);\nvoid on_pushButton_pressed();\nvoid on_pushButton_released();\nvoid on_pushButton_toggled(bool checked);\n```\n\n5.  接下来，在组合框上再次重复相同的步骤，直到`QComboBox`类下所有可用的槽函数都被添加到源代码中:\n\n```cpp\nvoid on_comboBox_activated(const QString &arg1);\nvoid on_comboBox_activated(int index);\nvoid on_comboBox_currentIndexChanged(const QString &arg1);\nvoid on_comboBox_currentIndexChanged(int index);\nvoid on_comboBox_currentTextChanged(const QString &arg1);\nvoid on_comboBox_editTextChanged(const QString &arg1);\nvoid on_comboBox_highlighted(const QString &arg1);\nvoid on_comboBox_highlighted(int index);\n```\n\n6.  `lineEdit`也是如此，都属于`QLineEdit`类:\n\n```cpp\nvoid on_lineEdit_cursorPositionChanged(int arg1, int arg2);\nvoid on_lineEdit_editingFinished();\nvoid on_lineEdit_returnPressed();\nvoid on_lineEdit_selectionChanged();\nvoid on_lineEdit_textChanged(const QString &arg1);\nvoid on_lineEdit_textEdited(const QString &arg1);\n```\n\n7.  之后，也为我们的旋转盒小部件添加来自`QSpinBox`类的槽函数，它相对较短:\n\n```cpp\nvoid on_spinBox_valueChanged(const QString &arg1);\nvoid on_spinBox_valueChanged(int arg1);\n```\n\n8.  最后，对我们的滑块小部件重复相同的步骤，得到类似的结果:\n\n```cpp\nvoid on_horizontalSlider_actionTriggered(int action);\nvoid on_horizontalSlider_rangeChanged(int min, int max);\nvoid on_horizontalSlider_sliderMoved(int position);\nvoid on_horizontalSlider_sliderPressed();\nvoid on_horizontalSlider_sliderReleased();\nvoid on_horizontalSlider_valueChanged(int value);\n```\n\n9.  完成后，打开`mainwindow.h`并添加`QDebug`标题，如以下代码所示:\n\n```cpp\n#ifndef MAINWINDOW_H\n#define MAINWINDOW_H\n\n#include <QMainWindow>\n#include <QDebug>\n\nnamespace Ui {\nclass MainWindow;\n}\n```\n\n10.  让我们实现按钮的插槽功能:\n\n```cpp\nvoid MainWindow::on_pushButton_clicked() {\n qDebug() << \"Push button clicked\";\n}\nvoid MainWindow::on_pushButton_clicked(bool checked) {\n qDebug() << \"Push button clicked: \" << checked;\n}\nvoid MainWindow::on_pushButton_pressed() {\n qDebug() << \"Push button pressed\";\n}\nvoid MainWindow::on_pushButton_released() {\n qDebug() << \"Push button released\";\n}\nvoid MainWindow::on_pushButton_toggled(bool checked) {\n qDebug() << \"Push button toggled: \" << checked;\n}\n```\n\n11.  如果您现在构建并运行项目，然后单击按钮，您将看到一个不同的状态被打印出来，但时间略有不同。这是因为在整个点击过程中，不同的动作会发出不同的信号:\n\n```cpp\nPush button pressed\nPush button released\nPush button clicked\nPush button clicked: false\n```\n\n12.  接下来，我们将进入组合框。由于默认的组合框是空的，让我们通过从`mainwindow.ui`双击它并添加弹出窗口中显示的选项来添加一些选项:\n\n![](img/f1cfe993-c837-420e-9d74-b80382bb1be8.png)\n\n13.  然后，我们来实现`mainwindow.cpp`中组合框的槽功能:\n\n```cpp\nvoid MainWindow::on_comboBox_activated(const QString &arg1) {\n qDebug() << \"Combo box activated: \" << arg1;\n}\nvoid MainWindow::on_comboBox_activated(int index) {\n qDebug() << \"Combo box activated: \" << index;\n}\nvoid MainWindow::on_comboBox_currentIndexChanged(const QString &arg1) {\n qDebug() << \"Combo box current index changed: \" << arg1;\n}\nvoid MainWindow::on_comboBox_currentIndexChanged(int index) {\n qDebug() << \"Combo box current index changed: \" << index;\n}\n```\n\n14.  我们将继续为组合框实现剩余的槽函数:\n\n```cpp\nvoid MainWindow::on_comboBox_currentTextChanged(const QString &arg1) {\n qDebug() << \"Combo box current text changed: \" << arg1;\n}\nvoid MainWindow::on_comboBox_editTextChanged(const QString &arg1) {\n qDebug() << \"Combo box edit text changed: \" << arg1;\n}\nvoid MainWindow::on_comboBox_highlighted(const QString &arg1) {\n qDebug() << \"Combo box highlighted: \" << arg1;\n}\nvoid MainWindow::on_comboBox_highlighted(int index) {\n qDebug() << \"Combo box highlighted: \" << index;\n}\n```\n\n15.  构建并运行项目。然后，尝试单击组合框，将鼠标悬停在其他选项上，并通过单击选择一个选项。您应该会在调试输出中看到类似于以下内容的结果:\n\n```cpp\nCombo box highlighted: 0\nCombo box highlighted: \"Option One\"\nCombo box highlighted: 1\nCombo box highlighted: \"Option Two\"\nCombo box highlighted: 2\nCombo box highlighted: \"Option Three\"\nCombo box current index changed: 2\nCombo box current index changed: \"Option Three\"\nCombo box current text changed: \"Option Three\"\nCombo box activated: 2\nCombo box activated: \"Option Three\"\n```\n\n16.  接下来，我们将继续进行行编辑并实现它的槽函数，如下面的代码所示:\n\n```cpp\nvoid MainWindow::on_lineEdit_cursorPositionChanged(int arg1, int arg2) {\n qDebug() << \"Line edit cursor position changed: \" << arg1 << arg2;\n}\nvoid MainWindow::on_lineEdit_editingFinished() {\n qDebug() << \"Line edit editing finished\";\n}\nvoid MainWindow::on_lineEdit_returnPressed() {\n qDebug() << \"Line edit return pressed\";\n}\n```\n\n17.  我们将继续实现行编辑的剩余槽功能:\n\n```cpp\nvoid MainWindow::on_lineEdit_selectionChanged() {\n qDebug() << \"Line edit selection changed\";\n}\nvoid MainWindow::on_lineEdit_textChanged(const QString &arg1) {\n qDebug() << \"Line edit text changed: \" << arg1;\n}\nvoid MainWindow::on_lineEdit_textEdited(const QString &arg1) {\n qDebug() << \"Line edit text edited: \" << arg1;\n}\n```\n\n18.  构建并运行项目。然后，点击线路编辑并输入`Hey`。您应该会在调试输出面板上看到类似如下的结果:\n\n```cpp\nLine edit cursor position changed: -1 0\nLine edit text edited: \"H\"\nLine edit text changed: \"H\"\nLine edit cursor position changed: 0 1\nLine edit text edited: \"He\"\nLine edit text changed: \"He\"\nLine edit cursor position changed: 1 2\nLine edit text edited: \"Hey\"\nLine edit text changed: \"Hey\"\nLine edit cursor position changed: 2 3\nLine edit editing finished\n```\n\n19.  之后，我们需要为旋转框小部件实现 slot 函数，如下面的代码所示:\n\n```cpp\nvoid MainWindow::on_spinBox_valueChanged(const QString &arg1){\n qDebug() << \"Spin box value changed: \" << arg1;\n}\nvoid MainWindow::on_spinBox_valueChanged(int arg1) {\n qDebug() << \"Spin box value changed: \" << arg1;\n}\n```\n\n20.  尝试构建和运行程序。然后，单击数字显示框上的箭头按钮，或者直接编辑框中的值–您应该会看到类似的内容:\n\n```cpp\nSpin box value changed: \"1\"\nSpin box value changed: 1\nSpin box value changed: \"2\"\nSpin box value changed: 2\nSpin box value changed: \"3\"\nSpin box value changed: 3\nSpin box value changed: \"2\"\nSpin box value changed: 2\nSpin box value changed: \"20\"\nSpin box value changed: 20\n```\n\n21.  最后，我们将实现水平滑块小部件的插槽功能:\n\n```cpp\nvoid MainWindow::on_horizontalSlider_actionTriggered(int action) {\n qDebug() << \"Slider action triggered\" << action;\n}\nvoid MainWindow::on_horizontalSlider_rangeChanged(int min, int max) {\n qDebug() << \"Slider range changed: \" << min << max;\n}\nvoid MainWindow::on_horizontalSlider_sliderMoved(int position) {\n qDebug() << \"Slider moved: \" << position;\n}\n```\n\n22.  继续为滑块实现插槽功能，如以下代码所示:\n\n```cpp\nvoid MainWindow::on_horizontalSlider_sliderPressed() {\n qDebug() << \"Slider pressed\";\n}\nvoid MainWindow::on_horizontalSlider_sliderReleased() {\n qDebug() << \"Slider released\";\n}\nvoid MainWindow::on_horizontalSlider_valueChanged(int value) {\n qDebug() << \"Slider value changed: \" << value;\n}\n```\n\n23.  构建并运行程序。然后，单击并向左和向右拖动滑块–您应该会看到类似以下的结果:\n\n```cpp\nSlider pressed\nSlider moved: 1\nSlider action triggered 7\nSlider value changed: 1\nSlider moved: 2\nSlider action triggered 7\nSlider value changed: 2\nSlider moved: 3\nSlider action triggered 7\nSlider value changed: 3\nSlider moved: 4\nSlider action triggered 7\nSlider value changed: 4\nSlider released\n```\n\n# 它是如何工作的...\n\n几乎每个小部件都有一组与其用途或目的相关的插槽功能。例如，当按钮被按下或释放时，它将开始发出触发与其相关的插槽功能的信号。定义小部件的这些预期行为具有槽函数，当用户触发一个动作时，这些槽函数被调用。作为程序员，我们所需要做的就是实现槽函数，并告诉 Qt 当这些槽函数被触发时该做什么。\n\n# 异步编程变得更加容易\n\n由于信号和槽机制本质上是异步的，我们可以将它用于用户界面之外的事情。在编程术语中，异步操作是一个独立工作的进程，允许程序继续其操作，而无需等待进程完成，这可能会使整个程序停滞。\n\nQt 5 允许您利用它的信号和槽机制轻松实现异步进程，而无需太多努力。在 Qt 5 引入了信号和槽的新语法后，这一点更加真实，新语法允许信号从`QObject`触发正常功能，而不是槽功能。\n\n在下面的例子中，我们将进一步探索这个机会，并学习如何通过 Qt 5 提供的信号和槽机制使用异步操作来提高程序的效率。\n\n# 怎么做...\n\n要了解如何使用信号和槽机制实现异步操作，让我们遵循以下示例:\n\n1.  创建 Qt 控制台应用项目:\n\n![](img/849ef28e-8e5a-4692-8638-edb29b347cff.png)\n\n2.  这种类型的项目只会为您提供一个`main.cpp`文件，而不是像我们之前的示例项目那样提供`mainwindow.h`和`mainwindow.cpp`。让我们打开`main.cpp`并添加以下标题:\n\n```cpp\n#include <QNetworkAccessManager>\n#include <QNetworkReply>\n#include <QDebug>\n```\n\n3.  然后，将以下代码添加到我们的`main()`函数中。我们将使用`QNetworkAccessManager`类向以下网址发起`GET`请求:\n\n```cpp\nint main(int argc, char *argv[]) {\n    QCoreApplication a(argc, argv);\n\n    QString *html = new QString;\n    qDebug() << \"Start\";\n\n    QNetworkAccessManager manager;\n    QNetworkRequest req(QUrl(\"http://www.dustyfeet.com\"));\n    QNetworkReply* reply = manager.get(req);\n```\n\n4.  然后，我们使用 C++ 11 的 lambda 表达式将`QNetworkReply`信号连接到内联函数:\n\n```cpp\n    QObject::connect(reply, &QNetworkReply::readyRead,\n    [reply, html]() {\n        html->append(QString(reply->readAll()));\n    });\n\n    QObject::connect(reply, &QNetworkReply::downloadProgress,\n    [reply](qint64 bytesReceived, qint64 bytesTotal) {\n        qDebug() << \"Progress: \" << bytesReceived << \"bytes /\" << bytesTotal << \"bytes\";\n    });\n```\n\n5.  我们也可以使用带有`connect()`的 lambda 表达式来调用不在`QObject`类下的函数:\n\n```cpp\n    QObject::connect(reply, &QNetworkReply::finished,\n    [=]() {\n        printHTML(*html);\n    });\n\n    return a.exec();\n}\n```\n\n6.  最后，我们定义`printHTML()`函数，如下代码所示:\n\n```cpp\nvoid printHTML(QString html) {\n    qDebug() << \"Done\";\n    qDebug() << html;\n}    \n```\n\n7.  如果您现在构建并运行该程序，您将会看到它即使没有声明任何槽函数也是有效的。Lambda 表达式使声明一个槽函数成为可选的，但是只有当您的代码非常短时才建议这样做:\n\n![](img/179c9d5e-18ce-47bc-97fc-f298288519c4.png)\n\n# 它是如何工作的...\n\n前面的例子是一个非常简单的应用，展示了使用 lambda 表达式将信号与 lambda 函数或正则函数连接起来，而不需要声明任何 slot 函数，因此不需要从`QObject`类继承。这对于调用不在 UI 对象下的异步进程特别有用。\n\nLambda 表达式是在另一个函数中匿名定义的函数，这与 JavaScript 中的匿名函数非常相似。lambda 函数的格式如下所示:\n\n```cpp\n[captured variables](arguments) {\n    lambda code\n}\n```\n\n您可以通过将变量放入捕获的变量部分来将变量插入 lambda 表达式，就像我们在本食谱的示例项目中所做的那样。我们捕获名为`reply`的`QNetworkReply`对象和名为`html`的`QString`对象，并将它们放入我们的λ表达式中。\n\n然后，我们可以在 lambda 代码中使用这些变量，如下面的代码所示:\n\n```cpp\n[reply, html]() {\n    html->append(QString(reply->readAll()));\n}\n```\n\n参数部分类似于一个普通的函数，您可以向参数输入值，并在 lambda 代码中使用它们。在这种情况下，`bytesReceived`和`bytesTotal`的值来自`downloadProgress`信号:\n\n```cpp\nQObject::connect(reply, &QNetworkReply::downloadProgress,\n[reply](qint64 bytesReceived, qint64 bytesTotal) {\n    qDebug() << \"Progress: \" << bytesReceived << \"bytes /\" << bytesTotal << \"bytes\";\n});\n```\n\n您还可以使用等号捕获函数中使用的所有变量。在这种情况下，我们捕获了`html`变量，但没有在捕获的变量区域中指定它:\n\n```cpp\n[=]() {\n    printHTML(*html);\n}\n```\n\n# 函数回调\n\n尽管 Qt 5 支持信号和插槽机制，但 Qt 5 中的一些功能仍然使用函数回调，如键盘输入、窗口大小调整、图形绘制等。由于这些事件只需要实现一次，因此不需要使用信号和时隙机制。\n\n# 怎么做...\n\n让我们从这个例子开始:\n\n1.  创建`Qt Widgets Application`项目，打开`mainwindow.h`，添加如下标题:\n\n```cpp\n#include <QDebug>\n#include <QResizeEvent>\n#include <QKeyEvent>\n#include <QMouseEvent>\n```\n\n2.  然后，在`mainwindow.h`中声明这些函数:\n\n```cpp\npublic:\n    explicit MainWindow(QWidget *parent = 0);\n    ~MainWindow();\n\n void resizeEvent(QResizeEvent *event);\n void keyPressEvent(QKeyEvent *event);\n void keyReleaseEvent(QKeyEvent *event);\n void mouseMoveEvent(QMouseEvent *event);\n void mousePressEvent(QMouseEvent *event);\n void mouseReleaseEvent(QMouseEvent *event);\n```\n\n3.  之后，打开`mainwindow.cpp`，将以下代码添加到类构造函数中:\n\n```cpp\nMainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) {\n    ui->setupUi(this);\n this->setMouseTracking(true);\n ui->centralWidget->setMouseTracking(true);\n}\n```\n\n4.  然后，定义`resizeEvent()`和`keyPressedEvent()`功能:\n\n```cpp\nvoid MainWindow::resizeEvent(QResizeEvent *event) {\n    qDebug() << \"Old size:\" << event->oldSize() << \", New size:\" << event->size();\n}\n\nvoid MainWindow::keyPressEvent(QKeyEvent *event) {\n    if (event->key() == Qt::Key_Escape) {\n        this->close();\n    }\n    qDebug() << event->text() << \"has been pressed\";\n}\n```\n\n5.  继续执行其余功能:\n\n```cpp\nvoid MainWindow::keyReleaseEvent(QKeyEvent *event) {\n    qDebug() << event->text() << \"has been released\";\n}\n\nvoid MainWindow::mouseMoveEvent(QMouseEvent *event) {\n    qDebug() << \"Position: \" << event->pos();\n}\n\nvoid MainWindow::mousePressEvent(QMouseEvent *event) {\n    qDebug() << \"Mouse pressed:\" << event->button();\n}\n\nvoid MainWindow::mouseReleaseEvent(QMouseEvent *event) {\n    qDebug() << \"Mouse released:\" << event->button();\n}\n```\n\n6.  构建并运行程序。然后，试着移动鼠标，重新缩放主窗口，按下键盘上的一些随机键，最后按下键盘上的 *Esc* 键关闭程序。您应该会看到类似于应用输出窗口中打印出来的调试文本:\n\n```cpp\nOld size: QSize(-1, -1) , New size: QSize(400, 300)\nOld size: QSize(400, 300) , New size: QSize(401, 300)\nOld size: QSize(401, 300) , New size: QSize(402, 300)\nPosition: QPoint(465,348)\nPosition: QPoint(438,323)\nPosition: QPoint(433,317)\n\"a\" has been pressed\n\"a\" has been released\n\"r\" has been pressed\n\"r\" has been released\n\"d\" has been pressed\n\"d\" has been released\n\"\\u001B\" has been pressed\n```\n\n# 它是如何工作的...\n\nQt 5 对象，尤其是主窗口，有十几个作为虚函数存在的内置回调。这些函数可以被覆盖，以便在被调用时执行预期的行为。Qt 5 可能会在满足预期条件时调用这些回调函数，如按下键盘按钮、移动鼠标光标、调整窗口大小等。\n\n我们在`mainwindow.h`文件中声明的函数是构建在`QWidget`类中的虚拟函数。我们只是用我们自己的代码覆盖它，以定义它被调用时的新行为。\n\n请注意，您必须为主窗口和中央窗口调用`setMouseTracking(true)`，以使`mouseMoveEvent()`回调生效。"
  },
  {
    "path": "docs/qt5-cpp-gui-prog-cb/03.md",
    "content": "# 三、Qt 和 QML 的状态和动画\n\n本章将涵盖以下食谱:\n\n*   Qt 中的属性动画\n*   使用缓和曲线控制属性动画\n*   创建动画组\n*   创建嵌套动画组\n*   Qt 中的状态机\n*   QML 的状态、过渡和动画\n*   使用动画师的动画小部件属性\n*   雪碧动画\n\n# 介绍\n\nQt 通过其强大的动画框架提供了一种简单的方法来动画小部件或继承`QObject`类的任何其他对象。动画可以单独使用，也可以与状态机框架一起使用，状态机框架允许根据小部件的当前活动状态播放不同的动画。Qt 的动画框架\n也支持分组动画，可以同时移动多个图形项，或者依次移动。\n\n# 技术要求\n\n本章的技术要求包括 Qt 5.11.2 MinGW 32 位、Qt Creator 4.8.2 和 Windows 10。\n\n本章使用的所有代码都可以从下面的 GitHub 链接下载:[https://GitHub . com/PacktPublishing/Qt5-CPP-GUI-编程-cook book-第二版/树/master/Chapter03](https://github.com/PacktPublishing/Qt5-CPP-GUI-Programming-Cookbook-Second-Edition/tree/master/Chapter03) 。\n\n查看以下视频，查看正在运行的代码:[http://bit.ly/2ToKkpU](http://bit.ly/2ToKkpU)\n\n# Qt 中的属性动画\n\n在这个例子中，我们将学习如何使用 Qt 的属性动画类来动画化我们的**图形用户界面** ( **图形用户界面**)元素，这是其强大动画框架的一部分，允许我们用最少的努力创建流畅的动画。\n\n# 怎么做…\n\n在下面的示例中，我们将创建一个新的小部件项目，并通过更改按钮的属性来激活按钮:\n\n1.  让我们创建一个新的`Qt Widgets Application`项目。之后，用 Qt Designer 打开`mainwindow.ui`，在主窗口放置一个按钮，如下图:\n\n![](img/e8af7acb-83d7-4175-bcb4-8e662dd63b31.png)\n\n2.  打开`mainwindow.cpp`，在源代码的开头添加下面一行代码:\n\n```cpp\n #include <QPropertyAnimation>\n```\n\n3.  之后，打开`mainwindow.cpp`，将以下代码添加到构造函数中:\n\n```cpp\nQPropertyAnimation *animation = new QPropertyAnimation(ui->pushButton, \"geometry\");\nanimation->setDuration(10000);\nanimation->setStartValue(ui->pushButton->geometry());\nanimation->setEndValue(QRect(200, 200, 100, 50));\nanimation->start();\n```\n\n# 它是如何工作的...\n\n制作图形用户界面元素动画的一种更常见的方法是通过 Qt 提供的属性动画类，称为`QPropertyAnimation`类。这个类是动画框架的一部分，它利用 Qt 中的计时器系统在给定的持续时间内改变图形用户界面元素的属性。\n\n我们在这里试图完成的是将按钮从一个位置移动到另一个位置，同时一路放大按钮的大小。通过在*第 2 步*的源代码中包含`QPropertyAnimation`头，我们将能够访问 Qt 提供的`QPropertyAnimation`类并利用其功能。\n\n*步骤 3* 中的代码基本上创建了一个新的属性动画，并将其应用到我们刚刚在 Qt Designer 中创建的**按钮**上。我们特别要求属性动画类更改按钮的几何属性，并将其持续时间设置为 3，000 毫秒(3 秒)。\n\n然后，动画的开始值被设置为按钮的初始几何形状，因为很明显，我们希望它从我们最初在 Qt Designer 中放置按钮的位置开始。最终值被设置为我们想要的样子；在这种情况下，我们将在`x: 200`和`y: 200`将按钮移动到新的位置，同时沿途将其大小更改为`width: 100`和`height: 50`。\n\n之后，调用`animation` *|* `start()`开始动画。编译并运行项目。您应该看到按钮开始在主窗口中缓慢移动，同时一次扩大一点大小，直到它到达目的地。您可以通过更改前面代码中的值来更改动画持续时间以及目标位置和比例。使用 Qt 的属性动画系统制作一个图形用户界面元素的动画真的那么简单！\n\n# 还有更多…\n\nQt 为我们提供了几个不同的子系统来为我们的 GUI 创建动画，包括计时器、时间线、动画框架、状态机框架和图形视图框架:\n\n*   **定时器** : Qt 为我们提供了重复性和单次触发的定时器。当达到超时值时，将通过 Qt 的信号和时隙机制触发一个事件回调函数。您可以利用计时器在给定的时间间隔内更改图形用户界面元素的属性(颜色、位置、比例等)来创建动画。\n*   **时间轴**:时间轴定期调用一个槽来动画化一个 GUI 元素。它非常类似于重复计时器，但是当槽被触发时，时间轴不是一直做同样的事情，而是向槽提供一个值来指示它的当前帧索引，以便您可以根据给定的值做不同的事情(例如偏移到子画面表的不同空间)。\n*   **动画框架**:动画框架通过允许动画化图形用户界面元素的属性，使得动画化图形用户界面元素变得容易。动画通过使用缓和曲线来控制。缓和曲线描述了一个控制动画速度的函数，导致不同的加速和减速模式。Qt 支持的缓和曲线类型包括线性、二次、三次、四次、正弦、指数、圆形和弹性。\n*   **状态机框架** : Qt 为我们提供了用于创建和执行状态图的类，这些类允许每个 GUI 元素在被信号触发时从一种状态移动到另一种状态。状态机框架中的状态图是分层的，这意味着每个状态也可以嵌套在其他状态中。\n*   **图形视图框架**:图形视图框架是一个强大的图形引擎，用于可视化和与大量定制的 2D 图形项目进行交互。如果你是一个有经验的程序员，你可以使用图形视图框架来绘制你的图形用户界面，并以完全手动的方式来制作它们的动画。\n\n通过利用我们在这里提到的所有强大功能，我们能够轻松地创建一个直观的现代图形用户界面。在本章中，我们将研究使用 Qt 制作图形用户界面元素动画的实用方法。\n\n# 使用缓和曲线控制属性动画\n\n在这个例子中，我们将学习如何利用缓和曲线使我们的动画更有趣。我们仍将使用之前的源代码，它使用属性动画来制作按钮动画。\n\n# 怎么做…\n\n在以下示例中，我们将学习如何在动画中添加缓和曲线:\n\n1.  在调用`start()`函数之前，定义缓和曲线并将其添加到属性动画中:\n\n```cpp\nQPropertyAnimation *animation = new QPropertyAnimation(ui->pushButton, \"geometry\");\nanimation->setDuration(3000);\nanimation->setStartValue(ui->pushButton->geometry());\nanimation->setEndValue(QRect(200, 200, 100, 50));\n\nQEasingCurve curve;\ncurve.setType(QEasingCurve::OutBounce);\nanimation->setEasingCurve(curve);\nanimation->start();\n```\n\n2.  调用`setLoopCount()`功能，设置您希望它重复多少个循环:\n\n```cpp\nQPropertyAnimation *animation = new QPropertyAnimation(ui->pushButton, \"geometry\");\nanimation->setDuration(3000);\nanimation->setStartValue(ui->pushButton->geometry());\nanimation->setEndValue(QRect(200, 200, 100, 50));\n\nQEasingCurve curve;\ncurve.setType(EasingCurve::OutBounce);\nanimation->setEasingCurve(curve);\nanimation->setLoopCount(2);\nanimation->start();\n```\n\n3.  将缓和曲线应用到动画之前，调用`setAmplitude()`、`setOvershoot()`和`setPeriod()`:\n\n```cpp\nQEasingCurve curve;\ncurve.setType(QEasingCurve::OutBounce);\ncurve.setAmplitude(1.00);\ncurve.setOvershoot(1.70);\ncurve.setPeriod(0.30);\nanimation->setEasingCurve(curve);\nanimation->start();\n```\n\n# 它是如何工作的...\n\n要让缓和曲线控制动画，您只需要定义一条**缓和曲线**，并在调用`start()`函数之前将其添加到属性动画中。你也可以尝试其他几种类型的缓和曲线，看看哪一种最适合你。这里有一个例子:\n\n```cpp\nanimation->setEasingCurve(QEasingCurve::OutBounce);\n```\n\n如果您希望动画在播放完毕后循环播放，您可以调用`setLoopCount()`函数来设置您希望它重复播放的循环次数，或将无限循环的值设置为`-1`:\n\n```cpp\nanimation->setLoopCount(-1);\n```\n\n在将缓和曲线应用到属性动画之前，可以设置几个参数来细化缓和曲线。这些参数包括**振幅**、**超调**、**周期**:\n\n*   **振幅**:振幅越高，将应用于动画的弹跳或弹性弹簧效果越高。\n*   **过冲**:一些曲线函数由于阻尼效应会产生过冲(超过其最终值)曲线。通过调整过冲值，我们能够增加或减少这种影响。\n*   **周期**:设置一个小的周期值会给曲线一个高的频率。大周期会给它一个小频率。\n\n然而，这些参数并不适用于所有曲线类型。请参考 Qt 文档，了解哪个参数适用于哪种曲线类型。\n\n# 还有更多…\n\n虽然**属性动画**工作得非常好，但有时看着图形用户界面元素以恒定的速度动画化会觉得有点无聊。我们可以通过添加**缓和曲线**来控制运动，让动画看起来更有趣。Qt 中可以使用许多类型的缓和曲线，以下是其中一些:\n\n![](img/3ba8e9b5-0ea7-466d-b356-5547fa9cf30c.png)\n\n从上图中可以看出，每条缓和曲线都会产生不同的缓和效果。\n\nFor the full list of easing curves available in Qt, please refer to the Qt documentation at [http://doc.qt.io/qt-5/qeasingcurve.html#Type-enum](http://doc.qt.io/qt-5/qeasingcurve.html#Type-enum).\n\n# 创建动画组\n\n在本例中，我们将学习如何使用动画组来管理组中包含的动画的状态。\n\n# 怎么做…\n\n让我们按照以下步骤创建一个动画组:\n\n1.  我们将使用前面的示例，但是这一次，我们将在主窗口中再添加两个按钮，如下图所示:\n\n![](img/17ef289c-05bf-4d4d-88bc-dd7e7d497866.png)\n\n2.  在主窗口的构造器中为每个按钮定义动画:\n\n```cpp\nQPropertyAnimation *animation1 = new QPropertyAnimation(ui->pushButton, \"geometry\");\nanimation1->setDuration(3000);\nanimation1->setStartValue(ui->pushButton->geometry());\nanimation1->setEndValue(QRect(50, 200, 100, 50));\n\nQPropertyAnimation *animation2 = new QPropertyAnimation(ui->pushButton_2, \"geometry\");\nanimation2->setDuration(3000);\nanimation2->setStartValue(ui->pushButton_2->geometry());\nanimation2->setEndValue(QRect(150, 200, 100, 50));\n\nQPropertyAnimation *animation3 = new QPropertyAnimation(ui->pushButton_3, \"geometry\");\nanimation3->setDuration(3000);\nanimation3->setStartValue(ui->pushButton_3->geometry());\nanimation3->setEndValue(QRect(250, 200, 100, 50));\n```\n\n3.  创建缓和曲线，并将相同的曲线应用于所有三个动画:\n\n```cpp\nQEasingCurve curve;\ncurve.setType(QEasingCurve::OutBounce);\ncurve.setAmplitude(1.00);\ncurve.setOvershoot(1.70);\ncurve.setPeriod(0.30);\n\nanimation1->setEasingCurve(curve);\nanimation2->setEasingCurve(curve);\nanimation3->setEasingCurve(curve);\n```\n\n4.  将缓和曲线应用于所有三个动画后，我们将创建一个动画组，并将所有三个动画添加到该组中:\n\n```cpp\nQParallelAnimationGroup *group = new QParallelAnimationGroup;\ngroup->addAnimation(animation1);\ngroup->addAnimation(animation2);\ngroup->addAnimation(animation3);\n```\n\n5.  从我们刚刚创建的动画组中调用`start()`函数:\n\n```cpp\ngroup->start();\n```\n\n# 它是如何工作的...\n\n由于我们现在使用的是**动画组**，我们不再从单个动画中调用`start()`功能。相反，我们将从刚刚创建的动画组中调用`start()`函数。如果您现在编译并运行该示例，您将看到所有三个按钮同时播放。这是因为我们使用的是**平行动画组**。您可以用一个**顺序动画组**替换它，然后再次运行该示例:\n\n```cpp\nQSequentialAnimationGroup *group = new QSequentialAnimationGroup;\n```\n\n这一次，一次只有一个按钮会播放动画，而其他按钮会耐心等待轮到它们。优先级是根据首先将哪个动画添加到动画组来设置的。您可以通过简单地重新排列添加到组中的动画序列来更改动画序列。例如，如果我们希望按钮 3 首先开始动画，然后是按钮 2，然后是按钮 1，代码将如下所示:\n\n```cpp\ngroup->addAnimation(animation3);\ngroup->addAnimation(animation2);\ngroup->addAnimation(animation1);\n```\n\n由于属性动画和动画组都继承自`QAbstractAnimator`类，这意味着您也可以将一个动画组添加到另一个动画组中，以形成更复杂的嵌套动画组。\n\n# 还有更多…\n\nQt 允许我们创建多个动画，并将它们分组到一个动画组中。一个组通常负责管理其动画的状态(也就是说，它决定何时开始、停止、恢复和暂停它们)。目前，Qt 为动画组提供了两种类型的类:`QParallelAnimationGroup`和`QSequentialAnimationGroup`:\n\n*   `QParallelAnimationGroup`:顾名思义，一个平行动画组同时运行其组内的所有动画。当持续时间最长的动画完成运行时，该组被视为已完成。\n*   `QSequentialAnimationGroup`:顺序动画组按顺序运行其动画，这意味着它一次只运行一个动画，并且当当前动画完成时只播放下一个动画。\n\n# 创建嵌套动画组\n\n使用**嵌套动画组**的一个很好的例子是，当你有几个平行的动画组，并且你想按顺序播放这些组。\n\n# 怎么做…\n\n让我们按照以下步骤创建一个嵌套动画组，以顺序播放不同的动画组:\n\n1.  我们将使用上一个示例中的用户界面，并在主窗口中添加几个按钮，如下所示:\n\n![](img/c8cf30be-b6a8-4bfb-b059-3ff3d6353d7d.png)\n\n2.  为按钮创建所有动画，然后创建**缓和曲线**并将其应用于所有动画:\n\n```cpp\nQPropertyAnimation *animation1 = new QPropertyAnimation(ui->pushButton, \"geometry\");\nanimation1->setDuration(3000);\nanimation1->setStartValue(ui->pushButton->geometry());\nanimation1->setEndValue(QRect(50, 50, 100, 50));\n\nQPropertyAnimation *animation2 = new QPropertyAnimation(ui->pushButton_2, \"geometry\");\nanimation2->setDuration(3000);\nanimation2->setStartValue(ui->pushButton_2->geometry());\nanimation2->setEndValue(QRect(150, 50, 100, 50));\n\nQPropertyAnimation *animation3 = new QPropertyAnimation(ui->pushButton_3, \"geometry\");\nanimation3->setDuration(3000);\nanimation3->setStartValue(ui->pushButton_3->geometry());\nanimation3->setEndValue(QRect(250, 50, 100, 50));\n```\n\n接下来，应用以下代码:\n\n```cpp\nQPropertyAnimation *animation4 = new QPropertyAnimation(ui->pushButton_4, \"geometry\");\nanimation4->setDuration(3000);\nanimation4->setStartValue(ui->pushButton_4->geometry());\nanimation4->setEndValue(QRect(50, 200, 100, 50));\n\nQPropertyAnimation *animation5 = new QPropertyAnimation(ui->pushButton_5, \"geometry\");\nanimation5->setDuration(3000);\nanimation5->setStartValue(ui->pushButton_5->geometry());\nanimation5->setEndValue(QRect(150, 200, 100, 50));\n\nQPropertyAnimation *animation6 = new QPropertyAnimation(ui->pushButton_6, \"geometry\");\nanimation6->setDuration(3000);\nanimation6->setStartValue(ui->pushButton_6->geometry());\nanimation6->setEndValue(QRect(250, 200, 100, 50));\n```\n\n然后，应用以下代码:\n\n```cpp\nQEasingCurve curve;\ncurve.setType(QEasingCurve::OutBounce);\ncurve.setAmplitude(1.00);\ncurve.setOvershoot(1.70);\ncurve.setPeriod(0.30);\n\nanimation1->setEasingCurve(curve);\nanimation2->setEasingCurve(curve);\nanimation3->setEasingCurve(curve);\nanimation4->setEasingCurve(curve);\nanimation5->setEasingCurve(curve);\nanimation6->setEasingCurve(curve);\n```\n\n3.  创建两个**动画组**，一个用于上栏的按钮，另一个用于下栏:\n\n```cpp\nQParallelAnimationGroup *group1 = new QParallelAnimationGroup;\ngroup1->addAnimation(animation1);\ngroup1->addAnimation(animation2);\ngroup1->addAnimation(animation3);\n\nQParallelAnimationGroup *group2 = new QParallelAnimationGroup;\ngroup2->addAnimation(animation4);\ngroup2->addAnimation(animation5);\ngroup2->addAnimation(animation6);\n```\n\n4.  我们将创建另一个动画组，用于存储我们之前创建的两个动画组:\n\n```cpp\nQSequentialAnimationGroup *groupAll = new QSequentialAnimationGroup;\ngroupAll->addAnimation(group1);\ngroupAll->addAnimation(group2);\ngroupAll->start();\n```\n\n# 它是如何工作的...\n\n我们在这里尝试做的是先播放上栏按钮的动画，然后播放下栏按钮的动画。由于两个动画组都是**平行动画组**，当调用`start()`功能时，属于各自组的按钮将同时被动画化。\n\n不过这次的组是**顺序动画组**，也就是说一次只播放一个平行动画组，第一个结束后再播放另一个。动画组是一个非常方便的系统，允许我们用简单的编码创建非常复杂的图形用户界面动画。Qt 将为我们处理困难的部分，这样我们就不必。\n\n# Qt 中的状态机\n\nA **状态机**可以用于很多目的，但是，在本章中，我们将只讨论与动画相关的主题。\n\n# 怎么做…\n\n在 Qt 中实现状态机一点也不难。让我们从以下步骤开始:\n\n1.  我们将为示例程序设置一个新的用户界面，如下所示:\n\n![](img/031409dc-4946-4ede-af75-303e813b9b55.png)\n\n2.  我们将在源代码中包含一些标题:\n\n```cpp\n#include <QStateMachine>\n#include <QPropertyAnimation>\n#include <QEventTransition>\n```\n\n3.  在我们主窗口的构造函数中，添加以下代码来创建一个新的**状态机**和两个**状态**，我们稍后将使用它们:\n\n```cpp\nQStateMachine *machine = new QStateMachine(this);\nQState *s1 = new QState();\nQState *s2 = new QState();\n```\n\n4.  我们将定义在每个状态下我们应该做什么，在这种情况下，将改变标签的**文本**，以及按钮的**位置**和**大小**:\n\n```cpp\nQState *s1 = new QState();\ns1->assignProperty(ui->stateLabel, \"text\", \"Current state: 1\");\ns1->assignProperty(ui->pushButton, \"geometry\", QRect(50, 200, 100, 50));\n\nQState *s2 = new QState();\ns2->assignProperty(ui->stateLabel, \"text\", \"Current state: 2\");\ns2->assignProperty(ui->pushButton, \"geometry\", QRect(200, 50, 140, 100));\n```\n\n5.  完成后，让我们继续在源代码中添加**事件转换**类:\n\n```cpp\nQEventTransition *t1 = new QEventTransition(ui->changeState, QEvent::MouseButtonPress);\nt1->setTargetState(s2);\ns1->addTransition(t1);\n\nQEventTransition *t2 = new QEventTransition(ui->changeState, QEvent::MouseButtonPress);\nt2->setTargetState(s1);\ns2->addTransition(t2);\n```\n\n6.  将我们刚刚创建的所有状态添加到**状态机**中，并将状态 1 定义为初始状态。然后，调用`machine->start()`运行状态机:\n\n```cpp\nmachine->addState(s1);\nmachine->addState(s2);\nmachine->setInitialState(s1);\nmachine->start();\n```\n\n7.  如果您现在运行示例程序，您会注意到一切都很好，除了按钮没有经历平滑过渡，它只是瞬间跳到我们之前设置的位置和大小。这是因为我们没有使用**属性动画**来创建平滑过渡。\n8.  回到事件转换步骤，添加以下代码行:\n\n```cpp\nQEventTransition *t1 = new QEventTransition(ui->changeState, QEvent::MouseButtonPress);\nt1->setTargetState(s2);\nt1->addAnimation(new QPropertyAnimation(ui->pushButton, \"geometry\"));\ns1->addTransition(t1);\n\nQEventTransition *t2 = new QEventTransition(ui->changeState, QEvent::MouseButtonPress);\nt2->setTargetState(s1);\nt2->addAnimation(new QPropertyAnimation(ui->pushButton, \"geometry\"));\ns2->addTransition(t2);\n```\n\n9.  您也可以在动画中添加一条**缓和曲线**，使其看起来更有趣:\n\n```cpp\nQPropertyAnimation *animation = new QPropertyAnimation(ui->pushButton, \"geometry\");\nanimation->setEasingCurve(QEasingCurve::OutBounce);\nQEventTransition *t1 = new QEventTransition(ui->changeState, QEvent::MouseButtonPress);\nt1->setTargetState(s2);\nt1->addAnimation(animation);\ns1->addTransition(t1);\n\nQEventTransition *t2 = new QEventTransition(ui->changeState, QEvent::MouseButtonPress);\nt2->setTargetState(s1);\nt2->addAnimation(animation);\ns2->addTransition(t2);\n```\n\n# 它是如何工作的...\n\n主窗口布局上有两个**按钮**和一个**标签**。左上角的按钮按下后会触发状态变化，而右上角的标签会改变文字，显示我们当前处于哪个状态，下面的按钮会根据当前状态进行动画显示。`QEventTransition`类定义了什么会触发一种状态和另一种状态之间的转换。\n\n在我们的例子中，当点击 changeState 按钮(左上角的那个)时，我们希望状态从状态 1 变为状态 2。之后，当再次按下同一个按钮时，我们也想从状态 2 变回状态 1。这可以通过创建另一个**事件转换**类并将目标状态设置回状态 1 来实现。然后，将这些转换添加到它们各自的状态中。我们告诉 Qt 使用属性动画类向目标值平滑地插入属性，而不是直接将属性分配给小部件。就是这么简单！没有必要设置起始值和结束值，因为我们已经调用了`assignProperty()`函数，该函数已经自动分配了结束值。\n\n# 还有更多…\n\nQt 中的**状态机框架**提供了用于创建和执行状态图的类。Qt 的事件系统用于驱动状态机，其中状态之间的转换可以通过使用*信号*来触发，然后另一端的*插槽*将被信号调用来执行一个动作，例如播放动画。\n\n一旦你理解了状态机的基础，你就可以用它们来做其他事情。状态机框架中的状态图是分层的。就像上一节中的动画组一样，状态也可以嵌套在其他状态中:\n\n![](img/1f4529ab-5d16-468e-aec2-433193891923.png)\n\n# QML 的状态、过渡和动画\n\n如果您更喜欢使用 QML 而不是 C++，Qt 还在 Qt Quick 中提供了类似的功能，允许您用最少的代码行轻松地制作图形用户界面元素的动画。在这个例子中，我们将学习如何与 QML 实现这一点。\n\n# 怎么做…\n\n让我们按照以下步骤开始创建一个不断改变背景颜色的窗口:\n\n1.  我们将创建一个新的 Qt 快速应用项目，并设置我们的用户界面，如下所示:\n\n![](img/b6e22930-753f-4dc6-a55b-34d4518e43b7.png)\n\n2.  以下是我的`main.qml`文件的样子:\n\n```cpp\nimport QtQuick 2.9\nimport QtQuick.Window 2.3\nWindow {\n    visible: true\n    width: 480;\n    height: 320;\n    Rectangle {\n        id: background;\n        anchors.fill: parent;\n        color: \"blue\";\n    }\n    Text {\n        text: qsTr(\"Hello World\");\n        anchors.centerIn: parent;\n        color: \"white\";\n        font.pointSize: 15;\n    }\n}\n```\n\n3.  将**彩色动画**添加到`Rectangle`对象:\n\n```cpp\nRectangle {\n    id: background;\n    anchors.fill: parent;\n    color: \"blue\";\n    SequentialAnimation on color {\n        ColorAnimation { to: \"yellow\"; duration: 1000 }\n        ColorAnimation { to: \"red\"; duration: 1000 }\n        ColorAnimation { to: \"blue\"; duration: 1000 }\n        loops: Animation.Infinite;\n    }\n}\n```\n\n4.  向`text`对象添加数字动画:\n\n```cpp\nText {\n    text: qsTr(\"Hello World\");\n    anchors.centerIn: parent;\n    color: \"white\";\n    font.pointSize: 15;\n    SequentialAnimation on opacity {\n        NumberAnimation { to: 0.0; duration: 200}\n        NumberAnimation { to: 1.0; duration: 200}\n        loops: Animation.Infinite;\n    }\n}\n```\n\n5.  向其中添加另一个数字动画:\n\n```cpp\nText {\n    text: qsTr(\"Hello World\");\n    anchors.centerIn: parent;\n    color: \"white\";\n    font.pointSize: 15;\n    SequentialAnimation on opacity {\n        NumberAnimation { to: 0.0; duration: 200}\n        NumberAnimation { to: 1.0; duration: 200}\n        loops: Animation.Infinite;\n    }\n    NumberAnimation on rotation {\n        from: 0;\n        to: 360;\n        duration: 2000;\n        loops: Animation.Infinite;\n    }\n}\n```\n\n6.  定义两个状态，一个叫做`PRESSED`状态，另一个叫做`RELEASED`状态。然后，将默认状态设置为`RELEASED`:\n\n```cpp\nRectangle {\n    id: background;\n    anchors.fill: parent;\n    state: \"RELEASED\";\n    states: [\n        State {\n            name: \"PRESSED\"\n            PropertyChanges { target: background; color: \"blue\"}\n        },\n        State {\n            name: \"RELEASED\"\n            PropertyChanges { target: background; color: \"red\"}\n        }\n    ]\n}\n```\n\n7.  之后，在`Rectangle`对象内创建一个鼠标区域，这样我们就可以点击它:\n\n```cpp\nMouseArea {\n    anchors.fill: parent;\n    onPressed: background.state = \"PRESSED\";\n    onReleased: background.state = \"RELEASED\";\n}\n```\n\n8.  给`Rectangle`对象添加一些过渡:\n\n```cpp\ntransitions: [\n    Transition {\n        from: \"PRESSED\"\n        to: \"RELEASED\"\n        ColorAnimation { target: background; duration: 200}\n    },\n    Transition {\n        from: \"RELEASED\"\n        to: \"PRESSED\"\n        ColorAnimation { target: background; duration: 200}\n    }\n]\n```\n\n# 它是如何工作的...\n\n主窗口由一个蓝色矩形和静态文本组成，上面写着 Hello World。我们希望背景颜色从蓝色变成黄色，然后变成红色，再循环回到蓝色。这可以通过使用 QML 的彩色动画类型轻松实现。我们在*步骤 3* 所做的基本上是在`Rectangle`对象内创建一个顺序动画组，然后在该组内创建三个不同颜色的动画，这将每 1000 毫秒(1 秒)改变一次对象的颜色。我们还设置动画无限循环。\n\n在*第 4 步*中，我们想使用数字动画来动画化静态文本的`alpha`值。我们在`Text`对象中创建了另一个连续动画组，并创建了两个数字动画来将`alpha`值从`0`动画化到`1`并返回。然后，我们设置动画无限循环。\n\n然后，在*步骤 5* 中，我们通过添加另一个数字动画来旋转 Hello World 文本。在*步骤 6* 中，我们想要在点击`Rectangle`对象时使其从一种颜色变为另一种颜色。松开鼠标后，`Rectangle`对象将变回初始颜色。为了实现这一点，首先我们需要定义两个状态，一个叫做`PRESSED`状态，另一个叫做`RELEASED`状态。然后，我们将默认状态设置为`RELEASED`。\n\n现在，当您编译并运行该示例时，背景将在按下时立即变为蓝色，并在释放鼠标时变回红色。这很好，我们可以通过在切换颜色时给它一点过渡来进一步增强它。这可以通过向`Rectangle`对象添加过渡来轻松实现。\n\n# 还有更多…\n\n在 QML，您可以使用八种不同类型的属性动画，如下所示:\n\n*   **锚点动画**:动画化锚点值的变化\n*   **颜色动画**:动画显示颜色值的变化\n*   **数字动画**:动画化实体类型值的变化\n*   **父动画**:动画显示父值的变化\n*   **路径动画**:沿路径动画化一个项目\n*   **属性动画**:动画显示属性值的变化\n*   **旋转动画**:动画显示旋转值的变化\n*   **矢量 3d 动画**:动画显示矢量 3D 值的变化\n\n就像 C++ 版本一样，这些动画也可以分组在一个动画组中，以顺序或并行播放动画。您还可以使用缓和曲线控制动画，并确定何时使用状态机播放这些动画，就像我们在上一节中所做的那样。\n\n# 使用动画师的动画小部件属性\n\n在本食谱中，我们将学习如何使用 QML 提供的动画制作功能来制作图形用户界面小部件属性的动画。\n\n# 怎么做…\n\n如果执行以下步骤，制作 QML 对象的动画非常容易:\n\n1.  创建一个`Rectangle`对象，并添加一个缩放动画师:\n\n```cpp\nRectangle {\n    id: myBox;\n    width: 50;\n    height: 50;\n    anchors.horizontalCenter: parent.horizontalCenter;\n    anchors.verticalCenter: parent.verticalCenter;\n    color: \"blue\";\n    ScaleAnimator {\n        target: myBox;\n        from: 5;\n        to: 1;\n        duration: 2000;\n        running: true;\n    }\n}\n```\n\n2.  添加旋转动画制作人，并在平行动画组中设置运行值，但不在任何单个动画制作人中设置:\n\n```cpp\nParallelAnimation {\n    ScaleAnimator {\n        target: myBox;\n        from: 5;\n        to: 1;\n        duration: 2000;\n    }\n    RotationAnimator {\n        target: myBox;\n        from: 0;\n        to: 360;\n        duration: 1000;\n    }\n    running: true;\n}\n```\n\n3.  向缩放动画器添加缓和曲线:\n\n```cpp\nScaleAnimator {\n    target: myBox;\n    from: 5;\n    to: 1;\n    duration: 2000;\n    easing.type: Easing.InOutElastic;\n    easing.amplitude: 2.0;\n    easing.period: 1.5;\n    running: true;\n}\n```\n\n# 它是如何工作的...\n\n动画师类型可以像任何其他动画类型一样使用。我们希望在 2000 毫秒(2 秒)内将矩形从 5 的大小缩放到 1 的大小。我们创建了一个蓝色`Rectangle`对象，并添加了一个缩放动画师。我们将初始值设置为 5，最终值设置为 1。然后，我们将动画持续时间设置为`2000`，并将运行值设置为真，以便在程序启动时播放。\n\n就像动画类型一样，动画师也可以分组(即平行动画组或顺序动画组)。QtQuick 还会将动画组视为*动画师*，并尽可能在场景图的渲染线程上运行。在*第二步*中，我们想要将两个不同的动画师组成一个平行的动画组，这样他们就可以同时一起运行。\n\n我们将保留之前创建的缩放动画师，并添加另一个旋转动画师来旋转`Rectangle`对象。这一次，在平行动画组中设置运行值，但不在任何单个动画制作人中设置。\n\n就像 C++ 版本一样，QML 也支持缓和曲线，它们可以很容易地应用于任何动画或动画师类型。\n\n在 QML 有一种叫做动画师的东西，它不同于通常的动画类型，尽管它们之间有一些相似之处。与常规动画类型不同，动画师类型直接在 Qt Quick 的场景图上操作，而不是 QML 对象及其属性。动画运行时，QML 属性的值不会更改，因为它只会在动画完成后更改。使用动画师类型的好处是它直接在场景图的渲染线程上操作，这意味着它的性能会比在 UI 线程上运行稍好。\n\n# 雪碧动画\n\n在这个例子中，我们将学习如何在 QML 创建雪碧动画。\n\n# 怎么做…\n\n让我们按照以下步骤在应用窗口中运行一匹马:\n\n1.  我们将需要添加我们的精灵表到 Qt 的资源系统，以便它可以在程序中使用。打开`qml.qrc`，点击添加|添加文件按钮。选择您的精灵工作表图像，并按下 *Ctrl* + *S* 保存资源文件。\n2.  在`main.qml`中创建新的空窗口:\n\n```cpp\nimport QtQuick 2.9\nimport QtQuick.Window 2.3\nWindow {\n    visible: true\n    width: 420\n    height: 380\n    Rectangle {\n        anchors.fill: parent\n        color: \"white\"\n    }\n}\n```\n\n3.  完成后，我们将开始在 QML 创建一个`AnimatedSprite`对象:\n\n```cpp\nimport QtQuick 2.9\nimport QtQuick.Window 2.3\nWindow {\n    visible: true;\n    width: 420;\n    height: 380;\n    Rectangle {\n        anchors.fill: parent;\n        color: \"white\";\n    }\n```\n\n然后，设置以下内容:\n\n```cpp\nAnimatedSprite {\n        id: sprite;\n        width: 128;\n        height: 128;\n        anchors.centerIn: parent;\n        source: \"qrc:///horse_1.png\";\n        frameCount: 11;\n        frameWidth: 128;\n        frameHeight: 128;\n        frameRate: 25;\n        loops: Animation.Infinite;\n        running: true;\n    }\n}\n```\n\n4.  在窗口中添加鼠标区域并检查`onClicked`事件:\n\n```cpp\nMouseArea {\n    anchors.fill: parent;\n    onClicked: {\n        if (sprite.paused)\n            sprite.resume();\n        else\n            sprite.pause();\n    }\n}\n```\n\n5.  如果你现在编译并运行示例程序，你会看到一匹小马在窗口中间奔跑。多有趣:\n\n![](img/189b740e-28c0-40a1-8be7-4578b73797fe.png)\n\n6.  接下来，我们想尝试做一些很酷的事情。我们会在播放它的跑步动画时，让马跑过窗户，无限循环！首先，我们需要从 QML 移除`anchors.centerIn: parent`并用`x`和`y`值替换它:\n\n```cpp\nAnimatedSprite {\n    id: sprite;\n    width: 128;\n    height: 128;\n    x: -128;\n    y: parent.height / 2;\n    source: \"qrc:///horse_1.png\";\n    frameCount: 11;\n    frameWidth: 128;\n    frameHeight: 128;\n    frameRate: 25;\n    loops: Animation.Infinite;\n    running: true;\n}\n```\n\n7.  向 sprite 对象添加数字动画并设置其属性，如下所示:\n\n```cpp\nNumberAnimation {\n    target: sprite;\n    property: \"x\";\n    from: -128;\n    to: 512;\n    duration: 3000;\n    loops: Animation.Infinite;\n    running: true;\n}\n```\n\n8.  如果你现在编译并运行示例程序，你会看到小马发疯了，开始跑过窗户！\n\n# 它是如何工作的...\n\n在这个配方中，我们将动画精灵对象放在窗口的中间，并将其图像源设置为我们刚刚添加到项目资源中的精灵表。然后，我们计算 sprite 表中有多少帧属于正在运行的动画，在本例中是 11 帧。我们还通知 Qt 动画每一帧的尺寸，在这种情况下是`128 x 128`。之后，我们将帧率设置为`25`以获得合适的速度，然后将其设置为无限循环。然后我们将运行值设置为`true`，这样当程序开始运行时，动画将默认播放。\n\n然后，在*第 4 步*中，我们希望能够暂停动画并通过点击窗口恢复动画。我们只需在点击鼠标区域时检查精灵当前是否暂停。如果精灵动画暂停，则动画恢复；否则，动画会暂停。\n\n在*步骤 6* 中，我们将`anchors.centerIn`替换为`x`和`y`值，这样动画精灵对象就不会锚定在窗口的中心，这将使其无法四处移动。然后，我们在动画精灵中创建一个数字动画来激活其`x`属性。我们将左侧的`start`值设置为窗外的某个地方，将右侧的`end`值设置为窗外的某个地方。之后，我们将持续时间设置为`3,000 milliseconds` (3 秒)，并使其无限循环。\n\n最后，我们还将`running`值设置为`true`，这样当程序开始运行时，它会默认播放动画。\n\n# 还有更多…\n\n精灵动画被广泛使用，尤其是在游戏开发中。精灵用于角色动画、粒子动画，甚至图形用户界面动画。一个精灵表由许多图像组合成一个，然后可以一次一个地分解并显示在屏幕上。精灵表中不同图像(或精灵)之间的过渡会产生动画错觉，我们通常称之为精灵动画。在 QML 使用`AnimatedSprite`类型可以轻松实现雪碧动画。\n\nIn this example program, I am using a free and open source image  that was created by **bluecarrot16** under the `CC-BY 3.0/GPL 3.0/GPL 2.0/OGA-BY 3.0` license. The image can be obtained legally at [http://opengameart.org/content/lpc-horse](http://opengameart.org/content/lpc-horse)."
  },
  {
    "path": "docs/qt5-cpp-gui-prog-cb/04.md",
    "content": "# 四、画家与 2D 图形\n\n本章涵盖的主题列表如下:\n\n*   在屏幕上绘制基本形状\n*   将形状导出到**可缩放矢量图形** ( **SVG** )文件\n*   坐标变换\n*   在屏幕上显示图像\n*   将图像效果应用于图形\n*   创建基本的绘画程序\n*   在 QML 渲染 2D 画布\n\n# 介绍\n\n在本章中，我们将学习如何用 Qt 在屏幕上渲染 2D 图形。在内部，Qt 使用一个名为 **QPainter** 的低级类在主窗口上渲染它的小部件。Qt 允许我们访问和使用`QPainter`类来绘制矢量图形、文本、2D 图像，甚至三维图形。\n\n您可以使用`QPainter`类来创建自己的自定义小部件，或者创建严重依赖于渲染计算机图形的程序，如视频游戏、照片编辑器和 3D 建模工具。\n\n# 技术要求\n\n本章的技术要求包括 Qt 5.11.2 MinGW 32 位、Qt Creator 4.8.2 和 Windows 10\n\n本章使用的所有代码均可从本章 GitHub 资源库下载，网址为:[https://GitHub . com/PacktPublishing/Qt5-CPP-GUI-Programming-Cookbook-第二版/tree/master/Chapter04](https://github.com/PacktPublishing/Qt5-CPP-GUI-Programming-Cookbook-Second-Edition/tree/master/Chapter04) 。\n\n查看以下视频，查看正在运行的代码:[http://bit.ly/2FrVYeq](http://bit.ly/2FrVYeq)\n\n# 在屏幕上绘制基本形状\n\n在本节中，我们将学习如何使用`QPainter`类绘制简单的矢量形状(直线、矩形、圆形等)并在主窗口上显示文本。我们还将学习如何使用`QPen`类更改这些矢量形状的绘制样式。\n\n# 怎么做…\n\n让我们按照这里列出的步骤在 Qt 窗口中显示基本形状:\n\n1.  首先，让我们创建一个新的`Qt Widgets Application`项目。\n2.  打开`mainwindow.ui`并移除菜单栏、主工具栏和状态栏对象，这样我们就得到一个干净、空的主窗口。右键单击工具栏小部件，并从弹出菜单中选择删除菜单栏:\n\n![](img/1aadbe98-f520-4a31-89af-f3e9440a5b12.png)\n\n3.  然后，打开`mainwindow.h`文件，添加以下代码以包含`QPainter`头文件:\n\n```cpp\n#include <QMainWindow>\n#include <QPainter>\n```\n\n4.  然后，声明类析构函数下面的`paintEvent()`事件处理程序:\n\n```cpp\npublic:\n    explicit MainWindow(QWidget *parent = 0);\n    ~MainWindow();\n    virtual void paintEvent(QPaintEvent *event);\n```\n\n5.  接下来，打开`mainwindow.cpp`文件，定义`paintEvent()`事件处理程序:\n\n```cpp\nvoid MainWindow::paintEvent(QPaintEvent *event) {}\n```\n\n6.  之后，我们将使用`paintEvent()`事件处理程序中的`QPainter`类向屏幕添加文本。在屏幕上(`20, 30`)位置绘制文本之前，我们先设置文本字体设置:\n\n```cpp\nQPainter textPainter;\ntextPainter.begin(this);\ntextPainter.setFont(QFont(\"Times\", 14, QFont::Bold));\ntextPainter.drawText(QPoint(20, 30), \"Testing\");\ntextPainter.end();\n```\n\n7.  然后，我们画一条从(`50, 60`)开始，到(`100, 100`)结束的直线:\n\n```cpp\nQPainter linePainter;\nlinePainter.begin(this);\nlinePainter.drawLine(QPoint(50, 60), QPoint(100, 100));\nlinePainter.end();\n```\n\n8.  我们也可以通过使用`QPainter`类调用`drawRect()`函数来轻松绘制矩形。但是，这一次，我们还在绘制形状之前对其应用背景图案:\n\n```cpp\nQPainter rectPainter;\nrectPainter.begin(this);\nrectPainter.setBrush(Qt::BDiagPattern);\nrectPainter.drawRect(QRect(40, 120, 80, 30));\nrectPainter.end();\n```\n\n9.  接下来，声明一个`QPen`类，将其颜色设置为红色，并将其绘制样式设置为`Qt::DashDotLine`。然后将`QPen`类应用于`QPainter`，在(`80, 200`)处画一个水平半径为`50`、垂直半径为`20`的椭圆:\n\n```cpp\nQPen ellipsePen;\nellipsePen.setColor(Qt::red);\nellipsePen.setStyle(Qt::DashDotLine);\n\nQPainter ellipsePainter;\nellipsePainter.begin(this);\nellipsePainter.setPen(ellipsePen);\nellipsePainter.drawEllipse(QPoint(80, 200), 50, 20);\nellipsePainter.end();\n```\n\n10.  我们也可以使用`QPainterPath`类来定义一个形状，然后将其传递给`QPainter`类进行渲染:\n\n```cpp\nQPainterPath rectPath;\nrectPath.addRect(QRect(150, 20, 100, 50));\n\nQPainter pathPainter;\npathPainter.begin(this);\npathPainter.setPen(QPen(Qt::red, 1, Qt::DashDotLine, Qt::FlatCap, Qt::MiterJoin));\npathPainter.setBrush(Qt::yellow);\npathPainter.drawPath(rectPath);\npathPainter.end();\n```\n\n11.  您也可以使用`QPainterPath`绘制任何其他形状，例如椭圆:\n\n```cpp\nQPainterPath ellipsePath;\nellipsePath.addEllipse(QPoint(200, 120), 50, 20);\n\nQPainter ellipsePathPainter;\nellipsePathPainter.begin(this);\nellipsePathPainter.setPen(QPen(QColor(79, 106, 25), 5, Qt::SolidLine, Qt::FlatCap, Qt::MiterJoin));\nellipsePathPainter.setBrush(QColor(122, 163, 39));\nellipsePathPainter.drawPath(ellipsePath);\nellipsePathPainter.end();\n```\n\n12.  `QPainter`也可以用来在屏幕上绘制一个图像文件。在以下示例中，我们加载了一个名为`tux.png`的图像文件，并将其绘制在屏幕上的(`100, 150`)位置:\n\n```cpp\nQImage image;\nimage.load(\"tux.png\");\n\nQPainter imagePainter(this);\nimagePainter.begin(this);\nimagePainter.drawImage(QPoint(100, 150), image);\nimagePainter.end();\n```\n\n13.  最终结果应该如下所示:\n\n![](img/3f34177f-c8c4-4754-a769-05ee80adfd5e.png)\n\n# 它是如何工作的...\n\n如果你想用`QPainter`在屏幕上画一些东西，你只需要告诉它应该画什么类型的图形(如文本、矢量形状、图像、多边形)和想要的位置和大小。`QPen`类决定了图形的轮廓应该是什么样的，例如它的颜色、线宽、线条样式(实线、虚线或虚线)、帽样式、连接样式等等。另一方面，`QBrush`设置图形背景的样式，如背景颜色、图案(纯色、渐变、密集画笔和交叉对角线)和位图。\n\n图形的选项应该在调用绘制函数之前设置(如`drawLine()`、`drawRect()`或`drawEllipse()`)。如果您的图形没有出现在屏幕上，并且您在 Qt Creator 中的应用输出窗口上看到诸如 QPainter::setPen: Painter 未激活和 QPainter::setBrush: Painter 未激活之类的警告，这意味着`QPainter`类当前未激活，并且您的程序不会触发其 paint 事件。要解决这个问题，请将主窗口设置为`QPainter`类的父窗口。通常，如果你在`mainwindow.cpp`文件中写代码，你所需要做的就是在初始化`QPainter`时把这个放在括号中。例如，请注意以下几点:\n\n```cpp\nQPainter linePainter(this);\n```\n\n`QImage`可以从计算机目录和程序资源加载图像。\n\n# 还有更多…\n\n把`QPainter`想象成一个拿着笔和空画布的机器人。你只需要告诉机器人它应该画什么类型的形状以及它在画布上的位置，然后机器人就会根据你的描述来完成它的工作。为了让您的生活更轻松， **QPainter** 类还提供了众多功能，例如`drawArc()`、`drawEllipse()`、`drawLine()`、`drawRect()`和`drawPie()`，让您可以轻松渲染预定义的形状。在 Qt 中，所有的小部件类(包括主窗口)都有一个名为`QWidget::paintEvent()`的事件处理程序。每当操作系统认为主窗口应该重新绘制小部件时，就会触发这个事件处理程序。很多事情可以导致这个决定，比如主窗口被缩放，一个小部件改变它的状态(也就是一个按钮被按下)，或者像`repaint()`或`update()`这样的功能在代码中被手动调用。在决定是否在同一组条件下触发更新事件时，不同的操作系统可能会有不同的行为。如果您正在制作一个需要持续一致的图形更新的程序，请使用计时器手动调用`repaint()`或`update()`。\n\n# 将形状导出到 SVG 文件\n\nSVG 是一种基于 XML 的语言，用于描述二维矢量图形。Qt 提供了将矢量形状保存为 SVG 文件的类。此功能可用于创建类似于 Adobe Illustrator 和 Inkscape 的简单矢量图形编辑器。在下一个示例中，我们将继续使用上一个示例中的相同项目文件。\n\n# 怎么做…\n\n让我们学习如何创建一个在屏幕上显示 SVG 图形的简单程序:\n\n1.  首先，让我们通过右键单击层次窗口上的主窗口小部件并从弹出菜单中选择创建菜单栏选项来创建菜单栏。之后，在菜单栏中添加一个文件选项，并在它下面添加一个另存为 SVG 操作:\n\n![](img/d1c9dd05-26ef-4f7a-a1d9-004a812365a4.png)\n\n2.  之后，您将在 Qt 创建器窗口底部的操作编辑器窗口中看到一个名为 actionSave_as_SVG 的项目。右键单击该项目，然后从弹出菜单中选择“转到插槽...”。现在将出现一个窗口，其中包含可用于特定操作的插槽列表。选择默认信号，称为触发()，然后单击确定按钮:\n\n![](img/cb32f3f3-b45f-4cbc-ab3b-24ecc7ea44d9.png)\n\n3.  单击确定按钮后，Qt 创建者将切换到脚本编辑器。你会意识到一个名为`on_actionSave_as_SVG_triggered()`的槽已经被自动添加到你的主窗口类中。在您的`mainwindow.h`文件的底部，您将看到如下内容:\n\n```cpp\nvoid MainWindow::on_actionSave_as_SVG_triggered() {}\n```\n\n4.  当您单击菜单栏中的另存为 SVG 选项时，将调用前面的函数。我们将在这个函数中编写代码，将所有矢量图形保存到一个 SVG 文件中。为此，我们需要首先在源文件的顶部包含一个名为`QSvgGenerator`的类头。这个头非常重要，因为它是生成 SVG 文件所必需的。然后，我们还需要包含另一个名为`QFileDialog`的类头，用于打开保存对话框:\n\n```cpp\n#include <QtSvg/QSvgGenerator>\n#include <QFileDialog>\n```\n\n5.  我们还需要将`svg`模块添加到我们的项目文件中，如下所示:\n\n```cpp\nQT += core gui svg\n```\n\n6.  然后，在`mainwindow.h`文件内创建一个名为`paintAll()`的新函数，如下代码所示:\n\n```cpp\npublic:\n    explicit MainWindow(QWidget *parent = 0);\n    ~MainWindow();\n    virtual void paintEvent(QPaintEvent *event);\n    void paintAll(QSvgGenerator *generator = 0);\n```\n\n7.  之后，在`mainwindow.cpp`文件中，将所有代码从`paintEvent()`移到`paintAll()`功能。然后，用一个单一的、统一的`QPainter`来替换所有单独的`QPainter`对象，以绘制所有图形。另外，在绘制任何东西之前调用`begin()`功能，在完成绘制之后调用`end()`功能。代码应该如下所示:\n\n```cpp\nvoid MainWindow::paintAll(QSvgGenerator *generator) {\n    QPainter painter;\n    if (engine)\n        painter.begin(engine);\n    else\n        painter.begin(this);\n    painter.setFont(QFont(\"Times\", 14, QFont::Bold));\n    painter.drawText(QPoint(20, 30), \"Testing\");\n    painter.drawLine(QPoint(50, 60), QPoint(100, 100));\n    painter.setBrush(Qt::BDiagPattern);\n    painter.drawRect(QRect(40, 120, 80, 30));\n```\n\n8.  我们接着创建`ellipsePen`和`rectPath`:\n\n```cpp\n    QPen ellipsePen;\n    ellipsePen.setColor(Qt::red);\n    ellipsePen.setStyle(Qt::DashDotLine);\n    painter.setPen(ellipsePen);\n    painter.drawEllipse(QPoint(80, 200), 50, 20);\n\n    QPainterPath rectPath;\n    rectPath.addRect(QRect(150, 20, 100, 50));\n    painter.setPen(QPen(Qt::red, 1, Qt::DashDotLine, Qt::FlatCap, Qt::MiterJoin));\n    painter.setBrush(Qt::yellow);\n    painter.drawPath(rectPath);\n```\n\n9.  然后，我们继续创建`ellipsePath`和`image`:\n\n```cpp\n    QPainterPath ellipsePath;\n    ellipsePath.addEllipse(QPoint(200, 120), 50, 20);\n    painter.setPen(QPen(QColor(79, 106, 25), 5, Qt::SolidLine, Qt::FlatCap, Qt::MiterJoin));\n    painter.setBrush(QColor(122, 163, 39));\n    painter.drawPath(ellipsePath);\n\n    QImage image;\n    image.load(\"tux.png\");\n    painter.drawImage(QPoint(100, 150), image);\n    painter.end();\n}\n```\n\n10.  由于我们已经将所有代码从`paintEvent()`移动到了`paintAll()`，我们现在将调用`paintEvent()`中的`paintAll()`函数，如下所示:\n\n```cpp\nvoid MainWindow::paintEvent(QPaintEvent *event) {\n    paintAll();\n}\n```\n\n11.  然后，我们将编写用于将图形导出为 SVG 文件的代码。代码将被写入由 Qt 生成的名为`on_actionSave_as_SVG_triggered()`的槽函数中。我们首先调用保存文件对话框，并从用户那里获得具有所需文件名的目录路径:\n\n```cpp\nvoid MainWindow::on_actionSave_as_SVG_triggered() {\n    QString filePath = QFileDialog::getSaveFileName(this, \"Save SVG\", \"\", \"SVG files (*.svg)\");\n    if (filePath == \"\")\n        return;\n}\n```\n\n12.  之后，创建一个`QSvgGenerator`对象并将图形保存到一个 SVG 文件中，方法是将`QSvgGenerator`对象传递到`paintAll()`函数:\n\n```cpp\nvoid MainWindow::on_actionSave_as_SVG_triggered() {\n    QString filePath = QFileDialog::getSaveFileName(this, \"Save SVG\", \"\", \"SVG files (*.svg)\");\n    if (filePath == \"\")\n        return;\n    QSvgGenerator generator;\n    generator.setFileName(filePath);\n    generator.setSize(QSize(this->width(), this->height()));\n    generator.setViewBox(QRect(0, 0, this->width(), this->height()));\n    generator.setTitle(\"SVG Example\");\n    generator.setDescription(\"This SVG file is generated by Qt.\");\n    paintAll(&generator);\n }\n```\n\n13.  现在编译并运行程序，您应该可以通过转到文件|另存为 SVG 来导出图形:\n\n![](img/2701d0b1-1ffe-45f8-a1f5-56391389f088.png)\n\n# 它是如何工作的...\n\n默认情况下，`QPainter`将使用其父对象的绘制引擎来绘制分配给它的图形。如果没有给`QPainter`分配任何父级，可以手动给它分配一个绘制引擎，这就是我们在这个例子中所做的。\n\n我们之所以将代码放入`paintAll()`是因为我们希望将相同的代码重用于两个不同的目的:在窗口上显示图形和将图形导出到一个 SVG 文件。请注意，`paintAll()`函数中生成器变量的默认值设置为`0`，这意味着除非指定，否则不需要`QSvgGenerator`对象来运行该函数。稍后，在`paintAll()`功能中，我们检查生成器对象是否存在。如果它确实存在，请将其用作油漆工的绘画引擎，如以下代码所示:\n\n```cpp\nif (engine)\n    painter.begin(engine);\nelse\n    painter.begin(this);\n```\n\n否则，将主窗口传递给`begin()`函数(因为我们是在`mainwindow.cpp`文件中编写代码，所以可以直接用这个来引用主窗口的指针)，这样它就会使用主窗口本身的绘制引擎，也就是说图形会被绘制到主窗口的表面上。在本例中，需要使用单个`QPainter`对象将图形保存到 SVG 文件中。如果您使用多个`QPainter`对象，生成的 SVG 文件将包含多个 XML 头定义，因此该文件将被任何图形编辑器软件视为无效。\n\n`QFileDialog::getSaveFileName()`将打开本机保存文件对话框，供用户选择保存目录并设置所需的文件名。一旦用户完成该操作，完整路径将作为字符串返回，我们将能够将该信息传递给`QSvgGenerator`对象以导出图形。\n\n请注意，在之前的截图中，SVG 文件中的企鹅已经被裁剪了。这是因为 SVG 的画布大小被设置为跟随主窗口的大小。为了帮助可怜的企鹅找回它的身体，在导出 SVG 文件之前，请将窗口放大。\n\n# 还有更多…\n\nSVG 以 XML 格式定义图形。由于它是矢量图形的一种形式，如果放大或调整大小，SVG 文件不会失去任何质量。SVG 格式不仅允许您在工作文件中存储矢量图形，还允许您存储光栅图形和文本，这或多或少类似于 Adobe Illustrator 的格式。SVG 还允许您将图形对象分组、设置样式、转换和合成到以前渲染的对象中。\n\nYou can check out the full specification of SVG graphics at [https://www.w3.org/TR/SVG](https://www.w3.org/TR/SVG).\n\n# 坐标变换\n\n在本例中，我们将学习如何使用坐标转换和计时器来创建实时时钟显示。\n\n# 怎么做…\n\n要创建我们的第一个图形时钟显示，让我们按照以下步骤操作:\n\n1.  首先，创建一个新的`Qt Widgets Application`项目。然后，打开`mainwindow.ui`并像之前一样移除菜单栏、主工具栏和状态栏。\n2.  之后，打开`mainwindow.h`文件，包括以下标题:\n\n```cpp\n#include <QTime>\n#include <QTimer>\n#include <QPainter>\n```\n\n3.  然后，声明`paintEvent()`函数，如是:\n\n```cpp\npublic:\n    explicit MainWindow(QWidget *parent = 0);\n    ~MainWindow();\n    virtual void paintEvent(QPaintEvent *event);\n```\n\n4.  在`mainwindow.cpp`文件中，创建三个数组来存储时针、分针和秒针的形状，其中每个数组包含三组坐标:\n\n```cpp\nvoid MainWindow::paintEvent(QPaintEvent *event) {\n    static const QPoint hourHand[3] = {\n        QPoint(4, 4),\n        QPoint(-4, 4),\n        QPoint(0, -40)\n    };\n    static const QPoint minuteHand[3] = {\n        QPoint(4, 4),\n        QPoint(-4, 4),\n        QPoint(0, -70)\n    };\n    static const QPoint secondHand[3] = {\n        QPoint(2, 2),\n        QPoint(-2, 2),\n        QPoint(0, -90)\n    };\n}\n```\n\n5.  之后，在数组下面添加以下代码来创建画师，并将其移动到主窗口的中心。此外，我们调整了画师的大小，使其非常适合主窗口，即使在调整窗口大小时也是如此:\n\n```cpp\nint side = qMin(width(), height());\nQPainter painter(this);\npainter.setRenderHint(QPainter::Antialiasing);\npainter.translate(width() / 2, height() / 2);\npainter.scale(side / 250.0, side / 250.0);\n```\n\n6.  完成后，我们将使用`for`循环开始绘制表盘。每个刻度盘旋转`6`度，因此`60`刻度盘将完成一整圈。此外，每隔`5`分钟的表盘看起来会稍长一些:\n\n```cpp\nfor (int i = 0; i < 60; ++ i) {\n    if ((i % 5) != 0)\n        painter.drawLine(92, 0, 96, 0);\n    else\n        painter.drawLine(86, 0, 96, 0);\n    painter.rotate(6.0);\n}\n```\n\n7.  然后，我们继续画时钟的指针。每只手的旋转都是根据当前时间和其各自在`360`度上的等效位置来计算的:\n\n```cpp\nQTime time = QTime::currentTime();\n\n// Draw hour hand\npainter.save();\npainter.rotate((time.hour() * 360) / 12);\npainter.setPen(Qt::NoPen);\npainter.setBrush(Qt::black);\npainter.drawConvexPolygon(hourHand, 3);\npainter.restore();\n```\n\n8.  让我们继续画时钟的分针:\n\n```cpp\n// Draw minute hand\npainter.save();\npainter.rotate((time.minute() * 360) / 60);\npainter.setPen(Qt::NoPen);\npainter.setBrush(Qt::black);\npainter.drawConvexPolygon(minuteHand, 3);\npainter.restore();\n```\n\n9.  然后，我们还画了几秒钟的手:\n\n```cpp\n// Draw second hand\npainter.save();\npainter.rotate((time.second() * 360) / 60);\npainter.setPen(Qt::NoPen);\npainter.setBrush(Qt::black);\npainter.drawConvexPolygon(secondHand, 3);\npainter.restore();\n```\n\n10.  最后但同样重要的是，创建一个计时器来每秒刷新图形，以便程序像真正的时钟一样工作:\n\n```cpp\nMainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) {\n    ui->setupUi(this);\n    QTimer* timer = new QTimer(this);\n    timer->start(1000);\n    connect(timer, QTimer::timeout, this, MainWindow::update);\n}\n```\n\n11.  现在编译并运行程序，您应该会看到如下内容:\n\n![](img/7159ea0e-a2ac-4c79-a538-0a0fd5f8796a.png)\n\n# 它是如何工作的...\n\n每个数组包含三个`QPoint`数据实例，它们形成一个细长三角形的形状。然后将数组传递给绘制者，并使用`drawConvexPolygon()`函数将其渲染为凸多边形。在绘制每个时钟指针之前，我们使用`painter.save()`保存`QPainter`对象的状态，然后使用坐标变换继续绘制指针。\n\n一旦完成绘制，我们通过调用`painter.restore()`将绘制者恢复到其先前的状态。该功能将撤销`painter. restore()`之前的所有转换，这样下一个时针将不会继承上一个时针的转换。在不使用`painter.save()`和`painter.restore()`的情况下，我们将不得不在绘制下一只手之前手动改变位置、旋转和缩放。\n\n不使用`painter.save()`和`painter.restore()`的一个很好的例子是在绘制表盘时。由于每个表盘的旋转比前一个增加了 6 度，我们根本不需要保存画师的状态。我们只需要循环调用`painter.rotate(6.0)`，每个表盘将继承前一个表盘的旋转。我们还使用模数运算符(%)来检查刻度盘所代表的单位是否可以除以 5。如果可以的话，我们把它画得稍微长一点。\n\n不使用定时器不断调用`update()`槽，时钟将无法正常工作。这是因为当父小部件的状态没有变化时`paintEvent()`不会被 Qt 调用，在这种情况下，父小部件是主窗口。因此，我们需要手动告诉 Qt，我们需要通过每秒调用`update()` 来刷新图形。我们使用`painter.setRenderHint(QPainter::Antialiasing)`功能在渲染时钟时启用抗锯齿。如果没有抗锯齿，图形将看起来非常参差不齐和像素化:\n\n![](img/488e7d5a-6558-4666-98c4-6c044f60e2ed.png)\n\n# 还有更多…\n\n`QPainter`类在屏幕上渲染图形之前，使用坐标系来确定图形的位置和大小。可以改变这些信息，使图形出现在不同的位置、旋转和大小。这个改变图形坐标信息的过程就是我们所说的**坐标变换**。有几种类型的转换；其中包括平移、旋转、缩放和剪切:\n\n![](img/edbc2919-7cdd-4535-b8bf-1e7e7fbf916a.png)\n\nQt 使用的坐标系原点位于左上角，这意味着 *x* 值向右增加，而 *y* 值向下增加。该坐标系可能与物理设备(如计算机屏幕)使用的坐标系不同。Qt 通过使用`QPaintDevice`类自动处理这个问题，该类将 Qt 的逻辑坐标映射到物理坐标。\n\n`QPainter`提供四种变换操作来执行不同类型的变换:\n\n*   `QPainter::translate()`:将图形的位置偏移一组给定的单位\n*   `QPainter::rotate()`:顺时针方向围绕原点旋转图形\n*   `QPainter::scale()`:以给定的因子偏移图形的大小\n*   `QPainter::shear()`:围绕原点扭曲图形的坐标系\n\n# 在屏幕上显示图像\n\nQt 不仅允许我们在屏幕上绘制形状和图像，还允许我们将多个图像叠加在一起，并使用不同类型的算法组合来自所有图层的像素信息，以创建非常有趣的结果。在这个例子中，我们将学习如何将图像叠加在一起，并对它们应用不同的合成效果。\n\n# 怎么做…\n\n让我们按照以下步骤创建一个简单的演示，展示不同图像合成的效果:\n\n1.  首先，建立一个新的`Qt Widgets Application`项目，删除菜单栏、主工具栏和状态栏，就像我们在第一个食谱中做的那样。\n2.  接下来，将`QPainter`类头添加到`mainwindow.h`文件中:\n\n```cpp\n#include <QPainter>\n```\n\n3.  之后，声明`paintEvent()`虚函数，如是:\n\n```cpp\nvirtual void paintEvent(QPaintEvent* event);\n```\n\n4.  在`mainwindow.cpp`中，我们将首先使用`QImage`类加载几个图像文件:\n\n```cpp\nvoid MainWindow::paintEvent(QPaintEvent* event) {\n    QImage image;\n    image.load(\"checker.png\");\n\n    QImage image2;\n    image2.load(\"tux.png\");\n\n    QImage image3;\n    image3.load(\"butterfly.png\");\n}\n```\n\n5.  然后，创建一个`QPainter`对象，并使用它绘制两对图像，其中一个图像位于另一个图像之上:\n\n```cpp\nQPainter painter(this);\npainter.drawImage(QPoint(10, 10), image);\npainter.drawImage(QPoint(10, 10), image2);\npainter.drawImage(QPoint(300, 10), image);\npainter.drawImage(QPoint(300, 40), image3);\n```\n\n6.  现在编译并运行程序，您应该会看到如下内容:\n\n![](img/b9f5946a-a03a-463b-b156-7f09bfd45d1d.png)\n\n7.  接下来，我们将在屏幕上绘制每个图像之前设置合成模式:\n\n```cpp\nQPainter painter(this);\npainter.setCompositionMode(QPainter::CompositionMode_Difference);\npainter.drawImage(QPoint(10, 10), image);\npainter.setCompositionMode(QPainter::CompositionMode_Multiply);\npainter.drawImage(QPoint(10, 10), image2);\npainter.setCompositionMode(QPainter::CompositionMode_Xor);\npainter.drawImage(QPoint(300, 10), image);\npainter.setCompositionMode(QPainter::CompositionMode_SoftLight);\npainter.drawImage(QPoint(300, 40), image3);\n```\n\n8.  再次编译并运行该程序，现在您将看到如下内容:\n\n![](img/ee58059c-f67e-42db-978c-809cff866850.png)\n\n# 它是如何工作的...\n\n用 Qt 绘制图像时，调用`drawImage()`函数的顺序将决定哪个图像先渲染，哪个图像后渲染。这将影响图像的深度顺序，并产生不同的结果。\n在前面的例子中，我们四次调用`drawImage()`函数，在屏幕上绘制了四个不同的图像。第一个`drawImage()`功能渲染`checker.png`，第二个`drawImage()`功能渲染`tux.png`(企鹅)。稍后渲染的图像将总是出现在其他图像的前面，这就是企鹅出现在格子图案前面的原因。右边的蝴蝶和格子也是如此。即使蝴蝶呈现在其前面，您仍然可以看到棋盘的原因是因为蝴蝶图像不是完全不透明的。\n\n现在，让我们反转渲染序列，看看会发生什么。我们将尝试先渲染企鹅，然后渲染格子。右边的另一对图像也是如此:首先渲染蝴蝶，然后是方格框:\n\n![](img/4da2c695-bd2a-46aa-b531-24a22a054412.png)\n\n要对图像应用构图效果，我们必须在绘制图像之前设置画家的构图模式，方法是调用`painter.setCompositionMode()`函数。您可以通过键入`QPainter::CompositionMode`从自动完成菜单中选择所需的合成模式。\n\n在前面的例子中，我们将`QPainter::CompositionMode_Difference`应用到左边的方格框中，它反转了颜色。接下来，我们将`QPainter::CompositionMode_Overlay`应用于企鹅，使其与棋盘混合，并且能够看到两个图像相互重叠。在右侧，我们将`QPainter::CompositionMode_Xor`应用于检查器，如果源和目标之间存在差异，则会显示颜色；否则，它将被渲染为黑色。因为它比较的是白色背景的差异，格子的不透明部分变成了完全黑色。我们还将`QPainter::CompositionMode_SoftLight`应用于蝴蝶图像。这将像素与对比度降低的背景混合在一起。如果您想在进行下一个渲染之前禁用您刚刚为上一个渲染设置的合成模式，只需将其设置回默认模式，即`QPainter::CompositionMode_SourceOver`。\n\n# 还有更多…\n\n例如，我们可以将多个图像叠加在一起，并使用 Qt 的图像合成功能将它们合并在一起，并根据我们使用的合成模式计算屏幕上的结果像素。这通常用于图像编辑软件，如 Photoshop 和 GIMP，以合成图像层。\nQt 中有 30 多种作曲模式可供选择。一些最常用的模式如下:\n\n*   `Clear`:目的地的像素设置为全透明，与源无关。\n*   `Source`:输出为源像素。该模式与`CompositionMode_Destination`相反。\n*   `Destination`:输出是目的像素。这意味着混合没有效果。该模式与`CompositionMode_Source`相反。\n*   `Source Over`:这通常被称为阿尔法混合。源的 alpha 用于混合目标顶部的像素。这是`QPainter`使用的默认模式。\n\n*   `Destination Over`:输出是源像素之上的目的地阿尔法之间的混合。这种模式的反面是`CompositionMode_SourceOver`。\n*   `Source In`:输出是源，其中 alpha 被目的地的 alpha 缩小。\n*   `Destination In`:输出是目的地，这里的 alpha 被源的 alpha 缩小。该模式与`CompositionMode_SourceIn`相反。\n*   `Source Out`:输出是源，其中 alpha 被目的地的倒数减少。\n*   `Destination Out`:输出是目的地，在这里阿尔法被源的倒数减少。该模式与`CompositionMode_SourceOut`相反。\n*   `Source Atop`:源像素混合在目的像素之上，源像素的 alpha 减去目的像素的 alpha。\n*   `Destination Atop`:目的像素混合在源之上，源像素的 alpha 减去目的像素的 alpha。该模式与`CompositionMode_SourceAtop`相反。\n*   `Xor`:这是 **Exclusive OR** 的缩写，是一种高级的混合模式，主要用于图像分析。使用这种合成模式要复杂得多。首先，源的阿尔法被目标阿尔法的倒数减少。然后，目的地的α被源α的倒数减少。最后，源和目标被合并以产生输出。\n\n更多，可以访问此链接: [pyside.github.io](http://pyside.github.io) 。\n\n下图显示了使用不同合成模式叠加两幅图像的结果:\n\n![](img/d773155d-f0bf-466a-9aae-3b5835eb3334.png)\n\n# 将图像效果应用于图形\n\nQt 为使用`QPainter`类绘制的任何图形添加图像效果提供了一种简单的方法。在本例中，我们将学习如何在屏幕上显示图形之前，对图形应用不同的图像效果，如投影、模糊、着色和不透明度效果。\n\n# 怎么做…\n\n让我们按照以下步骤学习如何将图像效果应用于文本和图形:\n\n1.  创建一个新的`Qt Widgets Application`项目，并移除菜单栏、主工具栏和状态栏。\n\n2.  通过转到文件|新文件或项目并添加项目所需的所有图像来创建新的资源文件:\n\n![](img/1f75750e-a670-45f8-abbe-4461609534a5.png)\n\n3.  接下来，打开`mainwindow.ui`并在窗口中添加四个标签。其中两个标签是文本，另外两个标签将加载我们刚刚添加到资源文件中的图像:\n\n![](img/a4830aeb-15e1-4017-becd-5baf66d84e53.png)\n\n4.  您可能已经注意到字体大小比默认大小大得多。这可以通过向标签小部件添加样式表来实现，例如，如下所示:\n\n```cpp\nfont: 26pt \"MS Shell Dlg 2\";\n```\n\n5.  之后，打开`mainwindow.cpp`并在源代码顶部包含以下标题:\n\n```cpp\n#include <QGraphicsBlurEffect>\n#include <QGraphicsDropShadowEffect>\n#include <QGraphicsColorizeEffect>\n#include <QGraphicsOpacityEffect>\n```\n\n6.  然后，在`MainWindow`类的构造函数中，添加以下代码来创建一个`DropShadowEffect`，并将其应用于其中一个标签:\n\n```cpp\nMainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) {\n    ui->setupUi(this);\n    QGraphicsDropShadowEffect* shadow = new\n    QGraphicsDropShadowEffect();\n    shadow->setXOffset(4);\n    shadow->setYOffset(4);\n    ui->label->setGraphicsEffect(shadow);\n}\n```\n\n7.  接下来，我们将创建一个`ColorizedEffect`并将其应用于其中一个图像，在本例中是蝴蝶。我们还将效果颜色设置为红色:\n\n```cpp\nQGraphicsColorizeEffect* colorize = new QGraphicsColorizeEffect();\ncolorize->setColor(QColor(255, 0, 0));\nui->butterfly->setGraphicsEffect(colorize);\n```\n\n8.  完成后，创建一个`BlurEffect`并将其`radius`设置为`12`。然后，将图形效果应用于另一个标签:\n\n```cpp\nQGraphicsBlurEffect* blur = new QGraphicsBlurEffect();\nblur->setBlurRadius(12);\nui->label2->setGraphicsEffect(blur);\n```\n\n9.  最后，创建一个`alpha`效果并将其应用到`penguin`图像。我们将`opacity`值设置为`0.2`，这意味着 20%的不透明度:\n\n```cpp\nQGraphicsOpacityEffect* alpha = new QGraphicsOpacityEffect();\nalpha->setOpacity(0.2);\nui->penguin->setGraphicsEffect(alpha);\n```\n\n10.  现在编译并运行该程序，您应该能够看到如下内容:\n\n![](img/82dfc447-7029-46a6-ad00-cc1b3acd6704.png)\n\n# 它是如何工作的...\n\n每个图形效果都是自己的类，继承了`QGraphicsEffect`父类。您可以通过创建一个继承`QGraphicsEffect`的新类并重新实现其中的一些功能来创建自己的自定义效果。\n每个效果都有一组专门为其创建的变量。例如，您可以设置彩色效果的颜色，但模糊效果中没有这样的变量。这是因为每个效果与其他效果有很大的不同，这也是为什么它需要成为自己的一个类，而不是对所有不同的效果使用同一个类。\n\n一次只能向小部件添加一个图形效果。如果您添加了多个效果，只有最后一个效果会应用到小部件，因为它会替换前一个效果。除此之外，请注意，如果您创建一个图形效果，比如投影效果，您也不能将其分配给两个不同的小部件，因为它只会分配给您应用它的最后一个小部件。如果需要将相同类型的效果应用于几个不同的小部件，可以创建几个相同类型的图形效果，并将它们分别应用于各自的小部件。\n\n# 还有更多…\n\n目前，Qt 支持模糊、阴影、着色和不透明效果。这些效果可以通过调用以下类来使用:`QGraphicsBlurEffect`、`QGraphicsDropShadowEffect`、`QGraphicsColorizeEffect`和`QGraphicsOpacityEffect`。所有这些类都是从`QGraphicsEffect`类继承而来的。您也可以通过创建`QGrapicsEffect`的子类(或任何其他现有效果)并重新实现`draw()`功能来创建自己的自定义图像效果。\n\n图形效果仅改变源的边框。如果想增加边框的边距，重新实现虚拟`boundingRectFor()`功能，每当这个矩形发生变化时，调用`updateBoundingRect()`通知框架。\n\n# 创建基本的绘画程序\n\n既然我们已经了解了这么多关于`QPainter`课程以及如何使用它在屏幕上显示图形的知识，我想是时候让我们做一些有趣的事情来将我们的知识付诸实践了。\n\n在这个食谱中，我们将学习如何制作一个基本的绘画程序，允许我们用不同的画笔大小和颜色在画布上绘制线条。我们还将学习如何使用`QImage`类和鼠标事件来构建绘画程序。\n\n# 怎么做…\n\n让我们通过以下步骤开始我们有趣的项目:\n\n1.  同样，我们从创建一个新的`Qt Widgets Application`项目并移除工具栏和状态栏开始。这次我们将保留菜单栏。\n2.  之后，像这样设置菜单栏:\n\n![](img/789427f1-445d-4489-a8de-f72e403e48f3.png)\n\n3.  我们将暂时保持菜单栏不变，因此让我们进入`mainwindow.h`文件。首先，包含项目所需的下列头文件:\n\n```cpp\n#include <QPainter>\n#include <QMouseEvent>\n#include <QFileDialog>\n```\n\n4.  接下来，声明我们将用于这个项目的变量，如下所示:\n\n```cpp\nprivate:\n    Ui::MainWindow *ui;\n    QImage image;\n    bool drawing;\n    QPoint lastPoint;\n    int brushSize;\n    QColor brushColor;\n```\n\n5.  然后，声明从`QWidget`类继承的事件回调函数。当相应的事件发生时，Qt 将触发这些功能。我们将覆盖这些函数，并告诉 Qt 在调用这些事件时该做什么:\n\n```cpp\npublic:\n    explicit MainWindow(QWidget *parent = 0);\n    ~MainWindow();\n    virtual void mousePressEvent(QMouseEvent *event);\n    virtual void mouseMoveEvent(QMouseEvent *event);\n    virtual void mouseReleaseEvent(QMouseEvent *event);\n    virtual void paintEvent(QPaintEvent *event);\n    virtual void resizeEvent(QResizeEvent *event);\n```\n\n6.  之后，转到`mainwindow.cpp`文件，将以下代码添加到类构造函数中，以设置一些变量:\n\n```cpp\nMainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) {\n    ui->setupUi(this);\n    image = QImage(this->size(), QImage::Format_RGB32);\n    image.fill(Qt::white);\n    drawing = false;\n    brushColor = Qt::black;\n    brushSize = 2;\n}\n```\n\n7.  接下来，我们将构造`mousePressEvent()`事件，并告诉 Qt 在按下鼠标左键时要做什么:\n\n```cpp\nvoid MainWindow::mousePressEvent(QMouseEvent *event) {\n    if (event->button() == Qt::LeftButton) {\n        drawing = true;\n        lastPoint = event->pos();\n    }\n}\n```\n\n8.  然后，我们将构造`mouseMoveEvent()`事件，并告诉 Qt 当鼠标移动时该做什么。在这种情况下，如果按住鼠标左键，我们希望在画布上绘制线条:\n\n```cpp\nvoid MainWindow::mouseMoveEvent(QMouseEvent *event) {\n    if ((event->buttons() & Qt::LeftButton) && drawing) {\n        QPainter painter(&image);\n        painter.setPen(QPen(brushColor, brushSize, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin));\n        painter.drawLine(lastPoint, event->pos());\n        lastPoint = event->pos();\n        this->update();\n    }\n}\n```\n\n9.  之后，我们还将构造`mouseReleaseEvent()`事件，当鼠标按钮被释放时将被触发:\n\n```cpp\nvoid MainWindow::mouseReleaseEvent(QMouseEvent *event) {\n    if (event->button() == Qt::LeftButton) {\n        drawing = false;\n    }\n}\n```\n\n10.  完成后，我们将进入`paintEvent()`事件，与我们在前面章节中看到的其他示例相比，这个事件非常简单:\n\n```cpp\nvoid MainWindow::paintEvent(QPaintEvent *event) {\n    QPainter canvasPainter(this);\n    canvasPainter.drawImage(this->rect(), image, image.rect());\n}\n```\n\n11.  还记得我们有一个无所事事的菜单栏吗？让我们右键单击图形用户界面编辑器下面的每个操作，并在弹出菜单中选择转到插槽。我们想告诉 Qt 当菜单栏上的每个选项都被选中时该怎么做:\n\n![](img/56eee704-6677-42ec-ac08-f724b63961a1.png)\n\n12.  然后，选择名为已触发()的默认插槽，并按下确定按钮。Qt 会在你的`mainwindow.h`和`mainwindow.cpp`文件中自动生成一个新的槽函数。完成所有操作后，您应该会在您的`mainwindow.h`文件中看到类似以下内容:\n\n```cpp\nprivate slots:\n    void on_actionSave_triggered();\n    void on_actionClear_triggered();\n    void on_action2px_triggered();\n    void on_action5px_triggered();\n    void on_action10px_triggered();\n    void on_actionBlack_triggered();\n    void on_actionWhite_triggered();\n    void on_actionRed_triggered();\n    void on_actionGreen_triggered();\n    void on_actionBlue_triggered();\n```\n\n13.  接下来，我们将告诉 Qt 当这些槽中的每一个被触发时该做什么:\n\n```cpp\nvoid MainWindow::on_actionSave_triggered() {\n    QString filePath = QFileDialog::getSaveFileName(this, \"Save Image\", \"\", \"PNG (*.png);;JPEG (*.jpg *.jpeg);;All files (*.*)\");\n    if (filePath == \"\")\n        return;\n    image.save(filePath);\n}\nvoid MainWindow::on_actionClear_triggered() {\n    image.fill(Qt::white);\n    this->update();\n}\n```\n\n14.  然后，我们继续实施其他插槽:\n\n```cpp\nvoid MainWindow::on_action2px_triggered() {\n    brushSize = 2;\n}\nvoid MainWindow::on_action5px_triggered() {\n    brushSize = 5;\n}\nvoid MainWindow::on_action10px_triggered() {\n    brushSize = 10;\n}\nvoid MainWindow::on_actionBlack_triggered() {\n    brushColor = Qt::black;\n}\n```\n\n15.  最后，我们实现剩余的槽函数:\n\n```cpp\nvoid MainWindow::on_actionWhite_triggered() {\n    brushColor = Qt::white;\n}\nvoid MainWindow::on_actionRed_triggered() {\n    brushColor = Qt::red;\n}\nvoid MainWindow::on_actionGreen_triggered() {\n    brushColor = Qt::green;\n}\nvoid MainWindow::on_actionBlue_triggered() {\n    brushColor = Qt::blue;\n}\n```\n\n16.  如果我们现在编译并运行程序，我们将得到一个简单但可用的画图程序:\n\n![](img/872f6717-1e80-4f3b-b228-a5f89a5ca002.png)\n\n# 它是如何工作的...\n\n在这个例子中，我们在程序启动时创建了一个`QImage`小部件。这个小部件充当画布，当窗口调整大小时，它将跟随窗口的大小。为了在画布上绘制一些东西，我们需要使用 Qt 提供的鼠标事件。这些事件将告诉我们光标的位置，我们将能够使用这些信息来改变画布上的像素。\n\n我们使用一个叫做`drawing`的布尔变量，让程序知道当按下鼠标按钮时是否应该开始绘图。在这种情况下，当按下鼠标左键时，`drawing`变量将被设置为`true`。我们还在鼠标左键按下时将当前光标位置保存到`lastPoint`变量，这样 Qt 就知道应该从哪里开始绘图了。当鼠标移动时，`mouseMoveEvent()`事件将由 Qt 触发。这是我们需要检查`drawing`变量是否设置为`true`的地方。如果是，那么`QPainter`可以根据我们提供的笔刷设置开始在`QImage`小部件上画线。画笔设置由`brushColor`和`brushSize` **组成。**这些设置被保存为变量，可以通过从菜单栏中选择不同的设置进行更改。\n\n用户在画布上画画时，请记得调用`update()`功能。否则，即使我们更改了画布的像素信息，画布也会保持空白。当我们从菜单栏中选择文件|清除来重置画布时，我们还必须调用`update()`功能。\n\n在这个例子中，我们使用`QImage::save()`来保存图像文件，非常简单明了。我们使用文件对话框让用户决定在哪里保存图像及其所需的文件名。然后，我们把信息传递给`QImage`，剩下的就由它自己来做了。如果我们没有为`QImage::save()`函数指定文件格式，`QImage`将通过查看所需文件名的扩展名来尝试找出它。\n\n# 在 QML 渲染 2D 画布\n\n在本章前面的所有示例中，我们已经讨论了使用 Qt 的 C++ API 渲染 2D 图形的方法和技术。然而，我们还没有学会如何使用强大的 QML 脚本来实现类似的结果。\n\n# 怎么做…\n\n在这个项目中，我们将做一些完全不同的事情:\n\n1.  像往常一样，我们应该做的第一步是通过转到文件|新文件或项目并选择 Qt 快速应用-空作为项目模板来创建一个新项目:\n\n![](img/8a804e02-515e-49c8-96f1-2b57ab741f62.png)\n\n2.  创建完新项目后，打开`main.qml`，它列在项目窗格的`qml.qrc`下。之后，为窗口设置一个标识，并将其`width`和`height`调整为更大的值，如下所示:\n\n```cpp\nimport QtQuick 2.11\nimport QtQuick.Window 2.11\n\nWindow {\n    id: myWindow\n    visible: true\n    width: 640\n    height: 480\n    title: qsTr(\"Hello World\")\n}\n```\n\n3.  然后，在`myWindow`下添加一个`Canvas`对象，称之为`myCanvas`。之后，我们将其`width`和`height`设为与`myWindow`相同:\n\n```cpp\nWindow {\n    id: myWindow\n    visible: true\n    width: 640\n    height: 480\n    Canvas {\n id: myCanvas\n width: myWindow.width\n height: myWindow.height\n }\n}\n```\n\n5.  接下来，我们定义`onPaint`事件被触发时会发生什么；在这种情况下，我们将在窗口上画一个十字:\n\n```cpp\nCanvas {\n    id: myCanvas\n    width: myWindow.width\n    height: myWindow.height\n    onPaint: {\n var context = getContext('2d')\n context.fillStyle = 'white'\n context.fillRect(0, 0, width, height)\n context.lineWidth = 2\n context.strokeStyle = 'black'\n```\n\n6.  让我们继续编写代码，就像这样:\n\n```cpp\n // Draw cross\n context.beginPath()\n context.moveTo(50, 50)\n context.lineTo(100, 100)\n context.closePath()\n context.stroke()\n context.beginPath()\n context.moveTo(100, 50)\n context.lineTo(50, 100)\n context.closePath()\n context.stroke()\n }\n}\n```\n\n7.  之后，我们添加以下代码在十字旁边画一个勾号:\n\n```cpp\n// Draw tick\ncontext.beginPath()\ncontext.moveTo(150, 90)\ncontext.lineTo(158, 100)\ncontext.closePath()\ncontext.stroke()\ncontext.beginPath()\ncontext.moveTo(180, 100)\ncontext.lineTo(210, 50)\ncontext.closePath()\ncontext.stroke()\n```\n\n8.  然后，通过添加以下代码绘制一个三角形:\n\n```cpp\n// Draw triangle\ncontext.lineWidth = 4\ncontext.strokeStyle = \"red\"\ncontext.fillStyle = \"salmon\"\ncontext.beginPath()\ncontext.moveTo(50,150)\ncontext.lineTo(150,150)\ncontext.lineTo(50,250)\ncontext.closePath()\ncontext.fill()\ncontext.stroke()\n```\n\n9.  然后，用下面的代码画一个半圆和一个整圆:\n\n```cpp\n// Draw circle\ncontext.lineWidth = 4\ncontext.strokeStyle = \"blue\"\ncontext.fillStyle = \"steelblue\"\nvar pi = 3.141592653589793\ncontext.beginPath()\ncontext.arc(220, 200, 60, 0, pi, true)\ncontext.closePath()\ncontext.fill()\ncontext.stroke()\n```\n\n10.  然后，我们画一条弧线:\n\n```cpp\ncontext.beginPath()\ncontext.arc(220, 280, 60, 0, 2 * pi, true)\ncontext.closePath()\ncontext.fill()\ncontext.stroke()\n```\n\n11.  最后，我们从一个文件中绘制一幅 2D 图像:\n\n```cpp\n// Draw image\ncontext.drawImage(\"tux.png\", 280, 10, 150, 174)\n```\n\n12.  但是，仅使用前面的代码无法在屏幕上成功渲染图像，因为您还必须事先加载图像文件。在`Canvas`对象内添加以下代码，要求 QML 在程序启动时加载图像文件，然后在加载图像时调用`requestPaint()`信号，从而触发`onPaint()`事件槽:\n\n```cpp\nonImageLoaded: requestPaint();\nonPaint: {\n    // The code we added previously\n}\n```\n\n13.  然后在项目面板上右键打开`qml.qrc`，选择在编辑器中打开。之后，将`tux.png`图像文件添加到我们的项目资源中:\n\n![](img/81a659f4-fdd0-4354-97ee-1ef4977ced2c.png)\n\n14.  现在构建并运行程序，您应该会得到以下结果:\n\n![](img/69329777-15bd-4c25-90e3-372518f897e7.png)\n\n这不是很好吗？"
  },
  {
    "path": "docs/qt5-cpp-gui-prog-cb/05.md",
    "content": "# 五、OpenGL 实现\n\n在这一章中，我们将介绍以下食谱:\n\n*   在 Qt 中设置**打开图形库** ( **OpenGL** )\n*   你好世界！\n*   渲染 2D 形状\n*   渲染三维形状\n*   OpenGL 中的纹理\n*   OpenGL 中的基本照明\n*   使用键盘控制移动对象\n*   QML 的三维画布\n\n# 介绍\n\n本章我们将学习如何使用 OpenGL，一个强大的渲染**应用接口** ( **API** ，并将其与 Qt 相结合。OpenGL 是一个跨语言、跨平台的 API，用于通过我们计算机图形芯片内的**图形处理单元** ( **GPU** )在屏幕上绘制 2D 和 3D 图形。在本章中，我们将学习 OpenGL 3 而不是 2，因为尽管与较新的可编程管道相比，固定功能管道对初学者来说更容易掌握，但它被认为是遗留代码，已被大多数现代 3D 渲染软件所否决。Qt 5 支持这两个版本，所以如果你需要你的软件向后兼容，切换到 OpenGL 2 应该没有问题。\n\n# 技术要求\n\n本章的技术要求包括 Qt 5.11.2 MinGW 32 位、Qt Creator 4.8.2 和 Windows 10。\n\n本章使用的所有代码都可以从下面的 GitHub 链接下载:[https://GitHub . com/PacktPublishing/Qt5-CPP-GUI-编程-cook book-第二版/树/master/Chapter05](https://github.com/PacktPublishing/Qt5-CPP-GUI-Programming-Cookbook-Second-Edition/tree/master/Chapter05) 。\n\n查看以下视频，查看正在运行的代码:[http://bit.ly/2FrX4Ho](http://bit.ly/2FrX4Ho)\n\n# 在 Qt 中设置 OpenGL\n\n在这个食谱中，我们将学习如何在 Qt 5 中设置 OpenGL。\n\n# 怎么做…\n\n按照以下步骤学习如何在 Qt 中设置 OpenGL:\n\n1.  通过转到文件|新文件或项目，创建一个新的 Qt 小部件应用。\n2.  我们将删除`mainwindow.ui`文件，因为我们不打算在本例中使用它。右键单击`mainwindow.ui`文件，从下拉菜单中选择删除文件。然后，会出现一个消息框，要求您确认。勾选永久删除文件，然后按确定按钮。\n3.  对`mainwindow.h`和`mainwindow.cpp`重复步骤 2。我们在这个项目中也不需要这两个文件。\n4.  打开您的项目文件(`.pro`)并通过在`QT +=`后面添加一个`opengl`关键字将`OpenGL`模块添加到您的项目中，如下所示:\n\n```cpp\nQT += core gui opengl\n```\n\n5.  您还需要在项目文件中添加另一行，以便它在启动过程中加载`OpenGL`和`GLu` (OpenGL 实用程序)库。没有这两个库，您的程序将无法运行:\n\n```cpp\nLIBS += -lopengl32 -lglu32\n```\n\n6.  打开`main.cpp`并用`QtOpenGL`表头替换`mainwindow.h`:\n\n```cpp\n#include <QtOpenGL>\n```\n\n7.  从您的`main.cpp`文件中删除所有与`MainWindow`类相关的代码，并用以下代码片段中突出显示的代码替换它:\n\n```cpp\n#include <QApplication>\n#include <QtOpenGL>\n\nint main(int argc, char *argv[]) {\n  QApplication app(argc, argv);\n\n  QOpenGLWindow window;\n window.setTitle(\"Hello World!\");\n window.resize(640, 480);\n window.show();\n\n  return app.exec();\n}\n```\n\n8.  如果您现在编译并运行项目，您将看到一个黑色背景的空窗口。别担心——你的程序现在运行在 OpenGL 上:\n\n![](img/99c2309a-54aa-455a-a790-2451fffa445d.png)\n\n# 它是如何工作的...\n\n为了访问与 OpenGL 相关的头文件，如`QtOpenGL`、`QOpenGLFunctions`，必须将`OpenGL`模块添加到项目文件(`.pro`)中。我们在主窗口中使用了`QOpenGLWindow`类，而不是`QMainWindow`，因为它被设计来轻松创建执行 OpenGL 渲染的窗口，并且由于其小部件模块中没有依赖项，因此与`QOpenGLWidget`相比，它提供了更好的性能。\n\n我们必须调用`setSurfaceType(QWindow::OpenGLSurface)`来告诉 Qt 我们更喜欢使用 OpenGL 将图像渲染到屏幕上，而不是`QPainter`。`QOpenGLWindow`类提供了几个虚拟功能(`initializeGL()`、`resizeGL()`、`paintGL()`等)，方便我们设置 OpenGL，进行图形渲染。在下面的例子中，我们将学习如何使用这些函数。\n\n# 还有更多…\n\nOpenGL 是一个跨语言、跨平台的应用编程接口，用于通过我们计算机图形芯片中的图形处理器在屏幕上绘制 2D 和 3D 图形。这些年来，计算机图形技术发展迅速——如此之快，以至于软件行业很难跟上它的步伐。\n\n2008 年，维护和开发 OpenGL 的公司 Khronos Group 宣布发布 OpenGL 3.0 规范，在整个行业引起了巨大的骚动和争议。这主要是因为 OpenGL 3.0 本应摒弃 OpenGL API 的整个固定功能管道，而对于大玩家来说，一夜之间从固定功能管道突然切换到可编程管道简直是一项不可能完成的任务。这导致了 OpenGL 的两个不同的主要版本被维护。\n\n在本章中，我们将使用较新的 OpenGL 3，而不是较旧的、已弃用的 OpenGL 2。这两个版本的编码风格和语法差异很大，切换起来非常麻烦。然而，性能的提高将使它值得花时间切换到 OpenGL 3。\n\n# 你好世界！\n\n在本章中，我们将学习如何在 Qt 5 中使用 OpenGL 3。`glBegin`、`glVertex2f`、`glColor3f`、`glMatrixMode`、`glLoadIdentity`等常见的 OpenGL 功能都已从 OpenGL 3 中移除。OpenGL 3 使用顶点缓冲区对象将数据批量发送到 GPU，而不是通过`glVertex2f()`等功能逐个发送，在等待 CPU 逐个提交数据的同时减缓了渲染速度。因此，我们将所有数据打包到顶点缓冲对象中，并将其全部发送到一个巨大的包中，并指示 GPU 通过着色器编程计算结果像素。我们还将学习如何通过一种叫做 **OpenGL 着色语言** ( **GLSL** )的类似 C 的编程语言来创建简单的着色器程序。\n\n# 怎么做…\n\n让我们从以下步骤开始:\n\n1.  我们将创建一个名为`RenderWindow`的新类，它继承自`QOpenGLWindow`类。转到文件|新建文件或项目，然后在文件和类类别下选择 C++ 类。命名类`RenderWindow`，将其基类设置为`QOpenGLWindow`。然后，继续创建 C++ 类:\n\n![](img/e8c864af-1fe4-4b4d-8123-6c6c0d03896c.png)\n\n2.  转到我们刚刚创建的`renderwindow.h`文件，并在源代码顶部添加以下标题:\n\n```cpp\n#include <GL/glu.h>\n#include <QtOpenGL>\n#include <QSurfaceFormat>\n#include <QOpenGLFunctions>\n#include <QOpenGLWindow>\n#include <QOpenGLBuffer>\n#include <QOpenGLVertexArrayObject>\n#include <QOpenGLShader>\n#include <QOpenGLShaderProgram>\n```\n\n3.  我们需要创建几个函数和变量，如下所示:\n\n```cpp\nclass RenderWindow : public QOpenGLWindow {\npublic:\n    RenderWindow();\n\nprotected:\n    void initializeGL();\n void paintGL();\n void paintEvent(QPaintEvent *event);\n void resizeEvent(QResizeEvent *event);\n```\n\n4.  我们将继续并添加一些私有变量:\n\n```cpp\nprivate:\n    QOpenGLContext* openGLContext;\n QOpenGLFunctions* openGLFunctions;\n QOpenGLShaderProgram* shaderProgram;\n QOpenGLVertexArrayObject* vao;\n QOpenGLBuffer* vbo_vertices;\n};\n```\n\n5.  打开`renderwindow.cpp`定义类构造函数，如下。我们必须告诉渲染窗口使用 OpenGL 曲面类型；启用运行 3.2 版的核心配置文件(而不是兼容性配置文件)；创建一个 OpenGL 上下文；最后，将我们刚刚创建的概要文件应用到上下文中:\n\n```cpp\nRenderWindow::RenderWindow() {\n    setSurfaceType(QWindow::OpenGLSurface);\n\n    QSurfaceFormat format;\n    format.setProfile(QSurfaceFormat::CoreProfile);\n    format.setVersion(3, 2);\n    setFormat(format);\n\n    openGLContext = new QOpenGLContext();\n    openGLContext->setFormat(format);\n    openGLContext->create();\n    openGLContext->makeCurrent(this);\n}\n```\n\n6.  我们需要定义`initializeGL()`函数，如下。此函数将在渲染开始前调用。首先，我们定义顶点和片段着色器:\n\n```cpp\nvoid RenderWindow::initializeGL() {\n    openGLFunctions = openGLContext->functions();\n\n    static const char *vertexShaderSource =\n    \"#version 330 core\\n\"\n    \"layout(location = 0) in vec2 posAttr;\\n\"\n    \"void main() {\\n\"\n    \"gl_Position = vec4(posAttr, 0.0, 1.0); }\";\n\n    static const char *fragmentShaderSource =\n    \"#version 330 core\\n\"\n    \"out vec4 col;\\n\"\n    \"void main() {\\n\"\n    \"col = vec4(1.0, 0.0, 0.0, 1.0); }\";\n```\n\n7.  我们启动`shaderProgram`并声明一个`vertices`数组。然后，我们还创建了一个`QOpenGLVertexArrayObject`对象:\n\n```cpp\n    shaderProgram = new QOpenGLShaderProgram(this);\n    shaderProgram->addShaderFromSourceCode(QOpenGLShader::Vertex, vertexShaderSource);\n    shaderProgram->addShaderFromSourceCode(QOpenGLShader::Fragment, fragmentShaderSource);\n    shaderProgram->link();\n\n    GLfloat vertices[] = {\n    -1.0f, -1.0f,\n    1.0f, -1.0f,\n    0.0f, 1.0f };\n\n    vao = new QOpenGLVertexArrayObject();\n    vao->create();\n    vao->bind();\n```\n\n8.  让我们通过定义`vbo_vertices`继续编写我们的代码:\n\n```cpp\n    vbo_vertices = new QOpenGLBuffer(QOpenGLBuffer::VertexBuffer);\n    vbo_vertices->create();\n    vbo_vertices->setUsagePattern(QOpenGLBuffer::StaticDraw);\n    vbo_vertices->bind();\n    vbo_vertices->allocate(vertices, sizeof(vertices) * sizeof(GLfloat));\n\n    vao->release();\n}\n```\n\n9.  我们将从给`paintEvent()`函数添加一些代码开始:\n\n```cpp\nvoid RenderWindow::paintEvent(QPaintEvent *event) {\n    Q_UNUSED(event);\n    glViewport(0, 0, width(), height());\n    glClearColor(0.39f, 0.58f, 0.93f, 1.f);\n    glClear(GL_COLOR_BUFFER_BIT);\n```\n\n10.  然后我们将在调用`glDrawArrays()`之前绑定 VAO 和着色器程序:\n\n```cpp\n    vao->bind();\n    shaderProgram->bind();\n    shaderProgram->bindAttributeLocation(\"posAttr\", 0);\n    shaderProgram->enableAttributeArray(0);\n    shaderProgram->setAttributeBuffer(0, GL_FLOAT, 0, 2);\n    glDrawArrays(GL_TRIANGLES, 0, 3);\n    shaderProgram->release();\n    vao->release();\n}\n```\n\n11.  通过添加以下代码，可以在调整渲染窗口大小时刷新视口:\n\n```cpp\nvoid RenderWindow::resizeEvent(QResizeEvent *event) {\n    Q_UNUSED(event);\n    glViewport(0, 0, this->width(), this->height());\n    this->update();\n}\n```\n\n12.  如果您现在编译并运行项目，您应该能够看到在蓝色背景前面绘制了一个红色矩形:\n\n![](img/422c9dfb-c4e0-4bf6-8dd5-c27581d029b2.png)\n\n# 它是如何工作的...\n\n我们必须将 OpenGL 版本设置为 3.x，将表面格式设置为核心配置文件，这样我们就可以访问更新的着色器管道，这与旧的、不推荐使用的兼容性配置文件完全不同。OpenGL 2.x 仍然存在于兼容性配置文件中，只是为了允许 OpenGL 程序在旧硬件上运行。创建的配置文件必须应用到 OpenGL 上下文，然后才能工作。\n\n在 OpenGL 3 和更高版本中，大部分计算都是通过着色器程序在 GPU 中完成的，因为所有常见的固定函数现在都已完全弃用。因此，在前面的示例中，我们创建了一个非常简单的顶点着色器和片段着色器。\n\n着色器程序由三个不同的部分组成:几何着色器(可选)、顶点着色器和片段着色器。几何着色器在将数据传递给顶点着色器之前计算几何的创建；顶点着色器在将数据传递给片段着色器之前处理顶点的位置和运动；最后，片段着色器计算并在屏幕上显示结果像素。\n\n在前面的例子中，我们只使用了顶点和片段着色器，并排除了几何着色器，因为它是可选的。您可以将 GLSL 代码保存在一个文本文件中，并通过调用`addShaderFromFile()`将其加载到您的 Qt 5 程序中，但是由于我们的着色器非常简单和简短，所以我们只是直接在我们的 C++ 源代码中定义它。\n\n之后，我们使用一种叫做**顶点缓冲对象** ( **VBO** )的东西来批量存储顶点位置，然后将其发送到 GPU。我们还可以使用 VBO 存储其他信息，如法线、纹理坐标和顶点颜色。你可以发送任何你想要的东西到图形处理器，只要它匹配你的着色器代码中的输入。然后，我们将 VBO 添加到一个**顶点数组对象** ( **VAO** )中，并将整个 VAO 发送到 GPU 进行处理。你可以在 VAO 中添加许多不同的 vbo，因为 VAO 就像任何普通的 C++ 数组一样。\n\n就像我们前几章所学的一样，所有的绘图都发生在`paintEvent()`函数内，只有在 Qt 认为需要刷新屏幕时才会调用。要强制 Qt 更新屏幕，手动调用`update()`。此外，我们必须通过调用`glViewport(x, y ,width, height)`来更新视窗屏幕的大小。\n\n# 渲染 2D 形状\n\n由于我们已经学习了如何在屏幕上绘制第一个矩形，我们将在这一部分进一步增强它。我们将采取前面的例子，并从那里继续。\n\n# 怎么做…\n\n让我们从这个例子开始:\n\n1.  打开`renderwindow.h`，再添加两个 vbo，一个叫做`vbo_vertices2`，另一个叫做`vbo_colors`，如下代码所示:\n\n```cpp\nprivate:\n    QOpenGLContext* openGLContext;\n    QOpenGLFunctions* openGLFunctions;\n    QOpenGLShaderProgram* shaderProgram;\n    QOpenGLVertexArrayObject* vao;\n    QOpenGLBuffer* vbo_vertices;\n    QOpenGLBuffer* vbo_vertices2;\n    QOpenGLBuffer* vbo_colors;\n```\n\n2.  打开`renderwindow.cpp`并将以下代码添加到着色器代码中，如以下片段中突出显示的那样:\n\n```cpp\nstatic const char *vertexShaderSource =\n    \"#version 330 core\\n\"\n    \"layout(location = 0) in vec2 posAttr;\\n\"\n \"layout(location = 1) in vec3 colAttr;\\n\"\n \"out vec3 fragCol;\\n\"\n    \"void main() {\\n\"\n \"fragCol = colAttr;\\n\"\n    \"gl_Position = vec4(posAttr, 1.0, 1.0); }\";\n```\n\n3.  将高亮显示的代码添加到片段着色器，如下所示:\n\n```cpp\nstatic const char *fragmentShaderSource =\n    \"#version 330 core\\n\"\n \"in vec3 fragCol;\\n\"\n    \"out vec4 col;\\n\"\n    \"void main() {\\n\"\n \"col = vec4(fragCol, 1.0); }\";\n```\n\n4.  将`vertices`数组改为如下内容:\n\n```cpp\nGLfloat vertices[] = {\n    -0.3f, -0.5f,\n    0.8f, -0.4f,\n    0.2f, 0.6f };\n\nGLfloat vertices2[] = {\n    0.5f, 0.3f,\n    0.4f, -0.8f,\n    -0.6f, -0.2f };\n\nGLfloat colors[] = {\n    1.0f, 0.0f, 0.0f,\n    0.0f, 1.0f, 0.0f,\n    0.0f, 0.0f, 1.0f };\n```\n\n5.  由于在前面的例子中我们已经初始化了`vbo_vertices`，这次我们只需要初始化另外两个 vbo，即`vbo_vertices`和`vbo_colors`:\n\n```cpp\nvbo_vertices2 = new QOpenGLBuffer(QOpenGLBuffer::VertexBuffer);\nvbo_vertices2->create();\nvbo_vertices2->setUsagePattern(QOpenGLBuffer::StaticDraw);\nvbo_vertices2->bind();\nvbo_vertices2->allocate(vertices2, sizeof(vertices2) * sizeof(GLfloat));\n\nvbo_colors = new QOpenGLBuffer(QOpenGLBuffer::VertexBuffer);\nvbo_colors->create();\nvbo_colors->setUsagePattern(QOpenGLBuffer::StaticDraw);\nvbo_colors->bind();\nvbo_colors->allocate(colors, sizeof(colors) * sizeof(GLfloat));\n```\n\n6.  在我们开始使用`glDrawArrays()`绘制三角形之前，我们还必须将`vbo_colors`的数据添加到着色器的`colAttr`属性中。在将数据发送到着色器之前，请确保调用`bind()`将 VBO 设置为当前活动的 VBO。位置标识(在本例中为 0 和 1)必须与着色器中使用的位置标识匹配:\n\n```cpp\nvbo_vertices->bind();\nshaderProgram->bindAttributeLocation(\"posAttr\", 0);\nshaderProgram->enableAttributeArray(0);\nshaderProgram->setAttributeBuffer(0, GL_FLOAT, 0, 2);\n\nvbo_colors->bind();\nshaderProgram->bindAttributeLocation(\"colAttr\", 1);\nshaderProgram->enableAttributeArray(1);\nshaderProgram->setAttributeBuffer(1, GL_FLOAT, 0, 3);\n\nglDrawArrays(GL_TRIANGLES, 0, 3);\n```\n\n7.  就在前面的代码之后，我们将发送`vbo_vertices2`和`vbo_colors`到着色器属性，并再次调用`glDrawArrays()`来绘制第二个三角形:\n\n```cpp\nvbo_vertices2->bind();\nshaderProgram->bindAttributeLocation(\"posAttr\", 0);\nshaderProgram->enableAttributeArray(0);\nshaderProgram->setAttributeBuffer(0, GL_FLOAT, 0, 2);\n\nvbo_colors->bind();\nshaderProgram->bindAttributeLocation(\"colAttr\", 1);\nshaderProgram->enableAttributeArray(1);\nshaderProgram->setAttributeBuffer(1, GL_FLOAT, 0, 3);\n\nglDrawArrays(GL_TRIANGLES, 0, 3);\n```\n\n8.  如果您现在构建程序，您应该能够在屏幕上看到两个三角形，其中一个三角形位于另一个三角形之上:\n\n![](img/a093b576-1004-4b32-9524-4a1964f67257.png)\n\n# 它是如何工作的...\n\nOpenGL 支持的几何图元类型有点、线、线段、线环、多边形、四边形、四边形条、三角形、三角形条和三角形扇。在这个例子中，我们画了两个三角形，其中每个形状都有一组顶点和颜色，这样 OpenGL 就知道形状应该如何渲染。\n\n彩虹色效果是通过为每个顶点赋予不同的颜色来创建的。OpenGL 将自动在每个顶点之间插入颜色，并将其显示在屏幕上。目前，首先呈现的形状将出现在稍后呈现的其他形状的后面。这是因为我们在 2D 空间中渲染形状，并且不涉及深度信息来检查哪个形状位于前面等等。我们将在下面的例子中学习如何进行深度检查。\n\n# 渲染三维形状\n\n在上一节中，我们学习了如何在屏幕上绘制简单的 2D 形状。然而，为了充分利用 OpenGL API，我们还需要学习如何使用它来渲染 3D 图像。简而言之，3D 图像只是使用 2D 形状创建的错觉，堆叠方式使它们看起来像是 3D 的。\n\n# 怎么做...\n\n这里的主要成分是深度值，它决定了哪些形状应该出现在其他形状的前面或后面。位于另一个曲面后面的图元形状(深度比另一个形状浅)将不会被渲染(或被部分渲染)。OpenGL 提供了一种简单的方法来实现这一点:\n\n1.  让我们从前面的 2D 例子继续我们的项目。通过在`renderwindow.cpp`的`initializeGL()`功能中添加`glEnable(GL_DEPTH_TEST)`来启用深度测试:\n\n```cpp\nvoid RenderWindow::initializeGL()\n{\n    openGLFunctions = openGLContext->functions();\n    glEnable(GL_DEPTH_TEST);\n```\n\n2.  我们将把我们的`vertices`数组变成更长的东西，这是一个三维立方体形状的顶点信息。我们现在可以移除`colors`数组，因为这次您没有向着色器提供颜色信息。我们也可以出于同样的原因除去`vbo_colors` VBO:\n\n```cpp\nGLfloat vertices[] = {\n    -1.0f,-1.0f,-1.0f,1.0f,-1.0f,-1.0f,-1.0f,-1.0f, 1.0f,\n    1.0f,-1.0f,-1.0f,1.0f,-1.0f, 1.0f,-1.0f,-1.0f, 1.0f,\n    -1.0f, 1.0f,-1.0f,-1.0f, 1.0f, 1.0f,1.0f, 1.0f,-1.0f,\n    1.0f, 1.0f,-1.0f,-1.0f, 1.0f, 1.0f,1.0f, 1.0f, 1.0f,\n    -1.0f,-1.0f, 1.0f,1.0f,-1.0f, 1.0f,-1.0f, 1.0f, 1.0f,\n    1.0f,-1.0f, 1.0f,1.0f, 1.0f, 1.0f,-1.0f, 1.0f, 1.0f,\n    -1.0f,-1.0f,-1.0f,-1.0f, 1.0f,-1.0f,1.0f,-1.0f,-1.0f,\n    1.0f,-1.0f,-1.0f,-1.0f, 1.0f,-1.0f,1.0f, 1.0f,-1.0f,\n    -1.0f,-1.0f, 1.0f,-1.0f, 1.0f,-1.0f,-1.0f,-1.0f,-1.0f,\n    -1.0f,-1.0f, 1.0f,-1.0f, 1.0f, 1.0f,-1.0f, 1.0f,-1.0f,\n    1.0f,-1.0f, 1.0f,1.0f,-1.0f,-1.0f,1.0f, 1.0f,-1.0f,\n    1.0f,-1.0f, 1.0f,1.0f, 1.0f,-1.0f,1.0f, 1.0f, 1.0f\n  };\n```\n\n3.  在`paintEvent()`功能中，我们必须将`GL_DEPTH_BUFFER_BIT`添加到`glClear()`功能中，因为我们在上一步`initializeGL()`中启用了深度检查:\n\n```cpp\nglClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);\n```\n\n4.  之后，我们需要将矩阵信息发送到名为**模型-视图-投影** ( **MVP** )的着色器，以便 GPU 知道如何在 2D 屏幕上渲染 3D 形状。MVP 矩阵是投影矩阵、视图矩阵和模型矩阵相乘的结果。乘法顺序非常重要，这样才能得到正确的结果:\n\n```cpp\nQMatrix4x4 matrixMVP;\nQMatrix4x4 model, view, projection;\nmodel.translate(0, 1, 0);\nmodel.rotate(45, 0, 1, 0);\nview.lookAt(QVector3D(5, 5, 5), QVector3D(0, 0, 0), QVector3D(0, 1, 0));\nprojection.perspective(60.0f, ((float)this->width()/(float)this->height()), 0.1f, 100.0f);\nmatrixMVP = projection * view * model;\nshaderProgram->setUniformValue(\"matrix\", matrixMVP);\n```\n\n5.  将`glDrawArrays()`中的最后一个值更改为`36`，因为我们现在在立方体形状中有 36 个三角形:\n\n```cpp\nglDrawArrays(GL_TRIANGLES, 0, 36);\n```\n\n6.  我们必须返回到着色器代码并更改它的某些部分，如以下代码中突出显示的那样:\n\n```cpp\nstatic const char *vertexShaderSource =\n    \"#version 330 core\\n\"\n    \"layout(location = 0) in vec3 posAttr;\\n\"\n    \"uniform mat4 matrix;\\n\"\n    \"out vec3 fragPos;\\n\"\n    \"void main() {\\n\"\n    \"fragPos = posAttr;\\n\"\n    \"gl_Position = matrix * vec4(posAttr, 1.0); }\";\n\nstatic const char *fragmentShaderSource =\n    \"#version 330 core\\n\"\n    \"in vec3 fragPos;\\n\"\n    \"out vec4 col;\\n\"\n    \"void main() {\\n\"\n    \"col = vec4(fragPos, 1.0); }\";\n```\n\n7.  如果您现在构建并运行项目，您应该会看到一个彩色的立方体出现在屏幕上。我们对颜色使用相同的`vertices`数组，这给出了这个多彩的结果:\n\n![](img/91023257-02b7-4ed2-bd3a-81b700113b6d.png)\n\n8.  尽管结果看起来很好，但如果我们真的想展示 3D 效果，那就是制作立方体的动画。为此，首先我们需要打开`renderwindow.h`并添加以下变量。请注意，在现代 C++ 标准中，您可以初始化头文件中的变量，而在旧的 C++ 标准中却不是这样:\n\n```cpp\nQTime* time;\nint currentTime = 0;\nint oldTime = 0;\nfloat deltaTime = 0;\nfloat rotation = 0;\n```\n\n9.  打开`renderwindow.cpp`，将以下高亮显示的代码添加到类构造函数中:\n\n```cpp\nopenGLContext = new QOpenGLContext();\nopenGLContext->setFormat(format);\nopenGLContext->create();\nopenGLContext->makeCurrent(this);\n\ntime = new QTime();\ntime->start();\n```\n\n10.  之后，在你的`paintEvent()`函数顶部添加以下高亮显示的代码。`deltaTime`是每帧经过时间的值，用于使动画速度一致，与帧率性能无关:\n\n```cpp\nvoid RenderWindow::paintEvent(QPaintEvent *event) {\n    Q_UNUSED(event);\n    // Delta time for each frame\n    currentTime = time->elapsed();\n deltaTime = (float)(currentTime - oldTime) / 1000.0f;\n oldTime = currentTime;\n```\n\n11.  在您的 MVP 矩阵代码上添加以下高亮显示的代码，并将`rotation`变量应用于`rotate()`函数，如下所示:\n\n```cpp\nrotation += deltaTime * 50;\n\nQMatrix4x4 matrixMVP;\nQMatrix4x4 model, view, projection;\nmodel.translate(0, 1, 0);\nmodel.rotate(rotation, 0, 1, 0);\n```\n\n12.  在`paintEvent()`函数结束时调用`update()`函数，以便在每次抽奖结束时反复调用`paintEvent()`。由于我们正在更改`paintEvent()`函数中的`rotation`值，我们可以给观众一种旋转立方体的错觉:\n\n```cpp\n    glDrawArrays(GL_TRIANGLES, 0, 36);\n\n    shaderProgram->release();\n    vao->release();\n\n    this->update();\n}\n```\n\n13.  如果你现在编译并运行程序，你应该会在你的渲染窗口中看到一个旋转的立方体！\n\n# 它是如何工作的...\n\n在任何 3D 渲染中，深度都非常重要，因此我们需要通过调用`glEnable(GL_DEPTH_TEST)`来启用 OpenGL 中的深度测试功能。当我们清除缓冲区时，我们还必须指定`GL_DEPH_BUFFER_BIT`，以便深度信息也被清除，以便正确渲染下一个图像。\n\n我们在 OpenGL 中使用 MVP 矩阵，这样 GPU 就知道如何正确渲染三维图形。在 OpenGL 3 及更高版本中，OpenGL 不再通过固定函数自动处理此问题。程序员可以根据他们的用例自由灵活地定义自己的矩阵，然后通过着色器将其提供给图形处理器，以渲染最终图像。模型矩阵包含 3D 对象的变换数据，即对象的位置、旋转和比例。另一方面，视图矩阵是摄像机或视图信息。最后，投影矩阵告诉图形处理器在将 3D 世界投影到 2D 屏幕上时使用哪种投影方法。\n\n在我们的示例中，我们使用了**透视**投影方法，该方法可以更好地感知距离和深度。透视投影的反面是**正投影**投影，使一切看起来都是平的平行的:\n\n![](img/41a191d8-f0a3-4729-9bd0-96cfd83301af.png)\n\n在这个例子中，我们使用了一个定时器来增加旋转值 50。将其乘以`deltaTime`值。`deltaTime`值因渲染帧速率而异。但是，它使以不同帧速率渲染的不同硬件的最终动画速度保持一致。\n\n记得手动调用`update()`让屏幕刷新，否则立方体不会动画化。\n\n# OpenGL 中的纹理\n\nOpenGL 允许我们将图像(也称为纹理)映射到三维形状或多边形。这个过程也被称为纹理映射。在这种情况下，Qt 5 似乎是与 OpenGL 的最佳组合，因为它提供了一种简单的方法来加载属于常见格式之一(BMP、JPEG、PNG、TARGA、TIFF 等)的图像，并且您不必自己实现它。我们将使用前面的例子与一个旋转的立方体，并试图用纹理映射它！\n\n# 怎么做…\n\n让我们按照以下步骤学习如何在 OpenGL 中使用纹理:\n\n1.  打开`renderwindow.h`并添加变量，这些变量在以下代码块中突出显示:\n\n```cpp\nQOpenGLContext* openGLContext;\nQOpenGLFunctions* openGLFunctions;\nQOpenGLShaderProgram* shaderProgram;\nQOpenGLVertexArrayObject* vao;\nQOpenGLBuffer* vbo_vertices;\nQOpenGLBuffer* vbo_uvs;\nQOpenGLTexture* texture;\n```\n\n2.  我们必须在`initializeGL()`函数中调用`glEnable(GL_TEXTURE_2D)`来启用纹理映射功能:\n\n```cpp\nvoid RenderWindow::initializeGL()\n{\n    openGLFunctions = openGLContext->functions();\n    glEnable(GL_DEPTH_TEST);\n    glEnable(GL_TEXTURE_2D);\n```\n\n3.  我们需要在`QOpenGLTexture`类下初始化我们的`texture`变量。我们将从我们的应用文件夹中加载一个名为`brick.jpg`的纹理图像，并通过调用`mirrored()`来翻转该图像。OpenGL 使用不同的坐标系，这就是为什么我们需要在将纹理传递给着色器之前翻转纹理。我们还会相应地将`min`和`max`过滤器设置为`Nearest`和`Linear`，如下所示:\n\n```cpp\ntexture = new QOpenGLTexture(QImage(qApp->applicationDirPath() + \"/brick.jpg\").mirrored());\ntexture->setMinificationFilter(QOpenGLTexture::Nearest);\ntexture->setMagnificationFilter(QOpenGLTexture::Linear);\n```\n\n4.  添加另一个名为`uvs`的数组。这是我们保存立方体对象纹理坐标的地方:\n\n```cpp\nGLfloat uvs[] = {\n    0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f,\n    1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f,\n    0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f,\n    1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f,\n    1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f,\n    0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f,\n    0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f,\n    1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f,\n    0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f,\n    0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f,\n    1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f,\n    1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f\n};\n```\n\n5.  我们必须修改我们的顶点着色器，以便它采用纹理坐标来计算纹理将应用于对象表面的位置。这里，我们只需将纹理坐标传递给片段着色器，无需修改:\n\n```cpp\nstatic const char *vertexShaderSource =\n    \"#version 330 core\\n\"\n    \"layout(location = 0) in vec3 posAttr;\\n\"\n \"layout(location = 1) in vec2 uvAttr;\\n\"\n    \"uniform mat4 matrix;\\n\"\n    \"out vec3 fragPos;\\n\"\n \"out vec2 fragUV;\\n\"\n    \"void main() {\\n\"\n    \"fragPos = posAttr;\\n\"\n \"fragUV = uvAttr;\\n\"\n    \"gl_Position = matrix * vec4(posAttr, 1.0); }\";\n```\n\n6.  在片段着色器中，我们通过调用`texture()`函数来创建纹理，该函数接收来自`fragUV`的纹理坐标信息和来自`tex`的图像采样器:\n\n```cpp\nstatic const char *fragmentShaderSource =\n    \"#version 330 core\\n\"\n    \"in vec3 fragPos;\\n\"\n    \"in vec2 fragUV;\\n\"\n    \"uniform sampler2D tex;\\n\"\n    \"out vec4 col;\\n\"\n    \"void main() {\\n\"\n    \"vec4 texCol = texture(tex, fragUV);\\n\"\n \"col = texCol; }\";\n```\n\n7.  我们还必须为纹理坐标初始化 VBO:\n\n```cpp\nvbo_uvs = new QOpenGLBuffer(QOpenGLBuffer::VertexBuffer);\nvbo_uvs->create();\nvbo_uvs->setUsagePattern(QOpenGLBuffer::StaticDraw);\nvbo_uvs->bind();\nvbo_uvs->allocate(uvs, sizeof(uvs) * sizeof(GLfloat));\n```\n\n8.  在`paintEvent()`函数中，调用`glDrawArrays()`之前，我们必须将纹理坐标信息发送给着色器，然后绑定纹理:\n\n```cpp\nvbo_uvs->bind();\nshaderProgram->bindAttributeLocation(\"uvAttr\", 1);\nshaderProgram->enableAttributeArray(1);\nshaderProgram->setAttributeBuffer(1, GL_FLOAT, 0, 2); \ntexture->bind();\nglDrawArrays(GL_TRIANGLES, 0, 36);\n```\n\n9.  如果您现在编译并运行程序，您应该会在屏幕上看到一个旋转的砖块立方体:\n\n![](img/d9cef5b0-cbb1-4a94-b4bc-773530fe38e9.png)\n\n# 它是如何工作的...\n\nQt 5 让加载纹理变得非常简单。只需要一行代码就可以加载一个图像文件，翻转它，并将其转换为与 OpenGL 兼容的纹理。纹理坐标是让 OpenGL 知道如何在屏幕上显示之前将纹理粘贴到对象表面的信息。\n\n`min`和`max`滤镜是滤镜，当应用于比分辨率更大的表面时，会使纹理看起来更好。该选项的默认设置是 GL _ NERSATE，代表**最近邻过滤**。当近距离观看时，该滤镜会使纹理看起来像像素一样。另一个常见的设置是 GL_LINEAR，代表**双线性过滤**。该过滤器采用两个相邻的片段，并对它们进行插值，以创建近似的颜色，这看起来比 GL _ release 好得多:\n\n![](img/363899cb-270d-4cfd-99b1-51644d7a92ad.png)\n\n# OpenGL 中的基本照明\n\n在这个例子中，我们将学习如何使用 OpenGL 和 Qt 5 为我们的 3D 场景添加一个简单的点光源。\n\n# 怎么做…\n\n让我们从以下步骤开始:\n\n1.  同样，我们将使用前面的例子，并在旋转的立方体附近添加一个点光源。打开`renderwindow.h`并在文件中添加另一个名为`vbo_normals`的变量:\n\n```cpp\nQOpenGLBuffer* vbo_uvs;\nQOpenGLBuffer* vbo_normals;\nQOpenGLTexture* texture;\n```\n\n2.  打开`renderwindow.cpp`并在`initializeGL()`功能中添加另一个名为`normals`的数组:\n\n```cpp\nGLfloat normals[] = {\n    0.0f, -1.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f, -1.0f, 0.0f,\n    0.0f, -1.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f, -1.0f, 0.0f,\n    0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f,\n    0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f,\n    1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f,\n    1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f,\n    0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f,\n    0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f,\n    -1.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f,\n    -1.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f,\n    0.0f, 0.0f, -1.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f, -1.0f,\n    0.0f, 0.0f, -1.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f, -1.0f\n};\n```\n\n3.  通过添加以下代码初始化`initializeGL()`中的`vbo_normals` VBO:\n\n```cpp\nvbo_normals = new QOpenGLBuffer(QOpenGLBuffer::VertexBuffer);\nvbo_normals->create();\nvbo_normals->setUsagePattern(QOpenGLBuffer::StaticDraw);\nvbo_normals->bind();\nvbo_normals->allocate(normals, sizeof(normals) * sizeof(GLfloat));\n```\n\n4.  由于这次我们将要编写的着色器将比我们在前面的例子中使用的要长得多，让我们将着色器代码移到文本文件中，并通过调用`addShaderFromSourceFile()`将它们加载到程序中:\n\n```cpp\nshaderProgram = new QOpenGLShaderProgram(this);\nshaderProgram->addShaderFromSourceFile(QOpenGLShader::Vertex, qApp->applicationDirPath() + \"/vertex.txt\");\nshaderProgram->addShaderFromSourceFile(QOpenGLShader::Fragment, qApp->applicationDirPath() + \"/fragment.txt\");\nshaderProgram->link();\n```\n\n5.  完成后，向`paintEvent()`函数添加以下代码，将法线 VBO 传递给着色器:\n\n```cpp\nvbo_normals->bind();\nshaderProgram->bindAttributeLocation(\"normalAttr\", 2);\nshaderProgram->enableAttributeArray(2);\nshaderProgram->setAttributeBuffer(2, GL_FLOAT, 0, 3);\n```\n\n6.  让我们打开刚刚创建的两个包含着色器代码的文本文件。首先，我们需要对顶点着色器进行一些更改，如下所示:\n\n```cpp\n#version 330 core\nlayout(location = 0) in vec3 posAttr;\nlayout(location = 1) in vec2 uvAttr;\nlayout(location = 2) in vec3 normalAttr;\nuniform mat4 matrix;\nout vec3 fragPos;\nout vec2 fragUV;\nout vec3 fragNormal;\n\nvoid main() {\n    fragPos = posAttr;\n    fragUV = uvAttr;\n    fragNormal = normalAttr;\n    gl_Position = matrix * vec4(posAttr, 1.0);\n}\n```\n\n7.  我们还将对片段着色器进行一些更改。我们将在着色器代码中创建一个名为`calcPointLight()`的函数:\n\n```cpp\n#version 330 core\nin vec3 fragPos;\nin vec2 fragUV;\nin vec3 fragNormal;\nuniform sampler2D tex;\nout vec4 col;\n\nvec3 calcPointLight() {\n vec4 texCol = texture(tex, fragUV);\n vec3 lightPos = vec3(1.0, 2.0, 1.5);\n vec3 lightDir = normalize(lightPos - fragPos);\n vec4 lightColor = vec4(1.0, 1.0, 1.0, 1.0);\n float lightIntensity = 1.0;\n```\n\n8.  继续前面的代码，我们使用`calcPointLight()`计算光照，并将结果片段输出到`col`变量，如下所示:\n\n```cpp\n    // Diffuse\n float diffuseStrength = 1.0;\n float diff = clamp(dot(fragNormal, lightDir), 0.0, 1.0);\n vec4 diffuse = diffuseStrength * diff * texCol * lightColor * lightIntensity;\n return diffuse;\n}\n\nvoid main() {\n    vec3 finalColor = calcPointLight();\n    col = vec4(finalColor, 1.0); \n}\n```\n\n9.  如果您现在编译并运行程序，您应该会看到灯光在起作用:\n\n![](img/8f1ba1ed-1ef4-405a-989f-8bda3c16888f.png)\n\n# 它是如何工作的...\n\n在 OpenGL 3 及更高版本中，固定功能照明不再存在。你不能再调用`glEnable(GL_LIGHT1)`给你的 3D 场景增加光线。添加灯光的新方法是在着色器中计算自己的灯光。这为您提供了根据需要创建所有类型灯光的灵活性。旧方法在大多数硬件中最多有 16 个灯的限制，但是，有了新的可编程管道，您可以在场景中有任意数量的灯；但是，照明模型将需要完全由您在着色器中编码，这不是一项容易的任务。\n\n除此之外，我们还需要为立方体的每个表面添加一个表面法线值。曲面法线指示曲面面向的位置，用于照明计算。\n\n前面的例子非常简化，让你理解光照在 OpenGL 中是如何工作的。在实际使用案例中，您可能需要从 C++ 传递一些变量，如光强度、光颜色和光位置，或者从材质文件中加载它，而不是在着色器代码中硬编码它。\n\n# 使用键盘控制移动对象\n\n在本节中，我们将了解如何使用键盘控件在 OpenGL 中移动对象。Qt 提供了一种使用虚拟功能检测键盘事件的简单方法，即`keyPressEvent()`和`keyReleaseEvent()`。我们将使用前面的例子并添加到其中。\n\n# 怎么做…\n\n要使用键盘控制移动对象，请按照下列步骤操作:\n\n1.  打开`renderwindow.h`并声明两个叫做`moveX`和`moveZ`的浮点数。然后，声明一个名为`movement`的`QVector3D`变量:\n\n```cpp\nQTime* time;\nint currentTime = 0;\nint oldTime = 0;\nfloat deltaTime = 0;\nfloat rotation = 0;\nfloat moveX = 0;\nfloat moveZ = 0;\nQVector3D movement = QVector3D(0, 0, 0);\n```\n\n2.  我们还将声明两个名为`keyPressEvent()`和`keyReleaseEvent()`的函数:\n\n```cpp\nprotected:\n    void initializeGL();\n    void paintEvent(QPaintEvent *event);\n    void resizeEvent(QResizeEvent *event);\n void keyPressEvent(QKeyEvent *event);\n void keyReleaseEvent(QKeyEvent *event);\n```\n\n3.  我们将在`renderwindow.cpp`中实现`keyPressEvent()`功能:\n\n```cpp\nvoid RenderWindow::keyPressEvent(QKeyEvent *event) {\n    if (event->key() == Qt::Key_W) { moveZ = -10; }\n    if (event->key() == Qt::Key_S) { moveZ = 10; }\n    if (event->key() == Qt::Key_A) { moveX = -10; }\n    if (event->key() == Qt::Key_D) { moveX = 10; }\n}\n```\n\n4.  我们还将实现`keyReleaseEvent()`功能:\n\n```cpp\nvoid RenderWindow::keyReleaseEvent(QKeyEvent *event) {\n    if (event->key() == Qt::Key_W) { moveZ = 0; }\n    if (event->key() == Qt::Key_S) { moveZ = 0; }\n    if (event->key() == Qt::Key_A) { moveX = 0; }\n    if (event->key() == Qt::Key_D) { moveX = 0; }\n}\n```\n\n5.  之后，我们将注释掉`paintEvent()`中的旋转代码，并添加移动代码，如下面的代码片段所示。我们不想被旋转分散注意力，只想专注于运动:\n\n```cpp\n//rotation += deltaTime * 50;\nmovement.setX(movement.x() + moveX * deltaTime);\nmovement.setZ(movement.z() + moveZ * deltaTime);\n\nQMatrix4x4 matrixMVP;\nQMatrix4x4 model, view, projection;\nmodel.translate(movement.x(), 1, movement.z());\n```\n\n6.  如果现在编译运行程序，按下 *W* 、 *A* 、 *S* 和 *D* 应该可以移动立方体。\n\n# 它是如何工作的...\n\n我们在这里所做的是不断添加`moveX`和`moveZ`值的`movement`向量的`x`和`z`值。当按下一个键时，`moveX`和`moveZ`将变成正数或负数，这取决于按下的是哪个按钮；否则，它将为零。在`keyPressEvent()`功能中，我们检查了按下的键盘按钮是 *W* 、 *A* 、 *S* 还是*D*；然后我们相应地设置变量。要获取 Qt 使用的关键名称的完整列表，请访问[http://doc.qt.io/qt-5/qt.html#Key-enum](http://doc.qt.io/qt-5/qt.html#Key-enum)。\n\n我们可以做运动输入的一种方法是按住同一个键，不释放它。Qt 5 会在一段时间后重复按键事件，但它不是很流畅，因为现代操作系统会限制按键事件以防止重复键入。不同操作系统之间的键盘输入间隔不同。您可以通过调用`QApplication::setKeyboardInterval()`来设置间隔，但这可能不适用于每个操作系统。因此，我们没有采用这种方法。\n\n取而代之的是，当按键被按下或释放时，我们只设置`moveX`和`moveZ`一次，然后我们在我们的游戏循环中不断地将该值应用到`movement`向量，使其连续移动而不受输入间隔的影响。\n\n# QML 的 3D 画布\n\n在这个食谱中，我们将学习如何使用 Qt 5 渲染 3D 图像。\n\n# 怎么做…\n\n让我们通过以下示例了解如何在 QML 使用 3D 画布:\n\n1.  让我们从在 Qt Creator 中创建新项目开始这个例子。这一次，我们将选择 Qt 快速应用——画布 3D，而不是我们在前面示例中选择的其他选项:\n\n![](img/b419ee0c-4250-4add-8a55-c656cf017934.png)\n\n2.  之后，Qt Creator 会问你是否要创建一个基于`three.js` JavaScript 库的项目。选中创建基于三个. js 的应用选项，然后按下一步按钮继续:\n\n![](img/d16a9663-c203-42cd-b708-8d144d49db15.png)\n\n3.  创建项目后，您会注意到项目资源中已经有一些 JavaScript ( `.js`)文件。这很正常，因为 Qt 画布 3D 应用使用 JavaScript 和 WebGL 技术在屏幕上呈现 3D 图像。在这种情况下，它正在运行一个名为`three.js`的基于 WebGL 的渲染库，与编写纯 WebGL 代码相比，这使得我们的编程工作更加简单和容易:\n\n![](img/edd8e505-c056-46f9-bdc2-c7e731fbd5c0.png)\n\n4.  向我们的项目资源添加一个图像文件——我们将在本例中使用它。通过在项目窗格中右键单击`qml.qrc`并选择在编辑器中打开，用 Qt 创建器打开`qml.qrc`。Qt Creator 打开资源文件后，点击**添加**按钮，然后点击【添加文件】按钮，从电脑中选择想要的图像文件。在我的例子中，我添加了一个`brick.jpg`图像，它将被用作我们的 3D 对象的表面纹理:\n\n![](img/e5442172-247a-401a-af97-1209432797c1.png)\n\n5.  之后，使用 Qt Creator 打开`glcode.js`。您将看到文件中已经编写了大量代码。它所做的基本上是使用`three.js`库在屏幕上渲染一个简单的 3D 立方体。您可以立即构建项目并运行它，看看它是什么样子。但是，我们将稍微修改一下代码，以定制它的输出。\n6.  在`initializeGL()`功能中，我们将为场景添加一个方向光，加载我们刚刚添加到项目资源中的纹理文件，然后将纹理应用到定义 3D 立方体表面属性的材质上:\n\n```cpp\nfunction initializeGL(canvas) {\n    scene = new THREE.Scene();\n    camera = new THREE.PerspectiveCamera(75, canvas.width / canvas.height, 0.1, 1000);\n    camera.position.z = 5;\n    var directionalLight = new THREE.DirectionalLight(0xffffff);\n directionalLight.position.set(1, 1, 1).normalize();\n scene.add(directionalLight);\n    var texture = THREE.ImageUtils.loadTexture('brick.jpg');\n var material = new THREE.MeshLambertMaterial({ map: texture });\n```\n\n7.  我们还将通过在所有维度中将其比例设置为`3`来使立方体的比例稍微大一点。我们还将背景颜色设置为浅灰色:\n\n```cpp\n var cubeGeometry = new THREE.BoxGeometry(3, 3, 3);\n    cube = new THREE.Mesh(cubeGeometry, material);\n    cube.rotation.set(0.785, 0.785, 0.0);\n    scene.add(cube);\n    renderer = new THREE.Canvas3DRenderer({ canvas: canvas, antialias: true, devicePixelRatio: canvas.devicePixelRatio });\n    renderer.setSize(canvas.width, canvas.height);\n    renderer.setClearColor(0xa9a9a9, 1);\n}\n```\n\n8.  在`paintGL()`功能中，在渲染场景之前，添加一行额外的代码来旋转三维立方体:\n\n```cpp\nfunction paintGL(canvas) {\n cube.rotation.y -= 0.005;\n    renderer.render(scene, camera);\n}\n```\n\n9.  我个人觉得窗口尺寸有点太小了，所以也在`main.qml`文件中更改了窗口的`width`和`height`，如下代码所示:\n\n```cpp\nimport QtQuick 2.4\nimport QtCanvas3D 1.0\nimport QtQuick.Window 2.2\nimport \"glcode.js\" as GLCode\nWindow {\n    title: qsTr(\"Qt_Canvas_3D\")\n width: 800\n height: 600\n    visible: true\n```\n\n10.  完成后，构建并运行项目。你应该能看到一个砖块纹理的三维立方体，在屏幕上慢慢旋转:\n\n![](img/fbd69931-29d2-4322-b51a-976185e796c0.png)\n\n# 它是如何工作的...\n\n最初，`three.js`是一个跨浏览器的 JavaScript 库/API，使用 WebGL 技术在 web 浏览器中显示动画 3D 计算机图形。然而，Qt Canvas 3D 也使用网络技术，特别是网络图形语言技术，来渲染 3D 图像，就像在网络浏览器上一样。这意味着 Qt 画布 3D 不仅支持`three.js`，而且所有基于 WebGL 技术的不同类型的库都将在 Qt 画布 3D 上完美工作。然而，Qt 画布 3D 仅适用于基于 QML 的项目，不适用于 C++。\n\nIf you are interested to learn more about `three.js`, check out their website at [http://threejs.org](http://threejs.org)."
  },
  {
    "path": "docs/qt5-cpp-gui-prog-cb/06.md",
    "content": "# 六、使用网络和管理大型文档\n\n在本章中，我们将介绍以下食谱:\n\n*   创建一个 TCP 服务器\n*   创建一个 TCP 客户端\n*   使用文件传输协议上传和下载文件\n\n# 介绍\n\n在本章中，我们将学习如何使用 Qt 5 的网络模块创建网络服务器程序和客户端程序。我们还将学习如何创建一个使用**文件传输协议** ( **FTP** )从服务器上传和下载文件的程序。最后，我们将学习如何使用 Qt 5 和 C++ 语言向特定的 web 服务发送 HTTP 请求。\n\n# 技术要求\n\n本章的技术要求包括 Qt 5.11.2 MinGW 32 位、Qt Creator 4.8.2、Windows 10、FileZilla Server 0.9.60。\n\n本章使用的所有代码可从以下 GitHub 链接下载:[https://GitHub . com/PacktPublishing/Qt5-CPP-GUI-Programming-cook book-第二版/树/主/第 06 章](https://github.com/PacktPublishing/Qt5-CPP-GUI-Programming-Cookbook-Second-Edition/tree/master/Chapter06)。\n\n查看以下视频，查看正在运行的代码:[http://bit.ly/2TqR9XQ](http://bit.ly/2TqR9XQ)\n\n# 创建一个 TCP 服务器\n\n在本食谱中，我们将学习如何在 Qt 5 中创建**传输控制协议** ( **TCP** )服务器。在我们能够创建一个允许您上传和下载文件的服务器之前，让我们将它缩小一点，并学习如何创建一个接收和发送文本的网络服务器。\n\n# 怎么做…\n\n按照以下步骤创建一个 TCP 服务器:\n\n1.  首先，让我们从文件|新文件或项目中创建一个 Qt 控制台应用项目，如下图所示:\n\n![](img/e7d57ddd-b9b3-458c-976d-9b94f588d2c9.png)\n\n2.  之后，再次转到文件|新建文件或项目。但是这次，在 C++ 类别下选择 C++ Class，如下图截图所示:\n\n![](img/868fddf4-fcfe-4c9e-8f9e-309c4a3060c6.png)\n\n3.  然后，将你的班级命名为`server`。将其基类设置为`QObject`，并确保在单击“下一步”按钮之前选中了“包含对象”选项。一旦创建了类，将为您创建两个文件-`server.h`和`server.cpp`，如下图所示:\n\n![](img/19f94d2a-c724-4294-a383-a3ca8088fb18.png)\n\n4.  之后，打开你的项目文件(`.pro`)并加入`network`模块，如下图所示:\n\n```cpp\nQT += core network\n```\n\n5.  完成后，打开`server.h`并添加以下标题:\n\n```cpp\n#include <QTcpServer>\n#include <QTcpSocket>\n#include <QVector>\n#include <QDebug>\n```\n\n6.  紧接着，声明`startServer()`和`sendMessageToClients()`函数，如下代码所示:\n\n```cpp\npublic:\n    server(QObject *parent = nullptr);\n    void startServer();\n    void sendMessageToClients(QString message);\n```\n\n7.  然后为`server`类声明以下槽函数:\n\n```cpp\npublic slots:\n    void newClientConnection();\n    void socketDisconnected();\n    void socketReadReady();\n    void socketStateChanged(QAbstractSocket::SocketState state);\n```\n\n8.  最后，声明两个私有变量，如下面的代码所示:\n\n```cpp\nprivate:\n    QTcpServer* chatServer;\n    QVector<QTcpSocket*>* allClients;\n```\n\n9.  完成上一步后，打开`server.cpp`并定义`startServer()`功能。这里，我们创建一个`QVector`容器来存储所有连接到服务器的客户端，并在后面的步骤中使用它来发送消息。这在以下示例中显示:\n\n```cpp\nvoid server::startServer() {\n    allClients = new QVector<QTcpSocket*>;\n    chatServer = new QTcpServer();\n    chatServer->setMaxPendingConnections(10);\n    connect(chatServer, &QTcpServer::newConnection, this, &server::newClientConnection);\n    if (chatServer->listen(QHostAddress::Any, 8001))\n        qDebug() << \"Server has started. Listening to port 8001.\";\n    else\n        qDebug() << \"Server failed to start. Error: \" + chatServer->errorString();\n}\n```\n\n10.  接下来，我们实现`sendMessageToClients()`函数，在这里我们迭代上一步刚刚创建的`allClients`容器，并将消息发送给他们每个人，如下例所示:\n\n```cpp\nvoid server::sendMessageToClients(QString message) {\n    if (allClients->size() > 0) {\n        for (int i = 0; i < allClients->size(); i++) {\n            if (allClients->at(i)->isOpen() && allClients->at(i)->isWritable()) {\n                allClients->at(i)->write(message.toUtf8());\n            }\n        }\n    }\n}\n```\n\n11.  之后，我们将开始实现插槽功能。让我们从下面的代码开始:\n\n```cpp\nvoid server::newClientConnection() {\n    QTcpSocket* client = chatServer->nextPendingConnection();\n    QString ipAddress = client->peerAddress().toString();\n    int port = client->peerPort();\n    connect(client, &QTcpSocket::disconnected, this, &server::socketDisconnected);\n    connect(client, &QTcpSocket::readyRead,this, &server::socketReadReady);\n    connect(client, &QTcpSocket::stateChanged, this, &server::socketStateChanged);\n    allClients->push_back(client);\n    qDebug() << \"Socket connected from \" + ipAddress + \":\" + QString::number(port);\n}\n```\n\n12.  然后，我们继续`socketDisconnected()`功能。当客户端与服务器断开连接时，将调用该 slot 函数，如下例所示:\n\n```cpp\nvoid server::socketDisconnected() {\n    QTcpSocket* client = qobject_cast<QTcpSocket*>(QObject::sender());\n    QString socketIpAddress = client->peerAddress().toString();\n    int port = client->peerPort();\n    qDebug() << \"Socket disconnected from \" + socketIpAddress + \":\" + QString::number(port);\n}\n```\n\n13.  接下来，我们将定义`socketReadReady()`函数，该函数将在客户端向服务器发送文本消息时被触发，如下例所示:\n\n```cpp\nvoid server::socketReadReady() {\n    QTcpSocket* client = qobject_cast<QTcpSocket*>(QObject::sender());\n    QString socketIpAddress = client->peerAddress().toString();\n    int port = client->peerPort();\n    QString data = QString(client->readAll());\n    qDebug() << \"Message: \" + data + \" (\" + socketIpAddress + \":\" + QString::number(port) + \")\";\n    sendMessageToClients(data);\n}\n```\n\n14.  之后，让我们实现`socketStateChanged()`函数，当客户端的联网状态发生变化时，将调用该函数，如下例所示:\n\n```cpp\nvoid server::socketStateChanged(QAbstractSocket::SocketState state) {\n    QTcpSocket* client = qobject_cast<QTcpSocket*>(QObject::sender());\n    QString socketIpAddress = client->peerAddress().toString();\n    int port = client->peerPort();\n\n    qDebug() << \"Socket state changed (\" + socketIpAddress + \":\" + QString::number(port) + \"): \" + desc;\n}\n```\n\n15.  我们还需要在`socketStateChanged()`中添加以下代码来打印出客户端的状态:\n\n```cpp\nQString desc;\nif (state == QAbstractSocket::UnconnectedState)\n    desc = \"The socket is not connected.\";\nelse if (state == QAbstractSocket::HostLookupState)\n    desc = \"The socket is performing a host name lookup.\";\nelse if (state == QAbstractSocket::ConnectingState)\n    desc = \"The socket has started establishing a connection.\";\nelse if (state == QAbstractSocket::ConnectedState)\n    desc = \"A connection is established.\";\nelse if (state == QAbstractSocket::BoundState)\n    desc = \"The socket is bound to an address and port.\";\nelse if (state == QAbstractSocket::ClosingState)\n    desc = \"The socket is about to close (data may still be waiting to be written).\";\nelse if (state == QAbstractSocket::ListeningState)\n    desc = \"For internal use only.\";\n```\n\n16.  最后，让我们打开`main.cpp`，并添加以下示例中突出显示的代码，以便启动服务器:\n\n```cpp\n#include <QCoreApplication>\n#include \"server.h\"\n\nint main(int argc, char *argv[]) {\n    QCoreApplication a(argc, argv);\n server* myServer = new server();\n myServer->startServer();\n    return a.exec();\n}\n```\n\n17.  您现在可以尝试运行服务器程序，但是您将无法测试它，因为我们还没有创建客户端程序，如下图所示:\n\n![](img/c159eebf-7f26-4a87-8470-13218799c729.png)\n\n18.  让我们继续下一个示例项目，并学习如何创建客户端程序。稍后我们将再次回来测试这个程序。\n\n# 它是如何工作的...\n\n网络连接主要有两种类型:**传输控制协议** ( **TCP** )连接和**用户数据报协议** ( **UDP** )连接。TCP 是可靠的网络连接，而 UDP 是不可靠的。\n\n这两种连接的目的截然不同:\n\n*   **TCP 联网**通常用于要求每一条数据都按顺序发送和接收的程序。它还确保客户端收到数据，并且服务器收到通知。像消息软件、网络服务器和数据库这样的程序使用 TCP 网络。\n*   **UDP 联网**另一方面，不需要服务器和客户端之间持续的牵手。由于连接不可靠，因此也没有关于数据是否被成功接收的反馈。丢弃数据包是可以容忍的，数据的顺序甚至可能与发送的顺序不同。UDP 连接通常由应用使用，这些应用将大量数据流式传输到客户端，而对其数据包传递没有严格要求，例如视频游戏、视频会议软件和域名系统。\n\n通过其信号和插槽机制，使用 Qt 5 创建网络软件要容易得多。我们需要做的就是将`QTcpServer`类和`QTcpSocket`类发出的信号连接到我们的槽函数。然后，我们将实现这些槽函数，并定义在这些函数中要做什么。\n\nWe used a **QVector** container to store the pointers to all the clients that have connected to the server so that we can use it to deliver the messages later on.\n\n为了使这个示例项目简单，我们简单地向所有客户发送文本消息，有点像群聊。你可以自由探索其他可能性，并做出自己的改变来改进程序。\n\n# 创建一个 TCP 客户端\n\n由于我们在前面的食谱中已经创建了一个 TCP 服务器，我们现在需要一个客户端程序来完成这个项目。因此，在本食谱中，我们将学习如何使用 Qt 5 及其网络模块创建一个 TCP 客户端程序。\n\n# 怎么做...\n\n要在 Qt 5 中创建一个 TCP 客户端，让我们执行以下操作:\n\n1.  首先，让我们从文件|新文件或项目创建一个新的 Qt Widgets 应用项目。\n2.  项目创建完成后，我们打开`mainwindow.ui`设置图形用户界面，如下图所示。请注意，中央小部件的布局方向必须垂直:\n\n![](img/76519ed9-4ed5-4fb6-83a6-698be17a72cf.png)\n\n3.  然后，右击显示“连接”的按钮，从菜单中创建一个`clicked()`插槽功能。然后，在发送按钮上重复相同的步骤。因此，将在源代码中为您创建两个 slot 函数，这可能看起来像也可能不像我们在下面的代码中看到的，这取决于您的小部件的名称:\n\n```cpp\nvoid on_connectButton_clicked();\nvoid on_sendButton_clicked();\n```\n\n4.  接下来，打开`mainwindow.h`并添加以下标题:\n\n```cpp\n#include <QDebug>\n#include <QTcpSocket>\n```\n\n5.  然后，声明`printMessage()`函数和三个槽函数:`socketConnected()`、`socketDisconnected()`和`socketReadyRead()`，如下代码所示:\n\n```cpp\npublic:\n    explicit MainWindow(QWidget *parent = 0);\n    ~MainWindow();\n void printMessage(QString message);\n\nprivate slots:\n    void on_connectButton_clicked();\n    void on_sendButton_clicked();\n void socketConnected();\n void socketDisconnected();\n void socketReadyRead();\n```\n\n6.  之后，还要声明以下变量:\n\n```cpp\nprivate:\n    Ui::MainWindow *ui;\n bool connectedToHost;\n QTcpSocket* socket;\n```\n\n7.  完成后，您可以进入`mainwindow.cpp`并定义`printMessage()`功能，如下例所示:\n\n```cpp\nvoid MainWindow::printMessage(QString message) {\n    ui->chatDisplay->append(message);\n}\n```\n\n8.  然后，我们将实现`on_connectButton_clicked()`功能，当点击连接按钮时，该功能将被触发，如下代码所示:\n\n```cpp\nvoid MainWindow::on_connectButton_clicked() {\n    if (!connectedToHost) {\n        socket = new QTcpSocket();\n        connect(socket, &QTcpSocket::connected, this, &MainWindow::socketConnected);\n        connect(socket, &QTcpSocket::disconnected, this, &MainWindow::socketDisconnected);\n        connect(socket, &QTcpSocket::readyRead, this, &MainWindow::socketReadyRead);\n        socket->connectToHost(\"127.0.0.1\", 8001);\n    } else {\n        QString name = ui->nameInput->text();\n        socket->write(\"<font color=\\\"Orange\\\">\" + name.toUtf8() + \" has left the chat room.</font>\");\n        socket->disconnectFromHost();\n    }\n}\n```\n\n9.  我们还定义了`on_sendButton_clicked()`函数，点击发送按钮后会调用该函数，如下例所示:\n\n```cpp\nvoid MainWindow::on_sendButton_clicked() {\n    QString name = ui->nameInput->text();\n    QString message = ui->messageInput->text();\n    socket->write(\"<font color=\\\"Blue\\\">\" + name.toUtf8() + \"</font>: \" + message.toUtf8());\n    ui->messageInput->clear();\n}\n```\n\n10.  紧接着，我们实现`socketConnected()`函数，当客户端程序成功连接到服务器时将调用该函数，如以下代码所示:\n\n```cpp\nvoid MainWindow::socketConnected() {\n    qDebug() << \"Connected to server.\";\n    printMessage(\"<font color=\\\"Green\\\">Connected to server.</font>\");\n    QString name = ui->nameInput->text();\n    socket->write(\"<font color=\\\"Purple\\\">\" + name.toUtf8() + \" has joined the chat room.</font>\");\n    ui->connectButton->setText(\"Disconnect\");\n    connectedToHost = true;\n}\n```\n\n11.  目前我们还没有完成。我们还需要实现`socketDisconnected()`功能，只要客户端与服务器断开连接，就会触发该功能，如下代码所示:\n\n```cpp\nvoid MainWindow::socketDisconnected() {\n    qDebug() << \"Disconnected from server.\";\n    printMessage(\"<font color=\\\"Red\\\">Disconnected from server.</font>\");\n    ui->connectButton->setText(\"Connect\");\n    connectedToHost = false;\n}\n```\n\n12.  最后，我们还需要定义`socketReadyRead()`函数，该函数打印出服务器发送的消息，如下例所示:\n\n```cpp\nvoid MainWindow::socketReadyRead() {\n    printMessage(socket->readAll());\n}\n```\n\n13.  在我们运行客户端程序之前，我们必须首先打开我们在前面的食谱中创建的服务器程序。然后，构建并运行客户端程序。程序打开后，点击连接按钮。成功连接到服务器后，在底部的行编辑小部件中键入内容，然后按发送按钮。您应该会看到类似以下截图的内容:\n\n![](img/c799ac13-2a91-469f-90c5-690ae1d38b84.png)\n\n14.  让我们转到服务器程序，如下图所示，看看终端窗口上是否有打印的内容:\n\n![](img/e9830b28-5413-45fd-844c-9853855d26f7.png)\n\n15.  恭喜，你成功创建了一个看起来有点像**互联网中继聊天** ( **IRC** )聊天室的程序！\n\n# 它是如何工作的...\n\n为了做到这一点，我们需要两个程序:一个连接所有客户端并传递其消息的服务器程序，以及一个由用户用来发送和接收来自其他用户的消息的客户端程序。\n\n由于服务器程序只是坐在幕后，默默地解决一切，它不需要任何用户界面，因此我们只需要它作为一个 Qt 控制台应用。\n\n然而，客户端程序需要一个视觉上令人愉快但易于使用的图形用户界面，以便用户读写他们的消息。因此，我们将客户端程序创建为 Qt Widgets 应用。\n\n与服务器程序相比，客户端程序相对简单。它所做的只是连接到服务器，发送用户输入的消息，并打印出服务器发送给它的所有内容。\n\n# 使用文件传输协议上传和下载文件\n\n我们已经学会了如何创建简单的聊天软件，在用户之间分发文本消息。接下来，我们将学习如何创建一个使用 FTP 上传和下载文件的程序。\n\n# 怎么做...\n\n让我们从观察以下步骤开始:\n\n1.  对于这个项目，我们需要安装一个名为 **FileZilla Server** 的软件，我们将把它用作 FTP 服务器。点击下载文件服务器按钮，可从[https://filezilla-project.org](https://filezilla-project.org)下载文件服务器，如下图截图所示:\n\n![](img/9bde6b56-bc61-40af-9be8-8e779b8d6e54.png)\n\n2.  下载安装程序后，运行安装程序并同意所有默认选项安装**文件服务器**，如下图所示:\n\n![](img/f6fb3800-d647-4b06-b1f3-bd8541b865c5.png)\n\n3.  完成后，打开文件服务器并按下连接按钮，如下图所示:\n\n![](img/a84487fc-8949-43ed-8747-0aa882ab4ed1.png)\n\n4.  服务器启动后，单击位于文件服务器控制面板顶部的用户图标，如下图所示:\n\n![](img/7db080be-d34e-4602-b0a9-f55b9844f4cf.png)\n\n5.  用户窗口打开后，单击用户列表下的添加按钮添加新用户。然后，在“共享文件夹”列表下添加一个共享文件夹，用户将从其中上传和下载文件，如下图所示:\n\n![](img/e6b41bee-9b7d-4b08-a33c-7fd92961fadf.png)\n\n6.  我们现在已经完成了设置**文件服务器**。让我们进入 Qt Creator，创建一个新的 Qt Widgets 应用项目。然后，打开`mainwindow.ui`，设置图形用户界面，如下图所示:\n\n![](img/848fb64c-93ca-46e6-98a1-114cbb070f4d.png)\n\n7.  然后，右键单击打开按钮、上传按钮和设置文件夹按钮，创建各自的`clicked()`槽功能，如下代码所示:\n\n```cpp\nprivate slots:\n    void on_openButton_clicked();\n    void on_uploadButton_clicked();\n    void on_setFolderButton_clicked();\n```\n\n8.  之后，双击列表小部件并选择转到插槽....然后，选择 itemDoubleClicked(qlistwightitem *)选项，点击确定，如下图截图所示:\n\n![](img/19b6a103-0936-41fa-8b00-7ea28adf1d40.png)\n\n9.  创建完槽函数后，打开`mainwindow.h`并添加以下标题:\n\n```cpp\n#include <QDebug>\n#include <QNetworkAccessManager>\n#include <QNetworkRequest>\n#include <QNetworkReply>\n#include <QFile>\n#include <QFileInfo>\n#include <QFileDialog>\n#include <QListWidgetItem>\n#include <QMessageBox>\n```\n\n10.  然后，声明`getFileList()`功能，如下所示:\n\n```cpp\npublic:\n    explicit MainWindow(QWidget *parent = 0);\n    ~MainWindow();\n void getFileList();\n```\n\n11.  然后声明以下插槽函数:\n\n```cpp\nvoid downloadFileListFinished();\nvoid uploadFileListFinished();\nvoid uploadFileProgress(qint64 bytesSent, qint64 bytesTotal);\nvoid uploadFileFinished();\nvoid downloadFileProgress(qint64 byteReceived,qint64 bytesTotal);\nvoid downloadFileFinished();\n```\n\n12.  紧接着，声明以下变量:\n\n```cpp\nprivate:\n    Ui::MainWindow *ui;\n QNetworkAccessManager* manager;\n QString ftpAddress;\n int ftpPort;\n QString username;\n QString password;\n QNetworkReply* downloadFileListReply;\n QNetworkReply* uploadFileListReply;\n QNetworkReply* uploadFileReply;\n QNetworkReply* downloadFileReply;\n QStringList fileList;\n QString uploadFileName;\n QString downloadFileName;\n```\n\n13.  然后，打开`mainwindow.cpp`，将以下代码添加到类构造函数中:\n\n```cpp\nMainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) {\n    ui->setupUi(this);\n manager = new QNetworkAccessManager(this);\n ftpAddress = \"ftp://127.0.0.1/\";\n ftpPort = 21;\n username = \"myuser\";\n password = \"123456\";\n getFileList();\n}\n```\n\n14.  之后，执行`getFileList()`功能，如下代码所示:\n\n```cpp\nvoid MainWindow::getFileList() {\n    QUrl ftpPath;\n    ftpPath.setUrl(ftpAddress + \"files.txt\");\n    ftpPath.setUserName(username);\n    ftpPath.setPassword(password);\n    ftpPath.setPort(ftpPort);\n    QNetworkRequest request;\n    request.setUrl(ftpPath);\n    downloadFileListReply = manager->get(request);\n    connect(downloadFileListReply, SIGNAL(finished()), this, SLOT(downloadFileListFinished()));\n}\n```\n\n15.  然后定义`on_openButton_clicked()`槽功能，点击【打开】按钮触发，如下代码所示:\n\n```cpp\nvoid MainWindow::on_openButton_clicked() {\n    QString fileName = QFileDialog::getOpenFileName(this, \"Select File\", qApp->applicationDirPath());\n    ui->uploadFileInput->setText(fileName);\n}\n```\n\n16.  完成后，实现单击上传按钮时调用的槽函数，如下例所示:\n\n```cpp\nvoid MainWindow::on_uploadButton_clicked() {\n    QFile* file = new QFile(ui->uploadFileInput->text());\n    QFileInfo fileInfo(*file);\n    uploadFileName = fileInfo.fileName();\n\n    QUrl ftpPath;\n    ftpPath.setUrl(ftpAddress + uploadFileName);\n    ftpPath.setUserName(username);\n    ftpPath.setPassword(password);\n    ftpPath.setPort(ftpPort);\n\n    if (file->open(QIODevice::ReadOnly)) {\n        ui->uploadProgress->setEnabled(true);\n        ui->uploadProgress->setValue(0);\n```\n\n17.  然后，继续实现前面的功能，如下面的代码所示:\n\n```cpp\n    QNetworkRequest request;\n    request.setUrl(ftpPath);\n\n    uploadFileReply = manager->put(request, file);\n    connect(uploadFileReply, QNetworkReply::uploadProgress, this, &MainWindow::uploadFileProgress);\n    connect(uploadFileReply, QNetworkReply::finished, this, &MainWindow::uploadFileFinished);\n    } else {\n        QMessageBox::warning(this, \"Invalid File\", \"Failed to open file for upload.\");\n    }\n}\n```\n\n18.  下面的代码显示了`on_setFolderButton_clicked()`槽功能的样子:\n\n```cpp\nvoid MainWindow::on_setFolderButton_clicked() {\n    QString folder = QFileDialog::getExistingDirectory(this, tr(\"Open Directory\"), qApp->applicationDirPath(), QFileDialog::ShowDirsOnly);\n    ui->downloadPath->setText(folder);\n}\n```\n\n19.  接下来，定义当双击列表小部件的一个项目时将触发的 slot 函数，如以下代码所示:\n\n```cpp\nvoid MainWindow::on_fileList_itemDoubleClicked(QListWidgetItem *item) {\n    downloadFileName = item->text();\n\n    QString folder = ui->downloadPath->text();\n    if (folder != \"\" && QDir(folder).exists()) {\n        QUrl ftpPath;\n        ftpPath.setUrl(ftpAddress + downloadFileName);\n        ftpPath.setUserName(username);\n        ftpPath.setPassword(password);\n        ftpPath.setPort(ftpPort);\n```\n\n20.  然后，使用以下代码继续实现前面的功能:\n\n```cpp\n    QNetworkRequest request;\n    request.setUrl(ftpPath);\n\n    downloadFileReply = manager->get(request);\n    connect(downloadFileReply, QNetworkReply::downloadProgress, this, MainWindow::downloadFileProgress);\n    connect(downloadFileReply, &QNetworkReply::finished, this, &MainWindow::downloadFileFinished);\n    } else {\n        QMessageBox::warning(this, \"Invalid Path\", \"Please set the download path before download.\");\n    }\n}\n```\n\n21.  我们还没有完全完成。接下来，我们将实现`downloadFileListFinished()`函数，当从服务器下载完文件列表后，会自动调用该函数，如下代码所示:\n\n```cpp\nvoid MainWindow::downloadFileListFinished() {\n    if(downloadFileListReply->error() != QNetworkReply::NoError)\n        QMessageBox::warning(this, \"Failed\", \"Failed to load file list: \" + downloadFileListReply->errorString());\n    else {\n        QByteArray responseData;\n        if (downloadFileListReply->isReadable())\n            responseData = downloadFileListReply->readAll();\n        ui->fileList->clear();\n        fileList = QString(responseData).split(\",\");\n        if (fileList.size() > 0) {\n            for (int i = 0; i < fileList.size(); i++) {\n                if (fileList.at(i) != \"\") {\n                    ui->fileList->addItem(fileList.at(i));\n                }\n            }\n        }\n    }\n}\n```\n\n22.  我们还需要实现将文件列表更新到服务器时调用的函数，如下例所示:\n\n```cpp\nvoid MainWindow::uploadFileListFinished() {\n    if(uploadFileListReply->error() != QNetworkReply::NoError)\n        QMessageBox::warning(this, \"Failed\", \"Failed to update file list: \" + uploadFileListReply->errorString());\n    else\n        getFileList();\n}\n```\n\n23.  `uploadFileProgress()`功能用于在上传文件到服务器时显示进度条，看起来像下面的代码:\n\n```cpp\nvoid MainWindow::uploadFileProgress(qint64 bytesSent, qint64 bytesTotal) {\n    qint64 percentage = 100 * bytesSent / bytesTotal;\n    ui->uploadProgress->setValue((int) percentage);\n}\n```\n\n24.  接下来，我们将定义文件上传到服务器后会发生什么。这个函数比其他函数稍微长一点，所以我们将它分成几个部分，这样您就不会被它弄糊涂了。首先，我们将遍历文件列表，看看它是否已经存在，如下例所示:\n\n```cpp\nvoid MainWindow::uploadFileFinished() {\n    if(uploadFileReply->error() != QNetworkReply::NoError)\n        QMessageBox::warning(this, \"Failed\", \"Failed to upload file: \" + uploadFileReply->errorString());\n    else {\n        bool exists = false;\n        if (fileList.size() > 0) {\n            for (int i = 0; i < fileList.size(); i++) {\n                if (fileList.at(i) == uploadFileName)\n                    exists = true;\n            }\n        }\n        if (!exists)\n            fileList.append(uploadFileName);\n```\n\n25.  然后，我们将创建一个名为`files.txt`的文本文件，并将最新的文件列表保存在其中，如下代码所示:\n\n```cpp\nQString fileName = \"files.txt\";\nQFile* file = new QFile(qApp->applicationDirPath() + \"/\" + fileName);\nfile->open(QIODevice::ReadWrite);\nif (fileList.size() > 0) {\n    for (int j = 0; j < fileList.size(); j++) {\n        if (fileList.at(j) != \"\")\n            file->write(QString(fileList.at(j) + \",\").toUtf8());\n    }\n}\nfile->close();\n```\n\n26.  之后打开`files.txt`发送到 FTP 服务器更新文件列表，如下代码所示:\n\n```cpp\nQFile* newFile = new QFile(qApp->applicationDirPath() + \"/\" + fileName);\nif (newFile->open(QIODevice::ReadOnly)) {\n    QUrl ftpPath;\n    ftpPath.setUrl(ftpAddress + fileName);\n    ftpPath.setUserName(username);\n    ftpPath.setPassword(password);\n    ftpPath.setPort(ftpPort);\n```\n\n27.  然后，继续编写将文件列表发送到 FTP 服务器的代码，如下例所示:\n\n```cpp\n            QNetworkRequest request;\n            request.setUrl(ftpPath);\n            uploadFileListReply = manager->put(request, newFile);\n            connect(uploadFileListReply, &QNetworkReply::finished, this, &MainWindow::uploadFileListFinished);\n            file->close();\n        }\n        QMessageBox::information(this, \"Success\", \"File successfully uploaded.\");\n    }\n}\n```\n\n28.  我们还需要定义更新进度条的函数，以便从 FTP 服务器下载文件，如以下代码所示:\n\n```cpp\nvoid MainWindow::downloadFileProgress(qint64 byteReceived,qint64 bytesTotal) {\n    qint64 percentage = 100 * byteReceived / bytesTotal;\n    ui->downloadProgress->setValue((int) percentage);\n}\n```\n\n29.  紧接着，我们可以开始实现`downloadFileFinished()`函数，当从服务器下载了一个文件时，就会调用这个函数。首先，我们需要检查文件是否已经成功下载，如下例所示:\n\n```cpp\nvoid MainWindow::downloadFileFinished() {\n    if(downloadFileReply->error() != QNetworkReply::NoError)\n        QMessageBox::warning(this, \"Failed\", \"Failed to download file: \" + downloadFileReply->errorString());\n    else {\n        QByteArray responseData;\n        if (downloadFileReply->isReadable())\n            responseData = downloadFileReply->readAll();\n```\n\n30.  如果有，我们会将文件保存到用户设置的下载目录中，如下面的代码所示:\n\n```cpp\n        if (!responseData.isEmpty()) {\n            QString folder = ui->downloadPath->text();\n            QFile file(folder + \"/\" + downloadFileName);\n            file.open(QIODevice::WriteOnly);\n            file.write((responseData));\n            file.close();\n            QMessageBox::information(this, \"Success\", \"File successfully downloaded.\");\n        }\n    }\n}\n```\n\n31.  在我们开始测试程序之前，让我们创建一个名为`files.txt`的空文本文件，并将其放在您链接到您在文件服务器中设置的 FTP 用户的文件夹中。\n\n32.  最后，构建并运行程序。试着上传一些文件到文件传输协议服务器。如果可行，文件列表应该更新并显示在列表小部件上。然后，尝试双击列表小部件上的文件名，并将文件下载到您的计算机上，如下图所示:\n\n![](img/302187bc-2fb8-458a-a897-1d35455ca456.png)\n\n33.  恭喜，您现在已经成功创建了一个工作的 FTP 程序！\n\n# 它是如何工作的...\n\n虽然这个项目要大得多，代码也更长，但它实际上与我们在前面的食谱中所做的 TCP 网络项目非常相似。我们还利用了 Qt 5 提供的信号和插槽机制，让我们的生活更加轻松。\n\n以前 Qt 直接通过`QFtp`类支持 FTP。然而，`QFtp`类在更新的版本中已经被弃用，所以我们必须通过使用网络类来实现我们自己的类，比如`QNetworkRequest`、`QNetworkReply`等等。\n\n因为我们不再有`QFtp`类，所以没有办法直接从**文件服务器**获取文件列表。因此，我们开发了一种解决这个问题的方法，即使用文本文件来存储文件列表。\n\n进度条对于这个项目显示上传和下载进度非常重要。这对于上传或下载大文件特别有用。"
  },
  {
    "path": "docs/qt5-cpp-gui-prog-cb/07.md",
    "content": "# 七、线程基础——异步编程\n\n本章将涵盖以下食谱:\n\n*   使用线程\n*   对象和线程\n*   数据保护和线程间共享数据\n*   使用不可运行的流程\n\n# 介绍\n\n大多数现代软件并行运行其进程，并将任务卸载到不同的线程，以利用现代 CPU 多核架构。这样，软件可以通过同时运行多个进程来提高效率，而不会影响性能。在本章中，我们将学习如何利用线程来提高我们的 Qt 5 应用的性能和效率。\n\n# 技术要求\n\n本章的技术要求包括安装 Qt 5.11.2 MinGW 32 位、Qt Creator 4.8.2 和 Windows 10。\n\n本章使用的所有代码都可以从下面的 GitHub 链接下载:[https://GitHub . com/PacktPublishing/Qt5-CPP-GUI-编程-cook book-第二版/树/master/Chapter07](https://github.com/PacktPublishing/Qt5-CPP-GUI-Programming-Cookbook-Second-Edition/tree/master/Chapter07) 。\n\n查看以下视频，查看正在运行的代码:[http://bit.ly/2TnFJUU](http://bit.ly/2TnFJUU)\n\n# 使用线程\n\nQt 5 提供了多种创建和使用线程的方法。您可以在高级方法和低级方法之间进行选择。高级方法更容易上手，但受限于你能用它们做什么。另一方面，低级方法更灵活，但对初学者不友好。在本食谱中，我们将学习如何使用其中一种高级方法轻松创建多线程 Qt 5 应用。\n\n# 怎么做…\n\n让我们按照以下步骤学习如何创建多线程应用:\n\n1.  创建一个 Qt 小部件应用并打开`main.cpp`。然后，在文件顶部添加以下标题:\n\n```cpp\n#include <QFuture>\n#include <QtConcurrent/QtConcurrent>\n#include <QFutureWatcher>\n#include <QThread>\n#include <QDebug>\n```\n\n2.  然后，在`main()`函数之前创建一个名为`printText()`的函数:\n\n```cpp\nvoid printText(QString text, int count) {\n    for (int i = 0; i < count; ++ i)\n        qDebug() << text << QThread::currentThreadId();\n    qDebug() << text << \"Done\";\n}\n```\n\n3.  之后，在`main()`功能中添加以下代码:\n\n```cpp\nint main(int argc, char *argv[]) {\n    QApplication a(argc, argv);\n    MainWindow w;\n    w.show();\n printText(\"A\", 100);\n printText(\"B\", 100);\n    return a.exec();\n}\n```\n\n4.  如果您现在构建并运行程序，您应该会看到在打印`B`之前先打印`A`。请注意，它们的线程标识都是相同的。这是因为我们正在主线程中运行`printText()`函数:\n\n```cpp\n...\n\"A\" 0x2b82c\n\"A\" 0x2b82c\n\"A\" 0x2b82c\n\"A\" Done\n...\n\"B\" 0x2b82c\n\"B\" 0x2b82c\n\"B\" 0x2b82c\n\"B\" Done\n```\n\n5.  为了将它们分成不同的线程，让我们使用 Qt 5 提供的名为`QFuture`的高级类。让我们注释掉`main()`中的两个`printText()`函数，并改用下面的代码:\n\n```cpp\nQFuture<void> f1 = QtConcurrent::run(printText, QString(\"A\"), 100);\nQFuture<void> f2 = QtConcurrent::run(printText, QString(\"B\"), 100);\nQFuture<void> f3 = QtConcurrent::run(printText, QString(\"C\"), 100);\nf1.waitForFinished();\nf2.waitForFinished();\nf3.waitForFinished();\n```\n\n6.  如果您再次构建并运行程序，您应该会在调试窗口上看到类似这样的内容，这意味着三个`printText()`函数现在并行运行:\n\n```cpp\n...\n\"A\" 0x271ec\n\"C\" 0x26808\n\"B\" 0x27a40\n\"A\" 0x271ec\n\"C\" Done\n\"B\" 0x27a40\n\"A\" Done\n\"B\" Done\n```\n\n7.  我们也可以使用`QFutureWatcher`通过信号和时隙机制通知一个`QObject`类:\n\n```cpp\nQFuture<void> f1 = QtConcurrent::run(printText, QString(\"A\"), 100);\nQFuture<void> f2 = QtConcurrent::run(printText, QString(\"B\"), 100);\nQFuture<void> f3 = QtConcurrent::run(printText, QString(\"C\"), 100);\n\nQFutureWatcher<void> futureWatcher;\nQObject::connect(&futureWatcher, QFutureWatcher<void>::finished, &w, MainWindow::mySlot);\nfutureWatcher.setFuture(f1);\n\nf1.waitForFinished();\nf2.waitForFinished();\nf3.waitForFinished();\n```\n\n8.  之后，打开`mainwindow.h`并声明槽功能:\n\n```cpp\npublic slots:\n    void mySlot();\n```\n\n9.  `mySlot()`功能在`mainwindow.cpp`中是这样的:\n\n```cpp\nvoid MainWindow::mySlot() {\n    qDebug() << \"Done!\" << QThread::currentThreadId();\n}\n```\n\n10.  如果您再次构建并运行该程序，这一次，您将看到如下结果:\n\n```cpp\n...\n\"A\" 0x271ec\n\"C\" 0x26808\n\"B\" 0x27a40\n\"A\" 0x271ec\n\"C\" Done\n\"B\" 0x27a40\n\"A\" Done\n\"B\" Done\nDone! 0x27ac0\n```\n\n11.  即使`QFutureWatcher`链接到`f1`，但是`Done!`消息只有在所有线程执行完毕后才会被打印出来。这是因为`mySlot()`正在主线程中运行，调试窗口中`Done!`消息旁边显示的线程标识证明了这一点。\n\n# 它是如何工作的...\n\n默认情况下，在任何 Qt 5 应用中都有一个主线程(也称为图形用户界面线程)。您创建的其他线程称为**工作线程**。\n\n与 GUI 相关的类，比如`QWidgets`、`QPixmap`，只能存在于主线程中，所以在处理这些类的时候一定要格外小心。\n\n`QFuture`是处理异步计算的高级类。\n\n我们使用`QFutureWatcher`类让`QFuture`与信号和插槽交互。您甚至可以使用它在进度条上显示操作进度。\n\n# 对象和线程\n\n接下来，我们想探索一些其他的方法，以便在 Qt 5 应用中使用线程。Qt 5 提供了一个名为`QThread`的类，它可以让你更好地控制如何创建和执行一个线程。一个`QThread`对象通过调用`run()`函数开始在线程中执行其事件循环。在这个例子中，我们将通过`QThread`学习如何让`QObject`异步协同工作。\n\n# 怎么做…\n\n让我们从执行以下步骤开始:\n\n1.  创建一个新的 Qt 小部件应用项目。然后，转到文件|新文件或项目...并创建一个 C++ 类:\n\n![](img/6d06aa21-1d73-4cb2-8aa8-78d3707d3131.png)\n\n2.  之后命名新类`MyWorker`，使其继承自`QObject`类。不要忘记默认包括`QObject`类:\n\n![](img/3bf18144-a9b2-4901-8905-5f994c534de8.png)\n\n3.  一旦创建了`MyWorker`类，打开`myworker.h`并在顶部添加以下标题:\n\n```cpp\n#include <QObject>\n#include <QDebug>\n```\n\n4.  之后，将以下信号和插槽功能也添加到文件中:\n\n```cpp\nsignals:\n    void showResults(int res);\n    void doneProcess();\n\npublic slots:\n    void process();\n```\n\n5.  接下来，打开`myworker.cpp`并执行`process()`功能:\n\n```cpp\nvoid MyWorker::process() {\n    int result = 0;\n    for (int i = 0; i < 2000000000; ++ i) {\n        result += 1;\n    }\n    emit showResults(result);\n    emit doneProcess();\n}\n```\n\n6.  之后，打开`mainwindow.h`并在顶部添加以下标题:\n\n```cpp\n#include <QDebug>\n#include <QThread>\n#include \"myworker.h\"\n```\n\n7.  然后，声明一个槽函数，如下面的代码所示:\n\n```cpp\npublic slots:\n    void handleResults(int res);\n```\n\n8.  完成后，打开`mainwindow.cpp`并执行`handResults()`功能:\n\n```cpp\nvoid MainWindow::handleResults(int res) {\n    qDebug() << \"Handle results\" << res;\n}\n```\n\n9.  最后，我们将向`MainWindow`类的类构造函数添加以下代码:\n\n```cpp\nMainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow){\n    ui->setupUi(this);\n    QThread* workerThread = new QThread;\n    MyWorker *workerObject = new MyWorker;\n    workerObject->moveToThread(workerThread);\n    connect(workerThread, &QThread::started, workerObject, &MyWorker::process);\n    connect(workerObject, &MyWorker::doneProcess, workerThread, &QThread::quit);\n    connect(workerObject, &MyWorker::doneProcess, workerObject, &MyWorker::deleteLater);\n    connect(workerObject, &MyWorker::showResults, this, &MainWindow::handleResults);\n    connect(workerThread, &QThread::finished, workerObject, &MyWorker::deleteLater);\n    workerThread->start();\n}\n```\n\n10.  立即构建并运行程序。您应该看到，在调试窗口上打印一行消息之前，主窗口会弹出，几秒钟内什么也不做:\n\n```cpp\nFinal result: 2000000000\n```\n\n11.  结果是在一个单独的线程中计算的，这就是为什么主窗口可以平滑显示，甚至可以在计算过程中用鼠标移动。为了查看在主线程上运行计算时的区别，让我们注释掉一些代码，直接调用`process()`函数:\n\n```cpp\n//QThread* workerThread = new QThread;\nMyWorker *workerObject = new MyWorker;\n//workerObject->moveToThread(workerThread);\n//connect(workerThread, &QThread::started, workerObject, &MyWorker::process);\n//connect(workerObject, &MyWorker::doneProcess, workerThread, &QThread::quit);\nconnect(workerObject, &MyWorker::doneProcess, workerObject, &MyWorker::deleteLater);\nconnect(workerObject, &MyWorker::showResults, this, &MainWindow::handleResults);\n//connect(workerThread, &QThread::finished, workerObject, &MyWorker::deleteLater);\n//workerThread->start();\nworkerObject->process();\n```\n\n12.  立即构建并运行项目。这一次，主窗口只会在计算完成后出现在屏幕上。这是因为计算阻塞了主线程(或图形用户界面线程)并阻止了主窗口的显示。\n\n# 它是如何工作的...\n\n`QThread`是异步运行进程的替代方法，除了使用`QFuture`类。与`QFuture`相比，它给了我们更多的控制权，我们将在下面的食谱中演示。\n\n请注意，移动到工作线程的`QObject`类不能有任何父类，因为 Qt 的设计方式是整个对象树必须存在于同一个线程中。因此，当你调用`moveToThread()`时，一个`QObject`类的所有孩子也会被移动到工作线程。\n\n如果您希望您的工作线程与主线程通信，请使用信号和插槽机制。\n\n我们使用`QThread`类提供的`started`信号通知我们的工作对象开始计算，因为工作线程已经创建。\n\n然后，当计算完成后，我们发出`showResult`和`doneProcess`信号通知线程退出，同时将最终结果传递给主线程进行打印。\n\n最后，我们还使用信号和槽机制来安全地删除工作线程和工作对象。\n\n# 数据保护和线程间共享数据\n\n即使多线程使进程异步运行，也会有线程必须停止并等待其他线程的时候。这通常发生在两个线程同时修改同一个变量的时候。强制线程相互等待以保护共享资源(如数据)是很常见的。Qt 5 还提供了低级方法和高级机制来同步线程。\n\n# 怎么做…\n\n我们将继续使用上一个示例项目中的代码，因为我们已经建立了一个多线程工作程序:\n\n1.  打开`myworker.h`，添加如下表头:\n\n```cpp\n#include <QObject>\n#include <QDebug>\n#include <QMutex>\n```\n\n2.  然后，我们将添加两个新变量，并对类构造函数进行一些更改:\n\n```cpp\npublic:\n    explicit MyWorker(QMutex *mutex);\n int* myInputNumber;\n QMutex* myMutex;\n\nsignals:\n    void showResults(int res);\n    void doneProcess();\n```\n\n3.  之后，打开`myworker.cpp`，将类构造函数改为如下代码。我们不再需要父输入，因为对象没有父项:\n\n```cpp\nMyWorker::MyWorker(QMutex *mutex) {\n myMutex = mutex;\n}\n```\n\n4.  我们还会将`process()`功能更改为如下所示:\n\n```cpp\nvoid MyWorker::process() {\n    myMutex->lock();\n    for (int i = 1; i < 100000; ++ i){\n        *myInputNumber += i * i + 2 * i + 3 * i;\n    }\n    myMutex->unlock();\n    emit showResults(*myInputNumber);\n    emit doneProcess();\n}\n```\n\n5.  完成后，打开`mainwindow.cpp`并对代码进行一些更改:\n\n```cpp\nMainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) {\n    ui->setupUi(this);\n\n    int myNumber = 5;\n    QMutex* newMutex = new QMutex;\n\n    QThread* workerThread = new QThread;\n    QThread* workerThread2 = new QThread;\n    QThread* workerThread3 = new QThread;\n    MyWorker *workerObject = new MyWorker(newMutex);\n    MyWorker *workerObject2 = new MyWorker(newMutex);\n    MyWorker *workerObject3 = new MyWorker(newMutex);\n```\n\n6.  之后，我们将工作对象的`myInputNumber`变量设置为`myNumber`。请注意，我们引用的是它的指针而不是值:\n\n```cpp\nworkerObject->myInputNumber = &myNumber;\nworkerObject->moveToThread(workerThread);\nconnect(workerThread, &QThread::started, workerObject, &MyWorker::process);\nconnect(workerObject, &MyWorker::doneProcess, workerThread, &QThread::quit);\nconnect(workerObject, &MyWorker::doneProcess, workerObject, &MyWorker::deleteLater);\nconnect(workerObject, &MyWorker::showResults, this, &MainWindow::handleResults);\nconnect(workerThread, &QThread::finished, workerObject, &MyWorker::deleteLater);\n```\n\n7.  重复上一步两次，设置`workerObject2`、`workerThread2`、`workerObject3`和`workerThread3`:\n\n```cpp\nworkerObject2->myInputNumber = &myNumber;\nworkerObject2->moveToThread(workerThread2);\nconnect(workerThread2, &QThread::started, workerObject2, &MyWorker::process);\nconnect(workerObject2, &MyWorker::doneProcess, workerThread2, &QThread::quit);\nconnect(workerObject2, &MyWorker::doneProcess, workerObject2, &MyWorker::deleteLater);\nconnect(workerObject2, &MyWorker::showResults, this, &MainWindow::handleResults);\nconnect(workerThread2, &QThread::finished, workerObject2, &MyWorker::deleteLater);\n\nworkerObject3->myInputNumber = &myNumber;\nworkerObject3->moveToThread(workerThread3);\nconnect(workerThread3, &QThread::started, workerObject3, &MyWorker::process);\nconnect(workerObject3, &MyWorker::doneProcess, workerThread3, &QThread::quit);\nconnect(workerObject3, &MyWorker::doneProcess, workerObject3, &MyWorker::deleteLater);\nconnect(workerObject3, &MyWorker::showResults, this, &MainWindow::handleResults);\nconnect(workerThread3, &QThread::finished, workerObject3, &MyWorker::deleteLater);\n```\n\n8.  最后，我们将通过调用`start()`开始运行这些线程:\n\n```cpp\nworkerThread->start();\nworkerThread2->start();\nworkerThread3->start();\n```\n\n9.  如果您现在构建并运行该程序，无论运行多少次，您都应该看到一致的结果:\n\n```cpp\nFinal result: -553579035\nFinal result: -1107158075\nFinal result: -1660737115\n```\n\n10.  我们每次运行程序都会得到结果，因为互斥锁确保只有一个线程能够修改数据，而其他线程等待数据完成。要了解没有互斥锁的区别，让我们注释一下代码:\n\n```cpp\nvoid MyWorker::process() {\n //myMutex->lock();\n\n    for (int i = 1; i < 100000; ++ i) {\n        *myInputNumber += i * i + 2 * i + 3 * i;\n    }\n\n //myMutex->unlock();\n\n    emit showResults(*myInputNumber);\n    emit doneProcess();\n}\n```\n\n11.  再次构建并运行程序。这一次，当你运行程序时，你会得到一个非常不同的结果。例如，我在三种情况下运行它时获得了以下结果:\n\n```cpp\n1st time:\nFinal result: -589341102\nFinal result: 403417142\nFinal result: -978935318\n\n2nd time:\nFinal result: 699389030\nFinal result: -175723048\nFinal result: 1293365532\n\n3rd time:\nFinal result: 1072831160\nFinal result: 472989964\nFinal result: -534842088\n```\n\n12.  发生这种情况是因为`myNumber`数据被所有线程以随机顺序同时操纵，这是由于并行计算的特性。通过锁定互斥体，我们确保数据只能由单个线程修改，从而消除了这个问题。\n\n# 它是如何工作的...\n\nQt 5 提供了`QMutex`和`QReadWriteLock`两个类，用于多个线程访问和修改同一数据时的数据保护。我们在前面的例子中只使用了`QMutex`，但是这两个类本质上非常相似。唯一的区别是`QReadWriteLock`允许其他线程在写入数据的同时读取数据。与`QMutex`不同，它将读和写状态分开，但一次只能出现一个状态(要么锁定为读，要么锁定为写)，而不是两者都出现。对于复杂的函数和语句，使用高级的`QMutexLocker`类代替`QMutex`来简化代码，更容易调试。\n\n这种方法的缺点是，当数据被单个线程修改时，所有其他线程都将无所事事。最好不要与多个线程共享数据，除非没有其他方法，因为这将停止其他线程并挫败并行计算的目标。\n\n# 使用不可运行的流程\n\n在本食谱中，我们将学习如何使用另一种高级方法轻松创建多线程 Qt 5 应用。我们将使用本食谱中的`QRunnable`和`QThreadPool`课程。\n\n# 怎么做…\n\n让我们从执行以下步骤开始:\n\n1.  创建一个新的 Qt Widgets Application 项目，并创建一个名为`MyProcess`的新 C++ 类，该类继承了`QRunnable`类。\n2.  接下来，打开`myprocess.h`并添加以下标题:\n\n```cpp\n#include <QRunnable>\n#include <QDebug>\n```\n\n3.  然后，声明`run()`功能，如下所示:\n\n```cpp\nclass MyProcess : public QRunnable {\npublic:\n    MyProcess();\n void run();\n};\n```\n\n4.  之后，打开`myprocess.cpp`并定义`run()`功能:\n\n```cpp\nvoid MyProcess::run() {\n    int myNumber = 0;\n    for (int i = 0; i < 100000000; ++ i) {\n        myNumber += i;\n    }\n    qDebug() << myNumber;\n}\n```\n\n5.  完成后，将以下标题添加到`mainwindow.h`:\n\n```cpp\n#include <QMainWindow>\n#include <QThreadPool>\n#include \"myprocess.h\"\n```\n\n6.  之后，我们将通过添加以下代码来实现类构造函数:\n\n```cpp\nMainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) {\n  ui->setupUi(this);\n\n  MyProcess* process = new MyProcess;\n  MyProcess* process2 = new MyProcess;\n  MyProcess* process3 = new MyProcess;\n  MyProcess* process4 = new MyProcess;\n\n  QThreadPool::globalInstance()->start(process);\n  QThreadPool::globalInstance()->start(process2);\n  QThreadPool::globalInstance()->start(process3);\n  QThreadPool::globalInstance()->start(process4);\n\n  qDebug() << QThreadPool::globalInstance()->activeThreadCount();\n}\n```\n\n7.  现在，构建并运行该项目。您应该看到这些进程正在不同的线程中成功运行，其中活动线程数为 4。\n8.  `QThreadPool`类在执行完最后一个进程后会自动停用线程。让我们尝试通过暂停程序三秒钟并再次打印出活动线程数来证明这一点:\n\n```cpp\nqDebug() << QThreadPool::globalInstance()->activeThreadCount();\nthis->thread()->sleep(3);\nqDebug() << QThreadPool::globalInstance()->activeThreadCount();\n```\n\n9.  再次构建并运行程序。这一次，您应该看到活动线程数是四，然后，三秒钟后，活动线程数变成零。这是因为所有的流程都已经执行了。\n\n# 它是如何工作的...\n\n`QRunnable`类与管理线程集合的`QThreadPool`类协同工作。`QThreadPool`类自动管理和回收单个`QThreads` 对象，以避免过于频繁地创建和销毁线程，这有助于降低计算成本。\n\n要使用`QThreadPool`，必须对`QRunnable`对象进行子类化，并实现名为`run()`的虚拟函数。默认情况下，当最后一个线程退出`run`功能时，`QThreadPool`会自动删除`QRunnable`对象。您可以通过调用`setAutoDelete()`将`autoDelete`变量更改为`false`来更改此行为。\n\n默认情况下，超过 30 秒未使用的线程将过期。您可以在线程运行之前通过调用`setExpiryTimeout()`来更改该持续时间。否则，它不会对超时设置产生任何影响。\n\n也可以通过调用`setMaxThreadCount()`设置可以使用的最大线程数。要获取当前活动线程的总数，只需调用`activeThreadCount()`。"
  },
  {
    "path": "docs/qt5-cpp-gui-prog-cb/08.md",
    "content": "# 八、使用 Qt5 构建触摸屏应用\n\n本章将涵盖以下食谱:\n\n*   为移动应用设置 Qt\n*   与 QML 一起设计基本用户界面\n*   触摸事件\n*   QML 的动画\n*   使用模型/视图显示信息\n*   集成 QML 和 C++\n\n# 介绍\n\nQt 不仅是 PC 平台的跨平台软件开发套件；它还支持移动平台，如 iOS 和安卓。Qt 的开发人员早在 2010 年就推出了 **Qt Quick** ，它提供了一种简单的方法来构建高度动态的自定义用户界面，用户只需最少的编码就可以轻松创建流畅的过渡和效果。Qt Quick 使用了一种名为 **QML** 的声明性脚本语言，类似于网络开发中使用的 **JavaScript** 语言。高级用户还可以用 C++ 创建自定义函数，并将其移植到 Qt Quick 以增强其功能。目前，Qt Quick 支持多个平台，如视窗、Linux、苹果、iOS 和安卓。\n\n# 技术要求\n\n本章的技术要求包括适用于 armeabi-v7a 的 Qt 5.11.2 MinGW 32 位/安卓、Qt Creator 4.8.2 和 Windows 10。\n\n本章使用的所有代码可从以下 GitHub 链接下载:[https://GitHub . com/PacktPublishing/Qt5-CPP-GUI-Programming-cook book-第二版/树/主/第 08 章](https://github.com/PacktPublishing/Qt5-CPP-GUI-Programming-Cookbook-Second-Edition/tree/master/Chapter08)。\n\n查看以下视频，查看正在运行的代码:[http://bit.ly/2FlnIS0](http://bit.ly/2FlnIS0)\n\n# 为移动应用设置 Qt\n\n在本例中，我们将学习如何在 Qt Quick 中设置我们的 Qt 项目，并使其能够被构建和导出到移动设备。\n\n# 怎么做…\n\n让我们开始学习如何使用 Qt 5 创建我们的第一个移动应用:\n\n1.  首先，让我们通过转到文件|新文件或项目来创建一个新项目。然后，会弹出一个窗口供您选择项目模板。选择 Qt 快速应用-空，然后单击选择...按钮，如下图所示:\n\n![](img/ff8de156-66b3-43b8-a972-d1dacfba0964.png)\n\n2.  之后，插入项目名称并选择项目位置。单击“下一步”按钮，它会要求您选择项目所需的最小 Qt 版本。\n\nPlease make sure that you select a version that exists on your computer, otherwise you won't be able to run it properly.\n\n3.  完成后，单击“下一步”按钮继续。\n\n4.  然后，Qt Creator 会问你想在你的项目中使用哪个工具包。这些**工具包**基本上是不同的编译器，你可以用它们来为不同的平台编译你的项目。由于我们正在为移动平台开发应用，我们将启用安卓套件(如果您运行的是苹果电脑，则启用 iOS 套件)，以便构建您的应用并将其导出到您的移动设备，如下图所示。您也可以启用其中一个桌面套件，以便事先在桌面上测试您的程序。请注意，如果您是第一次使用安卓套件，您需要配置它，以便 Qt 可以找到安卓软件开发工具包的目录。完成后，单击下一步:\n\n![](img/b02a67d4-5a03-4b9f-957c-7379ffff021e.png)\n\n5.  项目创建完成后，Qt Creator 会自动从您的项目中打开一个名为`main.qml`的文件。您将看到一种不同类型的脚本，如下面的代码所示，它与您通常的 C/C++ 项目非常不同:\n\n```cpp\nimport QtQuick 2.11\nimport QtQuick.Window 2.11\n\nWindow {\n    visible: true\n    width: 640\n    height: 480\n    title: qsTr(\"Hello World\")\n}\n```\n\n6.  点击位于 Qt Creator 左下角的绿色箭头按钮，立即构建并运行项目，如下图所示。如果您将默认工具包设置为其中一个桌面工具包，项目编译完成后将弹出一个空窗口:\n\n![](img/9cfcbae3-295d-43af-a71b-7314f5894b97.png)\n\n7.  我们可以在不同的工具包之间切换，方法是转到“项目”界面，选择您想要用来构建项目的工具包，如下图所示。您还可以管理计算机上所有可用的工具包，或者从“项目”界面向项目添加新的工具包:\n\n![](img/68416d33-f88a-43f8-bed1-c4103d52d7a1.png)\n\n8.  如果这是您第一次构建和运行项目，您需要在构建设置下为安卓工具包创建一个模板。一旦您点击了“构建安卓 APK”标签下的“创建模板”按钮，如下图所示，Qt 将生成在安卓设备上运行您的应用所需的所有文件。如果您不打算在项目中使用渐变，请禁用将渐变文件复制到安卓目录选项。否则，您可能会在尝试编译应用并将其部署到移动设备时遇到问题:\n\n![](img/25057cbb-7a8a-406e-bf5f-9c64fa7b05cc.png)\n\n9.  创建模板后，单击运行按钮。您现在应该会看到以下窗口弹出，询问它应该导出到哪个设备:\n\n![](img/6d578e64-4441-462e-b8be-b3a0ed40eb90.png)\n\n10.  选择当前连接到计算机的设备，然后按“确定”按钮。等待一段时间，让它构建项目，您应该可以在移动设备上运行一个空白应用。\n\n# 它是如何工作的…\n\nQt Quick 应用项目与表单应用项目有很大不同。你大部分时间都在写 QML 脚本，而不是写 C/C++ 代码。需要**安卓软件开发工具包** ( **SDK** )、**安卓原生开发工具包** ( **NDK** )、 **Java 开发工具包** ( **JDK** )和 **Apache Ant** 将你的应用构建并导出到安卓平台。或者，您也可以在您的安卓套件中使用 Gradle 而不是 Apache Ant。您所需要做的就是启用使用 Gradle 而不是 Ant 选项，并为 Qt 提供 Gradle 的安装路径。请注意，Qt Creator 目前不支持安卓工作室:\n\n![](img/62350aa8-4eb0-4a82-b9d7-f2527e771e6d.png)\n\n如果您在安卓设备上运行该应用，请确保您已经启用了 **USB 调试模式**。要启用 USB 调试模式，您需要首先在安卓设备上启用开发人员选项，方法是转到“设置”|“关于电话”，然后轻按“内部版本号”七次。之后，进入设置|开发者选项，你会看到菜单中的**调试选项。启用该选项后，您现在可以将应用导出到您的设备进行测试。**\n\n为了构建 iOS 平台，您需要在 Mac 上运行 Qt Creator，并确保最新的 Xcode 也安装在您的 Mac 上。要在 iOS 设备上测试您的应用，您需要在苹果公司注册一个开发人员帐户，在开发人员门户网站注册您的设备，并将配置安装到您的 Xcode，这比安卓要复杂得多。一旦您从苹果公司获得开发人员帐户，您将可以访问开发人员门户。\n\n# 与 QML 一起设计基本用户界面\n\n在这个例子中，我们将学习如何使用 Qt 快速设计器来设计我们程序的用户界面。\n\n# 怎么做…\n\n让我们按照以下步骤开始:\n\n1.  首先，创建一个新的 Qt Quick 应用项目，就像我们在前面的食谱中所做的那样。\n\nYou can also use the previous project files if you wish to.\n\n2.  您将在项目资源中看到一个 QML 文件— `main.qml`。这是我们实现应用逻辑的地方，但是我们还需要另一个 QML 文件来定义用户界面。\n\n3.  让我们通过转到文件|新文件或项目来创建 QML 用户界面文件，然后在 Qt 类别下选择 QtQuick 用户界面文件，如下图所示:\n\n![](img/ef137ada-5d01-4acf-bac9-40b1549891ce.png)\n\n4.  我们把组件名`Main`和组件表单名`MainForm`叫做:\n\n![](img/f6e90130-e122-4e81-ba44-d1ec234d23bc.png)\n\n5.  一旦`MainForm.ui.qml`文件被创建，它将被 Qt 创建者打开。与我们在前面章节中使用的编辑器相比，您将看到完全不同的用户界面编辑器。这个编辑器叫做 **Qt 快速设计器**，专门用来为 Qt 快速项目设计用户界面。该编辑器的组件描述如下:\n\n6.  我们将制作一个简单的登录屏幕。从“库”窗口，将两个文本小部件拖到画布上。\n\n7.  将文本小部件的文本属性设置为`Username:`和`Password:`:\n\n![](img/a977ca9c-027b-40a2-91c9-f08262fe8774.png)\n\n8.  将两个矩形从“资源库”窗口拖到画布上，然后将两个文本输入小部件拖到画布上，并将每个小部件都作为您刚刚添加到画布上的矩形的父级。将矩形的边框属性设置为`1`，半径设置为`5`。然后，将其中一个文本字段的回显模式设置为密码。\n9.  现在，我们将通过将鼠标区域小部件与矩形和文本小部件相结合来手动创建一个按钮小部件。将鼠标区域小部件拖到画布上，然后将矩形和文本小部件拖到画布上，并将它们作为鼠标区域的父项。将矩形的颜色设置为`#bdbdbd`，然后将其边框属性设置为`1`，将其半径设置为`5`。然后，将文本设置为`Login`，并确保鼠标区域的大小与矩形相同。\n10.  之后，将另一个矩形拖到画布上，作为登录表单的容器，这样看起来会很整洁。将其边框颜色设置为`#5e5858`，将其边框属性设置为`2`。然后，将其半径属性设置为`5`以使其拐角看起来有点圆。\n11.  确保我们在上一步中添加的矩形位于导航窗口中层次结构的顶部，以便它出现在所有其他小部件的后面。您可以通过按下位于导航窗口顶部的箭头按钮来排列小部件在层次结构中的位置，如下所示:\n\n![](img/ffcb33f5-7c8e-486e-82a2-149547c585db.png)\n\n12.  接下来，我们将导出三个小部件——鼠标区域和两个文本输入小部件——作为根项目的别名属性，以便稍后我们可以从`main.qml`文件访问这些小部件。通过单击小部件名称后面的小图标，并确保图标变为打开状态，可以导出小部件。\n13.  到目前为止，您的用户界面应该如下所示:\n\n![](img/b5c67662-253b-49b9-a835-9841655dcb5b.png)\n\n14.  现在让我们打开`main.qml`。默认情况下，Qt Creator 不会在 Qt 快速设计器中打开此文件，而是使用脚本编辑器打开。这是因为所有与用户界面设计相关的任务都是在`MainForm.ui.qml`中完成的，`main.qml`只是用来定义将要应用于 UI 的逻辑和功能。但是，您可以使用 Qt 快速设计器打开它，通过单击编辑器左侧边栏中的“设计”按钮来预览用户界面。\n15.  在脚本顶部，添加第三行，将对话框模块导入`main.qml`，如下代码所示:\n\n```cpp\nimport QtQuick 2.9\nimport QtQuick.Window 2.3\nimport QtQuick.Dialogs 1.2\n```\n\n16.  之后，用以下代码替换以下代码:\n\n```cpp\nWindow {\n    visible: true\n    width: 360\n    height: 360\n    MainForm {\n        anchors.fill: parent\n        loginButton.onClicked: {\n            messageDialog.text = \"Username is \" + userInput.text + \" and password is \" + passInput.text\n            messageDialog.visible = true\n        }\n    }\n```\n\n17.  我们继续定义`messageDialog`如下:\n\n```cpp\n    MessageDialog {\n        id: messageDialog\n        title: \"Fake login\"\n        text: \"\"\n        onAccepted: {\n            console.log(\"You have clicked the login button\")\n            Qt.quit()\n        }\n    }\n}\n```\n\n18.  在您的电脑上构建并运行此程序，当您单击登录按钮时，您应该会得到一个显示消息框的简单程序:\n\n![](img/b9cabb46-c416-44b2-a0d9-aa3f5299ee2b.png)\n\n# 它是如何工作的…\n\n从 Qt 5.4 开始，引入了一个新的文件扩展名`.ui.qml`。QML 引擎像正常的`.qml`文件一样处理它，但是禁止在其中写入任何逻辑实现。它作为用户界面定义模板，可以在不同的`.qml`文件中重用。UI 定义和逻辑实现的分离提高了 QML 代码的可维护性，并创建了更好的工作流。\n\nQt Quick–Basic 下的所有小部件都是最基本的小部件，我们可以使用它们来混合、匹配和创建新类型的小部件，如下所示:\n\n![](img/230d0ad8-094f-4220-8df6-2d0717f88030.png)\n\n在前面的示例中，我们已经学习了如何将三个小部件组合在一起——一个文本、一个鼠标区域和一个矩形，以形成一个按钮小部件。但是，如果你很懒，你可以通过转到库窗口中的\n导入选项卡，然后点击<添加导入>按钮，将预先制作的模块导入到你的 Qt Quick 项目中。然后，从下拉列表中选择要添加到项目中的模块，如下所示。一旦您在 QML 脚本和 C++ 编程方面取得了进步，您也可以创建自己的 Qt Quick 模块:\n\n![](img/199a9813-31f2-42c2-be50-34e5e35c5ca7.png)\n\n我们在`main.qml`中导入`QtQuick.Dialogs.qml 1.0`模块，创建了一个消息框，显示用户按下登录按钮时填写的用户名和密码，这样我们就可以证明用户界面功能在工作。如果部件没有从`MainForm.ui.qml`导出，我们将无法在`main.qml`中访问其属性。\n\n此时，我们可以将程序导出到 iOS 和 Android，但在一些分辨率更高或每像素密度更高**(**DPI**单位)的设备上，用户界面可能看起来不准确。我们将在本章后面讨论这个问题。**\n\n **# 触摸事件\n\n在本节中，我们将学习如何使用 Qt Quick 开发一个在移动设备上运行的触摸驱动应用。\n\n# 怎么做…\n\n让我们开始遵循以下分步指南:\n\n1.  创建一个新的 Qt 快速应用-空项目。\n2.  在 Qt 创建器中，右键单击`qml.qrc`并选择在编辑器中打开。然后，单击添加|添加文件，将`tux.png`添加到项目中，如下所示:\n\n![](img/f9349f23-a269-43a8-bba3-17bb93b94793.png)\n\n3.  接下来，打开`MainForm.ui.qml`。将图像小部件从“库”窗口拖到画布上。然后，将图像的来源设置为`tux.png`，将其`fillMode`设置为`PreserveAspectFit`。之后，将其宽度设置为`200`，高度设置为`220`。\n4.  确保鼠标区域小部件和图像小部件都作为根项目的别名属性导出，方法是单击它们各自小部件名称旁边的小图标。\n5.  之后，点击编辑器左侧边栏上的编辑按钮，切换到脚本编辑器。我们需要将鼠标区域小部件更改为多点触摸区域小部件，如以下代码所示:\n\n```cpp\nMultiPointTouchArea {\n    id: touchArea\n    anchors.fill: parent\n    touchPoints: [\n        TouchPoint { id: point1 },\n        TouchPoint { id: point2 }\n    ]\n}\n```\n\n6.  我们还将`Image`小部件默认设置为自动放置在窗口中心，如下所示:\n\n```cpp\nImage {\n    id: tux\n    x: (window.width / 2) - (tux.width / 2)\n    y: (window.height / 2) - (tux.height / 2)\n    width: 200\n    height: 220\n    fillMode: Image.PreserveAspectFit\n    source: \"tux.png\"\n}\n```\n\n7.  最终的用户界面应该如下所示:\n\n![](img/28cfc9e1-30ba-4737-9b14-fcaa6d229133.png)\n\n8.  完成后，让我们打开`main.qml`。首先，清除`MainForm`对象内除`anchors.fill: parent`以外的所有内容，如下代码所示:\n\n```cpp\nimport QtQuick 2.9\nimport QtQuick.Window 2.3\nWindow {\n    visible: true\n    MainForm {\n        anchors.fill: parent\n    }\n}\n```\n\n9.  之后，在`MainForm`对象中声明几个变量，用于重新缩放图像小部件。如果您想了解以下代码中使用的 property 关键字的更多信息，请查看本示例末尾的*还有更多……*部分:\n\n```cpp\nproperty int prevPointX: 0\nproperty int prevPointY: 0\nproperty int curPointX: 0\nproperty int curPointY: 0\nproperty int prevDistX: 0\nproperty int prevDistY: 0\nproperty int curDistX: 0\nproperty int curDistY: 0\nproperty int tuxWidth: tux.width\nproperty int tuxHeight: tux.height\n```\n\n10.  使用下面的代码，我们将定义当我们的手指触摸多点区域小部件时会发生什么。在这种情况下，如果多个手指触摸多点触摸区域，我们将保存第一和第二触摸点的位置。我们还保存了图像小部件的宽度和高度，以便稍后我们可以使用这些变量来计算手指开始移动时图像的比例:\n\n```cpp\ntouchArea.onPressed: {\n    if (touchArea.touchPoints[1].pressed) {\n        if (touchArea.touchPoints[1].x < touchArea.touchPoints[0].x)\n            prevDistX = touchArea.touchPoints[1].x - touchArea.touchPoints[0].x\n        else\n            prevDistX = touchArea.touchPoints[0].x - touchArea.touchPoints[1].x\n        if (touchArea.touchPoints[1].y < touchArea.touchPoints[0].y)\n            prevDistY = touchArea.touchPoints[1].y - touchArea.touchPoints[0].y\n        else\n            prevDistY = touchArea.touchPoints[0].y - touchArea.touchPoints[1].y\n        tuxWidth = tux.width\n        tuxHeight = tux.height\n    }\n}\n```\n\n11.  下图显示了当两个手指在**触摸区域**边界内触摸屏幕时，触摸点被注册的示例。**触摸区域.触摸点[0]** 是第一个注册的触摸点，**触摸区域.触摸点[1]** 是第二个。然后，我们计算两个触摸点之间的 *X* 和 *Y* 距离，并将其保存为**前距离**和**前距离**，如下所示:\n\n![](img/2c0319b1-b494-4583-bb51-7c6a7adab360.png)\n\n12.  之后，我们将使用以下代码定义当我们的手指保持与屏幕接触并且仍然在触摸区域的边界内移动时会发生什么。此时，我们将使用上一步中保存的变量来计算图像的比例。同时，如果我们检测到只发现了单次触摸，那么我们将移动图像，而不是改变其比例:\n\n```cpp\ntouchArea.onUpdated: {\n    if (!touchArea.touchPoints[1].pressed) {\n        tux.x += touchArea.touchPoints[0].x - touchArea.touchPoints[0].previousX\n        tux.y += touchArea.touchPoints[0].y - touchArea.touchPoints[0].previousY\n    }\n    else {\n        if (touchArea.touchPoints[1].x < touchArea.touchPoints[0].x)\n            curDistX = touchArea.touchPoints[1].x - touchArea.touchPoints[0].x\n        else\n            curDistX = touchArea.touchPoints[0].x - touchArea.touchPoints[1].x\n        if (touchArea.touchPoints[1].y < touchArea.touchPoints[0].y)\n            curDistY = touchArea.touchPoints[1].y - touchArea.touchPoints[0].y\n        else\n            curDistY = touchArea.touchPoints[0].y - touchArea.touchPoints[1].y\n        tux.width = tuxWidth + prevDistX - curDistX\n        tux.height = tuxHeight + prevDistY - curDistY\n    }\n}\n```\n\n13.  下图显示了移动触摸点的示例–**触摸区域.触摸点【0】**从点 **A** 移动到点 **B** ，而**触摸区域.触摸点【1】**从点 **C** 移动到点 **D** 。然后，我们可以通过查看先前的 **X** 、 **Y** 变量与当前变量之间的差异来确定触摸点移动了多少个单位:\n\n![](img/3ac5b2af-af66-46e9-8bfc-56b395367b97.png)\n\n14.  现在，您可以构建程序并将其导出到移动设备。您将无法在不支持多点触控的平台上测试此程序。一旦程序在移动设备(或支持多点触控的台式机/笔记本电脑)上运行，尝试两件事——仅将一个手指放在屏幕上并四处移动，将两个手指放在屏幕上并向相反方向移动。你应该看到的是，如果你只用一个手指，企鹅会被移动到另一个地方，如果你用两个手指，它会被放大或缩小，如下图截图所示:\n\n![](img/71ac9550-cbb7-4bf2-8d5e-a6361e55c858.png)\n\n# 它是如何工作的…\n\n当手指触摸设备屏幕时，多点触摸区域小部件触发**按下**事件，并记录内部阵列中每个触摸点的位置。我们可以通过告诉 Qt 您想要访问哪个触摸点来获得这些数据。第一次触摸会带有`0`的索引号，第二次触摸会是`1`，以此类推。然后，我们将这些数据保存到变量中，以便以后检索这些数据来计算企鹅图像的缩放比例。\n\n当一个或多个手指在移动时保持与屏幕接触时，多点触摸区域将触发**未激活**事件。然后，我们将检查有多少次触摸——如果只找到一次触摸，我们将根据手指移动的程度来移动企鹅图像。如果有多个触摸，我们将比较两个触摸之间的距离，并将其与之前保存的变量进行比较，以确定我们应该重新缩放图像的大小。\n\n该图显示，在屏幕上轻击手指将触发**按下**事件，而在屏幕上滑动手指将触发**按下**事件:\n\n![](img/37248e33-3de9-4368-911f-2b52d541a6d5.png)\n\n我们还必须检查第一次触摸是在第二次触摸的左侧还是右侧。这样，我们可以防止图像在手指移动的相反方向上缩放，从而产生不准确的结果。至于企鹅的移动，我们只需要得到当前触摸位置和前一个触摸位置之间的差异，将其添加到企鹅的坐标中，就完成了。单次触摸事件通常比多次触摸事件简单和直接得多。\n\n# 还有更多…\n\n在 Qt Quick 中，其所有组件都有内置属性，如`width`、`height`和`color`，默认情况下这些属性会附加到组件上。但是，Qt Quick 还允许您创建自己的自定义属性，并将它们附加到您在 QML 脚本中声明的组件。通过在类型(`int`、`float`等)关键字之前添加`property`关键字，可以在 QML 文档的对象声明中定义对象类型的自定义属性，例如:\n\n```cpp\nproperty int myValue;\n```\n\n您也可以通过在值前使用冒号(`:`)将自定义`property`绑定到一个值，如以下代码所示:\n\n```cpp\nproperty int myValue: 100;\n```\n\nTo learn more about the property types supported by Qt Quick, check out this link:\n[http://doc.qt.io/qt-5/qtqml-typesystem-basictypes.html](http://doc.qt.io/qt-5/qtqml-typesystem-basictypes.html)\n\n# QML 的动画\n\nQt 允许我们在不编写大量代码的情况下轻松地激活用户界面组件。在这个例子中，我们将学习如何通过应用动画使我们的程序的用户界面更有趣。\n\n# 怎么做…\n\n让我们按照以下步骤学习如何将动画添加到您的 Qt Quick 应用中:\n\n1.  再一次，我们将从头开始一切。因此，在 Qt Creator 中创建新的 Qt Quick 应用–清空项目并创建`MainForm.ui.qml`文件。\n2.  打开`MainForm.ui.qml`并转到“库”窗口中的“导入”选项卡，将名为`QtQuick.Controls`的 Qt Quick 模块添加到您的项目中。\n3.  之后，您将看到一个新的类别出现在 QML 类型选项卡中，称为 Qt Quick-Controls，其中包含许多可以放置在画布上的新小部件。\n4.  接下来，将三个按钮部件拖到画布上，并将它们的高度设置为`45`。然后，转到“属性”窗口上的“布局”选项卡，为所有三个按钮小部件启用左锚和右锚。确保锚点的目标设置为“父级”，边距保持为`0`。这将使按钮根据主窗口的宽度水平调整大小。之后，将第一个按钮的 y 值设置为`0`，第二个设置为`45`，第三个设置为`90`。用户界面现在应该如下所示:\n\n![](img/366537b6-5c1d-4b33-aca2-57ae75c25636.png)\n\n5.  现在，用编辑器打开`qml.qrc`，将`fan.png`添加到项目中，如下所示:\n\n![](img/94180090-eae3-4ba7-82fb-8048b82d7a1c.png)\n\n6.  然后，在画布上添加两个鼠标区域小部件。之后，在画布上拖动一个矩形小部件和一个图像小部件。将矩形和图像放在我们之前刚刚添加的鼠标区域中。\n7.  将矩形的颜色设置为`#0000ff`，并将`fan.png`应用于图像小部件。您的用户界面现在应该如下所示:\n\n![](img/6f374f2a-2f19-4e3f-8526-108faeea8aaf.png)\n\n8.  之后，通过点击小部件名称右侧的图标，将`MainForm.ui.qml`中的所有小部件导出为根项目的别名属性，如下所示:\n\n![](img/c23619b2-9372-46e9-975c-bbcfc16e7e22.png)\n\n9.  接下来，我们将对用户界面应用动画和逻辑，但我们不会在`MainForm.ui.qml`中这样做。相反，我们将在`main.qml`中完成这一切。\n10.  在`main.qml`中，去掉鼠标区域的默认代码，加上窗口的宽度和高度，这样我们就有更多的空间可以预览，如下所示:\n\n```cpp\nimport QtQuick 2.5\nimport QtQuick.Window 2.2\nWindow {\n    visible: true\n    width: 480\n    height: 550\n    MainForm {\n        anchors.fill: parent\n    }\n}\n```\n\n11.  之后，添加以下定义`MainForm`小部件中按钮行为的代码:\n\n```cpp\nbutton1 {\n    Behavior on y { SpringAnimation { spring: 2; damping: 0.2 } }\n    onClicked: {\n        button1.y = button1.y + (45 * 3)\n    }\n}\nbutton2 {\n    Behavior on y { SpringAnimation { spring: 2; damping: 0.2 } }\n    onClicked: {\n        button2.y = button2.y + (45 * 3)\n    }\n}\n```\n\n12.  在下面的代码中，我们继续定义`button3`:\n\n```cpp\nbutton3 {\n    Behavior on y { SpringAnimation { spring: 2; damping: 0.2 } }\n    onClicked: {\n        button3.y = button3.y + (45 * 3)\n    }\n}\n```\n\n13.  然后，按照风扇图像及其所连接的鼠标区域小部件的行为进行操作，如下所示:\n\n```cpp\nfan {\n    RotationAnimation on rotation {\n        id: anim01\n        loops: Animation.Infinite\n        from: 0\n        to: -360\n        duration: 1000\n    }\n}\n```\n\n14.  在下面的代码中，我们接着定义`mouseArea1`:\n\n```cpp\nmouseArea1 {\n    onPressed: {\n        if (anim01.paused)\n            anim01.resume()\n        else\n            anim01.pause()\n    }\n}\n```\n\n15.  最后但同样重要的是，添加矩形的行为和它所连接的鼠标区域小部件，如下所示:\n\n```cpp\nrectangle2 {\n    id: rect2\n    state: \"BLUE\"\n    states: [\n        State {\n            name: \"BLUE\"\n            PropertyChanges {\n                target: rect2\n                color: \"blue\"\n            }\n        },\n```\n\n16.  在下面的代码中，我们继续添加`RED`状态:\n\n```cpp\n        State {\n            name: \"RED\"\n            PropertyChanges {\n                target: rect2\n                color: \"red\"\n            }\n        }\n    ]\n}\n```\n\n17.  然后，我们通过如下定义`mouseArea2`来完成代码:\n\n```cpp\nmouseArea2 {\n    SequentialAnimation on x {\n        loops: Animation.Infinite\n        PropertyAnimation { to: 150; duration: 1500 }\n        PropertyAnimation { to: 50; duration: 500 }\n    }\n    onClicked: {\n        if (rect2.state == \"BLUE\")\n            rect2.state = \"RED\"\n        else\n            rect2.state = \"BLUE\"\n    }\n}\n```\n\n18.  如果您现在编译并运行程序，您应该会看到窗口顶部有三个按钮，左下角有一个移动的矩形，右下角有一个旋转的风扇，如下图所示。如果你点击任何一个按钮，它们会稍微向下移动，产生一个漂亮、平滑的动画。如果点击矩形，它会从**蓝色**变成**红色**。同时，如果在动画制作过程中单击风扇图像，它将暂停动画制作，如果再次单击它，它将恢复动画制作:\n\n![](img/f767b924-ab5c-4a5c-b2d9-19c6959f8712.png)\n\n# 它是如何工作的…\n\nQt 的 C++ 版本支持的大多数动画元素，如过渡、顺序动画和并行动画，也可以在 Qt Quick 中获得。如果你熟悉 C++ 中的 Qt 动画框架，你应该能够很容易地掌握这一点。\n\n在本例中，我们向所有三个按钮添加了一个弹簧动画元素，专门跟踪它们各自的 y 轴。如果 Qt 检测到 y 值发生了变化，小部件不会立即弹出到新的位置，而是会被插值，在画布上移动，并在到达目的地时执行一个模拟弹簧效果的小抖动动画。我们只需要写一行代码，剩下的留给 Qt。\n\n至于扇形图像，我们给它添加了旋转动画元素，并将持续时间设置为`1000 milliseconds`，这意味着它将在一秒钟内完成一个完整的旋转。我们还将其设置为无限循环动画。当我们点击它所连接的鼠标区域部件时，我们只需调用`pause()`或`resume()`来启用或禁用动画。\n\n接下来，对于矩形小部件，我们向其添加了两个状态，一个称为**蓝色**，一个称为**红色**，每个状态都带有一个**颜色**属性，该属性将在状态更改时应用于矩形。同时，我们在矩形所附着的鼠标区域小部件中添加了一个**顺序动画组**，然后在该组中添加了两个**属性动画**元素。还可以混合不同类型的组动画；Qt 可以很好地处理这个问题。\n\n# 使用模型/视图显示信息\n\nQt 包括一个**模型/视图框架**，该框架将数据的组织和管理方式与数据呈现给用户的方式分开。在这一节中，我们将学习如何使用模型/视图，特别是通过使用列表视图来显示信息，同时应用我们自己的定制来使它看起来很光滑。\n\n# 怎么做…\n\n让我们从以下步骤开始:\n\n1.  创建一个新的 Qt 快速应用——清空项目，用 Qt 创建器打开`qml.qrc`。向项目添加六个图像:`home.png`、`map.png`、`profile.png`、`search.png`、`settings.png`和`arrow.png`，如下所示:\n\n![](img/db080712-c2f7-4fb2-961b-f049536d3d9e.png)\n\n2.  之后，创建并打开`MainForm.ui.qml`，就像我们在前面所有示例中所做的那样。将“库”窗口中“季度快速视图”类别下的列表视图小部件拖到画布上。然后，通过单击布局窗口中间的按钮，将其锚点设置为填充父大小，如下图所示:\n\n![](img/35b46b67-86c4-47e8-a914-568ed3cd6626.png)\n\n3.  接下来，切换到脚本编辑器，因为我们将如下定义列表视图:\n\n```cpp\nimport QtQuick 2.9\n\nRectangle {\n    id: rectangle1\n\n    property alias listView1: listView1\n    property double sizeMultiplier: width / 480\n```\n\n4.  我们将通过添加列表视图继续编写代码，如下所示:\n\n```cpp\n    ListView {\n        id: listView1\n        y: 0\n        height: 160\n        orientation: ListView.Vertical\n        boundsBehavior: Flickable.StopAtBounds\n        anchors.fill: parent\n        delegate: Item {\n            width: 80 * sizeMultiplier\n            height: 55 * sizeMultiplier\n```\n\n5.  我们将继续向列表视图添加行，如下所示:\n\n```cpp\n            Row {\n                id: row1\n                Rectangle {\n                    width: listView1.width\n                    height: 55 * sizeMultiplier\n                    gradient: Gradient {\n                        GradientStop { position: 0.0; color: \"#ffffff\" }\n                        GradientStop { position: 1.0; color: \"#f0f0f0\" }\n                    }\n                    opacity: 1.0\n```\n\n6.  然后我们添加一个鼠标区域和一个图像，如下面的代码片段所示:\n\n```cpp\n                    MouseArea {\n                        id: mouseArea\n                        anchors.fill: parent\n                    }\n                    Image {                        \n                        anchors.verticalCenter: parent.verticalCenter                        \n                        x: 15 * sizeMultiplier\n                        width: 30 * sizeMultiplier\n                        height: 30 * sizeMultiplier\n                        source: icon\n                    }\n```\n\n7.  然后，继续添加两个文本对象，如下所示:\n\n```cpp\n                    Text {\n                        text: title\n                        font.family: \"Courier\"\n                        font.pixelSize: 17 * sizeMultiplier\n                        x: 55 * sizeMultiplier\n                        y: 10 * sizeMultiplier\n                    }                    \n                    Text {\n                        text: subtitle\n                        font.family: \"Verdana\"\n                        font.pixelSize: 9 * sizeMultiplier\n                        x: 55 * sizeMultiplier\n                        y: 30 * sizeMultiplier\n                    }\n```\n\n8.  之后，添加如下图像对象:\n\n```cpp\n                    Image {\n                        anchors.verticalCenter: parent.verticalCenter\n                        x: parent.width - 35 * sizeMultiplier\n                        width: 30 * sizeMultiplier\n                        height: 30 * sizeMultiplier\n                        source: \"arrow.png\"\n                    }\n                }\n            }\n        }\n```\n\n9.  使用下面的代码，我们将定义列表模型:\n\n```cpp\n        model: ListModel {\n            ListElement {\n                title: \"Home\"\n                subtitle: \"Go back to dashboard\"\n                icon: \"home.png\"\n            }\n\n            ListElement {\n                title: \"Map\"\n                subtitle: \"Help navigate to your destination\"\n                icon: \"map.png\"\n            }\n```\n\n10.  我们将继续编写代码:\n\n```cpp\n            ListElement {\n                title: \"Profile\"\n                subtitle: \"Customize your profile picture\"\n                icon: \"profile.png\"\n            }\n\n            ListElement {\n                title: \"Search\"\n                subtitle: \"Search for nearby places\"\n                icon: \"search.png\"\n            }\n```\n\n11.  我们现在将添加最终的列表元素，如以下代码所示:\n\n```cpp\n            ListElement {\n                title: \"Settings\"\n                subtitle: \"Customize your app settings\"\n                icon: \"settings.png\"\n            }\n        }\n    }\n}\n```\n\n12.  之后，打开`main.qml`并用以下内容替换代码:\n\n```cpp\nimport QtQuick 2.9\nimport QtQuick.Window 2.3\nWindow {\n    visible: true\n    width: 480\n    height: 480\n    MainForm {\n        anchors.fill: parent\n        MouseArea {\n            onPressed: row1.opacity = 0.5\n            onReleased: row1.opacity = 1.0\n        }\n    }\n}\n```\n\n13.  构建并运行程序，现在您的程序应该如下所示:\n\n![](img/9c7e968b-57c2-47ab-9c85-7cf49c7d26a9.png)\n\n# 它是如何工作的…\n\nQt Quick 允许我们轻松地定制列表视图中每一行的外观。委托定义了每一行的外观，模型是存储将在列表视图中显示的数据的地方。\n\n在本例中，我们添加了一个每行都有渐变的背景，然后我们还在项目的每一侧添加了一个图标、一个标题、一个描述和一个鼠标区域小部件，使列表视图的每一行都可以点击。委托不是静态的，因为我们允许模型更改标题、描述和图标，使每一行看起来都是唯一的。\n\n在`main.qml`中，我们定义了鼠标区域小部件的行为，当按下时，它将把自己的不透明度值减半，当释放时，它将返回到完全不透明。由于所有其他元素，如标题和图标，都是鼠标区域小部件的子元素，它们也将自动跟随其父小部件的行为，并变成半透明的。同样，我们最终解决了高分辨率和高 DPI 移动设备上的显示问题。这是一个非常简单的技巧——首先，我们定义了一个名为`sizeMultiplier`的变量。`sizeMultiplier`的值是窗口宽度除以一个预定义值的结果，比如说 480，这是我们用于电脑的当前窗口宽度。然后将`sizeMultiplier`乘以所有与大小和位置有关的小部件变量，包括字体大小。请注意，在这种情况下，您应该将`pixelSize`属性用于文本而不是`pointSize`，这样您将在乘以`sizeMultiplier`时获得正确的显示。下面的截图向您展示了有无`sizeMultiplier`时，该应用在移动设备上的外观:\n\n![](img/cf8cabc0-ccd0-44ea-a00e-a68816bd0299.png)\n\n请注意，一旦您将所有内容乘以`sizeMultiplier`变量，您可能会在编辑器中得到一个混乱的用户界面。这是因为宽度变量在编辑器中可能会返回`0`。因此，通过将`0`乘以`480`，您可能会得到结果`0`，这使得整个用户界面看起来很有趣。然而，当运行实际程序时，它看起来会很好。如果想在编辑器上预览\n用户界面，暂时将`sizeMultiplier`设置为`1`。\n\n# 集成 QML 和 C++\n\nQt 支持用 QML 引擎桥接 C++ 类。这种组合允许开发人员利用 QML 的简单性和 C++ 的灵活性。您甚至可以从外部库中集成 Qt 不支持的功能，然后将结果数据传递给 Qt Quick 以显示在用户界面中。在这个例子中，我们将学习如何将我们的用户界面组件从 QML 导出到 C++ 框架，并在屏幕上显示它们之前操作它们的属性。\n\n# 怎么做…\n\n让我们完成以下步骤:\n\n1.  再一次，我们将从头开始一切。因此，在 Qt Creator 中创建新的 Qt Quick 应用–清空项目并创建`MainForm.ui.qml`。然后，打开`MainForm.ui.qml`。\n2.  我们可以保留鼠标区域和文本小部件，但是将文本小部件放在窗口的底部。将文本小部件的**文本**属性更改为**使用 C++ 更改此文本**，并将其**字体大小**设置为 **18** 。之后，转到布局选项卡，启用**垂直中心定位点**和**水平中心定位点**，以确保它始终位于窗口中间的某个位置，而不管您如何重新缩放窗口。\n\n将**垂直中心锚**的**边距**设置为 **120** ，如下图所示:\n\n![](img/01e6126c-e635-485f-9f12-1fc403a05c36.png)\n\n3.  接下来，将矩形部件从**库**窗口拖到画布上，并将其颜色设置为`#ff0d0d`。将其**宽度**和**高度**设置为 **200** ，并启用垂直和水平中心锚。之后，将水平中心锚的**边距**设置为 **-14** 。您的用户界面现在应该如下所示:\n\n![](img/8669aeab-d732-4b3f-be4b-8a311026aa9f.png)\n\n4.  完成后，右键单击 Qt Creator 中的项目目录，然后选择添加新项...，如下图所示。然后，会弹出一个窗口，让你选择一个文件模板。选择 C++ 类，然后按选择。之后，它会要求您通过填写类的信息来定义 C++ 类。在这种情况下，在类名字段中插入`MyClass`，选择`QObject`作为基类。然后，确保选中“包含对象”选项，您现在可以单击“下一步”按钮，然后单击“完成”按钮。现在将创建两个文件— `myclass.h`和`myclass.cpp`，并将其添加到您的项目中:\n\n![](img/2d4a1004-97cd-40e2-bd88-2bdd3a6e7518.png)\n\n5.  现在，打开`myclass.h`，在类构造函数下添加一个变量和函数，如下代码所示:\n\n```cpp\n#ifndef MYCLASS_H\n#define MYCLASS_H\n#include <QObject>\nclass MyClass : public QObject\n{\n    Q_OBJECT\n    public:\n    explicit MyClass(QObject *parent = 0);\n    // Object pointer\n    QObject* myObject;\n    // Must call Q_INVOKABLE so that this function can be used in QML\n    Q_INVOKABLE void setMyObject(QObject* obj);\n};\n#endif // MYCLASS_H\n```\n\n6.  之后，打开`myclass.cpp`并定义`setMyObject()`功能如下:\n\n```cpp\n#include \"myclass.h\"\nMyClass::MyClass(QObject *parent) : QObject(parent)\n{\n}\nvoid MyClass::setMyObject(QObject* obj)\n{\n    // Set the object pointer\n    myObject = obj;\n}\n```\n\n7.  我们现在可以关闭`myclass.cpp`和打开`main.qml`。在文件的顶部，添加第三行，它导入了我们刚刚用 C++ 创建的自定义库:\n\n```cpp\nimport QtQuick 2.9\nimport QtQuick.Window 2.3\nimport MyClassLib 1.0\n```\n\n8.  然后，在`Window`对象中定义`MyClass`，并在`MainForm`对象中调用其`setMyObject()`函数，如下代码所示:\n\n```cpp\nWindow {\n    visible: true\n    width: 480\n    height: 320\n    MyClass {\n id: myclass\n }\n    MainForm {\n        anchors.fill: parent\n        mouseArea.onClicked: {\n            Qt.quit();\n        }\n        Component.onCompleted:\n myclass.setMyObject(messageText);\n    }\n}\n```\n\n9.  最后，打开`main.cpp`并向 QML 引擎注册定制类。我们还将使用 C++ 代码更改文本小部件和矩形的属性，如下所示:\n\n```cpp\n#include <QGuiApplication>\n#include <QQmlApplicationEngine>\n#include <QtQml>\n#include <QQuickView>\n#include <QQuickItem>\n#include <QQuickView>\n#include \"myclass.h\"\nint main(int argc, char *argv[])\n{\n    // Register your class to QML\n    qmlRegisterType<MyClass>(\"MyClassLib\", 1, 0, \"MyClass\");\n```\n\n10.  然后，继续创建对象，就像下面代码中突出显示的部分一样:\n\n```cpp\n    QGuiApplication app(argc, argv);\n    QQmlApplicationEngine engine;\n    engine.load(QUrl(QStringLiteral(\"qrc:/main.qml\")));\n\n    QObject* root = engine.rootObjects().value(0);\n QObject* messageText = root->findChild<QObject*>(\"messageText\");\n messageText->setProperty(\"text\", QVariant(\"C++ is now in control!\"));\n messageText->setProperty(\"color\", QVariant(\"green\"));\n QObject* square = root->findChild<QObject*>(\"square\");\n square->setProperty(\"color\", QVariant(\"blue\")); \n    return app.exec();\n}\n```\n\n11.  现在构建并运行程序，您应该会看到矩形和文本的颜色与您之前在 Qt Quick 中定义的完全不同，如下图所示。这是因为它们的属性被 C++ 代码改变了:\n\n![](img/37c70cc3-c011-44b7-9759-a3a05be12a59.png)\n\n# 它是如何工作的…\n\nQML 被设计成可以通过 C++ 代码轻松扩展。Qt QML 模块中的类使得 QML 对象能够从 C++ 中加载和操作。\n\n只有从`QObject`基类继承的类才能与 QML 集成，因为它是 Qt 生态系统的一部分。一旦类在 QML 引擎中注册，我们就可以从 QML 引擎中获取根项目，并使用它来找到我们想要操作的对象。之后，使用`setProperty()`功能更改属于小部件的任何属性。\n\n请注意，`Q_INVOKABLE`宏在您打算在 QML 调用的函数前面是必需的。没有它，Qt 将不会向 Qt Quick 公开该函数，并且您将无法调用它。**"
  },
  {
    "path": "docs/qt5-cpp-gui-prog-cb/09.md",
    "content": "# 九、XML 解析的简化\n\nXML 是一种称为**可扩展标记语言**的文件格式的文件扩展名，用于以结构化格式存储信息。XML 格式广泛用于网络。例如，HTML 是用于创建网页的文件格式，并且基于 XML 格式。从微软 Office 2007 开始，微软 Office 使用了基于 XML 的文件格式，如`.docx`、`.xlsx`、`.pptx`。\n\n本章将涵盖以下食谱:\n\n*   使用流读取器处理 XML 数据\n*   使用流编写器编写 XML 数据\n*   使用 domdocument 类处理 xml 数据\n*   使用 domdocument 类编写 xml 数据\n*   使用谷歌的地理编码应用编程接口\n\n# 技术要求\n\n本章的技术要求包括 Qt 5.11.2 MinGW 32 位、Qt Creator 4.8.2 和 Windows 10。\n\n本章使用的所有代码可从以下 GitHub 链接下载:[https://GitHub . com/PacktPublishing/Qt5-CPP-GUI-Programming-cook book-第二版/树/主/第 09 章](https://github.com/PacktPublishing/Qt5-CPP-GUI-Programming-Cookbook-Second-Edition/tree/master/Chapter09)。\n\n查看以下视频，查看正在运行的代码:[http://bit.ly/2ToL7XU](http://bit.ly/2ToL7XU)\n\n# 使用流读取器处理 XML 数据\n\n在这个食谱中，我们将学习如何处理从一个 XML 文件中获取的数据，并使用流阅读器提取它。\n\n# 怎么做…\n\n让我们按照以下步骤创建一个读取和处理 XML 文件的简单程序:\n\n1.  在您想要的位置创建一个新的 Qt 小部件应用项目。\n2.  打开任意文本编辑器，创建一个如下所示的 XML 文件，然后保存为`scene.xml`:\n\n```cpp\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<scene>\n    <object tag=\"building\">\n        <name>Library</name>\n        <position>120.0,0.0,50.68</position>\n        <rotation>0.0,0.0,0.0</rotation>\n        <scale>1.0,1.0,1.0</scale>\n    </object>\n```\n\n3.  继续编写 XML 代码，如以下代码所示:\n\n```cpp\n    <object tag=\"building\">\n        <name>Town Hall</name>\n        <position>80.2,0.0,20.5</position>\n        <rotation>0.0,0.0,0.0</rotation>\n        <scale>1.0,1.0,1.0</scale>\n    </object>\n    <object tag=\"prop\">\n        <name>Tree</name>\n        <position>10.46,-0.2,80.2</position>\n        <rotation>0.0,0.0,0.0</rotation>\n        <scale>1.0,1.0,1.0</scale>\n    </object>\n</scene>\n```\n\n4.  回到 Qt 创建器，打开`mainwindow.h`。在脚本顶部`#include <QMainWindow>`后添加以下标题:\n\n```cpp\n#include <QXmlStreamReader>\n#include <QDebug>\n#include <QFile>\n#include <QFileDialog>\n```\n\n5.  打开`mainwindow.ui`，从左侧的小部件框中拖动一个按钮到 UI 编辑器中。将按钮的对象名称更改为`loadXmlButton`，其显示文本更改为`Load XML`:\n\n![](img/e05e028c-7347-4fc8-a77d-0c490b6c707f.png)\n\n6.  右键单击按钮并选择转到插槽。将弹出一个窗口，显示可供选择的信号列表。\n7.  选择默认的点击()选项，然后按确定按钮。Qt 现在会在你的头文件和源文件中插入一个槽函数，叫做`on_loadXmlButton_clicked()`。\n8.  将以下代码添加到`on_loadXmlButton_clicked()`功能中:\n\n```cpp\nvoid MainWindow::on_loadXmlButton_clicked() {\n    QXmlStreamReader xml;\n\n    QString filename = QFileDialog::getOpenFileName(this, \"Open Xml\", \".\", \"Xml files (*.xml)\");\n    QFile file(filename);\n    if (!file.open(QFile::ReadOnly | QFile::Text))\n        qDebug() << \"Error loading XML file.\";\n    xml.setDevice(&file);\n```\n\n9.  我们继续编写代码。下面的代码循环遍历 XML 文件，并打印出每个属性的名称和值:\n\n```cpp\n    while(!xml.atEnd()) {\n        if (xml.isStartElement()) {\n            QString name = xml.name().toString();\n\n            if (name == \"object\") {\n                qDebug() << \"[Object]=================================\";\n\n                for (int i = 0; i < xml.attributes().size(); i++) {\n                    qDebug() << xml.attributes().at(i).name() << xml.attributes().at(i).value();\n                }\n            }\n```\n\n10.  从 XML 文件中读取元素文本，如以下代码所示:\n\n```cpp\n            if (name == \"name\" || name == \"position\" || name == \"rotation\" || name == \"scale\") {\n                QString text = xml.readElementText();\n                qDebug() << name << text;\n            }\n        }\n\n        if (xml.isEndElement()) {\n            QString name = xml.name().toString();\n\n            if (name == \"object\") {\n                qDebug() << \"=========================================\";\n            }\n        }\n```\n\n11.  完成代码前检查错误信息:\n\n```cpp\n        xml.readNext();\n    }\n\n    if (xml.hasError()) {\n        qDebug() << \"Error loading XML:\" << xml.errorString();\n    }\n}\n```\n\n12.  构建并运行项目，您将看到一个弹出窗口，看起来像您在 s *步骤 4* 中创建的窗口:\n\n![](img/2564ed21-f884-4c34-8db1-8410c2ae88fc.png)\n\n13.  点击`Load XML`按钮，屏幕上会弹出文件选择窗口。选择您在第 2 步中创建的 XML 文件，然后按选择按钮。您应该会在 Qt Creator 的应用输出窗口中看到以下调试文本，这表明程序已经成功地从您刚刚选择的 XML 文件中加载了数据:\n\n![](img/81566961-b789-4d63-b523-079adafd7ae6.png)\n\n# 它是如何工作的…\n\n在这个例子中，我们试图使用`QXmlStreamReader`类从一个 XML 文件中提取和处理数据。假设你正在制作一个电脑游戏，你正在使用 XML 文件来存储游戏场景中所有对象的属性。在这种情况下，XML 格式在以结构化方式存储数据方面起着重要作用，这使得提取变得容易。\n\n我们需要将与 XML 相关的类的头部添加到我们的源文件中，在本例中是`QXmlStreamReader`类。`QXmlStreamReader`类内置在 Qt 的核心库中，所以不需要包含任何额外的模块，这也意味着它是 Qt 中处理 XML 数据的推荐类。一旦我们点击`Load XML`按钮，就会调用`on_loadXmlButton_clicked()`槽；这就是我们编写代码来处理 XML 数据的地方。\n\n我们使用文件对话框来选择我们想要处理的 XML 文件。然后，我们将所选文件的文件名及其路径发送到`QFile`类，以打开并读取 XML 文件的文本数据。之后，文件的数据被发送到`QXmlStreamReader`类进行处理。\n\n我们使用`while`循环来读取整个 XML 文件，并检查流读取器处理的每个元素。我们确定元素是开始元素还是结束元素。如果它是一个 start 元素，我们检查该元素的名称，以确定该元素是否应该包含任何我们需要的数据。\n\n然后，我们以属性或文本的形式提取数据。一个元素可能有不止一个属性，这就是为什么我们必须遍历所有属性并逐个提取它们。\n\n# 还有更多…\n\n除了网络浏览器之外，许多商业游戏引擎和交互式应用都使用 XML 格式来存储游戏场景、网格和其他产品中使用的资产形式的信息。这是因为与其他文件格式相比，XML 格式提供了许多优势，例如紧凑的文件大小、高灵活性和可扩展性、易于文件恢复以及关系树结构，使其能够用于高效且对性能至关重要的应用，如搜索引擎、智能数据挖掘服务器和科学模拟。\n\n让我们稍微了解一下 XML 文件的格式。我们将使用`scene.xml`，我们在前面的例子中使用了*使用流阅读器*处理 XML 数据的方法，如下所示:\n\n```cpp\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<scene>\n    <object tag=\"building\">\n        <name>Library</name>\n        <position>120.0,0.0,50.68</position>\n        <rotation>0.0,0.0,0.0</rotation>\n        <scale>1.0,1.0,1.0</scale>\n    </object>\n```\n\n可以有多个对象标签:\n\n```cpp\n    <object tag=\"building\">\n        <name>Town Hall</name>\n        <position>80.2,0.0,20.5</position>\n        <rotation>0.0,0.0,0.0</rotation>\n        <scale>1.0,1.0,1.0</scale>\n    </object>\n    <object tag=\"prop\">\n        <name>Tree</name>\n        <position>10.46,-0.2,80.2</position>\n        <rotation>0.0,0.0,0.0</rotation>\n        <scale>1.0,1.0,1.0</scale>\n    </object>\n</scene>\n```\n\n在 XML 中，标签是一行标记文本，以`<`符号开始，以`>`符号结束；`<scene>`是一个叫场景的标签，`<object>`是一个叫对象的标签，以此类推。标签有三种风格:\n\n*   起始标签，例如`<scene>`\n*   结束标记，例如`</scene>`\n*   空元素标签，例如`<scene />`\n\n每当您编写开始标记时，它必须以结束标记结束，否则您的 XML 数据将无效。然而，空元素标记是一个独立的标记，它后面不需要结束标记。\n\n在`scene.xml`文件的顶部，您将看到一个名为`xml`的标签，它存储了 XML 格式的版本和编码类型，在本例中是 XML 1.0 版本和 UTF-8 (8 位 Unicode)编码。这一行称为 XML 声明，它必须存在于您的一个 XML 文件中才能验证其格式。\n\n之后，您会看到其中存储了属性的标签，例如`<object tag=\"building\">`。这意味着对象标签包含一个名为标签的属性，它包含一个值，building。您可以在标签中放入任意多的属性，例如`<object tag=\"building\" color=\"red\" name=\"LA Community Hospital\" coordinate=\"34.0191757,-118.2567239\">`。这些属性中的每一个都存储了不同的数据，可以使用 Qt 轻松检索。\n\n您还可以在开始标记和结束标记之间存储数据；例如`<name>Town Hall</name>`。然而，这个方法与空元素标签无关，因为它是一个独立的标签，后面没有结束标签。因此，您只能将属性存储在空元素标记中。\n\nTo learn more about the XML format, visit [http://www.w3schools.com/xml](http://www.w3schools.com/xml).\n\n# 使用流编写器编写 XML 数据\n\n由于我们已经在前面的食谱中学习了如何处理从 XML 文件中获得的数据，我们将继续学习如何将数据保存到 XML 文件中。我们将继续前面的例子，并对其进行补充。\n\n# 怎么做…\n\n我们将通过以下步骤学习如何将数据保存到 XML 文件中:\n\n1.  在`mainwindow.ui`上增加另一个按钮，然后将其对象名设置为`saveXmlButton`，标签设置为`Save XML`:\n\n![](img/06ef8b8f-dcc3-4922-bcd1-6859f0a45c69.png)\n\n2.  右键单击按钮并选择转到插槽。将弹出一个窗口，显示可供选择的信号列表。选择单击的()选项，然后单击确定。一个名为`on_saveXmlButton_clicked()`的信号函数现在将由 Qt 自动添加到您的`mainwindow.h`和`mainwindow.cpp`文件中:\n\n![](img/a51640fb-4ff2-4ec5-b514-ea9bb7c6ad6c.png)\n\n3.  将以下代码添加到`on_saveXmlButton_clicked()`功能中:\n\n```cpp\nQXmlStreamWriter xml;\nQString filename = QFileDialog::getSaveFileName(this, \"Save Xml\", \".\", \"Xml files (*.xml)\");\nQFile file(filename);\nif (!file.open(QFile::WriteOnly | QFile::Text))\n    qDebug() << \"Error saving XML file.\";\nxml.setDevice(&file);\nxml.setAutoFormatting(true);\nxml.writeStartDocument();\n```\n\n4.  让我们也写第一个接触元素:\n\n```cpp\nxml.writeStartElement(\"contact\");\nxml.writeAttribute(\"category\", \"Friend\");\nxml.writeTextElement(\"name\", \"John Doe\");\nxml.writeTextElement(\"age\", \"32\");\nxml.writeTextElement(\"address\", \"114B, 2nd Floor, Sterling Apartment, Morrison Town\");\nxml.writeTextElement(\"phone\", \"0221743566\");\nxml.writeEndElement();\n```\n\n5.  按如下方式书写第二个接触元素:\n\n```cpp\nxml.writeStartElement(\"contact\");\nxml.writeAttribute(\"category\", \"Family\");\nxml.writeTextElement(\"name\", \"Jane Smith\");\nxml.writeTextElement(\"age\", \"24\");\nxml.writeTextElement(\"address\", \"13, Ave Park, Alexandria\");\nxml.writeTextElement(\"phone\", \"0025728396\");\nxml.writeEndElement();\nxml.writeEndDocument();\n```\n\n6.  构建并运行程序，您应该会在程序用户界面上看到一个附加按钮:\n\n![](img/3abcd6d7-7e0b-4af7-8070-52047a38a13a.png)\n\n7.  点击`Save XML`按钮，屏幕上将出现保存文件对话框。键入所需的文件名，然后单击保存按钮。\n8.  用任何文本编辑器打开刚刚保存的 XML 文件。文件的第一部分应该如下所示:\n\n```cpp\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<contact category=\"Friend\">\n    <name>John Doe</name>\n    <age>32</age>\n    <address>114B, 2nd Floor, Sterling Apartment, Morrison Town</address>\n    <phone>0221743566</phone>\n</contact>\n```\n\n9.  文件的第二部分应该如下所示:\n\n```cpp\n<contact category=\"Family\">\n    <name>Jane Smith</name>\n    <age>24</age>\n    <address>13, Ave Park, Alexandria</address>\n    <phone>0025728396</phone>\n</contact>\n```\n\n# 它是如何工作的…\n\n保存过程类似于上一个示例中的加载 XML 文件。唯一不同的是，我们没有使用`QXmlStreamReader`类，而是改用了`QXmlStreamWriter`类。我们仍然使用文件对话框和`QFile`类来保存 XML 文件。这一次，我们要先把开放模式从`QFile::ReadOnly`改成`QFile::WriteOnly`，才能通过`QFile`班转到流写手。\n\n在我们开始向新的 XML 文件中写入任何数据之前，我们必须将自动格式化设置为 true，否则将没有间距；它还在 XML 文件中添加了新的行和缩进，使其整洁且易于阅读。但是，如果你的意图是让用户更难阅读和编辑，你可以忽略`setAutoFormatting()`功能。\n\n接下来，通过调用`writeStartDocument()`开始编写 XML 文件，后面是想要保存到文件中的所有元素，最后我们调用`writeEndDocument()`函数停止编写。每个元素必须有开始和结束标签，以便阅读过程正常工作。元素的属性将存储在开始标记中，而文本数据将存储在开始标记和结束标记之间。\n\n如果我们正在编写一个包含一组子元素的元素，我们必须在编写子元素之前调用`writeStartElement()`。保存其所有子元素后调用`writeEndElement()`以结束标记关闭该组。但是`writetextElement()`功能会自动为你添加结束标签，所以你不用担心那个。可以调用`writeAttribute()`函数给元素添加属性。您可以向特定元素添加的属性数量没有限制。\n\n# 使用 domdocument 类处理 xml 数据\n\nQt 允许多种解析 XML 数据的方法，包括我们在前面的例子中介绍的通用方法。这次，我们将学习如何使用另一个名为`QDomDocument`的类从 XML 文件中读取数据。\n\n# 怎么做…\n\n使用`QDomDocument`类处理 XML 数据非常简单:\n\n1.  打开项目(`.pro`)文件，在`core`和`gui`后添加`xml`文本，将 XML 模块添加到项目中，如下代码所示:\n\n```cpp\nQT += core gui xml\n```\n\n2.  创建一个带有按钮的用户界面，上面写着`Load XML`:\n\n![](img/4de306fb-2447-4a0d-9862-11dc21fa9461.png)\n\n3.  右键单击按钮，选择转到插槽…，然后选择单击的()选项。按下确定按钮，Qt 将在你的源代码中添加一个槽函数。\n4.  转到`mainwindow.h`并添加以下标题，以便我们可以利用这些类:\n\n```cpp\n#include <QDomDocument>\n#include <QDebug>\n#include <QFile>\n#include <QFileDialog>\n```\n\n5.  转到`mainwindow.cpp`并将以下代码插入按钮的`clicked()`插槽功能:\n\n```cpp\nvoid MainWindow::on_loadXmlButton_clicked() {\n    QDomDocument xml;\n    QString filename = QFileDialog::getOpenFileName(this, \"Open Xml\", \".\", \"Xml files (*.xml)\");\n    QFile file(filename);\n    if (!file.open(QFile::ReadOnly | QFile::Text))\n        qDebug() << \"Error loading XML file.\";\n    if (!xml.setContent(&file)) {\n        qDebug() << \"Error setting content.\";\n        file.close();\n        return;\n    }\n    file.close();\n```\n\n6.  我们继续编写代码来加载 XML:\n\n```cpp\nQDomElement element = xml.documentElement();\n    QDomNode node = element.firstChild();\n    while(!node.isNull()) {\n        QDomElement nodeElement = node.toElement();\n        if(!nodeElement.isNull()) {\n            if (nodeElement.tagName() == \"object\") {\n                qDebug() << \"[Object]=================================\";\n                QDomNode childNode = nodeElement.firstChild();\n```\n\n7.  前面的代码继续如下:\n\n```cpp\n                while (!childNode.isNull()) {\n                    QDomElement childNodeElement = childNode.toElement();\n                    QString name = childNodeElement.tagName();\n                    if (name==\"name\" || name==\"position\" || name==\"rotation\" || name==\"scale\") {\n                        QString text = childNodeElement.text();\n                        qDebug() << name << text;\n                    }\n                    childNode = childNode.nextSibling();\n                }\n            }\n            qDebug() << \"=========================================\";\n        }\n        node = node.nextSibling();\n    }\n}\n```\n\n8.  编译并运行程序。点击`Load XML`按钮，选择第一个例子中使用的 XML 文件。您应该会看到以下输出:\n\n![](img/26d7ad31-e4ad-4f49-888b-cd23b01107d5.png)\n\n# 它是如何工作的…\n\n与`QXmlStreamReader`相比，`QDomDocument`类在加载或保存 XML 数据时就不那么简单了。然而，`QDomDocument`通过确保每个元素递归地链接到其各自的父元素，就像在树结构中一样，严格地做到了这一点。与`QXmlStreamReader`不同，`QDomDocument`允许我们将数据保存到更早创建的元素中，保存在更晚的时间范围内。\n\n由于`QDomDocument`不是 Qt 核心库的一部分，所以我们必须手动将 XML 模块添加到我们的项目中。否则，我们将无法访问`QDomDocument`和与其相关的其他类。\n\n首先，我们加载 XML 文件，并将其内容提取到`QDomDocument`类。然后，我们获得它的文档元素，它充当根文档，并获得它的直接子文档。然后，我们将每个子节点转换为`QDomElement`并获得它们的标签名。通过检查标签名，我们能够确定我们期望从每个元素中得到的数据类型。\n\n由于这是带有标记名对象的第一层元素，我们不期望从它们那里得到任何数据；我们再次重复 s *tep 3* ，但这一次我们将在具有标签名对象的元素上执行，并获得它的所有直接子元素，这意味着文档元素的孙子。\n\n同样，通过检查标签名，我们能够知道我们可以从其子元素中得到什么数据。如果标签名称与我们期望的匹配(在本例中是名称、位置、旋转和缩放)，我们可以通过调用`QDomElement::text()`来获取它的数据。\n\n# 使用 domdocument 类编写 xml 数据\n\n在这个例子中，我们将学习如何使用`QDomDocument`类将数据写入一个 XML 文件。我们将继续前面的例子，只是添加更多的代码。\n\n# 怎么做…\n\n要了解如何使用`QDomDocument`类将数据保存到一个 XML 文件中，让我们执行以下操作:\n\n1.  在 UI 中增加第二个按钮，叫做`Save XML`:\n\n![](img/096849a6-9e61-4999-9044-151d5c169308.png)\n\n2.  右键单击`Save XML`按钮，选择转到插槽。然后，选择单击的()选项，并单击确定。一个新的 clicked()插槽函数将被添加到您的源文件中。\n3.  在按钮的`clicked()`槽功能中写入以下代码:\n\n```cpp\nvoid MainWindow::on_saveXmlButton_clicked() {\n    QString filename = QFileDialog::getSaveFileName(this, \"Save Xml\", \".\", \"Xml files (*.xml)\");\n    QFile file(filename);\n    if (!file.open(QFile::WriteOnly | QFile::Text)) {\n        qDebug() << \"Error saving XML file.\";\n        file.close();\n        return;\n    }\n```\n\n4.  前面的代码继续:\n\n```cpp\n    QDomDocument xml(\"contact\");\n\n    // John Doe\n    QDomElement root = xml.createElement(\"contact\");\n    root.setAttribute(\"category\", \"Family\");\n    xml.appendChild(root);\n\n    QDomElement tagName = xml.createElement(\"name\");\n    root.appendChild(tagName);\n    QDomText textName = xml.createTextNode(\"John Doe\");\n    tagName.appendChild(textName);\n```\n\n5.  保存`age`和`address`元素:\n\n```cpp\n    QDomElement tagAge = xml.createElement(\"age\");\n    root.appendChild(tagAge);\n    QDomText textAge = xml.createTextNode(\"32\");\n    tagAge.appendChild(textAge);\n\n    QDomElement tagAddress = xml.createElement(\"address\");\n    root.appendChild(tagAddress);\n    QDomText textAddress = xml.createTextNode(\"114B, 2nd Floor, Sterling Apartment, Morrisontown\");\n    tagAddress.appendChild(textAddress);\n```\n\n6.  让我们继续`phone`元素。继续下一个联系人:\n\n```cpp\n    QDomElement tagPhone = xml.createElement(\"phone\");\n    root.appendChild(tagPhone);\n    QDomText textPhone = xml.createTextNode(\"0221743566\");\n    tagPhone.appendChild(textPhone);\n\n    // Jane Smith\n    QDomElement root2 = xml.createElement(\"contact\");\n    root2.setAttribute(\"category\", \"Friend\");\n    xml.appendChild(root2);\n```\n\n7.  保存`name`和`age`:\n\n```cpp\n    QDomElement tagName2 = xml.createElement(\"name\");\n    root2.appendChild(tagName2);\n    QDomText textName2 = xml.createTextNode(\"Jane Smith\");\n    tagName2.appendChild(textName2);\n\n    QDomElement tagAge2 = xml.createElement(\"age\");\n    root2.appendChild(tagAge2);\n    QDomText textAge2 = xml.createTextNode(\"24\");\n    tagAge2.appendChild(textAge2);\n```\n\n8.  保存`address`和`phone`:\n\n```cpp\n    QDomElement tagAddress2 = xml.createElement(\"address\");\n    root2.appendChild(tagAddress2);\n    QDomText textAddress2 = xml.createTextNode(\"13, Ave Park, Alexandria\");\n    tagAddress2.appendChild(textAddress2);\n\n    QDomElement tagPhone2 = xml.createElement(\"phone\");\n    root2.appendChild(tagPhone2);\n    QDomText textPhone2 = xml.createTextNode(\"0025728396\");\n    tagPhone2.appendChild(textPhone2);\n```\n\n9.  将所有数据流式传输到 XML 文件中:\n\n```cpp\n    // Save to file\n    QTextStream output(&file);\n    output << xml.toString();\n    file.close();\n}\n```\n\n10.  编译运行程序，点击`Save XML`按钮。在保存文件对话框中输入所需的文件名，然后单击保存。\n11.  用任何文本编辑器打开保存在 s *步骤 4* 中的 XML 文件，您应该会看到如下内容:\n\n```cpp\n<!DOCTYPE contact>\n<contact category=\"Family\">\n    <name>John Doe</name>\n    <age>32</age>\n    <address>114B, 2nd Floor, Sterling Apartment, Morrisontown</address>\n    <phone>0221743566</phone>\n</contact>\n```\n\n12.  我们还可以看到它下面的第二个触点:\n\n```cpp\n<contact category=\"Friend\">\n    <name>Jane Smith</name>\n    <age>24</age>\n    <address>13, Ave Park, Alexandria</address>\n    <phone>0025728396</phone>\n</contact>\n```\n\n# 它是如何工作的…\n\n与前面的例子类似，我们首先启动文件对话框并声明一个`QDomDocument`对象。然后，我们通过调用`QDomDocument::createElement()`来创建根元素。从`QDomDocument`创建的任何元素都不会自动成为它的直接子元素，除非我们将新创建的元素追加为它的子元素。\n\n要创建`QDomDocument`的子元素，只需将新创建的元素追加到根元素中。通过利用`append()`功能，我们可以很容易地将 XML 数据排列成树形结构，而不用绕着它打转。这在我看来就是用`QDomDocument`代替`QXmlStreamReader`的好处。\n\n然后我们可以通过调用`QDomElement::setAttribute()`给元素添加属性。我们还可以通过调用`QDomDocument::createTextNode()`并将其附加到 XML 结构中的任何元素来创建一个文本节点。在我们完成了 XML 数据的结构化之后，我们可以将所有数据以文本的形式输出到`QTextStream`类，并允许它将数据保存到一个文件中。\n\n# 使用谷歌的地理编码应用编程接口\n\n在本例中，我们将学习如何使用谷歌的**地理编码 API** 获取特定位置的完整地址。\n\n# 怎么做…\n\n让我们按照以下步骤创建一个利用地理编码应用编程接口的程序:\n\n1.  创建一个新的 Qt 小部件应用项目。\n2.  打开`mainwindow.ui`并添加几个文本标签、输入字段和一个按钮，使您的用户界面看起来像这样:\n\n![](img/fa9de3ba-f86c-419a-8692-73822fd347d4.png)\n\n3.  打开您的项目(`.pro`)文件，并将网络模块添加到您的项目中。您只需在`core`和`gui`后添加`network`文本即可，如下代码所示:\n\n```cpp\nQT += core gui network\n```\n\n4.  打开`mainwindow.h`并在源代码中添加以下标题:\n\n```cpp\n#include <QMainWindow>:\n#include <QDebug>\n#include <QtNetwork/QNetworkAccessManager>\n#include <QtNetwork/QNetworkReply>\n#include <QXmlStreamReader>\n```\n\n5.  手动声明一个槽函数并调用它`getAddressFinished()`:\n\n```cpp\nprivate slots:\n    void getAddressFinished(QNetworkReply* reply);\n```\n\n6.  声明一个名为`addressRequest`的私有变量:\n\n```cpp\nprivate:\n    QNetworkAccessManager* addressRequest;\n```\n\n7.  再次打开`mainwindow.ui`，右键点击获取地址按钮，选择转到插槽。然后选择单击的()选项，并按确定。插槽功能现在将被添加到`mainwindow.h`和`mainwindow.cpp`源文件中。\n8.  打开`mainwindow.cpp`，将以下代码添加到类构造函数中:\n\n```cpp\nMainWindow::MainWindow(QWidget *parent) :\n QMainWindow(parent),\n ui(new Ui::MainWindow) {\n    ui->setupUi(this);\n addressRequest = new QNetworkAccessManager();\n connect(addressRequest, &QNetworkAccessManager::finished, this, &MainWindow::getAddressFinished);\n}\n```\n\n9.  将以下代码添加到我们刚刚手动声明的`getAddressFinished()`槽函数中:\n\n```cpp\nvoid MainWindow::getAddressFinished(QNetworkReply* reply) {\n    QByteArray bytes = reply->readAll();\n    //qDebug() << QString::fromUtf8(bytes.data(),\n    bytes.size());\n    QXmlStreamReader xml;\n    xml.addData(bytes);\n```\n\n10.  继续遍历 XML 文件，获取`text`元素:\n\n```cpp\n    while(!xml.atEnd()) {\n        if (xml.isStartElement()) {\n            QString name = xml.name().toString();\n            //qDebug() << name;\n            if (name == \"formatted_address\") {\n                QString text = xml.readElementText();\n                qDebug() << \"Address:\" << text;\n                return;\n            }\n        }\n        xml.readNext();\n    }\n```\n\n11.  检查任何错误文本:\n\n```cpp\n    if (xml.hasError()) {\n        qDebug() << \"Error loading XML:\" <<\n        xml.errorString();\n        return;\n    }\n    qDebug() << \"No result.\";\n}\n```\n\n12.  将以下代码添加到 Qt 创建的`clicked()`槽函数中:\n\n```cpp\nvoid MainWindow::on_getAddressButton_clicked() {\n    QString latitude = ui->latitude->text();\n    QString longitude = ui->longitude->text();\n    QNetworkRequest request;\n    request.setUrl(QUrl(\"http://maps.googleapis.com/maps/api/geocode/xml?latlng=\" + latitude + \",\" + longitude + \"&key=AIzaSyBhKayXIr2zgMW2olsxtuZ7x2QWyLo1itQ\"));\n    addressRequest->get(request);\n}\n```\n\n13.  构建并运行程序，您应该能够通过插入经度和纬度值并单击获取地址按钮来获取地址:\n\n![](img/5276a47d-8b8f-4b12-b264-57e658680984.png)\n\n14.  让我们试试经度`-73.9780838`和纬度`40.6712957`。单击获取地址按钮，您将在应用输出窗口中看到以下结果:\n\n```cpp\nAddress: \"180-190 7th Ave, Brooklyn, NY 11215, USA\"\n```\n\n# 它是如何工作的…\n\n我无法确切告诉你谷歌是如何从其后端系统获取地址的，但我可以教你如何使用`QNetworkRequest`向谷歌请求数据。您所需要做的就是将网络请求的网址设置为我在前面的源代码中使用的网址，并将纬度和经度信息都附加到该网址上。\n\n之后，我们能做的就是等待谷歌 API 服务器的响应。我们需要在向谷歌发送请求时指定 XML 作为所需的格式；否则，它可能会以 JSON 格式返回结果。这可以通过在网络请求 URL 中添加`xml`关键字来实现，如下图所示:\n\n```cpp\nrequest.setUrl(QUrl(\"http://maps.googleapis.com/maps/api/geocode/xml?keylatlng=\" + latitude + \",\" + longitude + \"&key=AIzaSyBhKayXIr2zgMW2olsxtuZ7x2QWyLo1itQ\"));\n```\n\n当程序收到谷歌的响应时，将调用`getAddressFinished()`槽函数，我们将能够通过`QNetworkReply`获取谷歌发送的数据。\n\n谷歌通常以 XML 格式的长文本回复，其中包含大量我们不需要的数据。我们使用`QXmlStreamReader`类来解析数据，因为在这种情况下，我们不必关心 XML 结构的父子关系。\n\n我们需要的只是存储在 XML 数据的`formatted_address`元素中的文本。由于`formatted_address`这个名字的元素不止一个，我们只需要找到第一个，忽略其余的。您也可以通过向谷歌提供地址并从其网络响应中获取位置坐标来实现相反的操作。\n\n# 还有更多…\n\n谷歌地理编码应用编程接口是谷歌地图应用编程接口网络服务的一部分，为您的地图应用提供地理数据。除了地理编码应用编程接口，您还可以使用它们的**位置应用编程接口**、**地理定位应用编程接口**和**时区应用编程接口**来实现您想要的结果。\n\nFor more information regarding the Google Maps APIs web services, visit\n[https://developers.google.com/maps/web-services](https://developers.google.com/maps/web-services)."
  },
  {
    "path": "docs/qt5-cpp-gui-prog-cb/10.md",
    "content": "# 十、转换库\n\n本章将涵盖以下食谱:\n\n*   转换数据\n*   转换图像\n*   转换视频\n*   兑换货币\n\n# 介绍\n\n保存在计算机环境中的数据以多种方式编码。有时，它可以直接用于某个目的，其他时候，它需要转换成另一种格式，以适应任务的上下文。将数据从一种格式转换为另一种格式的过程也因源格式和目标格式而异。\n\n有时，该过程可能非常复杂，尤其是在处理特征丰富且敏感的数据时，例如图像或视频转换。即使是转换过程中的一个小错误也可能导致文件不可用。\n\n# 技术要求\n\n本章的技术要求包括 Qt 5.11.2 MinGW 32 位、Qt Creator 4.8.2 和 Windows 10。\n\n本章使用的所有代码可从以下 GitHub 资源库下载，网址为[https://GitHub . com/PacktPublishing/Qt5-CPP-GUI-Programming-cook book-第二版/tree/master/Chapter10](https://github.com/PacktPublishing/Qt5-CPP-GUI-Programming-Cookbook-Second-Edition/tree/master/Chapter10) 。\n\n查看以下视频，查看正在运行的代码:[http://bit.ly/2ToKfCC](http://bit.ly/2ToKfCC)\n\n# 转换数据\n\nQt 提供了一组用于在不同类型的数据之间轻松转换的类和函数。这使得 Qt 不仅仅是一个 GUI 库；它是一个完整的软件开发平台。与 C++ 标准库提供的类似转换功能相比，我们将在下面的示例中使用的`QVariant`类使 Qt 更加灵活和强大。\n\n# 怎么做…\n\n让我们按照以下步骤学习如何在 Qt 中转换各种数据类型:\n\n1.  打开 Qt 创建器，转到文件|新文件或项目，创建一个新的 Qt 控制台应用项目:\n\n![](img/eac5111d-7d6f-4570-873d-39a1ff71f770.png)\n\n2.  打开`main.cpp`并添加以下标题:\n\n```cpp\n#include <QCoreApplication>\n#include <QDebug>\n#include <QtMath>\n#include <QDateTime>\n#include <QTextCodec>\n#include <iostream>\n```\n\n3.  在`main()`函数中，添加以下代码将字符串转换为数字:\n\n```cpp\nint numberA = 2;\nQString numberB = \"5\";\nqDebug() << \"1) \" << \"2 + 5 =\" << numberA + numberB.toInt();\n```\n\n4.  将数字转换回字符串:\n\n```cpp\nfloat numberC = 10.25;\nfloat numberD = 2;\nQString result = QString::number(numberC * numberD);\nqDebug() << \"2) \" << \"10.25 * 2 =\" << result;\n```\n\n5.  让我们看看如何使用`qFloor()`向下舍入一个值:\n\n```cpp\nfloat numberE = 10.3;\nfloat numberF = qFloor(numberE);\nqDebug() << \"3) \" << \"Floor of 10.3 is\" << numberF;\n```\n\n6.  使用`qCeil()`，我们能够将一个数舍入到不小于其初始值的最小整数值:\n\n```cpp\nfloat numberG = 10.3;\nfloat numberH = qCeil(numberG);\nqDebug() << \"4) \" << \"Ceil of 10.3 is\" << numberH;\n```\n\n7.  通过从以字符串格式写入的日期时间数据转换来创建日期时间变量:\n\n```cpp\nQString dateTimeAString = \"2016-05-04 12:24:00\";\nQDateTime dateTimeA = QDateTime::fromString(dateTimeAString, \"yyyy-MM-dd hh:mm:ss\");\nqDebug() << \"5) \" << dateTimeA;\n```\n\n8.  使用我们的自定义格式将日期时间变量转换回字符串:\n\n```cpp\nQDateTime dateTimeB = QDateTime::currentDateTime();\nQString dateTimeBString = dateTimeB.toString(\"dd/MM/yy hh:mm\");\nqDebug() << \"6) \" << dateTimeBString;\n```\n\n9.  调用`QString::toUpper()`函数将字符串变量转换成所有大写字母:\n\n```cpp\nQString hello1 = \"hello world!\";\nqDebug() << \"7) \" << hello1.toUpper();\n```\n\n10.  调用`QString::toLower()`将字符串完全转换为小写:\n\n```cpp\nQString hello2 = \"HELLO WORLD!\";\nqDebug() << \"8) \" << hello2.toLower();\n```\n\n11.  Qt 提供的`QVariant`类是一种非常强大的数据类型，无需程序员付出任何努力就可以轻松转换为其他类型:\n\n```cpp\nQVariant aNumber = QVariant(3.14159);\ndouble aResult = 12.5 * aNumber.toDouble();\nqDebug() << \"9) 12.5 * 3.14159 =\" << aResult;\n```\n\n12.  这展示了一个单独的`QVariant`变量是如何被同时转换成多个数据类型的，而无需程序员付出任何努力:\n\n```cpp\nqDebug() << \"10) \";\nQVariant myData = QVariant(10);\nqDebug() << myData;\nmyData = myData.toFloat() / 2.135;\nqDebug() << myData;\nmyData = true;\nqDebug() << myData;\nmyData = QDateTime::currentDateTime();\nqDebug() << myData;\nmyData = \"Good bye!\";\nqDebug() << myData;\n```\n\n13.  `main.cpp`中的完整源代码现在将如下所示:\n\n```cpp\n#include <QCoreApplication>\n#include <QDebug>\n#include <QtMath>\n#include <QDateTime>\n#include <QTextCodec>\n#include <iostream>\n\nint main(int argc, char *argv[]) {\n    QCoreApplication a(argc, argv);\n```\n\n14.  之后，让我们添加代码，将字符串转换为数字，反之亦然:\n\n```cpp\n    // String to number\n    int numberA = 2;\n    QString numberB = \"5\";\n    qDebug() << \"1) \" << \"2 + 5 =\" << numberA + numberB.toInt();\n\n    // Number to string\n    float numberC = 10.25;\n    float numberD = 2;\n    QString result = QString::number(numberC * numberD);\n    qDebug() << \"2) \" << \"10.25 * 2 =\" << result;\n```\n\n15.  编写代码将浮点数分别转换为它们最近的后继整数或前一整数:\n\n```cpp\n    // Floor\n    float numberE = 10.3;\n    float numberF = qFloor(numberE);\n    qDebug() << \"3) \" << \"Floor of 10.3 is\" << numberF;\n\n    // Ceil\n    float numberG = 10.3;\n    float numberH = qCeil(numberG);\n    qDebug() << \"4) \" << \"Ceil of 10.3 is\" << numberH;\n```\n\n16.  将字符串转换为日期时间格式，反之亦然:\n\n```cpp\n    // Date time from string\n    QString dateTimeAString = \"2016-05-04 12:24:00\";\n    QDateTime dateTimeA = QDateTime::fromString(dateTimeAString, \"yyyy-MM-dd hh:mm:ss\");\n    qDebug() << \"5) \" << dateTimeA;\n\n    // Date time to string\n    QDateTime dateTimeB = QDateTime::currentDateTime();\n    QString dateTimeBString = dateTimeB.toString(\"dd/MM/yy hh:mm\");\n    qDebug() << \"6) \" << dateTimeBString;\n```\n\n17.  继续添加代码以将字符串转换为大写或小写字符:\n\n```cpp\n    // String to all uppercase\n    QString hello1 = \"hello world!\";\n    qDebug() << \"7) \" << hello1.toUpper();\n\n    // String to all lowercase\n    QString hello2 = \"HELLO WORLD!\";\n    qDebug() << \"8) \" << hello2.toLower();\n```\n\n18.  将`QVariant`数据类型转换为其他类型:\n\n```cpp\n    // QVariant to double\n    QVariant aNumber = QVariant(3.14159);\n    double aResult = 12.5 * aNumber.toDouble();\n    qDebug() << \"9) 12.5 * 3.14159 =\" << aResult;\n\n    // QVariant different types\n    qDebug() << \"10) \";\n    QVariant myData = QVariant(10);\n    qDebug() << myData;\n    myData = myData.toFloat() / 2.135;\n    qDebug() << myData;\n    myData = true;\n    qDebug() << myData;\n```\n\n19.  将`QVariant`数据类型转换为`QDateTime`和`QString`:\n\n```cpp\n    myData = QDateTime::currentDateTime();\n    qDebug() << myData;\n    myData = \"Good bye!\";\n    qDebug() << myData;\n\n    return a.exec();\n}\n```\n\n20.  编译并运行项目，您应该会看到如下内容:\n\n![](img/252ea42c-9ddc-4a58-82d5-b93bd5f3776f.png)\n\n# 它是如何工作的...\n\nQt 提供的所有数据类型，如`QString`、`QDateTime`和`QVariant`，都包含一些函数，使得转换成其他类型变得简单明了。Qt 还提供了自己的对象转换功能，`qobject_cast()`，不依赖标准库。它还与 Qt 更兼容，并且可以很好地在 Qt 的小部件类型和数据类型之间进行转换。\n\nQt 还为您提供了`QtMath`类，它可以帮助您操作数字变量，例如将浮点数舍入或将角度从度数转换为弧度。`QVariant`是一个特殊的类，可以用来存储各种类型的数据，比如`int`、`float`、`char`、`string`等等。它可以通过检查变量中存储的值来自动确定数据类型。只需调用单个函数，如`toFloat()`、`toInt()`、`toBool()`、`toChar()`或`toString()`，也可以轻松地将数据转换为`QVariant`类支持的任何类型。\n\n# 还有更多…\n\n请注意，这些转换都需要计算能力。尽管现代计算机处理这些操作的速度非常快，但你应该注意不要一下子大量过量。如果要转换大量变量进行复杂计算，可能会显著降低计算机速度，因此请仅在必要时尝试转换变量。\n\n# 转换图像\n\n在本节中，我们将学习如何构建一个简单的图像转换器，将图像从一种格式转换为另一种格式。Qt 支持读写不同类型的图像格式，由于许可问题，这种支持以外部 DLL 文件的形式出现。但是，您不必担心这一点，因为只要您在项目中包含这些 DLL 文件，它就可以跨不同格式无缝工作。有些格式只支持读而不支持写，有些则两者都支持。\n\nYou can check out the full details at [http://doc.qt.io/qt-5/qtimageformats-index.html](http://doc.qt.io/qt-5/qtimageformats-index.html).\n\n# 怎么做…\n\nQt 内置的图像库让图像转换变得非常简单:\n\n1.  打开 Qt 创建器，创建一个新的 Qt 小部件应用项目。\n2.  打开`mainwindow.ui`并在画布上添加一行编辑和按钮选择图像文件，一个组合框选择所需的文件格式，另一个按钮开始转换过程:\n\n![](img/7a87f11b-b826-44c8-8f49-bb4b58ead3fd.png)\n\n3.  双击组合框，将出现一个窗口，您可以在其中编辑组合框。我们将通过点击+按钮三次并将项目`PNG`、`JPEG`和`BMP`重命名来添加三个项目到组合框列表中:\n\n![](img/9e6f2092-3dd4-4c74-afbe-543af56e873b.png)\n\n4.  右键单击其中一个按钮并选择转到插槽…，然后单击确定按钮。插槽功能将自动添加到您的源文件中。对另一个按钮也重复此步骤:\n\n![](img/e3a9460b-28b6-4037-a049-31edbdbb6d00.png)\n\n5.  让我们转到源代码。打开`mainwindow.h`并添加如下标题:\n\n```cpp\n#include <QMainWindow>\n#include <QFileDialog>\n#include <QMessageBox>\n#include <QDebug>\n```\n\n6.  打开`mainwindow.cpp`并定义当点击浏览按钮时会发生什么，这种情况下是打开文件对话框选择一个图像文件:\n\n```cpp\nvoid MainWindow::on_browseButton_clicked() {\n    QString fileName = QFileDialog::getOpenFileName(this, \"Open Image\", \"\", \"Image Files (*.png *.jpg *.bmp)\");\n    ui->filePath->setText(fileName);\n}\n```\n\n7.  定义单击“转换”按钮时会发生什么:\n\n```cpp\nvoid MainWindow::on_convertButton_clicked() {\n    QString fileName = ui->filePath->text();\n    if (fileName != \"\") {\n        QFileInfo fileInfo = QFile(fileName);\n        QString newFileName = fileInfo.path() + \"/\" + fileInfo.completeBaseName();\n        QImage image = QImage(ui->filePath->text());\n        if (!image.isNull()) {\n```\n\n8.  检查正在使用的格式:\n\n```cpp\n            // 0 = PNG, 1 = JPG, 2 = BMP\n            int format = ui->fileFormat->currentIndex();\n            if (format == 0) {\n                newFileName += \".png\";\n            }\n            else if (format == 1) {\n                newFileName += \".jpg\";\n            }\n            else if (format == 2) {\n                newFileName += \".bmp\";\n            }\n```\n\n9.  检查图像是否已转换:\n\n```cpp\n            qDebug() << newFileName << format;\n            if (image.save(newFileName, 0, -1)) {\n                QMessageBox::information(this, \"Success\", \"Image successfully converted.\");\n            }\n            else {\n                QMessageBox::warning(this, \"Failed\", \"Failed to convert image.\");\n            }\n        }\n```\n\n10.  显示消息框:\n\n```cpp\n        else {\n            QMessageBox::warning(this, \"Failed\", \"Failed to open image file.\");\n        }\n    }\n    else {\n        QMessageBox::warning(this, \"Failed\", \"No file is selected.\");\n    }\n}\n```\n\n11.  现在构建并运行程序，我们应该得到一个非常简单的图像转换器，如下所示:\n\n![](img/ac4c09fc-4f30-4d2d-b158-799edddcb2b3.png)\n\n# 它是如何工作的...\n\n前面的例子使用了来自 Qt 的原生`QImage`类，该类包含可以访问像素数据并对其进行操作的函数。它还用于加载图像文件，并根据图像的格式通过不同的解压缩方法提取其数据。提取数据后，您可以对其进行任何操作，例如在屏幕上显示图像、操纵其颜色信息、调整图像大小或以其他格式压缩图像并将其保存为文件。\n\n我们使用`QFileInfo`将文件名和扩展名分开，这样我们就可以用用户从组合框中选择的新格式修改扩展名。这样，我们可以将新转换的图像保存在与原始图像相同的文件夹中，并自动赋予它相同的文件名，除非是不同的格式。\n\n只要你想把图像转换成 Qt 支持的格式，只需要调用`QImage::save()`。在内部，Qt 将为您找出剩下的部分，并将图像输出为所选的格式。在`QImage::save()`功能中，有一个参数设置图像质量，另一个参数设置格式。在这个例子中，我们只是将两者都设置为默认值，以最高质量保存图像，并让 Qt 通过检查输出文件名中声明的扩展名来确定格式。\n\n# 还有更多…\n\n您也可以使用 Qt 提供的`QPdfWriter`类将图像转换为 PDF。本质上，您可以将选定的图像绘制到新创建的 PDF 文档的布局中，并相应地设置其分辨率。\n\nFor more information about the `QPdfWriter` class, visit [http://doc.qt.io/qt-5/qpdfwriter.html](http://doc.qt.io/qt-5/qpdfwriter.html).\n\n# 转换视频\n\n在这个食谱中，我们将使用 Qt 和 FFmpeg 创建一个简单的视频转换器，这是一个领先的免费开源多媒体框架。虽然 Qt 支持通过其小部件播放视频文件，但目前不支持视频转换。不要害怕！通过 Qt 提供的`QProcess`类，让你的程序与另一个独立程序合作，你仍然可以达到同样的目的。\n\n# 怎么做…\n\n让我们按照以下步骤制作一个简单的视频转换器:\n\n1.  从[http://ffmpeg.zeranoe.com/builds](http://ffmpeg.zeranoe.com/builds)下载 FFmpeg(静态包)，将内容解压到`C:/FFmpeg/`。\n2.  打开 Qt 创建器，并通过转到文件|新文件或项目来创建新的 Qt 小部件应用项目。\n3.  打开`mainwindow.ui`—我们将开始程序的用户界面。它的用户界面与前面的例子非常相似，只是我们在画布上添加了一个额外的文本编辑小部件，就在组合框下面:\n\n![](img/be74039a-eabf-4e2a-9d05-412f0bbf8d9f.png)\n\n4.  双击组合框，将出现一个窗口来编辑组合框。我们将通过单击+按钮三次向组合框列表中添加三个项目，并将项目`AVI`、`MP4`和`MOV`重命名为:\n\n![](img/9698e1ab-f94c-4f98-87dd-fd3e832cf910.png)\n\n5.  右键单击其中一个按钮并选择转到插槽…，然后单击确定按钮。一个插槽功能将自动添加到您的源文件。对另一个按钮也重复此步骤。\n6.  打开`mainwindow.h`并在顶部添加以下标题:\n\n```cpp\n#include <QMainWindow>\n#include <QFileDialog>\n#include <QProcess>\n#include <QMessageBox>\n#include <QScrollBar>\n#include <QDebug>\n```\n\n7.  在`public`关键字下添加以下指针:\n\n```cpp\npublic:\n    explicit MainWindow(QWidget *parent = 0);\n    ~MainWindow();\n    QProcess* process;\n    QString outputText;\n    QString fileName;\n    QString outputFileName;\n```\n\n8.  在 Qt 之前在*转换图像*配方中为我们创建的两个函数下增加三个额外的槽函数:\n\n```cpp\nprivate slots:\n    void on_browseButton_clicked();\n    void on_convertButton_clicked();\n    void processStarted();\n    void readyReadStandardOutput();\n    void processFinished();\n```\n\n9.  打开`mainwindow.cpp`，将以下代码添加到类构造函数中:\n\n```cpp\nMainWindow::MainWindow(QWidget *parent) :\nQMainWindow(parent), ui(new Ui::MainWindow) {\n    ui->setupUi(this);\n    process = new QProcess(this);\n    connect(process, QProcess::started, this, MainWindow::processStarted);\n    connect(process, QProcess::readyReadStandardOutput, this, MainWindow::readyReadStandardOutput);\n    connect(process, QProcess::finished, this,\n    MainWindow::processFinished);\n}\n```\n\n10.  定义单击“浏览”按钮时会发生什么，这种情况下会打开文件对话框，允许我们选择视频文件:\n\n```cpp\nvoid MainWindow::on_browseButton_clicked() {\n    QString fileName = QFileDialog::getOpenFileName(this, \"Open Video\", \"\", \"Video Files (*.avi *.mp4 *.mov)\");\n    ui->filePath->setText(fileName);\n}\n```\n\n11.  定义如果单击“转换”按钮会发生什么。这里，我们将文件名和参数传递给 FFmpeg，然后转换过程将由 FFmpeg 在外部处理:\n\n```cpp\nvoid MainWindow::on_convertButton_clicked() {\n    QString ffmpeg = \"C:/FFmpeg/bin/ffmpeg\";\n    QStringList arguments;\n    fileName = ui->filePath->text();\n    if (fileName != \"\") {\n        QFileInfo fileInfo = QFile(fileName);\n        outputFileName = fileInfo.path() + \"/\" + fileInfo.completeBaseName();\n```\n\n12.  检查文件的格式；具体来说，无论是`.avi`、`.mp4`，还是`.mov`:\n\n```cpp\n        if (QFile::exists(fileName)) {\n            int format = ui->fileFormat->currentIndex();\n            if (format == 0) {\n                outputFileName += \".avi\"; // AVI\n            }\n            else if (format == 1) {\n                outputFileName += \".mp4\"; // MP4\n            }\n            else if (format == 2) {\n                outputFileName += \".mov\"; // MOV\n            }\n```\n\n13.  使用以下代码开始转换:\n\n```cpp\n            qDebug() << outputFileName << format;\n            arguments << \"-i\" << fileName << outputFileName;\n            qDebug() << arguments;\n            process->setProcessChannelMode(QProcess::MergedChannels);\n            process->start(ffmpeg, arguments);\n        }\n```\n\n14.  显示消息框:\n\n```cpp\n        else {\n            QMessageBox::warning(this, \"Failed\", \"Failed to open video file.\");\n        }\n    }\n    else {\n        QMessageBox::warning(this, \"Failed\", \"No file is selected.\");\n    }\n}\n```\n\n15.  告诉程序转换过程开始后该做什么:\n\n```cpp\nvoid MainWindow::processStarted() {\n    qDebug() << \"Process started.\";\n    ui->browseButton->setEnabled(false);\n    ui->fileFormat->setEditable(false);\n    ui->convertButton->setEnabled(false);\n}\n```\n\n16.  写入每当 FFmpeg 向程序返回输出时在转换过程中调用的槽函数:\n\n```cpp\nvoid MainWindow::readyReadStandardOutput() {\n    outputText += process->readAllStandardOutput();\n    ui->outputDisplay->setText(outputText);\n    ui->outputDisplay->verticalScrollBar()->setSliderPosition(ui->outputDisplay->verticalScrollBar()->maximum());\n}\n```\n\n17.  定义整个转换过程完成后调用的 slot 函数:\n\n```cpp\nvoid MainWindow::processFinished() {\n    qDebug() << \"Process finished.\";\n    if (QFile::exists(outputFileName)) {\n        QMessageBox::information(this, \"Success\", \"Video successfully converted.\");\n    }\n    else {\n        QMessageBox::information(this, \"Failed\", \"Failed to convert video.\");\n    }\n    ui->browseButton->setEnabled(true);\n    ui->fileFormat->setEditable(true);\n    ui->convertButton->setEnabled(true);\n}\n```\n\n18.  构建并运行项目，您应该会得到一个简单但可行的视频转换器:\n\n![](img/afec0b79-b48c-4563-ab02-83dae201fd20.png)\n\n# 它是如何工作的...\n\nQt 提供的`QProcess`类用于启动外部程序并与之通信。在这种情况下，我们启动了位于`C:/FFmpeg/bin/`的`ffmpeg.exe`，作为一个流程，并开始与之沟通。我们还向它发送了一组参数，告诉它启动时要做什么。我们在这个例子中使用的参数是相对基本的:我们只告诉 FFmpeg 源图像的路径和输出文件名。\n\nFor more information regarding the argument settings available in FFmpeg, check out: [https://www.ffmpeg.org/ffmpeg.html](https://www.ffmpeg.org/ffmpeg.html).\n\nFFmpeg 不仅仅是转换视频文件。您也可以使用它来转换音频文件和图像。\n\nFor more information regarding all the formats supported by FFmpeg, check out: \n[https://www.ffmpeg.org/general.html#File-Formats](https://www.ffmpeg.org/general.html#File-Formats).\n\n除此之外，您还可以通过运行位于`C:/FFmpeg/bin`的`ffplay.exe`来播放视频或音频文件，或者通过运行`ffprobe.exe`以人类可读的方式打印出视频或音频文件的信息。\n\nCheck out FFmpeg's full documentation at [https://www.ffmpeg.org/about.html](https://www.ffmpeg.org/about.html).\n\n# 还有更多…\n\n使用这种方法你可以做很多事情。你不局限于 Qt 提供的东西，你可以通过仔细选择一个能提供你需要的东西的第三方程序来打破这样的限制。一个这样的例子是通过利用市场上可用的仅命令行防病毒扫描仪来制作您自己的防病毒图形用户界面，例如 **Avira ScanCL** 、**熊猫防病毒命令行扫描仪**、 **SAV32CLI** 和 **ClamAV** 。您可以使用 Qt 构建自己的图形用户界面，并向防病毒过程发送命令来告诉它该做什么。\n\n# 兑换货币\n\n在本例中，我们将学习如何在名为 **Fixer.io** 的外部服务提供商的帮助下，使用 Qt 创建一个简单的货币转换器。\n\n# 怎么做…\n\n通过以下简单步骤，让自己成为货币兑换商:\n\n1.  打开 Qt 创建器，从文件|新文件或项目中创建新的 Qt 小部件应用项目。\n2.  打开项目文件(`.pro`)并将`network`模块添加到我们的项目中:\n\n```cpp\nQT += core gui network\n```\n\n3.  打开`mainwindow.ui`，从 UI 中移除菜单栏、工具栏和状态栏。\n4.  在画布上添加三个水平布局、一条水平线和一个按钮。左键单击画布，然后单击画布顶部的垂直布局按钮继续。将按钮的标签设置为`Convert`。用户界面应该如下所示:\n\n![](img/3f03cc7c-1440-4dfc-82f1-b95a9e183464.png)\n\n5.  在顶部布局中添加两个标签，将左边的文本设置为`From**:**`，然后将右边的设置为`To:`。向第二个布局添加两个线编辑小部件，并将它们的默认值都设置为`1`:\n\n![](img/bf3cf63e-ab04-48fe-a2ff-5714e88cef00.png)\n\n6.  选择右侧的行编辑，并启用属性窗格中的只读复选框:\n\n![](img/83ea3526-e37e-4270-bb96-28ef4374bff2.png)\n\n7.  将光标属性设置为“禁止”，以便用户在小部件上鼠标悬停时知道它不可编辑:\n\n![](img/413cae05-af0c-42dc-89d2-ca1b005ad6ff.png)\n\n8.  在底部的第三个布局中添加两个组合框。我们暂时将它们留空:\n\n![](img/1727edbe-c18f-481e-b3b8-1afd77cb9175.png)\n\n9.  右键单击转换按钮并选择转到插槽。将弹出一个窗口，要求您选择合适的信号。让我们保留默认的 clicked()信号作为选择，然后单击 OK。Qt Creator 将自动为`mainwindow.h`和`mainwindow.cpp`添加一个槽功能。\n10.  打开`mainwindow.h`并确保以下标题被添加到源文件的顶部:\n\n```cpp\n#include <QMainWindow>\n#include <QDoubleValidator>\n#include <QNetworkAccessManager>\n#include <QNetworkRequest>\n#include <QNetworkReply>\n#include <QJsonDocument>\n#include <QJsonObject>\n#include <QDebug>\n#include <QMessageBox>\n```\n\n11.  增加另一个槽函数，叫做`finished()`:\n\n```cpp\nprivate slots:\n    void on_convertButton_clicked();\n    void finished(QNetworkReply* reply);\n```\n\n12.  在`private`标签下添加两个变量:\n\n```cpp\nprivate:\n    Ui::MainWindow *ui;\n    QNetworkAccessManager* manager;\n    QString targetCurrency;\n```\n\n13.  打开`mainwindow.cpp`文件。在类构造函数的两个组合框中添加几个货币短代码。在左侧的行编辑小部件中设置一个验证器，这样它只能接受数字输入。初始化网络访问管理器，并将其`finished()`信号连接到我们的`finished()`插槽功能:\n\n```cpp\nMainWindow::MainWindow(QWidget *parent) :\nQMainWindow(parent), ui(new Ui::MainWindow) {\n    ui->setupUi(this);\n    QStringList currencies;\n    currencies.push_back(\"EUR\");\n    currencies.push_back(\"USD\");\n    currencies.push_back(\"CAD\");\n    currencies.push_back(\"MYR\");\n    currencies.push_back(\"GBP\");\n```\n\n14.  我们继续前面的代码，将货币短格式插入组合框。然后，我们声明一个新的网络访问管理器，并将其**完成的**信号连接到我们的自定义插槽功能:\n\n```cpp\n    ui->currencyFrom->insertItems(0, currencies);\n    ui->currencyTo->insertItems(0, currencies);\n    QValidator *inputRange = new QDoubleValidator(this);\n    ui->amountFrom->setValidator(inputRange);\n    manager = new QNetworkAccessManager(this);\n    connect(manager, &QNetworkAccessManager::finished, this, &MainWindow::finished);\n}\n```\n\n15.  定义如果用户点击`Convert`按钮会发生什么:\n\n```cpp\nvoid MainWindow::on_convertButton_clicked() {\n    if (ui->amountFrom->text() != \"\") {\n        ui->convertButton->setEnabled(false);\n        QString from = ui->currencyFrom->currentText();\n        QString to = ui->currencyTo->currentText();\n        targetCurrency = to;\n        QString url = \"http://data.fixer.io/api/latest?base=\" + from + \"&symbols=\" + to + \"&access_key=616e8b801a222f144a9460b5e6942ca4\";\n```\n\n16.  通过调用`get()`启动请求:\n\n```cpp\n        QNetworkRequest request= QNetworkRequest(QUrl(url));\n        manager->get(request);\n    } else {\n        QMessageBox::warning(this, \"Error\", \"Please insert a value.\");\n    }\n}\n```\n\n17.  定义当`finished()`信号被触发时会发生什么:\n\n```cpp\nvoid MainWindow::finished(QNetworkReply* reply) {\n    QByteArray response = reply->readAll();\n    qDebug() << response;\n    QJsonDocument jsonResponse = QJsonDocument::fromJson(response);\n    QJsonObject jsonObj = jsonResponse.object();\n    QJsonObject jsonObj2 = jsonObj.value(\"rates\").toObject();\n    double rate = jsonObj2.value(targetCurrency).toDouble();\n```\n\n18.  继续从前面的代码编写代码，如下所示:\n\n```cpp\n    if (rate == 0)\n        rate = 1;\n    double amount = ui->amountFrom->text().toDouble();\n    double result = amount * rate;\n    ui->amountTo->setText(QString::number(result));\n    ui->convertButton->setEnabled(true);\n}\n```\n\n19.  编译并运行项目，您应该会得到一个简单的货币转换器，如下所示:\n\n![](img/af3bffb0-df5f-4330-a6ac-199cb63c3e18.png)\n\n# 它是如何工作的...\n\n类似于前面我们看到的使用外部程序来实现特定任务的例子，这次我们使用了一个外部服务提供商，它为我们提供了一个开放的**应用编程接口** ( **API** )，该接口对所有人都是免费的，并且易于使用。\n\n这样，我们就不用考虑用来检索最新汇率的方法了。相反，服务提供商已经为我们完成了工作；我们只需要发送一个礼貌的请求就可以了。然后，我们等待他们服务器的响应，并根据我们的预期目的处理数据。\n\n除了 fixer . io([http://fixer . io](http://fixer.io))之外，还有相当多不同的服务商可以选择。有些是免费的，但没有任何高级功能；有些给你提供高价。其中一些替代方案是**公开汇率**([https://openexchangerates.org](https://openexchangerates.org))**货币层****API**([https://currencylayer.com](https://currencylayer.com))**货币 API**([https://currency-api.appspot.com](https://currency-api.appspot.com))**XE 货币数据 API**([http://www.xe.com/xecurrencydata](http://www.xe.com/xecurrencydata))以及**JSON rates**([http://jsonrates.com](http://jsonrates.com))。\n\n在前面的代码中，您应该已经注意到一个访问密钥被传递给了 Fixer.io API，这是我为本教程注册的一个免费访问密钥。如果您将它用于自己的项目，您应该在 Fixer.io 创建一个帐户\n\n# 还有更多…\n\n除了货币汇率之外，您还可以使用这种方法来执行更高级的任务，这些任务可能太复杂，自己无法完成，或者根本无法访问，除非您使用专家提供的服务，例如可编程的**短信服务** ( **短信**)和语音服务、网络分析和统计生成以及在线支付网关。这些服务大部分不是免费的，但是你甚至不需要设置服务器基础设施和后端系统，就可以在几分钟内轻松实现这些功能；毫无疑问，这是让您的产品正常运行的最便宜、最快的方法，没有太多麻烦。"
  },
  {
    "path": "docs/qt5-cpp-gui-prog-cb/11.md",
    "content": "# 十一、使用 SQL 驱动和 Qt 访问数据库\n\n这一章将涵盖以下食谱:\n\n*   建立数据库\n*   连接到数据库\n*   编写基本的 SQL 查询\n*   用 Qt 创建登录屏幕\n*   在模型视图上显示数据库中的信息\n*   高级 SQL 查询\n\n# 介绍\n\n**结构化查询语言** ( **SQL** )是一种特殊的编程语言，用于管理关系数据库管理系统中保存的数据。SQL Server 是一个数据库系统，旨在使用多种 SQL 编程语言之一来管理其数据。\n\nIf you want to learn more about SQL, visit [http://www.w3schools.com/sql/sql_intro.asp](http://www.w3schools.com/sql/sql_intro.asp).\n\n# 技术要求\n\n本章的技术要求包括 Qt 5.11.2 MinGW 32 位，Qt Creator 4.8.2，Windows 10，以及带有 MySQL 的 XAMPP。\n\n本章使用的所有代码可从以下 GitHub 链接下载:[https://GitHub . com/PacktPublishing/Qt5-CPP-GUI-Programming-cook book-第二版/树/主/第 11 章](https://github.com/PacktPublishing/Qt5-CPP-GUI-Programming-Cookbook-Second-Edition/tree/master/Chapter11)。\n\n查看以下视频，查看正在运行的代码:[http://bit.ly/2TptLKn](http://bit.ly/2TptLKn)\n\n# 建立数据库\n\nQt 以插件/附加组件的形式支持几种不同类型的 SQL 驱动。然而，将这些驱动集成到您的 Qt 项目中是非常容易的。我们将在下面的示例中学习如何做到这一点。\n\n# 怎么做…\n\n在深入研究 Qt 之前，让我们先设置一下我们的 SQL Server:\n\n1.  安装并设置一个 MySQL 服务器。有很多方法可以安装它。一种方法是在[http://dev.mysql.com/downloads/mysql/](http://dev.mysql.com/downloads/mysql/)从官网下载 MySQL 并安装。之后，您还需要从[http://dev.mysql.com/downloads/workbench/](http://dev.mysql.com/downloads/workbench/)安装 MySQL 工作台来管理您的数据库。\n2.  另一种方法是在一个统一的安装程序中安装 MySQL 和其他有用应用附带的第三方包，如 Apache 网络服务器或 phpMyAdmin。这类包的例子有在 https://sourceforge.net/projects/xampp/的 XAMPP 和在 T4 的 https://www.appservnetwork.com/en/download/的苹果服务器。\n3.  在这个例子中，我们将安装 XAMPP。打开你的网络浏览器，从[https://sourceforge.net/projects/xampp/](https://sourceforge.net/projects/xampp/)下载 XAMPP 安装程序，安装在你的电脑上。\n\n4.  打开 XAMPP 控制面板，您应该会看到如下内容:\n\n![](img/77b58f97-8d53-4c4f-ba22-a2bff848370c.png)\n\n5.  我们需要 Apache 网络服务器和 MySQL 数据库服务器。单击控制面板上 Apache 和 MySQL 选项旁边的开始按钮。\n\n6.  服务器启动后，打开您的网络浏览器，访问[http://localhost/phpmyadmin/](http://localhost/phpmyadmin/)。您将看到一个名为 phpMyAdmin 的 web 界面，如下所示:\n\n![](img/dbba624a-1f3b-4945-bf93-8c36a4ed193d.png)\n\nphpMyAdmin 是一个基于网络的实用程序，可以帮助您管理 MySQL 数据库，很像 MySQL Workbench。在我看来，phpMyAdmin 要简单得多，更适合初学者，这就是为什么我建议用它来代替 MySQL Workbench。默认情况下，phpMyAdmin 使用默认用户帐户根自动登录 MySQL，该帐户根保存在其配置文件中。出于安全考虑，我们不想用它。所以，让我们为自己创建一个帐户。\n\n7.  转到位于顶部的“用户”选项卡，进入该页面后，单击位于底部的“添加用户帐户”。在“登录信息”窗格的字段中键入所需的用户名和密码。现在选择“本地”作为主机名选项。在底部，您将看到与全局权限相关的选项，选中全部检查选项，然后单击开始:\n\n![](img/a88271b2-4680-4fcc-901e-8786038d82ca.png)\n\n8.  转到 XAMPP 控制面板，并为 Apache 和 MySQL 单击停止。点击 Apache 列的 Config 按钮，选择 phpMyAdmin ( `config.inc.php`)选项；将使用您选择的文本编辑器打开`config.inc.php`文件。\n9.  在`config.inc.php`中搜索下一行，将`config`改为`cookie`:\n\n```cpp\n$cfg['Servers'][$i]['auth_type'] = 'config';\n$cfg['Servers'][$i]['auth_type'] = 'cookie';\n```\n\n10.  单击开始按钮，再次启动 Apache 和 MySQL。这样，我们强制 phpMyAdmin 重新加载其配置并应用更改。从您的网络浏览器再次转到 phpMyAdmin，这一次，应该会出现一个登录屏幕:\n\n![](img/a1b3d4d5-4aba-4794-b936-c23069d95db2.png)\n\n11.  登录 phpMyAdmin，然后点击侧边栏上的新建链接:\n\n![](img/f1cc29b1-2833-44d9-8efc-55e26c196ec4.png)\n\n12.  键入所需的数据库名称，然后按“创建”按钮。创建后，数据库名称将出现在侧栏上。单击数据库名称，它将带您进入另一个页面，显示一条消息:在数据库中找不到表。在该消息下，您可以通过填写所需的表名和表的列数来创建第一个数据表:\n\n![](img/d9740352-559b-43d8-b764-0f8c62ec121a.png)\n\n13.  单击开始按钮，您将进入另一个页面，在那里您将设置要创建的新表格。在本例中，我们创建了一个由五列数据组成的`employee`表:id、姓名、年龄、性别和已婚:\n\n![](img/e4001656-0cad-4e4b-acb0-d75830aff281.png)\n\n14.  点击保存，现在你可以看到`employee`表格名称出现在侧栏上。我们已经成功安装了 MySQL，并建立了我们的第一个数据库和数据表。\n\n15.  我们需要从 phpMyAdmin 向数据库中插入数据，以便我们能够在下一个示例中检索它。当您还在`employee`表格中时，点击插入标签。然后，您将进入另一页，将新数据插入`employee`表:\n\n![](img/0772b0bf-d506-499d-94f6-227d243e82cd.png)\n\n16.  让我们为我们的 Qt 项目设置 SQL 驱动。去你的 Qt 安装文件夹找`sqldrivers`文件夹就行了。比如我的位于`C:\\Qt\\5.5\\mingw492_32\\plugins\\sqldrivers`。\n17.  将整个`sqldrivers`文件夹复制到项目的构建目录中。您可以删除与正在运行的 SQL Server 无关的动态链接库文件。在我们的例子中，因为我们使用的是 MySQL 服务器，所以我们可以删除除`qsqlmysql.dll`和`qsqlmysqld.dll`之外的所有内容。结尾带有字母 d 的 DLL 文件仅用于调试版本，而另一个用于发布版本。将这些 DLL 文件放在它们各自的构建目录中，例如，用于调试构建的`builds/debug/sqldrivers/qsqlmysqld.dll`和用于发布构建的`builds/release/sqldrivers/qsqlmysql.dll`。\n18.  上一步提到的 DLL 文件是使 Qt 能够与不同类型的 SQL 架构进行通信的驱动。您可能还需要 SQL 客户端库的 DLL 文件，以便驱动工作。在我们的例子中，我们需要`libmysql.dll`位于与我们的程序可执行文件相同的目录中。可以从 MySQL 的安装目录中获取，也可以从官网:[https://dev.mysql.com/downloads/connector/cpp/](https://dev.mysql.com/downloads/connector/cpp/)下载`Connector/C++ `包。\n\n# 它是如何工作的…\n\nQt 为我们提供了 SQL 驱动，这样我们就可以轻松地连接到不同类型的 SQL Servers，而无需自己实现它们。\n\n目前，Qt 正式支持 SQLite、MySQL、ODBC 和 PostgreSQL。作为支持的架构之一的分叉的 SQL 架构，例如 Mariadb(MySQL 的一个分叉)，可能仍然可以与 Qt 兼容，没有太多麻烦。如果您正在使用 Qt 不支持的架构，您仍然可以通过使用`QNetworkAccessManager`向后端脚本(如 PHP、ASP 和 JSP)发送 HTTP 请求来间接与您的 SQL 数据库交互，然后后端脚本可以与数据库通信。\n\n如果你只需要一个简单的基于文件的数据库，不打算使用基于服务器的数据库，那么 SQLite 是一个不错的选择。\n\n在*连接数据库*食谱中，我们将学习如何使用 Qt 的 SQL 模块连接到我们的 SQL 数据库。\n\n# 连接到数据库\n\n在本食谱中，我们将学习如何将我们的 Qt 5 应用连接到 SQL Server。\n\n# 怎么做…\n\n在 Qt 中连接到 SQL Server 非常简单:\n\n1.  打开 Qt 创建器，创建一个新的 Qt 小部件应用项目。\n2.  打开您的项目文件(`.pro`)并将`sql`模块添加到您的项目中，如下所示:\n\n```cpp\nQT += core gui sql\n```\n\n3.  打开`mainwindow.ui`并将七个标签小部件、一个组合框和一个复选框拖到画布上。将四个标签的文本属性设置为`Name:`、`Age:`、`Gender:`和`Married:`。然后，将其余部分的 objectName 属性设置为`name`、`age`、`gender`和`married`。没有必要为前面四个标签设置对象名称，因为它们仅用于显示目的:\n\n![](img/abaa7eb0-135b-4baf-9414-3a60a817a51c.png)\n\n4.  打开`mainwindow.h`并在`QMainWindow`标题下添加以下标题:\n\n```cpp\n#include <QMainWindow>\n#include <QtSql>\n#include <QSqlDatabase>\n#include <QSqlQuery>\n#include <QDebug>\n```\n\n5.  打开`mainwindow.cpp`，将以下代码插入类构造函数:\n\n```cpp\nMainWindow::MainWindow(QWidget *parent) :\nQMainWindow(parent), ui(new Ui::MainWindow) {\n    ui->setupUi(this);\n    QSqlDatabase db = QSqlDatabase::addDatabase(\"QMYSQL\");\n    db.setHostName(\"127.0.0.1\");\n    db.setUserName(\"yourusername\");\n    db.setPassword(\"yourpassword\");\n    db.setDatabaseName(\"databasename\");\n```\n\n6.  数据库连接打开后，启动 SQL 查询:\n\n```cpp\n    if (db.open()) {\n        QSqlQuery query;\n        if (query.exec(\"SELECT name, age, gender, married FROM employee\")) {\n            while (query.next()) {\n                qDebug() << query.value(0) << query.value(1) << query.value(2) << query.value(3);\n                ui->name->setText(query.value(0).toString());\n                ui->age->setText(query.value(1).toString());\n                ui->gender->setCurrentIndex(query.value(2).toInt());\n                ui->married->setChecked(query.value(3).toBool());\n            }\n        }\n```\n\n7.  打印出任何错误文本:\n\n```cpp\nelse {\n            qDebug() << query.lastError().text();\n        }\n        db.close();\n    }\n    else {\n        qDebug() << \"Failed to connect to database.\";\n    }\n}\n```\n\n8.  现在编译并运行您的项目，您应该会得到如下结果:\n\n![](img/2d25a1ba-8109-40ec-8c6e-59e24fa377f6.png)\n\n# 它是如何工作的…\n\n前面的例子向您展示了如何使用从 SQL 模块派生的`QSqlDatabase`类连接到您的 SQL 数据库。如果不将模块添加到您的 Qt 项目中，您将无法访问任何与 SQL 相关的类。\n\n我们必须通过在调用`addDatabase()`函数时提到它来告诉 Qt 我们正在运行哪个 SQL 架构。Qt 支持的选项有`QSQLITE`、`QMYSQL`、`QMYSQL3`、`QODBC`、`QODBC3`、`QPSQL`和`QPSQL7`。如果您遇到一条错误消息，上面写着 QSqlDatabase: QMYSQL 驱动未加载，您应该检查 DLL 文件是否放置在正确的目录中。\n\n我们可以通过`QSqlQuery`类将我们的 SQL 语句发送到数据库，等待它返回结果，这些结果通常要么是您请求的数据，要么是由于语句无效而产生的错误消息。如果有来自数据库服务器的数据，将全部存储在`QSqlQuery`类中。您只需要在`QSqlQuery`类上做一个 while 循环来检查所有现有的记录，并通过调用`value()`函数来检索它们。\n\n# 编写基本的 SQL 查询\n\n在前面的例子中，我们编写了第一个 SQL 查询，它涉及`SELECT`语句。这次，我们将学习如何使用其他一些 SQL 语句，如`INSERT`、`UPDATE`、`DELETE`。\n\n# 怎么做…\n\n让我们创建一个简单的程序，按照以下步骤演示基本的 SQL 查询命令:\n\n1.  我们可以使用以前的项目文件，但是有几件事我们需要改变。打开`mainwindow.ui`并用线编辑小部件替换姓名和年龄的标签。然后，在画布上添加三个按钮，分别称为`UPDATE`、`INSERT`和`DELETE`:\n\n![](img/4f543375-6bf8-4245-917b-fe87ee690ce4.png)\n\n2.  打开`mainwindow.h`，在`private`继承下增加以下变量:\n\n```cpp\nprivate:\n    Ui::MainWindow *ui;\n    QSqlDatabase db;\n    bool connected;\n    int currentID;\n```\n\n3.  打开`mainwindow.cpp`转到类构造器。它仍然与前面的示例基本相同，只是我们将数据库连接状态存储在名为`connected`的`Boolean`变量中，并且我们还从数据库中获取数据的 ID 并将其存储到名为`currentID`的整数变量中:\n\n```cpp\nMainWindow::MainWindow(QWidget *parent) :\nQMainWindow(parent), ui(new Ui::MainWindow) {\n    ui->setupUi(this);\n    db = QSqlDatabase::addDatabase(\"QMYSQL\");\n    db.setHostName(\"127.0.0.1\");\n    db.setUserName(\"yourusername\");\n    db.setPassword(\"yourpassword\");\n    db.setDatabaseName(\"databasename\");\n    connected = db.open();\n```\n\n4.  连接到数据库后，让我们进行查询:\n\n```cpp\nif (connected) {\n        QSqlQuery query;\n        if (query.exec(\"SELECT id, name, age, gender, married FROM employee\")) {\n            while (query.next()) {\n                currentID = query.value(0).toInt();\n                ui->name->setText(query.value(1).toString());\n                ui->age->setText(query.value(2).toString());\n                ui->gender->setCurrentIndex(query.value(3).toInt());\n                ui->married->setChecked(query.value(4).toBool());\n            }\n        }\n```\n\n5.  打印出任何错误信息:\n\n```cpp\n   else {\n            qDebug() << query.lastError().text();\n        }\n    }\n    else {\n        qDebug() << \"Failed to connect to database.\";\n    }\n}\n```\n\n6.  转到`mainwindow.ui`，右键单击我们在*步骤 1* 中添加到画布上的一个按钮。选择转到插槽…并单击确定。在另一个按钮上重复这些步骤，现在您应该会看到三个插槽功能被添加到您的`mainwindow.h`和`mainwindow.cpp`中:\n\n```cpp\nprivate slots:\n    void on_updateButton_clicked();\n    void on_insertButton_clicked();\n    void on_deleteButton_clicked();\n```\n\n7.  打开`mainwindow.cpp`点击更新按钮，我们会声明程序会做什么:\n\n```cpp\nvoid MainWindow::on_updateButton_clicked() {\n    if (connected) {\n        if (currentID == 0) {\n            qDebug() << \"Nothing to update.\";\n        }\n        else {\n            QString id = QString::number(currentID);\n            QString name = ui->name->text();\n            QString age = ui->age->text();\n            QString gender = QString::number(ui->gender->currentIndex());\n            QString married = QString::number(ui->married->isChecked());\n```\n\n8.  进行更新查询，如下所示:\n\n```cpp\n\n            qDebug() << \"UPDATE employee SET name = '\" + name + \"', age = '\" + age + \"', gender = \" + gender + \", married = \" + married + \" WHERE id = \" + id;\n            QSqlQuery query;\n            if (query.exec(\"UPDATE employee SET name = '\" + name + \"', age = '\" + age + \"', gender = \" + gender + \", married = \" + married + \" WHERE id = \" + id)) {\n                qDebug() << \"Update success.\";\n            }\n```\n\n9.  打印出最后一条错误消息(如果有):\n\n```cpp\n   else {\n                qDebug() << query.lastError().text();\n            }\n        }\n    } else {\n        qDebug() << \"Failed to connect to database.\";\n    }\n}\n```\n\n10.  声明点击`INSERT`按钮会发生什么:\n\n```cpp\nvoid MainWindow::on_insertButton_clicked() {\n    if (connected) {\n        QString name = ui->name->text();\n        QString age = ui->age->text();\n        QString gender = QString::number(ui->gender->currentIndex());\n        QString married = QString::number(ui->married->isChecked());\n        qDebug() << \"INSERT INTO employee (name, age, gender, married) VALUES ('\" + name + \"','\" + age + \"', \" + gender + \",\" + married + \")\";\n```\n\n11.  进行`INSERT`查询，如是:\n\n```cpp\n        QSqlQuery query;\n        if (query.exec(\"INSERT INTO employee (name, age, gender, married) VALUES ('\" + name + \"','\" + age + \"', \" + gender + \",\" + married + \")\")) {\n            currentID = query.lastInsertId().toInt();\n            qDebug() << \"Insert success.\";\n        } else {\n            qDebug() << query.lastError().text();\n        }\n    } else {\n        qDebug() << \"Failed to connect to database.\";\n    }\n}\n```\n\n12.  声明点击`Delete`按钮会发生什么:\n\n```cpp\nvoid MainWindow::on_deleteButton_clicked() {\n    if (connected) {\n        if (currentID == 0) {\n            qDebug() << \"Nothing to delete.\";\n        } else {\n            QString id = QString::number(currentID);\n            qDebug() << \"DELETE FROM employee WHERE id = \" + id;\n            QSqlQuery query;\n            if (query.exec(\"DELETE FROM employee WHERE id = \" + id)) {\n                currentID = 0;\n                qDebug() << \"Delete success.\";\n            } else {\n                qDebug() << query.lastError().text();\n            }\n        }\n    } else {\n        qDebug() << \"Failed to connect to database.\";\n    }\n}\n```\n\n13.  在类析构函数处调用`QSqlDatabase::close()`在退出程序前正确终止 SQL 连接:\n\n```cpp\nMainWindow::~MainWindow() {\n    db.close();\n    delete ui;\n}\n```\n\n14.  现在编译并运行程序，您应该能够从数据库中选择默认数据。然后，您可以选择更新它或从数据库中删除它。也可以通过点击`Insert`按钮将新数据插入数据库。您可以使用 phpMyAdmin 检查数据是否被正确修改:\n\n![](img/9cec5255-9aae-4ff6-9213-8297d9a09b19.png)\n\n# 它是如何工作的…\n\n在我们继续向数据库发送 SQL 查询之前，检查数据库是否连接是非常重要的。因此，我们将该状态保存在一个变量中，并使用它在发送任何查询之前进行检查。但是，对于长时间保持打开的复杂程序，不建议这样做，因为在此期间数据库可能会断开连接，固定变量可能不准确。既然如此，不如通过调用`QSqlDatabase::isOpen()`来查看实际状态。\n\n`currentID`变量用于保存你从数据库中获取的当前数据的 ID。当您想要更新数据或从数据库中删除数据时，这个变量对于让数据库知道您试图更新或删除哪些数据至关重要。如果您正确设置了数据库表，MySQL 会将每一项数据视为一个唯一的条目，因此您可以确保在保存新数据时数据库中不会产生重复的标识。\n\n在数据库中插入新数据后，我们调用`QSqlQuery::lastInsertId()`获取新数据的 ID，并将其保存为`currentID`变量，使其成为我们可以从数据库中更新或删除的当前数据。在 Qt 中使用之前，在 phpMyAdmin 上测试您的 SQL 查询是一个好习惯。您可以立即发现您的 SQL 语句是正确的还是不正确的，而不是等待您的项目被构建，尝试它，然后重建它。作为程序员，我们必须以最有效的方式工作。努力工作，聪明工作。\n\n# 用 Qt 创建登录屏幕\n\n在这个食谱中，我们将学习如何使用我们的知识，并使用 Qt 和 MySQL 创建一个功能登录屏幕。\n\n# 怎么做…\n\n按照以下步骤创建您的第一个功能登录屏幕:\n\n1.  打开一个网页浏览器，转到 phpMyAdmin。我们将创建一个名为`user`的新数据表，如下所示:\n\n![](img/63cb039e-f865-43d0-9336-d410ebe0f976.png)\n\n2.  让我们将第一项数据插入到新创建的表中，并将`employeeID`设置为现有员工的 ID。这样，我们创建的用户帐户将链接到其中一名员工的数据:\n\n![](img/23269d83-384c-4414-a9ff-d8217e0db6c4.png)\n\n3.  打开 Qt 创建器，创建一个新的 Qt 小部件应用项目。我们将从`mainwindow.ui`开始。在画布上放置一个堆叠的小部件，并确保它包含两页。然后，在堆叠小部件中设置两个页面，如下所示:\n\n![](img/29724a45-d813-489a-b26e-3c00b89c58ae.png)\n\n4.  在堆叠小部件的第一页上，单击小部件顶部的编辑选项卡顺序图标，以便您可以调整程序中小部件的顺序:\n\n![](img/7a478ecb-8a86-460c-ade7-71bc3823dcb4.png)\n\n5.  单击“编辑标签顺序”图标后，您会看到一些数字出现在画布中每个小部件的顶部。确保这些数字看起来和下面截图中的一样。否则，点击数字改变它们的顺序。我们只对堆叠小部件的第一页这样做；保持第二页原样是可以的:\n\n![](img/05f901d0-acd5-49d9-90c1-66fa0a974785.png)\n\n6.  右键单击登录按钮并选择转到插槽。然后，确保选中了单击的()选项，并按“确定”。然后 Qt 会在你的项目源文件中为你创建一个槽函数。对“注销”按钮也重复此步骤。\n7.  打开`mainwindow.h`并在`#include <QMainWindow>`行后添加以下标题:\n\n```cpp\n#include <QMainWindow>\n#include <QtSql>\n#include <QSqlDatabase>\n#include <QSqlQuery>\n#include <QMessageBox>\n#include <QDebug>\n```\n\n8.  将以下变量添加到`mainwindow.h`:\n\n```cpp\nprivate:\n    Ui::MainWindow *ui;\n    QSqlDatabase db;\n```\n\n9.  打开`mainwindow.cpp`，将该代码放入类构造函数中:\n\n```cpp\nMainWindow::MainWindow(QWidget *parent) :\nQMainWindow(parent),\nui(new Ui::MainWindow) {\n    ui->setupUi(this);\n    ui->stackedWidget->setCurrentIndex(0);\n    db = QSqlDatabase::addDatabase(\"QMYSQL\");\n    db.setHostName(\"127.0.0.1\");\n    db.setUserName(\"yourusername\");\n    db.setPassword(\"yourpassword\");\n    db.setDatabaseName(\"databasename\");\n    if (!db.open()) {\n        qDebug() << \"Failed to connect to database.\";\n    }\n}\n```\n\n10.  定义如果单击登录按钮会发生什么:\n\n```cpp\nvoid MainWindow::on_loginButton_clicked() {\n    QString username = ui->username->text();\n    QString password = ui->password->text();\n    QSqlQuery query;\n    if (query.exec(\"SELECT employeeID from user WHERE username = '\" + username + \"' AND password = '\" + password + \"'\")) {\n        if (query.size() > 0) {\n            while (query.next()) {\n                QString employeeID = query.value(0).toString();\n                QSqlQuery query2;\n```\n\n11.  进行一个 SQL 查询:\n\n```cpp\n                if (query2.exec(\"SELECT name, age, gender, married FROM employee WHERE id = \" + employeeID)) {\n                    while (query2.next()) {\n                        QString name = query2.value(0).toString();\n                        QString age = query2.value(1).toString();\n                        int gender = query2.value(2).toInt();\n                        bool married = query2.value(3).toBool();\n                        ui->name->setText(name);\n                        ui->age->setText(age);\n```\n\n12.  我们继续前面的代码，在将堆叠的小部件切换到第二页之前，我们设置了`gender`和`married`文本:\n\n```cpp\n                        if (gender == 0)\n                            ui->gender->setText(\"Male\");\n                        else\n                            ui->gender->setText(\"Female\");\n                        if (married)\n                            ui->married->setText(\"Yes\");\n                        else\n                            ui->married->setText(\"No\");\n                        ui->stackedWidget->setCurrentIndex(1);\n                    }\n                }\n            }\n        }\n```\n\n13.  如果登录失败，打印错误消息:\n\n```cpp\n        else {\n            QMessageBox::warning(this, \"Login failed\", \"Invalid username or password.\");\n        }\n    } else {\n        qDebug() << query.lastError().text();\n    }\n}\n```\n\n14.  定义如果单击“注销”按钮会发生什么:\n\n```cpp\nvoid MainWindow::on_logoutButton_clicked() {\n    ui->stackedWidget->setCurrentIndex(0);\n}\n```\n\n15.  主窗口关闭时关闭数据库:\n\n```cpp\nMainWindow::~MainWindow() {\n    db.close();\n    delete ui;\n}\n```\n\n16.  编译并运行该程序，您应该能够使用虚拟帐户登录。登录后，您应该能够看到链接到用户帐户的虚拟员工信息。您也可以通过单击“注销”按钮来注销:\n\n![](img/b4dde1bd-53de-4dc5-b13e-c30b3254390b.png)\n\n# 它是如何工作的…\n\n在本例中，我们从`user`表中选择与我们插入文本字段的用户名和密码相匹配的数据。如果没有发现任何东西，这意味着我们提供了无效的用户名或密码。否则，从用户帐户中获取`employeeID`数据，并执行另一个 SQL 查询，从与`employeeID`变量匹配的`employee`表中查找信息。然后，根据程序的 UI 显示数据。\n\n我们必须在编辑选项卡顺序模式中设置小部件顺序，这样当程序启动时，第一个关注的小部件是用户名行编辑小部件。如果用户按下键盘上的 *Tab* 键，焦点应该切换到第二个小部件，即密码行编辑。不正确的小部件订单会破坏用户体验并赶走任何潜在用户。确保密码行编辑的回声模式选项设置为`Password`。出于安全目的，该设置将隐藏插入行编辑中的实际密码，并用点符号替换它。\n\n# 在模型视图上显示数据库中的信息\n\n在本食谱中，我们将学习如何在程序的模型视图上显示从我们的 SQL 数据库获得的多组数据。\n\n# 怎么做…\n\n按照以下步骤在模型视图小部件上显示数据库中的信息:\n\n1.  我们将使用名为`employee`的数据库表，我们在前面的示例中使用了该表，*使用 Qt* 创建登录屏幕。这一次，我们需要`employee`表中更多的数据。打开你的网络浏览器，登录你的 phpMyAdmin 控制面板。\n\n为更多的员工添加数据，以便我们可以在以后的程序中显示这些数据:\n\n![](img/e911e83b-7c5a-4566-b238-23d7402585df.png)\n\n2.  打开 Qt 创建器，创建一个新的 Qt 小部件应用项目，然后将 SQL 模块添加到您的项目中。\n3.  打开`mainwindow.ui`并在小部件框窗格下从项目小部件(基于项目)添加一个表格小部件(不是表格视图)。选择画布上的主窗口，然后单击“垂直布局”或“水平布局”按钮，使表格小部件保持主窗口的大小，即使在调整其大小时也是如此:\n\n![](img/815d4bc5-09e0-4e11-a9f0-150e9d1e8a5b.png)\n\n4.  双击表格部件，然后会出现一个窗口。在“列”选项卡下，通过单击左上角的+按钮添加五个项目。说出项目`ID`、`Name`、`Age`、`Gender`和`Married`的名称。完成后，单击确定:\n\n![](img/d9629862-2721-41d4-b18c-1e4cfc4a6f6d.png)\n\n5.  右键单击表格小部件，并在弹出菜单中选择转到插槽。一直向下滚动，在弹出窗口中选择项目已更改(QTableWidgetItem*)选项，然后按确定。将在两个源文件中创建一个槽函数。\n6.  打开`mainwindow.h`并将这些私有变量添加到`MainWindow`类中:\n\n```cpp\nprivate:\n    Ui::MainWindow *ui;\n    bool hasInit;\n    QSqlDatabase db;\n```\n\n7.  将以下类标题添加到`mainwindow.h`:\n\n```cpp\n#include <QtSql>\n#include <QSqlDatabase>\n#include <QSqlQuery>\n#include <QMessageBox>\n#include <QDebug>\n#include <QTableWidgetItem>\n```\n\n8.  打开`mainwindow.cpp`–我们将在那里编写大量代码。我们需要声明当程序启动时会发生什么。将以下代码添加到`MainWindow`类的构造函数中:\n\n```cpp\nMainWindow::MainWindow(QWidget *parent) :\nQMainWindow(parent),\nui(new Ui::MainWindow) {\n    hasInit = false;\n    ui->setupUi(this);\n    db = QSqlDatabase::addDatabase(\"QMYSQL\");\n    db.setHostName(\"127.0.0.1\");\n    db.setUserName(\"yourusername\");\n    db.setPassword(\"yourpassword\");\n    db.setDatabaseName(\"databasename\");\n    ui->tableWidget->setColumnHidden(0, true);\n```\n\n9.  SQL 查询代码如下所示:\n\n```cpp\n    if (db.open()) {\n        QSqlQuery query;\n        if (query.exec(\"SELECT id, name, age, gender, married FROM employee\")) {\n            while (query.next()) {\n                qDebug() << query.value(0) << query.value(1) << query.value(2) << query.value(3) << query.value(4);\n                QString id = query.value(0).toString();\n                QString name = query.value(1).toString();\n                QString age = query.value(2).toString();\n                int gender = query.value(3).toInt();\n                bool married = query.value(4).toBool();\n```\n\n10.  创建几个`QTableWidgetItem`对象:\n\n```cpp\n                ui->tableWidget->setRowCount(ui->tableWidget->rowCount() + 1);\n                QTableWidgetItem* idItem = new QTableWidgetItem(id);\n                QTableWidgetItem* nameItem = new QTableWidgetItem(name);\n                QTableWidgetItem* ageItem = new QTableWidgetItem(age);\n                QTableWidgetItem* genderItem = new QTableWidgetItem();\n                if (gender == 0)\n                    genderItem->setData(0, \"Male\");\n                else\n                    genderItem->setData(0, \"Female\");\n                QTableWidgetItem* marriedItem = new QTableWidgetItem();\n                if (married)\n                    marriedItem->setData(0, \"Yes\");\n                else\n                    marriedItem->setData(0, \"No\");\n```\n\n11.  将这些对象移动到表格小部件:\n\n```cpp\n                ui->tableWidget->setItem(ui->tableWidget->rowCount() - 1, 0, idItem);\n                ui->tableWidget->setItem(ui->tableWidget->rowCount() - 1, 1, nameItem);\n                ui->tableWidget->setItem(ui->tableWidget->rowCount() - 1, 2, ageItem);\n                ui->tableWidget->setItem(ui->tableWidget->rowCount() - 1, 3, genderItem);\n                ui->tableWidget->setItem(ui->tableWidget->rowCount() - 1, 4, marriedItem);\n            }\n            hasInit = true;\n        } else {\n            qDebug() << query.lastError().text();\n        }\n    } else {\n        qDebug() << \"Failed to connect to database.\";\n    }\n}\n```\n\n12.  声明表格小部件的一个项目被编辑后会发生什么。将以下代码添加到`on_tableWidget_itemChanged()`槽功能中:\n\n```cpp\nvoid MainWindow::on_tableWidget_itemChanged(QTableWidgetItem *item) {\n    if (hasInit) {\n        QString id = ui->tableWidget->item(item->row(), 0)->data(0).toString();\n        QString name = ui->tableWidget->item(item->row(), 1)->data(0).toString();\n        QString age = QString::number(ui->tableWidget->item(item->row(), 2)->data(0).toInt());\n        ui->tableWidget->item(item->row(), 2)->setData(0, age);\n        QString gender;\n        if (ui->tableWidget->item(item->row(), 3)->data(0).toString() == \"Male\") {\n            gender = \"0\";\n        } else {\n            ui->tableWidget->item(item->row(), 3)->setData(0, \"Female\");\n            gender = \"1\";\n        }\n        QString married;\n        if (ui->tableWidget->item(item->row(), 4)->data(0).toString() == \"No\") {\n            married = \"0\";\n        } else {\n            ui->tableWidget->item(item->row(), 4)->setData(0, \"Yes\");\n            married = \"1\";\n        }\n        qDebug() << id << name << age << gender << married;\n        QSqlQuery query;\n        if (query.exec(\"UPDATE employee SET name = '\" + name + \"', age = '\" + age + \"', gender = '\" + gender + \"', married = '\" + married + \"' WHERE id = \" + id)) {\n            QMessageBox::information(this, \"Update Success\", \"Data updated to database.\");\n        } else {\n            qDebug() << query.lastError().text();\n        }\n     }\n}\n```\n\n13.  在类析构函数处关闭数据库:\n\n```cpp\nMainWindow::~MainWindow() {\n    db.close();\n    delete ui;\n}\n```\n\n14.  现在编译并运行该示例，您应该会得到如下内容:\n\n![](img/b0808d44-a64c-4c73-883e-db3028921884.png)\n\n# 它是如何工作的…\n\n表格小部件与您在电子表格应用(如微软 Excel 和 OpenOffice Calc)中看到的类似。与其他类型的模型查看器(如列表视图或树视图)相比，表视图(或表小部件)是一个二维模型查看器，它以行和列的形式显示数据。\n\nQt 中表视图和表小部件的主要区别在于表小部件构建在表视图类之上，这意味着表小部件更容易使用，更适合初学者。但是，表小部件的灵活性较低，并且比表视图的可伸缩性差，如果您想要自定义表，这不是最佳选择。\n\n从 MySQL 中检索数据后，我们为每个数据项创建了一个`QTableWidgetItem`项，并设置应该将哪一列和哪一行添加到表小部件中。在向表格小部件添加项目之前，我们必须通过调用`QTableWidget::setRowCount()`来增加表格的行数。我们也可以通过简单地调用`QTableWidget::rowCount()`来获得表格小部件的当前行数。\n\n左边的第一列在视图中是隐藏的，因为我们只使用它来保存数据的标识，以便当其行中的一个数据项发生变化时，我们可以使用它来更新数据库。当其中一个单元格中的数据发生变化时，将调用`on_tableWidget_itemChanged()`槽函数。它不仅会在您编辑单元格中的数据时被调用，还会在数据从数据库中检索后首次添加到表中时被调用。为了确保该函数仅在我们编辑数据时触发，我们使用了一个名为`hasInit`的布尔变量来检查我们是否完成了初始化过程(向表中添加第一批数据)。如果`hasInit`为假，忽略函数调用。\n\n为了防止用户输入完全不相关的数据类型，例如在只包含数字的数据单元格中插入字母，我们会手动检查数据是否与我们在编辑时预期的接近。如果它不接近任何被认为有效的值，将其恢复为默认值。当然，这是一个简单的黑客攻击，它可以完成任务，但不是最专业的方法。或者，您可以尝试创建一个继承`QItemDelegate`类的新类，并定义模型视图的行为方式。然后，调用`QTableWidget::setItemDelegate()`将该类应用于您的表小部件。\n\n# 高级 SQL 查询\n\n按照这个食谱，你将学会如何使用高级的 SQL 语句，如`INNER JOIN`、`COUNT`、`LIKE`和`DISTINCT`。\n\n# 怎么做…\n\n除了在 SQL 数据库上执行简单的查询，您还可以做很多事情。让我们按照以下步骤学习如何:\n\n1.  我们需要在数据库中添加一些表，然后才能进入编程部分。打开你的网络浏览器，访问 phpMyAdmin。我们需要几个表来运行这个例子:\n\n![](img/f99b82ba-e5fe-4bac-81c3-0d40976e3456.png)\n\n2.  我将向您展示这个项目所需的每个表的结构，以及插入表中进行测试的虚拟数据。第一个表叫做`branch`，用来存储虚拟公司不同分支机构的 id 和名称:\n\n![](img/87fc38b6-0682-4f35-9871-9ad0444ea5c0.png)\n\n3.  其次，我们有`department`表，它存储了虚拟公司不同部门的 id 和名称，也通过分支 id 链接到`branch`数据:\n\n![](img/8e9da7a7-768a-4744-b64d-77d9f7749dab.png)\n\n4.  我们还有一个`employee`表，它存储了虚拟公司中所有员工的信息。该表类似于我们在前面示例中使用的表，除了它有两个额外的列，`birthday`和`departmentID`:\n\n![](img/58383cd2-5352-44e8-9ecd-f76d0a142e02.png)\n\n5.  我们还有一个名为`log`的表，其中包含每个员工登录时间的伪记录。`loginTime`列可以是`timestamp`或`date-time`变量类型:\n\n![](img/4ac2d60d-5f31-40b1-a617-4416d0d41c5e.png)\n\n6.  我们有`user`表，我们也在前面的例子中使用过:\n\n![](img/895e8d73-d329-4d8b-a795-2824aaccd3a5.png)\n\n7.  打开 Qt 创建器。这一次，我们没有选择 Qt 小部件应用，而是创建了一个 Qt 控制台应用:\n\n![](img/7be3768c-de38-4a03-8d78-b86b9608783e.png)\n\n8.  打开您的项目文件(`.pro`)并将`sql`模块添加到您的项目中:\n\n```cpp\nQT += core sql\nQT -= gui\n```\n\n9.  打开`main.cpp`，在源文件顶部添加以下头文件:\n\n```cpp\n#include <QSqlDatabase>\n#include <QSqlQuery>\n#include <QSqlError>\n#include <QDate>\n#include <QDebug>\n```\n\n10.  添加以下功能以显示 30 岁以上的员工:\n\n```cpp\nvoid filterAge() {\n    qDebug() << \"== Employees above 30 year old =============\";\n    QSqlQuery query;\n    if (query.exec(\"SELECT name, age FROM employee WHERE age > 30\")) {\n        while (query.next()) {\n            qDebug() << query.value(0).toString() << query.value(1).toString();\n        }\n    } else {\n        qDebug() << query.lastError().text();\n    }\n    qDebug() << \"\\n\";\n}\n```\n\n11.  添加以下功能来显示每个员工的部门和分支机构信息:\n\n```cpp\nvoid getDepartmentAndBranch() {\n    qDebug() << \"== Get employees' department and branch =============\";\n    QSqlQuery query;\n    if (query.exec(\"SELECT myEmployee.name, department.name, branch.name FROM (SELECT name, departmentID FROM employee) AS myEmployee INNER JOIN department ON department.id = myEmployee.departmentID INNER JOIN branch ON branch.id = department.branchID\")) {\n        while (query.next()) {\n            qDebug() << query.value(0).toString() << query.value(1).toString() << query.value(2).toString();\n        }\n    } else {\n        qDebug() << query.lastError().text();\n    }\n    qDebug() << \"\\n\";\n}\n```\n\n12.  添加以下功能，显示在纽约分公司工作且年龄在 30 岁以下的员工:\n\n```cpp\nvoid filterBranchAndAge() {\n    qDebug() << \"== Employees from New York and age below 30 =============\";\n    QSqlQuery query;\n    if (query.exec(\"SELECT myEmployee.name, myEmployee.age, department.name, branch.name FROM (SELECT name, age, departmentID FROM employee) AS myEmployee INNER JOIN department ON department.id = myEmployee.departmentID INNER JOIN branch ON branch.id = department.branchID WHERE branch.name = 'New York' AND age < 30\")) {\n        while (query.next()) {\n            qDebug() << query.value(0).toString() << query.value(1).toString() << query.value(2).toString() << query.value(3).toString();\n        }\n    } else {\n        qDebug() << query.lastError().text();\n    }\n    qDebug() << \"\\n\";\n}\n```\n\n13.  添加以下函数，计算虚拟公司中女性员工的总数:\n\n```cpp\nvoid countFemale() {\n    qDebug() << \"== Count female employees =============\";\n    QSqlQuery query;\n    if (query.exec(\"SELECT COUNT(gender) FROM employee WHERE gender = 1\")) {\n        while (query.next()) {\n            qDebug() << query.value(0).toString();\n        }\n    } else {\n        qDebug() << query.lastError().text();\n    }\n    qDebug() << \"\\n\";\n}\n```\n\n14.  增加以下功能，过滤员工列表，只显示姓名以`Ja`开头的人员:\n\n```cpp\nvoid filterName() {\n    qDebug() << \"== Employees name start with 'Ja' =============\";\n    QSqlQuery query;\n    if (query.exec(\"SELECT name FROM employee WHERE name LIKE '%Ja%'\")) {\n        while (query.next()) {\n            qDebug() << query.value(0).toString();\n        }\n    } else {\n        qDebug() << query.lastError().text();\n    }\n    qDebug() << \"\\n\";\n}\n```\n\n15.  增加以下功能，显示`August`生日的员工:\n\n```cpp\nvoid filterBirthday() {\n    qDebug() << \"== Employees birthday in August =============\";\n    QSqlQuery query;\n    if (query.exec(\"SELECT name, birthday FROM employee WHERE MONTH(birthday) = 8\")) {\n        while (query.next()) {\n            qDebug() << query.value(0).toString() << query.value(1).toDate().toString(\"d-MMMM-yyyy\");\n        }\n    } else {\n        qDebug() << query.lastError().text();\n    }\n    qDebug() << \"\\n\";\n}\n```\n\n16.  增加以下功能，检查谁在`27 April 2019`登录了虚拟系统，并在终端显示其姓名:\n\n```cpp\nvoid checkLog() {\n    qDebug() << \"== Employees who logged in on 27 April 2019 =============\";\n    QSqlQuery query;\n    if (query.exec(\"SELECT DISTINCT myEmployee.name, FROM (SELECT id, name FROM employee) AS myEmployee INNER JOIN user ON user.employeeID = myEmployee.id INNER JOIN log ON log.userID = user.id WHERE DATE(log.loginTime) = '2019-04-27'\")) {\n        while (query.next()) {\n            qDebug() << query.value(0).toString();\n        }\n    } else {\n        qDebug() << query.lastError().text();\n    }\n    qDebug() << \"\\n\";\n}\n```\n\n17.  在`main()`函数中，将程序连接到 MySQL 数据库，并调用我们在前面步骤中定义的所有函数。关闭数据库连接，我们就完成了:\n\n```cpp\nint main(int argc, char *argv[]) {\n    QCoreApplication a(argc, argv);\n    QSqlDatabase db = QSqlDatabase::addDatabase(\"QMYSQL\");\n    db.setHostName(\"127.0.0.1\");\n    db.setUserName(\"reonyx\");\n    db.setPassword(\"reonyx\");\n    db.setDatabaseName(\"testing\");\n    if (db.open()) {\n        filterAge();\n        getDepartmentAndBranch();\n        filterBranchAndAge();\n        countFemale();\n        filterName();\n        filterBirthday();\n        checkLog();\n        db.close();\n    } else {\n        qDebug() << \"Failed to connect to database.\";\n    }\n    return a.exec();\n}\n```\n\n18.  编译并运行项目，您应该会看到一个显示筛选结果的终端窗口:\n\n![](img/b5eb23c0-125c-435a-872d-61e13ec70c18.png)\n\n# 它是如何工作的…\n\n控制台应用没有图形用户界面，只在终端窗口中显示文本。这通常用于后端系统，因为与小部件应用相比，它使用更少的资源。我们在这个例子中使用了它，因为它显示结果更快，不需要在程序中放置任何小部件，在这种情况下我们不需要。\n\n我们将 SQL 查询分成不同的函数，这样更容易维护代码，也不会变得太乱。请注意，在 C++ 中，函数必须位于`main()`函数之前，否则它们将无法被`main()`调用。\n\n# 还有更多…\n\n上例中使用的`INNER JOIN`语句将两个表连接在一起，并从两个表中选择所有行，只要两个表中的列之间匹配。在 MySQL(以及一些其他类型的 SQL 架构)中，还有很多其他类型的`JOIN`语句可以使用，比如`LEFT JOIN`、`RIGHT JOIN`和`FULL OUTER JOIN`。\n\n下图显示了不同类型的`**JOIN**`语句及其效果:\n\n![](img/c7aa3a31-4002-4774-aa69-c132c16020a8.png)\n\n以下要点解释了`LIKE`和`DISTINCT`语句，我们在本食谱的示例代码中使用了这些语句:\n\n*   `LIKE`语句通常用于在数据库中搜索没有完整单词的字符串变量。请注意，有两个`%`符号，位于`search`关键字的前后。\n*   上例中使用的`DISTINCT`语句过滤掉具有完全相同变量的结果。比如没有`DISTINCT`语句，你会看到终端出现两个版本的拉里·金，因为有他在同一天登录系统的两条记录。通过添加`DISTINCT`语句，MySQL 将删除其中一个结果，并确保每个结果都是唯一的。\n*   你可能想知道`d-MMMM-yyyy`代表什么，为什么我们在前面的例子中使用它。这实际上是一个提供给 Qt 中`QDateTime`类的表达式，用于使用给定的格式显示日期时间结果。在这种情况下，它会将我们从 MySQL 获得的日期时间数据(2019-08-06)更改为我们指定的格式，结果是 2019 年 8 月 6 日。\n\n有关更多信息，请查看 Qt 在[http://doc.qt.io/qt-5/qdatetime.html#toString](http://doc.qt.io/qt-5/qdatetime.html#toString)的文档，其中包含可用于确定日期和时间字符串格式的表达式的完整列表。"
  },
  {
    "path": "docs/qt5-cpp-gui-prog-cb/12.md",
    "content": "# 十二、使用 Qt 网络引擎开发网络应用\n\n在本章中，我们将介绍以下食谱:\n\n*   介绍 Qt 网络引擎\n*   使用网络视图和网络设置\n*   在项目中嵌入谷歌地图\n*   从 JavaScript 调用 C++ 函数\n*   从 C++ 调用 JavaScript 函数\n\n# 介绍\n\nQt 包括一个名为 **Qt WebEngine** 的模块，它允许我们在程序中嵌入一个网页浏览器小部件，并使用它来显示网页或本地 HTML 内容。在 5.6 版本之前，Qt 使用了另一个类似的模块，叫做 **Qt WebKit** ，现在已经被弃用了，并且已经被基于 Chromium 的 WebEngine 模块所取代。Qt 还允许 JavaScript 和 C++ 代码之间通过**网络频道**进行通信，这使得我们能够以更有效的方式使用这个模块。\n\n# 技术要求\n\n本章的技术要求包括 Qt 5.11.2 MSVC 2017 64 位、Qt Creator 4.8.2 和 Windows 10。\n\n本章使用的所有代码都可以从本章的 GitHub 资源库下载，网址为[https://GitHub . com/PacktPublishing/Qt5-CPP-GUI-Programming-cook book-第二版/tree/master/Chapter12](https://github.com/PacktPublishing/Qt5-CPP-GUI-Programming-Cookbook-Second-Edition/tree/master/Chapter12) 。\n\n查看以下视频，查看正在运行的代码:[http://bit.ly/2ToK3Do](http://bit.ly/2ToK3Do)\n\n# 介绍 Qt 网络引擎\n\n在这个示例项目中，我们将探索 Qt 中 WebEngine 模块的基本特性，并尝试构建一个简单的工作 web 浏览器。从 Qt 5.6 开始，Qt 的 **WebKit** 模块被弃用，取而代之的是基于谷歌 Chromium 引擎的 **WebEngine** 模块。\n\n# 怎么做…\n\n首先，让我们设置我们的网络引擎项目:\n\n1.  如果计算机上没有安装 Microsoft Visual Studio，请下载并安装它。目前，Qt 的网络引擎模块只适用于 Visual C++ 编译器，而不适用于其他编译器，如 MinGW 或 Clang。这在未来可能会改变，但这完全取决于 Qt 开发人员是否希望将其移植到其他编译器。可以从[https://www.visualstudio.com](https://www.visualstudio.com)下载最新的 **Visual Studio** 。\n\n2.  确保你安装在电脑上的 Qt 版本支持 **Visual C++ 编译器**。您可以使用 Qt 的维护工具将 MSVC 2015 64 位组件添加到 Qt 安装中。此外，确保您已经在 Qt 版本中安装了 Qt 网络引擎组件:\n\n![](img/9ab29fa0-f021-470a-88ea-b42b5aef1b98.png)\n\n3.  打开 Qt 创建器，创建一个新的 Qt 小部件应用项目。选择使用 Visual C++ 编译器的工具包:\n\n![](img/fdaad9cc-27cd-4fa7-a487-1c81cd0a3e8a.png)\n\n4.  打开您的项目文件(`.pro`)并将以下模块添加到您的项目中:\n\n```cpp\nQT += core gui webengine webenginewidgets\n```\n\n5.  打开`mainwindow.ui`并删除菜单栏、主工具栏和状态栏对象，因为我们在这个项目中不需要这些对象:\n\n![](img/7822a063-b852-442b-908d-c6e4c3548b76.png)\n\n6.  在画布上放置两个水平布局，然后在顶部放置一个线条编辑小部件和一个布局按钮:\n\n![](img/53b6b038-8b28-45fb-a94c-9d8a86e3cf46.png)\n\n7.  选择画布，然后单击位于编辑器顶部的“垂直布局”按钮:\n\n![](img/547b9077-c171-46d0-a526-28df0ec04743.png)\n\n8.  布局将扩展，并遵循主窗口的大小。基于水平布局的宽度，线编辑也将水平扩展:\n\n![](img/d46410ae-bef6-41a7-9cd9-14bda3909b93.png)\n\n9.  在线编辑的左侧添加两个按钮。我们将使用这两个按钮在页面历史之间前后移动。在主窗口的底部添加一个进度条小部件，这样我们就可以知道页面是已经完成加载还是仍在进行中。此时，我们不必担心中间的水平布局，因为稍后我们将在*步骤 16* 使用 C++ 代码向其中添加 webview，空间将被占用:\n\n![](img/f7266ccc-c85f-496b-87e5-062ba28489c8.png)\n\n10.  右键单击其中一个按钮并选择转到插槽…，然后选择单击()并单击确定。在`mainwindow.h`和`mainwindow.cpp`会自动为你创建一个槽功能。对所有其他按钮也重复此步骤。\n11.  右键单击编辑行并选择转到插槽…，然后选择返回按下()并单击确定。现在将在`mainwindow.h`和`mainwindow.cpp`中为您自动创建另一个插槽功能。\n12.  让我们跳到`mainwindow.h`。我们需要做的第一件事就是在`mainwindow.h`中添加以下表头:\n\n```cpp\n#include <QtWebEngineWidgets/QtWebEngineWidgets>\n```\n\n13.  在类析构函数下声明`loadUrl()`函数:\n\n```cpp\npublic:\n    explicit MainWindow(QWidget *parent = 0);\n    ~MainWindow();\n    void loadUrl();\n```\n\n14.  在`mainwindow.h`中添加一个名为`loading()`的自定义槽函数:\n\n```cpp\nprivate slots:\n    void on_goButton_clicked();\n    void on_address_returnPressed();\n    void on_backButton_clicked();\n    void on_forwardButton_clicked();\n    void loading(int progress);\n```\n\n15.  声明一个`QWebEngineView`对象，称之为`webview`:\n\n```cpp\nprivate:\n    Ui::MainWindow *ui;\n    QWebEngineView* webview;\n```\n\n16.  打开`mainwindow.cpp`文件，启动网络引擎视图。将其添加到第二个水平布局，并将其`loadProgress()`信号连接到我们刚刚添加到`mainwindow.h`的`loading()`插槽功能:\n\n```cpp\nMainWindow::MainWindow(QWidget *parent) :\nQMainWindow(parent),\nui(new Ui::MainWindow) {\n    ui->setupUi(this);\n    webview = new QWebEngineView;\n    ui->horizontalLayout_2->addWidget(webview);\n    connect(webview, QWebEngineView::loadProgress, this, MainWindow::loading);\n}\n```\n\n17.  声明调用`loadUrl()`函数时会发生什么:\n\n```cpp\nvoid MainWindow::loadUrl() {\n    QUrl url = QUrl(ui->address->text());\n    url.setScheme(\"http\");\n    webview->page()->load(url);\n}\n```\n\n18.  点击开始按钮或按下进入 T2 键，调用开始功能:\n\n```cpp\nvoid MainWindow::on_goButton_clicked() {\n    loadUrl();\n}\nvoid MainWindow::on_address_returnPressed() {\n    loadUrl();\n}\n```\n\n19.  至于另外两个按钮，我们将要求`webview`加载上一页或下一页，如果它在历史堆栈中可用的话:\n\n```cpp\nvoid MainWindow::on_backButton_clicked() {\n    webview->back();\n}\nvoid MainWindow::on_forwardButton_clicked() {\n    webview->forward();\n}\n```\n\n20.  加载网页时更改`progressBar`的值:\n\n```cpp\nvoid MainWindow::loading(int progress) {\n    ui->progressBar->setValue(progress);\n}\n```\n\n21.  立即构建并运行该程序，您将获得一个非常基本但功能强大的网络浏览器:\n\n![](img/a8cbde08-cf95-4c8f-b398-28cba5f25518.png)\n\n# 它是如何工作的…\n\n旧的 webview 系统基于苹果的 WebKit 引擎，仅在 Qt 5.5 及其前身中提供。从 5.6 开始，WebKit 被 Qt 彻底抛弃，取而代之的是谷歌的 Chromium 引擎。API 已经完全改变，因此一旦迁移到 5.6，所有与 Qt WebKit 相关的代码都将无法正常工作。如果您是 Qt 新手，建议您跳过 WebKit，学习 WebEngine API，因为它正在成为 Qt 的新标准。\n\nIf you have used Qt's WebKit in the past, this webpage teaches you how to port your old code over to Web Engine: [https://wiki.qt.io/Porting_from_QtWebKit_to_QtWebEngine](https://wiki.qt.io/Porting_from_QtWebKit_to_QtWebEngine).\n\n在*步骤 16* 中，我们将属于 webview widget 的`loadProgress()`信号连接到`loading()`槽功能。在*步骤 17* 中，当网页浏览器正在加载您通过调用`QWebEnginePage::load()`请求的网页时，该信号将被自动调用。如果需要，也可以连接`loadStarted()`和`loadFinished()`信号。\n\n在*步骤 17* 中，我们使用`QUrl`类将行编辑获得的文本转换为网址格式。默认情况下，如果我们不指定 URL 方案(HTTP、HTTPS、FTP 等)，我们插入的地址将指向本地路径。如果我们给了它`packtpub.com`而不是`http://packtpub.com`，我们可能无法加载页面。因此，我们通过调用`QUrl::setScheme()`为其手动指定一个 URL 方案。这将确保在将地址传递给 webview 之前，该地址的格式正确。\n\n# 还有更多…\n\n如果您正在运行 Qt 5.6 或更高版本，并且由于某种原因您需要项目的 WebKit 模块(通常用于维护旧项目)，您可以从 GitHub 获取模块代码并自行构建:[https://github.com/qt/qtwebkit](https://github.com/qt/qtwebkit)。\n\n# 使用网络视图和网络设置\n\n在本食谱中，我们将深入研究 Qt 的网络引擎中可用的功能，并探索我们可以用来定制网络视图的设置。我们将使用前面示例中的源文件，并向其中添加更多代码。\n\n# 怎么做…\n\n让我们探索一下 Qt 网络引擎的一些基本特性:\n\n1.  打开`mainwindow.ui`，在进度条下添加垂直布局。向垂直布局添加纯文本编辑小部件(在输入小部件类别下)和按钮。将按钮显示更改为`Load HTML`，并将纯文本编辑小部件的明文属性设置如下:\n\n```cpp\n<Img src=\"https://www.google.cimg/branding/googlelogo/1x/googlelogo_color_272x92dp.png\"></img>\n<h1>Hello World!</h1>\n<h3>This is our custom HTML page.</h3>\n<script>alert(\"Hello!\");</script>\n```\n\n这是在您将代码添加到纯文本编辑的顶部后应该看到的样子:\n\n![](img/4d3ea5b2-e849-41b9-bbff-3c82d6786e24.png)\n\n2.  转到文件|新建文件或项目。将弹出一个窗口，要求您选择一个文件模板。选择 Qt 类别下的 Qt 资源文件，然后点击选择…按钮。键入所需的文件名，然后单击“下一步”，接着单击“完成”:\n\n![](img/6032a6da-f55c-4222-99ee-9055f21c5d9c.png)\n\n3.  通过在“项目”窗格中右键单击资源文件并选择“在编辑器中打开”选项，打开我们刚刚创建的资源文件。编辑器打开文件后，单击添加按钮，然后单击添加前缀。将前缀设置为`/`并点击添加，然后点击添加文件。将出现文件浏览器窗口，我们将选择`tux.png`图像文件并单击打开。图像文件现在被添加到我们的项目中，一旦编译完成，它将被嵌入到可执行文件(`.exe`)中:\n\n![](img/fe0bcce4-f161-45e0-9a2a-4cd07325763f.png)\n\n4.  打开`mainwindow.h`并添加以下标题:\n\n```cpp\n#include <QMainWindow>\n#include <QtWebEngineWidgets/QtWebEngineWidgets>\n#include <QDebug>\n#include <QFile>\n```\n\n5.  确保以下函数和指针已在`mainwindow.h`中声明:\n\n```cpp\npublic:\n    explicit MainWindow(QWidget *parent = 0);\n    ~MainWindow();\n    void loadUrl();\nprivate slots:\n    void on_goButton_clicked();\n    void on_address_returnPressed();\n    void on_backButton_clicked();\n    void on_forwardButton_clicked();\n    void startLoading();\n    void loading(int progress);\n    void loaded(bool ok);\n    void on_loadHtml_clicked();\nprivate:\n    Ui::MainWindow *ui;\n    QWebEngineView* webview;\n```\n\n6.  打开`mainwindow.cpp`，将以下代码添加到类构造函数中:\n\n```cpp\nMainWindow::MainWindow(QWidget *parent) :\nQMainWindow(parent),\nui(new Ui::MainWindow) {\n    ui->setupUi(this);\n    webview = new QWebEngineView;\n    ui->horizontalLayout_2->addWidget(webview);\n    //webview->page()->settings()->setAttribute(QWebEngineSettings::JavascriptEnabled, false);\n    //webview->page()->settings()->setAttribute(QWebEngineSettings::AutoLoadImages, false);\n    //QString fontFamily = webview->page()->settings()->fontFamily(QWebEngineSettings::SerifFont);\n    QString fontFamily = webview->page()->settings()->fontFamily(QWebEngineSettings::SansSerifFont);\n    int fontSize = webview->page()->settings()->fontSize(QWebEngineSettings::MinimumFontSize);\n    QFont myFont = QFont(fontFamily, fontSize);\n    webview->page()->settings()->setFontFamily(QWebEngineSettings::StandardFont, myFont.family());\n```\n\n7.  加载图像文件并将其放在 webview 上:\n\n```cpp\n    QFile file(\"://tux.png\");\n    if (file.open(QFile::ReadOnly)) {\n        QByteArray data = file.readAll();\n        webview->page()->setContent(data, \"img/png\");\n    } else {\n        qDebug() << \"File cannot be opened.\";\n    }\n    connect(webview, QWebEngineView::loadStarted, this, MainWindow::startLoading()));\n    connect(webview, QWebEngineView::loadProgress, this, MainWindow::loading(int)));\n    connect(webview, QWebEngineView::loadFinished, this, MainWindow::loaded(bool)));\n}\n```\n\n8.  `MainWindow::loadUrl()`功能与*介绍 Qt 网络引擎*部分的前一个例子相同，在加载页面之前将网址方案设置为 HTTP:\n\n```cpp\nvoid MainWindow::loadUrl() {\n    QUrl url = QUrl(ui->address->text());\n    url.setScheme(\"http\");\n    webview->page()->load(url);\n}\n```\n\n9.  以下功能也是如此，也与*介绍 Qt 网络引擎*部分中的前一个示例相同:\n\n```cpp\nvoid MainWindow::on_goButton_clicked() {\n    loadUrl();\n}\nvoid MainWindow::on_address_returnPressed() {\n    loadUrl();\n}\nvoid MainWindow::on_backButton_clicked() {\n    webview->back();\n}\nvoid MainWindow::on_forwardButton_clicked() {\n    webview->forward();\n}\n```\n\n10.  增加`MainWindow::startLoading()`和`MainWindow::loaded()`槽功能，由`loadStarted()`和`loadFinished()`信号调用。这两个功能基本上在页面开始加载时显示进度条，在页面加载完成时隐藏进度条:\n\n```cpp\nvoid MainWindow::startLoading() {\n    ui->progressBar->show();\n}\nvoid MainWindow::loading(int progress) {\n    ui->progressBar->setValue(progress);\n}\nvoid MainWindow::loaded(bool ok) {\n    ui->progressBar->hide();\n}\n```\n\n11.  点击`Load HTML`按钮，调用`webview->loadHtml()`将纯文本转换为 HTML 内容:\n\n```cpp\nvoid MainWindow::on_loadHtml_clicked() {\n    webview->setHtml(ui->source->toPlainText());\n}\n```\n\n12.  构建并运行程序，您应该会看到如下内容:\n\n![](img/a299388a-630d-4400-8c36-6b69324abdab.png)\n\n# 它是如何工作的…\n\n在本例中，我们使用 C++ 加载了一个图像文件，并将其设置为 WebView 的默认内容(而不是一个空白页)。我们可以通过在启动时加载一个带有图像的默认 HTML 文件来获得同样的结果。\n\n类构造函数中的一些代码已被注释掉。您可以删除双斜线`//`，看看它有什么不同–JavaScript 警报将不再出现(因为 JavaScript 被禁用)，图像将不再出现在您的网络视图中。\n\n另外一个可以尝试的就是将字体家族从`QWebEngineSettings::SansSerifFont`改为`QWebEngineSettings::SerifFont`。您会注意到 webview 中出现的字体略有不同:\n\n![](img/d7d29cc5-c893-4316-be3b-e77c969986f4.png)\n\n通过点击`Load HTML`按钮，我们要求 WebView 将纯文本编辑小部件的内容视为 HTML 代码，并将其加载为 HTML 页面。你可以用它来制作一个由 Qt 驱动的简单的 HTML 编辑器！\n\n# 在项目中嵌入谷歌地图\n\n在这个食谱中，我们将学习如何通过 Qt 的网络引擎模块将**谷歌地图**嵌入到我们的项目中。这个例子不太关注 Qt 和 C++，而是关注 HTML 代码中的**谷歌地图应用编程接口**。\n\n# 怎么做…\n\n让我们按照以下步骤创建一个显示谷歌地图的程序:\n\n1.  创建一个新的 Qt 小部件应用项目，并删除状态栏、菜单栏和主工具栏。\n2.  打开您的项目文件(`.pro`)并将以下模块添加到您的项目中:\n\n```cpp\nQT += core gui webengine webenginewidgets\n```\n\n3.  打开`mainwindow.ui`并在画布上添加垂直布局。然后，选择画布并点按画布顶部的“垂直布局”按钮。您将获得以下信息:\n\n![](img/b0142824-fec4-4747-95e0-30bdfcf8c3db.png)\n\n4.  打开`mainwindow.cpp`并在源代码顶部添加以下标题:\n\n```cpp\n#include <QtWebEngineWidgets/QWebEngineView>\n#include <QtWebEngineWidgets/QWebEnginePage>\n#include <QtWebEngineWidgets/QWebEngineSettings>\n```\n\n5.  将以下代码添加到`MainWindow`构造函数中:\n\n```cpp\nMainWindow::MainWindow(QWidget *parent) :\nQMainWindow(parent),\nui(new Ui::MainWindow) {\n    ui->setupUi(this);\n    QWebEngineView* webview = new QWebEngineView;\n    QUrl url = QUrl(\"qrc:/map.html\");\n    webview->page()->load(url);\n    ui->verticalLayout->addWidget(webview);\n}\n```\n\n6.  转到文件|新文件或项目，并创建一个 Qt 资源文件(`.qrc`)。我们将在我们的项目中添加一个名为`map.html`的 HTML 文件:\n\n![](img/76c75c65-69fd-42f5-822a-823e06b7de77.png)\n\n7.  用你喜欢的文本编辑器打开`map.html`。不建议使用 Qt Creator 打开一个 HTML 文件，因为它没有为 HTML 语法提供任何颜色编码。\n8.  通过声明重要标签开始编写 HTML 代码，如`<html>`、`<head>`、`<body>`:\n\n```cpp\n<!DOCTYPE html>\n<html>\n    <head>\n    </head>\n    <body ondragstart=\"return false\">\n    </body>\n</html>\n```\n\n9.  在正文中添加一个`<div>`标签，并将其 ID 设置为`map-canvas`:\n\n```cpp\n<body ondragstart=\"return false\">\n    <div id=\"map-canvas\" />\n</body>\n```\n\n10.  在 HTML 文档的头部添加以下代码:\n\n```cpp\n<meta name=\"viewport\" content=\"initial-scale=1.0, user-scalable=no\" />\n<style type=\"text/css\">\n    html { height: 100% }\n    body { height: 100%; margin: 0; padding: 0 }\n    #map-canvas { height: 100% }\n</style>\n<script type=\"text/javascript\" src=\"https://maps.googleapis.com/maps/api/js?key=YOUR_KEY_HERE&libraries=drawing\"></script>\n```\n\n11.  将下面的代码添加到 HTML 文档的头部，在我们上一步插入的代码下面:\n\n```cpp\n<script type=\"text/javascript\">\n    var map;\n    function initialize() {\n        // Add map\n        var mapOptions = {\n            center: new google.maps.LatLng(40.705311, -74.2581939), zoom: 6\n        };\n        map = new google.maps.Map(document.getElementById(\"map-canvas\"),mapOptions);\n        // Add event listener\n        google.maps.event.addListener(map, 'zoom_changed', function() {\n            //alert(map.getZoom());\n        });\n```\n\n12.  创建标记并将其放置在地图上:\n\n```cpp\n        // Add marker\n        var marker = new google.maps.Marker({\n            position: new google.maps.LatLng(40.705311, -74.2581939), map: map, title: \"Marker A\",\n        });\n        google.maps.event.addListener (marker, 'click', function() {\n            map.panTo(marker.getPosition());\n        });\n        marker.setMap(map);\n```\n\n13.  向地图添加多段线:\n\n```cpp\n        // Add polyline\n        var points = [ new google.maps.LatLng(39.8543, -73.2183), new google.maps.LatLng(41.705311, -75.2581939), new google.maps.LatLng(40.62388, -75.5483) ];\n        var polyOptions = {\n            path: points,\n            strokeColor: '#FF0000',\n            strokeOpacity: 1.0,\n            strokeWeight: 2\n        };\n        historyPolyline = new google.maps.Polyline(polyOptions);\n        historyPolyline.setMap(map);\n```\n\n14.  添加多边形形状:\n\n```cpp\n        // Add polygon\n        var points = [ new google.maps.LatLng(37.314166, -75.432), new google.maps.LatLng(40.2653, -74.4325), new google.maps.LatLng(38.8288, -76.5483) ];\n        var polygon = new google.maps.Polygon({\n            paths: points,\n            fillColor: '#000000',\n            fillOpacity: 0.2,\n            strokeWeight: 3,\n            strokeColor: '#fff000',\n        });\n        polygon.setMap(map);\n```\n\n15.  创建图形管理器并将其应用于地图:\n\n```cpp\n        // Setup drawing manager\n        var drawingManager = new google.maps.drawing.DrawingManager();\n        drawingManager.setMap(map);\n    }\n    google.maps.event.addDomListener(window, 'load', initialize);\n</script>\n```\n\n16.  编译并运行项目。您应该会看到以下内容:\n\n![](img/3e10a755-473a-45fd-9647-df7cf0fef747.png)\n\n# 它是如何工作的…\n\n谷歌允许你使用他们的 JavaScript 库在网页中嵌入谷歌地图，这个库叫做**谷歌地图应用编程接口**。通过 Qt 的 WebEngine 模块，我们可以将一个 HTML 文件加载到我们的 webview 小部件中，从而将谷歌地图嵌入到我们的 C++ 项目中，该小部件使用谷歌地图 API。这种方法的唯一缺点是，当没有互联网连接时，我们无法加载地图。\n\n只要谷歌允许，你的网站可以调用谷歌应用编程接口。如果你的计划是为了更大的流量，选择免费的应用编程接口。\n\n前往[https://console.developers.google.com](https://console.developers.google.com)获取一个免费密钥，用你从谷歌获得的 API 密钥替换 JavaScript 源代码路径中的`YOUR_KEY_HERE`。\n\n我们必须定义一个`<div>`对象，作为地图的容器。然后，当我们初始化地图时，我们指定`<div>`对象的标识，这样谷歌地图应用编程接口就知道嵌入地图时要寻找哪个 HTML 元素。默认情况下，我们将地图的中心设置为纽约的坐标，并将默认缩放级别设置为`6`。然后，我们添加一个事件侦听器，当地图的缩放级别发生变化时，它就会被触发。从代码中删除双斜线`//`，以查看它的运行情况。\n\n之后，我们通过 JavaScript 在地图上添加了一个标记。标记还附带一个事件监听器，点击标记时会触发`panTo()`功能。它基本上将地图视图平移到已单击的标记。虽然我们已经将绘图管理器添加到地图中(地图和卫星按钮旁边的图标按钮)，允许用户在地图上绘制任何类型的形状，但是也可以使用 JavaScript 手动添加形状，类似于我们在*步骤 12* 中添加标记的方式，在*如何操作中...*节。\n\n最后，你可能已经注意到标题被添加到`mainwindow.cpp`而不是`mainwindow.h`。这完全没问题，除非你在`mainwindow.h`中声明类指针——然后你必须在其中包含那些头。\n\n# 从 JavaScript 调用 C++ 函数\n\n在这个食谱中，我们将学习如何使用我们的知识，并使用 Qt 和 MySQL 创建一个功能登录屏幕。\n\n# 怎么做…\n\n让我们学习如何使用以下步骤从 JavaScript 调用 C++ 函数:\n\n1.  创建一个 Qt 小部件应用项目。打开项目文件(`.pro`)并向项目中添加以下模块:\n\n```cpp\nQT += core gui webengine webenginewidgets\n```\n\n2.  打开`mainwindow.ui`并删除主工具栏、菜单栏和状态栏，因为在这个示例程序中我们不需要这些。\n\n3.  向画布添加垂直布局，然后选择画布，并点按画布顶部的“垂直布局”按钮。在垂直布局的顶部添加一个文本标签，并将其文本设置为`Hello!`。通过如下设置样式表属性来增大字体:\n\n```cpp\nfont: 75 26pt \"MS Shell Dlg 2\";\n```\n\n这是我们将字体属性应用到样式表后的样子:\n\n![](img/534f9df2-539b-4b94-84ea-ff70441427aa.png)\n\n4.  转到文件|新建文件或项目并创建资源文件。将一个空的 HTML 文件以及所有属于 **jQuery** 、**booster**和 **Font Awesome** 的 JavaScript 文件、CSS 文件、字体文件等添加到您的项目资源中:\n\n![](img/901ac6ff-7b9f-4af6-b3e4-c5a5c31b611a.png)\n\n5.  打开你的 HTML 文件，在这种情况下叫做`test.html`。将所有必要的 JavaScript 和 CSS 文件链接到 HTML 源代码，在`<head>`标签之间:\n\n```cpp\n<!DOCTYPE html>\n<html>\n    <head>\n        <script src=\"qrc:///qtwebchannel/qwebchannel.js\"></script>\n        <script src=\"js/jquery.min.js\"></script>\n        <script src=\"js/bootstrap.js\"></script>\n        <link rel=\"stylesheet\" type=\"text/css\" href=\"css/bootstrap.css\">\n        <link rel=\"stylesheet\" type=\"text/css\" href=\"css/font-awesome.css\">\n    </head>\n    <body>\n    </body>\n</html>\n```\n\n6.  将以下 JavaScript 添加到`<head>`元素中，包装在`<script>`标签之间:\n\n```cpp\n<script>\n    $(document).ready(function() {\n        new QWebChannel(qt.webChannelTransport, function(channel) {\n            mainWindow = channel.objects.mainWindow;\n        });\n        $(\"#login\").click(function(e) {\n            e.preventDefault();\n            var user = $(\"#username\").val();\n            var pass = $(\"#password\").val();\n            mainWindow.showLoginInfo(user, pass);\n        });\n```\n\n7.  点击`changeText`按钮时打印`Good bye!`，代码如下:\n\n```cpp\n        $(\"#changeText\").click(function(e) {\n            e.preventDefault();\n            mainWindow.changeQtText(\"Good bye!\");\n        });\n    });\n</script>\n```\n\n8.  向`<body>`元素添加以下代码:\n\n```cpp\n<div class=\"container-fluid\">\n    <form id=\"example-form\" action=\"#\" class=\"container-fluid\">\n        <div class=\"form-group\">\n            <div class=\"col-md-12\"><h3>Call C++ Function from Javascript</h3></div>\n            <div class=\"col-md-12\">\n                <div class=\"alert alert-info\" role=\"alert\">\n                    <i class=\"fa fa-info-circle\"></i>\n                    <span id=\"infotext\">Click \"Login\" to send username and password variables to C++. Click \"Change Cpp Text\" to change the text label on Qt GUI.</span>\n                </div>\n            </div>\n```\n\n9.  继续前面的代码，这次我们为`username`和`password`创建了输入字段，底部有两个按钮，分别叫做`Login`和`Change Cpp Text`:\n\n```cpp\n            <div class=\"col-md-12\">\n                <label>Username:</label>\n                <input id=\"username\" type=\"text\"><p />\n            </div>\n            <div class=\"col-md-12\">\n                <label>Password:</label> <input id=\"password\" type=\"password\"><p />\n            </div>\n            <div class=\"col-md-12\">\n                <button id=\"login\" class=\"btn btn-success\" type=\"button\"><i class=\"fa fa-check\"></i> Login</button> <button id=\"changeText\" class=\"btn btn-primary\" type=\"button\"> <i class=\"fa fa-pencil\"></i> Change Cpp Text</button>\n            </div>\n        </div>\n    </form>\n</div>\n```\n\n10.  打开`mainwindow.h`，将以下公共功能添加到`MainWindow`类中:\n\n```cpp\npublic:\n    explicit MainWindow(QWidget *parent = 0);\n    ~MainWindow();\n    Q_INVOKABLE void changeQtText(QString newText);\n    Q_INVOKABLE void showLoginInfo(QString user, QString pass);\n```\n\n11.  打开`mainwindow.cpp`并在源代码顶部添加以下标题:\n\n```cpp\n#include <QtWebEngineWidgets/QWebEngineView>\n#include <QtWebChannel/QWebChannel>\n#include <QMessageBox>\n```\n\n12.  将以下代码添加到`MainWindow`构造函数中:\n\n```cpp\nMainWindow::MainWindow(QWidget *parent) :\nQMainWindow(parent),\nui(new Ui::MainWindow) {\n    qputenv(\"QTWEBENGINE_REMOTE_DEBUGGING\", \"1234\");\n    ui->setupUi(this);\n    QWebEngineView* webview = new QWebEngineView();\n    ui->verticalLayout->addWidget(webview);\n    QWebChannel* webChannel = new QWebChannel();\n    webChannel->registerObject(\"mainWindow\", this);\n    webview->page()->setWebChannel(webChannel);\n    webview->page()->load(QUrl(\"qrc:///html/test.html\"));\n}\n```\n\n13.  声明调用`changeQtText()`和`showLoginInfo()`时会发生什么:\n\n```cpp\nvoid MainWindow::changeQtText(QString newText) {\n    ui->label->setText(newText);\n}\nvoid MainWindow::showLoginInfo(QString user, QString pass) {\n    QMessageBox::information(this, \"Login info\", \"Username is \" + user + \" and password is \" + pass);\n}\n```\n\n14.  编译并运行程序；您应该会看到类似于下面截图的内容。如果你点击更改 Cpp 文本按钮，你好！顶部将改为再见！如果您单击登录按钮，将出现一个消息框，向您显示您在用户名和密码输入字段中键入的内容:\n\n![](img/e752452d-c14d-48de-bb82-9dc5d4ad8141.png)\n\n# 它是如何工作的…\n\n在这个例子中，我们使用了两个 JavaScript 库:jQuery 和 Bootstrap。我们还使用了一个名为 Font Awesome 的标志性字体包。这些第三方插件被用来使 HTML 用户界面更加有趣，并对不同的屏幕分辨率做出响应。我们还使用 jQuery 来检测文档的就绪状态，以及获取输入字段的值。\n\nYou can download jQuery from [https://jquery.com/download](https://jquery.com/download), Bootstrap from [http://getbootstrap.com/getting-started/#download](http://getbootstrap.com/getting-started/#download), and Font Awesome from [http://fontawesome.io](http://fontawesome.io).\n\nQt 的 WebEngine 使用了一种叫做 **WebChannel** 的机制，这种机制可以实现 C++ 程序和 HTML 页面之间的对等通信。网络引擎模块提供了一个 JavaScript 库，使得集成变得更加容易。默认情况下，JavaScript 嵌入到项目的资源中，因此不需要手动将其导入到项目中。您只需通过调用以下命令将其包含在您的 HTML 页面中:\n\n```cpp\n<script src=\"qrc:///qtwebchannel/qwebchannel.js\"></script>\n```\n\n一旦包含了`qwebchannel.js`，就可以初始化`QWebChannel`类，并将我们之前在 C++ 中注册的 Qt 对象分配给一个 JavaScript 变量。\n\n在 C++ 中，这是按如下方式完成的:\n\n```cpp\nQWebChannel* webChannel = new QWebChannel();\nwebChannel->registerObject(\"mainWindow\", this);\nwebview->page()->setWebChannel(webChannel);\n```\n\n然后在 JavaScript 中，这是按如下方式完成的:\n\n```cpp\nnew QWebChannel(qt.webChannelTransport, function(channel) {\n    mainWindow = channel.objects.mainWindow;\n});\n```\n\n你可能想知道这句话是什么意思:\n\n```cpp\nqputenv(\"QTWEBENGINE_REMOTE_DEBUGGING\", \"1234\");\n```\n\nQt 的 WebEngine 使用远程调试方法来检查 JavaScript 错误和其他问题。数字`1234`定义了您想要用于远程调试的端口号。\n\n启用远程调试后，打开基于 Chromium 的网页浏览器即可进入调试页面，如**谷歌 Chrome** (这在**火狐**等浏览器中无法使用)，输入`http://127.0.0.1:1234`。然后，您将看到如下页面:\n\n![](img/1f6699e2-b4be-40a9-9834-3ae9b8e487d7.png)\n\n第一页将显示你的程序中当前运行的所有 HTML 页面，在这种情况下是`test.html`。点击页面链接，它会把你带到另一个页面进行检查。您可以用它来检查 CSS 错误、JavaScript 错误和丢失的文件。请注意，一旦您的程序没有错误并准备好部署，就应该禁用远程调试。这是因为远程调试需要时间来初始化，并且会增加程序的启动时间。\n\n如果要从 JavaScript 调用 C++ 函数，必须将`Q_INVOKABLE`宏放在函数声明的前面；否则，它将不起作用:\n\n```cpp\nQ_INVOKABLE void changeQtText(QString newText);\n```\n\n# 从 C++ 调用 JavaScript 函数\n\n在之前的食谱中，我们学习了如何通过 Qt 的 **WebChannel** 系统从 JavaScript 调用 C++ 函数。在这个例子中，我们将尝试做相反的事情:从 C++ 代码中调用 JavaScript 函数。\n\n# 怎么做…\n\n我们可以通过以下步骤从 C++ 调用 JavaScript 函数:\n\n1.  创建一个新的 Qt 小部件应用项目，并将`webengine`和`webenginewidgets`模块添加到您的项目中。\n2.  打开`mainwindow.ui`并移除主工具栏、菜单栏和状态栏。\n3.  向画布添加垂直布局和水平布局。选择画布并单击垂直布局。确保水平布局位于垂直布局的底部。\n4.  向水平布局添加两个按钮；一个叫做更改 HTML 文本，另一个叫做播放 UI 动画。右键单击其中一个按钮，然后单击转到插槽。会弹出一个窗口，让你选择一个信号。选择单击的()选项，然后单击确定。Qt 会自动给你的源代码添加一个槽函数。对另一个按钮重复此步骤:\n\n![](img/22d465b7-e6f5-426e-862f-7fa9c69adec0.png)\n\n5.  打开`mainwindow.h`并添加以下标题:\n\n```cpp\n#include <QtWebEngineWidgets/QWebEngineView>\n#include <QtWebChannel/QWebChannel>\n#include <QMessageBox>\n```\n\n6.  声明名为`webview`的`QWebEngineView`对象的类指针:\n\n```cpp\npublic:\n    explicit MainWindow(QWidget *parent = 0);\n    ~MainWindow();\n    QWebEngineView* webview;\n```\n\n7.  打开`mainwindow.cpp`并向`MainWindow`构造函数添加以下代码:\n\n```cpp\nMainWindow::MainWindow(QWidget *parent) :\nQMainWindow(parent),\nui(new Ui::MainWindow) {\n    //qputenv(\"QTWEBENGINE_REMOTE_DEBUGGING\", \"1234\");\n    ui->setupUi(this);\n    webview = new QWebEngineView();\n    ui->verticalLayout->addWidget(webview);\n    QWebChannel* webChannel = new QWebChannel();\n    webChannel->registerObject(\"mainWindow\", this);\n    webview->page()->setWebChannel(webChannel);\n    webview->page()->load(QUrl(\"qrc:///html/test.html\"));\n}\n```\n\n8.  定义当点击`changeHtmlText`按钮和`playUIAnimation`按钮时会发生什么:\n\n```cpp\nvoid MainWindow::on_changeHtmlTextButton_clicked() {\n    webview->page()->runJavaScript(\"changeHtmlText('Text has been replaced by C++!');\");\n}\n\nvoid MainWindow::on_playUIAnimationButton_clicked() {\n    webview->page()->runJavaScript(\"startAnim();\");\n}\n```\n\n9.  让我们转到文件|新文件或项目，为我们的项目创建一个资源文件。选择 Qt 类别下的 Qt 资源文件，然后单击选择....插入所需的文件名，单击“下一步”，然后单击“完成”。\n10.  向项目资源中添加一个空的 HTML 文件和所有必需的附加组件(jQuery、Bootstrap 和 Font Awesome)。将`tux.png`图像文件也添加到资源文件中，因为我们将在*步骤 14* 中使用它。\n\n11.  打开我们刚刚创建的 HTML 文件，将其添加到项目资源中；在我们的例子中，它被称为`test.html`。向文件中添加以下 HTML 代码:\n\n```cpp\n<!DOCTYPE html>\n<html>\n    <head>\n        <script src=\"qrc:///qtwebchannel/qwebchannel.js\"></script>\n        <script src=\"js/jquery.min.js\"></script>\n        <script src=\"js/bootstrap.js\"></script>\n        <link rel=\"stylesheet\" type=\"text/css\" href=\"css/bootstrap.css\">\n        <link rel=\"stylesheet\" type=\"text/css\" href=\"css/font-awesome.css\">\n    </head>\n    <body>\n    </body>\n</html>\n```\n\n12.  将以下包装在`<script>`标签中的 JavaScript 代码添加到我们的 HTML 文件的`<head>`元素中:\n\n```cpp\n<script>\n    $(document).ready(function() {\n        $(\"#tux\").css({ opacity:0, width:\"0%\", height:\"0%\" });\n        $(\"#listgroup\").hide();\n        $(\"#listgroup2\").hide();\n        new QWebChannel(qt.webChannelTransport, function(channel) {\n            mainWindow = channel.objects.mainWindow;\n        });\n    });\n    function changeHtmlText(newText) {\n        $(\"#infotext\").html(newText);\n    }\n```\n\n13.  定义`startAnim()`功能:\n\n```cpp\n    function startAnim() {\n        // Reset\n        $(\"#tux\").css({ opacity:0, width:\"0%\", height:\"0%\" });\n        $(\"#listgroup\").hide();\n        $(\"#listgroup2\").hide();\n        $(\"#tux\").animate({ opacity:1.0, width:\"100%\", height:\"100%\" }, 1000, function() {\n            // tux animation complete\n            $(\"#listgroup\").slideDown(1000, function() {\n                // listgroup animation complete\n                $(\"#listgroup2\").fadeIn(1500);\n            });\n        });\n    }\n</script>\n```\n\n14.  将以下代码添加到我们的 HTML 文件的`<body>`元素中:\n\n```cpp\n<div class=\"container-fluid\">\n    <form id=\"example-form\" action=\"#\" class=\"container-fluid\">\n        <div class=\"form-group\">\n            <div class=\"col-md-12\"><h3>Call Javascript Function from C++ </h3></div>\n            <div class=\"col-md-12\">\n                <div class=\"alert alert-info\" role=\"alert\"><i class=\"fa fa-info-circle\"></i> <span id=\"infotext\"> Change this text using C++.</span></div>\n            </div>\n            <div class=\"col-md-2\">\n                <img id=\"tux\" src=\"tux.png\"></img>\n            </div>\n```\n\n15.  继续编写下面的代码，我们已经在其中添加了一个列表:\n\n```cpp\n            <div class=\"col-md-5\">\n                <ul id=\"listgroup\" class=\"list-group\">\n                    <li class=\"list-group-item\">Cras justoodio</li>\n                    <li class=\"list-group-item\">Dapibus acfacilisis in</li>\n                    <li class=\"list-group-item\">Morbi leorisus</li>\n                    <li class=\"list-group-item\">Porta acconsectetur ac</li>\n                    <li class=\"list-group-item\">Vestibulum ateros</li>\n                </ul>\n            </div>\n            <div id=\"listgroup2\" class=\"col-md-5\">\n                <a href=\"#\" class=\"list-group-item active\">\n                    <h4 class=\"list-group-item-heading\">Item heading</h4>\n                    <p class=\"list-group-item-text\">Cras justo odio</p>\n                </a>\n```\n\n16.  当我们将剩余的项目添加到第二个列表时，代码继续:\n\n```cpp\n                <a href=\"#\" class=\"list-group-item\">\n                    <h4 class=\"list-group-item-heading\">Item heading</h4>\n                    <p class=\"list-group-item-text\">Dapibus ac facilisis in</p>\n                </a>\n                <a href=\"#\" class=\"list-group-item\">\n                    <h4 class=\"list-group-item-heading\">Item heading</h4>\n                    <p class=\"list-group-item-text\">Morbi leo risus</p>\n                </a>\n            </div>\n        </div>\n    </form>\n</div>\n```\n\n17.  构建和运行程序；您应该会得到类似于下面截图中的结果。当您单击“更改 HTML 文本”按钮时，信息文本位于顶部面板中。如果您单击“播放用户界面动画”按钮，企鹅图像以及两组小部件将一个接一个地出现，具有不同的动画:\n\n![](img/97d5119a-2e8a-4b6b-94d8-24bca16c66d5.png)\n\n# 它是如何工作的…\n\n这个例子类似于上一个在*从 JavaScript* 部分调用 C++ 函数的例子。一旦我们包含了**网络频道** JavaScript 库并启动了`QWebChannel`类，我们就可以通过调用`webview->page()->runJavascript(\"jsFunctionNameHere();\")`从 C++ 中调用任何 JavaScript 函数。不要忘记将 C++ 中创建的网络频道也应用到网络视图的页面上；否则，它将无法与您的 HTML 文件中的`QWebChannel`类进行通信。\n\n默认情况下，我们更改企鹅图片的 CSS 属性，将其不透明度设置为`0`，宽度设置为`0%`，高度设置为`0%`。我们还通过调用`hide()` jQuery 函数隐藏了两个列表组。单击“播放用户界面动画”按钮时，我们会再次重复这些步骤，以防动画之前已经播放过(也就是说，之前单击过同一个按钮)，然后我们会再次隐藏它们，以便回放动画。\n\njQuery 的一个强大功能是，您可以定义动画完成后会发生什么，这允许我们按顺序播放动画。在这个例子中，我们从企鹅图像开始，并在一秒钟(1000 毫秒)内将它的 CSS 属性插入到目标设置中。一旦完成，我们开始另一个动画，这使得第一个列表组在一秒钟内从上到下滑动。之后，我们运行第三个动画，使第二个列表组在 1.5 秒内不知从哪里淡入。\n\n为了替换位于顶部面板的信息文本，我们创建了一个名为`changeHtmlText()`的 JavaScript 函数；在函数本身内部，我们通过引用其 ID 并调用`html()`来更改其内容，从而得到了 HTML 元素。"
  },
  {
    "path": "docs/qt5-cpp-gui-prog-cb/13.md",
    "content": "# 十三、性能优化\n\n在本章中，我们将介绍以下食谱:\n\n*   优化表单和 C++\n*   剖析和优化 QML\n*   渲染和动画\n\n# 介绍\n\nQt 5 以其优化的性能而闻名。但是，如果您的代码写得不好，性能问题可能仍然会出现。在向用户发布软件之前，我们有很多方法可以识别这些问题并解决它们。\n\n# 技术要求\n\n本章的技术要求包括 Qt 5.11.2 MinGW 32 位、Qt Creator 4.8.2 和 Windows 10。\n\n本章使用的所有代码都可以从以下 GitHub 链接下载:[https://GitHub . com/PacktPublishing/Qt5-CPP-GUI-编程-cook book-第二版/树/主/第 13 章](https://github.com/PacktPublishing/Qt5-CPP-GUI-Programming-Cookbook-Second-Edition/tree/master/Chapter13)。\n\n查看以下视频，查看正在运行的代码:[http://bit.ly/2FrX5Ls](http://bit.ly/2FrX5Ls)\n\n# 优化表单和 C++\n\n了解如何优化用 C++ 构建的基于表单的 Qt 5 应用非常重要。最好的方法是学习如何衡量和比较不同的方法，并决定哪种方法最适合你。\n\n# 怎么做...\n\n让我们从以下步骤开始:\n\n1.  让我们创建一个`Qt Widgets Application`项目并打开`mainwindow.cpp`。之后，在源代码顶部添加以下标题:\n\n```cpp\n#include <QPushButton>\n#include <QGridLayout>\n#include <QMessageBox>\n#include <QTime>\n#include <QDebug>\n```\n\n2.  创建一个`QGridLayout`对象，并将其父对象设置为`centralWidget`:\n\n```cpp\nMainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow)\n{\n    ui->setupUi(this);\n QGridLayout *layout = new QGridLayout(ui->centralWidget);\n```\n\n3.  创建一个`QTime`对象。我们将用它来衡量我们下一步行动的表现:\n\n```cpp\nQTime* time = new QTime;\ntime->start();\n```\n\n4.  我们将使用两个循环来填充网格布局的 600 个按钮，并在单击时将它们全部连接到一个 lambda 函数。然后，我们将测量经过的时间并打印出结果，如下所示:\n\n```cpp\nfor (int i = 0; i < 40; ++ i) {\n    for (int j = 0; j < 15; ++ j) {\n        QPushButton* newWidget = new QPushButton();\n        newWidget->setText(\"Button\");\n        layout->addWidget(newWidget, i, j);\n        connect(newWidget, QPushButton::clicked,\n        [this]() {\n            QMessageBox::information(this, \"Clicked\", \"Button has been clicked!\");\n        });\n    }\n}\nqDebug() << \"Test GUI:\" << time->elapsed() << \"msec\";\n```\n\n5.  如果我们现在构建并运行这个项目，我们会看到一个窗口充满了很多按钮。当我们点击其中一个时，屏幕上会弹出一个消息框。在我的电脑上创建和布局主窗口上的所有 600 个按钮只花了大约 9 毫秒。当我们移动窗口或调整它的大小时，也没有性能问题，这非常令人印象深刻。它证明了 Qt 5 可以很好地处理这个问题。但是，请注意，您的用户可能使用的是较旧的机器，在设计用户界面时，您可能需要格外小心:\n\n![](img/98fab7b3-4727-4f59-9b6b-58f84a44c9d0.png)\n\n6.  让我们为每个按钮添加一个样式表，如下所示:\n\n```cpp\nQPushButton* newWidget = new QPushButton();\nnewWidget->setText(\"Button\");\nnewWidget->setStyleSheet(\"background-color: blue; color: white;\");\nlayout->addWidget(newWidget, i, j);\n```\n\n7.  再次构建并运行程序。这一次，设置图形用户界面大约花了 75 毫秒。这意味着样式表确实对程序的性能有一些影响:\n\n![](img/e9e94e1f-9ffe-4ee2-af34-fe04556c16dc.png)\n\n8.  完成这些之后，让我们对不同类型的 C++ 容器进行一些性能测试。打开`main.cpp`并添加以下标题:\n\n```cpp\n#include \"mainwindow.h\"\n#include <QApplication>\n#include <QDebug>\n#include <QTime>\n#include <vector>\n#include <QVector>\n```\n\n9.  在`main()`功能之前创建一个`testArray()`功能:\n\n```cpp\nint testArray(int count) {\n    int sum = 0;\n    int *myarray = new int[count];\n    for (int i = 0; i < count; ++ i)\n        myarray[i] = i;\n    for (int j = 0; j < count; ++ j)\n        sum += myarray[j];\n    delete [] myarray;\n    return sum;\n}\n```\n\n10.  创建另一个名为`testVector()`的函数，如下所示:\n\n```cpp\nint testVector(int count) {\n    int sum = 0;\n    std::vector<int> myarray;\n    for (int i = 0; i < count; ++ i)\n        myarray.push_back(i);\n    for (int j = 0; j < count; ++ j)\n        sum += myarray.at(j);\n    return sum;\n}\n```\n\n11.  完成后，继续创建另一个名为`testQtVector()`的函数:\n\n```cpp\nint testQtVector(int count) {\n    int sum = 0;\n    QVector<int> myarray;\n    for (int i = 0; i < count; ++ i)\n        myarray.push_back(i);\n    for (int j = 0; j < count; ++ j)\n        sum += myarray.at(j);\n    return sum;\n}\n```\n\n12.  在`main()`函数中，定义一个`QTime`对象和一个名为`lastElapse` **的整数变量:**\n\n```cpp\nint main(int argc, char *argv[]) { \n    QApplication a(argc, argv);\n    MainWindow w;\n    w.show();\n\n QTime* time = new QTime;\n time->start();\n int lastElapse = 0;\n```\n\n13.  我们将调用我们在前面步骤中创建的三个函数来测试它们的性能:\n\n```cpp\nint result = testArray(100000000);\nqDebug() << \"Array:\" << (time->elapsed() - lastElapse) << \"msec\";\nlastElapse = time->elapsed();\n\nint result2 = testVector(100000000);\nqDebug() << \"STL vector:\" << (time->elapsed() - lastElapse) << \"msec\";\nlastElapse = time->elapsed();\n\nint result3 = testQtVector(100000000);\nqDebug() << \"Qt vector:\" << (time->elapsed() - lastElapse) << \"msec\";\nlastElapse = time->elapsed();\n```\n\n14.  立即构建并运行程序；我们将看到这些容器的性能差异。在我的电脑上，执行数组需要 650 毫秒，`STL vector`大约需要 3830 毫秒，`Qt vector`大约需要 5400 毫秒。因此，阵列仍然是产生最佳性能的容器，尽管与其他两个相比它缺乏特性。令人惊讶的是，Qt 自己的向量类的工作速度比 C++ 标准库提供的向量容器稍慢。\n\n# 它是如何工作的...\n\n创建`Qt Widgets Application`项目时，尝试执行以下操作来提高性能:\n\n*   避免向堆叠的小部件中添加太多页面并用小部件填充，因为 Qt 需要在渲染过程和事件处理过程中递归地找到所有页面，这将极大地影响程序的性能。\n*   请注意，`QWidget`类使用光栅引擎，一种软件渲染来渲染小部件，而不是使用图形处理器。然而，它足够轻，可以在大多数情况下保持良好的性能。或者，您应该考虑使用 QML 来代替您的程序的图形用户界面，因为它是完全硬件加速的。\n\n*   如果小部件不需要，关闭鼠标跟踪、桌面跟踪和其他事件捕获。这些跟踪和捕获会增加程序的 CPU 使用量:\n\n![](img/e78ceb77-1e3d-4d1c-8a17-0fec3b314e4e.png)\n\n*   保持你的样式表尽可能的简单。大型样式表需要更长的时间让 Qt 将信息解析到渲染系统中，这也会影响性能。\n*   不同的 C++ 容器产生不同的速度，如我们在前面的例子中所示。令人惊讶的是，Qt 的向量容器比 STL(c++ 标准库)的向量容器稍慢。总的来说，好的旧 C++ 数组仍然是最快的，但是不提供排序功能。使用最符合你要求的东西。\n*   对于大型操作，尽可能使用异步方法，因为它不会停止主进程并保持程序平稳运行。\n*   多线程非常适合在并行事件循环中运行不同的操作。然而，如果做得不好，它也会变得非常丑陋，例如，频繁地创建和销毁线程，或者没有计划好的线程间通信。\n*   除非绝对必要，尽量避免使用网络引擎。这是因为在你的程序中嵌入一个完整的网络浏览器是非常沉重和过分的，尤其是对于一个小的应用。如果您想创建以用户界面为中心的软件，您可以考虑使用 QML，而不是制作混合应用。\n*   通过像我们在前面的示例项目中所做的那样进行性能测试，您可以很容易地确定哪种方法是您的项目的最佳选择，以及如何使您的程序表现得更好。\n\n# 剖析和优化 QML\n\nQt 5 中的 QML 引擎利用硬件加速，使其渲染能力和性能优于旧的小部件用户界面。然而，这并不意味着你不应该关心优化；这是因为随着时间的推移，小的性能问题可能会滚雪球般变成更大的问题，并对您的产品声誉造成损害。\n\n# 怎么做...\n\n按照以下步骤开始分析和优化 QML 应用:\n\n1.  让我们创建一个 Qt 快速应用-空项目:\n\n![](img/27185062-1a39-4934-a1d9-60dbc4723616.png)\n\n2.  然后，转到分析| QML 探查器并运行 QML 探查器工具:\n\n![](img/12b02770-be32-41e4-8951-f794618e67c7.png)\n\n3.  你的 Qt Quick 项目将由 QML 评测器运行。“QML 探查器”窗口也将出现在代码编辑器下。程序通过测试点后，单击位于 QML 探查器窗口顶部栏的停止按钮，在本例中，成功创建了空窗口:\n\n![](img/5ac906f6-4416-4216-93b3-cb8006b70727.png)\n\n4.  停止探查器分析后，时间线将显示在“QML 探查器”窗口下的“时间线”选项卡上。您可以在三个选项卡之间切换，即时间线、火焰图和统计。您可以在 QML 探查器窗口底部的不同选项卡之间切换:\n\n![](img/ce3f2040-14a3-48b3-909f-8c6b965433a7.png)\n\n5.  让我们看看时间表选项卡。我们可以在时间轴显示下看到五个不同的类别:场景图、内存使用、编译、创建和绑定。这些类别为我们提供了程序执行过程中不同阶段和过程的概述。我们还可以看到时间线上正在显示一些彩色条。让我们单击“创建”类别下显示 QtQuick.Window/Window.的一个栏。单击后，我们将能够看到此操作的总持续时间以及矩形窗口上显示的代码位置，该窗口位于 QML 探查器窗口的顶部:\n\n![](img/4a74b84a-9345-41e0-8c66-28979ed815ef.png)\n\n6.  完成后，让我们继续打开火焰图选项卡。在火焰图选项卡下，您将看到应用的总时间、内存和分配的可视化百分比。您可以通过单击位于 QML 探查器窗口右上角的选择框，在总时间、内存和分配之间切换:\n\n![](img/26e3c8f8-71fb-4065-a1c7-6a83fdf81e59.png)\n\n7.  不仅如此，您还会看到 QML 代码编辑器上显示的百分比值:\n\n![](img/5a5ec1a7-3409-47eb-83f5-9bd80f63d83c.png)\n\n8.  打开“QML 探查器”窗口下的“统计”类别。该选项卡主要以表格形式向我们显示有关流程的信息:\n\n![](img/b45d21b3-ae04-4126-babc-144011e56a35.png)\n\n# 它是如何工作的...\n\n这类似于我们在前面使用 C++ 和小部件的示例项目中所做的，除了这一次，它是由 Qt 5 提供的 QML Profiler 工具自动分析的。\n\nQML 事件探查器不仅生成用于运行特定进程的总时间，还显示内存分配、应用的执行时间表以及其他信息，这些信息可以让您深入了解软件的性能。\n\n通过查看 QML 分析器分析的数据，您将能够发现代码的哪一部分降低了程序的速度，从而使您能够快速修复任何问题。\n\n在编写 QML 时，有一些规则需要注意，以避免性能瓶颈。例如，类型转换有时会很昂贵，尤其是在不紧密匹配的类型之间(例如，字符串到数字)。当你的项目随着时间的推移变得越来越大时，像这样的小问题很可能会变成瓶颈。\n\n除此之外，尽量不要在经常运行的代码块中多次使用`id`进行项目查找，如下例所示:\n\n```cpp\nItem {\n    width: 400\n    height: 400\n    Rectangle {\n        id: rect\n        anchors.fill: parent\n        color: \"green\"\n    }\n    Component.onCompleted: {\n        for (var i = 0; i < 1000; ++ i) {\n            console.log(\"red\", rect.color.r);\n            console.log(\"green\", rect.color.g);\n            console.log(\"blue\", rect.color.b);\n            console.log(\"alpha\", rect.color.a);\n        }\n}\n```\n\n相反，我们可以使用一个变量来缓存数据，并避免一次又一次地对同一项进行多次查找:\n\n```cpp\nComponent.onCompleted: {\n var rectColor = rect.color;\n    for (var i = 0; i < 1000; ++ i) {\n        console.log(\"red\", rectColor.r);\n        console.log(\"green\", rectColor.g);\n        console.log(\"blue\", rectColor.b);\n        console.log(\"alpha\", rectColor.a);\n    }\n}\n```\n\n此外，如果您更改绑定表达式的属性，尤其是在循环中，Qt 将被迫重复重新计算它。这将导致一些性能问题。用户应该遵循下一个代码片段，而不是这样做:\n\n```cpp\nItem {\n    id: myItem\n    width: 400\n    height: 400\n property int myValue: 0 Text {anchors.fill: parent\n text: myItem.myValue.toString() }\n    Component.onCompleted: {\n        for (var i = 0; i < 1000; ++ i) {\n myValue += 1;\n        }\n    }\n}\n```\n\n相反，我们可以使用一个临时变量来存储`myValue`的数据，然后在循环完成后将最终结果应用回`myValue`:\n\n```cpp\nComponent.onCompleted: {\n var temp = myValue;\n    for (var i = 0; i < 1000; ++ i) {\n temp += 1;\n    }\n myValue = temp;\n}\n```\n\n考虑使用锚点来定位用户界面项，而不是使用绑定。使用绑定进行项目定位非常缓慢且低效，尽管它允许最大的灵活性。\n\n# 渲染和动画\n\n对于渲染图形和动画的应用，良好的性能至关重要。当图形在屏幕上动画显示不流畅时，用户很容易注意到任何性能问题。在下面的例子中，我们将研究如何进一步优化图形密集型 Qt Quick 应用。\n\n# 怎么做...\n\n要了解如何在 QML 渲染动画，请遵循以下示例:\n\n1.  创建一个 Qt 快速应用-空项目。然后，右键单击我们项目面板下的资源图标，并将`tux.png`添加到我们项目的资源中:\n\n![](img/b9f3dced-a986-456f-9b52-584f94437f8e.png)\n\n2.  打开`main.qml`，将窗口大小改为`650 x 650`。我们还将把`id`添加到`Window`项目中，并将其命名为`window`:\n\n```cpp\nWindow {\n    id: window\n    visible: true\n width: 650\n height: 650\n```\n\n3.  在`Window`项内添加以下代码:\n\n```cpp\nproperty int frame: 0;\nonAfterRendering: { frame++ ; }\n\nTimer {\n    id: timer\n    interval: 1000\n    running: true\n    repeat: true\n    onTriggered: { frame = 0; }\n}\n```\n\n4.  紧接着，在下面加上`Repeater`和`Image`:\n\n```cpp\nRepeater {\n        model: 10\n        delegate:\n            Image {\n                id: tux\n                source: \"tux.png\"\n                sourceSize.width: 50\n                sourceSize.height: 60\n                width: 50\n                height: 60\n                smooth: false\n                antialiasing: false\n                asynchronous: true\n```\n\n5.  我们将继续并添加以下代码:\n\n```cpp\nproperty double startX: Math.random() * 600;\nproperty double startY: Math.random() * 600;\nproperty double endX: Math.random() * 600;\nproperty double endY: Math.random() * 600;\nproperty double speed: Math.random() * 3000 + 1000;\n\nRotationAnimation on rotation{\n    loops: Animation.Infinite\n    from: 0\n    to: 360\n    duration: Math.random() * 3000 + 1000;\n}\n```\n\n6.  完成后，在前面代码的底部添加以下代码:\n\n```cpp\nSequentialAnimation {\n    running: true\n    loops: Animation.Infinite\n    ParallelAnimation {\n        NumberAnimation {\n            target: tux\n            property: \"x\"\n            from: startX\n            to: endX\n            duration: speed\n            easing.type: Easing.InOutQuad\n    }\n```\n\n7.  前面的代码激活了图像的`x`属性。我们需要另一个`NumberAnimation`属性来激活`y`属性:\n\n```cpp\n    NumberAnimation {\n        target: tux\n        property: \"y\"\n        from: startY\n        to: endY\n        duration: speed\n        easing.type: Easing.InOutQuad\n    }\n}\n```\n\n8.  之后，我们重复`ParallelAnimation`的整个代码，除了这次，我们交换`from`和`to`的值，像这样:\n\n```cpp\nParallelAnimation {\n    NumberAnimation {\n        target: tux\n        property: \"x\"\n from: endX\n to: startX\n        duration: speed\n        easing.type: Easing.InOutQuad\n    }\n```\n\n9.  `y`属性的`NumberAnimation`也是如此:\n\n```cpp\n    NumberAnimation {\n        target: tux\n        property: \"y\"\n from: endY\n to: startY\n        duration: speed\n        easing.type: Easing.InOutQuad\n    }\n}\n```\n\n10.  然后，我们添加一个`Text`项来显示我们的应用的帧速率:\n\n```cpp\nText {\n    property int frame: 0\n    color: \"red\"\n    text: \"FPS: 0 fps\"\n    x: 20\n    y: 20\n    font.pointSize: 20\n```\n\n11.  让我们在`Text`下增加`Timer`，每秒更新一次帧率显示:\n\n```cpp\n    Timer {\n        id: fpsTimer\n        repeat: true\n        interval: 1000\n        running: true\n        onTriggered: {\n            parent.text = \"FPS: \" + frame + \" fps\"\n        }\n    }\n}\n```\n\n12.  如果我们现在构建并运行该程序，我们将能够看到几只企鹅以稳定的 60 fps 在屏幕上移动:\n\n![](img/69757823-3921-41b0-a846-590b91699689.png)\n\n13.  让我们返回代码，将`Repeater`项的`model`属性更改为`10000` **。**再次构建并运行程序；您应该会看到，您的窗口中充满了移动的企鹅，并且帧速率显著下降到大约 29 fps，考虑到我们拥有的企鹅数量，这还不算太坏:\n\n![](img/6eab277a-31b1-4674-b592-901a2efbbac9.png)\n\n14.  然后，回到我们的源代码，注释掉两个`sourceSize`属性。我们还将`smooth`和`antialiasing`属性设置为`false`，同时将`asynchronous`属性设置为`false`:\n\n```cpp\nImage {\n    id: tux\n    source: \"tux.png\"\n //sourceSize.width: 50\n //sourceSize.height: 60\n    width: 50\n    height: 60\n    smooth: true\n antialiasing: true\n asynchronous: false\n```\n\n15.  让我们再次构建并运行该程序。这一次，帧速率略微下降到 22 fps，但企鹅看起来更平滑，质量更好，即使在移动时也是如此:\n\n![](img/de9f4510-25d0-42a0-96a8-393d2b6b9d8f.png)\n\n# 它是如何工作的...\n\n当在屏幕上渲染动画图形时，为 Qt Quick 应用提供动力的 QML 引擎是非常优化和强大的。然而，我们仍然可以遵循一些技巧来加快速度。\n\n尽量利用 Qt 5 提供的内置功能，而不是自己实现，比如`Repeater`、`NumberAnimation`、`SequentialAnimation`。这是因为 Qt 5 开发人员在优化这些特性方面付出了巨大的努力，所以您不必这样做。\n\n`sourceSize`属性告诉 Qt 在将图像加载到内存之前调整图像的大小，这样大图像就不会占用过多的内存。\n\n当`smooth`属性被启用时，它告诉 Qt 过滤图像，使其在缩放或从自然大小转换时看起来更平滑。如果图像以与其`sourceSize`相同的速度渲染，不会有任何区别。此属性将影响您的应用在一些旧硬件上的性能。\n\n`antialiasing`属性告诉 Qt 去除图像边缘周围的混叠伪影，使其看起来更平滑。该属性还会影响程序的性能。\n\n`asynchronous`属性告诉 Qt 在低优先级线程下加载图像，这意味着你的程序在加载一个巨大的图像文件时不会停滞。\n\n我们使用帧速率来表示程序的性能。因为`onAfterRendering`总是在每一帧被调用，所以我们可以在每一个渲染中积累`frame`变量。然后，我们使用`Timer`每秒重置`frame`值。最后，我们使用`Text`项目在屏幕上显示该值。"
  },
  {
    "path": "docs/qt5-cpp-gui-prog-cb/README.md",
    "content": "# Qt5 C++ GUI 编程秘籍\n\n> 原书：[Qt5 C++ GUI Programming Cookbook](https://libgen.rs/book/index.php?md5=9BC2D959B55E8629DCD159B600A4BD90)\n> \n> 协议：[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)\n> \n> 阶段：机翻（1）\n> \n> 自豪地采用[谷歌翻译](https://translate.google.cn/)\n> \n> 让开源界提前感受入关。\n\n* [在线阅读](https://ccpp.apachecn.org)\n* [在线阅读（Gitee）](https://apachecn.gitee.io/apachecn-c-cpp-zh/)\n* [ApacheCN 学习资源](http://docs.apachecn.org/)\n\n## 贡献指南\n\n本项目需要校对，欢迎大家提交 Pull Request。\n\n> 请您勇敢地去翻译和改进翻译。虽然我们追求卓越，但我们并不要求您做到十全十美，因此请不要担心因为翻译上犯错——在大部分情况下，我们的服务器已经记录所有的翻译，因此您不必担心会因为您的失误遭到无法挽回的破坏。（改编自维基百科）\n\n## 联系方式\n\n### 负责人\n\n* [飞龙](https://github.com/wizardforcel): 562826179\n\n### 其他\n\n*   在我们的 [apachecn/apachecn-c-cpp-zh](https://github.com/apachecn/apachecn-c-cpp-zh) github 上提 issue.\n*   发邮件到 Email: `apachecn@163.com`.\n*   在我们的 [组织学习交流群](http://www.apachecn.org/organization/348.html) 中联系群主/管理员即可.\n\n## 赞助我们\n\n![](http://data.apachecn.org/img/about/donate.jpg)\n"
  },
  {
    "path": "docs/qt5-cpp-gui-prog-cb/SUMMARY.md",
    "content": "+   [Qt5 C++ GUI 编程秘籍](README.md)\n+   [零、前言](00.md)\n+   [一、将 Qt 设计器用于外观定制](01.md)\n+   [二、事件处理——信号和插槽](02.md)\n+   [三、Qt 和 QML 的状态和动画](03.md)\n+   [四、画家与 2D 图形](04.md)\n+   [五、OpenGL 实现](05.md)\n+   [六、使用网络和管理大型文档](06.md)\n+   [七、线程基础——异步编程](07.md)\n+   [八、使用 Qt5 构建触摸屏应用](08.md)\n+   [九、XML 解析的简化](09.md)\n+   [十、转换库](10.md)\n+   [十一、使用 SQL 驱动和 Qt 访问数据库](11.md)\n+   [十二、使用 Qt 网络引擎开发网络应用](12.md)\n+   [十三、性能优化](13.md)\n"
  },
  {
    "path": "index.html",
    "content": "<!-- index.html -->\n\n<!DOCTYPE html>\n<html>\n<head>\n  <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge,chrome=1\">\n  <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n  <meta charset=\"UTF-8\">\n  <meta name=\"referrer\" content=\"never\">\n  <link rel=\"stylesheet\" href=\"asset/vue.css\">\n  <link rel=\"stylesheet\" href=\"asset/style.css\">\n  <link rel=\"stylesheet\" href=\"asset/prism-darcula.css\">\n  \n  <!-- google ads -->\n  <script async src=\"//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js\"></script>\n    \n  <!-- google webmaster -->\n  <meta name=\"google-site-verification\" content=\"pyo9N70ZWyh8JB43bIu633mhxesJ1IcwWCZlM3jUfFo\" />\n\n  <link rel=\"stylesheet\" href=\"asset/dark-mode.css\">\n  <script src=\"asset/dark-mode.js\"></script>\n  <link rel=\"stylesheet\" href=\"asset/share.css\">\n  <script src=\"asset/share.js\"></script>\n  <link rel=\"stylesheet\" href=\"asset/edit.css\">\n  <script src=\"asset/edit.js\"></script>\n  <link rel=\"stylesheet\" href=\"asset/back-to-top.css\">\n  <script src=\"asset/back-to-top.js\"></script>\n  <link rel=\"stylesheet\" href=\"asset/sidebar.min.css\">\n</head>\n<body>\n  <div id=\"app\">now loading...</div>\n  <script>\n    window.$docsify = {\n      loadNavbar: 'NAV.md',\n      loadSidebar: 'SUMMARY.md',\n      name: 'ApacheCN C/C++ 译文集',\n      auto2top: true,\n      themeColor: '#004eb7',\n      repo: 'apachecn/apachecn-c-cpp-zh',\n      plugins: [window.docsPlugin],\n      alias: {\n        '/.*/SUMMARY.md': '/SUMMARY.md',\n      },\n      bdStatId: '38525fdac4b5d4403900b943d4e7dd91',\n      cnzzId: '1275211409',\n      search: {\n        paths: 'auto',\n        placeholder: '搜索',\n        noData: '没有结果',\n      },\n      copyCode: {\n        buttonText: '复制',\n        errorText: 'Error',\n        successText: 'OK!',\n      },\n    }\n  </script>\n  \n  <script src=\"asset/docsify-katex.js\"></script>\n  <link rel=\"stylesheet\" href=\"asset/katex.min.css\"/>\n  <script src=\"asset/docsify.min.js\"></script>\n  <script src=\"asset/search.min.js\"></script>\n  <script src=\"asset/prism-java.min.js\"></script>\n  <script src=\"asset/prism-c.min.js\"></script>\n  <script src=\"asset/prism-cpp.min.js\"></script>\n  <script src=\"asset/prism-csharp.min.js\"></script>\n  <script src=\"asset/prism-javascript.min.js\"></script>\n  <script src=\"asset/prism-python.min.js\"></script>\n  <script src=\"asset/prism-php.min.js\"></script>\n  <script src=\"asset/docsify-copy-code.min.js\"></script>\n  <script src=\"asset/docsify-baidu-push.js\"></script>\n  <script src=\"asset/docsify-baidu-stat.js\"></script>\n  <script src=\"asset/docsify-cnzz.js\"></script>\n  <script src=\"asset/docsify-apachecn-footer.js\"></script>\n  <script src=\"asset/docsify-clicker.js\"></script>\n  <link rel=\"stylesheet\" href=\"asset/docsify-quick-page.css\">\n  <script src=\"asset/docsify-quick-page.js\"></script>\n  <script src=\"asset/docsify-sidebar-collapse.min.js\"></script>\n</body>\n</html>"
  },
  {
    "path": "update.sh",
    "content": "git add -A\ngit commit -am \"$(date \"+%Y-%m-%d %H:%M:%S\")\"\ngit push"
  }
]